From 153acb7f644d755c45533b7c5311e3ffa47e8973 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 05:31:09 -0400 Subject: fix: harden engine across casino, economy, concurrency, security, and content --- internal/game/combat_mob.go | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) (limited to 'internal/game/combat_mob.go') diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go index c53d847..ace02b5 100644 --- a/internal/game/combat_mob.go +++ b/internal/game/combat_mob.go @@ -213,16 +213,16 @@ func (g *Game) applyMobMiss(sess *net.Session, p *player.Player, mob *world.MobI } } -func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { - g.Combat.Leave(p.Name) - - if p.HP <= 0 { - g.killPlayer(sess, p, mob) - return - } - +// resolveMobDeath runs the shared mob-death path for a mob whose HP has +// reached zero: kill bookkeeping, the victory broadcast, drops, on-kill +// triggers, and respawn scheduling. It returns false when the mob is nil or +// still alive. Every path that can reduce a mob to zero HP — a player +// attack, a finishing blow, and Dead Man's Switch retribution — must route +// through here, or the zeroed instance lingers in MobStore, is filtered out +// of MobsInRoom (HP > 0), and never respawns. +func (g *Game) resolveMobDeath(sess *net.Session, p *player.Player, mob *world.MobInstance) bool { if mob == nil || mob.HP > 0 { - return + return false } isTask := mob.IsTask() @@ -236,6 +236,20 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst // the player currently has that item equipped in a weapon-hand slot. g.runTrigger(sess, p, mob.OnKill, p.RoomID, p.IsWielding, false) g.scheduleMobRespawn(mob) + return true +} + +func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { + g.Combat.Leave(p.Name) + + if p.HP <= 0 { + g.killPlayer(sess, p, mob) + return + } + + if !g.resolveMobDeath(sess, p, mob) { + return + } if p.EscapeDir != "" { g.beginEscapeMove(p) @@ -360,6 +374,12 @@ func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobIns } } } + // Retribution can zero the mob: run the same death path the normal + // attack uses so it still drops loot and respawns instead of lingering + // at HP 0 and being permanently removed from the room. + if mob != nil && mob.HP <= 0 { + g.resolveMobDeath(sess, p, mob) + } p.DeactivateAllTechs() p.Stats.RecordDeath() sess.WriteLine(g.colorize(sess, "death", "Oh dear, you are dead!")) -- cgit v1.2.3