# 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 - [x] Player sees quest status change after accept without Bruno. - [x] Objective progress updates after gather/craft/encounter in Godot session. - [x] Failed GET/accept surfaces visible HUD error (match interaction client pattern). - [x] `docs/manual-qa/NEO-122.md` Godot steps exercisable (human QA passed 2026-06-07). ## Implementation reconciliation (shipped) - **`quest_progress_client.gd`:** GET parse v1 + POST accept; separate sync/accept HTTP; **`quest_row`**; signals. - **`quest_definitions_client.gd`:** GET world quest-definitions; **`display_name_for`** + **`quests_snapshot()`**. - **`main.gd`:** boot hydrate; Q / Shift+Q accept; refresh after gather/craft/**`targetDefeated`**/**encounter `completed`**/accept; HUD render. - **Scene:** **`QuestProgressClient`**, **`QuestDefinitionsClient`**, **`QuestProgressLabel`**, **`QuestAcceptFeedbackLabel`** after encounter labels. - **Tests:** `client/test/quest_progress_client_test.gd` (12 cases across both suites), `client/test/quest_definitions_client_test.gd`. - **Docs:** `client/README.md` quest HUD section; `docs/manual-qa/NEO-122.md`. ### NEO-122 follow-up (economy HUD layout — manual QA step 7) During NEO-122 QA, **`refine_scrap_standard`** was unreachable when the economy block sat below the viewport inside the tall **`HudRoot`** stack. Follow-up commits (post–first review) refactored prototype HUD layout **without changing quest contracts**: - **`HudRootScroll`** — **`ScrollContainer`** wrapping debug/combat/quest labels; height clamped above the economy panel. - **`EconomyHudSection`** — reparented to **`UICanvas`**; pinned bottom-left via **`prototype_economy_hud_section.gd`** **`_apply_viewport_layout`** (explicit **`position`/`size`**, opaque panel fill, **`z_index`**). - **`BodyScroll/Body`** — single economy scroll; child order **Recipes → Inventory → Skills → Gig XP** so craft rows appear first. - **`craft_recipe_panel.gd`** — flattened to **`VBoxContainer`** + **`RecipeRows`** (no nested scroll); populate resets economy body scroll to recipes via parent **`scroll_body_to_recipes()`**. - **Tests:** `client/test/craft_recipe_panel_test.gd`; viewport layout tests in **`prototype_economy_hud_section_test.gd`**. - **Regression:** spot-check [NEO-74](../manual-qa/NEO-74.md), [NEO-75](../manual-qa/NEO-75.md), [NEO-110](../manual-qa/NEO-110.md) after merge (collapsed economy toggle, feedback labels in **`HudRootScroll`**, encounter labels scroll when stack is tall). ## 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_encounter_progress_received`** when prototype pocket row **`state == "completed"`** (alongside inventory refresh — belt-and-suspenders when defeat-triggered quest GET was skipped or raced). - **`_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. | | `client/test/craft_recipe_panel_test.gd` | Recipe row populate; economy body scroll reset on repopulate; **`craft_requested`** signal. AAA layout. | | `client/test/prototype_economy_hud_section_test.gd` | Economy collapse toggle; viewport pin + **`HudRootScroll`** clamp (NEO-122 follow-up). 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** |