diff --git a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt index c08595b5bf80..6dd8db8ad710 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt @@ -51,6 +51,7 @@ import at.hannibal2.skyhanni.features.garden.fortuneguide.FFGuideGUI import at.hannibal2.skyhanni.features.garden.pests.PestFinder import at.hannibal2.skyhanni.features.garden.pests.PestProfitTracker import at.hannibal2.skyhanni.features.garden.visitor.GardenVisitorDropStatistics +import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryStrayTracker import at.hannibal2.skyhanni.features.mining.KingTalismanHelper import at.hannibal2.skyhanni.features.mining.MineshaftPityDisplay import at.hannibal2.skyhanni.features.mining.powdertracker.PowderTracker @@ -272,6 +273,10 @@ object Commands { "shresetseacreaturetracker", "Resets the Sea Creature Tracker", ) { SeaCreatureTracker.resetCommand() } + registerCommand( + "shresetstrayrabbittracker", + "Resets the Stray Rabbit Tracker", + ) { ChocolateFactoryStrayTracker.resetCommand() } registerCommand( "shfandomwiki", "Searches the fandom wiki with SkyHanni's own method.", diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/inventory/chocolatefactory/ChocolateFactoryConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/inventory/chocolatefactory/ChocolateFactoryConfig.java index c381444df5b9..e35bc4d7e790 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/inventory/chocolatefactory/ChocolateFactoryConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/inventory/chocolatefactory/ChocolateFactoryConfig.java @@ -221,4 +221,14 @@ public class ChocolateFactoryConfig { @ConfigEditorBoolean @FeatureToggle public boolean mythicRabbitRequirement = false; + + @Expose + @ConfigOption(name = "Stray Tracker", desc = "Track stray rabbits found in the Chocolate Factory menu.") + @ConfigEditorBoolean + @FeatureToggle + public boolean strayRabbitTracker = true; + + @Expose + @ConfigLink(owner = ChocolateFactoryConfig.class, field = "strayRabbitTracker") + public Position strayRabbitTrackerPosition = new Position(300, 300, false, true); } 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 b49a9c612fca..3410d9b4de08 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java +++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java @@ -28,6 +28,7 @@ import at.hannibal2.skyhanni.features.garden.pests.PestProfitTracker; import at.hannibal2.skyhanni.features.garden.pests.VinylType; import at.hannibal2.skyhanni.features.garden.visitor.VisitorReward; +import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryStrayTracker; import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryUpgrade; import at.hannibal2.skyhanni.features.inventory.wardrobe.WardrobeAPI; import at.hannibal2.skyhanni.features.mining.MineshaftPityDisplay; @@ -151,6 +152,9 @@ public static class PositionChange { @Expose public Integer hoppityShopYearOpened = null; + + @Expose + public ChocolateFactoryStrayTracker.Data strayTracker = new ChocolateFactoryStrayTracker.Data(); } @Expose diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStrayTracker.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStrayTracker.kt new file mode 100644 index 000000000000..5dd90a82b47b --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/chocolatefactory/ChocolateFactoryStrayTracker.kt @@ -0,0 +1,315 @@ +package at.hannibal2.skyhanni.features.inventory.chocolatefactory + +import at.hannibal2.skyhanni.events.GuiContainerEvent +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent +import at.hannibal2.skyhanni.events.SecondPassedEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.utils.CollectionUtils.addAsSingletonList +import at.hannibal2.skyhanni.utils.CollectionUtils.addOrPut +import at.hannibal2.skyhanni.utils.DelayedRun +import at.hannibal2.skyhanni.utils.InventoryUtils +import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.ItemUtils.itemName +import at.hannibal2.skyhanni.utils.ItemUtils.name +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NumberUtil.formatLong +import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher +import at.hannibal2.skyhanni.utils.RegexUtils.matches +import at.hannibal2.skyhanni.utils.TimeUtils.format +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.tracker.SkyHanniTracker +import at.hannibal2.skyhanni.utils.tracker.TrackerData +import com.google.gson.annotations.Expose +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +@SkyHanniModule +object ChocolateFactoryStrayTracker { + + private val config get() = ChocolateFactoryAPI.config + private var claimedStraysSlots = mutableListOf() + + /** + * REGEX-TEST: §9Zero §d§lCAUGHT! + * REGEX-TEST: §6§lGolden Rabbit §d§lCAUGHT! + * REGEX-TEST: §fAudi §d§lCAUGHT! + */ + private val strayCaughtPattern by ChocolateFactoryAPI.patternGroup.pattern( + "stray.caught", + "^§[a-f0-9].* §d§lCAUGHT!", + ) + + /** + * REGEX-TEST: §7You caught a stray §fMandy §7and §7gained §6+283,574 Chocolate§7! + * REGEX-TEST: §7You caught a stray §aSven §7and gained §7§6+397,004 Chocolate§7!' + */ + private val strayLorePattern by ChocolateFactoryAPI.patternGroup.pattern( + "stray.loreinfo", + "§7You caught a stray (?(?§[^§]*)) .* (?:§7)?§6\\+(?[\\d,]*) Chocolate§7!", + ) + + /** + * REGEX-TEST: §7You caught a stray §6§lGolden Rabbit§7! §7You gained §6+13,566,571 Chocolate§7! + */ + private val goldenStrayJackpotMountainPattern by ChocolateFactoryAPI.patternGroup.pattern( + "stray.goldenrawchoc", + "§7You caught a stray §6§lGolden Rabbit§7! §7You gained §6\\+(?[\\d,]*) Chocolate§7!", + ) + + /** + * REGEX-TEST: §7You caught a stray §6§lGolden Rabbit§7! §7You caught a glimpse of §6El Dorado§7, §7but he escaped and left behind §7§6313,780 Chocolate§7! + */ + private val goldenStrayDoradoEscape by ChocolateFactoryAPI.patternGroup.pattern( + "stray.goldendoradoescape", + "§7You caught a stray §6§lGolden Rabbit§7! §7You caught a glimpse of §6El Dorado§7, §7but he escaped and left behind §7§6\\+(?[\\d,]*) Chocolate§7!", + ) + + /** + * REGEX-TEST: §7You caught a stray §6§lGolden Rabbit§7! §7You caught §6El Dorado §7- quite the elusive rabbit! + */ + private val goldenStrayDoradoCaught by ChocolateFactoryAPI.patternGroup.pattern( + "stray.goldendoradocaught", + "§7You caught a stray §6§lGolden Rabbit§7! §7You caught §6El Dorado §7- quite the elusive rabbit!", + ) + + /** + * REGEX-TEST: §7You caught a stray §6§lGolden Rabbit§7! §7You caught §6El Dorado§7! Since you §7already have captured him before, §7you gained §6+324,364,585 Chocolate§7. + */ + private val goldenStrayDoradoDuplicate by ChocolateFactoryAPI.patternGroup.pattern( + "stray.goldendoradoduplicate", + "§7You caught a stray §6§lGolden Rabbit§7! §7You caught §6El Dorado§7! Since you §7already have captured him before, §7you gained §6\\+(?[\\d,]*) Chocolate§7.", + ) + + /** + * REGEX-TEST: §7You caught a stray §6§lGolden Rabbit§7! §7You gained §6+5 Chocolate §7until the §7end of the SkyBlock year! + * + */ + private val goldenStrayClick by ChocolateFactoryAPI.patternGroup.pattern( + "stray.goldenclick", + "§7You caught a stray §6§lGolden Rabbit§7! §7You gained §6\\+5 Chocolate §7until the §7end of the SkyBlock year!", + ) + + /** + * REGEX-TEST: §7You caught a stray §9Fish the Rabbit§7! §7You have already found §9Fish the §9Rabbit§7, so you received §655,935,257 §6Chocolate§7! + */ + private val fishTheRabbitPattern by ChocolateFactoryAPI.patternGroup.pattern( + "stray.fish", + "§7You caught a stray (?§.)Fish the Rabbit§7! §7You have already found (?:§.)?Fish the (?:§.)?Rabbit§7, so you received §6(?[\\d,]*) (?:§6)?Chocolate§7!", + ) + + private val rarityFormatMap = mapOf( + "common" to "§f", + "uncommon" to "§a", + "rare" to "§9", + "epic" to "§5", + "legendary" to "§6", + ) + + private val tracker = SkyHanniTracker("Stray Tracker", { Data() }, { it.chocolateFactory.strayTracker }) + { drawDisplay(it) } + + class Data : TrackerData() { + override fun reset() { + straysCaught.clear() + straysExtraChocMs.clear() + goldenTypesCaught.clear() + } + + @Expose + var straysCaught: MutableMap = mutableMapOf() + + @Expose + var straysExtraChocMs: MutableMap = mutableMapOf() + + @Expose + var goldenTypesCaught: MutableMap = mutableMapOf() + } + + private fun formLoreToSingleLine(lore: List): String { + val notEmptyLines = lore.filter { it.isNotEmpty() } + return notEmptyLines.joinToString(" ") + } + + private fun incrementRarity(rarity: String, chocAmount: Long = 0) { + tracker.modify { it.straysCaught.addOrPut(rarity, 1) } + val extraTime = ChocolateFactoryAPI.timeUntilNeed(chocAmount + 1) + tracker.modify { it.straysExtraChocMs.addOrPut(rarity, extraTime.inWholeMilliseconds) } + } + + private fun incrementGoldenType(typeCaught: String, amount: Int = 1) { + tracker.modify { it.goldenTypesCaught.addOrPut(typeCaught, amount) } + } + + private fun drawDisplay(data: Data): List> = buildList { + val extraChocMs = data.straysExtraChocMs.values.sum().milliseconds + val formattedExtraTime = extraChocMs.let { if (it == 0.milliseconds) "0s" else it.format() } + + addAsSingletonList( + Renderable.hoverTips( + "§6§lStray Tracker", + tips = listOf("§a+§b${formattedExtraTime} §afrom strays§7"), + ), + ) + rarityFormatMap.keys.forEach { rarity -> + extractHoverableOfRarity(rarity, data)?.let { addAsSingletonList(it) } + } + } + + private fun extractHoverableOfRarity(rarity: String, data: Data): Renderable? { + val caughtOfRarity = data.straysCaught[rarity] + val caughtString = caughtOfRarity?.toString() ?: return null + + val rarityExtraChocMs = data.straysExtraChocMs[rarity]?.milliseconds + val extraChocFormat = rarityExtraChocMs?.format() ?: "" + + val colorCode = rarityFormatMap[rarity] ?: "" + val lineHeader = "$colorCode${rarity.substring(0, 1).uppercase()}${rarity.substring(1)}§7: §r$colorCode" + val lineFormat = "${lineHeader}${caughtString}" + + return rarityExtraChocMs?.let { + val tip = + "§a+§b$extraChocFormat §afrom $colorCode$rarity strays§7${if (rarity == "legendary") extractGoldenTypesCaught(data) else ""}" + Renderable.hoverTips(Renderable.string(lineFormat), tips = tip.split("\n")) + } ?: Renderable.string(lineFormat) + } + + private fun extractGoldenTypesCaught(data: Data): String { + val goldenList = mutableListOf() + data.goldenTypesCaught["sidedish"]?.let { + goldenList.add("§b$it §6Side Dish" + if (it > 1) "es" else "") + } + data.goldenTypesCaught["jackpot"]?.let { + goldenList.add("§b$it §6Chocolate Jackpot" + if (it > 1) "s" else "") + } + data.goldenTypesCaught["mountain"]?.let { + goldenList.add("§b$it §6Chocolate Mountain" + if (it > 1) "s" else "") + } + data.goldenTypesCaught["dorado"]?.let { + goldenList.add((if (it >= 3) "§a" else "§b") + "$it§7/§a3 §6El Dorado §7Sighting" + if (it > 1) "s" else "") + } + data.goldenTypesCaught["stampede"]?.let { + goldenList.add("§b$it §6Stampede" + if (it > 1) "s" else "") + } + data.goldenTypesCaught["goldenclick"]?.let { + goldenList.add("§b$it §6Golden Click" + if (it > 1) "s" else "") + } + return if (goldenList.size == 0) "" else ("\n" + goldenList.joinToString("\n")) + } + + @SubscribeEvent + fun onTick(event: SecondPassedEvent) { + if (!isEnabled()) return + InventoryUtils.getItemsInOpenChest().filter { + !claimedStraysSlots.contains(it.slotIndex) + }.forEach { + strayCaughtPattern.matchMatcher(it.stack.name) { + if (it.stack.getLore().isEmpty()) return + claimedStraysSlots.add(it.slotIndex) + val loreLine = formLoreToSingleLine(it.stack.getLore()) + + // "Base" strays - Common -> Epic, raw choc only reward. + strayLorePattern.matchMatcher(loreLine) { + //Pretty sure base strays max at Epic, but... + val rarity = rarityFormatMap.entries.find { e -> e.value == group("rabbit").substring(0, 2) }?.key ?: "common" + incrementRarity(rarity, group("amount").formatLong()) + } + + // Fish the Rabbit + fishTheRabbitPattern.matchMatcher(loreLine) { + //Also fairly sure that Fish maxes out at Rare, but... + val rarity = rarityFormatMap.entries.find { e -> e.value == group("color").substring(0, 2) }?.key ?: "common" + incrementRarity(rarity, group("amount").formatLong()) + } + + // Golden Strays, Jackpot and Mountain, raw choc only reward. + goldenStrayJackpotMountainPattern.matchMatcher(loreLine) { + val amount = group("amount").formatLong().also { am -> incrementRarity("legendary", am) } + val multiplier = amount / ChocolateFactoryAPI.chocolatePerSecond + when (multiplier) { + in 479.0..481.0 -> incrementGoldenType("jackpot") + in 1499.0..1501.0 -> incrementGoldenType("mountain") + } + } + + // Golden Strays, "Golden Click" + goldenStrayClick.matchMatcher(loreLine) { + incrementGoldenType("goldenclick") + } + + // Golden Strays, hoard/stampede + if (loreLine == "§7You caught a stray §6§lGolden Rabbit§7! §7A hoard of §aStray Rabbits §7has appeared!") { + incrementGoldenType("stampede") + } + + // Golden Strays, El Dorado "glimpse" - 1/3 before capture + goldenStrayDoradoEscape.matchMatcher(loreLine) { + incrementRarity("legendary", group("amount").formatLong()) + incrementGoldenType("dorado") + } + + // Golden Strays, El Dorado caught - 3/3 + if (goldenStrayDoradoCaught.matches(loreLine)) { + incrementRarity("legendary") + tracker.modify { t -> t.goldenTypesCaught["dorado"] = 3 } + } + + // Golden Strays, El Dorado (duplicate catch) + goldenStrayDoradoDuplicate.matchMatcher(loreLine) { + incrementRarity("legendary", group("amount").formatLong()) + tracker.modify { t -> + t.goldenTypesCaught["dorado"] = t.goldenTypesCaught["dorado"]?.plus(1) ?: 4 + } + } + } + } + InventoryUtils.getItemsInOpenChest().filter { + claimedStraysSlots.contains(it.slotIndex) + }.forEach { + if (!strayCaughtPattern.matches(it.stack.name)) { + claimedStraysSlots.removeAt(claimedStraysSlots.indexOf(it.slotIndex)) + } + } + } + + @SubscribeEvent(priority = EventPriority.HIGH) + fun onSlotClick(event: GuiContainerEvent.SlotClickEvent) { + if (!isEnabled()) return + val index = event.slot?.slotIndex ?: return + if (index == -999) return + if (claimedStraysSlots.contains(index)) return + + val clickedStack = InventoryUtils.getItemsInOpenChest() + .find { it.slotNumber == event.slot.slotNumber && it.hasStack } + ?.stack ?: return + val nameText = (if (clickedStack.hasDisplayName()) clickedStack.displayName else clickedStack.itemName) + if (!nameText.equals("§6§lGolden Rabbit §8- §aSide Dish")) return + + claimedStraysSlots.add(index) + incrementGoldenType("sidedish") + incrementRarity("legendary", 0) + DelayedRun.runDelayed(1.seconds) { + claimedStraysSlots.remove(claimedStraysSlots.indexOf(index)) + } + } + + @SubscribeEvent + fun onBackgroundDraw(event: GuiRenderEvent.ChestGuiOverlayRenderEvent) { + if (!isEnabled()) return + tracker.renderDisplay(config.strayRabbitTrackerPosition) + } + + @SubscribeEvent + fun onInventoryOpen(event: InventoryFullyOpenedEvent) { + if (!isEnabled()) return + tracker.firstUpdate() + } + + fun resetCommand() { + tracker.resetCommand() + } + + private fun isEnabled() = LorenzUtils.inSkyBlock && config.strayRabbitTracker && ChocolateFactoryAPI.inChocolateFactory +}