Redesign cosmetics

CosmeticsMenu replaces the cloak and hat menus and stores: the player on
the left, turned by dragging, and on the right Cloaks and Hats tabs over a
grid of every cosmetic. Clicking an owned one equips it, and clicking one
not owned yet claims it, which is free. The equipped one is outlined and
badged, and the grid asks to connect when there's no Saturn session.

SkinPreview is now a drag-to-rotate player view, and its EntityDrawer draws
the player filling a box at a given yaw, returning false when there's no
player. CosmeticPreview and TabMenu go with the old screens.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-27 17:33:25 +02:00
co-authored by claude
parent a413648be9
commit 7264c7b38b
12 changed files with 412 additions and 568 deletions
@@ -1,43 +0,0 @@
package org.saturnclient.ui.components;
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 CosmeticPreview extends Element {
static int padding = 14;
IdentifierRef sprite;
private Runnable onClick;
private boolean isSelected;
public CosmeticPreview(boolean isSelected, IdentifierRef sprite, Runnable onClick) {
this.sprite = sprite;
this.onClick = onClick;
this.isSelected = isSelected;
this.width = 50;
this.height = 111;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
isSelected ? 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,
Theme.FOREGROUND.value);
}
@Override
public void click(int mouseX, int mouseY) {
if (onClick != null) {
onClick.run();
}
}
}
@@ -0,0 +1,125 @@
package org.saturnclient.ui.components;
import java.util.function.BooleanSupplier;
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.resources.Fonts;
import org.saturnclient.ui.resources.Textures;
/**
* One cosmetic in the cosmetics grid: its preview and name. It shows whether
* it's equipped, and cosmetics not owned yet are dimmed with a "Get" label.
*/
public class CosmeticTile extends Element {
public static final int HEIGHT = 168;
private static final int INSET = 8;
private static final int IMAGE_AREA = 124;
private static final float NAME_SCALE = 0.62f;
private static final float BADGE_SCALE = 0.54f;
private static final int BADGE_HEIGHT = 20;
/** How long a claim may take before it can be tried again, in ms */
private static final long CLAIM_TIMEOUT = 5000;
private final String name;
private final IdentifierRef preview;
private final float previewAspect;
private final BooleanSupplier owned;
private final BooleanSupplier equipped;
private final Runnable onEquip;
private final Runnable onClaim;
private long claimedAt = 0;
/**
* @param previewAspect the preview texture's width divided by its height
*/
public CosmeticTile(String name, IdentifierRef preview, float previewAspect, int width, BooleanSupplier owned,
BooleanSupplier equipped, Runnable onEquip, Runnable onClaim) {
this.name = name;
this.preview = preview;
this.previewAspect = previewAspect;
this.owned = owned;
this.equipped = equipped;
this.onEquip = onEquip;
this.onClaim = onClaim;
this.width = width;
this.height = HEIGHT;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
boolean isOwned = owned.getAsBoolean();
boolean isEquipped = equipped.getAsBoolean();
boolean hovered = ctx.isHovering();
int border = isEquipped ? Theme.ACCENT.value
: Theme.withAlpha(hovered ? 0.22f : 0.06f, Theme.FOREGROUND.value);
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, isEquipped ? 2 : 1,
Theme.withAlpha(0.72f, Theme.PRIMARY.value), border);
// Preview, fitted into its area
int areaWidth = width - INSET * 2;
renderScope.drawRoundedRectangle(INSET, INSET, areaWidth, IMAGE_AREA, Theme.WIDGET_RADIUS.value - 2,
Theme.withAlpha(0.05f, Theme.FOREGROUND.value));
int imageHeight = IMAGE_AREA - 12;
int imageWidth = (int) (imageHeight * previewAspect);
if (imageWidth > areaWidth - 12) {
imageWidth = areaWidth - 12;
imageHeight = (int) (imageWidth / previewAspect);
}
renderScope.drawTexture(preview, INSET + (areaWidth - imageWidth) / 2, INSET + (IMAGE_AREA - imageHeight) / 2,
0, 0, imageWidth, imageHeight, isOwned ? 0xFFFFFFFF : Theme.withAlpha(0.35f, 0xFFFFFFFF));
// Name
int nameY = INSET + IMAGE_AREA + (HEIGHT - INSET - IMAGE_AREA - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
renderScope.drawText(NAME_SCALE, Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - 24), 12, nameY,
Theme.FONT.value, isOwned ? Theme.FOREGROUND.value : Theme.getMutedFg());
// Badge in the preview's corner
if (isEquipped) {
drawBadge(renderScope, "On", Theme.ACCENT.value, Theme.ACCENT_FG.value, true);
} else if (!isOwned) {
drawBadge(renderScope, isClaiming() ? "Claiming" : "Get", hovered ? Theme.ACCENT.value : Theme.PRIMARY.value,
hovered ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value, false);
}
}
private void drawBadge(RenderScope renderScope, String text, int background, int foreground, boolean check) {
int icon = check ? 10 : 0;
int badgeWidth = 14 + icon + (check ? 4 : 0) + (int) (Fonts.getWidth(text, Theme.FONT.value) * BADGE_SCALE);
int x = width - INSET - 6 - badgeWidth;
int y = INSET + 6;
renderScope.drawRoundedRectangle(x, y, badgeWidth, BADGE_HEIGHT, 6, background);
if (check) {
renderScope.drawTexture(Textures.CHECK, x + 7, y + (BADGE_HEIGHT - icon) / 2, 0, 0, icon, icon, foreground);
}
int textY = y + (BADGE_HEIGHT - (int) (Fonts.getHeight() * BADGE_SCALE)) / 2;
renderScope.drawText(BADGE_SCALE, text, x + 7 + icon + (check ? 4 : 0), textY, Theme.FONT.value, foreground);
}
@Override
public void click(int mouseX, int mouseY) {
if (owned.getAsBoolean()) {
if (!equipped.getAsBoolean()) {
onEquip.run();
}
} else if (!isClaiming()) {
claimedAt = System.currentTimeMillis();
onClaim.run();
}
}
private boolean isClaiming() {
return System.currentTimeMillis() - claimedAt < CLAIM_TIMEOUT;
}
}
@@ -11,7 +11,7 @@ import org.saturnclient.ui.resources.Textures;
import org.saturnclient.ui.screens.ClientSettings; import org.saturnclient.ui.screens.ClientSettings;
import org.saturnclient.ui.screens.HudEditor; import org.saturnclient.ui.screens.HudEditor;
import org.saturnclient.ui.screens.ModMenu; import org.saturnclient.ui.screens.ModMenu;
import org.saturnclient.ui.screens.cosmetics.CloakMenu; import org.saturnclient.ui.screens.CosmeticsMenu;
/** /**
* The navigation rail beside Saturn's screens: the logo, one button per screen, * The navigation rail beside Saturn's screens: the logo, one button per screen,
@@ -37,7 +37,7 @@ public class Sidebar extends Element {
private final Item[] items = { private final Item[] items = {
new Item(Textures.ICON_MODS, () -> Providers.saturn.getClient().setScreen(new ModMenu())), new Item(Textures.ICON_MODS, () -> Providers.saturn.getClient().setScreen(new ModMenu())),
new Item(Textures.ICON_COSMETICS, () -> Providers.saturn.getClient().setScreen(new CloakMenu())), new Item(Textures.ICON_COSMETICS, () -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())),
new Item(Textures.ICON_HUD_EDITOR, () -> Providers.saturn.getClient().setScreen(new HudEditor())), new Item(Textures.ICON_HUD_EDITOR, () -> Providers.saturn.getClient().setScreen(new HudEditor())),
new Item(Textures.ICON_SETTINGS, () -> Providers.saturn.getClient().setScreen(new ClientSettings())), new Item(Textures.ICON_SETTINGS, () -> Providers.saturn.getClient().setScreen(new ClientSettings())),
}; };
@@ -1,26 +1,68 @@
package org.saturnclient.ui.components; package org.saturnclient.ui.components;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element; import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
/**
* The player, wearing their cosmetics, filling the element. Dragging it turns
* the player around.
*/
public class SkinPreview extends Element { public class SkinPreview extends Element {
public static EntityDrawer DRAWER; public static EntityDrawer DRAWER;
private boolean negativeAngle = false;
private float angle = 0.0f;
public SkinPreview(float angle, boolean negativeAngle) { /** Degrees turned per GUI pixel dragged */
this.angle = angle; private static final float DRAG_SPEED = 1.2f;
this.negativeAngle = negativeAngle; private static final float HINT_SCALE = 0.66f;
private float yaw;
private boolean dragging = false;
/**
* @param yaw degrees the player starts turned by: 0 faces the viewer, 180
* shows their back
*/
public SkinPreview(float yaw) {
this.yaw = yaw;
} }
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
DRAWER.render(renderScope, negativeAngle, angle, scale, ctx); if (DRAWER != null && DRAWER.render(renderScope, width, height, yaw)) {
return;
}
String hint = "Join a world to preview";
int x = (width - (int) (Fonts.getWidth(hint, Theme.getBodyFont()) * HINT_SCALE)) / 2;
renderScope.drawText(HINT_SCALE, hint, x, height / 2, Theme.getBodyFont(), Theme.getMutedFg());
}
@Override
public void mouseClicked(double mouseX, double mouseY, int button) {
dragging = mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height;
}
@Override
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
if (dragging) {
yaw = (yaw - (float) deltaX * DRAG_SPEED) % 360;
}
}
@Override
public void mouseReleased(double mouseX, double mouseY, int button) {
dragging = false;
} }
public static interface EntityDrawer { public static interface EntityDrawer {
public void render(RenderScope renderScope, boolean negativeAngle, float angle, float scale, /**
ElementContext ctx); * Draws the client's player filling a {@code width} x {@code height} box,
* turned by {@code yaw} degrees (0 faces the viewer).
*
* @return false if there's no player to draw, such as on the title screen
*/
public boolean render(RenderScope renderScope, int width, int height, float yaw);
} }
} }
@@ -1,63 +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;
import org.saturnclient.ui.Utils;
public class TabMenu extends Element {
public static class TabMenuComponent {
Runnable onClick;
IdentifierRef sprite;
public TabMenuComponent(IdentifierRef sprite, Runnable onClick) {
this.onClick = onClick;
this.sprite = sprite;
}
}
TabMenuComponent[] components;
public int selectedScreen = 0;
public TabMenu(int active, TabMenuComponent... components) {
this.components = components;
width = 25 * components.length + 6;
height = 30;
this.selectedScreen = active;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Theme.PRIMARY.value);
int col = 0;
for (TabMenuComponent component : components) {
int x = 25 * col + 5;
boolean hovering = ctx.isHovering(x, 5, 20, 20) || selectedScreen == col;
if (hovering) {
renderScope.drawRoundedRectangle(x, 5, 20, 20, Theme.WIDGET_RADIUS.value, Theme.ACCENT.value);
}
renderScope.drawTexture(component.sprite, x + 5, 10, 0, 0, 10, 10,
hovering ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value);
col++;
}
}
@Override
public void click(int mouseX, int mouseY) {
int col = 0;
for (TabMenuComponent component : components) {
int x = 25 * col + 5;
if (Utils.isHovering(mouseX - x, mouseY - 5, 20, 20, 1.0f)) {
component.onClick.run();
}
col++;
}
}
}
@@ -0,0 +1,233 @@
package org.saturnclient.ui.screens;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Theme;
import org.saturnclient.cosmetics.Cloaks;
import org.saturnclient.cosmetics.Hats;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.PanelScreen;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.anim.SlideFade;
import org.saturnclient.ui.components.CosmeticTile;
import org.saturnclient.ui.components.Sidebar;
import org.saturnclient.ui.components.SkinPreview;
import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.ChipGroup;
import org.saturnclient.ui.elements.Label;
import org.saturnclient.ui.elements.Scroll;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures;
/**
* Cloaks and hats: the player on the left, turned by dragging, and a grid of
* every cosmetic on the right. Clicking an owned cosmetic equips it; clicking
* one not owned yet claims it, which is free.
*/
public class CosmeticsMenu extends PanelScreen {
private enum Kind {
CLOAKS("Cloaks", "Cloak", Cloaks.ALL_CLOAKS, () -> Cloaks.availableCloaks, player -> player.cloak,
Cloaks::setCloak, ServiceClient::buyCloak, Textures::getCloakPreview, 0.5f, 150f),
HATS("Hats", "Hat", Hats.ALL_HATS, () -> Hats.availableHats, player -> player.hat,
Hats::setHat, ServiceClient::buyHat, Textures::getHatPreview, 1f, -25f);
final String title;
final String singular;
final String[] all;
final Supplier<List<String>> owned;
final Function<SaturnPlayer, String> equipped;
final Consumer<String> equip;
final Consumer<String> claim;
final Function<String, IdentifierRef> preview;
/** Width divided by height of the preview textures */
final float previewAspect;
/** The angle the player starts at, showing off this kind of cosmetic */
final float yaw;
Kind(String title, String singular, String[] all, Supplier<List<String>> owned,
Function<SaturnPlayer, String> equipped, Consumer<String> equip, Consumer<String> claim,
Function<String, IdentifierRef> preview, float previewAspect, float yaw) {
this.title = title;
this.singular = singular;
this.all = all;
this.owned = owned;
this.equipped = equipped;
this.equip = equip;
this.claim = claim;
this.preview = preview;
this.previewAspect = previewAspect;
this.yaw = yaw;
}
/**
* "" (nothing), then every cosmetic of this kind.
*/
List<String> ids() {
List<String> ids = new ArrayList<>();
ids.add("");
ids.addAll(List.of(all));
return ids;
}
String getEquipped() {
SaturnPlayer player = SaturnPlayer.get();
return player == null ? "" : equipped.apply(player);
}
}
/** Names that aren't just the id with each word capitalized */
private static final Map<String, String> NAMES = Map.of(
"", "None",
"amg", "AMG",
"amg_petronas", "AMG Petronas",
"bmw", "BMW",
"redbull", "Red Bull");
private static final int STAGE_WIDTH = 300;
private static final int STAGE_PADDING = 16;
private static final int STAGE_FOOTER = 70;
private static final int COLUMN_GAP = 20;
private static final int COLUMNS = 4;
private static final int TILE_GAP = 10;
private static final int GRID_Y = 58;
private static final int SCROLLBAR_SPACE = 14;
private static final float SMALL_SCALE = 0.6f;
private Kind kind = Kind.CLOAKS;
public CosmeticsMenu() {
super("Cosmetics", Sidebar.COSMETICS);
}
@Override
protected void panel() {
drawStage();
int x = contentX + STAGE_WIDTH + COLUMN_GAP;
int width = contentX + contentWidth - x;
// Header
draw(new Label(() -> "Cosmetics", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
.position(x, contentY + 2));
draw(new Label(this::getOwnedSummary, 0.66f, Theme::getBodyFont, Theme::getMutedFg)
.position(x, contentY + 28));
ChipGroup tabs = new ChipGroup(List.of(Kind.CLOAKS.title, Kind.HATS.title), kind.title, selected -> {
kind = selected.equals(Kind.HATS.title) ? Kind.HATS : Kind.CLOAKS;
init();
});
draw(tabs.position(x + width - tabs.width, contentY + 4));
// Grid
Scroll scroll = new Scroll(0).noBackground();
int gridHeight = contentHeight - GRID_Y;
if (SaturnPlayer.get() == null) {
String message = "Connect to Saturn to change your cosmetics";
int messageX = (width - (int) (Fonts.getWidth(message, Theme.getBodyFont()) * 0.7f)) / 2;
scroll.draw(new Label(() -> message, 0.7f, Theme::getBodyFont, Theme::getMutedFg)
.position(messageX, 60));
} else {
scroll.draw(createGrid(width));
}
draw(scroll.dimensions(width + SCROLLBAR_SPACE, gridHeight).position(x, contentY + GRID_Y));
}
/**
* The left column: the player, and what they have equipped.
*/
private void drawStage() {
draw(new Stage().dimensions(STAGE_WIDTH, contentHeight).position(contentX, contentY));
int previewY = contentY + STAGE_PADDING + 24;
int previewHeight = contentHeight - STAGE_PADDING - 24 - STAGE_FOOTER;
draw(new SkinPreview(kind.yaw).dimensions(STAGE_WIDTH, previewHeight).position(contentX, previewY));
int footerY = contentY + contentHeight - STAGE_FOOTER + 8;
draw(new Label(() -> getName(kind.getEquipped()), 1f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
.position(contentX + STAGE_PADDING + 4, footerY));
draw(new Label(() -> kind.singular + " · Equipped", 0.64f, Theme::getBodyFont, Theme::getMutedFg)
.position(contentX + STAGE_PADDING + 4, footerY + 26));
}
private Element createGrid(int width) {
int tileWidth = (width - TILE_GAP * (COLUMNS - 1)) / COLUMNS;
AnimationStagger grid = new AnimationStagger(AnimationConfig.cosmeticsMenu);
List<String> ids = kind.ids();
for (int i = 0; i < ids.size(); i++) {
String id = ids.get(i);
Kind k = kind;
CosmeticTile tile = new CosmeticTile(getName(id), k.preview.apply(id), k.previewAspect, tileWidth,
() -> k.owned.get().contains(id),
() -> k.getEquipped().equals(id),
() -> k.equip.accept(id),
() -> k.claim.accept(id));
tile.position((i % COLUMNS) * (tileWidth + TILE_GAP), (i / COLUMNS) * (CosmeticTile.HEIGHT + TILE_GAP));
tile.animation(new SlideFade(AnimationConfig.cosmeticsMenu, -12));
grid.draw(tile);
}
return grid;
}
private String getOwnedSummary() {
// The owned list also holds "", for nothing equipped.
long owned = List.of(kind.all).stream().filter(kind.owned.get()::contains).count();
return owned + " of " + kind.all.length + " " + kind.title.toLowerCase(Locale.ROOT)
+ " owned · every cosmetic is free to claim";
}
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();
}
/**
* The darker card behind the player, with its heading and a hint that it
* can be turned.
*/
private static class Stage extends Element {
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, 1,
Theme.withAlpha(0.45f, 0xFF000000), Theme.withAlpha(0.05f, Theme.FOREGROUND.value));
renderScope.drawText(SMALL_SCALE, "PREVIEW", STAGE_PADDING, STAGE_PADDING, Theme.FONT.value,
Theme.getMutedFg());
String hint = "Drag to rotate";
int hintWidth = (int) (Fonts.getWidth(hint, Theme.getBodyFont()) * SMALL_SCALE);
renderScope.drawText(SMALL_SCALE, hint, width - STAGE_PADDING - hintWidth, STAGE_PADDING,
Theme.getBodyFont(), Theme.getMutedFg());
renderScope.drawRectangle(STAGE_PADDING, height - STAGE_FOOTER, width - STAGE_PADDING * 2, 1,
Theme.getBorder());
}
}
}
@@ -11,7 +11,6 @@ import org.saturnclient.ui.elements.Button;
import org.saturnclient.ui.elements.ImageTexture; import org.saturnclient.ui.elements.ImageTexture;
import org.saturnclient.ui.elements.TextureButton; import org.saturnclient.ui.elements.TextureButton;
import org.saturnclient.ui.resources.Textures; import org.saturnclient.ui.resources.Textures;
import org.saturnclient.ui.screens.cosmetics.CloakMenu;
public class ShiftMenu extends SaturnScreen { public class ShiftMenu extends SaturnScreen {
public ShiftMenu() { public ShiftMenu() {
@@ -51,7 +50,7 @@ public class ShiftMenu extends SaturnScreen {
.animation(new Fade(AnimationConfig.shiftMenu))); .animation(new Fade(AnimationConfig.shiftMenu)));
stagger.draw(new TextureButton(Textures.COSMETICS, () -> { stagger.draw(new TextureButton(Textures.COSMETICS, () -> {
Providers.saturn.getClient().setScreen(new CloakMenu()); Providers.saturn.getClient().setScreen(new CosmeticsMenu());
}).dimensions(btnHeight, btnHeight).position(btnWidth + btnHeight + (spacing * 2), 0) }).dimensions(btnHeight, btnHeight).position(btnWidth + btnHeight + (spacing * 2), 0)
.animation(new Fade(AnimationConfig.shiftMenu))); .animation(new Fade(AnimationConfig.shiftMenu)));
@@ -14,7 +14,6 @@ import org.saturnclient.ui.elements.ImageTexture;
import org.saturnclient.ui.elements.TextureButton; import org.saturnclient.ui.elements.TextureButton;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures; import org.saturnclient.ui.resources.Textures;
import org.saturnclient.ui.screens.cosmetics.CloakMenu;
public class TitleMenu extends SaturnScreen { public class TitleMenu extends SaturnScreen {
public TitleMenu() { public TitleMenu() {
@@ -68,7 +67,7 @@ public class TitleMenu extends SaturnScreen {
.animation(new SlideFade(AnimationConfig.mainMenu, s + 8))); .animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
saturnStagger.draw(new TextureButton(Textures.CLOAK, () -> { saturnStagger.draw(new TextureButton(Textures.CLOAK, () -> {
Providers.saturn.getClient().setScreen(new CloakMenu()); Providers.saturn.getClient().setScreen(new CosmeticsMenu());
}).dimensions(s, s).position((s + 2) * 2, 0) }).dimensions(s, s).position((s + 2) * 2, 0)
.animation(new SlideFade(AnimationConfig.mainMenu, s + 8))); .animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
@@ -1,82 +0,0 @@
package org.saturnclient.ui.screens.cosmetics;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.cosmetics.Cloaks;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideY;
import org.saturnclient.ui.components.CosmeticPreview;
import org.saturnclient.ui.components.Sidebar;
import org.saturnclient.ui.components.SkinPreview;
import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.Scroll;
import org.saturnclient.ui.elements.TabMenu;
import org.saturnclient.ui.elements.TabMenu.TabMenuComponent;
import org.saturnclient.ui.resources.Textures;
public class CloakMenu extends SaturnScreen {
public CloakMenu() {
super("Cloaks Menu");
}
@Override
public void ui() {
int p = 10;
int g = 10;
Scroll scroll = new Scroll(p);
int row = 0;
int col = 0;
SaturnPlayer player = SaturnPlayer.get();
AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
if (player != null) {
for (String cloak : Cloaks.availableCloaks) {
stagger.draw(
new CosmeticPreview(
cloak == player.cloak,
Textures.getCloakPreview(cloak),
() -> Cloaks.setCloak(cloak))
.position((50 + g) * col, (111 + g) * row)
.animation(new SlideY(AnimationConfig.cosmeticsMenu, 14)));
if (col == 5) {
col = 0;
row++;
} else {
col++;
}
}
}
scroll.draw(stagger);
int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
draw(scroll
.dimensions(scrollWidth, 350)
.center(width, height));
draw(new SkinPreview(-30.0f, false)
.scale(3.5f)
.position(scroll.x + (scrollWidth - 220), scroll.y + 40)
.animation(new Fade(500)));
draw(new Sidebar(Sidebar.COSMETICS, this.provider::close)
.center(width, height, -(scrollWidth / 2 + 35), 0));
draw(new TabMenu(0,
new TabMenuComponent(Textures.CLOAK, () -> {
}),
new TabMenuComponent(Textures.HAT, () -> {
Providers.saturn.getClient().setScreen(new HatMenu());
}))
.center(width, height, 0, -195));
}
}
@@ -1,82 +0,0 @@
package org.saturnclient.ui.screens.cosmetics;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.cosmetics.Hats;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideY;
import org.saturnclient.ui.components.CosmeticPreview;
import org.saturnclient.ui.components.Sidebar;
import org.saturnclient.ui.components.SkinPreview;
import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.Scroll;
import org.saturnclient.ui.elements.TabMenu;
import org.saturnclient.ui.elements.TabMenu.TabMenuComponent;
import org.saturnclient.ui.resources.Textures;
public class HatMenu extends SaturnScreen {
public HatMenu() {
super("Hats Menu");
}
@Override
public void ui() {
int p = 10;
int g = 10;
Scroll scroll = new Scroll(p);
int row = 0;
int col = 0;
SaturnPlayer player = SaturnPlayer.get();
AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
if (player != null) {
for (String hat : Hats.availableHats) {
stagger.draw(
new CosmeticPreview(
hat == player.hat,
Textures.getHatPreview(hat),
() -> Hats.setHat(hat))
.dimensions(50, 50)
.position((50 + g) * col, (50 + g) * row)
.animation(new SlideY(AnimationConfig.cosmeticsMenu, 14)));
if (col == 5) {
col = 0;
row++;
} else {
col++;
}
}
}
scroll.draw(stagger);
int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
draw(scroll
.dimensions(scrollWidth, 350)
.center(width, height));
draw(new SkinPreview(170f, true)
.scale(3.5f)
.position(scroll.x + (scrollWidth - 220), scroll.y + 40)
.animation(new Fade(500)));
draw(new Sidebar(Sidebar.COSMETICS, this.provider::close)
.center(width, height, -(scrollWidth / 2 + 35), 0));
draw(new TabMenu(1,
new TabMenuComponent(Textures.CLOAK, () -> {
Providers.saturn.getClient().setScreen(new CloakMenu());
}),
new TabMenuComponent(Textures.HAT, () -> {
}))
.center(width, height, 0, -195));
}
}
@@ -1,142 +0,0 @@
package org.saturnclient.ui.screens.store;
// import org.saturnclient.cosmetics.cloak.Cloaks;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Theme;
import org.saturnclient.cosmetics.Cloaks;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideY;
import org.saturnclient.ui.components.CosmeticPreview;
import org.saturnclient.ui.components.Sidebar;
import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.ImageTexture;
import org.saturnclient.ui.elements.Scroll;
import org.saturnclient.ui.elements.TabMenu;
import org.saturnclient.ui.elements.Text;
import org.saturnclient.ui.elements.TabMenu.TabMenuComponent;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures;
// import com.mojang.blaze3d.systems.RenderSystem;
public class CloakStore extends SaturnScreen {
private long lastPurchaseTime = 0;
public CloakStore() {
super("Cloak Store");
}
public CloakStore(long lastPurchaseTime) {
super("Cloak Store");
this.lastPurchaseTime = lastPurchaseTime;
}
@Override
public void ui() {
int p = 10;
int gx = 10;
int gy = 20;
Scroll scroll = new Scroll(p);
int row = 0;
int col = 0;
SaturnPlayer player = SaturnPlayer.get();
AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
if (player != null) {
for (String cloak : Cloaks.ALL_CLOAKS) {
if (!Cloaks.availableCloaks.contains(cloak)) {
int x = (50 + gx) * col;
int y = (111 + gy) * row;
stagger.draw(
new CosmeticPreview(
cloak == player.cloak,
Textures.getCloakPreview(cloak),
() -> handlePurchase(cloak))
.position(x, y)
.animation(new SlideY(AnimationConfig.cosmeticsMenu, 16)));
String t = "100";
stagger.draw(
new Text(t)
.position(x + Fonts.centerX(50, t, Theme.FONT.value), y + 113)
.scale(0.5f)
.animation(new Fade(400)));
stagger.draw(
new ImageTexture(Textures.COINS)
.dimensions(16, 16)
.position(x + Fonts.getWidth(t, Theme.FONT.value) - 1, y + 112)
.scale(0.5f)
.animation(new Fade(400)));
if (col == 8) {
col = 0;
row++;
} else {
col++;
}
}
}
}
scroll.draw(stagger);
int scrollWidth = 480 + 40 + (gx * 2) + (p * 2);
draw(scroll
.dimensions(scrollWidth, 350)
.center(width, height, 15, 0));
draw(new Sidebar(Sidebar.COSMETICS, this.provider::close)
.center(width, height, -((scrollWidth - 30) / 2 + 35), 0));
draw(new TabMenu(0,
new TabMenuComponent(Textures.CLOAK, () -> {
}),
new TabMenuComponent(Textures.HAT, () -> {
Providers.saturn.getClient().setScreen(new HatStore());
}))
.center(width, height, 0, -195));
}
private void handlePurchase(String cloak) {
long now = System.currentTimeMillis();
if (now - lastPurchaseTime >= 3000) {
// Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait");
ServiceClient.buyCloak(cloak);
lastPurchaseTime = now;
new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
// RenderSystem.recordRenderCall(() -> {
// MinecraftProvider.PROVIDER.setScreen(new CloakStore(now));
// // Utils.notify(NotificationKind.Success,
// // "Purchase complete",
// // "Congrats, enjoy your new cloak!");
// });
}).start();
} else {
// Utils.notify(NotificationKind.Error,
// "Timeout error",
// "Please wait 3 seconds");
}
}
}
@@ -1,142 +0,0 @@
package org.saturnclient.ui.screens.store;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Theme;
import org.saturnclient.cosmetics.Hats;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideY;
import org.saturnclient.ui.components.CosmeticPreview;
import org.saturnclient.ui.components.Sidebar;
import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.ImageTexture;
import org.saturnclient.ui.elements.Scroll;
import org.saturnclient.ui.elements.TabMenu;
import org.saturnclient.ui.elements.Text;
import org.saturnclient.ui.elements.TabMenu.TabMenuComponent;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures;
// import com.mojang.blaze3d.systems.RenderSystem;
public class HatStore extends SaturnScreen {
private long lastPurchaseTime = 0;
public HatStore() {
super("Hat Store");
}
public HatStore(long lastPurchaseTime) {
super("Hat Store");
this.lastPurchaseTime = lastPurchaseTime;
}
@Override
public void ui() {
int p = 10;
int gx = 10;
int gy = 20;
Scroll scroll = new Scroll(p);
int row = 0;
int col = 0;
SaturnPlayer player = SaturnPlayer.get();
AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
if (player != null) {
for (String hat : Hats.ALL_HATS) {
if (!Hats.availableHats.contains(hat)) {
int x = (50 + gx) * col;
int y = (50 + gy) * row;
stagger.draw(
new CosmeticPreview(
hat == player.hat,
Textures.getHatPreview(hat),
() -> handlePurchase(hat))
.dimensions(50, 50)
.position(x, y)
.animation(new SlideY(AnimationConfig.cosmeticsMenu, 16)));
String t = "50";
stagger.draw(
new Text(t)
.position(x + Fonts.centerX(50, t, Theme.FONT.value), y + 53)
.scale(0.5f)
.animation(new Fade(400)));
stagger.draw(
new ImageTexture(Textures.COINS)
.dimensions(16, 16)
.position(x + Fonts.getWidth(t, Theme.FONT.value) + 4, y + 52)
.scale(0.5f)
.animation(new Fade(400)));
if (col == 8) {
col = 0;
row++;
} else {
col++;
}
}
}
}
scroll.draw(stagger);
int scrollWidth = 480 + 40 + (gx * 2) + (p * 2);
draw(scroll
.dimensions(scrollWidth, 350)
.center(width, height, 15, 0));
draw(new Sidebar(Sidebar.COSMETICS, this.provider::close)
.center(width, height, -((scrollWidth - 30) / 2 + 35), 0));
draw(new TabMenu(1,
new TabMenuComponent(Textures.CLOAK, () -> {
Providers.saturn.getClient().setScreen(new CloakStore());
}),
new TabMenuComponent(Textures.HAT, () -> {
}))
.center(width, height, 0, -195));
}
private void handlePurchase(String hat) {
long now = System.currentTimeMillis();
if (now - lastPurchaseTime >= 3000) {
// Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait");
ServiceClient.buyHat(hat);
lastPurchaseTime = now;
new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
// RenderSystem.recordRenderCall(() -> {
// MinecraftProvider.PROVIDER.setScreen(new HatStore(now));
// // Utils.notify(NotificationKind.Success,
// // "Purchase complete",
// // "Congrats, enjoy your new hat!");
// });
}).start();
} else {
// Utils.notify(NotificationKind.Error,
// "Timeout error",
// "Please wait 3 seconds");
}
}
}