fix: resolve slug edge cases and Act 5 Pandemonium level names (Fixes #147)

This commit is contained in:
troytt 2026-09-22 03:17:22 +00:00
parent d5c3c1d316
commit 244bdc1efb
3 changed files with 522 additions and 16 deletions

View File

@ -157,10 +157,10 @@ export const LEVEL_NAMES_ZH: Readonly<Record<number, string>> = {
130: '世界之石要塞三层',
131: '毁灭王座',
132: '世界之石大殿',
133: '遗忘之沙',
134: '巨窟',
135: '苦痛熔炉',
136: '混沌崔斯特瑞姆',
133: '主母巢穴',
134: '遗忘沙漠',
135: '痛苦熔炉',
136: '崔斯特姆',
}
/**
@ -457,6 +457,16 @@ const SLUG_TO_LEVEL_ID: ReadonlyMap<string, number> = (() => {
'worldstonekeep3': 130,
'throneofdestruction': 131,
'worldstonechamber': 132,
'theworldstonechamber': 132,
'pandemoniumfortress': 103,
'thepandemoniumfortress': 103,
'chaossanctuary': 108,
'thechaossanctuary': 108,
'matronsden': 133,
'forgottensands': 134,
'furnaceofpain': 135,
'ubertristram': 136,
'pandemoniumtristram': 136,
}
for (const [k, v] of Object.entries(aliases)) {
map.set(normalizeSlug(k), v)
@ -472,13 +482,21 @@ const SLUG_TO_LEVEL_ID: ReadonlyMap<string, number> = (() => {
* @returns the id (1..136), or null when unresolvable.
*/
export function levelIdOfSlug(slug: string): number | null {
const match = /^(\d+)(?:-|_|$)/.exec(slug)
if (slug == null || typeof slug !== 'string') return null
const trimmed = slug.trim()
if (!trimmed) return null
const match = /^(\d+)(?:-|_|$)/.exec(trimmed)
if (match?.[1] !== undefined) {
const parsed = Number(match[1])
if (parsed >= 1 && parsed <= 136) return parsed
}
const norm = normalizeSlug(slug)
return SLUG_TO_LEVEL_ID.get(norm) ?? null
const norm = normalizeSlug(trimmed)
return (
SLUG_TO_LEVEL_ID.get(norm) ??
SLUG_TO_LEVEL_ID.get(norm.replace(/^the/, '')) ??
SLUG_TO_LEVEL_ID.get('the' + norm) ??
null
)
}
export interface LevelNames {
@ -491,15 +509,24 @@ export interface LevelNames {
* Resolves both numeric level IDs (1..136) and string slugs (e.g. 'burial-grounds').
* Returns authentic Chinese and English names, never empty, never leaking raw slugs.
*/
export function getLevelNames(levelIdOrSlug: number | string): LevelNames {
export function getLevelNames(levelIdOrSlug: number | string | null | undefined): LevelNames {
if (levelIdOrSlug == null || levelIdOrSlug === '') {
return { nameZh: 'Unknown Area', nameEn: 'Unknown Area' }
}
let id: number | null = null
let rawStr = ''
if (typeof levelIdOrSlug === 'number') {
id = levelIdOrSlug
if (Number.isFinite(levelIdOrSlug) && Number.isInteger(levelIdOrSlug)) {
id = levelIdOrSlug
}
rawStr = String(levelIdOrSlug)
} else {
rawStr = levelIdOrSlug.trim()
rawStr = String(levelIdOrSlug).trim()
if (!rawStr) {
return { nameZh: 'Unknown Area', nameEn: 'Unknown Area' }
}
id = levelIdOfSlug(rawStr)
}
@ -532,12 +559,18 @@ export function getLevelNames(levelIdOrSlug: number | string): LevelNames {
* @returns the display name.
*/
export function sceneNameZh(
slug: string | number,
slug: string | number | null | undefined,
fallback?: string,
customMap?: Map<number, string> | Record<number, string>,
): string {
const slugStr = String(slug)
const id = typeof slug === 'number' ? slug : levelIdOfSlug(slugStr)
if (slug == null || slug === '') {
return fallback && !isRawSlug(fallback) ? fallback : 'Unknown Area'
}
const slugStr = typeof slug === 'string' ? slug.trim() : String(slug)
if (!slugStr) {
return fallback && !isRawSlug(fallback) ? fallback : 'Unknown Area'
}
const id = typeof slug === 'number' ? (Number.isInteger(slug) ? slug : null) : levelIdOfSlug(slugStr)
if (id !== null) {
if (customMap) {
const val = customMap instanceof Map ? customMap.get(id) : customMap[id]
@ -565,11 +598,17 @@ export function sceneNameZh(
* @returns canonical English level name.
*/
export function sceneNameEn(
slug: string | number,
slug: string | number | null | undefined,
fallback?: string,
): string {
const slugStr = String(slug)
const id = typeof slug === 'number' ? slug : levelIdOfSlug(slugStr)
if (slug == null || slug === '') {
return fallback && !isRawSlug(fallback) ? fallback : 'Unknown Area'
}
const slugStr = typeof slug === 'string' ? slug.trim() : String(slug)
if (!slugStr) {
return fallback && !isRawSlug(fallback) ? fallback : 'Unknown Area'
}
const id = typeof slug === 'number' ? (Number.isInteger(slug) ? slug : null) : levelIdOfSlug(slugStr)
if (id !== null && CANONICAL_LEVEL_NAME_KEYS[id]) {
return CANONICAL_LEVEL_NAME_KEYS[id]!
}

View File

@ -0,0 +1,453 @@
/**
* 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)
})
})
})

View File

@ -573,6 +573,10 @@ describe('Diablo II v1.13c UI / HUD (Issue #27)', () => {
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' })
expect(getLevelNames(133)).toEqual({ nameZh: '主母巢穴', nameEn: "Matron's Den" })
expect(getLevelNames(134)).toEqual({ nameZh: '遗忘沙漠', nameEn: 'Forgotten Sands' })
expect(getLevelNames(135)).toEqual({ nameZh: '痛苦熔炉', nameEn: 'Furnace of Pain' })
expect(getLevelNames(136)).toEqual({ nameZh: '崔斯特姆', nameEn: 'Tristram' })
// 3. Complete 136 level IDs validity: no empty strings, no raw hyphen slugs
for (let id = 1; id <= 136; id++) {
@ -588,12 +592,16 @@ describe('Diablo II v1.13c UI / HUD (Issue #27)', () => {
// 4. Slug resolution: both un-prefixed and numbered prefixes resolve cleanly
expect(getLevelNames('burial-grounds')).toEqual({ nameZh: '埋骨之地', nameEn: 'Burial Grounds' })
expect(getLevelNames('17-burial-grounds')).toEqual({ nameZh: '埋骨之地', nameEn: 'Burial Grounds' })
expect(getLevelNames(' 17-burial-grounds ')).toEqual({ nameZh: '埋骨之地', nameEn: 'Burial Grounds' })
expect(getLevelNames('rogue-encampment')).toEqual({ nameZh: '罗格营地', nameEn: 'Rogue Encampment' })
expect(getLevelNames('blood-moor')).toEqual({ nameZh: '鲜血荒地', nameEn: 'Blood Moor' })
expect(getLevelNames('harrogath')).toEqual({ nameZh: '哈洛加斯', nameEn: 'Harrogath' })
expect(getLevelNames('the-ancients-way')).toEqual({ nameZh: '远古之路', nameEn: "The Ancients' Way" })
expect(getLevelNames('icy-cellar')).toEqual({ nameZh: '冰窖', nameEn: 'Icy Cellar' })
expect(getLevelNames('arreat-summit')).toEqual({ nameZh: '亚瑞特山顶', nameEn: 'Arreat Summit' })
expect(getLevelNames('the-pandemonium-fortress')).toEqual({ nameZh: '群魔堡垒', nameEn: 'Pandemonium Fortress' })
expect(getLevelNames('THE-CHAOS-SANCTUARY')).toEqual({ nameZh: '混沌避难所', nameEn: 'Chaos Sanctuary' })
expect(getLevelNames('the-worldstone-chamber')).toEqual({ nameZh: '世界之石大殿', nameEn: 'Worldstone Chamber' })
// 5. sceneNameZh and sceneNameEn parity and slug leak prevention
expect(sceneNameZh(17)).toBe('埋骨之地')
@ -620,5 +628,11 @@ describe('Diablo II v1.13c UI / HUD (Issue #27)', () => {
titleEn: 'Burial Grounds',
untilMs: now + 3600,
})
// 8. Safe fallbacks for null/undefined/empty inputs without throwing TypeError
expect(getLevelNames(null as any)).toEqual({ nameZh: 'Unknown Area', nameEn: 'Unknown Area' })
expect(getLevelNames(undefined as any)).toEqual({ nameZh: 'Unknown Area', nameEn: 'Unknown Area' })
expect(sceneNameZh(null as any)).toBe('Unknown Area')
expect(sceneNameEn(null as any)).toBe('Unknown Area')
})
})