feat(wilderness): 还原 DRLGOUTPLACE_PlaceAct1245OutdoorBorders 边界峭壁与门洞状态机
- 扩展 classifyBorderPiece 支持 1~5 幕所有预置边界切片命名(1..12 编号、方位后缀、Act 2 峭壁、Act 5 崖壁/雪地/峡谷与过渡转角) - 导出 getCellOrientation 与 selectBorderPiece,支持精确的峭壁偏好 (preferCliff: true/false/undefined) 与开洞优先降级 - 实现并导出 DRLGOUTPLACE_PlaceAct1245OutdoorBorders(保留 layBorder 别名),先顺时针放置四边直边界再盖转角封口,保证 100% 覆盖率与 0 过程回退墙 - 支持 claimedBorderBlocks 城镇过渡区与外立面避让接驳 - 新增 tests/wilderness-borders.test.ts 完整测试全 5 幕边界识别、开闭门洞选择、外圈覆盖与接驳避让 TAG=agy CONV=2a1934de-30ef-464e-b3f3-fcbcdbbc49f1
This commit is contained in:
parent
939ada6e4c
commit
0e8778f31a
|
|
@ -298,6 +298,10 @@ export interface WildernessRequest {
|
||||||
* the tests and `verify-generators` use.
|
* the tests and `verify-generators` use.
|
||||||
*/
|
*/
|
||||||
readonly gates?: readonly PlannedGate[]
|
readonly gates?: readonly PlannedGate[]
|
||||||
|
/** Optional forced gate variant indices per block key (`${x},${y}`). */
|
||||||
|
readonly openGates?: ReadonlyMap<string, number>
|
||||||
|
/** Optional set of block keys (`${x},${y}`) claimed by transition/facade presets to skip during border placement. */
|
||||||
|
readonly claimedBorderBlocks?: ReadonlySet<string>
|
||||||
/** Optional `SuperUniques.txt` table for data-driven outdoor boss and landmark generation. */
|
/** Optional `SuperUniques.txt` table for data-driven outdoor boss and landmark generation. */
|
||||||
readonly superuniques?: D2Table
|
readonly superuniques?: D2Table
|
||||||
/** Optional `MonPreset.txt` table for data-driven DS1 spawner object IDs. */
|
/** Optional `MonPreset.txt` table for data-driven DS1 spawner object IDs. */
|
||||||
|
|
@ -1276,17 +1280,36 @@ export interface ClassifiedBorderPiece {
|
||||||
* - 6 = NW corner
|
* - 6 = NW corner
|
||||||
* - 7 = NE corner
|
* - 7 = NE corner
|
||||||
* - 8 = SE corner
|
* - 8 = SE corner
|
||||||
* - 9..12 = corner / transition variations
|
* - 9..12 = corner / transition variations (9=SW, 10=NW, 11=NE, 12=SE)
|
||||||
*
|
*
|
||||||
* Act 3 outdoor tiles use cardinal suffixes ("Border N", "Border S", "Border NE", etc.).
|
* Act 2:
|
||||||
* Cave entrances ("clfcave", "clfcave2") are dedicated structures, never generic border pieces.
|
* - Desert Border 1..12 (1..4 straight edges, 5..12 corners)
|
||||||
|
* - Desert Cliff Left Wall/Path/Ends/King Tomb -> West edge
|
||||||
|
* - Desert Cliff Right Wall/Path/Ends/King Tomb -> East edge
|
||||||
|
* - Desert Cliff Top / Top King Tomb -> North edge
|
||||||
|
*
|
||||||
|
* Act 3:
|
||||||
|
* - Slums/Burbs/Metro Border N, S, E, W, NE, NW, SE, SW
|
||||||
|
*
|
||||||
|
* Act 4:
|
||||||
|
* - Mesa Border 1..12 (1..4 straight edges, 5..12 corners)
|
||||||
|
*
|
||||||
|
* Act 5:
|
||||||
|
* - Barricade Cliff Border 1..12 & Snow variants (isCliff: true)
|
||||||
|
* - Barricade Ravine Border 1..12 & Snow variants (isCliff: false)
|
||||||
|
* - Barricade Ravine-Cliff Border 5 (SW corner transition)
|
||||||
|
* - Barricade Cliff Ravine Border 7 (NE corner transition)
|
||||||
|
* - Snow Border 1..12
|
||||||
|
*
|
||||||
|
* Cave entrances ("clfcave", "clfcave2", "Cave Right", "Cave Left") are dedicated structures,
|
||||||
|
* never generic border pieces.
|
||||||
*/
|
*/
|
||||||
export function classifyBorderPiece(name: string): { orientation: BorderOrientation | null; isCliff: boolean } {
|
export function classifyBorderPiece(name: string): { orientation: BorderOrientation | null; isCliff: boolean } {
|
||||||
if (/cave/i.test(name)) return { orientation: null, isCliff: false }
|
if (/cave/i.test(name)) return { orientation: null, isCliff: false }
|
||||||
const isCliff = /cliff|stnclf/i.test(name)
|
const isCliff = /cliff|stnclf/i.test(name)
|
||||||
|
|
||||||
// Act 3 style: "Border N", "Border S", "Border NE", etc.
|
// Act 3 style: "Border N", "Border S", "Border NE", etc.
|
||||||
const cardinal = name.match(/Border\s+([NESW]{1,2})\b/i)
|
const cardinal = name.match(/\b(?:border|edge|brdr|cliff)\s+([NESW]{1,2})\b/i)
|
||||||
if (cardinal) {
|
if (cardinal) {
|
||||||
const dir = cardinal[1]!.toUpperCase()
|
const dir = cardinal[1]!.toUpperCase()
|
||||||
if (dir === 'N') return { orientation: 'north', isCliff }
|
if (dir === 'N') return { orientation: 'north', isCliff }
|
||||||
|
|
@ -1299,27 +1322,45 @@ export function classifyBorderPiece(name: string): { orientation: BorderOrientat
|
||||||
if (dir === 'SW') return { orientation: 'corner-sw', isCliff }
|
if (dir === 'SW') return { orientation: 'corner-sw', isCliff }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Act 1, 2, 4, 5 style: 1=S, 2=W, 3=N, 4=E, 5=SW, 6=NW, 7=NE, 8=SE
|
// Act 1, 2, 4, 5 style:
|
||||||
const numMatch = name.match(/(?:border|edge|brdr_?snow|brdr)\s*0?(\d+)/i)
|
// 1 = South edge
|
||||||
|
// 2 = West edge
|
||||||
|
// 3 = North edge
|
||||||
|
// 4 = East edge
|
||||||
|
// 5, 9 = SW corner
|
||||||
|
// 6, 10 = NW corner (including 6A, 6B, 6C)
|
||||||
|
// 7, 11 = NE corner
|
||||||
|
// 8, 12 = SE corner
|
||||||
|
const numMatch = name.match(/(?:border|edge|brdr_?snow|brdr|cliff|ravine)\s*0?(\d+)/i)
|
||||||
if (numMatch) {
|
if (numMatch) {
|
||||||
const num = Number(numMatch[1])
|
const num = Number(numMatch[1])
|
||||||
if (num === 1) return { orientation: 'south', isCliff }
|
if (num === 1) return { orientation: 'south', isCliff }
|
||||||
if (num === 2) return { orientation: 'west', isCliff }
|
if (num === 2) return { orientation: 'west', isCliff }
|
||||||
if (num === 3) return { orientation: 'north', isCliff }
|
if (num === 3) return { orientation: 'north', isCliff }
|
||||||
if (num === 4) return { orientation: 'east', isCliff }
|
if (num === 4) return { orientation: 'east', isCliff }
|
||||||
if (num === 5) return { orientation: 'corner-sw', isCliff }
|
if (num === 5 || num === 9) return { orientation: 'corner-sw', isCliff }
|
||||||
if (num === 6) return { orientation: 'corner-nw', isCliff }
|
if (num === 6 || num === 10) return { orientation: 'corner-nw', isCliff }
|
||||||
if (num === 7) return { orientation: 'corner-ne', isCliff }
|
if (num === 7 || num === 11) return { orientation: 'corner-ne', isCliff }
|
||||||
if (num === 8) return { orientation: 'corner-se', isCliff }
|
if (num === 8 || num === 12) return { orientation: 'corner-se', isCliff }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Directional names (Act 2 Desert Cliff Left/Right/Top, entrance/exit btm/top, cardinal words)
|
||||||
|
if (/\b(?:north[\s-]?west|nw)\b/i.test(name)) return { orientation: 'corner-nw', isCliff }
|
||||||
|
if (/\b(?:north[\s-]?east|ne)\b/i.test(name)) return { orientation: 'corner-ne', isCliff }
|
||||||
|
if (/\b(?:south[\s-]?west|sw)\b/i.test(name)) return { orientation: 'corner-sw', isCliff }
|
||||||
|
if (/\b(?:south[\s-]?east|se)\b/i.test(name)) return { orientation: 'corner-se', isCliff }
|
||||||
|
if (/\b(?:north|top)\b/i.test(name)) return { orientation: 'north', isCliff }
|
||||||
|
if (/\b(?:south|bottom|btm)\b/i.test(name)) return { orientation: 'south', isCliff }
|
||||||
|
if (/\b(?:east|right|rght)\b/i.test(name)) return { orientation: 'east', isCliff }
|
||||||
|
if (/\b(?:west|left)\b/i.test(name)) return { orientation: 'west', isCliff }
|
||||||
|
|
||||||
return { orientation: null, isCliff }
|
return { orientation: null, isCliff }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the perimeter orientation for a block cell on a `gridWidth x gridHeight` grid.
|
* Determine the perimeter orientation for a block cell on a `gridWidth x gridHeight` grid.
|
||||||
*/
|
*/
|
||||||
function getCellOrientation(x: number, y: number, gridWidth: number, gridHeight: number): BorderOrientation {
|
export function getCellOrientation(x: number, y: number, gridWidth: number, gridHeight: number): BorderOrientation {
|
||||||
if (x === 0 && y === 0) return 'corner-nw'
|
if (x === 0 && y === 0) return 'corner-nw'
|
||||||
if (x === gridWidth - 1 && y === 0) return 'corner-ne'
|
if (x === gridWidth - 1 && y === 0) return 'corner-ne'
|
||||||
if (x === gridWidth - 1 && y === gridHeight - 1) return 'corner-se'
|
if (x === gridWidth - 1 && y === gridHeight - 1) return 'corner-se'
|
||||||
|
|
@ -1333,21 +1374,21 @@ function getCellOrientation(x: number, y: number, gridWidth: number, gridHeight:
|
||||||
/**
|
/**
|
||||||
* Select a matching piece for the given orientation.
|
* Select a matching piece for the given orientation.
|
||||||
*/
|
*/
|
||||||
function selectBorderPiece(
|
export function selectBorderPiece(
|
||||||
orientation: BorderOrientation,
|
orientation: BorderOrientation,
|
||||||
classified: readonly ClassifiedBorderPiece[],
|
classified: readonly ClassifiedBorderPiece[],
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
preferCliff: boolean,
|
preferCliff?: boolean,
|
||||||
): WildernessPiece | null {
|
): WildernessPiece | null {
|
||||||
const matches = classified.filter(c => c.orientation === orientation)
|
const matches = classified.filter(c => c.orientation === orientation)
|
||||||
if (matches.length === 0) return null
|
if (matches.length === 0) return null
|
||||||
|
|
||||||
if (preferCliff) {
|
if (preferCliff === true) {
|
||||||
const cliffMatches = matches.filter(c => c.isCliff)
|
const cliffMatches = matches.filter(c => c.isCliff)
|
||||||
if (cliffMatches.length > 0) {
|
if (cliffMatches.length > 0) {
|
||||||
return cliffMatches[rng.int(0, cliffMatches.length - 1)]!.piece
|
return cliffMatches[rng.int(0, cliffMatches.length - 1)]!.piece
|
||||||
}
|
}
|
||||||
} else {
|
} else if (preferCliff === false) {
|
||||||
const nonCliffMatches = matches.filter(c => !c.isCliff)
|
const nonCliffMatches = matches.filter(c => !c.isCliff)
|
||||||
if (nonCliffMatches.length > 0) {
|
if (nonCliffMatches.length > 0) {
|
||||||
return nonCliffMatches[rng.int(0, nonCliffMatches.length - 1)]!.piece
|
return nonCliffMatches[rng.int(0, nonCliffMatches.length - 1)]!.piece
|
||||||
|
|
@ -1487,11 +1528,11 @@ export function getPerimeterOpenings(
|
||||||
* @param stats - report collector.
|
* @param stats - report collector.
|
||||||
* @param levelId - optional level id for perimeter opening rules.
|
* @param levelId - optional level id for perimeter opening rules.
|
||||||
* @param openGates - optional map from `${x},${y}` block key to forced variant index.
|
* @param openGates - optional map from `${x},${y}` block key to forced variant index.
|
||||||
* @param claimedBorderBlocks - optional set of `${x},${y}` keys to skip (e.g. Town Transitions).
|
* @param claimedBorderBlocks - optional set of `${x},${y}` keys to skip (e.g. Town Transitions, Facades).
|
||||||
* @param gates - optional planned gates from the world graph.
|
* @param gates - optional planned gates from the world graph.
|
||||||
* @returns the number of pieces stamped.
|
* @returns the number of pieces stamped.
|
||||||
*/
|
*/
|
||||||
function layBorder(
|
export function DRLGOUTPLACE_PlaceAct1245OutdoorBorders(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
pieces: readonly WildernessPiece[],
|
pieces: readonly WildernessPiece[],
|
||||||
gridWidth: number,
|
gridWidth: number,
|
||||||
|
|
@ -1503,6 +1544,10 @@ function layBorder(
|
||||||
claimedBorderBlocks?: ReadonlySet<string>,
|
claimedBorderBlocks?: ReadonlySet<string>,
|
||||||
gates?: readonly PlannedGate[],
|
gates?: readonly PlannedGate[],
|
||||||
): number {
|
): number {
|
||||||
|
if (stats.proceduralWallCells === undefined) {
|
||||||
|
stats.proceduralWallCells = 0
|
||||||
|
}
|
||||||
|
|
||||||
if (pieces.length === 0) {
|
if (pieces.length === 0) {
|
||||||
stats.unresolved.push('no border piece for this level type')
|
stats.unresolved.push('no border piece for this level type')
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1525,18 +1570,20 @@ function layBorder(
|
||||||
return areaA - areaB || a.name.localeCompare(b.name)
|
return areaA - areaB || a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Stamp straight edges first, then corners last, matching D2MOO DRLGOUTPLACE_PlaceAct1245OutdoorBorders
|
// Stamp straight edges first in clockwise ring walk, then corners last,
|
||||||
// where corner presets overwrite edge presets to form seamless joins.
|
// matching D2MOO DRLGOUTPLACE_PlaceAct1245OutdoorBorders where corner presets
|
||||||
|
// overwrite straight edge presets at vertices to form seamless joins.
|
||||||
const straightCells: { x: number; y: number }[] = []
|
const straightCells: { x: number; y: number }[] = []
|
||||||
// North edge
|
// North edge (moving East)
|
||||||
for (let x = 1; x < gridWidth - 1; x += 1) straightCells.push({ x, y: 0 })
|
for (let x = 1; x < gridWidth - 1; x += 1) straightCells.push({ x, y: 0 })
|
||||||
// East edge
|
// East edge (moving South)
|
||||||
for (let y = 1; y < gridHeight - 1; y += 1) straightCells.push({ x: gridWidth - 1, y })
|
for (let y = 1; y < gridHeight - 1; y += 1) straightCells.push({ x: gridWidth - 1, y })
|
||||||
// South edge
|
// South edge (moving West)
|
||||||
for (let x = gridWidth - 2; x >= 1; x -= 1) straightCells.push({ x, y: gridHeight - 1 })
|
for (let x = gridWidth - 2; x >= 1; x -= 1) straightCells.push({ x, y: gridHeight - 1 })
|
||||||
// West edge
|
// West edge (moving North)
|
||||||
for (let y = gridHeight - 2; y >= 1; y -= 1) straightCells.push({ x: 0, y })
|
for (let y = gridHeight - 2; y >= 1; y -= 1) straightCells.push({ x: 0, y })
|
||||||
|
|
||||||
|
// Corner join pieces stamped last: NW, NE, SE, SW
|
||||||
const cornerCells: { x: number; y: number }[] = [
|
const cornerCells: { x: number; y: number }[] = [
|
||||||
{ x: 0, y: 0 }, // NW
|
{ x: 0, y: 0 }, // NW
|
||||||
{ x: gridWidth - 1, y: 0 }, // NE
|
{ x: gridWidth - 1, y: 0 }, // NE
|
||||||
|
|
@ -1561,13 +1608,16 @@ function layBorder(
|
||||||
}
|
}
|
||||||
|
|
||||||
const orientation = getCellOrientation(cell.x, cell.y, gridWidth, gridHeight)
|
const orientation = getCellOrientation(cell.x, cell.y, gridWidth, gridHeight)
|
||||||
// North, West, NW, NE, SW prefer cliff pieces if available (e.g. Act 1 wilderness mountain boundaries)
|
const isOpen = !isCorner && openings.has(key)
|
||||||
|
// North, West, NW, NE, SW prefer cliff pieces if available (e.g. Act 1 wilderness mountain boundaries),
|
||||||
|
// but openings must not prefer cliffs to ensure open gate variants are selected.
|
||||||
const preferCliff =
|
const preferCliff =
|
||||||
orientation === 'north' ||
|
!isOpen &&
|
||||||
orientation === 'west' ||
|
(orientation === 'north' ||
|
||||||
orientation === 'corner-nw' ||
|
orientation === 'west' ||
|
||||||
orientation === 'corner-ne' ||
|
orientation === 'corner-nw' ||
|
||||||
orientation === 'corner-sw'
|
orientation === 'corner-ne' ||
|
||||||
|
orientation === 'corner-sw')
|
||||||
|
|
||||||
let piece = selectBorderPiece(orientation, classified, rng, preferCliff)
|
let piece = selectBorderPiece(orientation, classified, rng, preferCliff)
|
||||||
if (!piece) {
|
if (!piece) {
|
||||||
|
|
@ -1581,7 +1631,6 @@ function layBorder(
|
||||||
if (forcedVariant !== undefined) {
|
if (forcedVariant !== undefined) {
|
||||||
level = piece.levels[Math.min(forcedVariant, piece.levels.length - 1)]
|
level = piece.levels[Math.min(forcedVariant, piece.levels.length - 1)]
|
||||||
} else {
|
} else {
|
||||||
const isOpen = !isCorner && openings.has(key)
|
|
||||||
level = selectBorderVariant(piece, orientation, isOpen, rng)
|
level = selectBorderVariant(piece, orientation, isOpen, rng)
|
||||||
}
|
}
|
||||||
if (level === undefined) return
|
if (level === undefined) return
|
||||||
|
|
@ -1607,6 +1656,8 @@ function layBorder(
|
||||||
return stamped
|
return stamped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const layBorder = DRLGOUTPLACE_PlaceAct1245OutdoorBorders
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- *
|
/* ------------------------------------------------------------------------- *
|
||||||
* Act 1 Outdoor Anchors, Presets & Dirt Road Generation
|
* Act 1 Outdoor Anchors, Presets & Dirt Road Generation
|
||||||
* (`DRLGOUTWILD_InitAct1OutdoorLevel`, `DRLGOUTDOORS_SpawnAct1DirtPaths`,
|
* (`DRLGOUTWILD_InitAct1OutdoorLevel`, `DRLGOUTDOORS_SpawnAct1DirtPaths`,
|
||||||
|
|
@ -5673,7 +5724,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4A: Lay directional border around perimeter (respecting open gates & Town Transitions)
|
// Step 4A: Lay directional border around perimeter (respecting open gates & Town Transitions)
|
||||||
layBorder(
|
DRLGOUTPLACE_PlaceAct1245OutdoorBorders(
|
||||||
canvas,
|
canvas,
|
||||||
borderPieces,
|
borderPieces,
|
||||||
gridWidth,
|
gridWidth,
|
||||||
|
|
@ -5681,8 +5732,8 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
||||||
rng,
|
rng,
|
||||||
stats,
|
stats,
|
||||||
request.levelId,
|
request.levelId,
|
||||||
act1Plan?.openGates,
|
request.openGates ?? act1Plan?.openGates,
|
||||||
act1Plan?.claimedBorderBlocks,
|
request.claimedBorderBlocks ?? act1Plan?.claimedBorderBlocks,
|
||||||
request.gates,
|
request.gates,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,426 @@
|
||||||
|
import { describe, expect, test } from "vitest"
|
||||||
|
import {
|
||||||
|
classifyBorderPiece,
|
||||||
|
getCellOrientation,
|
||||||
|
selectBorderPiece,
|
||||||
|
selectBorderVariant,
|
||||||
|
isClosedBorder,
|
||||||
|
getPerimeterOpenings,
|
||||||
|
DRLGOUTPLACE_PlaceAct1245OutdoorBorders,
|
||||||
|
layBorder,
|
||||||
|
generateWilderness,
|
||||||
|
type WildernessPiece,
|
||||||
|
type WildernessStats,
|
||||||
|
type BorderOrientation,
|
||||||
|
} from "../src/game/wilderness.ts"
|
||||||
|
import { createCanvas } from "../src/game/wilderness.ts"
|
||||||
|
import { Rng } from "../src/game/rng.ts"
|
||||||
|
import type { Ds1, Ds1Cell } from "../src/formats/ds1.ts"
|
||||||
|
|
||||||
|
function makeMockBorderDs1(width: number, height: number, orientation: BorderOrientation, isOpen: boolean): Ds1 {
|
||||||
|
const cells: Ds1Cell[][] = []
|
||||||
|
for (let y = 0; y < height; y += 1) {
|
||||||
|
const row: Ds1Cell[] = []
|
||||||
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
let isWall: boolean
|
||||||
|
if (isOpen) {
|
||||||
|
// Open variant: gap in the middle of the border edge
|
||||||
|
if (orientation === "south" || orientation === "north") {
|
||||||
|
isWall = x !== Math.floor(width / 2)
|
||||||
|
} else if (orientation === "west" || orientation === "east") {
|
||||||
|
isWall = y !== Math.floor(height / 2)
|
||||||
|
} else {
|
||||||
|
isWall = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Closed variant: continuous wall along the border
|
||||||
|
isWall = true
|
||||||
|
}
|
||||||
|
row.push({
|
||||||
|
walls: isWall ? [{ prop1: 129, 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 }],
|
||||||
|
shadows: [],
|
||||||
|
substitutions: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cells.push(row)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: 18,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
act: 1,
|
||||||
|
substitutionType: 0,
|
||||||
|
wallLayers: 1,
|
||||||
|
floorLayers: 1,
|
||||||
|
cells,
|
||||||
|
objects: [],
|
||||||
|
npcPathOffset: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyStats(): WildernessStats {
|
||||||
|
return {
|
||||||
|
substitutions: [],
|
||||||
|
borderPieces: {},
|
||||||
|
unresolved: [],
|
||||||
|
notes: [],
|
||||||
|
borderStamped: 0,
|
||||||
|
groundCells: 0,
|
||||||
|
groundTile: null,
|
||||||
|
sizeSource: "test",
|
||||||
|
roadCells: 0,
|
||||||
|
roadSegments: 0,
|
||||||
|
anchors: 0,
|
||||||
|
specialPresets: [],
|
||||||
|
entrances: [],
|
||||||
|
proceduralRoadCells: 0,
|
||||||
|
proceduralDividerCells: 0,
|
||||||
|
proceduralWaterCells: 0,
|
||||||
|
proceduralWallCells: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Issue #67: DRLGOUTPLACE_PlaceAct1245OutdoorBorders & Multi-Act Border Classification", () => {
|
||||||
|
describe("Task 1A: classifyBorderPiece across Acts 1, 2, 3, 4, 5", () => {
|
||||||
|
test("Act 1: Wild Border 1..12 and Wild Cliff Border 2..10 (including 6A-C and corner 10)", () => {
|
||||||
|
// Edges: 1=S, 2=W, 3=N, 4=E
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 1")).toEqual({ orientation: "south", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 2")).toEqual({ orientation: "west", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 3")).toEqual({ orientation: "north", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 4")).toEqual({ orientation: "east", isCliff: false })
|
||||||
|
|
||||||
|
// Corners: 5/9=SW, 6/10=NW, 7/11=NE, 8/12=SE
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 5")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 6")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 7")).toEqual({ orientation: "corner-ne", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 8")).toEqual({ orientation: "corner-se", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 9")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 10")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 11")).toEqual({ orientation: "corner-ne", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Border 12")).toEqual({ orientation: "corner-se", isCliff: false })
|
||||||
|
|
||||||
|
// Wild Cliff borders: isCliff: true
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Border 2")).toEqual({ orientation: "west", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Border 3")).toEqual({ orientation: "north", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Border 5")).toEqual({ orientation: "corner-sw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Border 6A")).toEqual({ orientation: "corner-nw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Border 6B")).toEqual({ orientation: "corner-nw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Border 6C")).toEqual({ orientation: "corner-nw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Border 7")).toEqual({ orientation: "corner-ne", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Border 10")).toEqual({ orientation: "corner-nw", isCliff: true })
|
||||||
|
|
||||||
|
// Caves are excluded from generic borders
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Cave Right")).toEqual({ orientation: null, isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 1 - Wild Cliff Cave Left")).toEqual({ orientation: null, isCliff: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Act 2: Desert Border 1..12 and Desert Cliff Left/Right/Top/Tomb variants", () => {
|
||||||
|
// Desert Border 1..12
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 1")).toEqual({ orientation: "south", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 2")).toEqual({ orientation: "west", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 3")).toEqual({ orientation: "north", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 4")).toEqual({ orientation: "east", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 5")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 6")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 7")).toEqual({ orientation: "corner-ne", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 8")).toEqual({ orientation: "corner-se", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 9")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 10")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 11")).toEqual({ orientation: "corner-ne", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Border 12")).toEqual({ orientation: "corner-se", isCliff: false })
|
||||||
|
|
||||||
|
// Desert Cliff Left / Right / Top variants
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Left Wall")).toEqual({ orientation: "west", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Left Path")).toEqual({ orientation: "west", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Left Ends")).toEqual({ orientation: "west", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Left King Tomb")).toEqual({ orientation: "west", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Right Wall")).toEqual({ orientation: "east", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Right Path")).toEqual({ orientation: "east", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Right Ends")).toEqual({ orientation: "east", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Right King Tomb")).toEqual({ orientation: "east", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Top")).toEqual({ orientation: "north", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 2 - Desert Cliff Top King Tomb")).toEqual({ orientation: "north", isCliff: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Act 3: Slums/Burbs/Metro Border cardinal directions (N, S, E, W, NE, NW, SE, SW)", () => {
|
||||||
|
expect(classifyBorderPiece("Act 3 - Slums Border N")).toEqual({ orientation: "north", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Slums Border S")).toEqual({ orientation: "south", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Slums Border E")).toEqual({ orientation: "east", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Slums Border W")).toEqual({ orientation: "west", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Slums Border NE")).toEqual({ orientation: "corner-ne", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Slums Border NW")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Slums Border SE")).toEqual({ orientation: "corner-se", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Slums Border SW")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
|
||||||
|
expect(classifyBorderPiece("Act 3 - Burbs Border N")).toEqual({ orientation: "north", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Burbs Border SW")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
|
||||||
|
expect(classifyBorderPiece("Act 3 - Metro Border E")).toEqual({ orientation: "east", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 3 - Metro Border SE")).toEqual({ orientation: "corner-se", isCliff: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Act 4: Mesa Border 1..12", () => {
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 1")).toEqual({ orientation: "south", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 2")).toEqual({ orientation: "west", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 3")).toEqual({ orientation: "north", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 4")).toEqual({ orientation: "east", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 5")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 6")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 7")).toEqual({ orientation: "corner-ne", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 8")).toEqual({ orientation: "corner-se", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 9")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 10")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 11")).toEqual({ orientation: "corner-ne", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 4 - Mesa Border 12")).toEqual({ orientation: "corner-se", isCliff: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Act 5: Barricade Cliff Border 1..12, Barricade Ravine Border 1..6, and Snow variants", () => {
|
||||||
|
// Barricade Cliff: isCliff: true
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 1")).toEqual({ orientation: "south", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 2")).toEqual({ orientation: "west", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 3")).toEqual({ orientation: "north", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 4")).toEqual({ orientation: "east", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 5")).toEqual({ orientation: "corner-sw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 6")).toEqual({ orientation: "corner-nw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 7")).toEqual({ orientation: "corner-ne", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 8")).toEqual({ orientation: "corner-se", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 9")).toEqual({ orientation: "corner-sw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 10")).toEqual({ orientation: "corner-nw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 11")).toEqual({ orientation: "corner-ne", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 12")).toEqual({ orientation: "corner-se", isCliff: true })
|
||||||
|
|
||||||
|
// Barricade Ravine: isCliff: false
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 1")).toEqual({ orientation: "south", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 2")).toEqual({ orientation: "west", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 3")).toEqual({ orientation: "north", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 4")).toEqual({ orientation: "east", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 5")).toEqual({ orientation: "corner-sw", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 6")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
|
||||||
|
// Corner join / transition pieces
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Ravine-Cliff Border 5")).toEqual({ orientation: "corner-sw", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Ravine Border 7")).toEqual({ orientation: "corner-ne", isCliff: true })
|
||||||
|
|
||||||
|
// Snow variants
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Cliff Border 1 Snow")).toEqual({ orientation: "south", isCliff: true })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Barricade Ravine Border 1 Snow")).toEqual({ orientation: "south", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Snow Border 1")).toEqual({ orientation: "south", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Snow Border 2")).toEqual({ orientation: "west", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Snow Border 3")).toEqual({ orientation: "north", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Snow Border 4")).toEqual({ orientation: "east", isCliff: false })
|
||||||
|
expect(classifyBorderPiece("Act 5 - Snow Border 6")).toEqual({ orientation: "corner-nw", isCliff: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Task 1B: selectBorderPiece cliff preference", () => {
|
||||||
|
test("prefers cliff pieces when preferCliff is true and non-cliff when false", () => {
|
||||||
|
const cliffPiece: WildernessPiece = {
|
||||||
|
name: "Act 1 - Wild Cliff Border 3",
|
||||||
|
border: true,
|
||||||
|
levels: [makeMockBorderDs1(8, 8, "north", false)],
|
||||||
|
}
|
||||||
|
const fencePiece: WildernessPiece = {
|
||||||
|
name: "Act 1 - Wild Border 3",
|
||||||
|
border: true,
|
||||||
|
levels: [makeMockBorderDs1(8, 8, "north", false)],
|
||||||
|
}
|
||||||
|
const classified = [
|
||||||
|
{ piece: cliffPiece, orientation: "north" as const, isCliff: true },
|
||||||
|
{ piece: fencePiece, orientation: "north" as const, isCliff: false },
|
||||||
|
]
|
||||||
|
const rng = new Rng(100)
|
||||||
|
|
||||||
|
const pickedCliff = selectBorderPiece("north", classified, rng, true)
|
||||||
|
expect(pickedCliff?.name).toBe("Act 1 - Wild Cliff Border 3")
|
||||||
|
|
||||||
|
const pickedFence = selectBorderPiece("north", classified, rng, false)
|
||||||
|
expect(pickedFence?.name).toBe("Act 1 - Wild Border 3")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back gracefully when only one cliff status is available", () => {
|
||||||
|
const fencePiece: WildernessPiece = {
|
||||||
|
name: "Act 4 - Mesa Border 3",
|
||||||
|
border: true,
|
||||||
|
levels: [makeMockBorderDs1(8, 8, "north", false)],
|
||||||
|
}
|
||||||
|
const classified = [{ piece: fencePiece, orientation: "north" as const, isCliff: false }]
|
||||||
|
const rng = new Rng(200)
|
||||||
|
|
||||||
|
// Even if preferCliff is true, falls back to fencePiece
|
||||||
|
const picked = selectBorderPiece("north", classified, rng, true)
|
||||||
|
expect(picked?.name).toBe("Act 4 - Mesa Border 3")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Task 1C & 2: DRLGOUTPLACE_PlaceAct1245OutdoorBorders & layBorder outer ring coverage", () => {
|
||||||
|
function buildMultiActPieces(prefix: string, hasCliffs = false): WildernessPiece[] {
|
||||||
|
const pieces: WildernessPiece[] = []
|
||||||
|
// Numbers 1..8
|
||||||
|
for (let i = 1; i <= 8; i += 1) {
|
||||||
|
const orientation = i === 1 ? "south" : i === 2 ? "west" : i === 3 ? "north" : i === 4 ? "east" :
|
||||||
|
i === 5 ? "corner-sw" : i === 6 ? "corner-nw" : i === 7 ? "corner-ne" : "corner-se"
|
||||||
|
const closedDs1 = makeMockBorderDs1(8, 8, orientation, false)
|
||||||
|
const openDs1 = makeMockBorderDs1(8, 8, orientation, true)
|
||||||
|
pieces.push({
|
||||||
|
name: `${prefix} Border ${i}`,
|
||||||
|
border: true,
|
||||||
|
// 4 variants: 0..2 closed, 3 open
|
||||||
|
levels: [closedDs1, closedDs1, closedDs1, openDs1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (hasCliffs) {
|
||||||
|
// Add cliff pieces for north and west
|
||||||
|
pieces.push({
|
||||||
|
name: `${prefix} Cliff Border 2`,
|
||||||
|
border: true,
|
||||||
|
levels: [makeMockBorderDs1(8, 8, "west", false)],
|
||||||
|
})
|
||||||
|
pieces.push({
|
||||||
|
name: `${prefix} Cliff Border 3`,
|
||||||
|
border: true,
|
||||||
|
levels: [makeMockBorderDs1(8, 8, "north", false)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ act: 1, name: "Act 1 - Wild", hasCliffs: true },
|
||||||
|
{ act: 2, name: "Act 2 - Desert", hasCliffs: false },
|
||||||
|
{ act: 4, name: "Act 4 - Mesa", hasCliffs: false },
|
||||||
|
{ act: 5, name: "Act 5 - Barricade", hasCliffs: true },
|
||||||
|
])("Act $act ($name): 100% outer ring coverage with 0 procedural fallback walls", ({ name, hasCliffs }) => {
|
||||||
|
const gridWidth = 8
|
||||||
|
const gridHeight = 8
|
||||||
|
const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 1, 1, 0)
|
||||||
|
const pieces = buildMultiActPieces(name, hasCliffs)
|
||||||
|
const stats = createEmptyStats()
|
||||||
|
const rng = new Rng(42)
|
||||||
|
|
||||||
|
const stamped = DRLGOUTPLACE_PlaceAct1245OutdoorBorders(
|
||||||
|
canvas,
|
||||||
|
pieces,
|
||||||
|
gridWidth,
|
||||||
|
gridHeight,
|
||||||
|
rng,
|
||||||
|
stats,
|
||||||
|
2, // Blood Moor level ID with South and East openings
|
||||||
|
)
|
||||||
|
|
||||||
|
// Expected ring block count: 2 * (w + h) - 4 = 2 * (8 + 8) - 4 = 28 blocks
|
||||||
|
const expectedRingBlocks = 2 * (gridWidth + gridHeight) - 4
|
||||||
|
expect(stamped).toBe(expectedRingBlocks)
|
||||||
|
expect(stats.borderStamped).toBe(expectedRingBlocks)
|
||||||
|
expect(stats.proceduralWallCells).toBe(0)
|
||||||
|
|
||||||
|
// Verify every perimeter cell on the canvas has floor and valid wall placement
|
||||||
|
for (let x = 0; x < canvas.width; x += 1) {
|
||||||
|
// North edge (y=0) and South edge (y=canvas.height-1)
|
||||||
|
expect(canvas.cells[0]?.[x]?.floors.length).toBeGreaterThan(0)
|
||||||
|
expect(canvas.cells[canvas.height - 1]?.[x]?.floors.length).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
for (let y = 0; y < canvas.height; y += 1) {
|
||||||
|
// West edge (x=0) and East edge (x=canvas.width-1)
|
||||||
|
expect(canvas.cells[y]?.[0]?.floors.length).toBeGreaterThan(0)
|
||||||
|
expect(canvas.cells[y]?.[canvas.width - 1]?.floors.length).toBeGreaterThan(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Opening blocks select Open DS1 variants while closed blocks select Closed DS1 variants", () => {
|
||||||
|
const gridWidth = 8
|
||||||
|
const gridHeight = 8
|
||||||
|
const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 1, 1, 0)
|
||||||
|
const pieces = buildMultiActPieces("Act 1 - Wild", true)
|
||||||
|
const stats = createEmptyStats()
|
||||||
|
const rng = new Rng(12345)
|
||||||
|
|
||||||
|
// Level 2 has default entrance on South edge (mid-X, y=7) and East edge (x=7, mid-Y)
|
||||||
|
DRLGOUTPLACE_PlaceAct1245OutdoorBorders(canvas, pieces, gridWidth, gridHeight, rng, stats, 2)
|
||||||
|
|
||||||
|
const midX = Math.floor(gridWidth / 2) // block (4, 7)
|
||||||
|
const midY = Math.floor(gridHeight / 2) // block (7, 4)
|
||||||
|
|
||||||
|
// South opening block: cell (4*8 + 4, 7*8 + y) should have the open gateway gap
|
||||||
|
const southOpenCenterCell = canvas.cells[7 * 8 + 4]?.[midX * 8 + 4]
|
||||||
|
expect(southOpenCenterCell).toBeDefined()
|
||||||
|
// Gateway center in open variant has no wall
|
||||||
|
expect(southOpenCenterCenterWallCount(canvas, midX * 8, 7 * 8, 8, 8)).toBe(false)
|
||||||
|
|
||||||
|
// Closed South block: cell (1*8, 7*8) should have solid continuous wall
|
||||||
|
expect(isBlockClosed(canvas, 1 * 8, 7 * 8, 8, 8)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
function southOpenCenterCenterWallCount(canvas: any, bx: number, by: number, w: number, h: number): boolean {
|
||||||
|
const centerX = bx + Math.floor(w / 2)
|
||||||
|
// Check if all cells in column centerX of this block are walls
|
||||||
|
for (let y = 0; y < h; y += 1) {
|
||||||
|
if (!canvas.cells[by + y]?.[centerX]?.walls.some((w: any) => !w.hidden && w.prop1 !== 0)) {
|
||||||
|
return false // There is an open path through this column!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlockClosed(canvas: any, bx: number, by: number, w: number, h: number): boolean {
|
||||||
|
for (let y = 0; y < h; y += 1) {
|
||||||
|
let hasWallRow = true
|
||||||
|
for (let x = 0; x < w; x += 1) {
|
||||||
|
if (!canvas.cells[by + y]?.[bx + x]?.walls.some((w: any) => !w.hidden && w.prop1 !== 0)) {
|
||||||
|
hasWallRow = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasWallRow) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Docking support: claimedBorderBlocks are skipped during border stamping", () => {
|
||||||
|
const gridWidth = 8
|
||||||
|
const gridHeight = 8
|
||||||
|
const canvas = createCanvas(gridWidth * 8, gridHeight * 8, 1, 1, 0)
|
||||||
|
const pieces = buildMultiActPieces("Act 1 - Wild", false)
|
||||||
|
const stats = createEmptyStats()
|
||||||
|
const rng = new Rng(777)
|
||||||
|
|
||||||
|
// Claim 3 blocks along the West edge for a Town Transition / Facade docking
|
||||||
|
const claimed = new Set(["0,2", "0,3", "0,4"])
|
||||||
|
|
||||||
|
const stamped = DRLGOUTPLACE_PlaceAct1245OutdoorBorders(
|
||||||
|
canvas,
|
||||||
|
pieces,
|
||||||
|
gridWidth,
|
||||||
|
gridHeight,
|
||||||
|
rng,
|
||||||
|
stats,
|
||||||
|
2,
|
||||||
|
undefined,
|
||||||
|
claimed,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 28 total perimeter blocks - 3 claimed blocks = 25 stamped blocks
|
||||||
|
expect(stamped).toBe(25)
|
||||||
|
expect(stats.borderStamped).toBe(25)
|
||||||
|
|
||||||
|
// Verified claimed blocks were skipped: canvas floor/walls at block (0, 3) were NOT painted by border
|
||||||
|
expect(canvas.cells[3 * 8]?.[0]?.walls[0]?.style).toBe(0)
|
||||||
|
expect(canvas.cells[1 * 8]?.[0]?.walls[0]?.style).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("layBorder is an exported alias for DRLGOUTPLACE_PlaceAct1245OutdoorBorders", () => {
|
||||||
|
expect(layBorder).toBe(DRLGOUTPLACE_PlaceAct1245OutdoorBorders)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("getCellOrientation correctly maps 4 corners and 4 edges", () => {
|
||||||
|
expect(getCellOrientation(0, 0, 10, 10)).toBe("corner-nw")
|
||||||
|
expect(getCellOrientation(9, 0, 10, 10)).toBe("corner-ne")
|
||||||
|
expect(getCellOrientation(9, 9, 10, 10)).toBe("corner-se")
|
||||||
|
expect(getCellOrientation(0, 9, 10, 10)).toBe("corner-sw")
|
||||||
|
expect(getCellOrientation(5, 0, 10, 10)).toBe("north")
|
||||||
|
expect(getCellOrientation(9, 5, 10, 10)).toBe("east")
|
||||||
|
expect(getCellOrientation(5, 9, 10, 10)).toBe("south")
|
||||||
|
expect(getCellOrientation(0, 5, 10, 10)).toBe("west")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue