13 KiB
13 KiB
NEO-74 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-74 |
| Title | E3.M2: Client craft UI + recipe list (E3S5-03) |
| Linear | https://linear.app/neon-sprawl/issue/NEO-74/e3m2-client-craft-ui-recipe-list-e3s5-03 |
| Module | E3.M2 — RefinementAndRecipeExecution · Epic 3 Slice 5 (client 3 of 4) · backlog E3S5-03 |
| Branch | NEO-74-e3m2-client-craft-ui-recipe-list |
| Server deps | NEO-68 — GET /game/world/recipe-definitions (Done); NEO-70 — POST …/craft (Done); NEO-72 — inventory HUD + refresh (Done); NEO-37 — skill-progression GET (Done) |
| Client deps | NEO-73 — SkillProgressionClient, gather refresh pattern (Done on main) |
| Pattern | NEO-72 / NEO-73 — thin HTTP clients, main.gd wiring, debug HUD labels, GdUnit mock transport |
| Downstream | NEO-75 capstone gather→refine→make loop; optional HUD collapse |
Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| Craft success copy | What should success feedback show? | outputsGranted from POST body — e.g. Craft: +1 {displayName}; still GET inventory + skill-progression after success (E3S5 refresh policy). |
User: POST outputsGranted summary. |
| Craft feedback label | Dedicated label vs reuse gather? | Dedicated CraftFeedbackLabel below GatherFeedbackLabel — mirrors cast/gather separation. |
User: dedicated CraftFeedbackLabel. |
| Recipe panel layout | Where to place recipe list + Craft buttons? | Left-column ScrollContainer below SkillProgressionLabel — matches debug HUD stack; NEO-75 can collapse if crowded. |
User: left scroll below skills. |
No other blocking decisions — list all eight prototype recipes per E3S5 kickoff table; qty 1 only (no spinner); deny copy Craft: denied — {reasonCode} mirrors gather; refine row added to existing SkillProgressionLabel (salvage row unchanged).
Goal, scope, and out-of-scope
Goal: Player selects a prototype recipe and POST /game/players/{id}/craft from Godot; sees success/deny, updated inventory, and refine XP on HUD — not Output-panel-only proof.
In scope (from Linear + E3S5-03):
recipe_definitions_client.gd: GET world recipe defs (NEO-68); cache ordered list; emitrecipes_ready.craft_client.gd: POST{ schemaVersion: 1, recipeId }(qty 1 implicit); parse success/deny +reasonCode+ I/O lists; emitcraft_result_received.craft_recipe_panel.gd(child underUICanvas): build scrollable recipe rows (id /displayName, inputs summary viaItemDefinitionsClient.display_name_for, CraftButtonper row); signalcraft_requested(recipe_id).CraftFeedbackLabel: successCraft: +N {displayName}fromoutputsGranted; denyCraft: denied — {reasonCode}; no refresh on deny.- Extend
SkillProgressionLabel: addrefine: level N, xp Mrow below salvage; boot hydrate + refresh after successful craft. main.gdcraft path: oncraft_requested→craft_client.request_craft(recipe_id); on success → paint feedback from POST outputs →inventory_client.request_sync_from_server()+skill_progression_client.request_sync_from_server(); on deny → feedback only (inventory unchanged per AC).- Boot: chain
recipe_definitions_client.request_sync_from_server()after item defs (or parallel); panel populates onrecipes_ready. - GdUnit: craft POST payload/parse; recipe defs parse; deny/success signal paths.
client/README.mdcraft UI subsection.docs/manual-qa/NEO-74.md.
Out of scope (from Linear):
- Station/bench world objects; craft quantity spinner; recipe filtering by skill level.
- HUD collapse/toggle (NEO-75 optional).
Acceptance criteria checklist
refine_scrap_standardandmake_field_stim_mk0craftable from UI when materials allow.- Denied craft shows
reasonCodeonCraftFeedbackLabel; inventory unchanged (verify via GET / no auto-refresh on deny). - Refine skill level/XP visible on
SkillProgressionLabeland increases after successful craft.
Technical approach
-
recipe_definitions_client.gd- Mirror
item_definitions_client.gd: injectable HTTP,_busyguard,GET …/world/recipe-definitions. parse_recipes_json(text)static: requireschemaVersion1,recipesarray.- Cache
Arrayof recipe dicts in server id order; emitrecipes_ready(recipes). - Helper
recipe_by_id(id)for panel lookups.
- Mirror
-
craft_client.gd- Mirror
ability_cast_client.gdPOST pattern: injectable HTTP,_busyguard. request_craft(recipe_id: String, quantity: int = 1) -> bool: POST body{ "schemaVersion": 1, "recipeId": recipe_id, "quantity": quantity }to…/players/{id}/craft.parse_craft_response_json(text)static: requireschemaVersion1,successbool.- Signal
craft_result_received(result: Dictionary)with parsed body (includessuccess,reasonCode,outputsGranted,inputsConsumed, optionalxpGrantSummary). - On HTTP transport failure / non-2xx / JSON mismatch:
craft_request_failed(recipe_id, reason)(mirror interaction client failure path). push_warningon deny with stable reason (NEO-31 pattern).
- Mirror
-
craft_recipe_panel.gd@exportrefs orsetup(...)forItemDefinitionsClientdisplay-name lookup.populate(recipes: Array): clearScrollContainer/VBoxContainerchildren; for each recipe row:Label:{displayName}({id}) + inputs line e.g.needs: 5× Scrap Metal Bulk(joininputswithdisplay_name_for(itemId)).Button: textCraft; pressed → emitcraft_requested(recipe.get("id")).
- Disable buttons while
craft_clientbusy (optionalset_craft_busy(busy)frommain).
-
main.gdcraft orchestration- Constants:
REFINE_SKILL_ID := "refine". - State:
_last_craft_recipe_id: String,_craft_busy(or delegate to client_busy). _setup_craft_ui(): wire clients, panel, feedback label; connectcraft_requested,craft_result_received,craft_request_failed.- Success handler:
- Format
outputsGrantedrows →Craft: +{qty} {displayName}(multi-output: comma-join). - Trigger inventory + skill progression refresh (same as gather success).
- Format
- Deny handler:
Craft: denied — {reasonCode}; no GET refresh. - Extend
_render_skill_progression_label(): salvage row +refinerow (two lines underSkills:header).
- Constants:
-
Scene layout (
main.tscn)- Add nodes:
RecipeDefinitionsClient,CraftClient,CraftFeedbackLabel,CraftRecipePanel(withScrollContainer>VBoxContainer). CraftFeedbackLabel: belowGatherFeedbackLabel(~offset_top 404); shiftCooldownSlotsLabeldown (~+28px) and cascade inventory/skills/panel offsets to avoid overlap.CraftRecipePanel: belowSkillProgressionLabel(~offset_top 728); fixed height scroll (~200–280px) for eight rows.- HTTP client nodes as siblings (same pattern as
InventoryClient).
- Add nodes:
-
Tests
craft_client_test.gd: mock 200 success →success=true,outputsGrantedparsed; mock 200 deny →success=false,reasonCode=insufficient_materials; verify POST URL + JSON body includesrecipeId. AAA layout.recipe_definitions_client_test.gd: mock 200 with two recipes →recipes_readylength + id order. AAA layout.craft_feedback_refresh_test.gd: stub handler — craft allow signal → inventory + skillrequest_sync_from_serverinvoked; deny → refresh not invoked. AAA layout.
-
Manual QA (
docs/manual-qa/NEO-74.md)- Fresh player: craft deny
insufficient_materialsforrefine_scrap_standard→ HUD reason + bag unchanged. - After gather ≥5 scrap:
refine_scrap_standardsuccess → feedback line, inventory showsrefined_plate_stock, refine XP bumped. - With 2 refined + 1 scrap:
make_field_stim_mk0success →field_stim_mk0in bag. - Deny paths spot-check: unknown recipe (if test hook), bag full (optional note — may need pre-fill).
- Fresh player: craft deny
-
Docs on land
- Update E3_M2 client slice note + documentation_and_implementation_alignment.md when implementation completes.
- Check E3S5-03 boxes in
E3S5-client-prototype-backlog.md.
Files to add
| Path | Purpose |
|---|---|
docs/plans/NEO-74-implementation-plan.md |
This plan. |
client/scripts/recipe_definitions_client.gd |
GET world recipe defs; recipes_ready; parse v1 recipes array. |
client/scripts/recipe_definitions_client.gd.uid |
Godot uid companion (tracked). |
client/scripts/craft_client.gd |
POST craft; craft_result_received / craft_request_failed. |
client/scripts/craft_client.gd.uid |
Godot uid companion (tracked). |
client/scripts/craft_recipe_panel.gd |
Scrollable recipe list + Craft buttons; craft_requested signal. |
client/scripts/craft_recipe_panel.gd.uid |
Godot uid companion (tracked). |
client/test/craft_client_test.gd |
GdUnit: POST payload, success/deny parse. AAA layout. |
client/test/recipe_definitions_client_test.gd |
GdUnit: parse v1 recipes array. AAA layout. |
client/test/craft_feedback_refresh_test.gd |
GdUnit: success → inventory + skill refresh; deny → no refresh. AAA layout. |
docs/manual-qa/NEO-74.md |
Craft spine UI session: deny, refine, make stim, refine XP delta. |
Files to modify
| Path | Rationale |
|---|---|
client/scripts/main.gd |
Boot wiring for recipe defs + craft clients; craft orchestration; CraftFeedbackLabel render; refine skill row; authority HTTP config for new clients. |
client/scenes/main.tscn |
Add RecipeDefinitionsClient, CraftClient, CraftFeedbackLabel, CraftRecipePanel; adjust HUD vertical offsets. |
client/README.md |
Craft UI subsection: recipe panel, craft POST, feedback labels, boot hydrate, server deps. |
Tests
| Test file | What it covers |
|---|---|
client/test/craft_client_test.gd |
Mock 200 success → craft_result_received with outputsGranted; mock deny → reasonCode; POST body recipeId + schemaVersion 1. AAA layout. |
client/test/recipe_definitions_client_test.gd |
Mock 200 → recipes_ready emits ordered array; schema mismatch → no emit. AAA layout. |
client/test/craft_feedback_refresh_test.gd |
Craft success handler triggers inventory + skill request_sync_from_server; deny path skips refresh (spy/mock). AAA layout. |
No new C# tests (client-only). No new Bruno (server covered by NEO-68/NEO-70).
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
| HUD vertical crowding (8 recipes) | Left scroll with ~200–280px max height; full collapse deferred to NEO-75. | adopted (kickoff) |
| Multi-output success copy | Comma-join outputsGranted rows — prototype recipes have single output except batch variants. |
adopted |
| Craft busy double-click | Guard with craft_client._busy; disable panel buttons while in flight. |
adopted |
| Deny without inventory refresh | Do not call inventory GET on success=false — AC requires unchanged bag; manual QA verifies. |
adopted |
| Item display names on recipe inputs | Reuse landed ItemDefinitionsClient cache; fall back to raw itemId. |
adopted |
| Slice 5 module docs | Update alignment register when NEO-74 lands, not at kickoff. | deferred (implementation) |
Decisions (kickoff)
- Craft success copy:
outputsGrantedfrom POST response formatted onCraftFeedbackLabel(user confirmed). - Craft feedback: dedicated
CraftFeedbackLabelbelow gather (user confirmed). - Recipe panel: left-column
ScrollContainerbelowSkillProgressionLabel(user confirmed). - Recipe scope: all eight prototype recipes with qty-1 Craft buttons (E3S5 backlog).
- Skill HUD: add
refinerow toSkillProgressionLabel; refresh after successful craft. - Refresh policy: GET inventory + skill-progression on craft success only; no GET on deny.