mioku-plugin-mc 2.1.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/config.md +157 -3
  2. package/index.ts +61 -9
  3. package/package.json +8 -3
  4. package/play/actions/registry.ts +196 -0
  5. package/play/ai/context-builder.ts +29 -0
  6. package/play/ai/debug-log.ts +135 -0
  7. package/play/ai/main-loop.ts +306 -0
  8. package/play/ai/main-tools.ts +230 -0
  9. package/play/ai/prompt.ts +207 -0
  10. package/play/ai/work-subroutine.ts +487 -0
  11. package/play/behavior/base-behavior.ts +82 -0
  12. package/play/behavior/catalog/approach-player.ts +65 -0
  13. package/play/behavior/catalog/defend.ts +68 -0
  14. package/play/behavior/catalog/explore.ts +72 -0
  15. package/play/behavior/catalog/factory.ts +26 -0
  16. package/play/behavior/catalog/farm-mobs.ts +43 -0
  17. package/play/behavior/catalog/follow.ts +80 -0
  18. package/play/behavior/catalog/gather.ts +135 -0
  19. package/play/behavior/catalog/idle.ts +130 -0
  20. package/play/behavior/catalog/seek-shelter.ts +85 -0
  21. package/play/behavior/engine.ts +243 -0
  22. package/play/behavior/survival/auto-eat.ts +46 -0
  23. package/play/behavior/survival/escape-lava.ts +44 -0
  24. package/play/behavior/survival/escape-water.ts +35 -0
  25. package/play/behavior/survival/flee-creeper.ts +76 -0
  26. package/play/behavior/survival/mlg-fall.ts +56 -0
  27. package/play/bot/bot-controller.ts +276 -0
  28. package/play/bot/play-bus.ts +48 -0
  29. package/play/combat/combat.ts +225 -0
  30. package/play/combat/index.ts +1 -0
  31. package/play/config.ts +39 -0
  32. package/play/context.ts +26 -0
  33. package/play/debug/README.md +74 -0
  34. package/play/debug/commands.ts +289 -0
  35. package/play/index.ts +247 -0
  36. package/play/mineflayer-shims.d.ts +22 -0
  37. package/play/missions/bundles/approach-player.ts +26 -0
  38. package/play/missions/bundles/explore.ts +24 -0
  39. package/play/missions/bundles/farm-mobs.ts +24 -0
  40. package/play/missions/bundles/follow-player.ts +40 -0
  41. package/play/missions/bundles/gather-resource.ts +37 -0
  42. package/play/missions/bundles/idle-wander.ts +24 -0
  43. package/play/missions/bundles/seek-shelter.ts +18 -0
  44. package/play/missions/mission-controller.ts +296 -0
  45. package/play/missions/registry.ts +107 -0
  46. package/play/path-engine/astar.ts +514 -0
  47. package/play/path-engine/goals.ts +84 -0
  48. package/play/path-engine/index.ts +4 -0
  49. package/play/path-engine/movements.ts +67 -0
  50. package/play/path-engine/path-engine.ts +540 -0
  51. package/play/session.ts +486 -0
  52. package/play/state/cooldowns.ts +40 -0
  53. package/play/state/event-journal.ts +72 -0
  54. package/play/state/memory-bus.ts +115 -0
  55. package/play/state/mode.ts +57 -0
  56. package/play/state/sensors/entity-scanner.ts +275 -0
  57. package/play/state/snapshot.ts +360 -0
  58. package/play/types.ts +284 -0
  59. package/play/util/async.ts +11 -0
  60. package/play/util/endpoint.ts +38 -0
  61. package/play/util/entities.ts +135 -0
  62. package/play/util/inventory.ts +75 -0
  63. package/skills/mc.ts +58 -0
