Skip to content

Commit

Permalink
Implemented crossbows (#957)
Browse files Browse the repository at this point in the history
Co-authored-by: DaPigGuy <[email protected]>
Co-authored-by: RoyalMCPE <[email protected]>
  • Loading branch information
3 people authored Jan 4, 2025
1 parent 0347c6e commit 613899c
Show file tree
Hide file tree
Showing 13 changed files with 403 additions and 13 deletions.
11 changes: 5 additions & 6 deletions server/entity/firework.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ 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, false)
return NewFireworkAttached(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, attached bool) *world.EntityHandle {
func NewFireworkAttached(opts world.EntitySpawnOpts, firework item.Firework, owner world.Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *world.EntityHandle {
conf := fireworkConf
conf.SidewaysVelocityMultiplier = sidewaysVelocityMultiplier
conf.UpwardsAcceleration = upwardsAcceleration
conf.Firework = firework
conf.ExistenceDuration = firework.RandomisedDuration()
conf.Attached = attached
Expand All @@ -27,10 +29,7 @@ func NewFireworkAttached(opts world.EntitySpawnOpts, firework item.Firework, own
return opts.New(FireworkType, conf)
}

var fireworkConf = FireworkBehaviourConfig{
SidewaysVelocityMultiplier: 1.15,
UpwardsAcceleration: 0.04,
}
var fireworkConf = FireworkBehaviourConfig{}

// FireworkType is a world.EntityType implementation for Firework.
var FireworkType fireworkType
Expand Down
4 changes: 2 additions & 2 deletions server/entity/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ var conf = world.EntityRegistryConfig{
EnderPearl: NewEnderPearl,
FallingBlock: NewFallingBlock,
Lightning: NewLightning,
Firework: func(opts world.EntitySpawnOpts, firework world.Item, owner world.Entity, attached bool) *world.EntityHandle {
return NewFireworkAttached(opts, firework.(item.Firework), owner, attached)
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)
},
Item: func(opts world.EntitySpawnOpts, it any) *world.EntityHandle {
return NewItem(opts, it.(item.Stack))
Expand Down
230 changes: 230 additions & 0 deletions server/item/crossbow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package item

import (
"time"
_ "unsafe"

"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, tx *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 {
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)
}
}

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

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

// ContinueCharge ...
func (c Crossbow) ContinueCharge(releaser Releaser, tx *world.Tx, ctx *UseContext, duration time.Duration) {
if !c.Item.Empty() {
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() {
return
}

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

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

// 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})
}

if progress >= 1 && quickChargeLevel > 0 {
tx.PlaySound(releaser.Position(), sound.Crossbow{Stage: sound.CrossbowStageLoadEnd, QuickCharge: hasQuickCharge})
}
}

// 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
}

creative := releaser.GameMode().CreativeInventory()
rot := releaser.Rotation().Neg()
dirVec := releaser.Rotation().Vec3().Normalize()

if firework, isFirework := c.Item.Item().(Firework); isFirework {
createFirework := tx.World().EntityRegistry().Config().Firework
fireworkEntity := createFirework(world.EntitySpawnOpts{
Position: torsoPosition(releaser),
Velocity: dirVec.Mul(0.1),
Rotation: rot,
}, firework, releaser, 1.15, 0, false)
tx.AddEntity(fireworkEntity)
ctx.DamageItem(3)
} else {
createArrow := tx.World().EntityRegistry().Config().Arrow
arrow := createArrow(world.EntitySpawnOpts{
Position: torsoPosition(releaser),
Velocity: dirVec.Mul(5.15),
Rotation: rot,
}, 9, releaser, false, false, !creative, 0, c.Item.Item().(Arrow).Tip)
tx.AddEntity(arrow)
ctx.DamageItem(1)
}

c.Item = Stack{}
held, left := releaser.HeldItems()
crossbow := held.WithItem(c)
releaser.SetHeldItems(crossbow, left)
tx.PlaySound(releaser.Position(), sound.Crossbow{Stage: sound.CrossbowStageShoot})
return true
}

// 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
49 changes: 49 additions & 0 deletions server/item/enchantment/quick_charge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package enchantment

import (
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"time"
)

// QuickCharge is an enchantment for quickly reloading a crossbow.
var QuickCharge quickCharge

type quickCharge struct{}

// Name ...
func (quickCharge) Name() string {
return "Quick Charge"
}

