Merge branch 'main' into viewport-capture

This commit is contained in:
MaxRobinsonTheGreat 2024-10-16 17:51:34 -05:00
commit a467bdf357
7 changed files with 316 additions and 84 deletions

View file

@ -1,8 +1,8 @@
diff --git a/node_modules/mineflayer-collectblock/lib/CollectBlock.js b/node_modules/mineflayer-collectblock/lib/CollectBlock.js
index 2c11e8c..4697873 100644
index 2c11e8c..bb49c11 100644
--- a/node_modules/mineflayer-collectblock/lib/CollectBlock.js
+++ b/node_modules/mineflayer-collectblock/lib/CollectBlock.js
@@ -77,7 +77,7 @@ function mineBlock(bot, block, options) {
@@ -77,10 +77,11 @@ function mineBlock(bot, block, options) {
}
yield bot.tool.equipForBlock(block, equipToolOptions);
// @ts-expect-error
@ -11,7 +11,20 @@ index 2c11e8c..4697873 100644
options.targets.removeTarget(block);
return;
}
@@ -195,6 +195,8 @@ class CollectBlock {
+
const tempEvents = new TemporarySubscriber_1.TemporarySubscriber(bot);
tempEvents.subscribeTo('itemDrop', (entity) => {
if (entity.position.distanceTo(block.position.offset(0.5, 0.5, 0.5)) <= 0.5) {
@@ -92,7 +93,7 @@ function mineBlock(bot, block, options) {
// Waiting for items to drop
yield new Promise(resolve => {
let remainingTicks = 10;
- tempEvents.subscribeTo('physicTick', () => {
+ tempEvents.subscribeTo('physicsTick', () => {
remainingTicks--;
if (remainingTicks <= 0) {
tempEvents.cleanup();
@@ -195,6 +196,8 @@ class CollectBlock {
throw (0, Util_1.error)('UnresolvedDependency', 'The mineflayer-collectblock plugin relies on the mineflayer-tool plugin to run!');
}
if (this.movements != null) {

View file

@ -0,0 +1,13 @@
diff --git a/node_modules/mineflayer-pvp/lib/PVP.js b/node_modules/mineflayer-pvp/lib/PVP.js
index 758c2b3..7c7220e 100644
--- a/node_modules/mineflayer-pvp/lib/PVP.js
+++ b/node_modules/mineflayer-pvp/lib/PVP.js
@@ -48,7 +48,7 @@ class PVP {
this.meleeAttackRate = new TimingSolver_1.MaxDamageOffset();
this.bot = bot;
this.movements = new mineflayer_pathfinder_1.Movements(bot, require('minecraft-data')(bot.version));
- this.bot.on('physicTick', () => this.update());
+ this.bot.on('physicsTick', () => this.update());
this.bot.on('entityGone', e => { if (e === this.target)
this.stop(); });
}

View file

@ -23,7 +23,7 @@ export const actionsList = [
name: '!newAction',
description: 'Perform new and unknown custom behaviors that are not available as a command.',
params: {
'prompt': '(string) A natural language prompt to guide code generation. Make a detailed step-by-step plan.'
'prompt': { type: 'string', description: 'A natural language prompt to guide code generation. Make a detailed step-by-step plan.' }
},
perform: async function (agent, prompt) {
// just ignore prompt - it is now in context in chat history
@ -75,8 +75,8 @@ export const actionsList = [
name: '!goToPlayer',
description: 'Go to the given player.',
params: {
'player_name': '(string) The name of the player to go to.',
'closeness': '(number) How close to get to the player.'
'player_name': {type: 'string', description: 'The name of the player to go to.'},
'closeness': {type: 'float', description: 'How close to get to the player.', domain: [0, Infinity]}
},
perform: wrapExecution(async (agent, player_name, closeness) => {
return await skills.goToPlayer(agent.bot, player_name, closeness);
@ -86,8 +86,8 @@ export const actionsList = [
name: '!followPlayer',
description: 'Endlessly follow the given player. Will defend that player if self_defense mode is on.',
params: {
'player_name': '(string) The name of the player to follow.',
'follow_dist': '(number) The distance to follow from.'
'player_name': {type: 'string', description: 'name of the player to follow.'},
'follow_dist': {type: 'float', description: 'The distance to follow from.', domain: [0, Infinity]}
},
perform: wrapExecution(async (agent, player_name, follow_dist) => {
await skills.followPlayer(agent.bot, player_name, follow_dist);
@ -97,9 +97,9 @@ export const actionsList = [
name: '!goToBlock',
description: 'Go to the nearest block of a given type.',
params: {
'type': '(string) The block type to go to.',
'closeness': '(number) How close to get to the block.',
'search_range': '(number) The distance to search for the block.'
'type': { type: 'BlockName', description: 'The block type to go to.' },
'closeness': { type: 'float', description: 'How close to get to the block.', domain: [0, Infinity] },
'search_range': { type: 'float', description: 'The distance to search for the block.', domain: [0, Infinity] }
},
perform: wrapExecution(async (agent, type, closeness, range) => {
await skills.goToNearestBlock(agent.bot, type, closeness, range);
@ -108,7 +108,7 @@ export const actionsList = [
{
name: '!moveAway',
description: 'Move away from the current location in any direction by a given distance.',
params: {'distance': '(number) The distance to move away.'},
params: {'distance': { type: 'float', description: 'The distance to move away.', domain: [0, Infinity] }},
perform: wrapExecution(async (agent, distance) => {
await skills.moveAway(agent.bot, distance);
})
@ -116,7 +116,7 @@ export const actionsList = [
{
name: '!rememberHere',
description: 'Save the current location with a given name.',
params: {'name': '(string) The name to remember the location as.'},
params: {'name': { type: 'string', description: 'The name to remember the location as.' }},
perform: async function (agent, name) {
const pos = agent.bot.entity.position;
agent.memory_bank.rememberPlace(name, pos.x, pos.y, pos.z);
@ -126,7 +126,7 @@ export const actionsList = [
{
name: '!goToPlace',
description: 'Go to a saved location.',
params: {'name': '(string) The name of the location to go to.'},
params: {'name': { type: 'string', description: 'The name of the location to go to.' }},
perform: wrapExecution(async (agent, name) => {
const pos = agent.memory_bank.recallPlace(name);
if (!pos) {
@ -140,9 +140,9 @@ export const actionsList = [
name: '!givePlayer',
description: 'Give the specified item to the given player.',
params: {
'player_name': '(string) The name of the player to give the item to.',
'item_name': '(string) The name of the item to give.' ,
'num': '(number) The number of items to give.'
'player_name': { type: 'string', description: 'The name of the player to give the item to.' },
'item_name': { type: 'ItemName', description: 'The name of the item to give.' },
'num': { type: 'int', description: 'The number of items to give.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, player_name, item_name, num) => {
await skills.giveToPlayer(agent.bot, item_name, player_name, num);
@ -151,7 +151,7 @@ export const actionsList = [
{
name: '!equip',
description: 'Equip the given item.',
params: {'item_name': '(string) The name of the item to equip.'},
params: {'item_name': { type: 'ItemName', description: 'The name of the item to equip.' }},
perform: wrapExecution(async (agent, item_name) => {
await skills.equip(agent.bot, item_name);
})
@ -160,8 +160,8 @@ export const actionsList = [
name: '!putInChest',
description: 'Put the given item in the nearest chest.',
params: {
'item_name': '(string) The name of the item to put in the chest.',
'num': '(number) The number of items to put in the chest.'
'item_name': { type: 'ItemName', description: 'The name of the item to put in the chest.' },
'num': { type: 'int', description: 'The number of items to put in the chest.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, item_name, num) => {
await skills.putInChest(agent.bot, item_name, num);
@ -171,8 +171,8 @@ export const actionsList = [
name: '!takeFromChest',
description: 'Take the given items from the nearest chest.',
params: {
'item_name': '(string) The name of the item to take.',
'num': '(number) The number of items to take.'
'item_name': { type: 'ItemName', description: 'The name of the item to take.' },
'num': { type: 'int', description: 'The number of items to take.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, item_name, num) => {
await skills.takeFromChest(agent.bot, item_name, num);
@ -190,8 +190,8 @@ export const actionsList = [
name: '!discard',
description: 'Discard the given item from the inventory.',
params: {
'item_name': '(string) The name of the item to discard.',
'num': '(number) The number of items to discard.',
'item_name': { type: 'ItemName', description: 'The name of the item to discard.' },
'num': { type: 'int', description: 'The number of items to discard.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, item_name, num) => {
const start_loc = agent.bot.entity.position;
@ -204,8 +204,8 @@ export const actionsList = [
name: '!collectBlocks',
description: 'Collect the nearest blocks of a given type.',
params: {
'type': '(string) The block type to collect.',
'num': '(number) The number of blocks to collect.'
'type': { type: 'BlockName', description: 'The block type to collect.' },
'num': { type: 'int', description: 'The number of blocks to collect.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: wrapExecution(async (agent, type, num) => {
await skills.collectBlock(agent.bot, type, num);
@ -215,7 +215,7 @@ export const actionsList = [
name: '!collectAllBlocks',
description: 'Collect all the nearest blocks of a given type until told to stop.',
params: {
'type': '(string) The block type to collect.'
'type': { type: 'BlockName', description: 'The block type to collect.' }
},
perform: wrapExecution(async (agent, type) => {
let success = await skills.collectBlock(agent.bot, type, 1);
@ -227,8 +227,8 @@ export const actionsList = [
name: '!craftRecipe',
description: 'Craft the given recipe a given number of times.',
params: {
'recipe_name': '(string) The name of the output item to craft.',
'num': '(number) 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.'
'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] }
},
perform: wrapExecution(async (agent, recipe_name, num) => {
await skills.craftRecipe(agent.bot, recipe_name, num);
@ -238,8 +238,8 @@ export const actionsList = [
name: '!smeltItem',
description: 'Smelt the given item the given number of times.',
params: {
'item_name': '(string) The name of the input item to smelt.',
'num': '(number) The number of times to smelt the item.'
'item_name': { type: 'string', description: 'The name of the input item to smelt.' },
'num': { type: 'int', description: 'The number of times to smelt the item.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: async function (agent, item_name, num) {
let response = await wrapExecution(async (agent) => {
@ -265,7 +265,7 @@ export const actionsList = [
{
name: '!placeHere',
description: 'Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches.',
params: {'type': '(string) The block type to place.'},
params: {type: 'string', description: 'The block type to place.'},
perform: wrapExecution(async (agent, type) => {
let pos = agent.bot.entity.position;
await skills.placeBlock(agent.bot, type, pos.x, pos.y, pos.z);
@ -274,7 +274,7 @@ export const actionsList = [
{
name: '!attack',
description: 'Attack and kill the nearest entity of a given type.',
params: {'type': '(string) The type of entity to attack.'},
params: {'type': 'string', description: 'The type of entity to attack.'},
perform: wrapExecution(async (agent, type) => {
await skills.attackNearest(agent.bot, type, true);
})
@ -289,7 +289,7 @@ export const actionsList = [
{
name: '!activate',
description: 'Activate the nearest object of a given type.',
params: {'type': '(string) The type of object to activate.'},
params: {'type': { type: 'BlockName', description: 'The type of object to activate.' }},
perform: wrapExecution(async (agent, type) => {
await skills.activateNearestBlock(agent.bot, type);
})
@ -305,8 +305,8 @@ export const actionsList = [
name: '!setMode',
description: 'Set a mode to on or off. A mode is an automatic behavior that constantly checks and responds to the environment.',
params: {
'mode_name': '(string) The name of the mode to enable.',
'on': '(bool) Whether to enable or disable the mode.'
'mode_name': { type: 'string', description: 'The name of the mode to enable.' },
'on': { type: 'boolean', description: 'Whether to enable or disable the mode.' }
},
perform: async function (agent, mode_name, on) {
const modes = agent.bot.modes;
@ -322,7 +322,7 @@ export const actionsList = [
name: '!goal',
description: 'Set a goal prompt to endlessly work towards with continuous self-prompting.',
params: {
'selfPrompt': '(string) The goal prompt.',
'selfPrompt': { type: 'string', description: 'The goal prompt.' },
},
perform: async function (agent, prompt) {
agent.self_prompter.start(prompt); // don't await, don't return
@ -340,8 +340,8 @@ export const actionsList = [
name: '!npcGoal',
description: 'Set a simple goal for an item or building to automatically work towards. Do not use for complex goals.',
params: {
'name': '(string) The name of the goal to set. Can be item or building name. If empty will automatically choose a goal.',
'quantity': '(number) The quantity of the goal to set. Default is 1.'
'name': { type: 'string', description: 'The name of the goal to set. Can be item or building name. If empty will automatically choose a goal.' },
'quantity': { type: 'int', description: 'The quantity of the goal to set. Default is 1.', domain: [1, Number.MAX_SAFE_INTEGER] }
},
perform: async function (agent, name=null, quantity=1) {
await agent.npc.setGoal(name, quantity);

View file

@ -1,6 +1,8 @@
import { getBlockId, getItemId } from "../../utils/mcdata.js";
import { actionsList } from './actions.js';
import { queryList } from './queries.js';
let suppressNoDomainWarning = false;
const commandList = queryList.concat(actionsList);
const commandMap = {};
@ -28,43 +30,138 @@ export function commandExists(commandName) {
return commandMap[commandName] !== undefined;
}
/**
* Converts a string into a boolean.
* @param {string} input
* @returns {boolean | null} the boolean or `null` if it could not be parsed.
* */
function parseBoolean(input) {
switch(input.toLowerCase()) {
case 'false': //These are interpreted as flase;
case 'f':
case '0':
case 'off':
return false;
case 'true': //These are interpreted as true;
case 't':
case '1':
case 'on':
return true;
default:
return null;
}
}
/**
* @param {number} value - the value to check
* @param {number} lowerBound
* @param {number} upperBound
* @param {string} endpointType - The type of the endpoints represented as a two character string. `'[)'` `'()'`
*/
function checkInInterval(number, lowerBound, upperBound, endpointType) {
switch (endpointType) {
case '[)':
return lowerBound <= number && number < upperBound;
case '()':
return lowerBound < number && number < upperBound;
case '(]':
return lowerBound < number && number <= upperBound;
case '[]':
return lowerBound <= number && number <= upperBound;
default:
throw new Error('Unknown endpoint type:', endpointType)
}
}
// todo: handle arrays?
/**
* Returns an object containing the command, the command name, and the comand parameters.
* If parsing unsuccessful, returns an error message as a string.
* @param {string} message - A message from a player or language model containing a command.
* @returns {string | Object}
*/
function parseCommandMessage(message) {
const commandMatch = message.match(commandRegex);
if (commandMatch) {
const commandName = "!"+commandMatch[1];
if (!commandMatch[2])
return { commandName, args: [] };
let args = commandMatch[2].match(argRegex);
if (args) {
for (let i = 0; i < args.length; i++) {
args[i] = args[i].trim();
}
if (!commandMatch) return `Command is incorrectly formatted`;
for (let i = 0; i < args.length; i++) {
let arg = args[i];
const commandName = "!"+commandMatch[1];
if (arg.includes('=')) {
// this sanitizes syntaxes like "x=2" and ignores the param name
let split = arg.split('=');
args[i] = split[1];
}
let args;
if (commandMatch[2]) args = commandMatch[2].match(argRegex);
else args = [];
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
args[i] = arg.substring(1, arg.length-1);
} else if (!isNaN(arg)) {
args[i] = Number(arg);
} else if (arg === 'true' || arg === 'false') {
args[i] = arg === 'true';
}
}
const command = getCommand(commandName);
if(!command) return `${commandName} is not a command.`
const params = commandParams(command);
const paramNames = commandParamNames(command);
if (args.length !== params.length)
return `Command ${command.name} was given ${args.length} args, but requires ${params.length} args.`;
for (let i = 0; i < args.length; i++) {
const param = params[i];
//Remove any extra characters
let arg = args[i].trim();
if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) {
arg = arg.substring(1, arg.length-1);
}
else
args = [];
return { commandName, args };
if (arg.includes('=')) {
// this sanitizes syntaxes like "x=2" and ignores the param name
let split = arg.split('=');
args[i] = split[1];
}
//Convert to the correct type
switch(param.type) {
case 'int':
arg = Number.parseInt(arg); break;
case 'float':
arg = Number.parseFloat(arg); break;
case 'boolean':
arg = parseBoolean(arg); break;
case 'BlockName':
case 'ItemName':
if (arg.endsWith('plank'))
arg += 's'; // catches common mistakes like "oak_plank" instead of "oak_planks"
case 'string':
break;
default:
throw new Error(`Command '${commandName}' parameter '${paramNames[i]}' has an unknown type: ${param.type}`);
}
if(arg === null || Number.isNaN(arg))
return `Error: Param '${paramNames[i]}' must be of type ${param.type}.`
if(typeof arg === 'number') { //Check the domain of numbers
const domain = param.domain;
if(domain) {
/**
* Javascript has a built in object for sets but not intervals.
* Currently the interval (lowerbound,upperbound] is represented as an Array: `[lowerbound, upperbound, '(]']`
*/
if (!domain[2]) domain[2] = '[)'; //By default, lower bound is included. Upper is not.
if(!checkInInterval(arg, ...domain)) {
return `Error: Param '${paramNames[i]}' must be an element of ${domain[2][0]}${domain[0]}, ${domain[1]}${domain[2][1]}.`;
//Alternatively arg could be set to the nearest value in the domain.
}
} else if (!suppressNoDomainWarning) {
console.warn(`Command '${commandName}' parameter '${paramNames[i]}' has no domain set. Expect any value [-Infinity, Infinity].`)
suppressNoDomainWarning = true; //Don't spam console. Only give the warning once.
}
} else if(param.type === 'BlockName') { //Check that there is a block with this name
if(getBlockId(arg) == null) return `Invalid block type: ${arg}.`
} else if(param.type === 'ItemName') { //Check that there is an item with this name
if(getItemId(arg) == null) return `Invalid item type: ${arg}.`
}
args[i] = arg;
}
return null;
return { commandName, args };
}
export function truncCommandMessage(message) {
@ -79,15 +176,36 @@ export function isAction(name) {
return actionsList.find(action => action.name === name) !== undefined;
}
function numParams(command) {
/**
* @param {Object} command
* @returns {Object[]} The command's parameters.
*/
function commandParams(command) {
if (!command.params)
return 0;
return Object.keys(command.params).length;
return [];
return Object.values(command.params);
}
/**
* @param {Object} command
* @returns {string[]} The names of the command's parameters.
*/
function commandParamNames(command) {
if (!command.params)
return [];
return Object.keys(command.params);
}
function numParams(command) {
return commandParams(command).length;
}
export async function executeCommand(agent, message) {
let parsed = parseCommandMessage(message);
if (parsed) {
if (typeof parsed === 'string')
return parsed; //The command was incorrectly formatted or an invalid input was given.
else {
console.log('parsed command:', parsed);
const command = getCommand(parsed.commandName);
const is_action = isAction(command.name);
let numArgs = 0;
@ -106,11 +224,18 @@ export async function executeCommand(agent, message) {
return result;
}
}
else
return `Command is incorrectly formatted`;
}
export function getCommandDocs() {
const typeTranslations = {
//This was added to keep the prompt the same as before type checks were implemented.
//If the language model is giving invalid inputs changing this might help.
'float': 'number',
'int': 'number',
'BlockName': 'string',
'ItemName': 'string',
'boolean': 'bool'
}
let docs = `\n*COMMAND DOCS\n You can use the following commands to perform actions and get information about the world.
Use the commands with the syntax: !commandName or !commandName("arg1", 1.2, ...) if the command takes arguments.\n
Do not use codeblocks. Only use one command in each response, trailing commands and comments will be ignored.\n`;
@ -119,7 +244,7 @@ export function getCommandDocs() {
if (command.params) {
docs += 'Params:\n';
for (let param in command.params) {
docs += param + ': ' + command.params[param] + '\n';
docs += `${param}: (${typeTranslations[command.params[param].type]??command.params[param].type}) ${command.params[param].description}\n`;
}
}
}

View file

@ -66,6 +66,23 @@ export const queryList = [
else if (agent.bot.game.gameMode === 'creative') {
res += '\n(You have infinite items in creative mode. You do not need to gather resources!!)';
}
let helmet = bot.inventory.slots[5];
let chestplate = bot.inventory.slots[6];
let leggings = bot.inventory.slots[7];
let boots = bot.inventory.slots[8];
res += '\nWEARING: ';
if (helmet)
res += `\nHead: ${helmet.name}`;
if (chestplate)
res += `\nTorso: ${chestplate.name}`;
if (leggings)
res += `\nLegs: ${leggings.name}`;
if (boots)
res += `\nFeet: ${boots.name}`;
if (!helmet && !chestplate && !leggings && !boots)
res += 'None';
return pad(res);
}
},

View file

@ -32,7 +32,6 @@ async function equipHighestAttack(bot) {
await bot.equip(weapon, 'hand');
}
export async function craftRecipe(bot, itemName, num=1) {
/**
* Attempt to craft the given item name from a recipe. May craft many items.
@ -44,14 +43,13 @@ export async function craftRecipe(bot, itemName, num=1) {
**/
let placedTable = false;
if (itemName.endsWith('plank'))
itemName += 's'; // catches common mistakes like "oak_plank" instead of "oak_planks"
// get recipes that don't require a crafting table
let recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, null);
let craftingTable = null;
const craftingTableRange = 32;
if (!recipes || recipes.length === 0) {
placeTable: if (!recipes || recipes.length === 0) {
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);
@ -69,7 +67,7 @@ export async function craftRecipe(bot, itemName, num=1) {
}
}
else {
log(bot, `You either do not have enough resources to craft ${itemName} or it requires a crafting table.`)
log(bot, `Crafting ${itemName} requires a crafting table.`)
return false;
}
}
@ -91,11 +89,22 @@ export async function craftRecipe(bot, itemName, num=1) {
const recipe = recipes[0];
console.log('crafting...');
await bot.craft(recipe, num, craftingTable);
log(bot, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(bot)[itemName]} ${itemName}.`);
//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);
await bot.craft(recipe, Math.min(craftLimit.num, num), craftingTable);
if(craftLimit.num<num) log(bot, `Not enough ${craftLimit.limitingResource} to craft ${num}, crafted ${craftLimit.num}. You now have ${world.getInventoryCounts(bot)[itemName]} ${itemName}.`);
else log(bot, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(bot)[itemName]} ${itemName}.`);
if (placedTable) {
await collectBlock(bot, 'crafting_table', 1);
}
//Equip any armor the bot may have crafted.
//There is probablly a more efficient method than checking the entire inventory but this is all mineflayer-armor-manager provides. :P
bot.armorManager.equipAll();
return true;
}
@ -682,7 +691,7 @@ export async function equip(bot, itemName) {
* @example
* await skills.equip(bot, "iron_pickaxe");
**/
let item = bot.inventory.items().find(item => item.name === itemName);
let item = bot.inventory.slots.find(slot => slot && slot.name === itemName);
if (!item) {
log(bot, `You do not have any ${itemName} to equip.`);
return false;
@ -696,12 +705,13 @@ export async function equip(bot, itemName) {
else if (itemName.includes('helmet')) {
await bot.equip(item, 'head');
}
else if (itemName.includes('chestplate')) {
else if (itemName.includes('chestplate') || itemName.includes('elytra')) {
await bot.equip(item, 'torso');
}
else {
await bot.equip(item, 'hand');
}
log(bot, `Equipped ${itemName}.`);
return true;
}

View file

@ -13,6 +13,11 @@ 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'];
export const MATCHING_WOOD_BLOCKS = [
'log',
@ -241,4 +246,53 @@ export function getBlockTool(blockName) {
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}
}