summaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/features/skillprogress
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni/features/skillprogress')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillProgress.kt472
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillTooltip.kt85
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillType.kt36
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillUtil.kt140
4 files changed, 733 insertions, 0 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillProgress.kt b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillProgress.kt
new file mode 100644
index 000000000..01f60ac87
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillProgress.kt
@@ -0,0 +1,472 @@
+package at.hannibal2.skyhanni.features.skillprogress
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.api.SkillAPI
+import at.hannibal2.skyhanni.api.SkillAPI.activeSkill
+import at.hannibal2.skyhanni.api.SkillAPI.lastUpdate
+import at.hannibal2.skyhanni.api.SkillAPI.oldSkillInfoMap
+import at.hannibal2.skyhanni.api.SkillAPI.showDisplay
+import at.hannibal2.skyhanni.api.SkillAPI.skillXPInfoMap
+import at.hannibal2.skyhanni.config.features.skillprogress.SkillProgressConfig
+import at.hannibal2.skyhanni.events.ActionBarUpdateEvent
+import at.hannibal2.skyhanni.events.ConfigLoadEvent
+import at.hannibal2.skyhanni.events.GuiRenderEvent
+import at.hannibal2.skyhanni.events.LorenzTickEvent
+import at.hannibal2.skyhanni.events.ProfileJoinEvent
+import at.hannibal2.skyhanni.events.SkillOverflowLevelupEvent
+import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.XP_NEEDED_FOR_60
+import at.hannibal2.skyhanni.utils.ChatUtils.chat
+import at.hannibal2.skyhanni.utils.ConditionalUtils.onToggle
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators
+import at.hannibal2.skyhanni.utils.NumberUtil.interpolate
+import at.hannibal2.skyhanni.utils.NumberUtil.roundToPrecision
+import at.hannibal2.skyhanni.utils.Quad
+import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderables
+import at.hannibal2.skyhanni.utils.RenderUtils.renderStringsAndItems
+import at.hannibal2.skyhanni.utils.SimpleTimeMark
+import at.hannibal2.skyhanni.utils.SoundUtils
+import at.hannibal2.skyhanni.utils.SoundUtils.playSound
+import at.hannibal2.skyhanni.utils.SpecialColour
+import at.hannibal2.skyhanni.utils.TimeUnit
+import at.hannibal2.skyhanni.utils.TimeUtils.format
+import at.hannibal2.skyhanni.utils.renderables.Renderable
+import at.hannibal2.skyhanni.utils.renderables.Renderable.Companion.horizontalContainer
+import net.minecraftforge.fml.common.eventhandler.EventPriority
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.awt.Color
+import kotlin.math.ceil
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+
+object SkillProgress {
+
+ private val config get() = SkyHanniMod.feature.skillProgress
+ private val barConfig get() = config.skillProgressBarConfig
+ private val allSkillConfig get() = config.allSkillDisplayConfig
+ val etaConfig get() = config.skillETADisplayConfig
+ val customGoalConfig get() = config.customGoalConfig
+
+ private var skillExpPercentage = 0.0
+ private var display = emptyList<Renderable>()
+ private var allDisplay = emptyList<Renderable>()
+ private var etaDisplay = emptyList<Renderable>()
+ private var lastGainUpdate = SimpleTimeMark.farPast()
+ private var maxWidth = 0
+ var hideInActionBar = listOf<String>()
+
+ @SubscribeEvent
+ fun onRenderOverlay(event: GuiRenderEvent.GuiOverlayRenderEvent) {
+ if (!isEnabled()) return
+ if (display.isEmpty()) return
+
+ if (showDisplay) {
+ renderDisplay()
+
+ if (barConfig.enabled.get()) {
+ renderBar()
+ }
+ }
+
+ if (etaConfig.enabled.get()) {
+ config.etaPosition.renderRenderables(etaDisplay, posLabel = "Skill ETA")
+ }
+ }
+
+ @SubscribeEvent
+ fun onGuiRender(event: GuiRenderEvent) {
+ if (!isEnabled()) return
+ if (display.isEmpty()) return
+
+ if (allSkillConfig.enabled.get()) {
+ config.allSkillPosition.renderRenderables(allDisplay, posLabel = "All Skills Display")
+ }
+ }
+
+ private fun renderDisplay() {
+ when (val textAlignment = config.textAlignmentProperty.get()) {
+ SkillProgressConfig.TextAlignment.NONE -> {
+ config.displayPosition.renderStringsAndItems(listOf(display), posLabel = "Skill Progress")
+ }
+
+ SkillProgressConfig.TextAlignment.CENTERED,
+ SkillProgressConfig.TextAlignment.LEFT,
+ SkillProgressConfig.TextAlignment.RIGHT,
+ -> {
+ config.displayPosition.renderRenderables(
+ listOf(Renderable.fixedSizeLine(horizontalContainer(display, horizontalAlign = textAlignment.alignment), maxWidth)),
+ posLabel = "Skill Progress")
+ }
+
+ else -> {}
+ }
+ }
+
+ private fun renderBar() {
+ val progress = if (barConfig.useTexturedBar.get()) {
+ val factor = (skillExpPercentage.toFloat().coerceAtMost(1f)) * 182
+ maxWidth = 182
+ Renderable.progressBar(
+ percent = factor.toDouble(),
+ startColor = Color(SpecialColour.specialToChromaRGB(barConfig.barStartColor)),
+ texture = barConfig.texturedBar.usedTexture.get(),
+ useChroma = barConfig.useChroma.get())
+
+ } else {
+ maxWidth = barConfig.regularBar.width
+ Renderable.progressBar(
+ percent = skillExpPercentage,
+ startColor = Color(SpecialColour.specialToChromaRGB(barConfig.barStartColor)),
+ endColor = Color(SpecialColour.specialToChromaRGB(barConfig.barStartColor)),
+ width = maxWidth,
+ height = barConfig.regularBar.height,
+ useChroma = barConfig.useChroma.get())
+ }
+
+ config.barPosition.renderRenderables(listOf(progress), posLabel = "Skill Progress Bar")
+ }
+
+ @SubscribeEvent
+ fun onProfileJoin(event: ProfileJoinEvent) {
+ display = emptyList()
+ allDisplay = emptyList()
+ etaDisplay = emptyList()
+ skillExpPercentage = 0.0
+ }
+
+ @SubscribeEvent
+ fun onTick(event: LorenzTickEvent) {
+ if (!isEnabled()) return
+ if (lastUpdate.passedSince() > 3.seconds) showDisplay = config.alwaysShow.get()
+
+ if (event.repeatSeconds(1)) {
+ allDisplay = formatAllDisplay(drawAllDisplay())
+ etaDisplay = drawETADisplay()
+ }
+
+ if (event.repeatSeconds(2)) {
+ update()
+ updateSkillInfo()
+ }
+ }
+
+ @SubscribeEvent
+ fun onLevelUp(event: SkillOverflowLevelupEvent) {
+ if (!isEnabled()) return
+ if (!config.overflowConfig.enableInChat) return
+ val skillName = event.skill.displayName
+ val oldLevel = event.oldLevel
+ val newLevel = event.newLevel
+ val skill = SkillAPI.storage?.get(event.skill) ?: return
+ val goalReached = newLevel == skill.customGoalLevel && customGoalConfig.enableInChat
+
+ val rewards = buildList {
+ add(" §r§7§8+§b1 Flexing Point")
+ if (newLevel % 5 == 0)
+ add(" §r§7§8+§d50 SkyHanni User Luck")
+ }
+ val messages = listOf(
+ "§3§l▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬",
+ " §r§b§lSKILL LEVEL UP §3$skillName §8$oldLevel➜§3$newLevel",
+ if (goalReached)
+ listOf(
+ "",
+ " §r§d§lGOAL REACHED!",
+ "",
+ ).joinToString("\n") else
+ "",
+ " §r§a§lREWARDS",
+ rewards.joinToString("\n"),
+ "§3§l▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"
+ )
+
+ chat(messages.joinToString("\n"), false)
+
+ if (goalReached)
+ chat("§lYou have reached your goal level of §b§l${skill.customGoalLevel} §e§lin the §b§l$skillName §e§lskill!")
+
+ SoundUtils.createSound("random.levelup", 1f, 1f).playSound()
+ }
+
+ @SubscribeEvent
+ fun onConfigLoad(event: ConfigLoadEvent) {
+ onToggle(
+ config.enabled,
+ config.alwaysShow,
+ config.showActionLeft,
+ config.useIcon,
+ config.usePercentage,
+ config.useSkillName,
+ config.overflowConfig.enableInDisplay,
+ config.overflowConfig.enableInProgressBar,
+ config.overflowConfig.enableInEtaDisplay,
+ barConfig.enabled,
+ barConfig.useChroma,
+ barConfig.useTexturedBar,
+ allSkillConfig.enabled,
+ etaConfig.enabled
+ ) {
+ updateDisplay()
+ update()
+ }
+ }
+
+ @SubscribeEvent(priority = EventPriority.LOW)
+ fun onActionBar(event: ActionBarUpdateEvent) {
+ if (!config.hideInActionBar) return
+ if (event.isCanceled) return
+ var msg = event.actionBar
+ for (line in hideInActionBar) {
+ msg = msg.replace(Regex("\\s*" + Regex.escape(line)), "")
+ }
+ msg = msg.trim()
+
+ event.changeActionBar(msg)
+ }
+
+ fun updateDisplay() {
+ display = drawDisplay()
+ }
+
+ private fun update() {
+ lastGainUpdate = SimpleTimeMark.now()
+ skillXPInfoMap.forEach {
+ it.value.xpGainLast = it.value.xpGainHour
+ }
+ }
+
+ private fun formatAllDisplay(map: Map<SkillType, Renderable>): List<Renderable> {
+ val newList = mutableListOf<Renderable>()
+ if (map.isEmpty()) return newList
+ for (skillType in allSkillConfig.skillEntryList) {
+ map[skillType]?.let {
+ newList.add(it)
+ }
+ }
+ return newList
+ }
+
+ private fun drawAllDisplay() = buildMap {
+ val skillMap = SkillAPI.storage ?: return@buildMap
+ val sortedMap = SkillType.entries.filter { it.displayName.isNotEmpty() }.sortedBy { it.displayName.take(2) }
+
+ for (skill in sortedMap) {
+ val skillInfo = skillMap[skill] ?: SkillAPI.SkillInfo(level = -1, overflowLevel = -1)
+ val lockedLevels = skillInfo.overflowCurrentXp > skillInfo.overflowCurrentXpMax
+ val useCustomGoalLevel = skillInfo.customGoalLevel != 0 && skillInfo.customGoalLevel > skillInfo.overflowLevel && customGoalConfig.enableInAllDisplay
+ val targetLevel = skillInfo.customGoalLevel
+ var xp = skillInfo.overflowTotalXp
+ if (targetLevel in 50 .. 60 && skillInfo.overflowLevel >= 50) xp += SkillUtil.xpRequiredForLevel(50.0)
+ else if (targetLevel > 60 && skillInfo.overflowLevel >= 60) xp += SkillUtil.xpRequiredForLevel(60.0)
+
+ var have = skillInfo.overflowTotalXp
+ val need = SkillUtil.xpRequiredForLevel(targetLevel.toDouble())
+ if (targetLevel in 51 .. 59) have += SkillUtil.xpRequiredForLevel(50.0)
+ else if (targetLevel > 60) have += SkillUtil.xpRequiredForLevel(60.0)
+
+ val (level, currentXp, currentXpMax, totalXp) =
+ if (useCustomGoalLevel)
+ Quad(skillInfo.overflowLevel, have, need, xp)
+ else if (config.overflowConfig.enableInAllDisplay.get() && !lockedLevels)
+ Quad(skillInfo.overflowLevel, skillInfo.overflowCurrentXp, skillInfo.overflowCurrentXpMax, skillInfo.overflowTotalXp)
+ else
+ Quad(skillInfo.level, skillInfo.currentXp, skillInfo.currentXpMax, skillInfo.totalXp)
+
+ this[skill] = if (level == -1) {
+ Renderable.clickAndHover(
+ "§cOpen your skills menu !",
+ listOf("§eClick here to execute §6/skills"),
+ onClick = { LorenzUtils.sendCommandToServer("skills") }
+ )
+ } else {
+ val tips = buildList {
+ add("§6Level: §b${level}")
+ add("§6Current XP: §b${currentXp.addSeparators()}")
+ add("§6Needed XP: §b${currentXpMax.addSeparators()}")
+ add("§6Total XP: §b${totalXp.addSeparators()}")
+ }
+ val nameColor = if (skill == activeSkill) "§2" else "§a"
+ Renderable.hoverTips(buildString {
+ append("$nameColor${skill.displayName} $level ")
+ append("§7(")
+ append("§b${currentXp.addSeparators()}")
+ if (currentXpMax != 0L) {
+ append("§6/")
+ append("§b${currentXpMax.addSeparators()}")
+ }
+ append("§7)")
+ }, tips)
+ }
+ }
+ }
+
+ private fun drawETADisplay() = buildList {
+ val activeSkill = activeSkill ?: return@buildList
+ val skillInfo = SkillAPI.storage?.get(activeSkill) ?: return@buildList
+ val xpInfo = skillXPInfoMap[activeSkill] ?: return@buildList
+ val skillInfoLast = oldSkillInfoMap[activeSkill] ?: return@buildList
+ oldSkillInfoMap[activeSkill] = skillInfo
+ val level = if (config.overflowConfig.enableInEtaDisplay.get() || config.customGoalConfig.enableInETADisplay) skillInfo.overflowLevel else skillInfo.level
+
+ val useCustomGoalLevel = skillInfo.customGoalLevel != 0 && skillInfo.customGoalLevel > skillInfo.overflowLevel && customGoalConfig.enableInETADisplay
+ var targetLevel = if (useCustomGoalLevel) skillInfo.customGoalLevel else level + 1
+ if (targetLevel <= level || targetLevel > 400) targetLevel = (level + 1)
+ val currentLevelNeededXp = SkillUtil.xpRequiredForLevel(level.toDouble()) + skillInfo.overflowCurrentXp
+ val targetNeededXp = SkillUtil.xpRequiredForLevel(targetLevel.toDouble())
+ var remaining = if (useCustomGoalLevel) targetNeededXp - currentLevelNeededXp else skillInfo.overflowCurrentXpMax - skillInfo.overflowCurrentXp
+
+ if (!useCustomGoalLevel) {
+ if (skillInfo.overflowCurrentXpMax == skillInfoLast.overflowCurrentXpMax) {
+ remaining = interpolate(remaining.toFloat(), (skillInfoLast.overflowCurrentXpMax - skillInfoLast.overflowCurrentXp).toFloat(), lastGainUpdate.toMillis()).toLong()
+ }
+ }
+
+ add(Renderable.string("§6Skill: §a${activeSkill.displayName} §8$level➜§3$targetLevel"))
+
+ if (useCustomGoalLevel)
+ add(Renderable.string("§7Needed XP: §e${remaining.addSeparators()}"))
+
+ var xpInterp = xpInfo.xpGainHour
+ if (xpInfo.xpGainHour < 1000) {
+ add(Renderable.string("§7In §cN/A"))
+ } else {
+ val duration = ((remaining) * 1000 * 60 * 60 / xpInterp.toLong()).milliseconds
+ val format = duration.format(TimeUnit.DAY)
+ add(Renderable.string("§7In §b$format " +
+ if (xpInfo.isActive) "" else "§c(PAUSED)"))
+ }
+
+ if (xpInfo.xpGainLast == xpInfo.xpGainHour && xpInfo.xpGainHour <= 0) {
+ add(Renderable.string("§7XP/h: §cN/A"))
+ } else {
+ xpInterp = interpolate(xpInfo.xpGainHour, xpInfo.xpGainLast, lastGainUpdate.toMillis())
+ add(Renderable.string("§7XP/h: §e${xpInterp.toLong().addSeparators()} " +
+ if (xpInfo.isActive) "" else "§c(PAUSED)"))
+ }
+
+
+ val session = xpInfo.timeActive.seconds.format(TimeUnit.HOUR)
+ add(Renderable.clickAndHover("§7Session: §e$session ${if (xpInfo.sessionTimerActive) "" else "§c(PAUSED)"}",
+ listOf("§eClick to reset!")) {
+ xpInfo.sessionTimerActive = false
+ xpInfo.shouldStartTimer = true
+ xpInfo.timeActive = 0L
+ chat("Timer for §b${activeSkill.displayName} §ehas been reset!")
+ })
+ }
+
+ private fun drawDisplay() = buildList {
+ val activeSkill = activeSkill ?: return@buildList
+ val skillMap = SkillAPI.storage ?: return@buildList
+ val skill = skillMap[activeSkill] ?: return@buildList
+ val useCustomGoalLevel = skill.customGoalLevel != 0 && skill.customGoalLevel > skill.overflowLevel
+ val targetLevel = skill.customGoalLevel
+ val xp = skill.totalXp
+ val currentLevel = if (xp <= XP_NEEDED_FOR_60) {
+ SkillUtil.getLevel(xp)
+ } else {
+ SkillUtil.calculateOverFlow(xp).first
+ }
+ var have = skill.overflowTotalXp
+ val need = SkillUtil.xpRequiredForLevel(targetLevel.toDouble())
+ if (targetLevel in 51 .. 59) have += SkillUtil.xpRequiredForLevel(50.0)
+ else if (targetLevel > 60) have += SkillUtil.xpRequiredForLevel(60.0)
+
+
+ val (level, currentXp, currentXpMax, _) =
+ if (useCustomGoalLevel && customGoalConfig.enableInDisplay)
+ Quad(currentLevel, have, need, xp)
+ else if (config.overflowConfig.enableInDisplay.get())
+ Quad(skill.overflowLevel, skill.overflowCurrentXp, skill.overflowCurrentXpMax, skill.overflowTotalXp)
+ else
+ Quad(skill.level, skill.currentXp, skill.currentXpMax, skill.totalXp)
+
+ if (config.showLevel.get())
+ add(Renderable.string("§9[§d$level§9] "))
+
+ if (config.useIcon.get()) {
+ add(Renderable.itemStack(activeSkill.item, 1.2))
+ }
+
+ add(Renderable.string(buildString {
+ append("§b+${skill.lastGain} ")
+
+ if (config.useSkillName.get())
+ append("${activeSkill.displayName} ")
+
+ val (barCurrent, barMax) =
+ if (useCustomGoalLevel && customGoalConfig.enableInProgressBar)
+ Pair(have, need)
+ else if (config.overflowConfig.enableInProgressBar.get())
+ Pair(skill.overflowCurrentXp, skill.overflowCurrentXpMax)
+ else
+ Pair(skill.currentXp, skill.currentXpMax)
+
+ val barPercent = if (barMax == 0L) 100F else 100F * barCurrent / barMax
+ skillExpPercentage = (barPercent.toDouble() / 100)
+
+ val percent = if (currentXpMax == 0L) 100F else 100F * currentXp / currentXpMax
+
+ if (config.usePercentage.get())
+ append("§7(§6${percent.roundToPrecision(2)}%§7)")
+ else {
+ if (currentXpMax == 0L)
+ append("§7(§6${currentXp.addSeparators()}§7)")
+ else
+ append("§7(§6${currentXp.addSeparators()}§7/§6${currentXpMax.addSeparators()}§7)")
+ }
+
+ if (config.showActionLeft.get() && percent != 100f) {
+ append(" - ")
+ val gain = skill.lastGain.replace(",", "")
+ val actionLeft = (ceil(currentXpMax.toDouble() - currentXp) / gain.toDouble()).toLong().addSeparators()
+ if (skill.lastGain != "" && !actionLeft.contains("-")) {
+ append("§6$actionLeft Left")
+ } else {
+ append("§6∞ Left")
+ }
+ }
+ }))
+ }
+
+ private fun updateSkillInfo() {
+ val activeSkill = activeSkill ?: return
+ val xpInfo = skillXPInfoMap.getOrPut(activeSkill) { SkillAPI.SkillXPInfo() }
+ val skillInfo = SkillAPI.storage?.get(activeSkill) ?: return
+ oldSkillInfoMap[activeSkill] = skillInfo
+
+ val totalXp = skillInfo.currentXp
+
+ if (xpInfo.lastTotalXp > 0) {
+ val delta = totalXp - xpInfo.lastTotalXp
+ if (delta > 0 && delta < 1000) {
+ xpInfo.xpGainQueue.add(0, delta)
+
+ calculateXPHour(xpInfo)
+ } else if (xpInfo.timer > 0) {
+ xpInfo.timer--
+ xpInfo.xpGainQueue.add(0, 0f)
+
+ calculateXPHour(xpInfo)
+ } else if (delta <= 0) {
+ xpInfo.isActive = false
+ }
+ }
+ xpInfo.lastTotalXp = totalXp.toFloat()
+ }
+
+ private fun calculateXPHour(xpInfo: SkillAPI.SkillXPInfo) {
+ while (xpInfo.xpGainQueue.size > 30) {
+ xpInfo.xpGainQueue.removeLast()
+ }
+
+ var totalGain = 0f
+ for (f in xpInfo.xpGainQueue) totalGain += f
+
+ xpInfo.xpGainHour = totalGain * (60 * 60) / xpInfo.xpGainQueue.size
+
+ xpInfo.isActive = true
+ }
+
+ private fun isEnabled() = LorenzUtils.inSkyBlock && config.enabled.get()
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillTooltip.kt b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillTooltip.kt
new file mode 100644
index 000000000..c09100cbd
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillTooltip.kt
@@ -0,0 +1,85 @@
+package at.hannibal2.skyhanni.features.skillprogress
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.api.SkillAPI
+import at.hannibal2.skyhanni.events.LorenzToolTipEvent
+import at.hannibal2.skyhanni.utils.InventoryUtils
+import at.hannibal2.skyhanni.utils.ItemUtils.cleanName
+import at.hannibal2.skyhanni.utils.ItemUtils.getLore
+import at.hannibal2.skyhanni.utils.ItemUtils.name
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators
+import at.hannibal2.skyhanni.utils.NumberUtil.roundToPrecision
+import at.hannibal2.skyhanni.utils.NumberUtil.toRoman
+import at.hannibal2.skyhanni.utils.StringUtils
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+class SkillTooltip {
+
+ private val config get() = SkyHanniMod.feature.skillProgress
+ private val overflowConfig get() = config.overflowConfig
+ private val customGoalConfig get() = config.customGoalConfig
+
+ @SubscribeEvent
+ fun onTooltip(event: LorenzToolTipEvent) {
+ if (!LorenzUtils.inSkyBlock) return
+ val inventoryName = InventoryUtils.openInventoryName()
+ val stack = event.itemStack
+ if (inventoryName == "Your Skills" && stack.getLore().any { it.contains("Click to view!") }) {
+ val iterator = event.toolTip.listIterator()
+ val split = stack.cleanName().split(" ")
+ val skillName = split.first()
+ val skill = SkillType.getByNameOrNull(skillName) ?: return
+ val useRoman = split.last().toIntOrNull() == null
+ val skillInfo = SkillAPI.storage?.get(skill) ?: return
+ val showCustomGoal = skillInfo.customGoalLevel != 0 && customGoalConfig.enableInSkillMenuTooltip
+ var next = false
+ for (line in iterator) {
+ val maxReached = "§7§8Max Skill level reached!"
+ if (line.contains(maxReached) && overflowConfig.enableInSkillMenuTooltip) {
+ val progress = (skillInfo.overflowCurrentXp.toDouble() / skillInfo.overflowCurrentXpMax) * 100
+ val percent = "§e${progress.roundToPrecision(1)}%"
+ val currentLevel = skillInfo.overflowLevel
+
+ val level = if (useRoman) currentLevel.toRoman() else currentLevel
+ val nextLevel = if (useRoman) (currentLevel + 1).toRoman() else currentLevel + 1
+ iterator.set("§7Progress to Level $nextLevel: $percent")
+
+ event.itemStack.name = "§a${skill.displayName} $level"
+ next = true
+ continue
+ }
+ val bar = " "
+ if (next && overflowConfig.enableInSkillMenuTooltip) {
+ if (line.contains(bar)) {
+ val progress = (skillInfo.overflowCurrentXp.toDouble() / skillInfo.overflowCurrentXpMax)
+ val progressBar = StringUtils.progressBar(progress)
+ iterator.set("$progressBar §e${skillInfo.overflowCurrentXp.addSeparators()}§6/§e${skillInfo.overflowCurrentXpMax.addSeparators()}")
+ iterator.add("")
+ }
+ }
+ if ((line.contains(bar) || line.contains("/")) && showCustomGoal) {
+ val targetLevel = skillInfo.customGoalLevel
+ var have = skillInfo.overflowTotalXp
+ val need = SkillUtil.xpRequiredForLevel(targetLevel.toDouble())
+ if (targetLevel in 50 .. 60 && skillInfo.overflowLevel >= 50) have += SkillUtil.xpRequiredForLevel(50.0)
+ else if (targetLevel > 60 && skillInfo.overflowLevel >= 60) have += SkillUtil.xpRequiredForLevel(60.0)
+ val progress = have.toDouble() / need
+ val progressBar = StringUtils.progressBar(progress)
+ val nextLevel = if (useRoman) targetLevel.toRoman() else targetLevel
+ val percent = "§e${(progress * 100).roundToPrecision(1)}%"
+ iterator.add("")
+ iterator.add("§7Progress to Level $nextLevel: $percent")
+ iterator.add("$progressBar §e${have.addSeparators()}§6/§e${need.addSeparators()}")
+ iterator.add("")
+ }
+ if (next && overflowConfig.enableInSkillMenuTooltip) {
+ if (line.contains(bar)) {
+ iterator.add("§b§lOVERFLOW XP:")
+ iterator.add("§7▸ ${skillInfo.overflowTotalXp.addSeparators()}")
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillType.kt b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillType.kt
new file mode 100644
index 000000000..bd44a63f3
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillType.kt
@@ -0,0 +1,36 @@
+package at.hannibal2.skyhanni.features.skillprogress
+
+import io.github.moulberry.notenoughupdates.util.Utils
+import net.minecraft.block.Block
+import net.minecraft.init.Blocks
+import net.minecraft.init.Items
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+
+enum class SkillType(val displayName: String, icon: Item) {
+ COMBAT("Combat", Items.golden_sword),
+ FARMING("Farming", Items.golden_hoe),
+ FISHING("Fishing", Items.fishing_rod),
+ MINING("Mining", Items.golden_pickaxe),
+ FORAGING("Foraging", Items.golden_axe),
+ ENCHANTING("Enchanting", Blocks.enchanting_table),
+ ALCHEMY("Alchemy", Items.brewing_stand),
+ CARPENTRY("Carpentry", Blocks.crafting_table),
+ TAMING("Taming", Items.spawn_egg),
+ ;
+
+ constructor(displayName: String, block: Block) : this(displayName, Item.getItemFromBlock(block))
+
+ val item: ItemStack by lazy { Utils.createItemStack(icon, displayName) }
+ val lowercaseName = displayName.lowercase()
+ val uppercaseName = displayName.uppercase()
+
+ override fun toString(): String = "§b$displayName"
+
+ companion object {
+ fun getByName(name: String) = getByNameOrNull(name) ?: error("Unknown Skill Type: '$name'")
+
+ fun getByNameOrNull(name: String) =
+ entries.firstOrNull { it.displayName.lowercase() == name.lowercase() }
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillUtil.kt b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillUtil.kt
new file mode 100644
index 000000000..54ea4bdc9
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillUtil.kt
@@ -0,0 +1,140 @@
+package at.hannibal2.skyhanni.features.skillprogress
+
+import at.hannibal2.skyhanni.api.SkillAPI
+import at.hannibal2.skyhanni.api.SkillAPI.activeSkill
+import at.hannibal2.skyhanni.api.SkillAPI.exactLevelingMap
+import at.hannibal2.skyhanni.api.SkillAPI.excludedSkills
+import at.hannibal2.skyhanni.api.SkillAPI.levelingMap
+import at.hannibal2.skyhanni.utils.Quad
+import com.google.common.base.Splitter
+import com.google.gson.JsonArray
+import io.github.moulberry.notenoughupdates.util.Constants
+import io.github.moulberry.notenoughupdates.util.Utils
+
+object SkillUtil {
+
+ val SPACE_SPLITTER = Splitter.on(" ").omitEmptyStrings().trimResults()
+ const val XP_NEEDED_FOR_60 = 111_672_425L
+
+ fun getSkillInfo(skill: SkillType): SkillAPI.SkillInfo? {
+ return SkillAPI.storage?.get(skill)
+ }
+
+ fun getSkillInfo(currentLevel: Int, currentXp: Long, neededXp: Long, totalXp: Long): Quad<Int, Long, Long, Long> {
+ return if (currentLevel == 50 && neededXp == 0L)
+ calculateOverFlow50(currentXp)
+ else if (currentLevel >= 60)
+ calculateOverFlow(currentXp)
+ else
+ Quad(currentLevel, currentXp, neededXp, totalXp)
+ }
+
+ /**
+ * @author Soopyboo32
+ */
+ fun calculateOverFlow(currentXp: Long): Quad<Int, Long, Long, Long> {
+ var xpCurrent = currentXp
+ var slope = 600000L
+ var xpForCurr = 7000000 + slope
+ var level = 60
+ var total = 0L
+ while (xpCurrent > xpForCurr) {
+ level++
+ xpCurrent -= xpForCurr
+ total += xpForCurr
+ xpForCurr += slope
+ if (level % 10 == 0) slope *= 2
+ }
+ total += xpCurrent
+ return Quad(level, xpCurrent, xpForCurr, total)
+ }
+
+ /**
+ * Calculate overflow starting at level 50
+ */
+ private fun calculateOverFlow50(currentXp: Long): Quad<Int, Long, Long, Long> {
+ var xpCurrent = currentXp
+ var level = 50
+ var total = 0L
+ var slope = 300000L
+ var xpForCurr = 4000000 + slope
+
+ while (level < 60 && xpCurrent > xpForCurr) {
+ level++
+ xpCurrent -= xpForCurr
+ total += xpForCurr
+ xpForCurr += slope
+ if (level % 10 == 0) slope *= 2
+ }
+
+ if (level >= 60) {
+ slope = 600000L
+ xpForCurr = 7000000 + slope
+ }
+
+ while (xpCurrent > xpForCurr) {
+ level++
+ xpCurrent -= xpForCurr
+ total += xpForCurr
+ xpForCurr += slope
+ if (level % 10 == 0) slope *= 2
+ }
+ total += xpCurrent
+
+ return Quad(level, xpCurrent, xpForCurr, total)
+ }
+
+ fun xpRequiredForLevel(levelWithProgress: Double): Long {
+ val level = levelWithProgress.toInt()
+ var slope = 600000L
+ var xpForCurr = 7000000 + slope
+ var totalXpRequired = 0L
+
+ for (i in 61 .. level) {
+ totalXpRequired += xpForCurr
+ xpForCurr += slope
+ if (i % 10 == 0) slope *= 2
+ }
+
+ val fractionalProgress = levelWithProgress - level
+ totalXpRequired += (xpForCurr * fractionalProgress).toLong()
+
+ val xp = if (level <= 60) {
+ levelingMap.filter { it.key < level }.values.sum().toLong()
+ } else {
+ totalXpRequired + levelingMap.values.sum()
+ }
+
+ return xp
+ }
+
+ fun getLevelExact(neededXp: Long): Int {
+ val defaultLevel = if (activeSkill in excludedSkills) 50 else 60
+ return exactLevelingMap.getOrDefault(neededXp.toInt(), defaultLevel)
+ }
+
+ fun getLevel(currentXp: Long): Int {
+ var level = 0
+ var remainingXp = currentXp
+ for ((i, v) in levelingMap) {
+ if (remainingXp >= v) {
+ remainingXp -= v
+ level++
+ }
+ }
+ return level
+ }
+
+ fun calculateLevelXp(levelingArray: JsonArray, level: Int): Double {
+ var totalXp = 0.0
+ for (i in 0 until level + 1) {
+ val xp = levelingArray[i].asDouble
+ totalXp += xp
+ }
+ return totalXp
+ }
+
+ fun levelArray(): JsonArray =
+ Utils.getElement(Constants.LEVELING, "leveling_xp").asJsonArray
+
+}