aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorolim88 <bobq4582@gmail.com>2025-03-03 04:07:43 +0000
committerGitHub <noreply@github.com>2025-03-03 12:07:43 +0800
commite428f5aca33d42bf35f6ee0dc8edc8df2cc753b3 (patch)
tree8348e7d8043e2cbe9fd99f2052bf11e808d848b8
parent9219939d0769b10f4ec504f57d98a60c24247185 (diff)
downloadSkyblocker-e428f5aca33d42bf35f6ee0dc8edc8df2cc753b3.tar.gz
Skyblocker-e428f5aca33d42bf35f6ee0dc8edc8df2cc753b3.tar.bz2
Skyblocker-e428f5aca33d42bf35f6ee0dc8edc8df2cc753b3.zip
Health bars (#1028)
* basic features working prototype for the feature * add config and main features * add option to show on mobs with only a health value also adds tooltips for options * clean up code and add more comments * increase default size * improve removing health code and incorrectly spell amour * show when the nametag is shown let it be rendered though walls if the nametag is being rendered * add suggested changes add fading between colours and fix problems * Disable depth test and cleanup * Migrate to oklab interpolation * Fix health bar alpha * Default off --------- Co-authored-by: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java78
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java33
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/HealthBars.java233
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ColorUtils.java51
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java46
-rw-r--r--src/main/resources/assets/skyblocker/lang/en_us.json19
-rw-r--r--src/test/java/de/hysky/skyblocker/utils/ColorUtilsTest.java19
8 files changed, 462 insertions, 19 deletions
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
index 118d84e2..d076b7fd 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
@@ -610,6 +610,84 @@ public class UIAndVisualsCategory {
.build()
)
+ //Custom Health bars
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.enabled"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.enabled.@Tooltip")))
+ .binding(defaults.uiAndVisuals.healthBars.enabled,
+ () -> config.uiAndVisuals.healthBars.enabled,
+ newValue -> config.uiAndVisuals.healthBars.enabled = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Float>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.scale"))
+ .binding(defaults.uiAndVisuals.healthBars.scale,
+ () -> config.uiAndVisuals.healthBars.scale,
+ newValue -> config.uiAndVisuals.healthBars.scale = newValue)
+ .controller(FloatFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.removeHealthFromName"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.removeHealthFromName.@Tooltip")))
+ .binding(defaults.uiAndVisuals.healthBars.removeHealthFromName,
+ () -> config.uiAndVisuals.healthBars.removeHealthFromName,
+ newValue -> config.uiAndVisuals.healthBars.removeHealthFromName = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.removeMaxHealthFromName"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.removeMaxHealthFromName.@Tooltip")))
+ .binding(defaults.uiAndVisuals.healthBars.removeMaxHealthFromName,
+ () -> config.uiAndVisuals.healthBars.removeMaxHealthFromName,
+ newValue -> config.uiAndVisuals.healthBars.removeMaxHealthFromName = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.applyToHealthOnlyMobs"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.applyToHealthOnlyMobs.@Tooltip")))
+ .binding(defaults.uiAndVisuals.healthBars.applyToHealthOnlyMobs,
+ () -> config.uiAndVisuals.healthBars.applyToHealthOnlyMobs,
+ newValue -> config.uiAndVisuals.healthBars.applyToHealthOnlyMobs = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.hideFullHealth"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.hideFullHealth.@Tooltip")))
+ .binding(defaults.uiAndVisuals.healthBars.hideFullHealth,
+ () -> config.uiAndVisuals.healthBars.hideFullHealth,
+ newValue -> config.uiAndVisuals.healthBars.hideFullHealth = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Color>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.fullBarColor"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.fullBarColor.@Tooltip")))
+ .binding(defaults.uiAndVisuals.healthBars.fullBarColor,
+ () -> config.uiAndVisuals.healthBars.fullBarColor,
+ newValue -> config.uiAndVisuals.healthBars.fullBarColor = newValue)
+ .controller(ColorControllerBuilder::create)
+ .build())
+ .option(Option.<Color>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.halfBarColor"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.halfBarColor.@Tooltip")))
+ .binding(defaults.uiAndVisuals.healthBars.halfBarColor,
+ () -> config.uiAndVisuals.healthBars.halfBarColor,
+ newValue -> config.uiAndVisuals.healthBars.halfBarColor = newValue)
+ .controller(ColorControllerBuilder::create)
+ .build())
+ .option(Option.<Color>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.emptyBarColor"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.healthBars.emptyBarColor.@Tooltip")))
+ .binding(defaults.uiAndVisuals.healthBars.emptyBarColor,
+ () -> config.uiAndVisuals.healthBars.emptyBarColor,
+ newValue -> config.uiAndVisuals.healthBars.emptyBarColor = newValue)
+ .controller(ColorControllerBuilder::create)
+ .build())
+ .build()
+ )
+
.build();
}
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
index 980a219a..90efa5d2 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
@@ -89,6 +89,9 @@ public class UIAndVisualsConfig {
@SerialEntry
public CompactDamage compactDamage = new CompactDamage();
+ @SerialEntry
+ public HealthBars healthBars = new HealthBars();
+
public static class ChestValue {
@SerialEntry
public boolean enableChestValue = true;
@@ -417,4 +420,34 @@ public class UIAndVisualsConfig {
@SerialEntry
public Color critDamageGradientEnd = new Color(0xFF5555);
}
+
+ public static class HealthBars {
+ @SerialEntry
+ public boolean enabled = false;
+
+ @SerialEntry
+ public float scale = 1.5f;
+
+ @SerialEntry
+ public boolean removeHealthFromName = true;
+
+ @SerialEntry
+ public boolean removeMaxHealthFromName = true;
+
+ @SerialEntry
+ public boolean applyToHealthOnlyMobs = true;
+
+ @SerialEntry
+ public boolean hideFullHealth = false;
+
+
+ @SerialEntry
+ public Color fullBarColor = new Color(0x00FF00);
+
+ @SerialEntry
+ public Color halfBarColor = new Color(0xFF4600);
+
+ @SerialEntry
+ public Color emptyBarColor = new Color(0xFF0000);
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
index 0bc9a393..477b7adc 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
@@ -10,6 +10,7 @@ import de.hysky.skyblocker.config.configs.SlayersConfig;
import de.hysky.skyblocker.config.configs.UIAndVisualsConfig;
import de.hysky.skyblocker.skyblock.CompactDamage;
import de.hysky.skyblocker.skyblock.FishingHelper;
+import de.hysky.skyblocker.skyblock.HealthBars;
import de.hysky.skyblocker.skyblock.SmoothAOTE;
import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
@@ -76,6 +77,7 @@ public abstract class ClientPlayNetworkHandlerMixin extends ClientCommonNetworkH
EggFinder.checkIfEgg(armorStandEntity);
CorpseFinder.checkIfCorpse(armorStandEntity);
+ HealthBars.heathBar(armorStandEntity);
try { //Prevent packet handling fails if something goes wrong so that entity trackers still update, just without compact damage numbers
CompactDamage.compactDamage(armorStandEntity);
} catch (Exception e) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/HealthBars.java b/src/main/java/de/hysky/skyblocker/skyblock/HealthBars.java
new file mode 100644
index 00000000..32f93040
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/HealthBars.java
@@ -0,0 +1,233 @@
+package de.hysky.skyblocker.skyblock;
+
+import de.hysky.skyblocker.annotations.Init;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ColorUtils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.objects.Object2FloatMap;
+import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents;
+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.Vec3d;
+import org.apache.commons.lang3.StringUtils;
+
+import java.awt.*;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class HealthBars {
+
+ private static final Identifier HEALTH_BAR_BACKGROUND_TEXTURE = Identifier.ofVanilla("textures/gui/sprites/boss_bar/white_background.png");
+ private static final Identifier HEALTH_BAR_TEXTURE = Identifier.ofVanilla("textures/gui/sprites/boss_bar/white_progress.png");
+ private static final Pattern HEALTH_PATTERN = Pattern.compile("(\\d{1,3}(,\\d{3})*(\\.\\d+)?)/(\\d{1,3}(,\\d{3})*(\\.\\d+)?)❤");
+ private static final Pattern HEALTH_ONLY_PATTERN = Pattern.compile("(\\d{1,3}(,\\d{3})*(\\.\\d+)?)❤");
+
+ private static final Object2FloatOpenHashMap<ArmorStandEntity> healthValues = new Object2FloatOpenHashMap<>();
+ private static final Object2IntOpenHashMap<ArmorStandEntity> mobStartingHealth = new Object2IntOpenHashMap<>();
+
+ @Init
+ public static void init() {
+ ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset());
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(HealthBars::render);
+ ClientEntityEvents.ENTITY_UNLOAD.register(HealthBars::onEntityDespawn);
+ }
+
+ private static void reset() {
+ healthValues.clear();
+ mobStartingHealth.clear();
+ }
+
+ /**
+ * remove dead armor stands from health bars
+ *
+ * @param entity dying entity
+ */
+ public static void onEntityDespawn(Entity entity, ClientWorld clientWorld) {
+ if (entity instanceof ArmorStandEntity armorStandEntity) {
+ healthValues.removeFloat(armorStandEntity);
+ mobStartingHealth.removeInt(armorStandEntity);
+ }
+ }
+
+ /**
+ * Processes armorstand updates and if it's a mob with health get the value of its health and save it the hashmap
+ *
+ * @param armorStand updated armorstand
+ */
+ public static void heathBar(ArmorStandEntity armorStand) {
+ if (!armorStand.isInvisible() || !armorStand.hasCustomName() || !armorStand.isCustomNameVisible() || !SkyblockerConfigManager.get().uiAndVisuals.healthBars.enabled) {
+ return;
+ }
+
+ //check if armor stand is dead and remove it from list
+ if (armorStand.isDead()) {
+ healthValues.removeFloat(armorStand);
+ mobStartingHealth.removeInt(armorStand);
+ return;
+ }
+
+ //check to see if the armor stand is a mob label with health
+ if (armorStand.getCustomName() == null) {
+ return;
+ }
+ Matcher healthMatcher = HEALTH_PATTERN.matcher(armorStand.getCustomName().getString());
+ //if a health ratio can not be found send onto health only pattern
+ if (!healthMatcher.find()) {
+ healthOnlyCheck(armorStand);
+ return;
+ }
+
+ //work out health value and save to hashMap
+ int firstValue = Integer.parseInt(healthMatcher.group(1).replace(",", ""));
+ int secondValue = Integer.parseInt(healthMatcher.group(4).replace(",", ""));
+ float health = (float) firstValue / secondValue;
+ healthValues.put(armorStand, health);
+
+ //edit armor stand name to remove health
+ boolean removeValue = SkyblockerConfigManager.get().uiAndVisuals.healthBars.removeHealthFromName;
+ boolean removeMax = SkyblockerConfigManager.get().uiAndVisuals.healthBars.removeMaxHealthFromName;
+ //if both disabled no need to edit name
+ if (!removeValue && !removeMax) {
+ return;
+ }
+ MutableText cleanedText = Text.empty();
+ List<Text> parts = armorStand.getCustomName().getSiblings();
+ //loop though name and add every part to a new text skipping over the hidden health values
+ int healthStartIndex = -1;
+ for (int i = 0; i < parts.size(); i++) {
+ //remove value from name
+ if (i < parts.size() - 4 && StringUtils.join(parts.subList(i + 1, i + 5).stream().map(Text::getString).toArray(), "").equals(healthMatcher.group(0))) {
+ healthStartIndex = i;
+ }
+ if (healthStartIndex != -1) {
+ //skip parts of the health offset form staring index
+ switch (i - healthStartIndex) {
+ case 0 -> { // space before health
+ if (removeMax && removeValue) {
+ continue;
+ }
+ }
+ case 1 -> { // current health value
+ if (removeValue) {
+ continue;
+ }
+ }
+ case 2 -> { // "/" separating health values
+ if (removeMax) {
+ continue;
+ }
+ }
+ case 3 -> { // max health value
+ if (removeMax) {
+ continue;
+ }
+ }
+ case 4 -> { // "❤" at end of health
+ if (removeMax && removeValue) {
+ continue;
+ }
+ }
+ }
+ }
+
+ cleanedText.append(parts.get(i));
+ }
+ armorStand.setCustomName(cleanedText);
+ }
+
+ /**
+ * Processes armor stands that only have a health value and no max health
+ *
+ * @param armorStand armorstand to check the name of
+ */
+ private static void healthOnlyCheck(ArmorStandEntity armorStand) {
+ if (!SkyblockerConfigManager.get().uiAndVisuals.healthBars.applyToHealthOnlyMobs || armorStand.getCustomName() == null) {
+ return;
+ }
+ Matcher healthOnlyMatcher = HEALTH_ONLY_PATTERN.matcher(armorStand.getCustomName().getString());
+ //if not found return
+ if (!healthOnlyMatcher.find()) {
+ return;
+ }
+
+ //get the current health of the mob
+ int currentHealth = Integer.parseInt(healthOnlyMatcher.group(1).replace(",", ""));
+
+ //if it's a new health only armor stand add to starting health lookup (not always full health if already damaged but best that can be done)
+ if (!mobStartingHealth.containsKey(armorStand)) {
+ mobStartingHealth.put(armorStand, currentHealth);
+ }
+
+ //add to health bar values
+ float health = (float) currentHealth / mobStartingHealth.getInt(armorStand);
+ healthValues.put(armorStand, health);
+
+ //if enabled remove from name
+ if (!SkyblockerConfigManager.get().uiAndVisuals.healthBars.removeHealthFromName) {
+ return;
+ }
+ MutableText cleanedText = Text.empty();
+ List<Text> parts = armorStand.getCustomName().getSiblings();
+ //loop though name and add every part to a new text skipping over the health value
+ for (int i = 0; i < parts.size(); i++) {
+ //skip space before value, value and heart from name
+ if (i < parts.size() - 2 && parts.subList(i + 1, i + 3).stream().map(Text::getString).collect(Collectors.joining()).equals(healthOnlyMatcher.group(0))) {
+ //skip the heart
+ i += 2;
+ continue;
+ }
+ cleanedText.append(parts.get(i));
+ }
+ armorStand.setCustomName(cleanedText);
+ }
+
+ /**
+ * Loops though armor stands with health bars and renders a bar for each of them just bellow the name label
+ *
+ * @param context render context
+ */
+ private static void render(WorldRenderContext context) {
+ if (!SkyblockerConfigManager.get().uiAndVisuals.healthBars.enabled || healthValues.isEmpty()) {
+ return;
+ }
+ Color fullColor = SkyblockerConfigManager.get().uiAndVisuals.healthBars.fullBarColor;
+ Color halfColor = SkyblockerConfigManager.get().uiAndVisuals.healthBars.halfBarColor;
+ Color emptyColor = SkyblockerConfigManager.get().uiAndVisuals.healthBars.emptyBarColor;
+ boolean hideFullHealth = SkyblockerConfigManager.get().uiAndVisuals.healthBars.hideFullHealth;
+ float scale = SkyblockerConfigManager.get().uiAndVisuals.healthBars.scale;
+ float tickDelta = context.tickCounter().getTickDelta(false);
+ float width = scale;
+ float height = scale * 0.1f;
+
+ for (Object2FloatMap.Entry<ArmorStandEntity> healthValue : healthValues.object2FloatEntrySet()) {
+ //if the health bar is full and the setting is enabled to hide it stop rendering it
+ float health = healthValue.getFloatValue();
+ if (hideFullHealth && health == 1) {
+ continue;
+ }
+
+ ArmorStandEntity armorStand = healthValue.getKey();
+ //only render health bar if name is visible
+ if (!armorStand.shouldRenderName()) {
+ return;
+ }
+ //gets the mixed color of the health bar
+ int mixedColor = ColorUtils.interpolate(health, emptyColor.getRGB(), halfColor.getRGB(), fullColor.getRGB());
+ float[] components = ColorUtils.getFloatComponents(mixedColor);
+ // Render the health bar texture with scaling based on health percentage
+ RenderHelper.renderTextureInWorld(context, armorStand.getCameraPosVec(tickDelta).add(0, 0.25 - height, 0), width, height, 1f, 1f, new Vec3d(width * -0.5f, 0, 0), HEALTH_BAR_BACKGROUND_TEXTURE, components, 1f, true);
+ RenderHelper.renderTextureInWorld(context, armorStand.getCameraPosVec(tickDelta).add(0, 0.25 - height, 0), width * health, height, health, 1f, new Vec3d(width * -0.5f, 0, 0.003f), HEALTH_BAR_TEXTURE, components, 1f, true);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java b/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java
index a9ea6a9f..0a2ca7d8 100644
--- a/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java
@@ -1,6 +1,7 @@
package de.hysky.skyblocker.utils;
import net.minecraft.util.DyeColor;
+import net.minecraft.util.math.ColorHelper;
import net.minecraft.util.math.MathHelper;
public class ColorUtils {
@@ -12,9 +13,9 @@ public class ColorUtils {
*/
public static float[] getFloatComponents(int color) {
return new float[] {
- ((color >> 16) & 0xFF) / 255f,
- ((color >> 8) & 0xFF) / 255f,
- (color & 0xFF) / 255f
+ ColorHelper.getRedFloat(color),
+ ColorHelper.getGreenFloat(color),
+ ColorHelper.getBlueFloat(color),
};
}
@@ -34,25 +35,37 @@ public class ColorUtils {
}
/**
- * Interpolates linearly between two colours.
+ * Interpolates between two colours.
*/
- //Credit to https://codepen.io/OliverBalfour/post/programmatically-making-gradients
public static int interpolate(int firstColor, int secondColor, double percentage) {
- int r1 = MathHelper.square((firstColor >> 16) & 0xFF);
- int g1 = MathHelper.square((firstColor >> 8) & 0xFF);
- int b1 = MathHelper.square(firstColor & 0xFF);
-
- int r2 = MathHelper.square((secondColor >> 16) & 0xFF);
- int g2 = MathHelper.square((secondColor >> 8) & 0xFF);
- int b2 = MathHelper.square(secondColor & 0xFF);
-
- double inverse = 1d - percentage;
-
- int r3 = (int) Math.floor(Math.sqrt(r1 * inverse + r2 * percentage));
- int g3 = (int) Math.floor(Math.sqrt(g1 * inverse + g2 * percentage));
- int b3 = (int) Math.floor(Math.sqrt(b1 * inverse + b2 * percentage));
+ return OkLabColor.interpolate(firstColor, secondColor, (float) percentage);
+ }
- return (r3 << 16) | (g3 << 8 ) | b3;
+ /**
+ * Interpolates between multiple colors.
+ *
+ * @param percentage percentage between 0 and 1
+ * @param colors the colors to interpolate between
+ * @return the interpolated color
+ * @see #interpolate(int, int, double)
+ */
+ public static int interpolate(double percentage, int... colors) {
+ int colorCount = colors.length;
+ if (colorCount == 0) {
+ return 0;
+ }
+ if (colorCount == 1 || percentage <= 0) {
+ return colors[0];
+ }
+ if (percentage >= 1) {
+ return colors[colorCount - 1];
+ }
+
+ double scaledPercentage = percentage * (colorCount - 1);
+ int index = (int) Math.floor(scaledPercentage);
+ double remainder = scaledPercentage - index;
+
+ return interpolate(colors[index], colors[index + 1], remainder);
}
/**
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
index e203767a..b77ef21b 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
@@ -292,6 +292,52 @@ public class RenderHelper {
RenderSystem.disableDepthTest();
}
+ /**
+ * Renders a texture in world space facing the player (like a name tag)
+ * @param context world render context
+ * @param pos world position
+ * @param width rendered width
+ * @param height rendered height
+ * @param textureWidth amount of texture rendered width
+ * @param textureHeight amount of texture rendered height
+ * @param renderOffset offset once it's been placed in the world facing the player
+ * @param texture reference to texture to render
+ * @param shaderColor color to apply to the texture
+ * @param throughWalls if it should render though walls
+ */
+ public static void renderTextureInWorld(WorldRenderContext context, Vec3d pos, float width, float height, float textureWidth, float textureHeight, Vec3d renderOffset, Identifier texture, float[] shaderColor, float alpha, boolean throughWalls) {
+ Matrix4f positionMatrix = new Matrix4f();
+ Camera camera = context.camera();
+ Vec3d cameraPos = camera.getPos();
+
+ positionMatrix
+ .translate((float) (pos.getX() - cameraPos.getX()), (float) (pos.getY() - cameraPos.getY()), (float) (pos.getZ() - cameraPos.getZ()))
+ .rotate(camera.getRotation());
+
+ Tessellator tessellator = RenderSystem.renderThreadTesselator();
+
+ RenderSystem.setShader(ShaderProgramKeys.POSITION_TEX);
+ RenderSystem.setShaderTexture(0, texture);
+ RenderSystem.setShaderColor(shaderColor[0], shaderColor[1], shaderColor[2], alpha);
+ RenderSystem.enableBlend();
+ RenderSystem.defaultBlendFunc();
+ RenderSystem.disableCull();
+ RenderSystem.depthFunc(throughWalls ? GL11.GL_ALWAYS : GL11.GL_LEQUAL);
+ BufferBuilder buffer = tessellator.begin(DrawMode.QUADS, VertexFormats.POSITION_TEXTURE);
+
+ buffer.vertex(positionMatrix, (float) renderOffset.getX(), (float) renderOffset.getY(), (float) renderOffset.getZ()).texture(1, 1 - textureHeight);
+ buffer.vertex(positionMatrix, (float) renderOffset.getX(), (float) renderOffset.getY() + height, (float) renderOffset.getZ()).texture(1, 1);
+ buffer.vertex(positionMatrix, (float) renderOffset.getX() + width, (float) renderOffset.getY() + height , (float) renderOffset.getZ()).texture(1 - textureWidth, 1);
+ buffer.vertex(positionMatrix, (float) renderOffset.getX() + width, (float) renderOffset.getY(), (float) renderOffset.getZ()).texture(1 - textureWidth, 1 - textureHeight);
+
+ BufferRenderer.drawWithGlobalProgram(buffer.end());
+
+ RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
+ RenderSystem.enableCull();
+ RenderSystem.depthFunc(GL11.GL_LEQUAL);
+ RenderSystem.disableDepthTest();
+ }
+
public static void renderText(WorldRenderContext context, Text text, Vec3d pos, boolean throughWalls) {
renderText(context, text, pos, 1, throughWalls);
}
diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json
index c3627f3c..0bb0fd0c 100644
--- a/src/main/resources/assets/skyblocker/lang/en_us.json
+++ b/src/main/resources/assets/skyblocker/lang/en_us.json
@@ -824,6 +824,25 @@
"skyblocker.config.uiAndVisuals.hideEmptyTooltips": "Hide empty item tooltips in menus",
"skyblocker.config.uiAndVisuals.hideEmptyTooltips.@Tooltip": "Hides the tooltip of an item if it doesn't have any information to display. Also blocks clicks on filler items like glass panes.",
+ "skyblocker.config.uiAndVisuals.healthBars": "Custom Mob Health Bars",
+ "skyblocker.config.uiAndVisuals.healthBars.enabled": "Enabled",
+ "skyblocker.config.uiAndVisuals.healthBars.enabled.@Tooltip": "(only updates when health updates)",
+ "skyblocker.config.uiAndVisuals.healthBars.scale": "Scale",
+ "skyblocker.config.uiAndVisuals.healthBars.removeHealthFromName": "Remove Health From Name",
+ "skyblocker.config.uiAndVisuals.healthBars.removeHealthFromName.@Tooltip": "Remove the current health of mobs from their nametag",
+ "skyblocker.config.uiAndVisuals.healthBars.removeMaxHealthFromName": "Remove Max Health From Name",
+ "skyblocker.config.uiAndVisuals.healthBars.removeMaxHealthFromName.@Tooltip": "Remove the maximum health of mobs from their nametag",
+ "skyblocker.config.uiAndVisuals.healthBars.applyToHealthOnlyMobs": "Apply to Health Only Mobs",
+ "skyblocker.config.uiAndVisuals.healthBars.applyToHealthOnlyMobs.@Tooltip": "Also add health bars to mobs with only a current health value (will sometimes be wrong if mob is damaged before loading in)",
+ "skyblocker.config.uiAndVisuals.healthBars.hideFullHealth": "Hide Full Health Bars",
+ "skyblocker.config.uiAndVisuals.healthBars.hideFullHealth.@Tooltip": "Don't show the health bar when its full",
+ "skyblocker.config.uiAndVisuals.healthBars.fullBarColor": "Full Health bar Color",
+ "skyblocker.config.uiAndVisuals.healthBars.fullBarColor.@Tooltip": "Color of health bar when mob is at full hp",
+ "skyblocker.config.uiAndVisuals.healthBars.halfBarColor": "Half Health Bar Color",
+ "skyblocker.config.uiAndVisuals.healthBars.halfBarColor.@Tooltip": "Color of health bar when mob is at half hp",
+ "skyblocker.config.uiAndVisuals.healthBars.emptyBarColor": "Empty Health Bar Color",
+ "skyblocker.config.uiAndVisuals.healthBars.emptyBarColor.@Tooltip": "Color of health bar when mob is at full hp",
+
"skyblocker.config.uiAndVisuals.inputCalculator": "Input Calculator",
"skyblocker.config.uiAndVisuals.inputCalculator.enabled": "Enable Sign Calculator",
"skyblocker.config.uiAndVisuals.inputCalculator.enabled.@Tooltip": "Enables the ability for you to do calculations when inputting values such as price for the ah.\n Key:\n S = 64\n E = 160\n K = 1,000\n M = 1,000,000\n B = 1,000,000,000\n\n purse/P = current purse value",
diff --git a/src/test/java/de/hysky/skyblocker/utils/ColorUtilsTest.java b/src/test/java/de/hysky/skyblocker/utils/ColorUtilsTest.java
new file mode 100644
index 00000000..bd250511
--- /dev/null
+++ b/src/test/java/de/hysky/skyblocker/utils/ColorUtilsTest.java
@@ -0,0 +1,19 @@
+package de.hysky.skyblocker.utils;
+
+import net.minecraft.util.math.ColorHelper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class ColorUtilsTest {
+ @Test
+ void testFloatComponents() {
+ Assertions.assertArrayEquals(new float[]{0.2f, 0.4f, 0.6f}, ColorUtils.getFloatComponents(ColorHelper.getArgb(51, 102, 153)));
+ }
+
+ @Test
+ void testInterpolate() {
+ Assertions.assertEquals(0x00FE00, ColorUtils.interpolate(0.5, 0xFF0000, 0x00FF00, 0x0000FF));
+ Assertions.assertEquals(0xD0A800, ColorUtils.interpolate(0.25, 0xFF0000, 0x00FF00, 0x0000FF));
+ Assertions.assertEquals(0x00A9BE, ColorUtils.interpolate(0.75, 0xFF0000, 0x00FF00, 0x0000FF));
+ }
+}