Compare commits

..

2 Commits

Author SHA1 Message Date
ae64ca6aa3 Weitere Fixes am Taskgame
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
2026-05-03 20:08:58 +02:00
ca0e933d95 Weiter am Taskgame gebastelt
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
2026-05-02 23:10:41 +02:00
149 changed files with 1664 additions and 602 deletions

View File

@@ -1,12 +1,3 @@
Umsetzung des Spiels:
Der Lockee hat eine Stunde Zeit das Spiel zu starten, dies geschieht per Knopfdruck
Wenn er dies nicht schafft -> bei Keyholder, benachrichtige Keyholder und lass sie/ihn entscheiden, ansonsten freeze wie bei freeze card
Übernimm die Logik des Spiels aus dem BDSM Game.
Falls eine Zeitstrafe eine temporäre Öffnung vor oder nach der Aufgabe benötigt, öffne das Lock für 5 Minuten. Überzogene Zeit wird addiert und am Ende des Locks gefreezed
Selbiges gilt, falls der finisher eine temporäre Öffnung danach erfoldert
Benötigt der Finisher eine Öffnung davor, verwende die Logik der Cum Card, und addiere diese Zeit auf die möglicherweise schon vorhandenen Freeze Zeit am Ende des Locks
wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen, wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen,

View File

@@ -0,0 +1,2 @@
ALTER TABLE lock_game
MODIFY COLUMN aufgaben TEXT NULL;

View File

