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

10 KiB

NEO-72 — Implementation plan

Story reference

Field Value
Key NEO-72
Title E3.M3: Client inventory snapshot HUD (E3S5-01)
Linear https://linear.app/neon-sprawl/issue/NEO-72/e3m3-client-inventory-snapshot-hud-e3s5-01
Module E3.M3 — ItemizationAndInventorySchema · Epic 3 Slice 5 (client 1 of 4) · backlog E3S5-01
Branch NEO-72-client-inventory-snapshot-hud
Server deps NEO-55GET /game/players/{id}/inventory (Done); NEO-53GET /game/world/item-definitions (Done)
Pattern NEO-24 / NEO-32 — thin HTTP client nodes, main.gd wiring, debug HUD labels, GdUnit mock transport
Downstream NEO-73 gather feedback calls inventory refresh; NEO-74 craft UI reuses inventory + defs clients

Kickoff clarifications

Topic Question Agent recommendation Answer
Display names Resolve displayName via GET /game/world/item-definitions or raw itemId only? Include item_definitions_client + cached lookup — NEO-53 is landed; readable labels help gather/craft QA in NEO-73/74. User: include defs + displayName.
Failed GET UX How to surface inventory GET errors? InventoryLabel error line + push_warning — visible in-game (NEO-28 cast deny HUD) and dev console (cooldown/interaction HTTP clients). User: HUD + push_warning.
Refresh key Manual refresh binding? Bind Iinventory_refresh — E3S5 backlog QA note; public request_sync_from_server() entrypoint for NEO-73. User: I key.

Goal, scope, and out-of-scope

Goal: Godot client for GET /game/players/{id}/inventory plus a readable bag/equipment debug HUD; boot hydrate and a manual refresh entrypoint for later Slice 5 stories.

In scope (from Linear + E3S5-01):

  • inventory_client.gd: GET snapshot, parse v1 envelope, emit signal with parsed body; public request_sync_from_server(); structured push_warning on transport/HTTP/JSON failures.
  • item_definitions_client.gd: GET world item defs once (or on boot), cache id → displayName (and optional inventorySlotKind for row prefix); emit definitions_ready.
  • InventoryLabel under UICanvas: lists non-empty bag slots + equipment slot 0 (show — empty when quantity 0); resolve labels via defs cache when available, fall back to raw itemId.
  • Boot hydrate after scene ready (chain: item defs → inventory GET, mirroring hotbar → cooldown boot order in NEO-32).
  • I input → inventory_refresh in project.godot; handled in main.gd _unhandled_key_input (same pattern as E/R interact forwarding).
  • GdUnit: mock HTTP — happy-path parse + error paths for both clients.
  • client/README.md inventory HUD subsection.
  • docs/manual-qa/NEO-72.md.

Out of scope (from Linear):

  • Drag-drop, stack split, equip UX polish.
  • POST inventory mutations from UI (gather/craft stories own refresh triggers).
  • Server route or DTO changes.

Acceptance criteria checklist

  • Boot shows current server inventory for dev-local-1 (empty bag + empty equipment stub on fresh dev player).
  • HUD lists all non-empty bagSlots and equipment slot 0 from GET body (with displayName when defs loaded).
  • Failed GET surfaces visible error on InventoryLabel and push_warning in Output.
  • I triggers manual inventory refresh; request_sync_from_server() callable for NEO-73 wiring.

