13 KiB
13 KiB
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 · Epic 3 Slice 5 (client 2 of 4) · backlog E3S5-02 |
| Branch | NEO-73-e3m1-client-gather-feedback-on-interact |
| Server deps | NEO-63 — gather via POST …/interact (Done); NEO-72 — inventory HUD + refresh (Done); NEO-37 — GET …/skill-progression (Done) |
| Pattern | NEO-28 / NEO-72 — thin HTTP clients, main.gd wiring, debug HUD labels, GdUnit mock transport |
| Downstream | NEO-75 capstone gather→refine loop; 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):
skill_progression_client.gd: GET snapshot; parse v1 envelope; emitprogression_received/progression_sync_failed; publicrequest_sync_from_server();skill_row(skill_id)helper forsalvage.SkillProgressionLabelunderUICanvas: showssalvage: level N, xp M(and optionalrefinestub line omitted until NEO-74).GatherFeedbackLabel: success delta line + deny lines for gather interacts.- Extend
interaction_request_client.gd: emitinteraction_result_received(interactable_id, allowed, reason_code)on parsed 200 (keep Outputprintfor dev). main.gdgather 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 asinteraction_radius_indicators.gd); fall back toprototype_resource_node_alphawhen none in range (server returnsout_of_range). - On
interaction_result_receivedwhen id is a known resource node:allowed=true: snapshot pre-gatherscrap_metal_bulkqty →inventory_client.request_sync_from_server()+skill_progression_client.request_sync_from_server()→ on both (or inventory-first then skill) compute delta → paintGatherFeedbackLabel.allowed=false: paintGatherFeedbackLabelwithGather: denied — {reasonCode}(includingnode_depleted); no refresh.
- Cache interactables catalog descriptors from
- Boot: chain
skill_progression_client.request_sync_from_server()alongside inventory hydrate (after authority config wired). - GdUnit: mock interact 200 →
mainhandler (or client signal) triggers inventory + skill refresh; skill client parse tests. client/README.mdgather feedback subsection.docs/manual-qa/NEO-73.md.
Out of scope (from Linear):
- New gather HTTP route; node respawn visuals; gather VFX.
refineskill 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) onGatherFeedbackLabel. - Salvage skill level/XP visible on
SkillProgressionLabeland changes after successful gather.
Implementation reconciliation (shipped)
- Clients:
skill_progression_client.gd;interaction_request_client.gdemitsinteraction_result_received+post_interact_id. - Picker:
prototype_interactable_picker.gd— nearest in-rangeresource_nodeon R (alpha fallback). - HUD:
GatherFeedbackLabel(delta / deny);SkillProgressionLabel(salvage row); inventory auto-refresh on gather success. - Tests:
skill_progression_client_test.gd,prototype_interactable_picker_test.gd,gather_feedback_refresh_test.gd; extendedinteraction_request_client_test.gd. - Docs:
client/README.mdgather section;NEO-73manual QA.
Technical approach
-
skill_progression_client.gd- Mirror
inventory_client.gd: injectable HTTP,_busyguard,GET …/skill-progression. parse_progression_json(text)static: requireschemaVersion1,skillsarray.skill_row(skill_id: String) -> Dictionary: find row byid; empty dict if missing.- Signals:
progression_received(snapshot),progression_sync_failed(reason).
- Mirror
-
interaction_request_client.gd- Add
signal interaction_result_received(interactable_id: String, allowed: bool, reason_code: String). - On 200 + parsed dict: emit signal;
reason_codeempty string when allowed. - Add
post_interact_id(interactable_id: String)public entry (used bymainfor nearest-node resolution); keeppost_interact_resource()as thin wrapper or deprecate in favor ofmain-resolved id only.
- Add
-
Nearest resource node helper (
main.gdor smallprototype_interactable_picker.gd)- Input: cached descriptor
Array, playerVector3world position. - Filter
kind == "resource_node"; compute horizontal distance toanchor; pick minimum<= interactionRadius. - Tie-break: ascending
interactableId(stable with server ordering). - Store descriptors in
_interactables_catalog: Arrayoncatalog_ready.
- Input: cached descriptor
-
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, computedelta = new_qty - _gather_pre_scrap_qty, formatGather: +{delta} {displayName}onGatherFeedbackLabel; clear flag. - Deny path:
Gather: denied — {reasonCode}on label (node_depleted,inventory_full,out_of_range, etc.). - Terminal E interact: optional
Interaction: …line or leaveGatherFeedbackLabelunchanged (prefer unchanged unless we want generic interact deny — keep gather label gather-specific only).
-
SkillProgressionLabelrendering- Header
Skills:; rowsalvage: level {level}, xp {xp}from cached snapshot. - Error line on
progression_sync_failed(mirror inventory error UX). - Re-render on
progression_receivedand after gather refresh.
- Header
-
Scene layout (
main.tscn)- Add
SkillProgressionClient,GatherFeedbackLabel,SkillProgressionLabelnodes. GatherFeedbackLabel: belowCastFeedbackLabel(~offset_top 378) or between cast and cooldown — short single-line feedback.SkillProgressionLabel: belowInventoryLabel(~offset_top 628) or stack under inventory block; shift layout if needed for readability.SkillProgressionClient: sibling HTTP client node likeInventoryClient.
- Add
-
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 → signalallowed=false,reason_code=node_depleted.gather_feedback_refresh_test.gd(or extendinteraction_request_client_test): stubmaingather handler / test double that verifies inventory + skillrequest_sync_from_servercalled on allowed resource node signal (AAA).
-
Docs on land
- Update E3_M1 client slice note + 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_nodefrom cached interactables catalog (user confirmed). - Gather success copy: inventory delta
+N displayNameafter refresh (user confirmed). - Skill HUD: dedicated
SkillProgressionLabelwith salvage row; boot + post-gather refresh (user confirmed). - Deny UX:
GatherFeedbackLabel+ existing Outputprint(interaction client). - No new gather HTTP on client; reuse
POST …/interact.