feat: 支持多机器人挂机、开拓日志、公屏完整日志与大地图
多机器人(src/bot/botPool.js、src/config/botsConfig.js) - .env 用 MC_BOTS 一次配置多个账号(JSON 数组或 用户名:密码 逗号分隔),未配置时回退单机器人 - 每个机器人独立连接 / 登录 / 扩地状态机,共享一份领土数据服务与运行日志 - 密码只从 .env 读取;data/bots.json 只存启用状态、备注名、自动开始与各自的配置覆盖 - 每个机器人可覆盖全局配置(策略 / 目标邦国 / 目标数量等),未覆盖字段回退全局默认 开拓日志(src/core/expansionLog.js) - 逐条记录开拓玩家、邦国、区块、方块坐标、时间与判定来源,追加写入 data/expansion-log.jsonl - 支持按机器人 / 邦国 / 时间筛选、统计、导出带 BOM 的 CSV,超过 16MB 自动轮转 游戏公屏完整日志与聊天框(src/core/chatLog.js) - 每个机器人的全部公屏 / 系统 / 动作栏消息按机器人分文件留档,自身发出的聊天与指令同样留档 - 控制台新增公屏聊天页签与消息发送框 登录验证门禁 - 游戏侧:未完成登录验证时拒绝发送指令 / 聊天,isReady() 也不放行扩地任务; 新增 server.loginConfirmTimeoutMs 兜底,避免服务器不回提示导致永远卡在未登录 - 控制台侧:新增 WEB_PASSWORD 与 /api/login、/api/session,页面登录框验证通过才允许操作 大地图(src/core/bigMapService.js) - 爬取服务器地图站点(squaremap)的瓦片图片与领土标记,一张图上看全部机器人坐标 - 坐标系对源站实测标定并用 27 个王城交叉验证;瓦片磁盘缓存,层级 0 启动预爬取、更高层级按需穿透 - 领土按 markers.json 原始多边形绘制(预编译 Path2D,even-odd 保留孔洞),叠加王城与机器人标签 配套改造 - TerritoryModel 新增 flyingChunksOf / ownChunksOf,邦国归属改为按名称显式传入, 不再依赖全局 selfStateName(多机器人会互相覆盖) - Web 接口以 /api/bots/:id/... 为主,原单机器人接口保留为默认机器人的等价别名 - 新增 scripts/check-web-ids.js 校验前端元素引用一致性 测试从 154 增至 242,全部通过。
这个提交包含在:
@@ -50,11 +50,22 @@ class ExpansionController {
|
||||
* @param {import('../bot/botManager').BotManager} deps.botManager
|
||||
* @param {import('../core/territoryDataService').TerritoryDataService} deps.dataService
|
||||
* @param {import('../util/logger').Logger} deps.logger
|
||||
* @param {string} [deps.botId] 所属机器人 id(多机器人场景,用于日志归属)
|
||||
* @param {string} [deps.botName] 所属机器人游戏名
|
||||
* @param {import('../core/expansionLog').ExpansionLog} [deps.expansionLog] 开拓日志
|
||||
*/
|
||||
constructor(deps) {
|
||||
this.botManager = deps.botManager;
|
||||
this.dataService = deps.dataService;
|
||||
this.logger = deps.logger;
|
||||
/** 所属机器人 id(默认取游戏名) */
|
||||
this.botId = deps.botId || (deps.botManager && deps.botManager.connection
|
||||
? deps.botManager.connection.username : 'bot');
|
||||
/** 所属机器人游戏名 */
|
||||
this.botName = deps.botName || (deps.botManager && deps.botManager.connection
|
||||
? deps.botManager.connection.username : this.botId);
|
||||
/** @type {import('../core/expansionLog').ExpansionLog|null} */
|
||||
this.expansionLog = deps.expansionLog || null;
|
||||
this.planner = new ExpansionPlanner();
|
||||
/** @type {import('../core/subtitleChatMonitor').SubtitleChatMonitor} */
|
||||
this.monitor = new SubtitleChatMonitor({
|
||||
@@ -111,6 +122,13 @@ class ExpansionController {
|
||||
this.combatTargetEntityId = -1;
|
||||
/** 当前是否正持续按住跳跃键(自动游泳用) */
|
||||
this.holdingJump = false;
|
||||
/**
|
||||
* 本机器人实际开拓的邦国名(start() 时解析并固化)。
|
||||
*
|
||||
* 多机器人场景下每个机器人可能属于不同邦国,绝不能依赖领土模型的全局
|
||||
* selfStateName(那会被最后启动的机器人覆盖)。
|
||||
*/
|
||||
this.stateName = '';
|
||||
/** 是否已经发送过开垦模式指令 */
|
||||
this._claimModeSent = false;
|
||||
/** @type {((status: object) => void)|null} 状态变化回调(Web 控制台用) */
|
||||
@@ -177,34 +195,36 @@ class ExpansionController {
|
||||
this.playerChunk = this._playerChunk();
|
||||
|
||||
// ===== 识别己方邦国 =====
|
||||
// 解析结果固化到 this.stateName,并写回本次任务配置,
|
||||
// 使规划器不再依赖领土模型的全局 selfStateName(多机器人会互相覆盖)。
|
||||
const model = this.dataService.getModel();
|
||||
let resolvedName = '';
|
||||
if (!this.config.stateName) {
|
||||
const auto = model.findContainingState(this.playerChunk);
|
||||
if (auto) {
|
||||
model.setSelfStateName(auto.name);
|
||||
this.dataService.setSelectedStateName(auto.name);
|
||||
resolvedName = auto.name;
|
||||
} else {
|
||||
// 兜底:通过 SimpCore API 按玩家名识别
|
||||
const detected = await this.dataService.detectStateByPlayer(bm.connection.username);
|
||||
if (detected) {
|
||||
this.dataService.getModel().setSelfStateName(detected);
|
||||
this.dataService.setSelectedStateName(detected);
|
||||
resolvedName = detected;
|
||||
} else {
|
||||
this.status = '无法自动识别邦国,请在控制台手动选择';
|
||||
this.state = State.IDLE;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (!model.state(this.config.stateName)) {
|
||||
this.status = '所选邦国不存在:' + this.config.stateName;
|
||||
this.state = State.IDLE;
|
||||
return false;
|
||||
} else {
|
||||
const model2 = this.dataService.getModel();
|
||||
if (!model2.state(this.config.stateName)) {
|
||||
this.status = '所选邦国不存在:' + this.config.stateName;
|
||||
this.state = State.IDLE;
|
||||
return false;
|
||||
}
|
||||
model2.setSelfStateName(this.config.stateName);
|
||||
this.dataService.setSelectedStateName(this.config.stateName);
|
||||
resolvedName = this.config.stateName;
|
||||
}
|
||||
this.stateName = resolvedName;
|
||||
this.config = Object.assign({}, requestedConfig, { stateName: resolvedName });
|
||||
// 全局选中邦国跟随最近启动的机器人,供控制台地图与数据源展示使用
|
||||
this.dataService.setSelectedStateName(resolvedName);
|
||||
|
||||
// ===== 复位任务状态 =====
|
||||
this.completedClaims = 0;
|
||||
@@ -231,7 +251,7 @@ class ExpansionController {
|
||||
}
|
||||
|
||||
this.setState(State.PLANNING, '正在生成扩张计划');
|
||||
this._log('开始自动扩地,邦国:' + this.dataService.getModel().selfStateNameValue());
|
||||
this._log(`开始自动扩地,邦国:` + this.stateName);
|
||||
await this.plan();
|
||||
return true;
|
||||
}
|
||||
@@ -669,7 +689,7 @@ class ExpansionController {
|
||||
*/
|
||||
_isAdjacentToOwn(chunk) {
|
||||
const model = this.dataService.getModel();
|
||||
const self = model.selfState();
|
||||
const self = model.state(this.stateName);
|
||||
const ownChunks = self ? self.chunks : null;
|
||||
const sessionClaims = model.sessionClaimsSet();
|
||||
const x = chunk.x;
|
||||
@@ -698,7 +718,7 @@ class ExpansionController {
|
||||
*/
|
||||
_ringDistance(chunk, model) {
|
||||
let min = Infinity;
|
||||
const self = model.selfState();
|
||||
const self = model.state(this.stateName);
|
||||
if (self) {
|
||||
for (const k of self.chunks) {
|
||||
const pos = chunkPos.decode(k);
|
||||
@@ -740,8 +760,10 @@ class ExpansionController {
|
||||
/**
|
||||
* 开垦成功(带具体坐标):把新占领区块加入模型与数据服务,累计完成数并进入 SUCCESS 状态。
|
||||
* @param {{x: number, z: number}} chunk
|
||||
* @param {number} [blockX] 方块坐标 X(服务器回报值)
|
||||
* @param {number} [blockZ] 方块坐标 Z(服务器回报值)
|
||||
*/
|
||||
onSuccess(chunk) {
|
||||
onSuccess(chunk, blockX, blockZ) {
|
||||
if (!this.isRunning()) return;
|
||||
const target = chunk || this.target;
|
||||
if (!target) return;
|
||||
@@ -749,8 +771,8 @@ class ExpansionController {
|
||||
// 去重:同一区块只计一次
|
||||
if (this.claimedSet.has(key)) return;
|
||||
this.claimedSet.add(key);
|
||||
this.dataService.getModel().addSessionClaim(target);
|
||||
this.dataService.addSessionClaim(target);
|
||||
this.dataService.getModel().addSessionClaim(target, this.stateName);
|
||||
this.dataService.addSessionClaim(target, this.stateName);
|
||||
// 占领成功后立即触发一次地图刷新(请求侧已带缓存击穿),缩短「会话占领等待确认」窗口
|
||||
this.dataService.refreshAsync();
|
||||
this.completedClaims++;
|
||||
@@ -758,6 +780,7 @@ class ExpansionController {
|
||||
this.target = target;
|
||||
this.lastProgress = 100;
|
||||
this._log(`占领成功 ${target.x},${target.z}`);
|
||||
this._recordClaim(target, blockX, blockZ, 'chat');
|
||||
this.setState(State.SUCCESS, `疆土已开拓 ${target.x},${target.z}`);
|
||||
}
|
||||
|
||||
@@ -774,16 +797,40 @@ class ExpansionController {
|
||||
// 去重:同一区块只计一次
|
||||
if (this.claimedSet.has(key)) return;
|
||||
this.claimedSet.add(key);
|
||||
this.dataService.getModel().addSessionClaim(this.target);
|
||||
this.dataService.addSessionClaim(this.target);
|
||||
this.dataService.getModel().addSessionClaim(this.target, this.stateName);
|
||||
this.dataService.addSessionClaim(this.target, this.stateName);
|
||||
this.dataService.refreshAsync();
|
||||
this.completedClaims++;
|
||||
this.consecutiveTimeoutMillis = 0;
|
||||
this.lastProgress = 100;
|
||||
this._log(`占领成功 ${this.target.x},${this.target.z}`);
|
||||
this._recordClaim(this.target, null, null, 'chat-generic');
|
||||
this.setState(State.SUCCESS, `疆土已开拓 ${this.target.x},${this.target.z}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入开拓日志:详细记录开拓玩家、区块、时间与来源。
|
||||
*
|
||||
* @param {{x: number, z: number}} chunk 区块坐标
|
||||
* @param {number|null} blockX 方块坐标 X
|
||||
* @param {number|null} blockZ 方块坐标 Z
|
||||
* @param {string} source 判定来源
|
||||
*/
|
||||
_recordClaim(chunk, blockX, blockZ, source) {
|
||||
if (!this.expansionLog) return;
|
||||
this.expansionLog.record({
|
||||
botId: this.botId,
|
||||
bot: this.botName,
|
||||
state: this.stateName,
|
||||
chunk,
|
||||
block: Number.isFinite(blockX) && Number.isFinite(blockZ) ? { x: blockX, z: blockZ } : null,
|
||||
source,
|
||||
progress: 100,
|
||||
// 本轮任务已开拓区块数,便于在日志里看出第几个
|
||||
sessionCount: this.completedClaims
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 生存辅助 ====================
|
||||
|
||||
/**
|
||||
@@ -943,7 +990,9 @@ class ExpansionController {
|
||||
}
|
||||
|
||||
_log(message, level) {
|
||||
if (this.logger) this.logger.log(message, level);
|
||||
if (!this.logger) return;
|
||||
// 多机器人共用一份运行日志:统一加机器人名前缀,便于区分来源
|
||||
this.logger.log(`[${this.botName}] ${message}`, level);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -951,6 +1000,9 @@ class ExpansionController {
|
||||
*/
|
||||
snapshot() {
|
||||
return {
|
||||
botId: this.botId,
|
||||
botName: this.botName,
|
||||
stateName: this.stateName,
|
||||
state: this.state,
|
||||
status: this.status,
|
||||
running: this.isRunning(),
|
||||
|
||||
在新工单中引用
屏蔽一个用户