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

12 KiB
Raw Blame History

NEO-70 — Implementation plan

Story reference

Field Value
Key NEO-70
Title E3.M2: Craft HTTP + Bruno gather→refine→make spine
Linear https://linear.app/neon-sprawl/issue/NEO-70/e3m2-craft-http-bruno-gatherrefinemake-spine
Module E3.M2 — RefinementAndRecipeExecution · Epic 3 Slice 3 · backlog E3M2-06
Branch NEO-70-craft-http-bruno-gather-refine-make-spine
Precursor NEO-69CraftOperations.TryCraft + CraftResult (Done on main)
Pattern NEO-55POST /game/players/{id}/inventory (known-player gate, schemaVersion, 200 + structured deny)
Client counterpart NEO-74 — E3S5-03 craft UI + recipe list (blocked by this issue; server-only in this story)
Blocks NEO-74 craft UI HTTP dependency

Kickoff clarifications

Topic Question Agent recommendation Answer
POST response envelope Promote CraftResult 1:1 vs include inventory echo? CraftResult fields onlyschemaVersion 1, success, reasonCode, inputsConsumed, outputsGranted, xpGrantSummary; separate GET inventory / skill-progression for Bruno spine (E3_M2 module doc wire table). User: CraftResult fields only.
Manual QA doc Add docs/manual-qa/NEO-70.md? Yes — E3M2-06 backlog calls for manual QA when craft HTTP lands (NEO-68 pattern). User: No — server-only story, no Godot; Bruno + README sufficient (NEO-55 pattern).

No other blocking decisions — request shape (recipeId + optional quantity default 1), route (POST /game/players/{id}/craft), known-player gate, and HTTP status mapping follow E3M2 kickoff defaults and NEO-55/NEO-38 precedent.

Goal, scope, and out-of-scope

Goal: Versioned POST /game/players/{id}/craft wired to CraftOperations.TryCraft; Bruno documents the full prototype loop gather → refine → make through combat-relevant field_stim_mk0 without inventory POST shortcuts.

In scope (from Linear + E3M2-06):

  • POST /game/players/{id}/craft + wire DTOs + API integration tests (AAA).
  • Known-player gate via IPositionStateStore (NEO-37 / NEO-55 pattern).
  • Bruno bruno/neon-sprawl-server/craft/ — full spine + deny paths for major craft reasonCode values.
  • server/README.md craft HTTP subsection; module doc + alignment table updates.

Out of scope (from Linear + backlog):

  • Godot craft UI — NEO-74.
  • Station interact / world bench placement.
  • docs/manual-qa/NEO-70.md (kickoff decision — Bruno + README only).
  • Telemetry hook sites (NEO-71).

Acceptance criteria checklist

  • POST success returns structured CraftResult wire JSON matching engine outcome.
  • Bruno spine: gather scrap_metal_bulkrefine_scrap_standard (or efficient path) → make_field_stim_mk0 without manual inventory POST shortcuts.
  • At least one deny path documented per major reasonCode (see Tests / Bruno below).
  • Epic 3 Slice 3 acceptance: player completes gather → refine → usable item (field_stim_mk0 consumable).

