Standardized YAML frontmatter on all 115 sprint briefing files across sprints 1-26 with title, description, type, status, sprint number, and team fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
13 KiB
Markdown
159 lines
13 KiB
Markdown
---
|
||
title: "Sprint 24 — Client Briefing"
|
||
description: "Character select screen, triangle activation response, news ticker display"
|
||
type: sprint
|
||
status: archived
|
||
sprint: 24
|
||
team: "client"
|
||
---
|
||
|
||
# Sprint 24: Signal — Client Tasks
|
||
|
||
**Goal:** Wire the storyteller's activation event into player-visible consequences, thread character archetype through the full session lifecycle, and deliver the first unscripted end-to-end v0.1 playthrough — from main menu to triangle activation.
|
||
|
||
**Branch:** `client`
|
||
**Agents:** Stig (UI/rendering), Hoshe (QA)
|
||
|
||
> **This is the capstone sprint for v0.1.** Client work this sprint wires the player-facing signal
|
||
> that makes the simulation legible as a story: the character select screen, the triangle
|
||
> activation response, and the news ticker. All three must land before #593 (playthrough proof)
|
||
> can be filed as done. Sprint 25 is playtest. There is no Sprint 26 before v0.1 ships.
|
||
|
||
---
|
||
|
||
## New Tickets
|
||
|
||
| # | Title | Blocked by |
|
||
|---|-------|------------|
|
||
| #588 | Character archetype selection — client: character select screen before session start | #587 (server: archetype in StartupMessage) |
|
||
| #590 | Triangle activation consumer — client: react to triangle_crisis_events (monologue chime + urgent overlay) | #589 (server: tell escalation emitted) |
|
||
| #592 | News ticker — client: scrolling ticker HUD element in The Last Shift zone | #591 (server: ticker in snapshot) |
|
||
|
||
Use `tooling/db/ticket show <id>` for full details.
|
||
|
||
---
|
||
|
||
## Key Decisions
|
||
|
||
- `decisions/scope.md` — D-027 (vertical slice success criteria — the 4 tests this sprint's work must satisfy), D-039 (wow moments — #1 Arrival: opening monologue; #5 News Ticker Gut-Punch: same ticker, opposite monologue reactions)
|
||
- `decisions/architecture.md` — D-020 (ObserverSnapshot is the only data crossing IPC; StartupMessage is the client→server init message; PROTOCOL_VERSION gates wire compatibility), D-042 (UI microcopy in `client/data/ui-strings.yaml` via UIStrings autoload)
|
||
- `decisions/content.md` — D-032 (separate monologue pools per character — client does not select the pool; the character string in StartupMessage drives server-side selection), D-074 (audio aesthetic — monologue chime = insert-tech: synthetic, precise, no reverb)
|
||
- `decisions/perception.md` — D-067 (recognition chime fires at onset of cognitive delay — `sfx_monologue_chime_urgent.ogg` is the correct asset for triangle activation), D-016 (internal monologue as perception bridge — client only displays what server sends; no client-side monologue logic)
|
||
|
||
---
|
||
|
||
## Notes
|
||
|
||
### #588 — Character archetype select screen
|
||
|
||
**What exists:** `client/ui/main_menu.gd` (130 lines) — "New Game" button triggers `SessionManager.new_game()` which creates a save directory and seeds `GameState.world_seed`. It then loads `GAME_SCENE` directly, with no character selection step. `client/scripts/autoloads/session_manager.gd` — `new_game()` returns a `game_id` but does not record which archetype was chosen. `client/scripts/autoloads/game_state.gd` — check whether a `character_archetype` field already exists (likely not — add it). `CharacterArchetype` is a server-side enum; the client needs to record the chosen value as a string (`"smuggler"` or `"detective"`) and include it in `StartupMessage` sent over IPC.
|
||
|
||
**What to deliver:**
|
||
|
||
1. `GameState.character_archetype: String` — new field, default `"detective"`. Persisted alongside `world_seed` in the save directory (`user://saves/<game-id>/character.txt` or extend the existing seed file format).
|
||
|
||
2. Character select scene — insert a step between "New Game" and loading `main.tscn`. This can be a new scene (`client/scenes/character_select.tscn`) or a modal panel within `main_menu.tscn`. Show two options: **Smuggler** and **Detective**. Each option shows the character name, a one-line role description, and a two-line tone description (see below). On selection, set `GameState.character_archetype`, then proceed to `main.tscn`.
|
||
|
||
**Smuggler card:**
|
||
- Name: `Smuggler`
|
||
- Role: `Freight logistics worker — Sova Transit`
|
||
- Tone: `Insider access. Social camouflage. The ring is your daily life.`
|
||
|
||
**Detective card:**
|
||
- Name: `Detective`
|
||
- Role: `Commission investigator — External assignment`
|
||
- Tone: `Institutional authority. Analytical lattice. You were sent here.`
|
||
|
||
These strings belong in `client/data/ui-strings.yaml` (D-042), not hardcoded in GDScript.
|
||
|
||
3. `Protocol.encode_startup_message()` update — add `character_archetype` to the StartupMessage dict before it is serialized. The server's `StartupMessage` struct now has `pub character_archetype: CharacterArchetype` (#587). Map client string `"smuggler"` → server enum variant `Smuggler`. In MessagePack/GDScript, this is just a string field added to the dict: `{ "world_seed": ..., "character_archetype": "Smuggler" }`.
|
||
|
||
4. `Protocol.PROTOCOL_VERSION = 19` — bump to match server #587. The client must send the new version on handshake. This is a **hard coordination point** with Dudley — client and server PRs must land together or in the same merge window. A version mismatch will crash the connection on the handshake check.
|
||
|
||
**UI constraints:** The character select screen must feel intentional, not an afterthought. Two full-width cards, dark background, character name in the sprint's color palette (consistent with main menu). No portraits (art is deferred). Cards are selectable via keyboard (left/right arrows) and mouse click. The selection is confirmed with Enter or a "Begin" button. ESC cancels back to the main menu without creating a save directory.
|
||
|
||
**Non-obvious gotcha:** `SessionManager.new_game()` currently creates the save directory before any game scene loads. The character select step happens after `new_game()` creates the directory but before the game scene loads. `GameState.character_archetype` must be set before `SimBridge` sends the `StartupMessage` — which happens when `main.tscn` is ready and `SimBridge._ready()` connects to the server. Verify the ordering: `new_game()` → character select panel → user picks archetype → `GameState.character_archetype` set → `main.tscn` loads → `SimBridge._ready()` fires → `StartupMessage` includes archetype.
|
||
|
||
**Blocked by:** #587 (server must define `character_archetype` field in `StartupMessage` before client serialization is finalized).
|
||
|
||
---
|
||
|
||
### #590 — Triangle activation consumer
|
||
|
||
**What exists:** `client/scripts/snapshot_event_router.gd` — routes snapshot fields to registered handlers. `client/scripts/main.gd` — registers handlers on `_router`. `client/scripts/autoloads/sim_bridge.gd` — `_on_snapshot_received()` decodes and emits snapshot. `client/scripts/protocol/protocol.gd` — `decode_snapshot()` returns a dict from the MessagePack bytes. The server snapshot wire type (`ObserverSnapshotWire`) has a `triangle_crisis_events: Vec<TriangleCrisisEventWire>` field (see `server/src/bridge/types.rs` line ~181). This field is present in the MessagePack output. **The client currently ignores it entirely** — there is no decode path for `triangle_crisis_events` in `protocol.gd` and no handler registered in `main.gd`.
|
||
|
||
**What to deliver:**
|
||
|
||
1. **Decode `triangle_crisis_events`** in `protocol.gd` `decode_snapshot()`. The field is an array of dicts, each with at minimum `{ "triangle_id": int }`. Add it to the returned snapshot dict as `"triangle_crisis_events": Array`.
|
||
|
||
2. **Handle activation in `main.gd`** — register a handler that reads `triangle_crisis_events` from the snapshot. When the array is non-empty (at least one event), fire the urgent monologue chime: `AudioManager.play_one_shot(AudioManager.CHIME_RECOGNITION, AudioManager.BUS_UI_SOUNDS)` — wait, check the constant name. The correct asset is `sfx_monologue_chime_urgent.ogg` (D-038, D-067 "sharper variant for contradiction/anomaly"). `AudioManager` has `const CHIME_RECOGNITION := "sfx_monologue_chime"` — add `const CHIME_ACTIVATION := "sfx_monologue_chime_urgent"` if it doesn't exist, then call `AudioManager.play_one_shot(CHIME_ACTIVATION, BUS_UI_SOUNDS)`.
|
||
|
||
3. **Deduplication** — the triangle activation is a one-shot event (v0.1 fires once per session per D-072/D-089). The client must not fire the chime on every subsequent tick that includes the event in the array. Track activated triangle IDs in a local `Set` in `main.gd`. If `triangle_id` is already in the set, skip. Add to set on first encounter.
|
||
|
||
4. **No overlay UI** — the monologue chime is the client-side signal. The copy team (#597) authors the proximity monologue lines that fire when the player observes the activated NPC's `tell_state: RoutineDeviation`. The client does not need to render a special overlay or notification — the tell state on the entity and the subsequent proximity monologue are the visible consequence. Keep client reaction to: chime + deduplication tracking only.
|
||
|
||
**Why no overlay:** D-039 wow moment #2 ("The Character's Eye") is about the monologue noticing something the player didn't. Adding a UI overlay would make it a notification, not a character observation. The feel is: you're wandering near Kael, suddenly you hear the chime — then the next monologue line is your character's internal voice noticing something is off. The server sends the `tell_state: RoutineDeviation` on the NPC entity; the client's entity renderer already renders this as visible entity data that can trigger `observe_npc` monologue.
|
||
|
||
**Blocked by:** #589 (server must send non-empty `triangle_crisis_events` for client to handle).
|
||
|
||
---
|
||
|
||
### #592 — News ticker HUD
|
||
|
||
**What exists:** `client/ui/hud.gd` and `hud.tscn` — main HUD container. `client/ui/time_display.gd` — insert-style time display already wired via `_router.register_always(time_display.update_from_state)` in `main.gd`. No ticker node or script exists. Server snapshot will carry `current_ticker: Optional<{ id, text, category }>` when player is in the bar zone (#591).
|
||
|
||
**What to deliver:**
|
||
|
||
1. `client/ui/news_ticker.gd` + `news_ticker.tscn` — a horizontal scrolling text bar. Design: narrow strip (24–32px tall), anchored top of screen or bottom above the dialogue box, full width. Background: dark semi-transparent (`Color(0.05, 0.05, 0.07, 0.75)`). Text: scrolls left at a constant rate (~60px/sec). Text content: the `text` field from `current_ticker`. When `current_ticker` is `null` (player is not in bar zone), the ticker hides itself (`visible = false`).
|
||
|
||
2. Wire in `main.gd` — add `@onready var news_ticker = $UILayer/NewsTicker` and register: `_router.register_always(news_ticker.update_from_state)`. Implement `news_ticker.update_from_state(snapshot: Dictionary)`: read `snapshot.get("current_ticker")`, update text if changed, show/hide based on null.
|
||
|
||
3. **Insert overlay compatibility** — the ticker lives on `UILayer` (z-layer 7 per D-049). When the insert overlay is open (`GameState.insert_active = true`), the ticker should NOT be hidden — the news terminal is a real-world object the player can see while their insert is open. Do not call `set_insert_active` on the ticker.
|
||
|
||
4. **Scrolling behavior** — the headline scrolls in from the right and exits left. When it exits, it restarts from the right with the same text (the server rotates the headline every 200 ticks; client just loops whatever it currently has). No crossfade, no fade-in. Pure marquee.
|
||
|
||
**UI location:** Confirm with the sprint visual check that the ticker does not occlude the time display (top-right insert) or the monologue display (top-center). If there is a conflict, anchor the ticker at the bottom-center above the dialogue box, 4px margin above.
|
||
|
||
**Blocked by:** #591 (server must send `current_ticker` field in snapshot before client has real data to render; before that, the ticker renders nothing and stays hidden).
|
||
|
||
---
|
||
|
||
## Dependency Chain
|
||
|
||
```
|
||
#587 (server: archetype in StartupMessage)
|
||
└→ #588 (character select screen) ← start after #587 is merged
|
||
└→ PROTOCOL_VERSION 17→19 bump (coordinate with server)
|
||
|
||
#589 (server: tell escalation)
|
||
└→ #590 (triangle activation consumer) ← start after #589 is merged
|
||
|
||
#591 (server: ticker in snapshot)
|
||
└→ #592 (news ticker HUD) ← start after #591 is merged
|
||
|
||
#588 + #590 + #592 → #593 (playthrough proof — server ticket)
|
||
```
|
||
|
||
All three client tickets are blocked on their respective server tickets. Start with `protocol.gd` decode additions speculatively (no server data yet — verify against `server/src/bridge/types.rs` for field names), then wire the handlers once server branches are merged to main.
|
||
|
||
---
|
||
|
||
## PR Workflow
|
||
|
||
```bash
|
||
tea pr create --repo jpmschweitzer/settled-reach --login schweitz \
|
||
--title "feat(client): character archetype select screen" \
|
||
--description "body" --base main --head client
|
||
```
|
||
|
||
---
|
||
|
||
## Sprint Completion (Client Criteria)
|
||
|
||
1. From main menu, "New Game" → character select screen appears. Both cards render. Keyboard and mouse selection work. ESC cancels without creating a save directory.
|
||
2. Selected archetype is persisted in `GameState.character_archetype` and sent in `StartupMessage`. Server receives correct archetype (verify via debug console `status` — it should report the active archetype if Dudley adds it to the status response).
|
||
3. `PROTOCOL_VERSION = 19` — client matches server. Connection handshake succeeds.
|
||
4. When `triangle_crisis_events` is non-empty in snapshot, `sfx_monologue_chime_urgent.ogg` fires once. Does not re-fire on subsequent ticks.
|
||
5. News ticker visible and scrolling in The Last Shift zone. Hidden in all other zones. Text matches the server-sent headline.
|
||
6. `make test-client` green on client branch. No regressions in existing test suite.
|