12 KiB
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-69 — CraftOperations.TryCraft + CraftResult (Done on main) |
| Pattern | NEO-55 — POST /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 only — schemaVersion 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 craftreasonCodevalues. server/README.mdcraft 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
CraftResultwire JSON matching engine outcome. - Bruno spine: gather
scrap_metal_bulk→refine_scrap_standard(or efficient path) →make_field_stim_mk0without 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_mk0consumable).
Technical approach
-
Route:
POST /game/players/{id}/craft— trimid; 404 when empty/unknown position (NEO-55 gate). -
Request (
CraftRequest,schemaVersion1):recipeId(required, non-empty after trim).quantity(optional, default 1 when omitted or null) — passed toCraftOperations.TryCraft.
-
Response (
CraftResponse,schemaVersion1) — promote NEO-69 envelope (kickoff decision):success:trueon commit,falseon rule deny.reasonCode:nullon success;CraftReasonCodesstring on deny.inputsConsumed:{ itemId, quantity }[](empty on deny).outputsGranted:{ itemId, quantity }[](empty on deny).xpGrantSummary:{ skillId, amount, sourceKind }on success; omitted ornullon deny.
-
HTTP status mapping (NEO-55 / NEO-38 precedent):
- 400 — null body,
schemaVersionmismatch, empty/missingrecipeId. - 404 — unknown player (position gate) or
CraftReasonCodes.InventoryStoreMissing/ProgressionStoreMissingwhen 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.
- 400 — null body,
-
Implementation: New
PlayerCraftApi.cs+CraftWireDtos.csinGame/Crafting/(map request →CraftOperations.TryCraft, mapCraftResult→ response). Wireapp.MapPlayerCraftApi()fromProgram.csafterMapPlayerInventoryApi(). -
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 ≥11scrap_metal_bulkwithout inventory POST (delta 5 + gamma 3 + beta 2 + alpha 1);POST craftrefine_scrap_efficient(10→2refined_plate_stock);POST craftmake_field_stim_mk0; assertsuccesstrue; optional follow-up GET inventory + GET skill-progression assertsfield_stim_mk0qty ≥1 andrefineXP increased.Post craft deny unknown recipe.bru—recipeIdunknown_recipe_id→successfalse,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 usemake_prototype_armoror a make recipe after filling bag slots).Post craft deny invalid quantity.bru—quantity: 0→invalid_quantity.folder.bru— match sibling folders.
-
Docs (on land): Expand
server/README.mdcraft section with POST route, request/response JSON, curl examples, reason codes. Update E3_M2 Implementation snapshot — markCraftResultpromoted 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: falsefor rule denies; 400 for contract errors; 404 for unknown player / store missing (NEO-55 precedent). - Optional
quantitydefaults to 1 when omitted (E3M2 backlog default).