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) } }) }