Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
1344 lines
77 KiB
HTML
1344 lines
77 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Vanilla Game – 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:not([onclick]) { cursor: default; }
|
||
.player-acc-header:not([onclick]):hover { border-color: transparent; }
|
||
.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; }
|
||
.toys-badge { font-size: 0.75rem; font-weight: 600; background: var(--color-primary); color: #fff; border-radius: 999px; padding: 0.1em 0.55em; margin-left: 0.5rem; vertical-align: middle; }
|
||
|
||
/* ── 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>Vanilla Game – Session einrichten</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>
|
||
<hr class="settings-divider">
|
||
<div style="font-size:0.78rem;font-weight:600;color:var(--color-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.75rem;">Mitspieler</div>
|
||
<div id="playersContainer"></div>
|
||
<button class="add-player-btn" id="btnAddPlayer" onclick="addPlayer()">+ Spieler hinzufügen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Accordion 2: 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>
|
||
<p style="font-size:0.85rem;color:var(--color-muted);margin-bottom:0.75rem;">
|
||
Gruppen verwalten: <a href="/games/vanilla/aufgaben.html" style="color:var(--color-primary);">Aufgaben-Verwaltung (Vanilla)</a>
|
||
</p>
|
||
<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 id="toys-badge" class="toys-badge" style="display:none;"></span></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 VANILLA_STORAGE_KEYS = [
|
||
'vanilla-session-id','vanilla-session-settings','vanilla-session-setup',
|
||
'vanilla-session-gruppen','vanilla-session-toys','vanilla-session-game',
|
||
];
|
||
const ACC_IDS = ['grundeinstellungen','aufgaben','toys'];
|
||
const MAX_PLAYERS = 2;
|
||
|
||
// ── State ──
|
||
let isGuestMode = false;
|
||
let guestEinladungId = null;
|
||
let guestOwnPlayerId = null;
|
||
let guestPollInterval = null;
|
||
let playerSeq = 0;
|
||
let playerIds = [];
|
||
let playerInvitations = {};
|
||
let playerProfilePics = {};
|
||
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('vanilla-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;
|
||
if (playerIds.indexOf(id) === 1) return; // zweiter Spieler bleibt immer offen
|
||
playerIds.forEach(pid => {
|
||
const btn = document.getElementById(`pacc-${pid}-btn`);
|
||
const body = document.getElementById(`pacc-${pid}-body`);
|
||
if (!btn || !body) return;
|
||
if (playerIds.indexOf(pid) === 1) return; // zweiter Spieler nie anfassen
|
||
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;
|
||
if (playerIds.indexOf(pid) === 1) return; // zweiter Spieler bleibt immer offen
|
||
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 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.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);
|
||
}
|
||
}
|
||
|
||
function disableSettingsForGuest() {
|
||
['sldSpieldauer'].forEach(id => { const el = document.getElementById(id); if (el) el.disabled = true; });
|
||
document.getElementById('guestSettingsHint').style.display = 'block';
|
||
document.getElementById('guestAufgabenHint').style.display = 'block';
|
||
document.getElementById('guestToysHint').style.display = 'block';
|
||
}
|
||
|
||
// ── 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 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>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>`;
|
||
}
|
||
|
||
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 isSecond = num === 2;
|
||
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}` : '';
|
||
const openCls = isSecond ? ' is-open' : '';
|
||
const chevron = isSecond ? '' : '<span class="player-acc-chevron">▾</span>';
|
||
const clickAttr = isSecond ? '' : ` onclick="togglePlayerAcc(${id})"`;
|
||
return `
|
||
<div class="player-acc-item" id="player-${id}">
|
||
<div class="player-acc-header${openCls}" id="pacc-${id}-btn"${clickAttr}>
|
||
<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}
|
||
${chevron}
|
||
</div>
|
||
<div class="player-acc-body${openCls}" 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) {
|
||
if (playerIds.length >= MAX_PLAYERS) return null;
|
||
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();
|
||
updateAddPlayerButton();
|
||
return id;
|
||
}
|
||
|
||
function updateAddPlayerButton() {
|
||
const btn = document.getElementById('btnAddPlayer');
|
||
if (btn) btn.style.display = playerIds.length >= MAX_PLAYERS ? 'none' : '';
|
||
}
|
||
|
||
function removePlayer(id) {
|
||
const inv = playerInvitations[id];
|
||
if (inv && ['PENDING','ACCEPTED_OWN','ACCEPTED_HOST'].includes(inv.status))
|
||
fetch(`/vanilla/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);
|
||
updateAddPlayerButton();
|
||
return;
|
||
}
|
||
document.getElementById('player-'+id)?.remove();
|
||
playerIds = playerIds.filter(x => x !== id);
|
||
delete playerInvitations[id];
|
||
delete playerProfilePics[id];
|
||
refreshPlayerTitles(); refreshRemoveButtons(); updateAddPlayerButton();
|
||
}
|
||
|
||
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));
|
||
} 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;
|
||
const badge = document.getElementById('toys-badge');
|
||
badge.style.display = 'none'; badge.textContent = '';
|
||
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 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');
|
||
const filtered = gruppen.filter(g =>
|
||
(g.aufgaben || []).length > 0 || (g.finisher || []).length > 0
|
||
);
|
||
if (!filtered.length) {
|
||
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
|
||
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden'; return;
|
||
}
|
||
ul.innerHTML = filtered.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: [], finisher: [] };
|
||
loadedForGruppen = key; return gruppenContent;
|
||
}
|
||
const gruppen = await Promise.all(selected.map(id => fetch(`/vanilla/gruppe/${id}`).then(r => r.ok ? r.json() : null)));
|
||
gruppenContent = { aufgaben: [], finisher: [] };
|
||
gruppen.filter(Boolean).forEach(g => {
|
||
gruppenContent.aufgaben.push(...(g.aufgaben || []));
|
||
gruppenContent.finisher.push(...(g.finisher || []));
|
||
});
|
||
loadedForGruppen = key; toysNeedReload = false;
|
||
return gruppenContent;
|
||
}
|
||
|
||
async function ladeToys() {
|
||
const toyList = document.getElementById('toyList');
|
||
const selected = getSelectedGruppen();
|
||
if (!selected.length) {
|
||
toyList.innerHTML = '<p class="empty-hint">Bitte zuerst Aufgaben-Gruppen auswählen.</p>';
|
||
toysNeedReload = false; return;
|
||
}
|
||
toyList.innerHTML = '<p class="empty-hint">Lade…</p>';
|
||
try {
|
||
const params = selected.map(id => `gruppenIds=${encodeURIComponent(id)}`).join('&');
|
||
const res = await fetch(`/vanilla/toy/required?${params}`);
|
||
if (!res.ok) throw new Error();
|
||
const toys = await res.json();
|
||
toysNeedReload = false;
|
||
const badge = document.getElementById('toys-badge');
|
||
if (toys.length) { badge.textContent = toys.length; badge.style.display = ''; }
|
||
else { badge.style.display = 'none'; badge.textContent = ''; }
|
||
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('');
|
||
} catch (_) {
|
||
toyList.innerHTML = '<p class="empty-hint">Fehler beim Laden der Toys.</p>';
|
||
}
|
||
}
|
||
|
||
function validateContent(content, 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`);
|
||
}
|
||
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 || [] });
|
||
pruefeChastityConstraint(id, inv.inviteeId);
|
||
updatePlayerHeader(id, inv.inviteeName);
|
||
} 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(`/vanilla/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;
|
||
if (!setupId) {
|
||
if (!sessionStorage.getItem('vanilla-setup-id')) sessionStorage.setItem('vanilla-setup-id', crypto.randomUUID());
|
||
setupId = sessionStorage.getItem('vanilla-setup-id');
|
||
}
|
||
await ensureDraftExists();
|
||
try {
|
||
const res = await fetch('/vanilla/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 spieldauerIdx = parseInt(document.getElementById('sldSpieldauer')?.value || '2');
|
||
const sd = SPIELDAUER[spieldauerIdx] || SPIELDAUER[2];
|
||
return { aufgabenProLevel: sd.aufgaben };
|
||
}
|
||
|
||
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: [] };
|
||
}
|
||
const name = document.getElementById(`p${id}-name`)?.value?.trim() || '';
|
||
const werkzeuge = getChecked(`p${id}-werkzeuge`);
|
||
return { name, geschlecht: null, spieltMit: [], rollen: [], werkzeuge,
|
||
userId: id === selfPlayerId ? myUserId : null,
|
||
eigenesGeraet: false };
|
||
});
|
||
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('/vanilla/setup', {
|
||
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(`/vanilla/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');
|
||
} else if (e.status === 'ACCEPTED_HOST' && !inv.defaults) {
|
||
try {
|
||
const profileRes = await fetch(`/login/${e.inviteeId}`);
|
||
if (profileRes.ok) {
|
||
const profile = await profileRes.json();
|
||
const geschlecht = profile.geschlecht || null;
|
||
inv.defaults = { geschlecht, werkzeuge: geschlecht ? (WERKZEUGE_DEFAULTS[geschlecht] || []) : [] };
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
renderPending(id);
|
||
}
|
||
} catch (_) {}
|
||
updateActionBtn();
|
||
}
|
||
|
||
function updateActionBtn() {
|
||
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
|
||
document.getElementById('btnAction').disabled = hasPending;
|
||
}
|
||
|
||
function restorePlayer(id, data) {
|
||
const werkzeuge = (data.werkzeuge && data.werkzeuge.length > 0)
|
||
? data.werkzeuge
|
||
: (data.geschlecht ? (WERKZEUGE_DEFAULTS[data.geschlecht] || []) : []);
|
||
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'); } });
|
||
}
|
||
|
||
async function ladeEinladungenAusDb(userIdToInfo) {
|
||
try {
|
||
const res = await fetch(`/vanilla/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 };
|
||
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;
|
||
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 };
|
||
}
|
||
const name = document.getElementById(`p${id}-name`)?.value.trim() || '';
|
||
const werkzeuge = getChecked(`p${id}-werkzeuge`);
|
||
setFieldError(`p${id}-name-err`, !name);
|
||
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
|
||
if (!name || !werkzeuge.length) valid = false;
|
||
return { name, geschlecht: null, spieltMit: [], rollen: [], werkzeuge,
|
||
userId: inv ? inv.inviteeId : (id === selfPlayerId ? myUserId : null),
|
||
eigenesGeraet: false };
|
||
});
|
||
if (!valid) { return null; }
|
||
return mitspieler;
|
||
}
|
||
|
||
// ── Spiel starten ──
|
||
async function spielStarten() {
|
||
hideMessage();
|
||
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) {
|
||
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-grundeinstellungen-btn').classList.add('is-open');
|
||
document.getElementById('acc-grundeinstellungen-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(`/vanilla/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 || [];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Toy-Filter
|
||
const checkedToyIds = new Set([...document.querySelectorAll('#toyList input[type="checkbox"]:checked')].map(cb => cb.value));
|
||
const toyMap = new Map();
|
||
[...content.aufgaben, ...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('vanilla-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),
|
||
finisher: content.finisher.filter(toyOk),
|
||
};
|
||
|
||
const spieldauerIdx = parseInt(document.getElementById('sldSpieldauer').value);
|
||
const sd = SPIELDAUER[spieldauerIdx];
|
||
const settings = {
|
||
aufgabenProLevel: sd.aufgaben,
|
||
};
|
||
|
||
const { errors, warnings } = validateContent(gameContent, 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('vanilla-session-gruppen', JSON.stringify(selected));
|
||
sessionStorage.setItem('vanilla-session-game', JSON.stringify(gameContent));
|
||
|
||
const sessionSetup = JSON.stringify({ settings, mitspieler });
|
||
sessionStorage.setItem('vanilla-session-settings', JSON.stringify(settings));
|
||
sessionStorage.setItem('vanilla-session-setup', sessionSetup);
|
||
|
||
try {
|
||
const sessionRes = await fetch('/vanilla', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
aufgabenProLevel: settings.aufgabenProLevel,
|
||
setupId: setupId,
|
||
}),
|
||
});
|
||
if (sessionRes.status === 409) throw new Error('Du hast bereits ein laufendes Vanilla-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(`/vanilla/${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 }),
|
||
});
|
||
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
|
||
}
|
||
|
||
const aufgabenRes = await fetch(`/vanilla/${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('vanilla-session-id', sessionId);
|
||
fetch('/vanilla/setup', { method: 'DELETE' }).catch(() => {});
|
||
window.location.href = '/games/vanilla/vanillaingame.html';
|
||
} catch (e) { showMessage(e.message, 'error'); btn.disabled = false; }
|
||
}
|
||
|
||
// ── Gast: Bereit ──
|
||
async function bereitMachen() {
|
||
const id = guestOwnPlayerId; if (!id) return;
|
||
const werkzeuge = getChecked(`p${id}-werkzeuge`);
|
||
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
|
||
if (!werkzeuge.length) {
|
||
showMessage('Bitte mindestens ein Werkzeug auswählen.', 'error'); return;
|
||
}
|
||
try {
|
||
const res = await fetch(`/vanilla/einladung/${guestEinladungId}/spielerdaten`, {
|
||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ spielerDatenJson: JSON.stringify({
|
||
geschlecht: null, spieltMit: [], rollen: [], werkzeuge,
|
||
})}),
|
||
});
|
||
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(`/vanilla/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(`/vanilla/${data.sessionId}/mitspieler/me`);
|
||
if (mRes.status === 200) {
|
||
const mData = await mRes.json();
|
||
sessionStorage.setItem('vanilla-guest-mitspieler-id', mData.mitspielerId);
|
||
sessionStorage.setItem('vanilla-guest-name', mData.name);
|
||
sessionStorage.setItem('vanilla-session-id', data.sessionId);
|
||
sessionStorage.setItem('vanilla-is-guest', 'true');
|
||
stopGuestPoll(); window.location.replace('/games/vanilla/vanillaingame.html');
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
function stopGuestPoll() { if (guestPollInterval) { clearInterval(guestPollInterval); guestPollInterval = null; } }
|
||
async function abbrechenGast() {
|
||
stopGuestPoll();
|
||
await fetch(`/vanilla/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) {
|
||
VANILLA_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
sessionStorage.setItem('vanilla-session-id', sid); window.location.href = '/games/vanilla/vanillaingame.html';
|
||
}
|
||
|
||
function sessionBeendenFragen(sid) {
|
||
zeigeSessionModal('Session wirklich beenden?', 'Die Session wird 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('/vanilla', { method: 'DELETE', headers: {'Content-Type':'application/json'}, body: JSON.stringify({sessionId:sid}) }); } catch(_) {}
|
||
VANILLA_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||
fetch('/vanilla/setup', { method: 'DELETE' }).catch(() => {});
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
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(`/vanilla?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('/vanilla/einladung/meine-aktive');
|
||
if (einladungRes.status === 200) {
|
||
const einladung = await einladungRes.json();
|
||
if (einladung.sessionId) {
|
||
try { const mRes = await fetch(`/vanilla/${einladung.sessionId}/mitspieler/me`); if (mRes.ok) { const mData = await mRes.json(); sessionStorage.setItem('vanilla-guest-mitspieler-id', mData.mitspielerId); sessionStorage.setItem('vanilla-guest-name', mData.name); } } catch (_) {}
|
||
sessionStorage.setItem('vanilla-session-id', einladung.sessionId);
|
||
sessionStorage.setItem('vanilla-is-guest', 'true');
|
||
window.location.replace('/games/vanilla/vanillaingame.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('/vanilla/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }).catch(() => ({ content: [] })),
|
||
fetch('/vanilla/abo/list?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }).catch(() => ({ content: [] })),
|
||
fetch('/vanilla/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('vanilla-setup-id')) sessionStorage.setItem('vanilla-setup-id', crypto.randomUUID());
|
||
setupId = sessionStorage.getItem('vanilla-setup-id');
|
||
document.getElementById('setupView').style.display = 'block';
|
||
|
||
let restoredFromSetup = false;
|
||
const savedSetup = sessionStorage.getItem('vanilla-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) => {
|
||
if (playerIds.length >= MAX_PLAYERS) return;
|
||
const id = addPlayer(p.name, i === 0, i === 0, false, false);
|
||
if (id) { restorePlayer(id, p); if (p.userId) userIdToInfo[p.userId] = { playerId: id, name: p.name }; }
|
||
});
|
||
Object.entries(userIdToInfo).forEach(([userId, info]) => { pruefeChastityConstraint(info.playerId, userId); });
|
||
await ladeEinladungenAusDb(userIdToInfo);
|
||
restoredFromSetup = true;
|
||
} else {
|
||
sessionStorage.removeItem('vanilla-session-setup');
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
if (!restoredFromSetup) {
|
||
const savedSettings = sessionStorage.getItem('vanilla-session-settings');
|
||
if (savedSettings) {
|
||
applySettings(JSON.parse(savedSettings));
|
||
} else {
|
||
let draft = null;
|
||
try {
|
||
const res = await fetch('/vanilla/setup');
|
||
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('vanilla-setup-id', draft.setupId); setupId = draft.setupId; }
|
||
if (draft.settingsJson) { sessionStorage.setItem('vanilla-session-settings', draft.settingsJson); applySettings(JSON.parse(draft.settingsJson)); }
|
||
if (draft.gruppenJson) { sessionStorage.setItem('vanilla-session-gruppen', draft.gruppenJson); savedGruppen = new Set(JSON.parse(draft.gruppenJson)); }
|
||
} else {
|
||
if (draft.setupId) {
|
||
try {
|
||
const invRes = await fetch(`/vanilla/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(`/vanilla/einladung/${e.einladungId}`, { method: 'DELETE' }).catch(() => {}))
|
||
);
|
||
}
|
||
} catch (_) {}
|
||
await fetch('/vanilla/setup', { method: 'DELETE' }).catch(() => {});
|
||
}
|
||
const newId = crypto.randomUUID();
|
||
sessionStorage.setItem('vanilla-setup-id', newId); setupId = newId;
|
||
sessionStorage.removeItem('vanilla-session-settings');
|
||
sessionStorage.removeItem('vanilla-session-gruppen');
|
||
}
|
||
} else if (draft) {
|
||
if (draft.setupId) { sessionStorage.setItem('vanilla-setup-id', draft.setupId); setupId = draft.setupId; }
|
||
if (draft.settingsJson) { sessionStorage.setItem('vanilla-session-settings', draft.settingsJson); applySettings(JSON.parse(draft.settingsJson)); }
|
||
if (draft.gruppenJson) { sessionStorage.setItem('vanilla-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] || []) : []);
|
||
const selfId = addPlayer(user ? user.name : '', true, true, !!selfGeschlecht, false);
|
||
if (selfId) {
|
||
if (user?.profilePicture) { playerProfilePics[selfId] = user.profilePicture; updatePlayerHeader(selfId, user.name); }
|
||
if (playerIds.length < MAX_PLAYERS) 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('vanilla-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();
|
||
|
||
if (einladung.setupId) {
|
||
try {
|
||
const draft = await fetch(`/vanilla/setup?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) => {
|
||
if (playerIds.length >= MAX_PLAYERS) return;
|
||
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 (id) { 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);
|
||
|
||
document.getElementById('acc-grundeinstellungen-btn').classList.add('is-open');
|
||
document.getElementById('acc-grundeinstellungen-body').classList.add('is-open');
|
||
openPlayerAcc(guestOwnPlayerId);
|
||
|
||
document.getElementById('setupView').style.display = 'block';
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|