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
package/play/types.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
export interface PlayServerConfig {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
host: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
username: string;
|
|
7
|
+
auth?: "offline" | "microsoft";
|
|
8
|
+
password?: string;
|
|
9
|
+
maxPlayMs: number;
|
|
10
|
+
joinCommands: string[];
|
|
11
|
+
allowedCommands: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GroupBinding {
|
|
15
|
+
groupId: number;
|
|
16
|
+
botSelfId: number;
|
|
17
|
+
allowedServerIds: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type PlayToolPermission = "owner" | "admin" | "member";
|
|
21
|
+
|
|
22
|
+
export interface WorkStatus {
|
|
23
|
+
running: boolean;
|
|
24
|
+
goal: string | null;
|
|
25
|
+
summary: string;
|
|
26
|
+
progress?: { current: number; target: number; unit: string };
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PlayConfig {
|
|
31
|
+
servers: PlayServerConfig[];
|
|
32
|
+
groups: GroupBinding[];
|
|
33
|
+
mainChatDebounceMs: number;
|
|
34
|
+
mainConversationFocusMs: number;
|
|
35
|
+
chatScanIntervalMs: number;
|
|
36
|
+
workSubroutineMaxMs: number;
|
|
37
|
+
workSubroutineMaxIterations: number;
|
|
38
|
+
toolPermission: PlayToolPermission;
|
|
39
|
+
behaviorTickIntervalMs: number;
|
|
40
|
+
goodbyeTimeoutMs: number;
|
|
41
|
+
qqSendPerMinute: number;
|
|
42
|
+
gameChatMinIntervalMs: number;
|
|
43
|
+
debug: { enabled: boolean };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_PLAY_CONFIG: PlayConfig = {
|
|
47
|
+
servers: [],
|
|
48
|
+
groups: [],
|
|
49
|
+
mainChatDebounceMs: 1_000,
|
|
50
|
+
mainConversationFocusMs: 30_000,
|
|
51
|
+
chatScanIntervalMs: 4 * 60_000,
|
|
52
|
+
workSubroutineMaxMs: 5 * 60_000,
|
|
53
|
+
workSubroutineMaxIterations: 25,
|
|
54
|
+
toolPermission: "admin",
|
|
55
|
+
behaviorTickIntervalMs: 200,
|
|
56
|
+
goodbyeTimeoutMs: 8_000,
|
|
57
|
+
qqSendPerMinute: 3,
|
|
58
|
+
gameChatMinIntervalMs: 1_500,
|
|
59
|
+
debug: { enabled: false },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export interface MovementInit {
|
|
63
|
+
name: string;
|
|
64
|
+
params?: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type MainLoopTrigger =
|
|
68
|
+
| "direct_game_chat"
|
|
69
|
+
| "direct_qq_chat"
|
|
70
|
+
| "chat_scan_due"
|
|
71
|
+
| "work_completed";
|
|
72
|
+
|
|
73
|
+
export interface PlaySessionStatus {
|
|
74
|
+
serverId: string;
|
|
75
|
+
serverName: string;
|
|
76
|
+
groupId: number;
|
|
77
|
+
botSelfId: number;
|
|
78
|
+
startedAt: number;
|
|
79
|
+
connected: boolean;
|
|
80
|
+
currentBehavior: string | null;
|
|
81
|
+
lastAction: string | null;
|
|
82
|
+
workStatus?: WorkStatus | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type PlayEventType =
|
|
86
|
+
| "game_chat"
|
|
87
|
+
| "qq_chat"
|
|
88
|
+
| "damage"
|
|
89
|
+
| "vitals_threshold"
|
|
90
|
+
| "day_phase"
|
|
91
|
+
| "death"
|
|
92
|
+
| "respawn"
|
|
93
|
+
| "inventory_change"
|
|
94
|
+
| "equipment_change"
|
|
95
|
+
| "mission_outcome"
|
|
96
|
+
| "action_outcome"
|
|
97
|
+
| "path_error"
|
|
98
|
+
| "chat_scan_due"
|
|
99
|
+
| "work_completed";
|
|
100
|
+
|
|
101
|
+
export interface PlayEvent<T = unknown> {
|
|
102
|
+
seq: number;
|
|
103
|
+
at: number;
|
|
104
|
+
type: PlayEventType;
|
|
105
|
+
data: T;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface EventBatch {
|
|
109
|
+
events: PlayEvent[];
|
|
110
|
+
cursor: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class PlayEventJournal {
|
|
114
|
+
private seq = 0;
|
|
115
|
+
private events: PlayEvent[] = [];
|
|
116
|
+
private listeners = new Set<(event: PlayEvent) => void>();
|
|
117
|
+
|
|
118
|
+
constructor(private readonly maxEntries = 300) {}
|
|
119
|
+
|
|
120
|
+
append<T>(type: PlayEventType, data: T): PlayEvent<T> {
|
|
121
|
+
const event: PlayEvent<T> = {
|
|
122
|
+
seq: ++this.seq,
|
|
123
|
+
at: Date.now(),
|
|
124
|
+
type,
|
|
125
|
+
data,
|
|
126
|
+
};
|
|
127
|
+
this.events.push(event);
|
|
128
|
+
if (this.events.length > this.maxEntries) {
|
|
129
|
+
this.events.splice(0, this.events.length - this.maxEntries);
|
|
130
|
+
}
|
|
131
|
+
for (const listener of this.listeners) {
|
|
132
|
+
try {
|
|
133
|
+
listener(event);
|
|
134
|
+
} catch {
|
|
135
|
+
// Journal consumers must not affect the game loop.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return event;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
readAfter(
|
|
142
|
+
cursor: number,
|
|
143
|
+
filter?: (event: PlayEvent) => boolean,
|
|
144
|
+
limit?: number,
|
|
145
|
+
): EventBatch {
|
|
146
|
+
const matched = this.events.filter(
|
|
147
|
+
(event) => event.seq > cursor && (!filter || filter(event)),
|
|
148
|
+
);
|
|
149
|
+
return {
|
|
150
|
+
events: limit && matched.length > limit ? matched.slice(-limit) : matched,
|
|
151
|
+
cursor: matched.at(-1)?.seq ?? cursor,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
subscribe(listener: (event: PlayEvent) => void): () => void {
|
|
156
|
+
this.listeners.add(listener);
|
|
157
|
+
return () => this.listeners.delete(listener);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
latestCursor(): number {
|
|
161
|
+
return this.seq;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
clear(): void {
|
|
165
|
+
this.events = [];
|
|
166
|
+
this.listeners.clear();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function asStringList(value: unknown): string[] {
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
return value
|
|
173
|
+
.map((v) => String(v ?? "").trim())
|
|
174
|
+
.filter((v) => v.length > 0);
|
|
175
|
+
}
|
|
176
|
+
if (typeof value === "string") {
|
|
177
|
+
return value
|
|
178
|
+
.split(/[\n,]+/)
|
|
179
|
+
.map((v) => v.trim())
|
|
180
|
+
.filter((v) => v.length > 0);
|
|
181
|
+
}
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function asNumber(value: unknown, fallback: number): number {
|
|
186
|
+
const n = Number(value);
|
|
187
|
+
return Number.isFinite(n) ? n : fallback;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function asPositiveNumber(value: unknown, fallback: number): number {
|
|
191
|
+
const n = Number(value);
|
|
192
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeServer(raw: any): PlayServerConfig {
|
|
196
|
+
const auth = raw?.auth === "microsoft" ? "microsoft" : "offline";
|
|
197
|
+
return {
|
|
198
|
+
id: String(raw?.id ?? "").trim(),
|
|
199
|
+
name: String(raw?.name ?? raw?.id ?? "").trim(),
|
|
200
|
+
host: String(raw?.host ?? "").trim(),
|
|
201
|
+
version: raw?.version ? String(raw.version).trim() : undefined,
|
|
202
|
+
username: String(raw?.username ?? "").trim(),
|
|
203
|
+
auth,
|
|
204
|
+
password: raw?.password ? String(raw.password) : undefined,
|
|
205
|
+
maxPlayMs: asPositiveNumber(raw?.maxPlayMs, 30 * 60_000),
|
|
206
|
+
joinCommands: asStringList(raw?.joinCommands),
|
|
207
|
+
allowedCommands: asStringList(raw?.allowedCommands).map((command) =>
|
|
208
|
+
command.startsWith("/") ? command : `/${command}`,
|
|
209
|
+
),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeBinding(raw: any): GroupBinding {
|
|
214
|
+
return {
|
|
215
|
+
groupId: asNumber(raw?.groupId ?? raw?.group_id, 0),
|
|
216
|
+
botSelfId: asNumber(raw?.botSelfId ?? raw?.bot_self_id, 0),
|
|
217
|
+
allowedServerIds: asStringList(raw?.allowedServerIds ?? raw?.allowed_server_ids),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function normalizePlayConfig(raw: any): PlayConfig {
|
|
222
|
+
const servers = Array.isArray(raw?.servers)
|
|
223
|
+
? raw.servers.map(normalizeServer).filter((s: PlayServerConfig) => s.id && s.host)
|
|
224
|
+
: [];
|
|
225
|
+
const groups = Array.isArray(raw?.groups)
|
|
226
|
+
? raw?.groups
|
|
227
|
+
.map(normalizeBinding)
|
|
228
|
+
.filter((g: GroupBinding) => g.groupId > 0 && g.botSelfId > 0)
|
|
229
|
+
: [];
|
|
230
|
+
const perm: PlayToolPermission =
|
|
231
|
+
raw?.toolPermission === "owner" ||
|
|
232
|
+
raw?.toolPermission === "admin" ||
|
|
233
|
+
raw?.toolPermission === "member"
|
|
234
|
+
? raw.toolPermission
|
|
235
|
+
: DEFAULT_PLAY_CONFIG.toolPermission;
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
servers,
|
|
239
|
+
groups,
|
|
240
|
+
mainChatDebounceMs: asPositiveNumber(
|
|
241
|
+
raw?.mainChatDebounceMs,
|
|
242
|
+
DEFAULT_PLAY_CONFIG.mainChatDebounceMs,
|
|
243
|
+
),
|
|
244
|
+
mainConversationFocusMs: asPositiveNumber(
|
|
245
|
+
raw?.mainConversationFocusMs,
|
|
246
|
+
DEFAULT_PLAY_CONFIG.mainConversationFocusMs,
|
|
247
|
+
),
|
|
248
|
+
chatScanIntervalMs: asPositiveNumber(
|
|
249
|
+
raw?.chatScanIntervalMs,
|
|
250
|
+
DEFAULT_PLAY_CONFIG.chatScanIntervalMs,
|
|
251
|
+
),
|
|
252
|
+
workSubroutineMaxMs: asPositiveNumber(
|
|
253
|
+
raw?.workSubroutineMaxMs,
|
|
254
|
+
DEFAULT_PLAY_CONFIG.workSubroutineMaxMs,
|
|
255
|
+
),
|
|
256
|
+
workSubroutineMaxIterations: asPositiveNumber(
|
|
257
|
+
raw?.workSubroutineMaxIterations,
|
|
258
|
+
DEFAULT_PLAY_CONFIG.workSubroutineMaxIterations,
|
|
259
|
+
),
|
|
260
|
+
toolPermission: perm,
|
|
261
|
+
behaviorTickIntervalMs: asPositiveNumber(
|
|
262
|
+
raw?.behaviorTickIntervalMs,
|
|
263
|
+
DEFAULT_PLAY_CONFIG.behaviorTickIntervalMs,
|
|
264
|
+
),
|
|
265
|
+
goodbyeTimeoutMs: asPositiveNumber(
|
|
266
|
+
raw?.goodbyeTimeoutMs,
|
|
267
|
+
DEFAULT_PLAY_CONFIG.goodbyeTimeoutMs,
|
|
268
|
+
),
|
|
269
|
+
qqSendPerMinute: asPositiveNumber(
|
|
270
|
+
raw?.qqSendPerMinute,
|
|
271
|
+
DEFAULT_PLAY_CONFIG.qqSendPerMinute,
|
|
272
|
+
),
|
|
273
|
+
gameChatMinIntervalMs: asPositiveNumber(
|
|
274
|
+
raw?.gameChatMinIntervalMs,
|
|
275
|
+
DEFAULT_PLAY_CONFIG.gameChatMinIntervalMs,
|
|
276
|
+
),
|
|
277
|
+
debug: {
|
|
278
|
+
enabled:
|
|
279
|
+
raw?.debug && typeof raw.debug === "object"
|
|
280
|
+
? Boolean(raw.debug.enabled)
|
|
281
|
+
: DEFAULT_PLAY_CONFIG.debug.enabled,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export async function withTimeoutMs<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
2
|
+
let timer: NodeJS.Timeout | undefined;
|
|
3
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
4
|
+
timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
|
|
5
|
+
});
|
|
6
|
+
try {
|
|
7
|
+
return await Promise.race([promise, timeout]);
|
|
8
|
+
} finally {
|
|
9
|
+
if (timer) clearTimeout(timer);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { promises as dns } from "dns";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PORT = 25565;
|
|
4
|
+
|
|
5
|
+
function parseHostPort(input: string): { host: string; port?: number } {
|
|
6
|
+
const host = input.trim();
|
|
7
|
+
const lastColon = host.lastIndexOf(":");
|
|
8
|
+
if (lastColon > 0 && lastColon < host.length - 1) {
|
|
9
|
+
const suffix = host.slice(lastColon + 1);
|
|
10
|
+
const n = Number(suffix);
|
|
11
|
+
if (Number.isInteger(n) && n > 0 && n <= 65535) {
|
|
12
|
+
return { host: host.slice(0, lastColon), port: n };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return { host };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function resolveMinecraftEndpoint(
|
|
19
|
+
input: string,
|
|
20
|
+
defaultPort: number = DEFAULT_PORT,
|
|
21
|
+
): Promise<{ host: string; port: number }> {
|
|
22
|
+
const { host: rawHost, port: explicitPort } = parseHostPort(input);
|
|
23
|
+
if (explicitPort !== undefined) {
|
|
24
|
+
return { host: rawHost, port: explicitPort };
|
|
25
|
+
}
|
|
26
|
+
const srvName = rawHost.startsWith("_minecraft._tcp.")
|
|
27
|
+
? rawHost
|
|
28
|
+
: `_minecraft._tcp.${rawHost}`;
|
|
29
|
+
try {
|
|
30
|
+
const records = await dns.resolveSrv(srvName);
|
|
31
|
+
if (records && records.length > 0) {
|
|
32
|
+
return { host: records[0].name, port: records[0].port };
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// no SRV record; fall back to direct connection
|
|
36
|
+
}
|
|
37
|
+
return { host: rawHost, port: defaultPort };
|
|
38
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const HOSTILE_MOBS = new Set([
|
|
2
|
+
"zombie", "husk", "drowned", "skeleton", "stray", "creeper", "spider", "cave_spider",
|
|
3
|
+
"enderman", "witch", "blaze", "ghast", "magma_cube", "slime", "phantom", "pillager",
|
|
4
|
+
"vindicator", "evoker", "ravager", "hoglin", "zoglin", "warden", "wither",
|
|
5
|
+
"ender_dragon", "shulker", "silverfish", "guardian", "elder_guardian", "piglin_brute",
|
|
6
|
+
"piglin", "zombified_piglin",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export function entityName(entity: any): string {
|
|
10
|
+
if (!entity) return "unknown";
|
|
11
|
+
const raw = entity.name || entity.entityType || entity.kind || "unknown";
|
|
12
|
+
return String(raw).toLowerCase().replace(/^minecraft:/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isHostileEntity(entity: any): boolean {
|
|
16
|
+
return HOSTILE_MOBS.has(entityName(entity));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isPassiveMob(entity: any): boolean {
|
|
20
|
+
const name = entityName(entity);
|
|
21
|
+
const passive = [
|
|
22
|
+
"cow", "pig", "sheep", "chicken", "rabbit", "horse", "donkey", "mule",
|
|
23
|
+
"villager", "iron_golem", "snow_golem", "cat", "ocelot", "wolf", "parrot",
|
|
24
|
+
"fox", "bee", "turtle", "panda", "llama", "trader_llama", "wandering_trader",
|
|
25
|
+
"mooshroom", "goat", "axolotl", "allay", "frog", "tadpole", "sniffer",
|
|
26
|
+
];
|
|
27
|
+
return passive.includes(name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function entityDistance(a: any, b: any): number {
|
|
31
|
+
if (!a?.position || !b?.position) return Infinity;
|
|
32
|
+
return Math.hypot(
|
|
33
|
+
a.position.x - b.position.x,
|
|
34
|
+
a.position.y - b.position.y,
|
|
35
|
+
a.position.z - b.position.z,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function listNearbyHostiles(bot: any, radius = 16): string[] {
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
const pos = bot?.entity?.position;
|
|
42
|
+
if (!pos) return out;
|
|
43
|
+
for (const id in bot.entities) {
|
|
44
|
+
const e = bot.entities[id];
|
|
45
|
+
if (e === bot.entity) continue;
|
|
46
|
+
if (!isHostileEntity(e)) continue;
|
|
47
|
+
const d = entityDistance({ position: pos }, e);
|
|
48
|
+
if (d <= radius) out.push(`${entityName(e)}@${d.toFixed(0)}`);
|
|
49
|
+
}
|
|
50
|
+
return out.slice(0, 8);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function listNearbyPlayers(bot: any, radius = 16): string[] {
|
|
54
|
+
const out: string[] = [];
|
|
55
|
+
const pos = bot?.entity?.position;
|
|
56
|
+
if (!pos) return out;
|
|
57
|
+
for (const name in bot.players) {
|
|
58
|
+
const p = bot.players[name];
|
|
59
|
+
if (!p || name === bot.username) continue;
|
|
60
|
+
const d = entityDistance({ position: pos }, p.entity);
|
|
61
|
+
if (d <= radius) out.push(`${name}@${d.toFixed(0)}`);
|
|
62
|
+
}
|
|
63
|
+
return out.slice(0, 8);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function nearestHostile(bot: any, radius = 16): any | null {
|
|
67
|
+
let best: any = null;
|
|
68
|
+
let bestDist = Infinity;
|
|
69
|
+
const pos = bot?.entity?.position;
|
|
70
|
+
if (!pos) return null;
|
|
71
|
+
for (const id in bot.entities) {
|
|
72
|
+
const e = bot.entities[id];
|
|
73
|
+
if (e === bot.entity || !isHostileEntity(e)) continue;
|
|
74
|
+
const d = entityDistance({ position: pos }, e);
|
|
75
|
+
if (d <= radius && d < bestDist) {
|
|
76
|
+
best = e;
|
|
77
|
+
bestDist = d;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return best;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function nearestPassiveMob(bot: any, radius = 20): any | null {
|
|
84
|
+
let best: any = null;
|
|
85
|
+
let bestDist = Infinity;
|
|
86
|
+
const pos = bot?.entity?.position;
|
|
87
|
+
if (!pos) return null;
|
|
88
|
+
for (const id in bot.entities) {
|
|
89
|
+
const e = bot.entities[id];
|
|
90
|
+
if (e === bot.entity || !isPassiveMob(e)) continue;
|
|
91
|
+
if (e.name === "player") continue;
|
|
92
|
+
const d = entityDistance({ position: pos }, e);
|
|
93
|
+
if (d <= radius && d < bestDist) {
|
|
94
|
+
best = e;
|
|
95
|
+
bestDist = d;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return best;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function nearestCreeper(bot: any, radius = 6): any | null {
|
|
102
|
+
let best: any = null;
|
|
103
|
+
let bestDist = Infinity;
|
|
104
|
+
const pos = bot?.entity?.position;
|
|
105
|
+
if (!pos) return null;
|
|
106
|
+
for (const id in bot.entities) {
|
|
107
|
+
const e = bot.entities[id];
|
|
108
|
+
if (entityName(e) !== "creeper") continue;
|
|
109
|
+
const d = entityDistance({ position: pos }, e);
|
|
110
|
+
if (d <= radius && d < bestDist) {
|
|
111
|
+
best = e;
|
|
112
|
+
bestDist = d;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return best;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function nearestPlayer(bot: any, radius = 16): any | null {
|
|
119
|
+
let best: any = null;
|
|
120
|
+
let bestDist = Infinity;
|
|
121
|
+
const pos = bot?.entity?.position;
|
|
122
|
+
if (!pos) return null;
|
|
123
|
+
const players: any = bot.players ?? {};
|
|
124
|
+
for (const name in players) {
|
|
125
|
+
if (name === bot.username) continue;
|
|
126
|
+
const entity = players[name]?.entity;
|
|
127
|
+
if (!entity?.position) continue;
|
|
128
|
+
const d = entityDistance({ position: pos }, entity);
|
|
129
|
+
if (d <= radius && d < bestDist) {
|
|
130
|
+
best = entity;
|
|
131
|
+
bestDist = d;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return best;
|
|
135
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Bot } from "mineflayer";
|
|
2
|
+
|
|
3
|
+
const FOOD_NAMES = new Set([
|
|
4
|
+
"bread", "apple", "golden_apple", "enchanted_golden_apple", "cooked_beef", "beef",
|
|
5
|
+
"cooked_porkchop", "porkchop", "cooked_chicken", "chicken", "cooked_mutton", "mutton",
|
|
6
|
+
"cooked_cod", "cod", "cooked_salmon", "salmon", "cooked_rabbit", "rabbit",
|
|
7
|
+
"baked_potato", "potato", "carrot", "beetroot", "beetroot_soup", "mushroom_stew",
|
|
8
|
+
"cookie", "melon_slice", "sweet_berries", "glow_berries", "dried_kelp",
|
|
9
|
+
"pumpkin_pie", "honey_bottle", "chorus_fruit",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export function isFood(name: string): boolean {
|
|
13
|
+
return FOOD_NAMES.has(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function findFood(bot: Bot): any | null {
|
|
17
|
+
const items = bot.inventory?.items?.() ?? [];
|
|
18
|
+
return items.find((i: any) => isFood(i.name)) ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function equipSword(bot: Bot): Promise<void> {
|
|
22
|
+
const items = bot.inventory?.items?.() ?? [];
|
|
23
|
+
const sword = items.find((i: any) => /sword$/.test(i.name));
|
|
24
|
+
if (sword) {
|
|
25
|
+
try {
|
|
26
|
+
await bot.equip(sword as any, "hand");
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function equipToolFor(bot: Bot, resource: string): Promise<boolean> {
|
|
34
|
+
const items = bot.inventory?.items?.() ?? [];
|
|
35
|
+
let tool: any;
|
|
36
|
+
if (resource === "wood") {
|
|
37
|
+
tool = items.find((i: any) => /_axe$/.test(i.name) && !/pickaxe/.test(i.name));
|
|
38
|
+
} else {
|
|
39
|
+
tool = items.find((i: any) => /_pickaxe$/.test(i.name));
|
|
40
|
+
}
|
|
41
|
+
if (tool) {
|
|
42
|
+
try {
|
|
43
|
+
await bot.equip(tool, "hand");
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function eatFood(bot: Bot): Promise<boolean> {
|
|
53
|
+
const food = findFood(bot);
|
|
54
|
+
if (!food) return false;
|
|
55
|
+
try {
|
|
56
|
+
await bot.equip(food, "hand");
|
|
57
|
+
await bot.consume();
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function hasShield(bot: Bot): boolean {
|
|
65
|
+
try {
|
|
66
|
+
const anyBot = bot as any;
|
|
67
|
+
if (anyBot.supportFeature?.("doesntHaveOffHandSlot")) return false;
|
|
68
|
+
const dest = anyBot.getEquipmentDestSlot?.("off-hand");
|
|
69
|
+
if (dest == null) return false;
|
|
70
|
+
const slot = bot.inventory?.slots?.[dest];
|
|
71
|
+
return !!slot && /shield/.test(String(slot.name));
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/skills/mc.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AISkill } from "mioku";
|
|
2
|
+
import type { PlayManager } from "../play";
|
|
3
|
+
|
|
4
|
+
export function createMcSkill(playManager: PlayManager): AISkill {
|
|
5
|
+
return {
|
|
6
|
+
name: "mc",
|
|
7
|
+
description:
|
|
8
|
+
"让机器人进入或离开 Minecraft 服务器,让它可以与玩家一起游玩。进入后机器人将自主行动(探索、跟随、战斗、收集),直到决定离开或时间耗尽。",
|
|
9
|
+
permission: "admin",
|
|
10
|
+
tools: [
|
|
11
|
+
{
|
|
12
|
+
name: "control_bot",
|
|
13
|
+
description:
|
|
14
|
+
"进入或退出 Minecraft 服务器。action='enter' + serverId 加入服务器;action='exit' 退出。机器人进入后会自主决定行为。仅在群管理员要求或继续游戏有意义时调用。",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
action: {
|
|
19
|
+
type: "string",
|
|
20
|
+
enum: ["enter", "exit"],
|
|
21
|
+
description: "'enter' 加入服务器,'exit' 退出当前服务器",
|
|
22
|
+
},
|
|
23
|
+
serverId: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "要进入的服务器 ID。enter 时需要,exit 时忽略。",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ["action"],
|
|
29
|
+
},
|
|
30
|
+
handler: async (args: any, runtimeCtx?: any) => {
|
|
31
|
+
const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
|
|
32
|
+
const groupId = Number(event?.group_id);
|
|
33
|
+
if (!Number.isFinite(groupId) || groupId <= 0) {
|
|
34
|
+
return { error: "无法识别当前群,请在群聊中调用" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const action = String(args?.action ?? "").toLowerCase();
|
|
38
|
+
if (action === "exit") {
|
|
39
|
+
const r = await playManager.exit(groupId);
|
|
40
|
+
return { success: r.success, message: r.message };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (action !== "enter") {
|
|
44
|
+
return { error: `未知 action: ${action}` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const serverId = String(args?.serverId ?? "").trim();
|
|
48
|
+
if (!serverId) return { error: "缺少 serverId" };
|
|
49
|
+
|
|
50
|
+
const r = await playManager.enter(groupId, serverId);
|
|
51
|
+
return r.success
|
|
52
|
+
? { success: true, serverId: r.serverId, message: r.message }
|
|
53
|
+
: { error: r.message };
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|