cortico 0.1.0
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/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +43 -0
- package/src/boot.ts +46 -0
- package/src/bot.ts +1342 -0
- package/src/config-file.ts +41 -0
- package/src/core/README.md +92 -0
- package/src/core/billing.ts +26 -0
- package/src/core/blobs.ts +106 -0
- package/src/core/bus.ts +311 -0
- package/src/core/config-schema.ts +201 -0
- package/src/core/config.ts +262 -0
- package/src/core/core.ts +699 -0
- package/src/core/cost.ts +244 -0
- package/src/core/event-store.ts +304 -0
- package/src/core/fork.ts +113 -0
- package/src/core/generation.ts +118 -0
- package/src/core/instance-lock.ts +215 -0
- package/src/core/ipc-logger.ts +105 -0
- package/src/core/language.ts +62 -0
- package/src/core/log-context.ts +38 -0
- package/src/core/loop.ts +1858 -0
- package/src/core/markers.ts +48 -0
- package/src/core/prefix.ts +121 -0
- package/src/core/run.ts +95 -0
- package/src/core/secrets.ts +18 -0
- package/src/core/session.ts +56 -0
- package/src/core/sessions.ts +261 -0
- package/src/core/state.ts +104 -0
- package/src/core/template.ts +72 -0
- package/src/core/timers.ts +129 -0
- package/src/core/tool-log.ts +135 -0
- package/src/core/transcript.ts +92 -0
- package/src/core/truncate.ts +121 -0
- package/src/core/types.ts +1140 -0
- package/src/core/usage-log.ts +79 -0
- package/src/core/util.ts +386 -0
- package/src/deploy-listing.ts +160 -0
- package/src/deploy.ts +162 -0
- package/src/extensions/README.md +72 -0
- package/src/extensions/dry-mount.ts +390 -0
- package/src/extensions/manifest.ts +153 -0
- package/src/extensions/runtime.ts +42 -0
- package/src/extensions.ts +519 -0
- package/src/launcher.ts +285 -0
- package/src/paths.ts +155 -0
- package/src/protocol/open-responses/LICENSE +201 -0
- package/src/protocol/open-responses/README.md +14 -0
- package/src/protocol/open-responses/context-helpers.ts +27 -0
- package/src/protocol/open-responses/context-log.ts +59 -0
- package/src/protocol/open-responses/context.ts +89 -0
- package/src/protocol/open-responses/generated.ts +115 -0
- package/src/protocol/open-responses/index.ts +33 -0
- package/src/protocol/open-responses/openapi.json +4230 -0
- package/src/protocol/open-responses/stream.ts +146 -0
- package/src/protocol/open-responses/tokens.ts +31 -0
- package/src/providers/README.md +86 -0
- package/src/providers/base.ts +111 -0
- package/src/providers/configuration.ts +97 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/settings.ts +443 -0
- package/src/providers/console/strings.ts +74 -0
- package/src/providers/console/types.ts +11 -0
- package/src/providers/llamacpp/archive.ts +148 -0
- package/src/providers/llamacpp/catalog.ts +247 -0
- package/src/providers/llamacpp/config.ts +36 -0
- package/src/providers/llamacpp/console/client.ts +6 -0
- package/src/providers/llamacpp/console/models-panel.ts +120 -0
- package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
- package/src/providers/llamacpp/console/server.ts +153 -0
- package/src/providers/llamacpp/index.ts +95 -0
- package/src/providers/llamacpp/native.ts +51 -0
- package/src/providers/llamacpp/options.ts +117 -0
- package/src/providers/llamacpp/runtime-store.ts +139 -0
- package/src/providers/llamacpp/runtime.ts +214 -0
- package/src/providers/llamacpp/server.ts +286 -0
- package/src/providers/llamacpp/strings.ts +230 -0
- package/src/providers/openai-responses-compat/config.ts +20 -0
- package/src/providers/openai-responses-compat/index.ts +89 -0
- package/src/providers/openai-responses-compat/native.ts +127 -0
- package/src/providers/openai-responses-compat/strings.ts +19 -0
- package/src/providers/pricebook.ts +75 -0
- package/src/providers/registry.ts +137 -0
- package/src/providers/strings.ts +71 -0
- package/src/providers/transport/chat.ts +156 -0
- package/src/providers/transport/errors.ts +48 -0
- package/src/providers/transport/history.ts +68 -0
- package/src/providers/transport/native-input.ts +83 -0
- package/src/providers/transport/native-types.ts +13 -0
- package/src/providers/transport/response-assembly.ts +191 -0
- package/src/providers/transport/response-http.ts +196 -0
- package/src/providers/transport/response-meters.ts +27 -0
- package/src/providers/transport/responses-input.ts +56 -0
- package/src/web/README.md +84 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
- package/src/web/client/console-pages/builtins.ts +10 -0
- package/src/web/client/console-pages/context.ts +135 -0
- package/src/web/client/console-pages/host.ts +633 -0
- package/src/web/client/console-pages/loader.ts +132 -0
- package/src/web/client/console-pages/strings.ts +90 -0
- package/src/web/client/console-pages/tools/strings.ts +49 -0
- package/src/web/client/console-pages/tools/view.ts +215 -0
- package/src/web/client/core/api.ts +200 -0
- package/src/web/client/core/language.ts +56 -0
- package/src/web/client/core/lifecycle.ts +132 -0
- package/src/web/client/core/router.ts +199 -0
- package/src/web/client/core/stream.ts +157 -0
- package/src/web/client/core/websocket.ts +82 -0
- package/src/web/client/features/appearance/index.ts +333 -0
- package/src/web/client/features/appearance/strings.ts +87 -0
- package/src/web/client/features/config/index.ts +11 -0
- package/src/web/client/features/config/strings.ts +41 -0
- package/src/web/client/features/config/view.ts +428 -0
- package/src/web/client/features/core/events.ts +197 -0
- package/src/web/client/features/core/index.ts +254 -0
- package/src/web/client/features/core/run.ts +56 -0
- package/src/web/client/features/core/runlog.ts +214 -0
- package/src/web/client/features/core/sessions.ts +53 -0
- package/src/web/client/features/core/strings.ts +157 -0
- package/src/web/client/features/extensions/index.ts +422 -0
- package/src/web/client/features/extensions/strings.ts +164 -0
- package/src/web/client/features/feature.ts +93 -0
- package/src/web/client/features/live/context.ts +280 -0
- package/src/web/client/features/live/fork.ts +156 -0
- package/src/web/client/features/live/index.ts +392 -0
- package/src/web/client/features/live/onboarding.ts +113 -0
- package/src/web/client/features/live/protocol.ts +122 -0
- package/src/web/client/features/live/sessions.ts +71 -0
- package/src/web/client/features/live/status.ts +68 -0
- package/src/web/client/features/live/strings.ts +252 -0
- package/src/web/client/features/live/timeline.ts +590 -0
- package/src/web/client/features/prompts/editor.ts +292 -0
- package/src/web/client/features/prompts/index.ts +346 -0
- package/src/web/client/features/prompts/strings.ts +136 -0
- package/src/web/client/features/prompts/view.ts +256 -0
- package/src/web/client/features/providers/index.ts +100 -0
- package/src/web/client/features/providers/strings.ts +23 -0
- package/src/web/client/features/settings/general.ts +28 -0
- package/src/web/client/features/settings/index.ts +82 -0
- package/src/web/client/features/settings/strings.ts +33 -0
- package/src/web/client/features/storage/index.ts +1 -0
- package/src/web/client/features/storage/strings.ts +45 -0
- package/src/web/client/features/storage/view.ts +154 -0
- package/src/web/client/features/usage/chart.ts +486 -0
- package/src/web/client/features/usage/index.ts +407 -0
- package/src/web/client/features/usage/labels.ts +46 -0
- package/src/web/client/features/usage/range.ts +36 -0
- package/src/web/client/features/usage/state.ts +37 -0
- package/src/web/client/features/usage/strings.ts +191 -0
- package/src/web/client/features/usage/tooltip.ts +86 -0
- package/src/web/client/features/usage/types.ts +96 -0
- package/src/web/client/features/worlds/index.ts +424 -0
- package/src/web/client/features/worlds/strings.ts +121 -0
- package/src/web/client/main.ts +273 -0
- package/src/web/client/shell/avatar.ts +233 -0
- package/src/web/client/shell/index.ts +549 -0
- package/src/web/client/shell/strings.ts +89 -0
- package/src/web/client/theme/handoff.ts +43 -0
- package/src/web/client/theme/palette.ts +109 -0
- package/src/web/client/theme/registry.ts +130 -0
- package/src/web/client/theme/storage.ts +72 -0
- package/src/web/client/theme/strings.ts +123 -0
- package/src/web/client/theme/studio.ts +366 -0
- package/src/web/client/ui/actions.ts +109 -0
- package/src/web/client/ui/data.ts +185 -0
- package/src/web/client/ui/dom.ts +23 -0
- package/src/web/client/ui/fields.ts +191 -0
- package/src/web/client/ui/format.ts +75 -0
- package/src/web/client/ui/icons.ts +183 -0
- package/src/web/client/ui/images.ts +91 -0
- package/src/web/client/ui/index.ts +89 -0
- package/src/web/client/ui/lamp.ts +125 -0
- package/src/web/client/ui/log.ts +98 -0
- package/src/web/client/ui/overlay.ts +184 -0
- package/src/web/client/ui/page.ts +8 -0
- package/src/web/client/ui/prompt-input.tsx +311 -0
- package/src/web/client/ui/sheet.ts +131 -0
- package/src/web/client/ui/strings.ts +71 -0
- package/src/web/console-pages.ts +367 -0
- package/src/web/files.ts +107 -0
- package/src/web/path-picker.ts +296 -0
- package/src/web/public/index.html +17 -0
- package/src/web/public/styles.css +1715 -0
- package/src/web/server.ts +1883 -0
- package/src/web/shared/client-panel.ts +603 -0
- package/src/web/shared/console-protocol.ts +560 -0
- package/src/web/shared/css.d.ts +1 -0
- package/src/web/shared/path-picker.ts +23 -0
- package/src/web/shared/theme.ts +256 -0
- package/src/web/theme-store.ts +37 -0
- package/src/world.ts +449 -0
- package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
- package/src/worlds/bilibili/README.md +176 -0
- package/src/worlds/bilibili/audience-admission.ts +995 -0
- package/src/worlds/bilibili/client.ts +528 -0
- package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
- package/src/worlds/bilibili/config.ts +185 -0
- package/src/worlds/bilibili/console/client.ts +202 -0
- package/src/worlds/bilibili/definition.ts +35 -0
- package/src/worlds/bilibili/gift-frame.ts +131 -0
- package/src/worlds/bilibili/normalize.ts +452 -0
- package/src/worlds/bilibili/overlay/announcement.ts +76 -0
- package/src/worlds/bilibili/overlay/assets.ts +82 -0
- package/src/worlds/bilibili/overlay/model.ts +494 -0
- package/src/worlds/bilibili/overlay/project.ts +202 -0
- package/src/worlds/bilibili/overlay/server.ts +286 -0
- package/src/worlds/bilibili/overlay/types.ts +217 -0
- package/src/worlds/bilibili/overlay/web/app.js +690 -0
- package/src/worlds/bilibili/overlay/web/editor.css +297 -0
- package/src/worlds/bilibili/overlay/web/editor.html +102 -0
- package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
- package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
- package/src/worlds/bilibili/overlay/web/styles.css +258 -0
- package/src/worlds/bilibili/protobuf.ts +123 -0
- package/src/worlds/bilibili/wire.ts +94 -0
- package/src/worlds/bilibili/world.ts +1389 -0
- package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
- package/src/worlds/console-fixture/console/client.ts +72 -0
- package/src/worlds/console-fixture/world.ts +73 -0
- package/src/worlds/index.ts +15 -0
- package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
- package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
- package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
- package/src/worlds/minecraft/README.md +917 -0
- package/src/worlds/minecraft/blueprint-plan.ts +915 -0
- package/src/worlds/minecraft/blueprint-registry.ts +566 -0
- package/src/worlds/minecraft/blueprint-repair.ts +236 -0
- package/src/worlds/minecraft/blueprint-resource.ts +272 -0
- package/src/worlds/minecraft/blueprint.ts +492 -0
- package/src/worlds/minecraft/body-lease.ts +340 -0
- package/src/worlds/minecraft/bridge.ts +749 -0
- package/src/worlds/minecraft/check.ts +651 -0
- package/src/worlds/minecraft/chests.ts +292 -0
- package/src/worlds/minecraft/client-launch.ts +272 -0
- package/src/worlds/minecraft/client-options.ts +85 -0
- package/src/worlds/minecraft/client-skins.ts +164 -0
- package/src/worlds/minecraft/client-window.ps1 +91 -0
- package/src/worlds/minecraft/client.ts +423 -0
- package/src/worlds/minecraft/combat-context.ts +109 -0
- package/src/worlds/minecraft/combat.ts +1552 -0
- package/src/worlds/minecraft/config.ts +499 -0
- package/src/worlds/minecraft/console/access.ts +218 -0
- package/src/worlds/minecraft/console/client.ts +244 -0
- package/src/worlds/minecraft/console/log.ts +110 -0
- package/src/worlds/minecraft/console/mount.ts +420 -0
- package/src/worlds/minecraft/console/skin.ts +257 -0
- package/src/worlds/minecraft/console/style.css +154 -0
- package/src/worlds/minecraft/console/world.ts +449 -0
- package/src/worlds/minecraft/deaths.ts +135 -0
- package/src/worlds/minecraft/definition.ts +18 -0
- package/src/worlds/minecraft/engine-child.ts +274 -0
- package/src/worlds/minecraft/engine-ipc.ts +98 -0
- package/src/worlds/minecraft/entity-facts.ts +132 -0
- package/src/worlds/minecraft/escape.ts +407 -0
- package/src/worlds/minecraft/executor.ts +14494 -0
- package/src/worlds/minecraft/explored.ts +156 -0
- package/src/worlds/minecraft/geometry.ts +205 -0
- package/src/worlds/minecraft/goal-plan.ts +647 -0
- package/src/worlds/minecraft/item-break.ts +126 -0
- package/src/worlds/minecraft/item-facts.ts +146 -0
- package/src/worlds/minecraft/item-pick.ts +67 -0
- package/src/worlds/minecraft/level-dat.ts +176 -0
- package/src/worlds/minecraft/log.ts +132 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
- package/src/worlds/minecraft/names.ts +377 -0
- package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
- package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
- package/src/worlds/minecraft/piglin.ts +49 -0
- package/src/worlds/minecraft/policy.ts +547 -0
- package/src/worlds/minecraft/precheck.ts +655 -0
- package/src/worlds/minecraft/proxy.ts +445 -0
- package/src/worlds/minecraft/ranged.ts +635 -0
- package/src/worlds/minecraft/readouts.ts +102 -0
- package/src/worlds/minecraft/round.ts +26 -0
- package/src/worlds/minecraft/search-observation.ts +92 -0
- package/src/worlds/minecraft/server-config.ts +467 -0
- package/src/worlds/minecraft/server.ts +658 -0
- package/src/worlds/minecraft/show.ts +84 -0
- package/src/worlds/minecraft/skills.ts +1763 -0
- package/src/worlds/minecraft/terrain.ts +1735 -0
- package/src/worlds/minecraft/window.ts +96 -0
- package/src/worlds/minecraft/works.ts +179 -0
- package/src/worlds/minecraft/world.ts +6438 -0
- package/src/worlds/qq/ENV_PROMPT.md +5 -0
- package/src/worlds/qq/config.ts +108 -0
- package/src/worlds/qq/console/client.ts +126 -0
- package/src/worlds/qq/console/events.ts +155 -0
- package/src/worlds/qq/console/gate.ts +232 -0
- package/src/worlds/qq/console/roster.ts +203 -0
- package/src/worlds/qq/console/style.css +65 -0
- package/src/worlds/qq/conversation.ts +28 -0
- package/src/worlds/qq/definition.ts +62 -0
- package/src/worlds/qq/driver.ts +375 -0
- package/src/worlds/qq/history-tools.ts +209 -0
- package/src/worlds/qq/normalize.ts +323 -0
- package/src/worlds/qq/qface-map.ts +282 -0
- package/src/worlds/qq/vision-prompt.ts +27 -0
- package/src/worlds/qq/vision.ts +642 -0
- package/src/worlds/qq/vlm.ts +142 -0
- package/src/worlds/qq/world.ts +1503 -0
- package/src/worlds/terminal/ENV_PROMPT.md +11 -0
- package/src/worlds/terminal/config.ts +9 -0
- package/src/worlds/terminal/definition.ts +14 -0
- package/src/worlds/terminal/world.ts +700 -0
- package/src/worlds/websearch/ENV_PROMPT.md +1 -0
- package/src/worlds/websearch/brave-client.ts +261 -0
- package/src/worlds/websearch/config.ts +75 -0
- package/src/worlds/websearch/definition.ts +20 -0
- package/src/worlds/websearch/world.ts +171 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 前置试算以纯读判据判断步骤当前是否可执行。
|
|
3
|
+
*
|
|
4
|
+
* 受理时检查全部步骤,hard/soft 否定结果均进入回执,全通过则静默,不改变执行。
|
|
5
|
+
* 出队时在每步动手前重查;hard 命中即停在该步,soft 只报告。
|
|
6
|
+
* hard 表示当前可证失败的材料、几何等条件;区块未加载或活物暂不在场归 soft。
|
|
7
|
+
*
|
|
8
|
+
* 不 equip、place、寻路或发包;goto 的路线试算成本较高,只由 dryRun 显式调用。
|
|
9
|
+
*/
|
|
10
|
+
import type { Bot } from 'mineflayer';
|
|
11
|
+
import type { SkillCall } from './skills.ts';
|
|
12
|
+
import type { Cell } from './geometry.ts';
|
|
13
|
+
import { matchItemName } from './chests.ts';
|
|
14
|
+
import { DRINKABLES } from './item-facts.ts';
|
|
15
|
+
import { itemMatchesPick, pickMissText, pickTargetOf } from './item-pick.ts';
|
|
16
|
+
import { zhDimension, zhEntity, zhName } from './names.ts';
|
|
17
|
+
import { bestRangedWeapon, hasRangedLos, hasUsableArrows, type RangedTarget } from './ranged.ts';
|
|
18
|
+
|
|
19
|
+
type PrecheckLevel = 'hard' | 'soft';
|
|
20
|
+
|
|
21
|
+
interface PrecheckNote {
|
|
22
|
+
level: PrecheckLevel;
|
|
23
|
+
/** 给她看的一句话;不带「没做成」这类完成体措辞,因为还没动工 */
|
|
24
|
+
text: string;
|
|
25
|
+
/** 判据代号,给台架与日志对账用 */
|
|
26
|
+
rule: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 背包里这件东西有几个。名字口径复用 chests.matchItemName,不自造第二套 */
|
|
30
|
+
function hasItem(bot: Bot, name: string, pick?: string): number {
|
|
31
|
+
let n = 0;
|
|
32
|
+
for (const it of bot.inventory.items()) {
|
|
33
|
+
if ((matchItemName(name, it.name) || zhName(it.name) === name)
|
|
34
|
+
&& itemMatchesPick(pick, it, bot.registry as never)) n += it.count;
|
|
35
|
+
}
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 读方块经 deps.blockAt 调用执行器的 blockAtCell,由它构造 Vec3。
|
|
41
|
+
* Mineflayer 要求坐标具有 floored(),不能传普通坐标对象。
|
|
42
|
+
*/
|
|
43
|
+
type BlockLike = { name: string; boundingBox?: string } | null | undefined;
|
|
44
|
+
|
|
45
|
+
/** 缺口并列时最多列几条配方;超出的只报条数,不糊一屏 */
|
|
46
|
+
const CRAFT_TIED_MAX = 3;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* craft:配方齐不齐。与 skillCraft 问的是同一份数据(bot.recipesAll + 背包),
|
|
50
|
+
* 所以它说凑不齐,真跑也一定凑不齐 —— 台架 `bench-precheck.ts` 锁死这条蕴含。
|
|
51
|
+
*/
|
|
52
|
+
function precheckCraft(bot: Bot, call: Extract<SkillCall, { skill: 'craft' }>): PrecheckNote | null {
|
|
53
|
+
if (call.grid) return null; // 自摆配方不预判
|
|
54
|
+
const item = call.item ?? '';
|
|
55
|
+
const def = bot.registry?.itemsByName?.[item];
|
|
56
|
+
if (!def) return { level: 'hard', text: `不认识「${item}」这种物品`, rule: 'craft.unknownItem' };
|
|
57
|
+
const label = zhName(def.name);
|
|
58
|
+
let all: Array<{ delta?: Array<{ id: number; count: number }> }> = [];
|
|
59
|
+
try {
|
|
60
|
+
all = (bot.recipesAll?.(def.id, null, true as never) ?? []) as never;
|
|
61
|
+
} catch { return null; }
|
|
62
|
+
if (all.length === 0) return null; // 没配方是另一类受阻,交给技能自己说
|
|
63
|
+
const have = new Map<number, number>();
|
|
64
|
+
for (const it of bot.inventory.items()) have.set(it.type, (have.get(it.type) ?? 0) + it.count);
|
|
65
|
+
const needsOf = (r: { delta?: Array<{ id: number; count: number }> }): Map<number, number> => {
|
|
66
|
+
const need = new Map<number, number>();
|
|
67
|
+
for (const d of r.delta ?? []) if (d.count < 0) need.set(d.id, (need.get(d.id) ?? 0) + -d.count);
|
|
68
|
+
return need;
|
|
69
|
+
};
|
|
70
|
+
const ready = all.find((r) => [...needsOf(r)].every(([id, n]) => (have.get(id) ?? 0) >= n));
|
|
71
|
+
if (ready) return null;
|
|
72
|
+
const options = all.map((r) => {
|
|
73
|
+
const need = needsOf(r);
|
|
74
|
+
const miss = [...need]
|
|
75
|
+
.map(([id, n]) => ({ id, gap: n - (have.get(id) ?? 0) }))
|
|
76
|
+
.filter((x) => x.gap > 0);
|
|
77
|
+
return { miss, total: miss.reduce((a, b) => a + b.gap, 0) };
|
|
78
|
+
});
|
|
79
|
+
const gapsOf = (o: { miss: Array<{ id: number; gap: number }> }): string => o.miss
|
|
80
|
+
.map((x) => `${zhName(bot.registry.items[x.id]?.name ?? String(x.id))}×${x.gap}`)
|
|
81
|
+
.join('、');
|
|
82
|
+
// 最小缺口并列时列出所有并列配方,与 skillCraft 使用相同的“或”连接语义。
|
|
83
|
+
const min = Math.min(...options.map((o) => o.total));
|
|
84
|
+
const tied = [...new Set(options.filter((o) => o.total === min).map(gapsOf))];
|
|
85
|
+
const more = tied.length > CRAFT_TIED_MAX ? `;还有 ${tied.length - CRAFT_TIED_MAX} 条配方缺口一样多` : '';
|
|
86
|
+
const gaps = tied.slice(0, CRAFT_TIED_MAX).join(' 或 ');
|
|
87
|
+
return { level: 'hard', text: `做${label}还差 ${gaps}${more}`, rule: 'craft.short' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** eat:点名的食物是否存在。与 skillEat 共用精确物品 id 口径。 */
|
|
91
|
+
function precheckEat(
|
|
92
|
+
bot: Bot,
|
|
93
|
+
call: Extract<SkillCall, { skill: 'eat' }>,
|
|
94
|
+
deps: PrecheckDeps,
|
|
95
|
+
): PrecheckNote | null {
|
|
96
|
+
const foods = (bot.registry?.foodsByName ?? {}) as Record<string, unknown>;
|
|
97
|
+
// 牛奶桶不给饱食度、不在 foods 表里,但它是喝得掉的(见 DRINKABLES)
|
|
98
|
+
if (!foods[call.item] && !DRINKABLES[call.item]) {
|
|
99
|
+
return { level: 'hard', text: notFoodText(bot, call.item), rule: 'eat.notFood' };
|
|
100
|
+
}
|
|
101
|
+
if (!bot.inventory.items().some((i) => i.name === call.item && i.count > 0)) {
|
|
102
|
+
const edible = edibleInBag(bot);
|
|
103
|
+
return edible
|
|
104
|
+
? { level: 'hard', text: `包里没有点名的${zhName(call.item)};${edible}`, rule: 'eat.noStock' }
|
|
105
|
+
: { level: 'hard', text: rationText(bot, deps), rule: 'eat.none' };
|
|
106
|
+
}
|
|
107
|
+
// 吃饱了是这一步的正常结局,执行器不会调用 consume()。
|
|
108
|
+
if (bot.food >= 20) return null;
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 返回背包可食物清单,空清单返回 null;用于点名食物缺失后的受阻说明。 */
|
|
113
|
+
export function edibleInBag(bot: Bot): string | null {
|
|
114
|
+
const foods = (bot.registry?.foodsByName ?? {}) as Record<string, unknown>;
|
|
115
|
+
const counts = new Map<string, number>();
|
|
116
|
+
for (const it of bot.inventory.items()) {
|
|
117
|
+
if (it.count > 0 && foods[it.name]) counts.set(it.name, (counts.get(it.name) ?? 0) + it.count);
|
|
118
|
+
}
|
|
119
|
+
if (counts.size === 0) return null;
|
|
120
|
+
return `包里能吃的有:${[...counts].map(([n, c]) => `${zhName(n)}×${c}`).join('、')}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 未知物品 id 的受阻说明,同时列出背包内词根相同的候选。 */
|
|
124
|
+
export function notFoodText(bot: Bot, item: string): string {
|
|
125
|
+
const known = (bot.registry?.itemsByName ?? {}) as Record<string, unknown>;
|
|
126
|
+
// 物品表没装出来时认不出"这个 id 不存在",退回旧说法:宁可少说不说错
|
|
127
|
+
const head = Object.keys(known).length === 0 || known[item]
|
|
128
|
+
? `${zhName(item)}不是可进食物品`
|
|
129
|
+
: `没有 ${item} 这样的物品`;
|
|
130
|
+
const roots = new Set(item.split('_').filter((t) => t !== 'raw' && t !== 'item' && t !== 'block'));
|
|
131
|
+
const like = [...new Set(bot.inventory.items()
|
|
132
|
+
.filter((i) => i.count > 0 && i.name !== item)
|
|
133
|
+
.filter((i) => i.name.split('_').some((t) => roots.has(t)))
|
|
134
|
+
.map((i) => i.name))];
|
|
135
|
+
const near = like.length > 0
|
|
136
|
+
? `;包里名字最像的是 ${like.map((n) => `${n}(${zhName(n)})`).join('、')}`
|
|
137
|
+
: '';
|
|
138
|
+
return `${head}${near}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 无食物时的事实读数,由 renderPrecheckNotes 渲染为独立的 [口粮] 行。 */
|
|
142
|
+
function rationText(bot: Bot, deps: PrecheckDeps): string {
|
|
143
|
+
const ate = deps.lastAte?.() ?? null;
|
|
144
|
+
const since = ate === null ? '这一场还没吃过东西' : `上次进食 ${Math.round((Date.now() - ate) / 60_000)} 分钟前`;
|
|
145
|
+
return `包里没有任何食物;${since};饱食度 ${Math.round(bot.food ?? 0)}/20;生命 ${Math.ceil(bot.health ?? 0)}/20`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 玩家主物品栏格数:9 格快捷栏 + 27 格背包。盔甲四槽与副手不在其中 */
|
|
149
|
+
export const PLAYER_SLOTS = 36;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 空格数。按**槽位**数,不按名字合并 —— 附魔件与工具堆叠上限是 1,每件各占一格,
|
|
153
|
+
* 合并名字会把「三把镐」算成一格。
|
|
154
|
+
*/
|
|
155
|
+
function freeSlots(bot: Bot): number {
|
|
156
|
+
return Math.max(0, PLAYER_SLOTS - bot.inventory.items().length);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 这件东西一格能摞几个;registry 读不到时按 64(原版多数如此) */
|
|
160
|
+
function stackMaxOf(bot: Bot, name: string): number {
|
|
161
|
+
return (bot.registry?.itemsByName?.[name]?.stackSize as number | undefined) ?? 64;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 再进 `count` 个 `name` 还要几格空位:先填包里已有的未满栈,剩下的按堆叠上限分格。
|
|
166
|
+
* 纯算术,不问世界。
|
|
167
|
+
*/
|
|
168
|
+
function slotsNeeded(bot: Bot, name: string, count: number): number {
|
|
169
|
+
const stackMax = stackMaxOf(bot, name);
|
|
170
|
+
let room = 0;
|
|
171
|
+
for (const it of bot.inventory.items()) {
|
|
172
|
+
if (matchItemName(name, it.name)) room += Math.max(0, stackMax - it.count);
|
|
173
|
+
}
|
|
174
|
+
return Math.ceil(Math.max(0, count - room) / stackMax);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 报告背包剩余格数与本步所需格数,不自动插入 toss。
|
|
179
|
+
* collect/pickup 的掉落种类在试算时未知,仅在背包无空格时报。
|
|
180
|
+
*/
|
|
181
|
+
function precheckSlots(bot: Bot, call: SkillCall): PrecheckNote | null {
|
|
182
|
+
const free = freeSlots(bot);
|
|
183
|
+
const count = Math.max(1, Number((call as { count?: number }).count ?? 1));
|
|
184
|
+
const item = (call as { item?: string }).item;
|
|
185
|
+
if (item) {
|
|
186
|
+
const need = slotsNeeded(bot, item, count);
|
|
187
|
+
if (need <= free) return null;
|
|
188
|
+
return {
|
|
189
|
+
level: 'soft',
|
|
190
|
+
text: `包里剩 ${free} 格空位,这一步${zhName(item)}×${count} 预计要占 ${need} 格`,
|
|
191
|
+
rule: `${call.skill}.slots`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (free > 0) return null;
|
|
195
|
+
return {
|
|
196
|
+
level: 'soft',
|
|
197
|
+
text: `包里 ${PLAYER_SLOTS} 格全满了,这一步是往包里装东西`,
|
|
198
|
+
rule: `${call.skill}.slotsFull`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 背包里有没有这件东西:equip / use item / stow / toss 共用。
|
|
204
|
+
* 名字口径与执行器的 invItemNamed 逐条同源(精确 → _后缀 → 前缀_),
|
|
205
|
+
* 提示语也照抄技能的模糊命中那一句 —— 同一件事只有一个说法。
|
|
206
|
+
*/
|
|
207
|
+
function precheckHasItem(
|
|
208
|
+
bot: Bot, item: string, verb: string, rule: string, pick?: string,
|
|
209
|
+
): PrecheckNote | null {
|
|
210
|
+
if (!item) return null;
|
|
211
|
+
if (hasItem(bot, item, pick) > 0) return null;
|
|
212
|
+
// 有同 id 的几件而挑选词一件没中:那几件各自是什么,当场摆出来
|
|
213
|
+
if (pick && hasItem(bot, item) > 0) {
|
|
214
|
+
const same = bot.inventory.items()
|
|
215
|
+
.filter((i) => matchItemName(item, i.name))
|
|
216
|
+
.map((i) => pickTargetOf(i, bot.registry as never));
|
|
217
|
+
return { level: 'hard', text: pickMissText('包里', item, pick, same), rule };
|
|
218
|
+
}
|
|
219
|
+
const near = bot.inventory.items().filter((i) => i.name.includes(item)).map((i) => zhName(i.name));
|
|
220
|
+
const hint = near.length > 0 ? `;名字带这几个字的有:${near.join('、')}` : '';
|
|
221
|
+
return { level: 'hard', text: `包里没有${zhName(item)},${verb}不了${hint}`, rule };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 五个「从包里拿东西下手」的技能:判据同一条,只有动词不同 */
|
|
225
|
+
function precheckItemStep(bot: Bot, call: SkillCall, verb: string): PrecheckNote | null {
|
|
226
|
+
const c = call as { item?: string; pick?: string };
|
|
227
|
+
return precheckHasItem(bot, c.item ?? '', verb, `${call.skill}.noStock`, c.pick);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 点名物品缺货但目标格已是该方块时,受阻文案与执行器一致。 */
|
|
231
|
+
function precheckUseItemAt(
|
|
232
|
+
bot: Bot,
|
|
233
|
+
call: { item?: string; at?: unknown },
|
|
234
|
+
deps: PrecheckDeps,
|
|
235
|
+
): PrecheckNote | null {
|
|
236
|
+
const item = String(call.item ?? '');
|
|
237
|
+
if (!item || hasItem(bot, item) > 0) return null;
|
|
238
|
+
const cell = call.at === undefined ? null : deps.resolve(call.at);
|
|
239
|
+
const b = (cell ? deps.blockAt(cell) : null) as BlockLike;
|
|
240
|
+
if (b && cell && matchItemName(item, b.name)) {
|
|
241
|
+
return {
|
|
242
|
+
level: 'hard',
|
|
243
|
+
text: `包里没有${zhName(item)};不过 (${cell.x},${cell.y},${cell.z}) 那一格本身就是${zhName(b.name)}——要右键它不用带 item`,
|
|
244
|
+
rule: 'use.itemIsTheBlock',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return precheckHasItem(bot, item, '用', 'use.noStock');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** 人在哪个维度。与执行器 dimensionOf 同一条读法;一行属性读,不为它牵一条跨文件的线 */
|
|
251
|
+
function dimensionOf(bot: Bot): string {
|
|
252
|
+
return String(bot.game?.dimension ?? 'overworld');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* surface 在下界不成立:顶上到 y=127 全是基岩,「露天」这件事不存在。
|
|
257
|
+
* 执行器本来就当场驳回(skillSurface 的下界闸),这里只是把同一条判据提到受理刻说,
|
|
258
|
+
* 终态不变 —— 省掉的是她排好一整单、跑到这一步才发现。
|
|
259
|
+
*/
|
|
260
|
+
function precheckSurfaceHere(bot: Bot): PrecheckNote | null {
|
|
261
|
+
if (!dimensionOf(bot).includes('nether')) return null;
|
|
262
|
+
return {
|
|
263
|
+
level: 'hard',
|
|
264
|
+
text: '在下界用不了 surface:顶上到 y=127 全是基岩,没有露天可去',
|
|
265
|
+
rule: 'surface.nether',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 床只在主世界能睡,在下界/末地点它当场爆炸。
|
|
271
|
+
*
|
|
272
|
+
* **soft 不 hard**:执行器有意不拦这一下(打末影龙就是拿床当伤害手段,见 skillUse 的
|
|
273
|
+
* bedBoom 注释),这里跟着不闸,只把后果提前到受理刻说清 —— 现有回执要等炸完才有
|
|
274
|
+
* 一句解释,而炸掉的可能是自己脚边那道传送门。
|
|
275
|
+
*/
|
|
276
|
+
function precheckBedHere(
|
|
277
|
+
bot: Bot,
|
|
278
|
+
call: { at?: unknown },
|
|
279
|
+
deps: PrecheckDeps,
|
|
280
|
+
): PrecheckNote | null {
|
|
281
|
+
if (call.at === undefined) return null;
|
|
282
|
+
const dim = dimensionOf(bot);
|
|
283
|
+
if (dim.includes('overworld')) return null;
|
|
284
|
+
const cell = deps.resolve(call.at);
|
|
285
|
+
if (!cell) return null;
|
|
286
|
+
const b = deps.blockAt(cell) as BlockLike;
|
|
287
|
+
if (!b || !b.name.endsWith('_bed')) return null;
|
|
288
|
+
return {
|
|
289
|
+
level: 'soft',
|
|
290
|
+
text: `(${cell.x},${cell.y},${cell.z}) 是${zhName(b.name)};床在${zhDimension(dim)}点下去会当场爆炸`
|
|
291
|
+
+ '(炸伤自己,也炸掉旁边的方块),躺不下',
|
|
292
|
+
rule: 'use.bedExplodes',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 种子族:锄地/种地的目标格口径判据认这些(与执行器的 SEED_CROP 同一批 id) */
|
|
297
|
+
const SEEDS = new Set([
|
|
298
|
+
'wheat_seeds', 'beetroot_seeds', 'carrot', 'potato', 'melon_seeds', 'pumpkin_seeds',
|
|
299
|
+
'torchflower_seeds', 'pitcher_pod', 'nether_wart',
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
/** 锄地、种地应指向土格;目标偏到上方空气时只报告可能偏差,不修改坐标。 */
|
|
303
|
+
function precheckSoilCell(
|
|
304
|
+
bot: Bot,
|
|
305
|
+
call: { item?: string; at?: unknown },
|
|
306
|
+
deps: PrecheckDeps,
|
|
307
|
+
): PrecheckNote | null {
|
|
308
|
+
const item = String(call.item ?? '');
|
|
309
|
+
if (call.at === undefined) return null;
|
|
310
|
+
if (!item.endsWith('_hoe') && !SEEDS.has(item)) return null;
|
|
311
|
+
const cell = deps.resolve(call.at);
|
|
312
|
+
if (!cell) return null;
|
|
313
|
+
const b = deps.blockAt(cell) as BlockLike;
|
|
314
|
+
if (!b || b.name !== 'air') return null;
|
|
315
|
+
const what = item.endsWith('_hoe') ? '锄头' : '种子';
|
|
316
|
+
return {
|
|
317
|
+
level: 'soft',
|
|
318
|
+
text: `(${cell.x},${cell.y},${cell.z}) 是空气;${what}要指着土那一格(可能是 (${cell.x},${cell.y - 1},${cell.z}))`,
|
|
319
|
+
rule: 'use.soilCell',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** tunnel 坡度判据与 skillTunnel 一致:Math.abs(rise) > run 表示超过 45°。 */
|
|
324
|
+
function precheckTunnel(bot: Bot, call: Extract<SkillCall, { skill: 'tunnel' }>, resolve: (a: unknown) => Cell | null): PrecheckNote | null {
|
|
325
|
+
const p = bot.entity?.position;
|
|
326
|
+
if (!p) return null;
|
|
327
|
+
const start = { x: Math.floor(p.x), y: Math.floor(p.y), z: Math.floor(p.z) };
|
|
328
|
+
const target = resolve(call.at);
|
|
329
|
+
if (!target) return null;
|
|
330
|
+
const run = Math.max(Math.abs(target.x - start.x), Math.abs(target.z - start.z));
|
|
331
|
+
const rise = target.y - start.y;
|
|
332
|
+
if (run === 0 && rise === 0) return { level: 'hard', text: '终点就是脚下这一格', rule: 'tunnel.here' };
|
|
333
|
+
if (run !== 0 && Math.abs(rise) > run) {
|
|
334
|
+
return { level: 'hard', text: `坡度超过 45°:横 ${run} 格要升降 ${Math.abs(rise)} 格`, rule: 'tunnel.slope' };
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** 深层作业判据线:目标 y 在这以下就是深板岩层,岩浆湖密度陡增 */
|
|
340
|
+
const DEEP_KIT_Y = 0;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* tunnel/excavate 的目标深度或 collect 的当前深度 y ≤ 0 且缺水桶时,
|
|
344
|
+
* 以 soft 报告深度和缺失事实,不阻断执行。
|
|
345
|
+
*/
|
|
346
|
+
function precheckDeepKit(bot: Bot, call: SkillCall, deps: PrecheckDeps): PrecheckNote | null {
|
|
347
|
+
let y: number | null = null;
|
|
348
|
+
if (call.skill === 'tunnel') {
|
|
349
|
+
y = deps.resolve((call as { at?: unknown }).at)?.y ?? null;
|
|
350
|
+
} else if (call.skill === 'excavate') {
|
|
351
|
+
const cells = deps.cellsOf(call);
|
|
352
|
+
if (cells && cells.length > 0) y = Math.min(...cells.map((c) => c.y));
|
|
353
|
+
} else if (call.skill === 'collect') {
|
|
354
|
+
// collect 没有目标坐标,就地找块:看她现在站的深度
|
|
355
|
+
y = bot.entity?.position ? Math.floor(bot.entity.position.y) : null;
|
|
356
|
+
}
|
|
357
|
+
if (y === null || y > DEEP_KIT_Y) return null;
|
|
358
|
+
if (hasItem(bot, 'water_bucket') > 0) return null;
|
|
359
|
+
return {
|
|
360
|
+
level: 'soft',
|
|
361
|
+
text: `这一步在 y=${y}(深板岩层),包里没有水桶;还没动工`,
|
|
362
|
+
rule: `${call.skill}.deepNoWater`,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function precheckBuild(
|
|
367
|
+
bot: Bot,
|
|
368
|
+
call: Extract<SkillCall, { skill: 'build' }>,
|
|
369
|
+
deps: PrecheckDeps,
|
|
370
|
+
): PrecheckNote | null {
|
|
371
|
+
// 蓝图允许盖到料尽,不适用单一材料的缺料判据。
|
|
372
|
+
if ('blueprint' in call) return null;
|
|
373
|
+
const mat = call.material;
|
|
374
|
+
// BoatItem 通过使用物品并由服务端沿玩家视线做射线放置,不响应 use_item_on。
|
|
375
|
+
// 船和竹筏应走 useBoat,不能按普通方块逐面放置。
|
|
376
|
+
if (/_(boat|raft)$/.test(mat)) {
|
|
377
|
+
return {
|
|
378
|
+
level: 'hard',
|
|
379
|
+
text: `${zhName(mat)}不是方块,build 放不出来(服务端按视线射线生成,只认「使用物品」);`
|
|
380
|
+
+ `改用 {"skill":"use","item":"${mat}","at":[x,y,z]} 指一格水面/地面`,
|
|
381
|
+
rule: 'build.notBlock',
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const have = hasItem(bot, mat);
|
|
385
|
+
// 贴面形态:参照方块不实心 ⇒ 那一处根本贴不上
|
|
386
|
+
if ('on' in call && Array.isArray(call.on)) {
|
|
387
|
+
const bad: Cell[] = [];
|
|
388
|
+
let unloaded = 0;
|
|
389
|
+
for (const spot of call.on) {
|
|
390
|
+
const ref = deps.resolve(spot.at);
|
|
391
|
+
if (!ref) continue;
|
|
392
|
+
const rb = deps.blockAt(ref) as BlockLike;
|
|
393
|
+
if (!rb) { unloaded++; continue; }
|
|
394
|
+
if (rb.boundingBox !== 'block') bad.push(ref);
|
|
395
|
+
}
|
|
396
|
+
if (bad.length > 0) {
|
|
397
|
+
const list = bad.slice(0, 3).map((c) => `(${c.x},${c.y},${c.z})`).join('、');
|
|
398
|
+
return { level: 'hard', text: `${bad.length} 处贴不住(${list} 不是实心方块)`, rule: 'build.noRef' };
|
|
399
|
+
}
|
|
400
|
+
if (have === 0) return { level: 'hard', text: `包里没有${zhName(mat)}`, rule: 'build.noStock' };
|
|
401
|
+
if (unloaded > 0) return { level: 'soft', text: `${unloaded} 处的区块没加载,读不到`, rule: 'build.unloaded' };
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
// 锚点形态:先看料
|
|
405
|
+
const cells = deps.cellsOf(call);
|
|
406
|
+
if (have === 0) return { level: 'hard', text: `包里没有${zhName(mat)}`, rule: 'build.noStock' };
|
|
407
|
+
if (cells && cells.length > 0) {
|
|
408
|
+
// 只数还需要动手的格(已经是该方块/被占的不算)
|
|
409
|
+
let todo = 0;
|
|
410
|
+
let unloaded = 0;
|
|
411
|
+
for (const c of cells) {
|
|
412
|
+
const b = deps.blockAt(c) as BlockLike;
|
|
413
|
+
if (!b) { unloaded++; continue; }
|
|
414
|
+
if (b.name === mat) continue;
|
|
415
|
+
if (b.boundingBox === 'block') continue;
|
|
416
|
+
todo++;
|
|
417
|
+
}
|
|
418
|
+
if (todo > have) {
|
|
419
|
+
return { level: 'hard', text: `要放 ${todo} 块${zhName(mat)},包里只有 ${have} 个,差 ${todo - have}`, rule: 'build.short' };
|
|
420
|
+
}
|
|
421
|
+
if (unloaded > 0) return { level: 'soft', text: `${unloaded} 格区块没加载,读不到`, rule: 'build.unloaded' };
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** 活物当前不在目标半径内仅报告 soft;它可能在步骤执行前进入范围。 */
|
|
427
|
+
function precheckTarget(bot: Bot, target: string, radius: number, rule: string): PrecheckNote | null {
|
|
428
|
+
if (!target) return null;
|
|
429
|
+
const want = String(target).toLowerCase();
|
|
430
|
+
let near = 0;
|
|
431
|
+
const p = bot.entity?.position;
|
|
432
|
+
if (!p) return null;
|
|
433
|
+
for (const e of Object.values(bot.entities ?? {})) {
|
|
434
|
+
const n = String((e as { name?: string }).name ?? '').toLowerCase();
|
|
435
|
+
if (n !== want) continue;
|
|
436
|
+
const q = (e as { position?: { x: number; y: number; z: number } }).position;
|
|
437
|
+
if (!q) continue;
|
|
438
|
+
if (Math.hypot(q.x - p.x, q.y - p.y, q.z - p.z) <= radius) near++;
|
|
439
|
+
}
|
|
440
|
+
if (near > 0) return null;
|
|
441
|
+
return { level: 'soft', text: `${radius} 格内这会儿没有${zhEntity(target)}`, rule };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function attackEntity(bot: Bot, target: string): RangedTarget | null {
|
|
445
|
+
const want = target.toLowerCase();
|
|
446
|
+
const p = bot.entity?.position;
|
|
447
|
+
if (!p) return null;
|
|
448
|
+
let best: { target: RangedTarget; distance: number } | null = null;
|
|
449
|
+
for (const entity of Object.values(bot.entities ?? {})) {
|
|
450
|
+
const e = entity as {
|
|
451
|
+
id?: number; name?: string; username?: string; type?: string;
|
|
452
|
+
position?: { x: number; y: number; z: number }; height?: number; width?: number;
|
|
453
|
+
};
|
|
454
|
+
const label = e.type === 'player' ? e.username : e.name;
|
|
455
|
+
if (typeof e.id !== 'number' || !e.position || label?.toLowerCase() !== want) continue;
|
|
456
|
+
const distance = Math.hypot(e.position.x - p.x, e.position.y - p.y, e.position.z - p.z);
|
|
457
|
+
if (distance > 32 || (best && best.distance <= distance)) continue;
|
|
458
|
+
best = {
|
|
459
|
+
target: {
|
|
460
|
+
id: e.id, position: e.position,
|
|
461
|
+
...(e.height === undefined ? {} : { height: e.height }),
|
|
462
|
+
...(e.width === undefined ? {} : { width: e.width }),
|
|
463
|
+
},
|
|
464
|
+
distance,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return best?.target ?? null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** 强制远程的确定性缺口;auto/melee 不在试算阶段替任务选择武器。 */
|
|
471
|
+
function precheckAttack(bot: Bot, call: Extract<SkillCall, { skill: 'attack' }>): PrecheckNote | null {
|
|
472
|
+
if (call.mode !== 'ranged' && call.mode !== 'kite') return precheckTarget(bot, call.target, 32, 'attack.noTarget');
|
|
473
|
+
if (!bestRangedWeapon(bot)) {
|
|
474
|
+
return { level: 'hard', text: '包里没有可用的弓;强制远程不会改用近战', rule: 'attack.noBow' };
|
|
475
|
+
}
|
|
476
|
+
if (!hasUsableArrows(bot)) {
|
|
477
|
+
return { level: 'hard', text: '包里没有普通箭;强制远程不会改用近战', rule: 'attack.noArrow' };
|
|
478
|
+
}
|
|
479
|
+
const target = attackEntity(bot, call.target);
|
|
480
|
+
if (!target) return precheckTarget(bot, call.target, 32, 'attack.noTarget');
|
|
481
|
+
if (!hasRangedLos(bot, target)) {
|
|
482
|
+
return { level: 'hard', text: `现在看不见${zhEntity(call.target)}的射线;强制远程不会改用近战`, rule: 'attack.noLos' };
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export interface PrecheckDeps {
|
|
488
|
+
/** 锚点 → 绝对格(执行器的 resolveAt) */
|
|
489
|
+
resolve: (a: unknown) => Cell | null;
|
|
490
|
+
/** 形状 → 格清单(执行器的 shapeCells);拿不到就返回 null,那一条判据跳过 */
|
|
491
|
+
cellsOf: (c: SkillCall) => Cell[] | null;
|
|
492
|
+
/** 读一格方块(执行器的 blockAtCell —— 它内部 new Vec3,不能直接传普通对象) */
|
|
493
|
+
blockAt: (cell: Cell) => { name: string; boundingBox?: string } | null | undefined;
|
|
494
|
+
/** 这一场上次吃东西的时刻(epoch ms);这一场还没吃过为 null。`[口粮]` 那行的四个数之一 */
|
|
495
|
+
lastAte?: () => number | null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
type MiningCall = Extract<SkillCall, { skill: 'collect' | 'excavate' | 'tunnel' }>;
|
|
499
|
+
|
|
500
|
+
/** 已知目标的挖掘步骤在动工前排除缺工具与必然无掉落。 */
|
|
501
|
+
/**
|
|
502
|
+
* lead 的三条:拴上那一下要包里真有绳(hard,缺了这一步一定驳回);要拴的那只这会儿
|
|
503
|
+
* 在不在身边(soft,活物会自己走过来);`tie` 指的那一格是不是栅栏(hard —— 只有栅栏
|
|
504
|
+
* 系得住绳,栅栏门和墙都不行,这条现在就可证)。
|
|
505
|
+
*/
|
|
506
|
+
function precheckLead(
|
|
507
|
+
bot: Bot,
|
|
508
|
+
call: { target?: string; tie?: unknown; off?: true },
|
|
509
|
+
deps: PrecheckDeps,
|
|
510
|
+
): PrecheckNote | null {
|
|
511
|
+
if (call.off) return null;
|
|
512
|
+
if (call.target) {
|
|
513
|
+
const stock = precheckHasItem(bot, 'lead', '拴', 'lead.noStock');
|
|
514
|
+
if (stock) return stock;
|
|
515
|
+
}
|
|
516
|
+
if (call.tie !== undefined) {
|
|
517
|
+
const cell = deps.resolve(call.tie);
|
|
518
|
+
const b = cell ? (deps.blockAt(cell) as BlockLike) : null;
|
|
519
|
+
if (cell && b && !(b.name.endsWith('_fence') || b.name === 'nether_brick_fence')) {
|
|
520
|
+
return {
|
|
521
|
+
level: 'hard',
|
|
522
|
+
text: `(${cell.x},${cell.y},${cell.z}) 是${zhName(b.name)},绳系不上去——只有栅栏系得住(栅栏门不行)`,
|
|
523
|
+
rule: 'lead.notFence',
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return call.target ? precheckTarget(bot, call.target, 32, 'lead.noTarget') : null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function precheckMiningTool(bot: Bot, call: SkillCall, deps: PrecheckDeps): PrecheckNote | null {
|
|
531
|
+
if (call.skill !== 'collect' && call.skill !== 'excavate' && call.skill !== 'tunnel') return null;
|
|
532
|
+
const tool = (call as MiningCall).tool;
|
|
533
|
+
const inventory = bot.inventory.items();
|
|
534
|
+
const exact = tool && tool !== 'fastest'
|
|
535
|
+
? inventory.find((entry) => entry.name === tool && entry.count > 0)
|
|
536
|
+
: null;
|
|
537
|
+
if (tool && tool !== 'fastest' && !exact) {
|
|
538
|
+
return {
|
|
539
|
+
level: 'hard',
|
|
540
|
+
text: `包里没有本步指定的${zhName(tool)};不会改用别的工具`,
|
|
541
|
+
rule: `${call.skill}.toolNoStock`,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const blocks = new Set<string>();
|
|
545
|
+
if (call.skill === 'collect') blocks.add(call.block);
|
|
546
|
+
if (call.skill === 'excavate') {
|
|
547
|
+
for (const cell of deps.cellsOf(call) ?? []) {
|
|
548
|
+
const block = deps.blockAt(cell);
|
|
549
|
+
if (block && block.name !== 'air' && block.name !== 'water' && block.name !== 'lava') blocks.add(block.name);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const registry = bot.registry as unknown as {
|
|
553
|
+
blocksByName?: Record<string, { harvestTools?: Record<string, unknown> } | undefined>;
|
|
554
|
+
items?: Record<number, { name?: string } | undefined>;
|
|
555
|
+
};
|
|
556
|
+
for (const block of blocks) {
|
|
557
|
+
const harvest = Object.keys(registry.blocksByName?.[block]?.harvestTools ?? {});
|
|
558
|
+
if (harvest.length === 0) continue;
|
|
559
|
+
const names = harvest.map((id) => registry.items?.[Number(id)]?.name).filter((name): name is string => !!name);
|
|
560
|
+
const rank = ['wooden', 'golden', 'stone', 'iron', 'diamond', 'netherite'];
|
|
561
|
+
const required = rank.flatMap((tier) => names.filter((name) => name.startsWith(tier)))[0] ?? names[0];
|
|
562
|
+
if (!exact && inventory.some((item) => harvest.includes(String(item.type)))) continue;
|
|
563
|
+
if (!exact) {
|
|
564
|
+
return {
|
|
565
|
+
level: 'hard',
|
|
566
|
+
text: `包里没有能保住${zhName(block)}掉落的工具${required ? `,要${zhName(required)}及以上` : ''};还没动方块`,
|
|
567
|
+
rule: `${call.skill}.toolNoDrop`,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (harvest.includes(String(exact.type))) continue;
|
|
571
|
+
return {
|
|
572
|
+
level: 'hard',
|
|
573
|
+
text: `本步指定的${zhName(exact.name)}挖${zhName(block)}不掉东西${required ? `,要${zhName(required)}及以上` : ''};不会改用别的工具`,
|
|
574
|
+
rule: `${call.skill}.toolNoDrop`,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 单步试算;全部通过或未覆盖时返回 null。goto 路线试算由 dryRun 单独处理。
|
|
582
|
+
* 每步至多返回一条,hard 优先于 soft。
|
|
583
|
+
*/
|
|
584
|
+
export function precheckStep(bot: Bot, call: SkillCall, deps: PrecheckDeps): PrecheckNote | null {
|
|
585
|
+
if (!bot?.inventory) return null;
|
|
586
|
+
if ((call as { dryRun?: boolean }).dryRun) return null; // 试算本身不用再试算
|
|
587
|
+
try {
|
|
588
|
+
const tool = precheckMiningTool(bot, call, deps);
|
|
589
|
+
if (tool) return tool;
|
|
590
|
+
switch (call.skill) {
|
|
591
|
+
case 'craft': return precheckCraft(bot, call as never) ?? precheckSlots(bot, call);
|
|
592
|
+
case 'eat': return precheckEat(bot, call, deps);
|
|
593
|
+
case 'build': return precheckBuild(bot, call as never, deps);
|
|
594
|
+
case 'tunnel': return precheckTunnel(bot, call as never, deps.resolve) ?? precheckDeepKit(bot, call, deps);
|
|
595
|
+
case 'excavate': return precheckDeepKit(bot, call, deps);
|
|
596
|
+
case 'equip': return precheckItemStep(bot, call, '拿');
|
|
597
|
+
case 'toss': return precheckItemStep(bot, call, '扔');
|
|
598
|
+
case 'stow': return precheckItemStep(bot, call, '存');
|
|
599
|
+
case 'anvil': return precheckItemStep(bot, call, '用');
|
|
600
|
+
case 'grindstone': return precheckItemStep(bot, call, '磨');
|
|
601
|
+
case 'take': return precheckSlots(bot, call);
|
|
602
|
+
case 'collect': return precheckSlots(bot, call) ?? precheckDeepKit(bot, call, deps);
|
|
603
|
+
case 'pickup': return precheckSlots(bot, call);
|
|
604
|
+
case 'surface': return precheckSurfaceHere(bot);
|
|
605
|
+
case 'lead': return precheckLead(bot, call as never, deps);
|
|
606
|
+
case 'use': {
|
|
607
|
+
const item = (call as { item?: string }).item;
|
|
608
|
+
if (item) {
|
|
609
|
+
return precheckUseItemAt(bot, call as { item?: string; at?: unknown }, deps)
|
|
610
|
+
?? precheckSoilCell(bot, call as { item?: string; at?: unknown }, deps)
|
|
611
|
+
?? precheckBedHere(bot, call as { at?: unknown }, deps);
|
|
612
|
+
}
|
|
613
|
+
const tgt = (call as { target?: string }).target;
|
|
614
|
+
if (tgt) return precheckTarget(bot, tgt, 32, 'use.noTarget');
|
|
615
|
+
return precheckBedHere(bot, call as { at?: unknown }, deps);
|
|
616
|
+
}
|
|
617
|
+
case 'attack': return precheckAttack(bot, call);
|
|
618
|
+
default: return null;
|
|
619
|
+
}
|
|
620
|
+
} catch {
|
|
621
|
+
return null; // 试算自己不许成为故障源
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* 整单试算(受理刻)。只收否定,全通返回空数组 —— 1418 次受理都挂一段试算,
|
|
627
|
+
* 她会调不动也会读吐;只报否定才让信号稀缺、token 成本近零。
|
|
628
|
+
*/
|
|
629
|
+
export function precheckSteps(bot: Bot, steps: SkillCall[], deps: PrecheckDeps): Array<{ index: number; note: PrecheckNote }> {
|
|
630
|
+
const out: Array<{ index: number; note: PrecheckNote }> = [];
|
|
631
|
+
for (let i = 0; i < steps.length; i++) {
|
|
632
|
+
const note = precheckStep(bot, steps[i], deps);
|
|
633
|
+
if (note) out.push({ index: i, note });
|
|
634
|
+
}
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** 受理回执注明“还没动工”,区分预检与执行结果;只报事实,不修改任务。 */
|
|
639
|
+
export function renderPrecheckNotes(hits: Array<{ index: number; note: PrecheckNote }>): string | null {
|
|
640
|
+
if (hits.length === 0) return null;
|
|
641
|
+
// 「包里没有任何食物」不与「你没有锄头」同排版:它是全场最高频的一条,混排三场
|
|
642
|
+
// 无人升级。拎成独立前缀行,格式照 `[现场]`——那句是水桶 25 次失败里唯一没让她跑偏的。
|
|
643
|
+
const ration = hits.find((h) => h.note.rule === 'eat.none');
|
|
644
|
+
const rest = hits.filter((h) => h.note.rule !== 'eat.none');
|
|
645
|
+
const hard = rest.filter((h) => h.note.level === 'hard');
|
|
646
|
+
const soft = rest.filter((h) => h.note.level === 'soft');
|
|
647
|
+
const one = (h: { index: number; note: PrecheckNote }): string => `第 ${h.index + 1} 步${h.note.text}`;
|
|
648
|
+
const parts: string[] = [];
|
|
649
|
+
if (hard.length > 0) parts.push(`试算(还没动工):${hard.map(one).join(';')}`);
|
|
650
|
+
if (soft.length > 0) parts.push(`另外:${soft.map(one).join(';')}`);
|
|
651
|
+
const body = parts.join('。');
|
|
652
|
+
if (!ration) return body;
|
|
653
|
+
const line = `[口粮] ${ration.note.text}`;
|
|
654
|
+
return body ? `${line}\n${body}` : line;
|
|
655
|
+
}
|