Server (6 tickets): template role/spatial/ownership schemas, triangle definition, generation, and escalation. Client (4 tickets): code quality refactors from review pass. Planning (1 ticket): #153 station district layout design discussion with purpose-assembled agent panel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
8.9 KiB
Markdown
116 lines
8.9 KiB
Markdown
# 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
|
|
```
|