// MaxLevel ...
func (quickCharge) MaxLevel() int {
return 3
}

// Cost ...
func (quickCharge) Cost(level int) (int, int) {
minCost := 12 + (level-1)*20
return minCost, 50
}

// Rarity ...
func (quickCharge) Rarity() item.EnchantmentRarity {
return item.EnchantmentRarityUncommon
}

// ChargeDuration returns the charge duration.
func (quickCharge) ChargeDuration(level int) time.Duration {
return time.Duration((1.25 - 0.25*float64(level)) * float64(time.Second))
}

// CompatibleWithEnchantment ...
func (quickCharge) CompatibleWithEnchantment(item.EnchantmentType) bool {
return true
}

// CompatibleWithItem ...
func (quickCharge) CompatibleWithItem(i world.Item) bool {
_, ok := i.(item.Crossbow)
return ok
}
2 changes: 1 addition & 1 deletion server/item/enchantment/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func init() {
// TODO: (32) Channeling.
// TODO: (33) Multishot.
// TODO: (34) Piercing.
// TODO: (35) Quick Charge.
item.RegisterEnchantment(35, QuickCharge)
item.RegisterEnchantment(36, SoulSpeed)
item.RegisterEnchantment(37, SwiftSneak)
}
4 changes: 2 additions & 2 deletions server/item/firework.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (f Firework) Use(tx *world.Tx, user User, ctx *UseContext) bool {
tx.PlaySound(pos, sound.FireworkLaunch{})
create := tx.World().EntityRegistry().Config().Firework
opts := world.EntitySpawnOpts{Position: pos, Rotation: user.Rotation()}
tx.AddEntity(create(opts, f, user, true))
tx.AddEntity(create(opts, f, user, 1.15, 0.04, true))

ctx.SubtractFromCount(1)
return true
Expand All @@ -42,7 +42,7 @@ func (f Firework) UseOnBlock(pos cube.Pos, _ cube.Face, clickPos mgl64.Vec3, tx
fpos := pos.Vec3().Add(clickPos)
create := tx.World().EntityRegistry().Config().Firework
opts := world.EntitySpawnOpts{Position: fpos, Rotation: cube.Rotation{rand.Float64() * 360, 90}}
tx.AddEntity(create(opts, f, user, false))
tx.AddEntity(create(opts, f, user, 1.15, 0.04, false))
tx.PlaySound(fpos, sound.FireworkLaunch{})

ctx.SubtractFromCount(1)
Expand Down
20 changes: 20 additions & 0 deletions server/item/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ type Releasable interface {
Requirements() []Stack
}

// Chargeable represents an item that can be charged.
type Chargeable interface {
// Charge is called when an item is being used.
Charge(releaser Releaser, tx *world.Tx, ctx *UseContext, duration time.Duration) bool
// ContinueCharge continues the charge.
ContinueCharge(releaser Releaser, tx *world.Tx, ctx *UseContext, duration time.Duration)
// ReleaseCharge is called when an item is being released.
ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext) bool
}

// User represents an entity that is able to use an item in the world, typically entities such as players,
// which interact with the world using an item.
type User interface {
Expand Down Expand Up @@ -217,6 +227,16 @@ func eyePosition(e world.Entity) mgl64.Vec3 {
return pos
}

// torsoPosition returns the position of the torso of the entity if the entity implements entity.Torsoed, or the
// actual position if it doesn't.
func torsoPosition(e world.Entity) mgl64.Vec3 {
pos := e.Position()
if torso, ok := e.(interface{ TorsoHeight() float64 }); ok {
pos = pos.Add(mgl64.Vec3{0, torso.TorsoHeight()})
}
return pos
}

// Int32FromRGBA converts a color.RGBA into an int32. These int32s are present in things such as signs and dyed leather armour.
func int32FromRGBA(x color.RGBA) int32 {
if x.R == 0 && x.G == 0 && x.B == 0 {
Expand Down
1 change: 1 addition & 0 deletions server/item/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func init() {
world.RegisterItem(Compass{})
world.RegisterItem(Cookie{})
world.RegisterItem(CopperIngot{})
world.RegisterItem(Crossbow{})
world.RegisterItem(Diamond{})
world.RegisterItem(DiscFragment{})
world.RegisterItem(DragonBreath{})
Expand Down
Loading

0 comments on commit 613899c

Please sign in to comment.