diff --git a/tests/run.lua b/tests/run.lua index 4204c47..27847f0 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -10336,6 +10336,179 @@ end function tests.nemesisEscalateRaisesGrudgeAndHealsTowardFullBothCapped() local pursuit = { species = "orc", grudge = 1, hpFraction = 0.3 } + Nemesis.escalate(pursuit) + eq(pursuit.grudge, 2, "escalating raises the grudge by one") + eq(pursuit.hpFraction, 0.5, "escalating heals the pursuit's carried hp fraction") + for _ = 1, 10 do Nemesis.escalate(pursuit) end + eq(pursuit.grudge, Nemesis.MAX_GRUDGE, "grudge never climbs past its cap") + eq(pursuit.hpFraction, 1, "hp fraction never heals past full") +end + +function tests.nemesisSpawnScalesHpAndAttackWithGrudgeAndAppliesTheCarriedHpFraction() + local base = Monsters.create(Monsters.byKey.orc, 1, 5, 5, 1) + local weak = Nemesis.spawn({ species = "orc", grudge = 1, hpFraction = 0.5 }, 2, 5, 5, 1) + eq(weak.maxHp, base.maxHp + Nemesis.bonusHp(1), "a grudge-one nemesis carries a small hp bonus over the plain species") + eq(weak.hp, math.floor(weak.maxHp * 0.5), "the spawned nemesis's hp reflects its carried hp fraction") + eq(weak.attack, base.attack + Nemesis.bonusAttack(1), "a grudge-one nemesis carries a small attack bonus") + ok(weak.nemesis, "the spawned entity is flagged as a nemesis") + eq(weak.nemesisGrudge, 1, "the spawned entity remembers its own grudge tier") + eq(weak.species, "orc", "the spawned nemesis keeps the species key its xp/faction lookups depend on") + + local fierce = Nemesis.spawn({ species = "orc", grudge = 4, hpFraction = 1 }, 3, 5, 5, 1) + ok(fierce.maxHp > weak.maxHp, "a higher grudge nemesis carries a larger hp bonus") + ok(fierce.attack > weak.attack, "a higher grudge nemesis carries a larger attack bonus") + + local sliver = Nemesis.spawn({ species = "orc", grudge = 1, hpFraction = 0.001 }, 4, 5, 5, 1) + ok(sliver.hp >= 1, "a nemesis spawned from a nearly-dead fraction never rounds down to zero hp") +end + +function tests.nemesisNameForEscalatesThroughEachEpithetAndClampsAtTheLast() + eq(Nemesis.nameFor("orc", 1), "orc, " .. Nemesis.EPITHETS[1], "grudge one uses the first epithet") + eq(Nemesis.nameFor("orc", #Nemesis.EPITHETS), "orc, " .. Nemesis.EPITHETS[#Nemesis.EPITHETS], + "grudge at the epithet count uses the last epithet") + eq(Nemesis.nameFor("orc", Nemesis.MAX_GRUDGE + 5), "orc, " .. Nemesis.EPITHETS[#Nemesis.EPITHETS], + "grudge beyond the epithet list clamps to the last epithet rather than indexing past it") +end + +function tests.nemesisXpBonusAndBountyGoldScaleWithGrudge() + ok(Nemesis.xpBonus(2) > Nemesis.xpBonus(1), "a higher grudge pays a bigger xp bonus") + ok(Nemesis.bountyGold(2) > Nemesis.bountyGold(1), "a higher grudge pays a bigger bounty") + eq(Nemesis.bountyGold(1), 10, "grudge one's bounty matches the catalog base") +end + +-- Seed 3, not 5: adding a tenth potion type (levitate) grew identify.lua's +-- potionLooks pool by one, which costs Identify.new one extra shuffle draw +-- at world creation - every seed's subsequent rng stream shifts by exactly +-- that much, including this test's nemesis-emergence chance roll. Reseeded +-- by actually running Game:descend() across a range and picking one where +-- the roll still succeeds, not guessed. +function tests.gameDescendCanRollANewNemesisFromAHuntingMonsterLeftOnTheDepartingFloor() + local game = stagedGame(3, arena(21, 11), 2, 2) + local orc = Monsters.create(Monsters.byKey.orc, 1, 10, 6, 1) + orc.awareness = Stealth.HUNTING + game.level.entities = { orc } + game.player.x, game.player.y = game.level.downX, game.level.downY + game:descend() + ok(game.nemesis ~= nil, "this seed's departure roll gives the hunting orc a chance to follow") + eq(game.nemesis.species, "orc", "the nemesis descriptor remembers which species followed") + local found + for _, entity in ipairs(game.level.entities) do + if entity.nemesis then found = entity end + end + ok(found ~= nil, "the nemesis is actually placed onto the floor just reached") + eq(found.nemesisGrudge, 1, "a freshly emerged nemesis starts at grudge one") +end + +function tests.gameDescendNeverRollsANewNemesisFromAMonsterThatIsNotActuallyHunting() + for seed = 1, 30 do + local game = stagedGame(seed, arena(21, 11), 2, 2) + local orc = Monsters.create(Monsters.byKey.orc, 1, 10, 6, 1) + game.level.entities = { orc } + game.player.x, game.player.y = game.level.downX, game.level.downY + game:descend() + ok(game.nemesis == nil, "a dormant monster never gives chase no matter the seed") + end +end + +function tests.gameNemesisFollowsAcrossMultipleDescentsEscalatingItsGrudgeEachTime() + local game = stagedGame(1, arena(21, 11), 2, 2) + game.nemesis = { species = "orc", grudge = 1, hpFraction = 1 } + local hunter = Nemesis.spawn(game.nemesis, game:nextMonsterId(), 10, 6, 1) + game.level.entities = { hunter } + game.player.x, game.player.y = game.level.downX, game.level.downY + + game:descend() + eq(game.nemesis.grudge, 2, "following once past the first stairwell raises the grudge to two") + local first + for _, entity in ipairs(game.level.entities) do if entity.nemesis then first = entity end end + ok(first ~= nil, "the escalated nemesis is placed on the second floor") + eq(first.nemesisGrudge, 2, "the respawned nemesis carries the escalated grudge") + + game.player.x, game.player.y = game.level.downX, game.level.downY + game:descend() + eq(game.nemesis.grudge, 3, "following a second time raises the grudge again") + local second + for _, entity in ipairs(game.level.entities) do if entity.nemesis then second = entity end end + ok(second ~= nil, "the nemesis follows onto the third floor too") + local nemesisCount = 0 + for _, entity in ipairs(game.level.entities) do + if entity.nemesis then nemesisCount = nemesisCount + 1 end + end + eq(nemesisCount, 1, "exactly one nemesis is ever on a floor at a time, never a duplicate") +end + +function tests.gameAscendCarriesAnActiveNemesisAlongButNeverRollsANewOne() + local game = stagedGame(1, arena(21, 11), 2, 2) + game.player.x, game.player.y = game.level.downX, game.level.downY + game:descend() + eq(game.depth, 2, "the game is now on the second floor") + eq(game.nemesis, nil, "no nemesis was following yet") + + for seed = 1, 20 do + local ascendGame = stagedGame(seed, arena(21, 11), 2, 2) + ascendGame.player.x, ascendGame.player.y = ascendGame.level.downX, ascendGame.level.downY + ascendGame:descend() + local orc = Monsters.create(Monsters.byKey.orc, 99, 10, 6, ascendGame.depth) + orc.awareness = Stealth.HUNTING + ascendGame.level.entities = { orc } + ascendGame.player.x, ascendGame.player.y = ascendGame.level.upX, ascendGame.level.upY + ascendGame:ascend() + ok(ascendGame.nemesis == nil, "climbing back up never manufactures a brand new nemesis") + end + + local followGame = stagedGame(1, arena(21, 11), 2, 2) + followGame.player.x, followGame.player.y = followGame.level.downX, followGame.level.downY + followGame:descend() + followGame.nemesis = { species = "orc", grudge = 2, hpFraction = 1 } + local hunter = Nemesis.spawn(followGame.nemesis, followGame:nextMonsterId(), 10, 6, followGame.depth) + followGame.level.entities = { hunter } + followGame.player.x, followGame.player.y = followGame.level.upX, followGame.level.upY + followGame:ascend() + eq(followGame.nemesis.grudge, 3, "an already-active nemesis still follows the player back up the stairs") + local found + for _, entity in ipairs(followGame.level.entities) do if entity.nemesis then found = entity end end + ok(found ~= nil, "the nemesis is placed on the floor just climbed back onto") +end + +-- companions.lua/mercenaries.lua/reanimation.lua's own module comments each +-- describe a bonded ally as sticking with the player for the run (a +-- companion "persists across the whole run once bonded", a hireling demands +-- its wage "for as long as it lives") - before Game:detachCarriedAllies/ +-- Game:plantCarriedAllies existed, Game:descend simply swapped self.level to +-- the next floor's own dungeon:get(depth) result, so every one of the three +-- was silently left behind, alive and well, on the departing floor forever. +function tests.gameDescendCarriesEveryLivingAllyOntoTheNextFloorInsteadOfAbandoningThem() + local game = stagedGame(1, arena(21, 11), 3, 6) + local oldLevel = game.level + local golem = Companions.summon(game, "golem") + local hireling = Mercenaries.hire(game, "sellsword") + local rat = Monsters.create(Monsters.byKey.rat, 99, 8, 6, 1) + Corpses.spawn(game.level, rat) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + ok(golem ~= nil and hireling ~= nil and thrall ~= nil, "all three allies bond before the descent") + + game.player.x, game.player.y = game.level.downX, game.level.downY + ok(game:descend(), "descending succeeds") + eq(game.depth, 2, "the game is now on the second floor") + ok(game:hasCompanion(), "the companion is with the player on the new floor") + ok(game:hasHireling(), "the hireling is with the player on the new floor") + local thrallFound + for _, entity in ipairs(game.level.entities) do + if entity.kind == "thrall" then thrallFound = entity end + end + eq(thrallFound, thrall, "the very same thrall instance rises with the player, not a rebuilt one") + + local leftBehind = 0 + for _, entity in ipairs(oldLevel.entities) do + if entity.kind == "companion" or entity.kind == "hireling" or entity.kind == "thrall" then + leftBehind = leftBehind + 1 + end + end + eq(leftBehind, 0, "none of the three allies are stranded on the departing floor") +end + +function tests.gameAscendCarriesEveryLivingAllyBackOntoThePreviousFloorToo() + local game = stagedGame(1, arena(21, 11), 3, 6) end local order = {