test: cover tests.fighterBerserkBoostsAttackAtLowHp

This commit is contained in:
dualfroz committed 2026-09-08 21:32:23 +02:00
1 parent f5f4e93cd3
commit 1a36a061ba
1 file changed
+173
+173
View File
@@ -2378,6 +2378,179 @@ function tests.fighterSecondWindRestoresPortionOfMaxHp()
game.player.skillCooldowns.secondWind = 0 game.player.skillCooldowns.secondWind = 0
game.player.hp = game.player.maxHp - 1 game.player.hp = game.player.maxHp - 1
game:useSkill("secondWind")
eq(game.player.hp, game.player.maxHp, "second wind never heals past max hp")
end
function tests.fighterBerserkBoostsAttackAtLowHp()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "fighter"
game.player.skills.berserk = true
local full = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
game.level.entities = { full }
game.player.hp = game.player.maxHp
game:playerAttack(full)
local dmgFull = full.maxHp - full.hp
local game2 = stagedGame(1, arena(21, 11), 3, 6)
game2.player.class = "fighter"
game2.player.skills.berserk = true
local low = Monsters.create(Monsters.byKey.orc, 2, 4, 6, 1)
game2.level.entities = { low }
game2.player.hp = math.floor(game2.player.maxHp * 0.3)
game2:playerAttack(low)
local dmgLow = low.maxHp - low.hp
ok(dmgLow > dmgFull, "berserk raises attack once hp drops to a third of max")
end
function tests.rogueBackstabDoublesDamageOnFullHpTarget()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "rogue"
game.player.skills.backstab = true
local fresh = Monsters.create(Monsters.byKey.rat, 1, 4, 6, 1)
game.level.entities = { fresh }
game:playerAttack(fresh)
local dmgFresh = fresh.maxHp - fresh.hp
local game2 = stagedGame(1, arena(21, 11), 3, 6)
game2.player.class = "rogue"
game2.player.skills.backstab = true
local hurt = Monsters.create(Monsters.byKey.rat, 2, 4, 6, 1)
hurt.hp = hurt.hp - 1
game2.level.entities = { hurt }
game2:playerAttack(hurt)
local dmgHurt = (hurt.maxHp - 1) - hurt.hp
eq(dmgFresh, dmgHurt * 2, "backstab doubles damage against a target still at full hp")
end
function tests.rogueEvasionCanFullyNegateMeleeHit()
local defender = { skills = { evasion = true } }
local sawDodge, sawHit = false, false
local rng = Rng.new(1)
for _ = 1, 60 do
if Skills.evades({ rng = rng }, defender) then sawDodge = true else sawHit = true end
end
ok(sawDodge, "evasion dodges at least once over enough attempts")
ok(sawHit, "evasion is a chance, not a guarantee, over enough attempts")
eq(Skills.evades({ rng = Rng.new(1) }, {}), false, "a defender without the skill never evades")
end
function tests.poisonBladeAppliesPoisonOnNextMeleeHitOnly()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "rogue"
game.player.skills.poisonBlade = true
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
orc.hp, orc.maxHp = 100, 100
game.level.entities = { orc }
eq(game:useSkill("poisonBlade"), true, "poison blade readies without needing a target")
game:playerAttack(orc)
ok(Status.has(orc, Status.POISON), "the readied venom lands on the very next melee hit")
orc.statuses = {}
game:playerAttack(orc)
eq(Status.has(orc, Status.POISON), false, "poison blade is spent after one hit, not reapplied on the next")
end
-- pendingPoisonBlade lives on the player between "ready the venom" and "land
-- the next swing" - a window a save/quit can happen inside, exactly like
-- reputation, sanity or the skill tree already have to survive Game:toData.
function tests.pendingPoisonBladeReadinessSurvivesAGameSaveLoadRoundTrip()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "rogue"
game.player.skills.poisonBlade = true
eq(game:useSkill("poisonBlade"), true, "poison blade readies without needing a target")
eq(game.player.pendingPoisonBlade, true, "readying the skill sets the pending flag before any save/load happens")
local restored = Game.fromData(game:toData())
eq(restored.player.pendingPoisonBlade, true, "a poison blade readied before a save is still primed after loading it back")
ok(Skills.consumePoisonBlade(restored.player), "the restored pending venom is still consumable on the very next swing")
end
function tests.mageOverloadBoostsMissileDamage()
local game = stagedGame(5, arena(21, 11), 3, 6)
local base = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1)
game.level.entities = { base }
game:castSpell("missile", base)
local dmgBase = base.maxHp - base.hp
local game2 = stagedGame(5, arena(21, 11), 3, 6)
game2.player.class = "mage"
game2.player.skills.overload = true
local boosted = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1)
game2.level.entities = { boosted }
game2:castSpell("missile", boosted)
local dmgBoosted = boosted.maxHp - boosted.hp
ok(dmgBoosted > dmgBase, "overload raises missile spell damage")
end
function tests.mageMeditateRestoresManaWithinCap()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "mage"
game.player.skills.meditate = true
game.player.mana = 2
game:useSkill("meditate")
eq(game.player.mana, 8, "meditate restores a flat amount of mana")
game.player.mana = game.player.maxMana - 1
game:useSkill("meditate")
eq(game.player.mana, game.player.maxMana, "meditate never overfills the mana pool")
end
function tests.mageManaShieldRaisesDefenseWhileManaHeld()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "mage"
game.player.skills.manaShield = true
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
local before = game.player.hp
game.player.mana = 10
game:monsterMelee(orc)
local dmgWithShield = before - game.player.hp
game.player.hp = before
game.player.mana = 0
game:monsterMelee(orc)
local dmgWithoutShield = before - game.player.hp
ok(dmgWithoutShield > dmgWithShield, "mana shield reduces damage taken while mana holds")
end
function tests.rangerMultishotHitsTwoTargetsForOneArrow()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "ranger"
game.player.skills.multishot = true
game.player.bow = Items.bow("shortbow", 3, 6)
game.player.inventory[#game.player.inventory + 1] = Items.ammo("arrow", 5)
local first = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1)
local second = Monsters.create(Monsters.byKey.orc, 2, 3, 8, 1)
game.level.entities = { first, second }
local success = game:useSkill("multishot", first, second)
eq(success, true, "multishot succeeds with two reachable targets")
ok(first.hp < first.maxHp, "the primary target takes damage")
ok(second.hp < second.maxHp, "the secondary target takes damage")
local stack
for _, item in ipairs(game.player.inventory) do
if item.kind == Items.AMMO then stack = item end
end
eq(stack.count, 4, "multishot consumes only a single arrow for both targets")
end
-- Multishot's split arrows must run through Resistances.apply exactly like a
-- single Ranged.fire shot does (both always tag physical - arrows never
-- carry an elemental damageType) - before this test the loop subtracted a
-- raw attack-minus-defense number straight off hp for both targets with no
-- mitigation call at all, so a physically resistant monster took a full,
-- unresisted hit from a skill that is supposed to be "an arrow that splits",
-- not a different damage model than the bow it comes out of.
function tests.rangerMultishotRespectsPhysicalResistanceOnBothSplitArrows()
local function primaryDamageDealt(resistant)
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "ranger"
game.player.skills.multishot = true
game.player.bow = Items.bow("shortbow", 3, 6)
game.player.inventory[#game.player.inventory + 1] = Items.ammo("arrow", 5)
local first = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1)
first.hp, first.maxHp = 100, 100
end end
local order = { local order = {