Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1296 lines
54 KiB
HTML
1296 lines
54 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>BDSM 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-tag.locked { background: rgba(239,68,68,0.2); color: #fca5a5; }
|
||
.debug-tag.expired { background: rgba(251,191,36,0.15); color: #fcd34d; }
|
||
.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('bdsm-session-game') || 'null');
|
||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null');
|
||
const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]');
|
||
const sessionId = sessionStorage.getItem('bdsm-session-id');
|
||
if (!sessionId) window.location.replace('/games/bdsm/neubdsm.html');
|
||
|
||
// Multi-Device: bin ich Gast?
|
||
const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true';
|
||
let myMitspielerId = sessionStorage.getItem('bdsm-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(`/bdsm/${sessionId}/aufgaben/next`);
|
||
if (res.status === 204) { zeigeFinaleDialog(); return; }
|
||
if (res.status === 400) { window.location.replace('/games/bdsm/neubdsm.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(`/bdsm/${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(`/bdsm/${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(false);
|
||
} 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(false);
|
||
} else {
|
||
if (el) el.textContent = formatTime(rem);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// ── Zentrale Abschluss-Funktion: Callback + abgelaufene Sperren im Backend verarbeiten ──
|
||
async function aufgabeAbschliessen(sperreAnwenden = false) {
|
||
clearTimer();
|
||
let allFreigaben = [];
|
||
try {
|
||
const res = await fetch(`/bdsm/${sessionId}/active-task/abschliessen`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sperreAnwenden }),
|
||
});
|
||
if (res.ok) allFreigaben = (await res.json()).abgelaufeneSperren || [];
|
||
} catch (_) {}
|
||
|
||
const danach = isGuest ? startGastPoll : ladeAufgabe;
|
||
if (isGuest) {
|
||
// Guest: alle Freigaben lokal zeigen (betreffen den eigenen Spieler)
|
||
const texte = allFreigaben.map(f => f.text);
|
||
if (texte.length > 0) zeigeAbgelaufeneSperre(texte, 0, danach);
|
||
else danach();
|
||
} else {
|
||
// Host: nur non-eigenesGeraet lokal zeigen; für eigenesGeraet per active-task weiterleiten
|
||
const lokal = allFreigaben.filter(f => !f.eigenesGeraet).map(f => f.text);
|
||
const gast = allFreigaben.filter(f => f.eigenesGeraet);
|
||
const weiter = () => pushGastFreigaben(gast, 0, danach);
|
||
if (lokal.length > 0) zeigeAbgelaufeneSperre(lokal, 0, weiter);
|
||
else weiter();
|
||
}
|
||
}
|
||
|
||
// Sendet Freigabe-Benachrichtigungen nacheinander an eigenesGeraet-Spieler
|
||
async function pushGastFreigaben(freigaben, index, danach) {
|
||
if (index >= freigaben.length) { danach(); return; }
|
||
const f = freigaben[index];
|
||
await saveAktiveAufgabe({
|
||
aufgabeText: f.text,
|
||
mitspielerId: f.mitspielerId,
|
||
eigenesGeraet: true,
|
||
isReleaseNotification: true,
|
||
}, null);
|
||
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);">Zeitstrafe-Benachrichtigung wird zugestellt…</div>`;
|
||
startHostPoll(() => pushGastFreigaben(freigaben, index + 1, danach));
|
||
}
|
||
|
||
function zeigeAbgelaufeneSperre(texte, index, danach) {
|
||
if (index >= texte.length) { danach(); return; }
|
||
playSound('/audio/release.mp3');
|
||
zeigeModal(
|
||
'Zeitstrafe abgelaufen',
|
||
texte[index],
|
||
[{ label: 'OK', primary: true, onClick: () => zeigeAbgelaufeneSperre(texte, index + 1, danach) }]
|
||
);
|
||
}
|
||
|
||
// ── Gast-Polling: wartet auf aktive Aufgabe für mein Gerät ──
|
||
async function startGastPoll() {
|
||
if (guestPollInterval) return;
|
||
if (!myMitspielerId) {
|
||
try {
|
||
const r = await fetch(`/bdsm/${sessionId}/mitspieler/me`);
|
||
if (r.status === 200) {
|
||
const d = await r.json();
|
||
myMitspielerId = d.mitspielerId;
|
||
sessionStorage.setItem('bdsm-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(`/bdsm/${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;
|
||
|
||
// Freigabe-Benachrichtigung: Zeitstrafe abgelaufen
|
||
if (task.isReleaseNotification) {
|
||
playSound('/audio/release.mp3');
|
||
zeigeModal('Zeitstrafe abgelaufen', task.aufgabeText,
|
||
[{ label: 'OK', primary: true, onClick: () => aufgabeAbschliessen(false) }]);
|
||
return;
|
||
}
|
||
|
||
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(false); }
|
||
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) {
|
||
const cb = task.callback;
|
||
if (task.timer != null) zeigeTimerAufgabe(task);
|
||
else if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
|
||
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(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(`/bdsm/${sessionId}/active-task`);
|
||
if (res.status === 404) { await spielSessionEnde(); return; }
|
||
if (res.status === 204) {
|
||
// Gast hat Aufgabe inkl. Sperre/Abgelaufene über abschliessen verarbeitet
|
||
stopHostPoll();
|
||
const next = _hostPollComplete || ladeAufgabe;
|
||
_hostPollComplete = null;
|
||
next();
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function zeigeAufgabe() {
|
||
const task = currentTask;
|
||
const cb = task.callback;
|
||
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 (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
|
||
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
|
||
else 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 zeigeSperreAufgabe(task) {
|
||
renderCard(task, `
|
||
<button onclick="aufgabeAbschliessen(true)">Zeitstrafe anwenden</button>
|
||
<button class="secondary" onclick="gnadeErweisen()">Gnade erweisen</button>`);
|
||
}
|
||
|
||
function gnadeErweisen() {
|
||
const name = currentTask?.nameAktiverMitspieler;
|
||
const title = name ? `${name}, erweist du wirklich Gnade?` : 'Wirklich Gnade erweisen?';
|
||
zeigeModal(title, 'Die Zeitstrafe wird nicht eingetragen und das Spiel geht weiter.', [
|
||
{ label: 'Ja, Gnade erweisen', primary: true, onClick: () => aufgabeAbschliessen(false) },
|
||
{ label: 'Nein', onClick: versteckeModal },
|
||
]);
|
||
}
|
||
|
||
function zeigeVerlaengernAufgabe(task) {
|
||
renderCard(task, `
|
||
<button onclick="aufgabeAbschliessen(true)">Ja, verlängern</button>
|
||
<button class="secondary" onclick="aufgabeAbschliessen(false)">Nein</button>`);
|
||
}
|
||
|
||
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(false);
|
||
} else {
|
||
if (el) el.textContent = formatTime(remaining);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
function timerAbbrechen() {
|
||
clearTimer();
|
||
aufgabeAbschliessen(false);
|
||
}
|
||
|
||
function zeigeEinfacheAufgabe(task) {
|
||
renderCard(task, `<button onclick="aufgabeAbschliessen(false)">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(`/bdsm/${sessionId}/backToLevel5`, { method: 'POST' });
|
||
} catch (_) {}
|
||
ladeAufgabe();
|
||
}
|
||
|
||
async function starteFinale() {
|
||
try {
|
||
const res = await fetch(`/bdsm/sperre/aktive?sessionId=${sessionId}`);
|
||
if (res.ok) {
|
||
const sperren = await res.json();
|
||
const texte = (sperren || [])
|
||
.filter(s => s.mitspieler?.sperrenVorFinaleAufloesen !== false)
|
||
.map(s => s.releaseText)
|
||
.filter(t => t);
|
||
if (texte.length > 0) { zeigeFinaleSperre(texte, 0); return; }
|
||
}
|
||
} catch (_) {}
|
||
ladeFinisher();
|
||
}
|
||
|
||
function zeigeFinaleSperre(texte, index) {
|
||
if (index >= texte.length) { ladeFinisher(); return; }
|
||
playSound('/audio/release.mp3');
|
||
zeigeModal(
|
||
'Zeitstrafe aufgelöst',
|
||
texte[index],
|
||
[{ label: 'OK', primary: true, onClick: () => zeigeFinaleSperre(texte, index + 1) }]
|
||
);
|
||
}
|
||
|
||
async function ladeFinisher() {
|
||
try {
|
||
const res = await fetch(`/bdsm/${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 = '';
|
||
pruefeKeyholderAngebot();
|
||
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>`;
|
||
}
|
||
|
||
// ── Keyholder-Angebot ──
|
||
// _keyholderOnDecline: was passiert wenn das Angebot abgelehnt/übersprungen wird
|
||
let _keyholderOnDecline = null;
|
||
|
||
async function pruefeKeyholderAngebot(onKeinAngebot) {
|
||
_keyholderOnDecline = onKeinAngebot || zeigeFinaleAbgeschlossen;
|
||
try {
|
||
const res = await fetch(`/bdsm/${sessionId}/keyholder-angebot`);
|
||
if (res.ok) {
|
||
const angebot = await res.json();
|
||
zeigeKeyholderAngebot(angebot);
|
||
return;
|
||
}
|
||
} catch (_) {}
|
||
_keyholderOnDecline();
|
||
}
|
||
|
||
function zeigeFinaleAbgeschlossen() {
|
||
zeigeModal(
|
||
'Das Finale ist abgeschlossen!',
|
||
'Wir hoffen, ihr hattet viel Spaß! 🎉',
|
||
[{ label: 'Session beenden', primary: true, onClick: spielAbschliessen }]
|
||
);
|
||
}
|
||
|
||
function zeigeKeyholderAngebot(angebot) {
|
||
zeigeModal(
|
||
'Keyholder-Angebot',
|
||
`${angebot.keyholderName}, möchtest du die Keyholder-Rolle für ${angebot.lockeeName} übernehmen?\n\nDu kannst dafür eines deiner vorhandenen Locks auswählen.`,
|
||
[
|
||
{ label: 'Ja, Keyholder werden', primary: true, onClick: () => ladeKeyholderLocks(angebot) },
|
||
{ label: 'Nein', onClick: () => _keyholderOnDecline() },
|
||
]
|
||
);
|
||
}
|
||
|
||
async function ladeKeyholderLocks(angebot) {
|
||
versteckeModal();
|
||
try {
|
||
const res = await fetch(`/bdsm/${sessionId}/keyholder-locks?keyholderUserId=${angebot.keyholderUserId}`);
|
||
if (!res.ok) { _keyholderOnDecline(); return; }
|
||
const locks = await res.json();
|
||
zeigeKeyholderLockAuswahl(angebot, locks);
|
||
} catch (_) {
|
||
_keyholderOnDecline();
|
||
}
|
||
}
|
||
|
||
function zeigeKeyholderLockAuswahl(angebot, locks) {
|
||
const overlay = document.getElementById('modal');
|
||
const card = overlay.querySelector('.modal-card');
|
||
const escapedLockeeName = escapeAttr(angebot.lockeeName);
|
||
const optionen = locks.map(l =>
|
||
`<button class="secondary" style="width:100%;margin-bottom:0.5rem;text-align:left;" onclick="keyholderLockGewaehlt('${l.lockId}','${angebot.lockeeUserId}','${angebot.keyholderUserId}','${escapedLockeeName}')">` +
|
||
`<strong>${escapeHtml(l.name)}</strong>` +
|
||
`<span style="font-size:0.8rem;color:var(--color-muted);margin-left:0.5rem;">${l.totalCards} Karten · alle ${l.pickEveryMinute} Min.${l.active ? ' · aktiv' : ''}</span>` +
|
||
`</button>`
|
||
).join('');
|
||
card.innerHTML = `
|
||
<div class="modal-title">Lock auswählen</div>
|
||
<div class="modal-text" style="text-align:left;margin-bottom:1rem;">Welches Lock soll für <strong>${escapeHtml(angebot.lockeeName)}</strong> verwendet werden?</div>
|
||
<div style="max-height:50vh;overflow-y:auto;">${optionen}</div>
|
||
<div class="modal-actions" style="margin-top:1rem;">
|
||
<button class="secondary" onclick="_keyholderOnDecline()">Abbrechen</button>
|
||
</div>`;
|
||
overlay.style.display = 'flex';
|
||
}
|
||
|
||
async function keyholderLockGewaehlt(lockId, lockeeUserId, keyholderUserId, lockeeName) {
|
||
versteckeModal();
|
||
try {
|
||
const res = await fetch(`/bdsm/${sessionId}/zu-chastity`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ lockId, lockeeUserId, keyholderUserId }),
|
||
});
|
||
if (res.status === 409) {
|
||
zeigeModal('Bereits gesperrt', `${escapeHtml(lockeeName)} ist bereits aktiv in einem Chastity Game gesperrt. Eine zweite Sperre ist nicht möglich.`,
|
||
[{ label: 'OK', primary: true, onClick: _keyholderOnDecline }]);
|
||
return;
|
||
}
|
||
if (!res.ok) throw new Error();
|
||
const data = await res.json();
|
||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
zeigeUnlockCodeModal(data.unlockCode, lockeeName);
|
||
} catch (_) {
|
||
zeigeModal('Fehler', 'Die Überführung ins Chastity Game ist fehlgeschlagen.',
|
||
[{ label: 'OK', primary: true, onClick: spielAbschliessen }]);
|
||
}
|
||
}
|
||
|
||
function zeigeUnlockCodeModal(code, lockeeName) {
|
||
const overlay = document.getElementById('modal');
|
||
const card = overlay.querySelector('.modal-card');
|
||
card.innerHTML = `
|
||
<div style="font-size:2rem;">🔒</div>
|
||
<div class="modal-title" id="unlockCodeTitle">Entsperrcode für ${escapeHtml(lockeeName)}</div>
|
||
<div class="modal-text" id="unlockCodeHint" style="font-size:0.85rem;">
|
||
Stelle die Kombination des Tresors auf diesen Code ein und verschließe den Schlüssel darin.
|
||
</div>
|
||
<div id="unlockCodeDisplay" style="
|
||
font-family: monospace; font-size: 2rem; letter-spacing: 0.3em;
|
||
background: var(--color-secondary); border-radius: 8px;
|
||
padding: 1rem 1.5rem; text-align: center; color: var(--color-primary);
|
||
line-height: 1.8; word-break: break-all; margin-bottom: 0.75rem;
|
||
">${escapeHtml(code)}</div>
|
||
<div id="unlockCodeCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);text-align:center;font-family:monospace;margin-bottom:0.5rem;"></div>
|
||
<button id="unlockCodeBtn" style="width:100%;">Code vergessen & weiter</button>`;
|
||
overlay.style.display = 'flex';
|
||
document.getElementById('unlockCodeBtn').onclick = () => starteChastityCodeScramble(code);
|
||
}
|
||
|
||
function starteChastityCodeScramble(realCode) {
|
||
const display = document.getElementById('unlockCodeDisplay');
|
||
const cdEl = document.getElementById('unlockCodeCountdown');
|
||
const hintEl = document.getElementById('unlockCodeHint');
|
||
const btn = document.getElementById('unlockCodeBtn');
|
||
const titleEl = document.getElementById('unlockCodeTitle');
|
||
const len = realCode.length;
|
||
const DURATION = 3 * 60;
|
||
let remaining = DURATION;
|
||
let stopped = false;
|
||
|
||
function randomCode() {
|
||
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
|
||
}
|
||
function finish() {
|
||
stopped = true;
|
||
clearInterval(scrambleInt);
|
||
clearInterval(countdownInt);
|
||
versteckeModal();
|
||
window.location.href = '/games/chastity/keyholder.html';
|
||
}
|
||
|
||
hintEl.style.display = 'none';
|
||
cdEl.style.display = '';
|
||
titleEl.textContent = 'Nun vergessen wir den Code…';
|
||
btn.textContent = 'Abbrechen';
|
||
btn.onclick = finish;
|
||
|
||
function updateCd() {
|
||
const m = Math.floor(remaining / 60);
|
||
const s = remaining % 60;
|
||
cdEl.textContent = `${m}:${String(s).padStart(2, '0')}`;
|
||
}
|
||
updateCd();
|
||
|
||
const scrambleInt = setInterval(() => { if (!stopped) display.textContent = randomCode(); }, 80);
|
||
const countdownInt = setInterval(() => {
|
||
if (stopped) return;
|
||
remaining--;
|
||
updateCd();
|
||
if (remaining <= 0) finish();
|
||
}, 1000);
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ── Session beenden ──
|
||
const BDSM_STORAGE_KEYS = [
|
||
'bdsm-session-id', 'bdsm-session-settings', 'bdsm-session-setup',
|
||
'bdsm-session-gruppen', 'bdsm-session-toys', 'bdsm-session-game',
|
||
];
|
||
|
||
function sessionBeendenFragen() {
|
||
zeigeModal(
|
||
'Wirklich beenden?',
|
||
'Möchtest du die aktive Session wirklich beenden?',
|
||
[
|
||
{ label: 'Ja, beenden', primary: true, onClick: () => { versteckeModal(); pruefeKeyholderAngebot(sessionLoeschen); } },
|
||
{ label: 'Nein', onClick: versteckeModal },
|
||
]
|
||
);
|
||
}
|
||
|
||
async function spielAbschliessen() {
|
||
versteckeModal();
|
||
try {
|
||
await fetch(`/bdsm/${sessionId}/abgeschlossen`, { method: 'POST' });
|
||
} catch (_) {}
|
||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
window.location.href = '/userhome.html';
|
||
}
|
||
|
||
async function sessionLoeschen() {
|
||
versteckeModal();
|
||
try {
|
||
await fetch('/bdsm', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ sessionId }),
|
||
});
|
||
} catch (_) { /* ignorieren */ }
|
||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
window.location.href = '/userhome.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(`/bdsm/${sessionId}/verlassen`, { method: 'DELETE' });
|
||
} catch (_) {}
|
||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
window.location.href = '/userhome.html';
|
||
}
|
||
|
||
async function spielSessionEnde() {
|
||
if (guestPollInterval) { clearInterval(guestPollInterval); guestPollInterval = null; }
|
||
stopHostPoll();
|
||
stopLevelPoll();
|
||
clearTimer();
|
||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
// Unterscheide: ordentlich beendet vs. abgebrochen
|
||
let ordentlich = false;
|
||
try {
|
||
const r = await fetch(`/bdsm/${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(`/bdsm/${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(`/bdsm/${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">BdsmGameEntity – 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('wahrscheinlichkeit Sperre', s.wahrscheinlichkeitSperre + '%')}
|
||
${kv('wahrscheinlichkeit Strafe', s.wahrscheinlichkeitStrafe + '%')}
|
||
${kv('zeitfaktorZeitstrafen', s.zeitfaktorZeitstrafen)}
|
||
${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>' : ''}
|
||
${!m.sperrenVorFinaleAufloesen ? '<span class="debug-badge warn">keine Auflösung</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 locked">alle gesperrt</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>`;
|
||
|
||
// ── Aktive Sperren ──
|
||
html += `<div class="debug-section"><div class="debug-section-title">AktiveSperreEntity (${data.aktiveSperren.length})</div>`;
|
||
if (data.aktiveSperren.length === 0) {
|
||
html += `<div style="color:var(--color-muted);font-size:0.8rem;">Keine aktiven Sperren</div>`;
|
||
}
|
||
data.aktiveSperren.forEach(sp => {
|
||
const fuerTags = (sp.fuer || []).map(w => `<span class="debug-tag locked">${w}</span>`).join('');
|
||
const abgelaufenBadge = sp.abgelaufen
|
||
? '<span class="debug-badge warn">ABGELAUFEN (nicht gepolllt)</span>'
|
||
: '<span class="debug-badge">aktiv</span>';
|
||
html += `<div class="debug-card">
|
||
<div class="debug-card-name">
|
||
${escapeHtml(sp.mitspielerName || '?')} ${abgelaufenBadge}
|
||
</div>
|
||
<div class="debug-kv"><span class="k">aktiveSperreId</span><span class="v">${sp.aktiveSperreId}</span></div>
|
||
<div class="debug-kv"><span class="k">minuten</span><span class="v">${fmt(sp.minuten)}</span></div>
|
||
<div class="debug-kv"><span class="k">startzeit</span><span class="v">${fmt(sp.startzeit)}</span></div>
|
||
<div class="debug-kv"><span class="k">endzeit</span><span class="v ${sp.abgelaufen ? 'expired' : ''}">${fmt(sp.endzeit)}</span></div>
|
||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">gesperrt für</span></div>
|
||
<div class="debug-tag-row">${fuerTags}</div>
|
||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">releaseText</span><span class="v" style="word-break:break-word;">${escapeHtml(sp.releaseText || '—')}</span></div>
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
|
||
document.getElementById('debugContent').innerHTML = html;
|
||
}
|
||
|
||
// ── Start ──
|
||
if (isGuest) {
|
||
startGastPoll();
|
||
} else {
|
||
checkAktiveAufgabe();
|
||
}
|
||
startLevelPoll();
|
||
</script>
|
||
</body>
|
||
</html>
|