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

SymbolShape
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

ConstantValuePurpose
PILLAR_HP300Tuned so 2–3 player weapon volleys clear a piece.
PILLAR_RADIUS30Collision/render radius (world units).
HAZARD_DPS18Chip damage — punishes camping, won’t insta-kill.
HAZARD_RADIUS70Hazard 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

IDDestructiblesHazardsUsed by
opennonenonedefault (no terrain)
pillar_ring6 pillars, ring at 65% radius (arena.ringPoints(6, 0.65))Hive Queen
pillar_cross4 pillars, + cross at 55% radius (arena.cardinalPoints(0.55))Doomsayer
hazard_padsnone4 damaging pads, ring at 50% radius (arena.ringPoints(4, 0.5))Awakened Mech phase 3
corridor8 pillars, 4 per wall along the long axis at ±55% short-axis offsetReactor Core (rect 800×400)
gauntlet6 pillars, two staggered rows of 3 at ±40% short-axis offsetJunkrat 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

  • positionFn only runs once. Patterns assume arenas don’t resize mid-encounter.
  • corridor and gauntlet use arena.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 the 0.55 / 0.65 / 0.4 multipliers to stay inside the playfield.
  • hazard_pads has no destructibles; open has neither. Consumers must handle empty destructibles arrays and absent hazardZones.

Producers / consumers

SideWhereWhat
Producerthis fileExports TERRAIN_PATTERNS keyed by TerrainPatternId.
ConsumerBossArena (../engine/boss/arena)Provides ringPoints, cardinalPoints, bounds, cx, cy used by positionFns.
ConsumerBoss-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.
ConsumerBoss 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 in corridor, fragile decoys in gauntlet) split into per-pattern values or a tuning table.
  • corridor and gauntlet both inline the same long-axis-detection block (horizontal = halfW >= halfH, longSpan/shortSpan derivation). If a third axis-aware pattern lands, extract a longAxisFrame(arena) helper returning { horizontal, longSpan, shortSpan, place(along, across) }.
  • The hard-coded t ∈ [-0.7, 0.7] corridor span and cols = [-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.
  • positionFn returning 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 multiple TerrainPillarSpec groups per pattern.
  • No rotation parameter — patterns always align to world axes. If a boss wants a rotated pillar_cross, add rotationRad to TerrainPatternDef or to per-spec generators.