Weitere Fehler im Chastity ingame game behoben
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-30 22:52:21 +02:00
parent 4bd4635faf
commit c472093f62
32 changed files with 1002 additions and 405 deletions

View File

@@ -0,0 +1,3 @@
ALTER TABLE aktive_sperre_fuer DROP FOREIGN KEY FK36uaxlluxoow36iy1pqd4ig8b;
ALTER TABLE aktive_sperre_fuer ADD CONSTRAINT fk_aktive_sperre_fuer_lock_game_lock
FOREIGN KEY (aktive_sperre_id) REFERENCES lock_game_lock (lock_game_lock_id);

View File

@@ -902,6 +902,10 @@
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.'; else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
}); });
window.addEventListener('pageshow', (e) => {
if (e.persisted && lockId) loadLock();
});
async function loadLock() { async function loadLock() {
const res = await fetch('/keyholder/cardlock/' + lockId); const res = await fetch('/keyholder/cardlock/' + lockId);
if (res.status === 404) { if (res.status === 404) {
@@ -1322,7 +1326,13 @@
const cdEl = document.getElementById('gameCardCountdown'); const cdEl = document.getElementById('gameCardCountdown');
function tick() { function tick() {
const diff = deadline - Date.now(); const diff = deadline - Date.now();
if (diff <= 0) { panel.style.display = 'none'; clearInterval(gameCardPanelTick); gameCardPanelTick = null; return; } if (diff <= 0) {
clearInterval(gameCardPanelTick); gameCardPanelTick = null;
panel.style.display = 'none';
fetch('/lock-game/penalty?lockId=' + lockId, { method: 'POST' }).catch(() => {});
loadLock();
return;
}
cdEl.textContent = fmtCountdown(diff); cdEl.textContent = fmtCountdown(diff);
} }
tick(); tick();
@@ -1335,7 +1345,8 @@
const data = await res.json(); const data = await res.json();
const gameSetId = data.gameSetId; const gameSetId = data.gameSetId;
const url = '/games/chastity/taskgame.html?lockId=' + lockId const url = '/games/chastity/taskgame.html?lockId=' + lockId
+ (gameSetId ? '&gameSetId=' + gameSetId : ''); + (gameSetId ? '&gameSetId=' + gameSetId : '')
+ '&fresh=1';
window.location.href = url; window.location.href = url;
} }

View File

@@ -405,13 +405,9 @@
<div id="subGameSet" style="display:none;"> <div id="subGameSet" style="display:none;">
<div class="form-row"> <div class="form-row">
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label> <label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
<div style="position:relative;"> <select id="fGameSetId" onchange="markDirty()">
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…" <option value="">Kein Aufgaben-Set</option>
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()"> </select>
<div id="gameSetDropdown" class="gs-dropdown"></div>
</div>
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
<input type="hidden" id="fGameSetId">
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div> <div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
</div> </div>
<div class="form-row" style="margin-bottom:0;"> <div class="form-row" style="margin-bottom:0;">
@@ -757,8 +753,30 @@
{ label: 'Lang' }, { label: 'Lang' },
{ label: 'Sehr lang' }, { label: 'Sehr lang' },
]; ];
let _gameSetSearchTimer = null; let _gameGroups = [];
let _gameSetResults = [];
async function loadGameGroups() {
try {
const res = await fetch('/lock-game/groups');
if (!res.ok) return;
_gameGroups = await res.json();
populateGameSetSelect();
} catch(e) { console.error(e); }
}
function populateGameSetSelect() {
const sel = document.getElementById('fGameSetId');
if (!sel) return;
const cur = sel.value;
sel.innerHTML = '<option value="">Kein Aufgaben-Set</option>';
_gameGroups.forEach(g => {
const opt = document.createElement('option');
opt.value = g.gruppenId;
opt.textContent = g.name + (g.beschreibung ? ' ' + g.beschreibung : '');
sel.appendChild(opt);
});
sel.value = cur;
}
function checkGameCardSection() { function checkGameCardSection() {
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0; const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
@@ -791,72 +809,6 @@
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label; document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label;
} }
function onGameSetSearchFocus() {
if (!document.getElementById('fGameSetId').value) onGameSetSearch(document.getElementById('gameSetSearch').value);
}
function onGameSetSearch(value) {
clearTimeout(_gameSetSearchTimer);
if (value.length === 0) {
_gameSetSearchTimer = setTimeout(() => doGameSetSearch(''), 0);
} else if (value.length < 2) {
document.getElementById('gameSetDropdown').style.display = 'none';
} else {
_gameSetSearchTimer = setTimeout(() => doGameSetSearch(value), 300);
}
}
async function doGameSetSearch(search) {
try {
const url = '/gruppe/chastity' + (search ? '?search=' + encodeURIComponent(search) : '');
const res = await fetch(url);
if (!res.ok) return;
const data = await res.json();
_gameSetResults = data.gruppen || [];
renderGameSetDropdown();
} catch(e) { console.error(e); }
}
function renderGameSetDropdown() {
const dd = document.getElementById('gameSetDropdown');
if (!dd) return;
if (!_gameSetResults.length) { dd.style.display = 'none'; return; }
dd.innerHTML = _gameSetResults.map(g => `
<div class="gs-dropdown-item" onclick="selectGameSet('${esc(g.gruppenId)}','${esc(g.name).replace(/'/g, "\\'")}')">
<div class="gs-item-name">${esc(g.name)}</div>
${g.beschreibung ? `<div class="gs-item-desc">${esc(g.beschreibung)}</div>` : ''}
</div>`).join('');
dd.style.display = 'block';
}
function selectGameSet(id, name, suppressDirty = false) {
document.getElementById('fGameSetId').value = id;
document.getElementById('gameSetSearch').value = '';
document.getElementById('gameSetDropdown').style.display = 'none';
document.getElementById('gameSetSelected').innerHTML =
`<span style="flex:1;">${esc(name)}</span>
<button type="button" onclick="clearGameSet()" title="Auswahl entfernen">✕</button>`;
document.getElementById('gameSetSelected').style.display = 'flex';
document.getElementById('errGameSet').style.display = 'none';
if (!suppressDirty) markDirty();
}
function clearGameSet() {
document.getElementById('fGameSetId').value = '';
document.getElementById('gameSetSearch').value = '';
document.getElementById('gameSetSelected').style.display = 'none';
document.getElementById('gameSetSelected').innerHTML = '';
markDirty();
}
document.addEventListener('click', e => {
const search = document.getElementById('gameSetSearch');
const dd = document.getElementById('gameSetDropdown');
if (dd && search && !search.contains(e.target) && !dd.contains(e.target)) {
dd.style.display = 'none';
}
});
// ── Karten-Info ── // ── Karten-Info ──
function openCardInfo(cardId) { function openCardInfo(cardId) {
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return; const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
@@ -1300,17 +1252,12 @@
// Task-Karte und Spiel-Karte // Task-Karte und Spiel-Karte
checkTaskCardSection(); checkTaskCardSection();
clearGameSet();
checkGameCardSection(); checkGameCardSection();
const gsi = template?.gameSpieldauerIdx ?? 2; const gsi = template?.gameSpieldauerIdx ?? 2;
document.getElementById('sldGameSpieldauer').value = gsi; document.getElementById('sldGameSpieldauer').value = gsi;
updateGameSpieldauer(gsi); updateGameSpieldauer(gsi);
if (template?.gameSetId) { populateGameSetSelect();
fetch(`/gruppe/${template.gameSetId}`) document.getElementById('fGameSetId').value = template?.gameSetId || '';
.then(r => r.ok ? r.json() : null)
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
.catch(() => {});
}
} }
if (type === 'TIMELOCK') { if (type === 'TIMELOCK') {
@@ -1664,6 +1611,7 @@
document.getElementById('templateList').innerHTML = ''; document.getElementById('templateList').innerHTML = '';
document.getElementById('listEmpty').style.display = 'none'; document.getElementById('listEmpty').style.display = 'none';
await loadTaskSets(); await loadTaskSets();
await loadGameGroups();
loadNextPage(); loadNextPage();
loadSubscribedTemplates(); loadSubscribedTemplates();
} }

View File

