From 4682db48c4ec004f6fdb02f2c24dad9978770328 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 24 May 2026 19:05:23 -0400 Subject: [PATCH] NEO-73: add implementation plan for client gather feedback. Kickoff clarifications: nearest in-range resource node on R, inventory delta on GatherFeedbackLabel, SkillProgressionLabel for salvage XP. --- docs/plans/NEO-73-implementation-plan.md | 152 +++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 docs/plans/NEO-73-implementation-plan.md diff --git a/docs/plans/NEO-73-implementation-plan.md b/docs/plans/NEO-73-implementation-plan.md new file mode 100644 index 0000000..b78dd03 --- /dev/null +++ b/docs/plans/NEO-73-implementation-plan.md @@ -0,0 +1,152 @@ +# NEO-73 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-73 | +| **Title** | E3.M1: Client gather feedback on interact (E3S5-02) | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-73/e3m1-client-gather-feedback-on-interact-e3s5-02 | +| **Module** | [E3.M1 — ResourceNodeAndGatherLoop](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) · Epic 3 **Slice 5** (client 2 of 4) · backlog **E3S5-02** | +| **Branch** | `NEO-73-e3m1-client-gather-feedback-on-interact` | +| **Server deps** | [NEO-63](https://linear.app/neon-sprawl/issue/NEO-63) — gather via **`POST …/interact`** (**Done**); [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) — inventory HUD + refresh (**Done**); [NEO-37](https://linear.app/neon-sprawl/issue/NEO-37) — **`GET …/skill-progression`** (**Done**) | +| **Pattern** | [NEO-28](https://linear.app/neon-sprawl/issue/NEO-28) / [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) — thin HTTP clients, `main.gd` wiring, debug HUD labels, GdUnit mock transport | +| **Downstream** | [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) capstone gather→refine loop; [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74) reuses **`SkillProgressionClient`** for **refine** row | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Four-node R wiring** | How should **R** pick the resource node id? | **Nearest in-range `resource_node`** from cached interactables catalog — no new keys; walk to each anchor, press **R**. | **User:** nearest in-range from catalog. | +| **Gather success copy** | What does **`GatherFeedbackLabel`** show? | **Inventory delta after refresh** — `+N {displayName}` for **`scrap_metal_bulk`** (compare pre/post snapshot qty). | **User:** inventory delta. | +| **Salvage XP HUD** | Where does salvage xp/level live? | **`SkillProgressionLabel`** — persistent salvage row; boot hydrate + refresh after successful gather. | **User:** dedicated skill progression label. | + +No other blocking decisions — **`node_depleted`** deny copy follows **`CastFeedbackLabel`** pattern (`Gather: denied — node_depleted`); terminal **E** interact unchanged. + +## Goal, scope, and out-of-scope + +**Goal:** After successful **`resource_node`** interact, the player **sees** inventory change and **salvage** XP movement on HUD — not Output-panel-only proof. + +**In scope (from Linear + [E3S5-02](E3S5-client-prototype-backlog.md#e3s5-02--client-gather-feedback-on-interact-e3m1)):** + +- **`skill_progression_client.gd`:** GET snapshot; parse v1 envelope; emit **`progression_received`** / **`progression_sync_failed`**; public **`request_sync_from_server()`**; **`skill_row(skill_id)`** helper for **`salvage`**. +- **`SkillProgressionLabel`** under **`UICanvas`:** shows **`salvage: level N, xp M`** (and optional **`refine`** stub line omitted until NEO-74). +- **`GatherFeedbackLabel`:** success delta line + deny lines for gather interacts. +- Extend **`interaction_request_client.gd`:** emit **`interaction_result_received(interactable_id, allowed, reason_code)`** on parsed 200 (keep Output **`print`** for dev). +- **`main.gd` gather path:** + - Cache interactables catalog descriptors from **`catalog_ready`**. + - **R** → resolve **nearest in-range** descriptor with **`kind == resource_node`** (horizontal X/Z, inclusive **`<= interactionRadius`** — same math as **`interaction_radius_indicators.gd`**); fall back to **`prototype_resource_node_alpha`** when none in range (server returns **`out_of_range`**). + - On **`interaction_result_received`** when id is a known resource node: + - **`allowed=true`:** snapshot pre-gather **`scrap_metal_bulk`** qty → **`inventory_client.request_sync_from_server()`** + **`skill_progression_client.request_sync_from_server()`** → on both (or inventory-first then skill) compute delta → paint **`GatherFeedbackLabel`**. + - **`allowed=false`:** paint **`GatherFeedbackLabel`** with **`Gather: denied — {reasonCode}`** (including **`node_depleted`**); no refresh. +- Boot: chain **`skill_progression_client.request_sync_from_server()`** alongside inventory hydrate (after authority config wired). +- GdUnit: mock interact 200 → **`main`** handler (or client signal) triggers inventory + skill refresh; skill client parse tests. +- **`client/README.md`** gather feedback subsection. +- **`docs/manual-qa/NEO-73.md`**. + +**Out of scope (from Linear):** + +- New gather HTTP route; node respawn visuals; gather VFX. +- **`refine`** skill row polish (NEO-74); resource-node-defs preview client (NEO-60 optional). + +## Acceptance criteria checklist + +- [ ] In-range interact on fresh node updates **inventory HUD** without manual **I** refresh. +- [ ] Depleted node shows **`node_depleted`** (or agreed copy) on **`GatherFeedbackLabel`**. +- [ ] **Salvage** skill level/XP visible on **`SkillProgressionLabel`** and changes after successful gather. + +## Technical approach + +1. **`skill_progression_client.gd`** + - Mirror **`inventory_client.gd`**: injectable HTTP, **`_busy`** guard, **`GET …/skill-progression`**. + - **`parse_progression_json(text)`** static: require **`schemaVersion` 1**, **`skills`** array. + - **`skill_row(skill_id: String) -> Dictionary`:** find row by **`id`**; empty dict if missing. + - Signals: **`progression_received(snapshot)`**, **`progression_sync_failed(reason)`**. + +2. **`interaction_request_client.gd`** + - Add **`signal interaction_result_received(interactable_id: String, allowed: bool, reason_code: String)`**. + - On 200 + parsed dict: emit signal; **`reason_code`** empty string when allowed. + - Add **`post_interact_id(interactable_id: String)`** public entry (used by **`main`** for nearest-node resolution); keep **`post_interact_resource()`** as thin wrapper or deprecate in favor of **`main`**-resolved id only. + +3. **Nearest resource node helper (`main.gd` or small `prototype_interactable_picker.gd`)** + - Input: cached descriptor **`Array`**, player **`Vector3`** world position. + - Filter **`kind == "resource_node"`**; compute horizontal distance to **`anchor`**; pick minimum **`<= interactionRadius`**. + - Tie-break: ascending **`interactableId`** (stable with server ordering). + - Store descriptors in **`_interactables_catalog: Array`** on **`catalog_ready`**. + +4. **Gather feedback orchestration (`main.gd`)** + - **`_gather_pre_scrap_qty: int`** — set from **`_total_bag_qty("scrap_metal_bulk")`** immediately before POST when posting a resource node id. + - Connect **`interaction_result_received`** → **`_on_interaction_result_for_gather`**. + - Success path: refresh inventory + skill progression; on **`inventory_received`**, if pending gather feedback flag set, compute **`delta = new_qty - _gather_pre_scrap_qty`**, format **`Gather: +{delta} {displayName}`** on **`GatherFeedbackLabel`**; clear flag. + - Deny path: **`Gather: denied — {reasonCode}`** on label ( **`node_depleted`**, **`inventory_full`**, **`out_of_range`**, etc.). + - Terminal **E** interact: optional **`Interaction: …`** line or leave **`GatherFeedbackLabel`** unchanged (prefer unchanged unless we want generic interact deny — keep gather label gather-specific only). + +5. **`SkillProgressionLabel` rendering** + - Header **`Skills:`**; row **`salvage: level {level}, xp {xp}`** from cached snapshot. + - Error line on **`progression_sync_failed`** (mirror inventory error UX). + - Re-render on **`progression_received`** and after gather refresh. + +6. **Scene layout (`main.tscn`)** + - Add **`SkillProgressionClient`**, **`GatherFeedbackLabel`**, **`SkillProgressionLabel`** nodes. + - **`GatherFeedbackLabel`:** below **`CastFeedbackLabel`** (~offset_top 378) or between cast and cooldown — short single-line feedback. + - **`SkillProgressionLabel`:** below **`InventoryLabel`** (~offset_top 628) or stack under inventory block; shift layout if needed for readability. + - **`SkillProgressionClient`:** sibling HTTP client node like **`InventoryClient`**. + +7. **Tests** + - **`skill_progression_client_test.gd`:** mock 200 with trio skills → **`skill_row("salvage")`** has xp/level; 404 → failure signal. + - **`interaction_request_client_test.gd`:** extend — mock 200 deny → signal **`allowed=false`**, **`reason_code=node_depleted`**. + - **`gather_feedback_refresh_test.gd`** (or extend **`interaction_request_client_test`**): stub **`main`** gather handler / test double that verifies inventory + skill **`request_sync_from_server`** called on allowed resource node signal (AAA). + +8. **Docs on land** + - Update [E3_M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) client slice note + [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E3.M1 row when implementation completes. + - **`E3S5-client-prototype-backlog.md`:** check E3S5-02 boxes. + +## Files to add + +| Path | Purpose | +|------|---------| +| `docs/plans/NEO-73-implementation-plan.md` | This plan. | +| `client/scripts/skill_progression_client.gd` | GET player skill progression; **`progression_received`** / **`progression_sync_failed`**; **`skill_row`**. | +| `client/scripts/skill_progression_client.gd.uid` | Godot uid companion (tracked). | +| `client/test/skill_progression_client_test.gd` | GdUnit: parse v1 skills; salvage row lookup; HTTP error path. AAA layout. | +| `client/test/gather_feedback_refresh_test.gd` | GdUnit: interact allow signal → inventory + skill refresh invoked (mock/spy). AAA layout. | +| `docs/manual-qa/NEO-73.md` | Fresh node gather, four-node **R** nearest pick, depletion deny, salvage XP delta, no manual **I**. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `client/scripts/interaction_request_client.gd` | Emit **`interaction_result_received`**; add **`post_interact_id`**. | +| `client/scripts/main.gd` | Catalog cache, nearest-node **R** routing, gather feedback orchestration, skill/inventory boot + signal wiring, HUD render helpers. | +| `client/scenes/main.tscn` | Add **`SkillProgressionClient`**, **`GatherFeedbackLabel`**, **`SkillProgressionLabel`**. | +| `client/test/interaction_request_client_test.gd` | Assert new signal on allow/deny mock responses. | +| `client/README.md` | Gather feedback: **R** nearest-node rule, HUD labels, boot hydrate, server deps. | + +## Tests + +| Test file | What it covers | +|-----------|----------------| +| `client/test/skill_progression_client_test.gd` | Mock 200 with **`salvage`** row **`xp`/`level`** → **`progression_received`** + **`skill_row`**; 404 → **`progression_sync_failed`**. AAA layout. | +| `client/test/interaction_request_client_test.gd` | Mock deny 200 → **`interaction_result_received(false, "node_depleted")`**; allow → **`allowed=true`**. AAA layout. | +| `client/test/gather_feedback_refresh_test.gd` | Lightweight **`main.gd`** gather handler test or extracted callable: resource node allow → **`request_sync_from_server`** on inventory + skill clients (spy nodes). AAA layout. | + +No new **C#** tests (client-only). No new Bruno (server covered by NEO-63). + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Async refresh race (inventory vs skill)** | Paint gather delta when inventory arrives; refresh skill label independently on **`progression_received`** — acceptable lag ≤1 RTT. | **adopted** | +| **Multi-item deltas** | Prototype yields only **`scrap_metal_bulk`**; sum bag qty for that id only. | **adopted** | +| **No in-range node on R** | Fall back to alpha id; server **`out_of_range`** + HUD deny line — same as today when far from nodes. | **adopted** | +| **Terminal E pollutes GatherFeedbackLabel** | Leave gather label unchanged on non-resource-node interacts. | **adopted** | +| **HUD vertical space** | Offset labels below inventory; optional collapse deferred to NEO-75. | **deferred** (NEO-75) | +| **Slice 5 module docs** | Update alignment register when NEO-73 lands, not at kickoff. | **deferred** (implementation) | + +## Decisions (kickoff) + +- **R wiring:** nearest in-range **`resource_node`** from cached interactables catalog (user confirmed). +- **Gather success copy:** inventory delta **`+N displayName`** after refresh (user confirmed). +- **Skill HUD:** dedicated **`SkillProgressionLabel`** with salvage row; boot + post-gather refresh (user confirmed). +- **Deny UX:** **`GatherFeedbackLabel`** + existing Output **`print`** (interaction client). +- **No new gather HTTP** on client; reuse **`POST …/interact`**.