const { Vec3 } = require('vec3') const nbt = require('prismarine-nbt') const Move = require('./move') const cardinalDirections = [ { x: -1, z: 0 }, // West { x: 1, z: 0 }, // East { x: 0, z: -1 }, // North { x: 0, z: 1 } // South ] const diagonalDirections = [ { x: -1, z: -1 }, { x: -1, z: 1 }, { x: 1, z: -1 }, { x: 1, z: 1 } ] class Movements { constructor (bot) { const registry = bot.registry this.bot = bot this.canDig = true this.digCost = 1 this.placeCost = 1 this.liquidCost = 1 this.entityCost = 1 this.dontCreateFlow = true this.dontMineUnderFallingBlock = true this.allow1by1towers = true this.allowFreeMotion = false this.allowParkour = true this.allowSprinting = true this.allowEntityDetection = true this.entitiesToAvoid = new Set() this.passableEntities = new Set(require('./passableEntities.json')) this.interactableBlocks = new Set(require('./interactable.json')) this.blocksCantBreak = new Set() this.blocksCantBreak.add(registry.blocksByName.chest.id) registry.blocksArray.forEach(block => { if (block.diggable) return this.blocksCantBreak.add(block.id) }) this.blocksToAvoid = new Set() this.blocksToAvoid.add(registry.blocksByName.fire.id) if (registry.blocksByName.cobweb) this.blocksToAvoid.add(registry.blocksByName.cobweb.id) if (registry.blocksByName.web) this.blocksToAvoid.add(registry.blocksByName.web.id) this.blocksToAvoid.add(registry.blocksByName.lava.id) this.liquids = new Set() this.liquids.add(registry.blocksByName.water.id) this.liquids.add(registry.blocksByName.lava.id) this.gravityBlocks = new Set() this.gravityBlocks.add(registry.blocksByName.sand.id) this.gravityBlocks.add(registry.blocksByName.gravel.id) this.climbables = new Set() this.climbables.add(registry.blocksByName.ladder.id) // this.climbables.add(registry.blocksByName.vine.id) this.emptyBlocks = new Set() this.replaceables = new Set() this.replaceables.add(registry.blocksByName.air.id) if (registry.blocksByName.cave_air) this.replaceables.add(registry.blocksByName.cave_air.id) if (registry.blocksByName.void_air) this.replaceables.add(registry.blocksByName.void_air.id) this.replaceables.add(registry.blocksByName.water.id) this.replaceables.add(registry.blocksByName.lava.id) this.scafoldingBlocks = [] this.scafoldingBlocks.push(registry.itemsByName.dirt.id) this.scafoldingBlocks.push(registry.itemsByName.cobblestone.id) const Block = require('prismarine-block')(bot.registry) this.fences = new Set() this.carpets = new Set() this.openable = new Set() registry.blocksArray.map(x => Block.fromStateId(x.minStateId, 0)).forEach(block => { if (block.shapes.length > 0) { // Fences or any block taller than 1, they will be considered as non-physical to avoid // trying to walk on them if (block.shapes[0][4] > 1) this.fences.add(block.type) // Carpets or any blocks smaller than 0.1, they will be considered as safe to walk in if (block.shapes[0][4] < 0.1) this.carpets.add(block.type) } else if (block.shapes.length === 0) { this.emptyBlocks.add(block.type) } }) registry.blocksArray.forEach(block => { if (this.interactableBlocks.has(block.name) && block.name.toLowerCase().includes('gate') && !block.name.toLowerCase().includes('iron')) { // console.info(block) this.openable.add(block.id) } }) this.canOpenDoors = false // Causes issues. Probably due to none paper servers. this.exclusionAreasStep = [] this.exclusionAreasBreak = [] this.exclusionAreasPlace = [] this.maxDropDown = 4 this.infiniteLiquidDropdownDistance = true this.entityIntersections = {} } exclusionPlace (block) { if (this.exclusionAreasPlace.length === 0) return 0 let weight = 0 for (const a of this.exclusionAreasPlace) { weight += a(block) } return weight } exclusionStep (block) { if (this.exclusionAreasStep.length === 0) return 0 let weight = 0 for (const a of this.exclusionAreasStep) { weight += a(block) } return weight } exclusionBreak (block) { if (this.exclusionAreasBreak.length === 0) return 0 let weight = 0 for (const a of this.exclusionAreasBreak) { weight += a(block) } return weight } countScaffoldingItems () { let count = 0 const items = this.bot.inventory.items() for (const id of this.scafoldingBlocks) { for (const j in items) { const item = items[j] if (item.type === id) count += item.count } } return count } getScaffoldingItem () { const items = this.bot.inventory.items() for (const id of this.scafoldingBlocks) { for (const j in items) { const item = items[j] if (item.type === id) return item } } return null } clearCollisionIndex () { this.entityIntersections = {} } /** * Finds blocks intersected by entity bounding boxes * and sets the number of ents intersecting in a dict. * Ignores entities that do not affect block placement */ updateCollisionIndex () { for (const ent of Object.values(this.bot.entities)) { if (ent === this.bot.entity) { continue } const avoidedEnt = this.entitiesToAvoid.has(ent.name) if (avoidedEnt || !this.passableEntities.has(ent.name)) { const entSquareRadius = ent.width / 2.0 const minY = Math.floor(ent.position.y) const maxY = Math.ceil(ent.position.y + ent.height) const minX = Math.floor(ent.position.x - entSquareRadius) const maxX = Math.ceil(ent.position.x + entSquareRadius) const minZ = Math.floor(ent.position.z - entSquareRadius) const maxZ = Math.ceil(ent.position.z + entSquareRadius) const cost = avoidedEnt ? 100 : 1 for (let y = minY; y < maxY; y++) { for (let x = minX; x < maxX; x++) { for (let z = minZ; z < maxZ; z++) { this.entityIntersections[`${x},${y},${z}`] = this.entityIntersections[`${x},${y},${z}`] ?? 0 this.entityIntersections[`${x},${y},${z}`] += cost // More ents = more weight } } } } } } /** * Gets number of entities who's bounding box intersects the node + offset * @param {import('vec3').Vec3} pos node position * @param {number} dx X axis offset * @param {number} dy Y axis offset * @param {number} dz Z axis offset * @returns {number} Number of entities intersecting block */ getNumEntitiesAt (pos, dx, dy, dz) { if (this.allowEntityDetection === false) return 0 if (!pos) return 0 const y = pos.y + dy const x = pos.x + dx const z = pos.z + dz return this.entityIntersections[`${x},${y},${z}`] ?? 0 } getBlock (pos, dx, dy, dz) { const b = pos ? this.bot.blockAt(new Vec3(pos.x + dx, pos.y + dy, pos.z + dz), false) : null if (!b) { return { replaceable: false, canFall: false, safe: false, physical: false, liquid: false, climbable: false, height: dy, openable: false } } b.climbable = this.climbables.has(b.type) b.safe = (b.boundingBox === 'empty' || b.climbable || this.carpets.has(b.type)) && !this.blocksToAvoid.has(b.type) b.physical = b.boundingBox === 'block' && !this.fences.has(b.type) b.replaceable = this.replaceables.has(b.type) && !b.physical b.liquid = this.liquids.has(b.type) b.height = pos.y + dy b.canFall = this.gravityBlocks.has(b.type) b.openable = this.openable.has(b.type) for (const shape of b.shapes) { b.height = Math.max(b.height, pos.y + dy + shape[4]) } return b } /** * Takes into account if the block is within a break exclusion area. * @param {import('prismarine-block').Block} block * @returns */ safeToBreak (block) { if (!this.canDig) { return false } if (this.dontCreateFlow) { // false if next to liquid if (this.getBlock(block.position, 0, 1, 0).liquid) return false if (this.getBlock(block.position, -1, 0, 0).liquid) return false if (this.getBlock(block.position, 1, 0, 0).liquid) return false if (this.getBlock(block.position, 0, 0, -1).liquid) return false if (this.getBlock(block.position, 0, 0, 1).liquid) return false } if (this.dontMineUnderFallingBlock) { // TODO: Determine if there are other blocks holding the entity up if (this.getBlock(block.position, 0, 1, 0).canFall || (this.getNumEntitiesAt(block.position, 0, 1, 0) > 0)) { return false } } return block.type && !this.blocksCantBreak.has(block.type) && this.exclusionBreak(block) < 100 } /** * Takes into account if the block is within the stepExclusionAreas. And returns 100 if a block to be broken is within break exclusion areas. * @param {import('prismarine-block').Block} block block * @param {[]} toBreak * @returns {number} */ safeOrBreak (block, toBreak) { let cost = 0 cost += this.exclusionStep(block) // Is excluded so can't move or break cost += this.getNumEntitiesAt(block.position, 0, 0, 0) * this.entityCost if (block.safe) return cost if (!this.safeToBreak(block)) return 100 // Can't break, so can't move toBreak.push(block.position) if (block.physical) cost += this.getNumEntitiesAt(block.position, 0, 1, 0) * this.entityCost // Add entity cost if there is an entity above (a breakable block) that will fall const tool = this.bot.pathfinder.bestHarvestTool(block) const enchants = (tool && tool.nbt) ? nbt.simplify(tool.nbt).Enchantments : [] const effects = this.bot.entity.effects const digTime = block.digTime(tool ? tool.type : null, false, false, false, enchants, effects) const laborCost = (1 + 3 * digTime / 1000) * this.digCost cost += laborCost return cost } getMoveJumpUp (node, dir, neighbors) { const blockA = this.getBlock(node, 0, 2, 0) const blockH = this.getBlock(node, dir.x, 2, dir.z) const blockB = this.getBlock(node, dir.x, 1, dir.z) const blockC = this.getBlock(node, dir.x, 0, dir.z) let cost = 2 // move cost (move+jump) const toBreak = [] const toPlace = [] if (blockA.physical && (this.getNumEntitiesAt(blockA.position, 0, 1, 0) > 0)) return // Blocks A, B and H are above C, D and the player's space, we need to make sure there are no entities that will fall down onto our building space if we break them if (blockH.physical && (this.getNumEntitiesAt(blockH.position, 0, 1, 0) > 0)) return if (blockB.physical && !blockH.physical && !blockC.physical && (this.getNumEntitiesAt(blockB.position, 0, 1, 0) > 0)) return // It is fine if an ent falls on B so long as we don't need to replace block C if (!blockC.physical) { if (node.remainingBlocks === 0) return // not enough blocks to place if (this.getNumEntitiesAt(blockC.position, 0, 0, 0) > 0) return // Check for any entities in the way of a block placement const blockD = this.getBlock(node, dir.x, -1, dir.z) if (!blockD.physical) { if (node.remainingBlocks === 1) return // not enough blocks to place if (this.getNumEntitiesAt(blockD.position, 0, 0, 0) > 0) return // Check for any entities in the way of a block placement if (!blockD.replaceable) { if (!this.safeToBreak(blockD)) return cost += this.exclusionBreak(blockD) toBreak.push(blockD.position) } cost += this.exclusionPlace(blockD) toPlace.push({ x: node.x, y: node.y - 1, z: node.z, dx: dir.x, dy: 0, dz: dir.z, returnPos: new Vec3(node.x, node.y, node.z) }) cost += this.placeCost // additional cost for placing a block } if (!blockC.replaceable) { if (!this.safeToBreak(blockC)) return cost += this.exclusionBreak(blockC) toBreak.push(blockC.position) } cost += this.exclusionPlace(blockC) toPlace.push({ x: node.x + dir.x, y: node.y - 1, z: node.z + dir.z, dx: 0, dy: 1, dz: 0 }) cost += this.placeCost // additional cost for placing a block blockC.height += 1 } const block0 = this.getBlock(node, 0, -1, 0) if (blockC.height - block0.height > 1.2) return // Too high to jump cost += this.safeOrBreak(blockA, toBreak) if (cost > 100) return cost += this.safeOrBreak(blockH, toBreak) if (cost > 100) return cost += this.safeOrBreak(blockB, toBreak) if (cost > 100) return neighbors.push(new Move(blockB.position.x, blockB.position.y, blockB.position.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } getMoveForward (node, dir, neighbors) { const blockB = this.getBlock(node, dir.x, 1, dir.z) const blockC = this.getBlock(node, dir.x, 0, dir.z) const blockD = this.getBlock(node, dir.x, -1, dir.z) let cost = 1 // move cost cost += this.exclusionStep(blockC) const toBreak = [] const toPlace = [] if (!blockD.physical && !blockC.liquid) { if (node.remainingBlocks === 0) return // not enough blocks to place if (this.getNumEntitiesAt(blockD.position, 0, 0, 0) > 0) return // D intersects an entity hitbox if (!blockD.replaceable) { if (!this.safeToBreak(blockD)) return cost += this.exclusionBreak(blockD) toBreak.push(blockD.position) } cost += this.exclusionPlace(blockC) toPlace.push({ x: node.x, y: node.y - 1, z: node.z, dx: dir.x, dy: 0, dz: dir.z }) cost += this.placeCost // additional cost for placing a block } cost += this.safeOrBreak(blockB, toBreak) if (cost > 100) return // Open fence gates if (this.canOpenDoors && blockC.openable && blockC.shapes && blockC.shapes.length !== 0) { toPlace.push({ x: node.x + dir.x, y: node.y, z: node.z + dir.z, dx: 0, dy: 0, dz: 0, useOne: true }) // Indicate that a block should be used on this block not placed } else { cost += this.safeOrBreak(blockC, toBreak) if (cost > 100) return } if (this.getBlock(node, 0, 0, 0).liquid) cost += this.liquidCost neighbors.push(new Move(blockC.position.x, blockC.position.y, blockC.position.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } getMoveDiagonal (node, dir, neighbors) { let cost = Math.SQRT2 // move cost const toBreak = [] const blockC = this.getBlock(node, dir.x, 0, dir.z) // Landing block or standing on block when jumping up by 1 const y = blockC.physical ? 1 : 0 const block0 = this.getBlock(node, 0, -1, 0) let cost1 = 0 const toBreak1 = [] const blockB1 = this.getBlock(node, 0, y + 1, dir.z) const blockC1 = this.getBlock(node, 0, y, dir.z) const blockD1 = this.getBlock(node, 0, y - 1, dir.z) cost1 += this.safeOrBreak(blockB1, toBreak1) cost1 += this.safeOrBreak(blockC1, toBreak1) if (blockD1.height - block0.height > 1.2) cost1 += this.safeOrBreak(blockD1, toBreak1) let cost2 = 0 const toBreak2 = [] const blockB2 = this.getBlock(node, dir.x, y + 1, 0) const blockC2 = this.getBlock(node, dir.x, y, 0) const blockD2 = this.getBlock(node, dir.x, y - 1, 0) cost2 += this.safeOrBreak(blockB2, toBreak2) cost2 += this.safeOrBreak(blockC2, toBreak2) if (blockD2.height - block0.height > 1.2) cost2 += this.safeOrBreak(blockD2, toBreak2) if (cost1 < cost2) { cost += cost1 toBreak.push(...toBreak1) } else { cost += cost2 toBreak.push(...toBreak2) } if (cost > 100) return cost += this.safeOrBreak(this.getBlock(node, dir.x, y, dir.z), toBreak) if (cost > 100) return cost += this.safeOrBreak(this.getBlock(node, dir.x, y + 1, dir.z), toBreak) if (cost > 100) return if (this.getBlock(node, 0, 0, 0).liquid) cost += this.liquidCost const blockD = this.getBlock(node, dir.x, -1, dir.z) if (y === 1) { // Case jump up by 1 if (blockC.height - block0.height > 1.2) return // Too high to jump cost += this.safeOrBreak(this.getBlock(node, 0, 2, 0), toBreak) if (cost > 100) return cost += 1 neighbors.push(new Move(blockC.position.x, blockC.position.y + 1, blockC.position.z, node.remainingBlocks, cost, toBreak)) } else if (blockD.physical || blockC.liquid) { neighbors.push(new Move(blockC.position.x, blockC.position.y, blockC.position.z, node.remainingBlocks, cost, toBreak)) } else if (this.getBlock(node, dir.x, -2, dir.z).physical || blockD.liquid) { if (!blockD.safe) return // don't self-immolate cost += this.getNumEntitiesAt(blockC.position, 0, -1, 0) * this.entityCost neighbors.push(new Move(blockC.position.x, blockC.position.y - 1, blockC.position.z, node.remainingBlocks, cost, toBreak)) } } getLandingBlock (node, dir) { let blockLand = this.getBlock(node, dir.x, -2, dir.z) while (blockLand.position && blockLand.position.y > this.bot.game.minY) { if (blockLand.liquid && blockLand.safe) return blockLand if (blockLand.physical) { if (node.y - blockLand.position.y <= this.maxDropDown) return this.getBlock(blockLand.position, 0, 1, 0) return null } if (!blockLand.safe) return null blockLand = this.getBlock(blockLand.position, 0, -1, 0) } return null } getMoveDropDown (node, dir, neighbors) { const blockB = this.getBlock(node, dir.x, 1, dir.z) const blockC = this.getBlock(node, dir.x, 0, dir.z) const blockD = this.getBlock(node, dir.x, -1, dir.z) let cost = 1 // move cost const toBreak = [] const toPlace = [] const blockLand = this.getLandingBlock(node, dir) if (!blockLand) return if (!this.infiniteLiquidDropdownDistance && ((node.y - blockLand.position.y) > this.maxDropDown)) return // Don't drop down into water cost += this.safeOrBreak(blockB, toBreak) if (cost > 100) return cost += this.safeOrBreak(blockC, toBreak) if (cost > 100) return cost += this.safeOrBreak(blockD, toBreak) if (cost > 100) return if (blockC.liquid) return // dont go underwater cost += this.getNumEntitiesAt(blockLand.position, 0, 0, 0) * this.entityCost // add cost for entities neighbors.push(new Move(blockLand.position.x, blockLand.position.y, blockLand.position.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } getMoveDown (node, neighbors) { const block0 = this.getBlock(node, 0, -1, 0) let cost = 1 // move cost const toBreak = [] const toPlace = [] const blockLand = this.getLandingBlock(node, { x: 0, z: 0 }) if (!blockLand) return cost += this.safeOrBreak(block0, toBreak) if (cost > 100) return if (this.getBlock(node, 0, 0, 0).liquid) return // dont go underwater cost += this.getNumEntitiesAt(blockLand.position, 0, 0, 0) * this.entityCost // add cost for entities neighbors.push(new Move(blockLand.position.x, blockLand.position.y, blockLand.position.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } getMoveUp (node, neighbors) { const block1 = this.getBlock(node, 0, 0, 0) if (block1.liquid) return if (this.getNumEntitiesAt(node, 0, 0, 0) > 0) return // an entity (besides the player) is blocking the building area const block2 = this.getBlock(node, 0, 2, 0) let cost = 1 // move cost const toBreak = [] const toPlace = [] cost += this.safeOrBreak(block2, toBreak) if (cost > 100) return if (!block1.climbable) { if (!this.allow1by1towers || node.remainingBlocks === 0) return // not enough blocks to place if (!block1.replaceable) { if (!this.safeToBreak(block1)) return toBreak.push(block1.position) } const block0 = this.getBlock(node, 0, -1, 0) if (block0.physical && block0.height - node.y < -0.2) return // cannot jump-place from a half block cost += this.exclusionPlace(block1) toPlace.push({ x: node.x, y: node.y - 1, z: node.z, dx: 0, dy: 1, dz: 0, jump: true }) cost += this.placeCost // additional cost for placing a block } if (cost > 100) return neighbors.push(new Move(node.x, node.y + 1, node.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } // Jump up, down or forward over a 1 block gap getMoveParkourForward (node, dir, neighbors) { const block0 = this.getBlock(node, 0, -1, 0) const block1 = this.getBlock(node, dir.x, -1, dir.z) if ((block1.physical && block1.height >= block0.height) || !this.getBlock(node, dir.x, 0, dir.z).safe || !this.getBlock(node, dir.x, 1, dir.z).safe) return if (this.getBlock(node, 0, 0, 0).liquid) return // cant jump from water let cost = 1 // Leaving entities at the ceiling level (along path) out for now because there are few cases where that will be important cost += this.getNumEntitiesAt(node, dir.x, 0, dir.z) * this.entityCost // If we have a block on the ceiling, we cannot jump but we can still fall let ceilingClear = this.getBlock(node, 0, 2, 0).safe && this.getBlock(node, dir.x, 2, dir.z).safe // Similarly for the down path let floorCleared = !this.getBlock(node, dir.x, -2, dir.z).physical const maxD = this.allowSprinting ? 4 : 2 for (let d = 2; d <= maxD; d++) { const dx = dir.x * d const dz = dir.z * d const blockA = this.getBlock(node, dx, 2, dz) const blockB = this.getBlock(node, dx, 1, dz) const blockC = this.getBlock(node, dx, 0, dz) const blockD = this.getBlock(node, dx, -1, dz) if (blockC.safe) cost += this.getNumEntitiesAt(blockC.position, 0, 0, 0) * this.entityCost if (ceilingClear && blockB.safe && blockC.safe && blockD.physical) { cost += this.exclusionStep(blockB) // Forward neighbors.push(new Move(blockC.position.x, blockC.position.y, blockC.position.z, node.remainingBlocks, cost, [], [], true)) break } else if (ceilingClear && blockB.safe && blockC.physical) { // Up if (blockA.safe && d !== 4) { // 4 Blocks forward 1 block up is very difficult and fails often cost += this.exclusionStep(blockA) if (blockC.height - block0.height > 1.2) break // Too high to jump cost += this.getNumEntitiesAt(blockB.position, 0, 0, 0) * this.entityCost neighbors.push(new Move(blockB.position.x, blockB.position.y, blockB.position.z, node.remainingBlocks, cost, [], [], true)) break } } else if ((ceilingClear || d === 2) && blockB.safe && blockC.safe && blockD.safe && floorCleared) { // Down const blockE = this.getBlock(node, dx, -2, dz) if (blockE.physical) { cost += this.exclusionStep(blockD) cost += this.getNumEntitiesAt(blockD.position, 0, 0, 0) * this.entityCost neighbors.push(new Move(blockD.position.x, blockD.position.y, blockD.position.z, node.remainingBlocks, cost, [], [], true)) } floorCleared = floorCleared && !blockE.physical } else if (!blockB.safe || !blockC.safe) { break } ceilingClear = ceilingClear && blockA.safe } } // for each cardinal direction: // "." is head. "+" is feet and current location. // "#" is initial floor which is always solid. "a"-"u" are blocks to check // // --0123-- horizontalOffset // | // +2 aho // +1 .bip // 0 +cjq // -1 #dkr // -2 els // -3 fmt // -4 gn // | // dy getNeighbors (node) { const neighbors = [] // Simple moves in 4 cardinal points for (const i in cardinalDirections) { const dir = cardinalDirections[i] this.getMoveForward(node, dir, neighbors) this.getMoveJumpUp(node, dir, neighbors) this.getMoveDropDown(node, dir, neighbors) if (this.allowParkour) { this.getMoveParkourForward(node, dir, neighbors) } } // Diagonals for (const i in diagonalDirections) { const dir = diagonalDirections[i] this.getMoveDiagonal(node, dir, neighbors) } this.getMoveDown(node, neighbors) this.getMoveUp(node, neighbors) return neighbors } } module.exports = Movements