meta { name: POST craft gather refine make spine type: http seq: 5 } docs { NEO-70 prototype spine: accumulate >=11 scrap_metal_bulk (gather when nodes allow, reuse persisted inventory after dotnet test), refine_scrap_efficient when refined_plate_stock < 2, then make_field_stim_mk0. NEO-118: best-effort quest objective wiring on craft success (active craft_recipe quests). Deny envelopes return empty inputsConsumed/outputsGranted arrays. No inventory POST shortcuts for spine materials. } script:pre-request { const axios = require("axios"); const { getInventory, sumItemQuantity } = require("./scripts/inventory-api-helper.js"); const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl"); const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId"); const jsonHeaders = { headers: { "Content-Type": "application/json" } }; const progressionBefore = await axios.get(`${baseUrl}/game/players/${playerId}/skill-progression`); const refineBefore = progressionBefore.data.skills.find((row) => row.id === "refine"); bru.setVar("refineXpBeforeSpine", refineBefore ? refineBefore.xp : 0); async function currentScrap() { return sumItemQuantity(await getInventory(bru), "scrap_metal_bulk"); } async function currentRefined() { return sumItemQuantity(await getInventory(bru), "refined_plate_stock"); } async function moveNear(x, z) { await axios.post( `${baseUrl}/game/players/${playerId}/move`, { schemaVersion: 1, target: { x, y: 0.9, z } }, jsonHeaders, ); } async function tryGather(interactableId) { const response = await axios.post( `${baseUrl}/game/players/${playerId}/interact`, { schemaVersion: 1, interactableId }, jsonHeaders, ); if (response.status !== 200) { throw new Error(`gather ${interactableId} HTTP ${response.status}`); } if (response.data?.allowed === true) { return; } if (response.data?.reasonCode === "node_depleted") { return; } throw new Error(`gather ${interactableId} denied: ${JSON.stringify(response.data)}`); } async function craft(recipeId) { const response = await axios.post( `${baseUrl}/game/players/${playerId}/craft`, { schemaVersion: 1, recipeId }, jsonHeaders, ); if (response.status !== 200 || response.data?.success !== true) { throw new Error(`craft ${recipeId} failed: ${response.status} ${JSON.stringify(response.data)}`); } return response.data; } const gatherPlan = [ { id: "prototype_urban_bulk_delta", x: 16, z: -2 }, { id: "prototype_bio_mat_gamma", x: 10, z: -2 }, { id: "prototype_subsurface_vein_beta", x: 16, z: -6 }, { id: "prototype_resource_node_alpha", x: 10, z: -6 }, ]; let scrap = await currentScrap(); for (const node of gatherPlan) { if (scrap >= 11) { break; } await moveNear(node.x, node.z); await tryGather(node.id); scrap = await currentScrap(); } if (scrap < 11) { throw new Error(`spine needs >=11 scrap_metal_bulk, have ${scrap} after optional gathers`); } if ((await currentRefined()) < 2) { await craft("refine_scrap_efficient"); } } post { url: {{baseUrl}}/game/players/{{playerId}}/craft body: json auth: none } headers { content-type: application/json } body:json { { "schemaVersion": 1, "recipeId": "make_field_stim_mk0" } } script:post-response { const axios = require("axios"); const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl"); const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId"); const inventory = await axios.get(`${baseUrl}/game/players/${playerId}/inventory`); const progression = await axios.get(`${baseUrl}/game/players/${playerId}/skill-progression`); bru.setVar("spineInventory", inventory.data); bru.setVar("spineProgression", progression.data); } tests { test("make_field_stim_mk0 succeeds after gather and refine", function () { expect(res.getStatus()).to.equal(200); const body = res.getBody(); expect(body.schemaVersion).to.equal(1); expect(body.success).to.equal(true); expect(body.reasonCode).to.equal(null); expect(body.outputsGranted).to.deep.include({ itemId: "field_stim_mk0", quantity: 1 }); expect(body.xpGrantSummary).to.be.an("object"); expect(body.xpGrantSummary.skillId).to.equal("refine"); expect(body.xpGrantSummary.amount).to.equal(10); expect(body.xpGrantSummary.sourceKind).to.equal("activity"); }); test("inventory includes field_stim_mk0 after spine", function () { const inventory = bru.getVar("spineInventory"); const total = inventory.bagSlots .filter((slot) => slot.itemId === "field_stim_mk0") .reduce((sum, slot) => sum + slot.quantity, 0); expect(total).to.be.at.least(1); }); test("refine skill XP increased after spine crafts", function () { const progression = bru.getVar("spineProgression"); const refine = progression.skills.find((row) => row.id === "refine"); const before = bru.getVar("refineXpBeforeSpine"); expect(refine).to.be.an("object"); expect(refine.xp - before).to.be.at.least(10); }); }