terrain-patterns.ts
PURPOSE
Boss-terrain runtime that applies, culls, damages, and ticks the destructible pillars and damaging hazard zones defined by a TerrainPatternId. On encounter start applyTerrainPattern() materializes the pattern’s pillars and hazard zones into the live world, tagging each entry with _isBossTerrain = true. On encounter end cullTerrainPattern() removes every tagged entry. tickHazardZones() is the per-frame hook that drains player HP while the ship overlaps a hazard pad. Spec reference is docs/superpowers/specs/2026-04-25-bosses-as-enemies-design.md §3.7.
OWNS
BossPillarinterface — circular destructible cover withkind: 'pillar',x,y,radius,hp,hpMax,alive, and the_isBossTerrain: truetag.BossHazardZoneinterface —kind: 'hazard',x,y,radius,damagePerSec, wall-clockphase(pulse-glow render), and the_isBossTerrain: truetag.BossTerrainEntryunion of the two.TerrainHostWorldminimal world contract —destructibles: BossTerrainEntry[] | unknown[]. Keeps the module decoupled fromcore/types.WorldState.- The
isBossTerraintype-guard helper. - The lifecycle functions
applyTerrainPattern,cullTerrainPattern,damageBossPillar,tickHazardZones,reapDeadBossPillars.
READS FROM
../boss/arena—BossArenatype, passed to each pattern’spositionFnso generators size relative to the live arena.../../data/terrain-patterns—TerrainPatternIdtype and theTERRAIN_PATTERNSregistry. Each pattern definition providesdestructibles(array of pillar specs withhp,radius,positionFn) and optionalhazardZones(array of hazard specs withradius,damagePerSec,positionFn).- Caller-supplied ship position and radius via
tickHazardZonesparameters (shipX,shipY,shipRadius). - Caller-supplied
dt(game-dt) andapplyDamagecallback for hazard ticks.
PUSHES TO
world.destructibles— pushes newBossPillarandBossHazardZonerecords on apply; splice-and-pop removal on cull and on reap.applyDamagecallback (caller-provided) — invoked withdamagePerSec * dtper overlap per frame.BossPillar.hp/BossPillar.alive—damageBossPillarmutates in place, clampshpto0, setsalive = falseat or below zero.BossHazardZone.phase— incremented bydteach tick for the warning-glow render.
DOES NOT
- Does not render anything. Pillars and hazard zones are visual responsibilities of the appropriate render pass (filled circle + outline for pillars, pulsing red halo for hazards).
- Does not wire bullet, AOE, or contact damage into
damageBossPillar— that routing is a Wave 1.5 integration agent task. - Does not call
tickHazardZonesfrom any update loop itself — the integration agent is responsible for calling it with game-dt from the main update loop. - Does not pick which
applyDamagepath to invoke — the integration agent passes the correct callback. - Does not enforce a single active pattern via shared state; idempotency is achieved by
applyTerrainPatterncallingcullTerrainPatternfirst. - Does not define the patterns themselves — those live in
data/terrain-patterns.ts. - Does not auto-reap dead pillars during damage;
reapDeadBossPillarsis a separate cleanup pass. - Does not protect against non-
BossTerrainEntryentries already inworld.destructibles; theisBossTerrainguard keeps them untouched on cull/reap/tick. - Does not use wall-dt for hazard damage; combat math uses game-dt per spec §7.
Signals
- Return value of
damageBossPillar— booleantrueexactly on the killing blow (pillar transitions fromaliveto dead),falseotherwise including when called on an already-dead pillar. - Entity-presence signal — any entry in
world.destructibleswith_isBossTerrain === trueis owned by this module and will be removed bycullTerrainPattern. - Per-overlap damage call —
applyDamage(damagePerSec * dt)per frame per overlapping hazard zone (multiple overlapping zones stack additively).
Entry points
applyTerrainPattern(patternId, arena, world)— culls any existing tagged entries, then for each pillar spec inTERRAIN_PATTERNS[patternId].destructiblesrunsspec.positionFn(arena)and pushes aBossPillarper position; same loop forhazardZonesif present. Empty patterns ('open') are a no-op after the cull.cullTerrainPattern(world)— reverse iteration overworld.destructibles; for each entry whereisBossTerrain(e)is true, swap with the last element and pop. Safe to call when no pattern is active.damageBossPillar(pillar, amount)— early-returnsfalseif pillar is not alive; otherwise subtractsamountfromhp, and onhp <= 0clamps to zero, setsalive = false, returnstrue.tickHazardZones(world, shipX, shipY, shipRadius, dt, applyDamage)— forward iteration; skips non-hazard tagged entries viakindcheck, incrementsphasebydton every hazard, computes squared distance against(radius + shipRadius), and callsapplyDamage(damagePerSec * dt)on overlap.reapDeadBossPillars(world)— reverse iteration; swap-and-pops any tagged pillar withalive === false. Mid-frame death is fine; this is a cleanup pass intended to run after death effects play.
Pattern notes
- Tag-based ownership — every spawned entry carries
_isBossTerrain: true. This is the sole criterion used bycullTerrainPattern,tickHazardZones, andreapDeadBossPillarsto recognize entries they own. Foreign entries already inworld.destructiblesare untouched. - Idempotent apply —
applyTerrainPatternalways culls before spawning, so calling it twice in a row leaves only one pattern’s worth of entries. - Swap-and-pop removal — both cull paths and reap use the index-swap-with-last +
pop()idiom in reverse iteration. O(1) per removal, order is not preserved. - Position generation runs once per encounter —
positionFn(arena)is invoked atapplyTerrainPatterntime only; positions are fixed for the duration of the pattern’s lifetime in the world. - Biome / variant selection lives upstream — this module is biome-agnostic; the variation it exposes is the
TerrainPatternId('open','pillar_ring','pillar_cross','hazard_pads','corridor','gauntlet') and the pattern definitions indata/terrain-patterns.ts. Boss roster (spec §4) maps each boss to its terrain. - Per-pillar HP is set at spawn from the spec —
hpandhpMaxare both initialized tospec.hp, no scaling done here. - Hazard pulse phase is wall-clock —
phaseaccumulates the samedtpassed in; pure render data, not gameplay state. - Decoupled world contract —
TerrainHostWorldexposes onlydestructiblesso tests can pass a stripped-down stand-in instead of a fullWorldState. Internal casts (as BossTerrainEntry[]) accept theunknown[]slot in the real world type. - Cull does not invoke death effects — it’s a hard removal for end-of-encounter cleanup. Use
damageBossPillar+reapDeadBossPillarsfor the gameplay death path.