import { describe, expect, it } from 'vitest' import { getSharedDataRegistry, getSharedMountedArchives, PET_SUB_SKILL_IDS, UNIVERSAL_PLAYER_SKILL_IDS, } from '../../src/game/engine/data-registry.ts' import { compute5BandScaling, computeDiminishingReturns, computeLinearScaling, computeSkillManaCost256, evaluateCalc, } from '../../src/game/engine/calc-ast.ts' import { FIXED_ONE, UnitStatList } from '../../src/game/engine/stat-list.ts' import { StateBus } from '../../src/game/engine/state-bus.ts' import { computeCorpseExplosion113c, computeEffectiveResistance, executeSUnitDmg, resolveVenomPoisonApplication, type CombatUnitContext, } from '../../src/game/engine/combat-pipeline.ts' import { AnimDispatcher, computeEffectiveFcr, computeEffectiveIas, } from '../../src/game/engine/anim-dispatcher.ts' import { MissileEngine } from '../../src/game/engine/missile-engine.ts' import { AuraScanner } from '../../src/game/engine/aura-scanner.ts' import { SummonManager } from '../../src/game/engine/summon-manager.ts' import { WorldArena } from '../../src/game/engine/world-arena.ts' import { evaluateSkill113c } from '../../src/game/skills/registry.ts' import { UnitSpriteLoader } from '../../src/render/unit-sprites.ts' import { createAndCastSkillInArena, evaluateSkillCurve113c } from '../helpers/skill-test-harness.ts' describe('R1 Universal 1.13c Skill Engine & 7-Class Sandbox Foundation', () => { it('R1.1: loads and indexes all 1.13c Excel tables and AnimData.d2 (570,304 bytes / 3,558 records) from /usr/local/google/home/taodao/d2-data', async () => { const registry = await getSharedDataRegistry() // Verify AnimData.d2 integrity expect(registry.animData.bytesConsumed).toBe(570304) expect(registry.animData.all.length).toBe(3558) // Verify all 221 player skills (210 class skills across 7 classes + 11 universal player skills) const allPlayerSkills = registry.getAllPlayerSkills() expect(allPlayerSkills.length).toBe(221) const classSkills = allPlayerSkills.filter(s => s.charClass !== '') expect(classSkills.length).toBe(210) for (const cls of ['ama', 'sor', 'nec', 'pal', 'bar', 'dru', 'ass']) { expect(classSkills.filter(s => s.charClass === cls).length).toBe(30) } for (const uniId of UNIVERSAL_PLAYER_SKILL_IDS) { expect(registry.getSkillById(uniId)).toBeDefined() } // Verify all 17 pet/trap AI sub-skills (IDs 281..338) const petSubSkills = registry.getPetSubSkills() expect(petSubSkills.length).toBe(17) expect(petSubSkills.map(s => s.id)).toEqual([...PET_SUB_SKILL_IDS]) // Verify 1.13c specific table values from Patch_D2.mpq const ce = registry.getSkillById(74)! // Corpse Explosion expect(ce.param1).toBe(70) expect(ce.param2).toBe(120) const hydra = registry.getSkillById(62)! // Hydra 1.13c cooldown = 40 frames (down from 50 in 1.10) expect(Number(hydra.delay)).toBe(40) expect(hydra.eMin).toBe(28) expect(hydra.eMax).toBe(39) const ww = registry.getSkillById(151)! // Whirlwind 1.13c mana reduced 50% expect(ww.mana).toBe(25) expect(ww.manaShift).toBe(7) expect(computeSkillManaCost256(ww, 1).manaCost).toBe(12) }) it('R1.1: evaluates D2Common calc AST formulas, 5-band scaling, 1.13c edge cases, and strictly base-only blvl synergies (Marrowwalk fix)', async () => { const registry = await getSharedDataRegistry() const fireWall = registry.getSkillById(51)! const boneWall = registry.getSkillById(78)! const fireGolem = registry.getSkillById(94)! const royalChain = registry.getMissileById(568)! // Edge Case #3: `royalstrikechainlightning` row 568 has `pSrvHitFunc = "*12"` and `pCltHitFunc = "*16"` expect(royalChain.pSrvHitFunc).toBe(12) expect(royalChain.pCltHitFunc).toBe(16) // Edge Case #1: Unclosed `(` in Fire Wall `EDmgSymPerCalc = "(skill('Warmth'.blvl)*par8+skill('Inferno'.blvl)*par7"` const statList = new UnitStatList(registry) statList.setBaseSkillLevel(37, 20) // Warmth blvl = 20 (* par8=4 => 80) statList.setBaseSkillLevel(41, 10) // Inferno blvl = 10 (* par7=1 => 10) // Add +10 bonus skills and level 33 charges to verify they do NOT leak into `.blvl`! statList.setBonusSkillLevel(37, 10) statList.setChargedSkillLevel(41, 33) const fwSyn = evaluateCalc(fireWall.eDmgSymPerCalc, { registry, activeSkill: fireWall, slvl: 20, blvl: 20, getBaseSkillLevel: id => statList.getBaseSkillLevel(id), getEffectiveSkillLevel: id => statList.getEffectiveSkillLevel(id), }) expect(fwSyn).toBe(20 * 4 + 10 * 1) // 90% // Edge Case #2: Unknown bare identifier `par34` in Bone Wall `calc2` evaluates to 0 const bwCalc2 = evaluateCalc(boneWall.calc2, { registry, activeSkill: boneWall, slvl: 10, blvl: 10, }) expect(bwCalc2).toBe(0) // Edge Case #4: Two-dot `sklvl('Holy Fire'.ln56.edmn)` in FireGolem SkillDesc const fgHolyFire = evaluateCalc("sklvl('Holy Fire'.ln56.edmn)", { registry, activeSkill: fireGolem, slvl: 10, blvl: 10, }) expect(fgHolyFire).toBeGreaterThan(0) // Verify Marrowwalk / +skills synergy exclusion on Bone Spear (ID 84, synergies: Bone Wall 78, Bone Prison 88, Teeth 67, Bone Armor 68) const boneSpearCurve = await evaluateSkillCurve113c(84, [67, 68, 78, 88]) expect(boneSpearCurve.slvl20WithSynergies.minElemDmg).toBeGreaterThan(boneSpearCurve.slvl20.minElemDmg * 2) // When synergy skills only have +20 bonus skills and level 33 item charges (blvl = 0), synergy bonus must be 0! expect(boneSpearCurve.slvl20WithBonusSkillsOnly.synergyBonusPct).toBe(0) expect(boneSpearCurve.slvl20WithBonusSkillsOnly.minElemDmg).toBe(boneSpearCurve.slvl20.minElemDmg) // Verify linear & diminishing returns macros expect(computeLinearScaling(10, 5, 20)).toBe(105) expect(computeDiminishingReturns(0, 35, 20)).toBeGreaterThan(25) expect(compute5BandScaling(20, 10, 2, 4, 6, 8, 10)).toBe(10 + 7 * 2 + 8 * 4 + 4 * 6) }) it('R1.2: enforces Curse Exclusivity (cursetype=1), STATE_ATTRACT (cursetype=2) overwrite immunity, Aura stacking, and reactive auraeventfunc', async () => { const registry = await getSharedDataRegistry() const monsterStats = new UnitStatList(registry) const monsterStateBus = new StateBus(monsterStats, registry) // 1. Apply Amplify Damage (`cursetype = 1`) const amp = monsterStateBus.applyState({ stateNameOrId: 'amplifydamage', sourceSkillId: 66, slvl: 10, durationFrames: 200, currentFrame: 10, stats: { amplify_phys_pierce: 100 }, }) expect(amp.applied).toBe(true) expect(monsterStateBus.hasState('amplifydamage')).toBe(true) // 2. Apply Decrepify (`cursetype = 1`) -> overwrites Amplify Damage const dec = monsterStateBus.applyState({ stateNameOrId: 'decrepify', sourceSkillId: 87, slvl: 5, durationFrames: 150, currentFrame: 20, stats: { amplify_phys_pierce: 50 }, }) expect(dec.applied).toBe(true) expect(dec.evictedState).toBe('amplifydamage') expect(monsterStateBus.hasState('amplifydamage')).toBe(false) expect(monsterStateBus.hasState('decrepify')).toBe(true) // 3. Apply Attract (`cursetype = 2`) -> coexists or locks monster, and blocks subsequent `cursetype = 1` curses! const attr = monsterStateBus.applyState({ stateNameOrId: 'attract', sourceSkillId: 86, slvl: 10, durationFrames: 300, currentFrame: 30, curseTypeOverride: 2, }) expect(attr.applied).toBe(true) expect(monsterStateBus.hasState('attract')).toBe(true) const blockedLowerRes = monsterStateBus.applyState({ stateNameOrId: 'lowerresist', sourceSkillId: 91, slvl: 20, durationFrames: 500, currentFrame: 40, curseTypeOverride: 1, }) expect(blockedLowerRes.applied).toBe(false) expect(blockedLowerRes.blockedByAttract).toBe(true) expect(monsterStateBus.hasState('attract')).toBe(true) expect(monsterStateBus.hasState('lowerresist')).toBe(false) // 4. Aura stacking & level override const palStats = new UnitStatList(registry) const palStateBus = new StateBus(palStats, registry) palStateBus.applyState({ stateNameOrId: 'might', sourceUnitId: 'merc', slvl: 10, durationFrames: 55, currentFrame: 0, stats: { damagepercent: 130 }, isAuraOverride: true, }) palStateBus.applyState({ stateNameOrId: 'might', sourceUnitId: 'paladin', slvl: 20, durationFrames: 30, currentFrame: 0, stats: { damagepercent: 230 }, isAuraOverride: true, }) palStateBus.applyState({ stateNameOrId: 'fanaticism', sourceUnitId: 'paladin', slvl: 20, durationFrames: 55, currentFrame: 0, stats: { damagepercent: 373, skill_ias: 35 }, isAuraOverride: true, }) // Higher Might (230) + Fanaticism (373) = 603% damagepercent while both active expect(palStats.getModifierBonus('damagepercent')).toBe(230 + 373) // When slvl 20 Might expires at frame 35, slvl 10 Might (130) remains active alongside Fanaticism (373)! palStateBus.tick(35) expect(palStats.getModifierBonus('damagepercent')).toBe(130 + 373) // 5. Reactive `auraeventfunc`: Energy Shield + Telekinesis blvl synergy & 1.13c Blood Golem no-negative-life rule const sorcStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE, maxmana: 1000 * FIXED_ONE, mana: 1000 * FIXED_ONE, }) sorcStats.setBaseSkillLevel(43, 16) // 16 hard points in Telekinesis => (32 - 16)/16 = 1.0 mana per HP redirected! const sorcBus = new StateBus(sorcStats, registry) sorcBus.applyState({ stateNameOrId: 'energyshield', sourceSkillId: 58, slvl: 20, stats: { energyshield_pct: 75 }, }) const esOut = sorcBus.triggerReactiveEvents({ event: 'absorbdamage', incomingPhys256: 200 * FIXED_ONE, incomingElem256: 0, }) expect(esOut.absorbedPhys256).toBe(150 * FIXED_ONE) expect(esOut.manaDrained256).toBe(150 * FIXED_ONE) // 1:1 ratio with 16 hard points in Telekinesis // Blood Golem 1.13c: taking melee damage does NOT drain Necromancer HP const necStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 800 * FIXED_ONE }) const golemBus = new StateBus(new UnitStatList(registry), registry) golemBus.applyState({ stateNameOrId: 'bloodgolem', sourceSkillId: 85, slvl: 20 }) golemBus.triggerReactiveEvents({ event: 'damagedinmelee', incomingPhys256: 500 * FIXED_ONE, ownerUnitStatList: necStats, }) expect(necStats.getHp()).toBe(800) // Unchanged when golem takes damage! golemBus.triggerReactiveEvents({ event: 'domeleedamage', incomingPhys256: 200 * FIXED_ONE, ownerUnitStatList: necStats, }) expect(necStats.getHp()).toBe(860) // Healed +30% of 200 = +60 HP on golem attack! }) it('R1.2: enforces 1.13c SUnitDmg combat pipeline rules (Conviction/LR 1/5 immunity break, Blessed Hammer vs Magic Immune Undead/Demon, Corpse Explosion 70-120%, Venom 10f clamp)', async () => { const registry = await getSharedDataRegistry() // 1. Conviction + Lower Resist 1/5 immunity-break rule // Base 110% Fire Res vs Conviction (-60%) + Lower Resist (-45%) = -105% raw -> 105 / 5 = 21% pierce -> 110 - 21 = 89% (< 100, broken!) // Then -25% passive_fire_pierce applies at 100% -> 89 - 25 = 64% effective resistance! const brokenRes = computeEffectiveResistance({ baseRes: 110, convictionPierce: 60, lowerResistPierce: 45, passiveEnemyPierce: 25, }) expect(brokenRes.isImmune).toBe(false) expect(brokenRes.resAfterConvictionLR).toBe(89) expect(brokenRes.effectiveRes).toBe(64) // Base 130% Cold Res vs Conviction (-60%) -> 60 / 5 = 12% -> 118% >= 100 -> still Immune! Level 20 Cold Mastery (-115%) cannot apply! const unbrokenRes = computeEffectiveResistance({ baseRes: 130, convictionPierce: 60, passiveEnemyPierce: 115, }) expect(unbrokenRes.isImmune).toBe(true) expect(unbrokenRes.effectiveRes).toBe(100) // 2. 1.13c Blessed Hammer (`skillId = 112`): does NOT ignore Magic Resistance of Undead or Demons! const palStats = new UnitStatList(registry) palStats.setBaseStat('concentration_damage_pct', 300) const pal: CombatUnitContext = { id: 'pal', name: 'Paladin', statList: palStats, stateBus: new StateBus(palStats, registry), } const magicImmuneUndeadStats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, magicresist: 100, // Magic Immune Undead (e.g. Achmel / Plague Bearer in 1.13c) }) const magicImmuneUndead: CombatUnitContext = { id: 'achmel', name: 'Magic Immune Undead', statList: magicImmuneUndeadStats, stateBus: new StateBus(magicImmuneUndeadStats, registry), isUndead: true, } const bhVsImmuneUndead = executeSUnitDmg(pal, magicImmuneUndead, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: 400 * FIXED_ONE, elemMax256: 400 * FIXED_ONE, concentrationBonusPct: 300, }) expect(bhVsImmuneUndead.immuneToElem).toBe(true) expect(bhVsImmuneUndead.totalDamage).toBe(0) // Against 50% Magic Resist Undead, Blessed Hammer applies +50% Concentration (150% -> 2.5x) and +50% Undead bonus (1.5x), then 50% magic resist: // 400 * 2.5 * 1.5 * 0.5 = 750 damage! const magicRes50UndeadStats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, magicresist: 50, }) const magicRes50Undead: CombatUnitContext = { id: 'mummy', name: '50% Magic Res Undead', statList: magicRes50UndeadStats, stateBus: new StateBus(magicRes50UndeadStats, registry), isUndead: true, } const bhVs50Undead = executeSUnitDmg(pal, magicRes50Undead, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: 400 * FIXED_ONE, elemMax256: 400 * FIXED_ONE, concentrationBonusPct: 300, }) expect(bhVs50Undead.immuneToElem).toBe(false) expect(bhVs50Undead.totalDamage).toBe(750) // 3. 1.13c Corpse Explosion (`skillId = 74`): 70%–120% base monster HP (50% Phys + 50% Fire) const ceMin = computeCorpseExplosion113c({ corpseBaseHp: 1000, rollPct: 70 }) const ceMax = computeCorpseExplosion113c({ corpseBaseHp: 1000, rollPct: 120 }) expect(ceMin.minRollPct).toBe(70) expect(ceMax.maxRollPct).toBe(120) expect(ceMin.finalTotalDamage).toBe(700) expect(ceMin.finalPhysDamage).toBe(350) expect(ceMin.finalFireDamage).toBe(350) expect(ceMax.finalTotalDamage).toBe(1200) // 4. 1.13c Venom (`skillId = 278`): 10-frame duration clamp preserving combined poison bitrates const venomCalc = resolveVenomPoisonApplication({ venomActive: true, venomBitrate256: 40 * FIXED_ONE, otherPoisonSources: [ { bitrate256: 10 * FIXED_ONE, frames: 250 }, // 10-second poison charm clamped to 10 frames! ], }) expect(venomCalc.durationFrames).toBe(10) expect(venomCalc.bitrate256).toBe(50 * FIXED_ONE) expect(venomCalc.totalDamage).toBe(500) }) it('R1.3: executes AnimDispatcher (EIAS/EFCR, STATE_SKILLDELAY), MissileEngine (NextDelay, Pierce), SummonManager (PetType caps & 17 sub-skills), and all 221 player skills', async () => { const registry = await getSharedDataRegistry() // 1. EIAS / EFCR diminishing returns expect(computeEffectiveFcr(105)).toBe(Math.trunc((120 * 105) / 225)) // 56 EFCR expect(computeEffectiveIas({ itemIas: 60, skillIas: 35, weaponWsm: -20 })).toBe(75) // capped at +75 // 2. AnimDispatcher STATE_SKILLDELAY on Hydra (`skillId = 62`, `delay = 40` frames in 1.13c) const sorcStats = new UnitStatList(registry) const sorcBus = new StateBus(sorcStats, registry) const dispatcher = new AnimDispatcher({ registry, statList: sorcStats, stateBus: sorcBus, charToken: 'so', weaponClass: 'hth', }) const hydraRec = registry.getSkillById(62)! const firstCast = dispatcher.startSkillAnimation({ skill: hydraRec, slvl: 20, currentTick: 10 }) expect(firstCast.allowed).toBe(true) for (let t = 11; t <= 30; t += 1) { const res = dispatcher.tick(t, () => {}) if (res.actionFired) break } expect(dispatcher.isSkillDelayActive(25)).toBe(true) const blockedRecast = dispatcher.startSkillAnimation({ skill: hydraRec, slvl: 20, currentTick: 25 }) expect(blockedRecast.allowed).toBe(false) expect(blockedRecast.reason).toBe('skill_delay') // 3. MissileEngine `NextDelay` immunity gate (`Multiple Shot` 12 / `Nova` 48 `nextDelay = 4` frames) const missileEngine = new MissileEngine(registry) const dummyStats = new UnitStatList(registry, { maxhp: 10000 * FIXED_ONE, hitpoints: 10000 * FIXED_ONE }) const dummy: CombatUnitContext = { id: 'dummy-1', name: 'Dummy', statList: dummyStats, stateBus: new StateBus(dummyStats, registry), } const posMap = new Map([['dummy-1', { x: 18, y: 0 }]]) const owner: CombatUnitContext = { id: 'ama', name: 'Amazon', statList: sorcStats, stateBus: sorcBus } // Spawn 2 `arrow` missiles with `NextDelay = 4` (`multiple shot` arrow `shockfield` or `nova`) missileEngine.spawnMissile({ missileNameOrId: 'nova', sourceSkillId: 48, slvl: 20, owner, startX: 0, startY: 0, targetX: 18, targetY: 0, dmgPacket: { skillId: 48, attackKind: 'spell', elemType: 'ltng', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true }, }) missileEngine.spawnMissile({ missileNameOrId: 'nova', sourceSkillId: 48, slvl: 20, owner, startX: 0, startY: 0, targetX: 18, targetY: 0, dmgPacket: { skillId: 48, attackKind: 'spell', elemType: 'ltng', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true }, }) const step1 = missileEngine.tick(100, [dummy], posMap) // Because `nova` has `NextDelay = 4`, only the FIRST nova missile hits `dummy-1` on tick 100! expect(step1.hits.length).toBe(1) expect(missileEngine.isTargetInNextDelay('dummy-1', 102)).toBe(true) expect(missileEngine.isTargetInNextDelay('dummy-1', 105)).toBe(false) // 4. SummonManager `PetType.txt` group exclusivity & 5-trap FIFO cap const summonManager = new SummonManager(registry) // Lay 6 Lightning Sentries (`271`) -> capped at 5 active traps with oldest FIFO evicted for (let i = 0; i < 6; i += 1) { summonManager.createPet({ owner, skillId: 271, slvl: 20 }) } expect(summonManager.getActivePets().filter(p => p.petType === 'assassintrap').length).toBe(5) // Druid Spirit Wolf (`227`) vs Summon Grizzly (`247`) (`group = 1` exclusivity) summonManager.createPet({ owner, skillId: 227, slvl: 20 }) summonManager.createPet({ owner, skillId: 227, slvl: 20 }) expect(summonManager.getActivePets().filter(p => p.sourceSkillId === 227).length).toBe(2) const grizzlyOut = summonManager.createPet({ owner, skillId: 247, slvl: 20 }) expect(grizzlyOut.evictedPetUids.length).toBe(2) expect(summonManager.getActivePets().filter(p => p.sourceSkillId === 227).length).toBe(0) expect(summonManager.getActivePets().filter(p => p.sourceSkillId === 247).length).toBe(1) // 5. Evaluate and execute ALL 221 player skills (`210` class skills + `11` universal skills) across WorldArena const allPlayerSkills = registry.getAllPlayerSkills() expect(allPlayerSkills.length).toBe(221) for (const sk of allPlayerSkills) { const evalRes = evaluateSkill113c({ registry, skill: sk, slvl: 20, blvl: 20, statList: sorcStats, }) expect(evalRes.skillId).toBe(sk.id) expect(evalRes.slvl).toBe(20) } // Representative arena execution across all 7 classes + universal skills const sampleSkills = [0, 1, 2, 12, 35, 47, 62, 64, 70, 74, 86, 112, 123, 149, 151, 228, 245, 271, 278, 280] for (const sid of sampleSkills) { const { outcome, debugState } = await createAndCastSkillInArena({ skillId: sid, slvl: 20 }) expect(outcome.executed).toBe(true) expect(debugState.ready).toBe(true) expect(debugState.castCount).toBeGreaterThanOrEqual(1) } }) it('R1.4: decodes all 7 player classes (am, so, ne, pa, ba, dz, ai), missile .dcc, overlay .dcc, and summon sprites with zero skipped frames', async () => { const mounted = await getSharedMountedArchives() const loader = new UnitSpriteLoader(mounted) for (const cls of ['am', 'so', 'ne', 'pa', 'ba', 'dz', 'ai']) { const sheet = await loader.loadPlayerClassSheet(cls, 'sc', 'hth') expect(sheet.directions).toBe(16) expect(sheet.framesPerDirection).toBeGreaterThanOrEqual(6) expect(sheet.skipped).toBe(0) } const fireballSprite = await loader.loadMissileSprite('fireball') expect(fireballSprite.groups.length).toBeGreaterThan(0) expect(fireballSprite.groups[0]!.frames.length).toBeGreaterThan(0) const ampOverlay = await loader.loadOverlaySprite('cursampdamage') expect(ampOverlay.groups.length).toBeGreaterThan(0) const skeletonPet = await loader.loadMonsterOrPetSheet('sk', 'nu', '1hs') expect(skeletonPet.directions).toBeGreaterThanOrEqual(8) expect(skeletonPet.skipped).toBe(0) const registry = await getSharedDataRegistry() const arena = new WorldArena({ registry, classCode: 'ass', skillId: 280, slvl: 20 }) arena.triggerCast() const dbg = arena.getDebugState() expect(dbg.charToken).toBe('ai') // Assassin uses 'ai' token per PlrType.txt expect(dbg.castCount).toBe(1) }, 20_000) })