/** * Diablo II: Lord of Destruction v1.13c Unified 7-Class, Missile, Overlay & Summon Sprite Loader (`UnitSpriteLoader`). * * - Maps all 7 player classes (`ama`->`am`, `sor`->`so`, `nec`->`ne`, `pal`->`pa`, `bar`->`ba`, `dru`->`dz`, `ass`->`ai`) * across `d2char.mpq` and `d2exp.mpq` with 16 composite layers (`hd..s8`). * - Decodes missile `.dcc` sprites (`data\global\missiles\.dcc`) via `decodeDcc`. * - Decodes state/cast overlay `.dcc`/`.dc6` sprites (`data\global\overlays\.dcc` or `.dc6`) * and distinguishes `PreDraw === true` (ground aura rings) vs `PreDraw === false` (overhead curses/flashes). * - Decodes summon/trap/morph monster sprites (`data\global\monsters\\`). */ import type { MountedArchives } from '../mpq/mount.ts' import { decodeDcc } from '../formats/dcc.ts' import { decodeDc6 } from '../formats/dc6.ts' import type { SpriteFrame, SpriteSheet } from '../formats/sprite.ts' import { loadCharacterSheet, type CharacterSheet } from '../game/character.ts' export const CLASS_CODE_TO_CHAR_TOKEN: Readonly> = { ama: 'am', am: 'am', amazon: 'am', sor: 'so', so: 'so', sorceress: 'so', nec: 'ne', ne: 'ne', necromancer: 'ne', pal: 'pa', pa: 'pa', paladin: 'pa', bar: 'ba', ba: 'ba', barbarian: 'ba', dru: 'dz', dz: 'dz', druid: 'dz', ass: 'ai', ai: 'ai', assassin: 'ai', } export function resolvePlayerCharToken(classOrToken: string): string { const key = classOrToken.trim().toLowerCase() return CLASS_CODE_TO_CHAR_TOKEN[key] ?? 'so' } export class UnitSpriteLoader { readonly archives: MountedArchives private readonly charSheetCache = new Map>() private readonly missileSheetCache = new Map>() private readonly overlaySheetCache = new Map>() private readonly monsterSheetCache = new Map>() constructor(archives: MountedArchives) { this.archives = archives } /** * Load and cache a composite character sheet for any of the 7 hero classes (`am`, `so`, `ne`, `pa`, `ba`, `dz`, `ai`). */ loadPlayerClassSheet( classOrToken: string, mode = 'sc', weaponClass = 'hth', ): Promise { const token = resolvePlayerCharToken(classOrToken) const anim = mode.trim().toLowerCase() const wclass = weaponClass.trim().toLowerCase() const key = `${token}:${anim}:${wclass}` const existing = this.charSheetCache.get(key) if (existing) return existing const promise = loadCharacterSheet(this.archives, token, anim, wclass) this.charSheetCache.set(key, promise) return promise } /** * Load and cache a missile sprite (`data\global\missiles\.dcc`). */ loadMissileSprite(celFile: string): Promise { const clean = celFile.trim().replace(/\.(dcc|dc6)$/i, '') const key = clean.toLowerCase() const existing = this.missileSheetCache.get(key) if (existing) return existing const promise = (async (): Promise => { const dccPath = `data\\global\\missiles\\${clean}.dcc` const bytes = await this.archives.read(dccPath) const dcc = decodeDcc(bytes) const groups = dcc.directions.map(dir => ({ frames: dir.frames.map( (f): SpriteFrame => ({ width: f.frame.width, height: f.frame.height, indices: f.frame.indices, mask: f.frame.mask, anchorX: dir.box.left, anchorY: dir.box.top, }), ), })) return { groups, width: null } })() this.missileSheetCache.set(key, promise) return promise } /** * Load and cache a state/cast overlay sprite (`data\global\overlays\.dcc` with `.dc6` fallback). * Accepts either an `Overlay.txt` `Filename` (`CurseAmplifyDamageEffect`) or `overlay` ID (`curseamplifydamage`). */ loadOverlaySprite(filenameOrOverlayId: string): Promise { const clean = filenameOrOverlayId.trim().replace(/\.(dcc|dc6)$/i, '') const key = clean.toLowerCase() const existing = this.overlaySheetCache.get(key) if (existing) return existing const promise = (async (): Promise => { const candidates = [ clean, key === 'cursampdamage' || key === 'curseamplifydamage' ? 'CurseAmplifyDamageEffect' : '', 'AuraFanatic', ].filter(Boolean) for (const cand of candidates) { const dccPath = `data\\global\\overlays\\${cand}.dcc` if (this.archives.find(dccPath)) { const bytes = await this.archives.read(dccPath) const dcc = decodeDcc(bytes) const groups = dcc.directions.map(dir => ({ frames: dir.frames.map( (f): SpriteFrame => ({ width: f.frame.width, height: f.frame.height, indices: f.frame.indices, mask: f.frame.mask, anchorX: dir.box.left, anchorY: dir.box.top, }), ), })) return { groups, width: null } } } const dc6Path = `data\\global\\overlays\\${clean}.dc6` const bytes = await this.archives.read(dc6Path) return decodeDc6(bytes) })() this.overlaySheetCache.set(key, promise) return promise } /** * Load and cache a summoned pet, trap, or shapeshift monster sprite (`data\global\monsters\\`). */ loadMonsterOrPetSheet( monCode: string, mode = 'nu', weaponClass = 'hth', ): Promise { const code = monCode.trim().toLowerCase() const anim = mode.trim().toLowerCase() const wclass = weaponClass.trim().toLowerCase() const key = `${code}:${anim}:${wclass}` const existing = this.monsterSheetCache.get(key) if (existing) return existing const promise = loadCharacterSheet( this.archives, code, anim, wclass, `data\\global\\monsters\\${code}\\`, ) this.monsterSheetCache.set(key, promise) return promise } }