@@ -22,22 +22,36 @@
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
height: 1.2rem;
} }
.game-text { .game-text {
font-size: 1rem; font-size: 1rem;
line-height: 1.6; line-height: 1.6;
color: var(--color-text); color: var(--color-text);
white-space: pre-wrap; white-space: pre-wrap;
height: 14rem;
overflow-y: auto;
} }
.game-timer { .game-timer {
font-size: 2.2rem; font-size: 2.2rem;
font-weight: 700; font-weight: 700;
color: var(--color-primary); color: var(--color-primary);
text-align: center; text-align: center;
margin: 0.75rem 0;
letter-spacing: 0.04em; letter-spacing: 0.04em;
height: 4rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
margin-top: 0.5rem;
} }
.game-timer.active { opacity: 1; }
.game-timer.urgent { color: #e74c3c; } .game-timer.urgent { color: #e74c3c; }
.game-btn-row {
margin-top: 1rem;
height: 2.75rem;
}
.level-display { .level-display {
display: flex; display: flex;
@@ -82,6 +96,20 @@
} }
.group-item input[type=radio] { accent-color: var(--color-primary); } .group-item input[type=radio] { accent-color: var(--color-primary); }
.toy-item {
display: flex; align-items: center; gap: 0.6rem;
padding: 0.6rem 0.85rem; border-radius: 8px;
background: var(--color-card); border: 1px solid var(--color-secondary);
margin-bottom: 0.5rem; cursor: pointer; transition: border-color 0.15s; user-select: none;
}
.toy-item.is-checked { border-color: var(--color-primary); }
.toy-item input { accent-color: var(--color-primary); flex-shrink: 0; width: 14px; height: 14px; cursor: pointer; }
.toy-item span { flex: 1; min-width: 0; }
.toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
.toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
.toy-item-img { width: 38px; height: 38px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.toys-hint { font-size: 0.85rem; color: var(--color-muted); margin: 0 0 1rem; line-height: 1.5; }
.btn-primary { .btn-primary {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
@@ -130,6 +158,21 @@
<!-- Freigegebene Locks (checkLocks-Meldungen) --> <!-- Freigegebene Locks (checkLocks-Meldungen) -->
<div id="lockMessages" class="lock-messages" style="display:none;"></div> <div id="lockMessages" class="lock-messages" style="display:none;"></div>
<!-- Toy-Auswahl vor Spielstart -->
<div id="toyBox" style="display:none;">
<div class="game-card">
<div class="game-label">Verfügbare Toys</div>
<p class="toys-hint">
Deaktiviere Toys, die nicht zur Verfügung stehen.
Aufgaben, die diese benötigen, werden deaktiviert.
</p>
<div id="toyToggleList"></div>
<div style="margin-top:1.25rem;">
<button class="btn-primary" onclick="handleToyConfirm()">▶ Spiel starten</button>
</div>
</div>
</div>
<!-- Initialisierung: Gruppe wählen --> <!-- Initialisierung: Gruppe wählen -->
<div id="initBox" class="init-box" style="display:none;"> <div id="initBox" class="init-box" style="display:none;">
<h2>Spiel-Set auswählen</h2> <h2>Spiel-Set auswählen</h2>
@@ -143,22 +186,22 @@
<!-- Laufendes Spiel --> <!-- Laufendes Spiel -->
<div id="gameBox" style="display:none;"> <div id="gameBox" style="display:none;">
<!-- Task oder Lock in Queue --> <!-- Einheitliche Spielkarte -->
<div id="queueBox" class="game-card" style="display:none;"> <div id="gameCard" class="game-card" style="display:none;">
<div class="game-label" id="queueLabel"></div> <div class="game-label" id="gameLabel"></div>
<div class="game-text" id="queueText"></div> <div class="game-text" id="gameText"></div>
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;"> <div class="game-timer" id="gameTimer"></div>
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button> <div class="game-btn-row">
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
</div> </div>
</div> </div>
<!-- Aktive Aufgabe (läuft) --> <!-- Release-Text (Sperren) -->
<div id="activeBox" class="game-card" style="display:none;"> <div id="lockReleaseBox" class="game-card" style="display:none;">
<div class="game-label">Aktive Aufgabe</div> <div class="game-label">🔓 Sperre aufgehoben</div>
<div class="game-text" id="activeText"></div> <div class="game-text" id="releaseText"></div>
<div class="game-timer" id="activeTimer" style="display:none;"></div> <div style="margin-top:1.1rem;">
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;"> <button class="btn-primary" id="btnReleaseOk">OK</button>
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
</div> </div>
</div> </div>
@@ -168,7 +211,12 @@
<h2>Level 6 erreicht!</h2> <h2>Level 6 erreicht!</h2>
<div class="game-label" id="finisherTitle"></div> <div class="game-label" id="finisherTitle"></div>
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div> <div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
<button class="btn-primary" onclick="completeGame()" style="margin-top:1.25rem;">Spiel beenden</button> <button class="btn-primary" id="btnFinisherOk" style="margin-top:1.25rem;">OK</button>
</div>
<!-- Debug -->
<div style="margin-top:1.5rem;">
<button onclick="debugExit()" style="width:100%;padding:0.45rem;border-radius:8px;border:1px dashed #666;background:transparent;color:#666;font-size:0.78rem;cursor:pointer;">🐛 Debug exit</button>
</div> </div>
</div> </div>
@@ -187,17 +235,18 @@
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const lockId = params.get('lockId'); const lockId = params.get('lockId');
const autoGameSetId = params.get('gameSetId'); const autoGameSetId = params.get('gameSetId');
let _state = null; const freshStart = params.get('fresh') === '1';
let _timerInt = null; let _resolvedGameSetId = autoGameSetId;
let _pendingIsLock = false; let _state = null;
let _pendingHasDuration = false; let _timerInt = null;
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
function goBack() { function goBack() {
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId; if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
else history.back(); else history.back();
} }
async function completeGame() { async function debugExit() {
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : ''); const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
await fetch(url, { method: 'POST' }); await fetch(url, { method: 'POST' });
goBack(); goBack();
@@ -207,40 +256,134 @@
async function boot() { async function boot() {
try { try {
const r = await fetch('/lock-game/state'); if (!freshStart) {
if (r.status === 404) { const r = await fetch('/lock-game/state');
if (autoGameSetId) { if (r.ok) {
await autoStartGame(autoGameSetId); _state = await r.json();
} else { hide('loadingHint');
await loadGroups(); await runGameLoop();
return;
} }
return; if (r.status !== 404) throw new Error('Fehler beim Laden des Spielzustands');
}
// Fresh start or no existing game: resolve gameSetId, then show toy selection
let gameSetId = autoGameSetId;
if (!gameSetId && lockId) {
try {
const lockR = await fetch('/keyholder/cardlock/' + lockId);
if (lockR.ok) {
const lockData = await lockR.json();
gameSetId = lockData.gameSetId || null;
}
} catch (_) {}
}
_resolvedGameSetId = gameSetId;
if (gameSetId) {
await loadAndShowToys(gameSetId);
} else {
await loadGroups();
} }
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
_state = await r.json();
hide('loadingHint');
await runGameLoop();
} catch (e) { } catch (e) {
showError(e.message); showError(e.message);
} }
} }
async function autoStartGame(gameSetId) { // ── Toy-Auswahl ──────────────────────────────────────────────────────────
async function loadAndShowToys(gameSetId) {
try { try {
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + gameSetId, { method: 'POST' }); const r = await fetch('/lock-game/toys?aufgabenGruppeId=' + gameSetId);
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen'); if (!r.ok) throw new Error('Fehler beim Laden der Toys');
const stateR = await fetch('/lock-game/state'); const toys = await r.json();
_state = await stateR.json();
hide('loadingHint'); hide('loadingHint');
await runGameLoop();
const list = document.getElementById('toyToggleList');
if (toys.length === 0) {
list.innerHTML = '<p style="font-size:0.85rem;color:var(--color-muted);font-style:italic;margin:0;">'
+ 'Keine Toys erforderlich alle Aufgaben werden gespielt.</p>';
} else {
list.innerHTML = toys.map(t => `
<label class="toy-item is-checked">
<input type="checkbox" value="${esc(t.toyId)}" checked>
<span>
<span class="toy-item-name">${esc(t.name)}</span>
${t.beschreibung ? `<span class="toy-item-desc">${esc(t.beschreibung)}</span>` : ''}
</span>
${t.bild ? `<img class="toy-item-img" src="data:image/png;base64,${t.bild}" alt="">` : ''}
</label>`).join('');
list.addEventListener('change', e => {
const cb = e.target;
if (cb.type === 'checkbox') cb.closest('.toy-item')?.classList.toggle('is-checked', cb.checked);
}, { once: false });
}
show('toyBox');
} catch (e) { } catch (e) {
showError(e.message); showError(e.message);
} }
} }
async function handleToyConfirm() {
const excludedToyIds = [];
document.querySelectorAll('#toyToggleList input[type="checkbox"]').forEach(cb => {
if (!cb.checked) excludedToyIds.push(cb.value);
});
hide('toyBox');
show('loadingHint');
try {
await startWithExcludedToys(_resolvedGameSetId, excludedToyIds);
} catch (e) {
showError(e.message);
}
}
async function startWithExcludedToys(gameSetId, excludedToyIds) {
const params = new URLSearchParams({ aufgabenGruppeId: gameSetId });
excludedToyIds.forEach(id => params.append('excludedToyIds', id));
const r = await fetch('/lock-game/init?' + params.toString(), { method: 'POST' });
if (r.status === 422) {
const body = await r.json().catch(() => ({}));
await showValidationError(body.error || 'Das Aufgaben-Set ist nicht vollständig.');
return;
}
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
hide('loadingHint');
await runGameLoop();
}
async function showValidationError(msg) {
hide('loadingHint');
showError('Das Spiel kann nicht gestartet werden: ' + msg
+ ' Du wirst in Kürze zurückgeleitet und erhältst eine Strafe.');
if (lockId) {
fetch('/lock-game/penalty?lockId=' + lockId, { method: 'POST' }).catch(() => {});
}
let secs = 5;
const interval = setInterval(() => {
const box = document.getElementById('errorBox');
if (box) box.textContent = 'Das Spiel kann nicht gestartet werden: ' + msg
+ ` Rückleitung in ${--secs}s…`;
if (secs <= 0) { clearInterval(interval); goBack(); }
}, 1000);
}
async function loadGroups() { async function loadGroups() {
const r = await fetch('/lock-game/groups'); const r = await fetch('/lock-game/groups');
const groups = r.ok ? await r.json() : []; const groups = r.ok ? await r.json() : [];
if (groups.length === 1) {
_resolvedGameSetId = groups[0].gruppenId;
await loadAndShowToys(groups[0].gruppenId);
return;
}
hide('loadingHint'); hide('loadingHint');
const list = document.getElementById('groupList'); const list = document.getElementById('groupList');
if (groups.length === 0) { if (groups.length === 0) {
@@ -270,40 +413,39 @@
if (!sel) return; if (!sel) return;
hide('initBox'); hide('initBox');
show('loadingHint'); show('loadingHint');
try { _resolvedGameSetId = sel.value;
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' }); await loadAndShowToys(sel.value);
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
hide('loadingHint');
await runGameLoop();
} catch (e) {
showError(e.message);
}
} }
// ── Game Loop ───────────────────────────────────────────────────────────── // ── Game Loop ─────────────────────────────────────────────────────────────
function setGameCard(label, text, action, btnLabel) {
document.getElementById('gameLabel').textContent = label;
document.getElementById('gameText').textContent = text;
const timerEl = document.getElementById('gameTimer');
timerEl.classList.remove('active', 'urgent');
timerEl.textContent = '';
_gameAction = action;
document.getElementById('gameBtn').textContent = btnLabel;
}
async function runGameLoop() { async function runGameLoop() {
hide('queueBox'); hide('gameCard');
hide('activeBox');
hide('finisherBox'); hide('finisherBox');
clearTimer(); clearTimer();
if (_state.level >= 6) { if (_state.level >= 6) {
await showFinisher(); await showFinisherFlow();
return; return;
} }
renderLevelBar(_state.level); renderLevelBar(_state.level);
// Aktive Aufgabe läuft noch
if (_state.activeTask) { if (_state.activeTask) {
showActiveTask(_state.activeTask, _state.activeTaskEnd); showActiveTask(_state.activeTask, _state.activeTaskEnd);
return; return;
} }
// Queue leer → nächsten Task holen
if (!_state.taskInQueue && !_state.lockInQueue) { if (!_state.taskInQueue && !_state.lockInQueue) {
await fetch('/lock-game/next-task', { method: 'POST' }); await fetch('/lock-game/next-task', { method: 'POST' });
const r = await fetch('/lock-game/state'); const r = await fetch('/lock-game/state');
@@ -317,90 +459,155 @@
if (_state.lockInQueue) { if (_state.lockInQueue) {
let sperre; let sperre;
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; } try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
_pendingIsLock = true; setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
} else if (_state.taskInQueue) { } else if (_state.taskInQueue) {
let aufgabe; let aufgabe;
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; } try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
_pendingIsLock = false; const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis); setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe'; hasDuration ? '▶ Starten' : '✓ Erledigt');
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
} }
show('gameBox'); show('gameBox');
show('queueBox'); show('gameCard');
} }
function showActiveTask(text, endIso) { function showActiveTask(text, endIso) {
document.getElementById('activeText').textContent = text;
show('gameBox'); show('gameBox');
show('activeBox'); show('gameCard');
const timerEl = document.getElementById('gameTimer');
timerEl.classList.remove('active', 'urgent');
timerEl.textContent = '';
document.getElementById('gameLabel').textContent = 'Aktive Aufgabe';
document.getElementById('gameText').textContent = text;
if (endIso) { if (endIso) {
const end = new Date(endIso); const end = new Date(endIso);
startTimer(end, document.getElementById('activeTimer')); if (end > Date.now()) {
_gameAction = 'active-running';
document.getElementById('gameBtn').textContent = '✕ Abbrechen';
startTimer(end, timerEl);
return;
}
}
_gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt';
}
function handleGameBtn() {
switch (_gameAction) {
case 'queue-start': doQueueStart(); break;
case 'queue-done': doQueueDone(); break;
case 'active-running': doCancelCountdown(); break;
case 'active-done': doErledigt(); break;
} }
} }
async function handleOk() { async function doQueueStart() {
// Locks prüfen (nach jeder Aktion) try {
await checkAndShowLocks(); await checkAndShowLocks();
if (_pendingIsLock || _pendingHasDuration) {
// Task/Sperre aktivieren und Timer starten
const r = await fetch('/lock-game/apply-task', { method: 'POST' }); const r = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!r.ok) { showError('Fehler'); return; } if (!r.ok) { showError('Fehler beim Starten'); return; }
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
} else { await runGameLoop();
// Keine Dauer → sofort nächsten Task } catch (e) { showError(e.message || 'Fehler (Starten)'); }
await fetch('/lock-game/next-task', { method: 'POST' });
const r = await fetch('/lock-game/state');
_state = await r.json();
}
await runGameLoop();
} }
async function handleErledigt() { async function doQueueDone() {
await checkAndShowLocks(); try {
await checkAndShowLocks();
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden'); return; }
const nextR = await fetch('/lock-game/next-task', { method: 'POST' });
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
}
if (_state.level >= 6) { function doCancelCountdown() {
await showFinisher(); clearTimer();
return; _gameAction = 'active-done';
} document.getElementById('gameBtn').textContent = '✓ Erledigt';
}
await fetch('/lock-game/next-task', { method: 'POST' }); async function doErledigt() {
const r = await fetch('/lock-game/state'); try {
_state = await r.json(); await checkAndShowLocks();
await runGameLoop(); if (_state.level >= 6) { await showFinisherFlow(); return; }
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');
_state = await stateR.json();
await runGameLoop();
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
} }
async function checkAndShowLocks() { async function checkAndShowLocks() {
const r = await fetch('/lock-game/check-locks', { method: 'POST' }); const r = await fetch('/lock-game/check-locks', { method: 'POST' });
if (!r.ok) return; if (!r.ok) return;
const texts = await r.json(); const texts = await r.json();
if (texts && texts.length > 0) { const valid = texts ? texts.filter(t => t != null && t !== '') : [];
if (valid.length > 0) {
const box = document.getElementById('lockMessages'); const box = document.getElementById('lockMessages');
box.innerHTML = texts.map(t => `<p>🔓 ${esc(t)}</p>`).join(''); box.innerHTML = valid.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
show('lockMessages'); show('lockMessages');
await new Promise(res => setTimeout(res, 2000)); await new Promise(res => setTimeout(res, 2000));
} }
} }
async function showFinisher() { async function showFinisherFlow() {
show('gameBox'); show('gameBox');
const r = await fetch('/lock-game/finisher'); hide('gameCard');
if (!r.ok) { hide('lockReleaseBox');
document.getElementById('finisherTitle').textContent = ''; hide('finisherBox');
document.getElementById('finisherText').textContent = 'Glückwunsch du hast Level 6 erreicht!';
} else { // 1. Release-Texte sequenziell anzeigen
const f = await r.json(); try {
document.getElementById('finisherTitle').textContent = f.kurzText || ''; const r = await fetch('/lock-game/release-locks');
document.getElementById('finisherText').textContent = f.text || ''; if (r.ok) {
} const texts = await r.json();
show('finisherBox'); for (const text of texts) {
await waitForReleaseOk(text);
}
}
} catch (_) { /* ignorieren */ }
// 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 */ }
document.getElementById('finisherTitle').textContent = finisher?.kurzText || '';
document.getElementById('finisherText').textContent = finisher?.text || 'Glückwunsch du hast Level 6 erreicht!';
// 3. Warten bis Nutzer OK drückt
await new Promise(resolve => {
document.getElementById('btnFinisherOk').onclick = resolve;
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' });
goBack();
}
function waitForReleaseOk(text) {
return new Promise(resolve => {
document.getElementById('releaseText').textContent = text;
document.getElementById('btnReleaseOk').onclick = () => {
hide('lockReleaseBox');
resolve();
};
show('lockReleaseBox');
});
} }
// ── Level-Bar ───────────────────────────────────────────────────────────── // ── Level-Bar ─────────────────────────────────────────────────────────────
@@ -413,8 +620,12 @@
// ── Timer ───────────────────────────────────────────────────────────────── // ── Timer ─────────────────────────────────────────────────────────────────
function playSound(src) {
try { new Audio(src).play().catch(() => {}); } catch (_) {}
}
function startTimer(endDate, el) { function startTimer(endDate, el) {
el.style.display = ''; el.classList.add('active');
clearTimer(); clearTimer();
_timerInt = setInterval(() => { _timerInt = setInterval(() => {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000)); const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
@@ -422,7 +633,12 @@
const s = String(diff % 60).padStart(2, '0'); const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s; el.textContent = m + ':' + s;
el.classList.toggle('urgent', diff < 30); el.classList.toggle('urgent', diff < 30);
if (diff === 0) clearTimer(); if (diff === 0) {
clearTimer();
playSound('/audio/alarm.mp3');
_gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt';
}
}, 1000); }, 1000);
} }

View File

@@ -1 +1,27 @@
src/main/java/de/oaa/xxx/config/CookieFactory.java package de.oaa.xxx.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class CookieFactory {
private final boolean secure;
public CookieFactory(@Value("${app.cookie.secure:true}") boolean secure) {
this.secure = secure;
}
public ResponseCookie jwtCookie(String token, Duration maxAge) {
return ResponseCookie.from("jwt", token)
.httpOnly(true)
.secure(secure)
.sameSite("Strict")
.path("/")
.maxAge(maxAge)
.build();
}
}

View File

@@ -644,10 +644,11 @@ public class CardLockController {
meName + " hat die Spiel-Karte nicht innerhalb einer Stunde gestartet. Das Lock wurde für 4 Stunden eingefroren.", meName + " hat die Spiel-Karte nicht innerhalb einer Stunde gestartet. Das Lock wurde für 4 Stunden eingefroren.",
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE); "/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
} }
cardLockServiceFactory.create(l).penaltyLockGame();
l.setGameCardParkedAt(null); l.setGameCardParkedAt(null);
cardlockRepository.save(l); cardlockRepository.save(l);
result.put("gameCardParkedAt", null); result.put("gameCardParkedAt", null);
result.put("gameSetId", null); result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
} else { } else {
result.put("gameCardParkedAt", l.getGameCardParkedAt().toString()); result.put("gameCardParkedAt", l.getGameCardParkedAt().toString());
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null); result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
@@ -655,7 +656,7 @@ public class CardLockController {
} }
} else { } else {
result.put("gameCardParkedAt", null); result.put("gameCardParkedAt", null);
result.put("gameSetId", null); result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
} }
result.put("gameActive", l.isGameActive()); result.put("gameActive", l.isGameActive());

