feat: add character images for Guilty Gear Strive and update fighting characters with DLC support
@@ -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);
|
||||||
|
|||||||
@@ -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 ──────────────────────────────────────────────────────────
|
||||||
|
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('menuDashboard'), to: '/', icon: 'dashboard' },
|
||||||
{ label: t('menuPlayers'), to: '/players', icon: 'groups' },
|
{ label: t('menuPlayers'), to: '/players', icon: 'groups' },
|
||||||
{ label: t('menuGraphics'), to: '/graphics', icon: 'collections' },
|
{ 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 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 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>
|
||||||
<div class="text-caption">
|
</Transition>
|
||||||
<span class="by-label">by</span> <a
|
|
||||||
class="by-link"
|
<!-- Chevron — siempre visible, arriba a la derecha -->
|
||||||
href="https://github.com/Pandipipas"
|
<QBtn
|
||||||
target="_blank"
|
flat
|
||||||
rel="noopener"
|
round
|
||||||
>Pandipipas</a>
|
dense
|
||||||
|
size="sm"
|
||||||
|
:icon="isCollapsed ? 'chevron_right' : 'chevron_left'"
|
||||||
|
class="collapse-btn"
|
||||||
|
@click="toggleCollapse"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<QSeparator />
|
||||||
|
|
||||||
|
<!-- ── Sección MAIN ───────────────────────────────────── -->
|
||||||
|
<div class="section-sep" :class="{ 'is-collapsed': isCollapsed }">
|
||||||
|
<span v-if="!isCollapsed" class="section-label">MAIN</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<QList padding>
|
||||||
<QSeparator class="q-mb-sm" />
|
|
||||||
<QList>
|
|
||||||
<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>
|
||||||
|
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;
|
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,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
|||||||