/*
* Copyright (C) 2024 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
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import io.github.moulberry.notenoughupdates.NotEnoughUpdates
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.profileviewer.ProfileViewer.Level
import io.github.moulberry.notenoughupdates.profileviewer.ProfileViewerUtils
import io.github.moulberry.notenoughupdates.profileviewer.SkyblockProfiles
import io.github.moulberry.notenoughupdates.profileviewer.data.APIDataJson
import io.github.moulberry.notenoughupdates.util.Constants
import io.github.moulberry.notenoughupdates.util.MC
import io.github.moulberry.notenoughupdates.util.UrsaClient
import io.github.moulberry.notenoughupdates.util.Utils
import io.github.moulberry.notenoughupdates.util.kotlin.Coroutines
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.GlStateManager
import net.minecraft.init.Blocks
import net.minecraft.item.ItemStack
import net.minecraft.util.ResourceLocation
import org.lwjgl.opengl.GL11
class GardenPage(pvInstance: GuiProfileViewer) : GuiProfileViewerPage(pvInstance) {
private val manager get() = NotEnoughUpdates.INSTANCE.manager
private var guiLeft = GuiProfileViewer.getGuiLeft()
private var guiTop = GuiProfileViewer.getGuiTop()
private var currentProfile: SkyblockProfiles.SkyblockProfile? = null
private var gardenData: GardenData? = null
private var eliteData: EliteWeightJson? = null
private var currentlyFetching = false
private lateinit var repoData: GardenRepoJson
private var apiData: APIDataJson? = null
private var mouseX: Int = 0
private var mouseY: Int = 0
private val visitorRarityToVisits: MutableMap = mutableMapOf()
private val visitorRarityToCompleted: MutableMap = mutableMapOf()
val background: ResourceLocation = ResourceLocation("notenoughupdates:profile_viewer/garden/background.png")
companion object {
private val cropTypeAdapter = object : TypeAdapter() {
override fun write(writer: JsonWriter, value: CropType) {
writer.value(value.name)
}
override fun read(reader: JsonReader): CropType? {
return CropType.fromApiName(reader.nextString())
}
}
val gson: Gson =
GsonBuilder().setPrettyPrinting().registerTypeAdapter(CropType::class.java, cropTypeAdapter.nullSafe())
.create()
}
override fun drawPage(mouseX: Int, mouseY: Int, partialTicks: Float) {
guiLeft = GuiProfileViewer.getGuiLeft()
guiTop = GuiProfileViewer.getGuiTop()
this.mouseX = mouseX
this.mouseY = mouseY
if (currentlyFetching) {
Utils.drawStringCentered("§eLoading Data", guiLeft + 220, guiTop + 101, true, 0)
return
}
if (Constants.GARDEN == null) {
Utils.drawStringCentered("§cMissing Repo Data", guiLeft + 220, guiTop + 101, true, 0)
Utils.showOutdatedRepoNotification("garden.json")
return
}
val newProfile = selectedProfile
if (newProfile != currentProfile) {
getData()
currentProfile = selectedProfile
return
}
if (gardenData == null || gardenData?.gardenExperience == 0) {
Utils.drawStringCentered("§cMissing Profile Data", guiLeft + 220, guiTop + 101, true, 0)
return
}
val selectedProfile = GuiProfileViewer.getSelectedProfile() ?: return
apiData = selectedProfile.APIDataJson ?: return
MC.textureManager.bindTexture(background)
Utils.drawTexturedRect(
guiLeft.toFloat(),
guiTop.toFloat(),
instance.sizeX.toFloat(),
instance.sizeY.toFloat(),
GL11.GL_NEAREST
)
renderPlots()
renderGardenLevel()
renderFarmingWeight()
renderCropUpgrades()
renderCropMilestones()
renderVisitorStats()
renderCompost()
}
private fun getData() {
currentlyFetching = true
val profileId = selectedProfile?.outerProfileJson?.get("profile_id")?.asString?.replace("-", "")
Coroutines.launchCoroutine {
gardenData = loadGardenData(profileId)
getVisitorData()
currentlyFetching = false
}
Coroutines.launchCoroutine {
eliteData = loadFarmingWeight(GuiProfileViewer.getProfile()?.uuid, profileId)
}
}
private fun loadGardenData(profileId: String?): GardenData? {
profileId ?: return null
val data = manager.ursaClient.get(UrsaClient.gardenForProfile(profileId)).get()
repoData = gson.fromJson(Constants.GARDEN, GardenRepoJson::class.java)
return gson.fromJson(data, GardenDataJson::class.java).garden
}
private fun getVisitorData() {
visitorRarityToVisits.clear()
visitorRarityToCompleted.clear()
for ((visitor, amount) in gardenData?.commissionData?.visits ?: return) {
val rarity = repoData.visitors[visitor]
if (rarity == null) {
println("Unknown visitor: $visitor")
continue
}
visitorRarityToVisits[rarity] = visitorRarityToVisits.getOrDefault(rarity, 0) + amount
visitorRarityToVisits[VisitorRarity.TOTAL] =
visitorRarityToVisits.getOrDefault(VisitorRarity.TOTAL, 0) + amount
}
for ((visitor, amount) in gardenData?.commissionData?.completed ?: return) {
val rarity = repoData.visitors[visitor] ?: continue
visitorRarityToCompleted[rarity] = visitorRarityToCompleted.getOrDefault(rarity, 0) + amount
visitorRarityToCompleted[VisitorRarity.TOTAL] =
visitorRarityToCompleted.getOrDefault(VisitorRarity.TOTAL, 0) + amount
}
}
private fun loadFarmingWeight(uuid: String?, profileId: String?): EliteWeightJson? {
uuid ?: return null
profileId ?: return null
val data = manager.apiUtils.request()
.url("https://api.elitebot.dev/weight/$uuid/$profileId")
.requestJson().get()
return gson.fromJson(data, EliteWeightJson::class.java)
}
private fun renderPlots() {
val top = guiTop + 79
val left = guiLeft + 192
GlStateManager.color(1f, 1f, 1f, 1f)
for (value in repoData.plots) {
Minecraft.getMinecraft().textureManager.bindTexture(GuiProfileViewer.pv_elements)
val x = left + value.value.x * 22
val y = top + value.value.y * 22
Utils.drawTexturedRect(
x.toFloat(),
y.toFloat(),
20f,
20f,
0f,
20 / 256f,
0f,
20 / 256f,
GL11.GL_NEAREST
)
if (gardenData?.unlockedPlotIds?.contains(value.key) != true) {
Utils.drawItemStack(ItemStack(Blocks.barrier), x + 2, y + 2)
if (mouseX >= x && mouseX <= x + 20 && mouseY >= y && mouseY <= y + 20) {
instance.tooltipToDisplay = listOf("§cLocked " + value.value.name)
}
continue
}
Utils.drawItemStack(ItemStack(Blocks.grass), x + 2, y + 2)
if (mouseX >= x && mouseX <= x + 20 && mouseY >= y && mouseY <= y + 20) {
instance.tooltipToDisplay = listOf(value.value.name)
}
}
Minecraft.getMinecraft().textureManager.bindTexture(GuiProfileViewer.pv_elements)
Utils.drawTexturedRect(
(left + 2 * 22).toFloat(),
(top + 2 * 22).toFloat(),
20f,
20f,
0f,
20 / 256f,
0f,
20 / 256f,
GL11.GL_NEAREST
)
val x = left + 2 * 22 + 2
val y = top + 2 * 22 + 2
var error = true
repoData.barn[gardenData?.selectedBarnSkin]?.let {
val itemStack = NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery().withKnownInternalName(it.item)
.resolveToItemStack()
Utils.drawItemStack(itemStack, x, y)
if (mouseX >= x && mouseX <= x + 20 && mouseY >= y && mouseY <= y + 20) {
instance.tooltipToDisplay =
listOf("§7Barn Skin: ${it.name}")
}
error = false
}
if (error) {
Utils.drawItemStack(ItemStack(Blocks.barrier), x, y)
if (mouseX >= x && mouseX <= x + 20 && mouseY >= y && mouseY <= y + 20) {
instance.tooltipToDisplay = listOf(
"§cUnknown barn Skin: ${gardenData?.selectedBarnSkin}",
"§cIf you expected it to be there please send a message in",
"§c§l#neu-support §r§con §ldiscord.gg/moulberry"
)
}
}
}
private fun renderCropUpgrades() {
val startHeight = guiTop + 105
var yPos = startHeight
var xPos = guiLeft + 26
Utils.renderShadowedString("§eCrop Upgrades", xPos + 70, yPos + 5, 105)
var averageUpgrade = 0
for ((index, crop) in CropType.values().withIndex()) {
if (index == 5) {
yPos = startHeight
xPos += 70
}
yPos += 14
val upgradeLevel = gardenData?.cropUpgradeLevels?.get(crop) ?: 0
averageUpgrade += upgradeLevel
val itemStack = manager.createItem(crop.itemId)
Utils.drawItemStack(itemStack, xPos + 2, yPos)
Utils.renderAlignedString(
"§e${crop.displayName}",
"§f$upgradeLevel",
(xPos + 20).toFloat(),
(yPos + 5).toFloat(),
50
)
if (mouseX >= xPos + 20 && mouseX <= xPos + 70 && mouseY >= yPos && mouseY <= yPos + 20) {
val tooltip = ArrayList()
tooltip.add("§a${crop.displayName}")
tooltip.add("")
if (repoData.cropUpgrades.size == upgradeLevel) {
tooltip.add("§7Current Tier: §a$upgradeLevel§7/§a${repoData.cropUpgrades.size}")
} else {
tooltip.add("§7Current Tier: §e$upgradeLevel§7/§a${repoData.cropUpgrades.size}")
}
tooltip.add("§7${crop.displayName} Fortune: §6+${upgradeLevel*5}☘")
tooltip.add("")
if (repoData.cropUpgrades.size == upgradeLevel) {
tooltip.add("§6Maxed")
} else {
tooltip.add("§7Cost:")
tooltip.add("§c${repoData.cropUpgrades[upgradeLevel]} §7Copper to Upgrade")
val totalCopper = repoData.cropUpgrades.sum()
val sum = totalCopper - repoData.cropUpgrades.subList(0, upgradeLevel).sum()
tooltip.add("§c$sum §7Copper to Max")
}
instance.tooltipToDisplay = tooltip
}
}
val x = guiLeft + 70 + 26 - 42
val y = startHeight + 5
if (mouseX in x..(x + 80) && mouseY in y..(y + 13)) {
instance.tooltipToDisplay = listOf("§eAverage Upgrade ${averageUpgrade/10}")
}
}
private fun renderCropMilestones() {
val startHeight = guiTop + 10
var yPos = startHeight
var xPos = guiLeft + 26
Utils.renderShadowedString("§eCrop Milestones", xPos + 70, yPos + 5, 105)
var averageMilestone = 0
for ((index, crop) in CropType.values().withIndex()) {
if (index == 5) {
yPos = startHeight
xPos += 70
}
yPos += 14
val itemStack = manager.createItem(crop.itemId)
Utils.drawItemStack(itemStack, xPos + 2, yPos)
val levelsInfo = repoData.cropMilestones[crop] ?: continue
val currentCollection = gardenData?.resourcesCollected?.get(crop) ?: 0
val levelInfo = getLevel(levelsInfo, currentCollection)
val collectionLevel = levelInfo.level.toInt()
val formattedAmount = StringUtils.formatNumber(currentCollection.toDouble())
var nextLevel = 0
var nextLevelString = "§6MAXED"
var maxLevel = 0
var maxLevelString = ""
var formattedPercentage = "100.00"
var formattedMaxLevelPercentage = "100.00"
var aboveMaxMilestoneNumber: String
var lastCropMilestoneAmountRequired = 0
for (i in 0..45) {
maxLevel += levelsInfo[i]
if (i < collectionLevel + 1) nextLevel += levelsInfo[i]
if (i == 45) lastCropMilestoneAmountRequired = levelsInfo[i]
}
if (!levelInfo.maxed) {
maxLevelString = StringUtils.formatNumber(maxLevel)
val remainingForNext = levelsInfo[collectionLevel] - (nextLevel - currentCollection)
val formattedRemainingForNext = StringUtils.formatNumber(remainingForNext.toDouble())
nextLevelString =
"§e$formattedRemainingForNext§6/§e${StringUtils.formatNumber(levelsInfo[collectionLevel].toDouble())}"
val percentage = (remainingForNext / levelsInfo[collectionLevel].toDouble()) * 100
formattedPercentage = String.format("%.2f", percentage)
val maxLevelPercentage = (currentCollection.toFloat() / maxLevel.toFloat()) * 100
formattedMaxLevelPercentage = String.format("%.2f", maxLevelPercentage)
}
val tooltip = ArrayList()
tooltip.add("§a${crop.displayName} $collectionLevel")
tooltip.add("§7Total: §a$formattedAmount")
tooltip.add("")
if (!levelInfo.maxed) {
tooltip.add("Progress to Tier " + (levelInfo.level.toInt() + 1) + ": §e$formattedPercentage%")
tooltip.add(nextLevelString)
tooltip.add("")
tooltip.add("Progress to Tier 46: §e$formattedMaxLevelPercentage%")
tooltip.add("§e$formattedAmount§6/§e$maxLevelString")
} else {
val aboveMaxMilestone =
(currentCollection.toDouble() - maxLevel.toFloat()) + lastCropMilestoneAmountRequired
aboveMaxMilestoneNumber = StringUtils.formatNumber(aboveMaxMilestone)
tooltip.add("§7Overflow: §6$aboveMaxMilestoneNumber")
tooltip.add("")
tooltip.add("§6Max tier reached!")
}
drawAlignedStringWithHover(
"§e${crop.displayName}",
"§f$collectionLevel",
xPos + 20,
yPos + 5,
50,
tooltip
)
averageMilestone += collectionLevel
}
val x = guiLeft + 70 + 26 - 42
val y = startHeight + 5
if (mouseX in x..(x + 80) && mouseY in y..(y + 13)) {
instance.tooltipToDisplay = listOf("§eAverage Milestone ${averageMilestone/10}")
}
}
private fun renderVisitorStats() {
val xPos = guiLeft + 322
var yPos = guiTop + 17
Utils.renderShadowedString("§eVisitors", xPos + 40, yPos - 2, 80)
// todo progress bar!
Utils.renderAlignedString(
"§eUnique Visitors",
"§f${gardenData?.commissionData?.uniqueNpcsServed ?: 0}/${repoData.visitors.size}",
xPos.toFloat(),
(yPos + 10).toFloat(),
80
)
yPos += 20
for (rarity in VisitorRarity.values()) {
val formattedVisits = StringUtils.formatNumber(visitorRarityToVisits.getOrDefault(rarity, 0))
val formattedCompleted = StringUtils.formatNumber(visitorRarityToCompleted.getOrDefault(rarity, 0))
val tooltip = listOf(
"§7Visits: §f$formattedVisits",
"§7Completed: §f$formattedCompleted",
)
val rarityStats = "§f$formattedCompleted/$formattedVisits"
drawAlignedStringWithHover(rarity.displayName, rarityStats, xPos, yPos, 80, tooltip)
yPos += 12
}
}
private fun renderGardenLevel() {
val top = guiTop + 20
val left = guiLeft + 190
val level = getLevel(repoData.gardenExperience, gardenData?.gardenExperience?.toLong())
if (level.maxed) {
instance.renderGoldBar((left).toFloat() + 16, (top + 10).toFloat(), 80f)
} else {
instance.renderBar(left.toFloat() + 16, (top + 10).toFloat(), 80f, level.level % 1)
}
val maxXp = level.maxXpForLevel.toInt()
val totalXpS = StringUtils.formatNumber(level.totalXp.toLong())
val gardenTooltip = ArrayList()
gardenTooltip.add("§2Garden")
if (level.maxed) {
gardenTooltip.add("§7Progress: §6MAXED!")
} else {
gardenTooltip.add(
"§7Progress: §5" +
StringUtils.shortNumberFormat(Math.round((level.level % 1) * maxXp)) +
"/" +
StringUtils.shortNumberFormat(maxXp)
)
}
gardenTooltip.add(
"§7Total XP: §5${totalXpS}§8 (" +
StringUtils.formatToTenths(instance.getPercentage("garden", level)) +
"% to ${level.maxLevel})"
)
drawAlignedStringWithHover("§2Garden", "§f${level.level.toInt()}", left + 36, top, 60, gardenTooltip)
Utils.drawItemStack(ItemStack(Blocks.grass), left + 16, top - 6)
val copper = apiData?.garden_player_data?.copper ?: 0
Utils.renderAlignedString(
"§cCopper",
"§f" + StringUtils.formatNumber(copper),
(left + 16).toFloat(),
(top + 20).toFloat(),
80
)
}
private fun renderFarmingWeight() {
val top = guiTop + 51
val left = guiLeft + 190
if (eliteData == null) {
drawAlignedStringWithHover(
"§eFarming Weight",
"§eLoading...",
left + 16,
top,
95,
listOf("§eLoading...", "§eTry again soon!")
)
return
}
val totalWeight = eliteData?.totalWeight ?: 0.0
val bonusWeight = eliteData?.bonusWeight?.values?.sum() ?: 0.0
val tooltip = buildList{
add("§7Total Weight: §f${StringUtils.formatNumber(totalWeight)}")
add("§7Bonus Weight: §f${StringUtils.formatNumber(bonusWeight)}")
for (crop in CropType.values()) {
val cropWeight = eliteData?.cropWeight?.get(crop) ?: 0.0
add("§7${crop.displayName}: §f${StringUtils.formatNumber(cropWeight)}")
}
add("")
add("Data provided by the Elitebot API.")
add("§eClick to view on the Elitebot website.")
}
drawAlignedStringWithHover(
"§eFarming Weight",
"§f${StringUtils.formatNumber(totalWeight.toInt())}",
left + 11,
top,
90,
tooltip
)
}
override fun mouseClicked(mouseX: Int, mouseY: Int, mouseButton: Int): Boolean {
if (mouseButton == 0) {
if (mouseX in (guiLeft + 190)..(guiLeft + 290) && mouseY in (guiTop + 51)..(guiTop + 60)) {
openWebsite()
}
}
return false
}
private fun renderCompost() {
val xPos = guiLeft + 322
var yPos = guiTop + 122
Utils.renderShadowedString("§eCompost Upgrades", xPos + 40, yPos - 2, 80)
yPos += 12
val (speed, multiDrop, fuelCap, organicMatterCap, costReduction) = gardenData?.composterData?.upgrades ?: return
for (i in 0..4) {
val upgradeName = when (i) {
0 -> "§aSpeed"
1 -> "§aMulti Drop"
2 -> "§aFuel Cap"
3 -> "§aOrganic Matter Cap"
4 -> "§aCost Reduction"
else -> 0
}
val upgradeAmount = when (i) {
0 -> speed
1 -> multiDrop
2 -> fuelCap
3 -> organicMatterCap
4 -> costReduction
else -> 0
}
val repoName = when (i) {
0 -> "speed"
1 -> "multi_drop"
2 -> "fuel_cap"
3 -> "organic_matter_cap"
4 -> "cost_reduction"
else -> 0
}
val tooltip = ArrayList()
val upgradeValues = repoData.composterUpgrades[repoName]?.get(upgradeAmount + 1)
val upgradeValuesCurrent = repoData.composterUpgrades[repoName]?.get(upgradeAmount)?.upgrade ?: 0
val upgradeValuesCurrentSt = StringUtils.formatNumber(upgradeValuesCurrent)
tooltip.add("$upgradeName $upgradeAmount")
if (upgradeValues != null) {
repoData.composterTooltips[repoName]?.replace("{}", "$upgradeValuesCurrentSt -> ${StringUtils.formatNumber(upgradeValues.upgrade)}")
?.let { tooltip.add(it) }
tooltip.add("")
tooltip.add("§7Cost:")
for (item in upgradeValues.items) {
val itemStack = manager.createItem(item.key.uppercase())
if (itemStack == null) {
println("Item not found: ${item.key}")
tooltip.add("§cUnknown Item: ${item.key}")
} else {
tooltip.add("§7${item.value}x ${itemStack.displayName}")
}
}
tooltip.add("§7${upgradeValues.copper} §cCopper")
} else {
repoData.composterTooltips[repoName]?.replace("{}", upgradeValuesCurrentSt)
?.let { tooltip.add(it) }
tooltip.add("§6Maxed")
}
drawAlignedStringWithHover("§e$upgradeName", "§f$upgradeAmount", xPos, yPos, 80, tooltip)
yPos += 12
}
}
private fun getLevel(experienceList: List, currentExp: Long?): Level {
val array = JsonArray()
experienceList.forEach { array.add(gson.toJsonTree(it)) }
return ProfileViewerUtils.getLevel(array, (currentExp ?: 0).toFloat(), experienceList.size, false)
}
private fun openWebsite() {
if (eliteData == null) return
Utils.openUrl("https://elitebot.dev/@" + GuiProfileViewer.getDisplayName() + "/" + GuiProfileViewer.getProfileName())
Utils.playPressSound()
}
private fun drawAlignedStringWithHover(
first: String,
second: String,
x: Int,
y: Int,
length: Int,
hover: List,
) {
Utils.renderAlignedString(first, second, x.toFloat(), y.toFloat(), length)
if (mouseX in x..(x + length) && mouseY in y..(y + 13)) {
instance.tooltipToDisplay = hover
}
}
}