test: cover tests.bossPhaseFieldsSurviveALevelSerializationRoundTrip

This commit is contained in:
dualfroz committed 2026-09-10 13:44:37 +02:00
1 parent 94f102b1e2
commit 61296a4dd3
1 file changed
+173
+173
View File
@@ -4973,6 +4973,179 @@ function tests.takeMonsterTurnSkipsAbilityAndAiWhileABossBracesOrUnleashesItsCat
warlord.enraged = true warlord.enraged = true
warlord.hp = math.floor(warlord.maxHp * spec.desperateAt) warlord.hp = math.floor(warlord.maxHp * spec.desperateAt)
game.level.entities = { warlord } game.level.entities = { warlord }
game:takeMonsterTurn(warlord)
eq(warlord.x, 4, "a bracing boss never steps toward the player through its ordinary ai")
local hpAfterBrace = game.player.hp
eq(hpAfterBrace, game.player.maxHp, "bracing alone never damages the player")
game:takeMonsterTurn(warlord)
ok(game.player.hp < hpAfterBrace, "unleashing the cataclysm damages the adjacent player")
local hpBeforeThird = game.player.hp
game:takeMonsterTurn(warlord)
ok(warlord.ability == nil or true, "a spent boss falls through to its plain rally ability or ai on its next turn")
ok(game.player.hp <= hpBeforeThird, "the boss keeps acting normally once its cataclysm is spent")
end
function tests.bossPhaseFieldsSurviveALevelSerializationRoundTrip()
local game = stagedGame(3, arena(21, 11), 5, 5)
local spec = Bosses.byKey.warlord
local warlord = Bosses.create(spec, 1, 6, 5)
warlord.enraged = true
warlord.hp = math.floor(warlord.maxHp * spec.desperateAt)
Phases.tick(game, warlord) -- brace: desperate + channeling
game.level.entities = { warlord }
local restoredLevel = Level.fromData(game.level:toData())
local restored = restoredLevel.entities[1]
eq(restored.desperate, true, "the boss's desperate flag survives a round trip")
eq(restored.channeling, true, "the boss's channeling flag survives a round trip")
eq(restored.defense, warlord.defense, "the boss's braced defense value survives a round trip")
Phases.tick(game, restored) -- unleash on the restored copy
eq(restored.novaSpent, true, "a restored boss can still unleash and latch novaSpent")
end
function tests.questGenerateProducesACullBounty()
local bounty = Quests.generate(Rng.new(2), 4, 10)
eq(bounty.kind, Quests.CULL, "this seed at depth 4 rolls a cull bounty")
eq(bounty.species, "necromancer", "the cull bounty's target species is fixed by the seed")
eq(bounty.need, 4, "the cull bounty's tally is fixed by the seed")
eq(bounty.have, 0, "a freshly generated cull bounty starts at zero progress")
eq(bounty.rewardGold, 47, "reward gold scales with the generating depth")
eq(bounty.rewardXp, 26, "reward xp scales with the generating depth")
eq(Quests.describe(bounty), "cull 4 necromancer (0/4)", "describe renders the cull bounty's live progress")
end
function tests.questGenerateProducesAHoardBounty()
local bounty = Quests.generate(Rng.new(1), 4, 10)
eq(bounty.kind, Quests.HOARD, "this seed at depth 4 rolls a hoard bounty")
eq(bounty.need, 120, "the hoard bounty's absolute gold target scales with depth")
eq(Quests.describe(bounty), "amass 120 gold", "describe renders the hoard bounty's target")
end
function tests.questGenerateProducesADepthBountyClampedToMaxDepth()
local shallow = Quests.generate(Rng.new(5), 4, 10)
eq(shallow.kind, Quests.DEPTH, "this seed at depth 4 rolls a depth bounty")
eq(shallow.need, 6, "an unclamped depth bounty targets a few floors further down")
local nearBottom = Quests.generate(Rng.new(5), 9, 10)
eq(nearBottom.kind, Quests.DEPTH, "the same seed at depth 9 also rolls a depth bounty")
eq(nearBottom.need, 10, "a depth bounty that would exceed the dungeon's floor is clamped to it")
end
function tests.questAcceptBindsThePlayerAndClaimsTheBoard()
local player = { quest = nil }
local board = { x = 5, y = 5, claimed = false, quest = Quests.generate(Rng.new(2), 4, 10) }
local success, line = Quests.accept(player, board)
eq(success, true, "accepting an unclaimed bounty while unbound succeeds")
eq(player.quest, board.quest, "the player is bound to the board's exact bounty")
eq(board.claimed, true, "the board is claimed the instant its bounty is taken")
ok(line:find("necromancer") ~= nil, "the acceptance line names the bounty struck")
end
function tests.questAcceptFailsWhenAlreadyHoldingOne()
local existing = Quests.generate(Rng.new(1), 4, 10)
local player = { quest = existing }
local board = { x = 5, y = 5, claimed = false, quest = Quests.generate(Rng.new(2), 4, 10) }
local success = Quests.accept(player, board)
eq(success, false, "a player already bound to a bounty cannot bind to a second")
eq(player.quest, existing, "the player's original bounty is untouched")
eq(board.claimed, false, "the untaken board stays unclaimed")
end
function tests.questAcceptFailsOnAnAlreadyClaimedBoard()
local player = { quest = nil }
local board = { x = 5, y = 5, claimed = true, quest = Quests.generate(Rng.new(2), 4, 10) }
local success = Quests.accept(player, board)
eq(success, false, "a board already claimed grants no second bounty")
eq(player.quest, nil, "the player stays unbound")
end
function tests.questRegisterKillIncrementsOnlyMatchingSpeciesWhileActive()
local player = { quest = { kind = Quests.CULL, species = "rat", need = 3, have = 0 } }
Quests.registerKill(player, "goblin")
eq(player.quest.have, 0, "a kill of an unrelated species does not advance the tally")
Quests.registerKill(player, "rat")
Quests.registerKill(player, "rat")
eq(player.quest.have, 2, "kills of the target species advance the tally")
Quests.registerKill(player, "rat")
Quests.registerKill(player, "rat")
eq(player.quest.have, 3, "the tally never climbs past the bounty's required count")
end
function tests.questResolvedDetectsEachKindsCompletion()
local culler = { quest = { kind = Quests.CULL, need = 3, have = 2 }, gold = 0 }
eq(Quests.resolved(culler, 1), false, "a cull short of its tally is not resolved")
culler.quest.have = 3
eq(Quests.resolved(culler, 1), true, "a cull at its tally is resolved")
local hoarder = { quest = { kind = Quests.HOARD, need = 100 }, gold = 40 }
eq(Quests.resolved(hoarder, 1), false, "a hoard short of its gold target is not resolved")
hoarder.gold = 100
eq(Quests.resolved(hoarder, 1), true, "a hoard at its gold target is resolved")
local pactbound = { quest = { kind = Quests.DEPTH, need = 5 }, gold = 0 }
eq(Quests.resolved(pactbound, 4), false, "a depth pact short of its floor is not resolved")
eq(Quests.resolved(pactbound, 5), true, "a depth pact met at its floor is resolved")
eq(Quests.resolved({ quest = nil, gold = 0 }, 5), false, "a player with no bounty is never resolved")
end
function tests.gameResolveQuestPaysOutAndClearsAFulfilledBounty()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.player.quest = { kind = Quests.CULL, species = "rat", need = 1, have = 1, rewardGold = 30, rewardXp = 15 }
local goldBefore, essenceBefore = game.player.gold, game.player.essence
local resolved = game:resolveQuest()
eq(resolved, true, "resolveQuest reports the payout it made")
eq(game.player.gold, goldBefore + 30, "the bounty's gold reward is paid")
eq(game.player.essence, essenceBefore + Crafting.essenceFor(15), "the bounty's xp reward also converts to essence")
eq(game.player.quest, nil, "a resolved bounty frees the player to bind another")
end
function tests.gameResolveQuestIsANoOpOnAnUnmetBounty()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.player.quest = { kind = Quests.CULL, species = "rat", need = 3, have = 1, rewardGold = 30, rewardXp = 15 }
local goldBefore = game.player.gold
local resolved = game:resolveQuest()
eq(resolved, false, "resolveQuest reports nothing was paid out")
eq(game.player.gold, goldBefore, "no gold changes hands on an unmet bounty")
ok(game.player.quest ~= nil, "the unmet bounty stays bound to the player")
end
function tests.gameTryAcceptQuestBindsAndLogsWhenStandingOnAnUnclaimedBoard()
local game = stagedGame(1, arena(21, 11), 5, 5)
local board = { x = 5, y = 5, claimed = false, quest = Quests.generate(Rng.new(2), 4, 10) }
local success = game:tryAcceptQuest(board)
eq(success, true, "tryAcceptQuest binds the player to the board it stands on")
eq(game.player.quest.kind, Quests.CULL, "the bound bounty matches the board's own")
eq(board.claimed, true, "the board is spent on acceptance")
end
function tests.autopilotAcceptsAnUnclaimedBountyForFreeWithoutSkippingItsTurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.level.questBoards = { { x = 5, y = 5, claimed = false, quest = Quests.generate(Rng.new(2), 4, 10) } }
local orc = Monsters.create(Monsters.byKey.orc, 1, 6, 5, 1)
game.level.entities = { orc }
local hpBefore = orc.hp
game:choosePlayerAction()
ok(game.player.quest ~= nil, "the autopilot binds itself to a bounty it stands on")
eq(game.level.questBoards[1].claimed, true, "the board is claimed once the autopilot steps onto it")
ok(orc.hp < hpBefore, "accepting the bounty costs no action, so the same turn still lands an attack")
end
function tests.autopilotSkipsAnAlreadyClaimedBoard()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.level.questBoards = { { x = 5, y = 5, claimed = true, quest = Quests.generate(Rng.new(2), 4, 10) } }
game:choosePlayerAction()
eq(game.player.quest, nil, "the autopilot never binds to an already claimed board")
end
function tests.autopilotDoesNotStackASecondBountyWhileOneIsActive()
local game = stagedGame(1, arena(21, 11), 5, 5)
local held = Quests.generate(Rng.new(1), 4, 10)
game.player.quest = held
game.level.questBoards = { { x = 5, y = 5, claimed = false, quest = Quests.generate(Rng.new(2), 4, 10) } }
end end
local order = { local order = {