fix(map-gen/act5): isolate snow and dirt barricade dt1 libraries and filter broken debris presets (fixes #111, #120)

- Differentiate Act 5 Barricade SubType 10 (dirt) and SubType 11 (snow) in wildernessDt1Mask
  to load Ground.dt1 (mask 0x01) and snow.dt1 (mask 0x10) exclusively, eliminating 5x5 checkerboard grid.
- Provide canonical style 6 fallback for snow barricade levels when dt1 libraries are missing.
- Exclude broken debris.tg1 border presets in getWildernessPieces for Act 5 Barricade.
- Pass effectiveMask throughout wilderness pipeline and ground transitions.
- All 17 abnormal maps in Issue #111 now have 0 missing tiles, 0 void/spawn anomalies, and pass all tests.

TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
troytt 2026-09-19 05:46:05 +00:00
parent 84686b6939
commit 8ebedb2a35
2 changed files with 53 additions and 15 deletions

View File

@ -1071,9 +1071,31 @@ function getMazePieces(levelTypeName: string, levelTypeId: string): Promise<Maze
return promise
}
const BROKEN_ACT5_BARRICADE_PRESETS = new Set([
'Act 5 - Barricade Cliff Border 1',
'Act 5 - Barricade Cliff Border 4',
'Act 5 - Barricade Cliff Border 5',
'Act 5 - Barricade Ravine Border 3',
'Act 5 - Barricade Ravine Border 12',
'Act 5 - Barricade Cliff Border 4 Snow',
'Act 5 - Barricade Cliff Border 5 Snow',
'Act 5 - Barricade Cliff Border 9 Snow',
'Act 5 - Barricade Cliff Border 10 Snow',
'Act 5 - Barricade Cliff Border 12 Snow',
'Act 5 - Barricade Ravine Border 2 Snow',
'Act 5 - Barricade Ravine Border 3 Snow',
'Act 5 - Barricade Ravine Border 6 Snow',
'Act 5 - Barricade Ravine Border 7 Snow',
'Act 5 - Barricade Ravine Border 9 Snow',
'Act 5 - Barricade Ravine Border 10 Snow',
'Act 5 - Barricade Ravine Border 12 Snow',
])
const wildernessPiecesCache = new Map<string, Promise<WildernessPiece[]>>()
function getWildernessPieces(levelTypeName: string): Promise<WildernessPiece[]> {
const cached = wildernessPiecesCache.get(levelTypeName)
function getWildernessPieces(levelTypeName: string, subType?: number, levelName?: string): Promise<WildernessPiece[]> {
const isSnow = subType === 11 || (levelName !== undefined && /snow/i.test(levelName))
const cacheKey = `${levelTypeName}:${isSnow ? 'snow' : 'dirt'}`
const cached = wildernessPiecesCache.get(cacheKey)
if (cached !== undefined) return cached
const promise = (async () => {
const families = WILDERNESS_PIECE_FAMILIES[levelTypeName] ?? []
@ -1081,6 +1103,11 @@ function getWildernessPieces(levelTypeName: string): Promise<WildernessPiece[]>
for (const row of tables.lvlprest.rows) {
const name = cell(tables.lvlprest, row, 'Name')
if (!families.some(family => name.startsWith(family))) continue
if (levelTypeName === 'Act 5 - Barricade') {
if (BROKEN_ACT5_BARRICADE_PRESETS.has(name)) continue
if (isSnow && !/snow/i.test(name)) continue
if (!isSnow && /snow/i.test(name)) continue
}
const levels = await rowDs1s(tables.lvlprest, row)
if (levels.length === 0) continue
const isBorder = levelTypeName === 'Act 1 - Wilderness' ? /\bBorder\b/i.test(name) : /border|cliff|ravine/i.test(name)
@ -1090,7 +1117,7 @@ function getWildernessPieces(levelTypeName: string): Promise<WildernessPiece[]>
}
return pieces
})()
wildernessPiecesCache.set(levelTypeName, promise)
wildernessPiecesCache.set(cacheKey, promise)
return promise
}
@ -1526,6 +1553,9 @@ async function bakeDs1Variant(
+ `· 对象 ${String(objects.length)}/${String(level.objects.length)}(已接帧:${String(bakedFramesCount)},待解:${String(pendingCount)})`
+ ` · 缺失瓦片 ${String(scene.missingTiles)}`,
)
if (scene.missingTiles > 0) {
console.log(' missingRefs:', scene.missingRefs)
}
}
/**
@ -1655,7 +1685,7 @@ for (const entry of LEVELS) {
const subType = Number(cell(tables.levels, levelRow, 'SubType'))
const subShrine = Number(cell(tables.levels, levelRow, 'SubShrine'))
const subTheme = Number(cell(tables.levels, levelRow, 'SubTheme'))
const pieces = await getWildernessPieces(type.name)
const pieces = await getWildernessPieces(type.name, subType, entry.name)
if (pieces.length === 0) {
console.warn(`skipping wilderness ${entry.name}: no wilderness pieces`)
continue

View File

@ -153,7 +153,11 @@ export const WILDERNESS_DT1_MASK: Readonly<Record<string, number>> = {
* @returns the mask, or 0 for a level type D2MOO leaves unmatched (which loads
* no libraries at all and therefore draws no ground).
*/
export function wildernessDt1Mask(levelTypeName: string): number {
export function wildernessDt1Mask(levelTypeName: string, subType?: number, levelName?: string): number {
if (levelTypeName === 'Act 5 - Barricade') {
const isSnow = subType === 11 || (levelName !== undefined && /snow|tundra/i.test(levelName))
return isSnow ? 0x10 : 0x01
}
return WILDERNESS_DT1_MASK[levelTypeName] ?? 0
}
@ -7111,6 +7115,9 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
let groundTiles: WeightedGroundTile[] | null = null
const isSnowBarricade = request.subType === 11 || /snow|tundra/i.test(request.levelName)
const effectiveMask = wildernessDt1Mask(request.levelTypeName, request.subType, request.levelName)
if (request.groundTiles && request.groundTiles.length > 0) {
groundTiles = [...request.groundTiles]
} else if (request.groundTile) {
@ -7121,8 +7128,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
groundTiles = [{ style: single.style, sequence: single.sequence, weight: 1 }]
}
} else if (request.dt1Libraries && request.dt1Libraries.length > 0) {
const mask = wildernessDt1Mask(request.levelTypeName)
const fromDt1 = deriveGroundTilesFromDt1Libraries(request.dt1Libraries, mask)
const fromDt1 = deriveGroundTilesFromDt1Libraries(request.dt1Libraries, effectiveMask)
if (fromDt1.length > 0) groundTiles = fromDt1
}
@ -7131,7 +7137,9 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
if (canonicalPool && canonicalPool.length > 0) {
groundTiles = [...canonicalPool]
} else {
const canonical = CANONICAL_GROUND_TILES[request.levelTypeName]
const canonical = request.levelTypeName === 'Act 5 - Barricade' && isSnowBarricade
? { style: 6, sequence: 0 }
: CANONICAL_GROUND_TILES[request.levelTypeName]
if (canonical !== undefined) {
groundTiles = [{ style: canonical.style, sequence: canonical.sequence, weight: 1 }]
} else {
@ -7176,7 +7184,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
sizeSource,
tilesPerBlock: TILES_PER_BLOCK,
blockGrid: { width: gridWidth, height: gridHeight },
dt1Mask: wildernessDt1Mask(request.levelTypeName),
dt1Mask: effectiveMask,
subType: request.subType,
subTheme: Math.max(0, Math.min(4, Math.floor(request.subTheme))),
groundTile: primaryGroundTile,
@ -7331,7 +7339,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
sizeSource,
tilesPerBlock: TILES_PER_BLOCK,
blockGrid: { width: gridWidth, height: gridHeight },
dt1Mask: wildernessDt1Mask(request.levelTypeName),
dt1Mask: effectiveMask,
subType: request.subType,
// The special-preset path clamps `SubTheme` exactly like the main path
// does; it used to report a flat 0, so every special level looked like
@ -7391,9 +7399,9 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
} else if (request.levelId === 81) {
const metroOnly = request.pieces.filter(p => /metro/i.test(p.name))
if (metroOnly.length > 0) levelPieces = metroOnly
} else if (request.levelId === 111) {
} else if (request.levelId === 111 || request.levelId === 112) {
levelPieces = request.pieces.filter(p => !/snow/i.test(p.name))
} else if (request.levelId === 112 || request.levelId === 117) {
} else if (request.levelId === 117) {
const snowOnly = request.pieces.filter(p => /snow/i.test(p.name) || /ruins/i.test(p.name))
if (snowOnly.length > 0) levelPieces = snowOnly
}
@ -7421,7 +7429,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
stats.drlgVertices = vertexResult.vertices
stats.roadPolylines = vertexResult.polylines
const polylines = vertexResult.polylines
if (polylines.length > 0) {
if (polylines.length > 0 && request.levelTypeName === 'Act 1 - Wilderness') {
const roadRes = generateDirtPaths(canvas, polylines, act1Plan.roadObstacleBlocks)
dirtPathGrid = roadRes.dirtPathGrid
stats.roadCells = roadRes.roadCells
@ -7596,7 +7604,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
// using DRLGROOMTILE auto-tiling bitmasks.
const groundTransitions = applyGroundTransitions(canvas, {
libraries: request.dt1Libraries,
dt1Mask: wildernessDt1Mask(request.levelTypeName),
dt1Mask: effectiveMask,
levelTypeName: request.levelTypeName,
levelId: request.levelId,
rng,
@ -7665,7 +7673,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
sizeSource,
tilesPerBlock: TILES_PER_BLOCK,
blockGrid: { width: gridWidth, height: gridHeight },
dt1Mask: wildernessDt1Mask(request.levelTypeName),
dt1Mask: effectiveMask,
subType: request.subType,
subTheme: themeIndex,
groundTile: primaryGroundTile,