# 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](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) · Epic 3 **Slice 5** (client 1 of 4) · backlog **E3S5-01** | | **Branch** | `NEO-72-client-inventory-snapshot-hud` | | **Server deps** | [NEO-55](https://linear.app/neon-sprawl/issue/NEO-55) — `GET /game/players/{id}/inventory` (**Done**); [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) — `GET /game/world/item-definitions` (**Done**) | | **Pattern** | [NEO-24](https://linear.app/neon-sprawl/issue/NEO-24) / [NEO-32](https://linear.app/neon-sprawl/issue/NEO-32) — thin HTTP client nodes, `main.gd` wiring, debug HUD labels, GdUnit mock transport | | **Downstream** | [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) gather feedback calls inventory refresh; [NEO-74](https://linear.app/neon-sprawl/issue/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 `I` → `inventory_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](E3S5-client-prototype-backlog.md#e3s5-01--client-inventory-snapshot-hud-e3m3)):** - **`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 - [x] Boot shows current server inventory for **`dev-local-1`** (empty bag + empty equipment stub on fresh dev player). - [x] HUD lists all **non-empty** **`bagSlots`** and **equipment slot 0** from GET body (with **`displayName`** when defs loaded). - [x] Failed GET surfaces visible error on **`InventoryLabel`** and **`push_warning`** in Output. - [x] **`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_refresh`** → **I** (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** - Update [E3_M3](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) client slice note + [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E3.M3 row when implementation completes. - **`E3S5-client-prototype-backlog.md`:** check E3S5-01 boxes. ## 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:** **`I` → `inventory_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. ## Implementation reconciliation (shipped) - **Clients:** `item_definitions_client.gd` (cached **`displayName`**), `inventory_client.gd` (**`inventory_received`** / **`inventory_sync_failed`**). - **HUD:** `UICanvas/InventoryLabel`; boot hydrate; **`I` → `inventory_refresh`**; error line + **`push_warning`** on failed GET. - **Tests:** `client/test/item_definitions_client_test.gd`, `client/test/inventory_client_test.gd`. - **Docs:** `client/README.md` inventory section; [`NEO-72` manual QA](../manual-qa/NEO-72.md); E3.M3 module + alignment register updated.