test: cover tests.runewordsAttackAndDefenseBonusReadTheWielderSGearLive
This commit is contained in:
1 parent
8f7fd7151e
commit
2c6fae4f44
1 file changed
+173
+173
@@ -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 = {
|
||||||
|
|||||||
Reference in new issue
Block a user