From a408c1ebfcf28062e24f131b250dc44d3229d839 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 13:23:03 +0200 Subject: [PATCH] test: cover tests.aMonsterSteppingOntoAPlayerSetTrapSpringsIt --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index ee27200..5f7357e 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -4627,6 +4627,179 @@ function tests.plantingATrapWhereOneAlreadyExistsFails() game.level.traps = { { x = 5, y = 5, effect = "poison", revealed = false } } game.player.inventory = { Items.kit("dart") } ok(not game:useItem(1), "planting a trap on an occupied tile fails") + eq(#game.player.inventory, 1, "a failed planting leaves the kit in the inventory") +end + +function tests.aMonsterSteppingOntoAPlayerSetTrapSpringsIt() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.traps = { { x = 6, y = 6, effect = "dart", revealed = false, placedByPlayer = true } } + local orc = Monsters.create(Monsters.byKey.orc, 1, 7, 6, 1) + game.level.entities = { orc } + local hpBefore = orc.hp + game:stepMonsterToward(orc, game.player.x, game.player.y) + eq(orc.x, 6, "the orc steps onto the trapped cell while closing on the player") + ok(orc.hp < hpBefore, "stepping onto a player-set dart trap wounds the monster") + eq(game.level.traps[1].revealed, true, "the sprung trap becomes revealed") +end + +-- Status.LEVITATE (see status.lua) makes Game:checkTrapFor and movePlayer's +-- own inline trap check both bail out before springTrap ever runs - and, +-- unlike the lava/chasm skip in checkTerrainFor, deliberately leaves the +-- trap unrevealed: floating clear of a pressure plate buys safety, not free +-- detection of what was under it. +function tests.checkTrapForSkipsAndLeavesUndetectedForALevitatingMonster() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.traps = { { x = 6, y = 6, effect = "dart", revealed = false } } + local orc = Monsters.create(Monsters.byKey.orc, 1, 7, 6, 1) + Status.apply(orc, Status.LEVITATE, 20, 1) + game.level.entities = { orc } + local hpBefore = orc.hp + game:stepMonsterToward(orc, game.player.x, game.player.y) + eq(orc.x, 6, "the levitating orc still steps onto the trapped cell while closing on the player") + eq(orc.hp, hpBefore, "a levitating monster drifting over a trap takes no damage from it") + eq(game.level.traps[1].revealed, false, "the trap it floated over is never revealed") +end + +function tests.movePlayerSkipsTheInlineTrapTriggerForALevitatingPlayer() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.traps = { { x = 6, y = 5, effect = "dart", revealed = false } } + Status.apply(game.player, Status.LEVITATE, 20, 1) + local hpBefore = game.player.hp + game:movePlayer(1, 0) + eq(game.player.x, 6, "the levitating player still moves onto the trapped cell") + eq(game.player.hp, hpBefore, "a levitating player takes no damage from the trap") + eq(game.level.traps[1].revealed, false, "the trap the player floated over stays hidden") +end + +function tests.aLethalTrapKillsAWeakMonsterAndCreditsThePlayer() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.traps = { { x = 6, y = 6, effect = "dart", revealed = false } } + local rat = Monsters.create(Monsters.byKey.rat, 1, 7, 6, 1) + rat.hp, rat.maxHp = 1, 1 + game.level.entities = { rat } + local xpBefore = game.player.xp + game:stepMonsterToward(rat, game.player.x, game.player.y) + ok(not rat:isAlive(), "a dart trap can finish off a weak monster") + ok(game.player.xp > xpBefore, "the player is credited xp for a trap kill") + eq(#game.level.entities, 0, "the fallen monster is swept from the level") +end + +function tests.aTeleportTrapRelocatesTheMonsterThatSpringsIt() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.traps = { { x = 6, y = 6, effect = "teleport", revealed = false } } + local orc = Monsters.create(Monsters.byKey.orc, 1, 7, 6, 1) + game.level.entities = { orc } + game:stepMonsterToward(orc, game.player.x, game.player.y) + ok(not (orc.x == 6 and orc.y == 6), "a teleport trap relocates the monster off the trapped cell") +end + +function tests.builderOccasionallyPlacesATrapKitFromDepthTwo() + local level = Builder.build(3, Rng.new(3), {}) + local found + for _, ground in ipairs(level.floorItems) do + if ground.item.kind == Items.KIT then found = ground.item end + end + ok(found ~= nil, "this seed's depth-3 floor places a trap kit") + local isKnownEffect = false + for _, effect in ipairs(Builder.TRAP_EFFECTS) do + if found.effect == effect then isKnownEffect = true end + end + ok(isKnownEffect, "the kit's effect is one of Builder.TRAP_EFFECTS") +end + +function tests.kitsStayOffTheEntranceFloor() + for seed = 1, 5 do + local level = Builder.build(1, Rng.new(seed), {}) + for _, ground in ipairs(level.floorItems) do + ok(ground.item.kind ~= Items.KIT, "no trap kit appears on the entrance floor") + end + end +end + +function tests.plantedTrapFieldsSurviveSerialization() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.player.inventory = { Items.kit("poison") } + game:useItem(1) + local restoredLevel = Level.fromData(game.level:toData()) + eq(#restoredLevel.traps, 1, "the planted trap survives a level round trip") + eq(restoredLevel.traps[1].effect, "poison", "the planted trap's effect survives a round trip") + eq(restoredLevel.traps[1].placedByPlayer, true, "the planted trap's origin flag survives a round trip") +end + +function tests.bossCreateBuildsAFreshUnenragedBoss() + local warlord = Bosses.create(Bosses.byKey.warlord, 9, 4, 6) + eq(warlord.isBoss, true, "a created boss is flagged isBoss") + eq(warlord.bossKey, "warlord", "a created boss remembers its catalog key") + eq(warlord.enraged, false, "a freshly created boss has not yet enraged") + eq(warlord.hp, warlord.maxHp, "a created boss starts at full hp") + eq(warlord.species, "boss_warlord", "a created boss's species is namespaced away from Monsters.byKey") +end + +function tests.bossAtFullHealthDoesNotEnrage() + local warlord = Bosses.create(Bosses.byKey.warlord, 1, 1, 1) + local before = warlord.attack + local line = Bosses.tryEnrage(warlord) + eq(line, nil, "a boss above its enrage threshold does not enrage") + eq(warlord.enraged, false, "the boss is still marked as not enraged") + eq(warlord.attack, before, "an unenraged boss keeps its base attack") +end + +function tests.bossEnragesOncePastItsHalfHpThresholdAndOnlyOnce() + local warlord = Bosses.create(Bosses.byKey.warlord, 1, 1, 1) + local spec = Bosses.byKey.warlord + warlord.hp = math.floor(warlord.maxHp * spec.enrageAt) + local attackBefore, speedBefore = warlord.attack, warlord.speed + local line = Bosses.tryEnrage(warlord) + ok(line ~= nil, "a bloodied boss enrages and logs a line") + eq(warlord.enraged, true, "the boss is now flagged enraged") + eq(warlord.attack, attackBefore + spec.enrageAttack, "enraging adds the catalog's flat attack bonus") + eq(warlord.baseAttack, attackBefore + spec.enrageAttack, "enraging also lifts the boss's base attack") + eq(warlord.speed, speedBefore + spec.enrageSpeed, "enraging adds the catalog's flat speed bonus") + local hpAfterFirst = warlord.hp + local second = Bosses.tryEnrage(warlord) + eq(second, nil, "an already-enraged boss does not enrage a second time") + eq(warlord.attack, attackBefore + spec.enrageAttack, "a second call never stacks the attack bonus again") + eq(warlord.hp, hpAfterFirst, "a second call grants no further heal") +end + +function tests.bossLootMatchesItsCatalogEntry() + local lich = Bosses.create(Bosses.byKey.lich, 1, 1, 1) + local loot = Bosses.lootFor(lich) + ok(loot ~= nil, "a boss with a valid bossKey drops its catalog loot") + eq(loot.artifact, "lich_phylactery", "the dropped item is the boss's exact catalog artifact") + eq(loot.unique, Artifacts.REGEN, "the boss loot carries its unique mechanic tag") + local unknown = Bosses.create({ key = "ghost" }, 1, 1, 1) + unknown.bossKey = "ghost" + eq(Bosses.lootFor(unknown), nil, "a boss with no catalog entry drops nothing") +end + +function tests.bossOnlyArtifactsAreExcludedFromTheRandomBuilderRoll() + local bossOnlyKeys = { "warlords_cleaver", "lich_phylactery", "bone_queen_carapace" } + for _, key in ipairs(bossOnlyKeys) do + ok(Artifacts.byKey[key] ~= nil, key .. " is a real catalog entry") + local inOrder = false + for _, orderKey in ipairs(Artifacts.order) do + if orderKey == key then inOrder = true end + end + ok(not inOrder, key .. " never appears in the builder's random artifact roll") + end +end + +function tests.builderOccasionallyPlacesABossFromDepthSix() + local level = Builder.build(6, Rng.new(8), {}) + local found + for _, entity in ipairs(level.entities) do + if entity.isBoss then found = entity end + end + ok(found ~= nil, "this seed's depth-6 floor places a named boss") + eq(found.bossKey, "warlord", "the only boss eligible at depth 6 is the warlord") +end + +function tests.bossesStayOffFloorsAboveDepthSix() + for seed = 1, 5 do + for depth = 1, 5 do + local level = Builder.build(depth, Rng.new(seed), {}) + for _, entity in ipairs(level.entities) do end local order = {