@myclaw163/clawclaw-cli 0.6.58 → 0.6.60
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 +440 -440
- 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/postinstall.mjs +20 -20
- package/scripts/sync-bundled-skill.mjs +244 -244
- package/scripts/sync-bundled-skill.test.mjs +152 -152
- package/skills/clawclaw/SKILL.md +244 -244
- package/skills/clawclaw/references/CHATTERBOX.md +142 -142
- package/skills/clawclaw/references/COMMANDS.md +129 -132
- package/skills/clawclaw/references/GAME-MECHANICS.md +186 -186
- 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 +59 -59
- 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/config.ts +30 -30
- package/src/commands/do.test.ts +73 -37
- package/src/commands/do.ts +126 -95
- package/src/commands/events.ts +22 -22
- 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 +248 -248
- package/src/commands/setup/hermes.test.ts +96 -96
- package/src/commands/setup/hermes.ts +76 -76
- package/src/commands/setup/index.ts +13 -13
- package/src/commands/setup/openclaw.test.ts +114 -114
- package/src/commands/setup/openclaw.ts +147 -147
- 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 +969 -969
- package/src/commands/watch.ts +720 -720
- 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/host-config-patcher.test.ts +130 -130
- package/src/lib/host-config-patcher.ts +151 -151
- 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 +56 -56
- 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-store.ts +124 -124
- 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 +110 -110
- package/src/sdk/types.ts +146 -146
- 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 +744 -744
- 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/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/keep-away-goal.ts +209 -209
- 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 +79 -79
- 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 +219 -219
- 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 +248 -248
- package/src/strategies/greeting.ts +53 -53
- 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 +763 -763
- 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 +96 -96
- package/src/strategies/warrior-memory.knowledge.md +20 -20
- package/src/strategies/warrior-memory.ts +16 -16
|
@@ -1,248 +1,248 @@
|
|
|
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
|
-
isKnowledgeThreat,
|
|
11
|
-
isKnowledgeTrusted,
|
|
12
|
-
killCooldownSecs,
|
|
13
|
-
killCommitRange,
|
|
14
|
-
matchesAnyTarget,
|
|
15
|
-
nonTeammatesVisible,
|
|
16
|
-
PROGRESS_INTERVAL_MS,
|
|
17
|
-
reportCorpseDecision,
|
|
18
|
-
SHRIMP_VISION_RANGE,
|
|
19
|
-
SHRIMP_VISION_RELEASE_RANGE,
|
|
20
|
-
taskMoveDecision,
|
|
21
|
-
} from '../game-utils.js';
|
|
22
|
-
import type { BehaviorDecision, StrategyContext } from '../types.js';
|
|
23
|
-
import { Goal } from './goal.js';
|
|
24
|
-
import { KeepAwayGoal, type KeepAwayGoalOptions } from './keep-away-goal.js';
|
|
25
|
-
import { KillVisibleTargetGoal } from './kill-target-goal.js';
|
|
26
|
-
import { SafeTaskOrPatrolGoal } from './safe-task-goal.js';
|
|
27
|
-
|
|
28
|
-
const SUB_PRIORITY = 0.5;
|
|
29
|
-
const STRANGER_TRIGGER_RADIUS = SHRIMP_VISION_RANGE;
|
|
30
|
-
const STRANGER_RELEASE_RADIUS = SHRIMP_VISION_RELEASE_RANGE;
|
|
31
|
-
const WITNESS_MEMORY_MS = 0;
|
|
32
|
-
const WITNESS_EXEMPT_COUNT = 2;
|
|
33
|
-
const TASK_RETURN_INERTIA_MS = 2_000;
|
|
34
|
-
const FLEE_KEY = 'warrior-flee';
|
|
35
|
-
const STRANGER_KEY = 'warrior-stranger-distance';
|
|
36
|
-
|
|
37
|
-
type ProgressTier = 'flee-bad' | 'keep-distance' | 'task';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 带刀虾·记忆进阶版(武士虾/枪虾,带刀好人)。好人阵营但能出刀,按行为表的优先级逐条判断:
|
|
41
|
-
*
|
|
42
|
-
* - P0 发现尸体:旁边有非保护对象且刀可用 → 先出刀;否则报警(靠近至可报距离)。
|
|
43
|
-
* - P1 处理危险:明确猎杀目标(启动参数 / hostile)刀好就追杀;
|
|
44
|
-
* suspect 只表示可疑,始终回避,不会仅凭怀疑主动出刀;
|
|
45
|
-
* 无可信同伴且单个无标记陌生人贴近 → 按普通虾 B 档先拉开距离。
|
|
46
|
-
* - P2 紧急任务:无危险时优先处理。
|
|
47
|
-
* - P3 兜底:做任务 / 巡逻
|
|
48
|
-
* (SafeTaskOrPatrolGoal:测地最近、威胁旁/途经威胁的任务硬排除、带粘性)。
|
|
49
|
-
*
|
|
50
|
-
* 谁算「认定坏人」「可信同伴」来自 ctx.knowledge(见 warrior-memory.knowledge.md),
|
|
51
|
-
* 队友由游戏事实判定(nonTeammatesVisible),知识库只提供推断。
|
|
52
|
-
*/
|
|
53
|
-
export class WarriorShrimpTop extends Goal {
|
|
54
|
-
private readonly taskGoal = new SafeTaskOrPatrolGoal(
|
|
55
|
-
// 威胁点 = 当前会触发追杀/后撤的对象,或未被目击者豁免的无标记陌生人;保护对象不算。
|
|
56
|
-
(s, c) => nonTeammatesVisible(s, c)
|
|
57
|
-
.filter(p => !this.isTrusted(p, c))
|
|
58
|
-
.filter(p => this.isThreat(p, c) || !this.witnessExempt)
|
|
59
|
-
.map(p => ({ x: p.x, y: p.y })),
|
|
60
|
-
);
|
|
61
|
-
private readonly strangerSeenAt = new Map<string, number>();
|
|
62
|
-
private witnessExempt = false;
|
|
63
|
-
|
|
64
|
-
constructor(private readonly huntTargets: string[] = []) {
|
|
65
|
-
super();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
69
|
-
const visible = nonTeammatesVisible(state, ctx);
|
|
70
|
-
this.updateWitnessMemory(visible, ctx);
|
|
71
|
-
const killReady = canUseKill(state);
|
|
72
|
-
|
|
73
|
-
// ── P0 发现尸体 ──
|
|
74
|
-
// 灭口贴脸目击者只在「此刻就在命案现场」(当帧视野有尸体)时考虑——记忆里某处有尸体不算现场,
|
|
75
|
-
// 否则会变成随便撞见一个人就灭口。
|
|
76
|
-
if (corpseAtScene(state) && killReady) {
|
|
77
|
-
const victim = this.nearestKillable(state, ctx, visible);
|
|
78
|
-
if (victim) {
|
|
79
|
-
this.clearSub();
|
|
80
|
-
return [{ action: Action.kill(victim.name) }];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// 报尸 / 前往报尸:可以追着记忆里看见过、已离开视野的尸体走回去报告。
|
|
84
|
-
if (hasKnownCorpse(ctx)) {
|
|
85
|
-
const report = reportCorpseDecision(state, ctx, { respectBlock: true });
|
|
86
|
-
if (report) {
|
|
87
|
-
this.clearSub();
|
|
88
|
-
return [report];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── P1-A hostile 刀好就追;suspect 只回避 ──
|
|
93
|
-
const threats = visible.filter(p => this.isThreat(p, ctx));
|
|
94
|
-
if (threats.length > 0) {
|
|
95
|
-
const huntTargets = threats.filter(p => this.isHuntTarget(p, ctx));
|
|
96
|
-
if (killReady && huntTargets.length > 0) {
|
|
97
|
-
this.setHunt(this.nearestByDistance(state, huntTargets).name);
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
|
-
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
101
|
-
this.setFlee();
|
|
102
|
-
this.emitProgress(state, ctx, visible, 'flee-bad');
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── P1-B 无可信同伴且无标记陌生人贴近 → 按普通虾 B 档拉开距离 ──
|
|
107
|
-
const trustedVisible = visible.some(p => this.isTrusted(p, ctx));
|
|
108
|
-
if (!trustedVisible) {
|
|
109
|
-
const worker = this.subGoal;
|
|
110
|
-
const strangerClose = visible.some(p =>
|
|
111
|
-
this.isUnmarkedStranger(p, ctx)
|
|
112
|
-
&& (p.distance ?? dist(p.x, p.y, state.you.x, state.you.y)) <= STRANGER_TRIGGER_RADIUS);
|
|
113
|
-
const stillBackingOff = worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY && !worker.isFinish(state, ctx);
|
|
114
|
-
if (!this.witnessExempt && (strangerClose || stillBackingOff)) {
|
|
115
|
-
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
116
|
-
this.setStrangerKeepAway();
|
|
117
|
-
this.emitProgress(state, ctx, visible, 'keep-distance');
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── P2 紧急任务 ──
|
|
123
|
-
const emergency = firstAvailableTask([], t => t.faction !== 'crab', ctx.emergency, ctx.blockedMoveTarget);
|
|
124
|
-
if (emergency) {
|
|
125
|
-
this.clearSub();
|
|
126
|
-
return [taskMoveDecision(state, ctx, emergency)];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ── P3 普通任务 / 巡逻 ──
|
|
130
|
-
this.clearSub();
|
|
131
|
-
this.emitProgress(state, ctx, visible, 'task');
|
|
132
|
-
return this.taskGoal.tick(state, ctx);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** 目击者豁免:当前只按本轮可见玩家判断,见到 2 个以上陌生人时不对无标记者后撤。 */
|
|
136
|
-
private updateWitnessMemory(visible: PlayerInfo[], ctx: StrategyContext): void {
|
|
137
|
-
const now = Date.now();
|
|
138
|
-
for (const p of visible) {
|
|
139
|
-
if (!this.isUnmarkedStranger(p, ctx)) continue;
|
|
140
|
-
const key = p.name || String(p.seat ?? '');
|
|
141
|
-
if (key) this.strangerSeenAt.set(key, now);
|
|
142
|
-
}
|
|
143
|
-
for (const [key, at] of this.strangerSeenAt) {
|
|
144
|
-
if (now - at > WITNESS_MEMORY_MS) this.strangerSeenAt.delete(key);
|
|
145
|
-
}
|
|
146
|
-
this.witnessExempt = this.strangerSeenAt.size >= WITNESS_EXEMPT_COUNT;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private isUnmarkedStranger(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
150
|
-
return !this.isTrusted(p, ctx) && !this.isThreat(p, ctx);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** 知识里 trusted 的玩家:可信同伴,永不击杀。 */
|
|
154
|
-
private isTrusted(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
155
|
-
return isKnowledgeTrusted(p, ctx);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** 明确猎杀目标:启动参数或 hostile;suspect 不在这里。 */
|
|
159
|
-
private isHuntTarget(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
160
|
-
return !this.isTrusted(p, ctx)
|
|
161
|
-
&& (matchesAnyTarget(p, this.huntTargets, ctx) || isKnowledgeHostile(p, ctx));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** 威胁:明确猎杀目标,或知识库 suspect 标记;trusted 永远排除。 */
|
|
165
|
-
private isThreat(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
166
|
-
return !this.isTrusted(p, ctx)
|
|
167
|
-
&& (this.isHuntTarget(p, ctx) || isKnowledgeThreat(p, ctx));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** 尸体场景下「认定坏人/身份不明」= 视野内最近的非保护非队友,且在出刀范围内。 */
|
|
171
|
-
private nearestKillable(state: GameState, ctx: StrategyContext, visible: PlayerInfo[]): PlayerInfo | null {
|
|
172
|
-
return visible
|
|
173
|
-
.filter(p => !this.isTrusted(p, ctx))
|
|
174
|
-
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
175
|
-
.filter(x => x.d <= killCommitRange(state.you.role))
|
|
176
|
-
.sort((a, b) => a.d - b.d)[0]?.p ?? null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private nearestByDistance(state: GameState, players: PlayerInfo[]): PlayerInfo {
|
|
180
|
-
return players
|
|
181
|
-
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
182
|
-
.sort((a, b) => a.d - b.d)[0].p;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private setHunt(targetName: string): void {
|
|
186
|
-
const worker = this.subGoal;
|
|
187
|
-
if (worker instanceof KillVisibleTargetGoal && worker.targetName === targetName) return;
|
|
188
|
-
if (this.subGoal) this.removeSubGoal();
|
|
189
|
-
this.setSubGoal(new KillVisibleTargetGoal(targetName, [], true), SUB_PRIORITY);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private setFlee(): void {
|
|
193
|
-
const worker = this.subGoal;
|
|
194
|
-
if (worker instanceof KeepAwayGoal && worker.key === FLEE_KEY) return;
|
|
195
|
-
if (this.subGoal) this.removeSubGoal();
|
|
196
|
-
this.setSubGoal(new KeepAwayGoal(
|
|
197
|
-
FLEE_KEY,
|
|
198
|
-
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isThreat(p, c)),
|
|
199
|
-
{ threatRadius: Infinity, noun: '认定坏人' } satisfies KeepAwayGoalOptions,
|
|
200
|
-
), SUB_PRIORITY);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private setStrangerKeepAway(): void {
|
|
204
|
-
const worker = this.subGoal;
|
|
205
|
-
if (worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY) return;
|
|
206
|
-
if (this.subGoal) this.removeSubGoal();
|
|
207
|
-
this.setSubGoal(new KeepAwayGoal(
|
|
208
|
-
STRANGER_KEY,
|
|
209
|
-
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isUnmarkedStranger(p, c)),
|
|
210
|
-
{
|
|
211
|
-
threatRadius: STRANGER_RELEASE_RADIUS,
|
|
212
|
-
noun: '陌生人',
|
|
213
|
-
planOpts: { steps: 4 },
|
|
214
|
-
} satisfies KeepAwayGoalOptions,
|
|
215
|
-
), SUB_PRIORITY);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private clearSub(): void {
|
|
219
|
-
if (this.subGoal) this.removeSubGoal();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private emitProgress(state: GameState, ctx: StrategyContext, visible: PlayerInfo[], tier: ProgressTier): void {
|
|
223
|
-
const now = Date.now();
|
|
224
|
-
if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
|
|
225
|
-
ctx.lastProgressNotifyAt = now;
|
|
226
|
-
|
|
227
|
-
const room = state.you.room ?? '未知';
|
|
228
|
-
const cd = killCooldownSecs(state);
|
|
229
|
-
const killsRemaining = state.you.kills_remaining;
|
|
230
|
-
|
|
231
|
-
let msg = `[进度] 当前在${room}`;
|
|
232
|
-
if (visible.length === 0) {
|
|
233
|
-
msg += ',附近无人,做任务/巡逻。';
|
|
234
|
-
} else if (tier === 'keep-distance') {
|
|
235
|
-
msg += ',无标记陌生人靠得太近,先拉开距离再做任务。';
|
|
236
|
-
} else {
|
|
237
|
-
const bad = visible.filter(p => this.isThreat(p, ctx)).map(p => p.name);
|
|
238
|
-
if (bad.length > 0) msg += `,发现认定坏人${bad.join('、')}`;
|
|
239
|
-
else if (this.witnessExempt) msg += `,附近有${visible.length}个无标记陌生人互为目击者,正常做任务`;
|
|
240
|
-
else msg += `,附近有${visible.length}人,正常做任务`;
|
|
241
|
-
if (tier === 'flee-bad') msg += killCooldownSecs(state) > 0 ? ',刀在冷却,正在回避' : ',正在处理';
|
|
242
|
-
if (cd > 0) msg += `(攻击冷却${cd}s)`;
|
|
243
|
-
if (killsRemaining === 0) msg += '(出刀次数已用完)';
|
|
244
|
-
msg += '。';
|
|
245
|
-
}
|
|
246
|
-
ctx.notifications.push(msg);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
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
|
+
isKnowledgeThreat,
|
|
11
|
+
isKnowledgeTrusted,
|
|
12
|
+
killCooldownSecs,
|
|
13
|
+
killCommitRange,
|
|
14
|
+
matchesAnyTarget,
|
|
15
|
+
nonTeammatesVisible,
|
|
16
|
+
PROGRESS_INTERVAL_MS,
|
|
17
|
+
reportCorpseDecision,
|
|
18
|
+
SHRIMP_VISION_RANGE,
|
|
19
|
+
SHRIMP_VISION_RELEASE_RANGE,
|
|
20
|
+
taskMoveDecision,
|
|
21
|
+
} from '../game-utils.js';
|
|
22
|
+
import type { BehaviorDecision, StrategyContext } from '../types.js';
|
|
23
|
+
import { Goal } from './goal.js';
|
|
24
|
+
import { KeepAwayGoal, type KeepAwayGoalOptions } from './keep-away-goal.js';
|
|
25
|
+
import { KillVisibleTargetGoal } from './kill-target-goal.js';
|
|
26
|
+
import { SafeTaskOrPatrolGoal } from './safe-task-goal.js';
|
|
27
|
+
|
|
28
|
+
const SUB_PRIORITY = 0.5;
|
|
29
|
+
const STRANGER_TRIGGER_RADIUS = SHRIMP_VISION_RANGE;
|
|
30
|
+
const STRANGER_RELEASE_RADIUS = SHRIMP_VISION_RELEASE_RANGE;
|
|
31
|
+
const WITNESS_MEMORY_MS = 0;
|
|
32
|
+
const WITNESS_EXEMPT_COUNT = 2;
|
|
33
|
+
const TASK_RETURN_INERTIA_MS = 2_000;
|
|
34
|
+
const FLEE_KEY = 'warrior-flee';
|
|
35
|
+
const STRANGER_KEY = 'warrior-stranger-distance';
|
|
36
|
+
|
|
37
|
+
type ProgressTier = 'flee-bad' | 'keep-distance' | 'task';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 带刀虾·记忆进阶版(武士虾/枪虾,带刀好人)。好人阵营但能出刀,按行为表的优先级逐条判断:
|
|
41
|
+
*
|
|
42
|
+
* - P0 发现尸体:旁边有非保护对象且刀可用 → 先出刀;否则报警(靠近至可报距离)。
|
|
43
|
+
* - P1 处理危险:明确猎杀目标(启动参数 / hostile)刀好就追杀;
|
|
44
|
+
* suspect 只表示可疑,始终回避,不会仅凭怀疑主动出刀;
|
|
45
|
+
* 无可信同伴且单个无标记陌生人贴近 → 按普通虾 B 档先拉开距离。
|
|
46
|
+
* - P2 紧急任务:无危险时优先处理。
|
|
47
|
+
* - P3 兜底:做任务 / 巡逻
|
|
48
|
+
* (SafeTaskOrPatrolGoal:测地最近、威胁旁/途经威胁的任务硬排除、带粘性)。
|
|
49
|
+
*
|
|
50
|
+
* 谁算「认定坏人」「可信同伴」来自 ctx.knowledge(见 warrior-memory.knowledge.md),
|
|
51
|
+
* 队友由游戏事实判定(nonTeammatesVisible),知识库只提供推断。
|
|
52
|
+
*/
|
|
53
|
+
export class WarriorShrimpTop extends Goal {
|
|
54
|
+
private readonly taskGoal = new SafeTaskOrPatrolGoal(
|
|
55
|
+
// 威胁点 = 当前会触发追杀/后撤的对象,或未被目击者豁免的无标记陌生人;保护对象不算。
|
|
56
|
+
(s, c) => nonTeammatesVisible(s, c)
|
|
57
|
+
.filter(p => !this.isTrusted(p, c))
|
|
58
|
+
.filter(p => this.isThreat(p, c) || !this.witnessExempt)
|
|
59
|
+
.map(p => ({ x: p.x, y: p.y })),
|
|
60
|
+
);
|
|
61
|
+
private readonly strangerSeenAt = new Map<string, number>();
|
|
62
|
+
private witnessExempt = false;
|
|
63
|
+
|
|
64
|
+
constructor(private readonly huntTargets: string[] = []) {
|
|
65
|
+
super();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
69
|
+
const visible = nonTeammatesVisible(state, ctx);
|
|
70
|
+
this.updateWitnessMemory(visible, ctx);
|
|
71
|
+
const killReady = canUseKill(state);
|
|
72
|
+
|
|
73
|
+
// ── P0 发现尸体 ──
|
|
74
|
+
// 灭口贴脸目击者只在「此刻就在命案现场」(当帧视野有尸体)时考虑——记忆里某处有尸体不算现场,
|
|
75
|
+
// 否则会变成随便撞见一个人就灭口。
|
|
76
|
+
if (corpseAtScene(state) && killReady) {
|
|
77
|
+
const victim = this.nearestKillable(state, ctx, visible);
|
|
78
|
+
if (victim) {
|
|
79
|
+
this.clearSub();
|
|
80
|
+
return [{ action: Action.kill(victim.name) }];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// 报尸 / 前往报尸:可以追着记忆里看见过、已离开视野的尸体走回去报告。
|
|
84
|
+
if (hasKnownCorpse(ctx)) {
|
|
85
|
+
const report = reportCorpseDecision(state, ctx, { respectBlock: true });
|
|
86
|
+
if (report) {
|
|
87
|
+
this.clearSub();
|
|
88
|
+
return [report];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── P1-A hostile 刀好就追;suspect 只回避 ──
|
|
93
|
+
const threats = visible.filter(p => this.isThreat(p, ctx));
|
|
94
|
+
if (threats.length > 0) {
|
|
95
|
+
const huntTargets = threats.filter(p => this.isHuntTarget(p, ctx));
|
|
96
|
+
if (killReady && huntTargets.length > 0) {
|
|
97
|
+
this.setHunt(this.nearestByDistance(state, huntTargets).name);
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
101
|
+
this.setFlee();
|
|
102
|
+
this.emitProgress(state, ctx, visible, 'flee-bad');
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── P1-B 无可信同伴且无标记陌生人贴近 → 按普通虾 B 档拉开距离 ──
|
|
107
|
+
const trustedVisible = visible.some(p => this.isTrusted(p, ctx));
|
|
108
|
+
if (!trustedVisible) {
|
|
109
|
+
const worker = this.subGoal;
|
|
110
|
+
const strangerClose = visible.some(p =>
|
|
111
|
+
this.isUnmarkedStranger(p, ctx)
|
|
112
|
+
&& (p.distance ?? dist(p.x, p.y, state.you.x, state.you.y)) <= STRANGER_TRIGGER_RADIUS);
|
|
113
|
+
const stillBackingOff = worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY && !worker.isFinish(state, ctx);
|
|
114
|
+
if (!this.witnessExempt && (strangerClose || stillBackingOff)) {
|
|
115
|
+
this.taskGoal.planTask(state, ctx, { holdUnsafeCurrentForMs: TASK_RETURN_INERTIA_MS });
|
|
116
|
+
this.setStrangerKeepAway();
|
|
117
|
+
this.emitProgress(state, ctx, visible, 'keep-distance');
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── P2 紧急任务 ──
|
|
123
|
+
const emergency = firstAvailableTask([], t => t.faction !== 'crab', ctx.emergency, ctx.blockedMoveTarget);
|
|
124
|
+
if (emergency) {
|
|
125
|
+
this.clearSub();
|
|
126
|
+
return [taskMoveDecision(state, ctx, emergency)];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── P3 普通任务 / 巡逻 ──
|
|
130
|
+
this.clearSub();
|
|
131
|
+
this.emitProgress(state, ctx, visible, 'task');
|
|
132
|
+
return this.taskGoal.tick(state, ctx);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 目击者豁免:当前只按本轮可见玩家判断,见到 2 个以上陌生人时不对无标记者后撤。 */
|
|
136
|
+
private updateWitnessMemory(visible: PlayerInfo[], ctx: StrategyContext): void {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
for (const p of visible) {
|
|
139
|
+
if (!this.isUnmarkedStranger(p, ctx)) continue;
|
|
140
|
+
const key = p.name || String(p.seat ?? '');
|
|
141
|
+
if (key) this.strangerSeenAt.set(key, now);
|
|
142
|
+
}
|
|
143
|
+
for (const [key, at] of this.strangerSeenAt) {
|
|
144
|
+
if (now - at > WITNESS_MEMORY_MS) this.strangerSeenAt.delete(key);
|
|
145
|
+
}
|
|
146
|
+
this.witnessExempt = this.strangerSeenAt.size >= WITNESS_EXEMPT_COUNT;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private isUnmarkedStranger(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
150
|
+
return !this.isTrusted(p, ctx) && !this.isThreat(p, ctx);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 知识里 trusted 的玩家:可信同伴,永不击杀。 */
|
|
154
|
+
private isTrusted(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
155
|
+
return isKnowledgeTrusted(p, ctx);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 明确猎杀目标:启动参数或 hostile;suspect 不在这里。 */
|
|
159
|
+
private isHuntTarget(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
160
|
+
return !this.isTrusted(p, ctx)
|
|
161
|
+
&& (matchesAnyTarget(p, this.huntTargets, ctx) || isKnowledgeHostile(p, ctx));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 威胁:明确猎杀目标,或知识库 suspect 标记;trusted 永远排除。 */
|
|
165
|
+
private isThreat(p: PlayerInfo, ctx: StrategyContext): boolean {
|
|
166
|
+
return !this.isTrusted(p, ctx)
|
|
167
|
+
&& (this.isHuntTarget(p, ctx) || isKnowledgeThreat(p, ctx));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 尸体场景下「认定坏人/身份不明」= 视野内最近的非保护非队友,且在出刀范围内。 */
|
|
171
|
+
private nearestKillable(state: GameState, ctx: StrategyContext, visible: PlayerInfo[]): PlayerInfo | null {
|
|
172
|
+
return visible
|
|
173
|
+
.filter(p => !this.isTrusted(p, ctx))
|
|
174
|
+
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
175
|
+
.filter(x => x.d <= killCommitRange(state.you.role))
|
|
176
|
+
.sort((a, b) => a.d - b.d)[0]?.p ?? null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private nearestByDistance(state: GameState, players: PlayerInfo[]): PlayerInfo {
|
|
180
|
+
return players
|
|
181
|
+
.map(p => ({ p, d: p.distance ?? dist(state.you.x, state.you.y, p.x, p.y) }))
|
|
182
|
+
.sort((a, b) => a.d - b.d)[0].p;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private setHunt(targetName: string): void {
|
|
186
|
+
const worker = this.subGoal;
|
|
187
|
+
if (worker instanceof KillVisibleTargetGoal && worker.targetName === targetName) return;
|
|
188
|
+
if (this.subGoal) this.removeSubGoal();
|
|
189
|
+
this.setSubGoal(new KillVisibleTargetGoal(targetName, [], true), SUB_PRIORITY);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private setFlee(): void {
|
|
193
|
+
const worker = this.subGoal;
|
|
194
|
+
if (worker instanceof KeepAwayGoal && worker.key === FLEE_KEY) return;
|
|
195
|
+
if (this.subGoal) this.removeSubGoal();
|
|
196
|
+
this.setSubGoal(new KeepAwayGoal(
|
|
197
|
+
FLEE_KEY,
|
|
198
|
+
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isThreat(p, c)),
|
|
199
|
+
{ threatRadius: Infinity, noun: '认定坏人' } satisfies KeepAwayGoalOptions,
|
|
200
|
+
), SUB_PRIORITY);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private setStrangerKeepAway(): void {
|
|
204
|
+
const worker = this.subGoal;
|
|
205
|
+
if (worker instanceof KeepAwayGoal && worker.key === STRANGER_KEY) return;
|
|
206
|
+
if (this.subGoal) this.removeSubGoal();
|
|
207
|
+
this.setSubGoal(new KeepAwayGoal(
|
|
208
|
+
STRANGER_KEY,
|
|
209
|
+
(s, c) => nonTeammatesVisible(s, c).filter(p => this.isUnmarkedStranger(p, c)),
|
|
210
|
+
{
|
|
211
|
+
threatRadius: STRANGER_RELEASE_RADIUS,
|
|
212
|
+
noun: '陌生人',
|
|
213
|
+
planOpts: { steps: 4 },
|
|
214
|
+
} satisfies KeepAwayGoalOptions,
|
|
215
|
+
), SUB_PRIORITY);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private clearSub(): void {
|
|
219
|
+
if (this.subGoal) this.removeSubGoal();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private emitProgress(state: GameState, ctx: StrategyContext, visible: PlayerInfo[], tier: ProgressTier): void {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (now - ctx.lastProgressNotifyAt < PROGRESS_INTERVAL_MS) return;
|
|
225
|
+
ctx.lastProgressNotifyAt = now;
|
|
226
|
+
|
|
227
|
+
const room = state.you.room ?? '未知';
|
|
228
|
+
const cd = killCooldownSecs(state);
|
|
229
|
+
const killsRemaining = state.you.kills_remaining;
|
|
230
|
+
|
|
231
|
+
let msg = `[进度] 当前在${room}`;
|
|
232
|
+
if (visible.length === 0) {
|
|
233
|
+
msg += ',附近无人,做任务/巡逻。';
|
|
234
|
+
} else if (tier === 'keep-distance') {
|
|
235
|
+
msg += ',无标记陌生人靠得太近,先拉开距离再做任务。';
|
|
236
|
+
} else {
|
|
237
|
+
const bad = visible.filter(p => this.isThreat(p, ctx)).map(p => p.name);
|
|
238
|
+
if (bad.length > 0) msg += `,发现认定坏人${bad.join('、')}`;
|
|
239
|
+
else if (this.witnessExempt) msg += `,附近有${visible.length}个无标记陌生人互为目击者,正常做任务`;
|
|
240
|
+
else msg += `,附近有${visible.length}人,正常做任务`;
|
|
241
|
+
if (tier === 'flee-bad') msg += killCooldownSecs(state) > 0 ? ',刀在冷却,正在回避' : ',正在处理';
|
|
242
|
+
if (cd > 0) msg += `(攻击冷却${cd}s)`;
|
|
243
|
+
if (killsRemaining === 0) msg += '(出刀次数已用完)';
|
|
244
|
+
msg += '。';
|
|
245
|
+
}
|
|
246
|
+
ctx.notifications.push(msg);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { StrategyEntry } from './types.js';
|
|
2
|
-
import { GoalRootStrategy } from './goals/goal-root-strategy.js';
|
|
3
|
-
import { KillFrenzyTop } from './goals/kill-frenzy-top.js';
|
|
4
|
-
|
|
5
|
-
export const strategy: StrategyEntry = {
|
|
6
|
-
id: 'kill-frenzy',
|
|
7
|
-
description:
|
|
8
|
-
'疯狂砍人:见谁杀谁(除队友外所有可见玩家),多人在场也照杀。按房间顺序巡逻,视野里只要有非队友就冲向最近的那个,50内且冷却好就出刀,冷却中保持10距离贴身跟随等冷却。与 kill-lone 不同,不会因为周围人多而暂停攻击。永不停手。',
|
|
9
|
-
create() {
|
|
10
|
-
return new GoalRootStrategy('kill-frenzy', () => new KillFrenzyTop(), { resetOnMeetingResume: false });
|
|
11
|
-
},
|
|
12
|
-
};
|
|
1
|
+
import type { StrategyEntry } from './types.js';
|
|
2
|
+
import { GoalRootStrategy } from './goals/goal-root-strategy.js';
|
|
3
|
+
import { KillFrenzyTop } from './goals/kill-frenzy-top.js';
|
|
4
|
+
|
|
5
|
+
export const strategy: StrategyEntry = {
|
|
6
|
+
id: 'kill-frenzy',
|
|
7
|
+
description:
|
|
8
|
+
'疯狂砍人:见谁杀谁(除队友外所有可见玩家),多人在场也照杀。按房间顺序巡逻,视野里只要有非队友就冲向最近的那个,50内且冷却好就出刀,冷却中保持10距离贴身跟随等冷却。与 kill-lone 不同,不会因为周围人多而暂停攻击。永不停手。',
|
|
9
|
+
create() {
|
|
10
|
+
return new GoalRootStrategy('kill-frenzy', () => new KillFrenzyTop(), { resetOnMeetingResume: false });
|
|
11
|
+
},
|
|
12
|
+
};
|