Weiter am Ingame Chastity Game gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
@@ -416,7 +416,7 @@
|
||||
|
||||
<label class="label-with-hint">
|
||||
<span>Beschreibung *</span>
|
||||
<button type="button" class="btn-hint" onclick="togglePlaceholderHint()" title="Platzhalter-Hilfe">i</button>
|
||||
<button type="button" id="iPlaceholderHintBtn" class="btn-hint" onclick="togglePlaceholderHint()" title="Platzhalter-Hilfe">i</button>
|
||||
</label>
|
||||
<div id="iPlaceholderHint" style="display:none;">
|
||||
<div class="placeholder-hint">
|
||||
@@ -451,15 +451,17 @@
|
||||
<div id="iLevelRow">
|
||||
<label for="iLevel">Level *</label>
|
||||
<input type="number" id="iLevel" min="1" max="5" placeholder="1–5">
|
||||
<label>Dauer (Sekunden)</label>
|
||||
<div class="modal-two-col">
|
||||
<div>
|
||||
<label for="iSekVon" style="margin-top:0;">Von</label>
|
||||
<input type="number" id="iSekVon" min="0" placeholder="z. B. 30">
|
||||
</div>
|
||||
<div>
|
||||
<label for="iSekBis" style="margin-top:0;">Bis</label>
|
||||
<input type="number" id="iSekBis" min="0" placeholder="z. B. 120">
|
||||
<div id="iSekRow">
|
||||
<label>Dauer (Sekunden)</label>
|
||||
<div class="modal-two-col">
|
||||
<div>
|
||||
<label for="iSekVon" style="margin-top:0;">Von</label>
|
||||
<input type="number" id="iSekVon" min="0" placeholder="z. B. 30">
|
||||
</div>
|
||||
<div>
|
||||
<label for="iSekBis" style="margin-top:0;">Bis</label>
|
||||
<input type="number" id="iSekBis" min="0" placeholder="z. B. 120">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -749,7 +751,7 @@
|
||||
<div class="gruppe-body" id="body-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type)}
|
||||
${g.availableIn !== 'CHASTITY_ONLY' ? renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type) : ''}
|
||||
${renderSubSection('Zeitstrafen',sortByName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type)}
|
||||
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
</div>
|
||||
@@ -1086,7 +1088,9 @@
|
||||
pubCb.checked = !g.privateGruppe;
|
||||
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
|
||||
document.getElementById('gPublicLabel').style.display = 'block';
|
||||
document.getElementById('gAvailableIn').value = g.availableIn || 'BDSM_ONLY';
|
||||
const avSel = document.getElementById('gAvailableIn');
|
||||
avSel.value = g.availableIn || 'BDSM_ONLY';
|
||||
avSel.disabled = true;
|
||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||
if (g.bild) {
|
||||
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
|
||||
@@ -1104,7 +1108,8 @@
|
||||
document.getElementById('gDesc').value = '';
|
||||
document.getElementById('gPublic').checked = false;
|
||||
document.getElementById('gPublicLabel').style.display = 'none';
|
||||
document.getElementById('gAvailableIn').value = 'BDSM_ONLY';
|
||||
document.getElementById('gAvailableIn').value = 'BDSM_ONLY';
|
||||
document.getElementById('gAvailableIn').disabled = false;
|
||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||
gruppeModal.classList.add('open');
|
||||
document.getElementById('gName').focus();
|
||||
@@ -1113,7 +1118,8 @@
|
||||
|
||||
function closeGruppeModal() {
|
||||
gruppeModal.classList.remove('open');
|
||||
document.getElementById('gPublic').disabled = false;
|
||||
document.getElementById('gPublic').disabled = false;
|
||||
document.getElementById('gAvailableIn').disabled = false;
|
||||
}
|
||||
|
||||
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
|
||||
@@ -1359,6 +1365,7 @@
|
||||
let currentItemGruppeId = null;
|
||||
let currentItemKind = null; // 'aufgabe' | 'strafe' | 'zeitstrafe'
|
||||
let currentItemEditId = null; // null = neu, sonst ID des zu bearbeitenden Items
|
||||
let _isChastityMode = false;
|
||||
|
||||
const ITEM_TITLES_NEW = { aufgabe: 'Aufgabe hinzufügen', strafe: 'Strafe hinzufügen', zeitstrafe: 'Zeitstrafe hinzufügen', finisher: 'Finisher hinzufügen' };
|
||||
const ITEM_TITLES_EDIT = { aufgabe: 'Aufgabe bearbeiten', strafe: 'Strafe bearbeiten', zeitstrafe: 'Zeitstrafe bearbeiten', finisher: 'Finisher bearbeiten' };
|
||||
@@ -1366,6 +1373,11 @@
|
||||
function _setupItemModal(kind) {
|
||||
const isZeit = kind === 'zeitstrafe';
|
||||
const isFinisher = kind === 'finisher';
|
||||
const isChastity = (_gruppeData[currentItemGruppeId]?.availableIn === 'CHASTITY_ONLY');
|
||||
_isChastityMode = isChastity;
|
||||
|
||||
// Placeholder hint button + text
|
||||
document.getElementById('iPlaceholderHintBtn').style.display = isChastity ? 'none' : '';
|
||||
document.querySelector('#iPlaceholderHint .placeholder-hint').innerHTML =
|
||||
isFinisher
|
||||
? 'In Texten können Platzhalter verwendet werden:<br>' +
|
||||
@@ -1374,15 +1386,38 @@
|
||||
: 'In Texten können Platzhalter verwendet werden:<br>' +
|
||||
'<code>{AKTIV}</code> – Name des aktiven Parts<br>' +
|
||||
'<code>{PASSIV}</code> – Name des passiven Parts';
|
||||
document.getElementById('iGeschlechtRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherAktivRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherPassivRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
|
||||
// Geschlecht: finisher only, and not in chastity
|
||||
document.getElementById('iGeschlechtRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Level row: aufgabe/strafe always; zeitstrafe only in chastity
|
||||
document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) || (isZeit && isChastity) ? 'block' : 'none';
|
||||
// Sekunden sub-section: not for chastity zeitstrafe
|
||||
document.getElementById('iSekRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
|
||||
// Aktiv Werkzeuge: aufgabe/strafe only
|
||||
document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.querySelector('#iWerkzeugAktivRow label').textContent = isChastity ? 'Benötigt' : 'Benötigt (aktiv)';
|
||||
['VAGINA', 'PENIS', 'UMSCHNALLDILDO'].forEach(v => {
|
||||
const lbl = document.querySelector(`#iWerkzeugAktiv input[value="${v}"]`)?.closest('label');
|
||||
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
||||
});
|
||||
|
||||
// Passiv Werkzeuge: aufgabe/strafe, not in chastity
|
||||
document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Finisher Werkzeuge: not in chastity
|
||||
document.getElementById('iWerkzeugFinisherAktivRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherPassivRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Zeitstrafe rows
|
||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||
['VAGINA', 'PENIS'].forEach(v => {
|
||||
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
|
||||
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
||||
});
|
||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function _resetItemFields() {
|
||||
@@ -1449,6 +1484,9 @@
|
||||
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
|
||||
document.getElementById('iReleaseText').value = d.releaseText || '';
|
||||
(d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; });
|
||||
if (_isChastityMode) {
|
||||
document.getElementById('iLevel').value = d.level != null ? d.level : '';
|
||||
}
|
||||
}
|
||||
|
||||
const preSelected = (d.benoetigteToys || []).filter(t => t.toyId);
|
||||
@@ -1480,7 +1518,7 @@
|
||||
}
|
||||
function buildItems(filter) {
|
||||
const f = filter || '';
|
||||
_items = STATIC.filter(s => !f || s.toLowerCase().includes(f)).map(s => ({ label: s, insert: s }));
|
||||
_items = (_isChastityMode ? [] : STATIC.filter(s => !f || s.toLowerCase().includes(f))).map(s => ({ label: s, insert: s }));
|
||||
const toys = _allToys.filter(t => !f || t.name.toLowerCase().includes(f));
|
||||
if (toys.length) {
|
||||
_items.push({ separator: true, label: 'Toys' });
|
||||
@@ -1606,13 +1644,14 @@
|
||||
method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
} else if (kind === 'finisher') {
|
||||
const geschlecht = document.querySelector('#iGeschlecht input:checked')?.value;
|
||||
if (!geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||
const geschlecht = _isChastityMode ? null : document.querySelector('#iGeschlecht input:checked')?.value;
|
||||
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||
payload = {
|
||||
kurzText, text, geschlecht,
|
||||
kurzText, text,
|
||||
geschlecht: geschlecht || null,
|
||||
gruppeId: isEdit ? undefined : currentItemGruppeId,
|
||||
benoetigtAktiv: checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
|
||||
@@ -1624,6 +1663,14 @@
|
||||
const sperreFuer = checkedValues('iSperreFuer');
|
||||
if (sperreFuer.length === 0) { showItemError('Bitte mindestens ein Werkzeug für die Sperre auswählen.'); return; }
|
||||
|
||||
let zeitLevel = null;
|
||||
if (_isChastityMode) {
|
||||
const lv = document.getElementById('iLevel').value.trim();
|
||||
if (!lv) { showItemError('Bitte ein Level angeben.'); return; }
|
||||
zeitLevel = parseInt(lv, 10);
|
||||
if (isNaN(zeitLevel) || zeitLevel < 1 || zeitLevel > 5) { showItemError('Level muss zwischen 1 und 5 liegen.'); return; }
|
||||
}
|
||||
|
||||
const minBis = document.getElementById('iMinBis').value.trim();
|
||||
payload = {
|
||||
kurzText, text,
|
||||
@@ -1632,6 +1679,7 @@
|
||||
minutenBis: minBis ? parseInt(minBis, 10) : null,
|
||||
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
||||
sperreFuer,
|
||||
level: zeitLevel,
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
|
||||
|
||||
@@ -1711,6 +1711,14 @@
|
||||
document.getElementById('btnDrawOk').style.display = 'none';
|
||||
document.getElementById('btnSpeedConfirm').style.display = '';
|
||||
}
|
||||
|
||||
if (dto.card === 'GAME_CARD') {
|
||||
const btn = document.getElementById('btnDrawOk');
|
||||
btn.textContent = '▶ Spiel starten';
|
||||
btn.onclick = function() {
|
||||
window.location.href = '/games/chastity/taskgame.html?lockId=' + lockId;
|
||||
};
|
||||
}
|
||||
}, 700);
|
||||
}, 1000);
|
||||
})
|
||||
|
||||
@@ -268,47 +268,6 @@
|
||||
.sim-stat-val { font-size:1rem; font-weight:700; color:var(--color-text); }
|
||||
.sim-stat-lbl { font-size:0.7rem; color:var(--color-muted); margin-top:0.15rem; text-transform:uppercase; letter-spacing:0.05em; }
|
||||
|
||||
/* ── Spiel-Sets (gruppe-card style) ── */
|
||||
.gs-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; cursor:pointer; transition:border-color 0.15s; }
|
||||
.gs-card:hover { border-color:var(--color-primary); }
|
||||
.gs-card-header { display:flex; align-items:center; gap:0.75rem; padding:0.85rem 1rem; user-select:none; }
|
||||
.gs-card-meta { flex:1; min-width:0; }
|
||||
.gs-card-name { font-size:0.95rem; font-weight:600; color:var(--color-text); }
|
||||
.gs-card-header-actions { display:flex; gap:0.4rem; flex-shrink:0; }
|
||||
|
||||
.gs-sub { margin-bottom:0.85rem; }
|
||||
.gs-sub:last-child { margin-bottom:0; }
|
||||
.gs-sub-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.35rem; }
|
||||
.gs-sub-title { font-size:0.7rem; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--color-primary); }
|
||||
.gs-sub-warn { color:#e74c3c !important; }
|
||||
|
||||
.gs-item-list { display:flex; flex-direction:column; gap:0.25rem; }
|
||||
.gs-list-item { border-radius:6px; background:var(--color-secondary); overflow:hidden; }
|
||||
.gs-list-item-row { display:flex; align-items:center; gap:0.5rem; padding:0.3rem 0.5rem 0.3rem 0.75rem; cursor:pointer; user-select:none; transition:background 0.12s; }
|
||||
.gs-list-item-row:hover { background:rgba(255,255,255,0.04); }
|
||||
.gs-list-item.open .gs-list-item-row { background:rgba(233,69,96,0.08); }
|
||||
.gs-list-item-text { color:var(--color-text); flex:1; min-width:0; font-size:0.83rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.gs-list-item-badges { display:flex; gap:0.3rem; flex-shrink:0; align-items:center; }
|
||||
.gs-badge { font-size:0.68rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(233,69,96,0.15); color:var(--color-primary); white-space:nowrap; }
|
||||
.gs-badge-neutral { background:rgba(255,255,255,0.07); color:var(--color-muted); }
|
||||
.gs-list-item-detail { display:none; padding:0.4rem 0.75rem 0.5rem; border-top:1px solid rgba(255,255,255,0.06); font-size:0.79rem; color:var(--color-muted); line-height:1.5; }
|
||||
.gs-list-item.open .gs-list-item-detail { display:block; }
|
||||
.gs-detail-text { color:var(--color-text); white-space:pre-wrap; margin-bottom:0.3rem; }
|
||||
.gs-detail-actions { display:flex; gap:0.35rem; margin-top:0.4rem; justify-content:flex-end; }
|
||||
.gs-btn-sub-add { background:none; border:1px solid var(--color-secondary); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.15rem 0.5rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||||
.gs-btn-sub-add:hover { border-color:var(--color-primary); color:var(--color-primary); }
|
||||
.gs-btn-item-edit { background:none; border:1px solid rgba(136,136,136,0.45); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||||
.gs-btn-item-edit:hover { border-color:var(--color-text); color:var(--color-text); }
|
||||
.gs-btn-item-delete { background:none; border:1px solid rgba(233,69,96,0.4); border-radius:5px; color:var(--color-primary); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:background 0.15s; width:auto; }
|
||||
.gs-btn-item-delete:hover { background:rgba(233,69,96,0.15); }
|
||||
.gs-sub-empty { font-size:0.78rem; color:var(--color-muted); padding:0.15rem 0; }
|
||||
|
||||
#gsSetModal, #gsItemModal { z-index:600; }
|
||||
|
||||
.gs-check-group { display:flex; flex-wrap:wrap; gap:0.4rem; }
|
||||
.gs-check-chip { display:inline-flex; align-items:center; gap:0.4rem; background:var(--color-secondary); border:1px solid rgba(255,255,255,0.1); border-radius:20px; padding:0.25rem 0.7rem; cursor:pointer; font-size:0.82rem; color:var(--color-text); transition:border-color 0.15s; user-select:none; }
|
||||
.gs-check-chip:has(input:checked) { border-color:var(--color-primary); }
|
||||
.gs-check-chip input { accent-color:var(--color-primary); width:auto; cursor:pointer; margin:0; flex-shrink:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -329,17 +288,6 @@
|
||||
<div class="template-list" id="taskSetList"></div>
|
||||
<p id="taskSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Aufgaben-Sets vorhanden.</p>
|
||||
|
||||
<!-- Spiel-Sets (für Spiel-Karte) -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin:2rem 0 1rem;gap:1rem;flex-wrap:wrap;">
|
||||
<div>
|
||||
<h2 style="margin:0;">Spiel-Sets</h2>
|
||||
<p style="margin:0.25rem 0 0;font-size:0.8rem;color:var(--color-muted);">Aufgaben-Sets für die Spiel-Karte im Karten-Lock · max. 5 Sets</p>
|
||||
</div>
|
||||
<button id="btnNewGameSet" onclick="openGsSetModal(null)" style="width:auto;padding:0.55rem 1.2rem;">+ Set anlegen</button>
|
||||
</div>
|
||||
<div class="template-list" id="gameSetList"></div>
|
||||
<p id="gameSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Spiel-Sets vorhanden.</p>
|
||||
|
||||
<h2 style="margin:2rem 0 1rem;">Abonnierte Vorlagen</h2>
|
||||
<div class="template-list" id="subscribedList"></div>
|
||||
<p id="subscribedEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Keine abonnierten Vorlagen vorhanden.</p>
|
||||
@@ -443,24 +391,6 @@
|
||||
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spiel-Karte (CardLock) -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Spiel-Karte (optional)</div>
|
||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||
<label>Spiel-Set</label>
|
||||
<select id="fGameSetId" onchange="onGameSetChange()">
|
||||
<option value="">Kein Spiel-Set</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-add" type="button" onclick="openGsSetModal(null,'template')">+ Neues Set anlegen</button>
|
||||
<div id="gameSetSpieldauerRow" style="display:none;margin-top:0.75rem;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.3rem;">
|
||||
<label for="sldGameSpieldauer" style="font-size:0.88rem;">Spieldauer</label>
|
||||
<span class="setting-value" id="valGameSpieldauer">Mittel</span>
|
||||
</div>
|
||||
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2" oninput="updateGameSpieldauer(this.value)" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Aufgaben (CardLock) -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Aufgaben (optional)</div>
|
||||
@@ -719,114 +649,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Set erstellen / umbenennen -->
|
||||
<div class="modal-backdrop" id="gsSetModal">
|
||||
<div class="modal-box" style="max-width:420px;" onclick="event.stopPropagation()">
|
||||
<h2 id="gsSetModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Neues Spiel-Set</h2>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Name *</label>
|
||||
<input type="text" id="gsSetName" maxlength="100" placeholder="Set-Name"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;transition:border-color 0.2s;"
|
||||
onkeydown="if(event.key==='Enter')saveGsSet()">
|
||||
<div id="gsSetError" style="color:#e74c3c;font-size:0.82rem;margin-top:0.75rem;display:none;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:1.5rem;">
|
||||
<button onclick="closeGsSetModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||||
<button onclick="saveGsSet()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Item hinzufügen / bearbeiten -->
|
||||
<div class="modal-backdrop" id="gsItemModal">
|
||||
<div class="modal-box" style="max-width:460px;" onclick="event.stopPropagation()">
|
||||
<h2 id="gsItemModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Aufgabe</h2>
|
||||
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Titel *</label>
|
||||
<input type="text" id="gsItemTitle" maxlength="150" placeholder="Titel …"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;margin-bottom:0.85rem;transition:border-color 0.2s;">
|
||||
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Beschreibung (optional)</label>
|
||||
<textarea id="gsItemDesc" rows="3" maxlength="600" placeholder="Beschreibung …"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;margin-bottom:0.85rem;"></textarea>
|
||||
|
||||
<!-- Aufgabe: Level + Dauer -->
|
||||
<div id="gsItemAufgabeRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||||
<select id="gsItemAufgabeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||||
</select>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Dauer (Minuten, optional)</label>
|
||||
<input type="number" id="gsItemMinutes" min="1" max="9999" placeholder="–"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
</div>
|
||||
|
||||
<!-- Aufgabe: benötigt -->
|
||||
<div id="gsItemBenoetigtRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Benötigt</div>
|
||||
<div class="gs-check-group">
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_MUND">Mund</label>
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_ANUS">Anus</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: Level + Strafminuten -->
|
||||
<div id="gsItemZeitstrafeRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||||
<select id="gsItemZeitstrafeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||||
</select>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Strafminuten (Von – Bis)</label>
|
||||
<div style="display:flex;gap:0.6rem;align-items:center;">
|
||||
<input type="number" id="gsItemMinMin" min="1" max="9999" placeholder="Min."
|
||||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
<span style="color:var(--color-muted);">–</span>
|
||||
<input type="number" id="gsItemMaxMin" min="1" max="9999" placeholder="Max."
|
||||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
</div>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin:0.65rem 0 0.3rem;">Text bei Aufhebung (optional)</label>
|
||||
<textarea id="gsItemReleaseText" rows="2" maxlength="2000"
|
||||
placeholder="Text der angezeigt wird, wenn die Sperre endet…"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;transition:border-color 0.2s;line-height:1.45;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: sperrt -->
|
||||
<div id="gsItemSperrtRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Sperrt</div>
|
||||
<div class="gs-check-group">
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_MUND"> Mund</label>
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_ANUS"> Anus</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe + Finisher: Temp. Öffnung -->
|
||||
<div id="gsItemUnlockRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Temp. Öffnung</div>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;margin-bottom:0.35rem;cursor:pointer;">
|
||||
<input type="checkbox" id="gsItemBefore" style="accent-color:var(--color-primary);width:15px;height:15px;"> Vor der Maßnahme erforderlich
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||||
<input type="checkbox" id="gsItemAfter" style="accent-color:var(--color-primary);width:15px;height:15px;"> Nach der Maßnahme erforderlich
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="gsItemError" style="color:#e74c3c;font-size:0.82rem;display:none;margin-bottom:0.5rem;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:0.5rem;">
|
||||
<button onclick="closeGsItemModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||||
<button onclick="saveGsItem()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Inhalt-Popup -->
|
||||
<div class="modal-backdrop" id="gsEditModal">
|
||||
<div class="modal-box" style="max-width:600px;width:calc(100% - 2rem);" onclick="event.stopPropagation()">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<h2 id="gsEditModalTitle" style="margin:0;color:var(--color-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></h2>
|
||||
<button onclick="closeGsEditModal()" style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;line-height:1;cursor:pointer;padding:0;width:auto;flex-shrink:0;">✕</button>
|
||||
</div>
|
||||
<div id="gsEditModalContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/card-display.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
@@ -1223,51 +1045,6 @@
|
||||
if (res.ok || res.status === 204) await loadTaskSets();
|
||||
}
|
||||
|
||||
const GS_TOOLS = [
|
||||
{ value: 'UMSCHNALLDILDO', label: 'Strap-on' },
|
||||
{ value: 'MUND', label: 'Oral' },
|
||||
{ value: 'ANUS', label: 'Anal' },
|
||||
];
|
||||
function gsGetChecked(prefix) {
|
||||
return GS_TOOLS.filter(t => document.getElementById(prefix + t.value)?.checked).map(t => t.value);
|
||||
}
|
||||
function gsSetChecked(prefix, values) {
|
||||
GS_TOOLS.forEach(t => {
|
||||
const el = document.getElementById(prefix + t.value);
|
||||
if (el) el.checked = (values || []).includes(t.value);
|
||||
});
|
||||
}
|
||||
|
||||
const GAME_SPIELDAUER = [
|
||||
{ label: 'Sehr kurz' },
|
||||
{ label: 'Kurz' },
|
||||
{ label: 'Mittel' },
|
||||
{ label: 'Lang' },
|
||||
{ label: 'Sehr lang' },
|
||||
];
|
||||
|
||||
function populateGameSetSelect() {
|
||||
const sel = document.getElementById('fGameSetId');
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">Kein Spiel-Set</option>';
|
||||
_gameSets.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id; opt.textContent = s.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.value = cur;
|
||||
}
|
||||
|
||||
function onGameSetChange() {
|
||||
const val = document.getElementById('fGameSetId')?.value;
|
||||
document.getElementById('gameSetSpieldauerRow').style.display = val ? '' : 'none';
|
||||
}
|
||||
|
||||
function updateGameSpieldauer(val) {
|
||||
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[val]?.label || '';
|
||||
}
|
||||
|
||||
function populateTaskSetSelects() {
|
||||
for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) {
|
||||
const sel = document.getElementById(selId);
|
||||
@@ -1476,14 +1253,6 @@
|
||||
onTaskSetChange('card');
|
||||
onTaskSetChange('timelock');
|
||||
|
||||
// Spiel-Set
|
||||
populateGameSetSelect();
|
||||
document.getElementById('fGameSetId').value = template?.gameSetId || '';
|
||||
onGameSetChange();
|
||||
const sdIdx = template?.gameSpieldauerIdx ?? 2;
|
||||
document.getElementById('sldGameSpieldauer').value = sdIdx;
|
||||
updateGameSpieldauer(sdIdx);
|
||||
|
||||
alignModalToContent();
|
||||
document.getElementById('modalBackdrop').classList.add('open');
|
||||
document.getElementById('modalDiscardConfirm').style.display = 'none';
|
||||
@@ -1527,13 +1296,7 @@
|
||||
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (document.getElementById('gsItemModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsItemModal();
|
||||
} else if (document.getElementById('gsSetModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsSetModal();
|
||||
} else if (document.getElementById('gsEditModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsEditModal();
|
||||
} else if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
|
||||
if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault(); tryCloseTaskSetModal();
|
||||
} else if (document.getElementById('modalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault(); tryCloseModal();
|
||||
@@ -1588,9 +1351,6 @@
|
||||
if (totalMax===0) { showModalError('Das Deck muss mindestens eine Karte enthalten.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
const hasTaskCards = (parseInt(document.getElementById('min_TASK').value)||0)>0 || (parseInt(document.getElementById('max_TASK').value)||0)>0;
|
||||
if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Aufgaben-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
const hasGameCards = (parseInt(document.getElementById('min_GAME_CARD').value)||0)>0 || (parseInt(document.getElementById('max_GAME_CARD').value)||0)>0;
|
||||
if (hasGameCards && !document.getElementById('fGameSetId').value) { showModalError('Spiel-Karten konfiguriert, aber kein Spiel-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
|
||||
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
|
||||
|
||||
const cardCountsMin={}, cardCountsMax={};
|
||||
@@ -1609,8 +1369,6 @@
|
||||
taskSetId: document.getElementById('fCardTaskSetId').value || null,
|
||||
requiresVerification: document.getElementById('fRequiresVerification').checked,
|
||||
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
|
||||
gameSetId: document.getElementById('fGameSetId').value || null,
|
||||
gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2,
|
||||
};
|
||||
} else {
|
||||
// TimeLock
|
||||
@@ -1895,426 +1653,8 @@
|
||||
observer.observe(document.getElementById('scrollSentinel'));
|
||||
|
||||
resetList();
|
||||
loadGameSets();
|
||||
|
||||
// ════════════════════════════════════════════════
|
||||
// Spiel-Sets
|
||||
// ════════════════════════════════════════════════
|
||||
|
||||
let _gameSets = [];
|
||||
let _gsEditSetId = null; // set being renamed
|
||||
let _gsSetCaller = null; // 'template' when opened from the template modal
|
||||
let _gsOpenSetId = null; // set currently open in the content popup
|
||||
let _gsItemType = null; // 'aufgabe' | 'zeitstrafe' | 'finisher'
|
||||
let _gsItemSetId = null;
|
||||
let _gsItemIdx = null; // null = new, number = editing
|
||||
|
||||
async function loadGameSets() {
|
||||
try {
|
||||
const res = await fetch('/chastity/game-sets');
|
||||
if (!res.ok) return;
|
||||
_gameSets = await res.json();
|
||||
renderGameSetList();
|
||||
populateGameSetSelect();
|
||||
if (_gsOpenSetId) renderGsEditModalContent(_gsOpenSetId);
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderGameSetList() {
|
||||
const list = document.getElementById('gameSetList');
|
||||
list.innerHTML = '';
|
||||
document.getElementById('gameSetEmpty').style.display = _gameSets.length ? 'none' : '';
|
||||
document.getElementById('btnNewGameSet').disabled = _gameSets.length >= 5;
|
||||
|
||||
_gameSets.forEach(s => {
|
||||
const aufgaben = s.aufgaben || [];
|
||||
const zeitstrafen = s.zeitstrafen || [];
|
||||
const finisher = s.finisher || [];
|
||||
const levelCounts = [1,2,3,4,5].map(l => aufgaben.filter(a => a.level === l).length);
|
||||
|
||||
const lvlBadges = levelCounts.map((c, i) => {
|
||||
const cls = c >= 3 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
|
||||
return `<span class="${cls}">L${i+1}: ${c}</span>`;
|
||||
}).join('');
|
||||
const finBadgeCls = finisher.length >= 1 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'gs-card';
|
||||
card.id = 'gscard_' + s.id;
|
||||
card.addEventListener('click', () => openGsEditModal(s.id));
|
||||
card.innerHTML = `
|
||||
<div class="gs-card-header">
|
||||
<div class="gs-card-meta">
|
||||
<div class="gs-card-name">${esc(s.name)}</div>
|
||||
<div style="display:flex;gap:0.3rem;flex-wrap:wrap;margin-top:0.3rem;">
|
||||
${lvlBadges}
|
||||
<span class="gs-badge gs-badge-neutral">Zeitstrafen: ${zeitstrafen.length}</span>
|
||||
<span class="${finBadgeCls}">Finisher: ${finisher.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gs-card-header-actions" onclick="event.stopPropagation()">
|
||||
<button class="gs-btn-item-edit" onclick="openGsSetModal('${s.id}')">✎</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGameSet('${s.id}',${JSON.stringify(s.name)})">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGsListItem(id) {
|
||||
document.getElementById(id)?.classList.toggle('open');
|
||||
}
|
||||
|
||||
// ── Set content popup ──────────────────────────
|
||||
|
||||
function openGsEditModal(setId) {
|
||||
_gsOpenSetId = setId;
|
||||
renderGsEditModalContent(setId);
|
||||
document.getElementById('gsEditModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeGsEditModal() {
|
||||
document.getElementById('gsEditModal').classList.remove('open');
|
||||
_gsOpenSetId = null;
|
||||
}
|
||||
|
||||
function renderGsEditModalContent(setId) {
|
||||
const container = document.getElementById('gsEditModalContent');
|
||||
if (!container) return;
|
||||
const s = _gameSets.find(x => x.id === setId);
|
||||
if (!s) { closeGsEditModal(); return; }
|
||||
document.getElementById('gsEditModalTitle').textContent = s.name;
|
||||
const aufgaben = s.aufgaben || [];
|
||||
const zeitstrafen = s.zeitstrafen || [];
|
||||
const finisher = s.finisher || [];
|
||||
let html = '';
|
||||
for (let l = 1; l <= 5; l++) {
|
||||
const items = aufgaben.map((a, i) => ({...a, _gi: i})).filter(a => a.level === l);
|
||||
const warnCls = items.length < 3 ? ' gs-sub-warn' : '';
|
||||
const itemsHtml = items.map(a => gsAufgabeRowHtml(s.id, a._gi, a)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title${warnCls}">Level ${l} <span style="font-weight:400;">(${items.length}/3+)</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('aufgabe','${s.id}',null,${l})">+ Aufgabe</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${itemsHtml}</div></div>`;
|
||||
}
|
||||
const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title">Zeitstrafen <span style="font-weight:400;">(${zeitstrafen.length})</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('zeitstrafe','${s.id}',null,null)">+ Zeitstrafe</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${zeitHtml}</div></div>`;
|
||||
const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : '';
|
||||
const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title${finWarnCls}">Finisher <span style="font-weight:400;">(${finisher.length}/1+)</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('finisher','${s.id}',null,null)">+ Finisher</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${finHtml}</div></div>`;
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Row HTML helpers ───────────────────────────
|
||||
|
||||
function gsAufgabeRowHtml(setId, gi, a) {
|
||||
const toolLabels = (a.benoetigt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
|
||||
const badges = [
|
||||
a.minutes ? `<span class="gs-badge gs-badge-neutral">${a.minutes} Min.</span>` : '',
|
||||
...toolLabels.map(l => `<span class="gs-badge gs-badge-neutral">${l}</span>`),
|
||||
].join('');
|
||||
const desc = a.description ? `<div class="gs-detail-text">${esc(a.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_a_${gi}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_a_${gi}')">
|
||||
<span class="gs-list-item-text">${esc(a.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('aufgabe','${setId}',${gi},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('aufgabe','${setId}',${gi})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('aufgabe','${setId}',${gi})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function gsZeitstrafeRowHtml(setId, idx, z) {
|
||||
const timeStr = (z.minMinutes != null ? z.minMinutes : '?') + '–' + (z.maxMinutes != null ? z.maxMinutes : '?') + ' Min.';
|
||||
const sperrtLabels = (z.sperrt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
|
||||
const badges = [
|
||||
z.level ? `<span class="gs-badge">L${z.level}</span>` : '',
|
||||
`<span class="gs-badge gs-badge-neutral">${timeStr}</span>`,
|
||||
...sperrtLabels.map(l => `<span class="gs-badge gs-badge-neutral">🔒 ${l}</span>`),
|
||||
z.releaseText ? `<span class="gs-badge gs-badge-neutral">📝 Aufhebung</span>` : '',
|
||||
z.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||||
z.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||||
].join('');
|
||||
const releaseRow = z.releaseText ? `<div style="font-size:0.75rem;color:var(--color-muted);margin-bottom:0.15rem;">Bei Aufhebung:</div><div class="gs-detail-text">${esc(z.releaseText)}</div>` : '';
|
||||
const desc = z.description ? `<div class="gs-detail-text">${esc(z.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_z_${idx}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_z_${idx}')">
|
||||
<span class="gs-list-item-text">${esc(z.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}${releaseRow}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('zeitstrafe','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('zeitstrafe','${setId}',${idx})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('zeitstrafe','${setId}',${idx})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function gsFinisherRowHtml(setId, idx, f) {
|
||||
const badges = [
|
||||
f.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||||
f.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||||
].join('');
|
||||
const desc = f.description ? `<div class="gs-detail-text">${esc(f.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_f_${idx}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_f_${idx}')">
|
||||
<span class="gs-list-item-text">${esc(f.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('finisher','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('finisher','${setId}',${idx})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('finisher','${setId}',${idx})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// ── Set create / rename modal ──────────────────
|
||||
|
||||
function openGsSetModal(id, caller) {
|
||||
_gsEditSetId = id || null;
|
||||
_gsSetCaller = caller || null;
|
||||
document.getElementById('gsSetModalTitle').textContent = id ? 'Spiel-Set umbenennen' : 'Neues Spiel-Set';
|
||||
document.getElementById('gsSetName').value = id ? (_gameSets.find(s => s.id === id)?.name || '') : '';
|
||||
document.getElementById('gsSetError').style.display = 'none';
|
||||
document.getElementById('gsSetModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('gsSetName').focus(), 50);
|
||||
}
|
||||
|
||||
function closeGsSetModal() {
|
||||
document.getElementById('gsSetModal').classList.remove('open');
|
||||
_gsEditSetId = _gsSetCaller = null;
|
||||
}
|
||||
|
||||
async function saveGsSet() {
|
||||
const name = document.getElementById('gsSetName').value.trim();
|
||||
const errEl = document.getElementById('gsSetError');
|
||||
if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
|
||||
errEl.style.display = 'none';
|
||||
const set = _gsEditSetId ? _gameSets.find(s => s.id === _gsEditSetId) : null;
|
||||
const url = _gsEditSetId ? `/chastity/game-sets/${_gsEditSetId}` : '/chastity/game-sets';
|
||||
const method = _gsEditSetId ? 'PUT' : 'POST';
|
||||
const body = { name,
|
||||
aufgaben: set?.aufgaben || [],
|
||||
zeitstrafen: set?.zeitstrafen || [],
|
||||
finisher: set?.finisher || [] };
|
||||
try {
|
||||
const res = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
||||
if (res.ok) {
|
||||
const saved = await res.json().catch(() => null);
|
||||
const caller = _gsSetCaller;
|
||||
closeGsSetModal();
|
||||
await loadGameSets();
|
||||
if (caller === 'template' && saved?.id) {
|
||||
document.getElementById('fGameSetId').value = saved.id;
|
||||
onGameSetChange();
|
||||
markDirty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const b = await res.json().catch(() => ({}));
|
||||
errEl.textContent = b.error || 'Fehler.'; errEl.style.display = '';
|
||||
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
|
||||
}
|
||||
|
||||
async function deleteGameSet(id, name) {
|
||||
if (!confirm(`Spiel-Set „${name}" wirklich löschen?`)) return;
|
||||
const res = await fetch(`/chastity/game-sets/${id}`, { method: 'DELETE' });
|
||||
if (res.ok || res.status === 204) loadGameSets();
|
||||
}
|
||||
|
||||
// ── Item modal ─────────────────────────────────
|
||||
|
||||
function openGsItemModal(type, setId, itemIdx, contextLevel) {
|
||||
_gsItemType = type;
|
||||
_gsItemSetId = setId;
|
||||
_gsItemIdx = itemIdx !== null && itemIdx !== undefined ? itemIdx : null;
|
||||
|
||||
const titles = { aufgabe: 'Aufgabe', zeitstrafe: 'Zeitstrafe', finisher: 'Finisher' };
|
||||
document.getElementById('gsItemModalTitle').textContent =
|
||||
(_gsItemIdx !== null ? 'Bearbeiten: ' : 'Neu: ') + titles[type];
|
||||
|
||||
// Reset fields
|
||||
document.getElementById('gsItemTitle').value = '';
|
||||
document.getElementById('gsItemDesc').value = '';
|
||||
document.getElementById('gsItemMinutes').value = '';
|
||||
document.getElementById('gsItemMinMin').value = '';
|
||||
document.getElementById('gsItemMaxMin').value = '';
|
||||
document.getElementById('gsItemReleaseText').value = '';
|
||||
document.getElementById('gsItemBefore').checked = false;
|
||||
document.getElementById('gsItemAfter').checked = false;
|
||||
document.getElementById('gsItemAufgabeLevel').value = contextLevel || 1;
|
||||
document.getElementById('gsItemZeitstrafeLevel').value = 1;
|
||||
document.getElementById('gsItemError').style.display = 'none';
|
||||
gsSetChecked('gsItemBen_', []);
|
||||
gsSetChecked('gsItemSperr_', []);
|
||||
|
||||
// Show/hide type-specific rows
|
||||
document.getElementById('gsItemAufgabeRow').style.display = type === 'aufgabe' ? '' : 'none';
|
||||
document.getElementById('gsItemBenoetigtRow').style.display = type === 'aufgabe' ? '' : 'none';
|
||||
document.getElementById('gsItemZeitstrafeRow').style.display = type === 'zeitstrafe' ? '' : 'none';
|
||||
document.getElementById('gsItemSperrtRow').style.display = type === 'zeitstrafe' ? '' : 'none';
|
||||
document.getElementById('gsItemUnlockRow').style.display = (type === 'zeitstrafe' || type === 'finisher') ? '' : 'none';
|
||||
|
||||
// Pre-fill when editing
|
||||
if (_gsItemIdx !== null) {
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (set) {
|
||||
let item;
|
||||
if (type === 'aufgabe') item = set.aufgaben[_gsItemIdx];
|
||||
if (type === 'zeitstrafe') item = set.zeitstrafen[_gsItemIdx];
|
||||
if (type === 'finisher') item = set.finisher[_gsItemIdx];
|
||||
if (item) {
|
||||
document.getElementById('gsItemTitle').value = item.title || '';
|
||||
document.getElementById('gsItemDesc').value = item.description || '';
|
||||
if (type === 'aufgabe') {
|
||||
document.getElementById('gsItemAufgabeLevel').value = item.level || 1;
|
||||
document.getElementById('gsItemMinutes').value = item.minutes || '';
|
||||
gsSetChecked('gsItemBen_', item.benoetigt || []);
|
||||
}
|
||||
if (type === 'zeitstrafe') {
|
||||
document.getElementById('gsItemZeitstrafeLevel').value = item.level || 1;
|
||||
document.getElementById('gsItemMinMin').value = item.minMinutes ?? '';
|
||||
document.getElementById('gsItemMaxMin').value = item.maxMinutes ?? '';
|
||||
document.getElementById('gsItemReleaseText').value = item.releaseText || '';
|
||||
gsSetChecked('gsItemSperr_', item.sperrt || []);
|
||||
}
|
||||
if (type === 'zeitstrafe' || type === 'finisher') {
|
||||
document.getElementById('gsItemBefore').checked = !!item.tempUnlockBeforeRequired;
|
||||
document.getElementById('gsItemAfter').checked = !!item.tempUnlockAfterRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('gsItemModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('gsItemTitle').focus(), 50);
|
||||
}
|
||||
|
||||
function closeGsItemModal() {
|
||||
document.getElementById('gsItemModal').classList.remove('open');
|
||||
_gsItemType = _gsItemSetId = _gsItemIdx = null;
|
||||
}
|
||||
|
||||
async function saveGsItem() {
|
||||
const title = document.getElementById('gsItemTitle').value.trim();
|
||||
const errEl = document.getElementById('gsItemError');
|
||||
if (!title) { errEl.textContent = 'Titel ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const set = _gameSets.find(s => s.id === _gsItemSetId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
|
||||
const desc = document.getElementById('gsItemDesc').value.trim() || null;
|
||||
let item;
|
||||
if (_gsItemType === 'aufgabe') {
|
||||
const min = parseInt(document.getElementById('gsItemMinutes').value);
|
||||
const ben = gsGetChecked('gsItemBen_');
|
||||
item = { title, description: desc,
|
||||
level: parseInt(document.getElementById('gsItemAufgabeLevel').value) || 1,
|
||||
minutes: isNaN(min) ? null : min,
|
||||
benoetigt: ben.length ? ben : null };
|
||||
if (_gsItemIdx !== null) updated.aufgaben[_gsItemIdx] = item;
|
||||
else updated.aufgaben.push(item);
|
||||
} else if (_gsItemType === 'zeitstrafe') {
|
||||
const minMin = parseInt(document.getElementById('gsItemMinMin').value);
|
||||
const maxMin = parseInt(document.getElementById('gsItemMaxMin').value);
|
||||
const sperrt = gsGetChecked('gsItemSperr_');
|
||||
const releaseText = document.getElementById('gsItemReleaseText').value.trim() || null;
|
||||
item = { title, description: desc,
|
||||
level: parseInt(document.getElementById('gsItemZeitstrafeLevel').value) || 1,
|
||||
minMinutes: isNaN(minMin) ? null : minMin,
|
||||
maxMinutes: isNaN(maxMin) ? null : maxMin,
|
||||
releaseText,
|
||||
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
|
||||
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked,
|
||||
sperrt: sperrt.length ? sperrt : null };
|
||||
if (_gsItemIdx !== null) updated.zeitstrafen[_gsItemIdx] = item;
|
||||
else updated.zeitstrafen.push(item);
|
||||
} else if (_gsItemType === 'finisher') {
|
||||
item = { title, description: desc,
|
||||
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
|
||||
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked };
|
||||
if (_gsItemIdx !== null) updated.finisher[_gsItemIdx] = item;
|
||||
else updated.finisher.push(item);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/chastity/game-sets/${_gsItemSetId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) { closeGsItemModal(); await loadGameSets(); }
|
||||
else { const b = await res.json().catch(()=>({})); errEl.textContent = b.error||'Fehler.'; errEl.style.display = ''; }
|
||||
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
|
||||
}
|
||||
|
||||
async function deleteGsItem(type, setId, idx) {
|
||||
if (!confirm('Eintrag wirklich löschen?')) return;
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
if (type === 'aufgabe') updated.aufgaben.splice(idx, 1);
|
||||
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx, 1);
|
||||
if (type === 'finisher') updated.finisher.splice(idx, 1);
|
||||
const res = await fetch(`/chastity/game-sets/${setId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) loadGameSets();
|
||||
}
|
||||
|
||||
async function duplicateGsItem(type, setId, idx) {
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
if (type === 'aufgabe') updated.aufgaben.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.aufgaben[idx])));
|
||||
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.zeitstrafen[idx])));
|
||||
if (type === 'finisher') updated.finisher.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.finisher[idx])));
|
||||
const res = await fetch(`/chastity/game-sets/${setId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) loadGameSets();
|
||||
}
|
||||
|
||||
document.getElementById('gsSetModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsSetModal(); });
|
||||
document.getElementById('gsItemModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsItemModal(); });
|
||||
document.getElementById('gsEditModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsEditModal(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
433
bin/main/static/games/chastity/taskgame.html
Normal file
433
bin/main/static/games/chastity/taskgame.html
Normal file
@@ -0,0 +1,433 @@
|
||||
<!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>Task Game – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.game-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.game-text {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.game-timer {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
text-align: center;
|
||||
margin: 0.75rem 0;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.game-timer.urgent { color: #e74c3c; }
|
||||
|
||||
.game-level-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-level-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.game-level-dot.active { background: var(--color-primary); }
|
||||
|
||||
.lock-messages {
|
||||
background: rgba(233,69,96,0.1);
|
||||
border: 1px solid rgba(233,69,96,0.3);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.lock-messages p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.5; }
|
||||
.lock-messages p:last-child { margin: 0; }
|
||||
|
||||
.init-box {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.init-box h2 { font-size: 1.1rem; margin: 0 0 0.75rem; }
|
||||
.group-list { display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem; }
|
||||
.group-item {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
.group-item:hover, .group-item.selected {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(var(--color-primary-rgb, 233,69,96), 0.06);
|
||||
}
|
||||
.group-item input[type=radio] { accent-color: var(--color-primary); }
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
padding: 0.65rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-secondary);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 0.92rem;
|
||||
cursor: pointer;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
#finisherBox {
|
||||
background: linear-gradient(135deg, rgba(233,69,96,0.15), rgba(155,89,182,0.12));
|
||||
border: 1px solid rgba(233,69,96,0.4);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
#finisherBox .trophy { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||||
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="container" style="max-width:480px;margin:0 auto;padding:1rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<button id="btnBack" onclick="goBack()"
|
||||
style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.4rem;">‹</button>
|
||||
<h1 style="margin:0;font-size:1.15rem;font-weight:700;">🎯 Task Game</h1>
|
||||
</div>
|
||||
|
||||
<!-- Level-Anzeige -->
|
||||
<div class="game-level-bar" id="levelBar" style="display:none;">
|
||||
<span style="font-size:0.78rem;color:var(--color-muted);font-weight:600;">Level</span>
|
||||
<div id="levelDots" style="display:flex;gap:0.35rem;"></div>
|
||||
<span id="levelText" style="font-size:0.78rem;color:var(--color-muted);margin-left:auto;"></span>
|
||||
</div>
|
||||
|
||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
|
||||
|
||||
<!-- Initialisierung: Gruppe wählen -->
|
||||
<div id="initBox" class="init-box" style="display:none;">
|
||||
<h2>Spiel-Set auswählen</h2>
|
||||
<p style="font-size:0.85rem;color:var(--color-muted);margin:0 0 1rem;">
|
||||
Wähle die Aufgabengruppe für dieses Spiel.
|
||||
</p>
|
||||
<div id="groupList" class="group-list"></div>
|
||||
<button class="btn-primary" id="btnStart" disabled onclick="startGame()">▶ Spiel starten</button>
|
||||
</div>
|
||||
|
||||
<!-- Laufendes Spiel -->
|
||||
<div id="gameBox" style="display:none;">
|
||||
|
||||
<!-- Task oder Lock in Queue -->
|
||||
<div id="queueBox" class="game-card" style="display:none;">
|
||||
<div class="game-label" id="queueLabel"></div>
|
||||
<div class="game-text" id="queueText"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktive Aufgabe (läuft) -->
|
||||
<div id="activeBox" class="game-card" style="display:none;">
|
||||
<div class="game-label">Aktive Aufgabe</div>
|
||||
<div class="game-text" id="activeText"></div>
|
||||
<div class="game-timer" id="activeTimer" style="display:none;"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Finisher -->
|
||||
<div id="finisherBox" style="display:none;">
|
||||
<div class="trophy">🏆</div>
|
||||
<h2>Level 6 erreicht!</h2>
|
||||
<div class="game-label" id="finisherTitle"></div>
|
||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
||||
<button class="btn-secondary" onclick="goBack()" style="margin-top:1.25rem;">Zurück zum Lock</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="loadingHint" style="text-align:center;color:var(--color-muted);padding:2rem 0;font-size:0.9rem;">
|
||||
Wird geladen…
|
||||
</div>
|
||||
<div id="errorBox" style="display:none;background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);
|
||||
border-radius:10px;padding:1rem;font-size:0.88rem;color:#e74c3c;margin-top:1rem;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const lockId = params.get('lockId');
|
||||
let _state = null;
|
||||
let _timerInt = null;
|
||||
let _pendingIsLock = false;
|
||||
let _pendingHasDuration = false;
|
||||
|
||||
function goBack() {
|
||||
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
||||
else history.back();
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async function boot() {
|
||||
try {
|
||||
const r = await fetch('/lock-game/state');
|
||||
if (r.status === 404) {
|
||||
await loadGroups();
|
||||
return;
|
||||
}
|
||||
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
||||
_state = await r.json();
|
||||
hide('loadingHint');
|
||||
await runGameLoop();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
const r = await fetch('/lock-game/groups');
|
||||
const groups = r.ok ? await r.json() : [];
|
||||
hide('loadingHint');
|
||||
const list = document.getElementById('groupList');
|
||||
if (groups.length === 0) {
|
||||
list.innerHTML = '<p style="font-size:0.85rem;color:var(--color-muted);">Keine passenden Aufgabengruppen gefunden.<br>Erstelle zuerst eine Gruppe vom Typ „Chastity Only".</p>';
|
||||
} else {
|
||||
list.innerHTML = groups.map(g => `
|
||||
<label class="group-item" onclick="selectGroup(this)">
|
||||
<input type="radio" name="gruppe" value="${g.gruppenId}" style="display:none;">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:0.95rem;">${esc(g.name)}</div>
|
||||
${g.beschreibung ? `<div style="font-size:0.8rem;color:var(--color-muted);">${esc(g.beschreibung)}</div>` : ''}
|
||||
</div>
|
||||
</label>`).join('');
|
||||
}
|
||||
show('initBox');
|
||||
}
|
||||
|
||||
function selectGroup(el) {
|
||||
document.querySelectorAll('.group-item').forEach(i => i.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
el.querySelector('input').checked = true;
|
||||
document.getElementById('btnStart').disabled = false;
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
const sel = document.querySelector('input[name="gruppe"]:checked');
|
||||
if (!sel) return;
|
||||
hide('initBox');
|
||||
show('loadingHint');
|
||||
try {
|
||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' });
|
||||
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
hide('loadingHint');
|
||||
await runGameLoop();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Game Loop ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function runGameLoop() {
|
||||
hide('queueBox');
|
||||
hide('activeBox');
|
||||
hide('finisherBox');
|
||||
clearTimer();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisher();
|
||||
return;
|
||||
}
|
||||
|
||||
renderLevelBar(_state.level);
|
||||
|
||||
// Aktive Aufgabe läuft noch
|
||||
if (_state.activeTask) {
|
||||
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue leer → nächsten Task holen
|
||||
if (!_state.taskInQueue && !_state.lockInQueue) {
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
}
|
||||
|
||||
showQueue();
|
||||
}
|
||||
|
||||
function showQueue() {
|
||||
if (_state.lockInQueue) {
|
||||
let sperre;
|
||||
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
||||
_pendingIsLock = true;
|
||||
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
|
||||
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
|
||||
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
|
||||
} else if (_state.taskInQueue) {
|
||||
let aufgabe;
|
||||
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
||||
_pendingIsLock = false;
|
||||
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe';
|
||||
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
|
||||
}
|
||||
show('gameBox');
|
||||
show('queueBox');
|
||||
}
|
||||
|
||||
function showActiveTask(text, endIso) {
|
||||
document.getElementById('activeText').textContent = text;
|
||||
show('gameBox');
|
||||
show('activeBox');
|
||||
|
||||
if (endIso) {
|
||||
const end = new Date(endIso);
|
||||
startTimer(end, document.getElementById('activeTimer'));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
// Locks prüfen (nach jeder Aktion)
|
||||
await checkAndShowLocks();
|
||||
|
||||
if (_pendingIsLock || _pendingHasDuration) {
|
||||
// Task/Sperre aktivieren und Timer starten
|
||||
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||||
if (!r.ok) { showError('Fehler'); return; }
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
} else {
|
||||
// Keine Dauer → sofort nächsten Task
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
}
|
||||
await runGameLoop();
|
||||
}
|
||||
|
||||
async function handleErledigt() {
|
||||
await checkAndShowLocks();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisher();
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
await runGameLoop();
|
||||
}
|
||||
|
||||
async function checkAndShowLocks() {
|
||||
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||
if (!r.ok) return;
|
||||
const texts = await r.json();
|
||||
if (texts && texts.length > 0) {
|
||||
const box = document.getElementById('lockMessages');
|
||||
box.innerHTML = texts.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
|
||||
show('lockMessages');
|
||||
await new Promise(res => setTimeout(res, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
async function showFinisher() {
|
||||
show('gameBox');
|
||||
const r = await fetch('/lock-game/finisher');
|
||||
if (!r.ok) {
|
||||
document.getElementById('finisherTitle').textContent = '';
|
||||
document.getElementById('finisherText').textContent = 'Glückwunsch – du hast Level 6 erreicht!';
|
||||
} else {
|
||||
const f = await r.json();
|
||||
document.getElementById('finisherTitle').textContent = f.kurzText || '';
|
||||
document.getElementById('finisherText').textContent = f.text || '';
|
||||
}
|
||||
show('finisherBox');
|
||||
}
|
||||
|
||||
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||||
|
||||
function renderLevelBar(level) {
|
||||
const bar = document.getElementById('levelBar');
|
||||
const dots = document.getElementById('levelDots');
|
||||
dots.innerHTML = [1,2,3,4,5].map(i =>
|
||||
`<div class="game-level-dot${i <= level ? ' active' : ''}"></div>`
|
||||
).join('');
|
||||
document.getElementById('levelText').textContent = 'Level ' + Math.min(level, 5);
|
||||
show('levelBar');
|
||||
}
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function startTimer(endDate, el) {
|
||||
el.style.display = '';
|
||||
clearTimer();
|
||||
_timerInt = setInterval(() => {
|
||||
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||
const m = String(Math.floor(diff / 60)).padStart(2, '0');
|
||||
const s = String(diff % 60).padStart(2, '0');
|
||||
el.textContent = m + ':' + s;
|
||||
el.classList.toggle('urgent', diff < 30);
|
||||
if (diff === 0) clearTimer();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
if (_timerInt) { clearInterval(_timerInt); _timerInt = null; }
|
||||
}
|
||||
|
||||
// ── Hilfsfunktionen ───────────────────────────────────────────────────────
|
||||
|
||||
function show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
|
||||
function hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
||||
function esc(s) { return String(s).replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function showError(msg) {
|
||||
hide('loadingHint');
|
||||
const box = document.getElementById('errorBox');
|
||||
box.textContent = msg || 'Ein Fehler ist aufgetreten.';
|
||||
box.style.display = '';
|
||||
}
|
||||
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
bin/main/static/img/card_game.png
Normal file
BIN
bin/main/static/img/card_game.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 370 KiB |
BIN
bin/main/static/img/card_slowmo.png
Normal file
BIN
bin/main/static/img/card_slowmo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
BIN
bin/main/static/img/card_speedup.png
Normal file
BIN
bin/main/static/img/card_speedup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 381 KiB |
Reference in New Issue
Block a user