Rename shop items to plain sprite labels and improve thumbnail visibility.

Use code-side display names in the shop UI so labels stay correct even when DB rows lag, and add a neutral thumbnail glow so dark colors remain visible.
This commit is contained in:
2026-05-24 10:09:50 -06:00
parent aa503dd5e3
commit e41fc10c77
8 changed files with 122 additions and 80 deletions

View File

@@ -262,6 +262,19 @@
@apply border-2 border-border bg-card px-2 py-1 font-pixel leading-snug text-card-foreground;
box-shadow: var(--retro-shadow-sm);
}
.shop-item-thumbnail {
background: hsl(var(--muted) / 0.22);
}
.shop-item-thumbnail-glow {
background: radial-gradient(
circle at 50% 58%,
hsl(52 55% 90% / 0.55) 0%,
hsl(48 30% 88% / 0.18) 45%,
transparent 72%
);
}
}
@keyframes emotion-pop {

View File

@@ -22,6 +22,7 @@ import {
} from "@/lib/shop-storage";
import {
getLayerLabel,
getShopItemDisplayName,
type ShopItemRecord,
type ShopLayerId,
} from "@/lib/shop-catalog";
@@ -103,6 +104,8 @@ function ShopItemCard({
? null
: getShopItemThumbnailColors(item, characterColors);
const displayName = getShopItemDisplayName(item);
const actionLabel = (() => {
if (isPending) {
return owned ? "Updating..." : "Processing...";
@@ -123,27 +126,33 @@ function ShopItemCard({
<Card className="flex h-full flex-col overflow-hidden">
<CardContent className="flex h-full flex-col gap-3 p-3">
<div className="flex min-w-0 items-start gap-3">
<div className="flex size-12 shrink-0 items-center justify-center overflow-hidden border-2 border-border bg-zinc-950/80 p-1">
{room ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={item.image_path || room.sceneImage}
alt=""
className="size-full object-cover image-pixelated"
/>
) : thumbnailColors ? (
<TintedSpriteIcon
src={item.image_path}
color={thumbnailColors.color}
skinColor={thumbnailColors.skinColor}
size={32}
/>
<div className="shop-item-thumbnail relative flex size-12 shrink-0 items-center justify-center overflow-hidden border-2 border-border p-1">
{thumbnailColors || room ? (
<div aria-hidden className="shop-item-thumbnail-glow pointer-events-none absolute inset-0" />
) : null}
<div className="relative z-10 flex size-full items-center justify-center">
{room ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={item.image_path || room.sceneImage}
alt=""
className="size-full object-cover image-pixelated"
/>
) : thumbnailColors ? (
<TintedSpriteIcon
src={item.image_path}
color={thumbnailColors.color}
skinColor={thumbnailColors.skinColor}
size={32}
className="drop-shadow-[0_0_3px_hsl(52_55%_90%_/_0.85)]"
/>
) : null}
</div>
</div>
<div className="flex min-w-0 flex-1 items-start justify-between gap-2">
<p className="min-w-0 pt-1 text-sm font-medium leading-snug">
{item.name}
{displayName}
</p>
<ShopItemPriceBadge
price={item.price}
@@ -249,7 +258,7 @@ export function ShopContent() {
try {
const item = await purchaseShopItem(itemId);
await equipShopItem(itemId);
toast(`Purchased and equipped ${item.name}.`, "success");
toast(`Purchased and equipped ${getShopItemDisplayName(item)}.`, "success");
await refreshShop();
} catch (purchaseError) {
toast(
@@ -274,10 +283,10 @@ export function ShopContent() {
if (item.type !== "room" && isEquipped) {
await unequipShopItem(item.id);
toast(`Unequipped ${item.name}.`, "default");
toast(`Unequipped ${getShopItemDisplayName(item)}.`, "default");
} else if (!isEquipped) {
await equipShopItem(item.id);
toast(`Equipped ${item.name}.`, "success");
toast(`Equipped ${getShopItemDisplayName(item)}.`, "success");
}
await refreshShop();

View File

@@ -12,7 +12,7 @@ import {
buildShopItemPreviewCustomization,
getShopPreviewRoomId,
} from "@/lib/shop-preview";
import type { ShopItemRecord } from "@/lib/shop-catalog";
import { getShopItemDisplayName, type ShopItemRecord } from "@/lib/shop-catalog";
import { routes } from "@/lib/routes";
type ShopItemPreviewModalProps = {
@@ -89,7 +89,7 @@ export function ShopItemPreviewModal({
>
Preview on your bit
</p>
<p className="mt-1 truncate text-sm">{item.name}</p>
<p className="mt-1 truncate text-sm">{getShopItemDisplayName(item)}</p>
</div>
<Button
type="button"

View File

@@ -61,25 +61,25 @@ function presetFromHex(id: string, name: string, hex: string): ColorPreset {
export const VARIANT_DISPLAY_NAMES: Record<string, string> = {
"skin-1": "Default Form",
"pants-1": "Slim Fit Jeans",
"pants-2": "Rolled Cuff Shorts",
"pants-3": "Pleated Skirt",
"shoes-1": "Canvas Sneakers",
"shoes-2": "Chunky High-Tops",
"shoes-3": "Neon Glow Runners",
"torso-1": "Crew Neck Tee",
"torso-2": "Zip-Up Hoodie",
"torso-3": "Button-Up Shirt",
"torso-4": "Crimson Blouse",
"eyes-1": "Round Gentle Eyes",
"eyes-2": "Starry Spark Eyes",
"head-1": "Spiked Mohawk",
"head-2": "Cozy Knit Beanie",
"head-3": "Retro Snapback Cap",
"head-4": "Long Hair",
"head-5": "Shoulder-Length Hair",
"head-6": "Flowing Side Locks",
"head-7": "Messy Shag Cut",
"pants-1": "Shorts",
"pants-2": "Pants",
"pants-3": "Skirt",
"shoes-1": "Boots",
"shoes-2": "Shoes",
"shoes-3": "Loafers",
"torso-1": "T-Shirt",
"torso-2": "Button-up",
"torso-3": "Vest",
"torso-4": "Robe",
"eyes-1": "Eyes",
"eyes-2": "Eyelashes",
"head-1": "Short",
"head-2": "Short-medium",
"head-3": "Mohawk",
"head-4": "Long",
"head-5": "Pixie",
"head-6": "Middle-part",
"head-7": "Bun",
};
export function getVariantDisplayName(variantId: string, fallbackLabel = variantId) {

View File

@@ -19,25 +19,25 @@ export const ROOM_SCENE_IMAGE_PATH = (id: RoomBackgroundId) =>
export const ROOM_BACKGROUNDS: RoomBackground[] = [
{
id: "room-1",
name: "Sunny Field",
name: "Plains",
free: true,
sceneImage: ROOM_SCENE_IMAGE_PATH("room-1"),
},
{
id: "room-2",
name: "Golden Dusk",
name: "Sunset",
free: true,
sceneImage: ROOM_SCENE_IMAGE_PATH("room-2"),
},
{
id: "room-3",
name: "Starlit Night",
name: "Forest",
free: false,
sceneImage: ROOM_SCENE_IMAGE_PATH("room-3"),
},
{
id: "room-4",
name: "Mystic Forest",
name: "Night",
free: false,
sceneImage: ROOM_SCENE_IMAGE_PATH("room-4"),
},

View File

@@ -1,7 +1,11 @@
import { isShopOnlyStyleVariant } from "@/lib/character/variant-access";
import type { CharacterLayerId } from "@/lib/character/presets";
import { CHARACTER_LAYERS, NONE_VARIANT_ID } from "@/lib/character/presets";
import { getShopRoomBackgrounds } from "@/lib/room-backgrounds";
import {
CHARACTER_LAYERS,
getVariantDisplayName,
NONE_VARIANT_ID,
} from "@/lib/character/presets";
import { getRoomBackground, getShopRoomBackgrounds } from "@/lib/room-backgrounds";
export type ShopLayerId = Exclude<CharacterLayerId, "skin">;
@@ -95,6 +99,14 @@ export function getLayerLabel(layerId: ShopLayerId) {
return CHARACTER_LAYERS.find((layer) => layer.id === layerId)?.label ?? layerId;
}
export function getShopItemDisplayName(item: ShopItemRecord): string {
if (item.type === "room") {
return getRoomBackground(item.id).name;
}
return getVariantDisplayName(item.id, item.name);
}
export function findShopItemById(
items: readonly ShopItemRecord[],
itemId: string,

View File

@@ -0,0 +1,30 @@
-- Align shop item display names with sprite-based labels.
update public.shop_items set name = 'Plains' where id = 'room-1';
update public.shop_items set name = 'Sunset' where id = 'room-2';
update public.shop_items set name = 'Forest' where id = 'room-3';
update public.shop_items set name = 'Night' where id = 'room-4';
update public.shop_items set name = 'Eyes' where id = 'eyes-1';
update public.shop_items set name = 'Eyelashes' where id = 'eyes-2';
update public.shop_items set name = 'Short' where id = 'head-1';
update public.shop_items set name = 'Short-medium' where id = 'head-2';
update public.shop_items set name = 'Mohawk' where id = 'head-3';
update public.shop_items set name = 'Long' where id = 'head-4';
update public.shop_items set name = 'Pixie' where id = 'head-5';
update public.shop_items set name = 'Middle-part' where id = 'head-6';
update public.shop_items set name = 'Bun' where id = 'head-7';
update public.shop_items set name = 'T-Shirt' where id = 'torso-1';
update public.shop_items set name = 'Button-up' where id = 'torso-2';
update public.shop_items set name = 'Vest' where id = 'torso-3';
update public.shop_items set name = 'Robe' where id = 'torso-4';
update public.shop_items set name = 'Shorts' where id = 'pants-1';
update public.shop_items set name = 'Pants' where id = 'pants-2';
update public.shop_items set name = 'Skirt' where id = 'pants-3';
update public.shop_items set name = 'Boots' where id = 'shoes-1';
update public.shop_items set name = 'Shoes' where id = 'shoes-2';
update public.shop_items set name = 'Loafers' where id = 'shoes-3';

View File

@@ -1,40 +1,18 @@
-- Character layer styles sold in the shop (id = variant id, type = layer id).
delete from public.user_items
where item_id in ('hat_blue', 'glasses_round', 'room_sunset', 'room-space', 'room-neon');
delete from public.shop_items
where id in (
'hat_blue',
'glasses_round',
'room_sunset',
'room-space',
'room-neon',
'pants-1',
'pants-2',
'shoes-1',
'shoes-2',
'torso-1',
'torso-2',
'eyes-1',
'eyes-2',
'head-1',
'head-2'
);
-- Shop-only catalog items (variant ids 3+ and paid rooms).
insert into public.shop_items (id, name, type, price, image_path)
values
('pants-3', 'Pleated Skirt', 'pants', 30, '/character/pants/pants-3.png'),
('shoes-3', 'Neon Glow Runners', 'shoes', 30, '/character/shoes/shoes-3.png'),
('torso-3', 'Button-Up Shirt', 'torso', 35, '/character/torso/torso-3.png'),
('torso-4', 'Crimson Blouse', 'torso', 40, '/character/torso/torso-4.png'),
('head-3', 'Retro Snapback Cap', 'head', 40, '/character/head/head-3.png'),
('head-4', 'Long Hair', 'head', 45, '/character/head/head-4.png'),
('head-5', 'Shoulder-Length Hair', 'head', 50, '/character/head/head-5.png'),
('head-6', 'Flowing Side Locks', 'head', 55, '/character/head/head-6.png'),
('head-7', 'Messy Shag Cut', 'head', 60, '/character/head/head-7.png'),
('room-3', 'Starlit Night', 'room', 40, '/room/room-3.png'),
('room-4', 'Mystic Forest', 'room', 50, '/room/room-4.png')
('head-3', 'Mohawk', 'head', 40, '/character/head/head-3.png'),
('head-4', 'Long', 'head', 45, '/character/head/head-4.png'),
('head-5', 'Pixie', 'head', 50, '/character/head/head-5.png'),
('head-6', 'Middle-part', 'head', 55, '/character/head/head-6.png'),
('head-7', 'Bun', 'head', 60, '/character/head/head-7.png'),
('torso-3', 'Vest', 'torso', 35, '/character/torso/torso-3.png'),
('torso-4', 'Robe', 'torso', 40, '/character/torso/torso-4.png'),
('pants-3', 'Skirt', 'pants', 30, '/character/pants/pants-3.png'),
('shoes-3', 'Loafers', 'shoes', 30, '/character/shoes/shoes-3.png'),
('room-3', 'Forest', 'room', 40, '/room/room-3.png'),
('room-4', 'Night', 'room', 50, '/room/room-4.png')
on conflict (id) do update
set
name = excluded.name,