From b3704a19b13ccd9a2283053f60248bc5b2ae113b Mon Sep 17 00:00:00 2001 From: nmccullagh Date: Sat, 3 Aug 2024 20:32:57 +0100 Subject: remade slayer helpers pr --- .../java/de/hysky/skyblocker/SkyblockerMod.java | 4 + .../config/categories/SlayersCategory.java | 46 ++++++ .../skyblocker/config/configs/SlayersConfig.java | 39 ++++++ .../mixins/ClientPlayNetworkHandlerMixin.java | 12 ++ .../skyblocker/mixins/WorldRendererMixin.java | 7 +- .../skyblock/crimson/slayer/AttunementColours.java | 35 +++++ .../crimson/slayer/FirePillarAnnouncer.java | 60 ++++++++ .../skyblock/entity/MobBoundingBoxes.java | 14 ++ .../hysky/skyblocker/skyblock/entity/MobGlow.java | 156 +++++++++++++++------ .../skyblock/slayers/SlayerEntitiesGlow.java | 119 ++++++++++++++++ .../de/hysky/skyblocker/utils/SlayerUtils.java | 57 ++++++-- 11 files changed, 496 insertions(+), 53 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/AttunementColours.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/FirePillarAnnouncer.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerEntitiesGlow.java (limited to 'src/main/java') diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index b1d25a01..51c65abc 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -28,6 +28,7 @@ import de.hysky.skyblocker.skyblock.end.BeaconHighlighter; import de.hysky.skyblocker.skyblock.end.EnderNodes; import de.hysky.skyblocker.skyblock.end.TheEnd; import de.hysky.skyblocker.skyblock.entity.MobBoundingBoxes; +import de.hysky.skyblocker.skyblock.entity.MobGlow; import de.hysky.skyblocker.skyblock.events.EventNotifications; import de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars; import de.hysky.skyblocker.skyblock.garden.FarmingHud; @@ -45,6 +46,7 @@ import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; import de.hysky.skyblocker.skyblock.rift.TheRift; import de.hysky.skyblocker.skyblock.searchoverlay.SearchOverManager; import de.hysky.skyblocker.skyblock.shortcut.Shortcuts; +import de.hysky.skyblocker.skyblock.slayers.SlayerEntitiesGlow; import de.hysky.skyblocker.skyblock.special.SpecialEffects; import de.hysky.skyblocker.skyblock.tabhud.TabHud; import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenMaster; @@ -202,6 +204,8 @@ public class SkyblockerMod implements ClientModInitializer { TooltipManager.init(); SlotTextManager.init(); BazaarHelper.init(); + MobGlow.init(); + SlayerEntitiesGlow.init(); Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20); Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200); 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 0a65aa3b..b06cf585 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.config.categories; import de.hysky.skyblocker.config.ConfigUtils; import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.configs.SlayersConfig; import dev.isxander.yacl3.api.ConfigCategory; import dev.isxander.yacl3.api.Option; import dev.isxander.yacl3.api.OptionDescription; @@ -17,6 +18,30 @@ public class SlayersCategory { return ConfigCategory.createBuilder() .name(Text.translatable("skyblocker.config.slayer")) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.slayer.highlightMinis")) + .description(OptionDescription.of( + Text.translatable("skyblocker.config.slayer.highlightMinis.@Tooltip[0]"), + Text.translatable("skyblocker.config.slayer.highlightMinis.@Tooltip[1]"), + Text.translatable("skyblocker.config.slayer.highlightMinis.@Tooltip[2]"))) + .binding(defaults.slayers.highlightMinis, + () -> config.slayers.highlightMinis, + newValue -> config.slayers.highlightMinis = newValue) + .controller(ConfigUtils::createEnumCyclingListController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.slayer.highlightBosses")) + .description(OptionDescription.of( + Text.translatable("skyblocker.config.slayer.highlightBosses.@Tooltip[0]"), + Text.translatable("skyblocker.config.slayer.highlightBosses.@Tooltip[1]"), + Text.translatable("skyblocker.config.slayer.highlightBosses.@Tooltip[2]"), + Text.translatable("skyblocker.config.slayer.highlightBosses.@Tooltip[3]"))) + .binding(defaults.slayers.highlightBosses, + () -> config.slayers.highlightBosses, + newValue -> config.slayers.highlightBosses = newValue) + .controller(ConfigUtils::createEnumCyclingListController) + .build()) + //Enderman Slayer .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.slayer.endermanSlayer")) @@ -138,6 +163,27 @@ public class SlayersCategory { .build()) .build()) + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.slayer.blazeSlayer")) + .collapsed(true) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.slayer.blazeSlayer.enableFirePillarAnnouncer")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.slayer.blazeSlayer.enableFirePillarAnnouncer.@Tooltip"))) + .binding(defaults.slayers.blazeSlayer.firePillarCountdown, + () -> config.slayers.blazeSlayer.firePillarCountdown, + newValue -> config.slayers.blazeSlayer.firePillarCountdown = newValue) + .controller(ConfigUtils::createEnumCyclingListController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.slayer.blazeSlayer.attunementHighlights")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.slayer.blazeSlayer.attunementHighlights.@Tooltip"))) + .binding(defaults.slayers.blazeSlayer.attunementHighlights, + () -> config.slayers.blazeSlayer.attunementHighlights, + newValue -> config.slayers.blazeSlayer.attunementHighlights = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + .build(); } } 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 ff6c2275..522c5b52 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/SlayersConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/SlayersConfig.java @@ -3,9 +3,22 @@ package de.hysky.skyblocker.config.configs; import dev.isxander.yacl3.config.v2.api.SerialEntry; public class SlayersConfig { + @SerialEntry + public HighlightSlayerEntities highlightMinis = HighlightSlayerEntities.OFF; + + @SerialEntry + public HighlightSlayerEntities highlightBosses = HighlightSlayerEntities.OFF; + + public enum HighlightSlayerEntities { + OFF, GLOW, HITBOX + } + @SerialEntry public EndermanSlayer endermanSlayer = new EndermanSlayer(); + @SerialEntry + public BlazeSlayer blazeSlayer = new BlazeSlayer(); + @SerialEntry public VampireSlayer vampireSlayer = new VampireSlayer(); @@ -20,6 +33,32 @@ public class SlayersConfig { public boolean highlightNukekubiHeads = true; } + public static class BlazeSlayer { + @SerialEntry + public FirePillar firePillarCountdown = FirePillar.SOUND_AND_VISUAL; + + @SerialEntry + public Boolean attunementHighlights = true; + + public enum FirePillar { + OFF("Off"), + VISUAL("Visual Indicator"), + SOUND_AND_VISUAL("Sound and Visual Indicator"); + + private final String description; + + FirePillar(String description) { + this.description = description; + } + + @Override + public String toString() { + return description; + } + } + + } + public static class VampireSlayer { @SerialEntry public boolean enableEffigyWaypoints = true; diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java index c0cd8b7b..190fc5f9 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java @@ -3,16 +3,20 @@ package de.hysky.skyblocker.mixins; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; import com.llamalad7.mixinextras.sugar.Local; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.SlayersConfig; import de.hysky.skyblocker.skyblock.CompactDamage; import de.hysky.skyblocker.skyblock.FishingHelper; import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder; import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; +import de.hysky.skyblocker.skyblock.crimson.slayer.FirePillarAnnouncer; import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.dwarven.WishingCompassSolver; import de.hysky.skyblocker.skyblock.end.BeaconHighlighter; import de.hysky.skyblocker.skyblock.end.EnderNodes; import de.hysky.skyblocker.skyblock.end.TheEnd; +import de.hysky.skyblocker.skyblock.slayers.SlayerEntitiesGlow; import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual; import de.hysky.skyblocker.utils.SlayerUtils; import de.hysky.skyblocker.utils.Utils; @@ -109,6 +113,7 @@ public abstract class ClientPlayNetworkHandlerMixin { if (packet.getStatus() == EntityStatuses.PLAY_DEATH_SOUND_OR_ADD_PROJECTILE_HIT_PARTICLES) { DungeonScore.handleEntityDeath(entity); TheEnd.onEntityDeath(entity); + SlayerEntitiesGlow.onEntityDeath(entity); } return entity; } @@ -117,6 +122,13 @@ public abstract class ClientPlayNetworkHandlerMixin { private void skyblocker$onEntityTrackerUpdate(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { if (!(entity instanceof ArmorStandEntity armorStandEntity)) return; + if (SkyblockerConfigManager.get().slayers.highlightMinis == SlayersConfig.HighlightSlayerEntities.GLOW && SlayerEntitiesGlow.isSlayerMiniMob(armorStandEntity) + || SkyblockerConfigManager.get().slayers.highlightBosses == SlayersConfig.HighlightSlayerEntities.GLOW && SlayerEntitiesGlow.isSlayer(armorStandEntity)) { + SlayerEntitiesGlow.setSlayerMobGlow(armorStandEntity); + } + + if (SkyblockerConfigManager.get().slayers.blazeSlayer.firePillarCountdown != SlayersConfig.BlazeSlayer.FirePillar.OFF) FirePillarAnnouncer.checkFirePillar(entity); + 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); diff --git a/src/main/java/de/hysky/skyblocker/mixins/WorldRendererMixin.java b/src/main/java/de/hysky/skyblocker/mixins/WorldRendererMixin.java index 2959d4b5..7126cbad 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/WorldRendererMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/WorldRendererMixin.java @@ -1,6 +1,8 @@ package de.hysky.skyblocker.mixins; import de.hysky.skyblocker.skyblock.dungeon.LividColor; +import de.hysky.skyblocker.skyblock.slayers.SlayerEntitiesGlow; +import net.minecraft.entity.decoration.ArmorStandEntity; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -42,7 +44,10 @@ public class WorldRendererMixin { boolean shouldShowBoundingBox = MobBoundingBoxes.shouldDrawMobBoundingBox(entity); if (shouldShowBoundingBox) { - MobBoundingBoxes.submitBox2BeRendered(entity.getBoundingBox(), MobBoundingBoxes.getBoxColor(entity)); + MobBoundingBoxes.submitBox2BeRendered( + entity instanceof ArmorStandEntity e ? SlayerEntitiesGlow.getSlayerMobBoundingBox(e) : entity.getBoundingBox(), + MobBoundingBoxes.getBoxColor(entity) + ); } } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/AttunementColours.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/AttunementColours.java new file mode 100644 index 00000000..9c38f775 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/AttunementColours.java @@ -0,0 +1,35 @@ +package de.hysky.skyblocker.skyblock.crimson.slayer; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.SlayerUtils; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; + +import java.awt.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AttunementColours { + private static final Pattern COLOUR_PATTERN = Pattern.compile("ASHEN|SPIRIT|CRYSTAL|AURIC"); + + /** + * Fetches highlight colour based on the Inferno Demonlord, or its demons', Hellion Shield Attunement + */ + public static int getColour(LivingEntity e) { + if (!SkyblockerConfigManager.get().slayers.blazeSlayer.attunementHighlights) return 0xf57738; + for (Entity entity : SlayerUtils.getEntityArmorStands(e)) { + Matcher matcher = COLOUR_PATTERN.matcher(entity.getDisplayName().getString()); + if (matcher.find()) { + String matchedColour = matcher.group(); + return switch (matchedColour) { + case "ASHEN" -> Color.DARK_GRAY.getRGB(); + case "SPIRIT" -> Color.WHITE.getRGB(); + case "CRYSTAL" -> Color.CYAN.getRGB(); + case "AURIC" -> Color.YELLOW.getRGB(); + default -> Color.RED.getRGB(); + }; + } + } + return Color.RED.getRGB(); + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..388e66dd --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/slayer/FirePillarAnnouncer.java @@ -0,0 +1,60 @@ +package de.hysky.skyblocker.skyblock.crimson.slayer; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.SlayersConfig; +import de.hysky.skyblocker.utils.SlayerUtils; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.render.title.Title; +import de.hysky.skyblocker.utils.render.title.TitleContainer; +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.util.Formatting; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FirePillarAnnouncer { + + private static final Pattern FIRE_PILLAR_PATTERN = Pattern.compile("(\\d+)s \\d+ hits"); + + /** + * Checks if an entity is the fire pillar when it has been updated (i.e. name change). This triggers twice on + * seven seconds remaining, so it's been reduced down to only announce the last 5 seconds until explosion. + *

