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.pull/195/head
parent
fe0d23b01c
commit
9d756c259b
|
|
@ -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": "<dev_player_id>",
|
||||
"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` |
|
||||
Loading…
Reference in New Issue