test: cover tests.companionActConfusedCompanionSometimesStumblesInste...
This commit is contained in:
1 parent
bc7d3c88d5
commit
9b44abbbfd
1 file changed
+173
+173
@@ -3935,6 +3935,179 @@ function tests.companionFollowsThePlayerWhenNoMonsterIsVisible()
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
local order = {
|
||||
|
||||
Reference in new issue
Block a user