diablo2-web/tests/skills/sor/adv-cold-m2-projectiles-str...

647 lines
28 KiB
TypeScript

/**
* Adversarial Stress-Testing & Boundary Verification Suite
* Milestone 2: Cold Projectiles & Cold Mastery (Ice Blast #199, Glacial Spike #209, Cold Mastery #219)
*
* Verification Categories:
* 1. Monotonicity & Continuity Sweeps (slvl 1 to 99 across all 5 level progression bands)
* 2. Strict Freeze Duration Monotonicity (slvl 1 to 99)
* 3. Linear Synergy Stress Testing (0 to 60 synergy points, overrides & object structures)
* 4. Extreme Boundaries & Robustness (slvl <= 0, fractional, NaN, negative, huge values > 1000)
* 5. Cold Mastery Resistance Pipeline & Floor Clamping (-100% cap, immunity bypass, broken immunities)
* 6. Differential Oracle Testing (Naive band summation vs closed-form implementations)
* 7. System Integration & Invariant Enforcement (BATCH1_SKILLS length, mana cost curves, 0 cooldown)
*/
import { describe, expect, it } from 'vitest'
import {
calculateIceBlastDamage,
calculateIceBlastFreezeLength,
calculateGlacialSpikeDamage,
calculateGlacialSpikeFreezeLength,
calculateColdMasteryPierce,
} from '../../../src/game/engine/missile-engine.ts'
import {
calculateSkillDamage,
getSkillManaCost,
getSkillCooldownTicks,
BATCH1_SKILLS,
} from '../../../src/game/skills.ts'
import { computeEffectiveResistance } from '../../../src/game/engine/combat-pipeline.ts'
describe('Adversarial Stress Test: Cold Projectiles & Mastery Milestone 2', () => {
// =========================================================================
// 1. Monotonicity & Continuity Sweeps (slvl 1 to 99 across 5 Progression Bands)
// =========================================================================
describe('1. Monotonicity & Continuity Sweeps (slvl 1 to 99)', () => {
it('Ice Blast: min <= max and strict positive monotonicity across all 99 levels', () => {
let prevDamage = calculateIceBlastDamage(1)
expect(prevDamage.min).toBe(8)
expect(prevDamage.max).toBe(12)
expect(prevDamage.min).toBeLessThanOrEqual(prevDamage.max)
for (let slvl = 2; slvl <= 99; slvl++) {
const current = calculateIceBlastDamage(slvl)
// 1. Min damage must never exceed max damage
expect(current.min).toBeLessThanOrEqual(current.max)
// 2. Strict monotonicity: every single level up MUST increase damage
expect(current.min).toBeGreaterThan(prevDamage.min)
expect(current.max).toBeGreaterThan(prevDamage.max)
prevDamage = current
}
})
it('Ice Blast: progression band slope verification across all 5 bands', () => {
// Band 1: slvl 1-8 -> +7.0 min, +7.5 max per level
for (let slvl = 1; slvl <= 7; slvl++) {
const dCurrent = calculateIceBlastDamage(slvl)
const dNext = calculateIceBlastDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(7.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(7.5, 5)
}
// Band 2: slvl 8-16 -> +14.0 min, +14.5 max per level
for (let slvl = 8; slvl <= 15; slvl++) {
const dCurrent = calculateIceBlastDamage(slvl)
const dNext = calculateIceBlastDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(14.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(14.5, 5)
}
// Band 3: slvl 16-22 -> +21.0 min, +21.5 max per level
for (let slvl = 16; slvl <= 21; slvl++) {
const dCurrent = calculateIceBlastDamage(slvl)
const dNext = calculateIceBlastDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(21.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(21.5, 5)
}
// Band 4: slvl 22-28 -> +28.0 min, +28.5 max per level
for (let slvl = 22; slvl <= 27; slvl++) {
const dCurrent = calculateIceBlastDamage(slvl)
const dNext = calculateIceBlastDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(28.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(28.5, 5)
}
// Band 5: slvl 28-99 -> +35.0 min, +35.5 max per level
for (let slvl = 28; slvl <= 98; slvl++) {
const dCurrent = calculateIceBlastDamage(slvl)
const dNext = calculateIceBlastDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(35.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(35.5, 5)
}
})
it('Glacial Spike: min <= max and strict positive monotonicity across all 99 levels', () => {
let prevDamage = calculateGlacialSpikeDamage(1)
expect(prevDamage.min).toBe(16)
expect(prevDamage.max).toBe(24)
expect(prevDamage.min).toBeLessThanOrEqual(prevDamage.max)
for (let slvl = 2; slvl <= 99; slvl++) {
const current = calculateGlacialSpikeDamage(slvl)
// 1. Min damage must never exceed max damage
expect(current.min).toBeLessThanOrEqual(current.max)
// 2. Strict monotonicity
expect(current.min).toBeGreaterThan(prevDamage.min)
expect(current.max).toBeGreaterThan(prevDamage.max)
prevDamage = current
}
})
it('Glacial Spike: progression band slope verification across all 5 bands', () => {
// Band 1: slvl 1-8 -> +7.0 min, +7.5 max per level
for (let slvl = 1; slvl <= 7; slvl++) {
const dCurrent = calculateGlacialSpikeDamage(slvl)
const dNext = calculateGlacialSpikeDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(7.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(7.5, 5)
}
// Band 2: slvl 8-16 -> +13.0 min, +13.5 max per level
for (let slvl = 8; slvl <= 15; slvl++) {
const dCurrent = calculateGlacialSpikeDamage(slvl)
const dNext = calculateGlacialSpikeDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(13.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(13.5, 5)
}
// Band 3: slvl 16-22 -> +14.0 min, +14.5 max per level
for (let slvl = 16; slvl <= 21; slvl++) {
const dCurrent = calculateGlacialSpikeDamage(slvl)
const dNext = calculateGlacialSpikeDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(14.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(14.5, 5)
}
// Band 4: slvl 22-28 -> +15.0 min, +15.5 max per level
for (let slvl = 22; slvl <= 27; slvl++) {
const dCurrent = calculateGlacialSpikeDamage(slvl)
const dNext = calculateGlacialSpikeDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(15.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(15.5, 5)
}
// Band 5: slvl 28-99 -> +16.0 min, +16.5 max per level
for (let slvl = 28; slvl <= 98; slvl++) {
const dCurrent = calculateGlacialSpikeDamage(slvl)
const dNext = calculateGlacialSpikeDamage(slvl + 1)
expect(dNext.min - dCurrent.min).toBeCloseTo(16.0, 5)
expect(dNext.max - dCurrent.max).toBeCloseTo(16.5, 5)
}
})
it('Cold Mastery: strict positive monotonicity across all 99 levels (+5% per level)', () => {
let prevPierce = calculateColdMasteryPierce(1)
expect(prevPierce).toBe(20)
for (let slvl = 2; slvl <= 99; slvl++) {
const currentPierce = calculateColdMasteryPierce(slvl)
expect(currentPierce).toBeGreaterThan(prevPierce)
expect(currentPierce - prevPierce).toBe(5)
prevPierce = currentPierce
}
expect(calculateColdMasteryPierce(1)).toBe(20)
expect(calculateColdMasteryPierce(10)).toBe(65)
expect(calculateColdMasteryPierce(20)).toBe(115)
expect(calculateColdMasteryPierce(30)).toBe(165)
expect(calculateColdMasteryPierce(99)).toBe(510)
})
})
// =========================================================================
// 2. Strict Freeze Duration Monotonicity (slvl 1 to 99)
// =========================================================================
describe('2. Strict Freeze Duration Monotonicity', () => {
it('Ice Blast: freeze length increases strictly by +5 frames per level across slvl 1 to 99', () => {
let prevLen = calculateIceBlastFreezeLength(1)
expect(prevLen).toBe(75) // 3.0s
for (let slvl = 2; slvl <= 99; slvl++) {
const currentLen = calculateIceBlastFreezeLength(slvl)
expect(currentLen).toBeGreaterThan(prevLen)
expect(currentLen - prevLen).toBe(5)
prevLen = currentLen
}
expect(calculateIceBlastFreezeLength(1)).toBe(75)
expect(calculateIceBlastFreezeLength(10)).toBe(120)
expect(calculateIceBlastFreezeLength(20)).toBe(170)
expect(calculateIceBlastFreezeLength(99)).toBe(75 + 98 * 5)
})
it('Ice Blast: freeze length strictly monotonic with non-zero synergy across slvl 1 to 99', () => {
for (const syn of [1, 10, 20, 60]) {
let prevLen = calculateIceBlastFreezeLength(1, syn)
for (let slvl = 2; slvl <= 99; slvl++) {
const currentLen = calculateIceBlastFreezeLength(slvl, syn)
expect(currentLen).toBeGreaterThan(prevLen)
prevLen = currentLen
}
}
})
it('Glacial Spike: freeze length increases strictly by +3 frames per level across slvl 1 to 99', () => {
let prevLen = calculateGlacialSpikeFreezeLength(1)
expect(prevLen).toBe(50) // 2.0s
for (let slvl = 2; slvl <= 99; slvl++) {
const currentLen = calculateGlacialSpikeFreezeLength(slvl)
expect(currentLen).toBeGreaterThan(prevLen)
expect(currentLen - prevLen).toBe(3)
prevLen = currentLen
}
expect(calculateGlacialSpikeFreezeLength(1)).toBe(50)
expect(calculateGlacialSpikeFreezeLength(10)).toBe(77)
expect(calculateGlacialSpikeFreezeLength(20)).toBe(107)
expect(calculateGlacialSpikeFreezeLength(99)).toBe(50 + 98 * 3)
})
it('Glacial Spike: freeze length strictly monotonic with non-zero synergy across slvl 1 to 99', () => {
for (const syn of [1, 10, 20, 60]) {
let prevLen = calculateGlacialSpikeFreezeLength(1, syn)
for (let slvl = 2; slvl <= 99; slvl++) {
const currentLen = calculateGlacialSpikeFreezeLength(slvl, syn)
expect(currentLen).toBeGreaterThan(prevLen)
prevLen = currentLen
}
}
})
})
// =========================================================================
// 3. Synergy Stress Testing (0 to 60 Points, Linearity & Objects)
// =========================================================================
describe('3. Synergy Stress Testing', () => {
it('Ice Blast: 0 to 60 synergy points scaling (+8% per point) is strictly linear and positive', () => {
const baseDmg = calculateIceBlastDamage(20, 0)
expect(baseDmg).toEqual({ min: 253, max: 266.5 })
for (let syn = 0; syn <= 60; syn++) {
const current = calculateIceBlastDamage(20, syn)
const expectedMultiplier = 1.0 + syn * 0.08
const expectedMin = Math.floor((253 * expectedMultiplier + 1e-9) * 10) / 10
const expectedMax = Math.floor((266.5 * expectedMultiplier + 1e-9) * 10) / 10
expect(current.min).toBeCloseTo(expectedMin, 1)
expect(current.max).toBeCloseTo(expectedMax, 1)
expect(current.min).toBeLessThanOrEqual(current.max)
}
// Check key milestones
expect(calculateIceBlastDamage(20, 0)).toEqual({ min: 253, max: 266.5 })
expect(calculateIceBlastDamage(20, 20)).toEqual({ min: 657.8, max: 692.9 }) // (1 + 1.6 = 2.6x)
expect(calculateIceBlastDamage(20, 60)).toEqual({ min: 1467.4, max: 1545.7 }) // (1 + 4.8 = 5.8x)
})
it('Ice Blast: multi-skill synergy object decomposition parity', () => {
// 4 synergy skills: Ice Bolt, Blizzard, Frozen Orb, Glacial Spike
const combined = calculateIceBlastDamage(20, {
iceBolt: 15,
blizzard: 15,
frozenOrb: 15,
glacialSpike: 15,
})
// 15 * 4 = 60 total points -> exactly identical to passing numeric 60
const numeric = calculateIceBlastDamage(20, 60)
expect(combined).toEqual(numeric)
// Verify each individual synergy contributes identically (symmetric +8%)
const viaIceBolt = calculateIceBlastDamage(20, { iceBolt: 20 })
const viaBlizzard = calculateIceBlastDamage(20, { blizzard: 20 })
const viaFrozenOrb = calculateIceBlastDamage(20, { frozenOrb: 20 })
const viaGlacialSpike = calculateIceBlastDamage(20, { glacialSpike: 20 })
expect(viaIceBolt).toEqual(viaBlizzard)
expect(viaBlizzard).toEqual(viaFrozenOrb)
expect(viaFrozenOrb).toEqual(viaGlacialSpike)
})
it('Ice Blast: synergy override options (synPctPerLvl and synMultiplier)', () => {
// Custom % per level override
const customPct = calculateIceBlastDamage(20, 10, { synPctPerLvl: 12 })
// 10 * 12% = +120% -> 2.2x
expect(customPct.min).toBeCloseTo(253 * 2.2, 1)
expect(customPct.max).toBeCloseTo(266.5 * 2.2, 1)
// Direct multiplier override
const directMult = calculateIceBlastDamage(20, undefined, { synMultiplier: 3.5 })
expect(directMult.min).toBeCloseTo(253 * 3.5, 1)
expect(directMult.max).toBeCloseTo(266.5 * 3.5, 1)
})
it('Glacial Spike: 0 to 60 synergy points scaling (+5% per point) is strictly linear and positive', () => {
const baseDmg = calculateGlacialSpikeDamage(20, 0)
expect(baseDmg).toEqual({ min: 225, max: 242.5 })
for (let syn = 0; syn <= 60; syn++) {
const current = calculateGlacialSpikeDamage(20, syn)
const expectedMultiplier = 1.0 + syn * 0.05
const expectedMin = Math.floor((225 * expectedMultiplier + 1e-9) * 10) / 10
const expectedMax = Math.floor((242.5 * expectedMultiplier + 1e-9) * 10) / 10
expect(current.min).toBeCloseTo(expectedMin, 1)
expect(current.max).toBeCloseTo(expectedMax, 1)
expect(current.min).toBeLessThanOrEqual(current.max)
}
// Check key milestones
expect(calculateGlacialSpikeDamage(20, 0)).toEqual({ min: 225, max: 242.5 })
expect(calculateGlacialSpikeDamage(20, 20)).toEqual({ min: 450, max: 485 }) // (1 + 1.0 = 2.0x)
expect(calculateGlacialSpikeDamage(20, 60)).toEqual({ min: 900, max: 970 }) // (1 + 3.0 = 4.0x)
})
it('Glacial Spike: multi-skill synergy object decomposition parity', () => {
// 4 synergy skills: Ice Bolt, Ice Blast, Blizzard, Frozen Orb
const combined = calculateGlacialSpikeDamage(20, {
iceBolt: 15,
iceBlast: 15,
blizzard: 15,
frozenOrb: 15,
})
// 15 * 4 = 60 total points -> exactly identical to passing numeric 60
const numeric = calculateGlacialSpikeDamage(20, 60)
expect(combined).toEqual(numeric)
// Verify each individual synergy contributes identically (symmetric +5%)
const viaIceBolt = calculateGlacialSpikeDamage(20, { iceBolt: 20 })
const viaIceBlast = calculateGlacialSpikeDamage(20, { iceBlast: 20 })
const viaBlizzard = calculateGlacialSpikeDamage(20, { blizzard: 20 })
const viaFrozenOrb = calculateGlacialSpikeDamage(20, { frozenOrb: 20 })
expect(viaIceBolt).toEqual(viaIceBlast)
expect(viaIceBlast).toEqual(viaBlizzard)
expect(viaBlizzard).toEqual(viaFrozenOrb)
})
it('Glacial Spike: freeze length synergy scaling with Blizzard (+3% per point)', () => {
const baseLen = calculateGlacialSpikeFreezeLength(20, 0)
expect(baseLen).toBe(107)
for (let syn = 0; syn <= 60; syn++) {
const len = calculateGlacialSpikeFreezeLength(20, syn)
const expected = Math.floor((107 * (100 + syn * 3)) / 100)
expect(len).toBe(expected)
}
// Object format parity
expect(calculateGlacialSpikeFreezeLength(20, { blizzard: 20 })).toBe(
calculateGlacialSpikeFreezeLength(20, 20),
)
})
})
// =========================================================================
// 4. Extreme Boundaries & Robustness Testing
// =========================================================================
describe('4. Extreme Boundaries & Robustness', () => {
it('handles slvl <= 0 gracefully without exceptions or negative damage', () => {
for (const invalidLvl of [0, -1, -50, -9999]) {
const ibDmg = calculateIceBlastDamage(invalidLvl)
expect(ibDmg).toEqual({ min: 8, max: 12 }) // Clamps to lvl 1 base
const ibFreeze = calculateIceBlastFreezeLength(invalidLvl)
expect(ibFreeze).toBe(75) // Clamps to lvl 1 base
const gsDmg = calculateGlacialSpikeDamage(invalidLvl)
expect(gsDmg).toEqual({ min: 16, max: 24 }) // Clamps to lvl 1 base
const gsFreeze = calculateGlacialSpikeFreezeLength(invalidLvl)
expect(gsFreeze).toBe(50) // Clamps to lvl 1 base
const cmPierce = calculateColdMasteryPierce(invalidLvl)
expect(cmPierce).toBe(0) // Passive returns 0 when inactive/unlearned
// Mana costs for invalid levels clamp to level 1
expect(getSkillManaCost(45, invalidLvl)).toBe(6.0)
expect(getSkillManaCost(55, invalidLvl)).toBe(10.0)
expect(getSkillManaCost(65, invalidLvl)).toBe(0)
}
})
it('handles NaN, Infinity, -Infinity, null, and undefined safely', () => {
for (const nonFinite of [NaN, Infinity, -Infinity, undefined as any, null as any]) {
const ibDmg = calculateIceBlastDamage(nonFinite)
expect(Number.isFinite(ibDmg.min)).toBe(true)
expect(Number.isFinite(ibDmg.max)).toBe(true)
expect(ibDmg).toEqual({ min: 8, max: 12 })
const ibFreeze = calculateIceBlastFreezeLength(nonFinite)
expect(Number.isFinite(ibFreeze)).toBe(true)
expect(ibFreeze).toBe(75)
const gsDmg = calculateGlacialSpikeDamage(nonFinite)
expect(Number.isFinite(gsDmg.min)).toBe(true)
expect(Number.isFinite(gsDmg.max)).toBe(true)
expect(gsDmg).toEqual({ min: 16, max: 24 })
const gsFreeze = calculateGlacialSpikeFreezeLength(nonFinite)
expect(Number.isFinite(gsFreeze)).toBe(true)
expect(gsFreeze).toBe(50)
const cmPierce = calculateColdMasteryPierce(nonFinite)
expect(cmPierce).toBe(0)
expect(getSkillManaCost(45, nonFinite)).toBe(6.0)
expect(getSkillManaCost(55, nonFinite)).toBe(10.0)
expect(getSkillManaCost(65, nonFinite)).toBe(0)
}
})
it('handles fractional slvl values by rounding down consistently (Math.floor)', () => {
// 5.2 and 5.9 both evaluate to slvl 5
expect(calculateIceBlastDamage(5.2)).toEqual(calculateIceBlastDamage(5))
expect(calculateIceBlastDamage(5.9)).toEqual(calculateIceBlastDamage(5))
expect(calculateIceBlastFreezeLength(5.2)).toBe(calculateIceBlastFreezeLength(5))
expect(calculateIceBlastFreezeLength(5.9)).toBe(calculateIceBlastFreezeLength(5))
expect(calculateGlacialSpikeDamage(5.2)).toEqual(calculateGlacialSpikeDamage(5))
expect(calculateGlacialSpikeDamage(5.9)).toEqual(calculateGlacialSpikeDamage(5))
expect(calculateGlacialSpikeFreezeLength(5.2)).toBe(calculateGlacialSpikeFreezeLength(5))
expect(calculateGlacialSpikeFreezeLength(5.9)).toBe(calculateGlacialSpikeFreezeLength(5))
expect(calculateColdMasteryPierce(5.2)).toBe(calculateColdMasteryPierce(5))
expect(calculateColdMasteryPierce(5.9)).toBe(calculateColdMasteryPierce(5))
})
it('handles very large values (> 1000) stably without overflow or precision collapse', () => {
const largeLvl = 1000
const ibDmg = calculateIceBlastDamage(largeLvl)
expect(Number.isFinite(ibDmg.min)).toBe(true)
expect(Number.isFinite(ibDmg.max)).toBe(true)
expect(ibDmg.min).toBeLessThanOrEqual(ibDmg.max)
expect(ibDmg.min).toBeGreaterThan(10000)
const gsDmg = calculateGlacialSpikeDamage(largeLvl)
expect(Number.isFinite(gsDmg.min)).toBe(true)
expect(Number.isFinite(gsDmg.max)).toBe(true)
expect(gsDmg.min).toBeLessThanOrEqual(gsDmg.max)
expect(gsDmg.min).toBeGreaterThan(10000)
const cmPierce = calculateColdMasteryPierce(largeLvl)
expect(cmPierce).toBe(20 + 999 * 5) // 5015% pierce
})
it('handles adversarial negative synergy values and clamped multiplier overrides', () => {
// Negative synergy numbers clamped to 0
expect(calculateIceBlastDamage(20, -50)).toEqual(calculateIceBlastDamage(20, 0))
expect(calculateGlacialSpikeDamage(20, -50)).toEqual(calculateGlacialSpikeDamage(20, 0))
expect(calculateIceBlastFreezeLength(20, -50)).toBe(calculateIceBlastFreezeLength(20, 0))
expect(calculateGlacialSpikeFreezeLength(20, -50)).toBe(calculateGlacialSpikeFreezeLength(20, 0))
// Negative synergy object members clamped to 0
expect(
calculateIceBlastDamage(20, { iceBolt: -10, blizzard: -20, frozenOrb: -5 } as any),
).toEqual(calculateIceBlastDamage(20, 0))
expect(
calculateGlacialSpikeDamage(20, { iceBolt: -10, blizzard: -20, frozenOrb: -5 } as any),
).toEqual(calculateGlacialSpikeDamage(20, 0))
// Multiplier overrides: negative or zero multiplier clamped to >= 0
expect(calculateIceBlastDamage(20, undefined, { synMultiplier: -2 })).toEqual({ min: 0, max: 0 })
expect(calculateIceBlastDamage(20, undefined, { synMultiplier: 0 })).toEqual({ min: 0, max: 0 })
expect(calculateGlacialSpikeDamage(20, undefined, { synMultiplier: -2 })).toEqual({ min: 0, max: 0 })
expect(calculateGlacialSpikeDamage(20, undefined, { synMultiplier: 0 })).toEqual({ min: 0, max: 0 })
})
})
// =========================================================================
// 5. Cold Mastery Combat Pipeline Adversarial Scenarios
// =========================================================================
describe('5. Cold Mastery Combat Pipeline Adversarial Scenarios', () => {
it('authentically clamps effective resistance at -100% floor for vulnerable enemies', () => {
// Slvl 20 Cold Mastery: -115% pierce
const cm20 = calculateColdMasteryPierce(20)
expect(cm20).toBe(115)
// Case 1: Enemy has 0% Cold Res -> 0 - 115 = -115%, but clamped at -100%
const res0 = computeEffectiveResistance({ baseRes: 0, passiveEnemyPierce: cm20 })
expect(res0.effectiveRes).toBe(-100)
expect(res0.isImmune).toBe(false)
// Case 2: Enemy has -50% Cold Res -> -50 - 115 = -165%, clamped at -100%
const resNeg50 = computeEffectiveResistance({ baseRes: -50, passiveEnemyPierce: cm20 })
expect(resNeg50.effectiveRes).toBe(-100)
// Case 3: Enemy has 50% Cold Res -> 50 - 115 = -65% (within [-100, 99])
const res50 = computeEffectiveResistance({ baseRes: 50, passiveEnemyPierce: cm20 })
expect(res50.effectiveRes).toBe(-65)
expect(res50.isImmune).toBe(false)
})
it('Cold Mastery alone CANNOT pierce Cold Immune monsters (res >= 100%)', () => {
const cm20 = calculateColdMasteryPierce(20) // 115% pierce
const cm99 = calculateColdMasteryPierce(99) // 510% pierce
// Target with 100% Cold Res (immune)
const res100 = computeEffectiveResistance({ baseRes: 100, passiveEnemyPierce: cm20 })
expect(res100.isImmune).toBe(true)
expect(res100.effectiveRes).toBe(100)
// Target with 140% Cold Res (immune) with slvl 99 Cold Mastery
const res140 = computeEffectiveResistance({ baseRes: 140, passiveEnemyPierce: cm99 })
expect(res140.isImmune).toBe(true)
expect(res140.effectiveRes).toBe(100)
})
it('Broken Cold Immunities receive 100% Cold Mastery pierce without 1/5 penalty', () => {
// Target with 110% Cold Res
// Conviction Level 25: -150% pierce
// When baseRes >= 100%, Conviction applies at 1/5th effectiveness -> -30%
// New resistance = 110 - 30 = 80% (< 100% -> immunity broken!)
// Once broken, Cold Mastery (slvl 20 = -115%) applies at full 100% effectiveness:
// Final effective resistance = 80 - 115 = -35%
const cm20 = calculateColdMasteryPierce(20)
const brokenResult = computeEffectiveResistance({
baseRes: 110,
convictionPierce: 150,
passiveEnemyPierce: cm20,
})
expect(brokenResult.isImmune).toBe(false)
expect(brokenResult.convictionAndLRApplied).toBe(30)
expect(brokenResult.resAfterConvictionLR).toBe(80)
expect(brokenResult.effectiveRes).toBe(-35)
})
it('Unbreakable Cold Immunities stay immune and bypass Cold Mastery entirely', () => {
// Target with 140% Cold Res
// Conviction Level 25 (-150% -> 1/5 = -30%)
// Res after Conviction = 140 - 30 = 110% (still >= 100% -> immune remains unbroken!)
// Cold Mastery must NOT apply!
const cm20 = calculateColdMasteryPierce(20)
const unbrokenResult = computeEffectiveResistance({
baseRes: 140,
convictionPierce: 150,
passiveEnemyPierce: cm20,
})
expect(unbrokenResult.isImmune).toBe(true)
expect(unbrokenResult.convictionAndLRApplied).toBe(30)
expect(unbrokenResult.resAfterConvictionLR).toBe(110)
expect(unbrokenResult.effectiveRes).toBe(100)
})
})
// =========================================================================
// 6. Differential Oracle Testing (Naive Band Summation vs Implementation)
// =========================================================================
describe('6. Differential Oracle Testing', () => {
// Naive brute-force simulator for 1.13c 5-band table evaluation
function naiveBandEvaluator(
baseMin: number,
baseMax: number,
minSteps: number[],
maxSteps: number[],
slvl: number,
): { min: number; max: number } {
const lvl = Math.max(1, Math.floor(slvl))
let curMin = baseMin
let curMax = baseMax
for (let l = 2; l <= lvl; l++) {
let stepIdx = 0
if (l >= 2 && l <= 8) stepIdx = 0
else if (l >= 9 && l <= 16) stepIdx = 1
else if (l >= 17 && l <= 22) stepIdx = 2
else if (l >= 23 && l <= 28) stepIdx = 3
else stepIdx = 4
curMin += minSteps[stepIdx]
curMax += maxSteps[stepIdx]
}
return { min: curMin / 2, max: curMax / 2 }
}
it('Ice Blast: matches naive brute-force 5-band oracle across all 99 levels', () => {
const minSteps = [14, 28, 42, 56, 70]
const maxSteps = [15, 29, 43, 57, 71]
for (let slvl = 1; slvl <= 99; slvl++) {
const oracle = naiveBandEvaluator(16, 24, minSteps, maxSteps, slvl)
const actual = calculateIceBlastDamage(slvl)
expect(actual.min).toBe(oracle.min)
expect(actual.max).toBe(oracle.max)
}
})
it('Glacial Spike: matches naive brute-force 5-band oracle across all 99 levels', () => {
const minSteps = [14, 26, 28, 30, 32]
const maxSteps = [15, 27, 29, 31, 33]
for (let slvl = 1; slvl <= 99; slvl++) {
const oracle = naiveBandEvaluator(32, 48, minSteps, maxSteps, slvl)
const actual = calculateGlacialSpikeDamage(slvl)
expect(actual.min).toBe(oracle.min)
expect(actual.max).toBe(oracle.max)
}
})
})
// =========================================================================
// 7. System Integration & Invariant Enforcement
// =========================================================================
describe('7. System Integration & Invariant Enforcement', () => {
it('BATCH1_SKILLS length remains strictly 38', () => {
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
})
it('calculateSkillDamage dispatcher cleanly routes skills 45, 55, and 65', () => {
expect(calculateSkillDamage(45, 10, { iceBolt: 5 })).toEqual(
calculateIceBlastDamage(10, { iceBolt: 5 }),
)
expect(calculateSkillDamage('iceblast', 10, 5)).toEqual(
calculateIceBlastDamage(10, 5),
)
expect(calculateSkillDamage(55, 10, { blizzard: 5 })).toEqual(
calculateGlacialSpikeDamage(10, { blizzard: 5 }),
)
expect(calculateSkillDamage('glacialspike', 10, 5)).toEqual(
calculateGlacialSpikeDamage(10, 5),
)
// Cold Mastery is passive, returns { min: 0, max: 0 }
expect(calculateSkillDamage(65, 20)).toEqual({ min: 0, max: 0 })
expect(calculateSkillDamage('coldmastery', 20)).toEqual({ min: 0, max: 0 })
})
it('mana costs and cooldown ticks adhere strictly to 1.13c specification', () => {
// Ice Blast: (12 + slvl - 1) * 0.5
expect(getSkillManaCost(45, 1)).toBe(6.0)
expect(getSkillManaCost(45, 10)).toBe(10.5)
expect(getSkillManaCost(45, 20)).toBe(15.5)
// Glacial Spike: (20 + slvl - 1) * 0.5
expect(getSkillManaCost(55, 1)).toBe(10.0)
expect(getSkillManaCost(55, 10)).toBe(14.5)
expect(getSkillManaCost(55, 20)).toBe(19.5)
// Cold Mastery: passive 0 mana
expect(getSkillManaCost(65, 1)).toBe(0)
expect(getSkillManaCost(65, 20)).toBe(0)
// 0 cooldown ticks
expect(getSkillCooldownTicks(45)).toBe(0)
expect(getSkillCooldownTicks(55)).toBe(0)
expect(getSkillCooldownTicks(65)).toBe(0)
})
})
})