From 8fd8c429a63baae27f6f90b1e0634672e44d794a Mon Sep 17 00:00:00 2001
From: Richard <56808540+littensy@users.noreply.github.com>
Date: Sun, 1 Oct 2023 23:57:07 -0700
Subject: [PATCH] feat: add pride & country skins, improve ux (#146)
* feat: add pride skins, more countries
* feat: players start with 100 cash
* feat: show skin name in alert, improve colors
---
src/client/app/components/alerts/alert.tsx | 4 +-
.../app/components/menu/skins/act-button.tsx | 40 ++-
src/client/app/components/menu/skins/utils.ts | 4 +-
.../app/stories/components/skins.story.tsx | 7 +-
src/client/app/utils/format-integer.ts | 2 +-
src/server/players/services/remotes.ts | 2 +-
src/shared/data/skins/skins.ts | 301 +++++++++++++++++-
src/shared/store/saves/save-slice.ts | 8 +-
src/shared/store/saves/save-types.ts | 8 +-
src/shared/utils/color-utils.ts | 8 +-
10 files changed, 342 insertions(+), 42 deletions(-)
diff --git a/src/client/app/components/alerts/alert.tsx b/src/client/app/components/alerts/alert.tsx
index 5383df9..c9b375b 100644
--- a/src/client/app/components/alerts/alert.tsx
+++ b/src/client/app/components/alerts/alert.tsx
@@ -44,8 +44,8 @@ export function Alert({ alert, index }: AlertProps) {
const style = useMemo(() => {
const highlight = composeBindings(hover, transition, (a, b) => a * b);
- const background = darken(alert.color.Lerp(palette.base, 0.25), 0.5);
- const backgroundSecondary = darken(alert.colorSecondary?.Lerp(palette.base, 0.25) || palette.white, 0.5);
+ const background = darken(alert.color.Lerp(palette.base, 0.25), 0.8);
+ const backgroundSecondary = darken(alert.colorSecondary?.Lerp(palette.base, 0.25) || palette.white, 0.8);
const message = brightenIfDark(alert.colorMessage || alert.color);
return { highlight, background, backgroundSecondary, message };
diff --git a/src/client/app/components/menu/skins/act-button.tsx b/src/client/app/components/menu/skins/act-button.tsx
index fa6771e..ab06bb3 100644
--- a/src/client/app/components/menu/skins/act-button.tsx
+++ b/src/client/app/components/menu/skins/act-button.tsx
@@ -8,6 +8,7 @@ import { Text } from "client/app/common/text";
import { useMotion, useRem } from "client/app/hooks";
import { composeBindings } from "client/app/utils/compose-bindings";
import { fonts } from "client/app/utils/fonts";
+import { formatInteger } from "client/app/utils/format-integer";
import { springs } from "client/app/utils/springs";
import { selectMenuCurrentSkin } from "client/store/menu";
import { sounds } from "shared/assets";
@@ -16,15 +17,25 @@ import { palette } from "shared/data/palette";
import { findSnakeSkin } from "shared/data/skins";
import { remotes } from "shared/remotes";
import { RANDOM_SKIN, selectCurrentPlayerSkin, selectPlayerBalance, selectPlayerSkins } from "shared/store/saves";
-import { brighten } from "shared/utils/color-utils";
+import { darken } from "shared/utils/color-utils";
interface Status {
readonly variant: "buy" | "not-enough-money" | "wear" | "wearing" | "none";
readonly price?: number;
}
-const darkGreen = brighten(palette.green, -3);
-const darkRed = brighten(palette.red, -3);
+const darkGreen = darken(palette.green, 0.5, 0.5);
+const darkRed = darken(palette.red, 0.25, 0.5);
+const darkBlue = darken(palette.blue, 0.25, 0.5);
+const darkPeach = darken(palette.peach, 0.25, 0.5);
+
+function stylize(text: unknown, color: Color3) {
+ if (text === `"${RANDOM_SKIN}"`) {
+ text = '"random"';
+ }
+
+ return `${text}`;
+}
function getStatus(equipped: string, current: string, inventory: readonly string[] = [], balance = 0): Status {
const equippedSkin = findSnakeSkin(equipped);
@@ -47,11 +58,11 @@ function getStatus(equipped: string, current: string, inventory: readonly string
export function ActButton() {
const rem = useRem();
- const equipped = useSelectorCreator(selectCurrentPlayerSkin, LOCAL_USER) ?? RANDOM_SKIN;
- const current = useSelector(selectMenuCurrentSkin);
+ const equippedSkin = useSelectorCreator(selectCurrentPlayerSkin, LOCAL_USER) ?? RANDOM_SKIN;
+ const currentSkin = useSelector(selectMenuCurrentSkin);
const inventory = useSelectorCreator(selectPlayerSkins, LOCAL_USER);
const balance = useSelectorCreator(selectPlayerBalance, LOCAL_USER);
- const status = getStatus(equipped, current, inventory, balance);
+ const status = getStatus(equippedSkin, currentSkin, inventory, balance);
const [primary, primaryMotion] = useMotion(new Color3());
const [secondary, secondaryMotion] = useMotion(new Color3());
@@ -75,14 +86,14 @@ export function ActButton() {
gradientSpinMotion.spring(gradientSpin.getValue() + 180, springs.molasses);
if (status.variant === "buy") {
- remotes.save.buySkin.fire(current);
+ remotes.save.buySkin.fire(currentSkin);
} else if (status.variant === "wear") {
- remotes.save.setSkin.fire(current);
+ remotes.save.setSkin.fire(currentSkin);
} else if (status.variant === "not-enough-money") {
sendAlert({
emoji: "🚨",
color: palette.red,
- message: `Sorry, you cannot afford the ${current} skin yet.`,
+ message: `Sorry, you cannot afford the ${stylize(currentSkin, palette.white)} skin yet.`,
sound: sounds.alert_bad,
});
}
@@ -112,13 +123,16 @@ export function ActButton() {
const text =
status.variant === "buy"
- ? `💵 Buy for $${status.price}`
+ ? `💵 Buy ${stylize(`"${currentSkin}"`, darkGreen)} for ${stylize(
+ "$" + formatInteger(status.price),
+ darkGreen,
+ )}`
: status.variant === "wear"
- ? "🎨 Wear"
+ ? `🎨 Wear ${stylize(`"${currentSkin}"`, darkBlue)}`
: status.variant === "wearing"
- ? "🎨 Wearing"
+ ? `🎨 Wearing ${stylize(`"${currentSkin}"`, darkPeach)}`
: status.variant === "not-enough-money"
- ? `🔒 Costs $${status.price}`
+ ? `🔒 ${stylize(`"${currentSkin}"`, darkRed)} costs ${stylize("$" + formatInteger(status.price), darkRed)}`
: "🔒 Locked";
return (
diff --git a/src/client/app/components/menu/skins/utils.ts b/src/client/app/components/menu/skins/utils.ts
index 7cc425c..84888c1 100644
--- a/src/client/app/components/menu/skins/utils.ts
+++ b/src/client/app/components/menu/skins/utils.ts
@@ -24,7 +24,7 @@ export function usePalette(id: string, shuffle?: readonly string[]): SnakePalett
return {
skin,
- primary: skin.primary || darken(skin.tint[0], 0.5, 0.5),
- secondary: skin.secondary || darken(skin.tint[0], 0.7, 0.5),
+ primary: skin.primary || darken(skin.tint[0], 0.5, 0.4),
+ secondary: skin.secondary || darken(skin.tint[0], 0.7, 0.4),
};
}
diff --git a/src/client/app/stories/components/skins.story.tsx b/src/client/app/stories/components/skins.story.tsx
index 349f7d4..6f26918 100644
--- a/src/client/app/stories/components/skins.story.tsx
+++ b/src/client/app/stories/components/skins.story.tsx
@@ -1,5 +1,6 @@
import { hoarcekat, useMountEffect } from "@rbxts/pretty-react-hooks";
import Roact from "@rbxts/roact";
+import { Alerts } from "client/app/components/alerts";
import { Menu } from "client/app/components/menu";
import { World } from "client/app/components/world";
import { RootProvider } from "client/app/providers/root-provider";
@@ -14,16 +15,14 @@ export = hoarcekat(() => {
useMountEffect(() => {
store.setMenuPage("skins");
- store.setPlayerSave(LOCAL_USER, {
- ...defaultPlayerSave,
- balance: 45,
- });
+ store.setPlayerSave(LOCAL_USER, defaultPlayerSave);
});
return (
+
);
});
diff --git a/src/client/app/utils/format-integer.ts b/src/client/app/utils/format-integer.ts
index 007e28a..facb554 100644
--- a/src/client/app/utils/format-integer.ts
+++ b/src/client/app/utils/format-integer.ts
@@ -1,6 +1,6 @@
/**
* Reformat a number to a string with a thousands separator.
*/
-export function formatInteger(value: string | number) {
+export function formatInteger(value: unknown) {
return tostring(value).reverse().gsub("%d%d%d", "%1,")[0].reverse().gsub("^,", "")[0];
}
diff --git a/src/server/players/services/remotes.ts b/src/server/players/services/remotes.ts
index 9bfb9c8..2095c75 100644
--- a/src/server/players/services/remotes.ts
+++ b/src/server/players/services/remotes.ts
@@ -44,7 +44,7 @@ export async function initRemoteService() {
colorMessage: skin?.primary || skin?.tint[0] || palette.mauve,
message:
skinId === RANDOM_SKIN
- ? "You are now wearing a random skin!"
+ ? 'You are now wearing a random skin!'
: `You are now wearing the ${skinId} skin!`,
});
} else {
diff --git a/src/shared/data/skins/skins.ts b/src/shared/data/skins/skins.ts
index f6879fb..e5a944a 100644
--- a/src/shared/data/skins/skins.ts
+++ b/src/shared/data/skins/skins.ts
@@ -6,7 +6,7 @@ import { SnakeSkin } from "../skins";
import { defaultSnakeSkin } from "./types";
import { blendColorSequence, duplicate } from "./utils";
-export const baseSnakeSkins: readonly SnakeSkin[] = accentList.map((id) => {
+const catppuccinSnakeSkins: readonly SnakeSkin[] = accentList.map((id) => {
return {
...defaultSnakeSkin,
id,
@@ -15,7 +15,7 @@ export const baseSnakeSkins: readonly SnakeSkin[] = accentList.map((id) => {
});
export const snakeSkins: readonly SnakeSkin[] = [
- ...baseSnakeSkins,
+ ...catppuccinSnakeSkins,
{
...defaultSnakeSkin,
@@ -55,6 +55,22 @@ export const snakeSkins: readonly SnakeSkin[] = [
...defaultSnakeSkin,
id: "france",
price: 100,
+ tint: [
+ palette.blue,
+ palette.blue,
+ palette.offwhite,
+ palette.offwhite,
+ palette.red,
+ palette.red,
+ palette.offwhite,
+ palette.offwhite,
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "mexico",
+ price: 100,
tint: [palette.blue, palette.blue, palette.offwhite, palette.offwhite, palette.red, palette.red],
},
@@ -79,31 +95,298 @@ export const snakeSkins: readonly SnakeSkin[] = [
tint: [palette.blue, palette.blue, palette.surface1, palette.surface1, palette.offwhite, palette.offwhite],
},
+ {
+ ...defaultSnakeSkin,
+ id: "brazil",
+ price: 100,
+ tint: [palette.green, palette.green, palette.yellow, palette.yellow, palette.blue, palette.blue],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "australia",
+ price: 100,
+ tint: [
+ palette.blue,
+ palette.blue,
+ palette.blue,
+ palette.white,
+ palette.white,
+ palette.offwhite,
+ palette.red,
+ palette.red,
+ palette.offwhite,
+ ],
+ texture: [
+ images.skins.snake_main,
+ images.skins.snake_main,
+ images.skins.snake_main,
+ images.skins.snake_stars,
+ images.skins.snake_stars,
+ images.skins.snake_main,
+ images.skins.snake_main,
+ images.skins.snake_main,
+ images.skins.snake_main,
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "finland",
+ price: 100,
+ tint: [palette.offwhite, palette.offwhite, palette.offwhite, palette.blue],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "norway",
+ price: 100,
+ tint: [palette.red, palette.red, palette.offwhite, palette.blue, palette.blue],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "denmark",
+ price: 100,
+ tint: [palette.red, palette.red, palette.offwhite],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "sweden",
+ price: 100,
+ tint: [palette.blue, palette.blue, palette.yellow],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "poland",
+ price: 100,
+ tint: [palette.offwhite, palette.offwhite, palette.red, palette.red],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "czech",
+ price: 100,
+ tint: [
+ palette.offwhite,
+ palette.offwhite,
+ palette.offwhite,
+ palette.blue,
+ palette.blue,
+ palette.red,
+ palette.red,
+ palette.red,
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "hungary",
+ price: 100,
+ tint: [palette.red, palette.red, palette.offwhite, palette.offwhite, palette.green, palette.green],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "south-africa",
+ price: 100,
+ tint: [
+ palette.red,
+ palette.red,
+ palette.offwhite,
+ palette.green,
+ palette.green,
+ palette.yellow,
+ palette.crust,
+ palette.crust,
+ palette.yellow,
+ palette.green,
+ palette.green,
+ palette.offwhite,
+ palette.blue,
+ palette.blue,
+ palette.offwhite,
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#ed5352"),
+ Color3.fromHex("#ef8c3d"),
+ Color3.fromHex("#f8c654"),
+ Color3.fromHex("#7cb788"),
+ Color3.fromHex("#4b98cb"),
+ Color3.fromHex("#bc59be"),
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "bi-pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#ea4689"),
+ Color3.fromHex("#ea4689"),
+ Color3.fromHex("#ea4689"),
+ Color3.fromHex("#b08dfb"),
+ Color3.fromHex("#3059bb"),
+ Color3.fromHex("#3059bb"),
+ Color3.fromHex("#3059bb"),
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "pan-pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#ea4689"),
+ Color3.fromHex("#ea4689"),
+ Color3.fromHex("#f4c757"),
+ Color3.fromHex("#f4c757"),
+ Color3.fromHex("#60b4ea"),
+ Color3.fromHex("#60b4ea"),
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "lesbian-pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#e86366"),
+ Color3.fromHex("#e58f3f"),
+ Color3.fromHex("#e8ba64"),
+ Color3.fromHex("#fcfffe"),
+ Color3.fromHex("#d2a8cd"),
+ Color3.fromHex("#b95bbd"),
+ Color3.fromHex("#862b6b"),
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "ace-pride",
+ price: 100,
+ tint: [palette.base, Color3.fromHex("#bcb6ba"), Color3.fromHex("#fcfffe"), Color3.fromHex("#b95bbd")],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "aro-pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#78b88b"),
+ Color3.fromHex("#a3dbb2"),
+ Color3.fromHex("#fcfffe"),
+ Color3.fromHex("#bcb6ba"),
+ palette.base,
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "agender-pride",
+ price: 100,
+ tint: [
+ palette.base,
+ Color3.fromHex("#bcb6ba"),
+ Color3.fromHex("#fcfffe"),
+ Color3.fromHex("#78b88b"),
+ Color3.fromHex("#fcfffe"),
+ Color3.fromHex("#bcb6ba"),
+ palette.base,
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "genderfluid-pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#e88599"),
+ Color3.fromHex("#fcfffe"),
+ Color3.fromHex("#b95bbd"),
+ palette.base,
+ Color3.fromHex("#2c5bbb"),
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "genderqueer-pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#b85cb9"),
+ Color3.fromHex("#b85cb9"),
+ Color3.fromHex("#fcfffe"),
+ Color3.fromHex("#fcfffe"),
+ Color3.fromHex("#79b78a"),
+ Color3.fromHex("#79b78a"),
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "trans-pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#94c8e5"),
+ Color3.fromHex("#f5cfc8"),
+ Color3.fromHex("#fcfffe"),
+ Color3.fromHex("#f5cfc8"),
+ ],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "nonbinary-pride",
+ price: 100,
+ tint: [Color3.fromHex("#f4c757"), Color3.fromHex("#fcfffe"), Color3.fromHex("#b95bbd"), palette.base],
+ },
+
+ {
+ ...defaultSnakeSkin,
+ id: "intersex-pride",
+ price: 100,
+ tint: [
+ Color3.fromHex("#f6c754"),
+ Color3.fromHex("#f6c754"),
+ Color3.fromHex("#f6c754"),
+ Color3.fromHex("#b95bbd"),
+ ],
+ },
+
{
...defaultSnakeSkin,
id: "peppermint",
- price: 200,
+ price: 150,
tint: [palette.red, palette.red, palette.offwhite, palette.offwhite],
},
{
...defaultSnakeSkin,
id: "candycorn",
- price: 250,
+ price: 150,
tint: [palette.yellow, palette.yellow, palette.peach, palette.peach, palette.offwhite],
},
{
...defaultSnakeSkin,
id: "zebra",
- price: 300,
+ price: 250,
tint: [palette.overlay0, palette.text],
},
{
...defaultSnakeSkin,
id: "honeybee",
- price: 450,
+ price: 350,
tint: [palette.mantle, palette.mantle, palette.yellow],
},
@@ -167,7 +450,7 @@ export const snakeSkins: readonly SnakeSkin[] = [
palette.blue,
palette.mauve,
],
- 18,
+ 30,
),
primary: Color3.fromRGB(186, 51, 84),
secondary: Color3.fromRGB(217, 97, 125),
@@ -261,3 +544,7 @@ export const snakeSkins: readonly SnakeSkin[] = [
secondary: palette.crust,
},
];
+
+export const baseSnakeSkins = snakeSkins.filter((skin) => {
+ return skin.price === 0;
+});
diff --git a/src/shared/store/saves/save-slice.ts b/src/shared/store/saves/save-slice.ts
index cf94259..eecb1fc 100644
--- a/src/shared/store/saves/save-slice.ts
+++ b/src/shared/store/saves/save-slice.ts
@@ -1,16 +1,12 @@
import { createProducer } from "@rbxts/reflex";
import { mapProperty } from "shared/utils/object-utils";
+import { PlayerSave } from "./save-types";
+
export interface SaveState {
readonly [id: string]: PlayerSave | undefined;
}
-export interface PlayerSave {
- readonly balance: number;
- readonly skins: readonly string[];
- readonly skin: string;
-}
-
const initialState: SaveState = {};
export const saveSlice = createProducer(initialState, {
diff --git a/src/shared/store/saves/save-types.ts b/src/shared/store/saves/save-types.ts
index 76692c3..b370a9e 100644
--- a/src/shared/store/saves/save-types.ts
+++ b/src/shared/store/saves/save-types.ts
@@ -1,12 +1,16 @@
import { t } from "@rbxts/t";
import { baseSnakeSkins } from "shared/data/skins";
-import { PlayerSave } from "./save-slice";
+export interface PlayerSave {
+ readonly balance: number;
+ readonly skins: readonly string[];
+ readonly skin: string;
+}
export const RANDOM_SKIN = "__random__";
export const defaultPlayerSave: PlayerSave = {
- balance: 0,
+ balance: 100,
skins: [RANDOM_SKIN, ...baseSnakeSkins.map((skin) => skin.id)],
skin: RANDOM_SKIN,
};
diff --git a/src/shared/utils/color-utils.ts b/src/shared/utils/color-utils.ts
index 78b9b4d..0dcb952 100644
--- a/src/shared/utils/color-utils.ts
+++ b/src/shared/utils/color-utils.ts
@@ -1,15 +1,15 @@
const lerpAlpha = (a: number, b: number, t: number) => math.clamp(a + (b - a) * t, 0, 1);
-export function brighten(color: Color3, amount: number, saturation = 0.1) {
+export function brighten(color: Color3, amount: number, desaturation = 0.25 * amount) {
const [h, s, v] = color.ToHSV();
- return Color3.fromHSV(h, lerpAlpha(s, 0, saturation * amount), lerpAlpha(v, 1, 0.7 * amount));
+ return Color3.fromHSV(h, lerpAlpha(s, 0, desaturation), lerpAlpha(v, 1, 0.7 * amount));
}
-export function darken(color: Color3, amount: number, saturation = 0.1) {
+export function darken(color: Color3, amount: number, saturation = 0.25 * amount) {
const [h, s, v] = color.ToHSV();
- return Color3.fromHSV(h, lerpAlpha(s, 1, saturation * amount), lerpAlpha(v, 0, 0.7 * amount));
+ return Color3.fromHSV(h, lerpAlpha(s, 1, saturation), lerpAlpha(v, 0, 0.7 * amount));
}
export function brightness(color: Color3) {