@myclaw163/clawclaw-cli 0.6.66 → 0.6.67
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 +427 -427
- package/bin/clawclaw-cli.mjs +3 -3
- package/package.json +48 -48
- package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -23
- package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -22
- package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -22
- package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -22
- package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -23
- package/scripts/find-hide-spots.py +157 -157
- package/scripts/postinstall.mjs +20 -20
- package/scripts/sync-bundled-skill.mjs +245 -245
- package/scripts/sync-bundled-skill.test.mjs +152 -152
- package/skills/clawclaw/SKILL.md +244 -245
- package/skills/clawclaw/references/CHATTERBOX.md +142 -142
- package/skills/clawclaw/references/COMMANDS.md +148 -148
- package/skills/clawclaw/references/GAME-MECHANICS.md +188 -188
- package/skills/clawclaw/references/HUB.md +48 -48
- package/skills/clawclaw/references/KNOWLEDGE.md +43 -43
- package/skills/clawclaw/references/STRATEGIES.md +57 -57
- package/skills/clawclaw/references/STREAM.md +91 -92
- package/skills/clawclaw/references/TACTICS.md +65 -65
- package/src/assets/clawclaw-ascii-map.txt +40 -40
- package/src/cli.ts +110 -110
- package/src/commands/_schema.ts +109 -109
- package/src/commands/account.ts +209 -209
- package/src/commands/do.test.ts +73 -73
- package/src/commands/do.ts +126 -126
- package/src/commands/events.test.ts +71 -71
- package/src/commands/events.ts +155 -155
- package/src/commands/game-map.test.ts +28 -28
- package/src/commands/game-start-plan.test.ts +84 -84
- package/src/commands/game.ts +1027 -1027
- package/src/commands/history-player.test.ts +102 -102
- package/src/commands/history.ts +573 -573
- package/src/commands/hub.test.ts +96 -96
- package/src/commands/hub.ts +234 -234
- package/src/commands/knowledge.test.ts +19 -19
- package/src/commands/knowledge.ts +168 -168
- package/src/commands/load.test.ts +51 -51
- package/src/commands/load.ts +13 -13
- package/src/commands/meeting-history.test.ts +106 -106
- package/src/commands/memory.ts +40 -40
- package/src/commands/peek.ts +45 -45
- package/src/commands/persona.ts +57 -57
- package/src/commands/setup/codex.ts +266 -248
- package/src/commands/skill.ts +128 -128
- package/src/commands/state.ts +46 -46
- package/src/commands/strategy.test.ts +135 -135
- package/src/commands/strategy.ts +180 -180
- package/src/commands/tts.ts +128 -128
- package/src/commands/upgrade.test.ts +82 -82
- package/src/commands/upgrade.ts +148 -148
- package/src/commands/watch.test.ts +966 -966
- package/src/commands/watch.ts +659 -659
- package/src/lib/auth.test.ts +59 -59
- package/src/lib/auth.ts +186 -186
- package/src/lib/command-meta.ts +37 -37
- package/src/lib/game-client.ts +391 -391
- package/src/lib/http-keepalive.ts +15 -15
- package/src/lib/http-transport.test.ts +42 -42
- package/src/lib/http-transport.ts +113 -113
- package/src/lib/hub-client.test.ts +56 -56
- package/src/lib/hub-client.ts +88 -88
- package/src/lib/hub-install.test.ts +98 -98
- package/src/lib/hub-install.ts +121 -121
- package/src/lib/hub-reminder.ts +75 -75
- package/src/lib/hub-unzip.test.ts +69 -69
- package/src/lib/hub-unzip.ts +62 -62
- package/src/lib/init-command.test.ts +75 -75
- package/src/lib/init-command.ts +120 -120
- package/src/lib/knowledge-store.test.ts +180 -180
- package/src/lib/knowledge-store.ts +374 -374
- package/src/lib/load-context.test.ts +52 -52
- package/src/lib/load-context.ts +52 -52
- package/src/lib/match-state.test.ts +134 -134
- package/src/lib/match-state.ts +94 -94
- package/src/lib/netease-tts.ts +83 -83
- package/src/lib/normalize.ts +42 -42
- package/src/lib/persona.test.ts +41 -41
- package/src/lib/persona.ts +72 -72
- package/src/lib/server-registry.ts +152 -152
- package/src/lib/skill-version.test.ts +48 -48
- package/src/lib/skill-version.ts +19 -19
- package/src/lib/strategy-export.test.ts +232 -232
- package/src/lib/strategy-export.ts +242 -242
- package/src/lib/tts-keys.ts +7 -7
- package/src/lib/tts-speech.test.ts +63 -63
- package/src/lib/tts-speech.ts +76 -76
- package/src/lib/workspace-argv.test.ts +49 -49
- package/src/lib/workspace-argv.ts +44 -44
- package/src/perception/player-history-store.test.ts +87 -87
- package/src/perception/player-history-store.ts +194 -194
- package/src/pipeline/event-format.test.ts +135 -135
- package/src/pipeline/event-format.ts +376 -376
- package/src/pipeline/event-hints.ts +173 -173
- package/src/pipeline/event-store.test.ts +28 -28
- package/src/pipeline/event-store.ts +193 -193
- package/src/pipeline/pipeline.ts +35 -35
- package/src/runtime/auto-upgrade.test.ts +66 -66
- package/src/runtime/auto-upgrade.ts +31 -31
- package/src/runtime/event-daemon.test.ts +107 -107
- package/src/runtime/event-daemon.ts +409 -409
- package/src/runtime/owner-control.ts +150 -150
- package/src/runtime/raw-ws-log.test.ts +33 -33
- package/src/runtime/raw-ws-log.ts +32 -32
- package/src/runtime/runtime-logger.ts +107 -107
- package/src/runtime/ws-client.test.ts +104 -104
- package/src/runtime/ws-client.ts +272 -272
- package/src/sdk/action.ts +166 -166
- package/src/sdk/index.ts +111 -111
- package/src/sdk/types.ts +159 -159
- package/src/strategies/avoid-lone.ts +11 -11
- package/src/strategies/avoid-players.knowledge.md +20 -20
- package/src/strategies/avoid-players.ts +15 -15
- package/src/strategies/corpse-patrol.ts +22 -22
- package/src/strategies/crab-sabotage.ts +21 -21
- package/src/strategies/custom-module.test.ts +269 -269
- package/src/strategies/find-player.ts +16 -16
- package/src/strategies/game-utils.test.ts +190 -190
- package/src/strategies/game-utils.ts +782 -782
- package/src/strategies/goals/anchor-linger.ts +77 -77
- package/src/strategies/goals/avoid-lone-top.ts +168 -168
- package/src/strategies/goals/avoid-players-top.test.ts +83 -83
- package/src/strategies/goals/avoid-players-top.ts +121 -121
- package/src/strategies/goals/conversation-goal.ts +51 -51
- package/src/strategies/goals/corpse-patrol-top.ts +91 -91
- package/src/strategies/goals/crab-octopus-reflexes.ts +93 -93
- package/src/strategies/goals/crab-sabotage-top.ts +197 -197
- package/src/strategies/goals/emergency-hunt-goal.ts +28 -28
- package/src/strategies/goals/find-player-top.ts +93 -93
- package/src/strategies/goals/flee-players-goal.ts +53 -53
- package/src/strategies/goals/follow-companion-goal.ts +106 -106
- package/src/strategies/goals/goal-manager.ts +41 -41
- package/src/strategies/goals/goal-root-strategy.ts +49 -49
- package/src/strategies/goals/goal.ts +28 -28
- package/src/strategies/goals/hide-top.ts +197 -197
- package/src/strategies/goals/keep-away-goal.ts +217 -217
- package/src/strategies/goals/kill-frenzy-top.ts +80 -80
- package/src/strategies/goals/kill-lone-top.ts +160 -160
- package/src/strategies/goals/kill-target-goal.ts +59 -59
- package/src/strategies/goals/kill-target-top.ts +109 -109
- package/src/strategies/goals/leaf-goal.ts +25 -25
- package/src/strategies/goals/linger-corpse-goal.ts +35 -35
- package/src/strategies/goals/lone-kill-core.ts +82 -82
- package/src/strategies/goals/lone-kill-goal.ts +24 -24
- package/src/strategies/goals/lone-kill-task-top.test.ts +85 -85
- package/src/strategies/goals/lone-kill-task-top.ts +86 -86
- package/src/strategies/goals/move-room-goal.ts +60 -60
- package/src/strategies/goals/normal-shrimp-top.test.ts +80 -80
- package/src/strategies/goals/normal-shrimp-top.ts +242 -242
- package/src/strategies/goals/paradise-fish-top.test.ts +126 -126
- package/src/strategies/goals/paradise-fish-top.ts +207 -207
- package/src/strategies/goals/patrol-top.ts +57 -57
- package/src/strategies/goals/report-patrol-top.ts +80 -80
- package/src/strategies/goals/safe-task-goal.ts +102 -102
- package/src/strategies/goals/social-task-top.ts +161 -161
- package/src/strategies/goals/task-kill-report-top.ts +163 -163
- package/src/strategies/goals/task-only-top.ts +57 -57
- package/src/strategies/goals/task-or-patrol-goal.ts +41 -41
- package/src/strategies/goals/task-report-top.ts +57 -57
- package/src/strategies/goals/wander-task-goal.ts +33 -33
- package/src/strategies/goals/warrior-shrimp-top.test.ts +86 -86
- package/src/strategies/goals/warrior-shrimp-top.ts +500 -500
- package/src/strategies/greeting.ts +53 -53
- package/src/strategies/hide-spots.ts +123 -123
- package/src/strategies/hide.ts +23 -23
- package/src/strategies/kill-frenzy.ts +12 -12
- package/src/strategies/kill-lone.knowledge.md +20 -20
- package/src/strategies/kill-lone.ts +13 -13
- package/src/strategies/kill-target.ts +18 -18
- package/src/strategies/loader.test.ts +678 -678
- package/src/strategies/loader.ts +172 -172
- package/src/strategies/lone-kill-task.ts +21 -21
- package/src/strategies/meeting-gate.test.ts +59 -59
- package/src/strategies/meeting-gate.ts +23 -23
- package/src/strategies/move-room.ts +15 -15
- package/src/strategies/new-events-backfill.ts +98 -98
- package/src/strategies/paradise-fish.knowledge.md +20 -20
- package/src/strategies/paradise-fish.ts +25 -25
- package/src/strategies/pathfind/distance-field.ts +150 -150
- package/src/strategies/pathfind/escape-planner.test.ts +197 -197
- package/src/strategies/pathfind/escape-planner.ts +355 -355
- package/src/strategies/pathfind/walkable-grid.ts +117 -117
- package/src/strategies/patrol.ts +11 -11
- package/src/strategies/player-targets.ts +13 -13
- package/src/strategies/report-patrol.ts +11 -11
- package/src/strategies/shrimp-memory.knowledge.md +20 -20
- package/src/strategies/shrimp-memory.ts +25 -25
- package/src/strategies/social-task.test.ts +28 -28
- package/src/strategies/social-task.ts +49 -49
- package/src/strategies/spawn.ts +82 -82
- package/src/strategies/speech-module.ts +123 -123
- package/src/strategies/strategy-loop.ts +771 -771
- package/src/strategies/task-kill-report.ts +17 -17
- package/src/strategies/task-only.ts +11 -11
- package/src/strategies/task-report.ts +22 -22
- package/src/strategies/types.ts +102 -102
- package/src/strategies/warrior-memory.knowledge.md +22 -22
- package/src/strategies/warrior-memory.ts +16 -16
|
@@ -1,500 +1,500 @@
|
|
|
1
|
-
import type { GameState, PlayerInfo, Position } from '../../sdk/types.js';
|
|
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
|
-
import {
|
|
6
|
-
canUseKill,
|
|
7
|
-
corpseAtScene,
|
|
8
|
-
dist,
|
|
9
|
-
firstAvailableTask,
|
|
10
|
-
hasKnownCorpse,
|
|
11
|
-
isKnowledgeHostile,
|
|
12
|
-
isKnowledgeThreat,
|
|
13
|
-
isKnowledgeTrusted,
|
|
14
|
-
killCooldownSecs,
|
|
15
|
-
killCommitRange,
|
|
16
|
-
killRangeFor,
|
|
17
|
-
matchesAnyTarget,
|
|
18
|
-
nonTeammatesVisible,
|
|
19
|
-
PROGRESS_INTERVAL_MS,
|
|
20
|
-
reportCorpseDecision,
|
|
21
|
-
SHRIMP_VISION_RANGE,
|
|
22
|
-
SHRIMP_VISION_RELEASE_RANGE,
|
|
23
|
-
taskMoveDecision,
|
|
24
|
-
} from '../game-utils.js';
|
|
25
|
-
import type { BehaviorDecision, StrategyContext } from '../types.js';
|
|
26
|
-
import { Goal } from './goal.js';
|
|
27
|
-
import { KEEP_AWAY_BASE_PLAN_OPTS, KeepAwayGoal, type KeepAwayGoalOptions } from './keep-away-goal.js';
|
|
28
|
-
import { KillVisibleTargetGoal } from './kill-target-goal.js';
|
|
29
|
-
import { SafeTaskOrPatrolGoal } from './safe-task-goal.js';
|
|
30
|
-
|
|
31
|
-
const SUB_PRIORITY = 0.5;
|
|
32
|
-
const STRANGER_TRIGGER_RADIUS = SHRIMP_VISION_RANGE;
|
|
33
|
-
const STRANGER_RELEASE_RADIUS = SHRIMP_VISION_RELEASE_RANGE;
|
|
34
|
-
const WITNESS_MEMORY_MS = 0;
|
|
35
|
-
const WITNESS_EXEMPT_COUNT = 2;
|
|
36
|
-
const TASK_RETURN_INERTIA_MS = 2_000;
|
|
37
|
-
const FLEE_KEY = 'warrior-flee';
|
|
38
|
-
const STRANGER_KEY = 'warrior-stranger-distance';
|
|
39
|
-
// 自卫总开关:false=遇到 suspect 一律只回避(绝不变向试探、绝不自卫出刀),等同纯回避档;true=启用下方绝境自卫流程。
|
|
40
|
-
const SELF_DEFENSE_ENABLED: boolean = false;
|
|
41
|
-
// 自卫出刀只能由「主动变向试探 + 验证对方跟随」达成,绝不靠被动转角/方位判定——那些会被走廊弯曲、寻路曲线、
|
|
42
|
-
// 贴身掠过污染(对方只是顺着走廊转、并非在追我,却累积出大转角)。流程:
|
|
43
|
-
// ① 同一 suspect 连续 JUKE_STREAK 帧「朝我贴身追击」(aimedPursuit:相对上帧位移 ≥ MOVE_EPS 且方向与「指向我」
|
|
44
|
-
// 夹角余弦 ≥ TOWARD_COS;cone 半角≈31.8°)且 planEscape 判定甩不掉 → 被咬住又逃不掉,开始试探;
|
|
45
|
-
// ② 主动横向变一次向(jukeTarget),变向幅度足够大,使「不跟随者」必然掉出 cone;
|
|
46
|
-
// ③ 变向后 JUKE_WINDOW 帧内,若它重新指向我的【新】位置(cone re-entry)= 跟随一次。只认变向【之后】的重新瞄准,
|
|
47
|
-
// 走廊弯曲转过的角度一概不算——直奔固定任务点 / 顺着走廊走的虾不会跟我的横向变向,永远掉出 cone、攒不到跟随。
|
|
48
|
-
// 变向后第 1 帧对方因反应延迟可能短暂掉出 cone,故用 WINDOW 容忍这一帧;窗口内始终不重新瞄准 = 没跟 → 重置。
|
|
49
|
-
// ④ 攒够 JUKE_FOLLOWS 次跟随、且已进 killRangeFor 射程、视线无墙,才出刀(趁 160 射程在合拢阶段先手,蟹只有 80)。
|
|
50
|
-
const SELF_DEFENSE_JUKE_STREAK = 2;
|
|
51
|
-
const SELF_DEFENSE_JUKE_WINDOW = 3;
|
|
52
|
-
const SELF_DEFENSE_JUKE_FOLLOWS = 2;
|
|
53
|
-
const SELF_DEFENSE_MOVE_EPS = 40;
|
|
54
|
-
const SELF_DEFENSE_TOWARD_COS = 0.85;
|
|
55
|
-
// 主动变向试探:横向位移目标距离;方向以横向为主、略带「背离对方」分量以免试探时丢距离;落点至少这么远才算有效变向。
|
|
56
|
-
const SELF_DEFENSE_JUKE_DIST = 170;
|
|
57
|
-
const SELF_DEFENSE_JUKE_LATERAL = 0.85;
|
|
58
|
-
const SELF_DEFENSE_JUKE_AWAY = 0.45;
|
|
59
|
-
const SELF_DEFENSE_JUKE_MIN_MOVE = 60;
|
|
60
|
-
|
|
61
|
-
type ProgressTier = 'flee-bad' | 'keep-distance' | 'task';
|
|
62
|
-
|
|
63
|
-
/** 单位向量;长度过小(重合)返回 null。 */
|
|
64
|
-
function unitVec(x: number, y: number): { x: number; y: number } | null {
|
|
65
|
-
const m = Math.hypot(x, y);
|
|
66
|
-
if (m < 1e-6) return null;
|
|
67
|
-
return { x: x / m, y: y / m };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** 两点世界坐标连线是否整段可走(无墙)。沿线段每半格采样一次,任一采样点落在墙上即判被挡。 */
|
|
71
|
-
function segmentWalkable(grid: WalkableGrid, ax: number, ay: number, bx: number, by: number): boolean {
|
|
72
|
-
const span = Math.hypot(bx - ax, by - ay);
|
|
73
|
-
const steps = Math.max(1, Math.ceil(span / (grid.tileSize * 0.5)));
|
|
74
|
-
for (let i = 0; i <= steps; i++) {
|
|
75
|
-
const t = i / steps;
|
|
76
|
-
const x = ax + (bx - ax) * t;
|
|
77
|
-
const y = ay + (by - ay) * t;
|
|
78
|
-
if (!grid.isWalkable(Math.floor(x / grid.tileSize), Math.floor(y / grid.tileSize))) return false;
|
|
79
|
-
}
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 带刀虾·记忆进阶版(武士虾/枪虾,带刀好人)。好人阵营但能出刀,按行为表的优先级逐条判断:
|
|
85
|
-
*
|
|
86
|
-
* - P0 发现尸体:旁边有非保护对象且刀可用 → 先出刀;否则报警(靠近至可报距离)。
|
|
87
|
-
* - P1 处理危险:明确猎杀目标(启动参数 / hostile)刀好就追杀;
|
|
88
|
-
* suspect 只表示可疑,默认回避,绝不仅凭怀疑或被动观察出刀——只有主动变向试探、验证它确实跟着我变向追击且退无可退才自卫先手;
|
|
89
|
-
* 无可信同伴且单个无标记陌生人贴近 → 按普通虾 B 档先拉开距离。
|
|
90
|
-
* - P2 紧急任务:无危险时优先处理。
|
|
91
|
-
* - P3 兜底:做任务 / 巡逻
|
|
92
|
-
* (SafeTaskOrPatrolGoal:测地最近、威胁旁/途经威胁的任务硬排除、带粘性)。
|
|
93
|
-
*
|
|
94
|
-
* 谁算「认定坏人」「可信同伴」来自 ctx.knowledge(见 warrior-memory.knowledge.md),
|
|
95
|
-
* 队友由游戏事实判定(nonTeammatesVisible),知识库只提供推断。
|
|
96
|
-
*/
|
|
97
|
-
export class WarriorShrimpTop extends Goal {
|
|
98
|
-
private readonly taskGoal = new SafeTaskOrPatrolGoal(
|
|
99
|
-
// 威胁点 = 当前会触发追杀/后撤的对象,或未被目击者豁免的无标记陌生人;保护对象不算。
|
|
100
|
-
(s, c) => nonTeammatesVisible(s, c)
|
|
101
|
-
.filter(p => !this.isTrusted(p, c))
|
|
102
|
-
.filter(p => this.isThreat(p, c) || !this.witnessExempt)
|
|
103
|
-
.map(p => ({ x: p.x, y: p.y })),
|
|
104
|
-
);
|
|
105
|
-
private readonly strangerSeenAt = new Map<string, number>();
|
|
106
|
-
private witnessExempt = false;
|
|
107
|
-
/** 自卫:当前被判定在追我的 suspect key、它上一帧坐标、连续「朝我贴身追击」帧数(决定何时开始试探)、
|
|
108
|
-
* 变向后等待对方跟随的剩余帧数(0=未在等)、发起变向那刻我的坐标(验证跟随用)、已验证的跟随次数、
|
|
109
|
-
* 本轮是否已推过自卫简报。 */
|
|
110
|
-
private selfDefenseTargetKey: string | null = null;
|
|
111
|
-
private selfDefenseSuspectPrev: { x: number; y: number } | null = null;
|
|
112
|
-
private selfDefenseStreak = 0;
|
|
113
|
-
private selfDefenseJukeWatch = 0;
|
|
114
|
-
private selfDefenseJukeFromPos: { x: number; y: number } | null = null;
|
|
115
|
-
private selfDefenseJukeFollows = 0;
|
|
116
|
-
private selfDefenseAlerted = false;
|
|
117
|
-
|
|
118
|
-
constructor(private readonly huntTargets: string[] = []) {
|
|
119
|
-
super();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** warrior-memory 开会后不重建 Top(resetOnMeetingResume:false);自卫追击计数按「连续游荡」语义,
|
|
123
|
-
* 开会是天然中断点,恢复后从零重算,避免拿会议前的追击 streak 一恢复就秒杀。 */
|
|
124
|
-
onMeetingResume(): void {
|
|
125
|
-
this.resetSelfDefense();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
129
|
-
const visible = nonTeammatesVisible(state, ctx);
|
|
130
|
-
this.updateWitnessMemory(visible, ctx);
|
|
131
|
-
const killReady = canUseKill(state);
|
|
132
|
-
|
|
133
|
-
// ── P0 发现尸体 ──
|
|
134
|
-
// 灭口贴脸目击者只在「此刻就在命案现场」(当帧视野有尸体)时考虑——记忆里某处有尸体不算现场,
|
|
135
|
-
// 否则会变成随便撞见一个人就灭口。
|
|
136
|
-
if (corpseAtScene(state) && killReady) {
|
|
137
|
-
const victim = this.nearestKillable(state, ctx, visible);
|
|
138
|
-
if (victim) {
|
|
139
|
-
this.clearSub();
|
|
140
|
-
return [{ action: Action.kill(victim.name) }];
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// 报尸 / 前往报尸:可以追着记忆里看见过、已离开视野的尸体走回去报告。
|
|
144
|
-
if (hasKnownCorpse(ctx)) {
|
|
145
|
-
const report = reportCorpseDecision(state, ctx, { respectBlock: true });
|
|
146
|
-
if (report) {
|
|
147
|
-
this.clearSub();
|
|
148
|
-
return [report];
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── P1-A hostile 刀好就追;suspect 只回避(避不开且能杀时自卫出刀) ──
|
|
153
|
-
const threats = visible.filter(p => this.isThreat(p, ctx));
|
|
154
|
-
if (threats.length > 0) {
|
|
155
|
-
const huntTargets = threats.filter(p => this.isHuntTarget(p, ctx));
|
|
156
|
-
if (killReady && huntTargets.length > 0) {
|
|
157
|
-
this.resetSelfDefense();
|
|
158
|
-
this.setHunt(this.nearestByDistance(state, huntTargets).name);
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
// 只剩 suspect、本应只回避;但若连续多帧确认同一个在朝我贴身追击且避让算法判定避不开,就主动横向变向试探,
|
|
162
|
-
// 只有它每次都跟着我的变向重新咬住(攒够跟随次数)、且已进 160 射程、视线无墙,才自卫先下手(趁 reach 优势在合拢阶段先手)+ Agent 自卫简报。
|
|
163
|
-
const selfDefense = this.selfDefenseKill(state, ctx, threats, killReady);
|
|
164
|
-
if (selfDefense) return selfDefense;
|
|
165
|
-
|
|
166
|
-
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
167
|
-
this.setFlee();
|
|
168
|
-
this.emitProgress(state, ctx, visible, 'flee-bad');
|
|
169
|
-
return [];
|
|
170
|
-
}
|
|
171
|
-
this.resetSelfDefense();
|
|
172
|
-
|
|
173
|
-
// ── P1-B 无可信同伴且无标记陌生人贴近 → 按普通虾 B 档拉开距离 ──
|
|
174
|
-
const trustedVisible = visible.some(p => this.isTrusted(p, ctx));
|
|
175
|
-
if (!trustedVisible) {
|
|
176
|
-
const worker = this.subGoal;
|
|
177
|
-
const strangerClose = visible.some(p =>
|
|
178
|
-
this.isUnmarkedStranger(p, ctx)
|
|
179
|
-
&& (p.distance ?? dist(p.x, p.y, state.you.x, state.you.y)) <= STRANGER_TRIGGER_RADIUS);
|
|
180
|
-
const stillBackingOff = worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY && !worker.isFinish(state, ctx);
|
|
181
|
-
if (!this.witnessExempt && (strangerClose || stillBackingOff)) {
|
|
182
|
-
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
183
|
-
this.setStrangerKeepAway();
|
|
184
|
-
this.emitProgress(state, ctx, visible, 'keep-distance');
|
|
185
|
-
return [];
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ── P2 紧急任务 ──
|
|
190
|
-
const emergency = firstAvailableTask([], t => t.faction !== 'crab', ctx.emergency, ctx.blockedMoveTarget);
|
|
191
|
-
if (emergency) {
|
|
192
|
-
this.clearSub();
|
|
193
|
-
return [taskMoveDecision(state, ctx, emergency)];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ── P3 普通任务 / 巡逻 ──
|
|
197
|
-
this.clearSub();
|
|
198
|
-
this.emitProgress(state, ctx, visible, 'task');
|
|
199
|
-
return this.taskGoal.tick(state, ctx);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** 目击者豁免:当前只按本轮可见玩家判断,见到 2 个以上陌生人时不对无标记者后撤。 */
|
|
203
|
-
private updateWitnessMemory(visible: PlayerInfo[], ctx: StrategyContext): void {
|
|
204
|
-
const now = Date.now();
|
|
205
|
-
for (const p of visible) {
|
|
206
|
-
if (!this.isUnmarkedStranger(p, ctx)) continue;
|
|
207
|
-
const key = p.name || String(p.seat ?? '');
|
|
208
|
-
if (key) this.strangerSeenAt.set(key, now);
|
|
209
|
-
}
|
|
210
|
-
for (const [key, at] of this.strangerSeenAt) {
|
|
211
|
-
if (now - at > WITNESS_MEMORY_MS) this.strangerSeenAt.delete(key);
|
|
212
|
-
}
|
|
213
|
-
this.witnessExempt = this.strangerSeenAt.size >= WITNESS_EXEMPT_COUNT;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private isUnmarkedStranger(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
217
|
-
return !this.isTrusted(p, ctx) && !this.isThreat(p, ctx);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** 知识里 trusted 的玩家:可信同伴,永不击杀。 */
|
|
221
|
-
private isTrusted(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
222
|
-
return isKnowledgeTrusted(p, ctx);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** 明确猎杀目标:启动参数或 hostile;suspect 不在这里。 */
|
|
226
|
-
private isHuntTarget(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
227
|
-
return !this.isTrusted(p, ctx)
|
|
228
|
-
&& (matchesAnyTarget(p, this.huntTargets, ctx) || isKnowledgeHostile(p, ctx));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** 威胁:明确猎杀目标,或知识库 suspect 标记;trusted 永远排除。 */
|
|
232
|
-
private isThreat(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
233
|
-
return !this.isTrusted(p, ctx)
|
|
234
|
-
&& (this.isHuntTarget(p, ctx) || isKnowledgeThreat(p, ctx));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** 尸体场景下「认定坏人/身份不明」= 视野内最近的非保护非队友,且在出刀范围内。 */
|
|
238
|
-
private nearestKillable(state: GameState, ctx: StrategyContext, visible: PlayerInfo[]): PlayerInfo | null {
|
|
239
|
-
return visible
|
|
240
|
-
.filter(p => !this.isTrusted(p, ctx))
|
|
241
|
-
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
242
|
-
.filter(x => x.d <= killCommitRange(state.you.role))
|
|
243
|
-
.sort((a, b) => a.d - b.d)[0]?.p ?? null;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private nearestByDistance(state: GameState, players: PlayerInfo[]): PlayerInfo {
|
|
247
|
-
return players
|
|
248
|
-
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
249
|
-
.sort((a, b) => a.d - b.d)[0].p;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* 自卫击杀 / 变向试探:只处理「只剩 suspect、本应只回避」的情形(hostile 在上面已优先追杀)。出刀只能由
|
|
254
|
-
* 「主动变向 + 验证对方跟随」达成,绝不靠被动转角/方位,避免「对方只是顺着走廊转、并非在追我」被误判。
|
|
255
|
-
* 每帧:
|
|
256
|
-
* ① aimedPursuit 判定本帧是否「朝我贴身追击」,维护连续帧数(变向等待期内的一帧不朝我是预期的反应延迟,先容忍)。
|
|
257
|
-
* ② 若处在变向等待期:它重新指向我的新位置 = 跟随一次(攒一次);窗口内始终不重新瞄准 = 没跟 → 重置。
|
|
258
|
-
* ③ 攒够 SELF_DEFENSE_JUKE_FOLLOWS 次跟随、已进 killRangeFor 射程、planEscape 判定甩不掉、视线无墙 → 出刀,
|
|
259
|
-
* 并给 Agent 推一条「被动自卫」简报(区别于主动猎杀,免得之后发言/投票露馅)。
|
|
260
|
-
* ④ 否则若已被贴身追够 JUKE_STREAK 帧、甩不掉、且不在等待期 → 主动横向变一次向逼它暴露(窄道无处可变才交回常规逃跑)。
|
|
261
|
-
*
|
|
262
|
-
* 趁 reach 优势:武士虾出刀范围(160)远大于蟹(80),确认在追后在合拢阶段先手,而不是等贴脸拼手速。出刀确认前
|
|
263
|
-
* 不清零追击状态——空刀(极端情况)下进度不该白丢;真杀掉后对方消失,下一帧威胁清空自然重置。
|
|
264
|
-
*/
|
|
265
|
-
private selfDefenseKill(
|
|
266
|
-
state: GameState,
|
|
267
|
-
ctx: StrategyContext,
|
|
268
|
-
threats: PlayerInfo[],
|
|
269
|
-
killReady: boolean,
|
|
270
|
-
): BehaviorDecision[] | null {
|
|
271
|
-
if (!SELF_DEFENSE_ENABLED) return null; // 开关关闭:直接回退到「只回避」,不进入任何试探/出刀逻辑。
|
|
272
|
-
const suspects = threats.filter(p => !this.isHuntTarget(p, ctx));
|
|
273
|
-
if (suspects.length === 0) {
|
|
274
|
-
this.resetSelfDefense();
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
const nearest = this.nearestByDistance(state, suspects);
|
|
278
|
-
const aimed = this.aimedPursuit(state, nearest);
|
|
279
|
-
|
|
280
|
-
if (!killReady) return null;
|
|
281
|
-
|
|
282
|
-
// 维护「朝我贴身追击」连续帧数(决定何时开始试探)。某帧不再朝我 = 追击中断,清零进度但【保留当前目标身份】
|
|
283
|
-
// 继续跟它(softResetPursuit,不是 resetSelfDefense——后者会把目标也丢掉、下一帧又当「第一次见到」永远攒不起来)。
|
|
284
|
-
// 例外:变向等待期内的一帧不朝我是预期的反应延迟,先容忍不清零。
|
|
285
|
-
if (aimed) {
|
|
286
|
-
this.selfDefenseStreak++;
|
|
287
|
-
} else if (this.selfDefenseJukeWatch === 0) {
|
|
288
|
-
this.softResetPursuit();
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// 变向等待期:它重新指向我的【新】位置 = 跟随一次。但必须等我相对变向起点真的横向拉开够远
|
|
293
|
-
// (jukeFollowed:站在对方位置看,「我变向前」与「我现在」已分得比 cone 还开)才算数——否则我变向移动还没生效、
|
|
294
|
-
// 或位移太小直线仍落在 cone 内时,会把「持续朝旧/当前位置走」误判成「响应我变向重新咬住」。窗口内始终凑不出
|
|
295
|
-
// 一次有效跟随 = 没跟(不是在追我)→ 清零进度重新观察。
|
|
296
|
-
if (this.selfDefenseJukeWatch > 0) {
|
|
297
|
-
if (aimed && this.jukeFollowed(state, nearest)) {
|
|
298
|
-
this.selfDefenseJukeFollows++;
|
|
299
|
-
this.selfDefenseJukeWatch = 0;
|
|
300
|
-
} else if (--this.selfDefenseJukeWatch === 0) {
|
|
301
|
-
this.softResetPursuit();
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const hopeless = this.escapeHopeless(state, threats);
|
|
307
|
-
const d = nearest.distance ?? dist(state.you.x, state.you.y, nearest.x, nearest.y);
|
|
308
|
-
if (
|
|
309
|
-
this.selfDefenseJukeFollows >= SELF_DEFENSE_JUKE_FOLLOWS
|
|
310
|
-
&& d <= killRangeFor(state.you.role)
|
|
311
|
-
&& hopeless
|
|
312
|
-
&& this.hasShotLine(state, nearest)
|
|
313
|
-
) {
|
|
314
|
-
this.clearSub();
|
|
315
|
-
if (!this.selfDefenseAlerted) {
|
|
316
|
-
this.selfDefenseAlerted = true;
|
|
317
|
-
ctx.notifications.push(`连续确认被${nearest.name}贴身追击、变向试探它每次都跟着咬住、退无可退,自卫先手出刀。`);
|
|
318
|
-
ctx.agentAlerts.push(
|
|
319
|
-
`我被${nearest.name}持续贴身追击、几次变向试探它都跟着甩不掉、已被逼到退无可退,刚出于自卫先下手把对方击杀了。`
|
|
320
|
-
+ `这是被动自卫不是主动猎杀:接下来发言/投票按「对方一直追着我跑、我被逼到死角才自保」的口径走,别承认无故先手。`,
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
return [{ action: Action.kill(nearest.name) }];
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// 还没攒够跟随:被贴身追够、甩不掉、且不在等待期时,主动横向变一次向逼它暴露。无处可变(窄道)才交回常规逃跑。
|
|
327
|
-
if (this.selfDefenseStreak >= SELF_DEFENSE_JUKE_STREAK && this.selfDefenseJukeWatch === 0 && hopeless) {
|
|
328
|
-
const juke = this.jukeTarget(state, nearest);
|
|
329
|
-
if (juke) {
|
|
330
|
-
this.selfDefenseJukeWatch = SELF_DEFENSE_JUKE_WINDOW;
|
|
331
|
-
this.selfDefenseJukeFromPos = { x: state.you.x, y: state.you.y };
|
|
332
|
-
this.clearSub();
|
|
333
|
-
return [{ action: Action.move(juke).withThinking('换个方向甩开') }];
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* 变向跟随是否成立:站在 suspect 当前位置看,「我发起变向那刻的位置」与「我现在的位置」的夹角已超出 cone
|
|
341
|
-
* (半角≈31.8°)。成立才说明我确实横向拉开够远——此时它仍朝我(aimed)= 它跟着我的变向重新瞄准了;
|
|
342
|
-
* 直奔我变向前位置/顺着走廊直走的虾此刻必然落在 cone 外、aimed 不成立,不会被算作跟随。
|
|
343
|
-
*/
|
|
344
|
-
private jukeFollowed(state: GameState, suspect: PlayerInfo): boolean {
|
|
345
|
-
const from = this.selfDefenseJukeFromPos;
|
|
346
|
-
if (!from) return false;
|
|
347
|
-
const toFrom = unitVec(from.x - suspect.x, from.y - suspect.y);
|
|
348
|
-
const toNow = unitVec(state.you.x - suspect.x, state.you.y - suspect.y);
|
|
349
|
-
if (!toFrom || !toNow) return false;
|
|
350
|
-
return toFrom.x * toNow.x + toFrom.y * toNow.y < SELF_DEFENSE_TOWARD_COS;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* 本帧同一 suspect 是否「朝我贴身追击」:相对上一帧位移 ≥ MOVE_EPS(确实在动,排除站桩做任务)且位移方向
|
|
355
|
-
* 与「指向我当前位置」夹角余弦 ≥ TOWARD_COS(朝我来;cone 半角≈31.8°)。换目标时整体重置并返回 false。
|
|
356
|
-
* 只读对方位移与「指向我」的关系,参考点一致;不累计任何转角(转角会被走廊弯曲污染,确认改由变向跟随负责)。
|
|
357
|
-
*/
|
|
358
|
-
private aimedPursuit(state: GameState, nearest: PlayerInfo): boolean {
|
|
359
|
-
const key = nearest.name || String(nearest.seat ?? '');
|
|
360
|
-
const curS = { x: nearest.x, y: nearest.y };
|
|
361
|
-
if (key !== this.selfDefenseTargetKey) {
|
|
362
|
-
this.resetSelfDefense();
|
|
363
|
-
this.selfDefenseTargetKey = key;
|
|
364
|
-
this.selfDefenseSuspectPrev = curS;
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
const sPrev = this.selfDefenseSuspectPrev;
|
|
368
|
-
this.selfDefenseSuspectPrev = curS;
|
|
369
|
-
if (!sPrev) return false;
|
|
370
|
-
|
|
371
|
-
const heading = unitVec(curS.x - sPrev.x, curS.y - sPrev.y);
|
|
372
|
-
const aim = unitVec(state.you.x - sPrev.x, state.you.y - sPrev.y);
|
|
373
|
-
const movedFar = dist(sPrev.x, sPrev.y, curS.x, curS.y) >= SELF_DEFENSE_MOVE_EPS;
|
|
374
|
-
return !!heading && !!aim && movedFar
|
|
375
|
-
&& heading.x * aim.x + heading.y * aim.y >= SELF_DEFENSE_TOWARD_COS;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/** planEscape 判定每条逃跑分支都会在推演内被追上(避不开);用与 KeepAwayGoal 一致的基线参数。 */
|
|
379
|
-
private escapeHopeless(state: GameState, threats: PlayerInfo[]): boolean {
|
|
380
|
-
const threatPoints = threats.map(p => ({ x: p.x, y: p.y }));
|
|
381
|
-
return planEscape(state.you, threatPoints, KEEP_AWAY_BASE_PLAN_OPTS).caught;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** 我到目标的连线是否无墙——确认了也别对着墙空刀(target_blocked_by_wall)。 */
|
|
385
|
-
private hasShotLine(state: GameState, target: PlayerInfo): boolean {
|
|
386
|
-
return segmentWalkable(loadWalkableGrid(), state.you.x, state.you.y, target.x, target.y);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* 主动变向试探的落点:垂直于「背离 suspect」轴的横向点(略带背离分量以免丢距离),取两侧中离 suspect 更远、
|
|
391
|
-
* 且我到落点整段无墙的一侧。两侧都不可走 / 反而明显贴近 suspect(窄道、贴墙)时返回 null,交回常规逃跑。
|
|
392
|
-
*/
|
|
393
|
-
private jukeTarget(state: GameState, suspect: PlayerInfo): Position | null {
|
|
394
|
-
const grid = loadWalkableGrid();
|
|
395
|
-
const me = state.you;
|
|
396
|
-
const curSep = dist(me.x, me.y, suspect.x, suspect.y);
|
|
397
|
-
const away = unitVec(me.x - suspect.x, me.y - suspect.y) ?? { x: 1, y: 0 };
|
|
398
|
-
const sides = [{ x: -away.y, y: away.x }, { x: away.y, y: -away.x }];
|
|
399
|
-
let best: { target: Position; sep: number } | null = null;
|
|
400
|
-
for (const perp of sides) {
|
|
401
|
-
const dir = unitVec(
|
|
402
|
-
perp.x * SELF_DEFENSE_JUKE_LATERAL + away.x * SELF_DEFENSE_JUKE_AWAY,
|
|
403
|
-
perp.y * SELF_DEFENSE_JUKE_LATERAL + away.y * SELF_DEFENSE_JUKE_AWAY,
|
|
404
|
-
);
|
|
405
|
-
if (!dir) continue;
|
|
406
|
-
const raw = { x: me.x + dir.x * SELF_DEFENSE_JUKE_DIST, y: me.y + dir.y * SELF_DEFENSE_JUKE_DIST };
|
|
407
|
-
const cell = grid.snapToWalkable(grid.worldToCell(raw.x, raw.y), 6);
|
|
408
|
-
if (cell < 0) continue;
|
|
409
|
-
const target = grid.cellToWorld(cell);
|
|
410
|
-
if (dist(me.x, me.y, target.x, target.y) < SELF_DEFENSE_JUKE_MIN_MOVE) continue;
|
|
411
|
-
if (!segmentWalkable(grid, me.x, me.y, target.x, target.y)) continue;
|
|
412
|
-
const sep = dist(target.x, target.y, suspect.x, suspect.y);
|
|
413
|
-
if (!best || sep > best.sep) best = { target, sep };
|
|
414
|
-
}
|
|
415
|
-
// 不往 suspect 身上凑:最优侧也比当前明显更近就放弃变向(交回常规逃跑)。
|
|
416
|
-
if (best && best.sep < curSep - SELF_DEFENSE_JUKE_MIN_MOVE) return null;
|
|
417
|
-
return best?.target ?? null;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/** 追击中断(某帧不再朝我 / 变向窗口内没凑出有效跟随):清零追击进度,但【保留当前目标身份与上一帧坐标】,
|
|
421
|
-
* 它若重新咬住可继续累计——区别于 resetSelfDefense 连目标一起丢(那样下一帧又当「第一次见到」永远攒不起来)。 */
|
|
422
|
-
private softResetPursuit(): void {
|
|
423
|
-
this.selfDefenseStreak = 0;
|
|
424
|
-
this.selfDefenseJukeWatch = 0;
|
|
425
|
-
this.selfDefenseJukeFromPos = null;
|
|
426
|
-
this.selfDefenseJukeFollows = 0;
|
|
427
|
-
this.selfDefenseAlerted = false;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/** 整体重置:连当前追击目标身份一起丢(换目标、目标消失、开会、转入主动猎杀时用)。 */
|
|
431
|
-
private resetSelfDefense(): void {
|
|
432
|
-
this.selfDefenseTargetKey = null;
|
|
433
|
-
this.selfDefenseSuspectPrev = null;
|
|
434
|
-
this.softResetPursuit();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
private setHunt(targetName: string): void {
|
|
438
|
-
const worker = this.subGoal;
|
|
439
|
-
if (worker instanceof KillVisibleTargetGoal && worker.targetName === targetName) return;
|
|
440
|
-
if (this.subGoal) this.removeSubGoal();
|
|
441
|
-
this.setSubGoal(new KillVisibleTargetGoal(targetName, [], true), SUB_PRIORITY);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private setFlee(): void {
|
|
445
|
-
const worker = this.subGoal;
|
|
446
|
-
if (worker instanceof KeepAwayGoal && worker.key === FLEE_KEY) return;
|
|
447
|
-
if (this.subGoal) this.removeSubGoal();
|
|
448
|
-
this.setSubGoal(new KeepAwayGoal(
|
|
449
|
-
FLEE_KEY,
|
|
450
|
-
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isThreat(p, c)),
|
|
451
|
-
{ threatRadius: Infinity, noun: '认定坏人' } satisfies KeepAwayGoalOptions,
|
|
452
|
-
), SUB_PRIORITY);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
private setStrangerKeepAway(): void {
|
|
456
|
-
const worker = this.subGoal;
|
|
457
|
-
if (worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY) return;
|
|
458
|
-
if (this.subGoal) this.removeSubGoal();
|
|
459
|
-
this.setSubGoal(new KeepAwayGoal(
|
|
460
|
-
STRANGER_KEY,
|
|
461
|
-
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isUnmarkedStranger(p, c)),
|
|
462
|
-
{
|
|
463
|
-
threatRadius: STRANGER_RELEASE_RADIUS,
|
|
464
|
-
noun: '陌生人',
|
|
465
|
-
planOpts: { steps: 4 },
|
|
466
|
-
} satisfies KeepAwayGoalOptions,
|
|
467
|
-
), SUB_PRIORITY);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
private clearSub(): void {
|
|
471
|
-
if (this.subGoal) this.removeSubGoal();
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private emitProgress(state: GameState, ctx: StrategyContext, visible: PlayerInfo[], tier: ProgressTier): void {
|
|
475
|
-
const now = Date.now();
|
|
476
|
-
if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
|
|
477
|
-
ctx.lastProgressNotifyAt = now;
|
|
478
|
-
|
|
479
|
-
const room = state.you.room ?? '未知';
|
|
480
|
-
const cd = killCooldownSecs(state);
|
|
481
|
-
const killsRemaining = state.you.kills_remaining;
|
|
482
|
-
|
|
483
|
-
let msg = `[进度] 当前在${room}`;
|
|
484
|
-
if (visible.length === 0) {
|
|
485
|
-
msg += ',附近无人,做任务/巡逻。';
|
|
486
|
-
} else if (tier === 'keep-distance') {
|
|
487
|
-
msg += ',无标记陌生人靠得太近,先拉开距离再做任务。';
|
|
488
|
-
} else {
|
|
489
|
-
const bad = visible.filter(p => this.isThreat(p, ctx)).map(p => p.name);
|
|
490
|
-
if (bad.length > 0) msg += `,发现认定坏人${bad.join('、')}`;
|
|
491
|
-
else if (this.witnessExempt) msg += `,附近有${visible.length}个无标记陌生人互为目击者,正常做任务`;
|
|
492
|
-
else msg += `,附近有${visible.length}人,正常做任务`;
|
|
493
|
-
if (tier === 'flee-bad') msg += killCooldownSecs(state) > 0 ? ',刀在冷却,正在回避' : ',正在处理';
|
|
494
|
-
if (cd > 0) msg += `(攻击冷却${cd}s)`;
|
|
495
|
-
if (killsRemaining === 0) msg += '(出刀次数已用完)';
|
|
496
|
-
msg += '。';
|
|
497
|
-
}
|
|
498
|
-
ctx.notifications.push(msg);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
1
|
+
import type { GameState, PlayerInfo, Position } from '../../sdk/types.js';
|
|
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
|
+
import {
|
|
6
|
+
canUseKill,
|
|
7
|
+
corpseAtScene,
|
|
8
|
+
dist,
|
|
9
|
+
firstAvailableTask,
|
|
10
|
+
hasKnownCorpse,
|
|
11
|
+
isKnowledgeHostile,
|
|
12
|
+
isKnowledgeThreat,
|
|
13
|
+
isKnowledgeTrusted,
|
|
14
|
+
killCooldownSecs,
|
|
15
|
+
killCommitRange,
|
|
16
|
+
killRangeFor,
|
|
17
|
+
matchesAnyTarget,
|
|
18
|
+
nonTeammatesVisible,
|
|
19
|
+
PROGRESS_INTERVAL_MS,
|
|
20
|
+
reportCorpseDecision,
|
|
21
|
+
SHRIMP_VISION_RANGE,
|
|
22
|
+
SHRIMP_VISION_RELEASE_RANGE,
|
|
23
|
+
taskMoveDecision,
|
|
24
|
+
} from '../game-utils.js';
|
|
25
|
+
import type { BehaviorDecision, StrategyContext } from '../types.js';
|
|
26
|
+
import { Goal } from './goal.js';
|
|
27
|
+
import { KEEP_AWAY_BASE_PLAN_OPTS, KeepAwayGoal, type KeepAwayGoalOptions } from './keep-away-goal.js';
|
|
28
|
+
import { KillVisibleTargetGoal } from './kill-target-goal.js';
|
|
29
|
+
import { SafeTaskOrPatrolGoal } from './safe-task-goal.js';
|
|
30
|
+
|
|
31
|
+
const SUB_PRIORITY = 0.5;
|
|
32
|
+
const STRANGER_TRIGGER_RADIUS = SHRIMP_VISION_RANGE;
|
|
33
|
+
const STRANGER_RELEASE_RADIUS = SHRIMP_VISION_RELEASE_RANGE;
|
|
34
|
+
const WITNESS_MEMORY_MS = 0;
|
|
35
|
+
const WITNESS_EXEMPT_COUNT = 2;
|
|
36
|
+
const TASK_RETURN_INERTIA_MS = 2_000;
|
|
37
|
+
const FLEE_KEY = 'warrior-flee';
|
|
38
|
+
const STRANGER_KEY = 'warrior-stranger-distance';
|
|
39
|
+
// 自卫总开关:false=遇到 suspect 一律只回避(绝不变向试探、绝不自卫出刀),等同纯回避档;true=启用下方绝境自卫流程。
|
|
40
|
+
const SELF_DEFENSE_ENABLED: boolean = false;
|
|
41
|
+
// 自卫出刀只能由「主动变向试探 + 验证对方跟随」达成,绝不靠被动转角/方位判定——那些会被走廊弯曲、寻路曲线、
|
|
42
|
+
// 贴身掠过污染(对方只是顺着走廊转、并非在追我,却累积出大转角)。流程:
|
|
43
|
+
// ① 同一 suspect 连续 JUKE_STREAK 帧「朝我贴身追击」(aimedPursuit:相对上帧位移 ≥ MOVE_EPS 且方向与「指向我」
|
|
44
|
+
// 夹角余弦 ≥ TOWARD_COS;cone 半角≈31.8°)且 planEscape 判定甩不掉 → 被咬住又逃不掉,开始试探;
|
|
45
|
+
// ② 主动横向变一次向(jukeTarget),变向幅度足够大,使「不跟随者」必然掉出 cone;
|
|
46
|
+
// ③ 变向后 JUKE_WINDOW 帧内,若它重新指向我的【新】位置(cone re-entry)= 跟随一次。只认变向【之后】的重新瞄准,
|
|
47
|
+
// 走廊弯曲转过的角度一概不算——直奔固定任务点 / 顺着走廊走的虾不会跟我的横向变向,永远掉出 cone、攒不到跟随。
|
|
48
|
+
// 变向后第 1 帧对方因反应延迟可能短暂掉出 cone,故用 WINDOW 容忍这一帧;窗口内始终不重新瞄准 = 没跟 → 重置。
|
|
49
|
+
// ④ 攒够 JUKE_FOLLOWS 次跟随、且已进 killRangeFor 射程、视线无墙,才出刀(趁 160 射程在合拢阶段先手,蟹只有 80)。
|
|
50
|
+
const SELF_DEFENSE_JUKE_STREAK = 2;
|
|
51
|
+
const SELF_DEFENSE_JUKE_WINDOW = 3;
|
|
52
|
+
const SELF_DEFENSE_JUKE_FOLLOWS = 2;
|
|
53
|
+
const SELF_DEFENSE_MOVE_EPS = 40;
|
|
54
|
+
const SELF_DEFENSE_TOWARD_COS = 0.85;
|
|
55
|
+
// 主动变向试探:横向位移目标距离;方向以横向为主、略带「背离对方」分量以免试探时丢距离;落点至少这么远才算有效变向。
|
|
56
|
+
const SELF_DEFENSE_JUKE_DIST = 170;
|
|
57
|
+
const SELF_DEFENSE_JUKE_LATERAL = 0.85;
|
|
58
|
+
const SELF_DEFENSE_JUKE_AWAY = 0.45;
|
|
59
|
+
const SELF_DEFENSE_JUKE_MIN_MOVE = 60;
|
|
60
|
+
|
|
61
|
+
type ProgressTier = 'flee-bad' | 'keep-distance' | 'task';
|
|
62
|
+
|
|
63
|
+
/** 单位向量;长度过小(重合)返回 null。 */
|
|
64
|
+
function unitVec(x: number, y: number): { x: number; y: number } | null {
|
|
65
|
+
const m = Math.hypot(x, y);
|
|
66
|
+
if (m < 1e-6) return null;
|
|
67
|
+
return { x: x / m, y: y / m };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 两点世界坐标连线是否整段可走(无墙)。沿线段每半格采样一次,任一采样点落在墙上即判被挡。 */
|
|
71
|
+
function segmentWalkable(grid: WalkableGrid, ax: number, ay: number, bx: number, by: number): boolean {
|
|
72
|
+
const span = Math.hypot(bx - ax, by - ay);
|
|
73
|
+
const steps = Math.max(1, Math.ceil(span / (grid.tileSize * 0.5)));
|
|
74
|
+
for (let i = 0; i <= steps; i++) {
|
|
75
|
+
const t = i / steps;
|
|
76
|
+
const x = ax + (bx - ax) * t;
|
|
77
|
+
const y = ay + (by - ay) * t;
|
|
78
|
+
if (!grid.isWalkable(Math.floor(x / grid.tileSize), Math.floor(y / grid.tileSize))) return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 带刀虾·记忆进阶版(武士虾/枪虾,带刀好人)。好人阵营但能出刀,按行为表的优先级逐条判断:
|
|
85
|
+
*
|
|
86
|
+
* - P0 发现尸体:旁边有非保护对象且刀可用 → 先出刀;否则报警(靠近至可报距离)。
|
|
87
|
+
* - P1 处理危险:明确猎杀目标(启动参数 / hostile)刀好就追杀;
|
|
88
|
+
* suspect 只表示可疑,默认回避,绝不仅凭怀疑或被动观察出刀——只有主动变向试探、验证它确实跟着我变向追击且退无可退才自卫先手;
|
|
89
|
+
* 无可信同伴且单个无标记陌生人贴近 → 按普通虾 B 档先拉开距离。
|
|
90
|
+
* - P2 紧急任务:无危险时优先处理。
|
|
91
|
+
* - P3 兜底:做任务 / 巡逻
|
|
92
|
+
* (SafeTaskOrPatrolGoal:测地最近、威胁旁/途经威胁的任务硬排除、带粘性)。
|
|
93
|
+
*
|
|
94
|
+
* 谁算「认定坏人」「可信同伴」来自 ctx.knowledge(见 warrior-memory.knowledge.md),
|
|
95
|
+
* 队友由游戏事实判定(nonTeammatesVisible),知识库只提供推断。
|
|
96
|
+
*/
|
|
97
|
+
export class WarriorShrimpTop extends Goal {
|
|
98
|
+
private readonly taskGoal = new SafeTaskOrPatrolGoal(
|
|
99
|
+
// 威胁点 = 当前会触发追杀/后撤的对象,或未被目击者豁免的无标记陌生人;保护对象不算。
|
|
100
|
+
(s, c) => nonTeammatesVisible(s, c)
|
|
101
|
+
.filter(p => !this.isTrusted(p, c))
|
|
102
|
+
.filter(p => this.isThreat(p, c) || !this.witnessExempt)
|
|
103
|
+
.map(p => ({ x: p.x, y: p.y })),
|
|
104
|
+
);
|
|
105
|
+
private readonly strangerSeenAt = new Map<string, number>();
|
|
106
|
+
private witnessExempt = false;
|
|
107
|
+
/** 自卫:当前被判定在追我的 suspect key、它上一帧坐标、连续「朝我贴身追击」帧数(决定何时开始试探)、
|
|
108
|
+
* 变向后等待对方跟随的剩余帧数(0=未在等)、发起变向那刻我的坐标(验证跟随用)、已验证的跟随次数、
|
|
109
|
+
* 本轮是否已推过自卫简报。 */
|
|
110
|
+
private selfDefenseTargetKey: string | null = null;
|
|
111
|
+
private selfDefenseSuspectPrev: { x: number; y: number } | null = null;
|
|
112
|
+
private selfDefenseStreak = 0;
|
|
113
|
+
private selfDefenseJukeWatch = 0;
|
|
114
|
+
private selfDefenseJukeFromPos: { x: number; y: number } | null = null;
|
|
115
|
+
private selfDefenseJukeFollows = 0;
|
|
116
|
+
private selfDefenseAlerted = false;
|
|
117
|
+
|
|
118
|
+
constructor(private readonly huntTargets: string[] = []) {
|
|
119
|
+
super();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** warrior-memory 开会后不重建 Top(resetOnMeetingResume:false);自卫追击计数按「连续游荡」语义,
|
|
123
|
+
* 开会是天然中断点,恢复后从零重算,避免拿会议前的追击 streak 一恢复就秒杀。 */
|
|
124
|
+
onMeetingResume(): void {
|
|
125
|
+
this.resetSelfDefense();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
129
|
+
const visible = nonTeammatesVisible(state, ctx);
|
|
130
|
+
this.updateWitnessMemory(visible, ctx);
|
|
131
|
+
const killReady = canUseKill(state);
|
|
132
|
+
|
|
133
|
+
// ── P0 发现尸体 ──
|
|
134
|
+
// 灭口贴脸目击者只在「此刻就在命案现场」(当帧视野有尸体)时考虑——记忆里某处有尸体不算现场,
|
|
135
|
+
// 否则会变成随便撞见一个人就灭口。
|
|
136
|
+
if (corpseAtScene(state) && killReady) {
|
|
137
|
+
const victim = this.nearestKillable(state, ctx, visible);
|
|
138
|
+
if (victim) {
|
|
139
|
+
this.clearSub();
|
|
140
|
+
return [{ action: Action.kill(victim.name) }];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 报尸 / 前往报尸:可以追着记忆里看见过、已离开视野的尸体走回去报告。
|
|
144
|
+
if (hasKnownCorpse(ctx)) {
|
|
145
|
+
const report = reportCorpseDecision(state, ctx, { respectBlock: true });
|
|
146
|
+
if (report) {
|
|
147
|
+
this.clearSub();
|
|
148
|
+
return [report];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── P1-A hostile 刀好就追;suspect 只回避(避不开且能杀时自卫出刀) ──
|
|
153
|
+
const threats = visible.filter(p => this.isThreat(p, ctx));
|
|
154
|
+
if (threats.length > 0) {
|
|
155
|
+
const huntTargets = threats.filter(p => this.isHuntTarget(p, ctx));
|
|
156
|
+
if (killReady && huntTargets.length > 0) {
|
|
157
|
+
this.resetSelfDefense();
|
|
158
|
+
this.setHunt(this.nearestByDistance(state, huntTargets).name);
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
// 只剩 suspect、本应只回避;但若连续多帧确认同一个在朝我贴身追击且避让算法判定避不开,就主动横向变向试探,
|
|
162
|
+
// 只有它每次都跟着我的变向重新咬住(攒够跟随次数)、且已进 160 射程、视线无墙,才自卫先下手(趁 reach 优势在合拢阶段先手)+ Agent 自卫简报。
|
|
163
|
+
const selfDefense = this.selfDefenseKill(state, ctx, threats, killReady);
|
|
164
|
+
if (selfDefense) return selfDefense;
|
|
165
|
+
|
|
166
|
+
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
167
|
+
this.setFlee();
|
|
168
|
+
this.emitProgress(state, ctx, visible, 'flee-bad');
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
this.resetSelfDefense();
|
|
172
|
+
|
|
173
|
+
// ── P1-B 无可信同伴且无标记陌生人贴近 → 按普通虾 B 档拉开距离 ──
|
|
174
|
+
const trustedVisible = visible.some(p => this.isTrusted(p, ctx));
|
|
175
|
+
if (!trustedVisible) {
|
|
176
|
+
const worker = this.subGoal;
|
|
177
|
+
const strangerClose = visible.some(p =>
|
|
178
|
+
this.isUnmarkedStranger(p, ctx)
|
|
179
|
+
&& (p.distance ?? dist(p.x, p.y, state.you.x, state.you.y)) <= STRANGER_TRIGGER_RADIUS);
|
|
180
|
+
const stillBackingOff = worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY && !worker.isFinish(state, ctx);
|
|
181
|
+
if (!this.witnessExempt && (strangerClose || stillBackingOff)) {
|
|
182
|
+
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
183
|
+
this.setStrangerKeepAway();
|
|
184
|
+
this.emitProgress(state, ctx, visible, 'keep-distance');
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── P2 紧急任务 ──
|
|
190
|
+
const emergency = firstAvailableTask([], t => t.faction !== 'crab', ctx.emergency, ctx.blockedMoveTarget);
|
|
191
|
+
if (emergency) {
|
|
192
|
+
this.clearSub();
|
|
193
|
+
return [taskMoveDecision(state, ctx, emergency)];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── P3 普通任务 / 巡逻 ──
|
|
197
|
+
this.clearSub();
|
|
198
|
+
this.emitProgress(state, ctx, visible, 'task');
|
|
199
|
+
return this.taskGoal.tick(state, ctx);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 目击者豁免:当前只按本轮可见玩家判断,见到 2 个以上陌生人时不对无标记者后撤。 */
|
|
203
|
+
private updateWitnessMemory(visible: PlayerInfo[], ctx: StrategyContext): void {
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
for (const p of visible) {
|
|
206
|
+
if (!this.isUnmarkedStranger(p, ctx)) continue;
|
|
207
|
+
const key = p.name || String(p.seat ?? '');
|
|
208
|
+
if (key) this.strangerSeenAt.set(key, now);
|
|
209
|
+
}
|
|
210
|
+
for (const [key, at] of this.strangerSeenAt) {
|
|
211
|
+
if (now - at > WITNESS_MEMORY_MS) this.strangerSeenAt.delete(key);
|
|
212
|
+
}
|
|
213
|
+
this.witnessExempt = this.strangerSeenAt.size >= WITNESS_EXEMPT_COUNT;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private isUnmarkedStranger(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
217
|
+
return !this.isTrusted(p, ctx) && !this.isThreat(p, ctx);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 知识里 trusted 的玩家:可信同伴,永不击杀。 */
|
|
221
|
+
private isTrusted(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
222
|
+
return isKnowledgeTrusted(p, ctx);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 明确猎杀目标:启动参数或 hostile;suspect 不在这里。 */
|
|
226
|
+
private isHuntTarget(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
227
|
+
return !this.isTrusted(p, ctx)
|
|
228
|
+
&& (matchesAnyTarget(p, this.huntTargets, ctx) || isKnowledgeHostile(p, ctx));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** 威胁:明确猎杀目标,或知识库 suspect 标记;trusted 永远排除。 */
|
|
232
|
+
private isThreat(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
233
|
+
return !this.isTrusted(p, ctx)
|
|
234
|
+
&& (this.isHuntTarget(p, ctx) || isKnowledgeThreat(p, ctx));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 尸体场景下「认定坏人/身份不明」= 视野内最近的非保护非队友,且在出刀范围内。 */
|
|
238
|
+
private nearestKillable(state: GameState, ctx: StrategyContext, visible: PlayerInfo[]): PlayerInfo | null {
|
|
239
|
+
return visible
|
|
240
|
+
.filter(p => !this.isTrusted(p, ctx))
|
|
241
|
+
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
242
|
+
.filter(x => x.d <= killCommitRange(state.you.role))
|
|
243
|
+
.sort((a, b) => a.d - b.d)[0]?.p ?? null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private nearestByDistance(state: GameState, players: PlayerInfo[]): PlayerInfo {
|
|
247
|
+
return players
|
|
248
|
+
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
249
|
+
.sort((a, b) => a.d - b.d)[0].p;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 自卫击杀 / 变向试探:只处理「只剩 suspect、本应只回避」的情形(hostile 在上面已优先追杀)。出刀只能由
|
|
254
|
+
* 「主动变向 + 验证对方跟随」达成,绝不靠被动转角/方位,避免「对方只是顺着走廊转、并非在追我」被误判。
|
|
255
|
+
* 每帧:
|
|
256
|
+
* ① aimedPursuit 判定本帧是否「朝我贴身追击」,维护连续帧数(变向等待期内的一帧不朝我是预期的反应延迟,先容忍)。
|
|
257
|
+
* ② 若处在变向等待期:它重新指向我的新位置 = 跟随一次(攒一次);窗口内始终不重新瞄准 = 没跟 → 重置。
|
|
258
|
+
* ③ 攒够 SELF_DEFENSE_JUKE_FOLLOWS 次跟随、已进 killRangeFor 射程、planEscape 判定甩不掉、视线无墙 → 出刀,
|
|
259
|
+
* 并给 Agent 推一条「被动自卫」简报(区别于主动猎杀,免得之后发言/投票露馅)。
|
|
260
|
+
* ④ 否则若已被贴身追够 JUKE_STREAK 帧、甩不掉、且不在等待期 → 主动横向变一次向逼它暴露(窄道无处可变才交回常规逃跑)。
|
|
261
|
+
*
|
|
262
|
+
* 趁 reach 优势:武士虾出刀范围(160)远大于蟹(80),确认在追后在合拢阶段先手,而不是等贴脸拼手速。出刀确认前
|
|
263
|
+
* 不清零追击状态——空刀(极端情况)下进度不该白丢;真杀掉后对方消失,下一帧威胁清空自然重置。
|
|
264
|
+
*/
|
|
265
|
+
private selfDefenseKill(
|
|
266
|
+
state: GameState,
|
|
267
|
+
ctx: StrategyContext,
|
|
268
|
+
threats: PlayerInfo[],
|
|
269
|
+
killReady: boolean,
|
|
270
|
+
): BehaviorDecision[] | null {
|
|
271
|
+
if (!SELF_DEFENSE_ENABLED) return null; // 开关关闭:直接回退到「只回避」,不进入任何试探/出刀逻辑。
|
|
272
|
+
const suspects = threats.filter(p => !this.isHuntTarget(p, ctx));
|
|
273
|
+
if (suspects.length === 0) {
|
|
274
|
+
this.resetSelfDefense();
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const nearest = this.nearestByDistance(state, suspects);
|
|
278
|
+
const aimed = this.aimedPursuit(state, nearest);
|
|
279
|
+
|
|
280
|
+
if (!killReady) return null;
|
|
281
|
+
|
|
282
|
+
// 维护「朝我贴身追击」连续帧数(决定何时开始试探)。某帧不再朝我 = 追击中断,清零进度但【保留当前目标身份】
|
|
283
|
+
// 继续跟它(softResetPursuit,不是 resetSelfDefense——后者会把目标也丢掉、下一帧又当「第一次见到」永远攒不起来)。
|
|
284
|
+
// 例外:变向等待期内的一帧不朝我是预期的反应延迟,先容忍不清零。
|
|
285
|
+
if (aimed) {
|
|
286
|
+
this.selfDefenseStreak++;
|
|
287
|
+
} else if (this.selfDefenseJukeWatch === 0) {
|
|
288
|
+
this.softResetPursuit();
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 变向等待期:它重新指向我的【新】位置 = 跟随一次。但必须等我相对变向起点真的横向拉开够远
|
|
293
|
+
// (jukeFollowed:站在对方位置看,「我变向前」与「我现在」已分得比 cone 还开)才算数——否则我变向移动还没生效、
|
|
294
|
+
// 或位移太小直线仍落在 cone 内时,会把「持续朝旧/当前位置走」误判成「响应我变向重新咬住」。窗口内始终凑不出
|
|
295
|
+
// 一次有效跟随 = 没跟(不是在追我)→ 清零进度重新观察。
|
|
296
|
+
if (this.selfDefenseJukeWatch > 0) {
|
|
297
|
+
if (aimed && this.jukeFollowed(state, nearest)) {
|
|
298
|
+
this.selfDefenseJukeFollows++;
|
|
299
|
+
this.selfDefenseJukeWatch = 0;
|
|
300
|
+
} else if (--this.selfDefenseJukeWatch === 0) {
|
|
301
|
+
this.softResetPursuit();
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const hopeless = this.escapeHopeless(state, threats);
|
|
307
|
+
const d = nearest.distance ?? dist(state.you.x, state.you.y, nearest.x, nearest.y);
|
|
308
|
+
if (
|
|
309
|
+
this.selfDefenseJukeFollows >= SELF_DEFENSE_JUKE_FOLLOWS
|
|
310
|
+
&& d <= killRangeFor(state.you.role)
|
|
311
|
+
&& hopeless
|
|
312
|
+
&& this.hasShotLine(state, nearest)
|
|
313
|
+
) {
|
|
314
|
+
this.clearSub();
|
|
315
|
+
if (!this.selfDefenseAlerted) {
|
|
316
|
+
this.selfDefenseAlerted = true;
|
|
317
|
+
ctx.notifications.push(`连续确认被${nearest.name}贴身追击、变向试探它每次都跟着咬住、退无可退,自卫先手出刀。`);
|
|
318
|
+
ctx.agentAlerts.push(
|
|
319
|
+
`我被${nearest.name}持续贴身追击、几次变向试探它都跟着甩不掉、已被逼到退无可退,刚出于自卫先下手把对方击杀了。`
|
|
320
|
+
+ `这是被动自卫不是主动猎杀:接下来发言/投票按「对方一直追着我跑、我被逼到死角才自保」的口径走,别承认无故先手。`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return [{ action: Action.kill(nearest.name) }];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 还没攒够跟随:被贴身追够、甩不掉、且不在等待期时,主动横向变一次向逼它暴露。无处可变(窄道)才交回常规逃跑。
|
|
327
|
+
if (this.selfDefenseStreak >= SELF_DEFENSE_JUKE_STREAK && this.selfDefenseJukeWatch === 0 && hopeless) {
|
|
328
|
+
const juke = this.jukeTarget(state, nearest);
|
|
329
|
+
if (juke) {
|
|
330
|
+
this.selfDefenseJukeWatch = SELF_DEFENSE_JUKE_WINDOW;
|
|
331
|
+
this.selfDefenseJukeFromPos = { x: state.you.x, y: state.you.y };
|
|
332
|
+
this.clearSub();
|
|
333
|
+
return [{ action: Action.move(juke).withThinking('换个方向甩开') }];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 变向跟随是否成立:站在 suspect 当前位置看,「我发起变向那刻的位置」与「我现在的位置」的夹角已超出 cone
|
|
341
|
+
* (半角≈31.8°)。成立才说明我确实横向拉开够远——此时它仍朝我(aimed)= 它跟着我的变向重新瞄准了;
|
|
342
|
+
* 直奔我变向前位置/顺着走廊直走的虾此刻必然落在 cone 外、aimed 不成立,不会被算作跟随。
|
|
343
|
+
*/
|
|
344
|
+
private jukeFollowed(state: GameState, suspect: PlayerInfo): boolean {
|
|
345
|
+
const from = this.selfDefenseJukeFromPos;
|
|
346
|
+
if (!from) return false;
|
|
347
|
+
const toFrom = unitVec(from.x - suspect.x, from.y - suspect.y);
|
|
348
|
+
const toNow = unitVec(state.you.x - suspect.x, state.you.y - suspect.y);
|
|
349
|
+
if (!toFrom || !toNow) return false;
|
|
350
|
+
return toFrom.x * toNow.x + toFrom.y * toNow.y < SELF_DEFENSE_TOWARD_COS;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 本帧同一 suspect 是否「朝我贴身追击」:相对上一帧位移 ≥ MOVE_EPS(确实在动,排除站桩做任务)且位移方向
|
|
355
|
+
* 与「指向我当前位置」夹角余弦 ≥ TOWARD_COS(朝我来;cone 半角≈31.8°)。换目标时整体重置并返回 false。
|
|
356
|
+
* 只读对方位移与「指向我」的关系,参考点一致;不累计任何转角(转角会被走廊弯曲污染,确认改由变向跟随负责)。
|
|
357
|
+
*/
|
|
358
|
+
private aimedPursuit(state: GameState, nearest: PlayerInfo): boolean {
|
|
359
|
+
const key = nearest.name || String(nearest.seat ?? '');
|
|
360
|
+
const curS = { x: nearest.x, y: nearest.y };
|
|
361
|
+
if (key !== this.selfDefenseTargetKey) {
|
|
362
|
+
this.resetSelfDefense();
|
|
363
|
+
this.selfDefenseTargetKey = key;
|
|
364
|
+
this.selfDefenseSuspectPrev = curS;
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
const sPrev = this.selfDefenseSuspectPrev;
|
|
368
|
+
this.selfDefenseSuspectPrev = curS;
|
|
369
|
+
if (!sPrev) return false;
|
|
370
|
+
|
|
371
|
+
const heading = unitVec(curS.x - sPrev.x, curS.y - sPrev.y);
|
|
372
|
+
const aim = unitVec(state.you.x - sPrev.x, state.you.y - sPrev.y);
|
|
373
|
+
const movedFar = dist(sPrev.x, sPrev.y, curS.x, curS.y) >= SELF_DEFENSE_MOVE_EPS;
|
|
374
|
+
return !!heading && !!aim && movedFar
|
|
375
|
+
&& heading.x * aim.x + heading.y * aim.y >= SELF_DEFENSE_TOWARD_COS;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** planEscape 判定每条逃跑分支都会在推演内被追上(避不开);用与 KeepAwayGoal 一致的基线参数。 */
|
|
379
|
+
private escapeHopeless(state: GameState, threats: PlayerInfo[]): boolean {
|
|
380
|
+
const threatPoints = threats.map(p => ({ x: p.x, y: p.y }));
|
|
381
|
+
return planEscape(state.you, threatPoints, KEEP_AWAY_BASE_PLAN_OPTS).caught;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** 我到目标的连线是否无墙——确认了也别对着墙空刀(target_blocked_by_wall)。 */
|
|
385
|
+
private hasShotLine(state: GameState, target: PlayerInfo): boolean {
|
|
386
|
+
return segmentWalkable(loadWalkableGrid(), state.you.x, state.you.y, target.x, target.y);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 主动变向试探的落点:垂直于「背离 suspect」轴的横向点(略带背离分量以免丢距离),取两侧中离 suspect 更远、
|
|
391
|
+
* 且我到落点整段无墙的一侧。两侧都不可走 / 反而明显贴近 suspect(窄道、贴墙)时返回 null,交回常规逃跑。
|
|
392
|
+
*/
|
|
393
|
+
private jukeTarget(state: GameState, suspect: PlayerInfo): Position | null {
|
|
394
|
+
const grid = loadWalkableGrid();
|
|
395
|
+
const me = state.you;
|
|
396
|
+
const curSep = dist(me.x, me.y, suspect.x, suspect.y);
|
|
397
|
+
const away = unitVec(me.x - suspect.x, me.y - suspect.y) ?? { x: 1, y: 0 };
|
|
398
|
+
const sides = [{ x: -away.y, y: away.x }, { x: away.y, y: -away.x }];
|
|
399
|
+
let best: { target: Position; sep: number } | null = null;
|
|
400
|
+
for (const perp of sides) {
|
|
401
|
+
const dir = unitVec(
|
|
402
|
+
perp.x * SELF_DEFENSE_JUKE_LATERAL + away.x * SELF_DEFENSE_JUKE_AWAY,
|
|
403
|
+
perp.y * SELF_DEFENSE_JUKE_LATERAL + away.y * SELF_DEFENSE_JUKE_AWAY,
|
|
404
|
+
);
|
|
405
|
+
if (!dir) continue;
|
|
406
|
+
const raw = { x: me.x + dir.x * SELF_DEFENSE_JUKE_DIST, y: me.y + dir.y * SELF_DEFENSE_JUKE_DIST };
|
|
407
|
+
const cell = grid.snapToWalkable(grid.worldToCell(raw.x, raw.y), 6);
|
|
408
|
+
if (cell < 0) continue;
|
|
409
|
+
const target = grid.cellToWorld(cell);
|
|
410
|
+
if (dist(me.x, me.y, target.x, target.y) < SELF_DEFENSE_JUKE_MIN_MOVE) continue;
|
|
411
|
+
if (!segmentWalkable(grid, me.x, me.y, target.x, target.y)) continue;
|
|
412
|
+
const sep = dist(target.x, target.y, suspect.x, suspect.y);
|
|
413
|
+
if (!best || sep > best.sep) best = { target, sep };
|
|
414
|
+
}
|
|
415
|
+
// 不往 suspect 身上凑:最优侧也比当前明显更近就放弃变向(交回常规逃跑)。
|
|
416
|
+
if (best && best.sep < curSep - SELF_DEFENSE_JUKE_MIN_MOVE) return null;
|
|
417
|
+
return best?.target ?? null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** 追击中断(某帧不再朝我 / 变向窗口内没凑出有效跟随):清零追击进度,但【保留当前目标身份与上一帧坐标】,
|
|
421
|
+
* 它若重新咬住可继续累计——区别于 resetSelfDefense 连目标一起丢(那样下一帧又当「第一次见到」永远攒不起来)。 */
|
|
422
|
+
private softResetPursuit(): void {
|
|
423
|
+
this.selfDefenseStreak = 0;
|
|
424
|
+
this.selfDefenseJukeWatch = 0;
|
|
425
|
+
this.selfDefenseJukeFromPos = null;
|
|
426
|
+
this.selfDefenseJukeFollows = 0;
|
|
427
|
+
this.selfDefenseAlerted = false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** 整体重置:连当前追击目标身份一起丢(换目标、目标消失、开会、转入主动猎杀时用)。 */
|
|
431
|
+
private resetSelfDefense(): void {
|
|
432
|
+
this.selfDefenseTargetKey = null;
|
|
433
|
+
this.selfDefenseSuspectPrev = null;
|
|
434
|
+
this.softResetPursuit();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private setHunt(targetName: string): void {
|
|
438
|
+
const worker = this.subGoal;
|
|
439
|
+
if (worker instanceof KillVisibleTargetGoal && worker.targetName === targetName) return;
|
|
440
|
+
if (this.subGoal) this.removeSubGoal();
|
|
441
|
+
this.setSubGoal(new KillVisibleTargetGoal(targetName, [], true), SUB_PRIORITY);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private setFlee(): void {
|
|
445
|
+
const worker = this.subGoal;
|
|
446
|
+
if (worker instanceof KeepAwayGoal && worker.key === FLEE_KEY) return;
|
|
447
|
+
if (this.subGoal) this.removeSubGoal();
|
|
448
|
+
this.setSubGoal(new KeepAwayGoal(
|
|
449
|
+
FLEE_KEY,
|
|
450
|
+
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isThreat(p, c)),
|
|
451
|
+
{ threatRadius: Infinity, noun: '认定坏人' } satisfies KeepAwayGoalOptions,
|
|
452
|
+
), SUB_PRIORITY);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private setStrangerKeepAway(): void {
|
|
456
|
+
const worker = this.subGoal;
|
|
457
|
+
if (worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY) return;
|
|
458
|
+
if (this.subGoal) this.removeSubGoal();
|
|
459
|
+
this.setSubGoal(new KeepAwayGoal(
|
|
460
|
+
STRANGER_KEY,
|
|
461
|
+
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isUnmarkedStranger(p, c)),
|
|
462
|
+
{
|
|
463
|
+
threatRadius: STRANGER_RELEASE_RADIUS,
|
|
464
|
+
noun: '陌生人',
|
|
465
|
+
planOpts: { steps: 4 },
|
|
466
|
+
} satisfies KeepAwayGoalOptions,
|
|
467
|
+
), SUB_PRIORITY);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private clearSub(): void {
|
|
471
|
+
if (this.subGoal) this.removeSubGoal();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private emitProgress(state: GameState, ctx: StrategyContext, visible: PlayerInfo[], tier: ProgressTier): void {
|
|
475
|
+
const now = Date.now();
|
|
476
|
+
if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
|
|
477
|
+
ctx.lastProgressNotifyAt = now;
|
|
478
|
+
|
|
479
|
+
const room = state.you.room ?? '未知';
|
|
480
|
+
const cd = killCooldownSecs(state);
|
|
481
|
+
const killsRemaining = state.you.kills_remaining;
|
|
482
|
+
|
|
483
|
+
let msg = `[进度] 当前在${room}`;
|
|
484
|
+
if (visible.length === 0) {
|
|
485
|
+
msg += ',附近无人,做任务/巡逻。';
|
|
486
|
+
} else if (tier === 'keep-distance') {
|
|
487
|
+
msg += ',无标记陌生人靠得太近,先拉开距离再做任务。';
|
|
488
|
+
} else {
|
|
489
|
+
const bad = visible.filter(p => this.isThreat(p, ctx)).map(p => p.name);
|
|
490
|
+
if (bad.length > 0) msg += `,发现认定坏人${bad.join('、')}`;
|
|
491
|
+
else if (this.witnessExempt) msg += `,附近有${visible.length}个无标记陌生人互为目击者,正常做任务`;
|
|
492
|
+
else msg += `,附近有${visible.length}人,正常做任务`;
|
|
493
|
+
if (tier === 'flee-bad') msg += killCooldownSecs(state) > 0 ? ',刀在冷却,正在回避' : ',正在处理';
|
|
494
|
+
if (cd > 0) msg += `(攻击冷却${cd}s)`;
|
|
495
|
+
if (killsRemaining === 0) msg += '(出刀次数已用完)';
|
|
496
|
+
msg += '。';
|
|
497
|
+
}
|
|
498
|
+
ctx.notifications.push(msg);
|
|
499
|
+
}
|
|
500
|
+
}
|