Weitere Fehler im Chastity ingame game behoben
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
This commit is contained in:
3
bin/main/db/migration/V7__fix_aktive_sperre_fuer_fk.sql
Normal file
3
bin/main/db/migration/V7__fix_aktive_sperre_fuer_fk.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE aktive_sperre_fuer DROP FOREIGN KEY FK36uaxlluxoow36iy1pqd4ig8b;
|
||||||
|
ALTER TABLE aktive_sperre_fuer ADD CONSTRAINT fk_aktive_sperre_fuer_lock_game_lock
|
||||||
|
FOREIGN KEY (aktive_sperre_id) REFERENCES lock_game_lock (lock_game_lock_id);
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -902,6 +902,10 @@
|
|||||||
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
|
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('pageshow', (e) => {
|
||||||
|
if (e.persisted && lockId) loadLock();
|
||||||
|
});
|
||||||
|
|
||||||
async function loadLock() {
|
async function loadLock() {
|
||||||
const res = await fetch('/keyholder/cardlock/' + lockId);
|
const res = await fetch('/keyholder/cardlock/' + lockId);
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
@@ -1322,7 +1326,13 @@
|
|||||||
const cdEl = document.getElementById('gameCardCountdown');
|
const cdEl = document.getElementById('gameCardCountdown');
|
||||||
function tick() {
|
function tick() {
|
||||||
const diff = deadline - Date.now();
|
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);
|
cdEl.textContent = fmtCountdown(diff);
|
||||||
}
|
}
|
||||||
tick();
|
tick();
|
||||||
@@ -1335,7 +1345,8 @@
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const gameSetId = data.gameSetId;
|
const gameSetId = data.gameSetId;
|
||||||
const url = '/games/chastity/taskgame.html?lockId=' + lockId
|
const url = '/games/chastity/taskgame.html?lockId=' + lockId
|
||||||
+ (gameSetId ? '&gameSetId=' + gameSetId : '');
|
+ (gameSetId ? '&gameSetId=' + gameSetId : '')
|
||||||
|
+ '&fresh=1';
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -405,13 +405,9 @@
|
|||||||
<div id="subGameSet" style="display:none;">
|
<div id="subGameSet" style="display:none;">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
||||||
<div style="position:relative;">
|
<select id="fGameSetId" onchange="markDirty()">
|
||||||
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
<option value="">Kein Aufgaben-Set</option>
|
||||||
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
</select>
|
||||||
<div id="gameSetDropdown" class="gs-dropdown"></div>
|
|
||||||
</div>
|
|
||||||
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
|
|
||||||
<input type="hidden" id="fGameSetId">
|
|
||||||
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom:0;">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
@@ -757,8 +753,30 @@
|
|||||||
{ label: 'Lang' },
|
{ label: 'Lang' },
|
||||||
{ label: 'Sehr lang' },
|
{ label: 'Sehr lang' },
|
||||||
];
|
];
|
||||||
let _gameSetSearchTimer = null;
|
let _gameGroups = [];
|
||||||
let _gameSetResults = [];
|
|
||||||
|
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() {
|
function checkGameCardSection() {
|
||||||
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
||||||
@@ -791,72 +809,6 @@
|
|||||||
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label;
|
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 ──
|
// ── Karten-Info ──
|
||||||
function openCardInfo(cardId) {
|
function openCardInfo(cardId) {
|
||||||
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
||||||
@@ -1300,17 +1252,12 @@
|
|||||||
|
|
||||||
// Task-Karte und Spiel-Karte
|
// Task-Karte und Spiel-Karte
|
||||||
checkTaskCardSection();
|
checkTaskCardSection();
|
||||||
clearGameSet();
|
|
||||||
checkGameCardSection();
|
checkGameCardSection();
|
||||||
const gsi = template?.gameSpieldauerIdx ?? 2;
|
const gsi = template?.gameSpieldauerIdx ?? 2;
|
||||||
document.getElementById('sldGameSpieldauer').value = gsi;
|
document.getElementById('sldGameSpieldauer').value = gsi;
|
||||||
updateGameSpieldauer(gsi);
|
updateGameSpieldauer(gsi);
|
||||||
if (template?.gameSetId) {
|
populateGameSetSelect();
|
||||||
fetch(`/gruppe/${template.gameSetId}`)
|
document.getElementById('fGameSetId').value = template?.gameSetId || '';
|
||||||
.then(r => r.ok ? r.json() : null)
|
|
||||||
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'TIMELOCK') {
|
if (type === 'TIMELOCK') {
|
||||||
@@ -1664,6 +1611,7 @@
|
|||||||
document.getElementById('templateList').innerHTML = '';
|
document.getElementById('templateList').innerHTML = '';
|
||||||
document.getElementById('listEmpty').style.display = 'none';
|
document.getElementById('listEmpty').style.display = 'none';
|
||||||
await loadTaskSets();
|
await loadTaskSets();
|
||||||
|
await loadGameGroups();
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
loadSubscribedTemplates();
|
loadSubscribedTemplates();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,22 +22,36 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
height: 1.2rem;
|
||||||
}
|
}
|
||||||
.game-text {
|
.game-text {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
height: 14rem;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.game-timer {
|
.game-timer {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0.75rem 0;
|
|
||||||
letter-spacing: 0.04em;
|
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-timer.urgent { color: #e74c3c; }
|
||||||
|
.game-btn-row {
|
||||||
|
margin-top: 1rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.level-display {
|
.level-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -82,6 +96,20 @@
|
|||||||
}
|
}
|
||||||
.group-item input[type=radio] { accent-color: var(--color-primary); }
|
.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 {
|
.btn-primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
@@ -130,6 +158,21 @@
|
|||||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||||
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
|
<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 -->
|
<!-- Initialisierung: Gruppe wählen -->
|
||||||
<div id="initBox" class="init-box" style="display:none;">
|
<div id="initBox" class="init-box" style="display:none;">
|
||||||
<h2>Spiel-Set auswählen</h2>
|
<h2>Spiel-Set auswählen</h2>
|
||||||
@@ -143,22 +186,22 @@
|
|||||||
<!-- Laufendes Spiel -->
|
<!-- Laufendes Spiel -->
|
||||||
<div id="gameBox" style="display:none;">
|
<div id="gameBox" style="display:none;">
|
||||||
|
|
||||||
<!-- Task oder Lock in Queue -->
|
<!-- Einheitliche Spielkarte -->
|
||||||
<div id="queueBox" class="game-card" style="display:none;">
|
<div id="gameCard" class="game-card" style="display:none;">
|
||||||
<div class="game-label" id="queueLabel"></div>
|
<div class="game-label" id="gameLabel"></div>
|
||||||
<div class="game-text" id="queueText"></div>
|
<div class="game-text" id="gameText"></div>
|
||||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
<div class="game-timer" id="gameTimer"></div>
|
||||||
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button>
|
<div class="game-btn-row">
|
||||||
|
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Aktive Aufgabe (läuft) -->
|
<!-- Release-Text (Sperren) -->
|
||||||
<div id="activeBox" class="game-card" style="display:none;">
|
<div id="lockReleaseBox" class="game-card" style="display:none;">
|
||||||
<div class="game-label">Aktive Aufgabe</div>
|
<div class="game-label">🔓 Sperre aufgehoben</div>
|
||||||
<div class="game-text" id="activeText"></div>
|
<div class="game-text" id="releaseText"></div>
|
||||||
<div class="game-timer" id="activeTimer" style="display:none;"></div>
|
<div style="margin-top:1.1rem;">
|
||||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
<button class="btn-primary" id="btnReleaseOk">OK</button>
|
||||||
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,7 +211,12 @@
|
|||||||
<h2>Level 6 erreicht!</h2>
|
<h2>Level 6 erreicht!</h2>
|
||||||
<div class="game-label" id="finisherTitle"></div>
|
<div class="game-label" id="finisherTitle"></div>
|
||||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -187,17 +235,18 @@
|
|||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const lockId = params.get('lockId');
|
const lockId = params.get('lockId');
|
||||||
const autoGameSetId = params.get('gameSetId');
|
const autoGameSetId = params.get('gameSetId');
|
||||||
|
const freshStart = params.get('fresh') === '1';
|
||||||
|
let _resolvedGameSetId = autoGameSetId;
|
||||||
let _state = null;
|
let _state = null;
|
||||||
let _timerInt = null;
|
let _timerInt = null;
|
||||||
let _pendingIsLock = false;
|
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
|
||||||
let _pendingHasDuration = false;
|
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
||||||
else history.back();
|
else history.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeGame() {
|
async function debugExit() {
|
||||||
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
||||||
await fetch(url, { method: 'POST' });
|
await fetch(url, { method: 'POST' });
|
||||||
goBack();
|
goBack();
|
||||||
@@ -207,40 +256,134 @@
|
|||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
try {
|
try {
|
||||||
|
if (!freshStart) {
|
||||||
const r = await fetch('/lock-game/state');
|
const r = await fetch('/lock-game/state');
|
||||||
if (r.status === 404) {
|
if (r.ok) {
|
||||||
if (autoGameSetId) {
|
|
||||||
await autoStartGame(autoGameSetId);
|
|
||||||
} else {
|
|
||||||
await loadGroups();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
|
||||||
_state = await r.json();
|
_state = await r.json();
|
||||||
hide('loadingHint');
|
hide('loadingHint');
|
||||||
await runGameLoop();
|
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) {
|
} catch (e) {
|
||||||
showError(e.message);
|
showError(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function autoStartGame(gameSetId) {
|
// ── Toy-Auswahl ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function loadAndShowToys(gameSetId) {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + gameSetId, { method: 'POST' });
|
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 });
|
||||||
|
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');
|
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||||
|
|
||||||
const stateR = await fetch('/lock-game/state');
|
const stateR = await fetch('/lock-game/state');
|
||||||
_state = await stateR.json();
|
_state = await stateR.json();
|
||||||
hide('loadingHint');
|
hide('loadingHint');
|
||||||
await runGameLoop();
|
await runGameLoop();
|
||||||
} catch (e) {
|
|
||||||
showError(e.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function loadGroups() {
|
||||||
const r = await fetch('/lock-game/groups');
|
const r = await fetch('/lock-game/groups');
|
||||||
const groups = r.ok ? await r.json() : [];
|
const groups = r.ok ? await r.json() : [];
|
||||||
|
|
||||||
|
if (groups.length === 1) {
|
||||||
|
_resolvedGameSetId = groups[0].gruppenId;
|
||||||
|
await loadAndShowToys(groups[0].gruppenId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
hide('loadingHint');
|
hide('loadingHint');
|
||||||
const list = document.getElementById('groupList');
|
const list = document.getElementById('groupList');
|
||||||
if (groups.length === 0) {
|
if (groups.length === 0) {
|
||||||
@@ -270,40 +413,39 @@
|
|||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
hide('initBox');
|
hide('initBox');
|
||||||
show('loadingHint');
|
show('loadingHint');
|
||||||
try {
|
_resolvedGameSetId = sel.value;
|
||||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' });
|
await loadAndShowToys(sel.value);
|
||||||
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 ─────────────────────────────────────────────────────────────
|
// ── 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() {
|
async function runGameLoop() {
|
||||||
hide('queueBox');
|
hide('gameCard');
|
||||||
hide('activeBox');
|
|
||||||
hide('finisherBox');
|
hide('finisherBox');
|
||||||
clearTimer();
|
clearTimer();
|
||||||
|
|
||||||
if (_state.level >= 6) {
|
if (_state.level >= 6) {
|
||||||
await showFinisher();
|
await showFinisherFlow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLevelBar(_state.level);
|
renderLevelBar(_state.level);
|
||||||
|
|
||||||
// Aktive Aufgabe läuft noch
|
|
||||||
if (_state.activeTask) {
|
if (_state.activeTask) {
|
||||||
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue leer → nächsten Task holen
|
|
||||||
if (!_state.taskInQueue && !_state.lockInQueue) {
|
if (!_state.taskInQueue && !_state.lockInQueue) {
|
||||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||||
const r = await fetch('/lock-game/state');
|
const r = await fetch('/lock-game/state');
|
||||||
@@ -317,90 +459,155 @@
|
|||||||
if (_state.lockInQueue) {
|
if (_state.lockInQueue) {
|
||||||
let sperre;
|
let sperre;
|
||||||
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
||||||
_pendingIsLock = true;
|
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
|
||||||
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
|
|
||||||
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
|
|
||||||
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
|
|
||||||
} else if (_state.taskInQueue) {
|
} else if (_state.taskInQueue) {
|
||||||
let aufgabe;
|
let aufgabe;
|
||||||
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
||||||
_pendingIsLock = false;
|
const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||||
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
|
||||||
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe';
|
hasDuration ? '▶ Starten' : '✓ Erledigt');
|
||||||
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
|
|
||||||
}
|
}
|
||||||
show('gameBox');
|
show('gameBox');
|
||||||
show('queueBox');
|
show('gameCard');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showActiveTask(text, endIso) {
|
function showActiveTask(text, endIso) {
|
||||||
document.getElementById('activeText').textContent = text;
|
|
||||||
show('gameBox');
|
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) {
|
if (endIso) {
|
||||||
const end = new Date(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);
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
_gameAction = 'active-done';
|
||||||
|
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||||
|
}
|
||||||
|
|
||||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
function handleGameBtn() {
|
||||||
const r = await fetch('/lock-game/state');
|
switch (_gameAction) {
|
||||||
_state = await r.json();
|
case 'queue-start': doQueueStart(); break;
|
||||||
|
case 'queue-done': doQueueDone(); break;
|
||||||
|
case 'active-running': doCancelCountdown(); break;
|
||||||
|
case 'active-done': doErledigt(); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doQueueStart() {
|
||||||
|
try {
|
||||||
|
await checkAndShowLocks();
|
||||||
|
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||||||
|
if (!r.ok) { showError('Fehler beim Starten'); return; }
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
await runGameLoop();
|
await runGameLoop();
|
||||||
|
} catch (e) { showError(e.message || 'Fehler (Starten)'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
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)'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function doCancelCountdown() {
|
||||||
|
clearTimer();
|
||||||
|
_gameAction = 'active-done';
|
||||||
|
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
async function checkAndShowLocks() {
|
||||||
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
const texts = await r.json();
|
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');
|
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');
|
show('lockMessages');
|
||||||
await new Promise(res => setTimeout(res, 2000));
|
await new Promise(res => setTimeout(res, 2000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showFinisher() {
|
async function showFinisherFlow() {
|
||||||
show('gameBox');
|
show('gameBox');
|
||||||
const r = await fetch('/lock-game/finisher');
|
hide('gameCard');
|
||||||
if (!r.ok) {
|
hide('lockReleaseBox');
|
||||||
document.getElementById('finisherTitle').textContent = '';
|
hide('finisherBox');
|
||||||
document.getElementById('finisherText').textContent = 'Glückwunsch – du hast Level 6 erreicht!';
|
|
||||||
} else {
|
// 1. Release-Texte sequenziell anzeigen
|
||||||
const f = await r.json();
|
try {
|
||||||
document.getElementById('finisherTitle').textContent = f.kurzText || '';
|
const r = await fetch('/lock-game/release-locks');
|
||||||
document.getElementById('finisherText').textContent = f.text || '';
|
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');
|
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 ─────────────────────────────────────────────────────────────
|
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||||||
@@ -413,8 +620,12 @@
|
|||||||
|
|
||||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function playSound(src) {
|
||||||
|
try { new Audio(src).play().catch(() => {}); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
function startTimer(endDate, el) {
|
function startTimer(endDate, el) {
|
||||||
el.style.display = '';
|
el.classList.add('active');
|
||||||
clearTimer();
|
clearTimer();
|
||||||
_timerInt = setInterval(() => {
|
_timerInt = setInterval(() => {
|
||||||
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||||
@@ -422,7 +633,12 @@
|
|||||||
const s = String(diff % 60).padStart(2, '0');
|
const s = String(diff % 60).padStart(2, '0');
|
||||||
el.textContent = m + ':' + s;
|
el.textContent = m + ':' + s;
|
||||||
el.classList.toggle('urgent', diff < 30);
|
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);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,27 @@
|
|||||||
src/main/java/de/oaa/xxx/config/CookieFactory.java
|
package de.oaa.xxx.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseCookie;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CookieFactory {
|
||||||
|
|
||||||
|
private final boolean secure;
|
||||||
|
|
||||||
|
public CookieFactory(@Value("${app.cookie.secure:true}") boolean secure) {
|
||||||
|
this.secure = secure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseCookie jwtCookie(String token, Duration maxAge) {
|
||||||
|
return ResponseCookie.from("jwt", token)
|
||||||
|
.httpOnly(true)
|
||||||
|
.secure(secure)
|
||||||
|
.sameSite("Strict")
|
||||||
|
.path("/")
|
||||||
|
.maxAge(maxAge)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -644,10 +644,11 @@ public class CardLockController {
|
|||||||
meName + " hat die Spiel-Karte nicht innerhalb einer Stunde gestartet. Das Lock wurde für 4 Stunden eingefroren.",
|
meName + " hat die Spiel-Karte nicht innerhalb einer Stunde gestartet. Das Lock wurde für 4 Stunden eingefroren.",
|
||||||
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
"/games/chastity/keyholder.html", de.oaa.xxx.social.entity.MessageCause.GAME_STATE);
|
||||||
}
|
}
|
||||||
|
cardLockServiceFactory.create(l).penaltyLockGame();
|
||||||
l.setGameCardParkedAt(null);
|
l.setGameCardParkedAt(null);
|
||||||
cardlockRepository.save(l);
|
cardlockRepository.save(l);
|
||||||
result.put("gameCardParkedAt", null);
|
result.put("gameCardParkedAt", null);
|
||||||
result.put("gameSetId", null);
|
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
|
||||||
} else {
|
} else {
|
||||||
result.put("gameCardParkedAt", l.getGameCardParkedAt().toString());
|
result.put("gameCardParkedAt", l.getGameCardParkedAt().toString());
|
||||||
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
|
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
|
||||||
@@ -655,7 +656,7 @@ public class CardLockController {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result.put("gameCardParkedAt", null);
|
result.put("gameCardParkedAt", null);
|
||||||
result.put("gameSetId", null);
|
result.put("gameSetId", l.getGameSetId() != null ? l.getGameSetId().toString() : null);
|
||||||
}
|
}
|
||||||
result.put("gameActive", l.isGameActive());
|
result.put("gameActive", l.isGameActive());
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
CardDTO card = null;
|
CardDTO card = null;
|
||||||
if (lock.isKeyholderRequestedUnlock()
|
if (lock.isKeyholderRequestedUnlock()
|
||||||
|| (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
|
|| (lock.getLatestOpeningtime() != null && lock.getLatestOpeningtime().isAfter(LocalDateTime.now()))) {
|
||||||
|
LOGGER.info("[CardLock {}] Karte gezogen: GREEN (erzwungen – Keyholder oder Öffnungszeitlimit)", lock.getLockee());
|
||||||
card = getGreenCard();
|
card = getGreenCard();
|
||||||
} else if (lock.isAccumulatePicks()) {
|
} else if (lock.isAccumulatePicks()) {
|
||||||
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
|
if (lock.getNextCardIn().isBefore(LocalDateTime.now())) {
|
||||||
@@ -139,9 +140,10 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
var cards = lock.getAvailableCards();
|
var cards = lock.getAvailableCards();
|
||||||
if (!cards.isEmpty()) {
|
if (!cards.isEmpty()) {
|
||||||
var card = cards.get(new Random().nextInt(cards.size()));
|
var card = cards.get(new Random().nextInt(cards.size()));
|
||||||
LOGGER.debug("Card drafted: {}", card);
|
|
||||||
lock.getAvailableCards().remove(card);
|
lock.getAvailableCards().remove(card);
|
||||||
return card.get().processCard(this);
|
CardDTO result = card.get().processCard(this);
|
||||||
|
LOGGER.info("[CardLock {}] Karte gezogen: {} | Verbleibende Karten: {}", lock.getLockee(), result.card(), lock.getAvailableCards().size());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte");
|
LOGGER.error("Keine Karten mehr im Lock - generiere Notfall Grüne Karte");
|
||||||
return getGreenCard();
|
return getGreenCard();
|
||||||
@@ -155,20 +157,20 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
|
|
||||||
public String doubleUp() {
|
public String doubleUp() {
|
||||||
var cards = lock.getAvailableCards();
|
var cards = lock.getAvailableCards();
|
||||||
LOGGER.debug("Double up {} cards", cards.size());
|
int before = cards.size();
|
||||||
lock.getAvailableCards().addAll(cards);
|
lock.getAvailableCards().addAll(cards);
|
||||||
LOGGER.debug("Now {} cards", lock.getAvailableCards().size());
|
LOGGER.info("[CardLock {}] DOUBLE_UP: Karten verdoppelt {} -> {}", lock.getLockee(), before, lock.getAvailableCards().size());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String reset() {
|
public String reset() {
|
||||||
LOGGER.debug("Reset to initial cards");
|
|
||||||
lock.setAvailableCards(lock.getInitialCards());
|
lock.setAvailableCards(lock.getInitialCards());
|
||||||
|
LOGGER.info("[CardLock {}] RESET: zurück auf {} initiale Karten", lock.getLockee(), lock.getInitialCards().size());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String green() {
|
public String green() {
|
||||||
LOGGER.debug("Green Card drafted");
|
LOGGER.info("[CardLock {}] GREEN: Unlock-Code bereitgestellt (Code vorhanden: {})", lock.getLockee(), lock.getUnlockCode() != null);
|
||||||
return lock.getUnlockCode();
|
return lock.getUnlockCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +184,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
|
LocalDateTime frozenTill = LocalDateTime.now().plus((long) multiplier, ChronoUnit.MINUTES);
|
||||||
lock.setFrozenUntil(frozenTill);
|
lock.setFrozenUntil(frozenTill);
|
||||||
lock.setNextCardIn(frozenTill);
|
lock.setNextCardIn(frozenTill);
|
||||||
LOGGER.debug("Frozen until {}", lock.getFrozenUntil());
|
LOGGER.info("[CardLock {}] FREEZE: eingefroren für {} Minuten (bis {})", lock.getLockee(), (long) multiplier, frozenTill);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +202,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingTaskMode = lock.getTaskMode().name();
|
pendingTaskMode = lock.getTaskMode().name();
|
||||||
|
LOGGER.info("[CardLock {}] TASK: Modus={}, testLock={}", lock.getLockee(), lock.getTaskMode(), lock.isTestLock());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,21 +212,23 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String redCard() {
|
public String redCard() {
|
||||||
|
LOGGER.info("[CardLock {}] RED: Rote Karte gezogen", lock.getLockee());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String yellowCard() {
|
public String yellowCard() {
|
||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
|
int before = lock.getAvailableCards().size();
|
||||||
if (random.nextBoolean()) {
|
if (random.nextBoolean()) {
|
||||||
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
||||||
LOGGER.debug("Adding Red card");
|
|
||||||
lock.getAvailableCards().add(CardEnum.RED);
|
lock.getAvailableCards().add(CardEnum.RED);
|
||||||
}
|
}
|
||||||
|
LOGGER.info("[CardLock {}] YELLOW: Rote Karten hinzugefügt | Kartenanzahl {} -> {}", lock.getLockee(), before, lock.getAvailableCards().size());
|
||||||
} else {
|
} else {
|
||||||
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
for (int i = 0; i < random.nextInt(1, 3); i++) {
|
||||||
LOGGER.debug("Removing Red card if possible");
|
|
||||||
lock.getAvailableCards().remove(CardEnum.RED);
|
lock.getAvailableCards().remove(CardEnum.RED);
|
||||||
}
|
}
|
||||||
|
LOGGER.info("[CardLock {}] YELLOW: Rote Karten entfernt | Kartenanzahl {} -> {}", lock.getLockee(), before, lock.getAvailableCards().size());
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -250,6 +255,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
// ── Cum cards ─────────────────────────────────────────────────────────────
|
// ── Cum cards ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public String cum(boolean tempUnlock) {
|
public String cum(boolean tempUnlock) {
|
||||||
|
LOGGER.info("[CardLock {}] CUM: tempUnlock={}", lock.getLockee(), tempUnlock);
|
||||||
if (tempUnlock) {
|
if (tempUnlock) {
|
||||||
startTempOpening(TempOpeningReason.CARD, 0);
|
startTempOpening(TempOpeningReason.CARD, 0);
|
||||||
}
|
}
|
||||||
@@ -278,10 +284,12 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String slowmo() {
|
public String slowmo() {
|
||||||
|
LOGGER.info("[CardLock {}] SLOWMO_CARD: Zeitlupen-Effekt ausgelöst", lock.getLockee());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String speedup() {
|
public String speedup() {
|
||||||
|
LOGGER.info("[CardLock {}] SPEEDUP_CARD: Beschleunigungs-Effekt ausgelöst", lock.getLockee());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +298,7 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
lock.setGameCardParkedAt(LocalDateTime.now());
|
lock.setGameCardParkedAt(LocalDateTime.now());
|
||||||
lock.setFrozenUntil(deadline);
|
lock.setFrozenUntil(deadline);
|
||||||
lock.setNextCardIn(deadline);
|
lock.setNextCardIn(deadline);
|
||||||
lock.setOpenPicks(0);
|
LOGGER.info("[CardLock {}] GAME_CARD: Spiel-Lock aktiv bis {}", lock.getLockee(), deadline);
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,4 +321,15 @@ public class CardLockService extends BaseLockService implements LockControlCallb
|
|||||||
}
|
}
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleLockGameFinished(int timeInMinutes) {
|
||||||
|
int freezeTime = (int) (timeInMinutes * new Random().nextDouble(1.0, 4.0));
|
||||||
|
freeze(freezeTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void penaltyLockGame() {
|
||||||
|
handleLockGameFinished(60);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ public abstract class BaseLockService {
|
|||||||
/** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */
|
/** TimeLock: lockControl.lock() nach dem Schließen der Hygiene-Öffnung aufrufen. */
|
||||||
protected void afterHygieneClosing() {}
|
protected void afterHygieneClosing() {}
|
||||||
|
|
||||||
|
protected abstract void handleLockGameFinished(int timeInMinutes);
|
||||||
|
|
||||||
|
public abstract void penaltyLockGame();
|
||||||
|
|
||||||
public BaseLockService(
|
public BaseLockService(
|
||||||
CommunityVerificationVoteRepository communityVerificationVoteRepository,
|
CommunityVerificationVoteRepository communityVerificationVoteRepository,
|
||||||
CommunityVerificationRepository communityVerificationRepository,
|
CommunityVerificationRepository communityVerificationRepository,
|
||||||
@@ -141,11 +145,21 @@ public abstract class BaseLockService {
|
|||||||
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
|
systemMessageService.send(senderId, receiverId, text, targetUrl, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Lock-Game Abschluss ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void lockGameFinished(int timeInMinutes) {
|
||||||
|
LOGGER.info("[Lock {}] lockGameFinished nach {} Minuten", getLock().getLockee(), timeInMinutes);
|
||||||
|
handleLockGameFinished(timeInMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── Aufgaben ──────────────────────────────────────────────────────────────
|
// ── Aufgaben ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public void task(Task task) {
|
public void task(Task task) {
|
||||||
BaseLockEntity lock = getLock();
|
BaseLockEntity lock = getLock();
|
||||||
LOGGER.debug("Apply task {}", task);
|
LOGGER.info("[Lock {}] Aufgabe zugewiesen: title='{}', minutes={}, description='{}'",
|
||||||
|
lock.getLockee(), task.getTitle(), task.getMinutes(), task.getDescription());
|
||||||
lock.setCurrentTask(task.getTitle());
|
lock.setCurrentTask(task.getTitle());
|
||||||
lock.setCurrentTaskDescription(task.getDescription());
|
lock.setCurrentTaskDescription(task.getDescription());
|
||||||
if (task.getMinutes() != null && task.getMinutes() > 0) {
|
if (task.getMinutes() != null && task.getMinutes() > 0) {
|
||||||
@@ -163,10 +177,14 @@ public abstract class BaseLockService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void applyRandomTask() {
|
protected void applyRandomTask() {
|
||||||
LOGGER.debug("Apply random task");
|
|
||||||
var tasks = getLock().getTasks();
|
var tasks = getLock().getTasks();
|
||||||
if (tasks != null && !tasks.isEmpty()) {
|
if (tasks != null && !tasks.isEmpty()) {
|
||||||
task(tasks.get(new Random().nextInt(tasks.size())));
|
int idx = new Random().nextInt(tasks.size());
|
||||||
|
LOGGER.info("[Lock {}] RANDOM-Aufgabe gezogen: Index {} von {} verfügbaren Aufgaben",
|
||||||
|
getLock().getLockee(), idx, tasks.size());
|
||||||
|
task(tasks.get(idx));
|
||||||
|
} else {
|
||||||
|
LOGGER.warn("[Lock {}] RANDOM-Aufgabe: keine Aufgaben im Lock vorhanden", getLock().getLockee());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import de.oaa.xxx.games.chastity.cardlock.CardLockServiceFactory;
|
||||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
|
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
|
||||||
import de.oaa.xxx.games.common.aufgaben.AvailableIn;
|
import de.oaa.xxx.games.common.aufgaben.AvailableIn;
|
||||||
@@ -24,6 +27,7 @@ import de.oaa.xxx.games.common.entity.AufgabeEntity;
|
|||||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||||
import de.oaa.xxx.games.common.entity.FinisherEntity;
|
import de.oaa.xxx.games.common.entity.FinisherEntity;
|
||||||
import de.oaa.xxx.games.common.entity.SperreEntity;
|
import de.oaa.xxx.games.common.entity.SperreEntity;
|
||||||
|
import de.oaa.xxx.games.common.entity.ToyEntity;
|
||||||
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
||||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||||
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
||||||
@@ -45,6 +49,7 @@ public class LockGameController {
|
|||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final CardlockRepository cardlockRepository;
|
private final CardlockRepository cardlockRepository;
|
||||||
|
private final CardLockServiceFactory cardLockServiceFactory;
|
||||||
|
|
||||||
public LockGameController(LockGameRepository lockGameRepository,
|
public LockGameController(LockGameRepository lockGameRepository,
|
||||||
LockGameLockRepository lockGameLockRepository,
|
LockGameLockRepository lockGameLockRepository,
|
||||||
@@ -54,7 +59,8 @@ public class LockGameController {
|
|||||||
FinisherRepository finisherRepository,
|
FinisherRepository finisherRepository,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
CardlockRepository cardlockRepository) {
|
CardlockRepository cardlockRepository,
|
||||||
|
CardLockServiceFactory cardLockServiceFactory) {
|
||||||
this.lockGameRepository = lockGameRepository;
|
this.lockGameRepository = lockGameRepository;
|
||||||
this.lockGameLockRepository = lockGameLockRepository;
|
this.lockGameLockRepository = lockGameLockRepository;
|
||||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||||
@@ -64,6 +70,7 @@ public class LockGameController {
|
|||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.cardlockRepository = cardlockRepository;
|
this.cardlockRepository = cardlockRepository;
|
||||||
|
this.cardLockServiceFactory = cardLockServiceFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */
|
/** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */
|
||||||
@@ -82,13 +89,55 @@ public class LockGameController {
|
|||||||
return ResponseEntity.ok(gruppen);
|
return ResponseEntity.ok(gruppen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt alle unique Toys zurück, die von Aufgaben, Sperren oder Finishern der Gruppe benötigt werden.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
@GetMapping("/toys")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> getToys(
|
||||||
|
@RequestParam UUID aufgabenGruppeId, Principal principal) {
|
||||||
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
|
var gruppeOpt = aufgabenGruppeRepository.findById(aufgabenGruppeId);
|
||||||
|
if (gruppeOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
AufgabenGruppeEntity gruppe = gruppeOpt.get();
|
||||||
|
if (gruppe.getUserId() != null && !gruppe.getUserId().equals(userId))
|
||||||
|
return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
Map<UUID, ToyEntity> toys = new java.util.LinkedHashMap<>();
|
||||||
|
aufgabeRepository.findByAufgabenGruppeIn(List.of(gruppe)).forEach(a -> {
|
||||||
|
if (a.getBenoetigteToys() != null)
|
||||||
|
a.getBenoetigteToys().forEach(t -> toys.putIfAbsent(t.getToyId(), t));
|
||||||
|
});
|
||||||
|
sperreRepository.findByAufgabenGruppeIn(List.of(gruppe)).forEach(s -> {
|
||||||
|
if (s.getBenoetigteToys() != null)
|
||||||
|
s.getBenoetigteToys().forEach(t -> toys.putIfAbsent(t.getToyId(), t));
|
||||||
|
});
|
||||||
|
finisherRepository.findByAufgabenGruppe(gruppe).forEach(f -> {
|
||||||
|
if (f.getBenoetigteToys() != null)
|
||||||
|
f.getBenoetigteToys().forEach(t -> toys.putIfAbsent(t.getToyId(), t));
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = toys.values().stream().map(t -> {
|
||||||
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("toyId", t.getToyId().toString());
|
||||||
|
m.put("name", t.getName());
|
||||||
|
m.put("beschreibung", t.getBeschreibung() != null ? t.getBeschreibung() : "");
|
||||||
|
m.put("bild", t.getBild() != null ? java.util.Base64.getEncoder().encodeToString(t.getBild()) : null);
|
||||||
|
return m;
|
||||||
|
}).toList();
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialisiert (oder startet neu) ein Lock-Game für den angemeldeten User.
|
* Initialisiert (oder startet neu) ein Lock-Game für den angemeldeten User.
|
||||||
* Lädt die angegebene Aufgabengruppe und baut daraus die AufgabenList.
|
* Lädt die angegebene Aufgabengruppe, filtert nach excludedToyIds und validiert.
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
@PostMapping("/init")
|
@PostMapping("/init")
|
||||||
public ResponseEntity<?> init(@RequestParam UUID aufgabenGruppeId, Principal principal) {
|
public ResponseEntity<?> init(
|
||||||
|
@RequestParam UUID aufgabenGruppeId,
|
||||||
|
@RequestParam(required = false) List<UUID> excludedToyIds,
|
||||||
|
Principal principal) {
|
||||||
UUID userId = userService.requireUser(principal).getUserId();
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
|
|
||||||
var gruppeOpt = aufgabenGruppeRepository.findById(aufgabenGruppeId);
|
var gruppeOpt = aufgabenGruppeRepository.findById(aufgabenGruppeId);
|
||||||
@@ -98,13 +147,39 @@ public class LockGameController {
|
|||||||
if (gruppe.getUserId() != null && !gruppe.getUserId().equals(userId))
|
if (gruppe.getUserId() != null && !gruppe.getUserId().equals(userId))
|
||||||
return ResponseEntity.status(403).build();
|
return ResponseEntity.status(403).build();
|
||||||
|
|
||||||
|
Set<UUID> excluded = excludedToyIds != null ? Set.copyOf(excludedToyIds) : Set.of();
|
||||||
|
|
||||||
var aufgaben = aufgabeRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream()
|
var aufgaben = aufgabeRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream()
|
||||||
|
.filter(a -> a.getBenoetigteToys() == null
|
||||||
|
|| a.getBenoetigteToys().stream().noneMatch(t -> excluded.contains(t.getToyId())))
|
||||||
.map(AufgabeEntity::toAufgabe).toList();
|
.map(AufgabeEntity::toAufgabe).toList();
|
||||||
var sperren = sperreRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream()
|
var sperren = sperreRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream()
|
||||||
|
.filter(s -> s.getBenoetigteToys() == null
|
||||||
|
|| s.getBenoetigteToys().stream().noneMatch(t -> excluded.contains(t.getToyId())))
|
||||||
.map(SperreEntity::toSperre).toList();
|
.map(SperreEntity::toSperre).toList();
|
||||||
var finisher = finisherRepository.findByAufgabenGruppe(gruppe).stream()
|
var finisher = finisherRepository.findByAufgabenGruppe(gruppe).stream()
|
||||||
|
.filter(f -> f.getBenoetigteToys() == null
|
||||||
|
|| f.getBenoetigteToys().stream().noneMatch(t -> excluded.contains(t.getToyId())))
|
||||||
.map(FinisherEntity::toFinisher).toList();
|
.map(FinisherEntity::toFinisher).toList();
|
||||||
|
|
||||||
|
// Validate: each level 1–5 needs at least one Aufgabe and one Sperre; at least one Finisher
|
||||||
|
for (int level = 1; level <= 5; level++) {
|
||||||
|
final int l = level;
|
||||||
|
long aufgabenCount = aufgaben.stream().filter(a -> l == a.getLevel()).count();
|
||||||
|
long sperrenCount = sperren.stream().filter(s -> l == s.getLevel()).count();
|
||||||
|
if (aufgabenCount == 0 || sperrenCount == 0) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(Map.of(
|
||||||
|
"error", "Level " + level + " hat nach der Toy-Filterung keine "
|
||||||
|
+ (aufgabenCount == 0 ? "Aufgaben" : "Zeitstrafen") + " mehr."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finisher.isEmpty()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(Map.of(
|
||||||
|
"error", "Kein Finisher nach der Toy-Filterung verfügbar."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
AufgabenList list = new AufgabenList();
|
AufgabenList list = new AufgabenList();
|
||||||
list.setAufgaben(aufgaben);
|
list.setAufgaben(aufgaben);
|
||||||
list.setSperren(sperren);
|
list.setSperren(sperren);
|
||||||
@@ -165,6 +240,19 @@ public class LockGameController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/abandon-task")
|
||||||
|
public ResponseEntity<?> abandonTask(Principal principal) {
|
||||||
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
|
var opt = lockGameRepository.findByUserId(userId);
|
||||||
|
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
|
try {
|
||||||
|
buildService(opt.get()).abandonActiveTask();
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/apply-task")
|
@PostMapping("/apply-task")
|
||||||
public ResponseEntity<Map<String, Object>> applyTask(Principal principal) {
|
public ResponseEntity<Map<String, Object>> applyTask(Principal principal) {
|
||||||
UUID userId = userService.requireUser(principal).getUserId();
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
@@ -190,6 +278,18 @@ public class LockGameController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/release-locks")
|
||||||
|
public ResponseEntity<List<String>> releaseLocks(Principal principal) {
|
||||||
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
|
var opt = lockGameRepository.findByUserId(userId);
|
||||||
|
if (opt.isEmpty()) return ResponseEntity.ok(List.of());
|
||||||
|
try {
|
||||||
|
return ResponseEntity.ok(buildService(opt.get()).releaseLocks());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.ok(List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/finisher")
|
@GetMapping("/finisher")
|
||||||
public ResponseEntity<?> getFinisher(Principal principal) {
|
public ResponseEntity<?> getFinisher(Principal principal) {
|
||||||
UUID userId = userService.requireUser(principal).getUserId();
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
@@ -206,9 +306,26 @@ public class LockGameController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@PostMapping("/penalty")
|
||||||
|
public ResponseEntity<?> penaltyGame(@RequestParam UUID lockId, Principal principal) {
|
||||||
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
|
cardlockRepository.findById(lockId).ifPresent(l -> {
|
||||||
|
if (l.getLockee().equals(userId)) {
|
||||||
|
l.setGameActive(false);
|
||||||
|
cardlockRepository.save(l);
|
||||||
|
cardLockServiceFactory.create(l).penaltyLockGame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@PostMapping("/complete")
|
@PostMapping("/complete")
|
||||||
public ResponseEntity<?> completeGame(@RequestParam(required = false) UUID lockId, Principal principal) {
|
public ResponseEntity<?> completeGame(
|
||||||
|
@RequestParam(required = false) UUID lockId,
|
||||||
|
@RequestParam(required = false, defaultValue = "0") int timeInMinutes,
|
||||||
|
Principal principal) {
|
||||||
UUID userId = userService.requireUser(principal).getUserId();
|
UUID userId = userService.requireUser(principal).getUserId();
|
||||||
var opt = lockGameRepository.findByUserId(userId);
|
var opt = lockGameRepository.findByUserId(userId);
|
||||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||||
@@ -219,6 +336,7 @@ public class LockGameController {
|
|||||||
if (lockId != null) {
|
if (lockId != null) {
|
||||||
cardlockRepository.findById(lockId).ifPresent(l -> {
|
cardlockRepository.findById(lockId).ifPresent(l -> {
|
||||||
if (l.getLockee().equals(userId)) {
|
if (l.getLockee().equals(userId)) {
|
||||||
|
cardLockServiceFactory.create(l).lockGameFinished(timeInMinutes);
|
||||||
l.setGameActive(false);
|
l.setGameActive(false);
|
||||||
l.setFrozenUntil(null);
|
l.setFrozenUntil(null);
|
||||||
l.setNextCardIn(LocalDateTime.now()
|
l.setNextCardIn(LocalDateTime.now()
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import java.util.List;
|
|||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -18,6 +21,8 @@ import de.oaa.xxx.games.common.aufgaben.Sperre;
|
|||||||
|
|
||||||
public class LockGameService {
|
public class LockGameService {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(LockGameService.class);
|
||||||
|
|
||||||
private LockGameEntity gamestate;
|
private LockGameEntity gamestate;
|
||||||
private LockGameRepository lockGameRepository;
|
private LockGameRepository lockGameRepository;
|
||||||
private LockGameLockRepository lockGameLockRepository;
|
private LockGameLockRepository lockGameLockRepository;
|
||||||
@@ -40,20 +45,36 @@ public class LockGameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void initNextTask() throws JsonProcessingException {
|
public void initNextTask() throws JsonProcessingException {
|
||||||
|
gamestate.setActiveTask(null);
|
||||||
|
gamestate.setActiveTaskEnd(null);
|
||||||
checkLevel();
|
checkLevel();
|
||||||
|
lockGameRepository.save(gamestate);
|
||||||
|
pickNextTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abandonActiveTask() throws JsonProcessingException {
|
||||||
|
gamestate.setActiveTask(null);
|
||||||
|
gamestate.setActiveTaskEnd(null);
|
||||||
|
lockGameRepository.save(gamestate);
|
||||||
|
pickNextTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pickNextTask() throws JsonProcessingException {
|
||||||
String result = null;
|
String result = null;
|
||||||
|
|
||||||
if (gamestate.getLevel() <= 5) {
|
if (gamestate.getLevel() <= 5) {
|
||||||
int nextInt = new Random().nextInt(1, 100);
|
int nextInt = new Random().nextInt(1, 100);
|
||||||
|
boolean isZeitstrafe = nextInt < 25;
|
||||||
|
LOGGER.info("[LockGame {}] Ziehung: Level={}, Würfel={}, Typ={}", gamestate.getUserId(),
|
||||||
|
gamestate.getLevel(), nextInt, isZeitstrafe ? "ZEITSTRAFE" : "AUFGABE");
|
||||||
|
|
||||||
if (nextInt < 25) {
|
if (isZeitstrafe) {
|
||||||
result = findZeitstrafe();
|
result = findZeitstrafe();
|
||||||
} else {
|
} else {
|
||||||
result = findAufgabe();
|
result = findAufgabe();
|
||||||
}
|
}
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = "Pause...";
|
LOGGER.info("[LockGame {}] Kein passender Eintrag gefunden → Pause (300s)", gamestate.getUserId());
|
||||||
gamestate.setTaskInQueue(result);
|
|
||||||
Aufgabe aufgabe = new Aufgabe();
|
Aufgabe aufgabe = new Aufgabe();
|
||||||
aufgabe.setText("Pause...");
|
aufgabe.setText("Pause...");
|
||||||
aufgabe.setSekundenVon(300);
|
aufgabe.setSekundenVon(300);
|
||||||
@@ -70,11 +91,15 @@ public class LockGameService {
|
|||||||
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
|
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
|
||||||
var level = gamestate.getLevel();
|
var level = gamestate.getLevel();
|
||||||
while (level > 0) {
|
while (level > 0) {
|
||||||
|
final var levelcp = level;
|
||||||
var sperren = aufgabenList.getSperren().stream()
|
var sperren = aufgabenList.getSperren().stream()
|
||||||
.filter(sperre -> sperre.getSperreFuer().stream().noneMatch(item -> list.contains(item))).toList();
|
.filter(sperre -> sperre.getLevel() == levelcp && sperre.getSperreFuer().stream().noneMatch(item -> list.contains(item))).toList();
|
||||||
if (!sperren.isEmpty()) {
|
if (!sperren.isEmpty()) {
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
Sperre sperre = sperren.get(new Random().nextInt(sperren.size()));
|
Sperre sperre = sperren.get(new Random().nextInt(sperren.size()));
|
||||||
|
LOGGER.info("[LockGame {}] ZEITSTRAFE gezogen: kurzText='{}', level={}, minuten={}-{}, sperreFuer={}",
|
||||||
|
gamestate.getUserId(), sperre.getKurzText(), sperre.getLevel(),
|
||||||
|
sperre.getMinutenVon(), sperre.getMinutenBis(), sperre.getSperreFuer());
|
||||||
gamestate.setLockInQueue(mapper.writeValueAsString(sperre));
|
gamestate.setLockInQueue(mapper.writeValueAsString(sperre));
|
||||||
return sperre.getText();
|
return sperre.getText();
|
||||||
}
|
}
|
||||||
@@ -87,12 +112,16 @@ public class LockGameService {
|
|||||||
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
|
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
|
||||||
var level = gamestate.getLevel();
|
var level = gamestate.getLevel();
|
||||||
while (level > 0) {
|
while (level > 0) {
|
||||||
|
final var levelcp = level;
|
||||||
var aufgaben = aufgabenList.getAufgaben().stream()
|
var aufgaben = aufgabenList.getAufgaben().stream()
|
||||||
.filter(aufgabe -> aufgabe.getBenoetigtAktiv().stream().noneMatch(item -> list.contains(item)))
|
.filter(aufgabe -> aufgabe.getLevel() == levelcp && aufgabe.getBenoetigtAktiv().stream().noneMatch(item -> list.contains(item)))
|
||||||
.toList();
|
.toList();
|
||||||
if (!aufgaben.isEmpty()) {
|
if (!aufgaben.isEmpty()) {
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
var aufgabe = aufgaben.get(new Random().nextInt(aufgaben.size()));
|
var aufgabe = aufgaben.get(new Random().nextInt(aufgaben.size()));
|
||||||
|
LOGGER.info("[LockGame {}] AUFGABE gezogen: kurzText='{}', level={}, sekunden={}-{}",
|
||||||
|
gamestate.getUserId(), aufgabe.getKurzText(), aufgabe.getLevel(),
|
||||||
|
aufgabe.getSekundenVon(), aufgabe.getSekundenBis());
|
||||||
gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe));
|
gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe));
|
||||||
return aufgabe.getText();
|
return aufgabe.getText();
|
||||||
}
|
}
|
||||||
@@ -108,12 +137,16 @@ public class LockGameService {
|
|||||||
gamestate.setActiveTask(aufgabe.getText());
|
gamestate.setActiveTask(aufgabe.getText());
|
||||||
gamestate.setTaskInQueue(null);
|
gamestate.setTaskInQueue(null);
|
||||||
var time = getAufgabeTime(aufgabe);
|
var time = getAufgabeTime(aufgabe);
|
||||||
gamestate.setActiveTaskEnd(LocalDateTime.now().plusSeconds(time));
|
gamestate.setActiveTaskEnd(time > 0 ? LocalDateTime.now().plusSeconds(time) : null);
|
||||||
|
LOGGER.info("[LockGame {}] AUFGABE aktiv: kurzText='{}', berechnete Zeit={}s (Range: {}s-{}s)",
|
||||||
|
gamestate.getUserId(), aufgabe.getKurzText(), time,
|
||||||
|
aufgabe.getSekundenVon(), aufgabe.getSekundenBis());
|
||||||
lockGameRepository.save(gamestate);
|
lockGameRepository.save(gamestate);
|
||||||
return time;
|
return time;
|
||||||
} else if (gamestate.getLockInQueue() != null) {
|
} else if (gamestate.getLockInQueue() != null) {
|
||||||
var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class);
|
var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class);
|
||||||
gamestate.setActiveTask(lock.getText());
|
String displayText = lock.getText() != null ? lock.getText() : lock.getKurzText();
|
||||||
|
gamestate.setActiveTask(displayText != null ? displayText : "Zeitstrafe aktiv");
|
||||||
gamestate.setLockInQueue(null);
|
gamestate.setLockInQueue(null);
|
||||||
applyLock(lock);
|
applyLock(lock);
|
||||||
}
|
}
|
||||||
@@ -157,7 +190,11 @@ public class LockGameService {
|
|||||||
entity.setGameId(gamestate.getGameId());
|
entity.setGameId(gamestate.getGameId());
|
||||||
entity.setLockFor(lock.getSperreFuer());
|
entity.setLockFor(lock.getSperreFuer());
|
||||||
entity.setReleaseText(lock.getReleaseText());
|
entity.setReleaseText(lock.getReleaseText());
|
||||||
entity.setReleaseTime(LocalDateTime.now().plusMinutes(getLockTime(lock)));
|
int lockMinutes = getLockTime(lock);
|
||||||
|
entity.setReleaseTime(LocalDateTime.now().plusMinutes(lockMinutes));
|
||||||
|
LOGGER.info("[LockGame {}] ZEITSTRAFE aktiv: kurzText='{}', berechnete Zeit={}min (Range: {}min-{}min), sperreFuer={}",
|
||||||
|
gamestate.getUserId(), lock.getKurzText(), lockMinutes,
|
||||||
|
lock.getMinutenVon(), lock.getMinutenBis(), lock.getSperreFuer());
|
||||||
lockGameLockRepository.save(entity);
|
lockGameLockRepository.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class CommunityTaskVoteScheduler {
|
|||||||
int winnerIndex;
|
int winnerIndex;
|
||||||
if (entries.isEmpty()) {
|
if (entries.isEmpty()) {
|
||||||
winnerIndex = new Random().nextInt(tasks.size());
|
winnerIndex = new Random().nextInt(tasks.size());
|
||||||
LOG.debug("No votes → random task index {}", winnerIndex);
|
LOG.info("[CardLock {}] COMMUNITY-Vote: keine Stimmen – zufällige Aufgabe, Index {}", vote.getLockId(), winnerIndex);
|
||||||
} else {
|
} else {
|
||||||
int[] counts = new int[tasks.size()];
|
int[] counts = new int[tasks.size()];
|
||||||
for (var e : entries) {
|
for (var e : entries) {
|
||||||
@@ -83,10 +83,12 @@ public class CommunityTaskVoteScheduler {
|
|||||||
if (counts[i] == max) winners.add(i);
|
if (counts[i] == max) winners.add(i);
|
||||||
}
|
}
|
||||||
winnerIndex = winners.get(new Random().nextInt(winners.size()));
|
winnerIndex = winners.get(new Random().nextInt(winners.size()));
|
||||||
LOG.debug("Vote winner: task index {} with {} votes", winnerIndex, max);
|
LOG.info("[CardLock {}] COMMUNITY-Vote: Gewinner Index {} mit {} Stimmen (gesamt {} Stimmen)",
|
||||||
|
vote.getLockId(), winnerIndex, max, entries.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
Task task = tasks.get(winnerIndex);
|
Task task = tasks.get(winnerIndex);
|
||||||
|
LOG.info("[CardLock {}] COMMUNITY-Aufgabe vergeben: title='{}', minutes={}", vote.getLockId(), task.getTitle(), task.getMinutes());
|
||||||
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
||||||
assigned.setLockId(lock.getLockId());
|
assigned.setLockId(lock.getLockId());
|
||||||
assigned.setTaskTitle(task.getTitle());
|
assigned.setTaskTitle(task.getTitle());
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -28,6 +30,8 @@ import de.oaa.xxx.user.UserService;
|
|||||||
@RequestMapping("/games/chastity/keyholder/choices")
|
@RequestMapping("/games/chastity/keyholder/choices")
|
||||||
public class KeyholderTaskChoiceController {
|
public class KeyholderTaskChoiceController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(KeyholderTaskChoiceController.class);
|
||||||
|
|
||||||
private final CardlockRepository cardlockRepository;
|
private final CardlockRepository cardlockRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
private final KeyholderTaskChoiceRepository keyholderTaskChoiceRepository;
|
||||||
@@ -121,6 +125,10 @@ public class KeyholderTaskChoiceController {
|
|||||||
}
|
}
|
||||||
assignedTaskRepository.save(assigned);
|
assignedTaskRepository.save(assigned);
|
||||||
|
|
||||||
|
LOG.info("[CardLock {}] KEYHOLDER-Aufgabe vergeben: title='{}', minutes={}, penaltyFreeze={}min, penaltyRedCards={}",
|
||||||
|
lock.getLockId(), task.getTitle(), task.getMinutes(),
|
||||||
|
assigned.getPenaltyFreezeMinutes(), assigned.getPenaltyRedCards());
|
||||||
|
|
||||||
choice.setActive(false);
|
choice.setActive(false);
|
||||||
keyholderTaskChoiceRepository.save(choice);
|
keyholderTaskChoiceRepository.save(choice);
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ public class KeyholderTaskChoiceScheduler {
|
|||||||
|
|
||||||
int taskIndex = new Random().nextInt(tasks.size());
|
int taskIndex = new Random().nextInt(tasks.size());
|
||||||
Task task = tasks.get(taskIndex);
|
Task task = tasks.get(taskIndex);
|
||||||
LOG.debug("Keyholder did not choose in time → random task index {}", taskIndex);
|
LOG.info("[CardLock {}] KEYHOLDER-Timeout: zufällige Aufgabe vergeben – Index {}, title='{}', minutes={}",
|
||||||
|
lock.getLockId(), taskIndex, task.getTitle(), task.getMinutes());
|
||||||
|
|
||||||
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
AssignedTaskEntity assigned = new AssignedTaskEntity();
|
||||||
assigned.setLockId(lock.getLockId());
|
assigned.setLockId(lock.getLockId());
|
||||||
|
|||||||
@@ -375,4 +375,15 @@ public class TimeLockService extends BaseLockService implements LockControlCallb
|
|||||||
public int getUnlockcodeLenght() {
|
public int getUnlockcodeLenght() {
|
||||||
return lock.getUnlockCodeLength();
|
return lock.getUnlockCodeLength();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleLockGameFinished(int timeInMinutes) {
|
||||||
|
int freezeTime = (int) (timeInMinutes * new Random().nextDouble(1.0, 4.0));
|
||||||
|
addTime(freezeTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void penaltyLockGame() {
|
||||||
|
handleLockGameFinished(60);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE aktive_sperre_fuer DROP FOREIGN KEY FK36uaxlluxoow36iy1pqd4ig8b;
|
||||||
|
ALTER TABLE aktive_sperre_fuer ADD CONSTRAINT fk_aktive_sperre_fuer_lock_game_lock
|
||||||
|
FOREIGN KEY (aktive_sperre_id) REFERENCES lock_game_lock (lock_game_lock_id);
|
||||||
@@ -902,6 +902,10 @@
|
|||||||
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
|
else document.getElementById('lockContent').textContent = 'Kein Lock angegeben.';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('pageshow', (e) => {
|
||||||
|
if (e.persisted && lockId) loadLock();
|
||||||
|
});
|
||||||
|
|
||||||
async function loadLock() {
|
async function loadLock() {
|
||||||
const res = await fetch('/keyholder/cardlock/' + lockId);
|
const res = await fetch('/keyholder/cardlock/' + lockId);
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
@@ -1322,7 +1326,13 @@
|
|||||||
const cdEl = document.getElementById('gameCardCountdown');
|
const cdEl = document.getElementById('gameCardCountdown');
|
||||||
function tick() {
|
function tick() {
|
||||||
const diff = deadline - Date.now();
|
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);
|
cdEl.textContent = fmtCountdown(diff);
|
||||||
}
|
}
|
||||||
tick();
|
tick();
|
||||||
@@ -1335,7 +1345,8 @@
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const gameSetId = data.gameSetId;
|
const gameSetId = data.gameSetId;
|
||||||
const url = '/games/chastity/taskgame.html?lockId=' + lockId
|
const url = '/games/chastity/taskgame.html?lockId=' + lockId
|
||||||
+ (gameSetId ? '&gameSetId=' + gameSetId : '');
|
+ (gameSetId ? '&gameSetId=' + gameSetId : '')
|
||||||
|
+ '&fresh=1';
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -405,13 +405,9 @@
|
|||||||
<div id="subGameSet" style="display:none;">
|
<div id="subGameSet" style="display:none;">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
||||||
<div style="position:relative;">
|
<select id="fGameSetId" onchange="markDirty()">
|
||||||
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
<option value="">Kein Aufgaben-Set</option>
|
||||||
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
</select>
|
||||||
<div id="gameSetDropdown" class="gs-dropdown"></div>
|
|
||||||
</div>
|
|
||||||
<div id="gameSetSelected" class="gs-selected" style="display:none;"></div>
|
|
||||||
<input type="hidden" id="fGameSetId">
|
|
||||||
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
<div class="field-error-msg" id="errGameSet" style="display:none;">Bitte ein Aufgaben-Set für Spiel-Karten auswählen.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-bottom:0;">
|
<div class="form-row" style="margin-bottom:0;">
|
||||||
@@ -757,8 +753,30 @@
|
|||||||
{ label: 'Lang' },
|
{ label: 'Lang' },
|
||||||
{ label: 'Sehr lang' },
|
{ label: 'Sehr lang' },
|
||||||
];
|
];
|
||||||
let _gameSetSearchTimer = null;
|
let _gameGroups = [];
|
||||||
let _gameSetResults = [];
|
|
||||||
|
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() {
|
function checkGameCardSection() {
|
||||||
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
||||||
@@ -791,72 +809,6 @@
|
|||||||
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[+val].label;
|
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 ──
|
// ── Karten-Info ──
|
||||||
function openCardInfo(cardId) {
|
function openCardInfo(cardId) {
|
||||||
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
||||||
@@ -1300,17 +1252,12 @@
|
|||||||
|
|
||||||
// Task-Karte und Spiel-Karte
|
// Task-Karte und Spiel-Karte
|
||||||
checkTaskCardSection();
|
checkTaskCardSection();
|
||||||
clearGameSet();
|
|
||||||
checkGameCardSection();
|
checkGameCardSection();
|
||||||
const gsi = template?.gameSpieldauerIdx ?? 2;
|
const gsi = template?.gameSpieldauerIdx ?? 2;
|
||||||
document.getElementById('sldGameSpieldauer').value = gsi;
|
document.getElementById('sldGameSpieldauer').value = gsi;
|
||||||
updateGameSpieldauer(gsi);
|
updateGameSpieldauer(gsi);
|
||||||
if (template?.gameSetId) {
|
populateGameSetSelect();
|
||||||
fetch(`/gruppe/${template.gameSetId}`)
|
document.getElementById('fGameSetId').value = template?.gameSetId || '';
|
||||||
.then(r => r.ok ? r.json() : null)
|
|
||||||
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'TIMELOCK') {
|
if (type === 'TIMELOCK') {
|
||||||
@@ -1664,6 +1611,7 @@
|
|||||||
document.getElementById('templateList').innerHTML = '';
|
document.getElementById('templateList').innerHTML = '';
|
||||||
document.getElementById('listEmpty').style.display = 'none';
|
document.getElementById('listEmpty').style.display = 'none';
|
||||||
await loadTaskSets();
|
await loadTaskSets();
|
||||||
|
await loadGameGroups();
|
||||||
loadNextPage();
|
loadNextPage();
|
||||||
loadSubscribedTemplates();
|
loadSubscribedTemplates();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,22 +22,36 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
height: 1.2rem;
|
||||||
}
|
}
|
||||||
.game-text {
|
.game-text {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
height: 14rem;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.game-timer {
|
.game-timer {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0.75rem 0;
|
|
||||||
letter-spacing: 0.04em;
|
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-timer.urgent { color: #e74c3c; }
|
||||||
|
.game-btn-row {
|
||||||
|
margin-top: 1rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.level-display {
|
.level-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -82,6 +96,20 @@
|
|||||||
}
|
}
|
||||||
.group-item input[type=radio] { accent-color: var(--color-primary); }
|
.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 {
|
.btn-primary {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
@@ -130,6 +158,21 @@
|
|||||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||||
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
|
<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 -->
|
<!-- Initialisierung: Gruppe wählen -->
|
||||||
<div id="initBox" class="init-box" style="display:none;">
|
<div id="initBox" class="init-box" style="display:none;">
|
||||||
<h2>Spiel-Set auswählen</h2>
|
<h2>Spiel-Set auswählen</h2>
|
||||||
@@ -143,22 +186,22 @@
|
|||||||
<!-- Laufendes Spiel -->
|
<!-- Laufendes Spiel -->
|
||||||
<div id="gameBox" style="display:none;">
|
<div id="gameBox" style="display:none;">
|
||||||
|
|
||||||
<!-- Task oder Lock in Queue -->
|
<!-- Einheitliche Spielkarte -->
|
||||||
<div id="queueBox" class="game-card" style="display:none;">
|
<div id="gameCard" class="game-card" style="display:none;">
|
||||||
<div class="game-label" id="queueLabel"></div>
|
<div class="game-label" id="gameLabel"></div>
|
||||||
<div class="game-text" id="queueText"></div>
|
<div class="game-text" id="gameText"></div>
|
||||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
<div class="game-timer" id="gameTimer"></div>
|
||||||
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button>
|
<div class="game-btn-row">
|
||||||
|
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Aktive Aufgabe (läuft) -->
|
<!-- Release-Text (Sperren) -->
|
||||||
<div id="activeBox" class="game-card" style="display:none;">
|
<div id="lockReleaseBox" class="game-card" style="display:none;">
|
||||||
<div class="game-label">Aktive Aufgabe</div>
|
<div class="game-label">🔓 Sperre aufgehoben</div>
|
||||||
<div class="game-text" id="activeText"></div>
|
<div class="game-text" id="releaseText"></div>
|
||||||
<div class="game-timer" id="activeTimer" style="display:none;"></div>
|
<div style="margin-top:1.1rem;">
|
||||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
<button class="btn-primary" id="btnReleaseOk">OK</button>
|
||||||
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -168,7 +211,12 @@
|
|||||||
<h2>Level 6 erreicht!</h2>
|
<h2>Level 6 erreicht!</h2>
|
||||||
<div class="game-label" id="finisherTitle"></div>
|
<div class="game-label" id="finisherTitle"></div>
|
||||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -187,17 +235,18 @@
|
|||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const lockId = params.get('lockId');
|
const lockId = params.get('lockId');
|
||||||
const autoGameSetId = params.get('gameSetId');
|
const autoGameSetId = params.get('gameSetId');
|
||||||
|
const freshStart = params.get('fresh') === '1';
|
||||||
|
let _resolvedGameSetId = autoGameSetId;
|
||||||
let _state = null;
|
let _state = null;
|
||||||
let _timerInt = null;
|
let _timerInt = null;
|
||||||
let _pendingIsLock = false;
|
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
|
||||||
let _pendingHasDuration = false;
|
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
||||||
else history.back();
|
else history.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeGame() {
|
async function debugExit() {
|
||||||
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
||||||
await fetch(url, { method: 'POST' });
|
await fetch(url, { method: 'POST' });
|
||||||
goBack();
|
goBack();
|
||||||
@@ -207,40 +256,134 @@
|
|||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
try {
|
try {
|
||||||
|
if (!freshStart) {
|
||||||
const r = await fetch('/lock-game/state');
|
const r = await fetch('/lock-game/state');
|
||||||
if (r.status === 404) {
|
if (r.ok) {
|
||||||
if (autoGameSetId) {
|
|
||||||
await autoStartGame(autoGameSetId);
|
|
||||||
} else {
|
|
||||||
await loadGroups();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
|
||||||
_state = await r.json();
|
_state = await r.json();
|
||||||
hide('loadingHint');
|
hide('loadingHint');
|
||||||
await runGameLoop();
|
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) {
|
} catch (e) {
|
||||||
showError(e.message);
|
showError(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function autoStartGame(gameSetId) {
|
// ── Toy-Auswahl ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function loadAndShowToys(gameSetId) {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + gameSetId, { method: 'POST' });
|
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 });
|
||||||
|
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');
|
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||||
|
|
||||||
const stateR = await fetch('/lock-game/state');
|
const stateR = await fetch('/lock-game/state');
|
||||||
_state = await stateR.json();
|
_state = await stateR.json();
|
||||||
hide('loadingHint');
|
hide('loadingHint');
|
||||||
await runGameLoop();
|
await runGameLoop();
|
||||||
} catch (e) {
|
|
||||||
showError(e.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function loadGroups() {
|
||||||
const r = await fetch('/lock-game/groups');
|
const r = await fetch('/lock-game/groups');
|
||||||
const groups = r.ok ? await r.json() : [];
|
const groups = r.ok ? await r.json() : [];
|
||||||
|
|
||||||
|
if (groups.length === 1) {
|
||||||
|
_resolvedGameSetId = groups[0].gruppenId;
|
||||||
|
await loadAndShowToys(groups[0].gruppenId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
hide('loadingHint');
|
hide('loadingHint');
|
||||||
const list = document.getElementById('groupList');
|
const list = document.getElementById('groupList');
|
||||||
if (groups.length === 0) {
|
if (groups.length === 0) {
|
||||||
@@ -270,40 +413,39 @@
|
|||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
hide('initBox');
|
hide('initBox');
|
||||||
show('loadingHint');
|
show('loadingHint');
|
||||||
try {
|
_resolvedGameSetId = sel.value;
|
||||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' });
|
await loadAndShowToys(sel.value);
|
||||||
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 ─────────────────────────────────────────────────────────────
|
// ── 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() {
|
async function runGameLoop() {
|
||||||
hide('queueBox');
|
hide('gameCard');
|
||||||
hide('activeBox');
|
|
||||||
hide('finisherBox');
|
hide('finisherBox');
|
||||||
clearTimer();
|
clearTimer();
|
||||||
|
|
||||||
if (_state.level >= 6) {
|
if (_state.level >= 6) {
|
||||||
await showFinisher();
|
await showFinisherFlow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLevelBar(_state.level);
|
renderLevelBar(_state.level);
|
||||||
|
|
||||||
// Aktive Aufgabe läuft noch
|
|
||||||
if (_state.activeTask) {
|
if (_state.activeTask) {
|
||||||
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue leer → nächsten Task holen
|
|
||||||
if (!_state.taskInQueue && !_state.lockInQueue) {
|
if (!_state.taskInQueue && !_state.lockInQueue) {
|
||||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||||
const r = await fetch('/lock-game/state');
|
const r = await fetch('/lock-game/state');
|
||||||
@@ -317,90 +459,155 @@
|
|||||||
if (_state.lockInQueue) {
|
if (_state.lockInQueue) {
|
||||||
let sperre;
|
let sperre;
|
||||||
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
||||||
_pendingIsLock = true;
|
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
|
||||||
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
|
|
||||||
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
|
|
||||||
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
|
|
||||||
} else if (_state.taskInQueue) {
|
} else if (_state.taskInQueue) {
|
||||||
let aufgabe;
|
let aufgabe;
|
||||||
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
||||||
_pendingIsLock = false;
|
const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||||
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
|
||||||
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe';
|
hasDuration ? '▶ Starten' : '✓ Erledigt');
|
||||||
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
|
|
||||||
}
|
}
|
||||||
show('gameBox');
|
show('gameBox');
|
||||||
show('queueBox');
|
show('gameCard');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showActiveTask(text, endIso) {
|
function showActiveTask(text, endIso) {
|
||||||
document.getElementById('activeText').textContent = text;
|
|
||||||
show('gameBox');
|
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) {
|
if (endIso) {
|
||||||
const end = new Date(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);
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
_gameAction = 'active-done';
|
||||||
|
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||||
|
}
|
||||||
|
|
||||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
function handleGameBtn() {
|
||||||
const r = await fetch('/lock-game/state');
|
switch (_gameAction) {
|
||||||
_state = await r.json();
|
case 'queue-start': doQueueStart(); break;
|
||||||
|
case 'queue-done': doQueueDone(); break;
|
||||||
|
case 'active-running': doCancelCountdown(); break;
|
||||||
|
case 'active-done': doErledigt(); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doQueueStart() {
|
||||||
|
try {
|
||||||
|
await checkAndShowLocks();
|
||||||
|
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||||||
|
if (!r.ok) { showError('Fehler beim Starten'); return; }
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
await runGameLoop();
|
await runGameLoop();
|
||||||
|
} catch (e) { showError(e.message || 'Fehler (Starten)'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
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)'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function doCancelCountdown() {
|
||||||
|
clearTimer();
|
||||||
|
_gameAction = 'active-done';
|
||||||
|
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
async function checkAndShowLocks() {
|
||||||
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
const texts = await r.json();
|
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');
|
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');
|
show('lockMessages');
|
||||||
await new Promise(res => setTimeout(res, 2000));
|
await new Promise(res => setTimeout(res, 2000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showFinisher() {
|
async function showFinisherFlow() {
|
||||||
show('gameBox');
|
show('gameBox');
|
||||||
const r = await fetch('/lock-game/finisher');
|
hide('gameCard');
|
||||||
if (!r.ok) {
|
hide('lockReleaseBox');
|
||||||
document.getElementById('finisherTitle').textContent = '';
|
hide('finisherBox');
|
||||||
document.getElementById('finisherText').textContent = 'Glückwunsch – du hast Level 6 erreicht!';
|
|
||||||
} else {
|
// 1. Release-Texte sequenziell anzeigen
|
||||||
const f = await r.json();
|
try {
|
||||||
document.getElementById('finisherTitle').textContent = f.kurzText || '';
|
const r = await fetch('/lock-game/release-locks');
|
||||||
document.getElementById('finisherText').textContent = f.text || '';
|
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');
|
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 ─────────────────────────────────────────────────────────────
|
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||||||
@@ -413,8 +620,12 @@
|
|||||||
|
|
||||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function playSound(src) {
|
||||||
|
try { new Audio(src).play().catch(() => {}); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
function startTimer(endDate, el) {
|
function startTimer(endDate, el) {
|
||||||
el.style.display = '';
|
el.classList.add('active');
|
||||||
clearTimer();
|
clearTimer();
|
||||||
_timerInt = setInterval(() => {
|
_timerInt = setInterval(() => {
|
||||||
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||||
@@ -422,7 +633,12 @@
|
|||||||
const s = String(diff % 60).padStart(2, '0');
|
const s = String(diff % 60).padStart(2, '0');
|
||||||
el.textContent = m + ':' + s;
|
el.textContent = m + ':' + s;
|
||||||
el.classList.toggle('urgent', diff < 30);
|
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);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user