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()— freshGameState: phasemenu, time/uiTime/wallTime0, level0, xp0, xpToNext84, timeDilation1, juiceDilation1, killStreak0, bestStreak0, tutorialStep0, emptyartifacts,upgradeCounts,modifierTotals,rewardQueue,missionGems,tutorialSeen: new Set<number>().makeGameState().stats—killsByType: {},totalKills,eliteKills,damageDealt,damageTaken,distance,coinsCollected,kills,timeElapsed,score,deathDefianceUsedall0;maxLevel: 1.- Mission state inside
GameState—missionPhase: '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,bossSpawnProfileallnull;bossEncounterTime: 0,_pendingBarDamage: 0,_activeBossDefId: null,_bossSpawnerDisabled: false,_testSpeedMult: 1.0. trackingblock — 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).worldKnobs—enemyHpMult: 0.5,enemyDamageMult: 0.75,enemySpeedMult: 1,enemyCountMult: 1.0,rewardMult: 1,rarityScale: 1.vision—baseRadius: 1200,dimWidth: 150,darkOpacity: 0.99,ambientLight: 0.03.bonuses—dailyBonusActive: false,rareSignalRewardMult: 1.0,deathDefianceTokens: 0.- Weapon & leveling —
weaponSlotsMax: 4,nonWeaponSlotsMax: 4,weaponsAcquired: 0, emptyactiveModifiers/weaponBoxes,allWeaponSlotsFilled: false,allSlotsFilled: false,_wheelState: null. - Reroll/banish —
rerolls: 3,banishes: 3,banishedKeys: new Set<string>(),banishTargeting: false. makeShip()— freshShipState: position/velocity0,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()— freshWorldState:seed: 0,biomeId: 'landing_site',planetId: 0,levelRadius: 2500,cometT: 45, empty arrays forenemies,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; emptychunksandvisitedChunksobjects;typeCounts: new Map();spawnTimers{ proximityTimer: 5, patrolTimer: 10, trickleAccum: 0, waveTimer: 40, waveCount: 0 }.makeCamera()—CameraStatewithx,y,targetX,targetYall0;zoom: 1,targetZoom: 1.makeInput()—InputStatewith all positions/joystick0,isDown/isThrusting/joystickActivefalse.isMobilecomputed fromnavigator.userAgentmatching/Android|iPhone|iPad|iPod|Touch/i(guarded bytypeof navigator !== 'undefined').makeUILayout()—UILayoutwith every numeric field0.- 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)— assignsW,H,uiScale,dpr.updateSafeInsets()— parses CSS custom properties--sat,--sab,--sal,--sarfromdocument.documentElementintosafeInsets.setDebugOverlay(v)/setDiagExpanded(v)— setters for module-level booleans.resetState()— rebuildsgame,ship,world,camera(does not touchplayerInput).
READS FROM
./types—GameState,ShipState,WorldState,CameraState,InputState,UILayoutinterfaces, and re-exportsBossArena.navigator.userAgent— only insidemakeInput()to seedisMobile.document.documentElementandgetComputedStyle— only insideupdateSafeInsets()to read--sat/--sab/--sal/--sar.
PUSHES TO
- Module-exported
letbindings —game,ship,world,camera,playerInput,W,H,uiScale,dpr,UI,debugOverlay,diagExpanded,safeInsetsare imported by sim/render/input subsystems throughout the engine. WorldState.playerBullets/enemies/enemyBullets/forecasts— pre-allocated as empty arrays so spawn/simulation passes canpushinto them without nullish checks.GameState.tracking.*andGameState.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 resetplayerInput,W/H/uiScale/dpr,UI,safeInsets,debugOverlay, ordiagExpanded.
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 bybridge.tsand 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 theGameScreendebug checkbox.setDiagExpanded(v)— toggles the diag perf panel.- Re-export
BossArenafrom./types.
Pattern notes
- Module-singleton pattern:
game/ship/world/camera/playerInputarelet-exported and rebuilt byresetState()rather than mutated in place, so each run starts from a fresh object identity. WorldStatepre-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 realSetinstances;typeCountsis a realMap. runDefstartsnull— populated bybridge.tsat 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. debugOverlayanddiagExpandedstartfalse— diag panel starts collapsed so it does not obscure gameplay.CHUNK_SIZE = 1024is the spatial grid cell size used by the chunk-keyedchunks/visitedChunksmaps inWorldState.isMobileis computed once atmakeInput()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:rootstylesheet.