From cdd9441aa587ef7ecb2ac285350f59846b433ff9 Mon Sep 17 00:00:00 2001 From: Emirlol <81419447+Emirlol@users.noreply.github.com> Date: Sun, 7 Jan 2024 02:06:28 +0300 Subject: Add dungeon score calculation on client-side --- .../skyblocker/skyblock/dungeon/DungeonScore.java | 290 ++++++++++++++++++--- .../skyblocker/skyblock/filters/MimicFilter.java | 26 ++ 2 files changed, 280 insertions(+), 36 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java (limited to 'src/main/java/de/hysky/skyblocker/skyblock') 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..ebec99dc 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -1,28 +1,81 @@ package de.hysky.skyblocker.skyblock.dungeon; + import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr; import de.hysky.skyblocker.utils.Constants; 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.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.ZombieEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; 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.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DungeonScore { - private static final SkyblockerConfig.DungeonScore CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; - private static final Pattern DUNGEON_CLEARED_PATTERN = Pattern.compile("Cleared: (?\\d+)% \\((?\\d+)\\)"); + private static final SkyblockerConfig.DungeonScore SCORE_CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; + private static final SkyblockerConfig.MimicMessages MIMIC_MESSAGES_CONFIG = SkyblockerConfigManager.get().locations.dungeons.mimicMessages; + private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Dungeon Score"); + private static final Pattern CLEARED_PATTERN = Pattern.compile("Cleared: (?\\d+)%.*"); + private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?\\d+\\.?\\d*)%"); + private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?.)](?: \\(\\w+\\))?"); + private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?\\d+)\\)"); + private static final Pattern TIME_PATTERN = Pattern.compile("Time: (?:(?\\d+(?=h))?h? ?(?\\d+(?=m))?m? ?(?\\d+(?=s))s|Soon!)"); + private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?\\d+)"); + private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?\\d+)"); + private static final Pattern DUNGEON_START_PATTERN = Pattern.compile("(?:Auto-closing|Starting) in: \\d:\\d+"); + private static final Pattern FLOOR_PATTERN = Pattern.compile(".*?(?=T)The Catacombs \\((?[EFM]\\D*\\d*)\\)"); + private static final Pattern DEATHS_PATTERN = Pattern.compile("Team Deaths: (?\\d+)"); + private static String currentFloor; private static boolean sent270; private static boolean sent300; + private static boolean isMimicKilled; + private static int puzzleCount; + //Caching the dungeon start state to prevent unnecessary scoreboard pattern matching after dungeon starts + private static boolean isDungeonStarted; + private static boolean isMayorPaul; + private static final HashMap floorRequirements = new HashMap<>(Map.ofEntries( + Map.entry("E", new FloorRequirement(30, 1200)), + Map.entry("F1", new FloorRequirement(30, 600)), + Map.entry("F2", new FloorRequirement(40, 600)), + Map.entry("F3", new FloorRequirement(50, 600)), + Map.entry("F4", new FloorRequirement(60, 720)), + Map.entry("F5", new FloorRequirement(70, 600)), + Map.entry("F6", new FloorRequirement(85, 720)), + Map.entry("F7", new FloorRequirement(100, 840)), + Map.entry("M1", new FloorRequirement(100, 480)), + Map.entry("M2", new FloorRequirement(100, 480)), + Map.entry("M3", new FloorRequirement(100, 480)), + Map.entry("M4", new FloorRequirement(100, 480)), + Map.entry("M5", new FloorRequirement(100, 480)), + Map.entry("M6", new FloorRequirement(100, 600)), + Map.entry("M7", new FloorRequirement(100, 840)) + )); public static void init() { Scheduler.INSTANCE.scheduleCyclic(DungeonScore::tick, 20); ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + ServerLivingEntityEvents.AFTER_DEATH.register((entity, source) -> { + if (isEntityMimic(entity)) { + if (MIMIC_MESSAGES_CONFIG.sendMimicMessages) MessageScheduler.INSTANCE.sendMessageAfterCooldown(MIMIC_MESSAGES_CONFIG.mimicMessage); + setMimicKilled(true); + } + }); } public static void tick() { @@ -31,46 +84,211 @@ public class DungeonScore { reset(); return; } + if (!isDungeonStarted) { + if (checkIfDungeonStarted()) onDungeonStart(); + return; + } + int score = calculateScore(); + if (SCORE_CONFIG.enableDungeonScore270 && !sent270 && score >= 270 && score < 300) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")); + client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 1f, 0.1f); + sent270 = true; + } + if (SCORE_CONFIG.enableDungeonScore300 && !sent300 && score >= 300) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")); + client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 1f, 1f); + sent300 = true; + } + } + + public static boolean isEntityMimic(Entity entity) { + if (!(entity instanceof ZombieEntity zombie)) return false; + if (!zombie.isBaby()) return false; + try { + DefaultedList armor = (DefaultedList) zombie.getArmorItems(); + if (armor.isEmpty()) return false; + NbtCompound helmetNbt = armor.get(3).getNbt(); + if (helmetNbt == null) return false; + return helmetNbt.getCompound("SkullOwner") + .getCompound("Properties") + .getList("textures", NbtElement.COMPOUND_TYPE) + .getCompound(0).getString("Value") + .equals("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTE5YzEyNTQzYmM3NzkyNjA1ZWY2OGUxZjg3NDlhZThmMmEzODFkOTA4NWQ0ZDRiNzgwYmExMjgyZDM1OTdhMCJ9fX0K"); + } catch (NullPointerException e) { + return false; + } catch (ClassCastException f) { + f.printStackTrace(); + return false; + } + } + private static boolean checkIfDungeonStarted() { 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; + Matcher matcher = DUNGEON_START_PATTERN.matcher(sidebarLine); + if (matcher.matches()) return false; } + return true; + } + + private static void onDungeonStart() { + setCurrentFloor(); + isDungeonStarted = true; + puzzleCount = getPuzzleCount(); + isMayorPaul = Utils.getMayor().equals("Paul"); + } + + private static int calculateScore() { + int timeScore = calculateTimeScore(); + int exploreScore = calculateExploreScore(); + int skillScore = calculateSkillScore(); + int paulScore = isMayorPaul ? 10 : 0; + int cryptsScore = Math.min(getCrypts(), 5); + int mimicScore = isMimicKilled ? 2 : 0; + int totalScore = timeScore + exploreScore + skillScore + paulScore + cryptsScore + mimicScore; + //Will be this way until ready for pr, so it's easy to debug. + LOGGER.info("Total Score: {} (Time: {}, Explore: {}, Skill: {}, Paul: {}, Crypts: {}, Mimic: {})", totalScore, timeScore, exploreScore, skillScore, paulScore, cryptsScore, mimicScore); + return totalScore; + } + + private static int calculateExploreScore() { + int completedRoomScore = (int) Math.floor(60.0 * getCompletedRooms() / getTotalRooms()); + int percentageRequirement = floorRequirements.get(currentFloor).percentage; + int secretsScore = (int) Math.floor(40 * Math.min(percentageRequirement, getSecretsPercentage()) / percentageRequirement); + return completedRoomScore + secretsScore; + } + + private static int calculateTimeScore() { + Matcher timeMatcher = PlayerListMgr.regexAt(45, TIME_PATTERN); + if (timeMatcher == null) { + LOGGER.error("Time pattern doesn't match"); + return 0; + } + int score = 100; + int hours = Optional.ofNullable(timeMatcher.group("hours")).map(Integer::parseInt).orElse(0); + int minutes = Optional.ofNullable(timeMatcher.group("minutes")).map(Integer::parseInt).orElse(0); + int seconds = Optional.ofNullable(timeMatcher.group("seconds")).map(Integer::parseInt).orElse(0); + int timeSpent = hours * 3600 + minutes * 60 + seconds; + int timeRequirement = floorRequirements.get(currentFloor).timeLimit; + if (timeSpent < timeRequirement) return score; + + double timePastRequirement = ((double) (timeSpent - timeRequirement) / timeRequirement) * 100; + if (timePastRequirement >= 0 && timePastRequirement < 20) { + score -= (int) timePastRequirement / 2; + } else if (timePastRequirement >= 20 && timePastRequirement < 40) { + score -= (int) (10 + (timePastRequirement - 20) / 4); + } else if (timePastRequirement >= 40 && timePastRequirement < 50) { + score -= (int) (15 + (timePastRequirement - 40) / 5); + } else if (timePastRequirement >= 50 && timePastRequirement < 60) { + score -= (int) (17 + (timePastRequirement - 50) / 6); + } else if (timePastRequirement >= 60) { + score -= (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7); + } + return score; + } + + private static int calculateSkillScore() { + return 20 + (int) Math.floor(80.0 * getCompletedRooms() / getTotalRooms()) - (2 * getDeathCount()) - (10 * getFailedPuzzles()); } private static void reset() { sent270 = false; sent300 = false; + isDungeonStarted = false; + isMimicKilled = false; + isMayorPaul = false; + puzzleCount = 0; + currentFloor = ""; + } + + public static void setMimicKilled(boolean killed) { + isMimicKilled = killed; + } + + private static int getTotalRooms() { + return (int) Math.round((getCompletedRooms()) / getClearPercentage()); //Clear% rounds to the closest integer so it can be off by 0.5% at most, this should be accurate enough + } + + private static int getCompletedRooms() { + Matcher completedRoomsMatcher = PlayerListMgr.regexAt(43, COMPLETED_ROOMS_PATTERN); + if (completedRoomsMatcher == null) { + LOGGER.error("Completed rooms pattern doesn't match"); + return 0; + } + return Integer.parseInt(completedRoomsMatcher.group("rooms")); + } + + 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; + } + LOGGER.error("Clear pattern doesn't match"); + return 0; + } + + private static int getDeathCount() { + Matcher matcher = PlayerListMgr.regexAt(25, DEATHS_PATTERN); + if (matcher == null) { + LOGGER.error("Death count pattern doesn't match"); + return 0; + } + //TODO: Turn this into a map of players and their deathcounts, get party members' pets, check if they have spirit pet, if they have it reduce their death count by 0.5 + return Integer.parseInt(matcher.group("deaths")); + } + + private static int getPuzzleCount() { + Matcher matcher = PlayerListMgr.regexAt(47, PUZZLE_COUNT_PATTERN); + if (matcher == null) { + LOGGER.error("Puzzle count pattern doesn't match"); + return 0; + } + return Integer.parseInt(matcher.group("count")); + } + + //Might be replaced to look for puzzle fail messages on chat instead of playerlist + private static int getFailedPuzzles() { + int failedPuzzles = 0; + for (int index = 0; index < puzzleCount; index++) { + Matcher puzzleMatcher = PlayerListMgr.regexAt(48 + index, PUZZLES_PATTERN); + if (puzzleMatcher == null) { + LOGGER.error("Puzzle pattern doesn't match"); + return 0; + } + if (puzzleMatcher.group("state").equals("✖")) failedPuzzles++; + } + return failedPuzzles; + } + + private static double getSecretsPercentage() { + Matcher matcher = PlayerListMgr.regexAt(44, SECRETS_PATTERN); + if (matcher == null) { + LOGGER.error("Secrets pattern doesn't match"); + return 0; + } + return Double.parseDouble(matcher.group("secper")); + } + + private static int getCrypts() { + Matcher matcher = PlayerListMgr.regexAt(33, CRYPTS_PATTERN); + if (matcher == null) { + LOGGER.error("Crypts pattern doesn't match"); + return 0; + } + return Integer.parseInt(matcher.group("crypts")); + } + + 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("Floor pattern doesn't match"); + } + + record FloorRequirement(int percentage, int timeLimit) { } } + 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..0fce5b2c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java @@ -0,0 +1,26 @@ +package de.hysky.skyblocker.skyblock.filters; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; +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.mimicMessages.mimicMessage + "\\E)$"); + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().messages.hideMimicKill; + } + + @Override + protected boolean onMatch(Text message, Matcher matcher) { + DungeonScore.setMimicKilled(true); + return false; + } +} -- cgit From 2b1239950626b4480b0ac7120f4c8429dfaf8b68 Mon Sep 17 00:00:00 2001 From: Emirlol <81419447+Emirlol@users.noreply.github.com> Date: Sun, 7 Jan 2024 02:58:21 +0300 Subject: Improved time score calculation --- .../de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) (limited to 'src/main/java/de/hysky/skyblocker/skyblock') 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 ebec99dc..6016813c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -35,7 +35,6 @@ public class DungeonScore { private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?\\d+\\.?\\d*)%"); private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?.)](?: \\(\\w+\\))?"); private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?\\d+)\\)"); - private static final Pattern TIME_PATTERN = Pattern.compile("Time: (?:(?\\d+(?=h))?h? ?(?\\d+(?=m))?m? ?(?\\d+(?=s))s|Soon!)"); private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?\\d+)"); private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?\\d+)"); private static final Pattern DUNGEON_START_PATTERN = Pattern.compile("(?:Auto-closing|Starting) in: \\d:\\d+"); @@ -49,6 +48,7 @@ public class DungeonScore { //Caching the dungeon start state to prevent unnecessary scoreboard pattern matching after dungeon starts private static boolean isDungeonStarted; private static boolean isMayorPaul; + private static long startingTime; private static final HashMap floorRequirements = new HashMap<>(Map.ofEntries( Map.entry("E", new FloorRequirement(30, 1200)), Map.entry("F1", new FloorRequirement(30, 600)), @@ -135,6 +135,7 @@ public class DungeonScore { isDungeonStarted = true; puzzleCount = getPuzzleCount(); isMayorPaul = Utils.getMayor().equals("Paul"); + startingTime = System.currentTimeMillis(); } private static int calculateScore() { @@ -158,16 +159,8 @@ public class DungeonScore { } private static int calculateTimeScore() { - Matcher timeMatcher = PlayerListMgr.regexAt(45, TIME_PATTERN); - if (timeMatcher == null) { - LOGGER.error("Time pattern doesn't match"); - return 0; - } int score = 100; - int hours = Optional.ofNullable(timeMatcher.group("hours")).map(Integer::parseInt).orElse(0); - int minutes = Optional.ofNullable(timeMatcher.group("minutes")).map(Integer::parseInt).orElse(0); - int seconds = Optional.ofNullable(timeMatcher.group("seconds")).map(Integer::parseInt).orElse(0); - int timeSpent = hours * 3600 + minutes * 60 + seconds; + int timeSpent = (int) (System.currentTimeMillis() - startingTime) / 1000; int timeRequirement = floorRequirements.get(currentFloor).timeLimit; if (timeSpent < timeRequirement) return score; -- cgit From 6a350f931609432bd93362b2dbdc26e23450e2b8 Mon Sep 17 00:00:00 2001 From: Emirlol <81419447+Emirlol@users.noreply.github.com> Date: Sun, 14 Jan 2024 18:21:20 +0300 Subject: Fixed most things and cleaned up code --- .../skyblocker/skyblock/dungeon/DungeonScore.java | 596 ++++++++++++--------- 1 file changed, 337 insertions(+), 259 deletions(-) (limited to 'src/main/java/de/hysky/skyblocker/skyblock') 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 6016813c..0334290d 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -1,21 +1,26 @@ 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.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.minecraft.client.MinecraftClient; import net.minecraft.entity.Entity; import net.minecraft.entity.mob.ZombieEntity; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.nbt.NbtElement; import net.minecraft.sound.SoundEvents; import net.minecraft.util.collection.DefaultedList; import org.slf4j.Logger; @@ -23,265 +28,338 @@ import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; -import java.util.Optional; +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 SCORE_CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; - private static final SkyblockerConfig.MimicMessages MIMIC_MESSAGES_CONFIG = SkyblockerConfigManager.get().locations.dungeons.mimicMessages; - private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Dungeon Score"); - private static final Pattern CLEARED_PATTERN = Pattern.compile("Cleared: (?\\d+)%.*"); - private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?\\d+\\.?\\d*)%"); - private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?.)](?: \\(\\w+\\))?"); - private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?\\d+)\\)"); - private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?\\d+)"); - private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?\\d+)"); - private static final Pattern DUNGEON_START_PATTERN = Pattern.compile("(?:Auto-closing|Starting) in: \\d:\\d+"); - private static final Pattern FLOOR_PATTERN = Pattern.compile(".*?(?=T)The Catacombs \\((?[EFM]\\D*\\d*)\\)"); - private static final Pattern DEATHS_PATTERN = Pattern.compile("Team Deaths: (?\\d+)"); - private static String currentFloor; - private static boolean sent270; - private static boolean sent300; - private static boolean isMimicKilled; - private static int puzzleCount; - //Caching the dungeon start state to prevent unnecessary scoreboard pattern matching after dungeon starts - private static boolean isDungeonStarted; - private static boolean isMayorPaul; - private static long startingTime; - private static final HashMap floorRequirements = new HashMap<>(Map.ofEntries( - Map.entry("E", new FloorRequirement(30, 1200)), - Map.entry("F1", new FloorRequirement(30, 600)), - Map.entry("F2", new FloorRequirement(40, 600)), - Map.entry("F3", new FloorRequirement(50, 600)), - Map.entry("F4", new FloorRequirement(60, 720)), - Map.entry("F5", new FloorRequirement(70, 600)), - Map.entry("F6", new FloorRequirement(85, 720)), - Map.entry("F7", new FloorRequirement(100, 840)), - Map.entry("M1", new FloorRequirement(100, 480)), - Map.entry("M2", new FloorRequirement(100, 480)), - Map.entry("M3", new FloorRequirement(100, 480)), - Map.entry("M4", new FloorRequirement(100, 480)), - Map.entry("M5", new FloorRequirement(100, 480)), - Map.entry("M6", new FloorRequirement(100, 600)), - Map.entry("M7", new FloorRequirement(100, 840)) - )); - - public static void init() { - Scheduler.INSTANCE.scheduleCyclic(DungeonScore::tick, 20); - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); - ServerLivingEntityEvents.AFTER_DEATH.register((entity, source) -> { - if (isEntityMimic(entity)) { - if (MIMIC_MESSAGES_CONFIG.sendMimicMessages) MessageScheduler.INSTANCE.sendMessageAfterCooldown(MIMIC_MESSAGES_CONFIG.mimicMessage); - setMimicKilled(true); - } - }); - } - - public static void tick() { - MinecraftClient client = MinecraftClient.getInstance(); - if (!Utils.isInDungeons() || client.player == null) { - reset(); - return; - } - if (!isDungeonStarted) { - if (checkIfDungeonStarted()) onDungeonStart(); - return; - } - int score = calculateScore(); - if (SCORE_CONFIG.enableDungeonScore270 && !sent270 && score >= 270 && score < 300) { - MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")); - client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 1f, 0.1f); - sent270 = true; - } - if (SCORE_CONFIG.enableDungeonScore300 && !sent300 && score >= 300) { - MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")); - client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 1f, 1f); - sent300 = true; - } - } - - public static boolean isEntityMimic(Entity entity) { - if (!(entity instanceof ZombieEntity zombie)) return false; - if (!zombie.isBaby()) return false; - try { - DefaultedList armor = (DefaultedList) zombie.getArmorItems(); - if (armor.isEmpty()) return false; - NbtCompound helmetNbt = armor.get(3).getNbt(); - if (helmetNbt == null) return false; - return helmetNbt.getCompound("SkullOwner") - .getCompound("Properties") - .getList("textures", NbtElement.COMPOUND_TYPE) - .getCompound(0).getString("Value") - .equals("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTE5YzEyNTQzYmM3NzkyNjA1ZWY2OGUxZjg3NDlhZThmMmEzODFkOTA4NWQ0ZDRiNzgwYmExMjgyZDM1OTdhMCJ9fX0K"); - } catch (NullPointerException e) { - return false; - } catch (ClassCastException f) { - f.printStackTrace(); - return false; - } - } - - private static boolean checkIfDungeonStarted() { - for (String sidebarLine : Utils.STRING_SCOREBOARD) { - Matcher matcher = DUNGEON_START_PATTERN.matcher(sidebarLine); - if (matcher.matches()) return false; - } - return true; - } - - private static void onDungeonStart() { - setCurrentFloor(); - isDungeonStarted = true; - puzzleCount = getPuzzleCount(); - isMayorPaul = Utils.getMayor().equals("Paul"); - startingTime = System.currentTimeMillis(); - } - - private static int calculateScore() { - int timeScore = calculateTimeScore(); - int exploreScore = calculateExploreScore(); - int skillScore = calculateSkillScore(); - int paulScore = isMayorPaul ? 10 : 0; - int cryptsScore = Math.min(getCrypts(), 5); - int mimicScore = isMimicKilled ? 2 : 0; - int totalScore = timeScore + exploreScore + skillScore + paulScore + cryptsScore + mimicScore; - //Will be this way until ready for pr, so it's easy to debug. - LOGGER.info("Total Score: {} (Time: {}, Explore: {}, Skill: {}, Paul: {}, Crypts: {}, Mimic: {})", totalScore, timeScore, exploreScore, skillScore, paulScore, cryptsScore, mimicScore); - return totalScore; - } - - private static int calculateExploreScore() { - int completedRoomScore = (int) Math.floor(60.0 * getCompletedRooms() / getTotalRooms()); - int percentageRequirement = floorRequirements.get(currentFloor).percentage; - int secretsScore = (int) Math.floor(40 * Math.min(percentageRequirement, getSecretsPercentage()) / percentageRequirement); - return completedRoomScore + secretsScore; - } - - private static int calculateTimeScore() { - int score = 100; - int timeSpent = (int) (System.currentTimeMillis() - startingTime) / 1000; - int timeRequirement = floorRequirements.get(currentFloor).timeLimit; - if (timeSpent < timeRequirement) return score; - - double timePastRequirement = ((double) (timeSpent - timeRequirement) / timeRequirement) * 100; - if (timePastRequirement >= 0 && timePastRequirement < 20) { - score -= (int) timePastRequirement / 2; - } else if (timePastRequirement >= 20 && timePastRequirement < 40) { - score -= (int) (10 + (timePastRequirement - 20) / 4); - } else if (timePastRequirement >= 40 && timePastRequirement < 50) { - score -= (int) (15 + (timePastRequirement - 40) / 5); - } else if (timePastRequirement >= 50 && timePastRequirement < 60) { - score -= (int) (17 + (timePastRequirement - 50) / 6); - } else if (timePastRequirement >= 60) { - score -= (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7); - } - return score; - } - - private static int calculateSkillScore() { - return 20 + (int) Math.floor(80.0 * getCompletedRooms() / getTotalRooms()) - (2 * getDeathCount()) - (10 * getFailedPuzzles()); - } - - private static void reset() { - sent270 = false; - sent300 = false; - isDungeonStarted = false; - isMimicKilled = false; - isMayorPaul = false; - puzzleCount = 0; - currentFloor = ""; - } - - public static void setMimicKilled(boolean killed) { - isMimicKilled = killed; - } - - private static int getTotalRooms() { - return (int) Math.round((getCompletedRooms()) / getClearPercentage()); //Clear% rounds to the closest integer so it can be off by 0.5% at most, this should be accurate enough - } - - private static int getCompletedRooms() { - Matcher completedRoomsMatcher = PlayerListMgr.regexAt(43, COMPLETED_ROOMS_PATTERN); - if (completedRoomsMatcher == null) { - LOGGER.error("Completed rooms pattern doesn't match"); - return 0; - } - return Integer.parseInt(completedRoomsMatcher.group("rooms")); - } - - 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; - } - LOGGER.error("Clear pattern doesn't match"); - return 0; - } - - private static int getDeathCount() { - Matcher matcher = PlayerListMgr.regexAt(25, DEATHS_PATTERN); - if (matcher == null) { - LOGGER.error("Death count pattern doesn't match"); - return 0; - } - //TODO: Turn this into a map of players and their deathcounts, get party members' pets, check if they have spirit pet, if they have it reduce their death count by 0.5 - return Integer.parseInt(matcher.group("deaths")); - } - - private static int getPuzzleCount() { - Matcher matcher = PlayerListMgr.regexAt(47, PUZZLE_COUNT_PATTERN); - if (matcher == null) { - LOGGER.error("Puzzle count pattern doesn't match"); - return 0; - } - return Integer.parseInt(matcher.group("count")); - } - - //Might be replaced to look for puzzle fail messages on chat instead of playerlist - private static int getFailedPuzzles() { - int failedPuzzles = 0; - for (int index = 0; index < puzzleCount; index++) { - Matcher puzzleMatcher = PlayerListMgr.regexAt(48 + index, PUZZLES_PATTERN); - if (puzzleMatcher == null) { - LOGGER.error("Puzzle pattern doesn't match"); - return 0; - } - if (puzzleMatcher.group("state").equals("✖")) failedPuzzles++; - } - return failedPuzzles; - } - - private static double getSecretsPercentage() { - Matcher matcher = PlayerListMgr.regexAt(44, SECRETS_PATTERN); - if (matcher == null) { - LOGGER.error("Secrets pattern doesn't match"); - return 0; - } - return Double.parseDouble(matcher.group("secper")); - } - - private static int getCrypts() { - Matcher matcher = PlayerListMgr.regexAt(33, CRYPTS_PATTERN); - if (matcher == null) { - LOGGER.error("Crypts pattern doesn't match"); - return 0; - } - return Integer.parseInt(matcher.group("crypts")); - } - - 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("Floor pattern doesn't match"); - } - - record FloorRequirement(int percentage, int timeLimit) { - } + private static final SkyblockerConfig.DungeonScore SCORE_CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; + private static final SkyblockerConfig.MimicMessages MIMIC_MESSAGES_CONFIG = SkyblockerConfigManager.get().locations.dungeons.mimicMessages; + private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Dungeon Score"); + //Scoreboard patterns + private static final Pattern CLEARED_PATTERN = Pattern.compile("Cleared: (?\\d+)%.*"); + private static final Pattern DUNGEON_START_PATTERN = Pattern.compile("(?:Auto-closing|Starting) in: \\d:\\d+"); + private static final Pattern FLOOR_PATTERN = Pattern.compile(".*?(?=T)The Catacombs \\((?[EFM]\\D*\\d*)\\)"); + //Playerlist patterns + private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?\\d+\\.?\\d*)%"); + private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?.)](?: \\(\\w+\\))?"); + private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?\\d+)\\)"); + private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?\\d+)"); + private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?\\d+)"); + //Chat patterns + private static final Pattern DEATHS_PATTERN = Pattern.compile(".*?\u2620 (?\\S+) .*"); + //Other patterns + private static final Pattern MIMIC_FLOOR_FILTER_PATTERN = Pattern.compile("[EFM][12345]?"); + + private static String currentFloor; + private static boolean sent270; + private static boolean sent300; + private static boolean isMimicKilled; + private static int puzzleCount; + private static boolean isDungeonStarted; + private static boolean isMayorPaul; + private static long startingTime; + private static int deathCount; + private static boolean firstDeathHasSpiritPet; + private static boolean bloodRoomCompleted; + private static final Map 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) -> { + String str = message.getString(); + checkMessageForDeaths(str); + checkMessageForWatcher(str); + }); + } + + public static void tick() { + MinecraftClient client = MinecraftClient.getInstance(); + if (!Utils.isInDungeons() || client.player == null) { + reset(); + return; + } + if (!isDungeonStarted) { + if (checkIfDungeonStarted()) onDungeonStart(); + return; + } + int score = calculateScore(); + if (!sent270 && score >= 270 && score < 300) { + if (SCORE_CONFIG.enableDungeonScore270Message) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown(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(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() { + sent270 = false; + sent300 = false; + isDungeonStarted = false; + isMimicKilled = false; + isMayorPaul = false; + firstDeathHasSpiritPet = false; + deathCount = 0; + currentFloor = ""; + } + + private static void onDungeonStart() { + setCurrentFloor(); + isDungeonStarted = true; + puzzleCount = getPuzzleCount(); + isMayorPaul = Utils.getMayor().equals("Paul"); + startingTime = System.currentTimeMillis(); + } + + private static int calculateScore() { + int timeScore = calculateTimeScore(); + int exploreScore = calculateExploreScore(); + int skillScore = calculateSkillScore(); + int bonusScore = calculateBonusScore(); + int totalScore = timeScore + exploreScore + skillScore + bonusScore; + if (currentFloor.equals("E")) totalScore = (int) (totalScore * 0.7); + //Will be this way until ready for pr, so it's easy to debug. + LOGGER.info("Total Score: {} (Time: {}, Explore: {}, Skill: {}, Bonus: {})", totalScore, timeScore, exploreScore, skillScore, bonusScore); + return totalScore; + } + + private static int calculateSkillScore() { + int extraCompletedRooms = 0; //This is needed for calculating the score before going in, so we have the result sooner + if (!DungeonManager.isInBoss()) extraCompletedRooms = bloodRoomCompleted ? 1 : 2; + return 20 + (int) Math.floor(80.0 * (getCompletedRooms() + extraCompletedRooms) / getTotalRooms()) - getPuzzlePenalty() - getDeathScorePenalty(); + } + + private static int calculateExploreScore() { + int extraCompletedRooms = 0; + if (!DungeonManager.isInBoss()) extraCompletedRooms = bloodRoomCompleted ? 1 : 2; + int completedRoomScore = (int) Math.floor(60.0 * (getCompletedRooms() + extraCompletedRooms) / getTotalRooms()); + int percentageRequirement = FloorRequirement.valueOf(currentFloor).percentage; + int secretsScore = (int) Math.floor(40 * Math.min(percentageRequirement, getSecretsPercentage()) / percentageRequirement); + return completedRoomScore + secretsScore; + } + + private static int calculateTimeScore() { + int score = 100; + int timeSpent = (int) (System.currentTimeMillis() - startingTime) / 1000; + int timeRequirement = FloorRequirement.valueOf(currentFloor).timeLimit; + if (timeSpent < timeRequirement) return score; + + double timePastRequirement = ((double) (timeSpent - timeRequirement) / timeRequirement) * 100; + if (timePastRequirement >= 0 && timePastRequirement < 20) { + score -= (int) timePastRequirement / 2; + } else if (timePastRequirement >= 20 && timePastRequirement < 40) { + score -= (int) (10 + (timePastRequirement - 20) / 4); + } else if (timePastRequirement >= 40 && timePastRequirement < 50) { + score -= (int) (15 + (timePastRequirement - 40) / 5); + } else if (timePastRequirement >= 50 && timePastRequirement < 60) { + score -= (int) (17 + (timePastRequirement - 50) / 6); + } else if (timePastRequirement >= 60) { + score -= (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7); + } + return score; + } + + private static int calculateBonusScore() { + int paulScore = isMayorPaul ? 10 : 0; + int cryptsScore = Math.min(getCrypts(), 5); + int mimicScore = isMimicKilled ? 2 : 0; + if (getSecretsPercentage() >= 100 && !MIMIC_FLOOR_FILTER_PATTERN.matcher(currentFloor).matches()) mimicScore = 2; //If mimic kill is not announced but all secrets are found, mimic must've been killed + return paulScore + cryptsScore + mimicScore; + } + + private static boolean checkIfDungeonStarted() { + return Utils.STRING_SCOREBOARD.stream().anyMatch(s -> DUNGEON_START_PATTERN.matcher(s).matches()); + } + + public static boolean isEntityMimic(Entity entity) { + if (!Utils.isInDungeons()) return false; + if (MIMIC_FLOOR_FILTER_PATTERN.matcher(currentFloor).matches()) return false; + if (entity == null) return false; + if (!(entity instanceof ZombieEntity zombie)) return false; + if (!zombie.isBaby()) return false; + try { + DefaultedList armor = (DefaultedList) zombie.getArmorItems(); + return armor.stream().allMatch(ItemStack::isEmpty); + } catch (NullPointerException e) { + return false; + } catch (ClassCastException f) { + f.printStackTrace(); + return false; + } + } + + public static void handleEntityDeath(Entity entity) { + if (isMimicKilled) return; + if (!isEntityMimic(entity)) return; + isMimicKilled = true; + } + + public static void setMimicKilled(boolean state) { + isMimicKilled = state; + } + + private static int getTotalRooms() { + int completedRooms = getCompletedRooms(); + return (int) Math.round(completedRooms / getClearPercentage()); + } + + private static int getCompletedRooms() { + Matcher matcher = PlayerListMgr.regexAt(43, COMPLETED_ROOMS_PATTERN); + return matcher != null ? Integer.parseInt(matcher.group("rooms")) : 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("Clear pattern doesn't match"); + return 0; + } + + 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; + } + + //Possible states: ✖, ✦, ✔ + 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) { + e.printStackTrace(); + LOGGER.error("[Skyblocker] Spirit pet lookup by name failed! Name: {} - Cause: {}", name, e.getMessage()); + } + return false; + }); + } + + private static void checkMessageForDeaths(String message) { + if (!Utils.isInDungeons()) return; + 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; + } + + 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("Floor pattern doesn't match"); + } + + 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; + } + } } -- cgit From cbdaa7a2b69ed2b17e36548b707400f416154c4f Mon Sep 17 00:00:00 2001 From: Emirlol <81419447+Emirlol@users.noreply.github.com> Date: Mon, 15 Jan 2024 19:05:55 +0300 Subject: Added dungeon score to the hud and cleaned up code --- .../skyblocker/skyblock/dungeon/DungeonScore.java | 73 ++++++++++++---------- .../skyblock/dungeon/DungeonScoreHUD.java | 38 +++++++++++ .../skyblocker/skyblock/filters/MimicFilter.java | 6 +- 3 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java (limited to 'src/main/java/de/hysky/skyblocker/skyblock') 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 0334290d..0a9aec50 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -56,13 +56,14 @@ public class DungeonScore { private static boolean sent270; private static boolean sent300; private static boolean isMimicKilled; - private static int puzzleCount; - private static boolean isDungeonStarted; + private static boolean dungeonStarted; private static boolean isMayorPaul; - private static long startingTime; - private static int deathCount; 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 SpiritPetCache = new HashMap<>(); public static void init() { @@ -70,6 +71,7 @@ public class DungeonScore { SkyblockEvents.LEAVE.register(SpiritPetCache::clear); ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); ClientReceiveMessageEvents.GAME.register((message, overlay) -> { + if (!Utils.isInDungeons() || !dungeonStarted) return; String str = message.getString(); checkMessageForDeaths(str); checkMessageForWatcher(str); @@ -82,11 +84,11 @@ public class DungeonScore { reset(); return; } - if (!isDungeonStarted) { + if (!dungeonStarted) { if (checkIfDungeonStarted()) onDungeonStart(); return; } - int score = calculateScore(); + score = calculateScore(); if (!sent270 && score >= 270 && score < 300) { if (SCORE_CONFIG.enableDungeonScore270Message) { MessageScheduler.INSTANCE.sendMessageAfterCooldown(SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")); @@ -116,19 +118,23 @@ public class DungeonScore { } private static void reset() { + currentFloor = ""; sent270 = false; sent300 = false; - isDungeonStarted = false; isMimicKilled = false; + dungeonStarted = false; isMayorPaul = false; firstDeathHasSpiritPet = false; + bloodRoomCompleted = false; + startingTime = 0L; + puzzleCount = 0; deathCount = 0; - currentFloor = ""; + score = 0; } private static void onDungeonStart() { setCurrentFloor(); - isDungeonStarted = true; + dungeonStarted = true; puzzleCount = getPuzzleCount(); isMayorPaul = Utils.getMayor().equals("Paul"); startingTime = System.currentTimeMillis(); @@ -147,15 +153,11 @@ public class DungeonScore { } private static int calculateSkillScore() { - int extraCompletedRooms = 0; //This is needed for calculating the score before going in, so we have the result sooner - if (!DungeonManager.isInBoss()) extraCompletedRooms = bloodRoomCompleted ? 1 : 2; - return 20 + (int) Math.floor(80.0 * (getCompletedRooms() + extraCompletedRooms) / getTotalRooms()) - getPuzzlePenalty() - getDeathScorePenalty(); + return 20 + (int) Math.floor(80.0 * (getCompletedRooms() + getExtraCompletedRooms()) / getTotalRooms()) - getPuzzlePenalty() - getDeathScorePenalty(); } private static int calculateExploreScore() { - int extraCompletedRooms = 0; - if (!DungeonManager.isInBoss()) extraCompletedRooms = bloodRoomCompleted ? 1 : 2; - int completedRoomScore = (int) Math.floor(60.0 * (getCompletedRooms() + extraCompletedRooms) / getTotalRooms()); + int completedRoomScore = (int) Math.floor(60.0 * (getCompletedRooms() + getExtraCompletedRooms()) / getTotalRooms()); int percentageRequirement = FloorRequirement.valueOf(currentFloor).percentage; int secretsScore = (int) Math.floor(40 * Math.min(percentageRequirement, getSecretsPercentage()) / percentageRequirement); return completedRoomScore + secretsScore; @@ -168,18 +170,11 @@ public class DungeonScore { if (timeSpent < timeRequirement) return score; double timePastRequirement = ((double) (timeSpent - timeRequirement) / timeRequirement) * 100; - if (timePastRequirement >= 0 && timePastRequirement < 20) { - score -= (int) timePastRequirement / 2; - } else if (timePastRequirement >= 20 && timePastRequirement < 40) { - score -= (int) (10 + (timePastRequirement - 20) / 4); - } else if (timePastRequirement >= 40 && timePastRequirement < 50) { - score -= (int) (15 + (timePastRequirement - 40) / 5); - } else if (timePastRequirement >= 50 && timePastRequirement < 60) { - score -= (int) (17 + (timePastRequirement - 50) / 6); - } else if (timePastRequirement >= 60) { - score -= (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7); - } - return score; + 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 score - (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7); } private static int calculateBonusScore() { @@ -191,7 +186,7 @@ public class DungeonScore { } private static boolean checkIfDungeonStarted() { - return Utils.STRING_SCOREBOARD.stream().anyMatch(s -> DUNGEON_START_PATTERN.matcher(s).matches()); + return Utils.STRING_SCOREBOARD.stream().noneMatch(s -> DUNGEON_START_PATTERN.matcher(s).matches()); } public static boolean isEntityMimic(Entity entity) { @@ -214,6 +209,7 @@ public class DungeonScore { public static void handleEntityDeath(Entity entity) { if (isMimicKilled) return; if (!isEntityMimic(entity)) return; + if (MIMIC_MESSAGES_CONFIG.sendMimicMessages) MessageScheduler.INSTANCE.sendMessageAfterCooldown(MIMIC_MESSAGES_CONFIG.mimicMessage); isMimicKilled = true; } @@ -221,6 +217,8 @@ public class DungeonScore { isMimicKilled = state; } + //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. private static int getTotalRooms() { int completedRooms = getCompletedRooms(); return (int) Math.round(completedRooms / getClearPercentage()); @@ -231,6 +229,12 @@ public class DungeonScore { return matcher != null ? Integer.parseInt(matcher.group("rooms")) : 0; } + private static int getExtraCompletedRooms() { //This is needed for calculating the score before going in the boss room & completing the blood room, so we have the result sooner + if (!bloodRoomCompleted) return 2; + if (!DungeonManager.isInBoss()) return 1; + return 0; + } + private static double getClearPercentage() { for (String sidebarLine : Utils.STRING_SCOREBOARD) { Matcher clearMatcher = CLEARED_PATTERN.matcher(sidebarLine); @@ -306,7 +310,6 @@ public class DungeonScore { } private static void checkMessageForDeaths(String message) { - if (!Utils.isInDungeons()) return; if (!message.startsWith("\u2620", 1)) return; Matcher matcher = DEATHS_PATTERN.matcher(message); if (!matcher.matches()) return; @@ -317,9 +320,7 @@ public class DungeonScore { else return s; }); CompletableFuture.supplyAsync(() -> hasSpiritPet(whoDied)) - .thenAccept(hasSpiritPet -> { - firstDeathHasSpiritPet = hasSpiritPet; - }); + .thenAccept(hasSpiritPet -> firstDeathHasSpiritPet = hasSpiritPet); } private static void checkMessageForWatcher(String message) { @@ -336,6 +337,14 @@ public class DungeonScore { LOGGER.error("Floor pattern doesn't match"); } + public static int getScore() { + return score; + } + + public static boolean isDungeonStarted() { + return dungeonStarted; + } + enum FloorRequirement { E(30, 1200), F1(30, 600), 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..9da12426 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java @@ -0,0 +1,38 @@ +package de.hysky.skyblocker.skyblock.dungeon; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public class DungeonScoreHUD { + + public static void init() { + HudRenderCallback.EVENT.register(DungeonScoreHUD::onHudRender); + } + + private static void onHudRender(DrawContext context, float tickDelta) { + if (!Utils.isInDungeons() || !DungeonScore.isDungeonStarted()) return; + + int x = SkyblockerConfigManager.get().locations.dungeons.mapX; + int y = SkyblockerConfigManager.get().locations.dungeons.mapY; + int size = (int) (128 * SkyblockerConfigManager.get().locations.dungeons.mapScaling); + context.drawCenteredTextWithShadow(MinecraftClient.getInstance().textRenderer, + Text.literal("Score: ").append(formatScore(DungeonScore.getScore())), + x + (size >> 1), + y + size + 5, + 0x00FFFFFF); + } + + private static Text formatScore(int score) { + if (score < 100) return Text.literal(String.valueOf(score)).withColor(0xDC1A1A).append(Text.literal(" (D)").formatted(Formatting.GRAY)); + if (score < 160) return Text.literal(String.valueOf(score)).withColor(0x4141FF).append(Text.literal(" (C)").formatted(Formatting.GRAY)); + if (score < 230) return Text.literal(String.valueOf(score)).withColor(0x7FCC19).append(Text.literal(" (B)").formatted(Formatting.GRAY)); + if (score < 270) return Text.literal(String.valueOf(score)).withColor(0x7F3FB2).append(Text.literal(" (A)").formatted(Formatting.GRAY)); + if (score < 300) return Text.literal(String.valueOf(score)).withColor(0xF1E252).append(Text.literal(" (S)").formatted(Formatting.GRAY)); + return Text.literal(String.valueOf(score)).withColor(0xF1E252).formatted(Formatting.BOLD).append(Text.literal(" (S+)").formatted(Formatting.GRAY)); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java index 0fce5b2c..51482794 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java @@ -2,6 +2,7 @@ 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; @@ -10,7 +11,7 @@ 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.mimicMessages.mimicMessage + "\\E)$"); + super("(?:Mimic dead!?|Mimic Killed!|\\$SKYTILS-DUNGEON-SCORE-MIMIC\\$|\\Q" + SkyblockerConfigManager.get().locations.dungeons.mimicMessages.mimicMessage + "\\E)$"); } @Override @@ -20,7 +21,8 @@ public class MimicFilter extends ChatPatternListener { @Override protected boolean onMatch(Text message, Matcher matcher) { + if (!Utils.isInDungeons()) return false; DungeonScore.setMimicKilled(true); - return false; + return true; } } -- cgit From 52eb325a71664cd48b527dde13ba1df64a3b4777 Mon Sep 17 00:00:00 2001 From: Emirlol <81419447+Emirlol@users.noreply.github.com> Date: Tue, 16 Jan 2024 10:12:39 +0300 Subject: Made the score hud element's location and size configurable --- .../skyblocker/skyblock/dungeon/DungeonMap.java | 12 +----- .../skyblock/dungeon/DungeonMapConfigScreen.java | 48 ++++++++++++++++------ .../skyblock/dungeon/DungeonScoreHUD.java | 41 +++++++++--------- 3 files changed, 57 insertions(+), 44 deletions(-) (limited to 'src/main/java/de/hysky/skyblocker/skyblock') 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 { } } - publ