diff --git a/docs/plans/NEO-122-implementation-plan.md b/docs/plans/NEO-122-implementation-plan.md new file mode 100644 index 0000000..f0c8f81 --- /dev/null +++ b/docs/plans/NEO-122-implementation-plan.md @@ -0,0 +1,187 @@ +# NEO-122 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-122 | +| **Title** | E7M1-11: Client quest progress + accept HUD (Godot) | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-122/e7m1-11-client-quest-progress-accept-hud-godot | +| **Module** | [E7.M1 — QuestStateMachine](../decomposition/modules/E7_M1_QuestStateMachine.md) · Epic 7 Slice 1 · backlog **E7M1-11** | +| **Branch** | `NEO-122-client-quest-progress-accept-hud` | +| **Server deps** | [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) — `GET /game/players/{id}/quest-progress` (**landed on `main`**); [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) — `POST …/quests/{questId}/accept` (**landed on `main`**); [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) — `GET /game/world/quest-definitions` (**landed on `main`**) | +| **Pattern** | [NEO-110](NEO-110-implementation-plan.md) — thin HTTP client, `main.gd` HUD render, event-driven refresh; [NEO-74](NEO-74-implementation-plan.md) — world-definitions client + POST mutation client; [NEO-72](NEO-72-implementation-plan.md) — display-name cache for HUD lines | +| **Blocks** | [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) — playable onboarding quest chain capstone (Godot manual QA) | +| **Server counterpart** | [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) + [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) — authoritative GET/POST; Bruno is **not** prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md) | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Quest definitions client** | Include optional `quest_definitions_client.gd`? | **Include** — NEO-115 GET landed; `recipe_definitions_client.gd` precedent; readable four-quest HUD. | **Adopted** — include | +| **Accept key bindings** | **Q** / **Shift+Q** semantics? | **Q** = explicit `prototype_quest_gather_intro`; **Shift+Q** = first catalog-order `not_started` quest with all prerequisites `completed` in current snapshot. | **Adopted** | +| **HUD quest list scope** | How many quests on **`QuestProgressLabel`**? | **All four** catalog quests, compact status per row (matches GET roster; supports chain visibility for NEO-123). | **Adopted** | + +**Additional defaults (no kickoff question — settled by backlog / precedent):** + +- **Refresh strategy:** boot hydrate + refresh after gather (interaction success → inventory finalize path), craft success, cast with **`targetDefeated`**, and successful accept POST — mirrors NEO-110 encounter + NEO-73 skill refresh; no periodic poll. +- **Error surfacing:** sync/accept failures paint visible HUD copy (`error — {reason}`) matching interaction/gather feedback pattern. +- **Accept POST body:** optional v1 `{ "schemaVersion": 1 }` (NEO-120); structured deny is HTTP **200** with **`accepted: false`**. +- **HUD placement:** **`UICanvas/HudRoot`** after **`EncounterCompleteLabel`**, before **`NpcStateLabel`** — quest spans gather/craft/combat loop (NEO-110 combat-area precedent extended). +- **Server changes:** none — client-only story. + +## Goal, scope, and out-of-scope + +**Goal:** Godot shows **server-owned** quest progress and accept feedback using **`GET …/quest-progress`** and **`POST …/quests/{questId}/accept`** — player-visible status after accept and after gather/craft/encounter objective events without Bruno. + +**In scope (from Linear + [E7M1-11](E7M1-prototype-backlog.md#e7m1-11--client-quest-progress-accept-hud-godot)):** + +- **`quest_progress_client.gd`:** GET snapshot parse v1; **`quest_row(quest_id, snapshot)`** helper; POST accept with optional v1 body; signals **`quest_progress_received`** / **`quest_sync_failed`** / **`quest_accept_result_received`** / **`quest_accept_failed`**; **`request_sync_from_server()`**, **`request_accept(quest_id)`**; **`push_warning`** on failures. +- **`quest_definitions_client.gd`:** GET **`/game/world/quest-definitions`**; cache ordered quest rows; **`display_name_for(quest_id)`**; signals **`definitions_ready`** / **`definitions_sync_failed`**. +- **`QuestProgressLabel`** — all four quests, compact status/step/counter summary. +- **`QuestAcceptFeedbackLabel`** — accept success/deny/failure copy. +- **`main.gd`:** boot hydrate (defs + progress); event-driven GET refresh; **Q** / **Shift+Q** accept bindings in **`_unhandled_key_input`**; eligible-quest picker for Shift+Q (catalog order + prerequisite check against snapshot). +- GdUnit tests with HTTP doubles ([testing-expectations.md](../../.cursor/rules/testing-expectations.md)). +- **`docs/manual-qa/NEO-122.md`** — Godot steps (server + client; no Bruno). +- **`client/README.md`** quest HUD subsection. + +**Out of scope (from Linear + backlog):** + +- Quest journal art, map pins, NPC dialog; final quest picker UI. +- Abandon/reset POST. +- Server route/DTO changes. +- Full four-quest capstone script (**[NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)**). +- Periodic quest poll timer. + +## Acceptance criteria checklist + +- [ ] Player sees quest status change after accept without Bruno. +- [ ] Objective progress updates after gather/craft/encounter in Godot session. +- [ ] Failed GET/accept surfaces visible HUD error (match interaction client pattern). +- [ ] `docs/manual-qa/NEO-122.md` Godot steps exercisable. + +## Technical approach + +### Server contract (landed — NEO-119 / NEO-120 / NEO-115) + +- **`GET /game/players/{id}/quest-progress`** → **`schemaVersion` 1**, **`playerId`**, **`quests[]`** with **`questId`**, **`status`** (`not_started` | `active` | `completed`), **`currentStepIndex`**, **`objectiveCounters`**, optional **`completedAt`**. +- **`POST /game/players/{id}/quests/{questId}/accept`** → **`schemaVersion` 1**, **`accepted`**, optional **`reasonCode`**, optional **`quest`** row (same shape as GET entry). HTTP **200** on structured deny. +- **`GET /game/world/quest-definitions`** → **`schemaVersion` 1**, **`quests[]`** with **`id`**, **`displayName`**, **`prerequisiteQuestIds`**, nested **`steps`**. + +### Client constants (`main.gd`) + +```gdscript +const PROTOTYPE_QUEST_GATHER_ID := "prototype_quest_gather_intro" +``` + +### 1. `quest_progress_client.gd` + +- Mirror **`encounter_progress_client.gd`** for GET: injectable HTTP, **`_sync_busy`** guard, 404 handling. +- Add POST accept on same node with separate **`_accept_busy`** guard (craft-client precedent — do not block GET refresh during accept round-trip). +- **`parse_quest_progress_json(text)`** static: require **`schemaVersion` 1**, **`quests`** array, non-empty **`playerId`**. +- **`parse_quest_accept_json(text)`** static: require **`schemaVersion` 1**, **`accepted`** bool present. +- **`quest_row(quest_id, snapshot)`** → row dict or `{}`. +- **`request_accept(quest_id)`** → POST with `{ "schemaVersion": 1 }`; returns **`true`** when queued. + +### 2. `quest_definitions_client.gd` + +- Mirror **`recipe_definitions_client.gd`**: GET **`/game/world/quest-definitions`**, cache **`quests`** array in catalog order. +- **`display_name_for(quest_id)`** — fallback raw id. +- **`quests_snapshot()`** → cached array copy for eligible-quest iteration in **`main.gd`**. + +### 3. Boot + refresh wiring (`main.gd`) + +- **`_setup_quest_progress_sync()`** from **`_ready()`** after authority HTTP config. +- Nodes in **`main.tscn`**: **`QuestProgressClient`**, **`QuestDefinitionsClient`**; **`QuestProgressLabel`**, **`QuestAcceptFeedbackLabel`** in **`HudRoot`** after **`EncounterCompleteLabel`**. +- Boot: **`request_sync_from_server()`** on both clients (parallel with encounter/gig boot). +- **`_request_quest_progress_refresh()`** helper; call from: + - **`_on_interaction_result_for_gather`** when interaction allowed (alongside skill progression refresh). + - **`_on_craft_result_received`** when **`success`**. + - **`_on_cast_result_received`** when **`targetDefeated`** (alongside encounter/gig refresh). + - **`_on_quest_accept_result_received`** after accept (always re-sync full snapshot even when response includes **`quest`** row). +- Re-render quest labels when **`definitions_ready`** if progress snapshot already loaded (NEO-110 item-defs re-paint precedent). + +### 4. Accept bindings (`main.gd`) + +| Key | Action | +|-----|--------| +| **Q** | **`request_accept(PROTOTYPE_QUEST_GATHER_ID)`** | +| **Shift+Q** | First quest in **`quest_definitions_client.quests_snapshot()`** catalog order where progress row **`status == "not_started"`** and every **`prerequisiteQuestIds`** entry has **`status == "completed"`** in **`_last_quest_progress_snapshot`**; if none eligible, paint feedback **`Quest accept: no eligible quest`**. | + +Debounce: skip if **`quest_progress_client`** accept busy (same as craft busy guard). + +### 5. HUD rendering + +**`QuestProgressLabel`** (header **`Quests:`**): + +| State | Copy (per quest, catalog order) | +|-------|----------------------------------| +| sync error | **`Quests: error — {reason}`** | +| loading | **`Quests: Loading…`** | +| **`not_started`** | **`{displayName}: not started`** | +| **`active`** | **`{displayName}: active step {index+1} {counter summary}`** — e.g. objective counter values joined | +| **`completed`** | **`{displayName}: completed`** (+ optional **`completedAt`** suffix) | + +**`QuestAcceptFeedbackLabel`** (header **`Quest accept:`**): + +| Outcome | Copy | +|---------|------| +| success | **`Quest accept: {displayName} accepted`** | +| deny | **`Quest accept: denied — {reasonCode}`** | +| HTTP/transport fail | **`Quest accept: failed — {reason}`** | +| idle | **`Quest accept: — (Q gather / Shift+Q next)`** | + +### 6. Scene + +- **`main.tscn`:** add client nodes under root; add two labels in **`HudRoot`** VBox **after** **`EncounterCompleteLabel`**, **before** **`NpcStateLabel`**. + +### 7. Tests (GdUnit, AAA) + +- Progress client: parse active/completed rows; **`quest_row`** lookup; GET 404 → **`quest_sync_failed`**; accept happy **`accepted: true`**; accept deny **`accepted: false`** + **`reasonCode`**. +- Definitions client: parse four-quest JSON; **`display_name_for`** returns **`displayName`**. + +### 8. Docs on land + +- **`docs/manual-qa/NEO-122.md`**: fresh server; Godot **F5**; **Q** accept gather intro → HUD shows active; gather/craft/defeat updates counters; **Shift+Q** accept refine after gather complete; deny feedback visible. +- Update [E7M1-prototype-backlog.md](E7M1-prototype-backlog.md) E7M1-11 checkboxes, [E7_M1](../decomposition/modules/E7_M1_QuestStateMachine.md) client slice note, [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) when implementation completes. + +## Files to add + +| Path | Purpose | +|------|---------| +| `docs/plans/NEO-122-implementation-plan.md` | This plan. | +| `client/scripts/quest_progress_client.gd` | GET quest-progress + POST accept; parse v1; **`quest_row`**; signals. | +| `client/scripts/quest_progress_client.gd.uid` | Godot uid companion (tracked). | +| `client/scripts/quest_definitions_client.gd` | GET world quest-definitions; display-name cache. | +| `client/scripts/quest_definitions_client.gd.uid` | Godot uid companion (tracked). | +| `client/test/quest_progress_client_test.gd` | GdUnit: GET parse, accept happy/deny, 404 failure. | +| `client/test/quest_progress_client_test.gd.uid` | GdUnit uid companion. | +| `client/test/quest_definitions_client_test.gd` | GdUnit: parse defs JSON; **`display_name_for`**. | +| `client/test/quest_definitions_client_test.gd.uid` | GdUnit uid companion. | +| `docs/manual-qa/NEO-122.md` | Godot manual QA (accept + progress refresh; no Bruno). | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `client/scenes/main.tscn` | Add **`QuestProgressClient`**, **`QuestDefinitionsClient`**, **`QuestProgressLabel`**, **`QuestAcceptFeedbackLabel`**. | +| `client/scripts/main.gd` | Boot/refresh wiring, Q/Shift+Q accept, HUD render, eligible-quest helper. | +| `client/README.md` | Quest progress + accept HUD subsection: server deps NEO-119/120/115, keys, refresh triggers. | + +## Tests + +| Test file | What it covers | +|-----------|----------------| +| `client/test/quest_progress_client_test.gd` | Mock GET 200: **`not_started`** / **`active`** with **`objectiveCounters`** / **`completed`** rows; **`quest_row`**; GET 404 → **`quest_sync_failed`**; POST accept **`accepted: true`** and **`accepted: false`** + **`reasonCode`**. AAA layout (`# Arrange` / `# Act` / `# Assert`). | +| `client/test/quest_definitions_client_test.gd` | Parse v1 defs JSON (four quests); **`display_name_for`** returns catalog **`displayName`**; unknown id fallback. AAA layout. | + +No new **C#** tests (client-only; server covered by **`QuestProgressApiTests`** / **`QuestAcceptApiTests`**). No Bruno changes. + +## Open questions / risks + +| Item | Agent recommendation | Status | +|------|----------------------|--------| +| **Concurrent GET + accept** | Separate **`_sync_busy`** / **`_accept_busy`** guards; overlapping GET drops (encounter precedent); accept not blocked by progress poll. | **adopted** | +| **Quest defs arrive after progress snapshot** | Re-render **`QuestProgressLabel`** on **`definitions_ready`** when snapshot non-empty (NEO-110 item-defs re-paint). | **adopted** | +| **Client-side eligible-quest guess vs server deny** | Shift+Q uses snapshot prerequisite check for UX only; server **`TryAccept`** remains authoritative on deny. | **adopted** | +| **NEO-123 capstone overlap** | NEO-122 proves HUD + accept + single-objective refresh; NEO-123 is full four-quest chain manual QA — keep scopes separate. | **deferred** |