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>; 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; }>(); 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 { 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 { 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(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); } }