Files
settled-reach/docs/sprints/sprint-20/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

125 lines
9.0 KiB
Markdown

---
title: "Sprint 20 — Client Briefing"
description: "Refactor game_state, dialogue_box, main.gd coordinator, unify YAML parsers"
type: sprint
status: archived
sprint: 20
team: "client"
---
# Sprint 20: Shape — Client Tasks
**Goal:** The social site template system gains its foundational schema; triangles become generatable and observable as escalating tensions; the client gains save/load UI and code quality improvements.
**Branch:** `client`
**Agents:** Stig (UI/rendering), Tyre (architecture), Hoshe (QA)
## Carry-over from Sprint 19
None — Sprint 19 complete. #554 (save/load client UI) is finishing in Sprint 19.
## New Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #557 | Refactor: game_state.gd derived state in apply_snapshot() | — |
| #558 | Refactor: dialogue_box.gd direct GameState mutation and AudioManager coupling | — |
| #559 | Refactor: main.gd god coordinator — extract SnapshotEventRouter | — |
| #560 | Refactor: unify duplicate YAML parsers | — |
Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/architecture.md` — D-020 (Godot is a pure renderer: no game logic in GDScript; GameState reflects server-authoritative data, not derived behavior), D-085 (per-game save directory structure: `user://saves/<timestamp>-<seed>/`, F5=quicksave, F6=quickload, loading screen lists dirs by last-modified), D-088 (3-state pause system: client sends pause requests, server is authoritative)
- `decisions/scope.md` — D-027 (vertical slice success criteria: game session must be resumable for 30-min playthroughs)
## Notes
### #557 — Refactor: game_state.gd derived state in apply_snapshot()
Code review finding: `apply_snapshot()` in `client/scripts/autoloads/game_state.gd` computes two derived values inline:
- `stationary_ticks` (increments when player position hasn't changed) — line 99 / ~line 131-133
- `current_zone_id` (derived from tile iteration) — line 105 / ~line 286
Per D-020, the Godot client is a pure renderer. Behavior-driving computations (stationary tick counting, zone identification) belong in the server, not in `apply_snapshot()`. The server already sends `zone_id` per tile — the client should read it directly rather than re-deriving it.
What this ticket must deliver:
- Move `stationary_ticks` accumulation out of `apply_snapshot()`. The server sends `stationary_ticks` (or equivalent) in the snapshot — if not yet present, add the field to `ObserverSnapshot` in `client/scripts/protocol/protocol.gd` and mark with a TODO for the server team to populate it. Client reads the server value directly.
- Move `current_zone_id` resolution to a simple property read from the snapshot (`player_tile.zone_id`), removing the tile iteration loop from `apply_snapshot()`.
- After: `apply_snapshot()` contains only direct field assignments from the snapshot dictionary — no conditional logic, no accumulation.
- Add a comment citing D-020 on each removed computation to document the rationale.
- Unit tests: `apply_snapshot()` with a snapshot missing the new fields should degrade gracefully (default values, no crash).
Integration points: `client/scripts/autoloads/game_state.gd` only. Protocol fields may need a minor extension in `client/scripts/protocol/protocol.gd` — coordinate with server team if new snapshot fields are required.
Gotcha: `stationary_ticks` drives `ListeningFocus` (D-071) — confirm the server already tracks and sends this value before removing client-side accumulation. If the server does not yet send it, add a feature-flagged fallback that keeps the old behavior with a deprecation comment.
### #558 — Refactor: dialogue_box.gd direct GameState mutation and AudioManager coupling
Code review finding: `client/ui/dialogue_box.gd` directly mutates `GameState.dialogue_active` at 3 call sites (lines ~289, ~321, ~334) and calls `AudioManager.apply_dip()` / `AudioManager.clear_dip()` directly.
Per D-020, UI components should not mutate shared state or call sibling autoloads directly — they should emit signals and let a coordinator (main.gd or a future SnapshotEventRouter) manage cross-component state.
What this ticket must deliver:
- Replace the 3 `GameState.dialogue_active = true/false` assignments with a signal: `signal dialogue_state_changed(active: bool)`. `main.gd` connects to this signal and updates `GameState.dialogue_active`.
- Replace `AudioManager.apply_dip("dialogue")` and `AudioManager.apply_dip("confrontation")` / `AudioManager.clear_dip()` calls with signals: `signal audio_dip_requested(profile: String)` and `signal audio_dip_cleared()`. `main.gd` connects to these and calls `AudioManager`.
- Result: `dialogue_box.gd` has zero references to `GameState` or `AudioManager`.
- Unit tests: mock signal receivers capture the emitted signals with correct arguments; no direct autoload calls remain.
Integration points: `client/ui/dialogue_box.gd` (source), `client/scripts/main.gd` (connects to new signals in `_ready()`). No server changes.
Gotcha: `InputMapper` checks `GameState.dialogue_active` to suppress movement. The signal path adds one frame of latency — verify that the signal fires synchronously within the same frame (use `call_immediate` or connect with `CONNECT_DEFERRED` depending on timing requirements). The `is_dialogue_active()` method on `dialogue_box.gd` (line 337) can remain as a local query without touching `GameState`.
### #559 — Refactor: main.gd god coordinator — extract SnapshotEventRouter
Code review finding: `client/scripts/main.gd` is 517 lines and dispatches to 15+ child nodes through a set of `consume_*` methods that all follow the same pattern: read field from snapshot, call method on child node.
What this ticket must deliver:
- Extract a `SnapshotEventRouter` class (`client/scripts/snapshot_event_router.gd`): takes the snapshot dictionary and routes each field to the correct child node via a registered handler map.
- Registration pattern: `router.register("monologue", monologue_display.consume_monologue)` — callable-based dispatch. Handlers are registered in `main.gd`'s `_ready()`.
- `main.gd` `_process()` calls `router.dispatch(snapshot)` instead of 15+ individual `if snapshot.has("X"): child.consume_X()` blocks.
- `main.gd` retains scene tree ownership (`@onready` node references), camera logic, and input handling — the router only handles snapshot dispatch.
- After: `main.gd` should be under 350 lines.
- Unit tests: construct a `SnapshotEventRouter` with mock handlers, dispatch a snapshot, assert each handler received the correct field value.
Integration points: `client/scripts/main.gd` (refactor target), new file `client/scripts/snapshot_event_router.gd`. No server changes, no protocol changes.
Gotcha: Some consume methods in `main.gd` have cross-field dependencies (e.g., camera position depends on both `player_position` and `_camera_anchored` state). Identify these upfront and keep them in `main.gd` directly — only pure per-field dispatch moves to the router. Do not force all logic into the router pattern.
### #560 — Refactor: unify duplicate YAML parsers
Code review finding: `client/scripts/checklist/checklist_evaluator.gd` contains its own YAML parser that partially duplicates `client/scripts/autoloads/ui_strings.gd`'s `_parse_yaml()` method.
What this ticket must deliver:
- Extract a shared `YamlParser` utility class at `client/scripts/util/yaml_parser.gd` (create the `util/` directory).
- `YamlParser` exposes a static method `parse(text: String) -> Dictionary` that handles the common subset of YAML used across both call sites (key: value pairs, nested maps, arrays).
- Replace `checklist_evaluator.gd`'s inline parser with `YamlParser.parse()`.
- Replace `ui_strings.gd`'s `_parse_yaml()` with `YamlParser.parse()` (or delegate to it, keeping the method signature stable).
- Unit tests: parse a sample YAML string with nested keys, arrays, and string values; assert round-trip correctness.
Integration points: `client/scripts/checklist/checklist_evaluator.gd`, `client/scripts/autoloads/ui_strings.gd`, new `client/scripts/util/yaml_parser.gd`. No server changes.
Gotcha: The two existing parsers may handle edge cases differently. Write the unit tests first against both parsers to document their current behavior, then unify. Prioritize correctness for existing content files (`client/data/ui-strings.yaml` and any checklist YAML files) — do not break live content.
## Dependency Chain
```
#557 (game_state derived state) ─┐
#558 (dialogue_box coupling) ├─ all parallel, no inter-dependency
#559 (main.gd SnapshotEventRouter)│ #558 feeds into #559 (signal wiring in main.gd)
#560 (unify YAML parsers) ─┘
```
#558 should complete before #559 so that the new signals from dialogue_box are wired into `main.gd` as part of the router work, not as a separate pass. Otherwise all four tickets run in parallel.
## PR Workflow
When ready to submit, create a PR with the `tea` CLI. All flags are required to avoid TTY prompts:
```bash
tea pr create --repo jpmschweitzer/settled-reach --login schweitz \
--title "feat(client): save/load UI and code quality refactors" \
--description "body" --base main --head client
```