// craft.* — thin skill wrappers around the existing actions.js crafting // primitives. Each one is generated by makeCraftSkill so adding a new // craftable is one entry in this file rather than a new module. // // All wood/stone crafts that need a workbench reuse actions.placeCraftingTable // internally: the skill's preconditions guarantee the raw materials, then // execute() defers to the actions.js implementation that handles // table-placement, recipe lookup and bot.craft(). import { craftPlanks, craftSticks, craftWoodenAxe, craftWoodenPickaxe, craftWoodenSword, placeCraftingTable, inv as invHelpers, } from "../actions.js"; function totalPlanks(bot) { return invHelpers.getAnyPlanksCount(bot); } function totalSticks(bot) { return invHelpers.getItemCount(bot, "stick"); } function totalLogs(bot) { return invHelpers.getAnyLogCount(bot); } function count(bot, name) { return invHelpers.getItemCount(bot, name); } function withTimeout(promise, ms, label) { let timer; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s`)), ms); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } function asResult(res, doneCode = "done") { if (res?.ok) { return { ok: true, code: doneCode, detail: res.detail, worldDelta: { crafted: res.detail?.item ?? doneCode } }; } const msg = String(res?.detail ?? ""); const code = msg.includes("no recipe") ? "no_recipe" : msg.includes("timed out") ? "timeout" : "failed"; return { ok: false, code, detail: res?.detail, worldDelta: null }; } // --- direct wrappers around existing actions --------------------------------- export const craftPlanksSkill = Object.freeze({ id: "craft.planks", title: "Craft planks from logs", timeoutMs: 20_000, preconditions(ctx) { if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" }; if (totalLogs(ctx.bot) < 1) return { ok: false, code: "missing_material", detail: "no log in inventory" }; return { ok: true }; }, async execute(ctx, args = {}) { return asResult(await craftPlanks(ctx.bot, args.count ?? 4)); }, }); export const craftSticksSkill = Object.freeze({ id: "craft.sticks", title: "Craft sticks from planks", timeoutMs: 20_000, preconditions(ctx) { if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" }; if (totalPlanks(ctx.bot) < 2) return { ok: false, code: "missing_material", detail: "need 2 planks" }; return { ok: true }; }, async execute(ctx, args = {}) { return asResult(await craftSticks(ctx.bot, args.count ?? 4)); }, }); export const craftWoodenAxeSkill = Object.freeze({ id: "craft.wooden-axe", title: "Craft wooden axe", timeoutMs: 30_000, preconditions(ctx) { if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" }; if (totalPlanks(ctx.bot) < 3) return { ok: false, code: "missing_material", detail: "need 3 planks" }; if (totalSticks(ctx.bot) < 2) return { ok: false, code: "missing_material", detail: "need 2 sticks" }; return { ok: true }; }, async execute(ctx) { return asResult(await craftWoodenAxe(ctx.bot)); }, }); export const craftWoodenPickaxeSkill = Object.freeze({ id: "craft.wooden-pickaxe", title: "Craft wooden pickaxe", timeoutMs: 30_000, preconditions(ctx) { if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" }; if (totalPlanks(ctx.bot) < 3) return { ok: false, code: "missing_material", detail: "need 3 planks" }; if (totalSticks(ctx.bot) < 2) return { ok: false, code: "missing_material", detail: "need 2 sticks" }; return { ok: true }; }, async execute(ctx) { return asResult(await craftWoodenPickaxe(ctx.bot)); }, }); export const craftWoodenSwordSkill = Object.freeze({ id: "craft.wooden-sword", title: "Craft wooden sword", timeoutMs: 30_000, preconditions(ctx) { if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" }; if (totalPlanks(ctx.bot) < 2) return { ok: false, code: "missing_material", detail: "need 2 planks" }; if (totalSticks(ctx.bot) < 1) return { ok: false, code: "missing_material", detail: "need 1 stick" }; return { ok: true }; }, async execute(ctx) { return asResult(await craftWoodenSword(ctx.bot)); }, }); // --- generic crafting (stone tools, furnace, chest, torch) ------------------- // Stone-tier and storage crafts share the same lookup-recipe-then-call-bot.craft // shape as the wooden tools, but with different precondition counts and item // names. We define a tiny factory. // // `requires` is a map from item name to required count. `needsTable: true` // means we place / reuse a crafting table first (via actions.placeCraftingTable). function makeRecipeSkill({ id, title, item, requires, needsTable, doneCode = "done" }) { return Object.freeze({ id, title, timeoutMs: 30_000, preconditions(ctx) { if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" }; for (const [name, n] of Object.entries(requires)) { const have = name === "planks" ? totalPlanks(ctx.bot) : count(ctx.bot, name); if (have < n) { return { ok: false, code: "missing_material", detail: `need ${n} ${name} (have ${have})` }; } } return { ok: true }; }, async execute(ctx) { const bot = ctx.bot; let tableBlock = null; if (needsTable) { const tableRes = await placeCraftingTable(bot); if (!tableRes.ok) { const msg = String(tableRes.detail ?? ""); const code = msg.includes("timed out") ? "timeout" : "missing_table"; return { ok: false, code, detail: tableRes.detail, worldDelta: null }; } tableBlock = tableRes.block; } const reg = bot?.registry; const itemId = reg?.itemsByName?.[item]?.id; if (itemId == null) { return { ok: false, code: "unsupported_version", detail: `unknown item ${item}`, worldDelta: null }; } const recipes = bot.recipesFor(itemId, null, 1, tableBlock); const recipe = recipes[0]; if (!recipe) { return { ok: false, code: "no_recipe", detail: `no recipe for ${item}`, worldDelta: null }; } try { await withTimeout(bot.craft(recipe, 1, tableBlock), 15_000, `craft(${item})`); return { ok: true, code: doneCode, detail: { item }, worldDelta: { crafted: item } }; } catch (e) { const msg = String(e?.message ?? ""); const code = msg.includes("timed out") ? "timeout" : "failed"; return { ok: false, code, detail: e.message, worldDelta: null }; } }, validate(ctx, result) { return result.ok && !!result.worldDelta?.crafted; }, }); } export const craftStoneAxeSkill = makeRecipeSkill({ id: "craft.stone-axe", title: "Craft stone axe", item: "stone_axe", requires: { cobblestone: 3, stick: 2 }, needsTable: true, }); export const craftStonePickaxeSkill = makeRecipeSkill({ id: "craft.stone-pickaxe", title: "Craft stone pickaxe", item: "stone_pickaxe", requires: { cobblestone: 3, stick: 2 }, needsTable: true, }); export const craftStoneSwordSkill = makeRecipeSkill({ id: "craft.stone-sword", title: "Craft stone sword", item: "stone_sword", requires: { cobblestone: 2, stick: 1 }, needsTable: true, }); export const craftFurnaceSkill = makeRecipeSkill({ id: "craft.furnace", title: "Craft furnace", item: "furnace", requires: { cobblestone: 8 }, needsTable: true, }); export const craftChestSkill = makeRecipeSkill({ id: "craft.chest", title: "Craft chest", item: "chest", requires: { planks: 8 }, needsTable: true, }); // Torch: 1 stick + 1 coal (or charcoal) → 4 torches. We accept either // fuel via precondition shortcut: if no coal AND no charcoal, fail with // missing_material so the curriculum surfaces the blocker rather than // silently fail at recipe lookup. export const craftTorchSkill = Object.freeze({ id: "craft.torch", title: "Craft torches", timeoutMs: 20_000, preconditions(ctx) { if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" }; if (totalSticks(ctx.bot) < 1) return { ok: false, code: "missing_material", detail: "need 1 stick" }; if (count(ctx.bot, "coal") < 1 && count(ctx.bot, "charcoal") < 1) { return { ok: false, code: "missing_material", detail: "need 1 coal or charcoal" }; } return { ok: true }; }, async execute(ctx) { const bot = ctx.bot; const reg = bot?.registry; const itemId = reg?.itemsByName?.["torch"]?.id; if (itemId == null) { return { ok: false, code: "unsupported_version", detail: "no torch in registry", worldDelta: null }; } const recipes = bot.recipesFor(itemId, null, 1, null); const recipe = recipes[0]; if (!recipe) { return { ok: false, code: "no_recipe", detail: "no recipe for torch (need coal+stick)", worldDelta: null }; } try { await withTimeout(bot.craft(recipe, 1, null), 15_000, "craft(torch)"); return { ok: true, code: "done", detail: { item: "torch" }, worldDelta: { crafted: "torch" } }; } catch (e) { return { ok: false, code: "failed", detail: e.message, worldDelta: null }; } }, });