@myclaw163/clawclaw-cli 0.6.70 → 0.6.74

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.
Files changed (212) hide show
  1. package/README.md +377 -427
  2. package/package.json +48 -48
  3. package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -23
  4. package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -22
  5. package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -22
  6. package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -22
  7. package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -23
  8. package/scripts/check-skill-command-surface.mjs +116 -0
  9. package/scripts/find-hide-spots.py +157 -157
  10. package/scripts/postinstall.mjs +20 -20
  11. package/scripts/sync-bundled-skill.mjs +244 -244
  12. package/scripts/sync-bundled-skill.test.mjs +152 -152
  13. package/skills/clawclaw/SKILL.md +246 -244
  14. package/skills/clawclaw/references/CHATTERBOX.md +141 -142
  15. package/skills/clawclaw/references/COMMANDS.md +155 -148
  16. package/skills/clawclaw/references/GAME-MECHANICS.md +188 -188
  17. package/skills/clawclaw/references/HUB.md +48 -48
  18. package/skills/clawclaw/references/KNOWLEDGE.md +42 -45
  19. package/skills/clawclaw/references/STRATEGIES.md +59 -59
  20. package/skills/clawclaw/references/STREAM.md +93 -91
  21. package/skills/clawclaw/references/TACTICS.md +65 -65
  22. package/src/assets/clawclaw-ascii-map.txt +40 -40
  23. package/src/cli.ts +110 -110
  24. package/src/commands/_schema.ts +124 -109
  25. package/src/commands/account.ts +209 -209
  26. package/src/commands/config.ts +30 -30
  27. package/src/commands/do.test.ts +84 -73
  28. package/src/commands/do.ts +130 -126
  29. package/src/commands/events.test.ts +71 -71
  30. package/src/commands/events.ts +221 -155
  31. package/src/commands/game-map.test.ts +28 -28
  32. package/src/commands/game-start-plan.test.ts +84 -84
  33. package/src/commands/game.ts +1113 -1042
  34. package/src/commands/history-player.test.ts +102 -102
  35. package/src/commands/history.ts +573 -573
  36. package/src/commands/hub.test.ts +96 -96
  37. package/src/commands/hub.ts +234 -234
  38. package/src/commands/knowledge.test.ts +13 -13
  39. package/src/commands/knowledge.ts +139 -139
  40. package/src/commands/load.test.ts +51 -51
  41. package/src/commands/load.ts +13 -13
  42. package/src/commands/meeting-history.test.ts +106 -106
  43. package/src/commands/memory.ts +40 -40
  44. package/src/commands/peek.ts +45 -45
  45. package/src/commands/persona.ts +57 -57
  46. package/src/commands/setup/codex.ts +266 -266
  47. package/src/commands/setup/hermes.test.ts +96 -96
  48. package/src/commands/setup/hermes.ts +76 -76
  49. package/src/commands/setup/index.ts +13 -13
  50. package/src/commands/setup/openclaw.test.ts +114 -114
  51. package/src/commands/setup/openclaw.ts +147 -147
  52. package/src/commands/skill.ts +128 -128
  53. package/src/commands/state.ts +46 -46
  54. package/src/commands/strategy.test.ts +145 -145
  55. package/src/commands/strategy.ts +181 -181
  56. package/src/commands/tts.ts +128 -128
  57. package/src/commands/upgrade.test.ts +82 -82
  58. package/src/commands/upgrade.ts +148 -148
  59. package/src/commands/watch.test.ts +999 -977
  60. package/src/commands/watch.ts +660 -658
  61. package/src/lib/auth.test.ts +74 -74
  62. package/src/lib/auth.ts +186 -186
  63. package/src/lib/command-meta.ts +37 -37
  64. package/src/lib/game-client.ts +403 -391
  65. package/src/lib/game-context.ts +92 -0
  66. package/src/lib/host-config-patcher.test.ts +130 -130
  67. package/src/lib/host-config-patcher.ts +151 -151
  68. package/src/lib/http-keepalive.ts +15 -15
  69. package/src/lib/http-transport.test.ts +42 -42
  70. package/src/lib/http-transport.ts +113 -113
  71. package/src/lib/hub-client.test.ts +56 -56
  72. package/src/lib/hub-client.ts +88 -88
  73. package/src/lib/hub-install.test.ts +98 -98
  74. package/src/lib/hub-install.ts +121 -121
  75. package/src/lib/hub-reminder.ts +56 -56
  76. package/src/lib/hub-unzip.test.ts +69 -69
  77. package/src/lib/hub-unzip.ts +62 -62
  78. package/src/lib/init-command.test.ts +75 -75
  79. package/src/lib/init-command.ts +120 -120
  80. package/src/lib/knowledge-store.test.ts +170 -170
  81. package/src/lib/knowledge-store.ts +369 -369
  82. package/src/lib/load-context.test.ts +52 -52
  83. package/src/lib/load-context.ts +52 -52
  84. package/src/lib/match-state.test.ts +134 -134
  85. package/src/lib/match-state.ts +94 -94
  86. package/src/lib/netease-tts.ts +83 -83
  87. package/src/lib/normalize.ts +42 -42
  88. package/src/lib/persona.test.ts +41 -41
  89. package/src/lib/persona.ts +72 -72
  90. package/src/lib/server-registry.ts +152 -152
  91. package/src/lib/skill-version.test.ts +48 -48
  92. package/src/lib/skill-version.ts +19 -19
  93. package/src/lib/strategy-export.test.ts +232 -232
  94. package/src/lib/strategy-export.ts +242 -242
  95. package/src/lib/tts-keys.ts +7 -7
  96. package/src/lib/tts-speech.test.ts +63 -63
  97. package/src/lib/tts-speech.ts +76 -76
  98. package/src/lib/workspace-argv.test.ts +49 -49
  99. package/src/lib/workspace-argv.ts +44 -44
  100. package/src/perception/player-history-store.test.ts +87 -87
  101. package/src/perception/player-history-store.ts +194 -194
  102. package/src/pipeline/event-format.test.ts +243 -215
  103. package/src/pipeline/event-format.ts +501 -485
  104. package/src/pipeline/event-hints.ts +195 -190
  105. package/src/pipeline/event-store.test.ts +28 -28
  106. package/src/pipeline/event-store.ts +193 -193
  107. package/src/pipeline/pipeline.ts +35 -35
  108. package/src/pipeline/player-projection.test.ts +119 -0
  109. package/src/pipeline/player-projection.ts +380 -0
  110. package/src/runtime/auto-upgrade.test.ts +66 -66
  111. package/src/runtime/auto-upgrade.ts +31 -31
  112. package/src/runtime/event-daemon.test.ts +209 -141
  113. package/src/runtime/event-daemon.ts +519 -457
  114. package/src/runtime/owner-control.ts +150 -150
  115. package/src/runtime/raw-ws-log.test.ts +33 -33
  116. package/src/runtime/raw-ws-log.ts +32 -32
  117. package/src/runtime/runtime-logger.ts +107 -107
  118. package/src/runtime/ws-client.test.ts +125 -104
  119. package/src/runtime/ws-client.ts +287 -272
  120. package/src/sdk/action.ts +166 -166
  121. package/src/sdk/index.ts +110 -110
  122. package/src/sdk/types.ts +161 -161
  123. package/src/strategies/avoid-lone.ts +12 -12
  124. package/src/strategies/avoid-players.knowledge.md +19 -19
  125. package/src/strategies/avoid-players.ts +16 -16
  126. package/src/strategies/corpse-patrol.ts +23 -23
  127. package/src/strategies/crab-sabotage.ts +22 -22
  128. package/src/strategies/custom-module.test.ts +270 -270
  129. package/src/strategies/find-player.ts +17 -17
  130. package/src/strategies/game-utils.test.ts +242 -242
  131. package/src/strategies/game-utils.ts +846 -846
  132. package/src/strategies/goals/anchor-linger.ts +77 -77
  133. package/src/strategies/goals/avoid-lone-top.ts +168 -168
  134. package/src/strategies/goals/avoid-players-top.test.ts +83 -83
  135. package/src/strategies/goals/avoid-players-top.ts +121 -121
  136. package/src/strategies/goals/conversation-goal.ts +51 -51
  137. package/src/strategies/goals/corpse-patrol-top.ts +113 -113
  138. package/src/strategies/goals/crab-octopus-reflexes.ts +101 -101
  139. package/src/strategies/goals/crab-sabotage-top.ts +197 -197
  140. package/src/strategies/goals/emergency-hunt-goal.ts +28 -28
  141. package/src/strategies/goals/find-player-top.ts +93 -93
  142. package/src/strategies/goals/flee-players-goal.ts +53 -53
  143. package/src/strategies/goals/follow-companion-goal.ts +106 -106
  144. package/src/strategies/goals/goal-manager.ts +41 -41
  145. package/src/strategies/goals/goal-root-strategy.ts +49 -49
  146. package/src/strategies/goals/goal.ts +28 -28
  147. package/src/strategies/goals/hide-top.ts +197 -197
  148. package/src/strategies/goals/keep-away-goal.ts +221 -221
  149. package/src/strategies/goals/kill-frenzy-top.ts +80 -80
  150. package/src/strategies/goals/kill-lone-top.ts +160 -160
  151. package/src/strategies/goals/kill-target-goal.ts +59 -59
  152. package/src/strategies/goals/kill-target-top.ts +109 -109
  153. package/src/strategies/goals/leaf-goal.ts +27 -27
  154. package/src/strategies/goals/linger-corpse-goal.ts +35 -35
  155. package/src/strategies/goals/lone-kill-core.ts +82 -82
  156. package/src/strategies/goals/lone-kill-goal.ts +24 -24
  157. package/src/strategies/goals/lone-kill-task-top.test.ts +85 -85
  158. package/src/strategies/goals/lone-kill-task-top.ts +133 -133
  159. package/src/strategies/goals/move-room-goal.ts +60 -60
  160. package/src/strategies/goals/normal-shrimp-top.test.ts +80 -80
  161. package/src/strategies/goals/normal-shrimp-top.ts +242 -242
  162. package/src/strategies/goals/paradise-fish-top.test.ts +126 -126
  163. package/src/strategies/goals/paradise-fish-top.ts +224 -224
  164. package/src/strategies/goals/patrol-top.ts +57 -57
  165. package/src/strategies/goals/report-patrol-top.ts +80 -80
  166. package/src/strategies/goals/safe-task-goal.ts +102 -102
  167. package/src/strategies/goals/social-task-top.ts +161 -161
  168. package/src/strategies/goals/task-kill-report-top.ts +163 -163
  169. package/src/strategies/goals/task-only-top.ts +57 -57
  170. package/src/strategies/goals/task-or-patrol-goal.ts +41 -41
  171. package/src/strategies/goals/task-report-top.ts +57 -57
  172. package/src/strategies/goals/wander-task-goal.ts +33 -33
  173. package/src/strategies/goals/warrior-shrimp-top.test.ts +87 -87
  174. package/src/strategies/goals/warrior-shrimp-top.ts +267 -267
  175. package/src/strategies/greeting.ts +53 -53
  176. package/src/strategies/hide-spots.ts +59 -59
  177. package/src/strategies/hide.ts +24 -24
  178. package/src/strategies/kill-frenzy.ts +13 -13
  179. package/src/strategies/kill-lone.knowledge.md +17 -17
  180. package/src/strategies/kill-lone.ts +14 -14
  181. package/src/strategies/kill-target.ts +19 -19
  182. package/src/strategies/loader.test.ts +678 -678
  183. package/src/strategies/loader.ts +179 -179
  184. package/src/strategies/lone-kill-task.ts +22 -22
  185. package/src/strategies/meeting-gate.test.ts +59 -59
  186. package/src/strategies/meeting-gate.ts +23 -23
  187. package/src/strategies/move-room.ts +16 -16
  188. package/src/strategies/new-events-backfill.ts +98 -98
  189. package/src/strategies/off-route-points.ts +105 -105
  190. package/src/strategies/paradise-fish.knowledge.md +19 -19
  191. package/src/strategies/paradise-fish.ts +26 -26
  192. package/src/strategies/pathfind/distance-field.ts +150 -150
  193. package/src/strategies/pathfind/escape-planner.test.ts +197 -197
  194. package/src/strategies/pathfind/escape-planner.ts +355 -355
  195. package/src/strategies/pathfind/walkable-grid.ts +117 -117
  196. package/src/strategies/patrol.ts +12 -12
  197. package/src/strategies/player-targets.ts +13 -13
  198. package/src/strategies/report-patrol.ts +12 -12
  199. package/src/strategies/shrimp-memory.knowledge.md +19 -19
  200. package/src/strategies/shrimp-memory.ts +26 -26
  201. package/src/strategies/social-task.test.ts +28 -28
  202. package/src/strategies/social-task.ts +50 -50
  203. package/src/strategies/spawn.ts +82 -82
  204. package/src/strategies/speech-module.ts +123 -123
  205. package/src/strategies/strategy-loop.test.ts +15 -0
  206. package/src/strategies/strategy-loop.ts +776 -771
  207. package/src/strategies/task-kill-report.ts +18 -18
  208. package/src/strategies/task-only.ts +12 -12
  209. package/src/strategies/task-report.ts +23 -23
  210. package/src/strategies/types.ts +109 -109
  211. package/src/strategies/warrior-memory.knowledge.md +21 -21
  212. package/src/strategies/warrior-memory.ts +17 -17
