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