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
GameStateshape: 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
ShipStateshape: 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
WorldStateshape: 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. CameraStateshape with optionaltoS/toWprojection methods, plusInputStateandUILayoutshapes.- State-factory functions (
makeGameState,makeShip,makeWorld,makeCamera,makeInput,makeUILayout) and the canonicalresetState()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 mutableSignalContextpayload reused per fire, recursion-depth guard. - The entity registry
Entity— monotonic id allocator, id→object map,_entityTypetag stamping, count getter. - The stat-modifier stack
Modifiers— modifier list, target index, dirty set, id allocator; supports flat / percent / set modes, stack types (independent/refreshwith max-stack cap), per-creator / per-target / per-source removal, automatic expiry, multiplier floor at zero. - The broad-phase
SpatialGridclass plus the singletonenemyGridkeyed 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
Setinstances). - 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/unhandledrejectionlisteners, 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/ thePoolManagerweighted-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
runDefreference and_levelKinddiscriminant stored onGameState). - 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), theClock, theSigbus, theEntityregistry, theModifiersstack, theenemyGridspatial index, theacquireSet/releaseSetbullet-hit-set pool, the per-frame cache accessors, theCFGtable, 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
debugOverlayis 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.updateand 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
statstrings and appliesbase + Σ flat × stacksthen× max(0, 1 + Σ pct × stacks)(orsetoverride) to the entity field of that name; it does not know whathpMaxorthrustmean. - 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-animationslocalStorage flag and the perf-flaglowPerflocalStorage 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/uiTimeaccumulation, 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 viaClock.
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 fromtimeDilation × levelSpeedMult × juiceClamped, step-cap scaling, accumulator drain, frame-end write of_dt/_rawDt/_stepsThisFrame/_simFrame, and unconditionalwallTime/uiTimeadvance.startNewGame— Sets phase to playing after running the canonical reset sequence:resetState→Entity.clear→Modifiers.clear→Sig.clear→ smoothed-FPS reset →Clock.reset→ frame-timing reset.resetFrameTiming— Synthetic-frame harness reset for tests pumping fake timestamps.resetState— Re-createsgame/ship/world/camerafrom the factory functions.setDimensions— Pushes canvas width / height / UI scale / DPR into the module-level singletons.updateSafeInsets— Reads CSS env-var insets viagetComputedStyleinto thesafeInsetssingleton.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.SpatialGridclass +enemyGridsingleton withclear/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/playerInputare exportedletbindings reassigned byresetState. 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 exactSIM_DTsteps. The step cap scales withsimRateso 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.recalcrebuilds the live fields from_baseplus the modifier stack whenever the dirty flag is set. The loop callsrecalcon the ship every step when the base snapshot is populated. Warp-puddle overrides are layered on top afterrecalcso the tween blends cleanly. - Signal bus is a fixed-width mutable payload. Every fire reuses the same
SignalContextobject — 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.
rebuildFrameCachewalksworld.enemiesonce, populates the alive list and the visible list (with a generous padding for sprite overhang), and trims tail entries so consumers iterating by.lengthdon’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/releaseSetkeep 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.tsre-exports the public symbols;./config.tsre-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_CAPSat module load; the asyncinitDeviceCapabilitiesadds the Battery API and listens forlevelchange/chargingchangeto keeppowerSaverLikelyfresh. - Remote errors are dependency-injected.
initRemoteErrorsreceives the analyticstrackEventcallback 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 preallocatedFloat32Arrayring buffers regardless of the overlay flag; only the overlay drawing and the diag-event log are gated behinddebugOverlay. 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.
resetStateis shallow. It re-runs the factory functions for game / ship / world / camera butstartNewGameis 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 startstartNewGame, notresetState.PoolManageris 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.