test: cover tests.gameEquippingAFreshCurseHidesItUntilYouTryToTakeItOff

This commit is contained in:
dualfroz committed 2026-09-11 14:00:28 +02:00
1 parent 232a447692
commit 3115db3fe3
1 file changed
+173
+173
View File
@@ -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 = {