PURPOSE

Factory functions and module-level singletons for all engine runtime state. Ports 02-state.js to typed factories backed by interfaces in ./types. Owns the live game, ship, world, camera, playerInput instances, plus canvas dimension globals and debug-toggle flags. resetState() rebuilds the four core singletons at the start of each run.

OWNS

  • makeGameState() — fresh GameState: phase menu, time/uiTime/wallTime 0, level 0, xp 0, xpToNext 84, timeDilation 1, juiceDilation 1, killStreak 0, bestStreak 0, tutorialStep 0, empty artifacts, upgradeCounts, modifierTotals, rewardQueue, missionGems, tutorialSeen: new Set<number>().
  • makeGameState().statskillsByType: {}, totalKills, eliteKills, damageDealt, damageTaken, distance, coinsCollected, kills, timeElapsed, score, deathDefianceUsed all 0; maxLevel: 1.
  • Mission state inside GameStatemissionPhase: 'objective', missionType: 'explore', _currentMissionId: '', missionTimer: 300, missionTimerMax: 300, heat: 1, plus empty mission-specific collections (beacons, scavengeCaches, gauntletGates, patrols, hazards, baitItems), and goal counts (beaconsTotal: 4, beaconsDone: 0, scavengeRequired: 0, gauntletRequired: 0, gatesOptional: 0, exterminateRequired: 0).
  • Boss state — activeEvent, bossRoom, bossArena, bossSpawnProfile all null; bossEncounterTime: 0, _pendingBarDamage: 0, _activeBossDefId: null, _bossSpawnerDisabled: false, _testSpeedMult: 1.0.
  • tracking block — per-run telemetry counters (damageTaken, totalHeal, totalShieldRegen, lowestHpPercent: 1.0, deathDefianceUsed, reviveTokenUsed, eventsCompleted, eventsAttempted, weaponsFound, upgradesChosen, artifactsCollected, abilityUses, maxDistance, detections, trapKills, trapKillPct, gatesHit, gatesRequired, payloadHpRemainingPct, objectHpRemainingPct, poisVisited, subZoneEntered, enemyBulletsFired, enemyBulletsHit).
  • worldKnobsenemyHpMult: 0.5, enemyDamageMult: 0.75, enemySpeedMult: 1, enemyCountMult: 1.0, rewardMult: 1, rarityScale: 1.
  • visionbaseRadius: 1200, dimWidth: 150, darkOpacity: 0.99, ambientLight: 0.03.
  • bonusesdailyBonusActive: false, rareSignalRewardMult: 1.0, deathDefianceTokens: 0.
  • Weapon & leveling — weaponSlotsMax: 4, nonWeaponSlotsMax: 4, weaponsAcquired: 0, empty activeModifiers/weaponBoxes, allWeaponSlotsFilled: false, allSlotsFilled: false, _wheelState: null.
  • Reroll/banish — rerolls: 3, banishes: 3, banishedKeys: new Set<string>(), banishTargeting: false.
  • makeShip() — fresh ShipState: position/velocity 0, angle: 0, turnSpeed: 0.628, alive: true, radius: 20, outerRadius: 30, hp: 80, hpMax: 80, hpRegen: 0, shield: 15, shieldMax: 15, shieldRegenRate: 6, shieldRegenDelay: 4, shieldRegenFillTime: 2, shieldColor: '80,255,120', thrust: 640, maxSpeed: 280, drag: 1.4, heat: 0, heatBurnRate: 25, heatCoolRate: 40, heatSpeedTarget: 80, heatBoostMult: 2.0, heatCurve: 'linear', burnoutSeverity: 1.0, coolingAccel: 1, magnetRange: 100, luck: 0, luckMult: 0, slots: 2, xpGainMult: 1.15, meleeMult: 1.0, shipClass: 'medium', shipScale: 1.0, ramSpeedBleed: 0.80, contactSpeedBleed: 0.95, terrainRestitution: 1.09, terrainFriction: 0.01, ramThreshold: 150, ramDamageLo: 40, ramDamageHi: 1000, pushRatio: 0.1, enemySolidity: 0.8, contactDecel: 0.6, contactCooldown: 0.25, rotates: true, accelCurve: 'linear', dragCurve: 'exponential', heatShakeThreshold: 0.85, warpGroupId: null, warpT: 0, _entityType: 'ship', _base: {}.
  • makeWorld() — fresh WorldState: seed: 0, biomeId: 'landing_site', planetId: 0, levelRadius: 2500, cometT: 45, empty arrays for enemies, enemyBullets, timedStrikes, playerBullets, structures, belt, junk, particles, xp, sigils, warnings, pickups, discoBalls, connNodes, jellyfish, blossoms, defer, events, locations, hubs, spokes, terrain, floaters, comets, patrols, dmgNumbers, weaponBoxes, artifactBoxes, destructibles, goldenStars, gems, starlightBeams, eventStars, regenStations, forecasts; empty chunks and visitedChunks objects; typeCounts: new Map(); spawnTimers { proximityTimer: 5, patrolTimer: 10, trickleAccum: 0, waveTimer: 40, waveCount: 0 }.
  • makeCamera()CameraState with x, y, targetX, targetY all 0; zoom: 1, targetZoom: 1.
  • makeInput()InputState with all positions/joystick 0, isDown/isThrusting/joystickActive false. isMobile computed from navigator.userAgent matching /Android|iPhone|iPad|iPod|Touch/i (guarded by typeof navigator !== 'undefined').
  • makeUILayout()UILayout with every numeric field 0.
  • Module-level singletons (let): game, ship, world, camera, playerInput, W, H, uiScale, dpr, UI, debugOverlay, diagExpanded, safeInsets.
  • Constants: CHUNK_SIZE = 1024.
  • setDimensions(w, h, scale, devicePixelRatio) — assigns W, H, uiScale, dpr.
  • updateSafeInsets() — parses CSS custom properties --sat, --sab, --sal, --sar from document.documentElement into safeInsets.
  • setDebugOverlay(v) / setDiagExpanded(v) — setters for module-level booleans.
  • resetState() — rebuilds game, ship, world, camera (does not touch playerInput).

