terrain-patterns.ts
Static layout library — six named patterns of destructible pillars and/or damaging hazard zones that drop into a boss arena on encounter start and clear on encounter end. Sized relative to the live arena via BossArena helpers (ringPoints, cardinalPoints, bounds, cx, cy).
Spec: docs/superpowers/specs/2026-04-25-bosses-as-enemies-design.md §3.7, §4 (boss roster terrain column).
Across the Styx
Bridge into the lake of the dead. Each pattern is data only — geometry resolves once on encounter start when handed the arena bounds. No per-frame work lives here.
Types
| Symbol | Shape |
|---|---|
TerrainPatternId | 'open' | 'pillar_ring' | 'pillar_cross' | 'hazard_pads' | 'corridor' | 'gauntlet' |
TerrainPillarSpec | { hp: number; radius: number; positionFn: (arena) => {x,y}[] } |
TerrainHazardSpec | { positionFn: (arena) => {x,y}[]; radius: number; damagePerSec: number } |
TerrainPatternDef | { id: TerrainPatternId; destructibles: TerrainPillarSpec[]; hazardZones?: TerrainHazardSpec[] } |
positionFn runs once on encounter start with the live BossArena. Returned array length determines pillar/hazard count.
Tuning constants
| Constant | Value | Purpose |
|---|---|---|
PILLAR_HP | 300 | Tuned so 2–3 player weapon volleys clear a piece. |
PILLAR_RADIUS | 30 | Collision/render radius (world units). |
HAZARD_DPS | 18 | Chip damage — punishes camping, won’t insta-kill. |
HAZARD_RADIUS | 70 | Hazard zone radius (world units). Ship overlap triggers DPS. |
Lethe — what is forgotten
Patterns are stateless. Pillar HP, hazard DPS, and counts are baked in at module load; runtime state lives on the spawned pillar/hazard entities, not here.
Patterns
| ID | Destructibles | Hazards | Used by |
|---|---|---|---|
open | none | none | default (no terrain) |
pillar_ring | 6 pillars, ring at 65% radius (arena.ringPoints(6, 0.65)) | — | Hive Queen |
pillar_cross | 4 pillars, + cross at 55% radius (arena.cardinalPoints(0.55)) | — | Doomsayer |
hazard_pads | none | 4 damaging pads, ring at 50% radius (arena.ringPoints(4, 0.5)) | Awakened Mech phase 3 |
corridor | 8 pillars, 4 per wall along the long axis at ±55% short-axis offset | — | Reactor Core (rect 800×400) |
gauntlet | 6 pillars, two staggered rows of 3 at ±40% short-axis offset | — | Junkrat Captains |
corridor geometry
Detects long axis from arena.bounds() (horizontal = halfW >= halfH). Each wall has 4 pillars evenly distributed at t ∈ [-0.7, 0.7] along the long axis. Walls sit at ±halfShort * 0.55 from arena center on the short axis. Total: 8 pillars in two parallel lines flanking a central corridor.
gauntlet geometry
Same long-axis detection as corridor. Two rows of 3 pillars each at columns [-0.55, 0, 0.55] * longSpan. Rows sit at ±shortSpan * 0.4 from center. The far row (r === 1) is staggered by longSpan * 0.275 along the long axis so adjacent rows don’t perfectly align — low cover that breaks sightlines without making lanes.
Cocytus — boundary checks
positionFnonly runs once. Patterns assume arenas don’t resize mid-encounter.corridorandgauntletusearena.bounds()and the bounding-box ratio to pick orientation. A near-square rect or any circle falls through to horizontal layout (halfW >= halfH).- No bounds clipping inside
positionFn— relies on the helpers (ringPoints,cardinalPoints) and the0.55 / 0.65 / 0.4multipliers to stay inside the playfield. hazard_padshas no destructibles;openhas neither. Consumers must handle emptydestructiblesarrays and absenthazardZones.
Producers / consumers
| Side | Where | What |
|---|---|---|
| Producer | this file | Exports TERRAIN_PATTERNS keyed by TerrainPatternId. |
| Consumer | BossArena (../engine/boss/arena) | Provides ringPoints, cardinalPoints, bounds, cx, cy used by positionFns. |
| Consumer | Boss-encounter setup (per spec §3.7) | Looks up pattern by ID, calls each positionFn with the live arena, spawns destructibles + hazards, clears them on encounter end. |
| Consumer | Boss roster (per spec §4) | Each boss row names a TerrainPatternId in its terrain column. |
EXTRACT-CANDIDATE
PILLAR_HP,PILLAR_RADIUS,HAZARD_DPS,HAZARD_RADIUS— currently single tuning knobs for all patterns. If future patterns need varied pillar HP (e.g. tankier pillars incorridor, fragile decoys ingauntlet) split into per-pattern values or a tuning table.corridorandgauntletboth inline the same long-axis-detection block (horizontal = halfW >= halfH,longSpan/shortSpanderivation). If a third axis-aware pattern lands, extract alongAxisFrame(arena)helper returning{ horizontal, longSpan, shortSpan, place(along, across) }.- The hard-coded
t ∈ [-0.7, 0.7]corridor span andcols = [-0.55, 0, 0.55]gauntlet columns are magic ratios — promote to named constants if any boss needs to tweak corridor tightness or gauntlet column count. positionFnreturning a flat{x,y}[]cannot express per-pillar HP variation. If asymmetric pillars are needed (e.g. central pillar tankier), widen to{x,y, hpScale?}[]or split into multipleTerrainPillarSpecgroups per pattern.- No rotation parameter — patterns always align to world axes. If a boss wants a rotated
pillar_cross, addrotationRadtoTerrainPatternDefor to per-spec generators.