feat: 大地图跟随机器人并支持可调图幅,新增定时消息,控制台界面重做并适配移动端
大地图跟随机器人 + 可调图幅 - 爬取与显示区域改为以机器人当前位置为中心(bigMap.followBot 默认开启), 不再傻傻地爬世界原点周边 - 界面新增「图幅」下拉(±512/1024/2048/4096/8192/全世界)与「爬取周边」按钮, 只爬当前图幅内的瓦片:层级 0 每张覆盖 4096 格,±4096 只需 9 张,全世界要 100 张 - POST /api/bigmap/crawl 支持 centerX/centerZ/rangeBlocks;GET /api/bigmap 支持 rangeBlocks/bot 查询参数;启动预爬取会先等机器人进入世界再以其位置为中心 领土地图以机器人所在邦国为中心 - 之前初始视图用的是控制器的 playerChunk,而任务启动前它是 (0,0), 导致地图永远停在世界原点附近。现在按「机器人所在区块 → 包含它的邦国 → 该邦国王城/外接矩形」定位 定时重复发送消息(src/bot/scheduledMessenger.js) - 每个机器人可配多条定时任务,按固定间隔重复发送同一内容,如每 300 秒 /u restore confirm - 以 / 开头按游戏指令发送,否则按公屏消息发送 - 只在已连接且通过登录验证时发送;未就绪的周期顺延且不补发,避免重连后一次性连发 - 与指令节流队列共用,间隔下限 5 秒、单机器人最多 20 条 - 控制台新增「定时消息」卡片(新增/删除/启停 + 剩余秒数倒计时) 控制台界面重做 + 移动端适配 - 重新设计的深色主题:统一圆角、阴影、焦点态与悬停态,卡片式分层 - 响应式三档:≥1200px 三栏;≤1200px 两栏;≤820px 单栏 + 底部导航 - 移动端:触控目标 ≥36px、地图/日志高度按视口自适应、避让底部安全区、 横屏压矮地图、尊重「减少动效」偏好、Toast 全宽 - 「添加玩家」折叠进 details;顶栏窄屏自动换行且按钮等分 修复 - POST /api/bigmap/crawl 响应把中心与图幅字段传错(实际爬取正确,返回值显示成原点) - 定时任务保存后接口返回原始列表而非归一化结果,前端拿不到剩余秒数 测试 308 -> 325,全部通过。
这个提交包含在:
+74
-6
@@ -313,12 +313,34 @@ class WebConsole {
|
||||
if (body.enabled !== undefined) patch.enabled = body.enabled === true;
|
||||
if (body.autoStart !== undefined) patch.autoStart = body.autoStart === true;
|
||||
if (body.config !== undefined) patch.config = body.config;
|
||||
if (body.schedules !== undefined) patch.schedules = body.schedules;
|
||||
const saved = this._setBotSetting(entry, patch);
|
||||
res.json({ ok: true, data: saved });
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
// 定时消息:整体替换某机器人的定时发送列表
|
||||
app.post('/api/bots/:id/schedules', (req, res) => {
|
||||
this._withEntry(req, res, (entry) => {
|
||||
const schedules = (req.body && req.body.schedules) || [];
|
||||
this._setBotSetting(entry, { schedules });
|
||||
// 返回带「剩余秒数」的快照,前端可直接渲染倒计时
|
||||
res.json({
|
||||
ok: true,
|
||||
data: this._settingSnapshot(entry).schedules || [],
|
||||
message: '定时任务已保存'
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/bots/:id/schedules', (req, res) => {
|
||||
this._withEntry(req, res, (entry) => {
|
||||
res.json({ ok: true, data: this._settingSnapshot(entry).schedules || [] });
|
||||
return null;
|
||||
});
|
||||
});
|
||||
app.post('/api/bots/:id/task/start', async (req, res) => {
|
||||
try {
|
||||
const entry = this.pool.require(req.params.id);
|
||||
@@ -464,7 +486,7 @@ class WebConsole {
|
||||
res.json({ ok: true, data: { enabled: false } });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true, data: this.bigMapPayload() });
|
||||
res.json({ ok: true, data: this.bigMapPayload(req.query || {}) });
|
||||
});
|
||||
|
||||
app.get('/api/bigmap/markers', (req, res) => {
|
||||
@@ -498,6 +520,7 @@ class WebConsole {
|
||||
}
|
||||
});
|
||||
|
||||
// 爬取地图瓦片:默认爬「机器人所在位置周边」的图幅,也可显式指定中心与半径
|
||||
app.post('/api/bigmap/crawl', async (req, res) => {
|
||||
if (!this.bigMap) {
|
||||
res.status(500).json({ ok: false, message: '未启用大地图' });
|
||||
@@ -505,11 +528,22 @@ class WebConsole {
|
||||
}
|
||||
const body = req.body || {};
|
||||
const zoom = Number.isFinite(Number(body.zoom)) ? Number(body.zoom) : undefined;
|
||||
const center = this._bigMapCenter({ query: { bot: body.bot } }, body);
|
||||
const rangeBlocks = Number.isFinite(Number(body.rangeBlocks)) ? Number(body.rangeBlocks) : undefined;
|
||||
// 爬取可能持续数十秒:立刻返回,进度通过 /api/bigmap 轮询
|
||||
this.bigMap.crawlAll(zoom, { force: body.force === true }).catch((err) => {
|
||||
this.bigMap.crawlAll(zoom, {
|
||||
force: body.force === true,
|
||||
centerBlockX: center.x,
|
||||
centerBlockZ: center.z,
|
||||
rangeBlocks
|
||||
}).catch((err) => {
|
||||
this.logger.log('大地图爬取失败:' + (err && err.message ? err.message : String(err)), 'error');
|
||||
});
|
||||
res.json({ ok: true, message: '已开始爬取地图瓦片', data: this.bigMap.snapshot() });
|
||||
res.json({
|
||||
ok: true,
|
||||
message: `已开始爬取 ${Math.round(center.x)},${Math.round(center.z)} 周边的瓦片`,
|
||||
data: this.bigMap.snapshot({ centerBlockX: center.x, centerBlockZ: center.z, rangeBlocks })
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/bigmap/cache/clear', (req, res) => {
|
||||
@@ -687,7 +721,7 @@ class WebConsole {
|
||||
defaultBotId: this.pool.defaultId || '',
|
||||
expansionLog: this.expansionLog ? this.expansionLog.stats() : null,
|
||||
chatLog: this.chatLog ? this.chatLog.snapshot() : null,
|
||||
bigMap: this.bigMap ? this.bigMap.snapshot() : null,
|
||||
bigMap: this.bigMap ? this.bigMapPayload() : null,
|
||||
serverTime: Date.now(),
|
||||
claimTime: {
|
||||
start: this.getConfig().claimStartHour,
|
||||
@@ -749,10 +783,44 @@ class WebConsole {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算大地图的关注中心。
|
||||
*
|
||||
* 默认以「当前选中的机器人」所在位置为中心 —— 大地图的意义就是看自己机器人周边,
|
||||
* 而不是永远盯着世界原点 (0,0)。没有可用坐标时回退到原点。
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {{centerX?: number, centerZ?: number}} [override] 显式指定的中心优先
|
||||
* @returns {{x: number, z: number, source: string}}
|
||||
*/
|
||||
_bigMapCenter(req, override) {
|
||||
const opts = override || {};
|
||||
if (Number.isFinite(Number(opts.centerX)) && Number.isFinite(Number(opts.centerZ))) {
|
||||
return { x: Number(opts.centerX), z: Number(opts.centerZ), source: 'explicit' };
|
||||
}
|
||||
const follow = !this.bigMap || !this.bigMap.config || !this.bigMap.config.bigMap
|
||||
|| this.bigMap.config.bigMap.followBot !== false;
|
||||
if (follow) {
|
||||
const requested = req && req.query ? req.query.bot : undefined;
|
||||
const entry = this.pool.get(requested) || this.defaultEntry;
|
||||
const position = entry ? entry.manager.snapshot().position : null;
|
||||
if (position) return { x: position.x, z: position.z, source: 'bot' };
|
||||
}
|
||||
return { x: 0, z: 0, source: 'origin' };
|
||||
}
|
||||
|
||||
/** @returns {object} 大地图元信息(坐标系、瓦片范围、缓存进度、机器人与领土) */
|
||||
bigMapPayload() {
|
||||
const snapshot = this.bigMap.snapshot();
|
||||
bigMapPayload(query) {
|
||||
const params = query || {};
|
||||
const center = this._bigMapCenter({ query: params });
|
||||
const rangeBlocks = Number.isFinite(Number(params.rangeBlocks)) ? Number(params.rangeBlocks) : undefined;
|
||||
const snapshot = this.bigMap.snapshot({
|
||||
centerBlockX: center.x,
|
||||
centerBlockZ: center.z,
|
||||
rangeBlocks
|
||||
});
|
||||
return Object.assign(snapshot, {
|
||||
center: { x: center.x, z: center.z, source: center.source },
|
||||
bots: this.pool.positions(),
|
||||
markers: this.bigMapMarkersPayload()
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户