diff --git a/bin/main/db/migration/V8__lock_game_aufgaben_text.sql b/bin/main/db/migration/V8__lock_game_aufgaben_text.sql new file mode 100644 index 0000000..7c9a1a0 --- /dev/null +++ b/bin/main/db/migration/V8__lock_game_aufgaben_text.sql @@ -0,0 +1,2 @@ +ALTER TABLE lock_game + MODIFY COLUMN aufgaben TEXT NULL; diff --git a/bin/main/de/oaa/xxx/games/bdsm/BdsmGameService.class b/bin/main/de/oaa/xxx/games/bdsm/BdsmGameService.class index 6d74e30..1273c5b 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/BdsmGameService.class and b/bin/main/de/oaa/xxx/games/bdsm/BdsmGameService.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/AufgabeController.class b/bin/main/de/oaa/xxx/games/bdsm/controller/AufgabeController.class index 6ee5090..6727f3f 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/AufgabeController.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/AufgabeController.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController.class b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController.class index cba4c4a..c13117c 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/BdsmGameController.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/FinisherController.class b/bin/main/de/oaa/xxx/games/bdsm/controller/FinisherController.class index fb0681c..90fc8f6 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/FinisherController.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/FinisherController.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/SperreController.class b/bin/main/de/oaa/xxx/games/bdsm/controller/SperreController.class index 5007d0d..0cfe94a 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/SperreController.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/SperreController.class differ diff --git a/bin/main/de/oaa/xxx/games/bdsm/controller/StrafeController.class b/bin/main/de/oaa/xxx/games/bdsm/controller/StrafeController.class index 2bf396f..d4eb608 100644 Binary files a/bin/main/de/oaa/xxx/games/bdsm/controller/StrafeController.class and b/bin/main/de/oaa/xxx/games/bdsm/controller/StrafeController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class index 796a7e0..fa24030 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$AssignTaskRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$CreateCardLockRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$CreateCardLockRequest.class index 2e38a51..574fddf 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$CreateCardLockRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$CreateCardLockRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class index e3b467a..6989bb8 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$FreezeRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class index ee9f477..056d2a6 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$ModifyCardsRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$SpeedConfirmRequest.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$SpeedConfirmRequest.class index 4a66943..ace8387 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$SpeedConfirmRequest.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController$SpeedConfirmRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class index a7863f5..c9c31c9 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.class index c2e2603..945b555 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockService.class b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockService.class index cc03d99..2677c8d 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockService.class and b/bin/main/de/oaa/xxx/games/chastity/cardlock/CardLockService.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockEntity.class b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockEntity.class index c199663..166de8c 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockEntity.class and b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockHelper.class b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockHelper.class new file mode 100644 index 0000000..b804fa6 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockHelper.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockService.class b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockService.class index 668d040..1552c2e 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/BaseLockService.class and b/bin/main/de/oaa/xxx/games/chastity/common/BaseLockService.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/GameState.class b/bin/main/de/oaa/xxx/games/chastity/common/GameState.class index 4b3c8ba..b754936 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/GameState.class and b/bin/main/de/oaa/xxx/games/chastity/common/GameState.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/LockGameController.class b/bin/main/de/oaa/xxx/games/chastity/common/LockGameController.class index 32b5510..f95aa89 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/LockGameController.class and b/bin/main/de/oaa/xxx/games/chastity/common/LockGameController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/LockGameEntity.class b/bin/main/de/oaa/xxx/games/chastity/common/LockGameEntity.class index 4e0c38a..db34e38 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/LockGameEntity.class and b/bin/main/de/oaa/xxx/games/chastity/common/LockGameEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/LockGameLockEntity.class b/bin/main/de/oaa/xxx/games/chastity/common/LockGameLockEntity.class index 70bb24a..39bf891 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/LockGameLockEntity.class and b/bin/main/de/oaa/xxx/games/chastity/common/LockGameLockEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/LockGameService$1.class b/bin/main/de/oaa/xxx/games/chastity/common/LockGameService$1.class new file mode 100644 index 0000000..2043ee3 Binary files /dev/null and b/bin/main/de/oaa/xxx/games/chastity/common/LockGameService$1.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/common/LockGameService.class b/bin/main/de/oaa/xxx/games/chastity/common/LockGameService.class index 872fc59..d9ed07d 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/common/LockGameService.class and b/bin/main/de/oaa/xxx/games/chastity/common/LockGameService.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.class b/bin/main/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.class index c5c9c9e..58eb67c 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.class and b/bin/main/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.class b/bin/main/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.class index de035b1..4a276a2 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.class and b/bin/main/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockAdditionalSettings.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockAdditionalSettings.class index d16eb07..474bea0 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockAdditionalSettings.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockAdditionalSettings.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$CreateTimeLockRequest.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$CreateTimeLockRequest.class index 9d49d31..dc71129 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$CreateTimeLockRequest.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$CreateTimeLockRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$FreezeRequest.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$FreezeRequest.class index c1ceaa8..4f52308 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$FreezeRequest.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController$FreezeRequest.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController.class index 3c29255..c3adeff 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockController.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.class index 70ec571..54134e0 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockService.class b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockService.class index f5f3c77..bc82577 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockService.class and b/bin/main/de/oaa/xxx/games/chastity/timelock/TimeLockService.class differ diff --git a/bin/main/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.class b/bin/main/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.class index 4dcc16b..d17fef3 100644 Binary files a/bin/main/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.class and b/bin/main/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.class differ diff --git a/bin/main/de/oaa/xxx/games/common/aufgaben/Finisher.class b/bin/main/de/oaa/xxx/games/common/aufgaben/Finisher.class index e0a1d1a..a2324ed 100644 Binary files a/bin/main/de/oaa/xxx/games/common/aufgaben/Finisher.class and b/bin/main/de/oaa/xxx/games/common/aufgaben/Finisher.class differ diff --git a/bin/main/de/oaa/xxx/games/common/aufgaben/Sperre.class b/bin/main/de/oaa/xxx/games/common/aufgaben/Sperre.class index b92ef14..9960d63 100644 Binary files a/bin/main/de/oaa/xxx/games/common/aufgaben/Sperre.class and b/bin/main/de/oaa/xxx/games/common/aufgaben/Sperre.class differ diff --git a/bin/main/de/oaa/xxx/games/common/entity/FinisherEntity.class b/bin/main/de/oaa/xxx/games/common/entity/FinisherEntity.class index 7a1a14b..d461d08 100644 Binary files a/bin/main/de/oaa/xxx/games/common/entity/FinisherEntity.class and b/bin/main/de/oaa/xxx/games/common/entity/FinisherEntity.class differ diff --git a/bin/main/de/oaa/xxx/games/common/entity/SperreEntity.class b/bin/main/de/oaa/xxx/games/common/entity/SperreEntity.class index 765dca3..13456b7 100644 Binary files a/bin/main/de/oaa/xxx/games/common/entity/SperreEntity.class and b/bin/main/de/oaa/xxx/games/common/entity/SperreEntity.class differ diff --git a/bin/main/static/games/aufgaben/aufgaben.html b/bin/main/static/games/aufgaben/aufgaben.html index 5c4251e..25f6230 100644 --- a/bin/main/static/games/aufgaben/aufgaben.html +++ b/bin/main/static/games/aufgaben/aufgaben.html @@ -263,6 +263,12 @@ 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-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 { 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; @@ -533,17 +539,11 @@
- -
- - -
+
@@ -765,7 +765,7 @@ ${g.beschreibung ? `
${esc(g.beschreibung)}
` : ''} ${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, 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)}
`; @@ -814,6 +814,7 @@ function renderAufgabe(a, type, gruppenId) { _itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId }; const badges = []; + (a.benoetigteToys || []).forEach(t => badges.push(`${esc(t.name || t)}`)); const zeit = formatSek(a.sekundenVon, a.sekundenBis); if (zeit) badges.push(`${esc(zeit)}`); if (a.level != null) badges.push(`Level ${esc(String(a.level))}`); @@ -826,6 +827,7 @@ const actionBtns = type === 'user' ? `
+
` : ''; @@ -841,6 +843,7 @@ function renderStrafe(s, type, gruppenId) { _itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId }; const badges = []; + (s.benoetigteToys || []).forEach(t => badges.push(`${esc(t.name || t)}`)); const zeit = formatSek(s.sekundenVon, s.sekundenBis); if (zeit) badges.push(`${esc(zeit)}`); if (s.level != null) badges.push(`Level ${esc(String(s.level))}`); @@ -853,6 +856,7 @@ const actionBtns = type === 'user' ? `
+
` : ''; @@ -868,8 +872,10 @@ function renderZeitstrafe(z, type, gruppenId) { _itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId }; const badges = []; + (z.benoetigteToys || []).forEach(t => badges.push(`${esc(t.name || t)}`)); const zeit = formatMin(z.minutenVon, z.minutenBis); if (zeit) badges.push(`${esc(zeit)}`); + if (z.level != null) badges.push(`Level ${esc(String(z.level))}`); const detailRows = []; if (z.text) detailRows.push(`
${esc(z.text)}
`); @@ -879,6 +885,7 @@ const actionBtns = type === 'user' ? `
+
` : ''; @@ -896,6 +903,7 @@ function renderFinisher(f, type, gruppenId) { _itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId }; const badges = []; + (f.benoetigteToys || []).forEach(t => badges.push(`${esc(t.name || t)}`)); if (f.geschlecht) badges.push(`${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}`); const detailRows = []; @@ -906,6 +914,7 @@ const actionBtns = type === 'user' ? `
+
` : ''; @@ -944,10 +953,36 @@ finisher: apiUrl('/finisher') }; 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) { 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]; if (!deleteUrl) return; const body = { [ITEM_DELETE_FIELD[kind]]: itemId }; @@ -1430,7 +1465,7 @@ const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label'); 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'; } @@ -1449,8 +1484,7 @@ document.querySelectorAll('#iWerkzeugFinisherPassiv 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.getElementById('iTempUnlockBefore').checked = false; - document.getElementById('iTempUnlockAfter').checked = false; + document.getElementById('iTempUnlockRequired').checked = false; _selectedToys = []; renderSelectedToys(); document.getElementById('itemModalError').style.display = 'none'; @@ -1495,6 +1529,9 @@ const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`); if (rb) rb.checked = true; } + if (_isChastityMode) { + document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true; + } } else { document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : ''; 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; }); if (_isChastityMode) { document.getElementById('iLevel').value = d.level != null ? d.level : ''; - document.getElementById('iTempUnlockBefore').checked = d.tempUnlockBeforeRequired === true; - document.getElementById('iTempUnlockAfter').checked = d.tempUnlockAfterRequired === true; + document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true; } } @@ -1666,11 +1702,12 @@ if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; } payload = { kurzText, text, - geschlecht: geschlecht || null, - gruppeId: isEdit ? undefined : currentItemGruppeId, - benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'), - benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'), - benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })) + geschlecht: geschlecht || null, + gruppeId: isEdit ? undefined : currentItemGruppeId, + benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'), + benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'), + benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })), + tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null }; url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher'); method = isEdit ? 'PUT' : 'POST'; @@ -1698,8 +1735,7 @@ releaseText: document.getElementById('iReleaseText').value.trim() || null, sperreFuer, level: zeitLevel, - tempUnlockBeforeRequired: _isChastityMode ? document.getElementById('iTempUnlockBefore').checked : null, - tempUnlockAfterRequired: _isChastityMode ? document.getElementById('iTempUnlockAfter').checked : null, + tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null, benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })) }; url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only diff --git a/bin/main/static/games/bdsm/neubdsm.html b/bin/main/static/games/bdsm/neubdsm.html index 965d1a1..ee791ef 100644 --- a/bin/main/static/games/bdsm/neubdsm.html +++ b/bin/main/static/games/bdsm/neubdsm.html @@ -679,6 +679,7 @@ // ── Gruppe lists ── function renderGruppeList(containerId, gruppen) { + gruppen = gruppen.filter(g => g.availableIn !== 'CHASTITY_ONLY'); const ul = document.getElementById(containerId); const section = ul.closest('[id^="section"]'); const selectAllWrap = section?.querySelector('.select-all-label'); diff --git a/bin/main/static/games/chastity/neulock.html b/bin/main/static/games/chastity/neulock.html index 85885ba..576ad31 100644 --- a/bin/main/static/games/chastity/neulock.html +++ b/bin/main/static/games/chastity/neulock.html @@ -256,9 +256,14 @@
- +
+
@@ -515,6 +520,7 @@ khInput.readOnly = true; khInput.style.opacity = '0.6'; document.getElementById('rowTestLock').style.display = 'none'; + document.getElementById('rowSpeedFactor').style.display = 'none'; document.getElementById('rowDetailsVisible').style.display = ''; } else { khInput.readOnly = false; @@ -728,6 +734,15 @@ 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 ── async function createSession() { document.getElementById('errorMsg').style.display = 'none'; @@ -756,6 +771,7 @@ const isFriendLockee = lockeeVal && lockeeVal !== myUserId; const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5); const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked; + const speedFactor = isTestLock ? parseInt(document.getElementById('speedFactor').value) : 1; let endpoint, body; @@ -769,6 +785,7 @@ testLock: isTestLock, unlockCodeLength: unlockCodeLen, controllType: selectedLockControl, + speedFactor: speedFactor, }; } else { // CardLock @@ -798,6 +815,7 @@ controllType: selectedLockControl, gameSetId: t.gameSetId || null, gameSpieldauerIdx: t.gameSpieldauerIdx ?? null, + speedFactor: speedFactor, }; } diff --git a/bin/main/static/games/chastity/taskgame.html b/bin/main/static/games/chastity/taskgame.html index 8a6aeef..b2760fa 100644 --- a/bin/main/static/games/chastity/taskgame.html +++ b/bin/main/static/games/chastity/taskgame.html @@ -53,6 +53,48 @@ 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 { display: flex; justify-content: center; @@ -190,6 +232,7 @@ + + + @@ -340,6 +402,7 @@ async function startWithExcludedToys(gameSetId, excludedToyIds) { const params = new URLSearchParams({ aufgabenGruppeId: gameSetId }); + if (lockId) params.append('lockId', lockId); excludedToyIds.forEach(id => params.append('excludedToyIds', id)); const r = await fetch('/lock-game/init?' + params.toString(), { method: 'POST' }); @@ -417,6 +480,27 @@ 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 = '
Benötigt
' + + list.map(w => { + const label = WERKZEUG_LABEL[w] || w; + return ``; + }).join(''); + box.style.display = 'flex'; + } + // ── Game Loop ───────────────────────────────────────────────────────────── function setGameCard(label, text, action, btnLabel) { @@ -432,10 +516,16 @@ async function runGameLoop() { hide('gameCard'); hide('finisherBox'); + hide('tempOpeningBox'); clearTimer(); - if (_state.level >= 6) { - await showFinisherFlow(); + if (_state.finisher) { + showFinisherUI(); + return; + } + + if (_state.tempOpeningTime) { + showTempOpeningDialog(); return; } @@ -460,12 +550,14 @@ let sperre; try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; } setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten'); + renderRequirements(null); } else if (_state.taskInQueue) { let aufgabe; try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; } const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis); setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done', hasDuration ? '▶ Starten' : '✓ Erledigt'); + renderRequirements(aufgabe.benoetigtAktiv); } show('gameBox'); show('gameCard'); @@ -479,6 +571,7 @@ timerEl.textContent = ''; document.getElementById('gameLabel').textContent = 'Aktive Aufgabe'; document.getElementById('gameText').textContent = text; + renderRequirements(_state.activeTaskBenoetigtAktiv); if (endIso) { const end = new Date(endIso); @@ -504,12 +597,31 @@ async function doQueueStart() { 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' }); if (!r.ok) { showError('Fehler beim Starten'); return; } - const stateR = await fetch('/lock-game/state'); - _state = await stateR.json(); - await runGameLoop(); + + if (wasLock && tempUnlockRequired) { + 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)'); } } @@ -526,16 +638,28 @@ } catch (e) { showError(e.message || 'Fehler (Erledigt)'); } } - function doCancelCountdown() { + async function doCancelCountdown() { 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'; document.getElementById('gameBtn').textContent = '✓ Erledigt'; } async function doErledigt() { try { - await checkAndShowLocks(); - if (_state.level >= 6) { await showFinisherFlow(); return; } + 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); + } + } const r = await fetch('/lock-game/next-task', { method: 'POST' }); if (!r.ok) { showError('Fehler beim Ziehen'); return; } const stateR = await fetch('/lock-game/state'); @@ -557,51 +681,87 @@ } } - async function showFinisherFlow() { + function showTempOpeningDialog() { show('gameBox'); hide('gameCard'); hide('lockReleaseBox'); 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 { - const r = await fetch('/lock-game/release-locks'); - if (r.ok) { - const texts = await r.json(); - for (const text of texts) { - await waitForReleaseOk(text); - } - } - } catch (_) { /* ignorieren */ } + await fetch('/lock-game/end-temp-opening', { method: 'POST' }); + await fetch('/lock-game/abandon-task', { method: 'POST' }); + const stateR = await fetch('/lock-game/state'); + _state = await stateR.json(); + hide('tempOpeningBox'); + await runGameLoop(); + } catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); } + } - // 2. Finisher laden und Zeit messen - const finisherStartTime = Date.now(); - let finisher = null; - try { - const r = await fetch('/lock-game/finisher'); - if (r.ok) finisher = await r.json(); - } catch (_) { /* ignorieren */ } + function showFinisherUI() { + show('gameBox'); + hide('gameCard'); + hide('lockReleaseBox'); - document.getElementById('finisherTitle').textContent = finisher?.kurzText || ''; - document.getElementById('finisherText').textContent = finisher?.text || 'Glückwunsch – du hast Level 6 erreicht!'; + let finisher = {}; + 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 - await new Promise(resolve => { - document.getElementById('btnFinisherOk').onclick = resolve; - show('finisherBox'); - }); + if (_state.finisherStartedAt) { + hide('finisherStart'); + show('finisherRunning'); + startElapsedTimer(new Date(_state.finisherStartedAt)); + } else { + show('finisherStart'); + hide('finisherRunning'); + } + show('finisherBox'); + } - // 4. Zeit berechnen und Spiel beenden - const timeInMinutes = Math.round((Date.now() - finisherStartTime) / 60000); - const params = new URLSearchParams({ timeInMinutes }); - if (lockId) params.set('lockId', lockId); - await fetch('/lock-game/complete?' + params.toString(), { method: 'POST' }); + async function doStartFinisher() { + await fetch('/lock-game/start-finisher', { method: 'POST' }); + const r = await fetch('/lock-game/state'); + _state = await r.json(); + 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(); } + 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) { return new Promise(resolve => { - document.getElementById('releaseText').textContent = text; + hide('gameCard'); + document.getElementById('releaseText').textContent = text || ''; document.getElementById('btnReleaseOk').onclick = () => { hide('lockReleaseBox'); resolve(); diff --git a/bin/test/de/oaa/xxx/games/chastity/cardlock/CardLockServiceTest.class b/bin/test/de/oaa/xxx/games/chastity/cardlock/CardLockServiceTest.class index 8097cd2..d54f676 100644 Binary files a/bin/test/de/oaa/xxx/games/chastity/cardlock/CardLockServiceTest.class and b/bin/test/de/oaa/xxx/games/chastity/cardlock/CardLockServiceTest.class differ diff --git a/src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java b/src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java index 21fd5cc..339f7e8 100644 --- a/src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java +++ b/src/main/java/de/oaa/xxx/games/bdsm/BdsmGameService.java @@ -127,12 +127,12 @@ public class BdsmGameService { newLock.setLockee(lockeeUserId); newLock.setKeyholder(keyholderUserId); newLock.setInitialCards(template.getInitialCards()); - newLock.setPickEveryMinute(template.getPickEveryMinute()); + newLock.setPickEverySeconds(template.getPickEverySeconds()); newLock.setAccumulatePicks(template.isAccumulatePicks()); newLock.setShowRemainingCards(template.isShowRemainingCards()); newLock.setLatestOpeningtime(template.getLatestOpeningtime()); - newLock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes()); - newLock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites()); + newLock.setHygineOpeningDurationSeconds(template.getHygineOpeningDurationSeconds()); + newLock.setHygineOpeningEverySeconds(template.getHygineOpeningEverySeconds()); newLock.setTasks(template.getTasks()); newLock.setRequiresVerification(template.isRequiresVerification()); newLock.setTestLock(false); @@ -149,10 +149,10 @@ public class BdsmGameService { newLock.setAvailableCards(template.getInitialCards() != null ? new ArrayList<>(template.getInitialCards()) : new ArrayList<>()); newLock.setOpenPicks(0); - if (template.getPickEveryMinute() != null) { - newLock.setNextCardIn(now.plusMinutes(template.getPickEveryMinute())); + if (template.getPickEverySeconds() != null) { + newLock.setNextCardIn(now.plusSeconds(template.getPickEverySeconds())); } - if (template.getHygineOpeningEveryMinites() != null) { + if (template.getHygineOpeningEverySeconds() != null) { newLock.setLastHygineOpening(now); } cardlockRepository.save(newLock); diff --git a/src/main/java/de/oaa/xxx/games/bdsm/controller/AufgabeController.java b/src/main/java/de/oaa/xxx/games/bdsm/controller/AufgabeController.java index 476b237..bc68740 100644 --- a/src/main/java/de/oaa/xxx/games/bdsm/controller/AufgabeController.java +++ b/src/main/java/de/oaa/xxx/games/bdsm/controller/AufgabeController.java @@ -103,6 +103,28 @@ public class AufgabeController { return ResponseEntity.ok().build(); } + @PostMapping("/copy/{aufgabeId}") + public ResponseEntity 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 public ResponseEntity delete(@RequestBody Aufgabe aufgabe) { try { diff --git a/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmGameController.java b/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmGameController.java index 6a3ac01..5b05c79 100644 --- a/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmGameController.java +++ b/src/main/java/de/oaa/xxx/games/bdsm/controller/BdsmGameController.java @@ -538,7 +538,7 @@ public class BdsmGameController extends BaseController { Map item = new LinkedHashMap<>(); item.put("lockId", l.getLockId()); 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("active", l.getStartTime() != null && l.getUnlockTime() == null); return item; diff --git a/src/main/java/de/oaa/xxx/games/bdsm/controller/FinisherController.java b/src/main/java/de/oaa/xxx/games/bdsm/controller/FinisherController.java index c6fe989..2244924 100644 --- a/src/main/java/de/oaa/xxx/games/bdsm/controller/FinisherController.java +++ b/src/main/java/de/oaa/xxx/games/bdsm/controller/FinisherController.java @@ -88,11 +88,32 @@ public class FinisherController { entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv()); entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv()); entity.setBenoetigteToys(resolveToys(finisher.getBenoetigteToys())); + entity.setTempUnlockRequired(finisher.getTempUnlockRequired()); finisherRepository.save(entity); LOGGER.debug("Finisher {} aktualisiert", finisherId); return ResponseEntity.ok().build(); } + @PostMapping("/copy/{finisherId}") + public ResponseEntity 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 public ResponseEntity delete(@RequestBody Finisher finisher) { try { diff --git a/src/main/java/de/oaa/xxx/games/bdsm/controller/SperreController.java b/src/main/java/de/oaa/xxx/games/bdsm/controller/SperreController.java index 83f6ce1..fd6111c 100644 --- a/src/main/java/de/oaa/xxx/games/bdsm/controller/SperreController.java +++ b/src/main/java/de/oaa/xxx/games/bdsm/controller/SperreController.java @@ -56,7 +56,7 @@ public class SperreController { @PostMapping public ResponseEntity create(@RequestBody Sperre sperre) { 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(); } AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(sperre.getGruppeId()).orElse(null); @@ -77,8 +77,7 @@ public class SperreController { @PutMapping("/{sperreId}") public ResponseEntity update(@PathVariable("sperreId") UUID sperreId, @RequestBody Sperre sperre) { - if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null - || sperre.getSperreFuer() == null || sperre.getSperreFuer().isEmpty()) { + if (sperre.getKurzText() == null || sperre.getText() == null || sperre.getMinutenVon() == null) { return ResponseEntity.badRequest().build(); } SperreEntity entity = sperreRepository.findById(sperreId).orElse(null); @@ -89,8 +88,7 @@ public class SperreController { entity.setMinutenVon(sperre.getMinutenVon()); entity.setMinutenBis(sperre.getMinutenBis()); entity.setLevel(sperre.getLevel()); - entity.setTempUnlockBeforeRequired(sperre.getTempUnlockBeforeRequired()); - entity.setTempUnlockAfterRequired(sperre.getTempUnlockAfterRequired()); + entity.setTempUnlockRequired(sperre.getTempUnlockRequired()); entity.setSperreFuer(sperre.getSperreFuer()); entity.setBenoetigteToys(resolveToys(sperre.getBenoetigteToys())); sperreRepository.save(entity); @@ -98,6 +96,28 @@ public class SperreController { return ResponseEntity.ok().build(); } + @PostMapping("/copy/{sperreId}") + public ResponseEntity 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 public ResponseEntity delete(@RequestBody Sperre sperre) { try { diff --git a/src/main/java/de/oaa/xxx/games/bdsm/controller/StrafeController.java b/src/main/java/de/oaa/xxx/games/bdsm/controller/StrafeController.java index c1b3e09..720aed0 100644 --- a/src/main/java/de/oaa/xxx/games/bdsm/controller/StrafeController.java +++ b/src/main/java/de/oaa/xxx/games/bdsm/controller/StrafeController.java @@ -94,6 +94,27 @@ public class StrafeController { return ResponseEntity.ok().build(); } + @PostMapping("/copy/{strafeId}") + public ResponseEntity 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 public ResponseEntity delete(@RequestBody Strafe strafe) { try { diff --git a/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java b/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java index c74e72c..d5dd892 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java +++ b/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockController.java @@ -125,7 +125,8 @@ public class CardLockController { List initialCards, Integer pickEveryMinute, boolean accumulatePicks, boolean showRemainingCards, LocalDateTime latestOpeningtime, Integer hygineOpeningDurationMinutes, Integer hygineOpeningEveryMinites, List 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(); @@ -166,12 +167,12 @@ public class CardLockController { lock.setLockee(lockee.getUserId()); lock.setKeyholder(myId); lock.setInitialCards(req.initialCards()); - lock.setPickEveryMinute(req.pickEveryMinute()); + lock.setPickEverySeconds(req.pickEveryMinute() * 60); lock.setAccumulatePicks(req.accumulatePicks()); lock.setShowRemainingCards(req.showRemainingCards()); lock.setLatestOpeningtime(req.latestOpeningtime()); - lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); - lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites()); + lock.setHygineOpeningDurationSeconds(req.hygineOpeningDurationMinutes() != null ? req.hygineOpeningDurationMinutes() * 60 : null); + lock.setHygineOpeningEverySeconds(req.hygineOpeningEveryMinites() != null ? req.hygineOpeningEveryMinites() * 60 : null); lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); lock.setRequiresVerification(req.requiresVerification()); lock.setTestLock(false); @@ -206,21 +207,23 @@ public class CardLockController { } 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(); lock.setName(req.name()); lock.setLockee(myId); lock.setKeyholder(null); // set only after invitation is confirmed lock.setInitialCards(req.initialCards()); - lock.setPickEveryMinute(req.pickEveryMinute()); + lock.setPickEverySeconds(req.pickEveryMinute() * 60); lock.setAccumulatePicks(req.accumulatePicks()); lock.setShowRemainingCards(req.showRemainingCards()); lock.setLatestOpeningtime(req.latestOpeningtime()); - lock.setHygineOpeningDurationMinutes(req.hygineOpeningDurationMinutes()); - lock.setHygineOpeningEveryMinites(req.hygineOpeningEveryMinites()); + lock.setHygineOpeningDurationSeconds(req.hygineOpeningDurationMinutes() != null ? req.hygineOpeningDurationMinutes() * 60 : null); + lock.setHygineOpeningEverySeconds(req.hygineOpeningEveryMinites() != null ? req.hygineOpeningEveryMinites() * 60 : null); lock.setTasks(req.tasks() != null ? req.tasks() : List.of()); lock.setRequiresVerification(req.requiresVerification()); lock.setTestLock(req.testLock()); + lock.setSpeedFactor(sf > 1 ? sf : null); lock.setTaskMode(req.taskMode() != null ? req.taskMode() : TaskMode.RANDOM); lock.setUnlockCodeLength(codeLines); lock.setControllType(controllType); @@ -231,8 +234,9 @@ public class CardLockController { lock.setStartTime(now); lock.setAvailableCards(new ArrayList<>(req.initialCards())); lock.setOpenPicks(0); - lock.setNextCardIn(now.plusMinutes(req.pickEveryMinute())); - if (req.hygineOpeningEveryMinites() != null) { + long firstCardSeconds = sf > 1 ? Math.max(6L, req.pickEveryMinute() * 60L / sf) : req.pickEveryMinute() * 60L; + lock.setNextCardIn(now.plusSeconds(firstCardSeconds)); + if (req.hygineOpeningEveryMinites() != null) { // stored as seconds already above lock.setLastHygineOpening(now); } cardlockRepository.save(lock); // erst speichern, damit Lock-ID vorhanden ist @@ -243,7 +247,7 @@ public class CardLockController { "🃏 Deine erste Karte ist bereit – jetzt ziehen!", "/games/chastity/activelock.html?lockId=" + lock.getLockId(), de.oaa.xxx.social.entity.MessageCause.GAME_STATE, - now.plusMinutes(req.pickEveryMinute())); + now.plusSeconds(firstCardSeconds)); // Initialen Unlock-Code / TTLock-PIN via LockControl setzen CardLockService initService = cardLockServiceFactory.create(lock); @@ -307,9 +311,10 @@ public class CardLockController { result.put("taskPending", taskPending); // 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 ? l.getNextCardIn() - : LocalDateTime.now().plusMinutes(l.getPickEveryMinute()); + : LocalDateTime.now().plusSeconds(drawSf > 1 ? Math.max(6L, l.getPickEverySeconds() / drawSf) : l.getPickEverySeconds()); systemMessageService.sendScheduled( myId, myId, "🃏 Deine nächste Karte ist bereit – jetzt ziehen!", @@ -345,7 +350,7 @@ public class CardLockController { cardLockServiceFactory.create(l).startHygieneOpening(); 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)); } @@ -471,13 +476,15 @@ public class CardLockController { long totalCards = l.getAvailableCards() != null ? l.getAvailableCards().size() : 0; // Hygiene-Berechnung - boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; + boolean hygieneEnabled = l.getHygineOpeningEverySeconds() != null; boolean hygieneOpeningDue = false; long hygieneSecondsRemaining = 0; 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(); if (base != null) { - LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites()); + LocalDateTime nextHygiene = base.plusSeconds(hygineSeconds); hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene); hygieneOpeningDue = hygieneSecondsRemaining <= 0; } @@ -507,7 +514,7 @@ public class CardLockController { result.put("hygieneOpeningStarted", l.getTempOpeningTime() != null ? l.getTempOpeningTime().toString() : null); result.put("hygieneDurationMinutes", - l.getHygineOpeningDurationMinutes() != null ? l.getHygineOpeningDurationMinutes() : 0); + l.getHygineOpeningDurationSeconds() != null ? l.getHygineOpeningDurationSeconds() / 60 : 0); result.put("hasKeyholder", l.getKeyholder() != null); result.put("keyholderInvitationPending", l.getKeyholder() == null && !invitationRepository.findByLockId(l.getLockId()).isEmpty()); @@ -680,7 +687,9 @@ public class CardLockController { UUID gameSetId = l.getGameSetId(); l.setGameCardParkedAt(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); cardlockRepository.save(l); @@ -976,13 +985,13 @@ public class CardLockController { l.getAvailableCards().forEach(c -> cardCounts.merge(c.name(), 1L, Long::sum)); } - boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; + boolean hygieneEnabled = l.getHygineOpeningEverySeconds() != null; boolean hygieneOpeningDue = false; long hygieneSecondsRemaining = 0; if (hygieneEnabled) { LocalDateTime base = l.getLastHygineOpening() != null ? l.getLastHygineOpening() : l.getStartTime(); if (base != null) { - LocalDateTime nextHygiene = base.plusMinutes(l.getHygineOpeningEveryMinites()); + LocalDateTime nextHygiene = base.plusSeconds(l.getHygineOpeningEverySeconds()); hygieneSecondsRemaining = ChronoUnit.SECONDS.between(LocalDateTime.now(), nextHygiene); hygieneOpeningDue = hygieneSecondsRemaining <= 0; } diff --git a/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.java b/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.java index c827d2c..0649ffe 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.java +++ b/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockEntity.java @@ -24,7 +24,7 @@ public class CardLockEntity extends BaseLockEntity { @Column(columnDefinition = "TEXT") private List initialCards; @Column - private Integer pickEveryMinute; + private Integer pickEverySeconds; @Column private boolean accumulatePicks; @Column diff --git a/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java b/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java index 4ba2516..b015691 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java +++ b/src/main/java/de/oaa/xxx/games/chastity/cardlock/CardLockService.java @@ -98,12 +98,12 @@ public class CardLockService extends BaseLockService implements LockControlCallb @Override protected void applyHygieneOvertime(Long overtime) { - long penalty = Math.round(overtime * 4 * getTimeMultiplier()); - LOGGER.debug("Apply {} Minutes Overtime (penalty: {})", overtime, penalty); + long penaltySeconds = Math.round(overtime * 4 * 60.0 * getTimeMultiplier()); + LOGGER.debug("Apply {} Minutes Overtime (penalty: {} seconds)", overtime, penaltySeconds); if (lock.getFrozenUntil() != null) { - lock.setFrozenUntil(lock.getFrozenUntil().plusMinutes(penalty)); + lock.setFrozenUntil(lock.getFrozenUntil().plusSeconds(penaltySeconds)); } else { - lock.setFrozenUntil(LocalDateTime.now().plusMinutes(penalty)); + lock.setFrozenUntil(LocalDateTime.now().plusSeconds(penaltySeconds)); } LOGGER.debug("Frozen until {}", lock.getFrozenUntil()); } @@ -128,7 +128,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb } } else { 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(); } } @@ -175,16 +175,16 @@ public class CardLockService extends BaseLockService implements LockControlCallb } 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); return ""; } - private String freeze(double multiplier) { - LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES); + private String freeze(double seconds) { + LocalDateTime frozenTill = LocalDateTime.now().plus((long) seconds, ChronoUnit.SECONDS); lock.setFrozenUntil(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 ""; } @@ -247,9 +247,9 @@ public class CardLockService extends BaseLockService implements LockControlCallb } public void startHygieneOpening() { - int base = lock.getHygineOpeningDurationMinutes() != null ? lock.getHygineOpeningDurationMinutes() : 30; - int duration = (int) Math.round(base * getTimeMultiplier()); - startTempOpening(TempOpeningReason.HYGIENE, duration); + int baseSecs = lock.getHygineOpeningDurationSeconds() != null ? lock.getHygineOpeningDurationSeconds() : (30 * 60); + int durationMinutes = (int) Math.round(baseSecs / 60.0 * getTimeMultiplier()); + startTempOpening(TempOpeningReason.HYGIENE, durationMinutes); } // ── Cum cards ───────────────────────────────────────────────────────────── @@ -313,23 +313,24 @@ public class CardLockService extends BaseLockService implements LockControlCallb private double getTimeMultiplier() { LocalDateTime now = LocalDateTime.now(); + double sf = (lock.getSpeedFactor() != null && lock.getSpeedFactor() > 1) ? lock.getSpeedFactor() : 1.0; if (lock.getSpeedupUntil() != null && lock.getSpeedupUntil().isAfter(now)) { - return 0.25; + return 0.25 / sf; } if (lock.getSlowmoUntil() != null && lock.getSlowmoUntil().isAfter(now)) { - return 4.0; + return 4.0 / sf; } - return 1.0; + return 1.0 / sf; } @Override - protected void handleLockGameFinished(int timeInMinutes) { - int freezeTime = (int) (timeInMinutes * new Random().nextDouble(1.0, 4.0)); - freeze(freezeTime); + protected void handleLockGameFinished(int timeInSeconds) { + double freezeSeconds = timeInSeconds * new Random().nextDouble(1.0, 4.0); + freeze(freezeSeconds); } @Override public void penaltyLockGame() { - handleLockGameFinished(60); + handleLockGameFinished(3600); } } diff --git a/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockEntity.java b/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockEntity.java index 593499e..dd5c65c 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockEntity.java +++ b/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockEntity.java @@ -64,9 +64,9 @@ public class BaseLockEntity { @Column private LocalDateTime lastHygineOpening; @Column - private Integer hygineOpeningDurationMinutes; + private Integer hygineOpeningDurationSeconds; @Column - private Integer hygineOpeningEveryMinites; + private Integer hygineOpeningEverySeconds; @Column private LocalDateTime tempOpeningTime; // If null, not while hygine opening @Column @@ -90,6 +90,9 @@ public class BaseLockEntity { @Column(nullable = false) private TaskMode taskMode = TaskMode.RANDOM; + @Column + private Integer speedFactor; + // --- Notfall- & Keyholder-Status --- @Column(nullable = false) private boolean keyholderRequestedUnlock = false; diff --git a/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockHelper.java b/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockHelper.java new file mode 100644 index 0000000..713d903 --- /dev/null +++ b/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockHelper.java @@ -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; + } +} diff --git a/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java b/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java index 2af66d7..b752a9b 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java +++ b/src/main/java/de/oaa/xxx/games/chastity/common/BaseLockService.java @@ -3,7 +3,6 @@ package de.oaa.xxx.games.chastity.common; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; import java.util.Random; import java.util.Set; 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.CommunityVerificationRepository; 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.KeyholderNotificationRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceEntity; import de.oaa.xxx.games.chastity.keyholder.KeyholderTaskChoiceRepository; import de.oaa.xxx.games.chastity.keyholder.KeyholderVerificationRepository; 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.UnlockCodeHistoryService; 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. */ protected void afterHygieneClosing() {} - protected abstract void handleLockGameFinished(int timeInMinutes); + protected abstract void handleLockGameFinished(int timeInSeconds); public abstract void penaltyLockGame(); @@ -111,18 +110,6 @@ public abstract class BaseLockService { // ── 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) { BaseLockEntity lock = getLock(); KeyholderNotificationEntity notification = new KeyholderNotificationEntity(); @@ -147,9 +134,9 @@ public abstract class BaseLockService { // ── Lock-Game Abschluss ─────────────────────────────────────────────────── - public void lockGameFinished(int timeInMinutes) { - LOGGER.info("[Lock {}] lockGameFinished nach {} Minuten", getLock().getLockee(), timeInMinutes); - handleLockGameFinished(timeInMinutes); + public void lockGameFinished(int timeInSeconds) { + LOGGER.info("[Lock {}] lockGameFinished nach {} Sekunden Overtime", getLock().getLockee(), timeInSeconds); + handleLockGameFinished(timeInSeconds); } @@ -227,7 +214,7 @@ public abstract class BaseLockService { public String endTempOpening() { var lock = getLock(); var now = LocalDateTime.now(); - var overtime = calcOvertime(); + var overtime = BaseLockHelper.calcOvertime(lock); if (overtime != null) { if (lock.getKeyholder() != null) { reportKeyholder(overtime); diff --git a/src/main/java/de/oaa/xxx/games/chastity/common/GameState.java b/src/main/java/de/oaa/xxx/games/chastity/common/GameState.java index 9a9f2e8..8c3bb16 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/common/GameState.java +++ b/src/main/java/de/oaa/xxx/games/chastity/common/GameState.java @@ -1,15 +1,22 @@ package de.oaa.xxx.games.chastity.common; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; public record GameState( - UUID gameID, + UUID gameID, UUID userId, Integer level, String activeTask, LocalDateTime activeTaskEnd, + List activeTaskBenoetigtAktiv, String taskInQueue, - String lockInQueue) { - + String lockInQueue, + LocalDateTime tempOpeningTime, + Integer tempOpeningDuration, + String tempOpeningCode, + LocalDateTime finisherStartedAt, + String finisher) { + } diff --git a/src/main/java/de/oaa/xxx/games/chastity/common/LockGameController.java b/src/main/java/de/oaa/xxx/games/chastity/common/LockGameController.java index b9de450..9cfc1b6 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/common/LockGameController.java +++ b/src/main/java/de/oaa/xxx/games/chastity/common/LockGameController.java @@ -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.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.AvailableIn; import de.oaa.xxx.games.common.entity.AufgabeEntity; @@ -50,6 +52,9 @@ public class LockGameController { private final ObjectMapper objectMapper; private final CardlockRepository cardlockRepository; private final CardLockServiceFactory cardLockServiceFactory; + private BaseLockRepository baseLockRepository; + private UnlockCodeHistoryService unlockCodeHistoryService; + private LockControlFactory lockControlFactory; public LockGameController(LockGameRepository lockGameRepository, LockGameLockRepository lockGameLockRepository, @@ -60,7 +65,10 @@ public class LockGameController { UserService userService, ObjectMapper objectMapper, CardlockRepository cardlockRepository, - CardLockServiceFactory cardLockServiceFactory) { + CardLockServiceFactory cardLockServiceFactory, + BaseLockRepository baseLockRepository, + UnlockCodeHistoryService unlockCodeHistoryService, + LockControlFactory lockControlFactory) { this.lockGameRepository = lockGameRepository; this.lockGameLockRepository = lockGameLockRepository; this.aufgabenGruppeRepository = aufgabenGruppeRepository; @@ -71,6 +79,9 @@ public class LockGameController { this.objectMapper = objectMapper; this.cardlockRepository = cardlockRepository; this.cardLockServiceFactory = cardLockServiceFactory; + this.baseLockRepository = baseLockRepository; + this.unlockCodeHistoryService = unlockCodeHistoryService; + this.lockControlFactory = lockControlFactory; } /** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */ @@ -136,6 +147,7 @@ public class LockGameController { @PostMapping("/init") public ResponseEntity init( @RequestParam UUID aufgabenGruppeId, + @RequestParam UUID lockId, @RequestParam(required = false) List excludedToyIds, Principal principal) { 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(); list.setAufgaben(aufgaben); list.setSperren(sperren); @@ -196,11 +212,15 @@ public class LockGameController { return g; }); + game.setLockId(lockId); game.setAufgaben(aufgabenJson); game.setLevel(1); game.setAufgabenProLevel(AUFGABEN_PRO_LEVEL); 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.setActiveTask(null); game.setActiveTaskEnd(null); @@ -290,17 +310,55 @@ public class LockGameController { } } - @GetMapping("/finisher") - public ResponseEntity getFinisher(Principal principal) { + @PostMapping("/start-finisher") + public ResponseEntity startFinisher(Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); var opt = lockGameRepository.findByUserId(userId); if (opt.isEmpty()) return ResponseEntity.notFound().build(); try { - var finisher = buildService(opt.get()).getFinisher(); - Map result = new LinkedHashMap<>(); - result.put("kurzText", finisher.getKurzText()); - result.put("text", finisher.getText()); - return ResponseEntity.ok(result); + buildService(opt.get()).startFinisher(); + return ResponseEntity.noContent().build(); + } catch (Exception e) { + return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); + } + } + + @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) { return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage())); } @@ -324,23 +382,20 @@ public class LockGameController { @PostMapping("/complete") public ResponseEntity completeGame( @RequestParam(required = false) UUID lockId, - @RequestParam(required = false, defaultValue = "0") int timeInMinutes, Principal principal) { UUID userId = userService.requireUser(principal).getUserId(); var opt = lockGameRepository.findByUserId(userId); if (opt.isEmpty()) return ResponseEntity.notFound().build(); LockGameEntity game = opt.get(); + int overtimeInSeconds = game.getOvertimeInSeconds() != null ? game.getOvertimeInSeconds() : 0; lockGameLockRepository.deleteAll(lockGameLockRepository.findByGameId(game.getGameId())); lockGameRepository.delete(game); if (lockId != null) { cardlockRepository.findById(lockId).ifPresent(l -> { if (l.getLockee().equals(userId)) { - cardLockServiceFactory.create(l).lockGameFinished(timeInMinutes); + cardLockServiceFactory.create(l).lockGameFinished(overtimeInSeconds); l.setGameActive(false); - l.setFrozenUntil(null); - l.setNextCardIn(LocalDateTime.now() - .plusMinutes(l.getPickEveryMinute() != null ? l.getPickEveryMinute() : 60)); cardlockRepository.save(l); } }); @@ -349,6 +404,6 @@ public class LockGameController { } private LockGameService buildService(LockGameEntity entity) throws Exception { - return new LockGameService(entity, lockGameRepository, lockGameLockRepository); + return new LockGameService(entity, lockGameRepository, lockGameLockRepository, baseLockRepository, unlockCodeHistoryService, lockControlFactory); } } diff --git a/src/main/java/de/oaa/xxx/games/chastity/common/LockGameEntity.java b/src/main/java/de/oaa/xxx/games/chastity/common/LockGameEntity.java index 1244a43..8e1bca5 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/common/LockGameEntity.java +++ b/src/main/java/de/oaa/xxx/games/chastity/common/LockGameEntity.java @@ -18,13 +18,15 @@ import lombok.Setter; @Setter @Entity @Table(name = "lock_game") -public class LockGameEntity { +public class LockGameEntity { @Id @Column private UUID gameId; @Column(unique = true) private UUID userId; + @Column + private UUID lockId; @OneToMany(mappedBy = "gameId", fetch = FetchType.EAGER) private List activeLocks = new ArrayList<>(); @Column @@ -42,9 +44,23 @@ public class LockGameEntity { @Column private LocalDateTime activeTaskEnd; @Column(columnDefinition = "TEXT") + private String activeTaskBenoetigtAktiv; + @Column(columnDefinition = "TEXT") private String taskInQueue; @Column(columnDefinition = "TEXT") private String lockInQueue; @Column 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; } diff --git a/src/main/java/de/oaa/xxx/games/chastity/common/LockGameLockEntity.java b/src/main/java/de/oaa/xxx/games/chastity/common/LockGameLockEntity.java index 50e5af5..8f6e29b 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/common/LockGameLockEntity.java +++ b/src/main/java/de/oaa/xxx/games/chastity/common/LockGameLockEntity.java @@ -39,4 +39,6 @@ public class LockGameLockEntity { private String releaseText; @Column private LocalDateTime releaseTime; + @Column + private Boolean tempUnlockRequired; } diff --git a/src/main/java/de/oaa/xxx/games/chastity/common/LockGameService.java b/src/main/java/de/oaa/xxx/games/chastity/common/LockGameService.java index d4fdb0d..360e122 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/common/LockGameService.java +++ b/src/main/java/de/oaa/xxx/games/chastity/common/LockGameService.java @@ -1,5 +1,6 @@ package de.oaa.xxx.games.chastity.common; +import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -14,39 +15,60 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; 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.AufgabenList; -import de.oaa.xxx.games.common.aufgaben.Finisher; 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 LockGameEntity gamestate; private LockGameRepository lockGameRepository; private LockGameLockRepository lockGameLockRepository; + private BaseLockRepository baseLockRepository; + private UnlockCodeHistoryService unlockCodeHistoryService; + private LockControlFactory lockControlFactory; + + private LockGameEntity gamestate; private AufgabenList aufgabenList; 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.lockGameRepository = lockGameRepository; this.lockGameLockRepository = lockGameLockRepository; + this.baseLockRepository = baseLockRepository; + this.unlockCodeHistoryService = unlockCodeHistoryService; + this.lockControlFactory = lockControlFactory; this.aufgabenList = new ObjectMapper().readValue(gamestate.getAufgaben(), AufgabenList.class); } public GameState getGameState() { + List benoetigtAktiv = null; + if (gamestate.getActiveTaskBenoetigtAktiv() != null) { + try { + benoetigtAktiv = new ObjectMapper().readValue(gamestate.getActiveTaskBenoetigtAktiv(), + new com.fasterxml.jackson.core.type.TypeReference>() {}); + } catch (Exception ignored) {} + } return new GameState(gamestate.getGameId(), gamestate.getUserId(), gamestate.getLevel(), - gamestate.getActiveTask(), gamestate.getActiveTaskEnd(), gamestate.getTaskInQueue(), - gamestate.getLockInQueue()); + gamestate.getActiveTask(), gamestate.getActiveTaskEnd(), benoetigtAktiv, + gamestate.getTaskInQueue(), gamestate.getLockInQueue(), gamestate.getTempOpeningTime(), + gamestate.getTempOpeningDurationInMinutes(), gamestate.getTempUnlockCode(), + gamestate.getFinisherStartedAt(), gamestate.getFinisher()); } public void initNextTask() throws JsonProcessingException { gamestate.setActiveTask(null); gamestate.setActiveTaskEnd(null); + gamestate.setActiveTaskBenoetigtAktiv(null); checkLevel(); lockGameRepository.save(gamestate); pickNextTask(); @@ -55,6 +77,7 @@ public class LockGameService { public void abandonActiveTask() throws JsonProcessingException { gamestate.setActiveTask(null); gamestate.setActiveTaskEnd(null); + gamestate.setActiveTaskBenoetigtAktiv(null); lockGameRepository.save(gamestate); pickNextTask(); } @@ -136,6 +159,9 @@ public class LockGameService { var aufgabe = mapper.readValue(gamestate.getTaskInQueue(), Aufgabe.class); gamestate.setActiveTask(aufgabe.getText()); gamestate.setTaskInQueue(null); + var benoetigtAktiv = aufgabe.getBenoetigtAktiv(); + gamestate.setActiveTaskBenoetigtAktiv( + (benoetigtAktiv != null && !benoetigtAktiv.isEmpty()) ? mapper.writeValueAsString(benoetigtAktiv) : null); var time = getAufgabeTime(aufgabe); gamestate.setActiveTaskEnd(time > 0 ? LocalDateTime.now().plusSeconds(time) : null); 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); String displayText = lock.getText() != null ? lock.getText() : lock.getKurzText(); gamestate.setActiveTask(displayText != null ? displayText : "Zeitstrafe aktiv"); + gamestate.setActiveTaskBenoetigtAktiv(null); gamestate.setLockInQueue(null); applyLock(lock); } @@ -168,11 +195,14 @@ public class LockGameService { return (int) (time * gamestate.getZeitfaktorZeitstrafen()); } - protected void checkLevel() { + protected void checkLevel() throws JsonProcessingException { var aufgabenAufAktuellemLevel = gamestate.getAufgabenAufAktuellemLevel(); if (++aufgabenAufAktuellemLevel >= 1 + gamestate.getAufgabenProLevel()) { aufgabenAufAktuellemLevel = 0; gamestate.setLevel(gamestate.getLevel() + 1); + if (gamestate.getLevel() >= 6) { + initFinisher(); + } } gamestate.setAufgabenAufAktuellemLevel(aufgabenAufAktuellemLevel); } @@ -190,6 +220,7 @@ public class LockGameService { entity.setGameId(gamestate.getGameId()); entity.setLockFor(lock.getSperreFuer()); entity.setReleaseText(lock.getReleaseText()); + entity.setTempUnlockRequired(lock.getTempUnlockRequired()); int lockMinutes = getLockTime(lock); entity.setReleaseTime(LocalDateTime.now().plusMinutes(lockMinutes)); LOGGER.info("[LockGame {}] ZEITSTRAFE aktiv: kurzText='{}', berechnete Zeit={}min (Range: {}min-{}min), sperreFuer={}", @@ -201,9 +232,13 @@ public class LockGameService { public List checkLocks() { var result = new ArrayList(); for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) { - if (entity.getReleaseTime().isAfter(LocalDateTime.now())) { + if (entity.getReleaseTime().isBefore(LocalDateTime.now())) { result.add(entity.getReleaseText()); + if (Boolean.TRUE.equals(entity.getTempUnlockRequired())) { + startTempOpening(); + } lockGameLockRepository.delete(entity); + } } return result; @@ -218,7 +253,77 @@ public class LockGameService { return result; } - public Finisher getFinisher() { - return aufgabenList.getFinisher().get(new Random().nextInt(aufgabenList.getFinisher().size())); + public void initFinisher() throws JsonProcessingException { + 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(); } } diff --git a/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.java b/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.java index 7d0dde5..6f00869 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.java +++ b/src/main/java/de/oaa/xxx/games/chastity/keyholder/KeyholderOfferController.java @@ -267,7 +267,7 @@ public class KeyholderOfferController { if (template instanceof TimeLockTemplateEntity tl) { TimeLockAdditionalSettings settings = new TimeLockAdditionalSettings( - controllType, myId, keyholderIdIfDirect, false, codeLen); + controllType, myId, keyholderIdIfDirect, false, codeLen, 1); TimeLockEntity lock = new TimeLockEntity(); timeLockServiceFactory.create(lock).init(tl, settings); timeLockRepository.save(lock); @@ -283,11 +283,11 @@ public class KeyholderOfferController { lock.setLockee(myId); lock.setKeyholder(keyholderIdIfDirect); 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.setShowRemainingCards(cl.isShowRemainingCards()); - lock.setHygineOpeningDurationMinutes(cl.getHygineOpeningDurationMinutes()); - lock.setHygineOpeningEveryMinites(cl.getHygineOpeningEveryMinites()); + lock.setHygineOpeningDurationSeconds(cl.getHygineOpeningDurationMinutes() != null ? cl.getHygineOpeningDurationMinutes() * 60 : null); + lock.setHygineOpeningEverySeconds(cl.getHygineOpeningEveryMinites() != null ? cl.getHygineOpeningEveryMinites() * 60 : null); lock.setTasks(cl.getTasks() != null ? cl.getTasks() : List.of()); lock.setRequiresVerification(cl.isRequiresVerification()); lock.setTestLock(false); @@ -299,7 +299,7 @@ public class KeyholderOfferController { lock.setStartTime(now); lock.setAvailableCards(new ArrayList<>(cards)); lock.setOpenPicks(0); - lock.setNextCardIn(now.plusMinutes(lock.getPickEveryMinute())); + lock.setNextCardIn(now.plusSeconds(lock.getPickEverySeconds())); if (cl.getHygineOpeningEveryMinites() != null) { lock.setLastHygineOpening(now); } diff --git a/src/main/java/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.java b/src/main/java/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.java index a17b212..e97f3c2 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.java +++ b/src/main/java/de/oaa/xxx/games/chastity/lockee/LockeeInvitationController.java @@ -198,11 +198,11 @@ public class LockeeInvitationController { .collect(java.util.stream.Collectors.groupingBy( c -> c.name(), java.util.stream.Collectors.counting())); 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("showRemainingCards", cardLock.isShowRemainingCards()); - result.put("hygineOpeningEveryMinites", cardLock.getHygineOpeningEveryMinites()); - result.put("hygineOpeningDurationMinutes", cardLock.getHygineOpeningDurationMinutes()); + result.put("hygineOpeningEveryMinites", cardLock.getHygineOpeningEverySeconds() != null ? cardLock.getHygineOpeningEverySeconds() / 60 : null); + result.put("hygineOpeningDurationMinutes", cardLock.getHygineOpeningDurationSeconds() != null ? cardLock.getHygineOpeningDurationSeconds() / 60 : null); result.put("requiresVerification", cardLock.isRequiresVerification()); result.put("taskCount", cardLock.getTasks() != null ? cardLock.getTasks().size() : 0); } @@ -246,8 +246,8 @@ public class LockeeInvitationController { cardLock.setUnlockCodeLength(codeLines); cardLock.setAvailableCards(new ArrayList<>(cardLock.getInitialCards())); cardLock.setOpenPicks(0); - cardLock.setNextCardIn(now.plusMinutes(cardLock.getPickEveryMinute())); - if (cardLock.getHygineOpeningEveryMinites() != null) { + cardLock.setNextCardIn(now.plusSeconds(cardLock.getPickEverySeconds())); + if (cardLock.getHygineOpeningEverySeconds() != null) { cardLock.setLastHygineOpening(now); } cardlockRepository.save(cardLock); @@ -258,7 +258,7 @@ public class LockeeInvitationController { timeLock.setEstimatedUnlockTime(now.plusMinutes(unlockMinutes)); timeLock.setUnlockCode(unlockCode); timeLock.setUnlockCodeLength(codeLines); - if (timeLock.getHygineOpeningEveryMinites() != null) { + if (timeLock.getHygineOpeningEverySeconds() != null) { timeLock.setLastHygineOpening(now); } timeLockRepository.save(timeLock); diff --git a/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockAdditionalSettings.java b/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockAdditionalSettings.java index 447fc3d..662c549 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockAdditionalSettings.java +++ b/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockAdditionalSettings.java @@ -4,6 +4,6 @@ import java.util.UUID; 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) { } diff --git a/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java b/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java index 98b70b2..b345f45 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java +++ b/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockController.java @@ -100,7 +100,8 @@ public class TimeLockController { UUID keyholder, boolean testLock, Integer unlockCodeLength, - LockControllType controllType + LockControllType controllType, + Integer speedFactor ) {} @PostMapping("/timelock") @@ -155,9 +156,10 @@ public class TimeLockController { 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( 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(); timeLockServiceFactory.create(lock).init(template, settings); timeLockRepository.save(lock); // Sicherstellen dass auch TRUST-Locks persistiert sind @@ -234,7 +236,7 @@ public class TimeLockController { && (l.getFrozenUntil() == null || l.getFrozenUntil().isAfter(now)); // Hygiene state - boolean hygieneEnabled = l.getHygineOpeningEveryMinites() != null; + boolean hygieneEnabled = l.getHygineOpeningEverySeconds() != null; boolean hygieneOpeningDue = false; long hygieneSecondsRemaining = 0; boolean hygieneOpeningActive = l.getTempOpeningTime() != null @@ -244,7 +246,9 @@ public class TimeLockController { if (lastH == null) { hygieneOpeningDue = true; } 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); if (secs <= 0) hygieneOpeningDue = true; else hygieneSecondsRemaining = secs; @@ -252,7 +256,7 @@ public class TimeLockController { } // Spin wheel state - boolean spinEnabled = l.getSpinsEveryMinutes() != null + boolean spinEnabled = l.getSpinsEverySeconds() != null && l.getSpinningWheelEntries() != null && !l.getSpinningWheelEntries().isEmpty(); boolean spinDue = false; String nextSpinIn = null; @@ -261,23 +265,23 @@ public class TimeLockController { if (times == null || times.isEmpty()) { spinDue = true; } 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; else nextSpinIn = next.toString(); } } // Task timing state - boolean taskTimingEnabled = l.getTaskEveryMinutes() != null; + boolean taskTimingEnabled = l.getTaskEverySeconds() != null; String nextTaskIn = null; if (taskTimingEnabled && l.getCurrentTask() == null) { List times = l.getTaskTimes(); LocalDateTime next; if (times == null || times.isEmpty()) { next = l.getStartTime() != null - ? l.getStartTime().plusMinutes(l.getTaskEveryMinutes()) : null; + ? l.getStartTime().plusSeconds(l.getTaskEverySeconds()) : null; } 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(); } @@ -347,7 +351,7 @@ public class TimeLockController { result.put("hygieneSecondsRemaining", hygieneSecondsRemaining); result.put("hygieneOpeningActive", hygieneOpeningActive); 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("verificationDue", verificationDue); @@ -402,8 +406,8 @@ public class TimeLockController { // Check spin is due List spinTimes = l.getSpinningWheelTimes(); - if (spinTimes != null && !spinTimes.isEmpty() && l.getSpinsEveryMinutes() != null) { - LocalDateTime next = spinTimes.get(spinTimes.size() - 1).plusMinutes(l.getSpinsEveryMinutes()); + if (spinTimes != null && !spinTimes.isEmpty() && l.getSpinsEverySeconds() != null) { + LocalDateTime next = spinTimes.get(spinTimes.size() - 1).plusSeconds(l.getSpinsEverySeconds()); 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(); var l = lockOpt.get(); 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")); TimeLockService service = timeLockServiceFactory.create(l); @@ -485,7 +489,7 @@ public class TimeLockController { return ResponseEntity.ok(Map.of( "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() : "")); } @@ -840,13 +844,13 @@ public class TimeLockController { lock.setEndTimeVisible(template.isEndTimeVisible()); lock.setTasks(template.getTasks()); lock.setTaskMode(template.getTaskCardMode()); - lock.setTaskEveryMinutes(template.getTaskEveryMinutes()); + lock.setTaskEverySeconds(template.getTaskEveryMinutes() != null ? template.getTaskEveryMinutes() * 60 : null); lock.setMinTasksPerDay(template.getMinTasksPerDay()); lock.setSpinningWheelEntries(template.getSpinningWheelEntries()); - lock.setSpinsEveryMinutes(template.getSpinsEveryMinutes()); + lock.setSpinsEverySeconds(template.getSpinsEveryMinutes() != null ? template.getSpinsEveryMinutes() * 60 : null); lock.setMinSpinsPerDay(template.getMinSpinsPerDay()); - lock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes()); - lock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites()); + lock.setHygineOpeningDurationSeconds(template.getHygineOpeningDurationMinutes() != null ? template.getHygineOpeningDurationMinutes() * 60 : null); + lock.setHygineOpeningEverySeconds(template.getHygineOpeningEveryMinites() != null ? template.getHygineOpeningEveryMinites() * 60 : null); lock.setPenaltyType(template.getPenaltyType()); lock.setPenaltyValue(template.getPenaltyValue()); lock.setMinTimeInMinutes(template.getMinTimeInMinutes()); diff --git a/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.java b/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.java index bed085d..d54a585 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.java +++ b/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockEntity.java @@ -29,7 +29,7 @@ public class TimeLockEntity extends BaseLockEntity { private Integer maxTimeInMinutes; @Column - private Integer taskEveryMinutes; + private Integer taskEverySeconds; @Column private Integer minTasksPerDay; @@ -37,7 +37,7 @@ public class TimeLockEntity extends BaseLockEntity { @Column(columnDefinition = "TEXT") private List spinningWheelEntries; @Column - private Integer spinsEveryMinutes; + private Integer spinsEverySeconds; @Column private Integer minSpinsPerDay; diff --git a/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java b/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java index 463bff2..160ca09 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java +++ b/src/main/java/de/oaa/xxx/games/chastity/timelock/TimeLockService.java @@ -132,24 +132,26 @@ public class TimeLockService extends BaseLockService implements LockControlCallb lock.setTestLock(settings.testlock()); 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 maxMinutes = template.getMaxTimeInMinutes() != null ? template.getMaxTimeInMinutes() : 60; int unlockTimeMinutes = (minMinutes != null && minMinutes < maxMinutes) ? minMinutes + new Random().nextInt(maxMinutes - minMinutes) : maxMinutes; - lock.setEstimatedUnlockTime(now.plusMinutes(unlockTimeMinutes)); + lock.setEstimatedUnlockTime(now.plusSeconds(Math.max(6L, unlockTimeMinutes * 60L / sf))); lock.setEndTimeVisible(template.isEndTimeVisible()); 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.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.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes()); - lock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites()); + lock.setHygineOpeningDurationSeconds(template.getHygineOpeningDurationMinutes() != null ? template.getHygineOpeningDurationMinutes() * 60 : null); + lock.setHygineOpeningEverySeconds(template.getHygineOpeningEveryMinites() != null ? template.getHygineOpeningEveryMinites() * 60 : null); if (template.getHygineOpeningEveryMinites() != null) { lock.setLastHygineOpening(now); } @@ -360,7 +362,7 @@ public class TimeLockService extends BaseLockService implements LockControlCallb // ── Hygiene opening ─────────────────────────────────────────────────────── public void startHygieneOpening() { - startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationMinutes()); + startTempOpening(TempOpeningReason.HYGIENE, lock.getHygineOpeningDurationSeconds() != null ? lock.getHygineOpeningDurationSeconds() / 60 : 0); } // ── LockControlCallback ─────────────────────────────────────────────────── @@ -377,13 +379,13 @@ public class TimeLockService extends BaseLockService implements LockControlCallb } @Override - protected void handleLockGameFinished(int timeInMinutes) { - int freezeTime = (int) (timeInMinutes * new Random().nextDouble(1.0, 4.0)); - addTime(freezeTime); + protected void handleLockGameFinished(int timeInSeconds) { + int freezeMinutes = (int) (timeInSeconds / 60.0 * new Random().nextDouble(1.0, 4.0)); + addTime(Math.max(1, freezeMinutes)); } @Override public void penaltyLockGame() { - handleLockGameFinished(60); + handleLockGameFinished(3600); } } diff --git a/src/main/java/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.java b/src/main/java/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.java index f121f78..f827935 100644 --- a/src/main/java/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.java +++ b/src/main/java/de/oaa/xxx/games/chastity/unlock/TempOpeningReason.java @@ -1,5 +1,5 @@ package de.oaa.xxx.games.chastity.unlock; public enum TempOpeningReason { - HYGIENE, CARD, TASK, TTLOCK_UNAUTHORIZED; + HYGIENE, CARD, TASK, TTLOCK_UNAUTHORIZED, TEMPORARY; } diff --git a/src/main/java/de/oaa/xxx/games/common/aufgaben/Finisher.java b/src/main/java/de/oaa/xxx/games/common/aufgaben/Finisher.java index 1a47182..8a9ba50 100644 --- a/src/main/java/de/oaa/xxx/games/common/aufgaben/Finisher.java +++ b/src/main/java/de/oaa/xxx/games/common/aufgaben/Finisher.java @@ -19,6 +19,7 @@ public class Finisher { private List benoetigtPassiv; private List benoetigteToys; private UUID gruppeId; + private Boolean tempUnlockRequired; @Override public String toString() { diff --git a/src/main/java/de/oaa/xxx/games/common/aufgaben/Sperre.java b/src/main/java/de/oaa/xxx/games/common/aufgaben/Sperre.java index 89233d4..3189d46 100644 --- a/src/main/java/de/oaa/xxx/games/common/aufgaben/Sperre.java +++ b/src/main/java/de/oaa/xxx/games/common/aufgaben/Sperre.java @@ -20,8 +20,7 @@ public class Sperre { private Integer minutenVon; private Integer minutenBis; private Integer level; - private Boolean tempUnlockBeforeRequired; - private Boolean tempUnlockAfterRequired; + private Boolean tempUnlockRequired; private List benoetigteToys; @Override diff --git a/src/main/java/de/oaa/xxx/games/common/entity/FinisherEntity.java b/src/main/java/de/oaa/xxx/games/common/entity/FinisherEntity.java index 36aabe6..5cc8f6f 100644 --- a/src/main/java/de/oaa/xxx/games/common/entity/FinisherEntity.java +++ b/src/main/java/de/oaa/xxx/games/common/entity/FinisherEntity.java @@ -55,6 +55,8 @@ public class FinisherEntity { @ManyToMany(cascade = CascadeType.DETACH) @JoinTable(name = "finisherToy", joinColumns = {@JoinColumn(name = "finisherId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")}) private List benoetigteToys; + @Column + private Boolean tempUnlockRequired; @Override public String toString() { @@ -71,6 +73,7 @@ public class FinisherEntity { finisher.setBenoetigtPassiv(benoetigtPassiv != null ? new ArrayList<>(benoetigtPassiv) : new ArrayList<>()); finisher.setBenoetigteToys(benoetigteToys != null ? benoetigteToys.stream().map(ToyEntity::toToy).toList() : new ArrayList<>()); finisher.setGruppeId(aufgabenGruppe.getGruppenId()); + finisher.setTempUnlockRequired(tempUnlockRequired); return finisher; } @@ -84,6 +87,7 @@ public class FinisherEntity { entity.setBenoetigtAktiv(finisher.getBenoetigtAktiv()); entity.setBenoetigtPassiv(finisher.getBenoetigtPassiv()); entity.setBenoetigteToys(toys != null ? toys : new ArrayList<>()); + entity.setTempUnlockRequired(finisher.getTempUnlockRequired()); return entity; } } diff --git a/src/main/java/de/oaa/xxx/games/common/entity/SperreEntity.java b/src/main/java/de/oaa/xxx/games/common/entity/SperreEntity.java index 15ee62b..d47ea71 100644 --- a/src/main/java/de/oaa/xxx/games/common/entity/SperreEntity.java +++ b/src/main/java/de/oaa/xxx/games/common/entity/SperreEntity.java @@ -53,9 +53,7 @@ public class SperreEntity { @Column private Integer level; @Column - private Boolean tempUnlockBeforeRequired; - @Column - private Boolean tempUnlockAfterRequired; + private Boolean tempUnlockRequired; @ManyToMany(cascade = CascadeType.DETACH) @JoinTable(name = "sperreToy", joinColumns = {@JoinColumn(name = "sperreId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")}) private List benoetigteToys; @@ -74,8 +72,7 @@ public class SperreEntity { sperre.setMinutenBis(minutenBis); sperre.setMinutenVon(minutenVon); sperre.setLevel(level); - sperre.setTempUnlockBeforeRequired(tempUnlockBeforeRequired); - sperre.setTempUnlockAfterRequired(tempUnlockAfterRequired); + sperre.setTempUnlockRequired(tempUnlockRequired); sperre.setReleaseText(releaseText); sperre.setSperreFuer(sperreFuer != null ? new ArrayList<>(sperreFuer) : new ArrayList<>()); sperre.setText(text); @@ -92,8 +89,7 @@ public class SperreEntity { entity.setMinutenBis(sperre.getMinutenBis()); entity.setMinutenVon(sperre.getMinutenVon()); entity.setLevel(sperre.getLevel()); - entity.setTempUnlockBeforeRequired(sperre.getTempUnlockBeforeRequired()); - entity.setTempUnlockAfterRequired(sperre.getTempUnlockAfterRequired()); + entity.setTempUnlockRequired(sperre.getTempUnlockRequired()); entity.setReleaseText(sperre.getReleaseText()); entity.setSperreFuer(sperre.getSperreFuer()); entity.setText(sperre.getText()); diff --git a/src/main/resources/db/migration/V8__lock_game_aufgaben_text.sql b/src/main/resources/db/migration/V8__lock_game_aufgaben_text.sql new file mode 100644 index 0000000..7c9a1a0 --- /dev/null +++ b/src/main/resources/db/migration/V8__lock_game_aufgaben_text.sql @@ -0,0 +1,2 @@ +ALTER TABLE lock_game + MODIFY COLUMN aufgaben TEXT NULL; diff --git a/src/main/resources/static/games/aufgaben/aufgaben.html b/src/main/resources/static/games/aufgaben/aufgaben.html index 5c4251e..25f6230 100644 --- a/src/main/resources/static/games/aufgaben/aufgaben.html +++ b/src/main/resources/static/games/aufgaben/aufgaben.html @@ -263,6 +263,12 @@ 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-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 { 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; @@ -533,17 +539,11 @@
- -
- - -
+
@@ -765,7 +765,7 @@ ${g.beschreibung ? `
${esc(g.beschreibung)}
` : ''} ${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, 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)}
`; @@ -814,6 +814,7 @@ function renderAufgabe(a, type, gruppenId) { _itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId }; const badges = []; + (a.benoetigteToys || []).forEach(t => badges.push(`${esc(t.name || t)}`)); const zeit = formatSek(a.sekundenVon, a.sekundenBis); if (zeit) badges.push(`${esc(zeit)}`); if (a.level != null) badges.push(`Level ${esc(String(a.level))}`); @@ -826,6 +827,7 @@ const actionBtns = type === 'user' ? `
+
` : ''; @@ -841,6 +843,7 @@ function renderStrafe(s, type, gruppenId) { _itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId }; const badges = []; + (s.benoetigteToys || []).forEach(t => badges.push(`${esc(t.name || t)}`)); const zeit = formatSek(s.sekundenVon, s.sekundenBis); if (zeit) badges.push(`${esc(zeit)}`); if (s.level != null) badges.push(`Level ${esc(String(s.level))}`); @@ -853,6 +856,7 @@ const actionBtns = type === 'user' ? `
+
` : ''; @@ -868,8 +872,10 @@ function renderZeitstrafe(z, type, gruppenId) { _itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId }; const badges = []; + (z.benoetigteToys || []).forEach(t => badges.push(`${esc(t.name || t)}`)); const zeit = formatMin(z.minutenVon, z.minutenBis); if (zeit) badges.push(`${esc(zeit)}`); + if (z.level != null) badges.push(`Level ${esc(String(z.level))}`); const detailRows = []; if (z.text) detailRows.push(`
${esc(z.text)}
`); @@ -879,6 +885,7 @@ const actionBtns = type === 'user' ? `
+
` : ''; @@ -896,6 +903,7 @@ function renderFinisher(f, type, gruppenId) { _itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId }; const badges = []; + (f.benoetigteToys || []).forEach(t => badges.push(`${esc(t.name || t)}`)); if (f.geschlecht) badges.push(`${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}`); const detailRows = []; @@ -906,6 +914,7 @@ const actionBtns = type === 'user' ? `
+
` : ''; @@ -944,10 +953,36 @@ finisher: apiUrl('/finisher') }; 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) { 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]; if (!deleteUrl) return; const body = { [ITEM_DELETE_FIELD[kind]]: itemId }; @@ -1430,7 +1465,7 @@ const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label'); 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'; } @@ -1449,8 +1484,7 @@ document.querySelectorAll('#iWerkzeugFinisherPassiv 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.getElementById('iTempUnlockBefore').checked = false; - document.getElementById('iTempUnlockAfter').checked = false; + document.getElementById('iTempUnlockRequired').checked = false; _selectedToys = []; renderSelectedToys(); document.getElementById('itemModalError').style.display = 'none'; @@ -1495,6 +1529,9 @@ const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`); if (rb) rb.checked = true; } + if (_isChastityMode) { + document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true; + } } else { document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : ''; 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; }); if (_isChastityMode) { document.getElementById('iLevel').value = d.level != null ? d.level : ''; - document.getElementById('iTempUnlockBefore').checked = d.tempUnlockBeforeRequired === true; - document.getElementById('iTempUnlockAfter').checked = d.tempUnlockAfterRequired === true; + document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true; } } @@ -1666,11 +1702,12 @@ if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; } payload = { kurzText, text, - geschlecht: geschlecht || null, - gruppeId: isEdit ? undefined : currentItemGruppeId, - benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'), - benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'), - benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })) + geschlecht: geschlecht || null, + gruppeId: isEdit ? undefined : currentItemGruppeId, + benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'), + benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'), + benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })), + tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null }; url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher'); method = isEdit ? 'PUT' : 'POST'; @@ -1698,8 +1735,7 @@ releaseText: document.getElementById('iReleaseText').value.trim() || null, sperreFuer, level: zeitLevel, - tempUnlockBeforeRequired: _isChastityMode ? document.getElementById('iTempUnlockBefore').checked : null, - tempUnlockAfterRequired: _isChastityMode ? document.getElementById('iTempUnlockAfter').checked : null, + tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null, benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })) }; url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only diff --git a/src/main/resources/static/games/bdsm/neubdsm.html b/src/main/resources/static/games/bdsm/neubdsm.html index 965d1a1..ee791ef 100644 --- a/src/main/resources/static/games/bdsm/neubdsm.html +++ b/src/main/resources/static/games/bdsm/neubdsm.html @@ -679,6 +679,7 @@ // ── Gruppe lists ── function renderGruppeList(containerId, gruppen) { + gruppen = gruppen.filter(g => g.availableIn !== 'CHASTITY_ONLY'); const ul = document.getElementById(containerId); const section = ul.closest('[id^="section"]'); const selectAllWrap = section?.querySelector('.select-all-label'); diff --git a/src/main/resources/static/games/chastity/neulock.html b/src/main/resources/static/games/chastity/neulock.html index 85885ba..576ad31 100644 --- a/src/main/resources/static/games/chastity/neulock.html +++ b/src/main/resources/static/games/chastity/neulock.html @@ -256,9 +256,14 @@
- +
+
@@ -515,6 +520,7 @@ khInput.readOnly = true; khInput.style.opacity = '0.6'; document.getElementById('rowTestLock').style.display = 'none'; + document.getElementById('rowSpeedFactor').style.display = 'none'; document.getElementById('rowDetailsVisible').style.display = ''; } else { khInput.readOnly = false; @@ -728,6 +734,15 @@ 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 ── async function createSession() { document.getElementById('errorMsg').style.display = 'none'; @@ -756,6 +771,7 @@ const isFriendLockee = lockeeVal && lockeeVal !== myUserId; const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5); const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked; + const speedFactor = isTestLock ? parseInt(document.getElementById('speedFactor').value) : 1; let endpoint, body; @@ -769,6 +785,7 @@ testLock: isTestLock, unlockCodeLength: unlockCodeLen, controllType: selectedLockControl, + speedFactor: speedFactor, }; } else { // CardLock @@ -798,6 +815,7 @@ controllType: selectedLockControl, gameSetId: t.gameSetId || null, gameSpieldauerIdx: t.gameSpieldauerIdx ?? null, + speedFactor: speedFactor, }; } diff --git a/src/main/resources/static/games/chastity/taskgame.html b/src/main/resources/static/games/chastity/taskgame.html index 8a6aeef..82c3651 100644 --- a/src/main/resources/static/games/chastity/taskgame.html +++ b/src/main/resources/static/games/chastity/taskgame.html @@ -52,6 +52,9 @@ margin-top: 1rem; height: 2.75rem; } + #confirmModal { display:none; } + #confirmModal.open { display:flex; } + .level-display { display: flex; @@ -190,7 +193,7 @@ + + + @@ -340,6 +362,7 @@ async function startWithExcludedToys(gameSetId, excludedToyIds) { const params = new URLSearchParams({ aufgabenGruppeId: gameSetId }); + if (lockId) params.append('lockId', lockId); excludedToyIds.forEach(id => params.append('excludedToyIds', id)); const r = await fetch('/lock-game/init?' + params.toString(), { method: 'POST' }); @@ -417,7 +440,9 @@ await loadAndShowToys(sel.value); } - // ── Game Loop ───────────────────────────────────────────────────────────── + // ── Benötigt-Checkboxen ─────────────────────────────────────────────────── + +// ── Game Loop ───────────────────────────────────────────────────────────── function setGameCard(label, text, action, btnLabel) { document.getElementById('gameLabel').textContent = label; @@ -432,10 +457,16 @@ async function runGameLoop() { hide('gameCard'); hide('finisherBox'); + hide('tempOpeningBox'); clearTimer(); - if (_state.level >= 6) { - await showFinisherFlow(); + if (_state.finisher) { + showFinisherUI(); + return; + } + + if (_state.tempOpeningTime) { + showTempOpeningDialog(); return; } @@ -460,12 +491,14 @@ let sperre; try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; } setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten'); + } else if (_state.taskInQueue) { let aufgabe; try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; } const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis); setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done', hasDuration ? '▶ Starten' : '✓ Erledigt'); + } show('gameBox'); show('gameCard'); @@ -497,19 +530,38 @@ switch (_gameAction) { case 'queue-start': doQueueStart(); 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; } } async function doQueueStart() { 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' }); if (!r.ok) { showError('Fehler beim Starten'); return; } - const stateR = await fetch('/lock-game/state'); - _state = await stateR.json(); - await runGameLoop(); + + if (wasLock && tempUnlockRequired) { + 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)'); } } @@ -526,16 +578,28 @@ } catch (e) { showError(e.message || 'Fehler (Erledigt)'); } } - function doCancelCountdown() { + async function doCancelCountdown() { 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'; document.getElementById('gameBtn').textContent = '✓ Erledigt'; } async function doErledigt() { try { - await checkAndShowLocks(); - if (_state.level >= 6) { await showFinisherFlow(); return; } + 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); + } + } const r = await fetch('/lock-game/next-task', { method: 'POST' }); if (!r.ok) { showError('Fehler beim Ziehen'); return; } const stateR = await fetch('/lock-game/state'); @@ -557,51 +621,87 @@ } } - async function showFinisherFlow() { + function showTempOpeningDialog() { show('gameBox'); hide('gameCard'); hide('lockReleaseBox'); 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 { - const r = await fetch('/lock-game/release-locks'); - if (r.ok) { - const texts = await r.json(); - for (const text of texts) { - await waitForReleaseOk(text); - } - } - } catch (_) { /* ignorieren */ } + await fetch('/lock-game/end-temp-opening', { method: 'POST' }); + await fetch('/lock-game/abandon-task', { method: 'POST' }); + const stateR = await fetch('/lock-game/state'); + _state = await stateR.json(); + hide('tempOpeningBox'); + await runGameLoop(); + } catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); } + } - // 2. Finisher laden und Zeit messen - const finisherStartTime = Date.now(); - let finisher = null; - try { - const r = await fetch('/lock-game/finisher'); - if (r.ok) finisher = await r.json(); - } catch (_) { /* ignorieren */ } + function showFinisherUI() { + show('gameBox'); + hide('gameCard'); + hide('lockReleaseBox'); - document.getElementById('finisherTitle').textContent = finisher?.kurzText || ''; - document.getElementById('finisherText').textContent = finisher?.text || 'Glückwunsch – du hast Level 6 erreicht!'; + let finisher = {}; + 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 - await new Promise(resolve => { - document.getElementById('btnFinisherOk').onclick = resolve; - show('finisherBox'); - }); + if (_state.finisherStartedAt) { + hide('finisherStart'); + show('finisherRunning'); + startElapsedTimer(new Date(_state.finisherStartedAt)); + } else { + show('finisherStart'); + hide('finisherRunning'); + } + show('finisherBox'); + } - // 4. Zeit berechnen und Spiel beenden - const timeInMinutes = Math.round((Date.now() - finisherStartTime) / 60000); - const params = new URLSearchParams({ timeInMinutes }); - if (lockId) params.set('lockId', lockId); - await fetch('/lock-game/complete?' + params.toString(), { method: 'POST' }); + async function doStartFinisher() { + await fetch('/lock-game/start-finisher', { method: 'POST' }); + const r = await fetch('/lock-game/state'); + _state = await r.json(); + 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(); } + 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) { return new Promise(resolve => { - document.getElementById('releaseText').textContent = text; + hide('gameCard'); + document.getElementById('releaseText').textContent = text || ''; document.getElementById('btnReleaseOk').onclick = () => { hide('lockReleaseBox'); resolve(); @@ -658,7 +758,37 @@ 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(); + + diff --git a/src/test/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceTest.java b/src/test/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceTest.java index 2af0d90..64dbd64 100644 --- a/src/test/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceTest.java +++ b/src/test/java/de/oaa/xxx/games/chastity/cardlock/CardLockServiceTest.java @@ -29,7 +29,7 @@ class CardLockServiceTest { @BeforeEach void setUp() { lock = new CardLockEntity(); - lock.setPickEveryMinute(60); + lock.setPickEverySeconds(60); lock.setNextCardIn(LocalDateTime.now().minusMinutes(1)); // controllType bleibt null → lockControlFactory.create() wird nicht aufgerufen