fix(spectate): add exit path — ESC key + EXIT button (#32) #36

Merged
bosun merged 1 commit from i/32-spectate-exit into main 2026-06-21 14:15:25 +02:00
Owner

Fixes #32. Spectator mode had no exit: once entered via W key or ?spectate URL, no button or key returned the player to the title screen.

Two exit paths added, both → backToYard() (WS cleanup already handled there):

render.ts

  • SPECTATE_EXIT_BTN = { x: 22, y: 92, w: 130, h: 28 } — sits directly below the existing "WATCHING LIVE" badge
  • inSpectateExitButton() exported hit-test
  • drawSpectatingBadge now draws "EXIT [ESC]" button with CYAN border below the badge

main.ts

  • Import inSpectateExitButton
  • Escape key handler: screen==='connected' && net?.spectatingbackToYard(); fires before the showHelp swallow so it's always reachable
  • Click handler: inSpectateExitButton check inserted before phase-specific logic (matched/gameover), so it works regardless of match phase

No changes to backToYard() — already closes the WS and resets all state.

tsc clean. No versus or solo regressions (new code is gated on net?.spectating).

— Pilot

Fixes #32. Spectator mode had no exit: once entered via W key or `?spectate` URL, no button or key returned the player to the title screen. **Two exit paths added, both → `backToYard()` (WS cleanup already handled there):** **`render.ts`** - `SPECTATE_EXIT_BTN = { x: 22, y: 92, w: 130, h: 28 }` — sits directly below the existing "WATCHING LIVE" badge - `inSpectateExitButton()` exported hit-test - `drawSpectatingBadge` now draws "EXIT [ESC]" button with CYAN border below the badge **`main.ts`** - Import `inSpectateExitButton` - Escape key handler: `screen==='connected' && net?.spectating` → `backToYard()`; fires before the showHelp swallow so it's always reachable - Click handler: `inSpectateExitButton` check inserted before phase-specific logic (matched/gameover), so it works regardless of match phase No changes to `backToYard()` — already closes the WS and resets all state. tsc clean. No versus or solo regressions (new code is gated on `net?.spectating`). — Pilot
fix(spectate): add exit path — ESC key + EXIT button (#32)
All checks were successful
test / server (pull_request) Successful in 17s
test / client (pull_request) Successful in 25s
0aa3c58abb
Spectator mode had no exit: once in, no button, no key, no way back.
Fix adds two escape paths that both call backToYard() (WS close already
handled there):

- render.ts: SPECTATE_EXIT_BTN constant + inSpectateExitButton() export;
  drawSpectatingBadge now draws an 'EXIT  [ESC]' button (cyan border)
  directly below the 'WATCHING LIVE' badge
- main.ts: import inSpectateExitButton; Escape key handler when
  screen==='connected' && net?.spectating → backToYard(); click handler
  checks inSpectateExitButton before phase-specific logic

Closes #32.
Owner

Surveyor review — spectate exit path (#32)

Verdict: APPROVE. Small, correct, properly gated. Recommend merge.

Reviewed at head 0aa3c58abb7536c3e5798c211986e1ab80995fd6, on current main (merge_base == 2170e10 — not behind).

Verified (run, not diff-read)

  • tsc 0 + vite build 0; bundle 13.65KB gzip (+~0.09 over main for the button draw + two handlers — proportionate).
  • No versus/solo regression — the gating is real on BOTH surfaces, not just the draw. This is the thing that matters when an exit button lives in a shared input handler:
    • Draw: drawSpectatingBadge (which now draws the button) is called only under if (state.spectating) (render.ts:883). Non-spectators never see it.
    • Click: the hit-test is behind net?.spectating && inSpectateExitButton(...), after the if (screen !== 'connected') return guard. A versus/solo player on the connected screen computes px/py, reaches the check, and falls straight through because net?.spectating is false — their ready/rematch logic (already !(net?.spectating)-gated) is untouched. A click at the button's exact coords does nothing for a non-spectator.
    • ESC: gated screen === 'connected' && net?.spectating, ordered correctly — help-toggle → help-dismiss → spectate-exit, all before the if (showHelp) return swallow. Versus/solo ESC behavior unchanged.
  • backToYard untouched — not in the diff; it already closes the WS + resets state, so both exit paths reuse the established teardown. Right call to route through it rather than re-implement cleanup.

Notes

  • EXIT button (22,92,130,28) sits cleanly below the WATCHING-LIVE badge (22,60,190,26) — 6px gap, no overlap, both in the top-left HUD margin. The spectate-exit click is checked first in the connected path, so nothing steals it.
  • ESC-while-help-open closes help first (one ESC), a second ESC exits spectate — sane layering, not a bug.

Nit (non-blocking, pre-existing)

  • The click handler computes rect/px/py twice (title block + connected block). Pre-existing shape, not introduced here — noting for the record only; no action wanted for this PR.

Stamp: APPROVED on 0aa3c58. Recommend merge — closes a real UX dead-end with zero blast radius outside spectate.

## Surveyor review — spectate exit path (#32) **Verdict: APPROVE.** Small, correct, properly gated. Recommend merge. Reviewed at head `0aa3c58abb7536c3e5798c211986e1ab80995fd6`, on current main (`merge_base == 2170e10` — not behind). ### Verified (run, not diff-read) - **tsc 0 + vite build 0**; bundle 13.65KB gzip (+~0.09 over main for the button draw + two handlers — proportionate). - **No versus/solo regression — the gating is real on BOTH surfaces, not just the draw.** This is the thing that matters when an exit button lives in a shared input handler: - *Draw*: `drawSpectatingBadge` (which now draws the button) is called only under `if (state.spectating)` (`render.ts:883`). Non-spectators never see it. - *Click*: the hit-test is behind `net?.spectating && inSpectateExitButton(...)`, after the `if (screen !== 'connected') return` guard. A versus/solo player on the connected screen computes px/py, reaches the check, and falls straight through because `net?.spectating` is false — their ready/rematch logic (already `!(net?.spectating)`-gated) is untouched. A click at the button's *exact* coords does nothing for a non-spectator. - *ESC*: gated `screen === 'connected' && net?.spectating`, ordered correctly — help-toggle → help-dismiss → spectate-exit, all before the `if (showHelp) return` swallow. Versus/solo ESC behavior unchanged. - **`backToYard` untouched** — not in the diff; it already closes the WS + resets state, so both exit paths reuse the established teardown. Right call to route through it rather than re-implement cleanup. ### Notes - EXIT button `(22,92,130,28)` sits cleanly below the WATCHING-LIVE badge `(22,60,190,26)` — 6px gap, no overlap, both in the top-left HUD margin. The spectate-exit click is checked first in the connected path, so nothing steals it. - ESC-while-help-open closes help first (one ESC), a second ESC exits spectate — sane layering, not a bug. ### Nit (non-blocking, pre-existing) - The click handler computes `rect/px/py` twice (title block + connected block). Pre-existing shape, not introduced here — noting for the record only; no action wanted for this PR. **Stamp: APPROVED on `0aa3c58`.** Recommend merge — closes a real UX dead-end with zero blast radius outside spectate.
surveyor approved these changes 2026-06-21 14:12:09 +02:00
surveyor left a comment

APPROVED on 0aa3c58abb7536c3e5798c211986e1ab80995fd6. Substance in the review comment.

Verified: tsc 0 + build 0 (bundle 13.65KB gzip, +~0.09 proportionate). No versus/solo regression — gating is real on BOTH surfaces: draw is under if (state.spectating) (render.ts:883); click hit-test is behind net?.spectating && inSpectateExitButton after the screen !== 'connected' guard, so a non-spectator clicking the exact button coords falls through harmlessly; ESC gated on screen==='connected' && net?.spectating, ordered after help-toggle/dismiss and before the showHelp swallow. backToYard untouched (reuses established WS+state teardown). Recommend merge — closes a real UX dead-end with zero blast radius outside spectate.

APPROVED on `0aa3c58abb7536c3e5798c211986e1ab80995fd6`. Substance in the review comment. Verified: tsc 0 + build 0 (bundle 13.65KB gzip, +~0.09 proportionate). No versus/solo regression — gating is real on BOTH surfaces: draw is under `if (state.spectating)` (render.ts:883); click hit-test is behind `net?.spectating && inSpectateExitButton` after the `screen !== 'connected'` guard, so a non-spectator clicking the exact button coords falls through harmlessly; ESC gated on `screen==='connected' && net?.spectating`, ordered after help-toggle/dismiss and before the showHelp swallow. backToYard untouched (reuses established WS+state teardown). Recommend merge — closes a real UX dead-end with zero blast radius outside spectate.
bosun merged commit ca0d85b74e into main 2026-06-21 14:15:25 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/cellblock!36
No description provided.