/*
* Copyright (C) 2023-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.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): 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 if (bestiaryObject.has("migration")) {
bestiaryObject.get("migration").asBoolean
} else {
// The account is new and therefore already uses the new format
true
}
}
/**
* 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 {
if (!hasMigrated(profileInfo) || Constants.BESTIARY == null) {
return mutableListOf()
}
val parsedCategories = mutableListOf()
val apiKills = profileInfo.getAsJsonObject("bestiary")!!.getAsJsonObject("kills") ?: return mutableListOf()
val apiDeaths = profileInfo.getAsJsonObject("bestiary").getAsJsonObject("deaths") ?: return mutableListOf()
val killsMap: HashMap = HashMap()
for (entry in apiKills.entrySet()) {
if (entry.key == "last_killed_mob") {
continue
}
killsMap[entry.key] = entry.value.asString.toIntOrNull() ?: -1
}
val deathsMap: HashMap = HashMap()
for (entry in apiDeaths.entrySet()) {
deathsMap[entry.key] = entry.value.asString.toIntOrNull() ?: -1
}
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,
deathsMap: HashMap
): Category {
val categoryName = categoryData["name"].asString
val computedMobs: MutableList = 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 = 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): 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): 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)
}
}