Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions server/entity/armour_stand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package entity

import (
"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/item/inventory"
"github.com/df-mc/dragonfly/server/world"
)

// NewArmourStand creates a new armour stand entity in the world with the given spawn options.
func NewArmourStand(opts world.EntitySpawnOpts) *world.EntityHandle {
conf := armourStandConf
conf.Armour = inventory.NewArmour(nil)
return opts.New(ArmourStandType, conf)
}

var armourStandConf = ArmourStandBehaviourConfig{}

var ArmourStandType armourStandType

// armourStandType is a world.EntityType implementation for armour stands.
type armourStandType struct{}

func (armourStandType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
return &Ent{tx: tx, handle: handle, data: data}
}

func (armourStandType) EncodeEntity() string { return "minecraft:armor_stand" }

func (armourStandType) BBox(world.Entity) cube.BBox {
return cube.Box(-0.25, 0, -0.25, 0.25, 1.975, 0.25)
}

func (armourStandType) DecodeNBT(m map[string]any, data *world.EntityData) {
c := ArmourStandBehaviourConfig{
Armour: inventory.NewArmour(nil),
PoseIndex: int(nbtconv.Int32(m, "PoseIndex")) % 13,
MainHand: nbtconv.MapItem(m, "MainHand"),
OffHand: nbtconv.MapItem(m, "Offhand"),
}
armours := nbtconv.Slice(m, "Armor")
for i := 0; i < 4; i++ {
itemMap, ok := armours[i].(map[string]any)
if !ok {
continue
}
var it item.Stack
switch i {
case 0:
nbtconv.Item(itemMap, &it)
c.Armour.SetHelmet(it)
case 1:
nbtconv.Item(itemMap, &it)
c.Armour.SetChestplate(it)
case 2:
nbtconv.Item(itemMap, &it)
c.Armour.SetLeggings(it)
case 3:
nbtconv.Item(itemMap, &it)
c.Armour.SetBoots(it)
}
}
data.Data = c.New()
}

func (armourStandType) EncodeNBT(data *world.EntityData) map[string]any {
a := data.Data.(*ArmourStandBehaviour)
return map[string]any{
"MainHand": nbtconv.WriteItem(a.conf.MainHand, true),
"Offhand": nbtconv.WriteItem(a.conf.OffHand, true),
"Armor": []map[string]any{
nbtconv.WriteItem(a.Armour().Helmet(), true),
nbtconv.WriteItem(a.Armour().Chestplate(), true),
nbtconv.WriteItem(a.Armour().Leggings(), true),
nbtconv.WriteItem(a.Armour().Boots(), true),
},
"PoseIndex": int32(a.conf.PoseIndex),
}
}
248 changes: 248 additions & 0 deletions server/entity/armour_stand_behaviour.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package entity

import (
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
"github.com/go-gl/mathgl/mgl64"
"time"
)

// ArmourStandBehaviourConfig holds optional parameters for
// ArmourStandBehaviour.
type ArmourStandBehaviourConfig struct {
Armour *inventory.Armour
// MainHand is the item equipped in the main hand slot of the armour stand.
MainHand item.Stack
// OffHand is the item equipped in the offhand slot of the armour stand.
OffHand item.Stack
// PoseIndex is the pose index of the armour stand. Possible values range
// from 0 to 12 (inclusive).
PoseIndex int
}

// Apply ...
func (conf ArmourStandBehaviourConfig) Apply(data *world.EntityData) {
data.Data = conf.New()
}

// New creates an ArmourStandBehaviour using the optional parameters in conf.
func (conf ArmourStandBehaviourConfig) New() *ArmourStandBehaviour {
a := &ArmourStandBehaviour{
conf: conf,
armours: conf.Armour,
lastOnGround: true,
}
a.passive = PassiveBehaviourConfig{
Gravity: 0.04,
Drag: 0.02,
Tick: a.tick,
}.New()
return a
}

// ArmourStandBehaviour implements the behaviour for armour stand entities.
type ArmourStandBehaviour struct {
conf ArmourStandBehaviourConfig

passive *PassiveBehaviour
hurt time.Duration
armours *inventory.Armour

lastOnGround bool
}

