diff --git a/tests/run.lua b/tests/run.lua index 7aec43c..142cddb 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -7568,6 +7568,179 @@ function tests.renderPanelShowsComboOnlyOnceItIsBuilt() ok(contains(Render.panel(game), "combo x3"), "a built-up combo is reported in the status panel") end +function tests.momentumFieldSurvivesSerialization() + local game = Game.new(3) + game.player.combo = 4 + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.player.combo, 4, "the player's momentum stack survives a save/load round trip") +end + +function tests.conductivityIsWetReadsTheEntitysOwnWetField() + eq(Conductivity.isWet({}), false, "an entity with no wet field at all reads as dry") + eq(Conductivity.isWet({ wet = 0 }), false, "a wet counter already run down to zero reads as dry") + eq(Conductivity.isWet({ wet = 1 }), true, "any positive wet counter reads as wet") +end + +function tests.conductivityTickSoaksOnWaterAndDecaysOneStepAtATimeOffIt() + local level = arena(21, 11) + level.map:set(6, 6, Tiles.WATER) + local entity = { x = 6, y = 6 } + Conductivity.tick(level, entity) + eq(entity.wet, Conductivity.SOAK_DURATION, "standing on water soaks an entity to the full duration") + entity.x = 7 + for i = Conductivity.SOAK_DURATION - 1, 0, -1 do + Conductivity.tick(level, entity) + eq(entity.wet, i, "the soak counter decays by exactly one per turn spent off water") + end + eq(Conductivity.isWet(entity), false, "the counter bottoms out at zero rather than going negative") +end + +function tests.conductivityTickReSoaksImmediatelyOnSteppingBackOntoWater() + local level = arena(21, 11) + level.map:set(6, 6, Tiles.WATER) + local entity = { x = 6, y = 6, wet = 1 } + Conductivity.tick(level, entity) + eq(entity.wet, Conductivity.SOAK_DURATION, + "stepping back onto water refreshes the counter to full rather than adding to it") +end + +function tests.conductivityApplyFireDampensDamageOnlyOnceSoaked() + eq(Conductivity.applyFire({}, 10), 10, "fire damage against a dry entity is untouched") + eq(Conductivity.applyFire({ wet = 2 }, 10), 5, + "fire damage against a soaked entity is halved on top of its resistance tier") +end + +function tests.conductivityApplyLightningBonusesDamageOnlyOnceSoaked() + eq(Conductivity.applyLightning({}, 10), 10, "lightning damage against a dry entity is untouched") + eq(Conductivity.applyLightning({ wet = 2 }, 10), 15, + "lightning damage against a soaked entity gets a 50 percent conductivity bonus") +end + +function tests.conductivityNeverConjuresDamageOutOfAnImmuneResistance() + eq(Conductivity.applyFire({ wet = 2, resist = { fire = Resistances.IMMUNE } }, 10), 0, + "fire immunity still floors damage at zero even while soaked") + eq(Conductivity.applyLightning({ wet = 2, resist = { lightning = Resistances.IMMUNE } }, 10), 0, + "lightning immunity still floors damage at zero even while soaked") +end + +function tests.conductivityResistsIgnitionOnlyWhileWet() + eq(Conductivity.resistsIgnition({}), false, "a dry entity does not resist ignition") + eq(Conductivity.resistsIgnition({ wet = 3 }), true, "a soaked entity resists catching fire") +end + +function tests.statusApplyRefusesBurningOnlyWhileSoakedButOtherEffectsStillApply() + local entity = Entity.new({ x = 1, y = 1, glyph = "@", name = "you", kind = "player", hp = 20, attack = 6, defense = 2 }) + entity.wet = 2 + local result = Status.apply(entity, Status.BURNING, 3, 2) + eq(result, nil, "applying burning to a soaked entity is refused outright") + eq(Status.has(entity, Status.BURNING), false, "a refused burning application leaves no trace on the entity") + Status.apply(entity, Status.POISON, 3, 2) + eq(Status.has(entity, Status.POISON), true, "a soaked entity still catches every other status effect normally") + entity.wet = 0 + Status.apply(entity, Status.BURNING, 3, 2) + eq(Status.has(entity, Status.BURNING), true, "once dry again, the same entity catches fire normally") +end + +function tests.statusRemoveStripsANamedStatusRegardlessOfItsRemainingDuration() + local entity = Entity.new({ x = 1, y = 1, glyph = "@", name = "you", kind = "player", hp = 20, attack = 6, defense = 2 }) + Status.apply(entity, Status.POISON, 99, 2) + eq(Status.remove(entity, Status.POISON), true, "removing a status that is present reports success") + eq(Status.has(entity, Status.POISON), false, "the removed status no longer reads as active") + eq(Status.remove(entity, Status.POISON), false, "removing an already-absent status reports nothing to do") + eq(Status.remove(entity, Status.CONFUSE), false, "removing a status that was never applied at all also reports nothing to do") +end + +function tests.statusCureHostileLiftsEveryDebuffButLeavesBuffsUntouched() + local entity = Entity.new({ x = 1, y = 1, glyph = "@", name = "you", kind = "player", hp = 20, attack = 6, defense = 2 }) + Status.apply(entity, Status.POISON, 5, 2) + Status.apply(entity, Status.CONFUSE, 6, 1) + Status.apply(entity, Status.STUN, 2, 1) + Status.apply(entity, Status.STRENGTH, 30, 3) + Status.apply(entity, Status.SHIELD, 15, 1) + local lifted = Status.cureHostile(entity) + eq(#lifted, 3, "cureHostile reports exactly the three debuffs it lifted") + eq(Status.has(entity, Status.POISON), false, "poison is gone after cureHostile") + eq(Status.has(entity, Status.CONFUSE), false, "confuse is gone after cureHostile") + eq(Status.has(entity, Status.STUN), false, "stun is gone after cureHostile") + ok(Status.has(entity, Status.STRENGTH), "a buff like strength survives cureHostile untouched") + ok(Status.has(entity, Status.SHIELD), "a buff like shield survives cureHostile untouched") + eq(#Status.cureHostile(entity), 0, "calling cureHostile again with nothing hostile left returns an empty list") +end + +function tests.hazardsAfflictDampensFireAndBlocksIgnitionForASoakedVictim() + local level = arena(21, 11) + Hazards.ignite(level, 5, 5, 3, 4) + local victim = Monsters.create(Monsters.byKey.rat, 1, 5, 5, 1) + victim.wet = 2 + local hpBefore = victim.hp + local line = Hazards.afflict(level, victim) + eq(victim.hp, hpBefore - 2, "standing in fire while soaked only deals half the usual potency") + ok(contains(line, "soaked"), "the afflict message calls out the victim's own soaked skin") + eq(Status.has(victim, Status.BURNING), false, "a soaked victim standing in fire never actually catches alight") +end + +function tests.gameConductivityTicksAMonsterEachTurnBasedOnTheTileUnderfoot() + local level = arena(21, 11) + level.map:set(6, 6, Tiles.WATER) + local game = stagedGame(1, level, 3, 6) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 6, 1) + game.level.entities = { rat } + game:takeMonsterTurn(rat) + eq(rat.wet, Conductivity.SOAK_DURATION, "a monster ending its turn on water is soaked to the full duration") + rat.x, rat.y = 7, 6 + game:takeMonsterTurn(rat) + eq(rat.wet, Conductivity.SOAK_DURATION - 1, "moving off water the following turn lets the soak counter decay by one") +end + +function tests.gameConductivityTicksThePlayerEachAutoStepBasedOnTheTileUnderfoot() + local level = arena(21, 11) + level.map:set(3, 6, Tiles.WATER) + local game = stagedGame(1, level, 3, 6) + game:autoStep() + eq(game.player.wet, Conductivity.SOAK_DURATION, "the player ending a turn on water is soaked to the full duration") +end + +function tests.spellShockBoltDealsMoreDamageToASoakedTargetThanADryOne() + local dryGame = stagedGame(1, arena(21, 11), 3, 6) + Spells.learn(dryGame.player, "shock") + local dry = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + dry.hp, dry.maxHp = 100, 100 + dryGame.level.entities = { dry } + local castOk = dryGame:castSpell("shock", dry) + ok(castOk, "casting a learned, in-range shock bolt succeeds") + local dryDealt = 100 - dry.hp + ok(dryDealt > 0, "shock bolt deals real lightning damage to a dry target") + + local wetGame = stagedGame(1, arena(21, 11), 3, 6) + Spells.learn(wetGame.player, "shock") + local soaked = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + soaked.hp, soaked.maxHp = 100, 100 + soaked.wet = 2 + wetGame.level.entities = { soaked } + wetGame:castSpell("shock", soaked) + local wetDealt = 100 - soaked.hp + eq(wetDealt, dryDealt + dryDealt // 2, + "shock bolt deals exactly its 50 percent conductivity bonus against a soaked target") + ok(contains(wetGame.log:last(), "water clinging"), + "casting shock bolt on a soaked target reports the conductivity through water") +end + +function tests.renderPanelShowsWetOnlyWhileSoaked() + local game = stagedGame(1, arena(21, 11), 3, 6) + ok(not contains(Render.panel(game), "wet"), "a fresh dry player shows no wet line") + game.player.wet = 2 + ok(contains(Render.panel(game), "wet (2)"), "a soaked player's status panel reports the remaining soak turns") +end + +function tests.builderCanRollATomeOfTheShockBoltSpell() + -- Range widened from 100 to 200: adding "cleanse" to Builder's `learnable` + -- list shifted rng:range(1, #learnable)'s result for effectively every + -- seed, and the nearest shock tome under the new list happens to sit at + -- seed 174 - still well within "a reasonable seed range". + local found end local order = {