Files
xxx-sphere-web/bin/main/static/games/bdsm/bdsmingame.html
Mario 2b0ce62d33
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Menp überarbeitet
2026-04-08 16:52:43 +02:00

1296 lines
54 KiB
HTML
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.
<!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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── 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>