View File

@@ -116,6 +116,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
CardDTO card = null; CardDTO card = null;
if (lock.isKeyholderRequestedUnlock() if (lock.isKeyholderRequestedUnlock()
|| (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) { || (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
LOGGER.info("[CardLock {}] Karte gezogen: GREEN (erzwungen Keyholder oder Öffnungszeitlimit)", lock.getLockee());
card = getGreenCard(); card = getGreenCard();
} else if (lock.isAccumulatePicks()) { } else if (lock.isAccumulatePicks()) {
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) { if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
@@ -139,9 +140,10 @@ public class CardLockService extends BaseLockService implements LockControlCallb
var cards = lock.getAvailableCards(); var cards = lock.getAvailableCards();
if (!cards.isEmpty()) { if (!cards.isEmpty()) {
var card = cards.get(new Random().nextInt(cards.size())); var card = cards.get(new Random().nextInt(cards.size()));
LOGGER.debug("Card drafted: {}", card);
lock.getAvailableCards().remove(card); lock.getAvailableCards().remove(card);
return card.get().processCard(this); CardDTO result = card.get().processCard(this);
LOGGER.info("[CardLock {}] Karte gezogen: {} | Verbleibende Karten: {}", lock.getLockee(), result.card(), lock.getAvailableCards().size());
return result;
} }
LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte"); LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte");
return getGreenCard(); return getGreenCard();
@@ -155,20 +157,20 @@ public class CardLockService extends BaseLockService implements LockControlCallb
public String doubleUp() { public String doubleUp() {
var cards = lock.getAvailableCards(); var cards = lock.getAvailableCards();
LOGGER.debug("Double up {} cards", cards.size()); int before = cards.size();
lock.getAvailableCards().addAll(cards); lock.getAvailableCards().addAll(cards);
LOGGER.debug("Now {} cards", lock.getAvailableCards().size()); LOGGER.info("[CardLock {}] DOUBLE_UP: Karten verdoppelt {} -> {}", lock.getLockee(), before, lock.getAvailableCards().size());
return ""; return "";
} }
public String reset() { public String reset() {
LOGGER.debug("Reset to initial cards");
lock.setAvailableCards(lock.getInitialCards()); lock.setAvailableCards(lock.getInitialCards());
LOGGER.info("[CardLock {}] RESET: zurück auf {} initiale Karten", lock.getLockee(), lock.getInitialCards().size());
return ""; return "";
} }
public String green() { public String green() {
LOGGER.debug("Green Card drafted"); LOGGER.info("[CardLock {}] GREEN: Unlock-Code bereitgestellt (Code vorhanden: {})", lock.getLockee(), lock.getUnlockCode() != null);
return lock.getUnlockCode(); return lock.getUnlockCode();
} }
@@ -182,7 +184,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES); LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
lock.setFrozenUntil(frozenTill); lock.setFrozenUntil(frozenTill);
lock.setNextCardIn(frozenTill); lock.setNextCardIn(frozenTill);
LOGGER.debug("Frozen until {}", lock.getFrozenUntil()); LOGGER.info("[CardLock {}] FREEZE: eingefroren für {} Minuten (bis {})", lock.getLockee(), (long) multiplier, frozenTill);
return ""; return "";
} }
@@ -200,6 +202,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
} }
} }
pendingTaskMode = lock.getTaskMode().name(); pendingTaskMode = lock.getTaskMode().name();
LOGGER.info("[CardLock {}] TASK: Modus={}, testLock={}", lock.getLockee(), lock.getTaskMode(), lock.isTestLock());
return ""; return "";
} }
@@ -209,21 +212,23 @@ public class CardLockService extends BaseLockService implements LockControlCallb
} }
public String redCard() { public String redCard() {
LOGGER.info("[CardLock {}] RED: Rote Karte gezogen", lock.getLockee());
return ""; return "";
} }
public String yellowCard() { public String yellowCard() {
Random random = new Random(); Random random = new Random();
int before = lock.getAvailableCards().size();
if (random.nextBoolean()) { if (random.nextBoolean()) {
for (int i = 0; i < random.nextInt(1, 3); i++) { for (int i = 0; i < random.nextInt(1, 3); i++) {
LOGGER.debug("Adding Red card");
lock.getAvailableCards().add(CardEnum.RED); lock.getAvailableCards().add(CardEnum.RED);
} }
LOGGER.info("[CardLock {}] YELLOW: Rote Karten hinzugefügt | Kartenanzahl {} -> {}", lock.getLockee(), before, lock.getAvailableCards().size());
} else { } else {
for (int i = 0; i < random.nextInt(1, 3); i++) { for (int i = 0; i < random.nextInt(1, 3); i++) {
LOGGER.debug("Removing Red card if possible");
lock.getAvailableCards().remove(CardEnum.RED); lock.getAvailableCards().remove(CardEnum.RED);
} }
LOGGER.info("[CardLock {}] YELLOW: Rote Karten entfernt | Kartenanzahl {} -> {}", lock.getLockee(), before, lock.getAvailableCards().size());
} }
return ""; return "";
} }
@@ -250,6 +255,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
// ── Cum cards ───────────────────────────────────────────────────────────── // ── Cum cards ─────────────────────────────────────────────────────────────
public String cum(boolean tempUnlock) { public String cum(boolean tempUnlock) {
LOGGER.info("[CardLock {}] CUM: tempUnlock={}", lock.getLockee(), tempUnlock);
if (tempUnlock) { if (tempUnlock) {
startTempOpening(TempOpeningReason.CARD, 0); startTempOpening(TempOpeningReason.CARD, 0);
} }
@@ -278,10 +284,12 @@ public class CardLockService extends BaseLockService implements LockControlCallb
} }
public String slowmo() { public String slowmo() {
LOGGER.info("[CardLock {}] SLOWMO_CARD: Zeitlupen-Effekt ausgelöst", lock.getLockee());
return ""; return "";
} }
public String speedup() { public String speedup() {
LOGGER.info("[CardLock {}] SPEEDUP_CARD: Beschleunigungs-Effekt ausgelöst", lock.getLockee());
return ""; return "";
} }
@@ -290,7 +298,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
lock.setGameCardParkedAt(LocalDateTime.now()); lock.setGameCardParkedAt(LocalDateTime.now());
lock.setFrozenUntil(deadline); lock.setFrozenUntil(deadline);
lock.setNextCardIn(deadline); lock.setNextCardIn(deadline);
lock.setOpenPicks(0); LOGGER.info("[CardLock {}] GAME_CARD: Spiel-Lock aktiv bis {}", lock.getLockee(), deadline);
return ""; return "";
} }
@@ -313,4 +321,15 @@ public class CardLockService extends BaseLockService implements LockControlCallb
} }
return 1.0; return 1.0;
} }
@Override
protected void handleLockGameFinished(int timeInMinutes) {
int freezeTime = (int) (timeInMinutes * new Random().nextDouble(1.0, 4.0));
freeze(freezeTime);
}
@Override
public void penaltyLockGame() {
handleLockGameFinished(60);
}
} }

