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,全部通过。
这个提交包含在:
@@ -428,3 +428,132 @@ test('规划队列为空时直接进入 COMPLETE', async () => {
|
||||
assert.strictEqual(controller.state, State.COMPLETE);
|
||||
assert.strictEqual(controller.status, '没有可用的相邻无主区块');
|
||||
});
|
||||
|
||||
// ==================== 多机器人:邦国归属与开拓日志 ====================
|
||||
|
||||
test('start 把识别到的邦国固化到 stateName,不依赖全局 selfStateName', async () => {
|
||||
const { controller, config } = setup({ selfName: 'A' });
|
||||
await controller.start(config);
|
||||
assert.strictEqual(controller.stateName, 'A');
|
||||
assert.strictEqual(controller.config.stateName, 'A', '应写回任务配置供规划器使用');
|
||||
});
|
||||
|
||||
test('两个机器人各自开拓不同邦国时互不覆盖', async () => {
|
||||
const map = new Map([
|
||||
['A', { name: 'A', capital: { x: 0, z: 0 }, chunks: new Set([chunkPos.key(0, 0)]), color: 0x75bd20 }],
|
||||
['B', { name: 'B', capital: { x: 20, z: 20 }, chunks: new Set([chunkPos.key(20, 20)]), color: 0xff43de }]
|
||||
]);
|
||||
const model = new TerritoryModel(map);
|
||||
const logger = new Logger({ echo: false });
|
||||
|
||||
const makeController = (name) => {
|
||||
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
|
||||
const botManager = new FakeBotManager(bot, model);
|
||||
botManager.connection = { username: name };
|
||||
const dataService = new FakeDataService(model);
|
||||
const controller = new ExpansionController({ botManager, dataService, logger, botId: name, botName: name });
|
||||
const config = defaultConfig();
|
||||
config.enforceClaimTime = false;
|
||||
config.autoEat = false;
|
||||
config.autoAttack = false;
|
||||
config.durabilityGuard = false;
|
||||
return { controller, config, model };
|
||||
};
|
||||
|
||||
const first = makeController('BotA');
|
||||
first.config.stateName = 'A';
|
||||
const second = makeController('BotB');
|
||||
second.config.stateName = 'B';
|
||||
|
||||
await first.controller.start(first.config);
|
||||
await second.controller.start(second.config);
|
||||
|
||||
// 全局 selfStateName 会被最后启动的机器人改写,但各控制器仍按自己的邦国工作
|
||||
assert.strictEqual(first.controller.stateName, 'A');
|
||||
assert.strictEqual(second.controller.stateName, 'B');
|
||||
|
||||
first.controller.playerChunk = { x: 0, z: 0 };
|
||||
second.controller.playerChunk = { x: 20, z: 20 };
|
||||
first.controller.monitor.onChat('疆土(0,1) [8,24] 已为你的邦国开拓');
|
||||
second.controller.monitor.onChat('疆土(20,21) [328,344] 已为你的邦国开拓');
|
||||
|
||||
assert.ok(model.state('A').chunks.has(chunkPos.key(0, 1)), 'A 的新占领应记到 A 名下');
|
||||
assert.ok(!model.state('A').chunks.has(chunkPos.key(20, 21)));
|
||||
assert.ok(model.state('B').chunks.has(chunkPos.key(20, 21)), 'B 的新占领应记到 B 名下');
|
||||
});
|
||||
|
||||
test('占领成功时写入开拓日志(含玩家、区块、时间与来源)', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-claimlog-'));
|
||||
const { ExpansionLog } = require('../src/core/expansionLog');
|
||||
const expansionLog = new ExpansionLog({ filePath: path.join(dir, 'expansion-log.jsonl') });
|
||||
|
||||
const { controller, config, bot } = setup();
|
||||
controller.expansionLog = expansionLog;
|
||||
controller.botId = 'BotA';
|
||||
controller.botName = 'BotA';
|
||||
await controller.start(config);
|
||||
const target = controller.target;
|
||||
bot.entity.position.x = target.x * 16 + 8;
|
||||
bot.entity.position.z = target.z * 16 + 8;
|
||||
controller.tick();
|
||||
|
||||
const before = Date.now();
|
||||
controller.monitor.onChat(`疆土(${target.x},${target.z}) [${target.x * 16 + 1},${target.z * 16 + 2}] 已为你的邦国开拓`);
|
||||
|
||||
const rows = expansionLog.list();
|
||||
assert.strictEqual(rows.length, 1);
|
||||
const entry = rows[0];
|
||||
assert.strictEqual(entry.bot, 'BotA');
|
||||
assert.strictEqual(entry.botId, 'BotA');
|
||||
assert.strictEqual(entry.state, 'A');
|
||||
assert.strictEqual(entry.cx, target.x);
|
||||
assert.strictEqual(entry.cz, target.z);
|
||||
assert.strictEqual(entry.bx, target.x * 16 + 1);
|
||||
assert.strictEqual(entry.bz, target.z * 16 + 2);
|
||||
assert.strictEqual(entry.source, 'chat');
|
||||
assert.ok(entry.t >= before && entry.t <= Date.now());
|
||||
assert.strictEqual(entry.sessionCount, 1);
|
||||
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('无坐标的通用成功消息记为 chat-generic 来源', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-claimlog2-'));
|
||||
const { ExpansionLog } = require('../src/core/expansionLog');
|
||||
const expansionLog = new ExpansionLog({ filePath: path.join(dir, 'expansion-log.jsonl') });
|
||||
|
||||
const { controller, config, bot } = setup();
|
||||
controller.expansionLog = expansionLog;
|
||||
await controller.start(config);
|
||||
const target = controller.target;
|
||||
bot.entity.position.x = target.x * 16 + 8;
|
||||
bot.entity.position.z = target.z * 16 + 8;
|
||||
controller.tick();
|
||||
controller.monitor.onChat('你的疆土已开拓');
|
||||
|
||||
const rows = expansionLog.list();
|
||||
assert.strictEqual(rows.length, 1);
|
||||
assert.strictEqual(rows[0].source, 'chat-generic');
|
||||
assert.strictEqual(rows[0].bx, null);
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('同一区块重复成功只记一条开拓日志', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-claimlog3-'));
|
||||
const { ExpansionLog } = require('../src/core/expansionLog');
|
||||
const expansionLog = new ExpansionLog({ filePath: path.join(dir, 'expansion-log.jsonl') });
|
||||
|
||||
const { controller, config, bot } = setup();
|
||||
controller.expansionLog = expansionLog;
|
||||
await controller.start(config);
|
||||
const target = controller.target;
|
||||
bot.entity.position.x = target.x * 16 + 8;
|
||||
bot.entity.position.z = target.z * 16 + 8;
|
||||
controller.tick();
|
||||
const message = `疆土(${target.x},${target.z}) [0,0] 已为你的邦国开拓`;
|
||||
controller.monitor.onChat(message);
|
||||
controller.monitor.onChat(message);
|
||||
|
||||
assert.strictEqual(expansionLog.list().length, 1);
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户