test: cover tests.companionActConfusedCompanionSometimesStumblesInste...

This commit is contained in:
dualfroz committed 2026-09-09 20:11:06 +02:00
1 parent bc7d3c88d5
commit 9b44abbbfd
1 file changed
+173
+173
View File
@@ -3935,6 +3935,179 @@ function tests.companionFollowsThePlayerWhenNoMonsterIsVisible()
end end
-- Companions.act never dispatches through ai.lua, so without Game:allyActs -- Companions.act never dispatches through ai.lua, so without Game:allyActs
-- Confused wired in first it would leave Status.CONFUSE completely inert on
-- a companion despite Game:springTrap already applying it to "any victim".
-- Mirrors tests.aiActConfusedMonsterSometimesStumblesInsteadOfChasing's own
-- loop-over-seeds shape, but for a point-blank melee decision instead of a
-- chase step.
function tests.companionActConfusedCompanionSometimesStumblesInsteadOfFightingAnAdjacentHostile()
local sawAttack, sawStumble = false, false
for seed = 1, 200 do
local game = stagedGame(seed, arena(21, 11), 3, 6)
local golem = Companions.summon(game, "golem")
local rat = Monsters.create(Monsters.byKey.rat, 50, golem.x + 1, golem.y, 1)
game.level.entities[#game.level.entities + 1] = rat
Status.apply(golem, Status.CONFUSE, 5, 1)
local hpBefore = rat.hp
Companions.act(game, golem)
if rat.hp < hpBefore then
sawAttack = true
else
sawStumble = true
end
if sawAttack and sawStumble then break end
end
ok(sawAttack, "a confused companion's roll sometimes lets it fight an adjacent hostile as normal")
ok(sawStumble, "a confused companion's roll sometimes overrides even a point-blank attack")
end
-- Unlike a confused monster's stumble (Ai.act -> Game:moveMonster, which
-- treats a step onto the player's cell as an attack), a confused companion
-- must never land a hit on the very player it is bonded to defend - the
-- companion spawns due east of the player (Companions.summon's own
-- ADJACENT order), so a westward stumble roll aims exactly at the player's
-- cell across this seed range, and the player's hp must stay untouched
-- every single time regardless of which of the 4 directions actually rolls.
function tests.companionActConfusedCompanionNeverAttacksThePlayerEvenIfTheStumbleAimsAtItsCell()
local sawMoved = false
for seed = 1, 300 do
local game = stagedGame(seed, arena(21, 11), 5, 5)
local golem = Companions.summon(game, "golem")
Status.apply(golem, Status.CONFUSE, 5, 1)
local hpBefore = game.player.hp
Companions.act(game, golem)
eq(game.player.hp, hpBefore, "a confused companion never attacks the player even when its stumble aims at their cell")
ok(not (golem.x == game.player.x and golem.y == game.player.y),
"a confused companion's stumble is blocked, not allowed to share the player's own cell")
if golem.x ~= 6 or golem.y ~= 5 then sawMoved = true end
end
ok(sawMoved, "the confused companion's stumble roll actually moves it somewhere across many seeds")
end
function tests.hearthSpriteMendsThePlayerWhenIdle()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.player.hp = game.player.maxHp - 5
local sprite = Companions.summon(game, "sprite")
local before = game.player.hp
Companions.act(game, sprite)
eq(game.player.hp, before + Companions.byKey.sprite.heal, "a hearth sprite mends its bonded player each idle turn")
end
function tests.useItemOnACharmBondsACompanionAndConsumesIt()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.player.inventory = { Items.charm("hound") }
ok(game:useItem(1), "using a charm succeeds")
eq(#game.player.inventory, 0, "the charm leaves the inventory once bonded")
eq(#game.level.entities, 1, "the bonded hound is added to the level")
eq(game.level.entities[1].species, "hound", "the bonded companion matches the charm's species")
end
function tests.takeMonsterTurnRoutesACompanionThroughCompanionsAct()
local game = stagedGame(1, arena(21, 11), 5, 5)
local hound = Companions.summon(game, "hound")
local rat = Monsters.create(Monsters.byKey.rat, 50, hound.x + 1, hound.y, 1)
game.level.entities[#game.level.entities + 1] = rat
game:takeMonsterTurn(hound)
eq(rat.hp, 0, "the full monster-turn pipeline dispatches a companion's turn to companions.act")
end
-- Game:hungerTorchTick already ticks Disease.tick(self.player) every player
-- turn; Game:takeMonsterTurn never called the same generic, entity-agnostic
-- function for anyone else, so an ally infected through the new
-- infighting.lua bite chance above would sit permanently incubating -
-- never erupting, never escalating, never curable by anything that reads
-- disease state - since nothing ever advanced its clock.
function tests.gameTakeMonsterTurnTicksAnInfectedCompanionsDiseaseExactlyLikeThePlayersOwnEndOfTurnDoes()
local game = stagedGame(1, arena(21, 11), 5, 5)
local hound = Companions.summon(game, "hound")
hound.hp, hound.maxHp = 1000, 1000
Disease.infect(hound, Disease.PARASITES)
local incubation = Disease.defs[Disease.PARASITES].incubation
for _ = 1, incubation - 1 do
game:takeMonsterTurn(hound)
eq(hound.disease.stage, 0, "the companion's infection keeps quietly incubating turn by turn")
end
game:takeMonsterTurn(hound)
eq(hound.disease.stage, 1, "the companion's infection erupts on schedule, exactly like the player's own would")
ok(contains(game.log:last(), "breaks out in gut parasites"), "the eruption is logged with the companion's own name")
end
function tests.aFallenCompanionIsLoggedAndSweptWithoutAwardingXp()
local game = stagedGame(1, arena(21, 11), 5, 5)
local hound = Companions.summon(game, "hound")
hound.hp = 1
Status.apply(hound, Status.POISON, 1, 5)
local xpBefore = game.player.xp
game:takeMonsterTurn(hound)
eq(#game.level.entities, 0, "a companion that dies to its own status tick is swept from the level")
eq(game.player.xp, xpBefore, "a fallen companion grants the player no xp")
ok(contains(game.log:last(), "falls"), "a fallen companion's death is logged")
end
function tests.builderOccasionallyPlacesACharmFromDepthThree()
local level = Builder.build(3, Rng.new(1), {})
local found
for _, ground in ipairs(level.floorItems) do
if ground.item.kind == Items.CHARM then found = ground.item end
end
ok(found ~= nil, "this seed's depth-3 floor places a companion's charm")
ok(Companions.byKey[found.companion] ~= nil, "the charm names a real companion species")
end
function tests.charmsStayOffTheShallowestFloors()
for seed = 1, 5 do
for depth = 1, 2 do
local level = Builder.build(depth, Rng.new(seed), {})
for _, ground in ipairs(level.floorItems) do
ok(ground.item.kind ~= Items.CHARM, "no charm appears above depth 3")
end
end
end
end
function tests.companionFieldsSurviveSerialization()
local game = stagedGame(1, arena(21, 11), 5, 5)
Companions.summon(game, "sprite")
local restoredLevel = Level.fromData(game.level:toData())
eq(#restoredLevel.entities, 1, "the companion survives a level round trip")
eq(restoredLevel.entities[1].kind, "companion", "the companion's kind survives a round trip")
eq(restoredLevel.entities[1].species, "sprite", "the companion's species survives a round trip")
end
function tests.companionGainXpBelowFirstThresholdBanksXpWithoutEvolving()
local game = stagedGame(1, arena(21, 11), 5, 5)
local hound = Companions.summon(game, "hound")
Companions.gainXp(game, hound, 5)
eq(hound.companionXp, 5, "xp below the first threshold still accumulates")
eq(hound.companionLevel, 1, "a companion under its first threshold stays at level 1")
eq(hound.name, "hunting hound", "an unevolved companion keeps its base name")
eq(hound.attack, 4, "an unevolved companion keeps its base attack")
end
function tests.companionGainXpAtFirstThresholdEvolvesRenamesAndGrowsStats()
local game = stagedGame(1, arena(21, 11), 5, 5)
local hound = Companions.summon(game, "hound")
Companions.gainXp(game, hound, 12)
eq(hound.companionLevel, 2, "reaching the first threshold advances the companion to level 2")
eq(hound.name, "warhound", "the first evolution renames the companion")
eq(hound.maxHp, 20, "the first evolution's hp bonus raises max hp")
eq(hound.hp, 20, "the first evolution's hp bonus also heals the same amount")
eq(hound.attack, 6, "the first evolution's attack bonus is applied")
eq(hound.defense, 1, "the first evolution's defense bonus is applied")
ok(contains(game.log:last(), "evolves"), "an evolution is logged")
end
function tests.companionGainXpCrossingBothThresholdsAtOnceAppliesBothStagesCumulatively()
local game = stagedGame(1, arena(21, 11), 5, 5)
local hound = Companions.summon(game, "hound")
Companions.gainXp(game, hound, 40)
eq(hound.companionLevel, 3, "a single large xp grant can vault past an intermediate threshold")
eq(hound.name, "dire hound", "the companion carries the highest stage's name, not the first")
eq(hound.maxHp, 30, "both stages' hp bonuses stack (14 base + 6 + 10)")
eq(hound.attack, 9, "both stages' attack bonuses stack (4 base + 2 + 3)")
eq(hound.defense, 2, "both stages' defense bonuses stack (0 base + 1 + 1)")
end
end end
local order = { local order = {