View File

@@ -70,6 +70,10 @@ public abstract class BaseLockService {
/** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */ /** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */
protected void afterHygieneClosing() {} protected void afterHygieneClosing() {}
protected abstract void handleLockGameFinished(int timeInMinutes);
public abstract void penaltyLockGame();
public BaseLockService( public BaseLockService(
CommunityVerificationVoteRepository communityVerificationVoteRepository, CommunityVerificationVoteRepository communityVerificationVoteRepository,
CommunityVerificationRepository communityVerificationRepository, CommunityVerificationRepository communityVerificationRepository,
@@ -141,11 +145,21 @@ public abstract class BaseLockService {
systemMessageService.send(senderId, receiverId, text, targetUrl, cause); systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
} }
// ── Lock-Game Abschluss ───────────────────────────────────────────────────
public void lockGameFinished(int timeInMinutes) {
LOGGER.info("[Lock {}] lockGameFinished nach {} Minuten", getLock().getLockee(), timeInMinutes);
handleLockGameFinished(timeInMinutes);
}
// ── Aufgaben ────────────────────────────────────────────────────────────── // ── Aufgaben ──────────────────────────────────────────────────────────────
public void task(Task task) { public void task(Task task) {
BaseLockEntity lock = getLock(); BaseLockEntity lock = getLock();
LOGGER.debug("Apply task {}", task); LOGGER.info("[Lock {}] Aufgabe zugewiesen: title='{}', minutes={}, description='{}'",
lock.getLockee(), task.getTitle(), task.getMinutes(), task.getDescription());
lock.setCurrentTask(task.getTitle()); lock.setCurrentTask(task.getTitle());
lock.setCurrentTaskDescription(task.getDescription()); lock.setCurrentTaskDescription(task.getDescription());
if (task.getMinutes() != null && task.getMinutes() > 0) { if (task.getMinutes() != null && task.getMinutes() > 0) {
@@ -163,10 +177,14 @@ public abstract class BaseLockService {
} }
protected void applyRandomTask() { protected void applyRandomTask() {
LOGGER.debug("Apply random task");
var tasks = getLock().getTasks(); var tasks = getLock().getTasks();
if (tasks != null && !tasks.isEmpty()) { if (tasks != null && !tasks.isEmpty()) {
task(tasks.get(new Random().nextInt(tasks.size()))); int idx = new Random().nextInt(tasks.size());
LOGGER.info("[Lock {}] RANDOM-Aufgabe gezogen: Index {} von {} verfügbaren Aufgaben",
getLock().getLockee(), idx, tasks.size());
task(tasks.get(idx));
} else {
LOGGER.warn("[Lock {}] RANDOM-Aufgabe: keine Aufgaben im Lock vorhanden", getLock().getLockee());
} }
} }

View File

@@ -5,8 +5,10 @@ import java.time.LocalDateTime;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.ObjectMapper; 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.cardlock.CardlockRepository;
import de.oaa.xxx.games.common.aufgaben.AufgabenList; import de.oaa.xxx.games.common.aufgaben.AufgabenList;
import de.oaa.xxx.games.common.aufgaben.AvailableIn; import de.oaa.xxx.games.common.aufgaben.AvailableIn;
@@ -24,6 +27,7 @@ import de.oaa.xxx.games.common.entity.AufgabeEntity;
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity; import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
import de.oaa.xxx.games.common.entity.FinisherEntity; import de.oaa.xxx.games.common.entity.FinisherEntity;
import de.oaa.xxx.games.common.entity.SperreEntity; import de.oaa.xxx.games.common.entity.SperreEntity;
import de.oaa.xxx.games.common.entity.ToyEntity;
import de.oaa.xxx.games.common.repository.AufgabeRepository; import de.oaa.xxx.games.common.repository.AufgabeRepository;
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository; import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
import de.oaa.xxx.games.common.repository.FinisherRepository; import de.oaa.xxx.games.common.repository.FinisherRepository;
@@ -45,6 +49,7 @@ public class LockGameController {
private final UserService userService; private final UserService userService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final CardLockServiceFactory cardLockServiceFactory;
public LockGameController(LockGameRepository lockGameRepository, public LockGameController(LockGameRepository lockGameRepository,
LockGameLockRepository lockGameLockRepository, LockGameLockRepository lockGameLockRepository,
@@ -54,7 +59,8 @@ public class LockGameController {
FinisherRepository finisherRepository, FinisherRepository finisherRepository,
UserService userService, UserService userService,
ObjectMapper objectMapper, ObjectMapper objectMapper,
CardlockRepository cardlockRepository) { CardlockRepository cardlockRepository,
CardLockServiceFactory cardLockServiceFactory) {
this.lockGameRepository = lockGameRepository; this.lockGameRepository = lockGameRepository;
this.lockGameLockRepository = lockGameLockRepository; this.lockGameLockRepository = lockGameLockRepository;
this.aufgabenGruppeRepository = aufgabenGruppeRepository; this.aufgabenGruppeRepository = aufgabenGruppeRepository;
@@ -64,6 +70,7 @@ public class LockGameController {
this.userService = userService; this.userService = userService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.cardLockServiceFactory = cardLockServiceFactory;
} }
/** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */ /** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */
@@ -82,13 +89,55 @@ public class LockGameController {
return ResponseEntity.ok(gruppen); return ResponseEntity.ok(gruppen);
} }
/**
* Gibt alle unique Toys zurück, die von Aufgaben, Sperren oder Finishern der Gruppe benötigt werden.
*/
@Transactional
@GetMapping("/toys")
public ResponseEntity<List<Map<String, Object>>> getToys(
@RequestParam UUID aufgabenGruppeId, Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
var gruppeOpt = aufgabenGruppeRepository.findById(aufgabenGruppeId);
if (gruppeOpt.isEmpty()) return ResponseEntity.notFound().build();
AufgabenGruppeEntity gruppe = gruppeOpt.get();
if (gruppe.getUserId() != null && !gruppe.getUserId().equals(userId))
return ResponseEntity.status(403).build();
Map<UUID, ToyEntity> toys = new java.util.LinkedHashMap<>();
aufgabeRepository.findByAufgabenGruppeIn(List.of(gruppe)).forEach(a -> {
if (a.getBenoetigteToys() != null)
a.getBenoetigteToys().forEach(t -> toys.putIfAbsent(t.getToyId(), t));
});
sperreRepository.findByAufgabenGruppeIn(List.of(gruppe)).forEach(s -> {
if (s.getBenoetigteToys() != null)
s.getBenoetigteToys().forEach(t -> toys.putIfAbsent(t.getToyId(), t));
});
finisherRepository.findByAufgabenGruppe(gruppe).forEach(f -> {
if (f.getBenoetigteToys() != null)
f.getBenoetigteToys().forEach(t -> toys.putIfAbsent(t.getToyId(), t));
});
var result = toys.values().stream().map(t -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("toyId", t.getToyId().toString());
m.put("name", t.getName());
m.put("beschreibung", t.getBeschreibung() != null ? t.getBeschreibung() : "");
m.put("bild", t.getBild() != null ? java.util.Base64.getEncoder().encodeToString(t.getBild()) : null);
return m;
}).toList();
return ResponseEntity.ok(result);
}
/** /**
* Initialisiert (oder startet neu) ein Lock-Game für den angemeldeten User. * Initialisiert (oder startet neu) ein Lock-Game für den angemeldeten User.
* Lädt die angegebene Aufgabengruppe und baut daraus die AufgabenList. * Lädt die angegebene Aufgabengruppe, filtert nach excludedToyIds und validiert.
*/ */
@Transactional @Transactional
@PostMapping("/init") @PostMapping("/init")
public ResponseEntity<?> init(@RequestParam UUID aufgabenGruppeId, Principal principal) { public ResponseEntity<?> init(
@RequestParam UUID aufgabenGruppeId,
@RequestParam(required = false) List<UUID> excludedToyIds,
Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
var gruppeOpt = aufgabenGruppeRepository.findById(aufgabenGruppeId); var gruppeOpt = aufgabenGruppeRepository.findById(aufgabenGruppeId);
@@ -98,13 +147,39 @@ public class LockGameController {
if (gruppe.getUserId() != null && !gruppe.getUserId().equals(userId)) if (gruppe.getUserId() != null && !gruppe.getUserId().equals(userId))
return ResponseEntity.status(403).build(); return ResponseEntity.status(403).build();
Set<UUID> excluded = excludedToyIds != null ? Set.copyOf(excludedToyIds) : Set.of();
var aufgaben = aufgabeRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream() var aufgaben = aufgabeRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream()
.filter(a -> a.getBenoetigteToys() == null
|| a.getBenoetigteToys().stream().noneMatch(t -> excluded.contains(t.getToyId())))
.map(AufgabeEntity::toAufgabe).toList(); .map(AufgabeEntity::toAufgabe).toList();
var sperren = sperreRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream() var sperren = sperreRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream()
.filter(s -> s.getBenoetigteToys() == null
|| s.getBenoetigteToys().stream().noneMatch(t -> excluded.contains(t.getToyId())))
.map(SperreEntity::toSperre).toList(); .map(SperreEntity::toSperre).toList();
var finisher = finisherRepository.findByAufgabenGruppe(gruppe).stream() var finisher = finisherRepository.findByAufgabenGruppe(gruppe).stream()
.filter(f -> f.getBenoetigteToys() == null
|| f.getBenoetigteToys().stream().noneMatch(t -> excluded.contains(t.getToyId())))
.map(FinisherEntity::toFinisher).toList(); .map(FinisherEntity::toFinisher).toList();
// Validate: each level 15 needs at least one Aufgabe and one Sperre; at least one Finisher
for (int level = 1; level <= 5; level++) {
final int l = level;
long aufgabenCount = aufgaben.stream().filter(a -> l == a.getLevel()).count();
long sperrenCount = sperren.stream().filter(s -> l == s.getLevel()).count();
if (aufgabenCount == 0 || sperrenCount == 0) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(Map.of(
"error", "Level " + level + " hat nach der Toy-Filterung keine "
+ (aufgabenCount == 0 ? "Aufgaben" : "Zeitstrafen") + " mehr."
));
}
}
if (finisher.isEmpty()) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(Map.of(
"error", "Kein Finisher nach der Toy-Filterung verfügbar."
));
}
AufgabenList list = new AufgabenList(); AufgabenList list = new AufgabenList();
list.setAufgaben(aufgaben); list.setAufgaben(aufgaben);
list.setSperren(sperren); list.setSperren(sperren);
@@ -165,6 +240,19 @@ public class LockGameController {
} }
} }
@PostMapping("/abandon-task")
public ResponseEntity<?> abandonTask(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.notFound().build();
try {
buildService(opt.get()).abandonActiveTask();
return ResponseEntity.noContent().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/apply-task") @PostMapping("/apply-task")
public ResponseEntity<Map<String, Object>> applyTask(Principal principal) { public ResponseEntity<Map<String, Object>> applyTask(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
@@ -190,6 +278,18 @@ public class LockGameController {
} }
} }
@GetMapping("/release-locks")
public ResponseEntity<List<String>> releaseLocks(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.ok(List.of());
try {
return ResponseEntity.ok(buildService(opt.get()).releaseLocks());
} catch (Exception e) {
return ResponseEntity.ok(List.of());
}
}
@GetMapping("/finisher") @GetMapping("/finisher")
public ResponseEntity<?> getFinisher(Principal principal) { public ResponseEntity<?> getFinisher(Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
@@ -206,9 +306,26 @@ public class LockGameController {
} }
} }
@Transactional
@PostMapping("/penalty")
public ResponseEntity<?> penaltyGame(@RequestParam UUID lockId, Principal principal) {
UUID userId = userService.requireUser(principal).getUserId();
cardlockRepository.findById(lockId).ifPresent(l -> {
if (l.getLockee().equals(userId)) {
l.setGameActive(false);
cardlockRepository.save(l);
cardLockServiceFactory.create(l).penaltyLockGame();
}
});
return ResponseEntity.noContent().build();
}
@Transactional @Transactional
@PostMapping("/complete") @PostMapping("/complete")
public ResponseEntity<?> completeGame(@RequestParam(required = false) UUID lockId, Principal principal) { public ResponseEntity<?> completeGame(
@RequestParam(required = false) UUID lockId,
@RequestParam(required = false, defaultValue = "0") int timeInMinutes,
Principal principal) {
UUID userId = userService.requireUser(principal).getUserId(); UUID userId = userService.requireUser(principal).getUserId();
var opt = lockGameRepository.findByUserId(userId); var opt = lockGameRepository.findByUserId(userId);
if (opt.isEmpty()) return ResponseEntity.notFound().build(); if (opt.isEmpty()) return ResponseEntity.notFound().build();
@@ -219,6 +336,7 @@ public class LockGameController {
if (lockId != null) { if (lockId != null) {
cardlockRepository.findById(lockId).ifPresent(l -> { cardlockRepository.findById(lockId).ifPresent(l -> {
if (l.getLockee().equals(userId)) { if (l.getLockee().equals(userId)) {
cardLockServiceFactory.create(l).lockGameFinished(timeInMinutes);
l.setGameActive(false); l.setGameActive(false);
l.setFrozenUntil(null); l.setFrozenUntil(null);
l.setNextCardIn(LocalDateTime.now() l.setNextCardIn(LocalDateTime.now()

View File

@@ -6,6 +6,9 @@ import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -18,6 +21,8 @@ import de.oaa.xxx.games.common.aufgaben.Sperre;
public class LockGameService { public class LockGameService {
private static final Logger LOGGER = LoggerFactory.getLogger(LockGameService.class);
private LockGameEntity gamestate; private LockGameEntity gamestate;
private LockGameRepository lockGameRepository; private LockGameRepository lockGameRepository;
private LockGameLockRepository lockGameLockRepository; private LockGameLockRepository lockGameLockRepository;
@@ -40,20 +45,36 @@ public class LockGameService {
} }
public void initNextTask() throws JsonProcessingException { public void initNextTask() throws JsonProcessingException {
gamestate.setActiveTask(null);
gamestate.setActiveTaskEnd(null);
checkLevel(); checkLevel();
lockGameRepository.save(gamestate);
pickNextTask();
}
public void abandonActiveTask() throws JsonProcessingException {
gamestate.setActiveTask(null);
gamestate.setActiveTaskEnd(null);
lockGameRepository.save(gamestate);
pickNextTask();
}
private void pickNextTask() throws JsonProcessingException {
String result = null; String result = null;
if (gamestate.getLevel() <= 5) { if (gamestate.getLevel() <= 5) {
int nextInt = new Random().nextInt(1, 100); int nextInt = new Random().nextInt(1, 100);
boolean isZeitstrafe = nextInt < 25;
LOGGER.info("[LockGame {}] Ziehung: Level={}, Würfel={}, Typ={}", gamestate.getUserId(),
gamestate.getLevel(), nextInt, isZeitstrafe ? "ZEITSTRAFE" : "AUFGABE");
if (nextInt < 25) { if (isZeitstrafe) {
result = findZeitstrafe(); result = findZeitstrafe();
} else { } else {
result = findAufgabe(); result = findAufgabe();
} }
if (result == null) { if (result == null) {
result = "Pause..."; LOGGER.info("[LockGame {}] Kein passender Eintrag gefunden → Pause (300s)", gamestate.getUserId());
gamestate.setTaskInQueue(result);
Aufgabe aufgabe = new Aufgabe(); Aufgabe aufgabe = new Aufgabe();
aufgabe.setText("Pause..."); aufgabe.setText("Pause...");
aufgabe.setSekundenVon(300); aufgabe.setSekundenVon(300);
@@ -70,11 +91,15 @@ public class LockGameService {
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList(); var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
var level = gamestate.getLevel(); var level = gamestate.getLevel();
while (level > 0) { while (level > 0) {
final var levelcp = level;
var sperren = aufgabenList.getSperren().stream() var sperren = aufgabenList.getSperren().stream()
.filter(sperre -> sperre.getSperreFuer().stream().noneMatch(item -> list.contains(item))).toList(); .filter(sperre -> sperre.getLevel() == levelcp && sperre.getSperreFuer().stream().noneMatch(item -> list.contains(item))).toList();
if (!sperren.isEmpty()) { if (!sperren.isEmpty()) {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
Sperre sperre = sperren.get(new Random().nextInt(sperren.size())); Sperre sperre = sperren.get(new Random().nextInt(sperren.size()));
LOGGER.info("[LockGame {}] ZEITSTRAFE gezogen: kurzText='{}', level={}, minuten={}-{}, sperreFuer={}",
gamestate.getUserId(), sperre.getKurzText(), sperre.getLevel(),
sperre.getMinutenVon(), sperre.getMinutenBis(), sperre.getSperreFuer());
gamestate.setLockInQueue(mapper.writeValueAsString(sperre)); gamestate.setLockInQueue(mapper.writeValueAsString(sperre));
return sperre.getText(); return sperre.getText();
} }
@@ -87,12 +112,16 @@ public class LockGameService {
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList(); var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
var level = gamestate.getLevel(); var level = gamestate.getLevel();
while (level > 0) { while (level > 0) {
final var levelcp = level;
var aufgaben = aufgabenList.getAufgaben().stream() var aufgaben = aufgabenList.getAufgaben().stream()
.filter(aufgabe -> aufgabe.getBenoetigtAktiv().stream().noneMatch(item -> list.contains(item))) .filter(aufgabe -> aufgabe.getLevel() == levelcp && aufgabe.getBenoetigtAktiv().stream().noneMatch(item -> list.contains(item)))
.toList(); .toList();
if (!aufgaben.isEmpty()) { if (!aufgaben.isEmpty()) {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
var aufgabe = aufgaben.get(new Random().nextInt(aufgaben.size())); var aufgabe = aufgaben.get(new Random().nextInt(aufgaben.size()));
LOGGER.info("[LockGame {}] AUFGABE gezogen: kurzText='{}', level={}, sekunden={}-{}",
gamestate.getUserId(), aufgabe.getKurzText(), aufgabe.getLevel(),
aufgabe.getSekundenVon(), aufgabe.getSekundenBis());
gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe)); gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe));
return aufgabe.getText(); return aufgabe.getText();
} }
@@ -108,12 +137,16 @@ public class LockGameService {
gamestate.setActiveTask(aufgabe.getText()); gamestate.setActiveTask(aufgabe.getText());
gamestate.setTaskInQueue(null); gamestate.setTaskInQueue(null);
var time = getAufgabeTime(aufgabe); var time = getAufgabeTime(aufgabe);
gamestate.setActiveTaskEnd(LocalDateTime.now().plusSeconds(time)); gamestate.setActiveTaskEnd(time > 0 ? LocalDateTime.now().plusSeconds(time) : null);
LOGGER.info("[LockGame {}] AUFGABE aktiv: kurzText='{}', berechnete Zeit={}s (Range: {}s-{}s)",
gamestate.getUserId(), aufgabe.getKurzText(), time,
aufgabe.getSekundenVon(), aufgabe.getSekundenBis());
lockGameRepository.save(gamestate); lockGameRepository.save(gamestate);
return time; return time;
} else if (gamestate.getLockInQueue() != null) { } else if (gamestate.getLockInQueue() != null) {
var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class); var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class);
gamestate.setActiveTask(lock.getText()); String displayText = lock.getText() != null ? lock.getText() : lock.getKurzText();
gamestate.setActiveTask(displayText != null ? displayText : "Zeitstrafe aktiv");
gamestate.setLockInQueue(null); gamestate.setLockInQueue(null);
applyLock(lock); applyLock(lock);
} }
@@ -157,7 +190,11 @@ public class LockGameService {
entity.setGameId(gamestate.getGameId()); entity.setGameId(gamestate.getGameId());
entity.setLockFor(lock.getSperreFuer()); entity.setLockFor(lock.getSperreFuer());
entity.setReleaseText(lock.getReleaseText()); entity.setReleaseText(lock.getReleaseText());
entity.setReleaseTime(LocalDateTime.now().plusMinutes(getLockTime(lock))); int lockMinutes = getLockTime(lock);
entity.setReleaseTime(LocalDateTime.now().plusMinutes(lockMinutes));
LOGGER.info("[LockGame {}] ZEITSTRAFE aktiv: kurzText='{}', berechnete Zeit={}min (Range: {}min-{}min), sperreFuer={}",
gamestate.getUserId(), lock.getKurzText(), lockMinutes,
lock.getMinutenVon(), lock.getMinutenBis(), lock.getSperreFuer());
lockGameLockRepository.save(entity); lockGameLockRepository.save(entity);
} }

View File

@@ -69,7 +69,7 @@ public class CommunityTaskVoteScheduler {
int winnerIndex; int winnerIndex;
if (entries.isEmpty()) { if (entries.isEmpty()) {
winnerIndex = new Random().nextInt(tasks.size()); winnerIndex = new Random().nextInt(tasks.size());
LOG.debug("No votes → random task index {}", winnerIndex); LOG.info("[CardLock {}] COMMUNITY-Vote: keine Stimmen zufällige Aufgabe, Index {}", vote.getLockId(), winnerIndex);
} else { } else {
int[] counts = new int[tasks.size()]; int[] counts = new int[tasks.size()];
for (var e : entries) { for (var e : entries) {
@@ -83,10 +83,12 @@ public class CommunityTaskVoteScheduler {
if (counts[i] == max) winners.add(i); if (counts[i] == max) winners.add(i);
} }
winnerIndex = winners.get(new Random().nextInt(winners.size())); winnerIndex = winners.get(new Random().nextInt(winners.size()));
LOG.debug("Vote winner: task index {} with {} votes", winnerIndex, max); LOG.info("[CardLock {}] COMMUNITY-Vote: Gewinner Index {} mit {} Stimmen (gesamt {} Stimmen)",
vote.getLockId(), winnerIndex, max, entries.size());
} }
Task task = tasks.get(winnerIndex); Task task = tasks.get(winnerIndex);
LOG.info("[CardLock {}] COMMUNITY-Aufgabe vergeben: title='{}', minutes={}", vote.getLockId(), task.getTitle(), task.getMinutes());
AssignedTaskEntity assigned = new AssignedTaskEntity(); AssignedTaskEntity assigned = new AssignedTaskEntity();
assigned.setLockId(lock.getLockId()); assigned.setLockId(lock.getLockId());
assigned.setTaskTitle(task.getTitle()); assigned.setTaskTitle(task.getTitle());

View File

@@ -8,6 +8,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -28,6 +30,8 @@ import de.oaa.xxx.user.UserService;
@RequestMapping("/games/chastity/keyholder/choices") @RequestMapping("/games/chastity/keyholder/choices")
public class KeyholderTaskChoiceController { public class KeyholderTaskChoiceController {
private static final Logger LOG = LoggerFactory.getLogger(KeyholderTaskChoiceController.class);
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository; private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
@@ -121,6 +125,10 @@ public class KeyholderTaskChoiceController {
} }
assignedTaskRepository.save(assigned); assignedTaskRepository.save(assigned);
LOG.info("[CardLock {}] KEYHOLDER-Aufgabe vergeben: title='{}', minutes={}, penaltyFreeze={}min, penaltyRedCards={}",
lock.getLockId(), task.getTitle(), task.getMinutes(),
assigned.getPenaltyFreezeMinutes(), assigned.getPenaltyRedCards());
choice.setActive(false); choice.setActive(false);
keyholderTaskChoiceRepository.save(choice); keyholderTaskChoiceRepository.save(choice);

View File

@@ -63,7 +63,8 @@ public class KeyholderTaskChoiceScheduler {
int taskIndex = new Random().nextInt(tasks.size()); int taskIndex = new Random().nextInt(tasks.size());
Task task = tasks.get(taskIndex); Task task = tasks.get(taskIndex);
LOG.debug("Keyholder did not choose in time → random task index {}", taskIndex); LOG.info("[CardLock {}] KEYHOLDER-Timeout: zufällige Aufgabe vergeben Index {}, title='{}', minutes={}",
lock.getLockId(), taskIndex, task.getTitle(), task.getMinutes());
AssignedTaskEntity assigned = new AssignedTaskEntity(); AssignedTaskEntity assigned = new AssignedTaskEntity();
assigned.setLockId(lock.getLockId()); assigned.setLockId(lock.getLockId());

View File

@@ -375,4 +375,15 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
public int getUnlockcodeLenght() { public int getUnlockcodeLenght() {
return lock.getUnlockCodeLength(); return lock.getUnlockCodeLength();
} }
@Override
protected void handleLockGameFinished(int timeInMinutes) {
int freezeTime = (int) (timeInMinutes * new Random().nextDouble(1.0, 4.0));
addTime(freezeTime);
}
@Override
public void penaltyLockGame() {
handleLockGameFinished(60);
}
} }

View File

@@ -0,0 +1,3 @@
ALTER TABLE aktive_sperre_fuer DROP FOREIGN KEY FK36uaxlluxoow36iy1pqd4ig8b;
ALTER TABLE aktive_sperre_fuer ADD CONSTRAINT fk_aktive_sperre_fuer_lock_game_lock
FOREIGN KEY (aktive_sperre_id) REFERENCES lock_game_lock (lock_game_lock_id);

View File

@@ -902,6 +902,10 @@
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.'; else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
}); });
window.addEventListener('pageshow', (e) => {
if (e.persisted && lockId) loadLock();
});
async function loadLock() { async function loadLock() {
const res = await fetch('/keyholder/cardlock/' + lockId); const res = await fetch('/keyholder/cardlock/' + lockId);
if (res.status === 404) { if (res.status === 404) {
@@ -1322,7 +1326,13 @@
const cdEl = document.getElementById('gameCardCountdown'); const cdEl = document.getElementById('gameCardCountdown');
function tick() { function tick() {
const diff = deadline - Date.now(); const diff = deadline - Date.now();
if (diff <= 0) { panel.style.display = 'none'; clearInterval(gameCardPanelTick); gameCardPanelTick = null; return; } if (diff <= 0) {
clearInterval(gameCardPanelTick); gameCardPanelTick = null;
panel.style.display = 'none';
fetch('/lock-game/penalty?lockId=' + lockId, { method: 'POST' }).catch(() => {});
loadLock();
return;
}
cdEl.textContent = fmtCountdown(diff); cdEl.textContent = fmtCountdown(diff);
} }
tick(); tick();
@@ -1335,7 +1345,8 @@
const data = await res.json(); const data = await res.json();
const gameSetId = data.gameSetId; const gameSetId = data.gameSetId;
const url = '/games/chastity/taskgame.html?lockId=' + lockId const url = '/games/chastity/taskgame.html?lockId=' + lockId
+ (gameSetId ? '&gameSetId=' + gameSetId : ''); + (gameSetId ? '&gameSetId=' + gameSetId : '')
+ '&fresh=1';
window.location.href = url; window.location.href = url;
} }

View File

@@ -405,13 +405,9 @@
<div id="subGameSet" style="display:none;"> <div id="subGameSet" style="display:none;">
<div class="form-row"> <div class="form-row">
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label> <label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
<div style="position:relative;"> <select id="fGameSetId" onchange="markDirty()">
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…" <option value="">Kein Aufgaben-Set</option>
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()"> </select>
<div id="gameSetDropdown" class="gs-dropdown"></div>
</div>
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
<input type="hidden" id="fGameSetId">
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div> <div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
</div> </div>
<div class="form-row" style="margin-bottom:0;"> <div class="form-row" style="margin-bottom:0;">
@@ -757,8 +753,30 @@
{ label: 'Lang' }, { label: 'Lang' },
{ label: 'Sehr lang' }, { label: 'Sehr lang' },
]; ];
let _gameSetSearchTimer = null; let _gameGroups = [];
let _gameSetResults = [];
async function loadGameGroups() {
try {
const res = await fetch('/lock-game/groups');
if (!res.ok) return;
_gameGroups = await res.json();
populateGameSetSelect();
} catch(e) { console.error(e); }
}
function populateGameSetSelect() {
const sel = document.getElementById('fGameSetId');
if (!sel) return;
const cur = sel.value;
sel.innerHTML = '<option value="">Kein Aufgaben-Set</option>';
_gameGroups.forEach(g => {
const opt = document.createElement('option');
opt.value = g.gruppenId;
opt.textContent = g.name + (g.beschreibung ? ' ' + g.beschreibung : '');
sel.appendChild(opt);
});
sel.value = cur;
}
function checkGameCardSection() { function checkGameCardSection() {
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0; const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
@@ -791,72 +809,6 @@
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label; document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label;
} }
function onGameSetSearchFocus() {
if (!document.getElementById('fGameSetId').value) onGameSetSearch(document.getElementById('gameSetSearch').value);
}
function onGameSetSearch(value) {
clearTimeout(_gameSetSearchTimer);
if (value.length === 0) {
_gameSetSearchTimer = setTimeout(() => doGameSetSearch(''), 0);
} else if (value.length < 2) {
document.getElementById('gameSetDropdown').style.display = 'none';
} else {
_gameSetSearchTimer = setTimeout(() => doGameSetSearch(value), 300);
}
}
async function doGameSetSearch(search) {
try {
const url = '/gruppe/chastity' + (search ? '?search=' + encodeURIComponent(search) : '');
const res = await fetch(url);
if (!res.ok) return;
const data = await res.json();
_gameSetResults = data.gruppen || [];
renderGameSetDropdown();
} catch(e) { console.error(e); }
}
function renderGameSetDropdown() {
const dd = document.getElementById('gameSetDropdown');
if (!dd) return;
if (!_gameSetResults.length) { dd.style.display = 'none'; return; }
dd.innerHTML = _gameSetResults.map(g => `
<div class="gs-dropdown-item" onclick="selectGameSet('${esc(g.gruppenId)}','${esc(g.name).replace(/'/g, "\\'")}')">
<div class="gs-item-name">${esc(g.name)}</div>
${g.beschreibung ? `<div class="gs-item-desc">${esc(g.beschreibung)}</div>` : ''}
</div>`).join('');
dd.style.display = 'block';
}
function selectGameSet(id, name, suppressDirty = false) {
document.getElementById('fGameSetId').value = id;
document.getElementById('gameSetSearch').value = '';
document.getElementById('gameSetDropdown').style.display = 'none';
document.getElementById('gameSetSelected').innerHTML =
`<span style="flex:1;">${esc(name)}</span>
<button type="button" onclick="clearGameSet()" title="Auswahl entfernen">✕</button>`;
document.getElementById('gameSetSelected').style.display = 'flex';
document.getElementById('errGameSet').style.display = 'none';
if (!suppressDirty) markDirty();
}
function clearGameSet() {
document.getElementById('fGameSetId').value = '';
document.getElementById('gameSetSearch').value = '';
document.getElementById('gameSetSelected').style.display = 'none';
document.getElementById('gameSetSelected').innerHTML = '';
markDirty();
}
document.addEventListener('click', e => {
const search = document.getElementById('gameSetSearch');
const dd = document.getElementById('gameSetDropdown');
if (dd && search && !search.contains(e.target) && !dd.contains(e.target)) {
dd.style.display = 'none';
}
});
// ── Karten-Info ── // ── Karten-Info ──
function openCardInfo(cardId) { function openCardInfo(cardId) {
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return; const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
@@ -1300,17 +1252,12 @@
// Task-Karte und Spiel-Karte // Task-Karte und Spiel-Karte
checkTaskCardSection(); checkTaskCardSection();
clearGameSet();
checkGameCardSection(); checkGameCardSection();
const gsi = template?.gameSpieldauerIdx ?? 2; const gsi = template?.gameSpieldauerIdx ?? 2;
document.getElementById('sldGameSpieldauer').value = gsi; document.getElementById('sldGameSpieldauer').value = gsi;
updateGameSpieldauer(gsi); updateGameSpieldauer(gsi);
if (template?.gameSetId) { populateGameSetSelect();
fetch(`/gruppe/${template.gameSetId}`) document.getElementById('fGameSetId').value = template?.gameSetId || '';
.then(r => r.ok ? r.json() : null)
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
.catch(() => {});
}
} }
if (type === 'TIMELOCK') { if (type === 'TIMELOCK') {
@@ -1664,6 +1611,7 @@
document.getElementById('templateList').innerHTML = ''; document.getElementById('templateList').innerHTML = '';
document.getElementById('listEmpty').style.display = 'none'; document.getElementById('listEmpty').style.display = 'none';
await loadTaskSets(); await loadTaskSets();
await loadGameGroups();
loadNextPage(); loadNextPage();
loadSubscribedTemplates(); loadSubscribedTemplates();
} }

View File

@@ -22,22 +22,36 @@
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
height: 1.2rem;
} }
.game-text { .game-text {
font-size: 1rem; font-size: 1rem;
line-height: 1.6; line-height: 1.6;
color: var(--color-text); color: var(--color-text);
white-space: pre-wrap; white-space: pre-wrap;
height: 14rem;
overflow-y: auto;
} }
.game-timer { .game-timer {
font-size: 2.2rem; font-size: 2.2rem;
font-weight: 700; font-weight: 700;
color: var(--color-primary); color: var(--color-primary);
text-align: center; text-align: center;
margin: 0.75rem 0;
letter-spacing: 0.04em; letter-spacing: 0.04em;
height: 4rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
margin-top: 0.5rem;
} }
.game-timer.active { opacity: 1; }
.game-timer.urgent { color: #e74c3c; } .game-timer.urgent { color: #e74c3c; }
.game-btn-row {
margin-top: 1rem;
height: 2.75rem;
}
.level-display { .level-display {
display: flex; display: flex;
@@ -82,6 +96,20 @@
} }
.group-item input[type=radio] { accent-color: var(--color-primary); } .group-item input[type=radio] { accent-color: var(--color-primary); }
.toy-item {
display: flex; align-items: center; gap: 0.6rem;
padding: 0.6rem 0.85rem; border-radius: 8px;
background: var(--color-card); border: 1px solid var(--color-secondary);
margin-bottom: 0.5rem; cursor: pointer; transition: border-color 0.15s; user-select: none;
}
.toy-item.is-checked { border-color: var(--color-primary); }
.toy-item input { accent-color: var(--color-primary); flex-shrink: 0; width: 14px; height: 14px; cursor: pointer; }
.toy-item span { flex: 1; min-width: 0; }
.toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
.toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
.toy-item-img { width: 38px; height: 38px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.toys-hint { font-size: 0.85rem; color: var(--color-muted); margin: 0 0 1rem; line-height: 1.5; }
.btn-primary { .btn-primary {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.75rem;
@@ -130,6 +158,21 @@
<!-- Freigegebene Locks (checkLocks-Meldungen) --> <!-- Freigegebene Locks (checkLocks-Meldungen) -->
<div id="lockMessages" class="lock-messages" style="display:none;"></div> <div id="lockMessages" class="lock-messages" style="display:none;"></div>
<!-- Toy-Auswahl vor Spielstart -->
<div id="toyBox" style="display:none;">
<div class="game-card">
<div class="game-label">Verfügbare Toys</div>
<p class="toys-hint">
Deaktiviere Toys, die nicht zur Verfügung stehen.
Aufgaben, die diese benötigen, werden deaktiviert.
</p>
<div id="toyToggleList"></div>
<div style="margin-top:1.25rem;">
<button class="btn-primary" onclick="handleToyConfirm()">▶ Spiel starten</button>
</div>
</div>
</div>
<!-- Initialisierung: Gruppe wählen --> <!-- Initialisierung: Gruppe wählen -->
<div id="initBox" class="init-box" style="display:none;"> <div id="initBox" class="init-box" style="display:none;">
<h2>Spiel-Set auswählen</h2> <h2>Spiel-Set auswählen</h2>
@@ -143,22 +186,22 @@
<!-- Laufendes Spiel --> <!-- Laufendes Spiel -->
<div id="gameBox" style="display:none;"> <div id="gameBox" style="display:none;">
<!-- Task oder Lock in Queue --> <!-- Einheitliche Spielkarte -->
<div id="queueBox" class="game-card" style="display:none;"> <div id="gameCard" class="game-card" style="display:none;">
<div class="game-label" id="queueLabel"></div> <div class="game-label" id="gameLabel"></div>
<div class="game-text" id="queueText"></div> <div class="game-text" id="gameText"></div>
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;"> <div class="game-timer" id="gameTimer"></div>
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button> <div class="game-btn-row">
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
</div> </div>
</div> </div>
<!-- Aktive Aufgabe (läuft) --> <!-- Release-Text (Sperren) -->
<div id="activeBox" class="game-card" style="display:none;"> <div id="lockReleaseBox" class="game-card" style="display:none;">
<div class="game-label">Aktive Aufgabe</div> <div class="game-label">🔓 Sperre aufgehoben</div>
<div class="game-text" id="activeText"></div> <div class="game-text" id="releaseText"></div>
<div class="game-timer" id="activeTimer" style="display:none;"></div> <div style="margin-top:1.1rem;">
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;"> <button class="btn-primary" id="btnReleaseOk">OK</button>
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
</div> </div>
</div> </div>
@@ -168,7 +211,12 @@
<h2>Level 6 erreicht!</h2> <h2>Level 6 erreicht!</h2>
<div class="game-label" id="finisherTitle"></div> <div class="game-label" id="finisherTitle"></div>
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div> <div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
<button class="btn-primary" onclick="completeGame()" style="margin-top:1.25rem;">Spiel beenden</button> <button class="btn-primary" id="btnFinisherOk" style="margin-top:1.25rem;">OK</button>
</div>
<!-- Debug -->
<div style="margin-top:1.5rem;">
<button onclick="debugExit()" style="width:100%;padding:0.45rem;border-radius:8px;border:1px dashed #666;background:transparent;color:#666;font-size:0.78rem;cursor:pointer;">🐛 Debug exit</button>
</div> </div>
</div> </div>
@@ -187,17 +235,18 @@
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const lockId = params.get('lockId'); const lockId = params.get('lockId');
const autoGameSetId = params.get('gameSetId'); const autoGameSetId = params.get('gameSetId');
let _state = null; const freshStart = params.get('fresh') === '1';
let _timerInt = null; let _resolvedGameSetId = autoGameSetId;
let _pendingIsLock = false; let _state = null;
let _pendingHasDuration = false; let _timerInt = null;
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
function goBack() { function goBack() {
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId; if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
else history.back(); else history.back();
} }
async function completeGame() { async function debugExit() {
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : ''); const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
await fetch(url, { method: 'POST' }); await fetch(url, { method: 'POST' });
goBack(); goBack();
@@ -207,40 +256,134 @@
async function boot() { async function boot() {
try { try {
const r = await fetch('/lock-game/state'); if (!freshStart) {
if (r.status === 404) { const r = await fetch('/lock-game/state');
if (autoGameSetId) { if (r.ok) {
await autoStartGame(autoGameSetId); _state = await r.json();
} else { hide('loadingHint');
await loadGroups(); await runGameLoop();
return;
} }
return; if (r.status !== 404) throw new Error('Fehler beim Laden des Spielzustands');
}
// Fresh start or no existing game: resolve gameSetId, then show toy selection
let gameSetId = autoGameSetId;
if (!gameSetId && lockId) {
try {
const lockR = await fetch('/keyholder/cardlock/' + lockId);
if (lockR.ok) {
const lockData = await lockR.json();
gameSetId = lockData.gameSetId || null;
}
} catch (_) {}
}
_resolvedGameSetId = gameSetId;
if (gameSetId) {
await loadAndShowToys(gameSetId);
} else {
await loadGroups();
} }
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
_state = await r.json();
hide('loadingHint');
await runGameLoop();
} catch (e) { } catch (e) {
showError(e.message); showError(e.message);
} }
} }
async function autoStartGame(gameSetId) { // ── Toy-Auswahl ──────────────────────────────────────────────────────────
async function loadAndShowToys(gameSetId) {
try { try {
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + gameSetId, { method: 'POST' }); const r = await fetch('/lock-game/toys?aufgabenGruppeId=' + gameSetId);
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen'); if (!r.ok) throw new Error('Fehler beim Laden der Toys');
const stateR = await fetch('/lock-game/state'); const toys = await r.json();
_state = await stateR.json();
hide('loadingHint'); hide('loadingHint');
await runGameLoop();
const list = document.getElementById('toyToggleList');
if (toys.length === 0) {
list.innerHTML = '<p style="font-size:0.85rem;color:var(--color-muted);font-style:italic;margin:0;">'
+ 'Keine Toys erforderlich alle Aufgaben werden gespielt.</p>';
} else {
list.innerHTML = toys.map(t => `
<label class="toy-item is-checked">
<input type="checkbox" value="${esc(t.toyId)}" checked>
<span>
<span class="toy-item-name">${esc(t.name)}</span>
${t.beschreibung ? `<span class="toy-item-desc">${esc(t.beschreibung)}</span>` : ''}
</span>
${t.bild ? `<img class="toy-item-img" src="data:image/png;base64,${t.bild}" alt="">` : ''}
</label>`).join('');
list.addEventListener('change', e => {
const cb = e.target;
if (cb.type === 'checkbox') cb.closest('.toy-item')?.classList.toggle('is-checked', cb.checked);
}, { once: false });
}
show('toyBox');
} catch (e) { } catch (e) {
showError(e.message); showError(e.message);
} }
} }
async function handleToyConfirm() {
const excludedToyIds = [];
document.querySelectorAll('#toyToggleList input[type="checkbox"]').forEach(cb => {
if (!cb.checked) excludedToyIds.push(cb.value);
});
hide('toyBox');
show('loadingHint');
try {
await startWithExcludedToys(_resolvedGameSetId, excludedToyIds);
} catch (e) {
showError(e.message);
}
}
async function startWithExcludedToys(gameSetId, excludedToyIds) {
const params = new URLSearchParams({ aufgabenGruppeId: gameSetId });
excludedToyIds.forEach(id => params.append('excludedToyIds', id));
const r = await fetch('/lock-game/init?' + params.toString(), { method: 'POST' });
if (r.status === 422) {
const body = await r.json().catch(() => ({}));
await showValidationError(body.error || 'Das Aufgaben-Set ist nicht vollständig.');
return;
}
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
hide('loadingHint');
await runGameLoop();
}
async function showValidationError(msg) {
hide('loadingHint');
showError('Das Spiel kann nicht gestartet werden: ' + msg
+ ' Du wirst in Kürze zurückgeleitet und erhältst eine Strafe.');
if (lockId) {
fetch('/lock-game/penalty?lockId=' + lockId, { method: 'POST' }).catch(() => {});
}
let secs = 5;
const interval = setInterval(() => {
const box = document.getElementById('errorBox');
if (box) box.textContent = 'Das Spiel kann nicht gestartet werden: ' + msg
+ ` Rückleitung in ${--secs}s…`;
if (secs <= 0) { clearInterval(interval); goBack(); }
}, 1000);
}
async function loadGroups() { async function loadGroups() {
const r = await fetch('/lock-game/groups'); const r = await fetch('/lock-game/groups');
const groups = r.ok ? await r.json() : []; const groups = r.ok ? await r.json() : [];
if (groups.length === 1) {
_resolvedGameSetId = groups[0].gruppenId;
await loadAndShowToys(groups[0].gruppenId);
return;
}
hide('loadingHint'); hide('loadingHint');
const list = document.getElementById('groupList'); const list = document.getElementById('groupList');
if (groups.length === 0) { if (groups.length === 0) {
@@ -270,40 +413,39 @@
if (!sel) return; if (!sel) return;
hide('initBox'); hide('initBox');
show('loadingHint'); show('loadingHint');
try { _resolvedGameSetId = sel.value;
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' }); await loadAndShowToys(sel.value);
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
hide('loadingHint');
await runGameLoop();
} catch (e) {
showError(e.message);
}
} }
// ── Game Loop ───────────────────────────────────────────────────────────── // ── Game Loop ─────────────────────────────────────────────────────────────
function setGameCard(label, text, action, btnLabel) {
document.getElementById('gameLabel').textContent = label;
document.getElementById('gameText').textContent = text;
const timerEl = document.getElementById('gameTimer');
timerEl.classList.remove('active', 'urgent');
timerEl.textContent = '';
_gameAction = action;
document.getElementById('gameBtn').textContent = btnLabel;
}
async function runGameLoop() { async function runGameLoop() {
hide('queueBox'); hide('gameCard');
hide('activeBox');
hide('finisherBox'); hide('finisherBox');
clearTimer(); clearTimer();
if (_state.level >= 6) { if (_state.level >= 6) {
await showFinisher(); await showFinisherFlow();
return; return;
} }
renderLevelBar(_state.level); renderLevelBar(_state.level);
// Aktive Aufgabe läuft noch
if (_state.activeTask) { if (_state.activeTask) {
showActiveTask(_state.activeTask, _state.activeTaskEnd); showActiveTask(_state.activeTask, _state.activeTaskEnd);
return; return;
} }
// Queue leer → nächsten Task holen
if (!_state.taskInQueue && !_state.lockInQueue) { if (!_state.taskInQueue && !_state.lockInQueue) {
await fetch('/lock-game/next-task', { method: 'POST' }); await fetch('/lock-game/next-task', { method: 'POST' });
const r = await fetch('/lock-game/state'); const r = await fetch('/lock-game/state');
@@ -317,90 +459,155 @@
if (_state.lockInQueue) { if (_state.lockInQueue) {
let sperre; let sperre;
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; } try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
_pendingIsLock = true; setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
} else if (_state.taskInQueue) { } else if (_state.taskInQueue) {
let aufgabe; let aufgabe;
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; } try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
_pendingIsLock = false; const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis); setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe'; hasDuration ? '▶ Starten' : '✓ Erledigt');
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
} }
show('gameBox'); show('gameBox');
show('queueBox'); show('gameCard');
} }
function showActiveTask(text, endIso) { function showActiveTask(text, endIso) {
document.getElementById('activeText').textContent = text;
show('gameBox'); show('gameBox');
show('activeBox'); show('gameCard');
const timerEl = document.getElementById('gameTimer');
timerEl.classList.remove('active', 'urgent');
timerEl.textContent = '';
document.getElementById('gameLabel').textContent = 'Aktive Aufgabe';
document.getElementById('gameText').textContent = text;
if (endIso) { if (endIso) {
const end = new Date(endIso); const end = new Date(endIso);
startTimer(end, document.getElementById('activeTimer')); if (end > Date.now()) {
_gameAction = 'active-running';
document.getElementById('gameBtn').textContent = '✕ Abbrechen';
startTimer(end, timerEl);
return;
}
}
_gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt';
}
function handleGameBtn() {
switch (_gameAction) {
case 'queue-start': doQueueStart(); break;
case 'queue-done': doQueueDone(); break;
case 'active-running': doCancelCountdown(); break;
case 'active-done': doErledigt(); break;
} }
} }
async function handleOk() { async function doQueueStart() {
// Locks prüfen (nach jeder Aktion) try {
await checkAndShowLocks(); await checkAndShowLocks();
if (_pendingIsLock || _pendingHasDuration) {
// Task/Sperre aktivieren und Timer starten
const r = await fetch('/lock-game/apply-task', { method: 'POST' }); const r = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!r.ok) { showError('Fehler'); return; } if (!r.ok) { showError('Fehler beim Starten'); return; }
const stateR = await fetch('/lock-game/state'); const stateR = await fetch('/lock-game/state');
_state = await stateR.json(); _state = await stateR.json();
} else { await runGameLoop();
// Keine Dauer → sofort nächsten Task } catch (e) { showError(e.message || 'Fehler (Starten)'); }
await fetch('/lock-game/next-task', { method: 'POST' });
const r = await fetch('/lock-game/state');
_state = await r.json();
}
await runGameLoop();
} }
async function handleErledigt() { async function doQueueDone() {
await checkAndShowLocks(); try {
await checkAndShowLocks();
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden'); return; }
const nextR = await fetch('/lock-game/next-task', { method: 'POST' });
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
}
if (_state.level >= 6) { function doCancelCountdown() {
await showFinisher(); clearTimer();
return; _gameAction = 'active-done';
} document.getElementById('gameBtn').textContent = '✓ Erledigt';
}
await fetch('/lock-game/next-task', { method: 'POST' }); async function doErledigt() {
const r = await fetch('/lock-game/state'); try {
_state = await r.json(); await checkAndShowLocks();
await runGameLoop(); if (_state.level >= 6) { await showFinisherFlow(); return; }
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');
_state = await stateR.json();
await runGameLoop();
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
} }
async function checkAndShowLocks() { async function checkAndShowLocks() {
const r = await fetch('/lock-game/check-locks', { method: 'POST' }); const r = await fetch('/lock-game/check-locks', { method: 'POST' });
if (!r.ok) return; if (!r.ok) return;
const texts = await r.json(); const texts = await r.json();
if (texts && texts.length > 0) { const valid = texts ? texts.filter(t => t != null && t !== '') : [];
if (valid.length > 0) {
const box = document.getElementById('lockMessages'); const box = document.getElementById('lockMessages');
box.innerHTML = texts.map(t => `<p>🔓 ${esc(t)}</p>`).join(''); box.innerHTML = valid.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
show('lockMessages'); show('lockMessages');
await new Promise(res => setTimeout(res, 2000)); await new Promise(res => setTimeout(res, 2000));
} }
} }
async function showFinisher() { async function showFinisherFlow() {
show('gameBox'); show('gameBox');
const r = await fetch('/lock-game/finisher'); hide('gameCard');
if (!r.ok) { hide('lockReleaseBox');
document.getElementById('finisherTitle').textContent = ''; hide('finisherBox');
document.getElementById('finisherText').textContent = 'Glückwunsch du hast Level 6 erreicht!';
} else { // 1. Release-Texte sequenziell anzeigen
const f = await r.json(); try {
document.getElementById('finisherTitle').textContent = f.kurzText || ''; const r = await fetch('/lock-game/release-locks');
document.getElementById('finisherText').textContent = f.text || ''; if (r.ok) {
} const texts = await r.json();
show('finisherBox'); for (const text of texts) {
await waitForReleaseOk(text);
}
}
} catch (_) { /* ignorieren */ }
// 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 */ }
document.getElementById('finisherTitle').textContent = finisher?.kurzText || '';
document.getElementById('finisherText').textContent = finisher?.text || 'Glückwunsch du hast Level 6 erreicht!';
// 3. Warten bis Nutzer OK drückt
await new Promise(resolve => {
document.getElementById('btnFinisherOk').onclick = resolve;
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' });
goBack();
}
function waitForReleaseOk(text) {
return new Promise(resolve => {
document.getElementById('releaseText').textContent = text;
document.getElementById('btnReleaseOk').onclick = () => {
hide('lockReleaseBox');
resolve();
};
show('lockReleaseBox');
});
} }
// ── Level-Bar ───────────────────────────────────────────────────────────── // ── Level-Bar ─────────────────────────────────────────────────────────────
@@ -413,8 +620,12 @@
// ── Timer ───────────────────────────────────────────────────────────────── // ── Timer ─────────────────────────────────────────────────────────────────
function playSound(src) {
try { new Audio(src).play().catch(() => {}); } catch (_) {}
}
function startTimer(endDate, el) { function startTimer(endDate, el) {
el.style.display = ''; el.classList.add('active');
clearTimer(); clearTimer();
_timerInt = setInterval(() => { _timerInt = setInterval(() => {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000)); const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
@@ -422,7 +633,12 @@
const s = String(diff % 60).padStart(2, '0'); const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s; el.textContent = m + ':' + s;
el.classList.toggle('urgent', diff < 30); el.classList.toggle('urgent', diff < 30);
if (diff === 0) clearTimer(); if (diff === 0) {
clearTimer();
playSound('/audio/alarm.mp3');
_gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt';
}
}, 1000); }, 1000);
} }