Technical approach

  1. item_definitions_client.gd

    • Injectable HTTP transport; base_url / shared authority config from main.gd (same as CooldownSnapshotClient).
    • request_sync_from_server()GET …/game/world/item-definitions.
    • On 200 + v1 JSON: build Dictionary map itemId → { displayName, inventorySlotKind }; emit definitions_ready(map).
    • Errors: push_warning("ItemDefinitionsClient: …"); emit nothing (inventory HUD falls back to raw ids).
  2. inventory_client.gd

    • request_sync_from_server()GET …/game/players/{dev_player_id}/inventory.
    • On 200: validate schemaVersion 1, emit inventory_received(snapshot: Dictionary) with full parsed body.
    • On failure (transport, non-2xx, non-JSON): push_warning("InventoryClient: …") + emit inventory_sync_failed(reason: String) so main.gd can paint the HUD error line without parsing a snapshot.
    • _busy guard like CooldownSnapshotClient (ignore overlapping GETs).
  3. Boot sequence in main.gd

    • Add _setup_inventory_sync() from _ready() after authority is wired.
    • Instantiate ItemDefinitionsClient + InventoryClient as child nodes; copy base_url / dev_player_id from PositionAuthorityClient.
    • Connect definitions_ready → cache map on main (or hold ref to defs client with display_name_for(item_id) helper method).
    • On boot: item_definitions_client.request_sync_from_server() then inventory_client.request_sync_from_server() (defs may still be in flight when first inventory paints — re-render label when defs arrive).
    • Connect inventory_received / inventory_sync_failed_render_inventory_label().
  4. HUD formatting (_render_inventory_label)

    • Header: Inventory: + optional (refresh: I) hint for QA.
    • Bag: iterate bagSlots; emit lines only where quantity > 0: slot {slotIndex}: {displayName or itemId} x{quantity}.
    • Equipment: always show slot 0: item line if occupied, else slot 0: — empty.
    • On inventory_sync_failed: replace body with Inventory: error — {reason} (keep header); also push_warning already fired from client.
  5. Scene + input

    • main.tscn: add InventoryClient, ItemDefinitionsClient nodes; InventoryLabel below CooldownSlotsLabel (offset ~460px top, autowrap, same font theme as sibling HUD labels).
    • project.godot: inventory_refreshI (physical key).
    • main.gd _unhandled_key_input: if inventory_refresh pressed → inventory_client.request_sync_from_server().
  6. Public refresh contract for NEO-73

    • Expose InventoryClient.request_sync_from_server() (no wrapper required in main unless we add refresh_inventory_from_server() alias — prefer direct client call from main helpers NEO-73 can reuse).
  7. Docs on land

Files to add

Path Purpose
docs/plans/NEO-72-implementation-plan.md This plan.
client/scripts/item_definitions_client.gd GET world item defs; cache id → display metadata; definitions_ready signal.
client/scripts/item_definitions_client.gd.uid Godot uid companion (tracked).
client/scripts/inventory_client.gd GET player inventory snapshot; inventory_received / inventory_sync_failed signals.
client/scripts/inventory_client.gd.uid Godot uid companion (tracked).
client/test/item_definitions_client_test.gd GdUnit: parse v1 items list; lookup helper; HTTP error push_warning.
client/test/inventory_client_test.gd GdUnit: parse bag/equipment slots; 404/500 failure emits inventory_sync_failed.
docs/manual-qa/NEO-72.md Boot empty inventory, Bruno seed + refresh, failed GET (server stopped), I refresh.

Files to modify

Path Rationale
client/scenes/main.tscn Add InventoryClient, ItemDefinitionsClient, InventoryLabel nodes.
client/scripts/main.gd Boot wiring, HUD render, I refresh input, defs cache + inventory signal handlers.
client/project.godot Register inventory_refresh input action (I).
client/README.md Inventory HUD subsection: bindings, label format, boot/refresh behavior, server deps.

Tests

Test file What it covers
client/test/item_definitions_client_test.gd Mock transport 200 with six-item v1 JSON → map contains scrap_metal_bulk displayName; non-200 → no definitions_ready, warning path. AAA layout.
client/test/inventory_client_test.gd Mock 200 with empty 24+1 slot grid → inventory_received payload has bagSlots.size() 24; 404 → inventory_sync_failed + warning; malformed JSON → failure signal. AAA layout.

No new C# tests (client-only; server contracts covered by NEO-55/NEO-53). No new Bruno (server already exercised).

Open questions / risks

Question / risk Agent recommendation Status
Defs/inventory race on boot First inventory paint may use raw ids; re-render on definitions_ready — acceptable for prototype. adopted
Empty bag UX Show Bag: (empty) when no occupied bag slots; equipment stub always listed. adopted
HUD clutter at 24 slots Non-empty-only rows keep label readable until NEO-75 optional collapse. adopted
Slice 5 module docs Update alignment register when NEO-72 lands, not at kickoff. deferred (implementation)

Decisions (kickoff)

  • Include item_definitions_client with displayName resolution (user confirmed).
  • Error UX: InventoryLabel error line + push_warning (user confirmed).
  • Manual refresh: Iinventory_refresh (user confirmed).
  • Equipment row: always show slot 0 (empty or occupied) per Linear AC “equipment stub slot”.
  • No POST inventory from UI in this story.