diff --git a/services/minecraft/src/skills/base.ts b/services/minecraft/src/skills/base.ts index ed7ae041c..e20c02400 100644 --- a/services/minecraft/src/skills/base.ts +++ b/services/minecraft/src/skills/base.ts @@ -1,14 +1,42 @@ import type { Bot } from 'mineflayer' /** - * Log a message to the bot's output + * Context for skill execution */ -export function log(bot: Bot, message: string): void { - bot.chat(`${message}`) +export interface SkillContext { + bot: Bot + // Whether the bot is in creative mode + isCreative: boolean + // Whether the bot should use cheats (like /tp, /setblock) + allowCheats: boolean + // Whether the bot should interrupt current action + shouldInterrupt: boolean + // Output buffer for logging + output: string[] } /** - * Type definition for a position in the world + * Create a new skill context + */ +export function createContext(bot: Bot): SkillContext { + return { + bot, + isCreative: bot.game.gameMode === 'creative', + allowCheats: false, + shouldInterrupt: false, + output: [], + } +} + +/** + * Log a message to the context's output buffer + */ +export function log(ctx: SkillContext, message: string): void { + ctx.output.push(message) +} + +/** + * Position in the world */ export interface Position { x: number @@ -17,6 +45,6 @@ export interface Position { } /** - * Type definition for a block face direction + * Block face direction */ export type BlockFace = 'top' | 'bottom' | 'north' | 'south' | 'east' | 'west' | 'side' diff --git a/services/minecraft/src/skills/blocks.ts b/services/minecraft/src/skills/blocks.ts index cca0cb14f..657101cc8 100644 --- a/services/minecraft/src/skills/blocks.ts +++ b/services/minecraft/src/skills/blocks.ts @@ -1,100 +1,18 @@ -import type { Bot } from 'mineflayer' -import type { BlockFace } from './base' -import Vec3 from 'vec3' +import type { BlockFace, SkillContext } from './base' +import { Vec3 } from 'vec3' import * as world from '../composables/world' import * as mc from '../utils/mcdata' import { log } from './base' import { goToPosition } from './movement' -export async function collectBlock( - bot: Bot, - blockType: string, - num: number = 1, - exclude: typeof Vec3[] | null = null, -): Promise { - if (num < 1) { - log(bot, `Invalid number of blocks to collect: ${num}.`) - return false - } - - const blocktypes: string[] = [blockType] - if (blockType === 'coal' || blockType === 'diamond' || blockType === 'emerald' - || blockType === 'iron' || blockType === 'gold' || blockType === 'lapis_lazuli' - || blockType === 'redstone') { - blocktypes.push(`${blockType}_ore`) - } - if (blockType.endsWith('ore')) { - blocktypes.push(`deepslate_${blockType}`) - } - if (blockType === 'dirt') { - blocktypes.push('grass_block') - } - - let collected = 0 - - for (let i = 0; i < num; i++) { - let blocks = world.getNearestBlocks(bot, blocktypes, 64) - if (exclude) { - blocks = blocks.filter( - block => !exclude.some(pos => - pos.x === block.position.x - && pos.y === block.position.y - && pos.z === block.position.z, - ), - ) - } - - const movements = new bot.pathfinder.Movements(bot) - movements.dontMineUnderFallingBlock = false - blocks = blocks.filter(block => movements.safeToBreak(block)) - - if (blocks.length === 0) { - log(bot, collected === 0 - ? `No ${blockType} nearby to collect.` - : `No more ${blockType} nearby to collect.`) - break - } - - const block = blocks[0] - await bot.tool.equipForBlock(block) - const itemId = bot.heldItem ? bot.heldItem.type : null - - if (!block.canHarvest(itemId)) { - log(bot, `Don't have right tools to harvest ${blockType}.`) - return false - } - - try { - await bot.collectBlock.collect(block) - collected++ - await autoLight(bot) - } - catch (err) { - if (err.name === 'NoChests') { - log(bot, `Failed to collect ${blockType}: Inventory full, no place to deposit.`) - break - } - log(bot, `Failed to collect ${blockType}: ${err}.`) - continue - } - - if (bot.interrupt_code) { - break - } - } - - log(bot, `Collected ${collected} ${blockType}.`) - return collected > 0 -} - /** * Place a torch if needed */ -async function autoLight(bot: Bot): Promise { - if (world.shouldPlaceTorch(bot)) { +async function autoLight(ctx: SkillContext): Promise { + if (world.shouldPlaceTorch(ctx.bot)) { try { - const pos = world.getPosition(bot) - return await placeBlock(bot, 'torch', pos.x, pos.y, pos.z, 'bottom', true) + const pos = world.getPosition(ctx.bot) + return await placeBlock(ctx, 'torch', pos.x, pos.y, pos.z, 'bottom', true) } catch { return false @@ -107,26 +25,49 @@ async function autoLight(bot: Bot): Promise { * Break a block at the specified position */ export async function breakBlockAt( - bot: Bot, + ctx: SkillContext, x: number, y: number, z: number, ): Promise { + const { bot } = ctx + validatePosition(x, y, z) + + const block = bot.blockAt(new Vec3(x, y, z)) + if (isUnbreakableBlock(block)) + return false + + if (ctx.allowCheats) { + return breakWithCheats(ctx, x, y, z) + } + + await moveIntoRange(bot, block) + + if (ctx.isCreative) { + return breakInCreative(ctx, block, x, y, z) + } + + return breakInSurvival(ctx, block, x, y, z) +} + +function validatePosition(x: number, y: number, z: number) { if (x == null || y == null || z == null) { throw new Error('Invalid position to break block at.') } +} - const block = bot.blockAt(new Vec3(x, y, z)) - if (block.name === 'air' || block.name === 'water' || block.name === 'lava') { - return false - } +function isUnbreakableBlock(block: any): boolean { + return block.name === 'air' || block.name === 'water' || block.name === 'lava' +} - if (bot.modes.isOn('cheat')) { - bot.chat(`/setblock ${Math.floor(x)} ${Math.floor(y)} ${Math.floor(z)} air`) - log(bot, `Used /setblock to break block at ${x}, ${y}, ${z}.`) - return true - } +async function breakWithCheats(ctx: SkillContext, x: number, y: number, z: number): Promise { + const { bot } = ctx + bot.chat(`/setblock ${Math.floor(x)} ${Math.floor(y)} ${Math.floor(z)} air`) + log(ctx, `Used /setblock to break block at ${x}, ${y}, ${z}.`) + return true +} +async function moveIntoRange(bot: any, block: any) { if (bot.entity.position.distanceTo(block.position) > 4.5) { const pos = block.position const movements = new bot.pathfinder.Movements(bot) @@ -135,18 +76,26 @@ export async function breakBlockAt( bot.pathfinder.setMovements(movements) await bot.pathfinder.goto(bot.pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, 4)) } +} - if (bot.game.gameMode !== 'creative') { - await bot.tool.equipForBlock(block) - const itemId = bot.heldItem?.type - if (!block.canHarvest(itemId)) { - log(bot, `Don't have right tools to break ${block.name}.`) - return false - } +async function breakInCreative(ctx: SkillContext, block: any, x: number, y: number, z: number): Promise { + const { bot } = ctx + await bot.dig(block, true) + log(ctx, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`) + return true +} + +async function breakInSurvival(ctx: SkillContext, block: any, x: number, y: number, z: number): Promise { + const { bot } = ctx + await bot.tool.equipForBlock(block) + const itemId = bot.heldItem?.type + if (!block.canHarvest(itemId)) { + log(ctx, `Don't have right tools to break ${block.name}.`) + return false } await bot.dig(block, true) - log(bot, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`) + log(ctx, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`) return true } @@ -154,7 +103,7 @@ export async function breakBlockAt( * Place a block at the specified position */ export async function placeBlock( - bot: Bot, + ctx: SkillContext, blockType: string, x: number, y: number, @@ -162,100 +111,157 @@ export async function placeBlock( placeOn: BlockFace = 'bottom', dontCheat = false, ): Promise { + const { bot } = ctx if (!mc.getBlockId(blockType)) { - log(bot, `Invalid block type: ${blockType}.`) + log(ctx, `Invalid block type: ${blockType}.`) return false } const targetDest = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)) - if (bot.modes.isOn('cheat') && !dontCheat) { - // Invert the facing direction - const face = placeOn === 'north' - ? 'south' - : placeOn === 'south' - ? 'north' - : placeOn === 'east' - ? 'west' - : placeOn === 'west' - ? 'east' - : placeOn - - let blockState = blockType - if (blockType.includes('torch') && placeOn !== 'bottom') { - blockState = blockType.replace('torch', 'wall_torch') - if (placeOn !== 'side' && placeOn !== 'top') { - blockState += `[facing=${face}]` - } - } - - if (blockType.includes('button') || blockType === 'lever') { - if (placeOn === 'top') { - blockState += '[face=ceiling]' - } - else if (placeOn === 'bottom') { - blockState += '[face=floor]' - } - else { - blockState += `[facing=${face}]` - } - } - - if (blockType === 'ladder' || blockType === 'repeater' || blockType === 'comparator') { - blockState += `[facing=${face}]` - } - - if (blockType.includes('stairs')) { - blockState += `[facing=${face}]` - } - - bot.chat(`/setblock ${Math.floor(x)} ${Math.floor(y)} ${Math.floor(z)} ${blockState}`) - - if (blockType.includes('door')) { - bot.chat(`/setblock ${Math.floor(x)} ${Math.floor(y + 1)} ${Math.floor(z)} ${blockState}[half=upper]`) - } - - if (blockType.includes('bed')) { - bot.chat(`/setblock ${Math.floor(x)} ${Math.floor(y)} ${Math.floor(z - 1)} ${blockState}[part=head]`) - } - - log(bot, `Used /setblock to place ${blockType} at ${targetDest}.`) - return true + if (ctx.allowCheats && !dontCheat) { + return placeWithCheats(ctx, blockType, targetDest, placeOn) } - let itemName = blockType - if (itemName === 'redstone_wire') { - itemName = 'redstone' + return placeWithoutCheats(ctx, blockType, targetDest, placeOn) +} + +function getBlockState(blockType: string, placeOn: BlockFace): string { + const face = getInvertedFace(placeOn) + let blockState = blockType + + if (blockType.includes('torch') && placeOn !== 'bottom') { + blockState = handleTorchState(blockType, placeOn, face) } + if (blockType.includes('button') || blockType === 'lever') { + blockState = handleButtonLeverState(blockState, placeOn, face) + } + + if (needsFacingState(blockType)) { + blockState += `[facing=${face}]` + } + + return blockState +} + +function getInvertedFace(placeOn: BlockFace): string { + const faceMap = { + north: 'south', + south: 'north', + east: 'west', + west: 'east', + } + return faceMap[placeOn] || placeOn +} + +function handleTorchState(blockType: string, placeOn: BlockFace, face: string): string { + let state = blockType.replace('torch', 'wall_torch') + if (placeOn !== 'side' && placeOn !== 'top') { + state += `[facing=${face}]` + } + return state +} + +function handleButtonLeverState(blockState: string, placeOn: BlockFace, face: string): string { + if (placeOn === 'top') { + return `${blockState}[face=ceiling]` + } + if (placeOn === 'bottom') { + return `${blockState}[face=floor]` + } + return `${blockState}[facing=${face}]` +} + +function needsFacingState(blockType: string): boolean { + return blockType === 'ladder' + || blockType === 'repeater' + || blockType === 'comparator' + || blockType.includes('stairs') +} + +async function placeWithCheats( + ctx: SkillContext, + blockType: string, + targetDest: Vec3, + placeOn: BlockFace, +): Promise { + const { bot } = ctx + const blockState = getBlockState(blockType, placeOn) + + bot.chat(`/setblock ${targetDest.x} ${targetDest.y} ${targetDest.z} ${blockState}`) + + if (blockType.includes('door')) { + bot.chat(`/setblock ${targetDest.x} ${targetDest.y + 1} ${targetDest.z} ${blockState}[half=upper]`) + } + + if (blockType.includes('bed')) { + bot.chat(`/setblock ${targetDest.x} ${targetDest.y} ${targetDest.z - 1} ${blockState}[part=head]`) + } + + log(ctx, `Used /setblock to place ${blockType} at ${targetDest}.`) + return true +} + +async function placeWithoutCheats( + ctx: SkillContext, + blockType: string, + targetDest: Vec3, + placeOn: BlockFace, +): Promise { + const { bot } = ctx + const itemName = blockType === 'redstone_wire' ? 'redstone' : blockType + let block = bot.inventory.items().find(item => item.name === itemName) - if (!block && bot.game.gameMode === 'creative') { + if (!block && ctx.isCreative) { await bot.creative.setInventorySlot(36, mc.makeItem(itemName, 1)) block = bot.inventory.items().find(item => item.name === itemName) } if (!block) { - log(bot, `Don't have any ${blockType} to place.`) + log(ctx, `Don't have any ${blockType} to place.`) return false } const targetBlock = bot.blockAt(targetDest) if (targetBlock.name === blockType) { - log(bot, `${blockType} already at ${targetBlock.position}.`) + log(ctx, `${blockType} already at ${targetBlock.position}.`) return false } const emptyBlocks = ['air', 'water', 'lava', 'grass', 'short_grass', 'tall_grass', 'snow', 'dead_bush', 'fern'] if (!emptyBlocks.includes(targetBlock.name)) { - log(bot, `${blockType} in the way at ${targetBlock.position}.`) - const removed = await breakBlockAt(bot, x, y, z) - if (!removed) { - log(bot, `Cannot place ${blockType} at ${targetBlock.position}: block in the way.`) + if (!await clearBlockSpace(ctx, targetBlock, blockType)) { return false } - await new Promise(resolve => setTimeout(resolve, 200)) } + const { buildOffBlock, faceVec } = findPlacementSpot(bot, targetDest, placeOn, emptyBlocks) + if (!buildOffBlock) { + log(ctx, `Cannot place ${blockType} at ${targetBlock.position}: nothing to place on.`) + return false + } + + await moveIntoPosition(ctx, blockType, targetBlock) + return await tryPlaceBlock(ctx, block, buildOffBlock, faceVec, blockType, targetDest) +} + +async function clearBlockSpace( + ctx: SkillContext, + targetBlock: any, + blockType: string, +): Promise { + const removed = await breakBlockAt(ctx, targetBlock.position.x, targetBlock.position.y, targetBlock.position.z, + ) + if (!removed) { + log(ctx, `Cannot place ${blockType} at ${targetBlock.position}: block in the way.`) + return false + } + await new Promise(resolve => setTimeout(resolve, 200)) + return true +} + +function findPlacementSpot(bot: any, targetDest: Vec3, placeOn: BlockFace, emptyBlocks: string[]) { const dirMap = { top: new Vec3(0, 1, 0), bottom: new Vec3(0, -1, 0), @@ -265,6 +271,22 @@ export async function placeBlock( west: new Vec3(-1, 0, 0), } + const dirs = getPlacementDirections(placeOn, dirMap) + + for (const d of dirs) { + const block = bot.blockAt(targetDest.plus(d)) + if (!emptyBlocks.includes(block.name)) { + return { + buildOffBlock: block, + faceVec: new Vec3(-d.x, -d.y, -d.z), + } + } + } + + return { buildOffBlock: null, faceVec: null } +} + +function getPlacementDirections(placeOn: BlockFace, dirMap: Record): Vec3[] { const dirs = [] if (placeOn === 'side') { dirs.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west) @@ -274,29 +296,13 @@ export async function placeBlock( } else { dirs.push(dirMap.bottom) - log(bot, `Unknown placeOn value "${placeOn}". Defaulting to bottom.`) } dirs.push(...Object.values(dirMap).filter(d => !dirs.includes(d))) + return dirs +} - let buildOffBlock = null - let faceVec = null - - for (const d of dirs) { - const block = bot.blockAt(targetDest.plus(d)) - if (!emptyBlocks.includes(block.name)) { - buildOffBlock = block - faceVec = new Vec3(-d.x, -d.y, -d.z) - break - } - } - - if (!buildOffBlock) { - log(bot, `Cannot place ${blockType} at ${targetBlock.position}: nothing to place on.`) - return false - } - - const pos = bot.entity.position - const posAbove = pos.plus(new Vec3(0, 1, 0)) +async function moveIntoPosition(ctx: SkillContext, blockType: string, targetBlock: any) { + const { bot } = ctx const dontMoveFor = [ 'torch', 'redstone_torch', @@ -312,33 +318,61 @@ export async function placeBlock( 'water_bucket', ] + const pos = bot.entity.position + const posAbove = pos.plus(new Vec3(0, 1, 0)) + if (!dontMoveFor.includes(blockType) && (pos.distanceTo(targetBlock.position) < 1 || posAbove.distanceTo(targetBlock.position) < 1)) { - const goal = bot.pathfinder.goals.GoalNear(targetBlock.position.x, targetBlock.position.y, targetBlock.position.z, 2) - const invertedGoal = bot.pathfinder.goals.GoalInvert(goal) - bot.pathfinder.setMovements(new bot.pathfinder.Movements(bot)) - await bot.pathfinder.goto(invertedGoal) + await moveAwayFromBlock(bot, targetBlock) } if (bot.entity.position.distanceTo(targetBlock.position) > 4.5) { - const pos = targetBlock.position - const movements = new bot.pathfinder.Movements(bot) - bot.pathfinder.setMovements(movements) - await bot.pathfinder.goto(bot.pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, 4)) + await moveToBlock(bot, targetBlock) } +} +async function moveAwayFromBlock(bot: any, targetBlock: any) { + const goal = bot.pathfinder.goals.GoalNear( + targetBlock.position.x, + targetBlock.position.y, + targetBlock.position.z, + 2, + ) + const invertedGoal = bot.pathfinder.goals.GoalInvert(goal) + bot.pathfinder.setMovements(new bot.pathfinder.Movements(bot)) + await bot.pathfinder.goto(invertedGoal) +} + +async function moveToBlock(bot: any, targetBlock: any) { + const pos = targetBlock.position + const movements = new bot.pathfinder.Movements(bot) + bot.pathfinder.setMovements(movements) + await bot.pathfinder.goto( + bot.pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, 4), + ) +} + +async function tryPlaceBlock( + ctx: SkillContext, + block: any, + buildOffBlock: any, + faceVec: Vec3, + blockType: string, + targetDest: Vec3, +): Promise { + const { bot } = ctx await bot.equip(block, 'hand') await bot.lookAt(buildOffBlock.position) try { await bot.placeBlock(buildOffBlock, faceVec) - log(bot, `Placed ${blockType} at ${targetDest}.`) + log(ctx, `Placed ${blockType} at ${targetDest}.`) await new Promise(resolve => setTimeout(resolve, 200)) return true } catch { - log(bot, `Failed to place ${blockType} at ${targetDest}.`) + log(ctx, `Failed to place ${blockType} at ${targetDest}.`) return false } } @@ -346,41 +380,49 @@ export async function placeBlock( /** * Use a door at the specified position */ -export async function useDoor(bot: Bot, doorPos: Vec3 | null = null): Promise { - if (!doorPos) { - const doorTypes = [ - 'oak_door', - 'spruce_door', - 'birch_door', - 'jungle_door', - 'acacia_door', - 'dark_oak_door', - 'mangrove_door', - 'cherry_door', - 'bamboo_door', - 'crimson_door', - 'warped_door', - ] - - for (const doorType of doorTypes) { - const block = world.getNearestBlock(bot, doorType, 16) - if (block) { - doorPos = block.position - break - } - } - } +export async function useDoor(ctx: SkillContext, doorPos: Vec3 | null = null): Promise { + const { bot } = ctx + doorPos = doorPos || await findNearestDoor(bot) if (!doorPos) { - log(bot, 'Could not find a door to use.') + log(ctx, 'Could not find a door to use.') return false } - await goToPosition(bot, doorPos.x, doorPos.y, doorPos.z, 1) + await goToPosition(ctx, doorPos.x, doorPos.y, doorPos.z, 1) while (bot.pathfinder.isMoving()) { await new Promise(resolve => setTimeout(resolve, 100)) } + return await operateDoor(ctx, doorPos) +} + +async function findNearestDoor(bot: any): Promise { + const doorTypes = [ + 'oak_door', + 'spruce_door', + 'birch_door', + 'jungle_door', + 'acacia_door', + 'dark_oak_door', + 'mangrove_door', + 'cherry_door', + 'bamboo_door', + 'crimson_door', + 'warped_door', + ] + + for (const doorType of doorTypes) { + const block = world.getNearestBlock(bot, doorType, 16) + if (block) { + return block.position + } + } + return null +} + +async function operateDoor(ctx: SkillContext, doorPos: Vec3): Promise { + const { bot } = ctx const doorBlock = bot.blockAt(doorPos) await bot.lookAt(doorPos) @@ -393,7 +435,7 @@ export async function useDoor(bot: Bot, doorPos: Vec3 | null = null): Promise { - x = Math.round(x) - y = Math.round(y) - z = Math.round(z) + const { bot } = ctx + const pos = { x: Math.round(x), y: Math.round(y), z: Math.round(z) } - const block = bot.blockAt(new Vec3(x, y, z)) - if (block.name !== 'grass_block' && block.name !== 'dirt' && block.name !== 'farmland') { - log(bot, `Cannot till ${block.name}, must be grass_block or dirt.`) + const block = bot.blockAt(new Vec3(pos.x, pos.y, pos.z)) + if (!canTillBlock(block)) { + log(ctx, `Cannot till ${block.name}, must be grass_block or dirt.`) return false } - const above = bot.blockAt(new Vec3(x, y + 1, z)) - if (above.name !== 'air') { - log(bot, `Cannot till, there is ${above.name} above the block.`) + const above = bot.blockAt(new Vec3(pos.x, pos.y + 1, pos.z)) + if (!isBlockClear(above)) { + log(ctx, `Cannot till, there is ${above.name} above the block.`) return false } - if (bot.entity.position.distanceTo(block.position) > 4.5) { - await goToPosition(bot, block.position.x, block.position.y, block.position.z, 4) - } + await moveIntoRange(bot, block) - if (block.name !== 'farmland') { - const hoe = bot.inventory.items().find(item => item.name.includes('hoe')) - if (!hoe) { - log(bot, 'Cannot till, no hoes.') - return false - } - await bot.equip(hoe, 'hand') - await bot.activateBlock(block) - log(bot, `Tilled block x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`) + if (!await tillBlock(ctx, block, pos)) { + return false } if (seedType) { - if (seedType.endsWith('seed') && !seedType.endsWith('seeds')) { - seedType += 's' // Fix common mistake - } - - const seeds = bot.inventory.items().find(item => item.name === seedType) - if (!seeds) { - log(bot, `No ${seedType} to plant.`) - return false - } - - await bot.equip(seeds, 'hand') - await bot.placeBlock(block, new Vec3(0, -1, 0)) - log(bot, `Planted ${seedType} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`) + return await sowSeeds(ctx, block, seedType, pos) } return true } +function canTillBlock(block: any): boolean { + return block.name === 'grass_block' || block.name === 'dirt' || block.name === 'farmland' +} + +function isBlockClear(block: any): boolean { + return block.name === 'air' +} + +async function tillBlock(ctx: SkillContext, block: any, pos: any): Promise { + const { bot } = ctx + if (block.name === 'farmland') { + return true + } + + const hoe = bot.inventory.items().find(item => item.name.includes('hoe')) + if (!hoe) { + log(ctx, 'Cannot till, no hoes.') + return false + } + + await bot.equip(hoe, 'hand') + await bot.activateBlock(block) + log(ctx, `Tilled block x:${pos.x.toFixed(1)}, y:${pos.y.toFixed(1)}, z:${pos.z.toFixed(1)}.`) + return true +} + +async function sowSeeds(ctx: SkillContext, block: any, seedType: string, pos: any): Promise { + const { bot } = ctx + seedType = fixSeedName(seedType) + + const seeds = bot.inventory.items().find(item => item.name === seedType) + if (!seeds) { + log(ctx, `No ${seedType} to plant.`) + return false + } + + await bot.equip(seeds, 'hand') + await bot.placeBlock(block, new Vec3(0, -1, 0)) + log(ctx, `Planted ${seedType} at x:${pos.x.toFixed(1)}, y:${pos.y.toFixed(1)}, z:${pos.z.toFixed(1)}.`) + return true +} + +function fixSeedName(seedType: string): string { + if (seedType.endsWith('seed') && !seedType.endsWith('seeds')) { + return `${seedType}s` // Fix common mistake + } + return seedType +} + /** * Activate the nearest block of a specific type */ -export async function activateNearestBlock(bot: Bot, type: string): Promise { +export async function activateNearestBlock(ctx: SkillContext, type: string): Promise { + const { bot } = ctx const block = world.getNearestBlock(bot, type, 16) if (!block) { - log(bot, `Could not find any ${type} to activate.`) + log(ctx, `Could not find any ${type} to activate.`) return false } - if (bot.entity.position.distanceTo(block.position) > 4.5) { - await goToPosition(bot, block.position.x, block.position.y, block.position.z, 4) - } - + await moveIntoRange(bot, block) await bot.activateBlock(block) - log(bot, `Activated ${type} at x:${block.position.x.toFixed(1)}, y:${block.position.y.toFixed(1)}, z:${block.position.z.toFixed(1)}.`) + log(ctx, `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 collectBlock( + ctx: SkillContext, + blockType: string, + num: number = 1, + exclude: typeof Vec3[] | null = null, +): Promise { + const { bot } = ctx + if (num < 1) { + log(ctx, `Invalid number of blocks to collect: ${num}.`) + return false + } + + const blocktypes = getBlockTypes(blockType) + let collected = 0 + + for (let i = 0; i < num; i++) { + const blocks = getValidBlocks(ctx, blocktypes, exclude) + + if (blocks.length === 0) { + logNoBlocksMessage(ctx, blockType, collected) + break + } + + const block = blocks[0] + if (!await canHarvestBlock(ctx, block, blockType)) { + return false + } + + if (!await tryCollectBlock(ctx, block, blockType)) { + break + } + + collected++ + + if (bot.interrupt_code) { + break + } + } + + log(ctx, `Collected ${collected} ${blockType}.`) + return collected > 0 +} + +function getBlockTypes(blockType: string): string[] { + const blocktypes: string[] = [blockType] + + const ores = ['coal', 'diamond', 'emerald', 'iron', 'gold', 'lapis_lazuli', 'redstone'] + if (ores.includes(blockType)) { + blocktypes.push(`${blockType}_ore`) + } + if (blockType.endsWith('ore')) { + blocktypes.push(`deepslate_${blockType}`) + } + if (blockType === 'dirt') { + blocktypes.push('grass_block') + } + + return blocktypes +} + +function getValidBlocks(ctx: SkillContext, blocktypes: string[], exclude: typeof Vec3[] | null): any[] { + const { bot } = ctx + let blocks = world.getNearestBlocks(bot, blocktypes, 64) + + if (exclude) { + blocks = blocks.filter( + block => !exclude.some(pos => + pos.x === block.position.x + && pos.y === block.position.y + && pos.z === block.position.z, + ), + ) + } + + const movements = new bot.pathfinder.Movements(bot) + movements.dontMineUnderFallingBlock = false + return blocks.filter(block => movements.safeToBreak(block)) +} + +function logNoBlocksMessage(ctx: SkillContext, blockType: string, collected: number): void { + log(ctx, collected === 0 + ? `No ${blockType} nearby to collect.` + : `No more ${blockType} nearby to collect.`) +} + +async function canHarvestBlock(ctx: SkillContext, block: any, blockType: string): Promise { + const { bot } = ctx + await bot.tool.equipForBlock(block) + const itemId = bot.heldItem ? bot.heldItem.type : null + + if (!block.canHarvest(itemId)) { + log(ctx, `Don't have right tools to harvest ${blockType}.`) + return false + } + return true +} + +async function tryCollectBlock(ctx: SkillContext, block: any, blockType: string): Promise { + const { bot } = ctx + try { + await bot.collectBlock.collect(block) + await autoLight(ctx) + return true + } + catch (err) { + if (err instanceof Error && err.name === 'NoChests') { + log(ctx, `Failed to collect ${blockType}: Inventory full, no place to deposit.`) + return false + } + log(ctx, `Failed to collect ${blockType}: ${err}.`) + return true + } +} diff --git a/services/minecraft/src/skills/combat.ts b/services/minecraft/src/skills/combat.ts index a6fdc989e..9946efd78 100644 --- a/services/minecraft/src/skills/combat.ts +++ b/services/minecraft/src/skills/combat.ts @@ -1,5 +1,5 @@ -import type { Bot } from 'mineflayer' import type { Entity } from 'prismarine-entity' +import type { SkillContext } from './base' import * as world from '../composables/world' import * as mc from '../utils/mcdata' import { log } from './base' @@ -7,7 +7,8 @@ import { log } from './base' /** * Equip the item with highest attack damage */ -async function equipHighestAttack(bot: Bot): Promise { +async function equipHighestAttack(ctx: SkillContext): Promise { + const { bot } = ctx const weapons = bot.inventory.items().filter(item => item.name.includes('sword') || (item.name.includes('axe') && !item.name.includes('pickaxe')), @@ -37,61 +38,65 @@ async function equipHighestAttack(bot: Bot): Promise { /** * Attack the nearest mob of the given type */ -export async function attackNearest(bot: Bot, mobType: string, kill = true): Promise { - bot.modes.pause('cowardice') - if (mobType === 'drowned' || mobType === 'cod' || mobType === 'salmon' - || mobType === 'tropical_fish' || mobType === 'squid') { - bot.modes.pause('self_preservation') +export async function attackNearest( + ctx: SkillContext, + mobType: string, + kill = true, +): Promise { + const { bot } = ctx + const mob = world.getNearbyEntities(bot, 24).find(entity => entity.name === mobType) + + if (mob) { + return await attackEntity(ctx, mob, kill) } - const mob = world.getNearbyEntities(bot, 24).find(entity => entity.name === mobType) - if (mob) { - return await attackEntity(bot, mob, kill) - } - log(bot, `Could not find any ${mobType} to attack.`) + log(ctx, `Could not find any ${mobType} to attack.`) return false } /** * Attack a specific entity */ -export async function attackEntity(bot: Bot, entity: Entity, kill = true): Promise { +export async function attackEntity( + ctx: SkillContext, + entity: Entity, + kill = true, +): Promise { + const { bot } = ctx const pos = entity.position - await equipHighestAttack(bot) + await equipHighestAttack(ctx) if (!kill) { if (bot.entity.position.distanceTo(pos) > 5) { await bot.pathfinder.goto(bot.pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, 4)) } await bot.attack(entity) - } - else { - bot.pvp.attack(entity) - while (world.getNearbyEntities(bot, 24).includes(entity)) { - await new Promise(resolve => setTimeout(resolve, 1000)) - if (bot.interrupt_code) { - bot.pvp.stop() - return false - } - } - log(bot, `Successfully killed ${entity.name}.`) return true } + + bot.pvp.attack(entity) + while (world.getNearbyEntities(bot, 24).includes(entity)) { + await new Promise(resolve => setTimeout(resolve, 1000)) + if (ctx.shouldInterrupt) { + bot.pvp.stop() + return false + } + } + + log(ctx, `Successfully killed ${entity.name}.`) return true } /** * Defend against nearby hostile mobs */ -export async function defendSelf(bot: Bot, range = 9): Promise { - bot.modes.pause('self_defense') - bot.modes.pause('cowardice') - +export async function defendSelf(ctx: SkillContext, range = 9): Promise { + const { bot } = ctx let attacked = false let enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), range) while (enemy) { - await equipHighestAttack(bot) + await equipHighestAttack(ctx) if (bot.entity.position.distanceTo(enemy.position) >= 4 && enemy.name !== 'creeper' && enemy.name !== 'phantom') { @@ -103,10 +108,10 @@ export async function defendSelf(bot: Bot, range = 9): Promise { if (bot.entity.position.distanceTo(enemy.position) <= 2) { try { - const inverted_goal = bot.pathfinder.goals.GoalInvert( + const invertedGoal = bot.pathfinder.goals.GoalInvert( bot.pathfinder.goals.GoalFollow(enemy, 2), ) - await bot.pathfinder.goto(inverted_goal, true) + await bot.pathfinder.goto(invertedGoal, true) } catch { /* might error if entity dies, ignore */ } } @@ -116,7 +121,7 @@ export async function defendSelf(bot: Bot, range = 9): Promise { await new Promise(resolve => setTimeout(resolve, 500)) enemy = world.getNearestEntityWhere(bot, entity => mc.isHostile(entity), range) - if (bot.interrupt_code) { + if (ctx.shouldInterrupt) { bot.pvp.stop() return false } @@ -124,10 +129,10 @@ export async function defendSelf(bot: Bot, range = 9): Promise { bot.pvp.stop() if (attacked) { - log(bot, `Successfully defended self.`) + log(ctx, 'Successfully defended self.') } else { - log(bot, `No enemies nearby to defend self from.`) + log(ctx, 'No enemies nearby to defend self from.') } return attacked } diff --git a/services/minecraft/src/skills/crafting.ts b/services/minecraft/src/skills/crafting.ts index b364117e9..2099555b4 100644 --- a/services/minecraft/src/skills/crafting.ts +++ b/services/minecraft/src/skills/crafting.ts @@ -1,90 +1,102 @@ -import type { Bot } from 'mineflayer' +import type { SkillContext } from './base' import * as world from '../composables/world' import * as mc from '../utils/mcdata' import { log } from './base' -import { collectBlock } from './blocks' +import { placeBlock } from './blocks' import { goToPosition } from './movement' /** * Craft items from a recipe */ -export async function craftRecipe(bot: Bot, itemName: string, num = 1): Promise { +export async function craftRecipe(ctx: SkillContext, itemName: string, num = 1): Promise { + const { bot } = ctx let placedTable = false if (mc.getItemCraftingRecipes(itemName).length === 0) { - log(bot, `${itemName} is either not an item, or it does not have a crafting recipe!`) + log(ctx, `${itemName} is either not an item, or it does not have a crafting recipe!`) return false } // Get recipes that don't require a crafting table - let recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, null) + const itemId = mc.getItemId(itemName) + if (itemId === null) { + log(ctx, `Invalid item name: ${itemName}`) + return false + } + + let recipes = bot.recipesFor(itemId, null, 1, null) let craftingTable = null const craftingTableRange = 32 if (!recipes || recipes.length === 0) { - recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, true) + recipes = bot.recipesFor(itemId, null, 1, true) if (!recipes || recipes.length === 0) { - log(bot, `You do not have the resources to craft a ${itemName}.`) + log(ctx, `You do not have the resources to craft a ${itemName}.`) return false } // Look for crafting table - craftingTable = world.getNearestBlock(bot, 'crafting_table', craftingTableRange) + const worldCtx = { bot, botCtx: { bot, botName: bot.username } } + craftingTable = world.getNearestBlock(worldCtx, 'crafting_table', craftingTableRange) if (!craftingTable) { // Try to place crafting table - const hasTable = world.getInventoryCounts(bot).crafting_table > 0 + const inventory = world.getInventoryCounts(worldCtx) + const hasTable = inventory.crafting_table > 0 if (hasTable) { - const pos = world.getNearestFreeSpace(bot, 1, 6) - await placeBlock(bot, 'crafting_table', pos.x, pos.y, pos.z) - craftingTable = world.getNearestBlock(bot, 'crafting_table', craftingTableRange) - if (craftingTable) { - recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, craftingTable) - placedTable = true + const pos = world.getNearestFreeSpace(worldCtx, 1, 6) + if (pos) { + await placeBlock(ctx, 'crafting_table', pos.x, pos.y, pos.z) + craftingTable = world.getNearestBlock(worldCtx, 'crafting_table', craftingTableRange) + if (craftingTable) { + recipes = bot.recipesFor(itemId, null, 1, craftingTable) + placedTable = true + } } } else { - log(bot, `Crafting ${itemName} requires a crafting table.`) + log(ctx, `Crafting ${itemName} requires a crafting table.`) return false } } else { - recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, craftingTable) + recipes = bot.recipesFor(itemId, null, 1, craftingTable) } } if (!recipes || recipes.length === 0) { - log(bot, `You do not have the resources to craft a ${itemName}. It requires: ${ + log(ctx, `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) { - await collectBlock(bot, 'crafting_table', 1) + if (placedTable && craftingTable) { + await bot.collectBlock.collect(craftingTable) } return false } if (craftingTable && bot.entity.position.distanceTo(craftingTable.position) > 4) { - await goToPosition(bot, craftingTable.position.x, craftingTable.position.y, craftingTable.position.z, 4) + await goToPosition(ctx, craftingTable.position.x, craftingTable.position.y, craftingTable.position.z, 4) } const recipe = recipes[0] // Check that the agent has sufficient items to use the recipe `num` times - const inventory = world.getInventoryCounts(bot) // Items in the agents inventory + const worldCtx = { bot, botCtx: { bot, botName: bot.username } } + const inventory = world.getInventoryCounts(worldCtx) // 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}.`) + log(ctx, `Not enough ${craftLimit.limitingResource} to craft ${num}, crafted ${craftLimit.num}. You now have ${world.getInventoryCounts(worldCtx)[itemName]} ${itemName}.`) } else { - log(bot, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(bot)[itemName]} ${itemName}.`) + log(ctx, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(worldCtx)[itemName]} ${itemName}.`) } - if (placedTable) { - await collectBlock(bot, 'crafting_table', 1) + if (placedTable && craftingTable) { + await bot.collectBlock.collect(craftingTable) } // Equip any armor the bot may have crafted @@ -96,57 +108,67 @@ export async function craftRecipe(bot: Bot, itemName: string, num = 1): Promise< /** * Smelt items in a furnace */ -export async function smeltItem(bot: Bot, itemName: string, num = 1): Promise { +export async function smeltItem(ctx: SkillContext, itemName: string, num = 1): Promise { + const { bot } = ctx if (!mc.isSmeltable(itemName)) { - log(bot, `Cannot smelt ${itemName}. Hint: make sure you are smelting the 'raw' item.`) + log(ctx, `Cannot smelt ${itemName}. Hint: make sure you are smelting the 'raw' item.`) return false } let placedFurnace = false const furnaceRange = 32 - let furnaceBlock = world.getNearestBlock(bot, 'furnace', furnaceRange) + const worldCtx = { bot, botCtx: { bot, botName: bot.username } } + let furnaceBlock = world.getNearestBlock(worldCtx, 'furnace', furnaceRange) if (!furnaceBlock) { // Try to place furnace - const hasFurnace = world.getInventoryCounts(bot).furnace > 0 + const inventory = world.getInventoryCounts(worldCtx) + const hasFurnace = inventory.furnace > 0 if (hasFurnace) { - const pos = world.getNearestFreeSpace(bot, 1, furnaceRange) - await placeBlock(bot, 'furnace', pos.x, pos.y, pos.z) - furnaceBlock = world.getNearestBlock(bot, 'furnace', furnaceRange) - placedFurnace = true + const pos = world.getNearestFreeSpace(worldCtx, 1, furnaceRange) + if (pos) { + await placeBlock(ctx, 'furnace', pos.x, pos.y, pos.z) + furnaceBlock = world.getNearestBlock(worldCtx, 'furnace', furnaceRange) + placedFurnace = true + } } } if (!furnaceBlock) { - log(bot, 'There is no furnace nearby and you have no furnace.') + log(ctx, 'There is no furnace nearby and you have no furnace.') return false } if (bot.entity.position.distanceTo(furnaceBlock.position) > 4) { - await goToPosition(bot, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4) + await goToPosition(ctx, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4) } - bot.modes.pause('unstuck') await bot.lookAt(furnaceBlock.position) const furnace = await bot.openFurnace(furnaceBlock) // Check if the furnace is already smelting something const inputItem = furnace.inputItem() - if (inputItem && inputItem.type !== mc.getItemId(itemName) && inputItem.count > 0) { - log(bot, `The furnace is currently smelting ${mc.getItemName(inputItem.type)}.`) + const itemId = mc.getItemId(itemName) + if (itemId === null) { + log(ctx, `Invalid item name: ${itemName}`) + return false + } + + if (inputItem && inputItem.type !== itemId && inputItem.count > 0) { + log(ctx, `The furnace is currently smelting ${mc.getItemName(inputItem.type) ?? 'unknown'}.`) if (placedFurnace) { - await collectBlock(bot, 'furnace', 1) + await bot.collectBlock.collect(furnaceBlock) } return false } // Check if the bot has enough items to smelt - const invCounts = world.getInventoryCounts(bot) + const invCounts = world.getInventoryCounts(worldCtx) if (!invCounts[itemName] || invCounts[itemName] < num) { - log(bot, `You do not have enough ${itemName} to smelt.`) + log(ctx, `You do not have enough ${itemName} to smelt.`) if (placedFurnace) { - await collectBlock(bot, 'furnace', 1) + await bot.collectBlock.collect(furnaceBlock) } return false } @@ -155,30 +177,30 @@ export async function smeltItem(bot: Bot, itemName: string, num = 1): Promise setTimeout(resolve, 10000)) let collected = false - if (furnace.outputItem()) { + const outputItem = furnace.outputItem() + if (outputItem) { smeltedItem = await furnace.takeOutput() if (smeltedItem) { total += smeltedItem.count @@ -203,7 +226,7 @@ export async function smeltItem(bot: Bot, itemName: string, num = 1): Promise { - const furnaceBlock = world.getNearestBlock(bot, 'furnace', 32) +export async function clearNearestFurnace(ctx: SkillContext): Promise { + const { bot } = ctx + const worldCtx = { bot, botCtx: { bot, botName: bot.username } } + const furnaceBlock = world.getNearestBlock(worldCtx, 'furnace', 32) if (!furnaceBlock) { - log(bot, 'No furnace nearby to clear.') + log(ctx, 'No furnace nearby to clear.') return false } if (bot.entity.position.distanceTo(furnaceBlock.position) > 4) { - await goToPosition(bot, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4) + await goToPosition(ctx, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4) } const furnace = await bot.openFurnace(furnaceBlock) @@ -247,15 +272,18 @@ export async function clearNearestFurnace(bot: Bot): Promise { // Take the items out of the furnace let smeltedItem, inputItem, fuelItem - if (furnace.outputItem()) { + const outputItem = furnace.outputItem() + if (outputItem) { smeltedItem = await furnace.takeOutput() } - if (furnace.inputItem()) { + const furnaceInput = furnace.inputItem() + if (furnaceInput) { inputItem = await furnace.takeInput() } - if (furnace.fuelItem()) { + const furnaceFuel = furnace.fuelItem() + if (furnaceFuel) { fuelItem = await furnace.takeFuel() } @@ -263,6 +291,6 @@ export async function clearNearestFurnace(bot: Bot): Promise { const inputName = inputItem ? `${inputItem.count} ${inputItem.name}` : '0 input items' const fuelName = fuelItem ? `${fuelItem.count} ${fuelItem.name}` : '0 fuel items' - log(bot, `Cleared furnace, received ${smeltedName}, ${inputName}, and ${fuelName}.`) + log(ctx, `Cleared furnace, received ${smeltedName}, ${inputName}, and ${fuelName}.`) return true } diff --git a/services/minecraft/src/skills/inventory.ts b/services/minecraft/src/skills/inventory.ts index aef535afe..d82ca2254 100644 --- a/services/minecraft/src/skills/inventory.ts +++ b/services/minecraft/src/skills/inventory.ts @@ -1,4 +1,5 @@ import type { Bot } from 'mineflayer' +import type { SkillContext } from './base' import * as world from '../composables/world' import { log } from './base' import { goToPosition } from './movement' @@ -6,7 +7,8 @@ import { goToPosition } from './movement' /** * Pick up nearby items */ -export async function pickupNearbyItems(bot: Bot): Promise { +export async function pickupNearbyItems(ctx: SkillContext): Promise { + const { bot } = ctx const distance = 8 const getNearestItem = (bot: Bot) => bot.nearestEntity(entity => @@ -29,17 +31,18 @@ export async function pickupNearbyItems(bot: Bot): Promise { pickedUp++ } - log(bot, `Picked up ${pickedUp} items.`) + log(ctx, `Picked up ${pickedUp} items.`) return true } /** * Equip an item */ -export async function equip(bot: Bot, itemName: string): Promise { +export async function equip(ctx: SkillContext, itemName: string): Promise { + const { bot } = ctx const item = bot.inventory.slots.find(slot => slot && slot.name === itemName) if (!item) { - log(bot, `You do not have any ${itemName} to equip.`) + log(ctx, `You do not have any ${itemName} to equip.`) return false } @@ -62,14 +65,15 @@ export async function equip(bot: Bot, itemName: string): Promise { await bot.equip(item, 'hand') } - log(bot, `Equipped ${itemName}.`) + log(ctx, `Equipped ${itemName}.`) return true } /** * Discard items */ -export async function discard(bot: Bot, itemName: string, num = -1): Promise { +export async function discard(ctx: SkillContext, itemName: string, num = -1): Promise { + const { bot } = ctx let discarded = 0 while (true) { @@ -88,57 +92,59 @@ export async function discard(bot: Bot, itemName: string, num = -1): Promise { +export async function putInChest(ctx: SkillContext, itemName: string, num = -1): Promise { + const { bot } = ctx const chest = world.getNearestBlock(bot, 'chest', 32) if (!chest) { - log(bot, 'Could not find a chest nearby.') + log(ctx, 'Could not find a chest nearby.') return false } const item = bot.inventory.items().find(item => item.name === itemName) if (!item) { - log(bot, `You do not have any ${itemName} to put in the chest.`) + log(ctx, `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(bot, chest.position.x, chest.position.y, chest.position.z, 2) + await goToPosition(ctx, chest.position.x, chest.position.y, chest.position.z, 2) const chestContainer = await bot.openContainer(chest) await chestContainer.deposit(item.type, null, toPut) await chestContainer.close() - log(bot, `Successfully put ${toPut} ${itemName} in the chest.`) + log(ctx, `Successfully put ${toPut} ${itemName} in the chest.`) return true } /** * Take items from a chest */ -export async function takeFromChest(bot: Bot, itemName: string, num = -1): Promise { +export async function takeFromChest(ctx: SkillContext, itemName: string, num = -1): Promise { + const { bot } = ctx const chest = world.getNearestBlock(bot, 'chest', 32) if (!chest) { - log(bot, 'Could not find a chest nearby.') + log(ctx, 'Could not find a chest nearby.') return false } - await goToPosition(bot, chest.position.x, chest.position.y, chest.position.z, 2) + await goToPosition(ctx, chest.position.x, chest.position.y, chest.position.z, 2) const chestContainer = await bot.openContainer(chest) const item = chestContainer.containerItems().find(item => item.name === itemName) if (!item) { - log(bot, `Could not find any ${itemName} in the chest.`) + log(ctx, `Could not find any ${itemName} in the chest.`) await chestContainer.close() return false } @@ -147,31 +153,32 @@ export async function takeFromChest(bot: Bot, itemName: string, num = -1): Promi await chestContainer.withdraw(item.type, null, toTake) await chestContainer.close() - log(bot, `Successfully took ${toTake} ${itemName} from the chest.`) + log(ctx, `Successfully took ${toTake} ${itemName} from the chest.`) return true } /** * View contents of a chest */ -export async function viewChest(bot: Bot): Promise { +export async function viewChest(ctx: SkillContext): Promise { + const { bot } = ctx const chest = world.getNearestBlock(bot, 'chest', 32) if (!chest) { - log(bot, 'Could not find a chest nearby.') + log(ctx, 'Could not find a chest nearby.') return false } - await goToPosition(bot, chest.position.x, chest.position.y, chest.position.z, 2) + await goToPosition(ctx, chest.position.x, chest.position.y, chest.position.z, 2) const chestContainer = await bot.openContainer(chest) const items = chestContainer.containerItems() if (items.length === 0) { - log(bot, 'The chest is empty.') + log(ctx, 'The chest is empty.') } else { - log(bot, 'The chest contains:') + log(ctx, 'The chest contains:') for (const item of items) { - log(bot, `${item.count} ${item.name}`) + log(ctx, `${item.count} ${item.name}`) } } @@ -182,7 +189,8 @@ export async function viewChest(bot: Bot): Promise { /** * Consume (eat/drink) an item */ -export async function consume(bot: Bot, itemName = ''): Promise { +export async function consume(ctx: SkillContext, itemName = ''): Promise { + const { bot } = ctx let item let name @@ -192,13 +200,13 @@ export async function consume(bot: Bot, itemName = ''): Promise { } if (!item) { - log(bot, `You do not have any ${name} to eat.`) + log(ctx, `You do not have any ${name} to eat.`) return false } await bot.equip(item, 'hand') await bot.consume() - log(bot, `Consumed ${item.name}.`) + log(ctx, `Consumed ${item.name}.`) return true } @@ -206,21 +214,22 @@ export async function consume(bot: Bot, itemName = ''): Promise { * Give items to a player */ export async function giveToPlayer( - bot: Bot, + ctx: SkillContext, itemType: string, username: string, num = 1, ): Promise { + const { bot } = ctx const player = bot.players[username]?.entity if (!player) { - log(bot, `Could not find ${username}.`) + log(ctx, `Could not find ${username}.`) return false } - await goToPosition(bot, player.position.x, player.position.y, player.position.z, 3) + await goToPosition(ctx, player.position.x, player.position.y, player.position.z, 3) if (bot.entity.position.y < player.position.y - 1) { - await goToPosition(bot, player.position.x, player.position.y, player.position.z, 1) + await goToPosition(ctx, player.position.x, player.position.y, player.position.z, 1) } if (bot.entity.position.distanceTo(player.position) < 2) { @@ -231,17 +240,17 @@ export async function giveToPlayer( await bot.lookAt(player.position) - if (await discard(bot, itemType, num)) { + if (await discard(ctx, itemType, num)) { let given = false bot.once('playerCollect', (collector, collected) => { if (collector.username === username) { - log(bot, `${username} received ${itemType}.`) + log(ctx, `${username} received ${itemType}.`) given = true } }) const start = Date.now() - while (!given && !bot.interrupt_code) { + while (!given && !ctx.shouldInterrupt) { await new Promise(resolve => setTimeout(resolve, 500)) if (given) { return true @@ -252,6 +261,6 @@ export async function giveToPlayer( } } - log(bot, `Failed to give ${itemType} to ${username}, it was never received.`) + log(ctx, `Failed to give ${itemType} to ${username}, it was never received.`) return false } diff --git a/services/minecraft/src/skills/movement.ts b/services/minecraft/src/skills/movement.ts index 0e809cc26..88a5846c5 100644 --- a/services/minecraft/src/skills/movement.ts +++ b/services/minecraft/src/skills/movement.ts @@ -1,5 +1,5 @@ -import type { Bot } from 'mineflayer' import type { Entity } from 'prismarine-entity' +import type { SkillContext } from './base' import * as world from '../composables/world' import { log } from './base' @@ -7,25 +7,26 @@ import { log } from './base' * Navigate to a specific position */ export async function goToPosition( - bot: Bot, + ctx: SkillContext, x: number, y: number, z: number, minDistance = 2, ): Promise { + const { bot } = ctx if (x == null || y == null || z == null) { - log(bot, `Missing coordinates, given x:${x} y:${y} z:${z}`) + log(ctx, `Missing coordinates, given x:${x} y:${y} z:${z}`) return false } - if (bot.modes.isOn('cheat')) { + if (ctx.allowCheats) { bot.chat(`/tp @s ${x} ${y} ${z}`) - log(bot, `Teleported to ${x}, ${y}, ${z}.`) + log(ctx, `Teleported to ${x}, ${y}, ${z}.`) return true } await bot.pathfinder.goto(bot.pathfinder.goals.GoalNear(x, y, z, minDistance)) - log(bot, `You have reached ${x}, ${y}, ${z}.`) + log(ctx, `You have reached ${x}, ${y}, ${z}.`) return true } @@ -33,25 +34,25 @@ export async function goToPosition( * Navigate to the nearest block of a specific type */ export async function goToNearestBlock( - bot: Bot, + ctx: SkillContext, blockType: string, minDistance = 2, range = 64, ): Promise { const MAX_RANGE = 512 if (range > MAX_RANGE) { - log(bot, `Maximum search range capped at ${MAX_RANGE}.`) + log(ctx, `Maximum search range capped at ${MAX_RANGE}.`) range = MAX_RANGE } - const block = world.getNearestBlock(bot, blockType, range) + const block = world.getNearestBlock(ctx.bot, blockType, range) if (!block) { - log(bot, `Could not find any ${blockType} in ${range} blocks.`) + log(ctx, `Could not find any ${blockType} in ${range} blocks.`) return false } - log(bot, `Found ${blockType} at ${block.position}.`) - await goToPosition(bot, block.position.x, block.position.y, block.position.z, minDistance) + log(ctx, `Found ${blockType} at ${block.position}.`) + await goToPosition(ctx, block.position.x, block.position.y, block.position.z, minDistance) return true } @@ -59,11 +60,12 @@ export async function goToNearestBlock( * Navigate to the nearest entity of a specific type */ export async function goToNearestEntity( - bot: Bot, + ctx: SkillContext, entityType: string, minDistance = 2, range = 64, ): Promise { + const { bot } = ctx const entity = world.getNearestEntityWhere( bot, entity => entity.name === entityType, @@ -71,14 +73,14 @@ export async function goToNearestEntity( ) if (!entity) { - log(bot, `Could not find any ${entityType} in ${range} blocks.`) + log(ctx, `Could not find any ${entityType} in ${range} blocks.`) return false } const distance = bot.entity.position.distanceTo(entity.position) - log(bot, `Found ${entityType} ${distance} blocks away.`) + log(ctx, `Found ${entityType} ${distance} blocks away.`) await goToPosition( - bot, + ctx, entity.position.x, entity.position.y, entity.position.z, @@ -90,55 +92,52 @@ export async function goToNearestEntity( /** * Navigate to a specific player */ -export async function goToPlayer(bot: Bot, username: string, distance = 3): Promise { - if (bot.modes.isOn('cheat')) { +export async function goToPlayer( + ctx: SkillContext, + username: string, + distance = 3, +): Promise { + const { bot } = ctx + if (ctx.allowCheats) { bot.chat(`/tp @s ${username}`) - log(bot, `Teleported to ${username}.`) + log(ctx, `Teleported to ${username}.`) return true } - bot.modes.pause('self_defense') - bot.modes.pause('cowardice') - const player = bot.players[username]?.entity if (!player) { - log(bot, `Could not find ${username}.`) + log(ctx, `Could not find ${username}.`) return false } await bot.pathfinder.goto(bot.pathfinder.goals.GoalFollow(player, distance), true) - log(bot, `You have reached ${username}.`) + log(ctx, `You have reached ${username}.`) return true } /** * Follow a player continuously */ -export async function followPlayer(bot: Bot, username: string, distance = 4): Promise { +export async function followPlayer( + ctx: SkillContext, + username: string, + distance = 4, +): Promise { + const { bot } = ctx const player = bot.players[username]?.entity if (!player) return false bot.pathfinder.setGoal(bot.pathfinder.goals.GoalFollow(player, distance), true) - log(bot, `You are now actively following player ${username}.`) + log(ctx, `You are now actively following player ${username}.`) - while (!bot.interrupt_code) { + while (!ctx.shouldInterrupt) { await new Promise(resolve => setTimeout(resolve, 500)) - if (bot.modes.isOn('cheat') + if (ctx.allowCheats && bot.entity.position.distanceTo(player.position) > 100 && player.isOnGround) { - await goToPlayer(bot, username) - } - - if (bot.modes.isOn('unstuck')) { - const isNearby = bot.entity.position.distanceTo(player.position) <= distance + 1 - if (isNearby) { - bot.modes.pause('unstuck') - } - else { - bot.modes.unpause('unstuck') - } + await goToPlayer(ctx, username) } } return true @@ -147,12 +146,13 @@ export async function followPlayer(bot: Bot, username: string, distance = 4): Pr /** * Move away from current position */ -export async function moveAway(bot: Bot, distance: number): Promise { +export async function moveAway(ctx: SkillContext, distance: number): Promise { + const { bot } = ctx const pos = bot.entity.position const goal = bot.pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, distance) const invertedGoal = bot.pathfinder.goals.GoalInvert(goal) - if (bot.modes.isOn('cheat')) { + if (ctx.allowCheats) { const move = new bot.pathfinder.Movements(bot) const path = await bot.pathfinder.getPathTo(move, invertedGoal, 10000) const lastMove = path.path[path.path.length - 1] @@ -168,7 +168,7 @@ export async function moveAway(bot: Bot, distance: number): Promise { await bot.pathfinder.goto(invertedGoal) const newPos = bot.entity.position - log(bot, `Moved away from nearest entity to ${newPos}.`) + log(ctx, `Moved away from nearest entity to ${newPos}.`) return true } @@ -176,10 +176,11 @@ export async function moveAway(bot: Bot, distance: number): Promise { * Move away from a specific entity */ export async function moveAwayFromEntity( - bot: Bot, + ctx: SkillContext, entity: Entity, distance = 16, ): Promise { + const { bot } = ctx const goal = bot.pathfinder.goals.GoalFollow(entity, distance) const invertedGoal = bot.pathfinder.goals.GoalInvert(goal) await bot.pathfinder.goto(invertedGoal) @@ -189,51 +190,12 @@ export async function moveAwayFromEntity( /** * Stay in current position */ -export async function stay(bot: Bot, seconds = 30): Promise { - bot.modes.pause('self_preservation') - bot.modes.pause('unstuck') - bot.modes.pause('cowardice') - bot.modes.pause('self_defense') - bot.modes.pause('hunting') - bot.modes.pause('torch_placing') - bot.modes.pause('item_collecting') - +export async function stay(ctx: SkillContext, seconds = 30): Promise { const start = Date.now() - while (!bot.interrupt_code && (seconds === -1 || Date.now() - start < seconds * 1000)) { + while (!ctx.shouldInterrupt && (seconds === -1 || Date.now() - start < seconds * 1000)) { await new Promise(resolve => setTimeout(resolve, 500)) } - log(bot, `Stayed for ${(Date.now() - start) / 1000} seconds.`) - return true -} -/** - * Sleep in the nearest bed within 32 blocks - */ -export async function goToBed(bot: Bot): Promise { - const beds: Vec3[] = bot.findBlocks({ - matching: (block: Block) => block.name.includes('bed'), - maxDistance: 32, - count: 1, - }) - - if (beds.length === 0) { - log(bot, 'Could not find a bed to sleep in.') - return false - } - - const loc: Vec3 = beds[0] - await goToPosition(bot, loc.x, loc.y, loc.z) - - const bed: Block | null = bot.blockAt(loc) - await bot.sleep(bed) - log(bot, 'You are in bed.') - - bot.modes.pause('unstuck') - - while (bot.isSleeping) { - await new Promise(resolve => setTimeout(resolve, 500)) - } - - log(bot, 'You have woken up.') + log(ctx, `Stayed for ${(Date.now() - start) / 1000} seconds.`) return true }