Technical approach

  1. Route: POST /game/players/{id}/craft — trim id; 404 when empty/unknown position (NEO-55 gate).

  2. Request (PlayerCraftRequest, schemaVersion 1):

    • recipeId (required, non-empty after trim).
    • quantity (optional, default 1 when omitted or null) — passed to CraftOperations.TryCraft.
  3. Response (CraftResponse, schemaVersion 1) — promote NEO-69 envelope (kickoff decision):

    • success: true on commit, false on rule deny.
    • reasonCode: null on success; CraftReasonCodes string on deny.
    • inputsConsumed: { itemId, quantity }[] (empty on deny).
    • outputsGranted: { itemId, quantity }[] (empty on deny).
    • xpGrantSummary: { skillId, amount, sourceKind } on success; omitted or null on deny.
  4. HTTP status mapping (NEO-55 / NEO-38 precedent):

    • 400 — null body, schemaVersion mismatch, empty/missing recipeId.
    • 404 — unknown player (position gate) or CraftReasonCodes.InventoryStoreMissing / ProgressionStoreMissing when store missing (mirror inventory POST store-missing → 404).
    • 200 + JSON — engine deny with success: false + reasonCode + empty I/O lists.
    • 200 + JSON — success with full envelope.
  5. Implementation: New PlayerCraftApi.cs + CraftWireDtos.cs in Game/Crafting/ (map request → CraftOperations.TryCraft, map CraftResult → response). Wire app.MapPlayerCraftApi() from Program.cs after MapPlayerInventoryApi().

  6. Bruno (bruno/neon-sprawl-server/craft/):

    • Post craft gather refine make spine.bru — pre-request script (axios): move + interact on four prototype resource nodes to accumulate ≥11 scrap_metal_bulk without inventory POST (delta 5 + gamma 3 + beta 2 + alpha 1); POST craft refine_scrap_efficient (10→2 refined_plate_stock); POST craft make_field_stim_mk0; assert success true; optional follow-up GET inventory + GET skill-progression asserts field_stim_mk0 qty ≥1 and refine XP increased.
    • Post craft deny unknown recipe.brurecipeId unknown_recipe_idsuccess false, unknown_recipe.
    • Post craft deny insufficient materials.bru — empty player (no gather seed) → insufficient_materials.
    • Post craft deny inventory full.bru — pre-request fills bag via inventory helper or repeated adds, then craft with output that cannot fit → inventory_full (may use make_prototype_armor or a make recipe after filling bag slots).
    • Post craft deny invalid quantity.bruquantity: 0invalid_quantity.
    • folder.bru — match sibling folders.
  7. Docs (on land): Expand server/README.md craft section with POST route, request/response JSON, curl examples, reason codes. Update E3_M2 Implementation snapshot — mark CraftResult promoted to wire; documentation_and_implementation_alignment.md E3.M2 row; E3M2-06 checkboxes in E3M2-prototype-backlog.md. Cross-link NEO-74 per full-stack epic decomposition.

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Crafting/CraftWireDtos.cs Versioned POST request/response + I/O row + XP summary JSON DTOs (schemaVersion 1).
server/NeonSprawl.Server/Game/Crafting/PlayerCraftApi.cs MapPlayerCraftApi — POST route, position gate, engine dispatch, JSON mapping.
server/NeonSprawl.Server.Tests/Game/Crafting/PlayerCraftApiTests.cs AAA HTTP integration: success, denies, 400/404, schema mismatch.
bruno/neon-sprawl-server/craft/folder.bru Bruno folder metadata.
bruno/neon-sprawl-server/craft/Post craft gather refine make spine.bru Full gather→refine→make spine (no inventory POST).
bruno/neon-sprawl-server/craft/Post craft deny unknown recipe.bru Deny unknown_recipe.
bruno/neon-sprawl-server/craft/Post craft deny insufficient materials.bru Deny insufficient_materials.
bruno/neon-sprawl-server/craft/Post craft deny inventory full.bru Deny inventory_full.
bruno/neon-sprawl-server/craft/Post craft deny invalid quantity.bru Deny invalid_quantity.
docs/plans/NEO-70-implementation-plan.md This plan.

Files to modify

Path Rationale
server/NeonSprawl.Server/Program.cs Register MapPlayerCraftApi() alongside other player mutation APIs.
server/README.md Document craft POST route, request/response shapes, curl examples, reason codes; remove “HTTP deferred to NEO-70” note.
server/NeonSprawl.Server/Game/Crafting/CraftResult.cs Update XML remark — wire promotion landed in NEO-70.
docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md Implementation snapshot — craft HTTP + wire CraftResult contract.
docs/decomposition/modules/documentation_and_implementation_alignment.md E3.M2 row — note NEO-70 craft POST when landed.
docs/plans/E3M2-prototype-backlog.md E3M2-06 acceptance checkboxes + landed note when complete.

Tests

Test file What it covers
server/NeonSprawl.Server.Tests/Game/Crafting/PlayerCraftApiTests.cs 404 unknown player. 400 bad schemaVersion, empty recipeId. Success: seed inventory via store helper (test-only), POST refine_scrap_standard, assert success true, inputsConsumed / outputsGranted, xpGrantSummary (refine, 10, activity), reasonCode null. Deny: unknown_recipe, insufficient_materials, invalid_quantity, inventory_full — each 200, success false, stable reasonCode, empty I/O lists; inventory unchanged where applicable. AAA per csharp-style.

Bruno scripts cover end-to-end gather→craft spine and manual dev-server verification. Store-missing codes (inventory_store_missing, progression_store_missing) covered by API tests with test doubles if practical; otherwise documented as 404 paths in README (not Bruno — requires broken store).

Major reasonCode coverage matrix:

reasonCode Automated API test Bruno
unknown_recipe yes yes
insufficient_materials yes yes
invalid_quantity yes yes
inventory_full yes yes
inventory_store_missing API test or README note no
progression_store_missing defer to engine tests (NEO-69) no

Open questions / risks

Question / risk Agent recommendation Status
Bruno scrap totals for spine Use four distinct prototype nodes (11 scrap) + refine_scrap_efficient so make_field_stim_mk0 has 2× refined_plate_stock + 1 scrap without inventory POST. adopted
Node depletion Spine Bruno must hit different nodeDefId rows (one interact each); document node order in Bruno docs block. adopted
No manual QA doc User chose Bruno + README only; Epic 3 Slice 3 “player completes loop” verified via Bruno spine + API tests. adopted
Client not in this story Cross-link NEO-74; Bruno-only verification is not prototype-complete per full-stack rule. adopted
NEO-71 telemetry No hook comments in NEO-70 — NEO-71 owns engine hook placement. adopted

Decisions (kickoff)

  • CraftResult-only wire response — no inventory echo (user confirmed).
  • No docs/manual-qa/NEO-70.md — Bruno + README only (user confirmed; no Godot in scope).
  • HTTP deny = 200 + success: false for rule denies; 400 for contract errors; 404 for unknown player / store missing (NEO-55 precedent).
  • Optional quantity defaults to 1 when omitted (E3M2 backlog default).