diff --git a/src/game/wilderness-acts.ts b/src/game/wilderness-acts.ts index 9ee82cc..5c5a162 100644 --- a/src/game/wilderness-acts.ts +++ b/src/game/wilderness-acts.ts @@ -418,6 +418,7 @@ export function stampAct3KurastCauseway( canvas: Canvas, pieces: readonly WildernessPiece[], stats: WildernessStats, + specialPlacedRects?: PlacedPresetRect[], ): Set { const placed = new Set() 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) diff --git a/src/game/wilderness-jungles.ts b/src/game/wilderness-jungles.ts index 571984f..fcb0776 100644 --- a/src/game/wilderness-jungles.ts +++ b/src/game/wilderness-jungles.ts @@ -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 { const placed = new Set() @@ -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 diff --git a/src/game/wilderness.ts b/src/game/wilderness.ts index d7f51f0..fe1f011 100644 --- a/src/game/wilderness.ts +++ b/src/game/wilderness.ts @@ -3122,7 +3122,7 @@ function stampAct3Jungle( stats: WildernessStats, placedRects?: PlacedPresetRect[], ): Set { - 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) diff --git a/tests/wilderness-acts.test.ts b/tests/wilderness-acts.test.ts index 33f727d..4e4dbe3 100644 --- a/tests/wilderness-acts.test.ts +++ b/tests/wilderness-acts.test.ts @@ -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({ diff --git a/tests/wilderness-roads-multi-act.test.ts b/tests/wilderness-roads-multi-act.test.ts index e818425..8166ca3 100644 --- a/tests/wilderness-roads-multi-act.test.ts +++ b/tests/wilderness-roads-multi-act.test.ts @@ -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) + }) }) })