diff --git a/gradle.properties b/gradle.properties index b765588..01ad493 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=2.0.0 +mod_version=2.0.1 maven_group=autodig archives_base_name=autodig diff --git a/src/main/java/autodig/client/gui/AutoDigGui.java b/src/main/java/autodig/client/gui/AutoDigGui.java index 1d7b531..ca54fb4 100644 --- a/src/main/java/autodig/client/gui/AutoDigGui.java +++ b/src/main/java/autodig/client/gui/AutoDigGui.java @@ -4,41 +4,59 @@ 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 - 响应式布局,根据任务动态显示选项。 + * 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; - // UI 组件引用 + // ---- 布局 ---- + 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 ButtonWidget levelBtn; - private ButtonWidget tunnelBtn; - private ButtonWidget countBtn; - private ButtonWidget armorDurBtn; - private ButtonWidget toolDurBtn; - private ButtonWidget fleeHBtn; - - // 任务按钮(用于刷新高亮) - private ButtonWidget btnNone; - private ButtonWidget btnOverworld; - private ButtonWidget btnNether; - public AutoDigGui(AutoDigModClient mod) { super(Text.literal("AutoDig")); this.mod = mod; @@ -49,201 +67,215 @@ public final class AutoDigGui extends Screen { @Override protected void init() { this.clearChildren(); - int centerX = this.width / 2; - int panelW = Math.min(380, this.width - 40); - int left = centerX - panelW / 2; - int right = left + panelW; - int y = 30; - int gap = 4; - int rowH = 20; - int checkboxGap = 24; - int sectionGap = 14; + this.scrollable.clear(); + this.texts.clear(); + this.scrollOffset = 0; + this.overworldOresField = null; + this.netherOresField = null; - // ===== 标题 ===== - // 居中已在 render 处理 + // 面板宽度自适应窗口,最小 200 保证 +/- 与输入框可用 + this.panelW = MathHelper.clamp(this.width - 16, 200, 400); + this.panelLeft = (this.width - this.panelW) / 2; - // ===== 任务选择 ===== - int btnW = (panelW - gap * 2) / 3; - this.btnNone = taskButton("空闲", MineTask.NONE, left, y, btnW); - this.btnOverworld = taskButton("主世界", MineTask.OVERWORLD, left + btnW + gap, y, btnW); - this.btnNether = taskButton("下界", MineTask.NETHER, left + (btnW + gap) * 2, y, btnW); - y += rowH + sectionGap; + // ===== 固定区:任务选择 ===== + 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)); - // ===== 通用开关(所有任务共用)===== - y = addSectionHeader(y, left, "通用设置", panelW); - y = addCheckboxRow(y, left, panelW, - "保命优先", () -> config.survivalFirst, v -> config.survivalFirst = v, - "优先绕开岩浆", () -> config.avoidLava, v -> config.avoidLava = v); - y = addCheckboxRow(y, left, panelW, - "自动进食", () -> config.autoEat, v -> config.autoEat = v, - "自动切换工具", () -> config.autoTool, v -> config.autoTool = v); - y = addCheckboxRow(y, left, panelW, - "杀戮光环", () -> config.killAura, v -> config.killAura = v, - "重生后回死亡点", () -> config.respawnReturn, v -> config.respawnReturn = v); - y = addCheckboxRow(y, left, panelW, - "避难时绝杀不冒进", () -> config.killAuraSafe, v -> config.killAuraSafe = v, - "下界协同杀戮光环", () -> config.killAuraInNether, v -> config.killAuraInNether = v); - y = addCheckboxRow(y, left, panelW, - "护甲耐久保护", () -> config.armorDurabilityProtection, v -> config.armorDurabilityProtection = v, - "工具耐久保护", () -> config.toolDurabilityProtection, v -> config.toolDurabilityProtection = v); - y += sectionGap; + // ===== 固定区:底部操作按钮(永远可见,不随内容滚动) ===== + 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)); - // ===== 主世界专用设置 ===== - if (config.task == MineTask.OVERWORLD) { - y = addSectionHeader(y, left, "主世界设置", panelW); - this.levelBtn = numberRow("挖掘层 (Y)", left, y, panelW, - () -> config.overworldLevel, v -> config.overworldLevel = v, -64, 320); - y += rowH + gap; - this.tunnelBtn = numberRow("主干巷道长度", left, y, panelW, - () -> config.tunnelLength, v -> config.tunnelLength = v, 4, 512); - y += rowH + gap; + this.viewportTop = taskY + 26; + this.viewportBottom = actionY - 8; - // 矿物输入框 - y = addTextFieldRow(y, left, panelW, "目标矿物 (逗号分隔)", - config.overworldOres, tf -> this.overworldOresField = tf, - v -> config.overworldOres = v); - y += sectionGap; + // ===== 滚动区内容(虚拟坐标从 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 += 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.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; + } } - // ===== 下界专用设置 ===== - if (config.task == MineTask.NETHER) { - y = addSectionHeader(y, left, "下界设置", panelW); - this.countBtn = numberRow("目标数量 (-1无限)", left, y, panelW, - () -> config.netherTargetCount, v -> config.netherTargetCount = v, -1, 512); - y += rowH + gap; - y = addCheckboxRow(y, left, panelW, - "挖完/背包满/血量低/耐久低 自动回主世界", () -> config.netherAutoReturn, v -> config.netherAutoReturn = v, - "", () -> false, v -> {}); // 单列占满 - y += rowH + gap; - this.fleeHBtn = numberRow("返程血量阈值%", left, y, panelW, - () -> config.netherFleeHealthThreshold, v -> config.netherFleeHealthThreshold = v, 0, 100); - y += rowH + gap; - - // 矿物输入框 - y = addTextFieldRow(y, left, panelW, "目标矿物 (逗号分隔)", - config.netherOres, tf -> this.netherOresField = tf, - v -> config.netherOres = v); - y += sectionGap; - } - - // ===== 装备耐久阈值(通用,但仅在开启保护时有效)===== - if (config.armorDurabilityProtection || config.toolDurabilityProtection) { - y = addSectionHeader(y, left, "装备耐久阈值", panelW); - this.armorDurBtn = numberRow("护甲耐久 %", left, y, panelW, - () -> config.armorDurabilityThreshold, v -> config.armorDurabilityThreshold = v, 1, 100); - y += rowH + gap; - this.toolDurBtn = numberRow("工具耐久 %", left, y, panelW, - () -> config.toolDurabilityThreshold, v -> config.toolDurabilityThreshold = v, 1, 100); - y += sectionGap; - } - - // ===== 底部按钮 ===== - int btnY = Math.max(y, this.height - 48); - int bW = (panelW - gap * 2) / 3; - this.addDrawableChild(textButton("开始", b -> start(), left, btnY, bW)); - this.addDrawableChild(textButton("停止", b -> stop(), left + bW + gap, btnY, bW)); - this.addDrawableChild(textButton("仅关闭", b -> closeOnly(), left + (bW + gap) * 2, btnY, bW)); + this.contentHeight = y; + this.clampScroll(); + this.applyScroll(); } - // ========== 布局辅助 ========== + // ================= 布局辅助 ================= - private int addSectionHeader(int y, int left, String title, int panelW) { - // 画分隔线 - this.addDrawableChild(ButtonWidget.builder(Text.literal(""), b -> {}).dimensions(left, y + 10, panelW, 1).build()); - // 标题文本会在 render 中绘制,这里只预留空间 - return y + 18; + /** 分区标题(金色文本 + 下划线)。 */ + private int header(int y, String text) { + this.texts.add(new TextEntry(this.panelLeft, y, () -> text, 0xFFD700, false, true)); + return y + 16; } - private int addCheckboxRow(int y, int left, int panelW, - String label1, java.util.function.BooleanSupplier g1, Consumer s1, - String label2, java.util.function.BooleanSupplier g2, Consumer s2) { - int halfW = (panelW - 8) / 2; - CheckboxWidget c1 = CheckboxWidget.builder(Text.literal(label1), this.textRenderer) - .pos(left, y).checked(g1.getAsBoolean()) - .callback((b, v) -> s1.accept(v)).build(); - c1.setWidth(halfW); - this.addDrawableChild(c1); - if (!label2.isEmpty()) { - CheckboxWidget c2 = CheckboxWidget.builder(Text.literal(label2), this.textRenderer) - .pos(left + halfW + 8, y).checked(g2.getAsBoolean()) - .callback((b, v) -> s2.accept(v)).build(); - c2.setWidth(halfW); - this.addDrawableChild(c2); + /** + * 一行复选框:两列放得下则并排;放不下(窄屏/长文本)自动上下堆叠, + * 保证文字不会互相重叠。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 addTextFieldRow(int y, int left, int panelW, String label, - String initialValue, Consumer refSetter, Consumer onChange) { - // 标签 - this.addDrawableChild(ButtonWidget.builder(Text.literal(label), b -> {}).dimensions(left, y, panelW, 18).build()); - y += 20; - // 输入框 - TextFieldWidget tf = new TextFieldWidget(this.textRenderer, left, y, panelW, 20, Text.literal("")); - tf.setText(initialValue); - tf.setChangedListener(onChange); - tf.setMaxLength(200); - refSetter.accept(tf); - this.addDrawableChild(tf); + /** 文本输入行:标签 + 输入框(占满面板宽度)。 */ + 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; } - private ButtonWidget numberRow(String label, int left, int y, int panelW, - java.util.function.IntSupplier getter, Consumer setter, int min, int max) { - int btnSize = 22; - int labelW = 110; - int valW = 60; - int x = left; - this.addDrawableChild(ButtonWidget.builder(Text.literal("-"), b -> { - setter.accept(clamp(getter.getAsInt() - 1, min, max)); - refreshNumberButtons(); - }).dimensions(x, y, btnSize, btnSize).build()); - x += btnSize + 4; - // 标签按钮(不可点击) - ButtonWidget lbl = ButtonWidget.builder(Text.literal(label), b -> {}).dimensions(x, y, labelW, btnSize).build(); - lbl.active = false; - this.addDrawableChild(lbl); - x += labelW + 4; - // 数值显示 - ButtonWidget val = ButtonWidget.builder(Text.literal("" + getter.getAsInt()), b -> {}).dimensions(x, y, valW, btnSize).build(); - val.active = false; - this.addDrawableChild(val); - x += valW + 4; - this.addDrawableChild(ButtonWidget.builder(Text.literal("+"), b -> { - setter.accept(clamp(getter.getAsInt() + 1, min, max)); - refreshNumberButtons(); - }).dimensions(x, y, btnSize, btnSize).build()); - return val; - } - - private int clamp(int v, int min, int max) { - return Math.max(min, Math.min(max, v)); - } - - private void refreshNumberButtons() { - if (levelBtn != null) levelBtn.setMessage(Text.literal("" + config.overworldLevel)); - if (tunnelBtn != null) tunnelBtn.setMessage(Text.literal("" + config.tunnelLength)); - if (countBtn != null) countBtn.setMessage(Text.literal("" + config.netherTargetCount)); - if (armorDurBtn != null) armorDurBtn.setMessage(Text.literal("" + config.armorDurabilityThreshold)); - if (toolDurBtn != null) toolDurBtn.setMessage(Text.literal("" + config.toolDurabilityThreshold)); - if (fleeHBtn != null) fleeHBtn.setMessage(Text.literal("" + config.netherFleeHealthThreshold)); + /** 注册可滚动控件(真实位置在 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 = config.task == task; + boolean selected = this.config.task == task; String text = selected ? "■ " + label : "□ " + label; - ButtonWidget b = textButton(text, button -> { - config.task = task; - init(); // 完全重建 UI 以显示/隐藏对应区域 - }, x, y, w); - this.addDrawableChild(b); - return b; + return this.addDrawableChild(this.textButton(text, b -> { + 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, 22).build(); + 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; @@ -251,30 +283,65 @@ public final class AutoDigGui extends Screen { @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, 10, 0xFFFFFF); + 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, - "任务: " + config.task.label() + " 状态: " + controller.statusText(), - 10, this.height - 20, 0xAAAAAA); - - // 绘制分组标题(因为用 ButtonWidget 做分隔线不好看,改用直接绘制) - int centerX = this.width / 2; - int panelW = Math.min(380, this.width - 40); - int left = centerX - panelW / 2; - int y = 30 + 20 + 14; // 任务行 + sectionGap - // 通用设置标题 - context.drawTextWithShadow(this.textRenderer, "通用设置", left, y - 16, 0xFFD700); - // 动态计算后续分组标题位置比较麻烦,改在 init 时记录或直接在 render 时估算 - // 简化:只画标题栏背景 + "任务: " + this.config.task.label() + " 状态: " + this.controller.statusText(), + this.panelLeft, this.height - 14, 0xAAAAAA); } - /** 把界面临时配置写回 mod 全局配置并保存。 */ + // ================= 应用与操作 ================= + + /** 同步文本输入框的最新值到本地配置副本。 */ + private void syncFields() { + if (this.overworldOresField != null) { + this.config.overworldOres = this.overworldOresField.getText(); + } + if (this.netherOresField != null) { + this.config.netherOres = this.netherOresField.getText(); + } + } + + /** 把界面临时配置写回 mod 全局配置并保存(供 controller 实时读取)。 */ private void applyToMod() { DigConfig target = this.mod.config(); target.task = this.config.task; @@ -301,19 +368,25 @@ public final class AutoDigGui extends Screen { } private void start() { + this.syncFields(); if (this.config.task == MineTask.NONE) { this.mod.sendToast("AutoDig: 请先选择一个挖矿任务(主世界 / 下界)"); return; } - // 同步输入框最新值 - if (this.overworldOresField != null) this.config.overworldOres = this.overworldOresField.getText(); - if (this.netherOresField != null) this.config.netherOres = this.netherOresField.getText(); + 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(); @@ -321,6 +394,7 @@ public final class AutoDigGui extends Screen { this.close(); } + /** 仅关闭:丢弃未应用的改动。 */ private void closeOnly() { this.applyOnClose = false; this.close(); @@ -329,9 +403,7 @@ public final class AutoDigGui extends Screen { @Override public void close() { if (this.applyOnClose) { - // 同步输入框最新值 - if (this.overworldOresField != null) this.config.overworldOres = this.overworldOresField.getText(); - if (this.netherOresField != null) this.config.netherOres = this.netherOresField.getText(); + this.syncFields(); this.applyToMod(); } super.close();