# 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.
