UNPKG

mineflayer-pvp

Version:

Adds support for basic PVP and PVE to Mineflayer bots.

244 lines (243 loc) 8.69 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PVP = void 0; const mineflayer_pathfinder_1 = require("mineflayer-pathfinder"); const TimingSolver_1 = require("./TimingSolver"); const mineflayer_utils_1 = require("mineflayer-utils"); /** * The main pvp manager plugin class. */ class PVP { /** * Creates a new instance of the PVP plugin. * * @param bot - The bot this plugin is being attached to. */ constructor(bot) { this.timeToNextAttack = 0; this.wasInRange = false; this.blockingExplosion = false; /** * How close the bot will attempt to get to the target when when pursuing it. */ this.followRange = 2; /** * How far away the target entity must be to lose the target. Target entities further than this * distance from the bot will be considered defeated. */ this.viewDistance = 128; /** * How close must the bot be to the target in order to try attacking it. */ this.attackRange = 3.5; /** * The timing solver to use when deciding how long to wait before preforming another attack * after finishing an attack. * * // TODO Check for 'hasAtttackCooldown' feature. If feature not present, default to RandomTicks solver. */ this.meleeAttackRate = new TimingSolver_1.MaxDamageOffset(); this.bot = bot; this.movements = new mineflayer_pathfinder_1.Movements(bot, require('minecraft-data')(bot.version)); this.bot.on('physicTick', () => this.update()); this.bot.on('entityGone', e => { if (e === this.target) this.stop(); }); } /** * Causes the bot to begin attacking an entity until it is killed or told to stop. * * @param target - The target to attack. */ attack(target) { return __awaiter(this, void 0, void 0, function* () { if (target === this.target) return; yield this.stop(); this.target = target; this.timeToNextAttack = 0; if (!this.target) return; const pathfinder = this.bot.pathfinder; if (this.movements) pathfinder.setMovements(this.movements); pathfinder.setGoal(new mineflayer_pathfinder_1.goals.GoalFollow(this.target, this.followRange), true); // @ts-expect-error this.bot.emit('startedAttacking'); }); } /** * Stops attacking the current entity. */ stop() { return __awaiter(this, void 0, void 0, function* () { if (this.target == null) return; this.target = undefined; const pathfinder = this.bot.pathfinder; pathfinder.stop(); try { yield this.onceWithTimeout('path_stop', 5000); } catch (err) { this.bot.removeAllListeners('path_stop'); pathfinder.setGoal(null); } // @ts-expect-error this.bot.emit('stoppedAttacking'); }); } /** * Resolve if event fires within the timeout. Rejects if the event did not fire within the timeout. * @param eventName Event name to listen to * @param timeout Timeout in ms * @returns {Promise<void>} */ onceWithTimeout(eventName, timeout) { return __awaiter(this, void 0, void 0, function* () { let callback = () => { }; let timeoutId; const cleanup = () => { clearTimeout(timeoutId); this.bot.removeListener(eventName, callback); }; return new Promise((resolve, reject) => { callback = () => { cleanup(); resolve(); }; this.bot.once(eventName, callback); timeoutId = setTimeout(() => { cleanup(); reject(); }, timeout); }); }); } /** * Stops attacking the current entity. Force stops pathfinder. May result in the bot falling off of things or failing jumps. * @returns void */ forceStop() { if (this.target == null) return; this.target = undefined; const pathfinder = this.bot.pathfinder; pathfinder.setGoal(null); // @ts-expect-error this.bot.emit('stoppedAttacking'); } /** * Called each tick to update attack timers. */ update() { this.checkExplosion(); this.checkRange(); if (!this.target || this.blockingExplosion) return; this.timeToNextAttack--; if (this.timeToNextAttack === -1) this.attemptAttack(); } /** * Updates whether the bot is in attack range of the target or not. */ checkRange() { if (!this.target) return; if (this.timeToNextAttack < 0) return; const dist = this.target.position.distanceTo(this.bot.entity.position); if (dist > this.viewDistance) { this.stop(); return; } const inRange = dist <= this.attackRange; if (!this.wasInRange && inRange) this.timeToNextAttack = 0; this.wasInRange = inRange; } /** * Blocks a creeper explosion with a shield. */ checkExplosion() { if (!this.target || !this.hasShield()) return; if (this.target.name && this.target.name === 'creeper' && this.target.metadata[16] && // @ts-ignore this.target.metadata[16] === 1) { this.blockingExplosion = true; this.bot.pathfinder.stop(); this.bot.lookAt(this.target.position.offset(0, 1, 0), true); this.bot.activateItem(true); setTimeout(() => { this.blockingExplosion = false; }, 2000); } } /** * Attempts to preform an attack on the target. */ attemptAttack() { if (!this.target) return; if (!this.wasInRange) { this.timeToNextAttack = this.meleeAttackRate.getTicks(this.bot); return; } const queue = new mineflayer_utils_1.TaskQueue(); const target = this.target; const shield = this.hasShield(); if (shield) { queue.addSync(() => this.bot.deactivateItem()); queue.add(cb => setTimeout(cb, 100)); } queue.add(cb => { if (target !== this.target) throw 'Target changed!'; this.bot.lookAt(this.target.position.offset(0, this.target.height, 0), true).then(() => cb()).catch(err => cb(err)); }); queue.addSync(() => { if (target !== this.target) throw 'Target changed!'; this.bot.attack(this.target); // @ts-expect-error this.bot.emit('attackedTarget'); }); if (shield) { queue.add(cb => setTimeout(cb, 150)); queue.addSync(() => { if (target !== this.target) throw 'Target changed!'; if (this.hasShield()) this.bot.activateItem(true); }); } queue.runAll((err) => { if (!err) this.timeToNextAttack = this.meleeAttackRate.getTicks(this.bot); }); } /** * Check if the bot currently has a shield equipped. */ hasShield() { if (this.bot.supportFeature('doesntHaveOffHandSlot')) return false; const slot = this.bot.inventory.slots[this.bot.getEquipmentDestSlot('off-hand')]; if (!slot) return false; return slot.name.includes('shield'); } } exports.PVP = PVP;