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,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 挖开与挖穿:excavate 按形状挖,tunnel 挖一条能走的通道、竖井、塔或螺旋楼梯。
|
|
3
|
+
*
|
|
4
|
+
* 通道以走得通为终态判据,不以挖够格数为准。
|
|
5
|
+
*/
|
|
6
|
+
import type { Bot } from 'mineflayer';
|
|
7
|
+
import pathfinderPkg from 'mineflayer-pathfinder';
|
|
8
|
+
import { type SkillCall } from './skills.ts';
|
|
9
|
+
import { Aborted, SkillBlocked, checkAbort, type SkillContext } from './skill-context.ts';
|
|
10
|
+
import {
|
|
11
|
+
AIR_NAMES, EXCAVATE_CELL_CAP, LIQUIDS, SHAPE_ZH, blockAtCell, cellKeyOf, cellText, dimensionOf,
|
|
12
|
+
feetOf, nearLavaAt, readRegion, resolveAt, shapeCells, type RegionReading,
|
|
13
|
+
} from './cell-facts.ts';
|
|
14
|
+
import { compositionText, noDropMaterials } from './receipt.ts';
|
|
15
|
+
import {
|
|
16
|
+
Upkeep, hitboxBlocks, permittedStockFor, placeReferenceFace, scaffoldNames, stationNotes,
|
|
17
|
+
sweepDrops,
|
|
18
|
+
} from './placement.ts';
|
|
19
|
+
import { worksNote } from './works.ts';
|
|
20
|
+
import { zhName } from './names.ts';
|
|
21
|
+
import { invCount, invSnapshot, lootNote } from './inventory.ts';
|
|
22
|
+
import { rasterize, type Cell } from './geometry.ts';
|
|
23
|
+
import { digBlock, gotoGoal, settleOnGround } from './travel.ts';
|
|
24
|
+
import { LEDGER_GUARD_BLOCKS, ledgerBlockFact } from './placed-ledger.ts';
|
|
25
|
+
import { CONTAINER_FIND, FURNACE_KINDS } from './chests.ts';
|
|
26
|
+
import { chooseTool, equipToolFor, miningToolPlan, nearBreak } from './tools.ts';
|
|
27
|
+
import { UNTIL_DIG_RADIUS, type UntilHit, untilBlockIds, untilHit, untilUnknownNote } from './until.ts';
|
|
28
|
+
|
|
29
|
+
const { goals } = pathfinderPkg;
|
|
30
|
+
|
|
31
|
+
/** 按形状清空间:自上而下、近的先;液体不按方块挖。 */
|
|
32
|
+
export async function skillExcavate(bot: Bot, call: Extract<SkillCall, { skill: 'excavate' }>, ctx: SkillContext): Promise<string> {
|
|
33
|
+
const cells = shapeCells(bot, call.shape, call.anchors, call.fill, EXCAVATE_CELL_CAP);
|
|
34
|
+
if (call.dryRun) {
|
|
35
|
+
const reading = readRegion(bot, cells);
|
|
36
|
+
const lines = [`试算${SHAPE_ZH[call.shape]}(共 ${cells.length} 格): ${compositionText(reading)}。`];
|
|
37
|
+
const noDrop = noDropMaterials(bot, reading);
|
|
38
|
+
if (noDrop.length > 0) lines.push(`现在的家伙挖 ${noDrop.join('、')} 不掉东西,挖碎就没了。`);
|
|
39
|
+
lines.push(...miningToolPlanNotes(bot, reading, ctx, call.tool));
|
|
40
|
+
lines.push(...stationNotes(bot, ctx, cells));
|
|
41
|
+
const hit = worksNote(
|
|
42
|
+
ctx.works?.inCells(dimensionOf(bot), cells) ?? [],
|
|
43
|
+
(ms) => ctx.clock?.(ms) ?? new Date(ms).toISOString().slice(11, 19),
|
|
44
|
+
);
|
|
45
|
+
if (hit) lines.push(`${hit}。`);
|
|
46
|
+
if (reading.unloaded > 0) lines.push(`${reading.unloaded} 格区块没加载。`);
|
|
47
|
+
return lines.join('') + '没动工';
|
|
48
|
+
}
|
|
49
|
+
const me = bot.entity.position;
|
|
50
|
+
const targets = cells
|
|
51
|
+
.filter((c) => {
|
|
52
|
+
const block = blockAtCell(bot, c);
|
|
53
|
+
return block !== null && !AIR_NAMES.has(block.name) && !LIQUIDS.has(block.name);
|
|
54
|
+
})
|
|
55
|
+
.sort((a, b) => b.y - a.y
|
|
56
|
+
|| (Math.hypot(a.x - me.x, a.z - me.z) - Math.hypot(b.x - me.x, b.z - me.z)));
|
|
57
|
+
if (targets.length === 0) {
|
|
58
|
+
// 液体不是挖掘目标;整片只有液体时必须明确受阻,不能报已经清空。
|
|
59
|
+
const reading = readRegion(bot, cells);
|
|
60
|
+
if (reading.counts.size === 0 && reading.unloaded === 0) {
|
|
61
|
+
return `那片 ${cells.length} 格本来就是空的,不用挖`;
|
|
62
|
+
}
|
|
63
|
+
const bits = [compositionText(reading)];
|
|
64
|
+
if (reading.unloaded > 0) bits.push(`${reading.unloaded} 格区块没加载`);
|
|
65
|
+
throw new SkillBlocked(
|
|
66
|
+
`一块都没挖:那片 ${cells.length} 格里没有挖得动的方块,但也不是空的(${bits.join(';')})`,
|
|
67
|
+
[...reading.counts].slice(0, 3).map(([n, e]) => `${zhName(n)}×${e.n},最近的在 ${cellText(e.nearest)}`),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
// 不接受沿自身站立列连续下挖;单格支撑仍须满足下方实心判据。
|
|
71
|
+
// 原地下降使用逐格等待落稳的 tunnel 竖井。
|
|
72
|
+
const feetAtStart = feetOf(bot);
|
|
73
|
+
const ownColumn = targets.filter((c) => c.x === feetAtStart.x && c.z === feetAtStart.z && c.y < feetAtStart.y);
|
|
74
|
+
if (ownColumn.length >= 2 && ownColumn.some((c) => c.y === feetAtStart.y - 1)) {
|
|
75
|
+
const bottom = Math.min(...ownColumn.map((c) => c.y));
|
|
76
|
+
throw new SkillBlocked(
|
|
77
|
+
`这列在你脚下(人在 ${cellText(feetAtStart)},要挖的 ${ownColumn.length} 格从脚下一直到 y${bottom}),先站到旁边再挖;要就地往下挖用 tunnel 竖井`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const keep = new Upkeep(bot, ctx);
|
|
82
|
+
const invBefore = invSnapshot(bot);
|
|
83
|
+
// 跳过账本中仍有物料的容器或工作站,并在回执说明;空账本不构成保护条件。
|
|
84
|
+
const ledgered = new Set(
|
|
85
|
+
(ctx.chests?.inCells(dimensionOf(bot), targets) ?? [])
|
|
86
|
+
.filter((r) => r.items.length > 0)
|
|
87
|
+
.map((r) => `${r.x},${r.y},${r.z}`),
|
|
88
|
+
);
|
|
89
|
+
// 成果登记只补充回执,不改变显式指定格的挖掘去留。
|
|
90
|
+
const worksHit = ctx.works?.inCells(dimensionOf(bot), targets) ?? [];
|
|
91
|
+
let guarded = 0;
|
|
92
|
+
let guardedSample = '';
|
|
93
|
+
/** 护住的里面有没有 take 掏不了的工作站(工作台):出路跟箱/炉不是同一条 */
|
|
94
|
+
let guardedStation = false;
|
|
95
|
+
let guardedAt: Cell | null = null;
|
|
96
|
+
let dug = 0;
|
|
97
|
+
let noDrop = 0;
|
|
98
|
+
let noDropSample = '';
|
|
99
|
+
let undiggable = 0;
|
|
100
|
+
let undiggableSample = '';
|
|
101
|
+
let nearLava = 0;
|
|
102
|
+
let underfoot = 0;
|
|
103
|
+
let underfootBelow = '';
|
|
104
|
+
let unreachable: Cell | null = null;
|
|
105
|
+
let unreachableN = 0;
|
|
106
|
+
// 这一片里的格子火把一律不贴:还没挖的贴上去回头连火把一起挖掉,已经挖空的那些
|
|
107
|
+
// 本来就该空着,贴进去等于自己给自己造障碍
|
|
108
|
+
const inRegion = new Set(targets.map(cellKeyOf));
|
|
109
|
+
for (const c of targets) {
|
|
110
|
+
checkAbort(ctx);
|
|
111
|
+
await settleOnGround(bot, ctx, 1_500);
|
|
112
|
+
let b = blockAtCell(bot, c);
|
|
113
|
+
if (!b || AIR_NAMES.has(b.name) || LIQUIDS.has(b.name)) continue; // 挖别处时已经塌了/通了
|
|
114
|
+
if (ledgered.has(`${c.x},${c.y},${c.z}`) && LEDGER_GUARD_BLOCKS.has(b.name)) {
|
|
115
|
+
guarded++;
|
|
116
|
+
guardedSample = `(${c.x}, ${c.y}, ${c.z}) 的${zhName(b.name)}`;
|
|
117
|
+
guardedAt = c;
|
|
118
|
+
if (!FURNACE_KINDS.has(b.name) && !CONTAINER_FIND.includes(b.name)) guardedStation = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (b.diggable === false) { undiggable++; undiggableSample = b.name; continue; }
|
|
122
|
+
// 邻接危险闸只检查岩浆,不因相邻水格阻断。
|
|
123
|
+
if (nearLavaAt(bot, c)) {
|
|
124
|
+
nearLava++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// 挖自身脚下支撑格前,必须确认再下一格实心;与 tunnel 竖井共用落脚规则。
|
|
128
|
+
const feetNow = feetOf(bot);
|
|
129
|
+
if (c.x === feetNow.x && c.y === feetNow.y - 1 && c.z === feetNow.z) {
|
|
130
|
+
const below = blockAtCell(bot, { x: c.x, y: c.y - 1, z: c.z });
|
|
131
|
+
if (!below || below.boundingBox !== 'block') {
|
|
132
|
+
underfoot++;
|
|
133
|
+
underfootBelow = below ? zhName(below.name) : '没加载的区块';
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!bot.canDigBlock(b)) {
|
|
138
|
+
try {
|
|
139
|
+
await gotoGoal(bot, new goals.GoalNear(c.x, c.y, c.z, 2), ctx);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err instanceof Aborted) throw err;
|
|
142
|
+
unreachable = unreachable ?? c;
|
|
143
|
+
unreachableN++;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
b = blockAtCell(bot, c);
|
|
147
|
+
if (!b || AIR_NAMES.has(b.name) || LIQUIDS.has(b.name)) continue;
|
|
148
|
+
if (!bot.canDigBlock(b)) { unreachable = unreachable ?? c; unreachableN++; continue; }
|
|
149
|
+
}
|
|
150
|
+
await equipToolFor(bot, b, ctx, miningToolPlan(call.tool));
|
|
151
|
+
if (typeof b.canHarvest === 'function' && !b.canHarvest(bot.heldItem?.type ?? null)) {
|
|
152
|
+
noDrop++;
|
|
153
|
+
noDropSample = b.name;
|
|
154
|
+
}
|
|
155
|
+
await digBlock(bot, b, ctx);
|
|
156
|
+
// 挖掉了就从登记上划掉:留着它下一趟会拿一格空气冒充她的成果
|
|
157
|
+
ctx.works?.forget(dimensionOf(bot), c.x, c.y, c.z);
|
|
158
|
+
dug++;
|
|
159
|
+
ctx.progress?.(dug, targets.length);
|
|
160
|
+
await keep.light((cc) => inRegion.has(cellKeyOf(cc)));
|
|
161
|
+
}
|
|
162
|
+
// 收尾走一趟掉落物:挖是站在坑外挖的,东西落进坑里没人捡。不扫的话 5×5×5 那种体量
|
|
163
|
+
// 只有七成进包 —— 挖掉了不等于到手,而回执报的是到手那个数。
|
|
164
|
+
if (dug > 0) await sweepDrops(bot, ctx);
|
|
165
|
+
const notes: string[] = [];
|
|
166
|
+
if (dug > 0) notes.push(lootNote(invBefore, bot));
|
|
167
|
+
if (noDrop > 0) notes.push(`其中 ${noDrop} 块(如${zhName(noDropSample)})手上的工具挖了不掉东西,挖碎了就没了`);
|
|
168
|
+
if (guarded > 0) {
|
|
169
|
+
// 工作台不适用 take;拆除须先接近到可见位置,再 collect 指名方块。
|
|
170
|
+
const how = guardedStation && guardedAt
|
|
171
|
+
? `要拆的话先 goto 到它跟前(挨着 ${cellText(guardedAt)} 那一格)再 collect 指名方块`
|
|
172
|
+
: '要拆的话先 take 掏空再 collect';
|
|
173
|
+
notes.push(`${guarded} 格是账本里的容器/工作站(${guardedSample}),没动它;${how}`);
|
|
174
|
+
}
|
|
175
|
+
const worksLine = worksNote(worksHit, (ms) => ctx.clock?.(ms) ?? new Date(ms).toISOString().slice(11, 19));
|
|
176
|
+
if (worksLine) notes.push(worksLine);
|
|
177
|
+
if (nearLava > 0) notes.push(`${nearLava} 块紧贴着岩浆,没动`);
|
|
178
|
+
if (underfoot > 0) notes.push(`${underfoot} 格是我此刻站着的支撑(它下面是${underfootBelow}),没动`);
|
|
179
|
+
if (undiggable > 0) notes.push(`${undiggable} 块是${zhName(undiggableSample)},根本挖不动`);
|
|
180
|
+
if (unreachableN > 0 && unreachable) notes.push(`${unreachableN} 块够不着(最近的在 (${unreachable.x}, ${unreachable.y}, ${unreachable.z}))`);
|
|
181
|
+
notes.push(...keep.tally());
|
|
182
|
+
const tail = `${notes.length > 0 ? `${notes.join(';')}。` : ''}挖完人在 ${cellText(feetOf(bot))}`;
|
|
183
|
+
if (dug === 0) throw new SkillBlocked(`一块都没挖成:${notes.join(';') || '全都够不着'}`);
|
|
184
|
+
return `挖开了 ${dug}/${targets.length} 块。${tail}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* tunnel 施工 1 格宽、2 格高的可通行通道;斜向坡度不得超过 45°。
|
|
189
|
+
* 水平位移为零时,向下为竖井、向上为垫脚塔;三种形态均以通行为终态判据。
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* 螺旋楼梯第 i 步(1 起)的落脚格:绕「脚下这格 + 它的 +x/+z 邻格」组成的 2×2
|
|
193
|
+
* 井筒转,每步升降 1。四步一圈,同一根角柱两次落脚差 4 格 —— 每步挖落脚、头顶、
|
|
194
|
+
* 再上一格共 3 格,圈与圈之间正好剩一格实心当上一圈的地板,上下都走得通
|
|
195
|
+
* (下行是普通台阶,上行是 1 格跳,跳跃余量就是那第 3 格)。
|
|
196
|
+
*/
|
|
197
|
+
/** 2×2 井筒的四种摆法:脚下这一格当井筒的哪个角(井筒最小角相对脚下的偏移) */
|
|
198
|
+
export type SpiralCorner = readonly [number, number];
|
|
199
|
+
export const SPIRAL_CORNERS: readonly SpiralCorner[] = [[0, 0], [-1, 0], [0, -1], [-1, -1]];
|
|
200
|
+
|
|
201
|
+
/** 井筒的四根角柱,按绕行顺序排,脚下这一格排在第 0 位 */
|
|
202
|
+
export function spiralQuad(start: Cell, corner: SpiralCorner): Array<{ x: number; z: number }> {
|
|
203
|
+
const [bx, bz] = [start.x + corner[0], start.z + corner[1]];
|
|
204
|
+
const ring = [{ x: bx, z: bz }, { x: bx + 1, z: bz }, { x: bx + 1, z: bz + 1 }, { x: bx, z: bz + 1 }];
|
|
205
|
+
const at = ring.findIndex((c) => c.x === start.x && c.z === start.z);
|
|
206
|
+
return [...ring.slice(at), ...ring.slice(0, at)];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function spiralFoot(start: Cell, i: number, up: boolean, corner: SpiralCorner): Cell {
|
|
210
|
+
const col = spiralQuad(start, corner)[i % 4];
|
|
211
|
+
return { x: col.x, y: start.y + (up ? i : -i), z: col.z };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 螺旋楼梯全程要挖的格子(每步 3 格),供试算与「接下来要挖的格子」名单用 */
|
|
215
|
+
export function spiralCells(start: Cell, steps: number, up: boolean, corner: SpiralCorner): Cell[] {
|
|
216
|
+
const out: Cell[] = [];
|
|
217
|
+
for (let i = 1; i <= steps; i++) {
|
|
218
|
+
const f = spiralFoot(start, i, up, corner);
|
|
219
|
+
out.push(f, { x: f.x, y: f.y + 1, z: f.z }, { x: f.x, y: f.y + 2, z: f.z });
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 第一圈按已有实心、可垫脚、液体或身体占位排序选择井筒方向;同分取 +x/+z。 */
|
|
225
|
+
export function pickSpiralCorner(bot: Bot, start: Cell, up: boolean, steps: number): SpiralCorner {
|
|
226
|
+
const turn = Math.min(4, steps);
|
|
227
|
+
const score = (corner: SpiralCorner): number => {
|
|
228
|
+
let s = 0;
|
|
229
|
+
for (let i = 1; i <= turn; i++) {
|
|
230
|
+
const f = spiralFoot(start, i, up, corner);
|
|
231
|
+
for (const c of [f, { x: f.x, y: f.y + 1, z: f.z }, { x: f.x, y: f.y + 2, z: f.z }]) {
|
|
232
|
+
const b = blockAtCell(bot, c);
|
|
233
|
+
if (!b) s -= 2;
|
|
234
|
+
else if (LIQUIDS.has(b.name)) s -= 4;
|
|
235
|
+
}
|
|
236
|
+
const under = { x: f.x, y: f.y - 1, z: f.z };
|
|
237
|
+
const below = blockAtCell(bot, under);
|
|
238
|
+
if (!below) s -= 2;
|
|
239
|
+
else if (LIQUIDS.has(below.name)) s -= 4;
|
|
240
|
+
else if (below.boundingBox === 'block') s += 2;
|
|
241
|
+
else if (placeReferenceFace(bot, under) !== null) s += 1;
|
|
242
|
+
if (i === 1 && hitboxBlocks(bot, under)) s -= 1;
|
|
243
|
+
}
|
|
244
|
+
return s;
|
|
245
|
+
};
|
|
246
|
+
let best = SPIRAL_CORNERS[0];
|
|
247
|
+
let bestScore = score(best);
|
|
248
|
+
for (const corner of SPIRAL_CORNERS.slice(1)) {
|
|
249
|
+
const s = score(corner);
|
|
250
|
+
if (s > bestScore) { best = corner; bestScore = s; }
|
|
251
|
+
}
|
|
252
|
+
return best;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** dryRun 的工具预案;只读背包与方块数据,不换手。 */
|
|
256
|
+
export function miningToolPlanNotes(
|
|
257
|
+
bot: Bot,
|
|
258
|
+
reading: RegionReading,
|
|
259
|
+
ctx: SkillContext,
|
|
260
|
+
requested: string | undefined,
|
|
261
|
+
): string[] {
|
|
262
|
+
const plan = miningToolPlan(requested);
|
|
263
|
+
if (plan.mode === 'exact'
|
|
264
|
+
&& !bot.inventory.items().some((item) => item.name === plan.item && item.count > 0)) {
|
|
265
|
+
return [`工具预案:包里没有本步指定的${zhName(plan.item)},不会改用别的工具。`];
|
|
266
|
+
}
|
|
267
|
+
const notes = new Set<string>();
|
|
268
|
+
for (const [name, entry] of reading.counts) {
|
|
269
|
+
if (LIQUIDS.has(name)) continue;
|
|
270
|
+
const decision = chooseTool(bot, entry.sample, ctx, plan);
|
|
271
|
+
if (decision.error) {
|
|
272
|
+
notes.add(`工具预案:${decision.error}。`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!decision.pick) {
|
|
276
|
+
notes.add(decision.need
|
|
277
|
+
? `工具预案:包里没有能保住${zhName(name)}掉落的${zhName(decision.need)}。`
|
|
278
|
+
: `工具预案:${zhName(name)}无需工具,会换下手上的耐久工具。`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const mode = plan.mode === 'economy' ? '节约模式'
|
|
282
|
+
: plan.mode === 'fastest' ? '本步 fastest'
|
|
283
|
+
: '本步精确指定';
|
|
284
|
+
const durability = nearBreak(decision.pick);
|
|
285
|
+
const drop = decision.canDrop
|
|
286
|
+
? ''
|
|
287
|
+
: `,挖碎不掉东西${decision.need ? `(要${zhName(decision.need)}及以上)` : ''}`;
|
|
288
|
+
const worn = durability ? `,只剩 ${durability.left}/${durability.max} 耐久` : '';
|
|
289
|
+
const reserve = decision.reserve?.reason === 'override' ? ',会覆盖 reserve'
|
|
290
|
+
: decision.reserve ? ',只有动用 reserve 里的它才能保住掉落' : '';
|
|
291
|
+
notes.add(`工具预案:${mode}用${zhName(decision.pick.name)}挖${zhName(name)}${drop}${worn}${reserve}。`);
|
|
292
|
+
if (notes.size >= 6) break;
|
|
293
|
+
}
|
|
294
|
+
return [...notes];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* tunnel 不挖传送面或门框,维度通道由 transit 处理。
|
|
299
|
+
* 普通黑曜石不属于维度通道边界,单独按材料限制报告。
|
|
300
|
+
*/
|
|
301
|
+
export const TRANSIT_BOUNDARY_BLOCKS = new Set([
|
|
302
|
+
'nether_portal', 'end_portal', 'end_gateway', 'end_portal_frame',
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
/** 挖不动(或慢到不值当)就停下的硬块。只陈述挖不动这件事,不冒充维度语义。 */
|
|
306
|
+
export const TUNNEL_HARD_BLOCKS = new Set(['obsidian', 'crying_obsidian']);
|
|
307
|
+
|
|
308
|
+
/** tunnel 撞上这一格该说的那句话;不该停就是 null */
|
|
309
|
+
export function tunnelStopWord(name: string, cc: Cell): string | null {
|
|
310
|
+
if (TRANSIT_BOUNDARY_BLOCKS.has(name)) {
|
|
311
|
+
return `挖到 (${cc.x}, ${cc.y}, ${cc.z}) 的${zhName(name)}跟前,这是维度通道边界,停在这。`;
|
|
312
|
+
}
|
|
313
|
+
if (TUNNEL_HARD_BLOCKS.has(name)) {
|
|
314
|
+
return `挖到 (${cc.x}, ${cc.y}, ${cc.z}) 的${zhName(name)}跟前,当前这把家伙挖不动它,停在这。`;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function skillTunnel(bot: Bot, call: Extract<SkillCall, { skill: 'tunnel' }>, ctx: SkillContext): Promise<string> {
|
|
320
|
+
const start = feetOf(bot);
|
|
321
|
+
const target = resolveAt(bot, call.at);
|
|
322
|
+
const run = Math.max(Math.abs(target.x - start.x), Math.abs(target.z - start.z));
|
|
323
|
+
const rise = target.y - start.y;
|
|
324
|
+
const vertical = run === 0;
|
|
325
|
+
const spiral = call.spiral === true && vertical;
|
|
326
|
+
const kind = spiral ? '螺旋楼梯' : !vertical ? '通道' : rise > 0 ? '塔' : '竖井';
|
|
327
|
+
if (vertical && rise === 0) throw new SkillBlocked('终点就是脚下这一格');
|
|
328
|
+
if (call.spiral === true && !vertical) {
|
|
329
|
+
throw new SkillBlocked(
|
|
330
|
+
`spiral 是垂直升降用的螺旋楼梯,at 要放正上/正下(现在横向差着 ${run} 格);斜着走用不带 spiral 的通道`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (!vertical && Math.abs(rise) > run) {
|
|
334
|
+
throw new SkillBlocked(
|
|
335
|
+
`坡度超过 45°:横 ${run} 格要升降 ${Math.abs(rise)} 格。` +
|
|
336
|
+
'基本垂直的升降可以把 at 放正上/正下,加 "spiral":true 挖成上下都能走的螺旋楼梯',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const floorRaw = rasterize('line', [start, target], 'solid');
|
|
340
|
+
if (!Array.isArray(floorRaw)) throw new SkillBlocked(floorRaw.error);
|
|
341
|
+
const floor = floorRaw;
|
|
342
|
+
const planned = floor.length - 1;
|
|
343
|
+
const corner: SpiralCorner = spiral
|
|
344
|
+
? pickSpiralCorner(bot, start, rise > 0, planned)
|
|
345
|
+
: SPIRAL_CORNERS[0];
|
|
346
|
+
if (call.dryRun) {
|
|
347
|
+
if (spiral) {
|
|
348
|
+
const carve = spiralCells(start, planned, rise > 0, corner);
|
|
349
|
+
const reading = readRegion(bot, carve);
|
|
350
|
+
const shaft = spiralQuad(start, corner);
|
|
351
|
+
const xs = shaft.map((c) => c.x);
|
|
352
|
+
const zs = shaft.map((c) => c.z);
|
|
353
|
+
const lines = [`试算螺旋楼梯(${rise > 0 ? '升' : '降'} ${planned} 格,` +
|
|
354
|
+
`绕 (${Math.min(...xs)}..${Math.max(...xs)}, ${Math.min(...zs)}..${Math.max(...zs)}) 的 2×2 井筒转,` +
|
|
355
|
+
`要挖 ${carve.length} 格)` +
|
|
356
|
+
`: ${compositionText(reading)}。`];
|
|
357
|
+
const noDrop = noDropMaterials(bot, reading);
|
|
358
|
+
if (noDrop.length > 0) lines.push(`现在的家伙挖 ${noDrop.join('、')} 不掉东西。`);
|
|
359
|
+
lines.push(...miningToolPlanNotes(bot, reading, ctx, call.tool));
|
|
360
|
+
lines.push(...stationNotes(bot, ctx, carve));
|
|
361
|
+
return lines.join('') + '没动工';
|
|
362
|
+
}
|
|
363
|
+
// 塔要挖的是每一格的头顶(脚下那条是垫出来的),竖井挖脚下那条,斜通道两条都挖
|
|
364
|
+
const carve = vertical
|
|
365
|
+
? floor.slice(1).map((c) => ({ x: c.x, y: rise > 0 ? c.y + 1 : c.y, z: c.z }))
|
|
366
|
+
: floor.flatMap((c) => [c, { x: c.x, y: c.y + 1, z: c.z }]);
|
|
367
|
+
const reading = readRegion(bot, carve);
|
|
368
|
+
const head = vertical
|
|
369
|
+
? `试算${kind}(${planned} 格,要挖 ${carve.length} 格)`
|
|
370
|
+
: `试算通道(${planned} 步,连头顶共 ${carve.length} 格)`;
|
|
371
|
+
const lines = [`${head}: ${compositionText(reading)}。`];
|
|
372
|
+
if (rise > 0 && vertical) {
|
|
373
|
+
const stock = permittedStockFor(bot, scaffoldNames(ctx), '垫脚', ctx, true);
|
|
374
|
+
if ('why' in stock) {
|
|
375
|
+
lines.push(`要垫 ${planned} 格,${stock.why}。`);
|
|
376
|
+
} else {
|
|
377
|
+
stock.permit.finish(false);
|
|
378
|
+
lines.push(`要垫 ${planned} 格,包里有 ${invCount(bot, (n) => n === stock.item.name)} 个${zhName(stock.item.name)}。`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const noDrop = noDropMaterials(bot, reading);
|
|
382
|
+
if (noDrop.length > 0) lines.push(`现在的家伙挖 ${noDrop.join('、')} 不掉东西。`);
|
|
383
|
+
lines.push(...miningToolPlanNotes(bot, reading, ctx, call.tool));
|
|
384
|
+
lines.push(...stationNotes(bot, ctx, carve));
|
|
385
|
+
return lines.join('') + '没动工';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const keep = new Upkeep(bot, ctx);
|
|
389
|
+
const invBefore = invSnapshot(bot);
|
|
390
|
+
let steps = 0;
|
|
391
|
+
let noDrop = 0;
|
|
392
|
+
let noDropSample = '';
|
|
393
|
+
const digCell = async (c: Cell): Promise<void> => {
|
|
394
|
+
const b = blockAtCell(bot, c);
|
|
395
|
+
if (!b || b.boundingBox !== 'block') return;
|
|
396
|
+
// 挖开前检查六个正邻格的岩浆,覆盖侧面和后方。
|
|
397
|
+
if (nearLavaAt(bot, c)) stop(`(${c.x}, ${c.y}, ${c.z}) 紧贴着岩浆,不敢挖,停在这。`);
|
|
398
|
+
await equipToolFor(bot, b, ctx, miningToolPlan(call.tool));
|
|
399
|
+
if (typeof b.canHarvest === 'function' && !b.canHarvest(bot.heldItem?.type ?? null)) {
|
|
400
|
+
noDrop++;
|
|
401
|
+
noDropSample = b.name;
|
|
402
|
+
}
|
|
403
|
+
await digBlock(bot, b, ctx);
|
|
404
|
+
};
|
|
405
|
+
/** 挖到哪一格、维持条件补了什么、掉了什么:挖通与没挖通两边共用的现场事实 */
|
|
406
|
+
const facts = (): string[] => {
|
|
407
|
+
// 塔的每一格是垫出来的,不是挖出来的;单位也跟着换,免得读成"挖了几步"
|
|
408
|
+
// (螺旋楼梯往上也是挖出来的,只有塌空那几格才垫)
|
|
409
|
+
const did = !spiral && vertical && rise > 0 ? '垫了' : '挖了';
|
|
410
|
+
const notes = [`${kind}${did} ${steps}/${planned} ${vertical ? '格' : '步'},人在 ${cellText(feetOf(bot))}`];
|
|
411
|
+
if (steps > 0) notes.push(lootNote(invBefore, bot));
|
|
412
|
+
if (noDrop > 0) notes.push(`其中 ${noDrop} 块(如${zhName(noDropSample)})挖了不掉东西`);
|
|
413
|
+
notes.push(...keep.tally());
|
|
414
|
+
return notes;
|
|
415
|
+
};
|
|
416
|
+
/** 走到终点那一格叫什么:三种形状各一个说法,成句与受阻句共用 */
|
|
417
|
+
const arrive = !vertical ? '挖通' : rise > 0 ? '到顶' : '到底';
|
|
418
|
+
const through = (): string => `${facts().join(';')}。${arrive}了`;
|
|
419
|
+
/**
|
|
420
|
+
* 早停名单:每挖完一格看一眼周身。命中是**正常收束**不是受阻 —— 她要的就是
|
|
421
|
+
* 「往下挖到碰见铁矿为止」,碰见了这一单就做完了(终点判据随之作废,见 deriveExpect)。
|
|
422
|
+
*/
|
|
423
|
+
const stop2 = call.until && call.until.length > 0 ? untilBlockIds(bot, call.until) : null;
|
|
424
|
+
const stopNote = stop2 ? untilUnknownNote(stop2.unknown) : '';
|
|
425
|
+
const earlyText = (h: UntilHit): string =>
|
|
426
|
+
`${facts().join(';')}。在 (${h.x}, ${h.y}, ${h.z}) 碰到了${h.what},停在这` +
|
|
427
|
+
`(没${arrive},until 说到这儿为止)${stopNote}`;
|
|
428
|
+
const early = (): UntilHit | null =>
|
|
429
|
+
(stop2 ? untilHit(bot, stop2.ids, UNTIL_DIG_RADIUS, false) : null);
|
|
430
|
+
/** 未到终点即受阻,分别报告实际挖掘位置、垫脚量和停止原因。 */
|
|
431
|
+
const stop = (why: string): never => {
|
|
432
|
+
throw new SkillBlocked(`${kind}没${arrive}:${why}`, facts());
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* 前方无支撑时,先尝试在有正交实心参照的缺口垫脚。
|
|
436
|
+
* 无法垫脚则先平移一步,再从新落点重铺剩余路线;不得反转此顺序。
|
|
437
|
+
*/
|
|
438
|
+
const footing = async (next: Cell, cur: Cell): Promise<Cell | null> => {
|
|
439
|
+
const under = { x: next.x, y: next.y - 1, z: next.z };
|
|
440
|
+
if (await keep.footing(under)) return next;
|
|
441
|
+
if (next.y >= cur.y) return null;
|
|
442
|
+
const level = { x: next.x, y: cur.y, z: next.z };
|
|
443
|
+
const below = blockAtCell(bot, { x: level.x, y: level.y - 1, z: level.z });
|
|
444
|
+
if (below?.boundingBox === 'block') return level;
|
|
445
|
+
if (await keep.footing({ x: level.x, y: level.y - 1, z: level.z })) return level;
|
|
446
|
+
return null;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 螺旋楼梯:一步一格绕井筒转,判据仍是走到终点那一格(y 到位即到,水平位置
|
|
451
|
+
* 在井筒的四根角柱里)。每步先验落脚——底下塌空就垫,垫不上路就断在这一格;
|
|
452
|
+
* 往上一样是挖(地板通常就是原生石头),只有塌空那几格才动垫脚料。
|
|
453
|
+
*/
|
|
454
|
+
if (spiral) {
|
|
455
|
+
const up = rise > 0;
|
|
456
|
+
const all = spiralCells(start, planned, up, corner);
|
|
457
|
+
while (steps < planned) {
|
|
458
|
+
checkAbort(ctx);
|
|
459
|
+
const i = steps + 1;
|
|
460
|
+
const next = spiralFoot(start, i, up, corner);
|
|
461
|
+
const carve: Cell[] = [next, { x: next.x, y: next.y + 1, z: next.z }, { x: next.x, y: next.y + 2, z: next.z }];
|
|
462
|
+
for (const cc of carve) {
|
|
463
|
+
const b = blockAtCell(bot, cc);
|
|
464
|
+
if (!b) return stop(`(${cc.x}, ${cc.y}, ${cc.z}) 的区块还没加载出来。`);
|
|
465
|
+
if (LIQUIDS.has(b.name)) return stop(`挖到 (${cc.x}, ${cc.y}, ${cc.z}) 碰上${zhName(b.name)},停在这。`);
|
|
466
|
+
const boundary = tunnelStopWord(b.name, cc);
|
|
467
|
+
if (boundary) return stop(boundary);
|
|
468
|
+
const guard = ledgerBlockFact(bot, ctx, cc);
|
|
469
|
+
if (guard) return stop(`挖到${guard}跟前,里头的东西不能跟着挖没,停在这。`);
|
|
470
|
+
}
|
|
471
|
+
const under = { x: next.x, y: next.y - 1, z: next.z };
|
|
472
|
+
const below = blockAtCell(bot, under);
|
|
473
|
+
if (!below) return stop(`落脚 (${under.x}, ${under.y}, ${under.z}) 的区块还没加载出来。`);
|
|
474
|
+
if (LIQUIDS.has(below.name)) return stop(`下一级台阶底下就是${zhName(below.name)},停在这。`);
|
|
475
|
+
if (below.boundingBox !== 'block' && !(await keep.footing(under))) {
|
|
476
|
+
// 螺旋垫的是井筒里侧邻格,要一个贴得住的参照面;塔垫的是自己脚下那一格
|
|
477
|
+
// (跳起来放,参照是脚底那一块),悬空里上行只有塔走得通
|
|
478
|
+
return stop(
|
|
479
|
+
`下一级台阶 (${next.x}, ${next.y}, ${next.z}) 底下塌空,垫也没垫上(${keep.why('footing')}),`
|
|
480
|
+
+ '楼梯断在这一格。同一段路走塔(at 放正上方、不带 spiral)垫的是自己脚下那一格,不吃邻格的参照面',
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
for (const cc of carve) await digCell(cc);
|
|
484
|
+
try {
|
|
485
|
+
await gotoGoal(bot, new goals.GoalBlock(next.x, next.y, next.z), ctx);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
if (err instanceof Aborted) throw err;
|
|
488
|
+
return stop(`挖开了却${up ? '跳不上去' : '下不去'}(${(err as Error).message})。`);
|
|
489
|
+
}
|
|
490
|
+
steps++;
|
|
491
|
+
ctx.progress?.(steps, planned);
|
|
492
|
+
const h = early();
|
|
493
|
+
if (h) return earlyText(h);
|
|
494
|
+
// 火把不贴在接下来要挖的格子上(与斜通道同一条教训)
|
|
495
|
+
const ahead = new Set(all.slice(steps * 3).map(cellKeyOf));
|
|
496
|
+
await keep.light((c) => ahead.has(cellKeyOf(c)));
|
|
497
|
+
}
|
|
498
|
+
const feet = feetOf(bot);
|
|
499
|
+
if (feet.y !== target.y) return stop(`该到 y=${target.y},人在 ${cellText(feet)}。`);
|
|
500
|
+
return through();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* 竖井与塔:一格一格,判据仍是走到终点那一格。
|
|
505
|
+
*
|
|
506
|
+
* 往下先看要挖的那一格底下还有没有底 —— 没有就是挖穿进了空腔,再挖就是往下掉,
|
|
507
|
+
* 停在这里报事实(竖井是单程的,回来靠往上的塔;要能走回头路,发单时用 spiral)。
|
|
508
|
+
* 往上没有路可走,每一格由 `climb` 垫出来,垫不上就是没到顶。
|
|
509
|
+
*/
|
|
510
|
+
if (vertical) {
|
|
511
|
+
const up = rise > 0;
|
|
512
|
+
// 每一格按当下脚底重算,不照发车时那张表:塔是靠垫脚往上挪的,人挪没挪到位当场就得知道
|
|
513
|
+
while (steps < planned) {
|
|
514
|
+
checkAbort(ctx);
|
|
515
|
+
const here = feetOf(bot);
|
|
516
|
+
const next = { x: target.x, y: here.y + (up ? 1 : -1), z: target.z };
|
|
517
|
+
const carve: Cell[] = [next, { x: next.x, y: next.y + 1, z: next.z }];
|
|
518
|
+
for (const cc of carve) {
|
|
519
|
+
const b = blockAtCell(bot, cc);
|
|
520
|
+
if (!b) return stop(`(${cc.x}, ${cc.y}, ${cc.z}) 的区块还没加载出来。`);
|
|
521
|
+
if (LIQUIDS.has(b.name)) return stop(`挖到 (${cc.x}, ${cc.y}, ${cc.z}) 碰上${zhName(b.name)},停在这。`);
|
|
522
|
+
const boundary = tunnelStopWord(b.name, cc);
|
|
523
|
+
if (boundary) return stop(boundary);
|
|
524
|
+
const guard = ledgerBlockFact(bot, ctx, cc);
|
|
525
|
+
if (guard) return stop(`挖到${guard}跟前,里头的东西不能跟着挖没,停在这。`);
|
|
526
|
+
}
|
|
527
|
+
if (!up) {
|
|
528
|
+
const under = { x: next.x, y: next.y - 1, z: next.z };
|
|
529
|
+
const below = blockAtCell(bot, under);
|
|
530
|
+
if (!below) return stop(`再往下 (${under.x}, ${under.y}, ${under.z}) 的区块还没加载出来。`);
|
|
531
|
+
if (LIQUIDS.has(below.name)) return stop(`再往下就是${zhName(below.name)},停在这。`);
|
|
532
|
+
if (below.boundingBox !== 'block' && !(await keep.footing(under))) {
|
|
533
|
+
return stop(`挖开 (${next.x}, ${next.y}, ${next.z}) 下面就是空的,垫也没垫上(${keep.why('footing')}),再挖就是往下掉。`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const cc of carve) await digCell(cc);
|
|
537
|
+
if (up) {
|
|
538
|
+
// 上升前检查新脚格和头格的正邻岩浆;原为空气时 digCell 不会检查它们。
|
|
539
|
+
for (const cc of carve) {
|
|
540
|
+
if (nearLavaAt(bot, cc)) return stop(`(${cc.x}, ${cc.y}, ${cc.z}) 紧贴着岩浆,不敢上去,停在这。`);
|
|
541
|
+
}
|
|
542
|
+
if (!(await keep.climb())) return stop(`${keep.why('climb')},上不去了。`);
|
|
543
|
+
await settleOnGround(bot, ctx, 1_500);
|
|
544
|
+
const feet = feetOf(bot);
|
|
545
|
+
if (feet.x !== next.x || feet.y !== next.y || feet.z !== next.z) {
|
|
546
|
+
return stop(`垫上了却没站上去:该在 ${cellText(next)},人在 ${cellText(feet)}。`);
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
try {
|
|
550
|
+
await gotoGoal(bot, new goals.GoalBlock(next.x, next.y, next.z), ctx);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
if (err instanceof Aborted) throw err;
|
|
553
|
+
return stop(`挖开了却下不去(${(err as Error).message})。`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
steps++;
|
|
557
|
+
ctx.progress?.(steps, planned);
|
|
558
|
+
const h = early();
|
|
559
|
+
if (h) return earlyText(h);
|
|
560
|
+
// 塔不插:下一格垫的正是现在脚底这一格,插在这儿就是自己占住自己的垫脚格
|
|
561
|
+
if (!up) await keep.light();
|
|
562
|
+
}
|
|
563
|
+
const feet = feetOf(bot);
|
|
564
|
+
if (feet.y !== target.y) return stop(`该到 ${cellText(target)},人在 ${cellText(feet)}。`);
|
|
565
|
+
return through();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let cur = start;
|
|
569
|
+
let plan = floor;
|
|
570
|
+
let i = 1;
|
|
571
|
+
while (i < plan.length) {
|
|
572
|
+
checkAbort(ctx);
|
|
573
|
+
let next = plan[i];
|
|
574
|
+
const support = blockAtCell(bot, { x: next.x, y: next.y - 1, z: next.z });
|
|
575
|
+
if (!support) return stop('前面的区块还没加载出来。');
|
|
576
|
+
if (LIQUIDS.has(support.name)) return stop(`再往前脚下就是${zhName(support.name)},停在这。`);
|
|
577
|
+
let replanned = false;
|
|
578
|
+
if (support.boundingBox !== 'block') {
|
|
579
|
+
const fixed = await footing(next, cur);
|
|
580
|
+
if (!fixed) return stop(`前面 (${next.x}, ${next.y}, ${next.z}) 脚下悬空,垫脚也没垫上(${keep.why('footing')}),路断在这一格。`);
|
|
581
|
+
replanned = fixed.y !== next.y;
|
|
582
|
+
next = fixed;
|
|
583
|
+
}
|
|
584
|
+
const carve: Cell[] = [next, { x: next.x, y: next.y + 1, z: next.z }];
|
|
585
|
+
if (next.y > cur.y) carve.push({ x: cur.x, y: cur.y + 2, z: cur.z }); // 上台阶要跳,头顶留一格
|
|
586
|
+
for (const cc of carve) {
|
|
587
|
+
const b = blockAtCell(bot, cc);
|
|
588
|
+
if (b && LIQUIDS.has(b.name)) return stop(`挖到 (${cc.x}, ${cc.y}, ${cc.z}) 碰上${zhName(b.name)},停在这。`);
|
|
589
|
+
const boundary = b ? tunnelStopWord(b.name, cc) : null;
|
|
590
|
+
if (boundary) return stop(boundary);
|
|
591
|
+
const guard = ledgerBlockFact(bot, ctx, cc);
|
|
592
|
+
if (guard) return stop(`挖到${guard}跟前,里头的东西不能跟着挖没,停在这。`);
|
|
593
|
+
}
|
|
594
|
+
for (const cc of carve) await digCell(cc);
|
|
595
|
+
try {
|
|
596
|
+
await gotoGoal(bot, new goals.GoalBlock(next.x, next.y, next.z), ctx);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
if (err instanceof Aborted) throw err;
|
|
599
|
+
return stop(`挖开了却走不过去(${(err as Error).message})。`);
|
|
600
|
+
}
|
|
601
|
+
cur = next;
|
|
602
|
+
steps++;
|
|
603
|
+
ctx.progress?.(steps, planned);
|
|
604
|
+
const h = early();
|
|
605
|
+
if (h) return earlyText(h);
|
|
606
|
+
if (replanned) {
|
|
607
|
+
const rest = rasterize('line', [cur, target], 'solid');
|
|
608
|
+
if (!Array.isArray(rest) || rest.length < 2) {
|
|
609
|
+
if (cur.x === target.x && cur.y === target.y && cur.z === target.z) return through();
|
|
610
|
+
// 走平避开的那一格塌方也带走了高度:横向到了终点头上/脚下,通道并没有到终点
|
|
611
|
+
const dy = target.y - cur.y;
|
|
612
|
+
return stop(`走平之后横向到了终点${dy < 0 ? '上方' : '下方'} ${Math.abs(dy)} 格,${cellText(target)} 还是没通。`);
|
|
613
|
+
}
|
|
614
|
+
plan = rest;
|
|
615
|
+
i = 1;
|
|
616
|
+
} else i++;
|
|
617
|
+
// 路线重铺后再收集后续脚格和头格,避免将它们用作火把支撑。
|
|
618
|
+
const ahead = new Set(plan.slice(i)
|
|
619
|
+
.flatMap((c) => [cellKeyOf(c), cellKeyOf({ x: c.x, y: c.y + 1, z: c.z })]));
|
|
620
|
+
await keep.light((c) => ahead.has(cellKeyOf(c)));
|
|
621
|
+
}
|
|
622
|
+
return through();
|
|
623
|
+
}
|
|
624
|
+
|