diff --git a/services/minecraft/src/composables/world.ts b/services/minecraft/src/composables/world.ts index fae3c94ec..db4719ca4 100644 --- a/services/minecraft/src/composables/world.ts +++ b/services/minecraft/src/composables/world.ts @@ -6,25 +6,52 @@ import type { Mineflayer } from '../libs/mineflayer' import pf from 'mineflayer-pathfinder' import * as mc from '../utils/mcdata' -export function getNearestFreeSpace(mineflayer: Mineflayer, size: number = 1, distance: number = 8): Vec3 | undefined { - const emptyPositions = mineflayer.bot.findBlocks({ - matching: (block: Block) => block?.name === 'air', +export function getNearestFreeSpace( + mineflayer: Mineflayer, + size: number = 1, + distance: number = 8, +): Vec3 | undefined { + /** + * Get the nearest empty space with solid blocks beneath it of the given size. + * @param {number} size - The (size x size) of the space to find, default 1. + * @param {number} distance - The maximum distance to search, default 8. + * @returns {Vec3} - The south west corner position of the nearest free space. + * @example + * let position = world.getNearestFreeSpace( 1, 8); + */ + const empty_pos = mineflayer.bot.findBlocks({ + matching: (block: Block | null) => { + return block !== null && block.name === 'air' + }, maxDistance: distance, count: 1000, }) - return emptyPositions.find((pos) => { + for (let i = 0; i < empty_pos.length; i++) { + let empty = true for (let x = 0; x < size; x++) { for (let z = 0; z < size; z++) { - const top = mineflayer.bot.blockAt(pos.offset(x, 0, z)) - const bottom = mineflayer.bot.blockAt(pos.offset(x, -1, z)) - if (!top || top.name !== 'air' || !bottom?.drops?.length || !bottom.diggable) { - return false + const top = mineflayer.bot.blockAt(empty_pos[i].offset(x, 0, z)) + const bottom = mineflayer.bot.blockAt(empty_pos[i].offset(x, -1, z)) + if ( + !top + || top.name !== 'air' + || !bottom + || (bottom.drops?.length ?? 0) === 0 + || !bottom.diggable + ) { + empty = false + break } } + if (!empty) + break } - return true - }) + if (empty) { + return empty_pos[i] + } + } + return undefined } export function getNearestBlocks(mineflayer: Mineflayer, blockTypes: string[] | string | null = null, distance: number = 16, count: number = 10000): Block[] { diff --git a/services/minecraft/src/skills/actions/collectBlock.ts b/services/minecraft/src/skills/actions/collectBlock.ts new file mode 100644 index 000000000..e88f209c4 --- /dev/null +++ b/services/minecraft/src/skills/actions/collectBlock.ts @@ -0,0 +1,166 @@ +import type { Block } from 'prismarine-block' +import type { Mineflayer } from '../../libs/mineflayer' +import { useLogg } from '@guiiai/logg' +import pathfinder from 'mineflayer-pathfinder' +import { getNearestBlocks } from '../../composables/world' +import { breakBlockAt } from '../blocks' +import { ensurePickaxe } from './ensure' +import { pickupNearbyItems } from './world-interactions' + +const logger = useLogg('Action:CollectBlock').useGlobalConfig() + +export async function collectBlock( + mineflayer: Mineflayer, + blockType: string, + num = 1, + range = 16, +): Promise { + if (num < 1) { + logger.log(`Invalid number of blocks to collect: ${num}.`) + return false + } + + const blockTypes = [blockType] + + // Add block variants + if ( + [ + 'coal', + 'diamond', + 'emerald', + 'iron', + 'gold', + 'lapis_lazuli', + 'redstone', + 'copper', + ].includes(blockType) + ) { + blockTypes.push(`${blockType}_ore`, `deepslate_${blockType}_ore`) + } + if (blockType.endsWith('ore')) { + blockTypes.push(`deepslate_${blockType}`) + } + if (blockType === 'dirt') { + blockTypes.push('grass_block') + } + + let collected = 0 + + while (collected < num) { + const blocks = getNearestBlocks(mineflayer, blockTypes, range) + + if (blocks.length === 0) { + if (collected === 0) + logger.log(`No ${blockType} nearby to collect.`) + else logger.log(`No more ${blockType} nearby to collect.`) + break + } + + const block = blocks[0] + + try { + // Equip appropriate tool + if (mineflayer.bot.game.gameMode !== 'creative') { + await mineflayer.bot.tool.equipForBlock(block) + const itemId = mineflayer.bot.heldItem ? mineflayer.bot.heldItem.type : null + if (!block.canHarvest(itemId)) { + logger.log(`Don't have right tools to harvest ${block.name}.`) + if (block.name.includes('ore') || block.name.includes('stone')) { + await ensurePickaxe(mineflayer) + } + throw new Error('Don\'t have right tools to harvest block.') + } + } + + // Implement vein mining + const veinBlocks = findVeinBlocks(mineflayer, block, 100, range, 1) + + for (const veinBlock of veinBlocks) { + if (collected >= num) + break + + // Move to the block using pathfinder + const goal = new pathfinder.goals.GoalGetToBlock( + veinBlock.position.x, + veinBlock.position.y, + veinBlock.position.z, + ) + await mineflayer.bot.pathfinder.goto(goal) + + // Break the block and collect drops + await mineAndCollect(mineflayer, veinBlock) + + collected++ + + // Check if inventory is full + if (mineflayer.bot.inventory.emptySlotCount() === 0) { + logger.log('Inventory is full, cannot collect more items.') + break + } + } + } + catch (err) { + logger.log(`Failed to collect ${blockType}: ${err}.`) + continue + } + } + + logger.log(`Collected ${collected} ${blockType}(s).`) + return collected > 0 +} + +// Helper function to mine a block and collect drops +async function mineAndCollect(mineflayer: Mineflayer, block: Block): Promise { + // Break the block + await breakBlockAt(mineflayer, block.position.x, block.position.y, block.position.z) + // Use your existing function to pick up nearby items + await pickupNearbyItems(mineflayer, 5) +} + +// Function to find connected blocks (vein mining) +function findVeinBlocks( + mineflayer: Mineflayer, + startBlock: Block, + maxBlocks = 100, + maxDistance = 16, + floodRadius = 1, +): Block[] { + const veinBlocks: Block[] = [] + const visited = new Set() + const queue: Block[] = [startBlock] + + while (queue.length > 0 && veinBlocks.length < maxBlocks) { + const block = queue.shift() + if (!block) + continue + const key = block.position.toString() + + if (visited.has(key)) + continue + visited.add(key) + + if (block.name !== startBlock.name) + continue + if (block.position.distanceTo(startBlock.position) > maxDistance) + continue + + veinBlocks.push(block) + + // Check neighboring blocks within floodRadius + for (let dx = -floodRadius; dx <= floodRadius; dx++) { + for (let dy = -floodRadius; dy <= floodRadius; dy++) { + for (let dz = -floodRadius; dz <= floodRadius; dz++) { + if (dx === 0 && dy === 0 && dz === 0) + continue // Skip the current block + const neighborPos = block.position.offset(dx, dy, dz) + const neighborBlock = mineflayer.bot.blockAt(neighborPos) + if (neighborBlock && !visited.has(neighborPos.toString())) { + queue.push(neighborBlock) + } + } + } + } + } + + return veinBlocks +} diff --git a/services/minecraft/src/skills/actions/ensure.ts b/services/minecraft/src/skills/actions/ensure.ts new file mode 100644 index 000000000..6bcc21b7b --- /dev/null +++ b/services/minecraft/src/skills/actions/ensure.ts @@ -0,0 +1,500 @@ +import type { Mineflayer } from '../../libs/mineflayer' +import { useLogg } from '@guiiai/logg' +import { getItemId } from '../../utils/mcdata' +import { craftRecipe } from '../crafting' +import { moveAway } from '../movement' +import { collectBlock } from './collectBlock' +import { gatherWood } from './gatherWood' +import { getItemCount } from './inventory' + +// Constants for crafting and gathering +const PLANKS_PER_LOG = 4 +const STICKS_PER_PLANK = 2 + +const logger = useLogg('Action:Ensure').useGlobalConfig() + +// Helper function to ensure a crafting table +export async function ensureCraftingTable(mineflayer: Mineflayer): Promise { + logger.log('Bot: Checking for a crafting table...') + + let hasCraftingTable = getItemCount(mineflayer, 'crafting_table') > 0 + + if (hasCraftingTable) { + logger.log('Bot: Crafting table is available.') + return true + } + + while (!hasCraftingTable) { + const planksEnsured = await ensurePlanks(mineflayer, 4) + if (!planksEnsured) { + logger.error('Bot: Failed to ensure planks.') + continue + } + + // Craft crafting table + hasCraftingTable = await craftRecipe(mineflayer, 'crafting_table', 1) + if (hasCraftingTable) { + mineflayer.bot.chat('I have made a crafting table.') + logger.log('Bot: Crafting table crafted.') + } + else { + logger.error('Bot: Failed to craft crafting table.') + } + } + + return hasCraftingTable +} + +// Helper function to ensure a specific amount of planks +export async function ensurePlanks(mineflayer: Mineflayer, neededAmount: number): Promise { + logger.log('Bot: Checking for planks...') + + let planksCount = getItemCount(mineflayer, 'planks') + + if (neededAmount < planksCount) { + logger.log('Bot: Have enough planks.') + return true + } + + while (neededAmount > planksCount) { + const logsNeeded = Math.ceil((neededAmount - planksCount) / PLANKS_PER_LOG) + + // Get all available log types in inventory + const availableLogs = mineflayer.bot.inventory + .items() + .filter(item => item.name.includes('log')) + + // If no logs available, gather more wood + if (availableLogs.length === 0) { + await gatherWood(mineflayer, logsNeeded, 80) + logger.error('Bot: Not enough logs for planks.') + continue + } + + // Iterate over each log type to craft planks + for (const log of availableLogs) { + const logType = log.name.replace('_log', '') // Get log type without "_log" suffix + const logsToCraft = Math.min(log.count, logsNeeded) + + logger.log( + `Trying to make ${logsToCraft * PLANKS_PER_LOG} ${logType}_planks`, + ) + logger.log(`NeededAmount: ${neededAmount}, while I have ${planksCount}`) + + const crafted = await craftRecipe( + mineflayer, + `${logType}_planks`, + logsToCraft * PLANKS_PER_LOG, + ) + if (crafted) { + planksCount = getItemCount(mineflayer, 'planks') + mineflayer.bot.chat( + `I have crafted ${logsToCraft * PLANKS_PER_LOG} ${logType} planks.`, + ) + logger.log(`Bot: ${logType} planks crafted.`) + } + else { + logger.error(`Bot: Failed to craft ${logType} planks.`) + return false + } + + // Check if we have enough planks after crafting + if (planksCount >= neededAmount) + break + } + } + + return planksCount >= neededAmount +}; + +// Helper function to ensure a specific amount of sticks +export async function ensureSticks(mineflayer: Mineflayer, neededAmount: number): Promise { + logger.log('Bot: Checking for sticks...') + + let sticksCount = getItemCount(mineflayer, 'stick') + + if (neededAmount <= sticksCount) { + logger.log('Bot: Have enough sticks.') + return true + } + + while (neededAmount >= sticksCount) { + const planksCount = getItemCount(mineflayer, 'planks') + const planksNeeded = Math.max( + Math.ceil((neededAmount - sticksCount) / STICKS_PER_PLANK), + 4, + ) + + if (planksCount >= planksNeeded) { + try { + const sticksId = getItemId('stick') + const recipe = await mineflayer.bot.recipesFor(sticksId, null, 1, null)[0] + await mineflayer.bot.craft(recipe, neededAmount - sticksCount) + sticksCount = getItemCount(mineflayer, 'stick') + mineflayer.bot.chat(`I have made ${Math.abs(neededAmount - sticksCount)} sticks.`) + logger.log(`Bot: Sticks crafted.`) + } + catch (err) { + logger.withError(err).error('Bot: Failed to craft sticks.') + return false + } + } + else { + await ensurePlanks(mineflayer, planksNeeded) + logger.error('Bot: Not enough planks for sticks.') + } + } + + return sticksCount >= neededAmount +} + +// Ensure a specific number of chests +export async function ensureChests(mineflayer: Mineflayer, quantity: number = 1): Promise { + logger.log(`Bot: Checking for ${quantity} chest(s)...`) + + // Count the number of chests the bot already has + let chestCount = getItemCount(mineflayer, 'chest') + + if (chestCount >= quantity) { + logger.log(`Bot: Already has ${quantity} or more chest(s).`) + return true + } + + while (chestCount < quantity) { + const planksEnsured = await ensurePlanks(mineflayer, 8 * quantity) // 8 planks per chest + if (!planksEnsured) { + logger.error('Bot: Failed to ensure planks for chest(s).') + continue + } + + // Craft the chest(s) + const crafted = await craftRecipe(mineflayer, 'chest', quantity - chestCount) + if (crafted) { + chestCount = getItemCount(mineflayer, 'chest') + mineflayer.bot.chat(`I have crafted ${quantity} chest(s).`) + logger.log(`Bot: ${quantity} chest(s) crafted.`) + continue + } + else { + logger.error('Bot: Failed to craft chest(s).') + } + } + return chestCount >= quantity +} + +// Ensure a specific number of furnaces +export async function ensureFurnaces(mineflayer: Mineflayer, quantity: number = 1): Promise { + logger.log(`Bot: Checking for ${quantity} furnace(s)...`) + + // Count the number of furnaces the bot already has + let furnaceCount = getItemCount(mineflayer, 'furnace') + + if (furnaceCount >= quantity) { + logger.log(`Bot: Already has ${quantity} or more furnace(s).`) + return true + } + + while (furnaceCount < quantity) { + const stoneEnsured = await ensureCobblestone(mineflayer, 8 * (quantity - furnaceCount)) // 8 stone blocks per furnace + if (!stoneEnsured) { + logger.error('Bot: Failed to ensure stone for furnace(s).') + continue + } + + // Craft the furnace(s) + const crafted = await craftRecipe(mineflayer, 'furnace', quantity - furnaceCount) + if (crafted) { + furnaceCount = getItemCount(mineflayer, 'furnace') + mineflayer.bot.chat(`I have crafted ${quantity} furnace(s).`) + logger.log(`Bot: ${quantity} furnace(s) crafted.`) + continue + } + else { + logger.error('Bot: Failed to craft furnace(s).') + } + } + return furnaceCount >= quantity +} + +// Ensure a specific number of torches +export async function ensureTorches(mineflayer: Mineflayer, quantity: number = 1): Promise { + logger.log(`Bot: Checking for ${quantity} torch(es)...`) + + // Count the number of torches the bot already has + let torchCount = getItemCount(mineflayer, 'torch') + + if (torchCount >= quantity) { + logger.log(`Bot: Already has ${quantity} or more torch(es).`) + return true + } + + while (torchCount < quantity) { + const sticksEnsured = await ensureSticks(mineflayer, quantity - torchCount) // 1 stick per 4 torches + const coalEnsured = await ensureCoal( + mineflayer, + Math.ceil((quantity - torchCount) / 4), + ) // 1 coal per 4 torches + + if (!sticksEnsured || !coalEnsured) { + logger.error('Bot: Failed to ensure sticks or coal for torch(es).') + continue + } + + // Craft the torch(es) + const crafted = await craftRecipe(mineflayer, 'torch', quantity - torchCount) + if (crafted) { + torchCount = getItemCount(mineflayer, 'torch') + mineflayer.bot.chat(`I have crafted ${quantity} torch(es).`) + logger.log(`Bot: ${quantity} torch(es) crafted.`) + continue + } + else { + logger.error('Bot: Failed to craft torch(es).') + } + } + return torchCount >= quantity +} + +// Ensure a campfire +// Todo: rework +export async function ensureCampfire(mineflayer: Mineflayer): Promise { + logger.log('Bot: Checking for a campfire...') + + const hasCampfire = getItemCount(mineflayer, 'campfire') > 0 + + if (hasCampfire) { + logger.log('Bot: Campfire is already available.') + return true + } + + const logsEnsured = await ensurePlanks(mineflayer, 3) // Need 3 logs for a campfire + const sticksEnsured = await ensureSticks(mineflayer, 3) // Need 3 sticks for a campfire + const coalEnsured = await ensureCoal(mineflayer, 1) // Need 1 coal or charcoal for a campfire + + if (!logsEnsured || !sticksEnsured || !coalEnsured) { + logger.error('Bot: Failed to ensure resources for campfire.') + } + + const crafted = await craftRecipe(mineflayer, 'campfire', 1) + if (crafted) { + mineflayer.bot.chat('I have crafted a campfire.') + logger.log('Bot: Campfire crafted.') + return true + } + else { + logger.error('Bot: Failed to craft campfire.') + } + + return hasCampfire +} + +// Helper function to gather cobblestone +export async function ensureCobblestone(mineflayer: Mineflayer, requiredCobblestone: number, maxDistance: number = 4): Promise { + let cobblestoneCount = getItemCount(mineflayer, 'cobblestone') + + while (cobblestoneCount < requiredCobblestone) { + logger.log('Bot: Gathering more cobblestone...') + const cobblestoneShortage = requiredCobblestone - cobblestoneCount + + try { + const success = await collectBlock( + mineflayer, + 'stone', + cobblestoneShortage, + maxDistance, + ) + if (!success) { + await moveAway(mineflayer, 30) + continue + } + } + catch (err) { + if (err instanceof Error && err.message.includes('right tools')) { + await ensurePickaxe(mineflayer) + continue + } + else { + logger.withError(err).error('Error collecting cobblestone') + await moveAway(mineflayer, 30) + continue + } + } + + cobblestoneCount = getItemCount(mineflayer, 'cobblestone') + } + + logger.log('Bot: Collected enough cobblestone.') + return true +} + +export async function ensureCoal(mineflayer: Mineflayer, neededAmount: number, maxDistance: number = 4): Promise { + logger.log('Bot: Checking for coal...') + let coalCount = getItemCount(mineflayer, 'coal') + + while (coalCount < neededAmount) { + logger.log('Bot: Gathering more coal...') + const coalShortage = neededAmount - coalCount + + try { + await collectBlock(mineflayer, 'stone', coalShortage, maxDistance) + } + catch (err) { + if (err instanceof Error && err.message.includes('right tools')) { + await ensurePickaxe(mineflayer) + continue + } + else { + logger.withError(err).error('Error collecting cobblestone:') + moveAway(mineflayer, 30) + continue + } + } + + coalCount = getItemCount(mineflayer, 'cobblestone') + } + + logger.log('Bot: Collected enough cobblestone.') + return true +} + +// Define the valid tool types as a union type +type ToolType = 'pickaxe' | 'sword' | 'axe' | 'shovel' | 'hoe' + +// Define the valid materials as a union type +type MaterialType = 'diamond' | 'golden' | 'iron' | 'stone' | 'wooden' + +// Constants for crafting tools +const TOOLS_MATERIALS: MaterialType[] = [ + 'diamond', + 'golden', + 'iron', + 'stone', + 'wooden', +] + +export function materialsForTool(tool: ToolType): number { + switch (tool) { + case 'pickaxe': + case 'axe': + return 3 + case 'sword': + case 'hoe': + return 2 + case 'shovel': + return 1 + default: + return 0 + } +} + +// Helper function to ensure a specific tool, checking from best materials to wood +async function ensureTool(mineflayer: Mineflayer, toolType: ToolType, quantity: number = 1): Promise { + logger.log(`Bot: Checking for ${quantity} ${toolType}(s)...`) + + const neededMaterials = materialsForTool(toolType) + + // Check how many of the tool the bot currently has + let toolCount = mineflayer.bot.inventory + .items() + .filter(item => item.name.includes(toolType)) + .length + + if (toolCount >= quantity) { + logger.log(`Bot: Already has ${quantity} or more ${toolType}(s).`) + return true + } + + while (toolCount < quantity) { + // Iterate over the tool materials from best (diamond) to worst (wooden) + for (const material of TOOLS_MATERIALS) { + const toolRecipe = `${material}_${toolType}` // Craft tool name like diamond_pickaxe, iron_sword + const hasResources = await hasResourcesForTool(mineflayer, material, neededMaterials) + + // Check if we have enough material for the current tool + if (hasResources) { + await ensureCraftingTable(mineflayer) + + const sticksEnsured = await ensureSticks(mineflayer, 2) + + if (!sticksEnsured) { + logger.error( + `Bot: Failed to ensure planks or sticks for wooden ${toolType}.`, + ) + continue + } + + // Craft the tool + const crafted = await craftRecipe(mineflayer, toolRecipe, 1) + if (crafted) { + toolCount++ + mineflayer.bot.chat( + `I have crafted a ${material} ${toolType}. Total ${toolType}(s): ${toolCount}/${quantity}`, + ) + logger.log( + `Bot: ${material} ${toolType} crafted. Total ${toolCount}/${quantity}`, + ) + if (toolCount >= quantity) + return true + } + else { + logger.error(`Bot: Failed to craft ${material} ${toolType}.`) + } + } + else if (material === 'wooden') { + // Crafting planks if we don't have enough resources for wooden tools + logger.log(`Bot: Crafting planks for ${material} ${toolType}...`) + await ensurePlanks(mineflayer, 4) + } + } + } + + return toolCount >= quantity +} + +// Helper function to check if the bot has enough materials to craft a tool of a specific material +export async function hasResourcesForTool( + mineflayer: Mineflayer, + material: MaterialType, + num = 3, // Number of resources needed for most tools +): Promise { + switch (material) { + case 'diamond': + return getItemCount(mineflayer, 'diamond') >= num + case 'golden': + return getItemCount(mineflayer, 'gold_ingot') >= num + case 'iron': + return getItemCount(mineflayer, 'iron_ingot') >= num + case 'stone': + return getItemCount(mineflayer, 'cobblestone') >= num + case 'wooden': + return getItemCount(mineflayer, 'planks') >= num + default: + return false + } +} + +// Helper functions for specific tools: + +// Ensure a pickaxe +export async function ensurePickaxe(mineflayer: Mineflayer, quantity: number = 1): Promise { + return await ensureTool(mineflayer, 'pickaxe', quantity) +}; + +// Ensure a sword +export async function ensureSword(mineflayer: Mineflayer, quantity: number = 1): Promise { + return await ensureTool(mineflayer, 'sword', quantity) +}; + +// Ensure an axe +export async function ensureAxe(mineflayer: Mineflayer, quantity: number = 1): Promise { + return await ensureTool(mineflayer, 'axe', quantity) +}; + +// Ensure a shovel +export async function ensureShovel(mineflayer: Mineflayer, quantity: number = 1): Promise { + return await ensureTool(mineflayer, 'shovel', quantity) +}; + +export async function ensureHoe(mineflayer: Mineflayer, quantity: number = 1): Promise { + return await ensureTool(mineflayer, 'hoe', quantity) +}; diff --git a/services/minecraft/src/skills/actions/gatherWood.ts b/services/minecraft/src/skills/actions/gatherWood.ts new file mode 100644 index 000000000..678468c16 --- /dev/null +++ b/services/minecraft/src/skills/actions/gatherWood.ts @@ -0,0 +1,100 @@ +import type { Mineflayer } from '../../libs/mineflayer' +import { useLogg } from '@guiiai/logg' +import { getNearestBlocks } from '../../composables/world' +import { sleep } from '../../utils/helper' +import { breakBlockAt } from '../blocks' +import { goToPosition, moveAway } from '../movement' +import { pickupNearbyItems } from './world-interactions' + +const logger = useLogg('Action:GatherWood').useGlobalConfig() + +/** + * Gather wood blocks nearby to collect logs. + * + * @param mineflayer The mineflayer instance. + * @param num The number of wood logs to gather. + * @param maxDistance The maximum distance to search for wood blocks. + * @returns Whether the wood gathering was successful. + */ +export async function gatherWood( + mineflayer: Mineflayer, + num: number, + maxDistance = 64, +): Promise { + logger.log(`Gathering wood... I need to collect ${num} logs.`) + mineflayer.bot.chat(`Gathering wood... I need to collect ${num} logs.`) + + try { + let logsCount = getLogsCount(mineflayer) + logger.log(`I currently have ${logsCount} logs.`) + + while (logsCount < num) { + // Gather 1 extra log to account for any failures + logger.log(`Looking for wood blocks nearby...`, logsCount, num) + + const woodBlock = mineflayer.bot.findBlock({ + matching: block => block.name.includes('log'), + maxDistance, + }) + + if (!woodBlock) { + logger.log('No wood blocks found nearby.') + await moveAway(mineflayer, 50) + continue + } + + const destinationReached = await goToPosition( + mineflayer, + woodBlock.position.x, + woodBlock.position.y, + woodBlock.position.z, + 2, + ) + + if (!destinationReached) { + logger.log('Unable to reach the wood block.') + continue // Try finding another wood block + } + + const aTree = await getNearestBlocks(mineflayer, woodBlock.name, 4, 4) + if (aTree.length === 0) { + logger.log('No wood blocks found nearby.') + await moveAway(mineflayer, 15) + continue + } + + try { + for (const aLog of aTree) { + await breakBlockAt(mineflayer, aLog.position.x, aLog.position.y, aLog.position.z) + await sleep(1200) // Simulate gathering delay + } + await pickupNearbyItems(mineflayer) + await sleep(2500) + logsCount = getLogsCount(mineflayer) + logger.log(`Collected logs. Total logs now: ${logsCount}.`) + } + catch (digError) { + console.error('Failed to break the wood block:', digError) + continue // Attempt to find and break another wood block + } + } + + logger.log(`Wood gathering complete! Total logs collected: ${logsCount}.`) + return true + } + catch (error) { + console.error('Failed to gather wood:', error) + return false + } +} + +/** + * Helper function to count the number of logs in the inventory. + * @returns The total number of logs. + */ +export function getLogsCount(mineflayer: Mineflayer): number { + return mineflayer.bot.inventory + .items() + .filter(item => item.name.includes('log')) + .reduce((acc, item) => acc + item.count, 0) +} diff --git a/services/minecraft/src/skills/actions/inventory.ts b/services/minecraft/src/skills/actions/inventory.ts new file mode 100644 index 000000000..c5efc50fd --- /dev/null +++ b/services/minecraft/src/skills/actions/inventory.ts @@ -0,0 +1,303 @@ +import type { Item } from 'prismarine-item' +import type { Mineflayer } from '../../libs/mineflayer' + +import { useLogg } from '@guiiai/logg' +import { getNearestBlock } from '../../composables/world' +import { goToPlayer, goToPosition } from '../movement' + +const logger = useLogg('Action:Inventory').useGlobalConfig() + +/** + * Equip an item from the bot's inventory. + * @param mineflayer The mineflayer instance. + * @param itemName The name of the item to equip. + * @returns Whether the item was successfully equipped. + */ +export async function equip(mineflayer: Mineflayer, itemName: string): Promise { + const item = mineflayer.bot.inventory + .items() + .find(item => item.name.includes(itemName)) + if (!item) { + logger.log(`You do not have any ${itemName} to equip.`) + return false + } + let destination: 'hand' | 'head' | 'torso' | 'legs' | 'feet' = 'hand' + if (itemName.includes('leggings')) + destination = 'legs' + else if (itemName.includes('boots')) + destination = 'feet' + else if (itemName.includes('helmet')) + destination = 'head' + else if (itemName.includes('chestplate')) + destination = 'torso' + + await mineflayer.bot.equip(item, destination) + return true +} + +/** + * Discard an item from the bot's inventory. + * @param mineflayer The mineflayer instance. + * @param itemName The name of the item to discard. + * @param num The number of items to discard. Default is -1 for all. + * @returns Whether the item was successfully discarded. + */ +export async function discard(mineflayer: Mineflayer, itemName: string, num = -1): Promise { + let discarded = 0 + while (true) { + const item = mineflayer.bot.inventory + .items() + .find(item => item.name.includes(itemName)) + if (!item) { + break + } + const toDiscard + = num === -1 ? item.count : Math.min(num - discarded, item.count) + await mineflayer.bot.toss(item.type, null, toDiscard) + discarded += toDiscard + if (num !== -1 && discarded >= num) { + break + } + } + if (discarded === 0) { + logger.log(`You do not have any ${itemName} to discard.`) + return false + } + logger.log(`Successfully discarded ${discarded} ${itemName}.`) + return true +} + +export async function putInChest(mineflayer: Mineflayer, itemName: string, num = -1): Promise { + const chest = getNearestBlock(mineflayer, 'chest', 32) + if (!chest) { + logger.log(`Could not find a chest nearby.`) + return false + } + const item = mineflayer.bot.inventory + .items() + .find(item => item.name.includes(itemName)) + if (!item) { + logger.log(`You do not have any ${itemName} to put in the chest.`) + return false + } + const toPut = num === -1 ? item.count : Math.min(num, item.count) + await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z) + const chestContainer = await mineflayer.bot.openContainer(chest) + await chestContainer.deposit(item.type, null, toPut) + await chestContainer.close() + logger.log(`Successfully put ${toPut} ${itemName} in the chest.`) + return true +} + +export async function takeFromChest( + mineflayer: Mineflayer, + itemName: string, + num = -1, +): Promise { + const chest = getNearestBlock(mineflayer, 'chest', 32) + if (!chest) { + logger.log(`Could not find a chest nearby.`) + return false + } + await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z) + const chestContainer = await mineflayer.bot.openContainer(chest) + const item = chestContainer + .containerItems() + .find(item => item.name.includes(itemName)) + if (!item) { + logger.log(`Could not find any ${itemName} in the chest.`) + await chestContainer.close() + return false + } + const toTake = num === -1 ? item.count : Math.min(num, item.count) + await chestContainer.withdraw(item.type, null, toTake) + await chestContainer.close() + logger.log(`Successfully took ${toTake} ${itemName} from the chest.`) + return true +} + +/** + * View the contents of a chest near the bot. + * @param mineflayer The mineflayer instance. + * @returns Whether the chest was successfully viewed. + */ +export async function viewChest(mineflayer: Mineflayer): Promise { + const chest = getNearestBlock(mineflayer, 'chest', 32) + if (!chest) { + logger.log(`Could not find a chest nearby.`) + return false + } + await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z) + const chestContainer = await mineflayer.bot.openContainer(chest) + const items = chestContainer.containerItems() + if (items.length === 0) { + logger.log(`The chest is empty.`) + } + else { + logger.log(`The chest contains:`) + for (const item of items) { + logger.log(`${item.count} ${item.name}`) + } + } + await chestContainer.close() + return true +} + +/** + * Ask to bot to eat a food item from its inventory. + * @param mineflayer The mineflayer instance. + * @param foodName The name of the food item to eat. + * @returns Whether the food was successfully eaten. + */ +export async function eat(mineflayer: Mineflayer, foodName = ''): Promise { + let item: Item | undefined + let name: string + if (foodName) { + item = mineflayer.bot.inventory.items().find(item => item.name.includes(foodName)) + name = foodName + } + else { + // @ts-expect-error -- ? + item = mineflayer.bot.inventory.items().find(item => item.foodPoints > 0) + name = 'food' + } + if (!item) { + logger.log(`You do not have any ${name} to eat.`) + return false + } + await mineflayer.bot.equip(item, 'hand') + await mineflayer.bot.consume() + logger.log(`Successfully ate ${item.name}.`) + return true +} + +/** + * Give an item to a player. + * @param mineflayer The mineflayer instance. + * @param itemType The name of the item to give. + * @param username The username of the player to give the item to. + * @param num The number of items to give. + * @returns Whether the item was successfully given. + */ +export async function giveToPlayer( + mineflayer: Mineflayer, + itemType: string, + username: string, + num = 1, +): Promise { + const player = mineflayer.bot.players[username]?.entity + if (!player) { + logger.log(`Could not find a player with username: ${username}.`) + return false + } + await goToPlayer(mineflayer, username) + await mineflayer.bot.lookAt(player.position) + await discard(mineflayer, itemType, num) + return true +} + +/** + * List the items in the bot's inventory. + * @param mineflayer The mineflayer instance. + * @returns An array of items in the bot's inventory. + */ +export async function listInventory(mineflayer: Mineflayer): Promise<{ name: string, count: number }[]> { + const items = await mineflayer.bot.inventory.items() + sayItems(mineflayer, items) + + return items.map(item => ({ + name: item.name, + count: item.count, + })) +} + +export async function checkForItem(mineflayer: Mineflayer, itemName: string): Promise { + const items = await mineflayer.bot.inventory.items() + const searchableItems = items.filter(item => item.name.includes(itemName)) + sayItems(mineflayer, searchableItems) +} + +export async function sayItems(mineflayer: Mineflayer, items: Array | null = null) { + if (!items) { + items = mineflayer.bot.inventory.items() + if (mineflayer.bot.registry.isNewerOrEqualTo('1.9') && mineflayer.bot.inventory.slots[45]) + items.push(mineflayer.bot.inventory.slots[45]) + } + const output = items.map(item => `${item.name} x ${item.count}`).join(', ') + if (output) { + mineflayer.bot.chat(`My inventory contains: ${output}`) + } + else { + mineflayer.bot.chat('My inventory is empty.`') + } +} + +/** + * Find the number of free slots in the bot's inventory. + * @param mineflayer The mineflayer instance. + * @returns The number of free slots in the bot's inventory. + */ +export function checkFreeSpace(mineflayer: Mineflayer): number { + const totalSlots = mineflayer.bot.inventory.slots.length + const usedSlots = mineflayer.bot.inventory.items().length + const freeSlots = totalSlots - usedSlots + logger.log(`You have ${freeSlots} free slots in your inventory.`) + return freeSlots +} + +/** + * Transfer all items from the bot's inventory to a chest. + * @param mineflayer The mineflayer instance. + * @returns Whether the items were successfully transferred. + */ +export async function transferAllToChest(mineflayer: Mineflayer): Promise { + const chest = getNearestBlock(mineflayer, 'chest', 32) + if (!chest) { + logger.log(`Could not find a chest nearby.`) + return false + } + await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z) + const chestContainer = await mineflayer.bot.openContainer(chest) + + for (const item of mineflayer.bot.inventory.items()) { + await chestContainer.deposit(item.type, null, item.count) + logger.log(`Put ${item.count} ${item.name} in the chest.`) + } + + await chestContainer.close() + return true +} + +/** + * Utility function to get item count in inventory + * @param mineflayer The mineflayer instance. + * @param itemName - The name of the item to count. + * @returns number of items in inventory + */ +export function getItemCount(mineflayer: Mineflayer, itemName: string): number { + return mineflayer.bot.inventory + .items() + .filter(item => item.name.includes(itemName)) + .reduce((acc, item) => acc + item.count, 0) +} + +/** + * Organize the bot's inventory. + * @param mineflayer The mineflayer instance. + * @returns Whether the inventory was successfully organized. + */ +export async function organizeInventory(mineflayer: Mineflayer): Promise { + const items = mineflayer.bot.inventory.items() + if (items.length === 0) { + logger.log(`Inventory is empty, nothing to organize.`) + return + } + + for (const item of items) { + await mineflayer.bot.moveSlotItem( + item.slot, + mineflayer.bot.inventory.findInventoryItem(item.type, null, false)?.slot ?? item.slot, + ) + } + logger.log(`Inventory has been organized.`) +} diff --git a/services/minecraft/src/skills/actions/world-interactions.ts b/services/minecraft/src/skills/actions/world-interactions.ts new file mode 100644 index 000000000..c4d1b77b7 --- /dev/null +++ b/services/minecraft/src/skills/actions/world-interactions.ts @@ -0,0 +1,372 @@ +import type { Bot } from 'mineflayer' +import type { Block } from 'prismarine-block' +import type { Mineflayer } from '../../libs/mineflayer' +import { useLogg } from '@guiiai/logg' +import pathfinder from 'mineflayer-pathfinder' +import { Vec3 } from 'vec3' +import { sleep } from '../../utils/helper' +import { getNearestBlock, makeItem } from '../../utils/mcdata' +import { goToPosition } from '../movement' + +const logger = useLogg('Action:WorldInteractions').useGlobalConfig() + +export async function placeBlock( + mineflayer: Mineflayer, + blockType: string, + x: number, + y: number, + z: number, + placeOn: string = 'bottom', +): Promise { + // if (!gameData.getBlockId(blockType)) { + // logger.log(`Invalid block type: ${blockType}.`); + // return false; + // } + + const targetDest = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)) + + let block = mineflayer.bot.inventory + .items() + .find(item => item.name.includes(blockType)) + if (!block && mineflayer.bot.game.gameMode === 'creative') { + // TODO: Rework + await mineflayer.bot.creative.setInventorySlot(36, makeItem(blockType, 1)) // 36 is first hotbar slot + block = mineflayer.bot.inventory.items().find(item => item.name.includes(blockType)) + } + if (!block) { + logger.log(`Don't have any ${blockType} to place.`) + return false + } + + const targetBlock = mineflayer.bot.blockAt(targetDest) + if (!targetBlock) { + logger.log(`No block found at ${targetDest}.`) + return false + } + + if (targetBlock.name === blockType) { + logger.log(`${blockType} already at ${targetBlock.position}.`) + return false + } + + const emptyBlocks = [ + 'air', + 'water', + 'lava', + 'grass', + 'tall_grass', + 'snow', + 'dead_bush', + 'fern', + ] + if (!emptyBlocks.includes(targetBlock.name)) { + logger.log( + `${targetBlock.name} is in the way at ${targetBlock.position}.`, + ) + const removed = await breakBlockAt(mineflayer, x, y, z) + if (!removed) { + logger.log( + `Cannot place ${blockType} at ${targetBlock.position}: block in the way.`, + ) + return false + } + await new Promise(resolve => setTimeout(resolve, 200)) // Wait for block to break + } + + // Determine the build-off block and face vector + const dirMap: { [key: string]: Vec3 } = { + top: new Vec3(0, 1, 0), + bottom: new Vec3(0, -1, 0), + north: new Vec3(0, 0, -1), + south: new Vec3(0, 0, 1), + east: new Vec3(1, 0, 0), + west: new Vec3(-1, 0, 0), + } + + const dirs: Vec3[] = [] + if (placeOn === 'side') { + dirs.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west) + } + else if (dirMap[placeOn]) { + dirs.push(dirMap[placeOn]) + } + else { + dirs.push(dirMap.bottom) + logger.log(`Unknown placeOn value "${placeOn}". Defaulting to bottom.`) + } + + // Add remaining directions + dirs.push(...Object.values(dirMap).filter(d => !dirs.includes(d))) + + let buildOffBlock: Block | null = null + let faceVec: Vec3 | null = null + + for (const d of dirs) { + const adjacentBlock = mineflayer.bot.blockAt(targetDest.plus(d)) + if (adjacentBlock && !emptyBlocks.includes(adjacentBlock.name)) { + buildOffBlock = adjacentBlock + faceVec = d.scaled(-1) // Invert direction + break + } + } + + if (!buildOffBlock || !faceVec) { + logger.log( + `Cannot place ${blockType} at ${targetBlock.position}: nothing to place on.`, + ) + return false + } + + // Move away if too close + const pos = mineflayer.bot.entity.position + const posAbove = pos.offset(0, 1, 0) + const dontMoveFor = [ + 'torch', + 'redstone_torch', + 'redstone', + 'lever', + 'button', + 'rail', + 'detector_rail', + 'powered_rail', + 'activator_rail', + 'tripwire_hook', + 'tripwire', + 'water_bucket', + ] + if ( + !dontMoveFor.includes(blockType) + && (pos.distanceTo(targetBlock.position) < 1 + || posAbove.distanceTo(targetBlock.position) < 1) + ) { + const goal = new pathfinder.goals.GoalInvert( + new pathfinder.goals.GoalNear( + targetBlock.position.x, + targetBlock.position.y, + targetBlock.position.z, + 2, + ), + ) + // bot.pathfinder.setMovements(new pf.Movements(bot)); + await mineflayer.bot.pathfinder.goto(goal) + } + + // Move closer if too far + if (mineflayer.bot.entity.position.distanceTo(targetBlock.position) > 4.5) { + await goToPosition( + mineflayer, + targetBlock.position.x, + targetBlock.position.y, + targetBlock.position.z, + 4, + ) + } + + await mineflayer.bot.equip(block, 'hand') + await mineflayer.bot.lookAt(buildOffBlock.position) + await sleep(500) + + try { + await mineflayer.bot.placeBlock(buildOffBlock, faceVec) + logger.log(`Placed ${blockType} at ${targetDest}.`) + await new Promise(resolve => setTimeout(resolve, 200)) + return true + } + catch (err) { + if (err instanceof Error) { + logger.log( + `Failed to place ${blockType} at ${targetDest}: ${err.message}`, + ) + } + else { + logger.log( + `Failed to place ${blockType} at ${targetDest}: ${String(err)}`, + ) + } + return false + } +} + +export async function breakBlockAt( + mineflayer: Mineflayer, + x: number, + y: number, + z: number, +): Promise { + if (x == null || y == null || z == null) { + throw new Error('Invalid position to break block at.') + } + const blockPos = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)) + const block = mineflayer.bot.blockAt(blockPos) + if (!block) { + logger.log(`No block found at position ${blockPos}.`) + return false + } + if (block.name !== 'air' && block.name !== 'water' && block.name !== 'lava') { + if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) { + await goToPosition(mineflayer, x, y, z) + } + if (mineflayer.bot.game.gameMode !== 'creative') { + await mineflayer.bot.tool.equipForBlock(block) + const itemId = mineflayer.bot.heldItem ? mineflayer.bot.heldItem.type : null + if (!block.canHarvest(itemId)) { + logger.log(`Don't have right tools to break ${block.name}.`) + return false + } + } + if (!mineflayer.bot.canDigBlock(block)) { + logger.log(`Cannot break ${block.name} at ${blockPos}.`) + return false + } + await mineflayer.bot.lookAt(block.position, true) // Ensure the bot has finished turning + await sleep(500) + try { + await mineflayer.bot.dig(block, true) + logger.log( + `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed( + 1, + )}, z:${z.toFixed(1)}.`, + ) + return true + } + catch (err) { + console.error(`Failed to dig the block: ${err}`) + return false + } + } + else { + logger.log( + `Skipping block at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed( + 1, + )} because it is ${block.name}.`, + ) + return false + } +} + +export async function activateNearestBlock(mineflayer: Mineflayer, type: string) { + /** + * Activate the nearest block of the given type. + * @param {string} type, the type of block to activate. + * @returns {Promise} true if the block was activated, false otherwise. + * @example + * await skills.activateNearestBlock( "lever"); + * + */ + const block = getNearestBlock(mineflayer.bot, type, 16) + if (!block) { + logger.log(`Could not find any ${type} to activate.`) + return false + } + if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) { + const pos = block.position + // bot.pathfinder.setMovements(new pf.Movements(bot)); + await mineflayer.bot.pathfinder.goto(new pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, 4)) + } + await mineflayer.bot.activateBlock(block) + logger.log( + `Activated ${type} at x:${block.position.x.toFixed( + 1, + )}, y:${block.position.y.toFixed(1)}, z:${block.position.z.toFixed(1)}.`, + ) + return true +} + +export async function tillAndSow( + mineflayer: Mineflayer, + x: number, + y: number, + z: number, + seedType: string | null = null, +): Promise { + x = Math.round(x) + y = Math.round(y) + z = Math.round(z) + const blockPos = new Vec3(x, y, z) + const block = mineflayer.bot.blockAt(blockPos) + if (!block) { + logger.log(`No block found at ${blockPos}.`) + return false + } + if ( + block.name !== 'grass_block' + && block.name !== 'dirt' + && block.name !== 'farmland' + ) { + logger.log(`Cannot till ${block.name}, must be grass_block or dirt.`) + return false + } + const above = mineflayer.bot.blockAt(blockPos.offset(0, 1, 0)) + if (above && above.name !== 'air') { + logger.log(`Cannot till, there is ${above.name} above the block.`) + return false + } + // Move closer if too far + if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) { + await goToPosition(mineflayer, x, y, z, 4) + } + if (block.name !== 'farmland') { + const hoe = mineflayer.bot.inventory.items().find(item => item.name.includes('hoe')) + if (!hoe) { + logger.log(`Cannot till, no hoes.`) + return false + } + await mineflayer.bot.equip(hoe, 'hand') + await mineflayer.bot.activateBlock(block) + logger.log( + `Tilled block x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`, + ) + } + + if (seedType) { + if (seedType.endsWith('seed') && !seedType.endsWith('seeds')) + seedType += 's' // Fixes common mistake + const seeds = mineflayer.bot.inventory + .items() + .find(item => item.name.includes(seedType || 'seed')) + if (!seeds) { + logger.log(`No ${seedType} to plant.`) + return false + } + await mineflayer.bot.equip(seeds, 'hand') + await mineflayer.bot.placeBlock(block, new Vec3(0, -1, 0)) + logger.log( + `Planted ${seedType} at x:${x.toFixed(1)}, y:${y.toFixed( + 1, + )}, z:${z.toFixed(1)}.`, + ) + } + return true +} + +export async function pickupNearbyItems( + mineflayer: Mineflayer, + distance = 8, +): Promise { + const getNearestItem = (bot: Bot) => + bot.nearestEntity( + entity => + entity.name === 'item' + && entity.onGround + && bot.entity.position.distanceTo(entity.position) < distance, + ) + let nearestItem = getNearestItem(mineflayer.bot) + + let pickedUp = 0 + while (nearestItem) { + // bot.pathfinder.setMovements(new pf.Movements(bot)); + await mineflayer.bot.pathfinder.goto( + new pathfinder.goals.GoalFollow(nearestItem, 0.8), + () => {}, + ) + await sleep(500) + const prev = nearestItem + nearestItem = getNearestItem(mineflayer.bot) + if (prev === nearestItem) { + break + } + pickedUp++ + } + logger.log(`Picked up ${pickedUp} items.`) + return true +} diff --git a/services/minecraft/src/skills/crafting.ts b/services/minecraft/src/skills/crafting.ts index a1efa06a1..795d75eff 100644 --- a/services/minecraft/src/skills/crafting.ts +++ b/services/minecraft/src/skills/crafting.ts @@ -1,281 +1,343 @@ +import type { Block } from 'prismarine-block' +import type { Item } from 'prismarine-item' +import type { Recipe } from 'prismarine-recipe' import type { Mineflayer } from '../libs/mineflayer' +import { useLogg } from '@guiiai/logg' import * as world from '../composables/world' +import { getInventoryCounts, getNearestBlock, getNearestFreeSpace } from '../composables/world' import * as mc from '../utils/mcdata' -import { log } from './base' +import { ensureCraftingTable } from './actions/ensure' import { collectBlock, placeBlock } from './blocks' -import { goToPosition } from './movement' +import { goToNearestBlock, goToPosition, moveAway } from './movement' -export async function craftRecipe(mineflayer: Mineflayer, itemName: string, num = 1): Promise { - let placedTable = false +const logger = useLogg('Skill:Crafting').useGlobalConfig() - if (mc.getItemCraftingRecipes(itemName)?.length === 0) { - log(mineflayer, `${itemName} is either not an item, or it does not have a crafting recipe!`) - return false - } +/* +Possible Scenarios: + +1. **Successful Craft Without Crafting Table**: + - The bot attempts to craft the item without a crafting table and succeeds. The function returns `true`. + +2. **Crafting Table Nearby**: + - The bot tries to craft without a crafting table but fails. + - The bot then checks for a nearby crafting table. + - If a crafting table is found, the bot moves to it and successfully crafts the item, returning `true`. + +3. **No Crafting Table Nearby, Place Crafting Table**: + - The bot fails to craft without a crafting table and does not find a nearby crafting table. + - The bot checks inventory for a crafting table, places it at a suitable location, and attempts crafting again. + - If successful, the function returns `true`. If the bot cannot find a suitable position or fails to craft, it returns `false`. + +4. **Insufficient Resources**: + - At any point, if the bot does not have the required resources to craft the item, it logs an appropriate message and returns `false`. + +5. **No Crafting Table and No Suitable Position**: + - If the bot does not find a crafting table and cannot find a suitable position to place one, it moves away and returns `false`. + +6. **Invalid Item Name**: + - If the provided item name is invalid, the function logs the error and returns `false`. +*/ +export async function craftRecipe( + mineflayer: Mineflayer, + incomingItemName: string, + num = 1, +): Promise { + let itemName = incomingItemName.replace(' ', '_').toLowerCase() + + if (itemName.endsWith('plank')) + itemName += 's' // Correct common mistakes - // Get recipes that don't require a crafting table const itemId = mc.getItemId(itemName) if (itemId === null) { - log(mineflayer, `Invalid item name: ${itemName}`) + logger.log(`Invalid item name: ${itemName}`) return false } - let recipes = mineflayer.bot.recipesFor(itemId, null, 1, null) - let craftingTable = null - const craftingTableRange = 32 - - if (!recipes || recipes.length === 0) { - recipes = mineflayer.bot.recipesFor(itemId, null, 1, true) - if (!recipes || recipes.length === 0) { - log(mineflayer, `You do not have the resources to craft a ${itemName}.`) - return false - } - - // Look for crafting table - craftingTable = world.getNearestBlock(mineflayer, 'crafting_table', craftingTableRange) - if (!craftingTable) { - // Try to place crafting table - const inventory = world.getInventoryCounts(mineflayer) - const hasTable = inventory.crafting_table > 0 - if (hasTable) { - const pos = world.getNearestFreeSpace(mineflayer, 1, 6) - if (pos) { - await placeBlock(mineflayer, 'crafting_table', pos.x, pos.y, pos.z) - craftingTable = world.getNearestBlock(mineflayer, 'crafting_table', craftingTableRange) - if (craftingTable) { - recipes = mineflayer.bot.recipesFor(itemId, null, 1, craftingTable) - placedTable = true - } - } + // Helper function to attempt crafting + async function attemptCraft( + recipes: Recipe[] | null, + craftingTable: Block | null = null, + ): Promise { + if (recipes && recipes.length > 0) { + const recipe = recipes[0] + try { + await mineflayer.bot.craft(recipe, num, craftingTable ?? undefined) + logger.log( + `Successfully crafted ${num} ${itemName}${ + craftingTable ? ' using crafting table' : '' + }.`, + ) + return true } - else { - log(mineflayer, `Crafting ${itemName} requires a crafting table.`) + catch (err) { + logger.log(`Failed to craft ${itemName}: ${(err as Error).message}`) return false } } - else { - recipes = mineflayer.bot.recipesFor(itemId, null, 1, craftingTable) - } - } - - if (!recipes || recipes.length === 0) { - log(mineflayer, `You do not have the resources to craft a ${itemName}. It requires: ${ - Object.entries(mc.getItemCraftingRecipes(itemName)?.[0] ?? {}) - .map(([key, value]) => `${key}: ${value}`) - .join(', ') - }.`) - if (placedTable && craftingTable) { - await collectBlock(mineflayer, 'crafting_table', 1) - } return false } - if (craftingTable && mineflayer.bot.entity.position.distanceTo(craftingTable.position) > 4) { - await goToPosition(mineflayer, craftingTable.position.x, craftingTable.position.y, craftingTable.position.z, 4) + // Helper function to move to a crafting table and attempt crafting with retry logic + async function moveToAndCraft(craftingTable: Block): Promise { + logger.log(`Crafting table found, moving to it.`) + const maxRetries = 2 + let attempts = 0 + let success = false + + while (attempts < maxRetries && !success) { + try { + await goToPosition( + mineflayer, + craftingTable.position.x, + craftingTable.position.y, + craftingTable.position.z, + 1, + ) + const recipes = mineflayer.bot.recipesFor(itemId, null, 1, craftingTable) + success = await attemptCraft(recipes, craftingTable) + } + catch (err) { + logger.log( + `Attempt ${attempts + 1} to move to crafting table failed: ${ + (err as Error).message + }`, + ) + } + attempts++ + } + + return success } - const recipe = recipes[0] - // Check that the agent has sufficient items to use the recipe `num` times - const inventory = world.getInventoryCounts(mineflayer) // Items in the agents inventory - const requiredIngredients = mc.ingredientsFromPrismarineRecipe(recipe) // Items required to use the recipe once - const craftLimit = mc.calculateLimitingResource(inventory, requiredIngredients) + // Helper function to find and use or place a crafting table + async function findAndUseCraftingTable( + craftingTableRange: number, + ): Promise { + let craftingTable = world.getNearestBlock(mineflayer, 'crafting_table', craftingTableRange) + if (craftingTable) { + return await moveToAndCraft(craftingTable) + } - await mineflayer.bot.craft(recipe, Math.min(craftLimit.num, num), craftingTable ?? undefined) + logger.log(`No crafting table nearby, attempting to place one.`) + const hasCraftingTable = await ensureCraftingTable(mineflayer) + if (!hasCraftingTable) { + logger.log(`Failed to ensure a crafting table to craft ${itemName}.`) + return false + } - if (craftLimit.num < num) { - log(mineflayer, `Not enough ${craftLimit.limitingResource} to craft ${num}, crafted ${craftLimit.num}. You now have ${world.getInventoryCounts(mineflayer)[itemName]} ${itemName}.`) - } - else { - log(mineflayer, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(mineflayer)[itemName]} ${itemName}.`) + const pos = getNearestFreeSpace(mineflayer, 1, 10) + if (pos) { + moveAway(mineflayer, 4) + logger.log( + `Placing crafting table at position (${pos.x}, ${pos.y}, ${pos.z}).`, + ) + await placeBlock(mineflayer, 'crafting_table', pos.x, pos.y, pos.z) + craftingTable = getNearestBlock(mineflayer, 'crafting_table', craftingTableRange) + if (craftingTable) { + return await moveToAndCraft(craftingTable) + } + } + else { + logger.log('No suitable position found to place the crafting table.') + moveAway(mineflayer, 5) + return false + } + + return false } - if (placedTable && craftingTable) { - await collectBlock(mineflayer, 'crafting_table', 1) + // Step 1: Try to craft without a crafting table + logger.log(`Step 1: Try to craft without a crafting table`) + const recipes = mineflayer.bot.recipesFor(itemId, null, 1, null) + if (recipes && (await attemptCraft(recipes))) { + return true } - // Equip any armor the bot may have crafted - mineflayer.bot.armorManager.equipAll() + // Step 2: Find and use a crafting table + logger.log(`Step 2: Find and use a crafting table`) + const craftingTableRange = 32 + if (await findAndUseCraftingTable(craftingTableRange)) { + return true + } - return true + return false } export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = 1): Promise { - if (!mc.isSmeltable(itemName)) { - log(mineflayer, `Cannot smelt ${itemName}. Hint: make sure you are smelting the 'raw' item.`) + const foods = [ + 'beef', + 'chicken', + 'cod', + 'mutton', + 'porkchop', + 'rabbit', + 'salmon', + 'tropical_fish', + ] + if (!itemName.includes('raw') && !foods.includes(itemName)) { + logger.log( + `Cannot smelt ${itemName}, must be a "raw" item, like "raw_iron".`, + ) return false - } + } // TODO: allow cobblestone, sand, clay, etc. let placedFurnace = false - const furnaceRange = 32 - let furnaceBlock = world.getNearestBlock(mineflayer, 'furnace', furnaceRange) - + let furnaceBlock = getNearestBlock(mineflayer, 'furnace', 32) if (!furnaceBlock) { // Try to place furnace - const inventory = world.getInventoryCounts(mineflayer) - const hasFurnace = inventory.furnace > 0 + const hasFurnace = getInventoryCounts(mineflayer).furnace > 0 if (hasFurnace) { - const pos = world.getNearestFreeSpace(mineflayer, 1, furnaceRange) + const pos = getNearestFreeSpace(mineflayer, 1, 32) if (pos) { await placeBlock(mineflayer, 'furnace', pos.x, pos.y, pos.z) - furnaceBlock = world.getNearestBlock(mineflayer, 'furnace', furnaceRange) - placedFurnace = true } + else { + logger.log('No suitable position found to place the furnace.') + return false + } + furnaceBlock = getNearestBlock(mineflayer, 'furnace', 32) + placedFurnace = true } } - if (!furnaceBlock) { - log(mineflayer, 'There is no furnace nearby and you have no furnace.') + logger.log(`There is no furnace nearby and I have no furnace.`) return false } - if (mineflayer.bot.entity.position.distanceTo(furnaceBlock.position) > 4) { - await goToPosition(mineflayer, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4) + await goToNearestBlock(mineflayer, 'furnace', 4, 32) } - await mineflayer.bot.lookAt(furnaceBlock.position) + logger.log('smelting...') const furnace = await mineflayer.bot.openFurnace(furnaceBlock) - // Check if the furnace is already smelting something const inputItem = furnace.inputItem() - const itemId = mc.getItemId(itemName) - if (itemId === null) { - log(mineflayer, `Invalid item name: ${itemName}`) - return false - } - - if (inputItem && inputItem.type !== itemId && inputItem.count > 0) { - log(mineflayer, `The furnace is currently smelting ${mc.getItemName(inputItem.type) ?? 'unknown'}.`) - if (placedFurnace) { + if ( + inputItem + && inputItem.type !== mc.getItemId(itemName) + && inputItem.count > 0 + ) { + logger.log( + `The furnace is currently smelting ${mc.getItemName( + inputItem.type, + )}.`, + ) + if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) - } return false } - // Check if the bot has enough items to smelt - const invCounts = world.getInventoryCounts(mineflayer) + const invCounts = getInventoryCounts(mineflayer) if (!invCounts[itemName] || invCounts[itemName] < num) { - log(mineflayer, `You do not have enough ${itemName} to smelt.`) - if (placedFurnace) { + logger.log(`I do not have enough ${itemName} to smelt.`) + if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) - } return false } // Fuel the furnace if (!furnace.fuelItem()) { - const fuel = mc.getSmeltingFuel(mineflayer.bot) - if (!fuel) { - log(mineflayer, `You have no fuel to smelt ${itemName}, you need coal, charcoal, or wood.`) - if (placedFurnace) { + const fuel = mineflayer.bot.inventory + .items() + .find(item => item.name === 'coal' || item.name === 'charcoal') + const putFuel = Math.ceil(num / 8) + if (!fuel || fuel.count < putFuel) { + logger.log( + `I do not have enough coal or charcoal to smelt ${num} ${itemName}, I need ${putFuel} coal or charcoal`, + ) + if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) - } return false } - - log(mineflayer, `Using ${fuel.name} as fuel.`) - const putFuel = Math.ceil(num / mc.getFuelSmeltOutput(fuel.name)) - - if (fuel.count < putFuel) { - log(mineflayer, `You don't have enough ${fuel.name} to smelt ${num} ${itemName}; you need ${putFuel}.`) - if (placedFurnace) { - await collectBlock(mineflayer, 'furnace', 1) - } - return false - } - await furnace.putFuel(fuel.type, null, putFuel) - log(mineflayer, `Added ${putFuel} ${mc.getItemName(fuel.type) ?? 'unknown'} to furnace fuel.`) + logger.log( + `Added ${putFuel} ${mc.getItemName(fuel.type)} to furnace fuel.`, + ) } - // Put the items in the furnace + const itemId = mc.getItemId(itemName) + if (itemId === null) { + logger.log(`Invalid item name: ${itemName}`) + return false + } await furnace.putInput(itemId, null, num) - // Wait for the items to smelt let total = 0 let collectedLast = true - let smeltedItem = null + let smeltedItem: Item | null = null await new Promise(resolve => setTimeout(resolve, 200)) - - mineflayer.once('interrupt', () => { - total = num // Force loop to end - }) - while (total < num) { await new Promise(resolve => setTimeout(resolve, 10000)) + logger.log('checking...') let collected = false - - const outputItem = furnace.outputItem() - if (outputItem) { + if (furnace.outputItem()) { smeltedItem = await furnace.takeOutput() if (smeltedItem) { total += smeltedItem.count collected = true } } - if (!collected && !collectedLast) { - break // If nothing was collected this time or last time + break // if nothing was collected this time or last time } - collectedLast = collected } - await mineflayer.bot.closeWindow(furnace) if (placedFurnace) { await collectBlock(mineflayer, 'furnace', 1) } - if (total === 0) { - log(mineflayer, `Failed to smelt ${itemName}.`) + logger.log(`Failed to smelt ${itemName}.`) return false } - if (total < num) { - log(mineflayer, `Only smelted ${total} ${mc.getItemName(smeltedItem?.type ?? 0) ?? 'unknown'}.`) + logger.log( + `Only smelted ${total} ${mc.getItemName(smeltedItem?.type || 0)}.`, + ) return false } - - log(mineflayer, `Successfully smelted ${itemName}, got ${total} ${mc.getItemName(smeltedItem?.type ?? 0) ?? 'unknown'}.`) + logger.log( + `Successfully smelted ${itemName}, got ${total} ${mc.getItemName( + smeltedItem?.type || 0, + )}.`, + ) return true } export async function clearNearestFurnace(mineflayer: Mineflayer): Promise { - const furnaceBlock = world.getNearestBlock(mineflayer, 'furnace', 32) + const furnaceBlock = getNearestBlock(mineflayer, 'furnace', 6) if (!furnaceBlock) { - log(mineflayer, 'No furnace nearby to clear.') + logger.log(`There is no furnace nearby.`) return false } - if (mineflayer.bot.entity.position.distanceTo(furnaceBlock.position) > 4) { - await goToPosition(mineflayer, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4) - } - + logger.log('clearing furnace...') const furnace = await mineflayer.bot.openFurnace(furnaceBlock) - + logger.log('opened furnace...') // Take the items out of the furnace - let smeltedItem, inputItem, fuelItem - - const outputItem = furnace.outputItem() - if (outputItem) { + let smeltedItem: Item | null = null + let inputItem: Item | null = null + let fuelItem: Item | null = null + if (furnace.outputItem()) smeltedItem = await furnace.takeOutput() - } - - const furnaceInput = furnace.inputItem() - if (furnaceInput) { + if (furnace.inputItem()) inputItem = await furnace.takeInput() - } - - const furnaceFuel = furnace.fuelItem() - if (furnaceFuel) { + if (furnace.fuelItem()) fuelItem = await furnace.takeFuel() - } - - const smeltedName = smeltedItem ? `${smeltedItem.count} ${smeltedItem.name}` : '0 smelted items' - const inputName = inputItem ? `${inputItem.count} ${inputItem.name}` : '0 input items' - const fuelName = fuelItem ? `${fuelItem.count} ${fuelItem.name}` : '0 fuel items' - - log(mineflayer, `Cleared furnace, received ${smeltedName}, ${inputName}, and ${fuelName}.`) + logger.log(smeltedItem, inputItem, fuelItem) + const smeltedName = smeltedItem + ? `${smeltedItem.count} ${smeltedItem.name}` + : `0 smelted items` + const inputName = inputItem + ? `${inputItem.count} ${inputItem.name}` + : `0 input items` + const fuelName = fuelItem + ? `${fuelItem.count} ${fuelItem.name}` + : `0 fuel items` + logger.log( + `Cleared furnace, received ${smeltedName}, ${inputName}, and ${fuelName}.`, + ) + await mineflayer.bot.closeWindow(furnace) return true } diff --git a/services/minecraft/src/skills/inventory.ts b/services/minecraft/src/skills/inventory.ts index 267a2432c..ddedfff6b 100644 --- a/services/minecraft/src/skills/inventory.ts +++ b/services/minecraft/src/skills/inventory.ts @@ -1,39 +1,8 @@ -import type { Bot } from 'mineflayer' import type { Mineflayer } from '../libs/mineflayer' -import pathfinderModel from 'mineflayer-pathfinder' import * as world from '../composables/world' import { log } from './base' import { goToPlayer, goToPosition } from './movement' -const { goals } = pathfinderModel - -export async function pickupNearbyItems(mineflayer: Mineflayer): Promise { - const distance = 8 - const getNearestItem = (bot: Bot) => - bot.nearestEntity(entity => - entity.name === 'item' - && bot.entity.position.distanceTo(entity.position) < distance, - ) - - let nearestItem = getNearestItem(mineflayer.bot) - let pickedUp = 0 - - while (nearestItem) { - await mineflayer.bot.pathfinder.goto(new goals.GoalFollow(nearestItem, 0.8)) - await new Promise(resolve => setTimeout(resolve, 200)) - - const prev = nearestItem - nearestItem = getNearestItem(mineflayer.bot) - if (prev === nearestItem) { - break - } - pickedUp++ - } - - log(mineflayer, `Picked up ${pickedUp} items.`) - return true -} - export async function equip(mineflayer: Mineflayer, itemName: string): Promise { const item = mineflayer.bot.inventory.slots.find(slot => slot && slot.name === itemName) if (!item) { diff --git a/services/minecraft/src/skills/movement.ts b/services/minecraft/src/skills/movement.ts index 41f003797..2a1ab9586 100644 --- a/services/minecraft/src/skills/movement.ts +++ b/services/minecraft/src/skills/movement.ts @@ -1,12 +1,16 @@ import type { Entity } from 'prismarine-entity' import type { Mineflayer } from '../libs/mineflayer' -import pathfinderModel from 'mineflayer-pathfinder' +import { useLogg } from '@guiiai/logg' +import { randomInt } from 'es-toolkit' +import pathfinder from 'mineflayer-pathfinder' +import { Vec3 } from 'vec3' import * as world from '../composables/world' import { sleep } from '../utils/helper' import { log } from './base' -const { goals, Movements } = pathfinderModel +const logger = useLogg('Skill:Movement').useGlobalConfig() +const { goals, Movements } = pathfinder export async function goToPosition( mineflayer: Mineflayer, @@ -152,28 +156,44 @@ export async function followPlayer( } export async function moveAway(mineflayer: Mineflayer, distance: number): Promise { - const pos = mineflayer.bot.entity.position - const goal = new goals.GoalNear(pos.x, pos.y, pos.z, distance) - const invertedGoal = new goals.GoalInvert(goal) + try { + const pos = mineflayer.bot.entity.position + let newX: number = 0 + let newZ: number = 0 + let suitableGoal = false - if (mineflayer.allowCheats) { - const move = new Movements(mineflayer.bot) - const path = await mineflayer.bot.pathfinder.getPathTo(move, invertedGoal, 10000) - const lastMove = path.path[path.path.length - 1] + while (!suitableGoal) { + const rand1 = randomInt(0, 2) + const rand2 = randomInt(0, 2) + const bigRand1 = randomInt(0, 101) + const bigRand2 = randomInt(0, 101) - if (lastMove) { - const x = Math.floor(lastMove.x) - const y = Math.floor(lastMove.y) - const z = Math.floor(lastMove.z) - mineflayer.bot.chat(`/tp @s ${x} ${y} ${z}`) - return true + newX = Math.floor( + pos.x + ((distance * bigRand1) / 100) * (rand1 ? 1 : -1), + ) + newZ = Math.floor( + pos.z + ((distance * bigRand2) / 100) * (rand2 ? 1 : -1), + ) + + const block = mineflayer.bot.blockAt(new Vec3(newX, pos.y - 1, newZ)) + + if (block?.name !== 'water' && block?.name !== 'lava') { + suitableGoal = true + } } - } - await mineflayer.bot.pathfinder.goto(invertedGoal) - const newPos = mineflayer.bot.entity.position - log(mineflayer, `Moved away from nearest entity to ${newPos}.`) - return true + const farGoal = new pathfinder.goals.GoalXZ(newX, newZ) + + await mineflayer.bot.pathfinder.goto(farGoal) + const newPos = mineflayer.bot.entity.position + logger.log(`Moved away from nearest entity to ${newPos}.`) + await sleep(500) + return true + } + catch (err) { + logger.log(`Failed to move away: ${(err as Error).message}`) + return false + } } export async function moveAwayFromEntity( diff --git a/services/minecraft/src/utils/mcdata.ts b/services/minecraft/src/utils/mcdata.ts index adbde4ead..8dc96307c 100644 --- a/services/minecraft/src/utils/mcdata.ts +++ b/services/minecraft/src/utils/mcdata.ts @@ -1,23 +1,28 @@ -/** - * @source https://github.com/kolbytn/mindcraft - */ +// src/utils/minecraftData.ts + import type { Bot } from 'mineflayer' -import minecraftData from 'minecraft-data' -import prismarine_items from 'prismarine-item' -import { botConfig } from '../composables/config' +import type { Entity } from 'prismarine-entity' +import minecraftData, { + type Biome, + type ShapedRecipe, + type ShapelessRecipe, +} from 'minecraft-data' +import prismarineItem from 'prismarine-item' -const mc_version = botConfig.version! -const mcdata = minecraftData(mc_version) -const Item = prismarine_items(mc_version) +const GAME_VERSION = '1.20' -interface MinecraftRecipe { - result: { id: number, count: number } - inShape?: Array> - ingredients?: Array<{ id: number, count: number }> - requiresTable?: boolean -} +export const gameData = minecraftData(GAME_VERSION) +export const Item = prismarineItem(GAME_VERSION) + +export const WOOD_TYPES: string[] = [ + 'oak', + 'spruce', + 'birch', + 'jungle', + 'acacia', + 'dark_oak', +] -export const WOOD_TYPES: string[] = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak'] export const MATCHING_WOOD_BLOCKS: string[] = [ 'log', 'planks', @@ -32,6 +37,7 @@ export const MATCHING_WOOD_BLOCKS: string[] = [ 'pressure_plate', 'trapdoor', ] + export const WOOL_COLORS: string[] = [ 'white', 'orange', @@ -51,55 +57,56 @@ export const WOOL_COLORS: string[] = [ 'black', ] -export function isHuntable(mob: { name?: string, metadata: any[] }): boolean { +export function isHuntable(mob: Entity): boolean { 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 + const animals: string[] = [ + 'chicken', + 'cow', + 'llama', + 'mooshroom', + 'pig', + 'rabbit', + 'sheep', + ] + return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16] // metadata[16] indicates baby status } -export function isHostile(mob: { name?: string, type?: string }): boolean { +export function isHostile(mob: Entity): boolean { if (!mob || !mob.name) return false - return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem' + return ( + (mob.type === 'mob' || mob.type === 'hostile') + && mob.name !== 'iron_golem' + && mob.name !== 'snow_golem' + ) } -export function getItemId(itemName: string): number | null { - const item = mcdata.itemsByName[itemName] - if (item) { - return item.id - } - return null +export function getItemId(itemName: string): number { + const item = gameData.itemsByName[itemName] + + return item?.id || 0 } -export function getItemName(itemId: number): string | null { - const item = mcdata.items[itemId] - if (item) { - return item.name - } - return null +export function getItemName(itemId: number): string { + const item = gameData.items[itemId] + return item.name || '' } -export function getBlockId(blockName: string): number | null { - const block = mcdata.blocksByName[blockName] - if (block) { - return block.id - } - return null +export function getBlockId(blockName: string): number { + const block = gameData.blocksByName?.[blockName] + return block?.id || 0 } -export function getBlockName(blockId: number): string | null { - const block = mcdata.blocks[blockId] - if (block) { - return block.name - } - return null +export function getBlockName(blockId: number): string { + const block = gameData.blocks[blockId] + return block.name || '' } export function getAllItems(ignore: string[] = []): any[] { - const items = [] - for (const itemId in mcdata.items) { - const item = mcdata.items[itemId] + const items: any[] = [] + for (const itemId in gameData.items) { + const item = gameData.items[itemId] if (!ignore.includes(item.name)) { items.push(item) } @@ -109,7 +116,7 @@ export function getAllItems(ignore: string[] = []): any[] { export function getAllItemIds(ignore: string[] = []): number[] { const items = getAllItems(ignore) - const itemIds = [] + const itemIds: number[] = [] for (const item of items) { itemIds.push(item.id) } @@ -117,9 +124,9 @@ export function getAllItemIds(ignore: string[] = []): number[] { } export function getAllBlocks(ignore: string[] = []): any[] { - const blocks = [] - for (const blockId in mcdata.blocks) { - const block = mcdata.blocks[blockId] + const blocks: any[] = [] + for (const blockId in gameData.blocks) { + const block = gameData.blocks[blockId] if (!ignore.includes(block.name)) { blocks.push(block) } @@ -129,76 +136,67 @@ export function getAllBlocks(ignore: string[] = []): any[] { export function getAllBlockIds(ignore: string[] = []): number[] { const blocks = getAllBlocks(ignore) - const blockIds = [] + const blockIds: number[] = [] for (const block of blocks) { blockIds.push(block.id) } return blockIds } -export function getAllBiomes(): any { - return mcdata.biomes +export function getAllBiomes(): Record { + return gameData.biomes } -export function getItemCraftingRecipes(itemName: string): Record[] | null { +export function getItemCraftingRecipes(itemName: string): any[] | null { const itemId = getItemId(itemName) - if (!itemId || !mcdata.recipes[itemId]) { + if (!itemId || !gameData.recipes[itemId]) { return null } const recipes: Record[] = [] - for (const r of mcdata.recipes[itemId] as MinecraftRecipe[]) { + for (const r of gameData.recipes[itemId]) { const recipe: Record = {} - let ingredients: Array<{ id: number, count: number }> = [] + let ingredients: number[] = [] - if (r.ingredients) { - ingredients = r.ingredients + if (isShapelessRecipe(r)) { + // Handle shapeless recipe + ingredients = r.ingredients.map((ing: any) => ing.id) } - else if (r.inShape) { - ingredients = r.inShape.flat() + else if (isShapedRecipe(r)) { + // Handle shaped recipe + ingredients = r.inShape + .flat() + .map((ing: any) => ing?.id) + .filter(Boolean) } - for (const ingredient of ingredients) { - const ingredientName = getItemName(ingredient.id) + for (const ingredientId of ingredients) { + const ingredientName = getItemName(ingredientId) if (ingredientName === null) continue - recipe[ingredientName] ??= 0 - recipe[ingredientName] += ingredient.count + if (!recipe[ingredientName]) + recipe[ingredientName] = 0 + recipe[ingredientName]++ } + recipes.push(recipe) } return recipes } -export function isSmeltable(itemName: string): boolean { - 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) +// Type guards +function isShapelessRecipe(recipe: any): recipe is ShapelessRecipe { + return 'ingredients' in recipe } -export function getSmeltingFuel(bot: Bot): any { - let fuel = bot.inventory.items().find(i => i.name === 'coal' || i.name === 'charcoal') - 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') +function isShapedRecipe(recipe: any): recipe is ShapedRecipe { + return 'inShape' in recipe } -export function getFuelSmeltOutput(fuelName: string): number { - if (fuelName === 'coal' || fuelName === 'charcoal') - return 8 - 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: string): string | undefined { +export function getItemSmeltingIngredient( + itemName: string, +): string | undefined { return { baked_potato: 'potato', steak: 'raw_beef', @@ -219,8 +217,10 @@ export function getItemSmeltingIngredient(itemName: string): string | undefined export function getItemBlockSources(itemName: string): string[] { const itemId = getItemId(itemName) const sources: string[] = [] + if (!itemId) + return sources for (const block of getAllBlocks()) { - if (block.drops.includes(itemId)) { + if (block.drops && block.drops.includes(itemId)) { sources.push(block.name) } } @@ -242,65 +242,37 @@ export function getItemAnimalSource(itemName: string): string | undefined { } export function getBlockTool(blockName: string): string | null { - const block = mcdata.blocksByName[blockName] + const block = gameData.blocksByName[blockName] if (!block || !block.harvestTools) { return null } - const toolId = Number(Object.keys(block.harvestTools)[0]) - return getItemName(toolId) + const toolIds = Object.keys(block.harvestTools).map(id => Number.parseInt(id)) + const toolName = getItemName(toolIds[0]) + return toolName || null // Assuming the first tool is the simplest } -export function makeItem(name: string, amount: number = 1): any { +export function makeItem(name: string, amount = 1): InstanceType { const itemId = getItemId(name) if (itemId === null) - throw new Error(`Unknown item: ${name}`) + throw new Error(`Item ${name} not found.`) return new Item(itemId, amount) } -export function ingredientsFromPrismarineRecipe(recipe: MinecraftRecipe): Record { - const requiredIngredients: Record = {} - 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) - if (ingredientName) { - requiredIngredients[ingredientName] ??= 0 - requiredIngredients[ingredientName] += ingredient.count - } - } - } - if (recipe.ingredients) { - for (const ingredient of recipe.ingredients) { - if (ingredient.id < 0) - continue - const ingredientName = getItemName(ingredient.id) - if (ingredientName) { - requiredIngredients[ingredientName] ??= 0 - requiredIngredients[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 requiredIngredients -} +// Function to get the nearest block of a specific type using Mineflayer +export function getNearestBlock( + bot: Bot, + blockType: string, + maxDistance: number, +) { + const blocks = bot.findBlocks({ + matching: block => block.name === blockType, + maxDistance, + count: 1, + }) -export function calculateLimitingResource( - availableItems: Record, - requiredItems: Record, - discrete: boolean = true, -): { num: number, limitingResource: T | null } { - let limitingResource: T | null = 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 } + if (blocks.length === 0) + return null + + const nearestBlockPosition = blocks[0] + return bot.blockAt(nearestBlockPosition) }