diff --git a/tests/run.lua b/tests/run.lua index 260babd..ee27200 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -4454,6 +4454,179 @@ function tests.shopStockCanRollAContractWithAPositiveCatalogPrice() local sawContract = false for seed = 1, 40 do for _, item in ipairs(Shop.stock(Rng.new(seed))) do + if item.kind == Items.CONTRACT then + sawContract = true + ok(Shop.priceOf(item) > 0, "a rolled contract carries a positive catalog price") + ok(Mercenaries.byKey[item.hireling] ~= nil, "a rolled contract names a real roster key") + end + end + end + ok(sawContract, "shop stock can roll a mercenary contract across a handful of seeds") +end + +function tests.autopilotHiresACarriedContractWhenNoHirelingIsYetUnderContract() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.entities = {} + game.player.inventory = { Items.contract("skirmisher") } + game:choosePlayerAction() + eq(#game.player.inventory, 0, "the autopilot uses a carried contract on sight when no hireling is yet under contract") + local hired = false + for _, e in ipairs(game.level.entities) do + if e.kind == "hireling" then hired = true end + end + ok(hired, "the autopilot's contract use actually hires the mercenary") +end + +function tests.artifactsCreateBuildsTheCatalogedItemShape() + local blade = Artifacts.create("bloodfang") + eq(blade.kind, Items.WEAPON, "bloodfang is a weapon-slot artifact") + eq(blade.unique, Artifacts.LIFESTEAL, "bloodfang carries the lifesteal tag") + eq(blade.artifact, "bloodfang", "the item remembers its catalog key") + local ring = Artifacts.create("ring_of_the_tide") + eq(ring.kind, Items.RING, "ring of the tide is a ring-slot artifact") + eq(ring.unique, Artifacts.REGEN, "ring of the tide carries the regen tag") + eq(Artifacts.create("nonexistent"), nil, "an unknown catalog key builds nothing") +end + +function tests.lifestealWeaponHealsThePlayerOnAHit() + local game = stagedGame(1, arena(21, 11), 3, 6) + Items.equipWeapon(Artifacts.create("bloodfang"), game.player) + game.player.hp = game.player.maxHp - 10 + local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + orc.hp, orc.maxHp = 30, 30 + game.level.entities = { orc } + local before = game.player.hp + game:playerAttack(orc) + ok(game.player.hp > before, "bloodfang heals the player for a share of the damage it deals") +end + +function tests.thornedArmorReflectsDamageBackAtAMeleeAttacker() + local game = stagedGame(1, arena(21, 11), 3, 6) + Items.equipArmor(Artifacts.create("aegis_of_embers"), game.player) + local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + game.level.entities = { orc } + local before = orc.hp + game:monsterMelee(orc) + ok(orc.hp < before, "aegis of embers burns the monster that hits its wearer") +end + +function tests.thornedArmorCanKillAWeakAttacker() + local game = stagedGame(1, arena(21, 11), 3, 6) + Items.equipArmor(Artifacts.create("aegis_of_embers"), game.player) + local rat = Monsters.create(Monsters.byKey.rat, 1, 4, 6, 1) + rat.hp, rat.maxHp = 1, 1 + game.level.entities = { rat } + local xpBefore = game.player.xp + game:monsterMelee(rat) + ok(not rat:isAlive(), "a thorn burn can finish off a weak attacker") + ok(game.player.xp > xpBefore, "the player is credited xp for a thorn kill") +end + +-- Aegis of Embers is fire damage by name ("Embers") and by its own log line +-- ("your armor sears") - before this fix the reflect subtracted a raw amount +-- straight off the attacker's hp with no Resistances.apply call at all, so a +-- fire-immune dragon (bestiary-real, reachable from depth 6) burned through +-- its own immunity attacking a thorned wearer. Same shape of bug +-- fighterCleaveSplashRespectsAFireImmuneTargetLikeThePrimaryHitAlreadyDoes +-- caught in skills.lua earlier the same day, this time in the thorns path. +function tests.thornedArmorRespectsAFireImmuneAttacker() + local game = stagedGame(1, arena(21, 11), 3, 6) + Items.equipArmor(Artifacts.create("aegis_of_embers"), game.player) + local dragon = Monsters.create(Monsters.byKey.dragon, 1, 4, 6, 1) + game.level.entities = { dragon } + local before = dragon.hp + game:monsterMelee(dragon) + eq(dragon.hp, before, "a fire-immune dragon takes zero thorns reflect from a fire-flavoured artifact") +end + +function tests.thornedArmorIsHalvedAgainstAFireResistantAttacker() + local function reflectDealt(resistant) + local game = stagedGame(1, arena(21, 11), 3, 6) + Items.equipArmor(Artifacts.create("aegis_of_embers"), game.player) + local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + orc.hp, orc.maxHp = 100, 100 + if resistant then orc.resist = { fire = Resistances.RESIST } end + game.level.entities = { orc } + game:monsterMelee(orc) + return 100 - orc.hp + end + local plain = reflectDealt(false) + local resisted = reflectDealt(true) + ok(resisted < plain, "a fire-resistant attacker takes measurably less thorns reflect than an unresisted one") + eq(resisted, math.floor(plain / 2), "fire resistance halves the thorns reflect, the same mitigation Resistances.apply gives every other fire source") +end + +function tests.ringOfTheTideRegeneratesHpEachTurn() + local game = Game.new(1) + Items.equipRing(Artifacts.create("ring_of_the_tide"), game.player) + game.player.hp = game.player.maxHp - 10 + local before = game.player.hp + game:hungerTorchTick() + eq(game.player.hp, before + 2, "ring of the tide regenerates its fixed amount each turn") +end + +function tests.piercingBowIgnoresPartOfTargetDefense() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.baseAttack = 5 + Items.equipBow(Artifacts.create("gale_bow"), game.player) + game.player.inventory[#game.player.inventory + 1] = Items.ammo("arrow", 5) + local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + orc.defense, orc.baseDefense, orc.hp, orc.maxHp = 6, 6, 40, 40 + game.level.entities = { orc } + game:fireAt(orc) + eq(orc.hp, 35, "a piercing bow's shot ignores a flat share of the target's defense") +end + +function tests.builderOccasionallyPlacesAnArtifactFromDepthFive() + local level = Builder.build(5, Rng.new(4), {}) + local found + for _, ground in ipairs(level.floorItems) do + if ground.item.artifact then found = ground.item end + end + ok(found ~= nil, "this seed's depth-5 floor places a named artifact") + ok(Artifacts.byKey[found.artifact] ~= nil, "the artifact names a real catalog entry") +end + +function tests.artifactsStayOffFloorsAboveDepthFive() + for seed = 1, 4 do + for depth = 1, 4 do + local level = Builder.build(depth, Rng.new(seed), {}) + for _, ground in ipairs(level.floorItems) do + ok(not ground.item.artifact, "no artifact appears above depth 5") + end + end + end +end + +function tests.artifactFieldsSurviveSerialization() + local game = Game.new(3) + Items.equipWeapon(Artifacts.create("bloodfang"), game.player) + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.player.weapon.artifact, "bloodfang", "an equipped artifact's catalog key survives a save/load round trip") + eq(restored.player.weapon.unique, Artifacts.LIFESTEAL, "an equipped artifact's unique tag survives a save/load round trip") + eq(restored.player.weapon.value, 0.5, "an equipped artifact's tuning value survives a save/load round trip") +end + +function tests.useItemOnAKitPlantsAHiddenTrapOnThePlayersTile() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.player.inventory = { Items.kit("dart") } + ok(game:useItem(1), "using a trap kit succeeds") + eq(#game.player.inventory, 0, "the kit leaves the inventory once planted") + local trap = game.level:trapAt(5, 5) + ok(trap ~= nil, "a trap is planted on the player's tile") + eq(trap.effect, "dart", "the planted trap matches the kit's effect") + eq(trap.revealed, false, "a freshly planted trap starts hidden") + eq(game.level.map:get(5, 5), Tiles.FLOOR, "a hidden trap does not change the tile's glyph") +end + +function tests.plantingATrapWhereOneAlreadyExistsFails() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.level.traps = { { x = 5, y = 5, effect = "poison", revealed = false } } + game.player.inventory = { Items.kit("dart") } + ok(not game:useItem(1), "planting a trap on an occupied tile fails") end local order = {