diff --git a/README.md b/README.md index 41a0756..568b260 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ - 目标有效性校验:目标若已被其他邦国占领则立即跳过 - 现实时间限制:仅 11:00-22:00(本地现实时间)允许开拓,其余时段启动会被拒绝、运行中到点自动停止 - 自动进食(饥饿时自动吃背包食物) +- 自动攻击敌对生物:扩地途中自动面向并攻击索敌范围内的敌对生物(满蓄力出手、不追击、不打断寻路);严格豁免不会主动攻击玩家的中立生物——铁傀儡、僵尸猪灵永不攻击,末影人(仅被激怒)、蜘蛛(仅黑暗中)、猪灵(仅玩家未穿金甲)按原版敌对条件反击,狼、蜜蜂、羊驼等其它中立生物一律不打 +- 耐久保护回城:每 2 秒检查主手/副手/四件盔甲耐久,任意一件剩余耐久低于阈值(默认 15%,可在 GUI 调整)时自动发送 `/u return` 回城并停止扩地,防止装备被怪物磨没 - 实时地图 GUI(通过 Ctrl+R 打开) - 监听聊天/字幕消息判断开垦结果 - 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞 @@ -26,7 +28,7 @@ ## 安装 1. 安装 Fabric Loader 0.16.14+ 和 Fabric API -2. 将 `autostore-1.2.5.jar` 放入 `.minecraft/mods/` 目录 +2. 将 `autostore-1.2.6.jar` 放入 `.minecraft/mods/` 目录 3. 启动游戏,按 `Ctrl+R` 打开 GUI ## 构建 @@ -36,7 +38,7 @@ ./build_mod.sh ``` -输出文件:`autostore-1.2.5.jar` +输出文件:`autostore-1.2.6.jar` ## 赞助支持 diff --git a/gradle.properties b/gradle.properties index debb63b..4c5429e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ yarn_mappings=1.21.4+build.8 loader_version=0.19.3 # Mod Properties -mod_version=1.2.5 +mod_version=1.2.6 maven_group=autostore archives_base_name=autostore diff --git a/src/main/java/autostore/client/combat/AutoCombat.java b/src/main/java/autostore/client/combat/AutoCombat.java new file mode 100644 index 0000000..b75a904 --- /dev/null +++ b/src/main/java/autostore/client/combat/AutoCombat.java @@ -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) +/** + * 战斗辅助工具类:自动攻击与耐久检查的纯函数集合。 + * + *

目标筛选遵循一条核心原则:只攻击会主动攻击玩家的生物。 + * 铁傀儡、僵尸猪灵等中立生物永远不在目标内;末影人/蜘蛛/猪灵等 + * "条件敌对"生物只在确实处于主动敌对状态时才被攻击,判定依据 + * 与原版 AI 一致且均可客户端验证(激怒标志/眼睛亮度/金盔甲)。

+ */ +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() { + } + + /** + * 判断实体是否为合法攻击目标。 + * + *

判定规则(按优先级): + *

+ * + * @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; + } +} diff --git a/src/main/java/autostore/client/config/ConfigStore.java b/src/main/java/autostore/client/config/ConfigStore.java index 0d0ff96..005ddbd 100644 --- a/src/main/java/autostore/client/config/ConfigStore.java +++ b/src/main/java/autostore/client/config/ConfigStore.java @@ -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) { // 解析失败时回退到默认配置 diff --git a/src/main/java/autostore/client/config/ExpansionConfig.java b/src/main/java/autostore/client/config/ExpansionConfig.java index 6ae6b2d..a8fcadd 100644 --- a/src/main/java/autostore/client/config/ExpansionConfig.java +++ b/src/main/java/autostore/client/config/ExpansionConfig.java @@ -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; diff --git a/src/main/java/autostore/client/controller/ExpansionController.java b/src/main/java/autostore/client/controller/ExpansionController.java index 78edec8..879c287 100644 --- a/src/main/java/autostore/client/controller/ExpansionController.java +++ b/src/main/java/autostore/client/controller/ExpansionController.java @@ -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; * *

工作流程:规划(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 状态等待传送完成。 + * + *

传送完成后的走向由 {@link #returnThenStop} 决定: + * 寻路超时回城会重新规划继续扩地;耐久保护回城则停止任务,等待玩家修复装备后手动重新开始。

+ * + * @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 且饥饿值低于阈值时暂停寻路并自动吃背包里的食物, diff --git a/src/main/java/autostore/client/gui/AutoStoreGui.java b/src/main/java/autostore/client/gui/AutoStoreGui.java index d66d740..98634ff 100644 --- a/src/main/java/autostore/client/gui/AutoStoreGui.java +++ b/src/main/java/autostore/client/gui/AutoStoreGui.java @@ -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;