aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt14
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/event/ChocolateFactoryConfig.java90
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/event/EventConfig.java4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/event/HoppityEggsConfig.java39
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java11
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/HoppityEggLocationsJson.kt19
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryAPI.kt256
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryBarnManager.kt77
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryInventory.kt115
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryStats.kt79
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggLocator.kt240
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggType.kt53
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggsManager.kt123
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggsShared.kt57
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/InventoryUtils.kt4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/UtilsPatterns.kt4
16 files changed, 1184 insertions, 1 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
index d1ce70441..374f414b4 100644
--- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
+++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
@@ -123,6 +123,13 @@ import at.hannibal2.skyhanni.features.dungeon.DungeonTeammateOutlines
import at.hannibal2.skyhanni.features.dungeon.HighlightDungeonDeathmite
import at.hannibal2.skyhanni.features.dungeon.TerracottaPhase
import at.hannibal2.skyhanni.features.event.UniqueGiftingOpportunitiesFeatures
+import at.hannibal2.skyhanni.features.event.chocolatefactory.ChocolateFactoryAPI
+import at.hannibal2.skyhanni.features.event.chocolatefactory.ChocolateFactoryBarnManager
+import at.hannibal2.skyhanni.features.event.chocolatefactory.ChocolateFactoryInventory
+import at.hannibal2.skyhanni.features.event.chocolatefactory.ChocolateFactoryStats
+import at.hannibal2.skyhanni.features.event.chocolatefactory.HoppityEggLocator
+import at.hannibal2.skyhanni.features.event.chocolatefactory.HoppityEggsManager
+import at.hannibal2.skyhanni.features.event.chocolatefactory.HoppityEggsShared
import at.hannibal2.skyhanni.features.event.diana.AllBurrowsList
import at.hannibal2.skyhanni.features.event.diana.BurrowWarpHelper
import at.hannibal2.skyhanni.features.event.diana.DianaProfitTracker
@@ -550,6 +557,7 @@ class SkyHanniMod {
loadModule(PestAPI)
loadModule(MiningAPI)
loadModule(FossilExcavatorAPI)
+ loadModule(ChocolateFactoryAPI)
// features
loadModule(BazaarOrderHelper())
@@ -605,6 +613,12 @@ class SkyHanniMod {
loadModule(SkyblockXPInChat())
loadModule(AreaMiniBossFeatures())
loadModule(MobHighlight())
+ loadModule(ChocolateFactoryBarnManager)
+ loadModule(ChocolateFactoryInventory)
+ loadModule(HoppityEggsManager)
+ loadModule(HoppityEggLocator)
+ loadModule(HoppityEggsShared)
+ loadModule(ChocolateFactoryStats)
loadModule(SpawnTimers())
loadModule(MarkedPlayerManager())
loadModule(SlayerMiniBossFeatures())
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/event/ChocolateFactoryConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/event/ChocolateFactoryConfig.java
new file mode 100644
index 000000000..ad129e0e3
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/event/ChocolateFactoryConfig.java
@@ -0,0 +1,90 @@
+package at.hannibal2.skyhanni.config.features.event;
+
+import at.hannibal2.skyhanni.config.FeatureToggle;
+import at.hannibal2.skyhanni.config.core.config.Position;
+import at.hannibal2.skyhanni.features.event.chocolatefactory.ChocolateFactoryStats.ChocolateFactoryStat;
+import com.google.gson.annotations.Expose;
+import io.github.notenoughupdates.moulconfig.annotations.Accordion;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDraggableList;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorSlider;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigLink;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigOption;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class ChocolateFactoryConfig {
+
+ @Expose
+ @ConfigOption(name = "Hoppity Eggs", desc = "")
+ @Accordion
+ public HoppityEggsConfig hoppityEggs = new HoppityEggsConfig();
+
+ @Expose
+ @ConfigOption(name = "Chocolate Factory Features", desc = "Global toggle for all chocolate factory features.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public boolean enabled = true;
+
+ @Expose
+ @ConfigOption(name = "Chocolate Factory Stats", desc = "Show general info about your chocolate factory.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public boolean statsDisplay = true;
+
+ @Expose
+ @ConfigOption(
+ name = "Stats List",
+ desc = "Drag text to change what displays in the chocolate factory stats list and what order the text appears in."
+ )
+ @ConfigEditorDraggableList
+ public List<ChocolateFactoryStat> statsDisplayList = new ArrayList<>(Arrays.asList(
+ ChocolateFactoryStat.HEADER,
+ ChocolateFactoryStat.CURRENT,
+ ChocolateFactoryStat.THIS_PRESTIGE,
+ ChocolateFactoryStat.ALL_TIME,
+ ChocolateFactoryStat.EMPTY,
+ ChocolateFactoryStat.PER_SECOND,
+ ChocolateFactoryStat.PER_MINUTE,
+ ChocolateFactoryStat.PER_HOUR,
+ ChocolateFactoryStat.PER_DAY,
+ ChocolateFactoryStat.EMPTY_2,
+ ChocolateFactoryStat.MULTIPLIER,
+ ChocolateFactoryStat.BARN,
+ ChocolateFactoryStat.LEADERBOARD_POS
+ ));
+
+ @Expose
+ @ConfigOption(name = "Show Stack Sizes", desc = "Shows additional info as many items in the chocolate menu as the stack size.")
+ @ConfigEditorBoolean
+ public boolean showStackSizes = true;
+
+ @Expose
+ @ConfigOption(name = "Highlight Upgrades", desc = "Highlight any upgrades that you can afford.")
+ @ConfigEditorBoolean
+ public boolean highlightUpgrades = true;
+
+ @Expose
+ @ConfigOption(name = "Use Middle Click", desc = "Click on slots with middle click to speed up interactions.")
+ @ConfigEditorBoolean
+ public boolean useMiddleClick = true;
+
+ @Expose
+ @ConfigOption(name = "Rabbit Warning", desc = "Warn when the rabbit that needs to be clicked appears.")
+ @ConfigEditorBoolean
+ public boolean rabbitWarning = true;
+
+ @Expose
+ @ConfigOption(
+ name = "Rabbit Crush Threshold",
+ desc = "How close should you be to your barn capacity should you be before being warned about needing to upgrade it."
+ )
+ @ConfigEditorSlider(minValue = 3, maxValue = 20, minStep = 1)
+ public int barnCapacityThreshold = 6;
+
+ @Expose
+ @ConfigLink(owner = ChocolateFactoryConfig.class, field = "statsDisplay")
+ public Position position = new Position(183, 160, false, true);
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/event/EventConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/event/EventConfig.java
index 149c8c6a4..b68664d79 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/features/event/EventConfig.java
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/event/EventConfig.java
@@ -23,6 +23,10 @@ public class EventConfig {
@Expose
public WinterConfig winter = new WinterConfig();
+ @Expose
+ @Category(name = "Hoppity", desc = "Features for the Hoppity event and the chocolate factory.")
+ public ChocolateFactoryConfig chocolateFactory = new ChocolateFactoryConfig();
+
@ConfigOption(name = "City Project", desc = "")
@Accordion
@Expose
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/event/HoppityEggsConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/event/HoppityEggsConfig.java
new file mode 100644
index 000000000..f935cb699
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/event/HoppityEggsConfig.java
@@ -0,0 +1,39 @@
+package at.hannibal2.skyhanni.config.features.event;
+
+import at.hannibal2.skyhanni.config.FeatureToggle;
+import at.hannibal2.skyhanni.config.core.config.Position;
+import com.google.gson.annotations.Expose;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigLink;
+import io.github.notenoughupdates.moulconfig.annotations.ConfigOption;
+
+public class HoppityEggsConfig {
+
+ @Expose
+ @ConfigOption(name = "Hoppity Waypoints", desc = "Toggle guess waypoints for Hoppity's Hunt.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public boolean waypoints = true;
+
+ @Expose
+ @ConfigOption(name = "Show All Waypoints", desc = "Show all possible egg waypoints for the current lobby. §e" +
+ "Only works when you don't have an Egglocator in your inventory.")
+ @ConfigEditorBoolean
+ public boolean showAllWaypoints = false;
+
+ @Expose
+ @ConfigOption(name = "Show Claimed Eggs", desc = "Displays which eggs have been found in the last SkyBlock day.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public boolean showClaimedEggs = false;
+
+ @Expose
+ @ConfigOption(name = "Shared Hoppity Waypoints", desc = "Enable being able to share and receive egg waypoints in your lobby.")
+ @ConfigEditorBoolean
+ @FeatureToggle
+ public boolean sharedWaypoints = true;
+
+ @Expose
+ @ConfigLink(owner = HoppityEggsConfig.class, field = "showClaimedEggs")
+ public Position position = new Position(33, 72, 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 3f62f3a1b..b802b2281 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java
+++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java
@@ -49,6 +49,17 @@ public class ProfileSpecificStorage {
public String currentPet = "";
@Expose
+ public ChocolateFactoryStorage chocolateFactory = new ChocolateFactoryStorage();
+
+ public static class ChocolateFactoryStorage {
+ @Expose
+ public int currentRabbits = 0;
+
+ @Expose
+ public int maxRabbits = -1;
+ }
+
+ @Expose
public MaxwellPowerStorage maxwell = new MaxwellPowerStorage();
public static class MaxwellPowerStorage {
diff --git a/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/HoppityEggLocationsJson.kt b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/HoppityEggLocationsJson.kt
new file mode 100644
index 000000000..1aed17041
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/HoppityEggLocationsJson.kt
@@ -0,0 +1,19 @@
+package at.hannibal2.skyhanni.data.jsonobjects.repo
+
+import at.hannibal2.skyhanni.data.IslandType
+import at.hannibal2.skyhanni.utils.LorenzVec
+import com.google.gson.annotations.Expose
+
+data class HoppityEggLocationsJson(
+ @Expose val eggLocations: Map<IslandType, List<LorenzVec>>,
+ @Expose val rabbitSlots: Map<Int, Int>,
+ @Expose val otherUpgradeSlots: Set<Int>,
+ @Expose val noPickblockSlots: Set<Int>,
+ @Expose val barnIndex: Int,
+ @Expose val infoIndex: Int,
+ @Expose val productionInfoIndex: Int,
+ @Expose val prestigeIndex: Int,
+ @Expose val milestoneIndex: Int,
+ @Expose val leaderboardIndex: Int,
+ @Expose val maxRabbits: Int,
+)
diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryAPI.kt b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryAPI.kt
new file mode 100644
index 000000000..9f77ea0cf
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryAPI.kt
@@ -0,0 +1,256 @@
+package at.hannibal2.skyhanni.features.event.chocolatefactory
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.config.features.event.ChocolateFactoryConfig
+import at.hannibal2.skyhanni.config.storage.ProfileSpecificStorage.ChocolateFactoryStorage
+import at.hannibal2.skyhanni.data.ProfileStorageData
+import at.hannibal2.skyhanni.data.jsonobjects.repo.DisabledFeaturesJson
+import at.hannibal2.skyhanni.data.jsonobjects.repo.HoppityEggLocationsJson
+import at.hannibal2.skyhanni.events.InventoryCloseEvent
+import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent
+import at.hannibal2.skyhanni.events.InventoryUpdatedEvent
+import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
+import at.hannibal2.skyhanni.events.RepositoryReloadEvent
+import at.hannibal2.skyhanni.utils.CollectionUtils.nextAfter
+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.name
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.NumberUtil.formatDouble
+import at.hannibal2.skyhanni.utils.NumberUtil.formatInt
+import at.hannibal2.skyhanni.utils.NumberUtil.formatLong
+import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimal
+import at.hannibal2.skyhanni.utils.SkyblockSeason
+import at.hannibal2.skyhanni.utils.SoundUtils
+import at.hannibal2.skyhanni.utils.StringUtils.matchFirst
+import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
+import at.hannibal2.skyhanni.utils.StringUtils.matches
+import at.hannibal2.skyhanni.utils.StringUtils.removeColor
+import at.hannibal2.skyhanni.utils.UtilsPatterns
+import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern
+import net.minecraft.item.ItemStack
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+object ChocolateFactoryAPI {
+
+ val config: ChocolateFactoryConfig get() = SkyHanniMod.feature.event.chocolateFactory
+ val profileStorage: ChocolateFactoryStorage? get() = ProfileStorageData.profileSpecific?.chocolateFactory
+
+ val patternGroup = RepoPattern.group("misc.chocolatefactory")
+ private val chocolateAmountPattern by patternGroup.pattern(
+ "chocolate.amount",
+ "(?<amount>[\\d,]+) Chocolate"
+ )
+ private val chocolatePerSecondPattern by patternGroup.pattern(
+ "chocolate.persecond",
+ "§6(?<amount>[\\d.,]+) §8per second"
+ )
+ private val chocolateAllTimePattern by patternGroup.pattern(
+ "chocolate.alltime",
+ "§7All-time Chocolate: §6(?<amount>[\\d,]+)"
+ )
+ private val chocolateThisPrestigePattern by patternGroup.pattern(
+ "chocolate.thisprestige",
+ "§7Chocolate this Prestige: §6(?<amount>[\\d,]+)"
+ )
+ private val chocolateMultiplierPattern by patternGroup.pattern(
+ "chocolate.multiplier",
+ "§7Total Multiplier: §6(?<amount>[\\d.]+)x"
+ )
+ private val barnAmountPattern by patternGroup.pattern(
+ "barn.amount",
+ "§7Your Barn: §.(?<rabbits>\\d+)§7/§.(?<max>\\d+) Rabbits"
+ )
+ private val prestigeLevelPattern by patternGroup.pattern(
+ "prestige.level",
+ "'§6Chocolate Factory (?<prestige>[IVX]+)"
+ )
+ private val clickMeRabbitPattern by patternGroup.pattern(
+ "rabbit.clickme",
+ "§e§lCLICK ME!"
+ )
+ private val leaderboardPlacePattern by patternGroup.pattern(
+ "leaderboard.place",
+ "§7You are §8#§b(?<position>[\\d,]+)"
+ )
+
+ var rabbitSlots = mapOf<Int, Int>()
+ var otherUpgradeSlots = setOf<Int>()
+ var noPickblockSlots = setOf<Int>()
+ var barnIndex = 34
+ private var infoIndex = 13
+ private var productionInfoIndex = 45
+ private var prestigeIndex = 28
+ var milestoneIndex = 53
+ private var leaderboardIndex = 51
+ var maxRabbits = 395
+
+ var inChocolateFactory = false
+
+ var currentPrestige = 0
+ var chocolateCurrent = 0L
+ var chocolateAllTime = 0L
+ var chocolatePerSecond = 0.0
+ var chocolateThisPrestige = 0L
+ var chocolateMultiplier = 1.0
+ var leaderboardPosition: Int? = null
+
+ val upgradeableSlots: MutableSet<Int> = mutableSetOf()
+ var bestUpgrade: Int? = null
+ var bestRabbitUpgrade: String? = null
+ var clickRabbitSlot: Int? = null
+
+ @SubscribeEvent
+ fun onInventoryOpen(event: InventoryFullyOpenedEvent) {
+ if (!isEnabled()) return
+ if (event.inventoryName != "Chocolate Factory") return
+ inChocolateFactory = true
+
+ DelayedRun.runNextTick {
+ updateInventoryItems(event.inventoryItems)
+ }
+ }
+
+ @SubscribeEvent
+ fun onInventoryUpdated(event: InventoryUpdatedEvent) {
+ if (!inChocolateFactory) return
+
+ updateInventoryItems(event.inventoryItems)
+ }
+
+ private fun updateInventoryItems(inventory: Map<Int, ItemStack>) {
+ val profileStorage = profileStorage ?: return
+
+ val infoItem = InventoryUtils.getItemAtSlotIndex(infoIndex) ?: return
+ val prestigeItem = InventoryUtils.getItemAtSlotIndex(prestigeIndex) ?: return
+ val productionInfoItem = InventoryUtils.getItemAtSlotIndex(productionInfoIndex) ?: return
+ val leaderboardItem = InventoryUtils.getItemAtSlotIndex(leaderboardIndex) ?: return
+
+ processInfoItems(infoItem, prestigeItem, productionInfoItem, leaderboardItem)
+
+ bestUpgrade = null
+ upgradeableSlots.clear()
+ var bestAffordableUpgradeRatio = Double.MAX_VALUE
+ var bestPossibleUpgradeRatio = Double.MAX_VALUE
+ clickRabbitSlot = null
+
+ for ((slotIndex, item) in inventory) {
+ if (config.rabbitWarning && clickMeRabbitPattern.matches(item.name)) {
+ SoundUtils.playBeepSound()
+ clickRabbitSlot = slotIndex
+ }
+
+ val lore = item.getLore()
+ val upgradeCost = lore.getUpgradeCost() ?: continue
+
+ if (slotIndex == barnIndex) {
+ lore.matchFirst(barnAmountPattern) {
+ profileStorage.currentRabbits = group("rabbits").formatInt()
+ profileStorage.maxRabbits = group("max").formatInt()
+
+ ChocolateFactoryBarnManager.trySendBarnFullMessage()
+ }
+ }
+
+ val canAfford = upgradeCost <= chocolateCurrent
+ if (canAfford) upgradeableSlots.add(slotIndex)
+
+ if (slotIndex in rabbitSlots) {
+ val chocolateIncrease = rabbitSlots[slotIndex] ?: 0
+ val upgradeRatio = upgradeCost.toDouble() / chocolateIncrease
+
+ if (canAfford && upgradeRatio < bestAffordableUpgradeRatio) {
+ bestUpgrade = slotIndex
+ bestAffordableUpgradeRatio = upgradeRatio
+ }
+ if (upgradeRatio < bestPossibleUpgradeRatio) {
+ bestPossibleUpgradeRatio = upgradeRatio
+ bestRabbitUpgrade = item.name
+ }
+ }
+ }
+ }
+
+ private fun processInfoItems(
+ chocolateItem: ItemStack,
+ prestigeItem: ItemStack,
+ productionItem: ItemStack,
+ leaderboardItem: ItemStack,
+ ) {
+ chocolateAmountPattern.matchMatcher(chocolateItem.name.removeColor()) {
+ chocolateCurrent = group("amount").formatLong()
+ }
+ for (line in chocolateItem.getLore()) {
+ chocolatePerSecondPattern.matchMatcher(line) {
+ chocolatePerSecond = group("amount").formatDouble()
+ }
+ chocolateAllTimePattern.matchMatcher(line) {
+ chocolateAllTime = group("amount").formatLong()
+ }
+ }
+ prestigeLevelPattern.matchMatcher(prestigeItem.name) {
+ currentPrestige = group("prestige").romanToDecimal()
+ }
+ prestigeItem.getLore().matchFirst(chocolateThisPrestigePattern) {
+ chocolateThisPrestige = group("amount").formatLong()
+ }
+ productionItem.getLore().matchFirst(chocolateMultiplierPattern) {
+ chocolateMultiplier = group("amount").formatDouble()
+ }
+ leaderboardItem.getLore().matchFirst(leaderboardPlacePattern) {
+ leaderboardPosition = group("position").formatInt()
+ }
+ if (!config.statsDisplay) return
+ ChocolateFactoryStats.updateDisplay()
+ }
+
+ @SubscribeEvent
+ fun onWorldChange(event: LorenzWorldChangeEvent) {
+ clearData()
+ }
+
+ @SubscribeEvent
+ fun onInventoryClose(event: InventoryCloseEvent) {
+ clearData()
+ }
+
+ private fun clearData() {
+ inChocolateFactory = false
+ }
+
+ @SubscribeEvent
+ fun onRepoReload(event: RepositoryReloadEvent) {
+ val data = event.getConstant<HoppityEggLocationsJson>("HoppityEggLocations")
+
+ HoppityEggLocator.eggLocations = data.eggLocations
+
+ rabbitSlots = data.rabbitSlots
+ otherUpgradeSlots = data.otherUpgradeSlots
+ noPickblockSlots = data.noPickblockSlots
+ barnIndex = data.barnIndex
+ infoIndex = data.infoIndex
+ productionInfoIndex = data.productionInfoIndex
+ prestigeIndex = data.prestigeIndex
+ milestoneIndex = data.milestoneIndex
+ leaderboardIndex = data.leaderboardIndex
+ maxRabbits = data.maxRabbits
+
+ val disabledFeatures = event.getConstant<DisabledFeaturesJson>("DisabledFeatures")
+ HOPPITY_EVENT_DISABLED = disabledFeatures.features["HOPPITY_EVENT_DISABLED"] ?: false
+ }
+
+ private var HOPPITY_EVENT_DISABLED = false
+
+ private fun List<String>.getUpgradeCost(): Long? {
+ val nextLine = this.nextAfter({ UtilsPatterns.costLinePattern.matches(it) }) ?: return null
+ return chocolateAmountPattern.matchMatcher(nextLine.removeColor()) {
+ group("amount").formatLong()
+ }
+ }
+
+ fun isEnabled() = LorenzUtils.inSkyBlock && config.enabled
+
+ fun isHoppityEvent() = SkyblockSeason.getCurrentSeason() == SkyblockSeason.SPRING &&
+ (LorenzUtils.isOnAlphaServer || !HOPPITY_EVENT_DISABLED)
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryBarnManager.kt b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryBarnManager.kt
new file mode 100644
index 000000000..353e0dfe3
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryBarnManager.kt
@@ -0,0 +1,77 @@
+package at.hannibal2.skyhanni.features.event.chocolatefactory
+
+import at.hannibal2.skyhanni.events.LorenzChatEvent
+import at.hannibal2.skyhanni.utils.ChatUtils
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.SimpleTimeMark
+import at.hannibal2.skyhanni.utils.SoundUtils
+import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import kotlin.time.Duration.Companion.seconds
+
+object ChocolateFactoryBarnManager {
+
+ private val config get() = ChocolateFactoryAPI.config
+ private val profileStorage get() = ChocolateFactoryAPI.profileStorage
+
+ private val newRabbitPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "rabbit.new",
+ "§d§lNEW RABBIT! §6\\+\\d Chocolate §7and §6\\+0.\\d+x Chocolate §7per second!"
+ )
+ private val rabbitDuplicatePattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "rabbit.duplicate",
+ "§7§lDUPLICATE RABBIT! §6\\+[\\d,]+ Chocolate"
+ )
+
+ var barnFull = false
+ private var lastBarnFullWarning = SimpleTimeMark.farPast()
+
+ @SubscribeEvent
+ fun onChat(event: LorenzChatEvent) {
+ if (!LorenzUtils.inSkyBlock) return
+
+ newRabbitPattern.matchMatcher(event.message) {
+ val profileStorage = profileStorage ?: return
+ profileStorage.currentRabbits += 1
+ trySendBarnFullMessage()
+ HoppityEggsManager.shareWaypointPrompt()
+ }
+
+ rabbitDuplicatePattern.matchMatcher(event.message) {
+ HoppityEggsManager.shareWaypointPrompt()
+ }
+ }
+
+ fun trySendBarnFullMessage() {
+ if (!ChocolateFactoryAPI.isEnabled()) return
+ val profileStorage = profileStorage ?: return
+
+ val remainingSpace = profileStorage.maxRabbits - profileStorage.currentRabbits
+ barnFull =
+ remainingSpace <= config.barnCapacityThreshold && profileStorage.maxRabbits < ChocolateFactoryAPI.maxRabbits
+ if (!barnFull) return
+
+ if (lastBarnFullWarning.passedSince() < 30.seconds) return
+
+ if (profileStorage.maxRabbits == -1) {
+ ChatUtils.clickableChat(
+ "Open your chocolate factory to see your barn's capacity status!",
+ "cf"
+ )
+ return
+ }
+
+ ChatUtils.clickableChat(
+ "§cYour barn is almost full! " +
+ "§7(${barnStatus()}). §cUpgrade it so they don't get crushed",
+ "cf"
+ )
+ SoundUtils.playBeepSound()
+ lastBarnFullWarning = SimpleTimeMark.now()
+ }
+
+ fun barnStatus(): String {
+ val profileStorage = profileStorage ?: return "Unknown"
+ return "${profileStorage.currentRabbits}/${profileStorage.maxRabbits} Rabbits"
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryInventory.kt b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryInventory.kt
new file mode 100644
index 000000000..bad268f96
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryInventory.kt
@@ -0,0 +1,115 @@
+package at.hannibal2.skyhanni.features.event.chocolatefactory
+
+import at.hannibal2.skyhanni.events.GuiContainerEvent
+import at.hannibal2.skyhanni.events.GuiRenderItemEvent
+import at.hannibal2.skyhanni.events.RenderInventoryItemTipEvent
+import at.hannibal2.skyhanni.utils.InventoryUtils
+import at.hannibal2.skyhanni.utils.ItemUtils.getLore
+import at.hannibal2.skyhanni.utils.ItemUtils.name
+import at.hannibal2.skyhanni.utils.LorenzColor
+import at.hannibal2.skyhanni.utils.NumberUtil.formatInt
+import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimal
+import at.hannibal2.skyhanni.utils.RenderUtils.drawSlotText
+import at.hannibal2.skyhanni.utils.RenderUtils.highlight
+import at.hannibal2.skyhanni.utils.StringUtils.matchFirst
+import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
+import at.hannibal2.skyhanni.utils.StringUtils.removeColor
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+object ChocolateFactoryInventory {
+
+ private val config get() = ChocolateFactoryAPI.config
+
+ private val rabbitAmountPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "rabbit.amount",
+ "Rabbit \\S+ - \\[(?<amount>\\d+)].*"
+ )
+ private val upgradeTierPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "upgradetier",
+ ".*\\s(?<tier>[IVXLC]+)"
+ )
+ private val unclaimedRewardsPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "unclaimedrewards",
+ "§7§aYou have \\d+ unclaimed rewards?!"
+ )
+
+ @SubscribeEvent
+ fun onRenderItemOverlayPost(event: GuiRenderItemEvent.RenderOverlayEvent.GuiRenderItemPost) {
+ if (!ChocolateFactoryAPI.inChocolateFactory) return
+ if (!config.highlightUpgrades) return
+
+ val item = event.stack ?: return
+ val itemName = item.name
+ if (itemName != ChocolateFactoryAPI.bestRabbitUpgrade) return
+
+ event.drawSlotText(event.x + 18, event.y, "§6✦", .8f)
+ }
+
+ @SubscribeEvent
+ fun onBackgroundDrawn(event: GuiContainerEvent.BackgroundDrawnEvent) {
+ if (!ChocolateFactoryAPI.inChocolateFactory) return
+ if (!config.highlightUpgrades) return
+
+ for (slot in InventoryUtils.getItemsInOpenChest()) {
+ if (slot.slotIndex in ChocolateFactoryAPI.upgradeableSlots) {
+ if (slot.slotIndex == ChocolateFactoryAPI.bestUpgrade) {
+ slot highlight LorenzColor.GREEN.addOpacity(200)
+ } else {
+ slot highlight LorenzColor.GREEN.addOpacity(75)
+ }
+ }
+ if (slot.slotIndex == ChocolateFactoryAPI.barnIndex && ChocolateFactoryBarnManager.barnFull) {
+ slot highlight LorenzColor.RED
+ }
+ if (slot.slotIndex == ChocolateFactoryAPI.clickRabbitSlot) {
+ slot highlight LorenzColor.RED
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onRenderItemTip(event: RenderInventoryItemTipEvent) {
+ if (!ChocolateFactoryAPI.inChocolateFactory) return
+ if (!config.showStackSizes) return
+
+ val item = event.stack
+ val itemName = item.name.removeColor()
+ val slotNumber = event.slot.slotNumber
+
+ if (slotNumber in ChocolateFactoryAPI.rabbitSlots) {
+ rabbitAmountPattern.matchMatcher(itemName) {
+ val rabbitTip = when (val rabbitAmount = group("amount").formatInt()) {
+ in (0..9) -> "$rabbitAmount"
+ in (10..74) -> "§a$rabbitAmount"
+ in (75..124) -> "§9$rabbitAmount"
+ in (125..174) -> "§5$rabbitAmount"
+ in (175..199) -> "§6$rabbitAmount"
+ 200 -> "§d$rabbitAmount"
+ else -> "§c$rabbitAmount"
+ }
+
+ event.stackTip = rabbitTip
+ }
+ }
+ if (slotNumber in ChocolateFactoryAPI.otherUpgradeSlots) {
+ upgradeTierPattern.matchMatcher(itemName) {
+ event.stackTip = group("tier").romanToDecimal().toString()
+ }
+ }
+ if (slotNumber == ChocolateFactoryAPI.milestoneIndex) {
+ item.getLore().matchFirst(unclaimedRewardsPattern) {
+ event.stackTip = "§c!!!"
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onSlotClick(event: GuiContainerEvent.SlotClickEvent) {
+ if (!ChocolateFactoryAPI.inChocolateFactory) return
+ val slot = event.slot ?: return
+ if (!config.useMiddleClick) return
+ if (slot.slotNumber in ChocolateFactoryAPI.noPickblockSlots) return
+
+ event.makePickblock()
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryStats.kt b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryStats.kt
new file mode 100644
index 000000000..e1df1672d
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/ChocolateFactoryStats.kt
@@ -0,0 +1,79 @@
+package at.hannibal2.skyhanni.features.event.chocolatefactory
+
+import at.hannibal2.skyhanni.events.GuiRenderEvent
+import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators
+import at.hannibal2.skyhanni.utils.RenderUtils.renderStrings
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+object ChocolateFactoryStats {
+
+ private val config get() = ChocolateFactoryAPI.config
+
+ private var displayList = listOf<String>()
+
+ @SubscribeEvent
+ fun onBackgroundDraw(event: GuiRenderEvent.ChestGuiOverlayRenderEvent) {
+ if (!ChocolateFactoryAPI.inChocolateFactory) return
+ if (!config.statsDisplay) return
+
+ config.position.renderStrings(displayList, posLabel = "Chocolate Factory Stats")
+ }
+
+ fun updateDisplay() {
+ val perSecond = ChocolateFactoryAPI.chocolatePerSecond
+ val perMinute = perSecond * 60
+ val perHour = perMinute * 60
+ val perDay = perHour * 24
+ val position = ChocolateFactoryAPI.leaderboardPosition?.addSeparators() ?: "???"
+
+ displayList = formatList(buildList {
+ add("§6§lChocolate Factory Stats")
+
+ add("§eCurrent Chocolate: §6${ChocolateFactoryAPI.chocolateCurrent.addSeparators()}")
+ add("§eThis Prestige: §6${ChocolateFactoryAPI.chocolateThisPrestige.addSeparators()}")
+ add("§eAll-time: §6${ChocolateFactoryAPI.chocolateAllTime.addSeparators()}")
+
+ add("§ePer Second: §6${perSecond.addSeparators()}")
+ add("§ePer Minute: §6${perMinute.addSeparators()}")
+ add("§ePer Hour: §6${perHour.addSeparators()}")
+ add("§ePer Day: §6${perDay.addSeparators()}")
+
+ add("§eChocolate Multiplier: §6${ChocolateFactoryAPI.chocolateMultiplier}")
+ add("§eBarn: §6${ChocolateFactoryBarnManager.barnStatus()}")
+
+ add("§ePosition: §7#§b$position")
+
+ add("")
+ add("")
+ add("")
+ })
+ }
+
+ private fun formatList(list: List<String>): List<String> {
+ return config.statsDisplayList
+ .filter { ChocolateFactoryAPI.currentPrestige != 1 || it != ChocolateFactoryStat.THIS_PRESTIGE }
+ .map { list[it.ordinal] }
+ }
+
+ enum class ChocolateFactoryStat(private val display: String) {
+ HEADER("§6§lChocolate Factory Stats"),
+ CURRENT("§eCurrent Chocolate: §65,272,230"),
+ THIS_PRESTIGE("§eThis Prestige: §6483,023,853"),
+ ALL_TIME("§eAll-time: §6641,119,115"),
+ PER_SECOND("§ePer Second: §63,780.72"),
+ PER_MINUTE("§ePer Minute: §6226,843.2"),
+ PER_HOUR("§ePer Hour: §613,610,592"),
+ PER_DAY("§ePer Day: §6326,654,208"),
+ MULTIPLIER("§eChocolate Multiplier: §61.77"),
+ BARN("§eBarn: §6171/190 Rabbits"),
+ LEADERBOARD_POS("§ePosition: §7#§b103"),
+ EMPTY(""),
+ EMPTY_2(""),
+ EMPTY_3(""),
+ ;
+
+ override fun toString(): String {
+ return display
+ }
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggLocator.kt b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggLocator.kt
new file mode 100644
index 000000000..eee3abc34
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggLocator.kt
@@ -0,0 +1,240 @@
+package at.hannibal2.skyhanni.features.event.chocolatefactory
+
+import at.hannibal2.skyhanni.data.IslandType
+import at.hannibal2.skyhanni.events.DebugDataCollectEvent
+import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent
+import at.hannibal2.skyhanni.events.LorenzTickEvent
+import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
+import at.hannibal2.skyhanni.events.ReceiveParticleEvent
+import at.hannibal2.skyhanni.test.GriffinUtils.drawWaypointFilled
+import at.hannibal2.skyhanni.utils.InventoryUtils
+import at.hannibal2.skyhanni.utils.ItemUtils.getInternalName
+import at.hannibal2.skyhanni.utils.LorenzColor
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.LorenzUtils.round
+import at.hannibal2.skyhanni.utils.LorenzVec
+import at.hannibal2.skyhanni.utils.NEUInternalName.Companion.asInternalName
+import at.hannibal2.skyhanni.utils.RenderUtils.draw3DLine
+import at.hannibal2.skyhanni.utils.RenderUtils.drawDynamicText
+import at.hannibal2.skyhanni.utils.SimpleTimeMark
+import net.minecraft.item.ItemStack
+import net.minecraft.util.EnumParticleTypes
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import kotlin.time.Duration.Companion.seconds
+
+object HoppityEggLocator {
+
+ private val config get() = ChocolateFactoryAPI.config.hoppityEggs
+
+ private val locatorItem = "EGGLOCATOR".asInternalName()
+
+ private var lastParticlePosition: LorenzVec? = null
+ private val validParticleLocations = mutableListOf<LorenzVec>()
+
+ private var drawLocations = false
+ private var firstPos = LorenzVec()
+ private var secondPos = LorenzVec()
+ private var possibleEggLocations = listOf<LorenzVec>()
+
+ private var ticksSinceLastParticleFound = -1
+ private var lastGuessMade = SimpleTimeMark.farPast()
+ private var eggLocationWeights = listOf<Double>()
+
+ var sharedEggLocation: LorenzVec? = null
+ var currentEggType: HoppityEggType? = null
+
+ var eggLocations: Map<IslandType, List<LorenzVec>> = mapOf()
+
+ @SubscribeEvent
+ fun onWorldChange(event: LorenzWorldChangeEvent) {
+ resetData()
+ }
+
+ private fun resetData() {
+ validParticleLocations.clear()
+ ticksSinceLastParticleFound = -1
+ possibleEggLocations = emptyList()
+ firstPos = LorenzVec()
+ secondPos = LorenzVec()
+ drawLocations = false
+ sharedEggLocation = null
+ currentEggType = null
+ }
+
+ @SubscribeEvent
+ fun onRenderWorld(event: LorenzRenderWorldEvent) {
+ if (!isEnabled()) return
+
+ event.draw3DLine(firstPos, secondPos, LorenzColor.RED.toColor(), 2, false)
+
+ if (drawLocations) {
+ for ((index, eggLocation) in possibleEggLocations.withIndex()) {
+ val eggLabel = "§aGuess #${index + 1}"
+ event.drawWaypointFilled(
+ eggLocation,
+ LorenzColor.GREEN.toColor(),
+ seeThroughBlocks = true,
+ )
+ event.drawDynamicText(eggLocation.add(y = 1), eggLabel, 1.5)
+ }
+ return
+ }
+
+ val sharedEggLocation = sharedEggLocation
+ if (sharedEggLocation != null && config.sharedWaypoints) {
+ event.drawWaypointFilled(
+ sharedEggLocation,
+ LorenzColor.GREEN.toColor(),
+ seeThroughBlocks = true,
+ )
+ event.drawDynamicText(sharedEggLocation.add(y = 1), "§aShared Egg", 1.5)
+ return
+ }
+
+ if (!config.showAllWaypoints) return
+ if (hasLocatorInInventory()) return
+ if (!HoppityEggType.eggsRemaining()) return
+
+ val islandEggsLocations = getCurrentIslandEggLocations() ?: return
+ for (eggLocation in islandEggsLocations) {
+ event.drawWaypointFilled(
+ eggLocation,
+ LorenzColor.GREEN.toColor(),
+ seeThroughBlocks = true,
+ )
+ event.drawDynamicText(eggLocation.add(y = 1), "§aEgg", 1.5)
+ }
+ }
+
+ fun eggFound() {
+ resetData()
+ }
+
+ @SubscribeEvent
+ fun onReceiveParticle(event: ReceiveParticleEvent) {
+ if (!isEnabled()) return
+ if (!hasLocatorInInventory()) return
+ if (!event.isVillagerParticle() && !event.isEnchantmentParticle()) return
+
+ val lastParticlePosition = lastParticlePosition ?: run {
+ lastParticlePosition = event.location
+ return
+ }
+ if (lastParticlePosition == event.location) {
+ validParticleLocations.add(event.location)
+ ticksSinceLastParticleFound = 0
+ }
+ HoppityEggLocator.lastParticlePosition = null
+ }
+
+ @SubscribeEvent
+ fun onTick(event: LorenzTickEvent) {
+ if (!isEnabled()) return
+ if (validParticleLocations.isEmpty()) return
+ ticksSinceLastParticleFound++
+
+ if (ticksSinceLastParticleFound < 6) return
+
+ calculateEggPosition()
+
+ ticksSinceLastParticleFound = 0
+ validParticleLocations.clear()
+ }
+
+ private fun calculateEggPosition() {
+ if (lastGuessMade.passedSince() < 1.seconds) return
+ lastGuessMade = SimpleTimeMark.now()
+ possibleEggLocations = emptyList()
+
+ val islandEggsLocations = getCurrentIslandEggLocations() ?: return
+ val listSize = validParticleLocations.size
+
+ if (listSize < 5) return
+
+ val secondPoint = validParticleLocations.removeLast()
+ firstPos = validParticleLocations.removeLast()
+
+ val xDiff = secondPoint.x - firstPos.x
+ val yDiff = secondPoint.y - firstPos.y
+ val zDiff = secondPoint.z - firstPos.z
+
+ secondPos = LorenzVec(
+ secondPoint.x + xDiff * 1000,
+ secondPoint.y + yDiff * 1000,
+ secondPoint.z + zDiff * 1000
+ )
+
+ val sortedEggs = islandEggsLocations.map {
+ it to it.getEggLocationWeight(firstPos, secondPos)
+ }.sortedBy { it.second }
+
+ eggLocationWeights = sortedEggs.map {
+ it.second.round(3)
+ }.take(5)
+
+ val filteredEggs = sortedEggs.filter {
+ it.second < 1
+ }.map { it.first }
+
+ val maxLineDistance = filteredEggs.sortedByDescending {
+ it.nearestPointOnLine(firstPos, secondPos).distance(firstPos)
+ }
+
+ if (maxLineDistance.isEmpty()) {
+ LorenzUtils.sendTitle("§cNo eggs found, try getting closer", 2.seconds)
+ return
+ }
+ secondPos = maxLineDistance.first().nearestPointOnLine(firstPos, secondPos)
+
+ possibleEggLocations = filteredEggs
+
+ drawLocations = true
+ }
+
+ fun getCurrentIslandEggLocations(): List<LorenzVec>? =
+ eggLocations[LorenzUtils.skyBlockIsland]
+
+ fun isValidEggLocation(location: LorenzVec): Boolean =
+ getCurrentIslandEggLocations()?.any { it.distance(location) < 5.0 } ?: false
+
+ private fun ReceiveParticleEvent.isVillagerParticle() =
+ type == EnumParticleTypes.VILLAGER_HAPPY && speed == 0.0f && count == 1
+
+ private fun ReceiveParticleEvent.isEnchantmentParticle() =
+ type == EnumParticleTypes.ENCHANTMENT_TABLE && speed == -2.0f && count == 10
+
+ private fun isEnabled() = LorenzUtils.inSkyBlock && config.waypoints
+ && ChocolateFactoryAPI.isHoppityEvent()
+
+ private val ItemStack.isLocatorItem get() = getInternalName() == locatorItem
+ fun hasLocatorInInventory() = InventoryUtils.getItemsInOwnInventory().any { it.isLocatorItem }
+
+ private fun LorenzVec.getEggLocationWeight(firstPoint: LorenzVec, secondPoint: LorenzVec): Double {
+ val distToLine = this.distanceToLine(firstPoint, secondPoint)
+ val distToStart = this.distance(firstPoint)
+ val distMultiplier = distToStart * 2 / 100 + 5
+ val disMultiplierSquared = distMultiplier * distMultiplier
+ return distToLine / disMultiplierSquared
+ }
+
+ @SubscribeEvent
+ fun onDebugDataCollect(event: DebugDataCollectEvent) {
+ event.title("Hoppity Eggs Locations")
+
+ if (!isEnabled()) {
+ event.addIrrelevant("not in skyblock or waypoints are disabled")
+ return
+ }
+
+ event.addData {
+ add("First Pos: $firstPos")
+ add("Second Pos: $secondPos")
+ add("Possible Egg Locations: ${possibleEggLocations.size}")
+ add("Egg Location Weights: $eggLocationWeights")
+ add("Last Time Checked: ${lastGuessMade.passedSince().inWholeSeconds}s ago")
+ add("Draw Locations: $drawLocations")
+ add("Shared Egg Location: ${sharedEggLocation ?: "None"}")
+ add("Current Egg Type: ${currentEggType ?: "None"}")
+ }
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggType.kt b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggType.kt
new file mode 100644
index 000000000..c0c88d5f6
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggType.kt
@@ -0,0 +1,53 @@
+package at.hannibal2.skyhanni.features.event.chocolatefactory
+
+import io.github.moulberry.notenoughupdates.util.SkyBlockTime
+
+enum class HoppityEggType(
+ val mealName: String,
+ val resetsAt: Int,
+ private val mealColour: String,
+ var lastResetDay: Int = -1,
+ private var claimed: Boolean = false
+) {
+ BREAKFAST("Breakfast", 7, "§a"),
+ LUNCH("Lunch", 14, "§9"),
+ DINNER("Dinner", 21, "§6"),
+ ;
+
+ fun markClaimed() {
+ claimed = true
+ }
+
+ fun markSpawned() {
+ claimed = false
+ }
+
+ fun isClaimed() = claimed
+ val formattedName by lazy { "$mealColour$mealName" }
+
+ companion object {
+ fun allFound() = entries.forEach { it.markClaimed() }
+
+ fun getMealByName(mealName: String) = entries.find { it.mealName == mealName }
+
+ fun checkClaimed() {
+ val currentSbTime = SkyBlockTime.now()
+ val currentSbDay = currentSbTime.day
+ val currentSbHour = currentSbTime.hour
+
+ for (eggType in entries) {
+ if (currentSbHour < eggType.resetsAt || eggType.lastResetDay == currentSbDay) continue
+ eggType.markSpawned()
+ eggType.lastResetDay = currentSbDay
+ if (HoppityEggLocator.currentEggType == eggType) {
+ HoppityEggLocator.currentEggType = null
+ HoppityEggLocator.sharedEggLocation = null
+ }
+ }
+ }
+
+ fun eggsRemaining(): Boolean {
+ return entries.any { !it.claimed }
+ }
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggsManager.kt b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggsManager.kt
new file mode 100644
index 000000000..43ab8c488
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggsManager.kt
@@ -0,0 +1,123 @@
+package at.hannibal2.skyhanni.features.event.chocolatefactory
+
+import at.hannibal2.skyhanni.events.GuiRenderEvent
+import at.hannibal2.skyhanni.events.LorenzChatEvent
+import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent
+import at.hannibal2.skyhanni.events.SecondPassedEvent
+import at.hannibal2.skyhanni.features.fame.ReminderUtils
+import at.hannibal2.skyhanni.test.command.ErrorManager
+import at.hannibal2.skyhanni.utils.ChatUtils
+import at.hannibal2.skyhanni.utils.DelayedRun
+import at.hannibal2.skyhanni.utils.LocationUtils
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.RecalculatingValue
+import at.hannibal2.skyhanni.utils.RenderUtils.renderStrings
+import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.fromNow
+import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.util.regex.Matcher
+import kotlin.time.Duration.Companion.seconds
+
+object HoppityEggsManager {
+
+ private val config get() = ChocolateFactoryAPI.config.hoppityEggs
+
+ private val eggFoundPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "egg.found",
+ "§d§lHOPPITY'S HUNT §r§dYou found a §r§.Chocolate (?<meal>\\w+) Egg.*"
+ )
+ private val noEggsLeftPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "egg.noneleft",
+ "§cThere are no hidden Chocolate Rabbit Eggs nearby! Try again later!"
+ )
+ private val eggSpawnedPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "egg.spawned",
+ "§d§lHOPPITY'S HUNT §r§dA §r§.Chocolate (?<meal>\\w+) Egg §r§dhas appeared!"
+ )
+ private val eggAlreadyCollectedPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "egg.alreadycollected",
+ "§cYou have already collected this Chocolate (?<meal>\\w+) Egg§r§c! Try again when it respawns!"
+ )
+
+ private var lastMeal: HoppityEggType? = null
+
+ @SubscribeEvent
+ fun onWorldChange(event: LorenzWorldChangeEvent) {
+ lastMeal = null
+ }
+
+ @SubscribeEvent
+ fun onChat(event: LorenzChatEvent) {
+ if (!LorenzUtils.inSkyBlock) return
+
+ eggFoundPattern.matchMatcher(event.message) {
+ HoppityEggLocator.eggFound()
+ val meal = getEggType(event)
+ meal.markClaimed()
+ lastMeal = meal
+ }
+
+ noEggsLeftPattern.matchMatcher(event.message) {
+ HoppityEggType.allFound()
+ return
+ }
+
+ eggAlreadyCollectedPattern.matchMatcher(event.message) {
+ getEggType(event).markClaimed()
+ }
+
+ eggSpawnedPattern.matchMatcher(event.message) {
+ getEggType(event).markSpawned()
+ }
+ }
+
+ internal fun Matcher.getEggType(event: LorenzChatEvent): HoppityEggType =
+ HoppityEggType.getMealByName(group("meal")) ?: run {
+ ErrorManager.skyHanniError(
+ "Unknown meal: ${group("meal")}",
+ "message" to event.message
+ )
+ }
+
+ fun shareWaypointPrompt() {
+ if (!config.sharedWaypoints) return
+ val meal = lastMeal ?: return
+ lastMeal = null
+
+ val currentLocation = LocationUtils.playerLocation()
+ DelayedRun.runNextTick {
+ ChatUtils.clickableChat(
+ "Click here to share the location of this chocolate egg with the server!",
+ onClick = { HoppityEggsShared.shareNearbyEggLocation(currentLocation, meal) },
+ expireAt = 30.seconds.fromNow()
+ )
+ }
+ }
+
+ private val hasLocatorInInventory = RecalculatingValue(1.seconds) {
+ HoppityEggLocator.hasLocatorInInventory()
+ }
+
+ @SubscribeEvent
+ fun onRenderOverlay(event: GuiRenderEvent.GuiOverlayRenderEvent) {
+ if (!LorenzUtils.inSkyBlock) return
+ if (!config.showClaimedEggs) return
+ if (ReminderUtils.isBusy()) return
+ if (!ChocolateFactoryAPI.isHoppityEvent()) return
+ if (!hasLocatorInInventory.getValue()) return
+
+ val displayList = HoppityEggType.entries
+ .filter { !it.isClaimed() }
+ .map { "§7 - ${it.formattedName}" }
+ .toMutableList()
+ displayList.add(0, "§bUnfound Eggs:")
+ if (displayList.size == 1) return
+
+ config.position.renderStrings(displayList, posLabel = "Hoppity Eggs")
+ }
+
+ @SubscribeEvent
+ fun onSecondPassed(event: SecondPassedEvent) {
+ HoppityEggType.checkClaimed()
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggsShared.kt b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggsShared.kt
new file mode 100644
index 000000000..3dac4b856
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/event/chocolatefactory/HoppityEggsShared.kt
@@ -0,0 +1,57 @@
+package at.hannibal2.skyhanni.features.event.chocolatefactory
+
+import at.hannibal2.skyhanni.events.LorenzChatEvent
+import at.hannibal2.skyhanni.features.event.chocolatefactory.HoppityEggsManager.getEggType
+import at.hannibal2.skyhanni.utils.ChatUtils
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.LorenzVec
+import at.hannibal2.skyhanni.utils.NumberUtil.formatInt
+import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
+import at.hannibal2.skyhanni.utils.StringUtils.removeColor
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+object HoppityEggsShared {
+
+ private val config get() = ChocolateFactoryAPI.config.hoppityEggs
+
+ /**
+ * REGEX-TEST: CalMWolfs: [SkyHanni] Breakfast Chocolate Egg located at x: 142, y: 71, z: -453
+ */
+ private val sharedEggPattern by ChocolateFactoryAPI.patternGroup.pattern(
+ "egg.shared",
+ ".*\\[SkyHanni] (?<meal>\\w+) Chocolate Egg located at x: (?<x>-?\\d+), y: (?<y>-?\\d+), z: (?<z>-?\\d+)"
+ )
+
+ @SubscribeEvent
+ fun onChat(event: LorenzChatEvent) {
+ if (!isEnabled()) return
+
+ sharedEggPattern.matchMatcher(event.message.removeColor()) {
+ val (x, y, z) = listOf(group("x"), group("y"), group("z")).map { it.formatInt() }
+ val eggLocation = LorenzVec(x, y, z)
+
+ val meal = getEggType(event)
+
+ if (meal.isClaimed()) return
+ if (!HoppityEggLocator.isValidEggLocation(eggLocation)) return
+
+ HoppityEggLocator.sharedEggLocation = eggLocation
+ HoppityEggLocator.currentEggType = meal
+ }
+ }
+
+ fun shareNearbyEggLocation(playerLocation: LorenzVec, meal: HoppityEggType) {
+ if (!isEnabled()) return
+ val islandEggsLocations = HoppityEggLocator.getCurrentIslandEggLocations() ?: return
+ val closestEgg = islandEggsLocations.minByOrNull { it.distance(playerLocation) } ?: return
+
+ val x = closestEgg.x.toInt()
+ val y = closestEgg.y.toInt()
+ val z = closestEgg.z.toInt()
+
+ val message = "[SkyHanni] ${meal.mealName} Chocolate Egg located at x: $x, y: $y, z: $z"
+ ChatUtils.sendCommandToServer("ac $message")
+ }
+
+ fun isEnabled() = LorenzUtils.inSkyBlock && config.waypoints && config.sharedWaypoints
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/InventoryUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/InventoryUtils.kt
index 9bc8ff5ae..69ca8fc59 100644
--- a/src/main/java/at/hannibal2/skyhanni/utils/InventoryUtils.kt
+++ b/src/main/java/at/hannibal2/skyhanni/utils/InventoryUtils.kt
@@ -36,7 +36,9 @@ object InventoryUtils {
fun ContainerChest.getInventoryName() = this.lowerChestInventory.displayName.unformattedText.trim()
- fun getItemsInOwnInventory() = Minecraft.getMinecraft().thePlayer.inventory.mainInventory.filterNotNull()
+ fun getItemsInOwnInventory() =
+ Minecraft.getMinecraft().thePlayer?.inventory?.mainInventory?.filterNotNull() ?: emptyList()
+
fun getItemsInOwnInventoryWithNull() = Minecraft.getMinecraft().thePlayer.inventory.mainInventory
fun countItemsInLowerInventory(predicate: (ItemStack) -> Boolean) =
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/UtilsPatterns.kt b/src/main/java/at/hannibal2/skyhanni/utils/UtilsPatterns.kt
index 18d450040..797207d0b 100644
--- a/src/main/java/at/hannibal2/skyhanni/utils/UtilsPatterns.kt
+++ b/src/main/java/at/hannibal2/skyhanni/utils/UtilsPatterns.kt
@@ -55,6 +55,10 @@ object UtilsPatterns {
"item.amount.behind",
"(?<name>(?:§.)*(?:[^§] ?)+)(?:§8x(?<amount>[\\d,]+))?"
)
+ val costLinePattern by patternGroup.pattern(
+ "item.cost.line",
+ "§7Cost"
+ )
val timeAmountPattern by patternGroup.pattern(
"time.amount",