Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
911 lines
38 KiB
HTML
911 lines
38 KiB
HTML
<!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,'<').replace(/>/g,'>'); }
|
||
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>
|