Skip to content

Added a fall manager for entities #549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
91 changes: 91 additions & 0 deletions server/entity/fall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package entity

import (
"github.com/df-mc/atomic"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/entity/damage"
"github.com/df-mc/dragonfly/server/entity/effect"
"github.com/df-mc/dragonfly/server/world"
"math"
)

// FallManager handles entities that can fall.
type FallManager struct {
e fallEntity
fallDistance atomic.Float64
}

// fallEntity is an entity that can fall.
type fallEntity interface {
world.Entity
OnGround() bool
}

// entityLander represents a block that reacts to an entity landing on it after falling.
type entityLander interface {
// EntityLand is called when an entity lands on the block.
EntityLand(pos cube.Pos, w *world.World, e world.Entity)
}

// NewFallManager returns a new fall manager.
func NewFallManager(e fallEntity) *FallManager {
return &FallManager{e: e}
}

// SetFallDistance sets the fall distance of the entity.
func (f *FallManager) SetFallDistance(distance float64) {
f.fallDistance.Store(distance)
}

// FallDistance returns the entity's fall distance.
func (f *FallManager) FallDistance() float64 {
return f.fallDistance.Load()
}

// ResetFallDistance resets the player's fall distance.
func (f *FallManager) ResetFallDistance() {
f.fallDistance.Store(0)
}

// UpdateFallState is called to update the entities falling state.
func (f *FallManager) UpdateFallState(distanceThisTick float64, checkOnGround bool) {
fallDistance := f.fallDistance.Load()
if checkOnGround && f.e.OnGround() {
if fallDistance > 0 {
f.fall(fallDistance)
f.ResetFallDistance()
}
} else if distanceThisTick < fallDistance {
f.fallDistance.Sub(distanceThisTick)
} else {
f.ResetFallDistance()
}
}

// fall is called when a falling entity hits the ground. It handles the landing, calling EntityLand if needed,
// and applying fall damage to living entities.
func (f *FallManager) fall(distance float64) {
var (
w = f.e.World()
pos = cube.PosFromVec3(f.e.Position())
b = w.Block(pos)
dmg = distance - 3
)
if len(b.Model().BBox(pos, w)) == 0 {
pos = pos.Sub(cube.Pos{0, 1})
b = w.Block(pos)
}
if h, ok := b.(entityLander); ok {
h.EntityLand(pos, w, f.e)
}

if p, ok := f.e.(Living); ok {
if boost, ok := p.Effect(effect.JumpBoost{}); ok {
dmg -= float64(boost.Level())
}
if dmg < 0.5 {
return
}
p.Hurt(math.Ceil(dmg), damage.SourceFall{})
}
}
24 changes: 12 additions & 12 deletions server/entity/falling_block.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package entity

