fix(wilderness): prevent destructive wiping of walls and presets in roads and bridges
1. Protect placed presets and preset walls from being wiped in stampThemedRoads: check placedRects in paintRoadTile, ensuring preset walls are never cleared. 2. Protect preset floors and walls in stampAct3KurastCauseway so artist DS1 tiles are not overwritten by procedural causeway passes; wire specialPlacedRects. 3. Protect placed preset walls and floors in synthesizeBridgePlanks in Act 3 jungles, recording placedRects for all jungle pieces. 4. Add unit tests for Causeway preset preservation and road preset wall preservation. TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
37dc936169
commit
fa307bff46
|
|
@ -418,6 +418,7 @@ export function stampAct3KurastCauseway(
|
|||
canvas: Canvas,
|
||||
pieces: readonly WildernessPiece[],
|
||||
stats: WildernessStats,
|
||||
specialPlacedRects?: PlacedPresetRect[],
|
||||
): Set<string> {
|
||||
const placed = new Set<string>()
|
||||
const piece = pieces.find(p => p.name === 'Act 3 - Bridge')
|
||||
|
|
@ -427,6 +428,15 @@ export function stampAct3KurastCauseway(
|
|||
stampDs1(canvas, ds1, 0, 0)
|
||||
placed.add('Act 3 - Bridge')
|
||||
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
||||
if (specialPlacedRects) {
|
||||
specialPlacedRects.push({
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: ds1.width,
|
||||
h: ds1.height,
|
||||
name: 'Act 3 - Bridge',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Construct / ensure canonical causeway layout:
|
||||
|
|
@ -438,6 +448,15 @@ export function stampAct3KurastCauseway(
|
|||
|
||||
for (let y = 0; y < canvas.height; y += 1) {
|
||||
for (let x = 0; x < canvas.width; x += 1) {
|
||||
if (ds1 && x >= 0 && x < ds1.width && y >= 0 && y < ds1.height) {
|
||||
const ds1Cell = ds1.cells[y]?.[x]
|
||||
const hasFloor = ds1Cell?.floors && ds1Cell.floors.length > 0 && !ds1Cell.floors[0]?.hidden
|
||||
const hasWall = ds1Cell?.walls && ds1Cell.walls.length > 0 && !ds1Cell.walls[0]?.hidden
|
||||
if (hasFloor || hasWall) {
|
||||
// Keep artist-placed preset tiles from ds1; do not destructively wipe
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (x >= walkwayLeft && x <= walkwayRight) {
|
||||
// Paved stone causeway: style 5, prop1 194
|
||||
paintFloor(canvas, x, y, makeFloor(5, 0, 194), true)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
import type { Ds1, Ds1Cell, Ds1Floor, Ds1Wall } from '../formats/ds1.ts'
|
||||
import { Rng } from './rng.ts'
|
||||
import type { Canvas, SubstitutionReport, WildernessPiece, WildernessStats } from './wilderness.ts'
|
||||
import type { Canvas, PlacedPresetRect, SubstitutionReport, WildernessPiece, WildernessStats } from './wilderness.ts'
|
||||
|
||||
/** Connection between two Act 3 zones. */
|
||||
export interface JungleZoneConnection {
|
||||
|
|
@ -242,6 +242,7 @@ function synthesizeBridgePlanks(
|
|||
bridgeX: number,
|
||||
bridgeY: number,
|
||||
orientation: 'horizontal' | 'vertical',
|
||||
bridgeDs1?: Ds1,
|
||||
): void {
|
||||
const makeBridgeFloor = (): Ds1Floor => ({
|
||||
prop1: 2,
|
||||
|
|
@ -261,6 +262,14 @@ function synthesizeBridgePlanks(
|
|||
if (cy >= 0 && cy < canvas.height && cx >= 0 && cx < canvas.width) {
|
||||
const row = canvas.cells[cy]
|
||||
if (row?.[cx]) {
|
||||
if (bridgeDs1 && dx < bridgeDs1.width && dy < bridgeDs1.height) {
|
||||
const ds1Cell = bridgeDs1.cells[dy]?.[dx]
|
||||
const hasPresetWall = ds1Cell?.walls && ds1Cell.walls.length > 0 && !ds1Cell.walls[0]?.hidden
|
||||
const hasPresetFloor = ds1Cell?.floors && ds1Cell.floors.length > 0 && !ds1Cell.floors[0]?.hidden && ds1Cell.floors[0]?.style !== 1
|
||||
if (hasPresetWall || hasPresetFloor) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
;(row[cx]!.floors as Ds1Floor[])[0] = makeBridgeFloor()
|
||||
}
|
||||
}
|
||||
|
|
@ -274,6 +283,14 @@ function synthesizeBridgePlanks(
|
|||
if (cy >= 0 && cy < canvas.height && cx >= 0 && cx < canvas.width) {
|
||||
const row = canvas.cells[cy]
|
||||
if (row?.[cx]) {
|
||||
if (bridgeDs1 && dx < bridgeDs1.width && dy < bridgeDs1.height) {
|
||||
const ds1Cell = bridgeDs1.cells[dy]?.[dx]
|
||||
const hasPresetWall = ds1Cell?.walls && ds1Cell.walls.length > 0 && !ds1Cell.walls[0]?.hidden
|
||||
const hasPresetFloor = ds1Cell?.floors && ds1Cell.floors.length > 0 && !ds1Cell.floors[0]?.hidden && ds1Cell.floors[0]?.style !== 1
|
||||
if (hasPresetWall || hasPresetFloor) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
;(row[cx]!.floors as Ds1Floor[])[0] = makeBridgeFloor()
|
||||
}
|
||||
}
|
||||
|
|
@ -343,6 +360,7 @@ export function DRLG_GenerateJungles(
|
|||
rng: Rng,
|
||||
stats: WildernessStats,
|
||||
stampDs1Fn: (canvas: Canvas, source: Ds1, originX: number, originY: number) => number,
|
||||
placedRects?: PlacedPresetRect[],
|
||||
): Set<string> {
|
||||
const placed = new Set<string>()
|
||||
|
||||
|
|
@ -523,8 +541,9 @@ export function DRLG_GenerateJungles(
|
|||
const offsetX = Math.max(0, Math.floor((32 - bDs1.width) / 2))
|
||||
const offsetY = Math.max(0, Math.floor((32 - bDs1.height) / 2))
|
||||
stampDs1Fn(canvas, bDs1, bx + offsetX, by + offsetY)
|
||||
placedRects?.push({ x: bx + offsetX, y: by + offsetY, w: bDs1.width, h: bDs1.height, name: bridgePiece.name })
|
||||
}
|
||||
synthesizeBridgePlanks(canvas, bx, by, orientation)
|
||||
synthesizeBridgePlanks(canvas, bx, by, orientation, bridgePiece?.levels[0])
|
||||
placed.add('Act 3 - Bridge')
|
||||
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
|
|
@ -538,9 +557,11 @@ export function DRLG_GenerateJungles(
|
|||
bridges.push({ col: bridgeCol, row: bridgeRow, x: bx, y: by, pieceName: 'Act 3 - Bridge' })
|
||||
|
||||
if (bridgePiece?.levels[0]) {
|
||||
stampDs1Fn(canvas, bridgePiece.levels[0], bx, by)
|
||||
const bDs1 = bridgePiece.levels[0]
|
||||
stampDs1Fn(canvas, bDs1, bx, by)
|
||||
placedRects?.push({ x: bx, y: by, w: bDs1.width, h: bDs1.height, name: bridgePiece.name })
|
||||
}
|
||||
synthesizeBridgePlanks(canvas, bx, by, 'horizontal')
|
||||
synthesizeBridgePlanks(canvas, bx, by, 'horizontal', bridgePiece?.levels[0])
|
||||
placed.add('Act 3 - Bridge')
|
||||
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
|
|
@ -553,11 +574,13 @@ export function DRLG_GenerateJungles(
|
|||
stampDs1Fn(canvas, tailPiece.levels[0], 0, 0)
|
||||
placed.add(tailPiece.name)
|
||||
stats.substitutions.push({ name: tailPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||
placedRects?.push({ x: 0, y: 0, w: tailPiece.levels[0].width, h: tailPiece.levels[0].height, name: tailPiece.name })
|
||||
}
|
||||
if (headPiece?.levels[0]) {
|
||||
stampDs1Fn(canvas, headPiece.levels[0], 0, (numRows - 1) * 32)
|
||||
placed.add(headPiece.name)
|
||||
stats.substitutions.push({ name: headPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||
placedRects?.push({ x: 0, y: (numRows - 1) * 32, w: headPiece.levels[0].width, h: headPiece.levels[0].height, name: headPiece.name })
|
||||
}
|
||||
|
||||
// 8. Stamp Interior River and Clearing Presets
|
||||
|
|
@ -575,6 +598,7 @@ export function DRLG_GenerateJungles(
|
|||
cell.pieceName = riverPiece.name
|
||||
placed.add(riverPiece.name)
|
||||
stats.substitutions.push({ name: riverPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||
placedRects?.push({ x, y, w: ds1.width, h: ds1.height, name: riverPiece.name })
|
||||
}
|
||||
} else if (cell.type === 'clearing') {
|
||||
const preferredOpening =
|
||||
|
|
@ -591,6 +615,7 @@ export function DRLG_GenerateJungles(
|
|||
cell.pieceName = clearingPiece.name
|
||||
placed.add(clearingPiece.name)
|
||||
stats.substitutions.push({ name: clearingPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||
placedRects?.push({ x, y, w: ds1.width, h: ds1.height, name: clearingPiece.name })
|
||||
clearings.push({
|
||||
col: c,
|
||||
row: r,
|
||||
|
|
@ -605,9 +630,11 @@ export function DRLG_GenerateJungles(
|
|||
// 9. Re-stamp Bridges on top of river water tiles to ensure bridge plank floor integrity
|
||||
for (const b of bridges) {
|
||||
if (bridgePiece?.levels[0]) {
|
||||
stampDs1Fn(canvas, bridgePiece.levels[0], b.x, b.y)
|
||||
const ds1 = bridgePiece.levels[0]
|
||||
stampDs1Fn(canvas, ds1, b.x, b.y)
|
||||
placedRects?.push({ x: b.x, y: b.y, w: ds1.width, h: ds1.height, name: bridgePiece.name })
|
||||
}
|
||||
synthesizeBridgePlanks(canvas, b.x, b.y, 'horizontal')
|
||||
synthesizeBridgePlanks(canvas, b.x, b.y, 'horizontal', bridgePiece?.levels[0])
|
||||
}
|
||||
|
||||
// 10. Record Layout Statistics
|
||||
|
|
|
|||
|
|
@ -3122,7 +3122,7 @@ function stampAct3Jungle(
|
|||
stats: WildernessStats,
|
||||
placedRects?: PlacedPresetRect[],
|
||||
): Set<string> {
|
||||
return DRLG_GenerateJungles(canvas, levelId, pieces, rng, stats, stampDs1)
|
||||
return DRLG_GenerateJungles(canvas, levelId, pieces, rng, stats, stampDs1, placedRects)
|
||||
}
|
||||
|
||||
/** Theme configuration for outdoor road rasterization. */
|
||||
|
|
@ -3156,7 +3156,7 @@ export function stampThemedRoads(
|
|||
): void {
|
||||
const isInsideProtectedPreset = (x: number, y: number): boolean => {
|
||||
for (const r of placedRects) {
|
||||
if (theme.isProtectedPreset ? !theme.isProtectedPreset(r.name) : /River|Bridge/i.test(r.name)) {
|
||||
if (theme.isProtectedPreset ? !theme.isProtectedPreset(r.name) : false) {
|
||||
continue
|
||||
}
|
||||
if (x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h) {
|
||||
|
|
@ -3176,7 +3176,11 @@ export function stampThemedRoads(
|
|||
if (existingFloor && (existingFloor.style === 2 || existingFloor.style === 3 || existingFloor.style === 4)) {
|
||||
return
|
||||
}
|
||||
const belongsToPreset = placedRects.some(r => x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h)
|
||||
if (cell.walls.some(w => !w.hidden && w.prop1 !== 0)) {
|
||||
if (belongsToPreset) {
|
||||
return
|
||||
}
|
||||
if (clearStrayClutter) {
|
||||
// Clear random LvlSub bush/tree clutter on the main highway spine
|
||||
;(cell.walls as Ds1Wall[]).length = 0
|
||||
|
|
@ -3508,7 +3512,7 @@ export function stampAct2DesertRoads(
|
|||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||
isIgnoredSpurPreset: (name) => /Fill|Berms/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
|
|
@ -3564,7 +3568,7 @@ export function stampAct3KurastAvenues(
|
|||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||
isIgnoredSpurPreset: (name) => /Fill/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
|
|
@ -3620,7 +3624,7 @@ export function stampAct5SnowRoads(
|
|||
unknown2: 0,
|
||||
hidden: false,
|
||||
}),
|
||||
isProtectedPreset: (name) => !/River|Bridge/i.test(name),
|
||||
isProtectedPreset: (name) => name !== 'Act 1 - River',
|
||||
isIgnoredSpurPreset: (name) => /Fill|Filler/i.test(name),
|
||||
},
|
||||
openGates,
|
||||
|
|
@ -5141,7 +5145,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
if (request.levelId === 83) {
|
||||
placedSpecial = stampAct3Travincal(canvas, request.pieces, stats, specialPlacedRects)
|
||||
} else if (request.levelId === 82) {
|
||||
placedSpecial = stampAct3KurastCauseway(canvas, request.pieces, stats)
|
||||
placedSpecial = stampAct3KurastCauseway(canvas, request.pieces, stats, specialPlacedRects)
|
||||
} else if (request.levelId >= 76 && request.levelId <= 78) {
|
||||
placedSpecial = stampAct3Jungle(canvas, request.levelId, request.pieces, rng, stats, specialPlacedRects)
|
||||
const populationRng = new Rng(request.seed ^ 0x6c07)
|
||||
|
|
|
|||
|
|
@ -13,15 +13,21 @@ import type {
|
|||
} from '../src/game/wilderness-acts.ts'
|
||||
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
||||
|
||||
function makeMockDs1(width: number, height: number, hasGapInWalls: boolean = false): Ds1 {
|
||||
function makeMockDs1(
|
||||
width: number,
|
||||
height: number,
|
||||
hasGapInWalls: boolean = false,
|
||||
floorStyle: number = 1,
|
||||
wallStyle: number = 1,
|
||||
): Ds1 {
|
||||
const cells: Ds1Cell[][] = []
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const row: Ds1Cell[] = []
|
||||
for (let x = 0; x < width; x += 1) {
|
||||
const isGap = hasGapInWalls && x === Math.floor(width / 2)
|
||||
row.push({
|
||||
walls: isGap ? [] : [{ prop1: 2, sequence: 0, style: 1, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
floors: [{ prop1: 2, sequence: 0, style: 1, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
walls: isGap ? [] : [{ prop1: 2, sequence: 0, style: wallStyle, type: 0, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
floors: [{ prop1: 2, sequence: 0, style: floorStyle, unknown1: 0, unknown2: 0, hidden: false }],
|
||||
shadows: [],
|
||||
substitutions: [],
|
||||
})
|
||||
|
|
@ -231,9 +237,9 @@ describe('Wilderness Outdoor Initializers for Acts 2-5 (Issue #51)', () => {
|
|||
})
|
||||
|
||||
test('Kurast Causeway (82) stamps causeway with central walkway, balustrades, and canal water', () => {
|
||||
const bridgeDs1 = makeMockDs1(48, 16)
|
||||
const groundDs1 = makeMockDs1(48, 16)
|
||||
const pieces: WildernessPiece[] = [
|
||||
{ name: 'Act 3 - Bridge', border: false, levels: [bridgeDs1] },
|
||||
{ name: 'Act 3 - Ground', border: false, levels: [groundDs1] },
|
||||
]
|
||||
|
||||
const result = generateWilderness({
|
||||
|
|
@ -271,6 +277,38 @@ describe('Wilderness Outdoor Initializers for Acts 2-5 (Issue #51)', () => {
|
|||
expect(foundCanalWater).toBe(true)
|
||||
})
|
||||
|
||||
test('Kurast Causeway (82) preserves preset when Act 3 - Bridge is provided', () => {
|
||||
const bridgeDs1 = makeMockDs1(48, 16, false, 29, 31)
|
||||
const pieces: WildernessPiece[] = [
|
||||
{ name: 'Act 3 - Bridge', border: false, levels: [bridgeDs1] },
|
||||
]
|
||||
|
||||
const result = generateWilderness({
|
||||
levelId: 82,
|
||||
levelName: 'Kurast Causeway',
|
||||
levelTypeName: 'Act 3 - Kurast',
|
||||
sizeX: 48,
|
||||
sizeY: 16,
|
||||
subType: 0,
|
||||
subTheme: 0,
|
||||
seed: 82001,
|
||||
pieces,
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
let foundPresetFloor = false
|
||||
let foundPresetWall = false
|
||||
for (let y = 0; y < result.level.height; y += 1) {
|
||||
for (let x = 0; x < result.level.width; x += 1) {
|
||||
const cell = result.level.cells[y]![x]!
|
||||
if (cell.floors.some(f => f.style === 29)) foundPresetFloor = true
|
||||
if (cell.walls.some(w => w.style === 31)) foundPresetWall = true
|
||||
}
|
||||
}
|
||||
expect(foundPresetFloor).toBe(true)
|
||||
expect(foundPresetWall).toBe(true)
|
||||
})
|
||||
|
||||
test('Kurast Bazaar (79) generates paved stone avenues, canals, and stone bridges', () => {
|
||||
const pieces = makeMockBorderPieces('Act 3')
|
||||
const result = generateWilderness({
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../src/game/wilderness.ts'
|
||||
import type { WildernessPiece, PlacedPresetRect, Canvas } from '../src/game/wilderness.ts'
|
||||
import { Rng } from '../src/game/rng.ts'
|
||||
import type { Ds1, Ds1Cell } from '../src/formats/ds1.ts'
|
||||
import type { Ds1, Ds1Cell, Ds1Wall } from '../src/formats/ds1.ts'
|
||||
|
||||
function makeMockDs1(width: number, height: number): Ds1 {
|
||||
const cells: Ds1Cell[][] = []
|
||||
|
|
@ -412,5 +412,33 @@ describe('Multi-Act Outdoor Roads & Boundary Divider Networks', () => {
|
|||
expect(roadFloors).toBe(0)
|
||||
expect(dividerWalls).toBe(0)
|
||||
})
|
||||
|
||||
it('stampThemedRoads preserves preset walls on the highway spine', () => {
|
||||
const canvas = createCanvas(width, height, 1, 1, 0)
|
||||
const rng = new Rng(42)
|
||||
const hubX = inX * TILES_PER_BLOCK + 4
|
||||
const hubY = outY * TILES_PER_BLOCK + 4
|
||||
// Preset directly intersects the road spine
|
||||
const placedRects: PlacedPresetRect[] = [
|
||||
{ name: 'Act 1 - Ruin', x: hubX - 2, y: hubY - 2, w: 5, h: 5 },
|
||||
]
|
||||
// Paint preset wall inside the preset on the highway spine
|
||||
const targetCell = canvas.cells[hubY]?.[hubX]
|
||||
expect(targetCell).toBeDefined()
|
||||
;(targetCell!.walls as Ds1Wall[]).push({
|
||||
prop1: 100,
|
||||
sequence: 0,
|
||||
style: 99,
|
||||
type: 1,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
hidden: false,
|
||||
})
|
||||
|
||||
stampAct1DirtRoads(canvas, gridW, gridH, inX, outY, placedRects, rng)
|
||||
|
||||
// The wall inside the preset must NOT have been wiped
|
||||
expect(targetCell!.walls.some(w => w.style === 99 && w.prop1 === 100)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue