From 68abc8ade546de62aee76e0b786993fc59fbc3cc Mon Sep 17 00:00:00 2001 From: Walker Selby Date: Sat, 21 Oct 2023 07:31:19 +0100 Subject: Internal Change: Misc Refactoring (#551) Internal Change: Misc Refactoring #551 --- .../skyhanni/features/combat/BestiaryData.kt | 489 ++++++++++++ .../skyhanni/features/combat/HideDamageSplash.kt | 28 + .../features/combat/damageindicator/BossType.kt | 111 +++ .../combat/damageindicator/DamageCounter.kt | 12 + .../damageindicator/DamageIndicatorManager.kt | 862 +++++++++++++++++++++ .../features/combat/damageindicator/EntityData.kt | 30 + .../combat/damageindicator/EntityResult.kt | 8 + .../features/combat/damageindicator/MobFinder.kt | 513 ++++++++++++ .../features/combat/damageindicator/OldDamage.kt | 3 + .../features/combat/endernodetracker/EnderNode.kt | 33 + .../combat/endernodetracker/EnderNodeTracker.kt | 239 ++++++ .../features/combat/ghostcounter/GhostCounter.kt | 494 ++++++++++++ .../features/combat/ghostcounter/GhostData.kt | 90 +++ .../combat/ghostcounter/GhostFormatting.kt | 182 +++++ .../features/combat/ghostcounter/GhostUtil.kt | 144 ++++ .../features/combat/mobs/AreaMiniBossFeatures.kt | 121 +++ .../combat/mobs/AshfangMinisNametagHider.kt | 28 + .../skyhanni/features/combat/mobs/MobHighlight.kt | 99 +++ .../skyhanni/features/combat/mobs/SpawnTimers.kt | 98 +++ 19 files changed, 3584 insertions(+) create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/BestiaryData.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/HideDamageSplash.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/BossType.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageCounter.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageIndicatorManager.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/EntityData.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/EntityResult.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/MobFinder.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/OldDamage.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/endernodetracker/EnderNode.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/endernodetracker/EnderNodeTracker.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/ghostcounter/GhostCounter.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/ghostcounter/GhostData.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/ghostcounter/GhostFormatting.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/ghostcounter/GhostUtil.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/mobs/AreaMiniBossFeatures.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/mobs/AshfangMinisNametagHider.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/mobs/MobHighlight.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/combat/mobs/SpawnTimers.kt (limited to 'src/main/java/at/hannibal2/skyhanni/features/combat') diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/BestiaryData.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/BestiaryData.kt new file mode 100644 index 000000000..5f0a4125e --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/combat/BestiaryData.kt @@ -0,0 +1,489 @@ +package at.hannibal2.skyhanni.features.combat + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator +import at.hannibal2.skyhanni.events.GuiContainerEvent +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.InventoryCloseEvent +import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent +import at.hannibal2.skyhanni.utils.InventoryUtils +import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.LorenzColor +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.addAsSingletonList +import at.hannibal2.skyhanni.utils.LorenzUtils.addButton +import at.hannibal2.skyhanni.utils.NumberUtil +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.NumberUtil.formatNumber +import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimalIfNeeded +import at.hannibal2.skyhanni.utils.NumberUtil.roundToPrecision +import at.hannibal2.skyhanni.utils.NumberUtil.toRoman +import at.hannibal2.skyhanni.utils.RenderUtils.highlight +import at.hannibal2.skyhanni.utils.RenderUtils.renderStringsAndItems +import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher +import at.hannibal2.skyhanni.utils.StringUtils.removeColor +import at.hannibal2.skyhanni.utils.renderables.Renderable +import net.minecraft.item.ItemStack +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + + +object BestiaryData { + + private val config get() = SkyHanniMod.feature.combat.bestiary + private var display = emptyList>() + private val mobList = mutableListOf() + private val stackList = mutableMapOf() + private val catList = mutableListOf() + private val progressPattern = "(?[0-9kKmMbB,.]+)/(?[0-9kKmMbB,.]+$)".toPattern() + private val titlePattern = "^(?:\\(\\d+/\\d+\\) )?(Bestiary|.+) ➜ (.+)$".toPattern() + private var inInventory = false + private var isCategory = false + private var indexes = listOf( + 10, 11, 12, 13, 14, 15, 16, + 19, 20, 21, 22, 23, 24, 25, + 28, 29, 30, 31, 32, 33, 34, + 37, 38, 39, 40, 41, 42, 43 + ) + + @SubscribeEvent + fun onBackgroundDraw(event: GuiRenderEvent.ChestGuiOverlayRenderEvent) { + if (!isEnabled()) return + if (inInventory) { + config.position.renderStringsAndItems( + display, + extraSpace = -1, + itemScale = 1.3, + posLabel = "Bestiary Data" + ) + } + } + + @SubscribeEvent + fun onRender(event: GuiContainerEvent.BackgroundDrawnEvent) { + if (!isEnabled()) return + if (inInventory) { + for (slot in InventoryUtils.getItemsInOpenChest()) { + val stack = slot.stack + val lore = stack.getLore() + if (lore.any { it == "§7Overall Progress: §b100% §7(§c§lMAX!§7)" || it == "§7Families Completed: §a100%" }) { + slot highlight LorenzColor.GREEN + } + } + } + } + + @SubscribeEvent + fun onInventoryOpen(event: InventoryFullyOpenedEvent) { + if (!isEnabled()) return + val inventoryName = event.inventoryName + val stack = event.inventoryItems[4] ?: return + if ((inventoryName == "Bestiary ➜ Fishing" || inventoryName == "Bestiary") || isBestiaryGui( + stack, + inventoryName + ) + ) { + isCategory = inventoryName == "Bestiary ➜ Fishing" || inventoryName == "Bestiary" + stackList.putAll(event.inventoryItems) + inInventory = true + update() + } + } + + @SubscribeEvent + fun onInventoryClose(event: InventoryCloseEvent) { + mobList.clear() + stackList.clear() + inInventory = false + } + + @SubscribeEvent + fun onConfigFix(event: ConfigUpdaterMigrator.ConfigFixEvent) { + event.move(2, "misc.bestiaryData", "combat.bestiary") + } + + private fun update() { + display = drawDisplay() + } + + private fun init() { + mobList.clear() + catList.clear() + if (isCategory) { + inCategory() + } else { + notInCategory() + } + } + + private fun inCategory() { + for ((index, stack) in stackList) { + if (stack.displayName == " ") continue + if (!indexes.contains(index)) continue + inInventory = true + val name = stack.displayName + var familiesFound: Long = 0 + var totalFamilies: Long = 0 + var familiesCompleted: Long = 0 + for ((lineIndex, loreLine) in stack.getLore().withIndex()) { + val line = loreLine.removeColor() + if (!line.startsWith(" ")) continue + val previousLine = stack.getLore()[lineIndex - 1] + val progress = line.substring(line.lastIndexOf(' ') + 1) + if (previousLine.contains("Families Found")) { + progressPattern.matchMatcher(progress) { + familiesFound = group("current").formatNumber() + totalFamilies = group("needed").formatNumber() + } + } else if (previousLine.contains("Families Completed")) { + progressPattern.matchMatcher(progress) { + familiesCompleted = group("current").formatNumber() + } + } + } + catList.add(Category(name, familiesFound, totalFamilies, familiesCompleted)) + } + } + + private fun notInCategory() { + for ((index, stack) in stackList) { + if (stack.displayName == " ") continue + if (!indexes.contains(index)) continue + inInventory = true + val name = " [IVX0-9]+$".toPattern().matcher(stack.displayName).replaceFirst("") + val level = " ([IVX0-9]+$)".toRegex().find(stack.displayName)?.groupValues?.get(1) ?: "0" + var totalKillToMax: Long = 0 + var currentTotalKill: Long = 0 + var totalKillToTier: Long = 0 + var currentKillToTier: Long = 0 + var actualRealTotalKill: Long = 0 + for ((lineIndex, line) in stack.getLore().withIndex()) { + val loreLine = line.removeColor() + if (loreLine.startsWith("Kills: ")) { + actualRealTotalKill = + "([0-9,.]+)".toRegex().find(loreLine)?.groupValues?.get(1)?.formatNumber() + ?: 0 + } + if (!loreLine.startsWith(" ")) continue + val previousLine = stack.getLore()[lineIndex - 1] + val progress = loreLine.substring(loreLine.lastIndexOf(' ') + 1) + if (previousLine.contains("Progress to Tier")) { + progressPattern.matchMatcher(progress) { + totalKillToTier = group("needed").formatNumber() + currentKillToTier = group("current").formatNumber() + } + } else if (previousLine.contains("Overall Progress")) { + progressPattern.matchMatcher(progress) { + totalKillToMax = group("needed").formatNumber() + currentTotalKill = group("current").formatNumber() + } + } + } + mobList.add( + BestiaryMob( + name, + level, + totalKillToMax, + currentTotalKill, + totalKillToTier, + currentKillToTier, + actualRealTotalKill + ) + ) + } + } + + private fun drawDisplay(): List> { + val newDisplay = mutableListOf>() + init() + + addCategories(newDisplay) + + if (mobList.isEmpty()) return newDisplay + + addList(newDisplay) + + addButtons(newDisplay) + + return newDisplay + } + + private fun sortMobList(): MutableList { + val sortedMobList = when (config.displayType) { + 0 -> mobList.sortedBy { it.percentToMax() } + 1 -> mobList.sortedBy { it.percentToTier() } + 2 -> mobList.sortedBy { it.totalKills } + 3 -> mobList.sortedByDescending { it.totalKills } + 4 -> mobList.sortedBy { it.killNeededToMax() } + 5 -> mobList.sortedByDescending { it.killNeededToMax() } + 6 -> mobList.sortedBy { it.killNeededToNextLevel() } + 7 -> mobList.sortedByDescending { it.killNeededToNextLevel() } + else -> mobList.sortedBy { it.totalKills } + }.toMutableList() + return sortedMobList + } + + private fun addList(newDisplay: MutableList>) { + val sortedMobList = sortMobList() + + newDisplay.addAsSingletonList("§7Bestiary Data") + for (mob in sortedMobList) { + val isUnlocked = mob.totalKills != 0.toLong() + val isMaxed = mob.percentToMax() >= 1 + if (!isUnlocked) { + newDisplay.add(buildList { + add(" §7- ") + add("${mob.name}: §cNot unlocked!") + }) + continue + } + if (isMaxed && config.hideMaxed) continue + val text = getMobLine(mob, isMaxed) + val tips = getMobHover(mob) + newDisplay.addAsSingletonList(Renderable.hoverTips(text, tips) { true }) + } + } + + private fun getMobHover(mob: BestiaryMob) = listOf( + "§6Name: §b${mob.name}", + "§6Level: §b${mob.level} ${if (!config.replaceRoman) "§7(${mob.level.romanToDecimalIfNeeded()})" else ""}", + "§6Total Kills: §b${mob.actualRealTotalKill.formatNumber()}", + "§6Kills needed to max: §b${mob.killNeededToMax().formatNumber()}", + "§6Kills needed to next lvl: §b${mob.killNeededToNextLevel().formatNumber()}", + "§6Current kill to next level: §b${mob.currentKillToNextLevel.formatNumber()}", + "§6Kill needed for next level: §b${mob.killNeededForNextLevel.formatNumber()}", + "§6Current kill to max: §b${mob.killToMax.formatNumber()}", + "§6Percent to max: §b${mob.percentToMaxFormatted()}", + "§6Percent to tier: §b${mob.percentToTierFormatted()}", + "", + "§7More info thing" + ) + + private fun getMobLine( + mob: BestiaryMob, + isMaxed: Boolean + ): String { + val displayType = config.displayType + var text = "" + text += " §7- " + text += "${mob.name} ${mob.level.romanOrInt()} " + text += if (isMaxed) { + "§c§lMAXED! §7(§b${mob.actualRealTotalKill.formatNumber()}§7 kills)" + } else { + when (displayType) { + 0, 1 -> { + val currentKill = when (displayType) { + 0 -> mob.totalKills + 1 -> mob.currentKillToNextLevel + else -> 0 + } + val killNeeded = when (displayType) { + 0 -> mob.killToMax + 1 -> mob.killNeededForNextLevel + else -> 0 + } + "§7(§b${currentKill.formatNumber()}§7/§b${killNeeded.formatNumber()}§7) §a${ + ((currentKill.toDouble() / killNeeded) * 100).roundToPrecision( + 2 + ) + }§6% ${if (displayType == 1) "§ato level ${mob.getNextLevel()}" else ""}" + } + + 2, 3 -> { + + "§6${mob.totalKills.formatNumber()} §7total kills" + } + + 4, 5 -> { + "§6${mob.killNeededToMax().formatNumber()} §7kills needed" + } + + 6, 7 -> { + "§6${mob.killNeededToNextLevel().formatNumber()} §7kills needed" + } + + else -> "§cYou are not supposed to see this, please report it to @HiZe on discord!" + } + } + return text + } + + private fun addButtons(newDisplay: MutableList>) { + newDisplay.addButton( + prefix = "§7Number Format: ", + getName = FormatType.entries[config.numberFormat].type, + onChange = { + config.numberFormat = (config.numberFormat + 1) % 2 + update() + }) + + newDisplay.addButton( + prefix = "§7Display Type: ", + getName = DisplayType.entries[config.displayType].type, + onChange = { + config.displayType = (config.displayType + 1) % 8 + update() + }) + + newDisplay.addButton( + prefix = "§7Number Type: ", + getName = NumberType.entries[if (config.replaceRoman) 0 else 1].type, + onChange = { + config.replaceRoman = !config.replaceRoman + update() + } + ) + newDisplay.addButton( + prefix = "§7Hide Maxed: ", + getName = HideMaxed.entries[if (config.hideMaxed) 1 else 0].type, + onChange = { + config.hideMaxed = !config.hideMaxed + update() + } + ) + } + + private fun addCategories(newDisplay: MutableList>) { + if (catList.isNotEmpty()) { + newDisplay.addAsSingletonList("§7Category") + for (cat in catList) { + newDisplay.add(buildList { + add(" §7- ${cat.name}§7: ") + val element = when { + cat.familiesCompleted == cat.totalFamilies -> "§c§lCompleted!" + cat.familiesFound == cat.totalFamilies -> "§b${cat.familiesCompleted}§7/§b${cat.totalFamilies} §7completed" + cat.familiesFound < cat.totalFamilies -> + "§b${cat.familiesFound}§7/§b${cat.totalFamilies} §7found, §b${cat.familiesCompleted}§7/§b${cat.totalFamilies} §7completed" + + else -> continue + } + add(element) + }) + } + } + } + + private fun isBestiaryGui(stack: ItemStack?, name: String): Boolean { + if (stack == null) return false + val bestiaryGuiTitleMatcher = titlePattern.matcher(name) + if (bestiaryGuiTitleMatcher.matches()) { + if ("Bestiary" != bestiaryGuiTitleMatcher.group(1)) { + var loreContainsFamiliesFound = false + for (line in stack.getLore()) { + if (line.removeColor().startsWith("Families Found")) { + loreContainsFamiliesFound = true + break + } + } + if (!loreContainsFamiliesFound) { + return false + } + } + return true + } else if (name == "Search Results") { + val loreList = stack.getLore() + if (loreList.size >= 2 && loreList[0].startsWith("§7Query: §a") + && loreList[1].startsWith("§7Results: §a") + ) { + return true + } + } + return false + } + + enum class FormatType(val type: String) { + SHORT("Short"), + LONG("Long") + } + + enum class NumberType(val type: String) { + INT("Normal (1, 2, 3)"), + ROMAN("Roman (I, II, III") + } + + enum class DisplayType(val type: String) { + GLOBAL_MAX("Global display (to max)"), + GLOBAL_TIER("Global display (to next tier)"), + LOWEST_TOTAL("Lowest total kills"), + HIGHEST_TOTAL("Highest total kills"), + LOWEST_NEEDED_MAX("Lowest kills needed to max"), + HIGHEST_NEEDED_MAX("Highest kills needed to max"), + LOWEST_NEEDED_TIER("Lowest kills needed to next tier"), + HIGHEST_NEEDED_TIER("Highest kills needed to next tier"), + } + + enum class HideMaxed(val type: String) { + NO("Show"), + YES("Hide") + } + + private fun Long.formatNumber(): String = when (config.numberFormat) { + 0 -> NumberUtil.format(this) + 1 -> this.addSeparators() + else -> "0" + } + + data class Category( + val name: String, + val familiesFound: Long, + val totalFamilies: Long, + val familiesCompleted: Long + ) + + data class BestiaryMob( + var name: String, + var level: String, + var killToMax: Long, + var totalKills: Long, + var killNeededForNextLevel: Long, + var currentKillToNextLevel: Long, + var actualRealTotalKill: Long + ) { + + fun killNeededToMax(): Long { + return 0L.coerceAtLeast(killToMax - totalKills) + } + + fun killNeededToNextLevel(): Long { + return 0L.coerceAtLeast(killNeededForNextLevel - currentKillToNextLevel) + } + + fun percentToMax() = totalKills.toDouble() / killToMax + + fun percentToMaxFormatted() = LorenzUtils.formatPercentage(percentToMax()) + + fun percentToTier() = currentKillToNextLevel.toDouble() / killNeededForNextLevel + + fun percentToTierFormatted() = LorenzUtils.formatPercentage(percentToTier()) + + fun getNextLevel() = level.getNextLevel() + } + + + private fun String.romanOrInt() = romanToDecimalIfNeeded().let { + if (config.replaceRoman || it == 0) it.toString() else it.toRoman() + } + + fun Any.getNextLevel(): String { + return when (this) { + is Int -> { + (this + 1).toString().romanOrInt() + } + + is String -> { + if (this == "0") { + "I".romanOrInt() + } else { + val intValue = romanToDecimalIfNeeded() + (intValue + 1).toRoman().romanOrInt() + } + } + + else -> { + "Unsupported type: ${this::class.simpleName}" + } + } + } + + private fun isEnabled() = LorenzUtils.inSkyBlock && config.enabled + +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/HideDamageSplash.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/HideDamageSplash.kt new file mode 100644 index 000000000..7b2684c40 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/combat/HideDamageSplash.kt @@ -0,0 +1,28 @@ +package at.hannibal2.skyhanni.features.combat + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator +import at.hannibal2.skyhanni.features.combat.damageindicator.DamageIndicatorManager +import at.hannibal2.skyhanni.utils.LorenzUtils +import net.minecraft.entity.EntityLivingBase +import net.minecraftforge.client.event.RenderLivingEvent +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +class HideDamageSplash { + + @SubscribeEvent(priority = EventPriority.HIGH) + fun onRenderDamage(event: RenderLivingEvent.Specials.Pre) { + if (!LorenzUtils.inSkyBlock) return + if (!SkyHanniMod.feature.combat.hideDamageSplash) return + + if (DamageIndicatorManager.isDamageSplash(event.entity)) { + event.isCanceled = true + } + } + + @SubscribeEvent + fun onConfigFix(event: ConfigUpdaterMigrator.ConfigFixEvent) { + event.move(2, "misc.hideDamageSplash", "combat.hideDamageSplash") + } +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/BossType.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/BossType.kt new file mode 100644 index 000000000..664aa63d4 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/BossType.kt @@ -0,0 +1,111 @@ +package at.hannibal2.skyhanni.features.combat.damageindicator + +enum class BossType( + val fullName: String, + val bossTypeToggle: Int, + val shortName: String = fullName, + val showDeathTime: Boolean = false +) { + GENERIC_DUNGEON_BOSS("Generic Dungeon boss", 0),//TODO split into different bosses + + //Nether Mini Bosses + NETHER_BLADESOUL("§8Bladesoul", 1), + NETHER_MAGMA_BOSS("§4Magma Boss", 1), + NETHER_ASHFANG("§cAshfang", 1), + NETHER_BARBARIAN_DUKE("§eBarbarian Duke", 1), + NETHER_MAGE_OUTLAW("§5Mage Outlaw", 1), + + NETHER_VANQUISHER("§5Vanquisher", 2), + + END_ENDSTONE_PROTECTOR("§cEndstone Protector", 3), + END_ENDER_DRAGON("Ender Dragon", 4),//TODO fix totally + + SLAYER_ZOMBIE_1("§aRevenant Horror 1", 5, "§aRev 1", showDeathTime = true), + SLAYER_ZOMBIE_2("§eRevenant Horror 2", 5, "§eRev 2", showDeathTime = true), + SLAYER_ZOMBIE_3("§cRevenant Horror 3", 5, "§cRev 3", showDeathTime = true), + SLAYER_ZOMBIE_4("§4Revenant Horror 4", 5, "§4Rev 4", showDeathTime = true), + SLAYER_ZOMBIE_5("§5Revenant Horror 5", 5, "§5Rev 5", showDeathTime = true), + + SLAYER_SPIDER_1("§aTarantula Broodfather 1", 6, "§aTara 1", showDeathTime = true), + SLAYER_SPIDER_2("§eTarantula Broodfather 2", 6, "§eTara 2", showDeathTime = true), + SLAYER_SPIDER_3("§cTarantula Broodfather 3", 6, "§cTara 3", showDeathTime = true), + SLAYER_SPIDER_4("§4Tarantula Broodfather 4", 6, "§4Tara 4", showDeathTime = true), + + SLAYER_WOLF_1("§aSven Packmaster 1", 7, "§aSven 1", showDeathTime = true), + SLAYER_WOLF_2("§eSven Packmaster 2", 7, "§eSven 2", showDeathTime = true), + SLAYER_WOLF_3("§cSven Packmaster 3", 7, "§cSven 3", showDeathTime = true), + SLAYER_WOLF_4("§4Sven Packmaster 4", 7, "§4Sven 4", showDeathTime = true), + + SLAYER_ENDERMAN_1("§aVoidgloom Seraph 1", 8, "§aVoid 1", showDeathTime = true), + SLAYER_ENDERMAN_2("§eVoidgloom Seraph 2", 8, "§eVoid 2", showDeathTime = true), + SLAYER_ENDERMAN_3("§cVoidgloom Seraph 3", 8, "§cVoid 3", showDeathTime = true), + SLAYER_ENDERMAN_4("§4Voidgloom Seraph 4", 8, "§4Void 4", showDeathTime = true), + + SLAYER_BLAZE_1("§aInferno Demonlord 1", 9, "§aInferno 1", showDeathTime = true), + SLAYER_BLAZE_2("§aInferno Demonlord 2", 9, "§aInferno 2", showDeathTime = true), + SLAYER_BLAZE_3("§aInferno Demonlord 3", 9, "§aInferno 3", showDeathTime = true), + SLAYER_BLAZE_4("§aInferno Demonlord 4", 9, "§aInferno 4", showDeathTime = true), + + SLAYER_BLAZE_TYPHOEUS_1("§aInferno Typhoeus 1", 9, "§aTyphoeus 1"), + SLAYER_BLAZE_TYPHOEUS_2("§eInferno Typhoeus 2", 9, "§eTyphoeus 2"), + SLAYER_BLAZE_TYPHOEUS_3("§cInferno Typhoeus 3", 9, "§cTyphoeus 3"), + SLAYER_BLAZE_TYPHOEUS_4("§cInferno Typhoeus 4", 9, "§cTyphoeus 4"), + + SLAYER_BLAZE_QUAZII_1("§aInferno Quazii 1", 9, "§aQuazii 1"), + SLAYER_BLAZE_QUAZII_2("§eInferno Quazii 2", 9, "§eQuazii 2"), + SLAYER_BLAZE_QUAZII_3("§cInferno Quazii 3", 9, "§cQuazii 3"), + SLAYER_BLAZE_QUAZII_4("§cInferno Quazii 4", 9, "§cQuazii 4"), + + SLAYER_BLOODFIEND_1("§aRiftstalker Bloodfiend 1", 23, "§aBlood 1", showDeathTime = true), + SLAYER_BLOODFIEND_2("§6Riftstalker Bloodfiend 2", 23, "§6Blood 2", showDeathTime = true), + SLAYER_BLOODFIEND_3("§cRiftstalker Bloodfiend 3", 23, "§cBlood 3", showDeathTime = true), + SLAYER_BLOODFIEND_4("§4Riftstalker Bloodfiend 4", 23, "§4Blood 4", showDeathTime = true), + SLAYER_BLOODFIEND_5("§5Riftstalker Bloodfiend 5", 23, "§5Blood 5", showDeathTime = true), + + HUB_HEADLESS_HORSEMAN("§6Headless Horseman", 10), + + DUNGEON_F1("", 11), + DUNGEON_F2("", 12), + DUNGEON_F3("", 13), + DUNGEON_F4_THORN("§cThorn", 14), + DUNGEON_F5("§dLivid", 15), + DUNGEON_F("", 16), + DUNGEON_75("", 17), + + MINOS_INQUISITOR("§5Minos Inquisitor", 18), + MINOS_CHAMPION("§2Minos Champion", 18), + GAIA_CONSTURUCT("§2Gaia Construct", 18), + MINOTAUR("§2Minotaur", 18), + + THUNDER("§cThunder", 19), + LORD_JAWBUS("§cLord Jawbus", 19), + + DUMMY("Dummy", 20), + ARACHNE_SMALL("§cSmall Arachne", 21), + ARACHNE_BIG("§4Big Arachne", 21), + + // The Rift + LEECH_SUPREME("§cLeech Supreme", 22), + BACTE("§aBacte", 22), + + WINTER_REINDRAKE("Reindrake", 24),//TODO fix totally + + //TODO arachne + + //TODO corelone + //TODO bal + + + /** + * TODO dungeon mini bosses + * shadow assassin + * lost adventurer + * frozen adventurer + * king midas + * in blood room: bonzo, scarf, ?? + * f7 blood room giants + * + */ + + //TODO diana mythological creatures +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageCounter.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageCounter.kt new file mode 100644 index 000000000..1595b728a --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageCounter.kt @@ -0,0 +1,12 @@ +package at.hannibal2.skyhanni.features.combat.damageindicator + +import java.util.LinkedList + +class DamageCounter { + + var currentDamage = 0L + var currentHealing = 0L + var oldDamages = LinkedList() + var firstTick = 0L + +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageIndicatorManager.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageIndicatorManager.kt new file mode 100644 index 000000000..7735c5835 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/DamageIndicatorManager.kt @@ -0,0 +1,862 @@ +package at.hannibal2.skyhanni.features.combat.damageindicator + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator +import at.hannibal2.skyhanni.data.ScoreboardData +import at.hannibal2.skyhanni.events.BossHealthChangeEvent +import at.hannibal2.skyhanni.events.DamageIndicatorDetectedEvent +import at.hannibal2.skyhanni.events.DamageIndicatorFinalBossEvent +import at.hannibal2.skyhanni.events.LorenzChatEvent +import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent +import at.hannibal2.skyhanni.events.LorenzTickEvent +import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent +import at.hannibal2.skyhanni.features.dungeon.DungeonAPI +import at.hannibal2.skyhanni.features.slayer.blaze.HellionShield +import at.hannibal2.skyhanni.features.slayer.blaze.setHellionShield +import at.hannibal2.skyhanni.utils.EntityUtils +import at.hannibal2.skyhanni.utils.EntityUtils.getNameTagWith +import at.hannibal2.skyhanni.utils.EntityUtils.hasNameTagWith +import at.hannibal2.skyhanni.utils.LocationUtils +import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer +import at.hannibal2.skyhanni.utils.LorenzColor +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.baseMaxHealth +import at.hannibal2.skyhanni.utils.LorenzUtils.between +import at.hannibal2.skyhanni.utils.LorenzUtils.editCopy +import at.hannibal2.skyhanni.utils.LorenzUtils.put +import at.hannibal2.skyhanni.utils.LorenzUtils.round +import at.hannibal2.skyhanni.utils.LorenzVec +import at.hannibal2.skyhanni.utils.NumberUtil +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText +import at.hannibal2.skyhanni.utils.StringUtils.removeColor +import at.hannibal2.skyhanni.utils.TimeUtils +import at.hannibal2.skyhanni.utils.getLorenzVec +import net.minecraft.client.Minecraft +import net.minecraft.client.entity.EntityOtherPlayerMP +import net.minecraft.client.renderer.GlStateManager +import net.minecraft.entity.EntityLiving +import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.item.EntityArmorStand +import net.minecraft.entity.monster.EntityEnderman +import net.minecraft.entity.monster.EntityMagmaCube +import net.minecraft.entity.monster.EntityZombie +import net.minecraft.entity.passive.EntityWolf +import net.minecraftforge.client.event.RenderLivingEvent +import net.minecraftforge.event.entity.EntityJoinWorldEvent +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.UUID +import kotlin.math.max + +class DamageIndicatorManager { + + private var mobFinder: MobFinder? = null + private val maxHealth = mutableMapOf() + private val config get() = SkyHanniMod.feature.combat.damageIndicator + + companion object { + private var data = mapOf() + private val damagePattern = "[✧✯]?(\\d+[⚔+✧❤♞☄✷ﬗ✯]*)".toPattern() + + fun isBoss(entity: EntityLivingBase) = data.values.any { it.entity == entity } + + fun isDamageSplash(entity: EntityLivingBase): Boolean { + if (entity.ticksExisted > 300 || entity !is EntityArmorStand) return false + if (!entity.hasCustomName()) return false + if (entity.isDead) return false + val name = entity.customNameTag.removeColor().replace(",", "") + + return damagePattern.matcher(name).matches() + } + + fun isBossSpawned(type: BossType) = data.entries.find { it.value.bossType == type } != null + + fun isBossSpawned(vararg types: BossType) = types.any { isBossSpawned(it) } + + fun getDistanceTo(vararg types: BossType): Double { + val playerLocation = LocationUtils.playerLocation() + return data.values.filter { it.bossType in types } + .map { it.entity.getLorenzVec().distance(playerLocation) } + .let { list -> + if (list.isEmpty()) Double.MAX_VALUE else list.minOf { it } + } + } + + fun getNearestDistanceTo(location: LorenzVec): Double { + return data.values + .map { it.entity.getLorenzVec() } + .minOfOrNull { it.distance(location) } ?: Double.MAX_VALUE + } + } + + @SubscribeEvent + fun onWorldChange(event: LorenzWorldChangeEvent) { + mobFinder = MobFinder() + data = emptyMap() + } + + @SubscribeEvent(receiveCanceled = true) + fun onChatMessage(event: LorenzChatEvent) { + mobFinder?.handleChat(event.message) + } + + @SubscribeEvent + fun onWorldRender(event: LorenzRenderWorldEvent) { + if (!isEnabled()) return + + GlStateManager.disableDepth() + GlStateManager.disableCull() + + val player = Minecraft.getMinecraft().thePlayer + + //TODO config to define between 100ms and 5 sec + val filter = data.filter { + val waitForRemoval = if (it.value.dead && !noDeathDisplay(it.value.bossType)) 4_000 else 100 + (System.currentTimeMillis() > it.value.timeLastTick + waitForRemoval) || (it.value.dead && noDeathDisplay(it.value.bossType)) + } + if (filter.isNotEmpty()) { + data = data.editCopy { + for (entry in filter) { + remove(entry.key) + } + } + } + + val sizeHealth: Double + val sizeNameAbove: Double + val sizeBossName: Double + val sizeFinalResults: Double + val smallestDistanceVew: Double + val thirdPersonView = Minecraft.getMinecraft().gameSettings.thirdPersonView + // 0 == normal + // 1 == f3 behind + // 2 == selfie + if (thirdPersonView == 1) { + sizeHealth = 2.8 + sizeNameAbove = 2.2 + sizeBossName = 2.4 + sizeFinalResults = 1.8 + + smallestDistanceVew = 10.0 + } else { + sizeHealth = 1.9 + sizeNameAbove = 1.8 + sizeBossName = 2.1 + sizeFinalResults = 1.4 + + smallestDistanceVew = 6.0 + } + + for (data in data.values) { + + //TODO test end stone protector in hole? - maybe change eye pos +// data.ignoreBlocks = +// data.bossType == BossType.END_ENDSTONE_PROTECTOR && Minecraft.getMinecraft().thePlayer.isSneaking + + if (!data.ignoreBlocks && !player.canEntityBeSeen(data.entity)) continue + if (data.bossType.bossTypeToggle !in config.bossesToShow) continue + + val entity = data.entity + + var healthText = data.healthText + val delayedStart = data.delayedStart + if (delayedStart != -1L && delayedStart > System.currentTimeMillis()) { + val delay = delayedStart - System.currentTimeMillis() + healthText = formatDelay(delay) + } + + val location = if (data.dead && data.deathLocation != null) { + data.deathLocation!! + } else { + val loc = entity.getLorenzVec() + if (data.dead) data.deathLocation = loc + loc + }.add(-0.5, 0.0, -0.5) + + + event.drawDynamicText(location, healthText, sizeHealth, smallestDistanceVew = smallestDistanceVew) + + if (data.nameAbove.isNotEmpty()) { + event.drawDynamicText( + location, + data.nameAbove, + sizeNameAbove, + -18f, + smallestDistanceVew = smallestDistanceVew + ) + } + + var bossName = when (config.bossName) { + 0 -> "" + 1 -> data.bossType.fullName + 2 -> data.bossType.shortName + else -> data.bossType.fullName + } + + if (data.namePrefix.isNotEmpty()) { + bossName = data.namePrefix + bossName + } + if (data.nameSuffix.isNotEmpty()) { + bossName += data.nameSuffix + } + event.drawDynamicText(location, bossName, sizeBossName, -9f, smallestDistanceVew = smallestDistanceVew) + + if (config.showDamageOverTime) { + var diff = 13f + val currentDamage = data.damageCounter.currentDamage + val currentHealing = data.damageCounter.currentHealing + if (currentDamage != 0L || currentHealing != 0L) { + val formatDamage = "§c" + NumberUtil.format(currentDamage) + val formatHealing = "§a+" + NumberUtil.format(currentHealing) + val finalResult = if (currentHealing == 0L) { + formatDamage + } else if (currentDamage == 0L) { + formatHealing + } else { + "$formatDamage §7/ $formatHealing" + } + event.drawDynamicText( + location, + finalResult, + sizeFinalResults, + diff, + smallestDistanceVew = smallestDistanceVew + ) + diff += 9f + } + for (damage in data.damageCounter.oldDamages) { + val formatDamage = "§c" + NumberUtil.format(damage.damage) + "/s" + val formatHealing = "§a+" + NumberUtil.format(damage.healing) + "/s" + val finalResult = if (damage.healing == 0L) { + formatDamage + } else if (damage.damage == 0L) { + formatHealing + } else { + "$formatDamage §7/ $formatHealing" + } + event.drawDynamicText( + location, + finalResult, + sizeFinalResults, + diff, + smallestDistanceVew = smallestDistanceVew + ) + diff += 9f + } + } + + } + GlStateManager.enableDepth() + GlStateManager.enableCull() + } + + private fun noDeathDisplay(bossType: BossType): Boolean { + return when (bossType) { + BossType.SLAYER_BLAZE_TYPHOEUS_1, + BossType.SLAYER_BLAZE_TYPHOEUS_2, + BossType.SLAYER_BLAZE_TYPHOEUS_3, + BossType.SLAYER_BLAZE_TYPHOEUS_4, + BossType.SLAYER_BLAZE_QUAZII_1, + BossType.SLAYER_BLAZE_QUAZII_2, + BossType.SLAYER_BLAZE_QUAZII_3, + BossType.SLAYER_BLAZE_QUAZII_4, + + //TODO f3/m3 4 guardians, f2/m2 4 boss room fighters + -> true + + else -> false + } + } + + private fun tickDamage(damageCounter: DamageCounter) { + val now = System.currentTimeMillis() + if (damageCounter.currentDamage != 0L || damageCounter.currentHealing != 0L) { + if (damageCounter.firstTick == 0L) { + damageCounter.firstTick = now + } + + if (now > damageCounter.firstTick + 1_000) { + damageCounter.oldDamages.add( + 0, OldDamage(now, damageCounter.currentDamage, damageCounter.currentHealing) + ) + damageCounter.firstTick = 0L + damageCounter.currentDamage = 0 + damageCounter.currentHealing = 0 + } + } + damageCounter.oldDamages.removeIf { now > it.time + 5_000 } + } + + private fun formatDelay(delay: Long): String { + val color = when { + delay < 1_000 -> LorenzColor.DARK_PURPLE + delay < 3_000 -> LorenzColor.LIGHT_PURPLE + + else -> LorenzColor.WHITE + } + val format = TimeUtils.formatDuration(delay, showMilliSeconds = true) + return color.getChatColor() + format + } + + @SubscribeEvent + fun onTick(event: LorenzTickEvent) { + if (!isEnabled()) return + data = data.editCopy { + EntityUtils.getEntities().mapNotNull(::checkEntity).forEach { this put it } + } + } + + private fun checkEntity(entity: EntityLivingBase): Pair? { + try { + val entityData = grabData(entity) ?: return null + if (LorenzUtils.inDungeons) { + checkFinalBoss(entityData.finalDungeonBoss, entity.entityId) + } + + val health = entity.health.toLong() + val maxHealth: Long + val biggestHealth = getMaxHealthFor(entity) + if (biggestHealth == 0L) { + val currentMaxHealth = entity.baseMaxHealth.toLong() + maxHealth = max(currentMaxHealth, health) + setMaxHealth(entity, maxHealth) + } else { + maxHealth = biggestHealth + } + + entityData.namePrefix = "" + entityData.nameSuffix = "" + entityData.nameAbove = "" + val customHealthText = if (health == 0L) { + entityData.dead = true + if (entityData.bossType.showDeathTime && config.timeToKillSlayer) { + entityData.nameAbove = entityData.timeToKill + } + "§cDead" + } else { + getCustomHealth(entityData, health, entity, maxHealth) ?: return null + } + + if (data.containsKey(entity.uniqueID)) { + val lastHealth = data[entity.uniqueID]!!.lastHealth + checkDamage(entityData, health, lastHealth) + tickDamage(entityData.damageCounter) + + BossHealthChangeEvent(entityData, lastHealth, health, maxHealth).postAndCatch() + } + entityData.lastHealth = health + + if (customHealthText.isNotEmpty()) { + entityData.healthText = customHealthText + } else { + val color = NumberUtil.percentageColor(health, maxHealth) + entityData.healthText = color.getChatColor() + NumberUtil.format(health) + } + entityData.timeLastTick = System.currentTimeMillis() + return entity.uniqueID to entityData + } catch (e: Throwable) { + e.printStackTrace() + return null + } + } + + private fun getCustomHealth( + entityData: EntityData, + health: Long, + entity: EntityLivingBase, + maxHealth: Long, + ): String? { + + when (entityData.bossType) { + BossType.DUNGEON_F4_THORN -> { + val thorn = checkThorn(health, maxHealth) + if (thorn == null) { + val floor = DungeonAPI.dungeonFloor + LorenzUtils.error("problems with thorn detection! ($floor, $health/$maxHealth)") + } + return thorn + } + + BossType.SLAYER_ENDERMAN_1, + BossType.SLAYER_ENDERMAN_2, + BossType.SLAYER_ENDERMAN_3, + BossType.SLAYER_ENDERMAN_4, + -> return checkEnderSlayer(entity as EntityEnderman, entityData, health.toInt(), maxHealth.toInt()) + + BossType.SLAYER_BLOODFIEND_1, + BossType.SLAYER_BLOODFIEND_2, + BossType.SLAYER_BLOODFIEND_3, + BossType.SLAYER_BLOODFIEND_4, + -> return checkVampireSlayer(entity as EntityOtherPlayerMP, entityData, health.toInt(), maxHealth.toInt()) + + BossType.SLAYER_BLAZE_1, + BossType.SLAYER_BLAZE_2, + BossType.SLAYER_BLAZE_3, + BossType.SLAYER_BLAZE_4, + BossType.SLAYER_BLAZE_QUAZII_2, + BossType.SLAYER_BLAZE_QUAZII_3, + BossType.SLAYER_BLAZE_QUAZII_4, + BossType.SLAYER_BLAZE_TYPHOEUS_2, + BossType.SLAYER_BLAZE_TYPHOEUS_3, + BossType.SLAYER_BLAZE_TYPHOEUS_4, + -> return checkBlazeSlayer(entity as EntityLiving, entityData, health.toInt(), maxHealth.toInt()) + + BossType.NETHER_MAGMA_BOSS -> return checkMagmaCube( + entity as EntityMagmaCube, + entityData, + health.toInt(), + maxHealth.toInt() + ) + + BossType.SLAYER_ZOMBIE_5 -> { + if ((entity as EntityZombie).hasNameTagWith(3, "§fBoom!")) { + //TODO fix +// val ticksAlive = entity.ticksExisted % (20 * 5) +// val remainingTicks = (5 * 20).toLong() - ticksAlive +// val format = formatDelay(remainingTicks * 50) +// entityData.nameSuffix = " §f§lBOOM - $format" + entityData.nameSuffix = " §f§lBOOM!" + } + } + + BossType.SLAYER_WOLF_3, + BossType.SLAYER_WOLF_4, + -> { + if ((entity as EntityWolf).hasNameTagWith(2, "§bCalling the pups!")) { + return "Pups!" + } + } + + BossType.NETHER_BARBARIAN_DUKE, + -> { + val location = entity.getLorenzVec() + entityData.ignoreBlocks = location.y == 117.0 && location.distanceToPlayer() < 15 + } + + else -> return "" + } + return "" + } + + private fun checkBlazeSlayer(entity: EntityLiving, entityData: EntityData, health: Int, maxHealth: Int): String { + var found = false + for (shield in HellionShield.entries) { + val armorStand = entity.getNameTagWith(3, shield.name) + if (armorStand != null) { + val number = armorStand.name.split(" ♨")[1].substring(0, 1) + entity.setHellionShield(shield) + entityData.nameAbove = shield.formattedName + " $number" + found = true + break + } + } + if (!found) { + entity.setHellionShield(null) + } + + if (!SkyHanniMod.feature.slayer.blazes.phaseDisplay) return "" + + var calcHealth = health + val calcMaxHealth: Int + entityData.namePrefix = when (entityData.bossType) { + BossType.SLAYER_BLAZE_1, + BossType.SLAYER_BLAZE_2, + -> { + val step = maxHealth / 2 + calcMaxHealth = step + if (health > step) { + calcHealth -= step + "§c1/2 " + } else { + calcHealth = health + "§a2/2 " + } + } + + BossType.SLAYER_BLAZE_3, + BossType.SLAYER_BLAZE_4, + -> { + val step = maxHealth / 3 + calcMaxHealth = step + if (health > step * 2) { + calcHealth -= step * 2 + "§c1/3 " + } else if (health > step) { + calcHealth -= step + "§e2/3 " + } else { + calcHealth = health + "§a3/3 " + } + } + + else -> return "" + } + + return NumberUtil.percentageColor( + calcHealth.toLong(), calcMaxHealth.toLong() + ).getChatColor() + NumberUtil.format(calcHealth) + } + + private fun checkMagmaCube( + entity: EntityMagmaCube, + entityData: EntityData, + health: Int, + maxHealth: Int, + ): String? { + val slimeSize = entity.slimeSize + entityData.namePrefix = when (slimeSize) { + 24 -> "§c1/6" + 22 -> "§e2/6" + 20 -> "§e3/6" + 18 -> "§e4/6" + 16 -> "§e5/6" + else -> { + val color = NumberUtil.percentageColor(health.toLong(), 10_000_000) + entityData.namePrefix = "§a6/6" + return color.getChatColor() + NumberUtil.format(health) + } + } + " §f" + + //hide while in the middle +// val position = entity.getLorenzVec() + //TODO other logic or something +// entityData.healthLineHidden = position.x == -368.0 && position.z == -804.0 + + var calcHealth = -1 + for (line in ScoreboardData.sidebarLinesRaw) { + if (line.contains("▎")) { + val color: String + if (line.startsWith("§7")) { + color = "§7" + } else if (line.startsWith("§e")) { + color = "§e" + } else if (line.startsWith("§6") || line.startsWith("§a") || line.startsWith("§c")) { + calcHealth = 0 + break + } else { + LorenzUtils.error("unknown magma boss health sidebar format!") + break + } + + val text = line.replace("\uD83C\uDF81" + color, "") + val max = 25.0 + val length = text.split("§e", "§7")[1].length + val missing = (health.toDouble() / max) * length + calcHealth = (health - missing).toInt() + } + } + if (calcHealth == -1) return null + + val color = NumberUtil.percentageColor(calcHealth.toLong(), maxHealth.toLong()) + return color.getChatColor() + NumberUtil.format(calcHealth) + } + + private fun checkEnderSlayer( + entity: EntityEnderman, + entityData: EntityData, + health: Int, + maxHealth: Int, + ): String? { + var calcHealth = health + val calcMaxHealth: Int + entityData.namePrefix = when (entityData.bossType) { + BossType.SLAYER_ENDERMAN_1, + BossType.SLAYER_ENDERMAN_2, + BossType.SLAYER_ENDERMAN_3, + -> { + val step = maxHealth / 3 + calcMaxHealth = step + if (health > step * 2) { + calcHealth -= step * 2 + "§c1/3 " + } else if (health > step) { + calcHealth -= step + "§e2/3 " + } else { + calcHealth = health + "§a3/3 " + } + } + + BossType.SLAYER_ENDERMAN_4 -> { + val step = maxHealth / 6 + calcMaxHealth = step + if (health > step * 5) { + calcHealth -= step * 5 + "§c1/6 " + } else if (health > step * 4) { + calcHealth -= step * 4 + "§e2/6 " + } else if (health > step * 3) { + calcHealth -= step * 3 + "§e3/6 " + } else if (health > step * 2) { + calcHealth -= step * 2 + "§e4/6 " + } else if (health > step) { + calcHealth -= step + "§e5/6 " + } else { + calcHealth = health + "§a6/6 " + } + } + + else -> return null + } + var result = NumberUtil.percentageColor( + calcHealth.toLong(), calcMaxHealth.toLong() + ).getChatColor() + NumberUtil.format(calcHealth) + + if (!SkyHanniMod.feature.slayer.endermen.phaseDisplay) { + result = "" + entityData.namePrefix = "" + } + + + //Hit phase + val armorStandHits = entity.getNameTagWith(3, " Hit") + if (armorStandHits != null) { + val maxHits = when (entityData.bossType) { + BossType.SLAYER_ENDERMAN_1 -> 15 + BossType.SLAYER_ENDERMAN_2 -> 30 + BossType.SLAYER_ENDERMAN_3 -> 60 + BossType.SLAYER_ENDERMAN_4 -> 100 + else -> 100 + } + val name = armorStandHits.name.removeColor() + + // TODO replace this super ugly workaround with regex + val text = name.between("Seraph ", " Hit") + val hits = try { + text.toInt() + } catch (e: NumberFormatException) { + text.substring(2).toInt() + } + + return NumberUtil.percentageColor(hits.toLong(), maxHits.toLong()).getChatColor() + "$hits Hits" + } + + //Laser phase + if (config.enderSlayer.laserPhaseTimer && entity.ridingEntity != null) { + val ticksAlive = entity.ridingEntity.ticksExisted.toLong() + //TODO more tests, more exact values, better logic? idk make this working perfectly pls + //val remainingTicks = 8 * 20 - ticksAlive + val remainingTicks = (7.4 * 20).toLong() - ticksAlive + + if (config.enderSlayer.showHealthDuringLaser) { + entityData.nameSuffix = " §f" + formatDelay(remainingTicks * 50) + } else { + return formatDelay(remainingTicks * 50) + } + } + + return result + } + + private fun checkVampireSlayer( + entity: EntityOtherPlayerMP, + entityData: EntityData, + health: Int, + maxHealth: Int, + ): String { + val config = config.vampireSlayer + + if (config.percentage) { + val percentage = LorenzUtils.formatPercentage(health.toDouble() / maxHealth) + entityData.nameSuffix = " §e$percentage" + } + + if (config.maniaCircles) { + entity.ridingEntity?.let { + val existed = it.ticksExisted + if (existed > 40) { + val end = (20 * 26) - existed + val time = end.toDouble() / 20 + entityData.nameAbove = "Mania Circles: §b${time.round(1)}s" + return "" + } + } + } + + if (config.hpTillSteak) { + val rest = maxHealth * 0.2 + val showHealth = health - rest + if (showHealth < 300) { + entityData.nameAbove = if (showHealth > 0) { + "§cHP till Steak: ${showHealth.addSeparators()}" + } else "§cSteak!" + } + } + + return "" + } + + private fun checkThorn(realHealth: Long, realMaxHealth: Long): String? { + val maxHealth: Int + val health = if (DungeonAPI.isOneOf("F4")) { + maxHealth = 4 + + if (realMaxHealth == 300_000L) { + // no derpy + when { + realHealth == 1L -> 0 + realHealth <= 66_000 -> 1 + realHealth <= 144_000 -> 2 + realHealth <= 222_000 -> 3 + realHealth <= 300_000 -> 4 + + else -> return null + } + } else { + // derpy + when { + realHealth == 1L -> 0 + realHealth <= 132_000 -> 1 + realHealth <= 288_000 -> 2 + realHealth <= 444_000 -> 3 + realHealth <= 600_000 -> 4 + + else -> return null + } + } + } else if (DungeonAPI.isOneOf("M4")) { + maxHealth = 6 + + if (realMaxHealth == 900_000L) { + // no derpy + when { + realHealth == 1L -> 0 + realHealth <= 135_000 -> 1 + realHealth <= 288_000 -> 2 + realHealth <= 441_000 -> 3 + realHealth <= 594_000 -> 4 + realHealth <= 747_000 -> 5 + realHealth <= 900_000L -> 6 + + else -> return null + } + } else { + // derpy + when { + realHealth == 1L -> 0 + realHealth <= 270_000 -> 1 + realHealth <= 576_000 -> 2 + realHealth <= 882_000 -> 3 + realHealth <= 1_188_000 -> 4 + realHealth <= 1_494_000 -> 5 + realHealth <= 1_800_000 -> 6 + + else -> return null + } + } + } else { + LorenzUtils.error("Invalid/impossible thorn floor!") + return null + } + val color = NumberUtil.percentageColor(health.toLong(), maxHealth.toLong()) + return color.getChatColor() + health + "/" + maxHealth + } + + private fun checkDamage(entityData: EntityData, health: Long, lastHealth: Long) { + val damage = lastHealth - health + val healing = health - lastHealth + if (damage > 0 && entityData.bossType != BossType.DUMMY) { + val damageCounter = entityData.damageCounter + damageCounter.currentDamage += damage + } + if (healing > 0) { + //Hide auto heal every 10 ticks (with rounding errors) + if ((healing == 15_000L || healing == 15_001L) && entityData.bossType == BossType.SLAYER_ZOMBIE_5) return + + val damageCounter = entityData.damageCounter + damageCounter.currentHealing += healing + } + } + + private fun grabData(entity: EntityLivingBase): EntityData? { + if (data.contains(entity.uniqueID)) return data[entity.uniqueID] + + + val entityResult = mobFinder?.tryAdd(entity) ?: return null + + val entityData = EntityData( + entity, + entityResult.ignoreBlocks, + entityResult.delayedStart, + entityResult.finalDungeonBoss, + entityResult.bossType, + foundTime = System.currentTimeMillis() + ) + DamageIndicatorDetectedEvent(entityData).postAndCatch() + return entityData + } + + private fun checkFinalBoss(finalBoss: Boolean, id: Int) { + if (finalBoss) { + DamageIndicatorFinalBossEvent(id).postAndCatch() + } + } + + private fun setMaxHealth(entity: EntityLivingBase, currentMaxHealth: Long) { + maxHealth[entity.uniqueID!!] = currentMaxHealth + } + + private fun getMaxHealthFor(entity: EntityLivingBase): Long { + return maxHealth.getOrDefault(entity.uniqueID!!, 0L) + } + + @SubscribeEvent + fun onEntityJoin(event: EntityJoinWorldEvent) { + mobFinder?.handleNewEntity(event.entity) + } + + private val dummyDamageCache = mutableListOf() + + @SubscribeEvent(priority = EventPriority.HIGH) + fun onRenderLiving(event: RenderLivingEvent.Specials.Pre) { + val entity = event.entity + + val entityData = data.values.find { + val distance = it.entity.getLorenzVec().distance(entity.getLorenzVec()) + distance < 4.5 + } + + if (isDamageSplash(entity)) { + val name = entity.customNameTag.removeColor().replace(",", "") + + if (entityData != null) { + if (config.hideDamageSplash) { + event.isCanceled = true + } + if (entityData.bossType == BossType.DUMMY) { + val uuid = entity.uniqueID + if (dummyDamageCache.contains(uuid)) return + dummyDamageCache.add(uuid) + val dmg = name.toCharArray().filter { Character.isDigit(it) }.joinToString("").toLong() + entityData.damageCounter.currentDamage += dmg + } + } + } else { + if (entityData != null && isEnabled() && config.hideVanillaNametag) { + val name = entity.name + if (name.contains("Plaesmaflux")) return + if (name.contains("Overflux")) return + if (name.contains("Mana Flux")) return + if (name.contains("Radiant")) return + event.isCanceled = true + } + } + } + + @SubscribeEvent + fun onConfigFix(event: ConfigUpdaterMigrator.ConfigFixEvent) { + event.move(2, "damageIndicator", "combat.damageIndicator") + event.move(3, "slayer.endermanPhaseDisplay", "slayer.endermen.phaseDisplay") + event.move(3, "slayer.blazePhaseDisplay", "slayer.blazes.phaseDisplay") + } + + fun isEnabled() = LorenzUtils.inSkyBlock && config.enabled +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/EntityData.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/EntityData.kt new file mode 100644 index 000000000..970bdd0dd --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/EntityData.kt @@ -0,0 +1,30 @@ +package at.hannibal2.skyhanni.features.combat.damageindicator + +import at.hannibal2.skyhanni.utils.LorenzVec +import at.hannibal2.skyhanni.utils.TimeUnit +import at.hannibal2.skyhanni.utils.TimeUtils +import net.minecraft.entity.EntityLivingBase + +class EntityData( + val entity: EntityLivingBase, + var ignoreBlocks: Boolean, + var delayedStart: Long, + val finalDungeonBoss: Boolean, + val bossType: BossType, + val damageCounter: DamageCounter = DamageCounter(), + val foundTime: Long, + + var lastHealth: Long = 0L, + var healthText: String = "", + var timeLastTick: Long = 0, + var namePrefix: String = "", + var nameSuffix: String = "", + var nameAbove: String = "", + var dead: Boolean = false, + var deathLocation: LorenzVec? = null, +) { + val timeToKill by lazy { + val duration = System.currentTimeMillis() - foundTime + "§e" + TimeUtils.formatDuration(duration, TimeUnit.SECOND, showMilliSeconds = true) + } +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/EntityResult.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/EntityResult.kt new file mode 100644 index 000000000..4f6920293 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/EntityResult.kt @@ -0,0 +1,8 @@ +package at.hannibal2.skyhanni.features.combat.damageindicator + +class EntityResult( + val delayedStart: Long = -1L, + val ignoreBlocks: Boolean = false, + val finalDungeonBoss: Boolean = false, + val bossType: BossType = BossType.GENERIC_DUNGEON_BOSS, +) \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/MobFinder.kt b/src/main/java/at/hannibal2/skyhanni/features/combat/damageindicator/MobFinder.kt new file mode 100644 index 000000000..9476eafb