diff --git a/gradle.properties b/gradle.properties index 6fe62d0..64b11af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.21.4 yarn_mappings=1.21.4+build.8 loader_version=0.16.10 # Mod Properties -mod_version=1.14.514.006 +mod_version=1.14.514.008 maven_group=org.branulf archives_base_name=branulf_toolbox # Dependencies diff --git a/src/main/java/org/branulf/toolbox/Main.java b/src/main/java/org/branulf/toolbox/Main.java index 489a873..ef04724 100644 --- a/src/main/java/org/branulf/toolbox/Main.java +++ b/src/main/java/org/branulf/toolbox/Main.java @@ -16,6 +16,7 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.option.KeyBinding; import net.minecraft.client.util.InputUtil; import net.minecraft.text.Text; +import org.branulf.toolbox.simplehud.SimpleHudHandler; import org.lwjgl.glfw.GLFW; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +48,7 @@ public class Main implements ClientModInitializer { public ElytraBoostHandler elytraBoostHandler; public PlayerDetectorHandler playerDetectorHandler; public DamageLoggerHandler damageLoggerHandler; + public SimpleHudHandler simpleHudHandler; @Override public void onInitializeClient() { @@ -68,6 +70,7 @@ public class Main implements ClientModInitializer { elytraBoostHandler = new ElytraBoostHandler(config); playerDetectorHandler = new PlayerDetectorHandler(config); damageLoggerHandler = new DamageLoggerHandler(config.damageLogger); + simpleHudHandler = new SimpleHudHandler(config); ClientTickEvents.END_CLIENT_TICK.register(client -> { while (openMainGuiKeyBinding.wasPressed()) { diff --git a/src/main/java/org/branulf/toolbox/MainScreen.java b/src/main/java/org/branulf/toolbox/MainScreen.java index 91bdb9e..8571a5b 100644 --- a/src/main/java/org/branulf/toolbox/MainScreen.java +++ b/src/main/java/org/branulf/toolbox/MainScreen.java @@ -35,6 +35,7 @@ public class MainScreen extends Screen { private final List scrollableWidgets = new ArrayList<>(); private ButtonWidget configButton; private TextFieldWidget playerDetectorRangeField; + private ButtonWidget simpleHudToggleButton; /** * 使用无法点击的按钮作为标题,是为了方便将所有UI元素(包括标题)都作为一个子类来管理, @@ -91,6 +92,11 @@ public class MainScreen extends Screen { currentContentY = addDamageLoggerButtons(currentContentY); currentContentY += SECTION_SPACING; + // HUD + currentContentY = addSectionTitle(Text.translatable("gui.branulf_toolbox.simple_hud.title").formatted(Formatting.AQUA), currentContentY); + currentContentY = addSimpleHudButtons(currentContentY); + currentContentY += SECTION_SPACING; + totalContentHeight = currentContentY; // 配置 @@ -334,6 +340,23 @@ public class MainScreen extends Screen { return row2Y + BUTTON_HEIGHT + PADDING; } + // HUD + private int addSimpleHudButtons(int currentContentY) { + ModConfig.SimpleHudConfig config = Main.getConfig().simpleHud; + + simpleHudToggleButton = ButtonWidget.builder( + Text.translatable("gui.branulf_toolbox.simple_hud.toggle"), + button -> { + config.enabled = !config.enabled; + Main.saveConfig(); + updateButtonStates(); + }) + .dimensions(this.width / 2 - BUTTON_WIDTH / 2, currentContentY, BUTTON_WIDTH, BUTTON_HEIGHT) + .build(); + scrollableWidgets.add(simpleHudToggleButton); + return currentContentY + BUTTON_HEIGHT + PADDING; + } + private void updateButtonStates() { ModConfig config = Main.getConfig(); ScrafitiptyPlusHandler scrafitiptyPlusHandler = Main.getInstance().scrafitiptyPlusHandler; @@ -388,6 +411,12 @@ public class MainScreen extends Screen { } else if (button.getMessage().getString().contains(Text.translatable("gui.branulf_toolbox.damage_logger.disable").getString())) { button.active = config.damageLogger.enabled; } + // HUD + else if (button == simpleHudToggleButton) { + button.setMessage(Text.translatable("gui.branulf_toolbox.simple_hud.toggle") + .append(Text.literal(" (" + (config.simpleHud.enabled ? Text.translatable("branulf_toolbox.enabled").getString() : Text.translatable("branulf_toolbox.disabled").getString()) + ")")) + .formatted(config.simpleHud.enabled ? Formatting.GREEN : Formatting.RED)); + } } } if (playerDetectorRangeField != null) { diff --git a/src/main/java/org/branulf/toolbox/ModConfig.java b/src/main/java/org/branulf/toolbox/ModConfig.java index f83ffa4..2be7a70 100644 --- a/src/main/java/org/branulf/toolbox/ModConfig.java +++ b/src/main/java/org/branulf/toolbox/ModConfig.java @@ -45,6 +45,11 @@ public class ModConfig implements ConfigData { @ConfigEntry.Gui.TransitiveObject public ShowEnchantsConfig showEnchants = new ShowEnchantsConfig(); + // HUD + @ConfigEntry.Category("simple_hud_settings") + @ConfigEntry.Gui.TransitiveObject + public SimpleHudConfig simpleHud = new SimpleHudConfig(); + // ————————————————————分割线———————————————————— @@ -131,4 +136,19 @@ public class ModConfig implements ConfigData { @ConfigEntry.Gui.Tooltip(count = 2) public boolean enabled = true; } + + // HUD + public static class SimpleHudConfig implements ConfigData { + @ConfigEntry.Gui.Tooltip(count = 0) + public boolean enabled = false; + + @ConfigEntry.Gui.Tooltip(count = 0) + public boolean compassHudEnabled = true; + + @ConfigEntry.Gui.Tooltip(count = 0) + public boolean healthHungerHudEnabled = true; + + @ConfigEntry.Gui.Tooltip(count = 0) + public boolean crossbowHudEnabled = true; + } } diff --git a/src/main/java/org/branulf/toolbox/simplehud/SimpleHudHandler.java b/src/main/java/org/branulf/toolbox/simplehud/SimpleHudHandler.java new file mode 100644 index 0000000..3aafd32 --- /dev/null +++ b/src/main/java/org/branulf/toolbox/simplehud/SimpleHudHandler.java @@ -0,0 +1,316 @@ +package org.branulf.toolbox.simplehud; + +import com.mojang.blaze3d.systems.ProjectionType; +import com.mojang.blaze3d.systems.RenderSystem; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.render.RenderTickCounter; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.render.*; +import net.minecraft.item.CrossbowItem; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.MathHelper; +import org.branulf.toolbox.Main; +import org.branulf.toolbox.ModConfig; +import org.joml.Matrix4f; +import net.minecraft.client.gl.ShaderProgramKeys; +import org.joml.Matrix4fStack; + +public class SimpleHudHandler { + private final ModConfig config; + private final MinecraftClient client; + + public SimpleHudHandler(ModConfig config) { + this.config = config; + this.client = MinecraftClient.getInstance(); + HudRenderCallback.EVENT.register(this::renderHud); + Main.LOGGER.info("BRanulf的实用工具箱 - 简单HUD已加载!"); + } + + private void renderHud(DrawContext context, RenderTickCounter renderTickCounter) { + float tickDelta = renderTickCounter.getTickDelta(true); + + if (client.player == null || client.isPaused() || !config.simpleHud.enabled) { + return; + } + + if (config.simpleHud.compassHudEnabled) { + renderCompassHud(context, tickDelta); + } + if (config.simpleHud.healthHungerHudEnabled) { + renderHealthHungerArcs(context, tickDelta); + } + if (config.simpleHud.crossbowHudEnabled) { + renderCrossbowReloadBar(context, tickDelta); + } + } + + // 罗盘HUD + private void renderCompassHud(DrawContext context, float tickDelta) { + ClientPlayerEntity player = client.player; + if (player == null) return; + + int screenWidth = context.getScaledWindowWidth(); + int screenHeight = context.getScaledWindowHeight(); + + int barWidth = 200; + int barHeight = 10; + int barX = (screenWidth - barWidth) / 2; + int barY = 5; + + context.fill(barX, barY, barX + barWidth, barY + barHeight, 0x40000000); + + float yaw = MathHelper.wrapDegrees(player.getYaw()); + + float normalizedYaw = (yaw + 360) % 360; + + context.fill(barX + barWidth / 2 - 1, barY, barX + barWidth / 2 + 1, barY + barHeight, 0x8800FF00); + + float visibleDegrees = 180f; + float degreesPerPixel = visibleDegrees / barWidth; + + String[] directions = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}; + int[] angles = {0, 45, 90, 135, 180, 225, 270, 315}; + + for (int i = 0; i < 360; i += 5) { + float angle = (float) i; + + float angleDifference = MathHelper.wrapDegrees(angle - normalizedYaw); + float markerX = barX + barWidth / 2 + (angleDifference / degreesPerPixel); + + if (markerX >= barX && markerX <= barX + barWidth) { + int markerHeight = 3; + int textColor = 0x77FFFFFF; + + boolean isMajorDirection = false; + for (int j = 0; j < angles.length; j++) { + if (angle == angles[j]) { + markerHeight = 8; + textColor = 0x77FFFF00; + context.drawTextWithShadow(client.textRenderer, directions[j], (int) markerX - client.textRenderer.getWidth(directions[j]) / 2, barY + barHeight + 2, textColor); + isMajorDirection = true; + break; + } + } + + if (!isMajorDirection) { + if (i % 15 == 0) { + markerHeight = 6; + textColor = 0x7700FFFF; + context.drawTextWithShadow(client.textRenderer, String.valueOf(i), (int) markerX - client.textRenderer.getWidth(String.valueOf(i)) / 2, barY + barHeight + 2, textColor); + } else if (i % 5 == 0) { + markerHeight = 4; + textColor = 0x77FFFFFF; + } + } + context.fill((int) markerX - 1, barY + barHeight - markerHeight, (int) markerX + 1, barY + barHeight, textColor); + } + } + + int angleBoxWidth = 50; + int angleBoxHeight = 15; + int angleBoxX = (screenWidth - angleBoxWidth) / 2; + int angleBoxY = barY + barHeight + 10; + + context.fill(angleBoxX, angleBoxY, angleBoxX + angleBoxWidth, angleBoxY + angleBoxHeight, 0x00000000); // 暂时不用背景 + String angleText = String.format("%.1f", normalizedYaw); + context.drawCenteredTextWithShadow(client.textRenderer, Text.literal(angleText), angleBoxX + angleBoxWidth / 2, angleBoxY + (angleBoxHeight - client.textRenderer.fontHeight) / 2, 0x77FFFFFF); + } + + // 生命饱食度俩半圆 + private void renderHealthHungerArcs(DrawContext context, float tickDelta) { + ClientPlayerEntity player = client.player; + if (player == null) return; + + int screenWidth = context.getScaledWindowWidth(); + int screenHeight = context.getScaledWindowHeight(); + int centerX = screenWidth / 2; + int centerY = screenHeight / 2; + + // 主参数 + int radius = 30; + int arcThickness = 4; + int totalArcDegrees = 90; + + // 白条参数(新增的,当辅助看) + int whiteBarThickness = 1; + int whiteBarColor = 0x77FFFFFF; + + float mainArcOuterEdgeRadius = radius + arcThickness / 2f; + float whiteBarCenterRadius = mainArcOuterEdgeRadius + whiteBarThickness / 2f; + + float maxHealth = player.getMaxHealth(); + if (maxHealth <= 0) maxHealth = 1.0f; + + // 伤害吸收 + float absorption = player.getAbsorptionAmount(); + float absorptionPercentage = absorption / maxHealth; + absorptionPercentage = MathHelper.clamp(absorptionPercentage, 0.0f, 1.0f); + if (absorption > 0) { + drawArc(context, centerX, centerY, (int)whiteBarCenterRadius, whiteBarThickness, 135, totalArcDegrees, absorptionPercentage, whiteBarColor, true); + } + + // 血条 + float health = player.getHealth(); + float healthPercentage = health / maxHealth; + int healthColor = getColorForPercentage(healthPercentage); + drawArc(context, centerX, centerY, radius, arcThickness, 135, totalArcDegrees, healthPercentage, healthColor, true); + + float maxHunger = 20.0f; + + // 隐藏饱食度 + float saturation = player.getHungerManager().getSaturationLevel(); + float saturationPercentage = saturation / maxHunger; + saturationPercentage = MathHelper.clamp(saturationPercentage, 0.0f, 1.0f); + if (saturation > 0) { + drawArc(context, centerX, centerY, (int)whiteBarCenterRadius, whiteBarThickness, 45, totalArcDegrees, saturationPercentage, whiteBarColor, false); + } + + // 饱食度 + float hunger = player.getHungerManager().getFoodLevel(); + float hungerPercentage = hunger / maxHunger; + int hungerColor = getColorForPercentage(hungerPercentage); + drawArc(context, centerX, centerY, radius, arcThickness, 45, totalArcDegrees, hungerPercentage, hungerColor, false); + } + + private int getColorForPercentage(float percentage) { + if (percentage > 0.80f) { + return 0x7700EEFF; // 青 + } else if (percentage > 0.30f) { + return 0x7700FF00; // 绿 + } else { + return 0x77FF0000; // 红 + } + } + + private void drawArc(DrawContext context, int centerX, int centerY, int radius, int thickness, int startAngleDegrees, int totalArcDegrees, float fillPercentage, int color, boolean clockwise) { + RenderSystem.backupProjectionMatrix(); + Matrix4f originalProjection = RenderSystem.getProjectionMatrix(); + Matrix4fStack modelViewStack = RenderSystem.getModelViewStack(); + modelViewStack.pushMatrix(); + + try { + RenderSystem.setShader(ShaderProgramKeys.POSITION_COLOR); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.disableDepthTest(); + RenderSystem.disableCull(); + + Matrix4f projectionMatrix = new Matrix4f().setOrtho( + 0, context.getScaledWindowWidth(), + context.getScaledWindowHeight(), 0, + -1000, 1000 + ); + RenderSystem.setProjectionMatrix(projectionMatrix, ProjectionType.ORTHOGRAPHIC); + + modelViewStack.identity(); + modelViewStack.translate(0, 0, 0); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder bufferBuilder = tessellator.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR); + + int segments = 60; + float angleStep = (float) totalArcDegrees / segments; + + int filledSegments = (int) (segments * fillPercentage); + + float alpha = ((color >> 24) & 0xFF) / 255.0F; + float red = ((color >> 16) & 0xFF) / 255.0F; + float green = ((color >> 8) & 0xFF) / 255.0F; + float blue = (color & 0xFF) / 255.0F; + + Matrix4f matrix4f = modelViewStack; + + for (int i = 0; i < filledSegments; i++) { + float currentAngle; + float nextAngle; + + if (clockwise) { + currentAngle = startAngleDegrees + i * angleStep; + nextAngle = startAngleDegrees + (i + 1) * angleStep; + } else { + currentAngle = startAngleDegrees - i * angleStep; + nextAngle = startAngleDegrees - (i + 1) * angleStep; + } + + float currentAngleRad = (float) Math.toRadians(currentAngle); + float nextAngleRad = (float) Math.toRadians(nextAngle); + + float innerRadius = radius - thickness / 2f; + float outerRadius = radius + thickness / 2f; + + float x1_inner = centerX + innerRadius * MathHelper.cos(currentAngleRad); + float y1_inner = centerY + innerRadius * MathHelper.sin(currentAngleRad); + float x1_outer = centerX + outerRadius * MathHelper.cos(currentAngleRad); + float y1_outer = centerY + outerRadius * MathHelper.sin(currentAngleRad); + + float x2_inner = centerX + innerRadius * MathHelper.cos(nextAngleRad); + float y2_inner = centerY + innerRadius * MathHelper.sin(nextAngleRad); + float x2_outer = centerX + outerRadius * MathHelper.cos(nextAngleRad); + float y2_outer = centerY + outerRadius * MathHelper.sin(nextAngleRad); + + bufferBuilder.vertex(matrix4f, x1_inner, y1_inner, 0).color(red, green, blue, alpha); + bufferBuilder.vertex(matrix4f, x1_outer, y1_outer, 0).color(red, green, blue, alpha); + bufferBuilder.vertex(matrix4f, x2_outer, y2_outer, 0).color(red, green, blue, alpha); + bufferBuilder.vertex(matrix4f, x2_inner, y2_inner, 0).color(red, green, blue, alpha); + } + + BuiltBuffer builtBuffer = bufferBuilder.end(); + if (builtBuffer != null) { + BufferRenderer.drawWithGlobalProgram(builtBuffer); + } + } finally { + modelViewStack.popMatrix(); + RenderSystem.setProjectionMatrix(originalProjection, ProjectionType.ORTHOGRAPHIC); + RenderSystem.enableDepthTest(); + RenderSystem.enableCull(); + RenderSystem.disableBlend(); + } + } + + // 弩换弹条(暂不可用) + private void renderCrossbowReloadBar(DrawContext context, float tickDelta) { + ClientPlayerEntity player = client.player; + if (player == null) return; + + ItemStack mainHandStack = player.getMainHandStack(); + if (!mainHandStack.isOf(Items.CROSSBOW)) { + return; + } + + int screenWidth = context.getScaledWindowWidth(); + int screenHeight = context.getScaledWindowHeight(); + + int barWidth = 100; + int barHeight = 3; + int barX = (screenWidth - barWidth) / 2; + int barY = screenHeight / 2 + 25; + + float pullProgress = CrossbowItem.getPullTime(mainHandStack, player); + pullProgress = MathHelper.clamp(pullProgress, 0.0f, 1.0f); + boolean isCharged = CrossbowItem.isCharged(mainHandStack); + + int fillColor; + int filledWidth; + + context.fill(barX, barY, barX + barWidth, barY + barHeight, 0x55000000); + + if (isCharged) { + fillColor = 0x7700EEFF; // 青 + filledWidth = barWidth; + context.fill(barX, barY, barX + filledWidth, barY + barHeight, fillColor); + } else if (pullProgress > 0.0f) { + fillColor = 0x7700FF00; // 绿 + filledWidth = (int) (barWidth * pullProgress); + int leftFillX = barX + (barWidth - filledWidth) / 2; + int rightFillX = barX + (barWidth + filledWidth) / 2; + context.fill(leftFillX, barY, rightFillX, barY + barHeight, fillColor); + } else { + // 未装填状态 + } + } +} diff --git a/src/main/resources/assets/branulf_toolbox/lang/en_us.json b/src/main/resources/assets/branulf_toolbox/lang/en_us.json index 6eb0715..0fe20a2 100644 --- a/src/main/resources/assets/branulf_toolbox/lang/en_us.json +++ b/src/main/resources/assets/branulf_toolbox/lang/en_us.json @@ -16,6 +16,7 @@ "text.autoconfig.branulf_toolbox.category.player_detector_settings": "Player Detector Settings", "text.autoconfig.branulf_toolbox.category.damage_logger_settings": "Damage Logger Settings", "text.autoconfig.branulf_toolbox.category.show_enchants_settings": "Show Enchants Settings", + "text.autoconfig.branulf_toolbox.category.simple_hud_settings": "Simple HUD Settings", "gui.branulf_toolbox.autocrossbow.title": "Auto Crossbow", "gui.branulf_toolbox.autocrossbow.enabled_on": "Enable Auto Crossbow", @@ -97,5 +98,12 @@ "text.autoconfig.branulf_toolbox.option.showEnchants.enabled": "Enable Show Enchants", "text.autoconfig.branulf_toolbox.option.showEnchants.enabled.@Tooltip[0]": "If enabled, the first character of the main enchantment will be displayed on item stacks.", - "text.autoconfig.branulf_toolbox.option.showEnchants.enabled.@Tooltip[1]": "Reference from Fabrication" + "text.autoconfig.branulf_toolbox.option.showEnchants.enabled.@Tooltip[1]": "Reference from Fabrication", + + "gui.branulf_toolbox.simple_hud.title": "Simple HUD", + "gui.branulf_toolbox.simple_hud.toggle": "Toggle Simple HUD", + "text.autoconfig.branulf_toolbox.option.simpleHud.enabled": "Enable Simple HUD", + "text.autoconfig.branulf_toolbox.option.simpleHud.compassHudEnabled": "Enable Compass HUD", + "text.autoconfig.branulf_toolbox.option.simpleHud.healthHungerHudEnabled": "Enable Health/Hunger HUD", + "text.autoconfig.branulf_toolbox.option.simpleHud.crossbowHudEnabled": "Enable Crossbow Reload HUD" } diff --git a/src/main/resources/assets/branulf_toolbox/lang/zh_cn.json b/src/main/resources/assets/branulf_toolbox/lang/zh_cn.json index 1fdf9c3..7f23999 100644 --- a/src/main/resources/assets/branulf_toolbox/lang/zh_cn.json +++ b/src/main/resources/assets/branulf_toolbox/lang/zh_cn.json @@ -16,6 +16,7 @@ "text.autoconfig.branulf_toolbox.category.player_detector_settings": "玩家探测设置", "text.autoconfig.branulf_toolbox.category.damage_logger_settings": "伤害记录设置", "text.autoconfig.branulf_toolbox.category.show_enchants_settings": "显示附魔设置", + "text.autoconfig.branulf_toolbox.category.simple_hud_settings": "简单HUD设置", "gui.branulf_toolbox.autocrossbow.title": "诸葛连弩", "gui.branulf_toolbox.autocrossbow.enabled_on": "启用诸葛连弩", @@ -97,5 +98,12 @@ "text.autoconfig.branulf_toolbox.option.showEnchants.enabled": "启用显示附魔", "text.autoconfig.branulf_toolbox.option.showEnchants.enabled.@Tooltip[0]": "如果启用,物品堆上将显示主要附魔的第一个字符。", - "text.autoconfig.branulf_toolbox.option.showEnchants.enabled.@Tooltip[1]": "参考自Fabrication" + "text.autoconfig.branulf_toolbox.option.showEnchants.enabled.@Tooltip[1]": "参考自Fabrication", + + "gui.branulf_toolbox.simple_hud.title": "简单HUD", + "gui.branulf_toolbox.simple_hud.toggle": "切换简单HUD", + "text.autoconfig.branulf_toolbox.option.simpleHud.enabled": "启用简单HUD", + "text.autoconfig.branulf_toolbox.option.simpleHud.compassHudEnabled": "启用罗盘HUD", + "text.autoconfig.branulf_toolbox.option.simpleHud.healthHungerHudEnabled": "启用生命/饱食度HUD", + "text.autoconfig.branulf_toolbox.option.simpleHud.crossbowHudEnabled": "启用弩装填HUD" }