aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-08-02 02:29:44 +0200
committerLinnea Gräf <nea@nea.moe>2025-08-02 02:29:44 +0200
commit5c0a7375e94f0b908086edbecbb0f82838e1b181 (patch)
tree559cf1b44ad8dc7d163bc314f1ddd9222971fa36
parentc60b85087d63e52eb26d504c7478b8b4663178ce (diff)
downloadSkyblocker-profile-viewer.tar.gz
Skyblocker-profile-viewer.tar.bz2
Skyblocker-profile-viewer.zip
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/model/GenericCatacombs.java37
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/ProfileViewerScreenRework.java11
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/pages/DungeonsPage.java97
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BarWidget.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/rework/widgets/BoxedTextWidget.java91
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Formatters.java34
-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)bin5058 -> 5058 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/profile_viewer/generic_background.png.mcmeta10
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
index 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
Binary files differ
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
+ }
+ }
+}