diff options
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock')
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 + }); + } +} |