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:
| Field | Default | Range | Owner |
|---|---|---|---|
timeDilation | 1 | 0 (freeze) → 4 (test speed) | Gameplay code (damage, rewards, wheel) |
juiceDilation | 1 | clamped to [0.1, 1.0] | Boss cinematics (Awakened Mech phase) |
_hitFreezeTimer | 0 | seconds 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
| Trigger | Duration | Source |
|---|---|---|
| Shield hit (per damage event) | 0.025s + shieldFrac * 0.04s | engine/combat/damage.ts |
| Hull hit (per damage event) | 0.04s + hullFrac * 0.06s | engine/combat/damage.ts |
| Shield break “system shock” | 0.10s (with invert overlay) | engine/combat/damage.ts |
| Combo-kill milestone | param-table p.freeze value | engine/combat/damage.ts |
| Charger electric slam | CHARGER_HIT_FREEZE const | engine/enemies/behaviors.ts |
| Weapon-chest collect | held until reward closes | engine/bridge.ts (_weaponChestFreeze) |
| Artifact-box collect | held until reward closes | engine/bridge.ts |
| Shooting-star collect | held until reward closes | engine/world/shooting-star.ts |
| Wheel of Fortune overlay | held until wheel exits | engine/bridge.ts |
| Event-reward upgrade animation | held until upgrade-show ends | engine/bridge.ts |
| Level-up reward screen | held until card picked | engine/bridge.ts + engine/rendering/hud.ts |
| Boss phase cinematic | short window (300-500ms) | data/bosses/awakened-mech.ts (uses juiceDilation) |
| Boss-test speed override | persists | engine/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 (
tickBossVfxLayersinengine/vfx/boss-layers.ts) - Death cinematic (
deathElapsedticks 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) wallTimeanduiTimeaccumulators
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
_hitFreezeTimercountdown (loop). - Reward freezes restore in their close handlers (
_weaponChestFreeze = false; timeDilation = 1). - Level transitions restore
timeDilation = 1defensively inengine/bridge.tsandengine/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.
Related
- Reward Cinematics — reveal state machine that drives reward-screen animation during freeze.
- Boss Phase Transitions — the only system that
writes
juiceDilation. - Hit Feedback — shield/hull flash + freeze tuning.