NEO-74: Add kickoff implementation plan for client craft UI.
Documents E3S5-03 scope, kickoff HUD decisions, and file/test targets for recipe list, craft POST client, and refine XP visibility.pull/108/head
parent
8e04cfc67b
commit
dff58adfe8
|
|
@ -0,0 +1,161 @@
|
|||
# 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](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) · Epic 3 **Slice 5** (client 3 of 4) · backlog **E3S5-03** |
|
||||
| **Branch** | `NEO-74-e3m2-client-craft-ui-recipe-list` |
|
||||
| **Server deps** | [NEO-68](https://linear.app/neon-sprawl/issue/NEO-68) — `GET /game/world/recipe-definitions` (**Done**); [NEO-70](https://linear.app/neon-sprawl/issue/NEO-70) — `POST …/craft` (**Done**); [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) — inventory HUD + refresh (**Done**); [NEO-37](https://linear.app/neon-sprawl/issue/NEO-37) — skill-progression GET (**Done**) |
|
||||
| **Client deps** | [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) — `SkillProgressionClient`, gather refresh pattern (**Done** on `main`) |
|
||||
| **Pattern** | [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) / [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) — thin HTTP clients, `main.gd` wiring, debug HUD labels, GdUnit mock transport |
|
||||
| **Downstream** | [NEO-75](https://linear.app/neon-sprawl/issue/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](E3S5-client-prototype-backlog.md#kickoff-decisions-slice-5-client); 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](E3S5-client-prototype-backlog.md#e3s5-03--client-craft-ui--recipe-list-e3m2)):**
|
||||
|
||||
- **`recipe_definitions_client.gd`:** GET world recipe defs (NEO-68); cache ordered list; emit **`recipes_ready`**.
|
||||
- **`craft_client.gd`:** POST `{ schemaVersion: 1, recipeId }` (qty **1** implicit); parse success/deny + **`reasonCode`** + I/O lists; emit **`craft_result_received`**.
|
||||
- **`craft_recipe_panel.gd`** (child under **`UICanvas`**): build scrollable recipe rows (id / **`displayName`**, inputs summary via **`ItemDefinitionsClient.display_name_for`**, **Craft** **`Button`** per row); signal **`craft_requested(recipe_id)`**.
|
||||
- **`CraftFeedbackLabel`:** success **`Craft: +N {displayName}`** from **`outputsGranted`**; deny **`Craft: denied — {reasonCode}`**; no refresh on deny.
|
||||
- Extend **`SkillProgressionLabel`:** add **`refine: level N, xp M`** row below salvage; boot hydrate + refresh after successful craft.
|
||||
- **`main.gd` craft path:** on **`craft_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 on **`recipes_ready`**.
|
||||
- GdUnit: craft POST payload/parse; recipe defs parse; deny/success signal paths.
|
||||
- **`client/README.md`** craft 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_standard`** and **`make_field_stim_mk0`** craftable from UI when materials allow.
|
||||
- [ ] Denied craft shows **`reasonCode`** on **`CraftFeedbackLabel`**; inventory unchanged (verify via GET / no auto-refresh on deny).
|
||||
- [ ] **Refine** skill level/XP visible on **`SkillProgressionLabel`** and increases after successful craft.
|
||||
|
||||
## Technical approach
|
||||
|
||||
1. **`recipe_definitions_client.gd`**
|
||||
- Mirror **`item_definitions_client.gd`**: injectable HTTP, **`_busy`** guard, **`GET …/world/recipe-definitions`**.
|
||||
- **`parse_recipes_json(text)`** static: require **`schemaVersion` 1**, **`recipes`** array.
|
||||
- Cache **`Array`** of recipe dicts in server id order; emit **`recipes_ready(recipes)`**.
|
||||
- Helper **`recipe_by_id(id)`** for panel lookups.
|
||||
|
||||
2. **`craft_client.gd`**
|
||||
- Mirror **`ability_cast_client.gd`** POST pattern: injectable HTTP, **`_busy`** guard.
|
||||
- **`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: require **`schemaVersion` 1**, **`success`** bool.
|
||||
- Signal **`craft_result_received(result: Dictionary)`** with parsed body (includes **`success`**, **`reasonCode`**, **`outputsGranted`**, **`inputsConsumed`**, optional **`xpGrantSummary`**).
|
||||
- On HTTP transport failure / non-2xx / JSON mismatch: **`craft_request_failed(recipe_id, reason)`** (mirror interaction client failure path).
|
||||
- **`push_warning`** on deny with stable reason (NEO-31 pattern).
|
||||
|
||||
3. **`craft_recipe_panel.gd`**
|
||||
- **`@export`** refs or **`setup(...)`** for **`ItemDefinitionsClient`** display-name lookup.
|
||||
- **`populate(recipes: Array)`**: clear **`ScrollContainer/VBoxContainer`** children; for each recipe row:
|
||||
- **`Label`**: **`{displayName}`** (`{id}`) + inputs line e.g. **`needs: 5× Scrap Metal Bulk`** (join **`inputs`** with **`display_name_for(itemId)`**).
|
||||
- **`Button`**: text **`Craft`**; pressed → emit **`craft_requested(recipe.get("id"))`**.
|
||||
- Disable buttons while **`craft_client`** busy (optional **`set_craft_busy(busy)`** from **`main`**).
|
||||
|
||||
4. **`main.gd` craft 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; connect **`craft_requested`**, **`craft_result_received`**, **`craft_request_failed`**.
|
||||
- Success handler:
|
||||
- Format **`outputsGranted`** rows → **`Craft: +{qty} {displayName}`** (multi-output: comma-join).
|
||||
- Trigger inventory + skill progression refresh (same as gather success).
|
||||
- Deny handler: **`Craft: denied — {reasonCode}`**; no GET refresh.
|
||||
- Extend **`_render_skill_progression_label()`**: salvage row + **`refine`** row (two lines under **`Skills:`** header).
|
||||
|
||||
5. **Scene layout (`main.tscn`)**
|
||||
- Add nodes: **`RecipeDefinitionsClient`**, **`CraftClient`**, **`CraftFeedbackLabel`**, **`CraftRecipePanel`** (with **`ScrollContainer` > `VBoxContainer`**).
|
||||
- **`CraftFeedbackLabel`:** below **`GatherFeedbackLabel`** (~offset_top 404); shift **`CooldownSlotsLabel`** down (~+28px) and cascade inventory/skills/panel offsets to avoid overlap.
|
||||
- **`CraftRecipePanel`:** below **`SkillProgressionLabel`** (~offset_top 728); fixed height scroll (~200–280px) for eight rows.
|
||||
- HTTP client nodes as siblings (same pattern as **`InventoryClient`**).
|
||||
|
||||
6. **Tests**
|
||||
- **`craft_client_test.gd`:** mock 200 success → **`success=true`**, **`outputsGranted`** parsed; mock 200 deny → **`success=false`**, **`reasonCode=insufficient_materials`**; verify POST URL + JSON body includes **`recipeId`**. AAA layout.
|
||||
- **`recipe_definitions_client_test.gd`:** mock 200 with two recipes → **`recipes_ready`** length + id order. AAA layout.
|
||||
- **`craft_feedback_refresh_test.gd`:** stub handler — craft allow signal → inventory + skill **`request_sync_from_server`** invoked; deny → refresh **not** invoked. AAA layout.
|
||||
|
||||
7. **Manual QA (`docs/manual-qa/NEO-74.md`)**
|
||||
- Fresh player: craft deny **`insufficient_materials`** for **`refine_scrap_standard`** → HUD reason + bag unchanged.
|
||||
- After gather ≥5 scrap: **`refine_scrap_standard`** success → feedback line, inventory shows **`refined_plate_stock`**, refine XP bumped.
|
||||
- With 2 refined + 1 scrap: **`make_field_stim_mk0`** success → **`field_stim_mk0`** in bag.
|
||||
- Deny paths spot-check: unknown recipe (if test hook), bag full (optional note — may need pre-fill).
|
||||
|
||||
8. **Docs on land**
|
||||
- Update [E3_M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) client slice note + [documentation_and_implementation_alignment.md](../decomposition/modules/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:** **`outputsGranted`** from POST response formatted on **`CraftFeedbackLabel`** (user confirmed).
|
||||
- **Craft feedback:** dedicated **`CraftFeedbackLabel`** below gather (user confirmed).
|
||||
- **Recipe panel:** left-column **`ScrollContainer`** below **`SkillProgressionLabel`** (user confirmed).
|
||||
- **Recipe scope:** all **eight** prototype recipes with qty-**1** **Craft** buttons (E3S5 backlog).
|
||||
- **Skill HUD:** add **`refine`** row to **`SkillProgressionLabel`**; refresh after successful craft.
|
||||
- **Refresh policy:** GET inventory + skill-progression on craft success only; no GET on deny.
|
||||
Loading…
Reference in New Issue