From bc7d3c88d540d39d88de9d89a034491aeb965a38 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 18:01:40 +0200 Subject: [PATCH] test: cover tests.potionOfFortificationAppliesAShield --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 1908f40..d172d6a 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -3762,6 +3762,179 @@ function tests.herbItemsSurviveSerialization() local path = os.tmpname() game:save(path) + local restored = Game.load(path) + os.remove(path) + + local restoredHerb + for _, item in ipairs(restored.player.inventory) do + if item.kind == Items.HERB then restoredHerb = item end + end + ok(restoredHerb ~= nil, "a foraged herb survives a save/load round trip") + eq(restoredHerb.herb, Alchemy.NIGHTCAP, "a herb's species survives a save/load round trip") +end + +function tests.potionOfFortificationAppliesAShield() + local game = Game.new(31) + Effects.apply("fortify", game, game.player, {}) + ok(Status.has(game.player, Status.SHIELD), "a fortify effect wards the drinker") +end + +function tests.usingAFortifyPotionAppliesShieldAndConsumesIt() + local game = stagedGame(1, arena(21, 11), 5, 5) + local potion = Items.potionOf("fortify") + game.player.inventory = { potion } + ok(game:useItem(1), "drinking a fortify potion succeeds") + eq(#game.player.inventory, 0, "drinking the potion consumes it") + ok(Status.has(game.player, Status.SHIELD), "the fortify potion wards the player") +end + +function tests.potionOfLevitationAppliesTheLevitateStatus() + local game = Game.new(31) + Effects.apply("levitate", game, game.player, {}) + ok(Status.has(game.player, Status.LEVITATE), "a levitate effect lifts the drinker off the ground") +end + +function tests.usingAPotionOfLevitationAppliesLevitateAndConsumesIt() + local game = stagedGame(1, arena(21, 11), 5, 5) + local potion = Items.potionOf("levitate") + game.player.inventory = { potion } + ok(game:useItem(1), "drinking a levitation potion succeeds") + eq(#game.player.inventory, 0, "drinking the potion consumes it") + ok(Status.has(game.player, Status.LEVITATE), "the levitation potion lifts the player off the ground") +end + +function tests.companionSummonPlacesItAdjacentToThePlayer() + local game = stagedGame(1, arena(21, 11), 5, 5) + local hound = Companions.summon(game, "hound") + ok(hound ~= nil, "summoning a valid species succeeds next to the player") + eq(hound.kind, "companion", "a summoned companion is tagged with the companion kind") + eq(math.max(math.abs(hound.x - 5), math.abs(hound.y - 5)), 1, "the companion lands adjacent to the player") + eq(#game.level.entities, 1, "the companion joins the level's entity list") +end + +function tests.companionSummonRejectsAnUnknownSpecies() + local game = stagedGame(1, arena(21, 11), 5, 5) + eq(Companions.summon(game, "wyrm"), nil, "an unknown species key summons nothing") + eq(#game.level.entities, 0, "no entity is added for an unknown species") +end + +function tests.companionSummonReplacesAnEarlierBond() + local game = stagedGame(1, arena(21, 11), 5, 5) + local first = Companions.summon(game, "hound") + local second = Companions.summon(game, "golem") + ok(not first:isAlive(), "bonding a new companion retires the earlier one") + ok(second:isAlive(), "the freshly bonded companion is alive") + local alive = 0 + for _, e in ipairs(game.level.entities) do + if e.kind == "companion" and e:isAlive() then alive = alive + 1 end + end + eq(alive, 1, "only one companion is ever alive at a time") +end + +function tests.companionAttacksAdjacentHostileMonsterAndCanKillIt() + local game = stagedGame(1, arena(21, 11), 5, 5) + local hound = Companions.summon(game, "hound") + local rat = Monsters.create(Monsters.byKey.rat, 50, hound.x + 1, hound.y, 1) + game.level.entities[#game.level.entities + 1] = rat + local xpBefore = game.player.xp + Companions.act(game, hound) + eq(rat.hp, 0, "a hound's hit kills the weak rat standing next to it") + ok(game.player.xp > xpBefore, "the player is credited xp for its companion's kill") +end + +-- Before this fix, companions.lua's own "adjacent" helper was Manhattan-1 +-- (cardinal only) while mercenaries.lua's and reanimation.lua's were +-- Chebyshev-1 (8-directional) despite both explicitly claiming to mirror +-- Companions.act's "same three-branch shape" - a hound diagonally next to a +-- monster fell through to stepMonsterToward instead of attacking, unlike a +-- hireling or thrall caught in the identical geometry. +function tests.companionAttacksADiagonallyAdjacentHostileMonsterJustLikeACardinalOne() + local game = stagedGame(1, arena(21, 11), 5, 5) + local hound = Companions.summon(game, "hound") + local rat = Monsters.create(Monsters.byKey.rat, 50, hound.x + 1, hound.y + 1, 1) + game.level.entities[#game.level.entities + 1] = rat + Companions.act(game, hound) + eq(rat.hp, 0, "a hound's hit kills a weak rat standing diagonally next to it, not just cardinally") +end + +function tests.companionAdvancesOnAVisibleMonsterItCannotYetReach() + local game = stagedGame(1, arena(21, 11), 5, 5) + local hound = Companions.summon(game, "hound") + local orc = Monsters.create(Monsters.byKey.orc, 50, hound.x + 5, hound.y, 1) + game.level.entities[#game.level.entities + 1] = orc + local startX = hound.x + Companions.act(game, hound) + ok(hound.x > startX, "the companion closes the distance to a monster it can see but not reach") +end + +function tests.companionIgnoresAMonsterBeyondItsSightRadius() + local game = stagedGame(1, arena(21, 11), 3, 3) + local hound = Companions.summon(game, "hound") + local orc = Monsters.create(Monsters.byKey.orc, 50, 19, 9, 1) + game.level.entities[#game.level.entities + 1] = orc + local startX, startY = hound.x, hound.y + Companions.act(game, hound) + eq(hound.x, startX, "a monster far outside sight range does not pull the companion off course (x)") + eq(hound.y, startY, "a monster far outside sight range does not pull the companion off course (y)") +end + +-- Before Companions.act's own nearestHostile clamped its sight to melee +-- range under Status.BLIND, a blinded companion kept its full listed sight +-- radius - the same gap Game:canSeePlayer's own clamp already closed for a +-- blinded hostile monster (canSeePlayerBlindedMonsterOnlyDetectsThePlayer +-- AtMeleeRange), just never mirrored onto the ally side. +function tests.companionActBlindedCompanionOnlyDetectsAnAdjacentHostileNotOneWithinItsSightRadius() + local game = stagedGame(1, arena(21, 11), 5, 5) + local hound = Companions.summon(game, "hound") + local farOrc = Monsters.create(Monsters.byKey.orc, 50, hound.x + 5, hound.y, 1) + game.level.entities[#game.level.entities + 1] = farOrc + Status.apply(hound, Status.BLIND, 6, 1) + local startX = hound.x + Companions.act(game, hound) + eq(hound.x, startX, "a blinded companion loses a sighting well within its listed sight radius") + + local rat = Monsters.create(Monsters.byKey.rat, 51, hound.x + 1, hound.y, 1) + game.level.entities[#game.level.entities + 1] = rat + local ratHpBefore = rat.hp + Companions.act(game, hound) + ok(rat.hp < ratHpBefore, "a blinded companion still fights a hostile once it stands adjacent") +end + +-- Companions.act called Combat.attack directly without ever routing through +-- Game:emitNoise, unlike Game:playerAttack/Game:monsterMelee which both do +-- (see gamePlayerAttackCanStirADormantBystanderThatIsNotEvenTheTarget and +-- gameMonsterMeleeCanStirADormantBystanderTooNotJustPlayerActions) - a +-- companion's own melee was silent to every dormant monster nearby, no +-- matter how loud its species' attack would be for the player or a hostile. +-- Fresh game per seed (unlike those two tests' single reused game) since a +-- companion attack, unlike a repeatable playerAttack/monsterMelee call, can +-- kill its one-hp-pool rat target outright. +function tests.companionActAttackingAnAdjacentMonsterEmitsNoiseThatCanStirADormantBystander() + local stirred = false + for seed = 1, 200 do + local game = stagedGame(seed, arena(21, 11), 3, 6) + local hound = Companions.summon(game, "hound") + local target = Monsters.create(Monsters.byKey.rat, 50, hound.x + 1, hound.y, 1) + local bystander = Monsters.create(Monsters.byKey.orc, 51, hound.x + 4, hound.y, 1) + bystander.awareness = Stealth.DORMANT + game.level.entities[#game.level.entities + 1] = target + game.level.entities[#game.level.entities + 1] = bystander + Companions.act(game, hound) + if bystander.awareness ~= Stealth.DORMANT then stirred = true; break end + end + ok(stirred, "a companion's own melee is exactly as loud as the player's or a hostile monster's") +end + +function tests.companionFollowsThePlayerWhenNoMonsterIsVisible() + local game = stagedGame(1, arena(21, 11), 10, 5) + local golem = Companions.summon(game, "golem") + golem.x, golem.y = 3, 5 + local startDist = math.abs(golem.x - game.player.x) + Companions.act(game, golem) + ok(math.abs(golem.x - game.player.x) < startDist, "a companion with no monster in sight closes in on the player") +end + +-- Companions.act never dispatches through ai.lua, so without Game:allyActs end local order = {