aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKevin <92656833+kevinthegreat1@users.noreply.github.com>2024-05-23 22:19:13 -0400
committerGitHub <noreply@github.com>2024-05-23 22:19:13 -0400
commitb6da8de30929b4722244062e948dca42a810a230 (patch)
treef5ea29a18d285f29397d5e2a5bbaac06194eb38e /src
parent610c64758fc8d0b8bea0145c33881b60c0747bd7 (diff)
parent1f6798df58af5eec4dfe954ab413c5a92d0fee63 (diff)
downloadSkyblocker-b6da8de30929b4722244062e948dca42a810a230.tar.gz
Skyblocker-b6da8de30929b4722244062e948dca42a810a230.tar.bz2
Skyblocker-b6da8de30929b4722244062e948dca42a810a230.zip
Merge pull request #683 from Emirlol/chocolate-factory-helper
Add chocolate factory helper
Diffstat (limited to 'src')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java49
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/debug/Debug.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java348
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java168
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java86
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ColorUtils.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ItemUtils.java7
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/RegexUtils.java55
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java61
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java4
-rw-r--r--src/main/resources/assets/skyblocker/lang/en_us.json12
16 files changed, 855 insertions, 18 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index e6b2d25a..a57b4177 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -2,15 +2,16 @@ package de.hysky.skyblocker;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
-
-import de.hysky.skyblocker.config.datafixer.ConfigDataFixer;
import de.hysky.skyblocker.config.ImageRepoLoader;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.config.datafixer.ConfigDataFixer;
import de.hysky.skyblocker.debug.Debug;
import de.hysky.skyblocker.skyblock.*;
import de.hysky.skyblocker.skyblock.calculators.CalculatorCommand;
import de.hysky.skyblocker.skyblock.chat.ChatRuleAnnouncementScreen;
import de.hysky.skyblocker.skyblock.chat.ChatRulesHandler;
+import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
+import de.hysky.skyblocker.skyblock.chocolatefactory.TimeTowerReminder;
import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra;
import de.hysky.skyblocker.skyblock.dungeon.*;
import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen;
@@ -45,10 +46,7 @@ import de.hysky.skyblocker.skyblock.waypoint.FairySouls;
import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual;
import de.hysky.skyblocker.skyblock.waypoint.OrderedWaypoints;
import de.hysky.skyblocker.skyblock.waypoint.Relics;
-import de.hysky.skyblocker.utils.ApiUtils;
-import de.hysky.skyblocker.utils.NEURepoManager;
-import de.hysky.skyblocker.utils.ProfileUtils;
-import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.*;
import de.hysky.skyblocker.utils.chat.ChatMessageListener;
import de.hysky.skyblocker.utils.discord.DiscordRPCManager;
import de.hysky.skyblocker.utils.render.RenderHelper;
@@ -183,6 +181,9 @@ public class SkyblockerMod implements ClientModInitializer {
BeaconHighlighter.init();
WarpAutocomplete.init();
MobBoundingBoxes.init();
+ EggFinder.init();
+ TimeTowerReminder.init();
+ SkyblockTime.init();
Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20);
Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200);
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
index 1528f853..9e4935cb 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
@@ -2,10 +2,11 @@ package de.hysky.skyblocker.config.categories;
import de.hysky.skyblocker.config.ConfigUtils;
import de.hysky.skyblocker.config.SkyblockerConfig;
-import dev.isxander.yacl3.api.*;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
import dev.isxander.yacl3.api.ConfigCategory;
import dev.isxander.yacl3.api.Option;
import dev.isxander.yacl3.api.OptionDescription;
+import dev.isxander.yacl3.api.OptionGroup;
import dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder;
import net.minecraft.text.Text;
@@ -137,6 +138,52 @@ public class HelperCategory {
.build())
.build())
+ //Chocolate Factory
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableChocolateFactoryHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableChocolateFactoryHelper.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.enableChocolateFactoryHelper,
+ () -> config.helpers.chocolateFactory.enableChocolateFactoryHelper,
+ newValue -> config.helpers.chocolateFactory.enableChocolateFactoryHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableEggFinder"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableEggFinder.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.enableEggFinder,
+ () -> config.helpers.chocolateFactory.enableEggFinder,
+ newValue -> config.helpers.chocolateFactory.enableEggFinder = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.sendEggFoundMessages"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.sendEggFoundMessages.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.sendEggFoundMessages,
+ () -> config.helpers.chocolateFactory.sendEggFoundMessages,
+ newValue -> config.helpers.chocolateFactory.sendEggFoundMessages = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Waypoint.Type>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.waypointType"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.waypointType.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.waypointType,
+ () -> config.helpers.chocolateFactory.waypointType,
+ newValue -> config.helpers.chocolateFactory.waypointType = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableTimeTowerReminder"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableTimeTowerReminder.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.enableTimeTowerReminder,
+ () -> config.helpers.chocolateFactory.enableTimeTowerReminder,
+ newValue -> config.helpers.chocolateFactory.enableTimeTowerReminder = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
.build();
}
}
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
index 2abff6ac..c0314924 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
@@ -1,5 +1,6 @@
package de.hysky.skyblocker.config.configs;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
import dev.isxander.yacl3.config.v2.api.SerialEntry;
public class HelperConfig {
@@ -19,6 +20,9 @@ public class HelperConfig {
@SerialEntry
public FairySouls fairySouls = new FairySouls();
+ @SerialEntry
+ public ChocolateFactory chocolateFactory = new ChocolateFactory();
+
public static class MythologicalRitual {
@SerialEntry
public boolean enableMythologicalRitualHelper = true;
@@ -62,4 +66,21 @@ public class HelperConfig {
@SerialEntry
public boolean highlightOnlyNearbySouls = false;
}
+
+ public static class ChocolateFactory {
+ @SerialEntry
+ public boolean enableChocolateFactoryHelper = true;
+
+ @SerialEntry
+ public boolean enableEggFinder = true;
+
+ @SerialEntry
+ public boolean sendEggFoundMessages = true;
+
+ @SerialEntry
+ public Waypoint.Type waypointType = Waypoint.Type.WAYPOINT;
+
+ @SerialEntry
+ public boolean enableTimeTowerReminder = true;
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java
index d9ac668c..d642ca5b 100644
--- a/src/main/java/de/hysky/skyblocker/debug/Debug.java
+++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java
@@ -83,9 +83,7 @@ public class Debug {
Iterable<ItemStack> equippedItems = armorStand.getEquippedItems();
for (ItemStack stack : equippedItems) {
- String texture = ItemUtils.getHeadTexture(stack);
-
- if (!texture.isEmpty()) context.getSource().sendFeedback(Text.of(texture));
+ ItemUtils.getHeadTextureOptional(stack).ifPresent(texture -> context.getSource().sendFeedback(Text.of(texture)));
}
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
index 2c2c1376..48389d40 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
@@ -3,9 +3,9 @@ package de.hysky.skyblocker.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.sugar.Local;
-import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.CompactDamage;
import de.hysky.skyblocker.skyblock.FishingHelper;
+import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
import de.hysky.skyblocker.skyblock.dungeon.DungeonScore;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
import de.hysky.skyblocker.skyblock.end.BeaconHighlighter;
@@ -84,7 +84,7 @@ public abstract class ClientPlayNetworkHandlerMixin {
return !(Utils.isOnHypixel() && ((Identifier) identifier).getNamespace().equals("badlion"));
}
- @WrapWithCondition(method = { "onScoreboardScoreUpdate", "onScoreboardScoreReset" }, at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false))
+ @WrapWithCondition(method = {"onScoreboardScoreUpdate", "onScoreboardScoreReset"}, at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false))
private boolean skyblocker$cancelUnknownScoreboardObjectiveWarnings(Logger instance, String message, Object objectiveName) {
return !Utils.isOnHypixel();
}
@@ -111,11 +111,18 @@ public abstract class ClientPlayNetworkHandlerMixin {
@Inject(method = "onEntityTrackerUpdate", at = @At("TAIL"))
private void skyblocker$onEntityTrackerUpdate(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) {
- if (!SkyblockerConfigManager.get().uiAndVisuals.compactDamage.enabled || !(entity instanceof ArmorStandEntity armorStandEntity)) return;
+ if (!(entity instanceof ArmorStandEntity armorStandEntity)) return;
+
+ EggFinder.checkIfEgg(armorStandEntity);
try { //Prevent packet handling fails if something goes wrong so that entity trackers still update, just without compact damage numbers
CompactDamage.compactDamage(armorStandEntity);
} catch (Exception e) {
LOGGER.error("[Skyblocker Compact Damage] Failed to compact damage number", e);
}
}
+
+ @Inject(method = "onEntityEquipmentUpdate", at = @At(value = "TAIL"))
+ private void skyblocker$onEntityEquip(EntityEquipmentUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) {
+ EggFinder.checkIfEgg(entity);
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java b/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java
index 2837364b..8285a823 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java
@@ -19,6 +19,7 @@ public class CompactDamage {
}
public static void compactDamage(ArmorStandEntity entity) {
+ if (!SkyblockerConfigManager.get().uiAndVisuals.compactDamage.enabled) return;
if (!entity.isInvisible() || !entity.hasCustomName() || !entity.isCustomNameVisible()) return;
Text customName = entity.getCustomName();
String customNameStringified = customName.getString();
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
new file mode 100644
index 00000000..2d530b6d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
@@ -0,0 +1,348 @@
+package de.hysky.skyblocker.skyblock.chocolatefactory;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.RegexUtils;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.client.item.TooltipType;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ChocolateFactorySolver extends ContainerSolver {
+ private static final Pattern CPS_PATTERN = Pattern.compile("([\\d,.]+) Chocolate per second");
+ private static final Pattern CPS_INCREASE_PATTERN = Pattern.compile("\\+([\\d,]+) Chocolate per second");
+ private static final Pattern COST_PATTERN = Pattern.compile("Cost ([\\d,]+) Chocolate");
+ private static final Pattern TOTAL_MULTIPLIER_PATTERN = Pattern.compile("Total Multiplier: ([\\d.]+)x");
+ private static final Pattern MULTIPLIER_INCREASE_PATTERN = Pattern.compile("\\+([\\d.]+)x Chocolate per second");
+ private static final Pattern CHOCOLATE_PATTERN = Pattern.compile("^([\\d,]+) Chocolate$");
+ private static final Pattern PRESTIGE_REQUIREMENT_PATTERN = Pattern.compile("Chocolate this Prestige: ([\\d,]+) +Requires (\\S+) Chocolate this Prestige!");
+ private static final Pattern TIME_TOWER_STATUS_PATTERN = Pattern.compile("Status: (ACTIVE|INACTIVE)");
+ private static final ObjectArrayList<Rabbit> cpsIncreaseFactors = new ObjectArrayList<>(6);
+ private static long totalChocolate = -1L;
+ private static double totalCps = -1.0;
+ private static double totalCpsMultiplier = -1.0;
+ private static long requiredUntilNextPrestige = -1L;
+ private static double timeTowerMultiplier = -1.0;
+ private static boolean isTimeTowerActive = false;
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
+ private static ItemStack bestUpgrade = null;
+ private static ItemStack bestAffordableUpgrade = null;
+
+ public ChocolateFactorySolver() {
+ super("^Chocolate Factory$");
+ ItemTooltipCallback.EVENT.register(ChocolateFactorySolver::handleTooltip);
+ }
+
+ @Override
+ protected boolean isEnabled() {
+ return SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper;
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) {
+ updateFactoryInfo(slots);
+ List<ColorHighlight> highlights = new ArrayList<>();
+
+ getPrestigeHighlight(slots.get(28)).ifPresent(highlights::add);
+
+ if (totalChocolate <= 0 || cpsIncreaseFactors.isEmpty()) return highlights; //Something went wrong or there's nothing we can afford.
+ Rabbit bestRabbit = cpsIncreaseFactors.getFirst();
+ bestUpgrade = bestRabbit.itemStack;
+ if (bestRabbit.cost <= totalChocolate) {
+ highlights.add(ColorHighlight.green(bestRabbit.slot));
+ return highlights;
+ }
+ highlights.add(ColorHighlight.yellow(bestRabbit.slot));
+
+ for (Rabbit rabbit : cpsIncreaseFactors.subList(1, cpsIncreaseFactors.size())) {
+ if (rabbit.cost <= totalChocolate) {
+ bestAffordableUpgrade = rabbit.itemStack;
+ highlights.add(ColorHighlight.green(rabbit.slot));
+ break;
+ }
+ }
+
+ return highlights;
+ }
+
+ private static void updateFactoryInfo(Int2ObjectMap<ItemStack> slots) {
+ cpsIncreaseFactors.clear();
+
+ for (int i = 29; i <= 33; i++) { // The 5 rabbits slots are in 29, 30, 31, 32 and 33.
+ ItemStack item = slots.get(i);
+ if (item.isOf(Items.PLAYER_HEAD)) {
+ getRabbit(item, i).ifPresent(cpsIncreaseFactors::add);
+ }
+ }
+
+ //Coach is in slot 42
+ getCoach(slots.get(42)).ifPresent(cpsIncreaseFactors::add);
+
+ //The clickable chocolate is in slot 13, holds the total chocolate
+ RegexUtils.getLongFromMatcher(CHOCOLATE_PATTERN.matcher(slots.get(13).getName().getString())).ifPresent(l -> totalChocolate = l);
+
+ //Cps item (cocoa bean) is in slot 45
+ String cpsItemLore = getConcatenatedLore(slots.get(45));
+ Matcher cpsMatcher = CPS_PATTERN.matcher(cpsItemLore);
+ RegexUtils.getDoubleFromMatcher(cpsMatcher).ifPresent(d -> totalCps = d);
+ Matcher multiplierMatcher = TOTAL_MULTIPLIER_PATTERN.matcher(cpsItemLore);
+ RegexUtils.getDoubleFromMatcher(multiplierMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0).ifPresent(d -> totalCpsMultiplier = d);
+
+ //Prestige item is in slot 28
+ Matcher prestigeMatcher = PRESTIGE_REQUIREMENT_PATTERN.matcher(getConcatenatedLore(slots.get(28)));
+ OptionalLong currentChocolate = RegexUtils.getLongFromMatcher(prestigeMatcher);
+ if (currentChocolate.isPresent()) {
+ String requirement = prestigeMatcher.group(2); //If the first one matched, we can assume the 2nd one is also matched since it's one whole regex
+ //Since the last character is either M or B we can just try to replace both characters. Only the correct one will actually replace anything.
+ String amountString = requirement.replace("M", "000000").replace("B", "000000000");
+ if (NumberUtils.isParsable(amountString)) {
+ requiredUntilNextPrestige = Long.parseLong(amountString) - currentChocolate.getAsLong();
+ }
+ }
+
+ //Time Tower is in slot 39
+ timeTowerMultiplier = romanToDecimal(StringUtils.substringAfterLast(slots.get(39).getName().getString(), ' ')) / 10.0; //The name holds the level, which is multiplier * 10 in roman numerals
+ Matcher timeTowerStatusMatcher = TIME_TOWER_STATUS_PATTERN.matcher(getConcatenatedLore(slots.get(39)));
+ if (timeTowerStatusMatcher.find()) {
+ isTimeTowerActive = timeTowerStatusMatcher.group(1).equals("ACTIVE");
+ }
+
+ //Compare cost/cpsIncrease rather than cpsIncrease/cost to avoid getting close to 0 and losing precision.
+ cpsIncreaseFactors.sort(Comparator.comparingDouble(rabbit -> rabbit.cost() / rabbit.cpsIncrease())); //Ascending order, lower = better
+ }
+
+ private static void handleTooltip(ItemStack stack, Item.TooltipContext tooltipContext, TooltipType tooltipType, List<Text> lines) {
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper) return;
+ if (!(MinecraftClient.getInstance().currentScreen instanceof GenericContainerScreen screen) || !screen.getTitle().getString().equals("Chocolate Factory")) return;
+
+ int lineIndex = lines.size();
+ //This boolean is used to determine if we should add a smooth line to separate the added information from the rest of the tooltip.
+ //It should be set to true if there's any information added, false otherwise.
+ boolean shouldAddLine = false;
+
+ String lore = concatenateLore(lines);
+ Matcher costMatcher = COST_PATTERN.matcher(lore);
+ OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher);
+ //Available on all items with a chocolate cost
+ if (cost.isPresent()) shouldAddLine = addUpgradeTimerToLore(lines, cost.getAsLong());
+
+ //Prestige item
+ if (stack.isOf(Items.DROPPER) && requiredUntilNextPrestige != -1L) {
+ shouldAddLine = addPrestigeTimerToLore(lines) || shouldAddLine;
+ }
+ //Time tower
+ else if (stack.isOf(Items.CLOCK)) {
+ shouldAddLine = addTimeTowerStatsToLore(lines) || shouldAddLine;
+ }
+ //Rabbits
+ else if (stack.isOf(Items.PLAYER_HEAD)) {
+ shouldAddLine = addRabbitStatsToLore(lines, stack) || shouldAddLine;
+ }
+
+ //This is an ArrayList, so this operation is probably not very efficient, but logically it's pretty much the only way I can think of
+ if (shouldAddLine) lines.add(lineIndex, ItemTooltip.createSmoothLine());
+ }
+
+ private static boolean addUpgradeTimerToLore(List<Text> lines, long cost) {
+ if (totalChocolate < 0L || totalCps < 0.0) return false;
+ lines.add(Text.empty()
+ .append(Text.literal("Time until upgrade: ").formatted(Formatting.GRAY))
+ .append(formatTime((cost - totalChocolate) / totalCps)));
+ return true;
+ }
+
+ private static boolean addPrestigeTimerToLore(List<Text> lines) {
+ if (requiredUntilNextPrestige == -1L || totalCps == -1.0) return false;
+ lines.add(Text.empty()
+ .append(Text.literal("Chocolate until next prestige: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(requiredUntilNextPrestige)).formatted(Formatting.GOLD)));
+ lines.add(Text.empty()
+ .append(Text.literal("Time until next prestige: ").formatted(Formatting.GRAY))
+ .append(formatTime(requiredUntilNextPrestige / totalCps)));
+ return true;
+ }
+
+ private static boolean addTimeTowerStatsToLore(List<Text> lines) {
+ if (totalCps < 0.0 || totalCpsMultiplier < 0.0 || timeTowerMultiplier < 0.0) return false;
+ lines.add(Text.literal("Current stats:").formatted(Formatting.GRAY));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(totalCps / totalCpsMultiplier * timeTowerMultiplier)).formatted(Formatting.GOLD)));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps : totalCps / totalCpsMultiplier * (timeTowerMultiplier + totalCpsMultiplier))).formatted(Formatting.GOLD)));
+ if (timeTowerMultiplier < 1.5) {
+ lines.add(Text.literal("Stats after upgrade:").formatted(Formatting.GRAY));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(totalCps / (totalCpsMultiplier) * (timeTowerMultiplier + 0.1))).formatted(Formatting.GOLD)));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps / totalCpsMultiplier * (totalCpsMultiplier + 0.1) : totalCps / totalCpsMultiplier * (timeTowerMultiplier + 0.1 + totalCpsMultiplier))).formatted(Formatting.GOLD)));
+ }
+ return true;
+ }
+
+ private static boolean addRabbitStatsToLore(List<Text> lines, ItemStack stack) {
+ if (cpsIncreaseFactors.isEmpty()) return false;
+ boolean changed = false;
+ for (Rabbit rabbit : cpsIncreaseFactors) {
+ if (rabbit.itemStack != stack) continue;
+ changed = true;
+ lines.add(Text.empty()
+ .append(Text.literal("CPS Increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
+
+ lines.add(Text.empty()
+ .append(Text.literal("Cost per CPS: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cost / rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
+
+ if (rabbit.itemStack == bestUpgrade) {
+ if (rabbit.cost <= totalChocolate) {
+ lines.add(Text.literal("Best upgrade").formatted(Formatting.GREEN));
+ } else {
+ lines.add(Text.literal("Best upgrade, can't afford").formatted(Formatting.YELLOW));
+ }
+ } else if (rabbit.itemStack == bestAffordableUpgrade && rabbit.cost <= totalChocolate) {
+ lines.add(Text.literal("Best upgrade you can afford").formatted(Formatting.GREEN));
+ }
+ }
+ return changed;
+ }
+
+ private static MutableText formatTime(double seconds) {
+ seconds = Math.ceil(seconds);
+ if (seconds <= 0) return Text.literal("Now").formatted(Formatting.GREEN);
+
+ StringBuilder builder = new StringBuilder();
+ if (seconds >= 86400) {
+ builder.append((int) (seconds / 86400)).append("d ");
+ seconds %= 86400;
+ }
+ if (seconds >= 3600) {
+ builder.append((int) (seconds / 3600)).append("h ");
+ seconds %= 3600;
+ }
+ if (seconds >= 60) {
+ builder.append((int) (seconds / 60)).append("m ");
+ seconds %= 60;
+ }
+ if (seconds >= 1) {
+ builder.append((int) seconds).append("s");
+ }
+ return Text.literal(builder.toString()).formatted(Formatting.GOLD);
+ }
+
+ /**
+ * Utility method.
+ */
+ private static String getConcatenatedLore(ItemStack item) {
+ return concatenateLore(ItemUtils.getLore(item));
+ }
+
+ /**
+ * Concatenates the lore of an item into one string.
+ * This is useful in case some pattern we're looking for is split into multiple lines, which would make it harder to regex.
+ */
+ private static String concatenateLore(List<Text> lore) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (int i = 0; i < lore.size(); i++) {
+ stringBuilder.append(lore.get(i).getString());
+ if (i != lore.size() - 1) stringBuilder.append(" ");
+ }
+ return stringBuilder.toString();
+ }
+
+ private static Optional<Rabbit> getCoach(ItemStack coachItem) {
+ if (!coachItem.isOf(Items.PLAYER_HEAD)) return Optional.empty();
+ String coachLore = getConcatenatedLore(coachItem);
+
+ if (totalCpsMultiplier == -1.0) return Optional.empty(); //We need the total multiplier to calculate the increase in cps.
+
+ Matcher multiplierIncreaseMatcher = MULTIPLIER_INCREASE_PATTERN.matcher(coachLore);
+ OptionalDouble currentCpsMultiplier = RegexUtils.getDoubleFromMatcher(multiplierIncreaseMatcher);
+ if (currentCpsMultiplier.isEmpty()) return Optional.empty();
+
+ OptionalDouble nextCpsMultiplier = RegexUtils.getDoubleFromMatcher(multiplierIncreaseMatcher);
+ if (nextCpsMultiplier.isEmpty()) { //This means that the coach isn't hired yet.
+ nextCpsMultiplier = currentCpsMultiplier; //So the first instance of the multiplier is actually the amount we'll get upon upgrading.
+ currentCpsMultiplier = OptionalDouble.of(0.0); //And so, we can re-assign values to the variables to make the calculation more readable.
+ }
+
+ Matcher costMatcher = COST_PATTERN.matcher(coachLore);
+ OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line
+ if (cost.isEmpty()) return Optional.empty();
+
+ return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), 42, coachItem));
+ }
+
+ private static Optional<Rabbit> getRabbit(ItemStack item, int slot) {
+ String lore = getConcatenatedLore(item);
+ Matcher cpsMatcher = CPS_INCREASE_PATTERN.matcher(lore);
+ OptionalInt currentCps = RegexUtils.getIntFromMatcher(cpsMatcher);
+ if (currentCps.isEmpty()) return Optional.empty();
+ OptionalInt nextCps = RegexUtils.getIntFromMatcher(cpsMatcher);
+ if (nextCps.isEmpty()) {
+ nextCps = currentCps; //This means that the rabbit isn't hired yet.
+ currentCps = OptionalInt.of(0); //So the first instance of the cps is actually the amount we'll get upon hiring.
+ }
+
+ Matcher costMatcher = COST_PATTERN.matcher(lore);
+ OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line
+ if (cost.isEmpty()) return Optional.empty();
+ return Optional.of(new Rabbit(nextCps.getAsInt() - currentCps.getAsInt(), cost.getAsInt(), slot, item));
+ }
+
+ private static Optional<ColorHighlight> getPrestigeHighlight(ItemStack item) {
+ List<Text> loreList = ItemUtils.getLore(item);
+ if (loreList.isEmpty()) return Optional.empty();
+
+ String lore = loreList.getLast().getString(); //The last line holds the text we're looking for
+ if (lore.equals("Click to prestige!")) return Optional.of(ColorHighlight.green(28));
+ return Optional.of(ColorHighlight.red(28));
+ }
+
+ private record Rabbit(double cpsIncrease, int cost, int slot, ItemStack itemStack) {
+ }
+
+ //Perhaps the part below can go to a separate file later on, but I couldn't find a proper name for the class, so they're staying here.
+ private static final Map<Character, Integer> romanMap = Map.of(
+ 'I', 1,
+ 'V', 5,
+ 'X', 10,
+ 'L', 50,
+ 'C', 100,
+ 'D', 500,
+ 'M', 1000
+ );
+
+ public static int romanToDecimal(String romanNumeral) {
+ int decimal = 0;
+ int lastNumber = 0;
+ for (int i = romanNumeral.length() - 1; i >= 0; i--) {
+ char ch = romanNumeral.charAt(i);
+ decimal = romanMap.get(ch) >= lastNumber ? decimal + romanMap.get(ch) : decimal - romanMap.get(ch);
+ lastNumber = romanMap.get(ch);
+ }
+ return decimal;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
new file mode 100644
index 00000000..c4fd7d4d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
@@ -0,0 +1,168 @@
+package de.hysky.skyblocker.skyblock.chocolatefactory;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.*;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
+import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.LinkedList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class EggFinder {
+ private static final Pattern eggFoundPattern = Pattern.compile("^(?:HOPPITY'S HUNT You found a Chocolate|You have already collected this Chocolate) (Breakfast|Lunch|Dinner)");
+ private static final Pattern newEggPattern = Pattern.compile("^HOPPITY'S HUNT A Chocolate (Breakfast|Lunch|Dinner) Egg has appeared!$");
+ private static final Logger logger = LoggerFactory.getLogger("Skyblocker Egg Finder");
+ private static final LinkedList<ArmorStandEntity> armorStandQueue = new LinkedList<>();
+ private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE};
+ private static boolean isLocationCorrect = false;
+
+ private EggFinder() {
+ }
+
+ public static void init() {
+ ClientPlayConnectionEvents.JOIN.register((ignored, ignored2, ignored3) -> invalidateState());
+ SkyblockEvents.LOCATION_CHANGE.register(EggFinder::handleLocationChange);
+ ClientReceiveMessageEvents.GAME.register(EggFinder::onChatMessage);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(EggFinder::renderWaypoints);
+ }
+
+ private static void handleLocationChange(Location location) {
+ for (Location possibleLocation : possibleLocations) {
+ if (location == possibleLocation) {
+ isLocationCorrect = true;
+ break;
+ }
+ }
+ if (!isLocationCorrect) {
+ armorStandQueue.clear();
+ return;
+ }
+
+ while (!armorStandQueue.isEmpty()) {
+ handleArmorStand(armorStandQueue.poll());
+ }
+ }
+
+ public static void checkIfEgg(Entity entity) {
+ if (entity instanceof ArmorStandEntity armorStand) checkIfEgg(armorStand);
+ }
+
+ public static void checkIfEgg(ArmorStandEntity armorStand) {
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return;
+ if (SkyblockTime.skyblockSeason.get() != SkyblockTime.Season.SPRING) return;
+ if (armorStand.hasCustomName() || !armorStand.isInvisible() || !ar