aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java64
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java23
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java22
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java77
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java256
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java81
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java68
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java279
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java100
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java23
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java3
-rw-r--r--src/main/resources/assets/skyblocker/lang/en_us.json16
-rw-r--r--src/main/resources/skyblocker.mixins.json2
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",