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.
236 lines
5.7 KiB
TypeScript
236 lines
5.7 KiB
TypeScript
import {
|
|
clampHsl,
|
|
hexToRgb,
|
|
hslToRgb,
|
|
rgbToHex,
|
|
rgbToHsl,
|
|
type HSL,
|
|
} from "./color-utils";
|
|
import type { LayerVariantState } from "./types";
|
|
|
|
export type ColorPreset = {
|
|
id: string;
|
|
name: string;
|
|
hex: string;
|
|
hsl: HSL;
|
|
};
|
|
|
|
export type LayerVariant = {
|
|
id: string;
|
|
label: string;
|
|
src: string;
|
|
};
|
|
|
|
export const NONE_VARIANT_ID = "none";
|
|
|
|
export const NONE_VARIANT: LayerVariant = {
|
|
id: NONE_VARIANT_ID,
|
|
label: "None",
|
|
src: "",
|
|
};
|
|
|
|
export type CharacterLayerId =
|
|
| "skin"
|
|
| "pants"
|
|
| "shoes"
|
|
| "torso"
|
|
| "eyes"
|
|
| "head";
|
|
|
|
export type CharacterLayer = {
|
|
id: CharacterLayerId;
|
|
label: string;
|
|
allowVariants: boolean;
|
|
variants: LayerVariant[];
|
|
};
|
|
|
|
function presetFromHsl(id: string, name: string, hsl: HSL): ColorPreset {
|
|
const clamped = clampHsl(hsl);
|
|
return {
|
|
id,
|
|
name,
|
|
hsl: clamped,
|
|
hex: rgbToHex(hslToRgb(clamped.h, clamped.s, clamped.l)),
|
|
};
|
|
}
|
|
|
|
function presetFromHex(id: string, name: string, hex: string): ColorPreset {
|
|
const rgb = hexToRgb(hex);
|
|
return presetFromHsl(id, name, rgbToHsl(rgb.r, rgb.g, rgb.b));
|
|
}
|
|
|
|
export const VARIANT_DISPLAY_NAMES: Record<string, string> = {
|
|
"skin-1": "Default Form",
|
|
"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) {
|
|
return VARIANT_DISPLAY_NAMES[variantId] ?? fallbackLabel;
|
|
}
|
|
|
|
function layerVariants(
|
|
layerId: Exclude<CharacterLayerId, "skin">,
|
|
label: string,
|
|
count: number,
|
|
): LayerVariant[] {
|
|
return Array.from({ length: count }, (_, index) => {
|
|
const number = index + 1;
|
|
const id = `${layerId}-${number}`;
|
|
return {
|
|
id,
|
|
label: getVariantDisplayName(id, `${label} ${number}`),
|
|
src: `/character/${layerId}/${layerId}-${number}.png`,
|
|
};
|
|
});
|
|
}
|
|
|
|
function layerVariantsWithNone(
|
|
layerId: Exclude<CharacterLayerId, "skin">,
|
|
label: string,
|
|
count: number,
|
|
): LayerVariant[] {
|
|
return [NONE_VARIANT, ...layerVariants(layerId, label, count)];
|
|
}
|
|
|
|
export const COLOR_PRESETS: ColorPreset[] = [
|
|
presetFromHsl("gray", "Gray", { h: 0, s: 0, l: 50 }),
|
|
presetFromHsl("white", "White", { h: 0, s: 0, l: 100 }),
|
|
presetFromHsl("black", "Black", { h: 0, s: 0, l: 5 }),
|
|
presetFromHex("red", "Red", "#FF2020"),
|
|
presetFromHex("orange", "Orange", "#FF8800"),
|
|
presetFromHex("yellow", "Yellow", "#FFD700"),
|
|
presetFromHex("green", "Green", "#22AA44"),
|
|
presetFromHex("blue", "Blue", "#2266FF"),
|
|
presetFromHex("indigo", "Indigo", "#4B0082"),
|
|
presetFromHex("porcelain", "Porcelain", "#FFDFC4"),
|
|
presetFromHex("fair", "Fair", "#F0C9A5"),
|
|
presetFromHex("light", "Light", "#E0AC69"),
|
|
presetFromHex("medium", "Medium", "#C68642"),
|
|
presetFromHex("tan", "Tan", "#A87149"),
|
|
presetFromHex("brown", "Brown", "#8D5524"),
|
|
presetFromHex("dark", "Dark", "#5C3317"),
|
|
presetFromHex("deep", "Deep", "#3B2212"),
|
|
presetFromHex("onyx", "Onyx", "#140C08"),
|
|
];
|
|
|
|
export const DEFAULT_GRAY_COLOR =
|
|
COLOR_PRESETS.find((preset) => preset.id === "gray")!;
|
|
|
|
export const DEFAULT_SKIN_COLOR = DEFAULT_GRAY_COLOR;
|
|
|
|
export const PIECE_SLOT_COUNT = 8;
|
|
|
|
/** Stack order: bottom → top */
|
|
export const CHARACTER_LAYERS: CharacterLayer[] = [
|
|
{
|
|
id: "skin",
|
|
label: "Skin",
|
|
allowVariants: false,
|
|
variants: [
|
|
{
|
|
id: "skin-1",
|
|
label: getVariantDisplayName("skin-1", "Skin 1"),
|
|
src: "/character/skin/skin-1.png",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "pants",
|
|
label: "Pants",
|
|
allowVariants: true,
|
|
variants: layerVariantsWithNone("pants", "Pants", 3),
|
|
},
|
|
{
|
|
id: "shoes",
|
|
label: "Shoes",
|
|
allowVariants: true,
|
|
variants: layerVariantsWithNone("shoes", "Shoes", 3),
|
|
},
|
|
{
|
|
id: "torso",
|
|
label: "Torso",
|
|
allowVariants: true,
|
|
variants: layerVariantsWithNone("torso", "Torso", 4),
|
|
},
|
|
{
|
|
id: "eyes",
|
|
label: "Eyes",
|
|
allowVariants: true,
|
|
variants: layerVariantsWithNone("eyes", "Eyes", 2),
|
|
},
|
|
{
|
|
id: "head",
|
|
label: "Head",
|
|
allowVariants: true,
|
|
variants: layerVariantsWithNone("head", "Head", 7),
|
|
},
|
|
];
|
|
|
|
export type CharacterCreatorTabId = CharacterLayerId | "room";
|
|
|
|
export const CHARACTER_CUSTOMIZATION_TABS: {
|
|
id: CharacterCreatorTabId;
|
|
label: string;
|
|
}[] = [
|
|
...CHARACTER_LAYERS.map((layer) => ({ id: layer.id, label: layer.label })),
|
|
{ id: "room", label: "Room" },
|
|
];
|
|
|
|
export const LAYER_DEFAULT_COLORS: Record<CharacterLayerId, HSL> = {
|
|
skin: DEFAULT_GRAY_COLOR.hsl,
|
|
pants: DEFAULT_GRAY_COLOR.hsl,
|
|
shoes: DEFAULT_GRAY_COLOR.hsl,
|
|
torso: DEFAULT_GRAY_COLOR.hsl,
|
|
eyes: DEFAULT_GRAY_COLOR.hsl,
|
|
head: DEFAULT_GRAY_COLOR.hsl,
|
|
};
|
|
|
|
export function buildDefaultVariants(): LayerVariantState {
|
|
return CHARACTER_LAYERS.reduce((state, layer) => {
|
|
state[layer.id] = layer.allowVariants
|
|
? NONE_VARIANT_ID
|
|
: (layer.variants[0]?.id ?? "");
|
|
return state;
|
|
}, {} as LayerVariantState);
|
|
}
|
|
|
|
export function getLayerById(layerId: CharacterLayerId): CharacterLayer {
|
|
return CHARACTER_LAYERS.find((layer) => layer.id === layerId)!;
|
|
}
|
|
|
|
export function getSelectedVariant(
|
|
layerId: CharacterLayerId,
|
|
variantId: string,
|
|
): LayerVariant | undefined {
|
|
if (variantId === NONE_VARIANT_ID) return undefined;
|
|
|
|
const variant = getLayerById(layerId).variants.find(
|
|
(entry) => entry.id === variantId,
|
|
);
|
|
|
|
if (!variant?.src) return undefined;
|
|
|
|
return variant;
|
|
}
|
|
|
|
/** @deprecated Use DEFAULT_SKIN_COLOR */
|
|
export const DEFAULT_BASE_COLOR = DEFAULT_SKIN_COLOR;
|