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,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 背包的读法与等待:按名字数数、找那一件、以及放置/合成/拾取后等服务端回话确认增量。
|
|
3
|
+
*
|
|
4
|
+
* 只读背包与等确认,不决定拿什么;该拿哪把工具在 tools.ts。
|
|
5
|
+
*/
|
|
6
|
+
import type { Bot } from 'mineflayer';
|
|
7
|
+
import { itemMatchesPick, pickLabel, pickMissText, pickTargetOf } from './item-pick.ts';
|
|
8
|
+
import { matchItemName } from './chests.ts';
|
|
9
|
+
import { type ItemLike } from './item-facts.ts';
|
|
10
|
+
import { SkillBlocked, checkAbort, sleep, type SkillContext } from './skill-context.ts';
|
|
11
|
+
import { zhName } from './names.ts';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/** 原版背包格数:9 快捷栏 + 27 主仓。盔甲 4 格与副手 1 格不在其中 */
|
|
15
|
+
export const INVENTORY_SLOTS = 36;
|
|
16
|
+
|
|
17
|
+
/** equip 的找法:精确名优先,退而求其次才用后缀/前缀(类别名 pickaxe→iron_pickaxe) */
|
|
18
|
+
export function invItemNamed(bot: Bot, want: string, pick?: string) {
|
|
19
|
+
const items = (bot.inventory?.items?.() ?? [])
|
|
20
|
+
.filter((i) => itemMatchesPick(pick, i, bot.registry as never));
|
|
21
|
+
return items.find((i) => i.name === want)
|
|
22
|
+
?? items.find((i) => i.name.endsWith(`_${want}`))
|
|
23
|
+
?? items.find((i) => i.name.startsWith(`${want}_`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** invItemNamed 的名字口径,摊开给"这个名字下有哪几件"用 */
|
|
27
|
+
export function namedLike(want: string, name: string): boolean {
|
|
28
|
+
return name === want || name.endsWith(`_${want}`) || name.startsWith(`${want}_`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 报得出玩家物品栏的东西。`bot.inventory` 和打开的容器窗口都是 Window,
|
|
33
|
+
* 两者的 `items()` 都只数 inventoryStart..inventoryEnd,也就是玩家那半边。
|
|
34
|
+
*/
|
|
35
|
+
export interface InvView {
|
|
36
|
+
items(): InvItem[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 库存判据。第二个入参是那一摞东西本身:同 id 的几件靠挑选词分辨(item-pick),
|
|
41
|
+
* 光看名字分不出来。只看名字的判据照旧写一元箭头。
|
|
42
|
+
*/
|
|
43
|
+
export type InvPred = (name: string, item: InvItem) => boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 容器开启时玩家库存以 currentWindow 为准;windowId 0 才更新 bot.inventory。
|
|
47
|
+
* 点击预测也修改当前窗口,close() 的 copyInventory() 才回灌 bot.inventory。
|
|
48
|
+
* 开窗期间的搬运增量须从窗口 items() 读取。
|
|
49
|
+
*/
|
|
50
|
+
export function playerInvIn(bot: Bot, win: unknown): InvView {
|
|
51
|
+
return typeof (win as InvView | null)?.items === 'function' ? (win as InvView) : bot.inventory;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function invCountIn(view: InvView, pred: InvPred): number {
|
|
55
|
+
return view.items().filter((i) => pred(i.name, i)).reduce((s, i) => s + i.count, 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function invCountByIdIn(view: InvView, id: number): number {
|
|
59
|
+
return view.items().filter((i) => i.type === id).reduce((s, i) => s + i.count, 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function invCount(bot: Bot, pred: InvPred): number {
|
|
63
|
+
return invCountIn(bot.inventory, pred);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 这一步点名的是哪一件:名字口径 + 挑选词,两道都过才算 */
|
|
67
|
+
export function itemPredOf(bot: Bot, item: string, pick?: string): InvPred {
|
|
68
|
+
return (n, it) => matchItemName(item, n) && itemMatchesPick(pick, it, bot.registry as never);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 包里没有这一步要的东西。两句话分开:连 id 都没有,还是有 id 而挑选词一件都没命中 ——
|
|
73
|
+
* 后者要把同 id 的那几件各自是什么摆出来,她下一步要么改挑选词要么改主意。
|
|
74
|
+
*/
|
|
75
|
+
export function noSuchItem(
|
|
76
|
+
bot: Bot, item: string, pick?: string, candidates?: readonly ItemLike[],
|
|
77
|
+
): SkillBlocked {
|
|
78
|
+
const same = candidates ?? bot.inventory.items().filter((i) => matchItemName(item, i.name));
|
|
79
|
+
if (pick && same.length > 0) {
|
|
80
|
+
const targets = same.map((i) => pickTargetOf(i, bot.registry as never));
|
|
81
|
+
return new SkillBlocked(pickMissText('包里', item, pick, targets));
|
|
82
|
+
}
|
|
83
|
+
return new SkillBlocked(`包里没有${zhName(item)}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 「附魔书(带「无限」的)」:点名到具体一件时,受阻话里也要带上挑选词 */
|
|
87
|
+
export function itemAsked(item: string, pick?: string): string {
|
|
88
|
+
return `${zhName(item)}${pick ? `(带「${pick}」的)` : ''}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 回执里怎么念这一件:点名挑过就念全标签(带附魔括号),没挑就念 id 的中文名 */
|
|
92
|
+
export function askedLabel(bot: Bot, name: string, pick?: string, item?: ItemLike | null): string {
|
|
93
|
+
return pick && item ? pickLabel(pickTargetOf(item, bot.registry as never)) : zhName(name);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 点名那一件的搬运:直接点它所在的那一格。
|
|
98
|
+
*
|
|
99
|
+
* `deposit`/`withdraw`/`transfer` 都按物品类型找槽(mineflayer 的 nbt 参数比的是
|
|
100
|
+
* 1.20.5 之前的 NBT,组件时代的附魔在它眼里一律为空),同 id 的几件对它没有分别 ——
|
|
101
|
+
* 挑中哪一件全看槽位顺序。所以点名到具体一件时只能按槽位搬。
|
|
102
|
+
* 只用在**一格一件**的东西上:能堆叠的一摞里每件都一样,按类型搬本来就没有歧义。
|
|
103
|
+
*/
|
|
104
|
+
export async function moveExactSlot(bot: Bot, from: number, to: number): Promise<void> {
|
|
105
|
+
await bot.moveSlotItem(from, to);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** minecraft-data 的 drops 简表不含概率掉落;补充表同时用于入包对账和按掉落物反查方块。 */
|
|
109
|
+
export function probabilisticDropsOf(name: string): string[] {
|
|
110
|
+
if (['short_grass', 'tall_grass', 'grass', 'fern', 'large_fern'].includes(name)) return ['wheat_seeds'];
|
|
111
|
+
if (name === 'dead_bush') return ['stick'];
|
|
112
|
+
if (name.endsWith('_leaves')) {
|
|
113
|
+
return [
|
|
114
|
+
'stick',
|
|
115
|
+
name.replace('_leaves', '_sapling'), // 没有对应树苗的(红树/杜鹃)对不上就是对不上,无害
|
|
116
|
+
...(name === 'oak_leaves' || name === 'dark_oak_leaves' ? ['apple'] : []),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 挖这些方块会掉出什么物品。核对入包数要认掉落物:挖 coal_ore 进包的是 coal,
|
|
124
|
+
* 挖 stone 进包的是 cobblestone,按方块名去数永远数出 0。
|
|
125
|
+
*/
|
|
126
|
+
export function dropNamesOf(bot: Bot, blockIds: number[]): Set<string> {
|
|
127
|
+
const blocks = bot.registry.blocks as unknown as Record<number, { name: string; drops?: unknown[] }>;
|
|
128
|
+
const items = bot.registry.items as unknown as Record<number, { name: string }>;
|
|
129
|
+
const names = new Set<string>();
|
|
130
|
+
for (const id of blockIds) {
|
|
131
|
+
const b = blocks[id];
|
|
132
|
+
if (!b) continue;
|
|
133
|
+
names.add(b.name);
|
|
134
|
+
for (const d of b.drops ?? []) {
|
|
135
|
+
const itemId = typeof d === 'number' ? d : (d as { drop?: number } | null)?.drop;
|
|
136
|
+
if (itemId !== undefined && items[itemId]) names.add(items[itemId].name);
|
|
137
|
+
}
|
|
138
|
+
for (const n of probabilisticDropsOf(b.name)) names.add(n);
|
|
139
|
+
}
|
|
140
|
+
return names;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function invCountById(bot: Bot, id: number): number {
|
|
144
|
+
return invCountByIdIn(bot.inventory, id);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 合成产物必须持续存在的稳定窗口。 */
|
|
148
|
+
export const CRAFT_SETTLE_MS = 600;
|
|
149
|
+
|
|
150
|
+
/** 等物品栏的账落到位最多等这么久。 */
|
|
151
|
+
export const INV_CONFIRM_MS = 2_000;
|
|
152
|
+
/** 挖下来的东西进包要等的上限:原版掉落物 10 tick 可拾取,再给一点走过去的余量 */
|
|
153
|
+
export const PICKUP_SETTLE_MS = 800;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 对账结果。三种下场分得开,是为了让回执照实说,而不是一律报"失败":
|
|
157
|
+
* - `confirmed` 服务端的账已经变了,`moved` 是稳定后的实际变化量;
|
|
158
|
+
* - `rolled-back` 变过又被收回,这是**确认没成**;
|
|
159
|
+
* - `timeout` 等到超时账都没动,这是**没等到确认**,成没成都还不知道。
|
|
160
|
+
*/
|
|
161
|
+
export type InvConfirm =
|
|
162
|
+
| { moved: number; status: 'confirmed' }
|
|
163
|
+
| { moved: 0; status: 'rolled-back' | 'timeout' };
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 等物品栏的账相对 `before` 朝 `dir` 方向变化(dir=1 是多出来,-1 是少掉),
|
|
167
|
+
* 返回稳定后的变化量。
|
|
168
|
+
*
|
|
169
|
+
* `settleMs > 0` 时增量还得在这段窗口里持续存在才算数——合成走这一条:
|
|
170
|
+
* 服务端可能先把产物塞进来再撤回,单次读取会把临时产物误报成功。
|
|
171
|
+
* 容器搬运不需要这段:`close()` 里的 `copyInventory()` 是一次确定的灌回,
|
|
172
|
+
* 见到就是准的,白等只会让每次存取多花半秒。
|
|
173
|
+
*/
|
|
174
|
+
export async function awaitInvConfirm(
|
|
175
|
+
read: () => number,
|
|
176
|
+
before: number,
|
|
177
|
+
dir: 1 | -1,
|
|
178
|
+
ctx: SkillContext,
|
|
179
|
+
settleMs = 0,
|
|
180
|
+
): Promise<InvConfirm> {
|
|
181
|
+
const deadline = Date.now() + INV_CONFIRM_MS;
|
|
182
|
+
while (Date.now() < deadline) {
|
|
183
|
+
checkAbort(ctx);
|
|
184
|
+
const moved = (read() - before) * dir;
|
|
185
|
+
if (moved > 0) {
|
|
186
|
+
if (settleMs <= 0) return { moved, status: 'confirmed' };
|
|
187
|
+
const settleUntil = Date.now() + settleMs;
|
|
188
|
+
while (Date.now() < settleUntil) {
|
|
189
|
+
checkAbort(ctx);
|
|
190
|
+
await sleep(100);
|
|
191
|
+
}
|
|
192
|
+
const settled = (read() - before) * dir;
|
|
193
|
+
return settled > 0 ? { moved: settled, status: 'confirmed' } : { moved: 0, status: 'rolled-back' };
|
|
194
|
+
}
|
|
195
|
+
await sleep(100);
|
|
196
|
+
}
|
|
197
|
+
return { moved: 0, status: 'timeout' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 等这一次合成的产物出现在物品栏里**并且留得住**,返回实际多出来的数量;
|
|
202
|
+
* 到点没出现、或出现后又被收回,都返回 0。
|
|
203
|
+
*/
|
|
204
|
+
export async function awaitCraftGain(bot: Bot, itemId: number, before: number, ctx: SkillContext): Promise<number> {
|
|
205
|
+
const r = await awaitInvConfirm(
|
|
206
|
+
() => invCountById(bot, itemId), before, 1, ctx, CRAFT_SETTLE_MS,
|
|
207
|
+
);
|
|
208
|
+
if (r.status === 'rolled-back') {
|
|
209
|
+
ctx.diag?.write({
|
|
210
|
+
lane: 'craft', event: 'rolled-back', taskId: ctx.taskId,
|
|
211
|
+
msg: `产物入包后又没了:${CRAFT_SETTLE_MS}ms 后一个不剩(服务端收回了)`,
|
|
212
|
+
data: { itemId, before },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return r.moved;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 全物品栏按物品名计数;配 lootNote 求一步前后的净增 */
|
|
219
|
+
export function invSnapshot(bot: Bot): Map<string, number> {
|
|
220
|
+
const m = new Map<string, number>();
|
|
221
|
+
for (const it of bot.inventory.items()) m.set(it.name, (m.get(it.name) ?? 0) + it.count);
|
|
222
|
+
return m;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 这一步实际进包了什么(净增部分);一样都没多也要说出来 */
|
|
226
|
+
export function lootNote(before: Map<string, number>, bot: Bot): string {
|
|
227
|
+
const gains = invGains(before, bot);
|
|
228
|
+
return gains.length > 0 ? `这一路拾取:${gains.join('、')}` : '这一路什么都没进包';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** 与 lootNote 同口径的净增清单;没有净增返回空数组 */
|
|
232
|
+
export function invGains(before: Map<string, number>, bot: Bot): string[] {
|
|
233
|
+
const gains: string[] = [];
|
|
234
|
+
for (const [name, n] of invSnapshot(bot)) {
|
|
235
|
+
const d = n - (before.get(name) ?? 0);
|
|
236
|
+
if (d > 0) gains.push(`${zhName(name)}×${d}`);
|
|
237
|
+
}
|
|
238
|
+
return gains;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 原版拾取范围会同时吸入沿途物品;净增分为点名目标与顺路拾得两栏,均回报。 */
|
|
242
|
+
export function invGainsSplit(
|
|
243
|
+
before: Map<string, number>, bot: Bot, item: string,
|
|
244
|
+
): { wanted: string[]; alongside: string[] } {
|
|
245
|
+
const wanted: string[] = [];
|
|
246
|
+
const alongside: string[] = [];
|
|
247
|
+
for (const [name, n] of invSnapshot(bot)) {
|
|
248
|
+
const d = n - (before.get(name) ?? 0);
|
|
249
|
+
if (d <= 0) continue;
|
|
250
|
+
(matchItemName(item, name) ? wanted : alongside).push(`${zhName(name)}×${d}`);
|
|
251
|
+
}
|
|
252
|
+
return { wanted, alongside };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 与 invGains 反向:这一步从包里少掉了什么(手上用出去的、装到别处去的) */
|
|
256
|
+
export function invLosses(before: Map<string, number>, bot: Bot): string[] {
|
|
257
|
+
const now = invSnapshot(bot);
|
|
258
|
+
const losses: string[] = [];
|
|
259
|
+
for (const [name, n] of before) {
|
|
260
|
+
const d = n - (now.get(name) ?? 0);
|
|
261
|
+
if (d > 0) losses.push(`${zhName(name)}×${d}`);
|
|
262
|
+
}
|
|
263
|
+
return losses;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** 窗口/背包里的一摞。`slot` 是**它所在那扇窗**的槽位号,点名搬运只认它 */
|
|
267
|
+
export type InvItem = ReturnType<Bot['inventory']['items']>[number];
|
|
268
|
+
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 动手那几下:挑兵器、够不够得着、挥击与冷却、远程接管的判据,以及附近敌对生物的读数。
|
|
3
|
+
*
|
|
4
|
+
* attack 技能与自保反射共用这一层;谁占着身体不在这里决定。
|
|
5
|
+
*/
|
|
6
|
+
import { piglinIsHostile } from './piglin.ts';
|
|
7
|
+
import { sleep, type TaskAttackLease } from './skill-context.ts';
|
|
8
|
+
import { headInWater } from './terrain.ts';
|
|
9
|
+
import {
|
|
10
|
+
HYBRID_MELEE_AT, KITE_MAX_RANGE, KITE_MIN_RANGE, bestRangedWeapon, hasRangedLos, hasUsableArrows,
|
|
11
|
+
type BowShotResult, type RangedTarget,
|
|
12
|
+
} from './ranged.ts';
|
|
13
|
+
import { type AttackMode } from './skills.ts';
|
|
14
|
+
import type { Bot } from 'mineflayer';
|
|
15
|
+
import pathfinderPkg from 'mineflayer-pathfinder';
|
|
16
|
+
import { Aborted, SkillBlocked, SkillNoop, checkAbort, type SkillContext } from './skill-context.ts';
|
|
17
|
+
import { isKnownTarget } from './entity-facts.ts';
|
|
18
|
+
import { dropGoal, findEntity, gotoGoal, levelTravelGoal } from './travel.ts';
|
|
19
|
+
import { zhEntity } from './names.ts';
|
|
20
|
+
import { chooseHybridWeapon, type HybridWeapon } from './ranged.ts';
|
|
21
|
+
import { zhErrorText } from './receipt.ts';
|
|
22
|
+
|
|
23
|
+
const { goals } = pathfinderPkg;
|
|
24
|
+
|
|
25
|
+
export type HurtSource = Parameters<Bot['attack']>[0];
|
|
26
|
+
|
|
27
|
+
/** entityHurt 不带 source 时(旧协议路径)回退猜攻击者的距离上限,沿用旧判据的 6 格 */
|
|
28
|
+
export const REFLEX_HURT_FALLBACK_RANGE = 6;
|
|
29
|
+
|
|
30
|
+
/** 半径内最近的敌对生物;排除玩家和自身,没有则返回 null。 */
|
|
31
|
+
export function nearestHostileWithin(bot: Bot, radius: number): HurtSource | null {
|
|
32
|
+
let best: HurtSource | null = null;
|
|
33
|
+
let bestD = radius;
|
|
34
|
+
for (const id of Object.keys(bot.entities)) {
|
|
35
|
+
const e = bot.entities[id as unknown as number];
|
|
36
|
+
if (!e?.position || e.type === 'player' || e === bot.entity) continue;
|
|
37
|
+
const name = e.name ?? '';
|
|
38
|
+
if (!HOSTILE.has(name) && !(name === 'piglin' && piglinIsHostile(bot, e))) continue;
|
|
39
|
+
const d = e.position.distanceTo(bot.entity.position);
|
|
40
|
+
if (d < bestD) { bestD = d; best = e as unknown as HurtSource; }
|
|
41
|
+
}
|
|
42
|
+
return best;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 剑 > 斧 > 镐 > 锹,同种再按材质:下界合金 > 钻石 > 铁 > 石 > 金 > 木 */
|
|
46
|
+
export const WEAPON_KIND_SCORE: Record<string, number> = { sword: 40, axe: 30, pickaxe: 20, shovel: 10 };
|
|
47
|
+
export const WEAPON_TIER_SCORE: Record<string, number> = {
|
|
48
|
+
netherite: 6, diamond: 5, iron: 4, stone: 3, golden: 2, wooden: 1,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** 剑 1.6 攻速 → 满伤间隔 625ms;冷却不满就挥是软伤害。战斗会话与技能共用一份 */
|
|
52
|
+
export function meleeCooldownMs(bot: Bot): number {
|
|
53
|
+
return attackCooldownMs(bot);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function weaponScore(name: string): number {
|
|
57
|
+
const kind = Object.keys(WEAPON_KIND_SCORE).find((k) => name.endsWith(`_${k}`));
|
|
58
|
+
if (!kind) return -1;
|
|
59
|
+
const tier = Object.keys(WEAPON_TIER_SCORE).find((t) => name.startsWith(`${t}_`));
|
|
60
|
+
return WEAPON_KIND_SCORE[kind] + (tier ? WEAPON_TIER_SCORE[tier] : 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function bestWeapon(bot: Bot) {
|
|
64
|
+
let best: ReturnType<Bot['inventory']['items']>[number] | null = null;
|
|
65
|
+
let score = -1;
|
|
66
|
+
for (const item of bot.inventory.items()) {
|
|
67
|
+
const s = weaponScore(item.name);
|
|
68
|
+
if (s > score) { score = s; best = item; }
|
|
69
|
+
}
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 1.9+ 攻速换算的满伤间隔。剑 1.6、斧 1.0,不满就挥是软伤害。 */
|
|
74
|
+
export function attackCooldownMs(bot: Bot): number {
|
|
75
|
+
const name = bot.heldItem?.name ?? '';
|
|
76
|
+
if (name.endsWith('_sword')) return 625;
|
|
77
|
+
if (name.endsWith('_axe')) return 1_000;
|
|
78
|
+
if (name === 'trident' || name.endsWith('_trident')) return 900;
|
|
79
|
+
if (name.endsWith('_pickaxe')) return 850;
|
|
80
|
+
if (name.endsWith('_shovel') || name.endsWith('_hoe')) return 1_000;
|
|
81
|
+
return 250;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const MELEE_REACH = 3.2;
|
|
85
|
+
export const MELEE_CHASE = 8;
|
|
86
|
+
export const HOP_MS = 280;
|
|
87
|
+
export const STRAFE_MS = 400;
|
|
88
|
+
|
|
89
|
+
export function releaseMelee(bot: Bot): void {
|
|
90
|
+
for (const k of ['forward', 'back', 'left', 'right', 'sprint', 'jump'] as const) {
|
|
91
|
+
bot.setControlState(k, false);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function aimAt(bot: Bot, entity: { position: { offset(x: number, y: number, z: number): unknown }; height?: number }): Promise<void> {
|
|
96
|
+
await bot.lookAt(entity.position.offset(0, entity.height ?? 1.6, 0) as never, true);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 跳劈:落地才跳,下落才出手。冲刺中 crit 不成,先松 sprint。
|
|
101
|
+
* 等不到下落(测试假实体没有速度)就 HOP_MS 后挥,不堵死循环。
|
|
102
|
+
*/
|
|
103
|
+
export async function hopCrit(bot: Bot): Promise<void> {
|
|
104
|
+
if ((bot.entity as { isInWater?: boolean }).isInWater) return;
|
|
105
|
+
if (bot.entity.onGround === false) return;
|
|
106
|
+
bot.setControlState('sprint', false);
|
|
107
|
+
bot.setControlState('jump', true);
|
|
108
|
+
const deadline = Date.now() + HOP_MS;
|
|
109
|
+
while (Date.now() < deadline) {
|
|
110
|
+
await sleep(50);
|
|
111
|
+
const vy = (bot.entity as { velocity?: { y?: number } }).velocity?.y;
|
|
112
|
+
if (typeof vy === 'number' && vy < 0) break;
|
|
113
|
+
}
|
|
114
|
+
bot.setControlState('jump', false);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function pressMelee(bot: Bot, entity: { position: { distanceTo(o: unknown): number }; name?: string }, strafeLeft: boolean | null): void {
|
|
118
|
+
const d = entity.position.distanceTo(bot.entity.position);
|
|
119
|
+
if (entity.name === 'creeper' && d < 3) {
|
|
120
|
+
bot.setControlState('forward', false);
|
|
121
|
+
bot.setControlState('back', true);
|
|
122
|
+
bot.setControlState('sprint', false);
|
|
123
|
+
} else {
|
|
124
|
+
bot.setControlState('back', false);
|
|
125
|
+
bot.setControlState('forward', d > 1.6);
|
|
126
|
+
bot.setControlState('sprint', d > 2.4);
|
|
127
|
+
}
|
|
128
|
+
bot.setControlState('left', strafeLeft === true);
|
|
129
|
+
bot.setControlState('right', strafeLeft === false);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function meleeSwing(
|
|
133
|
+
bot: Bot,
|
|
134
|
+
entity: Parameters<Bot['attack']>[0],
|
|
135
|
+
beforeAttack?: () => void,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
await hopCrit(bot);
|
|
138
|
+
await aimAt(bot, entity);
|
|
139
|
+
beforeAttack?.();
|
|
140
|
+
bot.attack(entity);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 水下攻击回执附氧气读数,不额外用氧气阈值阻断主动攻击。 */
|
|
144
|
+
export function underwaterOxygenNote(bot: Bot): string {
|
|
145
|
+
if (!headInWater(bot)) return '';
|
|
146
|
+
return `;人在水下,氧气 ${Math.max(0, Math.min(20, bot.oxygenLevel ?? 20))}/20`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function rangedTargetOf(entity: NonNullable<Bot['entities'][string]>): RangedTarget {
|
|
150
|
+
return {
|
|
151
|
+
id: entity.id,
|
|
152
|
+
position: entity.position,
|
|
153
|
+
...(entity.height === undefined ? {} : { height: entity.height }),
|
|
154
|
+
...(entity.width === undefined ? {} : { width: entity.width }),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function forcedRangedIssue(bot: Bot, target: RangedTarget): string | null {
|
|
159
|
+
if (!bestRangedWeapon(bot)) return '包里没有可用的弓';
|
|
160
|
+
if (!hasUsableArrows(bot)) return '包里没有普通箭';
|
|
161
|
+
if (!hasRangedLos(bot, target)) return '目标被方块挡住,没有射线';
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function rangedBlockedText(result: Exclude<BowShotResult, { kind: 'released' }>): string {
|
|
166
|
+
if (result.reason === 'no_arrow') return '普通箭用完了';
|
|
167
|
+
if (result.reason === 'no_los') return '目标被方块挡住,没有射线';
|
|
168
|
+
if (result.reason === 'no_solution') return '这段距离没有可用的弓箭弹道';
|
|
169
|
+
if (result.reason === 'too_close') return '目标贴得太近,弓拉不开安全距离';
|
|
170
|
+
if (result.cause === 'bow_lost') return '可用的弓不在手边了';
|
|
171
|
+
if (result.cause === 'bot_lost') return '连接断了';
|
|
172
|
+
if (result.cause === 'target_lost') return '目标离开视野了';
|
|
173
|
+
return '这一箭在放出前被取消了';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function attackStats(stats: TaskAttackLease): string {
|
|
177
|
+
return `挥击 ${stats.swings} 次命中 ${stats.meleeHits} 次,放箭 ${stats.arrows} 支命中 ${stats.rangedHits} 支` +
|
|
178
|
+
`${stats.hurts > 0 ? `,期间挨打 ${stats.hurts} 次` : ''}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function pressRanged(
|
|
182
|
+
bot: Bot,
|
|
183
|
+
distance: number,
|
|
184
|
+
mode: AttackMode,
|
|
185
|
+
strafeLeft: boolean,
|
|
186
|
+
): void {
|
|
187
|
+
const kite = mode === 'kite';
|
|
188
|
+
const retreat = distance <= HYBRID_MELEE_AT || (kite && distance < KITE_MIN_RANGE);
|
|
189
|
+
bot.setControlState('jump', false);
|
|
190
|
+
bot.setControlState('forward', kite && distance > KITE_MAX_RANGE);
|
|
191
|
+
bot.setControlState('back', retreat);
|
|
192
|
+
bot.setControlState('sprint', kite && distance > KITE_MAX_RANGE);
|
|
193
|
+
const lateral = kite && distance >= KITE_MIN_RANGE && distance <= KITE_MAX_RANGE;
|
|
194
|
+
bot.setControlState('left', lateral && strafeLeft);
|
|
195
|
+
bot.setControlState('right', lateral && !strafeLeft);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 一只在 32 格内的敌对生物读数 */
|
|
199
|
+
export interface HostileRead {
|
|
200
|
+
e: NonNullable<Bot['entities'][string]>;
|
|
201
|
+
d: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 32 格内所有敌对生物,由近到远。piglin 只在真敌对时算数(见 piglinIsHostile) */
|
|
205
|
+
export function hostilesAround(bot: Bot, from: { x: number; y: number; z: number }): HostileRead[] {
|
|
206
|
+
const out: HostileRead[] = [];
|
|
207
|
+
for (const id of Object.keys(bot.entities)) {
|
|
208
|
+
const e = bot.entities[id];
|
|
209
|
+
if (!e?.position || !e.name) continue;
|
|
210
|
+
if (!HOSTILE.has(e.name) && !(e.name === 'piglin' && piglinIsHostile(bot, e))) continue;
|
|
211
|
+
const d = e.position.distanceTo(from as never);
|
|
212
|
+
if (d <= 32) out.push({ e, d });
|
|
213
|
+
}
|
|
214
|
+
return out.sort((a, b) => a.d - b.d);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function nearestHostileTo(bot: Bot, from: { x: number; y: number; z: number }): HostileRead | null {
|
|
218
|
+
return hostilesAround(bot, from)[0] ?? null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const HOSTILE = new Set([
|
|
222
|
+
'zombie', 'skeleton', 'creeper', 'spider', 'cave_spider', 'enderman', 'witch',
|
|
223
|
+
'slime', 'phantom', 'drowned', 'husk', 'stray', 'pillager', 'vindicator',
|
|
224
|
+
'ravager', 'vex', 'evoker', 'silverfish', 'zombie_villager', 'blaze', 'ghast',
|
|
225
|
+
'magma_cube', 'wither_skeleton', 'warden', 'bogged', 'breeze',
|
|
226
|
+
'piglin_brute', 'hoglin', 'zoglin', 'endermite', 'illusioner', 'guardian',
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
export async function skillAttack(
|
|
230
|
+
bot: Bot,
|
|
231
|
+
target: string,
|
|
232
|
+
mode: AttackMode = 'auto',
|
|
233
|
+
ctx: SkillContext,
|
|
234
|
+
): Promise<string> {
|
|
235
|
+
if (!isKnownTarget(bot, target)) {
|
|
236
|
+
throw new SkillBlocked(`不认识「${target}」这种东西,认不出要打谁`);
|
|
237
|
+
}
|
|
238
|
+
const entity = findEntity(bot, target, 32);
|
|
239
|
+
// 要打的东西不在场 = 无事可做:没打输,是没得打(见 SkillNoop)
|
|
240
|
+
if (!entity) throw new SkillNoop(`附近 32 格内没有${zhEntity(target)}`);
|
|
241
|
+
const ranged = ctx.attack.ranged;
|
|
242
|
+
const forcedRanged = mode === 'ranged' || mode === 'kite';
|
|
243
|
+
if (forcedRanged) {
|
|
244
|
+
const issue = forcedRangedIssue(bot, rangedTargetOf(entity));
|
|
245
|
+
if (!ranged || issue) {
|
|
246
|
+
throw new SkillBlocked(`${issue ?? '远程控制器现在不可用'};${mode} 不会改用近战`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// 血线撤退只针对可能还手的目标;玩家、敌对生物及主动攻击的普通猪灵均适用。
|
|
250
|
+
const attackName = entity.name ?? '';
|
|
251
|
+
const dangerous = entity.type === 'player'
|
|
252
|
+
|| HOSTILE.has(attackName)
|
|
253
|
+
// 这一步是**主动**打它:攻击本身就是挑衅,金甲带来的中立当场作废,所以 provoked 传真
|
|
254
|
+
|| (attackName === 'piglin' && piglinIsHostile(bot, entity, true));
|
|
255
|
+
const stats = ctx.attack.acquire(entity.id);
|
|
256
|
+
const deadline = Date.now() + 45_000;
|
|
257
|
+
let lastSwing = 0;
|
|
258
|
+
let inMelee = false;
|
|
259
|
+
let strafeLeft = false;
|
|
260
|
+
let strafeAt = 0;
|
|
261
|
+
let weapon: HybridWeapon = forcedRanged ? 'ranged' : 'melee';
|
|
262
|
+
let desperate = false;
|
|
263
|
+
let retreatFailed = false;
|
|
264
|
+
const equipMelee = async (): Promise<void> => {
|
|
265
|
+
ranged?.abort();
|
|
266
|
+
const best = bestWeapon(bot);
|
|
267
|
+
if (best) await bot.equip(best, 'hand').catch(() => undefined);
|
|
268
|
+
};
|
|
269
|
+
if (weapon === 'melee') await equipMelee();
|
|
270
|
+
try {
|
|
271
|
+
while (!stats.dead && entity.isValid && Date.now() < deadline) {
|
|
272
|
+
checkAbort(ctx);
|
|
273
|
+
if (stats.disconnected) throw new SkillBlocked(`连接断了,主动攻击已取消;${attackStats(stats)}`);
|
|
274
|
+
const floor = ctx.fleeHealth();
|
|
275
|
+
if (!desperate && dangerous && floor > 0 && (bot.health ?? 20) < floor) {
|
|
276
|
+
// 撤退是任务的一部分:走完再汇报,不能丢下一个方向就报错收工
|
|
277
|
+
releaseMelee(bot);
|
|
278
|
+
ranged?.abort();
|
|
279
|
+
ctx.escape.active = true;
|
|
280
|
+
const hp = Math.ceil(bot.health ?? 0);
|
|
281
|
+
const start = bot.entity.position.clone();
|
|
282
|
+
const hurtMark = stats.hurts;
|
|
283
|
+
const away = bot.entity.position.minus(entity.position).normalize().scaled(24);
|
|
284
|
+
const dest = bot.entity.position.plus(away);
|
|
285
|
+
let pathError: string | null = null;
|
|
286
|
+
await gotoGoal(bot, levelTravelGoal(dest.x, dest.z), ctx).catch((error: unknown) => {
|
|
287
|
+
if (error instanceof Aborted) throw error;
|
|
288
|
+
pathError = error instanceof Error ? zhErrorText(error.message) : String(error);
|
|
289
|
+
});
|
|
290
|
+
const p = bot.entity.position;
|
|
291
|
+
const moved = p.distanceTo(start);
|
|
292
|
+
const distance = entity.position.distanceTo(p);
|
|
293
|
+
const hurtAgain = stats.hurts > hurtMark;
|
|
294
|
+
const safe = moved >= 3 && distance >= KITE_MIN_RANGE && !hurtAgain;
|
|
295
|
+
if (stats.dead || !entity.isValid) break;
|
|
296
|
+
if (safe) {
|
|
297
|
+
throw new SkillBlocked(
|
|
298
|
+
`${stats.swings > 0 ? '打到一半' : `没跟${zhEntity(target)}动手`},生命 ${hp}/20 低于撤退线;` +
|
|
299
|
+
`确认撤开 ${Math.round(moved * 10) / 10} 格,现在离目标 ${Math.round(distance * 10) / 10} 格` +
|
|
300
|
+
`${pathError ? `(${pathError})` : ''};${attackStats(stats)}${underwaterOxygenNote(bot)}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
// 跑不动或撤退中还在掉血时,血线的前提已经失效;本步继续持有身体回身还手。
|
|
304
|
+
desperate = true;
|
|
305
|
+
retreatFailed = true;
|
|
306
|
+
ctx.escape.active = false;
|
|
307
|
+
weapon = forcedRanged ? 'ranged' : 'melee';
|
|
308
|
+
if (weapon === 'melee') await equipMelee();
|
|
309
|
+
ctx.diag?.write({
|
|
310
|
+
lane: 'skill', event: 'attack-cornered', taskId: ctx.taskId,
|
|
311
|
+
msg: `主动攻击撤退失败:只挪 ${Math.round(moved * 10) / 10} 格${hurtAgain ? ',仍在受击' : ''},回身还手`,
|
|
312
|
+
data: { target, mode, moved, distance, hurtAgain, pathError },
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const d = entity.position.distanceTo(bot.entity.position);
|
|
317
|
+
const rangedReady = Boolean(ranged?.ready(bot));
|
|
318
|
+
const nextWeapon = forcedRanged
|
|
319
|
+
? 'ranged'
|
|
320
|
+
: mode === 'melee' ? 'melee' : chooseHybridWeapon(weapon, d, rangedReady);
|
|
321
|
+
if (nextWeapon !== weapon) {
|
|
322
|
+
weapon = nextWeapon;
|
|
323
|
+
releaseMelee(bot);
|
|
324
|
+
if (weapon === 'melee') await equipMelee();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (weapon === 'ranged') {
|
|
328
|
+
inMelee = false;
|
|
329
|
+
dropGoal(bot, 'task', '改用远程,寻路器让位', ctx.diag);
|
|
330
|
+
if (!rangedReady || !ranged) {
|
|
331
|
+
if (forcedRanged) {
|
|
332
|
+
throw new SkillBlocked(`${forcedRangedIssue(bot, rangedTargetOf(entity)) ?? '远程控制器现在不可用'};${mode} 不会改用近战;${attackStats(stats)}`);
|
|
333
|
+
}
|
|
334
|
+
weapon = 'melee';
|
|
335
|
+
await equipMelee();
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (Date.now() >= strafeAt) {
|
|
339
|
+
strafeLeft = !strafeLeft;
|
|
340
|
+
strafeAt = Date.now() + STRAFE_MS;
|
|
341
|
+
}
|
|
342
|
+
await aimAt(bot, entity);
|
|
343
|
+
pressRanged(bot, d, mode, strafeLeft);
|
|
344
|
+
// kite 先把过近距离拉开;ranged/auto 只需高于弓控制器的安全下限。
|
|
345
|
+
if ((mode === 'kite' && d < KITE_MIN_RANGE) || d <= HYBRID_MELEE_AT) {
|
|
346
|
+
await sleep(50);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const result = await ranged.shoot(rangedTargetOf(entity), stats.token);
|
|
350
|
+
checkAbort(ctx);
|
|
351
|
+
// entityDead can arrive while the bow controller is still settling its draw.
|
|
352
|
+
// The target's terminal event owns that race; a late shoot result must not spend
|
|
353
|
+
// an arrow in the task receipt or start a fallback attack.
|
|
354
|
+
if (stats.dead) break;
|
|
355
|
+
if (result.kind === 'released') {
|
|
356
|
+
stats.arrows += 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (forcedRanged) {
|
|
360
|
+
throw new SkillBlocked(`${rangedBlockedText(result)};${mode} 不会改用近战;${attackStats(stats)}`);
|
|
361
|
+
}
|
|
362
|
+
if (
|
|
363
|
+
result.reason === 'aborted' &&
|
|
364
|
+
['bot_lost', 'lease', 'death', 'target_lost'].includes(result.cause ?? '')
|
|
365
|
+
) {
|
|
366
|
+
throw new SkillBlocked(`${rangedBlockedText(result)};${attackStats(stats)}`);
|
|
367
|
+
}
|
|
368
|
+
weapon = 'melee';
|
|
369
|
+
await equipMelee();
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (d > MELEE_CHASE) {
|
|
374
|
+
inMelee = false;
|
|
375
|
+
releaseMelee(bot);
|
|
376
|
+
await gotoGoal(bot, new goals.GoalFollow(entity, 2), ctx).catch(() => undefined);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (!inMelee) {
|
|
380
|
+
dropGoal(bot, 'task', '够得着了,自己打', ctx.diag);
|
|
381
|
+
inMelee = true;
|
|
382
|
+
}
|
|
383
|
+
if (Date.now() >= strafeAt) {
|
|
384
|
+
strafeLeft = !strafeLeft;
|
|
385
|
+
strafeAt = Date.now() + STRAFE_MS;
|
|
386
|
+
}
|
|
387
|
+
await aimAt(bot, entity);
|
|
388
|
+
pressMelee(bot, entity, dangerous ? strafeLeft : null);
|
|
389
|
+
if (d <= MELEE_REACH && Date.now() - lastSwing >= attackCooldownMs(bot)) {
|
|
390
|
+
await meleeSwing(bot, entity, () => {
|
|
391
|
+
stats.lastSwingAt = Date.now();
|
|
392
|
+
stats.lastSwingTargetId = entity.id;
|
|
393
|
+
stats.swings += 1;
|
|
394
|
+
});
|
|
395
|
+
lastSwing = Date.now();
|
|
396
|
+
} else {
|
|
397
|
+
await sleep(50);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} finally {
|
|
401
|
+
releaseMelee(bot);
|
|
402
|
+
ranged?.abort();
|
|
403
|
+
ctx.escape.active = false;
|
|
404
|
+
ctx.attack.release(stats);
|
|
405
|
+
}
|
|
406
|
+
if (stats.dead) {
|
|
407
|
+
return `打死了${zhEntity(target)}(${attackStats(stats)})${retreatFailed ? ';撤退没走开后回身打完' : ''}${underwaterOxygenNote(bot)}`;
|
|
408
|
+
}
|
|
409
|
+
if (!entity.isValid) {
|
|
410
|
+
throw new SkillBlocked(`交手中${zhEntity(target)}离开了视野,没有把“消失”算成击杀;${attackStats(stats)}${underwaterOxygenNote(bot)}`);
|
|
411
|
+
}
|
|
412
|
+
if (entity.isValid) {
|
|
413
|
+
throw new SkillBlocked(
|
|
414
|
+
`${attackStats(stats)}没打死${zhEntity(target)},超时收手${underwaterOxygenNote(bot)}`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
throw new SkillBlocked(`没有确认${zhEntity(target)}死亡;${attackStats(stats)}`);
|
|
418
|
+
}
|
|
419
|
+
|