Phase State Machine

game.phase is the top-level state machine that gates every per-frame subsystem in the bridge update loop. It is a single string field on the global game state that the bridge, the loop, the renderer, and the React shell all read each frame to decide what to run, freeze, draw, or surface.

The enum

Defined in engine/core/types.ts as GamePhase:

PhaseMeaning
menuInitial state. Hub/mission-board screens are mounted; no mission is running. Set in makeGameState().
birthMission has been booted but spawn-intro is still playing (ship fade-in over _spawnIntroTimer = 0.8s). Physics ticks; mission timer does not.
playingActive gameplay. All gameplay systems tick (enemies, spawners, mission timer, triggers, bullets, AI).
levelupReward overlay active — modifier cards, weapon-chest pick, artifact pick, shooting-star choice, or auto-upgrade flythrough. World is frozen via timeDilation = 0 and the music low-pass clamps to 1000 Hz.
deadPlayer ship HP reached 0. Runs the “YOU DIED” cinematic, then either the revive prompt (death defiance available) or the fade-to-results countdown.
level_advanceTimer expired or boss cleared on a non-final level. Plays the level fade, advances _currentLevel, scales worldKnobs, rebuilds the world, then snaps back to playing.
completeFinal-level survive — short victory delay before results. Largely a legacy path; the boss-clear / timer-expiry paths flow through level_advance and level_success.
resultsRun over. MissionResult is populated, telemetry is finalized, onGameOver(missionResult) fires.
boss_introReserved for the boss-encounter intro cinematic (declared in the enum; used by encounter signals).
level_successIntermediate state on the win path that hands off to artifact_choice.
artifact_choiceEnd-of-level new-artifact pick (when the unowned-artifact pool is non-empty). Routes back through the levelup reward dispatch with _postRewardLevelAdvance set so the bridge knows to finish into level_advance after the pick.
rewardGeneric reward-overlay marker declared in the enum.
pausedDeclared in the enum.
failedDeclared in the enum.

The mission-level state field missionPhase (string, default 'objective') is separate from game.phase — it tracks mission objective state (objective / extraction / chase / etc.), not run lifecycle.

Transitions — the canonical path

menu ──start()──▶ birth ──spawn-intro done──▶ playing
                                                │
                                                ├─▶ levelup ──pick / animate──▶ playing
                                                │      │
                                                │      └─▶ artifact_choice ──▶ level_advance
                                                │
                                                ├─▶ dead ──cinematic / revive──▶ revived: playing
                                                │                              └─▶ declined: results
                                                │
                                                ├─▶ level_advance ──fade + rebuild──▶ playing
                                                │
                                                └─▶ complete ──delay──▶ results

Every assignment to game.phase in bridge.ts (lines 1411, 1432, 3076, 3134, 3155, 3172, 3516, 3587, 3892, 3906, 7440, 7635, 8126, 8349, 8705, 8742) is paired with a Sig.fire('phase_change', 0, 0, 0, 0, <newPhase>) so observers (audio bus, telemetry sampler, HUD) can react. The bridge also calls callbacks.onPhaseChange(newPhase) for the React shell.

Boot (menu → birth → playing)

makeGameState() seeds phase: 'menu'. When _missionHandle.start() runs (bridge.ts:8126), the phase flips straight to playing and ship._spawnIntroTimer is set to 0.8s. (The birth state is reserved for the spawn-intro window and is checked by the loop and camera; it is set elsewhere in the boot pipeline before start() resumes the loop.)

Level-up / reward (playing → levelup → playing)

The reward queue (game.rewardQueue) is drained at the top of each playing-block frame. When a card-eligible reward dequeues (level-up, weapon chest with free slots, shooting star, artifact box, end-of-level artifact), the bridge sets phase = 'levelup', clamps timeDilation = 0, low-passes the music, and starts the reveal state machine. Phase returns to playing once the player picks (and any upgrade_show or merge animation finishes — handled in hud.ts).

Auto-upgrade flythroughs (event reward artifact, weapon chest with full slots, weapon chest with no unowned weapons left) take the same phase = 'levelup' path but skip the choice screen — the _rewardFamily flag (weapon_chest_auto / event_reward_upgrade) controls the animation pipeline only.