+ * There's also not a great way to detect ownership of the fire pillar, so a crude range calculation is used to try and + * prevent another player's FirePillar appearing on the HUD. + * + * @param entity The updated entity that is checked to be a fire pillar + */ + public static void checkFirePillar(Entity entity) { + if (Utils.isInCrimson() && SlayerUtils.isInSlayer() && entity instanceof ArmorStandEntity) { + + String entityName = entity.getName().getString(); + Matcher matcher = FIRE_PILLAR_PATTERN.matcher(entityName); + + if (matcher.matches()) { + int seconds = Integer.parseInt(matcher.group(1)); + if (seconds > 5) return; + + // 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(); + if (!(referenceEntity != null ? referenceEntity : MinecraftClient.getInstance().player).getBlockPos().isWithinDistance(entity.getPos(), 24)) return; + announceFirePillarDetails(entityName); + } + } + } + + private static void announceFirePillarDetails(String entityName) { + Title title = new Title(MutableText.of(new PlainTextContent.Literal(entityName)).formatted(Formatting.BOLD, Formatting.DARK_PURPLE)); + + if (SkyblockerConfigManager.get().slayers.blazeSlayer.firePillarCountdown == SlayersConfig.BlazeSlayer.FirePillar.SOUND_AND_VISUAL) { + RenderHelper.displayInTitleContainerAndPlaySound(title, 15); + } else { + TitleContainer.addTitle(title, 15); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobBoundingBoxes.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobBoundingBoxes.java index f005fc06..ad2b9b8c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobBoundingBoxes.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobBoundingBoxes.java @@ -1,7 +1,9 @@ package de.hysky.skyblocker.skyblock.entity; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.SlayersConfig; import de.hysky.skyblocker.skyblock.dungeon.LividColor; +import de.hysky.skyblocker.skyblock.slayers.SlayerEntitiesGlow; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.render.FrustumUtils; import de.hysky.skyblocker.utils.render.RenderHelper; @@ -9,6 +11,7 @@ import de.hysky.skyblocker.utils.render.Renderable; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.minecraft.client.MinecraftClient; import net.minecraft.entity.Entity; import net.minecraft.entity.decoration.ArmorStandEntity; import net.minecraft.entity.player.PlayerEntity; @@ -40,6 +43,17 @@ public class MobBoundingBoxes { }; } + if (SkyblockerConfigManager.get().slayers.highlightMinis == SlayersConfig.HighlightSlayerEntities.HITBOX + && entity instanceof ArmorStandEntity le && SlayerEntitiesGlow.isSlayerMiniMob(le)) { + return true; + } + + if (SkyblockerConfigManager.get().slayers.highlightBosses == SlayersConfig.HighlightSlayerEntities.HITBOX + && entity instanceof ArmorStandEntity le) { + return le.getDisplayName().getString().contains(MinecraftClient.getInstance().getSession().getUsername()) || + entity.getDisplayName().getString().contains("Ⓣ") || entity.getDisplayName().getString().contains("Ⓐ"); + } + return false; } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java index 22474cf8..7c980a04 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java @@ -4,18 +4,20 @@ import com.google.common.collect.Streams; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra; +import de.hysky.skyblocker.skyblock.crimson.slayer.AttunementColours; import de.hysky.skyblocker.skyblock.dungeon.LividColor; import de.hysky.skyblocker.skyblock.end.TheEnd; +import de.hysky.skyblocker.skyblock.slayers.SlayerEntitiesGlow; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.SlayerUtils; import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.render.culling.OcclusionCulling; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.minecraft.client.MinecraftClient; import net.minecraft.entity.Entity; import net.minecraft.entity.decoration.ArmorStandEntity; -import net.minecraft.entity.mob.EndermanEntity; -import net.minecraft.entity.mob.MagmaCubeEntity; -import net.minecraft.entity.mob.ZombieEntity; +import net.minecraft.entity.mob.*; import net.minecraft.entity.passive.BatEntity; +import net.minecraft.entity.passive.WolfEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.predicate.entity.EntityPredicates; import net.minecraft.util.Formatting; @@ -23,60 +25,121 @@ import net.minecraft.util.math.Box; import net.minecraft.world.World; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; public class MobGlow { + /** * The Nukekubi head texture id is eb07594e2df273921a77c101d0bfdfa1115abed5b9b2029eb496ceba9bdbb4b3. */ public static final String NUKEKUBI_HEAD_TEXTURE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZWIwNzU5NGUyZGYyNzM5MjFhNzdjMTAxZDBiZmRmYTExMTVhYmVkNWI5YjIwMjllYjQ5NmNlYmE5YmRiYjRiMyJ9fX0="; + private static final long GLOW_CACHE_DURATION = 50; + private static final long PLAYER_CAN_SEE_CACHE_DURATION = 100; + private static final ConcurrentHashMap glowCache = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap canSeeCache = new ConcurrentHashMap<>(); - public static boolean shouldMobGlow(Entity entity) { - Box box = entity.getBoundingBox(); - - if (OcclusionCulling.getReducedCuller().isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) { - String name = entity.getName().getString(); - + public static void init() { + Scheduler.INSTANCE.scheduleCyclic(MobGlow::clearCache, 100*20); + } - // Dungeons - if (Utils.isInDungeons() && !entity.isInvisible()) { - return switch (entity) { - // Minibosses - case PlayerEntity p when name.equals("Lost Adventurer") || name.equals("Shadow Assassin") || name.equals("Diamond Guy") -> SkyblockerConfigManager.get().dungeons.starredMobGlow; - case PlayerEntity p when entity.getId() == LividColor.getCorrectLividId() -> LividColor.shouldGlow(name); + public static boolean shouldMobGlow(Entity entity) { + if (entity.isInvisible()) return false; - // Bats - case BatEntity b -> SkyblockerConfigManager.get().dungeons.starredMobGlow || SkyblockerConfigManager.get().dungeons.starredMobBoundingBoxes; + long currentTime = System.currentTimeMillis(); - // Armor Stands - case ArmorStandEntity _armorStand -> false; + boolean shouldGlow; + CacheEntry cachedGlow = glowCache.get(entity); + if (cachedGlow == null || (currentTime - cachedGlow.timestamp) > GLOW_CACHE_DURATION) { + shouldGlow = computeShouldMobGlow(entity); + glowCache.put(entity, new CacheEntry(shouldGlow, currentTime)); + } else { + shouldGlow = cachedGlow.value; + } - // Regular Mobs - default -> SkyblockerConfigManager.get().dungeons.starredMobGlow && isStarred(entity); - }; - } + if (shouldGlow) { + return playerCanSee(entity, currentTime); + } - return switch (entity) { - // Rift - case PlayerEntity p when Utils.isInTheRift() && !entity.isInvisible() && name.equals("Blobbercyst ") -> SkyblockerConfigManager.get().otherLocations.rift.blobbercystGlow; + return false; + } - // Enderman Slayer - // Highlights Nukekubi Heads - case ArmorStandEntity armorStand when Utils.isInTheEnd() && SlayerUtils.isInSlayer() && isNukekubiHead(armorStand) -> SkyblockerConfigManager.get().slayers.endermanSlayer.highlightNukekubiHeads; + /** + * Checks if the player can see the entity. + * Has "True sight" within an small aura, but since name tags exist I think this is fine... + */ + private static boolean playerCanSee(Entity entity, long currentTime) { - // Special Zelot - case EndermanEntity enderman when Utils.isInTheEnd() && !entity.isInvisible() -> TheEnd.isSpecialZealot(enderman); + CacheEntry canSee = canSeeCache.get(entity); + if (canSee == null || (currentTime - canSee.timestamp) > PLAYER_CAN_SEE_CACHE_DURATION) { + boolean playerCanSee = entity.distanceTo(MinecraftClient.getInstance().player) <= 15 || MinecraftClient.getInstance().player.canSee(entity); + canSeeCache.put(entity, new CacheEntry(playerCanSee, currentTime)); + return playerCanSee; + } - //dojo - case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.shouldGlow(getArmorStandName(zombie)); + return canSee.value; + } - //Kuudra - case MagmaCubeEntity magmaCube when Utils.isInKuudra() -> SkyblockerConfigManager.get().crimsonIsle.kuudra.kuudraGlow && magmaCube.getSize() == Kuudra.KUUDRA_MAGMA_CUBE_SIZE; + private static boolean computeShouldMobGlow(Entity entity) { + String name = entity.getName().getString(); - default -> false; + // Dungeons + if (Utils.isInDungeons() && !entity.isInvisible()) { + return switch (entity) { + // Minibosses + case PlayerEntity p when name.equals("Lost Adventurer") || name.equals("Shadow Assassin") || name.equals("Diamond Guy") -> + SkyblockerConfigManager.get().dungeons.starredMobGlow; + case PlayerEntity p when entity.getId() == LividColor.getCorrectLividId() -> + LividColor.shouldGlow(name); + // Bats + case BatEntity b -> + SkyblockerConfigManager.get().dungeons.starredMobGlow || SkyblockerConfigManager.get().dungeons.starredMobBoundingBoxes; + + // Armor Stands + case ArmorStandEntity _armorStand -> false; + + // Regular Mobs + default -> SkyblockerConfigManager.get().dungeons.starredMobGlow && isStarred(entity); }; } - return false; + return switch (entity) { + + // Rift Blobbercyst + case PlayerEntity p when Utils.isInTheRift() && !entity.isInvisible() && name.equals("Blobbercyst ") -> + SkyblockerConfigManager.get().otherLocations.rift.blobbercystGlow; + + // Dojo Helpers + case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> + DojoManager.shouldGlow(getArmorStandName(zombie)); + + //Kuudra + case MagmaCubeEntity magmaCube when Utils.isInKuudra() -> + SkyblockerConfigManager.get().crimsonIsle.kuudra.kuudraGlow && magmaCube.getSize() == Kuudra.KUUDRA_MAGMA_CUBE_SIZE; + + // Special Zealot && Slayer (Mini)Boss + case EndermanEntity enderman when Utils.isInTheEnd() && !entity.isInvisible() -> + TheEnd.isSpecialZealot(enderman) || SlayerEntitiesGlow.shouldGlow(enderman.getUuid()); + case ZombieEntity zombie when !(zombie instanceof ZombifiedPiglinEntity) && SlayerUtils.isInSlayerQuestType(SlayerUtils.REVENANT) -> + SlayerEntitiesGlow.shouldGlow(zombie.getUuid()); + case SpiderEntity spider when SlayerUtils.isInSlayerQuestType(SlayerUtils.TARA) -> + SlayerEntitiesGlow.shouldGlow(spider.getUuid()); + case WolfEntity wolf when SlayerUtils.isInSlayerQuestType(SlayerUtils.SVEN) -> + SlayerEntitiesGlow.shouldGlow(wolf.getUuid()); + case BlazeEntity blaze when SlayerUtils.isInSlayerQuestType(SlayerUtils.DEMONLORD) -> + SlayerEntitiesGlow.shouldGlow(blaze.getUuid()); + + // Enderman Slayer's Nukekubi Skulls + case ArmorStandEntity armorStand when Utils.isInTheEnd() && SlayerUtils.isInSlayer() && isNukekubiHead(armorStand) -> + SkyblockerConfigManager.get().slayers.endermanSlayer.highlightNukekubiHeads; + + // Blaze Slayer's Demonic minions + case WitherSkeletonEntity e when SkyblockerConfigManager.get().slayers.highlightBosses.toString().equals("GLOW") -> + SlayerUtils.isInSlayerQuestType(SlayerUtils.DEMONLORD) && e.distanceTo(MinecraftClient.getInstance().player) <= 10; + case ZombifiedPiglinEntity e when SkyblockerConfigManager.get().slayers.highlightBosses.toString().equals("GLOW") -> + SlayerUtils.isInSlayerQuestType(SlayerUtils.DEMONLORD) && e.distanceTo(MinecraftClient.getInstance().player) <= 10; + + default -> false; + }; } /** @@ -123,10 +186,16 @@ public class MobGlow { case PlayerEntity p when name.equals("Blobbercyst ") -> Formatting.GREEN.getColorValue(); case EndermanEntity enderman when TheEnd.isSpecialZealot(enderman) -> Formatting.RED.getColorValue(); - case ArmorStandEntity armorStand when isNukekubiHead(armorStand) -> Formatting.GREEN.getColorValue(); + case ArmorStandEntity armorStand when isNukekubiHead(armorStand) -> 0x990099; case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.getColor(); case MagmaCubeEntity magmaCube when Utils.isInKuudra() -> 0xf7510f; + // Blaze Slayer Attunement Colours + case ArmorStandEntity armorStand when SlayerUtils.isInSlayerQuestType(SlayerUtils.DEMONLORD) -> AttunementColours.getColour(armorStand); + case BlazeEntity blaze when SlayerUtils.isInSlayer() -> AttunementColours.getColour(blaze); + case ZombifiedPiglinEntity piglin when SlayerUtils.isInSlayer() -> AttunementColours.getColour(piglin); + case WitherSkeletonEntity wSkelly when SlayerUtils.isInSlayer() -> AttunementColours.getColour(wSkelly); + default -> 0xf57738; }; } @@ -137,4 +206,11 @@ public class MobGlow { private static boolean isNukekubiHead(ArmorStandEntity entity) { return Streams.stream(entity.getArmorItems()).map(ItemUtils::getHeadTexture).anyMatch(headTexture -> headTexture.contains(NUKEKUBI_HEAD_TEXTURE)); } -} + + private record CacheEntry(boolean value, long timestamp){}; + + private static void clearCache() { + canSeeCache.clear(); + glowCache.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerEntitiesGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerEntitiesGlow.java new file mode 100644 index 00000000..54060829 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerEntitiesGlow.java @@ -0,0 +1,119 @@ +package de.hysky.skyblocker.skyblock.slayers; + +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.utils.Location; +import de.hysky.skyblocker.utils.SlayerUtils; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.entity.mob.*; +import net.minecraft.entity.passive.WolfEntity; +import net.minecraft.util.math.Box; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class SlayerEntitiesGlow { + private static final Map SLAYER_MINI_NAMES = Map.ofEntries( + Map.entry("Revenant Sycophant", SlayerUtils.REVENANT), + Map.entry("Revenant Champion", SlayerUtils.REVENANT), + Map.entry("Deformed Revenant", SlayerUtils.REVENANT), + Map.entry("Atoned Champion", SlayerUtils.REVENANT), + Map.entry("Atoned Revenant", SlayerUtils.REVENANT), + Map.entry("Tarantula Vermin", SlayerUtils.TARA), + Map.entry("Tarantula Beast", SlayerUtils.TARA), + Map.entry("Mutant Tarantula", SlayerUtils.TARA), + Map.entry("Pack Enforcer", SlayerUtils.SVEN), + Map.entry("Sven Follower", SlayerUtils.SVEN), + Map.entry("Sven Alpha", SlayerUtils.SVEN), + Map.entry("Voidling Devotee", SlayerUtils.VOIDGLOOM), + Map.entry("Voidling Radical", SlayerUtils.VOIDGLOOM), + Map.entry("Voidcrazed Maniac", SlayerUtils.VOIDGLOOM), + Map.entry("Flare Demon", SlayerUtils.DEMONLORD), + Map.entry("Kindleheart Demon", SlayerUtils.DEMONLORD), + Map.entry("Burningsoul Demon", SlayerUtils.DEMONLORD) + ); + + private static final Map> SLAYER_MOB_TYPE = Map.of( + SlayerUtils.REVENANT, ZombieEntity.class, + SlayerUtils.TARA, SpiderEntity.class, + SlayerUtils.SVEN, WolfEntity.class, + SlayerUtils.VOIDGLOOM, EndermanEntity.class, + SlayerUtils.DEMONLORD, BlazeEntity.class + ); + + private static final Set MOBS_TO_GLOW = new HashSet<>(); + + public static boolean shouldGlow(UUID entityUUID) { + return MOBS_TO_GLOW.contains(entityUUID); + } + + public static boolean isSlayer(LivingEntity e) { + return SlayerUtils.isInSlayer() && SlayerUtils.getEntityArmorStands(e).stream().anyMatch(entity -> entity.getDisplayName().getString().contains(MinecraftClient.getInstance().getSession().getUsername())); + } + + public static boolean isSlayerMiniMob(LivingEntity entity) { + if (entity.getCustomName() == null) return false; + String entityName = entity.getCustomName().getString(); + return SLAYER_MINI_NAMES.keySet().stream().anyMatch(slayerMobName -> entityName.contains(slayerMobName) && SlayerUtils.isInSlayerQuestType(SLAYER_MINI_NAMES.get(slayerMobName))); + } + + public static Box getSlayerMobBoundingBox(LivingEntity entity) { + return switch (SlayerUtils.getSlayerType()) { + case SlayerUtils.REVENANT -> + new Box(entity.getX() - 0.4, entity.getY() - 0.1, entity.getZ() - 0.4, entity.getX() + 0.4, entity.getY() - 2.2, entity.getZ() + 0.4); + case SlayerUtils.TARA -> + new Box(entity.getX() - 0.9, entity.getY() - 0.2, entity.getZ() - 0.9, entity.getX() + 0.9, entity.getY() - 1.2, entity.getZ() + 0.9); + case SlayerUtils.VOIDGLOOM -> + new Box(entity.getX() - 0.4, entity.getY() - 0.2, entity.getZ() - 0.4, entity.getX() + 0.4, entity.getY() - 3, entity.getZ() + 0.4); + case SlayerUtils.SVEN -> + new Box(entity.getX() - 0.5, entity.getY() - 0.1, entity.getZ() - 0.5, entity.getX() + 0.5, entity.getY() - 1, entity.getZ() + 0.5); + default -> + new Box(entity.getX() - 0.5, entity.getY() + 0.1, entity.getZ() - 0.5, entity.getX() + 0.5, entity.getY() - 2.2, entity.getZ() + 0.5); + }; + } + + /** + *

Finds the closest matching MobEntity for the armorStand using entityClass and armorStand age difference to filter + * out impossible candidates, returning the closest mob of those remaining in the search box by block distance

+ * + * @param entityClass the mob type of the Slayer (i.e. ZombieEntity.class) + * @param armorStand the entity that contains the display name of the Slayer (mini)boss + */ + private static MobEntity findClosestMobEntity(Class entityClass, ArmorStandEntity armorStand) { + return armorStand.getWorld().getEntitiesByClass(entityClass, armorStand.getDimensions(null) + .getBoxAt(armorStand.getPos()).expand(1.5), entity -> + !entity.isDead() && entity.age > armorStand.age - 4 && entity.age < armorStand.age + 4) + .stream() + .filter(entity -> !(entity instanceof CaveSpiderEntity)) // CaveSpider extends Spider so filter out mob for BroodFather highlight + .min(Comparator.comparingDouble((MobEntity e) -> e.distanceTo(armorStand))) + .orElse(null); + } + + /** + *

Adds the Entity UUID to the Hashset of Slayer Mobs to glow

+ * + * @param armorStand the entity that contains the display name of the Slayer (mini)boss + */ + public static void setSlayerMobGlow(ArmorStandEntity armorStand) { + String slayerType = SlayerUtils.getSlayerType(); + Class entityClass = SLAYER_MOB_TYPE.get(slayerType); + if (entityClass != null) { + MobEntity closestEntity = findClosestMobEntity(entityClass, armorStand); + if (closestEntity != null) MOBS_TO_GLOW.add(closestEntity.getUuid()); + } + } + + public static void onEntityDeath(@Nullable Entity entity) { + if (entity != null && entity.getUuid() != null) MOBS_TO_GLOW.remove(entity.getUuid()); + } + + private static void clearGlow(Location location) { + MOBS_TO_GLOW.clear(); + } + + public static void init() { + SkyblockEvents.LOCATION_CHANGE.register(SlayerEntitiesGlow::clearGlow); + } +} \ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java b/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java index 2edd61f1..eb07dec9 100644 --- a/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java @@ -7,10 +7,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; //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 { + public static final String REVENANT = "Revenant Horror"; + public static final String TARA = "Tarantula Broodfather"; + public static final String SVEN = "Sven Packmaster"; + public static final String VOIDGLOOM = "Voidgloom Seraph"; + 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"); //TODO: Cache this, probably included in Packet system public static List getEntityArmorStands(Entity entity) { @@ -21,15 +30,15 @@ public class SlayerUtils { public static Entity getSlayerEntity() { if (MinecraftClient.getInstance().world != null) { for (Entity entity : MinecraftClient.getInstance().world.getEntities()) { - //Check if entity is Bloodfiend - if (entity.hasCustomName() && entity.getCustomName().getString().contains("Bloodfiend")) { - //Grab the players username - String username = MinecraftClient.getInstance().getSession().getUsername(); - //Check all armor stands around the boss - for (Entity armorStand : getEntityArmorStands(entity)) { - //Check if the display name contains the players username - if (armorStand.getDisplayName().getString().contains(username)) { - return entity; + if (entity.hasCustomName()) { + String entityName = entity.getCustomName().getString(); + Matcher matcher = SLAYER_PATTERN.matcher(entityName); + if (matcher.find()) { + String username = MinecraftClient.getInstance().getSession().getUsername(); + for (Entity armorStand : getEntityArmorStands(entity)) { + if (armorStand.getDisplayName().getString().contains(username)) { + return entity; + } } } } @@ -40,15 +49,39 @@ public class SlayerUtils { public static boolean isInSlayer() { try { - for (int i = 0; i < Utils.STRING_SCOREBOARD.size(); i++) { - String line = Utils.STRING_SCOREBOARD.get(i); - + for (String line : Utils.STRING_SCOREBOARD) { if (line.contains("Slay the boss!")) return true; } } catch (NullPointerException e) { LOGGER.error("[Skyblocker] Error while checking if player is in slayer", e); } + return false; + } + public static boolean isInSlayerQuestType(String slayer) { + try { + boolean quest = false; + boolean type = false; + for (String line : Utils.STRING_SCOREBOARD) { + if (line.contains("Slayer Quest")) quest = true; + if (line.contains(slayer)) type = true; + if (quest && type) return true; + } + } catch (NullPointerException e) { + LOGGER.error("[Skyblocker] Error while checking if player is in slayer quest type", e); + } return false; } + + public static String getSlayerType() { + try { + for (String line : Utils.STRING_SCOREBOARD) { + Matcher matcher = SLAYER_PATTERN.matcher(line); + if (matcher.find()) return matcher.group(); + } + } catch (NullPointerException | IndexOutOfBoundsException e) { + LOGGER.error("[Skyblocker] Error while checking slayer type", e); + } + return ""; + } } \ No newline at end of file -- cgit