Files
settled-reach/docs/sprints/sprint-24/client.md
T
jpmschweitzerandClaude Opus 4.6 bccdcfcdcb docs(docs): add frontmatter to all sprint briefings
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>
2026-03-14 00:15:45 +01:00

13 KiB
Raw Blame History

title, description, type, status, sprint, team
title description type status sprint team
Sprint 24 — Client Briefing Character select screen, triangle activation response, news ticker display sprint archived 24 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.gdnew_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.gddecode_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 (2432px 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

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.