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,isJackpotfor rendering (SLICE_DEFSinwheel-ui.ts).
Spin lifecycle
WheelState.phase walks through choosing → spinning → result → choosing. While spinning, spinSubPhase walks through three animation phases:
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.decel— cubic ease from 6 rad/s down to ~2 rad/s over the last 3 slice-widths.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:
| Slice | Effect |
|---|---|
weapon_1 / weapon_3 / weapon_10 | Adds 1 / 3 / 10 levels to a random equipped weapon, capped at WHEEL_MAX_WEAPON_LVL = 20. |
ship_1 / ship_3 | Applies 1 / 3 stacks of a random modifier from WHEEL_MOD_IDS = ['health', 'shield', 'speed', 'damage_all', 'heat']. |
artifact_1 / artifact_3 | Grants 1 / 3 levels to a random already-owned artifact (no-op if you own none). |
hp_25 / hp_50 / hp_99 | Subtracts 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.