diff --git a/profiles/_default.json b/profiles/_default.json index 2a67725..cfcae6d 100644 --- a/profiles/_default.json +++ b/profiles/_default.json @@ -200,6 +200,18 @@ [ {"role": "system", "content": "You are self-prompting with the goal: 'Build a house'. Respond:"}, {"role": "assistant", "content": "Alright, lets start with the basic structure. !newAction(\"Build an 8x8 base for the house.\")"} + ], + [ + {"role": "user", "content": "trex56: Lets make a wooden pickaxe together"}, + {"role": "assistant", "content": "Sure, let me check how to make a wooden pickaxe !getCraftingPlan(\"wooden_pickaxe\",1)"}, + {"role": "system", "content": "You are missing the following items:\n- 2 oak_log\n\nOnce you have these items, here's your crafting plan:\n\nCraft 1 oak_log -> 4 oak_planks\nCraft 1 oak_log -> 4 oak_planks\nCraft 2 oak_planks -> 4 stick\nCraft 3 oak_planks + 2 stick -> 1 wooden_pickaxe"}, + {"role": "assistant", "content": "I need 2 oak logs to make a wooden pickaxe. Do you have any?"} + ], + [ + {"role": "user", "content": "terrance: (FROM OTHER BOT) I want you to craft 5 chains"}, + {"role": "assistant", "content": "Sure, first let me check how to build a chain, !getCraftingPlan(\"chain\",5)"}, + {"role": "system", "content": "You are missing the following items:\n- 7 iron_ingot\n\nOnce you have these items, here's your crafting plan:\n\nCraft 2 iron_ingot -> 18 iron_nugget\nCraft 10 iron_nugget + 5 iron_ingot -> 5 chain"}, + {"role": "assistant", "content": "Seems like I need 7 iron_ingots for 5 chains. Do you have any?"} ] ], diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 6ee481b..9056d29 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -406,7 +406,7 @@ export const actionsList = [ convoManager.endConversation(player_name); return `Converstaion with ${player_name} ended.`; } - } + }, // { // commented for now, causes confusion with goal command // name: '!npcGoal', // description: 'Set a simple goal for an item or building to automatically work towards. Do not use for complex goals.', diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index b4dc74a..59a8416 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -176,5 +176,42 @@ export const queryList = [ perform: async function (agent) { return "Saved place names: " + agent.memory_bank.getKeys(); } - } + }, + { + name: '!getCraftingPlan', + description: "Provides a comprehensive crafting plan for a specified item. This includes a breakdown of required ingredients, the exact quantities needed, and an analysis of missing ingredients or extra items needed based on the bot's current inventory.", + params: { + targetItem: { + type: 'string', + description: 'The item that we are trying to craft' + }, + quantity: { + type: 'int', + description: 'The quantity of the item that we are trying to craft', + optional: true, + domain: [1, Infinity, '[)'], // Quantity must be at least 1, + default: 1 + } + }, + perform: function (agent, targetItem, quantity = 1) { + let bot = agent.bot; + + // Fetch the bot's inventory + const curr_inventory = world.getInventoryCounts(bot); + const target_item = targetItem; + let existingCount = curr_inventory[target_item] || 0; + var prefixMessage = ''; + if (existingCount > 0) { + curr_inventory[target_item] -= existingCount; + prefixMessage = `You already have ${existingCount} ${target_item} in your inventory. If you need to craft more,\n`; + } + + // Generate crafting plan + var craftingPlan = mc.getDetailedCraftingPlan(target_item, quantity, curr_inventory); + craftingPlan = prefixMessage + craftingPlan; + console.log('\n\n\n\n\n\n\n\n\n\n\n'); + console.log(craftingPlan); + return pad(craftingPlan); + } + }, ]; diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 23f30ad..7293c0d 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -79,7 +79,7 @@ export async function craftRecipe(bot, itemName, num=1) { } } if (!recipes || recipes.length === 0) { - log(bot, `You do not have the resources to craft a ${itemName}. It requires: ${Object.entries(mc.getItemCraftingRecipes(itemName)[0]).map(([key, value]) => `${key}: ${value}`).join(', ')}.`); + log(bot, `You do not have the resources to craft a ${itemName}. It requires: ${Object.entries(mc.getItemCraftingRecipes(itemName)[0][0]).map(([key, value]) => `${key}: ${value}`).join(', ')}.`); if (placedTable) { await collectBlock(bot, 'crafting_table', 1); } diff --git a/src/agent/npc/item_goal.js b/src/agent/npc/item_goal.js index 40589ba..9055f54 100644 --- a/src/agent/npc/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -204,7 +204,7 @@ class ItemWrapper { } createChildren() { - let recipes = mc.getItemCraftingRecipes(this.name); + let recipes = mc.getItemCraftingRecipes(this.name).map(([recipe, craftedCount]) => recipe); if (recipes) { for (let recipe of recipes) { let includes_blacklisted = false; diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 58cfbdb..2a3a27c 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -190,7 +190,10 @@ export function getItemCraftingRecipes(itemName) { recipe[ingredientName] = 0; recipe[ingredientName]++; } - recipes.push(recipe); + recipes.push([ + recipe, + {craftedCount : r.result.count} + ]); } return recipes; @@ -327,4 +330,156 @@ export function calculateLimitingResource(availableItems, requiredItems, discret } if(discrete) num = Math.floor(num); return {num, limitingResource} +} + +let loopingItems = new Set(); + +export function initializeLoopingItems() { + + loopingItems = new Set(['coal', + 'wheat', + 'diamond', + 'emerald', + 'raw_iron', + 'raw_gold', + 'redstone', + 'blue_wool', + 'packed_mud', + 'raw_copper', + 'iron_ingot', + 'dried_kelp', + 'gold_ingot', + 'slime_ball', + 'black_wool', + 'quartz_slab', + 'copper_ingot', + 'lapis_lazuli', + 'honey_bottle', + 'rib_armor_trim_smithing_template', + 'eye_armor_trim_smithing_template', + 'vex_armor_trim_smithing_template', + 'dune_armor_trim_smithing_template', + 'host_armor_trim_smithing_template', + 'tide_armor_trim_smithing_template', + 'wild_armor_trim_smithing_template', + 'ward_armor_trim_smithing_template', + 'coast_armor_trim_smithing_template', + 'spire_armor_trim_smithing_template', + 'snout_armor_trim_smithing_template', + 'shaper_armor_trim_smithing_template', + 'netherite_upgrade_smithing_template', + 'raiser_armor_trim_smithing_template', + 'sentry_armor_trim_smithing_template', + 'silence_armor_trim_smithing_template', + 'wayfinder_armor_trim_smithing_template']); +} + + +/** + * Gets a detailed plan for crafting an item considering current inventory + */ +export function getDetailedCraftingPlan(targetItem, count = 1, current_inventory = {}) { + initializeLoopingItems(); + if (!targetItem || count <= 0 || !getItemId(targetItem)) { + return "Invalid input. Please provide a valid item name and positive count."; + } + + if (isBaseItem(targetItem)) { + const available = current_inventory[targetItem] || 0; + if (available >= count) return "You have all required items already in your inventory!"; + return `${targetItem} is a base item, you need to find ${count - available} more in the world`; + } + + const inventory = { ...current_inventory }; + const leftovers = {}; + const plan = craftItem(targetItem, count, inventory, leftovers); + return formatPlan(plan); +} + +function isBaseItem(item) { + return loopingItems.has(item) || getItemCraftingRecipes(item) === null; +} + +function craftItem(item, count, inventory, leftovers, crafted = { required: {}, steps: [], leftovers: {} }) { + // Check available inventory and leftovers first + const availableInv = inventory[item] || 0; + const availableLeft = leftovers[item] || 0; + const totalAvailable = availableInv + availableLeft; + + if (totalAvailable >= count) { + // Use leftovers first, then inventory + const useFromLeft = Math.min(availableLeft, count); + leftovers[item] = availableLeft - useFromLeft; + + const remainingNeeded = count - useFromLeft; + if (remainingNeeded > 0) { + inventory[item] = availableInv - remainingNeeded; + } + return crafted; + } + + // Use whatever is available + const stillNeeded = count - totalAvailable; + if (availableLeft > 0) leftovers[item] = 0; + if (availableInv > 0) inventory[item] = 0; + + if (isBaseItem(item)) { + crafted.required[item] = (crafted.required[item] || 0) + stillNeeded; + return crafted; + } + + const recipe = getItemCraftingRecipes(item)?.[0]; + if (!recipe) { + crafted.required[item] = stillNeeded; + return crafted; + } + + const [ingredients, result] = recipe; + const craftedPerRecipe = result.craftedCount; + const batchCount = Math.ceil(stillNeeded / craftedPerRecipe); + const totalProduced = batchCount * craftedPerRecipe; + + // Add excess to leftovers + if (totalProduced > stillNeeded) { + leftovers[item] = (leftovers[item] || 0) + (totalProduced - stillNeeded); + } + + // Process each ingredient + for (const [ingredientName, ingredientCount] of Object.entries(ingredients)) { + const totalIngredientNeeded = ingredientCount * batchCount; + craftItem(ingredientName, totalIngredientNeeded, inventory, leftovers, crafted); + } + + // Add crafting step + const stepIngredients = Object.entries(ingredients) + .map(([name, amount]) => `${amount * batchCount} ${name}`) + .join(' + '); + crafted.steps.push(`Craft ${stepIngredients} -> ${totalProduced} ${item}`); + + return crafted; +} + +function formatPlan({ required, steps, leftovers }) { + const lines = []; + + if (Object.keys(required).length > 0) { + lines.push('You are missing the following items:'); + Object.entries(required).forEach(([item, count]) => + lines.push(`- ${count} ${item}`)); + lines.push('\nOnce you have these items, here\'s your crafting plan:'); + } else { + lines.push('You have all items required to craft this item!'); + lines.push('Here\'s your crafting plan:'); + } + + lines.push(''); + lines.push(...steps); + + if (Object.keys(leftovers).length > 0) { + lines.push('\nYou will have leftover:'); + Object.entries(leftovers).forEach(([item, count]) => + lines.push(`- ${count} ${item}`)); + } + + return lines.join('\n'); } \ No newline at end of file