14 KiB
14 KiB
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 · Epic 7 Slice 1 · backlog E7M1-11 |
| Branch | NEO-122-client-quest-progress-accept-hud |
| Server deps | NEO-119 — GET /game/players/{id}/quest-progress (landed on main); NEO-120 — POST …/quests/{questId}/accept (landed on main); NEO-115 — GET /game/world/quest-definitions (landed on main) |
| Pattern | NEO-110 — thin HTTP client, main.gd HUD render, event-driven refresh; NEO-74 — world-definitions client + POST mutation client; NEO-72 — display-name cache for HUD lines |
| Blocks | NEO-123 — playable onboarding quest chain capstone (Godot manual QA) |
| Server counterpart | NEO-119 + NEO-120 — authoritative GET/POST; Bruno is not prototype-complete per full-stack epic decomposition |
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 withaccepted: false. - HUD placement:
UICanvas/HudRootafterEncounterCompleteLabel, beforeNpcStateLabel— 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):
quest_progress_client.gd: GET snapshot parse v1;quest_row(quest_id, snapshot)helper; POST accept with optional v1 body; signalsquest_progress_received/quest_sync_failed/quest_accept_result_received/quest_accept_failed;request_sync_from_server(),request_accept(quest_id);push_warningon failures.quest_definitions_client.gd: GET/game/world/quest-definitions; cache ordered quest rows;display_name_for(quest_id); signalsdefinitions_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).
docs/manual-qa/NEO-122.md— Godot steps (server + client; no Bruno).client/README.mdquest 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).
- 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.mdGodot 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,QuestAcceptFeedbackLabelafter 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.mdquest 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→schemaVersion1,playerId,quests[]withquestId,status(not_started|active|completed),currentStepIndex,objectiveCounters, optionalcompletedAt.POST /game/players/{id}/quests/{questId}/accept→schemaVersion1,accepted, optionalreasonCode, optionalquestrow (same shape as GET entry). HTTP 200 on structured deny.GET /game/world/quest-definitions→schemaVersion1,quests[]withid,displayName,prerequisiteQuestIds, nestedsteps.
Client constants (main.gd)
const PROTOTYPE_QUEST_GATHER_ID := "prototype_quest_gather_intro"
1. quest_progress_client.gd
- Mirror
encounter_progress_client.gdfor GET: injectable HTTP,_sync_busyguard, 404 handling. - Add POST accept on same node with separate
_accept_busyguard (craft-client precedent — do not block GET refresh during accept round-trip). parse_quest_progress_json(text)static: requireschemaVersion1,questsarray, non-emptyplayerId.parse_quest_accept_json(text)static: requireschemaVersion1,acceptedbool present.quest_row(quest_id, snapshot)→ row dict or{}.request_accept(quest_id)→ POST with{ "schemaVersion": 1 }; returnstruewhen queued.
2. quest_definitions_client.gd
- Mirror
recipe_definitions_client.gd: GET/game/world/quest-definitions, cachequestsarray in catalog order. display_name_for(quest_id)— fallback raw id.quests_snapshot()→ cached array copy for eligible-quest iteration inmain.gd.
3. Boot + refresh wiring (main.gd)
_setup_quest_progress_sync()from_ready()after authority HTTP config.- Nodes in
main.tscn:QuestProgressClient,QuestDefinitionsClient;QuestProgressLabel,QuestAcceptFeedbackLabelinHudRootafterEncounterCompleteLabel. - Boot:
request_sync_from_server()on both clients (parallel with encounter/gig boot). _request_quest_progress_refresh()helper; call from:_on_interaction_result_for_gatherwhen interaction allowed (alongside skill progression refresh)._on_craft_result_receivedwhensuccess._on_cast_result_receivedwhentargetDefeated(alongside encounter/gig refresh)._on_quest_accept_result_receivedafter accept (always re-sync full snapshot even when response includesquestrow).
- Re-render quest labels when
definitions_readyif 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 inHudRootVBox afterEncounterCompleteLabel, beforeNpcStateLabel.
7. Tests (GdUnit, AAA)
- Progress client: parse active/completed rows;
quest_rowlookup; GET 404 →quest_sync_failed; accept happyaccepted: true; accept denyaccepted: false+reasonCode. - Definitions client: parse four-quest JSON;
display_name_forreturnsdisplayName.
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-11 checkboxes, E7_M1 client slice note, 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 |