import { readdirSync, readFileSync } from 'fs'; import { NPCData } from './data.js'; import { ItemGoal } from './item_goal.js'; import { BuildGoal } from './build_goal.js'; import { itemSatisfied, rotateXZ } from './utils.js'; import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; import * as mc from '../../utils/mcdata.js'; export class NPCContoller { constructor(agent) { this.agent = agent; this.data = NPCData.fromObject(agent.prompter.profile.npc); this.temp_goals = []; this.item_goal = new ItemGoal(agent, this.data); this.build_goal = new BuildGoal(agent); this.constructions = {}; this.last_goals = {}; } getBuiltPositions() { let positions = []; for (let name in this.data.built) { let position = this.data.built[name].position; let offset = this.constructions[name].offset; let sizex = this.constructions[name].blocks[0][0].length; let sizez = this.constructions[name].blocks[0].length; let sizey = this.constructions[name].blocks.length; for (let y = offset; y < sizey+offset; y++) { for (let z = 0; z < sizez; z++) { for (let x = 0; x < sizex; x++) { positions.push({x: position.x + x, y: position.y + y, z: position.z + z}); } } } } return positions; } init() { try { for (let file of readdirSync('src/agent/npc/construction')) { if (file.endsWith('.json')) { this.constructions[file.slice(0, -5)] = JSON.parse(readFileSync('src/agent/npc/construction/' + file, 'utf8')); } } } catch (e) { console.log('Error reading construction file'); } for (let name in this.constructions) { let sizez = this.constructions[name].blocks[0].length; let sizex = this.constructions[name].blocks[0][0].length; let max_size = Math.max(sizex, sizez); for (let y = 0; y < this.constructions[name].blocks.length; y++) { for (let z = 0; z < max_size; z++) { if (z >= this.constructions[name].blocks[y].length) this.constructions[name].blocks[y].push([]); for (let x = 0; x < max_size; x++) { if (x >= this.constructions[name].blocks[y][z].length) this.constructions[name].blocks[y][z].push(''); } } } } this.agent.bot.on('idle', async () => { if (this.data.goals.length === 0 && !this.data.curr_goal) return; // Wait a while for inputs before acting independently await new Promise((resolve) => setTimeout(resolve, 5000)); if (!this.agent.isIdle()) return; // Persue goal if (!this.agent.actions.resume_func) { this.executeNext(); this.agent.history.save(); } }); } async setGoal(name=null, quantity=1) { this.data.curr_goal = null; this.last_goals = {}; if (name) { this.data.curr_goal = {name: name, quantity: quantity}; return; } if (!this.data.do_set_goal) return; let past_goals = {...this.last_goals}; for (let goal in this.data.goals) { if (past_goals[goal.name] === undefined) past_goals[goal.name] = true; } let res = await this.agent.prompter.promptGoalSetting(this.agent.history.getHistory(), past_goals); if (res) { this.data.curr_goal = res; console.log('Set new goal: ', res.name, ' x', res.quantity); } else { console.log('Error setting new goal.'); } } async executeNext() { if (!this.agent.isIdle()) return; await this.agent.actions.runAction('npc:moveAway', async () => { await skills.moveAway(this.agent.bot, 2); }); if (!this.data.do_routine || this.agent.bot.time.timeOfDay < 13000) { // Exit any buildings let building = this.currentBuilding(); if (building == this.data.home) { let door_pos = this.getBuildingDoor(building); if (door_pos) { await this.agent.actions.runAction('npc:exitBuilding', async () => { await skills.useDoor(this.agent.bot, door_pos); await skills.moveAway(this.agent.bot, 2); // If the bot is too close to the building it will try to enter again }); } } // Work towards goals await this.executeGoal(); } else { // Reset goal at the end of the day this.data.curr_goal = null; // Return to home let building = this.currentBuilding(); if (this.data.home !== null && (building === null || building != this.data.home)) { let door_pos = this.getBuildingDoor(this.data.home); await this.agent.actions.runAction('npc:returnHome', async () => { await skills.useDoor(this.agent.bot, door_pos); }); } // Go to bed await this.agent.actions.runAction('npc:bed', async () => { await skills.goToBed(this.agent.bot); }); } if (this.agent.isIdle()) this.agent.bot.emit('idle'); } async executeGoal() { // If we need more blocks to complete a building, get those first let goals = this.temp_goals.concat(this.data.goals); if (this.data.curr_goal) goals = goals.concat([this.data.curr_goal]) this.temp_goals = []; let acted = false; for (let goal of goals) { // Obtain goal item or block if (this.constructions[goal.name] === undefined) { if (!itemSatisfied(this.agent.bot, goal.name, goal.quantity)) { let res = await this.item_goal.executeNext(goal.name, goal.quantity); this.last_goals[goal.name] = res; acted = true; break; } } // Build construction goal else { let res = null; if (this.data.built.hasOwnProperty(goal.name)) { res = await this.build_goal.executeNext( this.constructions[goal.name], this.data.built[goal.name].position, this.data.built[goal.name].orientation ); } else { res = await this.build_goal.executeNext(this.constructions[goal.name]); this.data.built[goal.name] = { name: goal.name, position: res.position, orientation: res.orientation }; } if (Object.keys(res.missing).length === 0) { this.data.home = goal.name; } for (let block_name in res.missing) { this.temp_goals.push({ name: block_name, quantity: res.missing[block_name] }) } if (res.acted) { acted = true; this.last_goals[goal.name] = Object.keys(res.missing).length === 0; break; } } } if (!acted && this.data.do_set_goal) await this.setGoal(); } currentBuilding() { let bot_pos = this.agent.bot.entity.position; for (let name in this.data.built) { let pos = this.data.built[name].position; let offset = this.constructions[name].offset; let sizex = this.constructions[name].blocks[0][0].length; let sizez = this.constructions[name].blocks[0].length; let sizey = this.constructions[name].blocks.length; if (this.data.built[name].orientation % 2 === 1) [sizex, sizez] = [sizez, sizex]; if (bot_pos.x >= pos.x && bot_pos.x < pos.x + sizex && bot_pos.y >= pos.y + offset && bot_pos.y < pos.y + sizey + offset && bot_pos.z >= pos.z && bot_pos.z < pos.z + sizez) { return name; } } return null; } getBuildingDoor(name) { if (name === null || this.data.built[name] === undefined) return null; let door_x = null; let door_z = null; let door_y = null; for (let y = 0; y < this.constructions[name].blocks.length; y++) { for (let z = 0; z < this.constructions[name].blocks[y].length; z++) { for (let x = 0; x < this.constructions[name].blocks[y][z].length; x++) { if (this.constructions[name].blocks[y][z][x] !== null && this.constructions[name].blocks[y][z][x].includes('door')) { door_x = x; door_z = z; door_y = y; break; } } if (door_x !== null) break; } if (door_x !== null) break; } if (door_x === null) return null; let sizex = this.constructions[name].blocks[0][0].length; let sizez = this.constructions[name].blocks[0].length; let orientation = 4 - this.data.built[name].orientation; // this conversion is opposite if (orientation == 4) orientation = 0; [door_x, door_z] = rotateXZ(door_x, door_z, orientation, sizex, sizez); door_y += this.constructions[name].offset; return { x: this.data.built[name].position.x + door_x, y: this.data.built[name].position.y + door_y, z: this.data.built[name].position.z + door_z }; } }