test: cover tests.runewordsAttackAndDefenseBonusReadTheWielderSGearLive

This commit is contained in:
dualfroz committed 2026-09-10 19:21:51 +02:00
1 parent 8f7fd7151e
commit 2c6fae4f44
1 file changed
+173
+173
View File
@@ -6530,6 +6530,179 @@ function tests.runewordsActiveWordMatchesOnlyAnExactChain()
Runewords.socket(sword, Runewords.rune("sol")) Runewords.socket(sword, Runewords.rune("sol"))
eq(Runewords.activeWord(sword), nil, "a single rune alone matches no cataloged word") eq(Runewords.activeWord(sword), nil, "a single rune alone matches no cataloged word")
Runewords.socket(sword, Runewords.rune("ith")) Runewords.socket(sword, Runewords.rune("ith"))
local word = Runewords.activeWord(sword)
ok(word ~= nil, "sol then ith completes the Zenith runeword")
eq(word.name, "Zenith", "the matched word is the cataloged Zenith")
Runewords.socket(sword, Runewords.rune("tal"))
eq(Runewords.activeWord(sword), nil, "a third rune growing past Zenith's length breaks its match")
end
function tests.runewordsAttackAndDefenseBonusReadTheWielderSGearLive()
local player = { weapon = Items.weapon("sword", 4), armor = Items.armor("chain mail", 2) }
eq(Runewords.attackBonus(player), 0, "no bonus applies before any runeword completes")
eq(Runewords.defenseBonus(player), 0, "no defense bonus applies before any runeword completes")
Runewords.socket(player.weapon, Runewords.rune("sol"))
Runewords.socket(player.weapon, Runewords.rune("ith"))
eq(Runewords.attackBonus(player), 3, "a completed Zenith on the weapon grants its flat attack bonus")
Runewords.socket(player.armor, Runewords.rune("tal"))
Runewords.socket(player.armor, Runewords.rune("ral"))
eq(Runewords.defenseBonus(player), 3, "a completed Bastion on the armor grants its flat defense bonus")
eq(Runewords.attackBonus(player), 3, "the armor's runeword does not leak an attack bonus")
end
function tests.gameSocketRuneAppliesItsRunewordBonusInActualCombat()
local game = stagedGame(1, arena(21, 11), 3, 6)
Items.equipWeapon(Items.weapon("sword", 4), game.player)
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
orc.defense, orc.baseDefense, orc.hp, orc.maxHp = 0, 0, 40, 40
game.level.entities = { orc }
local before = orc.hp
game:playerAttack(orc)
local plainDamage = before - orc.hp
local game2 = stagedGame(1, arena(21, 11), 3, 6)
Items.equipWeapon(Items.weapon("sword", 4), game2.player)
local orc2 = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
orc2.defense, orc2.baseDefense, orc2.hp, orc2.maxHp = 0, 0, 40, 40
game2.level.entities = { orc2 }
game2.player.inventory = { Runewords.rune("sol"), Runewords.rune("ith") }
ok(game2:socketRune("weapon", 1), "socketing the first rune into the equipped weapon succeeds")
ok(game2:socketRune("weapon", 1), "socketing the second rune completes the runeword")
eq(#game2.player.inventory, 0, "both runes leave the inventory once socketed")
ok(contains(game2.log:last(), "Zenith"), "completing a runeword is announced by name")
local before2 = orc2.hp
game2:playerAttack(orc2)
local runewordDamage = before2 - orc2.hp
ok(runewordDamage > plainDamage, "a completed Zenith runeword deals more damage than the bare weapon")
eq(runewordDamage - plainDamage, 3, "the extra damage matches Zenith's exact flat attack bonus")
end
function tests.gameSocketRuneFailsOnAnUnknownTargetOrANonRuneItem()
local game = stagedGame(1, arena(21, 11), 3, 6)
Items.equipWeapon(Items.weapon("sword", 4), game.player)
game.player.inventory = { Items.potion(10) }
eq(game:socketRune("weapon", 1), false, "socketing a non-rune item fails")
game.player.inventory = { Runewords.rune("sol") }
eq(game:socketRune("armor", 1), false, "socketing into gear the player has not equipped fails")
eq(#game.player.inventory, 1, "a failed socket attempt leaves the rune in inventory")
end
function tests.builderOccasionallyPlacesARuneFromDepthThree()
local level = Builder.build(3, Rng.new(1), {})
local found
for _, ground in ipairs(level.floorItems) do
if ground.item.kind == Items.RUNE then found = ground.item end
end
ok(found ~= nil, "this seed's depth-3 floor places a rune stone")
ok(found.rune ~= nil, "the placed rune names a real catalog key")
end
function tests.runesStayOffTheShallowestTwoFloors()
for seed = 1, 6 do
for depth = 1, 2 do
local level = Builder.build(depth, Rng.new(seed), {})
for _, ground in ipairs(level.floorItems) do
ok(ground.item.kind ~= Items.RUNE, "no rune appears on the shallowest two floors")
end
end
end
end
function tests.runeChainFieldsSurviveSerialization()
local game = Game.new(3)
local sword = Items.weapon("sword", 4)
Items.equipWeapon(sword, game.player)
Runewords.socket(sword, Runewords.rune("sol"))
Runewords.socket(sword, Runewords.rune("ith"))
local path = os.tmpname()
game:save(path)
local restored = Game.load(path)
os.remove(path)
eq(table.concat(restored.player.weapon.runes, "-"), "sol-ith", "a weapon's rune chain survives a save/load round trip")
ok(Runewords.activeWord(restored.player.weapon) ~= nil, "the restored chain still forms its runeword")
end
-- Atmosphere (atmosphere.lua): a single floor-wide weather condition drawn
-- once at generation, distinct from terrain.lua's per-cell hazards - every
-- hook here is a flat modifier applying to the whole floor for its lifetime.
function tests.atmosphereHooksAreZeroForAnyConditionTheyDoNotGovern()
eq(Atmosphere.sightPenalty(Atmosphere.CONSECRATED), 0, "consecrated ground carries no sight penalty")
eq(Atmosphere.regenBonus(Atmosphere.FOGGY), 0, "fog carries no regen bonus")
eq(Atmosphere.monsterAttackBonus(Atmosphere.FOGGY), 0, "fog carries no monster attack bonus")
eq(Atmosphere.sightPenalty(nil), 0, "a clear floor (no weather) carries no sight penalty")
end
function tests.atmosphereFoggyShrinksSightRadiusButNeverBelowOne()
eq(Atmosphere.sightPenalty(Atmosphere.FOGGY), 2, "fog carries a fixed sight penalty")
local game = stagedGame(1, arena(21, 11), 3, 6)
local clear = game:effectiveRadius()
game.level.weather = Atmosphere.FOGGY
eq(game:effectiveRadius(), clear - 2, "fog shaves its fixed penalty off the floor's own sight radius")
local darkGame = stagedGame(1, arenaDark(21, 11), 3, 6)
darkGame.player.light.fuel = 0
darkGame.level.weather = Atmosphere.FOGGY
eq(darkGame:effectiveRadius(), 1, "fog on an already-dark, torchless floor floors out at a minimum radius of one")
end
function tests.atmosphereConsecratedRegeneratesThePlayerEachTurn()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.level.weather = Atmosphere.CONSECRATED
game.player.hp = game.player.maxHp - 10
local before = game.player.hp
game:hungerTorchTick()
eq(game.player.hp, before + 1, "consecrated ground regenerates its fixed amount each turn")
end
function tests.atmosphereConsecratedNeverRegeneratesPastMaxHp()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.level.weather = Atmosphere.CONSECRATED
game.player.hp = game.player.maxHp
game:hungerTorchTick()
eq(game.player.hp, game.player.maxHp, "consecrated regen never lifts hp past its maximum")
end
function tests.builderOccasionallyDrawsEachWeatherConditionFromDepthTwo()
local infested = Builder.build(2, Rng.new(1), {})
eq(infested.weather, Atmosphere.INFESTED, "this seed's depth-2 floor draws an infested reading")
local rat
for _, entity in ipairs(infested.entities) do
if entity.species == "rat" then rat = entity end
end
ok(rat ~= nil, "an infested floor still places its usual monsters")
eq(rat.attack, Monsters.byKey.rat.attack + Atmosphere.monsterAttackBonus(Atmosphere.INFESTED),
"an infested floor bakes its attack bonus into every monster placed on it")
local consecrated = Builder.build(2, Rng.new(3), {})
eq(consecrated.weather, Atmosphere.CONSECRATED, "this seed's depth-2 floor draws a consecrated reading")
local foggy = Builder.build(2, Rng.new(6), {})
eq(foggy.weather, Atmosphere.FOGGY, "this seed's depth-2 floor draws a foggy reading")
end
function tests.weatherNeverAppearsOnTheEntranceFloor()
for seed = 1, 20 do
local level = Builder.build(1, Rng.new(seed), {})
eq(level.weather, nil, "no weather condition is ever drawn for the entrance floor")
end
end
function tests.weatherFieldSurvivesSerialization()
local level = arena(21, 11)
level.weather = Atmosphere.FOGGY
local restored = Level.fromData(level:toData())
eq(restored.weather, Atmosphere.FOGGY, "a floor's weather survives a round trip")
end
function tests.renderPanelReportsTheFloorSWeatherWhenPresent()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.level.weather = Atmosphere.INFESTED
ok(contains(Render.panel(game), "infested"), "the status panel names an active weather condition")
end
-- Biomes: a fixed, seed-independent theme per depth (contrast with
-- Atmosphere's own dice-rolled weather above), read by the builder's biome
-- pass to occasionally reinforce a floor with one more native monster or
end end
local order = { local order = {