Files
2026-10-22 17:07:08 +08:00

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