'use strict'; const { EventEmitter } = require('events'); const { Vec3 } = require('vec3'); /** 真实的 1.21.4 注册表(mineflayer-pathfinder 的 Movements 依赖它) */ let cachedRegistry = null; function defaultRegistry() { if (!cachedRegistry) { cachedRegistry = require('minecraft-data')('1.21.4'); } return cachedRegistry; } /** * 测试用的假 bot:只实现被测模块真正用到的接口, * 避免测试依赖真实 Minecraft 服务器。 */ class FakeBot extends EventEmitter { constructor(options) { super(); const opts = options || {}; this.username = opts.username || 'TestBot'; this.physicsEnabled = true; this.usingHeldItem = false; this.vehicle = null; this.food = 20; this.health = 20; this.game = { gameMode: opts.gameMode || 'survival' }; this.physics = Object.assign({ gravity: 0.08, playerSpeed: 0.1, sprintSpeed: 0.3, sneakSpeed: 0.3, stepHeight: 0.6, airborneInertia: 0.91, waterInertia: 0.8, yawSpeed: 3.0, pitchSpeed: 3.0 }, opts.physics || {}); this.entity = { position: new Vec3(opts.x || 0, opts.y || 64, opts.z || 0), velocity: new Vec3(0, 0, 0), yaw: 0, pitch: 0, eyeHeight: 1.62, isInWater: false, isInLava: false, onGround: true }; /** @type {Record} */ this.controls = {}; /** * 与 mineflayer 保持一致:用带 getter 的 `controlState` 暴露实时控制键。 * mineflayer 传给物理引擎的就是这个对象,所以诊断代码按它读输入。 */ this.controlState = {}; for (const control of ['forward', 'back', 'left', 'right', 'jump', 'sprint', 'sneak']) { Object.defineProperty(this.controlState, control, { get: () => this.controls[control] === true }); } this.heldItem = opts.heldItem || null; this.inventory = { slots: opts.slots || [] }; this.entities = {}; this.world = opts.world || { raycast: () => null }; this.registry = opts.registry || defaultRegistry(); this.pathfinder = null; this.blockMap = opts.blockMap || new Map(); /** * 模拟「已加载区块范围」:设置后,超出该半径(方块)的位置 blockAt 返回 null, * 用于测试寻路器只能规划已加载范围、需要分段寻路的行为。 * 圆心始终跟随实体当前位置,模拟「人走到哪、区块加载到哪」。 */ this.loadedRadius = opts.loadedRadius === undefined ? null : opts.loadedRadius; this.creative = { flying: false }; /** 版本能力探测:默认支持 1.21.3+ 的 player_input 包 */ this.features = Object.assign({ newPlayerInputPacket: true }, opts.features || {}); /** 假的协议客户端:记录写出的包,便于断言「有没有补发 player_input / tick_end」 */ this._client = opts.client || { state: 'play', written: [], write(name, params) { this.written.push({ name, params }); }, on() {} }; } supportFeature(name) { return this.features[name] === true; } /** 取出写出的指定包 */ writtenPackets(name) { return this._client.written.filter((p) => p.name === name); } clearWritten() { this._client.written.length = 0; } getControlState(control) { return this.controls[control] === true; } setControlState(control, state) { this.controls[control] = state === true; this.emit('controlState', control, state); } blockAt(pos) { const key = `${Math.floor(pos.x)},${Math.floor(pos.y)},${Math.floor(pos.z)}`; if (this.blockMap.has(key)) return this.blockMap.get(key); if (this.loadedRadius !== null) { const center = this.entity.position; if (Math.hypot(pos.x - center.x, pos.z - center.z) > this.loadedRadius) return null; } return { name: 'air', boundingBox: 'empty' }; } look(yaw, pitch) { this.entity.yaw = yaw; this.entity.pitch = pitch === undefined ? this.entity.pitch : pitch; return Promise.resolve(); } lookAt(point) { return this.look(this.entity.yaw, this.entity.pitch); } attack(entity, swing) { this.lastAttack = { entity, swing }; } activateItem() { this.usingHeldItem = true; } deactivateItem() { this.usingHeldItem = false; } async waitForTicks(ticks) { for (let i = 0; i < (ticks || 1); i++) this.emit('physicsTick'); } chat(message) { this.lastChat = message; } equip() { return Promise.resolve(); } consume() { this.usingHeldItem = false; this.food = 20; return Promise.resolve(); } mount() { this.vehicle = { id: 99, position: this.entity.position.clone(), name: 'oak_boat' }; } dismount() { this.vehicle = null; } moveVehicle() { this.lastVehicleMove = Array.from(arguments); } /** 推进一个物理 tick(触发合规层的旋转推进) */ tick() { this.emit('physicsTick'); } /** 推进 n 个物理 tick */ tickTimes(n) { for (let i = 0; i < n; i++) this.emit('physicsTick'); } /** 接受资源包(记录调用,供测试断言) */ acceptResourcePack() { this.resourcePackAccepted = true; } } module.exports = { FakeBot };