493 lines
12 KiB
JavaScript
493 lines
12 KiB
JavaScript
const { Vec3 } = require('vec3')
|
|
const { getShapeFaceCenters } = require('./shapes')
|
|
|
|
// Goal base class
|
|
class Goal {
|
|
// Return the distance between node and the goal
|
|
heuristic (node) {
|
|
return 0
|
|
}
|
|
|
|
// Return true if the node has reach the goal
|
|
isEnd (node) {
|
|
return true
|
|
}
|
|
|
|
// Return true if the goal has changed and the current path
|
|
// should be invalidated and computed again
|
|
hasChanged () {
|
|
return false
|
|
}
|
|
|
|
// Returns true if the goal is still valid for the goal,
|
|
// for the GoalFollow this would be true if the entity is not null
|
|
isValid () {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// One specific block that the player should stand inside at foot level
|
|
class GoalBlock extends Goal {
|
|
constructor (x, y, z) {
|
|
super()
|
|
this.x = Math.floor(x)
|
|
this.y = Math.floor(y)
|
|
this.z = Math.floor(z)
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dx = this.x - node.x
|
|
const dy = this.y - node.y
|
|
const dz = this.z - node.z
|
|
return distanceXZ(dx, dz) + Math.abs(dy)
|
|
}
|
|
|
|
isEnd (node) {
|
|
return node.x === this.x && node.y === this.y && node.z === this.z
|
|
}
|
|
}
|
|
|
|
// A block position that the player should get within a certain radius of, used for following entities
|
|
class GoalNear extends Goal {
|
|
constructor (x, y, z, range) {
|
|
super()
|
|
this.x = Math.floor(x)
|
|
this.y = Math.floor(y)
|
|
this.z = Math.floor(z)
|
|
this.rangeSq = range * range
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dx = this.x - node.x
|
|
const dy = this.y - node.y
|
|
const dz = this.z - node.z
|
|
return distanceXZ(dx, dz) + Math.abs(dy)
|
|
}
|
|
|
|
isEnd (node) {
|
|
const dx = this.x - node.x
|
|
const dy = this.y - node.y
|
|
const dz = this.z - node.z
|
|
return (dx * dx + dy * dy + dz * dz) <= this.rangeSq
|
|
}
|
|
}
|
|
|
|
// Useful for long-range goals that don't have a specific Y level
|
|
class GoalXZ extends Goal {
|
|
constructor (x, z) {
|
|
super()
|
|
this.x = Math.floor(x)
|
|
this.z = Math.floor(z)
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dx = this.x - node.x
|
|
const dz = this.z - node.z
|
|
return distanceXZ(dx, dz)
|
|
}
|
|
|
|
isEnd (node) {
|
|
return node.x === this.x && node.z === this.z
|
|
}
|
|
}
|
|
|
|
// Useful for finding builds that you don't have an exact Y level for, just an approximate X and Z level
|
|
class GoalNearXZ extends Goal {
|
|
constructor (x, z, range) {
|
|
super()
|
|
this.x = Math.floor(x)
|
|
this.z = Math.floor(z)
|
|
this.rangeSq = range * range
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dx = this.x - node.x
|
|
const dz = this.z - node.z
|
|
return distanceXZ(dx, dz)
|
|
}
|
|
|
|
isEnd (node) {
|
|
const dx = this.x - node.x
|
|
const dz = this.z - node.z
|
|
return (dx * dx + dz * dz) <= this.rangeSq
|
|
}
|
|
}
|
|
|
|
// Goal is a Y coordinate
|
|
class GoalY extends Goal {
|
|
constructor (y) {
|
|
super()
|
|
this.y = Math.floor(y)
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dy = this.y - node.y
|
|
return Math.abs(dy)
|
|
}
|
|
|
|
isEnd (node) {
|
|
return node.y === this.y
|
|
}
|
|
}
|
|
|
|
// Don't get into the block, but get directly adjacent to it. Useful for chests.
|
|
class GoalGetToBlock extends Goal {
|
|
constructor (x, y, z) {
|
|
super()
|
|
this.x = Math.floor(x)
|
|
this.y = Math.floor(y)
|
|
this.z = Math.floor(z)
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dx = node.x - this.x
|
|
const dy = node.y - this.y
|
|
const dz = node.z - this.z
|
|
return distanceXZ(dx, dz) + Math.abs(dy < 0 ? dy + 1 : dy)
|
|
}
|
|
|
|
isEnd (node) {
|
|
const dx = node.x - this.x
|
|
const dy = node.y - this.y
|
|
const dz = node.z - this.z
|
|
return Math.abs(dx) + Math.abs(dy < 0 ? dy + 1 : dy) + Math.abs(dz) === 1
|
|
}
|
|
}
|
|
|
|
// Path into a position were a blockface of block at x y z is visible.
|
|
class GoalLookAtBlock extends Goal {
|
|
constructor (pos, world, options = {}) {
|
|
super()
|
|
this.pos = pos
|
|
this.world = world
|
|
this.reach = options.reach || 4.5 // default survival: 4.5 creative: 5
|
|
this.entityHeight = options.entityHeight || 1.6
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dx = node.x - this.pos.x
|
|
const dy = node.y - this.pos.y
|
|
const dz = node.z - this.pos.z
|
|
return distanceXZ(dx, dz) + Math.abs(dy < 0 ? dy + 1 : dy)
|
|
}
|
|
|
|
isEnd (node) {
|
|
if (node.distanceTo(this.pos.offset(0, this.entityHeight, 0)) > this.reach) return false
|
|
// Check faces that could be seen from the current position. If the delta is smaller then 0.5 that means the bot cam most likely not see the face as the block is 1 block thick
|
|
// this could be false for blocks that have a smaller bounding box then 1x1x1
|
|
const dx = node.x - (this.pos.x + 0.5)
|
|
const dy = node.y + this.entityHeight - (this.pos.y + 0.5) // -0.5 because the bot position is calculated from the block position that is inside its feet so 0.5 - 1 = -0.5
|
|
const dz = node.z - (this.pos.z + 0.5)
|
|
// Check y first then x and z
|
|
const visibleFaces = {
|
|
y: Math.sign(Math.abs(dy) > 0.5 ? dy : 0),
|
|
x: Math.sign(Math.abs(dx) > 0.5 ? dx : 0),
|
|
z: Math.sign(Math.abs(dz) > 0.5 ? dz : 0)
|
|
}
|
|
const validFaces = []
|
|
for (const i in visibleFaces) {
|
|
if (!visibleFaces[i]) {
|
|
// skip as this face is not visible
|
|
continue
|
|
}
|
|
const targetPos = new Vec3(this.pos.x, this.pos.y, this.pos.z).offset(0.5 + (i === 'x' ? visibleFaces[i] * 0.5 : 0), 0.5 + (i === 'y' ? visibleFaces[i] * 0.5 : 0), 0.5 + (i === 'z' ? visibleFaces[i] * 0.5 : 0))
|
|
const startPos = new Vec3(node.x + 0.5, node.y + this.entityHeight, node.z + 0.5)
|
|
const rayPos = this.world.raycast(startPos, targetPos.clone().subtract(startPos).normalize(), this.reach)?.position
|
|
if (rayPos && rayPos.x === this.pos.x && rayPos.y === this.pos.y && rayPos.z === this.pos.z) {
|
|
validFaces.push({
|
|
face: rayPos.face,
|
|
targetPos
|
|
})
|
|
}
|
|
}
|
|
return validFaces.length !== 0
|
|
}
|
|
}
|
|
|
|
// Path into a position were a blockface of block at x y z is visible.
|
|
// You'll manually need to break the block. THIS WONT BREAK IT
|
|
class GoalBreakBlock extends Goal {
|
|
constructor (x, y, z, bot, options = {}) {
|
|
super()
|
|
this.goal = new GoalLookAtBlock(new Vec3(x, y, z), bot, options)
|
|
}
|
|
|
|
isEnd () {
|
|
return this.goal.isEnd()
|
|
}
|
|
|
|
heuristic (node) {
|
|
return this.goal.heuristic(node)
|
|
}
|
|
}
|
|
|
|
// A composite of many goals, any one of which satisfies the composite.
|
|
// For example, a GoalCompositeAny of block goals for every oak log in loaded
|
|
// chunks would result in it pathing to the easiest oak log to get to
|
|
class GoalCompositeAny extends Goal {
|
|
constructor (goals = []) {
|
|
super()
|
|
this.goals = goals
|
|
}
|
|
|
|
push (goal) {
|
|
this.goals.push(goal)
|
|
}
|
|
|
|
heuristic (node) {
|
|
let min = Number.MAX_VALUE
|
|
for (const i in this.goals) {
|
|
min = Math.min(min, this.goals[i].heuristic(node))
|
|
}
|
|
return min
|
|
}
|
|
|
|
isEnd (node) {
|
|
for (const i in this.goals) {
|
|
if (this.goals[i].isEnd(node)) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
hasChanged () {
|
|
for (const i in this.goals) {
|
|
if (this.goals[i].hasChanged()) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
isValid () {
|
|
return this.goals.reduce((pre, curr) => pre && curr.isValid(), true)
|
|
}
|
|
}
|
|
|
|
// A composite of many goals, all of them needs to be satisfied.
|
|
class GoalCompositeAll extends Goal {
|
|
constructor (goals = []) {
|
|
super()
|
|
this.goals = goals
|
|
}
|
|
|
|
push (goal) {
|
|
this.goals.push(goal)
|
|
}
|
|
|
|
heuristic (node) {
|
|
let max = Number.MIN_VALUE
|
|
for (const i in this.goals) {
|
|
max = Math.max(max, this.goals[i].heuristic(node))
|
|
}
|
|
return max
|
|
}
|
|
|
|
isEnd (node) {
|
|
for (const i in this.goals) {
|
|
if (!this.goals[i].isEnd(node)) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
hasChanged () {
|
|
for (const i in this.goals) {
|
|
if (this.goals[i].hasChanged()) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
isValid () {
|
|
return this.goals.reduce((pre, curr) => pre && curr.isValid(), true)
|
|
}
|
|
}
|
|
|
|
class GoalInvert extends Goal {
|
|
constructor (goal) {
|
|
super()
|
|
this.goal = goal
|
|
}
|
|
|
|
heuristic (node) {
|
|
return -this.goal.heuristic(node)
|
|
}
|
|
|
|
isEnd (node) {
|
|
return !this.goal.isEnd(node)
|
|
}
|
|
|
|
hasChanged () {
|
|
return this.goal.hasChanged()
|
|
}
|
|
|
|
isValid () {
|
|
return this.goal.isValid()
|
|
}
|
|
}
|
|
|
|
class GoalFollow extends Goal {
|
|
constructor (entity, range) {
|
|
super()
|
|
this.entity = entity
|
|
this.x = Math.floor(entity.position.x)
|
|
this.y = Math.floor(entity.position.y)
|
|
this.z = Math.floor(entity.position.z)
|
|
this.rangeSq = range * range
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dx = this.x - node.x
|
|
const dy = this.y - node.y
|
|
const dz = this.z - node.z
|
|
return distanceXZ(dx, dz) + Math.abs(dy)
|
|
}
|
|
|
|
isEnd (node) {
|
|
const dx = this.x - node.x
|
|
const dy = this.y - node.y
|
|
const dz = this.z - node.z
|
|
return (dx * dx + dy * dy + dz * dz) <= this.rangeSq
|
|
}
|
|
|
|
hasChanged () {
|
|
const p = this.entity.position.floored()
|
|
const dx = this.x - p.x
|
|
const dy = this.y - p.y
|
|
const dz = this.z - p.z
|
|
if ((dx * dx + dy * dy + dz * dz) > this.rangeSq) {
|
|
this.x = p.x
|
|
this.y = p.y
|
|
this.z = p.z
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
isValid () {
|
|
return this.entity != null
|
|
}
|
|
}
|
|
|
|
function distanceXZ (dx, dz) {
|
|
dx = Math.abs(dx)
|
|
dz = Math.abs(dz)
|
|
return Math.abs(dx - dz) + Math.min(dx, dz) * Math.SQRT2
|
|
}
|
|
|
|
/**
|
|
* Options:
|
|
* - range - maximum distance from the clicked face
|
|
* - faces - the directions of the faces the player can click
|
|
* - facing - the direction the player must be facing
|
|
* - facing3D - boolean, facing is 3D (true) or 2D (false)
|
|
* - half - 'top' or 'bottom', the half that must be clicked
|
|
* - LOS - true or false, should the bot have line of sight off the placement face. Default true.
|
|
*/
|
|
class GoalPlaceBlock extends Goal {
|
|
constructor (pos, world, options) {
|
|
super()
|
|
this.pos = pos.floored()
|
|
this.world = world
|
|
this.options = options
|
|
if (!this.options.range) this.options.range = 5
|
|
if (!('LOS' in this.options)) this.options.LOS = true
|
|
if (!this.options.faces) {
|
|
this.options.faces = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)]
|
|
}
|
|
this.options.facing = ['north', 'east', 'south', 'west', 'up', 'down'].indexOf(this.options.facing)
|
|
this.facesPos = []
|
|
for (const dir of this.options.faces) {
|
|
const ref = this.pos.plus(dir)
|
|
const refBlock = this.world.getBlock(ref)
|
|
if (!refBlock) continue
|
|
for (const center of getShapeFaceCenters(refBlock.shapes, dir.scaled(-1), this.options.half)) {
|
|
this.facesPos.push([dir, center.add(ref), ref])
|
|
}
|
|
}
|
|
}
|
|
|
|
heuristic (node) {
|
|
const dx = node.x - this.pos.x
|
|
const dy = node.y - this.pos.y
|
|
const dz = node.z - this.pos.z
|
|
return distanceXZ(dx, dz) + Math.abs(dy < 0 ? dy + 1 : dy)
|
|
}
|
|
|
|
isEnd (node) {
|
|
if (this.isStandingIn(node)) return false
|
|
const headPos = node.offset(0.5, 1.6, 0.5)
|
|
return this.getFaceAndRef(headPos) !== null
|
|
}
|
|
|
|
getFaceAndRef (headPos) {
|
|
for (const [face, to, ref] of this.facesPos) {
|
|
const dir = to.minus(headPos)
|
|
if (dir.norm() > this.options.range) continue
|
|
if (!this.checkFacing(dir)) continue
|
|
|
|
if (!this.options.LOS) {
|
|
return { face, to, ref }
|
|
}
|
|
|
|
const block = this.world.raycast(headPos, dir.normalize(), this.options.range)
|
|
if (block && block.position.equals(ref) && block.face === vectorToDirection(face.scaled(-1))) {
|
|
return { face, to, ref }
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
checkFacing (dir) {
|
|
if (this.options.facing < 0) return true
|
|
|
|
if (this.options.facing3D) {
|
|
const dH = Math.sqrt(dir.x * dir.x + dir.z * dir.z)
|
|
const vAngle = Math.atan2(dir.y, dH) * 180 / Math.PI
|
|
if (vAngle > 45) return this.options.facing === 4
|
|
if (vAngle < -45) return this.options.facing === 5
|
|
}
|
|
const angle = Math.atan2(dir.x, -dir.z) * 180 / Math.PI + 180 // Convert to [0,360[
|
|
const facing = Math.floor(angle / 90 + 0.5) & 0x3
|
|
|
|
if (this.options.facing === facing) return true
|
|
return false
|
|
}
|
|
|
|
isStandingIn (node) {
|
|
const dx = node.x - this.pos.x
|
|
const dy = node.y - this.pos.y
|
|
const dz = node.z - this.pos.z
|
|
return (Math.abs(dx) + Math.abs(dy < 0 ? dy + 1 : dy) + Math.abs(dz)) < 1
|
|
}
|
|
}
|
|
|
|
function vectorToDirection (v) {
|
|
if (v.y < 0) {
|
|
return 0
|
|
} else if (v.y > 0) {
|
|
return 1
|
|
} else if (v.z < 0) {
|
|
return 2
|
|
} else if (v.z > 0) {
|
|
return 3
|
|
} else if (v.x < 0) {
|
|
return 4
|
|
} else if (v.x > 0) {
|
|
return 5
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
Goal,
|
|
GoalBlock,
|
|
GoalNear,
|
|
GoalXZ,
|
|
GoalNearXZ,
|
|
GoalY,
|
|
GoalGetToBlock,
|
|
GoalCompositeAny,
|
|
GoalCompositeAll,
|
|
GoalInvert,
|
|
GoalFollow,
|
|
GoalPlaceBlock,
|
|
GoalBreakBlock,
|
|
GoalLookAtBlock
|
|
}
|