Files
BetiX/resources/js/pages/GamePlay.vue
Dolo 0280278978
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

1561 lines
54 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { Head, router } from '@inertiajs/vue3';
import { Maximize, MonitorPlay, BarChart2, RefreshCw, ArrowLeft, Trophy, Info, PlayCircle, ShieldCheck, Heart, Crosshair, Sparkles, Star } from 'lucide-vue-next';
import Button from '@/components/ui/button.vue';
import UserLayout from '@/layouts/user/userlayout.vue';
// Props from Inertia route
const props = defineProps<{
title?: string;
slug?: string | null;
src?: string | null;
provider?: string | null;
description?: string | null;
}>();
const loading = ref(true);
const error = ref<string | null>(null);
// UI State
const isCinema = ref<boolean>(false);
const isFullscreen = ref<boolean>(false);
const gameCardRef = ref<HTMLElement | null>(null);
try { isCinema.value = sessionStorage.getItem('gp:isCinema') === '1'; } catch {}
const showStats = ref(false);
const modeKey = computed(() => `gp:mode:${props.slug ?? 'default'}`);
const mode = ref<'real' | 'demo'>('demo');
const titleText = computed(() => props.title || props.slug || 'Game');
const providerText = computed(() => props.provider || 'Unknown');
// ---- Game Meta (fallbacks) ----
const fallbackMeta: Record<string, { description: string; provider?: string; rtp?: number; volatility?: 'low'|'medium'|'high'; image?: string; category?: string; }> = {
'gates-of-olympus': { description: 'Stürze dich mit Zeus in stürmische Multiplikatoren und Freispiele.', provider: 'Pragmatic Play', rtp: 96.5, volatility: 'high', image: 'https://cdn.softswiss.net/i/s3/pragmatic/GatesOfOlympus.png', category: 'mythology' },
'sweet-bonanza': { description: 'Süße Gewinne mit Bonbons und Kaskaden-Features.', provider: 'Pragmatic Play', rtp: 96.48, volatility: 'medium', image: 'https://cdn.softswiss.net/i/s3/pragmatic/SweetBonanza.png', category: 'fruits' },
'razor-shark': { description: 'Tauche ab nach Schätzen mit Mystery-Stacks und Multiplikatoren.', provider: 'Push Gaming', rtp: 96.7, volatility: 'high', image: 'https://cdn.softswiss.net/i/s3/pushgaming/RazorShark.png', category: 'ocean' },
'mental': { description: 'Extrem volatile Irrenanstalt nichts für schwache Nerven.', provider: 'Nolimit City', rtp: 96.08, volatility: 'high', image: 'https://cdn.softswiss.net/i/s3/nolimit/Mental.png', category: 'horror' },
'wanted-dead-or-a-wild': { description: 'WesternAction mit massiven Multiplikatoren und Bonuskäufen.', provider: 'Hacksaw', rtp: 96.38, volatility: 'high', image: 'https://cdn.softswiss.net/i/s3/hacksaw/WantedDeadOrAWild.png', category: 'western' },
};
const slug = computed(() => String(props.slug ?? '').toLowerCase());
const gameMeta = computed(() => fallbackMeta[slug.value] || null);
const descriptionText = computed(() => props.description || gameMeta.value?.description || 'Erlebe spannende Action und unglaubliche Gewinnchancen in diesem Top-Hit!');
// Players online (MVP mock: random walk)
const playerCount = ref<number>(Math.floor(80 + Math.random()*140));
let presenceTimer: number | undefined;
// Live winners (MVP mock)
const liveWins = ref([
{ id: 1, user: 'CryptoKing', game: titleText.value, amount: '0.05 BTC', isWin: true },
{ id: 2, user: 'LuckyLuke', game: titleText.value, amount: '1.2 ETH', isWin: true },
{ id: 3, user: 'Anna_99', game: titleText.value, amount: '500 USDT', isWin: true },
{ id: 4, user: 'Satoshi', game: titleText.value, amount: '0.01 BTC', isWin: true },
{ id: 5, user: 'Whale_Hunter', game: titleText.value, amount: '2.5 ETH', isWin: true },
{ id: 6, user: 'Dolo', game: titleText.value, amount: '1500 XRP', isWin: true },
]);
let winnersNextId = 7;
let winnersTimer: number | undefined;
function maskName(name: string) {
if (!name) return '';
if (name.length <= 3) return name;
return name.substring(0,2) + '***' + name.substring(name.length-1);
}
const slugify = (val: string) => String(val).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
// Similar games (simple heuristic using local list)
const catalog = [
{ name: 'Gates of Olympus', provider: 'Pragmatic Play', image: 'https://cdn.softswiss.net/i/s3/pragmatic/GatesOfOlympus.png' },
{ name: 'Sweet Bonanza', provider: 'Pragmatic Play', image: 'https://cdn.softswiss.net/i/s3/pragmatic/SweetBonanza.png' },
{ name: 'Wanted Dead or a Wild', provider: 'Hacksaw', image: 'https://cdn.softswiss.net/i/s3/hacksaw/WantedDeadOrAWild.png' },
{ name: 'Razor Shark', provider: 'Push Gaming', image: 'https://cdn.softswiss.net/i/s3/pushgaming/RazorShark.png' },
{ name: 'Mental', provider: 'Nolimit City', image: 'https://cdn.softswiss.net/i/s3/nolimit/Mental.png' },
{ name: 'Sugar Rush 1000', provider: 'Pragmatic Play', image: 'https://cdn.softswiss.net/i/s3/pragmatic/SugarRush.png' },
{ name: 'Le Bandit', provider: 'Hacksaw', image: 'https://cdn.softswiss.net/i/s3/hacksaw/LeBandit.png' },
{ name: 'Big Bamboo', provider: 'Push Gaming', image: 'https://cdn.softswiss.net/i/s3/pushgaming/BigBamboo.png' },
];
const similar = computed(() => {
const baseProv = providerText.value;
const current = slug.value;
const arr = catalog
.filter(g => g.provider === baseProv)
.filter(g => slugify(g.name) !== current)
.slice(0, 4)
.map(g => ({ ...g, slug: slugify(g.name) }));
if (arr.length < 4) {
const more = catalog.filter(g => slugify(g.name) !== current && !arr.find(a => a.name === g.name)).slice(0, 4 - arr.length);
arr.push(...more.map(g => ({ ...g, slug: slugify(g.name) })));
}
return arr;
});
function playGameBySlug(s: string, prov?: string) {
if (!s) return;
const providerSegment = encodeURIComponent((prov || providerText.value || 'betix').toLowerCase().replace(/\s+/g, '-'));
router.visit(`/games/play/${providerSegment}/${encodeURIComponent(s)}`);
}
const BETIX_ORIGINALS = ['dice', 'crash', 'mines', 'plinko'];
const isOriginal = computed(() => BETIX_ORIGINALS.includes(slug.value));
const originalLaunchUrl = ref<string | null>(null);
const originalLoading = ref(false);
async function launchOriginalSession() {
if (!isOriginal.value) return;
originalLoading.value = true;
loading.value = true;
try {
const csrf = (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '';
const res = await fetch('/api/originals/launch', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf },
body: JSON.stringify({ game: slug.value }),
signal: AbortSignal.timeout(20000),
});
if (!res.ok) throw new Error(`${res.status}`);
const data = await res.json();
originalLaunchUrl.value = data.launch_url;
loading.value = false;
} catch (e) {
console.error('[GamePlay] launchOriginalSession failed:', e);
error.value = 'Spiel konnte nicht gestartet werden.';
loading.value = false;
} finally {
originalLoading.value = false;
}
}
const iframeSrc = computed(() => {
if (isOriginal.value) {
return originalLaunchUrl.value ?? '';
}
if (props.slug) {
const q = `?mode=${encodeURIComponent(mode.value)}`;
return `/games/embed/${encodeURIComponent(String(props.slug))}${q}`;
}
return '';
});
function onLoad() {
loading.value = false;
}
function onError() {
loading.value = false;
error.value = 'Spiel konnte nicht geladen werden.';
}
function goBack() {
if (window.history.length > 1) {
window.history.back();
} else {
router.visit('/dashboard');
}
}
function reload() {
loading.value = true;
const el = document.getElementById('game-frame') as HTMLIFrameElement | null;
if (el) el.src = iframeSrc.value;
}
function scrollToCenter() {
gameCardRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function toggleCinema() {
if (isFullscreen.value) toggleFullscreen();
isCinema.value = !isCinema.value;
try { sessionStorage.setItem('gp:isCinema', isCinema.value ? '1' : '0'); } catch {}
if (isCinema.value) {
document.dispatchEvent(new CustomEvent('collapse-sidebar'));
document.dispatchEvent(new CustomEvent('hide-support-chat'));
nextTick(() => {
setTimeout(() => {
gameCardRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300);
});
} else {
isAmbiente.value = false;
document.dispatchEvent(new CustomEvent('expand-sidebar'));
document.dispatchEvent(new CustomEvent('show-support-chat'));
}
}
function selectCinema() {
showCinemaMenu.value = false;
if (isAmbiente.value) {
isAmbiente.value = false;
return;
}
toggleCinema();
}
function selectAmbiente() {
showCinemaMenu.value = false;
if (isAmbiente.value) {
isAmbiente.value = false;
return;
}
if (!isCinema.value) {
isCinema.value = true;
try { sessionStorage.setItem('gp:isCinema', '1'); } catch {}
document.dispatchEvent(new CustomEvent('collapse-sidebar'));
document.dispatchEvent(new CustomEvent('hide-support-chat'));
}
isAmbiente.value = true;
nextTick(() => {
setTimeout(() => {
gameCardRef.value?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300);
});
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
if (isCinema.value) {
isCinema.value = false;
try { sessionStorage.setItem('gp:isCinema', '0'); } catch {}
}
document.dispatchEvent(new CustomEvent('collapse-sidebar'));
document.dispatchEvent(new CustomEvent('hide-support-chat'));
gameCardRef.value?.requestFullscreen().catch(err => {
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
});
} else {
document.exitFullscreen();
document.dispatchEvent(new CustomEvent('expand-sidebar'));
}
}
function handleFullscreenChange() {
isFullscreen.value = !!document.fullscreenElement;
if (!isFullscreen.value) {
if (!isCinema.value) {
document.dispatchEvent(new CustomEvent('expand-sidebar'));
}
document.dispatchEvent(new CustomEvent('show-support-chat'));
}
}
function toggleStats() {
showStats.value = !showStats.value;
}
function setMode(next: 'real' | 'demo') {
if (mode.value === next) return;
mode.value = next;
try { localStorage.setItem(modeKey.value, next); } catch {}
if (props.slug) {
reload();
}
}
function onKeydown(e: KeyboardEvent) {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
if (e.key.toLowerCase() === 'c') {
e.preventDefault();
toggleCinema();
} else if (e.key.toLowerCase() === 'f') {
e.preventDefault();
toggleFullscreen();
} else if (e.key === 'Escape') {
if (showStats.value) showStats.value = false;
}
}
const isFavorite = ref(false);
function toggleFavorite() {
isFavorite.value = !isFavorite.value;
}
const showInfo = ref(false);
const isAmbiente = ref(false);
const showCinemaMenu = ref(false);
const cinemaMenuRef = ref<HTMLElement | null>(null);
const CURRENCIES = [
{ symbol: 'BTX', name: 'BetiX Coin', color: '#ff007a' },
{ symbol: 'BTC', name: 'Bitcoin', color: '#f7931a' },
{ symbol: 'ETH', name: 'Ethereum', color: '#627eea' },
{ symbol: 'SOL', name: 'Solana', color: '#9945ff' },
{ symbol: 'USDT', name: 'Tether', color: '#26a17b' },
];
const selectedCurrency = ref('BTX');
const showCurrencyMenu = ref(false);
const currencyMenuRef = ref<HTMLElement | null>(null);
function handleCinemaMenuOutside(e: MouseEvent) {
if (showCinemaMenu.value && cinemaMenuRef.value && !cinemaMenuRef.value.contains(e.target as Node)) {
showCinemaMenu.value = false;
}
if (showCurrencyMenu.value && currencyMenuRef.value && !currencyMenuRef.value.contains(e.target as Node)) {
showCurrencyMenu.value = false;
}
}
// Trigger balance refresh in UserLayout when the game iframe signals a round completed.
// The BetiX game server may postMessage after processing a round (e.g. { type: 'betix:round' }).
// We also listen for any postMessage from the game origin and schedule a refresh,
// because the round webhook fires asynchronously — give it a short delay.
let roundRefreshTimer: number | undefined;
function onGameMessage(e: MessageEvent) {
// Only react to messages from our game server (loose check for local dev)
if (typeof e.data !== 'object' || !e.data) return;
const type: string = e.data.type ?? '';
if (type.startsWith('betix:') || type === 'casino.embed.ready' || type === 'round' || type === 'result') {
// Debounce: cancel previous timer, wait ~1.5 s for the webhook to land before polling
clearTimeout(roundRefreshTimer);
roundRefreshTimer = window.setTimeout(() => {
document.dispatchEvent(new CustomEvent('balance:refresh'));
}, 1500);
}
}
onMounted(() => {
window.addEventListener('keydown', onKeydown);
window.addEventListener('message', onGameMessage);
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('click', handleCinemaMenuOutside);
try {
const saved = localStorage.getItem(modeKey.value);
if (saved === 'real' || saved === 'demo') mode.value = saved;
} catch {}
// BetiX Originals always launch in real-money mode via session
if (isOriginal.value) {
mode.value = 'real';
launchOriginalSession();
}
presenceTimer = window.setInterval(() => {
const delta = Math.floor((Math.random() - 0.5) * 6);
playerCount.value = Math.max(1, playerCount.value + delta);
}, 3000);
winnersTimer = window.setInterval(() => {
const users = ['Andri_X', 'CryptoKing', 'Neon_Ripper', 'Satoshi', 'Whale_Hunter', 'Dolo'];
const amounts = ['0.01 BTC', '0.5 ETH', '200 USDT', '1500 XRP', '0.2 BTC'];
const newWin = {
id: winnersNextId++,
user: users[Math.floor(Math.random() * users.length)],
game: titleText.value,
amount: amounts[Math.floor(Math.random() * amounts.length)],
isWin: true
};
liveWins.value.unshift(newWin);
if (liveWins.value.length > 6) liveWins.value.pop();
}, 2500);
if (isCinema.value) {
document.dispatchEvent(new CustomEvent('collapse-sidebar'));
// SupportChat may mount after GamePlay, defer so its listener is ready
nextTick(() => {
document.dispatchEvent(new CustomEvent('hide-support-chat'));
});
}
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeydown);
window.removeEventListener('message', onGameMessage);
clearTimeout(roundRefreshTimer);
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('click', handleCinemaMenuOutside);
if (presenceTimer) clearInterval(presenceTimer);
if (winnersTimer) clearInterval(winnersTimer);
// Restore support icon when navigating away from game page
document.dispatchEvent(new CustomEvent('show-support-chat'));
});
</script>
<template>
<UserLayout>
<Head :title="titleText" />
<!-- Ambiente Background Glow -->
<Teleport to="body">
<div v-if="isAmbiente" class="ambient-bg" />
</Teleport>
<div class="gameplay-page" :class="{ 'cinema-active': isCinema, 'fullscreen-active': isFullscreen, 'ambiente-active': isAmbiente }">
<!-- Full width Top Nav -->
<div class="top-nav" v-show="!isCinema && !isFullscreen">
<button class="back-link" @click="goBack">
<ArrowLeft class="w-4 h-4 mr-2" /> Zurück zur Lobby
</button>
<div class="game-path">
<span>Casino</span> <span class="sep">/</span>
<span>{{ providerText }}</span> <span class="sep">/</span>
<span class="current">{{ titleText }}</span>
</div>
</div>
<!-- The Game Player (Takes full width alone) -->
<div class="game-card" ref="gameCardRef" :class="{ cinema: isCinema, fullscreen: isFullscreen, ambiente: isAmbiente }">
<!-- Cinema/Fullscreen Close Button (Top Right) -->
<button v-if="(isFullscreen || isCinema) && !isAmbiente" class="exit-btn" @click="isFullscreen ? toggleFullscreen() : toggleCinema()" title="Schließen (ESC)">
<i data-lucide="x"></i> Schließen
</button>
<!-- Ambiente Exit Button -->
<button v-if="isAmbiente" class="exit-btn ambiente-exit-btn" @click="isAmbiente = false" title="Ambiente verlassen">
<Sparkles class="w-4 h-4" />
</button>
<!-- Game Header -->
<div class="gc-head" v-show="!isFullscreen && !isAmbiente">
<div class="gc-left">
<div class="game-icon">
<img :src="gameMeta?.image || 'https://api.dicebear.com/7.x/shapes/svg?seed=' + titleText" alt="icon" />
</div>
<div class="gc-title-area">
<h1 class="gc-title">{{ titleText }}</h1>
<div class="gc-subtitle">
<span class="provider-badge">{{ providerText }}</span>
<div class="gc-presence" title="Spieler aktiv">
<span class="presence-dot"></span>
{{ playerCount }} spielen
</div>
</div>
</div>
</div>
</div>
<!-- Game Body (Iframe) -->
<div class="gc-body">
<!-- Overlays -->
<div v-if="!iframeSrc && !originalLoading && !loading" class="gp-error">
<Info class="w-12 h-12 mb-3 text-gray-500" />
<h2>Spiel nicht verfügbar</h2>
<p>Die Quelle für dieses Spiel konnte nicht gefunden werden.</p>
</div>
<div v-if="loading" class="gp-loading">
<div class="loader-ring"></div>
<div class="loader-logo">
<PlayCircle class="w-8 h-8 text-[var(--primary)]" />
</div>
<span class="loader-text">Lade {{ titleText }}...</span>
</div>
<div v-if="error" class="gp-error">
<ShieldCheck class="w-12 h-12 mb-3 text-red-500" />
<h2>Verbindungsfehler</h2>
<p>{{ error }}</p>
<Button class="mt-6 neon-button px-8" @click="reload">Neu verbinden</Button>
</div>
<iframe
v-if="iframeSrc"
id="game-frame"
:src="iframeSrc"
class="gp-iframe"
allow="autoplay; fullscreen; clipboard-read; clipboard-write"
allowfullscreen
@load="onLoad"
@error="onError"
></iframe>
<!-- Floating tools in native fullscreen -->
<div v-if="isFullscreen" class="fs-tools">
<button class="tool-btn bg-black/50 hover:bg-black/80 text-white border-0" @click="toggleFullscreen" title="Vollbild beenden (ESC)">
<Maximize class="w-5 h-5" />
</button>
</div>
<!-- Stats Overlay -->
<transition name="slide-fade">
<aside v-if="showStats" class="gc-stats">
<div class="stats-head">
<div class="stats-title"><BarChart2 class="w-4 h-4 mr-2" /> Live Session</div>
<button class="stats-close" @click="showStats = false">×</button>
</div>
<div class="stats-body">
<div class="stat-box">
<span class="s-label">Gespielte Runden</span>
<span class="s-val">0</span>
</div>
<div class="stat-box">
<span class="s-label">Höchster Gewinn</span>
<span class="s-val text-[var(--primary)]">0.00</span>
</div>
<div class="divider-h"></div>
<div class="stat-row"><span>RTP</span><b>{{ gameMeta?.rtp ? gameMeta.rtp + '%' : 'N/A' }}</b></div>
<div class="stat-row"><span>Volatilität</span><b class="capitalize" :class="'vol-'+gameMeta?.volatility">{{ gameMeta?.volatility || 'N/A' }}</b></div>
</div>
</aside>
</transition>
</div>
<!-- Info Modal Overlay -->
<transition name="info-modal">
<div v-if="showInfo" class="info-modal-backdrop" @click.self="showInfo = false">
<div class="info-modal">
<div class="info-modal-head">
<div class="info-modal-title"><Info class="w-5 h-5 mr-2" /> Spielinfo</div>
<button class="info-modal-close" @click="showInfo = false">×</button>
</div>
<div class="info-modal-body">
<div class="info-game-header">
<img :src="gameMeta?.image || 'https://api.dicebear.com/7.x/shapes/svg?seed=' + titleText" class="info-thumb" />
<div>
<div class="info-game-name">{{ titleText }}</div>
<div class="info-game-prov">{{ providerText }}</div>
</div>
</div>
<div class="info-stats-grid">
<div class="info-stat">
<span class="is-label">RTP</span>
<span class="is-val primary">{{ gameMeta?.rtp ? gameMeta.rtp + '%' : 'N/A' }}</span>
</div>
<div class="info-stat">
<span class="is-label">Volatilität</span>
<span class="is-val capitalize" :class="'vol-' + gameMeta?.volatility">{{ gameMeta?.volatility || 'N/A' }}</span>
</div>
<div class="info-stat">
<span class="is-label">Kategorie</span>
<span class="is-val capitalize">{{ gameMeta?.category || 'Slot' }}</span>
</div>
<div class="info-stat">
<span class="is-label">Einsatzlimits</span>
<span class="is-val">0.10 500.00</span>
</div>
</div>
<div class="info-section">
<h4>Über das Spiel</h4>
<p>{{ descriptionText }}</p>
</div>
<div class="info-section">
<h4>Regeln & Features</h4>
<ul class="info-rules">
<li>Linien-Gewinne werden von links nach rechts gezählt</li>
<li>Höchstgewinn kann je nach Spiel variieren</li>
<li>Freispiele und Bonusrunden durch Scatter-Symbole</li>
<li>Wild-Symbole ersetzen alle anderen Symbole außer Scatter</li>
<li>Alle Gewinne werden als Vielfaches des Einsatzes ausgedrückt</li>
</ul>
</div>
<div class="info-section">
<h4>Verantwortungsvolles Spielen</h4>
<p class="info-rg-text">Dieses Spiel sollte nur zur Unterhaltung gespielt werden. Setze dir Limits und spiele verantwortungsbewusst. Bei Fragen wende dich an unseren <a href="/self-exclusion" class="info-link">Verantwortungsvollen Spielerschutz</a>.</p>
</div>
</div>
</div>
</div>
</transition>
<!-- Game Footer Toolbar -->
<div class="gc-foot" v-show="!isFullscreen && !isAmbiente">
<div class="foot-left">
<button class="tool-btn" :class="{ 'text-[var(--primary)]': isFavorite }" @click="toggleFavorite" title="Favorit">
<Heart class="w-5 h-5" :fill="isFavorite ? 'currentColor' : 'none'" />
</button>
<button class="tool-btn" :class="{ active: showStats }" @click="toggleStats" title="Statistik">
<BarChart2 class="w-5 h-5" />
</button>
<button class="tool-btn" :class="{ active: showInfo }" @click="showInfo = !showInfo" title="Spielinfo (RTP, Regeln)">
<Info class="w-5 h-5" />
</button>
<button class="tool-btn" @click="reload" title="Neu laden">
<RefreshCw class="w-5 h-5" />
</button>
</div>
<div class="foot-center">
<div class="foot-controls">
<!-- Demo / Real Switch -->
<div class="mode-switch" role="group" aria-label="Modus wählen">
<button class="mode-btn" :class="{ active: mode === 'demo' }" @click="setMode('demo')">Demo</button>
<button class="mode-btn" :class="{ active: mode === 'real' }" @click="setMode('real')">Real</button>
</div>
<div class="foot-divider" />
<!-- Currency Selector -->
<div class="currency-wrap" ref="currencyMenuRef">
<button
class="currency-btn"
@click="showCurrencyMenu = !showCurrencyMenu"
title="Währung wählen"
>
<span class="currency-dot" :style="{ background: CURRENCIES.find(c => c.symbol === selectedCurrency)?.color }" />
<span class="currency-label">{{ selectedCurrency }}</span>
<svg class="currency-chevron" :class="{ open: showCurrencyMenu }" viewBox="0 0 10 6" width="10" height="6"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>
</button>
<transition name="cinema-menu">
<div v-if="showCurrencyMenu" class="currency-dropdown">
<button
v-for="c in CURRENCIES"
:key="c.symbol"
class="currency-option"
:class="{ active: selectedCurrency === c.symbol }"
@click="selectedCurrency = c.symbol; showCurrencyMenu = false"
>
<span class="currency-dot" :style="{ background: c.color }" />
<span class="co-title">{{ c.symbol }}</span>
<span class="co-desc" style="margin-left: auto">{{ c.name }}</span>
</button>
</div>
</transition>
</div>
</div>
</div>
<div class="foot-right">
<button class="tool-btn mr-2" @click="scrollToCenter" title="Spiel zentrieren">
<Crosshair class="w-5 h-5" />
</button>
<div class="cinema-btn-wrap" ref="cinemaMenuRef">
<button
class="tool-btn mr-2"
:class="{ active: isCinema || isAmbiente }"
@click="showCinemaMenu = !showCinemaMenu"
title="Ansichts-Modi"
>
<MonitorPlay class="w-5 h-5" />
</button>
<transition name="cinema-menu">
<div v-if="showCinemaMenu" class="cinema-dropdown">
<button
class="cinema-option"
:class="{ active: isCinema && !isAmbiente }"
@click="selectCinema"
>
<MonitorPlay class="w-4 h-4 shrink-0" />
<div>
<div class="co-title">Kino</div>
<div class="co-desc">Vergrößerte Ansicht</div>
</div>
</button>
<button
class="cinema-option"
:class="{ active: isAmbiente }"
@click="selectAmbiente"
>
<Sparkles class="w-4 h-4 shrink-0" />
<div>
<div class="co-title">Ambiente</div>
<div class="co-desc">Immersives Erlebnis</div>
</div>
</button>
</div>
</transition>
</div>
<button class="tool-btn" @click="toggleFullscreen" title="Vollbild (F)">
<Maximize class="w-5 h-5" />
</button>
</div>
</div>
</div>
<!-- Content Below Slot -->
<div class="below-content" v-show="!isCinema && !isFullscreen">
<!-- Game Details -->
<div class="game-details">
<div class="detail-card">
<h2 class="section-title">Über {{ titleText }}</h2>
<p class="desc-text">{{ descriptionText }}</p>
<div class="feature-grid">
<div class="f-item">
<div class="f-icon"><ShieldCheck class="w-5 h-5 text-[var(--primary)]"/></div>
<div class="f-info">
<span>Provider</span>
<strong>{{ providerText }}</strong>
</div>
</div>
<div class="f-item">
<div class="f-icon"><BarChart2 class="w-5 h-5 text-[var(--primary)]"/></div>
<div class="f-info">
<span>Theoretischer RTP</span>
<strong>{{ gameMeta?.rtp ? gameMeta.rtp + '%' : 'Unbekannt' }}</strong>
</div>
</div>
<div class="f-item">
<div class="f-icon"><RefreshCw class="w-5 h-5 text-[var(--primary)]"/></div>
<div class="f-info">
<span>Volatilität</span>
<strong class="capitalize" :class="'vol-'+gameMeta?.volatility">{{ gameMeta?.volatility || 'Unbekannt' }}</strong>
</div>
</div>
</div>
</div>
</div>
<!-- Live Wins Section (Dashboard Style) -->
<section class="wins-section">
<div class="section-header">
<h2><Trophy class="w-5 h-5 text-yellow-500 inline mb-1" /> Live Wins in {{ titleText }}</h2>
<div class="flex items-center gap-2 text-xs text-[var(--primary)] font-bold uppercase">
<span class="w-2 h-2 bg-[var(--primary)] rounded-full animate-pulse"></span> Live Feed
</div>
</div>
<div class="wins-grid">
<transition-group name="wins-list">
<div v-for="win in liveWins" :key="win.id" class="win-card">
<div class="win-icon">
<img :src="`https://api.dicebear.com/7.x/avataaars/svg?seed=${win.user}`" alt="Avatar" class="w-8 h-8 rounded-full bg-[#222]" />
</div>
<div class="win-details">
<div class="win-user">{{ maskName(win.user) }}</div>
<div class="win-game">won in {{ win.game }}</div>
</div>
<div class="win-value">{{ win.amount }}</div>
</div>
</transition-group>
</div>
</section>
<!-- Similar Games Section (Dashboard Style) -->
<section class="game-section" v-if="similar.length">
<div class="section-header">
<h2><Star class="w-5 h-5 text-yellow-400 inline mb-1" /> Mehr von {{ providerText }}</h2>
</div>
<div class="game-grid">
<div v-for="slot in similar" :key="slot.slug" class="similar-game-card group" @click="playGameBySlug(slot.slug, slot.provider)">
<div class="card-image">
<img :src="slot.image" :alt="slot.name" loading="lazy">
<div class="card-overlay">
<Button class="play-btn" @click.stop.prevent="playGameBySlug(slot.slug, slot.provider)"><PlayCircle class="w-8 h-8 text-white" /></Button>
<span class="provider">{{ slot.provider }}</span>
</div>
</div>
<div class="card-info">
<div class="game-name">{{ slot.name }}</div>
</div>
</div>
</div>
</section>
</div>
</div>
</UserLayout>
</template>
<style scoped>
.gameplay-page {
padding: 30px;
max-width: 1400px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 30px;
transition: all 0.5s cubic-bezier(0.2, 0, 0, 1);
}
.gameplay-page.cinema-active {
max-width: 100%;
}
.gameplay-page.fullscreen-active {
max-width: 100%;
padding: 0;
gap: 0;
}
/* TOP NAV */
.top-nav {
display: flex;
align-items: center;
justify-content: space-between;
}
.back-link {
display: flex; align-items: center;
color: #a1a1aa; font-size: 14px; font-weight: 600;
background: transparent; border: none; cursor: pointer;
transition: color 0.2s;
}
.back-link:hover { color: #fff; }
.game-path {
font-size: 13px; color: #71717a; font-weight: 500;
}
.game-path .sep { margin: 0 8px; opacity: 0.5; }
.game-path .current { color: #e4e4e7; }
/* MAIN GAME CARD - Full width now */
.game-card {
background: #0a0a0a;
border: 1px solid #151515;
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
transition: all 0.6s cubic-bezier(0.2, 0, 0, 1);
width: 100%;
position: relative;
z-index: 10;
}
/* Cinema Mode Animation - Make it HUGE */
.game-card.cinema {
border-radius: 16px;
border-color: #222;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.8);
}
/* Native Fullscreen Animation */
.game-card.fullscreen {
border-radius: 0;
border: none;
background: #000;
width: 100vw;
height: 100vh;
}
/* Exit Button (for Cinema/Fullscreen) */
.exit-btn {
position: absolute;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(0,0,0,0.6);
border: 1px solid rgba(255,255,255,0.1);
color: #fff;
padding: 8px 16px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 13px;
cursor: pointer;
backdrop-filter: blur(10px);
transition: all 0.2s;
}
.exit-btn:hover {
background: var(--primary);
border-color: var(--primary);
}
/* Header */
.gc-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: linear-gradient(to bottom, #111, #0a0a0a);
border-bottom: 1px solid #151515;
}
.gc-left {
display: flex;
align-items: center;
gap: 16px;
}
.game-icon {
width: 48px; height: 48px;
border-radius: 12px;
overflow: hidden;
background: #111;
border: 1px solid #222;
}
.game-icon img { width: 100%; height: 100%; object-fit: cover; }
.gc-title-area {
display: flex;
flex-direction: column;
gap: 6px;
}
.gc-title {
color: #fff;
font-size: 20px;
font-weight: 800;
margin: 0; letter-spacing: -0.5px;
}
.gc-subtitle {
display: flex;
align-items: center;
gap: 12px;
}
.provider-badge {
color: #888;
font-size: 12px;
font-weight: 600;
background: rgba(255,255,255,0.05);
padding: 2px 8px;
border-radius: 6px;
}
.gc-presence {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
color: var(--primary);
}
.presence-dot {
width: 6px; height: 6px;
background: var(--primary);
border-radius: 50%;
box-shadow: 0 0 8px var(--primary);
animation: pulse 2s infinite;
}
/* Mode Switch */
.mode-switch {
display: inline-flex;
background: #050505;
border: 1px solid #151515;
border-radius: 10px;
padding: 4px;
}
.mode-btn {
padding: 8px 16px;
font-size: 13px;
font-weight: 700;
color: #888;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
}
.mode-btn:hover:not(.active) {
color: #ccc;
}
.mode-btn.active {
color: #fff;
background: #151515;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
/* Body */
.gc-body {
position: relative;
background: radial-gradient(circle at center, #111 0%, #000 100%);
display: flex;
align-items: center;
justify-content: center;
flex: 1;
transition: all 0.5s ease;
}
.gp-iframe {
width: 100%;
aspect-ratio: 16/9;
min-height: 600px;
max-height: 80vh;
border: none;
background: transparent;
display: block;
transition: all 0.5s ease;
}
/* In cinema mode, make iframe take maximum possible space */
.game-card.cinema .gp-iframe {
height: 80vh;
max-height: 85vh;
aspect-ratio: auto;
}
.game-card.fullscreen .gp-iframe {
height: 100vh;
max-height: none;
aspect-ratio: auto; /* fill the screen */
}
.fs-tools {
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
}
/* Loaders & Errors */
.gp-loading, .gp-error {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(10, 10, 10, 0.9);
backdrop-filter: blur(10px);
z-index: 10;
text-align: center;
}
.loader-ring {
width: 80px; height: 80px;
border: 4px solid rgba(223, 0, 106, 0.1);
border-left-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
position: absolute;
}
.loader-logo {
z-index: 2;
animation: pulse 2s infinite;
}
.loader-text {
margin-top: 60px;
color: #fff; font-size: 15px; font-weight: 600; letter-spacing: 1px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.gp-error h2 { color: #fff; font-size: 20px; font-weight: 800; margin: 0 0 8px; }
.gp-error p { color: #888; font-size: 14px; max-width: 300px; }
/* Stats Overlay */
.gc-stats {
position: absolute;
top: 20px;
left: 20px;
width: 280px;
background: rgba(10, 10, 10, 0.95);
border: 1px solid #222;
border-radius: 16px;
overflow: hidden;
z-index: 20;
backdrop-filter: blur(16px);
box-shadow: 0 15px 40px rgba(0,0,0,0.6);
}
.stats-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: #151515;
border-bottom: 1px solid #222;
}
.stats-title { font-size: 14px; font-weight: 700; color: #fff; display: flex; align-items: center; }
.stats-close { background: transparent; color: #888; border: none; font-size: 20px; cursor: pointer; padding: 0; line-height: 1;}
.stats-close:hover { color: #fff; }
.stats-body { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.stat-box {
background: #0a0a0a; padding: 12px; border-radius: 10px; border: 1px solid #151515;
display: flex; flex-direction: column; gap: 4px;
}
.s-label { font-size: 11px; color: #888; text-transform: uppercase; font-weight: 700; }
.s-val { font-size: 18px; color: #fff; font-weight: 800; font-family: monospace; }
.divider-h { height: 1px; background: #222; margin: 4px 0; }
.stat-row { display: flex; align-items: center; justify-content: space-between; font-size: 13px; }
.stat-row span { color: #888; }
.stat-row b { color: #fff; font-weight: 600; }
.vol-high { color: #ff007a; }
.vol-medium { color: #ffb700; }
.vol-low { color: #00ff9d; }
.slide-fade-enter-active, .slide-fade-leave-active { transition: all 0.3s ease; }
.slide-fade-enter-from, .slide-fade-leave-to { opacity: 0; transform: translateX(-20px); }
/* Footer Toolbar */
.gc-foot {
padding: 16px 24px;
background: #0a0a0a;
border-top: 1px solid #151515;
display: flex;
justify-content: space-between;
align-items: center;
}
.foot-left, .foot-right { display: flex; align-items: center; gap: 8px; flex: 1; }
.foot-right { justify-content: flex-end; }
.tool-btn {
width: 40px; height: 40px;
border-radius: 10px;
background: #111;
border: 1px solid #222;
color: #888;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.tool-btn:hover { color: #fff; background: #151515; transform: translateY(-2px); }
.tool-btn.active { color: var(--primary); border-color: var(--primary); background: rgba(223, 0, 106, 0.1); }
/* CONTENT BELOW SLOT */
.below-content {
display: flex;
flex-direction: column;
gap: 50px;
padding: 0 0 40px;
}
/* EXTRA INFO */
.game-details { width: 100%; }
.detail-card {
background: #0a0a0a; border: 1px solid #151515; border-radius: 20px; padding: 30px; box-shadow: 0 10px 40px rgba(0,0,0,0.5);
}
.section-title { color: #fff; font-size: 18px; font-weight: 800; margin-bottom: 16px; letter-spacing: 1px; text-transform: uppercase;}
.desc-text { color: #888; font-size: 14px; line-height: 1.7; margin-bottom: 30px; max-width: 1000px;}
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; }
.f-item { display: flex; align-items: center; gap: 16px; background: #111; padding: 16px; border-radius: 12px; border: 1px solid #222;}
.f-icon { width: 40px; height: 40px; background: #151515; border-radius: 10px; display: flex; align-items: center; justify-content: center; }
.f-info { display: flex; flex-direction: column; }
.f-info span { font-size: 11px; color: #888; text-transform: uppercase; font-weight: 700; }
.f-info strong { font-size: 14px; color: #fff; font-weight: 600; }
/* Dashboard Style Sections */
.section-header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 20px; }
.section-header h2 { font-size: 18px; font-weight: 800; color: white; text-transform: uppercase; letter-spacing: 1px; margin: 0;}
/* Wins Section */
.wins-section { width: 100%; }
.wins-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 15px; }
.win-card { background: #0a0a0a; border: 1px solid #151515; border-radius: 12px; padding: 12px 15px; display: flex; align-items: center; gap: 12px; transition: all 0.3s; }
.win-card:hover { border-color: #333; background: #111; transform: translateY(-2px); }
.win-icon { flex-shrink: 0; }
.win-details { flex: 1; overflow: hidden; }
.win-user { font-size: 13px; font-weight: 700; color: #fff; }
.win-game { font-size: 11px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.win-value { font-size: 13px; font-weight: 800; color: #00ff9d; text-align: right; }
.wins-list-move, .wins-list-enter-active, .wins-list-leave-active { transition: all 0.5s ease; position: relative; }
.wins-list-enter-from { opacity: 0; transform: translateY(-20px); }
.wins-list-leave-to { opacity: 0; transform: scale(0.9); }
/* Game Grid */
.game-section { width: 100%; }
.game-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
.similar-game-card { background: #0a0a0a; border-radius: 16px; overflow: hidden; border: 1px solid #151515; transition: transform 0.3s, box-shadow 0.3s; cursor: pointer; position: relative; }
.similar-game-card:hover { transform: translateY(-8px); box-shadow: 0 15px 40px rgba(0,0,0,0.6); border-color: #333; z-index: 2; }
.card-image { position: relative; aspect-ratio: 4/3; overflow: hidden; }
.card-image img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s; }
.similar-game-card:hover img { transform: scale(1.1); filter: brightness(0.4); }
.card-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s; }
.similar-game-card:hover .card-overlay { opacity: 1; }
.play-btn { width: 60px; height: 60px; border-radius: 50%; background: transparent; color: white; display: flex; align-items: center; justify-content: center; margin-bottom: 10px; transform: scale(0.8); transition: transform 0.2s; border: none; padding: 0;}
.similar-game-card:hover .play-btn { transform: scale(1); }
.play-btn:hover { filter: brightness(1.2); drop-shadow: 0 0 10px rgba(255,255,255,0.5);}
.provider { font-size: 10px; font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: 1px; }
.card-info { padding: 12px; background: #0a0a0a; border-top: 1px solid #151515; }
.game-name { font-size: 13px; font-weight: 700; color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Responsive */
@media (max-width: 1024px) {
.gc-foot { flex-wrap: wrap; gap: 16px; }
}
@media (max-width: 768px) {
.gameplay-page { padding: 12px; }
.gc-head { flex-direction: column; align-items: flex-start; gap: 16px; }
.gc-right { width: 100%; justify-content: space-between; }
.gp-iframe { aspect-ratio: auto; height: 50vh; }
.foot-left, .foot-right { justify-content: center; width: 100%; }
}
/* Info Modal */
.info-modal-backdrop {
position: fixed;
inset: 0;
z-index: 8000;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.info-modal {
background: #0d0d0d;
border: 1px solid #222;
border-radius: 18px;
width: 100%;
max-width: 520px;
max-height: 88vh;
overflow-y: auto;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.8), 0 0 40px rgba(223, 0, 106, 0.08);
}
.info-modal-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 16px;
border-bottom: 1px solid #1a1a1a;
position: sticky;
top: 0;
background: #0d0d0d;
z-index: 1;
}
.info-modal-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 800;
color: #fff;
}
.info-modal-close {
width: 32px;
height: 32px;
border-radius: 50%;
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #888;
font-size: 18px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.info-modal-close:hover { background: var(--primary); color: #fff; border-color: var(--primary); }
.info-modal-body { padding: 20px 24px 24px; display: flex; flex-direction: column; gap: 20px; }
.info-game-header {
display: flex;
align-items: center;
gap: 14px;
}
.info-thumb {
width: 60px;
height: 60px;
border-radius: 12px;
object-fit: cover;
border: 1px solid #222;
}
.info-game-name { font-size: 17px; font-weight: 800; color: #fff; }
.info-game-prov { font-size: 12px; color: #888; margin-top: 3px; }
.info-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.info-stat {
background: #111;
border: 1px solid #1a1a1a;
border-radius: 10px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 4px;
}
.is-label { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.06em; }
.is-val { font-size: 15px; font-weight: 800; color: #fff; }
.is-val.primary { color: var(--primary); }
.info-section h4 {
font-size: 13px;
font-weight: 800;
color: #aaa;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 0 0 10px;
}
.info-section p {
font-size: 13px;
color: #888;
line-height: 1.6;
margin: 0;
}
.info-rules {
margin: 0;
padding: 0 0 0 16px;
display: flex;
flex-direction: column;
gap: 6px;
}
.info-rules li { font-size: 12px; color: #777; line-height: 1.5; }
.info-rg-text { font-size: 12px; color: #555; }
.info-link { color: var(--primary); text-decoration: none; }
.info-link:hover { text-decoration: underline; }
/* ── FOOT-CENTER CONTROLS ──────────────────────────────────────────────── */
.foot-controls {
display: flex;
align-items: center;
gap: 12px;
}
.foot-divider {
width: 1px;
height: 24px;
background: #222;
flex-shrink: 0;
}
/* Currency selector */
.currency-wrap {
position: relative;
}
.currency-btn {
display: flex;
align-items: center;
gap: 7px;
height: 40px;
padding: 0 12px;
border-radius: 10px;
background: #111;
border: 1px solid #222;
color: #ccc;
cursor: pointer;
font-size: 13px;
font-weight: 700;
transition: all 0.2s;
white-space: nowrap;
}
.currency-btn:hover {
background: #151515;
color: #fff;
border-color: #333;
}
.currency-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.currency-label {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.5px;
}
.currency-chevron {
color: #666;
transition: transform 0.2s;
flex-shrink: 0;
}
.currency-chevron.open {
transform: rotate(180deg);
}
.currency-dropdown {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
background: #0d0d0d;
border: 1px solid #222;
border-radius: 14px;
padding: 6px;
display: flex;
flex-direction: column;
gap: 3px;
min-width: 200px;
box-shadow: 0 20px 50px rgba(0,0,0,0.8);
z-index: 200;
}
.currency-option {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border-radius: 10px;
border: 1px solid transparent;
background: transparent;
color: #888;
cursor: pointer;
text-align: left;
transition: all 0.15s;
width: 100%;
}
.currency-option:hover {
background: #151515;
color: #fff;
border-color: #2a2a2a;
}
.currency-option.active {
background: rgba(223, 0, 106, 0.1);
border-color: rgba(223, 0, 106, 0.3);
color: var(--primary);
}
.currency-option.active .co-desc {
color: rgba(223, 0, 106, 0.5);
}
/* Info modal transition */
.info-modal-enter-active, .info-modal-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }
.info-modal-enter-from, .info-modal-leave-to { opacity: 0; transform: scale(0.95); }
/* ── AMBIENTE MODE ─────────────────────────────────────────────────────── */
/* Fixed ambient glow that fills the entire viewport background */
.ambient-bg {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
background: #000;
}
.ambient-bg::before {
content: '';
position: absolute;
width: 140vmax;
height: 140vmax;
top: 50%;
left: 50%;
background: conic-gradient(
from 0deg at 50% 50%,
rgba(223, 0, 106, 0.08),
rgba(124, 58, 237, 0.06),
rgba(14, 165, 233, 0.05),
rgba(16, 185, 129, 0.04),
rgba(245, 158, 11, 0.05),
rgba(223, 0, 106, 0.08)
);
filter: blur(120px);
animation: ambient-spin 30s linear infinite;
transform: translate(-50%, -50%);
}
.ambient-bg::after {
content: '';
position: absolute;
width: 80vmax;
height: 80vmax;
top: 50%;
left: 50%;
background: conic-gradient(
from 180deg at 50% 50%,
rgba(124, 58, 237, 0.05),
rgba(223, 0, 106, 0.06),
rgba(14, 165, 233, 0.04),
rgba(124, 58, 237, 0.05)
);
filter: blur(100px);
animation: ambient-spin 22s linear infinite reverse;
transform: translate(-50%, -50%);
}
@keyframes ambient-spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
/* The gameplay-page sits above the ambient background */
.gameplay-page.ambiente-active {
position: relative;
z-index: 1;
}
/* Card in ambiente: no head/foot borders, glass feel, animated edge glow */
.game-card.ambiente {
background: rgba(5, 5, 5, 0.85);
border-color: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(24px);
animation: card-ambient-glow 5s ease-in-out infinite alternate;
}
@keyframes card-ambient-glow {
from { box-shadow: 0 0 40px rgba(0,0,0,0.9), 0 0 30px rgba(223, 0, 106, 0.12); }
to { box-shadow: 0 0 70px rgba(0,0,0,0.95), 0 0 60px rgba(223, 0, 106, 0.28), 0 0 100px rgba(124, 58, 237, 0.14); }
}
/* Ambiente exit button top-right, distinct from cinema exit */
.ambiente-exit-btn {
background: rgba(223, 0, 106, 0.12);
border-color: rgba(223, 0, 106, 0.35);
color: var(--primary);
padding: 8px;
gap: 0;
}
.ambiente-exit-btn:hover {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
/* ── CINEMA DROPDOWN ───────────────────────────────────────────────────── */
.cinema-btn-wrap {
position: relative;
}
.cinema-dropdown {
position: absolute;
bottom: calc(100% + 10px);
right: 0;
background: #0d0d0d;
border: 1px solid #222;
border-radius: 14px;
padding: 6px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 190px;
box-shadow: 0 20px 50px rgba(0,0,0,0.8), 0 0 30px rgba(0,0,0,0.5);
z-index: 200;
}
.cinema-option {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid transparent;
background: transparent;
color: #888;
cursor: pointer;
text-align: left;
transition: all 0.2s;
width: 100%;
}
.cinema-option:hover {
background: #151515;
color: #fff;
border-color: #2a2a2a;
}
.cinema-option.active {
background: rgba(223, 0, 106, 0.1);
border-color: rgba(223, 0, 106, 0.3);
color: var(--primary);
}
.co-title { font-size: 13px; font-weight: 700; line-height: 1.2; }
.co-desc { font-size: 11px; color: #555; margin-top: 2px; }
.cinema-option.active .co-desc { color: rgba(223, 0, 106, 0.6); }
.cinema-menu-enter-active, .cinema-menu-leave-active { transition: opacity 0.15s ease, transform 0.15s ease; }
.cinema-menu-enter-from, .cinema-menu-leave-to { opacity: 0; transform: translateY(6px); }
</style>