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

15 KiB
Raw Blame History

NEO-153 — E7M4-10: Client contract issue + progress HUD (Godot)

Linear: NEO-153
Branch: NEO-153-e7m4-10-client-contract-issue-progress-hud-godot
Backlog: E7M4-pre-production-backlog.mdE7M4-10
Module: E7_M4_ContractMissionGenerator.md
Pattern: NEO-122 (quest_progress_client.gd + quest_hud_controller.gd); NEO-110 (encounter-complete refresh spine); NEO-131 (completion reward transition label)
Precursors: NEO-151 DonePOST …/contracts/issue + GET …/contracts (on main); NEO-152 Done — contract telemetry hook sites (on main)
Blocks: NEO-154 (Godot repeatable contract capstone)
Server counterpart: NEO-151 — authoritative issue/list HTTP; Bruno is not prototype-complete per full-stack epic decomposition

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 + 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.

Implementation reconciliation (shipped)

  • contract_client.gd: GET/POST parse v1; active_contract_row, completion_reward_summary; separate sync/issue HTTP guards.
  • contract_hud_controller.gd: Shift+C prototype issue; active/completed label with encounterTemplateId; deny copy; in-session reward transition + economy refresh; issue patch cleared when GET row exists (NEO-122 accept-patch precedent); faction-standing re-paint on reward label.
  • main.tscn / main.gd: ContractClient node + three HUD labels; encounter-complete contract refresh hook.
  • Tests: contract_client_test.gd (7 cases), contract_hud_controller_test.gd (6 cases) — GdUnit green.
  • Docs: client/README.md contract HUD section; docs/manual-qa/NEO-153.md.

Technical approach

Server contract (landed — NEO-151)

  • POST /game/players/{id}/contracts/issueschemaVersion 1, issued, optional reasonCode, optional contract row. HTTP 200 on structured deny (quest accept precedent).
  • GET /game/players/{id}/contractsschemaVersion 1, playerId, contracts[]: active row (01) + 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_pocketencounterTemplateId prototype_combat_pocket; completion bundle scrap_metal_bulk ×5, salvage +15 XP.

Client constants (contract_hud_controller.gd or shared const block)

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:

{
  "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 activecompleted 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

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