26ed99fda6
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
250 lines
7.0 KiB
Go
250 lines
7.0 KiB
Go
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
|