Files
HaBit/lib/shop-catalog.ts
Harkamal Randhawa e41fc10c77 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.
2026-05-24 10:10:08 -06:00

116 lines
2.8 KiB
TypeScript

import { isShopOnlyStyleVariant } from "@/lib/character/variant-access";
import type { CharacterLayerId } from "@/lib/character/presets";
import {
CHARACTER_LAYERS,
getVariantDisplayName,
NONE_VARIANT_ID,
} from "@/lib/character/presets";
import { getRoomBackground, getShopRoomBackgrounds } from "@/lib/room-backgrounds";
export type ShopLayerId = Exclude<CharacterLayerId, "skin">;
export type ShopItemType = ShopLayerId | "room";
export type ShopItemRecord = {
id: string;
name: string;
type: ShopItemType;
price: number;
image_path: string;
};
export type ShopStyleItem = {
layerId: ShopLayerId;
variantId: string;
};
const SHOP_LAYER_IDS = new Set<ShopLayerId>([
"pants",
"shoes",
"torso",
"eyes",
"head",
]);
export function isShopLayerId(value: string): value is ShopLayerId {
return SHOP_LAYER_IDS.has(value as ShopLayerId);
}
/** Parse a character variant id (e.g. head-1) into layer + variant. */
export function parseVariantId(variantId: string): ShopStyleItem | null {
for (const layerId of SHOP_LAYER_IDS) {
if (variantId.startsWith(`${layerId}-`)) {
return { layerId, variantId };
}
}
return null;
}
export function parseShopStyleItem(
itemId: string,
itemType: string,
): ShopStyleItem | null {
if (!isShopLayerId(itemType) || !itemId.startsWith(`${itemType}-`)) {
return null;
}
return { layerId: itemType, variantId: itemId };
}
export function getShopCatalogFromPresets(): ShopItemRecord[] {
const styleItems = CHARACTER_LAYERS.flatMap((layer) => {
if (layer.id === "skin") {
return [];
}
return layer.variants
.filter(
(variant) =>
variant.id !== NONE_VARIANT_ID && isShopOnlyStyleVariant(variant.id),
)
.map((variant) => ({
id: variant.id,
name: variant.label,
type: layer.id as ShopLayerId,
price: getDefaultPriceForLayer(layer.id as ShopLayerId, variant.id),
image_path: variant.src,
}));
});
return [...styleItems, ...getShopRoomBackgrounds()];
}
function getDefaultPriceForLayer(layerId: ShopLayerId, variantId: string) {
const number = Number.parseInt(variantId.split("-").pop() ?? "1", 10);
const baseByLayer: Record<ShopLayerId, number> = {
pants: 20,
shoes: 20,
torso: 25,
eyes: 15,
head: 30,
};
return baseByLayer[layerId] + (number - 1) * 5;
}
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,
): ShopItemRecord | undefined {
return items.find((item) => item.id === itemId);
}