feat: add character images for Guilty Gear Strive and update fighting characters with DLC support
@@ -2,35 +2,6 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
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
|
||||
"Complaining about Paul's damage",
|
||||
'Nerfing Gigas',
|
||||
@@ -38,15 +9,6 @@ const loadQuotes = [
|
||||
'Sidestepping your electric',
|
||||
'Punishing hellsweep with 1,1,2',
|
||||
'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);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
import { usePlayerSide } from '../composables/usePlayerSide';
|
||||
import { CHARACTER_GAME_KEY } from '../composables/useCharacterGame';
|
||||
import { usePlayerSide } from '../composables/usePlayerSide';
|
||||
import { t } from '../i18n';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Props
|
||||
@@ -140,6 +140,19 @@ const sideImageLabel = computed(() => t(isLeft.value ? 'scoreboardLeftImage' : '
|
||||
<template #prepend>
|
||||
<QIcon name="sports_martial_arts" />
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -372,6 +385,19 @@ const sideImageLabel = computed(() => t(isLeft.value ? 'scoreboardLeftImage' : '
|
||||
<template #prepend>
|
||||
<QIcon name="sports_martial_arts" />
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -481,6 +507,27 @@ const sideImageLabel = computed(() => t(isLeft.value ? 'scoreboardLeftImage' : '
|
||||
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) {
|
||||
.scoreboard-preview__image-wrap {
|
||||
width: min(100%, 280px);
|
||||
|
||||
@@ -1,67 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { t } from './i18n';
|
||||
import { useScoreboardStore } from './stores/scoreboard';
|
||||
import { isShortcutMatch, useShortcutSettingsStore } from './stores/shortcut-settings';
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{ label: t('menuDashboard'), to: '/', icon: 'dashboard' },
|
||||
{ label: t('menuPlayers'), to: '/players', icon: 'groups' },
|
||||
{ label: t('menuGraphics'), to: '/graphics', icon: 'collections' },
|
||||
// ── Sidebar collapse ──────────────────────────────────────────────────────────
|
||||
const LS_KEY = 'sidebar_collapsed';
|
||||
const isCollapsed = ref(localStorage.getItem(LS_KEY) === 'true');
|
||||
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('menuAbout'), to: '/about', icon: 'info' },
|
||||
{ label: t('menuAbout'), to: '/about', icon: 'info' },
|
||||
]);
|
||||
|
||||
const logoUrl = new URL('./image.png', import.meta.url).href;
|
||||
const scoreboardStore = useScoreboardStore();
|
||||
// ── Online / Offline ──────────────────────────────────────────────────────────
|
||||
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 isEditableTarget = (target: EventTarget | null): boolean => {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return target.isContentEditable
|
||||
|| ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)
|
||||
|| Boolean(target.closest('[contenteditable="true"]'));
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
return (
|
||||
target.isContentEditable ||
|
||||
['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName) ||
|
||||
Boolean(target.closest('[contenteditable="true"]'))
|
||||
);
|
||||
};
|
||||
|
||||
const onShortcutPress = (event: KeyboardEvent) => {
|
||||
if (isEditableTarget(event.target) || document.body.dataset.shortcutRecording === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditableTarget(event.target) || document.body.dataset.shortcutRecording === 'true') return;
|
||||
const { shortcuts } = shortcutSettingsStore;
|
||||
if (isShortcutMatch(event, shortcuts.leftIncrement)) {
|
||||
scoreboardStore.leftScore += 1;
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
if (isShortcutMatch(event, shortcuts.leftIncrement)) { scoreboardStore.leftScore += 1; event.preventDefault(); return; }
|
||||
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(() => {
|
||||
window.addEventListener('keydown', onShortcutPress);
|
||||
window.addEventListener('online', onNetworkOnline);
|
||||
window.addEventListener('offline', onNetworkOffline);
|
||||
pingInterval = setInterval(checkOnline, 15_000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', onShortcutPress);
|
||||
window.removeEventListener('online', onNetworkOnline);
|
||||
window.removeEventListener('offline', onNetworkOffline);
|
||||
if (pingInterval) clearInterval(pingInterval);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -71,49 +91,117 @@ onUnmounted(() => {
|
||||
show-if-above
|
||||
side="left"
|
||||
bordered
|
||||
:width="220"
|
||||
:width="drawerWidth"
|
||||
class="sidebar-drawer"
|
||||
>
|
||||
<div class="sidebar-header q-pa-md">
|
||||
<div class="row items-center no-wrap">
|
||||
<img
|
||||
:src="logoUrl"
|
||||
alt="Logo"
|
||||
class="sidebar-logo"
|
||||
>
|
||||
<div class="q-ml-sm">
|
||||
<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>
|
||||
<!-- ── Header ─────────────────────────────────────────── -->
|
||||
<div class="sidebar-header" :class="{ 'is-collapsed': isCollapsed }">
|
||||
<img :src="logoUrl" alt="Logo" class="sidebar-logo">
|
||||
|
||||
<Transition name="slide-fade">
|
||||
<div v-if="!isCollapsed" class="sidebar-title">
|
||||
<span class="title-text">Scoreko-dev</span>
|
||||
<span v-if="appVersion" class="title-version">v{{ appVersion }}</span>
|
||||
</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>
|
||||
<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
|
||||
v-for="item in menuItems"
|
||||
v-for="item in mainItems"
|
||||
:key="item.to"
|
||||
clickable
|
||||
:to="item.to"
|
||||
exact
|
||||
active-class="sidebar-item-active"
|
||||
:class="{ 'nav-item-collapsed': isCollapsed }"
|
||||
>
|
||||
<QItemSection avatar>
|
||||
<QIcon :name="item.icon" />
|
||||
<QIcon :name="item.icon" size="sm" />
|
||||
</QItemSection>
|
||||
<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>
|
||||
|
||||
<!-- ── 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>
|
||||
|
||||
<QPageContainer>
|
||||
@@ -123,28 +211,176 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Drawer shell ─────────────────────────────────────────────────────────── */
|
||||
.sidebar-drawer :deep(.q-drawer__content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── 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 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.by-label {
|
||||
font-size: 0.75rem;
|
||||
.sidebar-title {
|
||||
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 {
|
||||
font-size: 0.75rem;
|
||||
color: #f50a64;
|
||||
text-decoration: none;
|
||||
/* ── Collapse button ──────────────────────────────────────────────────────── */
|
||||
.collapse-btn {
|
||||
flex-shrink: 0;
|
||||
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 {
|
||||
text-decoration: underline;
|
||||
/* ── Section separators ───────────────────────────────────────────────────── */
|
||||
.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;
|
||||
}
|
||||
|
||||
</style>
|
||||
.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>
|
||||
|
After Width: | Height: | Size: 828 KiB |
|
After Width: | Height: | Size: 515 KiB |
|
After Width: | Height: | Size: 605 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 762 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 776 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 515 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 516 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 716 KiB |
|
After Width: | Height: | Size: 414 KiB |
|
After Width: | Height: | Size: 804 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 4.1 MiB After Width: | Height: | Size: 1.9 MiB |
@@ -2,6 +2,7 @@ export interface FightingCharacterOption {
|
||||
label: string;
|
||||
value: string;
|
||||
image: string;
|
||||
dlc?: boolean;
|
||||
}
|
||||
|
||||
type GamePalette = readonly [startColor: string, endColor: string];
|
||||
@@ -58,7 +59,7 @@ const characterNamesByGame: Record<string, string[]> = {
|
||||
'Guilty Gear -Strive-': [
|
||||
'A.B.A',
|
||||
'Anji Mito',
|
||||
'Asuka R. Kreutz',
|
||||
'Asuka R.',
|
||||
'Axl Low',
|
||||
'Baiken',
|
||||
'Bedman?',
|
||||
@@ -90,7 +91,7 @@ const characterNamesByGame: Record<string, string[]> = {
|
||||
'Zato-1',
|
||||
],
|
||||
'Invincible VS': [
|
||||
'Allen The Alien',
|
||||
'Allen the Alien',
|
||||
'Anissa',
|
||||
'Atom Eve',
|
||||
'Battle Beast',
|
||||
@@ -104,7 +105,7 @@ const characterNamesByGame: Record<string, string[]> = {
|
||||
'Lucan',
|
||||
'Monster Girl',
|
||||
'Omni-Man',
|
||||
'Power Plex',
|
||||
'Powerplex',
|
||||
'Rex Splode',
|
||||
'Robot',
|
||||
'Thula',
|
||||
@@ -381,6 +382,57 @@ const getCharacterImage = (game: string, character: string, characterValue: stri
|
||||
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(
|
||||
Object.entries(characterNamesByGame).map(([game, characterNames]) => [
|
||||
game,
|
||||
@@ -391,6 +443,7 @@ export const fightingCharactersByGame: Record<string, FightingCharacterOption[]>
|
||||
label: character,
|
||||
value,
|
||||
image: getCharacterImage(game, character, value),
|
||||
dlc: dlcCharactersByGame[game]?.has(character) ?? false,
|
||||
};
|
||||
}),
|
||||
]),
|
||||
|
||||