PURPOSE

Wheel of Fortune overlay for the wheel event type. Owns the full-screen spinning prize wheel: slice generation, spin animation (fast / decel / tick / result sub-phases), pointer + center cap + outer ring rendering, SPIN and EXIT button rendering, and SPIN/EXIT hit-testing. Slices encode positive rewards (weapon, ship, artifact stacks; one weapon_10 jackpot) and negative HP penalties (-25%, -50%, -99%). Renders bottom-anchored result banner during the result phase.

OWNS

  • SLICE_DEFS — the static slice catalogue keyed by WheelSliceType, providing label, sublabel, color, isNegative, and isJackpot for every slice variant.
  • initWheelState(rng) — builds a fresh WheelState: shuffles 7 positives plus 1–3 randomly selected negatives into the wheel and zero-initializes the animation fields.
  • replaceWonSlice(state, wonIdx) — overwrites the indicated slice with a hp_25 penalty (used after a positive outcome is claimed).
  • triggerSpin(state, rng) — picks the landing index, computes the target rotation angle that aligns that slice’s center with the pointer after 5–7 full rotations, resets animation timers, and flips phase to spinning.
  • updateWheel(state, dt) — drives the spin state machine each frame: fast exponential-decay rotation with coin particle bursts, decel cubic-eased slowdown, tick discrete snaps to slice boundaries using TICK_INTERVALS = [0.06, 0.12, 0.22], transition to result with a 1.8s timer plus landing particle burst, and result → choosing reset once rewardApplied is true.
  • hitTestWheelSpin(px, py) / hitTestWheelExit(px, py) — point-in-rect tests against the SPIN and EXIT button hitboxes cached during the last draw.
  • drawWheel(ctx) — full overlay render: dark backdrop, rotating radial rays during spin, segmented pie wheel with per-slice fill / border / jackpot pulse / pop-out glow for the won slice, center cap, outer ring, downward-pointing pointer triangle, “WHEEL OF FORTUNE” title, result banner, and SPIN + EXIT round-rect buttons.
  • Module-private hitbox state _spinHitBox / _exitHitBox, helper _shuffle, helper _drawSegmentText, and helper _roundRect.
  • Spin tuning constants: SPIN_START_SPEED = 32, SPIN_DECAY = 0.8, SPIN_FAST_FLOOR = 6, DECEL_SPAN_SLICES = 3.

READS FROM

  • game._wheelState (read inside drawWheel; bridge passes the same object explicitly to updateWheel / triggerSpin / replaceWonSlice).
  • game.time for jackpot pulse and SPIN button pulse phase.
  • ship is imported from ../core (unused at present beyond the import).
  • W, H, uiScale from ../core for layout: wheel center (W/2, H*0.42), radius min(W,H)*0.32, button row at H*0.88.
  • WheelSlice, WheelSliceType, WheelState types from ../core/types.

PUSHES TO

  • Mutates the passed WheelState: phase, slices, spinAngle, startAngle, targetAngle, landedIdx, resultTimer, popOutAnim, spinSubPhase, spinElapsed, tickTimer, tickCount, ticksNeeded, rewardApplied, spinCount, bgRotation.
  • Particles.burst — yellow spark bursts radiating around the wheel rim during the fast spin phase (capped to the first 2.5 s).
  • Particles.burstHex — a 48-spark burst at the wheel center on landing, red (#ff4444) for negative results and gold (#ffd228) otherwise.
  • Canvas state via ctx — sets composite mode, fill/stroke styles, shadow, transform, alpha, font, and text alignment; every block is wrapped in save/restore.
  • Module-local _spinHitBox / _exitHitBox rectangles, refreshed at the end of every drawWheel call.

DOES NOT

  • Does not apply the reward. The bridge calls _applyWheelReward(game._wheelState) after updateWheel and is responsible for translating the won slice into weapon / ship / artifact gains or HP loss.
  • Does not allocate or free game._wheelState. The bridge constructs it via initWheelState when the wheel event starts and nulls it on EXIT.
  • Does not own input wiring. GameScreen calls hitTestWheelSpin / hitTestWheelExit from its pointer-down handler; this module only reports geometry.
  • Does not play audio.
  • Does not write to telemetry, Supabase, or any persistent store.
  • Does not gate on game pause flags, leveling, or other run state; the bridge guards entry by checking !game._wheelState before stepping leveling and reward queues.
  • Does not animate the EXIT button (no pulse), and does not respond to keyboard input.

Signals

  • phase (choosingspinningresultchoosing) is the public progression signal. Callers read it to gate SPIN re-arm and EXIT availability.
  • rewardApplied is set by the bridge after it consumes a result outcome; updateWheel only resets back to choosing once both resultTimer has elapsed and rewardApplied is true.
  • landedIdx >= 0 during result indicates a valid claim target; reset to -1 on the choosing transition.
  • spinCount increments at every landing — usable as a per-run counter.

Entry points

  • initWheelState(rng) — called by the bridge once when the wheel event begins (game._wheelState = initWheelState(() => Math.random())).
  • triggerSpin(state, rng) — called from the bridge SPIN handler when the SPIN button is tapped.
  • updateWheel(state, dt) — called every frame from the bridge update loop when game._wheelState is non-null.
  • replaceWonSlice(state, wonIdx) — called by the bridge after a positive outcome is paid out so the same slice cannot be claimed again on the next spin.
  • drawWheel(ctx) — called from the bridge render path when game._wheelState is non-null.
  • hitTestWheelSpin / hitTestWheelExit — called from GameScreen pointer handling.

Pattern notes

  • Pure-data + free-function module: no class, no singleton, no lifecycle object. State lives in the caller’s WheelState, which is owned by game._wheelState. The module is stateless apart from the two cached hitbox rectangles.
  • Spin animation is a deterministic state machine with four sub-phases (fast, decel, tick, plus the implicit result). The tick phase always snaps to slice boundaries and forces the final value to targetAngle so the landing is frame-exact regardless of frame timing.
  • Target-angle math accounts for cumulative spinAngle so successive spins always rotate forward by at least fullSpins * TAU.
  • Slice rendering uses a Fisher–Yates shuffle (_shuffle) seeded by the caller’s rng, keeping the wheel layout reproducible from a seeded run.
  • Hit-test geometry is published as a side effect of drawWheel: the buttons cannot be hit before the first frame they are drawn. This matches the rest of the engine’s pattern where layout and hit zones share one source of truth.
  • Particle effects are emitted by probability (Math.random() < dt * 14) rather than fixed cadence — frame-rate-independent spawn density.
  • Jackpot (weapon_10) and the lethal hp_99 slice each get a custom text style branch in _drawSegmentText; everything else falls through to the generic positive/negative styling.
  • _roundRect is a local quadratic-curve rounded-rect helper used only by SPIN and EXIT — no shared utility import.
  • The ship import from ../core is currently unused; the file relies only on game, W, H, and uiScale.