import (
"fmt"
"github.com/df-mc/atomic"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/entity/damage"
"github.com/df-mc/dragonfly/server/internal/nbtconv"
@@ -17,8 +16,8 @@ import (
type FallingBlock struct {
transform

block world.Block
fallDistance atomic.Float64
block world.Block
fall *FallManager

c *MovementComputer
}
@@ -33,6 +32,7 @@ func NewFallingBlock(block world.Block, pos mgl64.Vec3) *FallingBlock {
DragBeforeGravity: true,
},
}
b.fall = NewFallManager(b)
b.transform = newTransform(b, pos)
return b
}
@@ -59,7 +59,7 @@ func (f *FallingBlock) Block() world.Block {

// FallDistance ...
func (f *FallingBlock) FallDistance() float64 {
return f.fallDistance.Load()
return f.fall.FallDistance()
}

// damager ...
@@ -77,6 +77,11 @@ type landable interface {
Landed(w *world.World, pos cube.Pos)
}

// OnGround ...
func (f *FallingBlock) OnGround() bool {
return f.c.OnGround()
}

// Tick ...
func (f *FallingBlock) Tick(w *world.World, _ int64) {
f.mu.Lock()
@@ -86,12 +91,7 @@ func (f *FallingBlock) Tick(w *world.World, _ int64) {

m.Send()

distThisTick := f.vel.Y()
if distThisTick < f.fallDistance.Load() {
f.fallDistance.Sub(distThisTick)
} else {
f.fallDistance.Store(0)
}
f.fall.UpdateFallState(f.vel.Y(), false)

pos := cube.PosFromVec3(m.pos)
if pos[1] < w.Range()[0] {
@@ -101,7 +101,7 @@ func (f *FallingBlock) Tick(w *world.World, _ int64) {
if a, ok := f.block.(Solidifiable); (ok && a.Solidifies(pos, w)) || f.c.OnGround() {
if d, ok := f.block.(damager); ok {
damagePerBlock, maxDamage := d.Damage()
if dist := math.Ceil(f.fallDistance.Load() - 1.0); dist > 0 {
if dist := math.Ceil(f.FallDistance() - 1.0); dist > 0 {
force := math.Min(math.Floor(dist*damagePerBlock), maxDamage)
for _, e := range w.EntitiesWithin(f.BBox().Translate(m.pos).Grow(0.05), f.ignores) {
e.(Living).Hurt(force, damage.SourceBlock{Block: f.block})
@@ -137,7 +137,7 @@ func (f *FallingBlock) DecodeNBT(data map[string]any) any {
}
n := NewFallingBlock(b, nbtconv.MapVec3(data, "Pos"))
n.SetVelocity(nbtconv.MapVec3(data, "Motion"))
n.fallDistance.Store(nbtconv.Map[float64](data, "FallDistance"))
n.fall.SetFallDistance(nbtconv.Map[float64](data, "FallDistance"))
return n
}

3 changes: 3 additions & 0 deletions server/entity/living.go
Original file line number Diff line number Diff line change
@@ -45,6 +45,9 @@ type Living interface {
AddEffect(e effect.Effect)
// RemoveEffect removes any effect that might currently be active on the entity.
RemoveEffect(e effect.Type)
// Effect returns the effect instance and true if the Player has the effect. If not found, it will return an empty
// effect instance and false.
Effect(e effect.Type) (effect.Effect, bool)
// Effects returns any effect currently applied to the entity. The returned effects are guaranteed not to have
// expired when returned.
Effects() []effect.Effect
54 changes: 8 additions & 46 deletions server/player/player.go
Original file line number Diff line number Diff line change
@@ -67,8 +67,7 @@ type Player struct {
invisible, immobile, onGround, usingItem atomic.Bool
usingSince atomic.Int64

fireTicks atomic.Int64
fallDistance atomic.Float64
fireTicks atomic.Int64

breathing bool
airSupplyTicks atomic.Int64
@@ -83,6 +82,7 @@ type Player struct {
health *entity.HealthManager
experience *entity.ExperienceManager
effects *entity.EffectManager
fall *entity.FallManager

lastXPPickup atomic.Value[time.Time]
immunity atomic.Value[time.Time]
@@ -116,6 +116,7 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player {
health: entity.NewHealthManager(),
experience: entity.NewExperienceManager(),
effects: entity.NewEffectManager(),
fall: entity.NewFallManager(p),
gameMode: *atomic.NewValue[world.GameMode](world.GameModeSurvival),
h: *atomic.NewValue[Handler](NopHandler{}),
name: name,
@@ -262,12 +263,12 @@ func (p *Player) SendToast(title, message string) {

// ResetFallDistance resets the player's fall distance.
func (p *Player) ResetFallDistance() {
p.fallDistance.Store(0)
p.fall.ResetFallDistance()
}

// FallDistance returns the player's fall distance.
func (p *Player) FallDistance() float64 {
return p.fallDistance.Load()
return p.fall.FallDistance()
}

// SendTitle sends a title to the player. The title may be configured to change the duration it is displayed
@@ -473,45 +474,6 @@ func (p *Player) Heal(health float64, source healing.Source) {
p.addHealth(health)
}

// updateFallState is called to update the entities falling state.
func (p *Player) updateFallState(distanceThisTick float64) {
fallDistance := p.fallDistance.Load()
if p.OnGround() {
if fallDistance > 0 {
p.fall(fallDistance)
p.ResetFallDistance()
}
} else if distanceThisTick < fallDistance {
p.fallDistance.Sub(distanceThisTick)
} else {
p.ResetFallDistance()
}
}

// fall is called when a falling entity hits the ground.
func (p *Player) fall(distance float64) {
var (
w = p.World()
pos = cube.PosFromVec3(p.Position())
b = w.Block(pos)
)
if len(b.Model().BBox(pos, w)) == 0 {
pos = pos.Sub(cube.Pos{0, 1})
b = w.Block(pos)
}
if h, ok := b.(block.EntityLander); ok {
h.EntityLand(pos, w, p, &distance)
}
dmg := distance - 3
if boost, ok := p.Effect(effect.JumpBoost{}); ok {
dmg -= float64(boost.Level())
}
if dmg < 0.5 {
return
}
p.Hurt(math.Ceil(dmg), damage.SourceFall{})
}

// Hurt hurts the player for a given amount of damage. The source passed represents the cause of the damage,
// for example damage.SourceEntityAttack if the player is attacked by another entity.
// If the final damage exceeds the health that the player currently has, the player is killed and will have to
@@ -1899,7 +1861,7 @@ func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) {
p.checkBlockCollisions(w)
p.onGround.Store(p.checkOnGround(w))

p.updateFallState(deltaPos[1])
p.fall.UpdateFallState(deltaPos[1], true)

// The vertical axis isn't relevant for calculation of exhaustion points.
deltaPos[1] = 0
@@ -2592,7 +2554,7 @@ func (p *Player) load(data Data) {
p.AddEffect(potion)
}
p.fireTicks.Store(data.FireTicks)
p.fallDistance.Store(data.FallDistance)
p.fall.SetFallDistance(data.FallDistance)

p.loadInventory(data.Inventory)
}
@@ -2643,7 +2605,7 @@ func (p *Player) Data() Data {
},
Effects: p.Effects(),
FireTicks: p.fireTicks.Load(),
FallDistance: p.fallDistance.Load(),
FallDistance: p.fall.FallDistance(),
Dimension: p.World().Dimension().EncodeDimension(),
}
}