@@ -263,6 +263,12 @@
cursor: pointer; transition: border-color 0.15s, color 0.15s; cursor: pointer; transition: border-color 0.15s, color 0.15s;
} }
.btn-item-edit:hover { border-color: var(--color-text); color: var(--color-text); } .btn-item-edit:hover { border-color: var(--color-text); color: var(--color-text); }
.btn-item-copy {
background: none; border: 1px solid rgba(100,160,255,0.4); border-radius: 5px;
color: var(--color-muted); font-size: 0.75rem; padding: 0.2rem 0.6rem;
cursor: pointer; transition: border-color 0.15s, color 0.15s;
}
.btn-item-copy:hover { border-color: rgba(100,160,255,0.9); color: var(--color-text); }
.btn-item-delete { .btn-item-delete {
background: none; border: 1px solid rgba(233,69,96,0.4); border-radius: 5px; background: none; border: 1px solid rgba(233,69,96,0.4); border-radius: 5px;
color: var(--color-primary); font-size: 0.75rem; padding: 0.2rem 0.6rem; color: var(--color-primary); font-size: 0.75rem; padding: 0.2rem 0.6rem;
@@ -533,17 +539,11 @@
</div> </div>
</div> </div>
<div id="iTempUnlockRow"> <div id="iTempUnlockRow">
<label>Temporäre Öffnungen</label> <label class="toggle-switch" style="display:flex; align-items:center; gap:0.75rem; cursor:pointer; margin-top:0.25rem;">
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem;"> <input type="checkbox" id="iTempUnlockRequired">
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;"> <span class="toggle-track"></span>
<input type="checkbox" id="iTempUnlockBefore" style="accent-color:var(--color-primary); width:1rem; height:1rem;"> <span style="font-size:0.9rem;">Temporäre Öffnung erforderlich</span>
Temporäre Öffnung <em>vor</em> der Zeitstrafe erforderlich
</label> </label>
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;">
<input type="checkbox" id="iTempUnlockAfter" style="accent-color:var(--color-primary); width:1rem; height:1rem;">
Temporäre Öffnung <em>nach</em> der Zeitstrafe erforderlich
</label>
</div>
</div> </div>
<div id="iReleaseTextRow"> <div id="iReleaseTextRow">
<label for="iReleaseText">Text bei Aufhebung</label> <label for="iReleaseText">Text bei Aufhebung</label>
@@ -765,7 +765,7 @@
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''} ${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)} ${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
${g.availableIn !== 'CHASTITY_ONLY' ? 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('Zeitstrafen',sortByLevelThenName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type)}
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)} ${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
</div> </div>
</div>`; </div>`;
@@ -814,6 +814,7 @@
function renderAufgabe(a, type, gruppenId) { function renderAufgabe(a, type, gruppenId) {
_itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId }; _itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId };
const badges = []; const badges = [];
(a.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
const zeit = formatSek(a.sekundenVon, a.sekundenBis); const zeit = formatSek(a.sekundenVon, a.sekundenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`); if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`); if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
@@ -826,6 +827,7 @@
const actionBtns = type === 'user' ? ` const actionBtns = type === 'user' ? `
<div class="item-action-btns"> <div class="item-action-btns">
<button class="btn-item-edit" onclick="openEditItemModal('${esc(a.aufgabeId)}',event)">✎ Bearbeiten</button> <button class="btn-item-edit" onclick="openEditItemModal('${esc(a.aufgabeId)}',event)">✎ Bearbeiten</button>
<button class="btn-item-copy" onclick="duplicateItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
<button class="btn-item-delete" onclick="deleteItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">✕ Löschen</button> <button class="btn-item-delete" onclick="deleteItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : ''; </div>` : '';
@@ -841,6 +843,7 @@
function renderStrafe(s, type, gruppenId) { function renderStrafe(s, type, gruppenId) {
_itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId }; _itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId };
const badges = []; const badges = [];
(s.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
const zeit = formatSek(s.sekundenVon, s.sekundenBis); const zeit = formatSek(s.sekundenVon, s.sekundenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`); if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`); if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
@@ -853,6 +856,7 @@
const actionBtns = type === 'user' ? ` const actionBtns = type === 'user' ? `
<div class="item-action-btns"> <div class="item-action-btns">
<button class="btn-item-edit" onclick="openEditItemModal('${esc(s.strafeId)}',event)">✎ Bearbeiten</button> <button class="btn-item-edit" onclick="openEditItemModal('${esc(s.strafeId)}',event)">✎ Bearbeiten</button>
<button class="btn-item-copy" onclick="duplicateItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
<button class="btn-item-delete" onclick="deleteItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">✕ Löschen</button> <button class="btn-item-delete" onclick="deleteItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : ''; </div>` : '';
@@ -868,8 +872,10 @@
function renderZeitstrafe(z, type, gruppenId) { function renderZeitstrafe(z, type, gruppenId) {
_itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId }; _itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId };
const badges = []; const badges = [];
(z.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
const zeit = formatMin(z.minutenVon, z.minutenBis); const zeit = formatMin(z.minutenVon, z.minutenBis);
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`); if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
if (z.level != null) badges.push(`<span class="badge">Level ${esc(String(z.level))}</span>`);
const detailRows = []; const detailRows = [];
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`); if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
@@ -879,6 +885,7 @@
const actionBtns = type === 'user' ? ` const actionBtns = type === 'user' ? `
<div class="item-action-btns"> <div class="item-action-btns">
<button class="btn-item-edit" onclick="openEditItemModal('${esc(z.sperreId)}',event)">✎ Bearbeiten</button> <button class="btn-item-edit" onclick="openEditItemModal('${esc(z.sperreId)}',event)">✎ Bearbeiten</button>
<button class="btn-item-copy" onclick="duplicateItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
<button class="btn-item-delete" onclick="deleteItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">✕ Löschen</button> <button class="btn-item-delete" onclick="deleteItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : ''; </div>` : '';
@@ -896,6 +903,7 @@
function renderFinisher(f, type, gruppenId) { function renderFinisher(f, type, gruppenId) {
_itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId }; _itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId };
const badges = []; const badges = [];
(f.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
if (f.geschlecht) badges.push(`<span class="badge badge-neutral">${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}</span>`); if (f.geschlecht) badges.push(`<span class="badge badge-neutral">${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}</span>`);
const detailRows = []; const detailRows = [];
@@ -906,6 +914,7 @@
const actionBtns = type === 'user' ? ` const actionBtns = type === 'user' ? `
<div class="item-action-btns"> <div class="item-action-btns">
<button class="btn-item-edit" onclick="openEditItemModal('${esc(f.finisherId)}',event)">✎ Bearbeiten</button> <button class="btn-item-edit" onclick="openEditItemModal('${esc(f.finisherId)}',event)">✎ Bearbeiten</button>
<button class="btn-item-copy" onclick="duplicateItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
<button class="btn-item-delete" onclick="deleteItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">✕ Löschen</button> <button class="btn-item-delete" onclick="deleteItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
</div>` : ''; </div>` : '';
@@ -944,10 +953,36 @@
finisher: apiUrl('/finisher') finisher: apiUrl('/finisher')
}; };
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' }; const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' };
const ITEM_COPY_URL = {
aufgabe: apiUrl('/aufgabe/copy'),
strafe: '/strafe/copy',
zeitstrafe: '/sperre/copy',
finisher: apiUrl('/finisher/copy')
};
function duplicateItem(kind, itemId, gruppenId, event) {
event.stopPropagation();
const copyUrl = ITEM_COPY_URL[kind];
if (!copyUrl) return;
fetch(`${copyUrl}/${itemId}`, { method: 'POST' }).then(r => {
if (r.ok) {
pendingExpandId = gruppenId;
pendingExpandType = 'user';
_notifyOnLoad = true; loadUserGruppen();
} else {
document.getElementById('userActionError').textContent = 'Fehler beim Duplizieren (HTTP ' + r.status + ').';
}
}).catch(() => {
document.getElementById('userActionError').textContent = 'Verbindungsfehler.';
});
}
function deleteItem(kind, itemId, gruppenId, event) { function deleteItem(kind, itemId, gruppenId, event) {
event.stopPropagation(); event.stopPropagation();
if (!confirm('Eintrag wirklich löschen?')) return; openConfirmModal('Eintrag wirklich löschen?', () => _doDeleteItem(kind, itemId, gruppenId));
}
function _doDeleteItem(kind, itemId, gruppenId) {
const deleteUrl = ITEM_DELETE_URL[kind]; const deleteUrl = ITEM_DELETE_URL[kind];
if (!deleteUrl) return; if (!deleteUrl) return;
const body = { [ITEM_DELETE_FIELD[kind]]: itemId }; const body = { [ITEM_DELETE_FIELD[kind]]: itemId };
@@ -1430,7 +1465,7 @@
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label'); const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
if (lbl) lbl.style.display = isChastity ? 'none' : ''; if (lbl) lbl.style.display = isChastity ? 'none' : '';
}); });
document.getElementById('iTempUnlockRow').style.display = (isZeit && isChastity) ? 'block' : 'none'; document.getElementById('iTempUnlockRow').style.display = ((isZeit || isFinisher) && isChastity) ? 'block' : 'none';
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none'; document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
} }
@@ -1449,8 +1484,7 @@
document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false); document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false);
document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false); document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false);
document.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false); document.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false);
document.getElementById('iTempUnlockBefore').checked = false; document.getElementById('iTempUnlockRequired').checked = false;
document.getElementById('iTempUnlockAfter').checked = false;
_selectedToys = []; _selectedToys = [];
renderSelectedToys(); renderSelectedToys();
document.getElementById('itemModalError').style.display = 'none'; document.getElementById('itemModalError').style.display = 'none';
@@ -1495,6 +1529,9 @@
const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`); const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`);
if (rb) rb.checked = true; if (rb) rb.checked = true;
} }
if (_isChastityMode) {
document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true;
}
} else { } else {
document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : ''; document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : '';
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : ''; document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
@@ -1502,8 +1539,7 @@
(d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; }); (d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; });
if (_isChastityMode) { if (_isChastityMode) {
document.getElementById('iLevel').value = d.level != null ? d.level : ''; document.getElementById('iLevel').value = d.level != null ? d.level : '';
document.getElementById('iTempUnlockBefore').checked = d.tempUnlockBeforeRequired === true; document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true;
document.getElementById('iTempUnlockAfter').checked = d.tempUnlockAfterRequired === true;
} }
} }
@@ -1670,7 +1706,8 @@
gruppeId: isEdit ? undefined : currentItemGruppeId, gruppeId: isEdit ? undefined : currentItemGruppeId,
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'), benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'), benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })) benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })),
tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null
}; };
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher'); url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
method = isEdit ? 'PUT' : 'POST'; method = isEdit ? 'PUT' : 'POST';
@@ -1698,8 +1735,7 @@
releaseText: document.getElementById('iReleaseText').value.trim() || null, releaseText: document.getElementById('iReleaseText').value.trim() || null,
sperreFuer, sperreFuer,
level: zeitLevel, level: zeitLevel,
tempUnlockBeforeRequired: _isChastityMode ? document.getElementById('iTempUnlockBefore').checked : null, tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null,
tempUnlockAfterRequired: _isChastityMode ? document.getElementById('iTempUnlockAfter').checked : null,
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })) benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
}; };
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only

