aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java77
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java256
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java81
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java68
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java279
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java100
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java23
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java194
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java115
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java37
16 files changed, 1355 insertions, 16 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java
new file mode 100644
index 00000000..2f616a1e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java
@@ -0,0 +1,77 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.mob.WitherSkeletonEntity;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Vec3d;
+
+import java.awt.*;
+
+public class ControlTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+
+ private static WitherSkeletonEntity correctWitherSkeleton;
+ private static Vec3d lastPos;
+ private static long lastUpdate;
+ private static Vec3d pingOffset;
+ private static Vec3d lastPingOffset;
+
+ protected static void reset() {
+ correctWitherSkeleton = null;
+ lastPos = null;
+ lastUpdate = -1;
+ pingOffset = null;
+ lastPingOffset = null;
+ }
+
+ /**
+ * Find the correct WitherSkeleton entity when it spawns to start tracking it
+ *
+ * @param entity spawned entity
+ */
+ protected static void onEntitySpawn(Entity entity) {
+ if (entity instanceof WitherSkeletonEntity witherSkeleton && correctWitherSkeleton == null) {
+ correctWitherSkeleton = witherSkeleton;
+ }
+ }
+
+ /**
+ * Finds where to look in 3 ticks effected by ping
+ */
+ protected static void update() {
+ if (correctWitherSkeleton != null) {
+ //smoothly adjust the ping throughout the test
+ if (lastPos != null) {
+ lastPingOffset = pingOffset;
+ double ping = DojoManager.ping / 1000d;
+ //find distance between last position and current position of skeleton
+ Vec3d movementVector = correctWitherSkeleton.getPos().subtract(lastPos).multiply(1, 0.1, 1);
+ //adjust the vector to current ping (multiply by 1 + time in second until the next update offset by the players ping)
+ pingOffset = movementVector.multiply(1 + 3 / 20d + ping);
+ }
+ lastPos = correctWitherSkeleton.getPos();
+ lastUpdate = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * Renders an outline around where the player should aim (assumes values are updated every 3 ticks)
+ *
+ * @param context render context
+ */
+ protected static void render(WorldRenderContext context) {
+ if (CLIENT.player != null && correctWitherSkeleton != null && pingOffset != null && lastPingOffset != null) {
+ float tickDelta = context.tickCounter().getTickDelta(false);
+ //how long until net update
+ double updatePercent = (double) (System.currentTimeMillis() - lastUpdate) / 150;
+ Vec3d aimPos = correctWitherSkeleton.getEyePos().add(pingOffset.multiply(updatePercent)).add(lastPingOffset.multiply(1 - updatePercent));
+ Box targetBox = new Box(aimPos.add(-0.5, -0.5, -0.5), aimPos.add(0.5, 0.5, 0.5));
+ boolean playerLookingAtBox = targetBox.raycast(CLIENT.player.getCameraPosVec(tickDelta), CLIENT.player.getCameraPosVec(tickDelta).add(CLIENT.player.getRotationVec(tickDelta).multiply(30))).isPresent();
+ float[] boxColor = playerLookingAtBox ? Color.GREEN.getColorComponents(new float[]{0, 0, 0}) : Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0});
+ RenderHelper.renderOutline(context, targetBox, boxColor, 3, true);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java
new file mode 100644
index 00000000..ab0a0781
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java
@@ -0,0 +1,66 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMaps;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import net.minecraft.client.MinecraftClient;
+
+import java.util.Map;
+import java.util.Objects;
+
+public class DisciplineTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+
+ /**
+ * Stores what sword is needed for the name of a zombie
+ */
+ private static final Map<String, String> SWORD_TO_NAME_LOOKUP = Map.of(
+ "WOOD_SWORD", "Wood",
+ "IRON_SWORD", "Iron",
+ "GOLD_SWORD", "Gold",
+ "DIAMOND_SWORD", "Diamond"
+ );
+
+ /**
+ * Stores a color related to the color of the sword: wood = brown, iron = silver, gold = gold, diamond = cyan
+ */
+ private static final Object2IntMap<String> SWORD_TO_COLOR_LOOKUP = Object2IntMaps.unmodifiable(new Object2IntOpenHashMap<>(Map.of(
+ "WOOD_SWORD", 0xa52a2a,
+ "IRON_SWORD", 0xc0c0c0,
+ "GOLD_SWORD", 0xffd700,
+ "DIAMOND_SWORD", 0x00ffff
+ )));
+
+ /**
+ * Works out if a zombie should glow based on its name and the currently held item by the player
+ *
+ * @param name name of the zombie to see if it should glow
+ * @return if the zombie should glow
+ */
+ protected static boolean shouldGlow(String name) {
+ if (CLIENT == null || CLIENT.player == null) {
+ return false;
+ }
+ String heldId = CLIENT.player.getMainHandStack().getSkyblockId();
+ if (heldId == null) {
+ return false;
+ }
+ return Objects.equals(SWORD_TO_NAME_LOOKUP.get(heldId), name);
+ }
+
+ /**
+ * gets the color linked to the currently held sword for zombies to glow
+ *
+ * @return color linked to sword
+ */
+ protected static int getColor() {
+ if (DojoManager.currentChallenge != DojoManager.DojoChallenges.DISCIPLINE || CLIENT == null || CLIENT.player == null) {
+ return 0;
+ }
+ String heldId = CLIENT.player.getMainHandStack().getSkyblockId();
+ if (heldId == null) {
+ return 0;
+ }
+ return SWORD_TO_COLOR_LOOKUP.getOrDefault(heldId, 0);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java
new file mode 100644
index 00000000..323c985c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java
@@ -0,0 +1,256 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents;
+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.fabricmc.fabric.api.event.player.AttackEntityCallback;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.network.packet.c2s.query.QueryPingC2SPacket;
+import net.minecraft.network.packet.s2c.play.ParticleS2CPacket;
+import net.minecraft.text.Text;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Util;
+import net.minecraft.util.hit.EntityHitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DojoManager {
+
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final String START_MESSAGE = "[NPC] Master Tao: Ahhh, here we go! Let's get you into the Arena.";
+ private static final Pattern TEST_OF_PATTERN = Pattern.compile("\\s+Test of (\\w+) OBJECTIVES");
+ private static final String CHALLENGE_FINISHED_REGEX = "\\s+CHALLENGE ((COMPLETED)|(FAILED))";
+
+
+ protected enum DojoChallenges {
+ NONE("none", enabled -> false),
+ FORCE("Force", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableForceHelper),
+ STAMINA("Stamina", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableStaminaHelper),
+ MASTERY("Mastery", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableMasteryHelper),
+ DISCIPLINE("Discipline", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableDisciplineHelper),
+ SWIFTNESS("Swiftness", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableSwiftnessHelper),
+ CONTROL("Control", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableControlHelper),
+ TENACITY("Tenacity", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableTenacityHelper);
+
+ private final String name;
+ private final Predicate<Boolean> enabled;
+
+ DojoChallenges(String name, Predicate<Boolean> enabled) {
+ this.name = name;
+ this.enabled = enabled;
+ }
+
+ public static DojoChallenges from(String name) {
+ return Arrays.stream(DojoChallenges.values()).filter(n -> name.equals(n.name)).findFirst().orElse(NONE);
+ }
+ }
+
+ protected static DojoChallenges currentChallenge = DojoChallenges.NONE;
+ public static boolean inArena = false;
+ protected static long ping = -1;
+
+ public static void init() {
+ ClientReceiveMessageEvents.GAME.register(DojoManager::onMessage);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(DojoManager::render);
+ ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset());
+ ClientEntityEvents.ENTITY_LOAD.register(DojoManager::onEntitySpawn);
+ ClientEntityEvents.ENTITY_UNLOAD.register(DojoManager::onEntityDespawn);
+ AttackEntityCallback.EVENT.register(DojoManager::onEntityAttacked);
+ Scheduler.INSTANCE.scheduleCyclic(DojoManager::update, 3);
+ }
+
+ private static void reset() {
+ inArena = false;
+ currentChallenge = DojoChallenges.NONE;
+ ForceTestHelper.reset();
+ StaminaTestHelper.reset();
+ MasteryTestHelper.reset();
+ SwiftnessTestHelper.reset();
+ ControlTestHelper.reset();
+ TenacityTestHelper.reset();
+ }
+
+ /**
+ * works out if the player is in dojo and if so what challenge based on chat messages
+ *
+ * @param text message
+ * @param overlay is overlay
+ */
+ private static void onMessage(Text text, Boolean overlay) {
+ if (!Utils.isInCrimson() || overlay) {
+ return;
+ }
+ if (Objects.equals(Formatting.strip(text.getString()), START_MESSAGE)) {
+ inArena = true;
+ //update the players ping
+ getPing();
+ return;
+ }
+ if (!inArena) {
+ return;
+ }
+ if (text.getString().matches(CHALLENGE_FINISHED_REGEX)) {
+ reset();
+ return;
+ }
+
+ //look for a message saying what challenge is starting if one has not already been found
+ if (currentChallenge != DojoChallenges.NONE) {
+ return;
+ }
+ Matcher nextChallenge = TEST_OF_PATTERN.matcher(text.getString());
+ if (nextChallenge.matches()) {
+ currentChallenge = DojoChallenges.from(nextChallenge.group(1));
+ if (!currentChallenge.enabled.test(true)) {
+ currentChallenge = DojoChallenges.NONE;
+ }
+ }
+ }
+
+ private static void getPing() {
+ ClientPlayNetworkHandler networkHandler = CLIENT.getNetworkHandler();
+ if (networkHandler != null) {
+ networkHandler.sendPacket(new QueryPingC2SPacket(Util.getMeasuringTimeMs()));
+ }
+ }
+
+ public static void onPingResult(long ping) {
+ DojoManager.ping = ping;
+ }
+
+ private static void update() {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ switch (currentChallenge) {
+ case STAMINA -> StaminaTestHelper.update();
+ case CONTROL -> ControlTestHelper.update();
+ }
+ }
+
+ /**
+ * called from the {@link de.hysky.skyblocker.skyblock.entity.MobGlow} class and checks the current challenge to see if zombies should be glowing
+ *
+ * @param name name of the zombie
+ * @return if the zombie should glow
+ */
+ public static boolean shouldGlow(String name) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return false;
+ }
+ return switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.shouldGlow(name);
+ case DISCIPLINE -> DisciplineTestHelper.shouldGlow(name);
+ default -> false;
+ };
+ }
+
+ /**
+ * called from the {@link de.hysky.skyblocker.skyblock.entity.MobGlow} class and checks the current challenge to see zombie outline color
+ *
+ * @return if the zombie should glow
+ */
+ public static int getColor() {
+ if (!Utils.isInCrimson() || !inArena) {
+ return 0xf57738;
+ }
+ return switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.getColor();
+ case DISCIPLINE -> DisciplineTestHelper.getColor();
+ default -> 0xf57738;
+ };
+ }
+
+ /**
+ * when a block is updated check the current challenge and send the packet to correct helper
+ *
+ * @param pos the location of the updated block
+ * @param state the state of the new block
+ */
+ public static void onBlockUpdate(BlockPos pos, BlockState state) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ switch (currentChallenge) {
+ case MASTERY -> MasteryTestHelper.onBlockUpdate(pos, state);
+ case SWIFTNESS -> SwiftnessTestHelper.onBlockUpdate(pos, state);
+ }
+ }
+
+ private static void onEntitySpawn(Entity entity, ClientWorld clientWorld) {
+ if (!Utils.isInCrimson() || !inArena || CLIENT == null || CLIENT.player == null) {
+ return;
+ }
+ // Check if within 50 blocks and 5 blocks vertically
+ if (entity.squaredDistanceTo(CLIENT.player) > 2500 || Math.abs(entity.getBlockY() - CLIENT.player.getBlockY()) > 5) {
+ return;
+ }
+ switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.onEntitySpawn(entity);
+ case CONTROL -> ControlTestHelper.onEntitySpawn(entity);
+ case TENACITY -> TenacityTestHelper.onEntitySpawn(entity);
+ }
+ }
+
+ private static void onEntityDespawn(Entity entity, ClientWorld clientWorld) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.onEntityDespawn(entity);
+ case TENACITY -> TenacityTestHelper.onEntityDespawn(entity);
+ }
+ }
+
+ private static ActionResult onEntityAttacked(PlayerEntity playerEntity, World world, Hand hand, Entity entity, EntityHitResult entityHitResult) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return ActionResult.PASS;
+ }
+ if (currentChallenge == DojoChallenges.FORCE) {
+ ForceTestHelper.onEntityAttacked(entity);
+ }
+ return ActionResult.PASS;
+ }
+
+ public static void onParticle(ParticleS2CPacket packet) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ if (currentChallenge == DojoChallenges.TENACITY) {
+ TenacityTestHelper.onParticle(packet);
+ }
+ }
+
+ private static void render(WorldRenderContext context) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.render(context);
+ case STAMINA -> StaminaTestHelper.render(context);
+ case MASTERY -> MasteryTestHelper.render(context);
+ case SWIFTNESS -> SwiftnessTestHelper.render(context);
+ case CONTROL -> ControlTestHelper.render(context);
+ case TENACITY -> TenacityTestHelper.render(context);
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java
new file mode 100644
index 00000000..70d6a401
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java
@@ -0,0 +1,81 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.mob.ZombieEntity;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.Vec3d;
+
+import java.awt.*;
+import java.text.DecimalFormat;
+import java.util.Map;
+
+public class ForceTestHelper {
+
+ private static final DecimalFormat FORMATTER = new DecimalFormat("0.0");
+ private static final int ZOMBIE_LIFE_TIME = 10100;
+
+ private static final Object2LongOpenHashMap<ZombieEntity> zombies = new Object2LongOpenHashMap<>();
+
+ protected static void reset() {
+ zombies.clear();
+ }
+
+ /**
+ * If a zombie value is negative make it glow
+ *
+ * @param name zombies value
+ * @return if the zombie should glow
+ */
+ protected static boolean shouldGlow(String name) {
+ return name.contains("-");
+ }
+
+ protected static int getColor() {
+ return Color.RED.getRGB();
+ }
+
+ protected static void onEntitySpawn(Entity entity) {
+ if (entity instanceof ZombieEntity zombie) {
+ zombies.put(zombie, System.currentTimeMillis() + ZOMBIE_LIFE_TIME);
+ }
+ }
+
+ protected static void onEntityAttacked(Entity entity) {
+ if (entity instanceof ZombieEntity zombie) {
+ if (zombies.containsKey(zombie)) {
+ zombies.put(zombie, System.currentTimeMillis() + ZOMBIE_LIFE_TIME); //timer is reset when they are hit
+ }
+ }
+ }
+
+ protected static void onEntityDespawn(Entity entity) {
+ if (entity instanceof ZombieEntity zombie) {
+ zombies.removeLong(zombie);
+ }
+ }
+
+ protected static void render(WorldRenderContext context) {
+ //render times
+ long currentTime = System.currentTimeMillis();
+ for (Map.Entry<ZombieEntity, Long> zombie : zombies.object2LongEntrySet()) {
+ float secondsTime = Math.max((zombie.getValue() - currentTime) / 1000f, 0);
+
+ MutableText text = Text.literal(FORMATTER.format(secondsTime));
+ if (secondsTime > 1) {
+ text = text.formatted(Formatting.GREEN);
+ } else if (secondsTime > 0) {
+ text = text.formatted(Formatting.YELLOW);
+ } else {
+ text = text.formatted(Formatting.RED);
+ }
+
+ Vec3d labelPos = zombie.getKey().getCameraPosVec(context.tickCounter().getTickDelta(false));
+ RenderHelper.renderText(context, text, labelPos, 1.5f, true);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java
new file mode 100644
index 00000000..625b91eb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java
@@ -0,0 +1,68 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
+
+import java.awt.*;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MasteryTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final DecimalFormat FORMATTER = new DecimalFormat("0.00");
+ /**
+ * How long it takes for a block to turn red
+ */
+ private static final int BLOCK_LIFE_TIME = 6550;
+
+ private static final List<BlockPos> blockOrder = new ArrayList<>();
+ private static final Object2LongOpenHashMap<BlockPos> endTimes = new Object2LongOpenHashMap<>();
+
+ protected static void reset() {
+ blockOrder.clear();
+ endTimes.clear();
+ }
+
+ protected static void onBlockUpdate(BlockPos pos, BlockState state) {
+ if (CLIENT == null || CLIENT.player == null) {
+ return;
+ }
+ if (state.isOf(Blocks.LIME_WOOL)) {
+ blockOrder.add(pos);
+ //add lifetime of a block to the time to get time when block expires
+ // work out how long it will take between the player firing and arrow hitting the block and to subtract from time
+ long travelTime = (long) (CLIENT.player.getPos().distanceTo(pos.toCenterPos()) * 1000 / 60); //an arrow speed is about 60 blocks a second from a full draw
+ endTimes.put(pos, System.currentTimeMillis() + BLOCK_LIFE_TIME - DojoManager.ping - travelTime);
+ }
+ if (state.isAir()) {
+ blockOrder.remove(pos);
+ endTimes.removeLong(pos);
+ }
+ }
+
+ protected static void render(WorldRenderContext context) {
+ //render connecting lines
+ if (!blockOrder.isEmpty()) {
+ RenderHelper.renderLineFromCursor(context, blockOrder.getFirst().toCenterPos(), Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}), 1f, 2);
+ }
+ if (blockOrder.size() >= 2) {
+ RenderHelper.renderLinesFromPoints(context, new Vec3d[]{blockOrder.get(0).toCenterPos(), blockOrder.get(1).toCenterPos()}, Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}), 1, 2, false);
+ }
+
+ //render times
+ long currentTime = System.currentTimeMillis();
+ for (BlockPos pos : blockOrder) {
+ long blockEndTime = endTimes.getLong(pos);
+ float secondsTime = Math.max((blockEndTime - currentTime) / 1000f, 0);
+ RenderHelper.renderText(context, Text.literal(FORMATTER.format(secondsTime)), pos.add(0, 1, 0).toCenterPos(), 3, true);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java
new file mode 100644
index 00000000..3f7dfe56
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java
@@ -0,0 +1,279 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.math.Vec3i;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StaminaTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final int WALL_THRESHOLD_VALUE = 13;
+ private static final int WALL_HEIGHT = 5;
+ private static final float[] INCOMING_COLOR = new float[]{0f, 1f, 0f, 0f};
+ private static final float[] OUTGOING_COLOR = new float[]{1f, 0.64f, 0f, 0f};
+
+ private static final List<Box> wallHoles = new ArrayList<>();
+ private static final List<Box> lastHoles = new ArrayList<>();
+ private static final Map<Box, HoleDirection> holeDirections = new HashMap<>();
+ private static BlockPos middleBase;
+
+ private enum HoleDirection {
+ POSITIVE_X,
+ POSITIVE_Z,
+ NEGATIVE_X,
+ NEGATIVE_Z,
+ NEW,
+ UNCHANGED
+ }
+
+ protected static void reset() {
+ wallHoles.clear();
+ lastHoles.clear();
+ holeDirections.clear();
+ middleBase = null;
+ }
+
+ protected static void update() {
+
+ //search the world around the player for walls 30 x 10 x 30 area centered on player
+
+ List<BlockPos> currentBottomWallLocations = findWallBlocks();
+ if (currentBottomWallLocations == null) { //stop here if the center pos has not been found
+ return;
+ }
+ //find walls
+ List<Box> walls = findWalls(currentBottomWallLocations);
+
+ //find air then holes and add whole to list
+ lastHoles.clear();
+ lastHoles.addAll(wallHoles);
+ wallHoles.clear();
+ for (Box wall : walls) {
+ wallHoles.addAll(findHolesInBox(wall));
+ }
+ // get direction for the holes
+ Map<Box, HoleDirection> lastHoleDirections = new HashMap<>(holeDirections);
+ holeDirections.clear();
+ for (Box hole : wallHoles) {
+ HoleDirection holeDirection = getWholeDirection(hole);
+ if (holeDirection == HoleDirection.UNCHANGED) {
+ holeDirections.put(hole, lastHoleDirections.get(hole));
+ continue;
+ }
+ holeDirections.put(hole, holeDirection);
+ }
+ }
+
+ /**
+ * Locates the center of the game and once this is found scans the bottom of room for blocks that make up the walls
+ *
+ * @return list of blocks that make up the bottom of the walls
+ */
+ private static List<BlockPos> findWallBlocks() {
+ if (CLIENT == null || CLIENT.player == null || CLIENT.world == null) {
+ return null;
+ }
+ BlockPos playerPos = CLIENT.player.getBlockPos();
+ //find the center first before starting to look for walls
+ if (middleBase == null) {
+ for (int x = playerPos.getX() - 10; x < playerPos.getX() + 10; x++) {
+ for (int y = playerPos.getY() - 5; y < playerPos.getY(); y++) {
+ for (int z = playerPos.getZ() - 10; z < playerPos.getZ() + 10; z++) {
+ BlockPos pos = new BlockPos(x, y, z);
+ BlockState state = CLIENT.world.getBlockState(pos);
+ if (state.isOf(Blocks.CHISELED_STONE_BRICKS)) {
+ middleBase = pos;
+ return null;
+ }
+ }
+ }
+ }
+ return null;
+ }
+ List<BlockPos> currentBottomWallLocations = new ArrayList<>();
+ for (int x = middleBase.getX() - 15; x < middleBase.getX() + 15; x++) {
+ for (int z = middleBase.getZ() - 15; z < middleBase.getZ() + 15; z++) {
+ BlockPos pos = new BlockPos(x, middleBase.getY() + 1, z);
+ BlockState state = CLIENT.world.getBlockState(pos);
+ //find the bottom of walls
+ if (!state.isAir()) {
+ currentBottomWallLocations.add(pos);
+ }
+ }
+ }
+ return currentBottomWallLocations;
+ }
+
+ private static List<Box> findWalls(List<BlockPos> currentBottomWallLocations) {
+ Int2ObjectOpenHashMap<List<BlockPos>> possibleWallsX = new Int2ObjectOpenHashMap<>();
+ Int2ObjectOpenHashMap<List<BlockPos>> possibleWallsZ = new Int2ObjectOpenHashMap<>();
+ for (BlockPos block : currentBottomWallLocations) {
+ //add to the x walls
+ int x = block.getX();
+ if (!possibleWallsX.containsKey(x)) {
+ possibleWallsX.put(x, new ArrayList<>());
+
+ }
+ possibleWallsX.get(x).add(block);
+ //add to the z walls
+ int z = block.getZ();
+ if (!possibleWallsZ.containsKey(z)) {
+ possibleWallsZ.put(z, new ArrayList<>());
+ }
+ possibleWallsZ.get(z).add(block);
+ }
+
+ //extract only the lines that are long enough to be a wall and not from walls overlapping
+ List<List<BlockPos>> walls = new ArrayList<>();
+ for (List<BlockPos> line : possibleWallsX.values()) {
+ if (line.size() >= WALL_THRESHOLD_VALUE) {
+ walls.add(line);
+ }
+ }
+ for (List<BlockPos> line : possibleWallsZ.values()) {
+ if (line.size() >= WALL_THRESHOLD_VALUE) {
+ walls.add(line);
+ }
+ }
+
+ //final find the maximum values for each wall to output a box for them
+ List<Box> wallBoxes = new ArrayList<>();
+ for (List<BlockPos> wall : walls) {
+ BlockPos minPos = wall.getFirst();
+ BlockPos maxPos = wall.getFirst();
+ for (BlockPos pos : wall) {
+ if (pos.getX() < minPos.getX()) {
+ minPos = new BlockPos(pos.getX(), minPos.getY(), minPos.getZ());
+ }
+ if (pos.getZ() < minPos.getZ()) {
+ minPos = new BlockPos(minPos.getX(), minPos.getY(), pos.getZ());
+ }
+
+ if (pos.getX() > maxPos.getX()) {
+ maxPos = new BlockPos(pos.getX(), maxPos.getY(), maxPos.getZ());
+ }
+ if (pos.getZ() > maxPos.getZ()) {
+ maxPos = new BlockPos(maxPos.getX(), maxPos.getY(), pos.getZ());
+ }
+ }
+ //expand wall to top
+ maxPos = new BlockPos(maxPos.getX(), maxPos.getY() + WALL_HEIGHT, maxPos.getZ());
+
+ wallBoxes.add(Box.enclosing(minPos, maxPos));
+ }
+
+ return wallBoxes;
+ }
+
+ private static List<Box> findHolesInBox(Box box) {
+ List<Box> holes = new ArrayList<>();
+ if (CLIENT == null || CLIENT.player == null || CLIENT.world == null) {
+ return holes;
+ }
+ //get the direction vector
+ Vec3i wallDirection = box.getLengthX() == 1 ? new Vec3i(0, 0, 1) : new Vec3i(1, 0, 0);
+ //find the corners of boxes (only need 3)
+ List<BlockPos> topLeft = new ArrayList<>();
+ List<BlockPos> topRight = new ArrayList<>();
+ List<BlockPos> bottomLeft = new ArrayList<>();
+ for (int z = (int) box.minZ; z < box.maxZ; z++) {
+ for (int x = (int) box.minX; x < box.maxX; x++) {
+ for (int y = (int) box.minY; y < box.maxY; y++) {
+ BlockPos pos = new BlockPos(x, y, z);
+ BlockState state = CLIENT.world.getBlockState(pos);
+ if (!state.isAir()) {
+ //do not check non-air
+ continue;
+ }
+ boolean top = y == box.maxY - 1 || !CLIENT.world.getBlockState(pos.add(0, 1, 0)).isAir();
+ boolean bottom = !CLIENT.world.getBlockState(pos.add(0, -1, 0)).isAir();
+ boolean left = !CLIENT.world.getBlockState(pos.add(wallDirection)).isAir();
+ boolean right = !CLIENT.world.getBlockState(pos.subtract(wallDirection)).isAir();
+ if (top) {
+ if (left) {
+ topLeft.add(pos);
+ }
+ if (right) {
+ topRight.add(pos);
+ }
+ }
+ if (bottom && left) {
+ bottomLeft.add(pos);
+ }
+
+ }
+ }
+ }
+ // gets box around top of hole then expands to the bottom of hole
+ for (int i = 0; i < topLeft.size(); i++) {
+ if (topRight.size() <= i || bottomLeft.size() <= i) {
+ //if corners can not be found end looking
+ break;
+ }
+ Box hole = Box.enclosing(topLeft.get(i), topRight.get(i));
+ hole = hole.stretch(0, bottomLeft.get(i).getY() - topLeft.get(i).getY(), 0);
+ holes.add(hole);
+ }
+ return holes;
+ }
+
+ private static HoleDirection getWholeDirection(Box hole) {
+ //the value has not changed since last time
+ if (lastHoles.contains(hole)) {
+ return HoleDirection.UNCHANGED;
+ }
+ //check each direction to work out which way the whole is going
+ Box posX = hole.offset(1, 0, 0);
+ if (lastHoles.contains(posX)) {
+ return HoleDirection.POSITIVE_X;
+ }
+ Box negX = hole.offset(-1, 0, 0);
+ if (lastHoles.contains(negX)) {
+ return HoleDirection.NEGATIVE_X;
+ }
+ Box posZ = hole.offset(0, 0, 1);
+ if (lastHoles.contains(posZ)) {
+ return HoleDirection.POSITIVE_Z;
+ }
+ Box negZ = hole.offset(0, 0, -1);
+ if (lastHoles.contains(negZ)) {
+ return HoleDirection.NEGATIVE_Z;
+ }
+ // if pos can not be found mark as new
+ return HoleDirection.NEW;
+
+ }
+
+ protected static void render(WorldRenderContext context) {
+ if (wallHoles.isEmpty() || CLIENT == null || CLIENT.player == null) {
+ return;
+ }
+ BlockPos playerPos = CLIENT.player.getBlockPos();
+ for (Box hole : wallHoles) {
+ float[] color = isHoleIncoming(hole, holeDirections.get(hole), playerPos) ? INCOMING_COLOR : OUTGOING_COLOR;
+ RenderHelper.renderFilled(context, new BlockPos((int) hole.minX, (int) hole.minY, (int) hole.minZ), new Vec3d(hole.getLengthX(), hole.getLengthY(), hole.getLengthZ()), color, 0.3f, false);
+ }
+ }
+
+ private static boolean isHoleIncoming(Box holePos, HoleDirection holeDirection, BlockPos playerPos) {
+ return switch (holeDirection) {
+ case POSITIVE_X -> playerPos.getX() < holePos.minX;
+ case POSITIVE_Z -> playerPos.getZ() < holePos.minZ;
+ case NEGATIVE_X -> playerPos.getX() > holePos.maxX;
+ case NEGATIVE_Z -> playerPos.getZ() > holePos.maxZ;
+
+ default -> true;
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java
new file mode 100644
index 00000000..678005c4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.util.math.BlockPos;
+
+public class SwiftnessTestHelper {
+
+ private static BlockPos lastBlock;
+
+ protected static void reset() {
+ lastBlock = null;
+ }
+
+ protected static void onBlockUpdate(BlockPos pos, BlockState state) {
+ if (state.isOf(Blocks.LIME_WOOL)) {
+ lastBlock = pos.toImmutable();
+ }
+ }
+
+ /**
+ * Renders a green block around the newest block
+ *
+ * @param context render context
+ */
+ protected static void render(WorldRenderContext context) {
+ if (lastBlock == null) {
+ return;
+ }
+ RenderHelper.renderFilled(context, lastBlock, new float[]{0f, 1f, 0f}, 0.5f, true);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java
new file mode 100644
index 00000000..51e99fbd
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java
@@ -0,0 +1,100 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.network.packet.s2c.play.ParticleS2CPacket;
+import net.minecraft.particle.ParticleTypes;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.HitResult;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.RaycastContext;
+
+public class TenacityTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+
+ private static final Object2ObjectOpenHashMap<ArmorStandEntity, Vec3d> fireBallsWithStartPos = new Object2ObjectOpenHashMap<>();
+ private static final Object2ObjectOpenHashMap<ArmorStandEntity, Vec3d> particleOffsets = new Object2ObjectOpenHashMap<>();
+
+ protected static void reset() {
+ fireBallsWithStartPos.clear();
+ particleOffsets.clear();
+ }
+
+ protected static void render(WorldRenderContext context) {
+ for (ArmorStandEntity fireball : fireBallsWithStartPos.keySet()) {
+ Vec3d lineStart = fireBallsWithStartPos.get(fireball).add(particleOffsets.getOrDefault(fireball, Vec3d.ZERO));
+ Vec3d fireballPos = fireball.getPos().add(particleOffsets.getOrDefault(fireball, Vec3d.ZERO));
+
+ Vec3d distance = fireballPos.subtract(lineStart);
+ if (distance.length() > 0.02) { //if big enough gap try from start calculate and show trajectory
+ distance = distance.multiply(100);
+ Vec3d lineEnd = lineStart.add(distance);
+
+ RenderHelper.renderLinesFromPoints(context, new Vec3d[]{lineStart, lineEnd}, new float[]{1f, 0f, 0f}, 1, 3, false);
+
+ //get highlighted block
+ HitResult hitResult = raycast(lineStart, lineEnd, fireball);
+ if (hitResult != null && hitResult.getType() == HitResult.Type.BLOCK && hitResult instanceof BlockHitResult blockHitResult) {
+ RenderHelper.renderFilled(context, blockHitResult.getBlockPos(), new float[]{1f, 0f, 0f}, 0.5f, false);
+ }
+ }
+ }
+ }
+
+ protected static HitResult raycast(Vec3d start, Vec3d end, ArmorStandEntity fireball) {
+ if (CLIENT == null || CLIENT.world == null) {
+ return null;
+ }
+ return CLIENT.world.raycast(new RaycastContext(start, end, RaycastContext.ShapeType.OUTLINE, RaycastContext.FluidHandling.ANY, fireball));
+ }
+
+ /**
+ * If a spawned entity is an armour stand add it to the fireballs map (assuming all armour stands are fireballs)
+ *
+ * @param entity spawned entity
+ */
+ protected static void onEntitySpawn(Entity entity) {
+ if (entity instanceof ArmorStandEntity armorStand) {
+ fireBallsWithStartPos.put(armorStand, armorStand.getPos());
+ }
+ }
+
+ protected static void onEntityDespawn(Entity entity) {
+ if (entity instanceof ArmorStandEntity armorStand) {
+ fireBallsWithStartPos.remove(armorStand);
+ }
+ }
+
+ /**
+ * Uses the particles spawned with the fireballs to offset from the armour stand position to get a more accurate guess of where it's going
+ *
+ * @param packet particle packet
+ */
+ protected static void onParticle(ParticleS2CPacket packet) {
+ if (!ParticleTypes.FLAME.equals(packet.getParameters().getType())) {
+ return;
+ }
+ //get nearest fireball to particle
+ Vec3d particlePos = new Vec3d(packet.getX(), packet.getY(), packet.getZ());
+ ArmorStandEntity neareastFireball = null;
+ double clostestDistance = 50;
+ for (ArmorStandEntity fireball : fireBallsWithStartPos.keySet()) {
+ double distance = fireball.getPos().distanceTo(particlePos);
+ if (distance < clostestDistance) {
+ neareastFireball = fireball;
+ clostestDistance = distance;
+ }
+ }
+ if (neareastFireball == null) { //can not find fireball near particle
+ return;
+ }
+ //adjust fireball offset with particle pos
+ Vec3d delta = particlePos.subtract(neareastFireball.getPos());
+ //update values
+ particleOffsets.put(neareastFireball, delta);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java
index d6f9410b..81e328ca 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java
@@ -1,6 +1,7 @@
package de.hysky.skyblocker.skyblock.entity;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
import de.hysky.skyblocker.skyblock.dungeon.LividColor;
import de.hysky.skyblocker.skyblock.end.TheEnd;
import de.hysky.skyblocker.utils.ItemUtils;
@@ -10,6 +11,7 @@ import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
import net.minecraft.entity.Entity;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.entity.mob.EndermanEntity;
+import net.minecraft.entity.mob.ZombieEntity;
import net.minecraft.entity.passive.BatEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
@@ -28,6 +30,7 @@ public class MobGlow {
if (OcclusionCulling.getReducedCuller().isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
String name = entity.getName().getString();
+
// Dungeons
if (Utils.isInDungeons() && !entity.isInvisible()) {
return switch (entity) {
@@ -46,6 +49,7 @@ public class MobGlow {
};
}
+
return switch (entity) {
// Rift
case PlayerEntity p when Utils.isInTheRift() && !entity.isInvisible() && name.equals("Blobbercyst ") -> SkyblockerConfigManager.get().otherLocations.rift.blobbercystGlow;
@@ -57,6 +61,9 @@ public class MobGlow {
// Special Zelot
case EndermanEntity enderman when Utils.isInTheEnd() && !entity.isInvisible() -> TheEnd.isSpecialZealot(enderman);
+ //dojo
+ case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.shouldGlow(getArmorStandName(zombie));
+
default -> false;
};
}
@@ -66,6 +73,7 @@ public class MobGlow {
/**
* Checks if an entity is starred by checking if its armor stand contains a star in its name.
+ *
* @param entity the entity to check.
* @return true if the entity is starred, false otherwise
*/
@@ -74,6 +82,20 @@ public class MobGlow {
return !armorStands.isEmpty() && armorStands.getFirst().getName().getString().contains("✯");
}
+ /**
+ * Returns name of entity by finding closed armor stand and getting name of that
+ *
+ * @param entity the entity to check
+ * @return the name string of the entities label
+ */
+ public static String getArmorStandName(Entity entity) {
+ List<ArmorStandEntity> armorStands = getArmorStands(entity);
+ if (armorStands.isEmpty()) {
+ return "";
+ }
+ return armorStands.getFirst().getName().getString();
+ }
+
public static List<ArmorStandEntity> getArmorStands(Entity entity) {
return getArmorStands(entity.getWorld(), entity.getBoundingBox());
}
@@ -94,6 +116,7 @@ public class MobGlow {
case EndermanEntity enderman when TheEnd.isSpecialZealot(enderman) -> Formatting.RED.getColorValue();
case ArmorStandEntity armorStand when isNukekubiHead(armorStand) -> 0x990099;
+ case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.getColor();
default -> 0xf57738;
};
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java
new file mode 100644
index 00000000..42a52a85
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java
@@ -0,0 +1,194 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.serialization.Codec;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.mixins.accessors.SlotAccessor;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.inventory.SimpleInventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.StringNbtReader;
+import net.minecraft.nbt.visitor.StringNbtWriter;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.util.Identifier;
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * Opened here {@code de.hysky.skyblocker.mixins.MinecraftClientMixin#skyblocker$skyblockInventoryScreen}
+ * <br>
+ * Book button is moved here {@code de.hysky.skyblocker.mixins.InventoryScreenMixin#skyblocker}
+ */
+public class SkyblockInventoryScreen extends InventoryScreen {
+ private static final Logger LOGGER = LoggerFactory.getLogger("Equipment");
+ private static final Supplier<ItemStack[]> EMPTY_EQUIPMENT = () -> new ItemStack[]{ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY};
+ public static final ItemStack[] equipment = EMPTY_EQUIPMENT.get();
+ public static final ItemStack[] equipment_rift = EMPTY_EQUIPMENT.get();
+ private static final Codec<ItemStack[]> CODEC = ItemUtils.EMPTY_ALLOWING_ITEMSTACK_CODEC.listOf(4, 8) // min size at 4 for backwards compat
+ .xmap(itemStacks -> itemStacks.toArray(ItemStack[]::new), List::of).fieldOf("items").codec();
+
+ private static final Identifier SLOT_TEXTURE = Identifier.ofVanilla("container/slot");
+ private static final Identifier EMPTY_SLOT = Identifier.of(SkyblockerMod.NAMESPACE, "equipment/empty_icon");
+ private static final Path FOLDER = SkyblockerMod.CONFIG_DIR.resolve("equipment");
+
+ private final Slot[] equipmentSlots = new Slot[4];
+
+ private static void save(String profileId) {
+ try {
+ Files.createDirectories(FOLDER);
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to create folder for equipment!", e);
+ }
+ Path resolve = FOLDER.resolve(profileId + ".nbt");
+
+ try (BufferedWriter writer = Files.newBufferedWriter(resolve)) {
+
+ writer.write(new StringNbtWriter().apply(CODEC.encodeStart(NbtOps.INSTANCE, ArrayUtils.addAll(equipment, equipment_rift)).getOrThrow()));
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to save Equipment data", e);
+ }
+ }
+
+ private static void load(String profileId) {
+ Path resolve = FOLDER.resolve(profileId + ".nbt");
+ CompletableFuture.supplyAsync(() -> {
+ try (BufferedReader reader = Files.newBufferedReader(resolve)) {
+ return CODEC.parse(NbtOps.INSTANCE, StringNbtReader.parse(reader.lines().collect(Collectors.joining()))).getOrThrow();
+ } catch (NoSuchFileException ignored) {
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to load Equipment data", e);
+ }
+ return EMPTY_EQUIPMENT.get();
+ // Schedule on main thread to avoid any async weirdness
+ }).thenAccept(itemStacks -> MinecraftClient.getInstance().execute(() -> {
+ System.arraycopy(itemStacks, 0, equipment, 0, Math.min(itemStacks.length, 4));
+ if (itemStacks.length <= 4) return;
+ System.arraycopy(itemStacks, 4, equipment_rift, 0, Math.clamp(itemStacks.length - 4, 0, 4));
+ }));
+ }
+
+ public static void initEquipment() {
+
+ SkyblockEvents.PROFILE_CHANGE.register(((prevProfileId, profileId) -> {
+ if (!prevProfileId.isEmpty()) CompletableFuture.runAsync(() -> save(prevProfileId)).thenRun(() -> load(profileId));
+ else load(profileId);
+ }));
+
+ ClientLifecycleEvents.CLIENT_STOPPING.register(client1 -> {
+ String profileId = Utils.getProfileId();
+ if (!profileId.isBlank()) {
+ CompletableFuture.runAsync(() -> save(profileId));
+ }
+ });
+ }
+
+ public SkyblockInventoryScreen(PlayerEntity player) {
+ super(player);
+ SimpleInventory inventory = new SimpleInventory(Utils.isInTheRift() ? equipment_rift: equipment);
+
+ Slot slot = handler.slots.get(45);
+ ((SlotAccessor) slot).setX(slot.x + 21);
+ for (int i = 0; i < 4; i++) {
+ equipmentSlots[i] = new EquipmentSlot(inventory, i, 77, 8 + i * 18);
+ }
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ for (Slot equipmentSlot : equipmentSlots) {
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("/equipment");
+ return true;
+ }
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ /**
+ * Draws the equipment slots in the foreground layer after vanilla slots are drawn
+ * in {@link net.minecraft.client.gui.screen.ingame.HandledScreen#render(DrawContext, int, int, float) HandledScreen#render(DrawContext, int, int, float)}.
+ */
+ @Override
+ protected void drawForeground(DrawContext context, int mouseX, int mouseY) {
+ for (Slot equipmentSlot : equipmentSlots) {
+ drawSlot(context, equipmentSlot);
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) drawSlotHighlight(context, equipmentSlot.x, equipmentSlot.y, 0);
+ }
+
+ super.drawForeground(context, mouseX, mouseY);
+ }
+
+ @Override
+ protected void drawMouseoverTooltip(DrawContext context, int x, int y) {
+ super.drawMouseoverTooltip(context, x, y);
+ if (!handler.getCursorStack().isEmpty()) return;
+ for (Slot equipmentSlot : equipmentSlots) {
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, x, y) && equipmentSlot.hasStack()) {
+ ItemStack itemStack = equipmentSlot.getStack();
+ context.drawTooltip(this.textRenderer, this.getTooltipFromItem(itemStack), itemStack.getTooltipData(), x, y);
+ }
+ }
+ }
+
+ @Override
+ public void removed() {
+ super.removed();
+ // put the handler back how it was, the handler is the same while the player is alive/in the same world
+ Slot slot = handler.slots.get(45);
+ ((SlotAccessor) slot).setX(slot.x - 21);
+ }
+
+ @Override
+ protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {
+ super.drawBackground(context, delta, mouseX, mouseY);
+ for (int i = 0; i < 4; i++) {
+ context.drawGuiTexture(SLOT_TEXTURE, x + 76 + (i == 3 ? 21 : 0), y + 7 + i * 18, 18, 18);
+ }
+ }
+
+ @Override
+ protected void drawSlot(DrawContext context, Slot slot) {
+ super.drawSlot(context, slot);
+ if (slot instanceof EquipmentSlot && !slot.hasStack()) {
+ context.drawGuiTexture(EMPTY_SLOT, slot.x, slot.y, 16, 16);
+ }
+ }
+
+ private static class EquipmentSlot extends Slot {
+
+ public EquipmentSlot(Inventory inventory, int index, int x, int y) {
+ super(inventory, index, x, y);
+ }
+
+ @Override
+ public boolean canTakeItems(PlayerEntity playerEntity) {
+ return false;
+ }
+
+ @Override
+ public boolean canInsert(ItemStack stack) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
index 49d170b9..955ebc87 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
@@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.item.tooltip;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.GeneralConfig;
+import de.hysky.skyblocker.skyblock.item.tooltip.adders.CraftPriceTooltip;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
@@ -129,6 +130,8 @@ public class ItemTooltip {
LOGGER.error("Encountered unknown error while downloading tooltip data", e);
return null;
});
+
+ CraftPriceTooltip.clearCache();
}, 1200, true);
}
} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
index d82b2682..92adf49d 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
@@ -1,29 +1,23 @@
package de.hysky.skyblocker.skyblock.item.tooltip;
-import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
-import com.google.gson.JsonPrimitive;
-import com.google.gson.stream.JsonReader;
import de.hysky.skyblocker.SkyblockerMod;
-import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.GeneralConfig;
import de.hysky.skyblocker.utils.Http;
import de.hysky.skyblocker.utils.Utils;
+import org.jetbrains.annotations.Nullable;
-import java.io.StringReader;
import java.net.http.HttpHeaders;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Predicate;
-import org.jetbrains.annotations.Nullable;
-
public enum TooltipInfoType implements Runnable {
NPC("https://hysky.de/api/npcprice", itemTooltip -> itemTooltip.enableNPCPrice, true),
- BAZAAR("https://hysky.de/api/bazaar", itemTooltip -> itemTooltip.enableBazaarPrice || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableBazaarPrice, false),
- LOWEST_BINS("https://hysky.de/api/auctions/lowestbins", itemTooltip -> itemTooltip.enableLowestBIN || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableLowestBIN, false),
+ BAZAAR("https://hysky.de/api/bazaar", itemTooltip -> itemTooltip.enableBazaarPrice || itemTooltip.enableCraftingCost.getOrder() != null || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableBazaarPrice, false),
+ LOWEST_BINS("https://hysky.de/api/auctions/lowestbins", itemTooltip -> itemTooltip.enableLowestBIN || itemTooltip.enableCraftingCost.getOrder() != null || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableLowestBIN, false),
ONE_DAY_AVERAGE("https://hysky.de/api/auctions/lowestbins/average/1day.json", itemTooltip -> itemTooltip.enableAvgBIN, false),
THREE_DAY_AVERAGE("https://hysky.de/api/auctions/lowestbins/average/3day.json", itemTooltip -> itemTooltip.enableAvgBIN || SkyblockerConfigManager.get().uiAndVisuals.searchOverlay.enableAuctionHouse, itemTooltip -> itemTooltip.enableAvgBIN, false),
MOTES("https://hysky.de/api/motesprice", itemTooltip -> itemTooltip.enableMotesPrice, itemTooltip -> itemTooltip.enableMotesPrice && Utils.isInTheRift(), true),
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java
index e3a2ef04..cb8efb0c 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java
@@ -3,6 +3,7 @@ package de.hysky.skyblocker.skyblock.item.tooltip;
import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor;
import de.hysky.skyblocker.skyblock.chocolatefactory.ChocolateFactorySolver;
import de.hysky.skyblocker.skyblock.item.tooltip.adders.*;
+import de.hysky.skyblocker.skyblock.item.tooltip.adders.CraftPriceTooltip;
import de.hysky.skyblocker.utils.Utils;
import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
@@ -27,12 +28,13 @@ public class TooltipManager {
new BazaarPriceTooltip(2),
new LBinTooltip(3),
new AvgBinTooltip(4),
- new DungeonQualityTooltip(5),
- new MotesTooltip(6),
- new ObtainedDateTooltip(7),
- new MuseumTooltip(8),
- new ColorTooltip(9),
- new AccessoryTooltip(10),
+ new CraftPriceTooltip(5),
+ new DungeonQualityTooltip(6),
+ new MotesTooltip(7),
+ new ObtainedDateTooltip(8),
+ new MuseumTooltip(9),
+ new ColorTooltip(10),
+ new AccessoryTooltip(11),
};
private static final ArrayList<TooltipAdder> currentScreenAdders = new ArrayList<>();
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java
new file mode 100644
index 00000000..f7af446e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java
@@ -0,0 +1,115 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.config.configs.GeneralConfig;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import de.hysky.skyblocker.utils.NEURepoManager;
+import io.github.moulberry.repo.data.NEUIngredient;
+import io.github.moulberry.repo.data.NEUItem;
+import io.github.moulberry.repo.data.NEURecipe;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class CraftPriceTooltip extends TooltipAdder {
+ protected static final Logger LOGGER = LoggerFactory.getLogger(CraftPriceTooltip.class.getName());
+ private static final Map<String, Double> cachedCraftCosts = new ConcurrentHashMap<>();
+ private static final int MAX_RECURSION_DEPTH = 15;
+
+ public CraftPriceTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSloFt, ItemStack stack, List<Text> lines) {
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableCraftingCost == GeneralConfig.Craft.OFF) return;
+
+ String internalID = stack.getSkyblockId();
+ if (stack.getNeuName() == null || internalID == null) return;
+
+ if (TooltipInfoType.LOWEST_BINS.getData() == null || TooltipInfoType.BAZAAR.getData() == null) {
+ ItemTooltip.nullWarning();
+ return;
+ }
+
+ NEUItem neuItem = NEURepoManager.NEU_REPO.getItems().getItemBySkyblockId(internalID);
+ if (neuItem == null) return;
+
+ List<NEURecipe> neuRecipes = neuItem.getRecipes();
+ if (neuRecipes.isEmpty() || neuRecipes.getFirst() instanceof io.github.moulberry.repo.data.NEUKatUpgradeRecipe) return;
+
+ try {
+ double totalCraftCost = getItemCost(neuRecipes.getFirst(), 0);
+
+ if (totalCraftCost == 0) return;
+
+ int amountInStack;
+ if (lines.get(1).getString().endsWith("Sack")) {
+ String line = lines.get(3).getSiblings().get(1).getString().replace(",", "");
+ amountInStack = NumberUtils.isParsable(line) && !line.equals("0") ? Integer.parseInt(line) : stack.getCount();
+ } else amountInStack = stack.getCount();
+
+ neuRecipes.getFirst().getAllOutputs().stream().findFirst().ifPresent(outputIngredient ->
+ lines.add(Text.literal(String.format("%-20s", "Crafting Price:")).formatted(Formatting.GOLD)
+ .append(ItemTooltip.getCoinsMessage(totalCraftCost / outputIngredient.getAmount(), amountInStack))));
+
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Craft Price] Error calculating craftprice tooltip for: " + internalID, e);
+ }
+ }
+
+ private double getItemCost(NEURecipe recipe, int depth) {
+ if (depth >= MAX_RECURSION_DEPTH) return -1;
+
+ double totalCraftCost = 0;
+ for (NEUIngredient input : recipe.getAllInputs()) {
+ String inputItemName = input.getItemId();
+ double inputItemCount = input.getAmount();
+ if (cachedCraftCosts.containsKey(inputItemName)) {
+ totalCraftCost += cachedCraftCosts.get(inputItemName) * inputItemCount;
+ continue;
+ }
+
+ double itemCost = 0;
+
+ if (TooltipInfoType.BAZAAR.getData().has(inputItemName)) {
+ itemCost = TooltipInfoType.BAZAAR.getData().getAsJsonObject(inputItemName).get(SkyblockerConfigManager.get().general.itemTooltip.enableCraftingCost.getOrder()).getAsDouble();
+ } else if (TooltipInfoType.LOWEST_BINS.getData().has(inputItemName)) {
+ itemCost = TooltipInfoType.LOWEST_BINS.getData().get(inputItemName).getAsDouble();
+ }
+
+ if (itemCost > 0) {
+ cachedCraftCosts.put(inputItemName, itemCost);
+ }
+
+ NEUItem neuItem = NEURepoManager.NEU_REPO.getItems().getItemBySkyblockId(inputItemName);
+ if (neuItem != null) {
+ List<NEURecipe> neuRecipes = neuItem.getRecipes();
+ if (!neuRecipes.isEmpty()) {
+ double craftCost = getItemCost(neuRecipes.getFirst(), depth + 1);
+ if (craftCost != -1) itemCost = Math.min(itemCost, craftCost);
+ cachedCraftCosts.put(inputItemName, itemCost);
+ }
+ }
+
+ totalCraftCost += itemCost * inputItemCount;
+ }
+ return totalCraftCost;
+ }
+
+ public static void clearCache() {
+ cachedCraftCosts.clear();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java
index 672201d5..d556c9b2 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java
@@ -7,6 +7,7 @@ import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.math.NumberUtils;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@@ -20,9 +21,18 @@ public class NpcPriceTooltip extends TooltipAdder {
public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
final String internalID = stack.getSkyblockId();
if (internalID != null && TooltipInfoType.NPC.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ int amount;
+ if (lines.get(1).getString().endsWith("Sack")) {
+ //The amount is in the 2nd sibling of the 3rd line of the lore. here V
+ //Example line: empty[style={color=dark_purple,!italic}, siblings=[literal{Stored: }[style={color=gray}], literal{0}[style={color=dark_gray}], literal{/20k}[style={color=gray}]]
+ String line = lines.get(3).getSiblings().get(1).getString().replace(",", "");
+ amount = NumberUtils.isParsable(line) && !line.equals("0") ? Integer.parseInt(line) : stack.getCount();
+ } else {
+ amount = stack.getCount();
+ }
lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:"))
.formatted(Formatting.YELLOW)
- .append(ItemTooltip.getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), stack.getCount())));
+ .append(ItemTooltip.getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), amount)));
}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java
new file mode 100644
index 00000000..7131a567
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java
@@ -0,0 +1,37 @@
+package de.hysky.skyblocker.skyblock.mayors;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.HoverEvent;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public final class JerryTimer {
+ private JerryTimer() {
+ }
+ public static void init() {
+ //Example message: "§b ☺ §eThere is a §aGreen Jerry§e!"
+ //There are various formats, all of which start with the "§b ☺ " prefix and contain the word "<color> Jerry"
+ ClientReceiveMessageEvents.GAME.register((message, overlay) -> {
+ if (overlay || !Utils.getMayor().equals("Jerry") || !SkyblockerConfigManager.get().helpers.jerry.enableJerryTimer) return;
+ String text = message.getString();
+ //This part of hypixel still uses legacy text formatting, so we can't directly check for the actual text
+ if (!text.startsWith("§b ☺ ") || !text.contains("Jerry")) return;
+ HoverEvent hoverEvent = message.getStyle().getHoverEvent();
+ if (hoverEvent == null || hoverEvent.getAction() != HoverEvent.Action.SHOW_TEXT) return;
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ Scheduler.INSTANCE.schedule(() -> {
+ if (player == null || !Utils.isOnSkyblock()) return;
+ player.sendMessage(Constants.PREFIX.get().append(Text.literal("Jerry cooldown is over!")).formatted(Formatting.GREEN), false);
+ player.playSoundToPlayer(SoundEvents.ENTITY_VILLAGER_TRADE, SoundCategory.NEUTRAL, 100f, 1.0f);
+ }, 20*60*6); // 6 minutes
+ });
+ }
+}