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.
- package/config.md +157 -3
- package/index.ts +61 -9
- package/package.json +8 -3
- package/play/actions/registry.ts +196 -0
- package/play/ai/context-builder.ts +29 -0
- package/play/ai/debug-log.ts +135 -0
- package/play/ai/main-loop.ts +306 -0
- package/play/ai/main-tools.ts +230 -0
- package/play/ai/prompt.ts +207 -0
- package/play/ai/work-subroutine.ts +487 -0
- package/play/behavior/base-behavior.ts +82 -0
- package/play/behavior/catalog/approach-player.ts +65 -0
- package/play/behavior/catalog/defend.ts +68 -0
- package/play/behavior/catalog/explore.ts +72 -0
- package/play/behavior/catalog/factory.ts +26 -0
- package/play/behavior/catalog/farm-mobs.ts +43 -0
- package/play/behavior/catalog/follow.ts +80 -0
- package/play/behavior/catalog/gather.ts +135 -0
- package/play/behavior/catalog/idle.ts +130 -0
- package/play/behavior/catalog/seek-shelter.ts +85 -0
- package/play/behavior/engine.ts +243 -0
- package/play/behavior/survival/auto-eat.ts +46 -0
- package/play/behavior/survival/escape-lava.ts +44 -0
- package/play/behavior/survival/escape-water.ts +35 -0
- package/play/behavior/survival/flee-creeper.ts +76 -0
- package/play/behavior/survival/mlg-fall.ts +56 -0
- package/play/bot/bot-controller.ts +276 -0
- package/play/bot/play-bus.ts +48 -0
- package/play/combat/combat.ts +225 -0
- package/play/combat/index.ts +1 -0
- package/play/config.ts +39 -0
- package/play/context.ts +26 -0
- package/play/debug/README.md +74 -0
- package/play/debug/commands.ts +289 -0
- package/play/index.ts +247 -0
- package/play/mineflayer-shims.d.ts +22 -0
- package/play/missions/bundles/approach-player.ts +26 -0
- package/play/missions/bundles/explore.ts +24 -0
- package/play/missions/bundles/farm-mobs.ts +24 -0
- package/play/missions/bundles/follow-player.ts +40 -0
- package/play/missions/bundles/gather-resource.ts +37 -0
- package/play/missions/bundles/idle-wander.ts +24 -0
- package/play/missions/bundles/seek-shelter.ts +18 -0
- package/play/missions/mission-controller.ts +296 -0
- package/play/missions/registry.ts +107 -0
- package/play/path-engine/astar.ts +514 -0
- package/play/path-engine/goals.ts +84 -0
- package/play/path-engine/index.ts +4 -0
- package/play/path-engine/movements.ts +67 -0
- package/play/path-engine/path-engine.ts +540 -0
- package/play/session.ts +486 -0
- package/play/state/cooldowns.ts +40 -0
- package/play/state/event-journal.ts +72 -0
- package/play/state/memory-bus.ts +115 -0
- package/play/state/mode.ts +57 -0
- package/play/state/sensors/entity-scanner.ts +275 -0
- package/play/state/snapshot.ts +360 -0
- package/play/types.ts +284 -0
- package/play/util/async.ts +11 -0
- package/play/util/endpoint.ts +38 -0
- package/play/util/entities.ts +135 -0
- package/play/util/inventory.ts +75 -0
- package/skills/mc.ts +58 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { createBot, type Bot } from "mineflayer";
|
|
2
|
+
import mcData from "minecraft-data";
|
|
3
|
+
import type { PlayServerConfig } from "../types";
|
|
4
|
+
import { resolveMinecraftEndpoint } from "../util/endpoint";
|
|
5
|
+
import { withTimeoutMs } from "../util/async";
|
|
6
|
+
import { PlayBus, type GameChatLine } from "./play-bus";
|
|
7
|
+
import {
|
|
8
|
+
PathEngine,
|
|
9
|
+
DEFAULT_MOVEMENTS,
|
|
10
|
+
type MovementsConfig,
|
|
11
|
+
} from "../path-engine";
|
|
12
|
+
import { Combat } from "../combat";
|
|
13
|
+
|
|
14
|
+
const PVP_FOLLOW_RANGE = 2;
|
|
15
|
+
const PVP_ATTACK_RANGE = 3.0;
|
|
16
|
+
|
|
17
|
+
export interface BotControllerOptions {
|
|
18
|
+
server: PlayServerConfig;
|
|
19
|
+
bus: PlayBus;
|
|
20
|
+
log: (msg: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class BotController {
|
|
24
|
+
bot: Bot | null = null;
|
|
25
|
+
readonly server: PlayServerConfig;
|
|
26
|
+
private readonly bus: PlayBus;
|
|
27
|
+
private readonly log: (msg: string) => void;
|
|
28
|
+
private pathEngine?: PathEngine;
|
|
29
|
+
private combat?: Combat;
|
|
30
|
+
private joinedOnce = false;
|
|
31
|
+
|
|
32
|
+
constructor(opts: BotControllerOptions) {
|
|
33
|
+
this.server = opts.server;
|
|
34
|
+
this.bus = opts.bus;
|
|
35
|
+
this.log = opts.log;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async connect(): Promise<void> {
|
|
39
|
+
const { host, port } = await resolveMinecraftEndpoint(this.server.host);
|
|
40
|
+
const bot = createBot({
|
|
41
|
+
host,
|
|
42
|
+
port,
|
|
43
|
+
username: this.server.username,
|
|
44
|
+
auth: this.server.auth ?? "offline",
|
|
45
|
+
password: this.server.password,
|
|
46
|
+
version: this.server.version || undefined,
|
|
47
|
+
viewDistance: "normal",
|
|
48
|
+
} as any);
|
|
49
|
+
this.bot = bot;
|
|
50
|
+
|
|
51
|
+
this.attachListeners(bot);
|
|
52
|
+
|
|
53
|
+
return new Promise<void>((resolve, reject) => {
|
|
54
|
+
let settled = false;
|
|
55
|
+
const clean = () => {
|
|
56
|
+
bot.removeListener("spawn", onSpawn);
|
|
57
|
+
bot.removeListener("error", onEarlyError);
|
|
58
|
+
bot.removeListener("end", onEarlyEnd);
|
|
59
|
+
bot.removeListener("kicked", onEarlyKicked);
|
|
60
|
+
};
|
|
61
|
+
const onSpawn = () => {
|
|
62
|
+
if (settled) return;
|
|
63
|
+
settled = true;
|
|
64
|
+
this.setupPathEngine();
|
|
65
|
+
clean();
|
|
66
|
+
resolve();
|
|
67
|
+
};
|
|
68
|
+
const onEarlyError = (err: Error) => {
|
|
69
|
+
if (settled) return;
|
|
70
|
+
settled = true;
|
|
71
|
+
clean();
|
|
72
|
+
reject(err);
|
|
73
|
+
};
|
|
74
|
+
const onEarlyEnd = (reason: string) => {
|
|
75
|
+
if (settled) return;
|
|
76
|
+
settled = true;
|
|
77
|
+
clean();
|
|
78
|
+
reject(new Error(`连接结束: ${reason}`));
|
|
79
|
+
};
|
|
80
|
+
const onEarlyKicked = (reason: string) => {
|
|
81
|
+
if (settled) return;
|
|
82
|
+
settled = true;
|
|
83
|
+
clean();
|
|
84
|
+
reject(new Error(`被服务器踢出: ${reason}`));
|
|
85
|
+
};
|
|
86
|
+
bot.once("spawn", onSpawn);
|
|
87
|
+
bot.once("error", onEarlyError);
|
|
88
|
+
bot.once("end", onEarlyEnd);
|
|
89
|
+
bot.once("kicked", onEarlyKicked);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private attachListeners(bot: Bot): void {
|
|
94
|
+
bot.on("spawn", () => {
|
|
95
|
+
this.setupPathEngine();
|
|
96
|
+
this.bus.emit("spawn");
|
|
97
|
+
if (!this.joinedOnce) {
|
|
98
|
+
this.joinedOnce = true;
|
|
99
|
+
this.scheduleJoinCommands();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
bot.on("chat", (username: string, message: string) => {
|
|
103
|
+
if (username === bot.username) return;
|
|
104
|
+
this.emitChat({ kind: "chat", username, text: message, at: Date.now() });
|
|
105
|
+
});
|
|
106
|
+
bot.on("whisper", (username: string, message: string) => {
|
|
107
|
+
if (username === bot.username) return;
|
|
108
|
+
this.emitChat({
|
|
109
|
+
kind: "whisper",
|
|
110
|
+
username,
|
|
111
|
+
text: message,
|
|
112
|
+
at: Date.now(),
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
bot.on("messagestr", (message: string) => {
|
|
116
|
+
const text = String(message || "").trim();
|
|
117
|
+
if (!text) return;
|
|
118
|
+
this.emitChat({ kind: "system", text, at: Date.now() });
|
|
119
|
+
});
|
|
120
|
+
bot.on("playerJoined", (player: any) => {
|
|
121
|
+
const name = player?.username;
|
|
122
|
+
if (!name || name === bot.username) return;
|
|
123
|
+
this.bus.emit("playerJoined", name);
|
|
124
|
+
this.emitChat({
|
|
125
|
+
kind: "join",
|
|
126
|
+
username: name,
|
|
127
|
+
text: `${name} 加入了游戏`,
|
|
128
|
+
at: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
bot.on("playerLeft", (player: any) => {
|
|
132
|
+
const name = player?.username;
|
|
133
|
+
if (!name || name === bot.username) return;
|
|
134
|
+
this.bus.emit("playerLeft", name);
|
|
135
|
+
this.emitChat({
|
|
136
|
+
kind: "left",
|
|
137
|
+
username: name,
|
|
138
|
+
text: `${name} 离开了游戏`,
|
|
139
|
+
at: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
bot.on("health", () => this.bus.emit("health"));
|
|
143
|
+
bot.on("death", () => {
|
|
144
|
+
this.bus.emit("death");
|
|
145
|
+
this.emitChat({
|
|
146
|
+
kind: "death",
|
|
147
|
+
username: bot.username,
|
|
148
|
+
text: `${bot.username} 死亡了`,
|
|
149
|
+
at: Date.now(),
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
bot.on("respawn", () => this.bus.emit("respawn"));
|
|
153
|
+
bot.on("entityHurt", (entity: any) => this.bus.emit("entityHurt", entity));
|
|
154
|
+
(bot as any).on("playerCollect", (collector: any) => {
|
|
155
|
+
if (collector?.id === bot.entity?.id) this.bus.emit("inventoryChanged");
|
|
156
|
+
});
|
|
157
|
+
(bot as any).on("heldItemChanged", () => this.bus.emit("inventoryChanged"));
|
|
158
|
+
(bot.inventory as any)?.on?.("updateSlot", () => this.bus.emit("inventoryChanged"));
|
|
159
|
+
bot.on("physicTick", () => this.bus.emit("physicTick"));
|
|
160
|
+
bot.on("kicked", (reason: string) =>
|
|
161
|
+
this.bus.emit("kicked", String(reason || "")),
|
|
162
|
+
);
|
|
163
|
+
bot.on("end", (reason: string) =>
|
|
164
|
+
this.bus.emit("end", String(reason || "")),
|
|
165
|
+
);
|
|
166
|
+
bot.on("error", (err: Error) => {
|
|
167
|
+
this.log(`bot 错误: ${err}`);
|
|
168
|
+
this.bus.emit("error", err);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private setupPathEngine(): void {
|
|
173
|
+
const bot = this.bot;
|
|
174
|
+
if (!bot) return;
|
|
175
|
+
try {
|
|
176
|
+
const movements: MovementsConfig = { ...DEFAULT_MOVEMENTS };
|
|
177
|
+
this.pathEngine = new PathEngine(bot, (m) => this.log(m));
|
|
178
|
+
this.pathEngine.setMovements(movements);
|
|
179
|
+
this.pathEngine.thinkTimeout = 5_000;
|
|
180
|
+
this.pathEngine.tickTimeout = 40;
|
|
181
|
+
(bot as any).pathEngine = this.pathEngine;
|
|
182
|
+
this.combat = new Combat(bot, movements);
|
|
183
|
+
this.combat.followRange = PVP_FOLLOW_RANGE;
|
|
184
|
+
this.combat.attackRange = PVP_ATTACK_RANGE;
|
|
185
|
+
(bot as any).combat = this.combat;
|
|
186
|
+
this.log(
|
|
187
|
+
`PathEngine + Combat 已初始化 canDig=${movements.canDig} parkour=${movements.allowParkour} sprint=${movements.allowSprinting} tower=${movements.allow1by1towers} thinkTimeout=${this.pathEngine.thinkTimeout}`,
|
|
188
|
+
);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
this.log(`初始化 PathEngine 失败: ${err}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getMovements(): MovementsConfig | undefined {
|
|
195
|
+
return this.pathEngine?.movements;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async waitForChunksLoaded(): Promise<void> {
|
|
199
|
+
const bot = this.bot;
|
|
200
|
+
if (!bot) return;
|
|
201
|
+
const deadline = Date.now() + 20_000;
|
|
202
|
+
try {
|
|
203
|
+
await withTimeoutMs((bot as any).waitForChunksToLoad(), 20_000);
|
|
204
|
+
} catch {
|
|
205
|
+
this.log("等待区块加载超时,继续确认脚下方块");
|
|
206
|
+
}
|
|
207
|
+
while (Date.now() < deadline) {
|
|
208
|
+
const feet = bot.entity?.position;
|
|
209
|
+
if (feet && bot.blockAt(feet) != null) {
|
|
210
|
+
this.log("区块加载完成(脚下方块就绪)");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
214
|
+
}
|
|
215
|
+
this.log("脚下区块仍未加载,物理 tick 可能跳过");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private scheduleJoinCommands(): void {
|
|
219
|
+
const cmds = this.server.joinCommands ?? [];
|
|
220
|
+
if (cmds.length === 0) return;
|
|
221
|
+
const run = () => {
|
|
222
|
+
const bot = this.bot;
|
|
223
|
+
if (!bot) return;
|
|
224
|
+
cmds.forEach((cmd, i) => {
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
try {
|
|
227
|
+
bot.chat(cmd);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
this.log(`执行加入命令失败 (${i}): ${e}`);
|
|
230
|
+
}
|
|
231
|
+
}, i * 1_000);
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
setTimeout(run, 2_000);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private emitChat(line: GameChatLine): void {
|
|
238
|
+
this.bus.emit("chat", line);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
chat(message: string): void {
|
|
242
|
+
const bot = this.bot;
|
|
243
|
+
if (!bot) return;
|
|
244
|
+
try {
|
|
245
|
+
bot.chat(message);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
this.log(`发送聊天失败: ${err}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async disconnect(reason = "leaving"): Promise<void> {
|
|
252
|
+
const bot = this.bot;
|
|
253
|
+
if (!bot) return;
|
|
254
|
+
try {
|
|
255
|
+
this.pathEngine?.stop();
|
|
256
|
+
} catch {
|
|
257
|
+
// ignore
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
bot.quit(reason);
|
|
261
|
+
} catch {
|
|
262
|
+
// ignore
|
|
263
|
+
}
|
|
264
|
+
await new Promise<void>((resolve) => {
|
|
265
|
+
const timer = setTimeout(() => resolve(), 2_000);
|
|
266
|
+
bot.once("end", () => {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
resolve();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
isOnline(): boolean {
|
|
274
|
+
return !!this.bot && !!(this.bot as any).entity;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
|
|
3
|
+
export interface GameChatLine {
|
|
4
|
+
kind: "chat" | "whisper" | "join" | "left" | "death" | "system";
|
|
5
|
+
username?: string;
|
|
6
|
+
text: string;
|
|
7
|
+
at: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PlayBusEvents {
|
|
11
|
+
spawn: () => void;
|
|
12
|
+
chat: (line: GameChatLine) => void;
|
|
13
|
+
health: () => void;
|
|
14
|
+
death: () => void;
|
|
15
|
+
respawn: () => void;
|
|
16
|
+
kicked: (reason: string) => void;
|
|
17
|
+
end: (reason: string) => void;
|
|
18
|
+
error: (err: Error) => void;
|
|
19
|
+
playerJoined: (username: string) => void;
|
|
20
|
+
playerLeft: (username: string) => void;
|
|
21
|
+
entityHurt: (entity: any) => void;
|
|
22
|
+
inventoryChanged: () => void;
|
|
23
|
+
physicTick: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type PlayBusEvent = keyof PlayBusEvents;
|
|
27
|
+
|
|
28
|
+
export class PlayBus {
|
|
29
|
+
private emitter = new EventEmitter();
|
|
30
|
+
|
|
31
|
+
emit<K extends PlayBusEvent>(event: K, ...args: any[]): void {
|
|
32
|
+
this.emitter.emit(event, ...args);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
on<K extends PlayBusEvent>(event: K, listener: PlayBusEvents[K]): this {
|
|
36
|
+
this.emitter.on(event, listener as (...args: any[]) => void);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
off<K extends PlayBusEvent>(event: K, listener: PlayBusEvents[K]): this {
|
|
41
|
+
this.emitter.off(event, listener as (...args: any[]) => void);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
removeAll(): void {
|
|
46
|
+
this.emitter.removeAllListeners();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Bot } from "mineflayer";
|
|
2
|
+
import { GoalFollow, type MovementsConfig } from "../path-engine";
|
|
3
|
+
import { entityName } from "../util/entities";
|
|
4
|
+
import { hasShield } from "../util/inventory";
|
|
5
|
+
|
|
6
|
+
const SHIELD_BLOCK_DURATION_MS = 2000;
|
|
7
|
+
const PRE_ATTACK_DELAY_MS = 50;
|
|
8
|
+
const POST_ATTACK_DELAY_MS = 50;
|
|
9
|
+
const ATTACK_COOLDOWN_TICKS = 12;
|
|
10
|
+
const REEQUIP_INTERVAL_MS = 3000;
|
|
11
|
+
|
|
12
|
+
export class Combat {
|
|
13
|
+
private readonly bot: Bot;
|
|
14
|
+
private target: any = null;
|
|
15
|
+
private attacking = false;
|
|
16
|
+
private timeToNextAttack = 0;
|
|
17
|
+
private lastShieldBlockAt = 0;
|
|
18
|
+
private lastEquipAt = 0;
|
|
19
|
+
private resolveAttack: (() => void) | null = null;
|
|
20
|
+
|
|
21
|
+
movements: MovementsConfig;
|
|
22
|
+
followRange = 2;
|
|
23
|
+
attackRange = 3.5;
|
|
24
|
+
viewDistance = 128;
|
|
25
|
+
|
|
26
|
+
constructor(bot: Bot, movements: MovementsConfig) {
|
|
27
|
+
this.bot = bot;
|
|
28
|
+
this.movements = movements;
|
|
29
|
+
bot.on("physicTick", () => this.update());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
attack(target: any): Promise<void> {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
if (this.target && this.target !== target) {
|
|
35
|
+
this.stop();
|
|
36
|
+
}
|
|
37
|
+
this.target = target;
|
|
38
|
+
this.attacking = false;
|
|
39
|
+
this.timeToNextAttack = 0;
|
|
40
|
+
this.lastShieldBlockAt = 0;
|
|
41
|
+
this.lastEquipAt = 0;
|
|
42
|
+
this.resolveAttack = resolve;
|
|
43
|
+
|
|
44
|
+
void this.equipBestWeapon();
|
|
45
|
+
const engine = (this.bot as any).pathEngine;
|
|
46
|
+
if (engine) {
|
|
47
|
+
try {
|
|
48
|
+
engine.setGoal(new GoalFollow(target, this.followRange), true);
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
stop(): void {
|
|
57
|
+
if (this.resolveAttack) {
|
|
58
|
+
this.resolveAttack();
|
|
59
|
+
this.resolveAttack = null;
|
|
60
|
+
}
|
|
61
|
+
this.target = null;
|
|
62
|
+
this.attacking = false;
|
|
63
|
+
const engine = (this.bot as any).pathEngine;
|
|
64
|
+
if (engine) {
|
|
65
|
+
try {
|
|
66
|
+
engine.setGoal(null);
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (this.lastShieldBlockAt > 0) {
|
|
72
|
+
try {
|
|
73
|
+
(this.bot as any).deactivateItem();
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
this.lastShieldBlockAt = 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isInCombat(): boolean {
|
|
82
|
+
return this.target !== null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getTarget(): any | null {
|
|
86
|
+
return this.target;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async equipBestWeapon(): Promise<void> {
|
|
90
|
+
try {
|
|
91
|
+
const items = this.bot.inventory?.items?.() ?? [];
|
|
92
|
+
const sword = items.find((i: any) => /_sword$/.test(i.name));
|
|
93
|
+
const axe = items.find((i: any) => /_axe$/.test(i.name) && !/pickaxe/.test(i.name));
|
|
94
|
+
const best = sword ?? axe;
|
|
95
|
+
if (best) {
|
|
96
|
+
const held = this.bot.heldItem;
|
|
97
|
+
if (!held || held.type !== best.type) {
|
|
98
|
+
await this.bot.equip(best, "hand");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// ignore
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private update(): void {
|
|
107
|
+
if (!this.target) return;
|
|
108
|
+
const entity = this.bot.entity;
|
|
109
|
+
if (!entity) return;
|
|
110
|
+
|
|
111
|
+
if (!this.target.position) {
|
|
112
|
+
this.stop();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const targetPos = this.target.position;
|
|
117
|
+
const dist = Math.sqrt(
|
|
118
|
+
(targetPos.x - entity.position.x) ** 2 +
|
|
119
|
+
(targetPos.y - entity.position.y) ** 2 +
|
|
120
|
+
(targetPos.z - entity.position.z) ** 2,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (dist > this.viewDistance) {
|
|
124
|
+
this.stop();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.handleShield(targetPos, dist);
|
|
129
|
+
|
|
130
|
+
if (dist <= this.attackRange) {
|
|
131
|
+
if (!this.attacking && this.timeToNextAttack <= 0) {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
if (now - this.lastEquipAt > REEQUIP_INTERVAL_MS) {
|
|
134
|
+
this.lastEquipAt = now;
|
|
135
|
+
void this.equipBestWeapon();
|
|
136
|
+
}
|
|
137
|
+
this.attacking = true;
|
|
138
|
+
this.attemptAttack(targetPos);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
this.attacking = false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (this.timeToNextAttack > 0) this.timeToNextAttack--;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private handleShield(targetPos: any, _dist: number): void {
|
|
148
|
+
if (entityName(this.target) !== "creeper") {
|
|
149
|
+
if (this.lastShieldBlockAt > 0 && Date.now() - this.lastShieldBlockAt > SHIELD_BLOCK_DURATION_MS) {
|
|
150
|
+
try {
|
|
151
|
+
(this.bot as any).deactivateItem();
|
|
152
|
+
} catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
this.lastShieldBlockAt = 0;
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fuseActive = this.target.metadata?.[16] === 1;
|
|
161
|
+
if (fuseActive && hasShield(this.bot) && this.lastShieldBlockAt === 0) {
|
|
162
|
+
this.lastShieldBlockAt = Date.now();
|
|
163
|
+
try {
|
|
164
|
+
(this.bot as any).pathEngine?.stop();
|
|
165
|
+
(this.bot as any).lookAt(targetPos.offset(0, 1, 0), true);
|
|
166
|
+
(this.bot as any).activateItem(true);
|
|
167
|
+
} catch {
|
|
168
|
+
// ignore
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (this.lastShieldBlockAt > 0 && Date.now() - this.lastShieldBlockAt > SHIELD_BLOCK_DURATION_MS) {
|
|
174
|
+
try {
|
|
175
|
+
(this.bot as any).deactivateItem();
|
|
176
|
+
} catch {
|
|
177
|
+
// ignore
|
|
178
|
+
}
|
|
179
|
+
this.lastShieldBlockAt = 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async attemptAttack(targetPos: any): Promise<void> {
|
|
184
|
+
const target = this.target;
|
|
185
|
+
if (!target) {
|
|
186
|
+
this.attacking = false;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const shield = hasShield(this.bot);
|
|
190
|
+
try {
|
|
191
|
+
if (shield) {
|
|
192
|
+
try {
|
|
193
|
+
(this.bot as any).deactivateItem();
|
|
194
|
+
} catch {
|
|
195
|
+
// ignore
|
|
196
|
+
}
|
|
197
|
+
await new Promise((r) => setTimeout(r, PRE_ATTACK_DELAY_MS));
|
|
198
|
+
}
|
|
199
|
+
const height = target.height ?? 1;
|
|
200
|
+
try {
|
|
201
|
+
await (this.bot as any).lookAt(targetPos.offset(0, height, 0), true);
|
|
202
|
+
} catch {
|
|
203
|
+
// ignore
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await (this.bot as any).attack(target);
|
|
207
|
+
} catch {
|
|
208
|
+
// ignore
|
|
209
|
+
}
|
|
210
|
+
if (shield) {
|
|
211
|
+
await new Promise((r) => setTimeout(r, POST_ATTACK_DELAY_MS));
|
|
212
|
+
try {
|
|
213
|
+
(this.bot as any).activateItem(true);
|
|
214
|
+
} catch {
|
|
215
|
+
// ignore
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
this.timeToNextAttack = ATTACK_COOLDOWN_TICKS;
|
|
219
|
+
} catch {
|
|
220
|
+
// ignore
|
|
221
|
+
} finally {
|
|
222
|
+
this.attacking = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./combat";
|
package/play/config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ConfigService } from "mioku";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PLAY_CONFIG,
|
|
4
|
+
normalizePlayConfig,
|
|
5
|
+
type GroupBinding,
|
|
6
|
+
type PlayConfig,
|
|
7
|
+
type PlayServerConfig,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
export function createPlayConfigHandler(configService: ConfigService | undefined) {
|
|
11
|
+
let currentConfig: PlayConfig = { ...DEFAULT_PLAY_CONFIG };
|
|
12
|
+
|
|
13
|
+
const register = async () => {
|
|
14
|
+
if (!configService) return;
|
|
15
|
+
await configService.registerConfig("mc", "play", DEFAULT_PLAY_CONFIG);
|
|
16
|
+
const raw = await configService.getConfig("mc", "play");
|
|
17
|
+
currentConfig = normalizePlayConfig(raw);
|
|
18
|
+
configService.onConfigChange("mc", "play", (next) => {
|
|
19
|
+
currentConfig = normalizePlayConfig(next);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getConfig = () => currentConfig;
|
|
24
|
+
|
|
25
|
+
const findServer = (serverId: string): PlayServerConfig | null => {
|
|
26
|
+
const id = String(serverId ?? "").trim();
|
|
27
|
+
return currentConfig.servers.find((s) => s.id === id) ?? null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const findBinding = (groupId: number | string): GroupBinding | null => {
|
|
31
|
+
const gid = Number(groupId);
|
|
32
|
+
if (!Number.isFinite(gid) || gid <= 0) return null;
|
|
33
|
+
return currentConfig.groups.find((g) => g.groupId === gid) ?? null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return { register, getConfig, findServer, findBinding };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PlayConfigHandler = ReturnType<typeof createPlayConfigHandler>;
|
package/play/context.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MiokiContext } from "mioki";
|
|
2
|
+
import type { AIService, AIInstance, ConfigService } from "mioku";
|
|
3
|
+
import type { PlayConfig } from "./types";
|
|
4
|
+
import type { ConfigHandler } from "../utils/config-handler";
|
|
5
|
+
import type { WorkSubroutine, WorkTerminator } from "./ai/work-subroutine";
|
|
6
|
+
import type { PlaySession } from "./session";
|
|
7
|
+
|
|
8
|
+
export interface PlayPluginContext {
|
|
9
|
+
ctx: MiokiContext;
|
|
10
|
+
config: PlayConfig;
|
|
11
|
+
aiService: AIService | undefined;
|
|
12
|
+
configService: ConfigService | undefined;
|
|
13
|
+
syncConfigHandler: ConfigHandler;
|
|
14
|
+
mainInstance: AIInstance | undefined;
|
|
15
|
+
workInstance: AIInstance | undefined;
|
|
16
|
+
getPlayConfig: () => PlayConfig;
|
|
17
|
+
refreshInstances: () => void;
|
|
18
|
+
createWorkSubroutine: (opts: {
|
|
19
|
+
session: PlaySession;
|
|
20
|
+
goal: string;
|
|
21
|
+
terminator: WorkTerminator;
|
|
22
|
+
maxMs?: number;
|
|
23
|
+
maxIterations?: number;
|
|
24
|
+
}) => WorkSubroutine;
|
|
25
|
+
notifyChatScan?: () => void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# mc play 调试命令
|
|
2
|
+
|
|
3
|
+
> 仅当 `config/mc/play.json` 的 `debug.enabled = true` 且发送者为 bot 主人(owner)时生效。
|
|
4
|
+
> 这些命令不注册到帮助系统,仅供调试/测试行为引擎使用,不会触发 AI 循环。
|
|
5
|
+
|
|
6
|
+
正常模式下 AI 采用事件驱动:稳定执行任务时不会周期调用 Working AI;新主指令、受伤、昼夜变化或任务失败才会唤醒它。QQ 消息只进入 MC Main AI 的下次上下文,不单独触发,也不会由 MC Main AI 回复。
|
|
7
|
+
|
|
8
|
+
## 开关
|
|
9
|
+
|
|
10
|
+
在 `config/mc/play.json` 中:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "debug": { "enabled": true } }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
开启后,bot 主人在**已配置游玩绑定的群**里发送以下命令即可。`/join` 进入后**不会启动主模型循环**,bot 只连接服务器并保持 idle 行为;主人用命令手动操控。
|
|
17
|
+
|
|
18
|
+
## 命令清单
|
|
19
|
+
|
|
20
|
+
| 命令 | 作用 | 示例 |
|
|
21
|
+
|------|------|------|
|
|
22
|
+
| `/join <服务器ID>` | 以 debug 模式进入服务器(无 AI 循环) | `/join survival` |
|
|
23
|
+
| `/play <服务器ID>` | 进入服务器并启动完整 AI 循环(等同主人让 bot 正常进服) | `/play survival` |
|
|
24
|
+
| `/exit` | 离开当前服务器 | `/exit` |
|
|
25
|
+
| `/say <文本>` | 让 bot 在游戏内发言 | `/say 大家好~` |
|
|
26
|
+
| `/motion <移动行为> [key=value ...]` | 设置移动行为 | `/motion follow target=Steve distance=3` |
|
|
27
|
+
| `/motion <defend\|auto_eat> [key=...]` | 切换叠加状态(开/关) | `/motion defend radius=10` |
|
|
28
|
+
| `/stop` | 停止移动,回 idle(叠加状态保留) | `/stop` |
|
|
29
|
+
| `/clear` | 清空所有状态(移动 + 叠加) | `/clear` |
|
|
30
|
+
| `/off <名称>` | 关闭指定叠加状态 | `/off defend` |
|
|
31
|
+
| `/status` | 查看启用的状态 & 正在执行的行为 | `/status` |
|
|
32
|
+
| `/behaviors` | 列出所有行为及参数 | `/behaviors` |
|
|
33
|
+
| `/actions` | 列出 Working AI 可用的一次性动作 | `/actions` |
|
|
34
|
+
|
|
35
|
+
行为系统的并发模型、每个状态的含义、参数说明详见 [`play/behavior/README.md`](../behavior/README.md)。
|
|
36
|
+
|
|
37
|
+
## 移动行为(`/motion`,同时只能有一个)
|
|
38
|
+
|
|
39
|
+
- `idle`
|
|
40
|
+
- `follow target=<玩家名> [distance=<格>]`
|
|
41
|
+
- `gather resource=<wood|stone|coal|iron>`
|
|
42
|
+
- `farm_mobs`
|
|
43
|
+
- `explore`
|
|
44
|
+
- `approach_player target=<玩家名> [distance=<格>]`
|
|
45
|
+
- `seek_shelter`
|
|
46
|
+
|
|
47
|
+
## 叠加状态(`/motion`,可同时开多个,按优先级抢占移动)
|
|
48
|
+
|
|
49
|
+
- `defend [radius=<格, 默认8>]` - 自动战斗,敌对生物靠近时抢占移动去攻击
|
|
50
|
+
- `auto_eat` - 自动进食,饥饿且有食物且不在战斗时进食
|
|
51
|
+
|
|
52
|
+
生存层(escape_lava / mlg_fall / flee_creeper / escape_water)常驻,无需开启。
|
|
53
|
+
|
|
54
|
+
### 示例:同时跟随 + 自动战斗 + 自动进食
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
/join survival
|
|
58
|
+
/motion auto_eat # 开自动进食
|
|
59
|
+
/motion defend # 开自动战斗
|
|
60
|
+
/motion follow target=Steve distance=3 # 跟随 Steve
|
|
61
|
+
/status # 看三个状态都启用了
|
|
62
|
+
/stop # 停跟随(叠加保留)
|
|
63
|
+
/clear # 全清
|
|
64
|
+
/exit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 说明
|
|
68
|
+
|
|
69
|
+
- `/join` 走的是 debug 入口:bot 连接服务器、启动行为引擎与生存层(岩浆/MLG/苦力怕/溺水/饥饿仍会自动抢占保护),但**不启动主模型循环和工作模型**。所以 bot 不会自己说话或决策,全靠主人的命令。
|
|
70
|
+
- `/play` 走的是正常入口:bot 连接服务器并启动完整的 main + work AI 循环(与 QQ 触发 `control_bot` 进服效果一致)。**仅在调试时观察 AI 实际行为**时使用;生产场景请走 `control_bot` 工具。
|
|
71
|
+
- `/say` 和 `/motion` 需要已有一个进行中的会话(先 `/join` 或 `/play`)。bot 尚未连接完成时会回复"bot 尚未连接到服务器"。
|
|
72
|
+
- 生存层在 debug 模式下仍然生效——例如测试 `follow` 时若苦力怕靠近,会自动撤离。
|
|
73
|
+
- 正常的 AI 入口(chat 主模型调用 `mc.control_bot` 工具)与 debug 互不影响;AI 入口会启动完整循环。
|
|
74
|
+
- 关闭 debug 后这些命令不再被拦截,会当作普通群消息处理。
|