From b35beaeee7aa5de05952d90b664285235218c90a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 17:41:03 +0200 Subject: [PATCH] test: cover tests.hungerEatClampsAMealThatWouldOverflowPastTheCapWith... --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index fb60dd5..1080e17 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -3416,6 +3416,179 @@ function tests.hungerEatBelowTheCeilingBanksNutritionWithNoWaste() local bloated = Hunger.eat(entity, 100) ok(not bloated, "an ordinary meal below the cap is never wasted") eq(entity.satiation, Hunger.HUNGRY_MAX + 100, "the full nutrition value is banked") + ok(not Status.has(entity, Status.SLOW), "an ordinary meal never bloats the diner") +end + +function tests.hungerEatClampsAMealThatWouldOverflowPastTheCapWithoutBloating() + local entity = { satiation = Hunger.CAP - 50, statuses = {} } + local bloated = Hunger.eat(entity, 400) + ok(not bloated, "a meal eaten while still short of the cap is never wasted, even if it overflows") + eq(entity.satiation, Hunger.CAP, "the surplus past the cap is simply clamped away") + ok(not Status.has(entity, Status.SLOW), "clamping alone never bloats the diner") +end + +function tests.gameHungerTorchTickSuppressesRingRegenOnceThePlayerIsWeak() + local game = Game.new(1) + Items.equipRing(Artifacts.create("ring_of_the_tide"), game.player) + game.player.hp = game.player.maxHp - 10 + game.player.satiation = Hunger.WEAK_MAX + local before = game.player.hp + game:hungerTorchTick() + eq(game.player.hp, before, "a weak player's ring of the tide grants no regen this turn") +end + +function tests.gameUseItemEatingFoodRestoresSatiationAndLogsTheMeal() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.satiation = 200 + game.player.inventory = { Items.food("ration", 400) } + ok(game:useItem(1), "eating a carried ration succeeds") + eq(game.player.satiation, 600, "the ration's full nutrition is banked") + eq(#game.player.inventory, 0, "the eaten ration leaves the inventory") + ok(contains(game.log:last(), "eat"), "the meal is logged") +end + +function tests.gameUseItemEatingFoodWhileAlreadyFullAppliesSlowAndLogsBloat() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.satiation = Hunger.CAP + game.player.inventory = { Items.food("ration", 400) } + ok(game:useItem(1), "eating while already full still succeeds") + eq(game.player.satiation, Hunger.CAP, "an overfull meal never lifts satiation past the cap") + ok(Status.has(game.player, Status.SLOW), "eating past the cap leaves the player bloated and slow") + ok(contains(game.log:last(), "bloated"), "the wasted meal is logged as bloating") +end + +function tests.reanimationRaiseCreatesAThrallAdjacentToThePlayerAndSpendsTheCorpseAndSanity() + 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 corpse = game.level.corpses[1] + local sanityBefore = game.player.sanity + local thrall = Reanimation.raise(game, corpse) + ok(thrall ~= nil, "raising a fresh corpse succeeds") + eq(thrall.kind, "thrall", "a raised thrall is tagged with the thrall kind") + eq(math.max(math.abs(thrall.x - 5), math.abs(thrall.y - 5)), 1, "the thrall rises adjacent to the player") + eq(#game.level.corpses, 0, "raising the corpse consumes it") + eq(game.player.sanity, sanityBefore - Reanimation.SANITY_COST, "raising a thrall costs a fixed bite of sanity") +end + +function tests.reanimationRaiseRefusesARottenCorpseWithoutSpendingAnything() + 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 sanityBefore = game.player.sanity + local thrall, line = Reanimation.raise(game, game.level.corpses[1]) + eq(thrall, nil, "a rotten corpse cannot be raised") + eq(#game.level.corpses, 1, "a refused raise leaves the corpse in place") + eq(game.player.sanity, sanityBefore, "a refused raise spends no sanity") + ok(contains(line, "rotted"), "the refusal explains the corpse has rotted too far") +end + +function tests.reanimationRaiseRefusesWithoutEnoughSanityAndLeavesTheCorpseAlone() + 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.player.sanity = Reanimation.SANITY_COST - 1 + local thrall, line = Reanimation.raise(game, game.level.corpses[1]) + eq(thrall, nil, "raising fails when sanity is too low to pay the cost") + eq(#game.level.corpses, 1, "a refused raise for lack of sanity leaves the corpse untouched") + ok(contains(line, "mind"), "the refusal blames the player's own frail grip on their mind") +end + +function tests.reanimationRaiseScalesThrallStatsDownFromTheOriginSpecies() + local game = stagedGame(1, arena(21, 11), 5, 5) + local troll = Monsters.create(Monsters.byKey.troll, 1, 5, 5, 5) + Corpses.spawn(game.level, troll) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + eq(thrall.hp, math.floor(Monsters.byKey.troll.hp * Reanimation.HP_FRACTION), + "a thrall's hp is a fixed fraction of the living species' own hp") + eq(thrall.attack, Monsters.byKey.troll.attack - Reanimation.ATTACK_PENALTY, + "a thrall's attack is blunted by a fixed amount from the living species") + eq(thrall.defense, Monsters.byKey.troll.defense, "a thrall keeps the living species' own defense") +end + +function tests.reanimationRaiseFallsBackToAPlainSkeletonForAnUnrecognisedSpecies() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.corpses[1] = { x = 5, y = 5, species = "unknown-thing", name = "unknown-thing", decay = Corpses.DECAY_TURNS } + local thrall = Reanimation.raise(game, game.level.corpses[1]) + ok(thrall ~= nil, "a corpse of an unrecognised species can still be raised") + eq(thrall.hp, 8, "an unrecognised species falls back to a flat skeleton hp") + eq(thrall.attack, 2, "an unrecognised species falls back to a flat skeleton attack") +end + +function tests.reanimationRaiseRetiresAnEarlierThrallWhenRaisingAnother() + local game = stagedGame(1, arena(21, 11), 5, 5) + local rat1 = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + Corpses.spawn(game.level, rat1) + local first = Reanimation.raise(game, game.level.corpses[1]) + local rat2 = Monsters.create(Monsters.byKey.rat, 2, 6, 5, 1) + Corpses.spawn(game.level, rat2) + local second = Reanimation.raise(game, game.level.corpses[1]) + ok(not first:isAlive(), "raising a new thrall retires the earlier one") + ok(second:isAlive(), "the freshly raised thrall is alive") + local alive = 0 + for _, e in ipairs(game.level.entities) do + if e.kind == "thrall" and e:isAlive() then alive = alive + 1 end + end + eq(alive, 1, "only one thrall is ever alive at a time") +end + +function tests.reanimationActAttacksAnAdjacentHostileMonsterAndCanKillIt() + 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 + local xpBefore = game.player.xp + Reanimation.act(game, thrall) + eq(rat.hp, 0, "a thrall's hit kills the weak rat standing next to it") + ok(game.player.xp > xpBefore, "the player is credited xp for its thrall's kill") +end + +function tests.reanimationActAdvancesOnAVisibleMonsterItCannotYetReach() + 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]) + local orc = Monsters.create(Monsters.byKey.orc, 50, thrall.x + 5, thrall.y, 1) + game.level.entities[#game.level.entities + 1] = orc + local startX = thrall.x + Reanimation.act(game, thrall) + ok(thrall.x > startX, "the thrall closes the distance to a monster it can see but not reach") +end + +function tests.reanimationActIgnoresAMonsterBeyondItsSightRadius() + local game = stagedGame(1, arena(21, 11), 3, 3) + local rat0 = Monsters.create(Monsters.byKey.rat, 1, 3, 3, 1) + Corpses.spawn(game.level, rat0) + local thrall = Reanimation.raise(game, game.level.corpses[1]) + local orc = Monsters.create(Monsters.byKey.orc, 50, 19, 9, 1) + game.level.entities[#game.level.entities + 1] = orc + local startX, startY = thrall.x, thrall.y + Reanimation.act(game, thrall) + eq(thrall.x, startX, "a monster far outside sight range does not pull the thrall off course (x)") + eq(thrall.y, startY, "a monster far outside sight range does not pull the thrall off course (y)") +end + +-- Same gap as companionActBlindedCompanionOnlyDetectsAnAdjacentHostileNot +-- OneWithinItsSightRadius, but for Reanimation.act's own nearestHostile. +function tests.reanimationActBlindedThrallOnlyDetectsAnAdjacentHostileNotOneWithinItsSightRadius() + 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]) + local farOrc = Monsters.create(Monsters.byKey.orc, 50, thrall.x + 5, thrall.y, 1) + game.level.entities[#game.level.entities + 1] = farOrc + Status.apply(thrall, Status.BLIND, 6, 1) + local startX = thrall.x + Reanimation.act(game, thrall) + eq(thrall.x, startX, "a blinded thrall loses a sighting well within its listed sight radius") + + local rat = Monsters.create(Monsters.byKey.rat, 51, thrall.x + 1, thrall.y, 1) + game.level.entities[#game.level.entities + 1] = rat + local ratHpBefore = rat.hp + Reanimation.act(game, thrall) end local order = {