diff --git a/tests/run.lua b/tests/run.lua index 8084e1e..0d06528 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -2897,6 +2897,179 @@ function tests.gameCraftingWrappersDriveTheFullPipeline() end function tests.monsterKillsGrantEssenceScaledToTheirXpReward() + local game = stagedGame(1, arena(21, 11), 3, 6) + local rat = Monsters.create(Monsters.byKey.rat, 1, 4, 6, 1) + rat.hp = 1 + game.level.entities = { rat } + local essenceBefore = game.player.essence + game:playerAttack(rat) + eq(game.player.essence, essenceBefore + Crafting.essenceFor(Monsters.xpFor(rat)), + "a kill grants essence scaled to the monster's xp reward") +end + +function tests.craftingFieldsSurviveSerialization() + local game = Game.new(5) + local weapon = Items.weapon("dagger", 2) + Items.equipWeapon(weapon, game.player) + Crafting.socket(game.player, weapon, Crafting.gem(Crafting.VENOM, 2)) + game.player.essence = 37 + game.player.inventory[#game.player.inventory + 1] = Crafting.gem(Crafting.SHOCK, 1) + + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + + eq(restored.player.essence, 37, "essence survives a save/load round trip") + eq(restored.player.weapon.socket.element, Crafting.VENOM, "a socketed gem's element survives a save/load round trip") + eq(restored.player.weapon.socket.tier, 2, "a socketed gem's tier survives a save/load round trip") + local restoredGem + for _, item in ipairs(restored.player.inventory) do + if item.kind == Items.GEM then restoredGem = item end + end + ok(restoredGem ~= nil, "a loose gem in the inventory survives a save/load round trip") + eq(restoredGem.element, Crafting.SHOCK, "the loose gem's element survives a save/load round trip") +end + +function tests.alchemyBrewResolvesAKnownPairToItsFixedPotion() + local potion = Alchemy.brew(Alchemy.SUNPETAL, Alchemy.SUNPETAL) + eq(potion.effect, "heal", "a matched pair of the same herb brews that herb's aspect") + eq(potion.power, 18, "a doubled herb brews a stronger potion than a mixed pair") +end + +function tests.alchemyBrewIsOrderIndependent() + local forward = Alchemy.brew(Alchemy.BLOODROOT, Alchemy.NIGHTCAP) + local backward = Alchemy.brew(Alchemy.NIGHTCAP, Alchemy.BLOODROOT) + eq(forward.effect, backward.effect, "brewing a pair does not depend on which herb is named first") + eq(forward.power, backward.power, "a brewed potion's power does not depend on herb order either") +end + +function tests.alchemyBrewCurdlesOnAnOpposedPairing() + local potion, reason = Alchemy.brew(Alchemy.SUNPETAL, Alchemy.NIGHTCAP) + eq(potion, nil, "light and shade herbs curdle rather than brewing a potion") + eq(reason, "curdle", "a curdled brew names its failure") +end + +function tests.alchemyLookupRejectsAnUnknownHerb() + eq(Alchemy.lookup("mud", Alchemy.SUNPETAL), nil, "a nonexistent herb key has no recipe") +end + +function tests.gameBrewAtCombinesTwoHerbsIntoAPotionAtACauldron() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.cauldrons = { { x = 5, y = 5 } } + game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL), Alchemy.herb(Alchemy.BLOODROOT) } + ok(game:brewAt(1, 2), "brewing two herbs at a cauldron underfoot succeeds") + eq(#game.player.inventory, 1, "both source herbs are spent and one potion is added") + local brewed = game.player.inventory[1] + eq(brewed.kind, Items.POTION, "brewing yields an ordinary catalog potion") + eq(brewed.effect, "heal", "the brewed potion matches the herb pair's fixed recipe") + eq(brewed.power, 10, "the brewed potion's power matches the recipe, not the catalog default") + ok(Identify.isKnown(game.identify, brewed), "brewing a potion yourself identifies its type for free") +end + +function tests.gameBrewAtFailsWithoutACauldronUnderfoot() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL), Alchemy.herb(Alchemy.BLOODROOT) } + ok(not game:brewAt(1, 2), "brewing away from any cauldron fails") + eq(#game.player.inventory, 2, "a failed brew for lack of a cauldron spends nothing") +end + +function tests.gameBrewAtRequiresTwoHerbItems() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.cauldrons = { { x = 5, y = 5 } } + game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL) } + ok(not game:brewAt(1, 1), "brewing a herb against itself by index is rejected") + game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL), Items.potion(10) } + ok(not game:brewAt(1, 2), "a non-herb item cannot stand in for the second half of a brew") + eq(#game.player.inventory, 2, "a rejected brew attempt spends nothing") +end + +function tests.gameBrewAtCurdlesIntoGasInsteadOfAPotion() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.cauldrons = { { x = 5, y = 5 } } + game.player.inventory = { Alchemy.herb(Alchemy.SUNPETAL), Alchemy.herb(Alchemy.NIGHTCAP) } + ok(not game:brewAt(1, 2), "a curdled brew reports failure") + eq(#game.player.inventory, 0, "a curdled brew still spends both herbs") + ok(Hazards.at(game.level, 5, 5, Hazards.GAS) ~= nil, "a curdled brew vents a gas hazard at the brewer's feet") + ok(contains(game.log:recent(1)[1] or "", "curdle"), "the curdle is logged") +end + +function tests.buildersScatterHerbsFromTheFirstFloor() + local level = Builder.build(1, Rng.new(1), {}) + local herbs = 0 + for _, ground in ipairs(level.floorItems) do + if ground.item.kind == Items.HERB then herbs = herbs + 1 end + end + ok(herbs >= 2, "the entrance floor already scatters foraging herbs") +end + +function tests.cauldronsStayOffTheEntranceFloor() + for seed = 1, 5 do + local level = Builder.build(1, Rng.new(seed), {}) + eq(#level.cauldrons, 0, "no cauldron appears on the entrance floor") + end +end + +function tests.buildersOccasionallyPlaceACauldronFromDepthTwo() + local level = Builder.build(2, Rng.new(14), {}) + eq(#level.cauldrons, 1, "this seed's depth-2 floor places a cauldron") +end + +function tests.cauldronFieldsSurviveSerialization() + local level = arena(21, 11) + level.cauldrons = { { x = 4, y = 4 } } + local restored = Level.fromData(level:toData()) + eq(#restored.cauldrons, 1, "the cauldron list survives a round trip") + eq(restored.cauldrons[1].x, 4, "a cauldron's position survives a round trip") +end + +function tests.cauldronRendersItsGlyphWhenVisible() + local rendered = stagedGame(1, arena(21, 11), 3, 6) + rendered.level.cauldrons = { { x = 5, y = 6 } } + rendered:computeVisible() + ok(contains(rendered:render(), "U"), "a cauldron on a visible cell renders as its glyph") +end + +local function nodePlayer() + return { essence = 0, inventory = {} } +end + +local function nodeGame(seed) + local game = { player = nodePlayer(), rng = Rng.new(seed), level = { event = nil }, ascension = {} } + function game:essenceMultiplier() + return Events.essenceMultiplier(self.level.event) * Ascension.essenceMultiplier(self.ascension) + end + return game +end + +function tests.nodesHarvestingAnOreVeinPaysEssenceAndSpendsACharge() + local node = Nodes.new(Nodes.ORE) + local game = nodeGame(3) + local success, line = Nodes.harvest(game, node) + ok(success, "harvesting a fresh ore vein succeeds") + eq(node.charges, Nodes.MAX_CHARGES - 1, "a harvest spends exactly one charge") + ok(game.player.essence >= Nodes.ORE_YIELD_MIN and game.player.essence <= Nodes.ORE_YIELD_MAX, + "the essence paid out falls within the vein's yield range") + ok(contains(line, "essence"), "the harvest is logged") +end + +function tests.nodesHarvestingAnOreVeinDoublesItsYieldDuringRichVeins() + local node = Nodes.new(Nodes.ORE) + local game = nodeGame(3) + game.level.event = { key = Events.RICHVEINS, turnsLeft = 5 } + Nodes.harvest(game, node) + ok(game.player.essence >= Nodes.ORE_YIELD_MIN * 2 and game.player.essence <= Nodes.ORE_YIELD_MAX * 2, + "rich veins doubles the vein's ordinary yield range") +end + +function tests.nodesHarvestingAGemSeamYieldsARawTierOneGemOfItsElement() + local node = Nodes.new(Nodes.GEM, Crafting.FIRE) + local game = nodeGame(3) + local success = Nodes.harvest(game, node) + ok(success, "harvesting a fresh gem seam succeeds") + 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") end local order = {