diff --git a/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java index f0e256a72d..14f5c24499 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java @@ -42,6 +42,15 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig newValue -> config.slayers.highlightBosses = newValue) .controller(ConfigUtils::createEnumCyclingListController) .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.slayer.bossbar")) + .description(OptionDescription.of( + Text.translatable("skyblocker.config.slayer.bossbar.@Tooltip"))) + .binding(defaults.slayers.displayBossbar, + () -> config.slayers.displayBossbar, + newValue -> config.slayers.displayBossbar = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) //Enderman Slayer .group(OptionGroup.createBuilder() diff --git a/src/main/java/de/hysky/skyblocker/config/configs/SlayersConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/SlayersConfig.java index 7503108bb0..e3149d52ef 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/SlayersConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/SlayersConfig.java @@ -10,6 +10,9 @@ public class SlayersConfig { @SerialEntry public HighlightSlayerEntities highlightBosses = HighlightSlayerEntities.OFF; + @SerialEntry + public boolean displayBossbar = true; + public enum HighlightSlayerEntities { OFF, GLOW, HITBOX; diff --git a/src/main/java/de/hysky/skyblocker/mixins/BossBarHudMixin.java b/src/main/java/de/hysky/skyblocker/mixins/BossBarHudMixin.java new file mode 100644 index 0000000000..a15489c5f7 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/BossBarHudMixin.java @@ -0,0 +1,42 @@ +package de.hysky.skyblocker.mixins; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.slayers.SlayerBossBars; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.BossBarHud; +import net.minecraft.client.gui.hud.ClientBossBar; +import net.minecraft.entity.boss.BossBar; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BossBarHud.class) +public abstract class BossBarHudMixin { + + @Final + @Shadow + private MinecraftClient client; + + @Shadow + protected abstract void renderBossBar(DrawContext context, int x, int y, BossBar bossBar); + + @Inject(method = "render", at = @At("HEAD"), cancellable = true) + private void onRender(DrawContext context, CallbackInfo ci) { + + if (SkyblockerConfigManager.get().slayers.displayBossbar && SlayerBossBars.shouldRenderBossBar()) { + ClientBossBar bar = SlayerBossBars.updateBossBar(); + + int textWidth = this.client.textRenderer.getWidth(bar.getName()); + context.drawTextWithShadow(this.client.textRenderer, bar.getName(), context.getScaledWindowWidth() / 2 - textWidth / 2, 3, 16777215); + + this.renderBossBar(context, (context.getScaledWindowWidth() / 2) - 91, 12, bar); + + ci.cancel(); + } + + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/AttunementColors.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/AttunementColors.java index ba94812a2a..6612d97805 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/AttunementColors.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/AttunementColors.java @@ -17,7 +17,7 @@ public class AttunementColors { */ public static int getColor(LivingEntity e) { if (!SkyblockerConfigManager.get().slayers.blazeSlayer.attunementHighlights) return 0xf57738; - for (Entity entity : SlayerUtils.getEntityArmorStands(e)) { + for (Entity entity : SlayerUtils.getEntityArmorStands(e, 2.5f)) { Matcher matcher = COLOR_PATTERN.matcher(entity.getDisplayName().getString()); if (matcher.find()) { String matchedColour = matcher.group(); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/FirePillarAnnouncer.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/FirePillarAnnouncer.java index d232809674..d5db338ab8 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/FirePillarAnnouncer.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/FirePillarAnnouncer.java @@ -10,8 +10,6 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.entity.Entity; import net.minecraft.entity.decoration.ArmorStandEntity; -import net.minecraft.text.MutableText; -import net.minecraft.text.PlainTextContent; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import java.util.regex.Matcher; @@ -42,7 +40,7 @@ public static void checkFirePillar(Entity entity) { // There is an edge case where the slayer has entered demon phase and temporarily despawned with // an active fire pillar in play, So fallback to the player - Entity referenceEntity = SlayerUtils.getSlayerEntity(); + Entity referenceEntity = SlayerUtils.getSlayerArmorStandEntity(); if (!(referenceEntity != null ? referenceEntity : MinecraftClient.getInstance().player).getBlockPos().isWithinDistance(entity.getPos(), 22)) return; announceFirePillarDetails(entityName); } @@ -58,4 +56,4 @@ private static void announceFirePillarDetails(String entityName) { TitleContainer.addTitle(title, 15); } } -} \ No newline at end of file +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/ManiaIndicator.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/ManiaIndicator.java index 54ea909395..484c755d75 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/rift/ManiaIndicator.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/ManiaIndicator.java @@ -22,11 +22,11 @@ protected static void updateMania() { return; } - Entity slayerEntity = SlayerUtils.getSlayerEntity(); + Entity slayerEntity = SlayerUtils.getSlayerArmorStandEntity(); if (slayerEntity == null) return; boolean anyMania = false; - for (Entity entity : SlayerUtils.getEntityArmorStands(slayerEntity)) { + for (Entity entity : SlayerUtils.getEntityArmorStands(slayerEntity, 2.5f)) { if (entity.getDisplayName().toString().contains("MANIA")) { anyMania = true; BlockPos pos = MinecraftClient.getInstance().player.getBlockPos().down(); @@ -39,4 +39,4 @@ protected static void updateMania() { TitleContainer.removeTitle(title); } } -} \ No newline at end of file +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/StakeIndicator.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/StakeIndicator.java index 54bee5ec68..5938d273d9 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/rift/StakeIndicator.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/StakeIndicator.java @@ -17,11 +17,11 @@ protected static void updateStake() { TitleContainer.removeTitle(title); return; } - Entity slayerEntity = SlayerUtils.getSlayerEntity(); + Entity slayerEntity = SlayerUtils.getSlayerArmorStandEntity(); if (slayerEntity != null && slayerEntity.getDisplayName().toString().contains("҉")) { RenderHelper.displayInTitleContainerAndPlaySound(title); } else { TitleContainer.removeTitle(title); } } -} \ No newline at end of file +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/TwinClawsIndicator.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/TwinClawsIndicator.java index f6952ab499..b3a1f87bc3 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/rift/TwinClawsIndicator.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/TwinClawsIndicator.java @@ -20,11 +20,11 @@ protected static void updateIce() { return; } - Entity slayerEntity = SlayerUtils.getSlayerEntity(); + Entity slayerEntity = SlayerUtils.getSlayerArmorStandEntity(); if (slayerEntity == null) return; boolean anyClaws = false; - for (Entity entity : SlayerUtils.getEntityArmorStands(slayerEntity)) { + for (Entity entity : SlayerUtils.getEntityArmorStands(slayerEntity, 2.5f)) { if (entity.getDisplayName().toString().contains("TWINCLAWS")) { anyClaws = true; if (!TitleContainer.containsTitle(title) && !scheduled) { @@ -40,4 +40,4 @@ protected static void updateIce() { TitleContainer.removeTitle(title); } } -} \ No newline at end of file +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerBossBars.java b/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerBossBars.java new file mode 100644 index 0000000000..c33d7af859 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerBossBars.java @@ -0,0 +1,102 @@ +package de.hysky.skyblocker.skyblock.slayers; + +import de.hysky.skyblocker.utils.SlayerUtils; +import net.minecraft.client.gui.hud.ClientBossBar; +import net.minecraft.entity.boss.BossBar; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.text.Text; + +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SlayerBossBars { + private static final Pattern HEALTH_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?[kM]?)(?=❤)"); + private static int bossMaxHealth = -1; + private static long lastUpdateTime = 0; + private static final long UPDATE_INTERVAL = 400; + private static ClientBossBar bossBar; + public static final UUID uuid = UUID.randomUUID(); + + /** + * Determines if the boss bar should be rendered and updates the max health of the boss. + * Has a 400ms cooldown built-in. + */ + public static boolean shouldRenderBossBar() { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastUpdateTime < UPDATE_INTERVAL) { + return bossBar != null; + } + lastUpdateTime = currentTime; + + // Reset if no slayer + if (!SlayerUtils.isInSlayer()) { + bossMaxHealth = -1; + bossBar = null; + return false; + } + + // Update boss max health + if (SlayerUtils.getSlayerArmorStandEntity() != null && bossMaxHealth == -1) { + Matcher maxHealthMatcher = HEALTH_PATTERN.matcher(SlayerUtils.getSlayerArmorStandEntity().getName().getString()); + if (maxHealthMatcher.find()) bossMaxHealth = convertToInt(maxHealthMatcher.group(0)); + } + + return bossBar != null || SlayerUtils.getSlayerArmorStandEntity() != null; + } + + /** + * Updates the boss bar with the current slayer's health, called every frame. + * @return The updated boss bar. + */ + public static ClientBossBar updateBossBar() { + ArmorStandEntity slayer = SlayerUtils.getSlayerArmorStandEntity(); + if (bossBar == null) bossBar = new ClientBossBar(uuid, slayer != null ? slayer.getDisplayName() : Text.of("Attempting to Locate Slayer..."), 1f, BossBar.Color.PURPLE, BossBar.Style.PROGRESS, false, false, false); + + // If no slayer armor stand is found, display a red progress bar + if (slayer == null) { + bossBar.setStyle(BossBar.Style.PROGRESS); + bossBar.setColor(BossBar.Color.RED); + return bossBar; + } + + // Update the boss bar with the current slayer's health + Matcher healthMatcher = HEALTH_PATTERN.matcher(slayer.getName().getString()); + if (healthMatcher.find() && slayer.isAlive()) { + bossBar.setPercent(bossMaxHealth == -1 ? 1f : (float) convertToInt(healthMatcher.group(1)) / bossMaxHealth); + bossBar.setColor(BossBar.Color.PINK); + bossBar.setName(slayer.getDisplayName()); + bossBar.setStyle(BossBar.Style.NOTCHED_10); + } else { + bossBar.setColor(BossBar.Color.RED); + bossBar.setStyle(BossBar.Style.PROGRESS); + bossBar.setName(slayer.getDisplayName()); + } + + return bossBar; + } + + private static int convertToInt(String value) { + if (value == null || value.isEmpty()) { + return 0; + } + + value = value.trim().toLowerCase(); + double multiplier = 1.0; + + if (value.endsWith("m")) { + multiplier = 1_000_000; + value = value.substring(0, value.length() - 1); + } else if (value.endsWith("k")) { + multiplier = 1_000; + value = value.substring(0, value.length() - 1); + } + + try { + double numericValue = Double.parseDouble(value); + return (int) (numericValue * multiplier); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerEntitiesGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerEntitiesGlow.java index a87f737a41..88ae51df3f 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerEntitiesGlow.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerEntitiesGlow.java @@ -66,7 +66,7 @@ public static boolean shouldGlow(UUID entityUUID) { } public static boolean isSlayer(LivingEntity e) { - return SlayerUtils.isInSlayer() && SlayerUtils.getEntityArmorStands(e).stream().anyMatch(entity -> + return SlayerUtils.isInSlayer() && SlayerUtils.getEntityArmorStands(e, 2.5f).stream().anyMatch(entity -> entity.getDisplayName().getString().contains(MinecraftClient.getInstance().getSession().getUsername())); } diff --git a/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java b/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java index 8944eb3d7e..6eaa770870 100644 --- a/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java @@ -12,6 +12,7 @@ //TODO Slayer Packet system that can provide information about the current slayer boss, abstract so that different bosses can have different info public class SlayerUtils { + private static ArmorStandEntity slayerArmorStandEntity; public static final String REVENANT = "Revenant Horror"; public static final String TARA = "Tarantula Broodfather"; public static final String SVEN = "Sven Packmaster"; @@ -19,15 +20,20 @@ public class SlayerUtils { public static final String VAMPIRE = "Riftstalker Bloodfiend"; public static final String DEMONLORD = "Inferno Demonlord"; private static final Logger LOGGER = LoggerFactory.getLogger(SlayerUtils.class); - private static final Pattern SLAYER_PATTERN = Pattern.compile("Revenant Horror|Tarantula Broodfather|Sven Packmaster|Voidgloom Seraph|Inferno Demonlord|Riftstalker Bloodfiend"); + public static final Pattern SLAYER_PATTERN = Pattern.compile("Revenant Horror|Tarantula Broodfather|Sven Packmaster|Voidgloom Seraph|Inferno Demonlord|Riftstalker Bloodfiend"); //TODO: Cache this, probably included in Packet system - public static List getEntityArmorStands(Entity entity) { - return entity.getEntityWorld().getOtherEntities(entity, entity.getBoundingBox().expand(0.3F, 2.5F, 0.3F), x -> x instanceof ArmorStandEntity && x.hasCustomName()); + public static List getEntityArmorStands(Entity entity, float expandY) { + return entity.getEntityWorld().getOtherEntities(entity, entity.getBoundingBox().expand(0.3F, expandY, 0.3F), x -> x instanceof ArmorStandEntity && x.hasCustomName()); } //Eventually this should be modified so that if you hit a slayer boss all slayer features will work on that boss. - public static Entity getSlayerEntity() { + public static ArmorStandEntity getSlayerArmorStandEntity() { + // TODO: This should be set when the system to detect isInSlayer is made event-driven + if (slayerArmorStandEntity != null && slayerArmorStandEntity.isAlive()) { + return slayerArmorStandEntity; + } + if (MinecraftClient.getInstance().world != null) { for (Entity entity : MinecraftClient.getInstance().world.getEntities()) { if (entity.hasCustomName()) { @@ -35,22 +41,27 @@ public static Entity getSlayerEntity() { Matcher matcher = SLAYER_PATTERN.matcher(entityName); if (matcher.find()) { String username = MinecraftClient.getInstance().getSession().getUsername(); - for (Entity armorStand : getEntityArmorStands(entity)) { + for (Entity armorStand : getEntityArmorStands(entity, 1.5f)) { if (armorStand.getDisplayName().getString().contains(username)) { - return entity; + slayerArmorStandEntity = (ArmorStandEntity) entity; + return slayerArmorStandEntity; } } } } } } + + slayerArmorStandEntity = null; return null; } public static boolean isInSlayer() { try { for (String line : Utils.STRING_SCOREBOARD) { - if (line.contains("Slay the boss!")) return true; + if (line.contains("Slay the boss!")) { + return true; + } } } catch (NullPointerException e) { LOGGER.error("[Skyblocker] Error while checking if player is in slayer", e); @@ -102,4 +113,4 @@ public static String getSlayerType() { } return ""; } -} \ No newline at end of file +} diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 6e59ee2cdb..d393d9ec54 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -653,6 +653,9 @@ "skyblocker.config.slayer.blazeSlayer.attunementHighlights" : "Attunement Highlights", "skyblocker.config.slayer.blazeSlayer.attunementHighlights.@Tooltip": "If Boss Highlighting is on, applies a color matching the current attunement to the selected effect.", + "skyblocker.config.slayer.bossbar": "Slayer Bossbar", + "skyblocker.config.slayer.bossbar.@Tooltip": "Displays a bossbar for your active slayer boss", + "skyblocker.config.slayer.endermanSlayer": "Enderman Slayer", "skyblocker.config.slayer.endermanSlayer.enableYangGlyphsNotification": "Enable Yang Glyphs notification", "skyblocker.config.slayer.endermanSlayer.highlightBeacons": "Beacon Highlighting", diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index 912ce94eb9..57eea120e3 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -7,6 +7,7 @@ "AbstractInventoryScreenMixin", "BackgroundRendererMixin", "BatEntityMixin", + "BossBarHudMixin", "ClientPlayerEntityMixin", "ClientPlayNetworkHandlerMixin", "ClientWorldMixin",