diablo2-web/src/game/engine/data-registry.ts

1646 lines
59 KiB
TypeScript

/**
* Authoritative Diablo II: Lord of Destruction v1.13c Data Registry (`D2DataRegistry`).
*
* Mounts `/usr/local/google/home/taodao/d2-data` (`d2data.mpq`, `d2char.mpq`,
* `d2exp.mpq`, `Patch_D2.mpq`) in exact overlay precedence order so `Patch_D2.mpq`
* provides authoritative 1.13c `.txt` tables (`Skills.txt`, `SkillDesc.txt`,
* `Missiles.txt`, `States.txt`, `ItemStatCost.txt`, `CharStats.txt`, `MonStats.txt`,
* `MonStats2.txt`, `Overlay.txt`, `PetType.txt`, `ElemTypes.txt`, `Events.txt`,
* `SkillCalc.txt`, `MissCalc.txt`) while `d2exp.mpq` provides `HitClass.txt` and
* `AnimData.d2` (`570,304` bytes, `3,558` records).
*/
import { MpqArchive } from '../../mpq/archive.ts'
import { MountedArchives } from '../../mpq/mount.ts'
import { decodeAnimDataFile, type AnimDataFile, type AnimDataRecord } from '../../formats/animdata.ts'
import { parseTable, numberCell, textCell, type DataTable } from '../tables.ts'
import { CANONICAL_LEVEL_LIGHTING, getLevelLighting, type LevelLightingDef } from '../levels-meta.ts'
export const MPQ_LOAD_ORDER = [
'd2data.mpq',
'd2char.mpq',
'd2exp.mpq',
'Patch_D2.mpq',
] as const
export const UNIVERSAL_PLAYER_SKILL_IDS = [
0, // Attack
1, // Kick
2, // Throw
3, // Unsummon
4, // Left Hand Throw
5, // Left Hand Swing
217, // Scroll of Identify
218, // Book of Identify
219, // Scroll of Townportal
220, // Book of Townportal
350, // Delerium Change
] as const
export const PET_SUB_SKILL_IDS = [
281, // Wake Of Destruction Sentry
292, // Teleport 2 (Shadow Warrior/Master)
294, // Vine Attack (Plague Poppy)
296, // Barbs Aura (Spirit of Barbs)
297, // Wolverine Aura (Heart of Wolverine)
298, // Oak Sage Aura (Oak Sage)
306, // BoltSentry (Charged Bolt Sentry)
307, // CorpseCycler (Carrion Vine)
311, // mon inferno sentry
312, // mon death sentry
313, // sentry lightning
314, // fenris rage
324, // death sentry ltng
325, // VineCycler (Solar Creeper)
326, // BearSmite (Summon Grizzly)
337, // HydraMissile
338, // NecromageMissile
] as const
export interface SkillRecord {
readonly id: number
readonly name: string
readonly charClass: string // 'ama' | 'sor' | 'nec' | 'pal' | 'bar' | 'dru' | 'ass' | ''
readonly skillDesc: string
readonly srvStFunc: number
readonly srvDoFunc: number
readonly srvPrgFunc1: number
readonly srvPrgFunc2: number
readonly srvPrgFunc3: number
readonly prgCalc1: string
readonly prgCalc2: string
readonly prgCalc3: string
readonly prgDam: number
readonly srvMissile: string
readonly srvMissileA: string
readonly srvMissileB: string
readonly srvMissileC: string
readonly cltStFunc: number
readonly cltDoFunc: number
readonly cltMissile: string
readonly cltMissileA: string
readonly cltMissileB: string
readonly cltMissileC: string
readonly cltMissileD: string
readonly passive: boolean
readonly passiveState: string
readonly passiveStat1: string
readonly passiveCalc1: string
readonly passiveStat2: string
readonly passiveCalc2: string
readonly passiveStat3: string
readonly passiveCalc3: string
readonly passiveStat4: string
readonly passiveCalc4: string
readonly passiveStat5: string
readonly passiveCalc5: string
readonly aura: boolean
readonly auraFilter: number
readonly auraState: string
readonly auraTgtState: string
readonly auraLenCalc: string
readonly auraRangeCalc: string
readonly auraStat1: string
readonly auraStatCalc1: string
readonly auraStat2: string
readonly auraStatCalc2: string
readonly auraStat3: string
readonly auraStatCalc3: string
readonly auraStat4: string
readonly auraStatCalc4: string
readonly auraStat5: string
readonly auraStatCalc5: string
readonly auraStat6: string
readonly auraStatCalc6: string
readonly auraEvent1: string
readonly auraEventFunc1: number
readonly auraEvent2: string
readonly auraEventFunc2: number
readonly auraEvent3: string
readonly auraEventFunc3: number
readonly summon: string
readonly petType: string
readonly petMax: string
readonly sumMode: string
readonly sumSkill1: string
readonly sumSk1Calc: string
readonly sumSkill2: string
readonly sumSk2Calc: string
readonly sumSkill3: string
readonly sumSk3Calc: string
readonly sumSkill4: string
readonly sumSk4Calc: string
readonly sumSkill5: string
readonly sumSk5Calc: string
readonly sumUMod: number
readonly sumOverlay: string
readonly anim: string
readonly seqTrans: string
readonly monAnim: string
readonly seqNum: number
readonly seqInput: number
readonly range: string
readonly selectProc: number
readonly reqLevel: number
readonly reqStr: number
readonly reqDex: number
readonly reqInt: number
readonly reqVit: number
readonly reqSkill1: string
readonly reqSkill2: string
readonly reqSkill3: string
readonly restrict: number
readonly state1: string
readonly state2: string
readonly state3: string
readonly delay: string
readonly checkFunc: number
readonly nocostinstate: boolean
readonly decquant: boolean
readonly manaShift: number
readonly mana: number
readonly lvlMana: number
readonly minMana: number
readonly interrupt: boolean
readonly inTown: boolean
readonly periodic: boolean
readonly perDelay: string
readonly finishing: boolean
readonly progressive: boolean
readonly calc1: string
readonly calc2: string
readonly calc3: string
readonly calc4: string
readonly param1: number
readonly param2: number
readonly param3: number
readonly param4: number
readonly param5: number
readonly param6: number
readonly param7: number
readonly param8: number
readonly toHit: number
readonly toHitLev: number
readonly toHitCalc: string
readonly resultFlags: number
readonly hitFlags: number
readonly hitClass: number
readonly kick: boolean
readonly hitShift: number
readonly srcDam: number
readonly minDam: number
readonly minLevDam1: number
readonly minLevDam2: number
readonly minLevDam3: number
readonly minLevDam4: number
readonly minLevDam5: number
readonly maxDam: number
readonly maxLevDam1: number
readonly maxLevDam2: number
readonly maxLevDam3: number
readonly maxLevDam4: number
readonly maxLevDam5: number
readonly dmgSymPerCalc: string
readonly eType: string
readonly eMin: number
readonly eMinLev1: number
readonly eMinLev2: number
readonly eMinLev3: number
readonly eMinLev4: number
readonly eMinLev5: number
readonly eMax: number
readonly eMaxLev1: number
readonly eMaxLev2: number
readonly eMaxLev3: number
readonly eMaxLev4: number
readonly eMaxLev5: number
readonly eDmgSymPerCalc: string
readonly eLen: number
readonly eLevLen1: number
readonly eLevLen2: number
readonly eLevLen3: number
readonly eLenSymPerCalc: string
readonly aiType: number
readonly aiBonus: number
readonly castOverlay: string
readonly cltOverlayA: string
readonly cltOverlayB: string
readonly raw: Readonly<Record<string, string>>
}
export interface MissileRecord {
readonly id: number
readonly name: string
readonly pCltDoFunc: number
readonly pCltHitFunc: number
readonly pSrvDoFunc: number
readonly pSrvHitFunc: number
readonly pSrvDmgFunc: number
readonly srvCalc1: string
readonly param1: number
readonly param2: number
readonly param3: number
readonly param4: number
readonly param5: number
readonly cltCalc1: string
readonly cParam1: number
readonly cParam2: number
readonly cParam3: number
readonly cParam4: number
readonly cParam5: number
readonly sHitCalc1: string
readonly sHitPar1: number
readonly sHitPar2: number
readonly sHitPar3: number
readonly cHitCalc1: string
readonly cHitPar1: number
readonly cHitPar2: number
readonly cHitPar3: number
readonly dmgCalc1: string
readonly dParam1: number
readonly dParam2: number
readonly vel: number
readonly maxVel: number
readonly velLev: number
readonly accel: number
readonly range: number
readonly levRange: number
readonly light: number
readonly flicker: number
readonly red: number
readonly green: number
readonly blue: number
readonly initSteps: number
readonly activate: number
readonly loopAnim: number
readonly celFile: string
readonly animRate: number
readonly animLen: number
readonly animSpeed: number
readonly subLoop: boolean
readonly subStart: number
readonly subStop: number
readonly collideType: number
readonly collideKill: boolean
readonly collideFriend: boolean
readonly lastCollide: boolean
readonly collision: boolean
readonly clientCol: boolean
readonly nextHit: boolean
readonly nextDelay: number
readonly size: number
readonly returnFire: boolean
readonly trans: number
readonly alwaysExplode: boolean
readonly explosion: boolean
readonly town: boolean
readonly noUniqueMod: boolean
readonly noMultiShot: boolean
readonly holy: number
readonly canSlow: boolean
readonly canDestroy: boolean
readonly toHit: boolean
readonly pierce: boolean
readonly skill: string
readonly hitShift: number
readonly srcDamage: number
readonly minDamage: number
readonly minLevDam1: number
readonly minLevDam2: number
readonly minLevDam3: number
readonly minLevDam4: number
readonly minLevDam5: number
readonly maxDamage: number
readonly maxLevDam1: number
readonly maxLevDam2: number
readonly maxLevDam3: number
readonly maxLevDam4: number
readonly maxLevDam5: number
readonly dmgSymPerCalc: string
readonly eType: string
readonly eMin: number
readonly eMinLev1: number
readonly eMinLev2: number
readonly eMinLev3: number
readonly eMinLev4: number
readonly eMinLev5: number
readonly eMax: number
readonly eMaxLev1: number
readonly eMaxLev2: number
readonly eMaxLev3: number
readonly eMaxLev4: number
readonly eMaxLev5: number
readonly eDmgSymPerCalc: string
readonly eLen: number
readonly eLevLen1: number
readonly eLevLen2: number
readonly eLevLen3: number
readonly hitClass: number
readonly numDirections: number
readonly localBlood: number
readonly damageRate: number
readonly explosionMissile: string
readonly subMissile1: string
readonly subMissile2: string
readonly subMissile3: string
readonly hitSubMissile1: string
readonly hitSubMissile2: string
readonly hitSubMissile3: string
readonly hitSubMissile4: string
readonly cltSubMissile1: string
readonly cltSubMissile2: string
readonly cltSubMissile3: string
readonly cltHitSubMissile1: string
readonly cltHitSubMissile2: string
readonly cltHitSubMissile3: string
readonly cltHitSubMissile4: string
readonly raw: Readonly<Record<string, string>>
}
export interface StateRecord {
readonly id: number
readonly state: string
readonly group: number
readonly remhit: boolean
readonly nosend: boolean
readonly transform: boolean
readonly aura: boolean
readonly cursetype: number
readonly curable: boolean
readonly active: boolean
readonly restrict: boolean
readonly disguise: boolean
readonly blue: boolean
readonly attblue: boolean
readonly dmgblue: boolean
readonly armblue: boolean
readonly rfblue: boolean
readonly rlblue: boolean
readonly rcblue: boolean
readonly stambarblue: boolean
readonly rpblue: boolean
readonly attred: boolean
readonly dmgred: boolean
readonly armred: boolean
readonly rfred: boolean
readonly rlred: boolean
readonly rcred: boolean
readonly rpred: boolean
readonly exp: boolean
readonly plrstaydeath: boolean
readonly monstaydeath: boolean
readonly bossstaydeath: boolean
readonly hide: boolean
readonly shatter: boolean
readonly udead: boolean
readonly life: boolean
readonly green: boolean
readonly pgsv: boolean
readonly nooverlays: boolean
readonly noclear: boolean
readonly bossinv: boolean
readonly meleeonly: boolean
readonly notondead: boolean
readonly overlay1: string
readonly overlay2: string
readonly overlay3: string
readonly overlay4: string
readonly pgsvoverlay: string
readonly castoverlay: string
readonly removerlay: string
readonly stat: string
readonly setfunc: number
readonly remfunc: number
readonly missile: string
readonly skill: string
readonly itemtrans: string
readonly colorpri: number
readonly colorshift: number
readonly lightR: number
readonly lightG: number
readonly lightB: number
readonly onsound: string
readonly offsound: string
readonly raw: Readonly<Record<string, string>>
}
export interface ItemStatCostRecord {
readonly id: number
readonly stat: string
readonly sendBits: number
readonly signed: boolean
readonly valShift: number
readonly damageRelated: boolean
readonly op: number
readonly opParam: number
readonly opBase: string
readonly opStat1: string
readonly opStat2: string
readonly opStat3: string
readonly direct: boolean
readonly maxStat: string
readonly raw: Readonly<Record<string, string>>
}
export interface PetTypeRecord {
readonly idx: number
readonly petType: string
readonly group: number
readonly baseMax: number
readonly warp: boolean
readonly range: boolean
readonly partysend: boolean
readonly unsummon: boolean
readonly automap: boolean
readonly name: string
readonly drawhp: boolean
readonly icontype: number
readonly baseicon: string
readonly mclass1: number
readonly micon1: string
readonly mclass2: number
readonly micon2: string
readonly mclass3: number
readonly micon3: string
readonly raw: Readonly<Record<string, string>>
}
export interface OverlayRecord {
readonly id: number
readonly overlay: string
readonly filename: string
readonly frames: number
readonly animRate: number
readonly initRadius: number
readonly radius: number
readonly loopWaitTime: number
readonly trans: number
readonly xOffset: number
readonly yOffset: number
readonly height1: number
readonly height2: number
readonly height3: number
readonly height4: number
readonly preDraw: boolean
readonly red: number
readonly green: number
readonly blue: number
readonly raw: Readonly<Record<string, string>>
}
export interface MonStats2LightingRecord {
readonly id: string
readonly light: number
readonly lightR: number
readonly lightG: number
readonly lightB: number
readonly shadow: boolean
readonly raw: Readonly<Record<string, string>>
}
export interface MonStatsRecord {
readonly hcIdx: number
readonly id: string
readonly baseId: string
readonly nextInClass: string
readonly translvl: number
readonly nameStr: string
readonly monStatsEx: string
readonly code: string
readonly enabled: boolean
readonly rangedtype: boolean
readonly align: number
readonly isSpawn: boolean
readonly isMelee: boolean
readonly lUndead: boolean
readonly hUndead: boolean
readonly demon: boolean
readonly flying: boolean
readonly opendoors: boolean
readonly boss: boolean
readonly primeevil: boolean
readonly killable: boolean
readonly switchai: boolean
readonly noAura: boolean
readonly neverCount: boolean
readonly petIgnore: boolean
readonly deathDmg: boolean
readonly genericSpawn: boolean
readonly zoo: boolean
readonly velocity: number
readonly run: number
readonly minHP: number
readonly maxHP: number
readonly ac: number
readonly exp: number
readonly a1MinD: number
readonly a1MaxD: number
readonly a1TH: number
readonly a2MinD: number
readonly a2MaxD: number
readonly a2TH: number
readonly s1MinD: number
readonly s1MaxD: number
readonly s1TH: number
readonly resDm: number
readonly resMa: number
readonly resFi: number
readonly resLi: number
readonly resCo: number
readonly resPo: number
readonly skill1: string
readonly sk1mode: string
readonly sk1lvl: number
readonly skill2: string
readonly sk2mode: string
readonly sk2lvl: number
readonly skill3: string
readonly sk3mode: string
readonly sk3lvl: number
readonly raw: Readonly<Record<string, string>>
}
export interface CharStatsRecord {
readonly className: string
readonly classCode: string // 'ama' | 'sor' | 'nec' | 'pal' | 'bar' | 'dru' | 'ass'
readonly token: string // 'am' | 'so' | 'ne' | 'pa' | 'ba' | 'dz' | 'ai'
readonly str: number
readonly dex: number
readonly int: number
readonly vit: number
readonly stamina: number
readonly hpadd: number
readonly percentStr: number
readonly percentDex: number
readonly percentInt: number
readonly percentVit: number
readonly manaRegen: number
readonly toHitFactor: number
readonly walkVelocity: number
readonly runVelocity: number
readonly runDrain: number
readonly lifePerLevel: number
readonly staminaPerLevel: number
readonly manaPerLevel: number
readonly lifePerVitality: number
readonly staminaPerVitality: number
readonly manaPerMagic: number
readonly blockFactor: number
readonly startSkill: string
readonly baseWClass: string
readonly skills: readonly string[]
readonly raw: Readonly<Record<string, string>>
}
/**
* Helper to parse numeric cell while stripping leading `*` (Edge Case #3:
* `royalstrikechainlightning` row 568 has `pSrvHitFunc = "*12"` and `pCltHitFunc = "*16"`).
*/
function parseFuncIdCell(row: Readonly<Record<string, string>>, col: string, fallback = 0): number {
const raw = row[col]
if (raw === undefined) return fallback
const cleaned = raw.trim().replace(/^\*+/, '')
const val = Number(cleaned)
return Number.isFinite(val) ? val : fallback
}
const CLASS_NAME_TO_CODE: Readonly<Record<string, { code: string; token: string }>> = {
amazon: { code: 'ama', token: 'am' },
sorceress: { code: 'sor', token: 'so' },
necromancer: { code: 'nec', token: 'ne' },
paladin: { code: 'pal', token: 'pa' },
barbarian: { code: 'bar', token: 'ba' },
druid: { code: 'dru', token: 'dz' },
assassin: { code: 'ass', token: 'ai' },
}
export class D2DataRegistry {
readonly skillsTable: DataTable
readonly skillDescTable: DataTable
readonly missilesTable: DataTable
readonly statesTable: DataTable
readonly itemStatCostTable: DataTable
readonly charStatsTable: DataTable
readonly monStatsTable: DataTable
readonly monStats2Table: DataTable
readonly overlayTable: DataTable
readonly petTypeTable: DataTable
readonly elemTypesTable: DataTable
readonly hitClassTable: DataTable
readonly eventsTable: DataTable
readonly skillCalcTable: DataTable
readonly missCalcTable: DataTable
readonly animData: AnimDataFile
readonly skillsById = new Map<number, SkillRecord>()
readonly skillsByName = new Map<string, SkillRecord>()
readonly skillDescsByKey = new Map<string, Readonly<Record<string, string>>>()
readonly missilesById = new Map<number, MissileRecord>()
readonly missilesByName = new Map<string, MissileRecord>()
readonly statesById = new Map<number, StateRecord>()
readonly statesByName = new Map<string, StateRecord>()
readonly statsById = new Map<number, ItemStatCostRecord>()
readonly statsByName = new Map<string, ItemStatCostRecord>()
readonly charStatsByCode = new Map<string, CharStatsRecord>()
readonly monStatsByHcIdx = new Map<number, MonStatsRecord>()
readonly monStatsById = new Map<string, MonStatsRecord>()
readonly monStats2ById = new Map<string, MonStats2LightingRecord>()
readonly overlaysByName = new Map<string, OverlayRecord>()
readonly petTypesByIdx = new Map<number, PetTypeRecord>()
readonly petTypesByName = new Map<string, PetTypeRecord>()
readonly elemTypes = new Set<string>()
readonly hitClasses = new Map<string, number>()
readonly events = new Set<string>()
constructor(params: {
skillsTable: DataTable
skillDescTable: DataTable
missilesTable: DataTable
statesTable: DataTable
itemStatCostTable: DataTable
charStatsTable: DataTable
monStatsTable: DataTable
monStats2Table: DataTable
overlayTable: DataTable
petTypeTable: DataTable
elemTypesTable: DataTable
hitClassTable: DataTable
eventsTable: DataTable
skillCalcTable: DataTable
missCalcTable: DataTable
animData: AnimDataFile
}) {
this.skillsTable = params.skillsTable
this.skillDescTable = params.skillDescTable
this.missilesTable = params.missilesTable
this.statesTable = params.statesTable
this.itemStatCostTable = params.itemStatCostTable
this.charStatsTable = params.charStatsTable
this.monStatsTable = params.monStatsTable
this.monStats2Table = params.monStats2Table
this.overlayTable = params.overlayTable
this.petTypeTable = params.petTypeTable
this.elemTypesTable = params.elemTypesTable
this.hitClassTable = params.hitClassTable
this.eventsTable = params.eventsTable
this.skillCalcTable = params.skillCalcTable
this.missCalcTable = params.missCalcTable
this.animData = params.animData
this.indexSkillDescs()
this.indexSkills()
this.indexMissiles()
this.indexStates()
this.indexItemStatCost()
this.indexCharStats()
this.indexMonStats()
this.indexMonStats2()
this.indexOverlays()
this.indexPetTypes()
this.indexElemTypesAndEvents()
}
private indexSkillDescs(): void {
for (const row of this.skillDescTable.rows) {
const key = textCell(row, 'skilldesc').toLowerCase()
if (key) this.skillDescsByKey.set(key, row)
}
}
private indexSkills(): void {
for (const row of this.skillsTable.rows) {
const idStr = row['Id']
const name = textCell(row, 'skill')
if (idStr === undefined || name === '') continue
const id = Number(idStr)
if (!Number.isFinite(id)) continue
const record: SkillRecord = {
id,
name,
charClass: textCell(row, 'charclass').toLowerCase(),
skillDesc: textCell(row, 'skilldesc'),
srvStFunc: parseFuncIdCell(row, 'srvstfunc', 0),
srvDoFunc: parseFuncIdCell(row, 'srvdofunc', 0),
srvPrgFunc1: parseFuncIdCell(row, 'srvprgfunc1', 0),
srvPrgFunc2: parseFuncIdCell(row, 'srvprgfunc2', 0),
srvPrgFunc3: parseFuncIdCell(row, 'srvprgfunc3', 0),
prgCalc1: textCell(row, 'prgcalc1'),
prgCalc2: textCell(row, 'prgcalc2'),
prgCalc3: textCell(row, 'prgcalc3'),
prgDam: numberCell(row, 'prgdam', 0),
srvMissile: textCell(row, 'srvmissile'),
srvMissileA: textCell(row, 'srvmissilea'),
srvMissileB: textCell(row, 'srvmissileb'),
srvMissileC: textCell(row, 'srvmissilec'),
cltStFunc: parseFuncIdCell(row, 'cltstfunc', 0),
cltDoFunc: parseFuncIdCell(row, 'cltdofunc', 0),
cltMissile: textCell(row, 'cltmissile'),
cltMissileA: textCell(row, 'cltmissilea'),
cltMissileB: textCell(row, 'cltmissileb'),
cltMissileC: textCell(row, 'cltmissilec'),
cltMissileD: textCell(row, 'cltmissiled'),
passive: numberCell(row, 'passive', 0) !== 0,
passiveState: textCell(row, 'passivestate'),
passiveStat1: textCell(row, 'passivestat1'),
passiveCalc1: textCell(row, 'passivecalc1'),
passiveStat2: textCell(row, 'passivestat2'),
passiveCalc2: textCell(row, 'passivecalc2'),
passiveStat3: textCell(row, 'passivestat3'),
passiveCalc3: textCell(row, 'passivecalc3'),
passiveStat4: textCell(row, 'passivestat4'),
passiveCalc4: textCell(row, 'passivecalc4'),
passiveStat5: textCell(row, 'passivestat5'),
passiveCalc5: textCell(row, 'passivecalc5'),
aura: numberCell(row, 'aura', 0) !== 0,
auraFilter: numberCell(row, 'aurafilter', 0),
auraState: textCell(row, 'aurastate'),
auraTgtState: textCell(row, 'auratargetstate') || textCell(row, 'auratgtstate'),
auraLenCalc: textCell(row, 'auralencalc'),
auraRangeCalc: textCell(row, 'aurarangecalc'),
auraStat1: textCell(row, 'aurastat1'),
auraStatCalc1: textCell(row, 'aurastatcalc1'),
auraStat2: textCell(row, 'aurastat2'),
auraStatCalc2: textCell(row, 'aurastatcalc2'),
auraStat3: textCell(row, 'aurastat3'),
auraStatCalc3: textCell(row, 'aurastatcalc3'),
auraStat4: textCell(row, 'aurastat4'),
auraStatCalc4: textCell(row, 'aurastatcalc4'),
auraStat5: textCell(row, 'aurastat5'),
auraStatCalc5: textCell(row, 'aurastatcalc5'),
auraStat6: textCell(row, 'aurastat6'),
auraStatCalc6: textCell(row, 'aurastatcalc6'),
auraEvent1: textCell(row, 'auraevent1'),
auraEventFunc1: parseFuncIdCell(row, 'auraeventfunc1', 0),
auraEvent2: textCell(row, 'auraevent2'),
auraEventFunc2: parseFuncIdCell(row, 'auraeventfunc2', 0),
auraEvent3: textCell(row, 'auraevent3'),
auraEventFunc3: parseFuncIdCell(row, 'auraeventfunc3', 0),
summon: textCell(row, 'summon'),
petType: textCell(row, 'pettype'),
petMax: textCell(row, 'petmax'),
sumMode: textCell(row, 'summode'),
sumSkill1: textCell(row, 'sumskill1'),
sumSk1Calc: textCell(row, 'sumsk1calc'),
sumSkill2: textCell(row, 'sumskill2'),
sumSk2Calc: textCell(row, 'sumsk2calc'),
sumSkill3: textCell(row, 'sumskill3'),
sumSk3Calc: textCell(row, 'sumsk3calc'),
sumSkill4: textCell(row, 'sumskill4'),
sumSk4Calc: textCell(row, 'sumsk4calc'),
sumSkill5: textCell(row, 'sumskill5'),
sumSk5Calc: textCell(row, 'sumsk5calc'),
sumUMod: numberCell(row, 'sumumod', 0),
sumOverlay: textCell(row, 'sumoverlay'),
anim: textCell(row, 'anim', 'SC'),
seqTrans: textCell(row, 'seqtrans'),
monAnim: textCell(row, 'monanim'),
seqNum: numberCell(row, 'seqnum', 0),
seqInput: numberCell(row, 'seqinput', 0),
range: textCell(row, 'range', 'none'),
selectProc: numberCell(row, 'SelectProc', 0),
reqLevel: numberCell(row, 'reqlevel', 1),
reqStr: numberCell(row, 'reqstr', 0),
reqDex: numberCell(row, 'reqdex', 0),
reqInt: numberCell(row, 'reqint', 0),
reqVit: numberCell(row, 'reqvit', 0),
reqSkill1: textCell(row, 'reqskill1'),
reqSkill2: textCell(row, 'reqskill2'),
reqSkill3: textCell(row, 'reqskill3'),
restrict: numberCell(row, 'restrict', 0),
state1: textCell(row, 'state1'),
state2: textCell(row, 'state2'),
state3: textCell(row, 'state3'),
delay: textCell(row, 'delay'),
checkFunc: parseFuncIdCell(row, 'checkfunc', 0),
nocostinstate: numberCell(row, 'nocostinstate', 0) !== 0,
decquant: numberCell(row, 'decquant', 0) !== 0,
manaShift: numberCell(row, 'manashift', 8),
mana: numberCell(row, 'mana', 0),
lvlMana: numberCell(row, 'lvlmana', 0),
minMana: numberCell(row, 'minmana', 0),
interrupt: numberCell(row, 'interrupt', 0) !== 0,
inTown: numberCell(row, 'InTown', 0) !== 0,
periodic: numberCell(row, 'periodic', 0) !== 0,
perDelay: textCell(row, 'perdelay'),
finishing: numberCell(row, 'finishing', 0) !== 0,
progressive: numberCell(row, 'progressive', 0) !== 0,
calc1: textCell(row, 'calc1'),
calc2: textCell(row, 'calc2'),
calc3: textCell(row, 'calc3'),
calc4: textCell(row, 'calc4'),
param1: numberCell(row, 'Param1', 0),
param2: numberCell(row, 'Param2', 0),
param3: numberCell(row, 'Param3', 0),
param4: numberCell(row, 'Param4', 0),
param5: numberCell(row, 'Param5', 0),
param6: numberCell(row, 'Param6', 0),
param7: numberCell(row, 'Param7', 0),
param8: numberCell(row, 'Param8', 0),
toHit: numberCell(row, 'ToHit', 0),
toHitLev: numberCell(row, 'LevToHit', 0),
toHitCalc: textCell(row, 'ToHitCalc'),
resultFlags: numberCell(row, 'ResultFlags', 0),
hitFlags: numberCell(row, 'HitFlags', 0),
hitClass: numberCell(row, 'HitClass', 0),
kick: numberCell(row, 'Kick', 0) !== 0,
hitShift: numberCell(row, 'HitShift', 8),
srcDam: numberCell(row, 'SrcDam', 0),
minDam: numberCell(row, 'MinDam', 0),
minLevDam1: numberCell(row, 'MinLevDam1', 0),
minLevDam2: numberCell(row, 'MinLevDam2', 0),
minLevDam3: numberCell(row, 'MinLevDam3', 0),
minLevDam4: numberCell(row, 'MinLevDam4', 0),
minLevDam5: numberCell(row, 'MinLevDam5', 0),
maxDam: numberCell(row, 'MaxDam', 0),
maxLevDam1: numberCell(row, 'MaxLevDam1', 0),
maxLevDam2: numberCell(row, 'MaxLevDam2', 0),
maxLevDam3: numberCell(row, 'MaxLevDam3', 0),
maxLevDam4: numberCell(row, 'MaxLevDam4', 0),
maxLevDam5: numberCell(row, 'MaxLevDam5', 0),
dmgSymPerCalc: textCell(row, 'DmgSymPerCalc'),
eType: textCell(row, 'EType'),
eMin: numberCell(row, 'EMin', 0),
eMinLev1: numberCell(row, 'EMinLev1', 0),
eMinLev2: numberCell(row, 'EMinLev2', 0),
eMinLev3: numberCell(row, 'EMinLev3', 0),
eMinLev4: numberCell(row, 'EMinLev4', 0),
eMinLev5: numberCell(row, 'EMinLev5', 0),
eMax: numberCell(row, 'EMax', 0),
eMaxLev1: numberCell(row, 'EMaxLev1', 0),
eMaxLev2: numberCell(row, 'EMaxLev2', 0),
eMaxLev3: numberCell(row, 'EMaxLev3', 0),
eMaxLev4: numberCell(row, 'EMaxLev4', 0),
eMaxLev5: numberCell(row, 'EMaxLev5', 0),
eDmgSymPerCalc: textCell(row, 'EDmgSymPerCalc'),
eLen: numberCell(row, 'ELen', 0),
eLevLen1: numberCell(row, 'ELevLen1', 0),
eLevLen2: numberCell(row, 'ELevLen2', 0),
eLevLen3: numberCell(row, 'ELevLen3', 0),
eLenSymPerCalc: textCell(row, 'ELenSymPerCalc'),
aiType: numberCell(row, 'aitype', 0),
aiBonus: numberCell(row, 'aibonus', 0),
castOverlay: textCell(row, 'castoverlay'),
cltOverlayA: textCell(row, 'cltoverlaya'),
cltOverlayB: textCell(row, 'cltoverlayb'),
raw: row,
}
this.skillsById.set(id, record)
this.skillsByName.set(name.toLowerCase(), record)
if (record.skillDesc) {
const descLower = record.skillDesc.toLowerCase()
if (!this.skillsByName.has(descLower)) {
this.skillsByName.set(descLower, record)
}
}
}
}
private indexMissiles(): void {
for (const row of this.missilesTable.rows) {
const idStr = row['Id']
const name = textCell(row, 'Missile')
if (idStr === undefined || name === '') continue
const id = Number(idStr)
if (!Number.isFinite(id)) continue
const record: MissileRecord = {
id,
name,
pCltDoFunc: parseFuncIdCell(row, 'pCltDoFunc', 0),
pCltHitFunc: parseFuncIdCell(row, 'pCltHitFunc', 0),
pSrvDoFunc: parseFuncIdCell(row, 'pSrvDoFunc', 0),
pSrvHitFunc: parseFuncIdCell(row, 'pSrvHitFunc', 0),
pSrvDmgFunc: parseFuncIdCell(row, 'pSrvDmgFunc', 0),
srvCalc1: textCell(row, 'SrvCalc1'),
param1: numberCell(row, 'Param1', 0),
param2: numberCell(row, 'Param2', 0),
param3: numberCell(row, 'Param3', 0),
param4: numberCell(row, 'Param4', 0),
param5: numberCell(row, 'Param5', 0),
cltCalc1: textCell(row, 'CltCalc1'),
cParam1: numberCell(row, 'cParam1', 0),
cParam2: numberCell(row, 'cParam2', 0),
cParam3: numberCell(row, 'cParam3', 0),
cParam4: numberCell(row, 'cParam4', 0),
cParam5: numberCell(row, 'cParam5', 0),
sHitCalc1: textCell(row, 'SHitCalc1'),
sHitPar1: numberCell(row, 'sHitPar1', 0),
sHitPar2: numberCell(row, 'sHitPar2', 0),
sHitPar3: numberCell(row, 'sHitPar3', 0),
cHitCalc1: textCell(row, 'CHitCalc1'),
cHitPar1: numberCell(row, 'cHitPar1', 0),
cHitPar2: numberCell(row, 'cHitPar2', 0),
cHitPar3: numberCell(row, 'cHitPar3', 0),
dmgCalc1: textCell(row, 'DmgCalc1'),
dParam1: numberCell(row, 'dParam1', 0),
dParam2: numberCell(row, 'dParam2', 0),
vel: numberCell(row, 'Vel', 0),
maxVel: numberCell(row, 'MaxVel', 0),
velLev: numberCell(row, 'VelLev', 0),
accel: numberCell(row, 'Accel', 0),
range: numberCell(row, 'Range', 0),
levRange: numberCell(row, 'LevRange', 0),
light: numberCell(row, 'Light', 0),
flicker: numberCell(row, 'Flicker', 0),
red: numberCell(row, 'Red', 255),
green: numberCell(row, 'Green', 255),
blue: numberCell(row, 'Blue', 255),
initSteps: numberCell(row, 'InitSteps', 0),
activate: numberCell(row, 'Activate', 0),
loopAnim: numberCell(row, 'LoopAnim', 0),
celFile: textCell(row, 'CelFile'),
animRate: numberCell(row, 'animrate', 1024),
animLen: numberCell(row, 'AnimLen', 8),
animSpeed: numberCell(row, 'AnimSpeed', 16),
subLoop: numberCell(row, 'SubLoop', 0) !== 0,
subStart: numberCell(row, 'SubStart', 0),
subStop: numberCell(row, 'SubStop', 0),
collideType: numberCell(row, 'CollideType', 0),
collideKill: numberCell(row, 'CollideKill', 0) !== 0,
collideFriend: numberCell(row, 'CollideFriend', 0) !== 0,
lastCollide: numberCell(row, 'LastCollide', 0) !== 0,
collision: numberCell(row, 'Collision', 0) !== 0,
clientCol: numberCell(row, 'ClientCol', 0) !== 0,
nextHit: numberCell(row, 'NextHit', 0) !== 0,
nextDelay: numberCell(row, 'NextDelay', 0),
size: numberCell(row, 'Size', 1),
returnFire: numberCell(row, 'ReturnFire', 0) !== 0,
trans: numberCell(row, 'Trans', 0),
alwaysExplode: numberCell(row, 'AlwaysExplode', 0) !== 0,
explosion: numberCell(row, 'Explosion', 0) !== 0,
town: numberCell(row, 'Town', 0) !== 0,
noUniqueMod: numberCell(row, 'NoUniqueMod', 0) !== 0,
noMultiShot: numberCell(row, 'NoMultiShot', 0) !== 0,
holy: numberCell(row, 'Holy', 0),
canSlow: numberCell(row, 'CanSlow', 0) !== 0,
canDestroy: numberCell(row, 'CanDestroy', 0) !== 0,
toHit: numberCell(row, 'ToHit', 0) !== 0,
pierce: numberCell(row, 'Pierce', 0) !== 0,
skill: textCell(row, 'Skill'),
hitShift: numberCell(row, 'HitShift', 8),
srcDamage: numberCell(row, 'SrcDamage', 0),
minDamage: numberCell(row, 'MinDamage', 0),
minLevDam1: numberCell(row, 'MinLevDam1', 0),
minLevDam2: numberCell(row, 'MinLevDam2', 0),
minLevDam3: numberCell(row, 'MinLevDam3', 0),
minLevDam4: numberCell(row, 'MinLevDam4', 0),
minLevDam5: numberCell(row, 'MinLevDam5', 0),
maxDamage: numberCell(row, 'MaxDamage', 0),
maxLevDam1: numberCell(row, 'MaxLevDam1', 0),
maxLevDam2: numberCell(row, 'MaxLevDam2', 0),
maxLevDam3: numberCell(row, 'MaxLevDam3', 0),
maxLevDam4: numberCell(row, 'MaxLevDam4', 0),
maxLevDam5: numberCell(row, 'MaxLevDam5', 0),
dmgSymPerCalc: textCell(row, 'DmgSymPerCalc'),
eType: textCell(row, 'EType'),
eMin: numberCell(row, 'EMin', 0),
eMinLev1: numberCell(row, 'EMinLev1', 0),
eMinLev2: numberCell(row, 'EMinLev2', 0),
eMinLev3: numberCell(row, 'EMinLev3', 0),
eMinLev4: numberCell(row, 'EMinLev4', 0),
eMinLev5: numberCell(row, 'EMinLev5', 0),
eMax: numberCell(row, 'EMax', 0),
eMaxLev1: numberCell(row, 'EMaxLev1', 0),
eMaxLev2: numberCell(row, 'EMaxLev2', 0),
eMaxLev3: numberCell(row, 'EMaxLev3', 0),
eMaxLev4: numberCell(row, 'EMaxLev4', 0),
eMaxLev5: numberCell(row, 'EMaxLev5', 0),
eDmgSymPerCalc: textCell(row, 'EDmgSymPerCalc'),
eLen: numberCell(row, 'ELen', 0),
eLevLen1: numberCell(row, 'ELevLen1', 0),
eLevLen2: numberCell(row, 'ELevLen2', 0),
eLevLen3: numberCell(row, 'ELevLen3', 0),
hitClass: numberCell(row, 'HitClass', 0),
numDirections: numberCell(row, 'NumDirections', 16),
localBlood: numberCell(row, 'LocalBlood', 0),
damageRate: numberCell(row, 'DamageRate', 0),
explosionMissile: textCell(row, 'ExplosionMissile'),
subMissile1: textCell(row, 'SubMissile1'),
subMissile2: textCell(row, 'SubMissile2'),
subMissile3: textCell(row, 'SubMissile3'),
hitSubMissile1: textCell(row, 'HitSubMissile1'),
hitSubMissile2: textCell(row, 'HitSubMissile2'),
hitSubMissile3: textCell(row, 'HitSubMissile3'),
hitSubMissile4: textCell(row, 'HitSubMissile4'),
cltSubMissile1: textCell(row, 'CltSubMissile1'),
cltSubMissile2: textCell(row, 'CltSubMissile2'),
cltSubMissile3: textCell(row, 'CltSubMissile3'),
cltHitSubMissile1: textCell(row, 'CltHitSubMissile1'),
cltHitSubMissile2: textCell(row, 'CltHitSubMissile2'),
cltHitSubMissile3: textCell(row, 'CltHitSubMissile3'),
cltHitSubMissile4: textCell(row, 'CltHitSubMissile4'),
raw: row,
}
this.missilesById.set(id, record)
this.missilesByName.set(name.toLowerCase(), record)
}
}
private indexStates(): void {
for (const row of this.statesTable.rows) {
const idStr = row['id']
const state = textCell(row, 'state')
if (idStr === undefined || state === '') continue
const id = Number(idStr)
if (!Number.isFinite(id)) continue
const record: StateRecord = {
id,
state,
group: numberCell(row, 'group', 0),
remhit: numberCell(row, 'remhit', 0) !== 0,
nosend: numberCell(row, 'nosend', 0) !== 0,
transform: numberCell(row, 'transform', 0) !== 0,
aura: numberCell(row, 'aura', 0) !== 0,
cursetype: numberCell(row, 'cursetype', 0),
curable: numberCell(row, 'curable', 0) !== 0,
active: numberCell(row, 'active', 0) !== 0,
restrict: numberCell(row, 'restrict', 0) !== 0,
disguise: numberCell(row, 'disguise', 0) !== 0,
blue: numberCell(row, 'blue', 0) !== 0,
attblue: numberCell(row, 'attblue', 0) !== 0,
dmgblue: numberCell(row, 'dmgblue', 0) !== 0,
armblue: numberCell(row, 'armblue', 0) !== 0,
rfblue: numberCell(row, 'rfblue', 0) !== 0,
rlblue: numberCell(row, 'rlblue', 0) !== 0,
rcblue: numberCell(row, 'rcblue', 0) !== 0,
stambarblue: numberCell(row, 'stambarblue', 0) !== 0,
rpblue: numberCell(row, 'rpblue', 0) !== 0,
attred: numberCell(row, 'attred', 0) !== 0,
dmgred: numberCell(row, 'dmgred', 0) !== 0,
armred: numberCell(row, 'armred', 0) !== 0,
rfred: numberCell(row, 'rfred', 0) !== 0,
rlred: numberCell(row, 'rlred', 0) !== 0,
rcred: numberCell(row, 'rcred', 0) !== 0,
rpred: numberCell(row, 'rpred', 0) !== 0,
exp: numberCell(row, 'exp', 0) !== 0,
plrstaydeath: numberCell(row, 'plrstaydeath', 0) !== 0,
monstaydeath: numberCell(row, 'monstaydeath', 0) !== 0,
bossstaydeath: numberCell(row, 'bossstaydeath', 0) !== 0,
hide: numberCell(row, 'hide', 0) !== 0,
shatter: numberCell(row, 'shatter', 0) !== 0,
udead: numberCell(row, 'udead', 0) !== 0,
life: numberCell(row, 'life', 0) !== 0,
green: numberCell(row, 'green', 0) !== 0,
pgsv: numberCell(row, 'pgsv', 0) !== 0,
nooverlays: numberCell(row, 'nooverlays', 0) !== 0,
noclear: numberCell(row, 'noclear', 0) !== 0,
bossinv: numberCell(row, 'bossinv', 0) !== 0,
meleeonly: numberCell(row, 'meleeonly', 0) !== 0,
notondead: numberCell(row, 'notondead', 0) !== 0,
overlay1: textCell(row, 'overlay1'),
overlay2: textCell(row, 'overlay2'),
overlay3: textCell(row, 'overlay3'),
overlay4: textCell(row, 'overlay4'),
pgsvoverlay: textCell(row, 'pgsvoverlay'),
castoverlay: textCell(row, 'castoverlay'),
removerlay: textCell(row, 'removerlay'),
stat: textCell(row, 'stat'),
setfunc: parseFuncIdCell(row, 'setfunc', 0),
remfunc: parseFuncIdCell(row, 'remfunc', 0),
missile: textCell(row, 'missile'),
skill: textCell(row, 'skill'),
itemtrans: textCell(row, 'itemtrans'),
colorpri: numberCell(row, 'colorpri', 0),
colorshift: numberCell(row, 'colorshift', 0),
lightR: numberCell(row, 'light-r', 0),
lightG: numberCell(row, 'light-g', 0),
lightB: numberCell(row, 'light-b', 0),
onsound: textCell(row, 'onsound'),
offsound: textCell(row, 'offsound'),
raw: row,
}
this.statesById.set(id, record)
this.statesByName.set(state.toLowerCase(), record)
}
}
private indexItemStatCost(): void {
for (const row of this.itemStatCostTable.rows) {
const idStr = row['ID']
const stat = textCell(row, 'Stat')
if (idStr === undefined || stat === '') continue
const id = Number(idStr)
if (!Number.isFinite(id)) continue
const record: ItemStatCostRecord = {
id,
stat,
sendBits: numberCell(row, 'Send Bits', 0),
signed: numberCell(row, 'Signed', 0) !== 0,
valShift: numberCell(row, 'ValShift', 0),
damageRelated: numberCell(row, 'damagerelated', 0) !== 0,
op: numberCell(row, 'op', 0),
opParam: numberCell(row, 'op param', 0),
opBase: textCell(row, 'op base'),
opStat1: textCell(row, 'op stat1'),
opStat2: textCell(row, 'op stat2'),
opStat3: textCell(row, 'op stat3'),
direct: numberCell(row, 'direct', 0) !== 0,
maxStat: textCell(row, 'maxstat'),
raw: row,
}
this.statsById.set(id, record)
this.statsByName.set(stat.toLowerCase(), record)
}
}
private indexCharStats(): void {
for (const row of this.charStatsTable.rows) {
const className = textCell(row, 'class')
const mapping = CLASS_NAME_TO_CODE[className.toLowerCase()]
if (!mapping) continue
const skills: string[] = []
for (let i = 1; i <= 10; i += 1) {
const sk = textCell(row, `Skill ${String(i)}`)
if (sk) skills.push(sk)
}
const record: CharStatsRecord = {
className,
classCode: mapping.code,
token: mapping.token,
str: numberCell(row, 'str', 15),
dex: numberCell(row, 'dex', 15),
int: numberCell(row, 'int', 15),
vit: numberCell(row, 'vit', 15),
stamina: numberCell(row, 'stamina', 80),
hpadd: numberCell(row, 'hpadd', 20),
percentStr: numberCell(row, 'PercentStr', 25),
percentDex: numberCell(row, 'PercentDex', 25),
percentInt: numberCell(row, 'PercentInt', 25),
percentVit: numberCell(row, 'PercentVit', 25),
manaRegen: numberCell(row, 'ManaRegen', 120),
toHitFactor: numberCell(row, 'ToHitFactor', 0),
walkVelocity: numberCell(row, 'WalkVelocity', 6),
runVelocity: numberCell(row, 'RunVelocity', 9),
runDrain: numberCell(row, 'RunDrain', 20),
lifePerLevel: numberCell(row, 'LifePerLevel', 8),
staminaPerLevel: numberCell(row, 'StaminaPerLevel', 4),
manaPerLevel: numberCell(row, 'ManaPerLevel', 8),
lifePerVitality: numberCell(row, 'LifePerVitality', 8),
staminaPerVitality: numberCell(row, 'StaminaPerVitality', 4),
manaPerMagic: numberCell(row, 'ManaPerMagic', 8),
blockFactor: numberCell(row, 'BlockFactor', 20),
startSkill: textCell(row, 'StartSkill'),
baseWClass: textCell(row, 'baseWClass', 'hth'),
skills,
raw: row,
}
this.charStatsByCode.set(mapping.code, record)
this.charStatsByCode.set(mapping.token, record)
this.charStatsByCode.set(className.toLowerCase(), record)
}
}
private indexMonStats(): void {
for (const row of this.monStatsTable.rows) {
const hcIdxStr = row['hcIdx']
const id = textCell(row, 'Id')
if (hcIdxStr === undefined || id === '') continue
const hcIdx = Number(hcIdxStr)
if (!Number.isFinite(hcIdx)) continue
const record: MonStatsRecord = {
hcIdx,
id,
baseId: textCell(row, 'BaseId'),
nextInClass: textCell(row, 'NextInClass'),
translvl: numberCell(row, 'TransLvl', 0),
nameStr: textCell(row, 'NameStr'),
monStatsEx: textCell(row, 'MonStatsEx'),
code: textCell(row, 'Code'),
enabled: numberCell(row, 'enabled', 0) !== 0,
rangedtype: numberCell(row, 'rangedtype', 0) !== 0,
align: numberCell(row, 'Align', 0),
isSpawn: numberCell(row, 'isSpawn', 0) !== 0,
isMelee: numberCell(row, 'isMelee', 0) !== 0,
lUndead: numberCell(row, 'lUndead', 0) !== 0,
hUndead: numberCell(row, 'hUndead', 0) !== 0,
demon: numberCell(row, 'demon', 0) !== 0,
flying: numberCell(row, 'flying', 0) !== 0,
opendoors: numberCell(row, 'opendoors', 0) !== 0,
boss: numberCell(row, 'boss', 0) !== 0,
primeevil: numberCell(row, 'primeevil', 0) !== 0,
killable: numberCell(row, 'killable', 0) !== 0,
switchai: numberCell(row, 'switchai', 0) !== 0,
noAura: numberCell(row, 'noAura', 0) !== 0,
neverCount: numberCell(row, 'neverCount', 0) !== 0,
petIgnore: numberCell(row, 'petIgnore', 0) !== 0,
deathDmg: numberCell(row, 'deathDmg', 0) !== 0,
genericSpawn: numberCell(row, 'genericSpawn', 0) !== 0,
zoo: numberCell(row, 'zoo', 0) !== 0,
velocity: numberCell(row, 'Velocity', 6),
run: numberCell(row, 'Run', 9),
minHP: numberCell(row, 'minHP', 100),
maxHP: numberCell(row, 'maxHP', 150),
ac: numberCell(row, 'AC', 20),
exp: numberCell(row, 'Exp', 50),
a1MinD: numberCell(row, 'A1MinD', 5),
a1MaxD: numberCell(row, 'A1MaxD', 10),
a1TH: numberCell(row, 'A1TH', 50),
a2MinD: numberCell(row, 'A2MinD', 5),
a2MaxD: numberCell(row, 'A2MaxD', 10),
a2TH: numberCell(row, 'A2TH', 50),
s1MinD: numberCell(row, 'S1MinD', 5),
s1MaxD: numberCell(row, 'S1MaxD', 10),
s1TH: numberCell(row, 'S1TH', 50),
resDm: numberCell(row, 'ResDm', 0),
resMa: numberCell(row, 'ResMa', 0),
resFi: numberCell(row, 'ResFi', 0),
resLi: numberCell(row, 'ResLi', 0),
resCo: numberCell(row, 'ResCo', 0),
resPo: numberCell(row, 'ResPo', 0),
skill1: textCell(row, 'Skill1'),
sk1mode: textCell(row, 'Sk1mode'),
sk1lvl: numberCell(row, 'Sk1lvl', 1),
skill2: textCell(row, 'Skill2'),
sk2mode: textCell(row, 'Sk2mode'),
sk2lvl: numberCell(row, 'Sk2lvl', 1),
skill3: textCell(row, 'Skill3'),
sk3mode: textCell(row, 'Sk3mode'),
sk3lvl: numberCell(row, 'Sk3lvl', 1),
raw: row,
}
this.monStatsByHcIdx.set(hcIdx, record)
this.monStatsById.set(id.toLowerCase(), record)
}
}
private indexMonStats2(): void {
for (const row of this.monStats2Table.rows) {
const id = textCell(row, 'Id')
if (!id || id === 'expansion') continue
const record: MonStats2LightingRecord = {
id: id.toLowerCase(),
light: numberCell(row, 'Light', 0),
lightR: numberCell(row, 'light-r', 255),
lightG: numberCell(row, 'light-g', 255),
lightB: numberCell(row, 'light-b', 255),
shadow: numberCell(row, 'Shadow', 0) !== 0,
raw: row,
}
this.monStats2ById.set(id.toLowerCase(), record)
}
}
private indexOverlays(): void {
let idx = 0
for (const row of this.overlayTable.rows) {
const overlay = textCell(row, 'overlay')
if (!overlay) continue
const record: OverlayRecord = {
id: idx++,
overlay,
filename: textCell(row, 'Filename'),
frames: numberCell(row, 'Frames', 1),
animRate: numberCell(row, 'AnimRate', 2),
initRadius: numberCell(row, 'InitRadius', 0),
radius: numberCell(row, 'Radius', 0),
loopWaitTime: numberCell(row, 'LoopWaitTime', 0),
trans: numberCell(row, 'Trans', 0),
xOffset: numberCell(row, 'Xoffset', 0),
yOffset: numberCell(row, 'Yoffset', 0),
height1: numberCell(row, 'Height1', 0),
height2: numberCell(row, 'Height2', 0),
height3: numberCell(row, 'Height3', 0),
height4: numberCell(row, 'Height4', 0),
preDraw: numberCell(row, 'PreDraw', 0) !== 0,
red: numberCell(row, 'Red', 255),
green: numberCell(row, 'Green', 255),
blue: numberCell(row, 'Blue', 255),
raw: row,
}
this.overlaysByName.set(overlay.toLowerCase(), record)
}
}
private indexPetTypes(): void {
for (const row of this.petTypeTable.rows) {
const petType = textCell(row, 'pet type')
if (!petType) continue
const idx = numberCell(row, 'idx', 0)
const record: PetTypeRecord = {
idx,
petType,
group: numberCell(row, 'group', 0),
baseMax: numberCell(row, 'basemax', 0),
warp: numberCell(row, 'warp', 0) !== 0,
range: numberCell(row, 'range', 0) !== 0,
partysend: numberCell(row, 'partysend', 0) !== 0,
unsummon: numberCell(row, 'unsummon', 0) !== 0,
automap: numberCell(row, 'automap', 0) !== 0,
name: textCell(row, 'name'),
drawhp: numberCell(row, 'drawhp', 0) !== 0,
icontype: numberCell(row, 'icontype', 0),
baseicon: textCell(row, 'baseicon'),
mclass1: numberCell(row, 'mclass1', 0),
micon1: textCell(row, 'micon1'),
mclass2: numberCell(row, 'mclass2', 0),
micon2: textCell(row, 'micon2'),
mclass3: numberCell(row, 'mclass3', 0),
micon3: textCell(row, 'micon3'),
raw: row,
}
this.petTypesByIdx.set(idx, record)
this.petTypesByName.set(petType.toLowerCase(), record)
}
}
private indexElemTypesAndEvents(): void {
for (const row of this.elemTypesTable.rows) {
const code = textCell(row, 'Code')
if (code) this.elemTypes.add(code.toLowerCase())
}
for (const row of this.hitClassTable.rows) {
const code = textCell(row, 'Code')
const num = numberCell(row, 'Hit Class', 0)
if (code) this.hitClasses.set(code.toLowerCase(), num)
}
for (const row of this.eventsTable.rows) {
const evt = textCell(row, 'event')
if (evt) this.events.add(evt.toLowerCase())
}
}
getSkillById(id: number): SkillRecord | undefined {
return this.skillsById.get(id)
}
getSkillByName(name: string): SkillRecord | undefined {
return this.skillsByName.get(name.trim().toLowerCase())
}
getSkillDesc(key: string): Readonly<Record<string, string>> | undefined {
return this.skillDescsByKey.get(key.trim().toLowerCase())
}
getMissileById(id: number): MissileRecord | undefined {
return this.missilesById.get(id)
}
getMissileByName(name: string): MissileRecord | undefined {
return this.missilesByName.get(name.trim().toLowerCase())
}
getStateById(id: number): StateRecord | undefined {
return this.statesById.get(id)
}
getStateByName(name: string): StateRecord | undefined {
return this.statesByName.get(name.trim().toLowerCase())
}
getStatById(id: number): ItemStatCostRecord | undefined {
return this.statsById.get(id)
}
getStatByName(name: string): ItemStatCostRecord | undefined {
return this.statsByName.get(name.trim().toLowerCase())
}
getCharStats(codeOrName: string): CharStatsRecord | undefined {
return this.charStatsByCode.get(codeOrName.trim().toLowerCase())
}
getMonStats(idOrHcIdx: string | number): MonStatsRecord | undefined {
if (typeof idOrHcIdx === 'number') return this.monStatsByHcIdx.get(idOrHcIdx)
return this.monStatsById.get(idOrHcIdx.trim().toLowerCase())
}
getMonStats2Lighting(id: string): MonStats2LightingRecord | undefined {
return this.monStats2ById.get(id.trim().toLowerCase())
}
getLevelLighting(levelId: number): LevelLightingDef | undefined {
return getLevelLighting(levelId)
}
getOverlay(name: string): OverlayRecord | undefined {
return this.overlaysByName.get(name.trim().toLowerCase())
}
getPetType(nameOrIdx: string | number): PetTypeRecord | undefined {
if (typeof nameOrIdx === 'number') return this.petTypesByIdx.get(nameOrIdx)
return this.petTypesByName.get(nameOrIdx.trim().toLowerCase())
}
getAnimDataRecord(cofName: string): AnimDataRecord | undefined {
return this.animData.records.get(cofName.trim().toUpperCase())
}
/**
* Returns all 221 player skills (210 class skills across 7 classes + 11 universal player skills).
*/
getAllPlayerSkills(): SkillRecord[] {
const universalSet = new Set<number>(UNIVERSAL_PLAYER_SKILL_IDS)
const result: SkillRecord[] = []
for (const rec of this.skillsById.values()) {
if (rec.charClass !== '' || universalSet.has(rec.id)) {
result.push(rec)
}
}
return result.sort((a, b) => a.id - b.id)
}
/**
* Returns all 17 summoned pet and trap AI sub-skills (IDs 281..338).
*/
getPetSubSkills(): SkillRecord[] {
const result: SkillRecord[] = []
for (const id of PET_SUB_SKILL_IDS) {
const rec = this.skillsById.get(id)
if (rec) result.push(rec)
}
return result
}
/**
* Load the complete 1.13c D2DataRegistry from a MountedArchives stack.
*/
static async fromMountedArchives(mounted: MountedArchives): Promise<D2DataRegistry> {
const decoder = new TextDecoder('latin1')
const readTxt = async (path: string): Promise<DataTable> => {
const bytes = await mounted.read(path)
return parseTable(decoder.decode(bytes))
}
const [
skillsTable,
skillDescTable,
missilesTable,
statesTable,
itemStatCostTable,
charStatsTable,
monStatsTable,
monStats2Table,
overlayTable,
petTypeTable,
elemTypesTable,
hitClassTable,
eventsTable,
skillCalcTable,
missCalcTable,
animDataBytes,
] = await Promise.all([
readTxt('data\\global\\excel\\Skills.txt'),
readTxt('data\\global\\excel\\SkillDesc.txt'),
readTxt('data\\global\\excel\\Missiles.txt'),
readTxt('data\\global\\excel\\States.txt'),
readTxt('data\\global\\excel\\ItemStatCost.txt'),
readTxt('data\\global\\excel\\CharStats.txt'),
readTxt('data\\global\\excel\\MonStats.txt'),
readTxt('data\\global\\excel\\MonStats2.txt'),
readTxt('data\\global\\excel\\Overlay.txt'),
readTxt('data\\global\\excel\\PetType.txt'),
readTxt('data\\global\\excel\\ElemTypes.txt'),
readTxt('data\\global\\excel\\HitClass.txt'),
readTxt('data\\global\\excel\\Events.txt'),
readTxt('data\\global\\excel\\SkillCalc.txt'),
readTxt('data\\global\\excel\\MissCalc.txt'),
mounted.read('data\\global\\AnimData.d2'),
])
const animData = decodeAnimDataFile(animDataBytes)
return new D2DataRegistry({
skillsTable,
skillDescTable,
missilesTable,
statesTable,
itemStatCostTable,
charStatsTable,
monStatsTable,
monStats2Table,
overlayTable,
petTypeTable,
elemTypesTable,
hitClassTable,
eventsTable,
skillCalcTable,
missCalcTable,
animData,
})
}
}
let cachedRegistryPromise: Promise<D2DataRegistry> | null = null
let cachedMountedPromise: Promise<MountedArchives> | null = null
/**
* Mounts all 4 authoritative 1.13c MPQs from `/usr/local/google/home/taodao/d2-data`
* (or HTTP range source when running inside a browser).
*/
export async function getSharedMountedArchives(baseDir = '/usr/local/google/home/taodao/d2-data'): Promise<MountedArchives> {
if (cachedMountedPromise !== null) return cachedMountedPromise
cachedMountedPromise = (async () => {
const mounted = new MountedArchives()
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
const { httpRangeSource } = await import('../../mpq/source.ts')
const browserBase = baseDir.startsWith('/usr/local') ? 'samples/d2' : baseDir
for (const name of MPQ_LOAD_ORDER) {
const src = await httpRangeSource(`${browserBase}/${name}`)
mounted.add(name, await MpqArchive.open(src))
}
} else {
const { fileSource } = await import('../../mpq/file-source.ts')
for (const name of MPQ_LOAD_ORDER) {
const src = await fileSource(`${baseDir}/${name}`)
mounted.add(name, await MpqArchive.open(src))
}
}
return mounted
})()
return cachedMountedPromise
}
/**
* Returns the singleton cached `D2DataRegistry` loaded from the 1.13c MPQ stack.
*/
export async function getSharedDataRegistry(baseDir = '/usr/local/google/home/taodao/d2-data'): Promise<D2DataRegistry> {
if (cachedRegistryPromise === null) {
cachedRegistryPromise = (async () => {
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
const { memorySource } = await import('../../mpq/source.ts')
const browserBase = baseDir.startsWith('/usr/local') ? 'samples/d2' : baseDir
const [patchRes, animRes, hitClassRes] = await Promise.all([
fetch(`/${browserBase.replace(/^\/+/, '')}/Patch_D2.mpq`),
fetch('/samples/d2-packs/anim/AnimData.d2'),
fetch('/samples/d2-packs/anim/HitClass.txt'),
])
if (patchRes.ok && animRes.ok && hitClassRes.ok) {
const patchBytes = new Uint8Array(await patchRes.arrayBuffer())
const animBytes = new Uint8Array(await animRes.arrayBuffer())
const hitClassTxt = new TextDecoder('latin1').decode(await hitClassRes.arrayBuffer())
const patchMpq = await MpqArchive.open(memorySource(patchBytes, 'Patch_D2.mpq'))
const mounted = new MountedArchives()
mounted.add('Patch_D2.mpq', patchMpq)
const decoder = new TextDecoder('latin1')
const readTxt = async (path: string) => parseTable(decoder.decode(await mounted.read(path)))
const [
skillsTable,
skillDescTable,
missilesTable,
statesTable,
itemStatCostTable,
charStatsTable,
monStatsTable,
monStats2Table,
overlayTable,
petTypeTable,
elemTypesTable,
eventsTable,
skillCalcTable,
missCalcTable,
] = await Promise.all([
readTxt('data\\global\\excel\\Skills.txt'),
readTxt('data\\global\\excel\\SkillDesc.txt'),
readTxt('data\\global\\excel\\Missiles.txt'),
readTxt('data\\global\\excel\\States.txt'),
readTxt('data\\global\\excel\\ItemStatCost.txt'),
readTxt('data\\global\\excel\\CharStats.txt'),
readTxt('data\\global\\excel\\MonStats.txt'),
readTxt('data\\global\\excel\\MonStats2.txt'),
readTxt('data\\global\\excel\\Overlay.txt'),
readTxt('data\\global\\excel\\PetType.txt'),
readTxt('data\\global\\excel\\ElemTypes.txt'),
readTxt('data\\global\\excel\\Events.txt'),
readTxt('data\\global\\excel\\SkillCalc.txt'),
readTxt('data\\global\\excel\\MissCalc.txt'),
])
return new D2DataRegistry({
skillsTable,
skillDescTable,
missilesTable,
statesTable,
itemStatCostTable,
charStatsTable,
monStatsTable,
monStats2Table,
overlayTable,
petTypeTable,
elemTypesTable,
hitClassTable: parseTable(hitClassTxt),
eventsTable,
skillCalcTable,
missCalcTable,
animData: decodeAnimDataFile(animBytes),
})
}
}
const mounted = await getSharedMountedArchives(baseDir)
return D2DataRegistry.fromMountedArchives(mounted)
})()
}
return cachedRegistryPromise
}