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,85 @@
1
+ import { Vec3 } from "vec3";
2
+ import { Behavior, type BehaviorContext } from "../base-behavior";
3
+ import { GoalNear } from "../../path-engine";
4
+
5
+ export class SeekShelterBehavior extends Behavior {
6
+ readonly name = "seek_shelter";
7
+ readonly category = "movement" as const;
8
+ private moving = false;
9
+ private attempted = false;
10
+
11
+ onTick(ctx: BehaviorContext): void {
12
+ if (this.moving || this.attempted) return;
13
+ this.attempted = true;
14
+ const target = findShelteredPosition(ctx.bot);
15
+ if (!target) {
16
+ this.mission?.block("resource_not_found", "附近没有找到可到达的遮蔽位置");
17
+ return;
18
+ }
19
+ const engine = ctx.bot.pathEngine;
20
+ if (!engine) {
21
+ this.mission?.fail("path_unreachable", "寻路引擎不可用");
22
+ return;
23
+ }
24
+ this.moving = true;
25
+ engine
26
+ .goto(new GoalNear(target.x, target.y, target.z, 1))
27
+ .then(() => this.mission?.succeed("已到达遮蔽位置", target))
28
+ .catch((error: unknown) =>
29
+ this.mission?.fail("path_unreachable", String(error), target),
30
+ )
31
+ .finally(() => {
32
+ this.moving = false;
33
+ });
34
+ }
35
+
36
+ onStop(ctx: BehaviorContext): void {
37
+ try {
38
+ ctx.bot.pathEngine?.stop();
39
+ } catch {
40
+ // ignore
41
+ }
42
+ }
43
+ }
44
+
45
+ function findShelteredPosition(
46
+ bot: any,
47
+ ): { x: number; y: number; z: number } | null {
48
+ const position = bot.entity?.position;
49
+ if (!position) return null;
50
+ const y = Math.floor(position.y);
51
+ for (let radius = 0; radius <= 12; radius++) {
52
+ for (let dx = -radius; dx <= radius; dx++) {
53
+ for (let dz = -radius; dz <= radius; dz++) {
54
+ if (radius > 0 && Math.abs(dx) !== radius && Math.abs(dz) !== radius)
55
+ continue;
56
+ const x = Math.floor(position.x) + dx;
57
+ const z = Math.floor(position.z) + dz;
58
+ const floor = bot.blockAt(new Vec3(x, y - 1, z), false);
59
+ const feet = bot.blockAt(new Vec3(x, y, z), false);
60
+ const head = bot.blockAt(new Vec3(x, y + 1, z), false);
61
+ const roof = bot.blockAt(new Vec3(x, y + 2, z), false);
62
+ if (isSolid(floor) && isAir(feet) && isAir(head) && isSolid(roof))
63
+ return { x, y, z };
64
+ }
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ function isAir(block: any): boolean {
71
+ return (
72
+ !block ||
73
+ block.boundingBox === "empty" ||
74
+ block.name === "air" ||
75
+ block.name === "cave_air"
76
+ );
77
+ }
78
+
79
+ function isSolid(block: any): boolean {
80
+ return (
81
+ !!block &&
82
+ block.boundingBox !== "empty" &&
83
+ !/^(water|lava|fire)$/.test(String(block.name))
84
+ );
85
+ }
@@ -0,0 +1,243 @@
1
+ import type { Behavior, BehaviorContext } from "./base-behavior";
2
+ import type { MovementInit } from "../types";
3
+ import { createBehavior } from "./catalog/factory";
4
+ import type { BehaviorMode, ModeState } from "../state/mode";
5
+
6
+ export interface BehaviorEngineOptions {
7
+ ctxBuilder: () => BehaviorContext | null;
8
+ tickInterval: number;
9
+ survival: Behavior[];
10
+ overlays: Behavior[];
11
+ initialMovement?: MovementInit;
12
+ }
13
+
14
+ export interface BehaviorStateInfo {
15
+ name: string;
16
+ category: string;
17
+ priority: number;
18
+ enabled: boolean;
19
+ active: boolean;
20
+ }
21
+
22
+ export class BehaviorEngine {
23
+ private readonly survival: Behavior[];
24
+ private readonly overlays: Behavior[];
25
+ private movement: Behavior | null = null;
26
+ private missionId: string | null = null;
27
+ private missionBehaviors: Behavior[] = [];
28
+ private timer?: NodeJS.Timeout;
29
+ private current: Behavior | null = null;
30
+ private lastMode: BehaviorMode = "IDLE";
31
+ private readonly lastSwitch: ModeState["lastSwitch"] = {
32
+ from: null,
33
+ to: "IDLE",
34
+ reason: "init",
35
+ at: Date.now(),
36
+ };
37
+ private readonly ctxBuilder: () => BehaviorContext | null;
38
+ private readonly tickInterval: number;
39
+
40
+ constructor(opts: BehaviorEngineOptions) {
41
+ this.ctxBuilder = opts.ctxBuilder;
42
+ this.tickInterval = opts.tickInterval;
43
+ this.survival = opts.survival;
44
+ this.overlays = opts.overlays;
45
+ if (opts.initialMovement) {
46
+ this.movement = createBehavior(opts.initialMovement);
47
+ this.movement.enabled = true;
48
+ }
49
+ }
50
+
51
+ start(): void {
52
+ if (this.timer) return;
53
+ this.timer = setInterval(() => this.tick(), this.tickInterval);
54
+ }
55
+
56
+ stop(): void {
57
+ if (this.timer) {
58
+ clearInterval(this.timer);
59
+ this.timer = undefined;
60
+ }
61
+ const ctx = this.ctxBuilder();
62
+ if (this.current && ctx) {
63
+ try {
64
+ this.current.onStop(ctx);
65
+ } catch {
66
+ // ignore
67
+ }
68
+ }
69
+ this.current = null;
70
+ }
71
+
72
+ setMissionBehaviors(missionId: string, behaviors: Behavior[]): void {
73
+ this.missionId = missionId;
74
+ for (const b of behaviors) b.enabled = true;
75
+ this.missionBehaviors = behaviors;
76
+ }
77
+
78
+ removeMissionBehaviors(missionId: string): void {
79
+ if (this.missionId === missionId) {
80
+ this.missionBehaviors = [];
81
+ this.missionId = null;
82
+ }
83
+ }
84
+
85
+ toggleOverlay(name: string, enabled: boolean, params?: Record<string, string>): boolean {
86
+ const b = this.overlays.find((o) => o.name === name);
87
+ if (!b) return false;
88
+ if (params) b.configure(params);
89
+ b.enabled = enabled;
90
+ return true;
91
+ }
92
+
93
+ isOverlayEnabled(name: string): boolean {
94
+ const b = this.overlays.find((o) => o.name === name);
95
+ return b?.enabled ?? false;
96
+ }
97
+
98
+ clear(): void {
99
+ for (const o of this.overlays) o.enabled = false;
100
+ this.movement = createBehavior({ name: "idle", params: {} });
101
+ this.movement.enabled = true;
102
+ this.missionBehaviors = [];
103
+ this.missionId = null;
104
+ }
105
+
106
+ stopMission(): void {
107
+ this.movement = createBehavior({ name: "idle", params: {} });
108
+ this.movement.enabled = true;
109
+ this.missionBehaviors = [];
110
+ this.missionId = null;
111
+ }
112
+
113
+ currentLabel(): string | null {
114
+ return this.current?.name ?? null;
115
+ }
116
+
117
+ getStates(ctx: BehaviorContext): BehaviorStateInfo[] {
118
+ const all = this.getAllBehaviors();
119
+ return all.map((b) => ({
120
+ name: b.name,
121
+ category: b.category,
122
+ priority: b.priority,
123
+ enabled: b.effectivelyEnabled,
124
+ active: b.effectivelyEnabled && this.safeActive(b, ctx),
125
+ }));
126
+ }
127
+
128
+ getAllBehaviors(): Behavior[] {
129
+ const list: Behavior[] = [];
130
+ list.push(...this.survival);
131
+ list.push(...this.overlays);
132
+ for (const b of this.missionBehaviors) list.push(b);
133
+ if (this.movement) list.push(this.movement);
134
+ return list;
135
+ }
136
+
137
+ modeState(): ModeState {
138
+ const next = this.computeMode();
139
+ if (next !== this.lastMode) {
140
+ this.lastSwitch.from = this.lastMode;
141
+ this.lastSwitch.to = next;
142
+ this.lastSwitch.reason = next === "EMERGENCY" ? "emergency_engaged" : "emergency_cleared";
143
+ this.lastSwitch.at = Date.now();
144
+ this.lastMode = next;
145
+ }
146
+ return {
147
+ current: this.lastMode,
148
+ mission: null,
149
+ lastSwitch: { ...this.lastSwitch },
150
+ };
151
+ }
152
+
153
+ private tick(): void {
154
+ const ctx = this.ctxBuilder();
155
+ if (!ctx) return;
156
+
157
+ const candidates: Behavior[] = [];
158
+ for (const b of this.survival) candidates.push(b);
159
+ for (const b of this.overlays) candidates.push(b);
160
+ for (const b of this.missionBehaviors) candidates.push(b);
161
+ if (!this.missionHasActive(ctx) && this.movement) candidates.push(this.movement);
162
+
163
+ let winner: Behavior | null = null;
164
+ for (const b of candidates) {
165
+ if (!b.effectivelyEnabled) continue;
166
+ if (!this.safeActive(b, ctx)) continue;
167
+ if (!winner || b.priority > winner.priority) winner = b;
168
+ }
169
+
170
+ if (this.current !== winner) {
171
+ if (this.current) {
172
+ try {
173
+ this.current.onStop(ctx);
174
+ } catch (e) {
175
+ ctx.log(`行为 onStop 失败(${this.current.name}): ${e}`);
176
+ }
177
+ }
178
+ this.current = winner;
179
+ if (this.current) {
180
+ try {
181
+ this.current.onStart(ctx);
182
+ } catch (e) {
183
+ ctx.log(`行为 onStart 失败(${this.current.name}): ${e}`);
184
+ }
185
+ }
186
+ }
187
+
188
+ if (this.current) {
189
+ try {
190
+ this.current.onTick(ctx);
191
+ } catch (e) {
192
+ ctx.log(`行为 onTick 失败(${this.current.name}): ${e}`);
193
+ }
194
+ if (this.current.isFinished()) {
195
+ try {
196
+ this.current.onStop(ctx);
197
+ } catch {
198
+ // ignore
199
+ }
200
+ this.current = null;
201
+ }
202
+ }
203
+ }
204
+
205
+ private safeActive(b: Behavior, ctx: BehaviorContext): boolean {
206
+ try {
207
+ return b.isActive(ctx);
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ private missionHasActive(ctx: BehaviorContext): boolean {
214
+ for (const b of this.missionBehaviors) {
215
+ if (!b.effectivelyEnabled) continue;
216
+ if (this.safeActive(b, ctx)) return true;
217
+ }
218
+ return false;
219
+ }
220
+
221
+ private computeMode(): BehaviorMode {
222
+ const ctx = this.ctxBuilder();
223
+ if (!ctx) return this.lastMode;
224
+ const all = [...this.survival, ...this.overlays, ...this.missionBehaviors];
225
+ if (!this.missionHasActive(ctx) && this.movement) all.push(this.movement);
226
+ let emergency = false;
227
+ let mission = false;
228
+ for (const b of all) {
229
+ if (!b.effectivelyEnabled) continue;
230
+ if (!this.safeActive(b, ctx)) continue;
231
+ if (b.category === "survival") {
232
+ emergency = true;
233
+ break;
234
+ }
235
+ if (b.category === "movement" || b.category === "combat") {
236
+ mission = true;
237
+ }
238
+ }
239
+ if (emergency) return "EMERGENCY";
240
+ if (mission) return "MISSION";
241
+ return "IDLE";
242
+ }
243
+ }
@@ -0,0 +1,46 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { nearestHostile } from "../../util/entities";
3
+ import { eatFood, findFood } from "../../util/inventory";
4
+
5
+ const HUNGER_THRESHOLD = 10;
6
+ const COMBAT_BLOCK_RADIUS = 5;
7
+
8
+ export class AutoEatBehavior extends Behavior {
9
+ readonly name = "auto_eat";
10
+ readonly category = "maintenance" as const;
11
+ private eating = false;
12
+
13
+ isActive(ctx: BehaviorContext): boolean {
14
+ const bot = ctx.bot;
15
+ if ((bot.food ?? 20) > HUNGER_THRESHOLD) return false;
16
+ if (!findFood(bot)) return false;
17
+ return nearestHostile(bot, COMBAT_BLOCK_RADIUS) === null;
18
+ }
19
+
20
+ async onTick(ctx: BehaviorContext): Promise<void> {
21
+ if (this.eating) return;
22
+ this.eating = true;
23
+ try {
24
+ ctx.bot.clearControlStates();
25
+ } catch {
26
+ // ignore
27
+ }
28
+ try {
29
+ await eatFood(ctx.bot);
30
+ } finally {
31
+ this.eating = false;
32
+ }
33
+ }
34
+
35
+ onStop(): void {
36
+ this.eating = false;
37
+ }
38
+
39
+ contributesState(ctx: BehaviorContext): Record<string, unknown> {
40
+ return {
41
+ eating: this.eating,
42
+ food: ctx.bot.food ?? 0,
43
+ foodLow: (ctx.bot.food ?? 20) <= HUNGER_THRESHOLD,
44
+ };
45
+ }
46
+ }
@@ -0,0 +1,44 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+
3
+ export class EscapeLavaBehavior extends Behavior {
4
+ readonly name = "escape_lava";
5
+ readonly category = "survival" as const;
6
+
7
+ isActive(ctx: BehaviorContext): boolean {
8
+ const bot = ctx.bot;
9
+ const pos = bot.entity?.position;
10
+ if (!pos) return false;
11
+ const feet = bot.blockAt(pos);
12
+ const below = bot.blockAt(pos.offset(0, -1, 0));
13
+ const isDanger = (b: any) => {
14
+ const n = String(b?.name ?? "").toLowerCase();
15
+ return n === "lava" || n === "fire" || n === "flowing_lava";
16
+ };
17
+ return isDanger(feet) || isDanger(below);
18
+ }
19
+
20
+ onStart(ctx: BehaviorContext): void {
21
+ try { ctx.bot.pathEngine?.setGoal(null); } catch { /* ignore */ }
22
+ }
23
+
24
+ onTick(ctx: BehaviorContext): void {
25
+ ctx.bot.setControlState("jump", true);
26
+ ctx.bot.setControlState("sprint", true);
27
+ ctx.bot.setControlState("forward", true);
28
+ }
29
+
30
+ onStop(ctx: BehaviorContext): void {
31
+ try {
32
+ ctx.bot.clearControlStates();
33
+ } catch {
34
+ // ignore
35
+ }
36
+ }
37
+
38
+ contributesState(): Record<string, unknown> {
39
+ return {
40
+ active: true,
41
+ hazard: "lava_or_fire",
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,35 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+
3
+ export class EscapeWaterBehavior extends Behavior {
4
+ readonly name = "escape_water";
5
+ readonly category = "survival" as const;
6
+
7
+ isActive(ctx: BehaviorContext): boolean {
8
+ return (ctx.bot.oxygenLevel ?? 20) <= 0;
9
+ }
10
+
11
+ onStart(ctx: BehaviorContext): void {
12
+ try { ctx.bot.pathEngine?.setGoal(null); } catch { /* ignore */ }
13
+ }
14
+
15
+ onTick(ctx: BehaviorContext): void {
16
+ ctx.bot.setControlState("jump", true);
17
+ ctx.bot.setControlState("forward", true);
18
+ }
19
+
20
+ onStop(ctx: BehaviorContext): void {
21
+ try {
22
+ ctx.bot.clearControlStates();
23
+ } catch {
24
+ // ignore
25
+ }
26
+ }
27
+
28
+ contributesState(ctx: BehaviorContext): Record<string, unknown> {
29
+ return {
30
+ active: true,
31
+ oxygen: ctx.bot.oxygenLevel ?? 0,
32
+ drowning: true,
33
+ };
34
+ }
35
+ }
@@ -0,0 +1,76 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+ import { GoalXZ } from "../../path-engine";
3
+ import { nearestCreeper } from "../../util/entities";
4
+ import { hasShield } from "../../util/inventory";
5
+
6
+ const CREEPER_FLEE_RADIUS = 6;
7
+ const FLEE_DISTANCE = 10;
8
+ const FLEE_TIMEOUT_MS = 5_000;
9
+
10
+ export class FleeCreeperBehavior extends Behavior {
11
+ readonly name = "flee_creeper";
12
+ readonly category = "survival" as const;
13
+ private fleeing = false;
14
+
15
+ isActive(ctx: BehaviorContext): boolean {
16
+ if (hasShield(ctx.bot)) return false;
17
+ return nearestCreeper(ctx.bot, CREEPER_FLEE_RADIUS) !== null;
18
+ }
19
+
20
+ onTick(ctx: BehaviorContext): void {
21
+ if (this.fleeing) return;
22
+ const bot = ctx.bot;
23
+ const engine = bot.pathEngine;
24
+ if (!engine) return;
25
+ const creeper = nearestCreeper(bot, CREEPER_FLEE_RADIUS + 2);
26
+ if (!creeper) return;
27
+ const pos = bot.entity?.position;
28
+ const cp = creeper.position;
29
+ if (!pos || !cp) return;
30
+ const dx = pos.x - cp.x;
31
+ const dz = pos.z - cp.z;
32
+ const len = Math.hypot(dx, dz) || 1;
33
+ const tx = Math.floor(pos.x + (dx / len) * FLEE_DISTANCE);
34
+ const tz = Math.floor(pos.z + (dz / len) * FLEE_DISTANCE);
35
+ this.fleeing = true;
36
+ ctx.log(`flee_creeper -> (${tx},${tz})`);
37
+ const timer = setTimeout(() => {
38
+ try {
39
+ engine.stop();
40
+ } catch {
41
+ // ignore
42
+ }
43
+ }, FLEE_TIMEOUT_MS);
44
+ engine
45
+ .goto(new GoalXZ(tx, tz))
46
+ .catch(() => {
47
+ // ignore
48
+ })
49
+ .finally(() => {
50
+ clearTimeout(timer);
51
+ try {
52
+ engine.setGoal(null);
53
+ } catch {
54
+ // ignore
55
+ }
56
+ this.fleeing = false;
57
+ });
58
+ }
59
+
60
+ onStop(ctx: BehaviorContext): void {
61
+ try {
62
+ ctx.bot.pathEngine?.stop();
63
+ } catch {
64
+ // ignore
65
+ }
66
+ this.fleeing = false;
67
+ }
68
+
69
+ contributesState(): Record<string, unknown> {
70
+ return {
71
+ active: true,
72
+ hazard: "creeper",
73
+ fleeing: this.fleeing,
74
+ };
75
+ }
76
+ }
@@ -0,0 +1,56 @@
1
+ import { Behavior, type BehaviorContext } from "../base-behavior";
2
+
3
+ const FALL_VELOCITY_THRESHOLD = -0.7;
4
+
5
+ export class MlgFallBehavior extends Behavior {
6
+ readonly name = "mlg_fall";
7
+ readonly category = "survival" as const;
8
+ private placed = false;
9
+
10
+ isActive(ctx: BehaviorContext): boolean {
11
+ const bot = ctx.bot;
12
+ const vy = bot.entity?.velocity?.y;
13
+ if (vy === undefined || vy >= FALL_VELOCITY_THRESHOLD) return false;
14
+ const head = bot.entity?.position;
15
+ if (!head) return false;
16
+ const below = bot.blockAt(head.offset(0, -2, 0));
17
+ const n = String(below?.name ?? "").toLowerCase();
18
+ return n !== "water" && n !== "lava";
19
+ }
20
+
21
+ onStart(ctx: BehaviorContext): void {
22
+ this.placed = false;
23
+ try { ctx.bot.pathEngine?.setGoal(null); } catch { /* ignore */ }
24
+ }
25
+
26
+ onTick(ctx: BehaviorContext): void {
27
+ if (this.placed) return;
28
+ const bot = ctx.bot;
29
+ const bucket = bot.inventory?.items?.().find((i: any) => i.name === "water_bucket");
30
+ if (!bucket) return;
31
+ this.placed = true;
32
+ void (async () => {
33
+ try {
34
+ await bot.equip(bucket, "hand");
35
+ bot.look(0, Math.PI / 2, true);
36
+ bot.activateItem();
37
+ setTimeout(() => {
38
+ try {
39
+ bot.activateItem();
40
+ } catch {
41
+ // ignore
42
+ }
43
+ }, 400);
44
+ } catch {
45
+ this.placed = false;
46
+ }
47
+ })();
48
+ }
49
+
50
+ contributesState(): Record<string, unknown> {
51
+ return {
52
+ active: true,
53
+ waterBucketPlaced: this.placed,
54
+ };
55
+ }
56
+ }