Support custom character image folders and document structure

This commit is contained in:
Pandipipas
2026-02-12 02:02:07 +01:00
parent f38d15c681
commit de11a1dea7
2 changed files with 56 additions and 7 deletions
+21
View File
@@ -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.
+34 -6
View File
@@ -24,11 +24,11 @@ const paletteByGame: Record<string, [string, string]> = {
'2XKO': ['#22d3ee', '#0f766e'], '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 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 [startColor, endColor] = paletteByGame[game] ?? ['#334155', '#0f172a'];
const initials = character const initials = character
.split(/\s+/) .split(/\s+/)
@@ -55,14 +55,42 @@ const buildCharacterImage = (game: string, character: string) => {
return toDataUrl(svg.trim()); 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<string, string>;
const characterImageByKey = Object.entries(characterImageModules).reduce<Record<string, string>>((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<string, FightingCharacterOption[]> = Object.fromEntries( export const fightingCharactersByGame: Record<string, FightingCharacterOption[]> = Object.fromEntries(
Object.entries(characterNamesByGame).map(([game, characterNames]) => [ Object.entries(characterNamesByGame).map(([game, characterNames]) => [
game, game,
characterNames.map((character) => ({ characterNames.map((character) => {
const value = toSlug(character);
return {
label: character, label: character,
value: toCharacterValue(character), value,
image: buildCharacterImage(game, character), image: getCharacterImage(game, character, value),
})), };
}),
]), ]),
); );