Weiter am Taskgame gebastelt
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-05-02 23:10:41 +02:00
parent c472093f62
commit ca0e933d95
76 changed files with 987 additions and 288 deletions

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;
} }
} }
@@ -1666,11 +1702,12 @@
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; } if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
payload = { payload = {
kurzText, text, kurzText, text,
geschlecht: geschlecht || null, geschlecht: geschlecht || null,
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

@@ -53,6 +53,48 @@
height: 2.75rem; height: 2.75rem;
} }
.game-requirements {
margin: 0.75rem 0 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.game-requirements-label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--color-muted);
margin-bottom: 0.1rem;
}
.req-check {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.4rem 0.6rem;
border-radius: 7px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--color-secondary);
cursor: pointer;
user-select: none;
transition: border-color 0.15s, background 0.15s;
font-size: 0.9rem;
color: var(--color-text);
}
.req-check input[type="checkbox"] {
accent-color: var(--color-primary);
width: 15px;
height: 15px;
flex-shrink: 0;
cursor: pointer;
}
.req-check.done {
border-color: var(--color-primary);
background: rgba(233,69,96,0.07);
color: var(--color-muted);
text-decoration: line-through;
}
.level-display { .level-display {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -190,6 +232,7 @@
<div id="gameCard" class="game-card" style="display:none;"> <div id="gameCard" class="game-card" style="display:none;">
<div class="game-label" id="gameLabel"></div> <div class="game-label" id="gameLabel"></div>
<div class="game-text" id="gameText"></div> <div class="game-text" id="gameText"></div>
<div id="gameRequirements" class="game-requirements" style="display:none;"></div>
<div class="game-timer" id="gameTimer"></div> <div class="game-timer" id="gameTimer"></div>
<div class="game-btn-row"> <div class="game-btn-row">
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button> <button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
@@ -205,13 +248,32 @@
</div> </div>
</div> </div>
<!-- Temporäre Öffnung -->
<div id="tempOpeningBox" class="game-card" style="display:none;">
<div class="game-label">🔓 Temporäre Öffnung erforderlich</div>
<div class="game-text" id="tempOpeningTask"></div>
<div id="tempOpeningCodeRow" style="display:none; margin-top:1rem; text-align:center;">
<div class="game-label">Entsperrcode</div>
<div id="tempOpeningCode" style="font-size:1.8rem; font-weight:700; letter-spacing:0.18em; padding:0.6rem 0; color:var(--color-primary);"></div>
</div>
<div class="game-btn-row">
<button class="btn-primary" onclick="doEndTempOpening()">✓ Erledigt</button>
</div>
</div>
<!-- Finisher --> <!-- Finisher -->
<div id="finisherBox" style="display:none;"> <div id="finisherBox" style="display:none;">
<div class="trophy">🏆</div> <div class="trophy">🏆</div>
<h2>Level 6 erreicht!</h2> <h2>Level 6 erreicht!</h2>
<div class="game-label" id="finisherTitle"></div> <div class="game-label" id="finisherTitle"></div>
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div> <div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
<button class="btn-primary" id="btnFinisherOk" style="margin-top:1.25rem;">✓ OK</button> <div id="finisherStart" style="margin-top:1.25rem;">
<button class="btn-primary" onclick="doStartFinisher()">▶ Starten</button>
</div>
<div id="finisherRunning" style="display:none;margin-top:1.25rem;">
<div class="game-timer active" id="finisherTimer">00:00</div>
<button class="btn-primary" onclick="doEndFinisher()" style="margin-top:1rem;">✓ Erledigt</button>
</div>
</div> </div>
<!-- Debug --> <!-- Debug -->
@@ -340,6 +402,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' });
@@ -417,6 +480,27 @@
await loadAndShowToys(sel.value); await loadAndShowToys(sel.value);
} }
// ── Benötigt-Checkboxen ───────────────────────────────────────────────────
const WERKZEUG_LABEL = {
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
};
function renderRequirements(list) {
const box = document.getElementById('gameRequirements');
if (!list || list.length === 0) { box.style.display = 'none'; box.innerHTML = ''; return; }
box.innerHTML = '<div class="game-requirements-label">Benötigt</div>' +
list.map(w => {
const label = WERKZEUG_LABEL[w] || w;
return `<label class="req-check" onclick="this.classList.toggle('done',this.querySelector('input').checked)">
<input type="checkbox" onchange="this.closest('.req-check').classList.toggle('done',this.checked)">
<span>${label}</span>
</label>`;
}).join('');
box.style.display = 'flex';
}
// ── Game Loop ───────────────────────────────────────────────────────────── // ── Game Loop ─────────────────────────────────────────────────────────────
function setGameCard(label, text, action, btnLabel) { function setGameCard(label, text, action, btnLabel) {
@@ -432,10 +516,16 @@
async function runGameLoop() { async function runGameLoop() {
hide('gameCard'); hide('gameCard');
hide('finisherBox'); hide('finisherBox');
hide('tempOpeningBox');
clearTimer(); clearTimer();
if (_state.level >= 6) { if (_state.finisher) {
await showFinisherFlow(); showFinisherUI();
return;
}
if (_state.tempOpeningTime) {
showTempOpeningDialog();
return; return;
} }
@@ -460,12 +550,14 @@
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'); setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
renderRequirements(null);
} 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');
renderRequirements(aufgabe.benoetigtAktiv);
} }
show('gameBox'); show('gameBox');
show('gameCard'); show('gameCard');
@@ -479,6 +571,7 @@
timerEl.textContent = ''; timerEl.textContent = '';
document.getElementById('gameLabel').textContent = 'Aktive Aufgabe'; document.getElementById('gameLabel').textContent = 'Aktive Aufgabe';
document.getElementById('gameText').textContent = text; document.getElementById('gameText').textContent = text;
renderRequirements(_state.activeTaskBenoetigtAktiv);
if (endIso) { if (endIso) {
const end = new Date(endIso); const end = new Date(endIso);
@@ -504,12 +597,31 @@
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 (_) {}
}
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; }
const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); if (wasLock && tempUnlockRequired) {
await runGameLoop(); await fetch('/lock-game/start-temp-opening', { method: 'POST' });
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
showTempOpeningDialog();
} else 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');
_state = await stateR.json();
await runGameLoop();
} else {
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
}
} catch (e) { showError(e.message || 'Fehler (Starten)'); } } catch (e) { showError(e.message || 'Fehler (Starten)'); }
} }
@@ -526,16 +638,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 texts = await lockR.json();
for (const text of (texts || [])) {
if (text != null && text !== '') await waitForReleaseOk(text);
}
}
_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 texts = await lockR.json();
for (const text of (texts || [])) {
if (text != null && text !== '') await waitForReleaseOk(text);
}
}
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');
@@ -557,51 +681,87 @@
} }
} }
async function showFinisherFlow() { function showTempOpeningDialog() {
show('gameBox'); show('gameBox');
hide('gameCard'); hide('gameCard');
hide('lockReleaseBox'); hide('lockReleaseBox');
hide('finisherBox'); hide('finisherBox');
// 1. Release-Texte sequenziell anzeigen document.getElementById('tempOpeningTask').textContent = _state.activeTask || '';
const code = _state.tempOpeningCode;
if (code) {
document.getElementById('tempOpeningCode').textContent = code;
show('tempOpeningCodeRow');
} else {
hide('tempOpeningCodeRow');
}
show('tempOpeningBox');
}
async function doEndTempOpening() {
try { try {
const r = await fetch('/lock-game/release-locks'); await fetch('/lock-game/end-temp-opening', { method: 'POST' });
if (r.ok) { await fetch('/lock-game/abandon-task', { method: 'POST' });
const texts = await r.json(); const stateR = await fetch('/lock-game/state');
for (const text of texts) { _state = await stateR.json();
await waitForReleaseOk(text); hide('tempOpeningBox');
} await runGameLoop();
} } catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); }
} catch (_) { /* ignorieren */ } }
// 2. Finisher laden und Zeit messen function showFinisherUI() {
const finisherStartTime = Date.now(); show('gameBox');
let finisher = null; hide('gameCard');
try { hide('lockReleaseBox');
const r = await fetch('/lock-game/finisher');
if (r.ok) finisher = await r.json();
} catch (_) { /* ignorieren */ }
document.getElementById('finisherTitle').textContent = finisher?.kurzText || ''; let finisher = {};
document.getElementById('finisherText').textContent = finisher?.text || 'Glückwunsch du hast Level 6 erreicht!'; try { finisher = JSON.parse(_state.finisher); } catch (_) {}
document.getElementById('finisherTitle').textContent = finisher.kurzText || '';
document.getElementById('finisherText').textContent = finisher.text || '';
// 3. Warten bis Nutzer OK drückt if (_state.finisherStartedAt) {
await new Promise(resolve => { hide('finisherStart');
document.getElementById('btnFinisherOk').onclick = resolve; show('finisherRunning');
show('finisherBox'); startElapsedTimer(new Date(_state.finisherStartedAt));
}); } else {
show('finisherStart');
hide('finisherRunning');
}
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' }); hide('finisherStart');
show('finisherRunning');
startElapsedTimer(new Date(_state.finisherStartedAt));
}
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');
_timerInt = setInterval(() => {
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;
}, 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();

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

@@ -538,7 +538,7 @@ public class BdsmGameController extends BaseController {
Map<String, Object> item = new LinkedHashMap<>(); Map<String, Object> item = new LinkedHashMap<>();
item.put("lockId", l.getLockId()); item.put("lockId", l.getLockId());
item.put("name", l.getName() != null ? l.getName() : "Unbenanntes Lock"); item.put("name", l.getName() != null ? l.getName() : "Unbenanntes Lock");
item.put("pickEveryMinute", l.getPickEveryMinute()); item.put("pickEveryMinute", l.getPickEverySeconds() / 60);
item.put("totalCards", l.getInitialCards() != null ? l.getInitialCards().size() : 0); item.put("totalCards", l.getInitialCards() != null ? l.getInitialCards().size() : 0);
item.put("active", l.getStartTime() != null && l.getUnlockTime() == null); item.put("active", l.getStartTime() != null && l.getUnlockTime() == null);
return item; return item;

View File

@@ -88,11 +88,32 @@ public class FinisherController {
entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv()); entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv());
entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv()); entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv());
entity.setBenoetigteToys(resolveToys(finisher.getBenoetigteToys())); entity.setBenoetigteToys(resolveToys(finisher.getBenoetigteToys()));
entity.setTempUnlockRequired(finisher.getTempUnlockRequired());
finisherRepository.save(entity); finisherRepository.save(entity);
LOGGER.debug("Finisher {} aktualisiert", finisherId); LOGGER.debug("Finisher {} aktualisiert", finisherId);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PostMapping("/copy/{finisherId}")
public ResponseEntity<Void> copy(@PathVariable("finisherId") UUID finisherId) {
FinisherEntity source = finisherRepository.findById(finisherId).orElse(null);
if (source == null) return ResponseEntity.notFound().build();
AufgabenGruppeEntity gruppe = source.getAufgabenGruppe();
if (gruppe.getFinisher().size() >= 100) return ResponseEntity.status(409).build();
FinisherEntity copy = new FinisherEntity();
copy.setFinisherId(UUID.randomUUID());
copy.setAufgabenGruppe(gruppe);
copy.setKurzText(source.getKurzText() + " (Kopie)");
copy.setText(source.getText());
copy.setGeschlecht(source.getGeschlecht());
copy.setBenoetigtAktiv(source.getBenoetigtAktiv());
copy.setBenoetigtPassiv(source.getBenoetigtPassiv());
copy.setTempUnlockRequired(source.getTempUnlockRequired());
copy.setBenoetigteToys(new ArrayList<>(source.getBenoetigteToys() != null ? source.getBenoetigteToys() : List.of()));
finisherRepository.save(copy);
return ResponseEntity.ok().build();
}
@DeleteMapping @DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Finisher finisher) { public ResponseEntity<Void> delete(@RequestBody Finisher finisher) {
try { try {

View File

@@ -56,7 +56,7 @@ public class SperreController {
@PostMapping @PostMapping
public ResponseEntity<Void> create(@RequestBody Sperre sperre) { public ResponseEntity<Void> create(@RequestBody Sperre sperre) {
if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null
|| sperre.getGruppeId() == null || sperre.getSperreFuer() == null || sperre.getSperreFuer().isEmpty()) { || sperre.getGruppeId() == null) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(sperre.getGruppeId()).orElse(null); AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(sperre.getGruppeId()).orElse(null);
@@ -77,8 +77,7 @@ public class SperreController {
@PutMapping("/{sperreId}") @PutMapping("/{sperreId}")
public ResponseEntity<Void> update(@PathVariable("sperreId") UUID sperreId, @RequestBody Sperre sperre) { public ResponseEntity<Void> update(@PathVariable("sperreId") UUID sperreId, @RequestBody Sperre sperre) {
if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null) {
|| sperre.getSperreFuer() == null || sperre.getSperreFuer().isEmpty()) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
SperreEntity entity = sperreRepository.findById(sperreId).orElse(null); SperreEntity entity = sperreRepository.findById(sperreId).orElse(null);
@@ -89,8 +88,7 @@ public class SperreController {
entity.setMinutenVon(sperre.getMinutenVon()); entity.setMinutenVon(sperre.getMinutenVon());
entity.setMinutenBis(sperre.getMinutenBis()); entity.setMinutenBis(sperre.getMinutenBis());
entity.setLevel(sperre.getLevel()); entity.setLevel(sperre.getLevel());
entity.setTempUnlockBeforeRequired(sperre.getTempUnlockBeforeRequired()); entity.setTempUnlockRequired(sperre.getTempUnlockRequired());
entity.setTempUnlockAfterRequired(sperre.getTempUnlockAfterRequired());
entity.setSperreFuer(sperre.getSperreFuer()); entity.setSperreFuer(sperre.getSperreFuer());
entity.setBenoetigteToys(resolveToys(sperre.getBenoetigteToys())); entity.setBenoetigteToys(resolveToys(sperre.getBenoetigteToys()));
sperreRepository.save(entity); sperreRepository.save(entity);
@@ -98,6 +96,28 @@ public class SperreController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PostMapping("/copy/{sperreId}")
public ResponseEntity<Void> copy(@PathVariable("sperreId") UUID sperreId) {
SperreEntity source = sperreRepository.findById(sperreId).orElse(null);
if (source == null) return ResponseEntity.notFound().build();
AufgabenGruppeEntity gruppe = source.getAufgabenGruppe();
if (gruppe.getSperren().size() >= 100) return ResponseEntity.status(409).build();
SperreEntity copy = new SperreEntity();
copy.setSperreId(UUID.randomUUID());
copy.setAufgabenGruppe(gruppe);
copy.setKurzText(source.getKurzText() + " (Kopie)");
copy.setText(source.getText());
copy.setReleaseText(source.getReleaseText());
copy.setLevel(source.getLevel());
copy.setMinutenVon(source.getMinutenVon());
copy.setMinutenBis(source.getMinutenBis());
copy.setSperreFuer(source.getSperreFuer());
copy.setTempUnlockRequired(source.getTempUnlockRequired());
copy.setBenoetigteToys(new ArrayList<>(source.getBenoetigteToys() != null ? source.getBenoetigteToys() : List.of()));
sperreRepository.save(copy);
return ResponseEntity.ok().build();
}
@DeleteMapping @DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Sperre sperre) { public ResponseEntity<Void> delete(@RequestBody Sperre sperre) {
try { try {

View File

@@ -94,6 +94,27 @@ public class StrafeController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PostMapping("/copy/{strafeId}")
public ResponseEntity<Void> copy(@PathVariable("strafeId") UUID strafeId) {
StrafeEntity source = strafeRepository.findById(strafeId).orElse(null);
if (source == null) return ResponseEntity.notFound().build();
AufgabenGruppeEntity gruppe = source.getAufgabenGruppe();
if (gruppe.getStrafen().size() >= 100) return ResponseEntity.status(409).build();
StrafeEntity copy = new StrafeEntity();
copy.setStrafeId(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()));
strafeRepository.save(copy);
return ResponseEntity.ok().build();
}
@DeleteMapping @DeleteMapping
public ResponseEntity<Void> delete(@RequestBody Strafe strafe) { public ResponseEntity<Void> delete(@RequestBody Strafe strafe) {
try { try {

View File

@@ -125,7 +125,8 @@ public class CardLockController {
List<CardEnum> initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards, List<CardEnum> initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards,
LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites, LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites,
List<Task> tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines, List<Task> tasks, boolean requiresVerification, boolean testLock, Integer unlockCodeLines,
TaskMode taskMode, LockControllType controllType, UUID gameSetId, Integer gameSpieldauerIdx) { TaskMode taskMode, LockControllType controllType, UUID gameSetId, Integer gameSpieldauerIdx,
Integer speedFactor) {
} }
private static final SecureRandom RNG = new SecureRandom(); private static final SecureRandom RNG = new SecureRandom();
@@ -166,12 +167,12 @@ public class CardLockController {
lock.setLockee(lockee.getUserId()); lock.setLockee(lockee.getUserId());
lock.setKeyholder(myId); lock.setKeyholder(myId);
lock.setInitialCards(req.initialCards()); lock.setInitialCards(req.initialCards());
lock.setPickEveryMinute(req.pickEveryMinute()); lock.setPickEverySeconds(req.pickEveryMinute() * 60);
lock.setAccumulatePicks(req.accumulatePicks()); lock.setAccumulatePicks(req.accumulatePicks());
lock.setShowRemainingCards(req.showRemainingCards()); lock.setShowRemainingCards(req.showRemainingCards());
lock.setLatestOpeningtime(req.latestOpeningtime()); lock.setLatestOpeningtime(req.latestOpeningtime());
lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); lock.setHygineOpeningDurationSeconds(req.hygineOpeningDurationMinutes() != null ? req.hygineOpeningDurationMinutes() * 60 : null);
lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites()); lock.setHygineOpeningEverySeconds(req.hygineOpeningEveryMinites() != null ? req.hygineOpeningEveryMinites() * 60 : null);
lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
lock.setRequiresVerification(req.requiresVerification()); lock.setRequiresVerification(req.requiresVerification());
lock.setTestLock(false); lock.setTestLock(false);
@@ -206,21 +207,23 @@ public class CardLockController {
} }
int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) ? req.unlockCodeLines() : 5; int codeLines = (req.unlockCodeLines() != null && req.unlockCodeLines() >= 1) ? req.unlockCodeLines() : 5;
int sf = (req.testLock() && req.speedFactor() != null && req.speedFactor() > 1) ? req.speedFactor() : 1;
CardLockEntity lock = new CardLockEntity(); CardLockEntity lock = new CardLockEntity();
lock.setName(req.name()); lock.setName(req.name());
lock.setLockee(myId); lock.setLockee(myId);
lock.setKeyholder(null); // set only after invitation is confirmed lock.setKeyholder(null); // set only after invitation is confirmed
lock.setInitialCards(req.initialCards()); lock.setInitialCards(req.initialCards());
lock.setPickEveryMinute(req.pickEveryMinute()); lock.setPickEverySeconds(req.pickEveryMinute() * 60);
lock.setAccumulatePicks(req.accumulatePicks()); lock.setAccumulatePicks(req.accumulatePicks());
lock.setShowRemainingCards(req.showRemainingCards()); lock.setShowRemainingCards(req.showRemainingCards());
lock.setLatestOpeningtime(req.latestOpeningtime()); lock.setLatestOpeningtime(req.latestOpeningtime());
lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); lock.setHygineOpeningDurationSeconds(req.hygineOpeningDurationMinutes() != null ? req.hygineOpeningDurationMinutes() * 60 : null);
lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites()); lock.setHygineOpeningEverySeconds(req.hygineOpeningEveryMinites() != null ? req.hygineOpeningEveryMinites() * 60 : null);
lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); lock.setTasks(req.tasks() != null ? req.tasks() : List.of());
lock.setRequiresVerification(req.requiresVerification()); lock.setRequiresVerification(req.requiresVerification());
lock.setTestLock(req.testLock()); lock.setTestLock(req.testLock());
lock.setSpeedFactor(sf > 1 ? sf : null);
lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM); lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM);
lock.setUnlockCodeLength(codeLines); lock.setUnlockCodeLength(codeLines);
lock.setControllType(controllType); lock.setControllType(controllType);
@@ -231,8 +234,9 @@ public class CardLockController {
lock.setStartTime(now); lock.setStartTime(now);
lock.setAvailableCards(new ArrayList<>(req.initialCards())); lock.setAvailableCards(new ArrayList<>(req.initialCards()));
lock.setOpenPicks(0); lock.setOpenPicks(0);
lock.setNextCardIn(now.plusMinutes(req.pickEveryMinute())); long firstCardSeconds = sf > 1 ? Math.max(6L, req.pickEveryMinute() * 60L / sf) : req.pickEveryMinute() * 60L;
if (req.hygineOpeningEveryMinites() != null) { lock.setNextCardIn(now.plusSeconds(firstCardSeconds));
if (req.hygineOpeningEveryMinites() != null) { // stored as seconds already above
lock.setLastHygineOpening(now); lock.setLastHygineOpening(now);
} }
cardlockRepository.save(lock); // erst speichern, damit Lock-ID vorhanden ist cardlockRepository.save(lock); // erst speichern, damit Lock-ID vorhanden ist
@@ -243,7 +247,7 @@ public class CardLockController {
"🃏 Deine erste Karte ist bereit jetzt ziehen!", "🃏 Deine erste Karte ist bereit jetzt ziehen!",
"/games/chastity/activelock.html?lockId=" + lock.getLockId(), "/games/chastity/activelock.html?lockId=" + lock.getLockId(),
de.oaa.xxx.social.entity.MessageCause.GAME_STATE, de.oaa.xxx.social.entity.MessageCause.GAME_STATE,
now.plusMinutes(req.pickEveryMinute())); now.plusSeconds(firstCardSeconds));
// Initialen Unlock-Code / TTLock-PIN via LockControl setzen // Initialen Unlock-Code / TTLock-PIN via LockControl setzen
CardLockService initService = cardLockServiceFactory.create(lock); CardLockService initService = cardLockServiceFactory.create(lock);
@@ -307,9 +311,10 @@ public class CardLockController {
result.put("taskPending", taskPending); result.put("taskPending", taskPending);
// Nächste Karte: geplante Benachrichtigung anlegen (echte nextCardIn aus Entity) // Nächste Karte: geplante Benachrichtigung anlegen (echte nextCardIn aus Entity)
int drawSf = (l.getSpeedFactor() != null && l.getSpeedFactor() > 1) ? l.getSpeedFactor() : 1;
LocalDateTime nextCard = l.getNextCardIn() != null LocalDateTime nextCard = l.getNextCardIn() != null
? l.getNextCardIn() ? l.getNextCardIn()
: LocalDateTime.now().plusMinutes(l.getPickEveryMinute()); : LocalDateTime.now().plusSeconds(drawSf > 1 ? Math.max(6L, l.getPickEverySeconds() / drawSf) : l.getPickEverySeconds());
systemMessageService.sendScheduled( systemMessageService.sendScheduled(
myId, myId, myId, myId,
"🃏 Deine nächste Karte ist bereit jetzt ziehen!", "🃏 Deine nächste Karte ist bereit jetzt ziehen!",
@@ -345,7 +350,7 @@ public class CardLockController {
cardLockServiceFactory.create(l).startHygieneOpening(); cardLockServiceFactory.create(l).startHygieneOpening();
int actualDuration = l.getTempOpeningDuration() != null ? l.getTempOpeningDuration() int actualDuration = l.getTempOpeningDuration() != null ? l.getTempOpeningDuration()
: (l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 30); : (l.getHygineOpeningDurationSeconds() != null ? l.getHygineOpeningDurationSeconds() / 60 : 30);
return ResponseEntity.ok(Map.of("unlockCode", l.getUnlockCode(), "durationMinutes", actualDuration)); return ResponseEntity.ok(Map.of("unlockCode", l.getUnlockCode(), "durationMinutes", actualDuration));
} }
@@ -471,13 +476,15 @@ public class CardLockController {
long totalCards = l.getAvailableCards() != null ? l.getAvailableCards().size() : 0; long totalCards = l.getAvailableCards() != null ? l.getAvailableCards().size() : 0;
// Hygiene-Berechnung // Hygiene-Berechnung
boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; boolean hygieneEnabled = l.getHygineOpeningEverySeconds() != null;
boolean hygieneOpeningDue = false; boolean hygieneOpeningDue = false;
long hygieneSecondsRemaining = 0; long hygieneSecondsRemaining = 0;
if (hygieneEnabled) { if (hygieneEnabled) {
int lockSf = (l.getSpeedFactor() != null && l.getSpeedFactor() > 1) ? l.getSpeedFactor() : 1;
long hygineSeconds = lockSf > 1 ? Math.max(6L, l.getHygineOpeningEverySeconds() / lockSf) : l.getHygineOpeningEverySeconds();
LocalDateTime base = l.getLastHygineOpening() != null ? l.getLastHygineOpening() : l.getStartTime(); LocalDateTime base = l.getLastHygineOpening() != null ? l.getLastHygineOpening() : l.getStartTime();
if (base != null) { if (base != null) {
LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites()); LocalDateTime nextHygiene = base.plusSeconds(hygineSeconds);
hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene); hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene);
hygieneOpeningDue = hygieneSecondsRemaining <= 0; hygieneOpeningDue = hygieneSecondsRemaining <= 0;
} }
@@ -507,7 +514,7 @@ public class CardLockController {
result.put("hygieneOpeningStarted", result.put("hygieneOpeningStarted",
l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null); l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null);
result.put("hygieneDurationMinutes", result.put("hygieneDurationMinutes",
l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0); l.getHygineOpeningDurationSeconds() != null ? l.getHygineOpeningDurationSeconds() / 60 : 0);
result.put("hasKeyholder", l.getKeyholder() != null); result.put("hasKeyholder", l.getKeyholder() != null);
result.put("keyholderInvitationPending", result.put("keyholderInvitationPending",
l.getKeyholder() == null && !invitationRepository.findByLockId(l.getLockId()).isEmpty()); l.getKeyholder() == null && !invitationRepository.findByLockId(l.getLockId()).isEmpty());
@@ -680,7 +687,9 @@ public class CardLockController {
UUID gameSetId = l.getGameSetId(); UUID gameSetId = l.getGameSetId();
l.setGameCardParkedAt(null); l.setGameCardParkedAt(null);
l.setFrozenUntil(null); l.setFrozenUntil(null);
l.setNextCardIn(LocalDateTime.now().plusMinutes(l.getPickEveryMinute() != null ? l.getPickEveryMinute() : 60)); int pickSecs = l.getPickEverySeconds() != null ? l.getPickEverySeconds() : 3600;
int gameSf = (l.getSpeedFactor() != null && l.getSpeedFactor() > 1) ? l.getSpeedFactor() : 1;
l.setNextCardIn(LocalDateTime.now().plusSeconds(gameSf > 1 ? Math.max(6L, pickSecs / gameSf) : pickSecs));
l.setGameActive(true); l.setGameActive(true);
cardlockRepository.save(l); cardlockRepository.save(l);
@@ -976,13 +985,13 @@ public class CardLockController {
l.getAvailableCards().forEach(c -> cardCounts.merge(c.name(), 1L, Long::sum)); l.getAvailableCards().forEach(c -> cardCounts.merge(c.name(), 1L, Long::sum));
} }
boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; boolean hygieneEnabled = l.getHygineOpeningEverySeconds() != null;
boolean hygieneOpeningDue = false; boolean hygieneOpeningDue = false;
long hygieneSecondsRemaining = 0; long hygieneSecondsRemaining = 0;
if (hygieneEnabled) { if (hygieneEnabled) {
LocalDateTime base = l.getLastHygineOpening() != null ? l.getLastHygineOpening() : l.getStartTime(); LocalDateTime base = l.getLastHygineOpening() != null ? l.getLastHygineOpening() : l.getStartTime();
if (base != null) { if (base != null) {
LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites()); LocalDateTime nextHygiene = base.plusSeconds(l.getHygineOpeningEverySeconds());
hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene); hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene);
hygieneOpeningDue = hygieneSecondsRemaining <= 0; hygieneOpeningDue = hygieneSecondsRemaining <= 0;
} }

View File

@@ -24,7 +24,7 @@ public class CardLockEntity extends BaseLockEntity {
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private List<CardEnum> initialCards; private List<CardEnum> initialCards;
@Column @Column
private Integer pickEveryMinute; private Integer pickEverySeconds;
@Column @Column
private boolean accumulatePicks; private boolean accumulatePicks;
@Column @Column

View File

@@ -98,12 +98,12 @@ public class CardLockService extends BaseLockService implements LockControlCallb
@Override @Override
protected void applyHygieneOvertime(Long overtime) { protected void applyHygieneOvertime(Long overtime) {
long penalty = Math.round(overtime * 4 * getTimeMultiplier()); long penaltySeconds = Math.round(overtime * 4 * 60.0 * getTimeMultiplier());
LOGGER.debug("Apply {} Minutes Overtime (penalty: {})", overtime, penalty); LOGGER.debug("Apply {} Minutes Overtime (penalty: {} seconds)", overtime, penaltySeconds);
if (lock.getFrozenUntil() != null) { if (lock.getFrozenUntil() != null) {
lock.setFrozenUntil(lock.getFrozenUntil().plusMinutes(penalty)); lock.setFrozenUntil(lock.getFrozenUntil().plusSeconds(penaltySeconds));
} else { } else {
lock.setFrozenUntil(LocalDateTime.now().plusMinutes(penalty)); lock.setFrozenUntil(LocalDateTime.now().plusSeconds(penaltySeconds));
} }
LOGGER.debug("Frozen until {}", lock.getFrozenUntil()); LOGGER.debug("Frozen until {}", lock.getFrozenUntil());
} }
@@ -128,7 +128,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
} }
} else { } else {
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) { if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
lock.setNextCardIn(LocalDateTime.now().plusMinutes(Math.round(lock.getPickEveryMinute() * getTimeMultiplier()))); lock.setNextCardIn(LocalDateTime.now().plusSeconds(Math.round(lock.getPickEverySeconds() * 1.0 * getTimeMultiplier())));
card = getRandomCard(); card = getRandomCard();
} }
} }
@@ -175,16 +175,16 @@ public class CardLockService extends BaseLockService implements LockControlCallb
} }
public String freeze() { public String freeze() {
var multiplier = lock.getPickEveryMinute() * new Random().nextDouble(1.0, 4.0) * getTimeMultiplier(); var multiplier = lock.getPickEverySeconds() * 1.0 * new Random().nextDouble(1.0, 4.0) * getTimeMultiplier();
freeze(multiplier); freeze(multiplier);
return ""; return "";
} }
private String freeze(double multiplier) { private String freeze(double seconds) {
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES); LocalDateTime frozenTill = LocalDateTime.now().plus((long) seconds, ChronoUnit.SECONDS);
lock.setFrozenUntil(frozenTill); lock.setFrozenUntil(frozenTill);
lock.setNextCardIn(frozenTill); lock.setNextCardIn(frozenTill);
LOGGER.info("[CardLock {}] FREEZE: eingefroren für {} Minuten (bis {})", lock.getLockee(), (long) multiplier, frozenTill); LOGGER.info("[CardLock {}] FREEZE: eingefroren für {} Sekunden (bis {})", lock.getLockee(), (long) seconds, frozenTill);
return ""; return "";
} }
@@ -247,9 +247,9 @@ public class CardLockService extends BaseLockService implements LockControlCallb
} }
public void startHygieneOpening() { public void startHygieneOpening() {
int base = lock.getHygineOpeningDurationMinutes() != null ? lock.getHygineOpeningDurationMinutes() : 30; int baseSecs = lock.getHygineOpeningDurationSeconds() != null ? lock.getHygineOpeningDurationSeconds() : (30 * 60);
int duration = (int) Math.round(base * getTimeMultiplier()); int durationMinutes = (int) Math.round(baseSecs / 60.0 * getTimeMultiplier());
startTempOpening(TempOpeningReason.HYGIENE, duration); startTempOpening(TempOpeningReason.HYGIENE, durationMinutes);
} }
// ── Cum cards ───────────────────────────────────────────────────────────── // ── Cum cards ─────────────────────────────────────────────────────────────
@@ -313,23 +313,24 @@ public class CardLockService extends BaseLockService implements LockControlCallb
private double getTimeMultiplier() { private double getTimeMultiplier() {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
double sf = (lock.getSpeedFactor() != null && lock.getSpeedFactor() > 1) ? lock.getSpeedFactor() : 1.0;
if (lock.getSpeedupUntil() != null && lock.getSpeedupUntil().isAfter(now)) { if (lock.getSpeedupUntil() != null && lock.getSpeedupUntil().isAfter(now)) {
return 0.25; return 0.25 / sf;
} }
if (lock.getSlowmoUntil() != null && lock.getSlowmoUntil().isAfter(now)) { if (lock.getSlowmoUntil() != null && lock.getSlowmoUntil().isAfter(now)) {
return 4.0; return 4.0 / sf;
} }
return 1.0; return 1.0 / sf;
} }
@Override @Override
protected void handleLockGameFinished(int timeInMinutes) { protected void handleLockGameFinished(int timeInSeconds) {
int freezeTime = (int) (timeInMinutes * new Random().nextDouble(1.0, 4.0)); double freezeSeconds = timeInSeconds * new Random().nextDouble(1.0, 4.0);
freeze(freezeTime); freeze(freezeSeconds);
} }
@Override @Override
public void penaltyLockGame() { public void penaltyLockGame() {
handleLockGameFinished(60); handleLockGameFinished(3600);
} }
} }

