aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java48
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java429
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java44
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/DeathFilter.java25
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java28
6 files changed, 505 insertions, 81 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java
index e1af85ea..293d301f 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java
@@ -5,7 +5,6 @@ import de.hysky.skyblocker.utils.scheduler.Scheduler;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.render.MapRenderer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.util.math.MatrixStack;
@@ -13,12 +12,9 @@ import net.minecraft.item.FilledMapItem;
import net.minecraft.item.ItemStack;
import net.minecraft.item.map.MapState;
import net.minecraft.nbt.NbtCompound;
-import net.minecraft.util.Identifier;
import org.apache.commons.lang3.StringUtils;
public class DungeonMap {
- private static final Identifier MAP_BACKGROUND = new Identifier("textures/map/map_background.png");
-
public static void render(MatrixStack matrices) {
MinecraftClient client = MinecraftClient.getInstance();
if (client.player == null || client.world == null) return;
@@ -46,13 +42,7 @@ public class DungeonMap {
}
}
- public static void renderHUDMap(DrawContext context, int x, int y) {
- float scaling = SkyblockerConfigManager.get().locations.dungeons.mapScaling;
- int size = (int) (128 * scaling);
- context.drawTexture(MAP_BACKGROUND, x, y, 0, 0, size, size, size, size);
- }
-
- public static void init() {
+ public static void init() { //Todo: consider renaming the command to a more general name since it'll also have dungeon score and maybe other stuff in the future
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker")
.then(ClientCommandManager.literal("hud")
.then(ClientCommandManager.literal("dungeonmap")
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java
index 02b08254..0f42c495 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java
@@ -5,13 +5,17 @@ import de.hysky.skyblocker.utils.render.RenderHelper;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
import java.awt.*;
public class DungeonMapConfigScreen extends Screen {
- private int hudX = SkyblockerConfigManager.get().locations.dungeons.mapX;
- private int hudY = SkyblockerConfigManager.get().locations.dungeons.mapY;
+ private int mapX = SkyblockerConfigManager.get().locations.dungeons.mapX;
+ private int mapY = SkyblockerConfigManager.get().locations.dungeons.mapY;
+ private int scoreX = SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreX;
+ private int scoreY = SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreY;
+ private static final Identifier MAP_BACKGROUND = new Identifier("textures/map/map_background.png");
private final Screen parent;
protected DungeonMapConfigScreen() {
@@ -27,17 +31,23 @@ public class DungeonMapConfigScreen extends Screen {
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
super.render(context, mouseX, mouseY, delta);
renderBackground(context, mouseX, mouseY, delta);
- DungeonMap.renderHUDMap(context, hudX, hudY);
+ renderHUDMap(context, mapX, mapY);
+ renderHUDScore(context, scoreX, scoreY);
context.drawCenteredTextWithShadow(textRenderer, "Right Click To Reset Position", width >> 1, height >> 1, Color.GRAY.getRGB());
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
- float scaling = SkyblockerConfigManager.get().locations.dungeons.mapScaling;
- int size = (int) (128 * scaling);
- if (RenderHelper.pointIsInArea(mouseX, mouseY, hudX, hudY, hudX + size, hudY + size) && button == 0) {
- hudX = (int) Math.max(Math.min(mouseX - (size >> 1), this.width - size), 0);
- hudY = (int) Math.max(Math.min(mouseY - (size >> 1), this.height - size), 0);
+ int mapSize = (int) (128 * SkyblockerConfigManager.get().locations.dungeons.mapScaling);
+ float scoreScaling = SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreScaling;
+ int scoreWidth = (int) (textRenderer.getWidth(DungeonScoreHUD.getFormattedScoreText()) * scoreScaling);
+ int scoreHeight = (int) (textRenderer.fontHeight * scoreScaling);
+ if (RenderHelper.pointIsInArea(mouseX, mouseY, mapX, mapY, mapX + mapSize, mapY + mapSize) && button == 0) {
+ mapX = (int) Math.max(Math.min(mouseX - (mapSize >> 1), this.width - mapSize), 0);
+ mapY = (int) Math.max(Math.min(mouseY - (mapSize >> 1), this.height - mapSize), 0);
+ } else if (RenderHelper.pointIsInArea(mouseX, mouseY, scoreX, scoreY, scoreX + scoreWidth, scoreY + scoreHeight) && button == 0) {
+ scoreX = (int) Math.max(Math.min(mouseX - (scoreWidth >> 1), this.width - scoreWidth), 0);
+ scoreY = (int) Math.max(Math.min(mouseY - (scoreHeight >> 1), this.height - scoreHeight), 0);
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
}
@@ -45,8 +55,10 @@ public class DungeonMapConfigScreen extends Screen {
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (button == 1) {
- hudX = 2;
- hudY = 2;
+ mapX = 2;
+ mapY = 2;
+ scoreX = Math.max((int) ((mapX + (64 * SkyblockerConfigManager.get().locations.dungeons.mapScaling)) - textRenderer.getWidth(DungeonScoreHUD.getFormattedScoreText()) * SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreScaling / 2), 0);
+ scoreY = (int) (mapY + (128 * SkyblockerConfigManager.get().locations.dungeons.mapScaling) + 4);
}
return super.mouseClicked(mouseX, mouseY, button);
@@ -54,10 +66,22 @@ public class DungeonMapConfigScreen extends Screen {
@Override
public void close() {
- SkyblockerConfigManager.get().locations.dungeons.mapX = hudX;
- SkyblockerConfigManager.get().locations.dungeons.mapY = hudY;
+ SkyblockerConfigManager.get().locations.dungeons.mapX = mapX;
+ SkyblockerConfigManager.get().locations.dungeons.mapY = mapY;
+ SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreX = scoreX;
+ SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreY = scoreY;
SkyblockerConfigManager.save();
this.client.setScreen(parent);
}
+
+ public void renderHUDMap(DrawContext context, int x, int y) {
+ float scaling = SkyblockerConfigManager.get().locations.dungeons.mapScaling;
+ int size = (int) (128 * scaling);
+ context.drawTexture(MAP_BACKGROUND, x, y, 0, 0, size, size, size, size);
+ }
+
+ public void renderHUDScore(DrawContext context, int x, int y) {
+ DungeonScoreHUD.render(context, x, y);
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java
index d67d6988..10605d8b 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java
@@ -1,76 +1,389 @@
package de.hysky.skyblocker.skyblock.dungeon;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.utils.ApiUtils;
import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Http;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.mob.ZombieEntity;
+import net.minecraft.item.ItemStack;
import net.minecraft.sound.SoundEvents;
+import net.minecraft.util.collection.DefaultedList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.StreamSupport;
public class DungeonScore {
- private static final SkyblockerConfig.DungeonScore CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore;
- private static final Pattern DUNGEON_CLEARED_PATTERN = Pattern.compile("Cleared: (?<cleared>\\d+)% \\((?<score>\\d+)\\)");
- private static boolean sent270;
- private static boolean sent300;
-
- public static void init() {
- Scheduler.INSTANCE.scheduleCyclic(DungeonScore::tick, 20);
- ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset());
- }
-
- public static void tick() {
- MinecraftClient client = MinecraftClient.getInstance();
- if (!Utils.isInDungeons() || client.player == null) {
- reset();
- return;
- }
-
- for (String sidebarLine : Utils.STRING_SCOREBOARD) {
- Matcher dungeonClearedMatcher = DUNGEON_CLEARED_PATTERN.matcher(sidebarLine);
- if (!dungeonClearedMatcher.matches()) {
- continue;
- }
- int score = Integer.parseInt(dungeonClearedMatcher.group("score"));
- if (!DungeonManager.isInBoss()) score += 28;
- if (!sent270 && score >= 270 && score < 300) {
- if (CONFIG.enableDungeonScore270Message) {
- MessageScheduler.INSTANCE.sendMessageAfterCooldown(Constants.PREFIX.get().getString() + CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270"));
- }
- if (CONFIG.enableDungeonScore270Title) {
- client.inGameHud.setDefaultTitleFade();
- client.inGameHud.setTitle(Constants.PREFIX.get().append(CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")));
- }
- if (CONFIG.enableDungeonScore270Sound) {
- client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f);
- }
- sent270 = true;
- }
- if (!sent300 && score >= 300) {
- if (CONFIG.enableDungeonScore300Message) {
- MessageScheduler.INSTANCE.sendMessageAfterCooldown(Constants.PREFIX.get().getString() + CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300"));
- }
- if (CONFIG.enableDungeonScore300Title) {
- client.inGameHud.setDefaultTitleFade();
- client.inGameHud.setTitle(Constants.PREFIX.get().append(CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")));
- }
- if (CONFIG.enableDungeonScore300Sound) {
- client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f);
- }
- sent300 = true;
- }
- break;
- }
- }
-
- private static void reset() {
- sent270 = false;
- sent300 = false;
- }
+ private static final SkyblockerConfig.DungeonScore SCORE_CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore;
+ private static final SkyblockerConfig.MimicMessage MIMIC_MESSAGE_CONFIG = SkyblockerConfigManager.get().locations.dungeons.mimicMessage;
+ private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Dungeon Score");
+ //Scoreboard patterns
+ private static final Pattern CLEARED_PATTERN = Pattern.compile("Cleared: (?<cleared>\\d+)%.*");
+ private static final Pattern FLOOR_PATTERN = Pattern.compile(".*?(?=T)The Catacombs \\((?<floor>[EFM]\\D*\\d*)\\)");
+ //Playerlist patterns
+ private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?<secper>\\d+\\.?\\d*)%");
+ private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?<state>.)](?: \\(\\w+\\))?");
+ private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?<count>\\d+)\\)");
+ private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?<crypts>\\d+)");
+ private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?<rooms>\\d+)");
+ //Chat patterns
+ private static final Pattern DEATHS_PATTERN = Pattern.compile(" \\u2620 (?<whodied>\\S+) .*");
+ private static final Pattern MIMIC_PATTERN = Pattern.compile(".*?(?:Mimic dead!?|Mimic Killed!|\\$SKYTILS-DUNGEON-SCORE-MIMIC\\$|\\Q" + MIMIC_MESSAGE_CONFIG.mimicMessage + "\\E)$");
+ //Other patterns
+ private static final Pattern MIMIC_FLOORS_PATTERN = Pattern.compile("[FM][67]");
+
+ private static FloorRequirement floorRequirement;
+ private static String currentFloor;
+ private static boolean isCurrentFloorEntrance;
+ private static boolean floorHasMimics;
+ private static boolean sent270;
+ private static boolean sent300;
+ private static boolean mimicKilled;
+ private static boolean dungeonStarted;
+ private static boolean isMayorPaul;
+ private static boolean firstDeathHasSpiritPet;
+ private static boolean bloodRoomCompleted;
+ private static long startingTime;
+ private static int puzzleCount;
+ private static int deathCount;
+ private static int score;
+ private static final Map<String, Boolean> SpiritPetCache = new HashMap<>();
+
+ public static void init() {
+ Scheduler.INSTANCE.scheduleCyclic(DungeonScore::tick, 20);
+ SkyblockEvents.LEAVE.register(SpiritPetCache::clear);
+ ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset());
+ ClientReceiveMessageEvents.GAME.register((message, overlay) -> {
+ if (overlay || !Utils.isInDungeons()) return;
+ String str = message.getString();
+ if (!dungeonStarted) {
+ checkMessageForMort(str);
+ } else {
+ checkMessageForDeaths(str);
+ checkMessageForWatcher(str);
+ if (floorHasMimics) checkMessageForMimic(str); //Only called when the message is not cancelled & isn't on the action bar, complementing MimicFilter
+ }
+ });
+ ClientReceiveMessageEvents.GAME_CANCELED.register((message, overlay) -> {
+ if (overlay || !Utils.isInDungeons() || !dungeonStarted) return;
+ checkMessageForDeaths(message.getString());
+ });
+ }
+
+ public static void tick() {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (!Utils.isInDungeons() || client.player == null) {
+ reset();
+ return;
+ }
+ if (!dungeonStarted) return;
+
+ score = calculateScore();
+ if (!sent270 && !sent300 && score >= 270 && score < 300) {
+ if (SCORE_CONFIG.enableDungeonScore270Message) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270"));
+ }
+ if (SCORE_CONFIG.enableDungeonScore270Title) {
+ client.inGameHud.setDefaultTitleFade();
+ client.inGameHud.setTitle(Constants.PREFIX.get().append(SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")));
+ }
+ if (SCORE_CONFIG.enableDungeonScore270Sound) {
+ client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f);
+ }
+ sent270 = true;
+ }
+ if (!sent300 && score >= 300) {
+ if (SCORE_CONFIG.enableDungeonScore300Message) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300"));
+ }
+ if (SCORE_CONFIG.enableDungeonScore300Title) {
+ client.inGameHud.setDefaultTitleFade();
+ client.inGameHud.setTitle(Constants.PREFIX.get().append(SCORE_CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")));
+ }
+ if (SCORE_CONFIG.enableDungeonScore300Sound) {
+ client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f);
+ }
+ sent300 = true;
+ }
+ }
+
+ private static void reset() {
+ floorRequirement = null;
+ currentFloor = "";
+ isCurrentFloorEntrance = false;
+ floorHasMimics = false;
+ sent270 = false;
+ sent300 = false;
+ mimicKilled = false;
+ dungeonStarted = false;
+ isMayorPaul = false;
+ firstDeathHasSpiritPet = false;
+ bloodRoomCompleted = false;
+ startingTime = 0L;
+ puzzleCount = 0;
+ deathCount = 0;
+ score = 0;
+ }
+
+ private static void onDungeonStart() {
+ setCurrentFloor();
+ dungeonStarted = true;
+ puzzleCount = getPuzzleCount();
+ isMayorPaul = Utils.getMayor().equals("Paul");
+ startingTime = System.currentTimeMillis();
+ floorRequirement = FloorRequirement.valueOf(currentFloor);
+ floorHasMimics = MIMIC_FLOORS_PATTERN.matcher(currentFloor).matches();
+ if (currentFloor.equals("E")) isCurrentFloorEntrance = true;
+ }
+
+ private static int calculateScore() {
+ if (isCurrentFloorEntrance) return Math.round(calculateTimeScore() * 0.7f) + Math.round(calculateExploreScore() * 0.7f) + Math.round(calculateSkillScore() * 0.7f) + Math.round(calculateBonusScore() * 0.7f);
+ return calculateTimeScore() + calculateExploreScore() + calculateSkillScore() + calculateBonusScore();
+ }
+
+ private static int calculateSkillScore() {
+ int totalRooms = getTotalRooms(); //This is necessary to avoid division by 0 at the start of dungeons, which results in infinite score
+ return 20 + Math.max((totalRooms != 0 ? (int) (80.0 * (getCompletedRooms() + getExtraCompletedRooms()) / totalRooms) : 0) - getPuzzlePenalty() - getDeathScorePenalty(), 0); //Can't go below 20 skill score
+ }
+
+ private static int calculateExploreScore() {
+ int totalRooms = getTotalRooms(); //This is necessary to avoid division by 0 at the start of dungeons, which results in infinite score
+ int completedRoomScore = totalRooms != 0 ? (int) (60.0 * (getCompletedRooms() + getExtraCompletedRooms()) / totalRooms) : 0;
+ int secretsScore = (int) (40 * Math.min(floorRequirement.percentage, getSecretsPercentage()) / floorRequirement.percentage);
+ return Math.max(completedRoomScore + secretsScore, 0);
+ }
+
+ private static int calculateTimeScore() {
+ int score = 100;
+ int timeSpent = (int) (System.currentTimeMillis() - startingTime) / 1000;
+ if (timeSpent < floorRequirement.timeLimit) return score;
+
+ double timePastRequirement = ((double) (timeSpent - floorRequirement.timeLimit) / floorRequirement.timeLimit) * 100;
+ if (timePastRequirement < 20) return score - (int) timePastRequirement / 2;
+ if (timePastRequirement < 40) return score - (int) (10 + (timePastRequirement - 20) / 4);
+ if (timePastRequirement < 50) return score - (int) (15 + (timePastRequirement - 40) / 5);
+ if (timePastRequirement < 60) return score - (int) (17 + (timePastRequirement - 50) / 6);
+ return Math.max(score - (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7), 0); //This can theoretically go down to -20 if the time limit is one of the lower ones like 480, but individual score categories can't go below 0
+ }
+
+ private static int calculateBonusScore() {
+ int paulScore = isMayorPaul ? 10 : 0;
+ int cryptsScore = Math.min(getCrypts(), 5);
+ int mimicScore = mimicKilled ? 2 : 0;
+ if (getSecretsPercentage() >= 100 && floorHasMimics) mimicScore = 2; //If mimic kill is not announced but all secrets are found, mimic must've been killed
+ return paulScore + cryptsScore + mimicScore;
+ }
+
+ public static boolean isEntityMimic(Entity entity) {
+ if (!Utils.isInDungeons() || !floorHasMimics || !(entity instanceof ZombieEntity zombie) || !zombie.isBaby()) return false;
+ try {
+ DefaultedList<ItemStack> armor = (DefaultedList<ItemStack>) zombie.getArmorItems();
+ return armor.stream().allMatch(ItemStack::isEmpty);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to check if entity is a mimic!", e);
+ return false;
+ }
+ }
+
+ public static void handleEntityDeath(Entity entity) {
+ if (mimicKilled) return;
+ if (!isEntityMimic(entity)) return;
+ if (MIMIC_MESSAGE_CONFIG.sendMimicMessage) MessageScheduler.INSTANCE.sendMessageAfterCooldown(MIMIC_MESSAGE_CONFIG.mimicMessage);
+ mimicKilled = true;
+ }
+
+ public static void onMimicKill() {
+ mimicKilled = true;
+ }
+
+ //This is not very accurate at the beginning of the dungeon since clear percentage is rounded to the closest integer, so at lower percentages its effect on the result is quite high.
+ //For example: If clear percentage is 7% with a single room completed, it can be rounded from 6.5 or 7.49. In that range, the actual total room count can be either 14 or 15 while our result is 14.
+ //Score might fluctuate at first if the total room amount calculated changes as it gets more accurate with each room completed.
+ private static int getTotalRooms() {
+ return (int) Math.round(getCompletedRooms() / getClearPercentage());
+ }
+
+ private static int getCompletedRooms() {
+ Matcher matcher = PlayerListMgr.regexAt(43, COMPLETED_ROOMS_PATTERN);
+ return matcher != null ? Integer.parseInt(matcher.group("rooms")) : 0;
+ }
+
+ //This is needed for calculating the score before going in the boss room & completing the blood room, so we have the result sooner
+ //It might cause score to fluctuate when completing the blood room or entering the boss room as there's a slight delay between the room being completed (boolean set to true) and the scoreboard updating
+ private static int getExtraCompletedRooms() {
+ if (!bloodRoomCompleted) return isCurrentFloorEntrance ? 1 : 2;
+ if (!DungeonManager.isInBoss() && !isCurrentFloorEntrance) return 1;
+ return 0;
+ }
+
+ private static double getClearPercentage() {
+ for (String sidebarLine : Utils.STRING_SCOREBOARD) {
+ Matcher clearMatcher = CLEARED_PATTERN.matcher(sidebarLine);
+ if (!clearMatcher.matches()) continue;
+ return Double.parseDouble(clearMatcher.group("cleared")) / 100.0;
+ }
+ LOGGER.error("[Skyblocker] Clear pattern doesn't match!");
+ return 0;
+ }
+
+ //Score might fluctuate when the first death has spirit pet as the boolean will be set to true after getting a response from the api, which might take a while
+ private static int getDeathScorePenalty() {
+ return deathCount * 2 - (firstDeathHasSpiritPet ? 1 : 0);
+ }
+
+ private static int getPuzzleCount() {
+ Matcher matcher = PlayerListMgr.regexAt(47, PUZZLE_COUNT_PATTERN);
+ return matcher != null ? Integer.parseInt(matcher.group("count")) : 0;
+ }
+
+ private static int getPuzzlePenalty() {
+ int incompletePuzzles = 0;
+ for (int index = 0; index < puzzleCount; index++) {
+ Matcher puzzleMatcher = PlayerListMgr.regexAt(48 + index, PUZZLES_PATTERN);
+ if (puzzleMatcher == null) break;
+ if (puzzleMatcher.group("state").matches("[✖✦]")) incompletePuzzles++;
+ }
+ return incompletePuzzles * 10;
+ }
+
+ private static double getSecretsPercentage() {
+ Matcher matcher = PlayerListMgr.regexAt(44, SECRETS_PATTERN);
+ return matcher != null ? Double.parseDouble(matcher.group("secper")) : 0;
+ }
+
+ private static int getCrypts() {
+ Matcher matcher = PlayerListMgr.regexAt(33, CRYPTS_PATTERN);
+ if (matcher == null) matcher = PlayerListMgr.regexAt(32, CRYPTS_PATTERN); //If class milestone 9 is reached, crypts goes up by 1
+ return matcher != null ? Integer.parseInt(matcher.group("crypts")) : 0;
+ }
+
+ public static boolean hasSpiritPet(String name) {
+ return SpiritPetCache.computeIfAbsent(name, k -> {
+ String playeruuid = ApiUtils.name2Uuid(name);
+ try (Http.ApiResponse response = Http.sendHypixelRequest("skyblock/profiles", "?uuid=" + playeruuid)) {
+ if (!response.ok()) throw new IllegalStateException("Failed to get profile uuid for player " + name + "! Response: " + response.content());
+ JsonObject responseJson = JsonParser.parseString(response.content()).getAsJsonObject();
+
+ JsonObject player = StreamSupport.stream(responseJson.getAsJsonArray("profiles").spliterator(), false)
+ .map(JsonElement::getAsJsonObject)
+ .filter(profile -> profile.getAsJsonPrimitive("selected").getAsBoolean())
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No selected profile found!?"))
+ .getAsJsonObject("members").entrySet().stream()
+ .filter(entry -> entry.getKey().equals(playeruuid))
+ .map(Map.Entry::getValue)
+ .map(JsonElement::getAsJsonObject)
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("Player somehow not found inside their own profile!"));
+
+ for (JsonElement element : player.getAsJsonObject("pets_data").getAsJsonArray("pets")) {
+ if (!element.getAsJsonObject().get("type").getAsString().equals("SPIRIT")) continue;
+ if (!element.getAsJsonObject().get("tier").getAsString().equals("LEGENDARY")) continue;
+
+ return true;
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Spirit pet lookup by name failed! Name: {}", name, e);
+ }
+ return false;
+ });
+ }
+
+ private static void checkMessageForDeaths(String message) {
+ if (!message.startsWith("\u2620", 1)) return;
+ Matcher matcher = DEATHS_PATTERN.matcher(message);
+ if (!matcher.matches()) return;
+ deathCount++;
+ if (deathCount > 1) return;
+ final String whoDied = matcher.group("whodied").transform(s -> {
+ if (s.equals("You")) return MinecraftClient.getInstance().player.getName().getString(); //This will be wrong if the dead player is called 'You' but that's unlikely
+ else return s;
+ });
+ CompletableFuture.supplyAsync(() -> hasSpiritPet(whoDied))
+ .thenAccept(hasSpiritPet -> firstDeathHasSpiritPet = hasSpiritPet);
+ }
+
+ private static void checkMessageForWatcher(String message) {
+ if (message.equals("[BOSS] The Watcher: You have proven yourself. You may pass.")) bloodRoomCompleted = true;
+ }
+
+ private static void checkMessageForMort(String message) {
+ if (!message.equals("§e[NPC] §bMort§f: You should find it useful if you get lost.")) return;
+ onDungeonStart();
+ }
+
+ private static void checkMessageForMimic(String message) {
+ if (!MIMIC_PATTERN.matcher(message).matches()) return;
+ onMimicKill();
+ }
+
+ public static void setCurrentFloor() {
+ for (String sidebarLine : Utils.STRING_SCOREBOARD) {
+ Matcher floorMatcher = FLOOR_PATTERN.matcher(sidebarLine);
+ if (!floorMatcher.matches()) continue;
+ currentFloor = floorMatcher.group("floor");
+ return;
+ }
+ LOGGER.error("[Skyblocker] Floor pattern doesn't match!");
+ }
+
+ public static int getScore() {
+ return score;
+ }
+
+ public static boolean isDungeonStarted() {
+ return dungeonStarted;
+ }
+
+ //Feel free to refactor this if you can think of a better name.
+ public static boolean isMimicOnCurrentFloor() {
+ return floorHasMimics;
+ }
+
+ enum FloorRequirement {
+ E(30, 1200),
+ F1(30, 600),
+ F2(40, 600),
+ F3(50, 600),
+ F4(60, 720),
+ F5(70, 600),
+ F6(85, 720),
+ F7(100, 840),
+ M1(100, 480),
+ M2(100, 480),
+ M3(100, 480),
+ M4(100, 480),
+ M5(100, 480),
+ M6(100, 600),
+ M7(100, 840);
+
+ private final int percentage;
+ private final int timeLimit;
+
+ FloorRequirement(int percentage, int timeLimit) {
+ this.percentage = percentage;
+ this.timeLimit = timeLimit;
+ }
+ }
}
+
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java
new file mode 100644
index 00000000..1dfb1b95
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java
@@ -0,0 +1,44 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class DungeonScoreHUD {
+ private DungeonScoreHUD() {
+ }
+
+ //This is 4+5 wide, needed to offset the extra width from bold numbers (3×1 wide) in S+ and the "+" (6 wide) so that it doesn't go off the screen if the score is S+ and the hud element is at the right edge of the screen
+ private static final Text extraSpace = Text.literal(" ").append(Text.literal(" ").formatted(Formatting.BOLD));
+
+ public static void render(DrawContext context) {
+ int x = SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreX;
+ int y = SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreY;
+ render(context, x, y);
+ }
+
+ public static void render(DrawContext context, int x, int y) {
+ float scale = SkyblockerConfigManager.get().locations.dungeons.dungeonScore.scoreScaling;
+ MatrixStack matrixStack = context.getMatrices();
+ matrixStack.push();
+ matrixStack.scale(scale, scale, 0);
+ context.drawTextWithShadow(MinecraftClient.getInstance().textRenderer, getFormattedScoreText(), (int) (x / scale), (int) (y / scale), 0xFFFFFFFF);
+ matrixStack.pop();
+ }
+
+ public static Text getFormattedScoreText() {
+ return Text.translatable("skyblocker.dungeons.dungeonScore.scoreText", formatScore(DungeonScore.getScore()));
+ }
+
+ private static Text formatScore(int score) {
+ if (score < 100) return Text.literal(String.format("%03d", score)).withColor(0xDC1A1A).append(Text.literal(" (D)").formatted(Formatting.GRAY)).append(extraSpace);
+ if (score < 160) return Text.literal(String.format("%03d", score)).withColor(0x4141FF).append(Text.literal(" (C)").formatted(Formatting.GRAY)).append(extraSpace);
+ if (score < 230) return Text.literal(String.format("%03d", score)).withColor(0x7FCC19).append(Text.literal(" (B)").formatted(Formatting.GRAY)).append(extraSpace);
+ if (score < 270) return Text.literal(String.format("%03d", score)).withColor(0x7F3FB2).append(Text.literal(" (A)").formatted(Formatting.GRAY)).append(extraSpace);
+ if (score < 300) return Text.literal(String.format("%03d", score)).withColor(0xF1E252).append(Text.literal(" (S)").formatted(Formatting.GRAY)).append(extraSpace);
+ return Text.literal("").append(Text.literal(String.format("%03d", score)).withColor(0xF1E252).formatted(Formatting.BOLD)).append(Text.literal(" (S+)").formatted(Formatting.GRAY));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/DeathFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/DeathFilter.java
new file mode 100644
index 00000000..f2b9e7c5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/DeathFilter.java
@@ -0,0 +1,25 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.text.Text;
+
+import java.util.regex.Matcher;
+
+public class DeathFilter extends ChatPatternListener {
+
+ public DeathFilter() {
+ super(" \\u2620 .*");
+ }
+
+ @Override
+ protected ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideDeath;
+ }
+
+ @Override
+ protected boolean onMatch(Text message, Matcher matcher) {
+ return true;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java
new file mode 100644
index 00000000..cb845254
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java
@@ -0,0 +1,28 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.dungeon.DungeonScore;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.text.Text;
+
+import java.util.regex.Matcher;
+
+public class MimicFilter extends ChatPatternListener {
+ public MimicFilter() {
+ super(".*?(?:Mimic dead!?|Mimic Killed!|\\$SKYTILS-DUNGEON-SCORE-MIMIC\\$|\\Q" + SkyblockerConfigManager.get().locations.dungeons.mimicMessage.mimicMessage + "\\E)$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideMimicKill;
+ }
+
+ @Override
+ protected boolean onMatch(Text message, Matcher matcher) {
+ if (!Utils.isInDungeons() || !DungeonScore.isDungeonStarted() || !DungeonScore.isMimicOnCurrentFloor()) return false;
+ DungeonScore.onMimicKill(); //Only called when the message is cancelled | sent to action bar, complementing DungeonScore#checkMessageForMimic
+ return true;
+ }
+}