diff options
Diffstat (limited to 'src/main/kotlin')
2 files changed, 701 insertions, 0 deletions
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/bestiary/BestiaryData.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/bestiary/BestiaryData.kt new file mode 100644 index 00000000..69688fcc --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/bestiary/BestiaryData.kt @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2023 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>. + */ + +package io.github.moulberry.notenoughupdates.miscfeatures.profileviewer.bestiary + +import com.google.gson.JsonObject +import io.github.moulberry.notenoughupdates.util.Constants +import io.github.moulberry.notenoughupdates.util.ItemUtils +import io.github.moulberry.notenoughupdates.util.Utils +import io.github.moulberry.notenoughupdates.util.roundToDecimals +import kotlin.math.min + +object BestiaryData { + private val categoriesToParse = listOf( + "dynamic", + "hub", + "farming_1", + "garden", + "combat_1", + "combat_3", + "crimson_isle", + "mining_2", + "mining_3", + "crystal_hollows", + "foraging_1", + "spooky_festival", + "mythological_creatures", + "jerry", + "kuudra", + "catacombs", + "fishing" + ) + + /** + * Calculates the sum of all individual tiers for this profile + * + * @param computedCategories List of parsed categories + * @see BestiaryPage.parseBestiaryData + */ + @JvmStatic + fun calculateTotalBestiaryTiers(computedCategories: List<Category>): Int { + var tiers = 0.0 + computedCategories.forEach { + tiers += countTotalLevels(it) + } + return tiers.toInt() + } + + /** + * Calculate the skyblock xp awarded for the given bestiary progress + */ + @JvmStatic + fun calculateBestiarySkyblockXp(profileInfo: JsonObject): Int { + val totalTiers = calculateTotalBestiaryTiers(parseBestiaryData(profileInfo)) + var skyblockXp = 0 + + val slayingTask = Constants.SBLEVELS.getAsJsonObject("slaying_task") ?: return 0 + val xpPerTier = (slayingTask.get("bestiary_family_xp") ?: return 0).asInt + val xpPerMilestone = slayingTask.get("bestiary_milestone_xp").asInt + val maxXp = slayingTask.get("bestiary_progress").asInt + + skyblockXp += totalTiers * xpPerTier + + val milestones = (totalTiers / 100) + skyblockXp += milestones * xpPerMilestone + + return min(skyblockXp, maxXp) + } + + private fun countTotalLevels(category: Category): Int { + var levels = 0 + for (mob in category.mobs) { + levels += mob.mobLevelData.level + } + category.subCategories.forEach { levels += countTotalLevels(it) } + return levels + } + + /** + * Checks if a user profile has migrated. + * + * @param profileInfo skyblock profile information + */ + fun hasMigrated(profileInfo: JsonObject): Boolean { + val bestiaryObject = profileInfo.getAsJsonObject("bestiary") ?: return false + + return (bestiaryObject.get("migration") ?: return false).asBoolean + } + + /** + * Parse the bestiary data for the profile. Categories are taken from the `constants/bestiary.json` repo file + * + * @param profileInfo the JsonObject containing the bestiary data + */ + @JvmStatic + fun parseBestiaryData(profileInfo: JsonObject): MutableList<Category> { + if (!hasMigrated(profileInfo) || Constants.BESTIARY == null) { + return mutableListOf() + } + + val parsedCategories = mutableListOf<Category>() + + val apiKills = profileInfo.getAsJsonObject("bestiary")!!.getAsJsonObject("kills") ?: return mutableListOf() + val apiDeaths = profileInfo.getAsJsonObject("bestiary").getAsJsonObject("deaths") ?: return mutableListOf() + val killsMap: HashMap<String, Int> = HashMap() + for (entry in apiKills.entrySet()) { + killsMap[entry.key] = entry.value.asInt + } + val deathsMap: HashMap<String, Int> = HashMap() + for (entry in apiDeaths.entrySet()) { + deathsMap[entry.key] = entry.value.asInt + } + + for (categoryId in categoriesToParse) { + val categoryData = Constants.BESTIARY.getAsJsonObject(categoryId) + if (categoryData != null) { + parsedCategories.add(parseCategory(categoryData, categoryId, killsMap, deathsMap)) + } else { + Utils.showOutdatedRepoNotification("bestiary.json missing or outdated") + } + } + + return parsedCategories + } + + /** + * Parse one individual category, including potential subcategories + */ + private fun parseCategory( + categoryData: JsonObject, + categoryId: String, + killsMap: HashMap<String, Int>, + deathsMap: HashMap<String, Int> + ): Category { + val categoryName = categoryData["name"].asString + val computedMobs: MutableList<Mob> = mutableListOf() + val categoryIconData = categoryData["icon"].asJsonObject + + val categoryIcon = if (categoryIconData.has("skullOwner")) { + Utils.createSkull( + categoryName, categoryIconData["skullOwner"].asString, categoryIconData["texture"].asString + ) + } else { + ItemUtils.createItemStackFromId(categoryIconData["item"].asString, categoryName) + } + if (categoryData.has("hasSubcategories")) { // It must have some subcategories + val subCategories: MutableList<Category> = mutableListOf() + + val reserved = listOf("name", "icon", "hasSubcategories") + for (entry in categoryData.entrySet()) { + if (!reserved.contains(entry.key)) { + subCategories.add( + parseCategory( + entry.value.asJsonObject, + "${categoryId}_${entry.key}", + killsMap, + deathsMap + ) + ) + } + } + return Category(categoryId, categoryName, categoryIcon, emptyList(), subCategories, calculateFamilyDataOfSubcategories(subCategories)) + } else { + val categoryMobs = categoryData["mobs"].asJsonArray.map { it.asJsonObject } + + for (mobData in categoryMobs) { + val mobName = mobData["name"].asString + val mobIcon = if (mobData.has("skullOwner")) { + Utils.createSkull( + mobName, mobData["skullOwner"].asString, mobData["texture"].asString + ) + } else { + ItemUtils.createItemStackFromId(mobData["item"].asString, mobName) + } + + val cap = mobData["cap"].asDouble + val bracket = mobData["bracket"].asInt + + var kills = 0.0 + var deaths = 0.0 + + // The mobs array contains the individual names returned by the API + val mobsArray = mobData["mobs"].asJsonArray.map { it.asString } + for (s in mobsArray) { + kills += killsMap.getOrDefault(s, 0) + deaths += deathsMap.getOrDefault(s, 0) + } + + val levelData = calculateLevel(bracket, kills, cap) + computedMobs.add(Mob(mobName, mobIcon, kills, deaths, levelData)) + } + return Category(categoryId, categoryName, categoryIcon, computedMobs, emptyList(), calculateFamilyData(computedMobs)) + } + } + + /** + * Calculates the level for a given mob + * + * @param bracket applicable bracket number + * @param kills number of kills the player has on that mob type + * @param cap maximum kill limit for the mob + */ + private fun calculateLevel(bracket: Int, kills: Double, cap: Double): MobLevelData { + val bracketData = + Constants.BESTIARY["brackets"].asJsonObject[bracket.toString()].asJsonArray.map { it.asDouble } + var maxLevel = false + var progress = 0.0 + var effKills = 0.0 + var effReq = 0.0 + + val effectiveKills = if (kills >= cap) { + maxLevel = true + cap + } else { + kills + } + + val totalProgress = (effectiveKills/cap*100).roundToDecimals(1) + + var level = 0 + for (requiredKills in bracketData) { + if (effectiveKills >= requiredKills) { + level++ + } else { + val prevTierKills = if (level != 0) bracketData[(level - 1).coerceAtLeast(0)].toInt() else 0 + effKills = kills - prevTierKills + effReq = requiredKills - prevTierKills + progress = (effKills / effReq * 100).roundToDecimals(1) + break + } + } + return MobLevelData(level, maxLevel, progress, totalProgress, MobKillData(effKills, effReq, effectiveKills, cap)) + } + + private fun calculateFamilyData(mobs: List<Mob>): FamilyData { + var found = 0 + var completed = 0 + + for (mob in mobs) { + if(mob.kills > 0) found++ + if(mob.mobLevelData.maxLevel) completed++ + } + + return FamilyData(found, completed, mobs.size) + } + + private fun calculateFamilyDataOfSubcategories(subCategories: List<Category>): FamilyData { + var found = 0 + var completed = 0 + var total = 0 + + for (category in subCategories) { + val data = category.familyData + found += data.found + completed += data.completed + total += data.total + } + + return FamilyData(found, completed, total) + } +} diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/bestiary/BestiaryPage.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/bestiary/BestiaryPage.kt new file mode 100644 index 00000000..d2dfe910 --- /dev/null +++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/miscfeatures/profileviewer/bestiary/BestiaryPage.kt @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2022-2023 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>. + */ +package io.github.moulberry.notenoughupdates.miscfeatures.profileviewer.bestiary + +import io.github.moulberry.notenoughupdates.core.util.StringUtils +import io.github.moulberry.notenoughupdates.profileviewer.GuiProfileViewer +import io.github.moulberry.notenoughupdates.profileviewer.GuiProfileViewerPage +import io.github.moulberry.notenoughupdates.miscfeatures.profileviewer.bestiary.BestiaryData.calculateTotalBestiaryTiers +import io.github.moulberry.notenoughupdates.miscfeatures.profileviewer.bestiary.BestiaryData.hasMigrated +import io.github.moulberry.notenoughupdates.miscfeatures.profileviewer.bestiary.BestiaryData.parseBestiaryData +import io.github.moulberry.notenoughupdates.util.Constants +import io.github.moulberry.notenoughupdates.util.Utils +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.ScaledResolution +import net.minecraft.client.renderer.GlStateManager +import net.minecraft.client.renderer.RenderHelper +import net.minecraft.item.ItemStack +import net.minecraft.util.EnumChatFormatting +import net.minecraft.util.ResourceLocation +import org.lwjgl.input.Mouse +import org.lwjgl.opengl.GL11 +import java.awt.Color + +/** + * Individual mob entry in the Bestiary + */ +data class Mob( + val name: String, val icon: ItemStack, val kills: Double, val deaths: Double, val mobLevelData: MobLevelData +) + +/** + * A Bestiary category as defined in `constants/bestiary.json` + */ +data class Category( + val id: String, + val name: String, + val icon: ItemStack, + val mobs: List<Mob>, + val subCategories: List<Category>, + val familyData: FamilyData +) + +/** + * Level data for one specific mob + */ +data class MobLevelData( + val level: Int, val maxLevel: Boolean, val progress: Double, val totalProgress: Double, val mobKillData: MobKillData +) + +/** + * Kills data for one specific mob + */ +data class MobKillData( + val tierKills: Double, val tierReq: Double, val cappedKills: Double, val cap: Double +) + +/** + * Family data for one specific category + */ +data class FamilyData( + val found: Int, val completed: Int, val total: Int +) + +class BestiaryPage(instance: GuiProfileViewer?) : GuiProfileViewerPage(instance) { + private var selectedCategory = "dynamic" + private var selectedSubCategory = "" + private var tooltipToDisplay: MutableList<String> = mutableListOf() + private var bestiaryLevel = 0.0 + private var computedCategories: MutableList<Category> = mutableListOf() + private var lastProfileName = "" + + private val bestiaryTexture = ResourceLocation("notenoughupdates:pv_bestiary_tab.png") + private val mobListXCount = 9 + private val mobListYCount = 5 + private val mobListXPadding = (240 - mobListXCount * 20) / (mobListXCount + 1).toFloat() + private val mobListYPadding = (202 - mobListYCount * 20) / (mobListYCount + 1).toFloat() + + override fun drawPage(mouseX: Int, mouseY: Int, partialTicks: Float) { + val guiLeft = GuiProfileViewer.getGuiLeft() + val guiTop = GuiProfileViewer.getGuiTop() + + val selectedProfile = GuiProfileViewer.getSelectedProfile() ?: return + val profileInfo = selectedProfile.profileJson + val profileName = GuiProfileViewer.getProfileName() + + if (!hasMigrated(profileInfo) || Constants.BESTIARY == null) { + Utils.drawStringCentered( + "${EnumChatFormatting.RED}No valid bestiary data!", + guiLeft + 431 / 2f, + (guiTop + 101).toFloat(), + true, + 0 + ) + lastProfileName = profileName + return + } + // Do the initial parsing only once or on profile switch + if (computedCategories.isEmpty() || lastProfileName != profileName) { + computedCategories = parseBestiaryData(profileInfo) + bestiaryLevel = calculateTotalBestiaryTiers(computedCategories).toDouble() + } + + if (computedCategories.isEmpty() || Constants.BESTIARY == null) { + Utils.drawStringCentered( + "${EnumChatFormatting.RED}No valid bestiary data!", + guiLeft + 431 / 2f, + (guiTop + 101).toFloat(), + true, + 0 + ) + lastProfileName = profileName + return + } + lastProfileName = profileName + + val bestiarySize = computedCategories.size + val bestiaryXSize = (350f / (bestiarySize - 1 + 0.0000001f)).toInt() + + + // Render the category list + for ((categoryXIndex, category) in computedCategories.withIndex()) { + Minecraft.getMinecraft().textureManager.bindTexture(GuiProfileViewer.pv_elements) + + if (mouseX > guiLeft + 30 + bestiaryXSize * categoryXIndex && + mouseX < guiLeft + 30 + bestiaryXSize * categoryXIndex + 20 + && mouseY > guiTop + 10 && mouseY < guiTop + 10 + 20 + ) { + tooltipToDisplay.add(EnumChatFormatting.GRAY.toString() + category.name) + if (Mouse.getEventButtonState() && selectedCategory != category.id) { + selectedCategory = category.id + Utils.playPressSound() + } + } + + + if (category.id == selectedCategory) { + Utils.drawTexturedRect( + (guiLeft + 30 + bestiaryXSize * categoryXIndex).toFloat(), + (guiTop + 10).toFloat(), + 20f, + 20f, + 20 / 256f, + 0f, + 20 / 256f, + 0f, + GL11.GL_NEAREST + ) + } else { + Utils.drawTexturedRect( + (guiLeft + 30 + bestiaryXSize * categoryXIndex).toFloat(), + (guiTop + 10).toFloat(), + 20f, + 20f, + 0f, + 20 / 256f, + 0f, + 20 / 256f, + GL11.GL_NEAREST + ) + } + Utils.drawItemStack(category.icon, guiLeft + 32 + bestiaryXSize * categoryXIndex, guiTop + 12) + } + + val scaledResolution = ScaledResolution(Minecraft.getMinecraft()) + val width = scaledResolution.scaledWidth + val height = scaledResolution.scaledHeight + + Minecraft.getMinecraft().textureManager.bindTexture(bestiaryTexture) + Utils.drawTexturedRect(guiLeft.toFloat(), guiTop.toFloat(), 431f, 202f, GL11.GL_NEAREST) + GlStateManager.color(1f, 1f, 1f, 1f) + val color = Color(128, 128, 128, 255) + Utils.renderAlignedString( + EnumChatFormatting.RED.toString() + "Milestone: ", + "${EnumChatFormatting.GRAY}${(bestiaryLevel / 10)}", + (guiLeft + 280).toFloat(), + (guiTop + 50).toFloat(), + 110 + ) + + // Render the subcategories in the bottom right corner, if applicable + val selectedCategory = computedCategories.first { it.id == selectedCategory } + if (selectedCategory.subCategories.isNotEmpty()) { + if (selectedSubCategory == "") { + selectedSubCategory = selectedCategory.subCategories.first().id + } + + Utils.renderShadowedString( + "${EnumChatFormatting.RED}Subcategories", (guiLeft + 317).toFloat(), (guiTop + 165).toFloat(), 1000 + ) + GlStateManager.color(1f, 1f, 1f, 1f) + + val xStart = (guiLeft + 280).toFloat() + val y = (guiTop + 175).toFloat() + for ((i, subCategory) in selectedCategory.subCategories.withIndex()) { + Minecraft.getMinecraft().textureManager.bindTexture(GuiProfileViewer.pv_elements) + + if (subCategory.id == selectedSubCategory) { + Utils.drawTexturedRect( + (xStart + 24 * i.toFloat()), + y, + 20f, + 20f, + 20 / 256f, + 0f, + 20 / 256f, + 0f, + GL11.GL_NEAREST + ) + } else { + Utils.drawTexturedRect( + (xStart + 24 * i.toFloat()), + y, + 20f, + 20f, + 0f, + 20 / 256f, + 0f, + 20 / 256f, + GL11.GL_NEAREST + ) + } + Utils.drawItemStack(subCategory.icon, (xStart + 24 * i + 2).toInt(), y.toInt() + 2) + if (mouseX > xStart + 24 * i + && mouseX < xStart + 24 * (i + 1) + && mouseY > y + && mouseY < y + 16 + ) { + tooltipToDisplay.add(subCategory.name) + if (Mouse.getEventButtonState() && selectedSubCategory != subCategory.id) { + selectedSubCategory = subCategory.id + Utils.playPressSound() + } + } + } + } else { + selectedSubCategory = "" + } + + // Render family information + var catData = selectedCategory.familyData + Utils.renderAlignedString( + EnumChatFormatting.RED.toString() + "Families Found:", + (if (catData.found == catData.total) "§6" else "§7") + "${catData.found}/${catData.total}", + guiLeft + 280F, + guiTop + 70F, + 110 + ) + if (catData.found == catData.total) { + instance.renderGoldBar(guiLeft + 280F, guiTop + 80F, 112F) + } else { + instance.renderBar(guiLeft + 280F, guiTop + 80F, 112F, catData.found / catData.total.toFloat()) + } + + Utils.renderAlignedString( + EnumChatFormatting.RED.toString() + "Families Completed:", + (if (catData.completed == catData.total) "§6" else "§7") + "${catData.completed}/${catData.total}", + guiLeft + 280F, + guiTop + 90F, + 110 + ) + if (catData.completed == catData.total) { + instance.renderGoldBar(guiLeft + 280F, guiTop + 100F, 112F) + } else { + instance.renderBar(guiLeft + 280F, guiTop + 100F, 112F, catData.completed / catData.total.toFloat()) + } + + // Render subcategory family information, if possible + if (selectedSubCategory != "") { + catData = selectedCategory.subCategories.find { it.id == selectedSubCategory }!!.familyData + Utils.renderAlignedString( + EnumChatFormatting.RED.toString() + "Families Found:", + (if (catData.found == catData.total) "§6" else "§7") + "${catData.found}/${catData.total}", + guiLeft + 280F, + guiTop + 120F, + 110 + ) + if (catData.found == catData.total) { + instance.renderGoldBar(guiLeft + 280F, guiTop + 130F, 112F) + } else { + instance.renderBar(guiLeft + 280F, guiTop + 130F, 112F, catData.found / catData.total.toFloat()) + } + + Utils.renderAlignedString( + EnumChatFormatting.RED.toString() + "Families Completed:", + (if (catData.completed == catData.total) "§6" else "§7") + "${catData.completed}/${catData.total}", + guiLeft + 280F, + guiTop + 140F, + 110 + ) + if (catData.completed == catData.total) { + instance.renderGoldBar(guiLeft + 280F, guiTop + 150F, 112F) + } else { + instance.renderBar(guiLeft + 280F, guiTop + 150F, 112F, catData.completed / catData.total.toFloat()) + } + } + + // Determine which mobs should be displayed + val mobs = if (selectedSubCategory != "") { + selectedCategory.subCategories.first { it.id == selectedSubCategory }.mobs + } else { + selectedCategory.mobs + } + + // Render the mob list + for ((i, mob) in mobs.withIndex()) { + val stack = mob.icon + val xIndex = i % mobListXCount + val yIndex = i / mobListXCount + val x = 23 + mobListXPadding + (mobListXPadding + 20) * xIndex + val y = 30 + mobListYPadding + (mobListYPadding + 20) * yIndex + + GlStateManager.disableLighting() + RenderHelper.enableGUIStandardItemLighting() + GlStateManager.color(1f, 1f, 1f, 1f) + Minecraft.getMinecraft().textureManager.bindTexture(GuiProfileViewer.pv_elements) + Utils.drawTexturedRect( + guiLeft + x, + guiTop + y, + 20f, + 20f, + 0f, + 20 / 256f, + 0f, + 20f / 256f, + GL11.GL_NEAREST + ) + Utils.drawItemStack(stack, guiLeft + x.toInt() + 2, guiTop + y.toInt() + 2) + val kills = mob.kills + val deaths = mob.deaths + + if (mouseX > guiLeft + x.toInt() + 2 && mouseX < guiLeft + x.toInt() + 18) { + if (mouseY > guiTop + y.toInt() + 2 && mouseY < guiTop + y.toInt() + 18) { + tooltipToDisplay = ArrayList() + tooltipToDisplay.add( + "${mob.name} ${mob.mobLevelData.level}" + ) + tooltipToDisplay.add( + EnumChatFormatting.GRAY.toString() + "Kills: " + EnumChatFormatting.GREEN + + StringUtils.formatNumber(kills) + ) + tooltipToDisplay.add( + EnumChatFormatting.GRAY.toString() + "Deaths: " + EnumChatFormatting.GREEN + + StringUtils.formatNumber(deaths) + ) + tooltipToDisplay.add("") + + if (!mob.mobLevelData.maxLevel) { + tooltipToDisplay.add( + EnumChatFormatting.GRAY.toString() + "Progress to Tier ${mob.mobLevelData.level + 1}: " + + EnumChatFormatting.AQUA + "${mob.mobLevelData.progress}%" + ) + + var bar = "§3§l§m" + for (j in 1..14) { + var col = "" + if (mob.mobLevelData.progress < j * (100 / 14)) col = "§f§l§m" + bar += "$col " + } + tooltipToDisplay.add( + "${bar}§r§b ${StringUtils.formatNumber(mob.mobLevelData.mobKillData.tierKills)}/${ + StringUtils.formatNumber( + mob.mobLevelData.mobKillData.tierReq + ) + }" + ) + tooltipToDisplay.add("") + } + + tooltipToDisplay.add( + EnumChatFormatting.GRAY.toString() + "Overall Progress: " + EnumChatFormatting.AQUA + "${mob.mobLevelData.totalProgress}%" + + if (mob.mobLevelData.maxLevel) " §7(§c§lMAX!§r§7)" else "" + ) + var bar = "§3§l§m" + for (j in 1..14) { + var col = "" + if (mob.mobLevelData.totalProgress < j * (100 / 14)) col = "§f§l§m" + bar += "$col " + } + tooltipToDisplay.add( + "${bar}§r§b ${StringUtils.formatNumber(mob.mobLevelData.mobKillData.cappedKills)}/${ + StringUtils.formatNumber( + mob.mobLevelData.mobKillData.cap + ) + }" + ) + } + } + GlStateManager.color(1f, 1f, 1f, 1f) + Utils.drawStringCentered( + if (mob.mobLevelData.maxLevel) { + "${EnumChatFormatting.GOLD}${mob.mobLevelData.level}" + } else { + mob.mobLevelData.level.toString() + }, guiLeft + x + 10, guiTop + y + 26, true, color.rgb + ) + } + + // Render the accumulated tooltip, if applicable + if (tooltipToDisplay.isNotEmpty()) { + val grayTooltip: MutableList<String> = ArrayList(tooltipToDisplay.size) + for (line in tooltipToDisplay) { + grayTooltip.add(EnumChatFormatting.GRAY.toString() + line) + } + Utils.drawHoveringText(grayTooltip, mouseX, mouseY, width, height, -1) + tooltipToDisplay.clear() + } + } +} |