// armourStandDropOffset returns the position offset at which an item should be
// dropped from an armour stand, based on the type of item.
func armourStandDropOffset(stack item.Stack) mgl64.Vec3 {
var offset mgl64.Vec3
switch stack.Item().(type) {
case item.Helmet:
offset[1] = 1.8
case item.Chestplate:
offset[1] = 1.4
case item.Leggings:
offset[1] = 0.6
case item.Boots:
offset[1] = 0.2
default:
offset[1] = 1.4
}
return offset
}

// Tick ...
func (a *ArmourStandBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
return a.passive.Tick(e, tx)
}

// tick ...
func (a *ArmourStandBehaviour) tick(e *Ent, tx *world.Tx) {
if a.hurt > 0 {
a.hurt -= time.Millisecond * 50
if a.hurt < 0 {
a.hurt = 0
}
a.updateState(e)
}

if a.lastOnGround != a.passive.mc.OnGround() {
if !a.lastOnGround {
tx.PlaySound(e.Position(), sound.ArmourStandLand{})
}
a.lastOnGround = a.passive.mc.OnGround()
}
}

// Explode ...
func (a *ArmourStandBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, config block.ExplosionConfig) {
a.passive.Explode(e, src, impact, config)
}

// AcceptItem ...
func (a *ArmourStandBehaviour) AcceptItem(e *Ent, from world.Entity, _ *world.Tx, ctx *item.UseContext) bool {
if sneaker, ok := from.(interface {
Sneaking() bool
}); ok && sneaker.Sneaking() {
a.SetPoseIndex(e, (a.PoseIndex()+1)%13)
return false
}

heldItems, ok := from.(interface {
HeldItems() (mainHand, offHand item.Stack)
})
if !ok {
return false
}
mainHand, _ := heldItems.HeldItems()
if mainHand.Empty() {
return false
}
var (
dropItem item.Stack
add = mainHand.Grow(-mainHand.Count() + 1)
)
i := add.Item()
inv := a.armours
if _, isArmour := i.(item.Armour); isArmour {
switch i.(type) {
case item.Helmet:
dropItem = inv.Helmet()
inv.SetHelmet(add)
case item.Chestplate:
dropItem = inv.Chestplate()
inv.SetChestplate(add)
case item.Leggings:
dropItem = inv.Leggings()
inv.SetLeggings(add)
case item.Boots:
dropItem = inv.Boots()
inv.SetBoots(add)
}
a.updateArmours(e)
} else {
it, left := a.HeldItems()
dropItem = it
a.SetHeldItems(e, add, left)
}

ctx.SubtractFromCount(1)
ctx.NewItem, ctx.NewItemSurvivalOnly = dropItem, true
return true
}

// Attack ...
func (a *ArmourStandBehaviour) Attack(e *Ent, _ world.Entity, tx *world.Tx) {
if a.hurt > 0 {
a.destroy(e, tx)
return
}
tx.PlaySound(e.Position(), sound.ArmourStandPlace{})
a.hurt = time.Millisecond * 300
a.updateState(e)
}

// destroy destroys the armour stand, dropping all equipped items.
func (a *ArmourStandBehaviour) destroy(e *Ent, tx *world.Tx) {
tx.PlaySound(e.Position(), sound.ArmourStandBreak{})
a.dropAll(e, tx)
_ = e.Close()
}

// HurtDuration returns the remaining hurt duration of the armour stand.
func (a *ArmourStandBehaviour) HurtDuration() time.Duration {
return a.hurt
}

// dropAll drops all equipped items of the armour stand.
func (a *ArmourStandBehaviour) dropAll(e *Ent, tx *world.Tx) {
dropPos := e.Position()
drop := func(stack item.Stack) {
if stack.Empty() {
return
}
tx.AddEntity(NewItem(world.EntitySpawnOpts{Position: dropPos.Add(armourStandDropOffset(stack))}, stack))
}
for _, i := range a.armours.Items() {
drop(i)
}
mainHand, offHand := a.HeldItems()
drop(mainHand)
drop(offHand)
}

// Armour returns the armour equipped on the armour stand.
func (a *ArmourStandBehaviour) Armour() *inventory.Armour {
return a.armours
}

