Wheel of Fortune

Rare in-run event. The player enters a wheel-type event zone, the simulation freezes (game.timeDilation = 0, music low-pass drops to 1000 Hz), and a full-screen spinning-wheel overlay opens. The player presses SPIN to gamble, EXIT to leave. Distinct from regular reward picks: outcomes are randomized via a weighted spin rather than chosen from a card hand.

Implementation: src/starship-survivors/engine/rendering/wheel-ui.ts, opened from the event branch in src/starship-survivors/engine/bridge.ts around the cev.type === 'wheel' case, state typed in engine/core/types.ts (WheelState, WheelSlice, WheelSliceType).

Wheel composition

Each open builds a fresh slice list and shuffles it.

  • 7 fixed positive slice types: weapon_10 (jackpot, gold), weapon_3, weapon_1, ship_3, ship_1, artifact_3, artifact_1.
  • 1–3 negative slices picked at random from hp_25 (-25% HP), hp_50 (-50% HP), hp_99 (skull icon, -99% HP).
  • Slice count is therefore 8–10. After shuffle, slice order is random per visit.
  • Each slice carries label, sublabel, color, isNegative, isJackpot for rendering (SLICE_DEFS in wheel-ui.ts).

Spin lifecycle

WheelState.phase walks through choosing → spinning → result → choosing. While spinning, spinSubPhase walks through three animation phases:

  1. fast — exponential decay from 32 rad/s, floor 6 rad/s, ~1.4s. Spawns gold spark particles around the rim while spinning. Background radial rays rotate underneath.
  2. decel — cubic ease from 6 rad/s down to ~2 rad/s over the last 3 slice-widths.
  3. tick — 3 discrete snaps to slice boundaries with intervals [0.06, 0.12, 0.22] seconds, then slams to the exact target angle. Lands with a 48-particle burst (gold for positive, red for negative).

The target slice is picked uniformly at random at SPIN time (landedIdx = floor(rng * n)), and the target angle is back-solved so 5–7 full rotations occur before landing with the slice center under the top pointer.

Result phase

resultTimer counts down 1.8s while popOutAnim lerps 0→1, pushing the won slice outward by popOutAnim * 14 * uiScale pixels and replacing its fill with a white→slice-color radial gradient. A reward banner reads e.g. +3 WEAPON below the wheel (gold if jackpot, green if positive, red if negative). On resultTimer <= 0 and rewardApplied, the phase returns to choosing and SPIN re-enables.

Reward application (_applyWheelReward in bridge.ts)

Runs exactly once per result phase (rewardApplied guard). Mapping by slice.type:

SliceEffect
weapon_1 / weapon_3 / weapon_10Adds 1 / 3 / 10 levels to a random equipped weapon, capped at WHEEL_MAX_WEAPON_LVL = 20.
ship_1 / ship_3Applies 1 / 3 stacks of a random modifier from WHEEL_MOD_IDS = ['health', 'shield', 'speed', 'damage_all', 'heat'].
artifact_1 / artifact_3Grants 1 / 3 levels to a random already-owned artifact (no-op if you own none).
hp_25 / hp_50 / hp_99Subtracts 25% / 50% / 99% of ship.hpMax from current HP, clamped to a floor of 1 (never lethal).

After payout, the won slice is overwritten in place with a hp_25 penalty slice (replaceWonSlice), so each spot you land on degrades the wheel for subsequent spins on the same visit.

Spin count and exit

spinCount increments each landing — the player can keep pressing SPIN as long as they accept the worsening odds. EXIT closes the overlay only when phase !== 'spinning': it clears game._wheelState, restores timeDilation to the active test-speed multiplier (normally 1), and lifts the music low-pass back to 5000 Hz. There is no hard spin cap — the cap is self-imposed via accumulated penalty slices and HP damage.

Drop rate

The wheel event type is not in the default 71-entry event pool built by buildDefaultEventPool (see events). It is gated to specific run/world contexts that opt it into their pool. Within those contexts it uses the large event radius (220 px), placing it alongside magnet / weapon / artifact / cascade as a premium reward sub-event.

Sim freeze

Opening the wheel sets game.timeDilation = 0, freezing enemy AI, projectiles, XP collection, and level-up gating for the duration of the overlay. The overlay itself updates on rawDt (wall time) so the spin animation still runs at normal speed. Leveling, banishes, and reward-card auto-spawn are explicitly gated on !game._wheelState so banked XP and queued rewards wait until the overlay closes.

  • Events — full event-type list and pool weights.
  • Rewards — the standard card-pick reward flow this event bypasses.
  • Rendering — canvas-draw architecture the overlay plugs into.