/** * 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 = { 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) }) }) })