@myclaw163/clawclaw-cli 0.6.68 → 0.6.69
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/package.json +1 -1
- package/skills/clawclaw/references/KNOWLEDGE.md +1 -1
- package/src/commands/game.ts +15 -0
- package/src/commands/watch.test.ts +11 -0
- package/src/commands/watch.ts +2 -3
- package/src/lib/auth.test.ts +15 -0
- package/src/pipeline/event-format.test.ts +82 -2
- package/src/pipeline/event-format.ts +114 -5
- package/src/pipeline/event-hints.ts +20 -3
- package/src/runtime/event-daemon.test.ts +34 -0
- package/src/runtime/event-daemon.ts +51 -3
- package/src/sdk/index.ts +1 -1
- package/src/strategies/game-utils.test.ts +53 -1
- package/src/strategies/game-utils.ts +69 -17
- package/src/strategies/goals/crab-octopus-reflexes.ts +11 -3
- package/src/strategies/goals/keep-away-goal.ts +9 -5
- package/src/strategies/goals/lone-kill-task-top.ts +25 -9
- package/src/strategies/goals/warrior-shrimp-top.ts +13 -306
- package/src/strategies/hide-spots.ts +11 -75
- package/src/strategies/off-route-points.ts +105 -0
- package/src/strategies/warrior-memory.knowledge.md +2 -2
- package/src/strategies/warrior-memory.ts +1 -1
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import type { GameState, PlayerInfo
|
|
1
|
+
import type { GameState, PlayerInfo } from '../../sdk/types.js';
|
|
2
2
|
import { Action } from '../../sdk/action.js';
|
|
3
|
-
import { planEscape } from '../pathfind/escape-planner.js';
|
|
4
|
-
import { loadWalkableGrid, type WalkableGrid } from '../pathfind/walkable-grid.js';
|
|
5
3
|
import {
|
|
6
4
|
canUseKill,
|
|
7
5
|
corpseAtScene,
|
|
@@ -12,7 +10,6 @@ import {
|
|
|
12
10
|
isKnowledgeTrusted,
|
|
13
11
|
killCooldownSecs,
|
|
14
12
|
killCommitRange,
|
|
15
|
-
killRangeFor,
|
|
16
13
|
matchesAnyTarget,
|
|
17
14
|
nonTeammatesVisible,
|
|
18
15
|
PROGRESS_INTERVAL_MS,
|
|
@@ -23,7 +20,7 @@ import {
|
|
|
23
20
|
} from '../game-utils.js';
|
|
24
21
|
import type { BehaviorDecision, StrategyContext } from '../types.js';
|
|
25
22
|
import { Goal } from './goal.js';
|
|
26
|
-
import {
|
|
23
|
+
import { KeepAwayGoal, type KeepAwayGoalOptions } from './keep-away-goal.js';
|
|
27
24
|
import { KillVisibleTargetGoal } from './kill-target-goal.js';
|
|
28
25
|
import { SafeTaskOrPatrolGoal } from './safe-task-goal.js';
|
|
29
26
|
|
|
@@ -38,66 +35,19 @@ const STRANGER_KEY = 'warrior-stranger-distance';
|
|
|
38
35
|
// 尸体现场灭口开关:false=站在尸体旁也绝不因「身边有未报警的身份不明者」先手灭口,只报警(hostile/启动目标仍按 P1 追杀);
|
|
39
36
|
// true=刀可用时先击杀尸体旁最近的非 trusted 非队友。关掉是为根治「把尸体边未报警的无辜虾当凶手灭口」的误杀。
|
|
40
37
|
const CORPSE_SCENE_KILL_ENABLED: boolean = false;
|
|
41
|
-
// 自卫总开关:false=遇到被怀疑者一律只回避(绝不变向试探、绝不自卫出刀),等同纯回避档;true=启用下方绝境自卫流程。
|
|
42
|
-
const SELF_DEFENSE_ENABLED: boolean = true;
|
|
43
|
-
// 自卫出刀只能由「主动变向试探 + 验证对方跟随」达成,绝不靠被动转角/方位判定——那些会被走廊弯曲、寻路曲线、
|
|
44
|
-
// 贴身掠过污染(对方只是顺着走廊转、并非在追我,却累积出大转角)。流程:
|
|
45
|
-
// ① 同一被怀疑者连续 JUKE_STREAK 帧「朝我贴身追击」(aimedPursuit:相对上帧位移 ≥ MOVE_EPS 且方向与「指向我」
|
|
46
|
-
// 夹角余弦 ≥ TOWARD_COS;cone 半角≈31.8°)且 planEscape 判定甩不掉 → 被咬住又逃不掉,开始试探;
|
|
47
|
-
// ② 主动横向变一次向(jukeTarget),变向幅度足够大,使「不跟随者」必然掉出 cone;
|
|
48
|
-
// ③ 变向后 JUKE_WINDOW 帧内,比较对方【变向前朝向 hBefore】与【当前朝向 hNow】:朝向变化(hNow−hBefore)沿我 juke
|
|
49
|
-
// 方向偏转 reaim≥MARGIN、且当前朝向仍对准我新位置 = 跟随一次(jukeFollowed)。只认对方朝向【响应我变向】的再
|
|
50
|
-
// 瞄准——同向匀速通勤的虾朝向恒定(hNow≈hBefore,reaim≈0),无论它是否恰好直行掠过我旧位置都攒不到跟随;走廊弯
|
|
51
|
-
// 曲/几何发散一概不算。变向后第 1 帧对方因反应延迟可能短暂未转向,故用 WINDOW 容忍这一帧;窗口内始终不跟 → 重置。
|
|
52
|
-
// ④ 攒够 JUKE_FOLLOWS 次跟随、且已进 killRangeFor 射程、视线无墙,才出刀(趁 160 射程在合拢阶段先手,蟹只有 80)。
|
|
53
|
-
const SELF_DEFENSE_JUKE_STREAK = 2;
|
|
54
|
-
const SELF_DEFENSE_JUKE_WINDOW = 3;
|
|
55
|
-
const SELF_DEFENSE_JUKE_FOLLOWS = 2;
|
|
56
|
-
const SELF_DEFENSE_MOVE_EPS = 40;
|
|
57
|
-
const SELF_DEFENSE_TOWARD_COS = 0.85;
|
|
58
|
-
// 跟随判定的「再瞄准余量」:变向后对方【朝向变化】(hNow−hBefore)沿我 juke 方向的分量须达到这个余量,才算它响应了
|
|
59
|
-
// 我的变向。同向匀速通勤者朝向恒定、朝向变化≈0 → reaim≈0 天然被拒;真跟随者转向追我新位置、朝向变化沿 juke 方向 →
|
|
60
|
-
// reaim 为正(近距离 >0.2,越远越小但仍 ≫ 位置噪声)。取一个略高于噪声(≪0.01)的小正数即可分开两者;过大会误拒
|
|
61
|
-
// 中远距离的真跟随者、削弱真自卫。
|
|
62
|
-
const SELF_DEFENSE_REAIM_MARGIN = 0.05;
|
|
63
|
-
// 主动变向试探:横向位移目标距离;方向以横向为主、略带「背离对方」分量以免试探时丢距离;落点至少这么远才算有效变向。
|
|
64
|
-
const SELF_DEFENSE_JUKE_DIST = 170;
|
|
65
|
-
const SELF_DEFENSE_JUKE_LATERAL = 0.85;
|
|
66
|
-
const SELF_DEFENSE_JUKE_AWAY = 0.45;
|
|
67
|
-
const SELF_DEFENSE_JUKE_MIN_MOVE = 60;
|
|
68
38
|
|
|
69
39
|
type ProgressTier = 'flee-bad' | 'keep-distance' | 'task';
|
|
70
40
|
|
|
71
|
-
/** 单位向量;长度过小(重合)返回 null。 */
|
|
72
|
-
function unitVec(x: number, y: number): { x: number; y: number } | null {
|
|
73
|
-
const m = Math.hypot(x, y);
|
|
74
|
-
if (m < 1e-6) return null;
|
|
75
|
-
return { x: x / m, y: y / m };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** 两点世界坐标连线是否整段可走(无墙)。沿线段每半格采样一次,任一采样点落在墙上即判被挡。 */
|
|
79
|
-
function segmentWalkable(grid: WalkableGrid, ax: number, ay: number, bx: number, by: number): boolean {
|
|
80
|
-
const span = Math.hypot(bx - ax, by - ay);
|
|
81
|
-
const steps = Math.max(1, Math.ceil(span / (grid.tileSize * 0.5)));
|
|
82
|
-
for (let i = 0; i <= steps; i++) {
|
|
83
|
-
const t = i / steps;
|
|
84
|
-
const x = ax + (bx - ax) * t;
|
|
85
|
-
const y = ay + (by - ay) * t;
|
|
86
|
-
if (!grid.isWalkable(Math.floor(x / grid.tileSize), Math.floor(y / grid.tileSize))) return false;
|
|
87
|
-
}
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
41
|
/**
|
|
92
42
|
* 带刀虾·记忆进阶版(武士虾/枪虾,带刀好人)。好人阵营但能出刀,按行为表的优先级逐条判断:
|
|
93
43
|
*
|
|
94
44
|
* - P0 发现尸体:旁边是 hostile/启动猎杀目标且刀可用 → 现场先手击杀;未报警的身份不明者默认不灭口
|
|
95
45
|
* (CORPSE_SCENE_KILL_ENABLED,根治误杀);否则报警(靠近至可报距离)。
|
|
96
|
-
* - P1
|
|
97
|
-
* 其余非 trusted 一律按「被怀疑」处理(未标记 =
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
46
|
+
* - P1 处理危险:**只对【已确认 hostile】(启动参数 / 知识库 hostile)出刀**——刀好就追杀,刀冷就硬躲所有非好人。
|
|
47
|
+
* 其余非 trusted 一律按「被怀疑」处理(未标记 = 默认被怀疑):**只回避、绝不出刀**——无可信同伴且单个被怀疑者
|
|
48
|
+
* 贴近 → 按普通虾 B 档先拉开距离(多人互为目击者则照常做任务)。
|
|
49
|
+
* (历史上的「贴身追击判定 + 诱饵试探 + 绝境自卫反杀」整套已删除:几何上无法把「贴身跟我观察的好人」和「贴身追我的
|
|
50
|
+
* 凶手」区分开,自卫出刀迟早误杀同阵营好人、触发 warrior_shrimp_self_destruct 直接崩盘,代价远大于收益。)
|
|
101
51
|
* - P2 紧急任务:无危险时优先处理。
|
|
102
52
|
* - P3 兜底:做任务 / 巡逻
|
|
103
53
|
* (SafeTaskOrPatrolGoal:测地最近、威胁旁/途经威胁的任务硬排除、带粘性)。
|
|
@@ -115,31 +65,11 @@ export class WarriorShrimpTop extends Goal {
|
|
|
115
65
|
);
|
|
116
66
|
private readonly strangerSeenAt = new Map<string, number>();
|
|
117
67
|
private witnessExempt = false;
|
|
118
|
-
/** 自卫:当前被判定在追我的被怀疑者 key、它上一帧坐标、它本帧朝向、连续「朝我贴身追击」帧数(决定何时开始试探)、
|
|
119
|
-
* 变向后等待对方跟随的剩余帧数(0=未在等)、发起变向那刻我的坐标(验证跟随用)、已验证的跟随次数、
|
|
120
|
-
* 本轮是否已推过自卫简报。 */
|
|
121
|
-
private selfDefenseTargetKey: string | null = null;
|
|
122
|
-
private selfDefensePursuerPrev: { x: number; y: number } | null = null;
|
|
123
|
-
/** 追击者本帧朝向(上一帧→本帧的单位位移),aimedPursuit 每帧维护;jukeFollowed 用它判定对方是否响应我变向改朝向。 */
|
|
124
|
-
private selfDefensePursuerHeading: { x: number; y: number } | null = null;
|
|
125
|
-
private selfDefenseStreak = 0;
|
|
126
|
-
private selfDefenseJukeWatch = 0;
|
|
127
|
-
private selfDefenseJukeFromPos: { x: number; y: number } | null = null;
|
|
128
|
-
/** 发起变向那刻追击者的朝向(hBefore):jukeFollowed 比较它与变向后当前朝向的【变化】,判定对方是否真的转向追我新位置。 */
|
|
129
|
-
private selfDefenseJukePursuerHeading: { x: number; y: number } | null = null;
|
|
130
|
-
private selfDefenseJukeFollows = 0;
|
|
131
|
-
private selfDefenseAlerted = false;
|
|
132
68
|
|
|
133
69
|
constructor(private readonly huntTargets: string[] = []) {
|
|
134
70
|
super();
|
|
135
71
|
}
|
|
136
72
|
|
|
137
|
-
/** warrior-memory 开会后不重建 Top(resetOnMeetingResume:false);自卫追击计数按「连续游荡」语义,
|
|
138
|
-
* 开会是天然中断点,恢复后从零重算,避免拿会议前的追击 streak 一恢复就秒杀。 */
|
|
139
|
-
onMeetingResume(): void {
|
|
140
|
-
this.resetSelfDefense();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
73
|
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
144
74
|
const visible = nonTeammatesVisible(state, ctx);
|
|
145
75
|
this.updateWitnessMemory(visible, ctx);
|
|
@@ -173,17 +103,11 @@ export class WarriorShrimpTop extends Goal {
|
|
|
173
103
|
const huntTargets = threats.filter(p => this.isHuntTarget(p, ctx));
|
|
174
104
|
// P1-A 坏人/启动目标且刀好 → 主动追杀。
|
|
175
105
|
if (killReady && huntTargets.length > 0) {
|
|
176
|
-
this.resetSelfDefense();
|
|
177
106
|
this.setHunt(this.nearestByDistance(state, huntTargets).name);
|
|
178
107
|
return [];
|
|
179
108
|
}
|
|
180
|
-
// P1-B 绝境自卫:对被怀疑的贴身追击者(非坏人、非 trusted)——连续确认朝我追击、主动变向试探它每次都跟着咬住、
|
|
181
|
-
// 已进 160 射程、退无可退、视线无墙,才自卫先下手(趁 reach 优势在合拢阶段先手)+ Agent 自卫简报。
|
|
182
|
-
// 不要求先标 suspect:实战里真凶常没来得及标记就开始追杀,所以对所有被怀疑者都适用(juke 验证保证不误杀顺路虾)。
|
|
183
|
-
const selfDefense = this.selfDefenseKill(state, ctx, threats, killReady);
|
|
184
|
-
if (selfDefense) return selfDefense;
|
|
185
109
|
|
|
186
|
-
// P1-
|
|
110
|
+
// P1-B 有坏人/启动目标但刀冷(杀不了)→ 硬躲所有非好人。
|
|
187
111
|
if (huntTargets.length > 0) {
|
|
188
112
|
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
189
113
|
this.setFlee();
|
|
@@ -191,8 +115,8 @@ export class WarriorShrimpTop extends Goal {
|
|
|
191
115
|
return [];
|
|
192
116
|
}
|
|
193
117
|
|
|
194
|
-
// P1-
|
|
195
|
-
// 多个被怀疑者互为目击者(witnessExempt
|
|
118
|
+
// P1-C 只剩被怀疑者(无已确认坏人):无可信同伴且单个贴近、且未被目击者豁免 → 按普通虾 B 档拉开距离(只回避,绝不出刀);
|
|
119
|
+
// 多个被怀疑者互为目击者(witnessExempt)则不后撤,落到下面照常做任务。
|
|
196
120
|
const trustedVisible = visible.some(p => this.isTrusted(p, ctx));
|
|
197
121
|
const worker = this.subGoal;
|
|
198
122
|
const suspectedClose = threats.some(p =>
|
|
@@ -204,8 +128,6 @@ export class WarriorShrimpTop extends Goal {
|
|
|
204
128
|
this.emitProgress(state, ctx, visible, 'keep-distance');
|
|
205
129
|
return [];
|
|
206
130
|
}
|
|
207
|
-
} else {
|
|
208
|
-
this.resetSelfDefense();
|
|
209
131
|
}
|
|
210
132
|
|
|
211
133
|
// ── P2 紧急任务 ──
|
|
@@ -235,7 +157,7 @@ export class WarriorShrimpTop extends Goal {
|
|
|
235
157
|
this.witnessExempt = this.strangerSeenAt.size >= WITNESS_EXEMPT_COUNT;
|
|
236
158
|
}
|
|
237
159
|
|
|
238
|
-
/** 被怀疑者:非 trusted(好人)且非坏人/启动目标——未标记的默认就落这档,对应 B
|
|
160
|
+
/** 被怀疑者:非 trusted(好人)且非坏人/启动目标——未标记的默认就落这档,对应 B 档保持距离(只回避、绝不出刀)。 */
|
|
239
161
|
private isSuspected(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
240
162
|
return !this.isTrusted(p, ctx) && !this.isHuntTarget(p, ctx);
|
|
241
163
|
}
|
|
@@ -278,222 +200,6 @@ export class WarriorShrimpTop extends Goal {
|
|
|
278
200
|
.sort((a, b) => a.d - b.d)[0].p;
|
|
279
201
|
}
|
|
280
202
|
|
|
281
|
-
/**
|
|
282
|
-
* 自卫击杀 / 变向试探:只处理「被怀疑者」(非坏人、非 trusted)持续贴身追击的情形(坏人在上面已优先追杀/硬躲)。
|
|
283
|
-
* 实战里真凶往往还没被标记就开始追杀,所以这里对所有被怀疑者都适用,不要求先标 suspect——juke 验证保证只杀真在追我的。
|
|
284
|
-
* 出刀只能由「主动变向 + 验证对方跟随」达成,绝不靠被动转角/方位,避免「对方只是顺着走廊转、并非在追我」被误判。
|
|
285
|
-
* 每帧:
|
|
286
|
-
* ① aimedPursuit 判定本帧是否「朝我贴身追击」,维护连续帧数(变向等待期内的一帧不朝我是预期的反应延迟,先容忍)。
|
|
287
|
-
* ② 若处在变向等待期:它重新指向我的新位置 = 跟随一次(攒一次);窗口内始终不重新瞄准 = 没跟 → 重置。
|
|
288
|
-
* ③ 攒够 SELF_DEFENSE_JUKE_FOLLOWS 次跟随、已进 killRangeFor 射程、planEscape 判定甩不掉、视线无墙 → 出刀,
|
|
289
|
-
* 并给 Agent 推一条「被动自卫」简报(区别于主动猎杀,免得之后发言/投票露馅)。
|
|
290
|
-
* ④ 否则若已被贴身追够 JUKE_STREAK 帧、甩不掉、且不在等待期 → 主动横向变一次向逼它暴露(窄道无处可变才交回常规逃跑)。
|
|
291
|
-
*
|
|
292
|
-
* 趁 reach 优势:武士虾出刀范围(160)远大于蟹(80),确认在追后在合拢阶段先手,而不是等贴脸拼手速。出刀确认前
|
|
293
|
-
* 不清零追击状态——空刀(极端情况)下进度不该白丢;真杀掉后对方消失,下一帧威胁清空自然重置。
|
|
294
|
-
*/
|
|
295
|
-
private selfDefenseKill(
|
|
296
|
-
state: GameState,
|
|
297
|
-
ctx: StrategyContext,
|
|
298
|
-
threats: PlayerInfo[],
|
|
299
|
-
killReady: boolean,
|
|
300
|
-
): BehaviorDecision[] | null {
|
|
301
|
-
if (!SELF_DEFENSE_ENABLED) return null; // 开关关闭:直接回退到「只回避」,不进入任何试探/出刀逻辑。
|
|
302
|
-
// 目击者豁免同样限制自卫:现场≥2 个互为目击者的被怀疑者时不试探、不出刀。人多时蟹不敢当众动手(嫌疑会落到自己头上),
|
|
303
|
-
// 而拥挤又会把自卫的「退无可退」前提(escapeHopeless 把在场每个人都当障碍)刷成恒真;此档只做任务/回避,杜绝在人堆里误杀。
|
|
304
|
-
if (this.witnessExempt) {
|
|
305
|
-
this.resetSelfDefense();
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
const pursuers = threats.filter(p => !this.isHuntTarget(p, ctx));
|
|
309
|
-
if (pursuers.length === 0) {
|
|
310
|
-
this.resetSelfDefense();
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
const nearest = this.nearestByDistance(state, pursuers);
|
|
314
|
-
const aimed = this.aimedPursuit(state, nearest);
|
|
315
|
-
|
|
316
|
-
if (!killReady) return null;
|
|
317
|
-
|
|
318
|
-
// 维护「朝我贴身追击」连续帧数(决定何时开始试探)。某帧不再朝我 = 追击中断,清零进度但【保留当前目标身份】
|
|
319
|
-
// 继续跟它(softResetPursuit,不是 resetSelfDefense——后者会把目标也丢掉、下一帧又当「第一次见到」永远攒不起来)。
|
|
320
|
-
// 例外:变向等待期内的一帧不朝我是预期的反应延迟,先容忍不清零。
|
|
321
|
-
if (aimed) {
|
|
322
|
-
this.selfDefenseStreak++;
|
|
323
|
-
} else if (this.selfDefenseJukeWatch === 0) {
|
|
324
|
-
this.softResetPursuit();
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// 变向等待期:它重新指向我的【新】位置 = 跟随一次。但必须等我相对变向起点真的横向拉开够远
|
|
329
|
-
// (jukeFollowed:站在对方位置看,「我变向前」与「我现在」已分得比 cone 还开)才算数——否则我变向移动还没生效、
|
|
330
|
-
// 或位移太小直线仍落在 cone 内时,会把「持续朝旧/当前位置走」误判成「响应我变向重新咬住」。窗口内始终凑不出
|
|
331
|
-
// 一次有效跟随 = 没跟(不是在追我)→ 清零进度重新观察。
|
|
332
|
-
if (this.selfDefenseJukeWatch > 0) {
|
|
333
|
-
if (aimed && this.jukeFollowed(state, nearest)) {
|
|
334
|
-
this.selfDefenseJukeFollows++;
|
|
335
|
-
this.selfDefenseJukeWatch = 0;
|
|
336
|
-
} else if (--this.selfDefenseJukeWatch === 0) {
|
|
337
|
-
this.softResetPursuit();
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const hopeless = this.escapeHopeless(state, threats);
|
|
343
|
-
const d = nearest.distance ?? dist(state.you.x, state.you.y, nearest.x, nearest.y);
|
|
344
|
-
if (
|
|
345
|
-
this.selfDefenseJukeFollows >= SELF_DEFENSE_JUKE_FOLLOWS
|
|
346
|
-
&& d <= killRangeFor(state.you.role)
|
|
347
|
-
&& hopeless
|
|
348
|
-
&& this.hasShotLine(state, nearest)
|
|
349
|
-
) {
|
|
350
|
-
this.clearSub();
|
|
351
|
-
if (!this.selfDefenseAlerted) {
|
|
352
|
-
this.selfDefenseAlerted = true;
|
|
353
|
-
ctx.notifications.push(`连续确认被${nearest.name}贴身追击、变向试探它每次都跟着咬住、退无可退,自卫先手出刀。`);
|
|
354
|
-
ctx.agentAlerts.push(
|
|
355
|
-
`我被${nearest.name}持续贴身追击、几次变向试探它都跟着甩不掉、已被逼到退无可退,刚出于自卫先下手把对方击杀了。`
|
|
356
|
-
+ `这是被动自卫不是主动猎杀:接下来发言/投票按「对方一直追着我跑、我被逼到死角才自保」的口径走,别承认无故先手。`,
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
return [{ action: Action.kill(nearest.name) }];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 还没攒够跟随:被贴身追够、甩不掉、且不在等待期时,主动横向变一次向逼它暴露。无处可变(窄道)才交回常规逃跑。
|
|
363
|
-
if (this.selfDefenseStreak >= SELF_DEFENSE_JUKE_STREAK && this.selfDefenseJukeWatch === 0 && hopeless) {
|
|
364
|
-
const juke = this.jukeTarget(state, nearest);
|
|
365
|
-
if (juke) {
|
|
366
|
-
this.selfDefenseJukeWatch = SELF_DEFENSE_JUKE_WINDOW;
|
|
367
|
-
this.selfDefenseJukeFromPos = { x: state.you.x, y: state.you.y };
|
|
368
|
-
// hBefore:变向前对方朝向,jukeFollowed 拿它和窗口内的当前朝向比【变化量】判断是否响应我变向。
|
|
369
|
-
this.selfDefenseJukePursuerHeading = this.selfDefensePursuerHeading;
|
|
370
|
-
this.clearSub();
|
|
371
|
-
return [{ action: Action.move(juke).withThinking('换个方向甩开') }];
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* 变向跟随是否成立(对方【朝向变化】响应判定,非几何发散):比较追击者在我发起变向前的朝向(hBefore)与变向后
|
|
379
|
-
* 本帧的朝向(hNow),看朝向【变化量】(hNow−hBefore)是否朝我 juke 的方向偏转,且当前朝向确实对准我的新位置:
|
|
380
|
-
* · jukeDir = 我从变向起点到现在的实际位移方向(我往哪侧 juke)。
|
|
381
|
-
* · reaim = (hNow−hBefore)·jukeDir:对方朝向沿我 juke 方向偏转了多少。
|
|
382
|
-
* 同向匀速通勤者朝向恒定(hNow≈hBefore)→ reaim≈0 → 判否,与它是否恰好直行掠过我旧位置无关——这正是旧公式的
|
|
383
|
-
* 漏洞:旧公式只拿【当前朝向】比对【新旧位置】,追击者直行掠过旧位置时「指向旧位置」会翻到它身后,reaim 被刷到
|
|
384
|
-
* ≈2 误判成跟随。真跟随者为追我的横向变向把朝向转向新位置 → (hNow−hBefore) 沿 jukeDir 为正 → 判真。再加 headingAtNew
|
|
385
|
-
* 须 ≥ TOWARD_COS,排除「朝向变了但不是冲我新位置来」。不依赖任何固定转角阈值。
|
|
386
|
-
*/
|
|
387
|
-
private jukeFollowed(state: GameState, pursuer: PlayerInfo): boolean {
|
|
388
|
-
const from = this.selfDefenseJukeFromPos;
|
|
389
|
-
const hBefore = this.selfDefenseJukePursuerHeading;
|
|
390
|
-
const hNow = this.selfDefensePursuerHeading;
|
|
391
|
-
if (!from || !hBefore || !hNow) return false;
|
|
392
|
-
// 我相对变向起点还没真的拉开(位移太小)时,朝向变化无从衡量、jukeDir 也是噪声 → 先不判,留给窗口里后续帧。
|
|
393
|
-
const jukeDir = unitVec(state.you.x - from.x, state.you.y - from.y);
|
|
394
|
-
if (!jukeDir || dist(from.x, from.y, state.you.x, state.you.y) < SELF_DEFENSE_JUKE_MIN_MOVE) return false;
|
|
395
|
-
const aimNew = unitVec(state.you.x - pursuer.x, state.you.y - pursuer.y);
|
|
396
|
-
if (!aimNew) return false;
|
|
397
|
-
const headingAtNew = hNow.x * aimNew.x + hNow.y * aimNew.y;
|
|
398
|
-
const reaim = (hNow.x - hBefore.x) * jukeDir.x + (hNow.y - hBefore.y) * jukeDir.y;
|
|
399
|
-
return headingAtNew >= SELF_DEFENSE_TOWARD_COS && reaim >= SELF_DEFENSE_REAIM_MARGIN;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* 本帧同一追击者是否「朝我贴身追击」:相对上一帧位移 ≥ MOVE_EPS(确实在动,排除站桩做任务)且位移方向
|
|
404
|
-
* 与「指向我当前位置」夹角余弦 ≥ TOWARD_COS(朝我来;cone 半角≈31.8°)。换目标时整体重置并返回 false。
|
|
405
|
-
* 只读对方位移与「指向我」的关系,参考点一致;不累计任何转角(转角会被走廊弯曲污染,确认改由变向跟随负责)。
|
|
406
|
-
* 注意:这只是【触发器】,不是敌意判别——直线追我与直线通勤在被试探前逐帧完全相同(朝向都恒定、都指向我),
|
|
407
|
-
* 单凭每帧朝向/方差无法区分;判别力全在 jukeFollowed(试探后对方是否改朝向响应)。顺手把本帧朝向存进
|
|
408
|
-
* selfDefensePursuerHeading 供其使用(仅在 movedFar 时被消费,故站桩噪声朝向进不去)。
|
|
409
|
-
*/
|
|
410
|
-
private aimedPursuit(state: GameState, nearest: PlayerInfo): boolean {
|
|
411
|
-
const key = nearest.name || String(nearest.seat ?? '');
|
|
412
|
-
const curS = { x: nearest.x, y: nearest.y };
|
|
413
|
-
if (key !== this.selfDefenseTargetKey) {
|
|
414
|
-
this.resetSelfDefense();
|
|
415
|
-
this.selfDefenseTargetKey = key;
|
|
416
|
-
this.selfDefensePursuerPrev = curS;
|
|
417
|
-
return false;
|
|
418
|
-
}
|
|
419
|
-
const sPrev = this.selfDefensePursuerPrev;
|
|
420
|
-
this.selfDefensePursuerPrev = curS;
|
|
421
|
-
if (!sPrev) {
|
|
422
|
-
this.selfDefensePursuerHeading = null;
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const heading = unitVec(curS.x - sPrev.x, curS.y - sPrev.y);
|
|
427
|
-
this.selfDefensePursuerHeading = heading;
|
|
428
|
-
const aim = unitVec(state.you.x - sPrev.x, state.you.y - sPrev.y);
|
|
429
|
-
const movedFar = dist(sPrev.x, sPrev.y, curS.x, curS.y) >= SELF_DEFENSE_MOVE_EPS;
|
|
430
|
-
return !!heading && !!aim && movedFar
|
|
431
|
-
&& heading.x * aim.x + heading.y * aim.y >= SELF_DEFENSE_TOWARD_COS;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/** planEscape 判定每条逃跑分支都会在推演内被追上(避不开);用与 KeepAwayGoal 一致的基线参数。
|
|
435
|
-
* 这是自卫的【必要条件】(退无可退才试探/出刀),不是敌意证据——死路里它对任何人都恒真、与对方是否敌对无关;
|
|
436
|
-
* 敌意判别 100% 来自 jukeFollowed 的朝向响应。 */
|
|
437
|
-
private escapeHopeless(state: GameState, threats: PlayerInfo[]): boolean {
|
|
438
|
-
const threatPoints = threats.map(p => ({ x: p.x, y: p.y }));
|
|
439
|
-
return planEscape(state.you, threatPoints, KEEP_AWAY_BASE_PLAN_OPTS).caught;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/** 我到目标的连线是否无墙——确认了也别对着墙空刀(target_blocked_by_wall)。 */
|
|
443
|
-
private hasShotLine(state: GameState, target: PlayerInfo): boolean {
|
|
444
|
-
return segmentWalkable(loadWalkableGrid(), state.you.x, state.you.y, target.x, target.y);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* 主动变向试探的落点:垂直于「背离追击者」轴的横向点(略带背离分量以免丢距离),取两侧中离追击者更远、
|
|
449
|
-
* 且我到落点整段无墙的一侧。两侧都不可走 / 反而明显贴近追击者(窄道、贴墙)时返回 null,交回常规逃跑。
|
|
450
|
-
*/
|
|
451
|
-
private jukeTarget(state: GameState, pursuer: PlayerInfo): Position | null {
|
|
452
|
-
const grid = loadWalkableGrid();
|
|
453
|
-
const me = state.you;
|
|
454
|
-
const curSep = dist(me.x, me.y, pursuer.x, pursuer.y);
|
|
455
|
-
const away = unitVec(me.x - pursuer.x, me.y - pursuer.y) ?? { x: 1, y: 0 };
|
|
456
|
-
const sides = [{ x: -away.y, y: away.x }, { x: away.y, y: -away.x }];
|
|
457
|
-
let best: { target: Position; sep: number } | null = null;
|
|
458
|
-
for (const perp of sides) {
|
|
459
|
-
const dir = unitVec(
|
|
460
|
-
perp.x * SELF_DEFENSE_JUKE_LATERAL + away.x * SELF_DEFENSE_JUKE_AWAY,
|
|
461
|
-
perp.y * SELF_DEFENSE_JUKE_LATERAL + away.y * SELF_DEFENSE_JUKE_AWAY,
|
|
462
|
-
);
|
|
463
|
-
if (!dir) continue;
|
|
464
|
-
const raw = { x: me.x + dir.x * SELF_DEFENSE_JUKE_DIST, y: me.y + dir.y * SELF_DEFENSE_JUKE_DIST };
|
|
465
|
-
const cell = grid.snapToWalkable(grid.worldToCell(raw.x, raw.y), 6);
|
|
466
|
-
if (cell < 0) continue;
|
|
467
|
-
const target = grid.cellToWorld(cell);
|
|
468
|
-
if (dist(me.x, me.y, target.x, target.y) < SELF_DEFENSE_JUKE_MIN_MOVE) continue;
|
|
469
|
-
if (!segmentWalkable(grid, me.x, me.y, target.x, target.y)) continue;
|
|
470
|
-
const sep = dist(target.x, target.y, pursuer.x, pursuer.y);
|
|
471
|
-
if (!best || sep > best.sep) best = { target, sep };
|
|
472
|
-
}
|
|
473
|
-
// 不往追击者身上凑:最优侧也比当前明显更近就放弃变向(交回常规逃跑)。
|
|
474
|
-
if (best && best.sep < curSep - SELF_DEFENSE_JUKE_MIN_MOVE) return null;
|
|
475
|
-
return best?.target ?? null;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/** 追击中断(某帧不再朝我 / 变向窗口内没凑出有效跟随):清零追击进度,但【保留当前目标身份与上一帧坐标】,
|
|
479
|
-
* 它若重新咬住可继续累计——区别于 resetSelfDefense 连目标一起丢(那样下一帧又当「第一次见到」永远攒不起来)。 */
|
|
480
|
-
private softResetPursuit(): void {
|
|
481
|
-
this.selfDefenseStreak = 0;
|
|
482
|
-
this.selfDefenseJukeWatch = 0;
|
|
483
|
-
this.selfDefenseJukeFromPos = null;
|
|
484
|
-
this.selfDefenseJukePursuerHeading = null;
|
|
485
|
-
this.selfDefenseJukeFollows = 0;
|
|
486
|
-
this.selfDefenseAlerted = false;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/** 整体重置:连当前追击目标身份一起丢(换目标、目标消失、开会、转入主动猎杀时用)。 */
|
|
490
|
-
private resetSelfDefense(): void {
|
|
491
|
-
this.selfDefenseTargetKey = null;
|
|
492
|
-
this.selfDefensePursuerPrev = null;
|
|
493
|
-
this.selfDefensePursuerHeading = null;
|
|
494
|
-
this.softResetPursuit();
|
|
495
|
-
}
|
|
496
|
-
|
|
497
203
|
private setHunt(targetName: string): void {
|
|
498
204
|
const worker = this.subGoal;
|
|
499
205
|
if (worker instanceof KillVisibleTargetGoal && worker.targetName === targetName) return;
|
|
@@ -508,7 +214,8 @@ export class WarriorShrimpTop extends Goal {
|
|
|
508
214
|
this.setSubGoal(new KeepAwayGoal(
|
|
509
215
|
FLEE_KEY,
|
|
510
216
|
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isThreat(p, c)),
|
|
511
|
-
|
|
217
|
+
// 行为不变(仍躲所有非 trusted);措辞按真实身份逐个标注,免得把一起躲的被怀疑者也叫成「认定坏人」。
|
|
218
|
+
{ threatRadius: Infinity, noun: (p, c) => this.isHuntTarget(p, c) ? '认定坏人' : '被怀疑者' } satisfies KeepAwayGoalOptions,
|
|
512
219
|
), SUB_PRIORITY);
|
|
513
220
|
}
|
|
514
221
|
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import type { Position } from '../sdk/types.js';
|
|
2
|
-
import {
|
|
3
|
-
import { assessRoutes } from './pathfind/escape-planner.js';
|
|
2
|
+
import { nearestSafePoint, type OffRoutePoint } from './off-route-points.js';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
x: number;
|
|
8
|
-
y: number;
|
|
9
|
-
/** 所在房间,仅供日志/调试。 */
|
|
10
|
-
room: string;
|
|
11
|
-
}
|
|
4
|
+
/** 藏身角落,离路点的一种(见 OffRoutePoint)。 */
|
|
5
|
+
export type HideSpot = OffRoutePoint;
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
8
|
* 躲藏点:离线从服务端烘焙地图(config/clawclaw/clawclaw.tmj.baked.npz)算出的「藏身角落」。筛选条件:
|
|
@@ -47,77 +41,19 @@ export interface NearestSafeHideOptions {
|
|
|
47
41
|
blockedTarget?: Position | null;
|
|
48
42
|
}
|
|
49
43
|
|
|
50
|
-
const BLOCKED_RADIUS = 12;
|
|
51
|
-
|
|
52
|
-
function sameSpot(a: HideSpot, b: HideSpot): boolean {
|
|
53
|
-
return a.x === b.x && a.y === b.y;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** 所有点都被威胁封死时的兜底:离威胁最远的点(仍好过原地等死)。 */
|
|
57
|
-
function farthestFromThreats(threatPoints: Position[], blocked?: Position | null): HideSpot | null {
|
|
58
|
-
let best: HideSpot | null = null;
|
|
59
|
-
let bestMin = -Infinity;
|
|
60
|
-
for (const s of HIDE_SPOTS) {
|
|
61
|
-
if (blocked && dist(s.x, s.y, blocked.x, blocked.y) <= BLOCKED_RADIUS) continue;
|
|
62
|
-
const minD = threatPoints.length === 0 ? 0 : Math.min(...threatPoints.map(p => dist(s.x, s.y, p.x, p.y)));
|
|
63
|
-
if (minD > bestMin) {
|
|
64
|
-
bestMin = minD;
|
|
65
|
-
best = s;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return best;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
44
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* 全部被威胁封死时退回「离威胁最远」的点。
|
|
45
|
+
* 选「最近的安全躲藏点」:威胁旁的点硬排除、去路经过威胁附近的点排除,余者取测地最近,带粘性
|
|
46
|
+
* (当前点仍合法就不换),全部被封死时退回「离威胁最远」的点。实现复用 nearestSafePoint。
|
|
75
47
|
*/
|
|
76
48
|
export function nearestSafeHideSpot(
|
|
77
49
|
from: Position,
|
|
78
50
|
threatPoints: Position[] = [],
|
|
79
51
|
opts: NearestSafeHideOptions = {},
|
|
80
52
|
): HideSpot | null {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (candidates.length === 0) return farthestFromThreats(threatPoints, blocked);
|
|
89
|
-
|
|
90
|
-
// 无威胁时粘性可零成本短路(不必为路径检查扫距离场)。
|
|
91
|
-
if (opts.stickyTo && threatPoints.length === 0) {
|
|
92
|
-
const sticky = candidates.find(c => sameSpot(c, opts.stickyTo!));
|
|
93
|
-
if (sticky) return sticky;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const routes = assessRoutes(
|
|
97
|
-
from,
|
|
98
|
-
candidates.map(c => ({ x: c.x, y: c.y })),
|
|
99
|
-
threatPoints,
|
|
100
|
-
opts.pathThreatRadius ?? HIDE_PATH_THREAT_RADIUS,
|
|
101
|
-
);
|
|
102
|
-
const indexed = candidates.map((c, i) => ({ c, i }));
|
|
103
|
-
const viable = routes == null ? indexed : indexed.filter(({ i }) => !routes[i].nearThreat);
|
|
104
|
-
const pool = viable.length > 0 ? viable : indexed;
|
|
105
|
-
|
|
106
|
-
if (opts.stickyTo) {
|
|
107
|
-
const sticky = pool.find(({ c }) => sameSpot(c, opts.stickyTo!));
|
|
108
|
-
if (sticky) return sticky.c;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let best = pool[0].c;
|
|
112
|
-
let bestScore = Infinity;
|
|
113
|
-
for (const { c, i } of pool) {
|
|
114
|
-
const euclid = dist(from.x, from.y, c.x, c.y);
|
|
115
|
-
const geo = routes?.[i].distancePx;
|
|
116
|
-
const score = geo == null ? euclid : geo !== Infinity ? geo : euclid + 1e6;
|
|
117
|
-
if (score < bestScore) {
|
|
118
|
-
bestScore = score;
|
|
119
|
-
best = c;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return best;
|
|
53
|
+
return nearestSafePoint(HIDE_SPOTS, from, threatPoints, {
|
|
54
|
+
threatExcludeRadius: opts.threatExcludeRadius ?? HIDE_THREAT_EXCLUDE_RANGE,
|
|
55
|
+
pathThreatRadius: opts.pathThreatRadius ?? HIDE_PATH_THREAT_RADIUS,
|
|
56
|
+
stickyTo: opts.stickyTo ?? null,
|
|
57
|
+
blockedTarget: opts.blockedTarget ?? null,
|
|
58
|
+
});
|
|
123
59
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Position } from '../sdk/types.js';
|
|
2
|
+
import { dist } from './game-utils.js';
|
|
3
|
+
import { assessRoutes } from './pathfind/escape-planner.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 离线从烘焙地图算出的「离路点」:远离任务点/出生点、避开走廊与枢纽的低人流坐标
|
|
7
|
+
* (世界像素,与 GameState / 可行走网格同坐标系)。HIDE_SPOTS(藏身角落)是它的具体集合,
|
|
8
|
+
* 共用下面的选点器。
|
|
9
|
+
*/
|
|
10
|
+
export interface OffRoutePoint extends Position {
|
|
11
|
+
/** 所在房间,仅供日志/调试。 */
|
|
12
|
+
room: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NearestSafePointOptions<T extends Position = Position> {
|
|
16
|
+
/** 点离任一威胁多近就硬排除。 */
|
|
17
|
+
threatExcludeRadius: number;
|
|
18
|
+
/** 去该点的测地路径经过威胁这个距离内也排除(避免走向把守者)。 */
|
|
19
|
+
pathThreatRadius: number;
|
|
20
|
+
/** 当前已选点:仍是合法候选就保持不变,杜绝等距摇摆。 */
|
|
21
|
+
stickyTo?: T | null;
|
|
22
|
+
/** 被标记为不可达的移动目标:在其附近的点跳过。 */
|
|
23
|
+
blockedTarget?: Position | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const BLOCKED_RADIUS = 12;
|
|
27
|
+
|
|
28
|
+
function samePoint(a: Position, b: Position): boolean {
|
|
29
|
+
return a.x === b.x && a.y === b.y;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 所有点都被威胁封死时的兜底:离威胁最远的点(仍好过原地等死)。 */
|
|
33
|
+
function farthestFromThreats<T extends Position>(
|
|
34
|
+
points: readonly T[],
|
|
35
|
+
threatPoints: Position[],
|
|
36
|
+
blocked?: Position | null,
|
|
37
|
+
): T | null {
|
|
38
|
+
let best: T | null = null;
|
|
39
|
+
let bestMin = -Infinity;
|
|
40
|
+
for (const s of points) {
|
|
41
|
+
if (blocked && dist(s.x, s.y, blocked.x, blocked.y) <= BLOCKED_RADIUS) continue;
|
|
42
|
+
const minD = threatPoints.length === 0 ? 0 : Math.min(...threatPoints.map(p => dist(s.x, s.y, p.x, p.y)));
|
|
43
|
+
if (minD > bestMin) {
|
|
44
|
+
bestMin = minD;
|
|
45
|
+
best = s;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return best;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 从离线点集里选「测地最近的安全点」:威胁旁的点硬排除、去路经过威胁附近的点排除,余者取测地最近
|
|
53
|
+
* (distance-field 一次扫描;测地不可达按欧氏垫底),带粘性(当前点仍合法就不换)。全部被威胁封死
|
|
54
|
+
* 时退回「离威胁最远」的点。nearestSafeHideSpot 复用此选点器。
|
|
55
|
+
*/
|
|
56
|
+
export function nearestSafePoint<T extends Position>(
|
|
57
|
+
points: readonly T[],
|
|
58
|
+
from: Position,
|
|
59
|
+
threatPoints: Position[],
|
|
60
|
+
opts: NearestSafePointOptions<T>,
|
|
61
|
+
): T | null {
|
|
62
|
+
const excludeR = opts.threatExcludeRadius;
|
|
63
|
+
const blocked = opts.blockedTarget ?? null;
|
|
64
|
+
|
|
65
|
+
const candidates = points.filter(s =>
|
|
66
|
+
(!blocked || dist(s.x, s.y, blocked.x, blocked.y) > BLOCKED_RADIUS)
|
|
67
|
+
&& !threatPoints.some(p => dist(s.x, s.y, p.x, p.y) <= excludeR));
|
|
68
|
+
|
|
69
|
+
if (candidates.length === 0) return farthestFromThreats(points, threatPoints, blocked);
|
|
70
|
+
|
|
71
|
+
// 无威胁时粘性可零成本短路(不必为路径检查扫距离场)。
|
|
72
|
+
if (opts.stickyTo && threatPoints.length === 0) {
|
|
73
|
+
const sticky = candidates.find(c => samePoint(c, opts.stickyTo!));
|
|
74
|
+
if (sticky) return sticky;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const routes = assessRoutes(
|
|
78
|
+
from,
|
|
79
|
+
candidates.map(c => ({ x: c.x, y: c.y })),
|
|
80
|
+
threatPoints,
|
|
81
|
+
opts.pathThreatRadius,
|
|
82
|
+
);
|
|
83
|
+
const indexed = candidates.map((c, i) => ({ c, i }));
|
|
84
|
+
const viable = routes == null ? indexed : indexed.filter(({ i }) => !routes[i].nearThreat);
|
|
85
|
+
const pool = viable.length > 0 ? viable : indexed;
|
|
86
|
+
|
|
87
|
+
if (opts.stickyTo) {
|
|
88
|
+
const sticky = pool.find(({ c }) => samePoint(c, opts.stickyTo!));
|
|
89
|
+
if (sticky) return sticky.c;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 测地最近(distance-field 距离;测地不可达按欧氏垫底)。
|
|
93
|
+
let best = pool[0].c;
|
|
94
|
+
let bestScore = Infinity;
|
|
95
|
+
for (const { c, i } of pool) {
|
|
96
|
+
const euclid = dist(from.x, from.y, c.x, c.y);
|
|
97
|
+
const geo = routes?.[i].distancePx;
|
|
98
|
+
const score = geo == null ? euclid : geo !== Infinity ? geo : euclid + 1e6;
|
|
99
|
+
if (score < bestScore) {
|
|
100
|
+
bestScore = score;
|
|
101
|
+
best = c;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|------|------|
|
|
7
7
|
| `hostile`(坏人) | 敌对:刀可用时主动追杀;刀冷却或枪虾无次数时硬回避 |
|
|
8
8
|
| `trusted`(好人) | 可信:绝不攻击,也不会因其在场而对被怀疑者后撤 |
|
|
9
|
-
| 未标记(被怀疑) |
|
|
9
|
+
| 未标记(被怀疑) | 默认回避观察,刀可用也**绝不出刀**(无自卫例外) |
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**不对被怀疑者出刀(无自卫机制)**:武士虾只对【已确认 hostile / 启动猎杀目标】出刀。历史上的「被怀疑者持续贴身追击 → 诱饵死角试探 → 绝境自卫先手」整套已删除:几何上无法把「贴身跟我观察/盯防的好人」与「贴身追我的凶手」区分开(两者跟随轨迹一样),自卫出刀迟早误杀同阵营好人、触发 warrior_shrimp_self_destruct 直接崩盘,代价远大于收益。被怀疑者贴身追来也只回避(拉开距离 / 借人多互为目击掩护),绝不反杀。
|
|
12
12
|
|
|
13
13
|
被怀疑者(未标记)平时按陌生人处理:不主动猎杀;无 trusted 同伴且单个被怀疑者贴近 270px 内时先拉开距离,多个互为目击者时照常做任务;坏人刀冷时硬躲所有非好人。尸体场景对【未报警的身份不明者】只报警不灭口:站在尸体旁即便身边有被怀疑者,也绝不先手击杀,一律靠近报警;但若尸体旁是 hostile 或启动猎杀目标、刀可用,仍会现场先手击杀(凶手就在身边照杀)。
|
|
14
14
|
|
|
@@ -6,7 +6,7 @@ import { parseTargetArgs } from './player-targets.js';
|
|
|
6
6
|
export const strategy: StrategyEntry = {
|
|
7
7
|
id: 'warrior-memory',
|
|
8
8
|
description:
|
|
9
|
-
'带刀虾·记忆进阶版(武士虾/枪虾通用,好人带刀;kill-lone 的带记忆升级)。读取 ccl knowledge 两档标记(hostile=坏人 / trusted=好人),未标记的一律默认被怀疑:hostile 刀好时主动追杀、刀不好时硬回避,trusted
|
|
9
|
+
'带刀虾·记忆进阶版(武士虾/枪虾通用,好人带刀;kill-lone 的带记忆升级)。读取 ccl knowledge 两档标记(hostile=坏人 / trusted=好人),未标记的一律默认被怀疑:hostile 刀好时主动追杀、刀不好时硬回避,trusted 视为可信同伴且绝不攻击。**只对【已确认 hostile / 启动猎杀目标】出刀**——被怀疑者一律只回避、绝不出刀(无可信同伴且单个被怀疑者贴近270内时先拉开到300再做任务,多人互为目击者则照常做任务);不再有任何贴身追击判定/诱饵试探/绝境自卫(几何上分不开"贴身跟我观察的好人"和"贴身追我的凶手",自卫迟早误杀同阵营好人触发武士虾自爆,整套已删除)。发现尸体优先报警;尸体旁若是 hostile/启动猎杀目标且刀可用会现场先手击杀,但绝不会因尸体旁有未报警的身份不明者就灭口(已根治该误杀);无危险时优先紧急任务,否则做任务/巡逻。枪虾会尊重剩余出刀次数。',
|
|
10
10
|
create(args?: string[]) {
|
|
11
11
|
const huntTargets = args ? parseTargetArgs(args) : [];
|
|
12
12
|
return new GoalRootStrategy('warrior-memory', () => new WarriorShrimpTop(huntTargets), {
|