Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1020 lines
38 KiB
HTML
1020 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Vanilla Game – Im Spiel – xXx Sphere</title>
|
||
<link rel="stylesheet" href="/css/variables.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
<style>
|
||
.game-overview { }
|
||
|
||
.overview-section { margin-bottom: 2rem; }
|
||
.overview-section h2 {
|
||
color: var(--color-primary);
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
margin-bottom: 1rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid var(--color-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.kv-table { width: 100%; border-collapse: collapse; }
|
||
.kv-table td { padding: 0.35rem 0; font-size: 0.9rem; vertical-align: top; }
|
||
.kv-table td:first-child { color: var(--color-muted); width: 200px; }
|
||
|
||
.player-card {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 8px;
|
||
padding: 0.75rem 1rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
.player-name { font-weight: 600; margin-bottom: 0.4rem; }
|
||
.player-sub { font-size: 0.8rem; color: var(--color-muted); }
|
||
|
||
.tag-list { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.25rem; }
|
||
.tag {
|
||
background: var(--color-secondary);
|
||
border-radius: 6px;
|
||
padding: 0.2rem 0.6rem;
|
||
font-size: 0.8rem;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.count-row { display: flex; gap: 2.5rem; }
|
||
.count-item { text-align: center; }
|
||
.count-num { font-size: 2rem; font-weight: 700; color: var(--color-primary); }
|
||
.count-label { font-size: 0.75rem; color: var(--color-muted); margin-top: 0.1rem; }
|
||
|
||
/* ── Modal ── */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.75);
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 1.5rem;
|
||
}
|
||
.modal-card {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 14px;
|
||
padding: 2rem 2rem 1.75rem;
|
||
max-width: 460px;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
.modal-title {
|
||
font-size: 1.15rem;
|
||
font-weight: 700;
|
||
color: var(--color-text);
|
||
margin-bottom: 0.75rem;
|
||
line-height: 1.4;
|
||
}
|
||
.modal-text {
|
||
font-size: 0.95rem;
|
||
color: var(--color-muted);
|
||
line-height: 1.65;
|
||
margin-bottom: 1.5rem;
|
||
white-space: pre-wrap;
|
||
}
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
justify-content: center;
|
||
}
|
||
.modal-actions button { min-width: 110px; }
|
||
|
||
/* ── Aufgaben-Karte ── */
|
||
.task-card {
|
||
background: var(--color-card);
|
||
border: 1px solid var(--color-secondary);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
height: 260px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.task-card.loading {
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--color-muted);
|
||
font-style: italic;
|
||
}
|
||
.task-player-badge {
|
||
display: inline-block;
|
||
background: var(--color-primary);
|
||
color: #fff;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
padding: 0.2rem 0.7rem;
|
||
border-radius: 10px;
|
||
margin-bottom: 0.85rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.task-text {
|
||
flex: 1;
|
||
font-size: 1.05rem;
|
||
line-height: 1.7;
|
||
color: var(--color-text);
|
||
overflow: hidden;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 4;
|
||
-webkit-box-orient: vertical;
|
||
cursor: default;
|
||
}
|
||
.task-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.75rem;
|
||
margin-top: 1rem;
|
||
padding-top: 0.75rem;
|
||
border-top: 1px solid var(--color-secondary);
|
||
flex-shrink: 0;
|
||
}
|
||
.task-btns {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
align-items: center;
|
||
}
|
||
.task-timer-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
.timer-big {
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
color: var(--color-primary);
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1;
|
||
min-width: 3.5rem;
|
||
}
|
||
.timer-big.expired { color: var(--color-muted); }
|
||
.btn-sm-cancel {
|
||
background: transparent;
|
||
border: 1px solid var(--color-secondary);
|
||
color: var(--color-muted);
|
||
font-size: 0.75rem;
|
||
padding: 0.25rem 0.6rem;
|
||
font-weight: normal;
|
||
border-radius: 4px;
|
||
}
|
||
.btn-sm-cancel:hover {
|
||
border-color: var(--color-primary);
|
||
color: var(--color-primary);
|
||
background: transparent;
|
||
}
|
||
.btn-session-beenden {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--color-muted);
|
||
font-size: 0.78rem;
|
||
padding: 0;
|
||
font-weight: normal;
|
||
cursor: pointer;
|
||
text-decoration: underline;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
.btn-session-beenden:hover {
|
||
color: var(--color-text);
|
||
background: transparent;
|
||
}
|
||
|
||
/* ── Debug-Panel ── */
|
||
.debug-panel {
|
||
margin-top: 2rem;
|
||
border: 1px dashed var(--color-secondary);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
.debug-toggle {
|
||
width: 100%;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 0;
|
||
padding: 0.6rem 1rem;
|
||
font-size: 0.75rem;
|
||
color: var(--color-muted);
|
||
font-weight: normal;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.debug-toggle:hover { background: transparent; color: var(--color-text); }
|
||
.debug-toggle .arrow { transition: transform 0.2s; display: inline-block; }
|
||
.debug-toggle.open .arrow { transform: rotate(90deg); }
|
||
.debug-body {
|
||
display: none;
|
||
padding: 1rem;
|
||
border-top: 1px dashed var(--color-secondary);
|
||
}
|
||
.debug-body.open { display: block; }
|
||
.debug-section { margin-bottom: 1.25rem; }
|
||
.debug-section:last-child { margin-bottom: 0; }
|
||
.debug-section-title {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--color-primary);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
.debug-kv { font-size: 0.8rem; line-height: 1.8; }
|
||
.debug-kv span.k { color: var(--color-muted); display: inline-block; min-width: 190px; }
|
||
.debug-kv span.v { color: var(--color-text); font-family: monospace; }
|
||
.debug-card {
|
||
background: var(--color-secondary);
|
||
border-radius: 7px;
|
||
padding: 0.6rem 0.8rem;
|
||
margin-bottom: 0.5rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
.debug-card:last-child { margin-bottom: 0; }
|
||
.debug-card-name {
|
||
font-weight: 600;
|
||
margin-bottom: 0.3rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
.debug-badge {
|
||
background: var(--color-primary);
|
||
color: #fff;
|
||
font-size: 0.65rem;
|
||
border-radius: 5px;
|
||
padding: 0.1rem 0.4rem;
|
||
}
|
||
.debug-badge.warn { background: #b45309; }
|
||
.debug-tag-row { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.2rem; }
|
||
.debug-tag {
|
||
background: rgba(255,255,255,0.06);
|
||
border-radius: 4px;
|
||
padding: 0.1rem 0.45rem;
|
||
font-size: 0.72rem;
|
||
font-family: monospace;
|
||
}
|
||
.debug-refresh {
|
||
font-size: 0.72rem;
|
||
color: var(--color-muted);
|
||
background: transparent;
|
||
border: none;
|
||
padding: 0;
|
||
cursor: pointer;
|
||
margin-left: auto;
|
||
font-weight: normal;
|
||
}
|
||
.debug-refresh:hover { color: var(--color-text); background: transparent; }
|
||
|
||
/* ── Level-Anzeige ── */
|
||
.level-display {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.level-display img {
|
||
width: 72px;
|
||
height: 72px;
|
||
object-fit: contain;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="app">
|
||
|
||
<!-- ── Modal ── -->
|
||
<div class="modal-overlay" id="modal" style="display:none;">
|
||
<div class="modal-card">
|
||
<div class="modal-title" id="modalTitle"></div>
|
||
<div class="modal-text" id="modalText"></div>
|
||
<div class="modal-actions" id="modalActions"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<div class="content game-overview">
|
||
|
||
<!-- ── Level ── -->
|
||
<div class="level-display" id="levelDisplay" style="display:none;">
|
||
<img id="levelImg" src="" alt="Level">
|
||
</div>
|
||
|
||
<!-- ── Aufgabe ── -->
|
||
<div style="margin-bottom:2rem;">
|
||
<div class="task-card loading" id="taskCard">Aufgabe wird geladen…</div>
|
||
<div class="message" id="taskMessage" style="display:none; margin-top:0.75rem;"></div>
|
||
</div>
|
||
|
||
<!-- ── Debug-Perspektive ── -->
|
||
<div class="debug-panel" id="debugPanel">
|
||
<button class="debug-toggle" id="debugToggle" onclick="toggleDebug()">
|
||
<span class="arrow">▶</span> DEBUG – Entity-Zustand
|
||
<button class="debug-refresh" onclick="event.stopPropagation(); ladeDebug()">⟳ Aktualisieren</button>
|
||
</button>
|
||
<div class="debug-body" id="debugBody">
|
||
<div id="debugContent" style="color:var(--color-muted);font-size:0.8rem;">Wird geladen…</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/icons.js"></script>
|
||
<script src="/js/nav.js"></script>
|
||
<script>
|
||
const game = JSON.parse(sessionStorage.getItem('vanilla-session-game') || 'null');
|
||
const setup = JSON.parse(sessionStorage.getItem('vanilla-session-setup') || 'null');
|
||
const toys = JSON.parse(sessionStorage.getItem('vanilla-session-toys') || '[]');
|
||
const sessionId = sessionStorage.getItem('vanilla-session-id');
|
||
if (!sessionId) window.location.replace('/games/vanilla/neuvanilla.html');
|
||
|
||
// Multi-Device: bin ich Gast?
|
||
const isGuest = sessionStorage.getItem('vanilla-is-guest') === 'true';
|
||
let myMitspielerId = sessionStorage.getItem('vanilla-guest-mitspieler-id') || null;
|
||
let guestPollInterval = null;
|
||
|
||
// ── Modal ──
|
||
function zeigeModal(title, text, actions) {
|
||
document.getElementById('modalTitle').textContent = title;
|
||
const textEl = document.getElementById('modalText');
|
||
textEl.textContent = text;
|
||
textEl.style.display = text ? '' : 'none';
|
||
const actEl = document.getElementById('modalActions');
|
||
actEl.innerHTML = '';
|
||
actions.forEach(a => {
|
||
const btn = document.createElement('button');
|
||
btn.textContent = a.label;
|
||
if (!a.primary) btn.classList.add('secondary');
|
||
btn.onclick = () => { versteckeModal(); a.onClick(); };
|
||
actEl.appendChild(btn);
|
||
});
|
||
document.getElementById('modal').style.display = 'flex';
|
||
}
|
||
|
||
function versteckeModal() {
|
||
document.getElementById('modal').style.display = 'none';
|
||
}
|
||
|
||
// ── Hilfsfunktionen ──
|
||
function escapeAttr(str) {
|
||
return (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
function badgeHtml(name) {
|
||
return name ? `<div class="task-player-badge">${name} ist dran</div>` : '';
|
||
}
|
||
|
||
const SESSION_BEENDEN_BTN = `<button class="btn-session-beenden" onclick="sessionBeendenFragen()">Session beenden</button>`;
|
||
const SPIEL_VERLASSEN_BTN = `<button class="btn-session-beenden" onclick="spielVerlassenFragen()">Spiel verlassen</button>`;
|
||
|
||
function playSound(src) {
|
||
try { new Audio(src).play().catch(() => {}); } catch (_) {}
|
||
}
|
||
|
||
// ── Aufgaben-Logik ──
|
||
let currentTask = null;
|
||
let timerInterval = null;
|
||
|
||
function formatTime(sec) {
|
||
const m = Math.floor(sec / 60);
|
||
const s = sec % 60;
|
||
return `${m}:${String(s).padStart(2, '0')}`;
|
||
}
|
||
|
||
function clearTimer() {
|
||
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||
stopHostPoll();
|
||
}
|
||
|
||
function zeigeTaskFehler(text) {
|
||
const el = document.getElementById('taskMessage');
|
||
el.textContent = text;
|
||
el.className = 'message error';
|
||
el.style.display = '';
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card';
|
||
card.innerHTML = `
|
||
<div style="flex:1;"></div>
|
||
<div class="task-footer">
|
||
<div class="task-btns">
|
||
<button onclick="ladeAufgabe()">Erneut versuchen</button>
|
||
</div>
|
||
${SESSION_BEENDEN_BTN}
|
||
</div>`;
|
||
}
|
||
|
||
async function ladeAufgabe() {
|
||
clearTimer();
|
||
document.getElementById('taskMessage').style.display = 'none';
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card loading';
|
||
card.innerHTML = 'Aufgabe wird geladen…';
|
||
|
||
try {
|
||
const res = await fetch(`/vanilla/${sessionId}/aufgaben/next`);
|
||
if (res.status === 204) { zeigeFinaleDialog(); return; }
|
||
if (res.status === 400) { window.location.replace('/games/vanilla/neuvanilla.html'); return; }
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
currentTask = await res.json();
|
||
await saveAktiveAufgabe(currentTask, null);
|
||
if (currentTask.level) {
|
||
document.getElementById('levelImg').src = `/img/lvl${currentTask.level}.png`;
|
||
document.getElementById('levelDisplay').style.display = '';
|
||
}
|
||
if (currentTask.eigenesGeraet && currentTask.mitspielerId) {
|
||
zeigeAufgabe();
|
||
} else {
|
||
playSound('/audio/ping.mp3');
|
||
const name = currentTask.nameAktiverMitspieler || '';
|
||
const title = name ? `${name}, du bist an der Reihe` : 'Du bist an der Reihe';
|
||
zeigeModal(title, '', [{ label: 'OK', primary: true, onClick: zeigeAufgabe }]);
|
||
}
|
||
} catch (e) {
|
||
zeigeTaskFehler('Aufgabe konnte nicht geladen werden: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ── Aktive Aufgabe persistieren ──
|
||
async function saveAktiveAufgabe(task, timerStartedAt) {
|
||
try {
|
||
await fetch(`/vanilla/${sessionId}/active-task`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ taskJson: JSON.stringify(task), timerStartedAt }),
|
||
});
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function checkAktiveAufgabe() {
|
||
try {
|
||
const res = await fetch(`/vanilla/${sessionId}/active-task`);
|
||
if (res.status === 204 || !res.ok) { ladeAufgabe(); return; }
|
||
const data = await res.json();
|
||
currentTask = JSON.parse(data.taskJson);
|
||
if (currentTask.level) {
|
||
document.getElementById('levelImg').src = `/img/lvl${currentTask.level}.png`;
|
||
document.getElementById('levelDisplay').style.display = '';
|
||
}
|
||
if (data.elapsedSeconds !== null && data.elapsedSeconds !== undefined) {
|
||
const remaining = currentTask.timer - data.elapsedSeconds;
|
||
if (remaining <= 0) {
|
||
aufgabeAbschliessen();
|
||
} else {
|
||
restoreTimer(Math.floor(remaining));
|
||
}
|
||
} else {
|
||
zeigeAufgabe();
|
||
}
|
||
} catch (_) { ladeAufgabe(); }
|
||
}
|
||
|
||
function restoreTimer(remaining) {
|
||
const task = currentTask;
|
||
if (task.level) {
|
||
document.getElementById('levelImg').src = `/img/lvl${task.level}.png`;
|
||
document.getElementById('levelDisplay').style.display = '';
|
||
}
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card';
|
||
card.innerHTML = `
|
||
${badgeHtml(task.nameAktiverMitspieler)}
|
||
<div class="task-text" title="${escapeAttr(task.aufgabeText)}">${task.aufgabeText}</div>
|
||
<div class="task-footer">
|
||
<div class="task-timer-row" id="taskActions">
|
||
<div class="timer-big" id="timerValue">${formatTime(remaining)}</div>
|
||
<button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>
|
||
</div>
|
||
${SESSION_BEENDEN_BTN}
|
||
</div>`;
|
||
let rem = remaining;
|
||
timerInterval = setInterval(() => {
|
||
rem--;
|
||
const el = document.getElementById('timerValue');
|
||
if (rem <= 0) {
|
||
clearTimer();
|
||
if (el) { el.textContent = formatTime(0); el.classList.add('expired'); }
|
||
playSound('/audio/alarm.mp3');
|
||
aufgabeAbschliessen();
|
||
} else {
|
||
if (el) el.textContent = formatTime(rem);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// ── Aufgabe abschließen (vereinfacht – keine Sperre-Logik) ──
|
||
async function aufgabeAbschliessen() {
|
||
clearTimer();
|
||
await fetch(`/vanilla/${sessionId}/active-task/abschliessen`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sperreAnwenden: false })
|
||
}).catch(() => {});
|
||
naechsteAufgabe();
|
||
}
|
||
|
||
function naechsteAufgabe() {
|
||
if (isGuest) startGastPoll();
|
||
else ladeAufgabe();
|
||
}
|
||
|
||
// ── Gast-Polling: wartet auf aktive Aufgabe für mein Gerät ──
|
||
async function startGastPoll() {
|
||
if (guestPollInterval) return;
|
||
if (!myMitspielerId) {
|
||
try {
|
||
const r = await fetch(`/vanilla/${sessionId}/mitspieler/me`);
|
||
if (r.status === 200) {
|
||
const d = await r.json();
|
||
myMitspielerId = d.mitspielerId;
|
||
sessionStorage.setItem('vanilla-guest-mitspieler-id', myMitspielerId);
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card loading';
|
||
card.innerHTML = `
|
||
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--color-muted);">Warte auf Aufgabe…</div>
|
||
<div class="task-footer"><div class="task-btns"></div>${SPIEL_VERLASSEN_BTN}</div>`;
|
||
guestPollInterval = setInterval(pollGastAufgabe, 2500);
|
||
}
|
||
|
||
async function pollGastAufgabe() {
|
||
try {
|
||
const res = await fetch(`/vanilla/${sessionId}/active-task`);
|
||
if (res.status === 404) { await spielSessionEnde(); return; }
|
||
if (res.status === 204) {
|
||
const card = document.getElementById('taskCard');
|
||
if (!card.classList.contains('loading')) {
|
||
card.className = 'task-card loading';
|
||
card.innerHTML = `
|
||
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--color-muted);">Warte auf Aufgabe…</div>
|
||
<div class="task-footer"><div class="task-btns"></div>${SPIEL_VERLASSEN_BTN}</div>`;
|
||
}
|
||
return;
|
||
}
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
const task = JSON.parse(data.taskJson);
|
||
|
||
if (task.mitspielerId && task.mitspielerId === myMitspielerId) {
|
||
clearInterval(guestPollInterval); guestPollInterval = null;
|
||
|
||
currentTask = task;
|
||
if (task.level) {
|
||
document.getElementById('levelImg').src = `/img/lvl${task.level}.png`;
|
||
document.getElementById('levelDisplay').style.display = '';
|
||
}
|
||
if (data.elapsedSeconds !== null && data.elapsedSeconds !== undefined && task.timer != null) {
|
||
const remaining = task.timer - data.elapsedSeconds;
|
||
if (remaining <= 0) { aufgabeAbschliessen(); }
|
||
else restoreTimer(Math.floor(remaining));
|
||
} else {
|
||
playSound('/audio/ping.mp3');
|
||
const name = task.nameAktiverMitspieler || '';
|
||
const title = name ? `${name}, du bist dran` : 'Du bist dran';
|
||
zeigeModal(title, '', [{ label: 'OK', primary: true, onClick: () => zeigeGastAufgabe(task) }]);
|
||
}
|
||
} else {
|
||
const card = document.getElementById('taskCard');
|
||
const name = task.nameAktiverMitspieler || 'Jemand';
|
||
card.className = 'task-card loading';
|
||
card.innerHTML = `<div style="font-size:1rem;color:var(--color-muted);">${name} ist an der Reihe…</div>`;
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
function zeigeGastAufgabe(task) {
|
||
if (task.timer != null) zeigeTimerAufgabe(task);
|
||
else zeigeEinfacheAufgabe(task);
|
||
}
|
||
|
||
// ── Host: wenn Aufgabe einem eigenem-Gerät-Spieler gehört ──
|
||
let hostPollInterval = null;
|
||
let _hostPollComplete = null;
|
||
|
||
function startHostPoll(onComplete) {
|
||
_hostPollComplete = onComplete || ladeAufgabe;
|
||
if (hostPollInterval) return;
|
||
hostPollInterval = setInterval(pollHostAktiv, 2500);
|
||
}
|
||
|
||
function stopHostPoll() {
|
||
if (hostPollInterval) { clearInterval(hostPollInterval); hostPollInterval = null; }
|
||
}
|
||
|
||
async function pollHostAktiv() {
|
||
try {
|
||
const res = await fetch(`/vanilla/${sessionId}/active-task`);
|
||
if (res.status === 404) { await spielSessionEnde(); return; }
|
||
if (res.status === 204) {
|
||
stopHostPoll();
|
||
const next = _hostPollComplete || ladeAufgabe;
|
||
_hostPollComplete = null;
|
||
next();
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function zeigeAufgabe() {
|
||
const task = currentTask;
|
||
await saveAktiveAufgabe(task, null);
|
||
|
||
if (!isGuest && task.eigenesGeraet && task.mitspielerId) {
|
||
const name = task.nameAktiverMitspieler || 'Jemand';
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card';
|
||
card.innerHTML = `
|
||
<div style="flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:0.5rem;">
|
||
<div style="font-size:1rem;color:var(--color-muted);text-align:center;">${name} ist an der Reihe…</div>
|
||
</div>
|
||
<div class="task-footer">
|
||
<div class="task-btns"></div>
|
||
${SESSION_BEENDEN_BTN}
|
||
</div>`;
|
||
startHostPoll();
|
||
return;
|
||
}
|
||
|
||
if (task.timer != null) zeigeTimerAufgabe(task);
|
||
else zeigeEinfacheAufgabe(task);
|
||
}
|
||
|
||
function renderCard(task, footerInner) {
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card';
|
||
card.innerHTML = `
|
||
${badgeHtml(task.nameAktiverMitspieler)}
|
||
<div class="task-text" title="${escapeAttr(task.aufgabeText)}">${task.aufgabeText}</div>
|
||
<div class="task-footer">
|
||
<div class="task-btns" id="taskActions">${footerInner}</div>
|
||
${isGuest ? SPIEL_VERLASSEN_BTN : SESSION_BEENDEN_BTN}
|
||
</div>`;
|
||
}
|
||
|
||
function zeigeTimerAufgabe(task) {
|
||
renderCard(task, `<button onclick="timerStarten()">Starten</button>`);
|
||
}
|
||
|
||
function timerStarten() {
|
||
const task = currentTask;
|
||
const actions = document.getElementById('taskActions');
|
||
let remaining = task.timer;
|
||
actions.className = 'task-timer-row';
|
||
actions.innerHTML = `
|
||
<div class="timer-big" id="timerValue">${formatTime(remaining)}</div>
|
||
<button class="btn-sm-cancel" onclick="timerAbbrechen()">✕ Abbrechen</button>`;
|
||
saveAktiveAufgabe(task, new Date().toISOString());
|
||
|
||
timerInterval = setInterval(() => {
|
||
remaining--;
|
||
const el = document.getElementById('timerValue');
|
||
if (remaining <= 0) {
|
||
clearTimer();
|
||
if (el) { el.textContent = formatTime(0); el.classList.add('expired'); }
|
||
playSound('/audio/alarm.mp3');
|
||
aufgabeAbschliessen();
|
||
} else {
|
||
if (el) el.textContent = formatTime(remaining);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function timerAbbrechen() {
|
||
clearTimer();
|
||
aufgabeAbschliessen();
|
||
}
|
||
|
||
function zeigeEinfacheAufgabe(task) {
|
||
renderCard(task, `<button onclick="aufgabeAbschliessen()">Erledigt</button>`);
|
||
}
|
||
|
||
// ── Finale ──
|
||
let _finisherListe = [];
|
||
let _finisherIndex = 0;
|
||
|
||
function zeigeFinaleDialog() {
|
||
clearTimer();
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card loading';
|
||
card.innerHTML = 'Level 5 abgeschlossen…';
|
||
zeigeModal(
|
||
'Level 5 abgeschlossen!',
|
||
'Seid ihr bereit für das große Finale?',
|
||
[
|
||
{ label: 'Ja, Finale!', primary: true, onClick: starteFinale },
|
||
{ label: 'Nein, weiter spielen', onClick: zurueckZuLevel5 },
|
||
]
|
||
);
|
||
}
|
||
|
||
async function zurueckZuLevel5() {
|
||
try {
|
||
await fetch(`/vanilla/${sessionId}/backToLevel5`, { method: 'POST' });
|
||
} catch (_) {}
|
||
ladeAufgabe();
|
||
}
|
||
|
||
async function starteFinale() {
|
||
ladeFinisher();
|
||
}
|
||
|
||
async function ladeFinisher() {
|
||
try {
|
||
const res = await fetch(`/vanilla/${sessionId}/finisher`);
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const liste = await res.json();
|
||
naechsterFinisher(liste, 0);
|
||
} catch (e) {
|
||
zeigeTaskFehler('Finisher konnten nicht geladen werden: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function naechsterFinisher(liste, index) {
|
||
_finisherListe = liste;
|
||
_finisherIndex = index;
|
||
if (index >= liste.length) {
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card loading';
|
||
card.innerHTML = '';
|
||
zeigeFinaleAbgeschlossen();
|
||
return;
|
||
}
|
||
const finisher = liste[index];
|
||
const name = finisher.nameAktiverMitspieler || '';
|
||
zeigeModal(
|
||
name ? `${name} ist dran` : 'Finale',
|
||
'',
|
||
[{ label: 'OK', primary: true, onClick: zeigeFinisherAufgabe }]
|
||
);
|
||
}
|
||
|
||
async function zeigeFinisherAufgabe() {
|
||
const finisher = _finisherListe[_finisherIndex];
|
||
|
||
// Finisher für eigenesGeraet-Spieler: per active-task ans Gast-Gerät übertragen
|
||
if (!isGuest && finisher.eigenesGeraet && finisher.mitspielerId) {
|
||
await saveAktiveAufgabe(finisher, null);
|
||
const name = finisher.nameAktiverMitspieler || 'Jemand';
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card';
|
||
card.innerHTML = `
|
||
<div style="flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:0.5rem;">
|
||
<div style="font-size:1.8rem;">📱</div>
|
||
<div style="font-size:1rem;color:var(--color-muted);text-align:center;">${name} spielt Finale auf dem eigenen Gerät.</div>
|
||
</div>
|
||
<div class="task-footer"><div class="task-btns"></div></div>`;
|
||
startHostPoll(() => naechsterFinisher(_finisherListe, _finisherIndex + 1));
|
||
return;
|
||
}
|
||
|
||
const card = document.getElementById('taskCard');
|
||
card.className = 'task-card';
|
||
card.innerHTML = `
|
||
${badgeHtml(finisher.nameAktiverMitspieler)}
|
||
<div class="task-text" title="${escapeAttr(finisher.aufgabeText)}">${finisher.aufgabeText}</div>
|
||
<div class="task-footer">
|
||
<div class="task-btns">
|
||
<button onclick="naechsterFinisher(_finisherListe, _finisherIndex + 1)">Erledigt</button>
|
||
</div>
|
||
${isGuest ? SPIEL_VERLASSEN_BTN : SESSION_BEENDEN_BTN}
|
||
</div>`;
|
||
}
|
||
|
||
function zeigeFinaleAbgeschlossen() {
|
||
zeigeModal(
|
||
'Das Finale ist abgeschlossen!',
|
||
'Wir hoffen, ihr hattet viel Spaß! 🎉',
|
||
[{ label: 'Session beenden', primary: true, onClick: spielAbschliessen }]
|
||
);
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ── Session beenden ──
|
||
const VANILLA_STORAGE_KEYS = [
|
||
'vanilla-session-id', 'vanilla-session-settings', 'vanilla-session-setup',
|
||
'vanilla-session-gruppen', 'vanilla-session-toys', 'vanilla-session-game',
|
||
];
|
||
|
||
function sessionBeendenFragen() {
|
||
zeigeModal(
|
||
'Wirklich beenden?',
|
||
'Möchtest du die aktive Session wirklich beenden?',
|
||
[
|
||
{ label: 'Ja, beenden', primary: true, onClick: () => { versteckeModal(); sessionLoeschen(); } },
|
||
{ label: 'Nein', onClick: versteckeModal },
|
||
]
|
||
);
|
||
}
|
||
|
||
async function spielAbschliessen() {
|
||
versteckeModal();
|
||
try {
|
||
await fetch(`/vanilla/${sessionId}/abgeschlossen`, { method: 'POST' });
|
||
} catch (_) {}
|
||
VANILLA_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
window.location.href = '/userhome.html';
|
||
}
|
||
|
||
async function sessionLoeschen() {
|
||
versteckeModal();
|
||
try {
|
||
await fetch('/vanilla', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sessionId }),
|
||
});
|
||
} catch (_) {}
|
||
VANILLA_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
window.location.href = '/games/vanilla/neuvanilla.html';
|
||
}
|
||
|
||
// ── Spiel verlassen (Mitspieler) ──
|
||
function spielVerlassenFragen() {
|
||
zeigeModal(
|
||
'Spiel wirklich verlassen?',
|
||
'Du verlässt das Spiel. Alle Mitspieler werden benachrichtigt und das Spiel wird für alle beendet.',
|
||
[
|
||
{ label: 'Ja, verlassen', primary: true, onClick: spielVerlassen },
|
||
{ label: 'Nein', onClick: versteckeModal },
|
||
]
|
||
);
|
||
}
|
||
|
||
async function spielVerlassen() {
|
||
versteckeModal();
|
||
try {
|
||
await fetch(`/vanilla/${sessionId}/verlassen`, { method: 'DELETE' });
|
||
} catch (_) {}
|
||
VANILLA_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
window.location.href = '/userhome.html';
|
||
}
|
||
|
||
async function spielSessionEnde() {
|
||
if (guestPollInterval) { clearInterval(guestPollInterval); guestPollInterval = null; }
|
||
stopHostPoll();
|
||
stopLevelPoll();
|
||
clearTimer();
|
||
VANILLA_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
let ordentlich = false;
|
||
try {
|
||
const r = await fetch(`/vanilla/${sessionId}/beendet`);
|
||
ordentlich = r.ok;
|
||
} catch (_) {}
|
||
if (ordentlich) {
|
||
zeigeModal(
|
||
'Spiel beendet',
|
||
'Das Spiel wurde erfolgreich abgeschlossen. Danke fürs Mitspielen! 🎉',
|
||
[{ label: 'Zur Startseite', primary: true, onClick: () => window.location.href = '/userhome.html' }]
|
||
);
|
||
} else {
|
||
zeigeModal(
|
||
'Spiel abgebrochen',
|
||
'Das Spiel wurde vorzeitig beendet.',
|
||
[{ label: 'Zur Startseite', primary: true, onClick: () => window.location.href = '/userhome.html' }]
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── Level regelmäßig aktualisieren ──
|
||
let levelPollInterval = null;
|
||
let lastKnownLevel = null;
|
||
|
||
async function fetchAndShowLevel() {
|
||
try {
|
||
const res = await fetch(`/vanilla/${sessionId}`);
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
if (data.level) {
|
||
document.getElementById('levelImg').src = `/img/lvl${data.level}.png`;
|
||
document.getElementById('levelDisplay').style.display = '';
|
||
if (lastKnownLevel !== null && data.level > lastKnownLevel) {
|
||
playSound('/audio/lvlup.mp3');
|
||
}
|
||
lastKnownLevel = data.level;
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
function startLevelPoll() {
|
||
if (levelPollInterval) return;
|
||
fetchAndShowLevel();
|
||
levelPollInterval = setInterval(fetchAndShowLevel, 10000);
|
||
}
|
||
|
||
function stopLevelPoll() {
|
||
if (levelPollInterval) { clearInterval(levelPollInterval); levelPollInterval = null; }
|
||
}
|
||
|
||
// ── Debug-Perspektive ──
|
||
let debugOpen = false;
|
||
let debugAutoRefresh = null;
|
||
|
||
function toggleDebug() {
|
||
debugOpen = !debugOpen;
|
||
const toggle = document.getElementById('debugToggle');
|
||
const body = document.getElementById('debugBody');
|
||
toggle.classList.toggle('open', debugOpen);
|
||
body.classList.toggle('open', debugOpen);
|
||
if (debugOpen) {
|
||
ladeDebug();
|
||
debugAutoRefresh = setInterval(ladeDebug, 5000);
|
||
} else {
|
||
if (debugAutoRefresh) { clearInterval(debugAutoRefresh); debugAutoRefresh = null; }
|
||
}
|
||
}
|
||
|
||
function fmt(val) {
|
||
if (val === null || val === undefined) return '<span style="color:var(--color-muted)">—</span>';
|
||
if (typeof val === 'boolean') return val ? '<span style="color:#86efac">true</span>' : '<span style="color:#fca5a5">false</span>';
|
||
return String(val);
|
||
}
|
||
|
||
function kv(key, val) {
|
||
return `<div class="debug-kv"><span class="k">${key}</span><span class="v">${fmt(val)}</span></div>`;
|
||
}
|
||
|
||
async function ladeDebug() {
|
||
if (!debugOpen) return;
|
||
try {
|
||
const res = await fetch(`/vanilla/${sessionId}/debug`);
|
||
if (!res.ok) { document.getElementById('debugContent').textContent = `Fehler: HTTP ${res.status}`; return; }
|
||
const data = await res.json();
|
||
renderDebug(data);
|
||
} catch (e) {
|
||
document.getElementById('debugContent').textContent = 'Fehler: ' + e.message;
|
||
}
|
||
}
|
||
|
||
function renderDebug(data) {
|
||
const s = data.session;
|
||
let html = '';
|
||
|
||
// ── Session ──
|
||
html += `<div class="debug-section">
|
||
<div class="debug-section-title">VanillaGameEntity – Session</div>
|
||
${kv('sessionId', s.sessionId)}
|
||
${kv('userId', s.userId)}
|
||
${kv('setupId', s.setupId)}
|
||
${kv('startZeit', s.startZeit)}
|
||
${kv('letzteAktivitaet', s.letzteAktivitaet)}
|
||
${kv('level', s.level)}
|
||
${kv('aufgabenAufAktuellemLevel', s.aufgabenAufAktuellemLevel + ' / ' + s.aufgabenProLevel)}
|
||
${kv('hatAufgaben', s.hatAufgaben)}
|
||
${kv('hatActiveTask', s.hatActiveTask)}
|
||
${kv('taskStartedAt', s.taskStartedAt)}
|
||
</div>`;
|
||
|
||
// ── Mitspieler ──
|
||
html += `<div class="debug-section"><div class="debug-section-title">MitspielerEntity (${data.mitspieler.length})</div>`;
|
||
if (data.mitspieler.length === 0) {
|
||
html += `<div style="color:var(--color-muted);font-size:0.8rem;">Keine Mitspieler</div>`;
|
||
}
|
||
data.mitspieler.forEach(m => {
|
||
const werkzeugTags = (m.werkzeuge || []).map(w => `<span class="debug-tag">${w}</span>`).join('');
|
||
const rollenTags = (m.rollen || []).map(r => `<span class="debug-tag">${r}</span>`).join('');
|
||
const spieltMitTags= (m.spieltMit || []).map(g => `<span class="debug-tag">${g}</span>`).join('');
|
||
html += `<div class="debug-card">
|
||
<div class="debug-card-name">
|
||
${escapeHtml(m.name)}
|
||
<span class="debug-badge">${m.geschlecht || '?'}</span>
|
||
${m.eigenesGeraet ? '<span class="debug-badge warn">eigenesGerät</span>' : ''}
|
||
</div>
|
||
<div class="debug-kv"><span class="k">mitspielerId</span><span class="v">${m.mitspielerId}</span></div>
|
||
<div class="debug-kv"><span class="k">userId</span><span class="v">${fmt(m.userId)}</span></div>
|
||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">Rollen</span></div>
|
||
<div class="debug-tag-row">${rollenTags || '<span class="debug-tag" style="opacity:0.4">—</span>'}</div>
|
||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">Werkzeuge (verfügbar)</span></div>
|
||
<div class="debug-tag-row">${werkzeugTags || '<span class="debug-tag">keine</span>'}</div>
|
||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">spieltMit</span></div>
|
||
<div class="debug-tag-row">${spieltMitTags}</div>
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
|
||
document.getElementById('debugContent').innerHTML = html;
|
||
}
|
||
|
||
// ── Start ──
|
||
if (isGuest) {
|
||
startGastPoll();
|
||
} else {
|
||
checkAktiveAufgabe();
|
||
}
|
||
startLevelPoll();
|
||
</script>
|
||
</body>
|
||
</html>
|