@myclaw163/clawclaw-cli 0.6.71 → 0.6.76

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 (209) hide show
  1. package/README.md +387 -427
  2. package/bin/clawclaw-cli.mjs +3 -3
  3. package/package.json +48 -48
  4. package/personas//347/220/206/346/231/272/346/270/251/345/222/214.md +23 -23
  5. package/personas//350/200/201/350/260/213/346/267/261/347/256/227.md +22 -22
  6. package/personas//350/257/232/346/201/263/347/233/264/347/216/207.md +22 -22
  7. package/personas//350/275/273/346/235/276/346/264/273/346/263/274.md +22 -22
  8. package/personas//351/207/216/346/200/247/345/217/233/351/200/206.md +23 -23
  9. package/scripts/check-skill-command-surface.mjs +116 -0
  10. package/scripts/find-hide-spots.py +157 -157
  11. package/scripts/postinstall.mjs +20 -20
  12. package/scripts/sync-bundled-skill.mjs +245 -245
  13. package/scripts/sync-bundled-skill.test.mjs +152 -152
  14. package/skills/clawclaw/SKILL.md +248 -244
  15. package/skills/clawclaw/references/CHATTERBOX.md +141 -142
  16. package/skills/clawclaw/references/COMMANDS.md +160 -148
  17. package/skills/clawclaw/references/GAME-MECHANICS.md +188 -188
  18. package/skills/clawclaw/references/HUB.md +48 -48
  19. package/skills/clawclaw/references/KNOWLEDGE.md +42 -45
  20. package/skills/clawclaw/references/STRATEGIES.md +59 -59
  21. package/skills/clawclaw/references/STREAM.md +93 -91
  22. package/skills/clawclaw/references/TACTICS.md +65 -65
  23. package/src/assets/clawclaw-ascii-map.txt +40 -40
  24. package/src/cli.ts +112 -110
  25. package/src/commands/_schema.ts +124 -109
  26. package/src/commands/account.ts +209 -209
  27. package/src/commands/data.test.ts +33 -0
  28. package/src/commands/data.ts +22 -0
  29. package/src/commands/do.test.ts +84 -73
  30. package/src/commands/do.ts +130 -126
  31. package/src/commands/events.test.ts +100 -71
  32. package/src/commands/events.ts +250 -155
  33. package/src/commands/game-map.test.ts +28 -28
  34. package/src/commands/game-start-plan.test.ts +84 -84
  35. package/src/commands/game.ts +1113 -1047
  36. package/src/commands/history-player.test.ts +102 -102
  37. package/src/commands/history.ts +573 -573
  38. package/src/commands/hub.test.ts +96 -96
  39. package/src/commands/hub.ts +234 -234
  40. package/src/commands/knowledge.test.ts +13 -13
  41. package/src/commands/knowledge.ts +139 -139
  42. package/src/commands/load.test.ts +51 -51
  43. package/src/commands/load.ts +13 -13
  44. package/src/commands/meeting-history.test.ts +106 -106
  45. package/src/commands/memory.ts +40 -40
  46. package/src/commands/peek.ts +45 -45
  47. package/src/commands/persona.ts +57 -57
  48. package/src/commands/setup/codex.ts +266 -266
  49. package/src/commands/skill.ts +128 -128
  50. package/src/commands/state.ts +46 -46
  51. package/src/commands/strategy.test.ts +153 -145
  52. package/src/commands/strategy.ts +183 -181
  53. package/src/commands/tts.ts +128 -128
  54. package/src/commands/upgrade.test.ts +82 -82
  55. package/src/commands/upgrade.ts +148 -148
  56. package/src/commands/watch.test.ts +999 -977
  57. package/src/commands/watch.ts +660 -658
  58. package/src/lib/auth.test.ts +86 -74
  59. package/src/lib/auth.ts +223 -186
  60. package/src/lib/command-meta.ts +37 -37
  61. package/src/lib/game-client.ts +403 -391
  62. package/src/lib/game-context.ts +92 -0
  63. package/src/lib/http-keepalive.ts +15 -15
  64. package/src/lib/http-transport.test.ts +42 -42
  65. package/src/lib/http-transport.ts +113 -113
  66. package/src/lib/hub-client.test.ts +56 -56
  67. package/src/lib/hub-client.ts +88 -88
  68. package/src/lib/hub-install.test.ts +98 -98
  69. package/src/lib/hub-install.ts +160 -121
  70. package/src/lib/hub-reminder.ts +78 -75
  71. package/src/lib/hub-unzip.test.ts +69 -69
  72. package/src/lib/hub-unzip.ts +62 -62
  73. package/src/lib/init-command.test.ts +75 -75
  74. package/src/lib/init-command.ts +130 -120
  75. package/src/lib/knowledge-store.test.ts +170 -170
  76. package/src/lib/knowledge-store.ts +369 -369
  77. package/src/lib/load-context.test.ts +52 -52
  78. package/src/lib/load-context.ts +52 -52
  79. package/src/lib/match-state.test.ts +134 -134
  80. package/src/lib/match-state.ts +94 -94
  81. package/src/lib/netease-tts.ts +83 -83
  82. package/src/lib/normalize.ts +42 -42
  83. package/src/lib/persona.test.ts +41 -41
  84. package/src/lib/persona.ts +72 -72
  85. package/src/lib/server-registry.ts +152 -152
  86. package/src/lib/skill-version.test.ts +48 -48
  87. package/src/lib/skill-version.ts +19 -19
  88. package/src/lib/strategy-export.test.ts +240 -232
  89. package/src/lib/strategy-export.ts +247 -242
  90. package/src/lib/tts-keys.ts +7 -7
  91. package/src/lib/tts-speech.test.ts +63 -63
  92. package/src/lib/tts-speech.ts +76 -76
  93. package/src/lib/user-data.test.ts +96 -0
  94. package/src/lib/user-data.ts +400 -0
  95. package/src/lib/workspace-argv.test.ts +49 -49
  96. package/src/lib/workspace-argv.ts +44 -44
  97. package/src/perception/player-history-store.test.ts +87 -87
  98. package/src/perception/player-history-store.ts +194 -194
  99. package/src/pipeline/event-format.test.ts +243 -215
  100. package/src/pipeline/event-format.ts +501 -485
  101. package/src/pipeline/event-hints.ts +195 -190
  102. package/src/pipeline/event-store.test.ts +28 -28
  103. package/src/pipeline/event-store.ts +193 -193
  104. package/src/pipeline/pipeline.ts +35 -35
  105. package/src/pipeline/player-projection.test.ts +168 -0
  106. package/src/pipeline/player-projection.ts +370 -0
  107. package/src/runtime/auto-upgrade.test.ts +66 -66
  108. package/src/runtime/auto-upgrade.ts +31 -31
  109. package/src/runtime/event-daemon.test.ts +209 -141
  110. package/src/runtime/event-daemon.ts +519 -457
  111. package/src/runtime/owner-control.ts +150 -150
  112. package/src/runtime/raw-ws-log.test.ts +33 -33
  113. package/src/runtime/raw-ws-log.ts +32 -32
  114. package/src/runtime/runtime-logger.ts +107 -107
  115. package/src/runtime/ws-client.test.ts +125 -104
  116. package/src/runtime/ws-client.ts +287 -272
  117. package/src/sdk/action.ts +166 -166
  118. package/src/sdk/index.ts +110 -110
  119. package/src/sdk/types.ts +161 -161
  120. package/src/strategies/avoid-lone.ts +12 -12
  121. package/src/strategies/avoid-players.knowledge.md +19 -19
  122. package/src/strategies/avoid-players.ts +16 -16
  123. package/src/strategies/corpse-patrol.ts +23 -23
  124. package/src/strategies/crab-sabotage.ts +22 -22
  125. package/src/strategies/custom-module.test.ts +270 -270
  126. package/src/strategies/find-player.ts +17 -17
  127. package/src/strategies/game-utils.test.ts +242 -242
  128. package/src/strategies/game-utils.ts +846 -846
  129. package/src/strategies/goals/anchor-linger.ts +77 -77
  130. package/src/strategies/goals/avoid-lone-top.ts +168 -168
  131. package/src/strategies/goals/avoid-players-top.test.ts +83 -83
  132. package/src/strategies/goals/avoid-players-top.ts +121 -121
  133. package/src/strategies/goals/conversation-goal.ts +51 -51
  134. package/src/strategies/goals/corpse-patrol-top.ts +113 -113
  135. package/src/strategies/goals/crab-octopus-reflexes.ts +101 -101
  136. package/src/strategies/goals/crab-sabotage-top.ts +197 -197
  137. package/src/strategies/goals/emergency-hunt-goal.ts +28 -28
  138. package/src/strategies/goals/find-player-top.ts +93 -93
  139. package/src/strategies/goals/flee-players-goal.ts +53 -53
  140. package/src/strategies/goals/follow-companion-goal.ts +106 -106
  141. package/src/strategies/goals/goal-manager.ts +41 -41
  142. package/src/strategies/goals/goal-root-strategy.ts +49 -49
  143. package/src/strategies/goals/goal.ts +28 -28
  144. package/src/strategies/goals/hide-top.ts +197 -197
  145. package/src/strategies/goals/keep-away-goal.ts +221 -221
  146. package/src/strategies/goals/kill-frenzy-top.ts +80 -80
  147. package/src/strategies/goals/kill-lone-top.ts +160 -160
  148. package/src/strategies/goals/kill-target-goal.ts +59 -59
  149. package/src/strategies/goals/kill-target-top.ts +109 -109
  150. package/src/strategies/goals/leaf-goal.ts +27 -27
  151. package/src/strategies/goals/linger-corpse-goal.ts +35 -35
  152. package/src/strategies/goals/lone-kill-core.ts +82 -82
  153. package/src/strategies/goals/lone-kill-goal.ts +24 -24
  154. package/src/strategies/goals/lone-kill-task-top.test.ts +85 -85
  155. package/src/strategies/goals/lone-kill-task-top.ts +133 -133
  156. package/src/strategies/goals/move-room-goal.ts +60 -60
  157. package/src/strategies/goals/normal-shrimp-top.test.ts +80 -80
  158. package/src/strategies/goals/normal-shrimp-top.ts +242 -242
  159. package/src/strategies/goals/paradise-fish-top.test.ts +126 -126
  160. package/src/strategies/goals/paradise-fish-top.ts +224 -224
  161. package/src/strategies/goals/patrol-top.ts +57 -57
  162. package/src/strategies/goals/report-patrol-top.ts +80 -80
  163. package/src/strategies/goals/safe-task-goal.ts +102 -102
  164. package/src/strategies/goals/social-task-top.ts +161 -161
  165. package/src/strategies/goals/task-kill-report-top.ts +163 -163
  166. package/src/strategies/goals/task-only-top.ts +57 -57
  167. package/src/strategies/goals/task-or-patrol-goal.ts +41 -41
  168. package/src/strategies/goals/task-report-top.ts +57 -57
  169. package/src/strategies/goals/wander-task-goal.ts +33 -33
  170. package/src/strategies/goals/warrior-shrimp-top.test.ts +87 -87
  171. package/src/strategies/goals/warrior-shrimp-top.ts +267 -267
  172. package/src/strategies/greeting.ts +53 -53
  173. package/src/strategies/hide-spots.ts +59 -59
  174. package/src/strategies/hide.ts +24 -24
  175. package/src/strategies/kill-frenzy.ts +13 -13
  176. package/src/strategies/kill-lone.knowledge.md +17 -17
  177. package/src/strategies/kill-lone.ts +14 -14
  178. package/src/strategies/kill-target.ts +19 -19
  179. package/src/strategies/loader.test.ts +678 -678
  180. package/src/strategies/loader.ts +181 -179
  181. package/src/strategies/lone-kill-task.ts +22 -22
  182. package/src/strategies/meeting-gate.test.ts +59 -59
  183. package/src/strategies/meeting-gate.ts +23 -23
  184. package/src/strategies/move-room.ts +16 -16
  185. package/src/strategies/new-events-backfill.ts +98 -98
  186. package/src/strategies/off-route-points.ts +105 -105
  187. package/src/strategies/paradise-fish.knowledge.md +19 -19
  188. package/src/strategies/paradise-fish.ts +26 -26
  189. package/src/strategies/pathfind/distance-field.ts +150 -150
  190. package/src/strategies/pathfind/escape-planner.test.ts +197 -197
  191. package/src/strategies/pathfind/escape-planner.ts +355 -355
  192. package/src/strategies/pathfind/walkable-grid.ts +117 -117
  193. package/src/strategies/patrol.ts +12 -12
  194. package/src/strategies/player-targets.ts +13 -13
  195. package/src/strategies/report-patrol.ts +12 -12
  196. package/src/strategies/shrimp-memory.knowledge.md +19 -19
  197. package/src/strategies/shrimp-memory.ts +26 -26
  198. package/src/strategies/social-task.test.ts +28 -28
  199. package/src/strategies/social-task.ts +50 -50
  200. package/src/strategies/spawn.ts +82 -82
  201. package/src/strategies/speech-module.ts +123 -123
  202. package/src/strategies/strategy-loop.test.ts +15 -0
  203. package/src/strategies/strategy-loop.ts +776 -771
  204. package/src/strategies/task-kill-report.ts +18 -18
  205. package/src/strategies/task-only.ts +12 -12
  206. package/src/strategies/task-report.ts +23 -23
  207. package/src/strategies/types.ts +109 -109
  208. package/src/strategies/warrior-memory.knowledge.md +21 -21
  209. 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
+ }