test: cover tests.monstersCreateNeverAliasesTheSpeciesCatalogsInnateR...
This commit is contained in:
1 parent
aca2e06b3c
commit
6e216940ac
1 file changed
+173
+173
@@ -6184,6 +6184,179 @@ function tests.eliteFieldsSurviveSerialization()
|
|||||||
eq(restored.speed, orc.speed, "the swift affix's speed bonus survives serialization")
|
eq(restored.speed, orc.speed, "the swift affix's speed bonus survives serialization")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- monsters.lua: Monsters.create must hand every entity its own resist
|
||||||
|
-- table, never Monsters.byKey[species].resist itself - applyAffix above
|
||||||
|
-- mutates a promoted elite's entity.resist[element] in place the instant it
|
||||||
|
-- rolls "warded", and a shared reference there would leak that bump onto
|
||||||
|
-- every future spawn of the same species (and the catalog itself) for the
|
||||||
|
-- rest of the process, not just the one entity actually promoted. Found via
|
||||||
|
-- examine.lua's own test suite: a plain, unpromoted troll built late in a
|
||||||
|
-- long test run reported "fire resist" instead of its catalog "fire weak"
|
||||||
|
-- because an earlier test's elite promotion (through Builder.build, not
|
||||||
|
-- shown here) had already corrupted the shared table.
|
||||||
|
function tests.monstersCreateNeverAliasesTheSpeciesCatalogsInnateResistTable()
|
||||||
|
local first = Monsters.create(Monsters.byKey.troll, 1, 5, 5, 1)
|
||||||
|
local second = Monsters.create(Monsters.byKey.troll, 2, 6, 5, 1)
|
||||||
|
ok(first.resist ~= Monsters.byKey.troll.resist, "an entity's resist table is its own, not the catalog's")
|
||||||
|
first.resist.fire = Resistances.RESIST
|
||||||
|
eq(second.resist.fire, "weak", "mutating one troll's resist never leaks onto another troll of the same species")
|
||||||
|
eq(Monsters.byKey.troll.resist.fire, "weak", "mutating an entity's resist never corrupts the species catalog itself")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.builderOccasionallyPromotesAMonsterToEliteFromDepthThree()
|
||||||
|
local level = Builder.build(3, Rng.new(1), {})
|
||||||
|
local found
|
||||||
|
for _, e in ipairs(level.entities) do
|
||||||
|
if e.elite then found = e end
|
||||||
|
end
|
||||||
|
ok(found ~= nil, "this seed's depth-3 floor promotes a monster to elite")
|
||||||
|
eq(found.eliteTier, "elite", "a depth-3 promotion is a plain elite, never a champion")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.elitesNeverAppearOnTheShallowestTwoFloors()
|
||||||
|
for seed = 1, 20 do
|
||||||
|
for depth = 1, 2 do
|
||||||
|
local level = Builder.build(depth, Rng.new(seed), {})
|
||||||
|
for _, e in ipairs(level.entities) do
|
||||||
|
ok(not e.elite, "no elite appears above depth 3")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.eliteVampiricMonsterDrainsHpFromThePlayerOnAMeleeHit()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
|
||||||
|
orc.elite, orc.eliteAffixes = true, { "vampiric" }
|
||||||
|
orc.hp = orc.maxHp - 5
|
||||||
|
game.level.entities = { orc }
|
||||||
|
local before = orc.hp
|
||||||
|
game:monsterMelee(orc)
|
||||||
|
ok(orc.hp > before, "a vampiric elite heals on the melee hit it lands")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.eliteChillingMonsterSlowsThePlayerOnAMeleeHit()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
|
||||||
|
orc.elite, orc.eliteAffixes = true, { "chilling" }
|
||||||
|
game.level.entities = { orc }
|
||||||
|
ok(not Status.has(game.player, Status.SLOW), "the player starts unslowed")
|
||||||
|
game:monsterMelee(orc)
|
||||||
|
ok(Status.has(game.player, Status.SLOW), "a chilling elite's hit leaves the player slowed")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.eliteVolatileMonsterExplodesOnDeathDamagingAnAdjacentPlayer()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
local rat = Monsters.create(Monsters.byKey.rat, 1, 4, 6, 1)
|
||||||
|
rat.elite, rat.eliteAffixes = true, { "volatile" }
|
||||||
|
rat.hp = 0
|
||||||
|
game.level.entities = { rat }
|
||||||
|
local before = game.player.hp
|
||||||
|
game:awardKill(rat)
|
||||||
|
ok(game.player.hp < before, "a volatile elite's corpse bursts, damaging the adjacent player")
|
||||||
|
ok(contains(table.concat(game.log:recent(10), " "), "bursts"), "the burst is logged")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.eliteVolatileMonsterDoesNotBurstAnythingBeyondItsBlastRadius()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
local rat = Monsters.create(Monsters.byKey.rat, 1, 8, 6, 1)
|
||||||
|
rat.elite, rat.eliteAffixes = true, { "volatile" }
|
||||||
|
rat.hp = 0
|
||||||
|
game.level.entities = { rat }
|
||||||
|
local before = game.player.hp
|
||||||
|
game:awardKill(rat)
|
||||||
|
eq(game.player.hp, before, "a volatile corpse two cells away leaves the player untouched")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.eliteKillGrantsBonusXpOverThePlainSpeciesReward()
|
||||||
|
local game = stagedGame(9, arena(21, 11), 3, 6)
|
||||||
|
local rat = Monsters.create(Monsters.byKey.rat, 1, 4, 6, 1)
|
||||||
|
rat.hp = 1
|
||||||
|
rat.elite, rat.eliteTier, rat.eliteAffixes = true, "elite", { "brutal" }
|
||||||
|
game.level.entities = { rat }
|
||||||
|
local baseXp = Monsters.xpFor(rat)
|
||||||
|
local bonus = Elites.xpBonus(rat, baseXp)
|
||||||
|
ok(bonus > 0, "the elite bonus is a real, nonzero amount")
|
||||||
|
local xpBefore = game.player.xp
|
||||||
|
game:playerAttack(rat)
|
||||||
|
eq(game.player.xp, xpBefore + baseXp + bonus, "felling an elite grants a bonus over its plain species xp")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Vaults (vaults.lua): a lever puzzle gating a guaranteed treasure. Levers
|
||||||
|
-- and the door they share an id with are plain data on the level, exactly
|
||||||
|
-- like shrines and cauldrons above, so most of these tests build that data
|
||||||
|
-- directly rather than waiting on a builder roll.
|
||||||
|
|
||||||
|
function tests.vaultsPullThrowsALeverAndOpensASingleLeverVaultImmediately()
|
||||||
|
local level = { vaultLevers = {}, vaultDoors = {} }
|
||||||
|
local lever = { x = 1, y = 1, id = "v1", pulled = false }
|
||||||
|
level.vaultLevers = { lever }
|
||||||
|
level.vaultDoors = { { x = 2, y = 2, id = "v1", opened = false } }
|
||||||
|
local pulled, door = Vaults.pull(level, lever)
|
||||||
|
eq(pulled, true, "pulling a fresh lever succeeds")
|
||||||
|
eq(lever.pulled, true, "the lever is marked pulled")
|
||||||
|
ok(door ~= nil, "a single-lever vault's door opens on its only pull")
|
||||||
|
eq(door.id, "v1", "the returned door is the one this puzzle actually gates")
|
||||||
|
local pulledAgain, doorAgain = Vaults.pull(level, lever)
|
||||||
|
eq(pulledAgain, false, "pulling an already-thrown lever reports failure")
|
||||||
|
eq(doorAgain, nil, "a no-op pull reports no door")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.vaultsPullOpensTheDoorOnlyOnceEveryLeverIsThrown()
|
||||||
|
local level = { vaultLevers = {}, vaultDoors = {} }
|
||||||
|
local leverA = { x = 1, y = 1, id = "v2", pulled = false }
|
||||||
|
local leverB = { x = 1, y = 2, id = "v2", pulled = false }
|
||||||
|
level.vaultLevers = { leverA, leverB }
|
||||||
|
level.vaultDoors = { { x = 3, y = 3, id = "v2", opened = false } }
|
||||||
|
local pulledA, doorA = Vaults.pull(level, leverA)
|
||||||
|
eq(pulledA, true, "pulling the first of two levers succeeds")
|
||||||
|
eq(doorA, nil, "the door stays shut until every lever sharing its id is thrown")
|
||||||
|
local pulledB, doorB = Vaults.pull(level, leverB)
|
||||||
|
eq(pulledB, true, "pulling the second lever succeeds")
|
||||||
|
ok(doorB ~= nil, "the door opens the instant the last lever is thrown")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.vaultsTreasureRollsAGuaranteedAffixOnABaseStrongerThanAnyFloorDrop()
|
||||||
|
for seed = 1, 20 do
|
||||||
|
local item = Vaults.treasure(Rng.new(seed), 5)
|
||||||
|
ok(item.kind == Items.WEAPON or item.kind == Items.ARMOR,
|
||||||
|
"vault treasure is always a weapon or a suit of armor")
|
||||||
|
ok(item.affix ~= nil, "vault treasure always carries a guaranteed affix")
|
||||||
|
ok(item.power >= 7, "vault treasure's power exceeds anything the depth-5 floor loot table can roll")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gamePullLeverOpensASingleLeverVaultAndTracksTheStat()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
game.level.map:set(6, 6, Tiles.LEVER)
|
||||||
|
game.level.map:set(10, 6, Tiles.VAULT_DOOR)
|
||||||
|
local lever = { x = 6, y = 6, id = "v3", pulled = false }
|
||||||
|
game.level.vaultLevers = { lever }
|
||||||
|
game.level.vaultDoors = { { x = 10, y = 6, id = "v3", opened = false } }
|
||||||
|
eq(game.stats.vaultsOpened, 0, "a fresh run has opened no vaults")
|
||||||
|
local pulled = game:pullLever(lever)
|
||||||
|
eq(pulled, true, "pulling the lever succeeds")
|
||||||
|
eq(game.level.map:get(6, 6), Tiles.LEVER_PULLED, "the lever's own tile flips to its pulled glyph")
|
||||||
|
eq(game.level.map:get(10, 6), Tiles.DOOR_OPEN, "the vault door swings open once its only lever is thrown")
|
||||||
|
eq(game.stats.vaultsOpened, 1, "opening a vault advances the run's vault tally")
|
||||||
|
ok(contains(game.log:last(), "grinds open"), "opening the vault is logged")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gamePullLeverOnATwoLeverVaultWaitsForTheSecondPull()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
game.level.map:set(6, 6, Tiles.LEVER)
|
||||||
|
game.level.map:set(7, 6, Tiles.LEVER)
|
||||||
|
game.level.map:set(10, 6, Tiles.VAULT_DOOR)
|
||||||
|
local leverA = { x = 6, y = 6, id = "v4", pulled = false }
|
||||||
|
local leverB = { x = 7, y = 6, id = "v4", pulled = false }
|
||||||
|
game.level.vaultLevers = { leverA, leverB }
|
||||||
|
game.level.vaultDoors = { { x = 10, y = 6, id = "v4", opened = false } }
|
||||||
|
game:pullLever(leverA)
|
||||||
|
eq(game.level.map:get(10, 6), Tiles.VAULT_DOOR, "the door stays sealed with a lever still unthrown")
|
||||||
|
eq(game.stats.vaultsOpened, 0, "the vault tally does not advance until the puzzle is fully solved")
|
||||||
|
ok(contains(game.log:last(), "clunks into place"), "a partial pull is logged as a clunk, not an opening")
|
||||||
|
game:pullLever(leverB)
|
||||||
|
eq(game.level.map:get(10, 6), Tiles.DOOR_OPEN, "the door opens the instant the last lever is thrown")
|
||||||
end
|
end
|
||||||
|
|
||||||
local order = {
|
local order = {
|
||||||
|
|||||||
Reference in new issue
Block a user