389 lines
10 KiB
JavaScript

import debug from 'debug';
import { Queue, Task, isIterable } from 'compass-utils';
import { Vec3 } from 'vec3';
import 'enhanced-vec3';
import assert from 'node:assert/strict';
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),
};
export const AXIS_NAME = {
0: '-Z',
1: '-X',
2: '+Z',
3: '+X',
};
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'];
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.'); }
};
export class NotOnGroundError extends Error {
constructor() { super('bot is not on ground, cannot jump'); }
};
async function moveAxisTask(bot, task, axis_raw, target_raw, level) {
const axis = AXIS[axis_raw];
assert.equal(typeof axis, 'number', 'axis');
assert.ok(0 <= axis && axis <= 3, 'axis');
assert.equal(typeof level, 'number', 'level');
assert.ok(target_raw instanceof Vec3, 'target');
const stable_axis = "xz"[axis % 2];
const target = target_raw.clone();
target.centralizeXZ();
bot.clearControlStates();
bot.control.centralizeXZ();
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());
while (true) {
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 ${TRACK_TICKS} 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! remain: ${remaining_dis}.`);
pos.updateXZ(target);
bot.entity.velocity.setXZ(0, 0);
break;
}
if (remaining_dis < -0.5) {
logger('moveAxisTask() went past target.');
throw new MoveInterferedError();
}
}
bot.clearControlStates();
task._ready(time_used);
}
async function ladderAscendTask(bot, task, target_y) {
assert.equal(typeof target_y, 'number', 'target_y');
bot.control.centralizeXZ();
const start_pos = bot.entity.position.clone();
logger(`ladderAscendTask() initial position: ${start_pos}.`);
logger(`ladderAscendTask() target y: ${target_y}.`);
if (start_pos.y > target_y) {
throw new Error('Invalid Argument: target_y is smaller than current y.');
}
let controls = new ControlState('jump');
controls.apply(bot);
logger('ladderAscendTask() started.');
const TRACK_TICKS = 10;
let time_used = 0, last_y = start_pos.y;
while (true) {
await bot.waitForTicks(1);
task._interuptableHere();
time_used += 1;
const pos = bot.entity.position;
if (pos.xzDistanceTo(start_pos) > 1) { throw new MoveInterferedError(); }
bot.control.centralizeXZ();
if (Math.abs(pos.y - target_y) < 0.2) {
logger('ladderAscendTask() reached.');
bot.clearControlStates();
pos.y = target_y;
break;
}
if (pos.y - target_y > 0.4) {
logger('ladderAscendTask() went past target.');
throw new MoveInterferedError();
}
if (time_used % TRACK_TICKS == 0) {
let now_y = pos.y;
if (Math.abs(now_y - last_y) < 0.3) {
logger(`ladderAscendTask() move too little in past ${TRACK_TICKS} ticks!`);
logger(`ladderAscendTask() now pos.y=${now_y}`);
logger(`ladderAscendTask() ${TRACK_TICKS} ticks ago pos.y=${last_y}`);
throw new MovePathBlockedError();
}
last_y = now_y;
}
}
task._ready(time_used);
}
export default function inject(bot) {
bot.control = {};
bot.control.getState = () => { return ControlState.from(bot.controlState); };
bot.control.centralizeXZ = () => { bot.entity.position.centralizeXZ(); };
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];
assert.ok(typeof axis == 'number');
assert.ok(0 <= axis && axis <= 3);
assert.ok(typeof time == 'number');
if (!bot.entity.onGround) {
throw new NotOnGroundError();
}
bot.control.centralizeXZ();
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.setXZ(0, 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; }
assert.ok(typeof axis == 'number');
assert.ok(0 <= axis && axis <= 3);
assert.ok(typeof dis == 'number');
assert.ok(typeof tactic.sprint == 'boolean');
assert.ok(typeof tactic.speed == 'number');
if (!bot.entity.onGround) {
throw new NotOnGroundError();
}
const axis = AXIS[axis_raw];
bot.control.centralizeXZ();
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.updateXZ(target);
bot.entity.velocity.setXZ(0, 0);
}
bot.control.jump = async () => {
if (!bot.entity.onGround) {
throw new NotOnGroundError();
}
bot.setControlState('jump', true);
await bot.waitForTicks(1);
bot.setControlState('jump', false);
};
bot.control.jumpToHighest = () => {
let task = new Task();
queueMicrotask(async () => {
try {
task._start();
if (!bot.entity.onGround) {
throw new NotOnGroundError();
}
let controls = new ControlState('jump');
controls.apply(bot);
let time_used = 0;
while (true) {
await bot.waitForTicks(1);
task._interuptableHere();
time_used += 1;
if (time_used == 1) {
controls.jump = false;
controls.apply(bot);
}
if (bot.entity.velocity.y < 0) {
break;
}
}
task._ready(time_used);
} catch(err) {
bot.clearControlStates();
task._fail(err);
}
});
return task;
};
bot.control.ladderAscend = (target_y) => {
let task = new Task();
queueMicrotask(async () => {
try {
task._start();
await ladderAscendTask(bot, task, target_y);
} catch(err) {
bot.clearControlStates();
task._fail(err);
}
});
return task;
};
}