diff --git a/server/entity/register.go b/server/entity/register.go index b4f4440143..b0d334f1a0 100644 --- a/server/entity/register.go +++ b/server/entity/register.go @@ -26,6 +26,7 @@ var DefaultRegistry = conf.New([]world.EntityType{ SplashPotionType, TNTType, TextType, + TridentType, }) var conf = world.EntityRegistryConfig{ @@ -49,6 +50,16 @@ var conf = world.EntityRegistryConfig{ SplashPotion: func(opts world.EntitySpawnOpts, t any, owner world.Entity) *world.EntityHandle { return NewSplashPotion(opts, t.(potion.Potion), owner) }, + Trident: func(opts world.EntitySpawnOpts, conf world.TridentSpawnConfig) *world.EntityHandle { + c := TridentBehaviourConfig{Damage: conf.Damage, DisablePickup: conf.DisablePickup} + if conf.Owner != nil { + c.Owner = conf.Owner.H() + } + if conf.Item != nil { + c.Item = conf.Item.(item.Stack) + } + return opts.New(TridentType, c) + }, Arrow: func(opts world.EntitySpawnOpts, arrow world.ArrowSpawnConfig) *world.EntityHandle { tip := arrow.Tip.(potion.Potion) conf := arrowConf diff --git a/server/entity/trident.go b/server/entity/trident.go new file mode 100644 index 0000000000..f3b0e28abb --- /dev/null +++ b/server/entity/trident.go @@ -0,0 +1,306 @@ +package entity + +import ( + "math" + + "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/internal/nbtconv" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/enchantment" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +// NewTrident creates a thrown trident entity using the item.Stack passed. +func NewTrident(opts world.EntitySpawnOpts, owner world.Entity, it item.Stack) *world.EntityHandle { + conf := TridentBehaviourConfig{Item: it} + if owner != nil { + conf.Owner = owner.H() + } + return opts.New(TridentType, conf) +} + +// TridentBehaviourConfig allows the configuration of thrown tridents. +type TridentBehaviourConfig struct { + // Owner is the entity that threw the trident. + Owner *world.EntityHandle + // Damage is the base damage dealt by the trident. Defaults to 8 if left + // as 0. + Damage float64 + // Item is the trident item.Stack the projectile was thrown with. + Item item.Stack + // DisablePickup specifies if picking up the trident should be disabled. + // This is the case for tridents thrown in creative mode. + DisablePickup bool + // CollisionPosition specifies the position of the block the trident is + // stuck in. If non-empty, the trident will not move. + CollisionPosition cube.Pos +} + +func (conf TridentBehaviourConfig) Apply(data *world.EntityData) { + data.Data = conf.New() +} + +// New creates a TridentBehaviour using conf. +func (conf TridentBehaviourConfig) New() *TridentBehaviour { + if conf.Damage == 0 { + conf.Damage = 8 + } + proj := ProjectileBehaviourConfig{ + Owner: conf.Owner, + Gravity: 0.05, + Drag: 0.01, + Damage: -1, + SurviveBlockCollision: true, + DisablePickup: conf.DisablePickup, + CollisionPosition: conf.CollisionPosition, + } + if !conf.DisablePickup { + proj.PickupItem = conf.Item + } + return &TridentBehaviour{ProjectileBehaviour: proj.New(), conf: conf} +} + +// TridentBehaviour implements the behaviour of thrown tridents. +type TridentBehaviour struct { + *ProjectileBehaviour + conf TridentBehaviourConfig + + dealtDamage bool + returning bool + returnAge int +} + +// Returning returns true if the trident is currently returning to its owner +// as a result of the loyalty enchantment. +func (b *TridentBehaviour) Returning() bool { + return b.returning +} + +// Glint returns true if the trident stack carried by the entity is enchanted. +func (b *TridentBehaviour) Glint() bool { + return len(b.conf.Item.Enchantments()) > 0 +} + +// Tick runs the tick-based behaviour of a TridentBehaviour and returns the +// Movement within the tick. +func (b *TridentBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { + if b.close { + _ = e.Close() + return nil + } + if b.returning { + return b.tickReturning(e, tx) + } + if b.collided && b.tickAttached(e, tx) { + if b.loyaltyLevel() > 0 && b.ageCollided > 4 { + b.startReturning(e, tx) + return nil + } + if b.ageCollided > 1200 { + b.close = true + } + return nil + } + vel := e.Velocity() + m, result := b.tickMovement(e, tx) + e.data.Pos, e.data.Vel, e.data.Rot = m.pos, m.vel, m.rot + + b.collisionPos, b.collided, b.ageCollided = cube.Pos{}, false, 0 + if result == nil { + return m + } + + switch r := result.(type) { + case trace.EntityResult: + if l, ok := r.Entity().(Living); ok { + if !b.dealtDamage { + b.hitEntity(l, e, tx, vel) + } + b.collidedEntities = append(b.collidedEntities, l.H()) + } + // The trident deflects off the entity hit and drops to the ground. + e.data.Vel = mgl64.Vec3{vel[0] * -0.01, vel[1] * -0.1, vel[2] * -0.01} + if b.loyaltyLevel() > 0 { + b.startReturning(e, tx) + } + case trace.BlockResult: + bpos := r.BlockPosition() + if h, ok := tx.Block(bpos).(block.ProjectileHitter); ok { + h.ProjectileHit(bpos, tx, e, r.Face()) + } + tx.PlaySound(result.Position(), sound.TridentHitGround{}) + b.hitBlockSurviving(e, r, m, tx) + } + return m +} + +// hitEntity is called when the trident hits a Living entity. It deals damage +// to the entity, knocks it back and summons a lightning bolt if the trident +// is enchanted with channeling during a thunderstorm. +func (b *TridentBehaviour) hitEntity(l Living, e *Ent, tx *world.Tx, vel mgl64.Vec3) { + b.dealtDamage = true + owner, _ := b.conf.Owner.Entity(e.tx) + + dmg := b.conf.Damage + if ench, ok := b.conf.Item.Enchantment(enchantment.Impaling); ok && Wet(l, tx) { + dmg += enchantment.Impaling.Addend(ench.Level()) + } + if _, vulnerable := l.Hurt(dmg, ProjectileDamageSource{Projectile: e, Owner: owner}); vulnerable { + l.KnockBack(l.Position().Sub(vel), 0.45, 0.3608) + } + tx.PlaySound(e.Position(), sound.TridentHit{}) + + if _, ok := b.conf.Item.Enchantment(enchantment.Channeling); ok && tx.ThunderingAt(cube.PosFromVec3(l.Position())) { + tx.AddEntity(NewLightning(world.EntitySpawnOpts{Position: l.Position()})) + tx.PlaySound(l.Position(), sound.TridentThunder{}) + } +} + +// loyaltyLevel returns the level of the loyalty enchantment on the trident +// stack held by the entity, or 0 if it is not enchanted with loyalty. +func (b *TridentBehaviour) loyaltyLevel() int { + if ench, ok := b.conf.Item.Enchantment(enchantment.Loyalty); ok { + return ench.Level() + } + return 0 +} + +// startReturning makes the trident start returning to its owner. If the owner +// is not found in the world, the trident is dropped as an item instead. +func (b *TridentBehaviour) startReturning(e *Ent, tx *world.Tx) { + if _, ok := b.conf.Owner.Entity(tx); !ok { + b.drop(e, tx) + return + } + b.returning = true + b.collisionPos, b.collided, b.ageCollided = cube.Pos{}, false, 0 + + tx.PlaySound(e.Position(), sound.TridentReturn{}) + for _, v := range tx.Viewers(e.Position()) { + v.ViewEntityState(e) + } +} + +// tickReturning ticks the trident as it returns to its owner, accelerating +// towards the owner's eye position. The trident is dropped as an item if the +// owner is no longer available. +func (b *TridentBehaviour) tickReturning(e *Ent, tx *world.Tx) *Movement { + owner, ok := b.conf.Owner.Entity(tx) + living, alive := owner.(Living) + if !ok || (alive && living.Dead()) || b.returnAge > 1200 { + b.drop(e, tx) + return nil + } + b.returnAge++ + + pos, level := e.Position(), float64(b.loyaltyLevel()) + diff := EyePosition(owner).Sub(pos) + if diff.Len() < 1 { + b.pickUpReturned(e, tx, owner) + return nil + } + pos[1] += diff[1] * 0.015 * level + + vel := e.Velocity().Mul(0.95).Add(diff.Normalize().Mul(0.05 * level)) + end := pos.Add(vel) + rot := cube.Rotation{ + mgl64.RadToDeg(math.Atan2(vel[0], vel[2])), + mgl64.RadToDeg(math.Atan2(vel[1], math.Hypot(vel[0], vel[2]))), + } + m := &Movement{v: tx.Viewers(e.data.Pos), e: e, pos: end, vel: vel, dpos: end.Sub(e.data.Pos), dvel: vel.Sub(e.data.Vel), rot: rot} + e.data.Pos, e.data.Vel, e.data.Rot = end, vel, rot + return m +} + +// pickUpReturned makes the owner of the trident pick it up after it returned. +// If the owner has no space in its inventory, the trident is dropped as an +// item instead. +func (b *TridentBehaviour) pickUpReturned(e *Ent, tx *world.Tx, owner world.Entity) { + if b.conf.DisablePickup || b.conf.Item.Empty() { + _ = e.Close() + return + } + collector, ok := owner.(Collector) + if !ok { + b.drop(e, tx) + return + } + if n, _ := collector.Collect(b.conf.Item); n == 0 { + b.drop(e, tx) + return + } + for _, viewer := range tx.Viewers(e.Position()) { + viewer.ViewEntityAction(e, PickedUpAction{Collector: collector}) + } + _ = e.Close() +} + +// drop drops the trident stack held by the entity as an item and closes the +// entity. +func (b *TridentBehaviour) drop(e *Ent, tx *world.Tx) { + if !b.conf.DisablePickup && !b.conf.Item.Empty() { + create := tx.World().EntityRegistry().Config().Item + tx.AddEntity(create(world.EntitySpawnOpts{Position: e.Position()}, b.conf.Item)) + } + _ = e.Close() +} + +// Wet checks if the world.Entity passed is currently standing in water or +// exposed to rain. +func Wet(e world.Entity, tx *world.Tx) bool { + pos := cube.PosFromVec3(e.Position()) + if tx.RainingAt(pos) { + return true + } + if l, ok := tx.Liquid(pos); ok { + _, isWater := l.(block.Water) + return isWater + } + return false +} + +// TridentType is a world.EntityType implementation for thrown tridents. +var TridentType tridentType + +type tridentType struct{} + +func (t tridentType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + return &Ent{tx: tx, handle: handle, data: data} +} + +func (tridentType) EncodeEntity() string { return "minecraft:thrown_trident" } +func (tridentType) BBox(world.Entity) cube.BBox { + return cube.Box(-0.125, 0, -0.125, 0.125, 0.35, 0.125) +} + +func (tridentType) DecodeNBT(m map[string]any, data *world.EntityData) { + conf := TridentBehaviourConfig{ + Damage: float64(nbtconv.Float32(m, "Damage")), + Item: nbtconv.MapItem(m, "Trident"), + DisablePickup: !nbtconv.Bool(m, "player"), + CollisionPosition: nbtconv.Pos(m, "StuckToBlockPos"), + } + if conf.Item.Empty() { + conf.Item = item.NewStack(item.Trident{}, 1) + } + data.Data = conf.New() +} + +func (tridentType) EncodeNBT(data *world.EntityData) map[string]any { + b := data.Data.(*TridentBehaviour) + m := map[string]any{ + "Damage": float32(b.conf.Damage), + "player": boolByte(!b.conf.DisablePickup), + } + if !b.conf.Item.Empty() { + m["Trident"] = nbtconv.WriteItem(b.conf.Item, true) + } + if b.collided { + m["StuckToBlockPos"] = nbtconv.PosToInt32Slice(b.collisionPos) + } + return m +} diff --git a/server/item/enchantment/channeling.go b/server/item/enchantment/channeling.go new file mode 100644 index 0000000000..c4a9f7ae12 --- /dev/null +++ b/server/item/enchantment/channeling.go @@ -0,0 +1,43 @@ +package enchantment + +import ( + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// Channeling is a trident enchantment that produces lightning when thrown at a mob or lightning rod while a +// thunderstorm is occurring. +var Channeling channeling + +type channeling struct{} + +// Name ... +func (channeling) Name() string { + return "Channeling" +} + +// MaxLevel ... +func (channeling) MaxLevel() int { + return 1 +} + +// Cost ... +func (channeling) Cost(int) (int, int) { + return 25, 50 +} + +// Rarity ... +func (channeling) Rarity() item.EnchantmentRarity { + return item.EnchantmentRarityVeryRare +} + +// CompatibleWithEnchantment ... +func (channeling) CompatibleWithEnchantment(t item.EnchantmentType) bool { + return t != Riptide +} + +// CompatibleWithItem ... +func (channeling) CompatibleWithItem(i world.Item) bool { + _, ok := i.(item.Trident) + return ok +} diff --git a/server/item/enchantment/impaling.go b/server/item/enchantment/impaling.go new file mode 100644 index 0000000000..5c4db124cd --- /dev/null +++ b/server/item/enchantment/impaling.go @@ -0,0 +1,50 @@ +package enchantment + +import ( + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// Impaling is a trident enchantment that increases damage dealt to entities +// that are in contact with water or rain. +var Impaling impaling + +type impaling struct{} + +// Name ... +func (impaling) Name() string { + return "Impaling" +} + +// MaxLevel ... +func (impaling) MaxLevel() int { + return 5 +} + +// Cost ... +func (impaling) Cost(level int) (int, int) { + minCost := 1 + (level-1)*8 + return minCost, minCost + 20 +} + +// Rarity ... +func (impaling) Rarity() item.EnchantmentRarity { + return item.EnchantmentRarityRare +} + +// Addend is the extra amount of damage the Impaling enchantment adds when +// attacking mobs that are touching water +func (impaling) Addend(level int) float64 { + return float64(level) * 2.5 +} + +// CompatibleWithEnchantment ... +func (impaling) CompatibleWithEnchantment(item.EnchantmentType) bool { + return true +} + +// CompatibleWithItem ... +func (impaling) CompatibleWithItem(i world.Item) bool { + _, ok := i.(item.Trident) + return ok +} diff --git a/server/item/enchantment/loyalty.go b/server/item/enchantment/loyalty.go new file mode 100644 index 0000000000..78835cc147 --- /dev/null +++ b/server/item/enchantment/loyalty.go @@ -0,0 +1,43 @@ +package enchantment + +import ( + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// Loyalty is a trident enchantment that causes a thrown trident to return to its owner +// after hitting a block or an entity. +var Loyalty loyalty + +type loyalty struct{} + +// Name ... +func (loyalty) Name() string { + return "Loyalty" +} + +// MaxLevel ... +func (loyalty) MaxLevel() int { + return 3 +} + +// Cost ... +func (loyalty) Cost(level int) (int, int) { + return 5 + level*7, 50 +} + +// Rarity ... +func (loyalty) Rarity() item.EnchantmentRarity { + return item.EnchantmentRarityUncommon +} + +// CompatibleWithEnchantment ... +func (loyalty) CompatibleWithEnchantment(t item.EnchantmentType) bool { + return t != Riptide +} + +// CompatibleWithItem ... +func (loyalty) CompatibleWithItem(i world.Item) bool { + _, ok := i.(item.Trident) + return ok +} diff --git a/server/item/enchantment/register.go b/server/item/enchantment/register.go index 8e0b090baa..750973ac56 100644 --- a/server/item/enchantment/register.go +++ b/server/item/enchantment/register.go @@ -32,10 +32,10 @@ func init() { item.RegisterEnchantment(26, Mending) // TODO: (27) Curse of Binding. item.RegisterEnchantment(28, CurseOfVanishing) - // TODO: (29) Impaling. - // TODO: (30) Riptide. - // TODO: (31) Loyalty. - // TODO: (32) Channeling. + item.RegisterEnchantment(29, Impaling) + item.RegisterEnchantment(30, Riptide) + item.RegisterEnchantment(31, Loyalty) + item.RegisterEnchantment(32, Channeling) item.RegisterEnchantment(33, Multishot) item.RegisterEnchantment(34, Piercing) item.RegisterEnchantment(35, QuickCharge) diff --git a/server/item/enchantment/riptide.go b/server/item/enchantment/riptide.go new file mode 100644 index 0000000000..d1fce60779 --- /dev/null +++ b/server/item/enchantment/riptide.go @@ -0,0 +1,49 @@ +package enchantment + +import ( + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" +) + +// Riptide is a trident enchantment that launches its user when the trident is +// thrown while the user is in water or rain, instead of throwing the trident. +var Riptide riptide + +type riptide struct{} + +// Name ... +func (riptide) Name() string { + return "Riptide" +} + +// MaxLevel ... +func (riptide) MaxLevel() int { + return 3 +} + +// Cost ... +func (riptide) Cost(level int) (int, int) { + return 10 + level*7, 50 +} + +// Rarity ... +func (riptide) Rarity() item.EnchantmentRarity { + return item.EnchantmentRarityRare +} + +// RiptideForce returns the force with which the user is launched when +// releasing a riptide trident. +func (riptide) RiptideForce(level int) float64 { + return 3 * float64(1+level) / 4 +} + +// CompatibleWithEnchantment ... +func (riptide) CompatibleWithEnchantment(t item.EnchantmentType) bool { + return t != Loyalty && t != Channeling +} + +// CompatibleWithItem ... +func (riptide) CompatibleWithItem(i world.Item) bool { + _, ok := i.(item.Trident) + return ok +} diff --git a/server/item/register.go b/server/item/register.go index 07b7b075d4..c2e65dd9bb 100644 --- a/server/item/register.go +++ b/server/item/register.go @@ -123,6 +123,7 @@ func init() { world.RegisterItem(Stick{}) world.RegisterItem(Sugar{}) world.RegisterItem(Totem{}) + world.RegisterItem(Trident{}) world.RegisterItem(TropicalFish{}) world.RegisterItem(TurtleShell{}) world.RegisterItem(WarpedFungusOnAStick{}) diff --git a/server/item/trident.go b/server/item/trident.go new file mode 100644 index 0000000000..7bdd0faab7 --- /dev/null +++ b/server/item/trident.go @@ -0,0 +1,139 @@ +package item + +import ( + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" +) + +// Trident is a weapon that can be used to perform melee attacks, or be thrown as a projectile. +type Trident struct{} + +// MaxCount ... +func (Trident) MaxCount() int { + return 1 +} + +// AttackDamage ... +func (Trident) AttackDamage() float64 { + return 8.0 +} + +// HandEquipped ... +func (Trident) HandEquipped() bool { + return true +} + +// EnchantmentValue ... +func (Trident) EnchantmentValue() int { + return 1 +} + +// DurabilityInfo ... +func (Trident) DurabilityInfo() DurabilityInfo { + return DurabilityInfo{ + MaxDurability: 251, + BrokenItem: simpleItem(Stack{}), + AttackDurability: 1, + BreakDurability: 2, + } +} + +// RepairableBy ... +func (Trident) RepairableBy(i Stack) bool { + _, ok := i.Item().(Trident) + return ok +} + +// Release either throws the trident as a projectile or, if the trident is +// enchanted with riptide, launches the releaser in the direction it is facing. +func (Trident) Release(releaser Releaser, tx *world.Tx, ctx *UseContext, duration time.Duration) { + if duration.Milliseconds()/50 < 10 { + // The trident must be charged for at least ten ticks. + return + } + held, left := releaser.HeldItems() + if held.Durability() <= 14 { + // Tridents with low durability cannot be released. + return + } + + riptide := 0 + for _, enchant := range held.Enchantments() { + if _, ok := enchant.Type().(interface{ RiptideForce(int) float64 }); ok { + riptide = enchant.Level() + } + } + if riptide > 0 { + if !touchingWaterOrRain(releaser, tx) { + return + } + // The riptide launch motion is handled client-side. + if s, ok := releaser.(interface{ StartSpinning() }); ok { + s.StartSpinning() + } + ctx.DamageItem(1) + + tx.PlaySound(releaser.Position(), sound.TridentRiptide{Level: riptide}) + return + } + + creative := releaser.GameMode().CreativeInventory() + thrown := held.Grow(-held.Count() + 1) + if !creative { + dmg := 1 + for _, enchant := range held.Enchantments() { + if u, ok := enchant.Type().(interface { + Reduce(it world.Item, level, amount int) int + }); ok { + dmg = u.Reduce(held.Item(), enchant.Level(), dmg) + } + } + thrown = thrown.Damage(dmg) + releaser.SetHeldItems(Stack{}, left) + } + + if thrown.Empty() { + tx.PlaySound(releaser.Position(), sound.ItemBreak{}) + return + } + create := tx.World().EntityRegistry().Config().Trident + opts := world.EntitySpawnOpts{ + Position: eyePosition(releaser), + Velocity: releaser.Rotation().Vec3().Mul(2.5), + Rotation: releaser.Rotation().Neg(), + } + tx.AddEntity(create(opts, world.TridentSpawnConfig{ + Damage: 8, + Owner: releaser, + Item: thrown, + DisablePickup: creative, + })) + tx.PlaySound(releaser.Position(), sound.TridentThrow{}) +} + +// touchingWaterOrRain checks if the world.Entity passed is standing in +// water or exposed to rain. +func touchingWaterOrRain(e world.Entity, tx *world.Tx) bool { + pos := cube.PosFromVec3(e.Position()) + if tx.RainingAt(pos) { + return true + } + if l, ok := tx.Liquid(pos); ok && l.LiquidType() == "water" { + return true + } + l, ok := tx.Liquid(cube.PosFromVec3(eyePosition(e))) + return ok && l.LiquidType() == "water" +} + +// Requirements returns the required items to release this item. +func (Trident) Requirements() []Stack { + return []Stack{} +} + +// EncodeItem ... +func (Trident) EncodeItem() (name string, meta int16) { + return "minecraft:trident", 0 +} diff --git a/server/player/player.go b/server/player/player.go index 2049a0834f..4955be547a 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -57,7 +57,7 @@ type playerData struct { armour *inventory.Armour heldSlot *uint32 - sneaking, sprinting, swimming, gliding, crawling, flying, + sneaking, sprinting, swimming, gliding, spinning, crawling, flying, invisible, immobile, onGround, usingItem bool sleeping bool @@ -67,6 +67,7 @@ type playerData struct { glideTicks int64 fireTicks int64 + riptideTicks int64 fallDistance float64 breathing bool @@ -674,6 +675,12 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) { p.Wake() + if s, ok := src.(entity.AttackDamageSource); ok { + if attacker, ok := s.Attacker.(*Player); ok && attacker.Spinning() { + attacker.StopSpinning() + } + } + if p.Dead() { p.kill(src) } @@ -1222,6 +1229,43 @@ func (p *Player) StopGliding() { p.updateState() } +// StartSpinning makes the player start spinning if it is not currently doing so. +func (p *Player) StartSpinning() { + if p.spinning { + p.riptideTicks = 20 + return + } + trident, _ := p.HeldItems() + // Vanilla does not allow using a trident that is about to break. + if _, ok := trident.Item().(item.Trident); !ok || trident.Durability() <= 14 { + return + } + if _, ok := trident.Enchantment(enchantment.Riptide); !ok { + return + } + + // TODO; According to java edition this should be 20 ticks, but nukkit puts it as (50 + (riptideLevel << 5)) + p.riptideTicks = 20 + + p.spinning = true + p.updateState() +} + +// Spinning checks if the player is currently spinning. +func (p *Player) Spinning() bool { + return p.spinning +} + +// StopSpinning makes the player stop spinning if it is currently doing so. +func (p *Player) StopSpinning() { + if !p.spinning { + return + } + p.spinning = false + p.riptideTicks = 0 + p.updateState() +} + // StartFlying makes the player start flying if they aren't already. It requires the player to be in a gamemode which // allows flying. func (p *Player) StartFlying() { @@ -1883,6 +1927,12 @@ func (p *Player) AttackEntity(e world.Entity) bool { v.ViewEntityAction(living, entity.EnchantedHitAction{}) } } + if s, ok := i.Enchantment(enchantment.Impaling); ok && entity.Wet(living, p.tx) { + dmg += enchantment.Impaling.Addend(s.Level()) + for _, v := range p.tx.Viewers(living.Position()) { + v.ViewEntityAction(living, entity.EnchantedHitAction{}) + } + } if critical { dmg *= 1.5 } @@ -2645,6 +2695,17 @@ func (p *Player) Tick(tx *world.Tx, current int64) { p.Hurt(1, entity.SuffocationDamageSource{}) } + if p.Spinning() { + if p.collidedHorizontally { + p.StopSpinning() + } + if p.riptideTicks > 0 { + p.riptideTicks -= 1 + } else { + p.StopSpinning() + } + } + if p.OnFireDuration() > 0 { p.fireTicks -= 1 if !p.GameMode().AllowsTakingDamage() || p.OnFireDuration() <= 0 || p.tx.RainingAt(cube.PosFromVec3(p.Position())) { diff --git a/server/session/controllable.go b/server/session/controllable.go index d5386931d2..53635a4f74 100644 --- a/server/session/controllable.go +++ b/server/session/controllable.go @@ -98,6 +98,9 @@ type Controllable interface { StartGliding() Gliding() bool StopGliding() + StartSpinning() + Spinning() bool + StopSpinning() Jump() StartBreaking(pos cube.Pos, face cube.Face) diff --git a/server/session/entity_metadata.go b/server/session/entity_metadata.go index 71784c0a9f..acc5491179 100644 --- a/server/session/entity_metadata.go +++ b/server/session/entity_metadata.go @@ -59,6 +59,9 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) { if gl, ok := e.(glider); ok && gl.Gliding() { m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagGliding) } + if sp, ok := e.(spinner); ok && sp.Spinning() { + m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagDamageNearbyMobs) + } if bb, ok := e.(baby); ok && bb.Baby() { m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagBaby) } @@ -84,6 +87,12 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) { if c, ok := e.(arrow); ok && c.Critical() { m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagCritical) } + if r, ok := e.(returning); ok && r.Returning() { + m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagReturnTrident) + } + if g, ok := e.(glint); ok && g.Glint() { + m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagEnchanted) + } if g, ok := e.(gameMode); ok { if g.GameMode().HasCollision() { m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagHasCollision) @@ -242,6 +251,10 @@ type glider interface { Gliding() bool } +type spinner interface { + Spinning() bool +} + type baby interface { Baby() bool } @@ -314,6 +327,10 @@ type arrow interface { Critical() bool } +type returning interface { + Returning() bool +} + type orb interface { Experience() int } diff --git a/server/session/world.go b/server/session/world.go index 9e47cbf4bb..80ca51d29a 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -789,6 +789,25 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) pk.SoundType = packet.SoundEventCrossbowShoot case sound.ArrowHit: pk.SoundType = packet.SoundEventBowHit + case sound.TridentThrow: + pk.SoundType = packet.SoundEventTridentThrow + case sound.TridentHit: + pk.SoundType = packet.SoundEventTridentHit + case sound.TridentHitGround: + pk.SoundType = packet.SoundEventTridentHitGround + case sound.TridentReturn: + pk.SoundType = packet.SoundEventTridentReturn + case sound.TridentThunder: + pk.SoundType = packet.SoundEventTridentThunder + case sound.TridentRiptide: + switch so.Level { + case 1: + pk.SoundType = packet.SoundEventTridentRiptide1 + case 2: + pk.SoundType = packet.SoundEventTridentRiptide2 + default: + pk.SoundType = packet.SoundEventTridentRiptide3 + } case sound.ItemThrow: pk.SoundType, pk.EntityType = packet.SoundEventThrow, "minecraft:player" case sound.LevelUp: diff --git a/server/world/entity.go b/server/world/entity.go index fd890b27dd..0dcccb4896 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -503,6 +503,7 @@ type EntityRegistryConfig struct { Snowball func(opts EntitySpawnOpts, owner Entity) *EntityHandle SplashPotion func(opts EntitySpawnOpts, t any, owner Entity) *EntityHandle Lightning func(opts EntitySpawnOpts) *EntityHandle + Trident func(opts EntitySpawnOpts, conf TridentSpawnConfig) *EntityHandle } // ArrowSpawnConfig holds the options used to spawn an arrow entity. @@ -527,6 +528,18 @@ type ArrowSpawnConfig struct { Tip any } +// TridentSpawnConfig holds the options used to spawn a trident entity. +type TridentSpawnConfig struct { + // Damage specifies the damage dealt by the trident. + Damage float64 + // Owner is the entity that threw the trident. + Owner Entity + // Item is the item.Stack the trident was thrown with. + Item any + // DisablePickup specifies if entities are able to pickup the trident. + DisablePickup bool +} + // New creates an EntityRegistry using conf and the EntityTypes passed. func (conf EntityRegistryConfig) New(ent []EntityType) EntityRegistry { m := make(map[string]EntityType, len(ent)) diff --git a/server/world/sound/item.go b/server/world/sound/item.go index 69ddda6a2e..f5cb8f4e1f 100644 --- a/server/world/sound/item.go +++ b/server/world/sound/item.go @@ -73,6 +73,32 @@ const ( // ArrowHit is a sound played when an arrow hits ground. type ArrowHit struct{ sound } +// TridentThrow is a sound played when a trident is thrown. +type TridentThrow struct{ sound } + +// TridentHit is a sound played when a thrown trident hits an entity. +type TridentHit struct{ sound } + +// TridentHitGround is a sound played when a thrown trident hits the ground. +type TridentHitGround struct{ sound } + +// TridentReturn is a sound played when a thrown trident enchanted with loyalty +// starts returning to its owner. +type TridentReturn struct{ sound } + +// TridentThunder is a sound played when a trident enchanted with channeling +// summons a lightning bolt at an entity during a thunderstorm. +type TridentThunder struct{ sound } + +// TridentRiptide is a sound played when a player launches itself using a +// trident enchanted with riptide. +type TridentRiptide struct { + // Level is the level of the riptide enchantment. + Level int + + sound +} + // Teleport is a sound played upon teleportation of an enderman, or teleportation of a player by an ender pearl or a chorus fruit. type Teleport struct{ sound }