cortico 0.1.0 → 0.1.2
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 +7 -14
- package/package.json +1 -3
- package/src/boot.ts +18 -0
- package/src/bot.ts +41 -6
- package/src/core/README.md +4 -6
- package/src/core/blobs.ts +23 -2
- package/src/core/config-schema.ts +1 -1
- package/src/core/config.ts +2 -16
- package/src/core/core.ts +15 -5
- package/src/core/loop.ts +55 -27
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +19 -3
- package/src/core/util.ts +17 -1
- package/src/deploy.ts +4 -1
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +23 -2
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +23 -9
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +32 -8
- package/src/providers/base.ts +3 -1
- package/src/providers/configuration.ts +2 -1
- package/src/providers/console/hub.ts +270 -0
- package/src/providers/console/settings.ts +8 -18
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +8 -6
- package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
- package/src/providers/llamacpp/console/server.ts +3 -1
- package/src/providers/llamacpp/index.ts +3 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/strings.ts +6 -6
- package/src/providers/name.ts +8 -0
- package/src/providers/openai-responses-compat/config.ts +13 -0
- package/src/providers/openai-responses-compat/console/client.ts +5 -0
- package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
- package/src/providers/openai-responses-compat/console/server.ts +127 -0
- package/src/providers/openai-responses-compat/index.ts +38 -13
- package/src/providers/openai-responses-compat/native.ts +9 -4
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/registry.ts +5 -0
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +11 -6
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
- package/src/web/client/console-pages/host.ts +24 -7
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/core/strings.ts +2 -2
- package/src/web/client/features/extensions/index.ts +276 -41
- package/src/web/client/features/extensions/strings.ts +84 -10
- package/src/web/client/features/feature.ts +2 -2
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +34 -11
- package/src/web/client/features/live/onboarding.ts +4 -1
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +20 -14
- package/src/web/client/features/live/timeline.ts +2 -1
- package/src/web/client/features/providers/detail.ts +262 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +173 -87
- package/src/web/client/features/providers/strings.ts +44 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +13 -0
- package/src/web/client/features/settings/index.ts +1 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/features/worlds/index.ts +1 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +28 -48
- package/src/web/client/shell/strings.ts +0 -22
- package/src/web/client/ui/icons.ts +14 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/console-pages.ts +1 -1
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +143 -26
- package/src/web/server.ts +233 -19
- package/src/web/shared/client-panel.ts +4 -1
- package/src/web/shared/console-protocol.ts +4 -1
- package/src/worlds/bilibili/README.md +1 -1
- package/src/worlds/bilibili/overlay/server.ts +4 -2
- package/src/worlds/minecraft/ADAPT.md +66 -0
- package/src/worlds/minecraft/README.md +69 -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/mineflayer-fixes.ts +55 -1
- 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/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +53 -7
- package/src/worlds/terminal/world.ts +5 -3
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 合成、吃喝与换装:craft、eat、equip。
|
|
3
|
+
*
|
|
4
|
+
* 配方只认服务端给的那一份;吃喝与换装都等服务端回话再报。
|
|
5
|
+
*/
|
|
6
|
+
import type { Bot } from 'mineflayer';
|
|
7
|
+
import { SkillBlocked, checkAbort, sleep, type SkillContext } from './skill-context.ts';
|
|
8
|
+
import { type SkillCall } from './skills.ts';
|
|
9
|
+
import { zhName } from './names.ts';
|
|
10
|
+
import { gridText, zhErrorText } from './receipt.ts';
|
|
11
|
+
import {
|
|
12
|
+
CRAFT_SETTLE_MS, awaitCraftGain, invCount, invCountById, invGains, invItemNamed, invSnapshot,
|
|
13
|
+
namedLike, noSuchItem,
|
|
14
|
+
} from './inventory.ts';
|
|
15
|
+
import { CRAFTING_STATION, ensureStation } from './placement.ts';
|
|
16
|
+
import { HANDHELD_SUFFIXES, equipDestOf } from './tools.ts';
|
|
17
|
+
import { DRINKABLES } from './item-facts.ts';
|
|
18
|
+
import { noteAte } from './placed-ledger.ts';
|
|
19
|
+
import { edibleInBag, notFoodText } from './precheck.ts';
|
|
20
|
+
import { pickLabel, pickTargetOf } from './item-pick.ts';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `bot.craft` 吃的配方数据面(见 mineflayer-fixes 的覆写版):
|
|
24
|
+
* 只是"哪一格放哪个 id"的摆位指令,**不是许可证** —— 产出由服务端裁决。
|
|
25
|
+
*/
|
|
26
|
+
export interface CraftRecipeLike {
|
|
27
|
+
result: { id: number | null; count: number };
|
|
28
|
+
inShape?: Array<Array<{ id: number } | null>> | null;
|
|
29
|
+
ingredients?: Array<{ id: number }> | null;
|
|
30
|
+
requiresTable: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 一份配方一次要吃掉的材料(按 id 计数) */
|
|
34
|
+
export function craftNeeds(r: CraftRecipeLike): Map<number, number> {
|
|
35
|
+
const need = new Map<number, number>();
|
|
36
|
+
const bump = (id: number): void => { need.set(id, (need.get(id) ?? 0) + 1); };
|
|
37
|
+
if (r.inShape) {
|
|
38
|
+
for (const row of r.inShape) for (const cell of row) if (cell && cell.id >= 0) bump(cell.id);
|
|
39
|
+
} else if (r.ingredients) {
|
|
40
|
+
for (const ing of r.ingredients) if (ing.id >= 0) bump(ing.id);
|
|
41
|
+
}
|
|
42
|
+
return need;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 名字 → 物品定义;planks/bed 这两个类别词按包里的树种/羊毛色落地 */
|
|
46
|
+
export function craftItemDef(bot: Bot, item: string): { id: number; name: string } | undefined {
|
|
47
|
+
const registry = bot.registry;
|
|
48
|
+
let def = registry.itemsByName[item];
|
|
49
|
+
if (!def && item === 'planks') {
|
|
50
|
+
const log = bot.inventory.items().find((i) => i.name.endsWith('_log'));
|
|
51
|
+
def = registry.itemsByName[log ? log.name.replace('_log', '_planks') : 'oak_planks'];
|
|
52
|
+
}
|
|
53
|
+
if (!def && item === 'bed') {
|
|
54
|
+
const wool = bot.inventory.items().find((i) => i.name.endsWith('_wool'));
|
|
55
|
+
def = registry.itemsByName[wool ? wool.name.replace('_wool', '_bed') : 'white_bed'];
|
|
56
|
+
}
|
|
57
|
+
return def;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 她自己摆的格子 → 摆位指令。产物不预设:产出槽出什么就是什么 */
|
|
61
|
+
export function recipeFromGrid(bot: Bot, grid: string[][]): CraftRecipeLike {
|
|
62
|
+
const width = Math.max(...grid.map((r) => r.length));
|
|
63
|
+
const inShape = grid.map((row) => {
|
|
64
|
+
const cells: Array<{ id: number } | null> = [];
|
|
65
|
+
for (let x = 0; x < width; x++) {
|
|
66
|
+
const name = row[x] ?? '';
|
|
67
|
+
if (!name) { cells.push(null); continue; }
|
|
68
|
+
const def = bot.registry.itemsByName[name];
|
|
69
|
+
if (!def) throw new SkillBlocked(`不认识「${name}」这种物品`);
|
|
70
|
+
cells.push({ id: def.id });
|
|
71
|
+
}
|
|
72
|
+
return cells;
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
result: { id: null, count: 1 },
|
|
76
|
+
inShape,
|
|
77
|
+
// 2x2 徒手放得下,再大就要工作台
|
|
78
|
+
requiresTable: grid.length > 2 || width > 2,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function skillCraft(
|
|
83
|
+
bot: Bot, call: Extract<SkillCall, { skill: 'craft' }>, ctx: SkillContext,
|
|
84
|
+
): Promise<string> {
|
|
85
|
+
const nameOf = (id: number): string => {
|
|
86
|
+
const raw = (bot.registry.items as Record<number, { name: string }>)[id]?.name;
|
|
87
|
+
return raw ? zhName(raw) : `#${id}`;
|
|
88
|
+
};
|
|
89
|
+
const registry = bot.registry;
|
|
90
|
+
const tableDef = registry.blocksByName['crafting_table'];
|
|
91
|
+
const tableNear = bot.findBlocks({ matching: tableDef.id, maxDistance: 32, count: 1 });
|
|
92
|
+
|
|
93
|
+
let recipe: CraftRecipeLike;
|
|
94
|
+
let label: string;
|
|
95
|
+
let targetId: number | null = null;
|
|
96
|
+
if (call.grid) {
|
|
97
|
+
recipe = recipeFromGrid(bot, call.grid);
|
|
98
|
+
label = gridText(call.grid);
|
|
99
|
+
} else {
|
|
100
|
+
const def = craftItemDef(bot, call.item ?? '');
|
|
101
|
+
if (!def) throw new SkillBlocked(`不认识「${call.item}」这种物品`);
|
|
102
|
+
targetId = def.id;
|
|
103
|
+
label = zhName(def.name);
|
|
104
|
+
// 配方表里同一样东西可能有好几种摆法(木棍:竹子/木板)。挑手上材料齐的那一种;
|
|
105
|
+
// 都不齐就把配方要的直接材料照实说出来 —— 不再往下递归找"材料的材料"。
|
|
106
|
+
const all = bot.recipesAll(def.id, null, true as never) as unknown as CraftRecipeLike[];
|
|
107
|
+
if (all.length === 0) throw new SkillBlocked(`游戏的配方表里没有${label}的做法;要自己摆就写 grid`);
|
|
108
|
+
const have = new Map<number, number>();
|
|
109
|
+
for (const it of bot.inventory.items()) have.set(it.type, (have.get(it.type) ?? 0) + it.count);
|
|
110
|
+
const ready = all.find((r) => [...craftNeeds(r)].every(([id, n]) => (have.get(id) ?? 0) >= n));
|
|
111
|
+
if (!ready) {
|
|
112
|
+
const options = all.map((r) => [...craftNeeds(r)]
|
|
113
|
+
.map(([id, n]) => `${nameOf(id)}×${n}`).join(' + ')).join(' 或 ');
|
|
114
|
+
// 现有库存已满足本步需求时不判失败,允许依赖该物品的后续步骤继续。
|
|
115
|
+
const stock = invCountById(bot, def.id);
|
|
116
|
+
if (stock >= call.count) {
|
|
117
|
+
return `没现搓${label}:配方要 ${options},包里凑不齐;` +
|
|
118
|
+
`不过包里本来就有 ${stock} 个,够这一步要的 ${call.count} 个了`;
|
|
119
|
+
}
|
|
120
|
+
throw new SkillBlocked(`${label}的配方要:${options};包里凑不齐`);
|
|
121
|
+
}
|
|
122
|
+
recipe = ready;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const times = call.grid
|
|
126
|
+
? call.count
|
|
127
|
+
: Math.ceil(call.count / Math.max(1, recipe.result.count));
|
|
128
|
+
ctx.diag?.write({
|
|
129
|
+
lane: 'craft', event: call.grid ? 'grid' : 'recipe', taskId: ctx.taskId,
|
|
130
|
+
msg: `合成 ${label} ${times} 次` +
|
|
131
|
+
`(${recipe.requiresTable
|
|
132
|
+
? `要工作台:包里 ${invCount(bot, (n) => n === 'crafting_table')} 个,32 格内${tableNear.length > 0 ? '有' : '没有'}现成的`
|
|
133
|
+
: '徒手'})`,
|
|
134
|
+
data: {
|
|
135
|
+
grid: call.grid ?? null, expect: call.item ?? null, count: call.count, times,
|
|
136
|
+
requiresTable: recipe.requiresTable, tableNearby: tableNear.length > 0,
|
|
137
|
+
needs: [...craftNeeds(recipe)].map(([id, n]) => ({ item: nameOf(id), per: n })),
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const made: string[] = [];
|
|
142
|
+
const doneSoFar = (): string => (made.length > 0 ? `已完成: ${made.join('、')}。` : '');
|
|
143
|
+
let table: ReturnType<Bot['blockAt']> = null;
|
|
144
|
+
if (recipe.requiresTable) {
|
|
145
|
+
const station = await ensureStation(bot, CRAFTING_STATION, ctx);
|
|
146
|
+
made.push(station.note);
|
|
147
|
+
table = station.block;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 产量一律按库存净增算:服务端给了什么就报什么,不照配方表复述。
|
|
151
|
+
const before = invSnapshot(bot);
|
|
152
|
+
const targetBefore = targetId === null ? 0 : invCountById(bot, targetId);
|
|
153
|
+
/** 当场没读到入包的是第几次:槽位回灌会迟到,这是现场事实,不是判据(见下) */
|
|
154
|
+
const lateRounds: number[] = [];
|
|
155
|
+
for (let n = 0; n < times; n++) {
|
|
156
|
+
checkAbort(ctx);
|
|
157
|
+
const beforeOne = targetId === null ? null : invCountById(bot, targetId);
|
|
158
|
+
try {
|
|
159
|
+
await bot.craft(recipe as never, 1, recipe.requiresTable ? table ?? undefined : undefined);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const msg = zhErrorText((err as Error).message);
|
|
162
|
+
throw new SkillBlocked(
|
|
163
|
+
n === 0 ? `合成 ${label}: ${msg}` : `合成 ${label} 做到第 ${n + 1} 次时: ${msg}。${doneSoFar()}`,
|
|
164
|
+
[], 'server',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (targetId !== null && beforeOne !== null) {
|
|
168
|
+
const got = await awaitCraftGain(bot, targetId, beforeOne, ctx);
|
|
169
|
+
if (got === 0) lateRounds.push(n + 1);
|
|
170
|
+
ctx.diag?.write({
|
|
171
|
+
lane: 'craft', event: got === 0 ? 'no-gain' : 'gain', taskId: ctx.taskId,
|
|
172
|
+
msg: `合成 ${label} 第 ${n + 1}/${times} 次,入包 ${got} 个`,
|
|
173
|
+
data: { item: label, before: beforeOne, got, requiresTable: recipe.requiresTable },
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
await sleep(CRAFT_SETTLE_MS);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const gains = invGains(before, bot);
|
|
181
|
+
const targetGain = targetId === null ? 0 : invCountById(bot, targetId) - targetBefore;
|
|
182
|
+
ctx.diag?.write({
|
|
183
|
+
lane: 'craft', event: 'verify', taskId: ctx.taskId,
|
|
184
|
+
msg: gains.length > 0 ? `净增 ${gains.join('、')}` : '一样都没多出来',
|
|
185
|
+
data: { label, times, gains, targetGain, lateRounds },
|
|
186
|
+
});
|
|
187
|
+
/**
|
|
188
|
+
* 单次 no-gain 只记现场,槽位回灌可能落入下一次等待窗口。
|
|
189
|
+
* 合成以目标物品总净增裁决,不因某一次尚未入包立即停止。
|
|
190
|
+
*/
|
|
191
|
+
const late = lateRounds.length > 0
|
|
192
|
+
? `(第 ${lateRounds.join('、')} 次当场没读到入包,服务端槽位回灌迟到)`
|
|
193
|
+
: '';
|
|
194
|
+
if (gains.length === 0 || (targetId !== null && targetGain <= 0)) {
|
|
195
|
+
throw new SkillBlocked(
|
|
196
|
+
`摆了 ${times} 次${label},包里${targetId !== null && gains.length > 0
|
|
197
|
+
? `${label}一个都没多` : '一样都没多出来'}${late}。${doneSoFar()}`,
|
|
198
|
+
gains.length > 0 ? [`这一步包里多出来的是:${gains.join('、')}`] : [], 'server',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
// 合成出来的手持工具和武器立即拿上;运输用的方块不动。
|
|
202
|
+
const held = targetId !== null ? await equipIfHandheld(bot, nameOfId(bot, targetId)) : false;
|
|
203
|
+
return `${made.length > 0 ? `${made.join('、')};` : ''}合成出来:${gains.join('、')}` +
|
|
204
|
+
(targetId !== null && targetGain < call.count ? `(要 ${call.count} 个,只多出 ${targetGain} 个)${late}` : '') +
|
|
205
|
+
(held ? ',已经拿在手上' : '');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function nameOfId(bot: Bot, id: number): string {
|
|
209
|
+
return (bot.registry.items as Record<number, { name: string }>)[id]?.name ?? '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 合成的工具或武器自动切换到主手。
|
|
214
|
+
* 换手失败不回滚已完成的合成,并通过返回值报告。
|
|
215
|
+
*/
|
|
216
|
+
export async function equipIfHandheld(bot: Bot, itemName: string): Promise<boolean> {
|
|
217
|
+
if (!HANDHELD_SUFFIXES.some((s) => itemName.endsWith(s))) return false;
|
|
218
|
+
const item = bot.inventory.items().find((i) => i.name === itemName);
|
|
219
|
+
if (!item) return false;
|
|
220
|
+
try {
|
|
221
|
+
await bot.equip(item, 'hand');
|
|
222
|
+
return true;
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 等背包扣掉那一个食物的上限:原版进食动画 1.61 秒,余量给背包同步 */
|
|
229
|
+
export const EAT_SETTLE_MS = 2_500;
|
|
230
|
+
|
|
231
|
+
export async function consumeHeldFood(bot: Bot, foodName: string): Promise<string> {
|
|
232
|
+
// 牛奶这类东西不给饱食度,拿 food 读数当结果就是编:它的结果是状态效果没了、
|
|
233
|
+
// 手里剩个空桶(见 DRINKABLES)。判成没成仍用同一条「包里少了一个」的因果事实。
|
|
234
|
+
const drink = DRINKABLES[foodName] ?? null;
|
|
235
|
+
const before = bot.food;
|
|
236
|
+
const bagBefore = invCount(bot, (n) => n === foodName);
|
|
237
|
+
const emptyBefore = drink ? invCount(bot, (n) => n === drink.empty) : 0;
|
|
238
|
+
try {
|
|
239
|
+
await bot.consume();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const message = (err as Error).message;
|
|
242
|
+
if (message === 'Food is full') return `饥饿已经是 ${bot.food}/20,吃不下,没吃`;
|
|
243
|
+
throw new SkillBlocked(`吃不下去: ${zhErrorText(message)}`);
|
|
244
|
+
}
|
|
245
|
+
// consume() 可被 heldItemChanged 提前 resolve;food 也可能因跑步、死亡或重生变化。
|
|
246
|
+
// 进食以所选食物库存减少作为因果判据。
|
|
247
|
+
const settle = Date.now() + EAT_SETTLE_MS;
|
|
248
|
+
while (invCount(bot, (n) => n === foodName) === bagBefore && Date.now() < settle) await sleep(50);
|
|
249
|
+
// 回执同时报告进食前后读数,即使两次读数相同。
|
|
250
|
+
if (invCount(bot, (n) => n === foodName) >= bagBefore) {
|
|
251
|
+
throw new SkillBlocked(
|
|
252
|
+
drink
|
|
253
|
+
? `举着${zhName(foodName)}喝了一下,但包里还是 ${bagBefore} 个,没喝进去`
|
|
254
|
+
: `啃了一口${zhName(foodName)},但包里还是 ${bagBefore} 个、饥饿 ${before} → ${bot.food}/20,没吃进去`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (drink) {
|
|
258
|
+
// 空桶回没回包里也照报:她下一步要拿它去装水/装奶,数得出来才算这一口有交代
|
|
259
|
+
const emptyAfter = invCount(bot, (n) => n === drink.empty);
|
|
260
|
+
const back = emptyAfter > emptyBefore
|
|
261
|
+
? `,${zhName(drink.empty)}回到包里(现在 ${emptyAfter} 个)`
|
|
262
|
+
: `,但包里的${zhName(drink.empty)}还是 ${emptyAfter} 个`;
|
|
263
|
+
// 不记进食时刻:这一口不管饱,`[口粮]` 那行说「上次进食」就得是真吃过东西
|
|
264
|
+
return `喝了${drink.label},${drink.effect}${back}`;
|
|
265
|
+
}
|
|
266
|
+
noteAte(bot);
|
|
267
|
+
return `吃了一个${zhName(foodName)},饥饿 ${before} → ${bot.food}/20`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function skillEat(bot: Bot, itemName: string): Promise<string> {
|
|
271
|
+
const foods = (bot.registry.foodsByName ?? {}) as Record<string, unknown>;
|
|
272
|
+
if (!foods[itemName] && !DRINKABLES[itemName]) {
|
|
273
|
+
throw new SkillBlocked(notFoodText(bot, itemName));
|
|
274
|
+
}
|
|
275
|
+
// eat 的 item 是完整物品 id。这里不做后缀或模糊匹配,避免把另一样食物替换进来。
|
|
276
|
+
const food = bot.inventory.items().find((item) => item.name === itemName && item.count > 0);
|
|
277
|
+
if (!food) {
|
|
278
|
+
// 手边还有什么能吃是她下一单要用的事实;一样都没有就是「包里没有任何食物」那一句
|
|
279
|
+
throw new SkillBlocked(`包里没有点名的${zhName(itemName)};${edibleInBag(bot) ?? '包里没有任何食物'}`);
|
|
280
|
+
}
|
|
281
|
+
await bot.equip(food, 'hand');
|
|
282
|
+
return consumeHeldFood(bot, itemName);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** 按名字拿到手上 */
|
|
286
|
+
export async function equipNamed(bot: Bot, name: string): Promise<string> {
|
|
287
|
+
const item = bot.inventory.items().find((i) => i.name === name || i.name.endsWith(`_${name}`));
|
|
288
|
+
if (!item) throw new SkillBlocked(`包里没有${zhName(name)}`);
|
|
289
|
+
await bot.equip(item, 'hand');
|
|
290
|
+
return item.name;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* equipEmpty 优先切换到空快捷栏,再将主手物品移入背包。
|
|
295
|
+
* 背包也满时会将主手栈丢到地上,回执须说明。
|
|
296
|
+
*/
|
|
297
|
+
export async function emptyHand(bot: Bot): Promise<string> {
|
|
298
|
+
const held = bot.heldItem;
|
|
299
|
+
if (!held) return '主手本来就是空的';
|
|
300
|
+
const before = invCount(bot, (n) => n === held.name);
|
|
301
|
+
await bot.unequip('hand');
|
|
302
|
+
const left = invCount(bot, (n) => n === held.name);
|
|
303
|
+
if (left < before) {
|
|
304
|
+
return `主手腾空了;包是满的,${zhName(held.name)}×${before - left}被扔在了脚下`;
|
|
305
|
+
}
|
|
306
|
+
return `主手腾空了(原来拿的是${zhName(held.name)})`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** item 缺省时腾空主手;air 别名由 parseEquip 归一化。 */
|
|
310
|
+
export async function skillEquip(bot: Bot, call: Extract<SkillCall, { skill: 'equip' }>): Promise<string> {
|
|
311
|
+
const want = call.item;
|
|
312
|
+
if (!want) return emptyHand(bot);
|
|
313
|
+
// 精确名优先;退而求其次才用后缀(iron→iron_pickaxe),`includes` 会让
|
|
314
|
+
// equip "iron" 命中哪一件全看物品栏顺序,那不是她说的意思
|
|
315
|
+
const items = bot.inventory.items();
|
|
316
|
+
const item = invItemNamed(bot, want, call.pick);
|
|
317
|
+
if (!item) {
|
|
318
|
+
const same = items.filter((i) => namedLike(want, i.name));
|
|
319
|
+
if (call.pick && same.length > 0) throw noSuchItem(bot, want, call.pick, same);
|
|
320
|
+
const near = items.filter((i) => i.name.includes(want)).map((i) => zhName(i.name));
|
|
321
|
+
throw new SkillBlocked(
|
|
322
|
+
`包里没有${zhName(want)}` + (near.length > 0 ? `;名字带这几个字的有:${near.join('、')}` : ''),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const dest = equipDestOf(item.name, bot.registry);
|
|
326
|
+
await bot.equip(item, dest);
|
|
327
|
+
// 点名拿的时候回执念全标签:「拿起了弓」答不了「拿的是无限那把吗」
|
|
328
|
+
const what = call.pick ? pickLabel(pickTargetOf(item, bot.registry as never)) : zhName(item.name);
|
|
329
|
+
if (dest === 'hand') return `手里拿起了${what}`;
|
|
330
|
+
if (dest === 'off-hand') return `${what}挂上了副手`;
|
|
331
|
+
return `穿上了${what}`;
|
|
332
|
+
}
|
|
333
|
+
|