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

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-37GET …/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; 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.

Implementation reconciliation (shipped)

  • Clients: skill_progression_client.gd; interaction_request_client.gd emits interaction_result_received + post_interact_id.
  • Picker: prototype_interactable_picker.gd — nearest in-range resource_node on 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; extended interaction_request_client_test.gd.
  • Docs: client/README.md gather section; NEO-73 manual QA.

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

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/levelprogression_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.