247 lines
17 KiB
TypeScript
247 lines
17 KiB
TypeScript
/**
|
|
* Shared E2E Test Helpers for Module Split & Single Entry Consolidation (`tests/e2e-module-split/helpers.ts`).
|
|
*
|
|
* Uses dynamic `await import(absPath)` and `node:fs` inspection so that all 4 E2E test tiers
|
|
* compile cleanly with `tsc --noEmit` at all times and verify the target post-split layout.
|
|
*/
|
|
|
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
import { dirname, relative, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
export const ROOT_DIR = resolve(__dirname, '../..')
|
|
export const SRC_DIR = resolve(ROOT_DIR, 'src')
|
|
export const TOOLS_DIR = resolve(ROOT_DIR, 'tools')
|
|
|
|
export const CANONICAL_SRC_MODULES = [
|
|
'common',
|
|
'netproto',
|
|
'server',
|
|
'client',
|
|
'baker',
|
|
] as const
|
|
|
|
export const LEGACY_SRC_DIRS = [
|
|
'game',
|
|
'scene',
|
|
'mpq',
|
|
'formats',
|
|
'data',
|
|
'render',
|
|
'ui',
|
|
'frontend',
|
|
'audio',
|
|
'sim',
|
|
'i18n',
|
|
'net',
|
|
] as const
|
|
|
|
export const LEGACY_HTML_FILES = [
|
|
'index.html',
|
|
'acts.html',
|
|
'acts-zh.html',
|
|
'acts-en.html',
|
|
'bnet.html',
|
|
] as const
|
|
|
|
export const UNIVERSAL_DT1_PATHS = [
|
|
'data\\global\\tiles\\Act1\\Outdoors\\Blank.dt1',
|
|
'data\\global\\tiles\\Act1\\Barracks\\InvisWal.dt1',
|
|
'data\\global\\tiles\\Act1\\Barracks\\Warp.dt1',
|
|
] as const
|
|
|
|
export function resolveRepoPath(relPath: string): string {
|
|
return resolve(ROOT_DIR, relPath)
|
|
}
|
|
|
|
export function existsInRepo(relPath: string): boolean {
|
|
return existsSync(resolveRepoPath(relPath))
|
|
}
|
|
|
|
export function isDirInRepo(relPath: string): boolean {
|
|
const full = resolveRepoPath(relPath)
|
|
if (!existsSync(full)) return false
|
|
try {
|
|
return statSync(full).isDirectory()
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function readRepoFile(relPath: string): string {
|
|
const full = resolveRepoPath(relPath)
|
|
if (!existsSync(full)) {
|
|
throw new Error(`Expected file to exist in repository: ${relPath} (${full})`)
|
|
}
|
|
return readFileSync(full, 'utf8')
|
|
}
|
|
|
|
export function readJsonFile<T = Record<string, any>>(relPath: string): T {
|
|
return JSON.parse(readRepoFile(relPath)) as T
|
|
}
|
|
|
|
/**
|
|
* Dynamically imports a module by relative path from the repository root.
|
|
* If multiple candidate paths within the target canonical module are provided,
|
|
* imports the first existing path or throws a descriptive error listing candidates.
|
|
*/
|
|
export async function importRepoModule<T = Record<string, any>>(
|
|
...candidateRelPaths: readonly string[]
|
|
): Promise<T> {
|
|
for (const relPath of candidateRelPaths) {
|
|
const absPath = resolveRepoPath(relPath)
|
|
if (existsSync(absPath)) {
|
|
return (await import(absPath)) as T
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Expected module to exist at one of [${candidateRelPaths.join(', ')}] under ${ROOT_DIR}`,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Recursively collects all `.ts` files under a directory relative to `ROOT_DIR` (or absolute).
|
|
*/
|
|
export function collectTsFiles(dirOrRelPath: string): string[] {
|
|
const baseDir = dirOrRelPath.startsWith('/')
|
|
? dirOrRelPath
|
|
: resolveRepoPath(dirOrRelPath)
|
|
const results: string[] = []
|
|
if (!existsSync(baseDir)) return results
|
|
|
|
let entries: string[] = []
|
|
try {
|
|
entries = readdirSync(baseDir)
|
|
} catch {
|
|
return results
|
|
}
|
|
|
|
for (const name of entries) {
|
|
const full = resolve(baseDir, name)
|
|
let st
|
|
try {
|
|
st = statSync(full)
|
|
} catch {
|
|
continue
|
|
}
|
|
if (st.isDirectory()) {
|
|
results.push(...collectTsFiles(full))
|
|
} else if (name.endsWith('.ts') && !name.endsWith('.d.ts')) {
|
|
results.push(full)
|
|
}
|
|
}
|
|
return results.sort()
|
|
}
|
|
|
|
/**
|
|
* Strips block comments and single-line comments while preserving line structure.
|
|
*/
|
|
export function stripComments(source: string): string {
|
|
return source
|
|
.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, ' '))
|
|
.replace(/\/\/[^\n]*/g, '')
|
|
}
|
|
|
|
/**
|
|
* Strips comments and string literals so identifier scans only inspect code tokens.
|
|
*/
|
|
export function stripCommentsAndStrings(source: string): string {
|
|
const noComments = stripComments(source)
|
|
return noComments
|
|
.replace(/'(?:\\.|[^'\\])*'/g, "''")
|
|
.replace(/"(?:\\.|[^"\\])*"/g, '""')
|
|
.replace(/`(?:\\.|[^`\\])*`/g, '``')
|
|
}
|
|
|
|
/**
|
|
* Extracts all static and dynamic module import/export specifiers from TypeScript source code.
|
|
*/
|
|
export function extractImportSpecifiers(source: string): string[] {
|
|
const cleaned = stripComments(source)
|
|
const specs: string[] = []
|
|
const fromRe =
|
|
/(?:import|export)\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g
|
|
const dynRe = /\bimport\s*\(\s*(?:\/\*[\s\S]*?\*\/\s*)?['"]([^'"]+)['"]\s*\)/g
|
|
let m: RegExpExecArray | null
|
|
while ((m = fromRe.exec(cleaned)) !== null) {
|
|
if (m[1]) specs.push(m[1])
|
|
}
|
|
while ((m = dynRe.exec(cleaned)) !== null) {
|
|
if (m[1]) specs.push(m[1])
|
|
}
|
|
return specs
|
|
}
|
|
|
|
/**
|
|
* Resolves a relative import specifier from `sourceFileAbsPath` to a repo-relative path
|
|
* (`src/common/...`, `tools/...`, etc.), or returns the raw specifier if non-relative.
|
|
*/
|
|
export function resolveImportTargetRel(
|
|
sourceFileAbsPath: string,
|
|
specifier: string,
|
|
): string {
|
|
if (!specifier.startsWith('.')) return specifier
|
|
const resolved = resolve(dirname(sourceFileAbsPath), specifier)
|
|
return relative(ROOT_DIR, resolved).replace(/\\/g, '/')
|
|
}
|
|
|
|
/**
|
|
* Creates a self-contained in-memory `ArchiveSource` / `MountedArchives`-compatible fixture
|
|
* for testing `D2DataRegistry` and `getSharedDataRegistry` without filesystem or DOM globals.
|
|
*/
|
|
export function createSyntheticArchiveSource() {
|
|
const emptyAnimData = new Uint8Array(256 * 4) // 256 hash blocks with 0 records each
|
|
const tablesByPath: Record<string, string> = {
|
|
'data\\global\\excel\\skills.txt': [
|
|
'skill\tId\tcharclass\tskilldesc\tsrvstfunc\tsrvdofunc\tprgstack\tsrvprgfunc1\tsrvprgfunc2\tsrvprgfunc3\tprgcalc1\tprgcalc2\tprgcalc3\tprgdam\tsrvmissile\tdecquant\tlob\tsrvmissilea\tsrvmissileb\tsrvmissilec\tsrvoverlay\taurafilter\taurastate\tauratargetstate\tauralencalc\taurarangecalc\taurastat1\taurastatcalc1\tpassive\tsummon\tpettype\tpetmax\tsummode\tsumskill1\tsumsk1calc\tsumumod\tsumoverlay\tstsuccessonly\tstsound\tstsoundclass\tstsounddelay\tweaponsnd\tdosound\tdosound a\tdosound b\tcastoverlay\ttgtoverlay\ttgtsound\tprgoverlay\tprgsound\tcltstfunc\tcltdofunc\tcltprgfunc1\tcltprgfunc2\tcltprgfunc3\tcltmissile\tcltmissilea\tcltmissileb\tcltmissilec\tcltmissiled\tcltcalc1\tcltcalc2\tcltcalc3\twarp\timmediate\tenhanceable\tattackrank\tnoammo\trange\tweapsel\titypea1\titypea2\titypea3\tetypea1\tetypea2\titypeb1\titypeb2\titypeb3\tetypeb1\tetypeb2\tanim\tseqtrans\tmonanim\tseqnum\tseqinput\tdurable\tUseAttackRate\tLineOfSight\tTargetableOnly\tSearchEnemyXY\tSearchEnemyNear\tSearchOpenXY\tSelectProc\tTargetCorpse\tTargetPet\tTargetAlly\tTargetItem\tAttackNoMana\tTgtPlaceCheck\tKeepCursorStateOnKill\tContinueCastUnselected\tClearSelectedOnHold\tItemEffect\tItemCltEffect\tItemTgtDo\tItemTarget\tItemCheckStart\tItemCltCheckStart\tItemCastSound\tItemCastOverlay\tskpoints\treqlevel\tmaxlvl\treqstr\treqdex\treqint\treqvit\treqskill1\treqskill2\treqskill3\trestrict\tState1\tState2\tState3\tdelay\tleftskill\trepeat\tcheckfunc\tnocostinstate\tusemanaondo\tstartmana\tminmana\tmanashift\tmana\tlvlmana\tinterrupt\tInTown\tAura\tPeriodic\tPerDelay\tfinishing\tpassive\tprogressive\tgeneral\tscroll\tcalc1\tcalc2\tcalc3\tcalc4\tParam1\tParam2\tParam3\tParam4\tParam5\tParam6\tParam7\tParam8\tInGame\tToHit\tLevToHit\tToHitCalc\tResultFlags\tHitFlags\tHitClass\tKick\tHitShift\tSrcDam\tMinDam\tMinLevDam1\tMinLevDam2\tMinLevDam3\tMinLevDam4\tMinLevDam5\tMaxDam\tMaxLevDam1\tMaxLevDam2\tMaxLevDam3\tMaxLevDam4\tMaxLevDam5\tDmgSymPerCalc\tEType\tEMin\tEMinLev1\tEMinLev2\tEMinLev3\tEMinLev4\tEMinLev5\tEMax\tEMaxLev1\tEMaxLev2\tEMaxLev3\tEMaxLev4\tEMaxLev5\tEDmgSymPerCalc\tELen\tELevLen1\tELevLen2\tELevLen3\tELenSymPerCalc\taiType\taiBonus\tcost mult\tcost add',
|
|
'Attack\t0\t\tattack\t\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\t\t\t\t\t\t\t\t\t\t\t\t\t1\t\t\t\t\t\t\t\t\t\t1\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\t\th2h\t\t\t\t\t\t\t\t\t\t\t\tA1\tA1\tA1\t\t\t1\t1\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t1\t0\t0\t0\t0\t\t\t\t\t\t\t\t\t\t1\t0\t0\t0\t0\t0\t\t\t\t\t\t\t\t\t1\t\t\t\t\t0\t0\t8\t0\t0\t1\t0\t0\t0\t\t0\t0\t0\t1\t0\t\t\t\t\t\t\t\t\t\t\t\t\t1\t0\t0\t\t0\t0\t0\t0\t8\t128\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t\t\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t\t0\t0\t0\t0\t\t0\t0\t0\t0',
|
|
'Fire Bolt\t36\tsor\tfire bolt\t1\t6\t\t\t\t\t\t\t\t\tfirebolt\t\t\t\t\t\t\t\t\t\t\t\t\t\t0\t\t\t\t\t\t\t\t\t\tfire_cast_1\t\t\t\t\t\t\tfire_cast_1\t\t\t\t\t1\t1\t\t\t\tfirebolt\t\t\t\t\t\t\t\t\t\t1\t0\t\tnone\t\t\t\t\t\t\t\t\t\t\t\tSC\tSC\txx\t\t\t\t0\t1\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t1\t\t\t\t\t\t\t\t1\t1\t20\t0\t0\t0\t0\t\t\t\t\t\t\t\t\t1\t\t\t\t\t0\t1\t7\t5\t0\t1\t0\t0\t0\t\t0\t0\t0\t0\t1\t\t\t\t\t\t\t\t\t\t\t\t\t1\t0\t0\t\t0\t2\t0\t0\t8\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t\tfire\t3\t1\t2\t3\t5\t11\t6\t1\t2\t3\t5\t11\t(skill(\'Warmth\'.blvl)+skill(\'Fire Ball\'.blvl)+skill(\'Meteor\'.blvl))*par8\t0\t0\t0\t0\t\t1\t0\t128\t800',
|
|
].join('\n'),
|
|
'data\\global\\excel\\skilldesc.txt': 'skilldesc\tSkillPage\tSkillRow\tSkillColumn\tListRow\tListPool\tIconCel\tstr name\tstr short\tstr long\tstr alt\tstr mana\tdescdam\tddam calc1\tddam calc2\tp1dmgelem\tp1dmmin\tp1dmmax\tp1dmlen\tdescatt\nattack\t0\t0\t0\t0\t0\t0\tstrSkillName0\tstrSkillShort0\tstrSkillLong0\tstrSkillAlt0\t\t\t\t\t\t\t\t\t\nfire bolt\t3\t1\t2\t1\t0\t0\tSkillname36\tSkillsd36\tSkillld36\tSkillan36\tStrSkill3\t5\t\t\t\t\t\t\t',
|
|
'data\\global\\excel\\missiles.txt': 'Missile\tId\tbhit\tpCltDoFunc\tpCltHitFunc\tpSrvDoFunc\tpSrvHitFunc\tpSrvDmgFunc\tSrvCalc1\tCltCalc1\tVel\tMaxVel\tAccel\tRange\tLevRange\tLight\tFlicker\tRed\tGreen\tBlue\tInitSteps\tActivate\tLoopAnim\tCelFile\tanimrate\tAnimLen\tAnimSpeed\tRandStart\tSubLoop\tSubStart\tSubStop\tCollideType\tCollideKill\tCollideFriend\tLastCollide\tCollision\tClientCol\tClientSend\tNextHit\tNextDelay\tSize\tToHit\tAlwaysExplode\tExplosion\tTown\tNoUniqueMod\tNoMultiShot\tHoly\tCanSlow\tReturnFire\tGetHit\tSoftHit\tKnockBack\tTrans\tQty\tPierce\tSpecialSetup\tMissileSkill\tSkill\tResultFlags\tHitFlags\tHitShift\tApplyMastery\tSrcDamage\tHalf2HSrc\tSrcMissDmg\tMinDamage\tMinLevDam1\tMinLevDam2\tMinLevDam3\tMinLevDam4\tMinLevDam5\tMaxDamage\tMaxLevDam1\tMaxLevDam2\tMaxLevDam3\tMaxLevDam4\tMaxLevDam5\tDmgSymPerCalc\tEType\tEMin\tEMinLev1\tEMinLev2\tEMinLev3\tEMinLev4\tEMinLev5\tEMax\tEMaxLev1\tEMaxLev2\tEMaxLev3\tEMaxLev4\tEMaxLev5\tEDmgSymPerCalc\tELen\tELevLen1\tELevLen2\tELevLen3\tHitClass\tNumDirections\tLocalBlood\tDamageRate\tTravelSound\tHitSound\tProgSound\tProgOverlay\tExplosionMissile\tSubMissile1\tSubMissile2\tSubMissile3\tHitSubMissile1\tHitSubMissile2\tHitSubMissile3\tHitSubMissile4\tCltSubMissile1\tCltSubMissile2\tCltSubMissile3\tCltHitSubMissile1\tCltHitSubMissile2\tCltHitSubMissile3\tCltHitSubMissile4\nfirebolt\t59\t\t1\t\t1\t\t\t\t\t20\t20\t0\t40\t0\t6\t2\t255\t175\t65\t1\t0\t1\tFirebolt\t1024\t5\t16\t0\t0\t0\t0\t3\t1\t0\t1\t1\t1\t1\t0\t0\t1\t0\t1\t1\t0\t0\t0\t0\t1\t1\t1\t0\t0\t1\t0\t1\t0\t1\tFire Bolt\t4\t2\t8\t1\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t\tfire\t3\t1\t2\t3\t5\t11\t6\t1\t2\t3\t5\t11\t\t0\t0\t0\t0\t32\t16\t0\t\t\tfire_hit_1\t\t\tfire_small_explosion_1\t\t\t\t\t\t\t\t\t\t\t\t\t\t',
|
|
'data\\global\\excel\\states.txt': 'state\tid\tgroup\tremhit\tnosend\ttransform\taura\tcurable\tcurse\tactive\trestrict\tdisguise\tattblue\tdamblue\tarmblue\trfblue\trlblue\trcblue\trpblue\tstambarblue\tattred\tdamred\tarmred\trfred\trlred\trcred\trpred\texp\tplrstaydeath\tmonstaydeath\tbossstaydeath\thide\tshatter\tudead\tlifeshift\tlightr\tlightg\tlightb\titemtype\titemtrans\tcolorpri\tcolorshift\tlight-r\tlight-g\tlight-b\tonsound\toffsound\tgfxtype\tgfxclass\tcltevent\tclteventfunc\tcltactivefunc\tsrvactivefunc\tskill\tmissile\toverlay1\toverlay2\toverlay3\toverlay4\tpgsvoverlay\tcastoverlay\tremoverlay\tstat\tsetfunc\tremfunc\nnone\t0\nfreeze\t1',
|
|
'data\\global\\excel\\itemstatcost.txt': 'Stat\tID\tSend Other\tSigned\tSend Bits\tSend Param Bits\tUpdateAnimRate\tSaved\tCSvSigned\tCSvBits\tCSvParam\tfCallback\tfMin\tMinAccr\tEncode\tAdd\tMultiply\tDivide\tValShift\t1.09-Save Bits\t1.09-Save Add\tSave Bits\tSave Add\tSave Param Bits\tkeepzero\top\top param\top base\top stat1\top stat2\top stat3\tdirect\tmaxstat\titemspecific\tdamagerelated\tevent1\teventfunc1\tevent2\teventfunc2\tdescpriority\tdescfunc\tdescval\tdescstrpos\tdescstrneg\tdescstr2\tdgrp\tdgrpfunc\tdgrpval\tdgrpstrpos\tdgrpstrneg\tdgrpstr2\tstuff\nstrength\t0\t0\t1\t11\t0\t0\t1\t0\t10\t0\t0\t0\t0\t0\t105\t25\t1024\t0\t8\t32\t8\t32\t0\t0\t0\t0\t\t\t\t\t0\t\t0\t0\t\t\t\t\t67\t1\t2\tModStr1a\tModStr1a\t\t0\t0\t0\t\t\t\t',
|
|
'data\\global\\excel\\charstats.txt': 'class\tstr\tdex\tint\tvit\ttot\tstamina\thpadd\tPercentStr\tPercentDex\tPercentInt\tPercentVit\tManaRegen\tToHitFactor\tWalkVelocity\tRunVelocity\tRunDrain\tComment\tLifePerLevel\tStaminaPerLevel\tManaPerLevel\tLifePerVitality\tStaminaPerVitality\tManaPerMagic\tStatPerLevel\t#walk\t#run\t#swing\t#spell\t#gethit\t#bow\tBlockFactor\tStartSkill\tSkill 1\tSkill 2\tSkill 3\tSkill 4\tSkill 5\tSkill 6\tSkill 7\tSkill 8\tSkill 9\tSkill 10\tStrAllSkills\tStrSkillTab1\tStrSkillTab2\tStrSkillTab3\tStrClassOnly\tbaseWClass\titem1\titem1loc\titem1count\nSorceress\t10\t25\t35\t10\t80\t74\t30\t0\t0\t0\t0\t120\t-15\t6\t9\t20\t\t4\t4\t8\t8\t4\t8\t5\t8\t5\t9\t8\t6\t7\t20\tFire Bolt\tFire Bolt\tWarmth\tCharged Bolt\tIce Bolt\tFrozen Armor\tInferno\tStatic Field\tTelekinesis\tFrost Nova\tIce Blast\tModStr3b\tStrSklTabItem15\tStrSklTabItem14\tStrSklTabItem13\tSorceressOnly\thth\tsst\trarm\t1',
|
|
'data\\global\\excel\\monstats.txt': 'Id\thcIdx\tBaseId\tNextInClass\tTransLvl\tNameStr\tMonStatsEx\tMonProp\tMonType\tAI\tDescStr\tCode\tenabled\trangedtype\tplacespawn\tspawn\tspawnx\tspawny\tspawnmode\tminions1\tminions2\tSetBoss\tBossXfer\tPartyMin\tPartyMax\tMinGrp\tMaxGrp\tsparsePopulate\tVelocity\tRun\tRarity\tLevel\tLevel(N)\tLevel(H)\tMeleeRng\tMinHP\tMaxHP\tMinHP(N)\tMaxHP(N)\tMinHP(H)\tMaxHP(H)\tAC\tAC(N)\tAC(H)\tExp\tExp(N)\tExp(H)\tA1MinD\tA1MaxD\tA1TH\tA2MinD\tA2MaxD\tA2TH\tS1MinD\tS1MaxD\tS1TH\tEl1Mode\tEl1Type\tEl1Pct\tEl1MinD\tEl1MaxD\tEl1Dur\tTreasureClass1\tTreasureClass2\tTreasureClass3\tTreasureClass4\nfallen1\t0\tfallen1\tfallen2\t0\tFallen\tfallen1\t\tdemon\tFallen\t\tFA\t1\t0\t0\t\t0\t0\t\t\t\t0\t0\t0\t0\t4\t7\t0\t5\t9\t1\t1\t36\t67\t1\t50\t100\t100\t200\t200\t400\t5\t100\t500\t15\t500\t5000\t1\t3\t25\t0\t0\t0\t0\t0\t0\t\t\t0\t0\t0\t0\tAct 1 H2H A\tAct 1 Champ A\tAct 1 Unique A\tAct 1 H2H A',
|
|
'data\\global\\excel\\monstats2.txt': 'Id\tHeight\tOverlayHeight\tpixHeight\tSizeX\tSizeY\tSpawnCol\tMeleeRng\tBaseW\tHitClass\tHDv\tTRv\tLGv\tRAv\tLAv\tRHv\tLHv\tSHv\tS1v\tS2v\tS3v\tS4v\tS5v\tS6v\tS7v\tS8v\tHD\tTR\tLG\tRA\tLA\tRH\tLH\tSH\tS1\tS2\tS3\tS4\tS5\tS6\tS7\tS8\tTotalPieces\tmDT\tmNU\tmWL\tmGH\tmA1\tmA2\tmBL\tmSC\tmS1\tmS2\tmS3\tmS4\tmDD\tmKB\tmSQ\tmRN\tdDT\tdNU\tdWL\tdGH\tdA1\tdA2\tdBL\tdSC\tdS1\tdS2\tdS3\tdS4\tdDD\tdKB\tdSQ\tdRN\tA1mv\tA2mv\tSCmv\tS1mv\tS2mv\tS3mv\tS4mv\tnoGfxHitTest\thtTop\thtLeft\thtWidth\thtHeight\trevive\tsmall\tlarge\tsoft\tisAtt\tcorpseSel\talng\tisSel\tnoOvly\tisNotSel\tnoMap\tnoRg\tisNP\tnoShldBlock\tdeadCol\tflat\tUtrans\tUtrans(N)\tUtrans(H)\tHeart\tBodyPart\tInfernoLen\tInfernoAnim\tInfernoRollback\tResurrectMode\tResurrectSkill\nfallen1\t2\t2\t50\t1\t1\t0\t1\thth\t16\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t1\t1\t1\t1\t1\t1\t1\t1\t0\t0\t0\t0\t0\t0\t0\t0\t8\t1\t1\t1\t1\t1\t1\t0\t0\t1\t0\t0\t0\t1\t0\t0\t1\t16\t8\t8\t8\t8\t8\t0\t0\t8\t0\t0\t0\t8\t0\t0\t8\t0\t0\t0\t0\t0\t0\t0\t0\t-40\t-15\t30\t45\t1\t1\t0\t0\t1\t1\t0\t1\t0\t0\t0\t0\t0\t0\t0\t0\t2\t2\t2\t\t\t0\t0\t0\tNU\t',
|
|
'data\\global\\excel\\overlay.txt': 'overlay\tFilename\tversion\tFrames\tCharacter\tPreDraw\t1ofN\tDir\tOpen\tBeta\tXoffset\tYoffset\tHeight1\tHeight2\tHeight3\tHeight4\tAnimRate\tLoopWaitTime\tTrans\tInitRadius\tRadius\tRed\tGreen\tBlue\tNumDirections\tLocalBlood\nfire_cast_1\tFireCast\t0\t12\t\t0\t1\t0\t1\t0\t0\t0\t0\t75\t0\t0\t2\t0\t3\t0\t0\t0\t0\t0\t1\t0',
|
|
'data\\global\\excel\\pettype.txt': 'pet type\tidx\tgroup\tbasemax\twarp\trange\tpartysend\tunsummon\tautomap\tname\tdrawhp\ticontype\tbaseicon\tmclass1\tmpic1\nhydra\t10\t0\t0\t0\t0\t0\t0\t0\tstrUI20\t0\t0\t\t\t',
|
|
'data\\global\\excel\\elemtypes.txt': 'Elemental Type\tCode\nFire\tfire\nCold\tcold\nLightning\tltng\nPoison\tpois\nMagic\tmag',
|
|
'data\\global\\excel\\hitclass.txt': 'Hit Class\tCode\nHand To Hand\thth\nFire\tfire',
|
|
'data\\global\\excel\\events.txt': 'event\nmeleehit\nmissilehit',
|
|
'data\\global\\excel\\skillcalc.txt': 'code\tdescription\nln12\tpar1 + (lvl - 1) * par2',
|
|
'data\\global\\excel\\misscalc.txt': 'code\tdescription\nln12\tpar1 + (lvl - 1) * par2',
|
|
}
|
|
|
|
const encoder = new TextEncoder()
|
|
const normalize = (p: string) => p.trim().replace(/\//g, '\\').toLowerCase()
|
|
|
|
return {
|
|
readText(mpqRelativePath: string): string | null {
|
|
const norm = normalize(mpqRelativePath)
|
|
return tablesByPath[norm] ?? ''
|
|
},
|
|
readBytes(mpqRelativePath: string): Uint8Array | null {
|
|
const norm = normalize(mpqRelativePath)
|
|
if (norm.endsWith('animdata.d2')) {
|
|
return emptyAnimData
|
|
}
|
|
const txt = tablesByPath[norm] ?? ''
|
|
return encoder.encode(txt)
|
|
},
|
|
async read(mpqRelativePath: string): Promise<Uint8Array> {
|
|
const norm = normalize(mpqRelativePath)
|
|
if (norm.endsWith('animdata.d2')) {
|
|
return emptyAnimData
|
|
}
|
|
const txt = tablesByPath[norm] ?? ''
|
|
return encoder.encode(txt)
|
|
},
|
|
has(mpqRelativePath: string): boolean {
|
|
const norm = normalize(mpqRelativePath)
|
|
return norm.endsWith('animdata.d2') || norm in tablesByPath
|
|
},
|
|
}
|
|
}
|