fix(ui): 固定底栏+滚动区布局,彻底解决按钮越界与文字重叠
- 顶部任务选择与底部开始/停止/仅关闭按钮固定在屏幕内,永不出界 - 中间设置区改为可滚动容器(滚轮滚动、右侧滚动条、scissor 裁剪防溢出) - 面板宽度随窗口自适应(200~400px),窗口缩放不再挤压布局 - 复选框两列放不下时自动上下堆叠,消除文字重叠 - 数值行改为 [-] 标签 ... 值 [+] 结构,标签左对齐、数值右对齐、动态刷新 - 矿物输入框占满面板宽度,可见可编辑 - 空闲任务时显示引导文案而非一堆无关选项 - 开始前校验矿物列表非空
这个提交包含在:
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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(响应式 + 滚动布局)。
|
||||
*
|
||||
* <p>结构:顶部标题与任务选择、底部操作按钮(开始/停止/仅关闭)固定不动,
|
||||
* 中间为可滚动设置区(滚轮滚动,右侧有滚动条)。设置区内容按当前任务动态
|
||||
* 显示:主世界只显示主世界相关项,下界只显示下界相关项,避免无关选项干扰。
|
||||
* 面板宽度随窗口自适应,标签过长时复选框自动改为上下堆叠,避免文字重叠。</p>
|
||||
*/
|
||||
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<ScrollEntry> scrollable = new ArrayList<>();
|
||||
|
||||
/** 可滚动的纯文本(分区标题、数值、字段标签),渲染时按滚动偏移绘制 */
|
||||
private record TextEntry(int x, int virtualY, Supplier<String> text, int color, boolean rightAlign, boolean underline) {}
|
||||
private final List<TextEntry> 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<Boolean> s1,
|
||||
String label2, java.util.function.BooleanSupplier g2, Consumer<Boolean> 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<Boolean> set1,
|
||||
String label2, BooleanSupplier get2, Consumer<Boolean> 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<Boolean> 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<Integer> 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<TextFieldWidget> refSetter, Consumer<String> 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<TextFieldWidget> 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<Integer> 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<String> 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();
|
||||
|
||||
在新工单中引用
屏蔽一个用户