test: cover tests.rangerCamouflageShrinksDetectionOnDarkFloors

This commit is contained in:
dualfroz committed 2026-09-08 22:38:22 +02:00
1 parent 1a36a061ba
commit d46d2a49d7
1 file changed
+173
+173
View File
@@ -2551,6 +2551,179 @@ function tests.rangerMultishotRespectsPhysicalResistanceOnBothSplitArrows()
game.player.inventory[#game.player.inventory + 1] = Items.ammo("arrow", 5) game.player.inventory[#game.player.inventory + 1] = Items.ammo("arrow", 5)
local first = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1) local first = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1)
first.hp, first.maxHp = 100, 100 first.hp, first.maxHp = 100, 100
if resistant then first.resist = { physical = Resistances.RESIST } end
local second = Monsters.create(Monsters.byKey.orc, 2, 3, 8, 1)
second.hp, second.maxHp = 100, 100
game.level.entities = { first, second }
game:useSkill("multishot", first, second)
return 100 - first.hp
end
local plain = primaryDamageDealt(false)
local resisted = primaryDamageDealt(true)
ok(resisted < plain, "a physically resistant target takes measurably less multishot damage than an unresisted one")
eq(resisted, math.floor(plain / 2), "physical resistance halves the split-arrow hit, the same mitigation Resistances.apply gives a single Ranged.fire shot")
end
function tests.rangerCamouflageShrinksDetectionOnDarkFloors()
local game = stagedGame(1, arenaDark(21, 11), 3, 6)
local orc = Monsters.create(Monsters.byKey.orc, 1, 11, 6, 1)
eq(game:canSeePlayer(orc, 10), true, "without camouflage a dark floor does not shrink detection")
game.player.class = "ranger"
game.player.skills.camouflage = true
eq(game:canSeePlayer(orc, 10), false, "camouflage shrinks a monster's detection range on a dark floor")
game.level.dark = false
eq(game:canSeePlayer(orc, 10), true, "camouflage does nothing once the floor is lit")
end
-- Blindness overrides Game:effectiveRadius outright rather than stacking as
-- another penalty term (contrast Atmosphere.sightPenalty/Events.sightPenalty/
-- Ascension.sightPenalty, which all just shave a fixed amount off) - a fully
-- lit, unpenalized floor still clamps to a radius of one the instant the
-- player is blinded, and returns to normal the instant the status is gone.
function tests.effectiveRadiusBlindStatusOverridesLightAndWeatherToAMinimumOfOne()
local game = stagedGame(1, arena(21, 11), 3, 6)
local clear = game:effectiveRadius()
ok(clear > 1, "an unblinded, unpenalized floor has more than a radius of one to begin with")
Status.apply(game.player, Status.BLIND, 6, 1)
eq(game:effectiveRadius(), 1, "a blinded player's sight radius clamps to one regardless of light or weather")
game.player.statuses = {}
eq(game:effectiveRadius(), clear, "sight radius returns to normal once blindness is gone")
end
-- A blinded monster keeps its ordinary listed detection range on paper, but
-- Game:canSeePlayer clamps every one of ai.lua's behaviours (chase/pack/
-- ranged/flee/erratic/tunnel all route through this same helper) down to
-- melee-only awareness while it lasts.
function tests.canSeePlayerBlindedMonsterOnlyDetectsThePlayerAtMeleeRange()
local game = stagedGame(1, arena(21, 11), 3, 6)
local orc = Monsters.create(Monsters.byKey.orc, 1, 8, 6, 1)
eq(game:canSeePlayer(orc, 10), true, "an unblinded monster within its listed range sees the player")
Status.apply(orc, Status.BLIND, 6, 1)
eq(game:canSeePlayer(orc, 10), false, "a blinded monster loses a five-cell-away sighting despite its listed range")
local adjacentOrc = Monsters.create(Monsters.byKey.orc, 2, 4, 6, 1)
Status.apply(adjacentOrc, Status.BLIND, 6, 1)
eq(game:canSeePlayer(adjacentOrc, 10), true, "a blinded monster still senses the player once adjacent")
end
function tests.rangerTrueshotAddsFlatRangedDamage()
local game = stagedGame(4, arena(21, 11), 3, 6)
local base = Monsters.create(Monsters.byKey.orc, 1, 9, 6, 1)
game.level.entities = { base }
game.player.bow = Items.bow("short bow", 2, 6)
game.player.inventory[#game.player.inventory + 1] = Items.ammo("arrow", 2)
game:fireAt(base)
local dmgBase = base.maxHp - base.hp
local game2 = stagedGame(4, arena(21, 11), 3, 6)
game2.player.class = "ranger"
game2.player.skills.trueshot = true
local boosted = Monsters.create(Monsters.byKey.orc, 1, 9, 6, 1)
game2.level.entities = { boosted }
game2.player.bow = Items.bow("short bow", 2, 6)
game2.player.inventory[#game2.player.inventory + 1] = Items.ammo("arrow", 2)
game2:fireAt(boosted)
local dmgTrueshot = boosted.maxHp - boosted.hp
eq(dmgTrueshot, dmgBase + 3, "trueshot adds a flat +3 to every ranged hit")
end
function tests.autopilotLearnsSkillsAutomaticallyAsPointsAllow()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.class = "fighter"
game.player.skillPoints = 1
game:choosePlayerAction()
ok(Skills.has(game.player, "cleave"),
"the autopilot spends a banked point on the first level-eligible skill in class order")
eq(game.player.skillPoints, 0, "learning the skill spends the banked point")
end
function tests.skillFieldsSurviveSerialization()
local game = Game.new(5, { class = "mage" })
game.player.skills.overload = true
game.player.skillCooldowns.meditate = 2
game.player.skillPoints = 3
local path = os.tmpname()
game:save(path)
local restored = Game.load(path)
os.remove(path)
eq(restored.player.class, "mage", "class survives a save/load round trip")
eq(restored.player.skillPoints, 3, "skill points survive a save/load round trip")
eq(Skills.has(restored.player, "overload"), true, "learned skills survive a save/load round trip")
eq(restored.player.skillCooldowns.meditate, 2, "skill cooldowns survive a save/load round trip")
end
function tests.craftingForgeSpendsEssenceForATierOneGem()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.player.essence = Crafting.FORGE_COST - 1
local shortSuccess, shortReason = Crafting.forge(game.player, Crafting.FIRE)
eq(shortSuccess, false, "forging refuses when essence is short of the cost")
eq(shortReason, "not enough essence", "the refusal names the reason")
game.player.essence = Crafting.FORGE_COST
local success, gem = Crafting.forge(game.player, Crafting.FIRE)
ok(success, "forging succeeds with exactly the cost in essence")
eq(gem.tier, 1, "a forged gem starts at tier one")
eq(gem.element, Crafting.FIRE, "the gem carries the requested element")
eq(game.player.essence, 0, "the forge cost is spent")
local badSuccess, badReason = Crafting.forge(game.player, "shadow")
eq(badSuccess, false, "an unknown element is refused")
eq(badReason, "no such element", "the refusal names the reason")
end
function tests.craftingMergeGemsCombinesMatchingPairsIntoTheNextTier()
local success, _, merged = Crafting.mergeGems(Crafting.gem(Crafting.FIRE, 1), Crafting.gem(Crafting.FIRE, 1))
ok(success, "two matching chip gems merge")
eq(merged.tier, 2, "the merged gem is one tier higher")
eq(merged.element, Crafting.FIRE, "the merged gem keeps the shared element")
eq(merged.power, 2, "a gem's power always matches its tier")
local mismatchElement, elementReason = Crafting.mergeGems(Crafting.gem(Crafting.FIRE, 1), Crafting.gem(Crafting.FROST, 1))
eq(mismatchElement, false, "gems of different elements refuse to merge")
eq(elementReason, "elements do not match", "the refusal names the reason")
local mismatchTier, tierReason = Crafting.mergeGems(Crafting.gem(Crafting.FIRE, 1), Crafting.gem(Crafting.FIRE, 2))
eq(mismatchTier, false, "gems of different tiers refuse to merge")
eq(tierReason, "tiers do not match", "the refusal names the reason")
local atCap, capReason = Crafting.mergeGems(
Crafting.gem(Crafting.FIRE, Crafting.MAX_TIER), Crafting.gem(Crafting.FIRE, Crafting.MAX_TIER))
eq(atCap, false, "a gem already at the maximum tier cannot merge further")
eq(capReason, "already at maximum tier", "the refusal names the reason")
end
function tests.craftingSocketAddsPowerAndSwapsRiderOnResocket()
local game = stagedGame(1, arena(21, 11), 3, 6)
local weapon = Items.weapon("dagger", 2)
Items.equipWeapon(weapon, game.player)
local attackBaseline = game.player.attack
ok(Crafting.socket(game.player, weapon, Crafting.gem(Crafting.FIRE, 1)),
"a gem sockets into an equipped weapon")
eq(weapon.power, 3, "the gem's power lands on the item")
eq(game.player.attack, attackBaseline + 1, "socketing an equipped weapon updates attack immediately")
eq(weapon.socket.element, Crafting.FIRE, "the socket records its element")
ok(Crafting.socket(game.player, weapon, Crafting.gem(Crafting.FROST, 2)),
"resocketing replaces the previous gem")
eq(weapon.power, 4, "power reflects only the new gem, not a stack of both")
eq(game.player.attack, attackBaseline + 2, "attack reflects the swap, not an accumulation")
eq(weapon.socket.element, Crafting.FROST, "the new gem's element replaces the old one")
local success, reason = Crafting.socket(game.player, weapon, { kind = Items.WEAPON })
eq(success, false, "a non-gem refuses to socket")
eq(reason, "not a gem", "the refusal names the reason")
end
function tests.craftingTemperRaisesPowerAtRisingCostUpToACap()
local game = stagedGame(1, arena(21, 11), 3, 6)
local weapon = Items.weapon("dagger", 2)
end end
local order = { local order = {