Files
xxx-sphere-web/bin/main/static/games/bdsm/neubdsm.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

1486 lines
88 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 Session einrichten xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Accordion ── */
.acc-item { margin-bottom: 0.75rem; }
.acc-header {
display: flex; justify-content: space-between; align-items: center;
width: 100%; padding: 1rem 1.25rem;
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 10px; cursor: pointer;
font-size: 0.88rem; font-weight: 600; color: var(--color-text);
text-transform: uppercase; letter-spacing: 0.05em; text-align: left;
transition: border-color 0.15s;
}
.acc-header:hover { border-color: var(--color-primary); background: var(--color-card); }
.acc-header.is-open { border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-color: var(--color-primary); }
.acc-chevron { transition: transform 0.2s; font-size: 0.8rem; }
.acc-header.is-open .acc-chevron { transform: rotate(180deg); }
.acc-body { display: none; border: 1px solid var(--color-primary); border-top: none; border-radius: 0 0 10px 10px; padding: 1.25rem; }
.acc-body.is-open { display: block; }
/* ── Settings ── */
.setting-row { margin-bottom: 1.25rem; }
.setting-row:last-child { margin-bottom: 0; }
.setting-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.4rem; }
.setting-header label { font-size: 0.85rem; color: #aaa; margin: 0; }
.setting-value { font-size: 1rem; font-weight: 600; color: var(--color-primary); min-width: 3.5rem; text-align: right; }
input[type="range"] { width: 100%; accent-color: var(--color-primary); cursor: pointer; }
input[type="range"]:disabled { opacity: 0.45; cursor: default; }
.settings-divider { border: none; border-top: 1px solid var(--color-secondary); margin: 1.25rem 0; }
/* ── Player sub-accordions ── */
.player-acc-item { margin-bottom: 0.5rem; }
.player-acc-header {
display: flex; align-items: center; gap: 0.55rem;
padding: 0.7rem 1rem; background: var(--color-secondary);
border: 1px solid transparent; border-radius: 8px; cursor: pointer; transition: border-color 0.15s;
}
.player-acc-header:hover { border-color: var(--color-primary); }
.player-acc-header.is-open { border-color: var(--color-primary); border-radius: 8px 8px 0 0; }
.player-acc-body { display: none; border: 1px solid var(--color-primary); border-top: none; border-radius: 0 0 8px 8px; padding: 1.25rem; }
.player-acc-body.is-open { display: block; }
.player-acc-chevron { margin-left: auto; transition: transform 0.2s; font-size: 0.75rem; flex-shrink: 0; }
.player-acc-header.is-open .player-acc-chevron { transform: rotate(180deg); }
.player-title { font-weight: 600; color: var(--color-text); font-size: 0.93rem; white-space: nowrap; }
.player-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.player-avatar-initials { width: 28px; height: 28px; border-radius: 50%; background: var(--color-secondary); border: 1px solid var(--color-primary); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; color: var(--color-primary); flex-shrink: 0; text-transform: uppercase; user-select: none; }
.player-badge { background: var(--color-primary); color: #fff; font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 10px; font-weight: 600; white-space: nowrap; }
.player-badge-pending { background: var(--color-secondary); color: var(--color-muted); font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 10px; font-weight: 600; border: 1px solid var(--color-secondary); white-space: nowrap; }
.player-badge-accepted { background: #1a5c2a; color: #6fcf97; font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 10px; font-weight: 600; white-space: nowrap; }
.player-remove { background: transparent; border: 1px solid var(--color-secondary); color: var(--color-muted); padding: 0.2rem 0.45rem; font-size: 0.72rem; border-radius: 4px; cursor: pointer; transition: border-color 0.15s, color 0.15s; flex-shrink: 0; }
.player-remove:hover { border-color: var(--color-primary); color: var(--color-primary); background: transparent; }
.btn-invite { background: transparent; border: 1px solid var(--color-primary); color: var(--color-primary); padding: 0.2rem 0.5rem; font-size: 0.72rem; font-weight: 600; border-radius: 4px; cursor: pointer; transition: background 0.15s, color 0.15s; flex-shrink: 0; }
.btn-invite:hover { background: var(--color-primary); color: #fff; }
.btn-cancel-invite { background: transparent; border: 1px solid var(--color-secondary); color: var(--color-muted); padding: 0.2rem 0.6rem; font-size: 0.75rem; border-radius: 4px; cursor: pointer; }
.btn-cancel-invite:hover { border-color: var(--color-primary); color: var(--color-primary); background: transparent; }
/* ── Player form ── */
.card-field { margin-bottom: 1rem; }
.card-field:last-child { margin-bottom: 0; }
.card-field > label { font-size: 0.8rem; color: #aaa; margin: 0 0 0.5rem 0; display: block; }
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
.check-item { display: inline-flex; align-items: flex-start; gap: 0.45rem; background: var(--color-secondary); border: 1px solid transparent; border-radius: 6px; padding: 0.4rem 0.7rem; cursor: pointer; transition: border-color 0.15s; user-select: none; }
.check-item.is-checked { border-color: var(--color-primary); }
.check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; }
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
.add-player-btn { width: 100%; background: transparent; border: 1px dashed var(--color-secondary); color: var(--color-muted); padding: 0.7rem; border-radius: 8px; font-size: 0.88rem; font-weight: normal; cursor: pointer; transition: border-color 0.15s, color 0.15s; margin-top: 0.5rem; }
.add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; }
.pending-info { text-align: center; color: var(--color-muted); font-size: 0.9rem; padding: 0.75rem 0; }
.pending-name { font-weight: 600; color: var(--color-text); font-size: 1rem; margin-bottom: 0.2rem; }
/* ── Gruppe / Toy items ── */
.select-all-label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; user-select: none; font-size: 0.78rem; color: var(--color-muted); margin-bottom: 0.5rem; }
.select-all-label input { accent-color: var(--color-primary); width: 14px; height: 14px; cursor: pointer; flex-shrink: 0; }
.gruppe-list { list-style: none; padding: 0; margin: 0 0 1.25rem 0; }
.gruppe-list:last-child { margin-bottom: 0; }
.gruppe-item, .toy-item {
display: flex; align-items: center; gap: 0.6rem;
padding: 0.6rem 0.85rem; border-radius: 8px;
background: var(--color-card); border: 1px solid var(--color-secondary);
margin-bottom: 0.5rem; cursor: pointer; transition: border-color 0.15s; user-select: none;
}
.gruppe-item.is-checked, .toy-item.is-checked { border-color: var(--color-primary); }
.gruppe-item input, .toy-item input { accent-color: var(--color-primary); flex-shrink: 0; width: 14px; height: 14px; cursor: pointer; }
.gruppe-item span, .toy-item span { flex: 1; min-width: 0; }
.item-img { width: 38px; height: 38px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.gruppe-item-name, .toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
.gruppe-item-desc, .toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
.empty-hint { color: var(--color-muted); font-size: 0.875rem; font-style: italic; padding: 0.5rem 0; }
.aufgaben-section-label { font-size: 0.78rem; font-weight: 600; color: var(--color-muted); text-transform: uppercase; letter-spacing: 0.04em; margin: 1rem 0 0.5rem 0; }
.aufgaben-section-label:first-child { margin-top: 0; }
.toys-hint { font-size: 0.85rem; color: var(--color-muted); margin-bottom: 1rem; }
/* ── Guest hint ── */
.guest-hint { font-size: 0.8rem; color: var(--color-muted); font-style: italic; margin-bottom: 0.75rem; padding: 0.5rem 0.75rem; background: var(--color-secondary); border-radius: 6px; }
/* ── Wait view ── */
.wait-card { text-align: center; padding: 3rem 1rem; }
.wait-icon { font-size: 3rem; margin-bottom: 1.5rem; animation: pulse 2s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
.wait-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.75rem; }
.wait-sub { font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; margin-bottom: 2rem; }
/* ── Modals ── */
.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; max-width: 420px; width: 100%; }
.modal-title { font-size: 1.1rem; font-weight: 700; color: var(--color-text); margin-bottom: 0.75rem; }
.modal-text { font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; margin-bottom: 1.5rem; }
.modal-actions { display: flex; flex-direction: column; gap: 0.6rem; }
.modal-actions button { width: 100%; padding: 0.75rem; }
.friend-combobox { position: relative; }
.friend-dropdown { display: none; position: absolute; top: 100%; left: 0; right: 0; background: var(--color-card); border: 1px solid var(--color-secondary); border-radius: 8px; max-height: 220px; overflow-y: auto; z-index: 10; margin-top: 0.25rem; }
.friend-dropdown-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0.75rem; cursor: pointer; transition: background 0.1s; font-size: 0.9rem; font-weight: 600; }
.friend-dropdown-item:hover { background: var(--color-secondary); }
.selected-friend-box { display: none; margin-top: 0.75rem; padding: 0.6rem 0.75rem; background: var(--color-secondary); border-radius: 8px; font-size: 0.9rem; font-weight: 600; border: 1px solid var(--color-primary); color: var(--color-text); }
.friend-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; background: var(--color-secondary); flex-shrink: 0; }
</style>
</head>
<body class="app">
<div class="modal-overlay" id="sessionModal" style="display:none;">
<div class="modal-card">
<div class="modal-title" id="sessionModalTitle"></div>
<div class="modal-text" id="sessionModalText"></div>
<div class="modal-actions" id="sessionModalActions"></div>
</div>
</div>
<div class="modal-overlay" id="errorModal" style="display:none;">
<div class="modal-card" style="text-align:center;">
<div style="font-size:1.5rem;margin-bottom:0.75rem;">⚠️</div>
<div class="modal-title" id="errorModalTitle"></div>
<div style="font-size:0.9rem;color:var(--color-muted);line-height:1.5;margin-bottom:1.25rem;" id="errorModalText"></div>
<button onclick="document.getElementById('errorModal').style.display='none'">OK</button>
</div>
</div>
<div class="modal-overlay" id="friendModal" style="display:none;">
<div class="modal-card">
<div class="modal-title">Freund einladen</div>
<div class="friend-combobox">
<input type="text" id="friendSearch" placeholder="Name eingeben…" autocomplete="off" oninput="filterFreunde(this.value)">
<div class="friend-dropdown" id="friendDropdown"></div>
</div>
<div class="selected-friend-box" id="selectedFriendBox"></div>
<button id="btnEinladen" style="margin-top:1rem;width:100%;" disabled onclick="confirmedEinladen()">Einladen</button>
<button class="secondary" style="margin-top:0.6rem;width:100%;" onclick="schliesseFriendModal()">Abbrechen</button>
</div>
</div>
<div class="main" id="setupView" style="display:none;">
<div class="content">
<h1>BDSM Game</h1>
<p id="pageSubtitle" style="margin-bottom:1.5rem;">Session einrichten</p>
<!-- Accordion 1: Grundeinstellungen -->
<div class="acc-item">
<button class="acc-header is-open" id="acc-grundeinstellungen-btn" onclick="toggleAcc('grundeinstellungen')">
<span>Grundeinstellungen</span><span class="acc-chevron"></span>
</button>
<div class="acc-body is-open" id="acc-grundeinstellungen-body">
<div id="guestSettingsHint" class="guest-hint" style="display:none;">Einstellungen werden vom Host festgelegt nur zur Ansicht.</div>
<div class="setting-row">
<div class="setting-header"><label for="sldSpieldauer">Spieldauer</label><span class="setting-value" id="valSpieldauer">Mittel</span></div>
<input type="range" id="sldSpieldauer" min="0" max="4" value="2" oninput="updateSpieldauer(this.value)">
</div>
<div class="setting-row">
<div class="setting-header"><label for="sldStrafe">Wahrscheinlichkeit Strafe</label><span class="setting-value"><span id="valStrafe">15</span> %</span></div>
<input type="range" id="sldStrafe" min="0" max="100" value="15" oninput="document.getElementById('valStrafe').textContent=this.value;updateWarnung()">
</div>
<div class="setting-row">
<div class="setting-header"><label for="sldZeitstrafe">Wahrscheinlichkeit Zeitstrafe</label><span class="setting-value"><span id="valZeitstrafe">15</span> %</span></div>
<input type="range" id="sldZeitstrafe" min="0" max="100" value="15" oninput="document.getElementById('valZeitstrafe').textContent=this.value;updateWarnung()">
</div>
<div class="message" id="wahrschWarnung" style="display:none;margin-top:0.75rem;"></div>
<hr class="settings-divider">
<div class="card-field" style="margin-bottom:0;">
<label style="font-size:0.8rem;color:#aaa;margin-bottom:0.5rem;display:block;">Zeitstrafen</label>
<label class="check-item is-checked" id="globalSperreLabel">
<input type="checkbox" id="chkZeitstrafen" checked onchange="onGlobalZeitstrafen()">
<span class="check-item-label">Zeitstrafen vor dem Finale auflösen (Standard für alle Spieler)</span>
</label>
</div>
</div>
</div>
<!-- Accordion 2: Mitspieler -->
<div class="acc-item">
<button class="acc-header" id="acc-mitspieler-btn" onclick="toggleAcc('mitspieler')">
<span>Mitspieler</span><span class="acc-chevron"></span>
</button>
<div class="acc-body" id="acc-mitspieler-body">
<div id="playersContainer"></div>
<button class="add-player-btn" id="btnAddPlayer" onclick="addPlayer()">+ Spieler hinzufügen</button>
</div>
</div>
<!-- Accordion 3: Aufgaben -->
<div class="acc-item">
<button class="acc-header" id="acc-aufgaben-btn" onclick="toggleAcc('aufgaben')">
<span>Aufgaben</span><span class="acc-chevron"></span>
</button>
<div class="acc-body" id="acc-aufgaben-body">
<div id="guestAufgabenHint" class="guest-hint" style="display:none;">Aufgaben werden vom Host festgelegt nur zur Ansicht.</div>
<div id="sectionOwn">
<div class="aufgaben-section-label"><label class="select-all-label"><input type="checkbox" class="select-all-cb" data-list="listOwn"> Eigene Gruppen</label></div>
<ul class="gruppe-list" id="listOwn"><li class="empty-hint">Wird geladen…</li></ul>
</div>
<div id="sectionSubscribed">
<div class="aufgaben-section-label"><label class="select-all-label"><input type="checkbox" class="select-all-cb" data-list="listSubscribed"> Abonnierte Gruppen</label></div>
<ul class="gruppe-list" id="listSubscribed"><li class="empty-hint">Wird geladen…</li></ul>
</div>
<div id="sectionSystem">
<div class="aufgaben-section-label"><label class="select-all-label"><input type="checkbox" class="select-all-cb" data-list="listSystem"> System-Gruppen</label></div>
<ul class="gruppe-list" id="listSystem"><li class="empty-hint">Wird geladen…</li></ul>
</div>
</div>
</div>
<!-- Accordion 4: Toys -->
<div class="acc-item">
<button class="acc-header" id="acc-toys-btn" onclick="toggleAcc('toys')">
<span>Toys</span><span class="acc-chevron"></span>
</button>
<div class="acc-body" id="acc-toys-body">
<div id="guestToysHint" class="guest-hint" style="display:none;">Toys werden vom Host festgelegt nur zur Ansicht.</div>
<p class="toys-hint">Deaktiviere Toys, die nicht zur Verfügung stehen. Aufgaben, die diese benötigen, werden nicht gespielt.</p>
<div id="toyList"><p class="empty-hint">Bitte zuerst Aufgaben-Gruppen auswählen.</p></div>
</div>
</div>
<div class="message" id="message" style="margin-top:1rem;display:none;"></div>
<button class="full-width" id="btnAction" onclick="handleAction()" style="margin-top:1rem;">Spiel starten</button>
</div>
</div>
<div class="main" id="waitView" style="display:none;">
<div class="content wait-card">
<div class="wait-icon"></div>
<div class="wait-title">Warte auf Spielstart…</div>
<div class="wait-sub">Der Host startet das Spiel in Kürze. Diese Seite aktualisiert sich automatisch.</div>
<button class="secondary full-width" onclick="abbrechenGast()">Abbrechen</button>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
// ── Constants ──
const SPIELDAUER = [
{ label: 'Sehr kurz', aufgaben: 4, zeitfaktor: 0.5 },
{ label: 'Kurz', aufgaben: 6, zeitfaktor: 0.7 },
{ label: 'Mittel', aufgaben: 8, zeitfaktor: 1.0 },
{ label: 'Lang', aufgaben: 10, zeitfaktor: 1.5 },
{ label: 'Sehr lang', aufgaben: 14, zeitfaktor: 2.0 },
];
const GESCHLECHTER = [
{ value: 'MAENNLICH', label: 'Männlich' },
{ value: 'WEIBLICH', label: 'Weiblich' },
{ value: 'DIVERS', label: 'Divers' },
];
const ROLLEN = [
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe Aktiv' },
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe Passiv' },
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung Aktiv' },
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung Passiv' },
];
const WERKZEUGE_DEFAULTS = {
MAENNLICH: ['MUND','PENIS','ANUS','UMSCHNALLDILDO'],
WEIBLICH: ['MUND','VAGINA','ANUS','UMSCHNALLDILDO'],
DIVERS: ['MUND','ANUS','UMSCHNALLDILDO'],
};
const WERKZEUGE = [
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
];
const ROLE_LABELS = {
AUFGABE_AKTIV: 'Aufgabe Aktiv', AUFGABE_PASSIV: 'Aufgabe Passiv',
BESTRAFUNG_AKTIV: 'Bestrafung Aktiv', BESTRAFUNG_PASSIV: 'Bestrafung Passiv',
};
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
const BDSM_STORAGE_KEYS = [
'bdsm-session-id','bdsm-session-settings','bdsm-session-setup',
'bdsm-session-gruppen','bdsm-session-toys','bdsm-session-game',
];
const ACC_IDS = ['grundeinstellungen','mitspieler','aufgaben','toys'];
// ── State ──
let isGuestMode = false;
let guestEinladungId = null;
let guestOwnPlayerId = null;
let guestPollInterval = null;
let playerSeq = 0;
let playerIds = [];
let playerInvitations = {};
let playerProfilePics = {}; // id → Base64-String oder null
let pollIntervalId = null;
let myUserId = null;
let selfPlayerId = null;
let freundeListe = [];
let setupId = null;
let savedGruppen = new Set();
let gruppenContent = null;
let loadedForGruppen = null;
let warnungsAkzeptiert = false;
const savedToysRaw = sessionStorage.getItem('bdsm-session-toys');
let savedToyIds = savedToysRaw ? new Set(JSON.parse(savedToysRaw).map(t => t.toyId)) : null;
let toysNeedReload = true;
// ── Accordion ──
function toggleAcc(id) {
const wasOpen = document.getElementById(`acc-${id}-btn`).classList.contains('is-open');
ACC_IDS.forEach(accId => {
const btn = document.getElementById(`acc-${accId}-btn`);
const body = document.getElementById(`acc-${accId}-body`);
const open = accId === id && !wasOpen;
btn.classList.toggle('is-open', open);
body.classList.toggle('is-open', open);
});
if (id === 'toys' && !wasOpen && toysNeedReload) ladeToys();
}
function togglePlayerAcc(id) {
if (isGuestMode && id !== guestOwnPlayerId) return;
playerIds.forEach(pid => {
const btn = document.getElementById(`pacc-${pid}-btn`);
const body = document.getElementById(`pacc-${pid}-body`);
if (!btn || !body) return;
const open = pid === id && !btn.classList.contains('is-open');
btn.classList.toggle('is-open', open);
body.classList.toggle('is-open', open);
});
}
function openPlayerAcc(id) {
playerIds.forEach(pid => {
const btn = document.getElementById(`pacc-${pid}-btn`);
const body = document.getElementById(`pacc-${pid}-body`);
if (!btn || !body) return;
btn.classList.toggle('is-open', pid === id);
body.classList.toggle('is-open', pid === id);
});
}
// ── Session settings ──
function updateSpieldauer(val) {
document.getElementById('valSpieldauer').textContent = SPIELDAUER[val].label;
}
function updateWarnung() {
const strafe = parseInt(document.getElementById('sldStrafe').value);
const zs = parseInt(document.getElementById('sldZeitstrafe').value);
const summe = strafe + zs;
const el = document.getElementById('wahrschWarnung');
if (summe > 98) {
el.textContent = `Kombiniert ${summe} % Werte über 98 % sind nicht möglich.`;
el.className = 'message error'; el.style.display = 'block';
} else if (summe > 60) {
el.textContent = `Hinweis: Bei ${summe} % kombinierten Wahrscheinlichkeiten ist die Chance auf Vanilla-Aufgaben sehr gering.`;
el.className = 'message warning'; el.style.display = 'block';
} else { el.style.display = 'none'; }
}
function applySettings(s) {
function setSlider(id, displayId, value, transform) {
const el = document.getElementById(id); if (!el) return;
el.value = value;
document.getElementById(displayId).textContent = transform ? transform(value) : value;
}
if (s.wahrscheinlichkeitStrafe != null) setSlider('sldStrafe', 'valStrafe', s.wahrscheinlichkeitStrafe);
if (s.wahrscheinlichkeitSperre != null) setSlider('sldZeitstrafe', 'valZeitstrafe', s.wahrscheinlichkeitSperre);
if (s.aufgabenProLevel != null) {
const idx = SPIELDAUER.reduce((best, d, i) =>
Math.abs(d.aufgaben - s.aufgabenProLevel) < Math.abs(SPIELDAUER[best].aufgaben - s.aufgabenProLevel) ? i : best, 2);
setSlider('sldSpieldauer', 'valSpieldauer', idx, v => SPIELDAUER[v].label);
}
updateWarnung();
}
function disableSettingsForGuest() {
['sldSpieldauer','sldStrafe','sldZeitstrafe'].forEach(id => { const el = document.getElementById(id); if (el) el.disabled = true; });
const chk = document.getElementById('chkZeitstrafen'); if (chk) chk.disabled = true;
const lbl = document.getElementById('globalSperreLabel'); if (lbl) { lbl.style.pointerEvents = 'none'; lbl.style.opacity = '0.6'; }
document.getElementById('guestSettingsHint').style.display = 'block';
document.getElementById('guestAufgabenHint').style.display = 'block';
document.getElementById('guestToysHint').style.display = 'block';
}
function onGlobalZeitstrafen() {
const globalCb = document.getElementById('chkZeitstrafen');
const globalLbl = document.getElementById('globalSperreLabel');
if (globalLbl) globalLbl.classList.toggle('is-checked', globalCb.checked);
playerIds.forEach(id => {
const inv = playerInvitations[id];
if (inv && inv.status === 'ACCEPTED_OWN') return;
const cb = document.getElementById(`p${id}-sperrenAufloesen`);
if (!cb) return;
cb.checked = globalCb.checked; toggleSperreWarning(id);
});
}
// ── Player builders ──
function buildCheckItems(name, items, type, disabled = false) {
return items.map(({ value, label, desc }) => `
<label class="check-item${disabled ? ' is-disabled' : ''}">
<input type="${type}" name="${name}" value="${value}"${disabled ? ' disabled' : ''}>
<span><span class="check-item-label">${label}</span>${desc ? `<span class="check-item-desc">${desc}</span>` : ''}</span>
</label>`).join('');
}
function buildPlayerBody(id, nameValue, nameReadOnly = false, genderDisabled = false, allDisabled = false) {
const globalDefault = document.getElementById('chkZeitstrafen')?.checked ?? true;
const isCheckedCls = globalDefault ? ' is-checked' : '';
const nameHtml = nameReadOnly
? `<input type="text" id="p${id}-name" value="${nameValue}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`
: `<input type="text" id="p${id}-name" value="${nameValue}" placeholder="Name" autocomplete="off">`;
return `
<div class="card-field">
<label>Name</label>${nameHtml}
<div class="field-error" id="p${id}-name-err">Bitte Namen eingeben.</div>
</div>
<div class="card-field">
<label>Geschlecht${genderDisabled ? ' <span style="font-size:0.75rem;color:var(--color-muted);">(unveränderlich)</span>' : ''}</label>
<div class="check-group">${buildCheckItems('p'+id+'-geschlecht', GESCHLECHTER, 'radio', genderDisabled || allDisabled)}</div>
<div class="field-error" id="p${id}-geschlecht-err">Bitte Geschlecht auswählen.</div>
</div>
<div class="card-field">
<label>Spielt mit</label>
<div class="check-group">${buildCheckItems('p'+id+'-spieltmit', GESCHLECHTER, 'checkbox', allDisabled)}</div>
<div class="field-error" id="p${id}-spieltmit-err">Bitte mindestens eine Option wählen.</div>
<div class="field-error" id="p${id}-partner-err">Kein Mitspieler mit passendem Geschlecht vorhanden.</div>
</div>
<div class="card-field">
<label>Rollen</label>
<div class="check-group">${buildCheckItems('p'+id+'-rollen', ROLLEN, 'checkbox', allDisabled)}</div>
<div class="field-error" id="p${id}-rollen-err">Bitte mindestens eine Rolle wählen.</div>
</div>
<div class="card-field">
<label>Verfügbar</label>
<div class="check-group check-group--two-col">${buildCheckItems('p'+id+'-werkzeuge', WERKZEUGE, 'checkbox', allDisabled)}</div>
<div class="field-error" id="p${id}-werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
<div id="p${id}-chastity-hint" style="display:none;font-size:0.78rem;color:var(--color-muted);margin-top:0.4rem;font-style:italic;line-height:1.4;"></div>
</div>
<div class="card-field">
<label>Finale</label>
<label class="check-item${isCheckedCls}${allDisabled ? ' is-disabled' : ''}" id="p${id}-sperre-label">
<input type="checkbox" id="p${id}-sperrenAufloesen"${globalDefault ? ' checked' : ''}${allDisabled ? ' disabled' : ''} onchange="toggleSperreWarning(${id})">
<span class="check-item-label">Zeitstrafen vor dem Finale auflösen</span>
</label>
<div style="display:none;margin-top:0.4rem;font-size:0.78rem;color:var(--color-primary);" id="p${id}-sperre-warn">
⚠️ Diese Person könnte im Finale leer ausgehen.
</div>
</div>`;
}
function buildAvatarHtml(id, name) {
const pic = playerProfilePics[id];
if (pic) {
return `<img class="player-avatar" src="data:image/png;base64,${pic}" alt="">`;
}
const initial = (name || '?').trim().charAt(0);
return `<span class="player-avatar-initials">${initial}</span>`;
}
function buildPlayerAccHtml(id, nameValue, isSelf, nameReadOnly = false, genderDisabled = false, allDisabled = false) {
const num = playerIds.indexOf(id) + 1;
const selfBadge = isSelf ? '<span class="player-badge">Du</span>' : '';
const inviteBtn = (!isSelf && !allDisabled)
? `<button class="btn-invite" onclick="oeffneFreundeModal(${id});event.stopPropagation()">👥 Einladen</button>` : '';
const removeBtn = !allDisabled
? `<button class="player-remove" onclick="removePlayer(${id});event.stopPropagation()">✕</button>` : '';
const nameLabel = nameValue ? `: ${nameValue}` : '';
return `
<div class="player-acc-item" id="player-${id}">
<div class="player-acc-header" id="pacc-${id}-btn" onclick="togglePlayerAcc(${id})">
<span id="pacc-${id}-avatar">${buildAvatarHtml(id, nameValue)}</span>
<span class="player-title">Spieler ${num}<span id="pacc-${id}-namelabel">${nameLabel}</span></span>
${selfBadge}<span id="pacc-${id}-badge"></span>
${inviteBtn}${removeBtn}
<span class="player-acc-chevron">▾</span>
</div>
<div class="player-acc-body" id="pacc-${id}-body">
<div id="p${id}-body">${buildPlayerBody(id, nameValue, nameReadOnly, genderDisabled, allDisabled)}</div>
</div>
</div>`;
}
function addPlayer(nameValue = '', isSelf = false, nameReadOnly = false, genderDisabled = false, allDisabled = false) {
playerSeq++;
const id = playerSeq;
if (isSelf) selfPlayerId = id;
playerIds.push(id);
playerInvitations[id] = null;
playerProfilePics[id] = null;
document.getElementById('playersContainer').insertAdjacentHTML('beforeend',
buildPlayerAccHtml(id, nameValue, isSelf, nameReadOnly, genderDisabled, allDisabled));
refreshRemoveButtons();
return id;
}
function removePlayer(id) {
const inv = playerInvitations[id];
if (inv && ['PENDING','ACCEPTED_OWN','ACCEPTED_HOST'].includes(inv.status))
fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
playerInvitations[id] = null;
if (playerIds.length <= 2) {
document.getElementById(`pacc-${id}-badge`).innerHTML = '';
const invBtn = document.querySelector(`#player-${id} .btn-invite`);
if (invBtn) invBtn.style.display = '';
document.getElementById(`p${id}-body`).innerHTML = buildPlayerBody(id, '', false, false, false);
return;
}
document.getElementById('player-'+id)?.remove();
playerIds = playerIds.filter(x => x !== id);
delete playerInvitations[id];
delete playerProfilePics[id];
refreshPlayerTitles(); refreshRemoveButtons();
}
function updatePlayerHeader(id, name) {
const avatarEl = document.getElementById(`pacc-${id}-avatar`);
const nameLabel = document.getElementById(`pacc-${id}-namelabel`);
if (avatarEl) avatarEl.innerHTML = buildAvatarHtml(id, name);
if (nameLabel) nameLabel.textContent = name ? `: ${name}` : '';
}
function refreshPlayerTitles() {
playerIds.forEach((id, idx) => {
const titleEl = document.querySelector(`#player-${id} .player-title`);
if (!titleEl) return;
const nameLabel = document.getElementById(`pacc-${id}-namelabel`);
const currentName = nameLabel ? nameLabel.textContent.replace(/^: /, '') : '';
titleEl.innerHTML = `Spieler ${idx + 1}<span id="pacc-${id}-namelabel">${currentName ? ': ' + currentName : ''}</span>`;
});
}
function refreshRemoveButtons() {
playerIds.forEach((id, idx) => {
const btn = document.querySelector(`#player-${id} .player-remove`);
if (btn) btn.style.display = idx === 0 ? 'none' : '';
});
}
document.addEventListener('change', e => {
const input = e.target;
// Select-all checkbox for gruppe sections
if (input.classList.contains('select-all-cb')) {
const list = document.getElementById(input.dataset.list);
list.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = input.checked;
cb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
});
onGruppenChanged(); return;
}
// Gruppe item checkbox
if (input.closest('.gruppe-item')) {
input.closest('.gruppe-item')?.classList.toggle('is-checked', input.checked);
updateSelectAll(input.closest('.gruppe-list'));
onGruppenChanged(); return;
}
// Toy checkbox
if (input.closest('.toy-item')) {
input.closest('.toy-item')?.classList.toggle('is-checked', input.checked);
warnungsAkzeptiert = false; hideMessage(); return;
}
// Player checkboxes
if (input.type !== 'checkbox' && input.type !== 'radio') return;
if (input.type === 'radio') {
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r =>
r.closest('.check-item')?.classList.toggle('is-checked', r.checked));
if (input.checked && input.name.endsWith('-geschlecht')) {
const prefix = input.name.slice(0, -'-geschlecht'.length);
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
document.querySelectorAll(`input[name="${prefix}-werkzeuge"]`).forEach(cb => {
if (cb.closest('.check-item')?.dataset.chastitylocked) return;
cb.checked = defaults.includes(cb.value);
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
});
}
} else {
const lbl = input.closest('.check-item');
if (lbl?.dataset.chastitylocked) {
input.checked = false; lbl.classList.remove('is-checked');
const card = lbl.closest('[id^="player-"]');
if (card) flashChastityHint(card.id.replace('player-', ''));
return;
}
lbl?.classList.toggle('is-checked', input.checked);
}
});
function onGruppenChanged() {
warnungsAkzeptiert = false; hideMessage();
gruppenContent = null; loadedForGruppen = null; toysNeedReload = true;
if (document.getElementById('acc-toys-body').classList.contains('is-open')) ladeToys();
}
function updateSelectAll(list) {
if (!list) return;
const items = [...list.querySelectorAll('input[type="checkbox"]')];
if (!items.length) return;
const section = list.closest('[id^="section"]');
const cb = section?.querySelector('.select-all-cb'); if (!cb) return;
const n = items.filter(c => c.checked).length;
cb.checked = n === items.length; cb.indeterminate = n > 0 && n < items.length;
}
function getChecked(name) {
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
}
function getSelectedGruppen() {
return [...document.querySelectorAll('.gruppe-list input[type="checkbox"]:checked')].map(cb => cb.value);
}
function toggleSperreWarning(id) {
const cb = document.getElementById(`p${id}-sperrenAufloesen`);
const warn = document.getElementById(`p${id}-sperre-warn`);
const lbl = document.getElementById(`p${id}-sperre-label`);
if (!cb) return;
if (warn) warn.style.display = cb.checked ? 'none' : 'block';
if (lbl) lbl.classList.toggle('is-checked', cb.checked);
}
function setFieldError(id, show) {
const el = document.getElementById(id); if (el) el.style.display = show ? 'block' : 'none';
}
// ── Gruppe lists ──
function renderGruppeList(containerId, gruppen) {
const ul = document.getElementById(containerId);
const section = ul.closest('[id^="section"]');
const selectAllWrap = section?.querySelector('.select-all-label');
if (!gruppen.length) {
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden'; return;
}
ul.innerHTML = gruppen.map(g => {
const checked = savedGruppen.has(g.gruppenId);
return `<li><label class="gruppe-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
<span><span class="gruppe-item-name">${g.name}</span>${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}</span>
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
</label></li>`;
}).join('');
updateSelectAll(ul);
}
// ── Toys ──
async function ladeGruppenContent() {
const selected = getSelectedGruppen();
const key = JSON.stringify([...selected].sort());
if (loadedForGruppen === key && gruppenContent) return gruppenContent;
if (!selected.length) {
gruppenContent = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
loadedForGruppen = key; return gruppenContent;
}
const gruppen = await Promise.all(selected.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null)));
gruppenContent = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
gruppen.filter(Boolean).forEach(g => {
gruppenContent.aufgaben.push(...(g.aufgaben || []));
gruppenContent.strafen.push(...(g.strafen || []));
gruppenContent.sperren.push(...(g.sperren || []));
gruppenContent.finisher.push(...(g.finisher || []));
});
loadedForGruppen = key; toysNeedReload = false;
return gruppenContent;
}
async function ladeToys() {
const toyList = document.getElementById('toyList');
toyList.innerHTML = '<p class="empty-hint">Lade…</p>';
const content = await ladeGruppenContent();
const toyMap = new Map();
[...content.aufgaben, ...content.strafen, ...content.sperren, ...content.finisher].forEach(item =>
(item.benoetigteToys || []).forEach(t => { if (!toyMap.has(t.toyId)) toyMap.set(t.toyId, t); }));
const toys = [...toyMap.values()].sort((a, b) => a.name.localeCompare(b.name));
if (!toys.length) {
toyList.innerHTML = '<p class="empty-hint">Keine Toys erforderlich alle Aufgaben können gespielt werden.</p>'; return;
}
toyList.innerHTML = toys.map(toy => {
const checked = savedToyIds === null || savedToyIds.has(toy.toyId);
return `<label class="toy-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${toy.toyId}"${checked ? ' checked' : ''}>
<span><span class="toy-item-name">${toy.name}</span>${toy.beschreibung ? `<span class="toy-item-desc">${toy.beschreibung}</span>` : ''}</span>
${toy.bild ? `<img class="item-img" src="data:image/png;base64,${toy.bild}" alt="">` : ''}
</label>`;
}).join('');
}
function validateContent(content, settings, mitspieler) {
const errors = [], warnings = [];
const aufgabenByLevel = {};
content.aufgaben.forEach(a => { const l = a.level ?? 0; aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1; });
for (const [level, count] of Object.entries(aufgabenByLevel)) {
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) Minimum 5 erforderlich`);
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben empfohlen ≥ 10`);
}
if (settings.wahrscheinlichkeitStrafe > 1) {
const strafenByLevel = {};
content.strafen.forEach(s => { const l = s.level ?? 0; strafenByLevel[l] = (strafenByLevel[l] || 0) + 1; });
for (const level of Object.keys(aufgabenByLevel)) {
const count = strafenByLevel[level] || 0;
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) empfohlen ≥ 2`);
}
}
if (settings.wahrscheinlichkeitSperre > 1) {
const count = content.sperren.length;
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) empfohlen ≥ 5`);
}
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
for (const g of beteiligtGeschlecht) {
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
}
return { errors, warnings };
}
function showValidation(errors, warnings, mitHinweis) {
const el = document.getElementById('message');
el.innerHTML = [
...errors.map(e => `<div>✕ ${e}</div>`),
...warnings.map(w => `<div>⚠ ${w}</div>`),
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Spiel starten klicken um fortzufahren.</div>'] : []),
].join('');
el.className = `message ${errors.length ? 'error' : 'warning'}`; el.style.display = 'block';
}
// ── Invitation rendering ──
function renderPending(id) {
const inv = playerInvitations[id];
const badge = document.getElementById(`pacc-${id}-badge`);
const body = document.getElementById(`p${id}-body`);
const header = document.getElementById(`pacc-${id}-btn`);
const invBtn = header?.querySelector('.btn-invite');
if (!inv) {
if (badge) badge.innerHTML = '';
if (invBtn) invBtn.style.display = '';
if (body) body.innerHTML = buildPlayerBody(id, '', false, false, false);
playerProfilePics[id] = null;
updatePlayerHeader(id, '');
return;
}
if (invBtn) invBtn.style.display = 'none';
if (inv.status === 'PENDING') {
if (badge) badge.innerHTML = '<span class="player-badge-pending">Ausstehend</span>';
if (body) body.innerHTML = `<div class="pending-info"><div class="pending-name">${inv.inviteeName}</div><div>Einladung gesendet warte auf Antwort…</div><button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button></div>`;
updatePlayerHeader(id, inv.inviteeName);
} else if (inv.status === 'ACCEPTED_OWN') {
if (badge) badge.innerHTML = '<span class="player-badge-accepted">✓ Eigenes Gerät</span>';
if (body) body.innerHTML = `<div class="pending-info"><div class="pending-name">${inv.inviteeName}</div><div style="font-size:0.8rem;color:var(--color-muted);">Spieler konfiguriert Präferenzen auf dem eigenen Gerät.</div><button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button></div>`;
updatePlayerHeader(id, inv.inviteeName);
} else if (inv.status === 'ACCEPTED_HOST') {
if (badge) badge.innerHTML = '<span class="player-badge-accepted">✓ Host-Gerät</span>';
const hasGeschlecht = inv.defaults && inv.defaults.geschlecht;
if (body) body.innerHTML = buildPlayerBody(id, inv.inviteeName, true, hasGeschlecht, false);
if (inv.defaults) restorePlayer(id, { geschlecht: inv.defaults.geschlecht, spieltMit: inv.defaults.spieltMit || [], rollen: inv.defaults.rollen || [], werkzeuge: inv.defaults.werkzeuge || [] });
updatePlayerHeader(id, inv.inviteeName);
pruefeChastityConstraint(id, inv.inviteeId);
} else if (inv.status === 'DECLINED' || inv.status === 'CANCELLED') {
if (badge) badge.innerHTML = '';
if (invBtn) invBtn.style.display = '';
playerInvitations[id] = null;
playerProfilePics[id] = null;
if (body) body.innerHTML = buildPlayerBody(id, '', false, false, false);
updatePlayerHeader(id, '');
}
}
async function cancelEinladung(id) {
const inv = playerInvitations[id]; if (!inv) return;
await fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
playerInvitations[id] = null; renderPending(id);
}
// ── Freunde-Modal ──
let currentInvitePlayerId = null;
let selectedFriend = null;
async function oeffneFreundeModal(playerId) {
currentInvitePlayerId = playerId; selectedFriend = null;
document.getElementById('friendSearch').value = '';
document.getElementById('friendDropdown').style.display = 'none';
document.getElementById('friendDropdown').innerHTML = '';
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('btnEinladen').disabled = true;
document.getElementById('friendModal').style.display = 'flex';
if (freundeListe.length === 0) {
try { const r = await fetch('/social/friends'); freundeListe = r.ok ? await r.json() : []; } catch (_) { freundeListe = []; }
}
}
function filterFreunde(query) {
selectedFriend = null;
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('btnEinladen').disabled = true;
const dropdown = document.getElementById('friendDropdown'); dropdown.innerHTML = '';
const q = query.trim().toLowerCase();
if (!q) { dropdown.style.display = 'none'; return; }
const invitedIds = new Set(Object.values(playerInvitations)
.filter(inv => inv && ['PENDING','ACCEPTED_OWN','ACCEPTED_HOST'].includes(inv.status)).map(inv => inv.inviteeId));
const matches = freundeListe.filter(f => (f.user.name||'').toLowerCase().includes(q) && !invitedIds.has(f.user.userId));
if (!matches.length) {
dropdown.innerHTML = '<div style="padding:0.6rem 0.75rem;color:var(--color-muted);font-size:0.9rem;">Keine Treffer.</div>';
dropdown.style.display = 'block'; return;
}
matches.forEach(f => {
const item = document.createElement('div'); item.className = 'friend-dropdown-item';
item.addEventListener('click', () => selectFriend(f.user.userId, f.user.name || 'Unbekannt', f.user.profilePicture || null));
if (f.user.profilePicture) { const img = document.createElement('img'); img.className = 'friend-avatar'; img.src = 'data:image/png;base64,'+f.user.profilePicture; img.alt=''; item.appendChild(img); }
else { const av = document.createElement('div'); av.className = 'friend-avatar'; item.appendChild(av); }
const span = document.createElement('span'); span.textContent = f.user.name || 'Unbekannt'; item.appendChild(span);
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
}
function selectFriend(userId, name, profilePicture = null) {
selectedFriend = { userId, name, profilePicture };
document.getElementById('friendSearch').value = name;
document.getElementById('friendDropdown').style.display = 'none';
const box = document.getElementById('selectedFriendBox'); box.textContent = '✓ '+name; box.style.display = 'block';
document.getElementById('btnEinladen').disabled = false;
}
async function confirmedEinladen() { if (selectedFriend) await einladen(selectedFriend.userId, selectedFriend.name, selectedFriend.profilePicture); }
function schliesseFriendModal() { document.getElementById('friendModal').style.display = 'none'; currentInvitePlayerId = null; selectedFriend = null; }
async function einladen(inviteeId, inviteeName, inviteePicture = null) {
const id = currentInvitePlayerId; schliesseFriendModal(); if (!id) return;
// Sicherstellen dass setupId gesetzt ist
if (!setupId) {
if (!sessionStorage.getItem('bdsm-setup-id')) sessionStorage.setItem('bdsm-setup-id', crypto.randomUUID());
setupId = sessionStorage.getItem('bdsm-setup-id');
}
await ensureDraftExists();
try {
const res = await fetch('/bdsm/einladung', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ setupId, slotIndex: id, inviteeId }),
});
if (res.status === 409) { zeigePopup('Bereits eingeladen', `${inviteeName} ist bereits eingeladen oder nimmt schon am Spiel teil.`); return; }
if (!res.ok) throw new Error();
const data = await res.json();
playerInvitations[id] = { einladungId: data.einladungId, status: 'PENDING', inviteeId, inviteeName };
if (inviteePicture) { playerProfilePics[id] = inviteePicture; updatePlayerHeader(id, inviteeName); }
renderPending(id); startPoll();
} catch (_) { zeigePopup('Fehler', 'Einladung konnte nicht gesendet werden. Bitte versuche es erneut.'); }
}
async function ensureDraftExists() {
if (!setupId) return;
await saveDraftToServer();
}
function buildSettingsFromDom() {
const strafe = parseInt(document.getElementById('sldStrafe')?.value || '15');
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe')?.value || '15');
const spieldauerIdx = parseInt(document.getElementById('sldSpieldauer')?.value || '2');
const sd = SPIELDAUER[spieldauerIdx] || SPIELDAUER[2];
return { wahrscheinlichkeitStrafe: strafe, wahrscheinlichkeitSperre: zeitstrafe,
aufgabenProLevel: sd.aufgaben, zeitfaktorZeitstrafen: sd.zeitfaktor };
}
function buildSetupJsonFromDom() {
const settings = buildSettingsFromDom();
const mitspieler = playerIds.map(id => {
const inv = playerInvitations[id];
if (inv && (inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST' || inv.status === 'PENDING')) {
return { name: inv.inviteeName || '', userId: inv.inviteeId,
eigenesGeraet: inv.status === 'ACCEPTED_OWN',
geschlecht: null, spieltMit: [], rollen: [], werkzeuge: [], sperrenVorFinaleAufloesen: true };
}
const name = document.getElementById(`p${id}-name`)?.value?.trim() || '';
const geschlecht = getChecked(`p${id}-geschlecht`);
const spieltMit = getChecked(`p${id}-spieltmit`);
const rollen = getChecked(`p${id}-rollen`);
const werkzeuge = getChecked(`p${id}-werkzeuge`);
const sperre = document.getElementById(`p${id}-sperrenAufloesen`);
return { name, geschlecht: geschlecht[0] || null, spieltMit, rollen, werkzeuge,
userId: id === selfPlayerId ? myUserId : null,
eigenesGeraet: false, sperrenVorFinaleAufloesen: sperre ? sperre.checked : true };
});
return JSON.stringify({ settings, mitspieler });
}
async function saveDraftToServer() {
if (!setupId) return;
const settingsJson = JSON.stringify(buildSettingsFromDom());
const setupJson = buildSetupJsonFromDom();
const selected = getSelectedGruppen();
const gruppenJson = selected.length ? JSON.stringify(selected) : null;
await fetch('/bdsm/setup-draft', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ setupId, settingsJson, setupJson, gruppenJson }),
}).catch(() => {});
}
function startPoll() { if (pollIntervalId) return; pollIntervalId = setInterval(pollEinladungen, 3000); }
function stopPoll() { if (pollIntervalId) { clearInterval(pollIntervalId); pollIntervalId = null; } }
async function pollEinladungen() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (!hasPending) { stopPoll(); return; }
try {
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`); if (!res.ok) return;
const liste = await res.json();
for (const e of liste) {
const id = playerIds.find(pid => playerInvitations[pid]?.einladungId === e.einladungId);
if (!id) continue;
const inv = playerInvitations[id]; if (!inv || inv.status === e.status) continue;
inv.status = e.status;
if (e.status === 'DECLINED' || e.status === 'CANCELLED')
showMessage(`${inv.inviteeName} hat die Einladung abgelehnt oder abgebrochen.`, 'error');
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
try { const r = await fetch(`/user/${inv.inviteeId}/bdsm-defaults`); if (r.ok) inv.defaults = await r.json(); } catch (_) {}
}
renderPending(id);
}
} catch (_) {}
updateActionBtn();
}
function updateActionBtn() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
document.getElementById('btnAction').disabled = hasPending;
}
// ── Chastity ──
async function pruefeChastityConstraint(playerId, userId) {
if (!userId) return;
try {
const res = await fetch(`/bdsm/chastity-constraint?userId=${userId}`); if (!res.ok) return;
const { lockedWerkzeug } = await res.json(); if (!lockedWerkzeug) return;
const locked = lockedWerkzeug === 'BOTH' ? ['VAGINA','PENIS'] : [lockedWerkzeug];
locked.forEach(w => sperrWerkzeugCheckbox(playerId, w));
const hintEl = document.getElementById(`p${playerId}-chastity-hint`);
if (hintEl) hintEl.dataset.hintText = '🔒 Das System weiß aus halbwegs verlässlicher Quelle, dass diese Person dieses Körperteil gerade nicht einsetzen kann.';
} catch (_) {}
}
function sperrWerkzeugCheckbox(playerId, werkzeug) {
const cb = document.querySelector(`input[name="p${playerId}-werkzeuge"][value="${werkzeug}"]`); if (!cb) return;
cb.checked = false; cb.disabled = false;
const lbl = cb.closest('.check-item');
if (lbl) { lbl.classList.remove('is-checked','is-disabled'); lbl.dataset.chastitylocked = '1'; }
}
function flashChastityHint(playerId) {
const hintEl = document.getElementById(`p${playerId}-chastity-hint`); const text = hintEl?.dataset.hintText; if (!text) return;
document.getElementById('errorModalTitle').textContent = 'Nicht verfügbar';
document.getElementById('errorModalText').textContent = text;
document.getElementById('errorModal').style.display = 'flex';
}
document.addEventListener('click', e => {
const lbl = e.target.closest('.check-item[data-chastitylocked]'); if (!lbl) return;
const card = lbl.closest('[id^="player-"]'); if (card) flashChastityHint(card.id.replace('player-', ''));
});
function restorePlayer(id, data) {
if (data.geschlecht) {
const r = document.querySelector(`input[name="p${id}-geschlecht"][value="${data.geschlecht}"]`);
if (r) { r.checked = true; r.closest('.check-item')?.classList.add('is-checked'); }
}
(data.spieltMit || []).forEach(val => { const cb = document.querySelector(`input[name="p${id}-spieltmit"][value="${val}"]`); if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); } });
(data.rollen || []).forEach(val => { const cb = document.querySelector(`input[name="p${id}-rollen"][value="${val}"]`); if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); } });
(data.werkzeuge || []).forEach(val => { const cb = document.querySelector(`input[name="p${id}-werkzeuge"][value="${val}"]`); if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); } });
if (data.sperrenVorFinaleAufloesen === false) { const cb = document.getElementById(`p${id}-sperrenAufloesen`); if (cb) { cb.checked = false; toggleSperreWarning(id); } }
}
async function ladeEinladungenAusDb(userIdToInfo) {
try {
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`); if (!res.ok) return;
const einladungen = await res.json();
const aktive = einladungen.filter(e => ['PENDING','ACCEPTED_OWN','ACCEPTED_HOST'].includes(e.status));
for (const e of aktive) {
let playerId = userIdToInfo?.[e.inviteeId]?.playerId ?? playerIds.find(pid => pid === e.slotIndex);
if (!playerId) continue;
const inviteeName = userIdToInfo?.[e.inviteeId]?.name || e.inviteeName || '';
playerInvitations[playerId] = { einladungId: e.einladungId, status: e.status, inviteeId: e.inviteeId, inviteeName };
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
try { const r = await fetch(`/user/${e.inviteeId}/bdsm-defaults`); if (r.ok) playerInvitations[playerId].defaults = await r.json(); } catch (_) {}
}
renderPending(playerId);
}
if (aktive.some(e => e.status === 'PENDING')) startPoll();
updateActionBtn();
} catch (_) {}
}
// ── Action handler ──
function handleAction() {
if (isGuestMode) bereitMachen(); else spielStarten();
}
// ── Validate players → returns mitspieler array or null ──
function validiereUndSammleMitspieler() {
let valid = true;
playerIds.forEach(id => setFieldError(`p${id}-partner-err`, false));
const mitspieler = playerIds.map(id => {
const inv = playerInvitations[id];
if (inv && inv.status === 'ACCEPTED_OWN') {
return { name: inv.inviteeName, geschlecht: null, spieltMit: [], rollen: [], werkzeuge: [],
userId: inv.inviteeId, eigenesGeraet: true, einladungId: inv.einladungId, sperrenVorFinaleAufloesen: true };
}
const name = document.getElementById(`p${id}-name`)?.value.trim() || '';
const geschlecht = getChecked(`p${id}-geschlecht`);
const spieltMit = getChecked(`p${id}-spieltmit`);
const rollen = getChecked(`p${id}-rollen`);
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-name-err`, !name);
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!name || !geschlecht.length || !spieltMit.length || !rollen.length || !werkzeuge.length) valid = false;
const sperre = document.getElementById(`p${id}-sperrenAufloesen`);
return { name, geschlecht: geschlecht[0] || null, spieltMit, rollen, werkzeuge,
userId: inv ? inv.inviteeId : (id === selfPlayerId ? myUserId : null),
eigenesGeraet: false, sperrenVorFinaleAufloesen: sperre ? sperre.checked : true };
});
if (!valid) { return null; }
const configured = mitspieler.filter(p => !p.eigenesGeraet);
if (!mitspieler.some(p => p.eigenesGeraet)) {
const allRoles = new Set(configured.flatMap(p => p.rollen));
const missing = Object.keys(ROLE_LABELS).filter(r => !allRoles.has(r));
if (missing.length > 0) { showMessage('Folgende Rollen müssen mindestens einmal vergeben sein: ' + missing.map(r => ROLE_LABELS[r]).join(', '), 'error'); return null; }
let partnerFehler = false;
configured.forEach((player, i) => {
const andere = configured.filter((_, j) => j !== i).map(p => p.geschlecht);
if (!player.spieltMit.some(g => andere.includes(g))) {
setFieldError(`p${playerIds[mitspieler.indexOf(player)]}-partner-err`, true);
partnerFehler = true;
}
});
if (partnerFehler) { showMessage('Mindestens ein Spieler hat keinen kompatiblen Mitspieler.', 'error'); return null; }
}
return mitspieler;
}
// ── Spiel starten ──
async function spielStarten() {
hideMessage();
const strafe = parseInt(document.getElementById('sldStrafe').value);
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
if (strafe + zeitstrafe > 98) { showMessage('Die kombinierten Wahrscheinlichkeiten dürfen 98 % nicht überschreiten.', 'error'); return; }
if (Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING')) {
showMessage('Bitte warte, bis alle Einladungen beantwortet wurden.', 'error'); return;
}
const mitspieler = validiereUndSammleMitspieler();
if (mitspieler === null) {
// Open Mitspieler accordion to show errors
ACC_IDS.forEach(a => { document.getElementById(`acc-${a}-btn`)?.classList.remove('is-open'); document.getElementById(`acc-${a}-body`)?.classList.remove('is-open'); });
document.getElementById('acc-mitspieler-btn').classList.add('is-open');
document.getElementById('acc-mitspieler-body').classList.add('is-open');
if (!document.getElementById('message').style.display || document.getElementById('message').style.display === 'none')
showMessage('Bitte alle Felder für jeden Spieler ausfüllen.', 'error');
return;
}
if (getSelectedGruppen().length === 0) {
ACC_IDS.forEach(a => { document.getElementById(`acc-${a}-btn`)?.classList.remove('is-open'); document.getElementById(`acc-${a}-body`)?.classList.remove('is-open'); });
document.getElementById('acc-aufgaben-btn').classList.add('is-open');
document.getElementById('acc-aufgaben-body').classList.add('is-open');
showMessage('Bitte mindestens eine Aufgaben-Gruppe auswählen.', 'error'); return;
}
const btn = document.getElementById('btnAction'); btn.disabled = true;
const content = await ladeGruppenContent();
// ACCEPTED_OWN: bereit prüfen und Daten laden
if (mitspieler.some(p => p.eigenesGeraet)) {
const einladungRes = await fetch(`/bdsm/einladung?setupId=${setupId}`);
if (einladungRes.ok) {
const einladungen = await einladungRes.json();
const nichtBereit = einladungen.filter(e => e.status === 'ACCEPTED_OWN' && !e.bereit);
if (nichtBereit.length > 0) {
showMessage('Noch nicht alle Mitspieler auf eigenem Gerät haben sich bereit erklärt.', 'error');
btn.disabled = false; return;
}
for (const p of mitspieler) {
if (!p.eigenesGeraet || !p.einladungId) continue;
const inv = einladungen.find(e => e.einladungId === p.einladungId);
if (inv && inv.spielerDatenJson) {
const daten = JSON.parse(inv.spielerDatenJson);
p.geschlecht = daten.geschlecht; p.spieltMit = daten.spieltMit || [];
p.rollen = daten.rollen || []; p.werkzeuge = daten.werkzeuge || [];
p.sperrenVorFinaleAufloesen = daten.sperrenVorFinaleAufloesen !== false;
}
}
}
}
// Toy-Filter
const checkedToyIds = new Set([...document.querySelectorAll('#toyList input[type="checkbox"]:checked')].map(cb => cb.value));
const toyMap = new Map();
[...content.aufgaben, ...content.strafen, ...content.sperren, ...content.finisher].forEach(item =>
(item.benoetigteToys || []).forEach(t => toyMap.set(t.toyId, t)));
const checkedToys = [...checkedToyIds].map(id => toyMap.get(id)).filter(Boolean);
sessionStorage.setItem('bdsm-session-toys', JSON.stringify(checkedToys));
function toyOk(item) { const t = item.benoetigteToys||[]; return t.length === 0 || t.every(x => checkedToyIds.has(x.toyId)); }
const gameContent = {
aufgaben: content.aufgaben.filter(toyOk),
strafen: content.strafen.filter(toyOk),
sperren: content.sperren.filter(toyOk),
finisher: content.finisher.filter(toyOk),
};
const spieldauerIdx = parseInt(document.getElementById('sldSpieldauer').value);
const sd = SPIELDAUER[spieldauerIdx];
const settings = {
wahrscheinlichkeitStrafe: strafe, wahrscheinlichkeitSperre: zeitstrafe,
aufgabenProLevel: sd.aufgaben, zeitfaktorZeitstrafen: sd.zeitfaktor,
};
const { errors, warnings } = validateContent(gameContent, settings, mitspieler);
if (errors.length > 0) { showValidation(errors, warnings, false); warnungsAkzeptiert = false; btn.disabled = false; return; }
if (warnings.length > 0 && !warnungsAkzeptiert) { showValidation([], warnings, true); warnungsAkzeptiert = true; btn.disabled = false; return; }
const selected = getSelectedGruppen();
sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected));
sessionStorage.setItem('bdsm-session-game', JSON.stringify(gameContent));
const sessionSetup = JSON.stringify({ settings, mitspieler });
sessionStorage.setItem('bdsm-session-settings', JSON.stringify(settings));
sessionStorage.setItem('bdsm-session-setup', sessionSetup);
try {
const sessionRes = await fetch('/bdsm', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wahrscheinlichkeitStrafe: settings.wahrscheinlichkeitStrafe,
wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre,
aufgabenProLevel: settings.aufgabenProLevel,
zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen,
setupId: setupId,
}),
});
if (sessionRes.status === 409) throw new Error('Du hast bereits ein laufendes BDSM-Spiel. Bitte beende es zuerst.');
if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.');
const location = sessionRes.headers.get('Location');
const sessionId = location.split('/').pop();
for (const p of mitspieler) {
const res = await fetch(`/bdsm/${sessionId}/mitspieler`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: p.name, geschlecht: p.geschlecht, spieltMit: p.spieltMit,
rollen: p.rollen, verfuegbareWerkzeuge: p.werkzeuge, userId: p.userId || null,
eigenesGeraet: p.eigenesGeraet || false, sperrenVorFinaleAufloesen: p.sperrenVorFinaleAufloesen !== false }),
});
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
}
const aufgabenRes = await fetch(`/bdsm/${sessionId}/aufgaben`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(gameContent),
});
if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.');
sessionStorage.setItem('bdsm-session-id', sessionId);
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
window.location.href = '/games/bdsm/bdsmingame.html';
} catch (e) { showMessage(e.message, 'error'); btn.disabled = false; }
}
// ── Gast: Bereit ──
async function bereitMachen() {
const id = guestOwnPlayerId; if (!id) return;
const geschlecht = getChecked(`p${id}-geschlecht`);
const spieltMit = getChecked(`p${id}-spieltmit`);
const rollen = getChecked(`p${id}-rollen`);
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!geschlecht.length || !spieltMit.length || !rollen.length || !werkzeuge.length) {
showMessage('Bitte alle Felder ausfüllen.', 'error'); return;
}
const sperre = document.getElementById(`p${id}-sperrenAufloesen`);
try {
const res = await fetch(`/bdsm/einladung/${guestEinladungId}/spielerdaten`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ spielerDatenJson: JSON.stringify({
geschlecht: geschlecht[0], spieltMit, rollen, werkzeuge,
sperrenVorFinaleAufloesen: sperre ? sperre.checked : true,
})}),
});
if (!res.ok) throw new Error();
document.getElementById('setupView').style.display = 'none';
document.getElementById('waitView').style.display = 'block';
guestPollInterval = setInterval(pruefeSpielstart, 3000);
pruefeSpielstart();
} catch (_) { showMessage('Fehler beim Speichern. Bitte erneut versuchen.', 'error'); }
}
async function pruefeSpielstart() {
try {
const res = await fetch(`/bdsm/einladung/${guestEinladungId}`); if (!res.ok) return;
const data = await res.json();
if (data.status === 'CANCELLED') {
stopGuestPoll();
document.getElementById('setupView').style.display = 'block';
document.getElementById('waitView').style.display = 'none';
showMessage('Die Einladung wurde abgebrochen.', 'error'); return;
}
if (data.sessionId) {
try {
const mRes = await fetch(`/bdsm/${data.sessionId}/mitspieler/me`);
if (mRes.status === 200) {
const mData = await mRes.json();
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
sessionStorage.setItem('bdsm-guest-name', mData.name);
sessionStorage.setItem('bdsm-session-id', data.sessionId);
sessionStorage.setItem('bdsm-is-guest', 'true');
stopGuestPoll(); window.location.replace('/games/bdsm/bdsmingame.html');
}
} catch (_) {}
}
} catch (_) {}
}
function stopGuestPoll() { if (guestPollInterval) { clearInterval(guestPollInterval); guestPollInterval = null; } }
async function abbrechenGast() {
stopGuestPoll();
await fetch(`/bdsm/einladung/${guestEinladungId}`, { method: 'DELETE' }).catch(() => {});
window.location.href = '/userhome.html';
}
// ── Session-Modal ──
function zeigeSessionModal(title, text, actions) {
document.getElementById('sessionModalTitle').textContent = title;
const textEl = document.getElementById('sessionModalText'); textEl.textContent = text; textEl.style.display = text ? '' : 'none';
const actEl = document.getElementById('sessionModalActions'); actEl.innerHTML = '';
actions.forEach(a => { const btn = document.createElement('button'); btn.textContent = a.label; btn.className = a.primary ? 'full-width' : 'full-width secondary'; btn.onclick = () => a.onClick(); actEl.appendChild(btn); });
document.getElementById('sessionModal').style.display = 'flex';
}
function sessionFortfahren(sid) {
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
sessionStorage.setItem('bdsm-session-id', sid); window.location.href = '/games/bdsm/bdsmingame.html';
}
function sessionBeendenFragen(sid) {
zeigeSessionModal('Session wirklich beenden?', 'Die Session und alle aktiven Sperren werden gelöscht.', [
{ label: 'Ja, beenden', primary: true, onClick: () => sessionLoeschen(sid) },
{ label: 'Nein, fortfahren', onClick: () => sessionFortfahren(sid) },
]);
}
async function sessionLoeschen(sid) {
document.getElementById('sessionModal').style.display = 'none';
try { await fetch('/bdsm', { method: 'DELETE', headers: {'Content-Type':'application/json'}, body: JSON.stringify({sessionId:sid}) }); } catch(_) {}
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
}
function zeigePopup(title, text) { document.getElementById('errorModalTitle').textContent = title; document.getElementById('errorModalText').textContent = text; document.getElementById('errorModal').style.display = 'flex'; }
function showMessage(text, type) { const el = document.getElementById('message'); el.textContent = text; el.className = `message ${type}`; el.style.display = 'block'; }
function hideMessage() { document.getElementById('message').style.display = 'none'; }
// ── Init ──
async function init() {
try {
const meRes = await fetch('/login/me');
if (!meRes.ok) { window.location.replace('/login.html'); return; }
const user = await meRes.json();
myUserId = user?.userId || null;
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
if (sessionRes.status === 200) {
const session = await sessionRes.json();
await initHostSetup(user);
zeigeSessionModal('Aktive Session vorhanden', 'Du hast noch eine laufende Session. Möchtest du fortfahren?', [
{ label: 'Ja, fortfahren', primary: true, onClick: () => sessionFortfahren(session.sessionId) },
{ label: 'Nein', onClick: () => sessionBeendenFragen(session.sessionId) },
]);
return;
}
const einladungRes = await fetch('/bdsm/einladung/meine-aktive');
if (einladungRes.status === 200) {
const einladung = await einladungRes.json();
if (einladung.sessionId) {
try { const mRes = await fetch(`/bdsm/${einladung.sessionId}/mitspieler/me`); if (mRes.ok) { const mData = await mRes.json(); sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId); sessionStorage.setItem('bdsm-guest-name', mData.name); } } catch (_) {}
sessionStorage.setItem('bdsm-session-id', einladung.sessionId);
sessionStorage.setItem('bdsm-is-guest', 'true');
window.location.replace('/games/bdsm/bdsmingame.html'); return;
}
await initGuestMode(einladung, user); return;
}
await initHostSetup(user);
} catch (_) { document.getElementById('setupView').style.display = 'block'; }
}
async function ladeGruppenListen() {
try {
const [own, abo, system] = await Promise.all([
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }).catch(() => ({ content: [] })),
fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }).catch(() => ({ content: [] })),
fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }).catch(() => ({ content: [] })),
]);
renderGruppeList('listOwn', own.content || []);
renderGruppeList('listSubscribed', abo.content || []);
renderGruppeList('listSystem', system.content || []);
} catch (_) {
renderGruppeList('listOwn', []);
renderGruppeList('listSubscribed', []);
renderGruppeList('listSystem', []);
}
}
async function initHostSetup(user) {
if (!sessionStorage.getItem('bdsm-setup-id')) sessionStorage.setItem('bdsm-setup-id', crypto.randomUUID());
setupId = sessionStorage.getItem('bdsm-setup-id');
document.getElementById('setupView').style.display = 'block';
// Prüfe ob gespeicherter Setup mit gültigen Spielern vorhanden
let restoredFromSetup = false;
const savedSetup = sessionStorage.getItem('bdsm-session-setup');
if (savedSetup) {
try {
const { settings, mitspieler } = JSON.parse(savedSetup);
if (mitspieler && mitspieler.length > 0) {
if (settings) applySettings(settings);
const userIdToInfo = {};
mitspieler.forEach((p, i) => {
const id = addPlayer(p.name, i === 0, i === 0, false, false);
restorePlayer(id, p);
if (p.userId) userIdToInfo[p.userId] = { playerId: id, name: p.name };
});
mitspieler.forEach((p, i) => { if (p.userId) pruefeChastityConstraint(playerIds[i], p.userId); });
await ladeEinladungenAusDb(userIdToInfo);
restoredFromSetup = true;
} else {
// Leere oder ungültige Setup-Daten aus sessionStorage entfernen
sessionStorage.removeItem('bdsm-session-setup');
}
} catch (_) {}
}
if (!restoredFromSetup) {
const savedSettings = sessionStorage.getItem('bdsm-session-settings');
if (savedSettings) {
applySettings(JSON.parse(savedSettings));
} else {
let draft = null;
try {
const res = await fetch('/bdsm/setup-draft');
if (res.status === 200) draft = await res.json();
} catch (_) {}
if (draft && (draft.settingsJson || draft.setupJson || draft.gruppenJson)) {
const loadOld = await new Promise(resolve => {
zeigeSessionModal(
'Konfiguration gefunden',
'Es gibt noch eine unvollständige Einrichtung vom letzten Mal. Möchtest du fortfahren oder neu beginnen?',
[
{ label: 'Fortfahren', primary: true, onClick: () => { document.getElementById('sessionModal').style.display = 'none'; resolve(true); } },
{ label: 'Neu beginnen', onClick: () => { document.getElementById('sessionModal').style.display = 'none'; resolve(false); } },
]
);
});
if (loadOld) {
if (draft.setupId) { sessionStorage.setItem('bdsm-setup-id', draft.setupId); setupId = draft.setupId; }
if (draft.settingsJson) { sessionStorage.setItem('bdsm-session-settings', draft.settingsJson); applySettings(JSON.parse(draft.settingsJson)); }
if (draft.gruppenJson) { sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson); savedGruppen = new Set(JSON.parse(draft.gruppenJson)); }
} else {
// Alle Einladungen stornieren (Server sendet Benachrichtigung)
if (draft.setupId) {
try {
const invRes = await fetch(`/bdsm/einladung?setupId=${draft.setupId}`);
if (invRes.status === 200) {
const liste = await invRes.json();
await Promise.all(
liste
.filter(e => ['PENDING','ACCEPTED_OWN','ACCEPTED_HOST'].includes(e.status))
.map(e => fetch(`/bdsm/einladung/${e.einladungId}`, { method: 'DELETE' }).catch(() => {}))
);
}
} catch (_) {}
await fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
}
const newId = crypto.randomUUID();
sessionStorage.setItem('bdsm-setup-id', newId); setupId = newId;
sessionStorage.removeItem('bdsm-session-settings');
sessionStorage.removeItem('bdsm-session-gruppen');
}
} else if (draft) {
if (draft.setupId) { sessionStorage.setItem('bdsm-setup-id', draft.setupId); setupId = draft.setupId; }
if (draft.settingsJson) { sessionStorage.setItem('bdsm-session-settings', draft.settingsJson); applySettings(JSON.parse(draft.settingsJson)); }
if (draft.gruppenJson) { sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson); savedGruppen = new Set(JSON.parse(draft.gruppenJson)); }
}
}
const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
const selfGeschlecht = defaults.geschlecht || user?.geschlecht || null;
const selfWerkzeuge = defaults.werkzeuge?.length ? defaults.werkzeuge : (selfGeschlecht ? (WERKZEUGE_DEFAULTS[selfGeschlecht] || []) : []);
// Angemeldeten Benutzer als Spieler 1 eintragen, Felder aus Profil vorbelegen
const selfId = addPlayer(user ? user.name : '', true, true, !!selfGeschlecht, false);
if (user?.profilePicture) { playerProfilePics[selfId] = user.profilePicture; updatePlayerHeader(selfId, user.name); }
addPlayer();
restorePlayer(selfId, { geschlecht: selfGeschlecht, spieltMit: defaults.spieltMit || [], rollen: defaults.rollen || [], werkzeuge: selfWerkzeuge });
if (myUserId) pruefeChastityConstraint(selfId, myUserId);
await ladeEinladungenAusDb(null);
}
try {
savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]'));
} catch (_) { savedGruppen = new Set(); }
await ladeGruppenListen();
openPlayerAcc(playerIds[0]);
}
async function initGuestMode(einladung, user) {
isGuestMode = true;
guestEinladungId = einladung.einladungId;
document.getElementById('pageSubtitle').textContent = 'Konfiguriere deine Präferenzen';
document.getElementById('btnAction').textContent = 'Bereit';
document.getElementById('btnAddPlayer').style.display = 'none';
disableSettingsForGuest();
// Versuche Host-Draft zu laden
if (einladung.setupId) {
try {
const draft = await fetch(`/bdsm/setup-draft?setupId=${einladung.setupId}`).then(r => r.ok ? r.json() : null).catch(() => null);
if (draft?.settingsJson) applySettings(JSON.parse(draft.settingsJson));
if (draft?.setupJson) {
const { mitspieler } = JSON.parse(draft.setupJson);
mitspieler.forEach((p, i) => {
const isOwnSlot = einladung.slotIndex != null ? (i + 1 === einladung.slotIndex) : (p.eigenesGeraet && p.userId === myUserId);
const id = addPlayer(isOwnSlot ? (user?.name || p.name || '') : (p.name || ''), isOwnSlot, !isOwnSlot, !isOwnSlot, !isOwnSlot);
if (isOwnSlot) { guestOwnPlayerId = id; selfPlayerId = id; } else restorePlayer(id, p);
});
}
if (draft?.gruppenJson) { savedGruppen = new Set(JSON.parse(draft.gruppenJson)); await ladeGruppenListen(); }
} catch (_) {}
}
if (!guestOwnPlayerId) {
const id = addPlayer(user?.name || '', true, true, false, false);
guestOwnPlayerId = id; selfPlayerId = id;
}
if (user?.profilePicture) { playerProfilePics[guestOwnPlayerId] = user.profilePicture; updatePlayerHeader(guestOwnPlayerId, user?.name || ''); }
const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
restorePlayer(guestOwnPlayerId, { geschlecht: defaults.geschlecht || user?.geschlecht || null, spieltMit: defaults.spieltMit || [], rollen: defaults.rollen || [], werkzeuge: defaults.werkzeuge || [] });
if (myUserId) pruefeChastityConstraint(guestOwnPlayerId, myUserId);
// Mitspieler-Accordion öffnen, Grundeinstellungen schließen
document.getElementById('acc-grundeinstellungen-btn').classList.remove('is-open');
document.getElementById('acc-grundeinstellungen-body').classList.remove('is-open');
document.getElementById('acc-mitspieler-btn').classList.add('is-open');
document.getElementById('acc-mitspieler-body').classList.add('is-open');
openPlayerAcc(guestOwnPlayerId);
document.getElementById('setupView').style.display = 'block';
}
init();
</script>
</body>
</html>