test: cover tests.ascensionSightPenaltySumsAcrossActiveMutators

This commit is contained in:
dualfroz committed 2026-09-11 18:36:20 +02:00
1 parent 1c5b6ab591
commit a9d2900e79
1 file changed
+173
+173
View File
@@ -8779,6 +8779,179 @@ function tests.ascensionEssenceMultiplierAppliesFamineAlone()
eq(Ascension.essenceMultiplier({ "famine" }), 0.5, "famine halves essence income")
end
function tests.ascensionSightPenaltySumsAcrossActiveMutators()
eq(Ascension.sightPenalty(nil), 0, "no active mutators carries no sight penalty")
eq(Ascension.sightPenalty({ "nightblind" }), 2, "nightblind carries its own fixed sight penalty")
end
function tests.ascensionRenownBonusPercentSumsRatherThanCompounds()
eq(Ascension.renownBonusPercent(nil), 0, "no active mutators adds no renown bonus")
eq(Ascension.renownBonusPercent({ "famine" }), 10, "famine alone adds its own fixed bonus")
eq(Ascension.renownBonusPercent({ "famine", "nightblind" }), 20, "two mutators add their bonuses, not compound them")
end
function tests.ascensionApplyToPlayerHalvesMaxHpAndDoublesAttackForGlassCannon()
local player = { maxHp = 30, hp = 30, baseAttack = 5, attack = 5, baseDefense = 1, defense = 1 }
Ascension.applyToPlayer({ "glasscannon" }, player)
eq(player.maxHp, 15, "glass cannon halves max hp")
eq(player.hp, 15, "current hp is clamped down to the new max")
eq(player.attack, 10, "glass cannon doubles attack")
eq(player.baseAttack, 10, "glass cannon doubles base attack alongside the equipped total")
eq(player.defense, 1, "glass cannon leaves defense untouched")
end
function tests.ascensionApplyToPlayerHalvesDefenseForFrail()
local player = { maxHp = 30, hp = 30, baseAttack = 5, attack = 5, baseDefense = 3, defense = 3 }
Ascension.applyToPlayer({ "frail" }, player)
eq(player.defense, 1, "frail floors defense down by half")
eq(player.baseDefense, 1, "frail floors base defense down by half")
eq(player.attack, 5, "frail leaves attack untouched")
end
function tests.gameNewResolvesRequestedMutatorsAgainstMetaAndAppliesTheirDeltas()
local meta = Meta.new()
meta.bestDepth = 4
meta.wins = 1
local game = Game.new(5, { meta = meta, mutators = { "unknownkey", "glasscannon", "frail" } })
eq(#game.ascension, 2, "only the two unlocked, known mutators survive resolution")
eq(game.ascension[1], "frail", "active mutators are ordered by the catalog, not the request")
eq(game.ascension[2], "glasscannon", "active mutators are ordered by the catalog, not the request")
eq(game.player.maxHp, 15, "glasscannon halves the fresh player's max hp")
eq(game.player.defense, 0, "frail halves the fresh player's base defense down from 1")
end
function tests.gameMonsterMeleeDamageStacksBloodboundWithBloodmoonMultiplicatively()
local meta = Meta.new()
meta.bestDepth = 3
local game = Game.new(1, { meta = meta, mutators = { "bloodbound" } })
game.level = arena(21, 11)
game.player.x, game.player.y = 3, 6
game:computeVisible()
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
local before = game.player.hp
game.ascension = {}
game:monsterMelee(orc)
local plainDamage = before - game.player.hp
game.player.hp = before
game.ascension = { "bloodbound" }
game:monsterMelee(orc)
local bloodboundDamage = before - game.player.hp
eq(bloodboundDamage, math.floor(plainDamage * 1.5), "bloodbound alone scales melee damage by its own multiplier")
game.player.hp = before
game.level.event = { key = Events.BLOODMOON, turnsLeft = 5 }
game:monsterMelee(orc)
local stacked = before - game.player.hp
eq(stacked, math.floor(plainDamage * 1.5 * Events.BLOODMOON_MULTIPLIER),
"bloodbound and bloodmoon stack multiplicatively on the same hit")
end
function tests.gameKillEssenceStacksFamineWithRichVeinsMultiplicatively()
local meta = Meta.new()
meta.bestDepth = 2
local game = Game.new(1, { meta = meta, mutators = { "famine" } })
game.level = arena(21, 11)
game.player.x, game.player.y = 3, 6
game:computeVisible()
local rat = Monsters.create(Monsters.byKey.rat, 1, 4, 6, 1)
rat.hp = 1
game.level.entities = { rat }
game.ascension = {}
local before = game.player.essence
game:playerAttack(rat)
local plainEssence = game.player.essence - before
local rat2 = Monsters.create(Monsters.byKey.rat, 2, 4, 6, 1)
rat2.hp = 1
game.level.entities = { rat2 }
game.player.essence = before
game.ascension = { "famine" }
game:playerAttack(rat2)
local famineEssence = game.player.essence - before
eq(famineEssence, math.floor(plainEssence * 0.5), "famine alone halves essence earned from a kill")
local rat3 = Monsters.create(Monsters.byKey.rat, 3, 4, 6, 1)
rat3.hp = 1
game.level.entities = { rat3 }
game.player.essence = before
game.level.event = { key = Events.RICHVEINS, turnsLeft = 5 }
game:playerAttack(rat3)
local stackedEssence = game.player.essence - before
eq(stackedEssence, plainEssence,
"famine's half and rich veins' double multiply to exactly cancel back to the plain amount")
end
function tests.gameEffectiveRadiusStacksNightblindWithHush()
local meta = Meta.new()
meta.bestDepth = 2
local game = Game.new(1, { meta = meta, mutators = { "nightblind" } })
game.ascension = {}
local noAscension = game:effectiveRadius()
game.ascension = { "nightblind" }
eq(game:effectiveRadius(), noAscension - 2, "nightblind alone shrinks the effective sight radius by its own penalty")
game.level.event = { key = Events.HUSH, turnsLeft = 5 }
eq(game:effectiveRadius(), noAscension - 2 - Events.HUSH_SIGHT_PENALTY,
"nightblind and hush both shrink the effective sight radius, summed together")
end
function tests.metaRenownForAddsTheAscensionBonusOnTopOfTheBaseScore()
local stats = { deepestDepth = 5, bossKills = 0, questsCompleted = 0, won = false }
local base = Meta.renownFor(stats)
stats.mutators = { "famine" }
eq(Meta.renownFor(stats), base + (base * 10) // 100, "famine's 10% renown bonus is added on top of the base score")
stats.mutators = { "famine", "nightblind" }
eq(Meta.renownFor(stats), base + (base * 20) // 100, "two mutators' renown bonuses sum before being applied")
end
function tests.ascensionFieldSurvivesASaveLoadRoundTrip()
local meta = Meta.new()
meta.bestDepth = 3
local game = Game.new(4, { meta = meta, mutators = { "bloodbound" } })
local path = os.tmpname()
game:save(path)
local restored = Game.load(path)
os.remove(path)
eq(#restored.ascension, 1, "the active ascension mutator list survives a save/load round trip")
eq(restored.ascension[1], "bloodbound", "the restored mutator key matches what was active")
end
function tests.renderPanelShowsActiveAscensionMutatorsByKey()
local game = stagedGame(1, arena(21, 11), 3, 6)
ok(not contains(Render.panel(game), "ascension"), "no active mutators shows no ascension line at all")
game.ascension = { "famine", "nightblind" }
ok(contains(Render.panel(game), "ascension famine,nightblind"), "active mutators are listed by key in the panel")
end
-- durability.lua: weapon/armor/bow wear and repair.
function tests.durabilityFreshWeaponStartsAtMaxAndIsNotBroken()
local sword = Items.weapon("sword", 4)
eq(sword.durability, Durability.MAX, "a freshly forged weapon starts at max durability")
eq(sword.maxDurability, Durability.MAX, "a freshly forged weapon's max durability matches the catalog default")
ok(not Durability.isBroken(sword), "a freshly forged weapon is not broken")
end
function tests.durabilityWearWeaponDecrementsAndBreaksAtZeroSubtractingItsPower()
local hero = { attack = 5, inventory = {}, kind = "player", name = "you" }
Items.equipWeapon(Items.weapon("sword", 4), hero)
eq(hero.attack, 9, "the sword's power is baked into attack on equip")
local broke
for _ = 1, Durability.MAX - 1 do
broke = Durability.wearWeapon(hero)
ok(broke == nil, "the sword survives every hit before its last")
end
eq(hero.weapon.durability, 1, "one point of durability remains before the final hit")
broke = Durability.wearWeapon(hero)
ok(broke ~= nil, "the hit that empties durability reports a break")
ok(contains(broke, "breaks apart"), "the break line names what happened")
eq(hero.weapon.durability, 0, "durability floors at exactly zero")
eq(hero.attack, 5, "the sword's power is pulled back out of attack the instant it breaks")
end
function tests.durabilityWearWeaponIsANoOpOnceAlreadyBroken()
end
local order = {