Add design doc for overlay manager and player list
Event-driven state machine to coordinate room code display, audio, and a new player slot list. Fixes audio restart bug by centralizing lifecycle management. Adds new shard-based WebSocket event handling. Made-with: Cursor
This commit is contained in:
154
docs/plans/2026-03-20-overlay-manager-player-list-design.md
Normal file
154
docs/plans/2026-03-20-overlay-manager-player-list-design.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# Overlay Manager & Player List Design
|
||||
|
||||
**Date:** 2026-03-20
|
||||
**Status:** Approved
|
||||
|
||||
## Problem
|
||||
|
||||
The OBS overlay (`optimized-controls.html`) has two issues after the upstream Game Picker API migrated from Puppeteer to an ecast shard monitor:
|
||||
|
||||
1. **Audio doesn't restart on new room codes.** The overlay updates the room code text when `game.added` fires, but audio playback fails to reinitialize because `hideDisplay()` (triggered by `game.started` or `audience.joined`) leaves the audio element in a state that `startAnimation()` doesn't fully recover from. There is no centralized lifecycle management — show/hide logic is scattered across event handlers.
|
||||
|
||||
2. **No player list.** The new shard-based API pushes real-time player names and join events (`room.connected`, `lobby.player-joined`), but the overlay doesn't consume them. Players joining the lobby should be visible to viewers.
|
||||
|
||||
Additionally, several new WebSocket events (`room.connected`, `lobby.player-joined`, `lobby.updated`, `game.ended`, `room.disconnected`) are not handled, and the removed `audience.joined` event is still referenced.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Event-driven state machine** over reactive flags or per-component lifecycle management. A central `OverlayManager` coordinates all components through explicit state transitions, preventing the ad-hoc state bugs that cause the current audio issue.
|
||||
- **ES module split** (no build step) over single-file or bundled architecture. Keeps OBS browser source simplicity while improving maintainability.
|
||||
- **WebSocket-only player data** — no REST polling fallback for player lists.
|
||||
- **Overlay visible only during lobby state** — room code, audio, and player list all share the same lifecycle.
|
||||
|
||||
## Architecture
|
||||
|
||||
### State Machine
|
||||
|
||||
```
|
||||
idle → lobby → playing → ended → idle
|
||||
↑ |
|
||||
└─────────────────────────┘
|
||||
(new game.added)
|
||||
|
||||
Any state → disconnected → idle (on reconnect, no active lobby)
|
||||
→ lobby (on reconnect, active lobby)
|
||||
```
|
||||
|
||||
| State | Entry Trigger | Visible Components |
|
||||
|-------|--------------|-------------------|
|
||||
| `idle` | Initial load, `session.ended`, `game.ended`, `room.disconnected` | None |
|
||||
| `lobby` | `game.added`, `room.connected` (lobby state) | Room code + audio + player list |
|
||||
| `playing` | `game.started` | None |
|
||||
| `ended` | `game.ended` | None (transitions to `idle` after brief delay) |
|
||||
| `disconnected` | WebSocket close/error | None (reconnect logic runs) |
|
||||
|
||||
Key transition: `game.added` while already in `lobby` triggers a **full reset** — deactivate all components, update context, reactivate. This fixes the audio restart bug by design.
|
||||
|
||||
### Component Interface
|
||||
|
||||
Every component implements:
|
||||
|
||||
- `activate(context)` — enter active state with room/game/player context
|
||||
- `deactivate()` — fully clean up (stop audio, clear timers, hide elements)
|
||||
- `update(context)` — handle in-state updates (new player joined, etc.)
|
||||
- `getStatus()` — return current status for the debug panel
|
||||
|
||||
### File Layout
|
||||
|
||||
```
|
||||
OBS-stuff/
|
||||
├── optimized-controls.html # DOM + CSS + bootstrap script
|
||||
├── js/
|
||||
│ ├── state-manager.js # OverlayManager: state machine, component registry
|
||||
│ ├── websocket-client.js # Auth, connect, reconnect, event routing
|
||||
│ ├── room-code-display.js # Room code animation component
|
||||
│ ├── audio-controller.js # Audio playback lifecycle
|
||||
│ ├── player-list.js # Player slot list component
|
||||
│ └── controls.js # Settings panel + debug dashboard
|
||||
```
|
||||
|
||||
Loaded via `<script type="module">` — no build step.
|
||||
|
||||
## Player List
|
||||
|
||||
### Visual Design
|
||||
|
||||
Vertical numbered roster positioned to the left or right of the room code (configurable):
|
||||
|
||||
```
|
||||
1. xXSlayerXx ← filled (bright text)
|
||||
2. CoolPlayer42 ← filled
|
||||
3. ──────────── ← empty (dim placeholder)
|
||||
4. ────────────
|
||||
5. ────────────
|
||||
```
|
||||
|
||||
Slot count equals `maxPlayers` from the shard's `room.connected` event (fallback: game catalog `max_players` from `game.added`, default: 8).
|
||||
|
||||
### Configuration
|
||||
|
||||
- Enable/disable toggle
|
||||
- Position: left or right of room code
|
||||
- Font size, text color, empty slot color
|
||||
- Vertical offset
|
||||
|
||||
### Behavior
|
||||
|
||||
1. `activate(ctx)`: Create `maxPlayers` empty slots. Fade in using the same timing curve as the room code animation.
|
||||
2. `lobby.player-joined`: Fill next empty slot with player name (subtle fade-in on the name text). Uses the `players[]` array from the event to diff against displayed slots.
|
||||
3. `room.connected` (with existing players — e.g., reconnect): Bulk-fill all known players.
|
||||
4. `deactivate()`: Fade out all slots, clear the list.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- More players than `maxPlayers` (audience members): ignored, only player slots shown.
|
||||
- Player names unavailable: fallback to "Player 1", "Player 2", etc.
|
||||
- No `maxPlayers` data yet: default to 8 slots, update when data arrives.
|
||||
|
||||
## Debug Dashboard & Manual Overrides
|
||||
|
||||
New collapsible "Overlay Manager" section in the controls panel.
|
||||
|
||||
### State Display
|
||||
|
||||
- Current state badge (color-coded): `IDLE` / `LOBBY` / `PLAYING` / `ENDED` / `DISCONNECTED`
|
||||
- Room code, session ID, game title + pack
|
||||
- Player count: `3 / 8`
|
||||
|
||||
### Component Override Table
|
||||
|
||||
| Component | State | Override |
|
||||
|-----------|-------|----------|
|
||||
| Room Code | Active (cycling) | [Auto] / [Force Show] / [Force Hide] |
|
||||
| Audio | Active (playing) | [Auto] / [Force Show] / [Force Hide] |
|
||||
| Player List | Active (3/8) | [Auto] / [Force Show] / [Force Hide] |
|
||||
|
||||
- **Auto** (default): follows state machine
|
||||
- **Force Show**: always visible, uses last known context
|
||||
- **Force Hide**: always hidden regardless of state
|
||||
- Overrides are session-only (not persisted to localStorage)
|
||||
|
||||
### Event Log
|
||||
|
||||
Scrollable log of last ~20 WebSocket events with timestamps.
|
||||
|
||||
## WebSocket Event Mapping
|
||||
|
||||
| Event | New Behavior |
|
||||
|-------|-------------|
|
||||
| `game.added` | Transition to `lobby` with room code + game metadata |
|
||||
| `room.connected` | Enrich lobby context: `maxPlayers`, initial `players[]` |
|
||||
| `lobby.player-joined` | Update player list with new player |
|
||||
| `lobby.updated` | Update lobby context |
|
||||
| `game.started` | Transition to `playing` |
|
||||
| `game.ended` | Transition to `ended` → `idle` |
|
||||
| `room.disconnected` | Transition to `idle` (with reason logging) |
|
||||
| `session.started` | Subscribe to session, set session context |
|
||||
| `session.ended` | Transition to `idle`, clear all context |
|
||||
| `player-count.updated` | Update player count (manual REST override, less common now) |
|
||||
| `vote.received` | Pass-through logging |
|
||||
| `audience.joined` | **Removed** — no longer exists in the new API |
|
||||
|
||||
Auth flow unchanged: `POST /api/auth/login` with `{ key }`, JWT response, Bearer header.
|
||||
|
||||
Reconnection preserves current exponential backoff. On reconnect: re-auth, fetch active session, re-subscribe. If lobby is active, `room.connected` fires with current state.
|
||||
Reference in New Issue
Block a user