Skip to content
Open
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
Expand Up @@ -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"
Expand All @@ -17,8 +16,8 @@ import (
type FallingBlock struct {
transform

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

c *MovementComputer
}
Expand All @@ -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
}
Expand All @@ -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 ...
Expand All @@ -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()
Expand All @@ -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] {
Expand All @@ -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})
Expand Down Expand Up @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions server/entity/living.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 8 additions & 46 deletions server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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(),
}
}
Expand Down