11 KiB
11 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-55 — GET /game/players/{id}/inventory (Done); NEO-53 — GET /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 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):
inventory_client.gd: GET snapshot, parse v1 envelope, emit signal with parsed body; publicrequest_sync_from_server(); structuredpush_warningon transport/HTTP/JSON failures.item_definitions_client.gd: GET world item defs once (or on boot), cacheid → displayName(and optionalinventorySlotKindfor row prefix); emitdefinitions_ready.InventoryLabelunderUICanvas: lists non-empty bag slots + equipment slot 0 (show— emptywhen quantity 0); resolve labels via defs cache when available, fall back to rawitemId.- Boot hydrate after scene ready (chain: item defs → inventory GET, mirroring hotbar → cooldown boot order in NEO-32).
Iinput →inventory_refreshinproject.godot; handled inmain.gd_unhandled_key_input(same pattern as E/R interact forwarding).- GdUnit: mock HTTP — happy-path parse + error paths for both clients.
client/README.mdinventory 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
bagSlotsand equipment slot 0 from GET body (withdisplayNamewhen defs loaded). - Failed GET surfaces visible error on
InventoryLabelandpush_warningin Output. Itriggers manual inventory refresh;request_sync_from_server()callable for NEO-73 wiring.
Technical approach
-
item_definitions_client.gd- Injectable HTTP transport;
base_url/ shared authority config frommain.gd(same asCooldownSnapshotClient). request_sync_from_server()→GET …/game/world/item-definitions.- On 200 + v1 JSON: build
DictionarymapitemId → { displayName, inventorySlotKind }; emitdefinitions_ready(map). - Errors:
push_warning("ItemDefinitionsClient: …"); emit nothing (inventory HUD falls back to raw ids).
- Injectable HTTP transport;
-
inventory_client.gdrequest_sync_from_server()→GET …/game/players/{dev_player_id}/inventory.- On 200: validate
schemaVersion1, emitinventory_received(snapshot: Dictionary)with full parsed body. - On failure (transport, non-2xx, non-JSON):
push_warning("InventoryClient: …")+ emitinventory_sync_failed(reason: String)somain.gdcan paint the HUD error line without parsing a snapshot. _busyguard likeCooldownSnapshotClient(ignore overlapping GETs).
-
Boot sequence in
main.gd- Add
_setup_inventory_sync()from_ready()after authority is wired. - Instantiate
ItemDefinitionsClient+InventoryClientas child nodes; copybase_url/dev_player_idfromPositionAuthorityClient. - Connect
definitions_ready→ cache map onmain(or hold ref to defs client withdisplay_name_for(item_id)helper method). - On boot:
item_definitions_client.request_sync_from_server()theninventory_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().
- Add
-
HUD formatting (
_render_inventory_label)- Header:
Inventory:+ optional(refresh: I)hint for QA. - Bag: iterate
bagSlots; emit lines only wherequantity > 0:slot {slotIndex}: {displayName or itemId} x{quantity}. - Equipment: always show
slot 0: item line if occupied, elseslot 0: — empty. - On
inventory_sync_failed: replace body withInventory: error — {reason}(keep header); alsopush_warningalready fired from client.
- Header:
-
Scene + input
main.tscn: addInventoryClient,ItemDefinitionsClientnodes;InventoryLabelbelowCooldownSlotsLabel(offset ~460px top, autowrap, same font theme as sibling HUD labels).project.godot:inventory_refresh→ I (physical key).main.gd_unhandled_key_input: ifinventory_refreshpressed →inventory_client.request_sync_from_server().
-
Public refresh contract for NEO-73
- Expose
InventoryClient.request_sync_from_server()(no wrapper required inmainunless we addrefresh_inventory_from_server()alias — prefer direct client call frommainhelpers NEO-73 can reuse).
- Expose
-
Docs on land
- Update E3_M3 client slice note + 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_clientwithdisplayNameresolution (user confirmed). - Error UX:
InventoryLabelerror 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(cacheddisplayName),inventory_client.gd(inventory_received/inventory_sync_failed). - HUD:
UICanvas/InventoryLabel; boot hydrate;I→inventory_refresh; error line +push_warningon failed GET. - Tests:
client/test/item_definitions_client_test.gd,client/test/inventory_client_test.gd. - Docs:
client/README.mdinventory section;NEO-72manual QA; E3.M3 module + alignment register updated.