Weiter am Ingame Chastity Game gearbeitet
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled

This commit is contained in:
2026-04-26 22:53:05 +02:00
parent 4f2048bdc8
commit 0aa794600e
44 changed files with 2156 additions and 1419 deletions

View File

@@ -0,0 +1,433 @@
<!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,'&lt;').replace(/>/g,'&gt;'); }
function showError(msg) {
hide('loadingHint');
const box = document.getElementById('errorBox');
box.textContent = msg || 'Ein Fehler ist aufgetreten.';
box.style.display = '';
}
boot();
</script>
</body>
</html>