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:
@@ -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.HudEditor;
|
||||
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,
|
||||
@@ -37,7 +37,7 @@ public class Sidebar extends Element {
|
||||
|
||||
private final Item[] items = {
|
||||
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_SETTINGS, () -> Providers.saturn.getClient().setScreen(new ClientSettings())),
|
||||
};
|
||||
|
||||
@@ -1,26 +1,68 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* The player, wearing their cosmetics, filling the element. Dragging it turns
|
||||
* the player around.
|
||||
*/
|
||||
public class SkinPreview extends Element {
|
||||
public static EntityDrawer DRAWER;
|
||||
private boolean negativeAngle = false;
|
||||
private float angle = 0.0f;
|
||||
|
||||
public SkinPreview(float angle, boolean negativeAngle) {
|
||||
this.angle = angle;
|
||||
this.negativeAngle = negativeAngle;
|
||||
/** Degrees turned per GUI pixel dragged */
|
||||
private static final float DRAG_SPEED = 1.2f;
|
||||
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
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user