Smart crafting: walk to table, check materials, report results

Bridge craft_item now:
- Fuzzy-matches item names (wooden_pickaxe, wood_pickaxe, etc.)
- Searches for crafting table within 32 blocks
- Walks to crafting table if found but not close enough
- Checks if materials are available
- Reports specific failure reasons to chat:
  "I need a crafting table" / "I don't have the materials"
- Reports success: "Done! Crafted 1 wooden pickaxe."

Brain now:
- Reports craft success/failure to in-game chat
- Reports any HIGH priority task failure to chat
- Handles craft_item as async (waits for pathfinding + crafting)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
roberts 2026-03-30 13:53:48 -05:00
parent 813f8704bb
commit 513507e941
2 changed files with 102 additions and 10 deletions

View file

@ -647,18 +647,83 @@ async function handleAction(action, params = {}) {
case 'craft_item': { case 'craft_item': {
const { itemName, count: craftCount } = params; const { itemName, count: craftCount } = params;
const mcData = require('minecraft-data')(bot.version); const mcData = require('minecraft-data')(bot.version);
const item = mcData.itemsByName[itemName];
if (!item) throw new Error(`Unknown item: ${itemName}`);
// Find crafting table nearby if needed // Try to find item — handle both "wooden_pickaxe" and "planks" style names
const craftingTable = bot.findBlock({ let item = mcData.itemsByName[itemName];
matching: (block) => block.name.includes('crafting_table'), if (!item) {
maxDistance: 4, // Try common Bedrock name variants
const variants = [
itemName,
itemName.replace('wooden_', 'wood_'),
itemName.replace('wood_', 'wooden_'),
`minecraft:${itemName}`,
itemName.replace('_', ''),
];
for (const v of variants) {
item = mcData.itemsByName[v];
if (item) break;
}
// Try fuzzy match
if (!item) {
for (const [name, data] of Object.entries(mcData.itemsByName)) {
if (name.includes(itemName) || itemName.includes(name)) {
item = data;
break;
}
}
}
}
if (!item) {
return { crafted: false, error: `I don't know what "${itemName.replace(/_/g, ' ')}" is.` };
}
// Step 1: Find crafting table (search wider radius)
let craftingTable = bot.findBlock({
matching: (block) => block.name.includes('crafting_table') || block.name.includes('workbench'),
maxDistance: 32,
}); });
// Step 2: Walk to crafting table if found but not close enough
if (craftingTable) {
const dist = bot.entity.position.distanceTo(craftingTable.position);
if (dist > 4) {
log('client', 'INFO', `Walking to crafting table at ${craftingTable.position}`);
bot.pathfinder.setGoal(new GoalNear(
craftingTable.position.x, craftingTable.position.y, craftingTable.position.z, 2
));
// Wait for arrival (up to 15 seconds)
await new Promise((resolve) => {
const check = setInterval(() => {
const d = bot.entity.position.distanceTo(craftingTable.position);
if (d <= 3) { clearInterval(check); resolve(); }
}, 500);
setTimeout(() => { clearInterval(check); resolve(); }, 15000);
});
// Re-find the table now that we're close
craftingTable = bot.findBlock({
matching: (block) => block.name.includes('crafting_table') || block.name.includes('workbench'),
maxDistance: 4,
});
}
}
// Step 3: Check recipes
const recipes = bot.recipesFor(item.id, null, null, craftingTable || undefined); const recipes = bot.recipesFor(item.id, null, null, craftingTable || undefined);
if (recipes.length === 0) throw new Error(`No recipe found for ${itemName}`); if (recipes.length === 0) {
await bot.craft(recipes[0], craftCount || 1, craftingTable || undefined); const reason = craftingTable
return { crafted: itemName, count: craftCount || 1 }; ? `I don't have the materials to craft ${itemName.replace(/_/g, ' ')}.`
: `I need a crafting table to make ${itemName.replace(/_/g, ' ')}.`;
return { crafted: false, error: reason };
}
// Step 4: Craft!
try {
await bot.craft(recipes[0], craftCount || 1, craftingTable || undefined);
log('client', 'INFO', `Crafted ${craftCount || 1}x ${itemName}`);
return { crafted: true, item: itemName, count: craftCount || 1 };
} catch (e) {
return { crafted: false, error: `Crafting failed: ${e.message}` };
}
} }
// --- Use/Activate Block --- // --- Use/Activate Block ---

View file

@ -214,6 +214,29 @@ class DougBrain(QObject):
def on_response(resp: ResponseMessage): def on_response(resp: ResponseMessage):
if resp.status == "success": if resp.status == "success":
data = resp.data or {}
# Handle craft results specifically
if task.action == "craft_item":
self._waiting_for_action = False
if data.get("crafted"):
item = data.get("item", "item").replace("_", " ")
self._ws.send_request("send_chat", {
"message": f"Done! Crafted {data.get('count', 1)} {item}."
})
self._tasks.complete()
else:
error = data.get("error", "Something went wrong.")
self._ws.send_request("send_chat", {"message": error})
self._tasks.cancel()
return
# Handle other results with error messages
if task.action in ("open_chest", "dig_block", "equip_item"):
self._waiting_for_action = False
self._tasks.complete()
return
# For non-movement actions, complete immediately # For non-movement actions, complete immediately
if task.action not in ("move_to", "move_relative", "follow_player"): if task.action not in ("move_to", "move_relative", "follow_player"):
self._waiting_for_action = False self._waiting_for_action = False
@ -221,7 +244,11 @@ class DougBrain(QObject):
else: else:
self._waiting_for_action = False self._waiting_for_action = False
self._tasks.cancel() self._tasks.cancel()
log.debug(f"Action failed: {resp.error}") error = resp.error or "Something went wrong"
log.debug(f"Action failed: {error}")
# Report failure to chat for player-initiated tasks
if task.priority >= Priority.HIGH:
self._ws.send_request("send_chat", {"message": error})
self._ws.send_request(task.action, task.params, on_response) self._ws.send_request(task.action, task.params, on_response)