fix(maze): 让 UNIMPLEMENTED_PASSES 反映真实缺口
`UNIMPLEMENTED_PASSES` 此前被硬编码为 `[]`,注释声称 11 个核心地牢 pass 全部 实现,于是 `stats.unimplementedPasses` 在每一关都无条件报告「零缺口」——包括 那些 `specials` 表为空、根本不生成楼梯房间的 level type。 改为从 `MAZE_LEVEL_TYPE_PROFILES` 实测推导: - 新增 `unimplementedPassesFor(levelTypeName)`,按 level type 返回缺口: 没有 profile 行,或 `specials` 为空且不是 `singleRoom` 定制关。 - `UNIMPLEMENTED_PASSES` 改为所有 level type 缺口的并集,因此非空。 - `generateMaze` 的 `stats.unimplementedPasses` 改报本关自己的缺口, 而不是全局常量;`unresolvedRoles` / `notes` 本来就已随 stats 返回。 同时修正两处已经与代码不符的注释:`maze.ts` 里「per-act 装饰 pass 均未复现」 (它们已在 `maze-special-passes.ts` 中实现),以及 `pack-act-assets.ts` 里 「第二幕下水道 / 墓穴 / 第三幕地牢 specials 为空」(这三者现在都有 specials, 真正为空的是 Harem、Basement、Spider、Act 5 Lava)。 测试:原 `UNIMPLEMENTED_PASSES is completely empty` 断言的是被修正的错误行为, 改为断言它恰好列出所有 `specials` 为空的 level type;另加 4 个用例覆盖 按 level type 查询与 `generateMaze` 的 stats 透出。 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
4ece5aa3c1
commit
5b3a9db986
|
|
@ -730,9 +730,10 @@ function buildSceneLinks(
|
||||||
placedSeams.add(edge.to)
|
placedSeams.add(edge.to)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Last resort. Several level types — the Act 2 sewers, the tombs, the
|
// 5. Last resort. Several level types — the Act 2 Harem and Basement, the
|
||||||
// Act 3 dungeons — have an empty `specials` table in `MAZE_LEVEL_TYPE_PROFILES`
|
// Act 3 Spider Cavern, the Act 5 Infernal Pit — have an empty `specials`
|
||||||
// because their DRLG staircase pass has not been transcribed, so they
|
// table in `MAZE_LEVEL_TYPE_PROFILES` because their DRLG staircase pass
|
||||||
|
// has not been transcribed (`unimplementedPassesFor` names them), so they
|
||||||
// stamp no stair room, and their pieces carry no marker either. Leaving
|
// stamp no stair room, and their pieces carry no marker either. Leaving
|
||||||
// those edges unplaced makes a level you can enter and never leave, which
|
// those edges unplaced makes a level you can enter and never leave, which
|
||||||
// is a worse lie than an approximate staircase. Each one is spread to a
|
// is a worse lie than an approximate staircase. Each one is spread to a
|
||||||
|
|
|
||||||
|
|
@ -594,8 +594,11 @@ function entries(plainSides: readonly string[] = NESW): Omit<SpecialStep, 'kind'
|
||||||
* `specials` covers the replacement tables that live inline in that function.
|
* `specials` covers the replacement tables that live inline in that function.
|
||||||
* The per-act decoration passes that follow it in D2MOO — `DRLGMAZE_PlaceAct2TombStuff`,
|
* The per-act decoration passes that follow it in D2MOO — `DRLGMAZE_PlaceAct2TombStuff`,
|
||||||
* `DRLGMAZE_PlaceAct3DungeonStuff`, `DRLGMAZE_PlaceAct5IceStuff` and the rest —
|
* `DRLGMAZE_PlaceAct3DungeonStuff`, `DRLGMAZE_PlaceAct5IceStuff` and the rest —
|
||||||
* are **not** reproduced; they are listed in `stats.unimplementedPasses` so the
|
* live in `maze-special-passes.ts` and are named by a row's `specialPass`; each
|
||||||
* omission is visible rather than silent.
|
* one that runs is listed in `stats.specialPassesApplied`. A row with an empty
|
||||||
|
* `specials` table has no transcribed staircase pass at all, and
|
||||||
|
* {@link unimplementedPassesFor} reports that gap in `stats.unimplementedPasses`
|
||||||
|
* so the omission is visible rather than silent.
|
||||||
*/
|
*/
|
||||||
export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -850,10 +853,47 @@ export const MAZE_LEVEL_TYPE_PROFILES: readonly MazeLevelProfile[] = [
|
||||||
export const MAZE_SHARED_PRESET_NAMES: readonly string[] = ['Act 4 - Lava X']
|
export const MAZE_SHARED_PRESET_NAMES: readonly string[] = ['Act 4 - Lava X']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-act decoration passes D2MOO runs after the layout.
|
* The gaps one level type still has, derived from its own profile row.
|
||||||
* All 11 core dungeon passes are now implemented in `maze-special-passes.ts`.
|
*
|
||||||
|
* This used to be a module constant hardcoded to `[]` with a comment claiming
|
||||||
|
* every pass was implemented, which made `stats.unimplementedPasses` report
|
||||||
|
* "no gaps" for a level that stamps no staircase at all. The list is now
|
||||||
|
* *measured* from {@link MAZE_LEVEL_TYPE_PROFILES}, so it cannot drift away
|
||||||
|
* from the table it describes:
|
||||||
|
*
|
||||||
|
* - **no profile row** — `generateMaze` cannot build the type at all.
|
||||||
|
* - **an empty `specials` table** — the `Prev`/`Next`/quest replacement rows
|
||||||
|
* that `DRLGMAZE_GenerateLevel` runs inline have not been transcribed, so the
|
||||||
|
* level stamps no staircase room and the packer has to invent a fallback warp
|
||||||
|
* (see `scripts/pack-act-assets.ts`). A `singleRoom` type is exempt: it is one
|
||||||
|
* fixed `LvlPrest.txt` row whose stairs are part of the artwork.
|
||||||
|
*
|
||||||
|
* The 11 per-act decoration passes named in D2MOO (`DRLGMAZE_PlaceAct2TombStuff`
|
||||||
|
* and the rest) *are* reproduced, in `maze-special-passes.ts`; a type that runs
|
||||||
|
* one names it in `stats.specialPassesApplied` instead.
|
||||||
|
*
|
||||||
|
* @param levelTypeName - the `LvlTypes.txt` name.
|
||||||
|
* @returns the gaps, empty when the type is fully transcribed.
|
||||||
*/
|
*/
|
||||||
export const UNIMPLEMENTED_PASSES: readonly string[] = []
|
export function unimplementedPassesFor(levelTypeName: string): readonly string[] {
|
||||||
|
const profile = MAZE_LEVEL_TYPE_PROFILES.find(candidate => candidate.levelTypeName === levelTypeName)
|
||||||
|
if (profile === undefined) return [`DRLGMAZE_GenerateLevel profile (${levelTypeName})`]
|
||||||
|
const gaps: string[] = []
|
||||||
|
if (profile.specials.length === 0 && profile.singleRoom === undefined) {
|
||||||
|
gaps.push(`DRLGMAZE_GenerateLevel specials table (${levelTypeName})`)
|
||||||
|
}
|
||||||
|
return gaps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every gap {@link unimplementedPassesFor} can name, across all level types.
|
||||||
|
*
|
||||||
|
* Non-empty while any level type is still missing its replacement table. Prefer
|
||||||
|
* the per-level-type function: a caller that reports this constant for one level
|
||||||
|
* is reporting other levels' gaps as its own.
|
||||||
|
*/
|
||||||
|
export const UNIMPLEMENTED_PASSES: readonly string[] = MAZE_LEVEL_TYPE_PROFILES
|
||||||
|
.flatMap(profile => unimplementedPassesFor(profile.levelTypeName))
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- *
|
/* ------------------------------------------------------------------------- *
|
||||||
* Geometry
|
* Geometry
|
||||||
|
|
@ -2327,7 +2367,7 @@ export function generateMaze(request: MazeRequest): MazeResult {
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
unresolvedRoles: stats.unresolvedRoles,
|
unresolvedRoles: stats.unresolvedRoles,
|
||||||
unimplementedPasses: UNIMPLEMENTED_PASSES,
|
unimplementedPasses: unimplementedPassesFor(levelTypeName),
|
||||||
maxCellsX,
|
maxCellsX,
|
||||||
maxCellsY,
|
maxCellsY,
|
||||||
boundsRejected: stats.boundsRejected ?? 0,
|
boundsRejected: stats.boundsRejected ?? 0,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
* 9. DRLGMAZE_PlaceAct1Barracks (Monastery Barracks Smith Forge & Court Connect)
|
* 9. DRLGMAZE_PlaceAct1Barracks (Monastery Barracks Smith Forge & Court Connect)
|
||||||
* 10. DRLGMAZE_PlaceAct4Lava (River of Flame Hellforge, Bridges 1 & 2, Warp, Lava X fillers)
|
* 10. DRLGMAZE_PlaceAct4Lava (River of Flame Hellforge, Bridges 1 & 2, Warp, Lava X fillers)
|
||||||
* 11. DRLGMAZE_ScanReplaceSpecialAct2SewersPresets (Lut Gholein Sewers Radament's Lair, Dual Entrances, Waypoint)
|
* 11. DRLGMAZE_ScanReplaceSpecialAct2SewersPresets (Lut Gholein Sewers Radament's Lair, Dual Entrances, Waypoint)
|
||||||
* 12. Full generateMaze integration tests verifying specialPassesApplied and UNIMPLEMENTED_PASSES == [].
|
* 12. Full generateMaze integration tests verifying specialPassesApplied and the
|
||||||
|
* per-level-type `unimplementedPasses` gap list.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect, test } from 'vitest'
|
import { describe, expect, test } from 'vitest'
|
||||||
|
|
@ -21,7 +22,9 @@ import type { Ds1, Ds1Cell, Ds1Object } from '../src/formats/ds1.ts'
|
||||||
import {
|
import {
|
||||||
generateMaze,
|
generateMaze,
|
||||||
classifyMazePieceName,
|
classifyMazePieceName,
|
||||||
|
MAZE_LEVEL_TYPE_PROFILES,
|
||||||
UNIMPLEMENTED_PASSES,
|
UNIMPLEMENTED_PASSES,
|
||||||
|
unimplementedPassesFor,
|
||||||
type MazePiece,
|
type MazePiece,
|
||||||
type MazePieceKind,
|
type MazePieceKind,
|
||||||
type MazeRequest,
|
type MazeRequest,
|
||||||
|
|
@ -106,11 +109,64 @@ function createStandardPieceSet(levelTypeName: string, extraPieces: MazePiece[]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Diablo II DRLG Maze Special Passes', () => {
|
describe('Diablo II DRLG Maze Special Passes', () => {
|
||||||
test('UNIMPLEMENTED_PASSES is completely empty', () => {
|
// Was `expect(UNIMPLEMENTED_PASSES).toEqual([])`: the constant was hardcoded
|
||||||
expect(UNIMPLEMENTED_PASSES).toEqual([])
|
// to `[]`, so it asserted a claim the profile table contradicts. The gap list
|
||||||
expect(UNIMPLEMENTED_PASSES.length).toBe(0)
|
// is now derived, so the honest assertion is "it names exactly the level types
|
||||||
|
// whose `specials` table is still empty".
|
||||||
|
test('UNIMPLEMENTED_PASSES names every level type with no transcribed specials table', () => {
|
||||||
|
const emptySpecials = MAZE_LEVEL_TYPE_PROFILES
|
||||||
|
.filter(profile => profile.specials.length === 0 && profile.singleRoom === undefined)
|
||||||
|
.map(profile => profile.levelTypeName)
|
||||||
|
expect(emptySpecials.length).toBeGreaterThan(0)
|
||||||
|
expect(UNIMPLEMENTED_PASSES).toEqual(
|
||||||
|
emptySpecials.map(name => `DRLGMAZE_GenerateLevel specials table (${name})`),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('unimplementedPassesFor is empty for a fully transcribed level type', () => {
|
||||||
|
expect(unimplementedPassesFor('Act 1 - Cave')).toEqual([])
|
||||||
|
expect(unimplementedPassesFor('Act 2 - Tomb')).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unimplementedPassesFor reports the gap for a level type with no specials', () => {
|
||||||
|
expect(unimplementedPassesFor('Act 3 - Spider'))
|
||||||
|
.toEqual(['DRLGMAZE_GenerateLevel specials table (Act 3 - Spider)'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unimplementedPassesFor reports a level type that has no profile at all', () => {
|
||||||
|
expect(unimplementedPassesFor('Act 9 - Nowhere'))
|
||||||
|
.toEqual(['DRLGMAZE_GenerateLevel profile (Act 9 - Nowhere)'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generateMaze reports the level type gap, not a global constant', () => {
|
||||||
|
const spider = generateMaze({
|
||||||
|
levelId: 84,
|
||||||
|
levelName: 'Spider Cavern',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 5,
|
||||||
|
merge: 500,
|
||||||
|
seed: 8484,
|
||||||
|
pieces: createStandardPieceSet('Act 3 - Spider'),
|
||||||
|
})
|
||||||
|
expect(spider.stats.unimplementedPasses)
|
||||||
|
.toEqual(['DRLGMAZE_GenerateLevel specials table (Act 3 - Spider)'])
|
||||||
|
// The other two diagnostic channels must travel with it.
|
||||||
|
expect(Array.isArray(spider.stats.unresolvedRoles)).toBe(true)
|
||||||
|
expect(Array.isArray(spider.stats.notes)).toBe(true)
|
||||||
|
|
||||||
|
const cave = generateMaze({
|
||||||
|
levelId: 8,
|
||||||
|
levelName: 'Den of Evil',
|
||||||
|
sectionSize: 10,
|
||||||
|
minRooms: 5,
|
||||||
|
merge: 500,
|
||||||
|
seed: 8484,
|
||||||
|
pieces: createStandardPieceSet('Act 1 - Cave'),
|
||||||
|
})
|
||||||
|
expect(cave.stats.unimplementedPasses).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------- *
|
/* ----------------------------------------------------------------------- *
|
||||||
* 1. Act 2 - Tomb
|
* 1. Act 2 - Tomb
|
||||||
* ----------------------------------------------------------------------- */
|
* ----------------------------------------------------------------------- */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue