engine/core

PURPOSE — Foundation layer for the entire engine: owns the canonical mutable game/ship/world/camera/input singletons, the fixed-timestep accumulator loop that drives every other system, the deterministic sim clock, the central signal bus, the entity registry, the stat-modifier stack, the broad-phase spatial grid, the per-frame derived-data cache, the config tables (gameplay knobs, perf flags, version, economy, slots, accessibility, artifact-drop curves, sprite targets, rarity colors), device-capability probes, asset preload, remote-error reporting, render-diagnostic instrumentation, and the shared math/RNG/pool-management utilities every downstream system imports.

OWNS

  • Module-level singletons for game / ship / world / camera / playerInput / UI layout / canvas dimensions / DPR / safe-area insets / debug-overlay flag / diag-panel-expanded flag.
  • The full GameState shape: phase, sim/wall/ui timers, run-frame counter, sim-steps-this-frame counter, XP / level / streak / kill-streak / time-dilation / juice-dilation, mission state for every mission type (beacons, extraction, chase, scavenge, gauntlet, holdout, infiltrate, convoy, exterminate), boss room + arena + spawn profile + active-boss def-id, horde flag, tracking aggregates, side-quest, world knobs, vision, bonuses, revive prompt, hull-flash / invert-screen / hit-freeze / magnet-icon timers, level kind / hard-mode / sealed-arena / boss-level-cleared flags, enemy difficulty level, level speed multiplier, overtime state, portal state, wheel-of-fortune overlay state, weapon/non-weapon slot maxes, weapon boxes, active modifiers, reroll / banish per-run counters and banished-key set, run-definition reference.
  • The full ShipState shape: position / velocity / angle / turn speed, hull polygon, HP / shield / regen pipeline (delay, fill timer, broken timer, background regen progress, hit-flash, hit-angle, hits array), thrust / max-speed / drag, vehicle-feel fields (accel curve, drag curve, stop / heat shake, stop spring, prev-speed), heat system fields, invuln state and stuck-invuln watchdog wall-time, exclusive-state slot (currently Star Power) plus its max-timer snapshot, spawn-intro and damage-flash timers, electric-hit and weapon-fire-glow timers, magnet range / luck / luck-mult, weapons array, life steal, XP gain mult, melee mult, ship-class movement bleed, physics/collision tunables (ramSpeedBleed, contactSpeedBleed, terrainRestitution, terrainFriction, ram thresholds, push ratio, enemy solidity, contact decel/cooldown), super-handling / currency-bonus / objective-speed flags, death-defiance state, warp-buff scalar, warp-puddle group id + tween, fixed-angle override, entity id and base-stats snapshot.
  • The full WorldState shape: seed, all entity arrays (enemies, enemy bullets, timed strikes, player bullets, structures, belt, junk, particles, XP orbs, sigils, warnings, pickups, disco balls, conn nodes, jellyfish, blossoms, comets, patrols, damage numbers, weapon/artifact boxes, destructibles, golden stars, gems, starlight beams, event stars, regen stations, forecast hitboxes, floaters), chunk map, type counts, spawn timers (proximity / patrol / trickle / wave), deferred-action queue, events / locations / hubs / spokes / terrain, biome id, planet id, level radius, visited-chunks map.
  • CameraState shape with optional toS / toW projection methods, plus InputState and UILayout shapes.
  • State-factory functions (makeGameState, makeShip, makeWorld, makeCamera, makeInput, makeUILayout) and the canonical resetState() that re-creates game / ship / world / camera in place.
  • Fixed-timestep loop state: last-frame timestamp, accumulator, monotonic sim-frame counter; constants for sim step length, accumulator ceiling, and the base spiral-of-death step cap (scaled at call site by simRate).
  • The deterministic Clock (sim-time ms, sub-step counter for unique IDs) — sole writer is the loop’s per-step advance call.
  • The signal bus Sig — name-keyed priority-sorted listener map, fixed-shape mutable SignalContext payload reused per fire, recursion-depth guard.
  • The entity registry Entity — monotonic id allocator, id→object map, _entityType tag stamping, count getter.
  • The stat-modifier stack Modifiers — modifier list, target index, dirty set, id allocator; supports flat / percent / set modes, stack types (independent / refresh with max-stack cap), per-creator / per-target / per-source removal, automatic expiry, multiplier floor at zero.
  • The broad-phase SpatialGrid class plus the singleton enemyGrid keyed off the gameplay grid-cell config value; cell-key hashing, per-cell array pool, single shared query-results array for zero-alloc broad-phase queries.
  • The bullet-hit-set pool (capped acquire / release recycler for Set instances).
  • The smoothed-FPS scalar (EMA, reset hook).
  • The per-frame derived-data cache: visible-enemy and alive-enemy arrays plus counters, rebuilt once per RAF via rebuildFrameCache, with frustum + zoom + screen-projection callback supplied by the caller.
  • The full config surface: build-version string, gameplay constants object (CFG), debug flags, hull-light-shape table, rarity colors, economy table, sprite targets, weapon-slot taxonomy, weapon-slot maxes, weapon-box sit time, XP curve params, level-up choice count, scaling diminish, accessibility flags + skip-reward-animations persistence, perf flags (URL-param overrides, mobile auto-detect, low-perf inference, DPR pixel-budget override), perf-version-tag string, artifact-drop KPM curve constants, mission base timers, vision defaults.
  • Device-capability snapshot: CPU cores, device-memory GB, native DPR, screen dims, GPU vendor / renderer (probed via throwaway WebGL context), reduced-motion flag, mobile heuristic, battery level / charging via Battery API, power-saver inference, max-touch-points; async init plus mobile-helper getter and telemetry-shape builder.
  • Memory-pressure state: GC-pause inference counters (count / total ms / worst ms) derived from wall-time-vs-JS-time gap, optional Chrome heap snapshot, per-run reset.
  • Remote-error reporting: injected analytics callback, device-context snapshot, dedup map for identical error signatures within a window, global error / unhandledrejection listeners, render-error + perf-warning surfaces.
  • Render-diagnostic instrumentation: named-pass ring buffers (per-pass timing, per-pass worst-case, per-pass draw counts), GPU-side EMAs (delivered FPS, GPU overhead, RAF gap, drawImage count), stutter-score rolling window, pool-stat reporters (orbs / enemy bin / bullet bin), worldObjects + stickerSetup sub-pass micro-timer maps, canvas-health registry (per-buffer dims, MB, ctx-valid, last-update frame), spike-snapshot capture with per-run cap, sustained low-FPS detector, entity-count snapshot, error-log ring, and the diag-overlay drawing routine.
  • Asset preload pipeline: ship v4 bundles, enemy sprites, fonts, weapon + UI icons, planet sprites, terrain sprites; progress callback that fractions step count.
  • Shared math + RNG + pool-management utilities (lerp / clamp / easeIn* / easeOut* / smoothstep* / lerpClamped / swapRemove / mulberry32 / seededRandom / xpForLevel / enemyLevelMult / getEnemyLevel / distanceDifficultyMult / spawnRateMult / spawnDecay / pointToSegDist / normalizeAngle / angle / angleDiff / dist / dist2 / circleOverlap / pointInCircle / chunkKey / deg2rad / rad2deg / rand* / formatNumber / weightedPick / the PoolManager weighted-list resolver).

READS FROM

  • The browser platform: performance.now(), window, navigator, document, WebGLRenderingContext, URLSearchParams, localStorage, getComputedStyle, screen, matchMedia, the Battery API.
  • Run-config and level-progression data types (for the runDef reference and _levelKind discriminant stored on GameState).
  • The Input / Physics / Camera / Rapier-world / Rapier-ship / warp-puddle-effects sibling subsystems (called per sim step in the fixed loop).
  • The per-system preload entry points exposed by ships / enemies / weapon-icons / UI-icons rendering subsystems.

PUSHES TO

  • Every downstream engine subsystem: each one imports the singletons (game / ship / world / camera / playerInput / W / H / dpr / UI / safeInsets), the Clock, the Sig bus, the Entity registry, the Modifiers stack, the enemyGrid spatial index, the acquireSet / releaseSet bullet-hit-set pool, the per-frame cache accessors, the CFG table, the perf flags, the rarity / economy / weapon-slot / XP / accessibility constants, and the shared math/RNG/pool utilities.
  • The injected analytics trackEvent (used by remote-errors for render-error / global-error / unhandled-rejection / perf-warning events).
  • The render-diagnostic overlay (drawn into the supplied 2D context after all other HUD passes when debugOverlay is on).

DOES NOT

  • Render anything (apart from the optional diag overlay drawn into a context the caller already opened). No sprite, terrain, particle, or HUD drawing lives here.
  • Run physics or collision math. The loop dispatches to Physics.update and the Rapier world / ship sync; the spatial grid is a broad-phase index only — narrow-phase distance checks happen in the caller. There is no contact resolution, knockback, or damage in this module.
  • Spawn entities. The loop ticks spawn timers as part of GameState, but the spawner, GameMaster, and director live downstream.
  • Decide what a signal means. The bus dispatches synchronously by name with a fixed-width payload; signal semantics, name registries, and listener wiring belong to the systems that own each signal.
  • Define modifier semantics for any specific stat. The stack stores stat strings and applies base + Σ flat × stacks then × max(0, 1 + Σ pct × stacks) (or set override) to the entity field of that name; it does not know what hpMax or thrust mean.
  • Manage weapon slots, mission objectives, level progression, vision falloff, or any other gameplay rule beyond storing the state and the static config knobs each subsystem reads.
  • Persist player progress. The only persistence here is the accessibility skip-reward-animations localStorage flag and the perf-flag lowPerf localStorage flag.
  • Touch network or Supabase. Remote-error reports go through an analytics callback that is injected at boot — this module never imports a transport.
  • Handle audio, UI input bindings, or scene routing.
  • Tick anything in wall-clock except: the loop’s hit-freeze / invert-screen overlay timers (which must survive sim freeze), wallTime / uiTime accumulation, the stuck-invuln wall-time watchdog, the remote-error dedup window, and render-diag’s wall-clock pass measurements. Everything else advances in sim time via Clock.

Signals fired / Signals watched — This module owns the signal bus (Sig) itself but does not fire or subscribe to any signal. It exposes Sig.on / .off / .fire / .clear / .has for downstream systems and a fixed-shape SignalContext payload reused across every dispatch. Listener priority is descending (higher fires first); dispatch is synchronous; recursion depth is capped to prevent infinite signal loops. Sig.clear() is called from startNewGame alongside the entity / modifier / FPS / clock resets.

Entry points

  • gameLoopTick — RAF tick; accumulator-driven fixed-timestep dispatcher. Handles initial-frame timestamp seeding, tab-resume / long-stall accumulator drop, raw-dt cap, menu short-circuit, hit-freeze sim-skip path (with wall-clock invert-overlay tick), juice-dilation clamp, sim-rate composition from timeDilation × levelSpeedMult × juiceClamped, step-cap scaling, accumulator drain, frame-end write of _dt / _rawDt / _stepsThisFrame / _simFrame, and unconditional wallTime / uiTime advance.
  • startNewGame — Sets phase to playing after running the canonical reset sequence: resetStateEntity.clearModifiers.clearSig.clear → smoothed-FPS reset → Clock.reset → frame-timing reset.
  • resetFrameTiming — Synthetic-frame harness reset for tests pumping fake timestamps.
  • resetState — Re-creates game / ship / world / camera from the factory functions.
  • setDimensions — Pushes canvas width / height / UI scale / DPR into the module-level singletons.
  • updateSafeInsets — Reads CSS env-var insets via getComputedStyle into the safeInsets singleton.
  • setDebugOverlay, setDiagExpanded — Toggles for the diag overlay and its expanded view.
  • Clock.now / .uniqueNow / .advanceSimStep / .reset — Deterministic sim-time API (sole writer for the advance call is the loop).
  • Sig.on / .off / .fire / .clear / .has — Signal bus.
  • Entity.create / .get / .remove / .has / .clear / .count — Entity registry.
  • Modifiers.add / .remove / .removeByCreator / .removeByTarget / .removeBySource / .tick / .recalc / .dump / .markDirty / .clear — Stat-modifier stack.
  • SpatialGrid class + enemyGrid singleton with clear / insert / insertPoint / query / countNear.
  • acquireSet / releaseSet — Bullet-hit-tracking Set pool.
  • tickFps / getSmoothedFps / resetSmoothedFps — Smoothed FPS for HUD / diag.
  • rebuildFrameCache / getVisibleEnemies / getVisibleEnemyCount / getAliveEnemies / getAliveEnemyCount / clearFrameCache — Per-frame derived-data cache.
  • isMobile / initDeviceCapabilities / DEVICE_CAPS / getDeviceInfoForTelemetry — Device-capability probes.
  • memoryTick / getMemorySnapshot / resetMemoryPressure — Memory-pressure inference.
  • preloadAllAssets — Boot-time asset preload pipeline.
  • initRemoteErrors / reportRenderError / reportPerfWarning — Remote-error surface.
  • diagBeginFrame / diagBeginPass / diagEndPass / diagEndFrame / diagSetCounts / diagSetPassDrawCount / diagAddDrawCalls / diagSetWoSubTimings / diagSetSsSubTimings / diagSetPoolStats / diagTrackCanvas / diagGetPerfSnapshot / diagDrainSpikes / diagResetSpikes / diagEvent / drawDiagOverlay — Render-diagnostic instrumentation.
  • loadSkipRewardAnimations / setSkipRewardAnimations / isSkipRewardAnimations — Accessibility persistence.
  • PoolManager.init / .pick / .pickN / .getAll / .has — Weighted-list resolver for loot / enemy / terrain pools.
  • The math / RNG / geometry helpers: lerp, clamp, easeIn, easeOut, easeInOut, easeOutCubic, smoothstep, smoothstepRange, lerpClamped, swapRemove, mulberry32, seededRandom, xpForLevel, enemyLevelMult, getEnemyLevel, distanceDifficultyMult, spawnRateMult, spawnDecay, pointToSegDist, normalizeAngle, angle, angleDiff, dist, dist2, circleOverlap, pointInCircle, chunkKey, deg2rad, rad2deg, rand, randTo, randRange, randInt, formatNumber, weightedPick.

