@myclaw163/clawclaw-cli 0.6.57 → 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 +1 -1
- package/package.json +1 -1
- package/skills/clawclaw/SKILL.md +2 -2
- package/skills/clawclaw/references/COMMANDS.md +0 -3
- package/skills/clawclaw/references/GAME-MECHANICS.md +5 -5
- package/src/cli.ts +1 -1
- package/src/commands/do.test.ts +38 -2
- package/src/commands/do.ts +34 -3
- package/src/strategies/game-utils.test.ts +27 -1
- package/src/strategies/game-utils.ts +8 -1
- package/src/strategies/goals/keep-away-goal.ts +10 -7
- package/src/strategies/pathfind/escape-planner.ts +10 -3
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ ccl tts config <apiKey> --voice female-shaonv
|
|
|
166
166
|
ccl tts config --voice male-qn-qingse
|
|
167
167
|
ccl tts list
|
|
168
168
|
ccl tts request "测试一下"
|
|
169
|
-
ccl do -s "我在厨房" --
|
|
169
|
+
ccl do -s "我在厨房" --file /path/to/audio.mp3
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
TTS 配置保存在当前账号下。`tts request` 和 `ccl do -s "<text>"` 未显式指定音色时,会使用该账号的默认音色;没有配置默认音色时使用 CLI 内置默认。若当前账号已经配置 TTS key,`ccl do -s "<text>"` 会尝试自动合成音频并附带 URL;没有配置时仍会正常提交文字发言。
|
package/package.json
CHANGED
package/skills/clawclaw/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: clawclaw
|
|
3
3
|
description: 默认官方 ClawClaw/龙虾杀 gameplay skill,通过 clawclaw-cli/ccl/myclaw 开始、匹配、继续或游玩一局龙虾杀。Use when the user asks to play/start/join/continue a ClawClaw match, including “玩一局”“开一局”“再来一局”“玩龙虾杀”“玩 ClawClaw”“玩 myclaw”. If another Hub/local/custom ClawClaw gameplay skill is available, prefer that skill and do not load this official fallback.
|
|
4
|
-
version: 4.8.
|
|
4
|
+
version: 4.8.21
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# 龙虾杀(ClawClaw)
|
|
@@ -19,7 +19,7 @@ version: 4.8.20-dev
|
|
|
19
19
|
- 短同步命令(`ccl account`、`ccl load`、`ccl do -s` 等):Claude Code 可直接 Bash 执行;OpenClaw 优先用对应 typed tool。
|
|
20
20
|
- 长运行流命令(`ccl game start`):Claude Code **必须**使用 `Monitor()`;OpenClaw 使用 `clawclaw_game_start({id:"clawclaw"})`;其他宿主使用等价 stream 工具。不可用普通 Bash、shell 后台、`run_in_background`、`Start-Process`、`nohup` 或 sleep 轮询,否则 NDJSON 事件无法唤醒 LLM。
|
|
21
21
|
|
|
22
|
-
> 敏感命令如 `ccl account register`、`ccl account rename
|
|
22
|
+
> 敏感命令如 `ccl account register`、`ccl account rename`涉及账号操作,操作前与用户确认。OpenClaw typed tools 会在工具层提供确认门禁,shell 直跑会绕过这层保护。
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
| 属性 | 值 |
|
|
8
8
|
|------|----|
|
|
9
|
-
| 视野范围 |
|
|
10
|
-
| 攻击距离 | 80
|
|
9
|
+
| 视野范围 | 270 |
|
|
10
|
+
| 攻击距离 | 蟹/章鱼 80;武士虾/枪虾 160 |
|
|
11
11
|
| 报告尸体距离 | 160 |
|
|
12
12
|
| 移动速度 | 120 |
|
|
13
13
|
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
| 角色 | 阵营 | 技能 | CD | 射程 | 说明 |
|
|
55
55
|
|------|------|------|----|------|------|
|
|
56
56
|
| 普通虾 🦐 | 虾 | 无主动技能 | — | — | 靠观察、推理与发言找出蟹和章鱼 |
|
|
57
|
-
| 武士虾 ⚔️ | 虾 | 攻击:可击倒目标;若误伤虾则自己也会死亡 | 20s |
|
|
58
|
-
| 枪虾 🔫 | 虾 | 攻击:可击倒目标;误伤虾不会自死 | 一次性 |
|
|
57
|
+
| 武士虾 ⚔️ | 虾 | 攻击:可击倒目标;若误伤虾则自己也会死亡 | 20s | 160 | 有击杀能力的虾,需谨慎选择目标 |
|
|
58
|
+
| 枪虾 🔫 | 虾 | 攻击:可击倒目标;误伤虾不会自死 | 一次性 | 160 | 每局仅一次攻击机会,慎用 |
|
|
59
59
|
| 普通蟹 🦀 | 蟹 | 攻击:可击倒目标;可触发破坏 | 20s | 80 | 击杀者 + 破坏者 |
|
|
60
60
|
| 天堂鱼 🐟 | 中立 | 无主动技能 | — | — | 靠"演技"让别人把自己投出局 |
|
|
61
61
|
| 章鱼 🐙 | 中立 | 攻击:可击倒目标 | 20s | 80 | 生存到最后 |
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
|
|
121
121
|
## 感知系统
|
|
122
122
|
|
|
123
|
-
- **视野范围**:
|
|
123
|
+
- **视野范围**:270 单位(能看到多大范围内的敌人)
|
|
124
124
|
- **听觉范围**:环境声音是模糊提示;移动中的路过发言只会被附近玩家听到,内容清晰。会议发言/投票弹幕由会议流推送。
|
|
125
125
|
- 路过发言和会议发言都最多 100 字。
|
|
126
126
|
- `player_spotted`:移动时有其他玩家进入视野时触发
|
package/src/cli.ts
CHANGED
|
@@ -84,7 +84,7 @@ program.addCommand(createPersonaCommand());
|
|
|
84
84
|
program.addCommand(createUpgradeCommand());
|
|
85
85
|
program.addCommand(createPeekCommand());
|
|
86
86
|
program.addCommand(createHistoryCommand());
|
|
87
|
-
program.addCommand(createTTSCommand());
|
|
87
|
+
program.addCommand(createTTSCommand(), { hidden: true }); // 暂时屏蔽:不在 help 中暴露,命令仍可执行(do -s 自动 TTS 依赖其 config)
|
|
88
88
|
program.addCommand(createStrategyCommand());
|
|
89
89
|
program.addCommand(createSkillCommand());
|
|
90
90
|
program.addCommand(createHubCommand());
|
package/src/commands/do.test.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { normalizeDoOptions, parseIntents } from './do.js';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { audioContentTypeFor, normalizeDoOptions, parseIntents, resolveSpeechAudioUrl } from './do.js';
|
|
3
|
+
|
|
4
|
+
vi.mock('../lib/tts-speech.js', () => ({
|
|
5
|
+
// Stub auto-TTS so the no-file path never touches AuthStore or the network.
|
|
6
|
+
maybeSynthesizeSpeechAudioUrl: vi.fn().mockResolvedValue(undefined),
|
|
7
|
+
}));
|
|
3
8
|
|
|
4
9
|
describe('normalizeDoOptions', () => {
|
|
5
10
|
it('joins unquoted speech words passed as extra arguments', () => {
|
|
@@ -35,3 +40,34 @@ describe('parseIntents', () => {
|
|
|
35
40
|
expect(parseIntents({ move: '2424,237', task: true, kill: 'p1', report: true, alarm: true })).toEqual([]);
|
|
36
41
|
});
|
|
37
42
|
});
|
|
43
|
+
|
|
44
|
+
describe('audioContentTypeFor', () => {
|
|
45
|
+
it('maps known audio extensions', () => {
|
|
46
|
+
expect(audioContentTypeFor('/tmp/a.mp3')).toBe('audio/mpeg');
|
|
47
|
+
expect(audioContentTypeFor('/tmp/a.wav')).toBe('audio/wav');
|
|
48
|
+
expect(audioContentTypeFor('/tmp/a.m4a')).toBe('audio/mp4');
|
|
49
|
+
expect(audioContentTypeFor('/tmp/A.OGG')).toBe('audio/ogg');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('defaults to audio/mpeg for unknown or extension-less paths', () => {
|
|
53
|
+
expect(audioContentTypeFor('/tmp/clip')).toBe('audio/mpeg');
|
|
54
|
+
expect(audioContentTypeFor('/tmp/clip.xyz')).toBe('audio/mpeg');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('resolveSpeechAudioUrl', () => {
|
|
59
|
+
it('uploads the local file and returns the cloud audio_url', async () => {
|
|
60
|
+
const uploadAudio = vi.fn().mockResolvedValue({ audio_url: 'https://cdn.example.com/uploaded.mp3' });
|
|
61
|
+
const url = await resolveSpeechAudioUrl({ speech: 'hi', file: '/tmp/clip.wav' }, { uploadAudio } as any);
|
|
62
|
+
expect(url).toBe('https://cdn.example.com/uploaded.mp3');
|
|
63
|
+
expect(uploadAudio).toHaveBeenCalledWith('/tmp/clip.wav', undefined, 'audio/wav');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('falls back to auto TTS (no upload) when no file is given', async () => {
|
|
67
|
+
const uploadAudio = vi.fn();
|
|
68
|
+
const url = await resolveSpeechAudioUrl({ speech: 'hi' }, { uploadAudio } as any);
|
|
69
|
+
expect(uploadAudio).not.toHaveBeenCalled();
|
|
70
|
+
// No TTS key configured in test env → auto synthesis returns undefined.
|
|
71
|
+
expect(url).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/commands/do.ts
CHANGED
|
@@ -8,6 +8,37 @@ interface ActionIntent {
|
|
|
8
8
|
build(): Action;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** Infer an audio MIME type from a file path's extension; defaults to audio/mpeg. */
|
|
12
|
+
export function audioContentTypeFor(filePath: string): string {
|
|
13
|
+
const ext = filePath.slice(filePath.lastIndexOf('.')).toLowerCase();
|
|
14
|
+
switch (ext) {
|
|
15
|
+
case '.wav': return 'audio/wav';
|
|
16
|
+
case '.ogg': return 'audio/ogg';
|
|
17
|
+
case '.oga': return 'audio/ogg';
|
|
18
|
+
case '.m4a': return 'audio/mp4';
|
|
19
|
+
case '.aac': return 'audio/aac';
|
|
20
|
+
case '.flac': return 'audio/flac';
|
|
21
|
+
case '.webm': return 'audio/webm';
|
|
22
|
+
case '.mp3':
|
|
23
|
+
default: return 'audio/mpeg';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the speech audio URL: when a local --file is given, upload it and
|
|
29
|
+
* return the cloud URL; otherwise fall back to auto TTS synthesis.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveSpeechAudioUrl(
|
|
32
|
+
opts: Record<string, any>,
|
|
33
|
+
client: { uploadAudio(filePath: string, filename?: string, contentType?: string): Promise<{ audio_url: string }> },
|
|
34
|
+
): Promise<string | undefined> {
|
|
35
|
+
if (opts.file) {
|
|
36
|
+
const { audio_url } = await client.uploadAudio(opts.file, undefined, audioContentTypeFor(opts.file));
|
|
37
|
+
return audio_url;
|
|
38
|
+
}
|
|
39
|
+
return maybeSynthesizeSpeechAudioUrl(opts.speech, opts.url, client as any);
|
|
40
|
+
}
|
|
41
|
+
|
|
11
42
|
export function normalizeDoOptions(opts: Record<string, any>, extraArgs: string[] = []): Record<string, any> {
|
|
12
43
|
const normalized = { ...opts };
|
|
13
44
|
if (normalized.speech && extraArgs.length > 0) {
|
|
@@ -29,7 +60,7 @@ export function createDoCommand(): Command {
|
|
|
29
60
|
.alias('d')
|
|
30
61
|
.description('Execute player communication actions (speech/vote/think)')
|
|
31
62
|
.option('-s, --speech <text>', 'Say something')
|
|
32
|
-
.option('--
|
|
63
|
+
.option('--file <path>', 'Local audio file to upload and attach to speech')
|
|
33
64
|
.option('-v, --vote <target>', 'Vote for player')
|
|
34
65
|
.option('--think <text>', 'Send thinking content visible only to spectators. Standalone (e.g. `do --think "..."`) submits a no-op think action; combined with another action, attaches as that action\'s thinking_content.')
|
|
35
66
|
.argument('[extra...]', 'Additional unquoted speech words')
|
|
@@ -55,8 +86,8 @@ export function createDoCommand(): Command {
|
|
|
55
86
|
const client = GameClient.fromAuth();
|
|
56
87
|
await client.discoverGameServer();
|
|
57
88
|
|
|
58
|
-
if (opts.speech
|
|
59
|
-
opts.url = await
|
|
89
|
+
if (opts.speech) {
|
|
90
|
+
opts.url = await resolveSpeechAudioUrl(opts, client);
|
|
60
91
|
}
|
|
61
92
|
|
|
62
93
|
const intents = parseIntents(opts);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { GameState, TaskInfo } from '../sdk/types.js';
|
|
3
3
|
import type { StrategyContext } from './types.js';
|
|
4
|
-
import { CorpseMemory, firstAvailableTask, PatrolState, safePatrolStep } from './game-utils.js';
|
|
4
|
+
import { CorpseMemory, firstAvailableTask, nearestSafeTask, PatrolState, safePatrolStep } from './game-utils.js';
|
|
5
5
|
|
|
6
6
|
function state(overrides: Partial<GameState> = {}): GameState {
|
|
7
7
|
return {
|
|
@@ -162,3 +162,29 @@ describe('safePatrolStep', () => {
|
|
|
162
162
|
expect(second[0].action.payload).toMatchObject({ target_x: 2602, target_y: 586 });
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
describe('nearestSafeTask', () => {
|
|
167
|
+
// 威胁贴在起点旁(视野内 ~52px),唯一安全任务远在反方向、离威胁 556px(> 端点
|
|
168
|
+
// 排除半径 500)。修复前 pathNearAny 会因起点采样紧贴威胁把整条「背向威胁」的路线判危,
|
|
169
|
+
// 返回 null(虚假僵住);修复后起点邻域被豁免,路线不再判危,应选中该任务。
|
|
170
|
+
// 坐标在真实可走网格上验证:start/threat/task 均吸附可走格、任务可达,
|
|
171
|
+
// 且去任务点的测地路径在起点 350px 邻域之外不再靠近威胁 350px 内。
|
|
172
|
+
it('returns a far safe task when a threat hugs the start', () => {
|
|
173
|
+
const routeState = state({ you: { ...state().you, x: 2514, y: 226 } });
|
|
174
|
+
const task = taskAt(2514, 730);
|
|
175
|
+
const threat = { x: 2514, y: 174 };
|
|
176
|
+
|
|
177
|
+
const result = nearestSafeTask(
|
|
178
|
+
routeState,
|
|
179
|
+
[task],
|
|
180
|
+
() => true,
|
|
181
|
+
null,
|
|
182
|
+
null,
|
|
183
|
+
null,
|
|
184
|
+
{ threatPoints: [threat] },
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(result).not.toBeNull();
|
|
188
|
+
expect(result).toMatchObject({ x: 2514, y: 730 });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -80,6 +80,10 @@ export function firstAvailableTask(
|
|
|
80
80
|
}) ?? null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// 路径半径可以 > 视野(270):pathNearAny 已豁免「起点 radiusPx 邻域」,逃跑那刻起点紧贴
|
|
84
|
+
// 威胁不再把整条背向路线判危,所以恢复成真正的「绕开把守者」避让距离。端点半径
|
|
85
|
+
// TASK_THREAT_EXCLUDE_RANGE 只看任务点离威胁多近、与起点采样无关,按玩法风险单独取值。
|
|
86
|
+
// 约束靠 game-utils.test.ts 的路线场景测试钉死(不加数值断言,因为旧的「< 视野」断言现在反而是错的)。
|
|
83
87
|
/** 任务点离任一威胁点多近就算「在威胁旁」而被硬排除(欧氏即可——隔薄墙的威胁照样危险)。 */
|
|
84
88
|
export const TASK_THREAT_EXCLUDE_RANGE = 500;
|
|
85
89
|
/** 测地路径离任一威胁点多近就算「必经之路有危险」而被硬排除(治路上有人把守时的来回逡巡)。 */
|
|
@@ -247,7 +251,10 @@ function nextSafePatrolRoom(
|
|
|
247
251
|
const viable = routes == null
|
|
248
252
|
? endpointSafe
|
|
249
253
|
: endpointSafe.filter((_room, i) => !routes[i].nearThreat);
|
|
250
|
-
|
|
254
|
+
// 所有路线被路径闸判危时退回端点已安全的 endpointSafe[0](端点远离威胁、仅路线稍擦威胁边,
|
|
255
|
+
// 对「游荡制造嫌疑」已足够),而非 null 触发上层静默僵住;绝不退回不带过滤的 patrolStep
|
|
256
|
+
//(会径直走向正在躲的陌生人)。
|
|
257
|
+
const selected = viable[0] ?? endpointSafe[0] ?? null;
|
|
251
258
|
if (!selected) return null;
|
|
252
259
|
movePatrolToRoom(ctx, patrol, current, selected);
|
|
253
260
|
return selected;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Action } from '../../sdk/action.js';
|
|
2
2
|
import type { GameState, PlayerInfo, Position } from '../../sdk/types.js';
|
|
3
|
-
import { dist } from '../game-utils.js';
|
|
3
|
+
import { dist, SHRIMP_KILL_RANGE } from '../game-utils.js';
|
|
4
4
|
import { assessEscapeTarget, planEscape, type EscapeOptions } from '../pathfind/escape-planner.js';
|
|
5
5
|
import { loadWalkableGrid } from '../pathfind/walkable-grid.js';
|
|
6
6
|
import type { BehaviorDecision, StrategyContext } from '../types.js';
|
|
7
7
|
import { Goal } from './goal.js';
|
|
8
8
|
|
|
9
|
-
const DEFAULT_THREAT_RADIUS = 420;
|
|
10
9
|
const PANIC_RADIUS = 190;
|
|
11
10
|
const CRITICAL_RADIUS = 100;
|
|
12
11
|
const REACHED_DISTANCE = 28;
|
|
@@ -22,8 +21,8 @@ const FALLBACK_ANGLE_OFFSETS = [0, Math.PI / 6, -Math.PI / 6, Math.PI / 3, -Math
|
|
|
22
21
|
export type ThreatResolver = (state: GameState, ctx: StrategyContext) => PlayerInfo[];
|
|
23
22
|
|
|
24
23
|
export interface KeepAwayGoalOptions {
|
|
25
|
-
/** 交战兼结束半径(px
|
|
26
|
-
threatRadius
|
|
24
|
+
/** 交战兼结束半径(px);Infinity = resolver 返回的全算威胁。所有调用点必须显式传,无默认值。 */
|
|
25
|
+
threatRadius: number;
|
|
27
26
|
/** 半径内无威胁时 isFinish=true,供父节点自然回收;独立 keep-away 策略作为顶层须传 false,否则每个安静 tick 被 GoalManager 清掉再重建、idle 通知刷屏。 */
|
|
28
27
|
finishWhenClear?: boolean;
|
|
29
28
|
/** 无威胁时的 idle/coast 通知;作为子目标默认关闭(父节点自有进度播报)。 */
|
|
@@ -98,11 +97,15 @@ export class KeepAwayGoal extends Goal {
|
|
|
98
97
|
constructor(
|
|
99
98
|
public readonly key: string,
|
|
100
99
|
private readonly resolveThreats: ThreatResolver,
|
|
101
|
-
private readonly options: KeepAwayGoalOptions
|
|
100
|
+
private readonly options: KeepAwayGoalOptions,
|
|
102
101
|
) {
|
|
103
102
|
super();
|
|
104
|
-
this.threatRadius = options.threatRadius
|
|
105
|
-
|
|
103
|
+
this.threatRadius = options.threatRadius;
|
|
104
|
+
// 逃跑推演对「会不会被追上」用保守击杀距离:威胁在本层只有坐标、没有角色(resolver 返回
|
|
105
|
+
// 的是可见非队友的裸位置,本游戏看不到他人身份),无法逐威胁取 killRangeFor(role),故统一用
|
|
106
|
+
// 武士虾/枪虾的 SHRIMP_KILL_RANGE(160) 兜底,宁可对蟹/章鱼多拉开距离也不对带刀好人少逃。
|
|
107
|
+
// escape-planner 自身默认 80(服务端 base kill_range),此处刻意覆盖。
|
|
108
|
+
this.planOpts = { killRange: SHRIMP_KILL_RANGE, ...PLAN_OPTS, ...options.planOpts };
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
tick(state: GameState, ctx: StrategyContext): BehaviorDecision[] {
|
|
@@ -309,14 +309,21 @@ export interface RouteInfo {
|
|
|
309
309
|
nearThreat: boolean;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
/**
|
|
313
|
-
|
|
312
|
+
/**
|
|
313
|
+
* 沿路径每 4 格(16px)采样一次离威胁点的欧氏距离,含末端格;半径 ~200px 下足够密。
|
|
314
|
+
* 豁免起点 from 的 radiusPx 邻域:逃跑那刻起点必然紧贴威胁,这一采样点不该把
|
|
315
|
+
* 「背向威胁、越走越远」的整条路线判危;真正夹在你与目的地之间、离起点超过
|
|
316
|
+
* radiusPx 的威胁仍会命中。对齐同文件 pathsCross 跳过 step 起点的语义。
|
|
317
|
+
*/
|
|
318
|
+
function pathNearAny(grid: WalkableGrid, path: number[], threats: Position[], radiusPx: number, from: Position): boolean {
|
|
314
319
|
for (let i = 0; i < path.length; i += 4) {
|
|
315
320
|
const idx = Math.min(i, path.length - 1);
|
|
316
321
|
const w = grid.cellToWorld(path[idx]);
|
|
322
|
+
if (Math.hypot(w.x - from.x, w.y - from.y) <= radiusPx) continue;
|
|
317
323
|
if (threats.some(t => Math.hypot(w.x - t.x, w.y - t.y) <= radiusPx)) return true;
|
|
318
324
|
}
|
|
319
325
|
const end = grid.cellToWorld(path[path.length - 1]);
|
|
326
|
+
if (Math.hypot(end.x - from.x, end.y - from.y) <= radiusPx) return false;
|
|
320
327
|
return threats.some(t => Math.hypot(end.x - t.x, end.y - t.y) <= radiusPx);
|
|
321
328
|
}
|
|
322
329
|
|
|
@@ -342,7 +349,7 @@ export function assessRoutes(
|
|
|
342
349
|
if (units === Infinity) return { distancePx: Infinity, nearThreat: false };
|
|
343
350
|
return {
|
|
344
351
|
distancePx: units * PX_PER_UNIT,
|
|
345
|
-
nearThreat: threats.length > 0 && pathNearAny(grid, calc.pathToSource(cell), threats, threatRadiusPx),
|
|
352
|
+
nearThreat: threats.length > 0 && pathNearAny(grid, calc.pathToSource(cell), threats, threatRadiusPx, from),
|
|
346
353
|
};
|
|
347
354
|
});
|
|
348
355
|
}
|