/** * 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 = [ 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() 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() // 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) } }) }) }) })