test: cover tests.infightingStrikeAppliesTheAttackersEliteLifestealAg...

This commit is contained in:
dualfroz committed 2026-09-12 13:20:37 +02:00
1 parent c2c0a4c962
commit b40d8482e4
1 file changed
+173
+173
View File
@@ -10682,6 +10682,179 @@ function tests.infightingStrikeEmitsNoiseThatCanStirADormantBystanderNotInvolved
bystander.awareness = Stealth.DORMANT
game.level.entities = { goblin, orc, bystander }
Infighting.strike(game, goblin, orc)
if bystander.awareness ~= Stealth.DORMANT then stirred = true; break end
end
ok(stirred, "two monsters brawling among themselves are exactly as loud as any other fight")
end
-- Same shape of gap as the Noise fix just above, but for a different
-- Combat.attack side effect: Game:applyEliteOnHit (a vampiric elite's
-- lifesteal, a chilling elite's status rider) was only ever wired into
-- Game:monsterMelee/monsterShoot, both of which assume the victim is the
-- player - its log lines even hardcoded the word "you". An elite fighting a
-- rival monster or the player's own companion/hireling/thrall through
-- Infighting.strike got neither the heal nor the rider at all.
function tests.infightingStrikeAppliesTheAttackersEliteLifestealAgainstAnyDefenderNotJustThePlayer()
local game = stagedGame(1, arena(21, 11), 3, 3)
local orc = Monsters.create(Monsters.byKey.orc, 1, 10, 6, 1)
orc.elite, orc.eliteAffixes = true, { "vampiric" }
orc.hp = orc.maxHp - 5
local wolf = Entity.new({
id = 2, x = 11, y = 6, glyph = "d", name = "wolf", kind = "companion",
hp = 20, maxHp = 20, attack = 2, defense = 0, speed = 100,
})
game.level.entities = { orc, wolf }
local before = orc.hp
Infighting.strike(game, orc, wolf)
ok(orc.hp > before, "a vampiric elite heals on an infighting hit exactly as it does against the player")
ok(contains(game.log:last(), "wolf"), "the drain message names the actual victim, not the player")
ok(not contains(game.log:last(), "you"), "a wolf being drained is never described as draining \"you\"")
end
function tests.infightingStrikeAppliesTheAttackersEliteOnHitRiderAgainstAnyDefenderNotJustThePlayer()
local game = stagedGame(1, arena(21, 11), 3, 3)
local orc = Monsters.create(Monsters.byKey.orc, 1, 10, 6, 1)
orc.elite, orc.eliteAffixes = true, { "chilling" }
local wolf = Entity.new({
id = 2, x = 11, y = 6, glyph = "d", name = "wolf", kind = "hireling",
hp = 20, maxHp = 20, attack = 2, defense = 0, speed = 100,
})
game.level.entities = { orc, wolf }
ok(not Status.has(wolf, Status.SLOW), "the hireling starts unslowed")
Infighting.strike(game, orc, wolf)
ok(Status.has(wolf, Status.SLOW), "a chilling elite's infighting hit slows its actual victim, not just the player")
ok(contains(game.log:last(), "wolf"), "the rider message names the actual victim, not the player")
end
-- Game:monsterMelee has always rolled a bite-carrying species' disease.lua
-- infection against the player; Infighting.strike shares every other one of
-- monsterMelee's hooks (Combat.attack, applyEliteOnHit, emitNoise) but never
-- this one, so a rat fighting a rival monster or the player's own
-- companion/hireling/thrall through infighting could never actually spread
-- its parasites - the same shape of gap the elite-hook tests above already
-- closed for lifesteal and on-hit riders.
function tests.infightingStrikeCanTransmitTheAttackersBiteDiseaseToAnyDefenderNotJustThePlayer()
local game = stagedGame(1, arena(21, 11), 3, 3)
local rat = Monsters.create(Monsters.byKey.rat, 1, 10, 6, 1)
eq(rat.biteDisease, Disease.PARASITES, "a rat carries parasites to begin with")
local wolf = Entity.new({
id = 2, x = 11, y = 6, glyph = "d", name = "wolf", kind = "companion",
hp = 10000, maxHp = 10000, attack = 2, defense = 0, speed = 100,
})
game.level.entities = { rat, wolf }
local infected = false
for _ = 1, 400 do
Infighting.strike(game, rat, wolf)
if Disease.isInfected(wolf) then infected = true break end
end
ok(infected, "enough infighting bites from a parasite-carrying rival eventually infect a companion, not just the player")
eq(wolf.disease.key, Disease.PARASITES, "the companion caught exactly the rat's own carried affliction")
ok(contains(game.log:last(), "wolf"), "the infection message names the actual victim, not the player")
end
function tests.infightingTryActAttacksAnAdjacentRivalAndReturnsTrue()
local game = stagedGame(1, arena(21, 11), 3, 3)
local goblin = Monsters.create(Monsters.byKey.goblin, 1, 10, 6, 1)
local orc = Monsters.create(Monsters.byKey.orc, 2, 11, 6, 1)
game.level.entities = { goblin, orc }
local hpBefore = orc.hp
local acted = Infighting.tryAct(game, goblin)
ok(acted, "infighting consumes the goblin's turn when an adjacent rival is present")
ok(orc.hp < hpBefore, "the adjacent rival takes damage")
end
function tests.infightingTryActStepsTowardADistantRivalAndReturnsTrue()
local game = stagedGame(1, arena(21, 11), 3, 3)
local goblin = Monsters.create(Monsters.byKey.goblin, 1, 10, 6, 1)
local orc = Monsters.create(Monsters.byKey.orc, 2, 14, 6, 1)
game.level.entities = { goblin, orc }
local acted = Infighting.tryAct(game, goblin)
ok(acted, "infighting consumes the goblin's turn even while the rival is still distant")
eq(goblin.x, 11, "the goblin steps one cell toward the distant rival instead of standing still")
end
function tests.infightingTryActReturnsFalseWithNoQualifyingTarget()
local game = stagedGame(1, arena(21, 11), 3, 3)
local goblin = Monsters.create(Monsters.byKey.goblin, 1, 10, 6, 1)
local rat = Monsters.create(Monsters.byKey.rat, 2, 11, 6, 1)
game.level.entities = { goblin, rat }
ok(not Infighting.tryAct(game, goblin), "infighting yields the turn back to Ai.act when nothing qualifies")
end
function tests.gameTakeMonsterTurnLetsARivalFightInsteadOfChasingADistantPlayer()
local game = stagedGame(1, arena(21, 11), 19, 9)
local goblin = Monsters.create(Monsters.byKey.goblin, 1, 10, 6, 1)
local orc = Monsters.create(Monsters.byKey.orc, 2, 11, 6, 1)
game.level.entities = { goblin, orc }
local hpBefore = orc.hp
game:takeMonsterTurn(goblin)
ok(orc.hp < hpBefore, "a goblin that could chase a distant player instead fights the adjacent rival orc")
eq(goblin.x, 10, "fighting the rival in place keeps the goblin from stepping toward the player")
end
function tests.spellsDiscordAppliesTheDiscordStatusAndSpendsItsListedManaCost()
local game = stagedGame(1, arena(21, 11), 3, 6)
Spells.learn(game.player, "discord")
local orc = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1)
game.level.entities = { orc }
local manaBefore = game.player.mana
local success = game:castSpell("discord", orc)
ok(success, "casting a learned, in-range discord succeeds")
ok(Status.has(orc, Status.DISCORD), "the target now carries the discord status")
eq(game.player.mana, manaBefore - Spells.catalog.discord.cost, "casting discord spends exactly its listed mana cost")
end
function tests.spellsDiscordFailsOutOfRangeAndSpendsNothing()
local game = stagedGame(1, arena(21, 11), 3, 6)
Spells.learn(game.player, "discord")
local farOrc = Monsters.create(Monsters.byKey.orc, 1, 19, 6, 1)
game.level.entities = { farOrc }
local manaBefore = game.player.mana
local success = game:castSpell("discord", farOrc)
ok(not success, "discord cast beyond its range fails")
ok(not Status.has(farOrc, Status.DISCORD), "a failed cast never applies the status")
eq(game.player.mana, manaBefore, "a failed cast spends no mana")
end
function tests.builderCanRollATomeOfTheDiscordSpellSomewhereInARangeOfSeeds()
local found = false
for seed = 1, 100 do
local level = Builder.build(6, Rng.new(seed), {})
for _, ground in ipairs(level.floorItems) do
if ground.item.kind == Items.TOME and ground.item.spell == "discord" then found = true end
end
if found then break end
end
ok(found, "the discord tome is reachable dungeon loot within a reasonable seed range")
end
function tests.spellsConfuseAppliesTheConfuseStatusAndSpendsItsListedManaCost()
local game = stagedGame(1, arena(21, 11), 3, 6)
Spells.learn(game.player, "confuse")
local orc = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1)
game.level.entities = { orc }
local manaBefore = game.player.mana
local success = game:castSpell("confuse", orc)
ok(success, "casting a learned, in-range confuse succeeds")
ok(Status.has(orc, Status.CONFUSE), "the target now carries the confuse status")
eq(game.player.mana, manaBefore - Spells.catalog.confuse.cost, "casting confuse spends exactly its listed mana cost")
end
function tests.spellsConfuseFailsOutOfRangeAndSpendsNothing()
local game = stagedGame(1, arena(21, 11), 3, 6)
Spells.learn(game.player, "confuse")
local farOrc = Monsters.create(Monsters.byKey.orc, 1, 19, 6, 1)
game.level.entities = { farOrc }
local manaBefore = game.player.mana
local success = game:castSpell("confuse", farOrc)
ok(not success, "confuse cast beyond its range fails")
ok(not Status.has(farOrc, Status.CONFUSE), "a failed cast never applies the status")
eq(game.player.mana, manaBefore, "a failed cast spends no mana")
end
function tests.builderCanRollATomeOfTheConfuseSpellSomewhereInARangeOfSeeds()
local found = false
for seed = 1, 100 do
end
local order = {