mineflayer-navigate
Version:
mineflayer plugin which adds 3d pathfinding
385 lines (349 loc) • 11.5 kB
JavaScript
var aStar = require('a-star')
, EventEmitter = require('events').EventEmitter
module.exports = init;
// instantiated from init
var vec3=require('vec3');
var MONITOR_INTERVAL = 40;
var WATER_THRESHOLD = 20;
var DEFAULT_TIMEOUT = 10 * 1000; // 10 seconds
var DEFAULT_END_RADIUS = 0.1;
// if the distance is more than this number, navigator will opt to simply
// head in the correct direction and recalculate upon getting closer
var TOO_FAR_THRESHOLD = 150;
function init() {
return inject;
}
function inject(bot) {
var currentCourse = [];
var cardinalDirectionVectors = [
vec3(-1, 0, 0), // north
vec3( 1, 0, 0), // south
vec3( 0, 0, -1), // east
vec3( 0, 0, 1), // west
];
bot.navigate = new EventEmitter();
bot.navigate.to = navigateTo;
bot.navigate.stop = noop;
bot.navigate.walk = walk;
bot.navigate.findPathSync = findPathSync;
bot.navigate.blocksToAvoid = {
51: true, // fire
59: true, // crops
10: true, // lava
11: true, // lava
};
function onArrived() {
bot.navigate.emit("arrived");
}
function findPathSync(end, params) {
params = params || {};
end = end.floored()
var timeout = params.timeout == null ? DEFAULT_TIMEOUT : params.timeout;
var endRadius = params.endRadius == null ? DEFAULT_END_RADIUS : params.endRadius;
var tooFarThreshold = params.tooFarThreshold == null ? TOO_FAR_THRESHOLD : params.tooFarThreshold;
var actualIsEnd = params.isEnd || createIsEndWithRadius(end, endRadius);
var heuristic = createHeuristicFn(end);
var start = bot.entity.position.floored();
var tooFar = false;
if (start.distanceTo(end) > tooFarThreshold) {
// Too far to calculate reliably. Return 'tooFar' and a path to walk
// in the general direction of end.
actualIsEnd = function(node) {
// let's just go 100 meters (and not end in water)
return node.water === 0 && start.distanceTo(node.point) >= 100;
};
tooFar = true;
}
// search
var results = aStar({
start: new Node(start, 0),
isEnd: actualIsEnd,
neighbor: getNeighbors,
distance: distanceFunc,
heuristic: heuristic,
timeout: timeout,
});
results.status = tooFar ? 'tooFar' : results.status;
results.path = results.path.map(nodeCenterOffset);
return results;
}
function walk(currentCourse, callback) {
callback = callback || noop;
var lastNodeTime = new Date().getTime();
var monitorInterval = setInterval(monitorMovement, MONITOR_INTERVAL);
bot.navigate.stop('interrupted');
bot.navigate.stop = stop;
function monitorMovement() {
var nextPoint = currentCourse[0];
var currentPosition = bot.entity.position;
if (currentPosition.distanceTo(nextPoint) <= 0.2) {
// arrived at next point
lastNodeTime = new Date().getTime();
currentCourse.shift();
if (currentCourse.length === 0) {
// done
stop('arrived');
return;
}
// not done yet
nextPoint = currentCourse[0];
}
var delta = nextPoint.minus(currentPosition);
var gottaJump = false;
var horizontalDelta = Math.abs(delta.x + delta.z);
if (delta.y > 0.1) {
// gotta jump up when we're close enough
gottaJump = horizontalDelta < 1.75;
} else if (delta.y > -0.1) {
// possibly jump over a hole
gottaJump = 1.5 < horizontalDelta && horizontalDelta < 2.5;
}
bot.setControlState('jump', gottaJump);
// run toward next point
var lookAtY = currentPosition.y + bot.entity.height;
var lookAtPoint = vec3(nextPoint.x, lookAtY, nextPoint.z);
bot.lookAt(lookAtPoint);
bot.setControlState('forward', true);
// check for futility
if (new Date().getTime() - lastNodeTime > 1500) {
// should never take this long to go to the next node
bot.navigate.emit('obstructed');
stop('obstructed');
}
}
function stop(reason) {
bot.navigate.stop = noop;
clearInterval(monitorInterval);
bot.clearControlStates();
bot.navigate.emit("stop", reason);
callback(reason);
}
}
// publicly exposed
function navigateTo(end, params) {
params = params || {};
var onArrivedCb = params.onArrived ? params.onArrived : onArrived;
bot.navigate.stop('interrupted');
var results = findPathSync(end, params);
if (results.status === 'success') {
bot.navigate.emit("pathFound", results.path);
walk(results.path, function() {
onArrivedCb();
});
} else if (results.status === 'tooFar') {
// it's too far, just walk in the general direction and restart the search
bot.navigate.emit("pathPartFound", results.path);
walk(results.path, function() {
navigateTo(end, params);
});
} else if (results.status === 'noPath' || results.status === 'timeout') {
// can't find a path
bot.navigate.emit("cannotFind", results.path);
}
}
function getNeighbors(node) {
// 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
// |
// dz
//
var point = node.point;
var isSafeA = isSafe(bot.blockAt(point.offset(0, 2, 0)));
var result = [];
cardinalDirectionVectors.forEach(function(directionVector) {
var blockH, blockE;
var pointB = pointAt(1, 1);
var blockB = properties(pointB);
if (!blockB.safe) {
// we can do nothing in this direction
return;
}
var pointC = pointAt(1, 0);
var blockC = properties(pointC);
if (!blockC.safe) {
// can't walk forward
if (!blockC.physical) {
// too dangerous
return;
}
if (!isSafeA) {
// can't jump
return;
}
blockH = properties(pointAt(1, 2));
if (!blockH.safe) {
// no head room to stand on c
return;
}
// can jump up onto c
result.push(pointB);
return;
}
// c is open
var pointD = pointAt(1, -1);
var blockD = properties(pointD);
if (blockD.physical) {
// can walk onto d. this is the case of flat ground.
result.push(pointC);
return;
}
if (blockD.safe) {
// safe to drop through d
var pointE = pointAt(1, -2);
blockE = properties(pointE);
if (blockE.physical) {
// can drop onto e
result.push(pointD);
} else if (blockE.safe) {
// can drop through e
var pointF = pointAt(1, -3);
var blockF = properties(pointF);
if (blockF.physical) {
// can drop onto f
result.push(pointE);
} else if (blockF.safe) {
// can drop through f
var blockG = properties(pointAt(1, -4));
if (blockG.physical) {
result.push(pointF);
}
}
}
}
// might be able to jump over the d hole.
blockH = properties(pointAt(1, 2));
var blockO = properties(pointAt(2, 2));
var canJumpForward = isSafeA && blockH.safe && blockO.safe;
var pointI = pointAt(2, 1);
var blockI = properties(pointI);
var pointJ = pointAt(2, 0);
var blockJ = properties(pointJ);
if (canJumpForward && blockI.safe && blockJ.physical) {
// can jump over and up onto j
result.push(pointI);
}
var pointK = pointAt(2, -1);
var blockK = properties(pointK);
var canJumpPastJ = canJumpForward && blockJ.safe && blockI.safe;
if (canJumpPastJ && blockK.physical) {
// can jump over onto k
result.push(pointJ);
canJumpPastJ = false;
}
// might be able to walk and drop forward
var pointL = pointAt(2, -2);
var blockL = properties(pointL);
var canLandOnL = false;
if (blockI.safe && blockJ.safe && blockK.safe && blockL.physical) {
// can walk and drop onto l
canLandOnL = true;
result.push(pointK);
}
if (blockE === undefined) blockE = properties(pointAt(1, -2));
var canLandOnM = false;
if (blockE.safe) {
// can drop through e
var pointM = pointAt(2, -3);
var blockM = properties(pointM);
if (blockJ.safe && blockK.safe && blockL.safe && blockM.physical) {
// can walk and drop onto m
canLandOnM = true;
result.push(pointL);
}
var blockN = properties(pointAt(2, -4));
if (blockK.safe && blockL.safe && blockM.safe && blockN.physical) {
// can walk and drop onto n
result.push(pointM);
}
}
if (!canJumpPastJ) return;
// 3rd column
var blockP = properties(pointAt(3, 1));
var pointQ = pointAt(3, 0);
var blockQ = properties(pointQ);
var pointR = pointAt(3, -1);
var blockR = properties(pointR);
if (blockP.safe && blockQ.safe && blockR.physical) {
// can jump way over onto r
result.push(pointQ);
return;
}
var pointS = pointAt(3, -2);
var blockS = properties(pointS);
if (!canLandOnL && blockQ.safe && blockR.safe && blockS.physical) {
// can jump way over and down onto s
result.push(pointR);
return;
}
var blockT = properties(pointAt(3, -3));
if (!canLandOnM && blockR.safe && blockS.safe && blockT.physical) {
// can jump way over and down onto t
result.push(pointS);
return;
}
function pointAt(horizontalOffset, dy) {
return point.offset(directionVector.x * horizontalOffset, dy, directionVector.z * horizontalOffset);
}
function properties(point) {
var block = bot.blockAt(point);
return block ? {
safe: isSafe(block),
physical: block.boundingBox === 'block',
} : {
safe: false,
physical: false,
};
}
}); // cardinalDirectionVectors.forEach
return result.map(function(point) {
var faceBlock = bot.blockAt(point.offset(0, 1, 0));
var water = 0;
if (faceBlock.type === 0x08 || faceBlock.type === 0x09) {
water = node.water + 1;
}
return new Node(point, water);
}).filter(function(node) {
return node.water <= WATER_THRESHOLD;
});
}
function isSafe(block) {
return block.boundingBox === 'empty' &&
!bot.navigate.blocksToAvoid[block.type];
}
}
function createIsEndWithRadius(end, radius) {
return function(node) {
return node.point.distanceTo(end) <= radius;
};
}
function distanceFunc(nodeA, nodeB) {
return nodeA.point.distanceTo(nodeB.point);
}
function nodeCenterOffset(node) {
return node.point.offset(0.5, 0, 0.5);
}
function Node(point, water) {
this.point = point;
this.water = water;
}
Node.prototype.toString = function() {
// must declare a toString so that A* works.
return this.point.toString() + ":" + this.water;
};
function createHeuristicFn(end) {
return function(node) {
return node.point.distanceTo(end) + 5 * node.water;
};
}
function noop() {}