fix(wilderness): 修复 pack-act-assets 漏传 dt1Libraries 及 wilderness 自动贴图跨章节污染 (Issue #113)
- 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
This commit is contained in:
parent
370dcc05b1
commit
985d85ebb0
|
|
@ -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[],
|
||||
|
|
|
|||
|
|
@ -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<string>()
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue