Compare commits

...
6 Commits
Author SHA1 Message Date
selimaj-dev ef0ead6bf8 Merge pull request 'Fix UI bugs and bring the remaining screens into the new style' (#7) from ui-fixes into master
Reviewed-on: #7
2026-09-27 17:15:00 +00:00
selimaj-devandclaude 136621cc8d Remove UI code and icons nothing uses
TextureButton, ClickableTexture, Text, Notification and the FadeOut and
SlideX animations only Notification used, Utils.getOrNull and the
commented-out notify, Element.duration, the 17 PNG icons the Lucide SVGs
replaced, and the Textures constants for them and for files that don't
exist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:14:17 +02:00
selimaj-devandclaude 25d288da92 Rework the HUD editor
HUDs are drawn as they are in game, background included, through the
new HudModPreview.draw, and the world behind the editor isn't blurred.
Each has an outline that turns the accent color with the mod's name while
hovered or dragged, and a visible resize handle that's easier to grab.
Scale stays within its range and HUDs stay on screen, and a hint says how
to use the editor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:09:06 +02:00
selimaj-devandclaude f0971ff128 Use one button style and an icon-only dock
Buttons no longer have a primary style: every button is the translucent
outlined surface that brightens on hover, and only an icon button's icon
takes the accent color. The title screen's dock shows just its icons.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:06:01 +02:00
selimaj-devandclaude 2f4ea77e6f Restyle buttons, the Shift menu and the title screen
Button is now primary (accent fill) or secondary (a translucent outlined
surface), and IconButton is a square secondary button with a Lucide icon,
replacing the PNG TextureButtons. Neither is scaled with Element.scale any
more, which multiplied click positions instead of dividing them.

The Shift menu is the HUD editor, the mod menu (its button was labelled
"Settings") and cosmetics. The title screen centers the logo over a
compact stack of the game's menus, with the player, a dock of Saturn's
screens and the version along the bottom. The player chip shows the
player's face from their skin and whether they're connected to Saturn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:01:42 +02:00
selimaj-devandclaude 05dbec028a Fix and restyle the emote wheel
The slot index was taken before the middle slot was skipped, so page one
showed emotes 0-4 and 6-8: emote 5 never appeared and emote 8 showed on
two pages. Emotes now fill the eight cells around the middle in order,
and slots past the last emote aren't drawn, so they can't play a null
emote.

Emotes show as EmoteTiles: the preview in its own colors (it was tinted,
almost black on hover), the name under it, an accent outline on hover, and
an icon when an emote has no preview. The middle cell holds the page and
its arrows when there's more than one page. Emote names and cosmetic names
share Utils.nameFromId.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 18:43:27 +02:00
40 changed files with 686 additions and 475 deletions
@@ -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);
}
@@ -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/<id>.json} (Emotecraft
@@ -23,6 +24,13 @@ public class Emotes {
/** The emotes that loaded, in wheel order. */
public static List<String> 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";
@@ -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) {
}
+16 -14
View File
@@ -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> T getOrNull(List<T> 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));
// }
// }
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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<Item> items;
public Dock(List<Item> 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);
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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
@@ -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();
}
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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");
@@ -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);
}
/**
@@ -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<String> 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;
}
}
}
@@ -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<HudMod> 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
@@ -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));
}
}
@@ -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)));
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" x2="9.01" y1="9" y2="9"/><line x1="15" x2="15.01" y1="9" y2="9"/></svg>

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B