273 lines
11 KiB
TypeScript
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(' / ')
|
|
}
|
|
})
|
|
})
|
|
})
|