View File

@@ -679,6 +679,7 @@
// ── Gruppe lists ── // ── Gruppe lists ──
function renderGruppeList(containerId, gruppen) { function renderGruppeList(containerId, gruppen) {
gruppen = gruppen.filter(g => g.availableIn !== 'CHASTITY_ONLY');
const ul = document.getElementById(containerId); const ul = document.getElementById(containerId);
const section = ul.closest('[id^="section"]'); const section = ul.closest('[id^="section"]');
const selectAllWrap = section?.querySelector('.select-all-label'); const selectAllWrap = section?.querySelector('.select-all-label');

View File

@@ -256,9 +256,14 @@
</div> </div>
<div class="checkbox-row" id="rowTestLock"> <div class="checkbox-row" id="rowTestLock">
<input type="checkbox" id="testLock"> <input type="checkbox" id="testLock" onchange="onTestLockChange()">
<label for="testLock">Test-Lock <span class="form-hint">(kein echter Lock, zum Ausprobieren)</span></label> <label for="testLock">Test-Lock <span class="form-hint">(kein echter Lock, zum Ausprobieren)</span></label>
</div> </div>
<div id="rowSpeedFactor" style="display:none; align-items:center; gap:12px; padding:6px 0;">
<label for="speedFactor" style="white-space:nowrap;">Geschwindigkeit:</label>
<input type="range" id="speedFactor" min="1" max="10" value="1" style="flex:1;" oninput="document.getElementById('speedFactorLabel').textContent = '×' + this.value">
<span id="speedFactorLabel" style="min-width:32px; text-align:right;">×1</span>
</div>
</div> </div>
<div class="error-msg" id="errorMsg"></div> <div class="error-msg" id="errorMsg"></div>
@@ -515,6 +520,7 @@
khInput.readOnly = true; khInput.readOnly = true;
khInput.style.opacity = '0.6'; khInput.style.opacity = '0.6';
document.getElementById('rowTestLock').style.display = 'none'; document.getElementById('rowTestLock').style.display = 'none';
document.getElementById('rowSpeedFactor').style.display = 'none';
document.getElementById('rowDetailsVisible').style.display = ''; document.getElementById('rowDetailsVisible').style.display = '';
} else { } else {
khInput.readOnly = false; khInput.readOnly = false;
@@ -728,6 +734,15 @@
el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
function onTestLockChange() {
const checked = document.getElementById('testLock').checked;
document.getElementById('rowSpeedFactor').style.display = checked ? 'flex' : 'none';
if (!checked) {
document.getElementById('speedFactor').value = 1;
document.getElementById('speedFactorLabel').textContent = '×1';
}
}
// ── Absenden ── // ── Absenden ──
async function createSession() { async function createSession() {
document.getElementById('errorMsg').style.display = 'none'; document.getElementById('errorMsg').style.display = 'none';
@@ -756,6 +771,7 @@
const isFriendLockee = lockeeVal && lockeeVal !== myUserId; const isFriendLockee = lockeeVal && lockeeVal !== myUserId;
const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5); const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5);
const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked; const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked;
const speedFactor = isTestLock ? parseInt(document.getElementById('speedFactor').value) : 1;
let endpoint, body; let endpoint, body;
@@ -769,6 +785,7 @@
testLock: isTestLock, testLock: isTestLock,
unlockCodeLength: unlockCodeLen, unlockCodeLength: unlockCodeLen,
controllType: selectedLockControl, controllType: selectedLockControl,
speedFactor: speedFactor,
}; };
} else { } else {
// CardLock // CardLock
@@ -798,6 +815,7 @@
controllType: selectedLockControl, controllType: selectedLockControl,
gameSetId: t.gameSetId || null, gameSetId: t.gameSetId || null,
gameSpieldauerIdx: t.gameSpieldauerIdx ?? null, gameSpieldauerIdx: t.gameSpieldauerIdx ?? null,
speedFactor: speedFactor,
}; };
} }

View File

