272 lines
7.2 KiB
JavaScript
272 lines
7.2 KiB
JavaScript
import debug from 'debug';
|
|
import { Queue, Task, isIterable } from 'compass-utils';
|
|
import { Vec3 } from 'vec3';
|
|
const logger = debug('mineflayer-control');
|
|
|
|
// yaw = axis * Math.PI / 2
|
|
// name = "ZX"[axis % 2]
|
|
export const AXIS = {
|
|
'-Z': 0,
|
|
'-X': 1,
|
|
'+Z': 2,
|
|
'+X': 3,
|
|
NORTH: 0,
|
|
WEST: 1,
|
|
SOUTH: 2,
|
|
EAST: 3,
|
|
0: 0,
|
|
1: 1,
|
|
2: 2,
|
|
3: 3,
|
|
};
|
|
|
|
export const AXIS_UNIT = {
|
|
0: new Vec3(0, 0, -1),
|
|
1: new Vec3(-1, 0, 0),
|
|
2: new Vec3(0, 0, 1),
|
|
3: new Vec3(1, 0, 0),
|
|
};
|
|
|
|
AXIS_UNIT['-Z'] = AXIS_UNIT['NORTH'] = AXIS_UNIT[0];
|
|
AXIS_UNIT['-X'] = AXIS_UNIT['WEST'] = AXIS_UNIT[1];
|
|
AXIS_UNIT['+Z'] = AXIS_UNIT['SOUTH'] = AXIS_UNIT[2];
|
|
AXIS_UNIT['+X'] = AXIS_UNIT['EAST'] = AXIS_UNIT[3];
|
|
|
|
export const MOVE_LEVEL = {
|
|
WALK: 1,
|
|
SPRINT: 2,
|
|
// TODO: SPRINT_JUMP
|
|
};
|
|
|
|
export class ControlState {
|
|
constructor() {
|
|
this.clear();
|
|
for (let key of arguments) { this[key] = true; }
|
|
}
|
|
|
|
clear() {
|
|
for (let key of ControlState.CONTROLS) { this[key] = false; }
|
|
}
|
|
|
|
update(cs) {
|
|
for (let key of ControlState.CONTROLS) {
|
|
this[key] = cs[key] || false;
|
|
}
|
|
}
|
|
|
|
static from(cs) {
|
|
let res = new ControlState();
|
|
res.update(cs);
|
|
return res;
|
|
}
|
|
|
|
apply(bot) {
|
|
for (let key of ControlState.CONTROLS) {
|
|
bot.setControlState(key, this[key]);
|
|
}
|
|
}
|
|
|
|
enable(c) {
|
|
for (let key of arguments) { this[key] = true; }
|
|
}
|
|
|
|
disable(c) {
|
|
for (let key of arguments) { this[key] = false; }
|
|
}
|
|
};
|
|
|
|
ControlState.CONTROLS = ['forward', 'back', 'left', 'right', 'jump', 'sprint', 'sneak'];
|
|
|
|
function adjust05(x) {
|
|
return Math.floor(x) + .5;
|
|
}
|
|
|
|
function adjustXZ(vec) {
|
|
vec.x = adjust05(vec.x);
|
|
vec.z = adjust05(vec.z);
|
|
}
|
|
|
|
export class MoveInterferedError extends Error {
|
|
constructor() { super('Move task has been interfered by an external force.'); }
|
|
};
|
|
|
|
export class MovePathBlockedError extends Error {
|
|
constructor() { super('Move path is possiblely blocked.'); }
|
|
};
|
|
|
|
async function moveAxisTask(bot, task, axis_raw, target_raw, level) {
|
|
const axis = AXIS[axis_raw];
|
|
const stable_axis = "xz"[axis % 2];
|
|
const target = target_raw.clone();
|
|
adjustXZ(target);
|
|
|
|
bot.clearControlStates();
|
|
bot.control.adjustXZ();
|
|
|
|
let pos = bot.entity.position;
|
|
const delta = target.minus(pos);
|
|
let remaining_dis = delta.dot(AXIS_UNIT[axis]);
|
|
|
|
logger(`moveAxisTask() source: ${pos}.`);
|
|
logger(`moveAxisTask() target: ${target}.`);
|
|
logger(`moveAxisTask() delta: ${delta}.`);
|
|
logger(`moveAxisTask() distance: ${remaining_dis}.`);
|
|
logger(`moveAxisTask() stable_axis: ${stable_axis}.`);
|
|
logger(`moveAxisTask() Condition: ${delta[stable_axis]} ${delta.y}.`);
|
|
if (Math.abs(delta.y) > 0.5 + Number.EPSILON
|
|
|| Math.abs(delta[stable_axis]) > Number.EPSILON) {
|
|
throw new Error('Invalid Argument: target');
|
|
}
|
|
|
|
if (remaining_dis < 0) {
|
|
throw new Error('Invalid Argument: axis argument should reverse its sign.');
|
|
}
|
|
|
|
const stable_axis_value = target[stable_axis];
|
|
|
|
logger('moveAxisTask() pre adjust look angle');
|
|
await bot.look(axis * Math.PI / 2, 0, true);
|
|
logger('moveAxisTask() post adjust look angle');
|
|
task._interuptableHere();
|
|
|
|
const controls = new ControlState('forward');
|
|
if (level >= MOVE_LEVEL.SPRINT) { controls.sprint = true; }
|
|
logger('moveAxisTask() control', controls);
|
|
controls.apply(bot);
|
|
logger('moveAxisTask() started.');
|
|
|
|
let time_used = 0, pos_queue = new Queue();
|
|
const TRACK_TICKS = 5;
|
|
pos_queue.push(pos.clone());
|
|
do {
|
|
await bot.waitForTicks(1);
|
|
task._interuptableHere();
|
|
|
|
controls.apply(bot);
|
|
time_used += 1;
|
|
pos = bot.entity.position;
|
|
if (Math.abs(pos[stable_axis] - stable_axis_value) > 1.2) {
|
|
logger('moveAxisTask() stable axis changed.');
|
|
logger(`moveAxisTask() target.${stable_axis}: ${stable_axis_value}.`);
|
|
logger(`moveAxisTask() pos.${stable_axis}: ${pos[stable_axis]}.`);
|
|
throw new MoveInterferedError();
|
|
}
|
|
|
|
if (Math.abs(pos.y - target.y) > 0.5 + Number.EPSILON) {
|
|
logger('moveAxisTask() y changed to much.');
|
|
logger(`moveAxisTask() target.y=${target.y} vs. pos.y=${pos.y}`);
|
|
throw new MoveInterferedError();
|
|
}
|
|
|
|
pos[stable_axis] = stable_axis_value;
|
|
pos_queue.push(pos.clone());
|
|
if (pos_queue.size() > TRACK_TICKS) { pos_queue.popFront(); }
|
|
|
|
if (pos_queue.size() == 5) {
|
|
let pos5t = pos_queue.front();
|
|
if (pos.distanceSquared(pos5t) < Number.EPSILON) {
|
|
logger('moveAxisTask() position changed too little.');
|
|
logger(`moveAxisTask() position 5 ticks ago: ${pos5t}.`);
|
|
logger(`moveAxisTask() position now: ${pos}.`);
|
|
throw new MovePathBlockedError();
|
|
}
|
|
}
|
|
|
|
delta.update(target.minus(pos));
|
|
remaining_dis = delta.dot(AXIS_UNIT[axis]);
|
|
if (Math.abs(remaining_dis) <= 0.5) {
|
|
logger('moveAxisTask() very close to target now.');
|
|
pos.x = target.x;
|
|
pos.z = target.z;
|
|
bot.entity.velocity.x = 0;
|
|
bot.entity.velocity.z = 0;
|
|
break;
|
|
}
|
|
|
|
if (remaining_dis < -0.5) {
|
|
logger('moveAxisTask() went past target.');
|
|
throw new MoveInterferedError();
|
|
}
|
|
} while (true);
|
|
bot.clearControlStates();
|
|
task._ready(time_used);
|
|
}
|
|
|
|
export default function inject(bot) {
|
|
bot.control = {};
|
|
bot.control.getState = () => { return ControlState.from(bot.controlState); };
|
|
bot.control.adjustXZ = () => { adjustXZ(bot.entity.position); };
|
|
|
|
bot.control.moveAxis = (axis, target, level = MOVE_LEVEL.SPRINT) => {
|
|
let task = new Task();
|
|
queueMicrotask(async () => {
|
|
try {
|
|
task._start();
|
|
await moveAxisTask(bot, task, axis, target, level);
|
|
} catch(err) {
|
|
bot.clearControlStates();
|
|
task._fail(err);
|
|
};
|
|
});
|
|
return task;
|
|
};
|
|
|
|
bot.control.jumpUp = async (axis_raw, time=5) => {
|
|
const axis = AXIS[axis_raw];
|
|
bot.control.adjustXZ();
|
|
await bot.look(axis * Math.PI / 2, 0, true);
|
|
let controls = new ControlState('forward', 'jump');
|
|
let pos = bot.entity.position;
|
|
controls.apply(bot);
|
|
await bot.waitForTicks(time);
|
|
bot.clearControlStates();
|
|
bot.entity.position.update(pos.plus(AXIS_UNIT[axis]).offset(0, 1, 0));
|
|
bot.entity.velocity.x = 0;
|
|
bot.entity.velocity.z = 0;
|
|
await bot.waitForTicks(1);
|
|
};
|
|
|
|
bot.control.jumpForward = async (axis_raw, dis=2, tactic) => {
|
|
if (tactic == null) { tactic = {}; }
|
|
if (tactic.sprint == null) { tactic.sprint = dis > 3; }
|
|
if (tactic.speed == null) { tactic.speed = tactic.sprint ? .355 : .216; }
|
|
|
|
const axis = AXIS[axis_raw];
|
|
bot.control.adjustXZ();
|
|
let target = bot.entity.position.plus(AXIS_UNIT[axis].scaled(dis));
|
|
logger(`jumpForward() axis: ${"zx"[axis % 2]}`);
|
|
logger(`jumpForward() target: ${target}`);
|
|
logger(`jumpForward() tactic: sprint=${tactic.sprint}, speed=${tactic.speed}`);
|
|
await bot.look(axis * Math.PI / 2, 0, true);
|
|
|
|
let controls = new ControlState('forward', 'jump');
|
|
controls.sprint = tactic.sprint;
|
|
controls.apply(bot);
|
|
bot.entity.velocity.add(AXIS_UNIT[axis].scaled(tactic.speed));
|
|
|
|
await bot.waitForTicks(1);
|
|
controls.jump = false;
|
|
controls.apply(bot);
|
|
logger(`jumpForward() ${bot.entity.velocity}`);
|
|
|
|
await bot.waitForTicks(Math.floor((dis / tactic.speed) - 1));
|
|
bot.clearControlStates();
|
|
|
|
let pos = bot.entity.position;
|
|
logger(`jumpForward() done at ${pos}.`);
|
|
if (pos.distanceTo(target) > 1) {
|
|
throw new MoveInterferedError();
|
|
}
|
|
pos.x = target.x;
|
|
pos.z = target.z;
|
|
bot.entity.velocity.x = 0;
|
|
bot.entity.velocity.z = 0;
|
|
}
|
|
|
|
bot.control.jump = async () => {
|
|
bot.setControlState('jump', true);
|
|
await bot.waitForTicks(1);
|
|
bot.setControlState('jump', false);
|
|
};
|
|
}
|