From 985d85ebb04feba8d498faab1fe370f996a11496 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Fri, 18 Sep 2026 14:26:16 +0000 Subject: [PATCH] =?UTF-8?q?fix(wilderness):=20=E4=BF=AE=E5=A4=8D=20pack-ac?= =?UTF-8?q?t-assets=20=E6=BC=8F=E4=BC=A0=20dt1Libraries=20=E5=8F=8A=20wild?= =?UTF-8?q?erness=20=E8=87=AA=E5=8A=A8=E8=B4=B4=E5=9B=BE=E8=B7=A8=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E6=B1=A1=E6=9F=93=20(Issue=20#113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/pack-act-assets.ts: 在 generateWilderness 中透传 dt1Libraries: resolvedLibraries,保证地面自动过渡与瓦片解析能获得关卡所加载的全部 DT1 库。 - src/game/wilderness.ts: - applyGroundTransitions: 当 options.libraries 为空或未指定时直接返回 0 处过渡,修复 isTileAvailable 无库时无条件返回 true 的漏洞。 - 增加关卡类型与章节编号守卫(非 Act 1 - Wilderness 关卡与非 Act 1 关卡跳过),避免对 Mesa (104)、Desert (43)、Barricade (112, 117) 注入无效瓦片。 - 在 phase 1 与 phase 2 中严格校验 (update.style, update.sequence) 瓦片是否存在于当前 DT1 库中,确保不会写入不存在的过渡瓦片。 - tests/wilderness-ground-blend.test.ts: 更新并补充 auto-tiling 测试,覆盖无库安全回退以及非 Act 1 关卡拦截。 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1 --- scripts/pack-act-assets.ts | 7 +- src/game/wilderness.ts | 62 ++++++++++------- tests/wilderness-ground-blend.test.ts | 99 ++++++++++++++++++++++++++- 3 files changed, 137 insertions(+), 31 deletions(-) diff --git a/scripts/pack-act-assets.ts b/scripts/pack-act-assets.ts index ed686fb..4cb36fa 100644 --- a/scripts/pack-act-assets.ts +++ b/scripts/pack-act-assets.ts @@ -1663,8 +1663,8 @@ for (const entry of LEVELS) { const rows = await getSubstitutions(subType) const shrineRows = await getSubstitutions(subShrine) const libInfo = resolveLevelLibraries(tables, entry.levelId) - const libraries: Dt1[] = [] - for (const name of libInfo.dt1Names) libraries.push(await libraryOf(name)) + const resolvedLibraries: Dt1[] = [] + for (const name of libInfo.dt1Names) resolvedLibraries.push(await libraryOf(name)) // Solved once for the whole world, so all three variants cut the same // seams on the same edges and any variant docks with any neighbour. @@ -1687,9 +1687,10 @@ for (const entry of LEVELS) { substitutions: rows, shrineSubstitutions: shrineRows, gates, + dt1Libraries: resolvedLibraries, }) await bakeDs1Variant( - entry, entry.name, palette, paletteName, libInfo.dt1Names, libraries, + entry, entry.name, palette, paletteName, libInfo.dt1Names, resolvedLibraries, result.level, `generated:${label}`, label, seed, { entrances: (result.stats.entrances ?? []) as WildernessEntrance[], diff --git a/src/game/wilderness.ts b/src/game/wilderness.ts index d808f65..0918280 100644 --- a/src/game/wilderness.ts +++ b/src/game/wilderness.ts @@ -1509,29 +1509,34 @@ export function applyGroundTransitions( options?: GroundTransitionOptions, ): GroundTransitionResult { // Ground transition auto-tiling LUTs (DIRT_PATH_TILE_LUT, WATER_EDGE_TILE_LUT) are specific - // to Act 1 Wilderness dirt roads and river water. Skip for other level types. + // to Act 1 Wilderness dirt roads and river water. Skip for other level types and non-Act-1 levels. if (options?.levelTypeName !== undefined && options.levelTypeName !== 'Act 1 - Wilderness') { return { transitionCells: 0, roadTransitions: 0, waterTransitions: 0 } } + if (options?.levelId !== undefined && (options.levelId < 2 || options.levelId > 7)) { + return { transitionCells: 0, roadTransitions: 0, waterTransitions: 0 } + } - // Build set of available floor tiles if libraries are provided + // Require DT1 libraries to confirm tile availability. If not provided or empty, do not apply transitions. + if (!options?.libraries || options.libraries.length === 0) { + return { transitionCells: 0, roadTransitions: 0, waterTransitions: 0 } + } + + // Build set of available floor tiles from active DT1 libraries const availableTiles = new Set() - if (options?.libraries && options.libraries.length > 0) { - const mask = options.dt1Mask ?? 0xFFFFFFFF - for (let libIndex = 0; libIndex < options.libraries.length; libIndex += 1) { - if ((mask & (1 << libIndex)) === 0) continue - const lib = options.libraries[libIndex] - if (!lib) continue - for (const tile of lib.tiles) { - if (tile.type === 0) { - availableTiles.add(`${String(tile.style)}:${String(tile.sequence)}`) - } + const mask = options.dt1Mask ?? 0xFFFFFFFF + for (let libIndex = 0; libIndex < options.libraries.length; libIndex += 1) { + if ((mask & (1 << libIndex)) === 0) continue + const lib = options.libraries[libIndex] + if (!lib) continue + for (const tile of lib.tiles) { + if (tile.type === 0) { + availableTiles.add(`${String(tile.style)}:${String(tile.sequence)}`) } } } const isTileAvailable = (style: number, sequence: number): boolean => { - if (availableTiles.size === 0) return true return availableTiles.has(`${String(style)}:${String(sequence)}`) } @@ -1572,7 +1577,8 @@ export function applyGroundTransitions( if (!floor || floor.hidden || floor.prop1 === 0) continue if (isRoadCell(floor)) { - if (floor.style === 0) { + // Limit road transition (DIRT_PATH_TILE_LUT) to environments that actually use style 0 dirt roads + if (floor.style === 0 && (options.levelTypeName === undefined || options.levelTypeName === 'Act 1 - Wilderness')) { const roadMask = computeNeighborMask(canvas, x, y, (nCell) => { return isRoadCell(nCell?.floors[0]) }) @@ -1595,20 +1601,21 @@ export function applyGroundTransitions( } } } else if (isWaterCell(floor)) { - const waterMask = computeNeighborMask(canvas, x, y, (nCell) => { - return isWaterCell(nCell?.floors[0]) - }) + // Limit water transition (WATER_EDGE_TILE_LUT) to environments that actually use style 2 water + if (floor.style === 2 && (options.levelTypeName === undefined || options.levelTypeName === 'Act 1 - Wilderness')) { + const waterMask = computeNeighborMask(canvas, x, y, (nCell) => { + return isWaterCell(nCell?.floors[0]) + }) - if (waterMask === 0xFF) { - // Interior deep water - if (floor.style === 2 && floor.sequence !== 24) { - if (isTileAvailable(2, 24)) { - pendingUpdates.push({ x, y, style: 2, sequence: 24, prop1: 2, isWater: true }) + if (waterMask === 0xFF) { + // Interior deep water + if (floor.sequence !== 24) { + if (isTileAvailable(2, 24)) { + pendingUpdates.push({ x, y, style: 2, sequence: 24, prop1: 2, isWater: true }) + } } - } - } else { - // Boundary water cell - if (floor.style === 2) { + } else { + // Boundary water cell const targetSeq = WATER_EDGE_TILE_LUT[waterMask] ?? 0 if (targetSeq !== floor.sequence) { if (isTileAvailable(2, targetSeq)) { @@ -1626,6 +1633,9 @@ export function applyGroundTransitions( let waterTransitions = 0 for (const update of pendingUpdates) { + // Check if the target tile actually exists in the active DT1 libraries before applying + if (!isTileAvailable(update.style, update.sequence)) continue + const cell = canvas.cells[update.y]?.[update.x] if (!cell) continue const floor = cell.floors[0] diff --git a/tests/wilderness-ground-blend.test.ts b/tests/wilderness-ground-blend.test.ts index 3451a57..03a115c 100644 --- a/tests/wilderness-ground-blend.test.ts +++ b/tests/wilderness-ground-blend.test.ts @@ -818,7 +818,15 @@ describe("Wilderness Ground Tile Blending (Issue #52)", () => { } } - const res = applyGroundTransitions(canvas) + const roadDt1 = makeMockDt1( + Array.from({ length: 47 }, (_, i) => ({ type: 0, style: 0, sequence: i })), + ) + const res = applyGroundTransitions(canvas, { + libraries: [roadDt1], + dt1Mask: 1, + levelTypeName: 'Act 1 - Wilderness', + levelId: 2, + }) expect(res.roadTransitions).toBeGreaterThan(0) expect(res.waterTransitions).toBe(0) expect(res.transitionCells).toBe(res.roadTransitions) @@ -861,7 +869,15 @@ describe("Wilderness Ground Tile Blending (Issue #52)", () => { } } - const res = applyGroundTransitions(canvas) + const waterDt1 = makeMockDt1( + Array.from({ length: 25 }, (_, i) => ({ type: 0, style: 2, sequence: i })), + ) + const res = applyGroundTransitions(canvas, { + libraries: [waterDt1], + dt1Mask: 1, + levelTypeName: 'Act 1 - Wilderness', + levelId: 2, + }) expect(res.waterTransitions).toBeGreaterThan(0) expect(res.transitionCells).toBe(res.waterTransitions) @@ -881,6 +897,85 @@ describe("Wilderness Ground Tile Blending (Issue #52)", () => { } }) + test("returns 0 transitions when dt1Libraries are undefined or empty (Issue #113)", () => { + const canvas = createCanvas(9, 9, 1, 1, 0) + for (let y = 0; y < 9; y += 1) { + for (let x = 0; x < 9; x += 1) { + setFloor(canvas, x, y, { style: 0, sequence: 0 }) + } + } + for (let ry = 3; ry <= 5; ry += 1) { + for (let rx = 3; rx <= 5; rx += 1) { + setFloor(canvas, rx, ry, { style: 0, sequence: 1 }) + } + } + + // No options -> 0 transitions + const resNoOptions = applyGroundTransitions(canvas) + expect(resNoOptions.transitionCells).toBe(0) + expect(resNoOptions.roadTransitions).toBe(0) + expect(resNoOptions.waterTransitions).toBe(0) + + // Empty libraries array -> 0 transitions + const resEmptyLibs = applyGroundTransitions(canvas, { libraries: [] }) + expect(resEmptyLibs.transitionCells).toBe(0) + expect(resEmptyLibs.roadTransitions).toBe(0) + }) + + test("does not inject transition tiles into non-Act-1 environments (Issue #113)", () => { + const canvas = createCanvas(9, 9, 1, 1, 0) + for (let y = 0; y < 9; y += 1) { + for (let x = 0; x < 9; x += 1) { + setFloor(canvas, x, y, { style: 0, sequence: 0 }) + } + } + for (let ry = 3; ry <= 5; ry += 1) { + for (let rx = 3; rx <= 5; rx += 1) { + setFloor(canvas, rx, ry, { style: 0, sequence: 1 }) + } + } + + const roadDt1 = makeMockDt1( + Array.from({ length: 47 }, (_, i) => ({ type: 0, style: 0, sequence: i })), + ) + + // Mesa 1 (levelId 104, Act 4 - Mesa) + const resMesa = applyGroundTransitions(canvas, { + libraries: [roadDt1], + dt1Mask: 1, + levelTypeName: 'Act 4 - Mesa', + levelId: 104, + }) + expect(resMesa.transitionCells).toBe(0) + + // Desert 3 (levelId 43, Act 2 - Desert) + const resDesert = applyGroundTransitions(canvas, { + libraries: [roadDt1], + dt1Mask: 1, + levelTypeName: 'Act 2 - Desert', + levelId: 43, + }) + expect(resDesert.transitionCells).toBe(0) + + // Barricade 2 (levelId 112, Act 5 - Barricade) + const resBarricade2 = applyGroundTransitions(canvas, { + libraries: [roadDt1], + dt1Mask: 1, + levelTypeName: 'Act 5 - Barricade', + levelId: 112, + }) + expect(resBarricade2.transitionCells).toBe(0) + + // Barricade Snow (levelId 117, Act 5 - Barricade) + const resBarricadeSnow = applyGroundTransitions(canvas, { + libraries: [roadDt1], + dt1Mask: 1, + levelTypeName: 'Act 5 - Barricade', + levelId: 117, + }) + expect(resBarricadeSnow.transitionCells).toBe(0) + }) + test("WATER_EDGE_TILE_LUT has exactly 256 entries with 24 at 0xFF", () => { expect(WATER_EDGE_TILE_LUT).toHaveLength(256) expect(WATER_EDGE_TILE_LUT[0xFF]).toBe(24)