cortico 0.1.0 → 0.1.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/README.md +4 -11
- package/package.json +1 -3
- package/src/bot.ts +1 -1
- package/src/core/README.md +0 -4
- package/src/core/types.ts +1 -1
- package/src/core/util.ts +2 -1
- package/src/deploy.ts +8 -6
- package/src/providers/README.md +4 -5
- package/src/providers/base.ts +1 -1
- package/src/providers/configuration.ts +2 -1
- package/src/providers/console/settings.ts +8 -10
- package/src/providers/llamacpp/console/runtime-panel.ts +73 -32
- package/src/providers/llamacpp/console/server.ts +1 -20
- package/src/providers/llamacpp/index.ts +0 -2
- package/src/providers/llamacpp/options.ts +0 -2
- package/src/providers/llamacpp/strings.ts +6 -6
- package/src/providers/openai-responses-compat/index.ts +0 -3
- package/src/web/README.md +1 -5
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +35 -41
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +4 -4
- package/src/web/client/console-pages/host.ts +2 -2
- package/src/web/client/features/core/strings.ts +2 -2
- package/src/web/client/features/feature.ts +1 -1
- package/src/web/client/features/live/index.ts +8 -1
- package/src/web/client/features/live/onboarding.ts +4 -1
- package/src/web/client/features/live/strings.ts +11 -11
- package/src/web/client/features/live/timeline.ts +2 -1
- package/src/web/client/features/settings/general.ts +1 -0
- package/src/web/client/features/settings/index.ts +1 -0
- package/src/web/client/features/worlds/index.ts +1 -0
- package/src/web/client/shell/index.ts +27 -47
- package/src/web/client/shell/strings.ts +0 -22
- package/src/web/client/ui/icons.ts +5 -0
- package/src/web/console-pages.ts +1 -1
- package/src/web/public/styles.css +21 -8
- package/src/web/server.ts +11 -3
- package/src/web/shared/client-panel.ts +1 -1
- package/src/web/shared/console-protocol.ts +2 -1
- package/src/worlds/minecraft/README.md +61 -11
- package/src/worlds/minecraft/cell-facts.ts +226 -0
- package/src/worlds/minecraft/chests.ts +5 -0
- package/src/worlds/minecraft/containers.ts +325 -0
- package/src/worlds/minecraft/entity-facts.ts +31 -5
- package/src/worlds/minecraft/executor.ts +283 -10348
- package/src/worlds/minecraft/inventory.ts +268 -0
- package/src/worlds/minecraft/melee.ts +419 -0
- package/src/worlds/minecraft/placed-ledger.ts +152 -0
- package/src/worlds/minecraft/placement.ts +1038 -0
- package/src/worlds/minecraft/receipt.ts +340 -0
- package/src/worlds/minecraft/skill-context.ts +358 -0
- package/src/worlds/minecraft/skills-build.ts +1169 -0
- package/src/worlds/minecraft/skills-container.ts +1343 -0
- package/src/worlds/minecraft/skills-craft.ts +333 -0
- package/src/worlds/minecraft/skills-dig.ts +624 -0
- package/src/worlds/minecraft/skills-gather.ts +1230 -0
- package/src/worlds/minecraft/skills-interact.ts +1559 -0
- package/src/worlds/minecraft/tools.ts +331 -0
- package/src/worlds/minecraft/travel.ts +763 -0
- package/src/worlds/minecraft/until.ts +75 -0
- package/src/worlds/terminal/world.ts +8 -5
- package/src/providers/console/config.ts +0 -21
- package/src/providers/llamacpp/config.ts +0 -36
- package/src/providers/openai-responses-compat/config.ts +0 -20
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 容器与工作站的窗口:找到哪一个、开得稳、槽位怎么排、这一次看见了什么记进账本。
|
|
3
|
+
*
|
|
4
|
+
* 只管开窗与读写槽位,存什么取什么由技能族决定。
|
|
5
|
+
*/
|
|
6
|
+
import type { Bot } from 'mineflayer';
|
|
7
|
+
import pathfinderPkg from 'mineflayer-pathfinder';
|
|
8
|
+
import { Vec3 } from 'vec3';
|
|
9
|
+
import { type Cell } from './geometry.ts';
|
|
10
|
+
import { SkillBlocked, checkAbort, sleep, type SkillContext } from './skill-context.ts';
|
|
11
|
+
import { gotoGoal, reachCell } from './travel.ts';
|
|
12
|
+
import { blockAtCell, cellText, dimensionOf } from './cell-facts.ts';
|
|
13
|
+
import { zhName } from './names.ts';
|
|
14
|
+
import { moveExactSlot, playerInvIn, type InvPred } from './inventory.ts';
|
|
15
|
+
import { readDurability, readEnchants, type EnchantRegistry } from './item-facts.ts';
|
|
16
|
+
import {
|
|
17
|
+
CONTAINER_FIND, ChestBook, FURNACE_KINDS, chestBlockName, hasItem, hasRoom, type ChestRecord,
|
|
18
|
+
} from './chests.ts';
|
|
19
|
+
import { type ItemStack } from './terrain.ts';
|
|
20
|
+
import { contentsText } from './receipt.ts';
|
|
21
|
+
|
|
22
|
+
const { goals } = pathfinderPkg;
|
|
23
|
+
|
|
24
|
+
/** 三种磨损态的铁砧都认 */
|
|
25
|
+
export const ANVIL_BLOCKS = ['anvil', 'chipped_anvil', 'damaged_anvil'] as const;
|
|
26
|
+
/** 找工作方块的半径,与 smelt 找炉子一个量级 */
|
|
27
|
+
export const STATION_FIND_R = 16;
|
|
28
|
+
/** 放料/取货后等服务端回灌窗口槽位 */
|
|
29
|
+
export const WINDOW_SETTLE_MS = 600;
|
|
30
|
+
|
|
31
|
+
export interface StationWindow {
|
|
32
|
+
id: number;
|
|
33
|
+
type: string;
|
|
34
|
+
slots: Array<{ name: string; type: number; count: number } | null>;
|
|
35
|
+
inventoryStart: number;
|
|
36
|
+
inventoryEnd: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findStationCell(bot: Bot, names: readonly string[]): Cell | null {
|
|
40
|
+
const reg = bot.registry.blocksByName as unknown as Record<string, { id: number } | undefined>;
|
|
41
|
+
const ids = names.map((n) => reg[n]?.id).filter((n): n is number => typeof n === 'number');
|
|
42
|
+
if (ids.length === 0) return null;
|
|
43
|
+
const found = bot.findBlocks({ matching: ids, maxDistance: STATION_FIND_R, count: 1 });
|
|
44
|
+
return found.length > 0 ? { x: found[0].x, y: found[0].y, z: found[0].z } : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 走到 cell、开它的窗;那一格不是这种工作方块当场说清 */
|
|
48
|
+
export async function openStationWindow(
|
|
49
|
+
bot: Bot, ctx: SkillContext, cell: Cell, blockNames: readonly string[], zhStation: string,
|
|
50
|
+
): Promise<{ win: StationWindow; blockName: string }> {
|
|
51
|
+
await reachCell(bot, cell, ctx);
|
|
52
|
+
checkAbort(ctx);
|
|
53
|
+
const block = blockAtCell(bot, cell);
|
|
54
|
+
if (!block) throw new SkillBlocked(`${cellText(cell)} 所在区块没加载`);
|
|
55
|
+
if (!blockNames.includes(block.name)) {
|
|
56
|
+
throw new SkillBlocked(`${cellText(cell)} 那一格是${zhName(block.name)},不是${zhStation}`);
|
|
57
|
+
}
|
|
58
|
+
const win = await (bot as unknown as { openBlock(b: unknown): Promise<StationWindow> }).openBlock(block);
|
|
59
|
+
return { win, blockName: block.name };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 把包里符合判据的一件挪进窗口的某个格。
|
|
64
|
+
*
|
|
65
|
+
* 找料照**开着的那扇窗**自己的账读(见 playerInvIn),槽位号也只在那扇窗里成立。
|
|
66
|
+
* 一格一件的按槽位挪(见 moveExactSlot):`transfer` 按类型找槽,同 id 的几件在它
|
|
67
|
+
* 眼里没有分别,点名挑的那一件就白挑了。
|
|
68
|
+
*/
|
|
69
|
+
export async function putIntoStation(
|
|
70
|
+
bot: Bot, win: StationWindow, pred: InvPred, destSlot: number, asked: string,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const item = playerInvIn(bot, win).items().find((i) => pred(i.name, i));
|
|
73
|
+
if (!item) throw new SkillBlocked(`包里没有${asked}`);
|
|
74
|
+
if (item.count === 1) {
|
|
75
|
+
await moveExactSlot(bot, item.slot, destSlot);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await (bot as unknown as { transfer(o: Record<string, unknown>): Promise<void> }).transfer({
|
|
79
|
+
window: win, itemType: item.type, metadata: null, count: 1,
|
|
80
|
+
sourceStart: win.inventoryStart, sourceEnd: win.inventoryEnd,
|
|
81
|
+
destStart: destSlot, destEnd: destSlot + 1,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 一件东西的读数:耐久 + 附魔,回执格式统一 */
|
|
86
|
+
export function stationItemFacts(bot: Bot, item: { name: string } | null): string {
|
|
87
|
+
if (!item) return '';
|
|
88
|
+
const parts: string[] = [];
|
|
89
|
+
const dur = readDurability(item as never);
|
|
90
|
+
if (dur) parts.push(`耐久 ${dur.left}/${dur.max}`);
|
|
91
|
+
const ench = readEnchants(item as never, bot.registry as never);
|
|
92
|
+
parts.push(ench.length > 0 ? `附魔 ${ench.map((e) => `${e.name}${e.level}`).join('、')}` : '没有附魔');
|
|
93
|
+
return parts.join(',');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function findContainers(bot: Bot, range: number): Array<{ x: number; y: number; z: number; name: string; d: number }> {
|
|
97
|
+
const ids = CONTAINER_FIND
|
|
98
|
+
.map((n) => (bot.registry.blocksByName as Record<string, { id: number } | undefined>)[n]?.id)
|
|
99
|
+
.filter((id): id is number => id !== undefined);
|
|
100
|
+
if (ids.length === 0) return [];
|
|
101
|
+
const me = bot.entity.position;
|
|
102
|
+
const found = bot.findBlocks({ matching: ids, maxDistance: range, count: 16 });
|
|
103
|
+
const out: Array<{ x: number; y: number; z: number; name: string; d: number }> = [];
|
|
104
|
+
for (const p of found) {
|
|
105
|
+
const b = bot.blockAt(p);
|
|
106
|
+
if (!b) continue;
|
|
107
|
+
out.push({ x: p.x, y: p.y, z: p.z, name: b.name, d: p.distanceTo(me) });
|
|
108
|
+
}
|
|
109
|
+
out.sort((a, b) => a.d - b.d);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 报告本维度账本中最近箱子的上次观测位置与取整直线距离;没有则返回 null。
|
|
115
|
+
* 不跨维度比较坐标,也不决定走过去或放新箱子。
|
|
116
|
+
*/
|
|
117
|
+
export function knownChestNote(bot: Bot, chests: ChestBook | undefined): string | null {
|
|
118
|
+
const me = bot.entity.position;
|
|
119
|
+
let best: { rec: ChestRecord; d: number } | null = null;
|
|
120
|
+
for (const rec of chests?.chestsIn(dimensionOf(bot)) ?? []) {
|
|
121
|
+
const d = Math.hypot(rec.x - me.x, rec.y - me.y, rec.z - me.z);
|
|
122
|
+
if (!best || d < best.d) best = { rec, d };
|
|
123
|
+
}
|
|
124
|
+
if (!best) return null;
|
|
125
|
+
const r = best.rec;
|
|
126
|
+
const origin = r.placedAt !== undefined ? '你放的' : '你开过的';
|
|
127
|
+
return `;账上本维度最近的是${origin}${zhName(chestBlockName(r))}`
|
|
128
|
+
+ ` (${r.x}, ${r.y}, ${r.z}),直线 ${Math.round(best.d)} 格(上次看见)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 附近一个容器都扫不到:两处(stow / take)共用同一句措辞与同一段账本补注 */
|
|
132
|
+
export function noContainerNearby(bot: Bot, ctx: SkillContext): SkillBlocked {
|
|
133
|
+
return new SkillBlocked(`32 格内没有箱子${knownChestNote(bot, ctx.chests) ?? ''}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 箱内容按「名字 + 附魔」并栈,与背包同一条线(原版里带不同附魔的同名物品不能堆叠)。
|
|
138
|
+
* 只按名字并,五本各不相同的附魔书会念成「附魔书×5」:数量读得到,哪一本在里面读不到,
|
|
139
|
+
* 于是也点不了名(见 item-pick)。
|
|
140
|
+
*/
|
|
141
|
+
export function containerStacks(win: {
|
|
142
|
+
containerItems?: () => Array<{ name: string; count: number }>;
|
|
143
|
+
slots?: Array<{ name: string; count: number } | null>;
|
|
144
|
+
inventoryStart?: number;
|
|
145
|
+
}, registry?: EnchantRegistry | null): { items: ItemStack[]; usedSlots: number; slots: number } {
|
|
146
|
+
const raw = typeof win.containerItems === 'function'
|
|
147
|
+
? win.containerItems()
|
|
148
|
+
: (win.slots ?? []).slice(0, win.inventoryStart ?? 27).filter((s): s is { name: string; count: number } => s != null);
|
|
149
|
+
const merged = new Map<string, ItemStack>();
|
|
150
|
+
for (const it of raw) {
|
|
151
|
+
const ench = readEnchants(it as never, registry as never);
|
|
152
|
+
const key = ench.length > 0 ? `${it.name}|${ench.map((e) => `${e.name}${e.level}`).join(',')}` : it.name;
|
|
153
|
+
const cur = merged.get(key);
|
|
154
|
+
if (cur) cur.count += it.count;
|
|
155
|
+
else merged.set(key, { name: it.name, count: it.count, ...(ench.length > 0 ? { enchantments: ench } : {}) });
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
items: [...merged.values()],
|
|
159
|
+
usedSlots: raw.length,
|
|
160
|
+
slots: win.inventoryStart ?? 27,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function rememberChest(
|
|
165
|
+
ctx: SkillContext,
|
|
166
|
+
bot: Bot,
|
|
167
|
+
pos: { x: number; y: number; z: number },
|
|
168
|
+
win: Parameters<typeof containerStacks>[0],
|
|
169
|
+
): { items: ItemStack[]; usedSlots: number; slots: number } {
|
|
170
|
+
const snap = containerStacks(win, bot.registry as never);
|
|
171
|
+
ctx.chests?.remember(dimensionOf(bot), pos, snap.items, snap.usedSlots, snap.slots);
|
|
172
|
+
return snap;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 原版烧一件的耗时:熔炉 200 刻(10 秒);高炉与烟熏炉减半(100 刻)。
|
|
177
|
+
* 这是确定性数值,所以熔炉的到期**可以**估;作物不行(随机刻),别把这条挪去作物。
|
|
178
|
+
*/
|
|
179
|
+
export function smeltPerItemMs(blockName: string): number {
|
|
180
|
+
return blockName === 'furnace' ? 10_000 : 5_000;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 槽位有料不能证明正在烧炼;估时需要炉火和进度读数同时确认。 */
|
|
184
|
+
export function furnaceDoneAt(win: unknown, input: ItemStack | null, blockName: string, now: number): number | null {
|
|
185
|
+
const { fuel, progress } = win as { fuel?: number | null; progress?: number | null };
|
|
186
|
+
return input && typeof fuel === 'number' && fuel > 0 && typeof progress === 'number' && progress > 0
|
|
187
|
+
? now + (input.count - progress) * smeltPerItemMs(blockName)
|
|
188
|
+
: null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 窗口 slots 里的一格转成账本的 ItemStack;空格与读不出都算 null */
|
|
192
|
+
export function slotStack(win: unknown, i: number): ItemStack | null {
|
|
193
|
+
const slots = (win as { slots?: Array<{ name?: string; count?: number } | null> }).slots;
|
|
194
|
+
const s = slots?.[i];
|
|
195
|
+
return s && typeof s.name === 'string' && typeof s.count === 'number'
|
|
196
|
+
? { name: s.name, count: s.count }
|
|
197
|
+
: null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 右键开出来的窗口在关窗前记进容器账本,回执带上看见了什么。
|
|
202
|
+
* 箱子族整窗记;炉子族记三槽位,有炉火和进度读数才估到期。不是容器就什么都不做。
|
|
203
|
+
*/
|
|
204
|
+
export function rememberWindow(
|
|
205
|
+
bot: Bot,
|
|
206
|
+
ctx: SkillContext,
|
|
207
|
+
cell: Cell,
|
|
208
|
+
blockName: string,
|
|
209
|
+
win: NonNullable<Bot['currentWindow']>,
|
|
210
|
+
): string {
|
|
211
|
+
if (CONTAINER_FIND.includes(blockName)) {
|
|
212
|
+
const snap = rememberChest(ctx, bot, cell, win as Parameters<typeof containerStacks>[0]);
|
|
213
|
+
return `。箱里:${contentsText(snap.items)}`;
|
|
214
|
+
}
|
|
215
|
+
if (FURNACE_KINDS.has(blockName)) {
|
|
216
|
+
const state = { input: slotStack(win, 0), fuel: slotStack(win, 1), output: slotStack(win, 2) };
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const expected = furnaceDoneAt(win, state.input, blockName, now);
|
|
219
|
+
ctx.chests?.rememberFurnace(dimensionOf(bot), cell, blockName, state, now, expected);
|
|
220
|
+
const slot = (s: ItemStack | null): string => (s ? `${zhName(s.name)}×${s.count}` : '空');
|
|
221
|
+
return `。炉里:输入${slot(state.input)},燃料${slot(state.fuel)},输出${slot(state.output)}`;
|
|
222
|
+
}
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function orderForStow(
|
|
227
|
+
found: Array<{ x: number; y: number; z: number; name: string; d: number }>,
|
|
228
|
+
ctx: SkillContext,
|
|
229
|
+
bot: Bot,
|
|
230
|
+
item: string,
|
|
231
|
+
): typeof found {
|
|
232
|
+
const dim = dimensionOf(bot);
|
|
233
|
+
const withRoom: typeof found = [];
|
|
234
|
+
const unseen: typeof found = [];
|
|
235
|
+
const rest: typeof found = [];
|
|
236
|
+
// 堆叠上限是物品自带的属性(原版:大多 64,鸡蛋/雪球 16,工具/桶 1),registry 里就有
|
|
237
|
+
const stackMax = (bot.registry.itemsByName as Record<string, { stackSize?: number } | undefined>)
|
|
238
|
+
?.[item]?.stackSize ?? 64;
|
|
239
|
+
for (const s of found) {
|
|
240
|
+
const rec = ctx.chests?.get(dim, s);
|
|
241
|
+
if (rec && hasRoom(rec, item, stackMax)) withRoom.push(s);
|
|
242
|
+
else if (!rec) unseen.push(s);
|
|
243
|
+
else rest.push(s);
|
|
244
|
+
}
|
|
245
|
+
return [...withRoom, ...unseen, ...rest];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function orderForTake(
|
|
249
|
+
found: Array<{ x: number; y: number; z: number; name: string; d: number }>,
|
|
250
|
+
ctx: SkillContext,
|
|
251
|
+
bot: Bot,
|
|
252
|
+
item: string,
|
|
253
|
+
): typeof found {
|
|
254
|
+
const dim = dimensionOf(bot);
|
|
255
|
+
const withItem: typeof found = [];
|
|
256
|
+
const rest: typeof found = [];
|
|
257
|
+
for (const s of found) {
|
|
258
|
+
const rec = ctx.chests?.get(dim, s);
|
|
259
|
+
if (rec && hasItem(rec, item)) withItem.push(s);
|
|
260
|
+
else rest.push(s);
|
|
261
|
+
}
|
|
262
|
+
return [...withItem, ...rest];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** mineflayer 拿错窗口身份时抛的话:「Non-container window used as a container」一族 */
|
|
266
|
+
export function isWindowIdentityError(err: unknown): boolean {
|
|
267
|
+
return err instanceof Error && / window used as a /i.test(err.message);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 开容器前退役遗留窗口;返回窗口身份不符时关闭并重开一次,连续失败才受阻。
|
|
272
|
+
* 此处校验窗口身份,窗口状态版本由 mineflayer-fixes 的 stateId 守卫处理。
|
|
273
|
+
*/
|
|
274
|
+
export async function openWindowGuarded<T>(bot: Bot, ctx: SkillContext, open: () => Promise<T>): Promise<T> {
|
|
275
|
+
const cur = bot.currentWindow;
|
|
276
|
+
if (cur) {
|
|
277
|
+
ctx.diag?.write({
|
|
278
|
+
lane: 'skill', event: 'stale-window-retire',
|
|
279
|
+
msg: `开容器前发现还挂着窗口${cur.id}(${cur.type}),先关掉`,
|
|
280
|
+
data: { windowId: cur.id, type: String(cur.type) },
|
|
281
|
+
});
|
|
282
|
+
bot.closeWindow(cur);
|
|
283
|
+
await sleep(150);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
return await open();
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (!isWindowIdentityError(err)) throw err;
|
|
289
|
+
const w = bot.currentWindow;
|
|
290
|
+
ctx.diag?.write({
|
|
291
|
+
lane: 'skill', event: 'window-identity-retry',
|
|
292
|
+
msg: `开出来的窗口身份不对(${w ? `窗口${w.id} ${w.type}` : '窗口已不在'}),强制关窗重开一次`,
|
|
293
|
+
data: { windowId: w?.id, type: w ? String(w.type) : null },
|
|
294
|
+
});
|
|
295
|
+
if (w) bot.closeWindow(w);
|
|
296
|
+
await sleep(300);
|
|
297
|
+
try {
|
|
298
|
+
return await open();
|
|
299
|
+
} catch (err2) {
|
|
300
|
+
if (!isWindowIdentityError(err2)) throw err2;
|
|
301
|
+
throw new SkillBlocked(
|
|
302
|
+
'窗口串号了:上一个界面窗口没退干净,这次开容器拿到的是错的窗口身份;' +
|
|
303
|
+
'已强制关窗重试一次仍没成。东西都还在,没有丢——过几秒再开一次多半就好',
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function openNearbyContainer(
|
|
310
|
+
bot: Bot,
|
|
311
|
+
spot: { x: number; y: number; z: number; name: string },
|
|
312
|
+
ctx: SkillContext,
|
|
313
|
+
): Promise<Awaited<ReturnType<Bot['openContainer']>>> {
|
|
314
|
+
await gotoGoal(bot, new goals.GoalNear(spot.x, spot.y, spot.z, 2), ctx);
|
|
315
|
+
const block = bot.blockAt(new Vec3(spot.x, spot.y, spot.z));
|
|
316
|
+
if (!block) throw new SkillBlocked(`${zhName(spot.name)}不见了`);
|
|
317
|
+
return openWindowGuarded(bot, ctx, () => bot.openContainer(block));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface GenericWindow {
|
|
321
|
+
slots: Array<{ name: string; count: number; slot: number } | null>;
|
|
322
|
+
items(): Array<{ name: string; count: number; slot: number }>;
|
|
323
|
+
close(): void;
|
|
324
|
+
}
|
|
325
|
+
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/** 实体元数据字段位置取自 registry.entitiesByName 的 metadataKeys。 */
|
|
2
|
+
import type { Bot } from 'mineflayer';
|
|
3
|
+
import { matchBlockIds } from './travel.ts';
|
|
4
|
+
import { zhName } from './names.ts';
|
|
2
5
|
|
|
3
|
-
interface NamedStack { name?: string }
|
|
6
|
+
export interface NamedStack { name?: string }
|
|
4
7
|
|
|
5
|
-
interface FactEntity {
|
|
8
|
+
export interface FactEntity {
|
|
6
9
|
name?: string;
|
|
7
10
|
metadata?: unknown[];
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
interface FactBot {
|
|
13
|
+
export interface FactBot {
|
|
11
14
|
entity?: { uuid?: string } | null;
|
|
12
15
|
/** 登录时玩家 UUID 优先取 bot.player,其次取实体或协议客户端。 */
|
|
13
16
|
player?: { uuid?: string } | null;
|
|
@@ -18,7 +21,7 @@ interface FactBot {
|
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
function metaOf(bot: FactBot, entity: FactEntity, key: string): unknown {
|
|
24
|
+
export function metaOf(bot: FactBot, entity: FactEntity, key: string): unknown {
|
|
22
25
|
const name = entity.name;
|
|
23
26
|
if (!name) return undefined;
|
|
24
27
|
const keys = bot.registry?.entitiesByName?.[name]?.metadataKeys ?? [];
|
|
@@ -112,7 +115,7 @@ export function readHorseTamed(bot: FactBot, entity: FactEntity): boolean | null
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/** 羊毛元数据的低四位是颜色,第五位(0x10)是"剪过了"。 */
|
|
115
|
-
const WOOL_COLORS = [
|
|
118
|
+
export const WOOL_COLORS = [
|
|
116
119
|
'white', 'orange', 'magenta', 'light_blue', 'yellow', 'lime', 'pink', 'gray',
|
|
117
120
|
'light_gray', 'cyan', 'purple', 'blue', 'brown', 'green', 'red', 'black',
|
|
118
121
|
] as const;
|
|
@@ -130,3 +133,26 @@ export function readSheepColor(bot: FactBot, entity: FactEntity): string | null
|
|
|
130
133
|
if (typeof raw !== 'number') return null;
|
|
131
134
|
return WOOL_COLORS[raw & 0x0f] ?? null;
|
|
132
135
|
}
|
|
136
|
+
/** 可攻击目标名限定为实体类型 ID 或在线玩家名。 */
|
|
137
|
+
/** use.target 只接受活物;无效目标的回执说明可接受类型及改用 at 的方块写法。 */
|
|
138
|
+
export function unknownUseTargetText(bot: Bot, target: string): string {
|
|
139
|
+
const head = `use 的 target 只认活物(生物、玩家),「${target}」不是活物`;
|
|
140
|
+
const ids = matchBlockIds(bot, target);
|
|
141
|
+
if (ids.length === 0) return `${head};要右键一格方块的话写 {"skill":"use","at":[x,y,z]}`;
|
|
142
|
+
// 现场恰好找得到那一格就把坐标一并给出:她照抄就能用
|
|
143
|
+
const near = bot.findBlocks({ matching: ids, maxDistance: 16, count: 1 })[0] ?? null;
|
|
144
|
+
if (!near) {
|
|
145
|
+
return `${head},它是方块 —— 右键方块写 {"skill":"use","at":[x,y,z]};` +
|
|
146
|
+
`那一格在哪先用 {"skill":"find","target":"${target}","distance":16} 问一句`;
|
|
147
|
+
}
|
|
148
|
+
return `${head},它是方块 —— 右键它写 {"skill":"use","at":[${near.x},${near.y},${near.z}]}` +
|
|
149
|
+
`(${zhName(target)}就在那一格,16 格内看得见的最近一处)`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function isKnownTarget(bot: Bot, name: string): boolean {
|
|
153
|
+
const n = name.toLowerCase();
|
|
154
|
+
const entities = (bot.registry as unknown as { entitiesByName?: Record<string, unknown> }).entitiesByName;
|
|
155
|
+
if (entities?.[n]) return true;
|
|
156
|
+
return Object.keys(bot.players ?? {}).some((p) => p.toLowerCase() === n);
|
|
157
|
+
}
|
|
158
|
+
|