diablo2-web/tests/challenger-m2-level-names.t...

454 lines
24 KiB
TypeScript

/**
* tests/challenger-m2-level-names.test.ts
*
* Empirical Adversarial Challenger Verification Suite for Milestone M2 (Issue #147).
* Localized Map Area Names from .tbl and Levels.txt Parity.
*
* Adversarial Testing Spectrum:
* 1. Exhaustive 1..136 Level ID Audit:
* - Verify getLevelNames(id) returns non-empty nameZh and nameEn for all 1..136.
* - Verify 0 undefined, 0 null, 0 NaN, 0 raw hyphens, 0 raw underscores, 0 raw slugs.
* 2. 1.13c Ground Truth Alignment:
* - Explicitly verify Act 5 realignment (109..136) against Levels.txt / string.tbl / expansionstring.tbl:
* Specifically IDs 118, 119, 120, 121, 122, 123, 124, 131, 132.
* - Verify Act 1..4 key canonical areas (1, 2, 17, 33, 38, 40, 46, 74, 75, 83, 103, 108).
* 3. Multi-Format Slug Resolution:
* - Lowercase, uppercase, mixed-case, leading/trailing whitespace, numeric prefix (17-burial-grounds).
* - Underscore variants and normalized concatenated slugs.
* 4. Bizarre & Adversarial Inputs:
* - Zero (0), negative (-1, -100), out-of-range (137, 9999).
* - Floats (1.5), NaN, Infinity, -Infinity.
* - Empty strings, whitespace-only, punctuation ('---', '___', '!@#$%^&*()').
* - Ensure NEVER throws, NEVER crashes, NEVER returns blanks, gracefully falls back.
* 5. Slug Leak Prevention in sceneNameZh & sceneNameEn:
* - Ensure raw hyphenated fallbacks ('custom-dungeon-pack') are intercepted and converted to Title Case.
* 6. Dynamic .tbl Decoding & Global State Switch:
* - decodeLevelNamesFromTbl with synthetic .tbl, custom map overrides, setActiveLevelNames reset.
* 7. HUD Area Banner Integration:
* - WorldPanelsHud.showAreaBanner state retention and lack of double-prefixing.
*/
import { describe, expect, it } from 'vitest'
import {
getLevelNames,
levelIdOfSlug,
sceneNameZh,
sceneNameEn,
decodeLevelNamesFromTbl,
setActiveLevelNames,
getActiveLevelNames,
LEVEL_NAMES_ZH,
LEVEL_NAMES_EN,
CANONICAL_LEVEL_NAME_KEYS,
type LevelNames,
} from '../src/game/level-names-zh.ts'
import { WorldPanelsHud } from '../src/ui/world-panels.ts'
import { encodeTbl, type TblEntry } from '../scripts/lib/tbl-writer.ts'
import { decodeTbl } from '../src/formats/tbl.ts'
import type { D2Table } from '../src/game/acts.ts'
describe('Adversarial Challenger Suite: M2 Localized Area Names (#147)', () => {
// --------------------------------------------------------------------------
// 1. Exhaustive 1..136 Level ID Audit
// --------------------------------------------------------------------------
describe('1. Exhaustive 1..136 Level ID Audit', () => {
it('verifies getLevelNames(id) returns valid, non-empty nameZh and nameEn for all 1..136', () => {
expect(Object.keys(LEVEL_NAMES_ZH).length).toBe(136)
expect(Object.keys(CANONICAL_LEVEL_NAME_KEYS).length).toBe(136)
for (let id = 1; id <= 136; id++) {
const names: LevelNames = getLevelNames(id)
// Non-empty strings
expect(typeof names.nameZh).toBe('string')
expect(typeof names.nameEn).toBe('string')
expect(names.nameZh.trim().length, `Level ID ${id} nameZh is empty`).toBeGreaterThan(0)
expect(names.nameEn.trim().length, `Level ID ${id} nameEn is empty`).toBeGreaterThan(0)
// No forbidden sentinel substrings
for (const forbidden of ['undefined', 'null', 'NaN', 'Infinity', '[object Object]']) {
expect(names.nameZh).not.toContain(forbidden)
expect(names.nameEn).not.toContain(forbidden)
}
// No raw slugs or raw hyphens leaking into user display names
expect(names.nameZh, `Level ID ${id} nameZh contains raw hyphen`).not.toContain('-')
expect(names.nameEn, `Level ID ${id} nameEn contains raw hyphen`).not.toContain('-')
expect(names.nameZh, `Level ID ${id} nameZh contains raw underscore`).not.toContain('_')
expect(names.nameEn, `Level ID ${id} nameEn contains raw underscore`).not.toContain('_')
// Must match constant definitions
expect(names.nameZh).toBe(LEVEL_NAMES_ZH[id])
expect(names.nameEn).toBe(LEVEL_NAMES_EN[id])
expect(names.nameEn).toBe(CANONICAL_LEVEL_NAME_KEYS[id])
// Direct sceneNameZh & sceneNameEn wrappers
expect(sceneNameZh(id)).toBe(LEVEL_NAMES_ZH[id])
expect(sceneNameEn(id)).toBe(CANONICAL_LEVEL_NAME_KEYS[id])
}
})
})
// --------------------------------------------------------------------------
// 2. 1.13c Ground Truth Alignment Audit
// --------------------------------------------------------------------------
describe('2. Diablo II v1.13c Ground Truth Alignment', () => {
it('verifies critical Act 5 level IDs align with 1.13c Levels.txt and expansionstring.tbl', () => {
// Specifically required by Issue #147 and ground truth verification
const act5GroundTruth: Record<number, { nameZh: string; nameEn: string }> = {
109: { nameZh: '哈洛加斯', nameEn: 'Harrogath' },
110: { nameZh: '血腥丘陵', nameEn: 'Bloody Foothills' },
111: { nameZh: '冰冻高地', nameEn: 'Frigid Highlands' },
112: { nameZh: '亚瑞特高原', nameEn: 'Arreat Plateau' },
113: { nameZh: '水晶通道', nameEn: 'Crystalline Passage' },
114: { nameZh: '冰河', nameEn: 'Frozen River' },
115: { nameZh: '冰河路径', nameEn: 'Glacial Trail' },
116: { nameZh: '漂泊者洞穴', nameEn: 'Drifter Cavern' },
117: { nameZh: '冰冻苔原', nameEn: 'Frozen Tundra' },
118: { nameZh: '远古之路', nameEn: "The Ancients' Way" },
119: { nameZh: '冰窖', nameEn: 'Icy Cellar' },
120: { nameZh: '亚瑞特山顶', nameEn: 'Arreat Summit' },
121: { nameZh: '尼拉塞克的神殿', nameEn: "Nihlathak's Temple" },
122: { nameZh: '悲痛之厅', nameEn: 'Halls of Anguish' },
123: { nameZh: '痛苦之厅', nameEn: 'Halls of Pain' },
124: { nameZh: '沃德之厅', nameEn: 'Halls of Vaught' },
125: { nameZh: '艾巴当', nameEn: 'Abaddon' },
126: { nameZh: '阿克隆地道', nameEn: 'Pit of Acheron' },
127: { nameZh: '炼狱地坑', nameEn: 'Infernal Pit' },
128: { nameZh: '世界之石要塞一层', nameEn: 'Worldstone Keep Level 1' },
129: { nameZh: '世界之石要塞二层', nameEn: 'Worldstone Keep Level 2' },
130: { nameZh: '世界之石要塞三层', nameEn: 'Worldstone Keep Level 3' },
131: { nameZh: '毁灭王座', nameEn: 'Throne of Destruction' },
132: { nameZh: '世界之石大殿', nameEn: 'Worldstone Chamber' },
133: { nameZh: '主母巢穴', nameEn: "Matron's Den" },
134: { nameZh: '遗忘沙漠', nameEn: 'Forgotten Sands' },
135: { nameZh: '痛苦熔炉', nameEn: 'Furnace of Pain' },
136: { nameZh: '崔斯特姆', nameEn: 'Tristram' },
}
for (const [idStr, expected] of Object.entries(act5GroundTruth)) {
const id = Number(idStr)
const names = getLevelNames(id)
expect(names.nameZh, `Act 5 level ${id} nameZh`).toBe(expected.nameZh)
expect(names.nameEn, `Act 5 level ${id} nameEn`).toBe(expected.nameEn)
}
// Explicit assertions for the 9 key targets called out in mission prompt:
// 118, 119, 120, 121, 122, 123, 124, 131, 132
expect(getLevelNames(118)).toEqual({ nameZh: '远古之路', nameEn: "The Ancients' Way" })
expect(getLevelNames(119)).toEqual({ nameZh: '冰窖', nameEn: 'Icy Cellar' })
expect(getLevelNames(120)).toEqual({ nameZh: '亚瑞特山顶', nameEn: 'Arreat Summit' })
expect(getLevelNames(121)).toEqual({ nameZh: '尼拉塞克的神殿', nameEn: "Nihlathak's Temple" })
expect(getLevelNames(122)).toEqual({ nameZh: '悲痛之厅', nameEn: 'Halls of Anguish' })
expect(getLevelNames(123)).toEqual({ nameZh: '痛苦之厅', nameEn: 'Halls of Pain' })
expect(getLevelNames(124)).toEqual({ nameZh: '沃德之厅', nameEn: 'Halls of Vaught' })
expect(getLevelNames(131)).toEqual({ nameZh: '毁灭王座', nameEn: 'Throne of Destruction' })
expect(getLevelNames(132)).toEqual({ nameZh: '世界之石大殿', nameEn: 'Worldstone Chamber' })
})
it('verifies Acts 1..4 key benchmark areas match canonical D2 1.13c', () => {
// Act 1
expect(getLevelNames(1)).toEqual({ nameZh: '罗格营地', nameEn: 'Rogue Encampment' })
expect(getLevelNames(2)).toEqual({ nameZh: '鲜血荒地', nameEn: 'Blood Moor' })
expect(getLevelNames(8)).toEqual({ nameZh: '邪恶洞窟', nameEn: 'Den of Evil' })
expect(getLevelNames(17)).toEqual({ nameZh: '埋骨之地', nameEn: 'Burial Grounds' })
expect(getLevelNames(33)).toEqual({ nameZh: '大教堂', nameEn: 'Cathedral' })
expect(getLevelNames(37)).toEqual({ nameZh: '地下墓穴四层', nameEn: 'Catacombs Level 4' })
expect(getLevelNames(38)).toEqual({ nameZh: '崔斯特瑞姆', nameEn: 'Tristram' })
expect(getLevelNames(39)).toEqual({ nameZh: '奶牛关', nameEn: 'Moo Moo Farm' })
// Act 2
expect(getLevelNames(40)).toEqual({ nameZh: '鲁高因', nameEn: 'Lut Gholein' })
expect(getLevelNames(46)).toEqual({ nameZh: '术士峡谷', nameEn: 'Canyon of the Magi' })
expect(getLevelNames(73)).toEqual({ nameZh: '都瑞尔的巢穴', nameEn: "Duriel's Lair" })
expect(getLevelNames(74)).toEqual({ nameZh: '秘法圣所', nameEn: 'Arcane Sanctuary' })
// Act 3
expect(getLevelNames(75)).toEqual({ nameZh: '库拉斯特码头', nameEn: 'Kurast Docks' })
expect(getLevelNames(83)).toEqual({ nameZh: '崔凡克', nameEn: 'Travincal' })
expect(getLevelNames(102)).toEqual({ nameZh: '憎恨囚牢三层', nameEn: 'Durance of Hate Level 3' })
// Act 4
expect(getLevelNames(103)).toEqual({ nameZh: '群魔堡垒', nameEn: 'Pandemonium Fortress' })
expect(getLevelNames(107)).toEqual({ nameZh: '火焰之河', nameEn: 'River of Flame' })
expect(getLevelNames(108)).toEqual({ nameZh: '混沌避难所', nameEn: 'Chaos Sanctuary' })
})
})
// --------------------------------------------------------------------------
// 3. Multi-Format Slug Resolution
// --------------------------------------------------------------------------
describe('3. Multi-Format Slug Resolution Across Acts 1..5', () => {
const slugTestCases = [
// Act 1
{ input: 'burial-grounds', expectedZh: '埋骨之地', expectedEn: 'Burial Grounds', id: 17 },
{ input: '17-burial-grounds', expectedZh: '埋骨之地', expectedEn: 'Burial Grounds', id: 17 },
{ input: 'BURIAL-GROUNDS', expectedZh: '埋骨之地', expectedEn: 'Burial Grounds', id: 17 },
{ input: '17-BURIAL-GROUNDS', expectedZh: '埋骨之地', expectedEn: 'Burial Grounds', id: 17 },
{ input: ' burial-grounds ', expectedZh: '埋骨之地', expectedEn: 'Burial Grounds', id: 17 },
{ input: ' 17-burial-grounds ', expectedZh: '埋骨之地', expectedEn: 'Burial Grounds', id: 17 },
{ input: '17_burial_grounds', expectedZh: '埋骨之地', expectedEn: 'Burial Grounds', id: 17 },
{ input: 'burialgrounds', expectedZh: '埋骨之地', expectedEn: 'Burial Grounds', id: 17 },
{ input: 'rogue-encampment', expectedZh: '罗格营地', expectedEn: 'Rogue Encampment', id: 1 },
{ input: '1-rogue-encampment', expectedZh: '罗格营地', expectedEn: 'Rogue Encampment', id: 1 },
{ input: 'ROGUE-ENCAMPMENT', expectedZh: '罗格营地', expectedEn: 'Rogue Encampment', id: 1 },
{ input: '\t1-rogue-encampment\n', expectedZh: '罗格营地', expectedEn: 'Rogue Encampment', id: 1 },
{ input: 'blood-moor', expectedZh: '鲜血荒地', expectedEn: 'Blood Moor', id: 2 },
{ input: '2-blood-moor', expectedZh: '鲜血荒地', expectedEn: 'Blood Moor', id: 2 },
// Act 2
{ input: 'lut-gholein', expectedZh: '鲁高因', expectedEn: 'Lut Gholein', id: 40 },
{ input: '40-lut-gholein', expectedZh: '鲁高因', expectedEn: 'Lut Gholein', id: 40 },
{ input: 'LUT-GHOLEIN', expectedZh: '鲁高因', expectedEn: 'Lut Gholein', id: 40 },
{ input: 'canyon-of-the-magi', expectedZh: '术士峡谷', expectedEn: 'Canyon of the Magi', id: 46 },
{ input: '46-canyon-of-the-magi', expectedZh: '术士峡谷', expectedEn: 'Canyon of the Magi', id: 46 },
// Act 3
{ input: 'kurast-docks', expectedZh: '库拉斯特码头', expectedEn: 'Kurast Docks', id: 75 },
{ input: '75-kurast-docks', expectedZh: '库拉斯特码头', expectedEn: 'Kurast Docks', id: 75 },
{ input: 'travincal', expectedZh: '崔凡克', expectedEn: 'Travincal', id: 83 },
{ input: '83-travincal', expectedZh: '崔凡克', expectedEn: 'Travincal', id: 83 },
// Act 4
{ input: 'pandemonium-fortress', expectedZh: '群魔堡垒', expectedEn: 'Pandemonium Fortress', id: 103 },
{ input: '103-pandemonium-fortress', expectedZh: '群魔堡垒', expectedEn: 'Pandemonium Fortress', id: 103 },
{ input: 'the-pandemonium-fortress', expectedZh: '群魔堡垒', expectedEn: 'Pandemonium Fortress', id: 103 },
{ input: 'chaos-sanctuary', expectedZh: '混沌避难所', expectedEn: 'Chaos Sanctuary', id: 108 },
{ input: '108-chaos-sanctuary', expectedZh: '混沌避难所', expectedEn: 'Chaos Sanctuary', id: 108 },
{ input: 'THE-CHAOS-SANCTUARY', expectedZh: '混沌避难所', expectedEn: 'Chaos Sanctuary', id: 108 },
// Act 5
{ input: 'harrogath', expectedZh: '哈洛加斯', expectedEn: 'Harrogath', id: 109 },
{ input: '109-harrogath', expectedZh: '哈洛加斯', expectedEn: 'Harrogath', id: 109 },
{ input: 'HARROGATH', expectedZh: '哈洛加斯', expectedEn: 'Harrogath', id: 109 },
{ input: 'the-ancients-way', expectedZh: '远古之路', expectedEn: "The Ancients' Way", id: 118 },
{ input: '118-the-ancients-way', expectedZh: '远古之路', expectedEn: "The Ancients' Way", id: 118 },
{ input: 'ancients-way', expectedZh: '远古之路', expectedEn: "The Ancients' Way", id: 118 },
{ input: 'The Ancients\' Way', expectedZh: '远古之路', expectedEn: "The Ancients' Way", id: 118 },
{ input: 'icy-cellar', expectedZh: '冰窖', expectedEn: 'Icy Cellar', id: 119 },
{ input: '119-icy-cellar', expectedZh: '冰窖', expectedEn: 'Icy Cellar', id: 119 },
{ input: 'arreat-summit', expectedZh: '亚瑞特山顶', expectedEn: 'Arreat Summit', id: 120 },
{ input: '120-arreat-summit', expectedZh: '亚瑞特山顶', expectedEn: 'Arreat Summit', id: 120 },
{ input: 'nihlathaks-temple', expectedZh: '尼拉塞克的神殿', expectedEn: "Nihlathak's Temple", id: 121 },
{ input: '121-nihlathaks-temple', expectedZh: '尼拉塞克的神殿', expectedEn: "Nihlathak's Temple", id: 121 },
{ input: 'halls-of-anguish', expectedZh: '悲痛之厅', expectedEn: 'Halls of Anguish', id: 122 },
{ input: '122-halls-of-anguish', expectedZh: '悲痛之厅', expectedEn: 'Halls of Anguish', id: 122 },
{ input: 'halls-of-pain', expectedZh: '痛苦之厅', expectedEn: 'Halls of Pain', id: 123 },
{ input: '123-halls-of-pain', expectedZh: '痛苦之厅', expectedEn: 'Halls of Pain', id: 123 },
{ input: 'halls-of-vaught', expectedZh: '沃德之厅', expectedEn: 'Halls of Vaught', id: 124 },
{ input: '124-halls-of-vaught', expectedZh: '沃德之厅', expectedEn: 'Halls of Vaught', id: 124 },
{ input: 'throne-of-destruction', expectedZh: '毁灭王座', expectedEn: 'Throne of Destruction', id: 131 },
{ input: '131-throne-of-destruction', expectedZh: '毁灭王座', expectedEn: 'Throne of Destruction', id: 131 },
{ input: 'worldstone-chamber', expectedZh: '世界之石大殿', expectedEn: 'Worldstone Chamber', id: 132 },
{ input: '132-worldstone-chamber', expectedZh: '世界之石大殿', expectedEn: 'Worldstone Chamber', id: 132 },
{ input: 'the-worldstone-chamber', expectedZh: '世界之石大殿', expectedEn: 'Worldstone Chamber', id: 132 },
]
for (const tc of slugTestCases) {
it(`resolves slug variant: "${tc.input}" -> ID ${tc.id}`, () => {
expect(levelIdOfSlug(tc.input)).toBe(tc.id)
const names = getLevelNames(tc.input)
expect(names.nameZh).toBe(tc.expectedZh)
expect(names.nameEn).toBe(tc.expectedEn)
expect(sceneNameZh(tc.input)).toBe(tc.expectedZh)
expect(sceneNameEn(tc.input)).toBe(tc.expectedEn)
})
}
})
// --------------------------------------------------------------------------
// 4. Bizarre, Out-of-Range & Adversarial Inputs
// --------------------------------------------------------------------------
describe('4. Bizarre, Out-of-Range & Adversarial Inputs', () => {
it('gracefully handles level ID 0 without throwing or blank names', () => {
const names0 = getLevelNames(0)
expect(names0.nameZh).toBeTruthy()
expect(names0.nameEn).toBeTruthy()
expect(names0.nameZh.trim().length).toBeGreaterThan(0)
expect(names0.nameEn.trim().length).toBeGreaterThan(0)
expect(levelIdOfSlug('0')).toBeNull()
expect(sceneNameZh(0)).toBeTruthy()
expect(sceneNameEn(0)).toBeTruthy()
})
it('gracefully handles negative IDs (-1, -100) without throwing or blank names', () => {
for (const neg of [-1, -2, -100, -9999]) {
const names = getLevelNames(neg)
expect(names.nameZh).toBeTruthy()
expect(names.nameEn).toBeTruthy()
expect(names.nameZh.trim().length).toBeGreaterThan(0)
expect(names.nameEn.trim().length).toBeGreaterThan(0)
expect(names.nameZh).not.toContain('-')
expect(names.nameEn).not.toContain('-')
expect(levelIdOfSlug(String(neg))).toBeNull()
expect(sceneNameZh(neg)).toBeTruthy()
expect(sceneNameEn(neg)).toBeTruthy()
}
})
it('gracefully handles out-of-range IDs (137, 9999, 100000) without throwing', () => {
for (const oor of [137, 138, 200, 9999, 100000]) {
const names = getLevelNames(oor)
expect(names.nameZh).toBeTruthy()
expect(names.nameEn).toBeTruthy()
expect(names.nameZh.trim().length).toBeGreaterThan(0)
expect(names.nameEn.trim().length).toBeGreaterThan(0)
expect(levelIdOfSlug(String(oor))).toBeNull()
expect(sceneNameZh(oor)).toBeTruthy()
expect(sceneNameEn(oor)).toBeTruthy()
}
})
it('gracefully handles float and special IEEE 754 numbers (1.5, NaN, Infinity)', () => {
const nanRes = getLevelNames(NaN)
expect(nanRes.nameZh).toBeTruthy()
expect(nanRes.nameEn).toBeTruthy()
const infRes = getLevelNames(Infinity)
expect(infRes.nameZh).toBeTruthy()
expect(infRes.nameEn).toBeTruthy()
const floatRes = getLevelNames(1.5)
expect(floatRes.nameZh).toBeTruthy()
expect(floatRes.nameEn).toBeTruthy()
})
it('gracefully handles empty strings and whitespace-only inputs', () => {
for (const emptyInput of ['', ' ', '\t', '\n\r \t\n']) {
const names = getLevelNames(emptyInput)
expect(names.nameZh).toBe('Unknown Area')
expect(names.nameEn).toBe('Unknown Area')
expect(levelIdOfSlug(emptyInput)).toBeNull()
expect(sceneNameZh(emptyInput)).toBe('Unknown Area')
expect(sceneNameEn(emptyInput)).toBe('Unknown Area')
}
})
it('gracefully handles weird punctuation strings without hyphens leaking', () => {
const weirdStrings = [
'---',
'___',
'-_-_-',
'!@#$%^&*()_+',
'???::;;;',
'../../../etc/passwd',
]
for (const weird of weirdStrings) {
const names = getLevelNames(weird)
expect(names.nameZh).toBeTruthy()
expect(names.nameEn).toBeTruthy()
// Must not contain raw hyphen or raw underscore
expect(names.nameZh).not.toContain('_')
expect(names.nameEn).not.toContain('_')
}
})
it('gracefully handles null and undefined inputs without crashing (Anti-Crash Guard)', () => {
expect(() => getLevelNames(null as unknown as string)).not.toThrow()
expect(() => getLevelNames(undefined as unknown as string)).not.toThrow()
expect(() => sceneNameZh(null as unknown as string)).not.toThrow()
expect(() => sceneNameZh(undefined as unknown as string)).not.toThrow()
expect(() => sceneNameEn(null as unknown as string)).not.toThrow()
expect(() => sceneNameEn(undefined as unknown as string)).not.toThrow()
})
})
// --------------------------------------------------------------------------
// 5. Slug Leak Prevention in sceneNameZh & sceneNameEn
// --------------------------------------------------------------------------
describe('5. Slug Leak Prevention in Fallback Display', () => {
it('prevents raw un-spaced hyphenated slug leak when fallback parameter is given', () => {
// If someone calls sceneNameZh with a raw slug fallback like 'burial-grounds'
expect(sceneNameZh('burial-grounds', 'burial-grounds')).toBe('埋骨之地')
expect(sceneNameEn('burial-grounds', 'burial-grounds')).toBe('Burial Grounds')
// For unknown slug with hyphenated fallback:
const unknownSlug = 'custom-dungeon-layer-1'
expect(sceneNameZh(unknownSlug, unknownSlug)).toBe('Custom Dungeon Layer 1')
expect(sceneNameEn(unknownSlug, unknownSlug)).toBe('Custom Dungeon Layer 1')
// If fallback is already a clean human title, preserve it:
expect(sceneNameZh(unknownSlug, 'Custom Dungeon Layer 1')).toBe('Custom Dungeon Layer 1')
expect(sceneNameEn(unknownSlug, 'Custom Dungeon Layer 1')).toBe('Custom Dungeon Layer 1')
})
})
// --------------------------------------------------------------------------
// 6. Dynamic .tbl Decoding & Active Level Names
// --------------------------------------------------------------------------
describe('6. Dynamic .tbl Decoding & Active Level Names', () => {
it('supports round-trip TBL dictionary decode and active level overrides', () => {
const entries: TblEntry[] = [
{ key: 'Rogue Encampment', value: '萝格营区' },
{ key: 'Burial Grounds', value: '古老墓园' },
{ key: "The Ancients' Way", value: '先祖长廊' },
{ key: 'Worldstone Chamber', value: '世界石神殿' },
]
const encoded = encodeTbl(entries)
const decoded = decodeTbl(encoded, 'utf-8')
const customNames = decodeLevelNamesFromTbl(decoded)
expect(customNames.get(1)).toBe('萝格营区')
expect(customNames.get(17)).toBe('古老墓园')
expect(customNames.get(118)).toBe('先祖长廊')
expect(customNames.get(132)).toBe('世界石神殿')
// Untouched levels fall back to LEVEL_NAMES_ZH
expect(customNames.get(2)).toBe(LEVEL_NAMES_ZH[2])
expect(customNames.get(109)).toBe(LEVEL_NAMES_ZH[109])
// Test active level names global state
try {
setActiveLevelNames(customNames)
expect(getActiveLevelNames()).toBe(customNames)
expect(getLevelNames(1).nameZh).toBe('萝格营区')
expect(getLevelNames('17-burial-grounds').nameZh).toBe('古老墓园')
expect(getLevelNames(118).nameZh).toBe('先祖长廊')
expect(getLevelNames(132).nameZh).toBe('世界石神殿')
// English remains canonical
expect(getLevelNames(1).nameEn).toBe('Rogue Encampment')
expect(getLevelNames(17).nameEn).toBe('Burial Grounds')
expect(getLevelNames(118).nameEn).toBe("The Ancients' Way")
expect(getLevelNames(132).nameEn).toBe('Worldstone Chamber')
} finally {
setActiveLevelNames(null)
expect(getActiveLevelNames()).toBeNull()
expect(getLevelNames(1).nameZh).toBe('罗格营地')
}
})
})
// --------------------------------------------------------------------------
// 7. HUD Area Banner Integration
// --------------------------------------------------------------------------
describe('7. HUD Area Banner Integration', () => {
it('sets area banner in WorldPanelsHud with exact bilingual names and 3600ms duration', () => {
const hud = new WorldPanelsHud()
const now = 50000
const burial = getLevelNames(17)
hud.showAreaBanner(burial.nameZh, burial.nameEn, now)
expect(hud.areaBanner).toBeDefined()
expect(hud.areaBanner?.titleZh).toBe('埋骨之地')
expect(hud.areaBanner?.titleEn).toBe('Burial Grounds')
expect(hud.areaBanner?.untilMs).toBe(now + 3600)
const ancients = getLevelNames(118)
hud.showAreaBanner(ancients.nameZh, ancients.nameEn, now + 1000)
expect(hud.areaBanner?.titleZh).toBe('远古之路')
expect(hud.areaBanner?.titleEn).toBe("The Ancients' Way")
expect(hud.areaBanner?.untilMs).toBe(now + 1000 + 3600)
})
})
})