diff --git a/scripts/pack-act-assets.ts b/scripts/pack-act-assets.ts index 7d517e2..1ca34eb 100644 --- a/scripts/pack-act-assets.ts +++ b/scripts/pack-act-assets.ts @@ -524,6 +524,15 @@ const WILDERNESS_PIECE_FAMILIES: Readonly> = { 'Act 1 - Pond', 'Act 1 - Swamp Fill', 'Act 1 - Stone Fill', + 'Act 1 - Cottages', + 'Act 1 - Fallen Camp', + 'Act 1 - Camp', + 'Act 1 - Cairn Stones', + 'Act 1 - Inifus', + 'Act 1 - Tower', + 'Act 1 - Ruin', + 'Act 1 - Tree Fill', + 'Act 1 - Graveyard', ], 'Act 2 - Desert': ['Act 2 - Desert'], 'Act 3 - Jungle': ['Act 3 - Jungle'], diff --git a/scripts/verify-generators.ts b/scripts/verify-generators.ts index 39d4b78..68556cb 100644 --- a/scripts/verify-generators.ts +++ b/scripts/verify-generators.ts @@ -404,6 +404,15 @@ const WILDERNESS_PIECE_FAMILIES: Readonly> = { 'Act 1 - Pond', 'Act 1 - Swamp Fill', 'Act 1 - Stone Fill', + 'Act 1 - Cottages', + 'Act 1 - Fallen Camp', + 'Act 1 - Camp', + 'Act 1 - Cairn Stones', + 'Act 1 - Inifus', + 'Act 1 - Tower', + 'Act 1 - Ruin', + 'Act 1 - Tree Fill', + 'Act 1 - Graveyard', ], 'Act 2 - Desert': ['Act 2 - Desert'], 'Act 3 - Jungle': ['Act 3 - Jungle'], @@ -674,6 +683,17 @@ for (const { id, name } of wildLevels) { check(first.level.width === expectedBlocksX * AREA_CELLS && first.level.height === expectedBlocksY * AREA_CELLS, label, 'map extent does not match whole blocks of the declared size') check(missingShare <= MAX_MISSING_SHARE, label, `${(missingShare * 100).toFixed(2)}% of tile references are missing`) check(fill.share >= MIN_REACHABLE_SHARE, label, `only ${(fill.share * 100).toFixed(1)}% of walkable areas are reachable (${String(fill.reached)}/${String(fill.total)})`) + check(Number(first.stats.groundCells) === first.level.width * first.level.height, label, `ground floor coverage is incomplete: ${String(first.stats.groundCells)}/${String(first.level.width * first.level.height)}`) + if (id === 3) { + check(scene.walls.length > 500, label, `Cold Plains walls (${String(scene.walls.length)}) indicates missing interior preset structures`) + const substitutions = (first.stats.substitutions ?? []) as readonly { readonly name: string }[] + check(substitutions.some(s => s.name.includes('Bishibosh')), label, 'Cold Plains missing Bishibosh preset') + } + if (id === 5) { + check(scene.walls.length > 500, label, `Dark Wood walls (${String(scene.walls.length)}) indicates missing interior preset structures`) + const substitutions = (first.stats.substitutions ?? []) as readonly { readonly name: string }[] + check(substitutions.some(s => s.name.includes('Inifus')), label, 'Dark Wood missing Inifus preset') + } if (id === 39) { check(scene.walls.length > 700, label, `Moo Moo Farm walls (${String(scene.walls.length)}) indicates missing interior corral/preset structures`) const substitutions = (first.stats.substitutions ?? []) as readonly { readonly name: string }[] diff --git a/src/game/wilderness.ts b/src/game/wilderness.ts index 493227a..01093d3 100644 --- a/src/game/wilderness.ts +++ b/src/game/wilderness.ts @@ -74,7 +74,7 @@ * so this port takes the ground tile from the level's own border pieces instead * — see {@link WildernessRequest.groundTile}. */ -import type { Ds1, Ds1Cell, Ds1Floor, Ds1Object, Ds1Wall } from '../formats/ds1.ts' +import type { Ds1, Ds1Cell, Ds1Floor, Ds1Object, Ds1Wall, Ds1Substitution } from '../formats/ds1.ts' import { Rng } from './rng.ts' import { SUB_TILES_PER_TILE } from './map.ts' @@ -354,7 +354,61 @@ function stampDs1(canvas: Canvas, source: Ds1, originX: number, originY: number) if (targetX < 0 || targetX >= canvas.width) continue const cell = sourceRow[x] if (cell === undefined) continue - targetRow[targetX] = cell + const targetCell = targetRow[targetX]! + + // Floors: copy any non-empty floor layer from source. If source cell has no + // non-empty floors, target's ground floor is preserved intact. + for (let i = 0; i < cell.floors.length; i += 1) { + const floor = cell.floors[i] + if (floor !== undefined && !floor.hidden && floor.prop1 !== 0) { + const targetFloors = targetCell.floors as Ds1Floor[] + if (i < targetFloors.length) { + targetFloors[i] = floor + } else { + targetFloors.push(floor) + } + } + } + + // Walls: copy any non-empty wall layer from source. + for (let i = 0; i < cell.walls.length; i += 1) { + const wall = cell.walls[i] + if (wall !== undefined && !wall.hidden && wall.prop1 !== 0) { + const targetWalls = targetCell.walls as Ds1Wall[] + if (i < targetWalls.length) { + targetWalls[i] = wall + } else { + targetWalls.push(wall) + } + } + } + + // Shadows: copy any non-empty shadow layer from source. + for (let i = 0; i < cell.shadows.length; i += 1) { + const shadow = cell.shadows[i] + if (shadow !== undefined && !shadow.hidden && shadow.prop1 !== 0) { + const targetShadows = targetCell.shadows as Ds1Floor[] + if (i < targetShadows.length) { + targetShadows[i] = shadow + } else { + targetShadows.push(shadow) + } + } + } + + // Substitutions: copy non-zero substitution layer values. + for (let i = 0; i < cell.substitutions.length; i += 1) { + const sub = cell.substitutions[i] + if (sub !== undefined && sub.value !== 0) { + const targetSubs = targetCell.substitutions as Ds1Substitution[] + if (i < targetSubs.length) { + targetSubs[i] = sub + } else { + targetSubs.push(sub) + } + } + } + written += 1 } } @@ -767,6 +821,56 @@ function applySubstitutions( * fills. */ export const SPECIAL_PRESETS_BY_LEVEL: Readonly> = { + 2: [ + 'Act 1 - Pond', + 'Act 1 - Cottages 1', + 'Act 1 - Stone Fill 1', + 'Act 1 - Stone Fill 2', + ], + 3: [ + 'Act 1 - Cottages 2', + 'Act 1 - Cottages 3', + 'Act 1 - Fallen Camp Bishibosh', + 'Act 1 - Stone Fill 1', + 'Act 1 - Stone Fill 2', + ], + 4: [ + 'Act 1 - Cairn Stones', + 'Act 1 - Camp', + 'Act 1 - Tower Tome', + 'Act 1 - Cottages 1', + 'Act 1 - Cottages 3', + 'Act 1 - Fallen Camp 1', + 'Act 1 - Corral Fill', + ], + 5: [ + 'Act 1 - Inifus', + 'Act 1 - Ruin', + 'Act 1 - Tree Fill', + 'Act 1 - Cottages 2', + 'Act 1 - Cottages 3', + 'Act 1 - Fallen Camp 2', + 'Act 1 - Stone Fill 1', + 'Act 1 - Stone Fill 2', + ], + 6: [ + 'Act 1 - Tower 1', + 'Act 1 - Swamp Fill 1', + 'Act 1 - Swamp Fill 2', + 'Act 1 - Cottages 1', + 'Act 1 - Cottages 3', + 'Act 1 - Fallen Camp 1', + 'Act 1 - Stone Fill 1', + 'Act 1 - Stone Fill 2', + ], + 7: [ + 'Act 1 - Cottages 2', + 'Act 1 - Fallen Camp 2', + 'Act 1 - Corral Fill', + ], + 17: [ + 'Act 1 - Graveyard', + ], 39: [ 'Act 1 - Bivouac', 'Act 1 - Pond', @@ -1069,6 +1173,17 @@ export function generateWilderness(request: WildernessRequest): WildernessResult } } + let finalGroundCells = 0 + for (let y = 0; y < canvas.height; y += 1) { + const row = canvas.cells[y] + if (row === undefined) continue + for (let x = 0; x < canvas.width; x += 1) { + const c = row[x] + if (c !== undefined && c.floors.some(f => !f.hidden && f.prop1 !== 0)) finalGroundCells += 1 + } + } + stats.groundCells = finalGroundCells + if (!Number.isFinite(canvas.width * canvas.height) || canvas.width * canvas.height > (1 << 22)) { throw new Error(`${where}: synthesized map is ${String(canvas.width)}x${String(canvas.height)} cells, outside the supported bound`) }