@@ -0,0 +1,540 @@
1
+ import type { Bot } from "mineflayer";
2
+ import { Vec3 } from "vec3";
3
+ import { astar, chooseBestTool, estimateDigSeconds, type AStarNode } from "./astar";
4
+ import { DEFAULT_MOVEMENTS, type MovementsConfig } from "./movements";
5
+ import type { PathGoal } from "./goals";
6
+ import { nearestPlayer, nearestHostile } from "../util/entities";
7
+
8
+ interface PendingPromise {
9
+ resolve: () => void;
10
+ reject: (err: Error) => void;
11
+ }
12
+
13
+ const PILLAR_PLACE_DELAY_MS = 300;
14
+ const PLACE_BLOCK_TIMEOUT_MS = 2000;
15
+ const STUCK_DISTANCE_THRESHOLD = 0.15;
16
+ const STUCK_TICK_THRESHOLD = 60;
17
+
18
+ async function placeBlockSafe(
19
+ bot: Bot,
20
+ refBlock: any,
21
+ faceVec: { x: number; y: number; z: number },
22
+ ): Promise<boolean> {
23
+ if (!refBlock) return false;
24
+ return await new Promise<boolean>((resolve) => {
25
+ let settled = false;
26
+ const finish = (ok: boolean) => {
27
+ if (settled) return;
28
+ settled = true;
29
+ resolve(ok);
30
+ };
31
+ const timer = setTimeout(() => {
32
+ try { bot.removeAllListeners("blockUpdate"); } catch { /* ignore */ }
33
+ finish(false);
34
+ }, PLACE_BLOCK_TIMEOUT_MS);
35
+ const onUpdate = (...args: any[]) => {
36
+ const pos = args[0] || args[1];
37
+ if (pos && pos.x === refBlock.position.x && pos.y === refBlock.position.y && pos.z === refBlock.position.z) {
38
+ try { bot.off("blockUpdate", onUpdate); } catch { /* ignore */ }
39
+ clearTimeout(timer);
40
+ finish(true);
41
+ }
42
+ };
43
+ try {
44
+ bot.on("blockUpdate", onUpdate);
45
+ try {
46
+ const lookAt = refBlock.position;
47
+ const target = new Vec3(lookAt.x + 0.5, lookAt.y + 0.5, lookAt.z + 0.5);
48
+ void bot.lookAt(target, true);
49
+ } catch { /* ignore */ }
50
+ void bot.placeBlock(refBlock, new Vec3(faceVec.x, faceVec.y, faceVec.z))
51
+ .then(() => {
52
+ try { bot.off("blockUpdate", onUpdate); } catch { /* ignore */ }
53
+ clearTimeout(timer);
54
+ finish(true);
55
+ })
56
+ .catch(() => {
57
+ try { bot.off("blockUpdate", onUpdate); } catch { /* ignore */ }
58
+ clearTimeout(timer);
59
+ finish(false);
60
+ });
61
+ } catch {
62
+ try { bot.off("blockUpdate", onUpdate); } catch { /* ignore */ }
63
+ clearTimeout(timer);
64
+ finish(false);
65
+ }
66
+ });
67
+ }
68
+
69
+ async function faceBlockBeforeAction(
70
+ bot: Bot,
71
+ block: any,
72
+ height = 0.5,
73
+ ): Promise<void> {
74
+ if (!block?.position) return;
75
+ try {
76
+ const target = new Vec3(
77
+ block.position.x + 0.5,
78
+ block.position.y + height,
79
+ block.position.z + 0.5,
80
+ );
81
+ await bot.lookAt(target, true);
82
+ } catch {
83
+ // ignore
84
+ }
85
+ }
86
+
87
+ export class PathEngine {
88
+ private readonly bot: Bot;
89
+ private goal: PathGoal | null = null;
90
+ private path: AStarNode[] = [];
91
+ private pathIndex = 0;
92
+ private dynamic = false;
93
+ private dynamicTimer: ReturnType<typeof setInterval> | null = null;
94
+ private pending: PendingPromise | null = null;
95
+ private stuckCount = 0;
96
+ private lastStuckPos: { x: number; y: number; z: number } | null = null;
97
+ private computing = false;
98
+ private digging = false;
99
+ private placing = false;
100
+ private lastComputeAt = 0;
101
+ private lastFailAt = 0;
102
+ private lastFailLogAt = 0;
103
+ private currentMovementOriginalCost = 0;
104
+ private ticksOnCurrent = 0;
105
+ private ticksAway = 0;
106
+ private placeTimer: ReturnType<typeof setTimeout> | null = null;
107
+ movements: MovementsConfig = DEFAULT_MOVEMENTS;
108
+ thinkTimeout = 5000;
109
+ tickTimeout = 40;
110
+
111
+ private readonly log: (msg: string) => void;
112
+ private lastDebugAt = 0;
113
+
114
+ constructor(bot: Bot, log?: (msg: string) => void) {
115
+ this.bot = bot;
116
+ this.log = log ?? (() => {});
117
+ bot.on("physicTick", () => this.tick());
118
+ }
119
+
120
+ setGoal(goal: PathGoal | null, dynamic = false): void {
121
+ this.dynamic = dynamic;
122
+ this.goal = goal;
123
+ this.path = [];
124
+ this.pathIndex = 0;
125
+ this.stuckCount = 0;
126
+ this.ticksOnCurrent = 0;
127
+ this.ticksAway = 0;
128
+ this.lastStuckPos = null;
129
+ this.digging = false;
130
+ this.placing = false;
131
+ this.clearDynamicTimer();
132
+
133
+ if (!goal) {
134
+ this.stop();
135
+ return;
136
+ }
137
+
138
+ this.log(`[pathEngine] setGoal dynamic=${dynamic}`);
139
+ this.computePath();
140
+
141
+ if (dynamic) {
142
+ this.dynamicTimer = setInterval(() => {
143
+ if (this.goal && !this.computing && !this.digging && !this.placing) {
144
+ this.computePath();
145
+ }
146
+ }, 2000);
147
+ }
148
+ }
149
+
150
+ goto(goal: PathGoal): Promise<void> {
151
+ return new Promise((resolve, reject) => {
152
+ if (this.pending) {
153
+ this.pending.reject(new Error("replaced"));
154
+ this.pending = null;
155
+ }
156
+ this.pending = { resolve, reject };
157
+ this.setGoal(goal, false);
158
+ });
159
+ }
160
+
161
+ stop(): void {
162
+ this.path = [];
163
+ this.pathIndex = 0;
164
+ this.clearDynamicTimer();
165
+ try { this.bot.clearControlStates(); } catch { /* ignore */ }
166
+ if (this.pending) {
167
+ this.pending.reject(new Error("stopped"));
168
+ this.pending = null;
169
+ }
170
+ }
171
+
172
+ isMoving(): boolean {
173
+ return this.path.length > 0 && this.pathIndex < this.path.length;
174
+ }
175
+
176
+ setMovements(m: MovementsConfig): void {
177
+ this.movements = m;
178
+ }
179
+
180
+ private clearDynamicTimer(): void {
181
+ if (this.dynamicTimer) {
182
+ clearInterval(this.dynamicTimer);
183
+ this.dynamicTimer = null;
184
+ }
185
+ }
186
+
187
+ private computePath(): void {
188
+ if (this.computing || !this.goal) return;
189
+ const now = Date.now();
190
+ if (this.lastFailAt > 0 && now - this.lastFailAt < 5000) return;
191
+ const bot = this.bot;
192
+ const start = bot.entity?.position;
193
+ if (!start) return;
194
+ this.computing = true;
195
+ this.lastComputeAt = now;
196
+ try {
197
+ const result = astar(
198
+ bot,
199
+ { x: start.x, y: start.y, z: start.z },
200
+ this.goal,
201
+ this.movements,
202
+ 1200,
203
+ );
204
+ (this.bot as any).emit("pathEngine_update", result);
205
+ if ((result.status === "success" || result.status === "partial") && result.nodes.length > 0) {
206
+ this.path = result.nodes;
207
+ this.pathIndex = result.nodes.length > 1 ? 1 : 0;
208
+ this.lastFailAt = 0;
209
+ const desc = result.nodes
210
+ .slice(0, Math.min(result.nodes.length, 6))
211
+ .map((n) => {
212
+ const tag = n.jump ? "J" : n.ascend ? "A" : n.descend ? "D" : n.parkour ? "P" : n.pillar ? "Pi" : n.diagonal ? "Di" : "";
213
+ const bk = n.toBreak.length ? "B" : "";
214
+ const pl = n.toPlace.length ? "Pl" : "";
215
+ return `(${n.x},${n.y},${n.z}${tag}${bk}${pl})`;
216
+ })
217
+ .join("->");
218
+ this.log(
219
+ `[pathEngine] path ${result.status} len=${result.nodes.length} from=(${Math.floor(start.x)},${Math.floor(start.y)},${Math.floor(start.z)}) ${desc}`,
220
+ );
221
+ } else {
222
+ this.path = [];
223
+ this.lastFailAt = now;
224
+ if (now - this.lastFailLogAt > 3000) {
225
+ this.lastFailLogAt = now;
226
+ this.log(`[pathEngine] path 失败 status=${result.status} (5s 内不重试)`);
227
+ }
228
+ if (this.pending) {
229
+ this.pending.reject(new Error(`astar:${result.status}`));
230
+ this.pending = null;
231
+ }
232
+ }
233
+ } catch (err) {
234
+ this.log(`[pathEngine] computePath 异常: ${err}`);
235
+ } finally {
236
+ this.computing = false;
237
+ }
238
+ }
239
+
240
+ private tick(): void {
241
+ const bot = this.bot;
242
+ const entity = bot.entity;
243
+ if (!entity || !this.goal) return;
244
+
245
+ const pos = { x: entity.position.x, y: entity.position.y, z: entity.position.z };
246
+
247
+ if (this.goal.isReached(pos)) {
248
+ this.goalReachedAndStop();
249
+ return;
250
+ }
251
+
252
+ if (this.path.length === 1 && this.pathIndex === 0) {
253
+ this.path = [];
254
+ if (!this.computing && !this.digging && !this.placing) this.computePath();
255
+ return;
256
+ }
257
+
258
+ if (!this.path.length || this.pathIndex >= this.path.length) {
259
+ if (!this.computing && !this.digging && !this.placing) this.computePath();
260
+ return;
261
+ }
262
+
263
+ const next = this.path[this.pathIndex];
264
+ if (next.x === pos.x && next.y === pos.y && next.z === pos.z) {
265
+ this.pathIndex++;
266
+ this.ticksOnCurrent = 0;
267
+ return;
268
+ }
269
+
270
+ if (next.toBreak.length > 0 && !this.digging) {
271
+ this.handleDig(next);
272
+ return;
273
+ }
274
+ if (this.digging) return;
275
+
276
+ if (next.toPlace.length > 0 && !this.placing) {
277
+ this.handlePlace(next);
278
+ return;
279
+ }
280
+ if (this.placing) return;
281
+
282
+ this.executeMovement(next, pos);
283
+ }
284
+
285
+ private handleDig(next: AStarNode): void {
286
+ const bot = this.bot;
287
+ const target = next.toBreak[0];
288
+ if (!target) return;
289
+ const block = bot.blockAt(new Vec3(target.x, target.y, target.z), false) as any;
290
+ if (!block || isAirLike(block)) {
291
+ next.toBreak.shift();
292
+ return;
293
+ }
294
+ if (!this.digging) {
295
+ this.digging = true;
296
+ chooseBestTool(bot, block);
297
+ try { bot.clearControlStates(); } catch { /* ignore */ }
298
+ }
299
+ void (async () => {
300
+ try {
301
+ await faceBlockBeforeAction(bot, block, 0.5);
302
+ await bot.dig(block, true);
303
+ this.log(`[pathEngine] 挖完成 ${block.name}`);
304
+ } catch (err) {
305
+ this.log(`[pathEngine] 挖失败 ${block.name}: ${err}`);
306
+ } finally {
307
+ next.toBreak.shift();
308
+ this.digging = false;
309
+ this.ticksOnCurrent = 0;
310
+ }
311
+ })();
312
+ }
313
+
314
+ private handlePlace(next: AStarNode): void {
315
+ const bot = this.bot;
316
+ const target = next.toPlace[0];
317
+ if (!target) return;
318
+ const isPillar = !!next.pillar;
319
+ if (this.placing) return;
320
+ const scaffolding = this.getScaffoldingItem();
321
+ if (!scaffolding) {
322
+ this.log(`[pathEngine] 搭柱/搭桥失败:背包无脚手架`);
323
+ next.toPlace.shift();
324
+ return;
325
+ }
326
+ this.placing = true;
327
+ try { bot.clearControlStates(); } catch { /* ignore */ }
328
+
329
+ if (isPillar) {
330
+ void (async () => {
331
+ try {
332
+ await bot.equip(scaffolding, "hand");
333
+ try { bot.setControlState("sneak", true); } catch { /* ignore */ }
334
+ try { bot.setControlState("jump", true); } catch { /* ignore */ }
335
+ if (this.placeTimer) clearTimeout(this.placeTimer);
336
+ this.placeTimer = setTimeout(async () => {
337
+ try {
338
+ const refBlock = bot.blockAt(new Vec3(target.x, target.y, target.z), false) as any;
339
+ if (refBlock) {
340
+ const ok = await placeBlockSafe(bot, refBlock, { x: target.dx, y: target.dy, z: target.dz });
341
+ if (ok) this.log(`[pathEngine] 搭柱 ${scaffolding.name}`);
342
+ else this.log(`[pathEngine] 搭柱失败(无权限/距离等),跳过`);
343
+ }
344
+ } catch (err) {
345
+ this.log(`[pathEngine] 搭柱异常: ${err}`);
346
+ } finally {
347
+ try { bot.setControlState("jump", false); } catch { /* ignore */ }
348
+ try { bot.setControlState("sneak", false); } catch { /* ignore */ }
349
+ next.toPlace.shift();
350
+ this.placing = false;
351
+ this.ticksOnCurrent = 0;
352
+ }
353
+ }, PILLAR_PLACE_DELAY_MS);
354
+ } catch (err) {
355
+ this.log(`[pathEngine] 搭柱准备失败: ${err}`);
356
+ try { bot.setControlState("jump", false); } catch { /* ignore */ }
357
+ try { bot.setControlState("sneak", false); } catch { /* ignore */ }
358
+ next.toPlace.shift();
359
+ this.placing = false;
360
+ }
361
+ })();
362
+ } else {
363
+ void (async () => {
364
+ try {
365
+ await bot.equip(scaffolding, "hand");
366
+ const refBlock = bot.blockAt(new Vec3(target.x, target.y, target.z), false) as any;
367
+ if (refBlock) {
368
+ try { bot.setControlState("sneak", true); } catch { /* ignore */ }
369
+ const ok = await placeBlockSafe(bot, refBlock, { x: target.dx, y: target.dy, z: target.dz });
370
+ if (ok) this.log(`[pathEngine] 搭桥 ${scaffolding.name}`);
371
+ else this.log(`[pathEngine] 搭桥失败(无权限/距离等),跳过`);
372
+ }
373
+ } catch (err) {
374
+ this.log(`[pathEngine] 搭桥异常: ${err}`);
375
+ } finally {
376
+ try { bot.setControlState("sneak", false); } catch { /* ignore */ }
377
+ next.toPlace.shift();
378
+ this.placing = false;
379
+ this.ticksOnCurrent = 0;
380
+ }
381
+ })();
382
+ }
383
+ }
384
+
385
+ private getScaffoldingItem(): any | null {
386
+ const items = this.bot.inventory?.items?.() ?? [];
387
+ return (
388
+ items.find((i: any) => i.name === "dirt") ??
389
+ items.find((i: any) => i.name === "cobblestone") ??
390
+ items.find((i: any) => i.name === "netherrack") ??
391
+ items.find((i: any) => /_planks$/.test(i.name)) ??
392
+ items.find((i: any) => /_log$/.test(i.name)) ??
393
+ null
394
+ );
395
+ }
396
+
397
+ private executeMovement(next: AStarNode, pos: { x: number; y: number; z: number }): void {
398
+ const bot = this.bot;
399
+ const entity = bot.entity as any;
400
+ const nextCenter = { x: next.x + 0.5, y: next.y, z: next.z + 0.5 };
401
+ const dx = nextCenter.x - pos.x;
402
+ const dy = nextCenter.y - pos.y;
403
+ const dz = nextCenter.z - pos.z;
404
+ const horizDist = Math.sqrt(dx * dx + dz * dz);
405
+
406
+ if (this.ticksOnCurrent === 0) {
407
+ this.currentMovementOriginalCost = this.estimateCost(next);
408
+ }
409
+ this.ticksOnCurrent++;
410
+
411
+ const lookTarget = this.pickLookTarget(nextCenter, dx, dz, pos);
412
+ if (lookTarget) {
413
+ try { bot.look(Math.atan2(-lookTarget.dx, -lookTarget.dz), 0, true); } catch { /* ignore */ }
414
+ } else if (Math.abs(dx) > 0.01 || Math.abs(dz) > 0.01) {
415
+ try { bot.look(Math.atan2(-dx, -dz), 0, true); } catch { /* ignore */ }
416
+ }
417
+
418
+ const wantJump = this.shouldJump(next, pos, dx, dy, dz, horizDist);
419
+
420
+ try {
421
+ bot.setControlState("forward", true);
422
+ bot.setControlState("sprint", this.movements.allowSprinting);
423
+ bot.setControlState("jump", wantJump);
424
+ } catch { /* ignore */ }
425
+
426
+ if (this.arrivedAt(nextCenter, pos)) {
427
+ this.pathIndex++;
428
+ this.ticksOnCurrent = 0;
429
+ }
430
+
431
+ this.checkStuck(pos);
432
+ }
433
+
434
+ private pickLookTarget(
435
+ nextCenter: { x: number; y: number; z: number },
436
+ dx: number,
437
+ dz: number,
438
+ pos: { x: number; y: number; z: number },
439
+ ): { dx: number; dz: number } | null {
440
+ const moveYaw = Math.atan2(-dx, -dz);
441
+ try {
442
+ const bot = this.bot;
443
+ const player = nearestPlayer(bot, 8);
444
+ if (player?.position) {
445
+ const ddx = player.position.x - pos.x;
446
+ const ddz = player.position.z - pos.z;
447
+ const entityYaw = Math.atan2(-ddx, -ddz);
448
+ if (this.yawDiff(moveYaw, entityYaw) < Math.PI / 3) {
449
+ return { dx: ddx, dz: ddz };
450
+ }
451
+ }
452
+ const hostile = nearestHostile(bot, 8);
453
+ if (hostile?.position) {
454
+ const ddx = hostile.position.x - pos.x;
455
+ const ddz = hostile.position.z - pos.z;
456
+ const entityYaw = Math.atan2(-ddx, -ddz);
457
+ if (this.yawDiff(moveYaw, entityYaw) < Math.PI / 3) {
458
+ return { dx: ddx, dz: ddz };
459
+ }
460
+ }
461
+ } catch { /* ignore */ }
462
+ void nextCenter;
463
+ return null;
464
+ }
465
+
466
+ private yawDiff(a: number, b: number): number {
467
+ let d = a - b;
468
+ while (d > Math.PI) d -= 2 * Math.PI;
469
+ while (d < -Math.PI) d += 2 * Math.PI;
470
+ return Math.abs(d);
471
+ }
472
+
473
+ private shouldJump(
474
+ next: AStarNode,
475
+ pos: { x: number; y: number; z: number },
476
+ dx: number, dy: number, dz: number,
477
+ horizDist: number,
478
+ ): boolean {
479
+ const bot = this.bot;
480
+ if (next.ascend || next.parkour || next.pillar) return true;
481
+ if (dy > 0.3) return true;
482
+ const entity = bot.entity as any;
483
+ if (entity?.isInWater) return true;
484
+ if (horizDist > 1.2) return false;
485
+ if (horizDist < 0.05) return false;
486
+ if (Math.abs(dx) > 0.1 || Math.abs(dz) > 0.1) return true;
487
+ return false;
488
+ }
489
+
490
+ private arrivedAt(target: { x: number; y: number; z: number }, pos: { x: number; y: number; z: number }): boolean {
491
+ return (
492
+ Math.abs(pos.x - target.x) < 0.45 &&
493
+ Math.abs(pos.z - target.z) < 0.45 &&
494
+ Math.abs(pos.y - target.y) < 1.2
495
+ );
496
+ }
497
+
498
+ private estimateCost(node: AStarNode): number {
499
+ if (node.ascend || node.parkour || node.pillar) return 40;
500
+ if (node.descend) return 25;
501
+ return 20;
502
+ }
503
+
504
+ private checkStuck(pos: { x: number; y: number; z: number }): void {
505
+ if (this.lastStuckPos) {
506
+ const moved = Math.sqrt(
507
+ (pos.x - this.lastStuckPos.x) ** 2 +
508
+ (pos.y - this.lastStuckPos.y) ** 2 +
509
+ (pos.z - this.lastStuckPos.z) ** 2,
510
+ );
511
+ if (moved < STUCK_DISTANCE_THRESHOLD) {
512
+ this.stuckCount++;
513
+ if (this.stuckCount > STUCK_TICK_THRESHOLD) {
514
+ this.log(`[pathEngine] 卡住,重算`);
515
+ this.computePath();
516
+ this.stuckCount = 0;
517
+ }
518
+ } else {
519
+ this.stuckCount = 0;
520
+ }
521
+ }
522
+ this.lastStuckPos = { x: pos.x, y: pos.y, z: pos.z };
523
+ }
524
+
525
+ private goalReachedAndStop(): void {
526
+ this.path = [];
527
+ this.pathIndex = 0;
528
+ this.clearDynamicTimer();
529
+ try { this.bot.clearControlStates(); } catch { /* ignore */ }
530
+ (this.bot as any).emit("goal_reached");
531
+ if (this.pending) {
532
+ this.pending.resolve();
533
+ this.pending = null;
534
+ }
535
+ }
536
+ }
537
+
538
+ function isAirLike(b: any): boolean {
539
+ return !b || b.boundingBox === "empty";
540
+ }