diff --git a/server/entity/arrow.go b/server/entity/arrow.go index 23714f947..134797bd9 100644 --- a/server/entity/arrow.go +++ b/server/entity/arrow.go @@ -1,6 +1,8 @@ package entity import ( + "math/rand/v2" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" @@ -41,10 +43,26 @@ var arrowConf = ProjectileBehaviourConfig{ Gravity: 0.05, Drag: 0.01, Damage: 2.0, + damageCalculator: calculateArrowDamage, Sound: sound.ArrowHit{}, SurviveBlockCollision: true, } +// calculateArrowDamage calculates arrow damage using Bedrock Edition's formula. Arrows +// add 97% of their velocity to their base damage. Critical arrows deal 9-10 +// damage before Power is applied. +func calculateArrowDamage(baseDamage, velocity float64, critical bool, powerLevel int) float64 { + damage := baseDamage + velocity*0.97 + if critical { + damage += rand.Float64()*damage/2 + rand.Float64()*2 + damage = min(10, max(9, damage)) + } + if powerLevel > 0 { + damage *= 1 + float64(powerLevel+1)*0.25 + } + return damage +} + // boolByte returns 1 if the bool passed is true, or 0 if it is false. func boolByte(b bool) uint8 { if b { @@ -70,6 +88,8 @@ func (arrowType) BBox(world.Entity) cube.BBox { func (arrowType) DecodeNBT(m map[string]any, data *world.EntityData) { conf := arrowConf conf.Damage = float64(nbtconv.Float32(m, "Damage")) + conf.powerLevel = int(nbtconv.Uint8(m, "enchantPower")) + conf.Critical = nbtconv.Bool(m, "crit") conf.Potion = potion.From(nbtconv.Int32(m, "auxValue") - 1) conf.DisablePickup = !nbtconv.Bool(m, "player") if !nbtconv.Bool(m, "isCreative") { @@ -85,12 +105,13 @@ func (arrowType) EncodeNBT(data *world.EntityData) map[string]any { b := data.Data.(*ProjectileBehaviour) m := map[string]any{ "Damage": float32(b.conf.Damage), + "crit": boolByte(b.conf.Critical), "enchantPunch": byte(b.conf.KnockBackForceAddend / enchantment.Punch.KnockBackMultiplier()), "auxValue": int32(b.conf.Potion.Uint8() + 1), "player": boolByte(!b.conf.DisablePickup), "isCreative": boolByte(b.conf.PickupItem.Empty()), } - // TODO: Save critical flag if Minecraft ever saves it? + m["enchantPower"] = byte(b.conf.powerLevel) if b.collided { m["StuckToBlockPos"] = nbtconv.PosToInt32Slice(b.collisionPos) } diff --git a/server/entity/projectile.go b/server/entity/projectile.go index 1d9be719b..f2a2722fb 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -32,6 +32,10 @@ type ProjectileBehaviourConfig struct { // back. The base damage is multiplied with the velocity of the projectile // to calculate the final damage of the projectile. Damage float64 + // damageCalculator calculates the final damage of the projectile. If nil, + // Damage is multiplied by the projectile's velocity. + damageCalculator func(baseDamage, velocity float64, critical bool, powerLevel int) float64 + powerLevel int // Potion is the potion effect that is applied to an entity when the // projectile hits it. Potion potion.Potion @@ -293,9 +297,14 @@ func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m func (lt *ProjectileBehaviour) hitEntity(victim world.Entity, e *Ent, vel mgl64.Vec3) { owner, _ := lt.conf.Owner.Entity(e.tx) src := ProjectileDamageSource{Projectile: e, Owner: owner} - dmg := math.Ceil(lt.conf.Damage * vel.Len()) - if lt.conf.Critical { - dmg += rand.Float64() * dmg / 2 + var dmg float64 + if lt.conf.damageCalculator == nil { + dmg = math.Ceil(lt.conf.Damage * vel.Len()) + if lt.conf.Critical { + dmg += rand.Float64() * dmg / 2 + } + } else { + dmg = lt.conf.damageCalculator(lt.conf.Damage, vel.Len(), lt.conf.Critical, lt.conf.powerLevel) } // TODO: Piercing arrows should bypass shield blocking when shields are implemented. if _, vulnerable, ok := HurtEntity(victim, dmg, src); ok && vulnerable { diff --git a/server/entity/register.go b/server/entity/register.go index b4f444014..62d7d1fb4 100644 --- a/server/entity/register.go +++ b/server/entity/register.go @@ -53,6 +53,7 @@ var conf = world.EntityRegistryConfig{ tip := arrow.Tip.(potion.Potion) conf := arrowConf conf.Damage, conf.Potion, conf.Owner = arrow.Damage, tip, arrow.Owner.H() + conf.powerLevel = arrow.PowerLevel conf.KnockBackForceAddend = float64(arrow.PunchLevel) * enchantment.Punch.KnockBackMultiplier() conf.DisablePickup = arrow.DisablePickup if arrow.ObtainArrowOnPickup { diff --git a/server/item/bow.go b/server/item/bow.go index 30ad644a6..41dcdf080 100644 --- a/server/item/bow.go +++ b/server/item/bow.go @@ -61,7 +61,7 @@ func (Bow) Release(releaser Releaser, tx *world.Tx, ctx *UseContext, duration ti } held, _ := releaser.HeldItems() - damage, punchLevel, burnDuration, consume := 2.0, 0, time.Duration(0), !creative + powerLevel, punchLevel, burnDuration, consume := 0, 0, time.Duration(0), !creative for _, enchant := range held.Enchantments() { if f, ok := enchant.Type().(interface{ BurnDuration() time.Duration }); ok { burnDuration = f.BurnDuration() @@ -69,8 +69,8 @@ func (Bow) Release(releaser Releaser, tx *world.Tx, ctx *UseContext, duration ti if _, ok := enchant.Type().(interface{ KnockBackMultiplier() float64 }); ok { punchLevel = enchant.Level() } - if p, ok := enchant.Type().(interface{ PowerDamage(int) float64 }); ok { - damage += p.PowerDamage(enchant.Level()) + if _, ok := enchant.Type().(interface{ PowerDamage(int) float64 }); ok { + powerLevel = enchant.Level() } if i, ok := enchant.Type().(interface{ ConsumesArrows() bool }); ok && !i.ConsumesArrows() { consume = false @@ -84,7 +84,8 @@ func (Bow) Release(releaser Releaser, tx *world.Tx, ctx *UseContext, duration ti Rotation: releaser.Rotation().Neg(), } projectile := tx.AddEntity(create(opts, world.ArrowSpawnConfig{ - Damage: damage, + Damage: 1, + PowerLevel: powerLevel, Owner: releaser, Critical: force >= 1, ObtainArrowOnPickup: !creative && consume, diff --git a/server/item/crossbow.go b/server/item/crossbow.go index 1e173ab2c..fef3367e1 100644 --- a/server/item/crossbow.go +++ b/server/item/crossbow.go @@ -133,7 +133,7 @@ func (c Crossbow) ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext } arrowConf := world.ArrowSpawnConfig{ - Damage: 9, + Damage: 1, Owner: releaser, Critical: true, ObtainArrowOnPickup: !creative, @@ -179,7 +179,7 @@ func (c Crossbow) shoot(releaser Releaser, tx *world.Tx, offsetAngle float64, ar arrowConf.Tip = c.Item.Item().(Arrow).Tip arrow := createArrow(world.EntitySpawnOpts{ Position: torsoPosition(releaser), - Velocity: dirVec.Mul(5.15), + Velocity: dirVec.Mul(3.15), Rotation: rot.Neg(), }, arrowConf) tx.AddEntity(arrow) diff --git a/server/world/entity.go b/server/world/entity.go index fd890b27d..eb08bea70 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -509,6 +509,8 @@ type EntityRegistryConfig struct { type ArrowSpawnConfig struct { // Damage specifies the base damage dealt by the arrow. Damage float64 + // PowerLevel specifies the level of the Power enchantment applied to the arrow. + PowerLevel int // Owner is the entity that fired the arrow. Owner Entity // Critical specifies if the arrow should deal critical damage.