Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Health bars #1028

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,66 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig
.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"))
.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.barColor"))
.binding(defaults.uiAndVisuals.healthBars.barColor,
() -> config.uiAndVisuals.healthBars.barColor,
newValue -> config.uiAndVisuals.healthBars.barColor = newValue)
.controller(ColorControllerBuilder::create)
.build())
.build()
)

.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,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;
Expand Down Expand Up @@ -281,4 +284,27 @@ public static class CompactDamage {
@SerialEntry
public Color critDamageGradientEnd = new Color(0xFF5555);
}

public static class HealthBars {
@SerialEntry
public boolean enabled = true;

@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 barColor = new Color(0xFFFFFF);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import de.hysky.skyblocker.config.configs.SlayersConfig;
import de.hysky.skyblocker.skyblock.CompactDamage;
import de.hysky.skyblocker.skyblock.FishingHelper;
import de.hysky.skyblocker.skyblock.HealthBars;
import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
import de.hysky.skyblocker.skyblock.crimson.slayer.FirePillarAnnouncer;
Expand Down Expand Up @@ -126,6 +127,7 @@ public abstract class ClientPlayNetworkHandlerMixin {
EggFinder.checkIfEgg(armorStandEntity);
try { //Prevent packet handling fails if something goes wrong so that entity trackers still update, just without compact damage numbers
CompactDamage.compactDamage(armorStandEntity);
HealthBars.HeathBar(armorStandEntity);
} catch (Exception e) {
LOGGER.error("[Skyblocker Compact Damage] Failed to compact damage number", e);
}
Expand Down
227 changes: 227 additions & 0 deletions src/main/java/de/hysky/skyblocker/skyblock/HealthBars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package de.hysky.skyblocker.skyblock;

import de.hysky.skyblocker.annotations.Init;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
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;

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;
System.out.println(healthMatcher.group(0).toString()+"testing");
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 && StringUtils.join(parts.subList(i+1, i + 3).stream().map(Text::getString).toArray(), "").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 barColor = SkyblockerConfigManager.get().uiAndVisuals.healthBars.barColor;
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;
}
// 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, barColor, 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, barColor, true);
}
}
}
Loading