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

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

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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);
}