/** * Generates src/data/skills-catalog.ts with full Blizzard 1.13c ground-truth * skill properties, English and Chinese localized descriptions, 5-band damage progression, * mana formulas, and synergy networks. */ import { MpqArchive } from '../src/mpq/archive.ts' import { fileSource } from '../src/mpq/file-source.ts' import { MountedArchives } from '../src/mpq/mount.ts' import { decodeTbl } from '../src/formats/tbl.ts' import { loadActTables } from '../src/game/acts.ts' import { CANONICAL_SKILL_TREE } from '../src/game/skill-tree.ts' import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' const CLASS_NAMES_ZH: Record = { ama: '亚马逊', sor: '法师', nec: '死灵法师', pal: '圣骑士', bar: '野蛮人', dru: '德鲁伊', ass: '刺客', } const TAB_NAMES_ZH: Record = { ama: ['弓与弩技能', '被动与魔法技能', '标枪与长矛技能'], sor: ['火焰法术', '闪电法术', '冰冷法术'], nec: ['诅咒', '毒素与白骨法术', '召唤法术'], pal: ['作战技能', '攻击灵气', '防御灵气'], bar: ['战斗技能', '战斗专家', '呐喊'], dru: ['召唤技能', '变形技能', '元素技能'], ass: ['陷阱', '暗影武艺', '武学艺术'], } const EXPANSION_SKILL_NAMES_ZH: Record = { // Druid 221: '乌鸦', 222: '毒藤', 223: '狼人变化', 224: '变形术', 225: '火风暴', 226: '橡木智者', 227: '召唤恶狼', 228: '熊人变化', 229: '熔岩巨石', 230: '极地风暴', 231: '食尸藤', 232: '狂犬病', 233: '太阳藤', 234: '裂地', 235: '旋风甲', 236: '狼獾之心', 237: '召唤狂狼', 238: '狂怒', 239: '撞击', 240: '龙卷风', 241: '枯萎藤', 242: '火爪', 243: '震波', 244: '火山', 245: '暴风', 246: '灵之守护', 247: '召唤灰熊', 248: '狂怒', 249: '末日浩劫', 250: '毁天灭地', // Assassin 251: '火爆', 252: '爪支配', 253: '心灵震爆', 254: '虎击', 255: '龙爪', 256: '电能网', 257: '刃之小刀', 258: '加速', 259: '双手挥舞', 260: '眼镜蛇打击', 261: '闪电手雷', 262: '火之复生', 263: '武器格挡', 264: '魔影斗篷', 265: '飞龙在天', 266: '雷电爪', 267: '消解', 268: '狂怒刃', 269: '复仇之火', 270: '影分身', 271: '寒冰刃', 272: '龙尾', 273: '雷光守卫', 274: '火焰复苏', 275: '刃之护盾', 276: '毒牙', 277: '暗影大师', 278: '飞龙升天', 279: '死亡守卫', 280: '凤凰攻击', } const EXPANSION_SKILL_DESC_ZH: Record = { 221: { short: '召唤乌鸦啄瞎敌人', long: '召唤乌鸦啄击敌人眼睛,降低其视野' }, 222: { short: '召唤毒藤在地下散播毒素', long: '毒藤在地下穿行,让接触的敌人中毒' }, 223: { short: '变身为狼人形态', long: '提升攻击速度、准确率和移动速度' }, 224: { short: '提升变身形态的生命与持续时间', long: '被动提升狼人和熊人形态的最大生命值和变形持续时间' }, 225: { short: '向前方喷射混乱的地表烈火', long: '在地面释放多道混乱游走的烈焰裂隙' }, 226: { short: '召唤提升全队最大生命值的自然之灵', long: '光环增加附近所有队友的最大生命值' }, 227: { short: '召唤恶狼协助战斗', long: '召唤狼群撕咬敌人,提升自身命中' }, 228: { short: '变身为巨熊形态', long: '大幅提升防御力、生命值和打击力量' }, 229: { short: '发射滚动的熔岩火球击退敌人', long: '火石沿直线滚向敌人,造成物理和火焰双重爆炸伤害' }, 230: { short: '引导一道连续的严寒冻气', long: '持续喷射极地寒冰冻结并减速前方敌人' }, 231: { short: '召唤食尸藤吞噬尸体回复生命', long: '在地下吞噬倒下的怪物尸体,为德鲁伊回复生命' }, 232: { short: '狼人撕咬敌人传播剧毒传染病', long: '撕咬使目标感染狂犬病毒,毒素会快速传染给周围怪物' }, 233: { short: '召唤太阳藤吞噬尸体回复法力', long: '吞噬死去的怪物尸体,为德鲁伊持续回复法力值' }, 234: { short: '撕裂大地喷发地热蒸汽与熔火', long: '在地面裂隙中喷发出炽热地火' }, 235: { short: '召唤环绕身躯的旋风护盾吸收元素伤害', long: '吸收即将到来的火焰、冰冷和闪电元素伤害' }, 236: { short: '召唤提升全队物理伤害与准确率的灵体', long: '狼獾图腾提升周围所有同伴的物理伤害与攻击命中' }, 237: { short: '召唤更加凶残狂暴的狂狼', long: '狂狼可以吃下死者尸体进入狂乱嗜血状态' }, 238: { short: '狼人连续疯狂撕咬周围的所有敌人', long: '对周围复数敌人发动疾风骤雨般的连续强力撕咬' }, 239: { short: '熊人沉重重击击晕并重创敌人', long: '熊掌沉重拍击地面,将敌人震晕并击退' }, 240: { short: '释放多道不可预测的毁灭龙卷风', long: '向前方释放穿透撕裂敌人的强力物理龙卷风' }, 241: { short: '召唤藤蔓缠绕吸收毒素', long: '从敌人体内吸取并中和毒素' }, 242: { short: '变身形态下的烈焰利爪攻击', long: '在狼人或熊人形态下发动附加高额火焰伤害的利爪攻击' }, 243: { short: '熊人咆哮震出大范围冲击波击晕敌人', long: '熊吼发出锥形震波,大范围击晕所有前方敌人' }, 244: { short: '在地面引发微型火山喷发火球', long: '在指定位置升起火山,持续喷吐炽热石块砸向周围敌人' }, 245: { short: '在身周召唤严寒暴风席卷敌人', long: '环绕德鲁伊旋转的极寒风暴,持续对近身敌人造成冰冷伤害' }, 246: { short: '召唤带有荆棘反伤效果的自然之灵', long: '反弹受到的物理伤害给近战攻击者' }, 247: { short: '召唤威猛无比的森林灰熊', long: '召唤强大的巨熊,具备极高的生命值与重击压制力' }, 248: { short: '狼人狂暴撕咬', long: '连续撕咬目标' }, 249: { short: '天降冰霜陨石撕碎战场', long: '从天空降下极寒冰彗星轰炸全场' }, 250: { short: '天降烈火流星轰炸战场', long: '持续在德鲁伊身边下落炽热陨石与熔岩碎片' }, 251: { short: '投掷小型火焰爆炸炸弹', long: '投掷可以弹跳并爆炸的手雷,造成小范围火焰伤害' }, 252: { short: '提升爪类武器的伤害、命中与暴击', long: '被动提升使用爪类武器时的战斗效能' }, 253: { short: '释放精神冲击击退并转换敌人', long: '震慑目标心智,使其陷入昏迷甚至暂时为你作战' }, 254: { short: '武术聚气:积聚毁灭性的物理近战能量', long: '最多积聚3段气劲,释放时造成毁天灭地的巨大物理爆发' }, 255: { short: '连续飞腿踢击单个目标', long: '随着技能等级提高,发动2次至5次超快速的连续旋风踢' }, 256: { short: '在地面投下一道电能火花陷阱网', long: '电能网在地面停留,对踩上的敌人造成持续电击' }, 257: { short: '掷出飞刀穿透沿途敌人', long: '投射旋转利刃切入敌群' }, 258: { short: '短时间内大幅提升攻击与移动速度', long: '进入加速状态,脚步如飞,出招极快' }, 259: { short: '双持爪具同时挥击', long: '双手爪具同时砍向敌人' }, 260: { short: '武术聚气:吸取敌人的生命与法力', long: '3段聚气分别吸取生命、法力,以及两者兼顾' }, 261: { short: '投掷充能弹陷阱手雷', long: '落地后释放大量蜿蜒前行的充能电火花' }, 262: { short: '埋设火焰浪潮喷射陷阱', long: '当敌人接近时自动向前方喷射烈焰波浪' }, 263: { short: '双持爪类武器时格挡敌人的攻击', long: '甚至可以格挡法术飞弹与远程攻击' }, 264: { short: '唤来暗影帷幕降低敌人视野与防御', long: '使全场陷入致盲暗影,极大降低怪物的防御力' }, 265: { short: '瞬间传送到目标身旁发动强力重踢', long: '跨越障碍瞬移至敌人面前施展终结飞踢' }, 266: { short: '武术聚气:积聚连锁闪电能量', long: '3段聚气释放电球、充能弹以及连锁闪电' }, 267: { short: '提高所有元素抗性与物理伤害减免', long: '身躯半虚无化,减少受到的诅咒持续时间和所有伤害' }, 268: { short: '向周围四方狂暴发射飞舞的利刃', long: '引导飞刀风暴绞杀四周一切敌人' }, 269: { short: '释放持续追踪燃烧的火焰之墙', long: '在地面制造一道燃烧的火墙' }, 270: { short: '召唤刺客的暗影斗士同伴', long: '暗影斗士会模仿刺客当前快捷栏上的技能进行战斗' }, 271: { short: '武术聚气:积聚严寒冰霜风暴', long: '3段聚气分别冻结单体、冻结周围以及引发冰爆' }, 272: { short: '终结飞踢引发巨大烈焰冲击波', long: '释放聚气造成巨额火焰爆破并击退所有近身敌人' }, 273: { short: '安放强大的雷光守卫陷阱', long: '刺客核心陷阱,持续发射高伤害的穿透穿刺穿透雷光束' }, 274: { short: '安放火焰复苏地雷陷阱', long: '持续向周围四方喷射火焰新星' }, 275: { short: '召唤围绕身旁的利刃旋涡护甲', long: '环绕身躯的飞刀切碎任何企图近身的敌人' }, 276: { short: '给爪具和武器涂抹致命剧毒', long: '短时间内使所有武器攻击附带极高毒素伤害' }, 277: { short: '召唤拥有全部刺客技能的高阶暗影大师', long: '暗影大师具备极强的抗性与AI,能自主施展刺客全套技能' }, 278: { short: '跳跃至空中施展从天而降的终结重踏', long: '跃向半空踢翻目标并释放积聚的武学能量' }, 279: { short: '安放引爆附近死尸的死亡守卫陷阱', long: '引爆死去的怪物尸体造成尸爆,并能发射闪电' }, 280: { short: '武术终极聚气:召唤凤凰的烈焰、闪电与冰霜', long: '1段聚气陨石,2段聚气连锁闪电,3段聚气极寒冰晶' }, } interface SynergyParsed { providerName: string providerId: number percentPerLevel: number type: 'damage' | 'elemental' | 'duration' } const EXPANSION_EXTRA_STRINGS_ZH: Record = { ESkillHawk: '乌鸦:', ESkillWolves: '狼:', EskillLifeSteal: '生命偷取:', EskillManaSteal: '法力偷取:', Eskillpowerup1: '聚气 1 段 -', Eskillpowerup2: '聚气 2 段 -', Eskillpowerup3: '聚气 3 段 -', Eskillpuddgmper: '% 伤害', Eskillkickdamage: '踢击伤害:', EskillKickSing: '踢', EskillKickPlur: '踢', ESkillSpikes: '地刺:', Eskillfistsoffire1: '火焰伤害:', Eskillfistsoffire2: '火焰伤害范围:', ESkillShoots: '发射', ESkillTimes: '次', Eskillpudmana: '% 生命与法力偷取', Eskillpudlife: '% 生命偷取', EskillPerBlade: '每枚利刃', Eskillthunder1: '闪电伤害:', Eskillthunder2: '闪电新星伤害:', Eskillthunder3: '充能弹伤害:', Eskillbladesofice1: '冰冷伤害:', Eskillbladesofice2: '冰冷伤害范围:', Eskillbladesofice3: '冻结时间:', Eskillphoenix1: '陨石伤害:', Eskillphoenix2: '连锁闪电伤害:', Eskillphoenix3: '混沌冰球伤害:', } function parseSynergyFormula(formula: string, params: Record, nameToId: Map): SynergyParsed[] { if (!formula || typeof formula !== 'string') return [] const result: SynergyParsed[] = [] // Pattern: skill('Skill Name'.blvl) * par8 or (skill('A'.blvl) + skill('B'.blvl)) * par8 // Or: skill('A'.blvl)*par8 + skill('B'.blvl)*par7 const re = /skill\(['"]([^'"]+)['"]\.blvl\)(?:\s*\*\s*([a-zA-Z0-9_]+))?/g let match: RegExpExecArray | null while ((match = re.exec(formula)) !== null) { const providerName = match[1]! const paramKey = match[2] || 'par8' let percent = 0 if (/^\d+$/.test(paramKey)) { percent = Number(paramKey) } else { percent = params[paramKey] ?? 0 } const providerId = nameToId.get(providerName.toLowerCase()) ?? 0 if (providerId > 0) { result.push({ providerName, providerId, percentPerLevel: percent, type: 'elemental', }) } } // Also check if formula had form `(skill('A'.blvl)+skill('B'.blvl))*parX` if (result.length === 0) { const outerMatch = /\(([^)]+)\)\s*\*\s*([a-zA-Z0-9_]+)/.exec(formula) if (outerMatch) { const inner = outerMatch[1]! const paramKey = outerMatch[2]! const percent = /^\d+$/.test(paramKey) ? Number(paramKey) : (params[paramKey] ?? 0) const innerRe = /skill\(['"]([^'"]+)['"]\.blvl\)/g let m: RegExpExecArray | null while ((m = innerRe.exec(inner)) !== null) { const providerName = m[1]! const providerId = nameToId.get(providerName.toLowerCase()) ?? 0 if (providerId > 0) { result.push({ providerName, providerId, percentPerLevel: percent, type: 'elemental', }) } } } } return result } async function main() { console.log('Mounting MPQ archives...') const archives = new MountedArchives() for (const m of ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) { archives.add(m, await MpqArchive.open(await fileSource(`samples/d2/${m}`))) } const tables = await loadActTables(archives) const skillsTable = tables.skills! const skillDescTable = tables.skilldesc! const monstatsTable = tables.monstats! const engTbl = decodeTbl(await archives.read('data\\local\\LNG\\ENG\\string.tbl')) const engExpTbl = decodeTbl(await archives.read('data\\local\\LNG\\ENG\\expansionstring.tbl')) const engPatchTbl = decodeTbl(await archives.read('data\\local\\LNG\\ENG\\patchstring.tbl')) const chiTbl = decodeTbl(await archives.read('data\\local\\LNG\\CHI\\string.tbl'), 'utf-8') const chiPatchTbl = decodeTbl(await archives.read('data\\local\\LNG\\CHI\\patchstring.tbl'), 'utf-8') function getStrings(key: string): { en: string; zh: string } { if (!key) return { en: '', zh: '' } const en = engPatchTbl.dict.get(key) || engExpTbl.dict.get(key) || engTbl.dict.get(key) || key const zh = EXPANSION_EXTRA_STRINGS_ZH[key] || chiPatchTbl.dict.get(key) || chiTbl.dict.get(key) || en return { en: en.trim(), zh: zh.trim() } } // Load scratch/skills_extracted.json const extractedRaw = JSON.parse(readFileSync('scratch/skills_extracted.json', 'utf8')) const extractedById = new Map() for (const item of extractedRaw) { extractedById.set(item.id, item) } // Build name to ID map const nameToId = new Map() for (const node of CANONICAL_SKILL_TREE) { nameToId.set(node.name.toLowerCase(), node.id) } const catalog: any[] = [] const skillsById: Record = {} for (const node of CANONICAL_SKILL_TREE) { const id = node.id const ext = extractedById.get(id) || {} const cls = node.classCode const tabIndex = node.tabIndex const tabNameEn = node.tabName const tabNameZh = TAB_NAMES_ZH[cls]?.[tabIndex] || tabNameEn // Look up SkillDesc row const skillRow = skillsTable.rows.find(r => Number(r[1]) === id) const descName = skillRow ? skillRow[3] : '' const descRow = skillDescTable.rows.find(d => d[0]?.toLowerCase() === descName?.toLowerCase()) const nameKey = descRow ? descRow[7] : '' const shortKey = descRow ? descRow[8] : '' const longKey = descRow ? descRow[9] : '' const nameObj = getStrings(nameKey) let nameEn = nameObj.en || node.name let nameZh = nameObj.zh || nameEn if (EXPANSION_SKILL_NAMES_ZH[id]) { nameZh = EXPANSION_SKILL_NAMES_ZH[id]! } const shortObj = getStrings(shortKey) let shortEn = shortObj.en let shortZh = shortObj.zh if (EXPANSION_SKILL_DESC_ZH[id]) { shortZh = EXPANSION_SKILL_DESC_ZH[id]!.short } const longObj = getStrings(longKey) let longEn = longObj.en let longZh = longObj.zh if (EXPANSION_SKILL_DESC_ZH[id]) { longZh = EXPANSION_SKILL_DESC_ZH[id]!.long } // Combine params const params: Record = {} if (node.params) { for (const [k, v] of Object.entries(node.params)) { if (typeof v === 'number') params[k] = v } } if (ext.Param && Array.isArray(ext.Param)) { ext.Param.forEach((val: any, idx: number) => { const num = Number(val) if (!isNaN(num) && num !== 0) { params[`par${idx + 1}`] = num } }) } // Parse synergies received const synList: SynergyParsed[] = [] if (node.synergyFormulas.elemental) { synList.push(...parseSynergyFormula(node.synergyFormulas.elemental, params, nameToId)) } if (node.synergyFormulas.physical) { synList.push(...parseSynergyFormula(node.synergyFormulas.physical, params, nameToId)) } if (node.synergyFormulas.duration) { synList.push(...parseSynergyFormula(node.synergyFormulas.duration, params, nameToId)) } // Determine masteries that apply const masteries: number[] = [] if (cls === 'sor') { if (ext.EType === 'fire' && id !== 61) masteries.push(61) // Fire Mastery if (ext.EType === 'ltng' && id !== 63) masteries.push(63) // Lightning Mastery if (ext.EType === 'cold' && id !== 65) masteries.push(65) // Cold Mastery } else if (cls === 'pal') { if (id === 112) masteries.push(113) // Concentration applies to Blessed Hammer } else if (cls === 'ass') { if (['Tiger Strike', 'Dragon Talon', 'Fists of Fire', 'Dragon Claw', 'Cobra Strike', 'Claws of Thunder', 'Dragon Tail', 'Blades of Ice', 'Dragon Flight'].includes(node.name)) { masteries.push(252) // Claw Mastery } } // Extract descdam const descdam = descRow ? (descRow[skillDescTable.header.indexOf('descdam')] || '') : '' // Extract descLines (descline1..6) const descLines: any[] = [] if (descRow) { for (let i = 1; i <= 6; i++) { const lStr = descRow[skillDescTable.header.indexOf(`descline${i}`)] const lNum = Number(lStr) if (lNum && lNum > 0 && lNum !== 1) { const textaKey = descRow[skillDescTable.header.indexOf(`desctexta${i}`)] || '' const textbKey = descRow[skillDescTable.header.indexOf(`desctextb${i}`)] || '' const calca = descRow[skillDescTable.header.indexOf(`desccalca${i}`)] || '' const calcb = descRow[skillDescTable.header.indexOf(`desccalcb${i}`)] || '' const aObj = getStrings(textaKey) const bObj = getStrings(textbKey) descLines.push({ lineType: lNum, textaKey: textaKey || undefined, textbKey: textbKey || undefined, textaEn: aObj.en || undefined, textaZh: aObj.zh || undefined, textbEn: bObj.en || undefined, textbZh: bObj.zh || undefined, calca: calca || undefined, calcb: calcb || undefined, }) } } } // Extract dsc2Lines (dsc2line1..4) const dsc2Lines: any[] = [] if (descRow) { for (let i = 1; i <= 4; i++) { const lStr = descRow[skillDescTable.header.indexOf(`dsc2line${i}`)] const lNum = Number(lStr) if (lNum && lNum > 0 && lNum !== 1) { const textaKey = descRow[skillDescTable.header.indexOf(`dsc2texta${i}`)] || '' const textbKey = descRow[skillDescTable.header.indexOf(`dsc2textb${i}`)] || '' const calca = descRow[skillDescTable.header.indexOf(`dsc2calca${i}`)] || '' const calcb = descRow[skillDescTable.header.indexOf(`dsc2calcb${i}`)] || '' const aObj = getStrings(textaKey) const bObj = getStrings(textbKey) dsc2Lines.push({ lineType: lNum, textaKey: textaKey || undefined, textbKey: textbKey || undefined, textaEn: aObj.en || undefined, textaZh: aObj.zh || undefined, textbEn: bObj.en || undefined, textbZh: bObj.zh || undefined, calca: calca || undefined, calcb: calcb || undefined, }) } } } // Check minion summoning stats from MonStats.txt const summonCode = skillRow ? skillRow[skillsTable.header.indexOf('summon')] : '' let minionStats: any = undefined if (summonCode && summonCode !== '') { const mRow = monstatsTable.rows.find(r => r[0]?.toLowerCase() === summonCode.toLowerCase()) if (mRow) { minionStats = { code: summonCode, minHP: Number(mRow[monstatsTable.header.indexOf('minHP')]) || 0, maxHP: Number(mRow[monstatsTable.header.indexOf('maxHP')]) || 0, minDam: Number(mRow[monstatsTable.header.indexOf('A1MinD')]) || 0, maxDam: Number(mRow[monstatsTable.header.indexOf('A1MaxD')]) || 0, } } } const entry = { id, name: nameEn, nameZh, classCode: cls, classNameZh: CLASS_NAMES_ZH[cls], tabIndex, tabName: tabNameEn, tabNameZh, row: node.row, col: node.col, reqlevel: node.reqlevel, reqskills: node.reqskills, dependents: node.dependents, // Mana mana: ext.mana ?? 0, lvlmana: ext.lvlmana ?? 0, minmana: ext.minmana ?? 0, manashift: ext.manashift ?? 8, delay: ext.delay ?? 0, // Damage & mechanics descdam, hitshift: ext.HitShift ?? 8, srcDamage: ext.SrcDam ?? 0, minDam: ext.MinDam ?? 0, maxDam: ext.MaxDam ?? 0, minLevDam: ext.MinLevDam ?? [0, 0, 0, 0, 0], maxLevDam: ext.MaxLevDam ?? [0, 0, 0, 0, 0], eType: ext.EType ?? '', eMin: ext.EMin ?? 0, eMax: ext.EMax ?? 0, eMinLev: ext.EMinLev ?? [0, 0, 0, 0, 0], eMaxLev: ext.EMaxLev ?? [0, 0, 0, 0, 0], eLen: ext.ELen ?? 0, eLevLen: ext.ELevLen ?? [0, 0, 0], params, synergyFormulas: node.synergyFormulas, synergiesReceived: synList, synergiesGiven: [] as any[], masteries, descLines, dsc2Lines, minionStats, shortDesc: shortEn, shortDescZh: shortZh, longDesc: longEn, longDescZh: longZh, icon: `/skills/icon_${id}.png`, iconActive: `/skills/icon_${id}_active.png`, } catalog.push(entry) skillsById[id] = entry } // Populate synergies given for (const node of catalog) { for (const syn of node.synergiesReceived) { if (skillsById[syn.providerId]) { skillsById[syn.providerId].synergiesGiven.push({ targetId: node.id, targetName: node.name, targetNameZh: node.nameZh, percentPerLevel: syn.percentPerLevel, }) } } } console.log(`Successfully compiled ${catalog.length} skills with synergy networks! Writing file...`) const tsContent = `// Auto-generated Diablo II 1.13c 210-Skill Catalog // Ground truth parsed from Skills.txt, SkillDesc.txt, string.tbl, and MPQs. import type { CharacterClassCode } from '../game/classes.ts' export interface SynergyBonusInfo { readonly providerId: number readonly providerName: string readonly percentPerLevel: number readonly type: 'damage' | 'elemental' | 'duration' } export interface SynergyGivenInfo { readonly targetId: number readonly targetName: string readonly targetNameZh: string readonly percentPerLevel: number } export interface DescLineDefinition { readonly lineType: number readonly textaKey?: string | undefined readonly textbKey?: string | undefined readonly textaEn?: string | undefined readonly textaZh?: string | undefined readonly textbEn?: string | undefined readonly textbZh?: string | undefined readonly calca?: string | undefined readonly calcb?: string | undefined } export interface MinionBaseStats { readonly code: string readonly minHP: number readonly maxHP: number readonly minDam: number readonly maxDam: number } export interface SkillCatalogEntry { readonly id: number readonly name: string readonly nameZh: string readonly classCode: CharacterClassCode readonly classNameZh: string readonly tabIndex: number readonly tabName: string readonly tabNameZh: string readonly row: number readonly col: number readonly reqlevel: number readonly reqskills: readonly number[] readonly dependents: readonly number[] readonly mana: number readonly lvlmana: number readonly minmana: number readonly manashift: number readonly delay: number readonly descdam?: string | undefined readonly hitshift: number readonly srcDamage: number readonly minDam: number readonly maxDam: number readonly minLevDam: readonly number[] readonly maxLevDam: readonly number[] readonly eType: string readonly eMin: number readonly eMax: number readonly eMinLev: readonly number[] readonly eMaxLev: readonly number[] readonly eLen: number readonly eLevLen: readonly number[] readonly params: Readonly> readonly synergyFormulas: { readonly physical?: string | undefined readonly elemental?: string | undefined readonly duration?: string | undefined } readonly synergiesReceived: readonly SynergyBonusInfo[] readonly synergiesGiven: readonly SynergyGivenInfo[] readonly masteries: readonly number[] readonly descLines?: readonly DescLineDefinition[] | undefined readonly dsc2Lines?: readonly DescLineDefinition[] | undefined readonly minionStats?: MinionBaseStats | undefined readonly shortDesc: string readonly shortDescZh: string readonly longDesc: string readonly longDescZh: string readonly icon: string readonly iconActive: string } export const SKILLS_CATALOG: readonly SkillCatalogEntry[] = Object.freeze(${JSON.stringify(catalog, null, 2)}) export const SKILLS_BY_ID: Readonly> = Object.freeze( SKILLS_CATALOG.reduce>((acc, cur) => { acc[cur.id] = cur return acc }, {}) ) export const SKILLS_BY_CLASS: Readonly> = Object.freeze({ ama: SKILLS_CATALOG.filter(s => s.classCode === 'ama'), sor: SKILLS_CATALOG.filter(s => s.classCode === 'sor'), nec: SKILLS_CATALOG.filter(s => s.classCode === 'nec'), pal: SKILLS_CATALOG.filter(s => s.classCode === 'pal'), bar: SKILLS_CATALOG.filter(s => s.classCode === 'bar'), dru: SKILLS_CATALOG.filter(s => s.classCode === 'dru'), ass: SKILLS_CATALOG.filter(s => s.classCode === 'ass'), }) ` writeFileSync(join(process.cwd(), 'src', 'data', 'skills-catalog.ts'), tsContent, 'utf-8') console.log('Wrote src/data/skills-catalog.ts successfully!') } main().catch(err => { console.error(err) process.exit(1) })