504 lines
16 KiB
TypeScript
504 lines
16 KiB
TypeScript
/**
|
|
* Act 5 Bloody Foothills (Level 110, Act 5 - Siege 1) dynamic siege generation.
|
|
*
|
|
* Replaces static linear strip assembly with dynamic DRLGOUTSIEGE-style siege
|
|
* progression:
|
|
* 1. Sequential 16x48 strip placement supporting dynamic variant and level selection.
|
|
* 2. Battlefield trenches (style: 5, prop1: 194) with cleared wall channels.
|
|
* 3. Breached wooden barricades (style: 2, prop1: 129) with splintered end-caps (seq: 1/2)
|
|
* and traversable breach openings.
|
|
* 4. Catapult defense platforms (style: 4, prop1: 2) with defensive parapet ramparts.
|
|
* 5. Crumbled stone fort ruins interspersed between siege strips.
|
|
* 6. Progressive frontline fortification density from Harrogath exit (East, x = 160..240, low)
|
|
* towards Shenk the Overseer / Arreat Plateau barricade (West, x = 0..80, high).
|
|
*/
|
|
|
|
import type { Ds1, Ds1Floor, Ds1Wall } from '../formats/ds1.ts'
|
|
import { Rng } from './rng.ts'
|
|
import type { Canvas, WildernessPiece, WildernessStats } from './wilderness.ts'
|
|
import { stampDs1, emptyWall } from './wilderness.ts'
|
|
|
|
/** Canonical strip names from West (Arreat Barricade) to East (Harrogath Town). */
|
|
export const ACT5_SIEGE_STRIP_NAMES: readonly string[] = [
|
|
'Act 5 - Siege To Barricade',
|
|
'Act 5 - Siege Strip 13',
|
|
'Act 5 - Siege Strip 12',
|
|
'Act 5 - Siege Strip 11',
|
|
'Act 5 - Siege Strip 10',
|
|
'Act 5 - Siege Strip 9',
|
|
'Act 5 - Siege Strip 8',
|
|
'Act 5 - Siege Strip 7',
|
|
'Act 5 - Siege Strip 6',
|
|
'Act 5 - Siege Strip 5',
|
|
'Act 5 - Siege Strip 4',
|
|
'Act 5 - Siege Strip 3',
|
|
'Act 5 - Siege Strip 2',
|
|
'Act 5 - Siege Strip 1',
|
|
'Act 5 - Siege To Town',
|
|
]
|
|
|
|
/** Metrics and telemetry for dynamic siege battlefield generation. */
|
|
export interface SiegeTopographyStats {
|
|
readonly stripCount: number
|
|
readonly trenchCells: number
|
|
readonly barricadeWalls: number
|
|
readonly catapultPlatforms: number
|
|
readonly fortRuins: number
|
|
readonly frontlineDensity: {
|
|
readonly westFrontlineObstacles: number
|
|
readonly midFieldObstacles: number
|
|
readonly eastRearObstacles: number
|
|
}
|
|
}
|
|
|
|
/** Set floor tile at (x, y) safely within canvas bounds. */
|
|
function setFloor(canvas: Canvas, x: number, y: number, floor: Ds1Floor): void {
|
|
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) return
|
|
const row = canvas.cells[y]
|
|
if (!row) return
|
|
const cell = row[x]
|
|
if (!cell) return
|
|
const targetFloors = cell.floors as Ds1Floor[]
|
|
if (targetFloors.length > 0) {
|
|
targetFloors[0] = floor
|
|
} else {
|
|
targetFloors.push(floor)
|
|
}
|
|
}
|
|
|
|
/** Set wall tile at (x, y) safely within canvas bounds. */
|
|
function setWall(canvas: Canvas, x: number, y: number, wall: Ds1Wall): void {
|
|
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) return
|
|
const row = canvas.cells[y]
|
|
if (!row) return
|
|
const cell = row[x]
|
|
if (!cell) return
|
|
const targetWalls = cell.walls as Ds1Wall[]
|
|
if (targetWalls.length > 0) {
|
|
targetWalls[0] = wall
|
|
} else {
|
|
targetWalls.push(wall)
|
|
}
|
|
}
|
|
|
|
/** Clear all wall layers at (x, y) to guarantee traversability. */
|
|
function clearCellWalls(canvas: Canvas, x: number, y: number): void {
|
|
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) return
|
|
const row = canvas.cells[y]
|
|
if (!row) return
|
|
const cell = row[x]
|
|
if (!cell) return
|
|
const targetWalls = cell.walls as Ds1Wall[]
|
|
for (let i = 0; i < targetWalls.length; i += 1) {
|
|
targetWalls[i] = emptyWall()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stamp a dugout battlefield trench (style: 5, prop1: 194).
|
|
* Clears walls in the trench path to guarantee traversable channels.
|
|
*/
|
|
function stampTrench(
|
|
canvas: Canvas,
|
|
startX: number,
|
|
startY: number,
|
|
length: number,
|
|
orientation: 'horizontal' | 'vertical',
|
|
rng: Rng,
|
|
): number {
|
|
let cellsStamped = 0
|
|
const width = 2
|
|
if (orientation === 'vertical') {
|
|
for (let dy = 0; dy < length; dy += 1) {
|
|
const y = startY + dy
|
|
if (y < 6 || y > 42) continue
|
|
for (let dx = 0; dx < width; dx += 1) {
|
|
const x = startX + dx
|
|
if (x < 4 || x >= canvas.width - 4) continue
|
|
setFloor(canvas, x, y, {
|
|
style: 5,
|
|
sequence: rng.int(0, 5),
|
|
prop1: 194,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
clearCellWalls(canvas, x, y)
|
|
cellsStamped += 1
|
|
}
|
|
}
|
|
} else {
|
|
for (let dx = 0; dx < length; dx += 1) {
|
|
const x = startX + dx
|
|
if (x < 4 || x >= canvas.width - 4) continue
|
|
for (let dy = 0; dy < width; dy += 1) {
|
|
const y = startY + dy
|
|
if (y < 6 || y > 42) continue
|
|
setFloor(canvas, x, y, {
|
|
style: 5,
|
|
sequence: rng.int(0, 5),
|
|
prop1: 194,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
clearCellWalls(canvas, x, y)
|
|
cellsStamped += 1
|
|
}
|
|
}
|
|
}
|
|
return cellsStamped
|
|
}
|
|
|
|
/**
|
|
* Stamp a breached wooden barricade palisade (style: 2, prop1: 129).
|
|
* Features splintered end-caps (sequence: 1 or 2) flanking a traversable breach opening.
|
|
*/
|
|
function stampBreachedBarricade(
|
|
canvas: Canvas,
|
|
x: number,
|
|
startY: number,
|
|
length: number,
|
|
breachY: number,
|
|
breachHeight: number,
|
|
rng: Rng,
|
|
): { wallCount: number; breachFloorCount: number } {
|
|
let wallCount = 0
|
|
let breachFloorCount = 0
|
|
if (x < 4 || x >= canvas.width - 4) return { wallCount: 0, breachFloorCount: 0 }
|
|
|
|
for (let dy = 0; dy < length; dy += 1) {
|
|
const y = startY + dy
|
|
if (y < 6 || y > 42) continue
|
|
|
|
if (y >= breachY && y < breachY + breachHeight) {
|
|
// Traversable breach gap: clear walls and trample floor
|
|
clearCellWalls(canvas, x, y)
|
|
setFloor(canvas, x, y, {
|
|
style: 5,
|
|
sequence: 0,
|
|
prop1: 194,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
breachFloorCount += 1
|
|
} else if (y === breachY - 1 || y === breachY + breachHeight) {
|
|
// Splintered end-cap post at the breach edge
|
|
setWall(canvas, x, y, {
|
|
style: 2,
|
|
sequence: rng.int(1, 2),
|
|
prop1: 129,
|
|
type: 1,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
wallCount += 1
|
|
} else {
|
|
// Intact spiked wooden palisade
|
|
setWall(canvas, x, y, {
|
|
style: 2,
|
|
sequence: 0,
|
|
prop1: 129,
|
|
type: 1,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
wallCount += 1
|
|
}
|
|
}
|
|
return { wallCount, breachFloorCount }
|
|
}
|
|
|
|
/**
|
|
* Stamp a raised catapult defense platform (style: 4, prop1: 2) with defensive parapets.
|
|
* Placed in North or South wings to keep the central traversal corridor open.
|
|
*/
|
|
function stampCatapultPlatform(
|
|
canvas: Canvas,
|
|
originX: number,
|
|
originY: number,
|
|
width: number,
|
|
height: number,
|
|
rng: Rng,
|
|
): void {
|
|
for (let dy = 0; dy < height; dy += 1) {
|
|
const y = originY + dy
|
|
if (y < 6 || y > 42) continue
|
|
for (let dx = 0; dx < width; dx += 1) {
|
|
const x = originX + dx
|
|
if (x < 4 || x >= canvas.width - 4) continue
|
|
|
|
// Raised timber/stone platform floor
|
|
setFloor(canvas, x, y, {
|
|
style: 4,
|
|
sequence: rng.int(0, 3),
|
|
prop1: 2,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
|
|
// Parapet rampart along outer perimeter, leaving access opening on interior side
|
|
const isPerimeter = dx === 0 || dx === width - 1 || dy === 0 || dy === height - 1
|
|
const isAccessGap = (originY < 20 && dy === height - 1 && dx >= 1 && dx <= 2) ||
|
|
(originY > 26 && dy === 0 && dx >= 1 && dx <= 2)
|
|
if (isPerimeter && !isAccessGap) {
|
|
setWall(canvas, x, y, {
|
|
style: 4,
|
|
sequence: 0,
|
|
prop1: 2,
|
|
type: 1,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stamp crumbled stone fort ruins (style: 1) interspersed between strips.
|
|
* Features weathered stone flagstone floors and broken masonry walls.
|
|
*/
|
|
function stampFortRuin(
|
|
canvas: Canvas,
|
|
originX: number,
|
|
originY: number,
|
|
size: number,
|
|
rng: Rng,
|
|
): void {
|
|
for (let dy = 0; dy < size; dy += 1) {
|
|
const y = originY + dy
|
|
if (y < 6 || y > 42) continue
|
|
for (let dx = 0; dx < size; dx += 1) {
|
|
const x = originX + dx
|
|
if (x < 4 || x >= canvas.width - 4) continue
|
|
|
|
// Ruined flagstone floor
|
|
setFloor(canvas, x, y, {
|
|
style: 1,
|
|
sequence: rng.int(0, 2),
|
|
prop1: 2,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
|
|
// Weathered/crumbled perimeter wall with missing sections
|
|
const isPerimeter = dx === 0 || dx === size - 1 || dy === 0 || dy === size - 1
|
|
if (isPerimeter && rng.next() > 0.35) {
|
|
setWall(canvas, x, y, {
|
|
style: 1,
|
|
sequence: rng.int(0, 4),
|
|
prop1: 129,
|
|
type: 1,
|
|
unknown1: 0,
|
|
unknown2: 0,
|
|
hidden: false,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Find and choose strip DS1 piece supporting dynamic variants and level variations. */
|
|
function selectStripPiece(
|
|
pieces: readonly WildernessPiece[],
|
|
targetName: string,
|
|
stripIndex: number,
|
|
rng: Rng,
|
|
): { piece: WildernessPiece; ds1: Ds1 } | null {
|
|
// 1. Direct exact name match
|
|
const exact = pieces.filter(p => p.name === targetName && p.levels.length > 0)
|
|
if (exact.length > 0) {
|
|
const chosen = exact[rng.int(0, exact.length - 1)]!
|
|
const ds1 = chosen.levels[rng.int(0, chosen.levels.length - 1)]!
|
|
return { piece: chosen, ds1 }
|
|
}
|
|
|
|
// 2. Prefix or variant matches (e.g. "Act 5 - Siege Strip 5 A")
|
|
const variantMatches = pieces.filter(
|
|
p => p.name.startsWith(targetName) && p.levels.length > 0,
|
|
)
|
|
if (variantMatches.length > 0) {
|
|
const chosen = variantMatches[rng.int(0, variantMatches.length - 1)]!
|
|
const ds1 = chosen.levels[rng.int(0, chosen.levels.length - 1)]!
|
|
return { piece: chosen, ds1 }
|
|
}
|
|
|
|
// 3. Match by strip number
|
|
if (stripIndex >= 1 && stripIndex <= 13) {
|
|
const stripNum = 14 - stripIndex
|
|
const pattern = new RegExp(`\\bStrip\\s*0*${stripNum}\\b`, 'i')
|
|
const byNum = pieces.filter(p => pattern.test(p.name) && p.levels.length > 0)
|
|
if (byNum.length > 0) {
|
|
const chosen = byNum[rng.int(0, byNum.length - 1)]!
|
|
const ds1 = chosen.levels[rng.int(0, chosen.levels.length - 1)]!
|
|
return { piece: chosen, ds1 }
|
|
}
|
|
}
|
|
|
|
// 4. Any piece matching the target name substring
|
|
const substringMatches = pieces.filter(
|
|
p => p.name.toLowerCase().includes(targetName.toLowerCase()) && p.levels.length > 0,
|
|
)
|
|
if (substringMatches.length > 0) {
|
|
const chosen = substringMatches[rng.int(0, substringMatches.length - 1)]!
|
|
const ds1 = chosen.levels[rng.int(0, chosen.levels.length - 1)]!
|
|
return { piece: chosen, ds1 }
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Assemble the 15 sequential 16x48 siege strips for Act 5 Bloody Foothills (Level 110)
|
|
* with dynamic strip variations, battlefield trenches, breached barricades,
|
|
* catapult defense platforms, and fort ruins with progressive frontline density.
|
|
*/
|
|
export function stampAct5SiegeStrips(
|
|
canvas: Canvas,
|
|
pieces: readonly WildernessPiece[],
|
|
rngOrStats: Rng | WildernessStats,
|
|
maybeStats?: WildernessStats,
|
|
): Set<string> {
|
|
const rng = rngOrStats instanceof Rng ? rngOrStats : new Rng(42)
|
|
const stats = rngOrStats instanceof Rng ? maybeStats! : rngOrStats
|
|
const placed = new Set<string>()
|
|
|
|
// 1. Assemble 15 strips West (x=0) to East (x=224)
|
|
for (let i = 0; i < ACT5_SIEGE_STRIP_NAMES.length; i += 1) {
|
|
const targetName = ACT5_SIEGE_STRIP_NAMES[i]!
|
|
const selection = selectStripPiece(pieces, targetName, i, rng)
|
|
if (!selection) continue
|
|
|
|
stampDs1(canvas, selection.ds1, i * 16, 0)
|
|
placed.add(selection.piece.name)
|
|
stats.substitutions.push({
|
|
name: selection.piece.name,
|
|
role: 'object',
|
|
enabled: true,
|
|
clusters: 1,
|
|
})
|
|
}
|
|
|
|
// 2. Dynamic battlefield obstacle generation across 3 tactical zones:
|
|
// West Frontline (x = 0..80): Heavy fortification near Arreat Barricade / Shenk
|
|
// Mid Field (x = 80..160): Moderate fortifications in no-man's land
|
|
// East Rear (x = 160..240): Light defenses near Harrogath town gates
|
|
let totalTrenchCells = 0
|
|
let totalBarricadeWalls = 0
|
|
let catapultPlatformCount = 0
|
|
let fortRuinCount = 0
|
|
|
|
let westObstacles = 0
|
|
let midObstacles = 0
|
|
let eastObstacles = 0
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Zone 1: West Frontline (x in [0, 80)) - High density (Shenk's stronghold)
|
|
// -------------------------------------------------------------------------
|
|
// Trenches (3 trenches)
|
|
totalTrenchCells += stampTrench(canvas, 18, 8, 10, 'vertical', rng)
|
|
totalTrenchCells += stampTrench(canvas, 50, 28, 10, 'vertical', rng)
|
|
totalTrenchCells += stampTrench(canvas, 34, 12, 7, 'horizontal', rng)
|
|
westObstacles += 3
|
|
|
|
// Breached Barricades (3 barricades)
|
|
const b1 = stampBreachedBarricade(canvas, 26, 10, 24, 21, 5, rng)
|
|
totalBarricadeWalls += b1.wallCount
|
|
totalTrenchCells += b1.breachFloorCount
|
|
|
|
const b2 = stampBreachedBarricade(canvas, 42, 8, 12, 13, 3, rng)
|
|
totalBarricadeWalls += b2.wallCount
|
|
totalTrenchCells += b2.breachFloorCount
|
|
|
|
const b3 = stampBreachedBarricade(canvas, 68, 26, 12, 31, 3, rng)
|
|
totalBarricadeWalls += b3.wallCount
|
|
totalTrenchCells += b3.breachFloorCount
|
|
westObstacles += 3
|
|
|
|
// Catapult Platforms (2 platforms)
|
|
stampCatapultPlatform(canvas, 12, 10, 4, 4, rng)
|
|
stampCatapultPlatform(canvas, 58, 30, 4, 4, rng)
|
|
catapultPlatformCount += 2
|
|
westObstacles += 2
|
|
|
|
// Fort Ruins (2 ruins)
|
|
stampFortRuin(canvas, 30, 30, 4, rng)
|
|
stampFortRuin(canvas, 64, 10, 4, rng)
|
|
fortRuinCount += 2
|
|
westObstacles += 2
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Zone 2: Mid Field (x in [80, 160)) - Medium density (No-man's land)
|
|
// -------------------------------------------------------------------------
|
|
// Trenches (2 trenches)
|
|
totalTrenchCells += stampTrench(canvas, 98, 10, 10, 'vertical', rng)
|
|
totalTrenchCells += stampTrench(canvas, 138, 28, 10, 'vertical', rng)
|
|
midObstacles += 2
|
|
|
|
// Breached Barricades (2 barricades)
|
|
const b4 = stampBreachedBarricade(canvas, 114, 10, 24, 21, 5, rng)
|
|
totalBarricadeWalls += b4.wallCount
|
|
totalTrenchCells += b4.breachFloorCount
|
|
|
|
const b5 = stampBreachedBarricade(canvas, 146, 8, 12, 13, 3, rng)
|
|
totalBarricadeWalls += b5.wallCount
|
|
totalTrenchCells += b5.breachFloorCount
|
|
midObstacles += 2
|
|
|
|
// Catapult Platforms (1 platform)
|
|
stampCatapultPlatform(canvas, 86, 30, 4, 4, rng)
|
|
catapultPlatformCount += 1
|
|
midObstacles += 1
|
|
|
|
// Fort Ruins (1 ruin)
|
|
stampFortRuin(canvas, 124, 10, 4, rng)
|
|
fortRuinCount += 1
|
|
midObstacles += 1
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Zone 3: East Rear (x in [160, 240)) - Low density (Harrogath outskirts)
|
|
// -------------------------------------------------------------------------
|
|
// Trenches (1 trench)
|
|
totalTrenchCells += stampTrench(canvas, 186, 12, 10, 'vertical', rng)
|
|
eastObstacles += 1
|
|
|
|
// Breached Barricades (1 barricade)
|
|
const b6 = stampBreachedBarricade(canvas, 202, 10, 24, 21, 5, rng)
|
|
totalBarricadeWalls += b6.wallCount
|
|
totalTrenchCells += b6.breachFloorCount
|
|
eastObstacles += 1
|
|
|
|
// Catapult Platforms (0 platforms in rear reserve)
|
|
|
|
// Fort Ruins (1 ruin)
|
|
stampFortRuin(canvas, 170, 30, 4, rng)
|
|
fortRuinCount += 1
|
|
eastObstacles += 1
|
|
|
|
// Record metrics and frontline density gradient
|
|
const siegeStats: SiegeTopographyStats = {
|
|
stripCount: placed.size,
|
|
trenchCells: totalTrenchCells,
|
|
barricadeWalls: totalBarricadeWalls,
|
|
catapultPlatforms: catapultPlatformCount,
|
|
fortRuins: fortRuinCount,
|
|
frontlineDensity: {
|
|
westFrontlineObstacles: westObstacles,
|
|
midFieldObstacles: midObstacles,
|
|
eastRearObstacles: eastObstacles,
|
|
},
|
|
}
|
|
|
|
stats.siegeTopography = siegeStats
|
|
stats.notes.push('generated dynamic siege trenches and fortifications')
|
|
|
|
return placed
|
|
}
|