test: cover tests.corpsesButcheringFreshMeatAddsFoodToInventoryAndCon...

This commit is contained in:
dualfroz committed 2026-09-09 14:26:48 +02:00
1 parent 183e67da75
commit c58888f5c0
1 file changed
+173
+173
View File
@@ -3243,6 +3243,179 @@ function tests.corpsesGoesRottenPastItsThresholdButStaysButcherableUntilThen()
corpse.decay = Corpses.ROT_TURNS corpse.decay = Corpses.ROT_TURNS
ok(Corpses.isRotten(corpse), "a corpse right at its rot threshold counts as rotten") ok(Corpses.isRotten(corpse), "a corpse right at its rot threshold counts as rotten")
corpse.decay = Corpses.ROT_TURNS + 1 corpse.decay = Corpses.ROT_TURNS + 1
ok(not Corpses.isRotten(corpse), "a corpse one round above the threshold is still good")
end
function tests.corpsesButcheringFreshMeatAddsFoodToInventoryAndConsumesTheCorpse()
local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1)
local level = { corpses = {} }
Corpses.spawn(level, rat)
local corpse = level.corpses[1]
local game = { player = corpsePlayer(), level = level }
local line = Corpses.butcher(game, corpse)
eq(#level.corpses, 0, "butchering removes the corpse from the level")
eq(#game.player.inventory, 1, "butchering fresh meat adds a food item to the inventory")
eq(game.player.inventory[1].kind, Items.FOOD, "the yield is a food item")
eq(game.player.inventory[1].nutrition, corpse.nutrition, "the food item carries the corpse's own nutrition value")
ok(contains(line, "carve"), "a fresh butchering is logged as carving meat")
ok(#game.player.statuses == 0, "fresh meat never poisons the player")
end
function tests.corpsesButcheringRottenMeatPoisonsThePlayerInsteadAndStillConsumesTheCorpse()
local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1)
local level = { corpses = {} }
Corpses.spawn(level, rat)
local corpse = level.corpses[1]
corpse.decay = Corpses.ROT_TURNS
local game = { player = corpsePlayer(), level = level }
local line = Corpses.butcher(game, corpse)
eq(#level.corpses, 0, "butchering removes a rotten corpse too")
eq(#game.player.inventory, 0, "rotten meat yields no food item")
ok(Status.has(game.player, Status.POISON), "eating rotten meat poisons the player")
ok(contains(line, "rotted"), "a rotten butchering is logged as such")
end
function tests.corpseFieldsSurviveSerialization()
local level = arena(21, 11)
local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1)
Corpses.spawn(level, rat)
level.corpses[1].x, level.corpses[1].y = 4, 4
local restored = Level.fromData(level:toData())
eq(#restored.corpses, 1, "the corpse list survives a round trip")
eq(restored.corpses[1].species, "rat", "a corpse's species survives a round trip")
eq(restored.corpses[1].nutrition, level.corpses[1].nutrition, "a corpse's nutrition survives a round trip")
eq(restored.corpses[1].decay, level.corpses[1].decay, "a corpse's decay survives a round trip")
end
function tests.corpseRendersItsGlyphWhenVisible()
local rendered = stagedGame(1, arena(21, 11), 3, 6)
local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 6, 1)
Corpses.spawn(rendered.level, rat)
rendered:computeVisible()
ok(contains(rendered:render(), "%"), "a corpse on a visible cell renders as its glyph")
end
function tests.gameAwardKillLeavesACorpseForAWildMonsterButNotForUndeadOrMimic()
local game = stagedGame(1, arena(21, 11), 3, 6)
local rat = Monsters.create(Monsters.byKey.rat, 1, 4, 6, 1)
rat.hp = 0
game.level.entities = { rat }
game:awardKill(rat)
eq(#game.level.corpses, 1, "killing a wild monster through awardKill leaves a corpse")
eq(game.level.corpses[1].x, 4, "the corpse sits exactly where the monster fell")
local zombie = Monsters.create(Monsters.byKey.zombie, 2, 4, 7, 4)
zombie.hp = 0
game.level.entities = { zombie }
game:awardKill(zombie)
eq(#game.level.corpses, 1, "killing an undead monster through awardKill leaves no new corpse")
end
function tests.gameButcherCorpseAtFreshMeatFeedsThePlayerAndClearsTheCorpse()
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 before = #game.player.inventory
ok(game:butcherCorpse(), "butchering the corpse underfoot succeeds")
eq(#game.level.corpses, 0, "the corpse is gone after butchering")
eq(#game.player.inventory, before + 1, "the player's inventory gains the carved meat")
end
function tests.gameButcherCorpseFailsWithoutACorpseUnderfoot()
local game = stagedGame(1, arena(21, 11), 5, 5)
ok(not game:butcherCorpse(), "butchering empty ground fails")
end
function tests.autopilotButchersAFreshCorpseWhenIdleAndStandingOnIt()
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 before = #game.player.inventory
game:choosePlayerAction()
eq(#game.level.corpses, 0, "the autopilot butchers a fresh corpse it stands on")
eq(#game.player.inventory, before + 1, "the autopilot's butchering actually yields food")
end
function tests.autopilotLeavesARottenCorpseAloneRatherThanRiskingPoison()
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
game:choosePlayerAction()
eq(#game.level.corpses, 1, "the autopilot never touches a rotten corpse")
ok(not Status.has(game.player, Status.POISON), "the autopilot never risks poisoning itself on rotten meat")
end
function tests.hungerCurrentDefaultsToCapForAFreshEntity()
eq(Hunger.current({}), Hunger.CAP, "an entity with no satiation field reads as fully fed")
end
function tests.hungerTierOfClassifiesEachBandAtItsBoundaries()
eq(Hunger.tierOf({ satiation = Hunger.CAP }), Hunger.FED, "a full meter is fed")
eq(Hunger.tierOf({ satiation = Hunger.HUNGRY_MAX + 1 }), Hunger.FED, "just above the hungry ceiling is still fed")
eq(Hunger.tierOf({ satiation = Hunger.HUNGRY_MAX }), Hunger.HUNGRY, "the hungry ceiling itself reads as hungry")
eq(Hunger.tierOf({ satiation = Hunger.WEAK_MAX + 1 }), Hunger.HUNGRY, "just above the weak ceiling is still hungry")
eq(Hunger.tierOf({ satiation = Hunger.WEAK_MAX }), Hunger.WEAK, "the weak ceiling itself reads as weak")
eq(Hunger.tierOf({ satiation = 1 }), Hunger.WEAK, "one point above zero is still weak, never starving")
eq(Hunger.tierOf({ satiation = 0 }), Hunger.STARVING, "zero itself is starving")
end
function tests.hungerAttackAndDefensePenaltiesEscalateByTier()
eq(Hunger.attackPenalty({ satiation = Hunger.CAP }), 0, "a fed body carries no attack penalty")
eq(Hunger.attackPenalty({ satiation = Hunger.HUNGRY_MAX }), 0, "merely hungry still carries no attack penalty")
eq(Hunger.attackPenalty({ satiation = Hunger.WEAK_MAX }), -2, "weak costs two points of attack")
eq(Hunger.attackPenalty({ satiation = 0 }), -4, "starving costs four points of attack")
eq(Hunger.defensePenalty({ satiation = Hunger.CAP }), 0, "a fed body carries no defense penalty")
eq(Hunger.defensePenalty({ satiation = Hunger.HUNGRY_MAX }), 0, "merely hungry still carries no defense penalty")
eq(Hunger.defensePenalty({ satiation = Hunger.WEAK_MAX }), -1, "weak costs one point of defense")
eq(Hunger.defensePenalty({ satiation = 0 }), -3, "starving costs three points of defense")
end
function tests.entityEffectiveAttackAndDefenseFoldInTheHungerPenalty()
local entity = Entity.new({ x = 1, y = 1, glyph = "@", name = "you", kind = "player", hp = 20, attack = 6, defense = 4 })
eq(entity:effectiveAttack(), 6, "a fresh entity with no satiation field carries no penalty")
entity.satiation = 0
eq(entity:effectiveAttack(), 2, "a starving body's attack penalty is live in effectiveAttack")
eq(entity:effectiveDefense(), 1, "a starving body's defense penalty is live in effectiveDefense")
end
function tests.hungerSuppressesRegenOnlyAtWeakOrWorse()
ok(not Hunger.suppressesRegen({ satiation = Hunger.HUNGRY_MAX }), "merely hungry never blocks passive regen")
ok(Hunger.suppressesRegen({ satiation = Hunger.WEAK_MAX }), "weak blocks passive regen")
ok(Hunger.suppressesRegen({ satiation = 0 }), "starving blocks passive regen")
end
function tests.hungerTickDrainsByOneAndLogsOnlyOnATierCrossing()
local entity = { satiation = Hunger.HUNGRY_MAX + 2, hp = 20 }
eq(Hunger.tick(entity), nil, "a drain turn that stays within its tier reports no line")
eq(entity.satiation, Hunger.HUNGRY_MAX + 1, "the drain was still deducted even without a tier crossing")
eq(Hunger.tick(entity), "you feel hungry", "crossing down into hungry is announced")
eq(entity.satiation, Hunger.HUNGRY_MAX, "the meter keeps draining turn over turn")
eq(entity.hp, 20, "hunger alone never costs hp above starving")
end
function tests.hungerTickBitesForOneHpEveryRoundWhileStarving()
local entity = { satiation = 1, hp = 10 }
eq(Hunger.tick(entity), "you are starving", "crossing down into starving is announced")
eq(entity.satiation, 0, "starving is a floor, not a negative value")
eq(entity.hp, 9, "reaching starving costs a point of hp immediately")
eq(Hunger.tick(entity), "you are starving", "starving keeps announcing and biting every round it holds")
eq(entity.hp, 8, "a second starving round costs a second point of hp")
end
function tests.hungerEatCapsAtTheCeilingAndWastesAnOverfullMealIntoBloat()
local entity = { satiation = Hunger.CAP, statuses = {} }
local bloated = Hunger.eat(entity, 400)
ok(bloated, "eating while already at the cap reports the meal as wasted")
eq(entity.satiation, Hunger.CAP, "an overfull meal never lifts satiation past the cap")
ok(Status.has(entity, Status.SLOW), "an overfull meal leaves the diner slowed")
end
function tests.hungerEatBelowTheCeilingBanksNutritionWithNoWaste()
local entity = { satiation = Hunger.HUNGRY_MAX, statuses = {} }
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")
end end
local order = { local order = {