From 9d756c259b5fb43e35f71a097e5c9c07409ddeef Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 28 Jun 2026 13:17:20 -0400 Subject: [PATCH] NEO-153: kickoff implementation plan for contract HUD client slice. Documents E7M4-10 scope, adopted kickoff decisions (separate contract_hud_controller, fixed prototype_contract_dev_seed), file lists, and test plan ahead of Godot work. --- docs/plans/NEO-153-implementation-plan.md | 217 ++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 docs/plans/NEO-153-implementation-plan.md diff --git a/docs/plans/NEO-153-implementation-plan.md b/docs/plans/NEO-153-implementation-plan.md new file mode 100644 index 0000000..7e4f666 --- /dev/null +++ b/docs/plans/NEO-153-implementation-plan.md @@ -0,0 +1,217 @@ +# NEO-153 — E7M4-10: Client contract issue + progress HUD (Godot) + +**Linear:** [NEO-153](https://linear.app/neon-sprawl/issue/NEO-153) +**Branch:** `NEO-153-e7m4-10-client-contract-issue-progress-hud-godot` +**Backlog:** [E7M4-pre-production-backlog.md](E7M4-pre-production-backlog.md) — **E7M4-10** +**Module:** [E7_M4_ContractMissionGenerator.md](../decomposition/modules/E7_M4_ContractMissionGenerator.md) +**Pattern:** [NEO-122](NEO-122-implementation-plan.md) (`quest_progress_client.gd` + `quest_hud_controller.gd`); [NEO-110](NEO-110-implementation-plan.md) (encounter-complete refresh spine); [NEO-131](NEO-131-implementation-plan.md) (completion reward transition label) +**Precursors:** [NEO-151](https://linear.app/neon-sprawl/issue/NEO-151) **`Done`** — `POST …/contracts/issue` + `GET …/contracts` (**on `main`**); [NEO-152](https://linear.app/neon-sprawl/issue/NEO-152) **`Done`** — contract telemetry hook sites (**on `main`**) +**Blocks:** [NEO-154](https://linear.app/neon-sprawl/issue/NEO-154) (Godot repeatable contract capstone) +**Server counterpart:** [NEO-151](https://linear.app/neon-sprawl/issue/NEO-151) — authoritative issue/list HTTP; Bruno is **not** prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md) + +## Goal + +Player can **issue** and **track** an active contract in Godot without Bruno — **`Shift+C`** issues the frozen prototype template; HUD shows **`encounterTemplateId`** while active and a **reward summary line** when the contract completes in-session. + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|-------|----------|---------------------|--------| +| **HUD wiring** | Separate controller + labels vs extend quest HUD? | **Separate `contract_hud_controller.gd`** + **`ContractActiveLabel`**, **`ContractIssueFeedbackLabel`**, **`ContractRewardDeliveryLabel`** — keeps quest HUD bounded; matches [godot-client-script-organization](../../.cursor/rules/godot-client-script-organization.md) + NEO-122 extraction | **Adopted** — separate controller | +| **Shift+C seed bucket** | Fixed vs date vs session counter? | **Fixed `prototype_contract_dev_seed`** — NEO-153 single-loop AC; repeat re-issue seed rotation deferred to **NEO-154** | **Adopted** — fixed dev seed | + +**Additional defaults (no kickoff question — settled by backlog / precedent):** + +- **Issue key:** **Shift+C** (backlog E7M4-10); idle hint on **`ContractIssueFeedbackLabel`**. +- **Prototype POST fields:** **`templateId`** = **`prototype_contract_clear_combat_pocket`**; **`seedBucket`** = **`prototype_contract_dev_seed`**; **`schemaVersion`** **1**; body **`playerId`** matches path **`dev_player_id`**. +- **Refresh strategy:** boot hydrate + after successful issue POST + after encounter-progress GET when prototype encounter row reaches **`completed`** (extend **`_on_encounter_progress_received`** contract refresh — same spine as quest refresh today). +- **Completion reward label:** in-session **active → completed** transition only (NEO-131 idempotency — boot with prior completed row does not replay grant copy). +- **Deny copy:** readable HUD strings for backlog-listed codes plus stable generator codes surfaced on issue deny. +- **Server changes:** none — client-only story. + +## Scope and out-of-scope + +**In scope (from Linear + backlog):** + +- **`contract_client.gd`:** GET **`/game/players/{id}/contracts`** parse v1; POST **`/game/players/{id}/contracts/issue`**; helpers **`active_contract_row(snapshot)`**, **`completion_reward_summary(contract_instance_id, snapshot)`**; signals **`contracts_received`** / **`contracts_sync_failed`** / **`contract_issue_result_received`** / **`contract_issue_failed`**; separate sync/issue HTTP busy guards (quest/craft precedent). +- **`contract_hud_controller.gd`:** HUD render, **Shift+C** issue binding, deny copy, active-row display with **`encounterTemplateId`**, reward transition detection + summary lines (duplicate minimal grant formatters from quest HUD — item defs + faction standing clients injected). +- **`main.tscn`:** **`ContractClient`** node; three HUD labels in **`HudRootScroll/HudRoot`** after **`QuestRewardDeliveryLabel`**, before **`FactionStandingLabel`**. +- **`main.gd`:** thin setup — **`ContractHudController`**, authority HTTP config, encounter-complete contract refresh hook, **`_try_contract_issue_key_input`** in gameplay key router. +- GdUnit parse + controller tests with HTTP doubles. +- **`docs/manual-qa/NEO-153.md`** — Godot issue → clear pocket → reward line (no Bruno). +- **`client/README.md`** contract HUD subsection. + +**Out of scope (from Linear + backlog):** + +- Final contract board UI; zone picker (**E4.M1**). +- Repeat re-issue seed rotation (**NEO-154** capstone). +- Server route/DTO/Bruno changes. +- Telemetry ingest (**NEO-152** landed comment-only). + +**Client counterpart:** this story **is** the E7M4-10 client slice; pairs **NEO-151** server HTTP. + +## Acceptance criteria checklist + +- [ ] Active contract visible with encounter objective id after issue. +- [ ] Completed contract shows reward summary line on HUD. + +## Technical approach + +### Server contract (landed — NEO-151) + +- **`POST /game/players/{id}/contracts/issue`** → **`schemaVersion` 1**, **`issued`**, optional **`reasonCode`**, optional **`contract`** row. HTTP **200** on structured deny (quest accept precedent). +- **`GET /game/players/{id}/contracts`** → **`schemaVersion` 1**, **`playerId`**, **`contracts[]`**: active row (0–1) + up to 10 completed. Row fields: **`contractInstanceId`**, **`templateId`**, **`templateDisplayName`**, **`status`** (`active` \| `completed`), **`encounterTemplateId`**, **`seedBucket`**, **`issuedAt`**, optional **`completedAt`**, optional **`completionRewardSummary`** (same nested grant shape as quest NEO-129). +- Prototype template (**E7M4-01 freeze**): **`prototype_contract_clear_combat_pocket`** → **`encounterTemplateId`** **`prototype_combat_pocket`**; completion bundle **`scrap_metal_bulk` ×5**, **`salvage` +15 XP**. + +### Client constants (`contract_hud_controller.gd` or shared const block) + +```gdscript +const PROTOTYPE_CONTRACT_TEMPLATE_ID := "prototype_contract_clear_combat_pocket" +const PROTOTYPE_CONTRACT_SEED_BUCKET := "prototype_contract_dev_seed" +const ISSUE_IDLE_HINT := "Contract issue: — (Shift+C prototype)" +``` + +### 1. `contract_client.gd` + +Mirror **`quest_progress_client.gd`** structure: + +| Method / signal | Role | +|-----------------|------| +| **`request_sync_from_server()`** | GET contracts list | +| **`request_issue(template_id, seed_bucket)`** | POST issue with v1 body | +| **`parse_contracts_json(text)`** | Validate **`schemaVersion` 1**, **`playerId`**, **`contracts`** array | +| **`parse_contract_issue_json(text)`** | Validate **`issued`** bool present | +| **`active_contract_row(snapshot)`** | First row with **`status == "active"`** | +| **`completion_reward_summary(instance_id, snapshot)`** | Read **`completionRewardSummary`** from matching completed row | +| **`contracts_received`** / **`contracts_sync_failed`** | GET outcomes | +| **`contract_issue_result_received`** / **`contract_issue_failed`** | POST outcomes | + +POST body: + +```json +{ + "schemaVersion": 1, + "playerId": "", + "templateId": "prototype_contract_clear_combat_pocket", + "seedBucket": "prototype_contract_dev_seed" +} +``` + +On issue deny (**`issued: false`**), emit result with **`reasonCode`**; **`push_warning`** with code (quest accept pattern). + +### 2. `contract_hud_controller.gd` + +Extracted controller (NEO-122 precedent): + +- **`setup(contract_client, active_label, issue_label, reward_label, apply_http_config, item_defs_client, faction_standing_client)`** +- Boot: apply HTTP config → connect signals → **`request_sync_from_server()`** +- **`try_issue_key_input(event)`** — **Shift+C** only; calls **`request_issue(PROTOTYPE_CONTRACT_TEMPLATE_ID, PROTOTYPE_CONTRACT_SEED_BUCKET)`** +- **`request_contract_refresh()`** — delegate GET sync +- **`ContractActiveLabel`** render: + - sync error → **`Contract: error — {reason}`** + - no active/completed rows → **`Contract: —`** + - **`active`** → **`{templateDisplayName}: active — objective {encounterTemplateId}`** + - latest in-session completed (status tracking) → **`{templateDisplayName}: completed — objective {encounterTemplateId}`** +- **`ContractIssueFeedbackLabel`** — idle hint / sending / issued success / deny copy map: + +| **`reasonCode`** | HUD copy (prototype) | +|------------------|------------------------| +| **`active_contract_exists`** | **`Contract issue: denied — active contract already issued`** | +| **`no_eligible_template`** | **`Contract issue: denied — no eligible template (standing/band)`** | +| **`economy_cap_exceeded`** | **`Contract issue: denied — economy cap exceeded`** | +| **`invalid_reward_bundle`** | **`Contract issue: denied — invalid reward bundle`** | +| **`unknown_template`** | **`Contract issue: denied — unknown template`** | +| *(other / empty)* | **`Contract issue: denied — {reasonCode}`** | + +- **`ContractRewardDeliveryLabel`** — header **`Contract rewards:`**; on in-session **`active` → `completed`** transition for tracked instance id, format **`completionRewardSummary`** grant lines (item qty, skill XP, rep — same line grammar as **`quest_hud_controller._format_reward_summary_lines`**); otherwise **`—`**. + +**Transition tracking:** maintain **`_previous_status_by_instance`** + **`_last_reward_instance_id`** / **`_last_reward_summary`** keyed by **`contractInstanceId`** (parallel NEO-131 quest maps). + +### 3. `main.gd` wiring + +- Add **`ContractClient`** `@onready` reference (scene node). +- **`_setup_contract_hud()`** — instantiate **`ContractHudController`**, pass labels + **`_apply_authority_http_config_to_client`** + item/faction defs clients for grant display names. +- **`_try_contract_issue_key_input(event)`** — after quest accept router; delegate to controller. +- **`_request_contract_refresh()`** — called from controller hook + encounter complete path. +- In **`_on_encounter_progress_received`**, when prototype encounter **`state == "completed"`**, call **`_request_contract_refresh()`** (alongside existing inventory + quest refresh). + +### 4. Scene labels (`main.tscn`) + +Insert after **`QuestRewardDeliveryLabel`**: + +| Node | Default text | Color cue | +|------|--------------|-----------| +| **`ContractActiveLabel`** | **`Contract:\n—`** | warm amber (distinct from quest blue) | +| **`ContractIssueFeedbackLabel`** | idle hint | lavender (match quest accept) | +| **`ContractRewardDeliveryLabel`** | **`Contract rewards:\n—`** | mint (match quest rewards) | + +Add **`ContractClient`** sibling under scene root (with **`EncounterProgressClient`** / **`QuestProgressClient`**). + +### 5. Flow + +```mermaid +sequenceDiagram + participant Player + participant HUD as ContractHudController + participant Client as contract_client.gd + participant Server + participant Enc as encounter_progress_client + + Player->>HUD: Shift+C + HUD->>Client: request_issue(prototype template, dev seed) + Client->>Server: POST …/contracts/issue + Server-->>Client: issued + active row (encounterTemplateId) + Client-->>HUD: contract_issue_result_received + HUD->>Client: request_sync_from_server() + Client->>Server: GET …/contracts + Server-->>HUD: contracts_received → ContractActiveLabel + + Player->>Enc: defeat pocket NPCs + Enc->>Server: GET encounter-progress (via cast loop) + Server-->>Enc: encounter completed + Enc-->>HUD: main refresh hook + HUD->>Client: request_sync_from_server() + Server-->>HUD: completed row + completionRewardSummary + HUD-->>Player: ContractRewardDeliveryLabel grant lines +``` + +## Files to add + +| Path | Rationale | +|------|-----------| +| `client/scripts/contract_client.gd` | GET/POST HTTP bridge + parse helpers + signals | +| `client/scripts/contract_client.gd.uid` | Godot uid companion (tracked) | +| `client/scripts/contract_hud_controller.gd` | Issue key, HUD render, reward transition | +| `client/scripts/contract_hud_controller.gd.uid` | Godot uid companion | +| `client/test/contract_client_test.gd` | GdUnit: parse GET/issue JSON; active row helper; sync/issue signals | +| `client/test/contract_client_test.gd.uid` | GdUnit uid companion | +| `client/test/contract_hud_controller_test.gd` | GdUnit: deny copy, active label with encounter id, reward transition | +| `client/test/contract_hud_controller_test.gd.uid` | GdUnit uid companion | +| `docs/manual-qa/NEO-153.md` | Godot manual QA — issue, track active, complete, reward line | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `client/scenes/main.tscn` | Add **`ContractClient`** node + three contract HUD labels | +| `client/scripts/main.gd` | Thin contract HUD setup, gameplay key route, encounter-complete refresh hook | +| `client/README.md` | Contract issue + progress HUD subsection (keys, refresh triggers, server deps NEO-151) | + +## Tests + +| File | Coverage | +|------|----------| +| `client/test/contract_client_test.gd` | **`parse_contracts_json`**: active + completed rows with **`encounterTemplateId`** + **`completionRewardSummary`**; schema mismatch → null; GET 404 → **`contracts_sync_failed`**; POST **`issued: true`** / **`issued: false`** + **`reasonCode`**; **`active_contract_row`** helper. AAA (`# Arrange` / `# Act` / `# Assert`). | +| `client/test/contract_hud_controller_test.gd` | Deny copy for **`economy_cap_exceeded`**, **`active_contract_exists`**; active label includes **`prototype_combat_pocket`**; reward label populates on simulated active→completed transition with summary grants. AAA layout. | + +No new **C#** tests (client-only; server covered by **`ContractApiTests`**). No Bruno changes. + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| Fixed seed blocks re-issue after complete (same deterministic instance id) | **Accept for NEO-153** — single-loop AC; **NEO-154** adds seed rotation / fixture reset for repeat capstone | `adopted` | +| Duplicate reward formatter vs shared utility | **Duplicate minimal grant line helpers** inside **`contract_hud_controller.gd`** — avoid coupling to quest controller private methods | `adopted` | +| Issue while encounter already completed (server v1 edge) | **Document in manual QA** — prototype flow is issue-then-clear; HUD still shows **`active`** until server completes contract on a *future* matching clear (NEO-149 README edge case) | `adopted` | +| Concurrent GET + issue | Separate **`_sync_busy`** / **`_issue_busy`** guards; coalesce pending sync like quest client | `adopted` | +| Grant display names before item/faction defs load | Re-render contract reward label on **`definitions_ready`** / faction standing received when summary cached (NEO-122 item-defs re-paint precedent) | `adopted` |