Weiter am Taskgame gebastelt
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:
2
bin/main/db/migration/V8__lock_game_aufgaben_text.sql
Normal file
2
bin/main/db/migration/V8__lock_game_aufgaben_text.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE lock_game
|
||||
MODIFY COLUMN aufgaben TEXT NULL;
|
||||
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.
Binary file not shown.
BIN
bin/main/de/oaa/xxx/games/chastity/common/BaseLockHelper.class
Normal file
BIN
bin/main/de/oaa/xxx/games/chastity/common/BaseLockHelper.class
Normal file
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.
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.
@@ -263,6 +263,12 @@
|
||||
cursor: pointer; transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-item-edit:hover { border-color: var(--color-text); color: var(--color-text); }
|
||||
.btn-item-copy {
|
||||
background: none; border: 1px solid rgba(100,160,255,0.4); border-radius: 5px;
|
||||
color: var(--color-muted); font-size: 0.75rem; padding: 0.2rem 0.6rem;
|
||||
cursor: pointer; transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-item-copy:hover { border-color: rgba(100,160,255,0.9); color: var(--color-text); }
|
||||
.btn-item-delete {
|
||||
background: none; border: 1px solid rgba(233,69,96,0.4); border-radius: 5px;
|
||||
color: var(--color-primary); font-size: 0.75rem; padding: 0.2rem 0.6rem;
|
||||
@@ -533,17 +539,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="iTempUnlockRow">
|
||||
<label>Temporäre Öffnungen</label>
|
||||
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem;">
|
||||
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;">
|
||||
<input type="checkbox" id="iTempUnlockBefore" style="accent-color:var(--color-primary); width:1rem; height:1rem;">
|
||||
Temporäre Öffnung <em>vor</em> der Zeitstrafe erforderlich
|
||||
</label>
|
||||
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;">
|
||||
<input type="checkbox" id="iTempUnlockAfter" style="accent-color:var(--color-primary); width:1rem; height:1rem;">
|
||||
Temporäre Öffnung <em>nach</em> der Zeitstrafe erforderlich
|
||||
</label>
|
||||
</div>
|
||||
<label class="toggle-switch" style="display:flex; align-items:center; gap:0.75rem; cursor:pointer; margin-top:0.25rem;">
|
||||
<input type="checkbox" id="iTempUnlockRequired">
|
||||
<span class="toggle-track"></span>
|
||||
<span style="font-size:0.9rem;">Temporäre Öffnung erforderlich</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="iReleaseTextRow">
|
||||
<label for="iReleaseText">Text bei Aufhebung</label>
|
||||
@@ -765,7 +765,7 @@
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${g.availableIn !== 'CHASTITY_ONLY' ? renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, g.gruppenId, type) : ''}
|
||||
${renderSubSection('Zeitstrafen',sortByName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type)}
|
||||
${renderSubSection('Zeitstrafen',sortByLevelThenName(g.sperren || []), 'zeitstrafe',renderZeitstrafe, g.gruppenId, type)}
|
||||
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -814,6 +814,7 @@
|
||||
function renderAufgabe(a, type, gruppenId) {
|
||||
_itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId };
|
||||
const badges = [];
|
||||
(a.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
|
||||
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
|
||||
@@ -826,6 +827,7 @@
|
||||
const actionBtns = type === 'user' ? `
|
||||
<div class="item-action-btns">
|
||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(a.aufgabeId)}',event)">✎ Bearbeiten</button>
|
||||
<button class="btn-item-copy" onclick="duplicateItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
|
||||
<button class="btn-item-delete" onclick="deleteItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||
</div>` : '';
|
||||
|
||||
@@ -841,6 +843,7 @@
|
||||
function renderStrafe(s, type, gruppenId) {
|
||||
_itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId };
|
||||
const badges = [];
|
||||
(s.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
|
||||
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
|
||||
@@ -853,6 +856,7 @@
|
||||
const actionBtns = type === 'user' ? `
|
||||
<div class="item-action-btns">
|
||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(s.strafeId)}',event)">✎ Bearbeiten</button>
|
||||
<button class="btn-item-copy" onclick="duplicateItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
|
||||
<button class="btn-item-delete" onclick="deleteItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||
</div>` : '';
|
||||
|
||||
@@ -868,8 +872,10 @@
|
||||
function renderZeitstrafe(z, type, gruppenId) {
|
||||
_itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId };
|
||||
const badges = [];
|
||||
(z.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
|
||||
const zeit = formatMin(z.minutenVon, z.minutenBis);
|
||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
||||
if (z.level != null) badges.push(`<span class="badge">Level ${esc(String(z.level))}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
|
||||
@@ -879,6 +885,7 @@
|
||||
const actionBtns = type === 'user' ? `
|
||||
<div class="item-action-btns">
|
||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(z.sperreId)}',event)">✎ Bearbeiten</button>
|
||||
<button class="btn-item-copy" onclick="duplicateItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
|
||||
<button class="btn-item-delete" onclick="deleteItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||
</div>` : '';
|
||||
|
||||
@@ -896,6 +903,7 @@
|
||||
function renderFinisher(f, type, gruppenId) {
|
||||
_itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId };
|
||||
const badges = [];
|
||||
(f.benoetigteToys || []).forEach(t => badges.push(`<span class="item-detail-chip item-detail-chip-toy">${esc(t.name || t)}</span>`));
|
||||
if (f.geschlecht) badges.push(`<span class="badge badge-neutral">${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}</span>`);
|
||||
|
||||
const detailRows = [];
|
||||
@@ -906,6 +914,7 @@
|
||||
const actionBtns = type === 'user' ? `
|
||||
<div class="item-action-btns">
|
||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(f.finisherId)}',event)">✎ Bearbeiten</button>
|
||||
<button class="btn-item-copy" onclick="duplicateItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">⧉ Duplizieren</button>
|
||||
<button class="btn-item-delete" onclick="deleteItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||
</div>` : '';
|
||||
|
||||
@@ -944,10 +953,36 @@
|
||||
finisher: apiUrl('/finisher')
|
||||
};
|
||||
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' };
|
||||
const ITEM_COPY_URL = {
|
||||
aufgabe: apiUrl('/aufgabe/copy'),
|
||||
strafe: '/strafe/copy',
|
||||
zeitstrafe: '/sperre/copy',
|
||||
finisher: apiUrl('/finisher/copy')
|
||||
};
|
||||
|
||||
function duplicateItem(kind, itemId, gruppenId, event) {
|
||||
event.stopPropagation();
|
||||
const copyUrl = ITEM_COPY_URL[kind];
|
||||
if (!copyUrl) return;
|
||||
fetch(`${copyUrl}/${itemId}`, { method: 'POST' }).then(r => {
|
||||
if (r.ok) {
|
||||
pendingExpandId = gruppenId;
|
||||
pendingExpandType = 'user';
|
||||
_notifyOnLoad = true; loadUserGruppen();
|
||||
} else {
|
||||
document.getElementById('userActionError').textContent = 'Fehler beim Duplizieren (HTTP ' + r.status + ').';
|
||||
}
|
||||
}).catch(() => {
|
||||
document.getElementById('userActionError').textContent = 'Verbindungsfehler.';
|
||||
});
|
||||
}
|
||||
|
||||
function deleteItem(kind, itemId, gruppenId, event) {
|
||||
event.stopPropagation();
|
||||
if (!confirm('Eintrag wirklich löschen?')) return;
|
||||
openConfirmModal('Eintrag wirklich löschen?', () => _doDeleteItem(kind, itemId, gruppenId));
|
||||
}
|
||||
|
||||
function _doDeleteItem(kind, itemId, gruppenId) {
|
||||
const deleteUrl = ITEM_DELETE_URL[kind];
|
||||
if (!deleteUrl) return;
|
||||
const body = { [ITEM_DELETE_FIELD[kind]]: itemId };
|
||||
@@ -1430,7 +1465,7 @@
|
||||
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
|
||||
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
||||
});
|
||||
document.getElementById('iTempUnlockRow').style.display = (isZeit && isChastity) ? 'block' : 'none';
|
||||
document.getElementById('iTempUnlockRow').style.display = ((isZeit || isFinisher) && isChastity) ? 'block' : 'none';
|
||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
}
|
||||
|
||||
@@ -1449,8 +1484,7 @@
|
||||
document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iSperreFuer input').forEach(cb => cb.checked = false);
|
||||
document.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false);
|
||||
document.getElementById('iTempUnlockBefore').checked = false;
|
||||
document.getElementById('iTempUnlockAfter').checked = false;
|
||||
document.getElementById('iTempUnlockRequired').checked = false;
|
||||
_selectedToys = [];
|
||||
renderSelectedToys();
|
||||
document.getElementById('itemModalError').style.display = 'none';
|
||||
@@ -1495,6 +1529,9 @@
|
||||
const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`);
|
||||
if (rb) rb.checked = true;
|
||||
}
|
||||
if (_isChastityMode) {
|
||||
document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : '';
|
||||
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
|
||||
@@ -1502,8 +1539,7 @@
|
||||
(d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; });
|
||||
if (_isChastityMode) {
|
||||
document.getElementById('iLevel').value = d.level != null ? d.level : '';
|
||||
document.getElementById('iTempUnlockBefore').checked = d.tempUnlockBeforeRequired === true;
|
||||
document.getElementById('iTempUnlockAfter').checked = d.tempUnlockAfterRequired === true;
|
||||
document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1666,11 +1702,12 @@
|
||||
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||
payload = {
|
||||
kurzText, text,
|
||||
geschlecht: geschlecht || null,
|
||||
gruppeId: isEdit ? undefined : currentItemGruppeId,
|
||||
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
geschlecht: geschlecht || null,
|
||||
gruppeId: isEdit ? undefined : currentItemGruppeId,
|
||||
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })),
|
||||
tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null
|
||||
};
|
||||
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
|
||||
method = isEdit ? 'PUT' : 'POST';
|
||||
@@ -1698,8 +1735,7 @@
|
||||
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
||||
sperreFuer,
|
||||
level: zeitLevel,
|
||||
tempUnlockBeforeRequired: _isChastityMode ? document.getElementById('iTempUnlockBefore').checked : null,
|
||||
tempUnlockAfterRequired: _isChastityMode ? document.getElementById('iTempUnlockAfter').checked : null,
|
||||
tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null,
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
|
||||
|
||||
@@ -679,6 +679,7 @@
|
||||
|
||||
// ── Gruppe lists ──
|
||||
function renderGruppeList(containerId, gruppen) {
|
||||
gruppen = gruppen.filter(g => g.availableIn !== 'CHASTITY_ONLY');
|
||||
const ul = document.getElementById(containerId);
|
||||
const section = ul.closest('[id^="section"]');
|
||||
const selectAllWrap = section?.querySelector('.select-all-label');
|
||||
|
||||
@@ -256,9 +256,14 @@
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row" id="rowTestLock">
|
||||
<input type="checkbox" id="testLock">
|
||||
<input type="checkbox" id="testLock" onchange="onTestLockChange()">
|
||||
<label for="testLock">Test-Lock <span class="form-hint">(kein echter Lock, zum Ausprobieren)</span></label>
|
||||
</div>
|
||||
<div id="rowSpeedFactor" style="display:none; align-items:center; gap:12px; padding:6px 0;">
|
||||
<label for="speedFactor" style="white-space:nowrap;">Geschwindigkeit:</label>
|
||||
<input type="range" id="speedFactor" min="1" max="10" value="1" style="flex:1;" oninput="document.getElementById('speedFactorLabel').textContent = '×' + this.value">
|
||||
<span id="speedFactorLabel" style="min-width:32px; text-align:right;">×1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error-msg" id="errorMsg"></div>
|
||||
@@ -515,6 +520,7 @@
|
||||
khInput.readOnly = true;
|
||||
khInput.style.opacity = '0.6';
|
||||
document.getElementById('rowTestLock').style.display = 'none';
|
||||
document.getElementById('rowSpeedFactor').style.display = 'none';
|
||||
document.getElementById('rowDetailsVisible').style.display = '';
|
||||
} else {
|
||||
khInput.readOnly = false;
|
||||
@@ -728,6 +734,15 @@
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
function onTestLockChange() {
|
||||
const checked = document.getElementById('testLock').checked;
|
||||
document.getElementById('rowSpeedFactor').style.display = checked ? 'flex' : 'none';
|
||||
if (!checked) {
|
||||
document.getElementById('speedFactor').value = 1;
|
||||
document.getElementById('speedFactorLabel').textContent = '×1';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Absenden ──
|
||||
async function createSession() {
|
||||
document.getElementById('errorMsg').style.display = 'none';
|
||||
@@ -756,6 +771,7 @@
|
||||
const isFriendLockee = lockeeVal && lockeeVal !== myUserId;
|
||||
const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5);
|
||||
const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked;
|
||||
const speedFactor = isTestLock ? parseInt(document.getElementById('speedFactor').value) : 1;
|
||||
|
||||
let endpoint, body;
|
||||
|
||||
@@ -769,6 +785,7 @@
|
||||
testLock: isTestLock,
|
||||
unlockCodeLength: unlockCodeLen,
|
||||
controllType: selectedLockControl,
|
||||
speedFactor: speedFactor,
|
||||
};
|
||||
} else {
|
||||
// CardLock
|
||||
@@ -798,6 +815,7 @@
|
||||
controllType: selectedLockControl,
|
||||
gameSetId: t.gameSetId || null,
|
||||
gameSpieldauerIdx: t.gameSpieldauerIdx ?? null,
|
||||
speedFactor: speedFactor,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,48 @@
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.game-requirements {
|
||||
margin: 0.75rem 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.game-requirements-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
.req-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 7px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid var(--color-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.req-check input[type="checkbox"] {
|
||||
accent-color: var(--color-primary);
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.req-check.done {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(233,69,96,0.07);
|
||||
color: var(--color-muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.level-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -190,6 +232,7 @@
|
||||
<div id="gameCard" class="game-card" style="display:none;">
|
||||
<div class="game-label" id="gameLabel"></div>
|
||||
<div class="game-text" id="gameText"></div>
|
||||
<div id="gameRequirements" class="game-requirements" style="display:none;"></div>
|
||||
<div class="game-timer" id="gameTimer"></div>
|
||||
<div class="game-btn-row">
|
||||
<button class="btn-primary" id="gameBtn" onclick="handleGameBtn()" style="width:100%;height:100%;"></button>
|
||||
@@ -205,13 +248,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temporäre Öffnung -->
|
||||
<div id="tempOpeningBox" class="game-card" style="display:none;">
|
||||
<div class="game-label">🔓 Temporäre Öffnung erforderlich</div>
|
||||
<div class="game-text" id="tempOpeningTask"></div>
|
||||
<div id="tempOpeningCodeRow" style="display:none; margin-top:1rem; text-align:center;">
|
||||
<div class="game-label">Entsperrcode</div>
|
||||
<div id="tempOpeningCode" style="font-size:1.8rem; font-weight:700; letter-spacing:0.18em; padding:0.6rem 0; color:var(--color-primary);"></div>
|
||||
</div>
|
||||
<div class="game-btn-row">
|
||||
<button class="btn-primary" onclick="doEndTempOpening()">✓ Erledigt</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Finisher -->
|
||||
<div id="finisherBox" style="display:none;">
|
||||
<div class="trophy">🏆</div>
|
||||
<h2>Level 6 erreicht!</h2>
|
||||
<div class="game-label" id="finisherTitle"></div>
|
||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
||||
<button class="btn-primary" id="btnFinisherOk" style="margin-top:1.25rem;">✓ OK</button>
|
||||
<div id="finisherStart" style="margin-top:1.25rem;">
|
||||
<button class="btn-primary" onclick="doStartFinisher()">▶ Starten</button>
|
||||
</div>
|
||||
<div id="finisherRunning" style="display:none;margin-top:1.25rem;">
|
||||
<div class="game-timer active" id="finisherTimer">00:00</div>
|
||||
<button class="btn-primary" onclick="doEndFinisher()" style="margin-top:1rem;">✓ Erledigt</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug -->
|
||||
@@ -340,6 +402,7 @@
|
||||
|
||||
async function startWithExcludedToys(gameSetId, excludedToyIds) {
|
||||
const params = new URLSearchParams({ aufgabenGruppeId: gameSetId });
|
||||
if (lockId) params.append('lockId', lockId);
|
||||
excludedToyIds.forEach(id => params.append('excludedToyIds', id));
|
||||
const r = await fetch('/lock-game/init?' + params.toString(), { method: 'POST' });
|
||||
|
||||
@@ -417,6 +480,27 @@
|
||||
await loadAndShowToys(sel.value);
|
||||
}
|
||||
|
||||
// ── Benötigt-Checkboxen ───────────────────────────────────────────────────
|
||||
|
||||
const WERKZEUG_LABEL = {
|
||||
MUND: 'Mund', VAGINA: 'Vagina', PENIS: 'Penis',
|
||||
ANUS: 'Anus', UMSCHNALLDILDO: 'Umschnall-Dildo'
|
||||
};
|
||||
|
||||
function renderRequirements(list) {
|
||||
const box = document.getElementById('gameRequirements');
|
||||
if (!list || list.length === 0) { box.style.display = 'none'; box.innerHTML = ''; return; }
|
||||
box.innerHTML = '<div class="game-requirements-label">Benötigt</div>' +
|
||||
list.map(w => {
|
||||
const label = WERKZEUG_LABEL[w] || w;
|
||||
return `<label class="req-check" onclick="this.classList.toggle('done',this.querySelector('input').checked)">
|
||||
<input type="checkbox" onchange="this.closest('.req-check').classList.toggle('done',this.checked)">
|
||||
<span>${label}</span>
|
||||
</label>`;
|
||||
}).join('');
|
||||
box.style.display = 'flex';
|
||||
}
|
||||
|
||||
// ── Game Loop ─────────────────────────────────────────────────────────────
|
||||
|
||||
function setGameCard(label, text, action, btnLabel) {
|
||||
@@ -432,10 +516,16 @@
|
||||
async function runGameLoop() {
|
||||
hide('gameCard');
|
||||
hide('finisherBox');
|
||||
hide('tempOpeningBox');
|
||||
clearTimer();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisherFlow();
|
||||
if (_state.finisher) {
|
||||
showFinisherUI();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_state.tempOpeningTime) {
|
||||
showTempOpeningDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -460,12 +550,14 @@
|
||||
let sperre;
|
||||
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
||||
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', '▶ Starten');
|
||||
renderRequirements(null);
|
||||
} else if (_state.taskInQueue) {
|
||||
let aufgabe;
|
||||
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
||||
const hasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||
setGameCard('🎯 Neue Aufgabe', aufgabe.text || '', hasDuration ? 'queue-start' : 'queue-done',
|
||||
hasDuration ? '▶ Starten' : '✓ Erledigt');
|
||||
renderRequirements(aufgabe.benoetigtAktiv);
|
||||
}
|
||||
show('gameBox');
|
||||
show('gameCard');
|
||||
@@ -479,6 +571,7 @@
|
||||
timerEl.textContent = '';
|
||||
document.getElementById('gameLabel').textContent = 'Aktive Aufgabe';
|
||||
document.getElementById('gameText').textContent = text;
|
||||
renderRequirements(_state.activeTaskBenoetigtAktiv);
|
||||
|
||||
if (endIso) {
|
||||
const end = new Date(endIso);
|
||||
@@ -504,12 +597,31 @@
|
||||
|
||||
async function doQueueStart() {
|
||||
try {
|
||||
await checkAndShowLocks();
|
||||
const wasLock = !!_state.lockInQueue;
|
||||
let tempUnlockRequired = false;
|
||||
if (wasLock) {
|
||||
try { tempUnlockRequired = JSON.parse(_state.lockInQueue).tempUnlockRequired === true; } catch (_) {}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (wasLock && tempUnlockRequired) {
|
||||
await fetch('/lock-game/start-temp-opening', { method: 'POST' });
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
showTempOpeningDialog();
|
||||
} else if (wasLock) {
|
||||
const nextR = await fetch('/lock-game/abandon-task', { method: 'POST' });
|
||||
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
await runGameLoop();
|
||||
} else {
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
await runGameLoop();
|
||||
}
|
||||
} catch (e) { showError(e.message || 'Fehler (Starten)'); }
|
||||
}
|
||||
|
||||
@@ -526,16 +638,28 @@
|
||||
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
|
||||
}
|
||||
|
||||
function doCancelCountdown() {
|
||||
async function doCancelCountdown() {
|
||||
clearTimer();
|
||||
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||
if (lockR.ok) {
|
||||
const texts = await lockR.json();
|
||||
for (const text of (texts || [])) {
|
||||
if (text != null && text !== '') await waitForReleaseOk(text);
|
||||
}
|
||||
}
|
||||
_gameAction = 'active-done';
|
||||
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||
}
|
||||
|
||||
async function doErledigt() {
|
||||
try {
|
||||
await checkAndShowLocks();
|
||||
if (_state.level >= 6) { await showFinisherFlow(); return; }
|
||||
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||
if (lockR.ok) {
|
||||
const texts = await lockR.json();
|
||||
for (const text of (texts || [])) {
|
||||
if (text != null && text !== '') await waitForReleaseOk(text);
|
||||
}
|
||||
}
|
||||
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');
|
||||
@@ -557,51 +681,87 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function showFinisherFlow() {
|
||||
function showTempOpeningDialog() {
|
||||
show('gameBox');
|
||||
hide('gameCard');
|
||||
hide('lockReleaseBox');
|
||||
hide('finisherBox');
|
||||
|
||||
// 1. Release-Texte sequenziell anzeigen
|
||||
document.getElementById('tempOpeningTask').textContent = _state.activeTask || '';
|
||||
const code = _state.tempOpeningCode;
|
||||
if (code) {
|
||||
document.getElementById('tempOpeningCode').textContent = code;
|
||||
show('tempOpeningCodeRow');
|
||||
} else {
|
||||
hide('tempOpeningCodeRow');
|
||||
}
|
||||
show('tempOpeningBox');
|
||||
}
|
||||
|
||||
async function doEndTempOpening() {
|
||||
try {
|
||||
const r = await fetch('/lock-game/release-locks');
|
||||
if (r.ok) {
|
||||
const texts = await r.json();
|
||||
for (const text of texts) {
|
||||
await waitForReleaseOk(text);
|
||||
}
|
||||
}
|
||||
} catch (_) { /* ignorieren */ }
|
||||
await fetch('/lock-game/end-temp-opening', { method: 'POST' });
|
||||
await fetch('/lock-game/abandon-task', { method: 'POST' });
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
hide('tempOpeningBox');
|
||||
await runGameLoop();
|
||||
} catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); }
|
||||
}
|
||||
|
||||
// 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 */ }
|
||||
function showFinisherUI() {
|
||||
show('gameBox');
|
||||
hide('gameCard');
|
||||
hide('lockReleaseBox');
|
||||
|
||||
document.getElementById('finisherTitle').textContent = finisher?.kurzText || '';
|
||||
document.getElementById('finisherText').textContent = finisher?.text || 'Glückwunsch – du hast Level 6 erreicht!';
|
||||
let finisher = {};
|
||||
try { finisher = JSON.parse(_state.finisher); } catch (_) {}
|
||||
document.getElementById('finisherTitle').textContent = finisher.kurzText || '';
|
||||
document.getElementById('finisherText').textContent = finisher.text || '';
|
||||
|
||||
// 3. Warten bis Nutzer OK drückt
|
||||
await new Promise(resolve => {
|
||||
document.getElementById('btnFinisherOk').onclick = resolve;
|
||||
show('finisherBox');
|
||||
});
|
||||
if (_state.finisherStartedAt) {
|
||||
hide('finisherStart');
|
||||
show('finisherRunning');
|
||||
startElapsedTimer(new Date(_state.finisherStartedAt));
|
||||
} else {
|
||||
show('finisherStart');
|
||||
hide('finisherRunning');
|
||||
}
|
||||
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' });
|
||||
async function doStartFinisher() {
|
||||
await fetch('/lock-game/start-finisher', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
hide('finisherStart');
|
||||
show('finisherRunning');
|
||||
startElapsedTimer(new Date(_state.finisherStartedAt));
|
||||
}
|
||||
|
||||
async function doEndFinisher() {
|
||||
clearTimer();
|
||||
await fetch('/lock-game/end-finisher', { method: 'POST' });
|
||||
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
||||
await fetch(url, { method: 'POST' });
|
||||
goBack();
|
||||
}
|
||||
|
||||
function startElapsedTimer(startDate) {
|
||||
clearTimer();
|
||||
const el = document.getElementById('finisherTimer');
|
||||
_timerInt = setInterval(() => {
|
||||
const diff = Math.floor((Date.now() - startDate) / 1000);
|
||||
const m = String(Math.floor(diff / 60)).padStart(2, '0');
|
||||
const s = String(diff % 60).padStart(2, '0');
|
||||
el.textContent = m + ':' + s;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function waitForReleaseOk(text) {
|
||||
return new Promise(resolve => {
|
||||
document.getElementById('releaseText').textContent = text;
|
||||
hide('gameCard');
|
||||
document.getElementById('releaseText').textContent = text || '';
|
||||
document.getElementById('btnReleaseOk').onclick = () => {
|
||||
hide('lockReleaseBox');
|
||||
resolve();
|
||||
|
||||
Reference in New Issue
Block a user