The goal of this lesson is to upgrade the bot to protect a player by attacking hostile mobs and navigating to coordinates.
In Tutorial 1, our bot could only do one thing — follow a player. Now we're going to make it useful. By the end of this tutorial, the bot will:
This introduces an important concept: state management. The bot now has multiple behaviors and needs to decide what to do based on the current situation.
Same setup as Tutorial 1 — same dependencies:
mkdir bot-protect-a-player-from-hostile
cd bot-protect-a-player-from-hostile
npm init -y
npm install mineflayer mineflayer-pathfinder yargs
npm install -D typescript @types/node @types/yargs tsx
Use the same tsconfig.json and package.json scripts from Tutorial 1. Create src/index.ts with the same imports and CLI argument parsing. One new thing — we derive a hostileRange from the player range:
const playerRange = argv.range;
const hostileRange = argv.range * 1.5;
The hostile detection range is 1.5x the follow range. This means the bot starts attacking mobs before they reach the player — it acts like a perimeter guard.
Our bot now has multiple modes of operation, so we need state flags:
let needsPlayerCoords = false;
let isNavigatingToCoords = false;
needsPlayerCoords — true when the bot can't find the player and has already asked for coordinates. This prevents the bot from spamming "where are you?" every 500ms.isNavigatingToCoords — true when the bot is walking toward coordinates the player sent. While navigating, it skips the normal "find and follow player" loop.This is a simple state machine with two flags. In more complex bots you might use an enum or a proper state machine library, but for two states this works fine.
The main loop runs on spawn, just like Tutorial 1, but now it does more:
bot.on("spawn", () => {
bot.loadPlugin(pathfinder);
bot.pathfinder.setMovements(new Movements(bot));
bot.chat(`Hey, ${playerName}, I am here!`);
setInterval(() => {
// Skip if we're navigating to coordinates
if (isNavigatingToCoords) return;
const player = bot.nearestEntity((e) => e.type === "player" && e.username === playerName);
if (player) {
needsPlayerCoords = false;
const { x, y, z } = player.position;
bot.pathfinder.setGoal(new GoalNear(x, y, z, playerRange));
// Check for hostile entities and attack them
bot.nearestEntity((e) => {
if (e.type !== "hostile") return false;
if (bot.entity.position.distanceTo(e.position) <= hostileRange) {
bot.lookAt(e.position, true).then(() => {
bot.attack(e);
});
}
return true;
});
} else if (!needsPlayerCoords) {
needsPlayerCoords = true;
bot.chat(`${playerName}, I can't find you! Please share your coordinates.`);
}
}, interval);
});
Let's break down what's new compared to Tutorial 1:
if (isNavigatingToCoords) return — if the player sent coordinates and the bot is walking there, don't override the goal by trying to find the player visually.hostileRange, the bot looks at the mob (for aiming) and then attacks.bot.lookAt(e.position, true) — the true parameter forces the bot's head to snap to the target. The bot needs to face a mob to attack it. This returns a Promise, so we chain .then(() => bot.attack(e)).distanceTo() — mineflayer provides euclidean distance calculation on position vectors. We use it to check if a hostile is close enough to engage.needsPlayerCoords flag ensures we only ask once.We need a helper to understand coordinates in different formats, since players might type them differently:
function parseCoordinates(message: string) {
const coordPatterns = [
/(-?\d+)\s+(-?\d+)\s+(-?\d+)/, // "100 64 -200"
/(-?\d+),\s*(-?\d+),\s*(-?\d+)/, // "100,64,-200" or "100, 64, -200"
/(-?\d+)\/(-?\d+)\/(-?\d+)/, // "100/64/-200"
];
for (const pattern of coordPatterns) {
const match = message.match(pattern);
if (match) {
const [_, x, y, z] = match;
return { x: parseInt(x), y: parseInt(y), z: parseInt(z) };
}
}
return null;
}
Each regex captures three groups of digits (including negative numbers with -?). We try each pattern in order and return the first match. If nothing matches, we return null.
This is a small but important UX detail — the player shouldn't have to remember a specific format. Minecraft's F3 screen shows coordinates with slashes, the chat shows them with spaces, and some people naturally use commas.
Now we listen for chat messages and navigate when the player sends coordinates:
bot.on("chat", (username, message) => {
if (username === playerName) {
const coords = parseCoordinates(message);
if (coords) {
const { x, y, z } = coords;
bot.chat(`Thanks! I'm coming to coordinates: ${x}, ${y}, ${z}`);
isNavigatingToCoords = true;
needsPlayerCoords = false;
bot.pathfinder.setGoal(new GoalNear(x, y, z, playerRange));
// Still attack hostiles while navigating
bot.nearestEntity((e) => {
if (e.type !== "hostile") return false;
bot.attack(e);
return true;
});
// Check progress and reset when we arrive
const checkInterval = setInterval(() => {
const botPos = bot.entity.position;
if (isNavigatingToCoords) {
bot.chat(
`Current position: ${Math.floor(botPos.x)}, ${Math.floor(botPos.y)}, ${Math.floor(botPos.z)}`,
);
}
if (bot.entity.position.distanceTo({ x, y, z }) <= playerRange) {
isNavigatingToCoords = false;
clearInterval(checkInterval);
bot.chat("I have reached the target location!");
}
}, 4000);
}
}
});
Here's the flow:
parseCoordinates returns null and we ignore it.isNavigatingToCoords to true so the main loop stops trying to visually find the player.playerRange of the target, clear the interval and reset isNavigatingToCoords. The main loop takes over again and resumes normal follow/protect behavior.cd examples/bot-protect-a-player-from-hostile
npm install
npx tsx src/index.ts -- --player <your-minecraft-username>
The bot will follow you and attack any hostile mobs that get close. If it loses sight of you, send your coordinates in chat (e.g. 100 64 -200) and it'll navigate to you.
At this point we have a bot with hardcoded behaviors — it always follows, always attacks hostiles, and responds to a fixed set of chat patterns. In Tutorial 3, we'll break these behaviors out into MCP tools so that Claude can decide when and how to use them. Instead of rigid if/else logic, an LLM will be making the decisions.