Boss Arena Position Helpers

BossArena is a spatial helper layer wrapping a live BossRoom. Affixes, abilities, AI, and spawn profiles route through it so bosses never reference screen size or world coordinates directly. Every spawn position must come from one of: contains / clampPoint / randomPoint / cardinalPoints / ringPoints / edgePoints / oppositePlayer. Spec: docs/superpowers/specs/2026-04-25-bosses-as-enemies-design.md §2.6.

Built by createBossArena(room) in src/starship-survivors/engine/boss/arena.ts.

Arena geometry — cx, cy, radius, bounds

The arena exposes the room’s center as live getters:

  • arena.cxroom.cx
  • arena.cyroom.cy
  • arena.shape'circle' | 'rect'

Two footprint sources back the helpers:

  • Target footprint (room.targetR / room.targetW/room.targetH) is used by all spawn-placement helpers — randomPoint, cardinalPoints, ringPoints, edgePoints, radius, diagonal, bounds. Stable while the room is closing so placements don’t drift mid-animation.
  • Current footprint (room.currentR / room.currentW/room.currentH) is used by contains and clampPoint. Leash + cull check the actual boundary at this instant.

arena.radius() returns target radius (for circle) or min(targetW, targetH) / 2 (for rect). arena.diagonal() returns 2 * targetRadius or hypot(targetW, targetH). arena.bounds() returns {minX, minY, maxX, maxY} from the target footprint.

Ring points — count, fracOfRadius

ringPoints(count, fracOfRadius) returns count positions evenly spaced around a circle of radius targetRadius * fracOfRadius centered on (cx, cy). The first point is at angle 0 (east); subsequent points step by 2π / count clockwise (in screen coords).

const a = (i / count) * Math.PI * 2;
{ x: cx + cos(a) * r, y: cy + sin(a) * r }

Returns [] for count <= 0. Ignores shape — always a circle (not perimeter-walking like edgePoints). Use for: ability patterns that spawn projectiles or minions in a ring at a chosen fraction of the arena’s radius (e.g. 0.6 = inner ring, 0.95 = near edge).

Cardinal points — fracOfRadius

cardinalPoints(fracOfRadius) returns exactly four positions: E, S, W, N (in that order), each at targetRadius * fracOfRadius from center. Equivalent to ringPoints(4, fracOfRadius) but iteration order starts east and goes south first. Used by terrain-pattern positionFn and ability patterns that need the four anchor directions (e.g. four-pillar spawn, cardinal-beam attacks).

Edge points — perimeter walk

edgePoints(count) returns count positions evenly spaced along the outer boundary (radius-1, not radius-frac).

  • Circle: identical to ringPoints(count, 1.0).
  • Rect: walks the perimeter parametrically. t ranges 0..perimeter where perimeter = 2 * (targetW + targetH). The walk starts top-left corner, goes east along top, south down right side, west along bottom, north up left side.

Used by ability patterns that need wall-hugging spawns (rect arenas) or perimeter rings (circles).

Other helpers used by patterns

  • randomPoint(margin = 20) — uniform inside the arena. Circle uses r = R * sqrt(u) for uniform disc sampling; rect uses uniform per-axis.
  • clampPoint(x, y, margin = 20) — projects a point inward to stay inside the current boundary minus margin. Used by abilities that compute a candidate spot then need it legalized.
  • contains(x, y) — tests against current footprint. Used by leash + cull, never by spawn placement.
  • oppositePlayer(playerX, playerY, dist) — mirrors the player across center, projects to dist from center, clamps inside. Used by abilities that telegraph at the spot opposite the player (e.g. AoE the player must run toward).

How patterns compute positions around the boss

Pattern authors never read raw room dims. The flow is:

  1. Pattern receives arena: BossArena in its tick/spawn callback.
  2. For symmetric N-point patterns: arena.ringPoints(n, frac).
  3. For 4-anchor patterns: arena.cardinalPoints(frac).
  4. For wall/edge patterns: arena.edgePoints(n).
  5. For random scatter: arena.randomPoint(margin).
  6. For player-relative anchors: arena.oppositePlayer(px, py, dist).
  7. Any custom candidate position is run through arena.clampPoint before spawning.

DEFAULT_MARGIN = 20 for clampPoint and randomPoint. Patterns wanting tighter or looser packing pass an explicit margin.