2026-07-09 08:33:57 +08:00
|
|
|
package entity
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"math"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/df-mc/dragonfly/server/block"
|
|
|
|
|
"github.com/df-mc/dragonfly/server/block/cube"
|
|
|
|
|
"github.com/df-mc/dragonfly/server/internal/nbtconv"
|
|
|
|
|
"github.com/df-mc/dragonfly/server/item"
|
|
|
|
|
"github.com/df-mc/dragonfly/server/world"
|
|
|
|
|
"github.com/go-gl/mathgl/mgl64"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ItemBehaviourConfig holds optional parameters for an ItemBehaviour.
|
|
|
|
|
type ItemBehaviourConfig struct {
|
|
|
|
|
Item item.Stack
|
|
|
|
|
// Gravity is the amount of Y velocity subtracted every tick.
|
|
|
|
|
Gravity float64
|
|
|
|
|
// Drag is used to reduce all axes of the velocity every tick. Velocity is
|
|
|
|
|
// multiplied with (1-Drag) every tick.
|
|
|
|
|
Drag float64
|
|
|
|
|
// ExistenceDuration specifies how long the item stack should last. The
|
|
|
|
|
// default is time.Minute * 5.
|
|
|
|
|
ExistenceDuration time.Duration
|
|
|
|
|
// PickupDelay specifies how much time must expire before the item can be
|
|
|
|
|
// picked up by collectors. The default is time.Second / 2.
|
|
|
|
|
PickupDelay time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (conf ItemBehaviourConfig) Apply(data *world.EntityData) {
|
|
|
|
|
data.Data = conf.New()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New creates an ItemBehaviour using i and the optional parameters in conf.
|
|
|
|
|
func (conf ItemBehaviourConfig) New() *ItemBehaviour {
|
|
|
|
|
i := conf.Item
|
|
|
|
|
if i.Count() > i.MaxCount() {
|
|
|
|
|
i = i.Grow(i.MaxCount() - i.Count())
|
|
|
|
|
}
|
|
|
|
|
i = nbtconv.Item(nbtconv.WriteItem(i, true), nil)
|
|
|
|
|
|
|
|
|
|
if conf.PickupDelay == 0 {
|
|
|
|
|
conf.PickupDelay = time.Second / 2
|
|
|
|
|
}
|
|
|
|
|
if conf.ExistenceDuration == 0 {
|
2026-07-11 00:33:35 +07:00
|
|
|
conf.ExistenceDuration = time.Second * 15
|
2026-07-09 08:33:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b := &ItemBehaviour{conf: conf, i: i, pickupDelay: conf.PickupDelay}
|
|
|
|
|
b.passive = PassiveBehaviourConfig{
|
|
|
|
|
Gravity: conf.Gravity,
|
|
|
|
|
Drag: conf.Drag,
|
|
|
|
|
ExistenceDuration: conf.ExistenceDuration,
|
|
|
|
|
Tick: b.tick,
|
|
|
|
|
}.New()
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ItemBehaviour implements the behaviour of item entities.
|
|
|
|
|
type ItemBehaviour struct {
|
|
|
|
|
conf ItemBehaviourConfig
|
|
|
|
|
passive *PassiveBehaviour
|
|
|
|
|
i item.Stack
|
|
|
|
|
|
|
|
|
|
pickupDelay time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Item returns the item.Stack held by the entity.
|
|
|
|
|
func (i *ItemBehaviour) Item() item.Stack {
|
|
|
|
|
return i.i
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tick moves the entity, checks if it should be picked up by a nearby collector
|
|
|
|
|
// or if it should merge with nearby item entities.
|
|
|
|
|
func (i *ItemBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
|
|
|
|
pos := cube.PosFromVec3(e.Position())
|
|
|
|
|
blockPos := pos.Side(cube.FaceDown)
|
|
|
|
|
|
|
|
|
|
bl, ok := tx.Block(blockPos).(block.Hopper)
|
|
|
|
|
if ok && !bl.Powered && bl.CollectCooldown <= 0 {
|
|
|
|
|
addedCount, err := bl.Inventory(tx, blockPos).AddItem(i.i)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if addedCount == 0 {
|
|
|
|
|
return i.passive.Tick(e, tx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This is only reached if part of the item stack was collected into the hopper.
|
|
|
|
|
opts := world.EntitySpawnOpts{Position: pos.Vec3Centre()}
|
|
|
|
|
tx.AddEntity(NewItem(opts, i.Item().Grow(-addedCount)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ = e.Close()
|
|
|
|
|
bl.CollectCooldown = 8
|
|
|
|
|
tx.SetBlock(blockPos, bl, nil)
|
|
|
|
|
}
|
|
|
|
|
return i.passive.Tick(e, tx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Explode reacts to explosions. The item entity is destroyed, unless the item
|
|
|
|
|
// type is blast proof.
|
|
|
|
|
func (i *ItemBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, conf block.ExplosionConfig) {
|
|
|
|
|
if impact > 0 {
|
|
|
|
|
if expl, ok := i.Item().Item().(interface{ BlastProof() bool }); ok && expl.BlastProof() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_ = e.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tick checks if the item can be picked up or merged with nearby item stacks.
|
|
|
|
|
func (i *ItemBehaviour) tick(e *Ent, tx *world.Tx) {
|
|
|
|
|
if i.pickupDelay == 0 {
|
|
|
|
|
i.checkNearby(e, tx)
|
|
|
|
|
} else if i.pickupDelay < math.MaxInt16*(time.Second/20) {
|
|
|
|
|
i.pickupDelay -= time.Second / 20
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkNearby checks the nearby entities for item collectors and other item
|
|
|
|
|
// stacks. If a collector is found in range, the item will be picked up. If
|
|
|
|
|
// another item stack with the same item type is found in range, the item
|
|
|
|
|
// stacks will merge.
|
|
|
|
|
func (i *ItemBehaviour) checkNearby(e *Ent, tx *world.Tx) {
|
|
|
|
|
pos := e.Position()
|
|
|
|
|
bbox := e.H().Type().BBox(e)
|
|
|
|
|
grown := bbox.GrowVec3(mgl64.Vec3{1, 0.5, 1}).Translate(pos)
|
|
|
|
|
|
|
|
|
|
for other := range tx.EntitiesWithin(bbox.Translate(pos).Grow(2)) {
|
|
|
|
|
if e.H() == other.H() || !other.H().Type().BBox(other).Translate(other.Position()).IntersectsWith(grown) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if collector, ok := other.(Collector); ok {
|
|
|
|
|
// A collector was within range to pick up the entity.
|
|
|
|
|
i.collect(e, collector, tx)
|
|
|
|
|
return
|
|
|
|
|
} else if other.H().Type() == ItemType {
|
|
|
|
|
// Another item entity was in range to merge with.
|
|
|
|
|
if i.merge(e, other.(*Ent), tx) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// merge merges the item entity with another item entity.
|
|
|
|
|
func (i *ItemBehaviour) merge(e *Ent, other *Ent, tx *world.Tx) bool {
|
|
|
|
|
pos := e.Position()
|
|
|
|
|
otherBehaviour := other.Behaviour().(*ItemBehaviour)
|
|
|
|
|
if otherBehaviour.i.Count() == otherBehaviour.i.MaxCount() || i.i.Count() == i.i.MaxCount() || !i.i.Comparable(otherBehaviour.i) {
|
|
|
|
|
// Either stack is already filled up to the maximum, meaning we can't
|
|
|
|
|
// change anything any way, other the stack types weren't comparable.
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
a, b := otherBehaviour.i.AddStack(i.i)
|
|
|
|
|
|
|
|
|
|
tx.AddEntity(NewItem(world.EntitySpawnOpts{Position: other.Position(), Velocity: other.Velocity()}, a))
|
|
|
|
|
if !b.Empty() {
|
|
|
|
|
tx.AddEntity(NewItem(world.EntitySpawnOpts{Position: pos, Velocity: e.Velocity()}, b))
|
|
|
|
|
}
|
|
|
|
|
_ = e.Close()
|
|
|
|
|
_ = other.Close()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// collect makes a collector collect the item (or at least part of it).
|
|
|
|
|
func (i *ItemBehaviour) collect(e *Ent, collector Collector, tx *world.Tx) {
|
|
|
|
|
pos := e.Position()
|
|
|
|
|
n, _ := collector.Collect(i.i)
|
|
|
|
|
if n == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, viewer := range tx.Viewers(pos) {
|
|
|
|
|
viewer.ViewEntityAction(e, PickedUpAction{Collector: collector})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if n == i.i.Count() {
|
|
|
|
|
// The collector picked up the entire stack.
|
|
|
|
|
_ = e.Close()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// Create a new item entity and shrink it by the amount of items that the
|
|
|
|
|
// collector collected.
|
|
|
|
|
tx.AddEntity(NewItem(world.EntitySpawnOpts{Position: pos}, i.i.Grow(-n)))
|
|
|
|
|
_ = e.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collector represents an entity in the world that is able to collect an item, typically an entity such as
|
|
|
|
|
// a player or a zombie.
|
|
|
|
|
type Collector interface {
|
|
|
|
|
world.Entity
|
|
|
|
|
// Collect collects the stack passed. It is called if the Collector is standing near an item entity that
|
|
|
|
|
// may be picked up.
|
|
|
|
|
// The count of items collected from the stack n is returned, along with a
|
|
|
|
|
// bool that indicates if the Collector was in a state where it could
|
|
|
|
|
// collect any items in the first place.
|
|
|
|
|
Collect(stack item.Stack) (n int, ok bool)
|
|
|
|
|
}
|