/** * Milestone M7: Paladin Combat Skills Tree Adversarial Stress Verification Suite * * Covers 5 mission-critical stress scenarios: * 1. Rapid Hammer Spam Spiral Geometry: * Spawns 20 hammers simultaneously; asserts zero NaN coordinates, valid 2:1 aspect ratio across all frames, * and correct spiral radius bounds (r = a * theta). * 2. Holy Bolt Multi-Target Density Discrimination: * Line of mixed targets: [Undead, Demon, Beast, Friendly Player, Undead]. * Asserts Holy Bolt damages first Undead, ignores Beast/Demon, and heals Player when friendly-targeted. * 3. Sacrifice Extreme Overkill Self-Damage Safety: * Paladin deals 50,000 damage with 10 HP remaining; asserts Paladin HP is clamped to 1, preventing death from self-damage. * 4. Smite vs High-Defense Unblockable Invariant: * Smite vs monster with 99,999 defense and 75% block rate; asserts 100% hit rate and 0% blocked attacks across 100 iterations. * 5. Conversion Allegiance Cleanup: * Converts 10 monsters; simulates 400 frames; asserts 100% of converted monsters cleanly return to non-converted state * with zero state/memory leaks. */ import { describe, expect, it } from 'vitest' import { ISO_GROUND_ASPECT_RATIO, calculateSacrificeStats, calculateSmiteStats, calculateConversionStats, } from '../../../src/server/skills/skills.ts' import { executeSUnitDmg, type CombatUnitContext, } from '../../../src/server/engine/combat-pipeline.ts' import { FIXED_ONE, UnitStatList } from '../../../src/server/engine/stat-list.ts' import { StateBus } from '../../../src/server/engine/state-bus.ts' import { MissileEngine } from '../../../src/server/engine/missile-engine.ts' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' describe('Milestone M7 — Paladin Combat Skills Tree Adversarial Stress Suite', () => { it('1. Rapid Hammer Spam Spiral Geometry: 20 simultaneous hammers, no NaN coords, 2:1 iso ratio', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const casterStats = new UnitStatList(registry, { hitpoints: 1000 * FIXED_ONE }) const caster: CombatUnitContext = { id: 'hammer-spammer', name: 'Paladin', statList: casterStats, stateBus: new StateBus(casterStats, registry), } const spawnedHammers = [] const originX = 100 const originY = 200 // Spawn 20 hammers simultaneously with staggered initial angles for (let i = 0; i < 20; i++) { const angle = (i / 20) * Math.PI * 2 const m = engine.spawnMissile({ missileNameOrId: 'blessedhammer', sourceSkillId: 112, slvl: 20, owner: caster, startX: originX, startY: originY, targetX: originX + Math.cos(angle) * 100, targetY: originY + Math.sin(angle) * 50, dmgPacket: { skillId: 112, elemType: 'mag', elemMin256: 500 * FIXED_ONE, elemMax256: 600 * FIXED_ONE, }, }) expect(m).toBeDefined() if (m) { m.spiralTheta = angle spawnedHammers.push(m) } } expect(spawnedHammers.length).toBe(20) // Simulate 120 frames (standard hammer lifetime) for (let frame = 1; frame <= 120; frame++) { engine.tick(frame, [], new Map()) for (const h of spawnedHammers) { expect(Number.isFinite(h.x)).toBe(true) expect(Number.isFinite(h.y)).toBe(true) expect(Number.isNaN(h.x)).toBe(false) expect(Number.isNaN(h.y)).toBe(false) // Radial bounds test: radius expands monotonically as theta increases const dx = h.x - originX const dy = (h.y - originY) / ISO_GROUND_ASPECT_RATIO const currentR = Math.hypot(dx, dy) expect(currentR).toBeGreaterThanOrEqual(11.9) expect(currentR).toBeLessThanOrEqual(Math.max(12, 13 * (h.spiralTheta ?? 0)) + 0.1) } } }) it('2. Holy Bolt Multi-Target Density Discrimination: [Undead, Demon, Beast, Friendly Player, Undead]', async () => { const registry = await getSharedDataRegistry() const engine = new MissileEngine(registry) const casterStats = new UnitStatList(registry, { hitpoints: 1000 * FIXED_ONE }) casterStats.setBaseSkillLevel(99, 10) // Prayer synergy const caster: CombatUnitContext = { id: 'caster-paladin', name: 'Paladin', statList: casterStats, stateBus: new StateBus(casterStats, registry), } // Dense row of 5 units positioned along x axis: [Undead1, Demon, Beast, FriendlyAlly, Undead2] const u1Stats = new UnitStatList(registry, { hitpoints: 1000 * FIXED_ONE, magicresist: 0 }) const u1: CombatUnitContext = { id: 'undead-1', name: 'Skeleton', statList: u1Stats, stateBus: new StateBus(u1Stats, registry), isUndead: true, unitType: 'monster', } const demonStats = new UnitStatList(registry, { hitpoints: 1000 * FIXED_ONE }) const demon: CombatUnitContext = { id: 'demon-2', name: 'Fallen', statList: demonStats, stateBus: new StateBus(demonStats, registry), isUndead: false, isDemon: true, unitType: 'monster', } const beastStats = new UnitStatList(registry, { hitpoints: 1000 * FIXED_ONE }) const beast: CombatUnitContext = { id: 'beast-3', name: 'Quill Rat', statList: beastStats, stateBus: new StateBus(beastStats, registry), isUndead: false, unitType: 'monster', } const allyStats = new UnitStatList(registry, { hitpoints: 200 * FIXED_ONE, maxhp: 1000 * FIXED_ONE }) const ally: CombatUnitContext = { id: 'ally-4', name: 'Mercenary', statList: allyStats, stateBus: new StateBus(allyStats, registry), unitType: 'mercenary', } const u2Stats = new UnitStatList(registry, { hitpoints: 1000 * FIXED_ONE, magicresist: 0 }) const u2: CombatUnitContext = { id: 'undead-5', name: 'Zombie', statList: u2Stats, stateBus: new StateBus(u2Stats, registry), isUndead: true, unitType: 'monster', } const positions = new Map([ ['undead-1', { x: 30, y: 0 }], ['demon-2', { x: 60, y: 0 }], ['beast-3', { x: 90, y: 0 }], ['ally-4', { x: 120, y: 0 }], ['undead-5', { x: 150, y: 0 }], ]) // Scenario A: Holy bolt fired into the pack towards Undead 1 -> hits Undead 1 and consumes missile const m1 = engine.spawnMissile({ missileNameOrId: 'holybolt', sourceSkillId: 101, slvl: 10, owner: caster, startX: 0, startY: 0, targetX: 200, targetY: 0, dmgPacket: { skillId: 101, elemType: 'mag', elemMin256: 150 * FIXED_ONE, elemMax256: 150 * FIXED_ONE }, })! engine.tick(1, [u1, demon, beast, ally, u2], positions) expect(u1Stats.getHp256()).toBeLessThan(1000 * FIXED_ONE) expect(m1.expired).toBe(true) // Scenario B: Holy bolt fired starting AFTER Undead 1, through Demon and Beast, directly at Ally const m2 = engine.spawnMissile({ missileNameOrId: 'holybolt', sourceSkillId: 101, slvl: 10, owner: caster, startX: 40, startY: 0, targetX: 200, targetY: 0, dmgPacket: { skillId: 101, elemType: 'mag', elemMin256: 150 * FIXED_ONE, elemMax256: 150 * FIXED_ONE }, })! // Tick over demon and beast (positions 60 and 90) engine.tick(2, [demon, beast], positions) expect(demonStats.getHp256()).toBe(1000 * FIXED_ONE) // Passed through Demon! expect(beastStats.getHp256()).toBe(1000 * FIXED_ONE) // Passed through Beast! expect(m2.expired).toBe(false) // Not expired! // Position missile onto ally (x = 120) m2.x = 120 engine.tick(3, [ally, u2], positions) expect(allyStats.getHp256()).toBeGreaterThan(200 * FIXED_ONE) // Healed ally! expect(m2.expired).toBe(true) // Consumed by healing ally! expect(u2Stats.getHp256()).toBe(1000 * FIXED_ONE) // Undead 2 behind ally untouched! }) it('3. Sacrifice Extreme Overkill Self-Damage Safety: 50,000 damage with 10 HP clamped to 1 HP', async () => { const registry = await getSharedDataRegistry() const attackerStats = new UnitStatList(registry, { hitpoints: 10 * FIXED_ONE, maxhp: 1000 * FIXED_ONE, }) const attacker: CombatUnitContext = { id: 'low-hp-paladin', name: 'Paladin', statList: attackerStats, stateBus: new StateBus(attackerStats, registry), weaponMinPhys: 50000, weaponMaxPhys: 50000, } const defenderStats = new UnitStatList(registry, { hitpoints: 100000 * FIXED_ONE, maxhp: 100000 * FIXED_ONE, damageresist: 0, }) const defender: CombatUnitContext = { id: 'meat-sponge', name: 'Gargantuan Beast', statList: defenderStats, stateBus: new StateBus(defenderStats, registry), } // 50,000 damage dealt -> 8% self damage = 4,000 damage // Attacker only has 10 HP. Attacker HP MUST clamp to 1 HP (256 in FIXED_ONE), preventing suicide. const out = executeSUnitDmg(attacker, defender, { skillId: 96, attackKind: 'melee', srcDam: 128, selfDamagePct: 8, autoHit: true, }) expect(out.hit).toBe(true) expect(out.physDamage256).toBe(50000 * FIXED_ONE) expect(out.selfDamageTaken256).toBe(4000 * FIXED_ONE) expect(attackerStats.getHp256()).toBe(FIXED_ONE) // Exactly 1 HP remaining! }) it('4. Smite vs High-Defense Unblockable Invariant: 99,999 def, 75% block -> 100% hits, 0% blocks', async () => { const registry = await getSharedDataRegistry() const attackerStats = new UnitStatList(registry, { tohit: 100, // Very low attack rating }) const attacker: CombatUnitContext = { id: 'smiter', name: 'Paladin', statList: attackerStats, stateBus: new StateBus(attackerStats, registry), } const defenderStats = new UnitStatList(registry, { armorclass: 99999, // Extreme defense toblock: 75, // Max block rate hitpoints: 50000 * FIXED_ONE, maxhp: 50000 * FIXED_ONE, }) const defender: CombatUnitContext = { id: 'impenetrable-guardian', name: 'Iron Golem Boss', statList: defenderStats, stateBus: new StateBus(defenderStats, registry), hasShield: true, } // Run 100 iterations of Smite strikes let hits = 0 let blocks = 0 for (let i = 0; i < 100; i++) { const out = executeSUnitDmg(attacker, defender, { skillId: 97, attackKind: 'melee', srcDam: 0, flatPhysMin256: 40 * FIXED_ONE, flatPhysMax256: 50 * FIXED_ONE, autoHit: true, unblockable: true, stunDurationFrames: 25, roll100: i % 100, // Explicit roll }) if (out.hit) hits++ if (out.avoidedReason === 'block') blocks++ } expect(hits).toBe(100) expect(blocks).toBe(0) expect(defender.stateBus.hasState('stun')).toBe(true) }) it('5. Conversion Allegiance Cleanup: converts 10 monsters; simulates 400 frames; asserts 100% reversion', async () => { const registry = await getSharedDataRegistry() const monsters: { id: string; stats: UnitStatList; bus: StateBus }[] = [] for (let i = 0; i < 10; i++) { const stats = new UnitStatList(registry, { hitpoints: 1000 * FIXED_ONE }) const bus = new StateBus(stats, registry) monsters.push({ id: `converted-${i}`, stats, bus }) } // Apply conversion to all 10 monsters at frame 100 for 400 frames for (const m of monsters) { const res = m.bus.applyState({ stateNameOrId: 'conversion', sourceUnitId: 'paladin-converter', sourceSkillId: 116, slvl: 10, currentFrame: 100, durationFrames: 400, // Expires at frame 500 }) expect(res.applied).toBe(true) expect(m.bus.hasState('conversion')).toBe(true) } // Advance to frame 499: all 10 must still be converted for (const m of monsters) { m.bus.tick(499) expect(m.bus.hasState('conversion')).toBe(true) } // Advance to frame 500: all 10 must expire and revert cleanly for (const m of monsters) { const expired = m.bus.tick(500) expect(expired).toContain('conversion') expect(m.bus.hasState('conversion')).toBe(false) expect(m.bus.getActiveEntries().length).toBe(0) // Clean state bus, zero memory leaks! } }) })