@@ -23,6 +23,7 @@
letter-spacing: 0.08em; letter-spacing: 0.08em;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
height: 1.2rem; height: 1.2rem;
text-align: center;
} }
.game-text { .game-text {
font-size: 1rem; font-size: 1rem;
@@ -31,6 +32,7 @@
white-space: pre-wrap; white-space: pre-wrap;
height: 14rem; height: 14rem;
overflow-y: auto; overflow-y: auto;
text-align: center;
} }
.game-timer { .game-timer {
font-size: 2.2rem; font-size: 2.2rem;
@@ -52,6 +54,9 @@
margin-top: 1rem; margin-top: 1rem;
height: 2.75rem; height: 2.75rem;
} }
#confirmModal { display:none; }
#confirmModal.open { display:flex; }
.level-display { .level-display {
display: flex; display: flex;
@@ -64,16 +69,6 @@
object-fit: contain; object-fit: contain;
} }
.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 { .init-box {
background: var(--color-card); background: var(--color-card);
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
@@ -134,15 +129,13 @@
margin-top: 0.6rem; margin-top: 0.6rem;
} }
#finisherBox { #levelTrophy {
background: linear-gradient(135deg, rgba(233,69,96,0.15), rgba(155,89,182,0.12)); font-size: 3.5rem;
border: 1px solid rgba(233,69,96,0.4); line-height: 72px;
border-radius: 14px; width: 72px;
padding: 1.5rem; height: 72px;
text-align: center; 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> </style>
</head> </head>
<body class="app"> <body class="app">
@@ -153,11 +146,9 @@
<!-- Level-Anzeige --> <!-- Level-Anzeige -->
<div class="level-display" id="levelDisplay" style="display:none;"> <div class="level-display" id="levelDisplay" style="display:none;">
<img id="levelImg" src="" alt="Level"> <img id="levelImg" src="" alt="Level">
<span id="levelTrophy" style="display:none;">🏆</span>
</div> </div>
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
<!-- Toy-Auswahl vor Spielstart --> <!-- Toy-Auswahl vor Spielstart -->
<div id="toyBox" style="display:none;"> <div id="toyBox" style="display:none;">
<div class="game-card"> <div class="game-card">
@@ -198,20 +189,51 @@
<!-- Release-Text (Sperren) --> <!-- Release-Text (Sperren) -->
<div id="lockReleaseBox" class="game-card" style="display:none;"> <div id="lockReleaseBox" class="game-card" style="display:none;">
<div class="game-label">🔓 Sperre aufgehoben</div> <div class="game-label">🔓 Zeitstrafe verbüßt</div>
<div class="game-text" id="releaseText"></div> <div class="game-text" id="releaseText"></div>
<div style="margin-top:1.1rem;"> <div class="game-timer"></div>
<button class="btn-primary" id="btnReleaseOk">OK</button> <div class="game-btn-row">
<button class="btn-primary" id="btnReleaseOk" style="width:100%;height:100%;">OK</button>
</div>
</div>
<!-- Temporäre Öffnung -->
<div id="tempOpeningBox" class="game-card" style="display:none;">
<!-- Phase 1: Öffnung aktiv -->
<div id="tempPhase1">
<div class="game-label" style="text-align:center;">🔓 Entsperrcode</div>
<div class="game-text" id="tempOpeningCode"
style="display:flex;align-items:center;justify-content:center;
font-size:1.8rem;font-weight:700;letter-spacing:0.18em;
color:var(--color-primary);white-space:normal;"></div>
<div class="game-timer" id="tempOpeningTimer" style="margin-top:0.75rem;"></div>
<div class="game-btn-row">
<button class="btn-primary" style="width:100%;height:100%;" onclick="doEndTempOpening()">✓ Erledigt</button>
</div>
</div>
<!-- Phase 2: Neuer Code zum Schließen -->
<div id="tempPhase2" style="display:none;">
<div class="game-label" style="text-align:center;">🔒 Lock schließen</div>
<div class="game-text" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.75rem;white-space:normal;">
<span style="font-size:0.9rem;color:var(--color-muted);text-align:center;">Nutze den Entsperrcode um den Schlüssel wieder zu verschließen.</span>
<div id="tempNewCode" style="font-size:1.8rem;font-weight:700;letter-spacing:0.18em;color:var(--color-primary);text-align:center;"></div>
<div id="tempScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;text-align:center;"></div>
</div>
<div class="game-timer"></div>
<div class="game-btn-row">
<button class="btn-primary" id="tempPhase2Btn" style="width:100%;height:100%;" onclick="startTempScramble()">OK</button>
</div>
</div> </div>
</div> </div>
<!-- Finisher --> <!-- Finisher -->
<div id="finisherBox" style="display:none;"> <div id="finisherBox" class="game-card" style="display:none;">
<div class="trophy">🏆</div> <div class="game-label" id="finisherLabel"></div>
<h2>Level 6 erreicht!</h2> <div class="game-text" id="finisherText"></div>
<div class="game-label" id="finisherTitle"></div> <div class="game-timer" id="finisherTimer"></div>
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div> <div class="game-btn-row">
<button class="btn-primary" id="btnFinisherOk" style="margin-top:1.25rem;">✓ OK</button> <button class="btn-primary" id="finisherBtn" style="width:100%;height:100%;"></button>
</div>
</div> </div>
<!-- Debug --> <!-- Debug -->
@@ -229,6 +251,17 @@
</div> </div>
</div> </div>
<div class="modal-backdrop" id="confirmModal">
<div class="modal" style="max-width:420px;">
<h2>Bestätigung</h2>
<p id="confirmModalText" style="color:var(--color-text);margin-bottom:1.25rem;line-height:1.5;"></p>
<div class="modal-actions">
<button class="btn-cancel" id="confirmModalCancel">Nein</button>
<button class="btn-save" id="confirmModalOk" style="background:var(--color-danger,#e74c3c);">Ja, abbrechen</button>
</div>
</div>
</div>
<script src="/js/icons.js"></script> <script src="/js/icons.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script> <script>
@@ -240,6 +273,10 @@
let _state = null; let _state = null;
let _timerInt = null; let _timerInt = null;
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done' let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
let _tempOpeningTimerInt = null;
let _tempScrambleTimer = null;
let _tempScrambleCd = null;
let _tempOpeningFromQueue = false; // true only when opening was triggered from a queued Zeitstrafe (doQueueStart)
function goBack() { function goBack() {
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId; if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
@@ -340,6 +377,7 @@
async function startWithExcludedToys(gameSetId, excludedToyIds) { async function startWithExcludedToys(gameSetId, excludedToyIds) {
const params = new URLSearchParams({ aufgabenGruppeId: gameSetId }); const params = new URLSearchParams({ aufgabenGruppeId: gameSetId });
if (lockId) params.append('lockId', lockId);
excludedToyIds.forEach(id => params.append('excludedToyIds', id)); excludedToyIds.forEach(id => params.append('excludedToyIds', id));
const r = await fetch('/lock-game/init?' + params.toString(), { method: 'POST' }); const r = await fetch('/lock-game/init?' + params.toString(), { method: 'POST' });
@@ -353,6 +391,13 @@
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
hide('loadingHint'); hide('loadingHint');
// Remove ?fresh=1 from URL so F5 resumes instead of restarting.
const cleanParams = new URLSearchParams(location.search);
cleanParams.delete('fresh');
const cleanSearch = cleanParams.toString();
history.replaceState(null, '', location.pathname + (cleanSearch ? '?' + cleanSearch : ''));
await runGameLoop(); await runGameLoop();
} }
@@ -417,6 +462,8 @@
await loadAndShowToys(sel.value); await loadAndShowToys(sel.value);
} }
// ── Benötigt-Checkboxen ───────────────────────────────────────────────────
// ── Game Loop ───────────────────────────────────────────────────────────── // ── Game Loop ─────────────────────────────────────────────────────────────
function setGameCard(label, text, action, btnLabel) { function setGameCard(label, text, action, btnLabel) {
@@ -430,15 +477,27 @@
} }
async function runGameLoop() { async function runGameLoop() {
hide('gameCard');
hide('finisherBox');
clearTimer(); clearTimer();
if (_state.level >= 6) { if (_state.tempOpeningTime) {
await showFinisherFlow(); hide('finisherBox');
showTempOpeningDialog();
return; return;
} }
if (_state.finisher) {
hide('tempOpeningBox');
showFinisherUI();
return;
}
// Normal game states all use gameCard — show it once here so the card
// frame stays stable across transitions (queue ↔ active ↔ active-done).
show('gameBox');
show('gameCard');
hide('tempOpeningBox');
hide('finisherBox');
renderLevelBar(_state.level); renderLevelBar(_state.level);
if (_state.activeTask) { if (_state.activeTask) {
@@ -459,21 +518,20 @@
if (_state.lockInQueue) { if (_state.lockInQueue) {
let sperre; let sperre;
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; } try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten'); const btnLabel = sperre.tempUnlockRequired ? '⏱ Weiter zur temporären Öffnung' : '✓ Erledigt';
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', btnLabel);
} else if (_state.taskInQueue) { } else if (_state.taskInQueue) {
let aufgabe; let aufgabe;
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; } try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis); const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done', setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
hasDuration ? '▶ Starten' : '✓ Erledigt'); hasDuration ? '▶ Starten' : '✓ Erledigt');
} }
show('gameBox');
show('gameCard');
} }
function showActiveTask(text, endIso) { function showActiveTask(text, endIso) {
show('gameBox');
show('gameCard');
const timerEl = document.getElementById('gameTimer'); const timerEl = document.getElementById('gameTimer');
timerEl.classList.remove('active', 'urgent'); timerEl.classList.remove('active', 'urgent');
timerEl.textContent = ''; timerEl.textContent = '';
@@ -497,25 +555,50 @@
switch (_gameAction) { switch (_gameAction) {
case 'queue-start': doQueueStart(); break; case 'queue-start': doQueueStart(); break;
case 'queue-done': doQueueDone(); break; case 'queue-done': doQueueDone(); break;
case 'active-running': doCancelCountdown(); break; case 'active-running': openConfirmModal('Aufgabe wirklich abbrechen?', () => doCancelCountdown()); break;
case 'active-done': doErledigt(); break; case 'active-done': doErledigt(); break;
} }
} }
async function doQueueStart() { async function doQueueStart() {
try { try {
await checkAndShowLocks(); const wasLock = !!_state.lockInQueue;
let tempUnlockRequired = false;
if (wasLock) {
try { tempUnlockRequired = JSON.parse(_state.lockInQueue).tempUnlockRequired === true; } catch (_) {}
}
if (wasLock && tempUnlockRequired) {
// Erst Öffnung starten, Zeitstrafe wird nach der Öffnung angewendet
const r = await fetch('/lock-game/start-temp-opening', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten der Öffnung'); return; }
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
_tempOpeningFromQueue = true;
showTempOpeningDialog();
} else {
const r = await fetch('/lock-game/apply-task', { method: 'POST' }); const r = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten'); return; } if (!r.ok) { showError('Fehler beim Starten'); return; }
if (wasLock) {
const nextR = await fetch('/lock-game/abandon-task', { method: 'POST' });
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
}
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
await runGameLoop(); await runGameLoop();
}
} catch (e) { showError(e.message || 'Fehler (Starten)'); } } catch (e) { showError(e.message || 'Fehler (Starten)'); }
} }
async function doQueueDone() { async function doQueueDone() {
try { try {
await checkAndShowLocks(); const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) {
const sperren = await lockR.json();
for (const sperre of (sperren || [])) {
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
}
}
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' }); const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden'); return; } if (!applyR.ok) { showError('Fehler beim Anwenden'); return; }
const nextR = await fetch('/lock-game/next-task', { method: 'POST' }); const nextR = await fetch('/lock-game/next-task', { method: 'POST' });
@@ -526,16 +609,28 @@
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); } } catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
} }
function doCancelCountdown() { async function doCancelCountdown() {
clearTimer(); clearTimer();
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) {
const sperren = await lockR.json();
for (const sperre of (sperren || [])) {
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
}
}
_gameAction = 'active-done'; _gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt'; document.getElementById('gameBtn').textContent = '✓ Erledigt';
} }
async function doErledigt() { async function doErledigt() {
try { try {
await checkAndShowLocks(); const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (_state.level >= 6) { await showFinisherFlow(); return; } if (lockR.ok) {
const sperren = await lockR.json();
for (const sperre of (sperren || [])) {
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
}
}
const r = await fetch('/lock-game/next-task', { method: 'POST' }); const r = await fetch('/lock-game/next-task', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Ziehen'); return; } if (!r.ok) { showError('Fehler beim Ziehen'); return; }
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
@@ -544,64 +639,190 @@
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); } } catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
} }
async function checkAndShowLocks() { function showTempOpeningDialog() {
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
if (!r.ok) return;
const texts = await r.json();
const valid = texts ? texts.filter(t => t != null && t !== '') : [];
if (valid.length > 0) {
const box = document.getElementById('lockMessages');
box.innerHTML = valid.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
show('lockMessages');
await new Promise(res => setTimeout(res, 2000));
}
}
async function showFinisherFlow() {
show('gameBox'); show('gameBox');
hide('gameCard'); hide('gameCard');
hide('lockReleaseBox'); hide('lockReleaseBox');
hide('finisherBox'); hide('finisherBox');
// 1. Release-Texte sequenziell anzeigen document.getElementById('tempOpeningCode').textContent = _state.tempOpeningCode || '';
try {
const r = await fetch('/lock-game/release-locks'); show('tempPhase1');
if (r.ok) { hide('tempPhase2');
const texts = await r.json();
for (const text of texts) { const timerEl = document.getElementById('tempOpeningTimer');
await waitForReleaseOk(text); timerEl.textContent = '';
timerEl.classList.remove('active', 'urgent');
if (_state.tempOpeningTime && _state.tempOpeningDuration) {
const endTime = new Date(new Date(_state.tempOpeningTime).getTime() + _state.tempOpeningDuration * 60000);
if (endTime > Date.now()) startTempOpeningTimer(endTime, timerEl);
} }
show('tempOpeningBox');
} }
} catch (_) { /* ignorieren */ }
// 2. Finisher laden und Zeit messen function startTempOpeningTimer(endDate, el) {
const finisherStartTime = Date.now(); if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
let finisher = null; el.classList.add('active');
function tick() {
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) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
}
tick();
_tempOpeningTimerInt = setInterval(tick, 1000);
}
async function doEndTempOpening() {
try { try {
const r = await fetch('/lock-game/finisher'); if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
if (r.ok) finisher = await r.json();
} catch (_) { /* ignorieren */ }
document.getElementById('finisherTitle').textContent = finisher?.kurzText || ''; // Only apply the queued Zeitstrafe when the opening was explicitly started from one (Path 1).
document.getElementById('finisherText').textContent = finisher?.text || 'Glückwunsch du hast Level 6 erreicht!'; // When triggered by a background lock expiry via checkLocks() (Path 2), lockInQueue may be set
// to the *next* randomly-picked item — applying it would incorrectly create another background
// lock with tempUnlockRequired and produce an infinite opening loop.
const fromQueue = _tempOpeningFromQueue;
_tempOpeningFromQueue = false;
const hasLockInQueue = fromQueue && !!_state.lockInQueue;
if (hasLockInQueue) {
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden der Zeitstrafe'); return; }
}
// 3. Warten bis Nutzer OK drückt const endR = await fetch('/lock-game/end-temp-opening', { method: 'POST' });
await new Promise(resolve => { if (!endR.ok) { showError('Fehler beim Beenden der Öffnung'); return; }
document.getElementById('btnFinisherOk').onclick = resolve; const endData = await endR.json();
const newCode = endData.newUnlockCode;
if (newCode) {
document.getElementById('tempNewCode').textContent = newCode;
document.getElementById('tempScrambleCountdown').style.display = 'none';
document.getElementById('tempPhase2Btn').textContent = 'OK';
document.getElementById('tempPhase2Btn').onclick = startTempScramble;
hide('tempPhase1');
show('tempPhase2');
} else {
await finishTempOpening();
}
} catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); }
}
async function finishTempOpening() {
hide('tempOpeningBox');
await fetch('/lock-game/abandon-task', { method: 'POST' });
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
}
function startTempScramble() {
const codeEl = document.getElementById('tempNewCode');
const cdEl = document.getElementById('tempScrambleCountdown');
const btnEl = document.getElementById('tempPhase2Btn');
const len = codeEl.textContent.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
async function finish() {
stopped = true;
clearInterval(_tempScrambleTimer); _tempScrambleTimer = null;
clearInterval(_tempScrambleCd); _tempScrambleCd = null;
await finishTempOpening();
}
cdEl.style.display = '';
btnEl.textContent = 'Abbrechen';
btnEl.onclick = finish;
function updateCd() {
const m = Math.floor(remaining / 60);
const s = remaining % 60;
cdEl.textContent = `${m}:${String(s).padStart(2, '0')}`;
}
updateCd();
_tempScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
_tempScrambleCd = setInterval(() => {
if (stopped) return;
if (--remaining <= 0) finish();
else updateCd();
}, 1000);
}
function showFinisherUI() {
show('gameBox');
hide('gameCard');
hide('lockReleaseBox');
hide('tempOpeningBox');
renderLevelBar(6);
let finisher = {};
try { finisher = JSON.parse(_state.finisher); } catch (_) {}
document.getElementById('finisherLabel').textContent = finisher.kurzText || '';
document.getElementById('finisherText').textContent = finisher.text || '';
const timerEl = document.getElementById('finisherTimer');
const btnEl = document.getElementById('finisherBtn');
timerEl.classList.remove('active', 'urgent');
timerEl.textContent = '';
if (_state.finisherStartedAt) {
timerEl.classList.add('active');
startElapsedTimer(new Date(_state.finisherStartedAt));
btnEl.textContent = '✓ Erledigt';
btnEl.onclick = doEndFinisher;
} else {
btnEl.textContent = '▶ Starten';
btnEl.onclick = doStartFinisher;
}
show('finisherBox'); show('finisherBox');
}); }
// 4. Zeit berechnen und Spiel beenden async function doStartFinisher() {
const timeInMinutes = Math.round((Date.now() - finisherStartTime) / 60000); await fetch('/lock-game/start-finisher', { method: 'POST' });
const params = new URLSearchParams({ timeInMinutes }); const r = await fetch('/lock-game/state');
if (lockId) params.set('lockId', lockId); _state = await r.json();
await fetch('/lock-game/complete?' + params.toString(), { method: 'POST' }); const timerEl = document.getElementById('finisherTimer');
timerEl.classList.add('active');
startElapsedTimer(new Date(_state.finisherStartedAt));
const btnEl = document.getElementById('finisherBtn');
btnEl.textContent = '✓ Erledigt';
btnEl.onclick = doEndFinisher;
}
async function doEndFinisher() {
clearTimer();
await fetch('/lock-game/end-finisher', { method: 'POST' });
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
await fetch(url, { method: 'POST' });
goBack(); goBack();
} }
function startElapsedTimer(startDate) {
clearTimer();
const el = document.getElementById('finisherTimer');
function tick() {
const diff = Math.floor((Date.now() - startDate) / 1000);
const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s;
}
tick();
_timerInt = setInterval(tick, 1000);
}
function waitForReleaseOk(text) { function waitForReleaseOk(text) {
return new Promise(resolve => { return new Promise(resolve => {
document.getElementById('releaseText').textContent = text; hide('gameCard');
document.getElementById('releaseText').textContent = text || '';
document.getElementById('btnReleaseOk').onclick = () => { document.getElementById('btnReleaseOk').onclick = () => {
hide('lockReleaseBox'); hide('lockReleaseBox');
resolve(); resolve();
@@ -613,8 +834,12 @@
// ── Level-Bar ───────────────────────────────────────────────────────────── // ── Level-Bar ─────────────────────────────────────────────────────────────
function renderLevelBar(level) { function renderLevelBar(level) {
const lvl = Math.min(Math.max(level, 1), 5); const isFinisher = level >= 6;
document.getElementById('levelImg').src = `/img/lvl${lvl}.png`; document.getElementById('levelImg').style.display = isFinisher ? 'none' : '';
document.getElementById('levelTrophy').style.display = isFinisher ? '' : 'none';
if (!isFinisher) {
document.getElementById('levelImg').src = `/img/lvl${Math.min(Math.max(level, 1), 5)}.png`;
}
show('levelDisplay'); show('levelDisplay');
} }
@@ -627,7 +852,7 @@
function startTimer(endDate, el) { function startTimer(endDate, el) {
el.classList.add('active'); el.classList.add('active');
clearTimer(); clearTimer();
_timerInt = setInterval(() => { function tick() {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000)); const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
const m = String(Math.floor(diff / 60)).padStart(2, '0'); const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0'); const s = String(diff % 60).padStart(2, '0');
@@ -639,7 +864,9 @@
_gameAction = 'active-done'; _gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt'; document.getElementById('gameBtn').textContent = '✓ Erledigt';
} }
}, 1000); }
tick();
_timerInt = setInterval(tick, 1000);
} }
function clearTimer() { function clearTimer() {
@@ -658,6 +885,25 @@
box.style.display = ''; box.style.display = '';
} }
// ── Confirm Modal ─────────────────────────────────────────────────────────
const _confirmModal = document.getElementById('confirmModal');
document.getElementById('confirmModalCancel').addEventListener('click', closeConfirmModal);
document.getElementById('confirmModalOk').addEventListener('click', closeConfirmModal);
_confirmModal.addEventListener('click', e => { if (e.target === _confirmModal) closeConfirmModal(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && _confirmModal.classList.contains('open')) closeConfirmModal(); });
function closeConfirmModal() { _confirmModal.classList.remove('open'); }
function openConfirmModal(text, onConfirm) {
document.getElementById('confirmModalText').textContent = text;
const okBtn = document.getElementById('confirmModalOk');
const newOk = okBtn.cloneNode(true);
okBtn.parentNode.replaceChild(newOk, okBtn);
newOk.addEventListener('click', () => { closeConfirmModal(); onConfirm(); });
_confirmModal.classList.add('open');
}
boot(); boot();
</script> </script>
</body> </body>

View File

@@ -1,12 +1,3 @@
Umsetzung des Spiels:
Der Lockee hat eine Stunde Zeit das Spiel zu starten, dies geschieht per Knopfdruck
Wenn er dies nicht schafft -> bei Keyholder, benachrichtige Keyholder und lass sie/ihn entscheiden, ansonsten freeze wie bei freeze card
Übernimm die Logik des Spiels aus dem BDSM Game.
Falls eine Zeitstrafe eine temporäre Öffnung vor oder nach der Aufgabe benötigt, öffne das Lock für 5 Minuten. Überzogene Zeit wird addiert und am Ende des Locks gefreezed
Selbiges gilt, falls der finisher eine temporäre Öffnung danach erfoldert
Benötigt der Finisher eine Öffnung davor, verwende die Logik der Cum Card, und addiere diese Zeit auf die möglicherweise schon vorhandenen Freeze Zeit am Ende des Locks
wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen, wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen,

View File

@@ -4,12 +4,15 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter; import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter; import jakarta.persistence.Converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
@Converter @Converter
public class StringListConverter implements AttributeConverter<List<String>, String> { public class StringListConverter implements AttributeConverter<List<String>, String> {
private static final Logger LOGGER = LoggerFactory.getLogger(StringListConverter.class);
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
@Override @Override
@@ -18,6 +21,7 @@ public class StringListConverter implements AttributeConverter<List<String>, Str
try { try {
return mapper.writeValueAsString(list); return mapper.writeValueAsString(list);
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Fehler beim Serialisieren der String-Liste in DB-Spalte", e);
return null; return null;
} }
} }
@@ -32,6 +36,7 @@ public class StringListConverter implements AttributeConverter<List<String>, Str
} }
return mapper.readValue(json, new TypeReference<>() {}); return mapper.readValue(json, new TypeReference<>() {});
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Fehler beim Deserialisieren der String-Liste aus DB-Spalte: {}", json, e);
return List.of(); return List.of();
} }
} }

