package autodig.client.gui; import autodig.client.AutoDigModClient; import autodig.client.config.DigConfig; import autodig.client.config.MineTask; import autodig.client.controller.DigController; import java.util.ArrayList; import java.util.List; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.IntSupplier; import java.util.function.Supplier; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.CheckboxWidget; import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.text.Text; import net.minecraft.util.math.MathHelper; /** * AutoDig 游戏内 GUI(响应式 + 滚动布局)。 * *

结构:顶部标题与任务选择、底部操作按钮(开始/停止/仅关闭)固定不动, * 中间为可滚动设置区(滚轮滚动,右侧有滚动条)。设置区内容按当前任务动态 * 显示:主世界只显示主世界相关项,下界只显示下界相关项,避免无关选项干扰。 * 面板宽度随窗口自适应,标签过长时复选框自动改为上下堆叠,避免文字重叠。

*/ public final class AutoDigGui extends Screen { private final AutoDigModClient mod; private final DigConfig config; private final DigController controller; /** 关闭界面时是否把改动写回 mod 配置("仅关闭"会置 false) */ private boolean applyOnClose = true; // ---- 布局 ---- private int panelLeft; private int panelW; /** 滚动区可视范围(屏幕坐标) */ private int viewportTop; private int viewportBottom; // ---- 滚动状态 ---- private double scrollOffset; private int contentHeight; /** 可滚动控件与其虚拟坐标(真实 y = virtualY - scrollOffset) */ private record ScrollEntry(ClickableWidget widget, int x, int virtualY) {} private final List scrollable = new ArrayList<>(); /** 可滚动的纯文本(分区标题、数值、字段标签),渲染时按滚动偏移绘制 */ private record TextEntry(int x, int virtualY, Supplier text, int color, boolean rightAlign, boolean underline) {} private final List texts = new ArrayList<>(); private TextFieldWidget overworldOresField; private TextFieldWidget netherOresField; private TextFieldWidget trashField; public AutoDigGui(AutoDigModClient mod) { super(Text.literal("AutoDig")); this.mod = mod; this.config = mod.config().copy(); this.controller = mod.controller(); } @Override protected void init() { this.clearChildren(); this.scrollable.clear(); this.texts.clear(); this.scrollOffset = 0; this.overworldOresField = null; this.netherOresField = null; this.trashField = null; // 面板宽度自适应窗口,最小 200 保证 +/- 与输入框可用 this.panelW = MathHelper.clamp(this.width - 16, 200, 400); this.panelLeft = (this.width - this.panelW) / 2; // ===== 固定区:任务选择 ===== int taskY = 24; int w3 = (this.panelW - 8) / 3; this.addDrawableChild(this.taskButton("空闲", MineTask.NONE, this.panelLeft, taskY, w3)); this.addDrawableChild(this.taskButton("主世界", MineTask.OVERWORLD, this.panelLeft + w3 + 4, taskY, w3)); this.addDrawableChild(this.taskButton("下界", MineTask.NETHER, this.panelLeft + (w3 + 4) * 2, taskY, w3)); // ===== 固定区:底部操作按钮(永远可见,不随内容滚动) ===== int actionY = this.height - 44; this.addDrawableChild(this.textButton("开始", b -> this.start(), this.panelLeft, actionY, w3)); this.addDrawableChild(this.textButton("停止", b -> this.stop(), this.panelLeft + w3 + 4, actionY, w3)); this.addDrawableChild(this.textButton("仅关闭", b -> this.closeOnly(), this.panelLeft + (w3 + 4) * 2, actionY, w3)); this.viewportTop = taskY + 26; this.viewportBottom = actionY - 8; // ===== 滚动区内容(虚拟坐标从 0 开始) ===== int y = 0; if (this.config.task == MineTask.NONE) { this.texts.add(new TextEntry(this.panelLeft, 4, () -> "请先在上方选择挖矿任务", 0xBBBBBB, false, false)); this.texts.add(new TextEntry(this.panelLeft, 18, () -> "主世界:鱼骨巷道挖矿 / 下界:进传送门挖矿并自动返程", 0x888888, false, false)); y = 40; } else { y = this.header(y, "通用设置"); y = this.checkPair(y, "保命优先", () -> this.config.survivalFirst, v -> this.config.survivalFirst = v, "优先绕开岩浆", () -> this.config.avoidLava, v -> this.config.avoidLava = v); y = this.checkPair(y, "自动进食", () -> this.config.autoEat, v -> this.config.autoEat = v, "自动切换工具", () -> this.config.autoTool, v -> this.config.autoTool = v); y = this.checkPair(y, "重生后回死亡点", () -> this.config.respawnReturn, v -> this.config.respawnReturn = v, "杀戮光环", () -> this.config.killAura, v -> this.config.killAura = v); y = this.checkPair(y, "杀戮光环不冒进", () -> this.config.killAuraSafe, v -> this.config.killAuraSafe = v, "下界协同杀戮光环", () -> this.config.killAuraInNether, v -> this.config.killAuraInNether = v); y = this.checkPair(y, "护甲耐久保护", () -> this.config.armorDurabilityProtection, v -> this.config.armorDurabilityProtection = v, "工具耐久保护", () -> this.config.toolDurabilityProtection, v -> this.config.toolDurabilityProtection = v); y = this.checkPair(y, "自动换备用装备", () -> this.config.autoGearSwap, v -> this.config.autoGearSwap = v, "背包满丢垃圾", () -> this.config.autoTrash, v -> { this.syncFields(); // 保留输入框中未应用的编辑 this.config.autoTrash = v; this.init(); // 切换后显示/隐藏垃圾设置区 }); y += 10; y = this.header(y, "装备耐久阈值(低于即停,百分比)"); y = this.numberRow(y, "护甲耐久", () -> this.config.armorDurabilityThreshold, v -> this.config.armorDurabilityThreshold = v, 1, 100); y = this.numberRow(y, "工具耐久", () -> this.config.toolDurabilityThreshold, v -> this.config.toolDurabilityThreshold = v, 1, 100); y += 10; if (this.config.autoTrash) { y = this.header(y, "垃圾丢弃设置"); y = this.numberRow(y, "方块垃圾保留量", () -> this.config.trashReserve, v -> this.config.trashReserve = v, 0, 2304); y = this.fieldRow(y, "垃圾物品(逗号分隔的物品 id)", this.config.trashItems, tf -> this.trashField = tf); y += 10; } if (this.config.task == MineTask.OVERWORLD) { y = this.header(y, "主世界设置"); y = this.numberRow(y, "挖掘层 (Y)", () -> this.config.overworldLevel, v -> this.config.overworldLevel = v, -64, 320); y = this.numberRow(y, "巷道长度", () -> this.config.tunnelLength, v -> this.config.tunnelLength = v, 4, 512); y = this.fieldRow(y, "目标矿物(逗号分隔,如 diamond_ore,deepslate_diamond_ore)", this.config.overworldOres, tf -> this.overworldOresField = tf); y += 10; } else if (this.config.task == MineTask.NETHER) { y = this.header(y, "下界设置"); y = this.numberRow(y, "目标数量 (-1 无限)", () -> this.config.netherTargetCount, v -> this.config.netherTargetCount = v, -1, 512); y = this.checkPair(y, "挖完/背包满/血量低/耐久低 自动回主世界", () -> this.config.netherAutoReturn, v -> this.config.netherAutoReturn = v, null, null, null); y = this.numberRow(y, "返程血量阈值", () -> this.config.netherFleeHealthThreshold, v -> this.config.netherFleeHealthThreshold = v, 0, 100); y = this.fieldRow(y, "目标矿物(逗号分隔,如 ancient_debris,nether_gold_ore)", this.config.netherOres, tf -> this.netherOresField = tf); y += 10; } } this.contentHeight = y; this.clampScroll(); this.applyScroll(); } // ================= 布局辅助 ================= /** 分区标题(金色文本 + 下划线)。 */ private int header(int y, String text) { this.texts.add(new TextEntry(this.panelLeft, y, () -> text, 0xFFD700, false, true)); return y + 16; } /** * 一行复选框:两列放得下则并排;放不下(窄屏/长文本)自动上下堆叠, * 保证文字不会互相重叠。label2 为 null 时独占一行。 */ private int checkPair(int y, String label1, BooleanSupplier get1, Consumer set1, String label2, BooleanSupplier get2, Consumer set2) { CheckboxWidget c1 = this.checkbox(label1, get1, set1); if (label2 == null) { this.addScrollable(c1, this.panelLeft, y); return y + 22; } CheckboxWidget c2 = this.checkbox(label2, get2, set2); if (c1.getWidth() + c2.getWidth() + 12 <= this.panelW) { this.addScrollable(c1, this.panelLeft, y); this.addScrollable(c2, this.panelLeft + this.panelW - c2.getWidth(), y); return y + 22; } // 放不下:上下堆叠 this.addScrollable(c1, this.panelLeft, y); this.addScrollable(c2, this.panelLeft, y + 22); return y + 44; } private CheckboxWidget checkbox(String label, BooleanSupplier getter, Consumer setter) { return CheckboxWidget.builder(Text.literal(label), this.textRenderer) .pos(0, 0).checked(getter.getAsBoolean()) .callback((b, v) -> setter.accept(v)).build(); } /** 数值行:[-] 标签 ..... 值 [+],标签左对齐、数值右对齐,自适应面板宽度。 */ private int numberRow(int y, String label, IntSupplier getter, Consumer setter, int min, int max) { int btn = 20; this.addScrollable(ButtonWidget.builder(Text.literal("-"), b -> setter.accept(MathHelper.clamp(getter.getAsInt() - 1, min, max))) .dimensions(this.panelLeft, 0, btn, btn).build(), this.panelLeft, y); this.addScrollable(ButtonWidget.builder(Text.literal("+"), b -> setter.accept(MathHelper.clamp(getter.getAsInt() + 1, min, max))) .dimensions(this.panelLeft + this.panelW - btn, 0, btn, btn).build(), this.panelLeft + this.panelW - btn, y); this.texts.add(new TextEntry(this.panelLeft + btn + 6, y + 6, () -> label, 0xFFFFFF, false, false)); this.texts.add(new TextEntry(this.panelLeft + this.panelW - btn - 6, y + 6, () -> String.valueOf(getter.getAsInt()), 0x55FF55, true, false)); return y + 24; } /** 文本输入行:标签 + 输入框(占满面板宽度)。 */ private int fieldRow(int y, String label, String initial, Consumer ref) { this.texts.add(new TextEntry(this.panelLeft, y + 3, () -> label, 0xBBBBBB, false, false)); y += 14; TextFieldWidget tf = new TextFieldWidget(this.textRenderer, this.panelLeft, 0, this.panelW, 18, Text.literal("")); tf.setMaxLength(256); tf.setText(initial); ref.accept(tf); this.addScrollable(tf, this.panelLeft, y); return y + 24; } /** 注册可滚动控件(真实位置在 applyScroll 中按偏移计算)。 */ private void addScrollable(ClickableWidget widget, int x, int virtualY) { widget.setX(x); widget.setY(virtualY); this.scrollable.add(new ScrollEntry(widget, x, virtualY)); this.addDrawableChild(widget); } private ButtonWidget taskButton(String label, MineTask task, int x, int y, int w) { boolean selected = this.config.task == task; String text = selected ? "■ " + label : "□ " + label; return this.addDrawableChild(this.textButton(text, b -> { this.syncFields(); // 保留输入框中未应用的编辑 this.config.task = task; this.init(); // 切换任务:重建滚动区,只显示相关设置 }, x, y, w)); } private ButtonWidget textButton(String label, ButtonWidget.PressAction action, int x, int y, int w) { return ButtonWidget.builder(Text.literal(label), action).dimensions(x, y, w, 20).build(); } // ================= 滚动逻辑 ================= private double maxScroll() { return Math.max(0, this.contentHeight - (this.viewportBottom - this.viewportTop)); } private void clampScroll() { this.scrollOffset = MathHelper.clamp(this.scrollOffset, 0, this.maxScroll()); } /** 按滚动偏移重排控件位置,并把滚出可视区的控件隐藏(不渲染、不可点)。 */ private void applyScroll() { for (ScrollEntry e : this.scrollable) { int dy = e.virtualY() - (int) this.scrollOffset; e.widget().setX(e.x()); e.widget().setY(dy); e.widget().visible = dy + e.widget().getHeight() > this.viewportTop && dy < this.viewportBottom; } } @Override public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { if (super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) { return true; } double before = this.scrollOffset; this.scrollOffset = MathHelper.clamp( this.scrollOffset - verticalAmount * 24.0, 0, this.maxScroll()); if (this.scrollOffset != before) { this.applyScroll(); return true; } return false; } // ================= 渲染 ================= /** * GUI 打开时不允许游戏暂停:否则客户端 tick 不执行,控制器停摆。 */ @Override public boolean shouldPause() { return false; } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { this.renderBackground(context, mouseX, mouseY, delta); // 滚动区底色,增强可读性 context.fill(this.panelLeft - 6, this.viewportTop - 4, this.panelLeft + this.panelW + 6, this.viewportBottom + 4, 0x88000000); super.render(context, mouseX, mouseY, delta); context.drawCenteredTextWithShadow(this.textRenderer, "AutoDig 自动挖矿", this.width / 2, 8, 0xFFFFFF); // 滚动区文本(裁剪绘制,防止溢出到固定区) int offset = (int) this.scrollOffset; context.enableScissor(this.panelLeft - 6, this.viewportTop - 4, this.panelLeft + this.panelW + 6, this.viewportBottom + 4); for (TextEntry t : this.texts) { String text = t.text().get(); if (text == null || text.isEmpty()) { continue; } int dy = t.virtualY() - offset; int x = t.rightAlign() ? t.x() - this.textRenderer.getWidth(text) : t.x(); context.drawTextWithShadow(this.textRenderer, text, x, dy, t.color()); if (t.underline()) { context.fill(this.panelLeft, dy + 11, this.panelLeft + this.panelW, dy + 12, 0x55FFFFFF); } } context.disableScissor(); // 滚动条 double max = this.maxScroll(); if (max > 0) { int trackTop = this.viewportTop - 4; int trackBottom = this.viewportBottom + 4; int trackH = trackBottom - trackTop; int thumbH = Math.max(18, (int) (trackH * (double) trackH / Math.max(1, this.contentHeight))); int thumbY = trackTop + (int) ((trackH - thumbH) * (this.scrollOffset / max)); int barX = this.panelLeft + this.panelW + 8; context.fill(barX, trackTop, barX + 3, trackBottom, 0x40FFFFFF); context.fill(barX, thumbY, barX + 3, thumbY + thumbH, 0xB0FFFFFF); } // 底部状态栏 context.drawTextWithShadow(this.textRenderer, "任务: " + this.config.task.label() + " 状态: " + this.controller.statusText(), this.panelLeft, this.height - 14, 0xAAAAAA); } // ================= 应用与操作 ================= /** 同步文本输入框的最新值到本地配置副本。 */ private void syncFields() { if (this.overworldOresField != null) { this.config.overworldOres = this.overworldOresField.getText(); } if (this.netherOresField != null) { this.config.netherOres = this.netherOresField.getText(); } if (this.trashField != null) { this.config.trashItems = this.trashField.getText(); } } /** 把界面临时配置写回 mod 全局配置并保存(供 controller 实时读取)。 */ private void applyToMod() { DigConfig target = this.mod.config(); target.task = this.config.task; target.survivalFirst = this.config.survivalFirst; target.avoidLava = this.config.avoidLava; target.autoEat = this.config.autoEat; target.autoTool = this.config.autoTool; target.killAura = this.config.killAura; target.killAuraSafe = this.config.killAuraSafe; target.killAuraInNether = this.config.killAuraInNether; target.respawnReturn = this.config.respawnReturn; target.netherAutoReturn = this.config.netherAutoReturn; target.overworldLevel = this.config.overworldLevel; target.tunnelLength = this.config.tunnelLength; target.overworldOres = this.config.overworldOres; target.netherOres = this.config.netherOres; target.netherTargetCount = this.config.netherTargetCount; target.netherFleeHealthThreshold = this.config.netherFleeHealthThreshold; target.armorDurabilityProtection = this.config.armorDurabilityProtection; target.armorDurabilityThreshold = this.config.armorDurabilityThreshold; target.toolDurabilityProtection = this.config.toolDurabilityProtection; target.toolDurabilityThreshold = this.config.toolDurabilityThreshold; target.autoGearSwap = this.config.autoGearSwap; target.autoTrash = this.config.autoTrash; target.trashItems = this.config.trashItems; target.trashReserve = this.config.trashReserve; this.mod.saveConfig(); } private void start() { this.syncFields(); if (this.config.task == MineTask.NONE) { this.mod.sendToast("AutoDig: 请先选择一个挖矿任务(主世界 / 下界)"); return; } List ores = this.config.task == MineTask.OVERWORLD ? this.config.getOverworldOreList() : this.config.getNetherOreList(); if (ores.isEmpty()) { this.mod.sendToast("AutoDig: 目标矿物列表为空,请填写至少一个矿物 ID"); return; } this.applyToMod(); this.mod.sendToast("AutoDig: 已开始 " + this.config.task.label() + " 任务"); this.close(); } private void stop() { // 先把本地副本的任务置空再写回,避免 close() 二次应用时把旧任务写回去 this.config.task = MineTask.NONE; this.applyToMod(); this.controller.bridge().cancelAll(); this.mod.sendToast("AutoDig: 已停止"); this.close(); } /** 仅关闭:丢弃未应用的改动。 */ private void closeOnly() { this.applyOnClose = false; this.close(); } @Override public void close() { if (this.applyOnClose) { this.syncFields(); this.applyToMod(); } super.close(); } }