454 lines
24 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|