diff --git a/server/entity/fall.go b/server/entity/fall.go new file mode 100644 index 000000000..a2ae5e80f --- /dev/null +++ b/server/entity/fall.go @@ -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{}) + } +} diff --git a/server/entity/falling_block.go b/server/entity/falling_block.go index b491df6db..fafc68a6e 100644 --- a/server/entity/falling_block.go +++ b/server/entity/falling_block.go @@ -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 } diff --git a/server/entity/living.go b/server/entity/living.go index 9895149b8..cc5a72521 100644 --- a/server/entity/living.go +++ b/server/entity/living.go @@ -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 diff --git a/server/player/player.go b/server/player/player.go index 678c4fe8a..f65c9d105 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -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(), } }