Weitere Fehler im Chastity ingame game behoben
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
3
bin/main/db/migration/V7__fix_aktive_sperre_fuer_fk.sql
Normal file
3
bin/main/db/migration/V7__fix_aktive_sperre_fuer_fk.sql
Normal 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);
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -902,6 +902,10 @@
|
||||
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
|
||||
});
|
||||
|
||||
window.addEventListener('pageshow', (e) => {
|
||||
if (e.persisted && lockId) loadLock();
|
||||
});
|
||||
|
||||
async function loadLock() {
|
||||
const res = await fetch('/keyholder/cardlock/' + lockId);
|
||||
if (res.status === 404) {
|
||||
@@ -1322,7 +1326,13 @@
|
||||
const cdEl = document.getElementById('gameCardCountdown');
|
||||
function tick() {
|
||||
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);
|
||||
}
|
||||
tick();
|
||||
@@ -1335,7 +1345,8 @@
|
||||
const data = await res.json();
|
||||
const gameSetId = data.gameSetId;
|
||||
const url = '/games/chastity/taskgame.html?lockId=' + lockId
|
||||
+ (gameSetId ? '&gameSetId=' + gameSetId : '');
|
||||
+ (gameSetId ? '&gameSetId=' + gameSetId : '')
|
||||
+ '&fresh=1';
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
|
||||
@@ -405,13 +405,9 @@
|
||||
<div id="subGameSet" style="display:none;">
|
||||
<div class="form-row">
|
||||
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
||||
<div style="position:relative;">
|
||||
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
||||
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
||||
<div id="gameSetDropdown" class="gs-dropdown"></div>
|
||||
</div>
|
||||
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
|
||||
<input type="hidden" id="fGameSetId">
|
||||
<select id="fGameSetId" onchange="markDirty()">
|
||||
<option value="">Kein Aufgaben-Set</option>
|
||||
</select>
|
||||
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
@@ -757,8 +753,30 @@
|
||||
{ label: 'Lang' },
|
||||
{ label: 'Sehr lang' },
|
||||
];
|
||||
let _gameSetSearchTimer = null;
|
||||
let _gameSetResults = [];
|
||||
let _gameGroups = [];
|
||||
|
||||
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() {
|
||||
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
||||
@@ -791,72 +809,6 @@
|
||||
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 ──
|
||||
function openCardInfo(cardId) {
|
||||
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
||||
@@ -1300,17 +1252,12 @@
|
||||
|
||||
// Task-Karte und Spiel-Karte
|
||||
checkTaskCardSection();
|
||||
clearGameSet();
|
||||
checkGameCardSection();
|
||||
const gsi = template?.gameSpieldauerIdx ?? 2;
|
||||
document.getElementById('sldGameSpieldauer').value = gsi;
|
||||
updateGameSpieldauer(gsi);
|
||||
if (template?.gameSetId) {
|
||||
fetch(`/gruppe/${template.gameSetId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
|
||||
.catch(() => {});
|
||||
}
|
||||
populateGameSetSelect();
|
||||
document.getElementById('fGameSetId').value = template?.gameSetId || '';
|
||||
}
|
||||
|
||||
if (type === 'TIMELOCK') {
|
||||
@@ -1664,6 +1611,7 @@
|
||||
document.getElementById('templateList').innerHTML = '';
|
||||
document.getElementById('listEmpty').style.display = 'none';
|
||||
await loadTaskSets();
|
||||
await loadGameGroups();
|
||||
loadNextPage();
|
||||
loadSubscribedTemplates();
|
||||
}
|
||||
|
||||
@@ -22,22 +22,36 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.5rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
.game-text {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
white-space: pre-wrap;
|
||||
height: 14rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.game-timer {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
text-align: center;
|
||||
margin: 0.75rem 0;
|
||||
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-btn-row {
|
||||
margin-top: 1rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.level-display {
|
||||
display: flex;
|
||||
@@ -82,6 +96,20 @@
|
||||
}
|
||||
.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 {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
@@ -130,6 +158,21 @@
|
||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||
<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 -->
|
||||
<div id="initBox" class="init-box" style="display:none;">
|
||||
<h2>Spiel-Set auswählen</h2>
|
||||
@@ -143,22 +186,22 @@
|
||||
<!-- Laufendes Spiel -->
|
||||
<div id="gameBox" style="display:none;">
|
||||
|
||||
<!-- Task oder Lock in Queue -->
|
||||
<div id="queueBox" class="game-card" style="display:none;">
|
||||
<div class="game-label" id="queueLabel"></div>
|
||||
<div class="game-text" id="queueText"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button>
|
||||
<!-- Einheitliche Spielkarte -->
|
||||
<div id="gameCard" class="game-card" style="display:none;">
|
||||
<div class="game-label" id="gameLabel"></div>
|
||||
<div class="game-text" id="gameText"></div>
|
||||
<div class="game-timer" id="gameTimer"></div>
|
||||
<div class="game-btn-row">
|
||||
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktive Aufgabe (läuft) -->
|
||||
<div id="activeBox" class="game-card" style="display:none;">
|
||||
<div class="game-label">Aktive Aufgabe</div>
|
||||
<div class="game-text" id="activeText"></div>
|
||||
<div class="game-timer" id="activeTimer" style="display:none;"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
|
||||
<!-- Release-Text (Sperren) -->
|
||||
<div id="lockReleaseBox" class="game-card" style="display:none;">
|
||||
<div class="game-label">🔓 Sperre aufgehoben</div>
|
||||
<div class="game-text" id="releaseText"></div>
|
||||
<div style="margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnReleaseOk">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +211,12 @@
|
||||
<h2>Level 6 erreicht!</h2>
|
||||
<div class="game-label" id="finisherTitle"></div>
|
||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
||||
<button class="btn-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>
|
||||
@@ -187,17 +235,18 @@
|
||||
const params = new URLSearchParams(location.search);
|
||||
const lockId = params.get('lockId');
|
||||
const autoGameSetId = params.get('gameSetId');
|
||||
let _state = null;
|
||||
let _timerInt = null;
|
||||
let _pendingIsLock = false;
|
||||
let _pendingHasDuration = false;
|
||||
const freshStart = params.get('fresh') === '1';
|
||||
let _resolvedGameSetId = autoGameSetId;
|
||||
let _state = null;
|
||||
let _timerInt = null;
|
||||
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
|
||||
|
||||
function goBack() {
|
||||
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
||||
else history.back();
|
||||
}
|
||||
|
||||
async function completeGame() {
|
||||
async function debugExit() {
|
||||
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
||||
await fetch(url, { method: 'POST' });
|
||||
goBack();
|
||||
@@ -207,40 +256,134 @@
|
||||
|
||||
async function boot() {
|
||||
try {
|
||||
const r = await fetch('/lock-game/state');
|
||||
if (r.status === 404) {
|
||||
if (autoGameSetId) {
|
||||
await autoStartGame(autoGameSetId);
|
||||
} else {
|
||||
await loadGroups();
|
||||
if (!freshStart) {
|
||||
const r = await fetch('/lock-game/state');
|
||||
if (r.ok) {
|
||||
_state = await r.json();
|
||||
hide('loadingHint');
|
||||
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) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function autoStartGame(gameSetId) {
|
||||
// ── Toy-Auswahl ──────────────────────────────────────────────────────────
|
||||
|
||||
async function loadAndShowToys(gameSetId) {
|
||||
try {
|
||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + gameSetId, { method: 'POST' });
|
||||
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
const r = await fetch('/lock-game/toys?aufgabenGruppeId=' + gameSetId);
|
||||
if (!r.ok) throw new Error('Fehler beim Laden der Toys');
|
||||
const toys = await r.json();
|
||||
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) {
|
||||
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() {
|
||||
const r = await fetch('/lock-game/groups');
|
||||
const groups = r.ok ? await r.json() : [];
|
||||
|
||||
if (groups.length === 1) {
|
||||
_resolvedGameSetId = groups[0].gruppenId;
|
||||
await loadAndShowToys(groups[0].gruppenId);
|
||||
return;
|
||||
}
|
||||
|
||||
hide('loadingHint');
|
||||
const list = document.getElementById('groupList');
|
||||
if (groups.length === 0) {
|
||||
@@ -270,40 +413,39 @@
|
||||
if (!sel) return;
|
||||
hide('initBox');
|
||||
show('loadingHint');
|
||||
try {
|
||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' });
|
||||
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
hide('loadingHint');
|
||||
await runGameLoop();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
_resolvedGameSetId = sel.value;
|
||||
await loadAndShowToys(sel.value);
|
||||
}
|
||||
|
||||
// ── 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() {
|
||||
hide('queueBox');
|
||||
hide('activeBox');
|
||||
hide('gameCard');
|
||||
hide('finisherBox');
|
||||
clearTimer();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisher();
|
||||
await showFinisherFlow();
|
||||
return;
|
||||
}
|
||||
|
||||
renderLevelBar(_state.level);
|
||||
|
||||
// Aktive Aufgabe läuft noch
|
||||
if (_state.activeTask) {
|
||||
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue leer → nächsten Task holen
|
||||
if (!_state.taskInQueue && !_state.lockInQueue) {
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
@@ -317,90 +459,155 @@
|
||||
if (_state.lockInQueue) {
|
||||
let sperre;
|
||||
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
||||
_pendingIsLock = true;
|
||||
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
|
||||
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
|
||||
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
|
||||
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
|
||||
} else if (_state.taskInQueue) {
|
||||
let aufgabe;
|
||||
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
||||
_pendingIsLock = false;
|
||||
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe';
|
||||
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
|
||||
const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||
setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
|
||||
hasDuration ? '▶ Starten' : '✓ Erledigt');
|
||||
}
|
||||
show('gameBox');
|
||||
show('queueBox');
|
||||
show('gameCard');
|
||||
}
|
||||
|
||||
function showActiveTask(text, endIso) {
|
||||
document.getElementById('activeText').textContent = text;
|
||||
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) {
|
||||
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() {
|
||||
// Locks prüfen (nach jeder Aktion)
|
||||
await checkAndShowLocks();
|
||||
|
||||
if (_pendingIsLock || _pendingHasDuration) {
|
||||
// Task/Sperre aktivieren und Timer starten
|
||||
async function doQueueStart() {
|
||||
try {
|
||||
await checkAndShowLocks();
|
||||
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');
|
||||
_state = await stateR.json();
|
||||
} else {
|
||||
// Keine Dauer → sofort nächsten Task
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
}
|
||||
await runGameLoop();
|
||||
await runGameLoop();
|
||||
} catch (e) { showError(e.message || 'Fehler (Starten)'); }
|
||||
}
|
||||
|
||||
async function handleErledigt() {
|
||||
await checkAndShowLocks();
|
||||
async function doQueueDone() {
|
||||
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) {
|
||||
await showFinisher();
|
||||
return;
|
||||
}
|
||||
function doCancelCountdown() {
|
||||
clearTimer();
|
||||
_gameAction = 'active-done';
|
||||
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||
}
|
||||
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
await runGameLoop();
|
||||
async function doErledigt() {
|
||||
try {
|
||||
await checkAndShowLocks();
|
||||
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() {
|
||||
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||
if (!r.ok) return;
|
||||
const texts = await r.json();
|
||||
if (texts && texts.length > 0) {
|
||||
const valid = texts ? texts.filter(t => t != null && t !== '') : [];
|
||||
if (valid.length > 0) {
|
||||
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');
|
||||
await new Promise(res => setTimeout(res, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
async function showFinisher() {
|
||||
async function showFinisherFlow() {
|
||||
show('gameBox');
|
||||
const r = await fetch('/lock-game/finisher');
|
||||
if (!r.ok) {
|
||||
document.getElementById('finisherTitle').textContent = '';
|
||||
document.getElementById('finisherText').textContent = 'Glückwunsch – du hast Level 6 erreicht!';
|
||||
} else {
|
||||
const f = await r.json();
|
||||
document.getElementById('finisherTitle').textContent = f.kurzText || '';
|
||||
document.getElementById('finisherText').textContent = f.text || '';
|
||||
}
|
||||
show('finisherBox');
|
||||
hide('gameCard');
|
||||
hide('lockReleaseBox');
|
||||
hide('finisherBox');
|
||||
|
||||
// 1. Release-Texte sequenziell anzeigen
|
||||
try {
|
||||
const r = await fetch('/lock-game/release-locks');
|
||||
if (r.ok) {
|
||||
const texts = await r.json();
|
||||
for (const text of texts) {
|
||||
await waitForReleaseOk(text);
|
||||
}
|
||||
}
|
||||
} catch (_) { /* ignorieren */ }
|
||||
|
||||
// 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 ─────────────────────────────────────────────────────────────
|
||||
@@ -413,8 +620,12 @@
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function playSound(src) {
|
||||
try { new Audio(src).play().catch(() => {}); } catch (_) {}
|
||||
}
|
||||
|
||||
function startTimer(endDate, el) {
|
||||
el.style.display = '';
|
||||
el.classList.add('active');
|
||||
clearTimer();
|
||||
_timerInt = setInterval(() => {
|
||||
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||
@@ -422,7 +633,12 @@
|
||||
const s = String(diff % 60).padStart(2, '0');
|
||||
el.textContent = m + ':' + s;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||
}
|
||||
cardLockServiceFactory.create(l).penaltyLockGame();
|
||||
l.setGameCardParkedAt(null);
|
||||
cardlockRepository.save(l);
|
||||
result.put("gameCardParkedAt", null);
|
||||
result.put("gameSetId", null);
|
||||
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
|
||||
} else {
|
||||
result.put("gameCardParkedAt", l.getGameCardParkedAt().toString());
|
||||
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
|
||||
@@ -655,7 +656,7 @@ public class CardLockController {
|
||||
}
|
||||
} else {
|
||||
result.put("gameCardParkedAt", null);
|
||||
result.put("gameSetId", null);
|
||||
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
|
||||
}
|
||||
result.put("gameActive", l.isGameActive());
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
CardDTO card = null;
|
||||
if (lock.isKeyholderRequestedUnlock()
|
||||
|| (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
|
||||
LOGGER.info("[CardLock {}] Karte gezogen: GREEN (erzwungen – Keyholder oder Öffnungszeitlimit)", lock.getLockee());
|
||||
card = getGreenCard();
|
||||
} else if (lock.isAccumulatePicks()) {
|
||||
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
|
||||
@@ -139,9 +140,10 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
var cards = lock.getAvailableCards();
|
||||
if (!cards.isEmpty()) {
|
||||
var card = cards.get(new Random().nextInt(cards.size()));
|
||||
LOGGER.debug("Card drafted: {}", 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");
|
||||
return getGreenCard();
|
||||
@@ -155,20 +157,20 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
|
||||
public String doubleUp() {
|
||||
var cards = lock.getAvailableCards();
|
||||
LOGGER.debug("Double up {} cards", cards.size());
|
||||
int before = cards.size();
|
||||
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 "";
|
||||
}
|
||||
|
||||
public String reset() {
|
||||
LOGGER.debug("Reset to initial cards");
|
||||
lock.setAvailableCards(lock.getInitialCards());
|
||||
LOGGER.info("[CardLock {}] RESET: zurück auf {} initiale Karten", lock.getLockee(), lock.getInitialCards().size());
|
||||
return "";
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -182,7 +184,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
|
||||
lock.setFrozenUntil(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 "";
|
||||
}
|
||||
|
||||
@@ -200,6 +202,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
}
|
||||
}
|
||||
pendingTaskMode = lock.getTaskMode().name();
|
||||
LOGGER.info("[CardLock {}] TASK: Modus={}, testLock={}", lock.getLockee(), lock.getTaskMode(), lock.isTestLock());
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -209,21 +212,23 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
}
|
||||
|
||||
public String redCard() {
|
||||
LOGGER.info("[CardLock {}] RED: Rote Karte gezogen", lock.getLockee());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String yellowCard() {
|
||||
Random random = new Random();
|
||||
int before = lock.getAvailableCards().size();
|
||||
if (random.nextBoolean()) {
|
||||
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
||||
LOGGER.debug("Adding Red card");
|
||||
lock.getAvailableCards().add(CardEnum.RED);
|
||||
}
|
||||
LOGGER.info("[CardLock {}] YELLOW: Rote Karten hinzugefügt | Kartenanzahl {} -> {}", lock.getLockee(), before, lock.getAvailableCards().size());
|
||||
} else {
|
||||
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
||||
LOGGER.debug("Removing Red card if possible");
|
||||
lock.getAvailableCards().remove(CardEnum.RED);
|
||||
}
|
||||
LOGGER.info("[CardLock {}] YELLOW: Rote Karten entfernt | Kartenanzahl {} -> {}", lock.getLockee(), before, lock.getAvailableCards().size());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -250,6 +255,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
// ── Cum cards ─────────────────────────────────────────────────────────────
|
||||
|
||||
public String cum(boolean tempUnlock) {
|
||||
LOGGER.info("[CardLock {}] CUM: tempUnlock={}", lock.getLockee(), tempUnlock);
|
||||
if (tempUnlock) {
|
||||
startTempOpening(TempOpeningReason.CARD, 0);
|
||||
}
|
||||
@@ -278,10 +284,12 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
}
|
||||
|
||||
public String slowmo() {
|
||||
LOGGER.info("[CardLock {}] SLOWMO_CARD: Zeitlupen-Effekt ausgelöst", lock.getLockee());
|
||||
return "";
|
||||
}
|
||||
|
||||
public String speedup() {
|
||||
LOGGER.info("[CardLock {}] SPEEDUP_CARD: Beschleunigungs-Effekt ausgelöst", lock.getLockee());
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -290,7 +298,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
lock.setGameCardParkedAt(LocalDateTime.now());
|
||||
lock.setFrozenUntil(deadline);
|
||||
lock.setNextCardIn(deadline);
|
||||
lock.setOpenPicks(0);
|
||||
LOGGER.info("[CardLock {}] GAME_CARD: Spiel-Lock aktiv bis {}", lock.getLockee(), deadline);
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -313,4 +321,15 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ public abstract class BaseLockService {
|
||||
/** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */
|
||||
protected void afterHygieneClosing() {}
|
||||
|
||||
protected abstract void handleLockGameFinished(int timeInMinutes);
|
||||
|
||||
public abstract void penaltyLockGame();
|
||||
|
||||
public BaseLockService(
|
||||
CommunityVerificationVoteRepository communityVerificationVoteRepository,
|
||||
CommunityVerificationRepository communityVerificationRepository,
|
||||
@@ -141,11 +145,21 @@ public abstract class BaseLockService {
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
public void task(Task task) {
|
||||
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.setCurrentTaskDescription(task.getDescription());
|
||||
if (task.getMinutes() != null && task.getMinutes() > 0) {
|
||||
@@ -163,10 +177,14 @@ public abstract class BaseLockService {
|
||||
}
|
||||
|
||||
protected void applyRandomTask() {
|
||||
LOGGER.debug("Apply random task");
|
||||
var tasks = getLock().getTasks();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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 de.oaa.xxx.games.chastity.cardlock.CardLockServiceFactory;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
|
||||
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.FinisherEntity;
|
||||
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.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
||||
@@ -45,6 +49,7 @@ public class LockGameController {
|
||||
private final UserService userService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final CardLockServiceFactory cardLockServiceFactory;
|
||||
|
||||
public LockGameController(LockGameRepository lockGameRepository,
|
||||
LockGameLockRepository lockGameLockRepository,
|
||||
@@ -54,7 +59,8 @@ public class LockGameController {
|
||||
FinisherRepository finisherRepository,
|
||||
UserService userService,
|
||||
ObjectMapper objectMapper,
|
||||
CardlockRepository cardlockRepository) {
|
||||
CardlockRepository cardlockRepository,
|
||||
CardLockServiceFactory cardLockServiceFactory) {
|
||||
this.lockGameRepository = lockGameRepository;
|
||||
this.lockGameLockRepository = lockGameLockRepository;
|
||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||
@@ -64,6 +70,7 @@ public class LockGameController {
|
||||
this.userService = userService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
this.cardLockServiceFactory = cardLockServiceFactory;
|
||||
}
|
||||
|
||||
/** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */
|
||||
@@ -82,13 +89,55 @@ public class LockGameController {
|
||||
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.
|
||||
* Lädt die angegebene Aufgabengruppe und baut daraus die AufgabenList.
|
||||
* Lädt die angegebene Aufgabengruppe, filtert nach excludedToyIds und validiert.
|
||||
*/
|
||||
@Transactional
|
||||
@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();
|
||||
|
||||
var gruppeOpt = aufgabenGruppeRepository.findById(aufgabenGruppeId);
|
||||
@@ -98,13 +147,39 @@ public class LockGameController {
|
||||
if (gruppe.getUserId() != null && !gruppe.getUserId().equals(userId))
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
Set<UUID> excluded = excludedToyIds != null ? Set.copyOf(excludedToyIds) : Set.of();
|
||||
|
||||
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();
|
||||
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();
|
||||
var finisher = finisherRepository.findByAufgabenGruppe(gruppe).stream()
|
||||
.filter(f -> f.getBenoetigteToys() == null
|
||||
|| f.getBenoetigteToys().stream().noneMatch(t -> excluded.contains(t.getToyId())))
|
||||
.map(FinisherEntity::toFinisher).toList();
|
||||
|
||||
// Validate: each level 1–5 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();
|
||||
list.setAufgaben(aufgaben);
|
||||
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")
|
||||
public ResponseEntity<Map<String, Object>> applyTask(Principal principal) {
|
||||
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")
|
||||
public ResponseEntity<?> getFinisher(Principal principal) {
|
||||
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
|
||||
@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();
|
||||
var opt = lockGameRepository.findByUserId(userId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
@@ -219,6 +336,7 @@ public class LockGameController {
|
||||
if (lockId != null) {
|
||||
cardlockRepository.findById(lockId).ifPresent(l -> {
|
||||
if (l.getLockee().equals(userId)) {
|
||||
cardLockServiceFactory.create(l).lockGameFinished(timeInMinutes);
|
||||
l.setGameActive(false);
|
||||
l.setFrozenUntil(null);
|
||||
l.setNextCardIn(LocalDateTime.now()
|
||||
|
||||
@@ -6,6 +6,9 @@ import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -18,6 +21,8 @@ import de.oaa.xxx.games.common.aufgaben.Sperre;
|
||||
|
||||
public class LockGameService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LockGameService.class);
|
||||
|
||||
private LockGameEntity gamestate;
|
||||
private LockGameRepository lockGameRepository;
|
||||
private LockGameLockRepository lockGameLockRepository;
|
||||
@@ -40,20 +45,36 @@ public class LockGameService {
|
||||
}
|
||||
|
||||
public void initNextTask() throws JsonProcessingException {
|
||||
gamestate.setActiveTask(null);
|
||||
gamestate.setActiveTaskEnd(null);
|
||||
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;
|
||||
|
||||
if (gamestate.getLevel() <= 5) {
|
||||
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();
|
||||
} else {
|
||||
result = findAufgabe();
|
||||
}
|
||||
if (result == null) {
|
||||
result = "Pause...";
|
||||
gamestate.setTaskInQueue(result);
|
||||
LOGGER.info("[LockGame {}] Kein passender Eintrag gefunden → Pause (300s)", gamestate.getUserId());
|
||||
Aufgabe aufgabe = new Aufgabe();
|
||||
aufgabe.setText("Pause...");
|
||||
aufgabe.setSekundenVon(300);
|
||||
@@ -70,11 +91,15 @@ public class LockGameService {
|
||||
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
|
||||
var level = gamestate.getLevel();
|
||||
while (level > 0) {
|
||||
final var levelcp = level;
|
||||
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()) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
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));
|
||||
return sperre.getText();
|
||||
}
|
||||
@@ -87,12 +112,16 @@ public class LockGameService {
|
||||
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
|
||||
var level = gamestate.getLevel();
|
||||
while (level > 0) {
|
||||
final var levelcp = level;
|
||||
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();
|
||||
if (!aufgaben.isEmpty()) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
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));
|
||||
return aufgabe.getText();
|
||||
}
|
||||
@@ -108,12 +137,16 @@ public class LockGameService {
|
||||
gamestate.setActiveTask(aufgabe.getText());
|
||||
gamestate.setTaskInQueue(null);
|
||||
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);
|
||||
return time;
|
||||
} else if (gamestate.getLockInQueue() != null) {
|
||||
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);
|
||||
applyLock(lock);
|
||||
}
|
||||
@@ -157,7 +190,11 @@ public class LockGameService {
|
||||
entity.setGameId(gamestate.getGameId());
|
||||
entity.setLockFor(lock.getSperreFuer());
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ public class CommunityTaskVoteScheduler {
|
||||
int winnerIndex;
|
||||
if (entries.isEmpty()) {
|
||||
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 {
|
||||
int[] counts = new int[tasks.size()];
|
||||
for (var e : entries) {
|
||||
@@ -83,10 +83,12 @@ public class CommunityTaskVoteScheduler {
|
||||
if (counts[i] == max) winners.add(i);
|
||||
}
|
||||
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);
|
||||
LOG.info("[CardLock {}] COMMUNITY-Aufgabe vergeben: title='{}', minutes={}", vote.getLockId(), task.getTitle(), task.getMinutes());
|
||||
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
||||
assigned.setLockId(lock.getLockId());
|
||||
assigned.setTaskTitle(task.getTitle());
|
||||
|
||||
@@ -8,6 +8,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -28,6 +30,8 @@ import de.oaa.xxx.user.UserService;
|
||||
@RequestMapping("/games/chastity/keyholder/choices")
|
||||
public class KeyholderTaskChoiceController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(KeyholderTaskChoiceController.class);
|
||||
|
||||
private final CardlockRepository cardlockRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
||||
@@ -121,6 +125,10 @@ public class KeyholderTaskChoiceController {
|
||||
}
|
||||
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);
|
||||
keyholderTaskChoiceRepository.save(choice);
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ public class KeyholderTaskChoiceScheduler {
|
||||
|
||||
int taskIndex = new Random().nextInt(tasks.size());
|
||||
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();
|
||||
assigned.setLockId(lock.getLockId());
|
||||
|
||||
@@ -375,4 +375,15 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
|
||||
public int getUnlockcodeLenght() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -902,6 +902,10 @@
|
||||
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
|
||||
});
|
||||
|
||||
window.addEventListener('pageshow', (e) => {
|
||||
if (e.persisted && lockId) loadLock();
|
||||
});
|
||||
|
||||
async function loadLock() {
|
||||
const res = await fetch('/keyholder/cardlock/' + lockId);
|
||||
if (res.status === 404) {
|
||||
@@ -1322,7 +1326,13 @@
|
||||
const cdEl = document.getElementById('gameCardCountdown');
|
||||
function tick() {
|
||||
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);
|
||||
}
|
||||
tick();
|
||||
@@ -1335,7 +1345,8 @@
|
||||
const data = await res.json();
|
||||
const gameSetId = data.gameSetId;
|
||||
const url = '/games/chastity/taskgame.html?lockId=' + lockId
|
||||
+ (gameSetId ? '&gameSetId=' + gameSetId : '');
|
||||
+ (gameSetId ? '&gameSetId=' + gameSetId : '')
|
||||
+ '&fresh=1';
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
|
||||
@@ -405,13 +405,9 @@
|
||||
<div id="subGameSet" style="display:none;">
|
||||
<div class="form-row">
|
||||
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
||||
<div style="position:relative;">
|
||||
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
||||
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
||||
<div id="gameSetDropdown" class="gs-dropdown"></div>
|
||||
</div>
|
||||
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
|
||||
<input type="hidden" id="fGameSetId">
|
||||
<select id="fGameSetId" onchange="markDirty()">
|
||||
<option value="">Kein Aufgaben-Set</option>
|
||||
</select>
|
||||
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
@@ -757,8 +753,30 @@
|
||||
{ label: 'Lang' },
|
||||
{ label: 'Sehr lang' },
|
||||
];
|
||||
let _gameSetSearchTimer = null;
|
||||
let _gameSetResults = [];
|
||||
let _gameGroups = [];
|
||||
|
||||
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() {
|
||||
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
||||
@@ -791,72 +809,6 @@
|
||||
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 ──
|
||||
function openCardInfo(cardId) {
|
||||
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
||||
@@ -1300,17 +1252,12 @@
|
||||
|
||||
// Task-Karte und Spiel-Karte
|
||||
checkTaskCardSection();
|
||||
clearGameSet();
|
||||
checkGameCardSection();
|
||||
const gsi = template?.gameSpieldauerIdx ?? 2;
|
||||
document.getElementById('sldGameSpieldauer').value = gsi;
|
||||
updateGameSpieldauer(gsi);
|
||||
if (template?.gameSetId) {
|
||||
fetch(`/gruppe/${template.gameSetId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
|
||||
.catch(() => {});
|
||||
}
|
||||
populateGameSetSelect();
|
||||
document.getElementById('fGameSetId').value = template?.gameSetId || '';
|
||||
}
|
||||
|
||||
if (type === 'TIMELOCK') {
|
||||
@@ -1664,6 +1611,7 @@
|
||||
document.getElementById('templateList').innerHTML = '';
|
||||
document.getElementById('listEmpty').style.display = 'none';
|
||||
await loadTaskSets();
|
||||
await loadGameGroups();
|
||||
loadNextPage();
|
||||
loadSubscribedTemplates();
|
||||
}
|
||||
|
||||
@@ -22,22 +22,36 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.5rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
.game-text {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
white-space: pre-wrap;
|
||||
height: 14rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.game-timer {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
text-align: center;
|
||||
margin: 0.75rem 0;
|
||||
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-btn-row {
|
||||
margin-top: 1rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.level-display {
|
||||
display: flex;
|
||||
@@ -82,6 +96,20 @@
|
||||
}
|
||||
.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 {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
@@ -130,6 +158,21 @@
|
||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||
<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 -->
|
||||
<div id="initBox" class="init-box" style="display:none;">
|
||||
<h2>Spiel-Set auswählen</h2>
|
||||
@@ -143,22 +186,22 @@
|
||||
<!-- Laufendes Spiel -->
|
||||
<div id="gameBox" style="display:none;">
|
||||
|
||||
<!-- Task oder Lock in Queue -->
|
||||
<div id="queueBox" class="game-card" style="display:none;">
|
||||
<div class="game-label" id="queueLabel"></div>
|
||||
<div class="game-text" id="queueText"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button>
|
||||
<!-- Einheitliche Spielkarte -->
|
||||
<div id="gameCard" class="game-card" style="display:none;">
|
||||
<div class="game-label" id="gameLabel"></div>
|
||||
<div class="game-text" id="gameText"></div>
|
||||
<div class="game-timer" id="gameTimer"></div>
|
||||
<div class="game-btn-row">
|
||||
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktive Aufgabe (läuft) -->
|
||||
<div id="activeBox" class="game-card" style="display:none;">
|
||||
<div class="game-label">Aktive Aufgabe</div>
|
||||
<div class="game-text" id="activeText"></div>
|
||||
<div class="game-timer" id="activeTimer" style="display:none;"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
|
||||
<!-- Release-Text (Sperren) -->
|
||||
<div id="lockReleaseBox" class="game-card" style="display:none;">
|
||||
<div class="game-label">🔓 Sperre aufgehoben</div>
|
||||
<div class="game-text" id="releaseText"></div>
|
||||
<div style="margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnReleaseOk">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +211,12 @@
|
||||
<h2>Level 6 erreicht!</h2>
|
||||
<div class="game-label" id="finisherTitle"></div>
|
||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
||||
<button class="btn-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>
|
||||
@@ -187,17 +235,18 @@
|
||||
const params = new URLSearchParams(location.search);
|
||||
const lockId = params.get('lockId');
|
||||
const autoGameSetId = params.get('gameSetId');
|
||||
let _state = null;
|
||||
let _timerInt = null;
|
||||
let _pendingIsLock = false;
|
||||
let _pendingHasDuration = false;
|
||||
const freshStart = params.get('fresh') === '1';
|
||||
let _resolvedGameSetId = autoGameSetId;
|
||||
let _state = null;
|
||||
let _timerInt = null;
|
||||
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
|
||||
|
||||
function goBack() {
|
||||
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
||||
else history.back();
|
||||
}
|
||||
|
||||
async function completeGame() {
|
||||
async function debugExit() {
|
||||
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
||||
await fetch(url, { method: 'POST' });
|
||||
goBack();
|
||||
@@ -207,40 +256,134 @@
|
||||
|
||||
async function boot() {
|
||||
try {
|
||||
const r = await fetch('/lock-game/state');
|
||||
if (r.status === 404) {
|
||||
if (autoGameSetId) {
|
||||
await autoStartGame(autoGameSetId);
|
||||
} else {
|
||||
await loadGroups();
|
||||
if (!freshStart) {
|
||||
const r = await fetch('/lock-game/state');
|
||||
if (r.ok) {
|
||||
_state = await r.json();
|
||||
hide('loadingHint');
|
||||
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) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function autoStartGame(gameSetId) {
|
||||
// ── Toy-Auswahl ──────────────────────────────────────────────────────────
|
||||
|
||||
async function loadAndShowToys(gameSetId) {
|
||||
try {
|
||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + gameSetId, { method: 'POST' });
|
||||
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
const r = await fetch('/lock-game/toys?aufgabenGruppeId=' + gameSetId);
|
||||
if (!r.ok) throw new Error('Fehler beim Laden der Toys');
|
||||
const toys = await r.json();
|
||||
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) {
|
||||
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() {
|
||||
const r = await fetch('/lock-game/groups');
|
||||
const groups = r.ok ? await r.json() : [];
|
||||
|
||||
if (groups.length === 1) {
|
||||
_resolvedGameSetId = groups[0].gruppenId;
|
||||
await loadAndShowToys(groups[0].gruppenId);
|
||||
return;
|
||||
}
|
||||
|
||||
hide('loadingHint');
|
||||
const list = document.getElementById('groupList');
|
||||
if (groups.length === 0) {
|
||||
@@ -270,40 +413,39 @@
|
||||
if (!sel) return;
|
||||
hide('initBox');
|
||||
show('loadingHint');
|
||||
try {
|
||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' });
|
||||
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
hide('loadingHint');
|
||||
await runGameLoop();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
_resolvedGameSetId = sel.value;
|
||||
await loadAndShowToys(sel.value);
|
||||
}
|
||||
|
||||
// ── 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() {
|
||||
hide('queueBox');
|
||||
hide('activeBox');
|
||||
hide('gameCard');
|
||||
hide('finisherBox');
|
||||
clearTimer();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisher();
|
||||
await showFinisherFlow();
|
||||
return;
|
||||
}
|
||||
|
||||
renderLevelBar(_state.level);
|
||||
|
||||
// Aktive Aufgabe läuft noch
|
||||
if (_state.activeTask) {
|
||||
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue leer → nächsten Task holen
|
||||
if (!_state.taskInQueue && !_state.lockInQueue) {
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
@@ -317,90 +459,155 @@
|
||||
if (_state.lockInQueue) {
|
||||
let sperre;
|
||||
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
||||
_pendingIsLock = true;
|
||||
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
|
||||
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
|
||||
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
|
||||
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
|
||||
} else if (_state.taskInQueue) {
|
||||
let aufgabe;
|
||||
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
||||
_pendingIsLock = false;
|
||||
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe';
|
||||
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
|
||||
const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||
setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
|
||||
hasDuration ? '▶ Starten' : '✓ Erledigt');
|
||||
}
|
||||
show('gameBox');
|
||||
show('queueBox');
|
||||
show('gameCard');
|
||||
}
|
||||
|
||||
function showActiveTask(text, endIso) {
|
||||
document.getElementById('activeText').textContent = text;
|
||||
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) {
|
||||
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() {
|
||||
// Locks prüfen (nach jeder Aktion)
|
||||
await checkAndShowLocks();
|
||||
|
||||
if (_pendingIsLock || _pendingHasDuration) {
|
||||
// Task/Sperre aktivieren und Timer starten
|
||||
async function doQueueStart() {
|
||||
try {
|
||||
await checkAndShowLocks();
|
||||
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');
|
||||
_state = await stateR.json();
|
||||
} else {
|
||||
// Keine Dauer → sofort nächsten Task
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
}
|
||||
await runGameLoop();
|
||||
await runGameLoop();
|
||||
} catch (e) { showError(e.message || 'Fehler (Starten)'); }
|
||||
}
|
||||
|
||||
async function handleErledigt() {
|
||||
await checkAndShowLocks();
|
||||
async function doQueueDone() {
|
||||
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) {
|
||||
await showFinisher();
|
||||
return;
|
||||
}
|
||||
function doCancelCountdown() {
|
||||
clearTimer();
|
||||
_gameAction = 'active-done';
|
||||
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||
}
|
||||
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
await runGameLoop();
|
||||
async function doErledigt() {
|
||||
try {
|
||||
await checkAndShowLocks();
|
||||
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() {
|
||||
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||
if (!r.ok) return;
|
||||
const texts = await r.json();
|
||||
if (texts && texts.length > 0) {
|
||||
const valid = texts ? texts.filter(t => t != null && t !== '') : [];
|
||||
if (valid.length > 0) {
|
||||
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');
|
||||
await new Promise(res => setTimeout(res, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
async function showFinisher() {
|
||||
async function showFinisherFlow() {
|
||||
show('gameBox');
|
||||
const r = await fetch('/lock-game/finisher');
|
||||
if (!r.ok) {
|
||||
document.getElementById('finisherTitle').textContent = '';
|
||||
document.getElementById('finisherText').textContent = 'Glückwunsch – du hast Level 6 erreicht!';
|
||||
} else {
|
||||
const f = await r.json();
|
||||
document.getElementById('finisherTitle').textContent = f.kurzText || '';
|
||||
document.getElementById('finisherText').textContent = f.text || '';
|
||||
}
|
||||
show('finisherBox');
|
||||
hide('gameCard');
|
||||
hide('lockReleaseBox');
|
||||
hide('finisherBox');
|
||||
|
||||
// 1. Release-Texte sequenziell anzeigen
|
||||
try {
|
||||
const r = await fetch('/lock-game/release-locks');
|
||||
if (r.ok) {
|
||||
const texts = await r.json();
|
||||
for (const text of texts) {
|
||||
await waitForReleaseOk(text);
|
||||
}
|
||||
}
|
||||
} catch (_) { /* ignorieren */ }
|
||||
|
||||
// 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 ─────────────────────────────────────────────────────────────
|
||||
@@ -413,8 +620,12 @@
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function playSound(src) {
|
||||
try { new Audio(src).play().catch(() => {}); } catch (_) {}
|
||||
}
|
||||
|
||||
function startTimer(endDate, el) {
|
||||
el.style.display = '';
|
||||
el.classList.add('active');
|
||||
clearTimer();
|
||||
_timerInt = setInterval(() => {
|
||||
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||
@@ -422,7 +633,12 @@
|
||||
const s = String(diff % 60).padStart(2, '0');
|
||||
el.textContent = m + ':' + s;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user