fix: resolve slug edge cases and Act 5 Pandemonium level names (Fixes #147)
This commit is contained in:
parent
d5c3c1d316
commit
244bdc1efb
|
|
@ -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]!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue