/*
* 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 .
*/
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,
val subCategories: List,
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 = mutableListOf()
private var bestiaryLevel = 0.0
private var computedCategories: MutableList = 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 = ArrayList(tooltipToDisplay.size)
for (line in tooltipToDisplay) {
grayTooltip.add(EnumChatFormatting.GRAY.toString() + line)
}
Utils.drawHoveringText(grayTooltip, mouseX, mouseY, width, height, -1)
tooltipToDisplay.clear()
}
}
}