feat: 用 Node.js + mineflayer 重构 AutoStore,UI 改为网页控制台并适配 Grim 反作弊
由 Fabric 客户端模组 AutoStore v1.3.1 重构为独立 Node.js 机器人,无需游戏客户端。 核心实现: - 领土数据:Dynmap markers.json 拉取解析(奇偶填充识别国中飞地)、SimpCore 开放 API 第四数据源、 带缓存击穿的时间戳请求与内容未变化跳过解析 - 扩地规划:四种策略(方形外扩 / 区域 S 形扫描 / 玩家周围 / 绕国一圈)逐行对照移植, 接壤判定只算己方领土,飞地优先填补 - 扩地状态机:规划→寻路→等待开垦→成功/失败→下一目标→完成,含超时重试、 往边上走两步自救、连续超时回城、耐久保护回城 - 生存辅助:自动进食、自动攻击(豁免中立生物)、自动游泳防溺水、自动用船(默认关闭) - 寻路:Baritone → mineflayer-pathfinder,高度偏好用包装 Goal 等价移植 Grim 反作弊合规层(新增,核心差异点): - 旋转不超前:接管 bot.look 按 ≤8.6°/tick 推进朝向,消除 mineflayer 「本地朝向瞬间跳变、数据包滞后补发」导致的 Prediction 分歧 - 攻击三重校验:朝向已同步 + 距离 ≤3.0 格 + 视线可达 + 攻击冷却 + 非骑乘/进食 - 动作互斥:拦下疾跑+潜行、疾跑+使用物品等原版不可能出现的组合 - 物理参数只读自检 + 寻路约束(禁挖/禁放/禁跑酷/禁搭塔/限下落/绕开水域) 网页控制台(新增): - 状态面板、Canvas 领土地图(缩放/平移/图层)、分组配置表单、实时日志流 - REST + WebSocket 接口,支持 WEB_TOKEN 令牌鉴权 修复: - containsFuzzy 只取固定长度前缀的末行编辑距离,导致完全相同的子串也匹配不上 - 占领去重依赖 Set.add 返回值(JS 中恒为真),导致同一区块被重复计数 - 非法枚举值不再钳制到最近边界,改为回退默认值 测试:154 个用例,npm test 全绿;npm run check 语法自检通过。
这个提交包含在:
@@ -0,0 +1,151 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { Vec3 } = require('vec3');
|
||||
|
||||
const { FakeBot } = require('./helpers/fakeBot');
|
||||
const {
|
||||
isAttackTarget,
|
||||
findTarget,
|
||||
eyeToEntityDistance,
|
||||
findLowDurabilitySlot,
|
||||
wearsGoldArmor
|
||||
} = require('../src/bot/autoCombat');
|
||||
const { defaultConfig } = require('../src/config/expansionConfig');
|
||||
|
||||
/** 构造一个假实体 */
|
||||
function makeEntity(name, kind, position, extra) {
|
||||
return Object.assign({
|
||||
id: Math.floor(Math.random() * 100000),
|
||||
name,
|
||||
kind,
|
||||
type: 'mob',
|
||||
position: new Vec3(position[0], position[1], position[2]),
|
||||
width: 0.6,
|
||||
height: 1.8,
|
||||
isValid: true
|
||||
}, extra || {});
|
||||
}
|
||||
|
||||
test('敌对生物可被攻击', () => {
|
||||
const bot = new FakeBot();
|
||||
const config = defaultConfig();
|
||||
assert.strictEqual(isAttackTarget(makeEntity('zombie', 'Hostile mobs', [0, 64, 0]), bot, config), true);
|
||||
assert.strictEqual(isAttackTarget(makeEntity('skeleton', 'Hostile mobs', [0, 64, 0]), bot, config), true);
|
||||
assert.strictEqual(isAttackTarget(makeEntity('creeper', 'Hostile mobs', [0, 64, 0]), bot, config), true);
|
||||
});
|
||||
|
||||
test('绝不攻击玩家与中立生物', () => {
|
||||
const bot = new FakeBot();
|
||||
const config = defaultConfig();
|
||||
// 玩家
|
||||
assert.strictEqual(isAttackTarget(makeEntity('Steve', 'Passive mobs', [0, 64, 0], { type: 'player' }), bot, config), false);
|
||||
// 铁傀儡、僵尸猪灵(原版中立,绝不主动攻击)
|
||||
assert.strictEqual(isAttackTarget(makeEntity('iron_golem', 'Passive mobs', [0, 64, 0]), bot, config), false);
|
||||
assert.strictEqual(isAttackTarget(makeEntity('zombified_piglin', 'Hostile mobs', [0, 64, 0]), bot, config), false);
|
||||
// 宠物与常见中立生物
|
||||
for (const name of ['wolf', 'cat', 'llama', 'bee', 'fox', 'polar_bear', 'villager']) {
|
||||
assert.strictEqual(isAttackTarget(makeEntity(name, 'Passive mobs', [0, 64, 0]), bot, config), false, name + ' 不应被攻击');
|
||||
}
|
||||
});
|
||||
|
||||
test('条件敌对生物默认不打,可在配置里放开', () => {
|
||||
const bot = new FakeBot();
|
||||
const config = defaultConfig();
|
||||
assert.strictEqual(isAttackTarget(makeEntity('enderman', 'Hostile mobs', [0, 64, 0]), bot, config), false);
|
||||
assert.strictEqual(isAttackTarget(makeEntity('spider', 'Hostile mobs', [0, 64, 0]), bot, config), false);
|
||||
|
||||
config.combat.attackEnderman = true;
|
||||
config.combat.attackSpiders = true;
|
||||
assert.strictEqual(isAttackTarget(makeEntity('enderman', 'Hostile mobs', [0, 64, 0]), bot, config), true);
|
||||
assert.strictEqual(isAttackTarget(makeEntity('spider', 'Hostile mobs', [0, 64, 0]), bot, config), true);
|
||||
});
|
||||
|
||||
test('猪灵:未穿金甲才攻击', () => {
|
||||
const config = defaultConfig();
|
||||
const bareBot = new FakeBot();
|
||||
assert.strictEqual(isAttackTarget(makeEntity('piglin', 'Hostile mobs', [0, 64, 0]), bareBot, config), true);
|
||||
|
||||
const armoredBot = new FakeBot({ slots: [] });
|
||||
armoredBot.inventory.slots[5] = { name: 'golden_helmet' };
|
||||
assert.strictEqual(wearsGoldArmor(armoredBot), true);
|
||||
assert.strictEqual(isAttackTarget(makeEntity('piglin', 'Hostile mobs', [0, 64, 0]), armoredBot, config), false);
|
||||
});
|
||||
|
||||
test('高危生物默认跳过', () => {
|
||||
const bot = new FakeBot();
|
||||
const config = defaultConfig();
|
||||
for (const name of ['warden', 'ender_dragon', 'wither']) {
|
||||
assert.strictEqual(isAttackTarget(makeEntity(name, 'Hostile mobs', [0, 64, 0]), bot, config), false, name + ' 应被跳过');
|
||||
}
|
||||
config.combat.avoidDangerousMobs = false;
|
||||
assert.strictEqual(isAttackTarget(makeEntity('wither', 'Hostile mobs', [0, 64, 0]), bot, config), true);
|
||||
});
|
||||
|
||||
test('无效 / 已消失 / 非生物实体不被攻击', () => {
|
||||
const bot = new FakeBot();
|
||||
const config = defaultConfig();
|
||||
assert.strictEqual(isAttackTarget(null, bot, config), false);
|
||||
assert.strictEqual(isAttackTarget(makeEntity('zombie', 'Hostile mobs', [0, 64, 0], { isValid: false }), bot, config), false);
|
||||
assert.strictEqual(isAttackTarget(makeEntity('zombie', 'Hostile mobs', [0, 64, 0], { type: 'object' }), bot, config), false);
|
||||
assert.strictEqual(isAttackTarget(bot.entity, bot, config), false);
|
||||
});
|
||||
|
||||
test('findTarget 在索敌半径内挑最近的合法目标', () => {
|
||||
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
|
||||
const config = defaultConfig();
|
||||
const near = makeEntity('zombie', 'Hostile mobs', [1, 64, 0]);
|
||||
const far = makeEntity('skeleton', 'Hostile mobs', [4, 64, 0]);
|
||||
const neutral = makeEntity('iron_golem', 'Passive mobs', [0.5, 64, 0]);
|
||||
const outside = makeEntity('creeper', 'Hostile mobs', [20, 64, 0]);
|
||||
bot.entities = { 1: near, 2: far, 3: neutral, 4: outside };
|
||||
assert.strictEqual(findTarget(bot, 4.5, config), near);
|
||||
});
|
||||
|
||||
test('findTarget 半径内没有敌对生物时返回 null', () => {
|
||||
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
|
||||
const config = defaultConfig();
|
||||
bot.entities = { 1: makeEntity('cow', 'Passive mobs', [1, 64, 0]) };
|
||||
assert.strictEqual(findTarget(bot, 4.5, config), null);
|
||||
});
|
||||
|
||||
test('eyeToEntityDistance 计算到碰撞箱最近点的距离', () => {
|
||||
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
|
||||
const entity = makeEntity('zombie', 'Hostile mobs', [2, 64, 0]);
|
||||
const distance = eyeToEntityDistance(bot, entity);
|
||||
assert.ok(Math.abs(distance - 1.7) < 1e-6, `期望 1.7,实际 ${distance}`);
|
||||
});
|
||||
|
||||
test('耐久保护:识别剩余耐久低于阈值的装备', () => {
|
||||
const bot = new FakeBot({ slots: [] });
|
||||
// 主手:钻石剑,最大 1561,已用 1400 → 剩余 10.3%
|
||||
bot.heldItem = { name: 'diamond_sword', maxDurability: 1561, durabilityUsed: 1400 };
|
||||
const low = findLowDurabilitySlot(bot, 15);
|
||||
assert.ok(low);
|
||||
assert.strictEqual(low.name, '主手');
|
||||
assert.strictEqual(low.percent, 10);
|
||||
assert.strictEqual(low.itemName, 'diamond_sword');
|
||||
});
|
||||
|
||||
test('耐久保护:装备充足时返回 null', () => {
|
||||
const bot = new FakeBot({ slots: [] });
|
||||
bot.heldItem = { name: 'diamond_sword', maxDurability: 1561, durabilityUsed: 100 };
|
||||
bot.inventory.slots[5] = { name: 'diamond_helmet', maxDurability: 363, durabilityUsed: 10 };
|
||||
assert.strictEqual(findLowDurabilitySlot(bot, 15), null);
|
||||
});
|
||||
|
||||
test('耐久保护:检查四件盔甲与副手', () => {
|
||||
const bot = new FakeBot({ slots: [] });
|
||||
bot.inventory.slots[7] = { name: 'diamond_leggings', maxDurability: 495, durabilityUsed: 480 };
|
||||
const low = findLowDurabilitySlot(bot, 15);
|
||||
assert.ok(low);
|
||||
assert.strictEqual(low.name, '护腿');
|
||||
});
|
||||
|
||||
test('耐久保护:不可损坏物品被忽略', () => {
|
||||
const bot = new FakeBot({ slots: [] });
|
||||
bot.heldItem = { name: 'stone' };
|
||||
bot.inventory.slots[5] = { name: 'turtle_helmet', maxDurability: 0 };
|
||||
assert.strictEqual(findLowDurabilitySlot(bot, 90), null);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户