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);
+