From 73de8d4a93ef30e14322bb7bcfec4a71455efa5c Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 17:53:47 +0200 Subject: [PATCH] test: cover tests.reanimationActAttackingAnAdjacentMonsterEmitsNoiseT... --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 1080e17..1908f40 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -3589,6 +3589,179 @@ function tests.reanimationActBlindedThrallOnlyDetectsAnAdjacentHostileNotOneWith game.level.entities[#game.level.entities + 1] = rat local ratHpBefore = rat.hp Reanimation.act(game, thrall) + ok(rat.hp < ratHpBefore, "a blinded thrall still fights a hostile once it stands adjacent") +end + +-- Same gap as companionActAttackingAnAdjacentMonsterEmitsNoise..., mirrored +-- onto the thrall side. Fresh game per seed rather than a shared one both to +-- match that sibling test's shape and because Reanimation.act decrements +-- thrall.decay on every call, so a single thrall could not absorb 200 calls +-- regardless. +function tests.reanimationActAttackingAnAdjacentMonsterEmitsNoiseThatCanStirADormantBystander() + local stirred = false + for seed = 1, 200 do + local game = stagedGame(seed, arena(21, 11), 3, 6) + local corpseSource = Monsters.create(Monsters.byKey.rat, 40, game.player.x + 1, game.player.y, 1) + Corpses.spawn(game.level, corpseSource) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + local target = Monsters.create(Monsters.byKey.rat, 50, thrall.x + 1, thrall.y, 1) + local bystander = Monsters.create(Monsters.byKey.orc, 51, thrall.x + 4, thrall.y, 1) + bystander.awareness = Stealth.DORMANT + game.level.entities[#game.level.entities + 1] = target + game.level.entities[#game.level.entities + 1] = bystander + Reanimation.act(game, thrall) + if bystander.awareness ~= Stealth.DORMANT then stirred = true; break end + end + ok(stirred, "a thrall's own melee is exactly as loud as any other ally's or the player's") +end + +function tests.reanimationActFollowsThePlayerWhenNoMonsterIsVisible() + local game = stagedGame(1, arena(21, 11), 10, 5) + local rat0 = Monsters.create(Monsters.byKey.rat, 1, 10, 5, 1) + Corpses.spawn(game.level, rat0) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + thrall.x, thrall.y = 3, 5 + local startDist = math.abs(thrall.x - game.player.x) + Reanimation.act(game, thrall) + ok(math.abs(thrall.x - game.player.x) < startDist, "a thrall with no monster in sight closes in on the player") +end + +-- Same wiring proof as the companion/hireling versions above, but for +-- Reanimation.act - placed after its own decay tick/death check (upkeep +-- the confusion roll must never skip), just before the behaviour it can +-- override. +function tests.reanimationActConfusedThrallSometimesStumblesInsteadOfFightingAnAdjacentHostile() + local sawAttack, sawStumble = false, false + for seed = 1, 200 do + local game = stagedGame(seed, arena(21, 11), 3, 6) + local rat0 = Monsters.create(Monsters.byKey.rat, 1, 3, 6, 1) + Corpses.spawn(game.level, rat0) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + local rat = Monsters.create(Monsters.byKey.rat, 50, thrall.x + 1, thrall.y, 1) + game.level.entities[#game.level.entities + 1] = rat + Status.apply(thrall, Status.CONFUSE, 5, 1) + local hpBefore = rat.hp + Reanimation.act(game, thrall) + if rat.hp < hpBefore then + sawAttack = true + else + sawStumble = true + end + if sawAttack and sawStumble then break end + end + ok(sawAttack, "a confused thrall's roll sometimes lets it fight an adjacent hostile as normal") + ok(sawStumble, "a confused thrall's roll sometimes overrides even a point-blank attack") +end + +function tests.reanimationActCollapsesOnceItsBorrowedAnimationRunsOut() + local game = stagedGame(1, arena(21, 11), 5, 5) + local rat0 = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + Corpses.spawn(game.level, rat0) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + thrall.decay = 1 + game:takeMonsterTurn(thrall) + eq(#game.level.entities, 0, "a thrall whose animation runs out is swept from the level") + ok(contains(game.log:last(), "crumbles"), "a collapsing thrall's fate is logged") +end + +function tests.takeMonsterTurnRoutesAThrallThroughReanimationAct() + local game = stagedGame(1, arena(21, 11), 5, 5) + local chieftain = Monsters.create(Monsters.byKey.chieftain, 1, 5, 5, 6) + Corpses.spawn(game.level, chieftain) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + local rat = Monsters.create(Monsters.byKey.rat, 50, thrall.x + 1, thrall.y, 1) + game.level.entities[#game.level.entities + 1] = rat + game:takeMonsterTurn(thrall) + eq(rat.hp, 0, "the full monster-turn pipeline dispatches a thrall's turn to reanimation.act") +end + +function tests.gameRaiseCorpseSucceedsAndLogsTheCallAnswered() + local game = stagedGame(1, arena(21, 11), 5, 5) + local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + Corpses.spawn(game.level, rat) + ok(game:raiseCorpse(), "raising the corpse underfoot succeeds") + eq(#game.level.corpses, 0, "the corpse is gone after raising") + local hasThrall = false + for _, e in ipairs(game.level.entities) do + if e.kind == "thrall" then hasThrall = true end + end + ok(hasThrall, "a thrall joins the level once raised through the game action") + ok(contains(game.log:last(), "answers the call"), "a successful raise is logged") +end + +function tests.gameRaiseCorpseFailsWithoutACorpseUnderfoot() + local game = stagedGame(1, arena(21, 11), 5, 5) + ok(not game:raiseCorpse(), "raising empty ground fails") + ok(contains(game.log:last(), "nothing here"), "the failure is logged") +end + +function tests.scavengingHerbForMapsEachWildFactionToItsOwnFixedHerb() + eq(Scavenging.herbFor({ species = "rat" }), Alchemy.NIGHTCAP, "vermin always yields nightcap") + eq(Scavenging.herbFor({ species = "goblin" }), Alchemy.BLOODROOT, "goblinoids always yield bloodroot") + eq(Scavenging.herbFor({ species = "troll" }), Alchemy.EMBERLEAF, "beasts always yield emberleaf") + eq(Scavenging.herbFor({ species = "dragon" }), Alchemy.EMBERLEAF, "drakes yield the same herb as any other beast-aspected foe") +end + +function tests.scavengingHerbForReturnsNilForACorpseWithNoBestiaryEntry() + eq(Scavenging.herbFor({ species = "hound" }), nil, + "a bonded companion's own remains carry no bestiary-mapped herb") +end + +function tests.gameScavengeCorpseCarvesTheMatchingHerbAndClearsTheCorpse() + local game = stagedGame(1, arena(21, 11), 5, 5) + local goblin = Monsters.create(Monsters.byKey.goblin, 1, 5, 5, 2) + Corpses.spawn(game.level, goblin) + local before = #game.player.inventory + ok(game:scavengeCorpse(), "scavenging the corpse underfoot succeeds") + eq(#game.level.corpses, 0, "the corpse is gone after scavenging") + eq(#game.player.inventory, before + 1, "the player's inventory gains the carved herb") + local herb = game.player.inventory[#game.player.inventory] + eq(herb.herb, Alchemy.BLOODROOT, "the carved herb matches the goblin's own goblinoid aspect") + ok(contains(game.log:last(), "carve"), "a successful scavenge is logged") +end + +function tests.gameScavengeCorpseFailsWithoutACorpseUnderfoot() + local game = stagedGame(1, arena(21, 11), 5, 5) + ok(not game:scavengeCorpse(), "scavenging empty ground fails") + ok(contains(game.log:last(), "nothing here"), "the failure is logged") +end + +function tests.gameScavengeCorpseFailsOnARottenCorpse() + local game = stagedGame(1, arena(21, 11), 5, 5) + local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + Corpses.spawn(game.level, rat) + game.level.corpses[1].decay = Corpses.ROT_TURNS + local before = #game.player.inventory + ok(not game:scavengeCorpse(), "a rotten corpse yields no herb") + eq(#game.level.corpses, 1, "a failed scavenge leaves the corpse in place rather than spending it") + eq(#game.player.inventory, before, "no herb is added on a failed scavenge") +end + +function tests.gameScavengeCorpseFailsForASpeciesWithNoKnownHerb() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.corpses = { { x = 5, y = 5, species = "hound", name = "hound", nutrition = 40, decay = Corpses.DECAY_TURNS } } + ok(not game:scavengeCorpse(), "a companion's own remains carry nothing worth carving") + eq(#game.level.corpses, 1, "a failed scavenge leaves the corpse in place") +end + +function tests.reanimationFieldsSurviveSerialization() + local game = stagedGame(1, arena(21, 11), 5, 5) + local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + Corpses.spawn(game.level, rat) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + thrall.decay = 17 + local restoredLevel = Level.fromData(game.level:toData()) + eq(#restoredLevel.entities, 1, "the thrall survives a level round trip") + eq(restoredLevel.entities[1].kind, "thrall", "the thrall's kind survives a round trip") + eq(restoredLevel.entities[1].decay, 17, "the thrall's remaining animation survives a round trip") +end + +function tests.herbItemsSurviveSerialization() + local game = Game.new(5) + game.player.inventory[#game.player.inventory + 1] = Alchemy.herb(Alchemy.NIGHTCAP) + + local path = os.tmpname() + game:save(path) end local order = {