mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-04-21 21:52:07 +02:00
485 lines
No EOL
14 KiB
JavaScript
485 lines
No EOL
14 KiB
JavaScript
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<mc.ItemName, number>} 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.<T, number>} availableItems - The resources available; e.g, `{'cobble_stone': 7, 'stick': 10}`
|
|
* @param {Object.<T, number>} 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');
|
|
} |