// HeldItems returns the items equipped in the main hand and offhand slots of the armour stand.
func (a *ArmourStandBehaviour) HeldItems() (mainHand, offHand item.Stack) {
return a.conf.MainHand, a.conf.OffHand
}

// PoseIndex returns the pose index of the armour stand. Possible values range
// from 0 to 12 (inclusive).
func (a *ArmourStandBehaviour) PoseIndex() int {
return a.conf.PoseIndex
}

// SetHeldItems sets the items equipped in the main hand and offhand slots of the armour stand.
func (a *ArmourStandBehaviour) SetHeldItems(e *Ent, mainHand, offHand item.Stack) {
a.conf.MainHand = mainHand
a.conf.OffHand = offHand
a.updateHeldItems(e)
}

// SetPoseIndex sets the pose index of the armour stand. Possible values range
// from 0 to 12 (inclusive).
func (a *ArmourStandBehaviour) SetPoseIndex(e *Ent, poseIndex int) {
if poseIndex < 0 || poseIndex > 12 {
panic("pose index must be between 0 and 12")
}
a.conf.PoseIndex = poseIndex
a.updateState(e)
}

// updateArmours updates the armour stand's equipped items for all viewers.
func (a *ArmourStandBehaviour) updateArmours(e *Ent) {
for _, v := range e.tx.Viewers(e.Position()) {
v.ViewEntityArmour(e)
}
}

// updateHeldItems updates the armour stand's held items for all viewers.
func (a *ArmourStandBehaviour) updateHeldItems(e *Ent) {
for _, v := range e.tx.Viewers(e.Position()) {
v.ViewEntityItems(e)
}
}

// updateState updates the armour stand's state for all viewers.
func (a *ArmourStandBehaviour) updateState(e *Ent) {
for _, v := range e.tx.Viewers(e.data.Pos) {
v.ViewEntityState(e)
}
}
38 changes: 38 additions & 0 deletions server/entity/ent.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package entity
import (
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
"sync"
Expand All @@ -17,6 +18,23 @@ type Behaviour interface {
Tick(e *Ent, tx *world.Tx) *Movement
}

// ItemAcceptor is an interface that Behaviours or world.Entity may implement
// to accept items from players.
type ItemAcceptor interface {
// AcceptItem returns whether the entity accepts the item stack passed. This
// may be called when a player tries to interact with the entity.
AcceptItem(from world.Entity, tx *world.Tx, ctx *item.UseContext) bool
}

// Attackable is an interface that Behaviours or world.Entity may implement
// to allow entities to be attacked by other entities.
type Attackable interface {
// Attack is called when the entity is attacked by an attacker. Unlike world.Living
// entities, no damage value is passed. Used for entities that do not have health but
// can still be interacted with through attacks like armour stands.
Attack(attacker world.Entity, tx *world.Tx)
}

// Ent is a world.Entity implementation that allows entity implementations to
// share a lot of code. It is currently under development and is prone to
// (breaking) changes.
Expand Down Expand Up @@ -115,6 +133,26 @@ func (e *Ent) SetNameTag(s string) {
}
}

// AcceptItem returns whether the entity accepts the item stack passed. This may be
// called when a player tries to interact with the entity.
func (e *Ent) AcceptItem(from world.Entity, tx *world.Tx, ctx *item.UseContext) bool {
if acceptor, ok := e.Behaviour().(interface {
AcceptItem(e *Ent, from world.Entity, tx *world.Tx, ctx *item.UseContext) bool
}); ok {
return acceptor.AcceptItem(e, from, tx, ctx)
}
return false
}

// Attack is called when the entity is attacked by an attacker.
func (e *Ent) Attack(attacker world.Entity, tx *world.Tx) {
if attackable, ok := e.Behaviour().(interface {
Attack(e *Ent, attacker world.Entity, tx *world.Tx)
}); ok {
attackable.Attack(e, attacker, tx)
}
}

// Tick ticks Ent, progressing its lifetime and closing the entity if it is
// in the void.
func (e *Ent) Tick(tx *world.Tx, current int64) {
Expand Down
Loading