@@ -1,267 +1,267 @@
1
- import type { GameState, PlayerInfo } from '../../sdk/types.js';
2
- import { Action } from '../../sdk/action.js';
3
- import {
4
- canUseKill,
5
- corpseAtScene,
6
- dist,
7
- firstAvailableTask,
8
- hasKnownCorpse,
9
- isKnowledgeHostile,
10
- isKnowledgeTrusted,
11
- killCooldownSecs,
12
- killCommitRange,
13
- matchesAnyTarget,
14
- nonTeammatesVisible,
15
- PROGRESS_INTERVAL_MS,
16
- reportCorpseDecision,
17
- SHRIMP_VISION_RANGE,
18
- SHRIMP_VISION_RELEASE_RANGE,
19
- taskMoveDecision,
20
- } from '../game-utils.js';
21
- import type { BehaviorDecision, StrategyContext } from '../types.js';
22
- import { Goal } from './goal.js';
23
- import { KeepAwayGoal, type KeepAwayGoalOptions } from './keep-away-goal.js';
24
- import { KillVisibleTargetGoal } from './kill-target-goal.js';
25
- import { SafeTaskOrPatrolGoal } from './safe-task-goal.js';
26
-
27
- const SUB_PRIORITY = 0.5;
28
- const STRANGER_TRIGGER_RADIUS = SHRIMP_VISION_RANGE;
29
- const STRANGER_RELEASE_RADIUS = SHRIMP_VISION_RELEASE_RANGE;
30
- const WITNESS_MEMORY_MS = 0;
31
- const WITNESS_EXEMPT_COUNT = 2;
32
- const TASK_RETURN_INERTIA_MS = 2_000;
33
- const FLEE_KEY = 'warrior-flee';
34
- const STRANGER_KEY = 'warrior-stranger-distance';
35
- // 尸体现场灭口开关:false=站在尸体旁也绝不因「身边有未报警的身份不明者」先手灭口,只报警(hostile/启动目标仍按 P1 追杀);
36
- // true=刀可用时先击杀尸体旁最近的非 trusted 非队友。关掉是为根治「把尸体边未报警的无辜虾当凶手灭口」的误杀。
37
- const CORPSE_SCENE_KILL_ENABLED: boolean = false;
38
-
39
- type ProgressTier = 'flee-bad' | 'keep-distance' | 'task';
40
-
41
- /**
42
- * 带刀虾·记忆进阶版(武士虾/枪虾,带刀好人)。好人阵营但能出刀,按行为表的优先级逐条判断:
43
- *
44
- * - P0 发现尸体:旁边是 hostile/启动猎杀目标且刀可用 → 现场先手击杀;未报警的身份不明者默认不灭口
45
- * (CORPSE_SCENE_KILL_ENABLED,根治误杀);否则报警(靠近至可报距离)。
46
- * - P1 处理危险:**只对【已确认 hostile】(启动参数 / 知识库 hostile)出刀**——刀好就追杀,刀冷就硬躲所有非好人。
47
- * 其余非 trusted 一律按「被怀疑」处理(未标记 = 默认被怀疑):**只回避、绝不出刀**——无可信同伴且单个被怀疑者
48
- * 贴近 → 按普通虾 B 档先拉开距离(多人互为目击者则照常做任务)。
49
- * (历史上的「贴身追击判定 + 诱饵试探 + 绝境自卫反杀」整套已删除:几何上无法把「贴身跟我观察的好人」和「贴身追我的
50
- * 凶手」区分开,自卫出刀迟早误杀同阵营好人、触发 warrior_shrimp_self_destruct 直接崩盘,代价远大于收益。)
51
- * - P2 紧急任务:无危险时优先处理。
52
- * - P3 兜底:做任务 / 巡逻
53
- * (SafeTaskOrPatrolGoal:测地最近、威胁旁/途经威胁的任务硬排除、带粘性)。
54
- *
55
- * 谁算「坏人」「可信同伴」来自 ctx.knowledge(见 warrior-memory.knowledge.md),其余默认被怀疑;
56
- * 队友由游戏事实判定(nonTeammatesVisible),知识库只提供推断。
57
- */
58
- export class WarriorShrimpTop extends Goal {
59
- private readonly taskGoal = new SafeTaskOrPatrolGoal(
60
- // 威胁点 = 坏人(始终算),或未被目击者豁免的被怀疑者;可信同伴不算。
61
- (s, c) => nonTeammatesVisible(s, c)
62
- .filter(p => !this.isTrusted(p, c))
63
- .filter(p => this.isHuntTarget(p, c) || !this.witnessExempt)
64
- .map(p => ({ x: p.x, y: p.y })),
65
- );
66
- private readonly strangerSeenAt = new Map<string, number>();
67
- private witnessExempt = false;
68
-
69
- constructor(private readonly huntTargets: string[] = []) {
70
- super();
71
- }
72
-
73
- tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
74
- const visible = nonTeammatesVisible(state, ctx);
75
- this.updateWitnessMemory(visible, ctx);
76
- const killReady = canUseKill(state);
77
-
78
- // ── P0 发现尸体 ──
79
- // 只在「此刻就在命案现场」(当帧视野有尸体)时考虑现场出刀——记忆里某处有尸体不算现场。
80
- if (corpseAtScene(state) && killReady) {
81
- // 尸体旁的【已确认敌对 / 启动猎杀目标】始终先手击杀,不受灭口开关影响、且排在报尸之前——凶手就在身边时照杀。
82
- // 仅「未报警的身份不明者」(非 trusted、非 hostile)才受 CORPSE_SCENE_KILL_ENABLED 控制:默认关,
83
- // 根治「把尸体边未报警的无辜虾当凶手灭口」的误杀。
84
- const victim = this.nearestKillable(state, ctx, visible, true)
85
- ?? (CORPSE_SCENE_KILL_ENABLED ? this.nearestKillable(state, ctx, visible, false) : null);
86
- if (victim) {
87
- this.clearSub();
88
- return [{ action: Action.kill(victim.name) }];
89
- }
90
- }
91
- // 报尸 / 前往报尸:可以追着记忆里看见过、已离开视野的尸体走回去报告。
92
- if (hasKnownCorpse(ctx)) {
93
- const report = reportCorpseDecision(state, ctx, { respectBlock: true });
94
- if (report) {
95
- this.clearSub();
96
- return [report];
97
- }
98
- }
99
-
100
- // ── P1 处理危险:非 trusted 即潜在威胁(坏人 + 被怀疑) ──
101
- const threats = visible.filter(p => !this.isTrusted(p, ctx));
102
- if (threats.length > 0) {
103
- const huntTargets = threats.filter(p => this.isHuntTarget(p, ctx));
104
- // P1-A 坏人/启动目标且刀好 → 主动追杀。
105
- if (killReady && huntTargets.length > 0) {
106
- this.setHunt(this.nearestByDistance(state, huntTargets).name);
107
- return [];
108
- }
109
-
110
- // P1-B 有坏人/启动目标但刀冷(杀不了)→ 硬躲所有非好人。
111
- if (huntTargets.length > 0) {
112
- this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
113
- this.setFlee();
114
- this.emitProgress(state, ctx, visible, 'flee-bad');
115
- return [];
116
- }
117
-
118
- // P1-C 只剩被怀疑者(无已确认坏人):无可信同伴且单个贴近、且未被目击者豁免 → 按普通虾 B 档拉开距离(只回避,绝不出刀);
119
- // 多个被怀疑者互为目击者(witnessExempt)则不后撤,落到下面照常做任务。
120
- const trustedVisible = visible.some(p => this.isTrusted(p, ctx));
121
- const worker = this.subGoal;
122
- const suspectedClose = threats.some(p =>
123
- (p.distance ?? dist(p.x, p.y, state.you.x, state.you.y)) <= STRANGER_TRIGGER_RADIUS);
124
- const stillBackingOff = worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY && !worker.isFinish(state, ctx);
125
- if (!trustedVisible && !this.witnessExempt && (suspectedClose || stillBackingOff)) {
126
- this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
127
- this.setStrangerKeepAway();
128
- this.emitProgress(state, ctx, visible, 'keep-distance');
129
- return [];
130
- }
131
- }
132
-
133
- // ── P2 紧急任务 ──
134
- const emergency = firstAvailableTask([], t => t.faction !== 'crab', ctx.emergency, ctx.blockedMoveTarget);
135
- if (emergency) {
136
- this.clearSub();
137
- return [taskMoveDecision(state, ctx, emergency)];
138
- }
139
-
140
- // ── P3 普通任务 / 巡逻 ──
141
- this.clearSub();
142
- this.emitProgress(state, ctx, visible, 'task');
143
- return this.taskGoal.tick(state, ctx);
144
- }
145
-
146
- /** 目击者豁免:当前只按本轮可见玩家判断,见到 2 个以上被怀疑者时不对其后撤(多人互为目击者,蟹不敢当众动手)。 */
147
- private updateWitnessMemory(visible: PlayerInfo[], ctx: StrategyContext): void {
148
- const now = Date.now();
149
- for (const p of visible) {
150
- if (!this.isSuspected(p, ctx)) continue;
151
- const key = p.name || String(p.seat ?? '');
152
- if (key) this.strangerSeenAt.set(key, now);
153
- }
154
- for (const [key, at] of this.strangerSeenAt) {
155
- if (now - at > WITNESS_MEMORY_MS) this.strangerSeenAt.delete(key);
156
- }
157
- this.witnessExempt = this.strangerSeenAt.size >= WITNESS_EXEMPT_COUNT;
158
- }
159
-
160
- /** 被怀疑者:非 trusted(好人)且非坏人/启动目标——未标记的默认就落这档,对应 B 档保持距离(只回避、绝不出刀)。 */
161
- private isSuspected(p: PlayerInfo, ctx: StrategyContext): boolean {
162
- return !this.isTrusted(p, ctx) && !this.isHuntTarget(p, ctx);
163
- }
164
-
165
- /** 知识里 trusted 的玩家:可信同伴,永不击杀。 */
166
- private isTrusted(p: PlayerInfo, ctx: StrategyContext): boolean {
167
- return isKnowledgeTrusted(p, ctx);
168
- }
169
-
170
- /** 坏人 / 明确猎杀目标:启动参数或知识库 hostile;被怀疑者不在这里。 */
171
- private isHuntTarget(p: PlayerInfo, ctx: StrategyContext): boolean {
172
- return !this.isTrusted(p, ctx)
173
- && (matchesAnyTarget(p, this.huntTargets, ctx) || isKnowledgeHostile(p, ctx));
174
- }
175
-
176
- /** 潜在威胁 = 任何非 trusted(坏人 + 被怀疑);刀冷遇坏人时硬躲这一整组。 */
177
- private isThreat(p: PlayerInfo, ctx: StrategyContext): boolean {
178
- return !this.isTrusted(p, ctx);
179
- }
180
-
181
- /** 尸体场景出刀目标 = 视野内最近、在出刀范围内的非 trusted 非队友。hostileOnly=true 只取已确认敌对/启动猎杀目标
182
- * (hostile / hunt 参数),用于「尸体旁敌对照杀、不受灭口开关限制」;false 含未报警的身份不明者(受开关控制)。 */
183
- private nearestKillable(
184
- state: GameState,
185
- ctx: StrategyContext,
186
- visible: PlayerInfo[],
187
- hostileOnly: boolean,
188
- ): PlayerInfo | null {
189
- return visible
190
- .filter(p => !this.isTrusted(p, ctx))
191
- .filter(p => !hostileOnly || this.isHuntTarget(p, ctx))
192
- .map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
193
- .filter(x => x.d <= killCommitRange(state.you.role))
194
- .sort((a, b) => a.d - b.d)[0]?.p ?? null;
195
- }
196
-
197
- private nearestByDistance(state: GameState, players: PlayerInfo[]): PlayerInfo {
198
- return players
199
- .map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
200
- .sort((a, b) => a.d - b.d)[0].p;
201
- }
202
-
203
- private setHunt(targetName: string): void {
204
- const worker = this.subGoal;
205
- if (worker instanceof KillVisibleTargetGoal && worker.targetName === targetName) return;
206
- if (this.subGoal) this.removeSubGoal();
207
- this.setSubGoal(new KillVisibleTargetGoal(targetName, [], true), SUB_PRIORITY);
208
- }
209
-
210
- private setFlee(): void {
211
- const worker = this.subGoal;
212
- if (worker instanceof KeepAwayGoal && worker.key === FLEE_KEY) return;
213
- if (this.subGoal) this.removeSubGoal();
214
- this.setSubGoal(new KeepAwayGoal(
215
- FLEE_KEY,
216
- (s, c) => nonTeammatesVisible(s, c).filter(p => this.isThreat(p, c)),
217
- // 行为不变(仍躲所有非 trusted);措辞按真实身份逐个标注,免得把一起躲的被怀疑者也叫成「认定坏人」。
218
- { threatRadius: Infinity, noun: (p, c) => this.isHuntTarget(p, c) ? '认定坏人' : '被怀疑者' } satisfies KeepAwayGoalOptions,
219
- ), SUB_PRIORITY);
220
- }
221
-
222
- private setStrangerKeepAway(): void {
223
- const worker = this.subGoal;
224
- if (worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY) return;
225
- if (this.subGoal) this.removeSubGoal();
226
- this.setSubGoal(new KeepAwayGoal(
227
- STRANGER_KEY,
228
- (s, c) => nonTeammatesVisible(s, c).filter(p => this.isSuspected(p, c)),
229
- {
230
- threatRadius: STRANGER_RELEASE_RADIUS,
231
- noun: '被怀疑者',
232
- planOpts: { steps: 4 },
233
- } satisfies KeepAwayGoalOptions,
234
- ), SUB_PRIORITY);
235
- }
236
-
237
- private clearSub(): void {
238
- if (this.subGoal) this.removeSubGoal();
239
- }
240
-
241
- private emitProgress(state: GameState, ctx: StrategyContext, visible: PlayerInfo[], tier: ProgressTier): void {
242
- const now = Date.now();
243
- if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
244
- ctx.lastProgressNotifyAt = now;
245
-
246
- const room = state.you.room ?? '未知';
247
- const cd = killCooldownSecs(state);
248
- const killsRemaining = state.you.kills_remaining;
249
-
250
- let msg = `[进度] 当前在${room}`;
251
- if (visible.length === 0) {
252
- msg += ',附近无人,做任务/巡逻。';
253
- } else if (tier === 'keep-distance') {
254
- msg += ',被怀疑者靠得太近,先拉开距离再做任务。';
255
- } else {
256
- const bad = visible.filter(p => this.isHuntTarget(p, ctx)).map(p => p.name);
257
- if (bad.length > 0) msg += `,发现坏人${bad.join('、')}`;
258
- else if (this.witnessExempt) msg += `,附近有${visible.length}个被怀疑者互为目击者,正常做任务`;
259
- else msg += `,附近有${visible.length}人,正常做任务`;
260
- if (tier === 'flee-bad') msg += killCooldownSecs(state) > 0 ? ',刀在冷却,正在回避' : ',正在处理';
261
- if (cd > 0) msg += `(攻击冷却${cd}s)`;
262
- if (killsRemaining === 0) msg += '(出刀次数已用完)';
263
- msg += '。';
264
- }
265
- ctx.notifications.push(msg);
266
- }
267
- }
1
+ import type { GameState, PlayerInfo } from '../../sdk/types.js';
2
+ import { Action } from '../../sdk/action.js';
3
+ import {
4
+ canUseKill,
5
+ corpseAtScene,
6
+ dist,
7
+ firstAvailableTask,
8
+ hasKnownCorpse,
9
+ isKnowledgeHostile,
10
+ isKnowledgeTrusted,
11
+ killCooldownSecs,
12
+ killCommitRange,
13
+ matchesAnyTarget,
14
+ nonTeammatesVisible,
15
+ PROGRESS_INTERVAL_MS,
16
+ reportCorpseDecision,
17
+ SHRIMP_VISION_RANGE,
18
+ SHRIMP_VISION_RELEASE_RANGE,
19
+ taskMoveDecision,
20
+ } from '../game-utils.js';
21
+ import type { BehaviorDecision, StrategyContext } from '../types.js';
22
+ import { Goal } from './goal.js';
23
+ import { KeepAwayGoal, type KeepAwayGoalOptions } from './keep-away-goal.js';
24
+ import { KillVisibleTargetGoal } from './kill-target-goal.js';
25
+ import { SafeTaskOrPatrolGoal } from './safe-task-goal.js';
26
+
27
+ const SUB_PRIORITY = 0.5;
28
+ const STRANGER_TRIGGER_RADIUS = SHRIMP_VISION_RANGE;
29
+ const STRANGER_RELEASE_RADIUS = SHRIMP_VISION_RELEASE_RANGE;
30
+ const WITNESS_MEMORY_MS = 0;
31
+ const WITNESS_EXEMPT_COUNT = 2;
32
+ const TASK_RETURN_INERTIA_MS = 2_000;
33
+ const FLEE_KEY = 'warrior-flee';
34
+ const STRANGER_KEY = 'warrior-stranger-distance';
35
+ // 尸体现场灭口开关:false=站在尸体旁也绝不因「身边有未报警的身份不明者」先手灭口,只报警(hostile/启动目标仍按 P1 追杀);
36
+ // true=刀可用时先击杀尸体旁最近的非 trusted 非队友。关掉是为根治「把尸体边未报警的无辜虾当凶手灭口」的误杀。
37
+ const CORPSE_SCENE_KILL_ENABLED: boolean = false;
38
+
39
+ type ProgressTier = 'flee-bad' | 'keep-distance' | 'task';
40
+
41
+ /**
42
+ * 带刀虾·记忆进阶版(武士虾/枪虾,带刀好人)。好人阵营但能出刀,按行为表的优先级逐条判断:
43
+ *
44
+ * - P0 发现尸体:旁边是 hostile/启动猎杀目标且刀可用 → 现场先手击杀;未报警的身份不明者默认不灭口
45
+ * (CORPSE_SCENE_KILL_ENABLED,根治误杀);否则报警(靠近至可报距离)。
46
+ * - P1 处理危险:**只对【已确认 hostile】(启动参数 / 知识库 hostile)出刀**——刀好就追杀,刀冷就硬躲所有非好人。
47
+ * 其余非 trusted 一律按「被怀疑」处理(未标记 = 默认被怀疑):**只回避、绝不出刀**——无可信同伴且单个被怀疑者
48
+ * 贴近 → 按普通虾 B 档先拉开距离(多人互为目击者则照常做任务)。
49
+ * (历史上的「贴身追击判定 + 诱饵试探 + 绝境自卫反杀」整套已删除:几何上无法把「贴身跟我观察的好人」和「贴身追我的
50
+ * 凶手」区分开,自卫出刀迟早误杀同阵营好人、触发 warrior_shrimp_self_destruct 直接崩盘,代价远大于收益。)
51
+ * - P2 紧急任务:无危险时优先处理。
52
+ * - P3 兜底:做任务 / 巡逻
53
+ * (SafeTaskOrPatrolGoal:测地最近、威胁旁/途经威胁的任务硬排除、带粘性)。
54
+ *
55
+ * 谁算「坏人」「可信同伴」来自 ctx.knowledge(见 warrior-memory.knowledge.md),其余默认被怀疑;
56
+ * 队友由游戏事实判定(nonTeammatesVisible),知识库只提供推断。
57
+ */
58
+ export class WarriorShrimpTop extends Goal {
59
+ private readonly taskGoal = new SafeTaskOrPatrolGoal(
60
+ // 威胁点 = 坏人(始终算),或未被目击者豁免的被怀疑者;可信同伴不算。
61
+ (s, c) => nonTeammatesVisible(s, c)
62
+ .filter(p => !this.isTrusted(p, c))
63
+ .filter(p => this.isHuntTarget(p, c) || !this.witnessExempt)
64
+ .map(p => ({ x: p.x, y: p.y })),
65
+ );
66
+ private readonly strangerSeenAt = new Map<string, number>();
67
+ private witnessExempt = false;
68
+
69
+ constructor(private readonly huntTargets: string[] = []) {
70
+ super();
71
+ }
72
+
73
+ tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
74
+ const visible = nonTeammatesVisible(state, ctx);
75
+ this.updateWitnessMemory(visible, ctx);
76
+ const killReady = canUseKill(state);
77
+
78
+ // ── P0 发现尸体 ──
79
+ // 只在「此刻就在命案现场」(当帧视野有尸体)时考虑现场出刀——记忆里某处有尸体不算现场。
80
+ if (corpseAtScene(state) && killReady) {
81
+ // 尸体旁的【已确认敌对 / 启动猎杀目标】始终先手击杀,不受灭口开关影响、且排在报尸之前——凶手就在身边时照杀。
82
+ // 仅「未报警的身份不明者」(非 trusted、非 hostile)才受 CORPSE_SCENE_KILL_ENABLED 控制:默认关,
83
+ // 根治「把尸体边未报警的无辜虾当凶手灭口」的误杀。
84
+ const victim = this.nearestKillable(state, ctx, visible, true)
85
+ ?? (CORPSE_SCENE_KILL_ENABLED ? this.nearestKillable(state, ctx, visible, false) : null);
86
+ if (victim) {
87
+ this.clearSub();
88
+ return [{ action: Action.kill(victim.name) }];
89
+ }
90
+ }
91
+ // 报尸 / 前往报尸:可以追着记忆里看见过、已离开视野的尸体走回去报告。
92
+ if (hasKnownCorpse(ctx)) {
93
+ const report = reportCorpseDecision(state, ctx, { respectBlock: true });
94
+ if (report) {
95
+ this.clearSub();
96
+ return [report];
97
+ }
98
+ }
99
+
100
+ // ── P1 处理危险:非 trusted 即潜在威胁(坏人 + 被怀疑) ──
101
+ const threats = visible.filter(p => !this.isTrusted(p, ctx));
102
+ if (threats.length > 0) {
103
+ const huntTargets = threats.filter(p => this.isHuntTarget(p, ctx));
104
+ // P1-A 坏人/启动目标且刀好 → 主动追杀。
105
+ if (killReady && huntTargets.length > 0) {
106
+ this.setHunt(this.nearestByDistance(state, huntTargets).name);
107
+ return [];
108
+ }
109
+
110
+ // P1-B 有坏人/启动目标但刀冷(杀不了)→ 硬躲所有非好人。
111
+ if (huntTargets.length > 0) {
112
+ this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
113
+ this.setFlee();
114
+ this.emitProgress(state, ctx, visible, 'flee-bad');
115
+ return [];
116
+ }
117
+
118
+ // P1-C 只剩被怀疑者(无已确认坏人):无可信同伴且单个贴近、且未被目击者豁免 → 按普通虾 B 档拉开距离(只回避,绝不出刀);
119
+ // 多个被怀疑者互为目击者(witnessExempt)则不后撤,落到下面照常做任务。
120
+ const trustedVisible = visible.some(p => this.isTrusted(p, ctx));
121
+ const worker = this.subGoal;
122
+ const suspectedClose = threats.some(p =>
123
+ (p.distance ?? dist(p.x, p.y, state.you.x, state.you.y)) <= STRANGER_TRIGGER_RADIUS);
124
+ const stillBackingOff = worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY && !worker.isFinish(state, ctx);
125
+ if (!trustedVisible && !this.witnessExempt && (suspectedClose || stillBackingOff)) {
126
+ this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
127
+ this.setStrangerKeepAway();
128
+ this.emitProgress(state, ctx, visible, 'keep-distance');
129
+ return [];
130
+ }
131
+ }
132
+
133
+ // ── P2 紧急任务 ──
134
+ const emergency = firstAvailableTask([], t => t.faction !== 'crab', ctx.emergency, ctx.blockedMoveTarget);
135
+ if (emergency) {
136
+ this.clearSub();
137
+ return [taskMoveDecision(state, ctx, emergency)];
138
+ }
139
+
140
+ // ── P3 普通任务 / 巡逻 ──
141
+ this.clearSub();
142
+ this.emitProgress(state, ctx, visible, 'task');
143
+ return this.taskGoal.tick(state, ctx);
144
+ }
145
+
146
+ /** 目击者豁免:当前只按本轮可见玩家判断,见到 2 个以上被怀疑者时不对其后撤(多人互为目击者,蟹不敢当众动手)。 */
147
+ private updateWitnessMemory(visible: PlayerInfo[], ctx: StrategyContext): void {
148
+ const now = Date.now();
149
+ for (const p of visible) {
150
+ if (!this.isSuspected(p, ctx)) continue;
151
+ const key = p.name || String(p.seat ?? '');
152
+ if (key) this.strangerSeenAt.set(key, now);
153
+ }
154
+ for (const [key, at] of this.strangerSeenAt) {
155
+ if (now - at > WITNESS_MEMORY_MS) this.strangerSeenAt.delete(key);
156
+ }
157
+ this.witnessExempt = this.strangerSeenAt.size >= WITNESS_EXEMPT_COUNT;
158
+ }
159
+
160
+ /** 被怀疑者:非 trusted(好人)且非坏人/启动目标——未标记的默认就落这档,对应 B 档保持距离(只回避、绝不出刀)。 */
161
+ private isSuspected(p: PlayerInfo, ctx: StrategyContext): boolean {
162
+ return !this.isTrusted(p, ctx) && !this.isHuntTarget(p, ctx);
163
+ }
164
+
165
+ /** 知识里 trusted 的玩家:可信同伴,永不击杀。 */
166
+ private isTrusted(p: PlayerInfo, ctx: StrategyContext): boolean {
167
+ return isKnowledgeTrusted(p, ctx);
168
+ }
169
+
170
+ /** 坏人 / 明确猎杀目标:启动参数或知识库 hostile;被怀疑者不在这里。 */
171
+ private isHuntTarget(p: PlayerInfo, ctx: StrategyContext): boolean {
172
+ return !this.isTrusted(p, ctx)
173
+ && (matchesAnyTarget(p, this.huntTargets, ctx) || isKnowledgeHostile(p, ctx));
174
+ }
175
+
176
+ /** 潜在威胁 = 任何非 trusted(坏人 + 被怀疑);刀冷遇坏人时硬躲这一整组。 */
177
+ private isThreat(p: PlayerInfo, ctx: StrategyContext): boolean {
178
+ return !this.isTrusted(p, ctx);
179
+ }
180
+
181
+ /** 尸体场景出刀目标 = 视野内最近、在出刀范围内的非 trusted 非队友。hostileOnly=true 只取已确认敌对/启动猎杀目标
182
+ * (hostile / hunt 参数),用于「尸体旁敌对照杀、不受灭口开关限制」;false 含未报警的身份不明者(受开关控制)。 */
183
+ private nearestKillable(
184
+ state: GameState,
185
+ ctx: StrategyContext,
186
+ visible: PlayerInfo[],
187
+ hostileOnly: boolean,
188
+ ): PlayerInfo | null {
189
+ return visible
190
+ .filter(p => !this.isTrusted(p, ctx))
191
+ .filter(p => !hostileOnly || this.isHuntTarget(p, ctx))
192
+ .map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
193
+ .filter(x => x.d <= killCommitRange(state.you.role))
194
+ .sort((a, b) => a.d - b.d)[0]?.p ?? null;
195
+ }
196
+
197
+ private nearestByDistance(state: GameState, players: PlayerInfo[]): PlayerInfo {
198
+ return players
199
+ .map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
200
+ .sort((a, b) => a.d - b.d)[0].p;
201
+ }
202
+
203
+ private setHunt(targetName: string): void {
204
+ const worker = this.subGoal;
205
+ if (worker instanceof KillVisibleTargetGoal && worker.targetName === targetName) return;
206
+ if (this.subGoal) this.removeSubGoal();
207
+ this.setSubGoal(new KillVisibleTargetGoal(targetName, [], true), SUB_PRIORITY);
208
+ }
209
+
210
+ private setFlee(): void {
211
+ const worker = this.subGoal;
212
+ if (worker instanceof KeepAwayGoal && worker.key === FLEE_KEY) return;
213
+ if (this.subGoal) this.removeSubGoal();
214
+ this.setSubGoal(new KeepAwayGoal(
215
+ FLEE_KEY,
216
+ (s, c) => nonTeammatesVisible(s, c).filter(p => this.isThreat(p, c)),
217
+ // 行为不变(仍躲所有非 trusted);措辞按真实身份逐个标注,免得把一起躲的被怀疑者也叫成「认定坏人」。
218
+ { threatRadius: Infinity, noun: (p, c) => this.isHuntTarget(p, c) ? '认定坏人' : '被怀疑者' } satisfies KeepAwayGoalOptions,
219
+ ), SUB_PRIORITY);
220
+ }
221
+
222
+ private setStrangerKeepAway(): void {
223
+ const worker = this.subGoal;
224
+ if (worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY) return;
225
+ if (this.subGoal) this.removeSubGoal();
226
+ this.setSubGoal(new KeepAwayGoal(
227
+ STRANGER_KEY,
228
+ (s, c) => nonTeammatesVisible(s, c).filter(p => this.isSuspected(p, c)),
229
+ {
230
+ threatRadius: STRANGER_RELEASE_RADIUS,
231
+ noun: '被怀疑者',
232
+ planOpts: { steps: 4 },
233
+ } satisfies KeepAwayGoalOptions,
234
+ ), SUB_PRIORITY);
235
+ }
236
+
237
+ private clearSub(): void {
238
+ if (this.subGoal) this.removeSubGoal();
239
+ }
240
+
241
+ private emitProgress(state: GameState, ctx: StrategyContext, visible: PlayerInfo[], tier: ProgressTier): void {
242
+ const now = Date.now();
243
+ if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
244
+ ctx.lastProgressNotifyAt = now;
245
+
246
+ const room = state.you.room ?? '未知';
247
+ const cd = killCooldownSecs(state);
248
+ const killsRemaining = state.you.kills_remaining;
249
+
250
+ let msg = `[进度] 当前在${room}`;
251
+ if (visible.length === 0) {
252
+ msg += ',附近无人,做任务/巡逻。';
253
+ } else if (tier === 'keep-distance') {
254
+ msg += ',被怀疑者靠得太近,先拉开距离再做任务。';
255
+ } else {
256
+ const bad = visible.filter(p => this.isHuntTarget(p, ctx)).map(p => p.name);
257
+ if (bad.length > 0) msg += `,发现坏人${bad.join('、')}`;
258
+ else if (this.witnessExempt) msg += `,附近有${visible.length}个被怀疑者互为目击者,正常做任务`;
259
+ else msg += `,附近有${visible.length}人,正常做任务`;
260
+ if (tier === 'flee-bad') msg += killCooldownSecs(state) > 0 ? ',刀在冷却,正在回避' : ',正在处理';
261
+ if (cd > 0) msg += `(攻击冷却${cd}s)`;
262
+ if (killsRemaining === 0) msg += '(出刀次数已用完)';
263
+ msg += '。';
264
+ }
265
+ ctx.notifications.push(msg);
266
+ }
267
+ }
@@ -1,53 +1,53 @@
1
- import { Action } from '../sdk/action.js';
2
- import type { GameState } from '../sdk/types.js';
3
- import type { BehaviorDecision } from './types.js';
4
- import { TTS_TEXT_MAX_LENGTH } from '../lib/tts-keys.js';
5
- import { stripRoleIds } from './player-targets.js';
6
-
7
- export const MAX_GREETING_PHRASES = 3;
8
- export const GREETING_COOLDOWN_MS = 120_000;
9
-
10
- /**
11
- * 解析「打招呼话术」启动参数:剥掉 role-id 噪声与空串,校验条数(≤3)与单条长度(≤TTS 上限)。
12
- * 供 task-report / corpse-patrol / shrimp-memory / paradise-fish 共用——它们的启动参数语义都是「打招呼话术」。
13
- *
14
- * @param strategyId 仅用于错误文案。
15
- */
16
- export function parseGreetingArgs(args: string[] | undefined, strategyId: string): string[] {
17
- if (!args || args.length === 0) return [];
18
-
19
- const phrases = args.map(a => stripRoleIds(a)).filter(a => a.length > 0);
20
- if (phrases.length === 0) return [];
21
-
22
- if (phrases.length > MAX_GREETING_PHRASES) {
23
- throw new Error(`${strategyId} strategy accepts 0-${MAX_GREETING_PHRASES} greeting phrases (got ${phrases.length}).`);
24
- }
25
- for (const phrase of phrases) {
26
- if (phrase.length > TTS_TEXT_MAX_LENGTH) {
27
- throw new Error(`Greeting phrase too long (${phrase.length} chars, max ${TTS_TEXT_MAX_LENGTH}).`);
28
- }
29
- }
30
- return phrases;
31
- }
32
-
33
- /**
34
- * 打招呼节流器:视野内有人时随机发一条话术,之后 GREETING_COOLDOWN_MS 内不再发。
35
- * 被多个编排型 Top Goal(TaskReportTop / NormalShrimpTop 等)持有复用。
36
- */
37
- export class GreetingTracker {
38
- private lastGreetingAt: number | null = null;
39
-
40
- constructor(private readonly phrases: string[]) {}
41
-
42
- tryGreeting(state: GameState): BehaviorDecision | null {
43
- if (this.phrases.length === 0) return null;
44
- if (state.players.length === 0) return null;
45
-
46
- const now = Date.now();
47
- if (this.lastGreetingAt !== null && now - this.lastGreetingAt < GREETING_COOLDOWN_MS) return null;
48
-
49
- const text = this.phrases[Math.floor(Math.random() * this.phrases.length)];
50
- this.lastGreetingAt = now;
51
- return { action: Action.speech(text) };
52
- }
53
- }
1
+ import { Action } from '../sdk/action.js';
2
+ import type { GameState } from '../sdk/types.js';
3
+ import type { BehaviorDecision } from './types.js';
4
+ import { TTS_TEXT_MAX_LENGTH } from '../lib/tts-keys.js';
5
+ import { stripRoleIds } from './player-targets.js';
6
+
7
+ export const MAX_GREETING_PHRASES = 3;
8
+ export const GREETING_COOLDOWN_MS = 120_000;
9
+
10
+ /**
11
+ * 解析「打招呼话术」启动参数:剥掉 role-id 噪声与空串,校验条数(≤3)与单条长度(≤TTS 上限)。
12
+ * 供 task-report / corpse-patrol / shrimp-memory / paradise-fish 共用——它们的启动参数语义都是「打招呼话术」。
13
+ *
14
+ * @param strategyId 仅用于错误文案。
15
+ */
16
+ export function parseGreetingArgs(args: string[] | undefined, strategyId: string): string[] {
17
+ if (!args || args.length === 0) return [];
18
+
19
+ const phrases = args.map(a => stripRoleIds(a)).filter(a => a.length > 0);
20
+ if (phrases.length === 0) return [];
21
+
22
+ if (phrases.length > MAX_GREETING_PHRASES) {
23
+ throw new Error(`${strategyId} strategy accepts 0-${MAX_GREETING_PHRASES} greeting phrases (got ${phrases.length}).`);
24
+ }
25
+ for (const phrase of phrases) {
26
+ if (phrase.length > TTS_TEXT_MAX_LENGTH) {
27
+ throw new Error(`Greeting phrase too long (${phrase.length} chars, max ${TTS_TEXT_MAX_LENGTH}).`);
28
+ }
29
+ }
30
+ return phrases;
31
+ }
32
+
33
+ /**
34
+ * 打招呼节流器:视野内有人时随机发一条话术,之后 GREETING_COOLDOWN_MS 内不再发。
35
+ * 被多个编排型 Top Goal(TaskReportTop / NormalShrimpTop 等)持有复用。
36
+ */
37
+ export class GreetingTracker {
38
+ private lastGreetingAt: number | null = null;
39
+
40
+ constructor(private readonly phrases: string[]) {}
41
+
42
+ tryGreeting(state: GameState): BehaviorDecision | null {
43
+ if (this.phrases.length === 0) return null;
44
+ if (state.players.length === 0) return null;
45
+
46
+ const now = Date.now();
47
+ if (this.lastGreetingAt !== null && now - this.lastGreetingAt < GREETING_COOLDOWN_MS) return null;
48
+
49
+ const text = this.phrases[Math.floor(Math.random() * this.phrases.length)];
50
+ this.lastGreetingAt = now;
51
+ return { action: Action.speech(text) };
52
+ }
53
+ }