2024-01-25 13:25:36 -08:00
|
|
|
import * as skills from './library/skills.js';
|
|
|
|
import * as world from './library/world.js';
|
|
|
|
import * as mc from '../utils/mcdata.js';
|
2024-08-22 15:57:20 -05:00
|
|
|
import settings from '../../settings.js'
|
2024-10-02 00:57:28 -05:00
|
|
|
import { handleTranslation } from '../utils/translator.js';
|
2024-08-26 17:34:50 -05:00
|
|
|
|
|
|
|
|
|
|
|
async function say(agent, message) {
|
2024-09-27 17:03:00 -05:00
|
|
|
agent.bot.modes.behavior_log += message + '\n';
|
2024-08-22 15:57:20 -05:00
|
|
|
if (agent.shut_up || !settings.narrate_behavior) return;
|
2024-10-02 01:01:22 -05:00
|
|
|
let translation = await handleTranslation(message);
|
2024-08-26 17:34:50 -05:00
|
|
|
agent.bot.chat(translation);
|
2024-08-22 15:57:20 -05:00
|
|
|
}
|
2024-01-23 18:01:38 -06:00
|
|
|
|
|
|
|
// a mode is a function that is called every tick to respond immediately to the world
|
|
|
|
// it has the following fields:
|
|
|
|
// on: whether 'update' is called every tick
|
|
|
|
// active: whether an action has been triggered by the mode and hasn't yet finished
|
|
|
|
// paused: whether the mode is paused by another action that overrides the behavior (eg followplayer implements its own self defense)
|
|
|
|
// update: the function that is called every tick (if on is true)
|
|
|
|
// when a mode is active, it will trigger an action to be performed but won't wait for it to return output
|
2024-01-26 13:18:30 -06:00
|
|
|
|
2024-01-23 18:01:38 -06:00
|
|
|
// the order of this list matters! first modes will be prioritized
|
2024-01-26 13:18:30 -06:00
|
|
|
// while update functions are async, they should *not* be awaited longer than ~100ms as it will block the update loop
|
|
|
|
// to perform longer actions, use the execute function which won't block the update loop
|
2024-01-23 18:01:38 -06:00
|
|
|
const modes = [
|
2024-04-20 12:25:49 -05:00
|
|
|
{
|
|
|
|
name: 'self_preservation',
|
2024-09-25 00:15:55 -05:00
|
|
|
description: 'Respond to drowning, burning, and damage at low health. Interrupts all actions.',
|
2024-04-20 12:25:49 -05:00
|
|
|
interrupts: ['all'],
|
|
|
|
on: true,
|
|
|
|
active: false,
|
2024-04-20 22:22:26 -05:00
|
|
|
fall_blocks: ['sand', 'gravel', 'concrete_powder'], // includes matching substrings like 'sandstone' and 'red_sand'
|
2024-04-20 12:25:49 -05:00
|
|
|
update: async function (agent) {
|
2024-04-20 22:22:26 -05:00
|
|
|
const bot = agent.bot;
|
2024-05-29 21:54:47 -05:00
|
|
|
let block = bot.blockAt(bot.entity.position);
|
|
|
|
let blockAbove = bot.blockAt(bot.entity.position.offset(0, 1, 0));
|
2024-05-14 21:05:21 -05:00
|
|
|
if (!block) block = {name: 'air'}; // hacky fix when blocks are not loaded
|
|
|
|
if (!blockAbove) blockAbove = {name: 'air'};
|
|
|
|
if (blockAbove.name === 'water' || blockAbove.name === 'flowing_water') {
|
2024-04-20 12:25:49 -05:00
|
|
|
// does not call execute so does not interrupt other actions
|
2024-04-20 22:22:26 -05:00
|
|
|
if (!bot.pathfinder.goal) {
|
|
|
|
bot.setControlState('jump', true);
|
|
|
|
}
|
|
|
|
}
|
2024-05-14 21:05:21 -05:00
|
|
|
else if (this.fall_blocks.some(name => blockAbove.name.includes(name))) {
|
2024-04-20 22:22:26 -05:00
|
|
|
execute(this, agent, async () => {
|
|
|
|
await skills.moveAway(bot, 2);
|
|
|
|
});
|
2024-04-20 12:25:49 -05:00
|
|
|
}
|
2024-04-20 22:22:26 -05:00
|
|
|
else if (block.name === 'lava' || block.name === 'flowing_lava' || block.name === 'fire' ||
|
|
|
|
blockAbove.name === 'lava' || blockAbove.name === 'flowing_lava' || blockAbove.name === 'fire') {
|
2024-08-22 15:57:20 -05:00
|
|
|
say(agent, 'I\'m on fire!'); // TODO: gets stuck in lava
|
2024-04-20 12:25:49 -05:00
|
|
|
execute(this, agent, async () => {
|
2024-04-20 22:22:26 -05:00
|
|
|
let nearestWater = world.getNearestBlock(bot, 'water', 20);
|
2024-04-20 12:25:49 -05:00
|
|
|
if (nearestWater) {
|
2024-04-20 22:22:26 -05:00
|
|
|
const pos = nearestWater.position;
|
|
|
|
await skills.goToPosition(bot, pos.x, pos.y, pos.z, 0.2);
|
2024-08-22 15:57:20 -05:00
|
|
|
say(agent, 'Ahhhh that\'s better!');
|
2024-04-20 12:25:49 -05:00
|
|
|
}
|
|
|
|
else {
|
2024-04-20 22:22:26 -05:00
|
|
|
await skills.moveAway(bot, 5);
|
2024-04-20 12:25:49 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-04-20 22:22:26 -05:00
|
|
|
else if (Date.now() - bot.lastDamageTime < 3000 && (bot.health < 5 || bot.lastDamageTaken >= bot.health)) {
|
2024-08-22 15:57:20 -05:00
|
|
|
say(agent, 'I\'m dying!');
|
2024-04-20 12:25:49 -05:00
|
|
|
execute(this, agent, async () => {
|
2024-04-20 22:22:26 -05:00
|
|
|
await skills.moveAway(bot, 20);
|
2024-04-20 12:25:49 -05:00
|
|
|
});
|
|
|
|
}
|
2024-04-20 22:22:26 -05:00
|
|
|
else if (agent.isIdle()) {
|
|
|
|
bot.clearControlStates(); // clear jump if not in danger or doing anything else
|
|
|
|
}
|
2024-04-20 12:25:49 -05:00
|
|
|
}
|
|
|
|
},
|
2024-09-25 00:15:55 -05:00
|
|
|
{
|
|
|
|
name: 'unstuck',
|
|
|
|
description: 'Attempt to get unstuck when in the same place for a while. Interrupts some actions.',
|
2024-09-26 22:28:46 -05:00
|
|
|
interrupts: ['collectBlocks', 'goToPlayer', 'collectAllBlocks', 'goToPlace'],
|
2024-09-25 00:15:55 -05:00
|
|
|
on: true,
|
|
|
|
active: false,
|
|
|
|
prev_location: null,
|
2024-09-26 22:28:46 -05:00
|
|
|
distance: 2,
|
2024-09-25 00:15:55 -05:00
|
|
|
stuck_time: 0,
|
|
|
|
last_time: Date.now(),
|
2024-09-26 22:28:46 -05:00
|
|
|
max_stuck_time: 20,
|
2024-09-25 00:15:55 -05:00
|
|
|
update: async function (agent) {
|
|
|
|
if (agent.isIdle()) return;
|
|
|
|
const bot = agent.bot;
|
2024-09-26 22:28:46 -05:00
|
|
|
if (this.prev_location && this.prev_location.distanceTo(bot.entity.position) < this.distance) {
|
2024-09-25 00:15:55 -05:00
|
|
|
this.stuck_time += (Date.now() - this.last_time) / 1000;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.prev_location = bot.entity.position.clone();
|
|
|
|
this.stuck_time = 0;
|
|
|
|
}
|
|
|
|
if (this.stuck_time > this.max_stuck_time) {
|
|
|
|
say(agent, 'I\'m stuck!');
|
|
|
|
execute(this, agent, async () => {
|
|
|
|
await skills.moveAway(bot, 5);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.last_time = Date.now();
|
|
|
|
}
|
|
|
|
},
|
2024-04-13 22:56:18 -05:00
|
|
|
{
|
|
|
|
name: 'cowardice',
|
2024-09-25 00:15:55 -05:00
|
|
|
description: 'Run away from enemies. Interrupts all actions.',
|
2024-04-20 22:22:26 -05:00
|
|
|
interrupts: ['all'],
|
2024-04-13 22:56:18 -05:00
|
|
|
on: true,
|
|
|
|
active: false,
|
|
|
|
update: async function (agent) {
|
|
|
|
const enemy = world.getNearestEntityWhere(agent.bot, entity => mc.isHostile(entity), 16);
|
|
|
|
if (enemy && await world.isClearPath(agent.bot, enemy)) {
|
2024-08-22 15:57:20 -05:00
|
|
|
say(agent, `Aaa! A ${enemy.name}!`);
|
2024-04-13 22:56:18 -05:00
|
|
|
execute(this, agent, async () => {
|
2024-05-01 12:38:33 -07:00
|
|
|
await skills.avoidEnemies(agent.bot, 24);
|
2024-04-13 22:56:18 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2024-01-23 18:01:38 -06:00
|
|
|
{
|
|
|
|
name: 'self_defense',
|
2024-09-25 00:15:55 -05:00
|
|
|
description: 'Attack nearby enemies. Interrupts all actions.',
|
2024-01-26 12:11:32 -08:00
|
|
|
interrupts: ['all'],
|
2024-01-23 18:01:38 -06:00
|
|
|
on: true,
|
|
|
|
active: false,
|
2024-01-26 13:18:30 -06:00
|
|
|
update: async function (agent) {
|
2024-05-01 12:19:42 -07:00
|
|
|
const enemy = world.getNearestEntityWhere(agent.bot, entity => mc.isHostile(entity), 8);
|
2024-01-26 13:18:30 -06:00
|
|
|
if (enemy && await world.isClearPath(agent.bot, enemy)) {
|
2024-08-22 15:57:20 -05:00
|
|
|
say(agent, `Fighting ${enemy.name}!`);
|
2024-01-23 18:01:38 -06:00
|
|
|
execute(this, agent, async () => {
|
2024-05-01 12:19:42 -07:00
|
|
|
await skills.defendSelf(agent.bot, 8);
|
2024-01-23 18:01:38 -06:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'hunting',
|
2024-04-20 12:25:49 -05:00
|
|
|
description: 'Hunt nearby animals when idle.',
|
2024-09-25 00:15:55 -05:00
|
|
|
interrupts: [],
|
2024-01-23 18:01:38 -06:00
|
|
|
on: true,
|
|
|
|
active: false,
|
2024-01-26 13:18:30 -06:00
|
|
|
update: async function (agent) {
|
2024-01-26 12:11:32 -08:00
|
|
|
const huntable = world.getNearestEntityWhere(agent.bot, entity => mc.isHuntable(entity), 8);
|
2024-02-02 15:34:17 -06:00
|
|
|
if (huntable && await world.isClearPath(agent.bot, huntable)) {
|
2024-01-26 12:11:32 -08:00
|
|
|
execute(this, agent, async () => {
|
2024-08-22 15:57:20 -05:00
|
|
|
say(agent, `Hunting ${huntable.name}!`);
|
2024-01-26 12:11:32 -08:00
|
|
|
await skills.attackEntity(agent.bot, huntable);
|
|
|
|
});
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'item_collecting',
|
2024-04-20 12:25:49 -05:00
|
|
|
description: 'Collect nearby items when idle.',
|
2024-01-26 12:11:32 -08:00
|
|
|
interrupts: ['followPlayer'],
|
2024-01-23 18:01:38 -06:00
|
|
|
on: true,
|
|
|
|
active: false,
|
2024-02-02 15:34:17 -06:00
|
|
|
|
|
|
|
wait: 2, // number of seconds to wait after noticing an item to pick it up
|
2024-02-02 15:47:17 -06:00
|
|
|
prev_item: null,
|
|
|
|
noticed_at: -1,
|
2024-01-26 13:18:30 -06:00
|
|
|
update: async function (agent) {
|
2024-01-26 12:11:32 -08:00
|
|
|
let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8);
|
2024-09-26 22:28:46 -05:00
|
|
|
let empty_inv_slots = agent.bot.inventory.emptySlotCount();
|
|
|
|
if (item && item !== this.prev_item && await world.isClearPath(agent.bot, item) && empty_inv_slots > 1) {
|
2024-02-04 19:53:09 -06:00
|
|
|
if (this.noticed_at === -1) {
|
|
|
|
this.noticed_at = Date.now();
|
2024-02-02 15:34:17 -06:00
|
|
|
}
|
2024-02-04 19:53:09 -06:00
|
|
|
if (Date.now() - this.noticed_at > this.wait * 1000) {
|
2024-08-22 15:57:20 -05:00
|
|
|
say(agent, `Picking up item!`);
|
2024-02-04 19:53:09 -06:00
|
|
|
this.prev_item = item;
|
2024-01-23 18:01:38 -06:00
|
|
|
execute(this, agent, async () => {
|
2024-02-02 15:34:17 -06:00
|
|
|
await skills.pickupNearbyItems(agent.bot);
|
2024-01-23 18:01:38 -06:00
|
|
|
});
|
2024-02-02 15:47:17 -06:00
|
|
|
this.noticed_at = -1;
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
|
|
|
}
|
2024-02-02 15:34:17 -06:00
|
|
|
else {
|
2024-02-04 19:53:09 -06:00
|
|
|
this.noticed_at = -1;
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'torch_placing',
|
2024-04-20 12:25:49 -05:00
|
|
|
description: 'Place torches when idle and there are no torches nearby.',
|
2024-01-26 12:11:32 -08:00
|
|
|
interrupts: ['followPlayer'],
|
2024-01-23 18:01:38 -06:00
|
|
|
on: true,
|
|
|
|
active: false,
|
2024-06-03 18:23:01 -05:00
|
|
|
cooldown: 5,
|
|
|
|
last_place: Date.now(),
|
2024-01-23 18:01:38 -06:00
|
|
|
update: function (agent) {
|
2024-06-03 19:09:22 -05:00
|
|
|
if (world.shouldPlaceTorch(agent.bot)) {
|
2024-06-03 18:23:01 -05:00
|
|
|
if (Date.now() - this.last_place < this.cooldown * 1000) return;
|
|
|
|
execute(this, agent, async () => {
|
2024-01-26 12:11:32 -08:00
|
|
|
const pos = agent.bot.entity.position;
|
2024-06-22 15:19:10 -05:00
|
|
|
await skills.placeBlock(agent.bot, 'torch', pos.x, pos.y, pos.z, 'bottom', true);
|
2024-06-03 18:23:01 -05:00
|
|
|
});
|
|
|
|
this.last_place = Date.now();
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'idle_staring',
|
2024-04-20 12:25:49 -05:00
|
|
|
description: 'Animation to look around at entities when idle.',
|
2024-01-26 12:11:32 -08:00
|
|
|
interrupts: [],
|
2024-01-23 18:01:38 -06:00
|
|
|
on: true,
|
|
|
|
active: false,
|
|
|
|
|
|
|
|
staring: false,
|
|
|
|
last_entity: null,
|
|
|
|
next_change: 0,
|
|
|
|
update: function (agent) {
|
2024-01-26 12:11:32 -08:00
|
|
|
const entity = agent.bot.nearestEntity();
|
|
|
|
let entity_in_view = entity && entity.position.distanceTo(agent.bot.entity.position) < 10 && entity.name !== 'enderman';
|
|
|
|
if (entity_in_view && entity !== this.last_entity) {
|
|
|
|
this.staring = true;
|
|
|
|
this.last_entity = entity;
|
|
|
|
this.next_change = Date.now() + Math.random() * 1000 + 4000;
|
|
|
|
}
|
|
|
|
if (entity_in_view && this.staring) {
|
|
|
|
let isbaby = entity.type !== 'player' && entity.metadata[16];
|
|
|
|
let height = isbaby ? entity.height/2 : entity.height;
|
|
|
|
agent.bot.lookAt(entity.position.offset(0, height, 0));
|
|
|
|
}
|
|
|
|
if (!entity_in_view)
|
|
|
|
this.last_entity = null;
|
|
|
|
if (Date.now() > this.next_change) {
|
|
|
|
// look in random direction
|
|
|
|
this.staring = Math.random() < 0.3;
|
|
|
|
if (!this.staring) {
|
|
|
|
const yaw = Math.random() * Math.PI * 2;
|
|
|
|
const pitch = (Math.random() * Math.PI/2) - Math.PI/4;
|
|
|
|
agent.bot.look(yaw, pitch, false);
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
2024-01-26 12:11:32 -08:00
|
|
|
this.next_change = Date.now() + Math.random() * 10000 + 2000;
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2024-06-03 18:23:01 -05:00
|
|
|
{
|
|
|
|
name: 'cheat',
|
|
|
|
description: 'Use cheats to instantly place blocks and teleport.',
|
|
|
|
interrupts: [],
|
|
|
|
on: false,
|
|
|
|
active: false,
|
|
|
|
update: function (agent) { /* do nothing */ }
|
|
|
|
}
|
2024-01-23 18:01:38 -06:00
|
|
|
];
|
|
|
|
|
|
|
|
async function execute(mode, agent, func, timeout=-1) {
|
2024-05-04 15:42:30 -05:00
|
|
|
if (agent.self_prompter.on)
|
|
|
|
agent.self_prompter.stopLoop();
|
2024-01-23 18:01:38 -06:00
|
|
|
mode.active = true;
|
|
|
|
let code_return = await agent.coder.execute(async () => {
|
|
|
|
await func();
|
|
|
|
}, timeout);
|
|
|
|
mode.active = false;
|
|
|
|
console.log(`Mode ${mode.name} finished executing, code_return: ${code_return.message}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
class ModeController {
|
|
|
|
constructor(agent) {
|
|
|
|
this.agent = agent;
|
|
|
|
this.modes_list = modes;
|
|
|
|
this.modes_map = {};
|
2024-09-27 17:03:00 -05:00
|
|
|
this.behavior_log = '';
|
2024-01-23 18:01:38 -06:00
|
|
|
for (let mode of this.modes_list) {
|
|
|
|
this.modes_map[mode.name] = mode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
exists(mode_name) {
|
|
|
|
return this.modes_map[mode_name] != null;
|
|
|
|
}
|
|
|
|
|
|
|
|
setOn(mode_name, on) {
|
|
|
|
this.modes_map[mode_name].on = on;
|
|
|
|
}
|
|
|
|
|
|
|
|
isOn(mode_name) {
|
|
|
|
return this.modes_map[mode_name].on;
|
|
|
|
}
|
|
|
|
|
|
|
|
pause(mode_name) {
|
|
|
|
this.modes_map[mode_name].paused = true;
|
|
|
|
}
|
|
|
|
|
2024-08-22 15:57:20 -05:00
|
|
|
getMiniDocs() { // no descriptions
|
|
|
|
let res = 'Agent Modes:';
|
|
|
|
for (let mode of this.modes_list) {
|
|
|
|
let on = mode.on ? 'ON' : 'OFF';
|
|
|
|
res += `\n- ${mode.name}(${on})`;
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
getDocs() {
|
|
|
|
let res = 'Agent Modes:';
|
2024-01-23 18:01:38 -06:00
|
|
|
for (let mode of this.modes_list) {
|
|
|
|
let on = mode.on ? 'ON' : 'OFF';
|
|
|
|
res += `\n- ${mode.name}(${on}): ${mode.description}`;
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2024-02-02 15:34:17 -06:00
|
|
|
unPauseAll() {
|
|
|
|
for (let mode of this.modes_list) {
|
|
|
|
if (mode.paused) console.log(`Unpausing mode ${mode.name}`);
|
|
|
|
mode.paused = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-26 13:18:30 -06:00
|
|
|
async update() {
|
2024-01-26 15:41:55 -06:00
|
|
|
if (this.agent.isIdle()) {
|
2024-02-02 15:34:17 -06:00
|
|
|
this.unPauseAll();
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
|
|
|
for (let mode of this.modes_list) {
|
2024-09-26 22:28:46 -05:00
|
|
|
let interruptible = mode.interrupts.some(i => i === 'all') || mode.interrupts.some(i => i === this.agent.coder.cur_action_name);
|
2024-09-25 00:15:55 -05:00
|
|
|
if (mode.on && !mode.paused && !mode.active && (this.agent.isIdle() || interruptible)) {
|
2024-01-26 13:18:30 -06:00
|
|
|
await mode.update(this.agent);
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
2024-01-26 12:11:32 -08:00
|
|
|
if (mode.active) break;
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
|
|
|
}
|
2024-04-20 22:22:26 -05:00
|
|
|
|
2024-09-27 17:03:00 -05:00
|
|
|
flushBehaviorLog() {
|
|
|
|
const log = this.behavior_log;
|
|
|
|
this.behavior_log = '';
|
|
|
|
return log;
|
|
|
|
}
|
|
|
|
|
2024-04-20 22:22:26 -05:00
|
|
|
getJson() {
|
|
|
|
let res = {};
|
|
|
|
for (let mode of this.modes_list) {
|
|
|
|
res[mode.name] = mode.on;
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
loadJson(json) {
|
|
|
|
for (let mode of this.modes_list) {
|
|
|
|
if (json[mode.name] != undefined) {
|
|
|
|
mode.on = json[mode.name];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-01-23 18:01:38 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
export function initModes(agent) {
|
|
|
|
// the mode controller is added to the bot object so it is accessible from anywhere the bot is used
|
|
|
|
agent.bot.modes = new ModeController(agent);
|
2024-06-03 18:23:01 -05:00
|
|
|
let modes = agent.prompter.getInitModes();
|
|
|
|
if (modes) {
|
|
|
|
agent.bot.modes.loadJson(modes);
|
|
|
|
}
|
2024-01-25 13:25:36 -08:00
|
|
|
}
|