test: cover tests.gameEquippingAFreshCurseHidesItUntilYouTryToTakeItOff
This commit is contained in:
1 parent
232a447692
commit
3115db3fe3
1 file changed
+173
+173
@@ -7395,6 +7395,179 @@ function tests.gameEquippingOverABoundCursedWeaponFailsAndRevealsTheCurse()
|
|||||||
ok(game.player.weapon == before, "the bound weapon stays equipped")
|
ok(game.player.weapon == before, "the bound weapon stays equipped")
|
||||||
eq(Curses.isRevealed(before), true, "the failed swap reveals the curse")
|
eq(Curses.isRevealed(before), true, "the failed swap reveals the curse")
|
||||||
eq(#game.player.inventory, 1, "the replacement weapon stays in the pack, never spent")
|
eq(#game.player.inventory, 1, "the replacement weapon stays in the pack, never spent")
|
||||||
|
ok(contains(game.log:last(), "will not come off"), "the log explains the weapon is stuck")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameEquippingAFreshCurseHidesItUntilYouTryToTakeItOff()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
local cursedRing = Curses.afflict(Items.ring("ring of power", 1, 1), Curses.pool.ring[1])
|
||||||
|
game.player.inventory = { cursedRing }
|
||||||
|
ok(game:useItem(1), "equipping an empty slot with a cursed item still succeeds")
|
||||||
|
ok(game.player.ring == cursedRing, "the cursed ring is now equipped")
|
||||||
|
ok(not contains(game.log:last(), "cursed"), "the equip message never spoils an unrevealed curse")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameRemoveCurseScrollLiftsEveryCurseCurrentlyWorn()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
game.player.ring = Curses.afflict(Items.ring("ring of power", 0, 0), Curses.pool.ring[1])
|
||||||
|
game.player.inventory = { Items.scroll("removecurse") }
|
||||||
|
ok(game:useItem(1), "reading the scroll succeeds")
|
||||||
|
eq(Curses.isCursed(game.player.ring), false, "the worn ring's curse is lifted")
|
||||||
|
ok(contains(game.log:last(), "lifts"), "the log reports the cleansing")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameHungerTorchTickBleedsHpFromACursedRingEachTurn()
|
||||||
|
local game = Game.new(1)
|
||||||
|
game.player.ring = Curses.afflict(Items.ring("ring of power", 0, 0), Curses.pool.ring[1])
|
||||||
|
local before = game.player.hp
|
||||||
|
game:hungerTorchTick()
|
||||||
|
eq(game.player.hp, before - 1, "a cursed ring's affliction bleeds its fixed drain each turn")
|
||||||
|
ok(contains(game.log:last(), "saps"), "the drain is announced in the log")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.shrinePrayFortuneBlessingAlsoLiftsAnyCurseCurrentlyWorn()
|
||||||
|
local player = shrinePlayer(50)
|
||||||
|
player.ring = Curses.afflict(Items.ring("ring of power", 0, 0), Curses.pool.ring[1])
|
||||||
|
local game = { player = player, rng = Rng.new(1) }
|
||||||
|
local shrine = { x = 1, y = 1, kind = Shrines.FORTUNE, uses = 0 }
|
||||||
|
local success, lines = Shrines.pray(game, shrine)
|
||||||
|
eq(success, true, "the prayer resolves")
|
||||||
|
eq(Curses.isCursed(player.ring), false, "fortune's blessing lifts the worn ring's curse too")
|
||||||
|
ok(contains(lines[2], "cleanses"), "a lifted curse counts toward the cleansing message")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.renderPanelPrefixesCursedGearOnceRevealedButNotBefore()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
game.player.weapon = Curses.afflict(Items.weapon("dagger", 5), Curses.pool.weapon[1])
|
||||||
|
ok(not contains(Render.panel(game), "cursed"), "an unrevealed curse never shows in the status panel")
|
||||||
|
Curses.reveal(game.player.weapon)
|
||||||
|
ok(contains(Render.panel(game), "cursed dagger"), "a revealed curse is flagged in the status panel")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.builderOccasionallyPlacesACursedPieceOfGear()
|
||||||
|
local found
|
||||||
|
for seed = 1, 80 do
|
||||||
|
local level = Builder.build(2, Rng.new(seed), {})
|
||||||
|
for _, ground in ipairs(level.floorItems) do
|
||||||
|
if ground.item.cursed then found = ground.item end
|
||||||
|
end
|
||||||
|
if found then break end
|
||||||
|
end
|
||||||
|
ok(found ~= nil, "a cursed piece of gear eventually turns up across a handful of seeds")
|
||||||
|
ok(found.cursePenalty ~= nil or found.curseDrain ~= nil, "a cursed drop always carries a concrete penalty")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.builderNeverCursesAnItemThatAlreadyHasABeneficialAffix()
|
||||||
|
for seed = 1, 30 do
|
||||||
|
for depth = 1, 5 do
|
||||||
|
local level = Builder.build(depth, Rng.new(seed), {})
|
||||||
|
for _, ground in ipairs(level.floorItems) do
|
||||||
|
if ground.item.affix then
|
||||||
|
ok(not ground.item.cursed, "an item with a beneficial affix is never also cursed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.cursedFieldsSurviveSerialization()
|
||||||
|
local game = Game.new(3)
|
||||||
|
game.player.ring = Curses.afflict(Items.ring("ring of power", 1, 1), Curses.pool.ring[1])
|
||||||
|
Curses.reveal(game.player.ring)
|
||||||
|
local path = os.tmpname()
|
||||||
|
game:save(path)
|
||||||
|
local restored = Game.load(path)
|
||||||
|
os.remove(path)
|
||||||
|
eq(restored.player.ring.cursed, true, "a cursed ring's flag survives a save/load round trip")
|
||||||
|
eq(restored.player.ring.curseDrain, 1, "a cursed ring's drain survives a save/load round trip")
|
||||||
|
eq(restored.player.ring.curseRevealed, true, "a revealed curse stays revealed after reload")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.momentumStacksReadsZeroForAFreshEntity()
|
||||||
|
eq(Momentum.stacks({}), 0, "an entity with no combo field reads as zero stacks")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.momentumOnMeleeHitBuildsAndCapsAtItsMaxStacks()
|
||||||
|
local entity = {}
|
||||||
|
for i = 1, Momentum.MAX_STACKS do
|
||||||
|
Momentum.onMeleeHit(entity)
|
||||||
|
eq(Momentum.stacks(entity), i, "stack " .. i .. " builds one at a time")
|
||||||
|
end
|
||||||
|
Momentum.onMeleeHit(entity)
|
||||||
|
eq(Momentum.stacks(entity), Momentum.MAX_STACKS, "stacks never climb past the cap")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.momentumOnDamagedSnapsTheStackBackToZero()
|
||||||
|
local entity = { combo = 3 }
|
||||||
|
Momentum.onDamaged(entity)
|
||||||
|
eq(Momentum.stacks(entity), 0, "taking a hit clears the whole stack outright")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.momentumAttackBonusScalesLinearlyWithStacks()
|
||||||
|
local entity = {}
|
||||||
|
eq(Momentum.attackBonus(entity), 0, "no stacks means no bonus")
|
||||||
|
Momentum.onMeleeHit(entity)
|
||||||
|
Momentum.onMeleeHit(entity)
|
||||||
|
eq(Momentum.attackBonus(entity), 2 * Momentum.BONUS_PER_STACK, "the bonus scales with the current stack count")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.entityEffectiveAttackFoldsInMomentumAlongsideStatusModifiers()
|
||||||
|
local entity = Entity.new({ x = 1, y = 1, glyph = "@", name = "you", kind = "player", hp = 20, attack = 6, defense = 2 })
|
||||||
|
eq(entity:effectiveAttack(), 6, "a fresh entity's effective attack is just its base")
|
||||||
|
Momentum.onMeleeHit(entity)
|
||||||
|
Momentum.onMeleeHit(entity)
|
||||||
|
eq(entity:effectiveAttack(), 8, "two momentum stacks add their flat bonus on top of base attack")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gamePlayerAttackBuildsMomentumAndItReadsLiveInEffectiveAttack()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
|
||||||
|
orc.hp, orc.maxHp = 100, 100
|
||||||
|
game.level.entities = { orc }
|
||||||
|
eq(game.player:effectiveAttack(), game.player.attack, "a fresh player carries no momentum bonus")
|
||||||
|
game:playerAttack(orc)
|
||||||
|
eq(game.player.combo, 1, "a landed melee hit builds one stack")
|
||||||
|
eq(game.player:effectiveAttack(), game.player.attack + 1,
|
||||||
|
"the very next read of effectiveAttack already reflects the new stack")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameMonsterMeleeSnapsMomentumBackToZero()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
game.player.combo = 4
|
||||||
|
local orc = Monsters.create(Monsters.byKey.orc, 1, 2, 6, 1)
|
||||||
|
game:monsterMelee(orc)
|
||||||
|
eq(game.player.combo, 0, "getting hit by a monster's melee resets momentum to zero")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameEvadedMonsterAttackNeverBreaksMomentum()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
game.player.skills = { evasion = true }
|
||||||
|
local orc = Monsters.create(Monsters.byKey.orc, 1, 2, 6, 1)
|
||||||
|
local sawEvasion = false
|
||||||
|
for seed = 1, 40 do
|
||||||
|
game.rng = Rng.new(seed)
|
||||||
|
game.player.combo = 3
|
||||||
|
game:monsterMelee(orc)
|
||||||
|
if game.player.combo == 3 then sawEvasion = true break end
|
||||||
|
end
|
||||||
|
ok(sawEvasion, "an evaded attack never reaches Combat.attack, so momentum survives it")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.gameMimicAmbushAlsoBreaksMomentum()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 5, 5)
|
||||||
|
game.player.combo = 2
|
||||||
|
game.level.floorItems = { { x = 5, y = 5, item = Mimics.floorItem(Rng.new(1)) } }
|
||||||
|
game:pickupHere()
|
||||||
|
eq(game.player.combo, 0, "a mimic's ambush hit also breaks momentum")
|
||||||
|
end
|
||||||
|
|
||||||
|
function tests.renderPanelShowsComboOnlyOnceItIsBuilt()
|
||||||
|
local game = stagedGame(1, arena(21, 11), 3, 6)
|
||||||
|
ok(not contains(Render.panel(game), "combo"), "a fresh player with no momentum shows no combo line")
|
||||||
|
game.player.combo = 3
|
||||||
|
ok(contains(Render.panel(game), "combo x3"), "a built-up combo is reported in the status panel")
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local order = {
|
local order = {
|
||||||
|
|||||||
Reference in new issue
Block a user