diff --git a/docs/superpowers/specs/2026-03-13-menu-rework-design.md b/docs/superpowers/specs/2026-03-13-menu-rework-design.md index d353616..d77e70b 100644 --- a/docs/superpowers/specs/2026-03-13-menu-rework-design.md +++ b/docs/superpowers/specs/2026-03-13-menu-rework-design.md @@ -57,7 +57,10 @@ Replaces the current `RoomHeader` component. ``` - **Left:** "I❤️ESC" as the app title (text, not emoji — the heart is the Unicode character ❤️) -- **Right:** Room code (monospace, bold) + connection status dot (green/yellow/red) +- **Right:** Room code (monospace, bold) + connection status dot + - Green = `connected` + - Yellow = `connecting` + - Red = `disconnected` - `position: sticky; top: 0; z-index: 50` - `padding-top: env(safe-area-inset-top)` for iPhone notch/Dynamic Island - Act badge removed — the game content makes the current phase obvious @@ -135,26 +138,46 @@ Shows the player's current bingo card. Same `BingoCard` component as today, with When a player completes a bingo line: 1. **Server detects completion** — the server already detects bingo lines and awards 10 bonus points. Extended to mark the card as `completed`. -2. **Card stored** — the completed card moves to a `completedBingoCards` array in game state (per player). Contains: `playerId`, `displayName`, `card` (the full card data), `completedAt` (timestamp). +2. **Card stored** — the completed card moves to a `completedBingoCards` array in game state (per player). Contains: `playerId`, `displayName`, `card` (the full card data with marked squares), `completedAt` (ISO 8601 timestamp). 3. **Player sees** — a celebration message ("Bingo!") + "Draw New Card" button. -4. **Redraw** — new WS message `request_new_bingo_card`. Server generates a fresh card with different tropes. The old card is already in `completedBingoCards`. +4. **Redraw** — new WS message `request_new_bingo_card`. Server generates a fresh card with tropes not present on the just-completed card (best effort — if the trope pool is exhausted, duplicates are allowed). The old card is already in `completedBingoCards`. 5. **New card** — `myBingoCard` in game state updates to the fresh card. Player can continue playing. +6. **Redraw is only available during `live-event`** — same act gate as tapping squares. + +### Scoring Across Multiple Cards + +Bingo points accumulate across all cards (completed + active). `getBingoScore()` sums: +- 2 points per tapped square across all completed cards and the active card +- 10 bonus points per completed bingo line (each completed card contributed at least one line; the active card may also have lines) + +The `announcedBingo` set is changed to track `playerId:cardIndex` instead of just `playerId`, so multiple bingo announcements per player are possible. ### Data Changes -Shared types additions: -``` -completedBingoCards: { playerId: string, displayName: string, card: BingoCard, completedAt: string }[] +New Zod schema in `game-types.ts`: +```typescript +export const completedBingoCardSchema = z.object({ + playerId: z.string(), + displayName: z.string(), + card: bingoCardSchema, + completedAt: z.string(), +}) ``` -New WS client message: +Added to `gameStateSchema`: +```typescript +completedBingoCards: z.array(completedBingoCardSchema) ``` -{ type: "request_new_bingo_card" } + +New WS client message in `ws-messages.ts` (added to `clientMessage` discriminated union): +```typescript +export const requestNewBingoCardMessage = z.object({ type: z.literal("request_new_bingo_card") }) ``` Game state changes: - `myBingoCard` continues to represent the current active card - `completedBingoCards` is a new array on `GameState` (visible to all — host needs it for verification) +- Display view receives `completedBingoCards` for bingo announcement purposes only (no card detail needed on projector) --- @@ -162,7 +185,7 @@ Game state changes: Replaces both the current `Leaderboard` and `PlayerList` components as separate UI elements. -- **Lobby** (no scores yet): shows player list with names only — a "who's here" view +- **Lobby** (no scores yet): shows player list with names only — a "who's here" view. The `Leaderboard` component renders a simplified layout: just rank numbers and names, no score columns. - **All other acts**: full leaderboard table (rank, name, P/J/B/Q breakdown, total points) - **Scoring explanation**: "How scoring works" box at the bottom (same as current) @@ -195,7 +218,7 @@ Vertical stack of cards, only visible to the host. This tab contains all host-ex ### Bottom -7. **End Party** button — destructive (red), always available except when already ended +7. **End Party** button — destructive (red), always available except when already ended. This is the same action as advancing from scoring to ended, but available from any act as a shortcut. The act-specific "End Party" label in Act Controls (scoring → ended) is removed to avoid duplication. --- @@ -253,12 +276,31 @@ Unchanged. Full-screen passive view, no bottom nav. Continues to show: ### Game State Changes -- Add `completedBingoCards` to game state schema -- Include in `getGameStateForPlayer` and `getGameStateForDisplay` +- Add `completedBingoCards` to game state schema (Zod schema + TypeScript type) +- Include in `getGameStateForPlayer` (full card data for host verification) and `getGameStateForDisplay` (for announcements only) +- Update `getBingoScore()` to sum points across all completed cards + active card +- Change `announcedBingo` from `Set` (playerId) to track `playerId:cardIndex` pairs, allowing multiple bingo announcements per player + +### WS Handler Changes + +- Add `request_new_bingo_card` case to handler, gated to `live-event` act +- Add the message to the `clientMessage` discriminated union in `ws-messages.ts` + +### Route Transition Safety + +- WebSocket connection and Zustand store are managed in the layout route, not child routes. Navigating between tabs (child routes) does not trigger WS reconnection or store reset. --- ## 11. Migration / URL Compatibility - Old URLs (`/play/ABCD`, `/host/ABCD`) should redirect to `/play/ABCD/game` and `/host/ABCD/game` respectively -- TanStack Router can handle this via the layout route's default redirect +- Implemented via TanStack Router index routes that redirect (e.g., a `/play/$roomCode/` index route with `beforeLoad` that throws `redirect({ to: '/play/$roomCode/game' })`) + +--- + +## 12. Label Naming + +- Player bottom nav labels: "Game", "Bingo", "Leaderboard" (3 tabs, enough space for full labels) +- Host bottom nav labels: "Game", "Bingo", "Board", "Host" (4 tabs, "Board" is shortened from "Leaderboard" for space) +- Route paths use short names: `/game`, `/bingo`, `/board`, `/host`