184 lines
6.0 KiB
TypeScript
184 lines
6.0 KiB
TypeScript
/**
|
|
* 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\<CelFile>.dcc`) via `decodeDcc`.
|
|
* - Decodes state/cast overlay `.dcc`/`.dc6` sprites (`data\global\overlays\<Filename>.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\<Code>\`).
|
|
*/
|
|
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<Record<string, string>> = {
|
|
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<string, Promise<CharacterSheet>>()
|
|
private readonly missileSheetCache = new Map<string, Promise<SpriteSheet>>()
|
|
private readonly overlaySheetCache = new Map<string, Promise<SpriteSheet>>()
|
|
private readonly monsterSheetCache = new Map<string, Promise<CharacterSheet>>()
|
|
|
|
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<CharacterSheet> {
|
|
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\<celFile>.dcc`).
|
|
*/
|
|
loadMissileSprite(celFile: string): Promise<SpriteSheet> {
|
|
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<SpriteSheet> => {
|
|
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\<filename>.dcc` with `.dc6` fallback).
|
|
* Accepts either an `Overlay.txt` `Filename` (`CurseAmplifyDamageEffect`) or `overlay` ID (`curseamplifydamage`).
|
|
*/
|
|
loadOverlaySprite(filenameOrOverlayId: string): Promise<SpriteSheet> {
|
|
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<SpriteSheet> => {
|
|
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\<code>\`).
|
|
*/
|
|
loadMonsterOrPetSheet(
|
|
monCode: string,
|
|
mode = 'nu',
|
|
weaponClass = 'hth',
|
|
): Promise<CharacterSheet> {
|
|
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
|
|
}
|
|
}
|