850 lines
39 KiB
TypeScript
850 lines
39 KiB
TypeScript
/**
|
|
* Adversarial Stress & Correctness Verification Suite — Milestone 1: Three Sorceress Cold Armors
|
|
* Skills Under Test:
|
|
* - Frozen Armor (Skill #040, State 10, Group 1)
|
|
* - Shiver Armor (Skill #050, State 88, Group 1)
|
|
* - Chilling Armor (Skill #060, State 20, Group 1)
|
|
*
|
|
* Target Commit: e7d105d
|
|
* Author: Challenger 1 (teamwork_preview_challenger)
|
|
* Ground Truth Reference: Diablo II v1.13c (D2Game.dll, D2Common.dll, Skills.txt, Missiles.txt, Overlay.txt, States.txt)
|
|
*
|
|
* Verification Dimensions:
|
|
* 1. Exhaustive Mathematical Boundary Sweeps (slvl 1..99 x synergies 0..20 / 0..40):
|
|
* - Monotonicity of defense bonus, duration, cold damage, and freeze/chill lengths.
|
|
* - Zero NaN, zero undefined, and positive non-zero outputs for all valid inputs.
|
|
* - Exact formula validation against 1.13c ground truth analytical models.
|
|
* 2. Degenerate, Extreme & Adversarial Inputs:
|
|
* - Non-finite, negative, zero, and float slvl handling.
|
|
* - Missing, malformed, negative, and mixed synergy structures.
|
|
* 3. State Group 1 Mutual Exclusion Stress Testing:
|
|
* - Rapid successive casting bursts & 100-step randomized permutations.
|
|
* - Invariant: strictly 1 active Group 1 state at all times.
|
|
* - Level-override rules (different armor overwrites regardless of slvl; same armor rejects lower slvl).
|
|
* - Clean eviction of StatList defense modifiers upon armor transition.
|
|
* 4. Reactive Trigger Adversarial Edge Cases:
|
|
* - Frozen Armor: damagedinmelee with physical damage = 0 must NOT freeze.
|
|
* - Chilling Armor: hitbymissile with returnFire = false must NOT reflect.
|
|
* - Shiver Armor: attackedinmelee triggers proactively before attack connects regardless of damage.
|
|
* - Event cross-trigger isolation (no cross-firing between melee and missile events).
|
|
* 5. Full Invariant & System Integrity Checks:
|
|
* - BATCH1_SKILLS length strictly 38.
|
|
* - Mana costs: flat 7 (40), flat 11 (50), flat 17 (60).
|
|
* - Cooldown ticks: 0 for all three armors.
|
|
* - calculateSkillDamage routing parity.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
calculateFrozenArmorDefense,
|
|
calculateFrozenArmorDuration,
|
|
calculateFrozenArmorFreezeLength,
|
|
calculateShiverArmorDefense,
|
|
calculateShiverArmorDuration,
|
|
calculateShiverArmorDamage,
|
|
calculateShiverArmorChillLength,
|
|
calculateChillingArmorDefense,
|
|
calculateChillingArmorDuration,
|
|
calculateChillingArmorDamage,
|
|
calculateChillingArmorChillLength,
|
|
calculateColdArmorDuration,
|
|
calculateSkillDamage,
|
|
getSkillManaCost,
|
|
getSkillCooldownTicks,
|
|
getSkillCastOverlay,
|
|
BATCH1_SKILLS,
|
|
} from '../src/game/skills.ts'
|
|
import { getSharedDataRegistry } from '../src/game/engine/data-registry.ts'
|
|
import { UnitStatList, FIXED_ONE } from '../src/game/engine/stat-list.ts'
|
|
import { StateBus } from '../src/game/engine/state-bus.ts'
|
|
import { OVERLAY_METAS } from '../src/render/overlays-meta.ts'
|
|
import { MISSILE_METAS } from '../src/render/missiles-meta.ts'
|
|
|
|
describe('Challenger 1 Stress Suite — Three Sorceress Cold Armors (M1: #040, #050, #060)', () => {
|
|
// =========================================================================
|
|
// Dimension 1: Exhaustive Mathematical Boundary Sweeps (slvl 1..99 x syn 0..40)
|
|
// =========================================================================
|
|
describe('Dimension 1: Exhaustive Mathematical Boundary Sweeps & Monotonicity', () => {
|
|
describe('Frozen Armor (Skill 40)', () => {
|
|
it('evaluates % Enhanced Defense across slvl 1..99 with strict monotonicity and zero NaN', () => {
|
|
let prevDef = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const def = calculateFrozenArmorDefense(slvl)
|
|
expect(Number.isFinite(def)).toBe(true)
|
|
expect(Number.isNaN(def)).toBe(false)
|
|
expect(def).toBeGreaterThan(0)
|
|
// 1.13c formula: 30 + (slvl - 1) * 5
|
|
expect(def).toBe(30 + (slvl - 1) * 5)
|
|
expect(def).toBeGreaterThan(prevDef)
|
|
prevDef = def
|
|
}
|
|
expect(calculateFrozenArmorDefense(1)).toBe(30)
|
|
expect(calculateFrozenArmorDefense(10)).toBe(75)
|
|
expect(calculateFrozenArmorDefense(20)).toBe(125)
|
|
expect(calculateFrozenArmorDefense(99)).toBe(520)
|
|
})
|
|
|
|
it('evaluates Duration across slvl 1..99 and syn 0..40 with strict monotonicity and zero NaN', () => {
|
|
// Sweep 99 * 41 = 4059 test cases
|
|
for (let syn = 0; syn <= 40; syn++) {
|
|
let prevDur = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const dur = calculateFrozenArmorDuration(slvl, syn)
|
|
expect(Number.isFinite(dur)).toBe(true)
|
|
expect(Number.isNaN(dur)).toBe(false)
|
|
expect(dur).toBeGreaterThan(0)
|
|
// 1.13c formula: 3000 + (slvl - 1) * 300 + syn * 250
|
|
const expected = 3000 + (slvl - 1) * 300 + syn * 250
|
|
expect(dur).toBe(expected)
|
|
expect(dur).toBeGreaterThan(prevDur)
|
|
prevDur = dur
|
|
}
|
|
}
|
|
|
|
// Synergy monotonicity at fixed slvl
|
|
for (let slvl = 1; slvl <= 20; slvl++) {
|
|
let prevDur = 0
|
|
for (let syn = 0; syn <= 40; syn++) {
|
|
const dur = calculateFrozenArmorDuration(slvl, syn)
|
|
expect(dur).toBeGreaterThan(prevDur)
|
|
prevDur = dur
|
|
}
|
|
}
|
|
|
|
// Spot-checks
|
|
expect(calculateFrozenArmorDuration(1, 0)).toBe(3000) // 120s
|
|
expect(calculateFrozenArmorDuration(10, 0)).toBe(5700) // 228s
|
|
expect(calculateFrozenArmorDuration(20, 0)).toBe(8700) // 348s
|
|
expect(calculateFrozenArmorDuration(20, 40)).toBe(18700) // 748s
|
|
expect(calculateFrozenArmorDuration(99, 40)).toBe(42400) // 1696s
|
|
})
|
|
|
|
it('evaluates Freeze Length across slvl 1..99 and syn 0..40 with monotonicity and zero NaN', () => {
|
|
for (let syn = 0; syn <= 40; syn++) {
|
|
let prevFreeze = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const frz = calculateFrozenArmorFreezeLength(slvl, syn)
|
|
expect(Number.isFinite(frz)).toBe(true)
|
|
expect(Number.isNaN(frz)).toBe(false)
|
|
expect(frz).toBeGreaterThan(0)
|
|
// 1.13c formula: Math.floor((30 + (slvl - 1) * 3) * (100 + syn * 5) / 100)
|
|
const expected = Math.floor((30 + (slvl - 1) * 3) * (100 + syn * 5) / 100)
|
|
expect(frz).toBe(expected)
|
|
expect(frz).toBeGreaterThan(prevFreeze)
|
|
prevFreeze = frz
|
|
}
|
|
}
|
|
|
|
// Synergy monotonicity at fixed slvl
|
|
for (let slvl = 1; slvl <= 20; slvl++) {
|
|
let prevFreeze = 0
|
|
for (let syn = 0; syn <= 40; syn++) {
|
|
const frz = calculateFrozenArmorFreezeLength(slvl, syn)
|
|
expect(frz).toBeGreaterThanOrEqual(prevFreeze)
|
|
prevFreeze = frz
|
|
}
|
|
}
|
|
|
|
// Spot-checks
|
|
expect(calculateFrozenArmorFreezeLength(1, 0)).toBe(30) // 1.20s
|
|
expect(calculateFrozenArmorFreezeLength(10, 0)).toBe(57) // 2.28s
|
|
expect(calculateFrozenArmorFreezeLength(20, 0)).toBe(87) // 3.48s
|
|
expect(calculateFrozenArmorFreezeLength(20, 40)).toBe(261) // 10.44s
|
|
expect(calculateFrozenArmorFreezeLength(99, 40)).toBe(972) // 38.88s
|
|
})
|
|
|
|
it('asserts Frozen Armor deals strictly 0 damage across slvl 1..99', () => {
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const dmg = calculateSkillDamage(40, slvl)
|
|
expect(dmg).toEqual({ min: 0, max: 0 })
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Shiver Armor (Skill 50)', () => {
|
|
it('evaluates % Enhanced Defense across slvl 1..99 with strict monotonicity and zero NaN', () => {
|
|
let prevDef = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const def = calculateShiverArmorDefense(slvl)
|
|
expect(Number.isFinite(def)).toBe(true)
|
|
expect(Number.isNaN(def)).toBe(false)
|
|
expect(def).toBeGreaterThan(0)
|
|
// 1.13c formula: 45 + (slvl - 1) * 6
|
|
expect(def).toBe(45 + (slvl - 1) * 6)
|
|
expect(def).toBeGreaterThan(prevDef)
|
|
prevDef = def
|
|
}
|
|
expect(calculateShiverArmorDefense(1)).toBe(45)
|
|
expect(calculateShiverArmorDefense(10)).toBe(99)
|
|
expect(calculateShiverArmorDefense(20)).toBe(159)
|
|
expect(calculateShiverArmorDefense(99)).toBe(633)
|
|
})
|
|
|
|
it('evaluates Duration across slvl 1..99 and syn 0..40 with strict monotonicity and zero NaN', () => {
|
|
for (let syn = 0; syn <= 40; syn++) {
|
|
let prevDur = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const dur = calculateShiverArmorDuration(slvl, syn)
|
|
expect(Number.isFinite(dur)).toBe(true)
|
|
expect(Number.isNaN(dur)).toBe(false)
|
|
expect(dur).toBeGreaterThan(0)
|
|
const expected = 3000 + (slvl - 1) * 300 + syn * 250
|
|
expect(dur).toBe(expected)
|
|
expect(dur).toBeGreaterThan(prevDur)
|
|
prevDur = dur
|
|
}
|
|
}
|
|
expect(calculateShiverArmorDuration(1, 0)).toBe(3000)
|
|
expect(calculateShiverArmorDuration(20, 0)).toBe(8700)
|
|
expect(calculateShiverArmorDuration(20, 40)).toBe(18700)
|
|
})
|
|
|
|
it('evaluates Cold Damage across slvl 1..99 and syn 0..40 with strict monotonicity and zero NaN', () => {
|
|
for (let syn = 0; syn <= 40; syn += 5) {
|
|
let prevMin = 0
|
|
let prevMax = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const dmg = calculateShiverArmorDamage(slvl, { frozenArmor: syn, chillingArmor: 0 })
|
|
expect(Number.isFinite(dmg.min)).toBe(true)
|
|
expect(Number.isFinite(dmg.max)).toBe(true)
|
|
expect(Number.isNaN(dmg.min)).toBe(false)
|
|
expect(Number.isNaN(dmg.max)).toBe(false)
|
|
expect(dmg.min).toBeGreaterThan(0)
|
|
expect(dmg.max).toBeGreaterThanOrEqual(dmg.min)
|
|
expect(dmg.min).toBeGreaterThan(prevMin)
|
|
expect(dmg.max).toBeGreaterThan(prevMax)
|
|
prevMin = dmg.min
|
|
prevMax = dmg.max
|
|
}
|
|
}
|
|
|
|
// Synergy monotonicity at fixed slvl
|
|
for (let slvl = 1; slvl <= 20; slvl += 5) {
|
|
let prevMin = 0
|
|
for (let syn = 0; syn <= 40; syn++) {
|
|
const dmg = calculateShiverArmorDamage(slvl, syn)
|
|
expect(dmg.min).toBeGreaterThanOrEqual(prevMin)
|
|
prevMin = dmg.min
|
|
}
|
|
}
|
|
|
|
// Authentic 1.13c spot-checks (HitShift = 7 -> divide by 2)
|
|
expect(calculateShiverArmorDamage(1, 0)).toEqual({ min: 6, max: 8 })
|
|
expect(calculateShiverArmorDamage(10, 0)).toEqual({ min: 26, max: 32.5 })
|
|
expect(calculateShiverArmorDamage(20, 0)).toEqual({ min: 60, max: 71.5 })
|
|
// +40 syn points via object (+360% -> multiplier 4.6x): 60 * 4.6 = 276.0, 71.5 * 4.6 = 328.9
|
|
expect(calculateShiverArmorDamage(20, { frozenArmor: 20, chillingArmor: 20 })).toEqual({ min: 276.0, max: 328.9 })
|
|
// +40 syn points via numeric synergy points (40 pts -> 1 + 40*0.09 = 4.6x)
|
|
expect(calculateShiverArmorDamage(20, 40)).toEqual({ min: 276.0, max: 328.9 })
|
|
// +40 syn points via options.synMultiplier (4.6x)
|
|
expect(calculateShiverArmorDamage(20, undefined, { synMultiplier: 4.6 })).toEqual({ min: 276.0, max: 328.9 })
|
|
})
|
|
|
|
it('evaluates Chill Length across slvl 1..99 and syn 0..40 with piecewise monotonicity and zero NaN', () => {
|
|
for (let syn = 0; syn <= 40; syn += 10) {
|
|
let prevChill = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const chill = calculateShiverArmorChillLength(slvl, syn)
|
|
expect(Number.isFinite(chill)).toBe(true)
|
|
expect(Number.isNaN(chill)).toBe(false)
|
|
expect(chill).toBeGreaterThan(0)
|
|
expect(chill).toBeGreaterThanOrEqual(prevChill)
|
|
prevChill = chill
|
|
}
|
|
}
|
|
|
|
// Level band step checks without synergy:
|
|
// slvl 1-8: flat 100 frames
|
|
for (let lvl = 1; lvl <= 8; lvl++) {
|
|
expect(calculateShiverArmorChillLength(lvl, 0)).toBe(100)
|
|
}
|
|
// slvl 9-16: +25 frames / level
|
|
expect(calculateShiverArmorChillLength(9, 0)).toBe(125)
|
|
expect(calculateShiverArmorChillLength(10, 0)).toBe(150)
|
|
expect(calculateShiverArmorChillLength(16, 0)).toBe(300)
|
|
// slvl 17+: +50 frames / level
|
|
expect(calculateShiverArmorChillLength(17, 0)).toBe(350)
|
|
expect(calculateShiverArmorChillLength(20, 0)).toBe(500)
|
|
expect(calculateShiverArmorChillLength(99, 0)).toBe(4450)
|
|
|
|
// With +40 syn (+280% factor = 3.8x): 500 * 3.8 = 1900
|
|
expect(calculateShiverArmorChillLength(20, 40)).toBe(1900)
|
|
expect(calculateShiverArmorChillLength(20, { frozenArmor: 20, chillingArmor: 20 })).toBe(1900)
|
|
})
|
|
})
|
|
|
|
describe('Chilling Armor (Skill 60)', () => {
|
|
it('evaluates % Enhanced Defense across slvl 1..99 with strict monotonicity and zero NaN', () => {
|
|
let prevDef = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const def = calculateChillingArmorDefense(slvl)
|
|
expect(Number.isFinite(def)).toBe(true)
|
|
expect(Number.isNaN(def)).toBe(false)
|
|
expect(def).toBeGreaterThan(0)
|
|
expect(def).toBe(45 + (slvl - 1) * 5)
|
|
expect(def).toBeGreaterThan(prevDef)
|
|
prevDef = def
|
|
}
|
|
expect(calculateChillingArmorDefense(1)).toBe(45)
|
|
expect(calculateChillingArmorDefense(10)).toBe(90)
|
|
expect(calculateChillingArmorDefense(20)).toBe(140)
|
|
expect(calculateChillingArmorDefense(99)).toBe(535)
|
|
})
|
|
|
|
it('evaluates Duration across slvl 1..99 and syn 0..40 with strict monotonicity and zero NaN', () => {
|
|
for (let syn = 0; syn <= 40; syn++) {
|
|
let prevDur = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const dur = calculateChillingArmorDuration(slvl, syn)
|
|
expect(Number.isFinite(dur)).toBe(true)
|
|
expect(Number.isNaN(dur)).toBe(false)
|
|
expect(dur).toBeGreaterThan(0)
|
|
const expected = 3600 + (slvl - 1) * 150 + syn * 250
|
|
expect(dur).toBe(expected)
|
|
expect(dur).toBeGreaterThan(prevDur)
|
|
prevDur = dur
|
|
}
|
|
}
|
|
expect(calculateChillingArmorDuration(1, 0)).toBe(3600) // 144s
|
|
expect(calculateChillingArmorDuration(10, 0)).toBe(4950) // 198s
|
|
expect(calculateChillingArmorDuration(20, 0)).toBe(6450) // 258s
|
|
expect(calculateChillingArmorDuration(20, 40)).toBe(16450) // 658s
|
|
expect(calculateChillingArmorDuration(99, 40)).toBe(28300) // 1132s
|
|
})
|
|
|
|
it('evaluates Retaliatory Bolt Cold Damage across slvl 1..99 and syn 0..40 with monotonicity and zero NaN', () => {
|
|
for (let syn = 0; syn <= 40; syn += 5) {
|
|
let prevMin = 0
|
|
let prevMax = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const dmg = calculateChillingArmorDamage(slvl, { frozenArmor: syn, shiverArmor: 0 })
|
|
expect(Number.isFinite(dmg.min)).toBe(true)
|
|
expect(Number.isFinite(dmg.max)).toBe(true)
|
|
expect(Number.isNaN(dmg.min)).toBe(false)
|
|
expect(Number.isNaN(dmg.max)).toBe(false)
|
|
expect(dmg.min).toBeGreaterThan(0)
|
|
expect(dmg.max).toBeGreaterThanOrEqual(dmg.min)
|
|
expect(dmg.min).toBeGreaterThan(prevMin)
|
|
expect(dmg.max).toBeGreaterThan(prevMax)
|
|
prevMin = dmg.min
|
|
prevMax = dmg.max
|
|
}
|
|
}
|
|
|
|
// Spot-checks
|
|
expect(calculateChillingArmorDamage(1, 0)).toEqual({ min: 4, max: 6 })
|
|
expect(calculateChillingArmorDamage(10, 0)).toEqual({ min: 15, max: 21.5 })
|
|
expect(calculateChillingArmorDamage(20, 0)).toEqual({ min: 39, max: 50.5 })
|
|
// +40 syn points via object (+280% -> multiplier 3.8x): 39 * 3.8 = 148.2, 50.5 * 3.8 = 191.9
|
|
expect(calculateChillingArmorDamage(20, { frozenArmor: 20, shiverArmor: 20 })).toEqual({ min: 148.2, max: 191.9 })
|
|
// +40 syn points via numeric synergy points (40 pts -> 1 + 40*0.07 = 3.8x)
|
|
expect(calculateChillingArmorDamage(20, 40)).toEqual({ min: 148.2, max: 191.9 })
|
|
// +40 syn points via options.synMultiplier (3.8x)
|
|
expect(calculateChillingArmorDamage(20, undefined, { synMultiplier: 3.8 })).toEqual({ min: 148.2, max: 191.9 })
|
|
})
|
|
|
|
it('evaluates Chill Length across slvl 1..99 and syn 0..40 with piecewise monotonicity and zero NaN', () => {
|
|
for (let syn = 0; syn <= 40; syn += 10) {
|
|
let prevChill = 0
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
const chill = calculateChillingArmorChillLength(slvl, syn)
|
|
expect(Number.isFinite(chill)).toBe(true)
|
|
expect(Number.isNaN(chill)).toBe(false)
|
|
expect(chill).toBeGreaterThan(0)
|
|
expect(chill).toBeGreaterThanOrEqual(prevChill)
|
|
prevChill = chill
|
|
}
|
|
}
|
|
|
|
// Band step checks without synergy:
|
|
for (let lvl = 1; lvl <= 8; lvl++) {
|
|
expect(calculateChillingArmorChillLength(lvl, 0)).toBe(100)
|
|
}
|
|
expect(calculateChillingArmorChillLength(9, 0)).toBe(125)
|
|
expect(calculateChillingArmorChillLength(10, 0)).toBe(150)
|
|
expect(calculateChillingArmorChillLength(16, 0)).toBe(300)
|
|
// slvl 17+: +25 frames / level
|
|
expect(calculateChillingArmorChillLength(17, 0)).toBe(325)
|
|
expect(calculateChillingArmorChillLength(20, 0)).toBe(400)
|
|
expect(calculateChillingArmorChillLength(99, 0)).toBe(2375)
|
|
|
|
// With +40 syn (+400% factor = 5.0x): 400 * 5.0 = 2000
|
|
expect(calculateChillingArmorChillLength(20, 40)).toBe(2000)
|
|
expect(calculateChillingArmorChillLength(20, { frozenArmor: 20, shiverArmor: 20 })).toBe(2000)
|
|
})
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 2: Degenerate, Extreme & Adversarial Inputs
|
|
// =========================================================================
|
|
describe('Dimension 2: Degenerate, Extreme & Adversarial Inputs', () => {
|
|
it('clamps negative, zero, and non-integer slvl gracefully without throwing or returning NaN', () => {
|
|
const dirtyLvls = [0, -1, -99, 0.5, 12.8, NaN, Infinity, -Infinity]
|
|
for (const slvl of dirtyLvls) {
|
|
// Frozen Armor
|
|
const fDef = calculateFrozenArmorDefense(slvl)
|
|
const fDur = calculateFrozenArmorDuration(slvl)
|
|
const fFrz = calculateFrozenArmorFreezeLength(slvl)
|
|
expect(Number.isFinite(fDef)).toBe(true)
|
|
expect(Number.isFinite(fDur)).toBe(true)
|
|
expect(Number.isFinite(fFrz)).toBe(true)
|
|
|
|
// Shiver Armor
|
|
const sDef = calculateShiverArmorDefense(slvl)
|
|
const sDur = calculateShiverArmorDuration(slvl)
|
|
const sDmg = calculateShiverArmorDamage(slvl)
|
|
const sChl = calculateShiverArmorChillLength(slvl)
|
|
expect(Number.isFinite(sDef)).toBe(true)
|
|
expect(Number.isFinite(sDur)).toBe(true)
|
|
expect(Number.isFinite(sDmg.min)).toBe(true)
|
|
expect(Number.isFinite(sDmg.max)).toBe(true)
|
|
expect(Number.isFinite(sChl)).toBe(true)
|
|
|
|
// Chilling Armor
|
|
const cDef = calculateChillingArmorDefense(slvl)
|
|
const cDur = calculateChillingArmorDuration(slvl)
|
|
const cDmg = calculateChillingArmorDamage(slvl)
|
|
const cChl = calculateChillingArmorChillLength(slvl)
|
|
expect(Number.isFinite(cDef)).toBe(true)
|
|
expect(Number.isFinite(cDur)).toBe(true)
|
|
expect(Number.isFinite(cDmg.min)).toBe(true)
|
|
expect(Number.isFinite(cDmg.max)).toBe(true)
|
|
expect(Number.isFinite(cChl)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('handles degenerate synergy input shapes (undefined, empty, invalid objects)', () => {
|
|
const benignSynergies = [undefined, {}, { fake: 99 }, null as any]
|
|
for (const syn of benignSynergies) {
|
|
expect(() => calculateFrozenArmorDuration(20, syn)).not.toThrow()
|
|
expect(() => calculateShiverArmorDamage(20, syn)).not.toThrow()
|
|
expect(() => calculateChillingArmorDamage(20, syn)).not.toThrow()
|
|
expect(calculateFrozenArmorDuration(20, syn)).toBe(8700)
|
|
expect(calculateShiverArmorDamage(20, syn)).toEqual({ min: 60, max: 71.5 })
|
|
expect(calculateChillingArmorDamage(20, syn)).toEqual({ min: 39, max: 50.5 })
|
|
}
|
|
|
|
// Damage functions safely default to multiplier 1.0 when negative or non-finite numbers are passed
|
|
expect(calculateShiverArmorDamage(20, -10)).toEqual({ min: 60, max: 71.5 })
|
|
expect(calculateShiverArmorDamage(20, NaN)).toEqual({ min: 60, max: 71.5 })
|
|
expect(calculateShiverArmorDamage(20, Infinity)).toEqual({ min: 60, max: 71.5 })
|
|
|
|
expect(calculateChillingArmorDamage(20, -10)).toEqual({ min: 39, max: 50.5 })
|
|
expect(calculateChillingArmorDamage(20, NaN)).toEqual({ min: 39, max: 50.5 })
|
|
expect(calculateChillingArmorDamage(20, Infinity)).toEqual({ min: 39, max: 50.5 })
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 3: State Group 1 Mutual Exclusion Stress Testing
|
|
// =========================================================================
|
|
describe('Dimension 3: State Group 1 Mutual Exclusion Stress Testing', () => {
|
|
it('strictly maintains exactly 1 active Cold Armor during rapid alternating casts', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const bus = new StateBus(stats, registry)
|
|
|
|
const armors: { name: string; id: number }[] = [
|
|
{ name: 'frozenarmor', id: 40 },
|
|
{ name: 'shiverarmor', id: 50 },
|
|
{ name: 'chillingarmor', id: 60 },
|
|
]
|
|
|
|
// 30 cycles of Frozen -> Shiver -> Chilling (90 casts total)
|
|
for (let cycle = 0; cycle < 30; cycle++) {
|
|
for (const armor of armors) {
|
|
const res = bus.applyState({
|
|
stateNameOrId: armor.name,
|
|
sourceSkillId: armor.id,
|
|
slvl: 20,
|
|
stats: { skill_armor_percent: 100 + armor.id },
|
|
})
|
|
expect(res.applied).toBe(true)
|
|
|
|
// Invariant 1: Exactly 1 state with group === 1
|
|
const activeGroup1 = bus.getActiveEntries().filter(e => e.group === 1)
|
|
expect(activeGroup1.length).toBe(1)
|
|
expect(activeGroup1[0]!.stateName).toBe(armor.name)
|
|
|
|
// Invariant 2: hasState confirms only current armor is active
|
|
for (const other of armors) {
|
|
if (other.name === armor.name) {
|
|
expect(bus.hasState(other.name)).toBe(true)
|
|
} else {
|
|
expect(bus.hasState(other.name)).toBe(false)
|
|
}
|
|
}
|
|
|
|
// Invariant 3: statList modifiers from previous cold armor are completely removed
|
|
const activeMods = stats.getActiveModifiers()
|
|
const armorMods = activeMods.filter(m => armors.some(a => a.name === m.stateName))
|
|
expect(armorMods.length).toBe(1)
|
|
expect(armorMods[0]!.stateName).toBe(armor.name)
|
|
expect(armorMods[0]!.stats['skill_armor_percent']).toBe(100 + armor.id)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('preserves single-state invariant across 100 randomized rapid casting bursts', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const bus = new StateBus(stats, registry)
|
|
|
|
const armors = ['frozenarmor', 'shiverarmor', 'chillingarmor']
|
|
let lastCast = ''
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
const pick = armors[Math.floor(Math.random() * armors.length)]!
|
|
bus.applyState({
|
|
stateNameOrId: pick,
|
|
slvl: 10 + (i % 20),
|
|
})
|
|
lastCast = pick
|
|
|
|
const group1Entries = bus.getActiveEntries().filter(e => e.group === 1)
|
|
expect(group1Entries.length).toBe(1)
|
|
expect(group1Entries[0]!.stateName).toBe(lastCast)
|
|
}
|
|
})
|
|
|
|
it('enforces cross-level eviction: lower-slvl different armor evicts higher-slvl armor', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const bus = new StateBus(stats, registry)
|
|
|
|
// Start with Chilling Armor slvl 50
|
|
bus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 50 })
|
|
expect(bus.hasState('chillingarmor')).toBe(true)
|
|
expect(bus.getState('chillingarmor')?.slvl).toBe(50)
|
|
|
|
// Cast Frozen Armor slvl 1 -> must EVIC Chilling Armor
|
|
bus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 1 })
|
|
expect(bus.hasState('chillingarmor')).toBe(false)
|
|
expect(bus.hasState('frozenarmor')).toBe(true)
|
|
expect(bus.getState('frozenarmor')?.slvl).toBe(1)
|
|
|
|
// Cast Shiver Armor slvl 1 -> must EVICT Frozen Armor
|
|
bus.applyState({ stateNameOrId: 'shiverarmor', sourceSkillId: 50, slvl: 1 })
|
|
expect(bus.hasState('frozenarmor')).toBe(false)
|
|
expect(bus.hasState('shiverarmor')).toBe(true)
|
|
expect(bus.getState('shiverarmor')?.slvl).toBe(1)
|
|
})
|
|
|
|
it('enforces same-armor level rules: higher slvl overwrites; lower slvl is rejected', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const bus = new StateBus(stats, registry)
|
|
|
|
// Apply Frozen Armor slvl 20 with 3000 frames duration
|
|
bus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20, durationFrames: 3000 })
|
|
expect(bus.getState('frozenarmor')?.slvl).toBe(20)
|
|
|
|
// Attempt to cast Frozen Armor slvl 5 while slvl 20 is active -> rejected
|
|
const lowerRes = bus.applyState({
|
|
stateNameOrId: 'frozenarmor',
|
|
sourceSkillId: 40,
|
|
slvl: 5,
|
|
durationFrames: 3000,
|
|
})
|
|
expect(lowerRes.applied).toBe(false)
|
|
expect(bus.getState('frozenarmor')?.slvl).toBe(20)
|
|
|
|
// Cast Frozen Armor slvl 30 -> accepted and upgraded
|
|
const higherRes = bus.applyState({
|
|
stateNameOrId: 'frozenarmor',
|
|
sourceSkillId: 40,
|
|
slvl: 30,
|
|
durationFrames: 6000,
|
|
})
|
|
expect(higherRes.applied).toBe(true)
|
|
expect(bus.getState('frozenarmor')?.slvl).toBe(30)
|
|
})
|
|
|
|
it('expires cold armor cleanly on clock tick and clears group 1 state', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const bus = new StateBus(stats, registry)
|
|
|
|
bus.applyState({
|
|
stateNameOrId: 'frozenarmor',
|
|
sourceSkillId: 40,
|
|
slvl: 1,
|
|
durationFrames: 3000,
|
|
})
|
|
expect(bus.hasState('frozenarmor')).toBe(true)
|
|
|
|
// Advance frame clock past 3000
|
|
const expired = bus.tick(3001)
|
|
expect(expired).toContain('frozenarmor')
|
|
expect(bus.hasState('frozenarmor')).toBe(false)
|
|
expect(bus.getActiveEntries().filter(e => e.group === 1).length).toBe(0)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 4: Reactive Trigger Adversarial Edge Cases
|
|
// =========================================================================
|
|
describe('Dimension 4: Reactive Trigger Adversarial Edge Cases', () => {
|
|
describe('Frozen Armor (Skill 40) Edge Cases', () => {
|
|
it('freezes attacker when damagedinmelee with physical damage > 0', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
const attackerBus = new StateBus(attackerStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
|
|
|
|
const outcome = defenderBus.triggerReactiveEvents({
|
|
event: 'damagedinmelee',
|
|
incomingPhys256: 100 * FIXED_ONE,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
|
|
expect(outcome.attackerStatesApplied).toContain('freeze')
|
|
expect(attackerBus.hasState('freeze')).toBe(true)
|
|
// Frozen armor deals 0 damage to attacker
|
|
expect(outcome.attackerColdDamage256).toBe(0)
|
|
expect(outcome.reflectedPhys256).toBe(0)
|
|
})
|
|
|
|
it('does NOT freeze attacker if physical damage is 0 (e.g. 100% absorbed/miss/blocked)', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
const attackerBus = new StateBus(attackerStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
|
|
|
|
const outcome = defenderBus.triggerReactiveEvents({
|
|
event: 'damagedinmelee',
|
|
incomingPhys256: 0,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
|
|
expect(outcome.attackerStatesApplied).not.toContain('freeze')
|
|
expect(attackerBus.hasState('freeze')).toBe(false)
|
|
})
|
|
|
|
it('does NOT freeze attacker if melee damage is 100% elemental with 0 physical', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
const attackerBus = new StateBus(attackerStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
|
|
|
|
const outcome = defenderBus.triggerReactiveEvents({
|
|
event: 'damagedinmelee',
|
|
incomingPhys256: 0,
|
|
incomingElem256: 500 * FIXED_ONE,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
|
|
expect(outcome.attackerStatesApplied).not.toContain('freeze')
|
|
expect(attackerBus.hasState('freeze')).toBe(false)
|
|
})
|
|
|
|
it('does NOT trigger Frozen Armor on hitbymissile', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
const attackerBus = new StateBus(attackerStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
|
|
|
|
const outcome = defenderBus.triggerReactiveEvents({
|
|
event: 'hitbymissile',
|
|
incomingPhys256: 200 * FIXED_ONE,
|
|
returnFire: true,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
|
|
expect(outcome.attackerStatesApplied).not.toContain('freeze')
|
|
expect(attackerBus.hasState('freeze')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Shiver Armor (Skill 50) Edge Cases', () => {
|
|
it('proactively triggers on attackedinmelee BEFORE hit resolution, even if damage is 0', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
const attackerBus = new StateBus(attackerStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'shiverarmor', sourceSkillId: 50, slvl: 20 })
|
|
|
|
// Attacker begins melee swing (attackedinmelee) with 0 incoming damage
|
|
const outcome = defenderBus.triggerReactiveEvents({
|
|
event: 'attackedinmelee',
|
|
incomingPhys256: 0,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
|
|
expect(outcome.attackerStatesApplied).toContain('cold')
|
|
expect(attackerBus.hasState('cold')).toBe(true)
|
|
expect(outcome.attackerColdDamage256).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('does NOT trigger Shiver Armor on hitbymissile or damagedbymissile', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
const attackerBus = new StateBus(attackerStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'shiverarmor', sourceSkillId: 50, slvl: 20 })
|
|
|
|
const out1 = defenderBus.triggerReactiveEvents({
|
|
event: 'hitbymissile',
|
|
incomingPhys256: 100 * FIXED_ONE,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
expect(out1.attackerStatesApplied).not.toContain('cold')
|
|
expect(out1.attackerColdDamage256).toBe(0)
|
|
|
|
const out2 = defenderBus.triggerReactiveEvents({
|
|
event: 'damagedbymissile',
|
|
incomingPhys256: 100 * FIXED_ONE,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
expect(out2.attackerStatesApplied).not.toContain('cold')
|
|
expect(out2.attackerColdDamage256).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('Chilling Armor (Skill 60) Edge Cases', () => {
|
|
it('reflects chillingarmorbolt when struck by missile with returnFire: true', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
const attackerBus = new StateBus(attackerStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
|
|
|
|
const outcome = defenderBus.triggerReactiveEvents({
|
|
event: 'hitbymissile',
|
|
incomingPhys256: 100 * FIXED_ONE,
|
|
returnFire: true,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
|
|
expect(outcome.spawnedMissiles).toContain('chillingarmorbolt')
|
|
})
|
|
|
|
it('does NOT reflect chillingarmorbolt if incoming missile has returnFire: false', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
const attackerBus = new StateBus(attackerStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
|
|
|
|
const outcome = defenderBus.triggerReactiveEvents({
|
|
event: 'hitbymissile',
|
|
incomingPhys256: 100 * FIXED_ONE,
|
|
returnFire: false,
|
|
attackerStateBus: attackerBus,
|
|
})
|
|
|
|
expect(outcome.spawnedMissiles).not.toContain('chillingarmorbolt')
|
|
})
|
|
|
|
it('does NOT reflect chillingarmorbolt if returnFire is 0', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
|
|
|
|
const outcome = defenderBus.triggerReactiveEvents({
|
|
event: 'hitbymissile',
|
|
incomingPhys256: 100 * FIXED_ONE,
|
|
returnFire: 0,
|
|
})
|
|
|
|
expect(outcome.spawnedMissiles).not.toContain('chillingarmorbolt')
|
|
})
|
|
|
|
it('does NOT trigger Chilling Armor on damagedinmelee or attackedinmelee', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
|
|
const defenderBus = new StateBus(defenderStats, registry)
|
|
|
|
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
|
|
|
|
const out1 = defenderBus.triggerReactiveEvents({
|
|
event: 'damagedinmelee',
|
|
incomingPhys256: 100 * FIXED_ONE,
|
|
})
|
|
expect(out1.spawnedMissiles).not.toContain('chillingarmorbolt')
|
|
|
|
const out2 = defenderBus.triggerReactiveEvents({
|
|
event: 'attackedinmelee',
|
|
incomingPhys256: 100 * FIXED_ONE,
|
|
})
|
|
expect(out2.spawnedMissiles).not.toContain('chillingarmorbolt')
|
|
})
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Dimension 5: Full Invariant & System Integrity Checks
|
|
// =========================================================================
|
|
describe('Dimension 5: Full Invariant & System Integrity Checks', () => {
|
|
it('enforces BATCH1_SKILLS length strictly 38', () => {
|
|
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
|
|
})
|
|
|
|
it('verifies constant mana costs across slvl 1..99 for all three armors', () => {
|
|
for (let slvl = 1; slvl <= 99; slvl++) {
|
|
expect(getSkillManaCost(40, slvl)).toBe(7)
|
|
expect(getSkillManaCost(50, slvl)).toBe(11)
|
|
expect(getSkillManaCost(60, slvl)).toBe(17)
|
|
}
|
|
})
|
|
|
|
it('verifies zero cooldown ticks across all three armors', () => {
|
|
expect(getSkillCooldownTicks(40)).toBe(0)
|
|
expect(getSkillCooldownTicks(50)).toBe(0)
|
|
expect(getSkillCooldownTicks(60)).toBe(0)
|
|
})
|
|
|
|
it('verifies calculateSkillDamage routing consistency', () => {
|
|
for (let slvl = 1; slvl <= 30; slvl++) {
|
|
// Frozen Armor: always 0
|
|
expect(calculateSkillDamage(40, slvl)).toEqual({ min: 0, max: 0 })
|
|
|
|
// Shiver Armor: matches calculateShiverArmorDamage
|
|
const expectedShiver = calculateShiverArmorDamage(slvl)
|
|
expect(calculateSkillDamage(50, slvl)).toEqual(expectedShiver)
|
|
|
|
// Chilling Armor: matches calculateChillingArmorDamage
|
|
const expectedChilling = calculateChillingArmorDamage(slvl)
|
|
expect(calculateSkillDamage(60, slvl)).toEqual(expectedChilling)
|
|
}
|
|
})
|
|
|
|
it('verifies overlay and missile registrations', () => {
|
|
expect(OVERLAY_METAS['frozenarmor']).toBeDefined()
|
|
expect(OVERLAY_METAS['shiverarmor']).toBeDefined()
|
|
expect(OVERLAY_METAS['chillingarmor']).toBeDefined()
|
|
expect(OVERLAY_METAS['frozenarmor_hit']).toBeDefined()
|
|
expect(OVERLAY_METAS['shiverarmor_hit']).toBeDefined()
|
|
expect(OVERLAY_METAS['chillingarmor_hit']).toBeDefined()
|
|
expect(MISSILE_METAS['chillingarmorbolt']).toBeDefined()
|
|
|
|
expect(getSkillCastOverlay(40)).toBe('ice_cast_1')
|
|
expect(getSkillCastOverlay(50)).toBe('ice_cast_2')
|
|
expect(getSkillCastOverlay(60)).toBe('ice_cast_3')
|
|
})
|
|
})
|
|
})
|