From 64425658ba203ab5b70774903ce7e5bcf54af0d3 Mon Sep 17 00:00:00 2001 From: David Cole <40234707+DavidArthurCole@users.noreply.github.com> Date: Fri, 13 Dec 2024 05:47:27 -0500 Subject: [PATCH] Feature: Hitman Statistics (#2991) Co-authored-by: Cal --- .../storage/ProfileSpecificStorage.java | 6 +- .../features/event/hoppity/HoppityAPI.kt | 13 +- .../features/event/hoppity/HoppityEggType.kt | 7 +- .../ChocolateFactoryDataLoader.kt | 12 + .../chocolatefactory/ChocolateFactoryStats.kt | 75 ++++-- .../chocolatefactory/hitman/HitmanAPI.kt | 215 ++++++++++++++++++ .../HitmanSlots.kt} | 5 +- .../hannibal2/skyhanni/utils/SkyBlockTime.kt | 12 +- .../skyhanni/utils/SkyblockSeason.kt | 26 +++ 9 files changed, 341 insertions(+), 30 deletions(-) create mode 100644 src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanAPI.kt rename src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/{ChocolateFactoryHitmanSlots.kt => hitman/HitmanSlots.kt} (98%) diff --git a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java index 3514b9e8991c..2221cc9c2532 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java +++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java @@ -222,7 +222,7 @@ public HotspotRabbitStorage(@Nullable Integer year) { public static class HitmanStatsStorage { @Expose @Nullable - public Integer availableEggs; + public Integer availableEggs = null; @Expose @Nullable @@ -231,6 +231,10 @@ public static class HitmanStatsStorage { @Expose @Nullable public SimpleTimeMark allSlotsCooldown = null; + + @Expose + @Nullable + public Integer purchasedSlots = null; } @Expose diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/HoppityAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/HoppityAPI.kt index 302ca6d0861b..f1e6b9e519a7 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/HoppityAPI.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/HoppityAPI.kt @@ -162,10 +162,15 @@ object HoppityAPI { // If there is a time since lastHoppityCallAccept, we can assume this is an abiphone call private fun getBoughtType(): HoppityEggType = if (lastHoppityCallAccept != null) BOUGHT_ABIPHONE else BOUGHT - fun isHoppityEvent() = (SkyblockSeason.currentSeason == SkyblockSeason.SPRING || SkyHanniMod.feature.dev.debug.alwaysHoppitys) - fun getEventEndMark(): SimpleTimeMark? = if (isHoppityEvent()) { - SkyBlockTime.fromSbYearAndMonth(SkyBlockTime.now().year, 3).asTimeMark() - } else null + fun isHoppityEvent() = (SkyblockSeason.SPRING.isSeason() || SkyHanniMod.feature.dev.debug.alwaysHoppitys) + + fun getEventEndMark(): SimpleTimeMark? = if (isHoppityEvent()) getEventEndMark(SkyBlockTime.now().year) else null + + fun getEventEndMark(year: Int) = + SkyBlockTime.fromSeason(year, SkyblockSeason.SUMMER, SkyblockSeason.SkyblockSeasonModifier.EARLY).asTimeMark() + + fun getEventStartMark(year: Int) = + SkyBlockTime.fromSeason(year, SkyblockSeason.SPRING, SkyblockSeason.SkyblockSeasonModifier.EARLY).asTimeMark() fun rarityByRabbit(rabbit: String): LorenzRarity? = hoppityRarities.firstOrNull { it.chatColorCode == rabbit.substring(0, 2) diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/HoppityEggType.kt b/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/HoppityEggType.kt index 9a187a6feb25..6d498633a009 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/HoppityEggType.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/HoppityEggType.kt @@ -20,7 +20,7 @@ enum class HoppityEggType( val resetsAt: Int, var lastResetDay: Int = -1, private var claimed: Boolean = false, - private val altDay: Boolean = false + val altDay: Boolean = false ) { BREAKFAST("Breakfast", "§6", 7), LUNCH("Lunch", "§9", 14), @@ -51,6 +51,10 @@ enum class HoppityEggType( return now.copy(day = now.day + daysToAdd, hour = resetsAt, minute = 0, second = 0).asTimeMark().timeUntil() } + fun nextTime(): SimpleTimeMark { + return SimpleTimeMark.now() + timeUntil() + } + fun markClaimed(mark: SimpleTimeMark? = null) { mealLastFound[this] = mark ?: SimpleTimeMark.now() claimed = true @@ -93,6 +97,7 @@ enum class HoppityEggType( } val resettingEntries = entries.filter { it.resetsAt != -1 } + val sortedResettingEntries = resettingEntries.sortedBy { it.resetsAt } fun allFound() = resettingEntries.forEach { it.markClaimed() } diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryDataLoader.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryDataLoader.kt index c1b5b497b3cc..6e71e1ec1505 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryDataLoader.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryDataLoader.kt @@ -208,6 +208,15 @@ object ChocolateFactoryDataLoader { "§7Available eggs: §a(?\\d+)", ) + /** + * REGEX-TEST: §7Purchased slots: §a28§7/§a28 + * REGEX-TEST: §7Purchased slots: §e0§7/§a22 + */ + private val hitmanPurchasedSlotsPattern by ChocolateFactoryAPI.patternGroup.pattern( + "hitman.purchasedslots", + "§7Purchased slots: §.(?\\d+)§7\\/§a\\d+", + ) + /** * REGEX-TEST: §7Slot cooldown: §a8m 6s */ @@ -451,6 +460,9 @@ object ChocolateFactoryDataLoader { val nextAllSlots = (SimpleTimeMark.now() + timeUntilAllSlots) profileStorage.hitmanStats.allSlotsCooldown = nextAllSlots } + hitmanPurchasedSlotsPattern.matchMatcher(line) { + profileStorage.hitmanStats.purchasedSlots = group("amount").formatInt() + } } } diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStats.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStats.kt index c1b5951ae202..02d6b50915a4 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStats.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStats.kt @@ -3,7 +3,11 @@ package at.hannibal2.skyhanni.features.inventory.chocolatefactory import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator import at.hannibal2.skyhanni.events.GuiRenderEvent import at.hannibal2.skyhanni.events.SecondPassedEvent +import at.hannibal2.skyhanni.features.event.hoppity.HoppityAPI import at.hannibal2.skyhanni.features.event.hoppity.HoppityEventSummary +import at.hannibal2.skyhanni.features.inventory.chocolatefactory.hitman.HitmanAPI.getHitmanTimeToAll +import at.hannibal2.skyhanni.features.inventory.chocolatefactory.hitman.HitmanAPI.getHitmanTimeToFull +import at.hannibal2.skyhanni.features.inventory.chocolatefactory.hitman.HitmanAPI.getOpenSlots import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule import at.hannibal2.skyhanni.utils.ClipboardUtils import at.hannibal2.skyhanni.utils.LorenzUtils @@ -16,6 +20,7 @@ import at.hannibal2.skyhanni.utils.renderables.Renderable import com.google.gson.JsonElement import com.google.gson.JsonPrimitive import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import kotlin.time.Duration @SkyHanniModule object ChocolateFactoryStats { @@ -40,6 +45,7 @@ object ChocolateFactoryStats { config.position.renderRenderables(display, posLabel = "Chocolate Factory Stats") } + @Suppress("LongMethod", "CyclomaticComplexMethod") fun updateDisplay() { val profileStorage = profileStorage ?: return @@ -76,7 +82,23 @@ object ChocolateFactoryStats { val upgradeAvailableAt = ChocolateAmount.CURRENT.formattedTimeUntilGoal(profileStorage.bestUpgradeCost) - val map = buildMap { + val hitmanStats = profileStorage.hitmanStats + val availableHitmanEggs = hitmanStats.availableEggs?.takeIf { it > 0 }?.toString() ?: "§7None" + val hitmanSingleSlotCd = hitmanStats.slotCooldown?.takeIf { it.isInFuture() }?.timeUntil()?.format() ?: "§aAll Ready" + val hitmanAllSlotsCd = hitmanStats.allSlotsCooldown?.takeIf { it.isInFuture() }?.timeUntil()?.format() ?: "§aAll Ready" + val openSlotsNow = hitmanStats.getOpenSlots() + val purchasedSlots = hitmanStats.purchasedSlots ?: 0 + + val (hitmanAllSlotsTime, allSlotsEventInhibited) = hitmanStats.getHitmanTimeToAll() + val hitmanAllClaimString = hitmanAllSlotsTime.takeIf { it > Duration.ZERO }?.format() ?: "§aAll Ready" + val hitmanAllClaimReady = "${if (allSlotsEventInhibited) "§c" else "§b"}$hitmanAllClaimString" + + val (hitmanFullTime, hitmanFullEventInhibited) = hitmanStats.getHitmanTimeToFull() + val hitmanFullString = if (openSlotsNow == 0) "§7Cooldown..." + else hitmanFullTime.takeIf { it > Duration.ZERO }?.format() ?: "§cFull Now" + val hitmanSlotsFull = "${if (hitmanFullEventInhibited) "§c" else "§b"}$hitmanFullString" + + val map = buildMap { put(ChocolateFactoryStat.HEADER, "§6§lChocolate Factory ${ChocolateFactoryAPI.currentPrestige.toRoman()}") val maxSuffix = if (ChocolateFactoryAPI.isMax()) { @@ -118,25 +140,36 @@ object ChocolateFactoryStats { "§eRaw Per Second: §6${profileStorage.rawChocPerSecond.addSeparators()}", ) - if (ChocolateFactoryAPI.isMaxPrestige()) { - val allTime = ChocolateAmount.ALL_TIME.chocolate() - val nextChocolateMilestone = ChocolateFactoryAPI.getNextMilestoneChocolate(allTime) - val amountUntilNextMilestone = nextChocolateMilestone - allTime - val maxMilestoneEstimate = ChocolateAmount.ALL_TIME.formattedTimeUntilGoal(nextChocolateMilestone) - - if (amountUntilNextMilestone >= 0) { - put(ChocolateFactoryStat.TIME_TO_PRESTIGE, "§eTime To Next Milestone: $maxMilestoneEstimate") - put( - ChocolateFactoryStat.CHOCOLATE_UNTIL_PRESTIGE, - "§eChocolate To Next Milestone: §6${amountUntilNextMilestone.addSeparators()}", - ) - } - } else { - put(ChocolateFactoryStat.TIME_TO_PRESTIGE, "§eTime To Prestige: $prestigeEstimate") - put(ChocolateFactoryStat.CHOCOLATE_UNTIL_PRESTIGE, "§eChocolate To Prestige: §6$chocolateUntilPrestige") + val allTime = ChocolateAmount.ALL_TIME.chocolate() + val nextChocolateMilestone = ChocolateFactoryAPI.getNextMilestoneChocolate(allTime) + val amountUntilNextMilestone = nextChocolateMilestone - allTime + val amountFormat = amountUntilNextMilestone.addSeparators() + val maxMilestoneEstimate = ChocolateAmount.ALL_TIME.formattedTimeUntilGoal(nextChocolateMilestone) + val prestigeData = when { + !ChocolateFactoryAPI.isMaxPrestige() -> mapOf( + ChocolateFactoryStat.TIME_TO_PRESTIGE to "§eTime To Prestige: $prestigeEstimate", + ChocolateFactoryStat.CHOCOLATE_UNTIL_PRESTIGE to "§eChocolate To Prestige: §6$chocolateUntilPrestige" + ) + amountUntilNextMilestone >= 0 -> mapOf( + ChocolateFactoryStat.TIME_TO_PRESTIGE to "§eTime To Next Milestone: $maxMilestoneEstimate", + ChocolateFactoryStat.CHOCOLATE_UNTIL_PRESTIGE to "§eChocolate To Next Milestone: §6$amountFormat" + ) + else -> emptyMap() } + putAll(prestigeData) put(ChocolateFactoryStat.TIME_TO_BEST_UPGRADE, "§eBest Upgrade: $upgradeAvailableAt") + + put(ChocolateFactoryStat.HITMAN_HEADER, "§c§lRabbit Hitman") + put(ChocolateFactoryStat.AVAILABLE_HITMAN_EGGS, "§eAvailable Hitman Eggs: §6$availableHitmanEggs") + put(ChocolateFactoryStat.OPEN_HITMAN_SLOTS, "§eOpen Hitman Slots: §6$openSlotsNow") + put(ChocolateFactoryStat.HITMAN_SLOT_COOLDOWN, "§eHitman Slot Cooldown: §b$hitmanSingleSlotCd") + put(ChocolateFactoryStat.HITMAN_ALL_SLOTS, "§eAll Hitman Slots Cooldown: §b$hitmanAllSlotsCd") + + if (HoppityAPI.isHoppityEvent()) { + put(ChocolateFactoryStat.HITMAN_FULL_SLOTS, "§eFull Hitman Slots: §b$hitmanSlotsFull") + put(ChocolateFactoryStat.HITMAN_28_SLOTS, "§e$purchasedSlots Hitman Claims: $hitmanAllClaimReady") + } } val text = config.statsDisplayList.filter { it.shouldDisplay() }.flatMap { map[it]?.split("\n").orEmpty() } @@ -199,9 +232,13 @@ object ChocolateFactoryStats { "§eBest Upgrade: §b 59m 4s", { ChocolateFactoryAPI.profileStorage?.bestUpgradeCost != 0L }, ), - AVAILABLE_HITMAN_EGGS("§eAvailable Hitman Eggs: §b3"), - HITMAN_SLOT_COOLDOWN("§Hitman Slot Cooldown: §b8m 6s"), + HITMAN_HEADER("§c§lRabbit Hitman"), + AVAILABLE_HITMAN_EGGS("§eAvailable Hitman Eggs: §63"), + OPEN_HITMAN_SLOTS("§eOpen Hitman Slots: §63"), + HITMAN_SLOT_COOLDOWN("§eHitman Slot Cooldown: §b8m 6s"), HITMAN_ALL_SLOTS("§eAll Hitman Slots Cooldown: §b8h 8m 6s"), + HITMAN_FULL_SLOTS("§eFull Hitman Slots: §b2h 10m"), + HITMAN_28_SLOTS("§e28 Hitman Claims: §b3h 20m"), ; override fun toString(): String { diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanAPI.kt new file mode 100644 index 000000000000..df957b6f4fb0 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanAPI.kt @@ -0,0 +1,215 @@ +package at.hannibal2.skyhanni.features.inventory.chocolatefactory.hitman + +import at.hannibal2.skyhanni.config.storage.ProfileSpecificStorage.ChocolateFactoryStorage.HitmanStatsStorage +import at.hannibal2.skyhanni.features.event.hoppity.HoppityAPI +import at.hannibal2.skyhanni.features.event.hoppity.HoppityAPI.isAlternateDay +import at.hannibal2.skyhanni.features.event.hoppity.HoppityEggType +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.SkyBlockTime +import at.hannibal2.skyhanni.utils.inPartialMinutes +import kotlin.math.ceil +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes + +@SkyHanniModule +object HitmanAPI { + + private const val MINUTES_PER_DAY = 20 // Real minutes per SkyBlock day + private const val SB_HR_PER_DAY = 24 // SkyBlock hours per day + private val sortedEntries get() = HoppityEggType.sortedResettingEntries + private val orderOrdinalMap: Map by lazy { + sortedEntries.mapIndexed { index, hoppityEggType -> + hoppityEggType to sortedEntries[(index + 1) % sortedEntries.size] + }.toMap() + } + + /** + * Determine if the given meal will 'still' be claimed before the given duration + */ + private fun HoppityEggType.willBeClaimableAfter(duration: Duration): Boolean = this.timeUntil() < duration + private fun HoppityEggType.passesNotClaimed(tilSpawnDuration: Duration, initialAvailable: MutableList) = + (initialAvailable.contains(this) || this.willBeClaimableAfter(tilSpawnDuration)) + + /** + * Get the time until the given number of slots are available. + */ + private fun HitmanStatsStorage.getTimeToNumSlots(numSlots: Int): Duration { + val currentSlots = this.getOpenSlots() + if (currentSlots >= numSlots) return Duration.ZERO + val slotCooldown = this.slotCooldown ?: return Duration.ZERO + val minutesUntilSlot = slotCooldown.timeUntil().inPartialMinutes + val minutesUntilSlots = minutesUntilSlot + ((numSlots - currentSlots - 1) * MINUTES_PER_DAY) + return minutesUntilSlots.minutes + } + + /** + * Return the number of extra slots that will be available after the given duration. + */ + private fun HitmanStatsStorage.extraSlotsInDuration(duration: Duration, setSlotNumber: Int? = null): Int { + val currentSlots = (setSlotNumber ?: this.getOpenSlots()).takeIf { it < ((this.purchasedSlots ?: 0)) } ?: return 0 + val slotCooldown = this.slotCooldown ?: return 0 + val minutesUntilSlot = slotCooldown.timeUntil().inPartialMinutes + if (minutesUntilSlot >= duration.inPartialMinutes) return 0 + for (i in 1..(this.purchasedSlots ?: 0)) { + // If the next slot would put us at the max slot count, return the number of slots + if (currentSlots + i == ((this.purchasedSlots ?: 0))) return i + val minutesUntilSlots = minutesUntilSlot + ((i - 1) * MINUTES_PER_DAY) + if (minutesUntilSlots >= duration.inPartialMinutes) return i + } + return 0 // Should never reach here + } + + /** + * Determine the first meal that would be hunted by Hitman if given an infinite amount of time. + */ + private fun getFirstHuntedMeal(): HoppityEggType = + sortedEntries.filter { !it.isClaimed() }.minByOrNull { it.timeUntil() } + ?: sortedEntries.minByOrNull { it.timeUntil() } + ?: ErrorManager.skyHanniError("Could not find initial meal to hunt") + + /** + * Determine the next meal that would be hunted by Hitman if given an infinite amount of time. + */ + private fun getNextHuntedMeal( + previousMeal: HoppityEggType, + duration: Duration, + initialAvailable: MutableList + ): HoppityEggType = sortedEntries + .filter { it.passesNotClaimed(duration, initialAvailable) } + .let { passingEggs -> + passingEggs.firstOrNull { it.resetsAt > previousMeal.resetsAt && it.altDay == previousMeal.altDay } + ?: passingEggs.firstOrNull { it.altDay != previousMeal.altDay } + ?: orderOrdinalMap[previousMeal] + ?: ErrorManager.skyHanniError("Could not find next meal to hunt after $previousMeal") + } + + /** + * Return the time until the given number of rabbits can be hunted. + */ + private fun HitmanStatsStorage.getTimeToHuntCount(targetHuntCount: Int): Duration { + // Store the initial meal to hunt + var nextHuntMeal = getFirstHuntedMeal() + // Store a list of all the meals that will be available to hunt at their next spawn + val initialAvailable = sortedEntries.filter { !it.isClaimed() && it != nextHuntMeal }.toMutableList() + + // Will store the total time until the given number of meals can be hunted + var tilSpawnDuration = + if (nextHuntMeal.isClaimed()) nextHuntMeal.timeUntil() + (MINUTES_PER_DAY * 2).minutes // -next- cycle after spawn + else nextHuntMeal.timeUntil() // Otherwise, just the time until the next spawn + + // Determine how many hunts we need to perform - 1 is added to account for the initial meal calculation above + val huntsToPerform = targetHuntCount - (1 + (this.availableEggs ?: 0)) + // Loop through the meals until the given number of meals can be hunted + repeat(huntsToPerform) { + // Determine the next meal to hunt + val candidate = getNextHuntedMeal(nextHuntMeal, tilSpawnDuration, initialAvailable) + + // If the meal was initially available, we don't need to wait for it to spawn + if (initialAvailable.contains(candidate)) initialAvailable.remove(candidate) + // Otherwise we add the time until the next spawn + else tilSpawnDuration += candidate.timeFromAnother(nextHuntMeal) + + // Cycle through + nextHuntMeal = candidate + } + + return tilSpawnDuration + } + + /** + * Return the duration between two HoppityEggTypes' spawn times. + */ + private fun HoppityEggType.timeFromAnother(another: HoppityEggType): Duration { + val diffInSbHours = when { + this == another -> (SB_HR_PER_DAY * 2) + this.altDay != another.altDay -> SB_HR_PER_DAY - another.resetsAt + this.resetsAt + this.resetsAt > another.resetsAt -> this.resetsAt - another.resetsAt + else -> (SB_HR_PER_DAY * 2) - (this.resetsAt - another.resetsAt) + } + return (diffInSbHours * SkyBlockTime.SKYBLOCK_HOUR_MILLIS).milliseconds + } + + /** + * Return the number of slots that are currently open. + * This has to be calculated based on the cooldown of all slots, + * as Hypixel doesn't directly expose this information in the `/cf` + * menu, and only gives cooldown timers... + */ + fun HitmanStatsStorage.getOpenSlots(): Int { + val allSlotsCooldown = this.allSlotsCooldown ?: return this.purchasedSlots ?: 0 + if (allSlotsCooldown.isInPast()) return this.purchasedSlots ?: 0 + + val minutesUntilAll = allSlotsCooldown.timeUntil().inPartialMinutes + val slotsOnCooldown = ceil(minutesUntilAll / MINUTES_PER_DAY).toInt() + return (this.purchasedSlots ?: 0) - slotsOnCooldown - (this.availableEggs ?: 0) + } + + /** + * Get the time until slots are full (or the event ends). + */ + fun HitmanStatsStorage.getHitmanTimeToFull(): Pair { + val slotsOpenNow = this.getOpenSlots() + val eventEndMark = HoppityAPI.getEventEndMark() ?: return Pair(Duration.ZERO, false) + + var slotsToFill = slotsOpenNow + for (i in (0..20)) { // Runaway protection + // Calculate time needed to fill this many slots + val timeToSlots = this.getTimeToHuntCount(slotsToFill) + + // If now plus the time to fill the slots is after the event end, we're done + if (SimpleTimeMark.now() + timeToSlots > eventEndMark) return Pair(eventEndMark.timeUntil(), true) + + // How many additional slots did we gain in that time? + val extraSlotsInTime = this.extraSlotsInDuration(timeToSlots, slotsToFill) + + // If we didn't get any extra slots, we're done + if (extraSlotsInTime == 0) return Pair(timeToSlots, false) + + slotsToFill += extraSlotsInTime + } + // Should never reach here + return Pair(Duration.ZERO, false) + } + + /** + * Get the time until ALL purchased slots are full (or the event ends). + * This is distinct from getHitmanTimeToFull() in that it forces the + * calculation to use the purchased slot count, not letting itself be + * inhibited by the cooldown "catching up" to spawn timers. + */ + fun HitmanStatsStorage.getHitmanTimeToAll(): Pair { + val eventEndMark = HoppityAPI.getEventEndMark() ?: return Pair(Duration.ZERO, false) + + val timeToSlots = this.getTimeToNumSlots(this.purchasedSlots ?: 0) + val timeToHunt = this.getTimeToHuntCount(this.purchasedSlots ?: 0) + + // Figure out which timer is the inhibitor + val longerTime = if (timeToSlots > timeToHunt) timeToSlots else timeToHunt + + // If the inhibitor is longer than the event end, return the time until the event ends + if ((SimpleTimeMark.now() + longerTime) > eventEndMark) return Pair(eventEndMark.timeUntil(), true) + + // If the spawns are the inhibitor, return the time until the spawns + if (timeToHunt > timeToSlots) return Pair(timeToHunt, false) + + // Otherwise if slots are the inhibitor, we need to find the next spawn time after the slots are full + val timeMarkAllSlots = SimpleTimeMark.now() + timeToSlots + val sbTimeAllSlots = timeMarkAllSlots.toSkyBlockTime() + val isAllSlotDayAlt = sbTimeAllSlots.isAlternateDay() + + // Find the first HoppityEggType that spawns after the slots are full + val nextMealAfterAllSlots = HoppityEggType.sortedResettingEntries.firstOrNull { + it.resetsAt > sbTimeAllSlots.hour && it.altDay == isAllSlotDayAlt + } ?: HoppityEggType.sortedResettingEntries.filter { + it.altDay != isAllSlotDayAlt + }.minByOrNull { it.resetsAt } ?: ErrorManager.skyHanniError("Could not find next meal after all slots") + + // Return the adjusted time until the next meal + val sbDayDiff = if (nextMealAfterAllSlots.altDay != isAllSlotDayAlt) 1 else 0 + val sbHourDiff = nextMealAfterAllSlots.resetsAt - sbTimeAllSlots.hour + sbDayDiff * SB_HR_PER_DAY + return Pair(timeToSlots + (sbHourDiff * SkyBlockTime.SKYBLOCK_HOUR_MILLIS).milliseconds, false) + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryHitmanSlots.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanSlots.kt similarity index 98% rename from src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryHitmanSlots.kt rename to src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanSlots.kt index ec3a88f636f6..825d513b51a0 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryHitmanSlots.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/hitman/HitmanSlots.kt @@ -1,4 +1,4 @@ -package at.hannibal2.skyhanni.features.inventory.chocolatefactory +package at.hannibal2.skyhanni.features.inventory.chocolatefactory.hitman import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.events.GuiRenderEvent @@ -10,6 +10,7 @@ import at.hannibal2.skyhanni.events.hoppity.RabbitFoundEvent import at.hannibal2.skyhanni.events.render.gui.ReplaceItemEvent import at.hannibal2.skyhanni.features.event.hoppity.HoppityAPI.hitmanInventoryPattern import at.hannibal2.skyhanni.features.event.hoppity.HoppityEggType +import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryAPI import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryStrayTracker.formLoreToSingleLine import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule import at.hannibal2.skyhanni.utils.InventoryUtils.isTopInventory @@ -24,7 +25,7 @@ import net.minecraft.item.ItemStack import net.minecraftforge.fml.common.eventhandler.SubscribeEvent @SkyHanniModule -object ChocolateFactoryHitmanSlots { +object HitmanSlots { // /** diff --git a/src/main/java/at/hannibal2/skyhanni/utils/SkyBlockTime.kt b/src/main/java/at/hannibal2/skyhanni/utils/SkyBlockTime.kt index 1c9f4316a653..4f49a64b829a 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/SkyBlockTime.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/SkyBlockTime.kt @@ -41,14 +41,15 @@ data class SkyBlockTime( fun fromSbYear(year: Int): SkyBlockTime = fromInstant(Instant.ofEpochMilli(SKYBLOCK_EPOCH_START_MILLIS + (SKYBLOCK_YEAR_MILLIS * year))) - fun fromSbYearAndMonth(year: Int, month: Int): SkyBlockTime = - fromInstant( + fun fromSeason(year: Int, season: SkyblockSeason, modifier: SkyblockSeason.SkyblockSeasonModifier? = null): SkyBlockTime { + return fromInstant( Instant.ofEpochMilli( SKYBLOCK_EPOCH_START_MILLIS + (SKYBLOCK_YEAR_MILLIS * year) + - (SKYBLOCK_MONTH_MILLIS * (month - 1)) + (SKYBLOCK_MONTH_MILLIS * (season.getMonth(modifier))) ) ) + } fun now(): SkyBlockTime = fromInstant(Instant.now()) @@ -117,6 +118,11 @@ data class SkyBlockTime( else -> "th" } } + + operator fun SkyBlockTime.plus(duration: kotlin.time.Duration): SkyBlockTime { + val millis = toMillis() + duration.inWholeMilliseconds + return fromInstant(Instant.ofEpochMilli(millis)) + } } } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/SkyblockSeason.kt b/src/main/java/at/hannibal2/skyhanni/utils/SkyblockSeason.kt index d74b80959bcb..8aac5b34dc6b 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/SkyblockSeason.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/SkyblockSeason.kt @@ -16,8 +16,34 @@ enum class SkyblockSeason( WINTER("§9Winter", "§a5%+§cC", "§7Visitors give §a5% §7more §cCopper."), ; + enum class SkyblockSeasonModifier( + val str: String, + ) { + EARLY("Early"), + NONE(""), + LATE("Late"), + ; + + override fun toString(): String = str + } + + fun isSeason(): Boolean = currentSeason == this fun getPerk(abbreviate: Boolean): String = if (abbreviate) abbreviatedPerk else perk fun getSeason(abbreviate: Boolean): String = if (abbreviate) season.take(4) else season + fun getDisplayMonth(modifier: SkyblockSeasonModifier? = null): Int = getMonth(modifier) + 1 + fun getMonth(modifier: SkyblockSeasonModifier? = null): Int = + when (this) { + SPRING -> 1 + SUMMER -> 4 + AUTUMN -> 7 + WINTER -> 10 + }.minus( + when (modifier) { + SkyblockSeasonModifier.EARLY -> 1 + SkyblockSeasonModifier.LATE -> -1 + else -> 0 + } + ) companion object {