aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java64
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java7
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java16
-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/GenericContainerScreenHandlerMixin.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java12
-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/skyblock/item/SkyblockInventoryScreen.java194
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java37
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java7
-rw-r--r--src/main/resources/assets/skyblocker/lang/en_us.json23
-rw-r--r--src/main/resources/assets/skyblocker/lang/lol_us.json3
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.pngbin0 -> 173 bytes
-rw-r--r--src/main/resources/skyblocker.mixins.json2
31 files changed, 1489 insertions, 4 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index 05c7fb1e..f1eb4321 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;
@@ -36,6 +37,7 @@ import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview;
import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
import de.hysky.skyblocker.skyblock.item.tooltip.TooltipManager;
import de.hysky.skyblocker.skyblock.itemlist.ItemRepository;
+import de.hysky.skyblocker.skyblock.mayors.JerryTimer;
import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen;
import de.hysky.skyblocker.skyblock.rift.TheRift;
import de.hysky.skyblocker.skyblock.searchoverlay.SearchOverManager;
@@ -181,8 +183,10 @@ public class SkyblockerMod implements ClientModInitializer {
ApiUtils.init();
Debug.init();
Kuudra.init();
+ DojoManager.init();
RenderHelper.init();
FancyStatusBars.init();
+ SkyblockInventoryScreen.initEquipment();
EventNotifications.init();
containerSolverManager.init();
statusBarTracker.init();
@@ -192,6 +196,7 @@ public class SkyblockerMod implements ClientModInitializer {
EggFinder.init();
TimeTowerReminder.init();
SkyblockTime.init();
+ JerryTimer.init();
TooltipManager.init();
SlotTextManager.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/categories/HelperCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
index 9e4935cb..1518dfe7 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
@@ -38,6 +38,20 @@ public class HelperCategory {
.build())
.build())
+ //Jerry Timer
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.jerry"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.jerry.enableJerryTimer"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.jerry.enableJerryTimer.@Tooltip")))
+ .binding(defaults.helpers.jerry.enableJerryTimer,
+ () -> config.helpers.jerry.enableJerryTimer,
+ newValue -> config.helpers.jerry.enableJerryTimer = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
//Experiments Solver
.group(OptionGroup.createBuilder()
.name(Text.translatable("skyblocker.config.helpers.experiments"))
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
index a2a0f815..889b253a 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
@@ -68,6 +68,13 @@ public class UIAndVisualsCategory {
newValue -> config.uiAndVisuals.hideStatusEffectOverlay = newValue)
.controller(ConfigUtils::createBooleanController)
.build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.showEquipmentInInventory"))
+ .binding(defaults.uiAndVisuals.showEquipmentInInventory,
+ () -> config.uiAndVisuals.showEquipmentInInventory,
+ newValue -> config.uiAndVisuals.showEquipmentInInventory = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
//Chest Value FIXME change dropdown to color controller
.group(OptionGroup.createBuilder()
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/config/configs/HelperConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
index c0314924..33047f1c 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
@@ -12,6 +12,9 @@ public class HelperConfig {
public MythologicalRitual mythologicalRitual = new MythologicalRitual();
@SerialEntry
+ public Jerry jerry = new Jerry();
+
+ @SerialEntry
public Experiments experiments = new Experiments();
@SerialEntry
@@ -28,6 +31,11 @@ public class HelperConfig {
public boolean enableMythologicalRitualHelper = true;
}
+ public static class Jerry {
+ @SerialEntry
+ public boolean enableJerryTimer = false;
+ }
+
public static class Experiments {
@SerialEntry
public boolean enableChronomatronSolver = true;
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
index e016988b..80bdb1c9 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
@@ -28,6 +28,9 @@ public class UIAndVisualsConfig {
public boolean hideStatusEffectOverlay = false;
@SerialEntry
+ public boolean showEquipmentInInventory = true;
+
+ @SerialEntry
public ChestValue chestValue = new ChestValue();
@SerialEntry
diff --git a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java
index c268103d..93426143 100644
--- a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java
+++ b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java
@@ -26,6 +26,16 @@ public final class SkyblockEvents {
}
});
+ /**
+ * Called when the player's Skyblock profile changes.
+ * @implNote This is called upon receiving the chat message for the profile change rather than the exact moment of profile change, so it may be delayed by a few seconds.
+ */
+ public static final Event<ProfileChange> PROFILE_CHANGE = EventFactory.createArrayBacked(ProfileChange.class, callbacks -> (prev, profile) -> {
+ for (ProfileChange callback : callbacks) {
+ callback.onSkyblockProfileChange(prev, profile);
+ }
+ });
+
@Environment(EnvType.CLIENT)
@FunctionalInterface
public interface SkyblockJoin {
@@ -43,4 +53,10 @@ public final class SkyblockEvents {
public interface SkyblockLocationChange {
void onSkyblockLocationChange(Location location);
}
+
+ @Environment(EnvType.CLIENT)
+ @FunctionalInterface
+ public interface ProfileChange {
+ void onSkyblockProfileChange(String prevProfileId, String profileId);
+ }
}
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/GenericContainerScreenHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java
index f75af09a..3c3dbd52 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java
@@ -2,7 +2,11 @@ package de.hysky.skyblocker.mixins;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen;
+import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen;
+import de.hysky.skyblocker.utils.Utils;
import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.GenericContainerScreenHandler;
import net.minecraft.screen.ScreenHandler;
@@ -22,8 +26,21 @@ public abstract class GenericContainerScreenHandlerMixin extends ScreenHandler {
public void setStackInSlot(int slot, int revision, ItemStack stack) {
super.setStackInSlot(slot, revision, stack);
SkyblockerMod.getInstance().containerSolverManager.markDirty();
- if (MinecraftClient.getInstance().currentScreen instanceof PartyFinderScreen screen) {
- screen.markDirty();
+
+ Screen currentScreen = MinecraftClient.getInstance().currentScreen;
+ switch (currentScreen) {
+ case PartyFinderScreen screen -> screen.markDirty();
+ case GenericContainerScreen screen when screen.getTitle().getString().toLowerCase().contains("equipment") -> {
+ int line = slot/9;
+ if (line > 0 && line < 5 && slot % 9 == 1) {
+ boolean empty = stack.getName().getString().trim().toLowerCase().startsWith("empty");
+ if (Utils.isInTheRift())
+ SkyblockInventoryScreen.equipment_rift[line - 1] = empty ? ItemStack.EMPTY : stack;
+ else
+ SkyblockInventoryScreen.equipment[line - 1] = empty ? ItemStack.EMPTY : stack;
+ }
+ }
+ case null, default -> {}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java
index 0d833c22..2194e7a8 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java
@@ -1,11 +1,16 @@
package de.hysky.skyblocker.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.itemlist.ItemListWidget;
import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.gui.screen.ButtonTextures;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.TexturedButtonWidget;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@@ -15,4 +20,16 @@ public abstract class InventoryScreenMixin {
private RecipeBookWidget skyblocker$replaceRecipeBook(RecipeBookWidget original) {
return SkyblockerConfigManager.get().general.itemList.enableItemList && Utils.isOnSkyblock() ? new ItemListWidget() : original;
}
+
+ @WrapOperation(method = "init", at = @At(value = "NEW", target = "(IIIILnet/minecraft/client/gui/screen/ButtonTextures;Lnet/minecraft/client/gui/widget/ButtonWidget$PressAction;)Lnet/minecraft/client/gui/widget/TexturedButtonWidget;"))
+ private TexturedButtonWidget skyblocker$moveButton(int x, int y, int width, int height, ButtonTextures textures, ButtonWidget.PressAction pressAction, Operation<TexturedButtonWidget> original) {
+ if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) return original.call(x, y, width, height, textures, pressAction);
+ return new TexturedButtonWidget(x + 21, y, width, height, textures, pressAction);
+ }
+
+ @WrapOperation(method = "method_19891", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/ButtonWidget;setPosition(II)V"))
+ private void skyblocker$moveButtonWhenPressed(ButtonWidget instance, int i, int j, Operation<Void> original) {
+ if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) original.call(instance, i, j);
+ else instance.setPosition(i + 21, j);
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java b/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java
index b04f958f..f91ddc86 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java
@@ -1,7 +1,11 @@
package de.hysky.skyblocker.mixins;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.item.HotbarSlotLock;
import de.hysky.skyblocker.skyblock.item.ItemProtection;
+import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen;
import de.hysky.skyblocker.utils.JoinWorldPlaceholderScreen;
import de.hysky.skyblocker.utils.ReconfiguringPlaceholderScreen;
import de.hysky.skyblocker.utils.Utils;
@@ -9,8 +13,10 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.DownloadingTerrainScreen;
import net.minecraft.client.gui.screen.ReconfiguringScreen;
import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.entity.player.PlayerEntity;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -54,4 +60,10 @@ public abstract class MinecraftClientMixin {
private Screen modifyJoinWorld(Screen screen) {
return Utils.isOnSkyblock() ? new JoinWorldPlaceholderScreen() : screen;
}
+
+ @WrapOperation(method = "handleInputEvents", at = @At(value = "NEW", target = "(Lnet/minecraft/entity/player/PlayerEntity;)Lnet/minecraft/client/gui/screen/ingame/InventoryScreen;"))
+ private InventoryScreen skyblocker$skyblockInventoryScreen(PlayerEntity player, Operation<InventoryScreen> original) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) return new SkyblockInventoryScreen(player);
+ else return original.call(player);
+ }
} \ No newline at end of file
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/skyblock/item/SkyblockInventoryScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java
new file mode 100644
index 00000000..42a52a85
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java
@@ -0,0 +1,194 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.serialization.Codec;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.mixins.accessors.SlotAccessor;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.inventory.SimpleInventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.StringNbtReader;
+import net.minecraft.nbt.visitor.StringNbtWriter;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.util.Identifier;
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * Opened here {@code de.hysky.skyblocker.mixins.MinecraftClientMixin#skyblocker$skyblockInventoryScreen}
+ * <br>
+ * Book button is moved here {@code de.hysky.skyblocker.mixins.InventoryScreenMixin#skyblocker}
+ */
+public class SkyblockInventoryScreen extends InventoryScreen {
+ private static final Logger LOGGER = LoggerFactory.getLogger("Equipment");
+ private static final Supplier<ItemStack[]> EMPTY_EQUIPMENT = () -> new ItemStack[]{ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY};
+ public static final ItemStack[] equipment = EMPTY_EQUIPMENT.get();
+ public static final ItemStack[] equipment_rift = EMPTY_EQUIPMENT.get();
+ private static final Codec<ItemStack[]> CODEC = ItemUtils.EMPTY_ALLOWING_ITEMSTACK_CODEC.listOf(4, 8) // min size at 4 for backwards compat
+ .xmap(itemStacks -> itemStacks.toArray(ItemStack[]::new), List::of).fieldOf("items").codec();
+
+ private static final Identifier SLOT_TEXTURE = Identifier.ofVanilla("container/slot");
+ private static final Identifier EMPTY_SLOT = Identifier.of(SkyblockerMod.NAMESPACE, "equipment/empty_icon");
+ private static final Path FOLDER = SkyblockerMod.CONFIG_DIR.resolve("equipment");
+
+ private final Slot[] equipmentSlots = new Slot[4];
+
+ private static void save(String profileId) {
+ try {
+ Files.createDirectories(FOLDER);
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to create folder for equipment!", e);
+ }
+ Path resolve = FOLDER.resolve(profileId + ".nbt");
+
+ try (BufferedWriter writer = Files.newBufferedWriter(resolve)) {
+
+ writer.write(new StringNbtWriter().apply(CODEC.encodeStart(NbtOps.INSTANCE, ArrayUtils.addAll(equipment, equipment_rift)).getOrThrow()));
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to save Equipment data", e);
+ }
+ }
+
+ private static void load(String profileId) {
+ Path resolve = FOLDER.resolve(profileId + ".nbt");
+ CompletableFuture.supplyAsync(() -> {
+ try (BufferedReader reader = Files.newBufferedReader(resolve)) {
+ return CODEC.parse(NbtOps.INSTANCE, StringNbtReader.parse(reader.lines().collect(Collectors.joining()))).getOrThrow();
+ } catch (NoSuchFileException ignored) {
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to load Equipment data", e);
+ }
+ return EMPTY_EQUIPMENT.get();
+ // Schedule on main thread to avoid any async weirdness
+ }).thenAccept(itemStacks -> MinecraftClient.getInstance().execute(() -> {
+ System.arraycopy(itemStacks, 0, equipment, 0, Math.min(itemStacks.length, 4));
+ if (itemStacks.length <= 4) return;
+ System.arraycopy(itemStacks, 4, equipment_rift, 0, Math.clamp(itemStacks.length - 4, 0, 4));
+ }));
+ }
+
+ public static void initEquipment() {
+
+ SkyblockEvents.PROFILE_CHANGE.register(((prevProfileId, profileId) -> {
+ if (!prevProfileId.isEmpty()) CompletableFuture.runAsync(() -> save(prevProfileId)).thenRun(() -> load(profileId));
+ else load(profileId);
+ }));
+
+ ClientLifecycleEvents.CLIENT_STOPPING.register(client1 -> {
+ String profileId = Utils.getProfileId();
+ if (!profileId.isBlank()) {
+ CompletableFuture.runAsync(() -> save(profileId));
+ }
+ });
+ }
+
+ public SkyblockInventoryScreen(PlayerEntity player) {
+ super(player);
+ SimpleInventory inventory = new SimpleInventory(Utils.isInTheRift() ? equipment_rift: equipment);
+
+ Slot slot = handler.slots.get(45);
+ ((SlotAccessor) slot).setX(slot.x + 21);
+ for (int i = 0; i < 4; i++) {
+ equipmentSlots[i] = new EquipmentSlot(inventory, i, 77, 8 + i * 18);
+ }
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ for (Slot equipmentSlot : equipmentSlots) {
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("/equipment");
+ return true;
+ }
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ /**
+ * Draws the equipment slots in the foreground layer after vanilla slots are drawn
+ * in {@link net.minecraft.client.gui.screen.ingame.HandledScreen#render(DrawContext, int, int, float) HandledScreen#render(DrawContext, int, int, float)}.
+ */
+ @Override
+ protected void drawForeground(DrawContext context, int mouseX, int mouseY) {
+ for (Slot equipmentSlot : equipmentSlots) {
+ drawSlot(context, equipmentSlot);
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) drawSlotHighlight(context, equipmentSlot.x, equipmentSlot.y, 0);
+ }
+
+ super.drawForeground(context, mouseX, mouseY);
+ }
+
+ @Override
+ protected void drawMouseoverTooltip(DrawContext context, int x, int y) {
+ super.drawMouseoverTooltip(context, x, y);
+ if (!handler.getCursorStack().isEmpty()) return;
+ for (Slot equipmentSlot : equipmentSlots) {
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, x, y) && equipmentSlot.hasStack()) {
+ ItemStack itemStack = equipmentSlot.getStack();
+ context.drawTooltip(this.textRenderer, this.getTooltipFromItem(itemStack), itemStack.getTooltipData(), x, y);
+ }
+ }
+ }
+
+ @Override
+ public void removed() {
+ super.removed();
+ // put the handler back how it was, the handler is the same while the player is alive/in the same world
+ Slot slot = handler.slots.get(45);
+ ((SlotAccessor) slot).setX(slot.x - 21);
+ }
+
+ @Override
+ protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {
+ super.drawBackground(context, delta, mouseX, mouseY);
+ for (int i = 0; i < 4; i++) {
+ context.drawGuiTexture(SLOT_TEXTURE, x + 76 + (i == 3 ? 21 : 0), y + 7 + i * 18, 18, 18);
+ }
+ }
+
+ @Override
+ protected void drawSlot(DrawContext context, Slot slot) {
+ super.drawSlot(context, slot);
+ if (slot instanceof EquipmentSlot && !slot.hasStack()) {
+ context.drawGuiTexture(EMPTY_SLOT, slot.x, slot.y, 16, 16);
+ }
+ }
+
+ private static class EquipmentSlot extends Slot {
+
+ public EquipmentSlot(Inventory inventory, int index, int x, int y) {
+ super(inventory, index, x, y);
+ }
+
+ @Override
+ public boolean canTakeItems(PlayerEntity playerEntity) {
+ return false;
+ }
+
+ @Override
+ public boolean canInsert(ItemStack stack) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
index c7ea17dc..d94d6405 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
@@ -48,7 +48,7 @@ public class CommunityShopAdder extends SlotTextAdder {
String lastLine = lore.getLast().getString();
return List.of(SlotText.bottomLeft(switch (lastLine) {
case "Maxed out!" -> Text.literal("Max").withColor(0xfab387);
- case "Currently upgrading!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD);
+ case "Currently upgrading!", "Click to instantly upgrade!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD);
case "Click to claim!" -> Text.literal("✅").withColor(0xa6e3a1).formatted(Formatting.BOLD);
default -> Text.literal(String.valueOf(RomanNumerals.romanToDecimal(roman))).withColor(0xcba6f7);
}));
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java
new file mode 100644
index 00000000..7131a567
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java
@@ -0,0 +1,37 @@
+package de.hysky.skyblocker.skyblock.mayors;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.HoverEvent;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public final class JerryTimer {
+ private JerryTimer() {
+ }
+ public static void init() {
+ //Example message: "§b ☺ §eThere is a §aGreen Jerry§e!"
+ //There are various formats, all of which start with the "§b ☺ " prefix and contain the word "<color> Jerry"
+ ClientReceiveMessageEvents.GAME.register((message, overlay) -> {
+ if (overlay || !Utils.getMayor().equals("Jerry") || !SkyblockerConfigManager.get().helpers.jerry.enableJerryTimer) return;
+ String text = message.getString();
+ //This part of hypixel still uses legacy text formatting, so we can't directly check for the actual text
+ if (!text.startsWith("§b ☺ ") || !text.contains("Jerry")) return;
+ HoverEvent hoverEvent = message.getStyle().getHoverEvent();
+ if (hoverEvent == null || hoverEvent.getAction() != HoverEvent.Action.SHOW_TEXT) return;
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ Scheduler.INSTANCE.schedule(() -> {
+ if (player == null || !Utils.isOnSkyblock()) return;
+ player.sendMessage(Constants.PREFIX.get().append(Text.literal("Jerry cooldown is over!")).formatted(Formatting.GREEN), false);
+ player.playSoundToPlayer(SoundEvents.ENTITY_VILLAGER_TRADE, SoundCategory.NEUTRAL, 100f, 1.0f);
+ }, 20*60*6); // 6 minutes
+ });
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java
index 0b18ed9d..051110b2 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;
@@ -465,7 +468,11 @@ public class Utils {
if (message.startsWith(PROFILE_MESSAGE_PREFIX)) {
profile = message.substring(PROFILE_MESSAGE_PREFIX.length()).split("§b")[0];
} else if (message.startsWith(PROFILE_ID_PREFIX)) {
+ String prevProfileId = profileId;
profileId = message.substring(PROFILE_ID_PREFIX.length());
+ if (!prevProfileId.equals(profileId)) {
+ SkyblockEvents.PROFILE_CHANGE.invoker().onSkyblockProfileChange(prevProfileId, profileId);
+ }
MuseumItemCache.tick(profileId);
}
diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json
index 9cc25ca0..bab98150 100644
--- a/src/main/resources/assets/skyblocker/lang/en_us.json
+++ b/src/main/resources/assets/skyblocker/lang/en_us.json
@@ -317,6 +317,11 @@
"skyblocker.config.helpers.fishing.hideOtherPlayers": "Hide Other Players Rods",
"skyblocker.config.helpers.fishing.hideOtherPlayers.@Tooltip": "Hide other players fishing rods from showing for you",
+ "skyblocker.config.helpers.jerry": "Jerry",
+ "skyblocker.config.helpers.jerry.enableJerryTimer": "Enable Jerry Timer",
+ "skyblocker.config.helpers.jerry.enableJerryTimer.@Tooltip": "Sends a message in chat and plays a sound when the hidden jerry spawn cooldown is over.",
+
+
"skyblocker.config.helpers.mythologicalRitual": "Mythological Ritual Helper",
"skyblocker.config.helpers.mythologicalRitual.enableMythologicalRitualHelper": "Enable Mythological Ritual Helper",
@@ -610,6 +615,8 @@
"skyblocker.config.uiAndVisuals.searchOverlay.maxSuggestions": "Maximum Suggestions",
"skyblocker.config.uiAndVisuals.searchOverlay.maxSuggestions.@Tooltip": "The maximum number of suggested items to show.",
+ "skyblocker.config.uiAndVisuals.showEquipmentInInventory": "Show Equipment in Inventory",
+
"skyblocker.config.uiAndVisuals.tabHud": "Fancy tab HUD (Temporarily disabled outside dungeons)",
"skyblocker.config.uiAndVisuals.tabHud.enableHudBackground": "Enable HUD Background",
"skyblocker.config.uiAndVisuals.tabHud.enableHudBackground.@Tooltip": "Enables the background of the non-tab HUD.",
@@ -894,6 +901,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/assets/skyblocker/lang/lol_us.json b/src/main/resources/assets/skyblocker/lang/lol_us.json
new file mode 100644
index 00000000..4cda6799
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/lang/lol_us.json
@@ -0,0 +1,3 @@
+{
+ "key.categories.skyblocker": "Skibidiblocker"
+}
diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png
new file mode 100644
index 00000000..be89af40
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png
Binary files differ
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",