mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
Merge pull request #146 from Pandipipas/add-character-names-download-with-assets
Descargar y usar listas de personajes por juego desde los assets
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch, watchEffect, type Ref } from 'vue';
|
||||
import { getCountryLabel, getCountryOptions } from '../../../shared/countries';
|
||||
import { getCharactersByGame, getDefaultCharactersByGame } from '../../../shared/fighting-characters';
|
||||
import { buildCharactersByGame, getDefaultCharactersByGame } from '../../../shared/fighting-characters';
|
||||
import type { Schemas } from '../../../types';
|
||||
import { usePlayersStore } from '../stores/players';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
@@ -40,8 +40,11 @@ const allFightingGameOptions = computed(() => gameAssetsStore.installedGames.map
|
||||
|
||||
const fightingGameOptions = ref(allFightingGameOptions.value);
|
||||
|
||||
const characterOptions = computed(() => getCharactersByGame(scoreboardStore.scoreboard.game));
|
||||
type CharacterOption = ReturnType<typeof getCharactersByGame>[number];
|
||||
const characterOptions = computed(() => buildCharactersByGame(
|
||||
scoreboardStore.scoreboard.game,
|
||||
gameAssetsStore.characterNamesByGame[scoreboardStore.scoreboard.game] ?? [],
|
||||
));
|
||||
type CharacterOption = ReturnType<typeof buildCharactersByGame>[number];
|
||||
const leftCharacterOptions = ref<CharacterOption[]>([]);
|
||||
const rightCharacterOptions = ref<CharacterOption[]>([]);
|
||||
|
||||
@@ -720,8 +723,14 @@ watch(
|
||||
);
|
||||
|
||||
watch(
|
||||
() => scoreboardStore.scoreboard.game,
|
||||
(newGame, previousGame) => {
|
||||
() => [scoreboardStore.scoreboard.game, characterOptions.value] as const,
|
||||
([newGame, options], previousState) => {
|
||||
const previousGame = previousState?.[0];
|
||||
|
||||
if (newGame && gameAssetsStore.characterNamesByGame[newGame] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousGame) {
|
||||
charactersByGame.value[previousGame] = {
|
||||
leftCharacter: scoreboardStore.scoreboard.leftCharacter,
|
||||
@@ -729,7 +738,6 @@ watch(
|
||||
};
|
||||
}
|
||||
|
||||
const options = getCharactersByGame(scoreboardStore.scoreboard.game);
|
||||
leftCharacterOptions.value = options;
|
||||
rightCharacterOptions.value = options;
|
||||
const allowed = new Set(options.map((option) => option.value));
|
||||
|
||||
@@ -24,6 +24,7 @@ let progressListenerAttached = false;
|
||||
|
||||
export const useGameAssetsStore = defineStore('game-assets', () => {
|
||||
const installedGames = ref<string[]>([]);
|
||||
const characterNamesByGame = ref<Record<string, string[]>>({});
|
||||
const loadingByTitle = ref<Record<string, boolean>>({});
|
||||
const removingByTitle = ref<Record<string, boolean>>({});
|
||||
const progressByTitle = ref<Record<string, number>>({});
|
||||
@@ -53,9 +54,16 @@ export const useGameAssetsStore = defineStore('game-assets', () => {
|
||||
progressListenerAttached = true;
|
||||
}
|
||||
|
||||
const refreshCharacterNamesByGame = async () => {
|
||||
const response = await sendNodecgMessage<Record<string, string[]>>('scoreko-assets:listCharactersByGame');
|
||||
characterNamesByGame.value = response;
|
||||
return characterNamesByGame.value;
|
||||
};
|
||||
|
||||
const refreshInstalledGames = async () => {
|
||||
const response = await sendNodecgMessage<string[]>('scoreko-assets:listInstalled');
|
||||
installedGames.value = Array.isArray(response) ? response : [];
|
||||
await refreshCharacterNamesByGame();
|
||||
return installedGames.value;
|
||||
};
|
||||
|
||||
@@ -72,6 +80,7 @@ export const useGameAssetsStore = defineStore('game-assets', () => {
|
||||
try {
|
||||
const response = await sendNodecgMessage<{ installedGames: string[] }>('scoreko-assets:downloadGame', { title });
|
||||
installedGames.value = response.installedGames;
|
||||
await refreshCharacterNamesByGame();
|
||||
loadingByTitle.value = {
|
||||
...loadingByTitle.value,
|
||||
[title]: false,
|
||||
@@ -99,6 +108,7 @@ export const useGameAssetsStore = defineStore('game-assets', () => {
|
||||
try {
|
||||
const response = await sendNodecgMessage<{ installedGames: string[] }>('scoreko-assets:removeGame', { title });
|
||||
installedGames.value = response.installedGames;
|
||||
await refreshCharacterNamesByGame();
|
||||
return response;
|
||||
} finally {
|
||||
removingByTitle.value = {
|
||||
@@ -110,10 +120,12 @@ export const useGameAssetsStore = defineStore('game-assets', () => {
|
||||
|
||||
return {
|
||||
installedGames,
|
||||
characterNamesByGame,
|
||||
loadingByTitle,
|
||||
removingByTitle,
|
||||
progressByTitle,
|
||||
refreshInstalledGames,
|
||||
refreshCharacterNamesByGame,
|
||||
downloadGame,
|
||||
removeGame,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { nodecg } from './util/nodecg.js';
|
||||
const GITHUB_OWNER = 'Pandipipas';
|
||||
const GITHUB_REPO = 'scoreko-assets';
|
||||
const GITHUB_API_BASE = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/contents`;
|
||||
const CHARACTER_NAMES_FILE = 'fighting-characters.json';
|
||||
|
||||
let cachedDefaultBranch: string | null = null;
|
||||
|
||||
@@ -122,6 +123,37 @@ const listInstalledGames = async () => {
|
||||
return gameCatalog.filter((game) => installedSlugs.includes(game.slug)).map((game) => game.title);
|
||||
};
|
||||
|
||||
const parseCharacterNames = (content: string, gameTitle: string) => {
|
||||
const parsed = JSON.parse(content) as unknown;
|
||||
const names = Array.isArray(parsed)
|
||||
? parsed
|
||||
: typeof parsed === 'object' && parsed !== null && Array.isArray((parsed as { characters?: unknown }).characters)
|
||||
? (parsed as { characters: unknown[] }).characters
|
||||
: null;
|
||||
|
||||
if (!names || names.some((name) => typeof name !== 'string')) {
|
||||
throw new Error(`El archivo ${CHARACTER_NAMES_FILE} de ${gameTitle} no tiene un formato válido.`);
|
||||
}
|
||||
|
||||
return names;
|
||||
};
|
||||
|
||||
const listInstalledCharacterNamesByGame = async () => {
|
||||
const charactersByGame = await Promise.all(gameCatalog.map(async (game) => {
|
||||
const sourcePath = path.join(assetsRoot, game.slug, CHARACTER_NAMES_FILE);
|
||||
|
||||
try {
|
||||
const fileContent = await readFile(sourcePath, 'utf8');
|
||||
const names = parseCharacterNames(fileContent, game.title);
|
||||
return [game.title, names] as const;
|
||||
} catch {
|
||||
return [game.title, []] as const;
|
||||
}
|
||||
}));
|
||||
|
||||
return Object.fromEntries(charactersByGame) as Record<string, string[]>;
|
||||
};
|
||||
|
||||
const downloadGameAssets = async (gameTitle: string) => {
|
||||
const game = gameCatalog.find((entry) => entry.title === gameTitle);
|
||||
if (!game) {
|
||||
@@ -136,6 +168,11 @@ const downloadGameAssets = async (gameTitle: string) => {
|
||||
throw new Error(`No se encontraron archivos en ${repoFolderPath} dentro de scoreko-assets.`);
|
||||
}
|
||||
|
||||
const hasCharacterNamesFile = files.some((file) => file.path === `${repoFolderPath}/${CHARACTER_NAMES_FILE}`);
|
||||
if (!hasCharacterNamesFile) {
|
||||
throw new Error(`No se encontró ${CHARACTER_NAMES_FILE} en ${repoFolderPath} dentro de scoreko-assets.`);
|
||||
}
|
||||
|
||||
const totalBytes = files.reduce((acc, file) => acc + (file.size || 0), 0);
|
||||
let downloadedBytes = 0;
|
||||
|
||||
@@ -199,6 +236,18 @@ nodecg.listenFor('scoreko-assets:listInstalled', async (_payload: unknown, ack)
|
||||
}
|
||||
});
|
||||
|
||||
nodecg.listenFor('scoreko-assets:listCharactersByGame', async (_payload: unknown, ack) => {
|
||||
if (typeof ack !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ack(null, await listInstalledCharacterNamesByGame());
|
||||
} catch (error) {
|
||||
ack((error as Error).message);
|
||||
}
|
||||
});
|
||||
|
||||
nodecg.listenFor('scoreko-assets:downloadGame', async (payload: unknown, ack) => {
|
||||
if (typeof ack !== 'function') {
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useHead } from '@unhead/vue';
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { graphicsSettingsReplicant, playersReplicant, scoreboardReplicant } from '../../browser_shared/replicants';
|
||||
import { resolveCountryCode } from '../../shared/countries';
|
||||
import { getCharactersByGame } from '../../shared/fighting-characters';
|
||||
import { getCharacterAssetUrl } from '../../shared/fighting-characters';
|
||||
import type { Schemas } from '../../types';
|
||||
|
||||
useHead({ title: 'Scoreboard 2XKO' });
|
||||
@@ -35,9 +35,12 @@ const rightName = computed(() => scoreboard.value.rightNameOverride || players.v
|
||||
const leftTeam = computed(() => scoreboard.value.leftTeamOverride);
|
||||
const rightTeam = computed(() => scoreboard.value.rightTeamOverride);
|
||||
|
||||
const charMap = new Map(getCharactersByGame('2XKO').map((char) => [char.value, char.image]));
|
||||
const leftCharacterImage = computed(() => charMap.get(scoreboard.value.leftCharacter) ?? '');
|
||||
const rightCharacterImage = computed(() => charMap.get(scoreboard.value.rightCharacter) ?? '');
|
||||
const leftCharacterImage = computed(() => scoreboard.value.leftCharacter
|
||||
? getCharacterAssetUrl('2XKO', scoreboard.value.leftCharacter)
|
||||
: '');
|
||||
const rightCharacterImage = computed(() => scoreboard.value.rightCharacter
|
||||
? getCharacterAssetUrl('2XKO', scoreboard.value.rightCharacter)
|
||||
: '');
|
||||
|
||||
const flagModules = import.meta.glob('/node_modules/flag-icons/flags/4x3/*.svg', { import: 'default', query: '?url' }) as Record<string, () => Promise<string>>;
|
||||
const flagUrlCache: Record<string, string> = {};
|
||||
|
||||
@@ -11,224 +11,6 @@ type GamePalette = readonly [startColor: string, endColor: string];
|
||||
const DEFAULT_PLACEHOLDER_PALETTE: GamePalette = ['#334155', '#0f172a'];
|
||||
const MAX_INITIALS = 2;
|
||||
|
||||
const characterNamesByGame: Record<string, string[]> = {
|
||||
'Guilty Gear -Strive-': [
|
||||
'A.B.A',
|
||||
'Anji Mito',
|
||||
'Asuka R. Kreutz',
|
||||
'Axl Low',
|
||||
'Baiken',
|
||||
'Bedman?',
|
||||
'Bridget',
|
||||
'Chipp Zanuff',
|
||||
'Dizzy',
|
||||
'Elphelt Valentine',
|
||||
'Faust',
|
||||
'Giovanna',
|
||||
'Goldlewis Dickinson',
|
||||
'Happy Chaos',
|
||||
'I-No',
|
||||
'Jack-O',
|
||||
'Johnny',
|
||||
'Ky Kiske',
|
||||
'Leo Whitefang',
|
||||
'Lucy',
|
||||
'May',
|
||||
'Millia Rage',
|
||||
'Nagoriyuki',
|
||||
'Potemkin',
|
||||
'Ramlethal Valentine',
|
||||
'Sin Kiske',
|
||||
'Slayer',
|
||||
'Sol Badguy',
|
||||
'Testament',
|
||||
'Unika',
|
||||
'Venom',
|
||||
'Zato-1',
|
||||
],
|
||||
'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',
|
||||
'Braum',
|
||||
'Caitlyn',
|
||||
'Darius',
|
||||
'Ekko',
|
||||
'Illaoi',
|
||||
'Jinx',
|
||||
'Senna',
|
||||
'Teemo',
|
||||
'Vi',
|
||||
'Warwick',
|
||||
'Yasuo',
|
||||
],
|
||||
'Mortal Kombat 1': [
|
||||
'Ashrah',
|
||||
'Baraka',
|
||||
'Conan the Barbarian',
|
||||
'Cyrax',
|
||||
'Ermac',
|
||||
'Geras',
|
||||
'Ghostface',
|
||||
'Havik',
|
||||
'Homelander',
|
||||
'Johnny Cage',
|
||||
'Kenshi',
|
||||
'Kitana',
|
||||
'Kung Lao',
|
||||
'Li Mei',
|
||||
'Liu Kang',
|
||||
'Mileena',
|
||||
'Nitara',
|
||||
'Noob Saibot',
|
||||
'Omni-Man',
|
||||
'Peacemaker',
|
||||
'Quan Chi',
|
||||
'Raiden',
|
||||
'Rain',
|
||||
'Reiko',
|
||||
'Reptile',
|
||||
'Scorpion',
|
||||
'Sektor',
|
||||
'Shang Tsung',
|
||||
'Sindel',
|
||||
'Smoke',
|
||||
'Sub-Zero',
|
||||
'Takeda',
|
||||
'Tanya',
|
||||
'T-1000',
|
||||
],
|
||||
'THE KING OF FIGHTERS XV': [
|
||||
'Angel',
|
||||
'Antonov',
|
||||
'Ash Crimson',
|
||||
'Athena Asamiya',
|
||||
'Benimaru Nikaido',
|
||||
'Billy Kane',
|
||||
'Blue Mary',
|
||||
'Chizuru Kagura',
|
||||
'Chris',
|
||||
'Clark Still',
|
||||
'Dolores',
|
||||
'Duo Lon',
|
||||
'Elisabeth Blanctorche',
|
||||
'Gato',
|
||||
'Geese Howard',
|
||||
'Goenitz',
|
||||
'Heidern',
|
||||
'Hinako Shijo',
|
||||
'Iori Yagami',
|
||||
'Isla',
|
||||
'Joe Higashi',
|
||||
"K'",
|
||||
'Kim Kaphwan',
|
||||
'King',
|
||||
'King of Dinosaurs',
|
||||
'Krohnen McDougall',
|
||||
'Kula Diamond',
|
||||
'Kukri',
|
||||
'Kyo Kusanagi',
|
||||
'Leona Heidern',
|
||||
'Luong',
|
||||
'Mai Shiranui',
|
||||
'Maxima',
|
||||
'Meitenkun',
|
||||
'Najd',
|
||||
'Orochi Chris',
|
||||
'Orochi Shermie',
|
||||
'Orochi Yashiro',
|
||||
'Ralf Jones',
|
||||
'Ramón',
|
||||
'Robert Garcia',
|
||||
'Rock Howard',
|
||||
'Ryo Sakazaki',
|
||||
'Ryuji Yamazaki',
|
||||
'Shermie',
|
||||
'Shingo Yabuki',
|
||||
'Sylvie Paula Paula',
|
||||
'Terry Bogard',
|
||||
'Vanessa',
|
||||
'Whip',
|
||||
'Yashiro Nanakase',
|
||||
'Yuri Sakazaki',
|
||||
],
|
||||
};
|
||||
|
||||
const defaultCharacterPairByGame: Record<string, { leftCharacter: string; rightCharacter: string }> = {
|
||||
'Guilty Gear -Strive-': {
|
||||
leftCharacter: 'sol-badguy',
|
||||
@@ -296,11 +78,6 @@ const buildCharacterPlaceholder = (game: string, character: string) => {
|
||||
return toDataUrl(svg.trim());
|
||||
};
|
||||
|
||||
const getCharacterAssetUrl = (game: string, characterValue: string) => {
|
||||
const gameSlug = toSlug(game);
|
||||
return `/bundles/scoreko-dev/game-assets/${gameSlug}/characters/${characterValue}.png`;
|
||||
};
|
||||
|
||||
const characterImageModules = import.meta.glob('/src/shared/character-images/**/*.{png,jpg,jpeg,webp,avif,svg}', {
|
||||
eager: true,
|
||||
import: 'default',
|
||||
@@ -335,25 +112,21 @@ const getBundledCharacterImage = (game: string, characterValue: string) => {
|
||||
return characterImageByKey[`${gameSlug}/${characterValue}`] ?? '';
|
||||
};
|
||||
|
||||
const getCharacterImage = (game: string, character: string, characterValue: string) => getCharacterAssetUrl(game, characterValue);
|
||||
export const getCharacterAssetUrl = (game: string, characterValue: string) => {
|
||||
const gameSlug = toSlug(game);
|
||||
return `/bundles/scoreko-dev/game-assets/${gameSlug}/characters/${characterValue}.png`;
|
||||
};
|
||||
|
||||
export const fightingCharactersByGame: Record<string, FightingCharacterOption[]> = Object.fromEntries(
|
||||
Object.entries(characterNamesByGame).map(([game, characterNames]) => [
|
||||
game,
|
||||
characterNames.map((character) => {
|
||||
const value = toSlug(character);
|
||||
// Prefer packaged artwork and gracefully fallback to a generated image.
|
||||
return {
|
||||
label: character,
|
||||
value,
|
||||
image: getCharacterImage(game, character, value),
|
||||
bundledImage: getBundledCharacterImage(game, value),
|
||||
fallbackImage: buildCharacterPlaceholder(game, character),
|
||||
};
|
||||
}),
|
||||
]),
|
||||
);
|
||||
export const buildCharactersByGame = (game: string, characterNames: string[]) => characterNames.map((character) => {
|
||||
const value = toSlug(character);
|
||||
|
||||
export const getCharactersByGame = (game: string) => fightingCharactersByGame[game] ?? [];
|
||||
return {
|
||||
label: character,
|
||||
value,
|
||||
image: getCharacterAssetUrl(game, value),
|
||||
bundledImage: getBundledCharacterImage(game, value),
|
||||
fallbackImage: buildCharacterPlaceholder(game, character),
|
||||
};
|
||||
});
|
||||
|
||||
export const getDefaultCharactersByGame = (game: string) => defaultCharacterPairByGame[game];
|
||||
|
||||
Reference in New Issue
Block a user