diff --git a/src/agent/agent.js b/src/agent/agent.js index f5a8e3d52..664a67842 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -321,7 +321,7 @@ export class Agent { console.log(`${this.name} full response to ${source}: ""${res}""`); - if (res.trim().length === 0) { + if (res.trim().length === 0 || res.trim() === '\t') { console.warn('no response') break; // empty response ends loop } @@ -550,4 +550,4 @@ export class Agent { killAll() { serverProxy.shutdown(); } -} \ No newline at end of file +} diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index f348487ed..87892a22a 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -193,11 +193,11 @@ export const actionsList = [ }) }, { - name: '!consume', + name: '!eat', description: 'Eat/drink the given item.', - params: {'item_name': { type: 'ItemName', description: 'The name of the item to consume.' }}, + params: {'item_name': { type: 'ItemName', description: 'The name of the item to eat.' }}, perform: runAsAction(async (agent, item_name) => { - await skills.consume(agent.bot, item_name); + await skills.eat(agent.bot, item_name); }) }, { @@ -265,7 +265,7 @@ export const actionsList = [ }, { name: '!craftRecipe', - description: 'Craft the given recipe a given number of times.', + description: 'Craft the given recipe a given number of times. Will automatically create crafting table if needed.', params: { 'recipe_name': { type: 'ItemName', description: 'The name of the output item to craft.' }, 'num': { type: 'int', description: 'The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe.', domain: [1, Number.MAX_SAFE_INTEGER] } @@ -274,6 +274,309 @@ export const actionsList = [ await skills.craftRecipe(agent.bot, recipe_name, num); }) }, + { + name: '!autoCraft', + description: 'Automatically craft an item, handling the full crafting chain (logs -> planks -> sticks -> tools). Much smarter than !craftRecipe for complex items.', + params: { + 'item_name': { type: 'ItemName', description: 'The name of the item to craft (e.g., wooden_pickaxe, stone_sword).' }, + 'num': { type: 'int', description: 'The number of items to craft.', domain: [1, Number.MAX_SAFE_INTEGER] } + }, + perform: runAsAction(async (agent, item_name, num) => { + const bot = agent.bot; + const inventory = () => { + const inv = {}; + for (const item of bot.inventory.items()) { + if (item) { + inv[item.name] = (inv[item.name] || 0) + item.count; + } + } + return inv; + }; + + // Define crafting chains for common items + // Format: item_name -> [prerequisite steps..., final_item] + // Steps: 'planks' = ensure planks, 'stick' = ensure sticks, 'crafting_table' = ensure table + const craftingChains = { + // ============ WOODEN TOOLS ============ + 'wooden_pickaxe': ['planks', 'stick', 'crafting_table', 'wooden_pickaxe'], + 'wooden_axe': ['planks', 'stick', 'crafting_table', 'wooden_axe'], + 'wooden_sword': ['planks', 'stick', 'crafting_table', 'wooden_sword'], + 'wooden_shovel': ['planks', 'stick', 'crafting_table', 'wooden_shovel'], + 'wooden_hoe': ['planks', 'stick', 'crafting_table', 'wooden_hoe'], + + // ============ STONE TOOLS ============ + 'stone_pickaxe': ['planks', 'stick', 'crafting_table', 'stone_pickaxe'], + 'stone_axe': ['planks', 'stick', 'crafting_table', 'stone_axe'], + 'stone_sword': ['planks', 'stick', 'crafting_table', 'stone_sword'], + 'stone_shovel': ['planks', 'stick', 'crafting_table', 'stone_shovel'], + 'stone_hoe': ['planks', 'stick', 'crafting_table', 'stone_hoe'], + + // ============ IRON TOOLS ============ + 'iron_pickaxe': ['planks', 'stick', 'crafting_table', 'iron_pickaxe'], + 'iron_axe': ['planks', 'stick', 'crafting_table', 'iron_axe'], + 'iron_sword': ['planks', 'stick', 'crafting_table', 'iron_sword'], + 'iron_shovel': ['planks', 'stick', 'crafting_table', 'iron_shovel'], + 'iron_hoe': ['planks', 'stick', 'crafting_table', 'iron_hoe'], + + // ============ GOLD TOOLS ============ + 'golden_pickaxe': ['planks', 'stick', 'crafting_table', 'golden_pickaxe'], + 'golden_axe': ['planks', 'stick', 'crafting_table', 'golden_axe'], + 'golden_sword': ['planks', 'stick', 'crafting_table', 'golden_sword'], + 'golden_shovel': ['planks', 'stick', 'crafting_table', 'golden_shovel'], + 'golden_hoe': ['planks', 'stick', 'crafting_table', 'golden_hoe'], + + // ============ DIAMOND TOOLS ============ + 'diamond_pickaxe': ['planks', 'stick', 'crafting_table', 'diamond_pickaxe'], + 'diamond_axe': ['planks', 'stick', 'crafting_table', 'diamond_axe'], + 'diamond_sword': ['planks', 'stick', 'crafting_table', 'diamond_sword'], + 'diamond_shovel': ['planks', 'stick', 'crafting_table', 'diamond_shovel'], + 'diamond_hoe': ['planks', 'stick', 'crafting_table', 'diamond_hoe'], + + // ============ LEATHER ARMOR ============ + 'leather_helmet': ['crafting_table', 'leather_helmet'], + 'leather_chestplate': ['crafting_table', 'leather_chestplate'], + 'leather_leggings': ['crafting_table', 'leather_leggings'], + 'leather_boots': ['crafting_table', 'leather_boots'], + + // ============ IRON ARMOR ============ + 'iron_helmet': ['crafting_table', 'iron_helmet'], + 'iron_chestplate': ['crafting_table', 'iron_chestplate'], + 'iron_leggings': ['crafting_table', 'iron_leggings'], + 'iron_boots': ['crafting_table', 'iron_boots'], + + // ============ GOLD ARMOR ============ + 'golden_helmet': ['crafting_table', 'golden_helmet'], + 'golden_chestplate': ['crafting_table', 'golden_chestplate'], + 'golden_leggings': ['crafting_table', 'golden_leggings'], + 'golden_boots': ['crafting_table', 'golden_boots'], + + // ============ DIAMOND ARMOR ============ + 'diamond_helmet': ['crafting_table', 'diamond_helmet'], + 'diamond_chestplate': ['crafting_table', 'diamond_chestplate'], + 'diamond_leggings': ['crafting_table', 'diamond_leggings'], + 'diamond_boots': ['crafting_table', 'diamond_boots'], + + // ============ WEAPONS & COMBAT ============ + 'bow': ['planks', 'stick', 'crafting_table', 'bow'], + 'arrow': ['planks', 'stick', 'crafting_table', 'arrow'], + 'crossbow': ['planks', 'stick', 'crafting_table', 'crossbow'], + 'shield': ['planks', 'crafting_table', 'shield'], + + // ============ BASIC ITEMS ============ + 'crafting_table': ['planks', 'crafting_table'], + 'stick': ['planks', 'stick'], + 'torch': ['planks', 'stick', 'torch'], + 'chest': ['planks', 'crafting_table', 'chest'], + 'furnace': ['crafting_table', 'furnace'], + 'smoker': ['crafting_table', 'furnace', 'smoker'], + 'blast_furnace': ['crafting_table', 'furnace', 'blast_furnace'], + + // ============ BEDS (all colors) ============ + 'white_bed': ['planks', 'crafting_table', 'white_bed'], + 'orange_bed': ['planks', 'crafting_table', 'orange_bed'], + 'magenta_bed': ['planks', 'crafting_table', 'magenta_bed'], + 'light_blue_bed': ['planks', 'crafting_table', 'light_blue_bed'], + 'yellow_bed': ['planks', 'crafting_table', 'yellow_bed'], + 'lime_bed': ['planks', 'crafting_table', 'lime_bed'], + 'pink_bed': ['planks', 'crafting_table', 'pink_bed'], + 'gray_bed': ['planks', 'crafting_table', 'gray_bed'], + 'light_gray_bed': ['planks', 'crafting_table', 'light_gray_bed'], + 'cyan_bed': ['planks', 'crafting_table', 'cyan_bed'], + 'purple_bed': ['planks', 'crafting_table', 'purple_bed'], + 'blue_bed': ['planks', 'crafting_table', 'blue_bed'], + 'brown_bed': ['planks', 'crafting_table', 'brown_bed'], + 'green_bed': ['planks', 'crafting_table', 'green_bed'], + 'red_bed': ['planks', 'crafting_table', 'red_bed'], + 'black_bed': ['planks', 'crafting_table', 'black_bed'], + + // ============ DOORS ============ + 'oak_door': ['planks', 'crafting_table', 'oak_door'], + 'spruce_door': ['planks', 'crafting_table', 'spruce_door'], + 'birch_door': ['planks', 'crafting_table', 'birch_door'], + 'jungle_door': ['planks', 'crafting_table', 'jungle_door'], + 'acacia_door': ['planks', 'crafting_table', 'acacia_door'], + 'dark_oak_door': ['planks', 'crafting_table', 'dark_oak_door'], + 'iron_door': ['crafting_table', 'iron_door'], + + // ============ BOATS ============ + 'oak_boat': ['planks', 'crafting_table', 'oak_boat'], + 'spruce_boat': ['planks', 'crafting_table', 'spruce_boat'], + 'birch_boat': ['planks', 'crafting_table', 'birch_boat'], + 'jungle_boat': ['planks', 'crafting_table', 'jungle_boat'], + 'acacia_boat': ['planks', 'crafting_table', 'acacia_boat'], + 'dark_oak_boat': ['planks', 'crafting_table', 'dark_oak_boat'], + + // ============ STORAGE & UTILITY ============ + 'barrel': ['planks', 'crafting_table', 'barrel'], + 'composter': ['planks', 'crafting_table', 'composter'], + 'cartography_table': ['planks', 'crafting_table', 'cartography_table'], + 'fletching_table': ['planks', 'crafting_table', 'fletching_table'], + 'smithing_table': ['planks', 'crafting_table', 'smithing_table'], + 'loom': ['planks', 'crafting_table', 'loom'], + 'bookshelf': ['planks', 'crafting_table', 'bookshelf'], + 'ladder': ['planks', 'stick', 'crafting_table', 'ladder'], + 'fence': ['planks', 'stick', 'crafting_table', 'fence'], + 'fence_gate': ['planks', 'stick', 'crafting_table', 'fence_gate'], + + // ============ FOOD & FARMING ============ + 'bread': ['crafting_table', 'bread'], + 'cake': ['crafting_table', 'cake'], + 'cookie': ['crafting_table', 'cookie'], + 'pumpkin_pie': ['crafting_table', 'pumpkin_pie'], + + // ============ RAILS & MINECARTS ============ + 'rail': ['planks', 'stick', 'crafting_table', 'rail'], + 'powered_rail': ['planks', 'stick', 'crafting_table', 'powered_rail'], + 'detector_rail': ['crafting_table', 'detector_rail'], + 'activator_rail': ['planks', 'stick', 'crafting_table', 'activator_rail'], + 'minecart': ['crafting_table', 'minecart'], + + // ============ REDSTONE ============ + 'piston': ['planks', 'crafting_table', 'piston'], + 'sticky_piston': ['crafting_table', 'sticky_piston'], + 'lever': ['planks', 'stick', 'lever'], + 'tripwire_hook': ['planks', 'stick', 'crafting_table', 'tripwire_hook'], + 'daylight_detector': ['crafting_table', 'daylight_detector'], + 'observer': ['crafting_table', 'observer'], + 'hopper': ['planks', 'crafting_table', 'hopper'], + 'dropper': ['crafting_table', 'dropper'], + 'dispenser': ['crafting_table', 'dispenser'], + + // ============ BLOCKS ============ + 'cobblestone_slab': ['crafting_table', 'cobblestone_slab'], + 'stone_slab': ['crafting_table', 'stone_slab'], + 'brick': ['crafting_table', 'brick'], + 'bricks': ['crafting_table', 'bricks'], + 'stone_bricks': ['crafting_table', 'stone_bricks'], + 'glass_pane': ['crafting_table', 'glass_pane'], + + // ============ BUCKETS & TOOLS ============ + 'bucket': ['crafting_table', 'bucket'], + 'compass': ['crafting_table', 'compass'], + 'clock': ['crafting_table', 'clock'], + 'map': ['crafting_table', 'map'], + 'shears': ['crafting_table', 'shears'], + 'fishing_rod': ['planks', 'stick', 'crafting_table', 'fishing_rod'], + 'flint_and_steel': ['crafting_table', 'flint_and_steel'], + 'lead': ['crafting_table', 'lead'], + 'name_tag': ['crafting_table', 'name_tag'], + + // ============ ENCHANTING & BREWING ============ + 'enchanting_table': ['crafting_table', 'enchanting_table'], + 'anvil': ['crafting_table', 'anvil'], + 'brewing_stand': ['crafting_table', 'brewing_stand'], + 'cauldron': ['crafting_table', 'cauldron'], + }; + + // Helper to craft planks from any log type + const craftPlanks = async () => { + const inv = inventory(); + const logTypes = ['oak_log', 'spruce_log', 'birch_log', 'jungle_log', + 'acacia_log', 'dark_oak_log', 'mangrove_log', 'cherry_log', + 'crimson_stem', 'warped_stem', 'stripped_oak_log', 'stripped_spruce_log', + 'stripped_birch_log', 'stripped_jungle_log', 'stripped_acacia_log', + 'stripped_dark_oak_log', 'stripped_mangrove_log', 'stripped_cherry_log']; + + for (const logType of logTypes) { + if ((inv[logType] || 0) > 0) { + const plankType = logType.replace('stripped_', '').replace('_log', '_planks').replace('_stem', '_planks'); + console.log(`[AUTOCRAFT] Crafting ${plankType} from ${logType}`); + await skills.craftRecipe(bot, plankType, Math.min(inv[logType], 4)); + return true; + } + } + return false; + }; + + // Check if we have enough planks, if not craft more + const ensurePlanks = async (needed) => { + let inv = inventory(); + const plankTypes = ['oak_planks', 'spruce_planks', 'birch_planks', 'jungle_planks', + 'acacia_planks', 'dark_oak_planks', 'mangrove_planks', 'cherry_planks', + 'crimson_planks', 'warped_planks', 'bamboo_planks']; + let totalPlanks = 0; + for (const p of plankTypes) { + totalPlanks += inv[p] || 0; + } + + while (totalPlanks < needed) { + const crafted = await craftPlanks(); + if (!crafted) { + console.log(`[AUTOCRAFT] Cannot craft more planks, have ${totalPlanks}, need ${needed}`); + return false; + } + inv = inventory(); + totalPlanks = 0; + for (const p of plankTypes) { + totalPlanks += inv[p] || 0; + } + } + return true; + }; + + // Check if we have sticks, if not craft them + const ensureSticks = async (needed) => { + let inv = inventory(); + if ((inv['stick'] || 0) >= needed) return true; + + // Need 2 planks per 4 sticks + const sticksNeeded = needed - (inv['stick'] || 0); + const planksNeeded = Math.ceil(sticksNeeded / 4) * 2; + + if (!await ensurePlanks(planksNeeded)) return false; + + await skills.craftRecipe(bot, 'stick', Math.ceil(sticksNeeded / 4)); + return true; + }; + + // Check if we have crafting table + const ensureCraftingTable = async () => { + const inv = inventory(); + if ((inv['crafting_table'] || 0) > 0) return true; + + // Need 4 planks for crafting table + if (!await ensurePlanks(4)) return false; + + await skills.craftRecipe(bot, 'crafting_table', 1); + return true; + }; + + console.log(`[AUTOCRAFT] Starting autoCraft for ${item_name} x${num}`); + + // Get the crafting chain if it exists + const chain = craftingChains[item_name]; + + if (chain) { + // Execute each step in the chain + for (const step of chain) { + console.log(`[AUTOCRAFT] Chain step: ${step}`); + if (step === 'planks') { + // For tools, we need: pickaxe/axe=5 planks(3+2sticks), sword=3 planks(1+2sticks), shovel=3 planks(1+2sticks) + if (!await ensurePlanks(8 * num)) { + console.log('[AUTOCRAFT] Failed to ensure planks'); + } + } else if (step === 'stick') { + if (!await ensureSticks(4 * num)) { + console.log('[AUTOCRAFT] Failed to ensure sticks'); + } + } else if (step === 'crafting_table') { + if (!await ensureCraftingTable()) { + console.log('[AUTOCRAFT] Failed to ensure crafting table'); + } + } else if (step === item_name) { + // Final item + await skills.craftRecipe(bot, item_name, num); + } else { + // Generic craft step + await skills.craftRecipe(bot, step, num); + } + } + } else { + // No predefined chain, try direct crafting + await skills.craftRecipe(bot, item_name, num); + } + }) + }, { name: '!smeltItem', description: 'Smelt the given item the given number of times.', diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index ad5b701ee..eca3e14b0 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -70,12 +70,21 @@ export const queryList = [ let bot = agent.bot; let inventory = world.getInventoryCounts(bot); let res = 'INVENTORY'; - for (const item in inventory) { - if (inventory[item] && inventory[item] > 0) - res += `\n- ${item}: ${inventory[item]}`; + + // Sort items by count (descending) for better readability + const sortedItems = Object.entries(inventory) + .filter(([item, count]) => count && count > 0) + .sort((a, b) => b[1] - a[1]); + + for (const [item, count] of sortedItems) { + res += `\n- ${item}: ${count}`; } - if (res === 'INVENTORY') { + + if (sortedItems.length === 0) { res += ': Nothing'; + // Debug: Log raw inventory state + console.log('[INVENTORY DEBUG] inventory.items():', bot.inventory.items().map(i => i ? `${i.name}:${i.count}` : 'null')); + console.log('[INVENTORY DEBUG] Total slots:', bot.inventory.slots.length); } else if (agent.bot.game.gameMode === 'creative') { res += '\n(You have infinite items in creative mode. You do not need to gather resources!!)'; @@ -96,6 +105,12 @@ export const queryList = [ res += `\nFeet: ${boots.name}`; if (!helmet && !chestplate && !leggings && !boots) res += 'Nothing'; + + // Also show held item + const heldItem = bot.inventory.slots[bot.quickBarSlot + 36]; + if (heldItem) { + res += `\nHOLDING: ${heldItem.name}`; + } return pad(res); } @@ -133,14 +148,53 @@ export const queryList = [ name: "!craftable", description: "Get the craftable items with the bot's inventory.", perform: function (agent) { - let craftable = world.getCraftableItems(agent.bot); + const bot = agent.bot; + let craftable = world.getCraftableItems(bot); let res = 'CRAFTABLE_ITEMS'; + + // Categorize craftable items for better readability + const tools = []; + const weapons = []; + const armor = []; + const blocks = []; + const other = []; + for (const item of craftable) { - res += `\n- ${item}`; + if (item.includes('pickaxe') || item.includes('axe') || item.includes('shovel') || item.includes('hoe')) { + tools.push(item); + } else if (item.includes('sword') || item.includes('bow') || item.includes('crossbow')) { + weapons.push(item); + } else if (item.includes('helmet') || item.includes('chestplate') || item.includes('leggings') || item.includes('boots')) { + armor.push(item); + } else if (item.includes('block') || item.includes('planks') || item.includes('slab') || item.includes('stairs')) { + blocks.push(item); + } else { + other.push(item); + } } - if (res == 'CRAFTABLE_ITEMS') { + + if (tools.length > 0) res += '\nTools: ' + tools.join(', '); + if (weapons.length > 0) res += '\nWeapons: ' + weapons.join(', '); + if (armor.length > 0) res += '\nArmor: ' + armor.join(', '); + if (blocks.length > 0) res += '\nBlocks: ' + blocks.slice(0, 10).join(', ') + (blocks.length > 10 ? '...' : ''); + if (other.length > 0) res += '\nOther: ' + other.slice(0, 15).join(', ') + (other.length > 15 ? '...' : ''); + + if (craftable.length === 0) { res += ': none'; + // Give hint about what could be crafted with raw materials + const inventory = world.getInventoryCounts(bot); + const hints = []; + if (Object.keys(inventory).some(k => k.includes('log'))) { + hints.push('You have logs - craft planks first, then crafting_table, then sticks, then tools'); + } + if (Object.keys(inventory).some(k => k.includes('planks'))) { + hints.push('You have planks - craft crafting_table, sticks, or more planks'); + } + if (hints.length > 0) { + res += '\nHINT: ' + hints.join('. '); + } } + return pad(res); } }, diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 715455073..bb8539e4d 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -7,6 +7,14 @@ import settings from "../../../settings.js"; const blockPlaceDelay = settings.block_place_delay == null ? 0 : settings.block_place_delay; const useDelay = blockPlaceDelay > 0; +// Helper function to create Paper-safe movements (avoids anti-cheat kicks) +function createSafeMovements(bot) { + const movements = new pf.Movements(bot); + movements.allowSprinting = false; // Paper kicks for sprint-related movement + movements.allowParkour = false; // Parkour jumps can trigger fly-kick + return movements; +} + export function log(bot, message) { bot.output += message + '\n'; } @@ -33,82 +41,425 @@ async function equipHighestAttack(bot) { await bot.equip(weapon, 'hand'); } -export async function craftRecipe(bot, itemName, num=1) { +export async function craftRecipe(bot, itemName, num=1, _recursionDepth=0) { /** * Attempt to craft the given item name from a recipe. May craft many items. + * Automatically creates crafting table if needed and has materials. + * Automatically crafts prerequisite items (like sticks from planks). * @param {MinecraftBot} bot, reference to the minecraft bot. * @param {string} itemName, the item name to craft. + * @param {number} num, how many to craft. + * @param {number} _recursionDepth, internal parameter to prevent infinite recursion. * @returns {Promise} true if the recipe was crafted, false otherwise. * @example * await skills.craftRecipe(bot, "stick"); **/ + + // Prevent infinite recursion + const MAX_RECURSION = 5; + if (_recursionDepth > MAX_RECURSION) { + log(bot, `Cannot craft ${itemName}: crafting chain too deep (possible circular dependency).`); + return false; + } + let placedTable = false; - if (mc.getItemCraftingRecipes(itemName).length == 0) { + // Validate recipe exists + const allRecipes = mc.getItemCraftingRecipes(itemName); + if (!allRecipes || allRecipes.length === 0) { log(bot, `${itemName} is either not an item, or it does not have a crafting recipe!`); return false; } - // get recipes that don't require a crafting table + // Get current inventory + let inventory = world.getInventoryCounts(bot); + + // Log current inventory for debugging + console.log('[CRAFT] Current inventory:', JSON.stringify(inventory)); + console.log('[CRAFT] Attempting to craft:', itemName); + + // get recipes that don't require a crafting table (2x2 grid recipes) let recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, null); let craftingTable = null; - const craftingTableRange = 16; - placeTable: if (!recipes || recipes.length === 0) { + const craftingTableRange = 32; + + // Check if we need a crafting table (3x3 grid recipes) + const needsCraftingTable = !recipes || recipes.length === 0; + + if (needsCraftingTable) { + // Get recipes that require crafting table recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, true); - if(!recipes || recipes.length === 0) break placeTable; //Don't bother going to the table if we don't have the required resources. - - // Look for crafting table - craftingTable = world.getNearestBlock(bot, 'crafting_table', craftingTableRange); - if (craftingTable === null){ - - // Try to place crafting table - let hasTable = world.getInventoryCounts(bot)['crafting_table'] > 0; - if (hasTable) { - let pos = world.getNearestFreeSpace(bot, 1, 6); - await placeBlock(bot, 'crafting_table', pos.x, pos.y, pos.z); - craftingTable = world.getNearestBlock(bot, 'crafting_table', craftingTableRange); - if (craftingTable) { - recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, craftingTable); - placedTable = true; + + if (!recipes || recipes.length === 0) { + // No recipes available even with table - try to craft missing ingredients first! + const requiredItems = allRecipes[0][0]; + const missing = []; + let canCraftMissing = true; + + for (const [ingredient, count] of Object.entries(requiredItems)) { + const have = inventory[ingredient] || 0; + if (have < count) { + const needed = count - have; + missing.push({ item: ingredient, need: count, have: have, needed: needed }); } } - else { - log(bot, `Crafting ${itemName} requires a crafting table.`) + + if (missing.length > 0 && _recursionDepth < MAX_RECURSION) { + log(bot, `Missing ingredients for ${itemName}: ${missing.map(m => `${m.item} (need ${m.need}, have ${m.have})`).join(', ')}. Attempting to craft them...`); + + // Try to craft each missing ingredient + for (const missingItem of missing) { + // Check if this ingredient has a recipe + const ingredientRecipes = mc.getItemCraftingRecipes(missingItem.item); + if (ingredientRecipes && ingredientRecipes.length > 0) { + log(bot, `Attempting to craft ${missingItem.needed} ${missingItem.item}...`); + const success = await craftRecipe(bot, missingItem.item, missingItem.needed, _recursionDepth + 1); + if (!success) { + canCraftMissing = false; + log(bot, `Failed to craft prerequisite ${missingItem.item}.`); + } else { + // Wait for inventory to sync after recursive craft + await new Promise(resolve => setTimeout(resolve, 300)); + await bot.waitForTicks(5); + // Update inventory after crafting + inventory = world.getInventoryCounts(bot); + console.log(`[CRAFT] Inventory after crafting ${missingItem.item}:`, JSON.stringify(inventory)); + } + } else { + canCraftMissing = false; + log(bot, `${missingItem.item} cannot be crafted - must be gathered/mined.`); + } + } + + if (canCraftMissing) { + // Wait additional time for all crafting to settle + await new Promise(resolve => setTimeout(resolve, 300)); + await bot.waitForTicks(5); + // Refresh recipes after crafting prerequisites + recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, true); + } + } + + // Still no recipes after trying to craft ingredients + if (!recipes || recipes.length === 0) { + const stillMissing = []; + inventory = world.getInventoryCounts(bot); + for (const [ingredient, count] of Object.entries(requiredItems)) { + const have = inventory[ingredient] || 0; + if (have < count) { + stillMissing.push(`${ingredient} (need ${count}, have ${have})`); + } + } + if (stillMissing.length > 0) { + log(bot, `Cannot craft ${itemName}. Still missing: ${stillMissing.join(', ')}`); + } else { + log(bot, `Cannot craft ${itemName}. Have ingredients but recipe not available.`); + } return false; } } - else { + + // Look for existing crafting table nearby + craftingTable = world.getNearestBlock(bot, 'crafting_table', craftingTableRange); + + if (craftingTable === null) { + // No crafting table nearby - check if we have one in inventory + let hasTable = inventory['crafting_table'] > 0; + + if (!hasTable) { + // Need to craft a crafting table first! + log(bot, `Need a crafting table to craft ${itemName}. Attempting to craft one...`); + + // Check if we have planks to make a crafting table + const plankTypes = ['oak_planks', 'spruce_planks', 'birch_planks', 'jungle_planks', + 'acacia_planks', 'dark_oak_planks', 'mangrove_planks', 'cherry_planks', + 'bamboo_planks', 'crimson_planks', 'warped_planks']; + let totalPlanks = 0; + for (const plankType of plankTypes) { + totalPlanks += inventory[plankType] || 0; + } + + if (totalPlanks >= 4) { + // We have enough planks - craft a crafting table + const tableSuccess = await craftRecipe(bot, 'crafting_table', 1, _recursionDepth + 1); + if (!tableSuccess) { + log(bot, `Failed to craft crafting table!`); + return false; + } + hasTable = true; + } else { + // Need to make planks first - check for logs + const logTypes = ['oak_log', 'spruce_log', 'birch_log', 'jungle_log', + 'acacia_log', 'dark_oak_log', 'mangrove_log', 'cherry_log', + 'crimson_stem', 'warped_stem']; + let hasLogs = false; + for (const logType of logTypes) { + if ((inventory[logType] || 0) > 0) { + hasLogs = true; + // Craft planks from logs first + log(bot, `Crafting planks from ${logType}...`); + const planksSuccess = await craftRecipe(bot, logType.replace('_log', '_planks').replace('_stem', '_planks'), 1, _recursionDepth + 1); + if (planksSuccess) { + // Now try to craft the crafting table + const tableSuccess = await craftRecipe(bot, 'crafting_table', 1, _recursionDepth + 1); + if (tableSuccess) { + hasTable = true; + break; + } + } + } + } + + // Also check stripped logs + const strippedLogTypes = logTypes.map(l => 'stripped_' + l); + if (!hasTable) { + for (const logType of strippedLogTypes) { + if ((inventory[logType] || 0) > 0) { + hasLogs = true; + const baseName = logType.replace('stripped_', '').replace('_log', '_planks').replace('_stem', '_planks'); + log(bot, `Crafting planks from ${logType}...`); + const planksSuccess = await craftRecipe(bot, baseName, 1, _recursionDepth + 1); + if (planksSuccess) { + const tableSuccess = await craftRecipe(bot, 'crafting_table', 1, _recursionDepth + 1); + if (tableSuccess) { + hasTable = true; + break; + } + } + } + } + } + + if (!hasTable && !hasLogs) { + log(bot, `Cannot craft ${itemName}: need a crafting table, but have no planks or logs to make one.`); + return false; + } + + if (!hasTable) { + log(bot, `Failed to create crafting table from available materials.`); + return false; + } + } + } + + // Now we should have a crafting table in inventory - place it + if (hasTable || inventory['crafting_table'] > 0 || world.getInventoryCounts(bot)['crafting_table'] > 0) { + let pos = world.getNearestFreeSpace(bot, 1, 6); + if (pos) { + await placeBlock(bot, 'crafting_table', pos.x, pos.y, pos.z); + craftingTable = world.getNearestBlock(bot, 'crafting_table', craftingTableRange); + if (craftingTable) { + recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, craftingTable); + placedTable = true; + } + } else { + log(bot, `No suitable place to put crafting table nearby.`); + return false; + } + } + } else { + // Found existing crafting table recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, craftingTable); } } + + // Final check - do we have valid recipes? If not, try to craft missing ingredients 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][0]).map(([key, value]) => `${key}: ${value}`).join(', ')}.`); - if (placedTable) { - await collectBlock(bot, 'crafting_table', 1); + const requiredItems = allRecipes[0][0]; + inventory = world.getInventoryCounts(bot); + const missing = []; + + for (const [ingredient, count] of Object.entries(requiredItems)) { + const have = inventory[ingredient] || 0; + if (have < count) { + missing.push({ item: ingredient, need: count, have: have, needed: count - have }); + } + } + + // Try to craft missing ingredients + if (missing.length > 0 && _recursionDepth < MAX_RECURSION) { + log(bot, `Missing for ${itemName}: ${missing.map(m => `${m.item}`).join(', ')}. Trying to craft them...`); + let craftedAny = false; + + for (const missingItem of missing) { + const ingredientRecipes = mc.getItemCraftingRecipes(missingItem.item); + if (ingredientRecipes && ingredientRecipes.length > 0) { + const success = await craftRecipe(bot, missingItem.item, missingItem.needed, _recursionDepth + 1); + if (success) { + craftedAny = true; + } + } + } + + if (craftedAny) { + // Refresh recipes after crafting prerequisites + if (craftingTable) { + recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, craftingTable); + } else { + recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, null); + } + } + } + + // Final final check + if (!recipes || recipes.length === 0) { + inventory = world.getInventoryCounts(bot); + const stillMissing = []; + for (const [ingredient, count] of Object.entries(requiredItems)) { + const have = inventory[ingredient] || 0; + if (have < count) { + stillMissing.push(`${ingredient} (need ${count}, have ${have})`); + } + } + log(bot, `Cannot craft ${itemName}. Required: ${stillMissing.length > 0 ? stillMissing.join(', ') : Object.entries(requiredItems).map(([k, v]) => `${k}: ${v}`).join(', ')}`); + if (placedTable) { + await collectBlock(bot, 'crafting_table', 1); + } + return false; } - return false; } + // Go to crafting table if needed if (craftingTable && bot.entity.position.distanceTo(craftingTable.position) > 4) { await goToNearestBlock(bot, 'crafting_table', 4, craftingTableRange); } const recipe = recipes[0]; - console.log('crafting...'); - //Check that the agent has sufficient items to use the recipe `num` times. - const inventory = world.getInventoryCounts(bot); //Items in the agents inventory - const requiredIngredients = mc.ingredientsFromPrismarineRecipe(recipe); //Items required to use the recipe once. - const craftLimit = mc.calculateLimitingResource(inventory, requiredIngredients); + console.log('[CRAFT] Using recipe:', recipe); + + // Check that the agent has sufficient items to use the recipe `num` times + let currentInventory = world.getInventoryCounts(bot); + const requiredIngredients = mc.ingredientsFromPrismarineRecipe(recipe); + let craftLimit = mc.calculateLimitingResource(currentInventory, requiredIngredients); + + if (craftLimit.num === 0) { + // Try to craft the limiting resource first + const limitingItem = craftLimit.limitingResource; + const limitingRecipes = mc.getItemCraftingRecipes(limitingItem); + + if (limitingRecipes && limitingRecipes.length > 0 && _recursionDepth < MAX_RECURSION) { + log(bot, `Missing ${limitingItem} for ${itemName}. Attempting to craft it...`); + + // Calculate how many we need + const needed = requiredIngredients[limitingItem] || 1; + const success = await craftRecipe(bot, limitingItem, needed * num, _recursionDepth + 1); + + if (success) { + // Recalculate craft limit after crafting prerequisite + currentInventory = world.getInventoryCounts(bot); + craftLimit = mc.calculateLimitingResource(currentInventory, requiredIngredients); + } + } + + // Still can't craft + if (craftLimit.num === 0) { + log(bot, `Cannot craft ${itemName}: not enough ${craftLimit.limitingResource} (cannot be crafted or missing materials).`); + if (placedTable) { + await collectBlock(bot, 'crafting_table', 1); + } + return false; + } + } + + try { + // Get inventory before crafting to calculate what was made + const inventoryBefore = world.getInventoryCounts(bot); + const countBefore = inventoryBefore[itemName] || 0; + + // Calculate expected output based on recipe + const expectedOutput = recipe.result ? recipe.result.count : 1; + const craftAmount = Math.min(craftLimit.num, num); + const expectedTotal = countBefore + (expectedOutput * craftAmount); + + console.log(`[CRAFT] Before craft - ${itemName}: ${countBefore}, expecting to craft ${craftAmount} (output: ${expectedOutput} each)`); + + await bot.craft(recipe, craftAmount, craftingTable); + + // Wait for crafting animation/transaction to complete + await new Promise(resolve => setTimeout(resolve, 300)); + await bot.waitForTicks(10); + + // Force collect any items left in crafting slots by closing any open window + if (bot.currentWindow) { + try { + await bot.closeWindow(bot.currentWindow); + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (e) { + // Ignore close window errors + } + } + + // Check crafting output slot (slot 0) and try to collect if items are stuck there + const craftingOutputSlot = bot.inventory.slots[0]; + if (craftingOutputSlot && craftingOutputSlot.name === itemName) { + console.log(`[CRAFT] Items stuck in crafting output slot! Attempting to collect...`); + try { + // Click on the crafting output to collect items + await bot.clickWindow(0, 0, 0); // Left click on slot 0 + await new Promise(resolve => setTimeout(resolve, 200)); + await bot.waitForTicks(5); + } catch (e) { + console.log(`[CRAFT] Could not collect from crafting slot: ${e.message}`); + } + } + + // Final wait and inventory check + await new Promise(resolve => setTimeout(resolve, 200)); + await bot.waitForTicks(5); + + // Try to get the count, with fallback to expected value + let newCount = world.getInventoryCounts(bot)[itemName] || 0; + + console.log(`[CRAFT] After craft - ${itemName}: ${newCount}`); + + // If inventory still shows 0 but we know we crafted, check more carefully + if (newCount === 0 && expectedOutput > 0) { + // Double-check by looking at raw inventory slots + for (const item of bot.inventory.items()) { + if (item && item.name === itemName) { + newCount += item.count; + } + } + + // Also check all slots directly + if (newCount === 0) { + for (let slot = 9; slot < 45; slot++) { + const item = bot.inventory.slots[slot]; + if (item && item.name === itemName) { + newCount += item.count; + } + } + } + + // If STILL 0, use expected value for display purposes + if (newCount === 0) { + console.log(`[CRAFT] WARNING: Inventory sync issue! Items may have been lost or are in an unknown location.`); + console.log(`[CRAFT] Expected: ${expectedTotal}, Actual inventory count: 0`); + // Don't fake the count - report the problem honestly + } + } + + if (craftLimit.num < num) { + log(bot, `Not enough ${craftLimit.limitingResource} to craft ${num}, crafted ${craftLimit.num}. You now have ${newCount} ${itemName}.`); + } else { + log(bot, `Successfully crafted ${itemName}, you now have ${newCount} ${itemName}.`); + } + + // Return true even if count shows 0 - the craft itself succeeded + // The item might appear in inventory on next tick + } catch (err) { + log(bot, `Error crafting ${itemName}: ${err.message}`); + if (placedTable) { + await collectBlock(bot, 'crafting_table', 1); + } + return false; + } - await bot.craft(recipe, Math.min(craftLimit.num, num), craftingTable); - if(craftLimit.num= 4 && enemy.name !== 'creeper' && enemy.name !== 'phantom') { try { - bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setMovements(createSafeMovements(bot)); await bot.pathfinder.goto(new pf.goals.GoalFollow(enemy, 3.5), true); } catch (err) {/* might error if entity dies, ignore */} } if (bot.entity.position.distanceTo(enemy.position) <= 2) { try { - bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setMovements(createSafeMovements(bot)); let inverted_goal = new pf.goals.GoalInvert(new pf.goals.GoalFollow(enemy, 2)); await bot.pathfinder.goto(inverted_goal, true); } catch (err) {/* might error if entity dies, ignore */} @@ -442,7 +793,7 @@ export async function collectBlock(bot, blockType, num=1, exclude=null) { let collected = 0; - const movements = new pf.Movements(bot); + const movements = createSafeMovements(bot); movements.dontMineUnderFallingBlock = false; movements.dontCreateFlow = true; @@ -541,7 +892,7 @@ export async function pickupNearbyItems(bot) { let nearestItem = getNearestItem(bot); let pickedUp = 0; while (nearestItem) { - let movements = new pf.Movements(bot); + let movements = createSafeMovements(bot); movements.canDig = false; bot.pathfinder.setMovements(movements); await goToGoal(bot, new pf.goals.GoalFollow(nearestItem, 1)); @@ -583,7 +934,7 @@ export async function breakBlockAt(bot, x, y, z) { if (bot.entity.position.distanceTo(block.position) > 4.5) { let pos = block.position; - let movements = new pf.Movements(bot); + let movements = createSafeMovements(bot); movements.canPlaceOn = false; movements.allow1by1towers = false; bot.pathfinder.setMovements(movements); @@ -758,13 +1109,13 @@ export async function placeBlock(bot, blockType, x, y, z, placeOn='bottom', dont // too close let goal = new pf.goals.GoalNear(targetBlock.position.x, targetBlock.position.y, targetBlock.position.z, 2); let inverted_goal = new pf.goals.GoalInvert(goal); - bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setMovements(createSafeMovements(bot)); await bot.pathfinder.goto(inverted_goal); } if (bot.entity.position.distanceTo(targetBlock.position) > 4.5) { // too far let pos = targetBlock.position; - let movements = new pf.Movements(bot); + let movements = createSafeMovements(bot); bot.pathfinder.setMovements(movements); await goToGoal(bot, new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); } @@ -970,7 +1321,7 @@ export async function viewChest(bot) { return true; } -export async function consume(bot, itemName="") { +export async function eat(bot, itemName="") { /** * Eat/drink the given item. * @param {MinecraftBot} bot, reference to the minecraft bot. @@ -990,7 +1341,7 @@ export async function consume(bot, itemName="") { } await bot.equip(item, 'hand'); await bot.consume(); - log(bot, `Consumed ${item.name}.`); + log(bot, `Ate ${item.name}.`); return true; } @@ -1070,37 +1421,63 @@ export async function giveToPlayer(bot, itemType, username, num=1) { export async function goToGoal(bot, goal) { /** * Navigate to the given goal. Use doors and attempt minimally destructive movements. + * Paper-server safe: uses slower, more human-like movements. * @param {MinecraftBot} bot, reference to the minecraft bot. * @param {pf.goals.Goal} goal, the goal to navigate to. **/ - const nonDestructiveMovements = new pf.Movements(bot); + const nonDestructiveMovements = createSafeMovements(bot); const dontBreakBlocks = ['glass', 'glass_pane']; for (let block of dontBreakBlocks) { nonDestructiveMovements.blocksCantBreak.add(mc.getBlockId(block)); } nonDestructiveMovements.placeCost = 2; nonDestructiveMovements.digCost = 10; - - const destructiveMovements = new pf.Movements(bot); + // Paper anti-cheat safe settings + nonDestructiveMovements.allowSprinting = false; + nonDestructiveMovements.allowParkour = false; + nonDestructiveMovements.canDig = false; + nonDestructiveMovements.allow1by1towers = false; // Towers can trigger fly-kick + nonDestructiveMovements.allowFreeMotion = false; // Free motion can look like flying + nonDestructiveMovements.scafoldingBlocks = []; // Don't scaffold - looks suspicious + + const destructiveMovements = createSafeMovements(bot); + // Paper anti-cheat safe settings + destructiveMovements.allowSprinting = false; + destructiveMovements.allowParkour = false; + destructiveMovements.allow1by1towers = false; + destructiveMovements.allowFreeMotion = false; + destructiveMovements.maxDropDown = 3; // Don't drop too far - triggers damage checks let final_movements = destructiveMovements; - const pathfind_timeout = 1000; - if (await bot.pathfinder.getPathTo(nonDestructiveMovements, goal, pathfind_timeout).status === 'success') { - final_movements = nonDestructiveMovements; - log(bot, `Found non-destructive path.`); - } - else if (await bot.pathfinder.getPathTo(destructiveMovements, goal, pathfind_timeout).status === 'success') { - log(bot, `Found destructive path.`); - } - else { - log(bot, `Path not found, but attempting to navigate anyway using destructive movements.`); + // Increased timeout for complex paths (prevents "Took too long" errors) + const pathfind_timeout = 3000; + + try { + const nonDestructivePath = await bot.pathfinder.getPathTo(nonDestructiveMovements, goal, pathfind_timeout); + if (nonDestructivePath && nonDestructivePath.status === 'success') { + final_movements = nonDestructiveMovements; + log(bot, `Found non-destructive path.`); + } else { + const destructivePath = await bot.pathfinder.getPathTo(destructiveMovements, goal, pathfind_timeout); + if (destructivePath && destructivePath.status === 'success') { + log(bot, `Found destructive path.`); + } else { + log(bot, `Path not found, but attempting to navigate anyway using destructive movements.`); + } + } + } catch (pathErr) { + log(bot, `Path calculation error: ${pathErr.message}. Attempting simple navigation.`); } const doorCheckInterval = startDoorInterval(bot); bot.pathfinder.setMovements(final_movements); + + // Add small delay before starting movement (helps with Paper anti-cheat) + await new Promise(r => setTimeout(r, 100)); + try { await bot.pathfinder.goto(goal); clearInterval(doorCheckInterval); @@ -1173,7 +1550,7 @@ function startDoorInterval(bot) { } prev_pos = bot.entity.position.clone(); prev_check = now; - }, 200); + }, 500); _doorInterval = doorCheckInterval; return doorCheckInterval; } @@ -1181,6 +1558,7 @@ function startDoorInterval(bot) { export async function goToPosition(bot, x, y, z, min_distance=2) { /** * Navigate to the given position. + * Paper-safe: handles long distances by breaking into segments. * @param {MinecraftBot} bot, reference to the minecraft bot. * @param {number} x, the x coordinate to navigate to. If null, the bot's current x coordinate will be used. * @param {number} y, the y coordinate to navigate to. If null, the bot's current y coordinate will be used. @@ -1201,6 +1579,50 @@ export async function goToPosition(bot, x, y, z, min_distance=2) { return true; } + const targetPos = new Vec3(x, y, z); + const currentPos = bot.entity.position; + const totalDistance = currentPos.distanceTo(targetPos); + + // For long distances (>64 blocks), navigate in segments to avoid timeout + const MAX_SEGMENT = 64; + if (totalDistance > MAX_SEGMENT) { + log(bot, `Long distance detected (${Math.round(totalDistance)} blocks). Navigating in segments...`); + + // Calculate direction vector + const direction = targetPos.minus(currentPos).normalize(); + let currentTarget = currentPos.clone(); + let attempts = 0; + const maxAttempts = Math.ceil(totalDistance / MAX_SEGMENT) + 2; + + while (bot.entity.position.distanceTo(targetPos) > min_distance + 5 && attempts < maxAttempts) { + attempts++; + + // Move toward target in segments + const remaining = bot.entity.position.distanceTo(targetPos); + const segmentDist = Math.min(MAX_SEGMENT, remaining); + + currentTarget = bot.entity.position.plus(direction.scaled(segmentDist)); + + log(bot, `Segment ${attempts}: Moving ${Math.round(segmentDist)} blocks...`); + + try { + await goToGoal(bot, new pf.goals.GoalNear(currentTarget.x, currentTarget.y, currentTarget.z, 3)); + // Small pause between segments (helps with Paper anti-cheat) + await new Promise(r => setTimeout(r, 200)); + } catch (err) { + log(bot, `Segment failed: ${err.message}. Trying to continue...`); + // Try to move a bit anyway + await new Promise(r => setTimeout(r, 500)); + } + + // Check if we're making progress + if (bot.entity.position.distanceTo(targetPos) >= remaining - 2) { + log(bot, `Not making progress, stopping segmented navigation.`); + break; + } + } + } + const checkDigProgress = () => { if (bot.targetDigBlock) { const targetBlock = bot.targetDigBlock; @@ -1341,7 +1763,7 @@ export async function followPlayer(bot, username, distance=4) { if (!player) return false; - const move = new pf.Movements(bot); + const move = createSafeMovements(bot); move.digCost = 10; bot.pathfinder.setMovements(move); let doorCheckInterval = startDoorInterval(bot); @@ -1397,20 +1819,34 @@ export async function followPlayer(bot, username, distance=4) { export async function moveAway(bot, distance) { /** * Move away from current position in any direction. + * Paper-safe: limits distance and uses safe movements. * @param {MinecraftBot} bot, reference to the minecraft bot. * @param {number} distance, the distance to move away. * @returns {Promise} true if the bot moved away, false otherwise. * @example * await skills.moveAway(bot, 8); **/ + // Limit distance to prevent long pathfinding calculations that can cause disconnects + const maxSafeDistance = 32; + if (distance > maxSafeDistance) { + log(bot, `Limiting moveAway distance from ${distance} to ${maxSafeDistance} for safety.`); + distance = maxSafeDistance; + } + const pos = bot.entity.position; let goal = new pf.goals.GoalNear(pos.x, pos.y, pos.z, distance); let inverted_goal = new pf.goals.GoalInvert(goal); - bot.pathfinder.setMovements(new pf.Movements(bot)); + + const safeMovements = createSafeMovements(bot); + safeMovements.allowSprinting = false; + safeMovements.allowParkour = false; + safeMovements.allow1by1towers = false; + safeMovements.maxDropDown = 3; + bot.pathfinder.setMovements(safeMovements); if (bot.modes.isOn('cheat')) { - const move = new pf.Movements(bot); - const path = await bot.pathfinder.getPathTo(move, inverted_goal, 10000); + const move = createSafeMovements(bot); + const path = await bot.pathfinder.getPathTo(move, inverted_goal, 5000); let last_move = path.path[path.path.length-1]; if (last_move) { let x = Math.floor(last_move.x); @@ -1421,7 +1857,15 @@ export async function moveAway(bot, distance) { } } - await goToGoal(bot, inverted_goal); + // Add small delay before moving (helps with Paper anti-cheat) + await new Promise(r => setTimeout(r, 50)); + + try { + await goToGoal(bot, inverted_goal); + } catch (err) { + log(bot, `MoveAway error: ${err.message}`); + } + let new_pos = bot.entity.position; log(bot, `Moved away from ${pos.floored()} to ${new_pos.floored()}.`); return true; @@ -1430,15 +1874,31 @@ export async function moveAway(bot, distance) { export async function moveAwayFromEntity(bot, entity, distance=16) { /** * Move away from the given entity. + * Paper-safe: uses safe movements. * @param {MinecraftBot} bot, reference to the minecraft bot. * @param {Entity} entity, the entity to move away from. * @param {number} distance, the distance to move away. * @returns {Promise} true if the bot moved away, false otherwise. **/ + // Limit distance for safety + if (distance > 32) distance = 32; + let goal = new pf.goals.GoalFollow(entity, distance); let inverted_goal = new pf.goals.GoalInvert(goal); - bot.pathfinder.setMovements(new pf.Movements(bot)); - await bot.pathfinder.goto(inverted_goal); + + const safeMovements = createSafeMovements(bot); + safeMovements.allowSprinting = false; + safeMovements.allowParkour = false; + bot.pathfinder.setMovements(safeMovements); + + // Add small delay before moving + await new Promise(r => setTimeout(r, 50)); + + try { + await bot.pathfinder.goto(inverted_goal); + } catch (err) { + log(bot, `MoveAwayFromEntity error: ${err.message}`); + } return true; } @@ -1456,7 +1916,7 @@ export async function avoidEnemies(bot, distance=16) { while (enemy) { const follow = new pf.goals.GoalFollow(enemy, distance+1); // move a little further away const inverted_goal = new pf.goals.GoalInvert(follow); - bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setMovements(createSafeMovements(bot)); bot.pathfinder.setGoal(inverted_goal, true); await new Promise(resolve => setTimeout(resolve, 500)); enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), distance); @@ -1620,7 +2080,7 @@ export async function tillAndSow(bot, x, y, z, seedType=null) { // if distance is too far, move to the block if (bot.entity.position.distanceTo(block.position) > 4.5) { let pos = block.position; - bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setMovements(createSafeMovements(bot)); await goToGoal(bot, new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); } if (block.name !== 'farmland') { @@ -1665,7 +2125,7 @@ export async function activateNearestBlock(bot, type) { } if (bot.entity.position.distanceTo(block.position) > 4.5) { let pos = block.position; - bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setMovements(createSafeMovements(bot)); await goToGoal(bot, new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); } await bot.activateBlock(block); diff --git a/src/agent/library/world.js b/src/agent/library/world.js index d993a0931..731489b6c 100644 --- a/src/agent/library/world.js +++ b/src/agent/library/world.js @@ -261,11 +261,19 @@ export function getVillagerProfession(entity) { export function getInventoryStacks(bot) { let inventory = []; + // Get items from main inventory (slots 9-44) for (const item of bot.inventory.items()) { if (item != null) { inventory.push(item); } } + // Also check hotbar and armor slots explicitly + for (let slot = 0; slot < bot.inventory.slots.length; slot++) { + const item = bot.inventory.slots[slot]; + if (item != null && !inventory.find(i => i.slot === item.slot)) { + inventory.push(item); + } + } return inventory; } @@ -281,14 +289,75 @@ export function getInventoryCounts(bot) { * let hasWoodenPickaxe = inventory['wooden_pickaxe'] > 0; **/ let inventory = {}; - for (const item of bot.inventory.items()) { - if (item != null) { + + // Method 1: Check ALL slots directly - this is the most reliable method + // Minecraft inventory slots: + // 0: crafting output, 1-4: crafting input, 5-8: armor + // 9-35: main inventory, 36-44: hotbar + if (bot.inventory && bot.inventory.slots) { + for (let slot = 0; slot < bot.inventory.slots.length; slot++) { + const item = bot.inventory.slots[slot]; + if (item != null && item.name) { + // Skip crafting output slot (0) as it's temporary + if (slot === 0) continue; + + if (inventory[item.name] == null) { + inventory[item.name] = 0; + } + inventory[item.name] += item.count; + } + } + } + + // Method 2: Also use items() as backup in case slots missed something + if (bot.inventory && typeof bot.inventory.items === 'function') { + for (const item of bot.inventory.items()) { + if (item != null && item.name) { + // Check if we already counted this slot + const existingCount = inventory[item.name] || 0; + // Only add if the slot wasn't counted (items() returns main inventory + hotbar) + // slots 9-44 are what items() returns + if (item.slot >= 9 && item.slot <= 44) { + // This should already be counted from slots, but verify + const slotItem = bot.inventory.slots[item.slot]; + if (!slotItem) { + // Item exists in items() but not in slots - add it + if (inventory[item.name] == null) { + inventory[item.name] = 0; + } + inventory[item.name] += item.count; + } + } + } + } + } + + // Method 3: Check cursor/held item (item being moved with mouse) + if (bot.inventory && bot.inventory.selectedItem) { + const item = bot.inventory.selectedItem; + if (item && item.name) { if (inventory[item.name] == null) { inventory[item.name] = 0; } inventory[item.name] += item.count; } } + + // Method 4: Check the cursor slot directly (slot -1 in some implementations) + try { + if (bot.currentWindow && bot.currentWindow.selectedItem) { + const item = bot.currentWindow.selectedItem; + if (item && item.name) { + if (inventory[item.name] == null) { + inventory[item.name] = 0; + } + inventory[item.name] += item.count; + } + } + } catch (e) { + // Ignore errors from accessing currentWindow + } + return inventory; } diff --git a/src/agent/modes.js b/src/agent/modes.js index 21b7b955e..2f2f422f3 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -1,4 +1,4 @@ -import * as skills from './library/skills.js'; +import * as skills from './library/skills.js'; import * as world from './library/world.js'; import * as mc from '../utils/mcdata.js'; import settings from './settings.js' @@ -10,6 +10,74 @@ async function say(agent, message) { agent.openChat(message); } +// Helper: Check if it's nighttime in Minecraft (13000-23000 ticks or monsters spawning) +function isNightTime(bot) { + const time = bot.time.timeOfDay; + return time >= 13000 && time <= 23000; +} + +// Helper: Get best food item from inventory (prioritizes saturation) +function getBestFood(bot) { + const validFoods = [ + 'golden_apple', 'enchanted_golden_apple', 'cooked_beef', 'cooked_porkchop', + 'cooked_mutton', 'cooked_salmon', 'cooked_chicken', 'cooked_rabbit', 'cooked_cod', + 'bread', 'baked_potato', 'pumpkin_pie', 'golden_carrot', 'apple', 'carrot', + 'melon_slice', 'sweet_berries', 'glow_berries', 'cookie', 'beetroot', + 'dried_kelp', 'raw_beef', 'raw_porkchop', 'raw_mutton', 'raw_chicken', + 'raw_rabbit', 'raw_salmon', 'raw_cod', 'potato', 'rotten_flesh' + ]; + + for (const foodName of validFoods) { + const food = bot.inventory.items().find(item => item.name === foodName); + if (food) return food; + } + return null; +} + +// Helper: Get safest escape direction (away from enemies, towards light/open areas) +function getSafeEscapePosition(bot, enemies, distance = 24) { + const pos = bot.entity.position; + let bestDir = null; + let bestScore = -Infinity; + + // Check 8 directions + const directions = [ + [1, 0], [-1, 0], [0, 1], [0, -1], + [1, 1], [1, -1], [-1, 1], [-1, -1] + ]; + + for (const [dx, dz] of directions) { + const targetPos = pos.offset(dx * distance, 0, dz * distance); + let score = 0; + + // Score based on distance from enemies + for (const enemy of enemies) { + if (enemy && enemy.position) { + score += targetPos.distanceTo(enemy.position); + } + } + + // Prefer positions with solid ground + const groundBlock = bot.blockAt(targetPos.offset(0, -1, 0)); + if (groundBlock && groundBlock.name !== 'air' && groundBlock.name !== 'water' && groundBlock.name !== 'lava') { + score += 50; + } + + // Avoid lava/water + const targetBlock = bot.blockAt(targetPos); + if (targetBlock && (targetBlock.name === 'lava' || targetBlock.name === 'water')) { + score -= 1000; + } + + if (score > bestScore) { + bestScore = score; + bestDir = { x: targetPos.x, y: targetPos.y, z: targetPos.z }; + } + } + + return bestDir; +} + // a mode is a function that is called every tick to respond immediately to the world // it has the following fields: // on: whether 'update' is called every tick @@ -87,6 +155,195 @@ const modes_list = [ } } }, + { + name: 'fall_protection', + description: 'Use water bucket when falling from height (MLG water). Critical survival skill.', + interrupts: ['all'], + on: true, + active: false, + lastY: null, + fallStartY: null, + update: async function (agent) { + const bot = agent.bot; + const pos = bot.entity.position; + const vel = bot.entity.velocity; + + // Detect if falling (negative Y velocity) + if (vel.y < -0.5) { + if (this.fallStartY === null) { + this.fallStartY = pos.y; + } + + const fallDistance = this.fallStartY - pos.y; + + // If falling more than 10 blocks and going fast, try MLG water + if (fallDistance > 10 && vel.y < -0.8) { + const waterBucket = bot.inventory.items().find(item => item.name === 'water_bucket'); + if (waterBucket && !this.active) { + // Check if ground is near (within 4 blocks) + const groundBlock = bot.blockAt(pos.offset(0, -4, 0)); + if (groundBlock && groundBlock.name !== 'air' && groundBlock.name !== 'water') { + execute(this, agent, async () => { + say(agent, '¡Agua MLG!'); + try { + await bot.equip(waterBucket, 'hand'); + await bot.lookAt(pos.offset(0, -3, 0)); + bot.activateItem(); + await new Promise(r => setTimeout(r, 500)); + // Pick up water + const waterBlock = world.getNearestBlock(bot, 'water', 3); + if (waterBlock) { + await bot.lookAt(waterBlock.position); + bot.activateItem(); + } + } catch (e) { /* ignore */ } + }); + } + } + } + } else { + this.fallStartY = null; + } + + this.lastY = pos.y; + } + }, + { + name: 'auto_eat', + description: 'Automatically eat food when hunger is low. Essential for survival.', + interrupts: ['action:followPlayer', 'action:collectBlocks', 'action:goToPosition'], + on: true, + active: false, + lastEatTime: 0, + update: async function (agent) { + const bot = agent.bot; + + // Don't eat too frequently (cooldown 30 seconds) + if (Date.now() - this.lastEatTime < 30000) return; + + // Eat when hunger <= 15 (human-like behavior - we eat before starving) + if (bot.food <= 15) { + const food = getBestFood(bot); + if (food) { + this.lastEatTime = Date.now(); + execute(this, agent, async () => { + say(agent, `Tengo hambre, comiendo ${food.name.replace(/_/g, ' ')}...`); + try { + await bot.equip(food, 'hand'); + bot.deactivateItem(); + bot.activateItem(); + // Wait for eating animation (about 1.6 seconds in Minecraft) + await new Promise(r => setTimeout(r, 2000)); + bot.deactivateItem(); + say(agent, '¡Delicioso!'); + } catch (e) { + console.log('[AUTO_EAT] Error:', e.message); + } + }); + } + } + } + }, + { + name: 'auto_sleep', + description: 'Automatically sleep in a bed when night falls to avoid monsters.', + interrupts: ['action:followPlayer'], + on: true, + active: false, + lastSleepCheck: 0, + update: async function (agent) { + const bot = agent.bot; + + // Only check every 10 seconds + if (Date.now() - this.lastSleepCheck < 10000) return; + this.lastSleepCheck = Date.now(); + + // Check if it's night and we're not already sleeping + if (isNightTime(bot) && !bot.isSleeping) { + // Look for a bed within 32 blocks + const beds = bot.findBlocks({ + matching: (block) => block.name.includes('bed'), + maxDistance: 32, + count: 1 + }); + + if (beds.length > 0) { + execute(this, agent, async () => { + say(agent, 'Es de noche, voy a dormir...'); + try { + await skills.goToBed(bot); + } catch (e) { + // Bed might be occupied or obstructed + if (e.message && e.message.includes('occupied')) { + say(agent, 'La cama está ocupada.'); + } + } + }); + } + } + } + }, + { + name: 'smart_armor', + description: 'Automatically equip the best armor available before combat.', + interrupts: [], + on: true, + active: false, + lastCheck: 0, + update: async function (agent) { + const bot = agent.bot; + + // Only check every 5 seconds + if (Date.now() - this.lastCheck < 5000) return; + this.lastCheck = Date.now(); + + // Check if there's danger nearby + const enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), 24); + + if (enemy || !agent.isIdle()) { + // Use mineflayer-armor-manager to equip best armor + try { + bot.armorManager.equipAll(); + } catch (e) { /* ignore */ } + } + } + }, + { + name: 'inventory_manager', + description: 'Manage inventory by dropping junk items when full.', + interrupts: [], + on: true, + active: false, + lastCheck: 0, + junkItems: ['rotten_flesh', 'poisonous_potato', 'spider_eye', 'pufferfish', + 'dead_bush', 'dune_armor_trim', 'tide_armor_trim', 'netherite_upgrade_smithing_template'], + update: async function (agent) { + const bot = agent.bot; + + // Only check every 30 seconds + if (Date.now() - this.lastCheck < 30000) return; + this.lastCheck = Date.now(); + + // Check if inventory is nearly full (less than 3 empty slots) + const emptySlots = bot.inventory.emptySlotCount(); + + if (emptySlots <= 3) { + // Find junk items to drop + for (const junkName of this.junkItems) { + const junkItem = bot.inventory.items().find(item => item.name === junkName); + if (junkItem) { + execute(this, agent, async () => { + say(agent, `Tirando ${junkItem.name.replace(/_/g, ' ')} para hacer espacio...`); + try { + await bot.toss(junkItem.type, null, junkItem.count); + } catch (e) { /* ignore */ } + }); + return; // Drop one type at a time + } + } + } + } + }, { name: 'unstuck', description: 'Attempt to get unstuck when in the same place for a while. Interrupts some actions.', @@ -97,7 +354,7 @@ const modes_list = [ distance: 2, stuck_time: 0, last_time: Date.now(), - max_stuck_time: 20, + max_stuck_time: 60, prev_dig_block: null, update: async function (agent) { if (agent.isIdle()) { @@ -120,13 +377,129 @@ const modes_list = [ } const max_stuck_time = cur_dig_block?.name === 'obsidian' ? this.max_stuck_time * 2 : this.max_stuck_time; if (this.stuck_time > max_stuck_time) { - say(agent, 'I\'m stuck!'); this.stuck_time = 0; + this.unstuck_attempts = (this.unstuck_attempts || 0) + 1; + execute(this, agent, async () => { - const crashTimeout = setTimeout(() => { agent.cleanKill("Got stuck and couldn't get unstuck") }, 10000); - await skills.moveAway(bot, 5); + const crashTimeout = setTimeout(() => { console.log('[UNSTUCK] Timeout'); }, 45000); + + try { + const pos = bot.entity.position; + + // Check if we're underground (can't see sky) + let canSeeSky = false; + for (let y = 1; y <= 50; y++) { + const blockUp = bot.blockAt(pos.offset(0, y, 0)); + if (!blockUp || blockUp.name === 'air') { + canSeeSky = true; + break; + } + if (blockUp.name !== 'air' && blockUp.name !== 'cave_air') { + break; + } + } + + // Check if we have tools to dig + const hasPickaxe = bot.inventory.items().some(i => i.name.includes('pickaxe')); + + // Estrategia 1: Saltar primero + if (this.unstuck_attempts === 1) { + say(agent, 'Estoy atascado, saltando...'); + bot.setControlState('jump', true); + bot.setControlState('forward', true); + await new Promise(r => setTimeout(r, 1000)); + bot.clearControlStates(); + await skills.moveAway(bot, 3); + } + // Estrategia 2: Romper bloques alrededor (prioritize upward if underground) + else if (this.unstuck_attempts <= 3) { + say(agent, 'Rompiendo bloques...'); + // If underground, try to dig up first + const dirsUnderground = [[0,1,0],[0,2,0],[1,0,0],[-1,0,0],[0,0,1],[0,0,-1]]; + const dirsSurface = [[1,0,0],[-1,0,0],[0,0,1],[0,0,-1],[1,1,0],[-1,1,0],[0,1,1],[0,1,-1]]; + const dirs = !canSeeSky ? dirsUnderground : dirsSurface; + + for (const [dx,dy,dz] of dirs) { + const block = bot.blockAt(pos.offset(dx, dy, dz)); + if (block && block.name !== 'air' && block.name !== 'water' && block.name !== 'lava') { + // Check if we CAN break this block + const needsPickaxe = ['stone', 'cobblestone', 'andesite', 'diorite', 'granite', 'deepslate'].some(n => block.name.includes(n)); + if (needsPickaxe && !hasPickaxe) { + // Can't break stone without pickaxe, try another direction + continue; + } + if (block.hardness < 10 && block.hardness >= 0) { + try { + await skills.breakBlockAt(bot, pos.x+dx, pos.y+dy, pos.z+dz); + break; + } catch(e) {} + } + } + } + await skills.moveAway(bot, 4); + } + // Estrategia 3: If underground, try to dig staircase up + else if (this.unstuck_attempts <= 5) { + if (!canSeeSky && hasPickaxe) { + say(agent, 'Cavando escalera hacia arriba...'); + // Dig a staircase pattern upward + for (let step = 0; step < 3; step++) { + const upBlock = bot.blockAt(pos.offset(step, 1 + step, step)); + const upBlock2 = bot.blockAt(pos.offset(step, 2 + step, step)); + if (upBlock && upBlock.name !== 'air') { + try { await skills.breakBlockAt(bot, pos.x+step, pos.y+1+step, pos.z+step); } catch(e) {} + } + if (upBlock2 && upBlock2.name !== 'air') { + try { await skills.breakBlockAt(bot, pos.x+step, pos.y+2+step, pos.z+step); } catch(e) {} + } + // Move forward and up + bot.setControlState('jump', true); + bot.setControlState('forward', true); + await new Promise(r => setTimeout(r, 500)); + bot.clearControlStates(); + } + } else { + say(agent, 'Cavando...'); + const below = bot.blockAt(pos.offset(0, -1, 0)); + if (below && below.name !== 'air' && below.hardness < 10) { + try { await skills.breakBlockAt(bot, pos.x, pos.y-1, pos.z); } catch(e) {} + } + } + await skills.moveAway(bot, 5); + } + // Estrategia 4: Pillar up if underground + else { + const inv = bot.inventory.items(); + const block = inv.find(i => ['cobblestone','dirt','stone','netherrack','deepslate'].includes(i.name)); + + if (!canSeeSky && block) { + say(agent, 'Construyendo hacia arriba...'); + // Pillar up several blocks + for (let i = 0; i < 5; i++) { + try { + bot.setControlState('jump', true); + await new Promise(r => setTimeout(r, 250)); + await skills.placeBlock(bot, block.name, Math.floor(pos.x), Math.floor(pos.y) - 1 + i, Math.floor(pos.z), 'bottom', false); + } catch(e) { break; } + } + bot.clearControlStates(); + } else { + say(agent, 'Buscando salida...'); + if (block) { + try { + bot.setControlState('jump', true); + await new Promise(r => setTimeout(r, 200)); + await skills.placeBlock(bot, block.name, pos.x, pos.y-1, pos.z, 'bottom', false); + bot.clearControlStates(); + } catch(e) {} + } + } + await skills.moveAway(bot, 6); + this.unstuck_attempts = 0; + } + say(agent, 'Me libere.'); + } catch(e) { console.log('[UNSTUCK] Error:', e.message); } clearTimeout(crashTimeout); - say(agent, 'I\'m free.'); }); } this.last_time = Date.now(); @@ -139,79 +512,323 @@ const modes_list = [ }, { name: 'cowardice', - description: 'Run away from enemies. Interrupts all actions.', + description: 'Run away from dangerous enemies to safe positions. More intelligent escape.', interrupts: ['all'], on: true, active: false, + lastFleeTime: 0, update: async function (agent) { - const enemy = world.getNearestEntityWhere(agent.bot, entity => mc.isHostile(entity), 16); - if (enemy && await world.isClearPath(agent.bot, enemy)) { - say(agent, `Aaa! A ${enemy.name.replace("_", " ")}!`); + const bot = agent.bot; + + // Don't spam flee (cooldown 3 seconds) + if (Date.now() - this.lastFleeTime < 3000) return; + + // Detect enemies at realistic human visual range (24 blocks) + const enemies = []; + const nearbyEntities = world.getNearbyEntities(bot, 24); + for (const entity of nearbyEntities) { + if (mc.isHostile(entity)) { + enemies.push(entity); + } + } + + if (enemies.length === 0) return; + + const nearestEnemy = enemies[0]; + const distance = bot.entity.position.distanceTo(nearestEnemy.position); + + // Flee conditions: + // 1. Low health (< 8 hearts) + // 2. Multiple enemies (3+) + // 3. Creeper within 6 blocks (explosion danger) + // 4. No weapon equipped + const isCreeperClose = enemies.some(e => e.name === 'creeper' && bot.entity.position.distanceTo(e.position) < 6); + const hasWeapon = bot.inventory.items().some(i => i.name.includes('sword') || i.name.includes('axe')); + const shouldFlee = bot.health < 8 || enemies.length >= 3 || isCreeperClose || (!hasWeapon && distance < 12); + + if (shouldFlee && await world.isClearPath(bot, nearestEnemy)) { + this.lastFleeTime = Date.now(); + + const enemyName = nearestEnemy.name.replace(/_/g, ' '); + const fleeReason = isCreeperClose ? '¡Creeper!' : + enemies.length >= 3 ? '¡Muchos enemigos!' : + bot.health < 8 ? '¡Estoy herido!' : + '¡Peligro!'; + + say(agent, `${fleeReason} Huyendo de ${enemyName}!`); + execute(this, agent, async () => { - await skills.avoidEnemies(agent.bot, 24); + // Find safe escape position + const safePos = getSafeEscapePosition(bot, enemies, 28); + if (safePos) { + try { + await skills.goToPosition(bot, safePos.x, safePos.y, safePos.z, 2); + } catch (e) { + // Fallback to simple moveAway + await skills.avoidEnemies(bot, 28); + } + } else { + await skills.avoidEnemies(bot, 28); + } }); } } }, { name: 'self_defense', - description: 'Attack nearby enemies. Interrupts all actions.', + description: 'Attack nearby enemies when conditions are favorable. Smart combat.', interrupts: ['all'], on: true, active: false, + lastCombatTime: 0, update: async function (agent) { - const enemy = world.getNearestEntityWhere(agent.bot, entity => mc.isHostile(entity), 8); - if (enemy && await world.isClearPath(agent.bot, enemy)) { - say(agent, `Fighting ${enemy.name}!`); + const bot = agent.bot; + + // Combat cooldown 2 seconds + if (Date.now() - this.lastCombatTime < 2000) return; + + // Find enemies at combat range (12 blocks - realistic engagement distance) + const enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), 12); + + if (!enemy) return; + + const distance = bot.entity.position.distanceTo(enemy.position); + + // Conditions to fight: + // 1. Good health (> 6 hearts) + // 2. Enemy is close (< 10 blocks) + // 3. Not a creeper too close (let cowardice handle that) + // 4. Have some weapon or fists + const isCreeperClose = enemy.name === 'creeper' && distance < 5; + const canFight = bot.health > 6 && distance < 10 && !isCreeperClose; + + if (canFight && await world.isClearPath(bot, enemy)) { + this.lastCombatTime = Date.now(); + + // Equip best armor before combat + try { bot.armorManager.equipAll(); } catch (e) {} + + const enemyName = enemy.name.replace(/_/g, ' '); + say(agent, `¡Atacando ${enemyName}!`); + execute(this, agent, async () => { - await skills.defendSelf(agent.bot, 8); + await skills.defendSelf(bot, 12); }); } } }, { name: 'hunting', - description: 'Hunt nearby animals when idle.', + description: 'Hunt nearby animals when idle. Human-like vision range with smart pathfinding detection.', interrupts: ['action:followPlayer'], on: true, active: false, + lastHuntTime: 0, + failedAttempts: 0, // Track consecutive failures + lastFailedTarget: null, // Don't retry same target immediately + cooldownUntil: 0, // Extended cooldown after multiple failures + lastPosition: null, // Detect if we're stuck + stuckCount: 0, // Count stuck occurrences + preferredPrey: ['cow', 'pig', 'sheep', 'chicken', 'rabbit'], update: async function (agent) { - const huntable = world.getNearestEntityWhere(agent.bot, entity => mc.isHuntable(entity), 8); - if (huntable && await world.isClearPath(agent.bot, huntable)) { - execute(this, agent, async () => { - say(agent, `Hunting ${huntable.name}!`); - await skills.attackEntity(agent.bot, huntable); - }); + const bot = agent.bot; + const now = Date.now(); + + // Extended cooldown after multiple failures (exponential backoff) + if (now < this.cooldownUntil) return; + + // Normal hunt cooldown 5 seconds + if (now - this.lastHuntTime < 5000) return; + + // Check if we're stuck (same position repeatedly) + const currentPos = bot.entity.position.clone(); + if (this.lastPosition && currentPos.distanceTo(this.lastPosition) < 2) { + this.stuckCount++; + if (this.stuckCount >= 3) { + // We're stuck - don't hunt, let unstuck mode handle it + console.log('[HUNTING] Bot appears stuck, skipping hunt'); + this.cooldownUntil = now + 30000; // Wait 30 seconds + this.stuckCount = 0; + this.failedAttempts = 0; + return; + } + } else { + this.stuckCount = 0; + } + this.lastPosition = currentPos; + + // Don't hunt if monsters nearby + const enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), 16); + if (enemy) return; + + // Check if bot has tools to break blocks (needed if path is blocked) + const hasPickaxe = bot.inventory.items().some(i => i.name.includes('pickaxe')); + const hasAxe = bot.inventory.items().some(i => i.name.includes('_axe')); + const hasShovel = bot.inventory.items().some(i => i.name.includes('shovel')); + const hasTools = hasPickaxe || hasAxe || hasShovel; + + // Check if bot can see sky (is on surface) + const blockAbove = bot.blockAt(currentPos.offset(0, 2, 0)); + const canSeeSky = !blockAbove || blockAbove.name === 'air'; + + // If we've failed too many times and don't have tools, enter extended cooldown + if (this.failedAttempts >= 3 && !hasTools) { + console.log('[HUNTING] Too many failures without tools, entering extended cooldown'); + say(agent, 'No puedo cazar sin herramientas, buscaré otra cosa que hacer.'); + this.cooldownUntil = now + 60000; // 1 minute cooldown + this.failedAttempts = 0; + return; + } + + // If underground (can't see sky) and failing, prioritize getting out + if (!canSeeSky && this.failedAttempts >= 2) { + console.log('[HUNTING] Underground with failures, should surface first'); + this.cooldownUntil = now + 45000; // 45 second cooldown to let other modes work + this.failedAttempts = 0; + return; + } + + // Adjust search range based on situation + // If underground or failing, search closer + let searchRange = 60; // Default human vision range + if (!canSeeSky) searchRange = 24; // Limited vision underground + if (this.failedAttempts >= 1) searchRange = Math.max(16, searchRange - (this.failedAttempts * 15)); + + // Find prey + let huntable = null; + for (const preyName of this.preferredPrey) { + huntable = world.getNearestEntityWhere(bot, entity => + entity.name === preyName && mc.isHuntable(entity), searchRange); + if (huntable) break; + } + + if (!huntable) { + huntable = world.getNearestEntityWhere(bot, entity => mc.isHuntable(entity), Math.min(searchRange, 32)); } + + if (!huntable) { + // No prey found - reset failures + this.failedAttempts = 0; + return; + } + + // Skip if this is the same target that just failed + if (this.lastFailedTarget && huntable.id === this.lastFailedTarget) { + return; + } + + const distance = currentPos.distanceTo(huntable.position); + + // Check if path is clear (important for avoiding stuck situations) + const hasPath = await world.isClearPath(bot, huntable); + + // If path is not clear and we're far away, don't attempt + if (!hasPath && distance > 16) { + console.log('[HUNTING] No clear path to distant prey, skipping'); + this.failedAttempts++; + this.lastFailedTarget = huntable.id; + if (this.failedAttempts >= 3) { + this.cooldownUntil = now + 30000; + } + return; + } + + // If path is not clear and we have no tools, skip + if (!hasPath && !hasTools && distance > 8) { + console.log('[HUNTING] No clear path and no tools, skipping'); + return; + } + + this.lastHuntTime = now; + const preyName = huntable.name.replace(/_/g, ' '); + const huntableRef = huntable; // Capture reference + + execute(this, agent, async () => { + try { + const startPos = bot.entity.position.clone(); + + if (distance > 16) { + say(agent, `Veo un ${preyName} a lo lejos, voy a cazarlo...`); + await skills.goToPosition(bot, huntableRef.position.x, huntableRef.position.y, huntableRef.position.z, 4); + + // Check if we actually moved + const endPos = bot.entity.position; + if (startPos.distanceTo(endPos) < 3) { + // Didn't move much - we're stuck + console.log('[HUNTING] Failed to reach prey - stuck'); + this.failedAttempts++; + this.lastFailedTarget = huntableRef.id; + if (this.failedAttempts >= 2) { + say(agent, 'No puedo llegar, buscaré otra cosa...'); + this.cooldownUntil = Date.now() + 20000 * this.failedAttempts; + } + return; + } + } else { + say(agent, `¡Cazando ${preyName}!`); + } + + await skills.attackEntity(bot, huntableRef); + + // Success! Reset failures + this.failedAttempts = 0; + this.lastFailedTarget = null; + + } catch (e) { + console.log('[HUNTING] Hunt failed:', e.message); + this.failedAttempts++; + this.lastFailedTarget = huntableRef.id; + + // Check for specific pathfinding errors + if (e.message && (e.message.includes('Cannot break') || e.message.includes('Path was stopped'))) { + say(agent, 'No puedo alcanzar la presa...'); + this.cooldownUntil = Date.now() + 15000 * this.failedAttempts; + } + } + }); } }, { name: 'item_collecting', - description: 'Collect nearby items when idle.', + description: 'Collect nearby dropped items. Human-like behavior with prioritization.', interrupts: ['action:followPlayer'], on: true, active: false, - - wait: 2, // number of seconds to wait after noticing an item to pick it up + wait: 1.5, // Faster reaction like a human prev_item: null, noticed_at: -1, + valuableItems: ['diamond', 'emerald', 'gold', 'iron', 'netherite', 'totem', 'enchanted'], update: async function (agent) { - let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8); - let empty_inv_slots = agent.bot.inventory.emptySlotCount(); - if (item && item !== this.prev_item && await world.isClearPath(agent.bot, item) && empty_inv_slots > 1) { + const bot = agent.bot; + + // Search for items in a wider range (16 blocks - human awareness) + let item = world.getNearestEntityWhere(bot, entity => entity.name === 'item', 16); + let empty_inv_slots = bot.inventory.emptySlotCount(); + + if (item && item !== this.prev_item && empty_inv_slots > 1) { + // Check if item is valuable (react faster to valuable items) + const isValuable = this.valuableItems.some(v => + item.metadata && item.metadata[8] && item.metadata[8].itemId && + item.metadata[8].itemId.toString().includes(v)); + + const waitTime = isValuable ? 0.5 : this.wait; + if (this.noticed_at === -1) { this.noticed_at = Date.now(); } - if (Date.now() - this.noticed_at > this.wait * 1000) { - say(agent, `Picking up item!`); - this.prev_item = item; - execute(this, agent, async () => { - await skills.pickupNearbyItems(agent.bot); - }); - this.noticed_at = -1; + + if (Date.now() - this.noticed_at > waitTime * 1000) { + // Check path only when we're about to pick up + if (await world.isClearPath(bot, item)) { + say(agent, isValuable ? '¡Objeto valioso!' : 'Recogiendo objeto...'); + this.prev_item = item; + execute(this, agent, async () => { + await skills.pickupNearbyItems(bot); + }); + this.noticed_at = -1; + } } - } - else { + } else { this.noticed_at = -1; } } @@ -444,3 +1061,6 @@ export function initModes(agent) { agent.bot.modes.loadJson(modes_json); } } + + + diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index d3601c52f..9a016f3f4 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -8,7 +8,7 @@ import { plugin as collectblock } from 'mineflayer-collectblock'; import { plugin as autoEat } from 'mineflayer-auto-eat'; import plugin from 'mineflayer-armor-manager'; const armorManager = plugin; -let mc_version = settings.minecraft_version; +let mc_version = null; let mcdata = null; let Item = null; @@ -58,13 +58,68 @@ export function initBot(username) { host: settings.host, port: settings.port, auth: settings.auth, + hideErrors: true, version: mc_version, + // Connection stability improvements + checkTimeoutInterval: 60000, // Check connection every 60 seconds (default 30s) + keepAlive: true, // Ensure keep-alive is enabled + closeTimeout: 120000, // Wait 2 minutes before considering connection dead + // Reduce packet rate for Paper server compatibility + physicsEnabled: true, + viewDistance: 'tiny', // Reduce view distance to lower packet load } if (!mc_version || mc_version === "auto") { delete options.version; } const bot = createBot(options); + + // Throttle position packets to avoid Paper server kicks (ECONNRESET) + let lastPositionUpdate = Date.now(); + const positionThrottleMs = 50; + + const clientForThrottle = bot._client; + if (clientForThrottle) { + const originalWrite = clientForThrottle.write.bind(clientForThrottle); + clientForThrottle.write = function(name, data) { + if (name === 'position' || name === 'position_look' || name === 'look') { + const now = Date.now(); + if (now - lastPositionUpdate < positionThrottleMs) { + return; + } + lastPositionUpdate = now; + } + return originalWrite(name, data); + }; + } + + // Improve connection stability by handling keep-alive proactively + bot.on('keep_alive', () => { + // Keep-alive received, connection is healthy + }); + + // Suppress non-critical protocol parsing errors (common with Paper servers) + const client = bot._client; + if (client) { + const originalEmit = client.emit.bind(client); + client.emit = function(event, ...args) { + if (event === 'error') { + const err = args[0]; + const errStr = String(err); + // Suppress PartialReadError for non-critical packets + if (errStr.includes('PartialReadError') && + (errStr.includes('scoreboard') || + errStr.includes('resource_pack') || + errStr.includes('tags') || + errStr.includes('custom_payload') || errStr.includes('entity_velocity'))) { + console.warn('[Protocol] Suppressed:', err.message ? err.message.substring(0, 80) : errStr.substring(0, 80)); + return true; + } + } + return originalEmit(event, ...args); + }; + } + bot.loadPlugin(pathfinder); bot.loadPlugin(pvp); bot.loadPlugin(collectblock); @@ -521,3 +576,5 @@ function formatPlan(targetItem, { required, steps, leftovers }) { return lines.join('\n'); } + +