refactor character asset mapping and replicant sync persistence

This commit is contained in:
Pandipipas
2026-02-14 13:29:39 +01:00
parent f4eccf612f
commit 7c6086256d
2 changed files with 32 additions and 14 deletions
+10 -6
View File
@@ -43,6 +43,13 @@ export const syncStateWithReplicant = <T>(
storageKey?: string, storageKey?: string,
): void => { ): void => {
const isApplyingReplicant = ref(false); const isApplyingReplicant = ref(false);
const persistSnapshot = (value: T): void => {
if (!storageKey) {
return;
}
writeStorageSnapshot(storageKey, value);
};
watch( watch(
() => replicant?.data, () => replicant?.data,
@@ -55,9 +62,7 @@ export const syncStateWithReplicant = <T>(
state.value = normalize(value); state.value = normalize(value);
isApplyingReplicant.value = false; isApplyingReplicant.value = false;
if (storageKey) { persistSnapshot(state.value);
writeStorageSnapshot(storageKey, state.value);
}
}, },
{ deep: true, immediate: true }, { deep: true, immediate: true },
); );
@@ -65,14 +70,13 @@ export const syncStateWithReplicant = <T>(
watch( watch(
state, state,
(value) => { (value) => {
if (storageKey) { persistSnapshot(value);
writeStorageSnapshot(storageKey, value);
}
if (isApplyingReplicant.value || !replicant) { if (isApplyingReplicant.value || !replicant) {
return; return;
} }
// Replicants remain the source of truth for server/browser synchronization.
replicant.data = normalize(value); replicant.data = normalize(value);
replicant.save(); replicant.save();
}, },
+22 -8
View File
@@ -4,16 +4,19 @@ export interface FightingCharacterOption {
image: string; image: string;
} }
type GamePalette = readonly [startColor: string, endColor: string];
const DEFAULT_PLACEHOLDER_PALETTE: GamePalette = ['#334155', '#0f172a'];
const MAX_INITIALS = 2;
const characterNamesByGame: Record<string, string[]> = { const characterNamesByGame: Record<string, string[]> = {
'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'], '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'], '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<string, [string, string]> = { const paletteByGame: Record<string, GamePalette> = {
'Street Fighter 6': ['#f97316', '#b91c1c'], 'Street Fighter 6': ['#f97316', '#b91c1c'],
'TEKKEN 8': ['#2563eb', '#111827'], 'TEKKEN 8': ['#2563eb', '#111827'],
// '2XKO': ['#22d3ee', '#0f766e'],
}; };
const toSlug = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); 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 toDataUrl = (svg: string) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
const buildCharacterPlaceholder = (game: string, character: string) => { const buildCharacterPlaceholder = (game: string, character: string) => {
const [startColor, endColor] = paletteByGame[game] ?? ['#334155', '#0f172a']; const [startColor, endColor] = paletteByGame[game] ?? DEFAULT_PLACEHOLDER_PALETTE;
const initials = character const initials = character
.split(/\s+/) .split(/\s+/)
.map((part) => part[0]) .map((part) => part[0])
.join('') .join('')
.slice(0, 2) .slice(0, MAX_INITIALS)
.toUpperCase(); .toUpperCase();
const svg = ` const svg = `
@@ -53,16 +56,26 @@ const characterImageModules = import.meta.glob('/src/shared/character-images/**/
query: '?url', query: '?url',
}) as Record<string, string>; }) as Record<string, string>;
const characterImageByKey = Object.entries(characterImageModules).reduce<Record<string, string>>((acc, [path, url]) => { const resolveImageKey = (path: string): string | null => {
const segments = path.split('/'); const segments = path.split('/');
const gameFolder = segments.at(-2); const gameFolder = segments.at(-2);
const filename = segments.at(-1); const filename = segments.at(-1);
if (!gameFolder || !filename) { if (!gameFolder || !filename) {
return acc; return null;
} }
const characterSlug = filename.replace(/\.[^.]+$/, ''); const characterSlug = filename.replace(/\.[^.]+$/, '');
acc[`${gameFolder}/${characterSlug}`] = url; return `${gameFolder}/${characterSlug}`;
};
const characterImageByKey = Object.entries(characterImageModules).reduce<Record<string, string>>((acc, [path, url]) => {
const key = resolveImageKey(path);
if (!key) {
return acc;
}
acc[key] = url;
return acc; return acc;
}, {}); }, {});
@@ -77,6 +90,7 @@ export const fightingCharactersByGame: Record<string, FightingCharacterOption[]>
game, game,
characterNames.map((character) => { characterNames.map((character) => {
const value = toSlug(character); const value = toSlug(character);
// Prefer packaged artwork and gracefully fallback to a generated image.
return { return {
label: character, label: character,
value, value,