v1.2.6: 自动攻击敌对生物(豁免铁傀儡/僵尸猪灵等中立生物) + 低耐久自动/u return回城保护

这个提交包含在:
lyizai
2026-08-31 13:26:11 +08:00
父节点 70ab6cc698
当前提交 3669442d64
共修改 7 个文件,包含 394 行新增和 22 行删除
@@ -0,0 +1,238 @@
/*
* AutoStore 模组战斗辅助:自动攻击敌对生物与装备耐久保护。
*/
package autostore.client.combat;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.EndermanEntity;
import net.minecraft.entity.mob.Monster;
import net.minecraft.entity.mob.PiglinEntity;
import net.minecraft.entity.mob.SpiderEntity;
import net.minecraft.entity.mob.ZombifiedPiglinEntity;
import net.minecraft.entity.passive.IronGolemEntity;
import net.minecraft.entity.passive.TameableEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.util.Hand;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
@Environment(EnvType.CLIENT)
/**
* 战斗辅助工具类:自动攻击与耐久检查的纯函数集合。
*
* <p>目标筛选遵循一条核心原则:只攻击<b>会主动攻击玩家</b>的生物。
* 铁傀儡、僵尸猪灵等中立生物永远不在目标内;末影人/蜘蛛/猪灵等
* "条件敌对"生物只在确实处于主动敌对状态时才被攻击,判定依据
* 与原版 AI 一致且均可客户端验证(激怒标志/眼睛亮度/金盔甲)。</p>
*/
public final class AutoCombat {
/** 原版实体交互距离(格):视线到目标碰撞箱最近点超过该距离无法命中 */
private static final double ATTACK_RANGE_SQUARE = 3.0 * 3.0;
/** 耐久保护检查的装备槽位:主手、副手与四件盔甲 */
private static final EquipmentSlot[] GUARD_SLOTS = {
EquipmentSlot.MAINHAND, EquipmentSlot.OFFHAND,
EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET
};
private AutoCombat() {
}
/**
* 判断实体是否为合法攻击目标。
*
* <p>判定规则(按优先级):
* <ul>
* <li>玩家、已驯服宠物:绝不攻击</li>
* <li>铁傀儡:不会主动攻击玩家的中立生物,绝不攻击</li>
* <li>僵尸猪灵:群体报复机制,绝不攻击</li>
* <li>末影人:仅被激怒(瞪视)时主动攻击,此时才反击</li>
* <li>蜘蛛:仅黑暗中(眼睛亮度 &lt; 0.5,与原版索敌条件一致)主动攻击</li>
* <li>猪灵:玩家穿戴金盔甲时保持中立,未穿戴时才会攻击玩家</li>
* <li>其余敌对生物(僵尸/骷髅/苦力怕/卫道士/疣猪兽等):主动攻击</li>
* </ul></p>
*
* @param entity 待判定实体
* @param player 玩家自身
* @return 是否可作为攻击目标
*/
public static boolean isAttackTarget(Entity entity, PlayerEntity player) {
if (!(entity instanceof LivingEntity living)) {
return false;
}
if (living == player || !living.isAlive() || living.isSpectator()) {
return false;
}
// 绝不攻击玩家(含其他真实玩家)
if (living instanceof PlayerEntity) {
return false;
}
// 已驯服的宠物不攻击
if (living instanceof TameableEntity tameable && tameable.isTamed()) {
return false;
}
// 铁傀儡:不会主动攻击玩家的中立生物,绝不攻击
if (living instanceof IronGolemEntity) {
return false;
}
// 僵尸猪灵:激怒一只引来一群,绝不攻击
if (living instanceof ZombifiedPiglinEntity) {
return false;
}
// 末影人:仅被激怒(瞪视/先手攻击)时才主动敌对
if (living instanceof EndermanEntity enderman) {
return enderman.isAngry();
}
// 蜘蛛:与原版一致,仅黑暗中(眼睛亮度 < 0.5)才主动索敌
if (living instanceof SpiderEntity) {
return living.getBrightnessAtEyes() < 0.5f;
}
// 猪灵:玩家穿戴金盔甲时保持中立
if (living instanceof PiglinEntity) {
return !wearsGoldArmor(player);
}
// 其余敌对生物:直接攻击
return living instanceof Monster;
}
/**
* 在索敌范围内查找最近的合法攻击目标(不追击,仅就地战斗)。
*
* @param client 客户端实例
* @param player 玩家
* @param scanRange 索敌半径(格)
* @return 最近的攻击目标,范围内没有时返回 null
*/
public static LivingEntity findTarget(MinecraftClient client, PlayerEntity player, double scanRange) {
if (client == null || client.world == null || player == null) {
return null;
}
Box box = player.getBoundingBox().expand(scanRange);
LivingEntity best = null;
double bestDistance = Double.MAX_VALUE;
for (LivingEntity entity : client.world.getEntitiesByClass(LivingEntity.class, box,
candidate -> isAttackTarget(candidate, player))) {
double distance = eyeToBoxSquared(player.getEyePos(), entity.getBoundingBox());
if (distance < bestDistance) {
bestDistance = distance;
best = entity;
}
}
return best;
}
/**
* 执行一帧攻击:面向目标,在攻击距离内且攻击冷却充满时出手。
* 距离不够只面向不挥击(等 Baritone 寻路或目标靠近),不空挥不追击。
*
* @param client 客户端实例
* @param target 攻击目标
* @return 本帧是否实际发起了攻击
*/
public static boolean tickAttack(MinecraftClient client, LivingEntity target) {
PlayerEntity player = client == null ? null : client.player;
if (player == null || client.interactionManager == null || target == null || !target.isAlive()) {
return false;
}
// 使用物品(如进食/举盾)时不攻击,避免打断
if (player.isSpectator() || player.isUsingItem()) {
return false;
}
// 面向目标:原版服务端不校验朝向,但部分服务器反作弊会校验
faceEntity(player, target);
// 攻击距离校验:视线到目标碰撞箱最近点 ≤ 3 格(原版实体交互距离),够不到不空挥
if (eyeToBoxSquared(player.getEyePos(), target.getBoundingBox()) > ATTACK_RANGE_SQUARE) {
return false;
}
// 攻击冷却充满才出手,保证每次都满蓄力
if (player.getAttackCooldownProgress(0.0f) < 1.0f) {
return false;
}
client.interactionManager.attackEntity(player, target);
player.swingHand(Hand.MAIN_HAND);
player.resetLastAttackedTicks();
return true;
}
/**
* 检查已装备物品(主手/副手/四件盔甲)中是否有耐久低于阈值的。
*
* @param player 玩家
* @param thresholdPercent 耐久阈值(剩余耐久百分比 ≤ 该值视为过低)
* @return 第一个耐久过低的装备槽位,全部正常时返回 null
*/
public static EquipmentSlot findLowDurabilitySlot(PlayerEntity player, int thresholdPercent) {
if (player == null) {
return null;
}
int threshold = Math.max(1, thresholdPercent);
for (EquipmentSlot slot : GUARD_SLOTS) {
ItemStack stack = player.getEquippedStack(slot);
if (stack.isEmpty() || !stack.isDamageable()) {
continue;
}
int maxDamage = stack.getMaxDamage();
int remaining = maxDamage - stack.getDamage();
// 用整数乘法避免浮点误差:剩余百分比 ≤ 阈值
if (remaining * 100 <= threshold * maxDamage) {
return slot;
}
}
return null;
}
/** 返回装备槽位的中文名称(用于日志与聊天提示) */
public static String slotName(EquipmentSlot slot) {
return switch (slot) {
case MAINHAND -> "主手";
case OFFHAND -> "副手";
case HEAD -> "头盔";
case CHEST -> "胸甲";
case LEGS -> "护腿";
case FEET -> "靴子";
case BODY -> "动物盔甲";
};
}
/** 让玩家面向目标(转向目标中躯位置) */
private static void faceEntity(PlayerEntity player, LivingEntity target) {
Vec3d eye = player.getEyePos();
Vec3d aim = target.getPos().add(0.0, target.getHeight() * 0.5, 0.0);
double dx = aim.x - eye.x;
double dy = aim.y - eye.y;
double dz = aim.z - eye.z;
double horizontal = Math.sqrt(dx * dx + dz * dz);
float yaw = (float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0);
float pitch = (float) (-Math.toDegrees(Math.atan2(dy, horizontal)));
player.setYaw(yaw);
player.setPitch(MathHelper.clamp(pitch, -90.0f, 90.0f));
}
/** 计算视线点到包围盒最近点的距离平方(用于原版攻击距离判定) */
private static double eyeToBoxSquared(Vec3d eye, Box box) {
double dx = Math.max(Math.max(box.minX - eye.x, eye.x - box.maxX), 0.0);
double dy = Math.max(Math.max(box.minY - eye.y, eye.y - box.maxY), 0.0);
double dz = Math.max(Math.max(box.minZ - eye.z, eye.z - box.maxZ), 0.0);
return dx * dx + dy * dy + dz * dz;
}
/** 判断玩家四件盔甲中是否至少一件为金质(金盔甲可使猪灵保持中立) */
private static boolean wearsGoldArmor(PlayerEntity player) {
for (EquipmentSlot slot : new EquipmentSlot[]{EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET}) {
ItemStack stack = player.getEquippedStack(slot);
if (!stack.isEmpty() && Registries.ITEM.getId(stack.getItem()).getPath().contains("golden")) {
return true;
}
}
return false;
}
}
@@ -49,6 +49,10 @@ public final class ConfigStore {
// 注意:maxRings 由 /autostore rings 命令持久化,不得在此覆盖,否则命令设置会在重启后失效
loaded.autoEat = true;
loaded.eatThreshold = 14;
// 耐久保护阈值钳制到合理区间,防止异常值导致永不回城或刚出门就回城
if (loaded.durabilityThreshold < 1 || loaded.durabilityThreshold > 99) {
loaded.durabilityThreshold = 15;
}
return loaded;
} catch (Exception e) {
// 解析失败时回退到默认配置
@@ -91,6 +91,15 @@ public class ExpansionConfig {
/** 触发自动进食的饥饿值阈值(0-20) */
public int eatThreshold = 14;
/** 是否启用自动攻击:扩地途中自动面向并攻击范围内的敌对生物(铁傀儡/僵尸猪灵等中立生物永不攻击) */
public boolean autoAttack = true;
/** 是否启用耐久保护:装备耐久过低时发送 /u return 回城并停止扩地,防止装备被怪物磨没 */
public boolean durabilityGuard = true;
/** 耐久保护阈值:主手/副手/盔甲任意一件剩余耐久百分比 ≤ 该值时触发回城(1-99,默认 15) */
public int durabilityThreshold = 15;
/** 扩圈上限(0 表示不限制) */
public int maxRings;
@@ -129,6 +138,9 @@ public class ExpansionConfig {
c.avoidMobs = this.avoidMobs;
c.autoEat = this.autoEat;
c.eatThreshold = this.eatThreshold;
c.autoAttack = this.autoAttack;
c.durabilityGuard = this.durabilityGuard;
c.durabilityThreshold = this.durabilityThreshold;
c.maxRings = this.maxRings;
c.refreshIntervalSeconds = this.refreshIntervalSeconds;
return c;
@@ -5,6 +5,7 @@
package autostore.client.controller;
import autostore.client.bridge.BaritoneBridge;
import autostore.client.combat.AutoCombat;
import autostore.client.config.ExpansionConfig;
import autostore.client.monitor.SubtitleChatMonitor;
import autostore.client.planner.ExpansionPlanner;
@@ -29,6 +30,8 @@ import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.util.Hand;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
@@ -44,7 +47,8 @@ import net.minecraft.text.Text;
*
* <p>工作流程:规划(PLANNING)→ 寻路(PATHING)→ 等待开垦(WAITING)→ 成功/失败(SUCCESS/FAIL)→ 下一个目标(NEXT)→ 完成(COMPLETE)。
* 每帧由 {@link #tick()} 驱动,通过 Baritone 自动寻路到目标区块,
* 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。
* 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、自动攻击敌对生物、
* 低耐久自动回城、超时重试、手动停止等功能。
*/
public final class ExpansionController
implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
@@ -56,6 +60,10 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
private static final int CLAIM_START_HOUR = 11;
/** 允许开拓的现实时间结束小时(不含,本地时区),即 22:00 起停止开拓 */
private static final int CLAIM_END_HOUR = 22;
/** 自动攻击索敌半径(格):只攻击进入该范围的敌对生物,不追击,避免打断扩地寻路 */
private static final double AUTO_ATTACK_SCAN_RANGE = 4.5;
/** 耐久保护检查间隔(tick),40 tick ≈ 2 秒 */
private static final int DURABILITY_CHECK_INTERVAL_TICKS = 40;
/** Baritone 寻路桥接(负责自动移动) */
private final BaritoneBridge baritoneBridge = new BaritoneBridge();
/** 扩地规划器(决定下一个目标区块) */
@@ -112,6 +120,12 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
private long sideStepSinceMillis;
/** 边走两步开始时的玩家方块坐标(用于判断是否已走开约 2.5 格) */
private BlockPos sideStepStartPos;
/** 耐久保护检查计数器:每 {@link #DURABILITY_CHECK_INTERVAL_TICKS} 帧检查一次装备耐久 */
private int durabilityCheckCounter;
/** 本次回城是否由耐久保护触发:RETURNING 等待结束后停止任务(等待玩家修复装备),而非重新规划 */
private boolean returnThenStop;
/** 自动攻击当前目标的实体 ID(-1 为无目标),仅用于避免重复记录战斗日志 */
private int combatTargetEntityId = -1;
public ExpansionController(TerritoryDataService dataService) {
this.dataService = dataService;
@@ -231,6 +245,9 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
this.consecutiveTimeoutMillis = 0L;
this.sideStepCount = 0;
this.sideStepping = false;
this.returnThenStop = false;
this.durabilityCheckCounter = 0;
this.combatTargetEntityId = -1;
this.queue.clear();
this.claimedSet.clear();
this.target = null;
@@ -265,8 +282,8 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
/**
* 每帧驱动逻辑(由客户端 tick 事件调用):
* 更新玩家区块,优先处理自动进食,再按当前状态分发到对应的 tick 方法。
* 若已退出世界则自动停止任务。
* 更新玩家区块,依次处理自动进食、耐久保护、自动游泳、自动攻击,
* 再按当前状态分发到对应的 tick 方法。若已退出世界则自动停止任务。
*/
public void tick() {
MinecraftClient client = MinecraftClient.getInstance();
@@ -296,12 +313,18 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
if (this.tickAutoFood(client)) {
return;
}
// 耐久保护优先于一切寻路/战斗:装备耐久过低时先回城,防止装备被怪物磨没
if (this.tickDurabilityGuard(client)) {
return;
}
this.tickAutoSwim(client);
// 处于"往边上走两步"阶段:每帧驱动手动横向行走,走开后自动重试原目标
// 处于"往边上走两步"阶段:每帧驱动手动横向行走,走开后自动重试原目标(该阶段不自动攻击,避免抢方向键)
if (this.sideStepping) {
this.tickSideStep();
return;
}
// 自动攻击:面向并攻击范围内的敌对生物(不追击,不打断寻路状态机)
this.tickAutoAttack(client);
switch (this.state) {
case PLANNING: {
this.plan();
@@ -452,36 +475,48 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
}
/**
* 连续寻路超时达到阈值时触发回城:
* 取消当前寻路,向服务器发送 /u return 指令(约 10 秒后自动传送回可寻路的安全位置),
* 随后进入 RETURNING 状态等待传送完成,传送后由 {@link #tickReturning()} 重新规划并从头开始。
* 发送 /u return 回城:取消当前寻路,向服务器发送回城指令(约 10 秒后自动传送回安全位置),
* 随后进入 RETURNING 状态等待传送完成。
*
* <p>传送完成后的走向由 {@link #returnThenStop} 决定:
* 寻路超时回城会重新规划继续扩地;耐久保护回城则停止任务,等待玩家修复装备后手动重新开始。</p>
*
* @param reason 触发回城的原因(写入日志与聊天提示)
*/
private void sendReturnCommand() {
private void sendReturnCommand(String reason) {
this.baritoneBridge.cancel();
MinecraftClient client = MinecraftClient.getInstance();
if (client != null && client.player != null) {
this.log("\u8fde\u7eed\u5bfb\u8def\u8d85\u65f6\u8d85\u8fc7 " + this.config.returnThresholdSeconds + " \u79d2\uff0c\u53d1\u9001 /u return \u56de\u57ce\uff0c10 \u79d2\u540e\u81ea\u52a8\u4f20\u9001");
this.log(reason + ",发送 /u return 回城,10 秒后自动传送");
// 通过玩家网络处理器发送聊天指令 /u return(Yarn 映射:sendChatCommand 自动带上前缀斜杠)
client.player.networkHandler.sendChatCommand("u return");
client.player.sendMessage(Text.literal("[AutoStore] \u8fde\u7eed\u5bfb\u8def\u8d85\u65f6\uff0c\u5df2\u53d1\u9001 /u return\uff0c\u7b49\u5f85\u4f20\u9001\u540e\u91cd\u65b0\u6269\u5730"), false);
client.player.sendMessage(Text.literal("[AutoStore] " + reason + ",已发送 /u return,传送后"
+ (this.returnThenStop ? "停止扩地" : "重新扩地")), false);
} else {
this.log("\u8fde\u7eed\u5bfb\u8def\u8d85\u65f6\uff0c\u4f46\u73a9\u5bb6\u4e0d\u5728\u4e16\u754c\uff0c\u8df3\u8fc7");
this.log(reason + ",但玩家不在世界,跳过");
}
this.returnStartMillis = System.currentTimeMillis();
this.setState(State.RETURNING, "\u7b49\u5f85\u56de\u57ce\u4f20\u9001\uff0810 \u79d2\u540e\u81ea\u52a8\u4f20\u9001\uff09");
this.setState(State.RETURNING, "等待回城传送(10 秒后自动传送)");
}
/**
* 回城传送等待阶段每帧逻辑:
* 发送 /u return 后服务器约 10 秒把玩家传送回安全位置。等待 {@code returnWaitSeconds}
* 秒(覆盖传送延迟并留余量)后,清零连续超时计数并重新规划扩地计划,
* 从最高优先级区块(队首)重新开始寻路。
* 发送 /u return 后服务器约 10 秒把玩家传送回安全位置。
* 等待 {@code returnWaitSeconds} 秒(覆盖传送延迟并留余量)后:
* 耐久保护触发的回城 → 停止任务并提示修复装备;
* 寻路超时触发的回城 → 清零连续超时计数并重新规划扩地计划,从最高优先级区块重新开始。
*/
private void tickReturning() {
long elapsedMillis = System.currentTimeMillis() - this.returnStartMillis;
if (elapsedMillis < (long)this.config.returnWaitSeconds * 1000L) {
return;
}
if (this.returnThenStop) {
this.returnThenStop = false;
this.log("耐久保护回城完成,任务已停止;请修复/更换装备后重新开始扩地");
this.setState(State.STOPPED, "装备耐久过低已回城,请修复装备后重新开始");
return;
}
this.consecutiveTimeoutMillis = 0L;
this.log("\u56de\u57ce\u4f20\u9001\u5b8c\u6210\uff0c\u91cd\u65b0\u89c4\u5212\u5e76\u4ece\u6700\u9ad8\u4f18\u5148\u7ea7\u533a\u5757\u5f00\u59cb");
// 重新规划:清空队列重新生成计划,从队首(最高优先级)区块开始寻路
@@ -564,7 +599,7 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
*/
private void giveUpTarget(String reason) {
if (this.consecutiveTimeoutMillis >= (long)this.config.returnThresholdSeconds * 1000L) {
this.sendReturnCommand();
this.sendReturnCommand("连续寻路超时超过 " + this.config.returnThresholdSeconds + " 秒");
return;
}
if (this.target != null) {
@@ -868,6 +903,57 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
}
}
/**
* 自动攻击逻辑(每帧调用):
* 扩地途中索敌半径内出现敌对生物时,面向该生物并在攻击距离内自动出手(满蓄力),
* 不追击、不打断 Baritone 寻路;目标筛选由 {@link AutoCombat#isAttackTarget} 保证
* 绝不攻击铁傀儡、僵尸猪灵等不会主动攻击玩家的中立生物。
*/
private void tickAutoAttack(MinecraftClient client) {
if (!this.config.autoAttack || !this.isRunning()) {
this.combatTargetEntityId = -1;
return;
}
LivingEntity target = AutoCombat.findTarget(client, client.player, AUTO_ATTACK_SCAN_RANGE);
if (target == null) {
this.combatTargetEntityId = -1;
return;
}
// 仅在新目标出现时记录一次日志,避免每帧刷屏
if (target.getId() != this.combatTargetEntityId) {
this.combatTargetEntityId = target.getId();
this.log("发现敌对生物 " + target.getDisplayName().getString() + ",自动攻击中");
}
AutoCombat.tickAttack(client, target);
}
/**
* 耐久保护逻辑(每 {@link #DURABILITY_CHECK_INTERVAL_TICKS} 帧调用):
* 主手/副手/四件盔甲任意一件剩余耐久低于阈值时,发送 /u return 回城并停止扩地,
* 防止装备在继续战斗中被怪物磨没。回城传送完成后由 {@link #tickReturning()} 停止任务。
*
* @return true 表示本帧已触发回城(进入 RETURNING 状态)
*/
private boolean tickDurabilityGuard(MinecraftClient client) {
if (!this.config.durabilityGuard || !this.isRunning() || this.state == State.RETURNING) {
return false;
}
if (++this.durabilityCheckCounter < DURABILITY_CHECK_INTERVAL_TICKS) {
return false;
}
this.durabilityCheckCounter = 0;
EquipmentSlot low = AutoCombat.findLowDurabilitySlot(client.player, this.config.durabilityThreshold);
if (low == null) {
return false;
}
String reason = "装备耐久过低(" + AutoCombat.slotName(low)
+ " 剩余耐久≤" + this.config.durabilityThreshold + "%)";
this.returnThenStop = true;
this.log(reason + ",发送 /u return 回城并停止扩地,防止装备被怪物磨没");
this.sendReturnCommand(reason);
return true;
}
/**
* 自动进食逻辑(每帧调用):
* 开启 autoEat 且饥饿值低于阈值时暂停寻路并自动吃背包里的食物,
@@ -50,9 +50,12 @@ extends Screen {
// ---- 输入控件 ----
private TextFieldWidget stateField;
private TextFieldWidget targetCountField;
private TextFieldWidget durabilityField;
private CheckboxWidget sprintBox;
private CheckboxWidget jumpBox;
private CheckboxWidget avoidBox;
private CheckboxWidget attackBox;
private CheckboxWidget durabilityGuardBox;
private CheckboxWidget breakBox;
private CheckboxWidget placeBox;
private ButtonWidget refreshButton;
@@ -142,9 +145,10 @@ extends Screen {
this.addDrawableChild(this.strategyButton("\u7ed5\u56fd\u4e00\u5708", 4, left + 96, strategyY + 24, 90));
this.advancedY = strategyY + 24 + 20 + 8;
// ===== 只保留五个权限选择框:疾跑、跳跃、避怪、破坏、放置 =====
// ===== 权限/辅助选择框:疾跑、跳跃、避怪、攻击、破坏、放置、耐久回城 =====
int checkY1 = this.advancedY + 14;
int checkY2 = checkY1 + advCheckGap;
int checkY3 = checkY2 + advCheckGap;
int checkX = left;
this.sprintBox = this.checkbox("\u75be\u8dd1", checkX, checkY1, this.config.allowSprint, value -> {
this.config.allowSprint = value;
@@ -155,17 +159,29 @@ extends Screen {
this.avoidBox = this.checkbox("\u907f\u602a", checkX + 160, checkY1, this.config.avoidMobs, value -> {
this.config.avoidMobs = value;
});
this.breakBox = this.checkbox("\u7834\u574f", checkX, checkY2, this.config.allowBreak, value -> {
this.attackBox = this.checkbox("\u653b\u51fb", checkX, checkY2, this.config.autoAttack, value -> {
this.config.autoAttack = value;
});
this.breakBox = this.checkbox("\u7834\u574f", checkX + 88, checkY2, this.config.allowBreak, value -> {
this.config.allowBreak = value;
});
this.placeBox = this.checkbox("\u653e\u7f6e", checkX + 88, checkY2, this.config.allowPlace, value -> {
this.placeBox = this.checkbox("\u653e\u7f6e", checkX + 160, checkY2, this.config.allowPlace, value -> {
this.config.allowPlace = value;
});
// 第三行:耐久保护开关 + 回城阈值输入框(剩余耐久百分比,范围 1-99)
this.durabilityGuardBox = this.checkbox("\u8010\u4e45\u56de\u57ce", checkX, checkY3, this.config.durabilityGuard, value -> {
this.config.durabilityGuard = value;
});
this.durabilityField = this.numberField(this.config.durabilityThreshold, checkX + 96, checkY3, 34);
this.durabilityField.setMaxLength(2);
this.addDrawableChild(this.sprintBox);
this.addDrawableChild(this.jumpBox);
this.addDrawableChild(this.avoidBox);
this.addDrawableChild(this.attackBox);
this.addDrawableChild(this.breakBox);
this.addDrawableChild(this.placeBox);
this.addDrawableChild(this.durabilityGuardBox);
this.addDrawableChild(this.durabilityField);
this.initializeMapView();
}
@@ -476,6 +492,12 @@ extends Screen {
context.drawTextWithShadow(this.textRenderer, "\u90a6\u56fd", this.panelX + 4, this.stateFieldY + 4, -5592406);
context.drawTextWithShadow(this.textRenderer, "\u76ee\u6807\u6570\u91cf", this.panelX + 4, this.targetFieldY + 4, -5592406);
context.drawTextWithShadow(this.textRenderer, "\u6743\u9650\u8bbe\u7f6e", this.panelX + 4, this.advancedY, -5592406);
// 耐久回城阈值输入框的单位标签(位于输入框右侧)
if (this.durabilityField != null) {
context.drawTextWithShadow(this.textRenderer, "\u8010\u4e45%",
this.durabilityField.getX() + this.durabilityField.getWidth() + 4,
this.durabilityField.getY() + 6, -5592406);
}
// ===== 底部信息面板:日志(左)+ 状态(右),均单行截断,避免换行导致的重叠 =====
int infoTop = this.mapY + this.mapH + 10;
@@ -572,8 +594,13 @@ extends Screen {
this.config.allowSprint = this.sprintBox.isChecked();
this.config.allowJump = this.jumpBox.isChecked();
this.config.avoidMobs = this.avoidBox.isChecked();
this.config.autoAttack = this.attackBox.isChecked();
this.config.allowBreak = this.breakBox.isChecked();
this.config.allowPlace = this.placeBox.isChecked();
this.config.durabilityGuard = this.durabilityGuardBox.isChecked();
int threshold = this.parseInt(this.durabilityField.getText(), 15);
// 钳制阈值到 1-99,避免 0(永不触发)或 100(刚装备就回城)
this.config.durabilityThreshold = Math.max(1, Math.min(99, threshold));
}
/** 将本地配置副本同步到 mod 全局配置并保存到磁盘 */
@@ -585,8 +612,11 @@ extends Screen {
target.allowSprint = this.config.allowSprint;
target.allowJump = this.config.allowJump;
target.avoidMobs = this.config.avoidMobs;
target.autoAttack = this.config.autoAttack;
target.allowBreak = this.config.allowBreak;
target.allowPlace = this.config.allowPlace;
target.durabilityGuard = this.config.durabilityGuard;
target.durabilityThreshold = this.config.durabilityThreshold;
// 同步矩形圈选坐标到全局配置(修复:之前忘记同步,导致区域扩地总是为空)
target.regionMinX = this.config.regionMinX;
target.regionMaxX = this.config.regionMaxX;