647 lines
28 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|