From 3669442d642069c56376fb189c8fd98e05592e92 Mon Sep 17 00:00:00 2001 From: lyizai <3945107889@qq.com> Date: Mon, 31 Aug 2026 13:26:11 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.6:=20=E8=87=AA=E5=8A=A8=E6=94=BB=E5=87=BB?= =?UTF-8?q?=E6=95=8C=E5=AF=B9=E7=94=9F=E7=89=A9(=E8=B1=81=E5=85=8D?= =?UTF-8?q?=E9=93=81=E5=82=80=E5=84=A1/=E5=83=B5=E5=B0=B8=E7=8C=AA?= =?UTF-8?q?=E7=81=B5=E7=AD=89=E4=B8=AD=E7=AB=8B=E7=94=9F=E7=89=A9)=20+=20?= =?UTF-8?q?=E4=BD=8E=E8=80=90=E4=B9=85=E8=87=AA=E5=8A=A8/u=20return?= =?UTF-8?q?=E5=9B=9E=E5=9F=8E=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- gradle.properties | 2 +- .../autostore/client/combat/AutoCombat.java | 238 ++++++++++++++++++ .../autostore/client/config/ConfigStore.java | 4 + .../client/config/ExpansionConfig.java | 12 + .../controller/ExpansionController.java | 118 +++++++-- .../autostore/client/gui/AutoStoreGui.java | 36 ++- 7 files changed, 394 insertions(+), 22 deletions(-) create mode 100644 src/main/java/autostore/client/combat/AutoCombat.java 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() { + } + + /** + * 判断实体是否为合法攻击目标。 + * + *判定规则(按优先级): + *
工作流程:规划(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;