diff --git a/server/block/block.go b/server/block/block.go index c89d76304..70bccf391 100644 --- a/server/block/block.go +++ b/server/block/block.go @@ -20,6 +20,14 @@ type Activatable interface { Activate(pos cube.Pos, clickedFace cube.Face, tx *world.Tx, u item.User, ctx *item.UseContext) bool } +// WindChargeAffected represents a block that is toggled by a wind charge +// burst, such as doors, trapdoors and fence gates. +// TODO: Buttons, levers, bells and candles should also implement this. +type WindChargeAffected interface { + Activatable + WindChargeAffected() +} + // Pickable represents a block that may give a different item then the block itself when picked. type Pickable interface { // Pick returns the item that is picked when the block is picked. diff --git a/server/block/copper_door.go b/server/block/copper_door.go index c73f3f312..b75c7d43f 100644 --- a/server/block/copper_door.go +++ b/server/block/copper_door.go @@ -150,6 +150,9 @@ func (d CopperDoor) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.Use return true } +// WindChargeAffected ... +func (CopperDoor) WindChargeAffected() {} + func (d CopperDoor) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) { attemptOxidation(pos, tx, r, d) } diff --git a/server/block/copper_trapdoor.go b/server/block/copper_trapdoor.go index 919e1a091..6fbfc4088 100644 --- a/server/block/copper_trapdoor.go +++ b/server/block/copper_trapdoor.go @@ -93,6 +93,9 @@ func (t CopperTrapdoor) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item return true } +// WindChargeAffected ... +func (CopperTrapdoor) WindChargeAffected() {} + func (t CopperTrapdoor) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) { attemptOxidation(pos, tx, r, t) } diff --git a/server/block/wood_door.go b/server/block/wood_door.go index ea23271b7..072cfb97a 100644 --- a/server/block/wood_door.go +++ b/server/block/wood_door.go @@ -119,6 +119,9 @@ func (d WoodDoor) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.User, return true } +// WindChargeAffected ... +func (WoodDoor) WindChargeAffected() {} + // BreakInfo ... func (d WoodDoor) BreakInfo() BreakInfo { return newBreakInfo(3, alwaysHarvestable, axeEffective, oneOf(d)) diff --git a/server/block/wood_fence_gate.go b/server/block/wood_fence_gate.go index 60ff01dae..703be3b9c 100644 --- a/server/block/wood_fence_gate.go +++ b/server/block/wood_fence_gate.go @@ -80,7 +80,7 @@ func (f WoodFenceGate) shouldBeLowered(pos cube.Pos, tx *world.Tx) bool { // Activate ... func (f WoodFenceGate) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool { f.Open = !f.Open - if f.Open && f.Facing.Opposite() == u.Rotation().Direction() { + if f.Open && u != nil && f.Facing.Opposite() == u.Rotation().Direction() { f.Facing = f.Facing.Opposite() } tx.SetBlock(pos, f, nil) @@ -92,6 +92,9 @@ func (f WoodFenceGate) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item. return true } +// WindChargeAffected ... +func (WoodFenceGate) WindChargeAffected() {} + // SideClosed ... func (f WoodFenceGate) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { return false diff --git a/server/block/wood_trapdoor.go b/server/block/wood_trapdoor.go index e525fe7a9..f3ab3e667 100644 --- a/server/block/wood_trapdoor.go +++ b/server/block/wood_trapdoor.go @@ -67,6 +67,9 @@ func (t WoodTrapdoor) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, _ item.U return true } +// WindChargeAffected ... +func (WoodTrapdoor) WindChargeAffected() {} + // BreakInfo ... func (t WoodTrapdoor) BreakInfo() BreakInfo { return newBreakInfo(3, alwaysHarvestable, axeEffective, oneOf(t)) diff --git a/server/entity/register.go b/server/entity/register.go index e9c54922e..e6db53755 100644 --- a/server/entity/register.go +++ b/server/entity/register.go @@ -25,6 +25,7 @@ var DefaultRegistry = conf.New([]world.EntityType{ SplashPotionType, TNTType, TextType, + WindChargeType, }) var conf = world.EntityRegistryConfig{ @@ -35,6 +36,7 @@ var conf = world.EntityRegistryConfig{ EnderPearl: NewEnderPearl, FallingBlock: NewFallingBlock, Lightning: NewLightning, + WindCharge: NewWindCharge, 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) }, diff --git a/server/entity/wind_charge.go b/server/entity/wind_charge.go new file mode 100644 index 000000000..04fd6413e --- /dev/null +++ b/server/entity/wind_charge.go @@ -0,0 +1,113 @@ +package entity + +import ( + "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/world" + "github.com/df-mc/dragonfly/server/world/particle" + "github.com/df-mc/dragonfly/server/world/sound" +) + +// windChargeBurstRadius is the maximum radius within which entities are +// knocked back by a wind charge burst. +const windChargeBurstRadius = 2.5 + +// NewWindCharge creates a wind charge entity at a position with an owner +// entity. Wind charges fly in a straight line (no gravity) and create a burst +// of wind on impact that knocks back nearby entities and toggles certain +// interactive blocks. +func NewWindCharge(opts world.EntitySpawnOpts, owner world.Entity) *world.EntityHandle { + conf := windChargeConf + conf.Owner = owner.H() + return opts.New(WindChargeType, conf) +} + +// TODO: Wind charges should have increased drag when travelling through water +// or lava, but per-medium drag is not yet supported by ProjectileBehaviour. +var windChargeConf = ProjectileBehaviourConfig{ + Gravity: 0, + Drag: 0, + Damage: -1, + Particle: particle.WindExplosion{}, + Sound: sound.WindChargeBurst{}, + Hit: windChargeBurst, +} + +// windChargeBurst is called when a wind charge hits a target. It deals 1 HP +// damage on a direct entity hit, knocks back all living entities within the +// burst radius, and toggles interactive blocks at the impact point. +func windChargeBurst(e *Ent, tx *world.Tx, target trace.Result) { + pos := target.Position() + owner, _ := e.Behaviour().(*ProjectileBehaviour).Owner().Entity(tx) + + // Deal flat 1 HP damage to the directly-hit entity. + if er, ok := target.(trace.EntityResult); ok { + if l, ok := er.Entity().(Living); ok { + l.Hurt(1, ProjectileDamageSource{Projectile: e, Owner: owner}) + } + } + + // Apply knockback to all living entities within the burst radius. Impact + // scales with distance (closer = stronger) and is split into horizontal + // and vertical components. + box := e.H().Type().BBox(e).Translate(pos).Grow(windChargeBurstRadius) + for other := range tx.EntitiesWithin(box) { + if other.H() == e.H() { + continue + } + l, ok := other.(Living) + if !ok { + continue + } + entityPos := other.Position() + dist := entityPos.Sub(pos).Len() + impact := 1.3 - dist/windChargeBurstRadius + if impact <= 0 { + continue + } + + vel := l.Velocity() + // If the entity is directly above the impact, apply a flat upward + // boost. Otherwise split into horizontal and vertical components. + dx := entityPos[0] - pos[0] + dz := entityPos[2] - pos[2] + if dx*dx+dz*dz < 0.01 { + vel[1] += 1.1 + } else { + dir := entityPos.Sub(pos) + dir[1] = 0 + dir = dir.Normalize() + vel = vel.Add(dir.Mul(impact)) + vel[1] += impact * 0.4 + } + l.SetVelocity(vel) + } + + // Toggle interactive blocks at the impact point. + if r, ok := target.(trace.BlockResult); ok { + pos := r.BlockPosition() + if b, ok := tx.Block(pos).(block.WindChargeAffected); ok { + b.Activate(pos, r.Face(), tx, nil, nil) + } + } +} + +// WindChargeType is a world.EntityType implementation for WindCharge. +var WindChargeType windChargeType + +type windChargeType struct{} + +func (windChargeType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + return &Ent{tx: tx, handle: handle, data: data} +} + +func (windChargeType) EncodeEntity() string { return "minecraft:wind_charge_projectile" } +func (windChargeType) BBox(world.Entity) cube.BBox { + return cube.Box(-0.15625, 0, -0.15625, 0.15625, 0.3125, 0.15625) +} + +func (windChargeType) DecodeNBT(_ map[string]any, data *world.EntityData) { + data.Data = windChargeConf.New() +} +func (windChargeType) EncodeNBT(*world.EntityData) map[string]any { return nil } diff --git a/server/item/register.go b/server/item/register.go index 7e18533c0..3b5cf17ca 100644 --- a/server/item/register.go +++ b/server/item/register.go @@ -124,6 +124,7 @@ func init() { world.RegisterItem(TurtleShell{}) world.RegisterItem(WarpedFungusOnAStick{}) world.RegisterItem(Wheat{}) + world.RegisterItem(WindCharge{}) world.RegisterItem(WrittenBook{}) for _, t := range ArmourTiers() { world.RegisterItem(Helmet{Tier: t}) diff --git a/server/item/wind_charge.go b/server/item/wind_charge.go new file mode 100644 index 000000000..914a4b152 --- /dev/null +++ b/server/item/wind_charge.go @@ -0,0 +1,33 @@ +package item + +import ( + "time" + + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" +) + +// WindCharge is a throwable item that creates a burst of wind on impact, knocking back nearby entities and +// toggling certain blocks such as doors, trapdoors and fence gates. +type WindCharge struct{} + +// Use ... +func (WindCharge) Use(tx *world.Tx, user User, ctx *UseContext) bool { + create := tx.World().EntityRegistry().Config().WindCharge + opts := world.EntitySpawnOpts{Position: eyePosition(user), Velocity: user.Rotation().Vec3().Mul(3.0)} + tx.AddEntity(create(opts, user)) + tx.PlaySound(user.Position(), sound.ItemThrow{}) + + ctx.SubtractFromCount(1) + return true +} + +// Cooldown ... +func (WindCharge) Cooldown() time.Duration { + return time.Millisecond * 500 +} + +// EncodeItem ... +func (WindCharge) EncodeItem() (name string, meta int16) { + return "minecraft:wind_charge", 0 +} diff --git a/server/session/world.go b/server/session/world.go index 2b9dab62e..e88ec7912 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -483,6 +483,11 @@ func (s *Session) ViewParticle(pos mgl64.Vec3, p world.Particle) { EventType: packet.LevelEventParticleLegacyEvent | 88, Position: vec64To32(pos), }) + case particle.WindExplosion: + s.writePacket(&packet.LevelEvent{ + EventType: packet.LevelEventParticlesWindExplosion, + Position: vec64To32(pos), + }) } } @@ -652,6 +657,8 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) case sound.Dream(): pk.SoundType = packet.SoundEventGoatCall7 } + case sound.WindChargeBurst: + pk.SoundType = packet.SoundEventWindChargeBurst case sound.FireExtinguish: pk.SoundType = packet.SoundEventExtinguishFire case sound.Ignite: diff --git a/server/world/entity.go b/server/world/entity.go index dcc415ce9..a8bfab7c0 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -373,6 +373,7 @@ type EntityRegistryConfig struct { LingeringPotion func(opts EntitySpawnOpts, t any, owner Entity) *EntityHandle Snowball func(opts EntitySpawnOpts, owner Entity) *EntityHandle SplashPotion func(opts EntitySpawnOpts, t any, owner Entity) *EntityHandle + WindCharge func(opts EntitySpawnOpts, owner Entity) *EntityHandle Lightning func(opts EntitySpawnOpts) *EntityHandle } diff --git a/server/world/particle/entity.go b/server/world/particle/entity.go index f7d7e253a..434e6bf07 100644 --- a/server/world/particle/entity.go +++ b/server/world/particle/entity.go @@ -32,3 +32,6 @@ type Effect struct { // EntityFlame is a particle shown when an entity is set on fire. type EntityFlame struct{ particle } + +// WindExplosion is a particle shown when a wind charge bursts on impact. +type WindExplosion struct{ particle } diff --git a/server/world/sound/item.go b/server/world/sound/item.go index 69ddda6a2..a3837b84b 100644 --- a/server/world/sound/item.go +++ b/server/world/sound/item.go @@ -94,5 +94,8 @@ type GoatHorn struct { // blaze shoots a fireball. type FireCharge struct{ sound } +// WindChargeBurst is a sound played when a wind charge bursts on impact. +type WindChargeBurst struct{ sound } + // Totem is a sound played when a player uses a totem. type Totem struct{ sound }