feat: add character images for Guilty Gear Strive and update fighting characters with DLC support

This commit is contained in:
2026-05-20 16:34:37 +02:00
parent fd4201a882
commit 04f2c2037a
41 changed files with 420 additions and 122 deletions
-38
View File
@@ -2,35 +2,6 @@
import { ref } from 'vue'; import { ref } from 'vue';
const loadQuotes = [ const loadQuotes = [
// Misc
'Demanding rollback netcode',
'Disrespecting your plus frames',
'Taking your lunch money',
// Street Fighter
'Parrying your super',
'Fighting like gentlemen',
'Fighting a new rival',
'Keeping it classy',
"Protecting Russia's skies",
'Waking up with Dragon Punch',
'Teching those throws',
'Finding the heart of battle',
'Chucking plasma',
'Executing the Yeah Nah Yeah',
// Guilty Gear
'Counter-hitting Pilebunker',
'Riding the lightning',
'Knowing the smell of the game',
'Dropping the instant kill combo',
'What are you standing up for?!',
'Stealing your soul',
'Channelling your inner gorilla',
'Initiating danger time',
'Dragon Installing',
'Practising dust loops',
// BlazBlue
'Turning the wheel of fate',
'Escaping from crossing fate',
// Tekken // Tekken
"Complaining about Paul's damage", "Complaining about Paul's damage",
'Nerfing Gigas', 'Nerfing Gigas',
@@ -38,15 +9,6 @@ const loadQuotes = [
'Sidestepping your electric', 'Sidestepping your electric',
'Punishing hellsweep with 1,1,2', 'Punishing hellsweep with 1,1,2',
'Emailing Harada', 'Emailing Harada',
// Marvel
'Explaining the DHC glitch',
"When's Mahvel?",
'Thanking god for the machine',
'Setting up shop',
'Getting motivated',
'Activating X-Factor',
// Dragon Ball
'Adding yet another Goku',
]; ];
const randomIndex = Math.floor(Math.random() * loadQuotes.length); const randomIndex = Math.floor(Math.random() * loadQuotes.length);
@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject } from 'vue'; import { computed, inject } from 'vue';
import { useScoreboardStore } from '../stores/scoreboard';
import { usePlayerSide } from '../composables/usePlayerSide';
import { CHARACTER_GAME_KEY } from '../composables/useCharacterGame'; import { CHARACTER_GAME_KEY } from '../composables/useCharacterGame';
import { usePlayerSide } from '../composables/usePlayerSide';
import { t } from '../i18n'; import { t } from '../i18n';
import { useScoreboardStore } from '../stores/scoreboard';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Props // Props
@@ -140,6 +140,19 @@ const sideImageLabel = computed(() => t(isLeft.value ? 'scoreboardLeftImage' : '
<template #prepend> <template #prepend>
<QIcon name="sports_martial_arts" /> <QIcon name="sports_martial_arts" />
</template> </template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel class="scoreboard-preview__character-option">
{{ scope.opt.label }}
<span
v-if="scope.opt.dlc"
class="scoreboard-preview__dlc-badge"
>DLC</span>
</QItemLabel>
</QItemSection>
</QItem>
</template>
</QSelect> </QSelect>
</div> </div>
@@ -372,6 +385,19 @@ const sideImageLabel = computed(() => t(isLeft.value ? 'scoreboardLeftImage' : '
<template #prepend> <template #prepend>
<QIcon name="sports_martial_arts" /> <QIcon name="sports_martial_arts" />
</template> </template>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel class="scoreboard-preview__character-option">
{{ scope.opt.label }}
<span
v-if="scope.opt.dlc"
class="scoreboard-preview__dlc-badge"
>DLC</span>
</QItemLabel>
</QItemSection>
</QItem>
</template>
</QSelect> </QSelect>
</div> </div>
</template> </template>
@@ -481,6 +507,27 @@ const sideImageLabel = computed(() => t(isLeft.value ? 'scoreboardLeftImage' : '
color: rgba(255, 255, 255, 0.92); color: rgba(255, 255, 255, 0.92);
} }
.scoreboard-preview__character-option {
display: flex;
align-items: center;
gap: 6px;
}
.scoreboard-preview__dlc-badge {
display: inline-flex;
align-items: center;
padding: 1px 5px;
border-radius: 3px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.05em;
line-height: 14px;
background: rgba(139, 92, 246, 0.2);
color: #a78bfa;
border: 1px solid rgba(139, 92, 246, 0.45);
flex-shrink: 0;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.scoreboard-preview__image-wrap { .scoreboard-preview__image-wrap {
width: min(100%, 280px); width: min(100%, 280px);
+314 -78
View File
@@ -1,67 +1,87 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { t } from './i18n'; import { t } from './i18n';
import { useScoreboardStore } from './stores/scoreboard'; import { useScoreboardStore } from './stores/scoreboard';
import { isShortcutMatch, useShortcutSettingsStore } from './stores/shortcut-settings'; import { isShortcutMatch, useShortcutSettingsStore } from './stores/shortcut-settings';
const menuItems = computed(() => [ // ── Sidebar collapse ──────────────────────────────────────────────────────────
{ label: t('menuDashboard'), to: '/', icon: 'dashboard' }, const LS_KEY = 'sidebar_collapsed';
{ label: t('menuPlayers'), to: '/players', icon: 'groups' }, const isCollapsed = ref(localStorage.getItem(LS_KEY) === 'true');
{ label: t('menuGraphics'), to: '/graphics', icon: 'collections' }, const drawerWidth = computed(() => (isCollapsed.value ? 60 : 220));
watch(isCollapsed, (val) => localStorage.setItem(LS_KEY, String(val)));
const toggleCollapse = () => { isCollapsed.value = !isCollapsed.value; };
// ── Version ───────────────────────────────────────────────────────────────────
const appVersion = import.meta.env.PACKAGE_VERSION as string | undefined;
// ── Logo ──────────────────────────────────────────────────────────────────────
const logoUrl = new URL('./image.png', import.meta.url).href;
// ── Menu groups ───────────────────────────────────────────────────────────────
const mainItems = computed(() => [
{ label: t('menuDashboard'), to: '/', icon: 'dashboard' },
{ label: t('menuPlayers'), to: '/players', icon: 'groups' },
{ label: t('menuGraphics'), to: '/graphics', icon: 'collections' },
]);
const configItems = computed(() => [
{ label: t('menuSettings'), to: '/settings', icon: 'settings' }, { label: t('menuSettings'), to: '/settings', icon: 'settings' },
{ label: t('menuAbout'), to: '/about', icon: 'info' }, { label: t('menuAbout'), to: '/about', icon: 'info' },
]); ]);
const logoUrl = new URL('./image.png', import.meta.url).href; // ── Online / Offline ──────────────────────────────────────────────────────────
const scoreboardStore = useScoreboardStore(); const isOnline = ref(navigator.onLine);
const checkOnline = async () => {
try {
await fetch('https://www.google.com/favicon.ico', {
method: 'HEAD',
mode: 'no-cors',
cache: 'no-store',
});
isOnline.value = true;
} catch {
isOnline.value = false;
}
};
const onNetworkOnline = () => { isOnline.value = true; };
const onNetworkOffline = () => { isOnline.value = false; };
let pingInterval: ReturnType<typeof setInterval> | null = null;
// ── Keyboard shortcuts ────────────────────────────────────────────────────────
const scoreboardStore = useScoreboardStore();
const shortcutSettingsStore = useShortcutSettingsStore(); const shortcutSettingsStore = useShortcutSettingsStore();
const isEditableTarget = (target: EventTarget | null): boolean => { const isEditableTarget = (target: EventTarget | null): boolean => {
if (!(target instanceof HTMLElement)) { if (!(target instanceof HTMLElement)) return false;
return false; return (
} target.isContentEditable ||
['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName) ||
return target.isContentEditable Boolean(target.closest('[contenteditable="true"]'))
|| ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName) );
|| Boolean(target.closest('[contenteditable="true"]'));
}; };
const onShortcutPress = (event: KeyboardEvent) => { const onShortcutPress = (event: KeyboardEvent) => {
if (isEditableTarget(event.target) || document.body.dataset.shortcutRecording === 'true') { if (isEditableTarget(event.target) || document.body.dataset.shortcutRecording === 'true') return;
return;
}
const { shortcuts } = shortcutSettingsStore; const { shortcuts } = shortcutSettingsStore;
if (isShortcutMatch(event, shortcuts.leftIncrement)) { if (isShortcutMatch(event, shortcuts.leftIncrement)) { scoreboardStore.leftScore += 1; event.preventDefault(); return; }
scoreboardStore.leftScore += 1; if (isShortcutMatch(event, shortcuts.leftDecrement)) { scoreboardStore.leftScore = Math.max(0, scoreboardStore.leftScore - 1); event.preventDefault(); return; }
event.preventDefault(); if (isShortcutMatch(event, shortcuts.rightIncrement)) { scoreboardStore.rightScore += 1; event.preventDefault(); return; }
return; if (isShortcutMatch(event, shortcuts.rightDecrement)) { scoreboardStore.rightScore = Math.max(0, scoreboardStore.rightScore - 1); event.preventDefault(); }
}
if (isShortcutMatch(event, shortcuts.leftDecrement)) {
scoreboardStore.leftScore = Math.max(0, scoreboardStore.leftScore - 1);
event.preventDefault();
return;
}
if (isShortcutMatch(event, shortcuts.rightIncrement)) {
scoreboardStore.rightScore += 1;
event.preventDefault();
return;
}
if (isShortcutMatch(event, shortcuts.rightDecrement)) {
scoreboardStore.rightScore = Math.max(0, scoreboardStore.rightScore - 1);
event.preventDefault();
}
}; };
onMounted(() => { onMounted(() => {
window.addEventListener('keydown', onShortcutPress); window.addEventListener('keydown', onShortcutPress);
window.addEventListener('online', onNetworkOnline);
window.addEventListener('offline', onNetworkOffline);
pingInterval = setInterval(checkOnline, 15_000);
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('keydown', onShortcutPress); window.removeEventListener('keydown', onShortcutPress);
window.removeEventListener('online', onNetworkOnline);
window.removeEventListener('offline', onNetworkOffline);
if (pingInterval) clearInterval(pingInterval);
}); });
</script> </script>
@@ -71,49 +91,117 @@ onUnmounted(() => {
show-if-above show-if-above
side="left" side="left"
bordered bordered
:width="220" :width="drawerWidth"
class="sidebar-drawer" class="sidebar-drawer"
> >
<div class="sidebar-header q-pa-md"> <!-- Header -->
<div class="row items-center no-wrap"> <div class="sidebar-header" :class="{ 'is-collapsed': isCollapsed }">
<img <img :src="logoUrl" alt="Logo" class="sidebar-logo">
:src="logoUrl"
alt="Logo" <Transition name="slide-fade">
class="sidebar-logo" <div v-if="!isCollapsed" class="sidebar-title">
> <span class="title-text">Scoreko-dev</span>
<div class="q-ml-sm"> <span v-if="appVersion" class="title-version">v{{ appVersion }}</span>
<div class="text-subtitle1 text-weight-bold">
Scoreko-dev
</div>
<div class="text-caption">
<span class="by-label">by</span> <a
class="by-link"
href="https://github.com/Pandipipas"
target="_blank"
rel="noopener"
>Pandipipas</a>
</div>
</div> </div>
</div> </Transition>
<!-- Chevron siempre visible, arriba a la derecha -->
<QBtn
flat
round
dense
size="sm"
:icon="isCollapsed ? 'chevron_right' : 'chevron_left'"
class="collapse-btn"
@click="toggleCollapse"
/>
</div> </div>
<QSeparator class="q-mb-sm" />
<QList> <QSeparator />
<!-- Sección MAIN -->
<div class="section-sep" :class="{ 'is-collapsed': isCollapsed }">
<span v-if="!isCollapsed" class="section-label">MAIN</span>
</div>
<QList padding>
<QItem <QItem
v-for="item in menuItems" v-for="item in mainItems"
:key="item.to" :key="item.to"
clickable clickable
:to="item.to" :to="item.to"
exact exact
active-class="sidebar-item-active" active-class="sidebar-item-active"
:class="{ 'nav-item-collapsed': isCollapsed }"
> >
<QItemSection avatar> <QItemSection avatar>
<QIcon :name="item.icon" /> <QIcon :name="item.icon" size="sm" />
</QItemSection> </QItemSection>
<QItemSection> <QItemSection v-if="!isCollapsed">
<QItemLabel>{{ item.label }}</QItemLabel> <QItemLabel>{{ item.label }}</QItemLabel>
</QItemSection> </QItemSection>
<QTooltip
v-if="isCollapsed"
anchor="center right"
self="center left"
:offset="[10, 0]"
>
{{ item.label }}
</QTooltip>
</QItem> </QItem>
</QList> </QList>
<!-- Sección CONFIG -->
<div class="section-sep" :class="{ 'is-collapsed': isCollapsed }">
<span v-if="!isCollapsed" class="section-label">CONFIG</span>
</div>
<QList padding>
<QItem
v-for="item in configItems"
:key="item.to"
clickable
:to="item.to"
exact
active-class="sidebar-item-active"
:class="{ 'nav-item-collapsed': isCollapsed }"
>
<QItemSection avatar>
<QIcon :name="item.icon" size="sm" />
</QItemSection>
<QItemSection v-if="!isCollapsed">
<QItemLabel>{{ item.label }}</QItemLabel>
</QItemSection>
<QTooltip
v-if="isCollapsed"
anchor="center right"
self="center left"
:offset="[10, 0]"
>
{{ item.label }}
</QTooltip>
</QItem>
</QList>
<!-- Footer: Online / Offline -->
<div class="sidebar-footer" :class="{ 'is-collapsed': isCollapsed }">
<div class="online-row">
<span class="online-dot" :class="isOnline ? 'dot-online' : 'dot-offline'" />
<Transition name="slide-fade">
<span v-if="!isCollapsed" class="online-label">
{{ isOnline ? 'Online' : 'Offline' }}
</span>
</Transition>
<QTooltip
v-if="isCollapsed"
anchor="center right"
self="center left"
:offset="[10, 0]"
>
{{ isOnline ? 'Online' : 'Offline' }}
</QTooltip>
</div>
</div>
</QDrawer> </QDrawer>
<QPageContainer> <QPageContainer>
@@ -123,28 +211,176 @@ onUnmounted(() => {
</template> </template>
<style scoped> <style scoped>
/* ── Drawer shell ─────────────────────────────────────────────────────────── */
.sidebar-drawer :deep(.q-drawer__content) {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
/* ── Header ───────────────────────────────────────────────────────────────── */
.sidebar-header { .sidebar-header {
min-height: 72px; display: flex;
align-items: center;
gap: 10px;
padding: 14px 12px 14px 14px;
min-height: 64px;
position: relative;
flex-shrink: 0;
transition: padding 0.25s ease;
}
.sidebar-header.is-collapsed {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 4px;
} }
.sidebar-logo { .sidebar-logo {
width: 40px; width: 36px;
height: 40px; height: 36px;
object-fit: contain; object-fit: contain;
flex-shrink: 0;
} }
.by-label { .sidebar-title {
font-size: 0.75rem; display: flex;
flex-direction: column;
overflow: hidden;
flex: 1;
}
.title-text {
font-size: 0.875rem;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title-version {
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
font-size: 0.65rem;
letter-spacing: 0.05em;
opacity: 0.45;
margin-top: 1px;
} }
.by-link { /* ── Collapse button ──────────────────────────────────────────────────────── */
font-size: 0.75rem; .collapse-btn {
color: #f50a64; flex-shrink: 0;
text-decoration: none; opacity: 0.4;
transition: opacity 0.2s ease, transform 0.25s ease;
}
.collapse-btn:hover {
opacity: 1;
}
/* en modo expandido queda al extremo derecho */
.sidebar-header:not(.is-collapsed) .collapse-btn {
margin-left: auto;
} }
.by-link:hover { /* ── Section separators ───────────────────────────────────────────────────── */
text-decoration: underline; .section-sep {
display: flex;
align-items: center;
padding: 10px 14px 2px;
min-height: 28px;
transition: padding 0.2s ease, min-height 0.2s ease;
}
.section-sep.is-collapsed {
padding: 6px 12px 2px;
min-height: 0;
}
.section-sep.is-collapsed::after {
content: '';
display: block;
width: 100%;
height: 1px;
background: currentColor;
opacity: 0.12;
} }
.section-label {
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
opacity: 0.38;
}
/* ── Nav items (collapsed centrado) ──────────────────────────────────────── */
.nav-item-collapsed {
justify-content: center;
padding-left: 0;
padding-right: 0;
}
.nav-item-collapsed :deep(.q-item__section--avatar) {
min-width: unset;
padding-right: 0;
}
/* ── Footer ───────────────────────────────────────────────────────────────── */
.sidebar-footer {
margin-top: auto;
padding: 10px 14px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
flex-shrink: 0;
transition: padding 0.25s ease;
}
.sidebar-footer.is-collapsed {
padding: 10px 0;
display: flex;
justify-content: center;
}
.online-row {
display: flex;
align-items: center;
gap: 8px;
position: relative;
}
/* ── Online dot ───────────────────────────────────────────────────────────── */
.online-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
display: inline-block;
}
.dot-online {
background: #22c55e;
animation: pulse-green 2s ease-in-out infinite;
}
.dot-offline {
background: #ef4444;
}
.online-label {
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
font-size: 0.7rem;
letter-spacing: 0.04em;
opacity: 0.6;
white-space: nowrap;
}
/* ── Transitions ──────────────────────────────────────────────────────────── */
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
opacity: 0;
transform: translateX(-6px);
}
/* ── Pulse animation ──────────────────────────────────────────────────────── */
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55); }
70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); }
100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}
</style> </style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 828 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

+56 -3
View File
@@ -2,6 +2,7 @@ export interface FightingCharacterOption {
label: string; label: string;
value: string; value: string;
image: string; image: string;
dlc?: boolean;
} }
type GamePalette = readonly [startColor: string, endColor: string]; type GamePalette = readonly [startColor: string, endColor: string];
@@ -58,7 +59,7 @@ const characterNamesByGame: Record<string, string[]> = {
'Guilty Gear -Strive-': [ 'Guilty Gear -Strive-': [
'A.B.A', 'A.B.A',
'Anji Mito', 'Anji Mito',
'Asuka R. Kreutz', 'Asuka R.',
'Axl Low', 'Axl Low',
'Baiken', 'Baiken',
'Bedman?', 'Bedman?',
@@ -90,7 +91,7 @@ const characterNamesByGame: Record<string, string[]> = {
'Zato-1', 'Zato-1',
], ],
'Invincible VS': [ 'Invincible VS': [
'Allen The Alien', 'Allen the Alien',
'Anissa', 'Anissa',
'Atom Eve', 'Atom Eve',
'Battle Beast', 'Battle Beast',
@@ -104,7 +105,7 @@ const characterNamesByGame: Record<string, string[]> = {
'Lucan', 'Lucan',
'Monster Girl', 'Monster Girl',
'Omni-Man', 'Omni-Man',
'Power Plex', 'Powerplex',
'Rex Splode', 'Rex Splode',
'Robot', 'Robot',
'Thula', 'Thula',
@@ -381,6 +382,57 @@ const getCharacterImage = (game: string, character: string, characterValue: stri
return characterImageByKey[key] ?? buildCharacterPlaceholder(game, character); return characterImageByKey[key] ?? buildCharacterPlaceholder(game, character);
}; };
/**
* DLC characters per game. Update as new content is released.
* Characters not listed here are treated as base-roster.
*/
const dlcCharactersByGame: Record<string, ReadonlySet<string>> = {
'FATAL FURY: City of the Wolves': new Set([
'Chun-Li', // Season Pass (crossover)
'Cristiano Ronaldo', // Season Pass (celebrity)
'Ken Masters', // Season Pass (crossover)
'Kenshiro', // Season Pass (crossover)
'Nightmare Geese', // Season Pass
'Salvatore Ganacci', // Season Pass (celebrity)
'Vox Reaper', // Season Pass
]),
'Guilty Gear -Strive-': new Set([
// Season 1
'Goldlewis Dickinson', 'Jack-O', 'Happy Chaos', 'Baiken', 'Testament',
// Season 2
'Bridget', 'Sin Kiske', 'Bedman?', 'Asuka R. Kreutz', 'Johnny',
// Season 3
'Elphelt Valentine', 'A.B.A', 'Slayer', 'Dizzy', 'Venom',
// Season 4
'Lucy', 'Unika',
]),
'Mortal Kombat 1': new Set([
// Kombat Pack 1
'Ermac', 'Homelander', 'Omni-Man', 'Peacemaker', 'Quan Chi', 'Tanya',
// Kombat Pack 2
'Conan the Barbarian', 'Cyrax', 'Ghostface', 'Noob Saibot', 'Sektor',
'Shang Tsung', 'Takeda', 'T-1000',
]),
'Street Fighter 6': new Set([
// Year 1
'A.K.I.', 'Akuma', 'Bison', 'Ed',
// Year 2
'Alex', 'Elena', 'Mai', 'Sagat', 'Terry', 'Viper',
]),
'TEKKEN 8': new Set([
// Season 1
'Clive', 'Eddy', 'Heihachi', 'Lidia',
// Season 2
'Anna', 'Fahkumram', 'Kunimitsu', 'Miary Zo', 'Roger Jr',
]),
'THE KING OF FIGHTERS XV': new Set([
'Antonov', 'Elisabeth Blanctorche', 'Gato', 'Geese Howard', 'Goenitz',
'Hinako Shijo', 'Krohnen McDougall', 'Kukri', 'Luong', 'Najd',
'Orochi Chris', 'Orochi Shermie', 'Orochi Yashiro', 'Rock Howard',
'Sylvie Paula Paula',
]),
};
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,
@@ -391,6 +443,7 @@ export const fightingCharactersByGame: Record<string, FightingCharacterOption[]>
label: character, label: character,
value, value,
image: getCharacterImage(game, character, value), image: getCharacterImage(game, character, value),
dlc: dlcCharactersByGame[game]?.has(character) ?? false,
}; };
}), }),
]), ]),