Weiter am Ingame Chastity Game gearbeitet
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:
215
bin/main/db/migration/V3__migrate_chastity_game_set.sql
Normal file
215
bin/main/db/migration/V3__migrate_chastity_game_set.sql
Normal file
@@ -0,0 +1,215 @@
|
||||
-- Migration: Inhalte aus chastity_game_set in die normalen Aufgabengruppen-Tabellen übernehmen.
|
||||
--
|
||||
-- Jedes chastity_game_set eines Users wird zu einer privaten CHASTITY_ONLY-Aufgabengruppe:
|
||||
-- aufgaben (JSON) → aufgabe + aufgabe_benoetigt_passiv
|
||||
-- zeitstrafen (JSON) → sperre + sperre_sperre_fuer
|
||||
-- finisher (JSON) → finisher
|
||||
--
|
||||
-- Die Prozedur prüft zuerst, ob chastity_game_set existiert – auf leeren Datenbanken
|
||||
-- ist sie dadurch ein No-op.
|
||||
|
||||
DROP PROCEDURE IF EXISTS proc_migrate_chastity_game_set;
|
||||
|
||||
CREATE PROCEDURE proc_migrate_chastity_game_set()
|
||||
BEGIN
|
||||
DECLARE v_set_id VARCHAR(36);
|
||||
DECLARE v_owner_id VARCHAR(36);
|
||||
DECLARE v_set_name VARCHAR(255);
|
||||
DECLARE v_user_name VARCHAR(255);
|
||||
DECLARE v_aufgaben_json TEXT;
|
||||
DECLARE v_zeitstr_json TEXT;
|
||||
DECLARE v_finisher_json TEXT;
|
||||
|
||||
DECLARE v_gruppe_id VARCHAR(36);
|
||||
DECLARE v_aufgabe_id VARCHAR(36);
|
||||
DECLARE v_sperre_id VARCHAR(36);
|
||||
DECLARE v_finisher_id VARCHAR(36);
|
||||
|
||||
DECLARE v_outer_count INT;
|
||||
DECLARE v_inner_count INT;
|
||||
DECLARE i INT;
|
||||
DECLARE j INT;
|
||||
|
||||
DECLARE tbl_exists INT DEFAULT 0;
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT id, owner_id, name, aufgaben, zeitstrafen, finisher
|
||||
FROM chastity_game_set;
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
-- Add columns that may not yet exist (Hibernate ddl-auto runs after Flyway)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'aufgabe' AND COLUMN_NAME = 'level') THEN
|
||||
ALTER TABLE aufgabe ADD COLUMN level INT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sperre' AND COLUMN_NAME = 'level') THEN
|
||||
ALTER TABLE sperre ADD COLUMN level INT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sperre' AND COLUMN_NAME = 'temp_unlock_before_required') THEN
|
||||
ALTER TABLE sperre ADD COLUMN temp_unlock_before_required TINYINT(1) NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sperre' AND COLUMN_NAME = 'temp_unlock_after_required') THEN
|
||||
ALTER TABLE sperre ADD COLUMN temp_unlock_after_required TINYINT(1) NULL;
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO tbl_exists
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'chastity_game_set';
|
||||
|
||||
IF tbl_exists > 0 THEN
|
||||
|
||||
OPEN cur;
|
||||
|
||||
set_loop: LOOP
|
||||
FETCH cur INTO v_set_id, v_owner_id, v_set_name,
|
||||
v_aufgaben_json, v_zeitstr_json, v_finisher_json;
|
||||
IF done THEN LEAVE set_loop; END IF;
|
||||
|
||||
SELECT name INTO v_user_name
|
||||
FROM user
|
||||
WHERE user_id = v_owner_id
|
||||
LIMIT 1;
|
||||
|
||||
SET v_gruppe_id = UUID();
|
||||
|
||||
-- ── Aufgabengruppe anlegen ───────────────────────────────────────
|
||||
INSERT INTO aufgaben_gruppe
|
||||
(gruppen_id, name, beschreibung, user_id, private_gruppe, bild, von, available_in)
|
||||
VALUES
|
||||
(v_gruppe_id,
|
||||
v_set_name,
|
||||
'Importiert aus Chastity Game Set',
|
||||
v_owner_id,
|
||||
TRUE,
|
||||
NULL,
|
||||
v_user_name,
|
||||
'CHASTITY_ONLY');
|
||||
|
||||
-- ── Aufgaben ────────────────────────────────────────────────────
|
||||
-- GameSetAufgabe.minutes → sekunden_von / sekunden_bis (× 60)
|
||||
-- GameSetAufgabe.benoetigt → aufgabe_benoetigt_passiv
|
||||
SET v_outer_count = IFNULL(JSON_LENGTH(v_aufgaben_json), 0);
|
||||
SET i = 0;
|
||||
WHILE i < v_outer_count DO
|
||||
SET v_aufgabe_id = UUID();
|
||||
|
||||
INSERT INTO aufgabe
|
||||
(aufgabe_id, kurz_text, text, level, sekunden_von, sekunden_bis, gruppe_id)
|
||||
VALUES (
|
||||
v_aufgabe_id,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].title'))),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].description'))),
|
||||
CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].level'))), 'null') AS SIGNED),
|
||||
CASE
|
||||
WHEN JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].minutes')) IS NOT NULL
|
||||
THEN CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].minutes'))), 'null') AS SIGNED) * 60
|
||||
ELSE NULL
|
||||
END,
|
||||
CASE
|
||||
WHEN JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].minutes')) IS NOT NULL
|
||||
THEN CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].minutes'))), 'null') AS SIGNED) * 60
|
||||
ELSE NULL
|
||||
END,
|
||||
v_gruppe_id
|
||||
);
|
||||
|
||||
SET v_inner_count = IFNULL(
|
||||
JSON_LENGTH(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].benoetigt'))),
|
||||
0);
|
||||
SET j = 0;
|
||||
WHILE j < v_inner_count DO
|
||||
SET @wz = NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json,
|
||||
CONCAT('$[', i, '].benoetigt[', j, ']'))), 'null');
|
||||
IF @wz IS NOT NULL AND @wz != '' THEN
|
||||
INSERT IGNORE INTO aufgabe_benoetigt_passiv (aufgabe_id, werkzeug)
|
||||
VALUES (v_aufgabe_id, @wz);
|
||||
END IF;
|
||||
SET j = j + 1;
|
||||
END WHILE;
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
|
||||
-- ── Zeitstrafen → Sperren ────────────────────────────────────────
|
||||
-- GameSetZeitstrafe.minMinutes / maxMinutes → minuten_von / minuten_bis
|
||||
-- GameSetZeitstrafe.level / tempUnlock* → level / temp_unlock_before/after_required
|
||||
-- GameSetZeitstrafe.sperrt → sperre_sperre_fuer
|
||||
SET v_outer_count = IFNULL(JSON_LENGTH(v_zeitstr_json), 0);
|
||||
SET i = 0;
|
||||
WHILE i < v_outer_count DO
|
||||
SET v_sperre_id = UUID();
|
||||
|
||||
INSERT INTO sperre
|
||||
(sperre_id, kurz_text, text, release_text, minuten_von, minuten_bis,
|
||||
level, temp_unlock_before_required, temp_unlock_after_required, gruppe_id)
|
||||
VALUES (
|
||||
v_sperre_id,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].title'))),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].description'))),
|
||||
IF(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].releaseText')) IS NOT NULL,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].releaseText'))),
|
||||
NULL),
|
||||
CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].minMinutes'))), 'null') AS SIGNED),
|
||||
CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].maxMinutes'))), 'null') AS SIGNED),
|
||||
CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].level'))), 'null') AS SIGNED),
|
||||
CASE JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].tempUnlockBeforeRequired')))
|
||||
WHEN 'true' THEN 1 WHEN 'false' THEN 0 ELSE NULL END,
|
||||
CASE JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].tempUnlockAfterRequired')))
|
||||
WHEN 'true' THEN 1 WHEN 'false' THEN 0 ELSE NULL END,
|
||||
v_gruppe_id
|
||||
);
|
||||
|
||||
SET v_inner_count = IFNULL(
|
||||
JSON_LENGTH(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].sperrt'))),
|
||||
0);
|
||||
SET j = 0;
|
||||
WHILE j < v_inner_count DO
|
||||
SET @wz = NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json,
|
||||
CONCAT('$[', i, '].sperrt[', j, ']'))), 'null');
|
||||
IF @wz IS NOT NULL AND @wz != '' THEN
|
||||
INSERT IGNORE INTO sperre_sperre_fuer (sperre_id, werkzeug)
|
||||
VALUES (v_sperre_id, @wz);
|
||||
END IF;
|
||||
SET j = j + 1;
|
||||
END WHILE;
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
|
||||
-- ── Finisher ─────────────────────────────────────────────────────
|
||||
-- GameSetFinisher hat kein geschlecht und keine benoetigt-Listen
|
||||
-- tempUnlockBeforeRequired / tempUnlockAfterRequired haben kein Gegenstück in finisher
|
||||
SET v_outer_count = IFNULL(JSON_LENGTH(v_finisher_json), 0);
|
||||
SET i = 0;
|
||||
WHILE i < v_outer_count DO
|
||||
SET v_finisher_id = UUID();
|
||||
|
||||
INSERT INTO finisher
|
||||
(finisher_id, kurz_text, text, geschlecht, gruppe_id)
|
||||
VALUES (
|
||||
v_finisher_id,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_finisher_json, CONCAT('$[', i, '].title'))),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_finisher_json, CONCAT('$[', i, '].description'))),
|
||||
NULL,
|
||||
v_gruppe_id
|
||||
);
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
|
||||
END LOOP;
|
||||
|
||||
CLOSE cur;
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
CALL proc_migrate_chastity_game_set();
|
||||
|
||||
DROP PROCEDURE IF EXISTS proc_migrate_chastity_game_set;
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bin/main/de/oaa/xxx/games/chastity/common/GameState.class
Normal file
BIN
bin/main/de/oaa/xxx/games/chastity/common/GameState.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.
@@ -416,7 +416,7 @@
|
||||
|
||||
<label class="label-with-hint">
|
||||
<span>Beschreibung *</span>
|
||||
<button type="button" class="btn-hint" onclick="togglePlaceholderHint()" title="Platzhalter-Hilfe">i</button>
|
||||
<button type="button" id="iPlaceholderHintBtn" class="btn-hint" onclick="togglePlaceholderHint()" title="Platzhalter-Hilfe">i</button>
|
||||
</label>
|
||||
<div id="iPlaceholderHint" style="display:none;">
|
||||
<div class="placeholder-hint">
|
||||
@@ -451,15 +451,17 @@
|
||||
<div id="iLevelRow">
|
||||
<label for="iLevel">Level *</label>
|
||||
<input type="number" id="iLevel" min="1" max="5" placeholder="1–5">
|
||||
<label>Dauer (Sekunden)</label>
|
||||
<div class="modal-two-col">
|
||||
<div>
|
||||
<label for="iSekVon" style="margin-top:0;">Von</label>
|
||||
<input type="number" id="iSekVon" min="0" placeholder="z. B. 30">
|
||||
</div>
|
||||
<div>
|
||||
<label for="iSekBis" style="margin-top:0;">Bis</label>
|
||||
<input type="number" id="iSekBis" min="0" placeholder="z. B. 120">
|
||||
<div id="iSekRow">
|
||||
<label>Dauer (Sekunden)</label>
|
||||
<div class="modal-two-col">
|
||||
<div>
|
||||
<label for="iSekVon" style="margin-top:0;">Von</label>
|
||||
<input type="number" id="iSekVon" min="0" placeholder="z. B. 30">
|
||||
</div>
|
||||
<div>
|
||||
<label for="iSekBis" style="margin-top:0;">Bis</label>
|
||||
<input type="number" id="iSekBis" min="0" placeholder="z. B. 120">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -749,7 +751,7 @@
|
||||
<div class="gruppe-body" id="body-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${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('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
</div>
|
||||
@@ -1086,7 +1088,9 @@
|
||||
pubCb.checked = !g.privateGruppe;
|
||||
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
|
||||
document.getElementById('gPublicLabel').style.display = 'block';
|
||||
document.getElementById('gAvailableIn').value = g.availableIn || 'BDSM_ONLY';
|
||||
const avSel = document.getElementById('gAvailableIn');
|
||||
avSel.value = g.availableIn || 'BDSM_ONLY';
|
||||
avSel.disabled = true;
|
||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||
if (g.bild) {
|
||||
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
|
||||
@@ -1104,7 +1108,8 @@
|
||||
document.getElementById('gDesc').value = '';
|
||||
document.getElementById('gPublic').checked = false;
|
||||
document.getElementById('gPublicLabel').style.display = 'none';
|
||||
document.getElementById('gAvailableIn').value = 'BDSM_ONLY';
|
||||
document.getElementById('gAvailableIn').value = 'BDSM_ONLY';
|
||||
document.getElementById('gAvailableIn').disabled = false;
|
||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||
gruppeModal.classList.add('open');
|
||||
document.getElementById('gName').focus();
|
||||
@@ -1113,7 +1118,8 @@
|
||||
|
||||
function closeGruppeModal() {
|
||||
gruppeModal.classList.remove('open');
|
||||
document.getElementById('gPublic').disabled = false;
|
||||
document.getElementById('gPublic').disabled = false;
|
||||
document.getElementById('gAvailableIn').disabled = false;
|
||||
}
|
||||
|
||||
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
|
||||
@@ -1359,6 +1365,7 @@
|
||||
let currentItemGruppeId = null;
|
||||
let currentItemKind = null; // 'aufgabe' | 'strafe' | 'zeitstrafe'
|
||||
let currentItemEditId = null; // null = neu, sonst ID des zu bearbeitenden Items
|
||||
let _isChastityMode = false;
|
||||
|
||||
const ITEM_TITLES_NEW = { aufgabe: 'Aufgabe hinzufügen', strafe: 'Strafe hinzufügen', zeitstrafe: 'Zeitstrafe hinzufügen', finisher: 'Finisher hinzufügen' };
|
||||
const ITEM_TITLES_EDIT = { aufgabe: 'Aufgabe bearbeiten', strafe: 'Strafe bearbeiten', zeitstrafe: 'Zeitstrafe bearbeiten', finisher: 'Finisher bearbeiten' };
|
||||
@@ -1366,6 +1373,11 @@
|
||||
function _setupItemModal(kind) {
|
||||
const isZeit = kind === 'zeitstrafe';
|
||||
const isFinisher = kind === 'finisher';
|
||||
const isChastity = (_gruppeData[currentItemGruppeId]?.availableIn === 'CHASTITY_ONLY');
|
||||
_isChastityMode = isChastity;
|
||||
|
||||
// Placeholder hint button + text
|
||||
document.getElementById('iPlaceholderHintBtn').style.display = isChastity ? 'none' : '';
|
||||
document.querySelector('#iPlaceholderHint .placeholder-hint').innerHTML =
|
||||
isFinisher
|
||||
? 'In Texten können Platzhalter verwendet werden:<br>' +
|
||||
@@ -1374,15 +1386,38 @@
|
||||
: 'In Texten können Platzhalter verwendet werden:<br>' +
|
||||
'<code>{AKTIV}</code> – Name des aktiven Parts<br>' +
|
||||
'<code>{PASSIV}</code> – Name des passiven Parts';
|
||||
document.getElementById('iGeschlechtRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherAktivRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherPassivRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
|
||||
// Geschlecht: finisher only, and not in chastity
|
||||
document.getElementById('iGeschlechtRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Level row: aufgabe/strafe always; zeitstrafe only in chastity
|
||||
document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) || (isZeit && isChastity) ? 'block' : 'none';
|
||||
// Sekunden sub-section: not for chastity zeitstrafe
|
||||
document.getElementById('iSekRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
|
||||
// Aktiv Werkzeuge: aufgabe/strafe only
|
||||
document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.querySelector('#iWerkzeugAktivRow label').textContent = isChastity ? 'Benötigt' : 'Benötigt (aktiv)';
|
||||
['VAGINA', 'PENIS', 'UMSCHNALLDILDO'].forEach(v => {
|
||||
const lbl = document.querySelector(`#iWerkzeugAktiv input[value="${v}"]`)?.closest('label');
|
||||
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
||||
});
|
||||
|
||||
// Passiv Werkzeuge: aufgabe/strafe, not in chastity
|
||||
document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Finisher Werkzeuge: not in chastity
|
||||
document.getElementById('iWerkzeugFinisherAktivRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherPassivRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Zeitstrafe rows
|
||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||
['VAGINA', 'PENIS'].forEach(v => {
|
||||
const lbl = document.querySelector(`#iSperreFuer input[value="${v}"]`)?.closest('label');
|
||||
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
||||
});
|
||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function _resetItemFields() {
|
||||
@@ -1449,6 +1484,9 @@
|
||||
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
|
||||
document.getElementById('iReleaseText').value = d.releaseText || '';
|
||||
(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 : '';
|
||||
}
|
||||
}
|
||||
|
||||
const preSelected = (d.benoetigteToys || []).filter(t => t.toyId);
|
||||
@@ -1480,7 +1518,7 @@
|
||||
}
|
||||
function buildItems(filter) {
|
||||
const f = filter || '';
|
||||
_items = STATIC.filter(s => !f || s.toLowerCase().includes(f)).map(s => ({ label: s, insert: s }));
|
||||
_items = (_isChastityMode ? [] : STATIC.filter(s => !f || s.toLowerCase().includes(f))).map(s => ({ label: s, insert: s }));
|
||||
const toys = _allToys.filter(t => !f || t.name.toLowerCase().includes(f));
|
||||
if (toys.length) {
|
||||
_items.push({ separator: true, label: 'Toys' });
|
||||
@@ -1606,13 +1644,14 @@
|
||||
method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
} else if (kind === 'finisher') {
|
||||
const geschlecht = document.querySelector('#iGeschlecht input:checked')?.value;
|
||||
if (!geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||
const geschlecht = _isChastityMode ? null : document.querySelector('#iGeschlecht input:checked')?.value;
|
||||
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||
payload = {
|
||||
kurzText, text, geschlecht,
|
||||
kurzText, text,
|
||||
geschlecht: geschlecht || null,
|
||||
gruppeId: isEdit ? undefined : currentItemGruppeId,
|
||||
benoetigtAktiv: checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
|
||||
@@ -1624,6 +1663,14 @@
|
||||
const sperreFuer = checkedValues('iSperreFuer');
|
||||
if (sperreFuer.length === 0) { showItemError('Bitte mindestens ein Werkzeug für die Sperre auswählen.'); return; }
|
||||
|
||||
let zeitLevel = null;
|
||||
if (_isChastityMode) {
|
||||
const lv = document.getElementById('iLevel').value.trim();
|
||||
if (!lv) { showItemError('Bitte ein Level angeben.'); return; }
|
||||
zeitLevel = parseInt(lv, 10);
|
||||
if (isNaN(zeitLevel) || zeitLevel < 1 || zeitLevel > 5) { showItemError('Level muss zwischen 1 und 5 liegen.'); return; }
|
||||
}
|
||||
|
||||
const minBis = document.getElementById('iMinBis').value.trim();
|
||||
payload = {
|
||||
kurzText, text,
|
||||
@@ -1632,6 +1679,7 @@
|
||||
minutenBis: minBis ? parseInt(minBis, 10) : null,
|
||||
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
||||
sperreFuer,
|
||||
level: zeitLevel,
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
|
||||
|
||||
@@ -1711,6 +1711,14 @@
|
||||
document.getElementById('btnDrawOk').style.display = 'none';
|
||||
document.getElementById('btnSpeedConfirm').style.display = '';
|
||||
}
|
||||
|
||||
if (dto.card === 'GAME_CARD') {
|
||||
const btn = document.getElementById('btnDrawOk');
|
||||
btn.textContent = '▶ Spiel starten';
|
||||
btn.onclick = function() {
|
||||
window.location.href = '/games/chastity/taskgame.html?lockId=' + lockId;
|
||||
};
|
||||
}
|
||||
}, 700);
|
||||
}, 1000);
|
||||
})
|
||||
|
||||
@@ -268,47 +268,6 @@
|
||||
.sim-stat-val { font-size:1rem; font-weight:700; color:var(--color-text); }
|
||||
.sim-stat-lbl { font-size:0.7rem; color:var(--color-muted); margin-top:0.15rem; text-transform:uppercase; letter-spacing:0.05em; }
|
||||
|
||||
/* ── Spiel-Sets (gruppe-card style) ── */
|
||||
.gs-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; cursor:pointer; transition:border-color 0.15s; }
|
||||
.gs-card:hover { border-color:var(--color-primary); }
|
||||
.gs-card-header { display:flex; align-items:center; gap:0.75rem; padding:0.85rem 1rem; user-select:none; }
|
||||
.gs-card-meta { flex:1; min-width:0; }
|
||||
.gs-card-name { font-size:0.95rem; font-weight:600; color:var(--color-text); }
|
||||
.gs-card-header-actions { display:flex; gap:0.4rem; flex-shrink:0; }
|
||||
|
||||
.gs-sub { margin-bottom:0.85rem; }
|
||||
.gs-sub:last-child { margin-bottom:0; }
|
||||
.gs-sub-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.35rem; }
|
||||
.gs-sub-title { font-size:0.7rem; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--color-primary); }
|
||||
.gs-sub-warn { color:#e74c3c !important; }
|
||||
|
||||
.gs-item-list { display:flex; flex-direction:column; gap:0.25rem; }
|
||||
.gs-list-item { border-radius:6px; background:var(--color-secondary); overflow:hidden; }
|
||||
.gs-list-item-row { display:flex; align-items:center; gap:0.5rem; padding:0.3rem 0.5rem 0.3rem 0.75rem; cursor:pointer; user-select:none; transition:background 0.12s; }
|
||||
.gs-list-item-row:hover { background:rgba(255,255,255,0.04); }
|
||||
.gs-list-item.open .gs-list-item-row { background:rgba(233,69,96,0.08); }
|
||||
.gs-list-item-text { color:var(--color-text); flex:1; min-width:0; font-size:0.83rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.gs-list-item-badges { display:flex; gap:0.3rem; flex-shrink:0; align-items:center; }
|
||||
.gs-badge { font-size:0.68rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(233,69,96,0.15); color:var(--color-primary); white-space:nowrap; }
|
||||
.gs-badge-neutral { background:rgba(255,255,255,0.07); color:var(--color-muted); }
|
||||
.gs-list-item-detail { display:none; padding:0.4rem 0.75rem 0.5rem; border-top:1px solid rgba(255,255,255,0.06); font-size:0.79rem; color:var(--color-muted); line-height:1.5; }
|
||||
.gs-list-item.open .gs-list-item-detail { display:block; }
|
||||
.gs-detail-text { color:var(--color-text); white-space:pre-wrap; margin-bottom:0.3rem; }
|
||||
.gs-detail-actions { display:flex; gap:0.35rem; margin-top:0.4rem; justify-content:flex-end; }
|
||||
.gs-btn-sub-add { background:none; border:1px solid var(--color-secondary); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.15rem 0.5rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||||
.gs-btn-sub-add:hover { border-color:var(--color-primary); color:var(--color-primary); }
|
||||
.gs-btn-item-edit { background:none; border:1px solid rgba(136,136,136,0.45); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||||
.gs-btn-item-edit:hover { border-color:var(--color-text); color:var(--color-text); }
|
||||
.gs-btn-item-delete { background:none; border:1px solid rgba(233,69,96,0.4); border-radius:5px; color:var(--color-primary); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:background 0.15s; width:auto; }
|
||||
.gs-btn-item-delete:hover { background:rgba(233,69,96,0.15); }
|
||||
.gs-sub-empty { font-size:0.78rem; color:var(--color-muted); padding:0.15rem 0; }
|
||||
|
||||
#gsSetModal, #gsItemModal { z-index:600; }
|
||||
|
||||
.gs-check-group { display:flex; flex-wrap:wrap; gap:0.4rem; }
|
||||
.gs-check-chip { display:inline-flex; align-items:center; gap:0.4rem; background:var(--color-secondary); border:1px solid rgba(255,255,255,0.1); border-radius:20px; padding:0.25rem 0.7rem; cursor:pointer; font-size:0.82rem; color:var(--color-text); transition:border-color 0.15s; user-select:none; }
|
||||
.gs-check-chip:has(input:checked) { border-color:var(--color-primary); }
|
||||
.gs-check-chip input { accent-color:var(--color-primary); width:auto; cursor:pointer; margin:0; flex-shrink:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -329,17 +288,6 @@
|
||||
<div class="template-list" id="taskSetList"></div>
|
||||
<p id="taskSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Aufgaben-Sets vorhanden.</p>
|
||||
|
||||
<!-- Spiel-Sets (für Spiel-Karte) -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin:2rem 0 1rem;gap:1rem;flex-wrap:wrap;">
|
||||
<div>
|
||||
<h2 style="margin:0;">Spiel-Sets</h2>
|
||||
<p style="margin:0.25rem 0 0;font-size:0.8rem;color:var(--color-muted);">Aufgaben-Sets für die Spiel-Karte im Karten-Lock · max. 5 Sets</p>
|
||||
</div>
|
||||
<button id="btnNewGameSet" onclick="openGsSetModal(null)" style="width:auto;padding:0.55rem 1.2rem;">+ Set anlegen</button>
|
||||
</div>
|
||||
<div class="template-list" id="gameSetList"></div>
|
||||
<p id="gameSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Spiel-Sets vorhanden.</p>
|
||||
|
||||
<h2 style="margin:2rem 0 1rem;">Abonnierte Vorlagen</h2>
|
||||
<div class="template-list" id="subscribedList"></div>
|
||||
<p id="subscribedEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Keine abonnierten Vorlagen vorhanden.</p>
|
||||
@@ -443,24 +391,6 @@
|
||||
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spiel-Karte (CardLock) -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Spiel-Karte (optional)</div>
|
||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||
<label>Spiel-Set</label>
|
||||
<select id="fGameSetId" onchange="onGameSetChange()">
|
||||
<option value="">Kein Spiel-Set</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-add" type="button" onclick="openGsSetModal(null,'template')">+ Neues Set anlegen</button>
|
||||
<div id="gameSetSpieldauerRow" style="display:none;margin-top:0.75rem;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.3rem;">
|
||||
<label for="sldGameSpieldauer" style="font-size:0.88rem;">Spieldauer</label>
|
||||
<span class="setting-value" id="valGameSpieldauer">Mittel</span>
|
||||
</div>
|
||||
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2" oninput="updateGameSpieldauer(this.value)" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Aufgaben (CardLock) -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Aufgaben (optional)</div>
|
||||
@@ -719,114 +649,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Set erstellen / umbenennen -->
|
||||
<div class="modal-backdrop" id="gsSetModal">
|
||||
<div class="modal-box" style="max-width:420px;" onclick="event.stopPropagation()">
|
||||
<h2 id="gsSetModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Neues Spiel-Set</h2>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Name *</label>
|
||||
<input type="text" id="gsSetName" maxlength="100" placeholder="Set-Name"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;transition:border-color 0.2s;"
|
||||
onkeydown="if(event.key==='Enter')saveGsSet()">
|
||||
<div id="gsSetError" style="color:#e74c3c;font-size:0.82rem;margin-top:0.75rem;display:none;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:1.5rem;">
|
||||
<button onclick="closeGsSetModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||||
<button onclick="saveGsSet()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Item hinzufügen / bearbeiten -->
|
||||
<div class="modal-backdrop" id="gsItemModal">
|
||||
<div class="modal-box" style="max-width:460px;" onclick="event.stopPropagation()">
|
||||
<h2 id="gsItemModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Aufgabe</h2>
|
||||
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Titel *</label>
|
||||
<input type="text" id="gsItemTitle" maxlength="150" placeholder="Titel …"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;margin-bottom:0.85rem;transition:border-color 0.2s;">
|
||||
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Beschreibung (optional)</label>
|
||||
<textarea id="gsItemDesc" rows="3" maxlength="600" placeholder="Beschreibung …"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;margin-bottom:0.85rem;"></textarea>
|
||||
|
||||
<!-- Aufgabe: Level + Dauer -->
|
||||
<div id="gsItemAufgabeRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||||
<select id="gsItemAufgabeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||||
</select>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Dauer (Minuten, optional)</label>
|
||||
<input type="number" id="gsItemMinutes" min="1" max="9999" placeholder="–"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
</div>
|
||||
|
||||
<!-- Aufgabe: benötigt -->
|
||||
<div id="gsItemBenoetigtRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Benötigt</div>
|
||||
<div class="gs-check-group">
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_MUND">Mund</label>
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_ANUS">Anus</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: Level + Strafminuten -->
|
||||
<div id="gsItemZeitstrafeRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||||
<select id="gsItemZeitstrafeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||||
</select>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Strafminuten (Von – Bis)</label>
|
||||
<div style="display:flex;gap:0.6rem;align-items:center;">
|
||||
<input type="number" id="gsItemMinMin" min="1" max="9999" placeholder="Min."
|
||||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
<span style="color:var(--color-muted);">–</span>
|
||||
<input type="number" id="gsItemMaxMin" min="1" max="9999" placeholder="Max."
|
||||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
</div>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin:0.65rem 0 0.3rem;">Text bei Aufhebung (optional)</label>
|
||||
<textarea id="gsItemReleaseText" rows="2" maxlength="2000"
|
||||
placeholder="Text der angezeigt wird, wenn die Sperre endet…"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;transition:border-color 0.2s;line-height:1.45;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: sperrt -->
|
||||
<div id="gsItemSperrtRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Sperrt</div>
|
||||
<div class="gs-check-group">
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_MUND"> Mund</label>
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_ANUS"> Anus</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe + Finisher: Temp. Öffnung -->
|
||||
<div id="gsItemUnlockRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Temp. Öffnung</div>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;margin-bottom:0.35rem;cursor:pointer;">
|
||||
<input type="checkbox" id="gsItemBefore" style="accent-color:var(--color-primary);width:15px;height:15px;"> Vor der Maßnahme erforderlich
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||||
<input type="checkbox" id="gsItemAfter" style="accent-color:var(--color-primary);width:15px;height:15px;"> Nach der Maßnahme erforderlich
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="gsItemError" style="color:#e74c3c;font-size:0.82rem;display:none;margin-bottom:0.5rem;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:0.5rem;">
|
||||
<button onclick="closeGsItemModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||||
<button onclick="saveGsItem()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Inhalt-Popup -->
|
||||
<div class="modal-backdrop" id="gsEditModal">
|
||||
<div class="modal-box" style="max-width:600px;width:calc(100% - 2rem);" onclick="event.stopPropagation()">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<h2 id="gsEditModalTitle" style="margin:0;color:var(--color-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></h2>
|
||||
<button onclick="closeGsEditModal()" style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;line-height:1;cursor:pointer;padding:0;width:auto;flex-shrink:0;">✕</button>
|
||||
</div>
|
||||
<div id="gsEditModalContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/card-display.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
@@ -1223,51 +1045,6 @@
|
||||
if (res.ok || res.status === 204) await loadTaskSets();
|
||||
}
|
||||
|
||||
const GS_TOOLS = [
|
||||
{ value: 'UMSCHNALLDILDO', label: 'Strap-on' },
|
||||
{ value: 'MUND', label: 'Oral' },
|
||||
{ value: 'ANUS', label: 'Anal' },
|
||||
];
|
||||
function gsGetChecked(prefix) {
|
||||
return GS_TOOLS.filter(t => document.getElementById(prefix + t.value)?.checked).map(t => t.value);
|
||||
}
|
||||
function gsSetChecked(prefix, values) {
|
||||
GS_TOOLS.forEach(t => {
|
||||
const el = document.getElementById(prefix + t.value);
|
||||
if (el) el.checked = (values || []).includes(t.value);
|
||||
});
|
||||
}
|
||||
|
||||
const GAME_SPIELDAUER = [
|
||||
{ label: 'Sehr kurz' },
|
||||
{ label: 'Kurz' },
|
||||
{ label: 'Mittel' },
|
||||
{ label: 'Lang' },
|
||||
{ label: 'Sehr lang' },
|
||||
];
|
||||
|
||||
function populateGameSetSelect() {
|
||||
const sel = document.getElementById('fGameSetId');
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">Kein Spiel-Set</option>';
|
||||
_gameSets.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id; opt.textContent = s.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.value = cur;
|
||||
}
|
||||
|
||||
function onGameSetChange() {
|
||||
const val = document.getElementById('fGameSetId')?.value;
|
||||
document.getElementById('gameSetSpieldauerRow').style.display = val ? '' : 'none';
|
||||
}
|
||||
|
||||
function updateGameSpieldauer(val) {
|
||||
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[val]?.label || '';
|
||||
}
|
||||
|
||||
function populateTaskSetSelects() {
|
||||
for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) {
|
||||
const sel = document.getElementById(selId);
|
||||
@@ -1476,14 +1253,6 @@
|
||||
onTaskSetChange('card');
|
||||
onTaskSetChange('timelock');
|
||||
|
||||
// Spiel-Set
|
||||
populateGameSetSelect();
|
||||
document.getElementById('fGameSetId').value = template?.gameSetId || '';
|
||||
onGameSetChange();
|
||||
const sdIdx = template?.gameSpieldauerIdx ?? 2;
|
||||
document.getElementById('sldGameSpieldauer').value = sdIdx;
|
||||
updateGameSpieldauer(sdIdx);
|
||||
|
||||
alignModalToContent();
|
||||
document.getElementById('modalBackdrop').classList.add('open');
|
||||
document.getElementById('modalDiscardConfirm').style.display = 'none';
|
||||
@@ -1527,13 +1296,7 @@
|
||||
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (document.getElementById('gsItemModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsItemModal();
|
||||
} else if (document.getElementById('gsSetModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsSetModal();
|
||||
} else if (document.getElementById('gsEditModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsEditModal();
|
||||
} else if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
|
||||
if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault(); tryCloseTaskSetModal();
|
||||
} else if (document.getElementById('modalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault(); tryCloseModal();
|
||||
@@ -1588,9 +1351,6 @@
|
||||
if (totalMax===0) { showModalError('Das Deck muss mindestens eine Karte enthalten.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
const hasTaskCards = (parseInt(document.getElementById('min_TASK').value)||0)>0 || (parseInt(document.getElementById('max_TASK').value)||0)>0;
|
||||
if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Aufgaben-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
const hasGameCards = (parseInt(document.getElementById('min_GAME_CARD').value)||0)>0 || (parseInt(document.getElementById('max_GAME_CARD').value)||0)>0;
|
||||
if (hasGameCards && !document.getElementById('fGameSetId').value) { showModalError('Spiel-Karten konfiguriert, aber kein Spiel-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
|
||||
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
|
||||
|
||||
const cardCountsMin={}, cardCountsMax={};
|
||||
@@ -1609,8 +1369,6 @@
|
||||
taskSetId: document.getElementById('fCardTaskSetId').value || null,
|
||||
requiresVerification: document.getElementById('fRequiresVerification').checked,
|
||||
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
|
||||
gameSetId: document.getElementById('fGameSetId').value || null,
|
||||
gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2,
|
||||
};
|
||||
} else {
|
||||
// TimeLock
|
||||
@@ -1895,426 +1653,8 @@
|
||||
observer.observe(document.getElementById('scrollSentinel'));
|
||||
|
||||
resetList();
|
||||
loadGameSets();
|
||||
|
||||
// ════════════════════════════════════════════════
|
||||
// Spiel-Sets
|
||||
// ════════════════════════════════════════════════
|
||||
|
||||
let _gameSets = [];
|
||||
let _gsEditSetId = null; // set being renamed
|
||||
let _gsSetCaller = null; // 'template' when opened from the template modal
|
||||
let _gsOpenSetId = null; // set currently open in the content popup
|
||||
let _gsItemType = null; // 'aufgabe' | 'zeitstrafe' | 'finisher'
|
||||
let _gsItemSetId = null;
|
||||
let _gsItemIdx = null; // null = new, number = editing
|
||||
|
||||
async function loadGameSets() {
|
||||
try {
|
||||
const res = await fetch('/chastity/game-sets');
|
||||
if (!res.ok) return;
|
||||
_gameSets = await res.json();
|
||||
renderGameSetList();
|
||||
populateGameSetSelect();
|
||||
if (_gsOpenSetId) renderGsEditModalContent(_gsOpenSetId);
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderGameSetList() {
|
||||
const list = document.getElementById('gameSetList');
|
||||
list.innerHTML = '';
|
||||
document.getElementById('gameSetEmpty').style.display = _gameSets.length ? 'none' : '';
|
||||
document.getElementById('btnNewGameSet').disabled = _gameSets.length >= 5;
|
||||
|
||||
_gameSets.forEach(s => {
|
||||
const aufgaben = s.aufgaben || [];
|
||||
const zeitstrafen = s.zeitstrafen || [];
|
||||
const finisher = s.finisher || [];
|
||||
const levelCounts = [1,2,3,4,5].map(l => aufgaben.filter(a => a.level === l).length);
|
||||
|
||||
const lvlBadges = levelCounts.map((c, i) => {
|
||||
const cls = c >= 3 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
|
||||
return `<span class="${cls}">L${i+1}: ${c}</span>`;
|
||||
}).join('');
|
||||
const finBadgeCls = finisher.length >= 1 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'gs-card';
|
||||
card.id = 'gscard_' + s.id;
|
||||
card.addEventListener('click', () => openGsEditModal(s.id));
|
||||
card.innerHTML = `
|
||||
<div class="gs-card-header">
|
||||
<div class="gs-card-meta">
|
||||
<div class="gs-card-name">${esc(s.name)}</div>
|
||||
<div style="display:flex;gap:0.3rem;flex-wrap:wrap;margin-top:0.3rem;">
|
||||
${lvlBadges}
|
||||
<span class="gs-badge gs-badge-neutral">Zeitstrafen: ${zeitstrafen.length}</span>
|
||||
<span class="${finBadgeCls}">Finisher: ${finisher.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gs-card-header-actions" onclick="event.stopPropagation()">
|
||||
<button class="gs-btn-item-edit" onclick="openGsSetModal('${s.id}')">✎</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGameSet('${s.id}',${JSON.stringify(s.name)})">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGsListItem(id) {
|
||||
document.getElementById(id)?.classList.toggle('open');
|
||||
}
|
||||
|
||||
// ── Set content popup ──────────────────────────
|
||||
|
||||
function openGsEditModal(setId) {
|
||||
_gsOpenSetId = setId;
|
||||
renderGsEditModalContent(setId);
|
||||
document.getElementById('gsEditModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeGsEditModal() {
|
||||
document.getElementById('gsEditModal').classList.remove('open');
|
||||
_gsOpenSetId = null;
|
||||
}
|
||||
|
||||
function renderGsEditModalContent(setId) {
|
||||
const container = document.getElementById('gsEditModalContent');
|
||||
if (!container) return;
|
||||
const s = _gameSets.find(x => x.id === setId);
|
||||
if (!s) { closeGsEditModal(); return; }
|
||||
document.getElementById('gsEditModalTitle').textContent = s.name;
|
||||
const aufgaben = s.aufgaben || [];
|
||||
const zeitstrafen = s.zeitstrafen || [];
|
||||
const finisher = s.finisher || [];
|
||||
let html = '';
|
||||
for (let l = 1; l <= 5; l++) {
|
||||
const items = aufgaben.map((a, i) => ({...a, _gi: i})).filter(a => a.level === l);
|
||||
const warnCls = items.length < 3 ? ' gs-sub-warn' : '';
|
||||
const itemsHtml = items.map(a => gsAufgabeRowHtml(s.id, a._gi, a)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title${warnCls}">Level ${l} <span style="font-weight:400;">(${items.length}/3+)</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('aufgabe','${s.id}',null,${l})">+ Aufgabe</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${itemsHtml}</div></div>`;
|
||||
}
|
||||
const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title">Zeitstrafen <span style="font-weight:400;">(${zeitstrafen.length})</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('zeitstrafe','${s.id}',null,null)">+ Zeitstrafe</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${zeitHtml}</div></div>`;
|
||||
const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : '';
|
||||
const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title${finWarnCls}">Finisher <span style="font-weight:400;">(${finisher.length}/1+)</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('finisher','${s.id}',null,null)">+ Finisher</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${finHtml}</div></div>`;
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Row HTML helpers ───────────────────────────
|
||||
|
||||
function gsAufgabeRowHtml(setId, gi, a) {
|
||||
const toolLabels = (a.benoetigt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
|
||||
const badges = [
|
||||
a.minutes ? `<span class="gs-badge gs-badge-neutral">${a.minutes} Min.</span>` : '',
|
||||
...toolLabels.map(l => `<span class="gs-badge gs-badge-neutral">${l}</span>`),
|
||||
].join('');
|
||||
const desc = a.description ? `<div class="gs-detail-text">${esc(a.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_a_${gi}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_a_${gi}')">
|
||||
<span class="gs-list-item-text">${esc(a.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('aufgabe','${setId}',${gi},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('aufgabe','${setId}',${gi})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('aufgabe','${setId}',${gi})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function gsZeitstrafeRowHtml(setId, idx, z) {
|
||||
const timeStr = (z.minMinutes != null ? z.minMinutes : '?') + '–' + (z.maxMinutes != null ? z.maxMinutes : '?') + ' Min.';
|
||||
const sperrtLabels = (z.sperrt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
|
||||
const badges = [
|
||||
z.level ? `<span class="gs-badge">L${z.level}</span>` : '',
|
||||
`<span class="gs-badge gs-badge-neutral">${timeStr}</span>`,
|
||||
...sperrtLabels.map(l => `<span class="gs-badge gs-badge-neutral">🔒 ${l}</span>`),
|
||||
z.releaseText ? `<span class="gs-badge gs-badge-neutral">📝 Aufhebung</span>` : '',
|
||||
z.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||||
z.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||||
].join('');
|
||||
const releaseRow = z.releaseText ? `<div style="font-size:0.75rem;color:var(--color-muted);margin-bottom:0.15rem;">Bei Aufhebung:</div><div class="gs-detail-text">${esc(z.releaseText)}</div>` : '';
|
||||
const desc = z.description ? `<div class="gs-detail-text">${esc(z.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_z_${idx}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_z_${idx}')">
|
||||
<span class="gs-list-item-text">${esc(z.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}${releaseRow}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('zeitstrafe','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('zeitstrafe','${setId}',${idx})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('zeitstrafe','${setId}',${idx})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function gsFinisherRowHtml(setId, idx, f) {
|
||||
const badges = [
|
||||
f.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||||
f.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||||
].join('');
|
||||
const desc = f.description ? `<div class="gs-detail-text">${esc(f.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_f_${idx}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_f_${idx}')">
|
||||
<span class="gs-list-item-text">${esc(f.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('finisher','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('finisher','${setId}',${idx})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('finisher','${setId}',${idx})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// ── Set create / rename modal ──────────────────
|
||||
|
||||
function openGsSetModal(id, caller) {
|
||||
_gsEditSetId = id || null;
|
||||
_gsSetCaller = caller || null;
|
||||
document.getElementById('gsSetModalTitle').textContent = id ? 'Spiel-Set umbenennen' : 'Neues Spiel-Set';
|
||||
document.getElementById('gsSetName').value = id ? (_gameSets.find(s => s.id === id)?.name || '') : '';
|
||||
document.getElementById('gsSetError').style.display = 'none';
|
||||
document.getElementById('gsSetModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('gsSetName').focus(), 50);
|
||||
}
|
||||
|
||||
function closeGsSetModal() {
|
||||
document.getElementById('gsSetModal').classList.remove('open');
|
||||
_gsEditSetId = _gsSetCaller = null;
|
||||
}
|
||||
|
||||
async function saveGsSet() {
|
||||
const name = document.getElementById('gsSetName').value.trim();
|
||||
const errEl = document.getElementById('gsSetError');
|
||||
if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
|
||||
errEl.style.display = 'none';
|
||||
const set = _gsEditSetId ? _gameSets.find(s => s.id === _gsEditSetId) : null;
|
||||
const url = _gsEditSetId ? `/chastity/game-sets/${_gsEditSetId}` : '/chastity/game-sets';
|
||||
const method = _gsEditSetId ? 'PUT' : 'POST';
|
||||
const body = { name,
|
||||
aufgaben: set?.aufgaben || [],
|
||||
zeitstrafen: set?.zeitstrafen || [],
|
||||
finisher: set?.finisher || [] };
|
||||
try {
|
||||
const res = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
||||
if (res.ok) {
|
||||
const saved = await res.json().catch(() => null);
|
||||
const caller = _gsSetCaller;
|
||||
closeGsSetModal();
|
||||
await loadGameSets();
|
||||
if (caller === 'template' && saved?.id) {
|
||||
document.getElementById('fGameSetId').value = saved.id;
|
||||
onGameSetChange();
|
||||
markDirty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const b = await res.json().catch(() => ({}));
|
||||
errEl.textContent = b.error || 'Fehler.'; errEl.style.display = '';
|
||||
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
|
||||
}
|
||||
|
||||
async function deleteGameSet(id, name) {
|
||||
if (!confirm(`Spiel-Set „${name}" wirklich löschen?`)) return;
|
||||
const res = await fetch(`/chastity/game-sets/${id}`, { method: 'DELETE' });
|
||||
if (res.ok || res.status === 204) loadGameSets();
|
||||
}
|
||||
|
||||
// ── Item modal ─────────────────────────────────
|
||||
|
||||
function openGsItemModal(type, setId, itemIdx, contextLevel) {
|
||||
_gsItemType = type;
|
||||
_gsItemSetId = setId;
|
||||
_gsItemIdx = itemIdx !== null && itemIdx !== undefined ? itemIdx : null;
|
||||
|
||||
const titles = { aufgabe: 'Aufgabe', zeitstrafe: 'Zeitstrafe', finisher: 'Finisher' };
|
||||
document.getElementById('gsItemModalTitle').textContent =
|
||||
(_gsItemIdx !== null ? 'Bearbeiten: ' : 'Neu: ') + titles[type];
|
||||
|
||||
// Reset fields
|
||||
document.getElementById('gsItemTitle').value = '';
|
||||
document.getElementById('gsItemDesc').value = '';
|
||||
document.getElementById('gsItemMinutes').value = '';
|
||||
document.getElementById('gsItemMinMin').value = '';
|
||||
document.getElementById('gsItemMaxMin').value = '';
|
||||
document.getElementById('gsItemReleaseText').value = '';
|
||||
document.getElementById('gsItemBefore').checked = false;
|
||||
document.getElementById('gsItemAfter').checked = false;
|
||||
document.getElementById('gsItemAufgabeLevel').value = contextLevel || 1;
|
||||
document.getElementById('gsItemZeitstrafeLevel').value = 1;
|
||||
document.getElementById('gsItemError').style.display = 'none';
|
||||
gsSetChecked('gsItemBen_', []);
|
||||
gsSetChecked('gsItemSperr_', []);
|
||||
|
||||
// Show/hide type-specific rows
|
||||
document.getElementById('gsItemAufgabeRow').style.display = type === 'aufgabe' ? '' : 'none';
|
||||
document.getElementById('gsItemBenoetigtRow').style.display = type === 'aufgabe' ? '' : 'none';
|
||||
document.getElementById('gsItemZeitstrafeRow').style.display = type === 'zeitstrafe' ? '' : 'none';
|
||||
document.getElementById('gsItemSperrtRow').style.display = type === 'zeitstrafe' ? '' : 'none';
|
||||
document.getElementById('gsItemUnlockRow').style.display = (type === 'zeitstrafe' || type === 'finisher') ? '' : 'none';
|
||||
|
||||
// Pre-fill when editing
|
||||
if (_gsItemIdx !== null) {
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (set) {
|
||||
let item;
|
||||
if (type === 'aufgabe') item = set.aufgaben[_gsItemIdx];
|
||||
if (type === 'zeitstrafe') item = set.zeitstrafen[_gsItemIdx];
|
||||
if (type === 'finisher') item = set.finisher[_gsItemIdx];
|
||||
if (item) {
|
||||
document.getElementById('gsItemTitle').value = item.title || '';
|
||||
document.getElementById('gsItemDesc').value = item.description || '';
|
||||
if (type === 'aufgabe') {
|
||||
document.getElementById('gsItemAufgabeLevel').value = item.level || 1;
|
||||
document.getElementById('gsItemMinutes').value = item.minutes || '';
|
||||
gsSetChecked('gsItemBen_', item.benoetigt || []);
|
||||
}
|
||||
if (type === 'zeitstrafe') {
|
||||
document.getElementById('gsItemZeitstrafeLevel').value = item.level || 1;
|
||||
document.getElementById('gsItemMinMin').value = item.minMinutes ?? '';
|
||||
document.getElementById('gsItemMaxMin').value = item.maxMinutes ?? '';
|
||||
document.getElementById('gsItemReleaseText').value = item.releaseText || '';
|
||||
gsSetChecked('gsItemSperr_', item.sperrt || []);
|
||||
}
|
||||
if (type === 'zeitstrafe' || type === 'finisher') {
|
||||
document.getElementById('gsItemBefore').checked = !!item.tempUnlockBeforeRequired;
|
||||
document.getElementById('gsItemAfter').checked = !!item.tempUnlockAfterRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('gsItemModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('gsItemTitle').focus(), 50);
|
||||
}
|
||||
|
||||
function closeGsItemModal() {
|
||||
document.getElementById('gsItemModal').classList.remove('open');
|
||||
_gsItemType = _gsItemSetId = _gsItemIdx = null;
|
||||
}
|
||||
|
||||
async function saveGsItem() {
|
||||
const title = document.getElementById('gsItemTitle').value.trim();
|
||||
const errEl = document.getElementById('gsItemError');
|
||||
if (!title) { errEl.textContent = 'Titel ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const set = _gameSets.find(s => s.id === _gsItemSetId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
|
||||
const desc = document.getElementById('gsItemDesc').value.trim() || null;
|
||||
let item;
|
||||
if (_gsItemType === 'aufgabe') {
|
||||
const min = parseInt(document.getElementById('gsItemMinutes').value);
|
||||
const ben = gsGetChecked('gsItemBen_');
|
||||
item = { title, description: desc,
|
||||
level: parseInt(document.getElementById('gsItemAufgabeLevel').value) || 1,
|
||||
minutes: isNaN(min) ? null : min,
|
||||
benoetigt: ben.length ? ben : null };
|
||||
if (_gsItemIdx !== null) updated.aufgaben[_gsItemIdx] = item;
|
||||
else updated.aufgaben.push(item);
|
||||
} else if (_gsItemType === 'zeitstrafe') {
|
||||
const minMin = parseInt(document.getElementById('gsItemMinMin').value);
|
||||
const maxMin = parseInt(document.getElementById('gsItemMaxMin').value);
|
||||
const sperrt = gsGetChecked('gsItemSperr_');
|
||||
const releaseText = document.getElementById('gsItemReleaseText').value.trim() || null;
|
||||
item = { title, description: desc,
|
||||
level: parseInt(document.getElementById('gsItemZeitstrafeLevel').value) || 1,
|
||||
minMinutes: isNaN(minMin) ? null : minMin,
|
||||
maxMinutes: isNaN(maxMin) ? null : maxMin,
|
||||
releaseText,
|
||||
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
|
||||
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked,
|
||||
sperrt: sperrt.length ? sperrt : null };
|
||||
if (_gsItemIdx !== null) updated.zeitstrafen[_gsItemIdx] = item;
|
||||
else updated.zeitstrafen.push(item);
|
||||
} else if (_gsItemType === 'finisher') {
|
||||
item = { title, description: desc,
|
||||
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
|
||||
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked };
|
||||
if (_gsItemIdx !== null) updated.finisher[_gsItemIdx] = item;
|
||||
else updated.finisher.push(item);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/chastity/game-sets/${_gsItemSetId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) { closeGsItemModal(); await loadGameSets(); }
|
||||
else { const b = await res.json().catch(()=>({})); errEl.textContent = b.error||'Fehler.'; errEl.style.display = ''; }
|
||||
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
|
||||
}
|
||||
|
||||
async function deleteGsItem(type, setId, idx) {
|
||||
if (!confirm('Eintrag wirklich löschen?')) return;
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
if (type === 'aufgabe') updated.aufgaben.splice(idx, 1);
|
||||
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx, 1);
|
||||
if (type === 'finisher') updated.finisher.splice(idx, 1);
|
||||
const res = await fetch(`/chastity/game-sets/${setId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) loadGameSets();
|
||||
}
|
||||
|
||||
async function duplicateGsItem(type, setId, idx) {
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
if (type === 'aufgabe') updated.aufgaben.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.aufgaben[idx])));
|
||||
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.zeitstrafen[idx])));
|
||||
if (type === 'finisher') updated.finisher.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.finisher[idx])));
|
||||
const res = await fetch(`/chastity/game-sets/${setId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) loadGameSets();
|
||||
}
|
||||
|
||||
document.getElementById('gsSetModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsSetModal(); });
|
||||
document.getElementById('gsItemModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsItemModal(); });
|
||||
document.getElementById('gsEditModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsEditModal(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
433
bin/main/static/games/chastity/taskgame.html
Normal file
433
bin/main/static/games/chastity/taskgame.html
Normal file
@@ -0,0 +1,433 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Task Game – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.game-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.game-text {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.game-timer {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
text-align: center;
|
||||
margin: 0.75rem 0;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.game-timer.urgent { color: #e74c3c; }
|
||||
|
||||
.game-level-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-level-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.game-level-dot.active { background: var(--color-primary); }
|
||||
|
||||
.lock-messages {
|
||||
background: rgba(233,69,96,0.1);
|
||||
border: 1px solid rgba(233,69,96,0.3);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.lock-messages p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.5; }
|
||||
.lock-messages p:last-child { margin: 0; }
|
||||
|
||||
.init-box {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.init-box h2 { font-size: 1.1rem; margin: 0 0 0.75rem; }
|
||||
.group-list { display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem; }
|
||||
.group-item {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
.group-item:hover, .group-item.selected {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(var(--color-primary-rgb, 233,69,96), 0.06);
|
||||
}
|
||||
.group-item input[type=radio] { accent-color: var(--color-primary); }
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
padding: 0.65rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-secondary);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 0.92rem;
|
||||
cursor: pointer;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
#finisherBox {
|
||||
background: linear-gradient(135deg, rgba(233,69,96,0.15), rgba(155,89,182,0.12));
|
||||
border: 1px solid rgba(233,69,96,0.4);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
#finisherBox .trophy { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||||
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="container" style="max-width:480px;margin:0 auto;padding:1rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<button id="btnBack" onclick="goBack()"
|
||||
style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.4rem;">‹</button>
|
||||
<h1 style="margin:0;font-size:1.15rem;font-weight:700;">🎯 Task Game</h1>
|
||||
</div>
|
||||
|
||||
<!-- Level-Anzeige -->
|
||||
<div class="game-level-bar" id="levelBar" style="display:none;">
|
||||
<span style="font-size:0.78rem;color:var(--color-muted);font-weight:600;">Level</span>
|
||||
<div id="levelDots" style="display:flex;gap:0.35rem;"></div>
|
||||
<span id="levelText" style="font-size:0.78rem;color:var(--color-muted);margin-left:auto;"></span>
|
||||
</div>
|
||||
|
||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
|
||||
|
||||
<!-- Initialisierung: Gruppe wählen -->
|
||||
<div id="initBox" class="init-box" style="display:none;">
|
||||
<h2>Spiel-Set auswählen</h2>
|
||||
<p style="font-size:0.85rem;color:var(--color-muted);margin:0 0 1rem;">
|
||||
Wähle die Aufgabengruppe für dieses Spiel.
|
||||
</p>
|
||||
<div id="groupList" class="group-list"></div>
|
||||
<button class="btn-primary" id="btnStart" disabled onclick="startGame()">▶ Spiel starten</button>
|
||||
</div>
|
||||
|
||||
<!-- Laufendes Spiel -->
|
||||
<div id="gameBox" style="display:none;">
|
||||
|
||||
<!-- Task oder Lock in Queue -->
|
||||
<div id="queueBox" class="game-card" style="display:none;">
|
||||
<div class="game-label" id="queueLabel"></div>
|
||||
<div class="game-text" id="queueText"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktive Aufgabe (läuft) -->
|
||||
<div id="activeBox" class="game-card" style="display:none;">
|
||||
<div class="game-label">Aktive Aufgabe</div>
|
||||
<div class="game-text" id="activeText"></div>
|
||||
<div class="game-timer" id="activeTimer" style="display:none;"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Finisher -->
|
||||
<div id="finisherBox" style="display:none;">
|
||||
<div class="trophy">🏆</div>
|
||||
<h2>Level 6 erreicht!</h2>
|
||||
<div class="game-label" id="finisherTitle"></div>
|
||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
||||
<button class="btn-secondary" onclick="goBack()" style="margin-top:1.25rem;">Zurück zum Lock</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="loadingHint" style="text-align:center;color:var(--color-muted);padding:2rem 0;font-size:0.9rem;">
|
||||
Wird geladen…
|
||||
</div>
|
||||
<div id="errorBox" style="display:none;background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);
|
||||
border-radius:10px;padding:1rem;font-size:0.88rem;color:#e74c3c;margin-top:1rem;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const lockId = params.get('lockId');
|
||||
let _state = null;
|
||||
let _timerInt = null;
|
||||
let _pendingIsLock = false;
|
||||
let _pendingHasDuration = false;
|
||||
|
||||
function goBack() {
|
||||
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
||||
else history.back();
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async function boot() {
|
||||
try {
|
||||
const r = await fetch('/lock-game/state');
|
||||
if (r.status === 404) {
|
||||
await loadGroups();
|
||||
return;
|
||||
}
|
||||
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
||||
_state = await r.json();
|
||||
hide('loadingHint');
|
||||
await runGameLoop();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
const r = await fetch('/lock-game/groups');
|
||||
const groups = r.ok ? await r.json() : [];
|
||||
hide('loadingHint');
|
||||
const list = document.getElementById('groupList');
|
||||
if (groups.length === 0) {
|
||||
list.innerHTML = '<p style="font-size:0.85rem;color:var(--color-muted);">Keine passenden Aufgabengruppen gefunden.<br>Erstelle zuerst eine Gruppe vom Typ „Chastity Only".</p>';
|
||||
} else {
|
||||
list.innerHTML = groups.map(g => `
|
||||
<label class="group-item" onclick="selectGroup(this)">
|
||||
<input type="radio" name="gruppe" value="${g.gruppenId}" style="display:none;">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:0.95rem;">${esc(g.name)}</div>
|
||||
${g.beschreibung ? `<div style="font-size:0.8rem;color:var(--color-muted);">${esc(g.beschreibung)}</div>` : ''}
|
||||
</div>
|
||||
</label>`).join('');
|
||||
}
|
||||
show('initBox');
|
||||
}
|
||||
|
||||
function selectGroup(el) {
|
||||
document.querySelectorAll('.group-item').forEach(i => i.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
el.querySelector('input').checked = true;
|
||||
document.getElementById('btnStart').disabled = false;
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
const sel = document.querySelector('input[name="gruppe"]:checked');
|
||||
if (!sel) return;
|
||||
hide('initBox');
|
||||
show('loadingHint');
|
||||
try {
|
||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' });
|
||||
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
hide('loadingHint');
|
||||
await runGameLoop();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Game Loop ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function runGameLoop() {
|
||||
hide('queueBox');
|
||||
hide('activeBox');
|
||||
hide('finisherBox');
|
||||
clearTimer();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisher();
|
||||
return;
|
||||
}
|
||||
|
||||
renderLevelBar(_state.level);
|
||||
|
||||
// Aktive Aufgabe läuft noch
|
||||
if (_state.activeTask) {
|
||||
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue leer → nächsten Task holen
|
||||
if (!_state.taskInQueue && !_state.lockInQueue) {
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
}
|
||||
|
||||
showQueue();
|
||||
}
|
||||
|
||||
function showQueue() {
|
||||
if (_state.lockInQueue) {
|
||||
let sperre;
|
||||
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
||||
_pendingIsLock = true;
|
||||
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
|
||||
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
|
||||
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
|
||||
} else if (_state.taskInQueue) {
|
||||
let aufgabe;
|
||||
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
||||
_pendingIsLock = false;
|
||||
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe';
|
||||
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
|
||||
}
|
||||
show('gameBox');
|
||||
show('queueBox');
|
||||
}
|
||||
|
||||
function showActiveTask(text, endIso) {
|
||||
document.getElementById('activeText').textContent = text;
|
||||
show('gameBox');
|
||||
show('activeBox');
|
||||
|
||||
if (endIso) {
|
||||
const end = new Date(endIso);
|
||||
startTimer(end, document.getElementById('activeTimer'));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
// Locks prüfen (nach jeder Aktion)
|
||||
await checkAndShowLocks();
|
||||
|
||||
if (_pendingIsLock || _pendingHasDuration) {
|
||||
// Task/Sperre aktivieren und Timer starten
|
||||
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||||
if (!r.ok) { showError('Fehler'); return; }
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
} else {
|
||||
// Keine Dauer → sofort nächsten Task
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
}
|
||||
await runGameLoop();
|
||||
}
|
||||
|
||||
async function handleErledigt() {
|
||||
await checkAndShowLocks();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisher();
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
await runGameLoop();
|
||||
}
|
||||
|
||||
async function checkAndShowLocks() {
|
||||
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||
if (!r.ok) return;
|
||||
const texts = await r.json();
|
||||
if (texts && texts.length > 0) {
|
||||
const box = document.getElementById('lockMessages');
|
||||
box.innerHTML = texts.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
|
||||
show('lockMessages');
|
||||
await new Promise(res => setTimeout(res, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
async function showFinisher() {
|
||||
show('gameBox');
|
||||
const r = await fetch('/lock-game/finisher');
|
||||
if (!r.ok) {
|
||||
document.getElementById('finisherTitle').textContent = '';
|
||||
document.getElementById('finisherText').textContent = 'Glückwunsch – du hast Level 6 erreicht!';
|
||||
} else {
|
||||
const f = await r.json();
|
||||
document.getElementById('finisherTitle').textContent = f.kurzText || '';
|
||||
document.getElementById('finisherText').textContent = f.text || '';
|
||||
}
|
||||
show('finisherBox');
|
||||
}
|
||||
|
||||
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||||
|
||||
function renderLevelBar(level) {
|
||||
const bar = document.getElementById('levelBar');
|
||||
const dots = document.getElementById('levelDots');
|
||||
dots.innerHTML = [1,2,3,4,5].map(i =>
|
||||
`<div class="game-level-dot${i <= level ? ' active' : ''}"></div>`
|
||||
).join('');
|
||||
document.getElementById('levelText').textContent = 'Level ' + Math.min(level, 5);
|
||||
show('levelBar');
|
||||
}
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function startTimer(endDate, el) {
|
||||
el.style.display = '';
|
||||
clearTimer();
|
||||
_timerInt = setInterval(() => {
|
||||
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||
const m = String(Math.floor(diff / 60)).padStart(2, '0');
|
||||
const s = String(diff % 60).padStart(2, '0');
|
||||
el.textContent = m + ':' + s;
|
||||
el.classList.toggle('urgent', diff < 30);
|
||||
if (diff === 0) clearTimer();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
if (_timerInt) { clearInterval(_timerInt); _timerInt = null; }
|
||||
}
|
||||
|
||||
// ── Hilfsfunktionen ───────────────────────────────────────────────────────
|
||||
|
||||
function show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
|
||||
function hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
||||
function esc(s) { return String(s).replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function showError(msg) {
|
||||
hide('loadingHint');
|
||||
const box = document.getElementById('errorBox');
|
||||
box.textContent = msg || 'Ein Fehler ist aufgetreten.';
|
||||
box.style.display = '';
|
||||
}
|
||||
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
bin/main/static/img/card_game.png
Normal file
BIN
bin/main/static/img/card_game.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 370 KiB |
BIN
bin/main/static/img/card_slowmo.png
Normal file
BIN
bin/main/static/img/card_slowmo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
BIN
bin/main/static/img/card_speedup.png
Normal file
BIN
bin/main/static/img/card_speedup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 381 KiB |
@@ -3,12 +3,12 @@ package de.oaa.xxx.games.bdsm;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class AufgabeAnzeige {
|
||||
|
||||
private String nameAktiverMitspieler;
|
||||
private String aufgabeText;
|
||||
private Integer timer;
|
||||
@@ -16,6 +16,7 @@ public class AufgabeAnzeige {
|
||||
private Integer level;
|
||||
private UUID mitspielerId;
|
||||
private boolean eigenesGeraet;
|
||||
private LocalDateTime aufgabeBis;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -109,6 +109,16 @@ public class AufgabenGruppeController {
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/chastity")
|
||||
public ResponseEntity<AufgabenGruppeList> getChastity(@RequestParam(required = false) String search, Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
String searchPattern = search != null && !search.isEmpty() ? "%" + search + "%" : null;
|
||||
AufgabenGruppeList list = new AufgabenGruppeList();
|
||||
list.setGruppen(gruppeRepository.listChastityWithUserAndSearch(userId, searchPattern, PageRequest.of(0, 100))
|
||||
.stream().map(AufgabenGruppeEntity::toAufgabenGruppeDisplay).toList());
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/own")
|
||||
public ResponseEntity<AufgabenGruppeList> getOwn(@RequestParam("userId") UUID userId) {
|
||||
AufgabenGruppeList list = new AufgabenGruppeList();
|
||||
|
||||
@@ -56,7 +56,7 @@ public class FinisherController {
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> create(@RequestBody Finisher finisher) {
|
||||
if (finisher.getKurzText() == null || finisher.getText() == null
|
||||
|| finisher.getGeschlecht() == null || finisher.getGruppeId() == null) {
|
||||
|| finisher.getGruppeId() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
AufgabenGruppeEntity gruppeEntity = gruppeRepository.findById(finisher.getGruppeId()).orElse(null);
|
||||
@@ -77,7 +77,7 @@ public class FinisherController {
|
||||
|
||||
@PutMapping("/{finisherId}")
|
||||
public ResponseEntity<Void> update(@PathVariable("finisherId") UUID finisherId, @RequestBody Finisher finisher) {
|
||||
if (finisher.getKurzText() == null || finisher.getText() == null || finisher.getGeschlecht() == null) {
|
||||
if (finisher.getKurzText() == null || finisher.getText() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
FinisherEntity entity = finisherRepository.findById(finisherId).orElse(null);
|
||||
|
||||
@@ -88,6 +88,9 @@ public class SperreController {
|
||||
entity.setReleaseText(sperre.getReleaseText());
|
||||
entity.setMinutenVon(sperre.getMinutenVon());
|
||||
entity.setMinutenBis(sperre.getMinutenBis());
|
||||
entity.setLevel(sperre.getLevel());
|
||||
entity.setTempUnlockBeforeRequired(sperre.getTempUnlockBeforeRequired());
|
||||
entity.setTempUnlockAfterRequired(sperre.getTempUnlockAfterRequired());
|
||||
entity.setSperreFuer(sperre.getSperreFuer());
|
||||
entity.setBenoetigteToys(resolveToys(sperre.getBenoetigteToys()));
|
||||
sperreRepository.save(entity);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public record GameState(
|
||||
UUID gameID,
|
||||
UUID userId,
|
||||
Integer level,
|
||||
String activeTask,
|
||||
LocalDateTime activeTaskEnd,
|
||||
String taskInQueue,
|
||||
String lockInQueue) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
|
||||
import de.oaa.xxx.games.common.aufgaben.AvailableIn;
|
||||
import de.oaa.xxx.games.common.repository.AufgabeRepository;
|
||||
import de.oaa.xxx.games.common.repository.AufgabenGruppeRepository;
|
||||
import de.oaa.xxx.games.common.repository.FinisherRepository;
|
||||
import de.oaa.xxx.games.common.repository.SperreRepository;
|
||||
import de.oaa.xxx.games.common.entity.AufgabeEntity;
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.games.common.entity.FinisherEntity;
|
||||
import de.oaa.xxx.games.common.entity.SperreEntity;
|
||||
import de.oaa.xxx.user.UserService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/lock-game")
|
||||
public class LockGameController {
|
||||
|
||||
private static final int AUFGABEN_PRO_LEVEL = 3;
|
||||
|
||||
private final LockGameRepository lockGameRepository;
|
||||
private final LockGameLockRepository lockGameLockRepository;
|
||||
private final AufgabenGruppeRepository aufgabenGruppeRepository;
|
||||
private final AufgabeRepository aufgabeRepository;
|
||||
private final SperreRepository sperreRepository;
|
||||
private final FinisherRepository finisherRepository;
|
||||
private final UserService userService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public LockGameController(LockGameRepository lockGameRepository,
|
||||
LockGameLockRepository lockGameLockRepository,
|
||||
AufgabenGruppeRepository aufgabenGruppeRepository,
|
||||
AufgabeRepository aufgabeRepository,
|
||||
SperreRepository sperreRepository,
|
||||
FinisherRepository finisherRepository,
|
||||
UserService userService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.lockGameRepository = lockGameRepository;
|
||||
this.lockGameLockRepository = lockGameLockRepository;
|
||||
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
|
||||
this.aufgabeRepository = aufgabeRepository;
|
||||
this.sperreRepository = sperreRepository;
|
||||
this.finisherRepository = finisherRepository;
|
||||
this.userService = userService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/** Verfügbare CHASTITY_ONLY-Gruppen des angemeldeten Users. */
|
||||
@GetMapping("/groups")
|
||||
public ResponseEntity<List<Map<String, Object>>> getGroups(Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
var gruppen = aufgabenGruppeRepository.findByUserId(userId).stream()
|
||||
.filter(g -> g.getAvailableIn() == AvailableIn.CHASTITY_ONLY)
|
||||
.map(g -> {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("gruppenId", g.getGruppenId().toString());
|
||||
m.put("name", g.getName());
|
||||
m.put("beschreibung", g.getBeschreibung() != null ? g.getBeschreibung() : "");
|
||||
return m;
|
||||
}).toList();
|
||||
return ResponseEntity.ok(gruppen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert (oder startet neu) ein Lock-Game für den angemeldeten User.
|
||||
* Lädt die angegebene Aufgabengruppe und baut daraus die AufgabenList.
|
||||
*/
|
||||
@PostMapping("/init")
|
||||
public ResponseEntity<?> init(@RequestParam UUID aufgabenGruppeId, Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
|
||||
var gruppeOpt = aufgabenGruppeRepository.findById(aufgabenGruppeId);
|
||||
if (gruppeOpt.isEmpty())
|
||||
return ResponseEntity.notFound().build();
|
||||
AufgabenGruppeEntity gruppe = gruppeOpt.get();
|
||||
if (gruppe.getUserId() != null && !gruppe.getUserId().equals(userId))
|
||||
return ResponseEntity.status(403).build();
|
||||
|
||||
var aufgaben = aufgabeRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream()
|
||||
.map(AufgabeEntity::toAufgabe).toList();
|
||||
var sperren = sperreRepository.findByAufgabenGruppeIn(List.of(gruppe)).stream()
|
||||
.map(SperreEntity::toSperre).toList();
|
||||
var finisher = finisherRepository.findByAufgabenGruppe(gruppe).stream()
|
||||
.map(FinisherEntity::toFinisher).toList();
|
||||
|
||||
AufgabenList list = new AufgabenList();
|
||||
list.setAufgaben(aufgaben);
|
||||
list.setSperren(sperren);
|
||||
list.setFinisher(finisher);
|
||||
list.setStrafen(List.of());
|
||||
|
||||
try {
|
||||
String aufgabenJson = objectMapper.writeValueAsString(list);
|
||||
|
||||
LockGameEntity game = lockGameRepository.findByUserId(userId).orElseGet(() -> {
|
||||
LockGameEntity g = new LockGameEntity();
|
||||
g.setGameId(UUID.randomUUID());
|
||||
g.setUserId(userId);
|
||||
return g;
|
||||
});
|
||||
|
||||
game.setAufgaben(aufgabenJson);
|
||||
game.setLevel(1);
|
||||
game.setAufgabenProLevel(AUFGABEN_PRO_LEVEL);
|
||||
game.setAufgabenAufAktuellemLevel(0);
|
||||
game.setZeitfaktorZeitstrafen(1.0);
|
||||
game.setSetupId(aufgabenGruppeId);
|
||||
game.setActiveTask(null);
|
||||
game.setActiveTaskEnd(null);
|
||||
game.setTaskInQueue(null);
|
||||
game.setLockInQueue(null);
|
||||
lockGameLockRepository.deleteAll(lockGameLockRepository.findByGameId(game.getGameId()));
|
||||
lockGameRepository.save(game);
|
||||
|
||||
return ResponseEntity.ok(Map.of("gameId", game.getGameId().toString()));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/state")
|
||||
public ResponseEntity<GameState> getState(Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
var opt = lockGameRepository.findByUserId(userId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
try {
|
||||
return ResponseEntity.ok(buildService(opt.get()).getGameState());
|
||||
} catch (Exception ex) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/next-task")
|
||||
public ResponseEntity<?> nextTask(Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
var opt = lockGameRepository.findByUserId(userId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
try {
|
||||
buildService(opt.get()).initNextTask();
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/apply-task")
|
||||
public ResponseEntity<Map<String, Object>> applyTask(Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
var opt = lockGameRepository.findByUserId(userId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
try {
|
||||
Integer seconds = buildService(opt.get()).applyTaskInQueue();
|
||||
return ResponseEntity.ok(Map.of("seconds", seconds != null ? seconds : 0));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/check-locks")
|
||||
public ResponseEntity<List<String>> checkLocks(Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
var opt = lockGameRepository.findByUserId(userId);
|
||||
if (opt.isEmpty()) return ResponseEntity.ok(List.of());
|
||||
try {
|
||||
return ResponseEntity.ok(buildService(opt.get()).checkLocks());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/finisher")
|
||||
public ResponseEntity<?> getFinisher(Principal principal) {
|
||||
UUID userId = userService.requireUser(principal).getUserId();
|
||||
var opt = lockGameRepository.findByUserId(userId);
|
||||
if (opt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
try {
|
||||
var finisher = buildService(opt.get()).getFinisher();
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("kurzText", finisher.getKurzText());
|
||||
result.put("text", finisher.getText());
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private LockGameService buildService(LockGameEntity entity) throws Exception {
|
||||
return new LockGameService(entity, lockGameRepository, lockGameLockRepository);
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,10 @@ public class LockGameEntity {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
private UUID sessionId;
|
||||
private UUID gameId;
|
||||
@Column(unique = true)
|
||||
private UUID userId;
|
||||
@OneToMany(mappedBy = "lockId", fetch = FetchType.EAGER)
|
||||
@OneToMany(mappedBy = "gameId", fetch = FetchType.EAGER)
|
||||
private List<LockGameLockEntity> activeLocks = new ArrayList<>();
|
||||
@Column
|
||||
private Integer aufgabenProLevel;
|
||||
@@ -38,9 +38,13 @@ public class LockGameEntity {
|
||||
@Column
|
||||
private Double zeitfaktorZeitstrafen;
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String activeTaskJson;
|
||||
private String activeTask;
|
||||
@Column
|
||||
private LocalDateTime taskStartedAt;
|
||||
private LocalDateTime activeTaskEnd;
|
||||
@Column
|
||||
private String taskInQueue;
|
||||
@Column
|
||||
private String lockInQueue;
|
||||
@Column
|
||||
private UUID setupId;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -28,7 +29,7 @@ public class LockGameLockEntity {
|
||||
@Column
|
||||
private UUID lockGameLockId;
|
||||
@Column
|
||||
private UUID lockId;
|
||||
private UUID gameId;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(targetClass = Werkzeug.class, fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "aktiveSperre_fuer", joinColumns = @JoinColumn(name = "aktiveSperreId"))
|
||||
@@ -36,4 +37,6 @@ public class LockGameLockEntity {
|
||||
private List<Werkzeug> lockFor = new ArrayList<>();
|
||||
@Column
|
||||
private String releaseText;
|
||||
@Column
|
||||
private LocalDateTime releaseTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface LockGameLockRepository extends JpaRepository<LockGameLockEntity, UUID> {
|
||||
|
||||
List<LockGameLockEntity> findByGameId(UUID gameId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface LockGameRepository extends JpaRepository<LockGameEntity, UUID>{
|
||||
|
||||
Optional<LockGameEntity> findByUserId(UUID userId);
|
||||
}
|
||||
@@ -1,27 +1,182 @@
|
||||
package de.oaa.xxx.games.chastity.common;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import de.oaa.xxx.games.common.aufgaben.Werkzeug;
|
||||
import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
|
||||
import de.oaa.xxx.games.common.aufgaben.Aufgabe;
|
||||
import de.oaa.xxx.games.common.aufgaben.AufgabenList;
|
||||
import de.oaa.xxx.games.common.aufgaben.Finisher;
|
||||
import de.oaa.xxx.games.common.aufgaben.Sperre;
|
||||
|
||||
@Service
|
||||
public class LockGameService {
|
||||
|
||||
// public LockGameService(LockGameEntity gamestate) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// public String getNextTask() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// public String getCurrentTask() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// public void applyLock(Werkzeug applyFor, LocalDateTime applyTill, ) {
|
||||
//
|
||||
// }
|
||||
private LockGameEntity gamestate;
|
||||
private LockGameRepository lockGameRepository;
|
||||
private LockGameLockRepository lockGameLockRepository;
|
||||
private AufgabenList aufgabenList;
|
||||
|
||||
public LockGameService(LockGameEntity gamestate, LockGameRepository lockGameRepository,
|
||||
LockGameLockRepository lockGameLockRepository) throws JsonMappingException, JsonProcessingException {
|
||||
this.gamestate = gamestate;
|
||||
this.lockGameRepository = lockGameRepository;
|
||||
this.lockGameLockRepository = lockGameLockRepository;
|
||||
|
||||
this.aufgabenList = new ObjectMapper().readValue(gamestate.getAufgaben(), AufgabenList.class);
|
||||
|
||||
}
|
||||
|
||||
public GameState getGameState() {
|
||||
return new GameState(gamestate.getGameId(), gamestate.getUserId(), gamestate.getLevel(),
|
||||
gamestate.getActiveTask(), gamestate.getActiveTaskEnd(), gamestate.getTaskInQueue(),
|
||||
gamestate.getLockInQueue());
|
||||
}
|
||||
|
||||
public void initNextTask() throws JsonProcessingException {
|
||||
checkLevel();
|
||||
String result = null;
|
||||
|
||||
if (gamestate.getLevel() <= 5) {
|
||||
int nextInt = new Random().nextInt(1, 100);
|
||||
|
||||
if (nextInt < 25) {
|
||||
result = findZeitstrafe();
|
||||
} else {
|
||||
result = findAufgabe();
|
||||
}
|
||||
if (result == null) {
|
||||
result = "Pause...";
|
||||
gamestate.setTaskInQueue(result);
|
||||
Aufgabe aufgabe = new Aufgabe();
|
||||
aufgabe.setText("Pause...");
|
||||
aufgabe.setSekundenVon(300);
|
||||
aufgabe.setSekundenBis(300);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe));
|
||||
}
|
||||
|
||||
lockGameRepository.save(gamestate);
|
||||
}
|
||||
}
|
||||
|
||||
private String findZeitstrafe() throws JsonProcessingException {
|
||||
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
|
||||
var level = gamestate.getLevel();
|
||||
while (level > 0) {
|
||||
var sperren = aufgabenList.getSperren().stream()
|
||||
.filter(sperre -> sperre.getSperreFuer().stream().noneMatch(item -> list.contains(item))).toList();
|
||||
if (!sperren.isEmpty()) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Sperre sperre = sperren.get(new Random().nextInt(sperren.size()));
|
||||
gamestate.setLockInQueue(mapper.writeValueAsString(sperre));
|
||||
return sperre.getText();
|
||||
}
|
||||
level--;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String findAufgabe() throws JsonProcessingException {
|
||||
var list = gamestate.getActiveLocks().stream().flatMap(lock -> lock.getLockFor().stream()).toList();
|
||||
var level = gamestate.getLevel();
|
||||
while (level > 0) {
|
||||
var aufgaben = aufgabenList.getAufgaben().stream()
|
||||
.filter(aufgabe -> aufgabe.getBenoetigtAktiv().stream().noneMatch(item -> list.contains(item)))
|
||||
.toList();
|
||||
if (!aufgaben.isEmpty()) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
var aufgabe = aufgaben.get(new Random().nextInt(aufgaben.size()));
|
||||
gamestate.setTaskInQueue(mapper.writeValueAsString(aufgabe));
|
||||
return aufgabe.getText();
|
||||
}
|
||||
level--;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Integer applyTaskInQueue() throws JsonMappingException, JsonProcessingException {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
if (gamestate.getTaskInQueue() != null) {
|
||||
var aufgabe = mapper.readValue(gamestate.getTaskInQueue(), Aufgabe.class);
|
||||
gamestate.setActiveTask(aufgabe.getText());
|
||||
gamestate.setTaskInQueue(null);
|
||||
var time = getAufgabeTime(aufgabe);
|
||||
gamestate.setActiveTaskEnd(LocalDateTime.now().plusSeconds(time));
|
||||
return time;
|
||||
} else if (gamestate.getLockInQueue() != null) {
|
||||
var lock = mapper.readValue(gamestate.getLockInQueue(), Sperre.class);
|
||||
gamestate.setActiveTask(lock.getText());
|
||||
gamestate.setLockInQueue(null);
|
||||
applyLock(lock);
|
||||
}
|
||||
lockGameRepository.save(gamestate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private Integer getLockTime(Sperre sperre) {
|
||||
var time = new Random().nextInt(sperre.getMinutenVon(), sperre.getMinutenBis());
|
||||
return (int) (time * gamestate.getZeitfaktorZeitstrafen());
|
||||
}
|
||||
|
||||
private Integer getAufgabeTime(Aufgabe aufgabe) {
|
||||
var time = new Random().nextInt(aufgabe.getSekundenVon(), aufgabe.getSekundenBis());
|
||||
return (int) (time * gamestate.getZeitfaktorZeitstrafen());
|
||||
}
|
||||
|
||||
protected void checkLevel() {
|
||||
var aufgabenAufAktuellemLevel = gamestate.getAufgabenAufAktuellemLevel();
|
||||
if (++aufgabenAufAktuellemLevel >= 1 + gamestate.getAufgabenProLevel()) {
|
||||
aufgabenAufAktuellemLevel = 0;
|
||||
gamestate.setLevel(gamestate.getLevel() + 1);
|
||||
}
|
||||
gamestate.setAufgabenAufAktuellemLevel(aufgabenAufAktuellemLevel);
|
||||
}
|
||||
|
||||
public AufgabeAnzeige getCurrentTask() {
|
||||
AufgabeAnzeige anzeige = new AufgabeAnzeige();
|
||||
anzeige.setAufgabeText(gamestate.getActiveTask());
|
||||
anzeige.setAufgabeBis(gamestate.getActiveTaskEnd());
|
||||
return anzeige;
|
||||
}
|
||||
|
||||
public void applyLock(Sperre lock) {
|
||||
var entity = new LockGameLockEntity();
|
||||
entity.setLockGameLockId(UUID.randomUUID());
|
||||
entity.setGameId(gamestate.getGameId());
|
||||
entity.setLockFor(lock.getSperreFuer());
|
||||
entity.setReleaseText(lock.getReleaseText());
|
||||
entity.setReleaseTime(LocalDateTime.now().plusMinutes(getLockTime(lock)));
|
||||
lockGameLockRepository.save(entity);
|
||||
}
|
||||
|
||||
public List<String> checkLocks() {
|
||||
var result = new ArrayList<String>();
|
||||
for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) {
|
||||
if (entity.getReleaseTime().isAfter(LocalDateTime.now())) {
|
||||
result.add(entity.getReleaseText());
|
||||
lockGameLockRepository.delete(entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<String> releaseLocks() {
|
||||
var result = new ArrayList<String>();
|
||||
for (LockGameLockEntity entity : lockGameLockRepository.findByGameId(gamestate.getGameId())) {
|
||||
result.add(entity.getReleaseText());
|
||||
lockGameLockRepository.delete(entity);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Finisher getFinisher() {
|
||||
return aufgabenList.getFinisher().get(new Random().nextInt(aufgabenList.getFinisher().size()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ public class Sperre {
|
||||
private List<Werkzeug> sperreFuer;
|
||||
private Integer minutenVon;
|
||||
private Integer minutenBis;
|
||||
private Integer level;
|
||||
private Boolean tempUnlockBeforeRequired;
|
||||
private Boolean tempUnlockAfterRequired;
|
||||
private List<Toy> benoetigteToys;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -50,6 +50,12 @@ public class SperreEntity {
|
||||
private Integer minutenVon;
|
||||
@Column
|
||||
private Integer minutenBis;
|
||||
@Column
|
||||
private Integer level;
|
||||
@Column
|
||||
private Boolean tempUnlockBeforeRequired;
|
||||
@Column
|
||||
private Boolean tempUnlockAfterRequired;
|
||||
@ManyToMany(cascade = CascadeType.DETACH)
|
||||
@JoinTable(name = "sperreToy", joinColumns = {@JoinColumn(name = "sperreId")}, inverseJoinColumns = {@JoinColumn(name = "toyId")})
|
||||
private List<ToyEntity> benoetigteToys;
|
||||
@@ -67,6 +73,9 @@ public class SperreEntity {
|
||||
sperre.setKurzText(kurzText);
|
||||
sperre.setMinutenBis(minutenBis);
|
||||
sperre.setMinutenVon(minutenVon);
|
||||
sperre.setLevel(level);
|
||||
sperre.setTempUnlockBeforeRequired(tempUnlockBeforeRequired);
|
||||
sperre.setTempUnlockAfterRequired(tempUnlockAfterRequired);
|
||||
sperre.setReleaseText(releaseText);
|
||||
sperre.setSperreFuer(sperreFuer != null ? new ArrayList<>(sperreFuer) : new ArrayList<>());
|
||||
sperre.setText(text);
|
||||
@@ -82,6 +91,9 @@ public class SperreEntity {
|
||||
entity.setKurzText(sperre.getKurzText());
|
||||
entity.setMinutenBis(sperre.getMinutenBis());
|
||||
entity.setMinutenVon(sperre.getMinutenVon());
|
||||
entity.setLevel(sperre.getLevel());
|
||||
entity.setTempUnlockBeforeRequired(sperre.getTempUnlockBeforeRequired());
|
||||
entity.setTempUnlockAfterRequired(sperre.getTempUnlockAfterRequired());
|
||||
entity.setReleaseText(sperre.getReleaseText());
|
||||
entity.setSperreFuer(sperre.getSperreFuer());
|
||||
entity.setText(sperre.getText());
|
||||
|
||||
@@ -49,4 +49,8 @@ public interface AufgabenGruppeRepository extends JpaRepository<AufgabenGruppeEn
|
||||
/** Alle Gruppen – für BDSM-Spielstart (availableIn wird im Frontend hervorgehoben). */
|
||||
@Query("select g from AufgabenGruppeEntity g where (g.privateGruppe = false or g.userId = :userId) and (:search is null or g.name like :search)")
|
||||
List<AufgabenGruppeEntity> listAllWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||
|
||||
/** Nur CHASTITY_ONLY-Gruppen – für Spiel-Karte Aufgaben-Set-Auswahl. */
|
||||
@Query("select g from AufgabenGruppeEntity g where g.availableIn = de.oaa.xxx.games.common.aufgaben.AvailableIn.CHASTITY_ONLY and (g.privateGruppe = false or g.userId = :userId) and (:search is null or g.name like :search)")
|
||||
List<AufgabenGruppeEntity> listChastityWithUserAndSearch(@Param("userId") UUID userId, @Param("search") String search, PageRequest pageable);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ package de.oaa.xxx.games.common.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import de.oaa.xxx.games.common.entity.AufgabenGruppeEntity;
|
||||
import de.oaa.xxx.games.common.entity.FinisherEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FinisherRepository extends JpaRepository<FinisherEntity, UUID> {
|
||||
|
||||
List<FinisherEntity> findByAufgabenGruppe(AufgabenGruppeEntity gruppe);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
-- Migration: Inhalte aus chastity_game_set in die normalen Aufgabengruppen-Tabellen übernehmen.
|
||||
--
|
||||
-- Jedes chastity_game_set eines Users wird zu einer privaten CHASTITY_ONLY-Aufgabengruppe:
|
||||
-- aufgaben (JSON) → aufgabe + aufgabe_benoetigt_passiv
|
||||
-- zeitstrafen (JSON) → sperre + sperre_sperre_fuer
|
||||
-- finisher (JSON) → finisher
|
||||
--
|
||||
-- Die Prozedur prüft zuerst, ob chastity_game_set existiert – auf leeren Datenbanken
|
||||
-- ist sie dadurch ein No-op.
|
||||
|
||||
DROP PROCEDURE IF EXISTS proc_migrate_chastity_game_set;
|
||||
|
||||
CREATE PROCEDURE proc_migrate_chastity_game_set()
|
||||
BEGIN
|
||||
DECLARE v_set_id VARCHAR(36);
|
||||
DECLARE v_owner_id VARCHAR(36);
|
||||
DECLARE v_set_name VARCHAR(255);
|
||||
DECLARE v_user_name VARCHAR(255);
|
||||
DECLARE v_aufgaben_json TEXT;
|
||||
DECLARE v_zeitstr_json TEXT;
|
||||
DECLARE v_finisher_json TEXT;
|
||||
|
||||
DECLARE v_gruppe_id VARCHAR(36);
|
||||
DECLARE v_aufgabe_id VARCHAR(36);
|
||||
DECLARE v_sperre_id VARCHAR(36);
|
||||
DECLARE v_finisher_id VARCHAR(36);
|
||||
|
||||
DECLARE v_outer_count INT;
|
||||
DECLARE v_inner_count INT;
|
||||
DECLARE i INT;
|
||||
DECLARE j INT;
|
||||
|
||||
DECLARE tbl_exists INT DEFAULT 0;
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
|
||||
DECLARE cur CURSOR FOR
|
||||
SELECT id, owner_id, name, aufgaben, zeitstrafen, finisher
|
||||
FROM chastity_game_set;
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
-- Add columns that may not yet exist (Hibernate ddl-auto runs after Flyway)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'aufgabe' AND COLUMN_NAME = 'level') THEN
|
||||
ALTER TABLE aufgabe ADD COLUMN level INT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sperre' AND COLUMN_NAME = 'level') THEN
|
||||
ALTER TABLE sperre ADD COLUMN level INT NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sperre' AND COLUMN_NAME = 'temp_unlock_before_required') THEN
|
||||
ALTER TABLE sperre ADD COLUMN temp_unlock_before_required TINYINT(1) NULL;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sperre' AND COLUMN_NAME = 'temp_unlock_after_required') THEN
|
||||
ALTER TABLE sperre ADD COLUMN temp_unlock_after_required TINYINT(1) NULL;
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO tbl_exists
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'chastity_game_set';
|
||||
|
||||
IF tbl_exists > 0 THEN
|
||||
|
||||
OPEN cur;
|
||||
|
||||
set_loop: LOOP
|
||||
FETCH cur INTO v_set_id, v_owner_id, v_set_name,
|
||||
v_aufgaben_json, v_zeitstr_json, v_finisher_json;
|
||||
IF done THEN LEAVE set_loop; END IF;
|
||||
|
||||
SELECT name INTO v_user_name
|
||||
FROM user
|
||||
WHERE user_id = v_owner_id
|
||||
LIMIT 1;
|
||||
|
||||
SET v_gruppe_id = UUID();
|
||||
|
||||
-- ── Aufgabengruppe anlegen ───────────────────────────────────────
|
||||
INSERT INTO aufgaben_gruppe
|
||||
(gruppen_id, name, beschreibung, user_id, private_gruppe, bild, von, available_in)
|
||||
VALUES
|
||||
(v_gruppe_id,
|
||||
v_set_name,
|
||||
'Importiert aus Chastity Game Set',
|
||||
v_owner_id,
|
||||
TRUE,
|
||||
NULL,
|
||||
v_user_name,
|
||||
'CHASTITY_ONLY');
|
||||
|
||||
-- ── Aufgaben ────────────────────────────────────────────────────
|
||||
-- GameSetAufgabe.minutes → sekunden_von / sekunden_bis (× 60)
|
||||
-- GameSetAufgabe.benoetigt → aufgabe_benoetigt_passiv
|
||||
SET v_outer_count = IFNULL(JSON_LENGTH(v_aufgaben_json), 0);
|
||||
SET i = 0;
|
||||
WHILE i < v_outer_count DO
|
||||
SET v_aufgabe_id = UUID();
|
||||
|
||||
INSERT INTO aufgabe
|
||||
(aufgabe_id, kurz_text, text, level, sekunden_von, sekunden_bis, gruppe_id)
|
||||
VALUES (
|
||||
v_aufgabe_id,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].title'))),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].description'))),
|
||||
CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].level'))), 'null') AS SIGNED),
|
||||
CASE
|
||||
WHEN JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].minutes')) IS NOT NULL
|
||||
THEN CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].minutes'))), 'null') AS SIGNED) * 60
|
||||
ELSE NULL
|
||||
END,
|
||||
CASE
|
||||
WHEN JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].minutes')) IS NOT NULL
|
||||
THEN CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].minutes'))), 'null') AS SIGNED) * 60
|
||||
ELSE NULL
|
||||
END,
|
||||
v_gruppe_id
|
||||
);
|
||||
|
||||
SET v_inner_count = IFNULL(
|
||||
JSON_LENGTH(JSON_EXTRACT(v_aufgaben_json, CONCAT('$[', i, '].benoetigt'))),
|
||||
0);
|
||||
SET j = 0;
|
||||
WHILE j < v_inner_count DO
|
||||
SET @wz = NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_aufgaben_json,
|
||||
CONCAT('$[', i, '].benoetigt[', j, ']'))), 'null');
|
||||
IF @wz IS NOT NULL AND @wz != '' THEN
|
||||
INSERT IGNORE INTO aufgabe_benoetigt_passiv (aufgabe_id, werkzeug)
|
||||
VALUES (v_aufgabe_id, @wz);
|
||||
END IF;
|
||||
SET j = j + 1;
|
||||
END WHILE;
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
|
||||
-- ── Zeitstrafen → Sperren ────────────────────────────────────────
|
||||
-- GameSetZeitstrafe.minMinutes / maxMinutes → minuten_von / minuten_bis
|
||||
-- GameSetZeitstrafe.level / tempUnlock* → level / temp_unlock_before/after_required
|
||||
-- GameSetZeitstrafe.sperrt → sperre_sperre_fuer
|
||||
SET v_outer_count = IFNULL(JSON_LENGTH(v_zeitstr_json), 0);
|
||||
SET i = 0;
|
||||
WHILE i < v_outer_count DO
|
||||
SET v_sperre_id = UUID();
|
||||
|
||||
INSERT INTO sperre
|
||||
(sperre_id, kurz_text, text, release_text, minuten_von, minuten_bis,
|
||||
level, temp_unlock_before_required, temp_unlock_after_required, gruppe_id)
|
||||
VALUES (
|
||||
v_sperre_id,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].title'))),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].description'))),
|
||||
IF(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].releaseText')) IS NOT NULL,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].releaseText'))),
|
||||
NULL),
|
||||
CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].minMinutes'))), 'null') AS SIGNED),
|
||||
CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].maxMinutes'))), 'null') AS SIGNED),
|
||||
CAST(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].level'))), 'null') AS SIGNED),
|
||||
CASE JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].tempUnlockBeforeRequired')))
|
||||
WHEN 'true' THEN 1 WHEN 'false' THEN 0 ELSE NULL END,
|
||||
CASE JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].tempUnlockAfterRequired')))
|
||||
WHEN 'true' THEN 1 WHEN 'false' THEN 0 ELSE NULL END,
|
||||
v_gruppe_id
|
||||
);
|
||||
|
||||
SET v_inner_count = IFNULL(
|
||||
JSON_LENGTH(JSON_EXTRACT(v_zeitstr_json, CONCAT('$[', i, '].sperrt'))),
|
||||
0);
|
||||
SET j = 0;
|
||||
WHILE j < v_inner_count DO
|
||||
SET @wz = NULLIF(JSON_UNQUOTE(JSON_EXTRACT(v_zeitstr_json,
|
||||
CONCAT('$[', i, '].sperrt[', j, ']'))), 'null');
|
||||
IF @wz IS NOT NULL AND @wz != '' THEN
|
||||
INSERT IGNORE INTO sperre_sperre_fuer (sperre_id, werkzeug)
|
||||
VALUES (v_sperre_id, @wz);
|
||||
END IF;
|
||||
SET j = j + 1;
|
||||
END WHILE;
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
|
||||
-- ── Finisher ─────────────────────────────────────────────────────
|
||||
-- GameSetFinisher hat kein geschlecht und keine benoetigt-Listen
|
||||
-- tempUnlockBeforeRequired / tempUnlockAfterRequired haben kein Gegenstück in finisher
|
||||
SET v_outer_count = IFNULL(JSON_LENGTH(v_finisher_json), 0);
|
||||
SET i = 0;
|
||||
WHILE i < v_outer_count DO
|
||||
SET v_finisher_id = UUID();
|
||||
|
||||
INSERT INTO finisher
|
||||
(finisher_id, kurz_text, text, geschlecht, gruppe_id)
|
||||
VALUES (
|
||||
v_finisher_id,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_finisher_json, CONCAT('$[', i, '].title'))),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(v_finisher_json, CONCAT('$[', i, '].description'))),
|
||||
NULL,
|
||||
v_gruppe_id
|
||||
);
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
|
||||
END LOOP;
|
||||
|
||||
CLOSE cur;
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
CALL proc_migrate_chastity_game_set();
|
||||
|
||||
DROP PROCEDURE IF EXISTS proc_migrate_chastity_game_set;
|
||||
@@ -416,7 +416,7 @@
|
||||
|
||||
<label class="label-with-hint">
|
||||
<span>Beschreibung *</span>
|
||||
<button type="button" class="btn-hint" onclick="togglePlaceholderHint()" title="Platzhalter-Hilfe">i</button>
|
||||
<button type="button" id="iPlaceholderHintBtn" class="btn-hint" onclick="togglePlaceholderHint()" title="Platzhalter-Hilfe">i</button>
|
||||
</label>
|
||||
<div id="iPlaceholderHint" style="display:none;">
|
||||
<div class="placeholder-hint">
|
||||
@@ -451,15 +451,17 @@
|
||||
<div id="iLevelRow">
|
||||
<label for="iLevel">Level *</label>
|
||||
<input type="number" id="iLevel" min="1" max="5" placeholder="1–5">
|
||||
<label>Dauer (Sekunden)</label>
|
||||
<div class="modal-two-col">
|
||||
<div>
|
||||
<label for="iSekVon" style="margin-top:0;">Von</label>
|
||||
<input type="number" id="iSekVon" min="0" placeholder="z. B. 30">
|
||||
</div>
|
||||
<div>
|
||||
<label for="iSekBis" style="margin-top:0;">Bis</label>
|
||||
<input type="number" id="iSekBis" min="0" placeholder="z. B. 120">
|
||||
<div id="iSekRow">
|
||||
<label>Dauer (Sekunden)</label>
|
||||
<div class="modal-two-col">
|
||||
<div>
|
||||
<label for="iSekVon" style="margin-top:0;">Von</label>
|
||||
<input type="number" id="iSekVon" min="0" placeholder="z. B. 30">
|
||||
</div>
|
||||
<div>
|
||||
<label for="iSekBis" style="margin-top:0;">Bis</label>
|
||||
<input type="number" id="iSekBis" min="0" placeholder="z. B. 120">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -530,6 +532,19 @@
|
||||
<label class="werkzeug-check"><span>Anus</span><input type="checkbox" value="ANUS"></label>
|
||||
</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>
|
||||
</div>
|
||||
<div id="iReleaseTextRow">
|
||||
<label for="iReleaseText">Text bei Aufhebung</label>
|
||||
<textarea id="iReleaseText" rows="2" maxlength="2000" placeholder="Text der angezeigt wird, wenn die Sperre endet…"></textarea>
|
||||
@@ -749,7 +764,7 @@
|
||||
<div class="gruppe-body" id="body-${esc(g.gruppenId)}" style="display:none;">
|
||||
${g.beschreibung ? `<div class="gruppe-desc">${esc(g.beschreibung)}</div>` : ''}
|
||||
${renderSubSection('Aufgaben', sortByLevelThenName(g.aufgaben || []), 'aufgabe', renderAufgabe, g.gruppenId, type)}
|
||||
${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('Finisher', sortByGeschlecht(g.finisher || []), 'finisher', renderFinisher, g.gruppenId, type)}
|
||||
</div>
|
||||
@@ -1086,7 +1101,9 @@
|
||||
pubCb.checked = !g.privateGruppe;
|
||||
pubCb.disabled = g.privateGruppe; // Veröffentlichen nur über den Veröffentlichen-Button
|
||||
document.getElementById('gPublicLabel').style.display = 'block';
|
||||
document.getElementById('gAvailableIn').value = g.availableIn || 'BDSM_ONLY';
|
||||
const avSel = document.getElementById('gAvailableIn');
|
||||
avSel.value = g.availableIn || 'BDSM_ONLY';
|
||||
avSel.disabled = true;
|
||||
const imgWrap = document.getElementById('gCurrentImgWrap');
|
||||
if (g.bild) {
|
||||
document.getElementById('gCurrentImg').src = 'data:image/png;base64,' + g.bild;
|
||||
@@ -1104,7 +1121,8 @@
|
||||
document.getElementById('gDesc').value = '';
|
||||
document.getElementById('gPublic').checked = false;
|
||||
document.getElementById('gPublicLabel').style.display = 'none';
|
||||
document.getElementById('gAvailableIn').value = 'BDSM_ONLY';
|
||||
document.getElementById('gAvailableIn').value = 'BDSM_ONLY';
|
||||
document.getElementById('gAvailableIn').disabled = false;
|
||||
document.getElementById('gCurrentImgWrap').style.display = 'none';
|
||||
gruppeModal.classList.add('open');
|
||||
document.getElementById('gName').focus();
|
||||
@@ -1113,7 +1131,8 @@
|
||||
|
||||
function closeGruppeModal() {
|
||||
gruppeModal.classList.remove('open');
|
||||
document.getElementById('gPublic').disabled = false;
|
||||
document.getElementById('gPublic').disabled = false;
|
||||
document.getElementById('gAvailableIn').disabled = false;
|
||||
}
|
||||
|
||||
document.getElementById('openCreateBtn').addEventListener('click', () => openModal(null));
|
||||
@@ -1359,6 +1378,7 @@
|
||||
let currentItemGruppeId = null;
|
||||
let currentItemKind = null; // 'aufgabe' | 'strafe' | 'zeitstrafe'
|
||||
let currentItemEditId = null; // null = neu, sonst ID des zu bearbeitenden Items
|
||||
let _isChastityMode = false;
|
||||
|
||||
const ITEM_TITLES_NEW = { aufgabe: 'Aufgabe hinzufügen', strafe: 'Strafe hinzufügen', zeitstrafe: 'Zeitstrafe hinzufügen', finisher: 'Finisher hinzufügen' };
|
||||
const ITEM_TITLES_EDIT = { aufgabe: 'Aufgabe bearbeiten', strafe: 'Strafe bearbeiten', zeitstrafe: 'Zeitstrafe bearbeiten', finisher: 'Finisher bearbeiten' };
|
||||
@@ -1366,6 +1386,11 @@
|
||||
function _setupItemModal(kind) {
|
||||
const isZeit = kind === 'zeitstrafe';
|
||||
const isFinisher = kind === 'finisher';
|
||||
const isChastity = (_gruppeData[currentItemGruppeId]?.availableIn === 'CHASTITY_ONLY');
|
||||
_isChastityMode = isChastity;
|
||||
|
||||
// Placeholder hint button + text
|
||||
document.getElementById('iPlaceholderHintBtn').style.display = isChastity ? 'none' : '';
|
||||
document.querySelector('#iPlaceholderHint .placeholder-hint').innerHTML =
|
||||
isFinisher
|
||||
? 'In Texten können Platzhalter verwendet werden:<br>' +
|
||||
@@ -1374,15 +1399,39 @@
|
||||
: 'In Texten können Platzhalter verwendet werden:<br>' +
|
||||
'<code>{AKTIV}</code> – Name des aktiven Parts<br>' +
|
||||
'<code>{PASSIV}</code> – Name des passiven Parts';
|
||||
document.getElementById('iGeschlechtRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherAktivRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherPassivRow').style.display = isFinisher ? 'block' : 'none';
|
||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
|
||||
// Geschlecht: finisher only, and not in chastity
|
||||
document.getElementById('iGeschlechtRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Level row: aufgabe/strafe always; zeitstrafe only in chastity
|
||||
document.getElementById('iLevelRow').style.display = (!isZeit && !isFinisher) || (isZeit && isChastity) ? 'block' : 'none';
|
||||
// Sekunden sub-section: not for chastity zeitstrafe
|
||||
document.getElementById('iSekRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
|
||||
// Aktiv Werkzeuge: aufgabe/strafe only
|
||||
document.getElementById('iWerkzeugAktivRow').style.display = (!isZeit && !isFinisher) ? 'block' : 'none';
|
||||
document.querySelector('#iWerkzeugAktivRow label').textContent = isChastity ? 'Benötigt' : 'Benötigt (aktiv)';
|
||||
['VAGINA', 'PENIS', 'UMSCHNALLDILDO'].forEach(v => {
|
||||
const lbl = document.querySelector(`#iWerkzeugAktiv input[value="${v}"]`)?.closest('label');
|
||||
if (lbl) lbl.style.display = isChastity ? 'none' : '';
|
||||
});
|
||||
|
||||
// Passiv Werkzeuge: aufgabe/strafe, not in chastity
|
||||
document.getElementById('iWerkzeugPassivRow').style.display = (!isZeit && !isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Finisher Werkzeuge: not in chastity
|
||||
document.getElementById('iWerkzeugFinisherAktivRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
document.getElementById('iWerkzeugFinisherPassivRow').style.display = (isFinisher && !isChastity) ? 'block' : 'none';
|
||||
|
||||
// Zeitstrafe rows
|
||||
document.getElementById('iMinutenRow').style.display = isZeit ? 'block' : 'none';
|
||||
document.getElementById('iSperreFuerRow').style.display = isZeit ? 'block' : 'none';
|
||||
['VAGINA', 'PENIS'].forEach(v => {
|
||||
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('iReleaseTextRow').style.display = isZeit ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function _resetItemFields() {
|
||||
@@ -1400,6 +1449,8 @@
|
||||
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;
|
||||
_selectedToys = [];
|
||||
renderSelectedToys();
|
||||
document.getElementById('itemModalError').style.display = 'none';
|
||||
@@ -1449,6 +1500,11 @@
|
||||
document.getElementById('iMinBis').value = d.minutenBis != null ? d.minutenBis : '';
|
||||
document.getElementById('iReleaseText').value = d.releaseText || '';
|
||||
(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;
|
||||
}
|
||||
}
|
||||
|
||||
const preSelected = (d.benoetigteToys || []).filter(t => t.toyId);
|
||||
@@ -1480,7 +1536,7 @@
|
||||
}
|
||||
function buildItems(filter) {
|
||||
const f = filter || '';
|
||||
_items = STATIC.filter(s => !f || s.toLowerCase().includes(f)).map(s => ({ label: s, insert: s }));
|
||||
_items = (_isChastityMode ? [] : STATIC.filter(s => !f || s.toLowerCase().includes(f))).map(s => ({ label: s, insert: s }));
|
||||
const toys = _allToys.filter(t => !f || t.name.toLowerCase().includes(f));
|
||||
if (toys.length) {
|
||||
_items.push({ separator: true, label: 'Toys' });
|
||||
@@ -1606,13 +1662,14 @@
|
||||
method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
} else if (kind === 'finisher') {
|
||||
const geschlecht = document.querySelector('#iGeschlecht input:checked')?.value;
|
||||
if (!geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||
const geschlecht = _isChastityMode ? null : document.querySelector('#iGeschlecht input:checked')?.value;
|
||||
if (!_isChastityMode && !geschlecht) { showItemError('Bitte ein Geschlecht auswählen.'); return; }
|
||||
payload = {
|
||||
kurzText, text, geschlecht,
|
||||
kurzText, text,
|
||||
geschlecht: geschlecht || null,
|
||||
gruppeId: isEdit ? undefined : currentItemGruppeId,
|
||||
benoetigtAktiv: checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigtAktiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherAktiv'),
|
||||
benoetigtPassiv: _isChastityMode ? [] : checkedValues('iWerkzeugFinisherPassiv'),
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? apiUrl(`/finisher/${currentItemEditId}`) : apiUrl('/finisher');
|
||||
@@ -1624,6 +1681,14 @@
|
||||
const sperreFuer = checkedValues('iSperreFuer');
|
||||
if (sperreFuer.length === 0) { showItemError('Bitte mindestens ein Werkzeug für die Sperre auswählen.'); return; }
|
||||
|
||||
let zeitLevel = null;
|
||||
if (_isChastityMode) {
|
||||
const lv = document.getElementById('iLevel').value.trim();
|
||||
if (!lv) { showItemError('Bitte ein Level angeben.'); return; }
|
||||
zeitLevel = parseInt(lv, 10);
|
||||
if (isNaN(zeitLevel) || zeitLevel < 1 || zeitLevel > 5) { showItemError('Level muss zwischen 1 und 5 liegen.'); return; }
|
||||
}
|
||||
|
||||
const minBis = document.getElementById('iMinBis').value.trim();
|
||||
payload = {
|
||||
kurzText, text,
|
||||
@@ -1632,6 +1697,9 @@
|
||||
minutenBis: minBis ? parseInt(minBis, 10) : null,
|
||||
releaseText: document.getElementById('iReleaseText').value.trim() || null,
|
||||
sperreFuer,
|
||||
level: zeitLevel,
|
||||
tempUnlockBeforeRequired: _isChastityMode ? document.getElementById('iTempUnlockBefore').checked : null,
|
||||
tempUnlockAfterRequired: _isChastityMode ? document.getElementById('iTempUnlockAfter').checked : null,
|
||||
benoetigteToys: _selectedToys.map(t => ({ toyId: t.toyId }))
|
||||
};
|
||||
url = isEdit ? `/sperre/${currentItemEditId}` : '/sperre'; // BDSM-only
|
||||
|
||||
@@ -1711,6 +1711,14 @@
|
||||
document.getElementById('btnDrawOk').style.display = 'none';
|
||||
document.getElementById('btnSpeedConfirm').style.display = '';
|
||||
}
|
||||
|
||||
if (dto.card === 'GAME_CARD') {
|
||||
const btn = document.getElementById('btnDrawOk');
|
||||
btn.textContent = '▶ Spiel starten';
|
||||
btn.onclick = function() {
|
||||
window.location.href = '/games/chastity/taskgame.html?lockId=' + lockId;
|
||||
};
|
||||
}
|
||||
}, 700);
|
||||
}, 1000);
|
||||
})
|
||||
|
||||
@@ -260,6 +260,33 @@
|
||||
.radio-group label { display:flex; align-items:center; gap:0.5rem; cursor:pointer; font-size:0.9rem; margin:0; color:var(--color-text); }
|
||||
.radio-group input[type="radio"] { width:auto; padding:0; margin:0; }
|
||||
|
||||
/* ── Spiel-Set Suche ── */
|
||||
.gs-dropdown {
|
||||
display:none; position:absolute; top:100%; left:0; right:0; z-index:200;
|
||||
background:var(--color-card); border:1px solid var(--color-secondary);
|
||||
border-radius:6px; max-height:200px; overflow-y:auto;
|
||||
box-shadow:0 4px 14px rgba(0,0,0,0.35); margin-top:2px;
|
||||
}
|
||||
.gs-dropdown-item {
|
||||
padding:0.55rem 0.9rem; cursor:pointer; font-size:0.9rem;
|
||||
border-bottom:1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.gs-dropdown-item:last-child { border-bottom:none; }
|
||||
.gs-dropdown-item:hover { background:rgba(255,255,255,0.07); }
|
||||
.gs-item-name { font-weight:600; color:var(--color-text); }
|
||||
.gs-item-desc { font-size:0.78rem; color:var(--color-muted); margin-top:0.1rem; }
|
||||
.gs-selected {
|
||||
display:flex; align-items:center; gap:0.6rem;
|
||||
background:rgba(255,255,255,0.05); border:1px solid var(--color-secondary);
|
||||
border-radius:6px; padding:0.45rem 0.75rem; margin-top:0.35rem;
|
||||
font-size:0.88rem; color:var(--color-text);
|
||||
}
|
||||
.gs-selected button {
|
||||
background:none; border:none; color:var(--color-muted);
|
||||
cursor:pointer; padding:0; margin:0; font-size:1rem; width:auto; line-height:1;
|
||||
}
|
||||
.gs-selected button:hover { color:#e74c3c; background:none; }
|
||||
|
||||
/* ── Simulation ── */
|
||||
.sim-bar-track { background:var(--color-secondary); border-radius:6px; height:8px; overflow:hidden; margin:0.5rem 0 0.25rem; }
|
||||
.sim-bar-fill { height:100%; background:var(--color-primary); border-radius:6px; transition:width 0.1s linear; width:0%; }
|
||||
@@ -268,47 +295,6 @@
|
||||
.sim-stat-val { font-size:1rem; font-weight:700; color:var(--color-text); }
|
||||
.sim-stat-lbl { font-size:0.7rem; color:var(--color-muted); margin-top:0.15rem; text-transform:uppercase; letter-spacing:0.05em; }
|
||||
|
||||
/* ── Spiel-Sets (gruppe-card style) ── */
|
||||
.gs-card { background:var(--color-card); border:1px solid var(--color-secondary); border-radius:10px; overflow:hidden; cursor:pointer; transition:border-color 0.15s; }
|
||||
.gs-card:hover { border-color:var(--color-primary); }
|
||||
.gs-card-header { display:flex; align-items:center; gap:0.75rem; padding:0.85rem 1rem; user-select:none; }
|
||||
.gs-card-meta { flex:1; min-width:0; }
|
||||
.gs-card-name { font-size:0.95rem; font-weight:600; color:var(--color-text); }
|
||||
.gs-card-header-actions { display:flex; gap:0.4rem; flex-shrink:0; }
|
||||
|
||||
.gs-sub { margin-bottom:0.85rem; }
|
||||
.gs-sub:last-child { margin-bottom:0; }
|
||||
.gs-sub-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.35rem; }
|
||||
.gs-sub-title { font-size:0.7rem; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:var(--color-primary); }
|
||||
.gs-sub-warn { color:#e74c3c !important; }
|
||||
|
||||
.gs-item-list { display:flex; flex-direction:column; gap:0.25rem; }
|
||||
.gs-list-item { border-radius:6px; background:var(--color-secondary); overflow:hidden; }
|
||||
.gs-list-item-row { display:flex; align-items:center; gap:0.5rem; padding:0.3rem 0.5rem 0.3rem 0.75rem; cursor:pointer; user-select:none; transition:background 0.12s; }
|
||||
.gs-list-item-row:hover { background:rgba(255,255,255,0.04); }
|
||||
.gs-list-item.open .gs-list-item-row { background:rgba(233,69,96,0.08); }
|
||||
.gs-list-item-text { color:var(--color-text); flex:1; min-width:0; font-size:0.83rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.gs-list-item-badges { display:flex; gap:0.3rem; flex-shrink:0; align-items:center; }
|
||||
.gs-badge { font-size:0.68rem; padding:0.1rem 0.4rem; border-radius:20px; background:rgba(233,69,96,0.15); color:var(--color-primary); white-space:nowrap; }
|
||||
.gs-badge-neutral { background:rgba(255,255,255,0.07); color:var(--color-muted); }
|
||||
.gs-list-item-detail { display:none; padding:0.4rem 0.75rem 0.5rem; border-top:1px solid rgba(255,255,255,0.06); font-size:0.79rem; color:var(--color-muted); line-height:1.5; }
|
||||
.gs-list-item.open .gs-list-item-detail { display:block; }
|
||||
.gs-detail-text { color:var(--color-text); white-space:pre-wrap; margin-bottom:0.3rem; }
|
||||
.gs-detail-actions { display:flex; gap:0.35rem; margin-top:0.4rem; justify-content:flex-end; }
|
||||
.gs-btn-sub-add { background:none; border:1px solid var(--color-secondary); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.15rem 0.5rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||||
.gs-btn-sub-add:hover { border-color:var(--color-primary); color:var(--color-primary); }
|
||||
.gs-btn-item-edit { background:none; border:1px solid rgba(136,136,136,0.45); border-radius:5px; color:var(--color-muted); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:border-color 0.15s,color 0.15s; width:auto; }
|
||||
.gs-btn-item-edit:hover { border-color:var(--color-text); color:var(--color-text); }
|
||||
.gs-btn-item-delete { background:none; border:1px solid rgba(233,69,96,0.4); border-radius:5px; color:var(--color-primary); font-size:0.73rem; padding:0.18rem 0.55rem; cursor:pointer; transition:background 0.15s; width:auto; }
|
||||
.gs-btn-item-delete:hover { background:rgba(233,69,96,0.15); }
|
||||
.gs-sub-empty { font-size:0.78rem; color:var(--color-muted); padding:0.15rem 0; }
|
||||
|
||||
#gsSetModal, #gsItemModal { z-index:600; }
|
||||
|
||||
.gs-check-group { display:flex; flex-wrap:wrap; gap:0.4rem; }
|
||||
.gs-check-chip { display:inline-flex; align-items:center; gap:0.4rem; background:var(--color-secondary); border:1px solid rgba(255,255,255,0.1); border-radius:20px; padding:0.25rem 0.7rem; cursor:pointer; font-size:0.82rem; color:var(--color-text); transition:border-color 0.15s; user-select:none; }
|
||||
.gs-check-chip:has(input:checked) { border-color:var(--color-primary); }
|
||||
.gs-check-chip input { accent-color:var(--color-primary); width:auto; cursor:pointer; margin:0; flex-shrink:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -329,17 +315,6 @@
|
||||
<div class="template-list" id="taskSetList"></div>
|
||||
<p id="taskSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Aufgaben-Sets vorhanden.</p>
|
||||
|
||||
<!-- Spiel-Sets (für Spiel-Karte) -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin:2rem 0 1rem;gap:1rem;flex-wrap:wrap;">
|
||||
<div>
|
||||
<h2 style="margin:0;">Spiel-Sets</h2>
|
||||
<p style="margin:0.25rem 0 0;font-size:0.8rem;color:var(--color-muted);">Aufgaben-Sets für die Spiel-Karte im Karten-Lock · max. 5 Sets</p>
|
||||
</div>
|
||||
<button id="btnNewGameSet" onclick="openGsSetModal(null)" style="width:auto;padding:0.55rem 1.2rem;">+ Set anlegen</button>
|
||||
</div>
|
||||
<div class="template-list" id="gameSetList"></div>
|
||||
<p id="gameSetEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Noch keine Spiel-Sets vorhanden.</p>
|
||||
|
||||
<h2 style="margin:2rem 0 1rem;">Abonnierte Vorlagen</h2>
|
||||
<div class="template-list" id="subscribedList"></div>
|
||||
<p id="subscribedEmpty" style="display:none;color:var(--color-muted);font-size:0.9rem;">Keine abonnierten Vorlagen vorhanden.</p>
|
||||
@@ -443,43 +418,57 @@
|
||||
<label for="fShowRemaining">Art der verbleibenden Karten anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spiel-Karte (CardLock) -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Spiel-Karte (optional)</div>
|
||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||
<label>Spiel-Set</label>
|
||||
<select id="fGameSetId" onchange="onGameSetChange()">
|
||||
<option value="">Kein Spiel-Set</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-add" type="button" onclick="openGsSetModal(null,'template')">+ Neues Set anlegen</button>
|
||||
<div id="gameSetSpieldauerRow" style="display:none;margin-top:0.75rem;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.3rem;">
|
||||
<label for="sldGameSpieldauer" style="font-size:0.88rem;">Spieldauer</label>
|
||||
<span class="setting-value" id="valGameSpieldauer">Mittel</span>
|
||||
<!-- Aufgaben (CardLock) – nur sichtbar wenn TASK-Karte > 0 -->
|
||||
<div id="sectionCardTasks" style="display:none;">
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Aufgaben</div>
|
||||
<div style="margin-bottom:0.65rem;">
|
||||
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
||||
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
||||
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2" oninput="updateGameSpieldauer(this.value)" style="width:100%;">
|
||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||
<label>Aufgaben-Set <span class="required-star">*</span></label>
|
||||
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
|
||||
<option value="">Kein Aufgaben-Set</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
|
||||
<div id="cardTaskSetPreview" class="task-set-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Aufgaben (CardLock) -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Aufgaben (optional)</div>
|
||||
<div style="margin-bottom:0.65rem;">
|
||||
<div style="font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--color-muted);margin-bottom:0.45rem;">Wer entscheidet über die Aufgabe?</div>
|
||||
<div class="radio-group">
|
||||
<label><input type="radio" name="modalCardTaskMode" value="RANDOM" checked> Zufall</label>
|
||||
<label><input type="radio" name="modalCardTaskMode" value="KEYHOLDER" > Keyholder*In</label>
|
||||
<label><input type="radio" name="modalCardTaskMode" value="COMMUNITY" > Community</label>
|
||||
|
||||
<!-- Spiel-Karte – nur sichtbar wenn GAME_CARD > 0 -->
|
||||
<div id="sectionGameSetConfig" style="display:none;">
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">Spiel-Karte – Minispiel</div>
|
||||
<p style="font-size:0.82rem;color:var(--color-muted);margin:0 0 0.75rem;">Wenn eine Spiel-Karte gezogen wird, startet ein Minispiel mit dem gewählten Chastity-Aufgaben-Set.</p>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Aufgaben-Set (Chastity) <span class="required-star">*</span></label>
|
||||
<div style="position:relative;">
|
||||
<input type="text" id="gameSetSearch" placeholder="Name eingeben zum Suchen…"
|
||||
autocomplete="off" oninput="onGameSetSearch(this.value)" onfocus="onGameSetSearchFocus()">
|
||||
<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>
|
||||
|
||||
<div class="form-row" style="margin-bottom:0;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem;">
|
||||
<label for="sldGameSpieldauer" style="margin:0;">Spieldauer</label>
|
||||
<span id="valGameSpieldauer" style="font-size:0.85rem;color:var(--color-muted);">Mittel</span>
|
||||
</div>
|
||||
<input type="range" id="sldGameSpieldauer" min="0" max="4" value="2"
|
||||
oninput="updateGameSpieldauer(this.value)"
|
||||
style="width:100%;accent-color:var(--color-primary);">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:0.5rem;">
|
||||
<label>Aufgaben-Set</label>
|
||||
<select id="fCardTaskSetId" onchange="onTaskSetChange('card')">
|
||||
<option value="">Kein Aufgaben-Set</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-add" type="button" onclick="openTaskSetModal(null,'card')">+ Neues Set anlegen</button>
|
||||
<div id="cardTaskSetPreview" class="task-set-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -719,114 +708,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Set erstellen / umbenennen -->
|
||||
<div class="modal-backdrop" id="gsSetModal">
|
||||
<div class="modal-box" style="max-width:420px;" onclick="event.stopPropagation()">
|
||||
<h2 id="gsSetModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Neues Spiel-Set</h2>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Name *</label>
|
||||
<input type="text" id="gsSetName" maxlength="100" placeholder="Set-Name"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;transition:border-color 0.2s;"
|
||||
onkeydown="if(event.key==='Enter')saveGsSet()">
|
||||
<div id="gsSetError" style="color:#e74c3c;font-size:0.82rem;margin-top:0.75rem;display:none;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:1.5rem;">
|
||||
<button onclick="closeGsSetModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||||
<button onclick="saveGsSet()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Item hinzufügen / bearbeiten -->
|
||||
<div class="modal-backdrop" id="gsItemModal">
|
||||
<div class="modal-box" style="max-width:460px;" onclick="event.stopPropagation()">
|
||||
<h2 id="gsItemModalTitle" style="margin:0 0 1.25rem;color:var(--color-primary);">Aufgabe</h2>
|
||||
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Titel *</label>
|
||||
<input type="text" id="gsItemTitle" maxlength="150" placeholder="Titel …"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;margin-bottom:0.85rem;transition:border-color 0.2s;">
|
||||
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Beschreibung (optional)</label>
|
||||
<textarea id="gsItemDesc" rows="3" maxlength="600" placeholder="Beschreibung …"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;margin-bottom:0.85rem;"></textarea>
|
||||
|
||||
<!-- Aufgabe: Level + Dauer -->
|
||||
<div id="gsItemAufgabeRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||||
<select id="gsItemAufgabeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||||
</select>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Dauer (Minuten, optional)</label>
|
||||
<input type="number" id="gsItemMinutes" min="1" max="9999" placeholder="–"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
</div>
|
||||
|
||||
<!-- Aufgabe: benötigt -->
|
||||
<div id="gsItemBenoetigtRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Benötigt</div>
|
||||
<div class="gs-check-group">
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_MUND">Mund</label>
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemBen_ANUS">Anus</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: Level + Strafminuten -->
|
||||
<div id="gsItemZeitstrafeRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Level *</label>
|
||||
<select id="gsItemZeitstrafeLevel" style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;appearance:none;margin-bottom:0.65rem;">
|
||||
<option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option>
|
||||
</select>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin-bottom:0.3rem;">Strafminuten (Von – Bis)</label>
|
||||
<div style="display:flex;gap:0.6rem;align-items:center;">
|
||||
<input type="number" id="gsItemMinMin" min="1" max="9999" placeholder="Min."
|
||||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
<span style="color:var(--color-muted);">–</span>
|
||||
<input type="number" id="gsItemMaxMin" min="1" max="9999" placeholder="Max."
|
||||
style="flex:1;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.95rem;outline:none;">
|
||||
</div>
|
||||
<label style="display:block;font-size:0.8rem;color:#aaa;margin:0.65rem 0 0.3rem;">Text bei Aufhebung (optional)</label>
|
||||
<textarea id="gsItemReleaseText" rows="2" maxlength="2000"
|
||||
placeholder="Text der angezeigt wird, wenn die Sperre endet…"
|
||||
style="width:100%;box-sizing:border-box;padding:0.6rem 0.85rem;border:1px solid var(--color-secondary);border-radius:6px;background:var(--color-secondary);color:var(--color-text);font-size:0.88rem;font-family:inherit;resize:vertical;outline:none;transition:border-color 0.2s;line-height:1.45;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe: sperrt -->
|
||||
<div id="gsItemSperrtRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Sperrt</div>
|
||||
<div class="gs-check-group">
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_MUND"> Mund</label>
|
||||
<label class="gs-check-chip"><input type="checkbox" id="gsItemSperr_ANUS"> Anus</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zeitstrafe + Finisher: Temp. Öffnung -->
|
||||
<div id="gsItemUnlockRow" style="display:none;margin-bottom:0.85rem;">
|
||||
<div style="font-size:0.8rem;color:#aaa;margin-bottom:0.4rem;">Temp. Öffnung</div>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;margin-bottom:0.35rem;cursor:pointer;">
|
||||
<input type="checkbox" id="gsItemBefore" style="accent-color:var(--color-primary);width:15px;height:15px;"> Vor der Maßnahme erforderlich
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.88rem;cursor:pointer;">
|
||||
<input type="checkbox" id="gsItemAfter" style="accent-color:var(--color-primary);width:15px;height:15px;"> Nach der Maßnahme erforderlich
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="gsItemError" style="color:#e74c3c;font-size:0.82rem;display:none;margin-bottom:0.5rem;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;gap:0.75rem;margin-top:0.5rem;">
|
||||
<button onclick="closeGsItemModal()" style="background:var(--color-secondary);color:var(--color-text);border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;cursor:pointer;width:auto;">Abbrechen</button>
|
||||
<button onclick="saveGsItem()" style="background:var(--color-primary);color:#fff;border:none;border-radius:6px;padding:0.55rem 1.1rem;font-size:0.9rem;font-weight:600;cursor:pointer;width:auto;">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spiel-Set: Inhalt-Popup -->
|
||||
<div class="modal-backdrop" id="gsEditModal">
|
||||
<div class="modal-box" style="max-width:600px;width:calc(100% - 2rem);" onclick="event.stopPropagation()">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<h2 id="gsEditModalTitle" style="margin:0;color:var(--color-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></h2>
|
||||
<button onclick="closeGsEditModal()" style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;line-height:1;cursor:pointer;padding:0;width:auto;flex-shrink:0;">✕</button>
|
||||
</div>
|
||||
<div id="gsEditModalContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/card-defs.js"></script>
|
||||
<script src="/js/card-display.js"></script>
|
||||
<script src="/js/icons.js"></script>
|
||||
@@ -935,8 +816,114 @@
|
||||
function syncMinMax(id, val) {
|
||||
if (id.startsWith('min_')) { const mx = document.getElementById('max_'+id.slice(4)); if (mx && val > (parseInt(mx.value)||0)) mx.value = val; }
|
||||
else if (id.startsWith('max_')) { const mn = document.getElementById('min_'+id.slice(4)); if (mn && val < (parseInt(mn.value)||0)) mn.value = val; }
|
||||
if (id === 'min_GAME_CARD' || id === 'max_GAME_CARD') {
|
||||
if (val > 0) {
|
||||
// Gegenseitiger Ausschluss: Task-Karten nullen
|
||||
const minT = document.getElementById('min_TASK'); if (minT) minT.value = 0;
|
||||
const maxT = document.getElementById('max_TASK'); if (maxT) maxT.value = 0;
|
||||
checkTaskCardSection();
|
||||
}
|
||||
checkGameCardSection();
|
||||
}
|
||||
if (id === 'min_TASK' || id === 'max_TASK') {
|
||||
if (val > 0) {
|
||||
// Gegenseitiger Ausschluss: Game-Karten nullen
|
||||
const minG = document.getElementById('min_GAME_CARD'); if (minG) minG.value = 0;
|
||||
const maxG = document.getElementById('max_GAME_CARD'); if (maxG) maxG.value = 0;
|
||||
checkGameCardSection();
|
||||
}
|
||||
checkTaskCardSection();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spiel-Karte: Aufgaben-Set + Spieldauer ──
|
||||
const GAME_SPIELDAUER = [
|
||||
{ label: 'Sehr kurz' },
|
||||
{ label: 'Kurz' },
|
||||
{ label: 'Mittel' },
|
||||
{ label: 'Lang' },
|
||||
{ label: 'Sehr lang' },
|
||||
];
|
||||
let _gameSetSearchTimer = null;
|
||||
let _gameSetResults = [];
|
||||
|
||||
function checkGameCardSection() {
|
||||
const minV = parseInt(document.getElementById('min_GAME_CARD')?.value) || 0;
|
||||
const maxV = parseInt(document.getElementById('max_GAME_CARD')?.value) || 0;
|
||||
const sec = document.getElementById('sectionGameSetConfig');
|
||||
if (sec) sec.style.display = (minV > 0 || maxV > 0) ? '' : 'none';
|
||||
}
|
||||
|
||||
function updateGameSpieldauer(val) {
|
||||
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 ──
|
||||
function openCardInfo(cardId) {
|
||||
const c = CARD_DEFS.find(x => x.id === cardId); if (!c) return;
|
||||
@@ -1223,51 +1210,6 @@
|
||||
if (res.ok || res.status === 204) await loadTaskSets();
|
||||
}
|
||||
|
||||
const GS_TOOLS = [
|
||||
{ value: 'UMSCHNALLDILDO', label: 'Strap-on' },
|
||||
{ value: 'MUND', label: 'Oral' },
|
||||
{ value: 'ANUS', label: 'Anal' },
|
||||
];
|
||||
function gsGetChecked(prefix) {
|
||||
return GS_TOOLS.filter(t => document.getElementById(prefix + t.value)?.checked).map(t => t.value);
|
||||
}
|
||||
function gsSetChecked(prefix, values) {
|
||||
GS_TOOLS.forEach(t => {
|
||||
const el = document.getElementById(prefix + t.value);
|
||||
if (el) el.checked = (values || []).includes(t.value);
|
||||
});
|
||||
}
|
||||
|
||||
const GAME_SPIELDAUER = [
|
||||
{ label: 'Sehr kurz' },
|
||||
{ label: 'Kurz' },
|
||||
{ label: 'Mittel' },
|
||||
{ label: 'Lang' },
|
||||
{ label: 'Sehr lang' },
|
||||
];
|
||||
|
||||
function populateGameSetSelect() {
|
||||
const sel = document.getElementById('fGameSetId');
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">Kein Spiel-Set</option>';
|
||||
_gameSets.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id; opt.textContent = s.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.value = cur;
|
||||
}
|
||||
|
||||
function onGameSetChange() {
|
||||
const val = document.getElementById('fGameSetId')?.value;
|
||||
document.getElementById('gameSetSpieldauerRow').style.display = val ? '' : 'none';
|
||||
}
|
||||
|
||||
function updateGameSpieldauer(val) {
|
||||
document.getElementById('valGameSpieldauer').textContent = GAME_SPIELDAUER[val]?.label || '';
|
||||
}
|
||||
|
||||
function populateTaskSetSelects() {
|
||||
for (const selId of ['fCardTaskSetId', 'fTimelockTaskSetId']) {
|
||||
const sel = document.getElementById(selId);
|
||||
@@ -1424,6 +1366,19 @@
|
||||
tpFromMinutes('pe', template?.pickEveryMinute || 60);
|
||||
document.getElementById('fAccumulate').checked = template?.accumulatePicks || false;
|
||||
document.getElementById('fShowRemaining').checked = template?.showRemainingCards || false;
|
||||
|
||||
// Spiel-Karte
|
||||
clearGameSet();
|
||||
checkGameCardSection();
|
||||
const gsi = template?.gameSpieldauerIdx ?? 2;
|
||||
document.getElementById('sldGameSpieldauer').value = gsi;
|
||||
updateGameSpieldauer(gsi);
|
||||
if (template?.gameSetId) {
|
||||
fetch(`/gruppe/${template.gameSetId}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(g => { if (g?.name) selectGameSet(template.gameSetId, g.name, true); })
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'TIMELOCK') {
|
||||
@@ -1476,14 +1431,6 @@
|
||||
onTaskSetChange('card');
|
||||
onTaskSetChange('timelock');
|
||||
|
||||
// Spiel-Set
|
||||
populateGameSetSelect();
|
||||
document.getElementById('fGameSetId').value = template?.gameSetId || '';
|
||||
onGameSetChange();
|
||||
const sdIdx = template?.gameSpieldauerIdx ?? 2;
|
||||
document.getElementById('sldGameSpieldauer').value = sdIdx;
|
||||
updateGameSpieldauer(sdIdx);
|
||||
|
||||
alignModalToContent();
|
||||
document.getElementById('modalBackdrop').classList.add('open');
|
||||
document.getElementById('modalDiscardConfirm').style.display = 'none';
|
||||
@@ -1527,13 +1474,7 @@
|
||||
document.getElementById('modalBackdrop').addEventListener('click', e => { if (e.target===e.currentTarget) tryCloseModal(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (document.getElementById('gsItemModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsItemModal();
|
||||
} else if (document.getElementById('gsSetModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsSetModal();
|
||||
} else if (document.getElementById('gsEditModal').classList.contains('open')) {
|
||||
e.preventDefault(); closeGsEditModal();
|
||||
} else if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
|
||||
if (document.getElementById('taskSetModalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault(); tryCloseTaskSetModal();
|
||||
} else if (document.getElementById('modalBackdrop').classList.contains('open')) {
|
||||
e.preventDefault(); tryCloseModal();
|
||||
@@ -1589,8 +1530,13 @@
|
||||
const hasTaskCards = (parseInt(document.getElementById('min_TASK').value)||0)>0 || (parseInt(document.getElementById('max_TASK').value)||0)>0;
|
||||
if (hasTaskCards && !document.getElementById('fCardTaskSetId').value) { showModalError('Aufgaben-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
const hasGameCards = (parseInt(document.getElementById('min_GAME_CARD').value)||0)>0 || (parseInt(document.getElementById('max_GAME_CARD').value)||0)>0;
|
||||
if (hasGameCards && !document.getElementById('fGameSetId').value) { showModalError('Spiel-Karten konfiguriert, aber kein Spiel-Set ausgewählt.'); firstError=firstError||document.getElementById('modalError'); }
|
||||
|
||||
if (hasGameCards && !document.getElementById('fGameSetId').value) {
|
||||
document.getElementById('errGameSet').style.display = '';
|
||||
showModalError('Spiel-Karten konfiguriert, aber kein Aufgaben-Set ausgewählt.');
|
||||
firstError = firstError || document.getElementById('errGameSet');
|
||||
} else {
|
||||
document.getElementById('errGameSet').style.display = 'none';
|
||||
}
|
||||
if (firstError) { firstError.scrollIntoView({behavior:'smooth',block:'center'}); return; }
|
||||
|
||||
const cardCountsMin={}, cardCountsMax={};
|
||||
@@ -1609,8 +1555,8 @@
|
||||
taskSetId: document.getElementById('fCardTaskSetId').value || null,
|
||||
requiresVerification: document.getElementById('fRequiresVerification').checked,
|
||||
taskMode: document.querySelector('input[name="modalCardTaskMode"]:checked')?.value||'RANDOM',
|
||||
gameSetId: document.getElementById('fGameSetId').value || null,
|
||||
gameSpieldauerIdx: parseInt(document.getElementById('sldGameSpieldauer').value) || 2,
|
||||
gameSetId: hasGameCards ? (document.getElementById('fGameSetId').value || null) : null,
|
||||
gameSpieldauerIdx: hasGameCards ? (parseInt(document.getElementById('sldGameSpieldauer').value) || 2) : null,
|
||||
};
|
||||
} else {
|
||||
// TimeLock
|
||||
@@ -1895,426 +1841,8 @@
|
||||
observer.observe(document.getElementById('scrollSentinel'));
|
||||
|
||||
resetList();
|
||||
loadGameSets();
|
||||
|
||||
// ════════════════════════════════════════════════
|
||||
// Spiel-Sets
|
||||
// ════════════════════════════════════════════════
|
||||
|
||||
let _gameSets = [];
|
||||
let _gsEditSetId = null; // set being renamed
|
||||
let _gsSetCaller = null; // 'template' when opened from the template modal
|
||||
let _gsOpenSetId = null; // set currently open in the content popup
|
||||
let _gsItemType = null; // 'aufgabe' | 'zeitstrafe' | 'finisher'
|
||||
let _gsItemSetId = null;
|
||||
let _gsItemIdx = null; // null = new, number = editing
|
||||
|
||||
async function loadGameSets() {
|
||||
try {
|
||||
const res = await fetch('/chastity/game-sets');
|
||||
if (!res.ok) return;
|
||||
_gameSets = await res.json();
|
||||
renderGameSetList();
|
||||
populateGameSetSelect();
|
||||
if (_gsOpenSetId) renderGsEditModalContent(_gsOpenSetId);
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderGameSetList() {
|
||||
const list = document.getElementById('gameSetList');
|
||||
list.innerHTML = '';
|
||||
document.getElementById('gameSetEmpty').style.display = _gameSets.length ? 'none' : '';
|
||||
document.getElementById('btnNewGameSet').disabled = _gameSets.length >= 5;
|
||||
|
||||
_gameSets.forEach(s => {
|
||||
const aufgaben = s.aufgaben || [];
|
||||
const zeitstrafen = s.zeitstrafen || [];
|
||||
const finisher = s.finisher || [];
|
||||
const levelCounts = [1,2,3,4,5].map(l => aufgaben.filter(a => a.level === l).length);
|
||||
|
||||
const lvlBadges = levelCounts.map((c, i) => {
|
||||
const cls = c >= 3 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
|
||||
return `<span class="${cls}">L${i+1}: ${c}</span>`;
|
||||
}).join('');
|
||||
const finBadgeCls = finisher.length >= 1 ? 'gs-badge gs-badge-neutral' : 'gs-badge';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'gs-card';
|
||||
card.id = 'gscard_' + s.id;
|
||||
card.addEventListener('click', () => openGsEditModal(s.id));
|
||||
card.innerHTML = `
|
||||
<div class="gs-card-header">
|
||||
<div class="gs-card-meta">
|
||||
<div class="gs-card-name">${esc(s.name)}</div>
|
||||
<div style="display:flex;gap:0.3rem;flex-wrap:wrap;margin-top:0.3rem;">
|
||||
${lvlBadges}
|
||||
<span class="gs-badge gs-badge-neutral">Zeitstrafen: ${zeitstrafen.length}</span>
|
||||
<span class="${finBadgeCls}">Finisher: ${finisher.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gs-card-header-actions" onclick="event.stopPropagation()">
|
||||
<button class="gs-btn-item-edit" onclick="openGsSetModal('${s.id}')">✎</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGameSet('${s.id}',${JSON.stringify(s.name)})">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGsListItem(id) {
|
||||
document.getElementById(id)?.classList.toggle('open');
|
||||
}
|
||||
|
||||
// ── Set content popup ──────────────────────────
|
||||
|
||||
function openGsEditModal(setId) {
|
||||
_gsOpenSetId = setId;
|
||||
renderGsEditModalContent(setId);
|
||||
document.getElementById('gsEditModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeGsEditModal() {
|
||||
document.getElementById('gsEditModal').classList.remove('open');
|
||||
_gsOpenSetId = null;
|
||||
}
|
||||
|
||||
function renderGsEditModalContent(setId) {
|
||||
const container = document.getElementById('gsEditModalContent');
|
||||
if (!container) return;
|
||||
const s = _gameSets.find(x => x.id === setId);
|
||||
if (!s) { closeGsEditModal(); return; }
|
||||
document.getElementById('gsEditModalTitle').textContent = s.name;
|
||||
const aufgaben = s.aufgaben || [];
|
||||
const zeitstrafen = s.zeitstrafen || [];
|
||||
const finisher = s.finisher || [];
|
||||
let html = '';
|
||||
for (let l = 1; l <= 5; l++) {
|
||||
const items = aufgaben.map((a, i) => ({...a, _gi: i})).filter(a => a.level === l);
|
||||
const warnCls = items.length < 3 ? ' gs-sub-warn' : '';
|
||||
const itemsHtml = items.map(a => gsAufgabeRowHtml(s.id, a._gi, a)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title${warnCls}">Level ${l} <span style="font-weight:400;">(${items.length}/3+)</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('aufgabe','${s.id}',null,${l})">+ Aufgabe</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${itemsHtml}</div></div>`;
|
||||
}
|
||||
const zeitHtml = zeitstrafen.map((z, i) => gsZeitstrafeRowHtml(s.id, i, z)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title">Zeitstrafen <span style="font-weight:400;">(${zeitstrafen.length})</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('zeitstrafe','${s.id}',null,null)">+ Zeitstrafe</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${zeitHtml}</div></div>`;
|
||||
const finWarnCls = finisher.length < 1 ? ' gs-sub-warn' : '';
|
||||
const finHtml = finisher.map((f, i) => gsFinisherRowHtml(s.id, i, f)).join('') ||
|
||||
'<div class="gs-sub-empty">–</div>';
|
||||
html += `<div class="gs-sub">
|
||||
<div class="gs-sub-header">
|
||||
<span class="gs-sub-title${finWarnCls}">Finisher <span style="font-weight:400;">(${finisher.length}/1+)</span></span>
|
||||
<button class="gs-btn-sub-add" onclick="openGsItemModal('finisher','${s.id}',null,null)">+ Finisher</button>
|
||||
</div>
|
||||
<div class="gs-item-list">${finHtml}</div></div>`;
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Row HTML helpers ───────────────────────────
|
||||
|
||||
function gsAufgabeRowHtml(setId, gi, a) {
|
||||
const toolLabels = (a.benoetigt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
|
||||
const badges = [
|
||||
a.minutes ? `<span class="gs-badge gs-badge-neutral">${a.minutes} Min.</span>` : '',
|
||||
...toolLabels.map(l => `<span class="gs-badge gs-badge-neutral">${l}</span>`),
|
||||
].join('');
|
||||
const desc = a.description ? `<div class="gs-detail-text">${esc(a.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_a_${gi}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_a_${gi}')">
|
||||
<span class="gs-list-item-text">${esc(a.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('aufgabe','${setId}',${gi},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('aufgabe','${setId}',${gi})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('aufgabe','${setId}',${gi})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function gsZeitstrafeRowHtml(setId, idx, z) {
|
||||
const timeStr = (z.minMinutes != null ? z.minMinutes : '?') + '–' + (z.maxMinutes != null ? z.maxMinutes : '?') + ' Min.';
|
||||
const sperrtLabels = (z.sperrt || []).map(v => GS_TOOLS.find(t => t.value === v)?.label).filter(Boolean);
|
||||
const badges = [
|
||||
z.level ? `<span class="gs-badge">L${z.level}</span>` : '',
|
||||
`<span class="gs-badge gs-badge-neutral">${timeStr}</span>`,
|
||||
...sperrtLabels.map(l => `<span class="gs-badge gs-badge-neutral">🔒 ${l}</span>`),
|
||||
z.releaseText ? `<span class="gs-badge gs-badge-neutral">📝 Aufhebung</span>` : '',
|
||||
z.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||||
z.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||||
].join('');
|
||||
const releaseRow = z.releaseText ? `<div style="font-size:0.75rem;color:var(--color-muted);margin-bottom:0.15rem;">Bei Aufhebung:</div><div class="gs-detail-text">${esc(z.releaseText)}</div>` : '';
|
||||
const desc = z.description ? `<div class="gs-detail-text">${esc(z.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_z_${idx}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_z_${idx}')">
|
||||
<span class="gs-list-item-text">${esc(z.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}${releaseRow}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('zeitstrafe','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('zeitstrafe','${setId}',${idx})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('zeitstrafe','${setId}',${idx})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
function gsFinisherRowHtml(setId, idx, f) {
|
||||
const badges = [
|
||||
f.tempUnlockBeforeRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Vorher</span>` : '',
|
||||
f.tempUnlockAfterRequired ? `<span class="gs-badge gs-badge-neutral">🔓 Nachher</span>` : '',
|
||||
].join('');
|
||||
const desc = f.description ? `<div class="gs-detail-text">${esc(f.description)}</div>` : '';
|
||||
return `<div class="gs-list-item" id="gsli_${setId}_f_${idx}">
|
||||
<div class="gs-list-item-row" onclick="toggleGsListItem('gsli_${setId}_f_${idx}')">
|
||||
<span class="gs-list-item-text">${esc(f.title)}</span>
|
||||
<div class="gs-list-item-badges">${badges}</div>
|
||||
</div>
|
||||
<div class="gs-list-item-detail">${desc}
|
||||
<div class="gs-detail-actions">
|
||||
<button class="gs-btn-item-edit" onclick="openGsItemModal('finisher','${setId}',${idx},null)">✎ Bearbeiten</button>
|
||||
<button class="gs-btn-item-edit" onclick="duplicateGsItem('finisher','${setId}',${idx})">⧉ Kopie</button>
|
||||
<button class="gs-btn-item-delete" onclick="deleteGsItem('finisher','${setId}',${idx})">✕ Löschen</button>
|
||||
</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// ── Set create / rename modal ──────────────────
|
||||
|
||||
function openGsSetModal(id, caller) {
|
||||
_gsEditSetId = id || null;
|
||||
_gsSetCaller = caller || null;
|
||||
document.getElementById('gsSetModalTitle').textContent = id ? 'Spiel-Set umbenennen' : 'Neues Spiel-Set';
|
||||
document.getElementById('gsSetName').value = id ? (_gameSets.find(s => s.id === id)?.name || '') : '';
|
||||
document.getElementById('gsSetError').style.display = 'none';
|
||||
document.getElementById('gsSetModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('gsSetName').focus(), 50);
|
||||
}
|
||||
|
||||
function closeGsSetModal() {
|
||||
document.getElementById('gsSetModal').classList.remove('open');
|
||||
_gsEditSetId = _gsSetCaller = null;
|
||||
}
|
||||
|
||||
async function saveGsSet() {
|
||||
const name = document.getElementById('gsSetName').value.trim();
|
||||
const errEl = document.getElementById('gsSetError');
|
||||
if (!name) { errEl.textContent = 'Name ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
|
||||
errEl.style.display = 'none';
|
||||
const set = _gsEditSetId ? _gameSets.find(s => s.id === _gsEditSetId) : null;
|
||||
const url = _gsEditSetId ? `/chastity/game-sets/${_gsEditSetId}` : '/chastity/game-sets';
|
||||
const method = _gsEditSetId ? 'PUT' : 'POST';
|
||||
const body = { name,
|
||||
aufgaben: set?.aufgaben || [],
|
||||
zeitstrafen: set?.zeitstrafen || [],
|
||||
finisher: set?.finisher || [] };
|
||||
try {
|
||||
const res = await fetch(url, { method, headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
||||
if (res.ok) {
|
||||
const saved = await res.json().catch(() => null);
|
||||
const caller = _gsSetCaller;
|
||||
closeGsSetModal();
|
||||
await loadGameSets();
|
||||
if (caller === 'template' && saved?.id) {
|
||||
document.getElementById('fGameSetId').value = saved.id;
|
||||
onGameSetChange();
|
||||
markDirty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const b = await res.json().catch(() => ({}));
|
||||
errEl.textContent = b.error || 'Fehler.'; errEl.style.display = '';
|
||||
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
|
||||
}
|
||||
|
||||
async function deleteGameSet(id, name) {
|
||||
if (!confirm(`Spiel-Set „${name}" wirklich löschen?`)) return;
|
||||
const res = await fetch(`/chastity/game-sets/${id}`, { method: 'DELETE' });
|
||||
if (res.ok || res.status === 204) loadGameSets();
|
||||
}
|
||||
|
||||
// ── Item modal ─────────────────────────────────
|
||||
|
||||
function openGsItemModal(type, setId, itemIdx, contextLevel) {
|
||||
_gsItemType = type;
|
||||
_gsItemSetId = setId;
|
||||
_gsItemIdx = itemIdx !== null && itemIdx !== undefined ? itemIdx : null;
|
||||
|
||||
const titles = { aufgabe: 'Aufgabe', zeitstrafe: 'Zeitstrafe', finisher: 'Finisher' };
|
||||
document.getElementById('gsItemModalTitle').textContent =
|
||||
(_gsItemIdx !== null ? 'Bearbeiten: ' : 'Neu: ') + titles[type];
|
||||
|
||||
// Reset fields
|
||||
document.getElementById('gsItemTitle').value = '';
|
||||
document.getElementById('gsItemDesc').value = '';
|
||||
document.getElementById('gsItemMinutes').value = '';
|
||||
document.getElementById('gsItemMinMin').value = '';
|
||||
document.getElementById('gsItemMaxMin').value = '';
|
||||
document.getElementById('gsItemReleaseText').value = '';
|
||||
document.getElementById('gsItemBefore').checked = false;
|
||||
document.getElementById('gsItemAfter').checked = false;
|
||||
document.getElementById('gsItemAufgabeLevel').value = contextLevel || 1;
|
||||
document.getElementById('gsItemZeitstrafeLevel').value = 1;
|
||||
document.getElementById('gsItemError').style.display = 'none';
|
||||
gsSetChecked('gsItemBen_', []);
|
||||
gsSetChecked('gsItemSperr_', []);
|
||||
|
||||
// Show/hide type-specific rows
|
||||
document.getElementById('gsItemAufgabeRow').style.display = type === 'aufgabe' ? '' : 'none';
|
||||
document.getElementById('gsItemBenoetigtRow').style.display = type === 'aufgabe' ? '' : 'none';
|
||||
document.getElementById('gsItemZeitstrafeRow').style.display = type === 'zeitstrafe' ? '' : 'none';
|
||||
document.getElementById('gsItemSperrtRow').style.display = type === 'zeitstrafe' ? '' : 'none';
|
||||
document.getElementById('gsItemUnlockRow').style.display = (type === 'zeitstrafe' || type === 'finisher') ? '' : 'none';
|
||||
|
||||
// Pre-fill when editing
|
||||
if (_gsItemIdx !== null) {
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (set) {
|
||||
let item;
|
||||
if (type === 'aufgabe') item = set.aufgaben[_gsItemIdx];
|
||||
if (type === 'zeitstrafe') item = set.zeitstrafen[_gsItemIdx];
|
||||
if (type === 'finisher') item = set.finisher[_gsItemIdx];
|
||||
if (item) {
|
||||
document.getElementById('gsItemTitle').value = item.title || '';
|
||||
document.getElementById('gsItemDesc').value = item.description || '';
|
||||
if (type === 'aufgabe') {
|
||||
document.getElementById('gsItemAufgabeLevel').value = item.level || 1;
|
||||
document.getElementById('gsItemMinutes').value = item.minutes || '';
|
||||
gsSetChecked('gsItemBen_', item.benoetigt || []);
|
||||
}
|
||||
if (type === 'zeitstrafe') {
|
||||
document.getElementById('gsItemZeitstrafeLevel').value = item.level || 1;
|
||||
document.getElementById('gsItemMinMin').value = item.minMinutes ?? '';
|
||||
document.getElementById('gsItemMaxMin').value = item.maxMinutes ?? '';
|
||||
document.getElementById('gsItemReleaseText').value = item.releaseText || '';
|
||||
gsSetChecked('gsItemSperr_', item.sperrt || []);
|
||||
}
|
||||
if (type === 'zeitstrafe' || type === 'finisher') {
|
||||
document.getElementById('gsItemBefore').checked = !!item.tempUnlockBeforeRequired;
|
||||
document.getElementById('gsItemAfter').checked = !!item.tempUnlockAfterRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('gsItemModal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('gsItemTitle').focus(), 50);
|
||||
}
|
||||
|
||||
function closeGsItemModal() {
|
||||
document.getElementById('gsItemModal').classList.remove('open');
|
||||
_gsItemType = _gsItemSetId = _gsItemIdx = null;
|
||||
}
|
||||
|
||||
async function saveGsItem() {
|
||||
const title = document.getElementById('gsItemTitle').value.trim();
|
||||
const errEl = document.getElementById('gsItemError');
|
||||
if (!title) { errEl.textContent = 'Titel ist ein Pflichtfeld.'; errEl.style.display = ''; return; }
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const set = _gameSets.find(s => s.id === _gsItemSetId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
|
||||
const desc = document.getElementById('gsItemDesc').value.trim() || null;
|
||||
let item;
|
||||
if (_gsItemType === 'aufgabe') {
|
||||
const min = parseInt(document.getElementById('gsItemMinutes').value);
|
||||
const ben = gsGetChecked('gsItemBen_');
|
||||
item = { title, description: desc,
|
||||
level: parseInt(document.getElementById('gsItemAufgabeLevel').value) || 1,
|
||||
minutes: isNaN(min) ? null : min,
|
||||
benoetigt: ben.length ? ben : null };
|
||||
if (_gsItemIdx !== null) updated.aufgaben[_gsItemIdx] = item;
|
||||
else updated.aufgaben.push(item);
|
||||
} else if (_gsItemType === 'zeitstrafe') {
|
||||
const minMin = parseInt(document.getElementById('gsItemMinMin').value);
|
||||
const maxMin = parseInt(document.getElementById('gsItemMaxMin').value);
|
||||
const sperrt = gsGetChecked('gsItemSperr_');
|
||||
const releaseText = document.getElementById('gsItemReleaseText').value.trim() || null;
|
||||
item = { title, description: desc,
|
||||
level: parseInt(document.getElementById('gsItemZeitstrafeLevel').value) || 1,
|
||||
minMinutes: isNaN(minMin) ? null : minMin,
|
||||
maxMinutes: isNaN(maxMin) ? null : maxMin,
|
||||
releaseText,
|
||||
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
|
||||
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked,
|
||||
sperrt: sperrt.length ? sperrt : null };
|
||||
if (_gsItemIdx !== null) updated.zeitstrafen[_gsItemIdx] = item;
|
||||
else updated.zeitstrafen.push(item);
|
||||
} else if (_gsItemType === 'finisher') {
|
||||
item = { title, description: desc,
|
||||
tempUnlockBeforeRequired: document.getElementById('gsItemBefore').checked,
|
||||
tempUnlockAfterRequired: document.getElementById('gsItemAfter').checked };
|
||||
if (_gsItemIdx !== null) updated.finisher[_gsItemIdx] = item;
|
||||
else updated.finisher.push(item);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/chastity/game-sets/${_gsItemSetId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) { closeGsItemModal(); await loadGameSets(); }
|
||||
else { const b = await res.json().catch(()=>({})); errEl.textContent = b.error||'Fehler.'; errEl.style.display = ''; }
|
||||
} catch (e) { errEl.textContent = 'Netzwerkfehler.'; errEl.style.display = ''; }
|
||||
}
|
||||
|
||||
async function deleteGsItem(type, setId, idx) {
|
||||
if (!confirm('Eintrag wirklich löschen?')) return;
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
if (type === 'aufgabe') updated.aufgaben.splice(idx, 1);
|
||||
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx, 1);
|
||||
if (type === 'finisher') updated.finisher.splice(idx, 1);
|
||||
const res = await fetch(`/chastity/game-sets/${setId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) loadGameSets();
|
||||
}
|
||||
|
||||
async function duplicateGsItem(type, setId, idx) {
|
||||
const set = _gameSets.find(s => s.id === setId);
|
||||
if (!set) return;
|
||||
const updated = {
|
||||
name: set.name,
|
||||
aufgaben: JSON.parse(JSON.stringify(set.aufgaben || [])),
|
||||
zeitstrafen: JSON.parse(JSON.stringify(set.zeitstrafen || [])),
|
||||
finisher: JSON.parse(JSON.stringify(set.finisher || [])),
|
||||
};
|
||||
if (type === 'aufgabe') updated.aufgaben.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.aufgaben[idx])));
|
||||
if (type === 'zeitstrafe') updated.zeitstrafen.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.zeitstrafen[idx])));
|
||||
if (type === 'finisher') updated.finisher.splice(idx + 1, 0, JSON.parse(JSON.stringify(set.finisher[idx])));
|
||||
const res = await fetch(`/chastity/game-sets/${setId}`, {
|
||||
method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(updated)
|
||||
});
|
||||
if (res.ok) loadGameSets();
|
||||
}
|
||||
|
||||
document.getElementById('gsSetModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsSetModal(); });
|
||||
document.getElementById('gsItemModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsItemModal(); });
|
||||
document.getElementById('gsEditModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeGsEditModal(); });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
433
src/main/resources/static/games/chastity/taskgame.html
Normal file
433
src/main/resources/static/games/chastity/taskgame.html
Normal file
@@ -0,0 +1,433 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Task Game – xXx Sphere</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.game-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.game-text {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.game-timer {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
text-align: center;
|
||||
margin: 0.75rem 0;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.game-timer.urgent { color: #e74c3c; }
|
||||
|
||||
.game-level-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-level-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-secondary);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.game-level-dot.active { background: var(--color-primary); }
|
||||
|
||||
.lock-messages {
|
||||
background: rgba(233,69,96,0.1);
|
||||
border: 1px solid rgba(233,69,96,0.3);
|
||||
border-radius: 10px;
|
||||
padding: 1rem 1.1rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.lock-messages p { margin: 0 0 0.5rem; font-size: 0.9rem; line-height: 1.5; }
|
||||
.lock-messages p:last-child { margin: 0; }
|
||||
|
||||
.init-box {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.init-box h2 { font-size: 1.1rem; margin: 0 0 0.75rem; }
|
||||
.group-list { display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 1rem; }
|
||||
.group-item {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
.group-item:hover, .group-item.selected {
|
||||
border-color: var(--color-primary);
|
||||
background: rgba(var(--color-primary-rgb, 233,69,96), 0.06);
|
||||
}
|
||||
.group-item input[type=radio] { accent-color: var(--color-primary); }
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
padding: 0.65rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-secondary);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 0.92rem;
|
||||
cursor: pointer;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
#finisherBox {
|
||||
background: linear-gradient(135deg, rgba(233,69,96,0.15), rgba(155,89,182,0.12));
|
||||
border: 1px solid rgba(233,69,96,0.4);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
#finisherBox .trophy { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||||
#finisherBox h2 { font-size: 1.1rem; margin: 0 0 0.75rem; color: var(--color-primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="container" style="max-width:480px;margin:0 auto;padding:1rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem;">
|
||||
<button id="btnBack" onclick="goBack()"
|
||||
style="background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.4rem;">‹</button>
|
||||
<h1 style="margin:0;font-size:1.15rem;font-weight:700;">🎯 Task Game</h1>
|
||||
</div>
|
||||
|
||||
<!-- Level-Anzeige -->
|
||||
<div class="game-level-bar" id="levelBar" style="display:none;">
|
||||
<span style="font-size:0.78rem;color:var(--color-muted);font-weight:600;">Level</span>
|
||||
<div id="levelDots" style="display:flex;gap:0.35rem;"></div>
|
||||
<span id="levelText" style="font-size:0.78rem;color:var(--color-muted);margin-left:auto;"></span>
|
||||
</div>
|
||||
|
||||
<!-- Freigegebene Locks (checkLocks-Meldungen) -->
|
||||
<div id="lockMessages" class="lock-messages" style="display:none;"></div>
|
||||
|
||||
<!-- Initialisierung: Gruppe wählen -->
|
||||
<div id="initBox" class="init-box" style="display:none;">
|
||||
<h2>Spiel-Set auswählen</h2>
|
||||
<p style="font-size:0.85rem;color:var(--color-muted);margin:0 0 1rem;">
|
||||
Wähle die Aufgabengruppe für dieses Spiel.
|
||||
</p>
|
||||
<div id="groupList" class="group-list"></div>
|
||||
<button class="btn-primary" id="btnStart" disabled onclick="startGame()">▶ Spiel starten</button>
|
||||
</div>
|
||||
|
||||
<!-- Laufendes Spiel -->
|
||||
<div id="gameBox" style="display:none;">
|
||||
|
||||
<!-- Task oder Lock in Queue -->
|
||||
<div id="queueBox" class="game-card" style="display:none;">
|
||||
<div class="game-label" id="queueLabel"></div>
|
||||
<div class="game-text" id="queueText"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnOk" onclick="handleOk()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktive Aufgabe (läuft) -->
|
||||
<div id="activeBox" class="game-card" style="display:none;">
|
||||
<div class="game-label">Aktive Aufgabe</div>
|
||||
<div class="game-text" id="activeText"></div>
|
||||
<div class="game-timer" id="activeTimer" style="display:none;"></div>
|
||||
<div style="display:flex;gap:0.6rem;margin-top:1.1rem;">
|
||||
<button class="btn-primary" id="btnErledigt" onclick="handleErledigt()">✓ Erledigt</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Finisher -->
|
||||
<div id="finisherBox" style="display:none;">
|
||||
<div class="trophy">🏆</div>
|
||||
<h2>Level 6 erreicht!</h2>
|
||||
<div class="game-label" id="finisherTitle"></div>
|
||||
<div class="game-text" id="finisherText" style="margin-top:0.5rem;text-align:left;"></div>
|
||||
<button class="btn-secondary" onclick="goBack()" style="margin-top:1.25rem;">Zurück zum Lock</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="loadingHint" style="text-align:center;color:var(--color-muted);padding:2rem 0;font-size:0.9rem;">
|
||||
Wird geladen…
|
||||
</div>
|
||||
<div id="errorBox" style="display:none;background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);
|
||||
border-radius:10px;padding:1rem;font-size:0.88rem;color:#e74c3c;margin-top:1rem;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search);
|
||||
const lockId = params.get('lockId');
|
||||
let _state = null;
|
||||
let _timerInt = null;
|
||||
let _pendingIsLock = false;
|
||||
let _pendingHasDuration = false;
|
||||
|
||||
function goBack() {
|
||||
if (lockId) location.href = '/games/chastity/activelock.html?lockId=' + lockId;
|
||||
else history.back();
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async function boot() {
|
||||
try {
|
||||
const r = await fetch('/lock-game/state');
|
||||
if (r.status === 404) {
|
||||
await loadGroups();
|
||||
return;
|
||||
}
|
||||
if (!r.ok) throw new Error('Fehler beim Laden des Spielzustands');
|
||||
_state = await r.json();
|
||||
hide('loadingHint');
|
||||
await runGameLoop();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
const r = await fetch('/lock-game/groups');
|
||||
const groups = r.ok ? await r.json() : [];
|
||||
hide('loadingHint');
|
||||
const list = document.getElementById('groupList');
|
||||
if (groups.length === 0) {
|
||||
list.innerHTML = '<p style="font-size:0.85rem;color:var(--color-muted);">Keine passenden Aufgabengruppen gefunden.<br>Erstelle zuerst eine Gruppe vom Typ „Chastity Only".</p>';
|
||||
} else {
|
||||
list.innerHTML = groups.map(g => `
|
||||
<label class="group-item" onclick="selectGroup(this)">
|
||||
<input type="radio" name="gruppe" value="${g.gruppenId}" style="display:none;">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:0.95rem;">${esc(g.name)}</div>
|
||||
${g.beschreibung ? `<div style="font-size:0.8rem;color:var(--color-muted);">${esc(g.beschreibung)}</div>` : ''}
|
||||
</div>
|
||||
</label>`).join('');
|
||||
}
|
||||
show('initBox');
|
||||
}
|
||||
|
||||
function selectGroup(el) {
|
||||
document.querySelectorAll('.group-item').forEach(i => i.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
el.querySelector('input').checked = true;
|
||||
document.getElementById('btnStart').disabled = false;
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
const sel = document.querySelector('input[name="gruppe"]:checked');
|
||||
if (!sel) return;
|
||||
hide('initBox');
|
||||
show('loadingHint');
|
||||
try {
|
||||
const r = await fetch('/lock-game/init?aufgabenGruppeId=' + sel.value, { method: 'POST' });
|
||||
if (!r.ok) throw new Error('Initialisierung fehlgeschlagen');
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
hide('loadingHint');
|
||||
await runGameLoop();
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Game Loop ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function runGameLoop() {
|
||||
hide('queueBox');
|
||||
hide('activeBox');
|
||||
hide('finisherBox');
|
||||
clearTimer();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisher();
|
||||
return;
|
||||
}
|
||||
|
||||
renderLevelBar(_state.level);
|
||||
|
||||
// Aktive Aufgabe läuft noch
|
||||
if (_state.activeTask) {
|
||||
showActiveTask(_state.activeTask, _state.activeTaskEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue leer → nächsten Task holen
|
||||
if (!_state.taskInQueue && !_state.lockInQueue) {
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
}
|
||||
|
||||
showQueue();
|
||||
}
|
||||
|
||||
function showQueue() {
|
||||
if (_state.lockInQueue) {
|
||||
let sperre;
|
||||
try { sperre = JSON.parse(_state.lockInQueue); } catch { sperre = {}; }
|
||||
_pendingIsLock = true;
|
||||
_pendingHasDuration = !!(sperre.minutenVon || sperre.minutenBis);
|
||||
document.getElementById('queueLabel').textContent = '🔒 Neue Sperre';
|
||||
document.getElementById('queueText').textContent = sperre.text || _state.lockInQueue;
|
||||
} else if (_state.taskInQueue) {
|
||||
let aufgabe;
|
||||
try { aufgabe = JSON.parse(_state.taskInQueue); } catch { aufgabe = {}; }
|
||||
_pendingIsLock = false;
|
||||
_pendingHasDuration = !!(aufgabe.sekundenVon || aufgabe.sekundenBis);
|
||||
document.getElementById('queueLabel').textContent = '🎯 Neue Aufgabe';
|
||||
document.getElementById('queueText').textContent = aufgabe.text || _state.taskInQueue;
|
||||
}
|
||||
show('gameBox');
|
||||
show('queueBox');
|
||||
}
|
||||
|
||||
function showActiveTask(text, endIso) {
|
||||
document.getElementById('activeText').textContent = text;
|
||||
show('gameBox');
|
||||
show('activeBox');
|
||||
|
||||
if (endIso) {
|
||||
const end = new Date(endIso);
|
||||
startTimer(end, document.getElementById('activeTimer'));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
// Locks prüfen (nach jeder Aktion)
|
||||
await checkAndShowLocks();
|
||||
|
||||
if (_pendingIsLock || _pendingHasDuration) {
|
||||
// Task/Sperre aktivieren und Timer starten
|
||||
const r = await fetch('/lock-game/apply-task', { method: 'POST' });
|
||||
if (!r.ok) { showError('Fehler'); return; }
|
||||
const stateR = await fetch('/lock-game/state');
|
||||
_state = await stateR.json();
|
||||
} else {
|
||||
// Keine Dauer → sofort nächsten Task
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
}
|
||||
await runGameLoop();
|
||||
}
|
||||
|
||||
async function handleErledigt() {
|
||||
await checkAndShowLocks();
|
||||
|
||||
if (_state.level >= 6) {
|
||||
await showFinisher();
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch('/lock-game/next-task', { method: 'POST' });
|
||||
const r = await fetch('/lock-game/state');
|
||||
_state = await r.json();
|
||||
await runGameLoop();
|
||||
}
|
||||
|
||||
async function checkAndShowLocks() {
|
||||
const r = await fetch('/lock-game/check-locks', { method: 'POST' });
|
||||
if (!r.ok) return;
|
||||
const texts = await r.json();
|
||||
if (texts && texts.length > 0) {
|
||||
const box = document.getElementById('lockMessages');
|
||||
box.innerHTML = texts.map(t => `<p>🔓 ${esc(t)}</p>`).join('');
|
||||
show('lockMessages');
|
||||
await new Promise(res => setTimeout(res, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
async function showFinisher() {
|
||||
show('gameBox');
|
||||
const r = await fetch('/lock-game/finisher');
|
||||
if (!r.ok) {
|
||||
document.getElementById('finisherTitle').textContent = '';
|
||||
document.getElementById('finisherText').textContent = 'Glückwunsch – du hast Level 6 erreicht!';
|
||||
} else {
|
||||
const f = await r.json();
|
||||
document.getElementById('finisherTitle').textContent = f.kurzText || '';
|
||||
document.getElementById('finisherText').textContent = f.text || '';
|
||||
}
|
||||
show('finisherBox');
|
||||
}
|
||||
|
||||
// ── Level-Bar ─────────────────────────────────────────────────────────────
|
||||
|
||||
function renderLevelBar(level) {
|
||||
const bar = document.getElementById('levelBar');
|
||||
const dots = document.getElementById('levelDots');
|
||||
dots.innerHTML = [1,2,3,4,5].map(i =>
|
||||
`<div class="game-level-dot${i <= level ? ' active' : ''}"></div>`
|
||||
).join('');
|
||||
document.getElementById('levelText').textContent = 'Level ' + Math.min(level, 5);
|
||||
show('levelBar');
|
||||
}
|
||||
|
||||
// ── Timer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function startTimer(endDate, el) {
|
||||
el.style.display = '';
|
||||
clearTimer();
|
||||
_timerInt = setInterval(() => {
|
||||
const diff = Math.max(0, Math.round((endDate - Date.now()) / 1000));
|
||||
const m = String(Math.floor(diff / 60)).padStart(2, '0');
|
||||
const s = String(diff % 60).padStart(2, '0');
|
||||
el.textContent = m + ':' + s;
|
||||
el.classList.toggle('urgent', diff < 30);
|
||||
if (diff === 0) clearTimer();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
if (_timerInt) { clearInterval(_timerInt); _timerInt = null; }
|
||||
}
|
||||
|
||||
// ── Hilfsfunktionen ───────────────────────────────────────────────────────
|
||||
|
||||
function show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
|
||||
function hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
||||
function esc(s) { return String(s).replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function showError(msg) {
|
||||
hide('loadingHint');
|
||||
const box = document.getElementById('errorBox');
|
||||
box.textContent = msg || 'Ein Fehler ist aufgetreten.';
|
||||
box.style.display = '';
|
||||
}
|
||||
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
src/main/resources/static/img/card_game.png
Normal file
BIN
src/main/resources/static/img/card_game.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 370 KiB |
BIN
src/main/resources/static/img/card_slowmo.png
Normal file
BIN
src/main/resources/static/img/card_slowmo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
BIN
src/main/resources/static/img/card_speedup.png
Normal file
BIN
src/main/resources/static/img/card_speedup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 381 KiB |
Reference in New Issue
Block a user