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

This commit is contained in:
2026-07-09 08:33:57 +08:00
commit 26ed99fda6
845 changed files with 75419 additions and 0 deletions
+66
View File
@@ -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() {}
+83
View File
@@ -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
}
}
+98
View File
@@ -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
}
+63
View File
@@ -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
}
+95
View File
@@ -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 }
+22
View File
@@ -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
}
+132
View File
@@ -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()
}
+37
View File
@@ -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}
}
+18
View File
@@ -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}
}
+28
View File
@@ -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}
}
+18
View File
@@ -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}
}
+191
View File
@@ -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)
}
+30
View File
@@ -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}
}
+17
View File
@@ -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}
}
+27
View File
@@ -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}
}
+33
View File
@@ -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}
}
+28
View File
@@ -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}
}
+35
View File
@@ -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 }
+33
View File
@@ -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() {}
+40
View File
@@ -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}
}
+18
View File
@@ -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}
}
+18
View File
@@ -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}
}
+24
View File
@@ -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}
}
+18
View File
@@ -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}
}
+18
View File
@@ -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}
}
+42
View File
@@ -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 }
+36
View File
@@ -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() {}
+62
View File
@@ -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
}
+30
View File
@@ -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}
}
+28
View File
@@ -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}
}
+18
View File
@@ -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}
}
+41
View File
@@ -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}
}
+35
View File
@@ -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}
}
+23
View File
@@ -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}
}
+18
View File
@@ -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}
}
+27
View File
@@ -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}
}
+38
View File
@@ -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 }
+40
View File
@@ -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 }
+61
View File
@@ -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 }
+141
View File
@@ -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
}
+117
View File
@@ -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
}
+66
View File
@@ -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())}
}
+124
View File
@@ -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
}
+45
View File
@@ -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)}
}
+137
View File
@@ -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)
}
+60
View File
@@ -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)}
}
+146
View File
@@ -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
}
}
}
}
+13
View File
@@ -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()
}
+9
View File
@@ -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() {}
+42
View File
@@ -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)
}
+66
View File
@@ -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),
}
}
+198
View File
@@ -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)
}
+125
View File
@@ -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{} }
+54
View File
@@ -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())}
}
+54
View File
@@ -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)
}
+193
View File
@@ -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
}
+104
View File
@@ -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
}
+361
View File
@@ -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
}
}
}
}
}
+63
View File
@@ -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)
},
}
+40
View File
@@ -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 }
+60
View File
@@ -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())}
}
+102
View File
@@ -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))
}
}
}
+72
View File
@@ -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
}
+29
View File
@@ -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 }
+59
View File
@@ -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)}
}