feat(i18n): 补全暗黑2全量词缀汉化、修复语序倒装、充能技能负数与属性词条解析 (Issue #122)

This commit is contained in:
troytt 2026-09-19 12:35:08 +00:00
parent cef219155a
commit ca55cd29d8
6 changed files with 2117 additions and 645 deletions

View File

@ -0,0 +1,183 @@
import { spawn } from 'child_process'
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function runBrowserAudit() {
console.log('1. Starting Google Chrome headless...')
const chromeProc = spawn('/usr/bin/google-chrome', [
'--headless=new',
'--remote-debugging-port=9222',
'--no-sandbox',
'--disable-gpu',
'--disable-extensions',
'http://localhost:8080/boss.html',
])
// Wait for Chrome to be ready
let wsUrl = null
for (let i = 0; i < 30; i++) {
await sleep(300)
try {
const res = await fetch('http://127.0.0.1:9222/json/list')
const pages = await res.json()
const page = pages.find(p => p.type === 'page' && p.url.includes('boss.html'))
if (page && page.webSocketDebuggerUrl) {
wsUrl = page.webSocketDebuggerUrl
break
}
} catch (e) {
// keep trying
}
}
if (!wsUrl) {
chromeProc.kill()
throw new Error('Failed to connect to Chrome headless CDP within 10s')
}
console.log('2. Connected to Chrome CDP on page:', wsUrl)
const ws = new WebSocket(wsUrl)
await new Promise(resolve => ws.onopen = resolve)
let idCounter = 1
function sendCommand(method, params = {}) {
return new Promise((resolve, reject) => {
const id = idCounter++
const handler = event => {
const msg = JSON.parse(event.data)
if (msg.id === id) {
ws.removeEventListener('message', handler)
if (msg.error) reject(msg.error)
else resolve(msg.result)
}
}
ws.addEventListener('message', handler)
ws.send(JSON.stringify({ id, method, params }))
})
}
// Enable Runtime
await sendCommand('Runtime.enable')
// Wait 1.5s for initial drop tables to load
await sleep(1500)
console.log('3. Triggering Chinese language switch and 10x Boss Kill in browser DOM...')
const evalResult = await sendCommand('Runtime.evaluate', {
expression: `(async () => {
// 1. Click Chinese language pill
const zhPill = document.querySelector('.lang-pill[data-lang="zh"]');
if (zhPill) zhPill.click();
// 2. Click 10x kill button
const kill10Btn = document.getElementById('btn-kill-10');
if (kill10Btn) kill10Btn.click();
// Wait a short moment for DOM rendering
await new Promise(r => setTimeout(r, 800));
// 3. Extract items
const cards = Array.from(document.querySelectorAll('#loot-items-grid .d2-item-card'));
return cards.map(c => {
const title = c.querySelector('.item-title')?.textContent?.trim() || '';
const subTitle = c.querySelector('.item-subtitle')?.textContent?.trim() || '';
const quality = c.querySelector('.item-quality-pill')?.textContent?.trim() || '';
const ilvl = c.querySelector('.item-ilvl-pill')?.textContent?.trim() || '';
const stats = Array.from(c.querySelectorAll('.item-base-stats .base-stat-item')).map(s => s.textContent.trim());
const affixes = Array.from(c.querySelectorAll('.item-affix-lines .tooltip-stat-line')).map(a => a.textContent.trim());
return {
title,
subTitle,
quality,
ilvl,
stats,
affixes
};
});
})()`,
awaitPromise: true,
returnByValue: true,
})
console.log('4. Evaluation completed. Closing Chrome...')
ws.close()
chromeProc.kill()
const items = evalResult.result.value
console.log(`\nSuccessfully captured ${items.length} items from http://localhost:8080/boss.html!\n`)
return items
}
runBrowserAudit()
.then(items => {
console.log('================================================================');
console.log(`=== AUDITING ALL ${items.length} ITEMS GENERATED FROM 10x BOSS KILL ===`);
console.log('================================================================\n');
let errorsFound = 0;
const traditionalChars = ['屬', '無', '紅', '綠', '變', '爛', '誕', '誠', '雲', '犧', '嚇', '層', '靜', '換', '亂', '節', '甦'];
items.forEach((item, index) => {
console.log(`[#${index + 1}] [${item.quality}] ${item.title} (底材: ${item.subTitle || '无'}, ${item.ilvl})`);
if (item.stats.length > 0) {
console.log(` 基础属性: ${item.stats.join(' | ')}`);
}
if (item.affixes.length > 0) {
console.log(` 词条属性:`);
item.affixes.forEach(aff => console.log(` * ${aff}`));
}
// Check 1: Negative numbers in charged spells
item.affixes.forEach(aff => {
if (aff.includes('次充能') && (aff.includes('等级 -') || aff.includes('(-'))) {
console.error(` ❌ ERROR: Negative value in charged spell: "${aff}"`);
errorsFound++;
}
});
// Check 2: Missing English affix translations (e.g. English letters followed by 之 or preceded by 之)
const englishAffixMatch = item.title.match(/[a-zA-Z]+之|之[a-zA-Z]+/);
if (englishAffixMatch) {
console.error(` ❌ ERROR: Untranslated English affix in title: "${item.title}"`);
errorsFound++;
}
// Check 3: Word order anomaly: base name followed by '之' at the end of title
if (item.subTitle && item.title.endsWith(item.subTitle + '之')) {
console.error(` ❌ ERROR: Word order inversion (base followed by suffix particle): "${item.title}"`);
errorsFound++;
}
// Check 4: Traditional Chinese characters
const allText = item.title + ' ' + item.subTitle + ' ' + item.affixes.join(' ');
traditionalChars.forEach(ch => {
if (allText.includes(ch)) {
console.error(` ❌ ERROR: Traditional Chinese character '${ch}' found in: "${allText}"`);
errorsFound++;
}
});
console.log('');
});
console.log('================================================================');
if (errorsFound === 0) {
console.log(`🎉 AUDIT PASSED! All ${items.length} items checked with ZERO errors!`);
console.log(`✅ All names in correct Chinese grammar word order`);
console.log(`✅ 100% Affix translations matched without fallback artifacts`);
console.log(`✅ Charged spell positive charges and positive skill levels verified`);
console.log(`✅ Normalized Simplified Chinese characters verified`);
} else {
console.error(`⚠️ AUDIT FOUND ${errorsFound} ERRORS!`);
process.exit(1);
}
console.log('================================================================\n');
})
.catch(err => {
console.error('Audit execution failed:', err);
process.exit(1);
});

View File

@ -0,0 +1,607 @@
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
import { SKILL_NAMES_ZH } from '../src/game/tooltip-i18n.ts'
const dt = getEmbeddedDropTables()
const prefixes = Array.from(new Set(dt.magicAffixes.prefixes.all.map(p => p.name))).sort()
const suffixes = Array.from(new Set(dt.magicAffixes.suffixes.all.map(s => s.name))).sort()
// Complete 252 prefixes
const prefixMap: Record<string, string> = {
"Accursed": "诅咒之",
"Acrobat's": "杂技之",
"Amber": "琥珀之",
"Ambergris": "龙涎香之",
"Amethyst": "紫水晶之",
"Angel's": "天使之",
"Antimagic": "防魔之",
"Arcadian": "阿卡迪亚之",
"Arch-Angel's": "大天使之",
"Archer's": "弓箭手之",
"Arcing": "电弧之",
"Argent": "白银之",
"Artificer's": "工匠之",
"Assamic": "阿萨姆之",
"Astral": "星界之",
"Athlete": "运动之",
"Athlete's": "运动之",
"Aureolin": "钴黄之",
"Azure": "天青之",
"Bahamut's": "巴哈姆特之",
"Beryl": "绿柱石之",
"Berserker's": "狂战士之",
"Bishop's": "主教之",
"Blanched": "漂白之",
"Blank": "虚无之",
"Blazing": "炽热之",
"Blessed": "祝福之",
"Blighting": "枯萎之",
"Bloody": "血腥之",
"Bone": "白骨之",
"Boreal": "极北之",
"Bowyer's": "弓箭手之",
"Bright": "光明之",
"Bronze": "青铜之",
"Brown": "棕褐之",
"Brutal": "残忍之",
"Burgundy": "红棕之",
"Burly": "魁梧之",
"Burning": "燃烧之",
"Buzzing": "嗡鸣之",
"Calling": "呼唤之",
"Camphor": "樟脑之",
"Captain's": "领袖之",
"Carbuncle": "痈肿之",
"Caretaker's": "看顾者之",
"Carmine": "洋红之",
"Celestial": "天界之",
"Charged": "充能之",
"Chestnut": "栗色之",
"Chilling": "严寒之",
"Chromatic": "炫彩之",
"Cinnabar": "朱砂之",
"Cobalt": "钴蓝之",
"Commander's": "指挥官之",
"Communal": "公社之",
"Compact": "紧密之",
"Consecrated": "奉献之",
"Coral": "珊瑚之",
"Corosive": "腐蚀之",
"Crimson": "枣红之",
"Cruel": "残忍之",
"Cunning": "狡黠之",
"Deadly": "致命之",
"Dense": "致密之",
"Devious": "迂回之",
"Diamond": "钻石之",
"Divine": "神圣之",
"Dragon's": "龙之",
"Drake's": "德尔克之",
"Dun": "黑褐之",
"Eagleeye": "雕眼之",
"Earth": "大地之",
"Eburin": "象牙之",
"Echoing": "回音之",
"Elysian": "极乐之",
"Ember": "余烬之",
"Emerald": "绿宝石之",
"Entrapping": "陷阱之",
"Envenomed": "腐烂之",
"Eocene": "始新世之",
"Expert's": "专家之",
"Falconeye": "猎鹰之眼",
"Fanatic": "狂热之",
"Felicitous": "吉庆之",
"Feral": "野性之",
"Ferocious": "凶猛之",
"Fine": "精良之",
"Flaming": "烈焰之",
"Fletcher's": "制箭者之",
"Fool's": "愚人之",
"Forked": "分叉之",
"Fortified": "防御强化之",
"Fortuitous": "偶然之",
"Freezing": "冰冻之",
"Fungal": "真菌之",
"Furious": "狂怒之",
"Gaea's": "大地之",
"Gaia": "大地之",
"Garnet": "石榴石之",
"Glacial": "极冰之",
"Glimmering": "微光之",
"Glorious": "光荣之",
"Glowing": "发光之",
"Godly": "神圣之",
"Gold": "金之",
"Golemlord": "魔像领主之",
"Golemlord's": "魔像领主之",
"Grandmaster's": "宗师之",
"Graverobber's": "盗墓者之",
"Great Wyrm's": "大巨龙之",
"Gritty": "坚韧之",
"Guardian's": "护卫者之",
"Gymnastic": "体操之",
"Gymnast's": "体操之",
"Hallowed": "神圣之",
"Harpoonist's": "标枪之",
"Hawk Branded": "鹰之烙印之",
"Hawkeye": "鹰眼之",
"Hexing": "诅咒之",
"Hibernal": "极寒之",
"Hierophant's": "教皇之",
"Holy": "神圣之",
"Howling": "咆哮之",
"Iron": "铁之",
"Ivory": "象牙之",
"Jacinth": "风信子石之",
"Jack's": "随从之",
"Jade": "翡翠之",
"Jagged": "锯齿之",
"Jester's": "弄臣之",
"Jeweler's": "珠宝匠之",
"Joker's": "小丑之",
"Kenshi's": "剑士之",
"Keeper's": "守护者之",
"King's": "国王之",
"Knave's": "盗贼之",
"Knight's": "骑士之",
"Lancer's": "长矛手之",
"Lapis": "青金石之",
"Lapis Lazuli": "青金石之",
"Lion Branded": "狮子烙印之",
"Lizard's": "蜥蜴之",
"Lord's": "领主之",
"Loud": "响亮之",
"Lucky": "幸运之",
"Magekiller's": "法师杀手之",
"Maiden's": "处女之",
"Maroon": "褐红之",
"Marshal": "元帅之",
"Marshal's": "元帅之",
"Massive": "稳重之",
"Master's": "大师之",
"Mechanist's": "机械师之",
"Mentalist": "通灵之",
"Mentalist's": "通灵之",
"Merciless": "绝情之",
"Meteoric": "陨铁之",
"Miocene": "中新世之",
"Mnemonic": "记忆之",
"Monk's": "僧人之",
"Nature": "自然之",
"Nature's": "自然之",
"Necromancer": "死灵法师之",
"Necromancer's": "死灵法师之",
"Nickel": "白镍之",
"Noxious": "有害之",
"Null": "无效之",
"Ocher": "赭黄之",
"Oligocene": "渐新世之",
"Paleocene": "忠诚信使之",
"Paradox": "悖论之",
"Patriarch's": "先知之",
"Pearl": "珍珠之",
"Pestilent": "瘟疫之",
"Platinum": "白金之",
"Powered": "能量之",
"Preserver": "保护者之",
"Preserver's": "保护者之",
"Priest's": "祭司之",
"Prismatic": "彩虹之",
"Psychic": "心灵之",
"Pure": "纯净之",
"Raging": "暴怒之",
"Rainbow": "彩虹之",
"Realgar": "雄黄之",
"Red": "红之",
"Resonant": "共鸣之",
"Robineye": "知更鸟眼之",
"Rose Branded": "玫瑰烙印之",
"Ruby": "红宝石之",
"Rugged": "凹凸之",
"Russet": "铁锈之",
"Rusty": "生锈之",
"Sacred": "圣洁之",
"Saintly": "崇高之",
"Sanguinary": "鲜血之",
"Sapphire": "蓝宝石之",
"Savage": "野蛮之",
"Scarlet": "猩红之",
"Scintillating": "闪烁之",
"Scorching": "灼烧之",
"Screaming": "尖叫之",
"Sensei's": "老师之",
"Septic": "腐臭之",
"Serpent's": "海蛇之",
"Serrated": "锯齿之",
"Shadow": "阴影之",
"Shaman's": "萨满之",
"Sharp": "尖锐之",
"Shimmering": "微光之",
"Shivering": "寒颤之",
"Shocking": "雷霆之",
"Shogukusha's": "刺客之",
"Shouting": "咆哮之",
"Silver": "银之",
"Slayer's": "屠杀者之",
"Smoldering": "焖燃之",
"Smoking": "烟雾之",
"Snake's": "蛇之",
"Snowflake": "雪花之",
"Soldier's": "士兵之",
"Sounding": "鸣响之",
"Sparking": "火花之",
"Sparroweye": "麻雀眼之",
"Spearmaiden's": "标枪女郎之",
"Spiritual": "精神之",
"Stalwart": "健壮之",
"Static": "静电之",
"Steel": "钢之",
"Stout": "坚固之",
"Strange": "怪异之",
"Strong": "强壮之",
"Sturdy": "结实之",
"Summoner's": "召唤者之",
"Tangerine": "橙黄之",
"Terra's": "大地之",
"Thin": "轻薄之",
"Tin": "白铁之",
"Tireless": "不倦之",
"Topaz": "黄宝石之",
"Toxic": "剧毒之",
"Trainer's": "驯兽师之",
"Trickster's": "戏法师之",
"Triumphant": "得胜之",
"Trump": "胜者之",
"Turquoise": "绿松石之",
"Unearthly": "超凡之",
"Valkyrie's": "女武神之",
"Venomous": "剧毒之",
"Vermillion": "朱砂之",
"Veteran's": "老兵之",
"Vicious": "恶毒之",
"Victorious": "胜利之",
"Vigorous": "强健之",
"Viridian": "铭绿之",
"Visionary": "梦幻之",
"Vodoun": "伏都之",
"Volcanic": "火山之",
"Vulpine": "奸诈之",
"Wailing": "哀嚎之",
"Warden": "看守者之",
"Warder's": "监视者之",
"Warrior's": "战士之",
"Weird": "怪异之",
"Witch-hunter's": "猎巫者之",
"Wyrm's": "维特之",
"Yelling": "呐喊之",
"Zircon": "锆石之"
}
// Complete 288 suffixes
const suffixBaseMap: Record<string, string> = {
"Health": "健康之",
"Acceleration": "加速之",
"Anima": "生灵之",
"Balance": "平衡之",
"Equilibrium": "均势之",
"Stability": "安定之",
"Chance": "几率之",
"Greed": "贪欲之",
"Wealth": "财富之",
"Light": "光明之",
"Radiance": "光辉之",
"the Sun": "太阳之",
"Nirvana": "涅槃之",
"Vileness": "卑劣之",
"the Colosuss": "巨神之",
"the Jackal": "胡狼之",
"Protection": "守护之",
"Absorption": "吸收之",
"Life": "生命之",
"Warding": "防避之",
"the Sentinel": "步哨之",
"Guarding": "守卫之",
"Negation": "否定之",
"Piercing": "刺穿之",
"Bashing": "重击之",
"Puncturing": "爆破之",
"Thorns": "荆棘之",
"Spikes": "尖刺之",
"Readiness": "准备之",
"Alacrity": "迅捷之",
"Swiftness": "疾速之",
"Quickness": "快速之",
"Blocking": "格挡之",
"Deflecting": "偏向之",
"the Apprentice": "学徒之",
"the Magus": "法师之",
"Frost": "冰霜之",
"the Glacier": "冰川之",
"Flame": "火焰之",
"Fire": "烈火之",
"Burning": "燃烧之",
"Lightning": "闪电之",
"Thunder": "雷霆之",
"Shock": "电击之",
"Spark": "火花之",
"Static": "静电之",
"Craftsmanship": "工匠之",
"Quality": "品质之",
"Maiming": "残废之",
"Slaying": "杀戮之",
"Gore": "鲜血之",
"Carnage": "大屠杀之",
"Slaughter": "屠杀之",
"Butchery": "屠戮之",
"Evisceration": "剖腹之",
"Destruction": "毁灭之",
"Worth": "价值之",
"Measure": "度量之",
"Excellence": "卓越之",
"Performance": "精良之",
"Joyfulness": "欢欣之",
"Bliss": "至福之",
"Blight": "枯萎之",
"Venom": "毒素之",
"Pestilence": "瘟疫之",
"Anthrax": "炭疽之",
"Frostbite": "冻疮之",
"the Leech": "水蛭之",
"the Bat": "蝙蝠之",
"the Vampire": "吸血鬼之",
"the Lamprey": "八目鳗之",
"the Wraith": "幽灵之",
"the Ghoul": "食尸鬼之",
"the Locust": "蝗虫之",
"Daring": "果敢之",
"Truth": "真理之",
"Honor": "荣耀之",
"Avarice": "贪婪之",
"Luck": "幸运之",
"Good Luck": "好运之",
"Prosperity": "繁荣之",
"Fortune": "财富之",
"Perfection": "完美之",
"Enlightenment": "启迪之",
"Knowledge": "知识之",
"Substinence": "生计之",
"Vita": "活力之",
"Spirit": "精神之",
"Hope": "希望之",
"Freedom": "自由之",
"Atlus": "阿特拉斯之",
"Virility": "刚毅之",
"Traveling": "旅行之",
"Inertia": "抗性之",
"Self-Repair": "自我修复之",
"Fast Repair": "快速修复之",
"Ages": "万古之",
"Replenishing": "充盈之",
"Propogation": "繁衍之",
"the Kraken": "海妖之",
"Memory": "记忆之",
"the Elephant": "大象之",
"Power": "力量之",
"Grace": "优雅之",
"Grace and Power": "优雅与力量之",
"the Yeti": "雪人之",
"the Phoenix": "凤凰之",
"the Efreeti": "火灵之",
"the Cobra": "眼镜蛇之",
"the Elements": "元素之",
"the Icicle": "冰柱之",
"Winter": "严冬之",
"Frigidity": "寒冷之",
"Incineration": "焚化之",
"Passion": "激情之",
"Storms": "风暴之",
"Ennui": "厌倦之",
"Ire": "愤怒之",
"Wrath": "盛怒之",
"Transcendence": "卓越之",
"Envy": "嫉妒之",
"Fervor": "狂热之",
"Speed": "速度之",
"Haste": "急速之",
"Dexterity": "敏捷之",
"Skill": "技巧之",
"Accuracy": "准确之",
"Precision": "精确之",
"Sniping": "狙击之",
"the Bear": "巨熊之",
"Strength": "力量之",
"Might": "强力之",
"the Ox": "公牛之",
"the Giant": "巨人之",
"the Titan": "泰坦之",
"the Fox": "狐狸之",
"the Wolf": "苍狼之",
"the Tiger": "猛虎之",
"the Mammoth": "猛犸之",
"the Colossus": "巨神之",
"the Squid": "乌贼之",
"the Whale": "巨鲸之",
"Defiance": "蔑视之",
"Fumigation": "熏蒸之",
"Ease": "轻便之",
"Simplicity": "简朴之",
"Dawn": "黎明之",
"Sunlight": "阳光之",
"Energy": "能量之",
"Mind": "心灵之",
"Brilliance": "辉煌之",
"Sorcery": "巫术之",
"Wizardry": "法术之",
"Warmth": "温暖之",
"Remedy": "救治之",
"Amelioration": "改善之",
"Regeneration": "再生之",
"Regrowth": "新生之",
"Revivification": "复苏之",
"Spikers": "尖刺之",
"Passing": "消逝之",
"Stamina": "耐力之",
"Pacing": "步行之",
"Striding": "跨步之",
"Health Everlasting": "永恒健康之",
"Life Everlasting": "永恒生命之",
"Amianthus": "石棉之",
"Fire Quenching": "灭火之",
"Incombustibility": "不燃之",
"Coolness": "清凉之",
"Faith": "信仰之",
"Resistance": "抗性之",
"Insulation": "绝缘之",
"Grounding": "接地之",
"the Dynamo": "发电机之",
"Stoicism": "坚忍之",
"Warming": "保温之",
"Thawing": "解冻之",
"the Dunes": "沙丘之",
"the Sirocco": "热风之",
"Desire": "渴望之",
"Razors": "剃刀之",
"Swords": "剑刃之",
"Malice": "恶意之",
"the Mind": "心灵之",
"Telekinesis": "心灵传动之",
"Teleportation": "传送之",
"Teleport Shield": "传送护盾之",
"Inner Sight": "内视之",
"Slow Missiles": "慢速箭之",
"Damage Amplification": "伤害加深之",
"Amplify Damage": "伤害加深之",
"Weaken": "削弱之",
"Dim Vision": "微暗灵视之",
"Iron Maiden": "攻击反噬之",
"Terror": "恐惧之",
"Confusion": "迷乱之",
"Life Tap": "偷取生命之",
"Attraction": "吸引之",
"Decrepification": "衰老之",
"Lower Resistance": "降低抗性之",
"Taunting": "嘲弄之",
"Howling": "狂嚎之",
"Shouting": "大叫之",
"Battle Cry": "刺耳尖叫之",
"Battle Orders": "战斗体制之",
"Battle Command": "战斗指挥之",
"War Cry": "战鸣之",
"Stunning": "击晕之",
"Concentration": "专注之",
"Grim Ward": "残骸清理之",
"Item Finding": "寻找物品之",
"Potion Finding": "寻找药剂之",
"Holy Bolts": "圣光弹之",
"Blessed Hammers": "祝福之槌之",
"Conversion": "转化之",
"Sacrifice": "牺牲之",
"Zeal": "热诚之",
"Vengeance": "复仇之",
"Fist of the Heavens": "天堂之拳之",
"Static Field": "静电场之",
"Nova": "新星之",
"Novas": "新星之",
"Nova Shield": "新星护盾之",
"Frost Novas": "冰霜新星之",
"Frost Shield": "冰霜护盾之",
"Ice Blast": "冰风暴之",
"Ice Blasts": "冰风暴之",
"Icebolt": "冰弹之",
"Ice Bolts": "冰弹之",
"Glacial Spike": "冰尖柱之",
"Glacial Spikes": "冰尖柱之",
"Blizzard": "暴风雪之",
"Blizzards": "暴风雪之",
"Frozen Orb": "冰封球之",
"Frozen Orbs": "冰封球之",
"Frozen Armor": "冰封甲之",
"Shiver Armor": "碎冰甲之",
"Chilling Armor": "寒冰甲之",
"Charged Bolts": "充能弹之",
"Charged Shield": "充能护盾之",
"Charged Strike": "充能一击之",
"Lightning Strike": "雷电一击之",
"Lightning Javelin": "闪电标枪之",
"Lightning Fury": "闪电之怒之",
"Chain Lightning": "连锁闪电之",
"Thunder Storm": "雷暴之",
"Energy Shield": "能量护盾之",
"Hydras": "九头海蛇之",
"Hydra Shield": "九头蛇护盾之",
"Firebolt": "火弹之",
"Firebolts": "火弹之",
"Fire Bolts": "火弹之",
"Fire Ball": "火球之",
"Fire Balls": "火球之",
"Fire Wall": "火墙之",
"Fire Walls": "火墙之",
"Enchant": "强化之",
"Enchantment": "强化之",
"Meteor": "陨石之",
"Meteors": "陨石之",
"Blaze": "炽烈之径之",
"Blazing": "炽烈之径之",
"Firestorms": "火风暴之",
"Molten Boulders": "熔岩巨石之",
"Eruption": "火山喷发之",
"Cyclone Armor": "飓风装甲之",
"Twister": "龙卷风之",
"Volcano": "火山之",
"Tornado": "暴风之",
"Armageddon": "末日风暴之",
"Hurricane": "飓风之",
"Teeth": "牙之",
"Bone Armor": "白骨装甲之",
"Poison Dagger": "毒匕首之",
"Corpse Explosions": "尸体爆炸之",
"Bone Walls": "骨墙之",
"Poison Explosion": "毒爆之",
"Bone Spears": "骨矛之",
"Bone Imprisonment": "骨牢之",
"Poison Novas": "剧毒新星之",
"Bone Spirits": "白骨之魂之",
"Clay Golem Summoning": "黏土魔像之",
"Blood Golem Summoning": "鲜血魔像之",
"Iron Golem Creation": "钢铁魔像之",
"Fire Golem Summoning": "火焰魔像之",
"Raise Skeletons": "复生骷髅之",
"Raise Skeletal Mages": "骷髅法师之",
"Magic Arrows": "魔法箭之",
"Fire Arrows": "火箭之",
"Cold Arrows": "冰箭之",
"Multiple Shot": "多重箭之",
"Exploding Arrows": "爆裂箭之",
"Ice Arrows": "冻结箭之",
"Guided Arrows": "追踪箭之",
"Immolating Arrows": "牺牲之箭之",
"Freezing Arrows": "急冻箭之",
"Power Strike": "威力一击之",
"Poison Jab": "毒枪之",
"Jabbing": "戳刺之",
"Impaling Strike": "刺爆之",
"Fending": "击退之",
"Plague Jab": "瘟疫标枪之"
}
// Build full suffix map
const suffixMap: Record<string, string> = {}
for (const [k, v] of Object.entries(suffixBaseMap)) {
suffixMap['of ' + k] = v
suffixMap[k] = v
}
// Validation
const missingPre = prefixes.filter(p => !prefixMap[p])
const missingSuf = suffixes.filter(s => !suffixMap[s] && !suffixMap['of ' + s])
console.log('Validating coverage...')
console.log('Prefix coverage: ' + (prefixes.length - missingPre.length) + ' / ' + prefixes.length)
if (missingPre.length > 0) console.log('Missing prefixes:', missingPre)
console.log('Suffix coverage: ' + (suffixes.length - missingSuf.length) + ' / ' + suffixes.length)
if (missingSuf.length > 0) console.log('Missing suffixes:', missingSuf)
if (missingPre.length === 0 && missingSuf.length === 0) {
console.log('SUCCESS! 100% Affix Coverage achieved for all 252 prefixes and 288 suffixes.')
}

View File

@ -144,305 +144,18 @@ const RUNE_INFO: Record<string, { num: number; nameEn: string; lvl: number; weap
* - formatPropertyCode(code, min, max, par)
* - formatPropertyCode(code, value)
*/
const CANONICAL_SKILLS: readonly [number, string, string, string][] = [
[0, "Attack", "Attack", ""],
[1, "Kick", "Kick", ""],
[2, "Throw", "Throw", ""],
[3, "Unsummon", "Unsummon", ""],
[4, "Left Hand Throw", "Left Hand Throw", ""],
[5, "Left Hand Swing", "Left Hand Swing", ""],
[6, "Magic Arrow", "Magic Arrow", "ama"],
[7, "Fire Arrow", "Fire Arrow", "ama"],
[8, "Inner Sight", "Inner Sight", "ama"],
[9, "Critical Strike", "Critical Strike", "ama"],
[10, "Jab", "Jab", "ama"],
[11, "Cold Arrow", "Cold Arrow", "ama"],
[12, "Multiple Shot", "Multiple Shot", "ama"],
[13, "Dodge", "Dodge", "ama"],
[14, "Power Strike", "Power Strike", "ama"],
[15, "Poison Javelin", "Poison Javelin", "ama"],
[16, "Exploding Arrow", "Exploding Arrow", "ama"],
[17, "Slow Missiles", "Slow Missiles", "ama"],
[18, "Avoid", "Avoid", "ama"],
[19, "Impale", "Impale", "ama"],
[20, "Lightning Bolt", "Lightning Bolt", "ama"],
[21, "Ice Arrow", "Ice Arrow", "ama"],
[22, "Guided Arrow", "Guided Arrow", "ama"],
[23, "Penetrate", "Penetrate", "ama"],
[24, "Charged Strike", "Charged Strike", "ama"],
[25, "Plague Javelin", "Plague Javelin", "ama"],
[26, "Strafe", "Strafe", "ama"],
[27, "Immolation Arrow", "Immolation Arrow", "ama"],
[28, "Dopplezon", "Decoy", "ama"],
[29, "Evade", "Evade", "ama"],
[30, "Fend", "Fend", "ama"],
[31, "Freezing Arrow", "Freezing Arrow", "ama"],
[32, "Valkyrie", "Valkyrie", "ama"],
[33, "Pierce", "Pierce", "ama"],
[34, "Lightning Strike", "Lightning Strike", "ama"],
[35, "Lightning Fury", "Lightning Fury", "ama"],
[36, "Fire Bolt", "Fire Bolt", "sor"],
[37, "Warmth", "Warmth", "sor"],
[38, "Charged Bolt", "Charged Bolt", "sor"],
[39, "Ice Bolt", "Ice Bolt", "sor"],
[40, "Frozen Armor", "Frozen Armor", "sor"],
[41, "Inferno", "Inferno", "sor"],
[42, "Static Field", "Static Field", "sor"],
[43, "Telekinesis", "Telekinesis", "sor"],
[44, "Frost Nova", "Frost Nova", "sor"],
[45, "Ice Blast", "Ice Blast", "sor"],
[46, "Blaze", "Blaze", "sor"],
[47, "Fire Ball", "Fire Ball", "sor"],
[48, "Nova", "Nova", "sor"],
[49, "Lightning", "Lightning", "sor"],
[50, "Shiver Armor", "Shiver Armor", "sor"],
[51, "Fire Wall", "Fire Wall", "sor"],
[52, "Enchant", "Enchant", "sor"],
[53, "Chain Lightning", "Chain Lightning", "sor"],
[54, "Teleport", "Teleport", "sor"],
[55, "Glacial Spike", "Glacial Spike", "sor"],
[56, "Meteor", "Meteor", "sor"],
[57, "Thunder Storm", "Thunder Storm", "sor"],
[58, "Energy Shield", "Energy Shield", "sor"],
[59, "Blizzard", "Blizzard", "sor"],
[60, "Chilling Armor", "Chilling Armor", "sor"],
[61, "Fire Mastery", "Fire Mastery", "sor"],
[62, "Hydra", "Hydra", "sor"],
[63, "Lightning Mastery", "Lightning Mastery", "sor"],
[64, "Frozen Orb", "Frozen Orb", "sor"],
[65, "Cold Mastery", "Cold Mastery", "sor"],
[66, "Amplify Damage", "Amplify Damage", "nec"],
[67, "Teeth", "Teeth", "nec"],
[68, "Bone Armor", "Bone Armor", "nec"],
[69, "Skeleton Mastery", "Skeleton Mastery", "nec"],
[70, "Raise Skeleton", "Raise Skeleton", "nec"],
[71, "Dim Vision", "Dim Vision", "nec"],
[72, "Weaken", "Weaken", "nec"],
[73, "Poison Dagger", "Poison Dagger", "nec"],
[74, "Corpse Explosion", "Corpse Explosion", "nec"],
[75, "Clay Golem", "Clay Golem", "nec"],
[76, "Iron Maiden", "Iron Maiden", "nec"],
[77, "Terror", "Terror", "nec"],
[78, "Bone Wall", "Bone Wall", "nec"],
[79, "Golem Mastery", "Golem Mastery", "nec"],
[80, "Raise Skeletal Mage", "Raise Skeletal Mage", "nec"],
[81, "Confuse", "Confuse", "nec"],
[82, "Life Tap", "Life Tap", "nec"],
[83, "Poison Explosion", "Poison Explosion", "nec"],
[84, "Bone Spear", "Bone Spear", "nec"],
[85, "BloodGolem", "Blood Golem", "nec"],
[86, "Attract", "Attract", "nec"],
[87, "Decrepify", "Decrepify", "nec"],
[88, "Bone Prison", "Bone Prison", "nec"],
[89, "Summon Resist", "Summon Resist", "nec"],
[90, "IronGolem", "Iron Golem", "nec"],
[91, "Lower Resist", "Lower Resist", "nec"],
[92, "Poison Nova", "Poison Nova", "nec"],
[93, "Bone Spirit", "Bone Spirit", "nec"],
[94, "FireGolem", "Fire Golem", "nec"],
[95, "Revive", "Revive", "nec"],
[96, "Sacrifice", "Sacrifice", "pal"],
[97, "Smite", "Smite", "pal"],
[98, "Might", "Might", "pal"],
[99, "Prayer", "Prayer", "pal"],
[100, "Resist Fire", "Resist Fire", "pal"],
[101, "Holy Bolt", "Holy Bolt", "pal"],
[102, "Holy Fire", "Holy Fire", "pal"],
[103, "Thorns", "Thorns", "pal"],
[104, "Defiance", "Defiance", "pal"],
[105, "Resist Cold", "Resist Cold", "pal"],
[106, "Zeal", "Zeal", "pal"],
[107, "Charge", "Charge", "pal"],
[108, "Blessed Aim", "Blessed Aim", "pal"],
[109, "Cleansing", "Cleansing", "pal"],
[110, "Resist Lightning", "Resist Lightning", "pal"],
[111, "Vengeance", "Vengeance", "pal"],
[112, "Blessed Hammer", "Blessed Hammer", "pal"],
[113, "Concentration", "Concentration", "pal"],
[114, "Holy Freeze", "Holy Freeze", "pal"],
[115, "Vigor", "Vigor", "pal"],
[116, "Conversion", "Conversion", "pal"],
[117, "Holy Shield", "Holy Shield", "pal"],
[118, "Holy Shock", "Holy Shock", "pal"],
[119, "Sanctuary", "Sanctuary", "pal"],
[120, "Meditation", "Meditation", "pal"],
[121, "Fist of the Heavens", "Fist of the Heavens", "pal"],
[122, "Fanaticism", "Fanaticism", "pal"],
[123, "Conviction", "Conviction", "pal"],
[124, "Redemption", "Redemption", "pal"],
[125, "Salvation", "Salvation", "pal"],
[126, "Bash", "Bash", "bar"],
[127, "Sword Mastery", "Sword Mastery", "bar"],
[128, "Axe Mastery", "Axe Mastery", "bar"],
[129, "Mace Mastery", "Mace Mastery", "bar"],
[130, "Howl", "Howl", "bar"],
[131, "Find Potion", "Find Potion", "bar"],
[132, "Leap", "Leap", "bar"],
[133, "Double Swing", "Double Swing", "bar"],
[134, "Pole Arm Mastery", "Pole Arm Mastery", "bar"],
[135, "Throwing Mastery", "Throwing Mastery", "bar"],
[136, "Spear Mastery", "Spear Mastery", "bar"],
[137, "Taunt", "Taunt", "bar"],
[138, "Shout", "Shout", "bar"],
[139, "Stun", "Stun", "bar"],
[140, "Double Throw", "Double Throw", "bar"],
[141, "Increased Stamina", "Increased Stamina", "bar"],
[142, "Find Item", "Find Item", "bar"],
[143, "Leap Attack", "Leap Attack", "bar"],
[144, "Concentrate", "Concentrate", "bar"],
[145, "Iron Skin", "Iron Skin", "bar"],
[146, "Battle Cry", "Battle Cry", "bar"],
[147, "Frenzy", "Frenzy", "bar"],
[148, "Increased Speed", "Increased Speed", "bar"],
[149, "Battle Orders", "Battle Orders", "bar"],
[150, "Grim Ward", "Grim Ward", "bar"],
[151, "Whirlwind", "Whirlwind", "bar"],
[152, "Berserk", "Berserk", "bar"],
[153, "Natural Resistance", "Natural Resistance", "bar"],
[154, "War Cry", "War Cry", "bar"],
[155, "Battle Command", "Battle Command", "bar"],
[197, "DiabWall", "Firestorm", ""],
[217, "Scroll of Identify", "Scroll of Identify", ""],
[218, "Book of Identify", "Tome of Identify", ""],
[219, "Scroll of Townportal", "Scroll of Townportal", ""],
[220, "Book of Townportal", "Tome of Townportal", ""],
[221, "Raven", "Raven", "dru"],
[222, "Plague Poppy", "Poison Creeper", "dru"],
[223, "Wearwolf", "Werewolf", "dru"],
[224, "Shape Shifting", "Lycanthropy", "dru"],
[225, "Firestorm", "Firestorm", "dru"],
[226, "Oak Sage", "Oak Sage", "dru"],
[227, "Summon Spirit Wolf", "Summon Spirit Wolf", "dru"],
[228, "Wearbear", "Werebear", "dru"],
[229, "Molten Boulder", "Molten Boulder", "dru"],
[230, "Arctic Blast", "Arctic Blast", "dru"],
[231, "Cycle of Life", "Carrion Vine", "dru"],
[232, "Feral Rage", "Feral Rage", "dru"],
[233, "Maul", "Maul", "dru"],
[234, "Eruption", "Fissure", "dru"],
[235, "Cyclone Armor", "Cyclone Armor", "dru"],
[236, "Heart of Wolverine", "Heart of Wolverine", "dru"],
[237, "Summon Fenris", "Summon Dire Wolf", "dru"],
[238, "Rabies", "Rabies", "dru"],
[239, "Fire Claws", "Fire Claws", "dru"],
[240, "Twister", "Twister", "dru"],
[241, "Vines", "Solar Creeper", "dru"],
[242, "Hunger", "Hunger", "dru"],
[243, "Shock Wave", "Shock Wave", "dru"],
[244, "Volcano", "Volcano", "dru"],
[245, "Tornado", "Tornado", "dru"],
[246, "Spirit of Barbs", "Spirit of Barbs", "dru"],
[247, "Summon Grizzly", "Summon Grizzly", "dru"],
[248, "Fury", "Fury", "dru"],
[249, "Armageddon", "Armageddon", "dru"],
[250, "Hurricane", "Hurricane", "dru"],
[251, "Fire Trauma", "Fire Blast", "ass"],
[252, "Claw Mastery", "Claw Mastery", "ass"],
[253, "Psychic Hammer", "Psychic Hammer", "ass"],
[254, "Tiger Strike", "Tiger Strike", "ass"],
[255, "Dragon Talon", "Dragon Talon", "ass"],
[256, "Shock Field", "Shock Web", "ass"],
[257, "Blade Sentinel", "Blade Sentinel", "ass"],
[258, "Quickness", "Burst of Speed", "ass"],
[259, "Fists of Fire", "Fists of Fire", "ass"],
[260, "Dragon Claw", "Dragon Claw", "ass"],
[261, "Charged Bolt Sentry", "Charged Bolt Sentry", "ass"],
[262, "Wake of Fire Sentry", "Wake of Fire", "ass"],
[263, "Weapon Block", "Weapon Block", "ass"],
[264, "Cloak of Shadows", "Cloak of Shadows", "ass"],
[265, "Cobra Strike", "Cobra Strike", "ass"],
[266, "Blade Fury", "Blade Fury", "ass"],
[267, "Fade", "Fade", "ass"],
[268, "Shadow Warrior", "Shadow Warrior", "ass"],
[269, "Claws of Thunder", "Claws of Thunder", "ass"],
[270, "Dragon Tail", "Dragon Tail", "ass"],
[271, "Lightning Sentry", "Lightning Sentry", "ass"],
[272, "Inferno Sentry", "Wake of Inferno", "ass"],
[273, "Mind Blast", "Mind Blast", "ass"],
[274, "Blades of Ice", "Blades of Ice", "ass"],
[275, "Dragon Flight", "Dragon Flight", "ass"],
[276, "Death Sentry", "Death Sentry", "ass"],
[277, "Blade Shield", "Blade Shield", "ass"],
[278, "Venom", "Venom", "ass"],
[279, "Shadow Master", "Shadow Master", "ass"],
[280, "Royal Strike", "Phoenix Strike", "ass"],
[350, "Delerium Change", "Delirium", ""],
]
import {
CANONICAL_SKILLS,
resolveSkillInfo,
CANONICAL_SKILL_TABS,
CANONICAL_REANIMATE_MONSTERS,
} from './skills-data.ts'
const CLASS_ONLY_LABELS: Record<string, string> = {
ama: '(Amazon Only)',
sor: '(Sorceress Only)',
nec: '(Necromancer Only)',
pal: '(Paladin Only)',
bar: '(Barbarian Only)',
dru: '(Druid Only)',
ass: '(Assassin Only)',
}
const SKILL_LOOKUP_BY_ID = new Map<number, { name: string; classOnly?: string }>()
const SKILL_LOOKUP_BY_NAME = new Map<string, { name: string; classOnly?: string }>()
for (const [id, rawName, displayName, cls] of CANONICAL_SKILLS) {
const classOnly = cls ? CLASS_ONLY_LABELS[cls] : undefined
const info = classOnly ? { name: displayName, classOnly } : { name: displayName }
SKILL_LOOKUP_BY_ID.set(id, info)
SKILL_LOOKUP_BY_NAME.set(rawName.toLowerCase(), info)
SKILL_LOOKUP_BY_NAME.set(displayName.toLowerCase(), info)
}
/**
* Resolves a skill parameter (numeric ID or internal name from Skills.txt)
* into its 1.13c canonical English display name and optional class restriction.
*/
export function resolveSkillInfo(par: string | number | undefined): { name: string; classOnly?: string } {
if (par === undefined || par === '') return { name: 'Skill' }
const str = String(par).trim()
const num = Number(str)
if (!Number.isNaN(num) && SKILL_LOOKUP_BY_ID.has(num)) {
return SKILL_LOOKUP_BY_ID.get(num)!
}
const byName = SKILL_LOOKUP_BY_NAME.get(str.toLowerCase())
if (byName) return byName
return { name: str }
}
/**
* Canonical 1.13c skilltab mapping (par 0..20 from CharStats.txt StrSkillTab1..3 + StrClassOnly).
*/
const CANONICAL_SKILL_TABS: Record<number, string> = {
0: 'Bow and Crossbow Skills (Amazon Only)',
1: 'Passive and Magic Skills (Amazon Only)',
2: 'Javelin and Spear Skills (Amazon Only)',
3: 'Fire Skills (Sorceress Only)',
4: 'Lightning Skills (Sorceress Only)',
5: 'Cold Skills (Sorceress Only)',
6: 'Curses (Necromancer Only)',
7: 'Poison and Bone Skills (Necromancer Only)',
8: 'Summoning Skills (Necromancer Only)',
9: 'Combat Skills (Paladin Only)',
10: 'Offensive Auras (Paladin Only)',
11: 'Defensive Auras (Paladin Only)',
12: 'Combat Skills (Barbarian Only)',
13: 'Masteries (Barbarian Only)',
14: 'Warcries (Barbarian Only)',
15: 'Summoning Skills (Druid Only)',
16: 'Shape Shifting Skills (Druid Only)',
17: 'Elemental Skills (Druid Only)',
18: 'Traps (Assassin Only)',
19: 'Shadow Disciplines (Assassin Only)',
20: 'Martial Arts (Assassin Only)',
}
/**
* Canonical 1.13c MonStats.txt hcIdx -> NameStr for reanimate property.
*/
const CANONICAL_REANIMATE_MONSTERS: Record<number, string> = {
0: 'Skeleton',
1: 'Returned',
2: 'Bone Warrior',
3: 'Burning Dead',
4: 'Horror',
5: 'Zombie',
export {
resolveSkillInfo,
CANONICAL_SKILLS,
CANONICAL_SKILL_TABS,
CANONICAL_REANIMATE_MONSTERS,
}
function formatPerLevelVal(rawEighths: number): string {
@ -502,7 +215,7 @@ export function formatPropertyCode(
const plusVal = effectiveVal >= 0 ? `+${effectiveVal}` : `${effectiveVal}`
switch (code.trim().toLowerCase()) {
switch (code.trim().toLowerCase().replace(/^\*/, '')) {
// --- Skills & Class Skills (descFunc 1, 13, 14, 16, 27, 28) ---
case 'allskills':
return `${plusVal} to All Skills`
@ -590,8 +303,8 @@ export function formatPropertyCode(
}
case 'charged': {
const sk = resolveSkillInfo(par)
const charges = min || effectiveVal || 10
const slvl = max || 1
const charges = Math.abs(min || effectiveVal || 10)
const slvl = Math.abs(max || 1)
return `Level ${slvl} ${sk.name} (${charges}/${charges} Charges)`
}

304
src/game/skills-data.ts Normal file
View File

@ -0,0 +1,304 @@
/**
* Canonical 1.13c Skills Data & ID Lookup Table (Skills.txt).
*/
export const CANONICAL_SKILLS: readonly [number, string, string, string][] = [
[0, "Attack", "Attack", ""],
[1, "Kick", "Kick", ""],
[2, "Throw", "Throw", ""],
[3, "Unsummon", "Unsummon", ""],
[4, "Left Hand Throw", "Left Hand Throw", ""],
[5, "Left Hand Swing", "Left Hand Swing", ""],
[6, "Magic Arrow", "Magic Arrow", "ama"],
[7, "Fire Arrow", "Fire Arrow", "ama"],
[8, "Inner Sight", "Inner Sight", "ama"],
[9, "Critical Strike", "Critical Strike", "ama"],
[10, "Jab", "Jab", "ama"],
[11, "Cold Arrow", "Cold Arrow", "ama"],
[12, "Multiple Shot", "Multiple Shot", "ama"],
[13, "Dodge", "Dodge", "ama"],
[14, "Power Strike", "Power Strike", "ama"],
[15, "Poison Javelin", "Poison Javelin", "ama"],
[16, "Exploding Arrow", "Exploding Arrow", "ama"],
[17, "Slow Missiles", "Slow Missiles", "ama"],
[18, "Avoid", "Avoid", "ama"],
[19, "Impale", "Impale", "ama"],
[20, "Lightning Bolt", "Lightning Bolt", "ama"],
[21, "Ice Arrow", "Ice Arrow", "ama"],
[22, "Guided Arrow", "Guided Arrow", "ama"],
[23, "Penetrate", "Penetrate", "ama"],
[24, "Charged Strike", "Charged Strike", "ama"],
[25, "Plague Javelin", "Plague Javelin", "ama"],
[26, "Strafe", "Strafe", "ama"],
[27, "Immolation Arrow", "Immolation Arrow", "ama"],
[28, "Dopplezon", "Decoy", "ama"],
[29, "Evade", "Evade", "ama"],
[30, "Fend", "Fend", "ama"],
[31, "Freezing Arrow", "Freezing Arrow", "ama"],
[32, "Valkyrie", "Valkyrie", "ama"],
[33, "Pierce", "Pierce", "ama"],
[34, "Lightning Strike", "Lightning Strike", "ama"],
[35, "Lightning Fury", "Lightning Fury", "ama"],
[36, "Fire Bolt", "Fire Bolt", "sor"],
[37, "Warmth", "Warmth", "sor"],
[38, "Charged Bolt", "Charged Bolt", "sor"],
[39, "Ice Bolt", "Ice Bolt", "sor"],
[40, "Frozen Armor", "Frozen Armor", "sor"],
[41, "Inferno", "Inferno", "sor"],
[42, "Static Field", "Static Field", "sor"],
[43, "Telekinesis", "Telekinesis", "sor"],
[44, "Frost Nova", "Frost Nova", "sor"],
[45, "Ice Blast", "Ice Blast", "sor"],
[46, "Blaze", "Blaze", "sor"],
[47, "Fire Ball", "Fire Ball", "sor"],
[48, "Nova", "Nova", "sor"],
[49, "Lightning", "Lightning", "sor"],
[50, "Shiver Armor", "Shiver Armor", "sor"],
[51, "Fire Wall", "Fire Wall", "sor"],
[52, "Enchant", "Enchant", "sor"],
[53, "Chain Lightning", "Chain Lightning", "sor"],
[54, "Teleport", "Teleport", "sor"],
[55, "Glacial Spike", "Glacial Spike", "sor"],
[56, "Meteor", "Meteor", "sor"],
[57, "Thunder Storm", "Thunder Storm", "sor"],
[58, "Energy Shield", "Energy Shield", "sor"],
[59, "Blizzard", "Blizzard", "sor"],
[60, "Chilling Armor", "Chilling Armor", "sor"],
[61, "Fire Mastery", "Fire Mastery", "sor"],
[62, "Hydra", "Hydra", "sor"],
[63, "Lightning Mastery", "Lightning Mastery", "sor"],
[64, "Frozen Orb", "Frozen Orb", "sor"],
[65, "Cold Mastery", "Cold Mastery", "sor"],
[66, "Amplify Damage", "Amplify Damage", "nec"],
[67, "Teeth", "Teeth", "nec"],
[68, "Bone Armor", "Bone Armor", "nec"],
[69, "Skeleton Mastery", "Skeleton Mastery", "nec"],
[70, "Raise Skeleton", "Raise Skeleton", "nec"],
[71, "Dim Vision", "Dim Vision", "nec"],
[72, "Weaken", "Weaken", "nec"],
[73, "Poison Dagger", "Poison Dagger", "nec"],
[74, "Corpse Explosion", "Corpse Explosion", "nec"],
[75, "Clay Golem", "Clay Golem", "nec"],
[76, "Iron Maiden", "Iron Maiden", "nec"],
[77, "Terror", "Terror", "nec"],
[78, "Bone Wall", "Bone Wall", "nec"],
[79, "Golem Mastery", "Golem Mastery", "nec"],
[80, "Raise Skeletal Mage", "Raise Skeletal Mage", "nec"],
[81, "Confuse", "Confuse", "nec"],
[82, "Life Tap", "Life Tap", "nec"],
[83, "Poison Explosion", "Poison Explosion", "nec"],
[84, "Bone Spear", "Bone Spear", "nec"],
[85, "BloodGolem", "Blood Golem", "nec"],
[86, "Attract", "Attract", "nec"],
[87, "Decrepify", "Decrepify", "nec"],
[88, "Bone Prison", "Bone Prison", "nec"],
[89, "Summon Resist", "Summon Resist", "nec"],
[90, "IronGolem", "Iron Golem", "nec"],
[91, "Lower Resist", "Lower Resist", "nec"],
[92, "Poison Nova", "Poison Nova", "nec"],
[93, "Bone Spirit", "Bone Spirit", "nec"],
[94, "FireGolem", "Fire Golem", "nec"],
[95, "Revive", "Revive", "nec"],
[96, "Sacrifice", "Sacrifice", "pal"],
[97, "Smite", "Smite", "pal"],
[98, "Might", "Might", "pal"],
[99, "Prayer", "Prayer", "pal"],
[100, "Resist Fire", "Resist Fire", "pal"],
[101, "Holy Bolt", "Holy Bolt", "pal"],
[102, "Holy Fire", "Holy Fire", "pal"],
[103, "Thorns", "Thorns", "pal"],
[104, "Defiance", "Defiance", "pal"],
[105, "Resist Cold", "Resist Cold", "pal"],
[106, "Zeal", "Zeal", "pal"],
[107, "Charge", "Charge", "pal"],
[108, "Blessed Aim", "Blessed Aim", "pal"],
[109, "Cleansing", "Cleansing", "pal"],
[110, "Resist Lightning", "Resist Lightning", "pal"],
[111, "Vengeance", "Vengeance", "pal"],
[112, "Blessed Hammer", "Blessed Hammer", "pal"],
[113, "Concentration", "Concentration", "pal"],
[114, "Holy Freeze", "Holy Freeze", "pal"],
[115, "Vigor", "Vigor", "pal"],
[116, "Conversion", "Conversion", "pal"],
[117, "Holy Shield", "Holy Shield", "pal"],
[118, "Holy Shock", "Holy Shock", "pal"],
[119, "Sanctuary", "Sanctuary", "pal"],
[120, "Meditation", "Meditation", "pal"],
[121, "Fist of the Heavens", "Fist of the Heavens", "pal"],
[122, "Fanaticism", "Fanaticism", "pal"],
[123, "Conviction", "Conviction", "pal"],
[124, "Redemption", "Redemption", "pal"],
[125, "Salvation", "Salvation", "pal"],
[126, "Bash", "Bash", "bar"],
[127, "Sword Mastery", "Sword Mastery", "bar"],
[128, "Axe Mastery", "Axe Mastery", "bar"],
[129, "Mace Mastery", "Mace Mastery", "bar"],
[130, "Howl", "Howl", "bar"],
[131, "Find Potion", "Find Potion", "bar"],
[132, "Leap", "Leap", "bar"],
[133, "Double Swing", "Double Swing", "bar"],
[134, "Pole Arm Mastery", "Pole Arm Mastery", "bar"],
[135, "Throwing Mastery", "Throwing Mastery", "bar"],
[136, "Spear Mastery", "Spear Mastery", "bar"],
[137, "Taunt", "Taunt", "bar"],
[138, "Shout", "Shout", "bar"],
[139, "Stun", "Stun", "bar"],
[140, "Double Throw", "Double Throw", "bar"],
[141, "Increased Stamina", "Increased Stamina", "bar"],
[142, "Find Item", "Find Item", "bar"],
[143, "Leap Attack", "Leap Attack", "bar"],
[144, "Concentrate", "Concentrate", "bar"],
[145, "Iron Skin", "Iron Skin", "bar"],
[146, "Battle Cry", "Battle Cry", "bar"],
[147, "Frenzy", "Frenzy", "bar"],
[148, "Increased Speed", "Increased Speed", "bar"],
[149, "Battle Orders", "Battle Orders", "bar"],
[150, "Grim Ward", "Grim Ward", "bar"],
[151, "Whirlwind", "Whirlwind", "bar"],
[152, "Berserk", "Berserk", "bar"],
[153, "Natural Resistance", "Natural Resistance", "bar"],
[154, "War Cry", "War Cry", "bar"],
[155, "Battle Command", "Battle Command", "bar"],
[197, "DiabWall", "Firestorm", ""],
[217, "Scroll of Identify", "Scroll of Identify", ""],
[218, "Book of Identify", "Tome of Identify", ""],
[219, "Scroll of Townportal", "Scroll of Townportal", ""],
[220, "Book of Townportal", "Tome of Townportal", ""],
[221, "Raven", "Raven", "dru"],
[222, "Plague Poppy", "Poison Creeper", "dru"],
[223, "Wearwolf", "Werewolf", "dru"],
[224, "Shape Shifting", "Lycanthropy", "dru"],
[225, "Firestorm", "Firestorm", "dru"],
[226, "Oak Sage", "Oak Sage", "dru"],
[227, "Summon Spirit Wolf", "Summon Spirit Wolf", "dru"],
[228, "Wearbear", "Werebear", "dru"],
[229, "Molten Boulder", "Molten Boulder", "dru"],
[230, "Arctic Blast", "Arctic Blast", "dru"],
[231, "Cycle of Life", "Carrion Vine", "dru"],
[232, "Feral Rage", "Feral Rage", "dru"],
[233, "Maul", "Maul", "dru"],
[234, "Eruption", "Fissure", "dru"],
[235, "Cyclone Armor", "Cyclone Armor", "dru"],
[236, "Heart of Wolverine", "Heart of Wolverine", "dru"],
[237, "Summon Fenris", "Summon Dire Wolf", "dru"],
[238, "Rabies", "Rabies", "dru"],
[239, "Fire Claws", "Fire Claws", "dru"],
[240, "Twister", "Twister", "dru"],
[241, "Vines", "Solar Creeper", "dru"],
[242, "Hunger", "Hunger", "dru"],
[243, "Shock Wave", "Shock Wave", "dru"],
[244, "Volcano", "Volcano", "dru"],
[245, "Tornado", "Tornado", "dru"],
[246, "Spirit of Barbs", "Spirit of Barbs", "dru"],
[247, "Summon Grizzly", "Summon Grizzly", "dru"],
[248, "Fury", "Fury", "dru"],
[249, "Armageddon", "Armageddon", "dru"],
[250, "Hurricane", "Hurricane", "dru"],
[251, "Fire Trauma", "Fire Blast", "ass"],
[252, "Claw Mastery", "Claw Mastery", "ass"],
[253, "Psychic Hammer", "Psychic Hammer", "ass"],
[254, "Tiger Strike", "Tiger Strike", "ass"],
[255, "Dragon Talon", "Dragon Talon", "ass"],
[256, "Shock Field", "Shock Web", "ass"],
[257, "Blade Sentinel", "Blade Sentinel", "ass"],
[258, "Quickness", "Burst of Speed", "ass"],
[259, "Fists of Fire", "Fists of Fire", "ass"],
[260, "Dragon Claw", "Dragon Claw", "ass"],
[261, "Charged Bolt Sentry", "Charged Bolt Sentry", "ass"],
[262, "Wake of Fire Sentry", "Wake of Fire", "ass"],
[263, "Weapon Block", "Weapon Block", "ass"],
[264, "Cloak of Shadows", "Cloak of Shadows", "ass"],
[265, "Cobra Strike", "Cobra Strike", "ass"],
[266, "Blade Fury", "Blade Fury", "ass"],
[267, "Fade", "Fade", "ass"],
[268, "Shadow Warrior", "Shadow Warrior", "ass"],
[269, "Claws of Thunder", "Claws of Thunder", "ass"],
[270, "Dragon Tail", "Dragon Tail", "ass"],
[271, "Lightning Sentry", "Lightning Sentry", "ass"],
[272, "Inferno Sentry", "Wake of Inferno", "ass"],
[273, "Mind Blast", "Mind Blast", "ass"],
[274, "Blades of Ice", "Blades of Ice", "ass"],
[275, "Dragon Flight", "Dragon Flight", "ass"],
[276, "Death Sentry", "Death Sentry", "ass"],
[277, "Blade Shield", "Blade Shield", "ass"],
[278, "Venom", "Venom", "ass"],
[279, "Shadow Master", "Shadow Master", "ass"],
[280, "Royal Strike", "Phoenix Strike", "ass"],
[350, "Delerium Change", "Delirium", ""],
]
const CLASS_ONLY_LABELS: Record<string, string> = {
ama: '(Amazon Only)',
sor: '(Sorceress Only)',
nec: '(Necromancer Only)',
pal: '(Paladin Only)',
bar: '(Barbarian Only)',
dru: '(Druid Only)',
ass: '(Assassin Only)',
}
const SKILL_LOOKUP_BY_ID = new Map<number, { name: string; classOnly?: string }>()
const SKILL_LOOKUP_BY_NAME = new Map<string, { name: string; classOnly?: string }>()
for (const [id, rawName, displayName, cls] of CANONICAL_SKILLS) {
const classOnly = cls ? CLASS_ONLY_LABELS[cls] : undefined
const info = classOnly ? { name: displayName, classOnly } : { name: displayName }
SKILL_LOOKUP_BY_ID.set(id, info)
SKILL_LOOKUP_BY_NAME.set(rawName.toLowerCase(), info)
SKILL_LOOKUP_BY_NAME.set(displayName.toLowerCase(), info)
}
/**
* Resolves a skill parameter (numeric ID or internal name from Skills.txt)
* into its 1.13c canonical English display name and optional class restriction.
*/
export function resolveSkillInfo(par: string | number | undefined): { name: string; classOnly?: string } {
if (par === undefined || par === '') return { name: 'Skill' }
const str = String(par).trim()
const num = Number(str)
if (!Number.isNaN(num) && SKILL_LOOKUP_BY_ID.has(num)) {
return SKILL_LOOKUP_BY_ID.get(num)!
}
const byName = SKILL_LOOKUP_BY_NAME.get(str.toLowerCase())
if (byName) return byName
return { name: str }
}
/**
* Canonical 1.13c skilltab mapping (par 0..20 from CharStats.txt StrSkillTab1..3 + StrClassOnly).
*/
export const CANONICAL_SKILL_TABS: Record<number, string> = {
0: 'Bow and Crossbow Skills (Amazon Only)',
1: 'Passive and Magic Skills (Amazon Only)',
2: 'Javelin and Spear Skills (Amazon Only)',
3: 'Fire Skills (Sorceress Only)',
4: 'Lightning Skills (Sorceress Only)',
5: 'Cold Skills (Sorceress Only)',
6: 'Curses (Necromancer Only)',
7: 'Poison and Bone Skills (Necromancer Only)',
8: 'Summoning Skills (Necromancer Only)',
9: 'Combat Skills (Paladin Only)',
10: 'Offensive Auras (Paladin Only)',
11: 'Defensive Auras (Paladin Only)',
12: 'Combat Skills (Barbarian Only)',
13: 'Masteries (Barbarian Only)',
14: 'Warcries (Barbarian Only)',
15: 'Summoning Skills (Druid Only)',
16: 'Shape Shifting Skills (Druid Only)',
17: 'Elemental Skills (Druid Only)',
18: 'Traps (Assassin Only)',
19: 'Shadow Disciplines (Assassin Only)',
20: 'Martial Arts (Assassin Only)',
}
/**
* Canonical 1.13c MonStats.txt hcIdx -> NameStr for reanimate property.
*/
export const CANONICAL_REANIMATE_MONSTERS: Record<number, string> = {
0: 'Skeleton',
1: 'Returned',
2: 'Bone Warrior',
3: 'Burning Dead',
4: 'Horror',
5: 'Zombie',
}

File diff suppressed because it is too large Load Diff

View File

@ -82,10 +82,26 @@ describe('Issue #122: Equipment & Affix Chinese Localization (EN / ZH / Bilingua
expect(translateSkillName('Poison Creeper')).toBe('剧毒蔓藤')
})
it('translates rare and magic names correctly', () => {
it('translates rare and magic names correctly with proper Chinese word order', () => {
expect(translateRareName('Beast Edge')).toBe('野兽之刃')
expect(translateRareName('Shadow Brow')).toBe('阴影之额头')
// Prefix only
expect(translateMagicName('Sturdy Leather Armor', 'Leather Armor')).toBe('结实之皮甲')
// Suffix only: modifier before noun
expect(translateMagicName('War Spear of Swiftness', 'War Spear')).toBe('疾速之巨战长矛')
// Both prefix & suffix: modifiers before noun
expect(translateMagicName('Savage Short Sword of the Giant', 'Short Sword')).toBe('野蛮之巨人之短剑')
// 100% Affix Coverage test for previously missing items
expect(translateMagicName("Harpoonist's Ceremonial Javelin", 'Ceremonial Javelin')).toBe('标枪之祭典标枪')
expect(translateMagicName('Trump Edge Bow', 'Edge Bow')).toBe('胜者之锋锐之弓')
expect(translateMagicName('Gritty Broad Axe', 'Broad Axe')).toBe('坚韧之阔斧')
})
it('translates item names to normalized Simplified Chinese', () => {
expect(translateItemName('Flawless Ruby')).toBe('无瑕疵的红宝石')
expect(translateItemName('Flawless Emerald')).toBe('无瑕疵的绿宝石')
expect(translateItemName('The Humongous')).toBe('巨大无比')
expect(translateItemName('Steelclash')).toBe('作响的金属')
})
})
@ -93,8 +109,8 @@ describe('Issue #122: Equipment & Affix Chinese Localization (EN / ZH / Bilingua
it('formats skills in Chinese', () => {
expect(formatPropertyCodeI18n('allskills', 2, undefined, undefined, undefined, 'zh')).toBe('+2 所有技能')
expect(formatPropertyCodeI18n('ama', 1, undefined, undefined, undefined, 'zh')).toBe('+1 亚马逊技能等级')
expect(formatPropertyCodeI18n('skilltab', 2, undefined, undefined, 3, 'zh')).toBe('+2 火焰技能 (限法师使用)')
expect(formatPropertyCodeI18n('skill', 3, undefined, undefined, 'Teleport', 'zh')).toBe('+3 传送')
expect(formatPropertyCodeI18n('skill', 3, undefined, undefined, 'Teleport', 'zh')).toBe('+3 传送 (限法师使用)')
expect(formatPropertyCodeI18n('oskill', 1, undefined, undefined, 'Teleport', 'zh')).toBe('+1 传送')
})
it('formats attributes & life/mana in Chinese', () => {
@ -117,6 +133,17 @@ describe('Issue #122: Equipment & Affix Chinese Localization (EN / ZH / Bilingua
expect(formatPropertyCodeI18n('hp/lvl', 12, undefined, undefined, undefined, 'zh')).toBe('+1.5 生命 (依角色等级提升)')
expect(formatPropertyCodeI18n('rep-dur', 0, 0, undefined, 33, 'zh')).toBe('每 3 秒恢复 1 点耐久度')
})
it('handles charged spell properties with MPQ negative encoding', () => {
// In 1.13c MPQ, charges and level are stored as negative numbers e.g. min: -20, max: -3
expect(formatPropertyCodeI18n('charged', -20, -20, -3, 'Enchant', 'zh')).toBe('等级 3 强化 (20/20 次充能)')
expect(formatPropertyCodeI18n('charged', -20, -20, -3, 'Enchant', 'en')).toBe('Level 3 Enchant (20/20 Charges)')
})
it('handles asterisk-prefixed properties like *hp from UniqueItems.txt', () => {
expect(formatPropertyCodeI18n('*hp', -10, -10, -10, undefined, 'zh')).toBe('-10 生命')
expect(formatPropertyCodeI18n('*hp', -10, -10, -10, undefined, 'en')).toBe('-10 to Life')
})
})
describe('4. formatItemTooltip: EN, ZH, and Bilingual Modes', () => {