diff --git a/tests/run.lua b/tests/run.lua index 0d06528..1b841d3 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -3070,6 +3070,179 @@ function tests.nodesHarvestingAGemSeamYieldsARawTierOneGemOfItsElement() eq(#game.player.inventory, 1, "a gem lands in the inventory") local gem = game.player.inventory[1] eq(gem.kind, Items.GEM, "the yield is a gem item") + eq(gem.element, Crafting.FIRE, "the gem matches the seam's own element") + eq(gem.tier, 1, "a freshly pried gem is raw, tier one") +end + +function tests.nodesGoingFallowRefusesFurtherHarvestsUntilItReopens() + local node = Nodes.new(Nodes.ORE) + node.charges = 1 + local game = nodeGame(9) + ok(Nodes.harvest(game, node), "the last charge still pays out") + eq(node.charges, 0, "spending the last charge empties the vein") + eq(node.cooldown, Nodes.REGEN_TURNS, "emptying a vein starts its regen cooldown") + local success, line = Nodes.harvest(game, node) + eq(success, false, "a fallow vein refuses to be harvested again") + ok(contains(line, "played out"), "the refusal is logged") +end + +function tests.nodesStepCountsDownAndReopensAFallowNode() + local level = { nodes = { Nodes.new(Nodes.ORE) } } + local node = level.nodes[1] + node.charges = 0 + node.cooldown = 2 + Nodes.step(level) + eq(node.cooldown, 1, "each step counts the cooldown down by one") + eq(node.charges, 0, "the vein stays fallow until its cooldown reaches zero") + Nodes.step(level) + eq(node.cooldown, 0, "the cooldown reaches zero on schedule") + eq(node.charges, Nodes.MAX_CHARGES, "reaching zero cooldown fully reopens the vein") +end + +function tests.nodesFieldsSurviveSerialization() + local level = arena(21, 11) + local node = Nodes.new(Nodes.GEM, Crafting.FROST) + node.x, node.y = 4, 4 + node.charges = 1 + node.cooldown = 5 + level.nodes = { node } + local restored = Level.fromData(level:toData()) + eq(#restored.nodes, 1, "the node list survives a round trip") + eq(restored.nodes[1].kind, Nodes.GEM, "a node's kind survives a round trip") + eq(restored.nodes[1].element, Crafting.FROST, "a node's element survives a round trip") + eq(restored.nodes[1].charges, 1, "a node's charges survive a round trip") + eq(restored.nodes[1].cooldown, 5, "a node's cooldown survives a round trip") +end + +function tests.nodeRendersItsGlyphWhenVisible() + local rendered = stagedGame(1, arena(21, 11), 3, 6) + local ore = Nodes.new(Nodes.ORE) + ore.x, ore.y = 5, 6 + rendered.level.nodes = { ore } + rendered:computeVisible() + ok(contains(rendered:render(), "O"), "an ore vein on a visible cell renders as its glyph") +end + +function tests.gameHarvestNodeAtAnOreVeinAddsEssenceAndSpendsACharge() + local game = stagedGame(1, arena(21, 11), 5, 5) + local ore = Nodes.new(Nodes.ORE) + ore.x, ore.y = 5, 5 + game.level.nodes = { ore } + local before = game.player.essence + ok(game:harvestNode(), "harvesting the ore vein underfoot succeeds") + ok(game.player.essence > before, "harvesting an ore vein raises the player's essence") + eq(ore.charges, Nodes.MAX_CHARGES - 1, "harvesting spends one of the vein's charges") +end + +function tests.gameHarvestNodeFailsWithoutANodeUnderfoot() + local game = stagedGame(1, arena(21, 11), 5, 5) + ok(not game:harvestNode(), "harvesting empty ground fails") +end + +function tests.gameHarvestNodeOnAFallowVeinFails() + local game = stagedGame(1, arena(21, 11), 5, 5) + local ore = Nodes.new(Nodes.ORE) + ore.x, ore.y, ore.charges = 5, 5, 0 + game.level.nodes = { ore } + ok(not game:harvestNode(), "a fallow vein cannot be harvested") +end + +function tests.autopilotHarvestsAnOreVeinWhenIdleAndStandingOnIt() + local game = stagedGame(1, arena(21, 11), 5, 5) + local ore = Nodes.new(Nodes.ORE) + ore.x, ore.y = 5, 5 + game.level.nodes = { ore } + local before = game.player.essence + game:choosePlayerAction() + eq(ore.charges, Nodes.MAX_CHARGES - 1, "the autopilot harvests an unspent vein it stands on") + ok(game.player.essence > before, "the autopilot's harvest actually pays out essence") +end + +function tests.autopilotSkipsAFallowNode() + local game = stagedGame(1, arena(21, 11), 5, 5) + local ore = Nodes.new(Nodes.ORE) + ore.x, ore.y, ore.charges = 5, 5, 0 + game.level.nodes = { ore } + game:choosePlayerAction() + eq(ore.charges, 0, "the autopilot never touches a fallow node") +end + +function tests.buildersNeverPlaceANodeOnTheEntranceFloor() + for seed = 1, 5 do + local level = Builder.build(1, Rng.new(seed), {}) + eq(#level.nodes, 0, "no resource node appears on the entrance floor") + end +end + +function tests.buildersAlwaysPlaceAtLeastOneNodeFromDepthTwoOn() + for seed = 1, 5 do + for depth = 2, 5 do + local level = Builder.build(depth, Rng.new(seed), {}) + ok(#level.nodes >= 1, "every depth-2-and-deeper floor places at least one resource node") + ok(#level.nodes <= 2, "no floor places more than two resource nodes") + for _, node in ipairs(level.nodes) do + ok(node.kind == Nodes.ORE or node.kind == Nodes.GEM, "every placed node is a known kind") + end + end + end +end + +local function corpsePlayer() + return { inventory = {}, statuses = {} } +end + +function tests.corpsesSpawnsForAWildMonsterButNotForUndeadMimicOrBoss() + local level = { corpses = {} } + local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + Corpses.spawn(level, rat) + eq(#level.corpses, 1, "a wild monster leaves a corpse behind") + + local zombie = Monsters.create(Monsters.byKey.zombie, 2, 6, 6, 4) + Corpses.spawn(level, zombie) + eq(#level.corpses, 1, "an undead monster crumbles to dust, leaving no corpse") + + local mimic = Mimics.reveal(3, 7, 7, 4) + Corpses.spawn(level, mimic) + eq(#level.corpses, 1, "a mimic was never alive, so it leaves no corpse") + + local warlord = Bosses.create(Bosses.byKey.warlord, 4, 8, 8) + Corpses.spawn(level, warlord) + eq(#level.corpses, 1, "a boss's own scripted loot drop replaces a corpse") +end + +function tests.corpsesNutritionScalesWithTheFelledSpeciesMaxHpClampedToAFixedBand() + local bat = Monsters.create(Monsters.byKey.bat, 1, 5, 5, 1) + local batCorpse = Corpses.new(bat) + eq(batCorpse.nutrition, Corpses.NUTRITION_MIN, "a frail species clamps to the nutrition floor") + + local troll = Monsters.create(Monsters.byKey.troll, 2, 5, 5, 5) + local trollCorpse = Corpses.new(troll) + eq(trollCorpse.nutrition, troll.maxHp * Corpses.NUTRITION_PER_HP, "a tougher species scales nutrition with maxHp") + + local dragon = Monsters.create(Monsters.byKey.dragon, 3, 5, 5, 6) + local dragonCorpse = Corpses.new(dragon) + eq(dragonCorpse.nutrition, Corpses.NUTRITION_MAX, "an especially tough species clamps to the nutrition ceiling") +end + +function tests.corpsesStepCountsDownAndRotsAwayEntirely() + local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + local level = { corpses = {} } + Corpses.spawn(level, rat) + local corpse = level.corpses[1] + for _ = 1, Corpses.DECAY_TURNS - 1 do Corpses.step(level) end + eq(#level.corpses, 1, "a corpse survives right up to its final round") + eq(corpse.decay, 1, "decay counts down by exactly one each step") + Corpses.step(level) + eq(#level.corpses, 0, "a corpse vanishes outright once its decay reaches zero") +end + +function tests.corpsesGoesRottenPastItsThresholdButStaysButcherableUntilThen() + local rat = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + local corpse = Corpses.new(rat) + ok(not Corpses.isRotten(corpse), "a freshly spawned corpse is not yet rotten") + corpse.decay = Corpses.ROT_TURNS + ok(Corpses.isRotten(corpse), "a corpse right at its rot threshold counts as rotten") + corpse.decay = Corpses.ROT_TURNS + 1 end local order = {