diff --git a/src/main/java/org/saturnclient/common/provider/SaturnProvider.java b/src/main/java/org/saturnclient/common/provider/SaturnProvider.java index 5efbf2e..4d63eec 100644 --- a/src/main/java/org/saturnclient/common/provider/SaturnProvider.java +++ b/src/main/java/org/saturnclient/common/provider/SaturnProvider.java @@ -22,6 +22,12 @@ public interface SaturnProvider { /** The Saturn Client mod version, e.g. {@code 0.1.0-beta3+1.21.11}. */ public String getModVersion(); + /** + * The signed-in player's 64x64 skin texture, or the default skin until + * theirs has loaded. + */ + public IdentifierRef getPlayerSkin(); + /** Shows a warning toast. Safe to call from any thread. */ public void showWarning(String title, String message); } diff --git a/src/main/java/org/saturnclient/cosmetics/Emotes.java b/src/main/java/org/saturnclient/cosmetics/Emotes.java index b4f00dd..f245f64 100644 --- a/src/main/java/org/saturnclient/cosmetics/Emotes.java +++ b/src/main/java/org/saturnclient/cosmetics/Emotes.java @@ -8,6 +8,7 @@ import java.util.Optional; import org.saturnclient.common.provider.Providers; import org.saturnclient.emotes.core.Emote; import org.saturnclient.emotes.core.EmoteRegistry; +import org.saturnclient.ui.Utils; /** * Saturn's emotes, loaded from {@code assets/saturnclient/player_animations/.json} (Emotecraft @@ -23,6 +24,13 @@ public class Emotes { /** The emotes that loaded, in wheel order. */ public static List availableEmotes = new ArrayList<>(); + /** + * The emote's name as shown to players. + */ + public static String getName(String id) { + return id.equals("tpose") ? "T-Pose" : Utils.nameFromId(id); + } + public static void initialize() { for (String id : IDS) { String path = "/assets/saturnclient/player_animations/" + id + ".json"; diff --git a/src/main/java/org/saturnclient/ui/Element.java b/src/main/java/org/saturnclient/ui/Element.java index 41f90e7..6677713 100644 --- a/src/main/java/org/saturnclient/ui/Element.java +++ b/src/main/java/org/saturnclient/ui/Element.java @@ -11,7 +11,6 @@ public class Element { public boolean focused; public Animation animation; public float scale = 1.0f; - public Integer duration = null; // Element duration, will fade out if not null public void render(RenderScope renderScope, ElementContext ctx) { } diff --git a/src/main/java/org/saturnclient/ui/Utils.java b/src/main/java/org/saturnclient/ui/Utils.java index 5be5f4d..8b85ad4 100644 --- a/src/main/java/org/saturnclient/ui/Utils.java +++ b/src/main/java/org/saturnclient/ui/Utils.java @@ -1,7 +1,5 @@ package org.saturnclient.ui; -import java.util.List; - import org.saturnclient.common.provider.GLFWProvider; public class Utils { @@ -47,17 +45,21 @@ public class Utils { }; } - public static T getOrNull(List list, int index) { - return (index >= 0 && index < list.size()) ? list.get(index) : null; + /** + * A display name from an id: each underscore-separated word capitalized, so + * "black_hole_white" becomes "Black Hole White". + */ + public static String nameFromId(String id) { + StringBuilder builder = new StringBuilder(); + for (String word : id.split("_")) { + if (word.isEmpty()) { + continue; + } + if (!builder.isEmpty()) { + builder.append(' '); + } + builder.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)); + } + return builder.toString(); } - - // public static void notify(NotificationKind kind, String title, String toast) - // { - // if (SaturnClient.client.currentScreen instanceof SaturnScreen) { - // ((SaturnScreen) SaturnClient.client.currentScreen) - // .draw(new Notification(SaturnClient.client.currentScreen.width, - // SaturnClient.client.currentScreen.height, kind, - // title, toast)); - // } - // } } diff --git a/src/main/java/org/saturnclient/ui/anim/FadeOut.java b/src/main/java/org/saturnclient/ui/anim/FadeOut.java deleted file mode 100644 index 4172883..0000000 --- a/src/main/java/org/saturnclient/ui/anim/FadeOut.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.saturnclient.ui.anim; - -import org.saturnclient.config.AnimationConfig; -import org.saturnclient.ui.Element; - -public class FadeOut extends Animation { - float targetOpacity; - - public FadeOut(int duration) { - super(duration); - } - - public FadeOut(AnimationConfig config) { - super(config); - } - - @Override - public void tick(double progress, Element element) { - element.opacity = targetOpacity - (float) progress; - } - - @Override - public void init(Element element) { - targetOpacity = element.opacity; - // element.opacity = 1.0f; - } -} diff --git a/src/main/java/org/saturnclient/ui/anim/SlideX.java b/src/main/java/org/saturnclient/ui/anim/SlideX.java deleted file mode 100644 index 82792ba..0000000 --- a/src/main/java/org/saturnclient/ui/anim/SlideX.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.saturnclient.ui.anim; - -import org.saturnclient.config.AnimationConfig; -import org.saturnclient.ui.Element; - -public class SlideX extends Animation { - int offset = 20; - double startX; - - public SlideX(AnimationConfig config) { - super(config); - } - - public SlideX(AnimationConfig config, int offset) { - super(config); - this.offset = offset; - } - - public SlideX(int duration) { - super(duration); - } - - public SlideX(int duration, int offset) { - super(duration); - this.offset = offset; - } - - @Override - public void init(Element element) { - startX = element.x - offset; - element.x = (int) startX; - } - - @Override - public void tick(double progress, Element element) { - double currentX = startX + offset * progress; - element.x = (int) currentX; - } -} diff --git a/src/main/java/org/saturnclient/ui/components/AccountChip.java b/src/main/java/org/saturnclient/ui/components/AccountChip.java new file mode 100644 index 0000000..554f9c6 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/AccountChip.java @@ -0,0 +1,54 @@ +package org.saturnclient.ui.components; + +import org.saturnclient.client.ServiceClient; +import org.saturnclient.common.provider.Providers; +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.elements.Field; +import org.saturnclient.ui.resources.Fonts; + +/** + * The signed-in player: their skin's face, their name, and a dot that's green + * while connected to Saturn. + */ +public class AccountChip extends Element { + public static final int HEIGHT = 40; + + private static final int HEAD = 28; + private static final int PADDING = 6; + private static final int DOT = 7; + private static final float NAME_SCALE = 0.7f; + private static final int CONNECTED = 0xFF7FD1A8; + + private final String name; + + public AccountChip() { + String username = Providers.saturn.getClient().getUsername(); + this.name = username != null ? username : "Player"; + this.height = HEIGHT; + this.width = PADDING + HEAD + 10 + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + 10 + DOT + 12; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1, Theme.getPanelBg(), + Theme.getBorder()); + + // The face, then the hat layer over it, from the 64x64 skin + IdentifierRef skin = Providers.saturn.getPlayerSkin(); + int headY = (height - HEAD) / 2; + renderScope.drawTexture(skin, PADDING, headY, 8, 8, HEAD, HEAD, 8, 8, 64, 64); + renderScope.drawTexture(skin, PADDING, headY, 40, 8, HEAD, HEAD, 8, 8, 64, 64); + + int nameX = PADDING + HEAD + 10; + renderScope.drawText(NAME_SCALE, name, nameX, Field.textY(height, NAME_SCALE), Theme.FONT.value, + Theme.FOREGROUND.value); + + int dotX = nameX + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + 10; + renderScope.drawRoundedRectangle(dotX, (height - DOT) / 2, DOT, DOT, DOT / 2, + ServiceClient.isConnected() ? CONNECTED : Theme.withAlpha(0.3f, Theme.FOREGROUND.value)); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/Dock.java b/src/main/java/org/saturnclient/ui/components/Dock.java new file mode 100644 index 0000000..e5d174e --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/Dock.java @@ -0,0 +1,67 @@ +package org.saturnclient.ui.components; + +import java.util.List; + +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; + +/** + * A glass bar of square icon buttons. + */ +public class Dock extends Element { + public record Item(IdentifierRef icon, Runnable onClick) { + } + + public static final int HEIGHT = 52; + + private static final int PADDING = 6; + private static final int ITEM = HEIGHT - PADDING * 2; + private static final int ICON = 18; + private static final int GAP = 4; + + private final List items; + + public Dock(List items) { + this.items = items; + this.height = HEIGHT; + this.width = PADDING * 2 + items.size() * ITEM + (items.size() - 1) * GAP; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, 1, Theme.getPanelBg(), + Theme.withAlpha(0.1f, Theme.FOREGROUND.value)); + + for (int i = 0; i < items.size(); i++) { + int x = getItemX(i); + boolean hovered = ctx.isHovering(x, PADDING, ITEM, ITEM); + + if (hovered) { + renderScope.drawRoundedRectangle(x, PADDING, ITEM, ITEM, Theme.WIDGET_RADIUS.value, + Theme.withAlpha(0.08f, Theme.FOREGROUND.value)); + } + + int offset = (ITEM - ICON) / 2; + renderScope.drawTexture(items.get(i).icon(), x + offset, PADDING + offset, 0, 0, ICON, ICON, + hovered ? Theme.ACCENT.value : Theme.PRIMARY_FG.value); + } + } + + @Override + public void click(int mouseX, int mouseY) { + for (int i = 0; i < items.size(); i++) { + int x = getItemX(i); + if (mouseX >= x && mouseX <= x + ITEM) { + items.get(i).onClick().run(); + return; + } + } + } + + private static int getItemX(int index) { + return PADDING + index * (ITEM + GAP); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/EmoteTile.java b/src/main/java/org/saturnclient/ui/components/EmoteTile.java new file mode 100644 index 0000000..ede8058 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/EmoteTile.java @@ -0,0 +1,68 @@ +package org.saturnclient.ui.components; + +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.config.Theme; +import org.saturnclient.cosmetics.Emotes; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.resources.Fonts; +import org.saturnclient.ui.resources.Textures; + +/** + * One emote in the emote wheel: its preview in its own colors and its name. + * An emote without a preview shows an icon instead. + */ +public class EmoteTile extends Element { + public static final int SIZE = 86; + + private static final int INSET = 6; + private static final int NAME_AREA = 20; + private static final int PLACEHOLDER = 26; + private static final float NAME_SCALE = 0.56f; + + private final String name; + /** The preview, or null when the emote has none */ + private final IdentifierRef preview; + private final Runnable onPlay; + + public EmoteTile(String emote, Runnable onPlay) { + IdentifierRef texture = Textures.getEmotePreview(emote); + + this.name = Emotes.getName(emote); + this.preview = Textures.exists(texture) ? texture : null; + this.onPlay = onPlay; + this.width = SIZE; + this.height = SIZE; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + boolean hovered = ctx.isHovering(); + + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, hovered ? 2 : 1, + Theme.withAlpha(0.72f, Theme.PRIMARY.value), + hovered ? Theme.ACCENT.value : Theme.withAlpha(0.06f, Theme.FOREGROUND.value)); + + int imageSize = height - INSET * 2 - NAME_AREA; + + if (preview != null) { + renderScope.drawTexture(preview, (width - imageSize) / 2, INSET, 0, 0, imageSize, imageSize); + } else { + int iconY = INSET + (imageSize - PLACEHOLDER) / 2; + renderScope.drawTexture(Textures.ICON_EMOTE, (width - PLACEHOLDER) / 2, iconY, 0, 0, PLACEHOLDER, + PLACEHOLDER, Theme.getMutedFg()); + } + + String text = Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - INSET * 2); + int textX = (width - (int) (Fonts.getWidth(text, Theme.FONT.value) * NAME_SCALE)) / 2; + int textY = height - INSET - NAME_AREA + (NAME_AREA - (int) (Fonts.getHeight() * NAME_SCALE)) / 2; + renderScope.drawText(NAME_SCALE, text, textX, textY, Theme.FONT.value, + hovered ? Theme.FOREGROUND.value : Theme.PRIMARY_FG.value); + } + + @Override + public void click(int mouseX, int mouseY) { + onPlay.run(); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/HudModPreview.java b/src/main/java/org/saturnclient/ui/components/HudModPreview.java index a4e803a..ba1754d 100644 --- a/src/main/java/org/saturnclient/ui/components/HudModPreview.java +++ b/src/main/java/org/saturnclient/ui/components/HudModPreview.java @@ -15,13 +15,21 @@ public class HudModPreview extends Element { @Override public void render(RenderScope scope, ElementContext ctx) { - ModLayout layout = feature.getDimensions(); + draw(scope, feature); + } + + /** + * Draws a HUD mod with sample content and the background it has in game, + * at the origin and its layout's unscaled size. + */ + public static void draw(RenderScope scope, HudMod mod) { + ModLayout layout = mod.getDimensions(); if (layout.renderBackground) { scope.drawRoundedRectangle(0, 0, layout.width, layout.height, layout.radius.value, layout.bgColor.value); } - feature.renderDummy(scope); + mod.renderDummy(scope); } } diff --git a/src/main/java/org/saturnclient/ui/elements/Button.java b/src/main/java/org/saturnclient/ui/elements/Button.java index 3c0d5ed..e884242 100644 --- a/src/main/java/org/saturnclient/ui/elements/Button.java +++ b/src/main/java/org/saturnclient/ui/elements/Button.java @@ -6,24 +6,41 @@ import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.resources.Fonts; +/** + * A text button: a translucent, outlined surface that brightens on hover. + */ public class Button extends Element { - private String text; - private Runnable onClick; + public static final int HEIGHT = 40; + + private static final float TEXT_SCALE = 0.72f; + + private final String text; + private final Runnable onClick; public Button(String text, Runnable onClick) { this.text = text; this.onClick = onClick; - this.dimensions(Fonts.getWidth(text, Theme.FONT.value) + 50, 38); + this.width = (int) (Fonts.getWidth(text, Theme.FONT.value) * TEXT_SCALE) + 48; + this.height = HEIGHT; } @Override public void render(RenderScope renderScope, ElementContext ctx) { - renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, - ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value); + boolean hovered = ctx.isHovering(); + drawSurface(renderScope, width, height, hovered); - renderScope.drawText(text, Fonts.centerX(width, text, Theme.FONT.value), Fonts.centerY(height), - Theme.FONT.value, - ctx.isHovering() ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value); + int textX = (width - (int) (Fonts.getWidth(text, Theme.FONT.value) * TEXT_SCALE)) / 2; + renderScope.drawText(TEXT_SCALE, text, textX, Field.textY(height, TEXT_SCALE), Theme.FONT.value, + hovered ? Theme.FOREGROUND.value : Theme.PRIMARY_FG.value); + } + + /** + * The button's surface, shared with {@link IconButton}. + */ + static void drawSurface(RenderScope renderScope, int width, int height, boolean hovered) { + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1, + hovered ? Theme.withAlpha(0.9f, Theme.PRIMARY.value) : Theme.getPanelBg(), + Theme.withAlpha(hovered ? 0.25f : 0.1f, Theme.FOREGROUND.value)); } @Override diff --git a/src/main/java/org/saturnclient/ui/elements/ClickableTexture.java b/src/main/java/org/saturnclient/ui/elements/ClickableTexture.java deleted file mode 100644 index 5936fcb..0000000 --- a/src/main/java/org/saturnclient/ui/elements/ClickableTexture.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.saturnclient.ui.elements; - -import org.saturnclient.common.ref.asset.IdentifierRef; -import org.saturnclient.config.Theme; -import org.saturnclient.ui.Element; -import org.saturnclient.ui.ElementContext; -import org.saturnclient.ui.RenderScope; - -public class ClickableTexture extends Element { - IdentifierRef sprite; - private Runnable onClick; - - public ClickableTexture(IdentifierRef sprite, Runnable onClick) { - this.sprite = sprite; - this.onClick = onClick; - } - - @Override - public void render(RenderScope renderScope, ElementContext ctx) { - renderScope.drawTexture(sprite, 0, 0, 0, 0, width, height, - ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value); - } - - @Override - public void click(int mouseX, int mouseY) { - if (onClick != null) { - onClick.run(); - } - } -} diff --git a/src/main/java/org/saturnclient/ui/elements/IconButton.java b/src/main/java/org/saturnclient/ui/elements/IconButton.java new file mode 100644 index 0000000..8264913 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/IconButton.java @@ -0,0 +1,40 @@ +package org.saturnclient.ui.elements; + +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; + +/** + * A square {@link Button} showing an icon, which turns the accent color on + * hover. + */ +public class IconButton extends Element { + private static final float ICON_RATIO = 0.48f; + + private final IdentifierRef icon; + private final Runnable onClick; + + public IconButton(IdentifierRef icon, Runnable onClick) { + this.icon = icon; + this.onClick = onClick; + this.width = Button.HEIGHT; + this.height = Button.HEIGHT; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + boolean hovered = ctx.isHovering(); + Button.drawSurface(renderScope, width, height, hovered); + + int size = (int) (Math.min(width, height) * ICON_RATIO); + renderScope.drawTexture(icon, (width - size) / 2, (height - size) / 2, 0, 0, size, size, + hovered ? Theme.ACCENT.value : Theme.PRIMARY_FG.value); + } + + @Override + public void click(int mouseX, int mouseY) { + onClick.run(); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Notification.java b/src/main/java/org/saturnclient/ui/elements/Notification.java deleted file mode 100644 index 0e0b471..0000000 --- a/src/main/java/org/saturnclient/ui/elements/Notification.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.saturnclient.ui.elements; - -import org.saturnclient.config.Theme; -import org.saturnclient.config.property.ColorProperty; -import org.saturnclient.config.property.Property; -import org.saturnclient.ui.Element; -import org.saturnclient.ui.ElementContext; -import org.saturnclient.ui.RenderScope; -import org.saturnclient.ui.anim.SlideX; -import org.saturnclient.ui.resources.Fonts; -import org.saturnclient.ui.resources.Textures; - -public class Notification extends Element { - private static ColorProperty error = Property.color(0xFFbf212f); - private static ColorProperty info = Property.color(0xFF264b96); - private static ColorProperty success = Property.color(0xFF27b376); - - public static enum NotificationKind { - Error, - Info, - Success, - } - - NotificationKind kind; - String text; - String title; - - public Notification(int width, int height, NotificationKind kind, String title, String text) { - this.kind = kind; - this.text = text; - this.title = title; - this.width = (int) Math.max(Fonts.getWidth(text, 1) * 0.6f, Fonts.getWidth(title, 1)) + 20; - this.height = 50; - this.x = width - this.width - 15; - this.y = height - this.height - 15; - this.animation = new SlideX(700, -width); - this.duration = 3000; - } - - @Override - public void render(RenderScope renderScope, ElementContext ctx) { - renderScope.drawRoundedRectangle(0, 0, width, height, 10, Theme.PRIMARY.value); - switch (kind) { - case Error: - renderScope.drawTexture(Textures.CIRCLE_X, 9, 9, 0, 0, 15, 15, error.value); - break; - - case Info: - renderScope.drawTexture(Textures.INFO, 9, 9, 0, 0, 15, 15, info.value); - break; - - case Success: - renderScope.drawTexture(Textures.CHECK, 9, 9, 0, 0, 15, 15, success.value); - break; - } - renderScope.drawText(0.7f, title, 32, 10, 1, -1); - renderScope.drawText(0.5f, text, 32, 28, 1, -1); - } -} diff --git a/src/main/java/org/saturnclient/ui/elements/Text.java b/src/main/java/org/saturnclient/ui/elements/Text.java deleted file mode 100644 index 7de1737..0000000 --- a/src/main/java/org/saturnclient/ui/elements/Text.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.saturnclient.ui.elements; - -import org.saturnclient.config.Theme; -import org.saturnclient.ui.Element; -import org.saturnclient.ui.ElementContext; -import org.saturnclient.ui.RenderScope; -import org.saturnclient.ui.resources.Fonts; - -public class Text extends Element { - private String text; - - public Text(String text) { - this.text = text; - width = Fonts.getWidth(text, Theme.FONT.value); - height = Fonts.getHeight(); - } - - @Override - public void render(RenderScope renderScope, ElementContext ctx) { - renderScope.drawText(this.text, 0, 0, Theme.FONT.value, Theme.FOREGROUND.value); - } -} diff --git a/src/main/java/org/saturnclient/ui/elements/TextureButton.java b/src/main/java/org/saturnclient/ui/elements/TextureButton.java deleted file mode 100644 index 25c63b5..0000000 --- a/src/main/java/org/saturnclient/ui/elements/TextureButton.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.saturnclient.ui.elements; - -import org.saturnclient.common.ref.asset.IdentifierRef; -import org.saturnclient.config.Theme; -import org.saturnclient.ui.Element; -import org.saturnclient.ui.ElementContext; -import org.saturnclient.ui.RenderScope; - -public class TextureButton extends Element { - IdentifierRef sprite; - private Runnable onClick; - static int padding = 14; - - public TextureButton(IdentifierRef sprite, Runnable onClick) { - this.sprite = sprite; - this.onClick = onClick; - } - - @Override - public void render(RenderScope renderScope, ElementContext ctx) { - renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, - ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value); - - int texWidth = width - padding; - int texHeight = height - padding; - - renderScope.drawTexture(sprite, padding / 2, padding / 2, 0, 0, texWidth, texHeight, - ctx.isHovering() ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value); - } - - @Override - public void click(int mouseX, int mouseY) { - if (onClick != null) { - onClick.run(); - } - } -} diff --git a/src/main/java/org/saturnclient/ui/resources/Textures.java b/src/main/java/org/saturnclient/ui/resources/Textures.java index 8372396..01398ca 100644 --- a/src/main/java/org/saturnclient/ui/resources/Textures.java +++ b/src/main/java/org/saturnclient/ui/resources/Textures.java @@ -1,29 +1,14 @@ package org.saturnclient.ui.resources; +import java.io.IOException; +import java.io.InputStream; + +import org.saturnclient.common.provider.Providers; import org.saturnclient.common.ref.asset.IdentifierRef; public class Textures { public static final IdentifierRef CROSSHAIR_RANGE = IdentifierRef .ofSaturn("textures/gui/hud/crosshair_range.png"); - public static final IdentifierRef COSMETICS = IdentifierRef.ofSaturn("textures/gui/components/cosmetics.png"); - public static final IdentifierRef HUD_ICON = IdentifierRef.ofSaturn("textures/gui/components/hud.png"); - public static final IdentifierRef SEARCH = IdentifierRef.ofSaturn("textures/gui/components/search.png"); - public static final IdentifierRef CLOSE = IdentifierRef.ofSaturn("textures/gui/components/close.png"); - public static final IdentifierRef MODS_TAB = IdentifierRef.ofSaturn("textures/gui/components/mods_tab.png"); - public static final IdentifierRef SETTINGS = IdentifierRef.ofSaturn("textures/gui/components/settings.png"); - public static final IdentifierRef CLOAK = IdentifierRef.ofSaturn("textures/gui/components/cloak.png"); - public static final IdentifierRef HAT = IdentifierRef.ofSaturn("textures/gui/components/hat.png"); - public static final IdentifierRef RESET = IdentifierRef.ofSaturn("textures/gui/components/reset.png"); - public static final IdentifierRef TOGGLE_INDICATOR = IdentifierRef - .ofSaturn("textures/gui/components/toggle_indicator.png"); - public static final IdentifierRef LEFT = IdentifierRef.ofSaturn("textures/gui/components/left.png"); - public static final IdentifierRef RIGHT = IdentifierRef.ofSaturn("textures/gui/components/right.png"); - public static final IdentifierRef STORE = IdentifierRef.ofSaturn("textures/gui/components/store.png"); - public static final IdentifierRef COINS = IdentifierRef.ofSaturn("textures/gui/components/coins.png"); - public static final IdentifierRef SHIRT = IdentifierRef.ofSaturn("textures/gui/components/shirt.png"); - public static final IdentifierRef CIRCLE_X = IdentifierRef.ofSaturn("textures/gui/components/circle_x.png"); - public static final IdentifierRef INFO = IdentifierRef.ofSaturn("textures/gui/components/info.png"); - public static final IdentifierRef CHECK = IdentifierRef.ofSaturn("textures/gui/components/check.png"); // Lucide icons (SVG, drawn in white so they can be tinted) public static final IdentifierRef ICON_MODS = IdentifierRef.ofSaturn("textures/gui/components/mods.svg"); @@ -43,6 +28,7 @@ public class Textures { .ofSaturn("textures/gui/components/chevron_right.svg"); public static final IdentifierRef ICON_CHEVRON_DOWN = IdentifierRef .ofSaturn("textures/gui/components/chevron_down.svg"); + public static final IdentifierRef ICON_EMOTE = IdentifierRef.ofSaturn("textures/gui/components/emote.svg"); public static final IdentifierRef ICON_CHECK = IdentifierRef.ofSaturn("textures/gui/components/check.svg"); public static final IdentifierRef LOGO = IdentifierRef.ofSaturn("textures/logo/logo.png"); @@ -51,15 +37,17 @@ public class Textures { public static final IdentifierRef LOGO_TEXT_BIG = IdentifierRef.ofSaturn("textures/logo/text_big.png"); public static final IdentifierRef SPLASH = IdentifierRef.ofSaturn("textures/gui/splash.png"); - // Widgets - public static final IdentifierRef BUTTON_BORDER = IdentifierRef.ofSaturn("button_border"); - public static final IdentifierRef BUTTON = IdentifierRef.ofSaturn("button"); - public static final IdentifierRef BUTTON_OP = IdentifierRef.ofSaturn("button_op"); - public static final IdentifierRef SETTINGS_BG = IdentifierRef.ofSaturn("settings_bg"); - public static final IdentifierRef RECT = IdentifierRef.ofSaturn("rect"); - public static final IdentifierRef RECT_BORDER = IdentifierRef.ofSaturn("rect_border"); - public static final IdentifierRef MOD = IdentifierRef.ofSaturn("textures/gui/components/mod.png"); - public static final IdentifierRef MOD_BG = IdentifierRef.ofSaturn("textures/gui/components/mod_bg.png"); + /** + * Whether a texture file is in the resources, for falling back when an + * optional one (like an emote's preview) is missing. + */ + public static boolean exists(IdentifierRef texture) { + try (InputStream in = Providers.saturn.getClient().getResource(texture)) { + return in != null; + } catch (IOException e) { + return false; + } + } public static IdentifierRef getCloakPreview(String cloak) { return IdentifierRef.ofSaturn("textures/gui/cloak/" + (cloak.isEmpty() ? "none" : cloak) + ".png"); diff --git a/src/main/java/org/saturnclient/ui/screens/CosmeticsMenu.java b/src/main/java/org/saturnclient/ui/screens/CosmeticsMenu.java index 8fa0556..5e95bef 100644 --- a/src/main/java/org/saturnclient/ui/screens/CosmeticsMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/CosmeticsMenu.java @@ -19,6 +19,7 @@ import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.PanelScreen; import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.Utils; import org.saturnclient.ui.anim.SlideFade; import org.saturnclient.ui.components.CosmeticTile; import org.saturnclient.ui.components.Sidebar; @@ -193,18 +194,7 @@ public class CosmeticsMenu extends PanelScreen { private static String getName(String id) { String name = NAMES.get(id); - if (name != null) { - return name; - } - - StringBuilder builder = new StringBuilder(); - for (String word : id.split("_")) { - if (!builder.isEmpty()) { - builder.append(' '); - } - builder.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)); - } - return builder.toString(); + return name != null ? name : Utils.nameFromId(id); } /** diff --git a/src/main/java/org/saturnclient/ui/screens/EmoteWheel.java b/src/main/java/org/saturnclient/ui/screens/EmoteWheel.java index 2a870ec..771fd40 100644 --- a/src/main/java/org/saturnclient/ui/screens/EmoteWheel.java +++ b/src/main/java/org/saturnclient/ui/screens/EmoteWheel.java @@ -1,21 +1,43 @@ package org.saturnclient.ui.screens; -import org.saturnclient.cosmetics.Emotes; +import java.util.List; + import org.saturnclient.client.ServiceClient; import org.saturnclient.common.provider.Providers; +import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.config.AnimationConfig; import org.saturnclient.config.Config; +import org.saturnclient.config.Theme; +import org.saturnclient.cosmetics.Emotes; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.SaturnScreen; -import org.saturnclient.ui.Utils; import org.saturnclient.ui.anim.Fade; import org.saturnclient.ui.anim.SlideY; +import org.saturnclient.ui.components.EmoteTile; import org.saturnclient.ui.elements.AnimationStagger; import org.saturnclient.ui.elements.ImageTexture; -import org.saturnclient.ui.elements.TextureButton; +import org.saturnclient.ui.elements.Label; +import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Textures; +/** + * A 3x3 wheel of emotes to play, with the page controls in the middle. The + * emote wheel key opens and closes it (in the version's key handler). + */ public class EmoteWheel extends SaturnScreen { - int page = 1; + private static final int GAP = 10; + private static final int GRID = EmoteTile.SIZE * 3 + GAP * 2; + private static final int LOGO = 48; + + /** The grid's cells in the order emotes fill them, around the middle one */ + private static final int[][] SLOTS = { + { 0, 0 }, { 1, 0 }, { 2, 0 }, + { 0, 1 }, { 2, 1 }, + { 0, 2 }, { 1, 2 }, { 2, 2 } }; + + private int page = 0; public EmoteWheel() { super("Emote Wheel"); @@ -23,88 +45,131 @@ public class EmoteWheel extends SaturnScreen { @Override public void ui() { - int row = 0; - int col = 0; - - // ============================= - // Logo - // ============================= - - draw(new ImageTexture(Textures.LOGO_TEXT) - .dimensions(98, 10) - .center(width, height, 0, -105) - .animation(new Fade(AnimationConfig.logo.duration.value))); + List emotes = Emotes.availableEmotes; + int left = (width - GRID) / 2; + int top = (height - GRID) / 2 + LOGO / 2; draw(new ImageTexture(Config.getLogo()) - .dimensions(98, 98) - .center(width, height, 0, -149) - .animation(new SlideY(AnimationConfig.logo, -20))); + .dimensions(LOGO, LOGO) + .position((width - LOGO) / 2, top - 20 - LOGO) + .animation(new SlideY(AnimationConfig.logo, -12))); - // ============================= - // Navigation Buttons - // ============================= - - draw(new TextureButton(Textures.LEFT, () -> { - if (page > 1) { - page--; - elements.clear(); - ui(); - } - }).dimensions(30, 30) - .center(width, height, -50, 105) - .animation(new Fade(500))); - - draw(new TextureButton(Textures.RIGHT, () -> { - if ((page * 8) < Emotes.availableEmotes.size()) { - page++; - elements.clear(); - ui(); - } - }).dimensions(30, 30) - .center(width, height, 50, 105) - .animation(new Fade(500))); - - // ============================= - // Emote Grid (Staggered) - // ============================= - - AnimationStagger emoteStagger = new AnimationStagger(AnimationConfig.emoteWheel); - - while (row < 3) { - - int idx = (row * 3 + col) + ((page - 1) * 8); - - if (row == 1 && col == 1) { - col++; - } - - String emote = Utils.getOrNull(Emotes.availableEmotes, idx); - - emoteStagger.draw( - new TextureButton(Textures.getEmotePreview(emote), () -> { - Providers.saturn.playEmote(ServiceClient.uuid, emote); - ServiceClient.emote(emote); - provider.close(); - }) - .dimensions(70, 70) - .center(width, height, - -80 + (col * 80), - -35 + (row * 80)) - .animation(new SlideY(AnimationConfig.emoteWheel, 12))); - - if (col < 2) { - col++; - } else { - col = 0; - row++; - } + if (emotes.isEmpty()) { + String message = "No emotes available"; + int x = (width - (int) (Fonts.getWidth(message, Theme.getBodyFont()) * 0.7f)) / 2; + draw(new Label(() -> message, 0.7f, Theme::getBodyFont, Theme::getMutedFg).position(x, top + GRID / 2)); + return; } - draw(emoteStagger); + int pages = (emotes.size() + SLOTS.length - 1) / SLOTS.length; + page = Math.min(page, pages - 1); + + AnimationStagger grid = new AnimationStagger(AnimationConfig.emoteWheel); + + for (int slot = 0; slot < SLOTS.length; slot++) { + int index = page * SLOTS.length + slot; + if (index >= emotes.size()) { + break; + } + + String emote = emotes.get(index); + grid.draw(new EmoteTile(emote, () -> play(emote)) + .position(SLOTS[slot][0] * (EmoteTile.SIZE + GAP), SLOTS[slot][1] * (EmoteTile.SIZE + GAP)) + .animation(new SlideY(AnimationConfig.emoteWheel, 12))); + } + + draw(grid.position(left, top)); + + draw(new PageControl(page, pages, () -> turnPage(-1), () -> turnPage(1)) + .position(left + EmoteTile.SIZE + GAP, top + EmoteTile.SIZE + GAP) + .animation(new Fade(AnimationConfig.emoteWheel))); + } + + private void play(String emote) { + Providers.saturn.playEmote(ServiceClient.uuid, emote); + ServiceClient.emote(emote); + provider.close(); + } + + private void turnPage(int direction) { + page += direction; + init(); } @Override public boolean shouldPause() { return false; } + + /** + * The middle of the wheel: "Emotes", and the page with arrows to turn it + * when there's more than one. + */ + private static class PageControl extends Element { + private static final int ARROW = 16; + private static final int ARROW_AREA = 26; + + private final int page; + private final int pages; + private final Runnable onPrevious; + private final Runnable onNext; + + PageControl(int page, int pages, Runnable onPrevious, Runnable onNext) { + this.page = page; + this.pages = pages; + this.onPrevious = onPrevious; + this.onNext = onNext; + this.width = EmoteTile.SIZE; + this.height = EmoteTile.SIZE; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1, + Theme.getPanelBg(), Theme.getBorder()); + + float titleScale = 0.54f; + drawCentered(renderScope, "EMOTES", titleScale, pages > 1 ? 24 : (height - 10) / 2, Theme.getMutedFg()); + + if (pages <= 1) { + return; + } + + drawCentered(renderScope, (page + 1) + " / " + pages, 0.66f, 48, Theme.FOREGROUND.value); + + int arrowY = 48 + ((int) (Fonts.getHeight() * 0.66f) - ARROW) / 2; + drawArrow(renderScope, ctx, Textures.ICON_CHEVRON_LEFT, 4, arrowY, canGoBack()); + drawArrow(renderScope, ctx, Textures.ICON_CHEVRON_RIGHT, width - 4 - ARROW, arrowY, canGoForward()); + } + + private void drawArrow(RenderScope renderScope, ElementContext ctx, IdentifierRef icon, int x, int y, + boolean enabled) { + boolean hovered = enabled && ctx.isHovering(x - 5, 0, ARROW_AREA, height); + int color = !enabled ? Theme.withAlpha(0.2f, Theme.FOREGROUND.value) + : hovered ? Theme.ACCENT.value : Theme.getMutedFg(); + renderScope.drawTexture(icon, x, y, 0, 0, ARROW, ARROW, color); + } + + private void drawCentered(RenderScope renderScope, String text, float scale, int y, int color) { + int x = (width - (int) (Fonts.getWidth(text, Theme.FONT.value) * scale)) / 2; + renderScope.drawText(scale, text, x, y, Theme.FONT.value, color); + } + + @Override + public void click(int mouseX, int mouseY) { + if (mouseX < ARROW_AREA && canGoBack()) { + onPrevious.run(); + } else if (mouseX > width - ARROW_AREA && canGoForward()) { + onNext.run(); + } + } + + private boolean canGoBack() { + return page > 0; + } + + private boolean canGoForward() { + return page < pages - 1; + } + } } diff --git a/src/main/java/org/saturnclient/ui/screens/HudEditor.java b/src/main/java/org/saturnclient/ui/screens/HudEditor.java index 7047b26..8f10f80 100644 --- a/src/main/java/org/saturnclient/ui/screens/HudEditor.java +++ b/src/main/java/org/saturnclient/ui/screens/HudEditor.java @@ -1,59 +1,71 @@ package org.saturnclient.ui.screens; -import org.saturnclient.common.ref.render.MatrixStackRef; +import java.util.ArrayList; +import java.util.List; + import org.saturnclient.config.ConfigManager; +import org.saturnclient.config.Theme; import org.saturnclient.mod.HudMod; import org.saturnclient.mod.Mod; import org.saturnclient.mod.ModLayout; import org.saturnclient.mod.ModManager; import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.SaturnScreen; +import org.saturnclient.ui.components.HudModPreview; +import org.saturnclient.ui.resources.Fonts; -import java.util.ArrayList; -import java.util.List; - +/** + * Moves and resizes the enabled HUD mods, drawn as they are in game. Drag one + * to move it, or drag its corner handle to resize it. + * + * Unlike other Saturn screens this works in GUI pixels, like the HUD itself: + * it draws without the half scale and takes the mouse unscaled. + */ public class HudEditor extends SaturnScreen { + private static final String HINT = "Drag to move · Drag the corner to resize · Esc when done"; + + /** Size of the resize handle, and how far around it still grabs it */ + private static final int HANDLE = 4; + private static final int HANDLE_REACH = 4; + private static final float LABEL_SCALE = 0.4f; + private static final float HINT_SCALE = 0.45f; + private final List hudMods = new ArrayList<>(); + private HudMod draggingMod = null; private boolean resizing = false; private int offsetX, offsetY; - private static final int RESIZE_MARGIN = 3; public HudEditor() { super("HUD Editor"); for (Mod m : ModManager.ENABLED_MODS) { - if (m.isEnabled() && m instanceof HudMod) { - hudMods.add((HudMod) m); + if (m.isEnabled() && m instanceof HudMod hudMod) { + hudMods.add(hudMod); } } } + @Override + public void ui() { + // Placing HUDs needs a clear view of the game. + blurBackground = false; + } + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (button == 0) { - for (int m = hudMods.size() - 1; m >= 0; m--) { - HudMod mod = hudMods.get(m); + HudMod mod = getModAt(mouseX, mouseY); + + if (mod != null) { ModLayout dim = mod.getDimensions(); - - // Check if clicking the corner for resizing - if (isCorner(mouseX, mouseY, dim)) { - draggingMod = mod; - resizing = true; - offsetX = (int) (mouseX - (dim.x.value + dim.width * dim.scale.value)); - offsetY = (int) (mouseY - (dim.y.value + dim.height * dim.scale.value)); - return true; - } - - // Check if clicking inside the mod - if (isInside(mouseX, mouseY, dim)) { - draggingMod = mod; - resizing = false; - offsetX = (int) (mouseX - dim.x.value); - offsetY = (int) (mouseY - dim.y.value); - return true; - } + draggingMod = mod; + resizing = isOnHandle(mouseX, mouseY, dim); + offsetX = (int) (mouseX - (resizing ? right(dim) : dim.x.value)); + offsetY = (int) (mouseY - (resizing ? bottom(dim) : dim.y.value)); + return true; } } + return super.mouseClicked(mouseX, mouseY, button); } @@ -72,57 +84,121 @@ public class HudEditor extends SaturnScreen { ModLayout dim = draggingMod.getDimensions(); if (resizing) { - // Scale based on cursor movement - float newScale = Math.max(0.5f, ((float) (mouseX - dim.x.value) / dim.width)); - dim.scale.value = newScale; + // The handle follows the cursor: the scale is the new width over the unscaled width. + float scale = (float) (mouseX - offsetX - dim.x.value) / dim.width; + float min = dim.scale.min != null ? dim.scale.min : 0.5f; + float max = dim.scale.max != null ? dim.scale.max : 4f; + dim.scale.value = Math.max(min, Math.min(max, scale)); } else { - // Move the mod dim.x.value = (int) mouseX - offsetX; dim.y.value = (int) mouseY - offsetY; } + + keepOnScreen(dim); } + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); } - @Override - public void ui() { - } - @Override public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) { + // The screen hands over the mouse in Saturn units, twice GUI pixels. + int guiMouseX = mouseX / 2; + int guiMouseY = mouseY / 2; + HudMod hovered = draggingMod != null ? draggingMod : getModAt(guiMouseX, guiMouseY); + for (HudMod mod : hudMods) { ModLayout dim = mod.getDimensions(); - MatrixStackRef matrices = renderScope.getMatrixStack(); - matrices.push(); + boolean active = mod == hovered; - matrices.translate(dim.x.value, dim.y.value); - matrices.scale(dim.scale.value, dim.scale.value); + renderScope.getMatrixStack().push(); + renderScope.getMatrixStack().translate(dim.x.value, dim.y.value); + renderScope.getMatrixStack().scale(dim.scale.value, dim.scale.value); + HudModPreview.draw(renderScope, mod); + renderScope.getMatrixStack().pop(); - // Render the mod - mod.renderDummy(renderScope); + drawFrame(renderScope, mod, dim, active); + } - matrices.pop(); + drawHint(renderScope); + } - // Draw border around mod - int borderX = dim.x.value; - int borderY = dim.y.value; - int borderW = (int) (dim.width * dim.scale.value); - int borderH = (int) (dim.height * dim.scale.value); + /** + * The outline around a HUD, its resize handle, and its name while it's + * hovered or dragged. + */ + private void drawFrame(RenderScope renderScope, HudMod mod, ModLayout dim, boolean active) { + int x = dim.x.value; + int y = dim.y.value; + int w = right(dim) - x; + int h = bottom(dim) - y; + int color = active ? Theme.ACCENT.value : Theme.withAlpha(0.45f, Theme.FOREGROUND.value); - renderScope.drawBorder(borderX, borderY, borderW, borderH, 0xFFFFFFFF); // White border + if (active) { + renderScope.drawRectangle(x, y, w, h, Theme.withAlpha(0.08f, Theme.ACCENT.value)); + } + + renderScope.drawBorder(x - 1, y - 1, w + 2, h + 2, color); + renderScope.drawRectangle(x + w - HANDLE / 2, y + h - HANDLE / 2, HANDLE, HANDLE, color); + + if (active && mod instanceof Mod named) { + int labelHeight = (int) (Fonts.getHeight() * LABEL_SCALE); + int labelY = y - labelHeight - 4 < 0 ? y + h + 4 : y - labelHeight - 4; + renderScope.drawText(LABEL_SCALE, named.getName(), x, labelY, Theme.FONT.value, Theme.FOREGROUND.value); } } - private boolean isInside(double mouseX, double mouseY, ModLayout dim) { - return mouseX >= dim.x.value && mouseX <= dim.x.value + (dim.width * dim.scale.value) && - mouseY >= dim.y.value && mouseY <= dim.y.value + (dim.height * dim.scale.value); + private void drawHint(RenderScope renderScope) { + int guiWidth = width / 2; + int textWidth = (int) (Fonts.getWidth(HINT, Theme.getBodyFont()) * HINT_SCALE); + int textHeight = (int) (Fonts.getHeight() * HINT_SCALE); + int pillWidth = textWidth + 20; + int pillHeight = textHeight + 10; + int x = (guiWidth - pillWidth) / 2; + int y = 8; + + renderScope.drawRoundedRectangle(x, y, pillWidth, pillHeight, pillHeight / 2, 1, Theme.getPanelBg(), + Theme.getBorder()); + renderScope.drawText(HINT_SCALE, HINT, x + 10, y + 5, Theme.getBodyFont(), Theme.PRIMARY_FG.value); } - private boolean isCorner(double mouseX, double mouseY, ModLayout dim) { - float rightX = dim.x.value + dim.width * dim.scale.value; - float bottomY = dim.y.value + dim.height * dim.scale.value; + /** + * The HUD under the mouse, preferring the one drawn on top, or null. + */ + private HudMod getModAt(double mouseX, double mouseY) { + for (int i = hudMods.size() - 1; i >= 0; i--) { + ModLayout dim = hudMods.get(i).getDimensions(); + if (isOnHandle(mouseX, mouseY, dim) || isInside(mouseX, mouseY, dim)) { + return hudMods.get(i); + } + } + return null; + } - return Math.abs(mouseX - rightX) < RESIZE_MARGIN && Math.abs(mouseY - bottomY) < RESIZE_MARGIN; + private void keepOnScreen(ModLayout dim) { + int guiWidth = width / 2; + int guiHeight = height / 2; + int w = right(dim) - dim.x.value; + int h = bottom(dim) - dim.y.value; + + dim.x.value = Math.max(0, Math.min(dim.x.value, guiWidth - w)); + dim.y.value = Math.max(0, Math.min(dim.y.value, guiHeight - h)); + } + + private static int right(ModLayout dim) { + return dim.x.value + (int) (dim.width * dim.scale.value); + } + + private static int bottom(ModLayout dim) { + return dim.y.value + (int) (dim.height * dim.scale.value); + } + + private static boolean isInside(double mouseX, double mouseY, ModLayout dim) { + return mouseX >= dim.x.value && mouseX <= right(dim) && mouseY >= dim.y.value && mouseY <= bottom(dim); + } + + private static boolean isOnHandle(double mouseX, double mouseY, ModLayout dim) { + return Math.abs(mouseX - right(dim)) <= HANDLE_REACH && Math.abs(mouseY - bottom(dim)) <= HANDLE_REACH; } @Override diff --git a/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java b/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java index 2915e68..3f2d512 100644 --- a/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java @@ -8,18 +8,24 @@ import org.saturnclient.ui.anim.Fade; import org.saturnclient.ui.anim.SlideY; import org.saturnclient.ui.elements.AnimationStagger; import org.saturnclient.ui.elements.Button; +import org.saturnclient.ui.elements.IconButton; import org.saturnclient.ui.elements.ImageTexture; -import org.saturnclient.ui.elements.TextureButton; import org.saturnclient.ui.resources.Textures; +/** + * The in-game quick menu: the logo over the HUD editor, mod menu and + * cosmetics. + */ public class ShiftMenu extends SaturnScreen { + private static final int MODS_WIDTH = 120; + private static final int GAP = 6; + public ShiftMenu() { super("Shift Menu"); } @Override public void ui() { - // Logo animations (unchanged) draw(new ImageTexture(Textures.LOGO_TEXT) .dimensions(98, 10) .center(width, height, 0, -36) @@ -30,32 +36,24 @@ public class ShiftMenu extends SaturnScreen { .center(width, height, 0, -80) .animation(new SlideY(AnimationConfig.logo, -20))); - // Spacing and sizes - int btnWidth = 120; - int btnHeight = 40; - int spacing = 4; - - // Create stagger - AnimationStagger stagger = new AnimationStagger(AnimationConfig.shiftMenu.stagger.value); + int x = 0; - stagger.draw(new TextureButton(Textures.HUD_ICON, () -> { - Providers.saturn.getClient().setScreen(new HudEditor()); - }).dimensions(btnHeight, btnHeight).position(0, 0) - .animation(new Fade(AnimationConfig.shiftMenu))); + stagger.draw(new IconButton(Textures.ICON_HUD_EDITOR, + () -> Providers.saturn.getClient().setScreen(new HudEditor())) + .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu))); + x += Button.HEIGHT + GAP; - stagger.draw(new Button("Settings", () -> { - Providers.saturn.getClient().setScreen(new ModMenu()); - }).dimensions(btnWidth, btnHeight).position(btnHeight + spacing, 0) - .animation(new Fade(AnimationConfig.shiftMenu))); + stagger.draw(new Button("Mods", () -> Providers.saturn.getClient().setScreen(new ModMenu())) + .dimensions(MODS_WIDTH, Button.HEIGHT) + .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu))); + x += MODS_WIDTH + GAP; - stagger.draw(new TextureButton(Textures.COSMETICS, () -> { - Providers.saturn.getClient().setScreen(new CosmeticsMenu()); - }).dimensions(btnHeight, btnHeight).position(btnWidth + btnHeight + (spacing * 2), 0) - .animation(new Fade(AnimationConfig.shiftMenu))); + stagger.draw(new IconButton(Textures.ICON_COSMETICS, + () -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())) + .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu))); + x += Button.HEIGHT; - // Draw the stagger at the absolute center of the screen - draw(stagger.dimensions(btnWidth + (btnHeight * 2) + (spacing * 2), btnHeight).center(width, height, 0, - 0)); + draw(stagger.position((width - x) / 2, (height - Button.HEIGHT) / 2)); } } diff --git a/src/main/java/org/saturnclient/ui/screens/TitleMenu.java b/src/main/java/org/saturnclient/ui/screens/TitleMenu.java index d134334..3821d69 100644 --- a/src/main/java/org/saturnclient/ui/screens/TitleMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/TitleMenu.java @@ -1,21 +1,40 @@ package org.saturnclient.ui.screens; +import java.util.List; + import org.saturnclient.common.provider.Providers; import org.saturnclient.common.ref.game.MinecraftClientRef.MinecraftScreen; import org.saturnclient.config.AnimationConfig; import org.saturnclient.config.Config; +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; import org.saturnclient.ui.SaturnScreen; import org.saturnclient.ui.anim.Fade; import org.saturnclient.ui.anim.SlideFade; import org.saturnclient.ui.anim.SlideY; +import org.saturnclient.ui.components.AccountChip; +import org.saturnclient.ui.components.Dock; import org.saturnclient.ui.elements.AnimationStagger; import org.saturnclient.ui.elements.Button; import org.saturnclient.ui.elements.ImageTexture; -import org.saturnclient.ui.elements.TextureButton; +import org.saturnclient.ui.elements.Label; import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Textures; +/** + * Saturn's title screen: the logo over the game's menus in the middle, and + * along the bottom the player, a dock of Saturn's screens and the version. + */ public class TitleMenu extends SaturnScreen { + private static final int LOGO = 88; + private static final int WORDMARK_WIDTH = 200; + private static final int WORDMARK_HEIGHT = 20; + private static final int COLUMN_WIDTH = 280; + private static final int BUTTON_HEIGHT = 36; + private static final int GAP = 8; + private static final int EDGE = 28; + private static final float VERSION_SCALE = 0.62f; + public TitleMenu() { super("Title Menu"); } @@ -24,53 +43,72 @@ public class TitleMenu extends SaturnScreen { public void ui() { blurBackground = false; - draw(new ImageTexture(Textures.LOGO_TEXT).dimensions(98, 10).center(width, height, 0, -40) - .animation(new Fade(AnimationConfig.logo.duration.value))); - draw(new ImageTexture(Config.getLogo()).dimensions(98, 98).center(width, height, 0, -84) + // Logo and menus, centered together a little above the middle + int menusHeight = BUTTON_HEIGHT * 3 + GAP * 2; + int blockHeight = LOGO + 16 + WORDMARK_HEIGHT + 36 + menusHeight; + int top = (height - blockHeight) / 2 - 20; + + draw(new ImageTexture(Config.getLogo()) + .dimensions(LOGO, LOGO) + .position((width - LOGO) / 2, top) .animation(new SlideY(AnimationConfig.logo, -20))); + draw(new ImageTexture(Textures.LOGO_TEXT_BIG) + .dimensions(WORDMARK_WIDTH, WORDMARK_HEIGHT) + .position((width - WORDMARK_WIDTH) / 2, top + LOGO + 16) + .animation(new Fade(AnimationConfig.logo.duration.value))); - AnimationStagger mainButtonStagger = new AnimationStagger(AnimationConfig.mainMenu); + int rowHeight = BUTTON_HEIGHT + GAP; + int halfWidth = (COLUMN_WIDTH - GAP) / 2; + AnimationStagger menus = new AnimationStagger(AnimationConfig.mainMenu); - mainButtonStagger.draw(new Button("SINGLEPLAYER", () -> { - Providers.saturn.getClient().setScreen(MinecraftScreen.SelectWorld); - }).scale(0.69f).dimensions(420, 52).position(0, 0) - .animation(new SlideFade(AnimationConfig.mainMenu, -10))); + addMenuButton(menus, new Button("Singleplayer", + () -> Providers.saturn.getClient().setScreen(MinecraftScreen.SelectWorld)) + .dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, 0)); + addMenuButton(menus, new Button("Multiplayer", + () -> Providers.saturn.getClient().setScreen(MinecraftScreen.Multiplayer)) + .dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, rowHeight)); + addMenuButton(menus, new Button("Options", + () -> Providers.saturn.getClient().setScreen(MinecraftScreen.Options)) + .dimensions(halfWidth, BUTTON_HEIGHT).position(0, rowHeight * 2)); + addMenuButton(menus, new Button("Quit", + () -> Providers.saturn.getClient().scheduleStop()) + .dimensions(halfWidth, BUTTON_HEIGHT).position(COLUMN_WIDTH - halfWidth, rowHeight * 2)); - mainButtonStagger.draw(new Button("MULTIPLAYER", () -> { - Providers.saturn.getClient().setScreen(MinecraftScreen.Multiplayer); - }).scale(0.69f).dimensions(420, 52).position(0, 40) - .animation(new SlideFade(AnimationConfig.mainMenu, -10))); + draw(menus.position((width - COLUMN_WIDTH) / 2, top + LOGO + 16 + WORDMARK_HEIGHT + 36)); - mainButtonStagger.draw(new Button("OPTIONS", () -> { - Providers.saturn.getClient().setScreen(MinecraftScreen.Options); - }).scale(0.69f).dimensions(206, 52).position(0, 80) - .animation(new SlideFade(AnimationConfig.mainMenu, -10))); + // Along the bottom: the player, Saturn's screens, the version + draw(new AccountChip() + .position(EDGE, height - EDGE - AccountChip.HEIGHT) + .animation(new Fade(AnimationConfig.mainMenu))); - mainButtonStagger.draw(new Button("QUIT", () -> { - Providers.saturn.getClient().scheduleStop(); - }).scale(0.69f).dimensions(206, 52).position(146, 80) - .animation(new SlideFade(AnimationConfig.mainMenu, -10))); + Dock dock = new Dock(List.of( + new Dock.Item(Textures.ICON_MODS, () -> Providers.saturn.getClient().setScreen(new ModMenu())), + new Dock.Item(Textures.ICON_COSMETICS, + () -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())), + new Dock.Item(Textures.ICON_HUD_EDITOR, + () -> Providers.saturn.getClient().setScreen(new HudEditor())))); + draw(dock.position((width - dock.width) / 2, height - EDGE - Dock.HEIGHT) + .animation(new SlideFade(AnimationConfig.mainMenu, 12))); - draw(mainButtonStagger.dimensions(289, 143).center(width, height, 0, 50)); + String version = getVersionText(); + int versionWidth = (int) (Fonts.getWidth(version, Theme.getBodyFont()) * VERSION_SCALE); + int versionHeight = (int) (Fonts.getHeight() * VERSION_SCALE); + draw(new Label(() -> version, VERSION_SCALE, Theme::getBodyFont, Theme::getMutedFg) + .position(width - EDGE - versionWidth, height - EDGE - (AccountChip.HEIGHT + versionHeight) / 2) + .animation(new Fade(AnimationConfig.mainMenu))); + } - int s = Fonts.getHeight() + 20; + /** + * "Saturn Client 0.1.0 · Minecraft 1.21.11", from the mod version + * "0.1.0+1.21.11". + */ + private static String getVersionText() { + String[] version = Providers.saturn.getModVersion().split("\\+", 2); + String text = "Saturn Client " + version[0]; + return version.length > 1 ? text + " · Minecraft " + version[1] : text; + } - AnimationStagger saturnStagger = new AnimationStagger(AnimationConfig.mainMenu.stagger.value); - - saturnStagger.draw(new TextureButton(Textures.HUD_ICON, () -> { - Providers.saturn.getClient().setScreen(new HudEditor()); - }).dimensions(s, s).position(0, 0).animation(new SlideFade(AnimationConfig.mainMenu, s + 8))); - - saturnStagger.draw(new TextureButton(Textures.SETTINGS, () -> { - Providers.saturn.getClient().setScreen(new ModMenu()); - }).dimensions(s, s).position(s + 2, 0) - .animation(new SlideFade(AnimationConfig.mainMenu, s + 8))); - - saturnStagger.draw(new TextureButton(Textures.CLOAK, () -> { - Providers.saturn.getClient().setScreen(new CosmeticsMenu()); - }).dimensions(s, s).position((s + 2) * 2, 0) - .animation(new SlideFade(AnimationConfig.mainMenu, s + 8))); - - draw(saturnStagger.dimensions(((s + 2) * 2) + s, s).centerHorizontal(width, height, 0, 8)); + private static void addMenuButton(AnimationStagger stagger, Element button) { + stagger.draw(button.animation(new SlideFade(AnimationConfig.mainMenu, -10))); } } diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/check.png b/src/main/resources/assets/saturnclient/textures/gui/components/check.png deleted file mode 100644 index 039befb..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/check.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/circle_x.png b/src/main/resources/assets/saturnclient/textures/gui/components/circle_x.png deleted file mode 100644 index 11401de..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/circle_x.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/cloak.png b/src/main/resources/assets/saturnclient/textures/gui/components/cloak.png deleted file mode 100644 index ab853dd..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/cloak.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/close.png b/src/main/resources/assets/saturnclient/textures/gui/components/close.png deleted file mode 100644 index cf7f967..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/close.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/coins.png b/src/main/resources/assets/saturnclient/textures/gui/components/coins.png deleted file mode 100644 index 0946546..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/coins.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.png b/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.png deleted file mode 100644 index 725110f..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/emote.svg b/src/main/resources/assets/saturnclient/textures/gui/components/emote.svg new file mode 100644 index 0000000..912c98c --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/emote.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/hat.png b/src/main/resources/assets/saturnclient/textures/gui/components/hat.png deleted file mode 100644 index b84414f..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/hat.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/hud.png b/src/main/resources/assets/saturnclient/textures/gui/components/hud.png deleted file mode 100644 index a71c971..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/hud.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/info.png b/src/main/resources/assets/saturnclient/textures/gui/components/info.png deleted file mode 100644 index 81686b1..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/info.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/left.png b/src/main/resources/assets/saturnclient/textures/gui/components/left.png deleted file mode 100644 index 073a748..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/left.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/mods_tab.png b/src/main/resources/assets/saturnclient/textures/gui/components/mods_tab.png deleted file mode 100644 index 3c46cd0..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/mods_tab.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/reset.png b/src/main/resources/assets/saturnclient/textures/gui/components/reset.png deleted file mode 100644 index fa62098..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/reset.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/right.png b/src/main/resources/assets/saturnclient/textures/gui/components/right.png deleted file mode 100644 index fa6ee7c..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/right.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/search.png b/src/main/resources/assets/saturnclient/textures/gui/components/search.png deleted file mode 100644 index ac03290..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/search.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/settings.png b/src/main/resources/assets/saturnclient/textures/gui/components/settings.png deleted file mode 100644 index 93074e0..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/settings.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/shirt.png b/src/main/resources/assets/saturnclient/textures/gui/components/shirt.png deleted file mode 100644 index 96a85f1..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/shirt.png and /dev/null differ diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/store.png b/src/main/resources/assets/saturnclient/textures/gui/components/store.png deleted file mode 100644 index b067b97..0000000 Binary files a/src/main/resources/assets/saturnclient/textures/gui/components/store.png and /dev/null differ