Level end (playing → level_advance → playing or → complete → results)

Three triggers route through the _completeLevel() helper (bridge.ts:1398):

  • Boss killgame._bossLevelCleared set true by encounter.ts.
  • Timer expirygame.overtime set true by loop.ts when missionTimer ≤ 0 on normal / hard level kinds.
  • Vestigial portal entry — legacy sandbox path.

The helper checks isFinalLevel(). If final, phase = 'complete' with a short victory delay before results. If non-final and an unowned artifact is available, push end_of_level_artifact onto the reward queue (which flows through levelup and ends in level_advance via _postRewardLevelAdvance). Otherwise straight to level_advance.

level_advance runs a 0.3s fade, then advances _currentLevel, multiplies worldKnobs (HP ×2.25, damage ×2.15, speed ×1.14, count ×1.65, attack-speed ×1.50), rebuilds the world, resumes audio, and snaps phase back to playing.

Death (playing → dead → results)

When ship.alive flips false during a playing frame, the bridge cancels any active reward, fires telemetry, stops music, sets phase = 'dead'. If _deathDefianceUses < DEATH_DEFIANCE_MAX_USES, the cinematic runs and then the revive prompt is shown. If the player revives, phase returns to playing. If declined or auto-dismissed, deathTimer counts down (1.0s for declined, longer for cinematic-completed) and then phase = 'results' populates MissionResult and fires onGameOver.

Bossroom teardown (onBossEncounterEnd('player_death', ...)) runs on the death transition if a bossArena is active.

Per-phase update gates

The phase string is checked at many points in the per-frame loop to gate work:

CheckWhereWhat it gates
phase === 'playing' || phase === 'birth'loop.ts:49, camera.ts:71Physics step, Rapier step, ship sync, camera follow.
phase === 'playing'loop.ts:80Mission-timer decrement and overtime accumulation.
phase === 'playing'bridge.ts:1354Main gameplay systems block — enemy AI, spawners, weapon fire pipeline, triggers, level-end checks.
phase === 'playing'bridge.ts:1448, 1461Boss-clear and timer-expiry level-end triggers (gated so they don’t fire during levelup, dead, or level_advance).
phase === 'playing'bridge.ts:2758TriggerSystem tick.
phase === 'playing'bridge.ts:7434, 7450Hard-mode overlays, level-advance fade composition.
phase === 'dead'bridge.ts:3551Death cinematic, revive prompt, death-timer countdown.
phase === 'level_advance'bridge.ts:3642Level fade + world rebuild.
phase === 'complete'bridge.ts:3903Victory delay before results.
phase === 'levelup' || isRewardActive()bridge.ts:6664Card-overlay HUD rendering.
phase === 'dead' || phase === 'results'bridge.ts:7482Death/results overlay rendering.

Reward / artifact / weapon-chest dispatch in bridge.ts:3060+ is not gated on phase === 'playing' — it runs every frame so the reward queue drains and the reveal state machine ticks even after phase has flipped to levelup. Time dilation (timeDilation = 0) handles the freeze instead.

Subsystems that run regardless of phase (so cinematics, particles, and UI animate during pauses):

  • Particles, DmgNumbers, SmokeFX, ExplosionFX, PostFx, PlayerGlow, boss VFX layers — all use rawDt, not the dilated dt.
  • updateRewardState() — uses rawDt when _weaponChestFreeze or timeDilation === 0.
  • Sampler / telemetry runs every frame from boot to results.
  • Audio bus + music player tick continuously; phase_change signals trigger filter ramps.

Files

  • engine/core/types.ts:793GamePhase enum definition.
  • engine/core/state.ts:15 — initial phase: 'menu'.
  • engine/bridge.ts — every phase transition + every gated-system check (see line list above).
  • engine/core/loop.ts:49,80 — physics + mission-timer gating.
  • engine/rendering/camera.ts:71 — camera-follow gating.
  • engine/bridge-phase-state.ts — per-mission scratch state that the phase machine reads/writes (death timers, fade alphas, complete delay, revive flags).