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

9.0 KiB

title, description, type, status, sprint, team
title description type status sprint team
Sprint 20 — Client Briefing Refactor game_state, dialogue_box, main.gd coordinator, unify YAML parsers sprint archived 20 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:

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