neon-sprawl/docs/plans/NEO-122-implementation-plan.md

16 KiB
Raw Permalink Blame History

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-119GET /game/players/{id}/quest-progress (landed on main); NEO-120POST …/quests/{questId}/accept (landed on main); NEO-115GET /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 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):

  • 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).
  • 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).
  • 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 (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 (postfirst review) refactored prototype HUD layout without changing quest contracts:

  • HudRootScrollScrollContainer 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, NEO-75, NEO-110 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-progressschemaVersion 1, playerId, quests[] with questId, status (not_started | active | completed), currentStepIndex, objectiveCounters, optional completedAt.
  • POST /game/players/{id}/quests/{questId}/acceptschemaVersion 1, accepted, optional reasonCode, optional quest row (same shape as GET entry). HTTP 200 on structured deny.
  • GET /game/world/quest-definitionsschemaVersion 1, quests[] with id, displayName, prerequisiteQuestIds, nested steps.

Client constants (main.gd)

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

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