197 lines
14 KiB
Markdown
197 lines
14 KiB
Markdown
# 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).
|
|
- [ ] `docs/manual-qa/NEO-122.md` Godot steps exercisable (human QA pending).
|
|
|
|
## 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`**/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`.
|
|
|
|
## 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** |
|