READS FROM

  • ./typesGameState, ShipState, WorldState, CameraState, InputState, UILayout interfaces, and re-exports BossArena.
  • navigator.userAgent — only inside makeInput() to seed isMobile.
  • document.documentElement and getComputedStyle — only inside updateSafeInsets() to read --sat/--sab/--sal/--sar.

PUSHES TO

  • Module-exported let bindings — game, ship, world, camera, playerInput, W, H, uiScale, dpr, UI, debugOverlay, diagExpanded, safeInsets are imported by sim/render/input subsystems throughout the engine.
  • WorldState.playerBullets / enemies / enemyBullets / forecasts — pre-allocated as empty arrays so spawn/simulation passes can push into them without nullish checks.
  • GameState.tracking.* and GameState.stats.* — pre-shaped so per-frame telemetry can increment without re-initialization.

DOES NOT

  • Does not import any sim, render, audio, world, or weapon module — pure factories + module singletons only.
  • Does not subscribe to any signal, event bus, or React/Zustand store.
  • Does not run any per-frame logic, simulation step, or render pass.
  • Does not persist or load state from storage (Supabase, localStorage, IndexedDB).
  • Does not perform validation on factory output.
  • resetState() does not reset playerInput, W/H/uiScale/dpr, UI, safeInsets, debugOverlay, or diagExpanded.

Signals

None. This module exposes plain mutable bindings; consumers re-import the singleton each access.

Entry points

  • makeGameState(), makeShip(), makeWorld(), makeCamera(), makeInput(), makeUILayout() — factories.
  • resetState() — called at the start of each run by bridge.ts and the run lifecycle.
  • setDimensions(w, h, scale, dpr) — called by the canvas mount/resize path.
  • updateSafeInsets() — called on mount and on resize.
  • setDebugOverlay(v) — toggled by the GameScreen debug checkbox.
  • setDiagExpanded(v) — toggles the diag perf panel.
  • Re-export BossArena from ./types.

Pattern notes

  • Module-singleton pattern: game/ship/world/camera/playerInput are let-exported and rebuilt by resetState() rather than mutated in place, so each run starts from a fresh object identity.
  • WorldState pre-allocates every entity bucket as an empty array (playerBullets, enemies, enemyBullets, forecasts, etc.) so push/splice paths never null-check.
  • Collections that need set semantics (tutorialSeen, banishedKeys) are real Set instances; typeCounts is a real Map.
  • runDef starts null — populated by bridge.ts at mission start, per the inline comment.
  • Underscore-prefixed fields (_simFrame, _stepsThisFrame, _pendingBarDamage, _hullFlash, _invertScreenTimer, _hitFreezeTimer, _wheelState, _base, etc.) signal internal/private engine bookkeeping not part of public game state.
  • debugOverlay and diagExpanded start false — diag panel starts collapsed so it does not obscure gameplay.
  • CHUNK_SIZE = 1024 is the spatial grid cell size used by the chunk-keyed chunks / visitedChunks maps in WorldState.
  • isMobile is computed once at makeInput() call time, not re-evaluated per frame.
  • Safe-area insets are sourced from CSS custom properties rather than the env(safe-area-inset-*) API directly so the page can polyfill via a :root stylesheet.