Files
xxx-sphere-web/bin/main/static/games/chastity/taskgame.html
Mario ae64ca6aa3
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Weitere Fixes am Taskgame
2026-05-03 20:08:58 +02:00

911 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Game xXx Sphere</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.game-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 1.5rem;
margin-bottom: 1.25rem;
}
.game-label {
font-size: 0.72rem;
font-weight: 700;
color: var(--color-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.5rem;
height: 1.2rem;
text-align: center;
}
.game-text {
font-size: 1rem;
line-height: 1.6;
color: var(--color-text);
white-space: pre-wrap;
height: 14rem;
overflow-y: auto;
text-align: center;
}
.game-timer {
font-size: 2.2rem;
font-weight: 700;
color: var(--color-primary);
text-align: center;
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;
}
#confirmModal { display:none; }
#confirmModal.open { display:flex; }
.level-display {
display: flex;
justify-content: center;
margin-bottom: 1.25rem;
}
.level-display img {
width: 72px;
height: 72px;
object-fit: contain;
}
.init-box {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 1.5rem;
}
.init-box h2 { font-size: 1.1rem; margin: 0 0 0.75rem; }
.group-list { display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem; }
.group-item {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.75rem 1rem;
border: 1px solid var(--color-secondary);
border-radius: 10px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.group-item:hover, .group-item.selected {
border-color: var(--color-primary);
background: rgba(var(--color-primary-rgb, 233,69,96), 0.06);
}
.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;
border-radius: 10px;
border: none;
background: var(--color-primary);
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-secondary {
width: 100%;
padding: 0.65rem;
border-radius: 10px;
border: 1px solid var(--color-secondary);
background: transparent;
color: var(--color-text);
font-size: 0.92rem;
cursor: pointer;
margin-top: 0.6rem;
}
#levelTrophy {
font-size: 3.5rem;
line-height: 72px;
width: 72px;
height: 72px;
text-align: center;
}
</style>
</head>
<body class="app">
<div class="main">
<div class="content">
<h2 style="margin-bottom:1.25rem;">🎯 Task Game</h2>
<!-- Level-Anzeige -->
<div class="level-display" id="levelDisplay" style="display:none;">
<img id="levelImg" src="" alt="Level">
<span id="levelTrophy" style="display:none;">🏆</span>
</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>
<p style="font-size:0.85rem;color:var(--color-muted);margin:0 0 1rem;">
Wähle die Aufgabengruppe für dieses Spiel.
</p>
<div id="groupList" class="group-list"></div>
<button class="btn-primary" id="btnStart" disabled onclick="startGame()">▶ Spiel starten</button>
</div>
<!-- Laufendes Spiel -->
<div id="gameBox" style="display:none;">
<!-- 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>
<!-- Release-Text (Sperren) -->
<div id="lockReleaseBox" class="game-card" style="display:none;">
<div class="game-label">🔓 Zeitstrafe verbüßt</div>
<div class="game-text" id="releaseText"></div>
<div class="game-timer"></div>
<div class="game-btn-row">
<button class="btn-primary" id="btnReleaseOk" style="width:100%;height:100%;">OK</button>
</div>
</div>
<!-- Temporäre Öffnung -->
<div id="tempOpeningBox" class="game-card" style="display:none;">
<!-- Phase 1: Öffnung aktiv -->
<div id="tempPhase1">
<div class="game-label" style="text-align:center;">🔓 Entsperrcode</div>
<div class="game-text" id="tempOpeningCode"
style="display:flex;align-items:center;justify-content:center;
font-size:1.8rem;font-weight:700;letter-spacing:0.18em;
color:var(--color-primary);white-space:normal;"></div>
<div class="game-timer" id="tempOpeningTimer" style="margin-top:0.75rem;"></div>
<div class="game-btn-row">
<button class="btn-primary" style="width:100%;height:100%;" onclick="doEndTempOpening()">✓ Erledigt</button>
</div>
</div>
<!-- Phase 2: Neuer Code zum Schließen -->
<div id="tempPhase2" style="display:none;">
<div class="game-label" style="text-align:center;">🔒 Lock schließen</div>
<div class="game-text" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.75rem;white-space:normal;">
<span style="font-size:0.9rem;color:var(--color-muted);text-align:center;">Nutze den Entsperrcode um den Schlüssel wieder zu verschließen.</span>
<div id="tempNewCode" style="font-size:1.8rem;font-weight:700;letter-spacing:0.18em;color:var(--color-primary);text-align:center;"></div>
<div id="tempScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;text-align:center;"></div>
</div>
<div class="game-timer"></div>
<div class="game-btn-row">
<button class="btn-primary" id="tempPhase2Btn" style="width:100%;height:100%;" onclick="startTempScramble()">OK</button>
</div>
</div>
</div>
<!-- Finisher -->
<div id="finisherBox" class="game-card" style="display:none;">
<div class="game-label" id="finisherLabel"></div>
<div class="game-text" id="finisherText"></div>
<div class="game-timer" id="finisherTimer"></div>
<div class="game-btn-row">
<button class="btn-primary" id="finisherBtn" style="width:100%;height:100%;"></button>
</div>
</div>
<!-- Debug -->
<div style="margin-top:1.5rem;">
<button onclick="debugExit()" style="width:100%;padding:0.45rem;border-radius:8px;border:1px dashed #666;background:transparent;color:#666;font-size:0.78rem;cursor:pointer;">🐛 Debug exit</button>
</div>
</div>
<div id="loadingHint" style="text-align:center;color:var(--color-muted);padding:2rem 0;font-size:0.9rem;">
Wird geladen…
</div>
<div id="errorBox" style="display:none;background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);
border-radius:10px;padding:1rem;font-size:0.88rem;color:#e74c3c;margin-top:1rem;"></div>
</div>
</div>
<div class="modal-backdrop" id="confirmModal">
<div class="modal" style="max-width:420px;">
<h2>Bestätigung</h2>
<p id="confirmModalText" style="color:var(--color-text);margin-bottom:1.25rem;line-height:1.5;"></p>
<div class="modal-actions">
<button class="btn-cancel" id="confirmModalCancel">Nein</button>
<button class="btn-save" id="confirmModalOk" style="background:var(--color-danger,#e74c3c);">Ja, abbrechen</button>
</div>
</div>
</div>
<script src="/js/icons.js"></script>
<script src="/js/nav.js"></script>
<script>
const params = new URLSearchParams(location.search);
const lockId = params.get('lockId');
const autoGameSetId = params.get('gameSetId');
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'
let _tempOpeningTimerInt = null;
let _tempScrambleTimer = null;
let _tempScrambleCd = null;
let _tempOpeningFromQueue = false; // true only when opening was triggered from a queued Zeitstrafe (doQueueStart)
function goBack() {
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
else history.back();
}
async function debugExit() {
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
await fetch(url, { method: 'POST' });
goBack();
}
// ── Init ──────────────────────────────────────────────────────────────────
async function boot() {
try {
if (!freshStart) {
const r = await fetch('/lock-game/state');
if (r.ok) {
_state = await r.json();
hide('loadingHint');
await runGameLoop();
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();
}
} catch (e) {
showError(e.message);
}
}
// ── Toy-Auswahl ──────────────────────────────────────────────────────────
async function loadAndShowToys(gameSetId) {
try {
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');
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 });
if (lockId) params.append('lockId', lockId);
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');
// Remove ?fresh=1 from URL so F5 resumes instead of restarting.
const cleanParams = new URLSearchParams(location.search);
cleanParams.delete('fresh');
const cleanSearch = cleanParams.toString();
history.replaceState(null, '', location.pathname + (cleanSearch ? '?' + cleanSearch : ''));
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) {
list.innerHTML = '<p style="font-size:0.85rem;color:var(--color-muted);">Keine passenden Aufgabengruppen gefunden.<br>Erstelle zuerst eine Gruppe vom Typ „Chastity Only".</p>';
} else {
list.innerHTML = groups.map(g => `
<label class="group-item" onclick="selectGroup(this)">
<input type="radio" name="gruppe" value="${g.gruppenId}" style="display:none;">
<div>
<div style="font-weight:600;font-size:0.95rem;">${esc(g.name)}</div>
${g.beschreibung ? `<div style="font-size:0.8rem;color:var(--color-muted);">${esc(g.beschreibung)}</div>` : ''}
</div>
</label>`).join('');
}
show('initBox');
}
function selectGroup(el) {
document.querySelectorAll('.group-item').forEach(i => i.classList.remove('selected'));
el.classList.add('selected');
el.querySelector('input').checked = true;
document.getElementById('btnStart').disabled = false;
}
async function startGame() {
const sel = document.querySelector('input[name="gruppe"]:checked');
if (!sel) return;
hide('initBox');
show('loadingHint');
_resolvedGameSetId = sel.value;
await loadAndShowToys(sel.value);
}
// ── Benötigt-Checkboxen ───────────────────────────────────────────────────
// ── 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() {
clearTimer();
if (_state.tempOpeningTime) {
hide('finisherBox');
showTempOpeningDialog();
return;
}
if (_state.finisher) {
hide('tempOpeningBox');
showFinisherUI();
return;
}
// Normal game states all use gameCard — show it once here so the card
// frame stays stable across transitions (queue ↔ active ↔ active-done).
show('gameBox');
show('gameCard');
hide('tempOpeningBox');
hide('finisherBox');
renderLevelBar(_state.level);
if (_state.activeTask) {
showActiveTask(_state.activeTask, _state.activeTaskEnd);
return;
}
if (!_state.taskInQueue && !_state.lockInQueue) {
await fetch('/lock-game/next-task', { method: 'POST' });
const r = await fetch('/lock-game/state');
_state = await r.json();
}
showQueue();
}
function showQueue() {
if (_state.lockInQueue) {
let sperre;
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
const btnLabel = sperre.tempUnlockRequired ? '⏱ Weiter zur temporären Öffnung' : '✓ Erledigt';
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', btnLabel);
} else if (_state.taskInQueue) {
let aufgabe;
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
hasDuration ? '▶ Starten' : '✓ Erledigt');
}
}
function showActiveTask(text, endIso) {
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);
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': openConfirmModal('Aufgabe wirklich abbrechen?', () => doCancelCountdown()); break;
case 'active-done': doErledigt(); break;
}
}
async function doQueueStart() {
try {
const wasLock = !!_state.lockInQueue;
let tempUnlockRequired = false;
if (wasLock) {
try { tempUnlockRequired = JSON.parse(_state.lockInQueue).tempUnlockRequired === true; } catch (_) {}
}
if (wasLock && tempUnlockRequired) {
// Erst Öffnung starten, Zeitstrafe wird nach der Öffnung angewendet
const r = await fetch('/lock-game/start-temp-opening', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten der Öffnung'); return; }
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
_tempOpeningFromQueue = true;
showTempOpeningDialog();
} else {
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!r.ok) { showError('Fehler beim Starten'); return; }
if (wasLock) {
const nextR = await fetch('/lock-game/abandon-task', { method: 'POST' });
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
}
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
}
} catch (e) { showError(e.message || 'Fehler (Starten)'); }
}
async function doQueueDone() {
try {
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) {
const sperren = await lockR.json();
for (const sperre of (sperren || [])) {
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
}
}
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)'); }
}
async function doCancelCountdown() {
clearTimer();
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) {
const sperren = await lockR.json();
for (const sperre of (sperren || [])) {
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
}
}
_gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt';
}
async function doErledigt() {
try {
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
if (lockR.ok) {
const sperren = await lockR.json();
for (const sperre of (sperren || [])) {
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
}
}
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)'); }
}
function showTempOpeningDialog() {
show('gameBox');
hide('gameCard');
hide('lockReleaseBox');
hide('finisherBox');
document.getElementById('tempOpeningCode').textContent = _state.tempOpeningCode || '';
show('tempPhase1');
hide('tempPhase2');
const timerEl = document.getElementById('tempOpeningTimer');
timerEl.textContent = '';
timerEl.classList.remove('active', 'urgent');
if (_state.tempOpeningTime && _state.tempOpeningDuration) {
const endTime = new Date(new Date(_state.tempOpeningTime).getTime() + _state.tempOpeningDuration * 60000);
if (endTime > Date.now()) startTempOpeningTimer(endTime, timerEl);
}
show('tempOpeningBox');
}
function startTempOpeningTimer(endDate, el) {
if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
el.classList.add('active');
function tick() {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s;
el.classList.toggle('urgent', diff < 30);
if (diff === 0) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
}
tick();
_tempOpeningTimerInt = setInterval(tick, 1000);
}
async function doEndTempOpening() {
try {
if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
// Only apply the queued Zeitstrafe when the opening was explicitly started from one (Path 1).
// When triggered by a background lock expiry via checkLocks() (Path 2), lockInQueue may be set
// to the *next* randomly-picked item — applying it would incorrectly create another background
// lock with tempUnlockRequired and produce an infinite opening loop.
const fromQueue = _tempOpeningFromQueue;
_tempOpeningFromQueue = false;
const hasLockInQueue = fromQueue && !!_state.lockInQueue;
if (hasLockInQueue) {
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
if (!applyR.ok) { showError('Fehler beim Anwenden der Zeitstrafe'); return; }
}
const endR = await fetch('/lock-game/end-temp-opening', { method: 'POST' });
if (!endR.ok) { showError('Fehler beim Beenden der Öffnung'); return; }
const endData = await endR.json();
const newCode = endData.newUnlockCode;
if (newCode) {
document.getElementById('tempNewCode').textContent = newCode;
document.getElementById('tempScrambleCountdown').style.display = 'none';
document.getElementById('tempPhase2Btn').textContent = 'OK';
document.getElementById('tempPhase2Btn').onclick = startTempScramble;
hide('tempPhase1');
show('tempPhase2');
} else {
await finishTempOpening();
}
} catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); }
}
async function finishTempOpening() {
hide('tempOpeningBox');
await fetch('/lock-game/abandon-task', { method: 'POST' });
const stateR = await fetch('/lock-game/state');
_state = await stateR.json();
await runGameLoop();
}
function startTempScramble() {
const codeEl = document.getElementById('tempNewCode');
const cdEl = document.getElementById('tempScrambleCountdown');
const btnEl = document.getElementById('tempPhase2Btn');
const len = codeEl.textContent.length;
const DURATION = 3 * 60;
let remaining = DURATION;
let stopped = false;
function randomCode() {
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
}
async function finish() {
stopped = true;
clearInterval(_tempScrambleTimer); _tempScrambleTimer = null;
clearInterval(_tempScrambleCd); _tempScrambleCd = null;
await finishTempOpening();
}
cdEl.style.display = '';
btnEl.textContent = 'Abbrechen';
btnEl.onclick = finish;
function updateCd() {
const m = Math.floor(remaining / 60);
const s = remaining % 60;
cdEl.textContent = `${m}:${String(s).padStart(2, '0')}`;
}
updateCd();
_tempScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
_tempScrambleCd = setInterval(() => {
if (stopped) return;
if (--remaining <= 0) finish();
else updateCd();
}, 1000);
}
function showFinisherUI() {
show('gameBox');
hide('gameCard');
hide('lockReleaseBox');
hide('tempOpeningBox');
renderLevelBar(6);
let finisher = {};
try { finisher = JSON.parse(_state.finisher); } catch (_) {}
document.getElementById('finisherLabel').textContent = finisher.kurzText || '';
document.getElementById('finisherText').textContent = finisher.text || '';
const timerEl = document.getElementById('finisherTimer');
const btnEl = document.getElementById('finisherBtn');
timerEl.classList.remove('active', 'urgent');
timerEl.textContent = '';
if (_state.finisherStartedAt) {
timerEl.classList.add('active');
startElapsedTimer(new Date(_state.finisherStartedAt));
btnEl.textContent = '✓ Erledigt';
btnEl.onclick = doEndFinisher;
} else {
btnEl.textContent = '▶ Starten';
btnEl.onclick = doStartFinisher;
}
show('finisherBox');
}
async function doStartFinisher() {
await fetch('/lock-game/start-finisher', { method: 'POST' });
const r = await fetch('/lock-game/state');
_state = await r.json();
const timerEl = document.getElementById('finisherTimer');
timerEl.classList.add('active');
startElapsedTimer(new Date(_state.finisherStartedAt));
const btnEl = document.getElementById('finisherBtn');
btnEl.textContent = '✓ Erledigt';
btnEl.onclick = doEndFinisher;
}
async function doEndFinisher() {
clearTimer();
await fetch('/lock-game/end-finisher', { method: 'POST' });
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
await fetch(url, { method: 'POST' });
goBack();
}
function startElapsedTimer(startDate) {
clearTimer();
const el = document.getElementById('finisherTimer');
function tick() {
const diff = Math.floor((Date.now() - startDate) / 1000);
const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s;
}
tick();
_timerInt = setInterval(tick, 1000);
}
function waitForReleaseOk(text) {
return new Promise(resolve => {
hide('gameCard');
document.getElementById('releaseText').textContent = text || '';
document.getElementById('btnReleaseOk').onclick = () => {
hide('lockReleaseBox');
resolve();
};
show('lockReleaseBox');
});
}
// ── Level-Bar ─────────────────────────────────────────────────────────────
function renderLevelBar(level) {
const isFinisher = level >= 6;
document.getElementById('levelImg').style.display = isFinisher ? 'none' : '';
document.getElementById('levelTrophy').style.display = isFinisher ? '' : 'none';
if (!isFinisher) {
document.getElementById('levelImg').src = `/img/lvl${Math.min(Math.max(level, 1), 5)}.png`;
}
show('levelDisplay');
}
// ── Timer ─────────────────────────────────────────────────────────────────
function playSound(src) {
try { new Audio(src).play().catch(() => {}); } catch (_) {}
}
function startTimer(endDate, el) {
el.classList.add('active');
clearTimer();
function tick() {
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
const m = String(Math.floor(diff / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0');
el.textContent = m + ':' + s;
el.classList.toggle('urgent', diff < 30);
if (diff === 0) {
clearTimer();
playSound('/audio/alarm.mp3');
_gameAction = 'active-done';
document.getElementById('gameBtn').textContent = '✓ Erledigt';
}
}
tick();
_timerInt = setInterval(tick, 1000);
}
function clearTimer() {
if (_timerInt) { clearInterval(_timerInt); _timerInt = null; }
}
// ── Hilfsfunktionen ───────────────────────────────────────────────────────
function show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
function hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
function esc(s) { return String(s).replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function showError(msg) {
hide('loadingHint');
const box = document.getElementById('errorBox');
box.textContent = msg || 'Ein Fehler ist aufgetreten.';
box.style.display = '';
}
// ── Confirm Modal ─────────────────────────────────────────────────────────
const _confirmModal = document.getElementById('confirmModal');
document.getElementById('confirmModalCancel').addEventListener('click', closeConfirmModal);
document.getElementById('confirmModalOk').addEventListener('click', closeConfirmModal);
_confirmModal.addEventListener('click', e => { if (e.target === _confirmModal) closeConfirmModal(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && _confirmModal.classList.contains('open')) closeConfirmModal(); });
function closeConfirmModal() { _confirmModal.classList.remove('open'); }
function openConfirmModal(text, onConfirm) {
document.getElementById('confirmModalText').textContent = text;
const okBtn = document.getElementById('confirmModalOk');
const newOk = okBtn.cloneNode(true);
okBtn.parentNode.replaceChild(newOk, okBtn);
newOk.addEventListener('click', () => { closeConfirmModal(); onConfirm(); });
_confirmModal.classList.add('open');
}
boot();
</script>
</body>
</html>