diff --git a/src/shared/character-images/README.md b/src/shared/character-images/README.md new file mode 100644 index 0000000..e890076 --- /dev/null +++ b/src/shared/character-images/README.md @@ -0,0 +1,21 @@ +# Character image catalog + +Put custom character images here using this structure: + +```text +src/shared/character-images/ + street-fighter-6/ + ryu.png + ken.png + tekken-8/ + jin.png +``` + +Rules: + +- Folder name = game slug (`toLowerCase`, replace non alphanumeric with `-`). + - Example: `Guilty Gear -Strive-` -> `guilty-gear-strive` +- File name = character slug with the same rule. + - Example: `Chun-Li` -> `chun-li` +- Supported extensions: `.png`, `.jpg`, `.jpeg`, `.webp`, `.avif`, `.svg`. +- If an image is missing, the dashboard shows a generated placeholder preview. diff --git a/src/shared/fighting-characters.ts b/src/shared/fighting-characters.ts index e6666f6..2eebfdb 100644 --- a/src/shared/fighting-characters.ts +++ b/src/shared/fighting-characters.ts @@ -24,11 +24,11 @@ const paletteByGame: Record = { '2XKO': ['#22d3ee', '#0f766e'], }; -const toCharacterValue = (character: string) => character.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); +const toSlug = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); const toDataUrl = (svg: string) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; -const buildCharacterImage = (game: string, character: string) => { +const buildCharacterPlaceholder = (game: string, character: string) => { const [startColor, endColor] = paletteByGame[game] ?? ['#334155', '#0f172a']; const initials = character .split(/\s+/) @@ -55,14 +55,42 @@ const buildCharacterImage = (game: string, character: string) => { return toDataUrl(svg.trim()); }; +const characterImageModules = import.meta.glob('/src/shared/character-images/**/*.{png,jpg,jpeg,webp,avif,svg}', { + eager: true, + import: 'default', + query: '?url', +}) as Record; + +const characterImageByKey = Object.entries(characterImageModules).reduce>((acc, [path, url]) => { + const segments = path.split('/'); + const gameFolder = segments.at(-2); + const filename = segments.at(-1); + if (!gameFolder || !filename) { + return acc; + } + + const characterSlug = filename.replace(/\.[^.]+$/, ''); + acc[`${gameFolder}/${characterSlug}`] = url; + return acc; +}, {}); + +const getCharacterImage = (game: string, character: string, characterValue: string) => { + const gameSlug = toSlug(game); + const key = `${gameSlug}/${characterValue}`; + return characterImageByKey[key] ?? buildCharacterPlaceholder(game, character); +}; + export const fightingCharactersByGame: Record = Object.fromEntries( Object.entries(characterNamesByGame).map(([game, characterNames]) => [ game, - characterNames.map((character) => ({ - label: character, - value: toCharacterValue(character), - image: buildCharacterImage(game, character), - })), + characterNames.map((character) => { + const value = toSlug(character); + return { + label: character, + value, + image: getCharacterImage(game, character, value), + }; + }), ]), );