fix: 定位「寻路恒定 47 节点」的根因(专属配置被冻结)并改为按进度判超时
找到「寻路怎么调都只探索 47 个节点」的真凶 - 启动任务时若带了配置,会被整份存为该机器人的专属配置覆盖。它是一份**完整快照**, 等于把当时那一刻的全局配置冻结在这个机器人身上,之后改全局配置再也影响不到它。 实测这台机器人的覆盖里 allowBreak/allowParkour 都是 false,所以我前面所有 「放开挖方块 / 跑酷 / 不限搜索半径」的排查全部无效,一直在跟同一份旧配置较劲。 - 清除覆盖后寻路器的邻居生成数从 **2 变成 15**,机器人开始真正移动。 - 现在保存覆盖时会明确记一条日志(含策略/邦国/数量),说明「此后全局改动不影响它」。 - 新增「清除专属配置」按钮与 POST /api/bots/:id/settings/clear-config, 并在配置面板提示该机器人是否有专属覆盖 —— 这是「改了全局却没生效」的唯一退路。 寻路超时改为按进度判定 - 原来只看墙钟时间:目标常在几百格外,而寻路速度受地形与反作弊回弹影响可能只有 正常步行的十分之一,于是「走得慢但在稳步接近」会被误判成卡死,然后 /u return 回城把进度全部清零。现在只要与目标的曼哈顿距离有实质缩短就重置这一段计时。 - 首次观察只记录距离基准、不续期,保证「原地不动」仍会照常超时。 新增寻路诊断(排查这类问题的关键手段) - GET /api/bots/:id/status 的 pathingDiag 暴露:起点可走邻居数与具体坐标、 寻路器能否通过 world.getBlock 读到方块、四向可见距离。 - 邻居数极少(如 2)说明不是目标太远,而是移动生成器从起点就找不到分支, 此时调任何搜索参数都没用。 测试 355 -> 360,全部通过。
这个提交包含在:
@@ -852,6 +852,10 @@ class BotManager extends EventEmitter {
|
|||||||
packetStats: Object.assign({}, this.packetStats),
|
packetStats: Object.assign({}, this.packetStats),
|
||||||
world: this._worldDiagnostics(),
|
world: this._worldDiagnostics(),
|
||||||
pathing: this.pathing ? this.pathing.isPathing() : false,
|
pathing: this.pathing ? this.pathing.isPathing() : false,
|
||||||
|
/** 寻路诊断:邻居生成数、世界数据可读性、四向可见距离 */
|
||||||
|
pathingDiag: this.pathing && typeof this.pathing.diagnostics === 'function'
|
||||||
|
? this.pathing.diagnostics()
|
||||||
|
: null,
|
||||||
eating: this.eater ? this.eater.eating : false,
|
eating: this.eater ? this.eater.eating : false,
|
||||||
grim: this.grim ? this.grim.snapshot() : null
|
grim: this.grim ? this.grim.snapshot() : null
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-1
@@ -421,17 +421,43 @@ class BotPool extends EventEmitter {
|
|||||||
? normalizeConfig(overrideConfig, this.getConfigFor(id))
|
? normalizeConfig(overrideConfig, this.getConfigFor(id))
|
||||||
: this.getConfigFor(id);
|
: this.getConfigFor(id);
|
||||||
if (overrideConfig) {
|
if (overrideConfig) {
|
||||||
// 控制台点「开始扩地」时顺带保存该机器人的配置
|
// 控制台点「开始扩地」且勾了「仅对当前机器人生效」时,顺带把这份配置存为该机器人的专属配置。
|
||||||
|
// 注意这是一份**完整快照**:存下之后全局配置的改动就不再影响它了,
|
||||||
|
// 所以这里明确记一条日志,避免以后排查时一头雾水。
|
||||||
|
const previous = this.settings[id] && this.settings[id].config ? '(覆盖已有)' : '(新建)';
|
||||||
this.settings[id] = Object.assign({}, this.settings[id] || {}, { config });
|
this.settings[id] = Object.assign({}, this.settings[id] || {}, { config });
|
||||||
this._persistSettings();
|
this._persistSettings();
|
||||||
entry.config = config;
|
entry.config = config;
|
||||||
entry.manager.applyConfig(config);
|
entry.manager.applyConfig(config);
|
||||||
|
this._log(`${entry.connection.username} 已保存专属配置${previous}:`
|
||||||
|
+ `策略 ${config.strategy}、目标邦国「${config.stateName || '自动'}」、目标数量 ${config.targetCount};`
|
||||||
|
+ '此后全局配置的改动不会影响它,可在配置面板点「清除专属配置」恢复继承');
|
||||||
}
|
}
|
||||||
entry.controller.config = config;
|
entry.controller.config = config;
|
||||||
const ok = await entry.controller.start(config);
|
const ok = await entry.controller.start(config);
|
||||||
return { ok, message: ok ? '任务已启动' : entry.controller.status, entry };
|
return { ok, message: ok ? '任务已启动' : entry.controller.status, entry };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除某机器人的专属配置覆盖,恢复「继承全局默认配置」。
|
||||||
|
*
|
||||||
|
* 专属配置是一份完整快照,一旦存下,之后改全局配置就再也影响不到这个机器人 ——
|
||||||
|
* 很容易出现「我明明改了全局设置,它却还是老行为」的困惑,所以需要一条明确的退路。
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {object} 生效后的设置快照
|
||||||
|
*/
|
||||||
|
clearBotConfigOverride(id) {
|
||||||
|
const entry = this.require(id);
|
||||||
|
if (this.settings[id]) delete this.settings[id].config;
|
||||||
|
this._persistSettings();
|
||||||
|
entry.config = this.getConfigFor(id);
|
||||||
|
entry.manager.applyConfig(entry.config);
|
||||||
|
entry.controller.config = entry.config;
|
||||||
|
this._log(`${entry.connection.username} 的专属配置已清除,恢复继承全局默认配置`);
|
||||||
|
return this.settingSnapshot(id);
|
||||||
|
}
|
||||||
|
|
||||||
/** 停止某机器人的扩地任务 */
|
/** 停止某机器人的扩地任务 */
|
||||||
stopTask(id) {
|
stopTask(id) {
|
||||||
const entry = this.require(id);
|
const entry = this.require(id);
|
||||||
|
|||||||
+63
-2
@@ -325,9 +325,70 @@ class PathingService {
|
|||||||
return lo;
|
return lo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 寻路诊断:直接问寻路器「从当前位置能往哪些格子走」。
|
||||||
|
*
|
||||||
|
* 这是排查「A* 只探索几十个节点就 noPath」的关键手段:
|
||||||
|
* 如果 `neighbors` 为 0 或极少,说明不是目标太远,而是**寻路器读不到世界数据**
|
||||||
|
* (Movements 依赖 bot.world 的方块查询),此时调任何搜索参数都没用。
|
||||||
|
*
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
diagnostics() {
|
||||||
|
const bot = this.bot;
|
||||||
|
if (!bot || !bot.entity || !bot.pathfinder) return null;
|
||||||
|
const pos = bot.entity.position;
|
||||||
|
const feet = { x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) };
|
||||||
|
let neighbors = [];
|
||||||
|
let error = '';
|
||||||
|
try {
|
||||||
|
const movements = bot.pathfinder.movements;
|
||||||
|
// 寻路器内部用的就是这种节点结构(x/y/z + 代价字段)
|
||||||
|
neighbors = movements.getNeighbors({
|
||||||
|
x: feet.x,
|
||||||
|
y: feet.y,
|
||||||
|
z: feet.z,
|
||||||
|
remainingBlocks: 0,
|
||||||
|
cost: 0,
|
||||||
|
hash: `${feet.x},${feet.y},${feet.z}`
|
||||||
|
}) || [];
|
||||||
|
} catch (err) {
|
||||||
|
error = err && err.message ? err.message : String(err);
|
||||||
|
}
|
||||||
|
const world = bot.world;
|
||||||
|
const worldApi = {
|
||||||
|
hasGetBlock: typeof world.getBlock === 'function',
|
||||||
|
hasGetBlockStateId: typeof world.getBlockStateId === 'function',
|
||||||
|
hasGetColumn: typeof world.getColumn === 'function'
|
||||||
|
};
|
||||||
|
// 用寻路器同款 API 采样脚下方块,看它到底能不能读到世界
|
||||||
|
let blockViaWorld = null;
|
||||||
|
try {
|
||||||
|
const b = world.getBlock ? world.getBlock(bot.entity.position.offset(0, -1, 0)) : null;
|
||||||
|
blockViaWorld = b ? b.name : null;
|
||||||
|
} catch (err) {
|
||||||
|
blockViaWorld = 'ERR:' + (err && err.message ? err.message : String(err));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
feet,
|
||||||
|
neighborCount: neighbors.length,
|
||||||
|
neighbors: neighbors.slice(0, 8).map((n) => ({ x: n.x, y: n.y, z: n.z, cost: n.cost })),
|
||||||
|
error,
|
||||||
|
worldApi,
|
||||||
|
blockViaWorld,
|
||||||
|
blockViaBlockAt: bot.blockAt ? (bot.blockAt(bot.entity.position.offset(0, -1, 0)) || {}).name : null,
|
||||||
|
/** 四个方向能「看到」多远(方块):反映已加载区块范围 */
|
||||||
|
visibleDistance: {
|
||||||
|
north: Math.round(this._probeLoadedDistance(0, -1, 256)),
|
||||||
|
south: Math.round(this._probeLoadedDistance(0, 1, 256)),
|
||||||
|
east: Math.round(this._probeLoadedDistance(1, 0, 256)),
|
||||||
|
west: Math.round(this._probeLoadedDistance(-1, 0, 256))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 取消当前寻路 */
|
/** 取消当前寻路 */
|
||||||
cancel() {
|
cancel() { if (!this.isAvailable()) return;
|
||||||
if (!this.isAvailable()) return;
|
|
||||||
this._pathing = false;
|
this._pathing = false;
|
||||||
this.currentTarget = null;
|
this.currentTarget = null;
|
||||||
this.finalTarget = null;
|
this.finalTarget = null;
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ const State = {
|
|||||||
/** 环形日志中「导出日志」用的状态摘要键名 */
|
/** 环形日志中「导出日志」用的状态摘要键名 */
|
||||||
const EXPORT_SUMMARY_KEYS = ['状态', '目标区块', '玩家区块', '队列', '已占领', '重试', '最近进度'];
|
const EXPORT_SUMMARY_KEYS = ['状态', '目标区块', '玩家区块', '队列', '已占领', '重试', '最近进度'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判定「寻路有进展」的最小距离变化(区块)。
|
||||||
|
*
|
||||||
|
* 只要与目标的曼哈顿距离缩短了这么多,就认为是在稳步接近而不是卡死,
|
||||||
|
* 从而重置超时计时。取 1 是因为矿区/建筑区里寻路速度可能远低于正常步行,
|
||||||
|
* 阈值再大就会把慢速推进误判成超时。
|
||||||
|
*/
|
||||||
|
const PATH_PROGRESS_MIN_CHUNKS = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自动扩地控制器:扩地任务的核心状态机。
|
* 自动扩地控制器:扩地任务的核心状态机。
|
||||||
*
|
*
|
||||||
@@ -90,6 +99,8 @@ class ExpansionController {
|
|||||||
this.retryCount = 0;
|
this.retryCount = 0;
|
||||||
/** 当前阶段(寻路 / 等待)的开始时间戳 */
|
/** 当前阶段(寻路 / 等待)的开始时间戳 */
|
||||||
this.stageStartedMillis = 0;
|
this.stageStartedMillis = 0;
|
||||||
|
/** 上一次记录的「与目标的曼哈顿距离」,用于判断寻路是否在稳步推进 */
|
||||||
|
this._lastTargetDistance = null;
|
||||||
/** 最近一次开垦进度(0-100) */
|
/** 最近一次开垦进度(0-100) */
|
||||||
this.lastProgress = 0;
|
this.lastProgress = 0;
|
||||||
|
|
||||||
@@ -344,6 +355,8 @@ class ExpansionController {
|
|||||||
*/
|
*/
|
||||||
async plan() {
|
async plan() {
|
||||||
const bm = this.botManager;
|
const bm = this.botManager;
|
||||||
|
// 重新规划意味着目标可能变了(或被回城传送重置了位置),进度基准必须清空
|
||||||
|
this._lastTargetDistance = null;
|
||||||
if (this.config.targetCount > 0 && this.completedClaims >= this.config.targetCount) {
|
if (this.config.targetCount > 0 && this.completedClaims >= this.config.targetCount) {
|
||||||
this.finish();
|
this.finish();
|
||||||
return;
|
return;
|
||||||
@@ -436,6 +449,20 @@ class ExpansionController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const elapsedSeconds = Math.floor((Date.now() - this.stageStartedMillis) / 1000);
|
const elapsedSeconds = Math.floor((Date.now() - this.stageStartedMillis) / 1000);
|
||||||
|
// 「有没有在推进」比「花了多久」更能说明问题:
|
||||||
|
// 目标常常在几百格外,而寻路速度受地形与反作弊回弹影响可能只有正常步行的十分之一,
|
||||||
|
// 只看墙钟时间会把「走得慢但在稳步接近」误判成卡死,然后回城传送把进度全部清零。
|
||||||
|
// 因此只要与目标的曼哈顿距离有实质缩短,就重置这一段计时。
|
||||||
|
const distance = chunkPos.manhattan(this.playerChunk, this.target);
|
||||||
|
if (this._lastTargetDistance === null) {
|
||||||
|
// 首次观察只记基准,不动计时 —— 否则每个目标的第一 tick 都会被无条件续期
|
||||||
|
this._lastTargetDistance = distance;
|
||||||
|
} else if (distance <= this._lastTargetDistance - PATH_PROGRESS_MIN_CHUNKS) {
|
||||||
|
this._lastTargetDistance = distance;
|
||||||
|
this.stageStartedMillis = Date.now();
|
||||||
|
this.consecutiveTimeoutMillis = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (elapsedSeconds >= this.config.waitTimeoutSeconds) {
|
if (elapsedSeconds >= this.config.waitTimeoutSeconds) {
|
||||||
this.retryCount++;
|
this.retryCount++;
|
||||||
this.dataService.refreshAsync();
|
this.dataService.refreshAsync();
|
||||||
|
|||||||
+41
-5
@@ -510,19 +510,42 @@ function collectConfig() {
|
|||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
if ($('chkBotOverride').checked && selectedBotId) {
|
if (selectedBotId) {
|
||||||
const res = await api('/api/bots/' + encodeURIComponent(selectedBotId) + '/settings');
|
const res = await api('/api/bots/' + encodeURIComponent(selectedBotId) + '/settings');
|
||||||
currentConfig = res.data.config;
|
updateOverrideHint(res.data);
|
||||||
} else {
|
if ($('chkBotOverride').checked) {
|
||||||
const res = await api('/api/config');
|
currentConfig = res.data.config;
|
||||||
currentConfig = res.data;
|
renderConfig();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const res = await api('/api/config');
|
||||||
|
currentConfig = res.data;
|
||||||
renderConfig();
|
renderConfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast('读取配置失败:' + err.message, true);
|
toast('读取配置失败:' + err.message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提示「该机器人是否有一份专属配置覆盖」。
|
||||||
|
*
|
||||||
|
* 专属配置是一份完整快照,存下之后全局配置的改动就再也影响不到这个机器人 ——
|
||||||
|
* 不提示的话,很容易出现「我明明改了全局设置,它却还是老行为」的困惑。
|
||||||
|
*
|
||||||
|
* @param {object} setting `/api/bots/:id/settings` 的 data
|
||||||
|
*/
|
||||||
|
function updateOverrideHint(setting) {
|
||||||
|
const hint = $('overrideHint');
|
||||||
|
const text = $('overrideHintText');
|
||||||
|
if (!hint || !text) return;
|
||||||
|
const has = !!(setting && setting.hasOverrides);
|
||||||
|
hint.classList.toggle('hidden', !has);
|
||||||
|
if (has) {
|
||||||
|
text.textContent = '该机器人有一份专属配置(完整快照),全局配置的改动不会影响它。';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
try {
|
try {
|
||||||
const config = collectConfig();
|
const config = collectConfig();
|
||||||
@@ -2494,6 +2517,19 @@ function bindEvents() {
|
|||||||
$('btnSaveConfig').addEventListener('click', saveConfig);
|
$('btnSaveConfig').addEventListener('click', saveConfig);
|
||||||
$('btnReloadConfig').addEventListener('click', loadConfig);
|
$('btnReloadConfig').addEventListener('click', loadConfig);
|
||||||
$('chkBotOverride').addEventListener('change', loadConfig);
|
$('chkBotOverride').addEventListener('change', loadConfig);
|
||||||
|
$('btnClearOverride').addEventListener('click', async () => {
|
||||||
|
if (!selectedBotId) return;
|
||||||
|
try {
|
||||||
|
const res = await api('/api/bots/' + encodeURIComponent(selectedBotId) + '/settings/clear-config', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
toast(res.message || '已清除');
|
||||||
|
updateOverrideHint(res.data);
|
||||||
|
await loadConfig();
|
||||||
|
} catch (err) {
|
||||||
|
toast('清除失败:' + err.message, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$('configTabs').addEventListener('click', (e) => {
|
$('configTabs').addEventListener('click', (e) => {
|
||||||
const tab = e.target.closest('.tab');
|
const tab = e.target.closest('.tab');
|
||||||
|
|||||||
@@ -309,6 +309,10 @@
|
|||||||
<input type="checkbox" id="chkBotOverride">
|
<input type="checkbox" id="chkBotOverride">
|
||||||
仅对当前机器人生效(不勾选则写入全局默认配置)
|
仅对当前机器人生效(不勾选则写入全局默认配置)
|
||||||
</label>
|
</label>
|
||||||
|
<p class="hint hidden" id="overrideHint">
|
||||||
|
<span id="overrideHintText"></span>
|
||||||
|
<button id="btnClearOverride" class="btn btn-sm">清除专属配置</button>
|
||||||
|
</p>
|
||||||
<div class="tabs" id="configTabs">
|
<div class="tabs" id="configTabs">
|
||||||
<button class="tab active" data-tab="strategy">策略</button>
|
<button class="tab active" data-tab="strategy">策略</button>
|
||||||
<button class="tab" data-tab="timing">超时</button>
|
<button class="tab" data-tab="timing">超时</button>
|
||||||
|
|||||||
@@ -305,6 +305,19 @@ class WebConsole {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 清除专属配置覆盖,恢复继承全局默认配置
|
||||||
|
app.post('/api/bots/:id/settings/clear-config', (req, res) => {
|
||||||
|
if (typeof this.pool.clearBotConfigOverride !== 'function') {
|
||||||
|
res.status(400).json({ ok: false, message: '当前运行模式不支持清除专属配置' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._withEntry(req, res, (entry) => {
|
||||||
|
const saved = this.pool.clearBotConfigOverride(entry.id);
|
||||||
|
res.json({ ok: true, data: saved, message: '已清除专属配置,恢复继承全局默认配置' });
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/bots/:id/settings', (req, res) => {
|
app.post('/api/bots/:id/settings', (req, res) => {
|
||||||
this._withEntry(req, res, (entry) => {
|
this._withEntry(req, res, (entry) => {
|
||||||
const body = req.body || {};
|
const body = req.body || {};
|
||||||
|
|||||||
@@ -257,6 +257,42 @@ test('startTask 带配置覆盖时写入该机器人的专属配置', async () =
|
|||||||
assert.strictEqual(config.targetCount, 5);
|
assert.strictEqual(config.targetCount, 5);
|
||||||
// 另一个机器人不受影响
|
// 另一个机器人不受影响
|
||||||
assert.strictEqual(pool.getConfigFor('BotA').strategy, 1);
|
assert.strictEqual(pool.getConfigFor('BotA').strategy, 1);
|
||||||
|
assert.strictEqual(pool.settingSnapshot('BotB').hasOverrides, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearBotConfigOverride 清除专属配置后重新继承全局', () => {
|
||||||
|
const { pool } = setup({ config: { strategy: 1, targetCount: 10 } });
|
||||||
|
pool.setBotSetting('BotB', { config: { strategy: 3, targetCount: 5 } });
|
||||||
|
assert.strictEqual(pool.getConfigFor('BotB').strategy, 3);
|
||||||
|
assert.strictEqual(pool.settingSnapshot('BotB').hasOverrides, true);
|
||||||
|
|
||||||
|
const saved = pool.clearBotConfigOverride('BotB');
|
||||||
|
|
||||||
|
assert.strictEqual(saved.hasOverrides, false);
|
||||||
|
assert.strictEqual(pool.getConfigFor('BotB').strategy, 1, '应重新继承全局');
|
||||||
|
assert.strictEqual(pool.getConfigFor('BotB').targetCount, 10);
|
||||||
|
// 清除后应下发到运行时
|
||||||
|
assert.strictEqual(pool.get('BotB').manager.config.strategy, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('专属配置是完整快照:存下后全局改动不再影响它(这正是需要「清除」按钮的原因)', () => {
|
||||||
|
const { pool } = setup({ config: { allowBreak: false } });
|
||||||
|
pool.setBotSetting('BotA', { config: { allowBreak: true } });
|
||||||
|
|
||||||
|
// 改全局
|
||||||
|
const next = normalizeConfig({ allowBreak: false, targetCount: 42 }, pool.config);
|
||||||
|
pool.applyGlobalConfig(next);
|
||||||
|
|
||||||
|
assert.strictEqual(pool.getConfigFor('BotA').allowBreak, true, '专属配置优先于全局');
|
||||||
|
assert.strictEqual(pool.getConfigFor('BotB').targetCount, 42, '没有覆盖的机器人跟随全局');
|
||||||
|
|
||||||
|
pool.clearBotConfigOverride('BotA');
|
||||||
|
assert.strictEqual(pool.getConfigFor('BotA').allowBreak, false, '清除后跟随全局');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearBotConfigOverride 对不存在的机器人抛 404', () => {
|
||||||
|
const { pool } = setup();
|
||||||
|
assert.throws(() => pool.clearBotConfigOverride('不存在'), (err) => err.statusCode === 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('applyGlobalConfig 下发到全部机器人并保留各自的覆盖', () => {
|
test('applyGlobalConfig 下发到全部机器人并保留各自的覆盖', () => {
|
||||||
|
|||||||
@@ -415,6 +415,42 @@ test('未启动任务时 tick 不做任何事', () => {
|
|||||||
assert.strictEqual(controller.state, State.IDLE);
|
assert.strictEqual(controller.state, State.IDLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('寻路中只要在稳步接近目标,就不算超时(慢速推进不会被回城打断)', async () => {
|
||||||
|
const { controller, config, bot } = setup({ config: { waitTimeoutSeconds: 5, retries: 3 } });
|
||||||
|
await controller.start(config);
|
||||||
|
const target = controller.target;
|
||||||
|
assert.ok(target, '应选出目标区块');
|
||||||
|
|
||||||
|
// 先跑一次 tick 建立距离基准(plan() 会把基准清空,首次观察只记录不续期)
|
||||||
|
// 把机器人放在目标西侧 10 个区块处,后面每步靠近 2 格
|
||||||
|
const place = (offsetX) => {
|
||||||
|
bot.entity.position.x = (target.x + offsetX) * 16 + 8;
|
||||||
|
bot.entity.position.z = target.z * 16 + 8;
|
||||||
|
};
|
||||||
|
place(-10);
|
||||||
|
controller.tick();
|
||||||
|
|
||||||
|
// 模拟「走得很慢但在推进」:每次 tick 靠近 2 个区块,但耗时都超过超时阈值
|
||||||
|
for (let step = 1; step <= 4; step++) {
|
||||||
|
place(-10 + step * 2);
|
||||||
|
controller.stageStartedMillis = Date.now() - 10000; // 每次都「已超时」
|
||||||
|
controller.tick();
|
||||||
|
assert.strictEqual(controller.retryCount, 0,
|
||||||
|
`第 ${step} 次推进不应被判定为超时(retryCount=${controller.retryCount})`);
|
||||||
|
assert.notStrictEqual(controller.state, State.RETURNING, '不应触发回城');
|
||||||
|
}
|
||||||
|
assert.strictEqual(controller.consecutiveTimeoutMillis, 0, '有推进时连续超时应清零');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('寻路中原地不动时仍然会超时并重试(回归保护)', async () => {
|
||||||
|
const { controller, config } = setup({ config: { waitTimeoutSeconds: 5, retries: 3 } });
|
||||||
|
await controller.start(config);
|
||||||
|
controller.tick(); // 首次 tick 只记录距离基准
|
||||||
|
controller.stageStartedMillis = Date.now() - 10000;
|
||||||
|
controller.tick();
|
||||||
|
assert.strictEqual(controller.retryCount, 1, '原地不动必须照常判超时');
|
||||||
|
});
|
||||||
|
|
||||||
test('规划队列为空时直接进入 COMPLETE', async () => {
|
test('规划队列为空时直接进入 COMPLETE', async () => {
|
||||||
// 己方领土被完全包围且无空位:构造一个只有 1 个区块、四周全被他国占领的局面
|
// 己方领土被完全包围且无空位:构造一个只有 1 个区块、四周全被他国占领的局面
|
||||||
const { controller, config } = setup({
|
const { controller, config } = setup({
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户