Files
mc/server/item/crossbow.go
T

250 lines
7.0 KiB
Go
Raw Normal View History

2026-07-09 08:33:57 +08:00
package item
import (
"time"
_ "unsafe"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
)
// 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, _ *world.Tx, ctx *UseContext, duration time.Duration) bool {
if !c.Item.Empty() {
return false
}
creative := releaser.GameMode().CreativeInventory()
held, left := releaser.HeldItems()
if chargeDuration, _ := c.chargeDuration(held); duration < chargeDuration {
return false
}
projectileItem, ok := c.findProjectile(releaser, ctx)
if !ok {
return false
}
c.Item = projectileItem.Grow(-projectileItem.Count() + 1)
if !creative {
ctx.Consume(c.Item)
}
releaser.SetHeldItems(held.WithItem(c), left)
return true
}
// ContinueCharge ...
func (c Crossbow) ContinueCharge(releaser Releaser, tx *world.Tx, ctx *UseContext, duration time.Duration) {
if !c.Item.Empty() {
return
}
held, _ := releaser.HeldItems()
if _, ok := c.findProjectile(releaser, ctx); !ok {
return
}
chargeDuration, qcLevel := c.chargeDuration(held)
if duration.Seconds() <= 0.1 {
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*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.CrossbowLoad{Stage: sound.CrossbowLoadingMiddle, QuickCharge: qcLevel > 0})
}
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.
func (c Crossbow) ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext) bool {
if c.Item.Empty() {
return false
}
held, _ := releaser.HeldItems()
creative := releaser.GameMode().CreativeInventory()
pierceLevel, multishot := 0, false
for _, enchant := range held.Enchantments() {
if _, ok := enchant.Type().(interface{ MultipleProjectiles() bool }); ok {
multishot = true
}
if _, ok := enchant.Type().(interface{ Pierces() bool }); ok {
pierceLevel = enchant.Level()
}
}
arrowConf := world.ArrowSpawnConfig{
Damage: 9,
Owner: releaser,
Critical: true,
ObtainArrowOnPickup: !creative,
PiercingLevel: pierceLevel,
}
c.shoot(releaser, tx, 0, arrowConf)
if multishot {
arrowConf.ObtainArrowOnPickup = false
c.shoot(releaser, tx, -10, arrowConf)
c.shoot(releaser, tx, 10, arrowConf)
}
c.applyDamage(ctx)
c.Item = Stack{}
held, left := releaser.HeldItems()
crossbow := held.WithItem(c)
releaser.SetHeldItems(crossbow, left)
tx.PlaySound(releaser.Position(), sound.CrossbowShoot{})
return true
}
// CanCharge ...
func (c Crossbow) CanCharge(releaser Releaser, _ *world.Tx, ctx *UseContext) bool {
_, found := c.findProjectile(releaser, ctx)
return found && !c.Item.Empty()
}
// shoot fires the crossbow's loaded projectiles.
func (c Crossbow) shoot(releaser Releaser, tx *world.Tx, offsetAngle float64, arrowConf world.ArrowSpawnConfig) {
rot := releaser.Rotation()
dirVec := cube.Rotation{rot[0] + offsetAngle, rot[1]}.Vec3()
if firework, ok := c.Item.Item().(Firework); ok {
createFirework := tx.World().EntityRegistry().Config().Firework
projectile := createFirework(world.EntitySpawnOpts{
Position: torsoPosition(releaser),
Velocity: dirVec.Mul(0.8),
Rotation: rot.Neg(),
}, firework, releaser, 1.0, 0, false)
tx.AddEntity(projectile)
} else {
createArrow := tx.World().EntityRegistry().Config().Arrow
arrowConf.Tip = c.Item.Item().(Arrow).Tip
arrow := createArrow(world.EntitySpawnOpts{
Position: torsoPosition(releaser),
Velocity: dirVec.Mul(5.15),
Rotation: rot.Neg(),
}, arrowConf)
tx.AddEntity(arrow)
}
}
// applyDamage applies damage on a UseContext based on the projectile loaded
// in the crossboww.
func (c Crossbow) applyDamage(ctx *UseContext) {
if _, ok := c.Item.Item().(Firework); ok {
ctx.DamageItem(3)
} else {
ctx.DamageItem(1)
}
}
// MaxCount always returns 1.
func (Crossbow) MaxCount() int {
return 1
}
// DurabilityInfo ...
func (Crossbow) DurabilityInfo() DurabilityInfo {
return DurabilityInfo{
MaxDurability: 464,
BrokenItem: simpleItem(Stack{}),
}
}
// FuelInfo ...
func (Crossbow) FuelInfo() FuelInfo {
return newFuelInfo(time.Second * 15)
}
// EnchantmentValue ...
func (Crossbow) EnchantmentValue() int {
return 1
}
// EncodeItem ...
func (Crossbow) EncodeItem() (name string, meta int16) {
return "minecraft:crossbow", 0
}
// DecodeNBT ...
func (c Crossbow) DecodeNBT(data map[string]any) any {
c.Item = mapItem(data, "chargedItem")
return c
}
// EncodeNBT ...
func (c Crossbow) EncodeNBT() map[string]any {
if !c.Item.Empty() {
return map[string]any{"chargedItem": writeItem(c.Item, true)}
}
return nil
}
// noinspection ALL
//
//go:linkname writeItem github.com/df-mc/dragonfly/server/internal/nbtconv.WriteItem
func writeItem(s Stack, disk bool) map[string]any
// noinspection ALL
//
//go:linkname mapItem github.com/df-mc/dragonfly/server/internal/nbtconv.MapItem
func mapItem(x map[string]any, k string) Stack