PURPOSE

Draws the gameplay underlay — a thin world-space overlay rendered at 100% parallax that sits in the gap between the parallax backdrop and the gameplay layer. Acts as the “digital playing board” under events, terrain, and drop channels. Dispatches one of three variants based on the active biome: a flat-top tessellating hex grid (default), a horizontal water-surface wave pattern (biome delphi), or an orthogonal square topographic grid with random chunks cut out (biome old_earth).

OWNS

  • The three underlay variant renderers: drawHexGrid, drawWaveGrid, drawLatLongGrid.
  • The pre-baked tileable canvases for the hex and lat/long variants (_hexBake, _latLongBake) and their stroke-color memoization keys (_hexBakeColor, _latLongBakeColor).
  • The shared base-alpha breathing modulation (BASE_ALPHA, WAVE_AMPLITUDE, WAVE_SPEED, frameAlpha) used by every variant so all biomes’ boards breathe on the same cadence.
  • The world-space context setup (enterWorldSpace) that translates and scales the canvas once per draw rather than per primitive.
  • All variant tuning constants: hex radius and tile period, wave row spacing / wavelengths / amplitudes / phase speed, lat/long cell size / drop rate / repeat period, and supersample factors for the bakes.
  • The deprecated drawGameplayGrid alias kept alive during the rename to drawGameplayUnderlay.

READS FROM

  • resolvePaletteSlot('bg_star') from ./palette/palette-system — provides the stroke color for all three variants, so the underlay tint tracks the active biome’s mood.
  • getActiveBiomeId() from ./parallax/parallax-system — drives the variant switch in the dispatcher.
  • hash2i from ./parallax/layer-types — deterministic 2D hash used to decide which lat/long cells are dropped from the topographic grid (restricted to a 16×16 cell repeat for clean tiling).
  • Caller-supplied camera state and frame time: camX, camY, camZoom, viewW, viewH, tSeconds.

PUSHES TO

  • The supplied CanvasRenderingContext2D — sets transform, globalAlpha, strokeStyle, lineWidth, and issues drawImage (hex / lat-long) or stroke (wave) calls.
  • The two module-level baked canvases — created on first use and invalidated when the palette stroke color changes.

DOES NOT

  • Does not draw events, terrain, drop channels, or any gameplay entity — only the underlay pattern beneath them.
  • Does not manage its own render ordering — relies on the caller invoking it between parallax_fg and the events pass; anything drawn after this call naturally occludes the underlay.
  • Does not handle screen-space drawing, HUD, or post-processing.
  • Does not own palette resolution, biome selection, or camera math beyond converting view extents into world-space tile ranges.
  • Does not animate the hex or lat/long variants beyond the shared alpha breathing — only the wave variant has live time-driven motion in its geometry.
  • Does not early-out on zero-area views; it only guards against camZoom <= 0.

Signals

  • Reads the active-biome signal each frame via getActiveBiomeId(); switching biome immediately changes which variant runs on the next draw.
  • Bake invalidation is triggered implicitly: when resolvePaletteSlot('bg_star') returns a different stroke than the cached _hexBakeColor / _latLongBakeColor, the corresponding tile is re-baked.
  • No event bus subscriptions; the module is pure render-on-call.

Entry points

  • drawGameplayUnderlay(ctx, camX, camY, camZoom, viewW, viewH, tSeconds) — exported dispatcher. Called by bridge.ts immediately after the parallax foreground pass and before the events pass. Returns early when camZoom <= 0.
  • drawGameplayGrid — deprecated alias re-exporting drawGameplayUnderlay so legacy callers continue to work during the rename.

Pattern notes

  • Hex and lat/long variants pre-bake one tileable canvas at a supersample, then draw it as a tiled drawImage per frame — avoids per-frame beginPath / stroke work. The hex bake covers one tile period plus a one-cell margin so strokes that cross the tile edge wrap cleanly when the canvas is drawn next to itself.
  • Wave variant stays live because the surface ripples are time-driven; it composes two sinusoids of different wavelengths and phase speeds per row, and adapts its sample step to camZoom so finer steps are used when zoomed in.
  • All variants enter world space via the same enterWorldSpace helper (translate to view center, scale by camera zoom, translate by camera position) — scaling once is cheaper than computing screen coordinates per primitive, and the underlay’s parallax of 1.0 means this transform matches the gameplay layer exactly.
  • Tile-range math computes startX / startY by flooring the world-space left/top to the tile period, then iterates until the right/bottom edge — guarantees the visible region is fully covered without overdraw beyond one tile period.
  • Wave variant pads its X iteration by ±WAVE_LAMBDA_X and its row range by ±1 so the sinusoidal lines don’t visibly clip at the view edges.
  • Lat/long variant deliberately restricts the hash domain to a 16×16 cell repeat so the bake is tileable; the original implementation used an unbounded hash2i(col, row) with no period. At a 28% drop rate while the camera is moving, the repeat is not perceptible.
  • Wave variant scales its alpha by 1.6× relative to the shared frameAlpha so the live sinusoid reads as strongly as the static baked grids despite being a single stroke path.
  • Wave variant sets lineWidth = 1 / camZoom so the world-space stroke renders at roughly one screen pixel regardless of zoom; the baked variants rely on supersampling for crispness instead.
  • Module-level mutable state (_hexBake, _hexBakeColor, _latLongBake, _latLongBakeColor) is the only retained state — there is no init/teardown lifecycle and no per-frame allocation in the hot path.