diff --git a/tests/run.lua b/tests/run.lua index 3ceb356..bff8297 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -1665,6 +1665,179 @@ function tests.summonRespectsPopulationCap() local minionB = Monsters.create(Monsters.byKey.zombie, 3, 6, 7, 4) minionA.summonedBy = necro.id minionB.summonedBy = necro.id + game.level.entities = { necro, minionA, minionB } + local acted = Abilities.tryAct(game, necro) + eq(acted, false, "a necromancer already at its summon cap does not summon a third") + eq(#game.level.entities, 3, "the level still has exactly the two existing minions") +end + +function tests.curseAtRangeHurtsAndWeakens() + local game = stagedGame(1, arena(21, 11), 3, 6) + local shaman = Monsters.create(Monsters.byKey.shaman, 1, 7, 6, 4) + game.level.entities = { shaman } + local hpBefore = game.player.hp + local acted = Abilities.tryAct(game, shaman) + eq(acted, true, "a shaman with a clear shot curses instead of moving") + eq(game.player.hp, hpBefore - 1, "the curse bolt runs through Combat.damage (shaman attack 2 minus the player's base defense 1)") + eq(Status.has(game.player, Status.WEAKEN), true, "the curse also weakens the player") + eq(shaman.cooldown, 4, "casting the curse starts its cooldown") + ok(contains(game.log:last(), "curses you for 1"), "the logged amount matches the mitigated damage, not a flat number") +end + +-- The curse bolt used to bypass Combat.damage entirely (a flat, unmitigated +-- subtraction from hp), so a shaman hit for the same 3 no matter how armored +-- the player was, never broke a melee combo, and never wore armor - every +-- other monster-on-player attack (monsterMelee, monsterShoot, mimic/grave +-- ambush) already ran through Combat.damage + Momentum.onDamaged + +-- Durability.wearArmor. These three lock in the fix. +function tests.curseDamageIsMitigatedByThePlayersDefenseLikeAnyOtherAttack() + local game = stagedGame(1, arena(21, 11), 3, 6) + local shaman = Monsters.create(Monsters.byKey.shaman, 1, 7, 6, 4) + game.level.entities = { shaman } + game.player.defense = game.player.defense + 10 + local hpBefore = game.player.hp + Abilities.tryAct(game, shaman) + eq(game.player.hp, hpBefore - 1, "a shaman's attack (2) minus a heavily-defended player floors at Combat.damage's minimum of 1, not a flat 3") +end + +function tests.curseBreaksThePlayersMeleeMomentumStack() + local game = stagedGame(1, arena(21, 11), 3, 6) + local shaman = Monsters.create(Monsters.byKey.shaman, 1, 7, 6, 4) + game.level.entities = { shaman } + game.player.combo = 4 + Abilities.tryAct(game, shaman) + eq(Momentum.stacks(game.player), 0, "a landed curse bolt snaps momentum to zero exactly like a melee hit or a breath attack") +end + +function tests.curseWearsThePlayersEquippedArmor() + local game = stagedGame(1, arena(21, 11), 3, 6) + local shaman = Monsters.create(Monsters.byKey.shaman, 1, 7, 6, 4) + game.level.entities = { shaman } + game.player.armor = { name = "chainmail", kind = Items.ARMOR, power = 2, defense = 2 } + Durability.ensure(game.player.armor) + local durabilityBefore = game.player.armor.durability + Abilities.tryAct(game, shaman) + eq(game.player.armor.durability, durabilityBefore - 1, "the curse wears equipped armor exactly like a landed melee or breath hit does") +end + +function tests.curseWithheldWhenAdjacent() + local game = stagedGame(1, arena(21, 11), 3, 6) + local shaman = Monsters.create(Monsters.byKey.shaman, 1, 4, 6, 4) + game.level.entities = { shaman } + local acted = Abilities.tryAct(game, shaman) + eq(acted, false, "a shaman standing next to the player withholds its curse") +end + +function tests.shriekAtRangeBlindsThePlayer() + local game = stagedGame(1, arena(21, 11), 3, 6) + local banshee = Monsters.create(Monsters.byKey.banshee, 1, 7, 6, 5) + game.level.entities = { banshee } + eq(Status.has(game.player, Status.BLIND), false, "the player starts able to see") + local acted = Abilities.tryAct(game, banshee) + eq(acted, true, "a banshee with a clear shot shrieks instead of moving") + eq(Status.has(game.player, Status.BLIND), true, "the shriek blinds the player") + eq(banshee.cooldown, 6, "shrieking starts its cooldown") + ok(contains(game.log:last(), "blinds you"), "the shriek is logged") +end + +function tests.shriekWithheldWhenAdjacent() + local game = stagedGame(1, arena(21, 11), 3, 6) + local banshee = Monsters.create(Monsters.byKey.banshee, 1, 4, 6, 5) + game.level.entities = { banshee } + local acted = Abilities.tryAct(game, banshee) + eq(acted, false, "a banshee standing next to the player withholds its shriek") + eq(Status.has(game.player, Status.BLIND), false, "no shriek means no blindness") +end + +function tests.shriekIsUnavailableOutOfRange() + local game = stagedGame(1, arena(21, 11), 3, 6) + local banshee = Monsters.create(Monsters.byKey.banshee, 1, 20, 6, 5) + game.level.entities = { banshee } + local acted = Abilities.tryAct(game, banshee) + eq(acted, false, "a banshee out of shriek range falls through to its base ai") + eq(Status.has(game.player, Status.BLIND), false, "no shriek means no blindness") +end + +function tests.shriekBlindnessReducesTheVictimsEffectiveSightRadius() + local game = stagedGame(1, arena(21, 11), 3, 6) + local banshee = Monsters.create(Monsters.byKey.banshee, 1, 7, 6, 5) + game.level.entities = { banshee } + Abilities.tryAct(game, banshee) + eq(game:effectiveRadius(), 1, "blind (also inflicted by a banshee) still narrows sight to game.lua's own radius") +end + +function tests.abilityCooldownBlocksImmediateReuse() + local game = stagedGame(1, arena(21, 11), 3, 6) + local shaman = Monsters.create(Monsters.byKey.shaman, 1, 7, 6, 4) + game.level.entities = { shaman } + Abilities.tryAct(game, shaman) + local hpAfterFirst = game.player.hp + local acted = Abilities.tryAct(game, shaman) + eq(acted, false, "an ability on cooldown does not fire again") + eq(game.player.hp, hpAfterFirst, "no further damage is dealt while on cooldown") + for _ = 1, 4 do Abilities.tickCooldown(shaman) end + eq(shaman.cooldown, 0, "four ticks fully drains a four-turn cooldown") + local actedAgain = Abilities.tryAct(game, shaman) + eq(actedAgain, true, "the ability is usable again once its cooldown has ticked down") +end + +function tests.healerMendsNeediestAlly() + local game = stagedGame(1, arena(21, 11), 3, 6) + local cleric = Monsters.create(Monsters.byKey.cleric, 1, 6, 6, 5) + local hurt = Monsters.create(Monsters.byKey.rat, 2, 8, 6, 5) + hurt.hp = 1 + game.level.entities = { cleric, hurt } + local acted = Abilities.tryAct(game, cleric) + eq(acted, true, "a cleric with a hurt ally in sight heals instead of moving") + eq(hurt.hp, 7, "healing restores the fixed amount, capped at max hp") + eq(cleric.cooldown, 5, "healing starts its cooldown") +end + +function tests.healerSkipsWhenAlliesAreHealthy() + local game = stagedGame(1, arena(21, 11), 3, 6) + local cleric = Monsters.create(Monsters.byKey.cleric, 1, 6, 6, 5) + local ally = Monsters.create(Monsters.byKey.rat, 2, 8, 6, 5) + game.level.entities = { cleric, ally } + local acted = Abilities.tryAct(game, cleric) + eq(acted, false, "a cleric with no hurt allies falls through to its base ai") +end + +function tests.rallyBuffsAdjacentAllies() + local game = stagedGame(1, arena(21, 11), 3, 6) + local chieftain = Monsters.create(Monsters.byKey.chieftain, 1, 10, 6, 6) + local grunt = Monsters.create(Monsters.byKey.goblin, 2, 11, 6, 6) + game.level.entities = { chieftain, grunt } + eq(Status.has(grunt, Status.STRENGTH), false, "the grunt starts unbuffed") + local acted = Abilities.tryAct(game, chieftain) + eq(acted, true, "a chieftain next to an unbuffed ally rallies instead of moving") + eq(Status.has(grunt, Status.STRENGTH), true, "rallying buffs the adjacent ally") + eq(Status.has(chieftain, Status.STRENGTH), true, "rallying also buffs the chieftain itself") + eq(chieftain.cooldown, 8, "rallying starts its cooldown") +end + +function tests.rallySkipsWhenNoUnbuffedAlliesAdjacent() + local game = stagedGame(1, arena(21, 11), 3, 6) + local chieftain = Monsters.create(Monsters.byKey.chieftain, 1, 10, 6, 6) + game.level.entities = { chieftain } + local acted = Abilities.tryAct(game, chieftain) + eq(acted, false, "a chieftain with no adjacent allies falls through to its base ai") +end + +function tests.healerNeverMendsThePlayersOwnCompanionHirelingOrThrall() + local game = stagedGame(1, arena(21, 11), 3, 6) + local cleric = Monsters.create(Monsters.byKey.cleric, 1, 6, 6, 5) + local companion = Companions.summon(game, "hound") + companion.x, companion.y = 8, 6 + companion.hp = 1 + game.level.entities = { cleric, companion } + local acted = Abilities.tryAct(game, cleric) + eq(acted, false, "a hurt companion is not a valid heal target, so the cleric falls through instead") + eq(companion.hp, 1, "the player's own companion is left exactly as hurt as it was") +end + +function tests.rallyNeverBuffsThePlayersOwnCompanionHirelingOrThrall() + local game = stagedGame(1, arena(21, 11), 3, 6) + local chieftain = Monsters.create(Monsters.byKey.chieftain, 1, 10, 6, 6) end function tests.saveLoadRoundTrip()