Skip to content

Commit

Permalink
item/crossbow.go: Cleaned up code.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sandertv committed Jan 4, 2025
1 parent 613899c commit deeaac4
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 112 deletions.
8 changes: 6 additions & 2 deletions server/entity/firework.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import (
// for creating decorative explosions, boosting when flying with elytra, and
// loading into a crossbow as ammunition.
func NewFirework(opts world.EntitySpawnOpts, firework item.Firework) *world.EntityHandle {
return NewFireworkAttached(opts, firework, nil, 1.15, 0.04, false)
return newFirework(opts, firework, nil, 1.15, 0.04, false)
}

// NewFireworkAttached creates a firework entity with an owner that the firework
// may be attached to.
func NewFireworkAttached(opts world.EntitySpawnOpts, firework item.Firework, owner world.Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *world.EntityHandle {
func NewFireworkAttached(opts world.EntitySpawnOpts, firework item.Firework, owner world.Entity) *world.EntityHandle {
return newFirework(opts, firework, owner, 0, 0, true)
}

func newFirework(opts world.EntitySpawnOpts, firework item.Firework, owner world.Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *world.EntityHandle {
conf := fireworkConf
conf.SidewaysVelocityMultiplier = sidewaysVelocityMultiplier
conf.UpwardsAcceleration = upwardsAcceleration
Expand Down
2 changes: 1 addition & 1 deletion server/entity/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var conf = world.EntityRegistryConfig{
FallingBlock: NewFallingBlock,
Lightning: NewLightning,
Firework: func(opts world.EntitySpawnOpts, firework world.Item, owner world.Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *world.EntityHandle {
return NewFireworkAttached(opts, firework.(item.Firework), owner, sidewaysVelocityMultiplier, upwardsAcceleration, attached)
return newFirework(opts, firework.(item.Firework), owner, sidewaysVelocityMultiplier, upwardsAcceleration, attached)
},
Item: func(opts world.EntitySpawnOpts, it any) *world.EntityHandle {
return NewItem(opts, it.(item.Stack))
Expand Down
145 changes: 59 additions & 86 deletions server/item/crossbow.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,64 +8,36 @@ import (
"github.com/df-mc/dragonfly/server/world/sound"
)

// Crossbow is a ranged weapon similar to a bow that uses arrows or fireworks as ammunition.
// Crossbow is a ranged weapon similar to a bow that uses arrows or fireworks
// as ammunition.
type Crossbow struct {
// Item is the item the crossbow is charged with.
Item Stack
}

// Charge starts the charging process and checks if the charge duration meets the required duration.
func (c Crossbow) Charge(releaser Releaser, tx *world.Tx, ctx *UseContext, duration time.Duration) bool {
// Charge starts the charging process and checks if the charge duration meets
// the required duration.
func (c Crossbow) Charge(releaser Releaser, _ *world.Tx, ctx *UseContext, duration time.Duration) bool {
if !c.Item.Empty() {
return false
}

creative := releaser.GameMode().CreativeInventory()
held, left := releaser.HeldItems()

chargeDuration := time.Duration(1.25 * float64(time.Second))
for _, enchant := range held.Enchantments() {
if q, ok := enchant.Type().(interface{ ChargeDuration(int) time.Duration }); ok {
chargeDuration = min(chargeDuration, q.ChargeDuration(enchant.Level()))
}
}

if duration < chargeDuration {
if chargeDuration, _ := c.chargeDuration(held); duration < chargeDuration {
return false
}

var projectileItem Stack
if !left.Empty() {
_, isFirework := left.Item().(Firework)
_, isArrow := left.Item().(Arrow)
if isFirework || isArrow {
projectileItem = left
}
}

if projectileItem.Empty() {
var ok bool
projectileItem, ok = ctx.FirstFunc(func(stack Stack) bool {
_, isArrow := stack.Item().(Arrow)
return isArrow
})

if !ok && !creative {
return false
}

if projectileItem.Empty() {
projectileItem = NewStack(Arrow{}, 1)
}
projectileItem, ok := c.findProjectile(releaser, ctx)
if !ok {
return false
}

c.Item = projectileItem.Grow(-projectileItem.Count() + 1)
if !creative {
ctx.Consume(c.Item)
}

crossbow := held.WithItem(c)
releaser.SetHeldItems(crossbow, left)
releaser.SetHeldItems(held.WithItem(c), left)
return true
}

Expand All @@ -75,66 +47,69 @@ func (c Crossbow) ContinueCharge(releaser Releaser, tx *world.Tx, ctx *UseContex
return
}

creative := releaser.GameMode().CreativeInventory()
held, left := releaser.HeldItems()

chargeDuration, quickChargeLevel := time.Duration(1.25*float64(time.Second)), 0
for _, enchant := range held.Enchantments() {
if q, ok := enchant.Type().(interface{ ChargeDuration(int) time.Duration }); ok {
chargeDuration = min(chargeDuration, q.ChargeDuration(enchant.Level()))
quickChargeLevel = enchant.Level()
}
}

var projectileItem Stack
if !left.Empty() {
_, isFirework := left.Item().(Firework)
_, isArrow := left.Item().(Arrow)
if isFirework || isArrow {
projectileItem = left
}
}

if projectileItem.Empty() {
var ok bool
projectileItem, ok = ctx.FirstFunc(func(stack Stack) bool {
_, isArrow := stack.Item().(Arrow)
return isArrow
})

if !ok && !creative {
return
}

if projectileItem.Empty() {
projectileItem = NewStack(Arrow{}, 1)
}
}

if projectileItem.Empty() {
held, _ := releaser.HeldItems()
if _, ok := c.findProjectile(releaser, ctx); !ok {
return
}

hasQuickCharge := quickChargeLevel > 0
progress := float64(duration) / float64(chargeDuration)
chargeDuration, qcLevel := c.chargeDuration(held)
if duration.Seconds() <= 0.1 {
tx.PlaySound(releaser.Position(), sound.Crossbow{Stage: sound.CrossbowStageLoadStart, QuickCharge: hasQuickCharge})
tx.PlaySound(releaser.Position(), sound.CrossbowLoad{Stage: sound.CrossbowLoadingStart, QuickCharge: qcLevel > 0})
}

// Base reload time is 25 ticks; each Quick Charge level reduces by 5 ticks
multiplier := 25.0 / float64(25-(5*quickChargeLevel))
multiplier := 25.0 / float64(25-(5*qcLevel))

// Adjust ticks based on the multiplier
adjustedTicks := int(float64(duration.Milliseconds()) / (50 / multiplier))

// Play sound after every 16 ticks (adjusted by Quick Charge)
if adjustedTicks%16 == 0 {
tx.PlaySound(releaser.Position(), sound.Crossbow{Stage: sound.CrossbowStageMiddle, QuickCharge: hasQuickCharge})
tx.PlaySound(releaser.Position(), sound.CrossbowLoad{Stage: sound.CrossbowLoadingMiddle, QuickCharge: qcLevel > 0})
}

if progress >= 1 && quickChargeLevel > 0 {
tx.PlaySound(releaser.Position(), sound.Crossbow{Stage: sound.CrossbowStageLoadEnd, QuickCharge: hasQuickCharge})
if progress := float64(duration) / float64(chargeDuration); progress >= 1 {
tx.PlaySound(releaser.Position(), sound.CrossbowLoad{Stage: sound.CrossbowLoadingEnd, QuickCharge: qcLevel > 0})
}
}

// chargeDuration calculates the duration required to charge the crossbow and
// the quick charge enchantment level, if any.
func (c Crossbow) chargeDuration(s Stack) (dur time.Duration, quickChargeLvl int) {
dur, lvl := time.Duration(1.25*float64(time.Second)), 0
for _, enchant := range s.Enchantments() {
if q, ok := enchant.Type().(interface{ ChargeDuration(int) time.Duration }); ok {
dur = min(dur, q.ChargeDuration(enchant.Level()))
lvl = enchant.Level()
}
}
return dur, lvl
}

// findProjectile looks through the inventory of a Releaser to find a projectile
// to insert into the crossbow. It first checks the left hand for fireworks or
// arrows, and searches the rest of the inventory for arrows if no valid
// projectile was in the left hand. False is returned if no valid projectile was
// anywhere in the inventory.
func (c Crossbow) findProjectile(r Releaser, ctx *UseContext) (Stack, bool) {
_, left := r.HeldItems()
_, isFirework := left.Item().(Firework)
_, isArrow := left.Item().(Arrow)
if isFirework || isArrow {
return left, true
}
if res, ok := ctx.FirstFunc(func(stack Stack) bool {
_, ok := stack.Item().(Arrow)
return ok
}); ok {
return res, true
}
if r.GameMode().CreativeInventory() {
// No projectiles in inventory but the player is in creative mode, so
// return an arrow anyway.
return NewStack(Arrow{}, 1), true
}
return Stack{}, false
}

// ReleaseCharge checks if the item is fully charged and, if so, releases it.
Expand Down Expand Up @@ -171,7 +146,7 @@ func (c Crossbow) ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext
held, left := releaser.HeldItems()
crossbow := held.WithItem(c)
releaser.SetHeldItems(crossbow, left)
tx.PlaySound(releaser.Position(), sound.Crossbow{Stage: sound.CrossbowStageShoot})
tx.PlaySound(releaser.Position(), sound.CrossbowShoot{})
return true
}

Expand Down Expand Up @@ -212,9 +187,7 @@ func (c Crossbow) DecodeNBT(data map[string]any) any {
// EncodeNBT ...
func (c Crossbow) EncodeNBT() map[string]any {
if !c.Item.Empty() {
return map[string]any{
"chargedItem": writeItem(c.Item, true),
}
return map[string]any{"chargedItem": writeItem(c.Item, true)}
}
return nil
}
Expand Down
11 changes: 4 additions & 7 deletions server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -1375,11 +1375,11 @@ func (p *Player) UseItem() {
p.usingSince, p.usingItem = time.Now(), true
p.updateState()
}

if chargeable, ok := it.(item.Chargeable); ok {
switch usable := it.(type) {
case item.Chargeable:
useCtx := p.useContext()
if !p.usingItem {
if !chargeable.ReleaseCharge(p, p.tx, useCtx) {
if !usable.ReleaseCharge(p, p.tx, useCtx) {
// If the item was not charged yet, start charging.
p.usingSince, p.usingItem = time.Now(), true
}
Expand All @@ -1391,14 +1391,11 @@ func (p *Player) UseItem() {
// Stop charging and determine if the item is ready.
p.usingItem = false
dur := p.useDuration()
if chargeable.Charge(p, p.tx, useCtx, dur) {
if usable.Charge(p, p.tx, useCtx, dur) {
p.session().SendChargeItemComplete()
}
p.handleUseContext(useCtx)
p.updateState()
}

switch usable := it.(type) {
case item.Usable:
useCtx := p.useContext()
if !usable.Use(p.tx, p, useCtx) {
Expand Down
14 changes: 8 additions & 6 deletions server/session/world.go
Original file line number Diff line number Diff line change
Expand Up @@ -712,26 +712,28 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool)
pk.SoundType = packet.SoundEventBucketEmptyLava
case sound.BowShoot:
pk.SoundType = packet.SoundEventBow
case sound.Crossbow:
case sound.CrossbowLoad:
switch so.Stage {
case sound.CrossbowStageLoadStart:
case sound.CrossbowLoadingStart:
pk.SoundType = packet.SoundEventCrossbowLoadingStart
if so.QuickCharge {
pk.SoundType = packet.SoundEventCrossbowQuickChargeStart
}
case sound.CrossbowStageMiddle:
case sound.CrossbowLoadingMiddle:
pk.SoundType = packet.SoundEventCrossbowLoadingMiddle
if so.QuickCharge {
pk.SoundType = packet.SoundEventCrossbowQuickChargeMiddle
}
case sound.CrossbowStageLoadEnd:
case sound.CrossbowLoadingEnd:
pk.SoundType = packet.SoundEventCrossbowLoadingEnd
if so.QuickCharge {
pk.SoundType = packet.SoundEventCrossbowQuickChargeEnd
}
case sound.CrossbowStageShoot:
pk.SoundType = packet.SoundEventCrossbowShoot
default:
panic("invalid crossbow loading stage")
}
case sound.CrossbowShoot:
pk.SoundType = packet.SoundEventCrossbowShoot
case sound.ArrowHit:
pk.SoundType = packet.SoundEventBowHit
case sound.ItemThrow:
Expand Down
22 changes: 12 additions & 10 deletions server/world/sound/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ type BucketEmpty struct {
// BowShoot is a sound played when a bow is shot.
type BowShoot struct{ sound }

// Crossbow is a sound when a crossbow is being used.
type Crossbow struct {
// CrossbowLoad is a sound when a crossbow is being loaded.
type CrossbowLoad struct {
// Stage is the stage of the crossbow.
Stage int
// QuickCharge returns if the item being used has quick charge enchantment.
Expand All @@ -57,15 +57,17 @@ type Crossbow struct {
sound
}

// CrossbowShoot is a sound played when a crossbow is shot.
type CrossbowShoot struct{ sound }

const (
// CrossbowStageLoadStart is the stage where crossbows start to load.
CrossbowStageLoadStart = iota
// CrossbowStageMiddle is the stage where crossbow is loading and stops loading.
CrossbowStageMiddle
// CrossbowStageLoadEnd is the stage where crossbow is finished loading.
CrossbowStageLoadEnd
// CrossbowStageShoot is the stage where a crossbow is shot.
CrossbowStageShoot
// CrossbowLoadingStart is the stage where a crossbows start to load.
CrossbowLoadingStart = iota
// CrossbowLoadingMiddle is the stage where a crossbow loading but not yet
// finished loading.
CrossbowLoadingMiddle
// CrossbowLoadingEnd is the stage where a crossbow is finished loading.
CrossbowLoadingEnd
)

// ArrowHit is a sound played when an arrow hits ground.
Expand Down

0 comments on commit deeaac4

Please sign in to comment.