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) {