Time Dilation

Game-feel slow-motion and freeze-frame system. The fixed-timestep loop scales its simulation accumulator by a per-frame multiplier, so any code can pause, slow, or accelerate the entire world by writing one number. Used for hit-stop on damage events, reward-screen freezes, boss-cinematic slow-mo, the Wheel of Fortune overlay, and the boss-test 4x speed mode.

Mechanism

Three numbers on GameState (engine/core/types.ts) drive sim rate:

FieldDefaultRangeOwner
timeDilation10 (freeze) → 4 (test speed)Gameplay code (damage, rewards, wheel)
juiceDilation1clamped to [0.1, 1.0]Boss cinematics (Awakened Mech phase)
_hitFreezeTimer0seconds remaining (wall-time)Hit-stop driver

The main loop (engine/core/loop.ts) reads them like this:

const simRate = game.timeDilation * game.levelSpeedMult * juiceClamped;
accumulator += rawDt * simRate;

When simRate = 0, the accumulator never crosses SIM_DT, no sim steps run, and the world is fully paused. The render pass still draws every RAF frame — HUD overlays, reward cards, and boss-death cinematics that tick on rawDt keep animating through the freeze.

Hit-freeze fast path

A separate _hitFreezeTimer (counted in wall-time rawDt) gives a hard zero-sim window without touching the accumulator at all. While the timer is positive the loop early-returns before any sim step runs, then auto-restores timeDilation = _testSpeedMult || 1 when the timer hits zero. This is the mechanism every “freeze frame on impact” call site uses.

Call sites

TriggerDurationSource
Shield hit (per damage event)0.025s + shieldFrac * 0.04sengine/combat/damage.ts
Hull hit (per damage event)0.04s + hullFrac * 0.06sengine/combat/damage.ts
Shield break “system shock”0.10s (with invert overlay)engine/combat/damage.ts
Combo-kill milestoneparam-table p.freeze valueengine/combat/damage.ts
Charger electric slamCHARGER_HIT_FREEZE constengine/enemies/behaviors.ts
Weapon-chest collectheld until reward closesengine/bridge.ts (_weaponChestFreeze)
Artifact-box collectheld until reward closesengine/bridge.ts
Shooting-star collectheld until reward closesengine/world/shooting-star.ts
Wheel of Fortune overlayheld until wheel exitsengine/bridge.ts
Event-reward upgrade animationheld until upgrade-show endsengine/bridge.ts
Level-up reward screenheld until card pickedengine/bridge.ts + engine/rendering/hud.ts
Boss phase cinematicshort window (300-500ms)data/bosses/awakened-mech.ts (uses juiceDilation)
Boss-test speed overridepersistsengine/bridge.ts dev console (speed(4))

Reward screens freeze the world

When a reward screen opens — level-up choice, weapon chest, artifact box, event reward, shooting star — game._weaponChestFreeze = true and game.timeDilation = 0 together pause every gameplay system. The reward state machine in engine/rendering/hud.ts ticks on rawDt so card reveal, shine, and selection animations still play. Closing the screen (or completing the upgrade-show animation) restores both flags.

What still ticks during freeze

Anything that explicitly reads rawDt instead of dt:

  • Reward-card state machine (updateRewardState)
  • Boss-VFX layers (tickBossVfxLayers in engine/vfx/boss-layers.ts)
  • Death cinematic (deathElapsed ticks for the “YOU DIED” overlay)
  • Wheel of Fortune spinner (updateWheel)
  • Weapon-chest collect fly-to animation + particles
  • Camera shake/recovery
  • Ring-pop VFX (updateRingPop)
  • Hit-freeze invert-screen overlay (_invertScreenTimer)
  • wallTime and uiTime accumulators

Everything else (enemy AI, weapon cooldowns, mission timers, XP, level-ups, boss attacks) routes through game._dt, which is zero when sim is paused.

Juice channel — boss cinematics

juiceDilation is the boss-cinematic slow-mo channel. The Awakened Mech phase-transition code (data/bosses/awakened-mech.ts) uses pushTimeSlow(factor, durationMs) which sets juiceDilation to a fraction (e.g. 0.2) and reverts via a setTimeout. The loop clamps it to [0.1, 1.0] so a stuck/dropped revert can’t freeze the sim or accelerate it past baseline. A counter (_activeTimeSlows) handles overlapping calls so back-to-back pushes don’t race their reverts.

Boss-test speed override

game._testSpeedMult (dev-console speed(4)) scales timeDilation for boss test runs. The test runner uses values up to 4x to compress a 10-minute boss fight into ~2.5 minutes of wall time. The loop scales MAX_STEPS_BASE with simRate (Math.max(MAX_STEPS_BASE, Math.ceil(simRate * MAX_STEPS_BASE))) so high-speed runs don’t hit the spiral-of-death cap on minor frame jitter.

Restore conventions

  • Hit-freeze auto-restores via _hitFreezeTimer countdown (loop).
  • Reward freezes restore in their close handlers (_weaponChestFreeze = false; timeDilation = 1).
  • Level transitions restore timeDilation = 1 defensively in engine/bridge.ts and engine/rendering/hud.ts.
  • Juice slow-mo restores on setTimeout (boss-cinematic owner code).
  • Boss-test override restores in dev.speed(1).

If you write a new call site that sets timeDilation = 0, you own the restore path. There is no global “auto-thaw” — a missed restore freezes the game permanently.