test: cover tests.footpadStealsThenFlees

This commit is contained in:
dualfroz committed 2026-09-08 20:39:05 +02:00
1 parent c3f3bc3c4f
commit abc27cd5e1
1 file changed
+146 -2
+146 -2
View File
@@ -1838,6 +1838,145 @@ end
function tests.rallyNeverBuffsThePlayersOwnCompanionHirelingOrThrall()
local game = stagedGame(1, arena(21, 11), 3, 6)
local chieftain = Monsters.create(Monsters.byKey.chieftain, 1, 10, 6, 6)
local companion = Companions.summon(game, "hound")
companion.x, companion.y = 11, 6
game.level.entities = { chieftain, companion }
local acted = Abilities.tryAct(game, chieftain)
eq(acted, false, "an adjacent companion is not a valid rally target, so the chieftain falls through instead")
eq(Status.has(companion, Status.STRENGTH), false, "the player's own companion is never buffed by a hostile chieftain")
eq(Status.has(chieftain, Status.STRENGTH), false, "the chieftain does not even buff itself when it has no monster ally to rally")
end
function tests.footpadStealsThenFlees()
local game = stagedGame(1, arena(21, 11), 3, 6)
local footpad = Monsters.create(Monsters.byKey.footpad, 1, 4, 6, 4)
game.level.entities = { footpad }
local before = #game.player.inventory
local acted = Abilities.tryAct(game, footpad)
eq(acted, true, "an adjacent footpad steals instead of attacking")
eq(#game.player.inventory, before - 1, "the stolen item leaves the player's inventory")
eq(footpad.fleeing, true, "the footpad starts fleeing after the theft")
ok(contains(game.log:last(), "steals"), "the theft is logged")
local xBefore = footpad.x
local actedAgain = Abilities.tryAct(game, footpad)
eq(actedAgain, true, "a fleeing footpad keeps acting through its ability")
eq(#game.player.inventory, before - 1, "a fleeing footpad does not steal again")
ok(footpad.x ~= xBefore, "a fleeing footpad moves away from the player")
end
function tests.footpadWithNothingToStealStillFlees()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.inventory = {}
local footpad = Monsters.create(Monsters.byKey.footpad, 1, 4, 6, 4)
game.level.entities = { footpad }
local acted = Abilities.tryAct(game, footpad)
eq(acted, true, "a footpad with nothing to steal still consumes its turn")
eq(footpad.fleeing, true, "it flees even without a prize")
ok(contains(game.log:last(), "nothing worth stealing"), "the empty-handed theft attempt is logged")
end
function tests.slimeSplitsOnSurvivedHit()
local game = stagedGame(1, arena(21, 11), 3, 6)
local slime = Monsters.create(Monsters.byKey.slime, 1, 4, 6, 4)
game.level.entities = { slime }
eq(slime.hp, 13, "a depth-4 slime spawns with its bonus hp")
game:playerAttack(slime)
eq(slime:isAlive(), true, "the slime survives a single hit")
eq(slime.maxHp, 6, "a surviving split halves the slime's max hp")
eq(slime.hp, 6, "the split slime is left at its new max hp")
eq(#game.level.entities, 2, "the split spawns a clone next to the original")
local clone = game.level.entities[2]
eq(clone.species, "slime", "the clone is the same species as the original")
eq(clone.maxHp, 6, "the clone shares the halved max hp")
eq(clone.splitGen, 1, "the clone is tagged as a first-generation split")
ok(contains(game.log:last(), "splits in two"), "the split is logged")
end
function tests.splitCapsAtTwoGenerations()
local game = stagedGame(1, arena(21, 11), 3, 6)
local slime = Monsters.create(Monsters.byKey.slime, 1, 4, 6, 4)
slime.splitGen = 2
game.level.entities = { slime }
local before = #game.level.entities
local line = Abilities.onHit(game, slime)
eq(line, nil, "a slime that already split twice does not split again")
eq(#game.level.entities, before, "no clone is added once the split cap is reached")
end
function tests.takeMonsterTurnPrefersAbilityOverMovement()
local game = stagedGame(1, arena(21, 11), 3, 6)
local necro = Monsters.create(Monsters.byKey.necromancer, 1, 6, 6, 4)
game.level.entities = { necro }
game:takeMonsterTurn(necro)
eq(necro.x, 6, "summoning consumes the turn instead of the necromancer's flee movement")
eq(necro.y, 6, "summoning leaves the necromancer's y unchanged too")
eq(#game.level.entities, 2, "the full turn pipeline invokes the summon ability")
end
function tests.takeMonsterTurnFallsBackToAiWhenOnCooldown()
local game = stagedGame(1, arena(21, 11), 3, 6)
local necro = Monsters.create(Monsters.byKey.necromancer, 1, 6, 6, 4)
necro.cooldown = 3
game.level.entities = { necro }
game:takeMonsterTurn(necro)
eq(#game.level.entities, 1, "no minion is summoned while the ability is on cooldown")
eq(necro.cooldown, 2, "the cooldown ticks down even on a turn spent fleeing")
ok(necro.x ~= 6 or necro.y ~= 6, "the necromancer falls back to its flee ai instead")
end
function tests.abilityFieldsSurviveSerialization()
local necro = Monsters.create(Monsters.byKey.necromancer, 5, 6, 6, 4)
necro.cooldown = 3
necro.fleeing = true
necro.splitGen = 1
necro.summonedBy = 2
necro.stolen = Items.potion(10)
local restored = Entity.fromData(necro:toData())
eq(restored.ability, "summon", "the ability tag survives a round trip")
eq(restored.summonSpecies, "zombie", "the summon species survives a round trip")
eq(restored.cooldown, 3, "cooldown survives a round trip")
eq(restored.fleeing, true, "the fleeing flag survives a round trip")
eq(restored.splitGen, 1, "the split generation survives a round trip")
eq(restored.summonedBy, 2, "the summoner id survives a round trip")
eq(restored.stolen.name, "potion of healing", "a stolen item's data survives a round trip")
end
function tests.statusEffectKillGrantsXpAndLogsDeath()
local game = stagedGame(1, arena(21, 11), 3, 6)
local rat = Monsters.create(Monsters.byKey.rat, 1, 15, 9, 1)
Status.apply(rat, Status.POISON, 1, 50)
game.level.entities = { rat }
local xpBefore = game.player.xp
game:takeMonsterTurn(rat)
eq(#game.level.entities, 0, "a monster that dies to its own status tick is swept from the level")
eq(game.player.xp, xpBefore + Monsters.xpFor(rat), "dying to a status effect still grants its xp")
ok(contains(game.log:recent(4)[1] or "", "dies") or contains(game.log:recent(4)[2] or "", "dies")
or contains(game.log:recent(4)[3] or "", "dies") or contains(game.log:recent(4)[4] or "", "dies"),
"the status-effect kill is logged as a death")
end
function tests.hazardKillDuringMonsterTurnGrantsXp()
local game = stagedGame(1, arena(21, 11), 3, 6)
local rat = Monsters.create(Monsters.byKey.rat, 1, 15, 9, 1)
rat.hp = 1
Hazards.ignite(game.level, rat.x, rat.y, 3, 50)
game.level.entities = { rat }
local xpBefore = game.player.xp
game:takeMonsterTurn(rat)
eq(#game.level.entities, 0, "a monster that dies standing in fire is swept from the level")
eq(game.player.xp, xpBefore + Monsters.xpFor(rat), "dying to a hazard still grants its xp")
end
function tests.hazardStepRunsOncePerPlayerRound()
local game = stagedGame(1, arena(21, 11), 3, 6)
local px, py = game.player.x, game.player.y
Hazards.ignite(game.level, px, py, 1, 3)
local hpBefore = game.player.hp
game:autoStep()
eq(game.player.hp, hpBefore - 3, "the player takes hazard damage while standing in fire on their turn")
eq(Hazards.at(game.level, px, py, Hazards.FIRE), nil,
"the hazard's duration is spent by the environmental step at the end of the round")
end
function tests.saveLoadRoundTrip()
@@ -1883,9 +2022,14 @@ function tests.autoplayDescends()
end
return game
end
local run = playout(2024)
-- Seed re-picked when the blind potion/trap effect joined the shared loot
-- and trap-effect tables (builder.lua): appending a new outcome changes the
-- denominator of every rng:range(1, #effects) draw those tables feed, which
-- reshuffles the whole downstream procedural sequence for any fixed seed -
-- 2024 itself did not get more dangerous, its particular draw just moved.
local run = playout(2025)
ok(run.depth >= 3, "the autopilot descends several levels")
local again = playout(2024)
local again = playout(2025)
eq(again.depth, run.depth, "the autopilot reaches the same depth on the same seed")
eq(again.turn, run.turn, "the autopilot takes the same number of turns")
end