diff --git a/README.md b/README.md index 3a8d744..f55148d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Display Health and Mana in a ring around the crosshair on Hypixel SkyBlock. **New:** Ported from a 1.8.9 ChatTriggers module into a 1.21.10 Fabric mod. +The Fabric version of StatsRing depends on the [Cloth Config API](https://modrinth.com/mod/cloth-config). + Optionally include percentages, visual alerts on low stats, and smooth animation. Useful for combat across SkyBlock. diff --git a/build.gradle b/build.gradle index 3157e44..fb7c61e 100644 --- a/build.gradle +++ b/build.gradle @@ -11,11 +11,7 @@ base { } repositories { - // Add repositories to retrieve artifacts from in here. - // You should only use this when depending on other mods because - // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. - // See https://docs.gradle.org/current/userguide/declaring_repositories.html - // for more information about repositories. + maven { url "https://maven.shedaniel.me/" } } dependencies { @@ -26,7 +22,11 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" - + + // Cloth Config for settings GUI + modApi("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { + exclude(group: "net.fabricmc.fabric-api") + } } processResources { diff --git a/gradle.properties b/gradle.properties index 9937105..6e43a8f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,5 @@ maven_group=me.bubner.statsring archives_base_name=statsring # Dependencies -fabric_api_version=0.138.4+1.21.10 \ No newline at end of file +fabric_api_version=0.138.4+1.21.10 +cloth_config_version=20.0.148 \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/me/bubner/statsring/ConfigScreen.java b/src/main/java/me/bubner/statsring/ConfigScreen.java new file mode 100644 index 0000000..bd4dac4 --- /dev/null +++ b/src/main/java/me/bubner/statsring/ConfigScreen.java @@ -0,0 +1,95 @@ +package me.bubner.statsring; + +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +/** + * Cloth Config screen for StatsRing settings. + * Replaces the original Vigilance config GUI from config.js. + * + * @author Lucas Bubner, 2023 (Original CT module) + */ +public class ConfigScreen { + private ConfigScreen() { + } + + /** + * Create the Cloth Config settings screen. + * + * @param parent the parent screen to return to + * @param config the mod configuration instance + * @return the built config screen + */ + public static Screen create(Screen parent, ModConfig config) { + ConfigBuilder builder = ConfigBuilder.create() + .setParentScreen(parent) + .setTitle(Component.empty() + .append(Component.literal("Stats Ring: ").withStyle(ChatFormatting.BOLD)) + .append(Component.literal("Health ").withStyle(ChatFormatting.RED)) + .append(Component.literal("and ")) + .append(Component.literal("Mana ").withStyle(ChatFormatting.AQUA)) + .append(Component.literal("ring"))); + + ConfigEntryBuilder entryBuilder = builder.entryBuilder(); + + // Core category + ConfigCategory core = builder.getOrCreateCategory(Component.literal("Core")); + core.addEntry(entryBuilder.startBooleanToggle(Component.literal("Enable ring"), config.getActive()) + .setDefaultValue(true) + .setTooltip(Component.literal("Render the ring (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("active", String.valueOf(val))) + .build()); + core.addEntry(entryBuilder.startBooleanToggle(Component.literal("Show percentages"), config.getPercentage()) + .setDefaultValue(true) + .setTooltip(Component.literal("Render percentages of health/mana next to ring (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("percentage", String.valueOf(val))) + .build()); + core.addEntry(entryBuilder.startBooleanToggle(Component.literal("Show absorption"), config.getAbsorption()) + .setDefaultValue(true) + .setTooltip(Component.literal("Show over 100% health for absorption hearts (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("absorption", String.valueOf(val))) + .build()); + + // Display category + ConfigCategory display = builder.getOrCreateCategory(Component.literal("Display")); + display.addEntry(entryBuilder.startBooleanToggle(Component.literal("Show backing image"), config.getBackingImage()) + .setDefaultValue(true) + .setTooltip(Component.literal("Show the background image for the ring that surrounds the bars (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("backingImage", String.valueOf(val))) + .build()); + display.addEntry(entryBuilder.startBooleanToggle(Component.literal("Interpolate colour"), config.getInterpolateColour()) + .setDefaultValue(true) + .setTooltip(Component.literal("Linear interpolates mana and health colour depending on percentage filled (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("interpolateColour", String.valueOf(val))) + .build()); + display.addEntry(entryBuilder.startBooleanToggle(Component.literal("Interpolate bars"), config.getInterpolateBars()) + .setDefaultValue(true) + .setTooltip(Component.literal("Linear interpolates the filled progress of the bars (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("interpolateBars", String.valueOf(val))) + .build()); + + // Visual Warnings category + ConfigCategory warnings = builder.getOrCreateCategory(Component.literal("Visual Warnings")); + warnings.addEntry(entryBuilder.startIntField(Component.literal("Low HP percent alert threshold"), (int) config.getAlertLowHpPercent()) + .setDefaultValue(40) + .setMin(-1) + .setMax(100) + .setTooltip(Component.literal("Visually flashes HP when equal to or below this percentage (% from 0 to 100, -1 to disable)")) + .setSaveConsumer(val -> config.properties.setProperty("alertLowHpPercent", String.valueOf(val))) + .build()); + warnings.addEntry(entryBuilder.startIntField(Component.literal("Low Mana percent alert threshold"), (int) config.getAlertLowManaPercent()) + .setDefaultValue(20) + .setMin(-1) + .setMax(100) + .setTooltip(Component.literal("Visually flashes Mana when equal to or below this percentage (% from 0 to 100, -1 to disable)")) + .setSaveConsumer(val -> config.properties.setProperty("alertLowManaPercent", String.valueOf(val))) + .build()); + + builder.setSavingRunnable(config::save); + return builder.build(); + } +} diff --git a/src/main/java/me/bubner/statsring/ManaReadStatus.java b/src/main/java/me/bubner/statsring/ManaReadStatus.java new file mode 100644 index 0000000..89851ed --- /dev/null +++ b/src/main/java/me/bubner/statsring/ManaReadStatus.java @@ -0,0 +1,12 @@ +package me.bubner.statsring; + +/** + * Represents the mana read status from the action bar. + * + * @author Lucas Bubner, 2023 (Original CT module) + */ +public enum ManaReadStatus { + OK, + FROZEN, + NOT_ENOUGH_MANA +} diff --git a/src/main/java/me/bubner/statsring/ModConfig.java b/src/main/java/me/bubner/statsring/ModConfig.java new file mode 100644 index 0000000..a166787 --- /dev/null +++ b/src/main/java/me/bubner/statsring/ModConfig.java @@ -0,0 +1,79 @@ +package me.bubner.statsring; + +import net.fabricmc.loader.api.FabricLoader; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +/** + * Properties-based configuration for StatsRing. + * Serialises settings to a .properties file in the Fabric config directory. + * + * @author Lucas Bubner, 2023 (Original CT module) + */ +public class ModConfig { + private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("statsring.properties"); + public final Properties properties = new Properties(); + + public void load() { + if (Files.exists(CONFIG_PATH)) { + try (var reader = Files.newBufferedReader(CONFIG_PATH)) { + properties.load(reader); + } catch (IOException e) { + StatsRing.LOGGER.error("Failed to load config", e); + } + } else { + save(); + } + } + + public void save() { + try (var writer = Files.newBufferedWriter(CONFIG_PATH)) { + properties.store(writer, "StatsRing configuration"); + } catch (IOException e) { + StatsRing.LOGGER.error("Failed to save config", e); + } + } + + public boolean getActive() { + return Boolean.parseBoolean(properties.getProperty("active", "true")); + } + + public boolean getPercentage() { + return Boolean.parseBoolean(properties.getProperty("percentage", "true")); + } + + public boolean getAbsorption() { + return Boolean.parseBoolean(properties.getProperty("absorption", "true")); + } + + public boolean getBackingImage() { + return Boolean.parseBoolean(properties.getProperty("backingImage", "true")); + } + + public boolean getInterpolateColour() { + return Boolean.parseBoolean(properties.getProperty("interpolateColour", "true")); + } + + public boolean getInterpolateBars() { + return Boolean.parseBoolean(properties.getProperty("interpolateBars", "true")); + } + + public float getAlertLowHpPercent() { + try { + return Float.parseFloat(properties.getProperty("alertLowHpPercent", "40")); + } catch (NumberFormatException e) { + return 40f; + } + } + + public float getAlertLowManaPercent() { + try { + return Float.parseFloat(properties.getProperty("alertLowManaPercent", "20")); + } catch (NumberFormatException e) { + return 20f; + } + } +} diff --git a/src/main/java/me/bubner/statsring/StatsRing.java b/src/main/java/me/bubner/statsring/StatsRing.java index 19b64f8..886bbec 100644 --- a/src/main/java/me/bubner/statsring/StatsRing.java +++ b/src/main/java/me/bubner/statsring/StatsRing.java @@ -1,16 +1,42 @@ package me.bubner.statsring; import net.fabricmc.api.ClientModInitializer; - +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * StatsRing - Display Health and Mana in a ring around the crosshair on Hypixel SkyBlock. + * Ported from a 1.8.9 ChatTriggers module into a 1.21.10 Fabric mod. + * + * @author Lucas Bubner, 2023 (Original CT module) + */ public class StatsRing implements ClientModInitializer { - public static final String MOD_ID = "statsring"; - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + public static final String MOD_ID = "statsring"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + private volatile boolean scheduleOpenConfigScreen = false; + + @Override + public void onInitializeClient() { + final ModConfig config = new ModConfig(); + config.load(); + + new StatsRingRenderer(config).register(); - @Override - public void onInitializeClient() { - - } + // Register /ring command to open the settings GUI + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> + dispatcher.register(ClientCommandManager.literal("ring").executes(context -> { + scheduleOpenConfigScreen = true; + return 1; + })) + ); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (scheduleOpenConfigScreen) { + client.execute(() -> client.setScreen(ConfigScreen.create(client.screen, config))); + scheduleOpenConfigScreen = false; + } + }); + } } \ No newline at end of file diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java new file mode 100644 index 0000000..bfbf59a --- /dev/null +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -0,0 +1,247 @@ +package me.bubner.statsring; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElement; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.joml.Matrix3x2fStack; + +/** + * Port of main.js from the ChatTriggers StatsRing module. + * Handles action bar parsing for HP/Mana and renders the ring overlay on the HUD. + * + * @author Lucas Bubner, 2023 (Original CT module) + */ +public class StatsRingRenderer implements HudElement { + private static final ResourceLocation RING_TEXTURE = ResourceLocation.fromNamespaceAndPath("statsring", "ring-2.png"); + + private static final int HEIGHT_SCALE = 21; + private static final int RING_SIZE = 35; + private static final int BAR_WIDTH = 3; + private static final float PERCENT_SCALE = 0.75f; + + private static final int HP_BAR_X_OFFSET = -11; + private static final int MANA_BAR_X_OFFSET = 7; + private static final int BAR_BOTTOM_Y_OFFSET = 10; + + private static final int HP_ALERT_X_OFFSET = -14; + private static final int HP_ALERT_Y_OFFSET = -22; + private static final int MANA_ALERT_X_OFFSET = 5; + private static final int MANA_ALERT_Y_OFFSET = 13; + + private static final int HP_PERCENT_X_OFFSET_WIDE = -32; + private static final int HP_PERCENT_X_OFFSET = -28; + private static final int MANA_PERCENT_X_OFFSET = 13; + + private static final int NOT_ENOUGH_MANA_LATCH_TICKS = 40; + + private static final int COLOR_RED = 0xFFFF0000; + private static final int COLOR_AQUA = 0xFF00FFFF; + private static final int COLOR_WHITE = 0xFFFFFFFF; + private static final int COLOR_GRAY = 0xFF808080; + + private final ModConfig config; + private float hp = Float.NaN; + private float maxHp = Float.NaN; + private float mana = Float.NaN; + private float maxMana = Float.NaN; + private ManaReadStatus manaReadStatus = ManaReadStatus.OK; + private int notEnoughManaLatchTicks = 0; + private float secInterval = 0.4f; + private float hpScale = 0; + private float manaScale = 0; + private int ticks = 0; + private boolean cycle = false; + + public StatsRingRenderer(ModConfig config) { + this.config = config; + } + + /** + * Register all Fabric event listeners. + */ + public void register() { + ClientReceiveMessageEvents.GAME.register(this::onGameMessage); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> resetState()); + ClientTickEvents.END_CLIENT_TICK.register(client -> onTick()); + HudElementRegistry.attachElementAfter( + VanillaHudElements.CROSSHAIR, + ResourceLocation.fromNamespaceAndPath("statsring", "ring"), + this + ); + } + + private void onGameMessage(Component message, boolean overlay) { + if (!overlay) return; + if (!config.getActive() || !Util.isInSkyBlock()) return; + + String msg = message.getString(); + + // Extract health information + if (msg.contains("❤")) { + String hpStats = msg.split("❤")[0]; + String[] hpParts = hpStats.split("/"); + if (hpParts.length >= 2) { + hp = Util.parseStat(hpParts[0]); + maxHp = Util.parseStat(hpParts[1]); + } + } + + // Extract mana information + if (msg.contains("✎")) { + if (notEnoughManaLatchTicks <= 0) { + manaReadStatus = ManaReadStatus.OK; + } + String manaStats = msg.split("✎")[0]; + String[] manaParts = manaStats.split("/"); + if (manaParts.length >= 2) { + maxMana = Util.parseStat(manaParts[manaParts.length - 1]); + String[] p2 = manaParts[manaParts.length - 2].split(" "); + mana = Util.parseStat(p2[p2.length - 1]); + } + } else { + if (msg.contains("NOT ENOUGH MANA")) { + manaReadStatus = ManaReadStatus.NOT_ENOUGH_MANA; + notEnoughManaLatchTicks = NOT_ENOUGH_MANA_LATCH_TICKS; + } else if (notEnoughManaLatchTicks <= 0) { + manaReadStatus = ManaReadStatus.FROZEN; + } + } + } + + private void resetState() { + hp = Float.NaN; + maxHp = Float.NaN; + mana = Float.NaN; + maxMana = Float.NaN; + hpScale = 0; + manaScale = 0; + ticks = 0; + cycle = false; + notEnoughManaLatchTicks = 0; + } + + private void onTick() { + ticks++; + if (ticks >= (int) (20 * secInterval)) { + cycle = !cycle; + ticks = 0; + } + if (notEnoughManaLatchTicks > 0) { + notEnoughManaLatchTicks--; + if (notEnoughManaLatchTicks <= 0) { + manaReadStatus = ManaReadStatus.OK; + } + } + } + + @Override + public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { + boolean valuesAreNaN = Float.isNaN(hp) || Float.isNaN(maxHp) || Float.isNaN(mana) || Float.isNaN(maxMana); + if (!config.getActive() || valuesAreNaN || !Util.isInSkyBlock()) return; + + Minecraft mc = Minecraft.getInstance(); + int xCenter = mc.getWindow().getGuiScaledWidth() / 2; + int yCenter = mc.getWindow().getGuiScaledHeight() / 2; + + // Draw backing ring image + if (config.getBackingImage()) { + int ringX = xCenter - RING_SIZE / 2; + int ringY = yCenter - RING_SIZE / 2; + graphics.blit(RenderPipelines.GUI_TEXTURED, RING_TEXTURE, ringX, ringY, 0, 0, RING_SIZE, RING_SIZE, RING_SIZE, RING_SIZE); + } + + // === Health bar === + float healthPercent = (hp / maxHp) * 100f; + if (!config.getAbsorption()) healthPercent = Math.min(100f, healthPercent); + + int hpColour; + if (healthPercent > 100f) { + hpColour = Util.argb(255, 255, 217, 0); + } else { + int r = config.getInterpolateColour() ? Math.round(Util.lerp(139, 255, healthPercent / 100f)) : 255; + hpColour = Util.argb(255, r, 0, 0); + } + + float currentHpScale = Math.min(HEIGHT_SCALE, HEIGHT_SCALE * (hp / maxHp)); + hpScale = config.getInterpolateBars() ? Util.lerp(hpScale, currentHpScale, 0.1f) : currentHpScale; + int hpBarHeight = Math.round(hpScale); + + float lowHpPercent = config.getAlertLowHpPercent(); + if (lowHpPercent >= 0 && healthPercent <= lowHpPercent && cycle) { + Util.drawBoldText(graphics, mc, "!!!", xCenter + HP_ALERT_X_OFFSET, yCenter + HP_ALERT_Y_OFFSET, hpColour); + hpColour = config.getInterpolateColour() ? COLOR_RED : COLOR_WHITE; + graphics.fill(xCenter + HP_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - HEIGHT_SCALE, xCenter + HP_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET - hpBarHeight, Util.darkenRgb(hpColour, 0.25f)); + } + + graphics.fill(xCenter + HP_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - hpBarHeight, xCenter + HP_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET, hpColour); + + // === Mana bar === + float manaPercentage = Math.min(100f, (mana / maxMana) * 100f); + + int manaColour; + if (config.getInterpolateColour()) { + int r = Math.round(Util.lerp(200, 0, manaPercentage / 100f)); + int g = Math.round(Util.lerp(100, 255, manaPercentage / 100f)); + manaColour = Util.argb(255, r, g, 255); + } else { + manaColour = COLOR_AQUA; + } + + float currentManaScale = Math.min(HEIGHT_SCALE, HEIGHT_SCALE * (mana / maxMana)); + manaScale = config.getInterpolateBars() ? Util.lerp(manaScale, currentManaScale, 0.1f) : currentManaScale; + int manaBarHeight = Math.round(manaScale); + + float lowManaPercent = config.getAlertLowManaPercent(); + secInterval = manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA ? 0.2f : 0.4f; + + if (manaReadStatus == ManaReadStatus.FROZEN) { + graphics.fill(xCenter + MANA_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - HEIGHT_SCALE, xCenter + MANA_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET - manaBarHeight, Util.darkenRgb(COLOR_GRAY, 0.25f)); + } else if (((lowManaPercent >= 0 && manaPercentage <= lowManaPercent) || manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA) && !cycle) { + int foreColour = manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA ? COLOR_RED : (config.getInterpolateColour() ? COLOR_AQUA : COLOR_WHITE); + Util.drawBoldText(graphics, mc, "!!!", xCenter + MANA_ALERT_X_OFFSET, yCenter + MANA_ALERT_Y_OFFSET, foreColour); + manaColour = foreColour; + graphics.fill(xCenter + MANA_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - HEIGHT_SCALE, xCenter + MANA_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET - manaBarHeight, Util.darkenRgb(manaColour, 0.25f)); + } + + int manaBarColour = manaReadStatus != ManaReadStatus.FROZEN ? manaColour : COLOR_GRAY; + graphics.fill(xCenter + MANA_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - manaBarHeight, xCenter + MANA_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET, manaBarColour); + + // === Percentages === + if (!config.getPercentage()) return; + + int lineHeight = Math.round(mc.font.lineHeight * PERCENT_SCALE / 2f); + + // HP percentage + String hpText = Math.round(healthPercent) + "%"; + int hpTextX = Math.round(healthPercent) >= 100 ? xCenter + HP_PERCENT_X_OFFSET_WIDE : xCenter + HP_PERCENT_X_OFFSET; + int hpTextY = yCenter - lineHeight; + Matrix3x2fStack pose = graphics.pose(); + pose.pushMatrix(); + pose.translate(hpTextX, hpTextY); + pose.scale(PERCENT_SCALE); + pose.translate(-hpTextX, -hpTextY); + graphics.drawString(mc.font, hpText, hpTextX, hpTextY, hpColour, true); + pose.popMatrix(); + + // Mana percentage + int manaTextColour = manaReadStatus != ManaReadStatus.FROZEN ? manaColour : COLOR_GRAY; + String manaText = Math.round(manaPercentage) + "%"; + int manaTextX = xCenter + MANA_PERCENT_X_OFFSET; + int manaTextY = yCenter - lineHeight; + pose.pushMatrix(); + pose.translate(manaTextX, manaTextY); + pose.scale(PERCENT_SCALE); + pose.translate(-manaTextX, -manaTextY); + graphics.drawString(mc.font, manaText, manaTextX, manaTextY, manaTextColour, true); + pose.popMatrix(); + } +} diff --git a/src/main/java/me/bubner/statsring/Util.java b/src/main/java/me/bubner/statsring/Util.java new file mode 100644 index 0000000..4991bb2 --- /dev/null +++ b/src/main/java/me/bubner/statsring/Util.java @@ -0,0 +1,61 @@ +package me.bubner.statsring; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.world.scores.DisplaySlot; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.Scoreboard; + +import java.util.regex.Pattern; + +/** + * Shared utility methods for StatsRing rendering. + * + * @author Lucas Bubner, 2023 (Original CT module) + */ +public final class Util { + private static final Pattern COMMAS_FORMAT_COLOURS = Pattern.compile("(,|§.|§$)"); + + private Util() { + } + + public static boolean isInSkyBlock() { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return false; + Scoreboard scoreboard = mc.level.getScoreboard(); + Objective objective = scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR); + if (objective == null) return false; + return objective.getDisplayName().getString().contains("SKYBLOCK"); + } + + public static float parseStat(String stat) { + try { + // Remove any commas or format colours + return Integer.parseInt(COMMAS_FORMAT_COLOURS.matcher(stat).replaceAll("").trim()); + } catch (NumberFormatException e) { + return Float.NaN; + } + } + + public static float lerp(float start, float end, float t) { + return start + (end - start) * t; + } + + public static int argb(int a, int r, int g, int b) { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + public static int darkenRgb(int colour, float factor) { + int r = Math.round(((colour >> 16) & 0xFF) * factor); + int g = Math.round(((colour >> 8) & 0xFF) * factor); + int b = Math.round((colour & 0xFF) * factor); + return argb(255, r, g, b); + } + + public static void drawBoldText(GuiGraphics graphics, Minecraft mc, String text, int x, int y, int color) { + Component component = Component.literal(text).withStyle(Style.EMPTY.withBold(true)); + graphics.drawString(mc.font, component, x, y, color, true); + } +} diff --git a/src/main/resources/assets/statsring/ring-2.png b/src/main/resources/assets/statsring/ring-2.png index 836aa01..3224dd9 100644 Binary files a/src/main/resources/assets/statsring/ring-2.png and b/src/main/resources/assets/statsring/ring-2.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index fdaf9d8..379cbd5 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -25,6 +25,7 @@ "fabricloader": ">=0.18.4", "minecraft": "~1.21.10", "java": ">=21", - "fabric-api": "*" + "fabric-api": "*", + "cloth-config": "*" } } \ No newline at end of file