/** * Diablo II: Lord of Destruction v1.13c — Universal Skills (Cohort 8) Stress & Parity Suite. * * Covers all 11 Universal Skills (`charclass = ""` in `Skills.txt`): * - Skill #000: Attack (melee physical/durability, ranged bow/crossbow/magicarrow/explosivearrow & ammo depletion) * - Skill #001: Kick (1.13c STR+DEX+boot damage formula, srcDam=0, knockback=true) * - Skill #002: Throw (thrown weapon missile resolution, stack quantity decrement, Throwing Mastery #135 AR/%ED/Crit) * - Skill #003: Unsummon (pet ownership check, PetType.txt unsummon=0 Raven rejection, pet dismissal) * - Skill #004: Left Hand Throw (off-hand throwing weapon missile, off-hand stack decrement, off-hand damage) * - Skill #005: Left Hand Swing (requires equipped off-hand weapon, off-hand physical damage, durability drain) * - Skill #217: Scroll of Identify (targetItem.identified, scroll consumption, already_identified guard, InTown=1) * - Skill #218: Book of Identify (targetItem.identified, tome quantity consumption, empty_tome guard, InTown=1) * - Skill #219: Scroll of Townportal (townPortalSlot.cast, InTown=0 guard, portal replacement, act town resolution) * - Skill #220: Book of Townportal (townPortalSlot.cast, tome quantity consumption, InTown=0 guard, portal replacement) * - Skill #350: Delerium Change (States.txt id=177 delerium morph +33% FRW/IAS/FCR, 1500f duration, toggle unmorph, restrict=2 form guard) */ import { describe, expect, it } from 'vitest' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.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 { AuraScanner } from '../../../src/server/engine/aura-scanner.ts' import { SummonManager } from '../../../src/server/ai/summon-manager.ts' import { evaluateSkill113c, executeSkill113c } from '../../../src/server/skills/registry.ts' import type { CombatUnitContext } from '../../../src/server/engine/combat-pipeline.ts' import { TownPortalSlot } from '../../../src/common/world/portal.ts' function createUnit( registry: Awaited>, id: string, overrides: Partial = {}, ): CombatUnitContext { const statList = overrides.statList ?? new UnitStatList(registry, { level: 80, strength: 100, dexterity: 100, tohit: 5000, armorclass: 100, maxhp: 2000 * FIXED_ONE, hitpoints: 2000 * FIXED_ONE, maxmana: 500 * FIXED_ONE, mana: 500 * FIXED_ONE, }) const stateBus = overrides.stateBus ?? new StateBus(statList, registry) return { id, name: id, statList, stateBus, weaponMinPhys: 40, weaponMaxPhys: 60, ...overrides, } } describe('Cohort 8: Universal Skills (0-5, 217-220, 350) 1.13c Ground Truth Stress Suite', () => { it('Skill #000 (Attack): executes melee strike with durability drain, and ranged bow/crossbow/magic-arrow with ammo management', async () => { const registry = await getSharedDataRegistry() const skill = registry.getSkillById(0)! const missileEngine = new MissileEngine(registry) const auraScanner = new AuraScanner(registry) const summonManager = new SummonManager(registry) // 1. Melee Attack drains weapon durability on hit const meleeCaster = createUnit(registry, 'player-melee', { weaponItemType: 'sword' }) meleeCaster.statList.setBaseStat('durability', 20) meleeCaster.statList.setBaseStat('maxdurability', 40) const target = createUnit(registry, 'enemy-1') const evalRes = evaluateSkill113c({ registry, skill, slvl: 1, blvl: 1, statList: meleeCaster.statList }) const meleeOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster: meleeCaster, targets: [target], targetPositions: new Map([[target.id, { x: 20, y: 0 }]]), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 1, targetX: 20, targetY: 0, }) expect(meleeOut.executed).toBe(true) expect(meleeOut.totalDamageDealt).toBeGreaterThan(0) expect(meleeCaster.statList.getBaseStat('durability')).toBe(19) // 2. Ranged Bow Attack consumes ammo and spawns 'arrow' const bowCaster = createUnit(registry, 'player-bow', { weaponItemType: 'bow', isRanged: true, ammoQuantity: 5, }) const bowOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster: bowCaster, targets: [target], targetPositions: new Map([[target.id, { x: 60, y: 0 }]]), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 2, targetX: 60, targetY: 0, }) expect(bowOut.executed).toBe(true) expect(bowOut.missilesSpawned).toContain('arrow') expect(bowCaster.ammoQuantity).toBe(4) // 3. Ranged Crossbow with 0 ammo is rejected with 'no_ammo' const xbowEmpty = createUnit(registry, 'player-xbow-empty', { weaponItemType: 'crossbow', isRanged: true, ammoQuantity: 0, }) const noAmmoOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster: xbowEmpty, targets: [target], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 3, targetX: 60, targetY: 0, }) expect(noAmmoOut.executed).toBe(false) expect(noAmmoOut.error).toBe('no_ammo') // 4. Fires Magic Arrows bypasses empty quiver and spawns 'magicarrow' const magicBowCaster = createUnit(registry, 'player-magicbow', { weaponItemType: 'bow', isRanged: true, ammoQuantity: 0, firesMagicArrows: true, }) const magicArrowOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster: magicBowCaster, targets: [target], targetPositions: new Map([[target.id, { x: 60, y: 0 }]]), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 4, targetX: 60, targetY: 0, }) expect(magicArrowOut.executed).toBe(true) expect(magicArrowOut.missilesSpawned).toContain('magicarrow') }) it('Skill #001 (Kick): computes 1.13c STR+DEX+boot damage with srcDam=0 and applies knockback', async () => { const registry = await getSharedDataRegistry() const skill = registry.getSkillById(1)! const missileEngine = new MissileEngine(registry) const auraScanner = new AuraScanner(registry) const summonManager = new SummonManager(registry) // STR=100, DEX=100 -> (100+100-20)/4 = 45 min, (100+100-20)/3 = 60 max + boot 15..25 = 60..85 const kicker = createUnit(registry, 'kicker', { weaponMinPhys: 500, // high weapon damage must NOT be used because srcDam=0 for Kick weaponMaxPhys: 600, bootMinDam: 15, bootMaxDam: 25, }) const target = createUnit(registry, 'kick-target') const evalRes = evaluateSkill113c({ registry, skill, slvl: 1, blvl: 1, statList: kicker.statList }) const out = executeSkill113c({ registry, skill, evalResult: evalRes, caster: kicker, targets: [target], targetPositions: new Map([[target.id, { x: 16, y: 0 }]]), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 10, targetX: 16, targetY: 0, }) expect(out.executed).toBe(true) expect(out.notes).toContain('kick_damage:60-85') expect(out.notes).toContain('knockback') // Average of 60..85 is 72 (well below weapon 500..600) expect(out.totalDamageDealt).toBeGreaterThanOrEqual(60) expect(out.totalDamageDealt).toBeLessThan(150) }) it('Skill #002 (Throw) & Skill #004 (Left Hand Throw): resolves missile, decrements stack, and applies Throwing Mastery (#135)', async () => { const registry = await getSharedDataRegistry() const throwSkill = registry.getSkillById(2)! const leftThrowSkill = registry.getSkillById(4)! const missileEngine = new MissileEngine(registry) const auraScanner = new AuraScanner(registry) const summonManager = new SummonManager(registry) const barb = createUnit(registry, 'throw-barb', { isThrowing: true, weaponItemType: 'taxe', weaponQuantity: 3, weaponMinPhys: 50, weaponMaxPhys: 70, offHandIsThrowing: true, offHandWeaponItemType: 'tkni', offHandWeaponQuantity: 2, offHandMinPhys: 30, offHandMaxPhys: 50, }) barb.statList.setBaseSkillLevel(135, 10) // Throwing Mastery slvl 10 const target = createUnit(registry, 'throw-target') const evalThrow = evaluateSkill113c({ registry, skill: throwSkill, slvl: 1, blvl: 1, statList: barb.statList }) expect(evalThrow.missileA).toBe('javelin') const outRight = executeSkill113c({ registry, skill: throwSkill, evalResult: evalThrow, caster: barb, targets: [target], targetPositions: new Map([[target.id, { x: 40, y: 0 }]]), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 20, targetX: 40, targetY: 0, }) expect(outRight.executed).toBe(true) expect(outRight.missilesSpawned).toContain('throwingaxe') expect(barb.weaponQuantity).toBe(2) expect(outRight.notes.some(n => n.startsWith('throwing_mastery:slvl=10'))).toBe(true) expect(outRight.totalDamageDealt).toBeGreaterThan(60) // Boosted by Throwing Mastery %ED // Left Hand Throw (#4) const evalLeftThrow = evaluateSkill113c({ registry, skill: leftThrowSkill, slvl: 1, blvl: 1, statList: barb.statList }) const outLeft = executeSkill113c({ registry, skill: leftThrowSkill, evalResult: evalLeftThrow, caster: barb, targets: [target], targetPositions: new Map([[target.id, { x: 40, y: 0 }]]), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 21, targetX: 40, targetY: 0, }) expect(outLeft.executed).toBe(true) expect(outLeft.missilesSpawned).toContain('throwingknife') expect(barb.offHandWeaponQuantity).toBe(1) // Rejects when stack quantity hits 0 barb.weaponQuantity = 0 const outEmpty = executeSkill113c({ registry, skill: throwSkill, evalResult: evalThrow, caster: barb, targets: [target], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 22, targetX: 40, targetY: 0, }) expect(outEmpty.executed).toBe(false) expect(outEmpty.error).toBe('invalid_throw_or_no_quantity') }) it('Skill #003 (Unsummon): enforces pet ownership, rejects Raven (PetType.txt unsummon=0), and dismisses valid pets', async () => { const registry = await getSharedDataRegistry() const skill = registry.getSkillById(3)! const missileEngine = new MissileEngine(registry) const auraScanner = new AuraScanner(registry) const summonManager = new SummonManager(registry) const caster = createUnit(registry, 'druid-owner') const evalRes = evaluateSkill113c({ registry, skill, slvl: 1, blvl: 1, statList: caster.statList }) // 1. Summon a Raven (skill 221) — cannot be unsummoned const ravenRes = summonManager.createPet({ owner: caster, skillId: 221, slvl: 5 }) expect(ravenRes.created).toBe(true) expect(ravenRes.pet?.unsummonable).toBe(false) // Attempt to unsummon when only Raven exists -> no_pet_to_unsummon const noPetOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 30, targetX: 0, targetY: 0, }) expect(noPetOut.executed).toBe(false) expect(noPetOut.error).toBe('no_pet_to_unsummon') // Direct target on Raven -> pet_not_unsummonable const ravenTargetUnit = createUnit(registry, ravenRes.pet!.uid, { petType: 'raven', petOwnerId: caster.id, unsummonable: false, }) const rejectRavenOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster, targets: [ravenTargetUnit], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 31, targetX: 0, targetY: 0, }) expect(rejectRavenOut.executed).toBe(false) expect(rejectRavenOut.error).toBe('pet_not_unsummonable') // Foreign owner pet -> invalid_pet_owner const foreignPetTarget = createUnit(registry, 'pet-999', { petType: 'golem', petOwnerId: 'other-player', unsummonable: true, }) const rejectOwnerOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster, targets: [foreignPetTarget], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 32, targetX: 0, targetY: 0, }) expect(rejectOwnerOut.executed).toBe(false) expect(rejectOwnerOut.error).toBe('invalid_pet_owner') // Summon Spirit Wolf (skill 227) and unsummon it const wolfRes = summonManager.createPet({ owner: caster, skillId: 227, slvl: 5 }) expect(wolfRes.created).toBe(true) const dismissOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 33, targetX: 0, targetY: 0, }) expect(dismissOut.executed).toBe(true) expect(dismissOut.notes.some(n => n.startsWith('unsummoned_'))).toBe(true) expect(wolfRes.pet?.alive).toBe(false) expect(wolfRes.pet?.hp).toBe(0) }) it('Skill #005 (Left Hand Swing): enforces off-hand weapon presence and uses off-hand physical damage', async () => { const registry = await getSharedDataRegistry() const skill = registry.getSkillById(5)! const missileEngine = new MissileEngine(registry) const auraScanner = new AuraScanner(registry) const summonManager = new SummonManager(registry) const singleWielder = createUnit(registry, 'single-wield', { hasOffHandWeapon: false }) const target = createUnit(registry, 'lhs-target') const evalRes = evaluateSkill113c({ registry, skill, slvl: 1, blvl: 1, statList: singleWielder.statList }) const failOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster: singleWielder, targets: [target], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 40, targetX: 16, targetY: 0, }) expect(failOut.executed).toBe(false) expect(failOut.error).toBe('no_offhand_weapon') const dualWielder = createUnit(registry, 'dual-wield', { hasOffHandWeapon: true, weaponMinPhys: 10, weaponMaxPhys: 12, offHandMinPhys: 80, offHandMaxPhys: 100, }) dualWielder.statList.setBaseStat('durability', 30) dualWielder.statList.setBaseStat('maxdurability', 50) const okOut = executeSkill113c({ registry, skill, evalResult: evalRes, caster: dualWielder, targets: [target], targetPositions: new Map([[target.id, { x: 16, y: 0 }]]), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 41, targetX: 16, targetY: 0, }) expect(okOut.executed).toBe(true) expect(okOut.totalDamageDealt).toBeGreaterThanOrEqual(80) expect(dualWielder.statList.getBaseStat('durability')).toBe(29) }) it('Skills #217–#220 (Scroll & Book of Identify / Townportal): enforces scroll/tome quantity, InTown guard, and TownPortalSlot replacement', async () => { const registry = await getSharedDataRegistry() const scrollId = registry.getSkillById(217)! const bookId = registry.getSkillById(218)! const scrollTp = registry.getSkillById(219)! const bookTp = registry.getSkillById(220)! const missileEngine = new MissileEngine(registry) const auraScanner = new AuraScanner(registry) const summonManager = new SummonManager(registry) const caster = createUnit(registry, 'caster-scrolls', { scrollCount: 1, tomeQuantity: 2, }) const unidItem = { id: 'item-1', code: 'rin', identified: false } const eval217 = evaluateSkill113c({ registry, skill: scrollId, slvl: 1, blvl: 1, statList: caster.statList }) // 1. Scroll of Identify (217) works in town (InTown=1), identifies item, decrements scrollCount const idOut = executeSkill113c({ registry, skill: scrollId, evalResult: eval217, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 50, targetX: 0, targetY: 0, isTown: true, targetItem: unidItem, }) expect(idOut.executed).toBe(true) expect(unidItem.identified).toBe(true) expect(caster.scrollCount).toBe(0) expect(idOut.statesApplied).toEqual([]) // 2. Book of Identify (218) rejects already_identified item without consuming tome charge const eval218 = evaluateSkill113c({ registry, skill: bookId, slvl: 1, blvl: 1, statList: caster.statList }) const alreadyIdOut = executeSkill113c({ registry, skill: bookId, evalResult: eval218, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 51, targetX: 0, targetY: 0, targetItem: unidItem, }) expect(alreadyIdOut.executed).toBe(false) expect(alreadyIdOut.error).toBe('already_identified') expect(caster.tomeQuantity).toBe(2) // 3. Scroll of Townportal (219) is rejected in town (InTown=0) caster.scrollCount = 1 const eval219 = evaluateSkill113c({ registry, skill: scrollTp, slvl: 1, blvl: 1, statList: caster.statList }) const tpInTownOut = executeSkill113c({ registry, skill: scrollTp, evalResult: eval219, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 52, targetX: 0, targetY: 0, isTown: true, }) expect(tpInTownOut.executed).toBe(false) expect(tpInTownOut.error).toBe('cannot_cast_in_town') expect(caster.scrollCount).toBe(1) // 4. Scroll of Townportal (219) & Book of Townportal (220) outside town open and replace portal in TownPortalSlot const tpSlot = new TownPortalSlot() const tp1Out = executeSkill113c({ registry, skill: scrollTp, evalResult: eval219, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 53, targetX: 100, targetY: 200, isTown: false, actNo: 2, levelId: 42, casterPos: { x: 100, y: 200 }, townPortalSlot: tpSlot, }) expect(tp1Out.executed).toBe(true) expect(caster.scrollCount).toBe(0) expect(tpSlot.current()?.fromLevelId).toBe(42) expect(tpSlot.current()?.townLevelId).toBe(40) const eval220 = evaluateSkill113c({ registry, skill: bookTp, slvl: 1, blvl: 1, statList: caster.statList }) const tp2Out = executeSkill113c({ registry, skill: bookTp, evalResult: eval220, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 54, targetX: 300, targetY: 400, isTown: false, actNo: 5, levelId: 111, casterPos: { x: 300, y: 400 }, townPortalSlot: tpSlot, }) expect(tp2Out.executed).toBe(true) expect(caster.tomeQuantity).toBe(1) expect(tpSlot.current()?.fromLevelId).toBe(111) expect(tpSlot.current()?.townLevelId).toBe(109) }) it('Skill #350 (Delerium Change): applies delerium state (+33% FRW/IAS/FCR for 1500f), blocks non-restrict=1 skills, and toggles off on recast', async () => { const registry = await getSharedDataRegistry() const deleriumSkill = registry.getSkillById(350)! const attackSkill = registry.getSkillById(0)! const fireballSkill = registry.getSkillById(47)! const missileEngine = new MissileEngine(registry) const auraScanner = new AuraScanner(registry) const summonManager = new SummonManager(registry) const caster = createUnit(registry, 'delerium-caster') const target = createUnit(registry, 'delerium-target') const eval350 = evaluateSkill113c({ registry, skill: deleriumSkill, slvl: 1, blvl: 1, statList: caster.statList }) // 1. First cast morphs into Bone Fetish ('delerium') with +33% FRW/IAS/FCR for 1500 frames const morphOut = executeSkill113c({ registry, skill: deleriumSkill, evalResult: eval350, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 100, targetX: 0, targetY: 0, }) expect(morphOut.executed).toBe(true) expect(morphOut.statesApplied).toContain('delerium') const st = caster.stateBus.getState('delerium') expect(st).toBeDefined() expect(st?.stats?.['velocitypercent']).toBe(33) expect(st?.stats?.['attackrate']).toBe(33) expect(st?.stats?.['other_animrate']).toBe(33) expect(st?.expireFrame).toBe(1600) // 100 + 1500 // 2. While morphed, normal spell (Fire Ball #47, restrict != 1) is blocked const eval47 = evaluateSkill113c({ registry, skill: fireballSkill, slvl: 10, blvl: 10, statList: caster.statList }) const blockedSpell = executeSkill113c({ registry, skill: fireballSkill, evalResult: eval47, caster, targets: [target], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 101, targetX: 50, targetY: 0, }) expect(blockedSpell.executed).toBe(false) expect(blockedSpell.error).toBe('invalid_form') // 3. While morphed, basic Attack (#0) is permitted const eval0 = evaluateSkill113c({ registry, skill: attackSkill, slvl: 1, blvl: 1, statList: caster.statList }) const allowedAttack = executeSkill113c({ registry, skill: attackSkill, evalResult: eval0, caster, targets: [target], targetPositions: new Map([[target.id, { x: 16, y: 0 }]]), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 102, targetX: 16, targetY: 0, }) expect(allowedAttack.executed).toBe(true) // 4. Second cast of Delerium Change (#350) toggles off morph back to human form const unmorphOut = executeSkill113c({ registry, skill: deleriumSkill, evalResult: eval350, caster, targets: [], targetPositions: new Map(), corpses: [], missileEngine, auraScanner, summonManager, currentTick: 103, targetX: 0, targetY: 0, }) expect(unmorphOut.executed).toBe(true) expect(unmorphOut.notes).toContain('state_toggled_off:delerium') expect(caster.stateBus.hasState('delerium')).toBe(false) }) })