diablo2-web/tests/issue-480-tbl-item-parity.t...

273 lines
11 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest'
import {
CANONICAL_ENG_TBL_DICT,
CANONICAL_CHI_TBL_DICT,
CANONICAL_ISC_DESC_TABLE,
} from '../src/data/canonical-tbl-strings.ts'
import {
resolveAffixName,
resolveRareName,
resolveGemName,
resolveLowQualityName,
getEmbeddedItemNameChains,
lookupCanonicalTbl,
} from '../src/game/item-names.ts'
import {
resolveBaseItemNameEn,
resolveRareNameEn,
resolveMagicNameEn,
translateItemName,
translateRareName,
translateMagicName,
setTooltipLocale,
} from '../src/game/tooltip-i18n.ts'
import {
formatItemTooltip,
formatStatByIscDescFunc,
} from '../src/game/item-tooltip.ts'
import { createDroppedItem } from '../src/game/drop-pipeline.ts'
import { ItemQuality, type ItemBase } from '../src/game/items.ts'
import type { GroundItemEntity } from '../src/game/ground-items.ts'
import { formatGroundItemLabelText } from '../src/ui/ground-labels.ts'
import { STARTER_EQUIPPED_GEAR } from '../src/ui/inventory.ts'
import {
CLASS_EXCLUSIVE_SETS,
STARTER_STASH_ITEMS,
} from '../src/game/class-starter-profiles.ts'
describe('Issue #480: Canonical 1.13c .tbl & ItemStatCost.txt Parity across Inventory, Stash, and Ground', () => {
beforeEach(() => {
setTooltipLocale('en')
})
describe('1. Canonical .tbl String Dictionaries & ItemStatCost.txt Table', () => {
it('packs authentic 1.13c ENG and CHI .tbl dictionaries and ISC descfunc metadata', () => {
expect(Object.keys(CANONICAL_ENG_TBL_DICT).length).toBeGreaterThan(2000)
expect(Object.keys(CANONICAL_CHI_TBL_DICT).length).toBeGreaterThan(1000)
expect(Object.keys(CANONICAL_ISC_DESC_TABLE).length).toBeGreaterThan(150)
// Base item code hp1 in Misc.txt has internal name "Lesser Healing Potion",
// but string.tbl maps "hp1" -> "Minor Healing Potion" / "輕微治療葯劑"
expect(lookupCanonicalTbl('hp1', 'en')).toBe('Minor Healing Potion')
expect(lookupCanonicalTbl('hp1', 'zh')).toBe('輕微治療葯劑')
// RarePrefix.txt / RareSuffix.txt internal keys with RI or lowercase
expect(lookupCanonicalTbl('GhoulRI', 'en')).toBe('Ghoul')
expect(lookupCanonicalTbl('barRI', 'en')).toBe('Bar')
expect(lookupCanonicalTbl('bite', 'en')).toBe('Bite')
// MagicPrefix.txt / MagicSuffix.txt internal keys vs .tbl display strings
expect(lookupCanonicalTbl("Mechanist's", 'en')).toBe("Mechanic's")
expect(lookupCanonicalTbl('of the Colosuss', 'en')).toBe('of the Colossus')
expect(lookupCanonicalTbl('of Warmth', 'en')).toBe('of Thawing')
})
it('resolves affix, rare, gem, and low-quality names through .tbl in item-names.ts', () => {
const chains = getEmbeddedItemNameChains()
expect(resolveAffixName({ name: "Mechanist's" }, chains)).toBe("Mechanic's")
expect(resolveRareName('GhoulRI', chains)).toBe('食屍鬼之')
expect(resolveRareName('barRI', chains)).toBe('酒吧')
expect(resolveGemName({ name: 'Perfect Amethyst', code: 'gpv' }, chains)).toBe('完美的紫寶石')
expect(resolveLowQualityName('Low Quality', chains)).toBe('劣質的')
})
it('resolves English and Chinese base, rare, and magic names via .tbl in tooltip-i18n.ts', () => {
expect(resolveBaseItemNameEn('Lesser Healing Potion', 'hp1')).toBe('Minor Healing Potion')
expect(translateItemName('Lesser Healing Potion')).toBe('轻微治疗药剂')
expect(resolveRareNameEn('GhoulRI barRI', 'GhoulRI', 'barRI')).toBe('Ghoul Bar')
expect(resolveRareNameEn('Storm bite', 'Storm', 'bite')).toBe('Storm Bite')
expect(translateRareName('GhoulRI barRI')).toBe('食尸鬼之酒吧')
expect(resolveMagicNameEn("Mechanist's Cap of the Colosuss", 'Cap', "Mechanist's", 'of the Colosuss', 'cap')).toBe(
"Mechanic's Cap of the Colossus",
)
expect(translateMagicName("Mechanist's Cap of Warmth", 'Cap')).toBe('机械师之温暖之帽子')
})
})
describe('2. ItemStatCost.txt descfunc (1..28) & descval (0..2) Engine', () => {
it('formats discrete stats via CANONICAL_ISC_DESC_TABLE and .tbl keys', () => {
// strength: descFunc=1, descVal=1, descStrPos=ModStr1a ("to Strength" / "力量")
expect(formatStatByIscDescFunc('strength', 15, 15, 15, undefined, 'en')).toBe('+15 to Strength')
expect(formatStatByIscDescFunc('strength', 15, 15, 15, undefined, 'zh')).toBe('+15 力量')
// item_fastercastrate: descFunc=4, descVal=1, descStrPos=ModStr1c ("Faster Cast Rate" / "高速施展速度")
expect(formatStatByIscDescFunc('item_fastercastrate', 20, 20, 20, undefined, 'en')).toBe('+20% Faster Cast Rate')
expect(formatStatByIscDescFunc('item_fastercastrate', 20, 20, 20, undefined, 'zh')).toBe('+20% 高速施展速度')
// fireresist: descFunc=4, descVal=2, descStrPos=strModFireResist ("Fire Resist" / "抗火")
expect(formatStatByIscDescFunc('fireresist', 40, 40, 40, undefined, 'en')).toBe('Fire Resist +40%')
expect(formatStatByIscDescFunc('fireresist', 40, 40, 40, undefined, 'zh')).toBe('抗火 +40%')
// item_Cannotbefrozen: descFunc=3, descVal=0, descStrPos=ModStr2z ("Cannot Be Frozen" / "無法冰凍")
expect(formatStatByIscDescFunc('item_cannotbefrozen', 1, 1, 1, undefined, 'en')).toBe('Cannot Be Frozen')
expect(formatStatByIscDescFunc('item_cannotbefrozen', 1, 1, 1, undefined, 'zh')).toBe('無法冰凍')
// item_magicbonus: descFunc=7, descVal=1, descStrPos=ModStr1x ("Better Chance of Getting Magic Items")
expect(formatStatByIscDescFunc('item_magicbonus', 50, 50, 50, undefined, 'en')).toBe(
'50% Better Chance of Getting Magic Items',
)
})
})
describe('3. Ground Items (地面掉落物品): createDroppedItem & formatGroundItemLabelText', () => {
const hp1Base: ItemBase = {
id: 'hp1',
name: 'Lesser Healing Potion',
kind: 'misc',
invWidth: 1,
invHeight: 1,
maxStack: 1,
value: 25,
damage: 0,
defense: 0,
tags: ['hpot'],
level: 1,
}
const capBase: ItemBase = {
id: 'cap',
name: 'Cap',
kind: 'armor',
invWidth: 2,
invHeight: 2,
maxStack: 1,
value: 20,
damage: 0,
defense: 5,
tags: ['helm'],
level: 1,
}
it('resolves dropped item .name and .nameZh through .tbl and renders ground labels accurately', () => {
// hp1 has internal Misc.txt name "Lesser Healing Potion", must resolve to "Minor Healing Potion" / "轻微治疗药剂"
const hp1Drop = createDroppedItem(hp1Base, 'normal', { ilvl: 1, dwInitSeed: 480 })
expect(hp1Drop.name).toBe('Minor Healing Potion')
expect(hp1Drop.nameZh).toBe('轻微治疗药剂')
const makeGroundEntity = (partial: Partial<GroundItemEntity>): GroundItemEntity => ({
id: 'g-1',
item: hp1Drop,
name: hp1Drop.name,
nameZh: hp1Drop.nameZh ?? '',
quality: 'normal',
isGold: false,
amount: 1,
invWidth: 1,
invHeight: 1,
dropTime: 0,
x: 0,
y: 0,
cellX: 0,
cellY: 0,
sparklePhase: 0,
...partial,
})
expect(formatGroundItemLabelText(makeGroundEntity({}))).toBe('轻微治疗药剂')
// Ground item without pre-populated nameZh resolves via .tbl in formatGroundItemLabelText
expect(
formatGroundItemLabelText(
makeGroundEntity({
name: 'GhoulRI barRI',
nameZh: '',
quality: 'rare',
item: undefined,
}),
),
).toBe('食尸鬼之酒吧')
expect(
formatGroundItemLabelText(
makeGroundEntity({
name: "Mechanist's Cap of the Colosuss",
nameZh: '',
quality: 'magic',
item: undefined,
}),
),
).toBe('机械师之巨神之帽子')
})
it('formats tooltip titles for Rare and Magic items with internal .txt keys via .tbl', () => {
const capDrop = createDroppedItem(capBase, 'rare', { ilvl: 20, dwInitSeed: 480 })
const rareItem = {
...capDrop,
quality: ItemQuality.RARE,
rarity: 'rare' as const,
name: 'GhoulRI barRI',
rolledRareAffixes: {
name: 'GhoulRI barRI',
rarePrefix: { name: 'GhoulRI' },
rareSuffix: { name: 'barRI' },
affixes: [],
} as any,
}
const enTip = formatItemTooltip(rareItem, 'en')
expect(enTip.title).toBe('Ghoul Bar')
const zhTip = formatItemTooltip(rareItem, 'zh')
expect(zhTip.title).toBe('食尸鬼之酒吧')
})
})
describe('4. Inventory & Equipped Gear (背包与装备栏): 1-Stat-Per-Line .tbl Compliance', () => {
it('ensures STARTER_EQUIPPED_GEAR has no non-.tbl "/" merged multi-stat lines', () => {
for (const [slot, item] of Object.entries(STARTER_EQUIPPED_GEAR)) {
if (!item) continue
for (const stat of item.stats) {
// "高速跑步/行走" is the single stat FRW name in Chinese, not a multi-stat merge
const withoutFrw = stat.text.replace(/高速跑步\/行走/g, '')
expect(withoutFrw, `Slot ${slot} (${item.name}) has merged stat line: ${stat.text}`).not.toContain(' / ')
}
}
})
it('ensures all 7 classes in CLASS_EXCLUSIVE_SETS have 1-stat-per-line .tbl stats and setBonuses', () => {
for (const [cls, gear] of Object.entries(CLASS_EXCLUSIVE_SETS)) {
for (const [slot, item] of Object.entries(gear)) {
if (!item) continue
for (const stat of item.stats) {
const withoutFrw = stat.text.replace(/高速跑步\/行走/g, '')
expect(withoutFrw, `${cls}.${slot} (${item.name}) has merged stat line: ${stat.text}`).not.toContain(' / ')
}
for (const bonus of item.setBonuses ?? []) {
expect(bonus.text, `${cls}.${slot} (${item.name}) has merged setBonus: ${bonus.text}`).not.toContain(' / ')
}
}
}
})
})
describe('5. Stash Items (储物箱): Canonical 4-Slot Socketable .tbl Format & Rare Jewel Parity', () => {
it('ensures all 24 socketables (12 Runes + 12 Gems/Skulls) in STARTER_STASH_ITEMS have canonical 5-line .tbl descriptions', () => {
const socketables = STARTER_STASH_ITEMS.filter(
(gp) => gp.item.id.startsWith('stash-r-') || gp.item.id.startsWith('stash-gem-'),
)
expect(socketables.length).toBe(24)
for (const gp of socketables) {
const lines = gp.item.stats.map((s) => s.text)
expect(lines.length, `${gp.item.id} (${gp.item.name}) should have 5 canonical socketable lines`).toBe(5)
expect(lines[0]).toBe('可镶嵌进有凹槽的装备')
expect(lines[1]).toMatch(/^武器: /)
expect(lines[2]).toMatch(/^防具: /)
expect(lines[3]).toMatch(/^头盔: /)
expect(lines[4]).toMatch(/^盾牌: /)
}
})
it('ensures stash-jew-rare (Doom Gyre) splits Strength and Dexterity into separate .tbl lines', () => {
const rareJewel = STARTER_STASH_ITEMS.find((gp) => gp.item.id === 'stash-jew-rare')
expect(rareJewel).toBeDefined()
const texts = rareJewel!.item.stats.map((s) => s.text)
expect(texts).toContain('+9 力量')
expect(texts).toContain('+9 敏捷')
for (const t of texts) {
expect(t).not.toContain(' / ')
}
})
})
})