mindcraft/src/utils/mcdata.js

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');
}