mioku-plugin-mc 2.0.0 → 3.0.0

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 (64) hide show
  1. package/config.md +157 -3
  2. package/index.ts +38 -2
  3. package/package.json +26 -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/runtime.ts +14 -0
  52. package/play/session.ts +486 -0
  53. package/play/state/cooldowns.ts +40 -0
  54. package/play/state/event-journal.ts +72 -0
  55. package/play/state/memory-bus.ts +115 -0
  56. package/play/state/mode.ts +57 -0
  57. package/play/state/sensors/entity-scanner.ts +275 -0
  58. package/play/state/snapshot.ts +360 -0
  59. package/play/types.ts +284 -0
  60. package/play/util/async.ts +11 -0
  61. package/play/util/endpoint.ts +38 -0
  62. package/play/util/entities.ts +135 -0
  63. package/play/util/inventory.ts +75 -0
  64. package/skills.ts +63 -0
@@ -0,0 +1,82 @@
1
+ import type { Bot } from "mineflayer";
2
+ import type { MovementsConfig } from "../path-engine";
3
+
4
+ export interface BehaviorContext {
5
+ bot: Bot;
6
+ movements: MovementsConfig;
7
+ log: (msg: string) => void;
8
+ }
9
+
10
+ export interface BehaviorMissionReporter {
11
+ progress(value: unknown): void;
12
+ succeed(detail?: string, progress?: unknown): void;
13
+ fail(code: import("../state/mode").MissionErrorCode, detail: string, progress?: unknown): void;
14
+ block(code: import("../state/mode").MissionErrorCode, detail: string, progress?: unknown): void;
15
+ isCurrent(): boolean;
16
+ }
17
+
18
+ export type BehaviorCategory = "survival" | "combat" | "maintenance" | "movement";
19
+
20
+ export const CATEGORY_PRIORITY: Record<BehaviorCategory, number> = {
21
+ survival: 100,
22
+ combat: 70,
23
+ maintenance: 60,
24
+ movement: 50,
25
+ };
26
+
27
+ export abstract class Behavior {
28
+ abstract readonly name: string;
29
+ abstract readonly category: BehaviorCategory;
30
+ readonly priorityOverride?: number;
31
+ enabled = false;
32
+ protected params: Record<string, string> = {};
33
+ protected mission?: BehaviorMissionReporter;
34
+
35
+ get priority(): number {
36
+ return this.priorityOverride ?? CATEGORY_PRIORITY[this.category];
37
+ }
38
+
39
+ get effectivelyEnabled(): boolean {
40
+ return this.enabled || this.category === "survival";
41
+ }
42
+
43
+ isActive(_ctx: BehaviorContext): boolean {
44
+ return true;
45
+ }
46
+
47
+ onStart(_ctx: BehaviorContext): void | Promise<void> {
48
+ // noop by default
49
+ }
50
+
51
+ abstract onTick(ctx: BehaviorContext): void | Promise<void>;
52
+
53
+ onStop(_ctx: BehaviorContext): void | Promise<void> {
54
+ // noop by default
55
+ }
56
+
57
+ isFinished(): boolean {
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * 对外暴露的内部状态(LLM 调试 / snapshot 用)。
63
+ * 默认返回空对象;子类覆写以贡献字段。键名应稳定、字符串或简单类型,
64
+ * 不返回 entity.position 等敏感坐标(距离/ID/名字 安全)。
65
+ */
66
+ contributesState(_ctx: BehaviorContext): Record<string, unknown> {
67
+ return {};
68
+ }
69
+
70
+ configure(params: Record<string, string>): void {
71
+ this.params = params;
72
+ this.onConfigure(params);
73
+ }
74
+
75
+ protected onConfigure(_params: Record<string, string>): void {
76
+ // subclasses read params here
77
+ }
78
+
79
+ bindMission(reporter: BehaviorMissionReporter): void {
80
+ this.mission = reporter;
81
+ }
82
+ }
@@ -0,0 +1,65 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { GoalFollow } from "../../path-engine";
3
+ import { entityDistance } from "../../util/entities";
4
+
5
+ export class ApproachPlayerBehavior extends Behavior {
6
+ readonly name = "approach_player";
7
+ readonly category = "movement" as const;
8
+ private target = "";
9
+ private distance = 3;
10
+ private lastGoalAt = 0;
11
+ private missingSince = 0;
12
+
13
+ protected onConfigure(params: Record<string, string>): void {
14
+ this.target = params.target ?? "";
15
+ this.distance = Math.max(1, Number(params.distance) || 3);
16
+ }
17
+
18
+ isActive(): boolean {
19
+ return !!this.target;
20
+ }
21
+
22
+ onTick(ctx: BehaviorContext): void {
23
+ const player = ctx.bot.players[this.target]?.entity;
24
+ if (!player) {
25
+ if (!this.missingSince) this.missingSince = Date.now();
26
+ if (Date.now() - this.missingSince >= 5_000) {
27
+ this.mission?.block("target_not_found", `找不到玩家 ${this.target}`);
28
+ }
29
+ return;
30
+ }
31
+ this.missingSince = 0;
32
+ const distance = entityDistance(ctx.bot.entity, player);
33
+ this.mission?.progress({
34
+ target: this.target,
35
+ distance: Math.round(distance * 10) / 10,
36
+ });
37
+ if (distance <= this.distance) {
38
+ this.mission?.succeed(`已接近玩家 ${this.target}`, {
39
+ target: this.target,
40
+ distance,
41
+ });
42
+ return;
43
+ }
44
+ const engine = ctx.bot.pathEngine;
45
+ if (!engine || engine.isMoving() || Date.now() - this.lastGoalAt < 2_000)
46
+ return;
47
+ try {
48
+ engine.setGoal(new GoalFollow(player, this.distance), true);
49
+ this.lastGoalAt = Date.now();
50
+ } catch (error) {
51
+ this.mission?.fail("path_unreachable", String(error), {
52
+ target: this.target,
53
+ distance,
54
+ });
55
+ }
56
+ }
57
+
58
+ onStop(ctx: BehaviorContext): void {
59
+ try {
60
+ ctx.bot.pathEngine?.stop();
61
+ } catch {
62
+ // ignore
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,68 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { nearestHostile, entityName, entityDistance } from "../../util/entities";
3
+ import { equipSword } from "../../util/inventory";
4
+
5
+ export class SelfDefenseBehavior extends Behavior {
6
+ readonly name = "defend";
7
+ readonly category = "combat" as const;
8
+ private radius = 8;
9
+ private armed = false;
10
+ private attacking = false;
11
+
12
+ protected onConfigure(params: Record<string, string>): void {
13
+ this.radius = Number(params.radius) || 8;
14
+ }
15
+
16
+ isActive(ctx: BehaviorContext): boolean {
17
+ return nearestHostile(ctx.bot, this.radius) !== null;
18
+ }
19
+
20
+ onStart(): void {
21
+ this.armed = false;
22
+ this.attacking = false;
23
+ }
24
+
25
+ async onTick(ctx: BehaviorContext): Promise<void> {
26
+ const combat = ctx.bot.combat;
27
+ if (!combat) return;
28
+ if (!this.armed) {
29
+ await equipSword(ctx.bot);
30
+ this.armed = true;
31
+ }
32
+ const target = nearestHostile(ctx.bot, this.radius);
33
+ if (!target) {
34
+ this.attacking = false;
35
+ combat.stop();
36
+ return;
37
+ }
38
+ if (this.attacking) return;
39
+ ctx.log(
40
+ `defend 攻击 ${entityName(target)} (dist=${entityDistance(ctx.bot.entity, target).toFixed(1)})`,
41
+ );
42
+ this.attacking = true;
43
+ combat
44
+ .attack(target)
45
+ .catch(() => {
46
+ // ignore
47
+ })
48
+ .finally(() => {
49
+ this.attacking = false;
50
+ });
51
+ }
52
+
53
+ onStop(ctx: BehaviorContext): void {
54
+ try {
55
+ ctx.bot.combat?.stop();
56
+ } catch {
57
+ // ignore
58
+ }
59
+ }
60
+
61
+ contributesState(): Record<string, unknown> {
62
+ return {
63
+ radius: this.radius,
64
+ armed: this.armed,
65
+ attacking: this.attacking,
66
+ };
67
+ }
68
+ }
@@ -0,0 +1,72 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { GoalXZ } from "../../path-engine";
3
+
4
+ const EXPLORE_HALF_RANGE = 12;
5
+ const GOTO_TIMEOUT_MS = 10_000;
6
+
7
+ export class ExploreBehavior extends Behavior {
8
+ readonly name = "explore";
9
+ readonly category = "movement" as const;
10
+ private exploring = false;
11
+
12
+ onTick(ctx: BehaviorContext): void {
13
+ if (this.exploring) return;
14
+ const engine = ctx.bot.pathEngine;
15
+ if (!engine) return;
16
+ const pos = ctx.bot.entity?.position;
17
+ if (!pos) return;
18
+ const gx = Math.floor(pos.x + (Math.random() - 0.5) * EXPLORE_HALF_RANGE * 2);
19
+ const gz = Math.floor(pos.z + (Math.random() - 0.5) * EXPLORE_HALF_RANGE * 2);
20
+ this.exploring = true;
21
+ ctx.log(`explore -> (${gx},${gz})`);
22
+
23
+ const gotoPromise = engine.goto(new GoalXZ(gx, gz));
24
+ let timedOut = false;
25
+ const timer = setTimeout(() => {
26
+ timedOut = true;
27
+ try {
28
+ engine.stop();
29
+ } catch {
30
+ // ignore
31
+ }
32
+ }, GOTO_TIMEOUT_MS);
33
+
34
+ gotoPromise
35
+ .then(() => {
36
+ if (!timedOut) {
37
+ ctx.log(`explore 到达 (${gx},${gz})`);
38
+ this.mission?.succeed("已完成一次探索移动", { x: gx, z: gz });
39
+ } else {
40
+ this.mission?.fail("path_timeout", `探索目标 (${gx},${gz}) 超时`);
41
+ }
42
+ })
43
+ .catch((e: any) => {
44
+ ctx.log(`explore 放弃 (${gx},${gz}): ${e}`);
45
+ this.mission?.fail("path_unreachable", String(e), { x: gx, z: gz });
46
+ })
47
+ .finally(() => {
48
+ clearTimeout(timer);
49
+ try {
50
+ engine.setGoal(null);
51
+ } catch {
52
+ // ignore
53
+ }
54
+ this.exploring = false;
55
+ });
56
+ }
57
+
58
+ onStop(ctx: BehaviorContext): void {
59
+ try {
60
+ ctx.bot.pathEngine?.stop();
61
+ } catch {
62
+ // ignore
63
+ }
64
+ this.exploring = false;
65
+ }
66
+
67
+ contributesState(): Record<string, unknown> {
68
+ return {
69
+ exploring: this.exploring,
70
+ };
71
+ }
72
+ }
@@ -0,0 +1,26 @@
1
+ import type { Behavior } from "../base-behavior";
2
+ import type { MovementInit } from "../../types";
3
+ import { IdleWanderBehavior } from "./idle";
4
+ import { FollowPlayerBehavior } from "./follow";
5
+ import { GatherResourceBehavior } from "./gather";
6
+ import { FarmMobsBehavior } from "./farm-mobs";
7
+ import { ExploreBehavior } from "./explore";
8
+
9
+ const MOVEMENT_FACTORIES: Record<string, () => Behavior> = {
10
+ idle: () => new IdleWanderBehavior(),
11
+ follow: () => new FollowPlayerBehavior(),
12
+ gather: () => new GatherResourceBehavior(),
13
+ farm_mobs: () => new FarmMobsBehavior(),
14
+ explore: () => new ExploreBehavior(),
15
+ };
16
+
17
+ export function createBehavior(init: MovementInit): Behavior {
18
+ const factory = MOVEMENT_FACTORIES[init.name];
19
+ const behavior = factory ? factory() : new IdleWanderBehavior();
20
+ behavior.configure(init.params ?? {});
21
+ return behavior;
22
+ }
23
+
24
+ export function hasMovementFactory(name: string): boolean {
25
+ return name in MOVEMENT_FACTORIES;
26
+ }
@@ -0,0 +1,43 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { nearestPassiveMob } from "../../util/entities";
3
+ import { equipSword } from "../../util/inventory";
4
+
5
+ const HUNT_RADIUS = 20;
6
+
7
+ export class FarmMobsBehavior extends Behavior {
8
+ readonly name = "farm_mobs";
9
+ readonly category = "movement" as const;
10
+ private hunting = false;
11
+
12
+ async onTick(ctx: BehaviorContext): Promise<void> {
13
+ if (this.hunting) return;
14
+ const combat = ctx.bot.combat;
15
+ if (!combat) return;
16
+ const target = nearestPassiveMob(ctx.bot, HUNT_RADIUS);
17
+ if (!target) return;
18
+ this.hunting = true;
19
+ await equipSword(ctx.bot);
20
+ combat
21
+ .attack(target)
22
+ .catch(() => {
23
+ // ignore
24
+ })
25
+ .finally(() => {
26
+ this.hunting = false;
27
+ });
28
+ }
29
+
30
+ onStop(ctx: BehaviorContext): void {
31
+ try {
32
+ ctx.bot.combat?.stop();
33
+ } catch {
34
+ // ignore
35
+ }
36
+ }
37
+
38
+ contributesState(): Record<string, unknown> {
39
+ return {
40
+ hunting: this.hunting,
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,80 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { GoalFollow } from "../../path-engine";
3
+ import { entityDistance } from "../../util/entities";
4
+
5
+ export class FollowPlayerBehavior extends Behavior {
6
+ readonly name = "follow";
7
+ readonly category = "movement" as const;
8
+ private target = "";
9
+ private distance = 3;
10
+ private lastDist = -1;
11
+ private warnedNoEntity = false;
12
+ private lastGoalAt = 0;
13
+ private missingSince = 0;
14
+ private seenTarget = false;
15
+
16
+ protected onConfigure(params: Record<string, string>): void {
17
+ this.target = params.target ?? "";
18
+ this.distance = Number(params.distance) || 3;
19
+ }
20
+
21
+ isActive(ctx: BehaviorContext): boolean {
22
+ void ctx;
23
+ return !!this.target;
24
+ }
25
+
26
+ onTick(ctx: BehaviorContext): void {
27
+ const bot = ctx.bot;
28
+ const engine = bot.pathEngine;
29
+ const player = bot.players[this.target]?.entity;
30
+ if (!player) {
31
+ if (!this.missingSince) this.missingSince = Date.now();
32
+ if (!this.warnedNoEntity) {
33
+ ctx.log(`follow 找不到玩家 ${this.target} 的实体(不在视野/未加载)`);
34
+ this.warnedNoEntity = true;
35
+ }
36
+ if (Date.now() - this.missingSince >= (this.seenTarget ? 3_000 : 5_000)) {
37
+ const code = this.seenTarget ? "target_lost" : "target_not_found";
38
+ this.mission?.block(code, `无法继续跟随玩家 ${this.target}`, {
39
+ target: this.target,
40
+ seenTarget: this.seenTarget,
41
+ });
42
+ }
43
+ return;
44
+ }
45
+ this.seenTarget = true;
46
+ this.missingSince = 0;
47
+ this.warnedNoEntity = false;
48
+ this.lastDist = entityDistance(bot.entity, player);
49
+ if (!engine) return;
50
+ if (engine.isMoving()) return;
51
+ const now = Date.now();
52
+ if (now - this.lastGoalAt < 2000) return;
53
+ if (this.lastDist > this.distance + 1) {
54
+ try {
55
+ engine.setGoal(new GoalFollow(player, this.distance), true);
56
+ this.lastGoalAt = now;
57
+ ctx.log(`follow -> ${this.target} (dist=${this.lastDist.toFixed(1)}, goal=${this.distance})`);
58
+ } catch (e) {
59
+ ctx.log(`follow 寻路失败: ${e}`);
60
+ }
61
+ }
62
+ }
63
+
64
+ onStop(ctx: BehaviorContext): void {
65
+ try {
66
+ ctx.bot.pathEngine?.stop();
67
+ } catch {
68
+ // ignore
69
+ }
70
+ }
71
+
72
+ contributesState(): Record<string, unknown> {
73
+ return {
74
+ target: this.target || null,
75
+ distance: this.distance,
76
+ lastObservedDist: this.lastDist < 0 ? null : Math.round(this.lastDist * 10) / 10,
77
+ seenTarget: this.seenTarget,
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,135 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { GoalGetToBlock } from "../../path-engine";
3
+ import { equipToolFor } from "../../util/inventory";
4
+
5
+ const RESOURCE_MATCHERS: Record<string, (name: string) => boolean> = {
6
+ wood: (n) => /_log$/.test(n),
7
+ stone: (n) =>
8
+ /^(stone|cobblestone|andesite|diorite|granite|deepslate|tuff|basalt|calcite|dripstone_block)/.test(
9
+ n,
10
+ ),
11
+ coal: (n) => /coal_ore/.test(n),
12
+ iron: (n) => /iron_ore/.test(n),
13
+ dirt: (n) => /^(dirt|grass_block|coarse_dirt|rooted_dirt|podzol|mycelium)$/.test(n),
14
+ };
15
+
16
+ export class GatherResourceBehavior extends Behavior {
17
+ readonly name = "gather";
18
+ readonly category = "movement" as const;
19
+ private resource = "wood";
20
+ private targetCount = 1;
21
+ private gathered = 0;
22
+ private mining = false;
23
+ private missingSince = 0;
24
+
25
+ protected onConfigure(params: Record<string, string>): void {
26
+ this.resource = params.resource ?? "wood";
27
+ this.targetCount = Math.max(1, Number(params.count) || 1);
28
+ }
29
+
30
+ onTick(ctx: BehaviorContext): void {
31
+ if (this.mining) return;
32
+ const matcher = RESOURCE_MATCHERS[this.resource];
33
+ if (!matcher) return;
34
+ const block = ctx.bot.findBlock({
35
+ matching: (b: any) => !!b && matcher(b.name),
36
+ maxDistance: 32,
37
+ } as any);
38
+ if (!block) {
39
+ if (!this.missingSince) this.missingSince = Date.now();
40
+ if (Date.now() - this.missingSince >= 5_000) {
41
+ this.mission?.block("resource_not_found", `附近找不到资源 ${this.resource}`, {
42
+ resource: this.resource,
43
+ gathered: this.gathered,
44
+ targetCount: this.targetCount,
45
+ });
46
+ }
47
+ return;
48
+ }
49
+ this.missingSince = 0;
50
+
51
+ const engine = ctx.bot.pathEngine;
52
+ if (!engine) return;
53
+
54
+ this.mining = true;
55
+ void (async () => {
56
+ const timer = setTimeout(() => {
57
+ try {
58
+ engine.stop();
59
+ } catch {
60
+ // ignore
61
+ }
62
+ }, 12_000);
63
+ try {
64
+ const emptySlots = (ctx.bot.inventory as any)?.emptySlotCount?.();
65
+ if (emptySlots === 0) {
66
+ this.mission?.block("inventory_full", "背包已满,无法继续采集", {
67
+ resource: this.resource,
68
+ gathered: this.gathered,
69
+ targetCount: this.targetCount,
70
+ });
71
+ return;
72
+ }
73
+ const needsTool = this.resource !== "wood" && this.resource !== "dirt";
74
+ const equipped = await equipToolFor(ctx.bot, this.resource);
75
+ if (needsTool && !equipped) {
76
+ this.mission?.block("missing_tool", `采集 ${this.resource} 需要合适的镐`, {
77
+ resource: this.resource,
78
+ gathered: this.gathered,
79
+ targetCount: this.targetCount,
80
+ });
81
+ return;
82
+ }
83
+ if (this.mission && !this.mission.isCurrent()) return;
84
+ await engine.goto(
85
+ new GoalGetToBlock(block.position.x, block.position.y, block.position.z),
86
+ );
87
+ if (this.mission && !this.mission.isCurrent()) return;
88
+ await ctx.bot.dig(block);
89
+ this.gathered++;
90
+ const progress = {
91
+ resource: this.resource,
92
+ gathered: this.gathered,
93
+ targetCount: this.targetCount,
94
+ };
95
+ this.mission?.progress(progress);
96
+ if (this.gathered >= this.targetCount) {
97
+ this.mission?.succeed(`已采集 ${this.gathered} 个 ${this.resource}`, progress);
98
+ }
99
+ } catch (e) {
100
+ ctx.log(`gather 失败: ${e}`);
101
+ const message = String(e);
102
+ this.mission?.fail(
103
+ /timeout/i.test(message) ? "path_timeout" : "path_unreachable",
104
+ message,
105
+ { resource: this.resource, gathered: this.gathered, targetCount: this.targetCount },
106
+ );
107
+ } finally {
108
+ clearTimeout(timer);
109
+ try {
110
+ engine.setGoal(null);
111
+ } catch {
112
+ // ignore
113
+ }
114
+ this.mining = false;
115
+ }
116
+ })();
117
+ }
118
+
119
+ onStop(ctx: BehaviorContext): void {
120
+ try {
121
+ ctx.bot.pathEngine?.stop();
122
+ } catch {
123
+ // ignore
124
+ }
125
+ }
126
+
127
+ contributesState(): Record<string, unknown> {
128
+ return {
129
+ resource: this.resource,
130
+ targetCount: this.targetCount,
131
+ gathered: this.gathered,
132
+ mining: this.mining,
133
+ };
134
+ }
135
+ }
@@ -0,0 +1,130 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { nearestPlayer, nearestPassiveMob, nearestHostile } from "../../util/entities";
3
+
4
+ const LOOK_INTERVAL_MS = 500;
5
+ const WANDER_INTERVAL_MS = 4_000;
6
+ const WANDER_JITTER_MS = 3_000;
7
+ const STEP_MS = 800;
8
+ const LOOK_IDLE_MS = 1_500;
9
+ const DO_NOTHING_MIN_MS = 1_500;
10
+ const DO_NOTHING_JITTER_MS = 1_500;
11
+ const LOOK_RADIUS = 8;
12
+
13
+ type Action = "look" | "walk" | "idle";
14
+
15
+ export class IdleWanderBehavior extends Behavior {
16
+ readonly name = "idle";
17
+ readonly category = "movement" as const;
18
+ private nextWanderAt = 0;
19
+ private nextLookAt = 0;
20
+ private movingUntil = 0;
21
+ private idleUntil = 0;
22
+ private lastAction: Action = "idle";
23
+ private lookTarget: "player" | "passive" | "hostile" | null = null;
24
+
25
+ onStart(): void {
26
+ const now = Date.now();
27
+ this.nextWanderAt = now + 1_000;
28
+ this.nextLookAt = now;
29
+ this.movingUntil = 0;
30
+ this.idleUntil = 0;
31
+ this.lastAction = "idle";
32
+ this.lookTarget = null;
33
+ }
34
+
35
+ onTick(ctx: BehaviorContext): void {
36
+ const now = Date.now();
37
+ if (this.movingUntil && now >= this.movingUntil) {
38
+ try {
39
+ ctx.bot.clearControlStates();
40
+ } catch {
41
+ // ignore
42
+ }
43
+ this.movingUntil = 0;
44
+ }
45
+ if (!this.movingUntil && now >= this.nextLookAt) {
46
+ this.tickLook(ctx);
47
+ this.nextLookAt = now + LOOK_INTERVAL_MS;
48
+ }
49
+ if (this.idleUntil && now < this.idleUntil) return;
50
+ if (now >= this.nextWanderAt) {
51
+ this.tickRandomAction(ctx, now);
52
+ } else if (this.movingUntil > now) {
53
+ this.lastAction = "walk";
54
+ } else {
55
+ this.lastAction = "idle";
56
+ }
57
+ }
58
+
59
+ private tickLook(ctx: BehaviorContext): void {
60
+ const target = this.pickLookTarget(ctx);
61
+ try {
62
+ if (target) {
63
+ ctx.bot.lookAt(target.position.offset(0, 1, 0), true);
64
+ } else {
65
+ ctx.bot.look(Math.random() * Math.PI * 2, (Math.random() - 0.5) * 0.6, true);
66
+ }
67
+ } catch {
68
+ // ignore
69
+ }
70
+ }
71
+
72
+ private pickLookTarget(ctx: BehaviorContext): any | null {
73
+ const player = nearestPlayer(ctx.bot, LOOK_RADIUS);
74
+ if (player) {
75
+ this.lookTarget = "player";
76
+ return player;
77
+ }
78
+ const passive = nearestPassiveMob(ctx.bot, LOOK_RADIUS);
79
+ if (passive) {
80
+ this.lookTarget = "passive";
81
+ return passive;
82
+ }
83
+ const hostile = nearestHostile(ctx.bot, LOOK_RADIUS);
84
+ if (hostile) {
85
+ this.lookTarget = "hostile";
86
+ return hostile;
87
+ }
88
+ this.lookTarget = null;
89
+ return null;
90
+ }
91
+
92
+ private tickRandomAction(ctx: BehaviorContext, now: number): void {
93
+ const r = Math.random() * 7;
94
+ if (r < 2) {
95
+ this.lastAction = "look";
96
+ this.idleUntil = now + LOOK_IDLE_MS;
97
+ } else if (r < 4) {
98
+ try {
99
+ ctx.bot.look(Math.random() * Math.PI * 2, 0, true);
100
+ ctx.bot.setControlState("forward", true);
101
+ this.movingUntil = now + STEP_MS;
102
+ this.lastAction = "walk";
103
+ } catch {
104
+ // ignore
105
+ }
106
+ } else {
107
+ this.lastAction = "idle";
108
+ this.idleUntil = now + DO_NOTHING_MIN_MS + Math.random() * DO_NOTHING_JITTER_MS;
109
+ }
110
+ this.nextWanderAt = now + WANDER_INTERVAL_MS + Math.random() * WANDER_JITTER_MS;
111
+ }
112
+
113
+ onStop(ctx: BehaviorContext): void {
114
+ try {
115
+ ctx.bot.clearControlStates();
116
+ } catch {
117
+ // ignore
118
+ }
119
+ }
120
+
121
+ contributesState(): Record<string, unknown> {
122
+ const now = Date.now();
123
+ return {
124
+ lastAction: this.lastAction,
125
+ lookTarget: this.lookTarget,
126
+ nextWanderInMs: Math.max(0, this.nextWanderAt - now),
127
+ moving: this.movingUntil > now,
128
+ };
129
+ }
130
+ }