577 lines
18 KiB
TypeScript
577 lines
18 KiB
TypeScript
/**
|
||
* scripts/pack-canonical-tbl.ts
|
||
*
|
||
* Extracts canonical Diablo II v1.13c `.tbl` string table entries (ENG & CHI)
|
||
* and `ItemStatCost.txt` tooltip formatting metadata (`descfunc`, `descval`,
|
||
* `descstrpos`, `descstrneg`, `descstr2`, `dgrp*`) from `samples/d2/` MPQ archives
|
||
* (`d2data.mpq`, `d2exp.mpq`, `Patch_D2.mpq`) into `src/data/canonical-tbl-strings.ts`.
|
||
*
|
||
* Ground Truth References:
|
||
* - D2MOO `source/D2Common/src/DataTbls/ItemsTbls.cpp` (`0x6FD55280` .. `0x6FD5E020`)
|
||
* - D2Client.dll `sub_6FB23F80` (`descfunc` 1..28 & `dgrp` 1..2)
|
||
*/
|
||
|
||
import * as fs from 'node:fs'
|
||
import * as path from 'node:path'
|
||
import { MpqArchive } from '../src/mpq/archive.ts'
|
||
import { fileSource } from '../src/mpq/file-source.ts'
|
||
import { MountedArchives } from '../src/mpq/mount.ts'
|
||
import { loadStringTableChain } from '../src/game/item-names.ts'
|
||
import { parseTable } from '../src/game/tables.ts'
|
||
import {
|
||
ITEM_NAMES_ZH,
|
||
MAGIC_PREFIX_ZH,
|
||
MAGIC_SUFFIX_ZH,
|
||
RARE_PREFIX_ZH,
|
||
RARE_SUFFIX_ZH,
|
||
SKILL_NAMES_ZH,
|
||
CLASS_SKILL_NAMES_ZH,
|
||
CLASS_RESTRICTION_SUFFIX_ZH,
|
||
RUNE_INFO_ZH,
|
||
} from '../src/game/tooltip-i18n.ts'
|
||
|
||
const EXPANSION_CHI_FALLBACKS: Readonly<Record<string, string>> = {
|
||
strethereal: '无形的 (无法修复)',
|
||
ModStre9s: '无法破坏',
|
||
ModStre9t: '每秒修复 %d 点耐久度',
|
||
ModStre9u: '每 %d 秒修复 1 点耐久度',
|
||
ModStre9v: '自动恢复数量',
|
||
ModStre9i: '增加堆叠数量',
|
||
ModStre9c: '(以角色等级决定)',
|
||
ModStre9d: '(在夜间增加)',
|
||
ModStre9e: '(在白天增加)',
|
||
ModStre9f: '(在黎明附近增加)',
|
||
ModStre9g: '(在黄昏附近增加)',
|
||
ModStre9h: ' 次聚气的',
|
||
ModStre9w: '施展等级 %d',
|
||
ModStre9x: '当你挥击时',
|
||
ModStre9y: '当你被击中时',
|
||
ModStre9z: '当你击中敌人时',
|
||
ModStre10d: '(%d/%d 次聚气)',
|
||
ModStre10L: '几率施展',
|
||
ModStre10a: '聚气',
|
||
ModStre10b: '等级',
|
||
ModStre10c: '每级',
|
||
ModStre10e: '(',
|
||
ModStre10f: ')',
|
||
ModStre10g: '隐秘',
|
||
ModStre10h: '毒素免疫',
|
||
ModStre10i: '诅咒',
|
||
ModStre10j: '每个队伍成员',
|
||
ModStre10k: '踢击伤害:',
|
||
ItemExpansiveChanc1: '击中时有 %d%% 几率施展等级 %d %s',
|
||
ItemExpansiveChanc2: '受击时有 %d%% 几率施展等级 %d %s',
|
||
ItemExpansiveChancX: '攻击时有 %d%% 几率施展等级 %d %s',
|
||
ExInsertSockets: '可镶嵌于带凹槽的物品中',
|
||
StrGemX1: '头盔:',
|
||
StrGemX2: '盾牌:',
|
||
StrGemX3: '武器:',
|
||
StrGemX4: '护甲:',
|
||
GemXp1: '头盔:',
|
||
GemXp2: '盾牌:',
|
||
GemXp3: '武器:',
|
||
GemXp4: '护甲:',
|
||
WeaponDescOrb: '法珠等级',
|
||
ModStre8a: ' 德鲁伊技能',
|
||
ModStre8c: ' 凹槽',
|
||
ModStre8d: ' 对恶魔的攻击准确率',
|
||
ModStre8e: ' 对不死生物的攻击准确率',
|
||
ModStre8f: ' 对恶魔的伤害',
|
||
ModStre8g: '% 攻击准确率',
|
||
ModStre8h: ' 标枪与长矛技能',
|
||
ModStre8i: ' 被动与魔法技能',
|
||
ModStre8j: ' 弓与十字弓技能',
|
||
ModStre8k: ' 防御灵气技能',
|
||
ModStre8l: ' 攻击灵气技能',
|
||
ModStre8m: ' 战斗技能',
|
||
ModStre8n: ' 召唤技能',
|
||
ModStre8o: ' 毒素与白骨技能',
|
||
ModStre8p: ' 诅咒',
|
||
ModStre8q: ' 战嚎技能',
|
||
ModStre8r: ' 战斗技能',
|
||
ModStre8s: ' 战斗专家技能',
|
||
ModStre8t: ' 冰冷技能',
|
||
ModStre8u: ' 闪电技能',
|
||
ModStre8v: ' 火焰技能',
|
||
ModStre8w: ' 召唤技能',
|
||
ModStre8x: ' 变形技能',
|
||
ModStre8y: ' 元素技能',
|
||
ModStre8z: ' 陷阱技能',
|
||
ModStre9a: ' 影子训练技能',
|
||
ModStre9b: ' 武学艺术技能',
|
||
ice: '解冻安雅的药剂\n玛拉的药剂',
|
||
'0sc': '知识卷轴',
|
||
tr2: '右键点击施放\n抗性卷轴',
|
||
Armor: '防具',
|
||
Rings: '戒指',
|
||
'Class Specific': '职业专属',
|
||
// Expansion SuperUniques
|
||
'Siege Boss': '督军山克',
|
||
'Ancient Barbarian 1': '塔力克',
|
||
'Ancient Barbarian 2': '马道克',
|
||
'Ancient Barbarian 3': '科力克',
|
||
'Axe Dweller': '持斧者',
|
||
'Bonesaw Breaker': '碎骨破甲者',
|
||
'Dac Farren': '达克法恩',
|
||
'Megaflow Rectifier': '矫正者怪异',
|
||
'Eyeback Unleashed': '狂暴者眼魔',
|
||
'Threash Socket': '剥壳凹槽',
|
||
Pindleskin: '暴躁外皮',
|
||
'Snapchip Shatter': '碎冰冲击者',
|
||
'Anodized Elite': '地狱佳人',
|
||
'Vinvear Molech': '温韦尔·摩洛克',
|
||
'Sharp Tooth Sayer': '利齿杀手',
|
||
'Magma Torquer': '岩浆扭曲者',
|
||
'Blaze Ripper': '烈焰撕裂者',
|
||
Frozenstein: '弗罗森斯坦',
|
||
Nihlathak: '尼拉塞克',
|
||
'Nihlathak Town': '尼拉塞克',
|
||
'Baal Subject 1': '毁灭者卡兰索',
|
||
'Baal Subject 2': '被诅咒的阿克姆',
|
||
'Baal Subject 3': '血腥巴特克',
|
||
'Baal Subject 4': '不洁者凡塔',
|
||
'Baal Subject 5': '折磨者利斯特',
|
||
// Expansion Monsters & NPCs
|
||
'Baal Subject Mummy': '解开者',
|
||
BaalColdMage: '死亡法师',
|
||
'Wake of Destruction': '火焰之径',
|
||
'Charged Bolt Sentry': '充能弹卫哨',
|
||
'Lightning Sentry': '闪电卫哨',
|
||
'Blade Creeper': '刀刃守卫',
|
||
'Invis Pet': '仆从',
|
||
'Inferno Sentry': '地狱火卫哨',
|
||
'Death Sentry': '死亡卫哨',
|
||
'Shadow Warrior': '影子战士',
|
||
'Shadow Master': '影子大师',
|
||
'Druid Hawk': '乌鸦',
|
||
'Druid Spirit Wolf': '鬼狼',
|
||
'Druid Fenris': '狂狼',
|
||
'Spirit of Barbs': '荆棘之灵',
|
||
'Heart of Wolverine': '狼獾之心',
|
||
'Oak Sage': '橡木智者',
|
||
'Druid Plague Poppy': '瘟疫藤',
|
||
'Druid Cycle of Life': '太阳藤',
|
||
'Vine Creature': '食尸藤',
|
||
'Druid Bear': '灰熊',
|
||
Wolf: '狼',
|
||
Bear: '熊',
|
||
'Barricade Door': '路障门',
|
||
'Prison Door': '牢门',
|
||
'Barricade Tower': '路障塔',
|
||
'Siege Beast': '攻城兽',
|
||
SnowYeti1: '雪地流浪者',
|
||
SnowYeti2: '雪怪',
|
||
SnowYeti3: '寒冰怪',
|
||
SnowYeti4: '冰封深渊怪',
|
||
WolfRider1: '沉沦魔狼骑兵',
|
||
WolfRider2: '削肉者狼骑兵',
|
||
WolfRider3: '恶魔狼骑兵',
|
||
'Hell Temptress': '地狱女妖',
|
||
'Blood Temptress': '鲜血女妖',
|
||
'Blood Witch': '鲜血女巫',
|
||
'Hell Witch': '地狱女巫',
|
||
CatapultS: '投石车',
|
||
CatapultE: '投石车',
|
||
CatapultSiege: '投石车',
|
||
CatapultW: '投石车',
|
||
'Catapult Spotter S': '投石车',
|
||
'Catapult Spotter E': '投石车',
|
||
'Catapult Spotter Siege': '投石车',
|
||
'Catapult Spotter W': '投石车',
|
||
'Frozen Horror1': '冰冻潜行者',
|
||
'Frozen Horror2': '冰冻恐惧',
|
||
'Frozen Horror3': '冰冻灾祸',
|
||
'Frozen Horror4': '冰冻魔怪',
|
||
'Frozen Horror5': '冰冻焦灼者',
|
||
'Blood Lord1': '月亮之王',
|
||
'Blood Lord2': '暗夜之王',
|
||
'Blood Lord3': '鲜血之王',
|
||
'Blood Lord4': '地狱之王',
|
||
'Blood Lord5': '死亡之王',
|
||
Larzuk: '拉苏克',
|
||
Drehya: '安雅',
|
||
Malah: '玛拉',
|
||
'Qual-Kehk': '夸尔凯克',
|
||
'Act 5 Combatant': '野蛮人战士',
|
||
'Barricade Wall Right': '路障',
|
||
'Barricade Wall Left': '路障',
|
||
'Evil hut': '邪恶魔屋',
|
||
'Death Mauler1': '死亡重击者',
|
||
'Death Mauler2': '死亡斗士',
|
||
'Death Mauler3': '死亡斩击者',
|
||
'Death Mauler4': '死亡狂战士',
|
||
'Death Mauler5': '死亡统领',
|
||
POW: '被俘的野蛮人',
|
||
'Act 5 Townguard': '守卫',
|
||
'Ancient Statue 1': '守护者塔力克',
|
||
'Ancient Statue 2': '守卫者马道克',
|
||
'Ancient Statue 3': '保卫者科力克',
|
||
'Baal Throne': '巴尔',
|
||
'Baal Crab': '巴尔',
|
||
'Baal Taunt': '巴尔',
|
||
'Baal Crab to Stairs': '巴尔',
|
||
'Baal Tentacle': '腐烂触须',
|
||
'Baal Crab Clone': '巴尔',
|
||
'Baals Minion': '毁灭的仆从',
|
||
'Putrid Defiler1': '腐败玷污者',
|
||
'Putrid Defiler2': '卑劣玷污者',
|
||
'Putrid Defiler3': '恶臭玷污者',
|
||
'Putrid Defiler4': '酸臭玷污者',
|
||
'Putrid Defiler5': '腥臭玷污者',
|
||
'Pain Worm1': '痛苦之虫',
|
||
'Pain Worm2': '折磨之虫',
|
||
'Pain Worm3': '苦难之虫',
|
||
'Pain Worm4': '威胁之虫',
|
||
'Pain Worm5': '剧痛之虫',
|
||
}
|
||
|
||
async function main() {
|
||
const archives = new MountedArchives()
|
||
for (const m of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
||
archives.add(m, await MpqArchive.open(await fileSource(`samples/d2/${m}`)))
|
||
}
|
||
|
||
const eng = await loadStringTableChain(archives, 'ENG')
|
||
const chi = await loadStringTableChain(archives, 'CHI')
|
||
|
||
const readTxt = async (name: string) =>
|
||
parseTable(new TextDecoder().decode(await archives.read(`data/global/excel/${name}`)))
|
||
|
||
const keys = new Set<string>()
|
||
const addKey = (k?: string) => {
|
||
const t = k?.trim()
|
||
if (t && t !== 'Expansion' && !t.startsWith('*')) {
|
||
keys.add(t)
|
||
}
|
||
}
|
||
|
||
// 1. Base items (weapons.txt, armor.txt, misc.txt)
|
||
for (const fn of ['weapons.txt', 'armor.txt', 'misc.txt']) {
|
||
const t = await readTxt(fn)
|
||
for (const r of t.rows) {
|
||
addKey(r['namestr'])
|
||
addKey(r['code'])
|
||
}
|
||
}
|
||
|
||
// 2. Uniques, SetItems, Sets, Runes, Gems, LowQualityItems
|
||
for (const fn of ['UniqueItems.txt', 'SetItems.txt']) {
|
||
const t = await readTxt(fn)
|
||
for (const r of t.rows) addKey(r['index'])
|
||
}
|
||
for (const r of (await readTxt('Sets.txt')).rows) {
|
||
addKey(r['name'])
|
||
addKey(r['index'])
|
||
}
|
||
for (const r of (await readTxt('Runes.txt')).rows) {
|
||
addKey(r['Name'])
|
||
}
|
||
for (const r of (await readTxt('Gems.txt')).rows) {
|
||
addKey(r['name'])
|
||
addKey(r['code'])
|
||
}
|
||
for (const r of (await readTxt('LowQualityItems.txt')).rows) {
|
||
addKey(r['Name'])
|
||
}
|
||
|
||
// 3. MagicPrefix, MagicSuffix, AutoMagic, RarePrefix, RareSuffix
|
||
for (const fn of ['MagicPrefix.txt', 'MagicSuffix.txt', 'AutoMagic.txt']) {
|
||
const t = await readTxt(fn)
|
||
for (const r of t.rows) addKey(r['Name'])
|
||
}
|
||
for (const fn of ['RarePrefix.txt', 'RareSuffix.txt']) {
|
||
const t = await readTxt(fn)
|
||
for (const r of t.rows) addKey(r['name'])
|
||
}
|
||
|
||
// 4. ItemStatCost.txt description & display group keys
|
||
const isc = await readTxt('ItemStatCost.txt')
|
||
interface IscDescMeta {
|
||
id: number
|
||
stat: string
|
||
descPriority: number
|
||
descFunc: number
|
||
descVal: number
|
||
descStrPos: string
|
||
descStrNeg: string
|
||
descStr2: string
|
||
dgrp: number
|
||
dgrpFunc: number
|
||
dgrpVal: number
|
||
dgrpStrPos: string
|
||
dgrpStrNeg: string
|
||
dgrpStr2: string
|
||
}
|
||
const iscMeta: Record<string, IscDescMeta> = {}
|
||
|
||
for (const r of isc.rows) {
|
||
const stat = r['Stat']?.trim()
|
||
if (!stat) continue
|
||
for (const col of ['descstrpos', 'descstrneg', 'descstr2', 'dgrpstrpos', 'dgrpstrneg', 'dgrpstr2']) {
|
||
addKey(r[col])
|
||
}
|
||
const descFunc = Number(r['descfunc']?.trim() || 0)
|
||
const dgrp = Number(r['dgrp']?.trim() || 0)
|
||
if (descFunc > 0 || dgrp > 0) {
|
||
iscMeta[stat] = {
|
||
id: Number(r['ID']?.trim() || 0),
|
||
stat,
|
||
descPriority: Number(r['descpriority']?.trim() || 0),
|
||
descFunc,
|
||
descVal: Number(r['descval']?.trim() || 0),
|
||
descStrPos: r['descstrpos']?.trim() || '',
|
||
descStrNeg: r['descstrneg']?.trim() || '',
|
||
descStr2: r['descstr2']?.trim() || '',
|
||
dgrp,
|
||
dgrpFunc: Number(r['dgrpfunc']?.trim() || 0),
|
||
dgrpVal: Number(r['dgrpval']?.trim() || 0),
|
||
dgrpStrPos: r['dgrpstrpos']?.trim() || '',
|
||
dgrpStrNeg: r['dgrpstrneg']?.trim() || '',
|
||
dgrpStr2: r['dgrpstr2']?.trim() || '',
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. CharStats.txt, SkillDesc.txt, MonStats.txt, SuperUniques.txt, Levels.txt
|
||
for (const r of (await readTxt('CharStats.txt')).rows) {
|
||
for (const col of ['StrAllSkills', 'StrSkillTab1', 'StrSkillTab2', 'StrSkillTab3', 'StrClassOnly', 'class']) {
|
||
addKey(r[col])
|
||
}
|
||
}
|
||
for (const r of (await readTxt('SkillDesc.txt')).rows) {
|
||
addKey(r['str name'])
|
||
}
|
||
for (const r of (await readTxt('MonStats.txt')).rows) {
|
||
addKey(r['NameStr'])
|
||
addKey(r['DescStr'])
|
||
}
|
||
for (const r of (await readTxt('SuperUniques.txt')).rows) {
|
||
addKey(r['Name'])
|
||
}
|
||
for (const r of (await readTxt('Levels.txt')).rows) {
|
||
addKey(r['LevelName'])
|
||
addKey(r['LevelWarp'])
|
||
addKey(r['EntryFile'])
|
||
}
|
||
|
||
// 6. All ModStr*, strMod*, ItemStats*, Weapon*, StrGem*, GemXp*, ExInsert*, UI/HUD/Monster/Elite/Immunity/Vendor keys
|
||
const explicitUiAndMonsterKeys = new Set([
|
||
'Socketable',
|
||
'gld',
|
||
'Gold',
|
||
'strGoldLabel',
|
||
'Hiquality',
|
||
'Cracked',
|
||
'Crude',
|
||
'Damaged',
|
||
'Low Quality',
|
||
'Gemmed',
|
||
'GemmedNormalName',
|
||
'Skillname2',
|
||
'Skilldesc2',
|
||
'Skillname3',
|
||
'Skilldesc3',
|
||
'Skillihand',
|
||
'RunOn',
|
||
'RunOff',
|
||
'bank',
|
||
'stash',
|
||
'strGoldInStash',
|
||
'box',
|
||
'strUiMenu2',
|
||
'strClose',
|
||
'close',
|
||
'TalkMenu',
|
||
'NPCIdentify1',
|
||
'NPCMenuTradeRepair',
|
||
'NPCMenuTrade',
|
||
'gamble',
|
||
'lowercasecancel',
|
||
'strBSArmor',
|
||
'strBSWeapons',
|
||
'strBSMisc',
|
||
'NPCPurchaseItems',
|
||
'NPCSellItems',
|
||
'NPCRepairItems',
|
||
'uniquextrastrong',
|
||
'uniqueextrafast',
|
||
'uniquecursed',
|
||
'uniquemagicresistance',
|
||
'uniquefireenchanted',
|
||
'UndeadDescriptX',
|
||
'DemonID',
|
||
'Champion',
|
||
'minion',
|
||
])
|
||
|
||
for (const [k] of eng.dict.entries()) {
|
||
if (
|
||
k.startsWith('ModStr') ||
|
||
k.startsWith('strMod') ||
|
||
k.startsWith('ItemStats') ||
|
||
k.startsWith('Weapon') ||
|
||
k.startsWith('StrGem') ||
|
||
k.startsWith('GemXp') ||
|
||
k.startsWith('ExInsert') ||
|
||
k.startsWith('strchr') ||
|
||
k.startsWith('StrSklTree') ||
|
||
k.startsWith('StrSkill') ||
|
||
k.startsWith('minipanel') ||
|
||
k.startsWith('monsteruniqueprop') ||
|
||
k.startsWith('strScaryMonDesc') ||
|
||
k.startsWith('champ') ||
|
||
k.startsWith('Eskill') ||
|
||
explicitUiAndMonsterKeys.has(k)
|
||
) {
|
||
keys.add(k)
|
||
}
|
||
}
|
||
|
||
// 7. MonStats.txt (NameStr, DescStr), SuperUniques.txt (Name), and 1.13c monster hover/affix/immunity keys
|
||
for (const r of (await readTxt('monstats.txt')).rows) {
|
||
addKey(r['NameStr'])
|
||
addKey(r['DescStr'])
|
||
}
|
||
for (const r of (await readTxt('SuperUniques.txt')).rows) {
|
||
addKey(r['Name'])
|
||
}
|
||
for (const k of [
|
||
'DemonID',
|
||
'UndeadDescriptX',
|
||
'Champion',
|
||
'minion',
|
||
'champghostlyX',
|
||
'champfanaticX',
|
||
'champpossessedX',
|
||
'champberserkX',
|
||
'uniquextrastrong',
|
||
'uniqueextrafast',
|
||
'uniquecursed',
|
||
'uniquemagicresistance',
|
||
'uniquefireenchanted',
|
||
'monsteruniqueprop1',
|
||
'monsteruniqueprop2',
|
||
'monsteruniqueprop3',
|
||
'monsteruniqueprop4',
|
||
'monsteruniqueprop5',
|
||
'monsteruniqueprop6',
|
||
'monsteruniqueprop7',
|
||
'monsteruniqueprop8',
|
||
'monsteruniqueprop9',
|
||
'strScaryMonDesc1',
|
||
'strScaryMonDesc2',
|
||
'strScaryMonDesc3',
|
||
'strScaryMonDesc4',
|
||
'strScaryMonDesc5',
|
||
'strScaryMonDesc6',
|
||
]) {
|
||
addKey(k)
|
||
}
|
||
|
||
const sortedKeys = Array.from(keys).sort()
|
||
const engOut: Record<string, string> = {}
|
||
const chiOut: Record<string, string> = {}
|
||
|
||
for (const k of sortedKeys) {
|
||
const ev = eng.get(k)
|
||
if (ev !== undefined) {
|
||
engOut[k] = ev
|
||
}
|
||
const evTrim = ev?.trim() ?? ''
|
||
const cv =
|
||
chi.get(k) ??
|
||
EXPANSION_CHI_FALLBACKS[k] ??
|
||
ITEM_NAMES_ZH[ev ?? ''] ??
|
||
ITEM_NAMES_ZH[evTrim] ??
|
||
ITEM_NAMES_ZH[k] ??
|
||
SKILL_NAMES_ZH[ev ?? ''] ??
|
||
SKILL_NAMES_ZH[evTrim] ??
|
||
SKILL_NAMES_ZH[k] ??
|
||
CLASS_SKILL_NAMES_ZH[ev ?? ''] ??
|
||
CLASS_SKILL_NAMES_ZH[evTrim] ??
|
||
CLASS_SKILL_NAMES_ZH[k] ??
|
||
CLASS_RESTRICTION_SUFFIX_ZH[ev ?? ''] ??
|
||
CLASS_RESTRICTION_SUFFIX_ZH[evTrim] ??
|
||
CLASS_RESTRICTION_SUFFIX_ZH[k] ??
|
||
RUNE_INFO_ZH[k]?.nameZh
|
||
if (cv !== undefined) {
|
||
chiOut[k] = cv
|
||
}
|
||
}
|
||
|
||
// Provide canonical alias keys for 3206, 3208, 3210..3218 so both raw .tbl keys and descriptive keys resolve
|
||
const monsterModAliases: ReadonlyArray<readonly [string, string]> = [
|
||
['uniquextrafast', 'uniqueextrafast'],
|
||
['uniquemagicresistant', 'uniquemagicresistance'],
|
||
['uniquecoldenchanted', 'monsteruniqueprop1'],
|
||
['uniquelightningenchanted', 'monsteruniqueprop2'],
|
||
['uniquemanaburn', 'monsteruniqueprop3'],
|
||
['uniquespectralhit', 'monsteruniqueprop4'],
|
||
['uniqueteleport', 'monsteruniqueprop5'],
|
||
['uniquestoneskin', 'monsteruniqueprop6'],
|
||
['uniquemultishot', 'monsteruniqueprop7'],
|
||
['UniqueAuraEnchanted', 'monsteruniqueprop9'],
|
||
]
|
||
for (const [alias, canonical] of monsterModAliases) {
|
||
if (engOut[canonical] !== undefined) engOut[alias] = engOut[canonical]!
|
||
if (chiOut[canonical] !== undefined) chiOut[alias] = chiOut[canonical]!
|
||
}
|
||
|
||
// Also include expansionstring.tbl index 2745 ("Ethereal (Cannot be Repaired)") under canonical key 'strethereal'
|
||
const expTbl = eng.tables[1]
|
||
if (expTbl && expTbl[2745]) {
|
||
engOut['strethereal'] = expTbl[2745]!
|
||
}
|
||
chiOut['strethereal'] = EXPANSION_CHI_FALLBACKS['strethereal']!
|
||
|
||
const outPath = path.join(process.cwd(), 'src/data/canonical-tbl-strings.ts')
|
||
const fileContent = `/**
|
||
* AUTO-GENERATED by \`scripts/pack-canonical-tbl.ts\` from authoritative 1.13c MPQ archives:
|
||
* - \`d2data.mpq\` (\`string.tbl\`)
|
||
* - \`d2exp.mpq\` (\`expansionstring.tbl\`)
|
||
* - \`Patch_D2.mpq\` (\`patchstring.tbl\`)
|
||
* - \`data/global/excel/ItemStatCost.txt\`
|
||
*
|
||
* DO NOT EDIT MANUALLY.
|
||
*/
|
||
|
||
export interface CanonicalIscDescEntry {
|
||
readonly id: number
|
||
readonly stat: string
|
||
readonly descPriority: number
|
||
readonly descFunc: number
|
||
readonly descVal: number
|
||
readonly descStrPos: string
|
||
readonly descStrNeg: string
|
||
readonly descStr2: string
|
||
readonly dgrp: number
|
||
readonly dgrpFunc: number
|
||
readonly dgrpVal: number
|
||
readonly dgrpStrPos: string
|
||
readonly dgrpStrNeg: string
|
||
readonly dgrpStr2: string
|
||
}
|
||
|
||
export const CANONICAL_ENG_TBL_DICT: Readonly<Record<string, string>> = ${JSON.stringify(engOut, null, 2)}
|
||
|
||
export const CANONICAL_CHI_TBL_DICT: Readonly<Record<string, string>> = ${JSON.stringify(chiOut, null, 2)}
|
||
|
||
export const CANONICAL_ISC_DESC_TABLE: Readonly<Record<string, CanonicalIscDescEntry>> = ${JSON.stringify(iscMeta, null, 2)}
|
||
|
||
export function resolveTblKey(key: string, lang: 'CHI' | 'ENG' = 'CHI'): string | undefined {
|
||
const dict = lang === 'ENG' ? CANONICAL_ENG_TBL_DICT : CANONICAL_CHI_TBL_DICT
|
||
return dict[key] ?? dict[key.trim()]
|
||
}
|
||
`
|
||
|
||
fs.writeFileSync(outPath, fileContent, 'utf8')
|
||
console.log(
|
||
`Wrote ${outPath}: ${Object.keys(engOut).length} ENG keys, ${Object.keys(chiOut).length} CHI keys, ${Object.keys(iscMeta).length} ISC entries.`,
|
||
)
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error(err)
|
||
process.exit(1)
|
||
})
|