Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
434 lines
17 KiB
HTML
434 lines
17 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;
|
||
}
|
||
.game-text {
|
||
font-size: 1rem;
|
||
line-height: 1.6;
|
||
color: var(--color-text);
|
||
white-space: pre-wrap;
|
||
}
|
||
.game-timer {
|
||
font-size: 2.2rem;
|
||
font-weight: 700;
|
||
color: var(--color-primary);
|
||
text-align: center;
|
||
margin: 0.75rem 0;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
.game-timer.urgent { color: #e74c3c; }
|
||
|
||
.game-level-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.game-level-dot {
|
||
width: 10px; height: 10px;
|
||
border-radius: 50%;
|
||
background: var(--color-secondary);
|
||
transition: background 0.3s;
|
||
}
|
||
.game-level-dot.active { background: var(--color-primary); }
|
||
|
||
.lock-messages {
|
||
background: rgba(233,69,96,0.1);
|
||
border: 1px solid rgba(233,69,96,0.3);
|
||
border-radius: 10px;
|
||
padding: 1rem 1.1rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.lock-messages p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.5; }
|
||
.lock-messages p:last-child { margin: 0; }
|
||
|
||
.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); }
|
||
|
||
.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;
|
||
}
|
||
|
||
#finisherBox {
|
||
background: linear-gradient(135deg, rgba(233,69,96,0.15), rgba(155,89,182,0.12));
|
||
border: 1px solid rgba(233,69,96,0.4);
|
||
border-radius: 14px;
|
||
padding: 1.5rem;
|
||
text-align: center;
|
||
}
|
||
#finisherBox .trophy { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="app" class="container" style="max-width:480px;margin:0 auto;padding:1rem;">
|
||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem;">
|
||
<button id="btnBack" onclick="goBack()"
|
||
style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.4rem;">‹</button>
|
||
<h1 style="margin:0;font-size:1.15rem;font-weight:700;">🎯 Task Game</h1>
|
||
</div>
|
||
|
||
<!-- Level-Anzeige -->
|
||
<div class="game-level-bar" id="levelBar" style="display:none;">
|
||
<span style="font-size:0.78rem;color:var(--color-muted);font-weight:600;">Level</span>
|
||
<div id="levelDots" style="display:flex;gap:0.35rem;"></div>
|
||
<span id="levelText" style="font-size:0.78rem;color:var(--color-muted);margin-left:auto;"></span>
|
||
</div>
|
||
|
||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||
<div id="lockMessages" class="lock-messages" style="display:none;"></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;">
|
||
|
||
<!-- 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>
|
||
</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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Finisher -->
|
||
<div id="finisherBox" style="display:none;">
|
||
<div class="trophy">🏆</div>
|
||
<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-secondary" onclick="goBack()" style="margin-top:1.25rem;">Zurück zum Lock</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>
|
||
|
||
<script>
|
||
const params = new URLSearchParams(location.search);
|
||
const lockId = params.get('lockId');
|
||
let _state = null;
|
||
let _timerInt = null;
|
||
let _pendingIsLock = false;
|
||
let _pendingHasDuration = false;
|
||
|
||
function goBack() {
|
||
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
||
else history.back();
|
||
}
|
||
|
||
// ── Init ──────────────────────────────────────────────────────────────────
|
||
|
||
async function boot() {
|
||
try {
|
||
const r = await fetch('/lock-game/state');
|
||
if (r.status === 404) {
|
||
await loadGroups();
|
||
return;
|
||
}
|
||
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 loadGroups() {
|
||
const r = await fetch('/lock-game/groups');
|
||
const groups = r.ok ? await r.json() : [];
|
||
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');
|
||
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);
|
||
}
|
||
}
|
||
|
||
// ── Game Loop ─────────────────────────────────────────────────────────────
|
||
|
||
async function runGameLoop() {
|
||
hide('queueBox');
|
||
hide('activeBox');
|
||
hide('finisherBox');
|
||
clearTimer();
|
||
|
||
if (_state.level >= 6) {
|
||
await showFinisher();
|
||
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');
|
||
_state = await r.json();
|
||
}
|
||
|
||
showQueue();
|
||
}
|
||
|
||
function showQueue() {
|
||
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;
|
||
} 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;
|
||
}
|
||
show('gameBox');
|
||
show('queueBox');
|
||
}
|
||
|
||
function showActiveTask(text, endIso) {
|
||
document.getElementById('activeText').textContent = text;
|
||
show('gameBox');
|
||
show('activeBox');
|
||
|
||
if (endIso) {
|
||
const end = new Date(endIso);
|
||
startTimer(end, document.getElementById('activeTimer'));
|
||
}
|
||
}
|
||
|
||
async function handleOk() {
|
||
// Locks prüfen (nach jeder Aktion)
|
||
await checkAndShowLocks();
|
||
|
||
if (_pendingIsLock || _pendingHasDuration) {
|
||
// Task/Sperre aktivieren und Timer starten
|
||
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||
if (!r.ok) { showError('Fehler'); 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();
|
||
}
|
||
|
||
async function handleErledigt() {
|
||
await checkAndShowLocks();
|
||
|
||
if (_state.level >= 6) {
|
||
await showFinisher();
|
||
return;
|
||
}
|
||
|
||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||
const r = await fetch('/lock-game/state');
|
||
_state = await r.json();
|
||
await runGameLoop();
|
||
}
|
||
|
||
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 box = document.getElementById('lockMessages');
|
||
box.innerHTML = texts.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
|
||
show('lockMessages');
|
||
await new Promise(res => setTimeout(res, 2000));
|
||
}
|
||
}
|
||
|
||
async function showFinisher() {
|
||
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');
|
||
}
|
||
|
||
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||
|
||
function renderLevelBar(level) {
|
||
const bar = document.getElementById('levelBar');
|
||
const dots = document.getElementById('levelDots');
|
||
dots.innerHTML = [1,2,3,4,5].map(i =>
|
||
`<div class="game-level-dot${i <= level ? ' active' : ''}"></div>`
|
||
).join('');
|
||
document.getElementById('levelText').textContent = 'Level ' + Math.min(level, 5);
|
||
show('levelBar');
|
||
}
|
||
|
||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||
|
||
function startTimer(endDate, el) {
|
||
el.style.display = '';
|
||
clearTimer();
|
||
_timerInt = setInterval(() => {
|
||
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();
|
||
}, 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 = '';
|
||
}
|
||
|
||
boot();
|
||
</script>
|
||
</body>
|
||
</html>
|