Compare commits
4 Commits
843acea652
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ae64ca6aa3 | |||
| ca0e933d95 | |||
| c472093f62 | |||
| 4bd4635faf |
@@ -1,12 +1,3 @@
|
|||||||
Umsetzung des Spiels:
|
|
||||||
Der Lockee hat eine Stunde Zeit das Spiel zu starten, dies geschieht per Knopfdruck
|
|
||||||
Wenn er dies nicht schafft -> bei Keyholder, benachrichtige Keyholder und lass sie/ihn entscheiden, ansonsten freeze wie bei freeze card
|
|
||||||
Übernimm die Logik des Spiels aus dem BDSM Game.
|
|
||||||
Falls eine Zeitstrafe eine temporäre Öffnung vor oder nach der Aufgabe benötigt, öffne das Lock für 5 Minuten. Überzogene Zeit wird addiert und am Ende des Locks gefreezed
|
|
||||||
Selbiges gilt, falls der finisher eine temporäre Öffnung danach erfoldert
|
|
||||||
Benötigt der Finisher eine Öffnung davor, verwende die Logik der Cum Card, und addiere diese Zeit auf die möglicherweise schon vorhandenen Freeze Zeit am Ende des Locks
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen,
|
wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen,
|
||||||
|
|||||||
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);
|
||||||
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.
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.
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.
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.
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;
|
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-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 {
|
.btn-item-delete {
|
||||||
background: none; border: 1px solid rgba(233,69,96,0.4); border-radius: 5px;
|
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;
|
color: var(--color-primary); font-size: 0.75rem; padding: 0.2rem 0.6rem;
|
||||||
@@ -533,17 +539,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="iTempUnlockRow">
|
<div id="iTempUnlockRow">
|
||||||
<label>Temporäre Öffnungen</label>
|
<label class="toggle-switch" style="display:flex; align-items:center; gap:0.75rem; cursor:pointer; margin-top:0.25rem;">
|
||||||
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem;">
|
<input type="checkbox" id="iTempUnlockRequired">
|
||||||
<label style="display:flex; align-items:center; gap:0.6rem; font-size:0.85rem; cursor:pointer;">
|
<span class="toggle-track"></span>
|
||||||
<input type="checkbox" id="iTempUnlockBefore" style="accent-color:var(--color-primary); width:1rem; height:1rem;">
|
<span style="font-size:0.9rem;">Temporäre Öffnung erforderlich</span>
|
||||||
Temporäre Öffnung <em>vor</em> der Zeitstrafe erforderlich
|
</label>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="iReleaseTextRow">
|
<div id="iReleaseTextRow">
|
||||||
<label for="iReleaseText">Text bei Aufhebung</label>
|
<label for="iReleaseText">Text bei Aufhebung</label>
|
||||||
@@ -765,7 +765,7 @@
|
|||||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||||
${g.availableIn !== 'CHASTITY_ONLY' ? renderSubSection('Strafen', sortByLevelThenName(g.strafen || []), 'strafe', renderStrafe, 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)}
|
${renderSubSection('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -814,6 +814,7 @@
|
|||||||
function renderAufgabe(a, type, gruppenId) {
|
function renderAufgabe(a, type, gruppenId) {
|
||||||
_itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId };
|
_itemData[a.aufgabeId] = { ...a, _kind: 'aufgabe', _gruppenId: gruppenId };
|
||||||
const badges = [];
|
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);
|
const zeit = formatSek(a.sekundenVon, a.sekundenBis);
|
||||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
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>`);
|
if (a.level != null) badges.push(`<span class="badge">Level ${esc(String(a.level))}</span>`);
|
||||||
@@ -826,6 +827,7 @@
|
|||||||
const actionBtns = type === 'user' ? `
|
const actionBtns = type === 'user' ? `
|
||||||
<div class="item-action-btns">
|
<div class="item-action-btns">
|
||||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(a.aufgabeId)}',event)">✎ Bearbeiten</button>
|
<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>
|
<button class="btn-item-delete" onclick="deleteItem('aufgabe','${esc(a.aufgabeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
@@ -841,6 +843,7 @@
|
|||||||
function renderStrafe(s, type, gruppenId) {
|
function renderStrafe(s, type, gruppenId) {
|
||||||
_itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId };
|
_itemData[s.strafeId] = { ...s, _kind: 'strafe', _gruppenId: gruppenId };
|
||||||
const badges = [];
|
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);
|
const zeit = formatSek(s.sekundenVon, s.sekundenBis);
|
||||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
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>`);
|
if (s.level != null) badges.push(`<span class="badge">Level ${esc(String(s.level))}</span>`);
|
||||||
@@ -853,6 +856,7 @@
|
|||||||
const actionBtns = type === 'user' ? `
|
const actionBtns = type === 'user' ? `
|
||||||
<div class="item-action-btns">
|
<div class="item-action-btns">
|
||||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(s.strafeId)}',event)">✎ Bearbeiten</button>
|
<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>
|
<button class="btn-item-delete" onclick="deleteItem('strafe','${esc(s.strafeId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
@@ -868,8 +872,10 @@
|
|||||||
function renderZeitstrafe(z, type, gruppenId) {
|
function renderZeitstrafe(z, type, gruppenId) {
|
||||||
_itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId };
|
_itemData[z.sperreId] = { ...z, _kind: 'zeitstrafe', _gruppenId: gruppenId };
|
||||||
const badges = [];
|
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);
|
const zeit = formatMin(z.minutenVon, z.minutenBis);
|
||||||
if (zeit) badges.push(`<span class="badge badge-neutral">${esc(zeit)}</span>`);
|
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 = [];
|
const detailRows = [];
|
||||||
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
|
if (z.text) detailRows.push(`<div class="item-detail-text">${esc(z.text)}</div>`);
|
||||||
@@ -879,6 +885,7 @@
|
|||||||
const actionBtns = type === 'user' ? `
|
const actionBtns = type === 'user' ? `
|
||||||
<div class="item-action-btns">
|
<div class="item-action-btns">
|
||||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(z.sperreId)}',event)">✎ Bearbeiten</button>
|
<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>
|
<button class="btn-item-delete" onclick="deleteItem('zeitstrafe','${esc(z.sperreId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
@@ -896,6 +903,7 @@
|
|||||||
function renderFinisher(f, type, gruppenId) {
|
function renderFinisher(f, type, gruppenId) {
|
||||||
_itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId };
|
_itemData[f.finisherId] = { ...f, _kind: 'finisher', _gruppenId: gruppenId };
|
||||||
const badges = [];
|
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>`);
|
if (f.geschlecht) badges.push(`<span class="badge badge-neutral">${esc(GESCHLECHT_LABEL[f.geschlecht] || f.geschlecht)}</span>`);
|
||||||
|
|
||||||
const detailRows = [];
|
const detailRows = [];
|
||||||
@@ -906,6 +914,7 @@
|
|||||||
const actionBtns = type === 'user' ? `
|
const actionBtns = type === 'user' ? `
|
||||||
<div class="item-action-btns">
|
<div class="item-action-btns">
|
||||||
<button class="btn-item-edit" onclick="openEditItemModal('${esc(f.finisherId)}',event)">✎ Bearbeiten</button>
|
<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>
|
<button class="btn-item-delete" onclick="deleteItem('finisher','${esc(f.finisherId)}','${esc(gruppenId)}',event)">✕ Löschen</button>
|
||||||
</div>` : '';
|
</div>` : '';
|
||||||
|
|
||||||
@@ -944,10 +953,36 @@
|
|||||||
finisher: apiUrl('/finisher')
|
finisher: apiUrl('/finisher')
|
||||||
};
|
};
|
||||||
const ITEM_DELETE_FIELD = { aufgabe: 'aufgabeId', strafe: 'strafeId', zeitstrafe: 'sperreId', finisher: 'finisherId' };
|
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) {
|
function deleteItem(kind, itemId, gruppenId, event) {
|
||||||
event.stopPropagation();
|
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];
|
const deleteUrl = ITEM_DELETE_URL[kind];
|
||||||
if (!deleteUrl) return;
|
if (!deleteUrl) return;
|
||||||
const body = { [ITEM_DELETE_FIELD[kind]]: itemId };
|
const body = { [ITEM_DELETE_FIELD[kind]]: itemId };
|
||||||
@@ -1430,7 +1465,7 @@
|
|||||||
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
|
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
|
||||||
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
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';
|
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1449,8 +1484,7 @@
|
|||||||
document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false);
|
document.querySelectorAll('#iWerkzeugFinisherPassiv input').forEach(cb => cb.checked = false);
|
||||||
document.querySelectorAll('#iSperreFuer 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.querySelectorAll('#iGeschlecht input').forEach(rb => rb.checked = false);
|
||||||
document.getElementById('iTempUnlockBefore').checked = false;
|
document.getElementById('iTempUnlockRequired').checked = false;
|
||||||
document.getElementById('iTempUnlockAfter').checked = false;
|
|
||||||
_selectedToys = [];
|
_selectedToys = [];
|
||||||
renderSelectedToys();
|
renderSelectedToys();
|
||||||
document.getElementById('itemModalError').style.display = 'none';
|
document.getElementById('itemModalError').style.display = 'none';
|
||||||
@@ -1495,6 +1529,9 @@
|
|||||||
const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`);
|
const rb = document.querySelector(`#iGeschlecht input[value="${d.geschlecht}"]`);
|
||||||
if (rb) rb.checked = true;
|
if (rb) rb.checked = true;
|
||||||
}
|
}
|
||||||
|
if (_isChastityMode) {
|
||||||
|
document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : '';
|
document.getElementById('iMinVon').value = d.minutenVon != null ? d.minutenVon : '';
|
||||||
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
|
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; });
|
(d.sperreFuer || []).forEach(w => { const cb = document.querySelector(`#iSperreFuer input[value="${w}"]`); if (cb) cb.checked = true; });
|
||||||
if (_isChastityMode) {
|
if (_isChastityMode) {
|
||||||
document.getElementById('iLevel').value = d.level != null ? d.level : '';
|
document.getElementById('iLevel').value = d.level != null ? d.level : '';
|
||||||
document.getElementById('iTempUnlockBefore').checked = d.tempUnlockBeforeRequired === true;
|
document.getElementById('iTempUnlockRequired').checked = d.tempUnlockRequired === true;
|
||||||
document.getElementById('iTempUnlockAfter').checked = d.tempUnlockAfterRequired === true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1666,11 +1702,12 @@
|
|||||||
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||||
payload = {
|
payload = {
|
||||||
kurzText, text,
|
kurzText, text,
|
||||||
geschlecht: geschlecht || null,
|
geschlecht: geschlecht || null,
|
||||||
gruppeId: isEdit ? undefined : currentItemGruppeId,
|
gruppeId: isEdit ? undefined : currentItemGruppeId,
|
||||||
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
|
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
|
||||||
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
|
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
|
||||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId })),
|
||||||
|
tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null
|
||||||
};
|
};
|
||||||
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
|
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
|
||||||
method = isEdit ? 'PUT' : 'POST';
|
method = isEdit ? 'PUT' : 'POST';
|
||||||
@@ -1698,8 +1735,7 @@
|
|||||||
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
||||||
sperreFuer,
|
sperreFuer,
|
||||||
level: zeitLevel,
|
level: zeitLevel,
|
||||||
tempUnlockBeforeRequired: _isChastityMode ? document.getElementById('iTempUnlockBefore').checked : null,
|
tempUnlockRequired: _isChastityMode ? document.getElementById('iTempUnlockRequired').checked : null,
|
||||||
tempUnlockAfterRequired: _isChastityMode ? document.getElementById('iTempUnlockAfter').checked : null,
|
|
||||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||||
};
|
};
|
||||||
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
|
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
|
||||||
|
|||||||
@@ -679,6 +679,7 @@
|
|||||||
|
|
||||||
// ── Gruppe lists ──
|
// ── Gruppe lists ──
|
||||||
function renderGruppeList(containerId, gruppen) {
|
function renderGruppeList(containerId, gruppen) {
|
||||||
|
gruppen = gruppen.filter(g => g.availableIn !== 'CHASTITY_ONLY');
|
||||||
const ul = document.getElementById(containerId);
|
const ul = document.getElementById(containerId);
|
||||||
const section = ul.closest('[id^="section"]');
|
const section = ul.closest('[id^="section"]');
|
||||||
const selectAllWrap = section?.querySelector('.select-all-label');
|
const selectAllWrap = section?.querySelector('.select-all-label');
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,9 +256,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkbox-row" id="rowTestLock">
|
<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>
|
<label for="testLock">Test-Lock <span class="form-hint">(kein echter Lock, zum Ausprobieren)</span></label>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="error-msg" id="errorMsg"></div>
|
<div class="error-msg" id="errorMsg"></div>
|
||||||
@@ -515,6 +520,7 @@
|
|||||||
khInput.readOnly = true;
|
khInput.readOnly = true;
|
||||||
khInput.style.opacity = '0.6';
|
khInput.style.opacity = '0.6';
|
||||||
document.getElementById('rowTestLock').style.display = 'none';
|
document.getElementById('rowTestLock').style.display = 'none';
|
||||||
|
document.getElementById('rowSpeedFactor').style.display = 'none';
|
||||||
document.getElementById('rowDetailsVisible').style.display = '';
|
document.getElementById('rowDetailsVisible').style.display = '';
|
||||||
} else {
|
} else {
|
||||||
khInput.readOnly = false;
|
khInput.readOnly = false;
|
||||||
@@ -728,6 +734,15 @@
|
|||||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
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 ──
|
// ── Absenden ──
|
||||||
async function createSession() {
|
async function createSession() {
|
||||||
document.getElementById('errorMsg').style.display = 'none';
|
document.getElementById('errorMsg').style.display = 'none';
|
||||||
@@ -756,6 +771,7 @@
|
|||||||
const isFriendLockee = lockeeVal && lockeeVal !== myUserId;
|
const isFriendLockee = lockeeVal && lockeeVal !== myUserId;
|
||||||
const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5);
|
const unlockCodeLen = isFriendLockee ? null : (parseInt(document.getElementById('unlockCodeLines').value) || 5);
|
||||||
const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked;
|
const isTestLock = isFriendLockee ? false : document.getElementById('testLock').checked;
|
||||||
|
const speedFactor = isTestLock ? parseInt(document.getElementById('speedFactor').value) : 1;
|
||||||
|
|
||||||
let endpoint, body;
|
let endpoint, body;
|
||||||
|
|
||||||
@@ -769,6 +785,7 @@
|
|||||||
testLock: isTestLock,
|
testLock: isTestLock,
|
||||||
unlockCodeLength: unlockCodeLen,
|
unlockCodeLength: unlockCodeLen,
|
||||||
controllType: selectedLockControl,
|
controllType: selectedLockControl,
|
||||||
|
speedFactor: speedFactor,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// CardLock
|
// CardLock
|
||||||
@@ -798,6 +815,7 @@
|
|||||||
controllType: selectedLockControl,
|
controllType: selectedLockControl,
|
||||||
gameSetId: t.gameSetId || null,
|
gameSetId: t.gameSetId || null,
|
||||||
gameSpieldauerIdx: t.gameSpieldauerIdx ?? null,
|
gameSpieldauerIdx: t.gameSpieldauerIdx ?? null,
|
||||||
|
speedFactor: speedFactor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,22 +22,41 @@
|
|||||||
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;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.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;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.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;
|
||||||
|
}
|
||||||
|
#confirmModal { display:none; }
|
||||||
|
#confirmModal.open { display:flex; }
|
||||||
|
|
||||||
|
|
||||||
.level-display {
|
.level-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -50,16 +69,6 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lock-messages {
|
|
||||||
background: rgba(233,69,96,0.1);
|
|
||||||
border: 1px solid rgba(233,69,96,0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 1rem 1.1rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
.lock-messages p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.5; }
|
|
||||||
.lock-messages p:last-child { margin: 0; }
|
|
||||||
|
|
||||||
.init-box {
|
.init-box {
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
border: 1px solid var(--color-secondary);
|
border: 1px solid var(--color-secondary);
|
||||||
@@ -82,6 +91,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;
|
||||||
@@ -106,15 +129,13 @@
|
|||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#finisherBox {
|
#levelTrophy {
|
||||||
background: linear-gradient(135deg, rgba(233,69,96,0.15), rgba(155,89,182,0.12));
|
font-size: 3.5rem;
|
||||||
border: 1px solid rgba(233,69,96,0.4);
|
line-height: 72px;
|
||||||
border-radius: 14px;
|
width: 72px;
|
||||||
padding: 1.5rem;
|
height: 72px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
#finisherBox .trophy { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
|
||||||
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="app">
|
<body class="app">
|
||||||
@@ -125,10 +146,23 @@
|
|||||||
<!-- Level-Anzeige -->
|
<!-- Level-Anzeige -->
|
||||||
<div class="level-display" id="levelDisplay" style="display:none;">
|
<div class="level-display" id="levelDisplay" style="display:none;">
|
||||||
<img id="levelImg" src="" alt="Level">
|
<img id="levelImg" src="" alt="Level">
|
||||||
|
<span id="levelTrophy" style="display:none;">🏆</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
<!-- Toy-Auswahl vor Spielstart -->
|
||||||
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
|
<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;">
|
||||||
@@ -143,32 +177,68 @@
|
|||||||
<!-- 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">🔓 Zeitstrafe verbüßt</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 class="game-timer"></div>
|
||||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
<div class="game-btn-row">
|
||||||
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
|
<button class="btn-primary" id="btnReleaseOk" style="width:100%;height:100%;">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Temporäre Öffnung -->
|
||||||
|
<div id="tempOpeningBox" class="game-card" style="display:none;">
|
||||||
|
<!-- Phase 1: Öffnung aktiv -->
|
||||||
|
<div id="tempPhase1">
|
||||||
|
<div class="game-label" style="text-align:center;">🔓 Entsperrcode</div>
|
||||||
|
<div class="game-text" id="tempOpeningCode"
|
||||||
|
style="display:flex;align-items:center;justify-content:center;
|
||||||
|
font-size:1.8rem;font-weight:700;letter-spacing:0.18em;
|
||||||
|
color:var(--color-primary);white-space:normal;"></div>
|
||||||
|
<div class="game-timer" id="tempOpeningTimer" style="margin-top:0.75rem;"></div>
|
||||||
|
<div class="game-btn-row">
|
||||||
|
<button class="btn-primary" style="width:100%;height:100%;" onclick="doEndTempOpening()">✓ Erledigt</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Phase 2: Neuer Code zum Schließen -->
|
||||||
|
<div id="tempPhase2" style="display:none;">
|
||||||
|
<div class="game-label" style="text-align:center;">🔒 Lock schließen</div>
|
||||||
|
<div class="game-text" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.75rem;white-space:normal;">
|
||||||
|
<span style="font-size:0.9rem;color:var(--color-muted);text-align:center;">Nutze den Entsperrcode um den Schlüssel wieder zu verschließen.</span>
|
||||||
|
<div id="tempNewCode" style="font-size:1.8rem;font-weight:700;letter-spacing:0.18em;color:var(--color-primary);text-align:center;"></div>
|
||||||
|
<div id="tempScrambleCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);font-family:monospace;text-align:center;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="game-timer"></div>
|
||||||
|
<div class="game-btn-row">
|
||||||
|
<button class="btn-primary" id="tempPhase2Btn" style="width:100%;height:100%;" onclick="startTempScramble()">OK</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Finisher -->
|
<!-- Finisher -->
|
||||||
<div id="finisherBox" style="display:none;">
|
<div id="finisherBox" class="game-card" style="display:none;">
|
||||||
<div class="trophy">🏆</div>
|
<div class="game-label" id="finisherLabel"></div>
|
||||||
<h2>Level 6 erreicht!</h2>
|
<div class="game-text" id="finisherText"></div>
|
||||||
<div class="game-label" id="finisherTitle"></div>
|
<div class="game-timer" id="finisherTimer"></div>
|
||||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
<div class="game-btn-row">
|
||||||
<button class="btn-primary" onclick="completeGame()" style="margin-top:1.25rem;">✓ Spiel beenden</button>
|
<button class="btn-primary" id="finisherBtn" style="width:100%;height:100%;"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug -->
|
||||||
|
<div style="margin-top:1.5rem;">
|
||||||
|
<button onclick="debugExit()" style="width:100%;padding:0.45rem;border-radius:8px;border:1px dashed #666;background:transparent;color:#666;font-size:0.78rem;cursor:pointer;">🐛 Debug exit</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -181,23 +251,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" id="confirmModal">
|
||||||
|
<div class="modal" style="max-width:420px;">
|
||||||
|
<h2>Bestätigung</h2>
|
||||||
|
<p id="confirmModalText" style="color:var(--color-text);margin-bottom:1.25rem;line-height:1.5;"></p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" id="confirmModalCancel">Nein</button>
|
||||||
|
<button class="btn-save" id="confirmModalOk" style="background:var(--color-danger,#e74c3c);">Ja, abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/js/icons.js"></script>
|
<script src="/js/icons.js"></script>
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
<script>
|
<script>
|
||||||
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');
|
||||||
let _state = null;
|
const freshStart = params.get('fresh') === '1';
|
||||||
let _timerInt = null;
|
let _resolvedGameSetId = autoGameSetId;
|
||||||
let _pendingIsLock = false;
|
let _state = null;
|
||||||
let _pendingHasDuration = false;
|
let _timerInt = null;
|
||||||
|
let _gameAction = null; // 'queue-start' | 'queue-done' | 'active-running' | 'active-done'
|
||||||
|
let _tempOpeningTimerInt = null;
|
||||||
|
let _tempScrambleTimer = null;
|
||||||
|
let _tempScrambleCd = null;
|
||||||
|
let _tempOpeningFromQueue = false; // true only when opening was triggered from a queued Zeitstrafe (doQueueStart)
|
||||||
|
|
||||||
function goBack() {
|
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 +293,142 @@
|
|||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/lock-game/state');
|
if (!freshStart) {
|
||||||
if (r.status === 404) {
|
const r = await fetch('/lock-game/state');
|
||||||
if (autoGameSetId) {
|
if (r.ok) {
|
||||||
await autoStartGame(autoGameSetId);
|
_state = await r.json();
|
||||||
} else {
|
hide('loadingHint');
|
||||||
await loadGroups();
|
await runGameLoop();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
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();
|
||||||
}
|
}
|
||||||
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
|
||||||
_state = await r.json();
|
|
||||||
hide('loadingHint');
|
|
||||||
await runGameLoop();
|
|
||||||
} 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('Initialisierung fehlgeschlagen');
|
if (!r.ok) throw new Error('Fehler beim Laden der Toys');
|
||||||
const stateR = await fetch('/lock-game/state');
|
const toys = await r.json();
|
||||||
_state = await stateR.json();
|
|
||||||
hide('loadingHint');
|
hide('loadingHint');
|
||||||
await runGameLoop();
|
|
||||||
|
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) {
|
} catch (e) {
|
||||||
showError(e.message);
|
showError(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleToyConfirm() {
|
||||||
|
const excludedToyIds = [];
|
||||||
|
document.querySelectorAll('#toyToggleList input[type="checkbox"]').forEach(cb => {
|
||||||
|
if (!cb.checked) excludedToyIds.push(cb.value);
|
||||||
|
});
|
||||||
|
hide('toyBox');
|
||||||
|
show('loadingHint');
|
||||||
|
try {
|
||||||
|
await startWithExcludedToys(_resolvedGameSetId, excludedToyIds);
|
||||||
|
} catch (e) {
|
||||||
|
showError(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startWithExcludedToys(gameSetId, excludedToyIds) {
|
||||||
|
const params = new URLSearchParams({ aufgabenGruppeId: gameSetId });
|
||||||
|
if (lockId) params.append('lockId', lockId);
|
||||||
|
excludedToyIds.forEach(id => params.append('excludedToyIds', id));
|
||||||
|
const r = await fetch('/lock-game/init?' + params.toString(), { method: 'POST' });
|
||||||
|
|
||||||
|
if (r.status === 422) {
|
||||||
|
const body = await r.json().catch(() => ({}));
|
||||||
|
await showValidationError(body.error || 'Das Aufgaben-Set ist nicht vollständig.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||||
|
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
|
hide('loadingHint');
|
||||||
|
|
||||||
|
// Remove ?fresh=1 from URL so F5 resumes instead of restarting.
|
||||||
|
const cleanParams = new URLSearchParams(location.search);
|
||||||
|
cleanParams.delete('fresh');
|
||||||
|
const cleanSearch = cleanParams.toString();
|
||||||
|
history.replaceState(null, '', location.pathname + (cleanSearch ? '?' + cleanSearch : ''));
|
||||||
|
|
||||||
|
await runGameLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showValidationError(msg) {
|
||||||
|
hide('loadingHint');
|
||||||
|
showError('Das Spiel kann nicht gestartet werden: ' + msg
|
||||||
|
+ ' Du wirst in Kürze zurückgeleitet und erhältst eine Strafe.');
|
||||||
|
|
||||||
|
if (lockId) {
|
||||||
|
fetch('/lock-game/penalty?lockId=' + lockId, { method: 'POST' }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
let secs = 5;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const box = document.getElementById('errorBox');
|
||||||
|
if (box) box.textContent = 'Das Spiel kann nicht gestartet werden: ' + msg
|
||||||
|
+ ` Rückleitung in ${--secs}s…`;
|
||||||
|
if (secs <= 0) { clearInterval(interval); goBack(); }
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadGroups() {
|
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 +458,53 @@
|
|||||||
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 ─────────────────────────────────────────────────────────────
|
// ── Benötigt-Checkboxen ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ── Game Loop ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function setGameCard(label, text, action, btnLabel) {
|
||||||
|
document.getElementById('gameLabel').textContent = label;
|
||||||
|
document.getElementById('gameText').textContent = text;
|
||||||
|
const timerEl = document.getElementById('gameTimer');
|
||||||
|
timerEl.classList.remove('active', 'urgent');
|
||||||
|
timerEl.textContent = '';
|
||||||
|
_gameAction = action;
|
||||||
|
document.getElementById('gameBtn').textContent = btnLabel;
|
||||||
|
}
|
||||||
|
|
||||||
async function runGameLoop() {
|
async function runGameLoop() {
|
||||||
hide('queueBox');
|
|
||||||
hide('activeBox');
|
|
||||||
hide('finisherBox');
|
|
||||||
clearTimer();
|
clearTimer();
|
||||||
|
|
||||||
if (_state.level >= 6) {
|
if (_state.tempOpeningTime) {
|
||||||
await showFinisher();
|
hide('finisherBox');
|
||||||
|
showTempOpeningDialog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_state.finisher) {
|
||||||
|
hide('tempOpeningBox');
|
||||||
|
showFinisherUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal game states all use gameCard — show it once here so the card
|
||||||
|
// frame stays stable across transitions (queue ↔ active ↔ active-done).
|
||||||
|
show('gameBox');
|
||||||
|
show('gameCard');
|
||||||
|
hide('tempOpeningBox');
|
||||||
|
hide('finisherBox');
|
||||||
|
|
||||||
renderLevelBar(_state.level);
|
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,115 +518,357 @@
|
|||||||
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;
|
const btnLabel = sperre.tempUnlockRequired ? '⏱ Weiter zur temporären Öffnung' : '✓ Erledigt';
|
||||||
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
|
setGameCard('🔒 Neue Sperre', sperre.text || sperre.kurzText || '', 'queue-start', btnLabel);
|
||||||
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('queueBox');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showActiveTask(text, endIso) {
|
function showActiveTask(text, endIso) {
|
||||||
document.getElementById('activeText').textContent = text;
|
const timerEl = document.getElementById('gameTimer');
|
||||||
show('gameBox');
|
timerEl.classList.remove('active', 'urgent');
|
||||||
show('activeBox');
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_gameAction = 'active-done';
|
||||||
|
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGameBtn() {
|
||||||
|
switch (_gameAction) {
|
||||||
|
case 'queue-start': doQueueStart(); break;
|
||||||
|
case 'queue-done': doQueueDone(); break;
|
||||||
|
case 'active-running': openConfirmModal('Aufgabe wirklich abbrechen?', () => doCancelCountdown()); break;
|
||||||
|
case 'active-done': doErledigt(); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOk() {
|
async function doQueueStart() {
|
||||||
// Locks prüfen (nach jeder Aktion)
|
try {
|
||||||
await checkAndShowLocks();
|
const wasLock = !!_state.lockInQueue;
|
||||||
|
let tempUnlockRequired = false;
|
||||||
|
if (wasLock) {
|
||||||
|
try { tempUnlockRequired = JSON.parse(_state.lockInQueue).tempUnlockRequired === true; } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (_pendingIsLock || _pendingHasDuration) {
|
if (wasLock && tempUnlockRequired) {
|
||||||
// Task/Sperre aktivieren und Timer starten
|
// Erst Öffnung starten, Zeitstrafe wird nach der Öffnung angewendet
|
||||||
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
|
const r = await fetch('/lock-game/start-temp-opening', { method: 'POST' });
|
||||||
if (!r.ok) { showError('Fehler'); return; }
|
if (!r.ok) { showError('Fehler beim Starten der Öffnung'); return; }
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
|
_tempOpeningFromQueue = true;
|
||||||
|
showTempOpeningDialog();
|
||||||
|
} else {
|
||||||
|
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||||||
|
if (!r.ok) { showError('Fehler beim Starten'); return; }
|
||||||
|
if (wasLock) {
|
||||||
|
const nextR = await fetch('/lock-game/abandon-task', { method: 'POST' });
|
||||||
|
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
|
||||||
|
}
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
|
await runGameLoop();
|
||||||
|
}
|
||||||
|
} catch (e) { showError(e.message || 'Fehler (Starten)'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doQueueDone() {
|
||||||
|
try {
|
||||||
|
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||||
|
if (lockR.ok) {
|
||||||
|
const sperren = await lockR.json();
|
||||||
|
for (const sperre of (sperren || [])) {
|
||||||
|
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||||||
|
if (!applyR.ok) { showError('Fehler beim Anwenden'); return; }
|
||||||
|
const nextR = await fetch('/lock-game/next-task', { method: 'POST' });
|
||||||
|
if (!nextR.ok) { showError('Fehler beim Ziehen'); return; }
|
||||||
const stateR = await fetch('/lock-game/state');
|
const stateR = await fetch('/lock-game/state');
|
||||||
_state = await stateR.json();
|
_state = await stateR.json();
|
||||||
} else {
|
await runGameLoop();
|
||||||
// Keine Dauer → sofort nächsten Task
|
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
|
||||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
|
||||||
const r = await fetch('/lock-game/state');
|
|
||||||
_state = await r.json();
|
|
||||||
}
|
|
||||||
await runGameLoop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleErledigt() {
|
async function doCancelCountdown() {
|
||||||
await checkAndShowLocks();
|
|
||||||
|
|
||||||
if (_state.level >= 6) {
|
|
||||||
await showFinisher();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
|
||||||
const r = await fetch('/lock-game/state');
|
|
||||||
_state = await r.json();
|
|
||||||
await runGameLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkAndShowLocks() {
|
|
||||||
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
|
||||||
if (!r.ok) return;
|
|
||||||
const texts = await r.json();
|
|
||||||
if (texts && texts.length > 0) {
|
|
||||||
const box = document.getElementById('lockMessages');
|
|
||||||
box.innerHTML = texts.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
|
|
||||||
show('lockMessages');
|
|
||||||
await new Promise(res => setTimeout(res, 2000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showFinisher() {
|
|
||||||
show('gameBox');
|
|
||||||
const r = await fetch('/lock-game/finisher');
|
|
||||||
if (!r.ok) {
|
|
||||||
document.getElementById('finisherTitle').textContent = '';
|
|
||||||
document.getElementById('finisherText').textContent = 'Glückwunsch – du hast Level 6 erreicht!';
|
|
||||||
} else {
|
|
||||||
const f = await r.json();
|
|
||||||
document.getElementById('finisherTitle').textContent = f.kurzText || '';
|
|
||||||
document.getElementById('finisherText').textContent = f.text || '';
|
|
||||||
}
|
|
||||||
show('finisherBox');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function renderLevelBar(level) {
|
|
||||||
const lvl = Math.min(Math.max(level, 1), 5);
|
|
||||||
document.getElementById('levelImg').src = `/img/lvl${lvl}.png`;
|
|
||||||
show('levelDisplay');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function startTimer(endDate, el) {
|
|
||||||
el.style.display = '';
|
|
||||||
clearTimer();
|
clearTimer();
|
||||||
_timerInt = setInterval(() => {
|
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||||
|
if (lockR.ok) {
|
||||||
|
const sperren = await lockR.json();
|
||||||
|
for (const sperre of (sperren || [])) {
|
||||||
|
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_gameAction = 'active-done';
|
||||||
|
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doErledigt() {
|
||||||
|
try {
|
||||||
|
const lockR = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||||
|
if (lockR.ok) {
|
||||||
|
const sperren = await lockR.json();
|
||||||
|
for (const sperre of (sperren || [])) {
|
||||||
|
if (sperre.releaseText) await waitForReleaseOk(sperre.releaseText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const r = await fetch('/lock-game/next-task', { method: 'POST' });
|
||||||
|
if (!r.ok) { showError('Fehler beim Ziehen'); return; }
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
|
await runGameLoop();
|
||||||
|
} catch (e) { showError(e.message || 'Fehler (Erledigt)'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTempOpeningDialog() {
|
||||||
|
show('gameBox');
|
||||||
|
hide('gameCard');
|
||||||
|
hide('lockReleaseBox');
|
||||||
|
hide('finisherBox');
|
||||||
|
|
||||||
|
document.getElementById('tempOpeningCode').textContent = _state.tempOpeningCode || '';
|
||||||
|
|
||||||
|
show('tempPhase1');
|
||||||
|
hide('tempPhase2');
|
||||||
|
|
||||||
|
const timerEl = document.getElementById('tempOpeningTimer');
|
||||||
|
timerEl.textContent = '';
|
||||||
|
timerEl.classList.remove('active', 'urgent');
|
||||||
|
if (_state.tempOpeningTime && _state.tempOpeningDuration) {
|
||||||
|
const endTime = new Date(new Date(_state.tempOpeningTime).getTime() + _state.tempOpeningDuration * 60000);
|
||||||
|
if (endTime > Date.now()) startTempOpeningTimer(endTime, timerEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
show('tempOpeningBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTempOpeningTimer(endDate, el) {
|
||||||
|
if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
|
||||||
|
el.classList.add('active');
|
||||||
|
function tick() {
|
||||||
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||||
const m = String(Math.floor(diff / 60)).padStart(2, '0');
|
const m = String(Math.floor(diff / 60)).padStart(2, '0');
|
||||||
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) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
_tempOpeningTimerInt = setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doEndTempOpening() {
|
||||||
|
try {
|
||||||
|
if (_tempOpeningTimerInt) { clearInterval(_tempOpeningTimerInt); _tempOpeningTimerInt = null; }
|
||||||
|
|
||||||
|
// Only apply the queued Zeitstrafe when the opening was explicitly started from one (Path 1).
|
||||||
|
// When triggered by a background lock expiry via checkLocks() (Path 2), lockInQueue may be set
|
||||||
|
// to the *next* randomly-picked item — applying it would incorrectly create another background
|
||||||
|
// lock with tempUnlockRequired and produce an infinite opening loop.
|
||||||
|
const fromQueue = _tempOpeningFromQueue;
|
||||||
|
_tempOpeningFromQueue = false;
|
||||||
|
const hasLockInQueue = fromQueue && !!_state.lockInQueue;
|
||||||
|
if (hasLockInQueue) {
|
||||||
|
const applyR = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||||||
|
if (!applyR.ok) { showError('Fehler beim Anwenden der Zeitstrafe'); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const endR = await fetch('/lock-game/end-temp-opening', { method: 'POST' });
|
||||||
|
if (!endR.ok) { showError('Fehler beim Beenden der Öffnung'); return; }
|
||||||
|
const endData = await endR.json();
|
||||||
|
|
||||||
|
const newCode = endData.newUnlockCode;
|
||||||
|
if (newCode) {
|
||||||
|
document.getElementById('tempNewCode').textContent = newCode;
|
||||||
|
document.getElementById('tempScrambleCountdown').style.display = 'none';
|
||||||
|
document.getElementById('tempPhase2Btn').textContent = 'OK';
|
||||||
|
document.getElementById('tempPhase2Btn').onclick = startTempScramble;
|
||||||
|
hide('tempPhase1');
|
||||||
|
show('tempPhase2');
|
||||||
|
} else {
|
||||||
|
await finishTempOpening();
|
||||||
|
}
|
||||||
|
} catch (e) { showError(e.message || 'Fehler beim Abschluss der temporären Öffnung'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finishTempOpening() {
|
||||||
|
hide('tempOpeningBox');
|
||||||
|
await fetch('/lock-game/abandon-task', { method: 'POST' });
|
||||||
|
const stateR = await fetch('/lock-game/state');
|
||||||
|
_state = await stateR.json();
|
||||||
|
await runGameLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTempScramble() {
|
||||||
|
const codeEl = document.getElementById('tempNewCode');
|
||||||
|
const cdEl = document.getElementById('tempScrambleCountdown');
|
||||||
|
const btnEl = document.getElementById('tempPhase2Btn');
|
||||||
|
const len = codeEl.textContent.length;
|
||||||
|
const DURATION = 3 * 60;
|
||||||
|
let remaining = DURATION;
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
function randomCode() {
|
||||||
|
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
|
||||||
|
}
|
||||||
|
async function finish() {
|
||||||
|
stopped = true;
|
||||||
|
clearInterval(_tempScrambleTimer); _tempScrambleTimer = null;
|
||||||
|
clearInterval(_tempScrambleCd); _tempScrambleCd = null;
|
||||||
|
await finishTempOpening();
|
||||||
|
}
|
||||||
|
|
||||||
|
cdEl.style.display = '';
|
||||||
|
btnEl.textContent = 'Abbrechen';
|
||||||
|
btnEl.onclick = finish;
|
||||||
|
|
||||||
|
function updateCd() {
|
||||||
|
const m = Math.floor(remaining / 60);
|
||||||
|
const s = remaining % 60;
|
||||||
|
cdEl.textContent = `${m}:${String(s).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
updateCd();
|
||||||
|
|
||||||
|
_tempScrambleTimer = setInterval(() => { if (!stopped) codeEl.textContent = randomCode(); }, 1000);
|
||||||
|
_tempScrambleCd = setInterval(() => {
|
||||||
|
if (stopped) return;
|
||||||
|
if (--remaining <= 0) finish();
|
||||||
|
else updateCd();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showFinisherUI() {
|
||||||
|
show('gameBox');
|
||||||
|
hide('gameCard');
|
||||||
|
hide('lockReleaseBox');
|
||||||
|
hide('tempOpeningBox');
|
||||||
|
|
||||||
|
renderLevelBar(6);
|
||||||
|
|
||||||
|
let finisher = {};
|
||||||
|
try { finisher = JSON.parse(_state.finisher); } catch (_) {}
|
||||||
|
document.getElementById('finisherLabel').textContent = finisher.kurzText || '';
|
||||||
|
document.getElementById('finisherText').textContent = finisher.text || '';
|
||||||
|
|
||||||
|
const timerEl = document.getElementById('finisherTimer');
|
||||||
|
const btnEl = document.getElementById('finisherBtn');
|
||||||
|
timerEl.classList.remove('active', 'urgent');
|
||||||
|
timerEl.textContent = '';
|
||||||
|
|
||||||
|
if (_state.finisherStartedAt) {
|
||||||
|
timerEl.classList.add('active');
|
||||||
|
startElapsedTimer(new Date(_state.finisherStartedAt));
|
||||||
|
btnEl.textContent = '✓ Erledigt';
|
||||||
|
btnEl.onclick = doEndFinisher;
|
||||||
|
} else {
|
||||||
|
btnEl.textContent = '▶ Starten';
|
||||||
|
btnEl.onclick = doStartFinisher;
|
||||||
|
}
|
||||||
|
show('finisherBox');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doStartFinisher() {
|
||||||
|
await fetch('/lock-game/start-finisher', { method: 'POST' });
|
||||||
|
const r = await fetch('/lock-game/state');
|
||||||
|
_state = await r.json();
|
||||||
|
const timerEl = document.getElementById('finisherTimer');
|
||||||
|
timerEl.classList.add('active');
|
||||||
|
startElapsedTimer(new Date(_state.finisherStartedAt));
|
||||||
|
const btnEl = document.getElementById('finisherBtn');
|
||||||
|
btnEl.textContent = '✓ Erledigt';
|
||||||
|
btnEl.onclick = doEndFinisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doEndFinisher() {
|
||||||
|
clearTimer();
|
||||||
|
await fetch('/lock-game/end-finisher', { method: 'POST' });
|
||||||
|
const url = '/lock-game/complete' + (lockId ? '?lockId=' + lockId : '');
|
||||||
|
await fetch(url, { method: 'POST' });
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startElapsedTimer(startDate) {
|
||||||
|
clearTimer();
|
||||||
|
const el = document.getElementById('finisherTimer');
|
||||||
|
function tick() {
|
||||||
|
const diff = Math.floor((Date.now() - startDate) / 1000);
|
||||||
|
const m = String(Math.floor(diff / 60)).padStart(2, '0');
|
||||||
|
const s = String(diff % 60).padStart(2, '0');
|
||||||
|
el.textContent = m + ':' + s;
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
_timerInt = setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForReleaseOk(text) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
hide('gameCard');
|
||||||
|
document.getElementById('releaseText').textContent = text || '';
|
||||||
|
document.getElementById('btnReleaseOk').onclick = () => {
|
||||||
|
hide('lockReleaseBox');
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
show('lockReleaseBox');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function renderLevelBar(level) {
|
||||||
|
const isFinisher = level >= 6;
|
||||||
|
document.getElementById('levelImg').style.display = isFinisher ? 'none' : '';
|
||||||
|
document.getElementById('levelTrophy').style.display = isFinisher ? '' : 'none';
|
||||||
|
if (!isFinisher) {
|
||||||
|
document.getElementById('levelImg').src = `/img/lvl${Math.min(Math.max(level, 1), 5)}.png`;
|
||||||
|
}
|
||||||
|
show('levelDisplay');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function playSound(src) {
|
||||||
|
try { new Audio(src).play().catch(() => {}); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTimer(endDate, el) {
|
||||||
|
el.classList.add('active');
|
||||||
|
clearTimer();
|
||||||
|
function tick() {
|
||||||
|
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||||
|
const m = String(Math.floor(diff / 60)).padStart(2, '0');
|
||||||
|
const s = String(diff % 60).padStart(2, '0');
|
||||||
|
el.textContent = m + ':' + s;
|
||||||
|
el.classList.toggle('urgent', diff < 30);
|
||||||
|
if (diff === 0) {
|
||||||
|
clearTimer();
|
||||||
|
playSound('/audio/alarm.mp3');
|
||||||
|
_gameAction = 'active-done';
|
||||||
|
document.getElementById('gameBtn').textContent = '✓ Erledigt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
_timerInt = setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
function clearTimer() {
|
function clearTimer() {
|
||||||
if (_timerInt) { clearInterval(_timerInt); _timerInt = null; }
|
if (_timerInt) { clearInterval(_timerInt); _timerInt = null; }
|
||||||
}
|
}
|
||||||
@@ -442,6 +885,25 @@
|
|||||||
box.style.display = '';
|
box.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Confirm Modal ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const _confirmModal = document.getElementById('confirmModal');
|
||||||
|
document.getElementById('confirmModalCancel').addEventListener('click', closeConfirmModal);
|
||||||
|
document.getElementById('confirmModalOk').addEventListener('click', closeConfirmModal);
|
||||||
|
_confirmModal.addEventListener('click', e => { if (e.target === _confirmModal) closeConfirmModal(); });
|
||||||
|
document.addEventListener('keydown', e => { if (e.key === 'Escape' && _confirmModal.classList.contains('open')) closeConfirmModal(); });
|
||||||
|
|
||||||
|
function closeConfirmModal() { _confirmModal.classList.remove('open'); }
|
||||||
|
|
||||||
|
function openConfirmModal(text, onConfirm) {
|
||||||
|
document.getElementById('confirmModalText').textContent = text;
|
||||||
|
const okBtn = document.getElementById('confirmModalOk');
|
||||||
|
const newOk = okBtn.cloneNode(true);
|
||||||
|
okBtn.parentNode.replaceChild(newOk, okBtn);
|
||||||
|
newOk.addEventListener('click', () => { closeConfirmModal(); onConfirm(); });
|
||||||
|
_confirmModal.classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
boot();
|
boot();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Binary file not shown.
@@ -1,12 +1,3 @@
|
|||||||
Umsetzung des Spiels:
|
|
||||||
Der Lockee hat eine Stunde Zeit das Spiel zu starten, dies geschieht per Knopfdruck
|
|
||||||
Wenn er dies nicht schafft -> bei Keyholder, benachrichtige Keyholder und lass sie/ihn entscheiden, ansonsten freeze wie bei freeze card
|
|
||||||
Übernimm die Logik des Spiels aus dem BDSM Game.
|
|
||||||
Falls eine Zeitstrafe eine temporäre Öffnung vor oder nach der Aufgabe benötigt, öffne das Lock für 5 Minuten. Überzogene Zeit wird addiert und am Ende des Locks gefreezed
|
|
||||||
Selbiges gilt, falls der finisher eine temporäre Öffnung danach erfoldert
|
|
||||||
Benötigt der Finisher eine Öffnung davor, verwende die Logik der Cum Card, und addiere diese Zeit auf die möglicherweise schon vorhandenen Freeze Zeit am Ende des Locks
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen,
|
wenn ich dates erfasse kann ich diese auch zu einer Verantstaltung machen,
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import jakarta.persistence.AttributeConverter;
|
import jakarta.persistence.AttributeConverter;
|
||||||
import jakarta.persistence.Converter;
|
import jakarta.persistence.Converter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Converter
|
@Converter
|
||||||
public class StringListConverter implements AttributeConverter<List<String>, String> {
|
public class StringListConverter implements AttributeConverter<List<String>, String> {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(StringListConverter.class);
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -18,6 +21,7 @@ public class StringListConverter implements AttributeConverter<List<String>, Str
|
|||||||
try {
|
try {
|
||||||
return mapper.writeValueAsString(list);
|
return mapper.writeValueAsString(list);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Fehler beim Serialisieren der String-Liste in DB-Spalte", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +36,7 @@ public class StringListConverter implements AttributeConverter<List<String>, Str
|
|||||||
}
|
}
|
||||||
return mapper.readValue(json, new TypeReference<>() {});
|
return mapper.readValue(json, new TypeReference<>() {});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn("Fehler beim Deserialisieren der String-Liste aus DB-Spalte: {}", json, e);
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package de.oaa.xxx.dating;
|
|||||||
import de.oaa.xxx.social.SseService;
|
import de.oaa.xxx.social.SseService;
|
||||||
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
import de.oaa.xxx.subscription.SubscriptionLimitService;
|
||||||
import de.oaa.xxx.user.Geschlecht;
|
import de.oaa.xxx.user.Geschlecht;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import de.oaa.xxx.user.Neigung;
|
import de.oaa.xxx.user.Neigung;
|
||||||
import de.oaa.xxx.user.UserEntity;
|
import de.oaa.xxx.user.UserEntity;
|
||||||
import de.oaa.xxx.user.UserRepository;
|
import de.oaa.xxx.user.UserRepository;
|
||||||
@@ -24,6 +26,8 @@ import java.util.stream.Collectors;
|
|||||||
@RequestMapping("/dating")
|
@RequestMapping("/dating")
|
||||||
public class DatingController {
|
public class DatingController {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(DatingController.class);
|
||||||
|
|
||||||
private static final Set<VorliebeBewertung> POSITIVE_RATINGS = Set.of(
|
private static final Set<VorliebeBewertung> POSITIVE_RATINGS = Set.of(
|
||||||
VorliebeBewertung.MAG_ICH, VorliebeBewertung.UNBEDINGT, VorliebeBewertung.WILL_AUSPROBIEREN);
|
VorliebeBewertung.MAG_ICH, VorliebeBewertung.UNBEDINGT, VorliebeBewertung.WILL_AUSPROBIEREN);
|
||||||
|
|
||||||
@@ -254,7 +258,10 @@ public class DatingController {
|
|||||||
return values.stream()
|
return values.stream()
|
||||||
.map(s -> {
|
.map(s -> {
|
||||||
try { return Enum.valueOf(enumClass, s); }
|
try { return Enum.valueOf(enumClass, s); }
|
||||||
catch (IllegalArgumentException e) { return null; }
|
catch (IllegalArgumentException e) {
|
||||||
|
LOGGER.warn("Ungültiger Enum-Wert in Dating: {}", s);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user