up3
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
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
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SwingArmAction is a world.EntityAction that makes an entity or player swing its arm.
|
||||
type SwingArmAction struct{ action }
|
||||
|
||||
// HurtAction is a world.EntityAction that makes an entity display the animation for being hurt. The entity will be
|
||||
// shown as red for a short duration.
|
||||
type HurtAction struct{ action }
|
||||
|
||||
// CriticalHitAction is a world.EntityAction that makes an entity display critical hit particles. This will show stars
|
||||
// around the entity.
|
||||
type CriticalHitAction struct {
|
||||
action
|
||||
// Count is the count of particles around the entity.
|
||||
Count int
|
||||
}
|
||||
|
||||
// EnchantedHitAction is a world.Action that makes an entity display enchanted hit particles. This will show circles
|
||||
// around the entity.
|
||||
type EnchantedHitAction struct {
|
||||
action
|
||||
// Count is the count of particles around the entity.
|
||||
Count int
|
||||
}
|
||||
|
||||
// DeathAction is a world.EntityAction that makes an entity display the death animation. After this animation, the
|
||||
// entity disappears from viewers watching it.
|
||||
type DeathAction struct{ action }
|
||||
|
||||
// EatAction is a world.EntityAction that makes an entity display the eating particles at its mouth to viewers with the
|
||||
// item in its hand being eaten.
|
||||
type EatAction struct{ action }
|
||||
|
||||
// ArrowShakeAction makes an arrow entity display a shaking animation for the given duration.
|
||||
type ArrowShakeAction struct {
|
||||
// Duration is the duration of the shake.
|
||||
Duration time.Duration
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
// PickedUpAction is a world.EntityAction that makes an item get picked up by a collector. After this animation, the
|
||||
// item disappears from viewers watching it.
|
||||
type PickedUpAction struct {
|
||||
// Collector is the entity that collected the item.
|
||||
Collector world.Entity
|
||||
|
||||
action
|
||||
}
|
||||
|
||||
// FireworkExplosionAction is a world.EntityAction that makes a Firework rocket display an explosion particle.
|
||||
type FireworkExplosionAction struct{ action }
|
||||
|
||||
// TotemUseAction is a world.EntityAction that displays the totem use particles and animation.
|
||||
type TotemUseAction struct{ action }
|
||||
|
||||
// action implements the Action interface. Structures in this package may embed it to gets its functionality
|
||||
// out of the box.
|
||||
type action struct{}
|
||||
|
||||
func (action) EntityAction() {}
|
||||
@@ -0,0 +1,83 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/internal/nbtconv"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewAreaEffectCloud creates a new area effect cloud entity and returns it.
|
||||
func NewAreaEffectCloud(opts world.EntitySpawnOpts, p potion.Potion) *world.EntityHandle {
|
||||
config := areaEffectCloudConf
|
||||
config.Potion = p
|
||||
for _, e := range p.Effects() {
|
||||
if _, ok := e.Type().(effect.LastingType); !ok {
|
||||
config.ReapplicationDelay = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
return opts.New(AreaEffectCloudType, config)
|
||||
}
|
||||
|
||||
var areaEffectCloudConf = AreaEffectCloudBehaviourConfig{
|
||||
RadiusUseGrowth: -0.5,
|
||||
RadiusTickGrowth: -0.005,
|
||||
ReapplicationDelay: time.Second * 2,
|
||||
}
|
||||
|
||||
// NewAreaEffectCloudWith ...
|
||||
func NewAreaEffectCloudWith(opts world.EntitySpawnOpts, t potion.Potion, duration, reapplicationDelay, durationOnUse time.Duration, radius, radiusOnUse, radiusGrowth float64) *world.EntityHandle {
|
||||
config := AreaEffectCloudBehaviourConfig{
|
||||
Potion: t,
|
||||
Radius: radius,
|
||||
RadiusUseGrowth: radiusOnUse,
|
||||
RadiusTickGrowth: radiusGrowth,
|
||||
Duration: duration,
|
||||
DurationUseGrowth: durationOnUse,
|
||||
ReapplicationDelay: reapplicationDelay,
|
||||
}
|
||||
return opts.New(AreaEffectCloudType, config)
|
||||
}
|
||||
|
||||
// AreaEffectCloudType is a world.EntityType implementation for AreaEffectCloud.
|
||||
var AreaEffectCloudType areaEffectCloudType
|
||||
|
||||
type areaEffectCloudType struct{}
|
||||
|
||||
func (t areaEffectCloudType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (areaEffectCloudType) EncodeEntity() string { return "minecraft:area_effect_cloud" }
|
||||
func (areaEffectCloudType) BBox(e world.Entity) cube.BBox {
|
||||
r := e.(*Ent).Behaviour().(*AreaEffectCloudBehaviour).Radius()
|
||||
return cube.Box(-r, 0, -r, r, 0.5, r)
|
||||
}
|
||||
|
||||
func (areaEffectCloudType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
data.Data = AreaEffectCloudBehaviourConfig{
|
||||
Potion: potion.From(nbtconv.Int32(m, "PotionId")),
|
||||
Radius: float64(nbtconv.Float32(m, "Radius")),
|
||||
RadiusUseGrowth: float64(nbtconv.Float32(m, "RadiusOnUse")),
|
||||
RadiusTickGrowth: float64(nbtconv.Float32(m, "RadiusPerTick")),
|
||||
Duration: nbtconv.TickDuration[int32](m, "Duration"),
|
||||
DurationUseGrowth: nbtconv.TickDuration[int32](m, "ReapplicationDelay"),
|
||||
ReapplicationDelay: nbtconv.TickDuration[int32](m, "DurationOnUse"),
|
||||
}.New()
|
||||
}
|
||||
|
||||
func (areaEffectCloudType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
a := data.Data.(*AreaEffectCloudBehaviour)
|
||||
return map[string]any{
|
||||
"PotionId": int32(a.conf.Potion.Uint8()),
|
||||
"ReapplicationDelay": int32(a.conf.ReapplicationDelay),
|
||||
"RadiusPerTick": float32(a.conf.RadiusTickGrowth),
|
||||
"RadiusOnUse": float32(a.conf.RadiusUseGrowth),
|
||||
"DurationOnUse": int32(a.conf.DurationUseGrowth),
|
||||
"Radius": float32(a.radius),
|
||||
"Duration": int32(a.duration),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"iter"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AreaEffectCloudBehaviourConfig contains optional parameters for an area
|
||||
// effect cloud entity.
|
||||
type AreaEffectCloudBehaviourConfig struct {
|
||||
Potion potion.Potion
|
||||
// Radius specifies the initial radius of the cloud. Defaults to 3.0.
|
||||
Radius float64
|
||||
// RadiusUseGrowth is the value that is added to the radius every time the
|
||||
// effect cloud is used/consumed. This is typically a negative value. (-0.5)
|
||||
RadiusUseGrowth float64
|
||||
// RadiusTickGrowth is the value added to the radius every tick. This is
|
||||
// typically a negative value. (-0.005)
|
||||
RadiusTickGrowth float64
|
||||
// Duration specifies the initial duration of the cloud. Defaults to 30s.
|
||||
Duration time.Duration
|
||||
// DurationUseGrowth is the duration that is added to the effect cloud every
|
||||
// time it is used/consumed. This is 0 in normal situations.
|
||||
DurationUseGrowth time.Duration
|
||||
// ReapplicationDelay specifies the delay with which the effects from the
|
||||
// cloud can be re-applied to users.
|
||||
ReapplicationDelay time.Duration
|
||||
}
|
||||
|
||||
func (conf AreaEffectCloudBehaviourConfig) Apply(data *world.EntityData) {
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
// New creates an AreaEffectCloudBehaviour using the parameter in conf and t.
|
||||
func (conf AreaEffectCloudBehaviourConfig) New() *AreaEffectCloudBehaviour {
|
||||
if conf.Radius == 0 {
|
||||
conf.Radius = 3.0
|
||||
}
|
||||
if conf.Duration == 0 {
|
||||
conf.Duration = time.Second * 30
|
||||
}
|
||||
stationary := StationaryBehaviourConfig{ExistenceDuration: conf.Duration}
|
||||
return &AreaEffectCloudBehaviour{
|
||||
conf: conf,
|
||||
stationary: stationary.New(),
|
||||
duration: conf.Duration,
|
||||
radius: conf.Radius,
|
||||
targets: make(map[*world.EntityHandle]time.Duration),
|
||||
}
|
||||
}
|
||||
|
||||
// AreaEffectCloudBehaviour is the cloud that is created when: lingering
|
||||
// potions are thrown; creepers with potion effects explode; dragon fireballs
|
||||
// hit the ground.
|
||||
type AreaEffectCloudBehaviour struct {
|
||||
conf AreaEffectCloudBehaviourConfig
|
||||
|
||||
stationary *StationaryBehaviour
|
||||
|
||||
duration time.Duration
|
||||
radius float64
|
||||
targets map[*world.EntityHandle]time.Duration
|
||||
}
|
||||
|
||||
// Radius returns the current radius of the area effect cloud.
|
||||
func (a *AreaEffectCloudBehaviour) Radius() float64 {
|
||||
return a.radius
|
||||
}
|
||||
|
||||
// Effects returns the effects the area effect cloud provides.
|
||||
func (a *AreaEffectCloudBehaviour) Effects() []effect.Effect {
|
||||
return a.conf.Potion.Effects()
|
||||
}
|
||||
|
||||
// Tick ...
|
||||
func (a *AreaEffectCloudBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
||||
a.stationary.Tick(e, tx)
|
||||
if a.stationary.close || e.Age() < time.Second/2 {
|
||||
// The cloud lives for at least half a second before it may begin
|
||||
// spreading effects and growing/shrinking.
|
||||
return nil
|
||||
}
|
||||
|
||||
pos := e.Position()
|
||||
if a.subtractTickRadius() {
|
||||
for _, v := range tx.Viewers(pos) {
|
||||
v.ViewEntityState(e)
|
||||
}
|
||||
}
|
||||
|
||||
if int16(e.Age()/(time.Second*20))%10 != 0 {
|
||||
// Area effect clouds only trigger updates every ten ticks.
|
||||
return nil
|
||||
}
|
||||
|
||||
for target, expiration := range a.targets {
|
||||
if e.Age() >= expiration {
|
||||
delete(a.targets, target)
|
||||
}
|
||||
}
|
||||
if a.applyEffects(pos, e, a.filter(tx.EntitiesWithin(e.H().Type().BBox(e).Translate(pos)))) {
|
||||
for _, v := range tx.Viewers(pos) {
|
||||
v.ViewEntityState(e)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AreaEffectCloudBehaviour) filter(seq iter.Seq[world.Entity]) iter.Seq[world.Entity] {
|
||||
return func(yield func(world.Entity) bool) {
|
||||
for e := range seq {
|
||||
_, target := a.targets[e.H()]
|
||||
_, living := e.(Living)
|
||||
if !living || target {
|
||||
continue
|
||||
}
|
||||
if !yield(e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// applyEffects applies the effects of an area effect cloud at pos to all
|
||||
// entities passed if they were within the radius and don't have an active
|
||||
// cooldown period.
|
||||
func (a *AreaEffectCloudBehaviour) applyEffects(pos mgl64.Vec3, ent *Ent, entities iter.Seq[world.Entity]) bool {
|
||||
var update bool
|
||||
for e := range entities {
|
||||
delta := e.Position().Sub(pos)
|
||||
delta[1] = 0
|
||||
if delta.Len() <= a.radius {
|
||||
l := e.(Living)
|
||||
for _, eff := range a.Effects() {
|
||||
if lasting, ok := eff.Type().(effect.LastingType); ok {
|
||||
l.AddEffect(effect.New(lasting, eff.Level(), eff.Duration()/4))
|
||||
continue
|
||||
}
|
||||
l.AddEffect(eff)
|
||||
}
|
||||
|
||||
a.targets[e.H()] = ent.Age() + a.conf.ReapplicationDelay
|
||||
a.subtractUseDuration()
|
||||
a.subtractUseRadius()
|
||||
|
||||
update = true
|
||||
}
|
||||
}
|
||||
return update
|
||||
}
|
||||
|
||||
// subtractTickRadius grows the cloud's radius by the radiusTickGrowth value. If the
|
||||
// radius goes under 1/2, it will close the entity.
|
||||
func (a *AreaEffectCloudBehaviour) subtractTickRadius() bool {
|
||||
a.radius += a.conf.RadiusTickGrowth
|
||||
if a.radius < 0.5 {
|
||||
a.stationary.close = true
|
||||
}
|
||||
return a.conf.RadiusTickGrowth != 0
|
||||
}
|
||||
|
||||
// subtractUseDuration grows duration by the durationUseGrowth factor. If duration
|
||||
// goes under zero, it will close the entity.
|
||||
func (a *AreaEffectCloudBehaviour) subtractUseDuration() {
|
||||
a.duration += a.conf.DurationUseGrowth
|
||||
if a.duration <= 0 {
|
||||
a.stationary.close = true
|
||||
}
|
||||
}
|
||||
|
||||
// subtractUseRadius grows radius by the radiusUseGrowth factor. If radius goes
|
||||
// under 1/2, it will close the entity.
|
||||
func (a *AreaEffectCloudBehaviour) subtractUseRadius() {
|
||||
a.radius += a.conf.RadiusUseGrowth
|
||||
if a.radius <= 0.5 {
|
||||
a.stationary.close = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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/enchantment"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/sound"
|
||||
)
|
||||
|
||||
// NewArrow creates a new Arrow and returns it. It is equivalent to calling NewTippedArrow with `potion.Potion{}` as
|
||||
// tip.
|
||||
func NewArrow(opts world.EntitySpawnOpts, owner world.Entity) *world.EntityHandle {
|
||||
return NewTippedArrowWithDamage(opts, 2.0, owner, potion.Potion{})
|
||||
}
|
||||
|
||||
// NewArrowWithDamage creates a new Arrow with the given base damage, and returns it. It is equivalent to calling
|
||||
// NewTippedArrowWithDamage with `potion.Potion{}` as tip.
|
||||
func NewArrowWithDamage(opts world.EntitySpawnOpts, damage float64, owner world.Entity) *world.EntityHandle {
|
||||
return NewTippedArrowWithDamage(opts, damage, owner, potion.Potion{})
|
||||
}
|
||||
|
||||
// NewTippedArrow creates a new Arrow with a potion effect added to an entity when hit.
|
||||
func NewTippedArrow(opts world.EntitySpawnOpts, owner world.Entity, tip potion.Potion) *world.EntityHandle {
|
||||
return NewTippedArrowWithDamage(opts, 2.0, owner, tip)
|
||||
}
|
||||
|
||||
// NewTippedArrowWithDamage creates a new Arrow with a potion effect added to an entity when hit and, and returns it.
|
||||
// It uses the given damage as the base damage.
|
||||
func NewTippedArrowWithDamage(opts world.EntitySpawnOpts, damage float64, owner world.Entity, tip potion.Potion) *world.EntityHandle {
|
||||
conf := arrowConf
|
||||
conf.Damage = damage
|
||||
conf.Potion = tip
|
||||
conf.Owner = owner.H()
|
||||
return opts.New(ArrowType, conf)
|
||||
}
|
||||
|
||||
var arrowConf = ProjectileBehaviourConfig{
|
||||
Gravity: 0.05,
|
||||
Drag: 0.01,
|
||||
Damage: 2.0,
|
||||
Sound: sound.ArrowHit{},
|
||||
SurviveBlockCollision: true,
|
||||
}
|
||||
|
||||
// boolByte returns 1 if the bool passed is true, or 0 if it is false.
|
||||
func boolByte(b bool) uint8 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ArrowType is a world.EntityType implementation for Arrow.
|
||||
var ArrowType arrowType
|
||||
|
||||
type arrowType struct{}
|
||||
|
||||
func (t arrowType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (arrowType) EncodeEntity() string { return "minecraft:arrow" }
|
||||
func (arrowType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
|
||||
func (arrowType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
conf := arrowConf
|
||||
conf.Damage = float64(nbtconv.Float32(m, "Damage"))
|
||||
conf.Potion = potion.From(nbtconv.Int32(m, "auxValue") - 1)
|
||||
conf.DisablePickup = !nbtconv.Bool(m, "player")
|
||||
if !nbtconv.Bool(m, "isCreative") {
|
||||
conf.PickupItem = item.NewStack(item.Arrow{Tip: conf.Potion}, 1)
|
||||
}
|
||||
conf.KnockBackForceAddend = enchantment.Punch.KnockBackMultiplier() * float64(nbtconv.Uint8(m, "enchantPunch"))
|
||||
conf.CollisionPosition = nbtconv.Pos(m, "StuckToBlockPos")
|
||||
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
func (arrowType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
b := data.Data.(*ProjectileBehaviour)
|
||||
m := map[string]any{
|
||||
"Damage": float32(b.conf.Damage),
|
||||
"enchantPunch": byte(b.conf.KnockBackForceAddend / enchantment.Punch.KnockBackMultiplier()),
|
||||
"auxValue": int32(b.conf.Potion.Uint8() + 1),
|
||||
"player": boolByte(!b.conf.DisablePickup),
|
||||
"isCreative": boolByte(b.conf.PickupItem.Empty()),
|
||||
}
|
||||
// TODO: Save critical flag if Minecraft ever saves it?
|
||||
if b.collided {
|
||||
m["StuckToBlockPos"] = nbtconv.PosToInt32Slice(b.collisionPos)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/block/cube/trace"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/particle"
|
||||
"github.com/df-mc/dragonfly/server/world/sound"
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
// NewBottleOfEnchanting ...
|
||||
func NewBottleOfEnchanting(opts world.EntitySpawnOpts, owner world.Entity) *world.EntityHandle {
|
||||
conf := bottleOfEnchantingConf
|
||||
conf.Owner = owner.H()
|
||||
return opts.New(BottleOfEnchantingType, conf)
|
||||
}
|
||||
|
||||
var bottleOfEnchantingConf = ProjectileBehaviourConfig{
|
||||
Gravity: 0.07,
|
||||
Drag: 0.01,
|
||||
Particle: particle.Splash{},
|
||||
Sound: sound.GlassBreak{},
|
||||
Hit: spawnExperience,
|
||||
Damage: -1,
|
||||
}
|
||||
|
||||
// spawnExperience spawns experience orbs with a value of 3-11 at the target of
|
||||
// a trace.Result.
|
||||
func spawnExperience(_ *Ent, tx *world.Tx, target trace.Result) {
|
||||
for _, orb := range NewExperienceOrbs(target.Position(), rand.IntN(9)+3) {
|
||||
tx.AddEntity(orb)
|
||||
}
|
||||
}
|
||||
|
||||
// BottleOfEnchantingType is a world.EntityType for BottleOfEnchanting.
|
||||
var BottleOfEnchantingType bottleOfEnchantingType
|
||||
|
||||
type bottleOfEnchantingType struct{}
|
||||
|
||||
func (t bottleOfEnchantingType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
// Glint returns true if the bottle should render with glint. It always returns
|
||||
// true for bottles of enchanting.
|
||||
func (bottleOfEnchantingType) Glint() bool {
|
||||
return true
|
||||
}
|
||||
func (bottleOfEnchantingType) EncodeEntity() string {
|
||||
return "minecraft:xp_bottle"
|
||||
}
|
||||
func (bottleOfEnchantingType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
|
||||
func (bottleOfEnchantingType) DecodeNBT(_ map[string]any, data *world.EntityData) {
|
||||
data.Data = bottleOfEnchantingConf.New()
|
||||
}
|
||||
|
||||
func (bottleOfEnchantingType) EncodeNBT(_ *world.EntityData) map[string]any {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/enchantment"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
)
|
||||
|
||||
type (
|
||||
// AttackDamageSource is used for damage caused by other entities, for
|
||||
// example when a player attacks another player.
|
||||
AttackDamageSource struct {
|
||||
// Attacker holds the attacking entity. The entity may be a player or
|
||||
// any other entity.
|
||||
Attacker world.Entity
|
||||
}
|
||||
|
||||
// VoidDamageSource is used for damage caused by an entity being in the
|
||||
// void.
|
||||
VoidDamageSource struct{}
|
||||
|
||||
// SuffocationDamageSource is used for damage caused by an entity
|
||||
// suffocating in a block.
|
||||
SuffocationDamageSource struct{}
|
||||
|
||||
// DrowningDamageSource is used for damage caused by an entity drowning in
|
||||
// water.
|
||||
DrowningDamageSource struct{}
|
||||
|
||||
// FallDamageSource is used for damage caused by falling.
|
||||
FallDamageSource struct{}
|
||||
|
||||
// GlideDamageSource is used for damage caused by gliding into a block.
|
||||
GlideDamageSource struct{}
|
||||
|
||||
// LightningDamageSource is used for damage caused by being struck by
|
||||
// lightning.
|
||||
LightningDamageSource struct{}
|
||||
|
||||
// ProjectileDamageSource is used for damage caused by a projectile.
|
||||
ProjectileDamageSource struct {
|
||||
// Projectile and Owner are the world.Entity that dealt the damage and
|
||||
// the one that fired the projectile respectively.
|
||||
Projectile, Owner world.Entity
|
||||
}
|
||||
|
||||
// ExplosionDamageSource is used for damage caused by an explosion.
|
||||
ExplosionDamageSource struct{}
|
||||
)
|
||||
|
||||
func (FallDamageSource) ReducedByArmour() bool { return false }
|
||||
func (FallDamageSource) ReducedByResistance() bool { return true }
|
||||
func (FallDamageSource) Fire() bool { return false }
|
||||
func (FallDamageSource) AffectedByEnchantment(e item.EnchantmentType) bool {
|
||||
return e == enchantment.FeatherFalling
|
||||
}
|
||||
func (FallDamageSource) IgnoreTotem() bool { return false }
|
||||
func (GlideDamageSource) ReducedByArmour() bool { return false }
|
||||
func (GlideDamageSource) ReducedByResistance() bool { return true }
|
||||
func (GlideDamageSource) Fire() bool { return false }
|
||||
func (GlideDamageSource) IgnoreTotem() bool { return false }
|
||||
func (LightningDamageSource) ReducedByArmour() bool { return true }
|
||||
func (LightningDamageSource) ReducedByResistance() bool { return true }
|
||||
func (LightningDamageSource) Fire() bool { return false }
|
||||
func (LightningDamageSource) IgnoreTotem() bool { return false }
|
||||
func (AttackDamageSource) ReducedByArmour() bool { return true }
|
||||
func (AttackDamageSource) ReducedByResistance() bool { return true }
|
||||
func (AttackDamageSource) Fire() bool { return false }
|
||||
func (AttackDamageSource) IgnoreTotem() bool { return false }
|
||||
func (VoidDamageSource) ReducedByResistance() bool { return false }
|
||||
func (VoidDamageSource) ReducedByArmour() bool { return false }
|
||||
func (VoidDamageSource) Fire() bool { return false }
|
||||
func (VoidDamageSource) IgnoreTotem() bool { return true }
|
||||
func (SuffocationDamageSource) ReducedByResistance() bool { return false }
|
||||
func (SuffocationDamageSource) ReducedByArmour() bool { return false }
|
||||
func (SuffocationDamageSource) Fire() bool { return false }
|
||||
func (SuffocationDamageSource) IgnoreTotem() bool { return false }
|
||||
func (DrowningDamageSource) ReducedByResistance() bool { return false }
|
||||
func (DrowningDamageSource) ReducedByArmour() bool { return false }
|
||||
func (DrowningDamageSource) Fire() bool { return false }
|
||||
func (DrowningDamageSource) IgnoreTotem() bool { return false }
|
||||
func (ProjectileDamageSource) ReducedByResistance() bool { return true }
|
||||
func (ProjectileDamageSource) ReducedByArmour() bool { return true }
|
||||
func (ProjectileDamageSource) Fire() bool { return false }
|
||||
func (ProjectileDamageSource) AffectedByEnchantment(e item.EnchantmentType) bool {
|
||||
return e == enchantment.ProjectileProtection
|
||||
}
|
||||
func (ProjectileDamageSource) IgnoreTotem() bool { return false }
|
||||
func (ExplosionDamageSource) ReducedByResistance() bool { return true }
|
||||
func (ExplosionDamageSource) ReducedByArmour() bool { return true }
|
||||
func (ExplosionDamageSource) Fire() bool { return false }
|
||||
func (ExplosionDamageSource) AffectedByEnchantment(e item.EnchantmentType) bool {
|
||||
return e == enchantment.BlastProtection
|
||||
}
|
||||
func (ExplosionDamageSource) IgnoreTotem() bool { return false }
|
||||
@@ -0,0 +1,22 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
// Eyed represents an entity that has eyes.
|
||||
type Eyed interface {
|
||||
// EyeHeight returns the offset from their base position that the eyes of an entity are found at.
|
||||
EyeHeight() float64
|
||||
}
|
||||
|
||||
// EyePosition returns the position of the eyes of the entity if the entity implements entity.Eyed, or the
|
||||
// actual position if it doesn't.
|
||||
func EyePosition(e world.Entity) mgl64.Vec3 {
|
||||
pos := e.Position()
|
||||
if eyed, ok := e.(Eyed); ok {
|
||||
pos[1] += eyed.EyeHeight()
|
||||
}
|
||||
return pos
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// EffectManager manages the effects of an entity. The effect manager will only store effects that last for
|
||||
// a specific duration. Instant effects are applied instantly and not stored.
|
||||
type EffectManager struct {
|
||||
initialEffects []effect.Effect
|
||||
effects map[reflect.Type]effect.Effect
|
||||
}
|
||||
|
||||
// NewEffectManager creates and returns a new initialised EffectManager.
|
||||
func NewEffectManager(eff ...effect.Effect) *EffectManager {
|
||||
return &EffectManager{effects: make(map[reflect.Type]effect.Effect), initialEffects: eff}
|
||||
}
|
||||
|
||||
// Add adds an effect to the manager. If the effect is instant, it is applied to the Living entity passed
|
||||
// immediately. If not, the effect is added to the EffectManager and is applied to the entity every time the
|
||||
// Tick method is called.
|
||||
// Effect levels of 0 or below will not do anything.
|
||||
// Effect returns the final effect it added to the entity. That might be the effect passed or an effect with
|
||||
// a higher level/duration than the one passed. Add panics if the effect has a negative duration or level.
|
||||
func (m *EffectManager) Add(e effect.Effect, entity Living) effect.Effect {
|
||||
lvl, dur := e.Level(), e.Duration()
|
||||
if lvl <= 0 {
|
||||
panic(fmt.Sprintf("(*EffectManager).Add: effect cannot have level of 0 or below: %v", lvl))
|
||||
}
|
||||
if dur < 0 {
|
||||
panic(fmt.Sprintf("(*EffectManager).Add: effect cannot have negative duration: %v", dur))
|
||||
}
|
||||
|
||||
m.flushInitialEffects(entity)
|
||||
|
||||
t, ok := e.Type().(effect.LastingType)
|
||||
if !ok {
|
||||
e.Type().Apply(entity, e)
|
||||
return e
|
||||
}
|
||||
typ := reflect.TypeOf(e.Type())
|
||||
|
||||
existing, ok := m.effects[typ]
|
||||
if !ok {
|
||||
m.effects[typ] = e
|
||||
|
||||
t.Start(entity, lvl)
|
||||
return e
|
||||
}
|
||||
if existing.Level() > lvl || (existing.Level() == lvl && ((existing.Duration() > dur && !e.Infinite()) || existing.Infinite())) {
|
||||
return existing
|
||||
}
|
||||
m.effects[typ] = e
|
||||
|
||||
existing.Type().(effect.LastingType).End(entity, existing.Level())
|
||||
t.Start(entity, lvl)
|
||||
return e
|
||||
}
|
||||
|
||||
// Remove removes any Effect present in the EffectManager with the type of the effect passed.
|
||||
func (m *EffectManager) Remove(e effect.Type, entity Living) {
|
||||
m.flushInitialEffects(entity)
|
||||
|
||||
t := reflect.TypeOf(e)
|
||||
if existing, ok := m.effects[t]; ok {
|
||||
delete(m.effects, t)
|
||||
existing.Type().(effect.LastingType).End(entity, existing.Level())
|
||||
}
|
||||
}
|
||||
|
||||
// Effect returns the effect instance and true if the entity has the effect. If not found, it will return an empty
|
||||
// effect instance and false.
|
||||
func (m *EffectManager) Effect(e effect.Type) (effect.Effect, bool) {
|
||||
for _, eff := range m.initialEffects {
|
||||
if eff.Type() == e {
|
||||
return eff, true
|
||||
}
|
||||
}
|
||||
|
||||
existing, ok := m.effects[reflect.TypeOf(e)]
|
||||
return existing, ok
|
||||
}
|
||||
|
||||
// Effects returns a list of all effects currently present in the effect manager. This will never include
|
||||
// effects that have expired.
|
||||
func (m *EffectManager) Effects() []effect.Effect {
|
||||
return append(slices.Collect(maps.Values(m.effects)), m.initialEffects...)
|
||||
}
|
||||
|
||||
// Tick ticks the EffectManager, applying all of its effects to the Living entity passed when applicable and
|
||||
// removing expired effects.
|
||||
func (m *EffectManager) Tick(entity Living, tx *world.Tx) {
|
||||
update := false
|
||||
|
||||
m.flushInitialEffects(entity)
|
||||
|
||||
for i, eff := range m.effects {
|
||||
if m.expired(eff) {
|
||||
delete(m.effects, i)
|
||||
eff.Type().(effect.LastingType).End(entity, eff.Level())
|
||||
update = true
|
||||
continue
|
||||
}
|
||||
eff.Type().Apply(entity, eff)
|
||||
m.effects[i] = eff.TickDuration()
|
||||
}
|
||||
|
||||
if update {
|
||||
for _, v := range tx.Viewers(entity.Position()) {
|
||||
v.ViewEntityState(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushInitialEffects flushes the initial effects, applying them onto the Living entity passed.
|
||||
func (m *EffectManager) flushInitialEffects(entity Living) {
|
||||
initialEffects := m.initialEffects
|
||||
m.initialEffects = nil
|
||||
for _, e := range initialEffects {
|
||||
m.Add(e, entity)
|
||||
}
|
||||
}
|
||||
|
||||
// expired checks if an Effect has expired.
|
||||
func (m *EffectManager) expired(e effect.Effect) bool {
|
||||
return e.Duration() <= 0 && !e.Infinite()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Absorption is a lasting effect that increases the health of an entity over
|
||||
// the maximum. Once this extra health is lost, it will not regenerate.
|
||||
var Absorption absorption
|
||||
|
||||
type absorption struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Start ...
|
||||
func (absorption) Start(e world.Entity, lvl int) {
|
||||
if i, ok := e.(interface {
|
||||
SetAbsorption(health float64)
|
||||
}); ok {
|
||||
i.SetAbsorption(4 * float64(lvl))
|
||||
}
|
||||
}
|
||||
|
||||
// End ...
|
||||
func (absorption) End(e world.Entity, _ int) {
|
||||
if i, ok := e.(interface {
|
||||
SetAbsorption(health float64)
|
||||
}); ok {
|
||||
i.SetAbsorption(0)
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (absorption) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x25, G: 0x52, B: 0xa5, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Blindness is a lasting effect that greatly reduces the vision range of the
|
||||
// entity affected.
|
||||
var Blindness blindness
|
||||
|
||||
type blindness struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (blindness) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x1f, G: 0x1f, B: 0x23, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// ConduitPower is a lasting effect that grants the affected entity the ability
|
||||
// to breathe underwater and allows the entity to break faster when underwater
|
||||
// or in the rain. (Similarly to haste.)
|
||||
var ConduitPower conduitPower
|
||||
|
||||
type conduitPower struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Multiplier returns the mining speed multiplier from this effect.
|
||||
func (conduitPower) Multiplier(lvl int) float64 {
|
||||
v := 1 - float64(lvl)*0.1
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (conduitPower) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x1d, G: 0xc2, B: 0xd1, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Darkness is a lasting effect that causes the player's vision to dim
|
||||
// occasionally.
|
||||
var Darkness darkness
|
||||
|
||||
type darkness struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (darkness) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x29, G: 0x27, B: 0x21, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LastingType represents an effect type that can have a duration. An effect
|
||||
// can be made using it by calling effect.New with the LastingType.
|
||||
type LastingType interface {
|
||||
Type
|
||||
// Start is called for lasting effects when they are initially added.
|
||||
Start(e world.Entity, lvl int)
|
||||
// End is called for lasting effects when they are removed.
|
||||
End(e world.Entity, lvl int)
|
||||
}
|
||||
|
||||
// Type is an effect implementation that can be applied to an entity.
|
||||
type Type interface {
|
||||
// RGBA returns the colour of the effect. If multiple effects are present,
|
||||
// the colours will be mixed together to form a new colour.
|
||||
RGBA() color.RGBA
|
||||
// Apply applies the effect to an entity. Apply is called only once for
|
||||
// instant effects, such as instantHealth, while it is called every tick for
|
||||
// lasting effects. The Effect holding the Type is passed along with the
|
||||
// current tick.
|
||||
Apply(e world.Entity, eff Effect)
|
||||
}
|
||||
|
||||
// Effect is an effect that can be added to an entity. Effects are either
|
||||
// instant (applying the effect only once) or lasting (applying the effect
|
||||
// every tick).
|
||||
type Effect struct {
|
||||
t Type
|
||||
d time.Duration
|
||||
lvl int
|
||||
potency float64
|
||||
ambient, particlesHidden bool
|
||||
infinite bool
|
||||
tick int
|
||||
}
|
||||
|
||||
// NewInstant returns a new instant Effect using the Type passed. The effect
|
||||
// will be applied to an entity once and will expire immediately after.
|
||||
// NewInstant creates an Effect with a potency of 1.0.
|
||||
func NewInstant(t Type, lvl int) Effect {
|
||||
return NewInstantWithPotency(t, lvl, 1)
|
||||
}
|
||||
|
||||
// NewInstantWithPotency returns a new instant Effect using the Type and level
|
||||
// passed. The effect will be applied to an entity once and expire immediately
|
||||
// after. The potency passed additionally influences the strength of the effect.
|
||||
// A higher potency (> 1.0) increases the effect power, while a lower potency
|
||||
// (< 1.0) reduces it.
|
||||
func NewInstantWithPotency(t Type, lvl int, potency float64) Effect {
|
||||
return Effect{t: t, lvl: lvl, potency: potency}
|
||||
}
|
||||
|
||||
// New creates a new Effect using a LastingType passed. Once added to an entity, the time.Duration passed will be ticked down
|
||||
// by the entity until it reaches a duration of 0.
|
||||
func New(t LastingType, lvl int, d time.Duration) Effect {
|
||||
return Effect{t: t, lvl: lvl, d: d}
|
||||
}
|
||||
|
||||
// NewAmbient creates a new ambient (reduced particles, as when using a beacon) Effect using a LastingType passed. Once added
|
||||
// to an entity, the time.Duration passed will be ticked down by the entity until it reaches a duration of 0.
|
||||
func NewAmbient(t LastingType, lvl int, d time.Duration) Effect {
|
||||
return Effect{t: t, lvl: lvl, d: d, ambient: true}
|
||||
}
|
||||
|
||||
// NewInfinite creates a new Effect using a LastingType passed. Once added to an entity, the effect will persist indefinitely,
|
||||
// until the effect is removed.
|
||||
func NewInfinite(t LastingType, lvl int) Effect {
|
||||
return Effect{t: t, lvl: lvl, infinite: true}
|
||||
}
|
||||
|
||||
// WithoutParticles returns the same Effect with particles disabled. Adding the effect to players will not display the
|
||||
// particles around the player.
|
||||
func (e Effect) WithoutParticles() Effect {
|
||||
e.particlesHidden = true
|
||||
return e
|
||||
}
|
||||
|
||||
// ParticlesHidden returns true if the Effect had its particles hidden by calling WithoutParticles.
|
||||
func (e Effect) ParticlesHidden() bool {
|
||||
return e.particlesHidden
|
||||
}
|
||||
|
||||
// Level returns the level of the Effect.
|
||||
func (e Effect) Level() int {
|
||||
return e.lvl
|
||||
}
|
||||
|
||||
// Duration returns the leftover duration of the Effect. The duration returned is always 0 if NewInstant or NewInfinite
|
||||
// were used to create the effect.
|
||||
func (e Effect) Duration() time.Duration {
|
||||
return e.d
|
||||
}
|
||||
|
||||
// Ambient returns whether the Effect is an ambient effect, leading to reduced particles shown to the client. False is
|
||||
// always returned if the Effect was created using New or NewInstant.
|
||||
func (e Effect) Ambient() bool {
|
||||
return e.ambient
|
||||
}
|
||||
|
||||
// Infinite returns if the Effect duration is infinite.
|
||||
func (e Effect) Infinite() bool {
|
||||
return e.infinite
|
||||
}
|
||||
|
||||
// Type returns the underlying type of the Effect. It is either of the type Type or LastingType, depending on whether it
|
||||
// was created using New or NewAmbient, or NewInstant.
|
||||
func (e Effect) Type() Type {
|
||||
return e.t
|
||||
}
|
||||
|
||||
// TickDuration ticks the effect duration, subtracting time.Second/20 from the leftover time and returning the resulting
|
||||
// Effect.
|
||||
func (e Effect) TickDuration() Effect {
|
||||
if _, ok := e.t.(LastingType); ok {
|
||||
if !e.Infinite() {
|
||||
e.d -= time.Second / 20
|
||||
}
|
||||
e.tick++
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Tick returns the current tick of the Effect. This is the number of ticks that
|
||||
// the Effect has been applied for.
|
||||
func (e Effect) Tick() int {
|
||||
return e.tick
|
||||
}
|
||||
|
||||
// nopLasting is a lasting effect with no (server-side) behaviour. It does not implement the RGBA method.
|
||||
type nopLasting struct{}
|
||||
|
||||
func (nopLasting) Apply(world.Entity, Effect) {}
|
||||
func (nopLasting) End(world.Entity, int) {}
|
||||
func (nopLasting) Start(world.Entity, int) {}
|
||||
|
||||
// ResultingColour calculates the resulting colour of the effects passed and returns a bool specifying if the
|
||||
// effects were ambient effects, which will cause their particles to display less frequently.
|
||||
func ResultingColour(effects []Effect) (color.RGBA, bool) {
|
||||
r, g, b, a, n := 0, 0, 0, 0, 0
|
||||
ambient := true
|
||||
for _, e := range effects {
|
||||
if e.particlesHidden {
|
||||
// Don't take effects with hidden particles into account for colour
|
||||
// calculation: Their particles are hidden after all.
|
||||
continue
|
||||
}
|
||||
c := e.Type().RGBA()
|
||||
r += int(c.R)
|
||||
g += int(c.G)
|
||||
b += int(c.B)
|
||||
a += int(c.A)
|
||||
n++
|
||||
if !e.Ambient() {
|
||||
ambient = false
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return color.RGBA{R: 0x38, G: 0x5d, B: 0xc6, A: 0xff}, false
|
||||
}
|
||||
return color.RGBA{R: uint8(r / n), G: uint8(g / n), B: uint8(b / n), A: uint8(a / n)}, ambient
|
||||
}
|
||||
|
||||
// living represents a living entity that has health and the ability to move around.
|
||||
type living interface {
|
||||
world.Entity
|
||||
// Health returns the health of the entity.
|
||||
Health() float64
|
||||
// MaxHealth returns the maximum health of the entity.
|
||||
MaxHealth() float64
|
||||
// SetMaxHealth changes the maximum health of the entity to the value passed.
|
||||
SetMaxHealth(v float64)
|
||||
// Hurt hurts the entity for a given amount of damage. The source passed represents the cause of the
|
||||
// damage, for example entity.AttackDamageSource if the entity is attacked by another entity.
|
||||
// If the final damage exceeds the health that the player currently has, the entity is killed.
|
||||
Hurt(damage float64, source world.DamageSource) (n float64, vulnerable bool)
|
||||
// Heal heals the entity for a given amount of health. The source passed represents the cause of the
|
||||
// healing, for example entity.FoodHealingSource if the entity healed by having a full food bar. If the health
|
||||
// added to the original health exceeds the entity's max health, Heal may not add the full amount.
|
||||
Heal(health float64, source world.HealingSource)
|
||||
// speed returns the current speed of the living entity. The default value is different for each entity.
|
||||
Speed() float64
|
||||
// SetSpeed sets the speed of an entity to a new value.
|
||||
SetSpeed(float64)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// FatalPoison is a lasting effect that causes the affected entity to lose
|
||||
// health gradually. fatalPoison, unlike poison, can kill the entity it is
|
||||
// applied to.
|
||||
var FatalPoison fatalPoison
|
||||
|
||||
type fatalPoison struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Apply ...
|
||||
func (fatalPoison) Apply(e world.Entity, eff Effect) {
|
||||
interval := max(50>>(eff.Level()-1), 1)
|
||||
if eff.Tick()%interval == 0 {
|
||||
if l, ok := e.(living); ok {
|
||||
l.Hurt(1, PoisonDamageSource{Fatal: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (fatalPoison) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x4e, G: 0x93, B: 0x31, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// FireResistance is a lasting effect that grants immunity to fire & lava damage.
|
||||
var FireResistance fireResistance
|
||||
|
||||
type fireResistance struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (fireResistance) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xff, G: 0x99, B: 0x00, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Haste is a lasting effect that increases the mining speed of a player by 20%
|
||||
// for each level of the effect.
|
||||
var Haste haste
|
||||
|
||||
type haste struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Multiplier returns the mining speed multiplier from this effect.
|
||||
func (haste) Multiplier(lvl int) float64 {
|
||||
v := 1 - float64(lvl)*0.1
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (haste) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xd9, G: 0xc0, B: 0x43, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// HealthBoost causes the affected entity to have its maximum health changed
|
||||
// for a specific duration.
|
||||
var HealthBoost healthBoost
|
||||
|
||||
type healthBoost struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Start ...
|
||||
func (healthBoost) Start(e world.Entity, lvl int) {
|
||||
if l, ok := e.(living); ok {
|
||||
l.SetMaxHealth(l.MaxHealth() + 4*float64(lvl))
|
||||
}
|
||||
}
|
||||
|
||||
// End ...
|
||||
func (healthBoost) End(e world.Entity, lvl int) {
|
||||
if l, ok := e.(living); ok {
|
||||
l.SetMaxHealth(l.MaxHealth() - 4*float64(lvl))
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (healthBoost) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xf8, G: 0x7d, B: 0x23, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Hunger is a lasting effect that causes an affected player to gradually lose
|
||||
// saturation and food.
|
||||
var Hunger hunger
|
||||
|
||||
type hunger struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Apply ...
|
||||
func (hunger) Apply(e world.Entity, eff Effect) {
|
||||
if i, ok := e.(interface {
|
||||
Exhaust(points float64)
|
||||
}); ok {
|
||||
i.Exhaust(float64(eff.Level()) * 0.005)
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (hunger) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x58, G: 0x76, B: 0x53, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// InstantDamage is an instant effect that causes a living entity to
|
||||
// immediately take some damage, depending on the level and the potency of the
|
||||
// effect.
|
||||
var InstantDamage instantDamage
|
||||
|
||||
type instantDamage struct{}
|
||||
|
||||
// Apply ...
|
||||
func (i instantDamage) Apply(e world.Entity, eff Effect) {
|
||||
base := 3 << eff.Level()
|
||||
if l, ok := e.(living); ok {
|
||||
l.Hurt(float64(base)*eff.potency, InstantDamageSource{})
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (instantDamage) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xa9, G: 0x65, B: 0x6a, A: 0xff}
|
||||
}
|
||||
|
||||
// InstantDamageSource is used for damage caused by an effect.instantDamage
|
||||
// applied to an entity.
|
||||
type InstantDamageSource struct{}
|
||||
|
||||
func (InstantDamageSource) ReducedByArmour() bool { return false }
|
||||
func (InstantDamageSource) ReducedByResistance() bool { return true }
|
||||
func (InstantDamageSource) Fire() bool { return false }
|
||||
func (InstantDamageSource) IgnoreTotem() bool { return false }
|
||||
@@ -0,0 +1,33 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// InstantHealth is an instant effect that causes the player that it is applied
|
||||
// to immediately regain some health. The amount of health regained depends on
|
||||
// the effect level and potency.
|
||||
var InstantHealth instantHealth
|
||||
|
||||
type instantHealth struct{}
|
||||
|
||||
// Apply instantly heals the world.Entity passed for a bit of health, depending on the effect level and
|
||||
// potency.
|
||||
func (i instantHealth) Apply(e world.Entity, eff Effect) {
|
||||
base := 2 << eff.Level()
|
||||
if l, ok := e.(living); ok {
|
||||
l.Heal(float64(base)*eff.potency, InstantHealingSource{})
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (instantHealth) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xf8, G: 0x24, B: 0x23, A: 0xff}
|
||||
}
|
||||
|
||||
// InstantHealingSource is a healing source used when an entity regains
|
||||
// health from an effect.instantHealth.
|
||||
type InstantHealingSource struct{}
|
||||
|
||||
func (InstantHealingSource) HealingSource() {}
|
||||
@@ -0,0 +1,40 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Invisibility is a lasting effect that causes the affected entity to turn
|
||||
// invisible. While invisible, the entity's armour is still visible and effect
|
||||
// particles will still be displayed.
|
||||
var Invisibility invisibility
|
||||
|
||||
type invisibility struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Start ...
|
||||
func (invisibility) Start(e world.Entity, _ int) {
|
||||
if i, ok := e.(interface {
|
||||
SetInvisible()
|
||||
SetVisible()
|
||||
}); ok {
|
||||
i.SetInvisible()
|
||||
}
|
||||
}
|
||||
|
||||
// End ...
|
||||
func (invisibility) End(e world.Entity, _ int) {
|
||||
if i, ok := e.(interface {
|
||||
SetInvisible()
|
||||
SetVisible()
|
||||
}); ok {
|
||||
i.SetVisible()
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (invisibility) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xf6, G: 0xf6, B: 0xf6, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// JumpBoost is a lasting effect that causes the affected entity to be able to
|
||||
// jump much higher, depending on the level of the effect.
|
||||
var JumpBoost jumpBoost
|
||||
|
||||
type jumpBoost struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (jumpBoost) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xfd, G: 0xff, B: 0x84, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Levitation is a lasting effect that causes the affected entity to slowly
|
||||
// levitate upwards. It is roughly the opposite of the slowFalling effect.
|
||||
var Levitation levitation
|
||||
|
||||
type levitation struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (levitation) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xce, G: 0xff, B: 0xff, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
)
|
||||
|
||||
// MiningFatigue is a lasting effect that decreases the mining speed of a
|
||||
// player by 10% for each level of the effect.
|
||||
var MiningFatigue miningFatigue
|
||||
|
||||
type miningFatigue struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Multiplier returns the mining speed multiplier from this effect.
|
||||
func (miningFatigue) Multiplier(lvl int) float64 {
|
||||
return math.Pow(3, float64(lvl))
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (miningFatigue) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x4a, G: 0x42, B: 0x17, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Nausea is a lasting effect that causes the screen to warp, similarly to when
|
||||
// entering a nether portal.
|
||||
var Nausea nausea
|
||||
|
||||
type nausea struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (nausea) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x55, G: 0x1d, B: 0x4a, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// NightVision is a lasting effect that causes the affected entity to see in
|
||||
// dark places as though they were fully lit up.
|
||||
var NightVision nightVision
|
||||
|
||||
type nightVision struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (nightVision) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xc2, G: 0xff, B: 0x66, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Poison is a lasting effect that causes the affected entity to lose health
|
||||
// gradually. poison cannot kill, unlike fatalPoison.
|
||||
var Poison poison
|
||||
|
||||
type poison struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Apply ...
|
||||
func (poison) Apply(e world.Entity, eff Effect) {
|
||||
interval := max(50>>(eff.Level()-1), 1)
|
||||
if eff.Tick()%interval == 0 {
|
||||
if l, ok := e.(living); ok && l.Health() > 1 {
|
||||
l.Hurt(1, PoisonDamageSource{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (poison) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x87, G: 0xa3, B: 0x63, A: 0xff}
|
||||
}
|
||||
|
||||
// PoisonDamageSource is used for damage caused by an effect.poison or
|
||||
// effect.fatalPoison applied to an entity.
|
||||
type PoisonDamageSource struct {
|
||||
// Fatal specifies if the damage was caused by effect.fatalPoison and if
|
||||
// the damage could therefore kill the entity.
|
||||
Fatal bool
|
||||
}
|
||||
|
||||
func (PoisonDamageSource) ReducedByResistance() bool { return true }
|
||||
func (PoisonDamageSource) ReducedByArmour() bool { return false }
|
||||
func (PoisonDamageSource) Fire() bool { return false }
|
||||
func (PoisonDamageSource) IgnoreTotem() bool { return false }
|
||||
@@ -0,0 +1,36 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Regeneration is an effect that causes the entity that it is added to slowly
|
||||
// regenerate health. The level of the effect influences the speed with which
|
||||
// the entity regenerates.
|
||||
var Regeneration regeneration
|
||||
|
||||
type regeneration struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Apply applies health to the world.Entity passed if the duration of the effect is at the right tick.
|
||||
func (regeneration) Apply(e world.Entity, eff Effect) {
|
||||
interval := max(50>>(eff.Level()-1), 1)
|
||||
if eff.Tick()%interval == 0 {
|
||||
if l, ok := e.(living); ok {
|
||||
l.Heal(1, RegenerationHealingSource{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (regeneration) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xcd, G: 0x5c, B: 0xab, A: 0xff}
|
||||
}
|
||||
|
||||
// RegenerationHealingSource is a healing source used when an entity regenerates
|
||||
// health from an effect.regeneration.
|
||||
type RegenerationHealingSource struct{}
|
||||
|
||||
func (RegenerationHealingSource) HealingSource() {}
|
||||
@@ -0,0 +1,62 @@
|
||||
package effect
|
||||
|
||||
// Register registers an Effect with a specific ID to translate from and to on disk and network. An Effect
|
||||
// instance may be created by creating a struct instance in this package like
|
||||
// effect.regeneration{}.
|
||||
func Register(id int, e Type) {
|
||||
effects[id] = e
|
||||
effectIds[e] = id
|
||||
}
|
||||
|
||||
// init registers all implemented effects.
|
||||
func init() {
|
||||
Register(1, Speed)
|
||||
Register(2, Slowness)
|
||||
Register(3, Haste)
|
||||
Register(4, MiningFatigue)
|
||||
Register(5, Strength)
|
||||
Register(6, InstantHealth)
|
||||
Register(7, InstantDamage)
|
||||
Register(8, JumpBoost)
|
||||
Register(9, Nausea)
|
||||
Register(10, Regeneration)
|
||||
Register(11, Resistance)
|
||||
Register(12, FireResistance)
|
||||
Register(13, WaterBreathing)
|
||||
Register(14, Invisibility)
|
||||
Register(15, Blindness)
|
||||
Register(16, NightVision)
|
||||
Register(17, Hunger)
|
||||
Register(18, Weakness)
|
||||
Register(19, Poison)
|
||||
Register(20, Wither)
|
||||
Register(21, HealthBoost)
|
||||
Register(22, Absorption)
|
||||
Register(23, Saturation)
|
||||
Register(24, Levitation)
|
||||
Register(25, FatalPoison)
|
||||
Register(26, ConduitPower)
|
||||
Register(27, SlowFalling)
|
||||
// TODO: (28) Bad omen. (Requires villages ...)
|
||||
// TODO: (29) Hero of the village. (Requires villages ...)
|
||||
Register(30, Darkness)
|
||||
}
|
||||
|
||||
var (
|
||||
effects = map[int]Type{}
|
||||
effectIds = map[Type]int{}
|
||||
)
|
||||
|
||||
// ByID attempts to return an effect by the ID it was registered with. If found, the effect found
|
||||
// is returned and the bool true.
|
||||
func ByID(id int) (Type, bool) {
|
||||
effect, ok := effects[id]
|
||||
return effect, ok
|
||||
}
|
||||
|
||||
// ID attempts to return the ID an effect was registered with. If found, the id is returned and
|
||||
// the bool true.
|
||||
func ID(e Type) (int, bool) {
|
||||
id, ok := effectIds[e]
|
||||
return id, ok
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Resistance is a lasting effect that reduces the damage taken from any
|
||||
// sources except for void damage or custom damage.
|
||||
var Resistance resistance
|
||||
|
||||
type resistance struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Multiplier returns a damage multiplier for the damage source passed.
|
||||
func (resistance) Multiplier(e world.DamageSource, lvl int) float64 {
|
||||
if !e.ReducedByResistance() {
|
||||
return 1
|
||||
}
|
||||
if v := 1 - 0.2*float64(lvl); v >= 0 {
|
||||
return v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (resistance) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x91, G: 0x46, B: 0xf0, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Saturation is a lasting effect that causes the affected player to regain
|
||||
// food and saturation.
|
||||
var Saturation saturation
|
||||
|
||||
type saturation struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Apply ...
|
||||
func (saturation) Apply(e world.Entity, eff Effect) {
|
||||
if i, ok := e.(interface {
|
||||
Saturate(food int, saturation float64)
|
||||
}); ok {
|
||||
i.Saturate(eff.Level(), 2*float64(eff.Level()))
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (saturation) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xf8, G: 0x24, B: 0x23, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// SlowFalling is a lasting effect that causes the affected entity to fall very
|
||||
// slowly.
|
||||
var SlowFalling slowFalling
|
||||
|
||||
type slowFalling struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (slowFalling) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xf3, G: 0xcf, B: 0xb9, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Slowness is a lasting effect that decreases the movement speed of a living
|
||||
// entity by 15% for each level that the effect has.
|
||||
var Slowness slowness
|
||||
|
||||
type slowness struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Start ...
|
||||
func (slowness) Start(e world.Entity, lvl int) {
|
||||
slowness := 1 - float64(lvl)*0.15
|
||||
if slowness <= 0 {
|
||||
slowness = 0.00001
|
||||
}
|
||||
if l, ok := e.(living); ok {
|
||||
l.SetSpeed(l.Speed() * slowness)
|
||||
}
|
||||
}
|
||||
|
||||
// End ...
|
||||
func (slowness) End(e world.Entity, lvl int) {
|
||||
slowness := 1 - float64(lvl)*0.15
|
||||
if slowness <= 0 {
|
||||
slowness = 0.00001
|
||||
}
|
||||
if l, ok := e.(living); ok {
|
||||
l.SetSpeed(l.Speed() / slowness)
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (slowness) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x8b, G: 0xaf, B: 0xe0, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Speed is a lasting effect that increases the speed of an entity by 20% for
|
||||
// each level that the effect has.
|
||||
var Speed speed
|
||||
|
||||
type speed struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Start ...
|
||||
func (speed) Start(e world.Entity, lvl int) {
|
||||
speed := 1 + float64(lvl)*0.2
|
||||
if l, ok := e.(living); ok {
|
||||
l.SetSpeed(l.Speed() * speed)
|
||||
}
|
||||
}
|
||||
|
||||
// End ...
|
||||
func (speed) End(e world.Entity, lvl int) {
|
||||
speed := 1 + float64(lvl)*0.2
|
||||
if l, ok := e.(living); ok {
|
||||
l.SetSpeed(l.Speed() / speed)
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (speed) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x33, G: 0xeb, B: 0xff, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Strength is a lasting effect that increases the damage dealt with melee
|
||||
// attacks when applied to an entity.
|
||||
var Strength strength
|
||||
|
||||
type strength struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Multiplier returns the damage multiplier of the effect.
|
||||
func (strength) Multiplier(lvl int) float64 {
|
||||
return 0.3 * float64(lvl)
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (strength) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0xff, G: 0xc7, B: 0x00, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// WaterBreathing is a lasting effect that allows the affected entity to breath
|
||||
// underwater until the effect expires.
|
||||
var WaterBreathing waterBreathing
|
||||
|
||||
type waterBreathing struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (waterBreathing) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x98, G: 0xda, B: 0xc0, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Weakness is a lasting effect that reduces the damage dealt to other entities
|
||||
// with melee attacks.
|
||||
var Weakness weakness
|
||||
|
||||
type weakness struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Multiplier returns the damage multiplier of the effect.
|
||||
func (weakness) Multiplier(lvl int) float64 {
|
||||
v := 0.2 * float64(lvl)
|
||||
if v > 1 {
|
||||
v = 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (weakness) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x48, G: 0x4d, B: 0x48, A: 0xff}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Wither is a lasting effect that causes an entity to take continuous damage
|
||||
// that is capable of killing an entity.
|
||||
var Wither wither
|
||||
|
||||
type wither struct {
|
||||
nopLasting
|
||||
}
|
||||
|
||||
// Apply ...
|
||||
func (wither) Apply(e world.Entity, eff Effect) {
|
||||
interval := max(80>>eff.Level(), 1)
|
||||
if eff.Tick()%interval == 0 {
|
||||
if l, ok := e.(living); ok {
|
||||
l.Hurt(1, WitherDamageSource{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RGBA ...
|
||||
func (wither) RGBA() color.RGBA {
|
||||
return color.RGBA{R: 0x73, G: 0x61, B: 0x56, A: 0xff}
|
||||
}
|
||||
|
||||
// WitherDamageSource is used for damage caused by an effect.wither applied
|
||||
// to an entity.
|
||||
type WitherDamageSource struct{}
|
||||
|
||||
func (WitherDamageSource) ReducedByResistance() bool { return true }
|
||||
func (WitherDamageSource) ReducedByArmour() bool { return false }
|
||||
func (WitherDamageSource) Fire() bool { return false }
|
||||
func (WitherDamageSource) IgnoreTotem() bool { return false }
|
||||
@@ -0,0 +1,40 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/particle"
|
||||
)
|
||||
|
||||
// NewEgg creates an Egg entity. Egg is as a throwable entity that can be used
|
||||
// to spawn chicks.
|
||||
func NewEgg(opts world.EntitySpawnOpts, owner world.Entity) *world.EntityHandle {
|
||||
conf := eggConf
|
||||
conf.Owner = owner.H()
|
||||
return opts.New(EggType, conf)
|
||||
}
|
||||
|
||||
// TODO: Spawn chicken(e) 12.5% of the time.
|
||||
var eggConf = ProjectileBehaviourConfig{
|
||||
Gravity: 0.03,
|
||||
Drag: 0.01,
|
||||
Particle: particle.EggSmash{},
|
||||
ParticleCount: 6,
|
||||
}
|
||||
|
||||
// EggType is a world.EntityType implementation for Egg.
|
||||
var EggType eggType
|
||||
|
||||
type eggType struct{}
|
||||
|
||||
func (t eggType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (eggType) EncodeEntity() string { return "minecraft:egg" }
|
||||
func (eggType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
|
||||
func (eggType) DecodeNBT(_ map[string]any, data *world.EntityData) { data.Data = eggConf.New() }
|
||||
func (eggType) EncodeNBT(_ *world.EntityData) map[string]any { return nil }
|
||||
@@ -0,0 +1,61 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/block/cube/trace"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/particle"
|
||||
"github.com/df-mc/dragonfly/server/world/sound"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
// NewEnderPearl creates an EnderPearl entity. EnderPearl is a smooth, greenish-
|
||||
// blue item used to teleport.
|
||||
func NewEnderPearl(opts world.EntitySpawnOpts, owner world.Entity) *world.EntityHandle {
|
||||
conf := enderPearlConf
|
||||
conf.Owner = owner.H()
|
||||
return opts.New(EnderPearlType, conf)
|
||||
}
|
||||
|
||||
var enderPearlConf = ProjectileBehaviourConfig{
|
||||
Gravity: 0.03,
|
||||
Drag: 0.01,
|
||||
Particle: particle.EndermanTeleport{},
|
||||
Sound: sound.Teleport{},
|
||||
Hit: teleport,
|
||||
}
|
||||
|
||||
// teleporter represents a living entity that can teleport.
|
||||
type teleporter interface {
|
||||
// Teleport teleports the entity to the position given.
|
||||
Teleport(pos mgl64.Vec3)
|
||||
Living
|
||||
}
|
||||
|
||||
// teleport teleports the owner of an Ent to a trace.Result's position.
|
||||
func teleport(e *Ent, tx *world.Tx, target trace.Result) {
|
||||
owner, _ := e.Behaviour().(*ProjectileBehaviour).Owner().Entity(tx)
|
||||
if user, ok := owner.(teleporter); ok {
|
||||
tx.PlaySound(user.Position(), sound.Teleport{})
|
||||
user.Teleport(target.Position())
|
||||
user.Hurt(5, FallDamageSource{})
|
||||
}
|
||||
}
|
||||
|
||||
// EnderPearlType is a world.EntityType implementation for EnderPearl.
|
||||
var EnderPearlType enderPearlType
|
||||
|
||||
type enderPearlType struct{}
|
||||
|
||||
func (t enderPearlType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (enderPearlType) EncodeEntity() string { return "minecraft:ender_pearl" }
|
||||
func (enderPearlType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
func (enderPearlType) DecodeNBT(_ map[string]any, data *world.EntityData) {
|
||||
data.Data = enderPearlConf.New()
|
||||
}
|
||||
func (enderPearlType) EncodeNBT(*world.EntityData) map[string]any { return nil }
|
||||
@@ -0,0 +1,141 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Behaviour implements the behaviour of an Ent.
|
||||
type Behaviour interface {
|
||||
// Tick ticks the Ent using the Behaviour. A Movement is returned that
|
||||
// specifies the movement of the entity over the tick. Nil may be returned
|
||||
// if the entity did not move.
|
||||
Tick(e *Ent, tx *world.Tx) *Movement
|
||||
}
|
||||
|
||||
// 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.
|
||||
type Ent struct {
|
||||
tx *world.Tx
|
||||
handle *world.EntityHandle
|
||||
data *world.EntityData
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// Open converts a world.EntityHandle to an Ent in a world.Tx.
|
||||
func Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) *Ent {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (e *Ent) H() *world.EntityHandle {
|
||||
return e.handle
|
||||
}
|
||||
|
||||
func (e *Ent) Behaviour() Behaviour {
|
||||
return e.data.Data.(Behaviour)
|
||||
}
|
||||
|
||||
// Explode propagates the explosion behaviour of the underlying Behaviour.
|
||||
func (e *Ent) Explode(src mgl64.Vec3, impact float64, conf block.ExplosionConfig) {
|
||||
if expl, ok := e.Behaviour().(interface {
|
||||
Explode(e *Ent, src mgl64.Vec3, impact float64, conf block.ExplosionConfig)
|
||||
}); ok {
|
||||
expl.Explode(e, src, impact, conf)
|
||||
}
|
||||
}
|
||||
|
||||
// Position returns the current position of the entity.
|
||||
func (e *Ent) Position() mgl64.Vec3 {
|
||||
return e.data.Pos
|
||||
}
|
||||
|
||||
// Velocity returns the current velocity of the entity. The values in the Vec3 returned represent the speed on
|
||||
// that axis in blocks/tick.
|
||||
func (e *Ent) Velocity() mgl64.Vec3 {
|
||||
return e.data.Vel
|
||||
}
|
||||
|
||||
// SetVelocity sets the velocity of the entity. The values in the Vec3 passed represent the speed on
|
||||
// that axis in blocks/tick.
|
||||
func (e *Ent) SetVelocity(v mgl64.Vec3) {
|
||||
e.data.Vel = v
|
||||
}
|
||||
|
||||
// Rotation returns the rotation of the entity.
|
||||
func (e *Ent) Rotation() cube.Rotation {
|
||||
return e.data.Rot
|
||||
}
|
||||
|
||||
// Age returns the total time lived of this entity. It increases by
|
||||
// time.Second/20 for every time Tick is called.
|
||||
func (e *Ent) Age() time.Duration {
|
||||
return e.data.Age
|
||||
}
|
||||
|
||||
// OnFireDuration ...
|
||||
func (e *Ent) OnFireDuration() time.Duration {
|
||||
return e.data.FireDuration
|
||||
}
|
||||
|
||||
// SetOnFire ...
|
||||
func (e *Ent) SetOnFire(duration time.Duration) {
|
||||
duration = max(duration, 0)
|
||||
stateChanged := (e.data.FireDuration > 0) != (duration > 0)
|
||||
|
||||
e.data.FireDuration = duration
|
||||
if stateChanged {
|
||||
for _, v := range e.tx.Viewers(e.data.Pos) {
|
||||
v.ViewEntityState(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extinguish ...
|
||||
func (e *Ent) Extinguish() {
|
||||
e.SetOnFire(0)
|
||||
}
|
||||
|
||||
// NameTag returns the name tag of the entity. An empty string is returned if
|
||||
// no name tag was set.
|
||||
func (e *Ent) NameTag() string {
|
||||
return e.data.Name
|
||||
}
|
||||
|
||||
// SetNameTag changes the name tag of an entity. The name tag is removed if an
|
||||
// empty string is passed.
|
||||
func (e *Ent) SetNameTag(s string) {
|
||||
e.data.Name = s
|
||||
for _, v := range e.tx.Viewers(e.Position()) {
|
||||
v.ViewEntityState(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
y := e.data.Pos[1]
|
||||
if y < float64(tx.Range()[0]) && current%10 == 0 {
|
||||
_ = e.Close()
|
||||
return
|
||||
}
|
||||
e.SetOnFire(e.OnFireDuration() - time.Second/20)
|
||||
|
||||
if m := e.Behaviour().Tick(e, tx); m != nil {
|
||||
m.Send()
|
||||
}
|
||||
e.data.Age += time.Second / 20
|
||||
}
|
||||
|
||||
// Close closes the Ent and removes the associated entity from the world.
|
||||
func (e *Ent) Close() error {
|
||||
e.once.Do(func() {
|
||||
e.tx.RemoveEntity(e)
|
||||
_ = e.handle.Close()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// ExperienceManager manages experience and levels for entities, and provides functions to add, remove, and calculate
|
||||
// experience needed for upcoming levels.
|
||||
type ExperienceManager struct {
|
||||
experience int
|
||||
dec float64
|
||||
}
|
||||
|
||||
// NewExperienceManager returns a new ExperienceManager with no experience.
|
||||
func NewExperienceManager() *ExperienceManager {
|
||||
return &ExperienceManager{}
|
||||
}
|
||||
|
||||
// Experience returns the amount of experience the manager currently has.
|
||||
func (e *ExperienceManager) Experience() int {
|
||||
return e.experience
|
||||
}
|
||||
|
||||
// Add adds experience to the total experience and recalculates the level and progress if necessary. Passing a negative
|
||||
// value is valid. If the new experience would otherwise drop below 0, it is set to 0.
|
||||
func (e *ExperienceManager) Add(amount int) (level int, progress float64) {
|
||||
if e.experience += amount; e.experience < 0 {
|
||||
e.Reset()
|
||||
}
|
||||
return progressFromExperience(e.total())
|
||||
}
|
||||
|
||||
// total returns the total amount of experience including the extra decimals provided for more accuracy.
|
||||
func (e *ExperienceManager) total() float64 {
|
||||
return float64(e.experience) + e.dec
|
||||
}
|
||||
|
||||
// Level returns the current experience level.
|
||||
func (e *ExperienceManager) Level() int {
|
||||
level, _ := progressFromExperience(e.total())
|
||||
return level
|
||||
}
|
||||
|
||||
// SetLevel sets the level of the manager.
|
||||
func (e *ExperienceManager) SetLevel(level int) {
|
||||
if level < 0 || level > math.MaxInt32 {
|
||||
panic(fmt.Sprintf("level must be between 0 and 2,147,483,647, got %v", level))
|
||||
}
|
||||
_, progress := progressFromExperience(e.total())
|
||||
e.experience = experienceForLevels(level) + int(float64(experienceForLevel(level))*progress)
|
||||
}
|
||||
|
||||
// Progress returns the progress towards the next level.
|
||||
func (e *ExperienceManager) Progress() float64 {
|
||||
_, progress := progressFromExperience(e.total())
|
||||
return progress
|
||||
}
|
||||
|
||||
// SetProgress sets the progress of the manager.
|
||||
func (e *ExperienceManager) SetProgress(progress float64) {
|
||||
if progress < 0 || progress > 1 {
|
||||
panic(fmt.Sprintf("progress must be between 0 and 1, got %f", progress))
|
||||
}
|
||||
currentLevel, _ := progressFromExperience(e.total())
|
||||
progressExp := float64(experienceForLevel(currentLevel)) * progress
|
||||
e.experience = experienceForLevels(currentLevel) + int(progressExp)
|
||||
e.dec = progressExp - math.Trunc(progressExp)
|
||||
}
|
||||
|
||||
// Reset resets the total experience, level, and progress of the manager to zero.
|
||||
func (e *ExperienceManager) Reset() {
|
||||
e.experience, e.dec = 0, 0
|
||||
}
|
||||
|
||||
// progressFromExperience returns the level and progress from the total experience given.
|
||||
func progressFromExperience(experience float64) (level int, progress float64) {
|
||||
var a, b, c float64
|
||||
|
||||
switch {
|
||||
case experience <= float64(experienceForLevels(16)):
|
||||
a, b = 1.0, 6.0
|
||||
case experience <= float64(experienceForLevels(31)):
|
||||
a, b, c = 2.5, -40.5, 360.0
|
||||
default:
|
||||
a, b, c = 4.5, -162.5, 2220.0
|
||||
}
|
||||
|
||||
var sol float64
|
||||
if d := b*b - 4*a*(c-experience); d > 0 {
|
||||
s := math.Sqrt(d)
|
||||
sol = math.Max((-b+s)/(2*a), (-b-s)/(2*a))
|
||||
} else if d == 0 {
|
||||
sol = -b / (2 * a)
|
||||
}
|
||||
return int(sol), sol - math.Trunc(sol)
|
||||
}
|
||||
|
||||
// experienceForLevels calculates the amount of experience needed in total to reach a certain level.
|
||||
func experienceForLevels(level int) int {
|
||||
if level <= 16 {
|
||||
return level*level + level*6
|
||||
} else if level <= 31 {
|
||||
return int(float64(level*level)*2.5 - 40.5*float64(level) + 360)
|
||||
}
|
||||
return int(float64(level*level)*4.5 - 162.5*float64(level) + 2220)
|
||||
}
|
||||
|
||||
// experienceForLevel returns the amount experience needed to reach level + 1.
|
||||
func experienceForLevel(level int) int {
|
||||
if level <= 15 {
|
||||
return 2*level + 7
|
||||
} else if level <= 30 {
|
||||
return 5*level - 38
|
||||
}
|
||||
return 9*level - 158
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// orbSplitSizes contains split sizes used for dropping experience orbs.
|
||||
var orbSplitSizes = []int{2477, 1237, 617, 307, 149, 73, 37, 17, 7, 3, 1}
|
||||
|
||||
// NewExperienceOrbs takes in a position and an amount and automatically splits the amount into multiple orbs, returning
|
||||
// a slice of the created orbs.
|
||||
func NewExperienceOrbs(pos mgl64.Vec3, amount int) (orbs []*world.EntityHandle) {
|
||||
for amount > 0 {
|
||||
size := orbSplitSizes[slices.IndexFunc(orbSplitSizes, func(value int) bool {
|
||||
return amount >= value
|
||||
})]
|
||||
|
||||
orbs = append(orbs, NewExperienceOrb(world.EntitySpawnOpts{Position: pos}, size))
|
||||
amount -= size
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NewExperienceOrb creates a new experience orb and returns it.
|
||||
func NewExperienceOrb(opts world.EntitySpawnOpts, xp int) *world.EntityHandle {
|
||||
conf := experienceOrbConf
|
||||
conf.Experience = xp
|
||||
if opts.Velocity.Len() == 0 {
|
||||
opts.Velocity = mgl64.Vec3{(rand.Float64()*0.2 - 0.1) * 2, rand.Float64() * 0.4, (rand.Float64()*0.2 - 0.1) * 2}
|
||||
}
|
||||
return opts.New(ExperienceOrbType, conf)
|
||||
}
|
||||
|
||||
var experienceOrbConf = ExperienceOrbBehaviourConfig{
|
||||
Gravity: 0.04,
|
||||
Drag: 0.02,
|
||||
}
|
||||
|
||||
// ExperienceOrbType is a world.EntityType implementation for ExperienceOrb.
|
||||
var ExperienceOrbType experienceOrbType
|
||||
|
||||
type experienceOrbType struct{}
|
||||
|
||||
func (t experienceOrbType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (experienceOrbType) EncodeEntity() string { return "minecraft:xp_orb" }
|
||||
func (experienceOrbType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
|
||||
func (experienceOrbType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
conf := experienceOrbConf
|
||||
conf.Experience = int(nbtconv.Int32(m, "Value"))
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
func (experienceOrbType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
return map[string]any{"Value": int32(data.Data.(*ExperienceOrbBehaviour).Experience())}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExperienceOrbBehaviourConfig holds optional parameters for the creation of
|
||||
// an ExperienceOrbBehaviour.
|
||||
type ExperienceOrbBehaviourConfig struct {
|
||||
// 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 experience orb should last. The
|
||||
// default is time.Minute * 5.
|
||||
ExistenceDuration time.Duration
|
||||
// Experience is the amount of experience held by the orb. Default is 1.
|
||||
Experience int
|
||||
}
|
||||
|
||||
func (conf ExperienceOrbBehaviourConfig) Apply(data *world.EntityData) {
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
// New creates an ExperienceOrbBehaviour using the parameters in conf.
|
||||
func (conf ExperienceOrbBehaviourConfig) New() *ExperienceOrbBehaviour {
|
||||
if conf.Experience == 0 {
|
||||
conf.Experience = 1
|
||||
}
|
||||
if conf.ExistenceDuration == 0 {
|
||||
conf.ExistenceDuration = time.Minute * 5
|
||||
}
|
||||
b := &ExperienceOrbBehaviour{conf: conf, lastSearch: time.Now()}
|
||||
b.passive = PassiveBehaviourConfig{
|
||||
Gravity: conf.Gravity,
|
||||
Drag: conf.Drag,
|
||||
ExistenceDuration: conf.ExistenceDuration,
|
||||
Tick: b.tick,
|
||||
}.New()
|
||||
return b
|
||||
}
|
||||
|
||||
// ExperienceOrbBehaviour implements Behaviour for an experience orb entity.
|
||||
type ExperienceOrbBehaviour struct {
|
||||
conf ExperienceOrbBehaviourConfig
|
||||
|
||||
passive *PassiveBehaviour
|
||||
|
||||
lastSearch time.Time
|
||||
target *world.EntityHandle
|
||||
}
|
||||
|
||||
// Experience returns the amount of experience the orb carries.
|
||||
func (exp *ExperienceOrbBehaviour) Experience() int {
|
||||
return exp.conf.Experience
|
||||
}
|
||||
|
||||
// Tick finds a target for the experience orb and moves the orb towards it.
|
||||
func (exp *ExperienceOrbBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
||||
return exp.passive.Tick(e, tx)
|
||||
}
|
||||
|
||||
// followBox is the bounding box used to search for collectors to follow for experience orbs.
|
||||
var followBox = cube.Box(-8, -8, -8, 8, 8, 8)
|
||||
|
||||
// tick finds a target for the experience orb and moves the orb towards it.
|
||||
func (exp *ExperienceOrbBehaviour) tick(e *Ent, tx *world.Tx) {
|
||||
targetEnt, ok := exp.target.Entity(tx)
|
||||
target, _ := targetEnt.(experienceCollector)
|
||||
|
||||
pos := e.Position()
|
||||
hasTarget := ok && !target.Dead() && pos.Sub(target.Position()).Len() <= 8
|
||||
if !hasTarget && time.Since(exp.lastSearch) >= time.Second {
|
||||
exp.findTarget(tx, pos)
|
||||
} else if hasTarget {
|
||||
exp.moveToTarget(e, target)
|
||||
}
|
||||
}
|
||||
|
||||
// findTarget attempts to find a target for an experience orb in w around pos.
|
||||
func (exp *ExperienceOrbBehaviour) findTarget(tx *world.Tx, pos mgl64.Vec3) {
|
||||
exp.target = nil
|
||||
for o := range tx.EntitiesWithin(followBox.Translate(pos)) {
|
||||
if ec, ok := o.(experienceCollector); ok && ec.CanCollectExperience() {
|
||||
exp.target = o.H()
|
||||
break
|
||||
}
|
||||
}
|
||||
exp.lastSearch = time.Now()
|
||||
}
|
||||
|
||||
// moveToTarget applies velocity to the experience orb so that it moves towards
|
||||
// its current target. If it intersects with the target, the orb is collected.
|
||||
func (exp *ExperienceOrbBehaviour) moveToTarget(e *Ent, target experienceCollector) {
|
||||
pos, dst := e.Position(), target.Position()
|
||||
if o, ok := target.(Eyed); ok {
|
||||
dst[1] += o.EyeHeight() / 2
|
||||
}
|
||||
diff := dst.Sub(pos).Mul(0.125)
|
||||
if dist := diff.LenSqr(); dist < 1 {
|
||||
e.SetVelocity(e.Velocity().Add(diff.Normalize().Mul(0.2 * math.Pow(1-math.Sqrt(dist), 2))))
|
||||
}
|
||||
|
||||
if e.H().Type().BBox(e).Translate(pos).IntersectsWith(target.H().Type().BBox(target).Translate(target.Position())) && target.CollectExperience(exp.conf.Experience) {
|
||||
_ = e.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// experienceCollector represents an entity that can collect experience orbs.
|
||||
type experienceCollector interface {
|
||||
Living
|
||||
// CollectExperience makes the player collect the experience points passed,
|
||||
// adding it to the experience manager. A bool is returned indicating
|
||||
// whether the player was able to collect the experience, or not, due to the
|
||||
// 100ms delay between experience collection.
|
||||
CollectExperience(value int) bool
|
||||
// CanCollectExperience returns whether the player can collect experience or not.
|
||||
CanCollectExperience() bool
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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/world"
|
||||
)
|
||||
|
||||
// NewFallingBlock creates a new FallingBlock entity.
|
||||
func NewFallingBlock(opts world.EntitySpawnOpts, block world.Block) *world.EntityHandle {
|
||||
conf := fallingBlockConf
|
||||
conf.Block = block
|
||||
return opts.New(FallingBlockType, conf)
|
||||
}
|
||||
|
||||
var fallingBlockConf = FallingBlockBehaviourConfig{
|
||||
Gravity: 0.04,
|
||||
Drag: 0.02,
|
||||
}
|
||||
|
||||
// FallingBlockType is a world.EntityType implementation for FallingBlock.
|
||||
var FallingBlockType fallingBlockType
|
||||
|
||||
type fallingBlockType struct{}
|
||||
|
||||
func (t fallingBlockType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
func (fallingBlockType) EncodeEntity() string { return "minecraft:falling_block" }
|
||||
func (fallingBlockType) NetworkOffset() float64 { return 0.49 }
|
||||
func (fallingBlockType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.49, 0, -0.49, 0.49, 0.98, 0.49)
|
||||
}
|
||||
|
||||
func (fallingBlockType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
conf := fallingBlockConf
|
||||
conf.Block = nbtconv.Block(m, "FallingBlock")
|
||||
conf.DistanceFallen = nbtconv.Float64(m, "FallDistance")
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
func (fallingBlockType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
b := data.Data.(*FallingBlockBehaviour)
|
||||
return map[string]any{"FallDistance": b.passive.fallDistance, "FallingBlock": nbtconv.WriteBlock(b.block)}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
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"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
// FallingBlockBehaviourConfig holds optional parameters for
|
||||
// FallingBlockBehaviour.
|
||||
type FallingBlockBehaviourConfig struct {
|
||||
Block world.Block
|
||||
// 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
|
||||
// DistanceFallen specifies how far the falling block has already fallen.
|
||||
// Blocks that damage entities on impact, like anvils, deal increased damage
|
||||
// based on the distance fallen.
|
||||
DistanceFallen float64
|
||||
}
|
||||
|
||||
func (conf FallingBlockBehaviourConfig) Apply(data *world.EntityData) {
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
// New creates a FallingBlockBehaviour using the optional parameters in conf and
|
||||
// a block type.
|
||||
func (conf FallingBlockBehaviourConfig) New() *FallingBlockBehaviour {
|
||||
behaviour := &FallingBlockBehaviour{block: conf.Block}
|
||||
behaviour.passive = PassiveBehaviourConfig{
|
||||
Gravity: conf.Gravity,
|
||||
Drag: conf.Drag,
|
||||
Tick: behaviour.tick,
|
||||
}.New()
|
||||
behaviour.passive.fallDistance = conf.DistanceFallen
|
||||
return behaviour
|
||||
}
|
||||
|
||||
// FallingBlockBehaviour implements the behaviour for falling block entities.
|
||||
type FallingBlockBehaviour struct {
|
||||
passive *PassiveBehaviour
|
||||
block world.Block
|
||||
}
|
||||
|
||||
// Block returns the world.Block of the entity.
|
||||
func (f *FallingBlockBehaviour) Block() world.Block {
|
||||
return f.block
|
||||
}
|
||||
|
||||
// Tick implements the movement and solidification behaviour of falling blocks.
|
||||
func (f *FallingBlockBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
||||
return f.passive.Tick(e, tx)
|
||||
}
|
||||
|
||||
// tick checks if the falling block should solidify.
|
||||
func (f *FallingBlockBehaviour) tick(e *Ent, tx *world.Tx) {
|
||||
pos := e.Position()
|
||||
bpos := cube.PosFromVec3(pos)
|
||||
if a, ok := f.block.(Solidifiable); (ok && a.Solidifies(bpos, tx)) || f.passive.mc.OnGround() {
|
||||
f.solidify(e, pos, tx)
|
||||
}
|
||||
}
|
||||
|
||||
// solidify attempts to solidify the falling block at the position passed. It
|
||||
// also deals damage to any entities standing at that position. If the block at
|
||||
// the position could not be replaced by the falling block, the block will drop
|
||||
// as an item.
|
||||
func (f *FallingBlockBehaviour) solidify(e *Ent, pos mgl64.Vec3, tx *world.Tx) {
|
||||
bpos := cube.PosFromVec3(pos)
|
||||
|
||||
if d, ok := f.block.(damager); ok {
|
||||
f.damageEntities(e, d, pos, tx)
|
||||
}
|
||||
if l, ok := f.block.(landable); ok {
|
||||
l.Landed(tx, bpos)
|
||||
}
|
||||
f.passive.close = true
|
||||
|
||||
if r, ok := tx.Block(bpos).(replaceable); ok && r.ReplaceableBy(f.block) {
|
||||
tx.SetBlock(bpos, f.block, nil)
|
||||
} else if i, ok := f.block.(world.Item); ok {
|
||||
opts := world.EntitySpawnOpts{Position: bpos.Vec3Middle()}
|
||||
tx.AddEntity(NewItem(opts, item.NewStack(i, 1)))
|
||||
}
|
||||
}
|
||||
|
||||
// damageEntities attempts to damage any entities standing below the falling
|
||||
// block. This functionality is used by falling anvils.
|
||||
func (f *FallingBlockBehaviour) damageEntities(e *Ent, d damager, pos mgl64.Vec3, tx *world.Tx) {
|
||||
damagePerBlock, maxDamage := d.Damage()
|
||||
dist := math.Ceil(f.passive.fallDistance - 1.0)
|
||||
if dist <= 0 {
|
||||
return
|
||||
}
|
||||
dmg := math.Min(math.Floor(dist*damagePerBlock), maxDamage)
|
||||
src := block.DamageSource{Block: f.block}
|
||||
|
||||
for e := range filterLiving(tx.EntitiesWithin(e.H().Type().BBox(e).Translate(pos).Grow(0.05))) {
|
||||
e.(Living).Hurt(dmg, src)
|
||||
}
|
||||
if b, ok := f.block.(breakable); ok && dmg > 0.0 && rand.Float64() < (dist+1)*0.05 {
|
||||
f.block = b.Break()
|
||||
}
|
||||
}
|
||||
|
||||
// Solidifiable represents a block that can solidify by specific adjacent blocks. An example is concrete
|
||||
// powder, which can turn into concrete by touching water.
|
||||
type Solidifiable interface {
|
||||
// Solidifies returns whether the falling block can solidify at the position it is currently in. If so,
|
||||
// the block will immediately stop falling.
|
||||
Solidifies(pos cube.Pos, tx *world.Tx) bool
|
||||
}
|
||||
|
||||
type replaceable interface {
|
||||
ReplaceableBy(b world.Block) bool
|
||||
}
|
||||
|
||||
// damager ...
|
||||
type damager interface {
|
||||
Damage() (damagePerBlock, maxDamage float64)
|
||||
}
|
||||
|
||||
// breakable ...
|
||||
type breakable interface {
|
||||
Break() world.Block
|
||||
}
|
||||
|
||||
// landable ...
|
||||
type landable interface {
|
||||
Landed(tx *world.Tx, pos cube.Pos)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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/world"
|
||||
)
|
||||
|
||||
// NewFirework creates a firework entity. Firework is an item (and entity) used
|
||||
// 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 newFirework(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) *world.EntityHandle {
|
||||
return newFirework(opts, firework, owner, 0, 0, true)
|
||||
}
|
||||
|
||||
func newFirework(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
|
||||
if attached {
|
||||
conf.Owner = owner.H()
|
||||
}
|
||||
return opts.New(FireworkType, conf)
|
||||
}
|
||||
|
||||
var fireworkConf = FireworkBehaviourConfig{}
|
||||
|
||||
// FireworkType is a world.EntityType implementation for Firework.
|
||||
var FireworkType fireworkType
|
||||
|
||||
type fireworkType struct{}
|
||||
|
||||
func (t fireworkType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (fireworkType) EncodeEntity() string { return "minecraft:fireworks_rocket" }
|
||||
func (fireworkType) BBox(world.Entity) cube.BBox { return cube.BBox{} }
|
||||
|
||||
func (fireworkType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
conf := fireworkConf
|
||||
conf.Firework = nbtconv.MapItem(m, "Item").Item().(item.Firework)
|
||||
conf.ExistenceDuration = conf.Firework.RandomisedDuration()
|
||||
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
func (fireworkType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
return map[string]any{"Item": nbtconv.WriteItem(item.NewStack(data.Data.(*FireworkBehaviour).Firework(), 1), true)}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube/trace"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/sound"
|
||||
"iter"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FireworkBehaviourConfig holds optional parameters for a FireworkBehaviour.
|
||||
type FireworkBehaviourConfig struct {
|
||||
Firework item.Firework
|
||||
Owner *world.EntityHandle
|
||||
// ExistenceDuration is the duration that an entity with this behaviour
|
||||
// should last. Once this time expires, the entity is closed. If
|
||||
// ExistenceDuration is 0, the entity will never expire automatically.
|
||||
ExistenceDuration time.Duration
|
||||
// SidewaysVelocityMultiplier is a value that the sideways velocity (X/Z) is
|
||||
// multiplied with every tick. For normal fireworks this is 1.15.
|
||||
SidewaysVelocityMultiplier float64
|
||||
// UpwardsAcceleration is a value added to the firework's velocity every
|
||||
// tick. For normal fireworks, this is 0.04.
|
||||
UpwardsAcceleration float64
|
||||
// Attached specifies if the firework is attached to its owner. If true,
|
||||
// the firework will boost the speed of the owner while flying.
|
||||
Attached bool
|
||||
}
|
||||
|
||||
func (conf FireworkBehaviourConfig) Apply(data *world.EntityData) {
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
// New creates a FireworkBehaviour for an fw and owner using the optional
|
||||
// parameters in conf.
|
||||
func (conf FireworkBehaviourConfig) New() *FireworkBehaviour {
|
||||
b := &FireworkBehaviour{conf: conf}
|
||||
b.passive = PassiveBehaviourConfig{
|
||||
ExistenceDuration: conf.ExistenceDuration,
|
||||
Expire: b.explode,
|
||||
Tick: b.tick,
|
||||
}.New()
|
||||
return b
|
||||
}
|
||||
|
||||
// FireworkBehaviour implements Behaviour for a firework entity.
|
||||
type FireworkBehaviour struct {
|
||||
conf FireworkBehaviourConfig
|
||||
passive *PassiveBehaviour
|
||||
}
|
||||
|
||||
// Firework returns the underlying item.Firework of the FireworkBehaviour.
|
||||
func (f *FireworkBehaviour) Firework() item.Firework {
|
||||
return f.conf.Firework
|
||||
}
|
||||
|
||||
// Attached specifies if the firework is attached to its owner.
|
||||
func (f *FireworkBehaviour) Attached() bool {
|
||||
return f.conf.Attached
|
||||
}
|
||||
|
||||
// Owner returns the world.Entity that launched the firework.
|
||||
func (f *FireworkBehaviour) Owner() *world.EntityHandle {
|
||||
return f.conf.Owner
|
||||
}
|
||||
|
||||
// Tick moves the firework and makes it explode when it reaches its maximum
|
||||
// duration.
|
||||
func (f *FireworkBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
||||
return f.passive.Tick(e, tx)
|
||||
}
|
||||
|
||||
// tick ticks the entity, updating its velocity either with a constant factor
|
||||
// or based on the owner's position and velocity if attached.
|
||||
func (f *FireworkBehaviour) tick(e *Ent, tx *world.Tx) {
|
||||
owner, ok := f.conf.Owner.Entity(tx)
|
||||
if f.conf.Attached && ok {
|
||||
// The client will propel itself to match the firework's velocity since
|
||||
// we set the appropriate metadata.
|
||||
e.data.Pos = owner.Position()
|
||||
} else {
|
||||
e.data.Vel[0] *= f.conf.SidewaysVelocityMultiplier
|
||||
e.data.Vel[1] += f.conf.UpwardsAcceleration
|
||||
e.data.Vel[2] *= f.conf.SidewaysVelocityMultiplier
|
||||
}
|
||||
}
|
||||
|
||||
// explode causes an explosion at the position of the firework, spawning
|
||||
// particles and damaging nearby entities.
|
||||
func (f *FireworkBehaviour) explode(e *Ent, tx *world.Tx) {
|
||||
owner, _ := f.conf.Owner.Entity(tx)
|
||||
pos, explosions := e.Position(), f.conf.Firework.Explosions
|
||||
|
||||
for _, v := range tx.Viewers(pos) {
|
||||
v.ViewEntityAction(e, FireworkExplosionAction{})
|
||||
}
|
||||
for _, explosion := range explosions {
|
||||
if explosion.Shape == item.FireworkShapeHugeSphere() {
|
||||
tx.PlaySound(pos, sound.FireworkHugeBlast{})
|
||||
} else {
|
||||
tx.PlaySound(pos, sound.FireworkBlast{})
|
||||
}
|
||||
if explosion.Twinkle {
|
||||
tx.PlaySound(pos, sound.FireworkTwinkle{})
|
||||
}
|
||||
}
|
||||
|
||||
if len(explosions) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
force := float64(len(explosions)*2) + 5.0
|
||||
for victim := range filterLiving(tx.EntitiesWithin(e.H().Type().BBox(e).Translate(pos).Grow(5.25))) {
|
||||
tpos := victim.Position()
|
||||
dist := pos.Sub(tpos).Len()
|
||||
if dist > 5.0 {
|
||||
// The maximum distance allowed is 5.0 blocks.
|
||||
continue
|
||||
}
|
||||
dmg := force * math.Sqrt((5.0-dist)/5.0)
|
||||
src := ProjectileDamageSource{Owner: owner, Projectile: e}
|
||||
|
||||
if pos == tpos {
|
||||
victim.(Living).Hurt(dmg, src)
|
||||
continue
|
||||
}
|
||||
if _, ok := trace.Perform(pos, tpos, tx, victim.H().Type().BBox(victim).Grow(0.3), nil); ok {
|
||||
victim.(Living).Hurt(dmg, src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterLiving(seq iter.Seq[world.Entity]) iter.Seq[world.Entity] {
|
||||
return func(yield func(world.Entity) bool) {
|
||||
for e := range seq {
|
||||
if _, living := e.(Living); !living {
|
||||
continue
|
||||
}
|
||||
if !yield(e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// Flammable is an interface for entities that can be set on fire.
|
||||
type Flammable interface {
|
||||
// OnFireDuration returns duration of fire in ticks.
|
||||
OnFireDuration() time.Duration
|
||||
// SetOnFire sets the entity on fire for the specified duration.
|
||||
SetOnFire(duration time.Duration)
|
||||
// Extinguish extinguishes the entity.
|
||||
Extinguish()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package entity
|
||||
|
||||
type (
|
||||
// FoodHealingSource is a healing source used for when an entity regenerates health automatically when their food
|
||||
// bar is at least 90% filled.
|
||||
FoodHealingSource struct{}
|
||||
)
|
||||
|
||||
func (FoodHealingSource) HealingSource() {}
|
||||
@@ -0,0 +1,42 @@
|
||||
package entity
|
||||
|
||||
// HealthManager handles the health of an entity.
|
||||
type HealthManager struct {
|
||||
health float64
|
||||
max float64
|
||||
}
|
||||
|
||||
// NewHealthManager returns a new health manager with the health and max health provided.
|
||||
func NewHealthManager(health, max float64) *HealthManager {
|
||||
if health > max {
|
||||
health = max
|
||||
}
|
||||
return &HealthManager{health: health, max: max}
|
||||
}
|
||||
|
||||
// Health returns the current health of an entity.
|
||||
func (m *HealthManager) Health() float64 {
|
||||
return m.health
|
||||
}
|
||||
|
||||
// AddHealth adds a given amount of health points to the player. If the health added to the current health
|
||||
// exceeds the max, health will be set to the max. If the health is instead negative and results in a health
|
||||
// lower than 0, the final health will be 0.
|
||||
func (m *HealthManager) AddHealth(health float64) {
|
||||
m.health = max(min(m.health+health, m.max), 0)
|
||||
}
|
||||
|
||||
// MaxHealth returns the maximum health of the entity.
|
||||
func (m *HealthManager) MaxHealth() float64 {
|
||||
return m.max
|
||||
}
|
||||
|
||||
// SetMaxHealth changes the max health of an entity to the maximum passed. If the maximum is set to 0 or
|
||||
// lower, SetMaxHealth will default to a value of 1.
|
||||
func (m *HealthManager) SetMaxHealth(max float64) {
|
||||
if max <= 0 {
|
||||
max = 1
|
||||
}
|
||||
m.max = max
|
||||
m.health = min(m.health, max)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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/world"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewItem creates a new item entity using the item stack passed. The item
|
||||
// entity will be positioned at the position passed. If the stack's count
|
||||
// exceeds its max count, the count of the stack will be changed to the
|
||||
// maximum.
|
||||
func NewItem(opts world.EntitySpawnOpts, i item.Stack) *world.EntityHandle {
|
||||
conf := itemConf
|
||||
conf.Item = i
|
||||
return opts.New(ItemType, conf)
|
||||
}
|
||||
|
||||
// NewItemPickupDelay creates a new item entity containing item stack i. A
|
||||
// delay may be specified which defines for how long the item stack cannot be
|
||||
// picked up from the ground.
|
||||
func NewItemPickupDelay(opts world.EntitySpawnOpts, i item.Stack, delay time.Duration) *world.EntityHandle {
|
||||
conf := itemConf
|
||||
conf.Item = i
|
||||
conf.PickupDelay = delay
|
||||
return opts.New(ItemType, conf)
|
||||
}
|
||||
|
||||
var itemConf = ItemBehaviourConfig{
|
||||
Gravity: 0.04,
|
||||
Drag: 0.02,
|
||||
}
|
||||
|
||||
// ItemType is a world.EntityType implementation for Item.
|
||||
var ItemType itemType
|
||||
|
||||
type itemType struct{}
|
||||
|
||||
func (t itemType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (itemType) EncodeEntity() string { return "minecraft:item" }
|
||||
func (itemType) NetworkOffset() float64 { return 0.125 }
|
||||
func (itemType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
|
||||
func (itemType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
conf := itemConf
|
||||
conf.Item = nbtconv.MapItem(m, "Item")
|
||||
conf.PickupDelay = time.Duration(nbtconv.Int64(m, "PickupDelay")) * (time.Second / 20)
|
||||
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
func (itemType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
b := data.Data.(*ItemBehaviour)
|
||||
return map[string]any{
|
||||
"Health": int16(5),
|
||||
"PickupDelay": int64(b.pickupDelay / (time.Second * 20)),
|
||||
"Item": nbtconv.WriteItem(b.Item(), true),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
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 {
|
||||
conf.ExistenceDuration = time.Minute * 5
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/sound"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewLightning creates a lightning entity. The lightning entity will be
|
||||
// positioned at the position passed. Lightning is a lethal element to
|
||||
// thunderstorms. Lightning momentarily increases the skylight's brightness to
|
||||
// slightly greater than full daylight.
|
||||
func NewLightning(opts world.EntitySpawnOpts) *world.EntityHandle {
|
||||
return NewLightningWithDamage(opts, 5, true, time.Second*8)
|
||||
}
|
||||
|
||||
// NewLightningWithDamage creates a new lightning entities using the damage and
|
||||
// fire properties passed.
|
||||
func NewLightningWithDamage(opts world.EntitySpawnOpts, dmg float64, blockFire bool, entityFireDuration time.Duration) *world.EntityHandle {
|
||||
conf := lightningConf
|
||||
conf.Tick = (&lightningState{
|
||||
Damage: dmg,
|
||||
EntityFireDuration: entityFireDuration,
|
||||
BlockFire: blockFire,
|
||||
state: 2,
|
||||
lifetime: rand.IntN(4) + 1,
|
||||
}).tick
|
||||
return opts.New(LightningType, conf)
|
||||
}
|
||||
|
||||
var lightningConf = StationaryBehaviourConfig{SpawnSounds: []world.Sound{sound.Explosion{}, sound.Thunder{}}, ExistenceDuration: time.Second}
|
||||
|
||||
// lightningState holds the state of a lightning entity.
|
||||
type lightningState struct {
|
||||
Damage float64
|
||||
EntityFireDuration time.Duration
|
||||
BlockFire bool
|
||||
state, lifetime int
|
||||
}
|
||||
|
||||
// tick carries out lightning logic, dealing damage and setting blocks/entities
|
||||
// on fire when appropriate.
|
||||
func (s *lightningState) tick(e *Ent, tx *world.Tx) {
|
||||
pos := e.Position()
|
||||
|
||||
if s.state--; s.state < 0 {
|
||||
if s.lifetime == 0 {
|
||||
_ = e.Close()
|
||||
} else if s.state < -rand.IntN(10) {
|
||||
s.lifetime--
|
||||
s.state = 1
|
||||
|
||||
if s.BlockFire && tx.World().Difficulty().FireSpreadIncrease() >= 10 {
|
||||
s.spreadFire(tx, cube.PosFromVec3(pos))
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.state > 0 {
|
||||
s.dealDamage(e, tx)
|
||||
}
|
||||
}
|
||||
|
||||
// dealDamage deals damage to all entities around the lightning and sets them
|
||||
// on fire.
|
||||
func (s *lightningState) dealDamage(e *Ent, tx *world.Tx) {
|
||||
pos := e.Position()
|
||||
bb := e.H().Type().BBox(e).GrowVec3(mgl64.Vec3{3, 6, 3}).Translate(pos.Add(mgl64.Vec3{0, 3}))
|
||||
for e := range tx.EntitiesWithin(bb) {
|
||||
// Only damage entities that weren't already dead.
|
||||
if l, ok := e.(Living); ok && l.Health() > 0 {
|
||||
if s.Damage > 0 {
|
||||
l.Hurt(s.Damage, LightningDamageSource{})
|
||||
}
|
||||
if f, ok := e.(Flammable); ok && f.OnFireDuration() < s.EntityFireDuration {
|
||||
f.SetOnFire(s.EntityFireDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// spreadFire attempts to place fire at the position of the lightning and does
|
||||
// 4 additional attempts to spread it around that position.
|
||||
func (s *lightningState) spreadFire(tx *world.Tx, pos cube.Pos) {
|
||||
s.fire().Start(tx, pos)
|
||||
for i := 0; i < 4; i++ {
|
||||
pos.Add(cube.Pos{rand.IntN(3) - 1, rand.IntN(3) - 1, rand.IntN(3) - 1})
|
||||
s.fire().Start(tx, pos)
|
||||
}
|
||||
}
|
||||
|
||||
// fire returns a fire block.
|
||||
func (s *lightningState) fire() interface {
|
||||
Start(tx *world.Tx, pos cube.Pos)
|
||||
} {
|
||||
return fire().(interface {
|
||||
Start(tx *world.Tx, pos cube.Pos)
|
||||
})
|
||||
}
|
||||
|
||||
// fire returns a fire block.
|
||||
func fire() world.Block {
|
||||
f, ok := world.BlockByName("minecraft:fire", map[string]any{"age": int32(0)})
|
||||
if !ok {
|
||||
panic("could not find fire block")
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// LightningType is a world.EntityType implementation for Lightning.
|
||||
var LightningType lightningType
|
||||
|
||||
type lightningType struct{}
|
||||
|
||||
func (t lightningType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
func (t lightningType) DecodeNBT(_ map[string]any, data *world.EntityData) {
|
||||
data.Data = lightningConf.New()
|
||||
}
|
||||
func (t lightningType) EncodeNBT(*world.EntityData) map[string]any { return nil }
|
||||
func (lightningType) EncodeEntity() string { return "minecraft:lightning_bolt" }
|
||||
func (lightningType) BBox(world.Entity) cube.BBox { return cube.BBox{} }
|
||||
@@ -0,0 +1,54 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/internal/nbtconv"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/particle"
|
||||
)
|
||||
|
||||
// NewLingeringPotion creates a new lingering potion. LingeringPotion is a
|
||||
// variant of a splash potion that can be thrown to leave clouds with status
|
||||
// effects that linger on the ground in an area.
|
||||
func NewLingeringPotion(opts world.EntitySpawnOpts, t potion.Potion, owner world.Entity) *world.EntityHandle {
|
||||
colour, _ := effect.ResultingColour(t.Effects())
|
||||
|
||||
conf := splashPotionConf
|
||||
conf.Potion = t
|
||||
conf.Particle = particle.Splash{Colour: colour}
|
||||
conf.Hit = potionSplash(0.25, t, true)
|
||||
conf.Owner = owner.H()
|
||||
return opts.New(LingeringPotionType, conf)
|
||||
}
|
||||
|
||||
// LingeringPotionType is a world.EntityType implementation for LingeringPotion.
|
||||
var LingeringPotionType lingeringPotionType
|
||||
|
||||
type lingeringPotionType struct{}
|
||||
|
||||
func (t lingeringPotionType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (lingeringPotionType) EncodeEntity() string {
|
||||
return "minecraft:lingering_potion"
|
||||
}
|
||||
func (lingeringPotionType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
|
||||
func (lingeringPotionType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
conf := splashPotionConf
|
||||
conf.Potion = potion.From(nbtconv.Int32(m, "PotionId"))
|
||||
colour, _ := effect.ResultingColour(conf.Potion.Effects())
|
||||
conf.Particle = particle.Splash{Colour: colour}
|
||||
conf.Hit = potionSplash(0.25, conf.Potion, true)
|
||||
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
func (lingeringPotionType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
return map[string]any{"PotionId": int32(data.Data.(*ProjectileBehaviour).conf.Potion.Uint8())}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
// Living represents an entity that is alive and that has health. It is able to take damage and will die upon
|
||||
// taking fatal damage.
|
||||
type Living interface {
|
||||
world.Entity
|
||||
// Health returns the health of the entity.
|
||||
Health() float64
|
||||
// MaxHealth returns the maximum health of the entity.
|
||||
MaxHealth() float64
|
||||
// SetMaxHealth changes the maximum health of the entity to the value passed.
|
||||
SetMaxHealth(v float64)
|
||||
// Dead checks if the entity is considered dead. True is returned if the health of the entity is equal to or
|
||||
// lower than 0.
|
||||
Dead() bool
|
||||
// Hurt hurts the entity for a given amount of damage. The source passed represents the cause of the
|
||||
// damage, for example AttackDamageSource if the entity is attacked by another entity.
|
||||
// If the final damage exceeds the health that the entity currently has, the entity is killed.
|
||||
// Hurt returns the final amount of damage dealt to the Living entity and returns whether the Living entity
|
||||
// was vulnerable to the damage at all.
|
||||
Hurt(damage float64, src world.DamageSource) (n float64, vulnerable bool)
|
||||
// Heal heals the entity for a given amount of health. The source passed represents the cause of the
|
||||
// healing, for example FoodHealingSource if the entity healed by having a full food bar. If the health
|
||||
// added to the original health exceeds the entity's max health, Heal may not add the full amount.
|
||||
Heal(health float64, src world.HealingSource)
|
||||
// KnockBack knocks the entity back with a given force and height. A source is passed which indicates the
|
||||
// source of the velocity, typically the position of an attacking entity. The source is used to calculate
|
||||
// the direction which the entity should be knocked back in.
|
||||
KnockBack(src mgl64.Vec3, force, height float64)
|
||||
// Velocity returns the players current velocity.
|
||||
Velocity() mgl64.Vec3
|
||||
// SetVelocity updates the entity's velocity.
|
||||
SetVelocity(velocity mgl64.Vec3)
|
||||
// AddEffect adds an entity.Effect to the entity. If the effect is instant, it is applied to the entity
|
||||
// immediately. If not, the effect is applied to the entity every time the Tick method is called.
|
||||
// AddEffect will overwrite any effects present if the level of the effect is higher than the existing one, or
|
||||
// if the effects' levels are equal and the new effect has a longer duration.
|
||||
AddEffect(e effect.Effect)
|
||||
// RemoveEffect removes any effect that might currently be active on the entity.
|
||||
RemoveEffect(e effect.Type)
|
||||
// Effects returns any effect currently applied to the entity. The returned effects are guaranteed not to have
|
||||
// expired when returned.
|
||||
Effects() []effect.Effect
|
||||
// Speed returns the current speed of the living entity. The default value is different for each entity.
|
||||
Speed() float64
|
||||
// SetSpeed sets the speed of an entity to a new value.
|
||||
SetSpeed(float64)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"math"
|
||||
)
|
||||
|
||||
// MovementComputer is used to compute movement of an entity. When constructed, the Gravity of the entity
|
||||
// the movement is computed for must be passed.
|
||||
type MovementComputer struct {
|
||||
Gravity, Drag float64
|
||||
DragBeforeGravity bool
|
||||
|
||||
onGround bool
|
||||
}
|
||||
|
||||
// Movement represents the movement of a world.Entity as a result of a call to MovementComputer.TickMovement. The
|
||||
// resulting position and velocity can be obtained by calling Position and Velocity. These can be sent to viewers by
|
||||
// calling Send.
|
||||
type Movement struct {
|
||||
v []world.Viewer
|
||||
e world.Entity
|
||||
pos, vel, dpos, dvel mgl64.Vec3
|
||||
rot cube.Rotation
|
||||
onGround bool
|
||||
}
|
||||
|
||||
// Send sends the Movement to any viewers watching the entity at the time of the movement. If the position/velocity
|
||||
// changes were negligible, nothing is sent.
|
||||
func (m *Movement) Send() {
|
||||
posChanged := !m.dpos.ApproxEqualThreshold(zeroVec3, epsilon)
|
||||
velChanged := !m.dvel.ApproxEqualThreshold(zeroVec3, epsilon)
|
||||
|
||||
for _, v := range m.v {
|
||||
if posChanged {
|
||||
v.ViewEntityMovement(m.e, m.pos, m.rot, m.onGround)
|
||||
}
|
||||
if velChanged {
|
||||
v.ViewEntityVelocity(m.e, m.vel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Position returns the position as a result of the Movement as an mgl64.Vec3.
|
||||
func (m *Movement) Position() mgl64.Vec3 {
|
||||
return m.pos
|
||||
}
|
||||
|
||||
// Velocity returns the velocity after the Movement as an mgl64.Vec3.
|
||||
func (m *Movement) Velocity() mgl64.Vec3 {
|
||||
return m.vel
|
||||
}
|
||||
|
||||
// Rotation returns the rotation, yaw and pitch, of the entity after the Movement.
|
||||
func (m *Movement) Rotation() cube.Rotation {
|
||||
return m.rot
|
||||
}
|
||||
|
||||
// TickMovement performs a movement tick on an entity. Velocity is applied and changed according to the values
|
||||
// of its Drag and Gravity.
|
||||
// The new position of the entity after movement is returned.
|
||||
// The resulting Movement can be sent to viewers by calling Movement.Send.
|
||||
func (c *MovementComputer) TickMovement(e world.Entity, pos, vel mgl64.Vec3, rot cube.Rotation, tx *world.Tx) *Movement {
|
||||
viewers := tx.Viewers(pos)
|
||||
|
||||
velBefore := vel
|
||||
vel = c.applyHorizontalForces(tx, pos, c.applyVerticalForces(vel))
|
||||
dPos, vel := c.checkCollision(tx, e, pos, vel)
|
||||
|
||||
return &Movement{v: viewers, e: e,
|
||||
pos: pos.Add(dPos), vel: vel, dpos: dPos, dvel: vel.Sub(velBefore),
|
||||
rot: rot, onGround: c.onGround,
|
||||
}
|
||||
}
|
||||
|
||||
// OnGround checks if the entity that this computer calculates is currently on the ground.
|
||||
func (c *MovementComputer) OnGround() bool {
|
||||
return c.onGround
|
||||
}
|
||||
|
||||
// zeroVec3 is a mgl64.Vec3 with zero values.
|
||||
var zeroVec3 mgl64.Vec3
|
||||
|
||||
// epsilon is the epsilon used for thresholds for change used for change in position and velocity.
|
||||
const epsilon = 0.001
|
||||
|
||||
// applyVerticalForces applies gravity and drag on the Y axis, based on the Gravity and Drag values set.
|
||||
func (c *MovementComputer) applyVerticalForces(vel mgl64.Vec3) mgl64.Vec3 {
|
||||
if c.DragBeforeGravity {
|
||||
vel[1] *= 1 - c.Drag
|
||||
}
|
||||
vel[1] -= c.Gravity
|
||||
if !c.DragBeforeGravity {
|
||||
vel[1] *= 1 - c.Drag
|
||||
}
|
||||
return vel
|
||||
}
|
||||
|
||||
// applyHorizontalForces applies friction to the velocity based on the Drag value, reducing it on the X and Z axes.
|
||||
func (c *MovementComputer) applyHorizontalForces(tx *world.Tx, pos, vel mgl64.Vec3) mgl64.Vec3 {
|
||||
friction := 1 - c.Drag
|
||||
if c.onGround {
|
||||
if f, ok := tx.Block(cube.PosFromVec3(pos).Side(cube.FaceDown)).(interface {
|
||||
Friction() float64
|
||||
}); ok {
|
||||
friction *= f.Friction()
|
||||
} else {
|
||||
friction *= 0.6
|
||||
}
|
||||
}
|
||||
vel[0] *= friction
|
||||
vel[2] *= friction
|
||||
return vel
|
||||
}
|
||||
|
||||
// checkCollision handles the collision of the entity with blocks, adapting the velocity of the entity if it
|
||||
// happens to collide with a block.
|
||||
// The final velocity and the Vec3 that the entity should move is returned.
|
||||
func (c *MovementComputer) checkCollision(tx *world.Tx, e world.Entity, pos, vel mgl64.Vec3) (mgl64.Vec3, mgl64.Vec3) {
|
||||
// TODO: Implement collision with other entities.
|
||||
deltaX, deltaY, deltaZ := vel[0], vel[1], vel[2]
|
||||
|
||||
// Entities only ever have a single bounding box.
|
||||
entityBBox := e.H().Type().BBox(e).Translate(pos)
|
||||
blocks := blockBBoxsAround(tx, entityBBox.Extend(vel))
|
||||
|
||||
if !mgl64.FloatEqualThreshold(deltaY, 0, epsilon) {
|
||||
// First we move the entity BBox on the Y axis.
|
||||
for _, blockBBox := range blocks {
|
||||
deltaY = entityBBox.YOffset(blockBBox, deltaY)
|
||||
}
|
||||
entityBBox = entityBBox.Translate(mgl64.Vec3{0, deltaY})
|
||||
}
|
||||
if !mgl64.FloatEqualThreshold(deltaX, 0, epsilon) {
|
||||
// Then on the X axis.
|
||||
for _, blockBBox := range blocks {
|
||||
deltaX = entityBBox.XOffset(blockBBox, deltaX)
|
||||
}
|
||||
entityBBox = entityBBox.Translate(mgl64.Vec3{deltaX})
|
||||
}
|
||||
if !mgl64.FloatEqualThreshold(deltaZ, 0, epsilon) {
|
||||
// And finally on the Z axis.
|
||||
for _, blockBBox := range blocks {
|
||||
deltaZ = entityBBox.ZOffset(blockBBox, deltaZ)
|
||||
}
|
||||
}
|
||||
if !mgl64.FloatEqual(vel[1], 0) {
|
||||
// The Y velocity of the entity is currently not 0, meaning it is moving either up or down. We can
|
||||
// then assume the entity is not currently on the ground.
|
||||
c.onGround = false
|
||||
}
|
||||
if !mgl64.FloatEqual(deltaX, vel[0]) {
|
||||
vel[0] = 0
|
||||
}
|
||||
if !mgl64.FloatEqual(deltaY, vel[1]) {
|
||||
// The entity either hit the ground or hit the ceiling.
|
||||
if vel[1] < 0 {
|
||||
// The entity was going down, so we can assume it is now on the ground.
|
||||
c.onGround = true
|
||||
}
|
||||
vel[1] = 0
|
||||
}
|
||||
if !mgl64.FloatEqual(deltaZ, vel[2]) {
|
||||
vel[2] = 0
|
||||
}
|
||||
return mgl64.Vec3{deltaX, deltaY, deltaZ}, vel
|
||||
}
|
||||
|
||||
// blockBBoxsAround returns all blocks around the entity passed, using the BBox passed to make a prediction of
|
||||
// what blocks need to have their BBox returned.
|
||||
func blockBBoxsAround(tx *world.Tx, box cube.BBox) []cube.BBox {
|
||||
grown := box.Grow(0.25)
|
||||
min, max := grown.Min(), grown.Max()
|
||||
minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2]))
|
||||
maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2]))
|
||||
|
||||
// A prediction of one BBox per block, plus an additional 2, in case
|
||||
blockBBoxs := make([]cube.BBox, 0, (maxX-minX)*(maxY-minY)*(maxZ-minZ)+2)
|
||||
for y := minY; y <= maxY; y++ {
|
||||
for x := minX; x <= maxX; x++ {
|
||||
for z := minZ; z <= maxZ; z++ {
|
||||
pos := cube.Pos{x, y, z}
|
||||
boxes := tx.Block(pos).Model().BBox(pos, tx)
|
||||
for _, box := range boxes {
|
||||
blockBBoxs = append(blockBBoxs, box.Translate(mgl64.Vec3{float64(x), float64(y), float64(z)}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return blockBBoxs
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PassiveBehaviourConfig holds optional parameters for a PassiveBehaviour.
|
||||
type PassiveBehaviourConfig struct {
|
||||
// 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 is the duration that an entity with this behaviour
|
||||
// should last. Once this time expires, the entity is closed. If
|
||||
// ExistenceDuration is 0, the entity will never expire automatically.
|
||||
ExistenceDuration time.Duration
|
||||
// Expire is called when the entity expires due to its age reaching the
|
||||
// ExistenceDuration.
|
||||
Expire func(e *Ent, tx *world.Tx)
|
||||
// Tick is called for every tick that the entity is alive. Tick is called
|
||||
// after the entity moves on a tick.
|
||||
Tick func(e *Ent, tx *world.Tx)
|
||||
}
|
||||
|
||||
func (conf PassiveBehaviourConfig) Apply(data *world.EntityData) {
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
// New creates a PassiveBehaviour using the parameters in conf.
|
||||
func (conf PassiveBehaviourConfig) New() *PassiveBehaviour {
|
||||
if conf.ExistenceDuration == 0 {
|
||||
conf.ExistenceDuration = math.MaxInt64
|
||||
}
|
||||
return &PassiveBehaviour{conf: conf, fuse: conf.ExistenceDuration, mc: &MovementComputer{
|
||||
Gravity: conf.Gravity,
|
||||
Drag: conf.Drag,
|
||||
DragBeforeGravity: true,
|
||||
}}
|
||||
}
|
||||
|
||||
// PassiveBehaviour implements Behaviour for entities that act passively. This
|
||||
// means that they can move, but only under influence of the environment, which
|
||||
// includes, for example, falling, and flowing water.
|
||||
type PassiveBehaviour struct {
|
||||
conf PassiveBehaviourConfig
|
||||
mc *MovementComputer
|
||||
|
||||
close bool
|
||||
fallDistance float64
|
||||
fuse time.Duration
|
||||
}
|
||||
|
||||
// Explode adds velocity to a passive entity to blast it away from the
|
||||
// explosion's source.
|
||||
func (p *PassiveBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, _ block.ExplosionConfig) {
|
||||
e.data.Vel = e.data.Vel.Add(e.data.Pos.Sub(src).Normalize().Mul(impact))
|
||||
}
|
||||
|
||||
// Fuse returns the leftover time until PassiveBehaviourConfig.Expire is called,
|
||||
// or -1 if this function is not set.
|
||||
func (p *PassiveBehaviour) Fuse() time.Duration {
|
||||
if p.conf.Expire != nil {
|
||||
return p.fuse
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Tick implements the behaviour for a passive entity. It performs movement and
|
||||
// updates its state.
|
||||
func (p *PassiveBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
||||
if p.close {
|
||||
_ = e.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
m := p.mc.TickMovement(e, e.data.Pos, e.data.Vel, e.data.Rot, tx)
|
||||
e.data.Pos, e.data.Vel = m.pos, m.vel
|
||||
p.fallDistance = math.Max(p.fallDistance-m.dvel[1], 0)
|
||||
|
||||
p.fuse = p.conf.ExistenceDuration - e.Age()
|
||||
|
||||
if p.conf.Tick != nil {
|
||||
p.conf.Tick(e, tx)
|
||||
}
|
||||
|
||||
if p.Fuse()%(time.Second/4) == 0 {
|
||||
for _, v := range tx.Viewers(m.pos) {
|
||||
v.ViewEntityState(e)
|
||||
}
|
||||
}
|
||||
|
||||
if e.Age() > p.conf.ExistenceDuration {
|
||||
p.close = true
|
||||
if p.conf.Expire != nil {
|
||||
p.conf.Expire(e, tx)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/block/cube/trace"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
// ProjectileBehaviourConfig allows the configuration of projectiles. Calling
|
||||
// ProjectileBehaviourConfig.New() creates a ProjectileBehaviour using these
|
||||
// settings.
|
||||
type ProjectileBehaviourConfig struct {
|
||||
Owner *world.EntityHandle
|
||||
// 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
|
||||
// Damage specifies the base damage dealt by the Projectile. If set to a
|
||||
// negative number, entities hit are not hurt at all and are not knocked
|
||||
// back. The base damage is multiplied with the velocity of the projectile
|
||||
// to calculate the final damage of the projectile.
|
||||
Damage float64
|
||||
// Potion is the potion effect that is applied to an entity when the
|
||||
// projectile hits it.
|
||||
Potion potion.Potion
|
||||
// KnockBackForceAddend is the additional horizontal velocity that is
|
||||
// applied to an entity when it is hit by the projectile.
|
||||
KnockBackForceAddend float64
|
||||
// KnockBackHeightAddend is the additional vertical velocity that is applied
|
||||
// to an entity when it is hit by the projectile.
|
||||
KnockBackHeightAddend float64
|
||||
// Particle is a particle that is spawned when the projectile hits a
|
||||
// target, either a block or an entity. No particle is spawned if left nil.
|
||||
Particle world.Particle
|
||||
// ParticleCount is the amount of particles that should be spawned if
|
||||
// Particle is not nil. ParticleCount will be set to 1 if Particle is not
|
||||
// nil and ParticleCount is 0.
|
||||
ParticleCount int
|
||||
// Sound is a sound that is played when the projectile hits a target, either
|
||||
// a block or an entity. No sound is played if left nil.
|
||||
Sound world.Sound
|
||||
// Critical specifies if the projectile is critical. This spawns critical
|
||||
// hit particles behind the projectile and causes it to deal up to 50% more
|
||||
// damage.
|
||||
Critical bool
|
||||
// Hit is a function that is called when the projectile Ent hits a target
|
||||
// (the trace.Result). The target is either of the type trace.EntityResult
|
||||
// or trace.BlockResult. Hit may be set to run additional behaviour when a
|
||||
// projectile hits a target.
|
||||
Hit func(e *Ent, tx *world.Tx, target trace.Result)
|
||||
// SurviveBlockCollision specifies if a projectile with this
|
||||
// ProjectileBehaviour should survive collision with a block. If set to
|
||||
// false, the projectile will break when hitting a block (like a snowball).
|
||||
// If set to true, the projectile will survive like an arrow does.
|
||||
SurviveBlockCollision bool
|
||||
// BlockCollisionVelocityMultiplier is the multiplier used to modify the
|
||||
// velocity of a projectile that has SurviveBlockCollision set to true. The
|
||||
// default, 0, will cause the projectile to lose its velocity completely. A
|
||||
// multiplier such as 0.5 will reduce the projectile's velocity, but retain
|
||||
// half of it after inverting the axis on which the projectile collided.
|
||||
BlockCollisionVelocityMultiplier float64
|
||||
// DisablePickup specifies if picking up the projectile should be disabled,
|
||||
// which is relevant in the case SurviveBlockCollision is set to true. Some
|
||||
// projectiles, such as arrows, cannot be picked up if they are shot by
|
||||
// monsters like skeletons.
|
||||
DisablePickup bool
|
||||
// PickupItem is the item that is given to a player when it picks up this
|
||||
// projectile. If left as an empty item.Stack, no item is given upon pickup.
|
||||
PickupItem item.Stack
|
||||
// CollisionPosition specifies the position that the projectile is stuck
|
||||
// in. If non-empty, the entity will not move.
|
||||
CollisionPosition cube.Pos
|
||||
// PiercingLevel is the crossbow Piercing enchantment level. The projectile
|
||||
// passes through PiercingLevel entities and damages PiercingLevel+1 in
|
||||
// total. A value of 0 means no piercing.
|
||||
PiercingLevel int
|
||||
}
|
||||
|
||||
func (conf ProjectileBehaviourConfig) Apply(data *world.EntityData) {
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
// New creates a new ProjectileBehaviour using conf. The owner passed may be nil
|
||||
// if the projectile does not have one.
|
||||
func (conf ProjectileBehaviourConfig) New() *ProjectileBehaviour {
|
||||
if conf.ParticleCount == 0 && conf.Particle != nil {
|
||||
conf.ParticleCount = 1
|
||||
}
|
||||
return &ProjectileBehaviour{conf: conf, collided: conf.CollisionPosition != cube.Pos{}, collisionPos: conf.CollisionPosition, mc: &MovementComputer{
|
||||
Gravity: conf.Gravity,
|
||||
Drag: conf.Drag,
|
||||
DragBeforeGravity: true,
|
||||
}}
|
||||
}
|
||||
|
||||
// ProjectileBehaviour implements the behaviour of projectiles. Its specifics
|
||||
// may be configured using ProjectileBehaviourConfig.
|
||||
type ProjectileBehaviour struct {
|
||||
conf ProjectileBehaviourConfig
|
||||
mc *MovementComputer
|
||||
ageCollided int
|
||||
close bool
|
||||
|
||||
collisionPos cube.Pos
|
||||
collided bool
|
||||
|
||||
collidedEntities []*world.EntityHandle
|
||||
}
|
||||
|
||||
// Owner returns the owner of the projectile.
|
||||
func (lt *ProjectileBehaviour) Owner() *world.EntityHandle {
|
||||
return lt.conf.Owner
|
||||
}
|
||||
|
||||
// Explode adds velocity to a projectile to blast it away from the explosion's
|
||||
// source.
|
||||
func (lt *ProjectileBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, _ block.ExplosionConfig) {
|
||||
e.data.Vel = e.Velocity().Add(e.Position().Sub(src).Normalize().Mul(impact))
|
||||
}
|
||||
|
||||
// Potion returns the potion.Potion that is applied to an entity if hit by the
|
||||
// projectile.
|
||||
func (lt *ProjectileBehaviour) Potion() potion.Potion {
|
||||
return lt.conf.Potion
|
||||
}
|
||||
|
||||
// Critical returns true if ProjectileBehaviourConfig.Critical was set to true
|
||||
// and if the projectile has not collided.
|
||||
func (lt *ProjectileBehaviour) Critical() bool {
|
||||
return lt.conf.Critical && !lt.collided
|
||||
}
|
||||
|
||||
// Tick runs the tick-based behaviour of a ProjectileBehaviour and returns the
|
||||
// Movement within the tick. Tick handles the movement, collision and hitting
|
||||
// of a projectile.
|
||||
func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
||||
if lt.close {
|
||||
_ = e.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
if lt.collided && lt.tickAttached(e, tx) {
|
||||
if lt.ageCollided > 1200 {
|
||||
lt.close = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
vel := e.Velocity()
|
||||
m, result := lt.tickMovement(e, tx)
|
||||
e.data.Pos, e.data.Vel = m.pos, m.vel
|
||||
|
||||
lt.collisionPos, lt.collided, lt.ageCollided = cube.Pos{}, false, 0
|
||||
|
||||
if result == nil {
|
||||
return m
|
||||
}
|
||||
|
||||
for i := 0; i < lt.conf.ParticleCount; i++ {
|
||||
tx.AddParticle(result.Position(), lt.conf.Particle)
|
||||
}
|
||||
if lt.conf.Sound != nil {
|
||||
tx.PlaySound(result.Position(), lt.conf.Sound)
|
||||
}
|
||||
|
||||
switch r := result.(type) {
|
||||
case trace.EntityResult:
|
||||
if l, ok := r.Entity().(Living); ok {
|
||||
if lt.conf.Damage >= 0 {
|
||||
lt.hitEntity(l, e, vel)
|
||||
}
|
||||
lt.collidedEntities = append(lt.collidedEntities, l.H())
|
||||
}
|
||||
case trace.BlockResult:
|
||||
bpos := r.BlockPosition()
|
||||
if h, ok := tx.Block(bpos).(block.ProjectileHitter); ok {
|
||||
h.ProjectileHit(bpos, tx, e, r.Face())
|
||||
}
|
||||
if lt.conf.SurviveBlockCollision {
|
||||
lt.hitBlockSurviving(e, r, m, tx)
|
||||
return m
|
||||
}
|
||||
lt.close = true
|
||||
}
|
||||
if lt.conf.Hit != nil {
|
||||
lt.conf.Hit(e, tx, result)
|
||||
}
|
||||
|
||||
if len(lt.collidedEntities) > lt.conf.PiercingLevel {
|
||||
lt.close = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// tickAttached performs the attached logic for a projectile. It checks if the
|
||||
// projectile is still attached to a block and if it can be picked up.
|
||||
func (lt *ProjectileBehaviour) tickAttached(e *Ent, tx *world.Tx) bool {
|
||||
boxes := tx.Block(lt.collisionPos).Model().BBox(lt.collisionPos, tx)
|
||||
box := e.H().Type().BBox(e).Translate(e.Position())
|
||||
|
||||
for _, bb := range boxes {
|
||||
if box.IntersectsWith(bb.Translate(lt.collisionPos.Vec3()).Grow(0.05)) {
|
||||
if lt.ageCollided > 5 && !lt.conf.DisablePickup {
|
||||
lt.tryPickup(e, tx)
|
||||
}
|
||||
lt.ageCollided++
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// tryPickup checks for nearby projectile collectors and closes the entity if
|
||||
// one was found.
|
||||
func (lt *ProjectileBehaviour) tryPickup(e *Ent, tx *world.Tx) {
|
||||
translated := e.H().Type().BBox(e).Translate(e.Position())
|
||||
grown := translated.GrowVec3(mgl64.Vec3{1, 0.5, 1})
|
||||
for other := range tx.EntitiesWithin(translated.Grow(2)) {
|
||||
if !other.H().Type().BBox(other).Translate(other.Position()).IntersectsWith(grown) {
|
||||
continue
|
||||
}
|
||||
collector, ok := other.(Collector)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := collector.Collect(lt.conf.PickupItem); !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// A collector was within range and able to pick up the entity.
|
||||
lt.close = true
|
||||
for _, viewer := range tx.Viewers(e.Position()) {
|
||||
viewer.ViewEntityAction(e, PickedUpAction{Collector: collector})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hitBlockSurviving is called if
|
||||
// ProjectileBehaviourConfig.SurviveBlockCollision is set to true and the
|
||||
// projectile collides with a block. If the resulting velocity is roughly 0,
|
||||
// it sets the projectile as having collided with the block.
|
||||
func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m *Movement, tx *world.Tx) {
|
||||
// Create an epsilon for deciding if the projectile has slowed down enough
|
||||
// for us to consider it as having collided for the final time. We take the
|
||||
// square root because FloatEqualThreshold squares it, which is not what we
|
||||
// want.
|
||||
eps := math.Sqrt(0.1 * (1 - lt.conf.BlockCollisionVelocityMultiplier))
|
||||
if mgl64.FloatEqualThreshold(e.Velocity().Len(), 0, eps) {
|
||||
e.SetVelocity(mgl64.Vec3{})
|
||||
lt.collisionPos, lt.collided = r.BlockPosition(), true
|
||||
|
||||
for _, v := range tx.Viewers(m.pos) {
|
||||
v.ViewEntityAction(e, ArrowShakeAction{Duration: time.Millisecond * 350})
|
||||
v.ViewEntityState(e)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// hitEntity is called when a projectile hits a Living. It deals damage to the
|
||||
// entity and knocks it back. Additionally, it applies any potion effects and
|
||||
// fire if applicable.
|
||||
func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) {
|
||||
owner, _ := lt.conf.Owner.Entity(e.tx)
|
||||
src := ProjectileDamageSource{Projectile: e, Owner: owner}
|
||||
dmg := math.Ceil(lt.conf.Damage * vel.Len())
|
||||
if lt.conf.Critical {
|
||||
dmg += rand.Float64() * dmg / 2
|
||||
}
|
||||
// TODO: Piercing arrows should bypass shield blocking when shields are implemented.
|
||||
if _, vulnerable := l.Hurt(dmg, src); vulnerable {
|
||||
l.KnockBack(l.Position().Sub(vel), 0.45+lt.conf.KnockBackForceAddend, 0.3608+lt.conf.KnockBackHeightAddend)
|
||||
|
||||
for _, eff := range lt.conf.Potion.Effects() {
|
||||
if lasting, ok := eff.Type().(effect.LastingType); ok {
|
||||
l.AddEffect(effect.New(lasting, eff.Level(), time.Duration(float64(eff.Duration())/8)))
|
||||
continue
|
||||
}
|
||||
l.AddEffect(eff)
|
||||
}
|
||||
if flammable, ok := l.(Flammable); ok && e.OnFireDuration() > 0 {
|
||||
flammable.SetOnFire(time.Second * 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tickMovement ticks the movement of a projectile. It updates the position and
|
||||
// rotation of the projectile based on its velocity and updates the velocity
|
||||
// based on gravity and drag.
|
||||
func (lt *ProjectileBehaviour) tickMovement(e *Ent, tx *world.Tx) (*Movement, trace.Result) {
|
||||
pos, vel := e.Position(), e.Velocity()
|
||||
viewers := tx.Viewers(pos)
|
||||
|
||||
velBefore := vel
|
||||
vel = lt.mc.applyHorizontalForces(tx, pos, lt.mc.applyVerticalForces(vel))
|
||||
rot := cube.Rotation{
|
||||
mgl64.RadToDeg(math.Atan2(vel[0], vel[2])),
|
||||
mgl64.RadToDeg(math.Atan2(vel[1], math.Hypot(vel[0], vel[2]))),
|
||||
}
|
||||
|
||||
var (
|
||||
end = pos.Add(vel)
|
||||
hit trace.Result
|
||||
ok bool
|
||||
)
|
||||
if !mgl64.FloatEqual(end.Sub(pos).LenSqr(), 0) {
|
||||
if hit, ok = trace.Perform(pos, end, tx, e.H().Type().BBox(e).Grow(1.0), lt.ignores(e)); ok {
|
||||
if _, ok := hit.(trace.BlockResult); ok {
|
||||
// Undo the gravity because the velocity as a result of gravity
|
||||
// at the point of collision should be 0.
|
||||
vel[1] = (vel[1] + lt.mc.Gravity) / (1 - lt.mc.Drag)
|
||||
x, y, z := vel.Mul(lt.conf.BlockCollisionVelocityMultiplier).Elem()
|
||||
// Calculate multipliers for all coordinates: 1 for the ones that
|
||||
// weren't on the same axis as the one collided with, -1 for the one
|
||||
// that was on that axis to deflect the projectile.
|
||||
mx, my, mz := hit.Face().Axis().Vec3().Mul(-2).Add(mgl64.Vec3{1, 1, 1}).Elem()
|
||||
|
||||
vel = mgl64.Vec3{x * mx, y * my, z * mz}
|
||||
} else if lt.conf.PiercingLevel == 0 {
|
||||
vel = zeroVec3
|
||||
}
|
||||
end = hit.Position()
|
||||
}
|
||||
}
|
||||
return &Movement{v: viewers, e: e, pos: end, vel: vel, dpos: end.Sub(pos), dvel: vel.Sub(velBefore), rot: rot}, hit
|
||||
}
|
||||
|
||||
// ignores returns a function to ignore entities in trace.Perform that are
|
||||
// either a spectator, not living, the entity itself, its owner in the first
|
||||
// 5 ticks, or an entity it already collided with.
|
||||
func (lt *ProjectileBehaviour) ignores(e *Ent) trace.EntityFilter {
|
||||
return func(seq iter.Seq[world.Entity]) iter.Seq[world.Entity] {
|
||||
return func(yield func(world.Entity) bool) {
|
||||
for other := range seq {
|
||||
g, ok := other.(interface{ GameMode() world.GameMode })
|
||||
spectator := ok && !g.GameMode().HasCollision()
|
||||
itself := e.H() == other.H()
|
||||
_, living := other.(Living)
|
||||
owner := e.data.Age < time.Second/4 && lt.conf.Owner == other.H()
|
||||
collidedEntity := slices.Contains(lt.collidedEntities, other.H())
|
||||
if spectator || itself || !living || owner || collidedEntity {
|
||||
continue
|
||||
}
|
||||
if !yield(other) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/enchantment"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
)
|
||||
|
||||
// DefaultRegistry is a world.EntityRegistry that registers all default entities
|
||||
// implemented by Dragonfly.
|
||||
var DefaultRegistry = conf.New([]world.EntityType{
|
||||
AreaEffectCloudType,
|
||||
ArrowType,
|
||||
BottleOfEnchantingType,
|
||||
EggType,
|
||||
EnderPearlType,
|
||||
ExperienceOrbType,
|
||||
FallingBlockType,
|
||||
FireworkType,
|
||||
ItemType,
|
||||
LightningType,
|
||||
LingeringPotionType,
|
||||
SnowballType,
|
||||
SplashPotionType,
|
||||
TNTType,
|
||||
TextType,
|
||||
})
|
||||
|
||||
var conf = world.EntityRegistryConfig{
|
||||
TNT: NewTNT,
|
||||
Egg: NewEgg,
|
||||
Snowball: NewSnowball,
|
||||
BottleOfEnchanting: NewBottleOfEnchanting,
|
||||
EnderPearl: NewEnderPearl,
|
||||
FallingBlock: NewFallingBlock,
|
||||
Lightning: NewLightning,
|
||||
Firework: func(opts world.EntitySpawnOpts, firework world.Item, owner world.Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *world.EntityHandle {
|
||||
return newFirework(opts, firework.(item.Firework), owner, sidewaysVelocityMultiplier, upwardsAcceleration, attached)
|
||||
},
|
||||
Item: func(opts world.EntitySpawnOpts, it any) *world.EntityHandle {
|
||||
return NewItem(opts, it.(item.Stack))
|
||||
},
|
||||
LingeringPotion: func(opts world.EntitySpawnOpts, t any, owner world.Entity) *world.EntityHandle {
|
||||
return NewLingeringPotion(opts, t.(potion.Potion), owner)
|
||||
},
|
||||
SplashPotion: func(opts world.EntitySpawnOpts, t any, owner world.Entity) *world.EntityHandle {
|
||||
return NewSplashPotion(opts, t.(potion.Potion), owner)
|
||||
},
|
||||
Arrow: func(opts world.EntitySpawnOpts, arrow world.ArrowSpawnConfig) *world.EntityHandle {
|
||||
tip := arrow.Tip.(potion.Potion)
|
||||
conf := arrowConf
|
||||
conf.Damage, conf.Potion, conf.Owner = arrow.Damage, tip, arrow.Owner.H()
|
||||
conf.KnockBackForceAddend = float64(arrow.PunchLevel) * enchantment.Punch.KnockBackMultiplier()
|
||||
conf.DisablePickup = arrow.DisablePickup
|
||||
if arrow.ObtainArrowOnPickup {
|
||||
conf.PickupItem = item.NewStack(item.Arrow{Tip: tip}, 1)
|
||||
}
|
||||
conf.Critical = arrow.Critical
|
||||
conf.PiercingLevel = arrow.PiercingLevel
|
||||
return opts.New(ArrowType, conf)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/particle"
|
||||
)
|
||||
|
||||
// NewSnowball creates a snowball entity at a position with an owner entity.
|
||||
func NewSnowball(opts world.EntitySpawnOpts, owner world.Entity) *world.EntityHandle {
|
||||
conf := snowballConf
|
||||
conf.Owner = owner.H()
|
||||
return opts.New(SnowballType, conf)
|
||||
}
|
||||
|
||||
var snowballConf = ProjectileBehaviourConfig{
|
||||
Gravity: 0.03,
|
||||
Drag: 0.01,
|
||||
Particle: particle.SnowballPoof{},
|
||||
ParticleCount: 6,
|
||||
}
|
||||
|
||||
// SnowballType is a world.EntityType implementation for snowballs.
|
||||
var SnowballType snowballType
|
||||
|
||||
type snowballType struct{}
|
||||
|
||||
func (t snowballType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (snowballType) EncodeEntity() string { return "minecraft:snowball" }
|
||||
func (snowballType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
|
||||
func (snowballType) DecodeNBT(_ map[string]any, data *world.EntityData) {
|
||||
data.Data = snowballConf.New()
|
||||
}
|
||||
func (snowballType) EncodeNBT(*world.EntityData) map[string]any { return nil }
|
||||
@@ -0,0 +1,60 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/internal/nbtconv"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/particle"
|
||||
"github.com/df-mc/dragonfly/server/world/sound"
|
||||
)
|
||||
|
||||
// NewSplashPotion creates a splash potion. SplashPotion is an item that grants
|
||||
// effects when thrown.
|
||||
func NewSplashPotion(opts world.EntitySpawnOpts, t potion.Potion, owner world.Entity) *world.EntityHandle {
|
||||
colour, _ := effect.ResultingColour(t.Effects())
|
||||
|
||||
conf := splashPotionConf
|
||||
conf.Potion = t
|
||||
conf.Particle = particle.Splash{Colour: colour}
|
||||
conf.Hit = potionSplash(1, t, false)
|
||||
conf.Owner = owner.H()
|
||||
|
||||
return opts.New(SplashPotionType, conf)
|
||||
}
|
||||
|
||||
var splashPotionConf = ProjectileBehaviourConfig{
|
||||
Gravity: 0.05,
|
||||
Drag: 0.01,
|
||||
Damage: -1,
|
||||
Sound: sound.GlassBreak{},
|
||||
}
|
||||
|
||||
// SplashPotionType is a world.EntityType implementation for SplashPotion.
|
||||
var SplashPotionType splashPotionType
|
||||
|
||||
type splashPotionType struct{}
|
||||
|
||||
func (t splashPotionType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (splashPotionType) EncodeEntity() string { return "minecraft:splash_potion" }
|
||||
func (splashPotionType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.125, 0, -0.125, 0.125, 0.25, 0.125)
|
||||
}
|
||||
|
||||
func (splashPotionType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
conf := splashPotionConf
|
||||
conf.Potion = potion.From(nbtconv.Int32(m, "PotionId"))
|
||||
colour, _ := effect.ResultingColour(conf.Potion.Effects())
|
||||
conf.Particle = particle.Splash{Colour: colour}
|
||||
conf.Hit = potionSplash(1, conf.Potion, false)
|
||||
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
func (splashPotionType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
return map[string]any{"PotionId": int32(data.Data.(*ProjectileBehaviour).conf.Potion.Uint8())}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/block/cube/trace"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SplashableBlock is a block that can be splashed with a splash bottle.
|
||||
type SplashableBlock interface {
|
||||
world.Block
|
||||
// Splash is called when a water bottle splashes onto a block.
|
||||
Splash(tx *world.Tx, pos cube.Pos)
|
||||
}
|
||||
|
||||
// SplashableEntity is an entity that can be splashed with a splash bottle.
|
||||
type SplashableEntity interface {
|
||||
world.Entity
|
||||
// Splash is called when a water bottle splashes onto an entity.
|
||||
Splash(tx *world.Tx, pos mgl64.Vec3)
|
||||
}
|
||||
|
||||
// potionSplash returns a function that creates a potion splash with a specific
|
||||
// duration multiplier and potion type.
|
||||
func potionSplash(durMul float64, pot potion.Potion, linger bool) func(e *Ent, tx *world.Tx, res trace.Result) {
|
||||
return func(e *Ent, tx *world.Tx, res trace.Result) {
|
||||
pos := e.Position()
|
||||
effects := pot.Effects()
|
||||
box := e.H().Type().BBox(e).Translate(pos)
|
||||
|
||||
if len(effects) > 0 {
|
||||
for otherE := range filterLiving(tx.EntitiesWithin(box.GrowVec3(mgl64.Vec3{8.25, 4.25, 8.25}))) {
|
||||
otherPos := otherE.Position()
|
||||
if !otherE.H().Type().BBox(otherE).Translate(otherPos).IntersectsWith(box.GrowVec3(mgl64.Vec3{4.125, 2.125, 4.125})) {
|
||||
continue
|
||||
}
|
||||
|
||||
dist := otherPos.Sub(pos).Len()
|
||||
if dist > 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
f := 1 - dist/4
|
||||
if entityResult, ok := res.(trace.EntityResult); ok && entityResult.Entity().H() == otherE.H() {
|
||||
f = 1
|
||||
}
|
||||
|
||||
splashed := otherE.(Living)
|
||||
for _, eff := range effects {
|
||||
if _, ok := eff.Type().(effect.LastingType); !ok {
|
||||
splashed.AddEffect(effect.NewInstantWithPotency(eff.Type(), eff.Level(), f))
|
||||
continue
|
||||
}
|
||||
|
||||
dur := time.Duration(float64(eff.Duration()) * durMul * f)
|
||||
if dur < time.Second {
|
||||
continue
|
||||
}
|
||||
splashed.AddEffect(effect.New(eff.Type().(effect.LastingType), eff.Level(), dur))
|
||||
}
|
||||
}
|
||||
} else if pot == potion.Water() {
|
||||
switch result := res.(type) {
|
||||
case trace.BlockResult:
|
||||
blockPos := result.BlockPosition().Side(result.Face())
|
||||
if tx.Block(blockPos) == fire() {
|
||||
tx.SetBlock(blockPos, nil, nil)
|
||||
}
|
||||
|
||||
for _, f := range cube.HorizontalFaces() {
|
||||
if h := blockPos.Side(f); tx.Block(h) == fire() {
|
||||
tx.SetBlock(h, nil, nil)
|
||||
}
|
||||
|
||||
if b, ok := tx.Block(blockPos.Side(f)).(SplashableBlock); ok {
|
||||
b.Splash(tx, blockPos.Side(f))
|
||||
}
|
||||
}
|
||||
|
||||
resultPos := result.BlockPosition()
|
||||
if b, ok := tx.Block(resultPos).(SplashableBlock); ok {
|
||||
b.Splash(tx, resultPos)
|
||||
}
|
||||
case trace.EntityResult:
|
||||
// TODO: Damage endermen, blazes, striders and snow golems when implemented and rehydrate axolotls.
|
||||
}
|
||||
|
||||
for otherE := range filterLiving(tx.EntitiesWithin(box.GrowVec3(mgl64.Vec3{8.25, 4.25, 8.25}))) {
|
||||
if splashE, ok := otherE.(SplashableEntity); ok {
|
||||
splashE.Splash(tx, otherE.Position())
|
||||
}
|
||||
}
|
||||
}
|
||||
if linger {
|
||||
tx.AddEntity(NewAreaEffectCloud(world.EntitySpawnOpts{Position: pos}, pot))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StationaryBehaviourConfig holds settings that influence the way
|
||||
// StationaryBehaviour operates. StationaryBehaviourConfig.New() may be called
|
||||
// to create a new behaviour with this config.
|
||||
type StationaryBehaviourConfig struct {
|
||||
// ExistenceDuration is the duration that an entity with this behaviour
|
||||
// should last. Once this time expires, the entity is closed. If
|
||||
// ExistenceDuration is 0, the entity will never expire automatically.
|
||||
ExistenceDuration time.Duration
|
||||
// SpawnSounds is a slice of sounds to be played upon the spawning of the
|
||||
// entity.
|
||||
SpawnSounds []world.Sound
|
||||
// Tick is a function called every world tick. It may be used to implement
|
||||
// additional behaviour for stationary entities.
|
||||
Tick func(e *Ent, tx *world.Tx)
|
||||
}
|
||||
|
||||
func (conf StationaryBehaviourConfig) Apply(data *world.EntityData) {
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
// New creates a StationaryBehaviour using the settings provided in conf.
|
||||
func (conf StationaryBehaviourConfig) New() *StationaryBehaviour {
|
||||
if conf.ExistenceDuration == 0 {
|
||||
conf.ExistenceDuration = math.MaxInt64
|
||||
}
|
||||
return &StationaryBehaviour{conf: conf}
|
||||
}
|
||||
|
||||
// StationaryBehaviour implements the behaviour of an entity that is unable to
|
||||
// move, such as a text entity or an area effect cloud. Applying velocity to
|
||||
// such entities will not move them.
|
||||
type StationaryBehaviour struct {
|
||||
conf StationaryBehaviourConfig
|
||||
close bool
|
||||
}
|
||||
|
||||
// Tick checks if the entity should be closed and runs whatever additional
|
||||
// behaviour the entity might require.
|
||||
func (s *StationaryBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
||||
if s.close {
|
||||
_ = e.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Age() == 0 {
|
||||
for _, ss := range s.conf.SpawnSounds {
|
||||
tx.PlaySound(e.Position(), ss)
|
||||
}
|
||||
}
|
||||
if s.conf.Tick != nil {
|
||||
s.conf.Tick(e, tx)
|
||||
}
|
||||
|
||||
if e.Age() > s.conf.ExistenceDuration {
|
||||
s.close = true
|
||||
}
|
||||
// Stationary entities never move. Always return nil here.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Immobile always returns true.
|
||||
func (s *StationaryBehaviour) Immobile() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
// NewText creates and returns a new Text entity with the text and position provided.
|
||||
func NewText(text string, pos mgl64.Vec3) *world.EntityHandle {
|
||||
return world.EntitySpawnOpts{Position: pos, NameTag: text}.New(TextType, textConf)
|
||||
}
|
||||
|
||||
var textConf StationaryBehaviourConfig
|
||||
|
||||
// TextType is a world.EntityType implementation for Text.
|
||||
var TextType textType
|
||||
|
||||
type textType struct{}
|
||||
|
||||
func (t textType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
func (textType) EncodeEntity() string { return "dragonfly:text" }
|
||||
func (textType) BBox(world.Entity) cube.BBox { return cube.BBox{} }
|
||||
func (textType) NetworkEncodeEntity() string { return "minecraft:falling_block" }
|
||||
|
||||
func (textType) DecodeNBT(_ map[string]any, data *world.EntityData) { data.Data = textConf.New() }
|
||||
func (textType) EncodeNBT(_ *world.EntityData) map[string]any { return nil }
|
||||
@@ -0,0 +1,59 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"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/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewTNT creates a new primed TNT entity.
|
||||
func NewTNT(opts world.EntitySpawnOpts, fuse time.Duration) *world.EntityHandle {
|
||||
conf := tntConf
|
||||
conf.ExistenceDuration = fuse
|
||||
if opts.Velocity.Len() == 0 {
|
||||
angle := rand.Float64() * math.Pi * 2
|
||||
opts.Velocity = mgl64.Vec3{-math.Sin(angle) * 0.02, 0.1, -math.Cos(angle) * 0.02}
|
||||
}
|
||||
return opts.New(TNTType, conf)
|
||||
}
|
||||
|
||||
var tntConf = PassiveBehaviourConfig{
|
||||
Gravity: 0.04,
|
||||
Drag: 0.02,
|
||||
Expire: explodeTNT,
|
||||
}
|
||||
|
||||
// explodeTNT creates an explosion at the position of e.
|
||||
func explodeTNT(e *Ent, tx *world.Tx) {
|
||||
block.ExplosionConfig{ItemDropChance: 1}.Explode(tx, e.Position())
|
||||
}
|
||||
|
||||
// TNTType is a world.EntityType implementation for TNT.
|
||||
var TNTType tntType
|
||||
|
||||
type tntType struct{}
|
||||
|
||||
func (t tntType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
return &Ent{tx: tx, handle: handle, data: data}
|
||||
}
|
||||
|
||||
func (tntType) EncodeEntity() string { return "minecraft:tnt" }
|
||||
func (tntType) NetworkOffset() float64 { return 0.49 }
|
||||
func (tntType) BBox(world.Entity) cube.BBox {
|
||||
return cube.Box(-0.49, 0, -0.49, 0.49, 0.98, 0.49)
|
||||
}
|
||||
|
||||
func (t tntType) DecodeNBT(m map[string]any, data *world.EntityData) {
|
||||
conf := tntConf
|
||||
conf.ExistenceDuration = nbtconv.TickDuration[uint8](m, "Fuse")
|
||||
data.Data = conf.New()
|
||||
}
|
||||
|
||||
func (tntType) EncodeNBT(data *world.EntityData) map[string]any {
|
||||
return map[string]any{"Fuse": uint8(data.Data.(*PassiveBehaviour).Fuse().Milliseconds() / 50)}
|
||||
}
|
||||
Reference in New Issue
Block a user