import minecraftData from 'minecraft-data'; import settings from '../../settings.js'; import { createBot } from 'mineflayer'; import prismarine_items from 'prismarine-item'; import { pathfinder } from 'mineflayer-pathfinder'; import { plugin as pvp } from 'mineflayer-pvp'; import { plugin as collectblock } from 'mineflayer-collectblock'; import { plugin as autoEat } from 'mineflayer-auto-eat'; import plugin from 'mineflayer-armor-manager'; const armorManager = plugin; const mc_version = settings.minecraft_version; const mcdata = minecraftData(mc_version); const Item = prismarine_items(mc_version); /** * @typedef {string} ItemName * @typedef {string} BlockName */ export const WOOD_TYPES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak', 'mangrove', 'cherry']; export const MATCHING_WOOD_BLOCKS = [ 'log', 'planks', 'sign', 'boat', 'fence_gate', 'door', 'fence', 'slab', 'stairs', 'button', 'pressure_plate', 'trapdoor' ] export const WOOL_COLORS = [ 'white', 'orange', 'magenta', 'light_blue', 'yellow', 'lime', 'pink', 'gray', 'light_gray', 'cyan', 'purple', 'blue', 'brown', 'green', 'red', 'black' ] export function initBot(username) { let bot = createBot({ username: username, host: settings.host, port: settings.port, auth: settings.auth, version: mc_version, }); bot.loadPlugin(pathfinder); bot.loadPlugin(pvp); bot.loadPlugin(collectblock); bot.loadPlugin(autoEat); bot.loadPlugin(armorManager); // auto equip armor bot.once('resourcePack', () => { bot.acceptResourcePack(); }); return bot; } export function isHuntable(mob) { if (!mob || !mob.name) return false; const animals = ['chicken', 'cow', 'llama', 'mooshroom', 'pig', 'rabbit', 'sheep']; return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16]; // metadata 16 is not baby } export function isHostile(mob) { if (!mob || !mob.name) return false; return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem'; } export function getItemId(itemName) { let item = mcdata.itemsByName[itemName]; if (item) { return item.id; } return null; } export function getItemName(itemId) { let item = mcdata.items[itemId] if (item) { return item.name; } return null; } export function getBlockId(blockName) { let block = mcdata.blocksByName[blockName]; if (block) { return block.id; } return null; } export function getBlockName(blockId) { let block = mcdata.blocks[blockId] if (block) { return block.name; } return null; } export function getAllItems(ignore) { if (!ignore) { ignore = []; } let items = [] for (const itemId in mcdata.items) { const item = mcdata.items[itemId]; if (!ignore.includes(item.name)) { items.push(item); } } return items; } export function getAllItemIds(ignore) { const items = getAllItems(ignore); let itemIds = []; for (const item of items) { itemIds.push(item.id); } return itemIds; } export function getAllBlocks(ignore) { if (!ignore) { ignore = []; } let blocks = [] for (const blockId in mcdata.blocks) { const block = mcdata.blocks[blockId]; if (!ignore.includes(block.name)) { blocks.push(block); } } return blocks; } export function getAllBlockIds(ignore) { const blocks = getAllBlocks(ignore); let blockIds = []; for (const block of blocks) { blockIds.push(block.id); } return blockIds; } export function getAllBiomes() { return mcdata.biomes; } export function getItemCraftingRecipes(itemName) { let itemId = getItemId(itemName); if (!mcdata.recipes[itemId]) { return null; } let recipes = []; for (let r of mcdata.recipes[itemId]) { let recipe = {}; let ingredients = []; if (r.ingredients) { ingredients = r.ingredients; } else if (r.inShape) { ingredients = r.inShape.flat(); } for (let ingredient of ingredients) { let ingredientName = getItemName(ingredient); if (ingredientName === null) continue; if (!recipe[ingredientName]) recipe[ingredientName] = 0; recipe[ingredientName]++; } recipes.push([ recipe, {craftedCount : r.result.count} ]); } return recipes; } export function isSmeltable(itemName) { const misc_smeltables = ['beef', 'chicken', 'cod', 'mutton', 'porkchop', 'rabbit', 'salmon', 'tropical_fish', 'potato', 'kelp', 'sand', 'cobblestone', 'clay_ball']; return itemName.includes('raw') || itemName.includes('log') || misc_smeltables.includes(itemName); } export function getSmeltingFuel(bot) { let fuel = bot.inventory.items().find(i => i.name === 'coal' || i.name === 'charcoal' || i.name === 'blaze_rod') if (fuel) return fuel; fuel = bot.inventory.items().find(i => i.name.includes('log') || i.name.includes('planks')) if (fuel) return fuel; return bot.inventory.items().find(i => i.name === 'coal_block' || i.name === 'lava_bucket'); } export function getFuelSmeltOutput(fuelName) { if (fuelName === 'coal' || fuelName === 'charcoal') return 8; if (fuelName === 'blaze_rod') return 12; if (fuelName.includes('log') || fuelName.includes('planks')) return 1.5 if (fuelName === 'coal_block') return 80; if (fuelName === 'lava_bucket') return 100; return 0; } export function getItemSmeltingIngredient(itemName) { return { baked_potato: 'potato', steak: 'raw_beef', cooked_chicken: 'raw_chicken', cooked_cod: 'raw_cod', cooked_mutton: 'raw_mutton', cooked_porkchop: 'raw_porkchop', cooked_rabbit: 'raw_rabbit', cooked_salmon: 'raw_salmon', dried_kelp: 'kelp', iron_ingot: 'raw_iron', gold_ingot: 'raw_gold', copper_ingot: 'raw_copper', glass: 'sand' }[itemName]; } export function getItemBlockSources(itemName) { let itemId = getItemId(itemName); let sources = []; for (let block of getAllBlocks()) { if (block.drops.includes(itemId)) { sources.push(block.name); } } return sources; } export function getItemAnimalSource(itemName) { return { raw_beef: 'cow', raw_chicken: 'chicken', raw_cod: 'cod', raw_mutton: 'sheep', raw_porkchop: 'pig', raw_rabbit: 'rabbit', raw_salmon: 'salmon', leather: 'cow', wool: 'sheep' }[itemName]; } export function getBlockTool(blockName) { let block = mcdata.blocksByName[blockName]; if (!block || !block.harvestTools) { return null; } return getItemName(Object.keys(block.harvestTools)[0]); // Double check first tool is always simplest } export function makeItem(name, amount=1) { return new Item(getItemId(name), amount); } /** * Returns the number of ingredients required to use the recipe once. * * @param {Recipe} recipe * @returns {Object} an object describing the number of each ingredient. */ export function ingredientsFromPrismarineRecipe(recipe) { let requiredIngedients = {}; if (recipe.inShape) for (const ingredient of recipe.inShape.flat()) { if(ingredient.id<0) continue; //prismarine-recipe uses id -1 as an empty crafting slot const ingredientName = getItemName(ingredient.id); requiredIngedients[ingredientName] ??=0; requiredIngedients[ingredientName] += ingredient.count; } if (recipe.ingredients) for (const ingredient of recipe.ingredients) { if(ingredient.id<0) continue; const ingredientName = getItemName(ingredient.id); requiredIngedients[ingredientName] ??=0; requiredIngedients[ingredientName] -= ingredient.count; //Yes, the `-=` is intended. //prismarine-recipe uses positive numbers for the shaped ingredients but negative for unshaped. //Why this is the case is beyond my understanding. } return requiredIngedients; } /** * Calculates the number of times an action, such as a crafing recipe, can be completed before running out of resources. * @template T - doesn't have to be an item. This could be any resource. * @param {Object.} availableItems - The resources available; e.g, `{'cobble_stone': 7, 'stick': 10}` * @param {Object.} requiredItems - The resources required to complete the action once; e.g, `{'cobble_stone': 3, 'stick': 2}` * @param {boolean} discrete - Is the action discrete? * @returns {{num: number, limitingResource: (T | null)}} the number of times the action can be completed and the limmiting resource; e.g `{num: 2, limitingResource: 'cobble_stone'}` */ export function calculateLimitingResource(availableItems, requiredItems, discrete=true) { let limitingResource = null; let num = Infinity; for (const itemType in requiredItems) { if (availableItems[itemType] < requiredItems[itemType] * num) { limitingResource = itemType; num = availableItems[itemType] / requiredItems[itemType]; } } 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'); }