feat(i18n): 装备与词条中英文多语言切换支持 EN/ZH/双语 (fixes #122)
This commit is contained in:
parent
ea6913950e
commit
7f7b769840
73
boss.html
73
boss.html
|
|
@ -526,6 +526,65 @@
|
|||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Showcase Toolbar & Language Switcher (Issue #122) */
|
||||
.showcase-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #0d0906;
|
||||
border: 1px solid #2a1c11;
|
||||
border-radius: 6px;
|
||||
padding: 8px 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
.toolbar-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--gold-bright);
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.lang-switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.lang-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-right: 4px;
|
||||
}
|
||||
.lang-pill {
|
||||
background: #18110b;
|
||||
border: 1px solid #3d2a1b;
|
||||
color: #aa9c8d;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.lang-pill:hover {
|
||||
background: #24170e;
|
||||
border-color: var(--border-gold);
|
||||
color: var(--gold-bright);
|
||||
}
|
||||
.lang-pill.active {
|
||||
background: linear-gradient(180deg, #44260d 0%, #2e1806 100%);
|
||||
border-color: #ff9800;
|
||||
color: #ffe082;
|
||||
box-shadow: 0 0 8px rgba(255, 152, 0, 0.35);
|
||||
}
|
||||
|
||||
/* Diablo II Authentic Tooltip / Item Card */
|
||||
.d2-item-card {
|
||||
background: #080604;
|
||||
|
|
@ -822,6 +881,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Showcase Toolbar / Language Switcher -->
|
||||
<div class="showcase-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="toolbar-icon">💎</span>
|
||||
<span class="toolbar-title">战利品展示面板</span>
|
||||
</div>
|
||||
<div class="lang-switch-row">
|
||||
<span class="lang-label">装备语言:</span>
|
||||
<button type="button" class="lang-pill active" data-lang="en">EN 原版</button>
|
||||
<button type="button" class="lang-pill" data-lang="zh">中文</button>
|
||||
<button type="button" class="lang-pill" data-lang="bilingual">EN / ZH 双语</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loot Grid -->
|
||||
<div class="loot-grid" id="loot-items-grid">
|
||||
<!-- Rendered by src/boss.ts -->
|
||||
|
|
|
|||
70
src/boss.ts
70
src/boss.ts
|
|
@ -13,6 +13,7 @@
|
|||
import { getEmbeddedDropTables } from './game/embedded-drop-tables.ts'
|
||||
import { executeDropPipeline, type DropTables } from './game/drop-pipeline.ts'
|
||||
import { formatItemTooltip, type FormattedItemTooltip } from './game/item-tooltip.ts'
|
||||
import { getTooltipLocale, setTooltipLocale, type TooltipLocale } from './game/tooltip-i18n.ts'
|
||||
import type { Item } from './game/items.ts'
|
||||
import { computeScaledNoDrop, computeEffectivePlayers } from './game/treasure-engine.ts'
|
||||
|
||||
|
|
@ -148,6 +149,7 @@ interface DropSessionRecord {
|
|||
}
|
||||
|
||||
const sessionHistory: DropSessionRecord[] = []
|
||||
let latestDroppedItems: Item[] = []
|
||||
let totalKills = 0
|
||||
let totalItemsDropped = 0
|
||||
let totalUniques = 0
|
||||
|
|
@ -171,6 +173,7 @@ export function initBossApp(): void {
|
|||
bindDifficultySelector()
|
||||
bindSettings()
|
||||
bindActionButtons()
|
||||
bindLanguageSelector()
|
||||
bindKeyboardShortcuts()
|
||||
|
||||
// 3. Initial UI update
|
||||
|
|
@ -369,11 +372,25 @@ function bindActionButtons(): void {
|
|||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
latestDroppedItems = []
|
||||
renderLootShowcase([])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function bindLanguageSelector(): void {
|
||||
const pills = document.querySelectorAll<HTMLButtonElement>('.lang-pill')
|
||||
pills.forEach(pill => {
|
||||
pill.addEventListener('click', () => {
|
||||
const lang = pill.dataset.lang as TooltipLocale | undefined
|
||||
if (!lang) return
|
||||
setTooltipLocale(lang)
|
||||
pills.forEach(p => p.classList.toggle('active', p === pill))
|
||||
renderLootShowcase(latestDroppedItems)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function bindKeyboardShortcuts(): void {
|
||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
// Don't trigger if typing in an input
|
||||
|
|
@ -494,15 +511,19 @@ function renderKillBanner(times: number, dropCount: number, durationMs: string):
|
|||
}
|
||||
|
||||
function renderLootShowcase(items: Item[]): void {
|
||||
latestDroppedItems = items
|
||||
const container = document.getElementById('loot-items-grid')
|
||||
if (!container) return
|
||||
|
||||
const locale = getTooltipLocale()
|
||||
|
||||
if (items.length === 0) {
|
||||
const isZh = locale === 'zh' || locale === 'bilingual'
|
||||
container.innerHTML = `
|
||||
<div class="empty-loot-placeholder">
|
||||
<div class="placeholder-icon">⚔️</div>
|
||||
<div class="placeholder-title">尚未击杀首领</div>
|
||||
<div class="placeholder-desc">点击左侧「模拟击杀 Boss」或按下 [空格键] 体验 1.13c 真实掉落与词缀属性生成</div>
|
||||
<div class="placeholder-title">${isZh ? '尚未击杀首领' : 'No Boss Slain Yet'}</div>
|
||||
<div class="placeholder-desc">${isZh ? '点击左侧「模拟击杀 Boss」或按下 [空格键] 体验 1.13c 真实掉落与词缀属性生成' : 'Click "Kill Boss" on the left sidebar or press [Space] to simulate authentic 1.13c drops & affixes.'}</div>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
|
|
@ -510,69 +531,80 @@ function renderLootShowcase(items: Item[]): void {
|
|||
|
||||
// Render Item Cards
|
||||
container.innerHTML = items.map((item, idx) => {
|
||||
const tooltip = formatItemTooltip(item)
|
||||
return renderSingleItemCard(tooltip, idx + 1)
|
||||
const tooltip = formatItemTooltip(item, locale)
|
||||
return renderSingleItemCard(tooltip, idx + 1, locale)
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderSingleItemCard(t: FormattedItemTooltip, index: number): string {
|
||||
function renderSingleItemCard(t: FormattedItemTooltip, index: number, locale: TooltipLocale = getTooltipLocale()): string {
|
||||
const linesHtml = t.lines.map(l => {
|
||||
const colorClass = `line-${l.color || 'blue'}`
|
||||
return `<div class="tooltip-stat-line ${colorClass}">${escapeHtml(l.text)}</div>`
|
||||
}).join('')
|
||||
|
||||
const isZh = locale === 'zh' || locale === 'bilingual'
|
||||
const lblDefense = isZh ? '防御: ' : 'Defense: '
|
||||
const lblOneHand = isZh ? '单手伤害: ' : 'One-Hand Damage: '
|
||||
const lblTwoHand = isZh ? '双手伤害: ' : 'Two-Hand Damage: '
|
||||
const lblDurability = isZh ? '耐久度: ' : 'Durability: '
|
||||
const lblReqLevel = isZh ? '需要等级: ' : 'Required Level: '
|
||||
const lblReqStr = isZh ? '需要力量: ' : 'Required Strength: '
|
||||
const lblReqDex = isZh ? '需要敏捷: ' : 'Required Dexterity: '
|
||||
const wordOf = isZh ? ' / ' : ' of '
|
||||
const wordTo = isZh ? ' 至 ' : ' to '
|
||||
|
||||
const baseStatsHtml: string[] = []
|
||||
if (t.defense !== undefined) {
|
||||
const defText =
|
||||
t.baseDefense !== undefined && t.baseDefense !== t.defense
|
||||
? `<span class="val-blue">${t.defense}</span> <span class="val-base">(${t.baseDefense})</span>`
|
||||
: `<span class="val-white">${t.defense}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item">Defense: ${defText}</div>`)
|
||||
baseStatsHtml.push(`<div class="base-stat-item">${lblDefense}${defText}</div>`)
|
||||
}
|
||||
if (t.oneHandDamage) {
|
||||
const d = t.oneHandDamage
|
||||
const dmgText =
|
||||
d.isEnhanced && d.baseMin !== undefined && d.baseMax !== undefined
|
||||
? `<span class="val-blue">${d.min} to ${d.max}</span> <span class="val-base">(${d.baseMin} to ${d.baseMax})</span>`
|
||||
: `<span class="val-white">${d.min} to ${d.max}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item">One-Hand Damage: ${dmgText}</div>`)
|
||||
? `<span class="val-blue">${d.min}${wordTo}${d.max}</span> <span class="val-base">(${d.baseMin}${wordTo}${d.baseMax})</span>`
|
||||
: `<span class="val-white">${d.min}${wordTo}${d.max}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item">${lblOneHand}${dmgText}</div>`)
|
||||
}
|
||||
if (t.twoHandDamage) {
|
||||
const d = t.twoHandDamage
|
||||
const dmgText =
|
||||
d.isEnhanced && d.baseMin !== undefined && d.baseMax !== undefined
|
||||
? `<span class="val-blue">${d.min} to ${d.max}</span> <span class="val-base">(${d.baseMin} to ${d.baseMax})</span>`
|
||||
: `<span class="val-white">${d.min} to ${d.max}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item">Two-Hand Damage: ${dmgText}</div>`)
|
||||
? `<span class="val-blue">${d.min}${wordTo}${d.max}</span> <span class="val-base">(${d.baseMin}${wordTo}${d.baseMax})</span>`
|
||||
: `<span class="val-white">${d.min}${wordTo}${d.max}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item">${lblTwoHand}${dmgText}</div>`)
|
||||
}
|
||||
if (t.durability) {
|
||||
const dur = t.durability
|
||||
const durText =
|
||||
dur.isEnhanced && dur.baseMax !== undefined
|
||||
? `<span class="val-blue">${dur.current} of ${dur.max}</span> <span class="val-base">(${dur.baseMax})</span>`
|
||||
: `<span class="val-white">${dur.current} of ${dur.max}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item">Durability: ${durText}</div>`)
|
||||
? `<span class="val-blue">${dur.current}${wordOf}${dur.max}</span> <span class="val-base">(${dur.baseMax})</span>`
|
||||
: `<span class="val-white">${dur.current}${wordOf}${dur.max}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item">${lblDurability}${durText}</div>`)
|
||||
}
|
||||
if (t.reqLevel !== undefined) {
|
||||
const lvlText =
|
||||
t.baseReqLevel !== undefined && t.baseReqLevel !== t.reqLevel
|
||||
? `<span class="val-blue">${t.reqLevel}</span> <span class="val-base">(${t.baseReqLevel})</span>`
|
||||
: `<span class="val-white">${t.reqLevel}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item req">Required Level: ${lvlText}</div>`)
|
||||
baseStatsHtml.push(`<div class="base-stat-item req">${lblReqLevel}${lvlText}</div>`)
|
||||
}
|
||||
if (t.reqStr !== undefined) {
|
||||
const strText =
|
||||
t.baseReqStr !== undefined && t.baseReqStr !== t.reqStr
|
||||
? `<span class="val-blue">${t.reqStr}</span> <span class="val-base">(${t.baseReqStr})</span>`
|
||||
: `<span class="val-white">${t.reqStr}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item req">Required Strength: ${strText}</div>`)
|
||||
baseStatsHtml.push(`<div class="base-stat-item req">${lblReqStr}${strText}</div>`)
|
||||
}
|
||||
if (t.reqDex !== undefined) {
|
||||
const dexText =
|
||||
t.baseReqDex !== undefined && t.baseReqDex !== t.reqDex
|
||||
? `<span class="val-blue">${t.reqDex}</span> <span class="val-base">(${t.baseReqDex})</span>`
|
||||
: `<span class="val-white">${t.reqDex}</span>`
|
||||
baseStatsHtml.push(`<div class="base-stat-item req">Required Dexterity: ${dexText}</div>`)
|
||||
baseStatsHtml.push(`<div class="base-stat-item req">${lblReqDex}${dexText}</div>`)
|
||||
}
|
||||
|
||||
return `
|
||||
|
|
@ -581,7 +613,7 @@ function renderSingleItemCard(t: FormattedItemTooltip, index: number): string {
|
|||
<div class="item-title" style="color: ${t.qualityColor};">${escapeHtml(t.title)}</div>
|
||||
${t.subTitle ? `<div class="item-subtitle">${escapeHtml(t.subTitle)}</div>` : ''}
|
||||
<div class="item-tags">
|
||||
<span class="item-quality-pill" style="border-color: ${t.qualityColor}; color: ${t.qualityColor};">${t.quality.toUpperCase()}</span>
|
||||
<span class="item-quality-pill" style="border-color: ${t.qualityColor}; color: ${t.qualityColor};">${escapeHtml(t.quality.toUpperCase())}</span>
|
||||
<span class="item-ilvl-pill">ilvl ${t.ilvl}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,23 @@ import {
|
|||
type ItemStatCostTable,
|
||||
type ItemStatCostRecord,
|
||||
} from './properties.ts'
|
||||
import {
|
||||
getTooltipLocale,
|
||||
setTooltipLocale,
|
||||
translateItemName,
|
||||
translateMagicName,
|
||||
translateRareName,
|
||||
formatPropertyCodeI18n,
|
||||
RUNE_INFO_ZH,
|
||||
type TooltipLocale,
|
||||
} from './tooltip-i18n.ts'
|
||||
|
||||
export {
|
||||
getTooltipLocale,
|
||||
setTooltipLocale,
|
||||
translateItemName,
|
||||
type TooltipLocale,
|
||||
} from './tooltip-i18n.ts'
|
||||
|
||||
export interface FormattedStatLine {
|
||||
text: string
|
||||
|
|
@ -439,7 +456,11 @@ export function formatPropertyCode(
|
|||
minOrMax?: number,
|
||||
maxOrPar?: number | string,
|
||||
maybePar?: string | number,
|
||||
locale?: TooltipLocale,
|
||||
): string {
|
||||
if (locale && locale !== 'en') {
|
||||
return formatPropertyCodeI18n(code, valueOrMin, minOrMax, maxOrPar, maybePar, locale)
|
||||
}
|
||||
let value: number
|
||||
let min: number
|
||||
let max: number
|
||||
|
|
@ -1965,7 +1986,7 @@ function getAffixEffectiveLevelReq(aff: RolledAffix, itemClass: string): number
|
|||
/**
|
||||
* Generates formatted tooltip structure for any dropped Diablo II Item.
|
||||
*/
|
||||
export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
||||
export function formatItemTooltip(item: Item, locale: TooltipLocale = getTooltipLocale()): FormattedItemTooltip {
|
||||
const base = item.base
|
||||
const quality = String(item.rarity || item.quality || 'normal').toLowerCase()
|
||||
const ilvl = item.level ?? item.ilvl ?? 1
|
||||
|
|
@ -1982,18 +2003,35 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
const runeData = isRune && runeKey ? RUNE_INFO[runeKey] : undefined
|
||||
|
||||
if (runeData) {
|
||||
title = runeData.nameEn
|
||||
subTitle = `Rune (${runeData.num})`
|
||||
qualityColor = '#ff8c00'
|
||||
lines.push({ text: 'Can be Inserted into Socketed Items', color: 'white' })
|
||||
lines.push({ text: `Weapons: ${runeData.weapon}`, color: 'blue' })
|
||||
lines.push({ text: `Armor/Helms/Shields: ${runeData.armor}`, color: 'blue' })
|
||||
const rZh = runeKey ? RUNE_INFO_ZH[runeKey] : undefined
|
||||
if (locale === 'zh') {
|
||||
title = (rZh ? rZh.nameZh : runeData.nameEn) + '符文'
|
||||
subTitle = `符文 (${runeData.num})`
|
||||
qualityColor = '#ff8c00'
|
||||
lines.push({ text: '可镶嵌进有凹槽的装备', color: 'white' })
|
||||
lines.push({ text: `武器: ${rZh ? rZh.weaponZh : runeData.weapon}`, color: 'blue' })
|
||||
lines.push({ text: `防具/头盔/盾牌: ${rZh ? rZh.armorZh : runeData.armor}`, color: 'blue' })
|
||||
} else if (locale === 'bilingual') {
|
||||
title = `${runeData.nameEn} (${rZh ? rZh.nameZh : ''})`
|
||||
subTitle = `Rune (${runeData.num})`
|
||||
qualityColor = '#ff8c00'
|
||||
lines.push({ text: '可镶嵌进有凹槽的装备', color: 'white' })
|
||||
lines.push({ text: `武器: ${rZh ? rZh.weaponZh : runeData.weapon}`, color: 'blue' })
|
||||
lines.push({ text: `防具/头盔/盾牌: ${rZh ? rZh.armorZh : runeData.armor}`, color: 'blue' })
|
||||
} else {
|
||||
title = runeData.nameEn
|
||||
subTitle = `Rune (${runeData.num})`
|
||||
qualityColor = '#ff8c00'
|
||||
lines.push({ text: 'Can be Inserted into Socketed Items', color: 'white' })
|
||||
lines.push({ text: `Weapons: ${runeData.weapon}`, color: 'blue' })
|
||||
lines.push({ text: `Armor/Helms/Shields: ${runeData.armor}`, color: 'blue' })
|
||||
}
|
||||
return {
|
||||
title,
|
||||
subTitle,
|
||||
quality: 'Rune',
|
||||
quality: locale === 'zh' ? '符文' : 'Rune',
|
||||
qualityColor,
|
||||
baseType: 'Rune',
|
||||
baseType: locale === 'zh' ? '符文' : 'Rune',
|
||||
ilvl,
|
||||
reqLevel: runeData.lvl > 0 ? runeData.lvl : undefined,
|
||||
lines,
|
||||
|
|
@ -2003,13 +2041,13 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
// Check if this is Gold
|
||||
if (base.id === 'gld' || (base.kind !== 'weapon' && base.kind !== 'armor' && base.name.trim().toLowerCase() === 'gold')) {
|
||||
const amount = (item.stats.amount ?? item.value ?? 100)
|
||||
title = `${amount.toLocaleString()} Gold`
|
||||
title = locale === 'zh' ? `${amount.toLocaleString()} 金币` : `${amount.toLocaleString()} Gold`
|
||||
qualityColor = '#ffd700'
|
||||
return {
|
||||
title,
|
||||
quality: 'Normal',
|
||||
quality: locale === 'zh' ? '普通' : 'Normal',
|
||||
qualityColor,
|
||||
baseType: 'Gold',
|
||||
baseType: locale === 'zh' ? '金币' : 'Gold',
|
||||
ilvl: 1,
|
||||
lines: [],
|
||||
}
|
||||
|
|
@ -2028,8 +2066,18 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
qualityColor = '#c8a15a'
|
||||
const uniqDef = item.uniqueItemDef as UniqueItem | undefined
|
||||
if (uniqDef) {
|
||||
title = uniqDef.index
|
||||
subTitle = base.name
|
||||
if (locale === 'zh') {
|
||||
title = translateItemName(uniqDef.index)
|
||||
subTitle = translateItemName(base.name)
|
||||
} else if (locale === 'bilingual') {
|
||||
const uZh = translateItemName(uniqDef.index)
|
||||
const bZh = translateItemName(base.name)
|
||||
title = `${uniqDef.index} (${uZh})`
|
||||
subTitle = `${base.name} (${bZh})`
|
||||
} else {
|
||||
title = uniqDef.index
|
||||
subTitle = base.name
|
||||
}
|
||||
if (uniqDef.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, uniqDef.levelreq)
|
||||
if (item.rolledProps && item.rolledProps.length > 0) {
|
||||
rawProps.push(...item.rolledProps)
|
||||
|
|
@ -2047,14 +2095,25 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
subTitle = base.name
|
||||
subTitle = locale === 'zh' ? translateItemName(base.name) : locale === 'bilingual' ? `${base.name} (${translateItemName(base.name)})` : base.name
|
||||
}
|
||||
} else if (quality === 'set') {
|
||||
qualityColor = '#00e600'
|
||||
const setDef = item.setItemDef as SetItem | undefined
|
||||
if (setDef) {
|
||||
title = setDef.index
|
||||
subTitle = `${base.name} · [${setDef.set}]`
|
||||
if (locale === 'zh') {
|
||||
title = translateItemName(setDef.index)
|
||||
subTitle = `${translateItemName(base.name)} · [${translateItemName(setDef.set)}]`
|
||||
} else if (locale === 'bilingual') {
|
||||
const sZh = translateItemName(setDef.index)
|
||||
const bZh = translateItemName(base.name)
|
||||
const setZh = translateItemName(setDef.set)
|
||||
title = `${setDef.index} (${sZh})`
|
||||
subTitle = `${base.name} (${bZh}) · [${setDef.set} (${setZh})]`
|
||||
} else {
|
||||
title = setDef.index
|
||||
subTitle = `${base.name} · [${setDef.set}]`
|
||||
}
|
||||
if (setDef.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, setDef.levelreq)
|
||||
if (item.rolledProps && item.rolledProps.length > 0) {
|
||||
rawProps.push(...item.rolledProps)
|
||||
|
|
@ -2072,19 +2131,28 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
subTitle = base.name
|
||||
subTitle = locale === 'zh' ? translateItemName(base.name) : locale === 'bilingual' ? `${base.name} (${translateItemName(base.name)})` : base.name
|
||||
}
|
||||
} else if (quality === 'rare') {
|
||||
qualityColor = '#ffff60'
|
||||
subTitle = base.name
|
||||
const rareDef = item.rolledRareAffixes as GeneratedRareAffixes | undefined
|
||||
if (rareDef) {
|
||||
if (rareDef.name) title = rareDef.name
|
||||
if (rareDef.affixes) {
|
||||
for (const aff of rareDef.affixes) {
|
||||
const effReq = getAffixEffectiveLevelReq(aff, itemClass)
|
||||
if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
|
||||
}
|
||||
const enName = rareDef?.name || item.name
|
||||
if (locale === 'zh') {
|
||||
title = translateRareName(enName)
|
||||
subTitle = translateItemName(base.name)
|
||||
} else if (locale === 'bilingual') {
|
||||
const rZh = translateRareName(enName)
|
||||
const bZh = translateItemName(base.name)
|
||||
title = `${enName} (${rZh})`
|
||||
subTitle = `${base.name} (${bZh})`
|
||||
} else {
|
||||
title = enName
|
||||
subTitle = base.name
|
||||
}
|
||||
if (rareDef && rareDef.affixes) {
|
||||
for (const aff of rareDef.affixes) {
|
||||
const effReq = getAffixEffectiveLevelReq(aff, itemClass)
|
||||
if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
|
||||
}
|
||||
}
|
||||
if (item.rolledProps && item.rolledProps.length > 0) {
|
||||
|
|
@ -2114,10 +2182,21 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
}
|
||||
} else if (quality === 'magic') {
|
||||
qualityColor = '#4880ff'
|
||||
subTitle = base.name
|
||||
const magicDef = item.rolledMagicAffixes as GeneratedAffixes | undefined
|
||||
const enName = magicDef?.name || item.name
|
||||
if (locale === 'zh') {
|
||||
title = translateMagicName(enName, base.name)
|
||||
subTitle = translateItemName(base.name)
|
||||
} else if (locale === 'bilingual') {
|
||||
const mZh = translateMagicName(enName, base.name)
|
||||
const bZh = translateItemName(base.name)
|
||||
title = `${enName} (${mZh})`
|
||||
subTitle = `${base.name} (${bZh})`
|
||||
} else {
|
||||
title = enName
|
||||
subTitle = base.name
|
||||
}
|
||||
if (magicDef) {
|
||||
if (magicDef.name) title = magicDef.name
|
||||
if (magicDef.prefix) {
|
||||
const effReq = getAffixEffectiveLevelReq(magicDef.prefix, itemClass)
|
||||
if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
|
||||
|
|
@ -2172,7 +2251,14 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
}
|
||||
} else if (quality === 'superior') {
|
||||
qualityColor = '#ffffff'
|
||||
title = item.name && item.name.startsWith('Superior ') ? item.name : `Superior ${base.name}`
|
||||
const enName = item.name && item.name.startsWith('Superior ') ? item.name : `Superior ${base.name}`
|
||||
if (locale === 'zh') {
|
||||
title = translateItemName(enName)
|
||||
} else if (locale === 'bilingual') {
|
||||
title = `${enName} (${translateItemName(enName)})`
|
||||
} else {
|
||||
title = enName
|
||||
}
|
||||
if (item.rolledProps && item.rolledProps.length > 0) {
|
||||
rawProps.push(...item.rolledProps)
|
||||
} else if (base.kind === 'weapon') {
|
||||
|
|
@ -2182,13 +2268,27 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
}
|
||||
} else if (quality === 'low') {
|
||||
qualityColor = '#a0a0a0'
|
||||
title = item.name && item.name !== base.name ? item.name : `Cracked ${base.name}`
|
||||
const enName = item.name && item.name !== base.name ? item.name : `Cracked ${base.name}`
|
||||
if (locale === 'zh') {
|
||||
title = translateItemName(enName)
|
||||
} else if (locale === 'bilingual') {
|
||||
title = `${enName} (${translateItemName(enName)})`
|
||||
} else {
|
||||
title = enName
|
||||
}
|
||||
if (item.rolledProps && item.rolledProps.length > 0) {
|
||||
rawProps.push(...item.rolledProps)
|
||||
}
|
||||
} else {
|
||||
qualityColor = '#e0d5c1'
|
||||
title = base.name
|
||||
const enName = base.name
|
||||
if (locale === 'zh') {
|
||||
title = translateItemName(enName)
|
||||
} else if (locale === 'bilingual') {
|
||||
title = `${enName} (${translateItemName(enName)})`
|
||||
} else {
|
||||
title = enName
|
||||
}
|
||||
if (item.rolledProps && item.rolledProps.length > 0) {
|
||||
rawProps.push(...item.rolledProps)
|
||||
}
|
||||
|
|
@ -2198,7 +2298,7 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
const mergedProps = aggregateAndSortProperties(rawProps)
|
||||
for (const prop of mergedProps) {
|
||||
lines.push({
|
||||
text: formatPropertyCode(prop.code, prop.value, prop.min, prop.max, prop.param),
|
||||
text: formatPropertyCode(prop.code, prop.value, prop.min, prop.max, prop.param, locale),
|
||||
color: 'blue',
|
||||
isMagic: true,
|
||||
})
|
||||
|
|
@ -2243,11 +2343,13 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
|
||||
const explicitSockets = Number((item as any).sockets || item.totalSockets || rolledSockets || 0)
|
||||
|
||||
if (isEthereal && !lines.some(l => l.text.includes('Ethereal (Cannot be Repaired)'))) {
|
||||
lines.push({ text: 'Ethereal (Cannot be Repaired)', color: 'blue', isMagic: true })
|
||||
if (isEthereal && !lines.some(l => l.text.includes('Ethereal') || l.text.includes('无形'))) {
|
||||
const ethText = locale === 'zh' || locale === 'bilingual' ? '无形 (无法修复)' : 'Ethereal (Cannot be Repaired)'
|
||||
lines.push({ text: ethText, color: 'blue', isMagic: true })
|
||||
}
|
||||
if (explicitSockets > 0 && !lines.some(l => l.text.includes('Socketed ('))) {
|
||||
lines.push({ text: `Socketed (${explicitSockets})`, color: 'blue', isMagic: true })
|
||||
if (explicitSockets > 0 && !lines.some(l => l.text.includes('Socketed') || l.text.includes('凹槽'))) {
|
||||
const sockText = locale === 'zh' || locale === 'bilingual' ? `凹槽 (${explicitSockets})` : `Socketed (${explicitSockets})`
|
||||
lines.push({ text: sockText, color: 'blue', isMagic: true })
|
||||
}
|
||||
|
||||
// 1. Defense (Enhanced / Low Quality / Ethereal vs Base)
|
||||
|
|
@ -2365,13 +2467,13 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
|
|||
const sockets = explicitSockets > 0 ? explicitSockets : (base as any).gemsockets > 0 ? (base as any).gemsockets : undefined
|
||||
|
||||
const displayQuality =
|
||||
quality === 'unique' ? 'Unique' :
|
||||
quality === 'set' ? 'Set' :
|
||||
quality === 'rare' ? 'Rare' :
|
||||
quality === 'magic' ? 'Magic' :
|
||||
quality === 'superior' ? 'Superior' :
|
||||
quality === 'low' ? 'Low Quality' :
|
||||
isRune ? 'Rune' : 'Normal'
|
||||
quality === 'unique' ? (locale === 'zh' ? '暗金' : 'Unique') :
|
||||
quality === 'set' ? (locale === 'zh' ? '套装' : 'Set') :
|
||||
quality === 'rare' ? (locale === 'zh' ? '亮金' : 'Rare') :
|
||||
quality === 'magic' ? (locale === 'zh' ? '魔法' : 'Magic') :
|
||||
quality === 'superior' ? (locale === 'zh' ? '超强' : 'Superior') :
|
||||
quality === 'low' ? (locale === 'zh' ? '劣质' : 'Low Quality') :
|
||||
isRune ? (locale === 'zh' ? '符文' : 'Rune') : (locale === 'zh' ? '普通' : 'Normal')
|
||||
|
||||
return {
|
||||
title,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,208 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import {
|
||||
formatItemTooltip,
|
||||
formatPropertyCode,
|
||||
getTooltipLocale,
|
||||
setTooltipLocale,
|
||||
translateItemName,
|
||||
} from '../src/game/item-tooltip.ts'
|
||||
import {
|
||||
translateSkillName,
|
||||
translateRareName,
|
||||
translateMagicName,
|
||||
formatPropertyCodeI18n,
|
||||
ITEM_NAMES_ZH,
|
||||
SKILL_NAMES_ZH,
|
||||
CANONICAL_SKILL_TABS_ZH,
|
||||
RUNE_INFO_ZH,
|
||||
} from '../src/game/tooltip-i18n.ts'
|
||||
import type { Item } from '../src/game/items.ts'
|
||||
|
||||
describe('Issue #122: Equipment & Affix Chinese Localization (EN / ZH / Bilingual)', () => {
|
||||
beforeEach(() => {
|
||||
// Ensure default is always English
|
||||
setTooltipLocale('en')
|
||||
})
|
||||
|
||||
describe('1. Locale State & Switching', () => {
|
||||
it('defaults to en (1.13c English original)', () => {
|
||||
expect(getTooltipLocale()).toBe('en')
|
||||
})
|
||||
|
||||
it('can switch between en, zh, and bilingual', () => {
|
||||
setTooltipLocale('zh')
|
||||
expect(getTooltipLocale()).toBe('zh')
|
||||
setTooltipLocale('bilingual')
|
||||
expect(getTooltipLocale()).toBe('bilingual')
|
||||
setTooltipLocale('en')
|
||||
expect(getTooltipLocale()).toBe('en')
|
||||
})
|
||||
})
|
||||
|
||||
describe('2. Item & Skill Name Dictionaries', () => {
|
||||
it('translates base equipment names correctly', () => {
|
||||
expect(translateItemName('Cap')).toBe('帽子')
|
||||
expect(translateItemName('Shako')).toBe('军帽')
|
||||
expect(translateItemName('Archon Plate')).toBe('执政官铠甲')
|
||||
expect(translateItemName('Colossus Blade')).toBe('巨神之刃')
|
||||
expect(translateItemName('Ring')).toBe('戒指')
|
||||
expect(translateItemName('Amulet')).toBe('项链')
|
||||
})
|
||||
|
||||
it('translates unique item names correctly', () => {
|
||||
expect(translateItemName('The Stone of Jordan')).toBe('乔丹之石')
|
||||
expect(translateItemName('Harlequin Crest')).toBe('谐角之冠')
|
||||
expect(translateItemName('Stormshield')).toBe('暴风之盾')
|
||||
expect(translateItemName('The Oculus')).toBe('眼球')
|
||||
})
|
||||
|
||||
it('translates set item names and set names correctly', () => {
|
||||
expect(translateItemName("Tal Rasha's Wrappings")).toBe('塔·拉夏的外袍')
|
||||
expect(translateItemName("Tal Rasha's Guardianship")).toBe('塔·拉夏的守护')
|
||||
expect(translateItemName("Immortal King's Soul Cage")).toBe('不朽之王的灵魂牢笼')
|
||||
expect(translateItemName("Griswold's Legacy")).toBe('格瑞斯华尔德的传奇')
|
||||
})
|
||||
|
||||
it('translates superior and low quality prefixes', () => {
|
||||
expect(translateItemName('Superior Cap')).toBe('超强的帽子')
|
||||
expect(translateItemName('Cracked Ring Mail')).toBe('劣质的锁环甲')
|
||||
expect(translateItemName('Crude Short Sword')).toBe('粗糙的短剑')
|
||||
expect(translateItemName('Low Quality Breast Plate')).toBe('低品质的胸甲')
|
||||
})
|
||||
|
||||
it('translates classic and LoD skills correctly', () => {
|
||||
expect(translateSkillName('Teleport')).toBe('传送')
|
||||
expect(translateSkillName('Fire Ball')).toBe('火球')
|
||||
expect(translateSkillName('Blizzard')).toBe('暴风雪')
|
||||
expect(translateSkillName('Whirlwind')).toBe('旋风')
|
||||
expect(translateSkillName('Blessed Hammer')).toBe('祝福之锤')
|
||||
// Expansion Druid & Assassin skills
|
||||
expect(translateSkillName('Hurricane')).toBe('飓风')
|
||||
expect(translateSkillName('Phoenix Strike')).toBe('凤凰击')
|
||||
expect(translateSkillName('Poison Creeper')).toBe('剧毒蔓藤')
|
||||
})
|
||||
|
||||
it('translates rare and magic names correctly', () => {
|
||||
expect(translateRareName('Beast Edge')).toBe('野兽之刃')
|
||||
expect(translateRareName('Shadow Brow')).toBe('阴影之额头')
|
||||
expect(translateMagicName('Sturdy Leather Armor', 'Leather Armor')).toBe('结实之皮甲')
|
||||
})
|
||||
})
|
||||
|
||||
describe('3. Property Code Formatting in Chinese vs English', () => {
|
||||
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 传送')
|
||||
})
|
||||
|
||||
it('formats attributes & life/mana in Chinese', () => {
|
||||
expect(formatPropertyCodeI18n('str', 20, undefined, undefined, undefined, 'zh')).toBe('+20 力量')
|
||||
expect(formatPropertyCodeI18n('dex', 15, undefined, undefined, undefined, 'zh')).toBe('+15 敏捷')
|
||||
expect(formatPropertyCodeI18n('all-stats', 10, undefined, undefined, undefined, 'zh')).toBe('+10 所有属性')
|
||||
expect(formatPropertyCodeI18n('hp', 50, undefined, undefined, undefined, 'zh')).toBe('+50 生命')
|
||||
expect(formatPropertyCodeI18n('mana', 40, undefined, undefined, undefined, 'zh')).toBe('+40 法力')
|
||||
})
|
||||
|
||||
it('formats combat speeds and resistances in Chinese', () => {
|
||||
expect(formatPropertyCodeI18n('fcr', 20, undefined, undefined, undefined, 'zh')).toBe('+20% 快速施法速度')
|
||||
expect(formatPropertyCodeI18n('fhr', 30, undefined, undefined, undefined, 'zh')).toBe('+30% 快速打击恢复')
|
||||
expect(formatPropertyCodeI18n('ias', 20, undefined, undefined, undefined, 'zh')).toBe('+20% 增加攻击速度')
|
||||
expect(formatPropertyCodeI18n('res-all', 15, undefined, undefined, undefined, 'zh')).toBe('所有抗性 +15')
|
||||
expect(formatPropertyCodeI18n('res-fire', 30, undefined, undefined, undefined, 'zh')).toBe('抗火 +30%')
|
||||
})
|
||||
|
||||
it('formats per-level scaling and repair rate in Chinese', () => {
|
||||
expect(formatPropertyCodeI18n('hp/lvl', 12, undefined, undefined, undefined, 'zh')).toBe('+1.5 生命 (依角色等级提升)')
|
||||
expect(formatPropertyCodeI18n('rep-dur', 0, 0, undefined, 33, 'zh')).toBe('每 3 秒恢复 1 点耐久度')
|
||||
})
|
||||
})
|
||||
|
||||
describe('4. formatItemTooltip: EN, ZH, and Bilingual Modes', () => {
|
||||
const sampleHarlequin = {
|
||||
id: 'item-shako-unique',
|
||||
name: 'Harlequin Crest',
|
||||
rarity: 'unique',
|
||||
level: 87,
|
||||
base: {
|
||||
id: 'uap',
|
||||
name: 'Shako',
|
||||
kind: 'armor',
|
||||
defense: 141,
|
||||
durability: 12,
|
||||
reqstr: 50,
|
||||
levelreq: 62,
|
||||
},
|
||||
stats: {},
|
||||
uniqueItemDef: {
|
||||
index: 'Harlequin Crest',
|
||||
level: 69,
|
||||
levelreq: 62,
|
||||
code: 'uap',
|
||||
rarity: 1,
|
||||
props: [
|
||||
{ code: 'allskills', min: 2, max: 2 },
|
||||
{ code: 'all-stats', min: 2, max: 2 },
|
||||
{ code: 'hp/lvl', min: 12, max: 12 },
|
||||
{ code: 'mana/lvl', min: 12, max: 12 },
|
||||
{ code: 'dmag', min: 10, max: 10 },
|
||||
{ code: 'mag%', min: 50, max: 50 },
|
||||
],
|
||||
},
|
||||
} as unknown as Item
|
||||
|
||||
it('formats in pure 1.13c English by default (locale === "en")', () => {
|
||||
const tt = formatItemTooltip(sampleHarlequin, 'en')
|
||||
expect(tt.title).toBe('Harlequin Crest')
|
||||
expect(tt.subTitle).toBe('Shako')
|
||||
expect(tt.quality).toBe('Unique')
|
||||
expect(tt.lines.some(l => l.text === '+2 to All Skills')).toBe(true)
|
||||
expect(tt.lines.some(l => l.text === '+2 to All Attributes')).toBe(true)
|
||||
expect(tt.lines.some(l => l.text === '50% Better Chance of Getting Magic Items')).toBe(true)
|
||||
})
|
||||
|
||||
it('formats in Simplified Chinese when locale === "zh"', () => {
|
||||
const tt = formatItemTooltip(sampleHarlequin, 'zh')
|
||||
expect(tt.title).toBe('谐角之冠')
|
||||
expect(tt.subTitle).toBe('军帽')
|
||||
expect(tt.quality).toBe('暗金')
|
||||
expect(tt.lines.some(l => l.text === '+2 所有技能')).toBe(true)
|
||||
expect(tt.lines.some(l => l.text === '+2 所有属性')).toBe(true)
|
||||
expect(tt.lines.some(l => l.text === '50% 获得魔法装备几率')).toBe(true)
|
||||
})
|
||||
|
||||
it('formats in Bilingual mode when locale === "bilingual"', () => {
|
||||
const tt = formatItemTooltip(sampleHarlequin, 'bilingual')
|
||||
expect(tt.title).toBe('Harlequin Crest (谐角之冠)')
|
||||
expect(tt.subTitle).toBe('Shako (军帽)')
|
||||
expect(tt.lines.some(l => l.text === '+2 所有技能')).toBe(true)
|
||||
expect(tt.lines.some(l => l.text === '+2 所有属性')).toBe(true)
|
||||
})
|
||||
|
||||
it('formats Runes correctly across all 3 modes', () => {
|
||||
const sampleRune = {
|
||||
id: 'item-rune-01',
|
||||
name: 'El Rune',
|
||||
code: 'r01',
|
||||
rarity: 'normal',
|
||||
base: { id: 'r01', name: 'El Rune', kind: 'misc' },
|
||||
stats: {},
|
||||
} as unknown as Item
|
||||
const ttEn = formatItemTooltip(sampleRune, 'en')
|
||||
expect(ttEn.title).toBe('El Rune')
|
||||
expect(ttEn.subTitle).toBe('Rune (1)')
|
||||
expect(ttEn.lines[0]?.text).toBe('Can be Inserted into Socketed Items')
|
||||
|
||||
const ttZh = formatItemTooltip(sampleRune, 'zh')
|
||||
expect(ttZh.title).toBe('艾尔符文')
|
||||
expect(ttZh.subTitle).toBe('符文 (1)')
|
||||
expect(ttZh.lines[0]?.text).toBe('可镶嵌进有凹槽的装备')
|
||||
|
||||
const ttBi = formatItemTooltip(sampleRune, 'bilingual')
|
||||
expect(ttBi.title).toBe('El Rune (艾尔)')
|
||||
expect(ttBi.subTitle).toBe('Rune (1)')
|
||||
expect(ttBi.lines[0]?.text).toBe('可镶嵌进有凹槽的装备')
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue