neon-sprawl/docs/plans/NEO-74-implementation-plan.md

14 KiB
Raw Permalink Blame History

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-68GET /game/world/recipe-definitions (Done); NEO-70POST …/craft (Done); NEO-72 — inventory HUD + refresh (Done); NEO-37 — skill-progression GET (Done)
Client deps NEO-73SkillProgressionClient, 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; 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_requestedcraft_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.

Implementation reconciliation (shipped)

  • Clients: recipe_definitions_client.gd, craft_client.gd; craft_recipe_panel.gd scroll list + Craft buttons.
  • HUD: CraftFeedbackLabel (POST outputsGranted / deny); SkillProgressionLabel salvage + refine rows; inventory refresh on craft success only.
  • Tests: craft_client_test.gd, recipe_definitions_client_test.gd, craft_feedback_refresh_test.gd.
  • Docs: client/README.md craft section; NEO-74 manual QA.

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 (~200280px) 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

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 ~200280px 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.