Tint shop item thumbnails with the user's character layer colors.
Replace default gray previews so clothing and accessories in the shop grid reflect each layer's saved color, including skin tone for eyes.
This commit is contained in:
@@ -8,10 +8,11 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { useToast } from "@/components/ui/toast-provider";
|
import { useToast } from "@/components/ui/toast-provider";
|
||||||
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
|
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
|
||||||
import { getAvatarCustomization } from "@/lib/avatar-customization-storage";
|
import {
|
||||||
import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
|
defaultAvatarCustomization,
|
||||||
import { defaultAvatarCustomization } from "@/lib/avatar-customization-storage";
|
getAvatarCustomization,
|
||||||
import { DEFAULT_GRAY_COLOR } from "@/lib/character/presets";
|
type AvatarCustomization,
|
||||||
|
} from "@/lib/avatar-customization-storage";
|
||||||
import { getRoomBackground, normalizeRoomBackgroundId } from "@/lib/room-backgrounds";
|
import { getRoomBackground, normalizeRoomBackgroundId } from "@/lib/room-backgrounds";
|
||||||
import {
|
import {
|
||||||
equipShopItem,
|
equipShopItem,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
type ShopLayerId,
|
type ShopLayerId,
|
||||||
} from "@/lib/shop-catalog";
|
} from "@/lib/shop-catalog";
|
||||||
import { ShopItemPreviewModal } from "@/components/shop/shop-item-preview-modal";
|
import { ShopItemPreviewModal } from "@/components/shop/shop-item-preview-modal";
|
||||||
|
import { getShopItemThumbnailColors } from "@/lib/shop-preview";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function ShopItemPriceBadge({
|
function ShopItemPriceBadge({
|
||||||
@@ -80,6 +82,7 @@ function ShopItemCard({
|
|||||||
equipped,
|
equipped,
|
||||||
canAfford,
|
canAfford,
|
||||||
isPending,
|
isPending,
|
||||||
|
characterColors,
|
||||||
onPurchase,
|
onPurchase,
|
||||||
onEquipToggle,
|
onEquipToggle,
|
||||||
onPreview,
|
onPreview,
|
||||||
@@ -89,11 +92,16 @@ function ShopItemCard({
|
|||||||
equipped: boolean;
|
equipped: boolean;
|
||||||
canAfford: boolean;
|
canAfford: boolean;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
|
characterColors: AvatarCustomization["colors"];
|
||||||
onPurchase: () => void;
|
onPurchase: () => void;
|
||||||
onEquipToggle: () => void;
|
onEquipToggle: () => void;
|
||||||
onPreview: () => void;
|
onPreview: () => void;
|
||||||
}) {
|
}) {
|
||||||
const room = item.type === "room" ? getRoomBackground(item.id) : null;
|
const room = item.type === "room" ? getRoomBackground(item.id) : null;
|
||||||
|
const thumbnailColors =
|
||||||
|
item.type === "room"
|
||||||
|
? null
|
||||||
|
: getShopItemThumbnailColors(item, characterColors);
|
||||||
|
|
||||||
const actionLabel = (() => {
|
const actionLabel = (() => {
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
@@ -123,13 +131,14 @@ function ShopItemCard({
|
|||||||
alt=""
|
alt=""
|
||||||
className="size-full object-cover image-pixelated"
|
className="size-full object-cover image-pixelated"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : thumbnailColors ? (
|
||||||
<TintedSpriteIcon
|
<TintedSpriteIcon
|
||||||
src={item.image_path}
|
src={item.image_path}
|
||||||
color={DEFAULT_GRAY_COLOR.hsl}
|
color={thumbnailColors.color}
|
||||||
|
skinColor={thumbnailColors.skinColor}
|
||||||
size={32}
|
size={32}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 items-start justify-between gap-2">
|
<div className="flex min-w-0 flex-1 items-start justify-between gap-2">
|
||||||
@@ -355,6 +364,7 @@ export function ShopContent() {
|
|||||||
equipped={equippedRoomBackground === item.id}
|
equipped={equippedRoomBackground === item.id}
|
||||||
canAfford={coins !== null && coins >= item.price}
|
canAfford={coins !== null && coins >= item.price}
|
||||||
isPending={pendingItemId === item.id}
|
isPending={pendingItemId === item.id}
|
||||||
|
characterColors={baseCustomization.colors}
|
||||||
onPurchase={() => void handlePurchase(item.id)}
|
onPurchase={() => void handlePurchase(item.id)}
|
||||||
onEquipToggle={() => void handleEquipToggle(item)}
|
onEquipToggle={() => void handleEquipToggle(item)}
|
||||||
onPreview={() => setPreviewItem(item)}
|
onPreview={() => setPreviewItem(item)}
|
||||||
@@ -378,6 +388,7 @@ export function ShopContent() {
|
|||||||
equipped={equippedItems.includes(item.id)}
|
equipped={equippedItems.includes(item.id)}
|
||||||
canAfford={coins !== null && coins >= item.price}
|
canAfford={coins !== null && coins >= item.price}
|
||||||
isPending={pendingItemId === item.id}
|
isPending={pendingItemId === item.id}
|
||||||
|
characterColors={baseCustomization.colors}
|
||||||
onPurchase={() => void handlePurchase(item.id)}
|
onPurchase={() => void handlePurchase(item.id)}
|
||||||
onEquipToggle={() => void handleEquipToggle(item)}
|
onEquipToggle={() => void handleEquipToggle(item)}
|
||||||
onPreview={() => setPreviewItem(item)}
|
onPreview={() => setPreviewItem(item)}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
|
import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
|
||||||
import { parseVariantId, parseShopStyleItem, type ShopItemRecord } from "@/lib/shop-catalog";
|
import type { HSL } from "@/lib/character/color-utils";
|
||||||
|
import type { LayerColorState } from "@/lib/character/types";
|
||||||
|
import {
|
||||||
|
isShopLayerId,
|
||||||
|
parseVariantId,
|
||||||
|
parseShopStyleItem,
|
||||||
|
type ShopItemRecord,
|
||||||
|
} from "@/lib/shop-catalog";
|
||||||
import {
|
import {
|
||||||
normalizeRoomBackgroundId,
|
normalizeRoomBackgroundId,
|
||||||
type RoomBackgroundId,
|
type RoomBackgroundId,
|
||||||
@@ -17,6 +24,21 @@ export function getShopPreviewRoomId(
|
|||||||
return normalizeRoomBackgroundId(base.roomBackground);
|
return normalizeRoomBackgroundId(base.roomBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Tint colors for shop grid thumbnails — match the user's current layer colors. */
|
||||||
|
export function getShopItemThumbnailColors(
|
||||||
|
item: ShopItemRecord,
|
||||||
|
colors: LayerColorState,
|
||||||
|
): { color: HSL; skinColor?: HSL } {
|
||||||
|
if (!isShopLayerId(item.type)) {
|
||||||
|
return { color: colors.torso };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: colors[item.type],
|
||||||
|
skinColor: item.type === "eyes" ? colors.skin : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Build a non-persisted avatar state showing how a shop item would look. */
|
/** Build a non-persisted avatar state showing how a shop item would look. */
|
||||||
export function buildShopItemPreviewCustomization(
|
export function buildShopItemPreviewCustomization(
|
||||||
base: AvatarCustomization,
|
base: AvatarCustomization,
|
||||||
|
|||||||
Reference in New Issue
Block a user