View File

@@ -3,6 +3,8 @@ package de.oaa.xxx.dating;
import de.oaa.xxx.social.SseService; import de.oaa.xxx.social.SseService;
import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.subscription.SubscriptionLimitService;
import de.oaa.xxx.user.Geschlecht; import de.oaa.xxx.user.Geschlecht;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.user.Neigung; import de.oaa.xxx.user.Neigung;
import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
@@ -24,6 +26,8 @@ import java.util.stream.Collectors;
@RequestMapping("/dating") @RequestMapping("/dating")
public class DatingController { public class DatingController {
private static final Logger LOGGER = LoggerFactory.getLogger(DatingController.class);
private static final Set<VorliebeBewertung> POSITIVE_RATINGS = Set.of( private static final Set<VorliebeBewertung> POSITIVE_RATINGS = Set.of(
VorliebeBewertung.MAG_ICH, VorliebeBewertung.UNBEDINGT, VorliebeBewertung.WILL_AUSPROBIEREN); VorliebeBewertung.MAG_ICH, VorliebeBewertung.UNBEDINGT, VorliebeBewertung.WILL_AUSPROBIEREN);
@@ -254,7 +258,10 @@ public class DatingController {
return values.stream() return values.stream()
.map(s -> { .map(s -> {
try { return Enum.valueOf(enumClass, s); } try { return Enum.valueOf(enumClass, s); }
catch (IllegalArgumentException e) { return null; } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Enum-Wert in Dating: {}", s);
return null;
}
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.toList(); .toList();

View File

@@ -4,6 +4,8 @@ import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.social.entity.MessageCause; import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.repository.BlockRepository; import de.oaa.xxx.social.repository.BlockRepository;
import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.subscription.SubscriptionLimitService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.oaa.xxx.user.Geschlecht; import de.oaa.xxx.user.Geschlecht;
import de.oaa.xxx.user.Neigung; import de.oaa.xxx.user.Neigung;
import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserEntity;
@@ -24,6 +26,8 @@ import java.util.stream.Collectors;
@RequestMapping("/dating/dates") @RequestMapping("/dating/dates")
public class DatingDateController { public class DatingDateController {
private static final Logger LOGGER = LoggerFactory.getLogger(DatingDateController.class);
private static final int MAX_DATES_STANDARD = 1; private static final int MAX_DATES_STANDARD = 1;
private static final int MAX_DATES_PRO = 5; private static final int MAX_DATES_PRO = 5;
@@ -388,7 +392,10 @@ public class DatingDateController {
return values.stream() return values.stream()
.map(s -> { .map(s -> {
try { return Enum.valueOf(enumClass, s); } try { return Enum.valueOf(enumClass, s); }
catch (IllegalArgumentException e) { return null; } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Enum-Wert in DatingDate: {}", s);
return null;
}
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.toList(); .toList();

View File

@@ -102,6 +102,7 @@ public class EmailChangeController {
try { try {
tokenId = UUID.fromString(token); tokenId = UUID.fromString(token);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültige E-Mail-Adresse beim Ändern: {}", e.getMessage());
response.sendRedirect("/login.html"); response.sendRedirect("/login.html");
return; return;
} }

View File

@@ -147,6 +147,7 @@ public class FeedController extends BaseController {
try { try {
typ = BeitragTyp.valueOf(req.beitragTyp()); typ = BeitragTyp.valueOf(req.beitragTyp());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Ungültiger BeitragTyp: {}", req.beitragTyp());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
@@ -202,6 +203,7 @@ public class FeedController extends BaseController {
try { try {
typ = BeitragTyp.valueOf(req.beitragTyp()); typ = BeitragTyp.valueOf(req.beitragTyp());
} catch (Exception e) { } catch (Exception e) {
LOGGER.warn("Ungültiger BeitragTyp: {}", req.beitragTyp());
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }

View File

@@ -4,6 +4,8 @@ import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService; import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.support.SupportUserService; import de.oaa.xxx.support.SupportUserService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -20,6 +22,8 @@ import java.util.concurrent.ConcurrentHashMap;
@RequestMapping("/api/feedback") @RequestMapping("/api/feedback")
public class FeedbackController { public class FeedbackController {
private static final Logger LOGGER = LoggerFactory.getLogger(FeedbackController.class);
private final MailService mailService; private final MailService mailService;
private final FeedbackRepository feedbackRepository; private final FeedbackRepository feedbackRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
@@ -104,7 +108,7 @@ public class FeedbackController {
); );
mailService.send(email); mailService.send(email);
} catch (Exception e) { } catch (Exception e) {
// Mail-Server nicht erreichbar Eintrag ist bereits gespeichert LOGGER.warn("Mail-Server nicht erreichbar Feedback-Eintrag gespeichert, E-Mail nicht versandt", e);
} }
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();

View File

@@ -127,12 +127,12 @@ public class BdsmGameService {
newLock.setLockee(lockeeUserId); newLock.setLockee(lockeeUserId);
newLock.setKeyholder(keyholderUserId); newLock.setKeyholder(keyholderUserId);
newLock.setInitialCards(template.getInitialCards()); newLock.setInitialCards(template.getInitialCards());
newLock.setPickEveryMinute(template.getPickEveryMinute()); newLock.setPickEverySeconds(template.getPickEverySeconds());
newLock.setAccumulatePicks(template.isAccumulatePicks()); newLock.setAccumulatePicks(template.isAccumulatePicks());
newLock.setShowRemainingCards(template.isShowRemainingCards()); newLock.setShowRemainingCards(template.isShowRemainingCards());
newLock.setLatestOpeningtime(template.getLatestOpeningtime()); newLock.setLatestOpeningtime(template.getLatestOpeningtime());
newLock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes()); newLock.setHygineOpeningDurationSeconds(template.getHygineOpeningDurationSeconds());
newLock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites()); newLock.setHygineOpeningEverySeconds(template.getHygineOpeningEverySeconds());
newLock.setTasks(template.getTasks()); newLock.setTasks(template.getTasks());
newLock.setRequiresVerification(template.isRequiresVerification()); newLock.setRequiresVerification(template.isRequiresVerification());
newLock.setTestLock(false); newLock.setTestLock(false);
@@ -149,10 +149,10 @@ public class BdsmGameService {
newLock.setAvailableCards(template.getInitialCards() != null newLock.setAvailableCards(template.getInitialCards() != null
? new ArrayList<>(template.getInitialCards()) : new ArrayList<>()); ? new ArrayList<>(template.getInitialCards()) : new ArrayList<>());
newLock.setOpenPicks(0); newLock.setOpenPicks(0);
if (template.getPickEveryMinute() != null) { if (template.getPickEverySeconds() != null) {
newLock.setNextCardIn(now.plusMinutes(template.getPickEveryMinute())); newLock.setNextCardIn(now.plusSeconds(template.getPickEverySeconds()));
} }
if (template.getHygineOpeningEveryMinites() != null) { if (template.getHygineOpeningEverySeconds() != null) {
newLock.setLastHygineOpening(now); newLock.setLastHygineOpening(now);
} }
cardlockRepository.save(newLock); cardlockRepository.save(newLock);

View File

@@ -103,6 +103,28 @@ public class AufgabeController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PostMapping("/copy/{aufgabeId}")
public ResponseEntity<Void> copy(@PathVariable("aufgabeId") UUID aufgabeId, Principal principal) {
AufgabeEntity source = aufgabeRepository.findById(aufgabeId).orElse(null);
if (source == null) return ResponseEntity.notFound().build();
AufgabenGruppeEntity gruppe = source.getAufgabenGruppe();
int limit = limitService.maxTasksPerGroup(userService.requireUser(principal).getUserId());
if (gruppe.getAufgaben().size() >= limit) return ResponseEntity.status(409).build();
AufgabeEntity copy = new AufgabeEntity();
copy.setAufgabeId(UUID.randomUUID());
copy.setAufgabenGruppe(gruppe);
copy.setKurzText(source.getKurzText() + " (Kopie)");
copy.setText(source.getText());
copy.setLevel(source.getLevel());
copy.setSekundenVon(source.getSekundenVon());
copy.setSekundenBis(source.getSekundenBis());
copy.setBenoetigtAktiv(source.getBenoetigtAktiv());
copy.setBenoetigtPassiv(source.getBenoetigtPassiv());
copy.setBenoetigteToys(new ArrayList<>(source.getBenoetigteToys() != null ? source.getBenoetigteToys() : List.of()));
aufgabeRepository.save(copy);
return ResponseEntity.ok().build();
}
@DeleteMapping @DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Aufgabe aufgabe) { public ResponseEntity<Void> delete(@RequestBody Aufgabe aufgabe) {
try { try {

View File

@@ -199,8 +199,10 @@ public class AufgabenGruppeController {
aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId()); aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId());
return ResponseEntity.status(201).build(); return ResponseEntity.status(201).build();
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
LOGGER.warn("Konflikt beim Speichern der AufgabenGruppe: {}", e.getMessage());
return ResponseEntity.status(409).build(); return ResponseEntity.status(409).build();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOGGER.warn("Ungültiger Wert in AufgabenGruppe: {}", e.getMessage());
String msg = e.getMessage(); String msg = e.getMessage();
if (msg != null && msg.contains("nicht gefunden")) return ResponseEntity.notFound().build(); if (msg != null && msg.contains("nicht gefunden")) return ResponseEntity.notFound().build();
return ResponseEntity.status(403).build(); return ResponseEntity.status(403).build();

Some files were not shown because too many files have changed in this diff Show More