166 lines
8.8 KiB
TypeScript
166 lines
8.8 KiB
TypeScript
import type {Bot} from '@wp2508/mineflayer';
|
|
import type {FunctionArgument} from '../../../types/bot/function/function.ts';
|
|
import {on_function_cancel} from '../cancellation.ts';
|
|
|
|
type Entity = NonNullable<ReturnType<Bot['entityAtCursor']>>;
|
|
export type EntityOperation = 'attack' | 'item' | 'open' | 'empty_hand';
|
|
export type CursorArgs = [reach: number, ignore_blocks: boolean, need_look?: boolean];
|
|
export type NearestArgs = [name: string, reach: number, lock_target: boolean, need_look?: boolean];
|
|
export type CursorItemArgs = [reach: number, ignore_blocks: boolean, off_hand: boolean, need_look?: boolean];
|
|
export type NearestItemArgs = [name: string, reach: number, off_hand: boolean, lock_target: boolean, need_look?: boolean];
|
|
|
|
const target_locks = new WeakMap<Bot, {
|
|
self: Bot['entity']; client: Bot['_client']; targets: Map<string, Entity>;
|
|
}>();
|
|
|
|
export function entity_arguments(nearest: boolean, item: boolean, attack = false): FunctionArgument[] {
|
|
const arg = (name: string, type: FunctionArgument['type'], label: string, description: string): FunctionArgument =>
|
|
({name, type, i18n: {lang: 'zh-CN', name: label, description}});
|
|
const reach = arg(nearest && !attack ? 'max_distance' : 'reach', 'number', '最大距离', '眼睛到实体碰撞箱的最大距离,必须为有限正数');
|
|
const args = nearest
|
|
? [arg('name', 'string', '实体类型 ID 或用户名', '不区分大小写的子串匹配,如 zom 或 minecraft:zom;也支持玩家用户名'), reach]
|
|
: [reach, arg('ignore_blocks', 'boolean', '忽略方块遮挡', '是否忽略光标射线上的方块遮挡')];
|
|
if (item) args.push(arg('off_hand', 'boolean', '使用副手', 'false 使用主手,true 使用副手,所选手必须持有物品'));
|
|
if (nearest) args.push(arg('lock_target', 'boolean', '持续锁定实体', '相同函数和参数持续操作首次选中的实体,直到实体消失;超距时保留锁定但不操作'));
|
|
args.push({...arg('need_look', 'boolean', '瞄准实体', '是否先瞄准实体中心,默认 false'), default: false});
|
|
return args;
|
|
}
|
|
|
|
function valid_entity(bot: Bot, target: Entity): boolean {
|
|
return !!target && target !== bot.entity && target.id !== bot.entity?.id
|
|
&& !(bot.username && target.username === bot.username)
|
|
&& target.isValid !== false && bot.entities?.[target.id] === target
|
|
&& Number.isFinite(target.id) && !!target.position
|
|
&& [target.position.x, target.position.y, target.position.z, target.width, target.height].every(Number.isFinite)
|
|
&& target.width > 0 && target.height > 0;
|
|
}
|
|
|
|
function distance(bot: Bot, target: Entity): number {
|
|
const self = bot.entity;
|
|
const eye_height = (self as Entity & {eyeHeight?: number})?.eyeHeight ?? self?.height;
|
|
if (!self?.position || ![self.position.x, self.position.y, self.position.z, eye_height].every(Number.isFinite)) return Infinity;
|
|
const half = target.width / 2;
|
|
const dx = Math.max(Math.abs(self.position.x - target.position.x) - half, 0);
|
|
const dy = Math.max(target.position.y - (self.position.y + eye_height), self.position.y + eye_height - (target.position.y + target.height), 0);
|
|
const dz = Math.max(Math.abs(self.position.z - target.position.z) - half, 0);
|
|
return Math.hypot(dx, dy, dz);
|
|
}
|
|
|
|
function has_item(bot: Bot, off_hand: boolean): boolean {
|
|
return off_hand ? !bot.supportFeature('doesntHaveOffHandSlot') && !!bot.inventory?.slots[45] : !!bot.heldItem;
|
|
}
|
|
|
|
function open_container(bot: Bot, target: Entity, active: () => boolean): Promise<boolean> {
|
|
if (bot.currentWindow || !active()) return Promise.resolve(false);
|
|
return new Promise(resolve => {
|
|
let settled = false;
|
|
const finish = (success: boolean) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
bot.removeListener('windowOpen', on_open);
|
|
bot.removeListener('end', on_end);
|
|
unwatch();
|
|
resolve(success);
|
|
};
|
|
const on_open = (window: Bot['currentWindow']) => {
|
|
finish(!!window && active() && bot.currentWindow === window);
|
|
};
|
|
const on_end = () => finish(false);
|
|
const timer = setTimeout(() => finish(false), 5000);
|
|
const unwatch = on_function_cancel(bot, on_end);
|
|
bot.on('windowOpen', on_open);
|
|
bot.on('end', on_end);
|
|
try {
|
|
Promise.resolve(bot.interactEntity(target, {hand: 0})).catch(() => finish(false));
|
|
} catch {
|
|
finish(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function run_entity_operation(
|
|
bot: Bot, args: unknown[], nearest: boolean, operation: EntityOperation, function_name: string = operation,
|
|
): Promise<boolean> {
|
|
const item = operation === 'item';
|
|
const required_count = (item ? 3 : 2) + (nearest ? 1 : 0);
|
|
if (!Array.isArray(args) || args.length !== required_count && args.length !== required_count + 1) return false;
|
|
if (args.length > required_count && typeof args[required_count] !== 'boolean') return false;
|
|
const need_look = args[required_count] === true;
|
|
args = args.slice(0, required_count);
|
|
const reach = args[nearest ? 1 : 0];
|
|
if (typeof reach !== 'number' || !Number.isFinite(reach) || reach <= 0) return false;
|
|
if (nearest ? typeof args[0] !== 'string' || !args[0].trim() : typeof args[1] !== 'boolean') return false;
|
|
if (item && typeof args[2] !== 'boolean') return false;
|
|
if (nearest && typeof args.at(-1) !== 'boolean') return false;
|
|
const self = bot.entity;
|
|
const client = bot._client;
|
|
let ended = false;
|
|
let cancel!: () => void;
|
|
const cancelled = new Promise<false>(resolve => { cancel = () => resolve(false); });
|
|
const on_end = () => { ended = true; cancel(); };
|
|
const unwatch = on_function_cancel(bot, on_end);
|
|
const active = () => !ended && !!self && bot.entity === self && bot._client === client && self.isValid !== false;
|
|
try {
|
|
bot.on('end', on_end);
|
|
if (operation === 'empty_hand' && bot.heldItem) return false;
|
|
if (!active() || operation === 'open' && bot.currentWindow || item && !has_item(bot, args[2] as boolean)) return false;
|
|
let target: Entity | undefined;
|
|
if (nearest) {
|
|
let locks = target_locks.get(bot);
|
|
if (!locks || locks.self !== self || locks.client !== client) {
|
|
locks = {self, client, targets: new Map()};
|
|
target_locks.set(bot, locks);
|
|
}
|
|
for (const [key, entity] of locks.targets) {
|
|
if (!valid_entity(bot, entity)) locks.targets.delete(key);
|
|
}
|
|
const lock_target = args.at(-1) === true;
|
|
const key = JSON.stringify([function_name, [...args, need_look]]);
|
|
target = lock_target ? locks.targets.get(key) : undefined;
|
|
if (!target) {
|
|
const name = (args[0] as string).trim().toLowerCase();
|
|
const entity_name = name.replace(/^minecraft:/, '');
|
|
target = Object.values(bot.entities).filter(entity => valid_entity(bot, entity)
|
|
&& (entity_name.length > 0 && typeof entity.name === 'string'
|
|
&& entity.name.toLowerCase().replace(/^minecraft:/, '').includes(entity_name)
|
|
|| typeof entity.username === 'string' && entity.username.toLowerCase().includes(name)))
|
|
.map(entity => ({entity, distance: distance(bot, entity)}))
|
|
.filter(entry => Number.isFinite(entry.distance) && entry.distance <= reach)
|
|
.sort((a, b) => a.distance - b.distance)[0]?.entity;
|
|
if (lock_target && target) locks.targets.set(key, target);
|
|
}
|
|
} else {
|
|
target = bot.entityAtCursor(reach, true, args[1] as boolean) ?? undefined;
|
|
}
|
|
const in_reach = () => active() && valid_entity(bot, target!) && distance(bot, target!) <= reach;
|
|
if (!target || !in_reach()) return false;
|
|
if (need_look) {
|
|
if (!await Promise.race([
|
|
bot.lookAt(target.position.offset(0, target.height / 2, 0)).then(() => true, () => false), cancelled,
|
|
])) return false;
|
|
if (!in_reach()) return false;
|
|
}
|
|
if (operation === 'attack') {
|
|
bot.attack(target);
|
|
return true;
|
|
}
|
|
if (operation === 'empty_hand') {
|
|
if (bot.heldItem) return false;
|
|
bot.interactEntity(target, {hand: 0});
|
|
return true;
|
|
}
|
|
if (item) {
|
|
if (!has_item(bot, args[2] as boolean)) return false;
|
|
bot.interactEntity(target, {hand: args[2] ? 1 : 0, position: target.position.offset(0, target.height / 2, 0)});
|
|
return true;
|
|
}
|
|
return await open_container(bot, target, active);
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
unwatch();
|
|
bot.removeListener('end', on_end);
|
|
}
|
|
}
|