diff options
author | Linnea Gräf <nea@nea.moe> | 2025-08-02 02:29:44 +0200 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2025-08-02 02:29:44 +0200 |
commit | 5c0a7375e94f0b908086edbecbb0f82838e1b181 (patch) | |
tree | 559cf1b44ad8dc7d163bc314f1ddd9222971fa36 | |
parent | c60b85087d63e52eb26d504c7478b8b4663178ce (diff) | |
download | Skyblocker-profile-viewer.tar.gz Skyblocker-profile-viewer.tar.bz2 Skyblocker-profile-viewer.zip |
cata pageprofile-viewer
-rw-r--r-- | src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java | 37 | ||||
-rw-r--r-- | src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/ProfileViewerScreenRework.java | 11 | ||||
-rw-r--r-- | src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/pages/DungeonsPage.java | 97 | ||||
-rw-r--r-- | src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BarWidget.java | 4 | ||||
-rw-r--r-- | src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BoxedTextWidget.java | 91 | ||||
-rw-r--r-- | src/main/java/de/hysky/skyblocker/utils/Formatters.java | 34 | ||||
-rw-r--r-- | src/main/resources/assets/skyblocker/textures/gui/sprites/profile_viewer/generic_background.png (renamed from src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png) | bin | 5058 -> 5058 bytes | |||
-rw-r--r-- | src/main/resources/assets/skyblocker/textures/gui/sprites/profile_viewer/generic_background.png.mcmeta | 10 |
8 files changed, 267 insertions, 17 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java index 12d477aa..6e484465 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java @@ -104,9 +104,18 @@ public class GenericCatacombs { public @Nullable Double seven; /** + * @see #getValue + */ + public double getValueOrZero(int oneIndexedFloor) { + var value = getValue(oneIndexedFloor); + if (value == null) return 0; + return value; + } + + /** * @param oneIndexedFloor one indexed floor (F1 = 1), with Entrance = 0. */ - public @Nullable Double getBest(int oneIndexedFloor) { + public @Nullable Double getValue(int oneIndexedFloor) { return switch (oneIndexedFloor) { case 0 -> entrance; case 1 -> one; @@ -127,6 +136,30 @@ public class GenericCatacombs { } public static class AggregateStat extends PerFloorDisambiguation { - public @Nullable Double total; + /** + * @see #getManuallyCalculatedTotal() + */ + public double total; + + private static double coerce0(@Nullable Double d) { + return d != null ? d : 0; + } + + /** + * {@link #total} seems to be off by quite a bit sometimes. This manually calculates the total. + */ + public double getManuallyCalculatedTotal() { + double result = 0; + result += coerce0(entrance); + result += coerce0(one); + result += coerce0(two); + result += coerce0(three); + result += coerce0(four); + result += coerce0(five); + result += coerce0(six); + result += coerce0(seven); + return result; + } + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/ProfileViewerScreenRework.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/ProfileViewerScreenRework.java index c3b265c9..500f51d6 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/ProfileViewerScreenRework.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/ProfileViewerScreenRework.java @@ -34,6 +34,11 @@ public class ProfileViewerScreenRework extends Screen { public static final Gson GSON = new GsonBuilder() .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) .create(); + + /** + * Convention of whether to use text shadow in pv draw calls. + */ + public static final boolean TEXT_SHADOW = false; public static final List<Function<ProfileLoadState.SuccessfulLoad, ProfileViewerPage>> PAGE_CONSTRUCTORS = new ArrayList<>(); @@ -149,7 +154,11 @@ public class ProfileViewerScreenRework extends Screen { .thenApplyAsync(load -> { displayLoadedProfile(load); return load; - }, MinecraftClient.getInstance()); + }, MinecraftClient.getInstance()) + .exceptionally(ex -> { + LOGGER.error("Failed to apply profile load", ex); + return new ProfileLoadState.Error(ex.getMessage()); + }); } //</editor-fold> diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/pages/DungeonsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/pages/DungeonsPage.java index 154b27e9..1514cdad 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/pages/DungeonsPage.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/pages/DungeonsPage.java @@ -1,19 +1,30 @@ package de.hysky.skyblocker.skyblock.profileviewer.rework.pages; import de.hysky.skyblocker.annotations.Init; +import de.hysky.skyblocker.skyblock.profileviewer.model.DefaultCatacombs; import de.hysky.skyblocker.skyblock.profileviewer.model.Dungeons; +import de.hysky.skyblocker.skyblock.profileviewer.model.GenericCatacombs; import de.hysky.skyblocker.skyblock.profileviewer.model.PlayerData; import de.hysky.skyblocker.skyblock.profileviewer.rework.ProfileLoadState; import de.hysky.skyblocker.skyblock.profileviewer.rework.ProfileViewerPage; import de.hysky.skyblocker.skyblock.profileviewer.rework.ProfileViewerScreenRework; import de.hysky.skyblocker.skyblock.profileviewer.rework.ProfileViewerWidget; import de.hysky.skyblocker.skyblock.profileviewer.rework.widgets.BarWidget; +import de.hysky.skyblocker.skyblock.profileviewer.rework.widgets.BoxedTextWidget; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; import de.hysky.skyblocker.skyblock.tabhud.util.Ico; import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.OptionalInt; +import java.util.stream.IntStream; + +import static de.hysky.skyblocker.utils.Formatters.*; public class DungeonsPage implements ProfileViewerPage { @@ -21,25 +32,83 @@ public class DungeonsPage implements ProfileViewerPage { public DungeonsPage(ProfileLoadState.SuccessfulLoad load) { var dungeonsData = load.member().dungeons; - List<ProfileViewerWidget> dungeons = new ArrayList<>(); - dungeons.add(new BarWidget(PlayerData.Skill.CATACOMBS.getName(), PlayerData.Skill.CATACOMBS.getIcon(), PlayerData.Skill.CATACOMBS.getLevelInfo(dungeonsData.dungeonInfo.catacombs.experience), OptionalInt.empty(), OptionalInt.empty())); - dungeons.add(new BarWidget(Dungeons.Class.HEALER.getName(), Dungeons.Class.HEALER.getIcon(), dungeonsData.getClassData(Dungeons.Class.HEALER).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); - dungeons.add(new BarWidget(Dungeons.Class.MAGE.getName(), Dungeons.Class.MAGE.getIcon(), dungeonsData.getClassData(Dungeons.Class.MAGE).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); - dungeons.add(new BarWidget(Dungeons.Class.BERSERK.getName(), Dungeons.Class.BERSERK.getIcon(), dungeonsData.getClassData(Dungeons.Class.BERSERK).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); - dungeons.add(new BarWidget(Dungeons.Class.ARCHER.getName(), Dungeons.Class.ARCHER.getIcon(), dungeonsData.getClassData(Dungeons.Class.ARCHER).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); - dungeons.add(new BarWidget(Dungeons.Class.TANK.getName(), Dungeons.Class.TANK.getIcon(), dungeonsData.getClassData(Dungeons.Class.TANK).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); + widgets.add( + widget(0, 0, new BarWidget(PlayerData.Skill.CATACOMBS.getName(), PlayerData.Skill.CATACOMBS.getIcon(), PlayerData.Skill.CATACOMBS.getLevelInfo(dungeonsData.dungeonInfo.catacombs.experience), OptionalInt.empty(), OptionalInt.empty())) + ); + List<BarWidget> classes = new ArrayList<>(); + classes.add(new BarWidget(Dungeons.Class.HEALER.getName(), Dungeons.Class.HEALER.getIcon(), dungeonsData.getClassData(Dungeons.Class.HEALER).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); + classes.add(new BarWidget(Dungeons.Class.MAGE.getName(), Dungeons.Class.MAGE.getIcon(), dungeonsData.getClassData(Dungeons.Class.MAGE).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); + classes.add(new BarWidget(Dungeons.Class.BERSERK.getName(), Dungeons.Class.BERSERK.getIcon(), dungeonsData.getClassData(Dungeons.Class.BERSERK).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); + classes.add(new BarWidget(Dungeons.Class.ARCHER.getName(), Dungeons.Class.ARCHER.getIcon(), dungeonsData.getClassData(Dungeons.Class.ARCHER).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); + classes.add(new BarWidget(Dungeons.Class.TANK.getName(), Dungeons.Class.TANK.getIcon(), dungeonsData.getClassData(Dungeons.Class.TANK).getLevelInfo(), OptionalInt.empty(), OptionalInt.empty())); + + LevelFinder.LevelInfo classAverageLevelInfo = new LevelFinder.LevelInfo(0, 0); + for (var widget : classes) { + var currentClassInfo = widget.getLevelInfo(); + classAverageLevelInfo.level += currentClassInfo.level; + classAverageLevelInfo.fill += currentClassInfo.fill; // Should partial levels count towards class average? + if (classAverageLevelInfo.nextLevelXP == 0 + || classAverageLevelInfo.nextLevelXP < currentClassInfo.nextLevelXP + || (classAverageLevelInfo.nextLevelXP == currentClassInfo.nextLevelXP && classAverageLevelInfo.levelXP < currentClassInfo.levelXP)) { + classAverageLevelInfo.levelXP = currentClassInfo.levelXP; + classAverageLevelInfo.nextLevelXP = currentClassInfo.nextLevelXP; // TODO: this model for XP to next level is not really correct. This is XP towards the next fifth of a level. A more correct approach would be a lot more complicated than this. + } + classAverageLevelInfo.xp += currentClassInfo.xp; + } + double classAverage = (classAverageLevelInfo.level + classAverageLevelInfo.fill) / 5.0; + classAverageLevelInfo.level = (int) classAverage; + classAverageLevelInfo.fill = classAverage - classAverageLevelInfo.level; + classes.addFirst(new BarWidget("All Classes", Ico.NETHER_STAR, classAverageLevelInfo, OptionalInt.empty(), OptionalInt.empty())); int i = 0; - for (var dungeon : dungeons) { - int x = i < 6 ? 88 : 88 + 113; - int y = (i % 6) * (2 + 26); - i++; + for (var classWidget : classes) { widgets.add(widget( - x, y, dungeon + ProfileViewerScreenRework.PAGE_WIDTH - BarWidget.WIDTH, (BarWidget.HEIGHT + 2) * i, classWidget )); + i++; } - widgets.add(widget(0, 0, new EntityViewerWidget(load.mainMemberId()))); - widgets.add(widget(0, 112, new PlayerMetaWidget(load))); + + int runTotal = (int) (dungeonsData.dungeonInfo.catacombs.tierCompletions.getManuallyCalculatedTotal() + dungeonsData.dungeonInfo.masterModeCatacombs.tierCompletions.getManuallyCalculatedTotal()); + + widgets.add(widget( + 0, BarWidget.HEIGHT + 5, BoxedTextWidget.boxedText(BarWidget.WIDTH - BoxedTextWidget.PADDING * 2, + List.of( + Text.of("Secrets: " + INTEGER_NUMBERS.format(dungeonsData.secrets)), + Text.of("Secrets/Run: " + DOUBLE_NUMBERS.format(dungeonsData.secrets / (float) runTotal)) + )) + )); + + var runWidget = widget( + BarWidget.WIDTH + 5, 0, + createFloorStatWidget(dungeonsData.dungeonInfo.catacombs, "F") + ); + widgets.add(runWidget); + widgets.add(widget( + BarWidget.WIDTH + 5, runWidget.getHeight() + 5, + createFloorStatWidget(dungeonsData.dungeonInfo.masterModeCatacombs, "M") + )); + // TODO: for tomorrow morning me: add a toggle button + } + + ProfileViewerWidget createFloorStatWidget(GenericCatacombs cata, String prefix) { + return BoxedTextWidget.boxedTextWithHover(ProfileViewerScreenRework.PAGE_WIDTH - BarWidget.WIDTH * 2 - 10 - BoxedTextWidget.PADDING * 2, + IntStream.of(1, 2, 3, 4, 5, 6, 7) + .mapToObj(floor -> + BoxedTextWidget.hover( + Text.of(prefix + floor + " Runs: " + INTEGER_NUMBERS.format(cata.tierCompletions.getValueOrZero(floor))), + List.of( + Text.literal("Personal Best: ").formatted(Formatting.GRAY).append(formatPB(cata.fastestTime.getValue(floor))), + Text.literal("Personal Best (S): ").formatted(Formatting.GRAY).append(formatPB(cata.fastestTimeS.getValue(floor))), + Text.literal("Personal Best (S+): ").formatted(Formatting.GREEN).append(formatPB(cata.fastestTimeSPlus.getValue(floor))) + )) + ).toList()); + } + + + private static Text formatPB(@Nullable Double d) { + if (d != null) + return Text.literal(formatTimespan(Duration.ofMillis(d.longValue()))).formatted(Formatting.GOLD); + return Text.literal("N/A").formatted(Formatting.DARK_GRAY); } @Init diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BarWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BarWidget.java index 854b167d..a12b80b0 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BarWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BarWidget.java @@ -35,6 +35,10 @@ public final class BarWidget implements ProfileViewerWidget { private final OptionalInt levelCap; private final OptionalInt softSkillCap; + public LevelFinder.LevelInfo getLevelInfo() { + return levelInfo; + } + public BarWidget( String name, ItemStack icon, diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BoxedTextWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BoxedTextWidget.java new file mode 100644 index 00000000..54700afe --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BoxedTextWidget.java @@ -0,0 +1,91 @@ +package de.hysky.skyblocker.skyblock.profileviewer.rework.widgets; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.rework.ProfileViewerScreenRework; +import de.hysky.skyblocker.skyblock.profileviewer.rework.ProfileViewerWidget; +import de.hysky.skyblocker.utils.render.HudHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Identifier; + +import java.util.List; +import java.util.stream.StreamSupport; + +public class BoxedTextWidget implements ProfileViewerWidget { + + private static final Identifier BACKGROUND = Identifier.of(SkyblockerMod.NAMESPACE, "profile_viewer/generic_background"); + public static final int PADDING = 2; + private static final int GAP = 1; + + public record TextWithHover( + Text text, + List<Text> hover + ) {} + + final int width; + final int height; + final List<TextWithHover> textLines; + final TextRenderer textRenderer; + + public BoxedTextWidget(int width, int height, List<TextWithHover> textLines, TextRenderer textRenderer) { + this.width = width; + this.height = height; + this.textLines = textLines; + this.textRenderer = textRenderer; + } + + public static BoxedTextWidget boxedTextWithHover(int width, List<TextWithHover> textLines) { + var textRenderer = MinecraftClient.getInstance().textRenderer; + return new BoxedTextWidget(width + 2 * PADDING, (textRenderer.fontHeight + GAP) * textLines.size() - GAP + 2 * PADDING, textLines, textRenderer); + } + + + public static TextWithHover hover(Text text, List<Text> hover) { + return new TextWithHover(text, hover); + } + + public static TextWithHover nohover(Text text) { + return new TextWithHover(text, List.of()); + } + + public static BoxedTextWidget boxedText(int width, Iterable<Text> textLines) { + return boxedTextWithHover(width, StreamSupport.stream(textLines.spliterator(), false) + .map(it -> new TextWithHover(it, List.of())) + .toList() + ); + } + + @Override + public void render(DrawContext drawContext, int x, int y, int mouseX, int mouseY, float deltaTicks) { + HudHelper.renderNineSliceColored(drawContext, BACKGROUND, x, y, width, height, Colors.WHITE); + int lineSkip = GAP + textRenderer.fontHeight; + var matrices = drawContext.getMatrices(); + var availableSpace = width - 2 * PADDING; + for (int i = 0; i < textLines.size(); i++) { + var line = textLines.get(i); + var textWidth = textRenderer.getWidth(line.text()); + matrices.pushMatrix(); + matrices.translate(x + PADDING, y + PADDING + i * lineSkip + textRenderer.fontHeight / 2F); + if (textWidth > availableSpace) + matrices.scale((float) availableSpace / textWidth); + drawContext.drawText(textRenderer, line.text(), 0, -textRenderer.fontHeight / 2, Colors.WHITE, ProfileViewerScreenRework.TEXT_SHADOW); + matrices.popMatrix(); + if (!line.hover().isEmpty() && isHovered(x + PADDING, y + PADDING + i * lineSkip, Math.min(textWidth, availableSpace), textRenderer.fontHeight, mouseX, mouseY)) + drawContext.drawTooltip(textRenderer, line.hover(), mouseX, mouseY); + } + } + + @Override + public int getHeight() { + return height; + } + + @Override + public int getWidth() { + return width; + } + +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Formatters.java b/src/main/java/de/hysky/skyblocker/utils/Formatters.java index 21c13d74..22091331 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Formatters.java +++ b/src/main/java/de/hysky/skyblocker/utils/Formatters.java @@ -9,6 +9,7 @@ import net.minecraft.util.Util; import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; +import java.time.Duration; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Locale; @@ -75,6 +76,39 @@ public class Formatters { } } + public static String formatTimespan(Duration duration) { + return formatTimespanMs(duration.toMillis()); + } + + public static String formatTimespanMs(long millis) { + var isNegative = false; + if (millis < 0) { + isNegative = true; + millis = -millis; + } + long seconds = millis / 1000; + millis = millis % 1000; + long minutes = seconds / 60; + seconds = seconds % 60; + long hours = minutes / 60; + minutes = minutes % 60; + var builder = new StringBuilder(); + if (hours != 0) + builder.append(hours).append(':'); + if (!builder.isEmpty() || minutes != 0) { + builder.append("%02d:".formatted(minutes)); + } + if (!builder.isEmpty()) { + builder.append("%02d.".formatted(seconds)); + } else { + builder.append(seconds).append('.'); + } + builder.append(millis / 100); + if (isNegative) + builder.insert(0, '-'); + return builder.toString(); + } + /** * Returns the formatting for the time, always returns 12 hour in test environments. */ diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/profile_viewer/generic_background.png Binary files differindex 379557e4..379557e4 100644 --- a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png +++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/profile_viewer/generic_background.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/profile_viewer/generic_background.png.mcmeta b/src/main/resources/assets/skyblocker/textures/gui/sprites/profile_viewer/generic_background.png.mcmeta new file mode 100644 index 00000000..2412d8fe --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/profile_viewer/generic_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 109, + "height": 110, + "border": 2 + } + } +} |