'use strict'; const Vec3 = require('vec3'); const { goals: { GoalNear } } = require('mineflayer-pathfinder'); const { sleep } = require('../../utils'); class Navigation { constructor(args) { this.bot = args.bot; this.commands = [ { name: 'goto', description: 'Move the bot to coordinates or a player position', parameters: [ { name: 'target', type: 'string', required: true, description: 'Coordinates "x y z" or player name' }, { name: 'range', type: 'number', required: false, description: 'Stop distance from target (default: 0)' } ], category: 'movement' }, { name: 'come', description: 'Come to the requesting player with recovery on failure', parameters: [ { name: 'player', type: 'string', required: false, description: 'Player to come to (defaults to requester)' } ], category: 'movement' }, { name: 'follow', description: 'Go to a player and maintain distance', parameters: [ { name: 'target', type: 'string', required: true, description: 'Player to follow' }, { name: 'range', type: 'number', required: false, description: 'Follow distance (default: 3)' } ], category: 'movement' }, { name: 'stop', description: 'Stop current pathfinding', parameters: [], category: 'movement' } ]; } async init() {} async unload() { try { this.bot.bot.pathfinder.stop(); } catch (e) { /* ignore */ } return true; } async handleCommand(from, command, ...args) { switch (command) { case 'goto': { const [target, rangeStr] = args; const range = parseFloat(rangeStr) || 0; if (!target) return 'Specify coordinates "x y z" or a player name'; // Parse coordinates const parts = target.split(' '); if (parts.length >= 3) { const coords = parts.map(Number); if (coords.every(c => !isNaN(c))) { return this._goWithRecovery(new Vec3(coords[0], coords[1], coords[2]), range); } } // Player target const player = this.bot.bot.players[target]; if (player && player.entity) { return this._goWithRecovery(player.entity.position, range); } return `Target not found: ${target}`; } case 'come': { const [playerName] = args; const target = playerName || from; const player = this.bot.bot.players[target]; if (!player || !player.entity) return `Cannot find ${target}`; return this._goWithRecovery(player.entity.position, 3); } case 'follow': { const [target, rangeStr] = args; const range = parseFloat(rangeStr) || 3; const player = this.bot.bot.players[target]; if (!player || !player.entity) return `Cannot find ${target}`; return this._goWithRecovery(player.entity.position, range); } case 'stop': try { this.bot.bot.pathfinder.stop(); } catch (e) { /* ignore */ } return 'Movement stopped'; default: return `Unknown navigation command: ${command}`; } } // Smart movement with recovery — retries with 360 scan and waypoint unstick when goTo fails async _goWithRecovery(targetPos, range) { let attempts = 0; while (attempts < 3) { const ok = await this.bot.goTo({ where: targetPos, range }); if (ok) return `Arrived at destination`; attempts++; console.log(`[Navigation] goTo failed (attempt ${attempts}/3), applying recovery...`); // 360 visual refresh const bot = this.bot.bot; for (let i = 0; i < 4; i++) { await bot.look(bot.entity.yaw + Math.PI / 2, 0, true); await sleep(150); } // Perpendicular waypoint to unstick const pos = bot.entity.position; if (!isNaN(pos.x) && !isNaN(pos.z)) { const yaw = Math.atan2(targetPos.z - pos.z, targetPos.x - pos.x); const perps = [yaw + Math.PI / 2, yaw - Math.PI / 2, yaw + Math.PI]; for (const p of perps) { const wx = pos.x + Math.cos(p) * 3; const wz = pos.z + Math.sin(p) * 3; try { await bot.pathfinder.goto( new GoalNear(wx, pos.y, wz, 1) ); await sleep(1000); bot.clearControlStates(); break; } catch (e) { /* try next */ } } // Backward nudge bot.setControlState('back', true); await sleep(300); bot.clearControlStates(); await sleep(200); } } return 'Failed to reach destination after recovery attempts'; } } module.exports = Navigation;