Pattern notes

  • Mutable module-level singletons. game / ship / world / camera / playerInput are exported let bindings reassigned by resetState. Every downstream system imports them by name and reads/writes them directly — there is no store, no DI container, no event sourcing. This is the deliberate substrate the rest of the engine is built on.
  • Fixed-timestep + accumulator. RAF supplies wall-clock dt; the loop multiplies by simRate (time dilation × level speed × clamped juice dilation) and drains the accumulator in exact SIM_DT steps. The step cap scales with simRate so high-speed test runs don’t hit spiral-of-death on jitter. Hit-freeze skips the sim entirely while still ticking wall-clock-coupled overlays and the stuck-invuln watchdog.
  • Determinism via Clock. The sim clock is the only authoritative time source for gameplay state. It advances exactly once per sim step. Sub-counter exists for same-step monotonic IDs. Wall-clock (wallTime / uiTime / Date.now() / performance.now()) is reserved for scheduling, telemetry timestamps, diag instrumentation, dedup windows, and overlays that must persist across hit-freeze.
  • Two-tier state shape on entities. Each entity stores _base (snapshot of design-time stats) plus the live mutated fields; Modifiers.recalc rebuilds the live fields from _base plus the modifier stack whenever the dirty flag is set. The loop calls recalc on the ship every step when the base snapshot is populated. Warp-puddle overrides are layered on top after recalc so the tween blends cleanly.
  • Signal bus is a fixed-width mutable payload. Every fire reuses the same SignalContext object — listeners must read it synchronously and never retain the reference. Recursion-depth guard makes accidental feedback loops bounce silently rather than blow the stack.
  • Broad-phase grid is rebuilt every frame. The contract is clear-then-insert at the top of the frame, then read via query (returning a shared results array — caller consumes before the next call). Cell-key hashing packs (cx, cy) into one integer; cell-array reuse via a pool avoids GC pressure.
  • Per-frame cache for derived data. rebuildFrameCache walks world.enemies once, populates the alive list and the visible list (with a generous padding for sprite overhang), and trims tail entries so consumers iterating by .length don’t see stale references. This is the canonical place to add other “scanned-once-per-frame” derived facts.
  • Pooled Sets for bullet hit tracking. acquireSet / releaseSet keep a capped reuse bin; bullets get a cleared Set on spawn and return it on despawn so the per-frame allocation cost stays flat under heavy bullet load.
  • Config is a flat re-export barrel. The ./config/ subdir splits gameplay knobs / debug / artifact-drops / economy / sprites / weapons-leveling / accessibility / perf-flags / version into per-concern files; ./config/index.ts re-exports the public symbols; ./config.ts re-exports those for the original import path. Perf-flag inference (mobile, low-perf, DPR pixel-budget) runs at module load and is frozen for the run.
  • Device capabilities are probed once and exposed as a mutable snapshot. The sync probes (GPU via throwaway WebGL context, CPU cores, screen, reduced-motion, mobile) populate DEVICE_CAPS at module load; the async initDeviceCapabilities adds the Battery API and listens for levelchange / chargingchange to keep powerSaverLikely fresh.
  • Remote errors are dependency-injected. initRemoteErrors receives the analytics trackEvent callback at boot — this module never imports a transport directly, which keeps the engine layer free of metagame deps.
  • Render diagnostics is always-on-cheap. Per-pass performance.now() calls record into preallocated Float32Array ring buffers regardless of the overlay flag; only the overlay drawing and the diag-event log are gated behind debugOverlay. Telemetry can snapshot perf without the user enabling anything.
  • Spike snapshots are per-run capped. The first N frames that exceed the spike threshold get full per-pass + per-draw-count + entity-count capture; beyond the cap, continuously bad performance is silent so payload size stays bounded.
  • resetState is shallow. It re-runs the factory functions for game / ship / world / camera but startNewGame is the canonical run-start sequence — it also clears the entity registry, the modifier stack, the signal bus, the smoothed-FPS scalar, and the deterministic clock, then resets frame timing. Callers that want a clean run start startNewGame, not resetState.
  • PoolManager is a separate weighted-list resolver (loot / enemy / terrain pools), distinct from the bullet-hit-set object pool. Both live here because they’re cross-cutting; their use sites are scattered downstream.