diff options
18 files changed, 1144 insertions, 1 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index 05c7fb1e..c3b0e0f2 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -11,6 +11,7 @@ import de.hysky.skyblocker.skyblock.chat.ChatRuleAnnouncementScreen; import de.hysky.skyblocker.skyblock.chat.ChatRulesHandler; import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder; import de.hysky.skyblocker.skyblock.chocolatefactory.TimeTowerReminder; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra; import de.hysky.skyblocker.skyblock.dungeon.*; import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen; @@ -181,6 +182,7 @@ public class SkyblockerMod implements ClientModInitializer { ApiUtils.init(); Debug.init(); Kuudra.init(); + DojoManager.init(); RenderHelper.init(); FancyStatusBars.init(); EventNotifications.init(); diff --git a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java index fed8fa96..e8127a0e 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java @@ -81,6 +81,68 @@ public class CrimsonIsleCategory { .controller(IntegerFieldControllerBuilder::create) .build()) .build()) - .build(); + //dojo + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo")) + .collapsed(false) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.forceHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.forceHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableForceHelper, + () -> config.crimsonIsle.dojo.enableForceHelper, + newValue -> config.crimsonIsle.dojo.enableForceHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.staminaHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.staminaHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableStaminaHelper, + () -> config.crimsonIsle.dojo.enableStaminaHelper, + newValue -> config.crimsonIsle.dojo.enableStaminaHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.masteryHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.masteryHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableMasteryHelper, + () -> config.crimsonIsle.dojo.enableMasteryHelper, + newValue -> config.crimsonIsle.dojo.enableMasteryHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.disciplineHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.disciplineHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableDisciplineHelper, + () -> config.crimsonIsle.dojo.enableDisciplineHelper, + newValue -> config.crimsonIsle.dojo.enableDisciplineHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.swiftnessHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.swiftnessHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableSwiftnessHelper, + () -> config.crimsonIsle.dojo.enableSwiftnessHelper, + newValue -> config.crimsonIsle.dojo.enableSwiftnessHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.controlHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.controlHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableControlHelper, + () -> config.crimsonIsle.dojo.enableControlHelper, + newValue -> config.crimsonIsle.dojo.enableControlHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.tenacityHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.tenacityHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableTenacityHelper, + () -> config.crimsonIsle.dojo.enableTenacityHelper, + newValue -> config.crimsonIsle.dojo.enableTenacityHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + + .build(); } } diff --git a/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java index 8dd93aee..451d1983 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java @@ -7,6 +7,10 @@ public class CrimsonIsleConfig { @SerialEntry public Kuudra kuudra = new Kuudra(); + @SerialEntry + public Dojo dojo = new Dojo(); + + public static class Kuudra { @SerialEntry public boolean supplyWaypoints = true; @@ -32,4 +36,27 @@ public class CrimsonIsleConfig { @SerialEntry public int arrowPoisonThreshold = 32; } + + public static class Dojo { + @SerialEntry + public boolean enableForceHelper = true; + + @SerialEntry + public boolean enableStaminaHelper = true; + + @SerialEntry + public boolean enableMasteryHelper = true; + + @SerialEntry + public boolean enableDisciplineHelper = true; + + @SerialEntry + public boolean enableSwiftnessHelper = true; + + @SerialEntry + public boolean enableControlHelper = true; + + @SerialEntry + public boolean enableTenacityHelper = true; + } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java index 48389d40..7bbbac81 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java @@ -6,6 +6,7 @@ import com.llamalad7.mixinextras.sugar.Local; import de.hysky.skyblocker.skyblock.CompactDamage; import de.hysky.skyblocker.skyblock.FishingHelper; import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.end.BeaconHighlighter; @@ -97,6 +98,7 @@ public abstract class ClientPlayNetworkHandlerMixin { @Inject(method = "onParticle", at = @At("RETURN")) private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) { MythologicalRitual.onParticle(packet); + DojoManager.onParticle(packet); EnderNodes.onParticle(packet); } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java new file mode 100644 index 00000000..6c10e5d2 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java @@ -0,0 +1,23 @@ +package de.hysky.skyblocker.mixins; + + +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.block.BlockState; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientWorld.class) +public class ClientWorldMixin { + + @Inject(method = "handleBlockUpdate", at = @At("RETURN")) + private void skyblocker$handleBlockUpdate(BlockPos pos, BlockState state, int flags, CallbackInfo ci) { + if (Utils.isInCrimson()) { + DojoManager.onBlockUpdate(pos.toImmutable(), state); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java new file mode 100644 index 00000000..a9fae752 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java @@ -0,0 +1,22 @@ +package de.hysky.skyblocker.mixins; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.network.PingMeasurer; +import net.minecraft.util.profiler.MultiValueDebugSampleLogImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(PingMeasurer.class) +public class PingMeasurerMixin { + + @WrapOperation(method = "onPingResult", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/MultiValueDebugSampleLogImpl;push(J)V")) + private void skyblocker$onPingResult(MultiValueDebugSampleLogImpl log, long ping, Operation<Void> operation) { + if (Utils.isInCrimson()) { + DojoManager.onPingResult(ping); + } + operation.call(log, ping); + } +} 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/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index 7ee5ec38..dad7ec02 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -120,6 +120,9 @@ public class Utils { public static boolean isInKuudra() { return location == Location.KUUDRAS_HOLLOW; } + public static boolean isInCrimson() { + return location == Location.CRIMSON_ISLE; + } public static boolean isInModernForagingIsland() { return location == Location.MODERN_FORAGING_ISLAND; diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 9cc25ca0..8fa87e3b 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -894,6 +894,22 @@ "skyblocker.fancyAuctionHouse.yourAuction": "This is your auction!", "skyblocker.fancyAuctionHouse.youPay": "You pay: %s", + "skyblocker.crimson.dojo": "Dojo", + "skyblocker.crimson.dojo.forceHelper": "Enable Force Helper", + "skyblocker.crimson.dojo.forceHelper.@Tooltip": "Shows timer showing how long until a zombie despawns and outlines negative zombies.", + "skyblocker.crimson.dojo.staminaHelper": "Enable Stamina Helper", + "skyblocker.crimson.dojo.staminaHelper.@Tooltip": "Highlights the holes in the walls turning orange once you have been through a wall.", + "skyblocker.crimson.dojo.masteryHelper": "Enable Mastery Helper", + "skyblocker.crimson.dojo.masteryHelper.@Tooltip": "Shows count down to when to relase the bow and a path to follow.", + "skyblocker.crimson.dojo.disciplineHelper": "Enable Discipline Helper", + "skyblocker.crimson.dojo.disciplineHelper.@Tooltip": "Outlines the zombies to attack with currently held sword.", + "skyblocker.crimson.dojo.swiftnessHelper": "Enable Swiftness Helper", + "skyblocker.crimson.dojo.swiftnessHelper.@Tooltip": "highlights the newest wool block to go to.", + "skyblocker.crimson.dojo.controlHelper": "Enable Control Helper", + "skyblocker.crimson.dojo.controlHelper.@Tooltip": "enders an outline around where to aim.", + "skyblocker.crimson.dojo.tenacityHelper": "Enable Tenacity Helper", + "skyblocker.crimson.dojo.tenacityHelper.@Tooltip": "Shows a path for each fireball and predicted block they are going to hit.", + "skyblocker.crimson.kuudra.noArrowPoison": "No Arrow Poison!", "skyblocker.crimson.kuudra.lowArrowPoison": "Low on Arrow Poison!", diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index 27755550..685758f7 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -8,6 +8,7 @@ "BatEntityMixin", "ClientPlayerEntityMixin", "ClientPlayNetworkHandlerMixin", + "ClientWorldMixin", "CommandTreeS2CPacketMixin", "ComponentHolderMixin", "DataTrackerMixin", @@ -26,6 +27,7 @@ "LivingEntityRendererMixin", "MinecraftClientMixin", "MouseMixin", + "PingMeasurerMixin", "PlayerInventoryMixin", "PlayerListHudMixin", "PlayerSkinProviderMixin", |