diff --git a/scripts/pack-act-assets.ts b/scripts/pack-act-assets.ts index b0ba3db..638a55f 100644 --- a/scripts/pack-act-assets.ts +++ b/scripts/pack-act-assets.ts @@ -541,6 +541,7 @@ function buildSceneLinks( const outEntrances: SceneEntrance[] = [] const outWarps: SceneWarp[] = [] const unplaced: { toLevelId: number; reason: string }[] = [] + const notes: string[] = [] const placedSeams = new Set() const placedWarps = new Set() @@ -751,6 +752,9 @@ function buildSceneLinks( unplaced.push({ toLevelId: edge.to, reason: 'no walkable ground for a fallback warp' }) return } + const fromName = worldGraph.levels.get(levelId)?.name ?? `level ${String(levelId)}` + const toName = worldGraph.levels.get(edge.to)?.name ?? `level ${String(edge.to)}` + notes.push(`fallback warp placed for edge ${fromName} -> ${toName}`) outWarps.push(makeWarp( edge, directionTo(edge.to), @@ -797,7 +801,7 @@ function buildSceneLinks( } } - return { entrances: outEntrances, warps: outWarps, waypoints, unplacedEdges: unplaced } + return { entrances: outEntrances, warps: outWarps, waypoints, unplacedEdges: unplaced, notes } } const allNames = await archives.listFiles() @@ -1157,6 +1161,7 @@ async function bakeDs1Variant( readonly mazeWarps: readonly MazeWarp[] readonly mazeWaypoints?: readonly MazeWaypoint[] readonly landmarks?: readonly { readonly id: string; readonly tileX: number; readonly tileY: number }[] + readonly notes?: readonly string[] | undefined } = { entrances: [], mazeWarps: [] }, ): Promise { const scene: IsoMapScene = buildIsoMapScene(level, libraries, seed) @@ -1445,6 +1450,10 @@ async function bakeDs1Variant( // Edges the world graph has that this copy of the level has nowhere to // put. Reported rather than dropped: each one is a hole in the world. unplacedEdges: sceneLinks.unplacedEdges, + notes: [ + ...(generatorLinks.notes ?? []), + ...(sceneLinks.notes ?? []), + ], }, } const sceneBytes = new TextEncoder().encode(JSON.stringify(sceneJson)) @@ -1649,6 +1658,7 @@ for (const entry of LEVELS) { ...(Array.isArray(result.stats.landmarkEntities) ? { landmarks: result.stats.landmarkEntities as readonly { readonly id: string; readonly tileX: number; readonly tileY: number }[] } : {}), + ...(Array.isArray(result.stats.notes) ? { notes: result.stats.notes } : {}), }, ) } catch (err) { diff --git a/scripts/verify-packs.ts b/scripts/verify-packs.ts index a08ffb8..3d9c4bb 100644 --- a/scripts/verify-packs.ts +++ b/scripts/verify-packs.ts @@ -295,7 +295,10 @@ interface PackedLinks { readonly source?: 'tile' | 'room' | 'fallback' }[] readonly waypoints?: readonly { readonly waypointId: number }[] - readonly stats?: { readonly unplacedEdges?: readonly { readonly toLevelId: number; readonly reason: string }[] } + readonly stats?: { + readonly unplacedEdges?: readonly { readonly toLevelId: number; readonly reason: string }[] + readonly notes?: readonly string[] + } } const graph = JSON.parse(await readFile(join(packDir, 'world-graph.json'), 'utf8')) as PackedWorldGraph @@ -385,13 +388,24 @@ for (const key of orphanEdges.slice(0, 20)) { } check(orphanEdges.length === 0, `world: no graph edge left without an opening (${String(orphanEdges.length)} orphans)`) -// Not an assertion: an approximate staircase is traversable, just not authentic. -// The number is printed every run so it cannot creep up unnoticed, and it only -// falls when a level type's DRLG staircase pass gets transcribed. +// Cap fallback staircases at the known baseline (80 across 15 levels) so invented +// positions cannot silently proliferate without being accounted for. +const MAX_EXPECTED_FALLBACK_WARPS = 80 +check( + warpSources.fallback <= MAX_EXPECTED_FALLBACK_WARPS, + `world: fallback warps (${String(warpSources.fallback)}) within expected threshold (<= ${String(MAX_EXPECTED_FALLBACK_WARPS)})`, +) + console.log( `\n楼梯来源:美术瓦片 ${String(warpSources.tile)},迷宫楼梯间 ${String(warpSources.room)},` + `兜底 ${String(warpSources.fallback)}(涉及 ${String(approximateLevels.size)} 个关卡)`, ) +if (approximateLevels.size > 0) { + const breakdown = [...approximateLevels] + .map(id => `${String(id)} (${graphLevelName.get(id) ?? 'unknown'})`) + .join(', ') + console.log(` 兜底关卡列表: ${breakdown}`) +} console.log(`\n${String(checks - failures)}/${String(checks)} 项断言通过(逐像素比对了 ${(comparedPixels / 1048576).toFixed(1)} MB 的索引数据)`) if (failures > 0) process.exit(1) diff --git a/src/game/level-links.ts b/src/game/level-links.ts index b7b8b63..7d32da6 100644 --- a/src/game/level-links.ts +++ b/src/game/level-links.ts @@ -180,6 +180,8 @@ export interface SceneLinks { * hole in the world, and the verifier reports on this list. */ readonly unplacedEdges: readonly { readonly toLevelId: number; readonly reason: string }[] + /** Diagnostic notes and warnings from link resolution (e.g. fallback warps placed). */ + readonly notes?: readonly string[] } /** diff --git a/src/game/wilderness.ts b/src/game/wilderness.ts index d42c4d9..55e3ba1 100644 --- a/src/game/wilderness.ts +++ b/src/game/wilderness.ts @@ -325,6 +325,8 @@ export interface WildernessResult { /** What the generator did, for reporting and tests. */ readonly stats: Record & { readonly landmarkEntities?: LandmarkEntity[] + readonly notes?: string[] + readonly unresolved?: string[] } }