View File

@@ -64,9 +64,9 @@ public class BaseLockEntity {
@Column @Column
private LocalDateTime lastHygineOpening; private LocalDateTime lastHygineOpening;
@Column @Column
private Integer hygineOpeningDurationMinutes; private Integer hygineOpeningDurationSeconds;
@Column @Column
private Integer hygineOpeningEveryMinites; private Integer hygineOpeningEverySeconds;
@Column @Column
private LocalDateTime tempOpeningTime; // If null, not while hygine opening private LocalDateTime tempOpeningTime; // If null, not while hygine opening
@Column @Column
@@ -90,6 +90,9 @@ public class BaseLockEntity {
@Column(nullable = false) @Column(nullable = false)
private TaskMode taskMode = TaskMode.RANDOM; private TaskMode taskMode = TaskMode.RANDOM;
@Column
private Integer speedFactor;
// --- Notfall- & Keyholder-Status --- // --- Notfall- & Keyholder-Status ---
@Column(nullable = false) @Column(nullable = false)
private boolean keyholderRequestedUnlock = false; private boolean keyholderRequestedUnlock = false;

View File

@@ -0,0 +1,20 @@
package de.oaa.xxx.games.chastity.common;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public class BaseLockHelper {
private BaseLockHelper() {}
public static Long calcOvertime(BaseLockEntity lock) {
LocalDateTime now = LocalDateTime.now();
if (lock.getTempOpeningTime() != null && lock.getTempOpeningDuration() != null) {
LocalDateTime dueTime = lock.getTempOpeningTime().plusMinutes(lock.getTempOpeningDuration());
if (now.isAfter(dueTime)) {
return ChronoUnit.MINUTES.between(dueTime, now);
}
}
return null;
}
}

View File

@@ -3,7 +3,6 @@ package de.oaa.xxx.games.chastity.common;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@@ -17,13 +16,13 @@ import de.oaa.xxx.games.chastity.community.CommunityTaskVoteEntity;
import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository; import de.oaa.xxx.games.chastity.community.CommunityTaskVoteRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationRepository;
import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository; import de.oaa.xxx.games.chastity.community.CommunityVerificationVoteRepository;
import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationEntity;
import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderNotificationRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity;
import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository;
import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository;
import de.oaa.xxx.games.chastity.tasks.Task; import de.oaa.xxx.games.chastity.tasks.Task;
import de.oaa.xxx.games.chastity.timelock.TimeLockRepository;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason; import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService; import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.games.history.GameHistoryEntity; import de.oaa.xxx.games.history.GameHistoryEntity;
@@ -70,7 +69,7 @@ public abstract class BaseLockService {
/** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */ /** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */
protected void afterHygieneClosing() {} protected void afterHygieneClosing() {}
protected abstract void handleLockGameFinished(int timeInMinutes); protected abstract void handleLockGameFinished(int timeInSeconds);
public abstract void penaltyLockGame(); public abstract void penaltyLockGame();
@@ -111,18 +110,6 @@ public abstract class BaseLockService {
// ── Gemeinsame Hilfsmethoden ────────────────────────────────────────────── // ── Gemeinsame Hilfsmethoden ──────────────────────────────────────────────
protected Long calcOvertime() {
LocalDateTime now = LocalDateTime.now();
BaseLockEntity lock = getLock();
if (lock.getTempOpeningTime() != null && lock.getTempOpeningDuration() != null) {
LocalDateTime dueTime = lock.getTempOpeningTime().plusMinutes(lock.getTempOpeningDuration());
if (now.isAfter(dueTime)) {
return ChronoUnit.MINUTES.between(dueTime, now);
}
}
return null;
}
protected void reportKeyholder(Long overtime) { protected void reportKeyholder(Long overtime) {
BaseLockEntity lock = getLock(); BaseLockEntity lock = getLock();
KeyholderNotificationEntity notification = new KeyholderNotificationEntity(); KeyholderNotificationEntity notification = new KeyholderNotificationEntity();
@@ -147,9 +134,9 @@ public abstract class BaseLockService {
// ── Lock-Game Abschluss ─────────────────────────────────────────────────── // ── Lock-Game Abschluss ───────────────────────────────────────────────────
public void lockGameFinished(int timeInMinutes) { public void lockGameFinished(int timeInSeconds) {
LOGGER.info("[Lock {}] lockGameFinished nach {} Minuten", getLock().getLockee(), timeInMinutes); LOGGER.info("[Lock {}] lockGameFinished nach {} Sekunden Overtime", getLock().getLockee(), timeInSeconds);
handleLockGameFinished(timeInMinutes); handleLockGameFinished(timeInSeconds);
} }
@@ -227,7 +214,7 @@ public abstract class BaseLockService {
public String endTempOpening() { public String endTempOpening() {
var lock = getLock(); var lock = getLock();
var now = LocalDateTime.now(); var now = LocalDateTime.now();
var overtime = calcOvertime(); var overtime = BaseLockHelper.calcOvertime(lock);
if (overtime != null) { if (overtime != null) {
if (lock.getKeyholder() != null) { if (lock.getKeyholder() != null) {
reportKeyholder(overtime); reportKeyholder(overtime);

View File

@@ -1,15 +1,22 @@
package de.oaa.xxx.games.chastity.common; package de.oaa.xxx.games.chastity.common;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public record GameState( public record GameState(
UUID gameID, UUID gameID,
UUID userId, UUID userId,
Integer level, Integer level,
String activeTask, String activeTask,
LocalDateTime activeTaskEnd, LocalDateTime activeTaskEnd,
List<String> activeTaskBenoetigtAktiv,
String taskInQueue, String taskInQueue,
String lockInQueue) { String lockInQueue,
LocalDateTime tempOpeningTime,
Integer tempOpeningDuration,
String tempOpeningCode,
LocalDateTime finisherStartedAt,
String finisher) {
} }

View File

@@ -21,6 +21,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.games.chastity.cardlock.CardLockServiceFactory; import de.oaa.xxx.games.chastity.cardlock.CardLockServiceFactory;
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository; import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.games.common.aufgaben.AufgabenList; import de.oaa.xxx.games.common.aufgaben.AufgabenList;
import de.oaa.xxx.games.common.aufgaben.AvailableIn; import de.oaa.xxx.games.common.aufgaben.AvailableIn;
import de.oaa.xxx.games.common.entity.AufgabeEntity; import de.oaa.xxx.games.common.entity.AufgabeEntity;
@@ -50,6 +52,9 @@ public class LockGameController {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final CardLockServiceFactory cardLockServiceFactory; private final CardLockServiceFactory cardLockServiceFactory;
private BaseLockRepository baseLockRepository;
private UnlockCodeHistoryService unlockCodeHistoryService;
private LockControlFactory lockControlFactory;
public LockGameController(LockGameRepository lockGameRepository, public LockGameController(LockGameRepository lockGameRepository,
LockGameLockRepository lockGameLockRepository, LockGameLockRepository lockGameLockRepository,
@@ -60,7 +65,10 @@ public class LockGameController {
UserService userService, UserService userService,
ObjectMapper objectMapper, ObjectMapper objectMapper,
CardlockRepository cardlockRepository, CardlockRepository cardlockRepository,
CardLockServiceFactory cardLockServiceFactory) { CardLockServiceFactory cardLockServiceFactory,
BaseLockRepository baseLockRepository,
UnlockCodeHistoryService unlockCodeHistoryService,
LockControlFactory lockControlFactory) {
this.lockGameRepository = lockGameRepository; this.lockGameRepository = lockGameRepository;
this.lockGameLockRepository = lockGameLockRepository; this.lockGameLockRepository = lockGameLockRepository;
this.aufgabenGruppeRepository = aufgabenGruppeRepository; this.aufgabenGruppeRepository = aufgabenGruppeRepository;
@@ -71,6 +79,9 @@ public class LockGameController {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.cardLockServiceFactory = cardLockServiceFactory; this.cardLockServiceFactory = cardLockServiceFactory;
this.baseLockRepository = baseLockRepository;
this.unlockCodeHistoryService = unlockCodeHistoryService;
this.lockControlFactory = lockControlFactory;
} }
/** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */ /** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */
@@ -136,6 +147,7 @@ public class LockGameController {
@PostMapping("/init") @PostMapping("/init")
public ResponseEntity<?> init( public ResponseEntity<?> init(
@RequestParam UUID aufgabenGruppeId, @RequestParam UUID aufgabenGruppeId,
@RequestParam UUID lockId,
@RequestParam(required = false) List<UUID> excludedToyIds, @RequestParam(required = false) List<UUID> excludedToyIds,
Principal principal) { Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
@@ -180,6 +192,10 @@ public class LockGameController {
)); ));
} }
aufgaben.forEach(a -> { if (a.getBenoetigteToys() != null) a.getBenoetigteToys().forEach(t -> t.setBild(null)); });
sperren.forEach(s -> { if (s.getBenoetigteToys() != null) s.getBenoetigteToys().forEach(t -> t.setBild(null)); });
finisher.forEach(f -> { if (f.getBenoetigteToys() != null) f.getBenoetigteToys().forEach(t -> t.setBild(null)); });
AufgabenList list = new AufgabenList(); AufgabenList list = new AufgabenList();
list.setAufgaben(aufgaben); list.setAufgaben(aufgaben);
list.setSperren(sperren); list.setSperren(sperren);
@@ -196,11 +212,15 @@ public class LockGameController {
return g; return g;
}); });
game.setLockId(lockId);
game.setAufgaben(aufgabenJson); game.setAufgaben(aufgabenJson);
game.setLevel(1); game.setLevel(1);
game.setAufgabenProLevel(AUFGABEN_PRO_LEVEL); game.setAufgabenProLevel(AUFGABEN_PRO_LEVEL);
game.setAufgabenAufAktuellemLevel(0); game.setAufgabenAufAktuellemLevel(0);
game.setZeitfaktorZeitstrafen(1.0); double zeitfaktor = baseLockRepository.findById(lockId)
.map(l -> l.getSpeedFactor() != null && l.getSpeedFactor() > 1 ? 1.0 / l.getSpeedFactor() : 1.0)
.orElse(1.0);
game.setZeitfaktorZeitstrafen(zeitfaktor);
game.setSetupId(aufgabenGruppeId); game.setSetupId(aufgabenGruppeId);
game.setActiveTask(null); game.setActiveTask(null);
game.setActiveTaskEnd(null); game.setActiveTaskEnd(null);
@@ -290,17 +310,55 @@ public class LockGameController {
} }
} }
@GetMapping("/finisher") @PostMapping("/start-finisher")
public ResponseEntity<?> getFinisher(Principal principal) { public ResponseEntity<?> startFinisher(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId); var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.notFound().build(); if (opt.isEmpty()) return ResponseEntity.notFound().build();
try { try {
var finisher = buildService(opt.get()).getFinisher(); buildService(opt.get()).startFinisher();
Map<String, Object> result = new LinkedHashMap<>(); return ResponseEntity.noContent().build();
result.put("kurzText", finisher.getKurzText()); } catch (Exception e) {
result.put("text", finisher.getText()); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
return ResponseEntity.ok(result); }
}
@PostMapping("/end-finisher")
public ResponseEntity<?> endFinisher(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.notFound().build();
try {
buildService(opt.get()).endFinisher();
return ResponseEntity.noContent().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
}
}
@Transactional
@PostMapping("/start-temp-opening")
public ResponseEntity<?> startTempOpening(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.notFound().build();
try {
buildService(opt.get()).startTempOpening();
return ResponseEntity.noContent().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
}
}
@Transactional
@PostMapping("/end-temp-opening")
public ResponseEntity<?> endTempOpening(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.notFound().build();
try {
buildService(opt.get()).endTempOpening();
return ResponseEntity.noContent().build();
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
} }
@@ -324,23 +382,20 @@ public class LockGameController {
@PostMapping("/complete") @PostMapping("/complete")
public ResponseEntity<?> completeGame( public ResponseEntity<?> completeGame(
@RequestParam(required = false) UUID lockId, @RequestParam(required = false) UUID lockId,
@RequestParam(required = false, defaultValue = "0") int timeInMinutes,
Principal principal) { Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId); var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.notFound().build(); if (opt.isEmpty()) return ResponseEntity.notFound().build();
LockGameEntity game = opt.get(); LockGameEntity game = opt.get();
int overtimeInSeconds = game.getOvertimeInSeconds() != null ? game.getOvertimeInSeconds() : 0;
lockGameLockRepository.deleteAll(lockGameLockRepository.findByGameId(game.getGameId())); lockGameLockRepository.deleteAll(lockGameLockRepository.findByGameId(game.getGameId()));
lockGameRepository.delete(game); lockGameRepository.delete(game);
if (lockId != null) { if (lockId != null) {
cardlockRepository.findById(lockId).ifPresent(l -> { cardlockRepository.findById(lockId).ifPresent(l -> {
if (l.getLockee().equals(userId)) { if (l.getLockee().equals(userId)) {
cardLockServiceFactory.create(l).lockGameFinished(timeInMinutes); cardLockServiceFactory.create(l).lockGameFinished(overtimeInSeconds);
l.setGameActive(false); l.setGameActive(false);
l.setFrozenUntil(null);
l.setNextCardIn(LocalDateTime.now()
.plusMinutes(l.getPickEveryMinute() != null ? l.getPickEveryMinute() : 60));
cardlockRepository.save(l); cardlockRepository.save(l);
} }
}); });
@@ -349,6 +404,6 @@ public class LockGameController {
} }
private LockGameService buildService(LockGameEntity entity) throws Exception { private LockGameService buildService(LockGameEntity entity) throws Exception {
return new LockGameService(entity, lockGameRepository, lockGameLockRepository); return new LockGameService(entity, lockGameRepository, lockGameLockRepository, baseLockRepository, unlockCodeHistoryService, lockControlFactory);
} }
} }

View File

@@ -18,13 +18,15 @@ import lombok.Setter;
@Setter @Setter
@Entity @Entity
@Table(name = "lock_game") @Table(name = "lock_game")
public class LockGameEntity {
public class LockGameEntity {
@Id @Id
@Column @Column
private UUID gameId; private UUID gameId;
@Column(unique = true) @Column(unique = true)
private UUID userId; private UUID userId;
@Column
private UUID lockId;
@OneToMany(mappedBy = "gameId", fetch = FetchType.EAGER) @OneToMany(mappedBy = "gameId", fetch = FetchType.EAGER)
private List<LockGameLockEntity> activeLocks = new ArrayList<>(); private List<LockGameLockEntity> activeLocks = new ArrayList<>();
@Column @Column
@@ -42,9 +44,23 @@ public class LockGameEntity {
@Column @Column
private LocalDateTime activeTaskEnd; private LocalDateTime activeTaskEnd;
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String activeTaskBenoetigtAktiv;
@Column(columnDefinition = "TEXT")
private String taskInQueue; private String taskInQueue;
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String lockInQueue; private String lockInQueue;
@Column @Column
private UUID setupId; private UUID setupId;
@Column
private LocalDateTime tempOpeningTime;
@Column
private Integer tempOpeningDurationInMinutes;
@Column
private String tempUnlockCode;
@Column
private Integer overtimeInSeconds;
@Column
private LocalDateTime finisherStartedAt;
@Column(columnDefinition = "TEXT")
private String finisher;
} }

View File

@@ -39,4 +39,6 @@ public class LockGameLockEntity {
private String releaseText; private String releaseText;
@Column @Column
private LocalDateTime releaseTime; private LocalDateTime releaseTime;
@Column
private Boolean tempUnlockRequired;
} }

View File

@@ -1,5 +1,6 @@
package de.oaa.xxx.games.chastity.common; package de.oaa.xxx.games.chastity.common;
import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -14,39 +15,60 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.oaa.xxx.games.bdsm.AufgabeAnzeige; import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
import de.oaa.xxx.games.chastity.lockcontroll.LockControlCallback;
import de.oaa.xxx.games.chastity.lockcontroll.LockControlFactory;
import de.oaa.xxx.games.chastity.unlock.TempOpeningReason;
import de.oaa.xxx.games.chastity.unlock.UnlockCodeHistoryService;
import de.oaa.xxx.games.common.aufgaben.Aufgabe; import de.oaa.xxx.games.common.aufgaben.Aufgabe;
import de.oaa.xxx.games.common.aufgaben.AufgabenList; import de.oaa.xxx.games.common.aufgaben.AufgabenList;
import de.oaa.xxx.games.common.aufgaben.Finisher;
import de.oaa.xxx.games.common.aufgaben.Sperre; import de.oaa.xxx.games.common.aufgaben.Sperre;
public class LockGameService { public class LockGameService implements LockControlCallback {
private static final Logger LOGGER = LoggerFactory.getLogger(LockGameService.class); private static final Logger LOGGER = LoggerFactory.getLogger(LockGameService.class);
private LockGameEntity gamestate;
private LockGameRepository lockGameRepository; private LockGameRepository lockGameRepository;
private LockGameLockRepository lockGameLockRepository; private LockGameLockRepository lockGameLockRepository;
private BaseLockRepository baseLockRepository;
private UnlockCodeHistoryService unlockCodeHistoryService;
private LockControlFactory lockControlFactory;
private LockGameEntity gamestate;
private AufgabenList aufgabenList; private AufgabenList aufgabenList;
public LockGameService(LockGameEntity gamestate, LockGameRepository lockGameRepository, public LockGameService(LockGameEntity gamestate, LockGameRepository lockGameRepository,
LockGameLockRepository lockGameLockRepository) throws JsonMappingException, JsonProcessingException { LockGameLockRepository lockGameLockRepository, BaseLockRepository baseLockRepository,
UnlockCodeHistoryService unlockCodeHistoryService, LockControlFactory lockControlFactory) throws JsonMappingException, JsonProcessingException {
this.gamestate = gamestate; this.gamestate = gamestate;
this.lockGameRepository = lockGameRepository; this.lockGameRepository = lockGameRepository;
this.lockGameLockRepository = lockGameLockRepository; this.lockGameLockRepository = lockGameLockRepository;
this.baseLockRepository = baseLockRepository;
this.unlockCodeHistoryService = unlockCodeHistoryService;
this.lockControlFactory = lockControlFactory;
this.aufgabenList = new ObjectMapper().readValue(gamestate.getAufgaben(), AufgabenList.class); this.aufgabenList = new ObjectMapper().readValue(gamestate.getAufgaben(), AufgabenList.class);
} }
public GameState getGameState() { public GameState getGameState() {
List<String> benoetigtAktiv = null;
if (gamestate.getActiveTaskBenoetigtAktiv() != null) {
try {
benoetigtAktiv = new ObjectMapper().readValue(gamestate.getActiveTaskBenoetigtAktiv(),
new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {});
} catch (Exception ignored) {}
}
return new GameState(gamestate.getGameId(), gamestate.getUserId(), gamestate.getLevel(), return new GameState(gamestate.getGameId(), gamestate.getUserId(), gamestate.getLevel(),
gamestate.getActiveTask(), gamestate.getActiveTaskEnd(), gamestate.getTaskInQueue(), gamestate.getActiveTask(), gamestate.getActiveTaskEnd(), benoetigtAktiv,
gamestate.getLockInQueue()); gamestate.getTaskInQueue(), gamestate.getLockInQueue(), gamestate.getTempOpeningTime(),
gamestate.getTempOpeningDurationInMinutes(), gamestate.getTempUnlockCode(),
gamestate.getFinisherStartedAt(), gamestate.getFinisher());
} }
public void initNextTask() throws JsonProcessingException { public void initNextTask() throws JsonProcessingException {
gamestate.setActiveTask(null); gamestate.setActiveTask(null);
gamestate.setActiveTaskEnd(null); gamestate.setActiveTaskEnd(null);
gamestate.setActiveTaskBenoetigtAktiv(null);
checkLevel(); checkLevel();
lockGameRepository.save(gamestate); lockGameRepository.save(gamestate);
pickNextTask(); pickNextTask();
@@ -55,6 +77,7 @@ public class LockGameService {
public void abandonActiveTask() throws JsonProcessingException { public void abandonActiveTask() throws JsonProcessingException {
gamestate.setActiveTask(null); gamestate.setActiveTask(null);
gamestate.setActiveTaskEnd(null); gamestate.setActiveTaskEnd(null);
gamestate.setActiveTaskBenoetigtAktiv(null);
lockGameRepository.save(gamestate); lockGameRepository.save(gamestate);
pickNextTask(); pickNextTask();
} }
@@ -136,6 +159,9 @@ public class LockGameService {
var aufgabe = mapper.readValue(gamestate.getTaskInQueue(), Aufgabe.class); var aufgabe = mapper.readValue(gamestate.getTaskInQueue(), Aufgabe.class);
gamestate.setActiveTask(aufgabe.getText()); gamestate.setActiveTask(aufgabe.getText());
gamestate.setTaskInQueue(null); gamestate.setTaskInQueue(null);
var benoetigtAktiv = aufgabe.getBenoetigtAktiv();
gamestate.setActiveTaskBenoetigtAktiv(
(benoetigtAktiv != null && !benoetigtAktiv.isEmpty()) ? mapper.writeValueAsString(benoetigtAktiv) : null);
var time = getAufgabeTime(aufgabe); var time = getAufgabeTime(aufgabe);
gamestate.setActiveTaskEnd(time > 0 ? LocalDateTime.now().plusSeconds(time) : null); gamestate.setActiveTaskEnd(time > 0 ? LocalDateTime.now().plusSeconds(time) : null);
LOGGER.info("[LockGame {}] AUFGABE aktiv: kurzText='{}', berechnete Zeit={}s (Range: {}s-{}s)", LOGGER.info("[LockGame {}] AUFGABE aktiv: kurzText='{}', berechnete Zeit={}s (Range: {}s-{}s)",
@@ -147,6 +173,7 @@ public class LockGameService {
var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class); var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class);
String displayText = lock.getText() != null ? lock.getText() : lock.getKurzText(); String displayText = lock.getText() != null ? lock.getText() : lock.getKurzText();
gamestate.setActiveTask(displayText != null ? displayText : "Zeitstrafe aktiv"); gamestate.setActiveTask(displayText != null ? displayText : "Zeitstrafe aktiv");
gamestate.setActiveTaskBenoetigtAktiv(null);
gamestate.setLockInQueue(null); gamestate.setLockInQueue(null);
applyLock(lock); applyLock(lock);
} }
@@ -168,11 +195,14 @@ public class LockGameService {
return (int) (time * gamestate.getZeitfaktorZeitstrafen()); return (int) (time * gamestate.getZeitfaktorZeitstrafen());
} }
protected void checkLevel() { protected void checkLevel() throws JsonProcessingException {
var aufgabenAufAktuellemLevel = gamestate.getAufgabenAufAktuellemLevel(); var aufgabenAufAktuellemLevel = gamestate.getAufgabenAufAktuellemLevel();
if (++aufgabenAufAktuellemLevel >= 1 + gamestate.getAufgabenProLevel()) { if (++aufgabenAufAktuellemLevel >= 1 + gamestate.getAufgabenProLevel()) {
aufgabenAufAktuellemLevel = 0; aufgabenAufAktuellemLevel = 0;
gamestate.setLevel(gamestate.getLevel() + 1); gamestate.setLevel(gamestate.getLevel() + 1);
if (gamestate.getLevel() >= 6) {
initFinisher();
}
} }
gamestate.setAufgabenAufAktuellemLevel(aufgabenAufAktuellemLevel); gamestate.setAufgabenAufAktuellemLevel(aufgabenAufAktuellemLevel);
} }
@@ -190,6 +220,7 @@ public class LockGameService {
entity.setGameId(gamestate.getGameId()); entity.setGameId(gamestate.getGameId());
entity.setLockFor(lock.getSperreFuer()); entity.setLockFor(lock.getSperreFuer());
entity.setReleaseText(lock.getReleaseText()); entity.setReleaseText(lock.getReleaseText());
entity.setTempUnlockRequired(lock.getTempUnlockRequired());
int lockMinutes = getLockTime(lock); int lockMinutes = getLockTime(lock);
entity.setReleaseTime(LocalDateTime.now().plusMinutes(lockMinutes)); entity.setReleaseTime(LocalDateTime.now().plusMinutes(lockMinutes));
LOGGER.info("[LockGame {}] ZEITSTRAFE aktiv: kurzText='{}', berechnete Zeit={}min (Range: {}min-{}min), sperreFuer={}", LOGGER.info("[LockGame {}] ZEITSTRAFE aktiv: kurzText='{}', berechnete Zeit={}min (Range: {}min-{}min), sperreFuer={}",
@@ -201,9 +232,13 @@ public class LockGameService {
public List<String> checkLocks() { public List<String> checkLocks() {
var result = new ArrayList<String>(); var result = new ArrayList<String>();
for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) { for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) {
if (entity.getReleaseTime().isAfter(LocalDateTime.now())) { if (entity.getReleaseTime().isBefore(LocalDateTime.now())) {
result.add(entity.getReleaseText()); result.add(entity.getReleaseText());
if (Boolean.TRUE.equals(entity.getTempUnlockRequired())) {
startTempOpening();
}
lockGameLockRepository.delete(entity); lockGameLockRepository.delete(entity);
} }
} }
return result; return result;
@@ -218,7 +253,77 @@ public class LockGameService {
return result; return result;
} }
public Finisher getFinisher() { public void initFinisher() throws JsonProcessingException {
return aufgabenList.getFinisher().get(new Random().nextInt(aufgabenList.getFinisher().size())); var finisher = aufgabenList.getFinisher().get(new Random().nextInt(aufgabenList.getFinisher().size()));
ObjectMapper mapper = new ObjectMapper();
gamestate.setFinisher(mapper.writeValueAsString(finisher));
gamestate.setActiveTask(null);
gamestate.setActiveTaskBenoetigtAktiv(null);
lockGameLockRepository.deleteAll(lockGameLockRepository.findByGameId(gamestate.getGameId()));
lockGameRepository.save(gamestate);
}
public void startFinisher() {
gamestate.setFinisherStartedAt(LocalDateTime.now());
lockGameRepository.save(gamestate);
}
public void endFinisher() {
var seconds = Duration.between(gamestate.getFinisherStartedAt(), LocalDateTime.now()).toSeconds();
addOvertime(seconds);
}
private void addOvertime(Long seconds) {
var currentOvertime = gamestate.getOvertimeInSeconds() != null ? gamestate.getOvertimeInSeconds() : 0;
gamestate.setOvertimeInSeconds(currentOvertime + seconds.intValue());
lockGameRepository.save(gamestate);
}
public void startTempOpening() {
startTempOpening(5);
}
private void startTempOpening(Integer duration) {
assert duration != null;
var lock = baseLockRepository.findById(gamestate.getLockId()).get();
gamestate.setTempOpeningTime(LocalDateTime.now());
gamestate.setTempOpeningDurationInMinutes(duration);
gamestate.setTempUnlockCode(lock.getUnlockCode());
lockGameRepository.save(gamestate);
unlockCodeHistoryService.save(lock.getLockee(), lock.getLockId(), lock.getName(), lock.getUnlockCode(), TempOpeningReason.TASK.toString());
}
public void endTempOpening() {
var lock = baseLockRepository.findById(gamestate.getLockId()).get();
var overtime = BaseLockHelper.calcOvertime(lock);
if (overtime != null) {
addOvertime(overtime);
}
gamestate.setTempOpeningTime(null);
gamestate.setTempOpeningDurationInMinutes(null);
gamestate.setTempUnlockCode(null);
lockGameRepository.save(gamestate);
if (lock.getControllType() != null) {
var lockControl = lockControlFactory.create(lock.getControllType(), this, lock.getLockee());
if (lockControl != null
&& lock.getControllType() != de.oaa.xxx.games.chastity.lockcontroll.LockControllType.UNLOCK_CODE) {
lockControl.lock();
}
}
}
@Override
public void setUnlockCode(String code) {
var lock = baseLockRepository.findById(gamestate.getLockId()).get();
lock.setUnlockCode(code);
baseLockRepository.save(lock);
}
@Override
public int getUnlockcodeLenght() {
var lock = baseLockRepository.findById(gamestate.getLockId()).get();
return lock.getUnlockCodeLength();
} }
} }

View File

@@ -267,7 +267,7 @@ public class KeyholderOfferController {
if (template instanceof TimeLockTemplateEntity tl) { if (template instanceof TimeLockTemplateEntity tl) {
TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings( TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings(
controllType, myId, keyholderIdIfDirect, false, codeLen); controllType, myId, keyholderIdIfDirect, false, codeLen, 1);
TimeLockEntity lock = new TimeLockEntity(); TimeLockEntity lock = new TimeLockEntity();
timeLockServiceFactory.create(lock).init(tl, settings); timeLockServiceFactory.create(lock).init(tl, settings);
timeLockRepository.save(lock); timeLockRepository.save(lock);
@@ -283,11 +283,11 @@ public class KeyholderOfferController {
lock.setLockee(myId); lock.setLockee(myId);
lock.setKeyholder(keyholderIdIfDirect); lock.setKeyholder(keyholderIdIfDirect);
lock.setInitialCards(cards); lock.setInitialCards(cards);
lock.setPickEveryMinute(cl.getPickEveryMinute() != null ? cl.getPickEveryMinute() : 60); lock.setPickEverySeconds((cl.getPickEveryMinute() != null ? cl.getPickEveryMinute() : 60) * 60);
lock.setAccumulatePicks(cl.isAccumulatePicks()); lock.setAccumulatePicks(cl.isAccumulatePicks());
lock.setShowRemainingCards(cl.isShowRemainingCards()); lock.setShowRemainingCards(cl.isShowRemainingCards());
lock.setHygineOpeningDurationMinutes(cl.getHygineOpeningDurationMinutes()); lock.setHygineOpeningDurationSeconds(cl.getHygineOpeningDurationMinutes() != null ? cl.getHygineOpeningDurationMinutes() * 60 : null);
lock.setHygineOpeningEveryMinites(cl.getHygineOpeningEveryMinites()); lock.setHygineOpeningEverySeconds(cl.getHygineOpeningEveryMinites() != null ? cl.getHygineOpeningEveryMinites() * 60 : null);
lock.setTasks(cl.getTasks() != null ? cl.getTasks() : List.of()); lock.setTasks(cl.getTasks() != null ? cl.getTasks() : List.of());
lock.setRequiresVerification(cl.isRequiresVerification()); lock.setRequiresVerification(cl.isRequiresVerification());
lock.setTestLock(false); lock.setTestLock(false);
@@ -299,7 +299,7 @@ public class KeyholderOfferController {
lock.setStartTime(now); lock.setStartTime(now);
lock.setAvailableCards(new ArrayList<>(cards)); lock.setAvailableCards(new ArrayList<>(cards));
lock.setOpenPicks(0); lock.setOpenPicks(0);
lock.setNextCardIn(now.plusMinutes(lock.getPickEveryMinute())); lock.setNextCardIn(now.plusSeconds(lock.getPickEverySeconds()));
if (cl.getHygineOpeningEveryMinites() != null) { if (cl.getHygineOpeningEveryMinites() != null) {
lock.setLastHygineOpening(now); lock.setLastHygineOpening(now);
} }

View File

@@ -198,11 +198,11 @@ public class LockeeInvitationController {
.collect(java.util.stream.Collectors.groupingBy( .collect(java.util.stream.Collectors.groupingBy(
c -> c.name(), java.util.stream.Collectors.counting())); c -> c.name(), java.util.stream.Collectors.counting()));
result.put("cardCounts", cardCounts); result.put("cardCounts", cardCounts);
result.put("pickEveryMinute", cardLock.getPickEveryMinute()); result.put("pickEveryMinute", cardLock.getPickEverySeconds() != null ? cardLock.getPickEverySeconds() / 60 : null);
result.put("accumulatePicks", cardLock.isAccumulatePicks()); result.put("accumulatePicks", cardLock.isAccumulatePicks());
result.put("showRemainingCards", cardLock.isShowRemainingCards()); result.put("showRemainingCards", cardLock.isShowRemainingCards());
result.put("hygineOpeningEveryMinites", cardLock.getHygineOpeningEveryMinites()); result.put("hygineOpeningEveryMinites", cardLock.getHygineOpeningEverySeconds() != null ? cardLock.getHygineOpeningEverySeconds() / 60 : null);
result.put("hygineOpeningDurationMinutes", cardLock.getHygineOpeningDurationMinutes()); result.put("hygineOpeningDurationMinutes", cardLock.getHygineOpeningDurationSeconds() != null ? cardLock.getHygineOpeningDurationSeconds() / 60 : null);
result.put("requiresVerification", cardLock.isRequiresVerification()); result.put("requiresVerification", cardLock.isRequiresVerification());
result.put("taskCount", cardLock.getTasks() != null ? cardLock.getTasks().size() : 0); result.put("taskCount", cardLock.getTasks() != null ? cardLock.getTasks().size() : 0);
} }
@@ -246,8 +246,8 @@ public class LockeeInvitationController {
cardLock.setUnlockCodeLength(codeLines); cardLock.setUnlockCodeLength(codeLines);
cardLock.setAvailableCards(new ArrayList<>(cardLock.getInitialCards())); cardLock.setAvailableCards(new ArrayList<>(cardLock.getInitialCards()));
cardLock.setOpenPicks(0); cardLock.setOpenPicks(0);
cardLock.setNextCardIn(now.plusMinutes(cardLock.getPickEveryMinute())); cardLock.setNextCardIn(now.plusSeconds(cardLock.getPickEverySeconds()));
if (cardLock.getHygineOpeningEveryMinites() != null) { if (cardLock.getHygineOpeningEverySeconds() != null) {
cardLock.setLastHygineOpening(now); cardLock.setLastHygineOpening(now);
} }
cardlockRepository.save(cardLock); cardlockRepository.save(cardLock);
@@ -258,7 +258,7 @@ public class LockeeInvitationController {
timeLock.setEstimatedUnlockTime(now.plusMinutes(unlockMinutes)); timeLock.setEstimatedUnlockTime(now.plusMinutes(unlockMinutes));
timeLock.setUnlockCode(unlockCode); timeLock.setUnlockCode(unlockCode);
timeLock.setUnlockCodeLength(codeLines); timeLock.setUnlockCodeLength(codeLines);
if (timeLock.getHygineOpeningEveryMinites() != null) { if (timeLock.getHygineOpeningEverySeconds() != null) {
timeLock.setLastHygineOpening(now); timeLock.setLastHygineOpening(now);
} }
timeLockRepository.save(timeLock); timeLockRepository.save(timeLock);

View File

@@ -4,6 +4,6 @@ import java.util.UUID;
import de.oaa.xxx.games.chastity.lockcontroll.LockControllType; import de.oaa.xxx.games.chastity.lockcontroll.LockControllType;
public record TimeLockAdditionalSettings(LockControllType controllType, UUID lockee, UUID keyholder, boolean testlock, Integer unlockCodeLength) { public record TimeLockAdditionalSettings(LockControllType controllType, UUID lockee, UUID keyholder, boolean testlock, Integer unlockCodeLength, Integer speedFactor) {
} }

View File

@@ -100,7 +100,8 @@ public class TimeLockController {
UUID keyholder, UUID keyholder,
boolean testLock, boolean testLock,
Integer unlockCodeLength, Integer unlockCodeLength,
LockControllType controllType LockControllType controllType,
Integer speedFactor
) {} ) {}
@PostMapping("/timelock") @PostMapping("/timelock")
@@ -155,9 +156,10 @@ public class TimeLockController {
return ResponseEntity.status(403).body(Map.of("error", "subscription_required")); return ResponseEntity.status(403).body(Map.of("error", "subscription_required"));
} }
int speedFactor = (req.testLock() && req.speedFactor() != null && req.speedFactor() > 1) ? req.speedFactor() : 1;
TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings( TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings(
req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE, req.controllType() != null ? req.controllType() : LockControllType.UNLOCK_CODE,
myId, req.keyholder(), req.testLock(), codeLen); myId, req.keyholder(), req.testLock(), codeLen, speedFactor);
TimeLockEntity lock = new TimeLockEntity(); TimeLockEntity lock = new TimeLockEntity();
timeLockServiceFactory.create(lock).init(template, settings); timeLockServiceFactory.create(lock).init(template, settings);
timeLockRepository.save(lock); // Sicherstellen dass auch TRUST-Locks persistiert sind timeLockRepository.save(lock); // Sicherstellen dass auch TRUST-Locks persistiert sind
@@ -234,7 +236,7 @@ public class TimeLockController {
&& (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now)); && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now));
// Hygiene state // Hygiene state
boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; boolean hygieneEnabled = l.getHygineOpeningEverySeconds() != null;
boolean hygieneOpeningDue = false; boolean hygieneOpeningDue = false;
long hygieneSecondsRemaining = 0; long hygieneSecondsRemaining = 0;
boolean hygieneOpeningActive = l.getTempOpeningTime() != null boolean hygieneOpeningActive = l.getTempOpeningTime() != null
@@ -244,7 +246,9 @@ public class TimeLockController {
if (lastH == null) { if (lastH == null) {
hygieneOpeningDue = true; hygieneOpeningDue = true;
} else { } else {
LocalDateTime nextH = lastH.plusMinutes(l.getHygineOpeningEveryMinites()); int tlSf = (l.getSpeedFactor() != null && l.getSpeedFactor() > 1) ? l.getSpeedFactor() : 1;
long hygineSeconds = tlSf > 1 ? Math.max(6L, l.getHygineOpeningEverySeconds() / tlSf) : l.getHygineOpeningEverySeconds();
LocalDateTime nextH = lastH.plusSeconds(hygineSeconds);
long secs = ChronoUnit.SECONDS.between(now, nextH); long secs = ChronoUnit.SECONDS.between(now, nextH);
if (secs <= 0) hygieneOpeningDue = true; if (secs <= 0) hygieneOpeningDue = true;
else hygieneSecondsRemaining = secs; else hygieneSecondsRemaining = secs;
@@ -252,7 +256,7 @@ public class TimeLockController {
} }
// Spin wheel state // Spin wheel state
boolean spinEnabled = l.getSpinsEveryMinutes() != null boolean spinEnabled = l.getSpinsEverySeconds() != null
&& l.getSpinningWheelEntries() != null && !l.getSpinningWheelEntries().isEmpty(); && l.getSpinningWheelEntries() != null && !l.getSpinningWheelEntries().isEmpty();
boolean spinDue = false; boolean spinDue = false;
String nextSpinIn = null; String nextSpinIn = null;
@@ -261,23 +265,23 @@ public class TimeLockController {
if (times == null || times.isEmpty()) { if (times == null || times.isEmpty()) {
spinDue = true; spinDue = true;
} else { } else {
LocalDateTime next = times.get(times.size() - 1).plusMinutes(l.getSpinsEveryMinutes()); LocalDateTime next = times.get(times.size() - 1).plusSeconds(l.getSpinsEverySeconds());
if (next.isBefore(now)) spinDue = true; if (next.isBefore(now)) spinDue = true;
else nextSpinIn = next.toString(); else nextSpinIn = next.toString();
} }
} }
// Task timing state // Task timing state
boolean taskTimingEnabled = l.getTaskEveryMinutes() != null; boolean taskTimingEnabled = l.getTaskEverySeconds() != null;
String nextTaskIn = null; String nextTaskIn = null;
if (taskTimingEnabled && l.getCurrentTask() == null) { if (taskTimingEnabled && l.getCurrentTask() == null) {
List<LocalDateTime> times = l.getTaskTimes(); List<LocalDateTime> times = l.getTaskTimes();
LocalDateTime next; LocalDateTime next;
if (times == null || times.isEmpty()) { if (times == null || times.isEmpty()) {
next = l.getStartTime() != null next = l.getStartTime() != null
? l.getStartTime().plusMinutes(l.getTaskEveryMinutes()) : null; ? l.getStartTime().plusSeconds(l.getTaskEverySeconds()) : null;
} else { } else {
next = times.get(times.size() - 1).plusMinutes(l.getTaskEveryMinutes()); next = times.get(times.size() - 1).plusSeconds(l.getTaskEverySeconds());
} }
if (next != null && next.isAfter(now)) nextTaskIn = next.toString(); if (next != null && next.isAfter(now)) nextTaskIn = next.toString();
} }
@@ -347,7 +351,7 @@ public class TimeLockController {
result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); result.put("hygieneSecondsRemaining", hygieneSecondsRemaining);
result.put("hygieneOpeningActive", hygieneOpeningActive); result.put("hygieneOpeningActive", hygieneOpeningActive);
result.put("hygieneOpeningStarted", l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null); result.put("hygieneOpeningStarted", l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null);
result.put("hygieneDurationMinutes", l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0); result.put("hygieneDurationMinutes", l.getHygineOpeningDurationSeconds() != null ? l.getHygineOpeningDurationSeconds() / 60 : 0);
result.put("verificationRequired", l.isRequiresVerification()); result.put("verificationRequired", l.isRequiresVerification());
result.put("verificationDue", verificationDue); result.put("verificationDue", verificationDue);
@@ -402,8 +406,8 @@ public class TimeLockController {
// Check spin is due // Check spin is due
List<LocalDateTime> spinTimes = l.getSpinningWheelTimes(); List<LocalDateTime> spinTimes = l.getSpinningWheelTimes();
if (spinTimes != null && !spinTimes.isEmpty() && l.getSpinsEveryMinutes() != null) { if (spinTimes != null && !spinTimes.isEmpty() && l.getSpinsEverySeconds() != null) {
LocalDateTime next = spinTimes.get(spinTimes.size() - 1).plusMinutes(l.getSpinsEveryMinutes()); LocalDateTime next = spinTimes.get(spinTimes.size() - 1).plusSeconds(l.getSpinsEverySeconds());
if (next.isAfter(now)) return ResponseEntity.status(409).body(Map.of("error", "not_due")); if (next.isAfter(now)) return ResponseEntity.status(409).body(Map.of("error", "not_due"));
} }
@@ -477,7 +481,7 @@ public class TimeLockController {
if (lockOpt.isEmpty()) return ResponseEntity.notFound().build(); if (lockOpt.isEmpty()) return ResponseEntity.notFound().build();
var l = lockOpt.get(); var l = lockOpt.get();
if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build(); if (!l.getLockee().equals(myId)) return ResponseEntity.status(403).build();
if (l.getHygineOpeningEveryMinites() == null) return ResponseEntity.status(409).build(); if (l.getHygineOpeningEverySeconds() == null) return ResponseEntity.status(409).build();
if (l.getTempOpeningTime() != null) return ResponseEntity.status(409).body(Map.of("error", "already_open")); if (l.getTempOpeningTime() != null) return ResponseEntity.status(409).body(Map.of("error", "already_open"));
TimeLockService service = timeLockServiceFactory.create(l); TimeLockService service = timeLockServiceFactory.create(l);
@@ -485,7 +489,7 @@ public class TimeLockController {
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "", "unlockCode", l.getUnlockCode() != null ? l.getUnlockCode() : "",
"durationMinutes", l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0, "durationMinutes", l.getHygineOpeningDurationSeconds() != null ? l.getHygineOpeningDurationSeconds() / 60 : 0,
"openedAt", l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : "")); "openedAt", l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : ""));
} }
@@ -840,13 +844,13 @@ public class TimeLockController {
lock.setEndTimeVisible(template.isEndTimeVisible()); lock.setEndTimeVisible(template.isEndTimeVisible());
lock.setTasks(template.getTasks()); lock.setTasks(template.getTasks());
lock.setTaskMode(template.getTaskCardMode()); lock.setTaskMode(template.getTaskCardMode());
lock.setTaskEveryMinutes(template.getTaskEveryMinutes()); lock.setTaskEverySeconds(template.getTaskEveryMinutes() != null ? template.getTaskEveryMinutes() * 60 : null);
lock.setMinTasksPerDay(template.getMinTasksPerDay()); lock.setMinTasksPerDay(template.getMinTasksPerDay());
lock.setSpinningWheelEntries(template.getSpinningWheelEntries()); lock.setSpinningWheelEntries(template.getSpinningWheelEntries());
lock.setSpinsEveryMinutes(template.getSpinsEveryMinutes()); lock.setSpinsEverySeconds(template.getSpinsEveryMinutes() != null ? template.getSpinsEveryMinutes() * 60 : null);
lock.setMinSpinsPerDay(template.getMinSpinsPerDay()); lock.setMinSpinsPerDay(template.getMinSpinsPerDay());
lock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes()); lock.setHygineOpeningDurationSeconds(template.getHygineOpeningDurationMinutes() != null ? template.getHygineOpeningDurationMinutes() * 60 : null);
lock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites()); lock.setHygineOpeningEverySeconds(template.getHygineOpeningEveryMinites() != null ? template.getHygineOpeningEveryMinites() * 60 : null);
lock.setPenaltyType(template.getPenaltyType()); lock.setPenaltyType(template.getPenaltyType());
lock.setPenaltyValue(template.getPenaltyValue()); lock.setPenaltyValue(template.getPenaltyValue());
lock.setMinTimeInMinutes(template.getMinTimeInMinutes()); lock.setMinTimeInMinutes(template.getMinTimeInMinutes());

View File

@@ -29,7 +29,7 @@ public class TimeLockEntity extends BaseLockEntity {
private Integer maxTimeInMinutes; private Integer maxTimeInMinutes;
@Column @Column
private Integer taskEveryMinutes; private Integer taskEverySeconds;
@Column @Column
private Integer minTasksPerDay; private Integer minTasksPerDay;
@@ -37,7 +37,7 @@ public class TimeLockEntity extends BaseLockEntity {
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private List<SpinningWheelEntry> spinningWheelEntries; private List<SpinningWheelEntry> spinningWheelEntries;
@Column @Column
private Integer spinsEveryMinutes; private Integer spinsEverySeconds;
@Column @Column
private Integer minSpinsPerDay; private Integer minSpinsPerDay;

View File

@@ -132,24 +132,26 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
lock.setTestLock(settings.testlock()); lock.setTestLock(settings.testlock());
lock.setUnlockCodeLength(settings.unlockCodeLength() != null ? settings.unlockCodeLength() : 5); lock.setUnlockCodeLength(settings.unlockCodeLength() != null ? settings.unlockCodeLength() : 5);
int sf = (settings.speedFactor() != null && settings.speedFactor() > 1) ? settings.speedFactor() : 1;
lock.setSpeedFactor(sf > 1 ? sf : null);
Integer minMinutes = template.getMinTimeInMinutes(); Integer minMinutes = template.getMinTimeInMinutes();
Integer maxMinutes = template.getMaxTimeInMinutes() != null ? template.getMaxTimeInMinutes() : 60; Integer maxMinutes = template.getMaxTimeInMinutes() != null ? template.getMaxTimeInMinutes() : 60;
int unlockTimeMinutes = (minMinutes != null && minMinutes < maxMinutes) int unlockTimeMinutes = (minMinutes != null && minMinutes < maxMinutes)
? minMinutes + new Random().nextInt(maxMinutes - minMinutes) ? minMinutes + new Random().nextInt(maxMinutes - minMinutes)
: maxMinutes; : maxMinutes;
lock.setEstimatedUnlockTime(now.plusMinutes(unlockTimeMinutes)); lock.setEstimatedUnlockTime(now.plusSeconds(Math.max(6L, unlockTimeMinutes * 60L / sf)));
lock.setEndTimeVisible(template.isEndTimeVisible()); lock.setEndTimeVisible(template.isEndTimeVisible());
lock.setTasks(template.getTasks()); lock.setTasks(template.getTasks());
lock.setTaskEveryMinutes(template.getTaskEveryMinutes()); lock.setTaskEverySeconds(template.getTaskEveryMinutes() != null ? Math.max(6, template.getTaskEveryMinutes() * 60 / sf) : null);
lock.setMinTasksPerDay(template.getMinTasksPerDay()); lock.setMinTasksPerDay(template.getMinTasksPerDay());
lock.setSpinningWheelEntries(template.getSpinningWheelEntries()); lock.setSpinningWheelEntries(template.getSpinningWheelEntries());
lock.setSpinsEveryMinutes(template.getSpinsEveryMinutes()); lock.setSpinsEverySeconds(template.getSpinsEveryMinutes() != null ? Math.max(6, template.getSpinsEveryMinutes() * 60 / sf) : null);
lock.setMinSpinsPerDay(template.getMinSpinsPerDay()); lock.setMinSpinsPerDay(template.getMinSpinsPerDay());
lock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes()); lock.setHygineOpeningDurationSeconds(template.getHygineOpeningDurationMinutes() != null ? template.getHygineOpeningDurationMinutes() * 60 : null);
lock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites()); lock.setHygineOpeningEverySeconds(template.getHygineOpeningEveryMinites() != null ? template.getHygineOpeningEveryMinites() * 60 : null);
if (template.getHygineOpeningEveryMinites() != null) { if (template.getHygineOpeningEveryMinites() != null) {
lock.setLastHygineOpening(now); lock.setLastHygineOpening(now);
} }
@@ -360,7 +362,7 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
// ── Hygiene opening ─────────────────────────────────────────────────────── // ── Hygiene opening ───────────────────────────────────────────────────────
public void startHygieneOpening() { public void startHygieneOpening() {
startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes()); startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationSeconds() != null ? lock.getHygineOpeningDurationSeconds() / 60 : 0);
} }
// ── LockControlCallback ─────────────────────────────────────────────────── // ── LockControlCallback ───────────────────────────────────────────────────
@@ -377,13 +379,13 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
} }
@Override @Override
protected void handleLockGameFinished(int timeInMinutes) { protected void handleLockGameFinished(int timeInSeconds) {
int freezeTime = (int) (timeInMinutes * new Random().nextDouble(1.0, 4.0)); int freezeMinutes = (int) (timeInSeconds / 60.0 * new Random().nextDouble(1.0, 4.0));
addTime(freezeTime); addTime(Math.max(1, freezeMinutes));
} }
@Override @Override
public void penaltyLockGame() { public void penaltyLockGame() {
handleLockGameFinished(60); handleLockGameFinished(3600);
} }
} }

View File

@@ -1,5 +1,5 @@
package de.oaa.xxx.games.chastity.unlock; package de.oaa.xxx.games.chastity.unlock;
public enum TempOpeningReason { public enum TempOpeningReason {
HYGIENE, CARD, TASK, TTLOCK_UNAUTHORIZED; HYGIENE, CARD, TASK, TTLOCK_UNAUTHORIZED, TEMPORARY;
} }

View File

@@ -19,6 +19,7 @@ public class Finisher {
private List<Werkzeug> benoetigtPassiv; private List<Werkzeug> benoetigtPassiv;
private List<Toy> benoetigteToys; private List<Toy> benoetigteToys;
private UUID gruppeId; private UUID gruppeId;
private Boolean tempUnlockRequired;
@Override @Override
public String toString() { public String toString() {

View File

@@ -20,8 +20,7 @@ public class Sperre {
private Integer minutenVon; private Integer minutenVon;
private Integer minutenBis; private Integer minutenBis;
private Integer level; private Integer level;
private Boolean tempUnlockBeforeRequired; private Boolean tempUnlockRequired;
private Boolean tempUnlockAfterRequired;
private List<Toy> benoetigteToys; private List<Toy> benoetigteToys;
@Override @Override

View File

@@ -55,6 +55,8 @@ public class FinisherEntity {
@ManyToMany(cascade = CascadeType.DETACH) @ManyToMany(cascade = CascadeType.DETACH)
@JoinTable(name = "finisherToy", joinColumns = {@JoinColumn(name = "finisherId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")}) @JoinTable(name = "finisherToy", joinColumns = {@JoinColumn(name = "finisherId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")})
private List<ToyEntity> benoetigteToys; private List<ToyEntity> benoetigteToys;
@Column
private Boolean tempUnlockRequired;
@Override @Override
public String toString() { public String toString() {
@@ -71,6 +73,7 @@ public class FinisherEntity {
finisher.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>()); finisher.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>());
finisher.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>()); finisher.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>());
finisher.setGruppeId(aufgabenGruppe.getGruppenId()); finisher.setGruppeId(aufgabenGruppe.getGruppenId());
finisher.setTempUnlockRequired(tempUnlockRequired);
return finisher; return finisher;
} }
@@ -84,6 +87,7 @@ public class FinisherEntity {
entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv()); entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv());
entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv()); entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv());
entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>()); entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>());
entity.setTempUnlockRequired(finisher.getTempUnlockRequired());
return entity; return entity;
} }
} }

View File

@@ -53,9 +53,7 @@ public class SperreEntity {
@Column @Column
private Integer level; private Integer level;
@Column @Column
private Boolean tempUnlockBeforeRequired; private Boolean tempUnlockRequired;
@Column
private Boolean tempUnlockAfterRequired;
@ManyToMany(cascade = CascadeType.DETACH) @ManyToMany(cascade = CascadeType.DETACH)
@JoinTable(name = "sperreToy", joinColumns = {@JoinColumn(name = "sperreId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")}) @JoinTable(name = "sperreToy", joinColumns = {@JoinColumn(name = "sperreId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")})
private List<ToyEntity> benoetigteToys; private List<ToyEntity> benoetigteToys;
@@ -74,8 +72,7 @@ public class SperreEntity {
sperre.setMinutenBis(minutenBis); sperre.setMinutenBis(minutenBis);
sperre.setMinutenVon(minutenVon); sperre.setMinutenVon(minutenVon);
sperre.setLevel(level); sperre.setLevel(level);
sperre.setTempUnlockBeforeRequired(tempUnlockBeforeRequired); sperre.setTempUnlockRequired(tempUnlockRequired);
sperre.setTempUnlockAfterRequired(tempUnlockAfterRequired);
sperre.setReleaseText(releaseText); sperre.setReleaseText(releaseText);
sperre.setSperreFuer(sperreFuer != null ? new ArrayList<>(sperreFuer) : new ArrayList<>()); sperre.setSperreFuer(sperreFuer != null ? new ArrayList<>(sperreFuer) : new ArrayList<>());
sperre.setText(text); sperre.setText(text);
@@ -92,8 +89,7 @@ public class SperreEntity {
entity.setMinutenBis(sperre.getMinutenBis()); entity.setMinutenBis(sperre.getMinutenBis());
entity.setMinutenVon(sperre.getMinutenVon()); entity.setMinutenVon(sperre.getMinutenVon());
entity.setLevel(sperre.getLevel()); entity.setLevel(sperre.getLevel());
entity.setTempUnlockBeforeRequired(sperre.getTempUnlockBeforeRequired()); entity.setTempUnlockRequired(sperre.getTempUnlockRequired());
entity.setTempUnlockAfterRequired(sperre.getTempUnlockAfterRequired());
entity.setReleaseText(sperre.getReleaseText()); entity.setReleaseText(sperre.getReleaseText());
entity.setSperreFuer(sperre.getSperreFuer()); entity.setSperreFuer(sperre.getSperreFuer());
entity.setText(sperre.getText()); entity.setText(sperre.getText());

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;
} }
} }
@@ -1666,11 +1702,12 @@
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; } if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
payload = { payload = {
kurzText, text, kurzText, text,
geschlecht: geschlecht || null, geschlecht: geschlecht || null,
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

@@ -52,6 +52,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;
@@ -190,7 +193,7 @@
<div id="gameCard" class="game-card" style="display:none;"> <div id="gameCard" class="game-card" style="display:none;">
<div class="game-label" id="gameLabel"></div> <div class="game-label" id="gameLabel"></div>
<div class="game-text" id="gameText"></div> <div class="game-text" id="gameText"></div>
<div class="game-timer" id="gameTimer"></div> <div class="game-timer" id="gameTimer"></div>
<div class="game-btn-row"> <div class="game-btn-row">
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button> <button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
</div> </div>
@@ -205,13 +208,32 @@
</div> </div>
</div> </div>
<!-- Temporäre Öffnung -->
<div id="tempOpeningBox" class="game-card" style="display:none;">
<div class="game-label">🔓 Temporäre Öffnung erforderlich</div>
<div class="game-text" id="tempOpeningTask"></div>
<div id="tempOpeningCodeRow" style="display:none; margin-top:1rem; text-align:center;">
<div class="game-label">Entsperrcode</div>
<div id="tempOpeningCode" style="font-size:1.8rem; font-weight:700; letter-spacing:0.18em; padding:0.6rem 0; color:var(--color-primary);"></div>
</div>
<div class="game-btn-row">
<button class="btn-primary" onclick="doEndTempOpening()">✓ Erledigt</button>
</div>
</div>
<!-- Finisher --> <!-- Finisher -->
<div id="finisherBox" style="display:none;"> <div id="finisherBox" style="display:none;">
<div class="trophy">🏆</div> <div class="trophy">🏆</div>
<h2>Level 6 erreicht!</h2> <h2>Level 6 erreicht!</h2>
<div class="game-label" id="finisherTitle"></div> <div class="game-label" id="finisherTitle"></div>
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div> <div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
<button class="btn-primary" id="btnFinisherOk" style="margin-top:1.25rem;">✓ OK</button> <div id="finisherStart" style="margin-top:1.25rem;">
<button class="btn-primary" onclick="doStartFinisher()">▶ Starten</button>
</div>
<div id="finisherRunning" style="display:none;margin-top:1.25rem;">
<div class="game-timer active" id="finisherTimer">00:00</div>
<button class="btn-primary" onclick="doEndFinisher()" style="margin-top:1rem;">✓ Erledigt</button>
</div>
</div> </div>
<!-- Debug --> <!-- Debug -->
@@ -340,6 +362,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' });
@@ -417,7 +440,9 @@
await loadAndShowToys(sel.value); await loadAndShowToys(sel.value);
} }
// ── Game Loop ───────────────────────────────────────────────────────────── // ── Benötigt-Checkboxen ───────────────────────────────────────────────────
// ── Game Loop ─────────────────────────────────────────────────────────────
function setGameCard(label, text, action, btnLabel) { function setGameCard(label, text, action, btnLabel) {
document.getElementById('gameLabel').textContent = label; document.getElementById('gameLabel').textContent = label;
@@ -432,10 +457,16 @@
async function runGameLoop() { async function runGameLoop() {
hide('gameCard'); hide('gameCard');
hide('finisherBox'); hide('finisherBox');
hide('tempOpeningBox');
clearTimer(); clearTimer();
if (_state.level >= 6) { if (_state.finisher) {
await showFinisherFlow(); showFinisherUI();
return;
}
if (_state.tempOpeningTime) {
showTempOpeningDialog();
return; return;
} }
@@ -460,12 +491,14 @@
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'); setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
} 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('gameBox');
show('gameCard'); show('gameCard');
@@ -497,19 +530,38 @@
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 (_) {}
}
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; }
const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); if (wasLock && tempUnlockRequired) {
await runGameLoop(); await fetch('/lock-game/start-temp-opening', { method: 'POST' });
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
showTempOpeningDialog();
} else 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');
_state = await stateR.json();
await runGameLoop();
} else {
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
}
} catch (e) { showError(e.message || 'Fehler (Starten)'); } } catch (e) { showError(e.message || 'Fehler (Starten)'); }
} }
@@ -526,16 +578,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 texts = await lockR.json();
for (const text of (texts || [])) {
if (text != null && text !== '') await waitForReleaseOk(text);
}
}
_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 texts = await lockR.json();
for (const text of (texts || [])) {
if (text != null && text !== '') await waitForReleaseOk(text);
}
}
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');
@@ -557,51 +621,87 @@
} }
} }
async function showFinisherFlow() { function showTempOpeningDialog() {
show('gameBox'); show('gameBox');
hide('gameCard'); hide('gameCard');
hide('lockReleaseBox'); hide('lockReleaseBox');
hide('finisherBox'); hide('finisherBox');
// 1. Release-Texte sequenziell anzeigen document.getElementById('tempOpeningTask').textContent = _state.activeTask || '';
const code = _state.tempOpeningCode;
if (code) {
document.getElementById('tempOpeningCode').textContent = code;
show('tempOpeningCodeRow');
} else {
hide('tempOpeningCodeRow');
}
show('tempOpeningBox');
}
async function doEndTempOpening() {
try { try {
const r = await fetch('/lock-game/release-locks'); await fetch('/lock-game/end-temp-opening', { method: 'POST' });
if (r.ok) { await fetch('/lock-game/abandon-task', { method: 'POST' });
const texts = await r.json(); const stateR = await fetch('/lock-game/state');
for (const text of texts) { _state = await stateR.json();
await waitForReleaseOk(text); hide('tempOpeningBox');
} await runGameLoop();
} } catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); }
} catch (_) { /* ignorieren */ } }
// 2. Finisher laden und Zeit messen function showFinisherUI() {
const finisherStartTime = Date.now(); show('gameBox');
let finisher = null; hide('gameCard');
try { hide('lockReleaseBox');
const r = await fetch('/lock-game/finisher');
if (r.ok) finisher = await r.json();
} catch (_) { /* ignorieren */ }
document.getElementById('finisherTitle').textContent = finisher?.kurzText || ''; let finisher = {};
document.getElementById('finisherText').textContent = finisher?.text || 'Glückwunsch du hast Level 6 erreicht!'; try { finisher = JSON.parse(_state.finisher); } catch (_) {}
document.getElementById('finisherTitle').textContent = finisher.kurzText || '';
document.getElementById('finisherText').textContent = finisher.text || '';
// 3. Warten bis Nutzer OK drückt if (_state.finisherStartedAt) {
await new Promise(resolve => { hide('finisherStart');
document.getElementById('btnFinisherOk').onclick = resolve; show('finisherRunning');
show('finisherBox'); startElapsedTimer(new Date(_state.finisherStartedAt));
}); } else {
show('finisherStart');
hide('finisherRunning');
}
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' }); hide('finisherStart');
show('finisherRunning');
startElapsedTimer(new Date(_state.finisherStartedAt));
}
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');
_timerInt = setInterval(() => {
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;
}, 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();
@@ -658,7 +758,37 @@
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>
<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>
</body> </body>
</html> </html>

View File

@@ -29,7 +29,7 @@ class CardLockServiceTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
lock = new CardLockEntity(); lock = new CardLockEntity();
lock.setPickEveryMinute(60); lock.setPickEverySeconds(60);
lock.setNextCardIn(LocalDateTime.now().minusMinutes(1)); lock.setNextCardIn(LocalDateTime.now().minusMinutes(1));
// controllType bleibt null → lockControlFactory.create() wird nicht aufgerufen // controllType bleibt null → lockControlFactory.create() wird nicht aufgerufen