Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion server/entity/arrow.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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") {
Expand All @@ -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)
}
Expand Down
15 changes: 12 additions & 3 deletions server/entity/projectile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions server/entity/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 5 additions & 4 deletions server/item/bow.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,16 @@ 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()
}
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
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions server/item/crossbow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions server/world/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down