refactor(wilderness): 移除凭空发明的程序化十字矮石墙网络,回归原版自然地貌阻隔 (#65)

- 将 stampThemedDividers 重构为兼容性 no-op,完全废弃人为硬编码的十字切分矮石墙
- generateWilderness 移除 stampOutdoorDividers 调用,回归原版 Act 1-5 依据 DS1 预设边界及地形生成的自然阻隔机制
- 更新 wilderness 单元与多幕道路测试,验证 0 人造矮石墙及真实地貌阻隔保留

TAG=agy
CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
troytt 2026-09-18 01:38:00 +00:00
parent a7e526da64
commit d0efb04395
4 changed files with 44 additions and 184 deletions

View File

@ -3327,145 +3327,24 @@ export function stampThemedRoads(
}
/**
* General outdoor divider network laying connected stone walls, sandstone berms,
* or balustrades with proper corner joins, gate archways, and endcaps.
* General outdoor divider network.
* @deprecated Procedural cross divider networks were removed in Issue #65 to restore
* Blizzard DRLG fidelity. In original Diablo II, wilderness barriers are formed naturally
* by cliff borders, water bodies, and LvlSub substitutions without artificial cross stone walls.
*/
export function stampThemedDividers(
canvas: Canvas,
gridWidth: number,
gridHeight: number,
inX: number,
outY: number,
placedRects: readonly PlacedPresetRect[],
rng: Rng,
theme: OutdoorDividerTheme,
_canvas: Canvas,
_gridWidth: number,
_gridHeight: number,
_inX: number,
_outY: number,
_placedRects: readonly PlacedPresetRect[],
_rng: Rng,
_theme: OutdoorDividerTheme,
stats?: WildernessStats,
): void {
if (gridWidth < 6 || gridHeight < 6) return
const wallStyle = theme.wallStyle
const wallProp1 = theme.wallProp1 ?? 129
const placedCells = new Set<string>()
const makeWall = (type: number, sequence: number): Ds1Wall => ({
prop1: wallProp1,
sequence,
style: wallStyle,
type,
unknown1: 0,
unknown2: 0,
hidden: false,
})
const placeWall = (x: number, y: number, wall: Ds1Wall): void => {
const cell = canvas.cells[y]?.[x]
if (!cell) return
const targetWalls = cell.walls as Ds1Wall[]
if (targetWalls.length > 0) {
targetWalls[0] = wall
} else {
targetWalls.push(wall)
}
placedCells.add(`${x},${y}`)
}
// Offset divider lines by 2 blocks (16 tiles) from the main road spine
const offsetBlockY = rng.int(0, 1) === 0 ? -2 : 2
const offsetBlockX = rng.int(0, 1) === 0 ? -2 : 2
const wallY = Math.max(12, Math.min(canvas.height - 12, (outY + offsetBlockY) * TILES_PER_BLOCK))
const wallX = Math.max(12, Math.min(canvas.width - 12, (inX + offsetBlockX) * TILES_PER_BLOCK))
const isCellBlockedForWall = (x: number, y: number): boolean => {
if (x < 6 || x >= canvas.width - 6 || y < 6 || y >= canvas.height - 6) return true
// Leave 2-tile margin around all placed special presets
for (const r of placedRects) {
if (x >= r.x - 2 && x < r.x + r.w + 2 && y >= r.y - 2 && y < r.y + r.h + 2) {
return true
}
}
// Leave 3-tile wide archway gate openings wherever a road, bridge, or water passes
for (let dy = -1; dy <= 1; dy += 1) {
for (let dx = -1; dx <= 1; dx += 1) {
const c = canvas.cells[y + dy]?.[x + dx]
if (!c) continue
if (c.floors.some(f => !f.hidden && (f.style === 5 || f.style === 1 || f.style === 4 || f.style === 2 || f.style === 3 || theme.roadStyles?.includes(f.style)))) {
return true
}
}
}
const cell = canvas.cells[y]?.[x]
if (!cell) return true
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) return true
return false
}
// Build valid tile masks along wallY (EW) and wallX (NS), inserting periodic pasture gates
const validEW: boolean[] = Array.from({ length: canvas.width }, (_, x) => {
const isPastureGate = (x % 24) >= 11 && (x % 24) <= 13 && Math.abs(x - wallX) > 3
return !isPastureGate && !isCellBlockedForWall(x, wallY)
})
const validNS: boolean[] = Array.from({ length: canvas.height }, (_, y) => {
const isPastureGate = (y % 24) >= 11 && (y % 24) <= 13 && Math.abs(y - wallY) > 3
return !isPastureGate && !isCellBlockedForWall(wallX, y)
})
const extractRuns = (mask: readonly boolean[]): Array<{ start: number; end: number }> => {
const runs: Array<{ start: number; end: number }> = []
let start = -1
for (let i = 0; i < mask.length; i += 1) {
if (mask[i]) {
if (start === -1) start = i
} else if (start !== -1) {
if (i - start >= 4) runs.push({ start, end: i - 1 })
start = -1
}
}
if (start !== -1 && mask.length - start >= 4) {
runs.push({ start, end: mask.length - 1 })
}
return runs
}
const ewRuns = extractRuns(validEW)
const nsRuns = extractRuns(validNS)
const ewCoversCorner = ewRuns.some(r => wallX >= r.start && wallX <= r.end)
const nsCoversCorner = nsRuns.some(r => wallY >= r.start && wallY <= r.end)
const hasCornerJoin = ewCoversCorner && nsCoversCorner
// Stamp East-West divider wall segments (`type = 2`)
for (const run of ewRuns) {
for (let x = run.start; x <= run.end; x += 1) {
if (x === wallX && hasCornerJoin) {
placeWall(x, wallY, makeWall(3, 0)) // Corner / Cross join (`type=3`)
} else if (x === run.start) {
placeWall(x, wallY, makeWall(2, 1)) // West / Right-of-gap End-Cap (`type=2, seq=1`)
} else if (x === run.end) {
placeWall(x, wallY, makeWall(2, 2)) // East / Left-of-gap End-Cap (`type=2, seq=2`)
} else {
placeWall(x, wallY, makeWall(2, 0)) // Continuous EW Wall (`type=2, seq=0`)
}
}
}
// Stamp North-South divider wall segments (`type = 1`)
for (const run of nsRuns) {
for (let y = run.start; y <= run.end; y += 1) {
if (y === wallY && hasCornerJoin) {
placeWall(wallX, y, makeWall(3, 0)) // Corner / Cross join (`type=3`)
} else if (y === run.start) {
placeWall(wallX, y, makeWall(1, 1)) // North / Bottom-of-gap End-Cap (`type=1, seq=1`)
} else if (y === run.end) {
placeWall(wallX, y, makeWall(1, 2)) // South / Top-of-gap End-Cap (`type=1, seq=2`)
} else {
placeWall(wallX, y, makeWall(1, 0)) // Continuous NS Wall (`type=1, seq=0`)
}
}
}
if (stats) {
stats.proceduralDividerCells = (stats.proceduralDividerCells ?? 0) + placedCells.size
stats.proceduralWallCells = (stats.proceduralWallCells ?? 0) + placedCells.size
stats.proceduralDividerCells = 0
}
}
@ -5480,20 +5359,6 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
request.levelsTable,
stats,
)
stampOutdoorDividers(
canvas,
act,
request.levelId,
gridWidth,
gridHeight,
inX,
outY,
allPlacedRects,
rng,
request.levelTypeName,
request.levelsTable,
stats,
)
// Act 2 Desert Topography: Canyons, oasis pools, sand caravan trails, tomb entrances (DRLGOUTDESR_InitAct2OutdoorLevel)
if (

View File

@ -194,7 +194,7 @@ describe("Wilderness Features & Border Rules", () => {
}
}
expect(stoneWallCount).toBeGreaterThan(20)
expect(stoneWallCount).toBe(0)
expect(dirtRoadCount).toBeGreaterThan(30)
})
@ -364,7 +364,7 @@ describe("Wilderness Features & Border Rules", () => {
expect(result.level.cells[75]![44]!.floors[0]!.style).toBe(5)
expect(result.level.cells[44]![75]!.floors[0]!.style).toBe(5)
// 3. Verify interior stone wall segments (style=2, prop1=129) use continuous sequence=0 and end-caps sequence=1/2
// 3. Verify no artificial interior stone wall segments (style=2, prop1=129) are stamped (Issue #65)
let foundContinuousEW = false
let foundEndCapEW = false
for (let y = 10; y < 70; y += 1) {
@ -376,8 +376,8 @@ describe("Wilderness Features & Border Rules", () => {
}
}
}
expect(foundContinuousEW).toBe(true)
expect(foundEndCapEW).toBe(true)
expect(foundContinuousEW).toBe(false)
expect(foundEndCapEW).toBe(false)
})
test("records unresolved presets and notes when requested preset piece is missing", () => {

View File

@ -116,7 +116,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
expect(spurFound).toBe(true)
})
it('stampAct2SandstoneBerms paints sandstone berms with style: 1, prop1: 129, corner joins, and road archway breaks', () => {
it('stampAct2SandstoneBerms is a no-op removing artificial cross dividers (Issue #65)', () => {
const canvas = createCanvas(width, height, 1, 1, 0)
const rng = new Rng(42)
const placedRects: PlacedPresetRect[] = []
@ -126,12 +126,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
stampAct2SandstoneBerms(canvas, gridW, gridH, inX, outY, placedRects, rng)
const bermWalls = countWallTiles(canvas, 1, 129)
expect(bermWalls).toBeGreaterThan(20)
// Berms should not block the road where the road crosses the divider
// Highway crosses dividing lines at (inX * 8 + 4, outY * 8 + 4)
const crossRoad = canvas.cells[outY * TILES_PER_BLOCK + 4]?.[inX * TILES_PER_BLOCK + 4]
expect(crossRoad?.walls.some(w => w.style === 1 && w.prop1 === 129)).toBe(false)
expect(bermWalls).toBe(0)
})
})
@ -149,7 +144,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
expect(avenueFloors).toBeGreaterThan(50)
})
it('stampAct3KurastBalustrades paints balustrades with style: 1, prop1: 129', () => {
it('stampAct3KurastBalustrades is a no-op removing artificial cross dividers (Issue #65)', () => {
const canvas = createCanvas(width, height, 1, 1, 0)
const rng = new Rng(100)
const placedRects: PlacedPresetRect[] = []
@ -158,7 +153,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
stampAct3KurastBalustrades(canvas, gridW, gridH, inX, outY, placedRects, rng)
const balustradeWalls = countWallTiles(canvas, 1, 129)
expect(balustradeWalls).toBeGreaterThan(20)
expect(balustradeWalls).toBe(0)
})
})
@ -176,7 +171,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
expect(snowRoadFloors).toBeGreaterThan(50)
})
it('stampAct5FrozenTrailDivides paints frozen trail divides with style: 2, prop1: 129', () => {
it('stampAct5FrozenTrailDivides is a no-op removing artificial cross dividers (Issue #65)', () => {
const canvas = createCanvas(width, height, 1, 1, 0)
const rng = new Rng(555)
const placedRects: PlacedPresetRect[] = []
@ -185,49 +180,49 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
stampAct5FrozenTrailDivides(canvas, gridW, gridH, inX, outY, placedRects, rng)
const frozenDivides = countWallTiles(canvas, 2, 129)
expect(frozenDivides).toBeGreaterThan(20)
expect(frozenDivides).toBe(0)
})
})
describe('Unified Dispatchers: stampOutdoorRoads & stampOutdoorDividers', () => {
it('dispatches Act 2 Desert based on levelId 41', () => {
it('dispatches Act 2 Desert based on levelId 41 without artificial cross dividers', () => {
const canvas = createCanvas(width, height, 1, 1, 0)
const rng = new Rng(123)
stampOutdoorRoads(canvas, 1, 41, gridW, gridH, inX, outY, [], rng)
stampOutdoorDividers(canvas, 1, 41, gridW, gridH, inX, outY, [], rng)
expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40)
expect(countWallTiles(canvas, 1, 129)).toBeGreaterThan(15)
expect(countWallTiles(canvas, 1, 129)).toBe(0)
})
it('dispatches Act 3 Kurast based on levelId 80', () => {
it('dispatches Act 3 Kurast based on levelId 80 without artificial cross dividers', () => {
const canvas = createCanvas(width, height, 1, 1, 0)
const rng = new Rng(123)
stampOutdoorRoads(canvas, 1, 80, gridW, gridH, inX, outY, [], rng)
stampOutdoorDividers(canvas, 1, 80, gridW, gridH, inX, outY, [], rng)
expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40)
expect(countWallTiles(canvas, 1, 129)).toBeGreaterThan(15)
expect(countWallTiles(canvas, 1, 129)).toBe(0)
})
it('dispatches Act 5 Snow based on levelId 111', () => {
it('dispatches Act 5 Snow based on levelId 111 without artificial cross dividers', () => {
const canvas = createCanvas(width, height, 1, 1, 0)
const rng = new Rng(123)
stampOutdoorRoads(canvas, 1, 111, gridW, gridH, inX, outY, [], rng)
stampOutdoorDividers(canvas, 1, 111, gridW, gridH, inX, outY, [], rng)
expect(countFloorTiles(canvas, 1, 194)).toBeGreaterThan(40)
expect(countWallTiles(canvas, 2, 129)).toBeGreaterThan(15)
expect(countWallTiles(canvas, 2, 129)).toBe(0)
})
it('dispatches Act 1 Dirt Roads based on levelId 3', () => {
it('dispatches Act 1 Dirt Roads based on levelId 3 without artificial cross dividers', () => {
const canvas = createCanvas(width, height, 1, 1, 0)
const rng = new Rng(123)
stampOutdoorRoads(canvas, 1, 3, gridW, gridH, inX, outY, [], rng)
stampOutdoorDividers(canvas, 1, 3, gridW, gridH, inX, outY, [], rng)
expect(countFloorTiles(canvas, 5, 194)).toBeGreaterThan(40)
expect(countWallTiles(canvas, 2, 129)).toBeGreaterThan(15)
expect(countWallTiles(canvas, 2, 129)).toBe(0)
})
it('dispatches based on levelTypeName when levelId is generic', () => {
@ -235,19 +230,19 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
stampOutdoorRoads(c2, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 2 - Desert')
stampOutdoorDividers(c2, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 2 - Desert')
expect(countFloorTiles(c2, 1, 194)).toBeGreaterThan(40)
expect(countWallTiles(c2, 1, 129)).toBeGreaterThan(15)
expect(countWallTiles(c2, 1, 129)).toBe(0)
const c3 = createCanvas(width, height, 1, 1, 0)
stampOutdoorRoads(c3, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 3 - Kurast')
stampOutdoorDividers(c3, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 3 - Kurast')
expect(countFloorTiles(c3, 1, 194)).toBeGreaterThan(40)
expect(countWallTiles(c3, 1, 129)).toBeGreaterThan(15)
expect(countWallTiles(c3, 1, 129)).toBe(0)
const c5 = createCanvas(width, height, 1, 1, 0)
stampOutdoorRoads(c5, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 5 - Barricade')
stampOutdoorDividers(c5, 1, 999, gridW, gridH, inX, outY, [], new Rng(1), 'Act 5 - Barricade')
expect(countFloorTiles(c5, 1, 194)).toBeGreaterThan(40)
expect(countWallTiles(c5, 2, 129)).toBeGreaterThan(15)
expect(countWallTiles(c5, 2, 129)).toBe(0)
})
it('dispatches based on act number fallback', () => {
@ -255,7 +250,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
stampOutdoorRoads(c2, 2, 999, gridW, gridH, inX, outY, [], new Rng(1))
stampOutdoorDividers(c2, 2, 999, gridW, gridH, inX, outY, [], new Rng(1))
expect(countFloorTiles(c2, 1, 194)).toBeGreaterThan(40)
expect(countWallTiles(c2, 1, 129)).toBeGreaterThan(15)
expect(countWallTiles(c2, 1, 129)).toBe(0)
})
it('strictly skips Cow Level (levelId 39)', () => {
@ -279,7 +274,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
levels: [borderDs1],
}
it('generates Act 2 Rocky Waste (Level 41) with caravan routes and sandstone berms', () => {
it('generates Act 2 Rocky Waste (Level 41) with caravan routes and authentic canyon topography walls', () => {
const result = generateWilderness({
levelId: 41,
levelName: 'Rocky Waste',
@ -309,7 +304,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
expect(sandstoneBerms).toBeGreaterThan(15)
})
it('generates Act 3 Kurast Bazaar (Level 80) with Kurast avenues and balustrades', () => {
it('generates Act 3 Kurast Bazaar (Level 80) with Kurast avenues and authentic causeway balustrades', () => {
const act3Border: WildernessPiece = {
name: 'Act 3 - Kurast Border',
border: true,
@ -344,7 +339,7 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
expect(balustrades).toBeGreaterThan(15)
})
it('generates Act 5 Frigid Highlands (Level 111) with snow roads and frozen trail divides', () => {
it('generates Act 5 Frigid Highlands (Level 111) with snow roads and authentic siege barricade walls', () => {
const act5Border: WildernessPiece = {
name: 'Act 5 - Barricade Border',
border: true,

View File

@ -280,8 +280,8 @@ describe('BordType 0 really is one cluster for the whole level', () => {
})
})
describe('Procedural terrain pass telemetry tracks real cell counts (Issue #62)', () => {
test('Act 1 wilderness tracks procedural road and divider wall cells matching canvas', () => {
describe('Procedural terrain pass telemetry tracks real cell counts (Issue #62 & #65)', () => {
test('Act 1 wilderness tracks procedural road cells with zero artificial dividers (Issue #65)', () => {
const result = generateWilderness({
levelId: 2,
levelName: 'Cold Plains',
@ -296,10 +296,10 @@ describe('Procedural terrain pass telemetry tracks real cell counts (Issue #62)'
})
expect(result.stats.proceduralRoadCells).toBeGreaterThan(0)
expect(result.stats.proceduralDividerCells).toBeGreaterThan(0)
expect(result.stats.proceduralWallCells).toBe(result.stats.proceduralDividerCells)
expect(result.stats.proceduralDividerCells).toBe(0)
expect(result.stats.proceduralWallCells).toBe(0)
// Count actual divider wall cells on canvas (walls with style 2, prop1 129)
// Verify 0 artificial divider wall cells on canvas (walls with style 2, prop1 129)
let wallCount = 0
for (let y = 0; y < result.level.height; y += 1) {
for (let x = 0; x < result.level.width; x += 1) {
@ -309,7 +309,7 @@ describe('Procedural terrain pass telemetry tracks real cell counts (Issue #62)'
}
}
}
expect(result.stats.proceduralWallCells).toBe(wallCount)
expect(wallCount).toBe(0)
})
test('Act 2 desert tracks procedural roads, water (oasis), and walls (canyon + berms)', () => {