From 7c6086256de178b65a06b53ee8ad7c0768d8d4b3 Mon Sep 17 00:00:00 2001 From: Pandipipas <62224708+Pandipipas@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:29:39 +0100 Subject: [PATCH] refactor character asset mapping and replicant sync persistence --- src/dashboard/example/stores/store-sync.ts | 16 +++++++----- src/shared/fighting-characters.ts | 30 ++++++++++++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/dashboard/example/stores/store-sync.ts b/src/dashboard/example/stores/store-sync.ts index a5842f0..90c5bc4 100644 --- a/src/dashboard/example/stores/store-sync.ts +++ b/src/dashboard/example/stores/store-sync.ts @@ -43,6 +43,13 @@ export const syncStateWithReplicant = ( storageKey?: string, ): void => { const isApplyingReplicant = ref(false); + const persistSnapshot = (value: T): void => { + if (!storageKey) { + return; + } + + writeStorageSnapshot(storageKey, value); + }; watch( () => replicant?.data, @@ -55,9 +62,7 @@ export const syncStateWithReplicant = ( state.value = normalize(value); isApplyingReplicant.value = false; - if (storageKey) { - writeStorageSnapshot(storageKey, state.value); - } + persistSnapshot(state.value); }, { deep: true, immediate: true }, ); @@ -65,14 +70,13 @@ export const syncStateWithReplicant = ( watch( state, (value) => { - if (storageKey) { - writeStorageSnapshot(storageKey, value); - } + persistSnapshot(value); if (isApplyingReplicant.value || !replicant) { return; } + // Replicants remain the source of truth for server/browser synchronization. replicant.data = normalize(value); replicant.save(); }, diff --git a/src/shared/fighting-characters.ts b/src/shared/fighting-characters.ts index a172e26..3d8236c 100644 --- a/src/shared/fighting-characters.ts +++ b/src/shared/fighting-characters.ts @@ -4,16 +4,19 @@ export interface FightingCharacterOption { image: string; } +type GamePalette = readonly [startColor: string, endColor: string]; + +const DEFAULT_PLACEHOLDER_PALETTE: GamePalette = ['#334155', '#0f172a']; +const MAX_INITIALS = 2; + const characterNamesByGame: Record = { 'Street Fighter 6': ['A.K.I.', 'Akuma', 'Alex', 'Bison', 'Blanka', 'Cammy', 'Chun-Li', 'Dee Jay', 'Dhalsim', 'E. Honda', 'Ed', 'Elena', 'Guile', 'Jamie', 'JP', 'Juri', 'Ken', 'Kimberly', 'Lily', 'Luke', 'Mai', 'Manon', 'Marisa', 'Rashid', 'Ryu', 'Sagat', 'Terry', 'Viper', 'Zangief'], 'TEKKEN 8': ['Alisa', 'Anna', 'Armor King', 'Asuka', 'Azucena', 'Bob', 'Bryan', 'Claudio', 'Clive', 'Devil Jin', 'Dragunov', 'Eddy', 'Fahkumram', 'Feng', 'Heihachi', 'Hwoarang', 'Jack-8', 'Jin', 'Jun', 'Kazuya', 'King', 'Kuma', 'Kunimitsu', 'Lars', 'Law', 'Lee', 'Leo', 'Leroy', 'Lidia', 'Lili', 'Miary Zo', 'Nina', 'Panda', 'Paul', 'Raven', 'Reina', 'Roger Jr', 'Shaheen', 'Steve', 'Victor', 'Xiaoyu', 'Yoshimitsu', 'Zafina'], -// '2XKO': ['Ahri', 'Akali', 'Blitzcrank', 'Braum', 'Caitlyn', 'Darius', 'Ekko', 'Illaoi', 'Jinx', 'Senna', 'Teemo', 'Vi', 'Warwick', 'Yasuo'], }; -const paletteByGame: Record = { +const paletteByGame: Record = { 'Street Fighter 6': ['#f97316', '#b91c1c'], 'TEKKEN 8': ['#2563eb', '#111827'], -// '2XKO': ['#22d3ee', '#0f766e'], }; const toSlug = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); @@ -21,12 +24,12 @@ const toSlug = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, '-' const toDataUrl = (svg: string) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; const buildCharacterPlaceholder = (game: string, character: string) => { - const [startColor, endColor] = paletteByGame[game] ?? ['#334155', '#0f172a']; + const [startColor, endColor] = paletteByGame[game] ?? DEFAULT_PLACEHOLDER_PALETTE; const initials = character .split(/\s+/) .map((part) => part[0]) .join('') - .slice(0, 2) + .slice(0, MAX_INITIALS) .toUpperCase(); const svg = ` @@ -53,16 +56,26 @@ const characterImageModules = import.meta.glob('/src/shared/character-images/**/ query: '?url', }) as Record; -const characterImageByKey = Object.entries(characterImageModules).reduce>((acc, [path, url]) => { +const resolveImageKey = (path: string): string | null => { const segments = path.split('/'); const gameFolder = segments.at(-2); const filename = segments.at(-1); + if (!gameFolder || !filename) { - return acc; + return null; } const characterSlug = filename.replace(/\.[^.]+$/, ''); - acc[`${gameFolder}/${characterSlug}`] = url; + return `${gameFolder}/${characterSlug}`; +}; + +const characterImageByKey = Object.entries(characterImageModules).reduce>((acc, [path, url]) => { + const key = resolveImageKey(path); + if (!key) { + return acc; + } + + acc[key] = url; return acc; }, {}); @@ -77,6 +90,7 @@ export const fightingCharactersByGame: Record game, characterNames.map((character) => { const value = toSlug(character); + // Prefer packaged artwork and gracefully fallback to a generated image. return { label: character, value,