diablo2-web/tests/skills/sor/adv-cold-fire-boundaries.te...

1023 lines
42 KiB
TypeScript

/**
* Adversarial Edge-Case Stress Testing & Coverage Boundary Verification Suite
* Focus: Sorceress Cold & Fire Trees (All 11 Skills & 16 Calculation Functions)
*
* Test Dimensions:
* 1. Zero, Negative, and Extreme Skill Levels across all 16 calculation functions:
* - calculateFrozenArmorDefense, calculateShiverArmorDamage, calculateChillingArmorDamage
* - calculateIceBlastDamage, calculateIceBlastFreezeLength
* - calculateGlacialSpikeDamage, calculateGlacialSpikeFreezeLength
* - calculateColdMasteryPierce, calculateFireWallDamage, calculateBlazeDamage
* - calculateInfernoDamage, calculateInfernoContinuousMana256
* - calculateEnchantDamage, calculateEnchantAttackRating, calculateEnchantDuration
* - calculateFireMasteryBonus
* Tested with: slvl = 0, slvl = -1, slvl = 99, slvl = 1000, NaN, undefined, fractions, infinities.
* 2. Mana Depletion and Channeling Exhaustion:
* - Inferno channeled stream mid-stream depletion to 0 mana (halt, state reset, event logging).
* - Casting rejection with insufficient mana (mana < cost) with zero state corruption.
* 3. Cooldown & Cast Delay Integrity:
* - Fire Wall (35 frames), Meteor (30 frames), Blizzard (45 frames), Hydra (40 frames), Frozen Orb (25 frames).
* - Global cast delay group locking: blocks other delayed spells while allowing non-delayed spells.
* 4. Screen & Spatial Collision Boundaries:
* - Extreme coordinates (-9999, +9999) & sub-pixel fractions for Glacial Spike burst & Ice Blast.
* - Collision raycasting (hasLineOfSight) deadlock-freedom and boundary precision.
*/
import { describe, expect, it } from 'vitest'
import {
calculateFrozenArmorDefense,
calculateShiverArmorDamage,
calculateChillingArmorDamage,
calculateIceBlastDamage,
calculateIceBlastFreezeLength,
calculateGlacialSpikeDamage,
calculateGlacialSpikeFreezeLength,
calculateColdMasteryPierce,
calculateFireWallDamage,
calculateBlazeDamage,
calculateInfernoDamage,
calculateInfernoContinuousMana256,
calculateEnchantDamage,
calculateEnchantAttackRating,
calculateEnchantDuration,
calculateFireMasteryBonus,
calculateFireWallPatches,
hasLineOfSight,
MissileEngine,
} from '../../../src/game/engine/missile-engine.ts'
import {
BATCH1_SKILLS,
getSkillManaCost,
getSkillCooldownTicks,
calculateSkillDamage,
executeSkill,
type SkillExecutionContext,
} from '../../../src/game/skills.ts'
import { castSkill, getSkillMana } from '../../../src/scene/act-scene.ts'
import { createPlayer } from '../../../src/game/combat.ts'
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
import { UnitStatList } from '../../../src/game/engine/stat-list.ts'
import { StateBus } from '../../../src/game/engine/state-bus.ts'
import type { CombatUnitContext } from '../../../src/game/engine/combat-pipeline.ts'
function createMockCombatUnit(
id: string,
options?: {
hp?: number
coldResist?: number
cannotBeFrozen?: boolean
coldMasterySlvl?: number
},
): CombatUnitContext {
const statList = new UnitStatList()
const hp = options?.hp ?? 500
statList.setBaseStat('hp', hp * 256)
statList.setBaseStat('maxhp', hp * 256)
statList.setBaseStat('hitpoints', hp * 256)
if (options?.coldResist !== undefined) {
statList.setBaseStat('coldresist', options.coldResist)
}
if (options?.cannotBeFrozen) {
statList.setBaseStat('cannot_be_frozen', 1)
}
if (options?.coldMasterySlvl !== undefined && options.coldMasterySlvl > 0) {
const pierce = calculateColdMasteryPierce(options.coldMasterySlvl)
statList.setBaseStat('passive_cold_pierce', pierce)
}
const stateBus = new StateBus(statList)
return {
id,
name: id,
statList,
stateBus,
}
}
describe('Adversarial Boundary Stress Suite — Cold & Fire Skills', () => {
// ==========================================================================
// Dimension 1: Zero, Negative, and Extreme Skill Levels
// ==========================================================================
describe('Dimension 1: Zero, Negative, and Extreme Skill Levels', () => {
const testLevels: Array<any> = [
0,
-1,
-100,
-Infinity,
NaN,
undefined,
1,
20,
99,
1000,
10000,
0.5,
15.7,
]
describe('Defense, Buff & Passive Calculations', () => {
it('calculateFrozenArmorDefense: safely handles all degenerate and extreme slvl inputs', () => {
for (const slvl of testLevels) {
expect(() => calculateFrozenArmorDefense(slvl)).not.toThrow()
const def = calculateFrozenArmorDefense(slvl)
expect(Number.isFinite(def)).toBe(true)
expect(Number.isNaN(def)).toBe(false)
expect(def).toBeGreaterThanOrEqual(30) // Minimum level 1 defense bonus
}
// Specific boundary assertions
expect(calculateFrozenArmorDefense(0)).toBe(30)
expect(calculateFrozenArmorDefense(-1)).toBe(30)
expect(calculateFrozenArmorDefense(NaN)).toBe(30)
expect(calculateFrozenArmorDefense(undefined as any)).toBe(30)
expect(calculateFrozenArmorDefense(99)).toBe(30 + 98 * 5) // 520%
expect(calculateFrozenArmorDefense(1000)).toBe(30 + 999 * 5) // 5025%
})
it('calculateColdMasteryPierce: clamps to 0 for non-positive or non-finite inputs, scales monotonically', () => {
for (const slvl of testLevels) {
expect(() => calculateColdMasteryPierce(slvl)).not.toThrow()
const pierce = calculateColdMasteryPierce(slvl)
expect(Number.isFinite(pierce)).toBe(true)
expect(Number.isNaN(pierce)).toBe(false)
expect(pierce).toBeGreaterThanOrEqual(0)
}
expect(calculateColdMasteryPierce(0)).toBe(0)
expect(calculateColdMasteryPierce(-1)).toBe(0)
expect(calculateColdMasteryPierce(-100)).toBe(0)
expect(calculateColdMasteryPierce(NaN)).toBe(0)
expect(calculateColdMasteryPierce(undefined as any)).toBe(0)
expect(calculateColdMasteryPierce(1)).toBe(20)
expect(calculateColdMasteryPierce(20)).toBe(20 + 19 * 5) // 115%
expect(calculateColdMasteryPierce(99)).toBe(20 + 98 * 5) // 510%
expect(calculateColdMasteryPierce(1000)).toBe(20 + 999 * 5) // 5015%
})
it('calculateFireMasteryBonus: clamps to 0 for non-positive or non-finite inputs, scales monotonically', () => {
for (const slvl of testLevels) {
expect(() => calculateFireMasteryBonus(slvl)).not.toThrow()
const bonus = calculateFireMasteryBonus(slvl)
expect(Number.isFinite(bonus)).toBe(true)
expect(Number.isNaN(bonus)).toBe(false)
expect(bonus).toBeGreaterThanOrEqual(0)
}
expect(calculateFireMasteryBonus(0)).toBe(0)
expect(calculateFireMasteryBonus(-1)).toBe(0)
expect(calculateFireMasteryBonus(NaN)).toBe(0)
expect(calculateFireMasteryBonus(undefined as any)).toBe(0)
expect(calculateFireMasteryBonus(1)).toBe(30)
expect(calculateFireMasteryBonus(20)).toBe(30 + 19 * 7) // 163%
expect(calculateFireMasteryBonus(99)).toBe(30 + 98 * 7) // 716%
expect(calculateFireMasteryBonus(1000)).toBe(30 + 999 * 7) // 7023%
})
it('calculateEnchantAttackRating: clamps to 0 for non-positive or non-finite inputs', () => {
for (const slvl of testLevels) {
expect(() => calculateEnchantAttackRating(slvl)).not.toThrow()
const ar = calculateEnchantAttackRating(slvl)
expect(Number.isFinite(ar)).toBe(true)
expect(Number.isNaN(ar)).toBe(false)
expect(ar).toBeGreaterThanOrEqual(0)
}
expect(calculateEnchantAttackRating(0)).toBe(0)
expect(calculateEnchantAttackRating(-1)).toBe(0)
expect(calculateEnchantAttackRating(NaN)).toBe(0)
expect(calculateEnchantAttackRating(undefined as any)).toBe(0)
expect(calculateEnchantAttackRating(1)).toBe(20)
expect(calculateEnchantAttackRating(20)).toBe(20 + 19 * 9) // 191%
expect(calculateEnchantAttackRating(99)).toBe(20 + 98 * 9) // 902%
expect(calculateEnchantAttackRating(1000)).toBe(20 + 999 * 9) // 9011%
})
it('calculateEnchantDuration: clamps to 0 for non-positive or non-finite inputs', () => {
for (const slvl of testLevels) {
expect(() => calculateEnchantDuration(slvl)).not.toThrow()
const dur = calculateEnchantDuration(slvl)
expect(Number.isFinite(dur)).toBe(true)
expect(Number.isNaN(dur)).toBe(false)
expect(dur).toBeGreaterThanOrEqual(0)
}
expect(calculateEnchantDuration(0)).toBe(0)
expect(calculateEnchantDuration(-1)).toBe(0)
expect(calculateEnchantDuration(NaN)).toBe(0)
expect(calculateEnchantDuration(undefined as any)).toBe(0)
expect(calculateEnchantDuration(1)).toBe(3600) // 144s
expect(calculateEnchantDuration(20)).toBe(3600 + 19 * 600) // 15000 frames
expect(calculateEnchantDuration(99)).toBe(3600 + 98 * 600) // 62400 frames
expect(calculateEnchantDuration(1000)).toBe(3600 + 999 * 600) // 603000 frames
})
it('calculateInfernoContinuousMana256: safely handles degenerate and extreme inputs', () => {
for (const slvl of testLevels) {
expect(() => calculateInfernoContinuousMana256(slvl)).not.toThrow()
const mana256 = calculateInfernoContinuousMana256(slvl)
expect(Number.isFinite(mana256)).toBe(true)
expect(Number.isNaN(mana256)).toBe(false)
expect(mana256).toBeGreaterThanOrEqual(144) // Level 1 drain
}
expect(calculateInfernoContinuousMana256(0)).toBe(144)
expect(calculateInfernoContinuousMana256(-1)).toBe(144)
expect(calculateInfernoContinuousMana256(NaN)).toBe(144)
expect(calculateInfernoContinuousMana256(undefined as any)).toBe(144)
expect(calculateInfernoContinuousMana256(1)).toBe(144)
expect(calculateInfernoContinuousMana256(20)).toBe((36 + 19) * 4) // 220
expect(calculateInfernoContinuousMana256(99)).toBe((36 + 98) * 4) // 536
expect(calculateInfernoContinuousMana256(1000)).toBe((36 + 999) * 4) // 4140
})
})
describe('Cold Armor & Cold Projectile Damage and Freeze Calculations', () => {
it('calculateShiverArmorDamage: safe fallbacks, finite ranges, min <= max', () => {
for (const slvl of testLevels) {
expect(() => calculateShiverArmorDamage(slvl)).not.toThrow()
const dmg = calculateShiverArmorDamage(slvl)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(dmg.min).toBeLessThanOrEqual(dmg.max)
expect(dmg.min).toBeGreaterThanOrEqual(6)
}
expect(calculateShiverArmorDamage(0)).toEqual({ min: 6, max: 8 })
expect(calculateShiverArmorDamage(-1)).toEqual({ min: 6, max: 8 })
expect(calculateShiverArmorDamage(NaN)).toEqual({ min: 6, max: 8 })
expect(calculateShiverArmorDamage(undefined as any)).toEqual({ min: 6, max: 8 })
expect(calculateShiverArmorDamage(99).min).toBeGreaterThan(calculateShiverArmorDamage(20).min)
expect(calculateShiverArmorDamage(1000).min).toBeGreaterThan(calculateShiverArmorDamage(99).min)
})
it('calculateChillingArmorDamage: safe fallbacks, finite ranges, min <= max', () => {
for (const slvl of testLevels) {
expect(() => calculateChillingArmorDamage(slvl)).not.toThrow()
const dmg = calculateChillingArmorDamage(slvl)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(dmg.min).toBeLessThanOrEqual(dmg.max)
expect(dmg.min).toBeGreaterThanOrEqual(4)
}
expect(calculateChillingArmorDamage(0)).toEqual({ min: 4, max: 6 })
expect(calculateChillingArmorDamage(-1)).toEqual({ min: 4, max: 6 })
expect(calculateChillingArmorDamage(NaN)).toEqual({ min: 4, max: 6 })
expect(calculateChillingArmorDamage(undefined as any)).toEqual({ min: 4, max: 6 })
expect(calculateChillingArmorDamage(99).min).toBeGreaterThan(calculateChillingArmorDamage(20).min)
expect(calculateChillingArmorDamage(1000).min).toBeGreaterThan(calculateChillingArmorDamage(99).min)
})
it('calculateIceBlastDamage: safe fallbacks, finite ranges, min <= max', () => {
for (const slvl of testLevels) {
expect(() => calculateIceBlastDamage(slvl)).not.toThrow()
const dmg = calculateIceBlastDamage(slvl)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(dmg.min).toBeLessThanOrEqual(dmg.max)
expect(dmg.min).toBeGreaterThanOrEqual(8)
}
expect(calculateIceBlastDamage(0)).toEqual({ min: 8, max: 12 })
expect(calculateIceBlastDamage(-1)).toEqual({ min: 8, max: 12 })
expect(calculateIceBlastDamage(NaN)).toEqual({ min: 8, max: 12 })
expect(calculateIceBlastDamage(undefined as any)).toEqual({ min: 8, max: 12 })
expect(calculateIceBlastDamage(99).min).toBeGreaterThan(calculateIceBlastDamage(20).min)
expect(calculateIceBlastDamage(1000).min).toBeGreaterThan(calculateIceBlastDamage(99).min)
})
it('calculateIceBlastFreezeLength: safe fallbacks, positive finite frames', () => {
for (const slvl of testLevels) {
expect(() => calculateIceBlastFreezeLength(slvl)).not.toThrow()
const frz = calculateIceBlastFreezeLength(slvl)
expect(Number.isFinite(frz)).toBe(true)
expect(frz).toBeGreaterThanOrEqual(75) // Level 1 freeze length
}
expect(calculateIceBlastFreezeLength(0)).toBe(75)
expect(calculateIceBlastFreezeLength(-1)).toBe(75)
expect(calculateIceBlastFreezeLength(NaN)).toBe(75)
expect(calculateIceBlastFreezeLength(undefined as any)).toBe(75)
expect(calculateIceBlastFreezeLength(99)).toBe(75 + 98 * 5) // 565 frames
expect(calculateIceBlastFreezeLength(1000)).toBe(75 + 999 * 5) // 5070 frames
})
it('calculateGlacialSpikeDamage: safe fallbacks, finite ranges, min <= max', () => {
for (const slvl of testLevels) {
expect(() => calculateGlacialSpikeDamage(slvl)).not.toThrow()
const dmg = calculateGlacialSpikeDamage(slvl)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(dmg.min).toBeLessThanOrEqual(dmg.max)
expect(dmg.min).toBeGreaterThanOrEqual(16)
}
expect(calculateGlacialSpikeDamage(0)).toEqual({ min: 16, max: 24 })
expect(calculateGlacialSpikeDamage(-1)).toEqual({ min: 16, max: 24 })
expect(calculateGlacialSpikeDamage(NaN)).toEqual({ min: 16, max: 24 })
expect(calculateGlacialSpikeDamage(undefined as any)).toEqual({ min: 16, max: 24 })
expect(calculateGlacialSpikeDamage(99).min).toBeGreaterThan(calculateGlacialSpikeDamage(20).min)
expect(calculateGlacialSpikeDamage(1000).min).toBeGreaterThan(calculateGlacialSpikeDamage(99).min)
})
it('calculateGlacialSpikeFreezeLength: safe fallbacks, positive finite frames', () => {
for (const slvl of testLevels) {
expect(() => calculateGlacialSpikeFreezeLength(slvl)).not.toThrow()
const frz = calculateGlacialSpikeFreezeLength(slvl)
expect(Number.isFinite(frz)).toBe(true)
expect(frz).toBeGreaterThanOrEqual(50) // Level 1 freeze length
}
expect(calculateGlacialSpikeFreezeLength(0)).toBe(50)
expect(calculateGlacialSpikeFreezeLength(-1)).toBe(50)
expect(calculateGlacialSpikeFreezeLength(NaN)).toBe(50)
expect(calculateGlacialSpikeFreezeLength(undefined as any)).toBe(50)
expect(calculateGlacialSpikeFreezeLength(99)).toBe(50 + 98 * 3) // 344 frames
expect(calculateGlacialSpikeFreezeLength(1000)).toBe(50 + 999 * 3) // 3047 frames
})
})
describe('Fire Ground, Stream & Buff Calculations', () => {
it('calculateFireWallDamage: safe fallbacks, finite ranges and 256ths precision', () => {
for (const slvl of testLevels) {
expect(() => calculateFireWallDamage(slvl)).not.toThrow()
const dmg = calculateFireWallDamage(slvl)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(Number.isFinite(dmg.min256)).toBe(true)
expect(Number.isFinite(dmg.max256)).toBe(true)
expect(Number.isFinite(dmg.perSecMin)).toBe(true)
expect(Number.isFinite(dmg.perSecMax)).toBe(true)
expect(dmg.min).toBeLessThanOrEqual(dmg.max)
expect(dmg.min256).toBeLessThanOrEqual(dmg.max256)
expect(dmg.perSecMin).toBeLessThanOrEqual(dmg.perSecMax)
}
const fallback = calculateFireWallDamage(0)
expect(fallback.min).toBe(15)
expect(fallback.max).toBe(20)
expect(fallback.min256).toBe(240)
expect(fallback.max256).toBe(320)
expect(calculateFireWallDamage(-1)).toEqual(fallback)
expect(calculateFireWallDamage(NaN)).toEqual(fallback)
expect(calculateFireWallDamage(undefined as any)).toEqual(fallback)
})
it('calculateBlazeDamage: safe fallbacks, finite ranges and 256ths precision', () => {
for (const slvl of testLevels) {
expect(() => calculateBlazeDamage(slvl)).not.toThrow()
const dmg = calculateBlazeDamage(slvl)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(Number.isFinite(dmg.min256)).toBe(true)
expect(Number.isFinite(dmg.max256)).toBe(true)
expect(Number.isFinite(dmg.perSecMin)).toBe(true)
expect(Number.isFinite(dmg.perSecMax)).toBe(true)
expect(dmg.min).toBeLessThanOrEqual(dmg.max)
expect(dmg.min256).toBeLessThanOrEqual(dmg.max256)
expect(dmg.perSecMin).toBeLessThanOrEqual(dmg.perSecMax)
}
const fallback = calculateBlazeDamage(0)
expect(fallback.min).toBe(4)
expect(fallback.max).toBe(8)
expect(fallback.min256).toBe(64)
expect(fallback.max256).toBe(128)
expect(calculateBlazeDamage(-1)).toEqual(fallback)
expect(calculateBlazeDamage(NaN)).toEqual(fallback)
expect(calculateBlazeDamage(undefined as any)).toEqual(fallback)
})
it('calculateInfernoDamage: safe fallbacks, finite ranges and 256ths precision', () => {
for (const slvl of testLevels) {
expect(() => calculateInfernoDamage(slvl)).not.toThrow()
const dmg = calculateInfernoDamage(slvl)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(Number.isFinite(dmg.min256)).toBe(true)
expect(Number.isFinite(dmg.max256)).toBe(true)
expect(Number.isFinite(dmg.perSecMin)).toBe(true)
expect(Number.isFinite(dmg.perSecMax)).toBe(true)
expect(dmg.min).toBeLessThanOrEqual(dmg.max)
expect(dmg.min256).toBeLessThanOrEqual(dmg.max256)
expect(dmg.perSecMin).toBeLessThanOrEqual(dmg.perSecMax)
}
const fallback = calculateInfernoDamage(0)
expect(fallback.min).toBe(32)
expect(fallback.max).toBe(64)
expect(fallback.min256).toBe(128)
expect(fallback.max256).toBe(256)
expect(calculateInfernoDamage(-1)).toEqual(fallback)
expect(calculateInfernoDamage(NaN)).toEqual(fallback)
expect(calculateInfernoDamage(undefined as any)).toEqual(fallback)
})
it('calculateEnchantDamage: safe fallbacks, finite ranges and 256ths precision', () => {
for (const slvl of testLevels) {
expect(() => calculateEnchantDamage(slvl)).not.toThrow()
const dmg = calculateEnchantDamage(slvl)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(Number.isFinite(dmg.min256)).toBe(true)
expect(Number.isFinite(dmg.max256)).toBe(true)
expect(dmg.min).toBeLessThanOrEqual(dmg.max)
expect(dmg.min256).toBeLessThanOrEqual(dmg.max256)
}
const fallback = calculateEnchantDamage(0)
expect(fallback.min).toBe(8)
expect(fallback.max).toBe(10)
expect(fallback.min256).toBe(2048)
expect(fallback.max256).toBe(2560)
expect(calculateEnchantDamage(-1)).toEqual(fallback)
expect(calculateEnchantDamage(NaN)).toEqual(fallback)
expect(calculateEnchantDamage(undefined as any)).toEqual(fallback)
})
})
describe('Synergy Degenerate Inputs Fuzzing', () => {
it('handles negative, null, empty object, and partial synergies without throwing or propagating NaN', () => {
const degenerateSynergies = [
null,
undefined,
0,
-5,
-999,
{},
{ warmth: -10, fireMastery: -20 },
{ frozenArmor: -1, chillingArmor: -5 },
{ iceBolt: -5, blizzard: 0, frozenOrb: -10, glacialSpike: undefined },
{ synMultiplier: 0.5 },
{ synMultiplier: 1.0 },
]
for (const syn of degenerateSynergies) {
expect(() => calculateShiverArmorDamage(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateShiverArmorDamage(10, syn as any).min)).toBe(false)
expect(() => calculateChillingArmorDamage(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateChillingArmorDamage(10, syn as any).min)).toBe(false)
expect(() => calculateIceBlastDamage(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateIceBlastDamage(10, syn as any).min)).toBe(false)
expect(() => calculateIceBlastFreezeLength(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateIceBlastFreezeLength(10, syn as any))).toBe(false)
expect(() => calculateGlacialSpikeDamage(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateGlacialSpikeDamage(10, syn as any).min)).toBe(false)
expect(() => calculateGlacialSpikeFreezeLength(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateGlacialSpikeFreezeLength(10, syn as any))).toBe(false)
expect(() => calculateFireWallDamage(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateFireWallDamage(10, syn as any).min)).toBe(false)
expect(() => calculateBlazeDamage(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateBlazeDamage(10, syn as any).min)).toBe(false)
expect(() => calculateInfernoDamage(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateInfernoDamage(10, syn as any).min)).toBe(false)
expect(() => calculateEnchantDamage(10, syn as any)).not.toThrow()
expect(Number.isNaN(calculateEnchantDamage(10, syn as any).min)).toBe(false)
}
})
})
})
// ==========================================================================
// Dimension 2: Mana Depletion and Channeling Exhaustion
// ==========================================================================
describe('Dimension 2: Mana Depletion and Channeling Exhaustion', () => {
it('Inferno channeled stream: halts immediately when mana reaches 0 mid-stream and cleans up resources', () => {
const player = {
x: 100,
y: 100,
facing: 0,
cooldown: 0,
mana: 10,
maxMana: 100,
infernoChanneling: false,
infernoManaRemainder256: 0,
}
const engine: any = {
world: {
player,
events: [],
},
projectiles: [],
spawnOverlay: () => {},
metrics: { casts: 0 },
}
const status = { textContent: '' } as any
const context = {
engine,
runtime: {} as any,
hudManager: null,
status,
}
// Step 1: Initial cast consumes 6 mana
const cast1 = castSkill(41, 200, 100, context)
expect(cast1).toBe(true)
expect(player.mana).toBe(4) // 10 - 6 = 4
expect(player.infernoChanneling).toBe(true)
expect(player.infernoManaRemainder256).toBe(0)
expect(engine.projectiles).toHaveLength(1)
expect(engine.metrics.casts).toBe(1)
// Step 2: Continuous channeling frames (slvl 1 drains 144 / 256 per frame)
// Call 2: frame 1: accumulator 144, wholeMana 0, mana remains 4
const cast2 = castSkill(41, 200, 100, context)
expect(cast2).toBe(true)
expect(player.mana).toBe(4)
expect(player.infernoManaRemainder256).toBe(144)
expect(engine.projectiles).toHaveLength(2)
// Call 3: frame 2: accumulator 144+144 = 288, wholeMana 1, remainder 32, mana becomes 3
const cast3 = castSkill(41, 200, 100, context)
expect(cast3).toBe(true)
expect(player.mana).toBe(3)
expect(player.infernoManaRemainder256).toBe(32)
expect(engine.projectiles).toHaveLength(3)
// Step 3: Deplete player mana to 0 mid-stream
player.mana = 0
player.infernoManaRemainder256 = 100
// Call 4: Next frame while mana is 0 -> MUST halt stream and reject cast
const castEmpty = castSkill(41, 200, 100, context)
expect(castEmpty).toBe(false)
expect(status.textContent).toBe('法力不足!')
expect(player.infernoChanneling).toBe(false)
expect(player.infernoManaRemainder256).toBe(0)
// Ensure no new projectile was spawned when mana exhausted
expect(engine.projectiles).toHaveLength(3)
expect(engine.world.events).toContainEqual(
expect.objectContaining({ kind: 'noMana' }),
)
// Step 4: Subsequent call while exhausted continues to reject cleanly
const castSubsequent = castSkill(41, 200, 100, context)
expect(castSubsequent).toBe(false)
expect(player.infernoChanneling).toBe(false)
expect(engine.projectiles).toHaveLength(3)
})
it('Inferno: starting cast is rejected immediately when mana < 6 without setting channeling state', () => {
const player = {
x: 100,
y: 100,
facing: 0,
cooldown: 0,
mana: 5, // Insufficient (needs 6)
maxMana: 100,
infernoChanneling: false,
infernoManaRemainder256: 0,
}
const engine: any = {
world: {
player,
events: [],
},
projectiles: [],
spawnOverlay: () => {},
metrics: { casts: 0 },
}
const status = { textContent: '' } as any
const context = {
engine,
runtime: {} as any,
hudManager: null,
status,
}
const res = castSkill(41, 200, 100, context)
expect(res).toBe(false)
expect(player.mana).toBe(5) // Untouched
expect(player.infernoChanneling).toBe(false)
expect(player.infernoManaRemainder256).toBe(0)
expect(engine.projectiles).toHaveLength(0)
expect(engine.world.events).toContainEqual(
expect.objectContaining({ kind: 'noMana' }),
)
})
it('rejects casting spells when mana < cost across all Cold and Fire skills without state corruption', () => {
const skillsToTest = [
{ id: 40, name: 'Frozen Armor' },
{ id: 50, name: 'Shiver Armor' },
{ id: 60, name: 'Chilling Armor' },
{ id: 45, name: 'Ice Blast' },
{ id: 55, name: 'Glacial Spike' },
{ id: 51, name: 'Fire Wall' },
{ id: 46, name: 'Blaze' },
{ id: 41, name: 'Inferno' },
{ id: 52, name: 'Enchant' },
{ id: 59, name: 'Blizzard' },
{ id: 56, name: 'Meteor' },
]
for (const skill of skillsToTest) {
const manaCost = getSkillMana(skill.id, null)
const insufficientMana = Math.max(0, manaCost - 1)
const player = {
x: 100,
y: 100,
facing: 0,
cooldown: 0,
mana: insufficientMana,
maxMana: 100,
infernoChanneling: false,
infernoManaRemainder256: 0,
blazeActive: false,
enchantActive: false,
}
const engine: any = {
world: {
player,
events: [],
},
projectiles: [],
overlays: [],
spawnOverlay: () => {},
metrics: { casts: 0 },
}
const status = { textContent: '' } as any
const context = {
engine,
runtime: {} as any,
hudManager: null,
status,
}
const success = castSkill(skill.id, 200, 100, context)
expect(success).toBe(false)
// INVARIANTS: Zero resource/state corruption
expect(player.mana).toBe(insufficientMana)
expect(player.cooldown).toBe(0)
expect(engine.metrics.casts).toBe(0)
expect(engine.projectiles).toHaveLength(0)
expect(engine.overlays).toHaveLength(0)
expect(status.textContent).toBe('法力不足!')
expect(engine.world.events).toContainEqual(
expect.objectContaining({ kind: 'noMana' }),
)
}
})
})
// ==========================================================================
// Dimension 3: Cooldown & Cast Delay Integrity
// ==========================================================================
describe('Dimension 3: Cooldown & Cast Delay Integrity', () => {
it('verifies exact 1.13c cast delay values across delayed skills and zero cooldown for non-delayed skills', () => {
// Delayed skills:
expect(getSkillCooldownTicks(51)).toBe(35) // Fire Wall: 35 frames (1.40s)
expect(getSkillCooldownTicks(56)).toBe(30) // Meteor: 30 frames (1.20s)
expect(getSkillCooldownTicks(59)).toBe(45) // Blizzard: 45 frames (1.80s)
expect(getSkillCooldownTicks(62)).toBe(40) // Hydra: 40 frames (1.60s)
expect(getSkillCooldownTicks(64)).toBe(25) // Frozen Orb: 25 frames (1.00s)
// Non-delayed Cold & Fire skills (strictly 0 frames):
const nonDelayedColdFireSkills = [
40, // Frozen Armor
50, // Shiver Armor
60, // Chilling Armor
45, // Ice Blast
55, // Glacial Spike
65, // Cold Mastery
36, // Fire Bolt
47, // Fire Ball
37, // Warmth
41, // Inferno
46, // Blaze
52, // Enchant
61, // Fire Mastery
]
for (const skillId of nonDelayedColdFireSkills) {
expect(getSkillCooldownTicks(skillId)).toBe(0)
}
})
it('executeSkill: cast delay locks global cast delay group while allowing non-delayed spells', () => {
const caster = createPlayer(100, 100, 100, 100)
const context: SkillExecutionContext = {
caster,
skillLevel: 1,
}
// Step 1: Simulate caster on Blizzard cast delay (45 ticks remaining)
caster.cooldown = 45
// Step 2: Attempting to cast any other delayed spell MUST be rejected by cooldown check
// Werewolf (223, delay 25)
const delayedCast1 = executeSkill(223, context)
expect(delayedCast1.kind).toBe('cooldown')
if (delayedCast1.kind === 'cooldown') {
expect(delayedCast1.ticksLeft).toBe(45)
}
// Firestorm (225, delay 15)
const delayedCast2 = executeSkill(225, context)
expect(delayedCast2.kind).toBe('cooldown')
if (delayedCast2.kind === 'cooldown') {
expect(delayedCast2.ticksLeft).toBe(45)
}
// Molten Boulder (229, delay 50)
const delayedCast3 = executeSkill(229, context)
expect(delayedCast3.kind).toBe('cooldown')
if (delayedCast3.kind === 'cooldown') {
expect(delayedCast3.ticksLeft).toBe(45)
}
// Step 3: Under the exact same cooldown (cooldown = 45), casting non-delayed spells MUST succeed!
// Ice Bolt (39, delay 0)
const nonDelayed1 = executeSkill(39, context)
expect(nonDelayed1.kind).toBe('projectile')
if (nonDelayed1.kind === 'projectile') {
expect(nonDelayed1.cooldownTicks).toBe(0)
}
// Fire Bolt (36, delay 0)
const nonDelayed2 = executeSkill(36, context)
expect(nonDelayed2.kind).toBe('projectile')
if (nonDelayed2.kind === 'projectile') {
expect(nonDelayed2.cooldownTicks).toBe(0)
}
// Fire Ball (47, delay 0)
const nonDelayed3 = executeSkill(47, context)
expect(nonDelayed3.kind).toBe('projectile')
if (nonDelayed3.kind === 'projectile') {
expect(nonDelayed3.cooldownTicks).toBe(0)
}
// Frost Nova (44, delay 0)
const nonDelayed4 = executeSkill(44, context)
expect(nonDelayed4.kind).toBe('area')
if (nonDelayed4.kind === 'area') {
expect(nonDelayed4.cooldownTicks).toBe(0)
}
// Teleport (54, delay 0)
const nonDelayed5 = executeSkill(54, context)
expect(nonDelayed5.kind).toBe('movement')
if (nonDelayed5.kind === 'movement') {
expect(nonDelayed5.cooldownTicks).toBe(0)
}
})
it('castSkill: verifies cooldown duration application and tick countdown unlocking', () => {
const player = {
x: 100,
y: 100,
facing: 0,
cooldown: 0,
mana: 100,
maxMana: 100,
infernoChanneling: false,
infernoManaRemainder256: 0,
}
const engine: any = {
world: {
player,
events: [],
},
projectiles: [],
spawnOverlay: () => {},
metrics: { casts: 0 },
}
const status = { textContent: '' } as any
const context = {
engine,
runtime: {} as any,
hudManager: null,
status,
}
// Cast Fire Wall (51): sets cooldown to exactly 35
const fwCast = castSkill(51, 200, 100, context)
expect(fwCast).toBe(true)
expect(player.cooldown).toBe(35)
// While cooldown > 0, immediate second cast is blocked
const blockedCast = castSkill(51, 200, 100, context)
expect(blockedCast).toBe(false)
// Countdown ticks from 35 down to 1
for (let t = 35; t > 0; t--) {
expect(player.cooldown).toBe(t)
const rejected = castSkill(51, 200, 100, context)
expect(rejected).toBe(false)
player.cooldown -= 1
}
// Once at 0, casting succeeds again
expect(player.cooldown).toBe(0)
const unblockedCast = castSkill(51, 200, 100, context)
expect(unblockedCast).toBe(true)
expect(player.cooldown).toBe(35)
// Meteor (56): sets cooldown to exactly 30
player.cooldown = 0
const meteorCast = castSkill(56, 200, 100, context)
expect(meteorCast).toBe(true)
expect(player.cooldown).toBe(30)
// Blizzard (59): sets cooldown to exactly 45
player.cooldown = 0
const blizzardCast = castSkill(59, 200, 100, context)
expect(blizzardCast).toBe(true)
expect(player.cooldown).toBe(45)
})
})
// ==========================================================================
// Dimension 4: Screen & Spatial Collision Boundaries
// ==========================================================================
describe('Dimension 4: Screen & Spatial Collision Boundaries', () => {
describe('Raycasting (hasLineOfSight) Boundary & Stress Tests', () => {
it('hasLineOfSight: handles extreme coordinates (-9999 to +9999) without deadlocks, NaN or stack overflows', () => {
const extremeVectors = [
{ x0: -9999, y0: -9999, x1: 9999, y1: 9999 },
{ x0: 9999, y0: -9999, x1: -9999, y1: 9999 },
{ x0: -9999, y0: 0, x1: 9999, y1: 0 },
{ x0: 0, y0: -9999, x1: 0, y1: 9999 },
{ x0: -9999, y0: 9999, x1: 9999, y1: -9999 },
]
// Open terrain (no obstacles) -> completes rapidly
for (const vec of extremeVectors) {
const t0 = Date.now()
const result = hasLineOfSight(vec.x0, vec.y0, vec.x1, vec.y1, undefined)
const elapsed = Date.now() - t0
expect(result).toBe(true)
expect(elapsed).toBeLessThan(100) // Must not hang/deadlock
}
// Obstacle in path at (0, 0)
const wallTerrain = (x: number, y: number) => Math.hypot(x, y) < 16
expect(hasLineOfSight(-100, 0, 100, 0, wallTerrain)).toBe(false)
// Clear trajectory above obstacle
expect(hasLineOfSight(-100, 50, 100, 50, wallTerrain)).toBe(true)
})
it('hasLineOfSight: handles sub-pixel fractions and degenerate zero distance', () => {
// Zero distance (x0 === x1, y0 === y1)
expect(hasLineOfSight(500, 500, 500, 500)).toBe(true)
expect(hasLineOfSight(-9999.123, 9999.456, -9999.123, 9999.456)).toBe(true)
// Sub-pixel micro increments
expect(hasLineOfSight(100.123456, 200.654321, 100.123457, 200.654322)).toBe(true)
})
})
describe('MissileEngine Projectile & Collision Stress', () => {
it('spawns and steps Glacial Spike & Ice Blast at extreme coordinates without NaN or crashes', async () => {
const registry = await getSharedDataRegistry()
const engine = new MissileEngine(registry)
const attacker = createMockCombatUnit('sorceress')
const target = createMockCombatUnit('far_target')
// Spawn Glacial Spike across extreme boundary: start (-9999, -9999), target (+9999, +9999)
const mGlacial = engine.spawnMissile({
missileNameOrId: 'glacialspike',
sourceSkillId: 55,
slvl: 20,
owner: attacker,
startX: -9999,
startY: -9999,
targetX: 9999,
targetY: 9999,
dmgPacket: {
skillId: 55,
elemType: 'cold',
elemMin256: 1000 * 256,
elemMax256: 1000 * 256,
elemDurationFrames: 100,
},
})
expect(mGlacial).not.toBeNull()
expect(Number.isFinite(mGlacial!.vx)).toBe(true)
expect(Number.isFinite(mGlacial!.vy)).toBe(true)
expect(Number.isNaN(mGlacial!.vx)).toBe(false)
expect(Number.isNaN(mGlacial!.vy)).toBe(false)
// Spawn Ice Blast at reverse extreme coordinates (+9999, -9999) -> (-9999, +9999)
const mBlast = engine.spawnMissile({
missileNameOrId: 'iceblast',
sourceSkillId: 45,
slvl: 20,
owner: attacker,
startX: 9999,
startY: -9999,
targetX: -9999,
targetY: 9999,
dmgPacket: {
skillId: 45,
elemType: 'cold',
elemMin256: 1000 * 256,
elemMax256: 1000 * 256,
elemDurationFrames: 100,
},
})
expect(mBlast).not.toBeNull()
expect(Number.isFinite(mBlast!.vx)).toBe(true)
expect(Number.isFinite(mBlast!.vy)).toBe(true)
// Update loop with target at extreme coordinate
const targetPositions = new Map<string, { x: number; y: number }>()
targetPositions.set('far_target', { x: 9999, y: 9999 })
expect(() => engine.tick(1, [target], targetPositions)).not.toThrow()
})
it('Glacial Spike AoE burst: strictly respects 64px radial boundary on wall collision at sub-pixel coordinates', async () => {
const registry = await getSharedDataRegistry()
const attacker = createMockCombatUnit('sorceress')
// Target exactly at wall impact point (0px offset)
const tDirect = createMockCombatUnit('t_direct')
// Target at 64.0px radius (inside burst boundary)
const tEdgeIn = createMockCombatUnit('t_64px')
// Target at 64.1px radius (outside burst boundary)
const tEdgeOut = createMockCombatUnit('t_64_1px')
// Target at 100px radius (far outside)
const tFar = createMockCombatUnit('t_100px')
const targets = [tDirect, tEdgeIn, tEdgeOut, tFar]
const targetPositions = new Map<string, { x: number; y: number }>()
// Sub-pixel wall impact coordinate: (100.25, 200.75)
const impactX = 100.25
const impactY = 200.75
targetPositions.set(tDirect.id, { x: impactX, y: impactY })
targetPositions.set(tEdgeIn.id, { x: impactX, y: impactY + 64.0 })
targetPositions.set(tEdgeOut.id, { x: impactX, y: impactY + 64.1 })
targetPositions.set(tFar.id, { x: impactX, y: impactY + 100.0 })
// Terrain with blocking obstacle at impactX
const terrain = {
isMissileBlocked: (x: number) => x >= impactX,
}
const engine = new MissileEngine(registry, terrain)
engine.spawnMissile({
missileNameOrId: 'glacialspike',
sourceSkillId: 55,
slvl: 20,
owner: attacker,
startX: impactX - 24,
startY: impactY,
targetX: impactX + 24,
targetY: impactY,
vx: 24,
vy: 0,
dmgPacket: {
skillId: 55,
elemType: 'cold',
elemMin256: 500 * 256,
elemMax256: 500 * 256,
elemDurationFrames: 100,
},
})
const updateResult = engine.tick(1, targets, targetPositions, terrain)
// Sub-missiles spawned on detonation
expect(updateResult.subMissilesSpawned).toContain('glacialspikeejecta')
// Verify splash hits: tDirect and tEdgeIn hit, tEdgeOut and tFar NOT hit
const hitTargetIds = updateResult.hits.map(h => h.targetId)
expect(hitTargetIds).toContain(tDirect.id)
expect(hitTargetIds).toContain(tEdgeIn.id)
expect(hitTargetIds).not.toContain(tEdgeOut.id)
expect(hitTargetIds).not.toContain(tFar.id)
// Freeze state applied to units within 64px
expect(tDirect.stateBus.hasState('freeze')).toBe(true)
expect(tEdgeIn.stateBus.hasState('freeze')).toBe(true)
expect(tEdgeOut.stateBus.hasState('freeze')).toBe(false)
expect(tFar.stateBus.hasState('freeze')).toBe(false)
})
it('calculateFireWallPatches: produces robust integer layouts at extreme coordinates and zero distance', () => {
// Extreme coordinates
const patchesExtreme = calculateFireWallPatches(-9999, -9999, 9999, 9999)
expect(patchesExtreme).toHaveLength(15)
for (const p of patchesExtreme) {
expect(Number.isFinite(p.x)).toBe(true)
expect(Number.isFinite(p.y)).toBe(true)
expect(Number.isNaN(p.x)).toBe(false)
expect(Number.isNaN(p.y)).toBe(false)
}
// Degenerate zero distance: caster === target
const patchesZero = calculateFireWallPatches(150, 150, 150, 150)
expect(patchesZero).toHaveLength(15)
for (const p of patchesZero) {
expect(Number.isFinite(p.x)).toBe(true)
expect(Number.isFinite(p.y)).toBe(true)
}
// Sub-pixel positions
const patchesSub = calculateFireWallPatches(10.123, 20.456, 30.789, 40.012)
expect(patchesSub).toHaveLength(15)
for (const p of patchesSub) {
expect(Number.isInteger(p.x)).toBe(true)
expect(Number.isInteger(p.y)).toBe(true)
}
})
})
})
})