test: cover tests.proficiencyAttackBonusReadsTheHighestRankCrossedFor...

This commit is contained in:
dualfroz committed 2026-09-11 18:26:30 +02:00
1 parent 8613a59377
commit 1c5b6ab591
1 file changed
+173
+173
View File
@@ -8606,6 +8606,179 @@ end
function tests.proficiencyOnMeleeHitAccumulatesAndReturnsOnlyOnARankCrossing() function tests.proficiencyOnMeleeHitAccumulatesAndReturnsOnlyOnARankCrossing()
local entity = {} local entity = {}
for i = 1, Proficiency.RANKS[1].hits - 1 do
local key = Proficiency.onMeleeHit(entity)
eq(key, nil, "no rank crossed yet on hit " .. i)
end
local key, bonus = Proficiency.onMeleeHit(entity)
eq(key, Proficiency.UNARMED, "the rank-crossing hit reports the mastered key")
eq(bonus, Proficiency.RANKS[1].bonus, "the rank-crossing hit reports that rank's own bonus")
local nextKey = Proficiency.onMeleeHit(entity)
eq(nextKey, nil, "the very next hit past a rank reports no further milestone")
end
function tests.proficiencyAttackBonusReadsTheHighestRankCrossedForTheCurrentKeyAlone()
local entity = { weapon = { name = "sword" } }
for _ = 1, Proficiency.RANKS[1].hits do Proficiency.onMeleeHit(entity) end
eq(Proficiency.attackBonus(entity), Proficiency.RANKS[1].bonus, "mastery with the equipped weapon applies")
entity.weapon = { name = "mace" }
eq(Proficiency.attackBonus(entity), 0, "switching to an unmastered weapon drops the bonus entirely")
entity.weapon = { name = "sword" }
eq(Proficiency.attackBonus(entity), Proficiency.RANKS[1].bonus,
"re-equipping the mastered weapon picks its bonus straight back up")
end
function tests.entityEffectiveAttackFoldsInProficiencyAlongsideMomentumAndSanity()
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 carries no mastery bonus")
for _ = 1, Proficiency.RANKS[1].hits do Proficiency.onMeleeHit(entity) end
eq(entity:effectiveAttack(), 6 + Proficiency.RANKS[1].bonus, "a mastered weapon's flat bonus lands on effective attack")
end
function tests.gamePlayerAttackAccumulatesProficiencyAndLogsOnlyOnARankCrossing()
local game = stagedGame(1, arena(21, 11), 3, 6)
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
orc.hp, orc.maxHp = 999, 999
game.level.entities = { orc }
for _ = 1, Proficiency.RANKS[1].hits - 1 do
game:playerAttack(orc)
end
ok(not contains(game.log:last(), "skilled"), "no milestone logged before the rank is actually crossed")
game:playerAttack(orc)
ok(contains(game.log:last(), "skilled"), "crossing a mastery rank is announced as the very last log line")
eq(game.player.proficiency[Proficiency.UNARMED], Proficiency.RANKS[1].hits,
"an unarmed player's landed hits accumulate under the fists key")
end
function tests.renderPanelShowsMasteryOnlyOnceARankIsCrossed()
local game = stagedGame(1, arena(21, 11), 3, 6)
ok(not contains(Render.panel(game), "mastery"), "a fresh player with no mastery shows no mastery line")
for _ = 1, Proficiency.RANKS[1].hits do Proficiency.onMeleeHit(game.player) end
ok(contains(Render.panel(game), string.format("mastery +%d with %s", Proficiency.RANKS[1].bonus, Proficiency.UNARMED)),
"a crossed mastery rank is reported in the status panel")
end
function tests.proficiencyFieldSurvivesSerialization()
local game = Game.new(3)
for _ = 1, Proficiency.RANKS[1].hits do Proficiency.onMeleeHit(game.player) end
local path = os.tmpname()
game:save(path)
local restored = Game.load(path)
os.remove(path)
eq(restored.player.proficiency[Proficiency.UNARMED], Proficiency.RANKS[1].hits,
"the player's accumulated mastery survives a save/load round trip")
end
function tests.proficiencyOnHitWithAndBonusForTrackAnExplicitKeyIndependentOfKeyFor()
local entity = { weapon = { name = "sword" } }
for _ = 1, Proficiency.RANKS[1].hits do Proficiency.onHitWith(entity, "short bow") end
eq(Proficiency.bonusFor(entity, "short bow"), Proficiency.RANKS[1].bonus,
"an explicit key masters independently of whatever keyFor would report")
eq(Proficiency.attackBonus(entity), 0,
"the equipped melee weapon's own mastery is untouched by an explicit-key bow's own hits")
end
function tests.gameFireAtAccumulatesBowSpecificProficiencySeparateFromMelee()
local game = stagedGame(4, arena(21, 11), 3, 6)
local orc = Monsters.create(Monsters.byKey.orc, 1, 9, 6, 1)
orc.hp, orc.maxHp = 999, 999
game.level.entities = { orc }
game.player.bow = Items.bow("short bow", 2, 6)
game.player.inventory[#game.player.inventory + 1] = Items.ammo("arrow", Proficiency.RANKS[1].hits + 1)
for i = 1, Proficiency.RANKS[1].hits - 1 do
game:fireAt(orc)
ok(not contains(game.log:last(), "skilled"), "no milestone logged before the rank is actually crossed")
end
game:fireAt(orc)
ok(contains(game.log:last(), "skilled"), "crossing a bow mastery rank is announced as the shot's own last line")
eq(game.player.proficiency["short bow"], Proficiency.RANKS[1].hits, "bow hits accumulate under the bow's own name")
eq(game.player.proficiency[Proficiency.UNARMED], nil, "firing a bow never touches the unarmed melee key")
end
function tests.metaGravesFieldDefaultsToEmptyAndSurvivesASaveLoadRoundTrip()
local fresh = Meta.load("/tmp/warren_meta_does_not_exist_98765.sav")
eq(#fresh.graves, 0, "a freshly defaulted meta state has no recorded graves")
local meta = Meta.new()
Graves.record(meta, { className = "rogue", depth = 4, gold = 30, essence = 2, turn = 15 })
local path = os.tmpname()
Meta.save(meta, path)
local restored = Meta.load(path)
os.remove(path)
eq(#restored.graves, 1, "a recorded grave survives a save/load round trip")
eq(restored.graves[1].className, "rogue", "a restored grave keeps its own fallen class")
eq(restored.graves[1].depth, 4, "a restored grave keeps its own depth")
end
-- ascension.lua: opt-in run mutators traded for extra renown.
function tests.ascensionCatalogEntriesHaveUniqueKeysAndAtLeastOneEffectHook()
local seen = {}
for _, def in ipairs(Ascension.MUTATORS) do
ok(not seen[def.key], def.key .. " appears only once in the catalog")
seen[def.key] = true
local hasHook = def.damageMultiplier or def.essenceMultiplier or def.sightPenalty or def.applyToPlayer
ok(hasHook ~= nil, def.key .. " carries at least one actual gameplay effect")
end
end
function tests.ascensionIsUnlockedRequiresMeetingBothDepthAndWinRequirements()
local meta = Meta.new()
meta.bestDepth = 2
ok(Ascension.isUnlocked(meta, "famine"), "famine unlocks once best depth reaches 2")
ok(not Ascension.isUnlocked(meta, "bloodbound"), "bloodbound still needs depth 3")
ok(not Ascension.isUnlocked(meta, "glasscannon"), "glasscannon still needs a first win")
meta.wins = 1
ok(Ascension.isUnlocked(meta, "glasscannon"), "glasscannon unlocks once a win is banked")
eq(Ascension.isUnlocked(meta, "madeupkey"), false, "an unknown key is never unlocked")
end
function tests.ascensionIsUnlockedWithNoMetaLocksEveryCatalogedMutator()
for _, key in ipairs(Ascension.order) do
ok(not Ascension.isUnlocked(nil, key), key .. " stays locked for a run started without a meta file")
end
end
function tests.ascensionResolveDropsUnknownAndStillLockedKeys()
local meta = Meta.new()
meta.bestDepth = 2
local active = Ascension.resolve(meta, { "famine", "bloodbound", "madeupkey" })
eq(#active, 1, "only the one unlocked, known key survives resolution")
eq(active[1], "famine", "famine is the survivor")
end
function tests.ascensionResolveDeduplicatesAndReordersToCatalogOrder()
local meta = Meta.new()
meta.bestDepth = 4
local active = Ascension.resolve(meta, { "nightblind", "famine", "nightblind", "famine" })
eq(#active, 2, "a key repeated in the request is not duplicated in the resolved list")
eq(active[1], "famine", "the resolved order follows the catalog, not the request")
eq(active[2], "nightblind", "the resolved order follows the catalog, not the request")
end
function tests.ascensionResolveWithNoRequestReturnsEmpty()
eq(#Ascension.resolve(Meta.new(), nil), 0, "a nil request resolves to no active mutators")
eq(#Ascension.resolve(Meta.new(), {}), 0, "an empty request resolves to no active mutators")
end
function tests.ascensionIsActiveTrueOnlyForKeysInTheActiveList()
local active = { "famine", "frail" }
ok(Ascension.isActive(active, "famine"), "famine is reported active")
ok(not Ascension.isActive(active, "bloodbound"), "bloodbound is reported inactive")
ok(not Ascension.isActive(nil, "famine"), "a nil active list reports every key inactive")
end
function tests.ascensionDamageMultiplierMultipliesAcrossActiveMutators()
eq(Ascension.damageMultiplier(nil), 1, "no active mutators leaves damage unscaled")
eq(Ascension.damageMultiplier({ "famine" }), 1, "a mutator with no damage hook leaves damage unscaled")
eq(Ascension.damageMultiplier({ "bloodbound" }), 1.5, "bloodbound scales damage by its own fixed multiplier")
end
function tests.ascensionEssenceMultiplierAppliesFamineAlone()
eq(Ascension.essenceMultiplier(nil), 1, "no active mutators leaves essence unscaled")
eq(Ascension.essenceMultiplier({ "bloodbound" }), 1, "a mutator with no essence hook leaves essence unscaled")
eq(Ascension.essenceMultiplier({ "famine" }), 0.5, "famine halves essence income")
end
end end
local order = { local order = {