aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hardening_retribution_mob_death_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/hardening_retribution_mob_death_test.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/game/hardening_retribution_mob_death_test.go')
-rw-r--r--internal/game/hardening_retribution_mob_death_test.go90
1 files changed, 90 insertions, 0 deletions
diff --git a/internal/game/hardening_retribution_mob_death_test.go b/internal/game/hardening_retribution_mob_death_test.go
new file mode 100644
index 0000000..cf05820
--- /dev/null
+++ b/internal/game/hardening_retribution_mob_death_test.go
@@ -0,0 +1,90 @@
+package game
+
+// Regression: Dead Man's Switch retribution can zero a mob inside killPlayer,
+// after endCombat has already taken the player-death branch and returned.
+// That left the mob at HP 0 in MobStore: filtered out of MobsInRoom (HP > 0)
+// and skipped by SeedMobs, it vanished from the room until restart, with no
+// drops and no respawn. Retribution kills must route through the same
+// mob-death helper the normal attack path uses.
+
+import (
+ "testing"
+
+ "thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+const retributionMobYAML = `id: retribution_target
+name: test target
+combat:
+ aggressive: false
+ attack_types:
+ - crush
+ kind: combat
+ respawn_ticks: 1
+ size: medium
+ stats:
+ attack: 1
+ defense: 1
+ hp: 1
+ max_melee_hit: 1
+ speed: 5
+ strength: 1
+drops:
+ remains: bones
+`
+
+func TestRetributionKillRunsMobDeathPath(t *testing.T) {
+ dir := t.TempDir()
+ seedWorld(t, dir)
+ writeDataFile(t, dir, "items", "bones.yaml", "id: bones\nname: bones\nstackable: false\nvalue: 1\n")
+ writeDataFile(t, dir, "techs", "retribution.yaml",
+ "name: Dead Man's Switch\nlevel: 46\neffects:\n retribution_pct: 0.25\n")
+ writeDataFile(t, dir, "mobs", "retribution_target.yaml", retributionMobYAML)
+
+ g := newHardeningGame(t, dir)
+
+ g.RunOnOwner(func() {
+ g.MobStore.ReloadDefs(dir)
+ def, err := g.MobStore.LoadDef("retribution_target")
+ if err != nil {
+ t.Fatalf("load mob def: %v", err)
+ }
+ mob := g.MobStore.SpawnTransient(def, &behavior.SpawnMobConfig{ID: "retribution_target"}, 2, "")
+ // A normal seeded mob, not a trigger despawn: respawn must restore it.
+ mob.SpawnedByTrigger = false
+ mob.RespawnTicks = 1
+ mob.Drops.Remains = "bones"
+ mob.HP = 1
+ mob.MaxHP = 1
+
+ p := player.New("alice")
+ p.RoomID = 2
+ p.HP = 0
+ p.ActivateTech("retribution")
+
+ conn := newChanConn()
+ sess := &net.Session{Conn: conn, Player: p}
+ sess.SetState(net.StateGame)
+
+ g.endCombat(sess, p, mob)
+
+ if mob.HP != 0 {
+ t.Fatalf("mob HP = %d, want 0 after retribution", mob.HP)
+ }
+ if ground := g.World.GroundItems(2); ground["bones"] != 1 {
+ t.Fatalf("retribution kill did not run the drops path: ground=%v", ground)
+ }
+
+ // The death path schedules a respawn; one tick must restore the mob
+ // in its home room rather than leave it gone at HP 0.
+ g.Ticks.Tick()
+ if mob.HP != mob.MaxHP || mob.HP <= 0 {
+ t.Fatalf("mob did not respawn: HP=%d MaxHP=%d", mob.HP, mob.MaxHP)
+ }
+ if mob.RoomID != 2 {
+ t.Fatalf("mob respawned in room %d, want 2", mob.RoomID)
+ }
+ })
+}