diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/hardening_retribution_mob_death_test.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-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.go | 90 |
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) + } + }) +} |
