Weiter am BDSM Multiplayer gearbeitet und verknüpfung der verschiedenen games
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
java
|
||||
id("org.springframework.boot") version "3.5.11"
|
||||
id("org.springframework.boot") version "3.5.12"
|
||||
id("io.spring.dependency-management") version "1.1.7"
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.oaa.xxx.games.bdsm;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -47,29 +48,40 @@ public class BdsmGameDurchfuehren {
|
||||
if (level == 6) {
|
||||
return null;
|
||||
}
|
||||
AufgabeAnzeige anzeige = null;
|
||||
int nextInt = new Random().nextInt(1, 100);
|
||||
|
||||
// Sonderfälle: bleiben wie bisher (inkl. eigener interner Fallbacks)
|
||||
if (nextInt == 1) {
|
||||
anzeige = findUltimativeStrafe();
|
||||
AufgabeAnzeige anzeige = findUltimativeStrafe();
|
||||
if (anzeige != null) return anzeige;
|
||||
} else if (nextInt == 2) {
|
||||
anzeige = findSperreVerlaengern();
|
||||
} else if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) {
|
||||
anzeige = findeAufgabe();
|
||||
} else if (nextInt > wahrscheinlichkeitSperre + 2) {
|
||||
anzeige = findeStrafe();
|
||||
AufgabeAnzeige anzeige = findSperreVerlaengern();
|
||||
if (anzeige != null) return anzeige;
|
||||
} else {
|
||||
anzeige = findeSperre();
|
||||
}
|
||||
if (anzeige == null) {
|
||||
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
|
||||
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
|
||||
String text = "Ups, da ist etwas schief gelaufen. Keine potenzielle Aufgabe gefunden. Entweder seid ihr inzwischen so gut weggesperrt, dass wirklich keine Aufgaben mehr zur Verfügung stehen, oder uns ist ein Fehler unterlaufen. {AKTIV} und {PASSIV} überbrücken die Zeit mit ein wenig Petting.";
|
||||
anzeige = new AufgabeAnzeige();
|
||||
anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : "");
|
||||
setMitspielerInfo(anzeige, aktiv);
|
||||
anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?"));
|
||||
anzeige.setTimer(120);
|
||||
// Reihenfolge der Kategorien: gewürfelte zuerst, dann die anderen
|
||||
List<Supplier<AufgabeAnzeige>> reihenfolge;
|
||||
if (nextInt > wahrscheinlichkeitSperre + wahrscheinlichkeitStrafe + 2) {
|
||||
reihenfolge = List.of(this::findeAufgabe, this::findeStrafe, this::findeSperre);
|
||||
} else if (nextInt > wahrscheinlichkeitSperre + 2) {
|
||||
reihenfolge = List.of(this::findeStrafe, this::findeAufgabe, this::findeSperre);
|
||||
} else {
|
||||
reihenfolge = List.of(this::findeSperre, this::findeStrafe, this::findeAufgabe);
|
||||
}
|
||||
for (Supplier<AufgabeAnzeige> finder : reihenfolge) {
|
||||
AufgabeAnzeige anzeige = finder.get();
|
||||
if (anzeige != null) return anzeige;
|
||||
}
|
||||
}
|
||||
|
||||
// Echtes Fallback: nur wenn wirklich keine Kategorie eine Aufgabe liefert
|
||||
Mitspieler aktiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_AKTIV);
|
||||
Mitspieler passiv = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, aktiv);
|
||||
String text = "Ups, da ist etwas schief gelaufen. Keine potenzielle Aufgabe gefunden. Entweder seid ihr inzwischen so gut weggesperrt, dass wirklich keine Aufgaben mehr zur Verfügung stehen, oder uns ist ein Fehler unterlaufen. {AKTIV} und {PASSIV} überbrücken die Zeit mit ein wenig Petting.";
|
||||
AufgabeAnzeige anzeige = new AufgabeAnzeige();
|
||||
anzeige.setNameAktiverMitspieler(aktiv != null ? aktiv.getName() : "");
|
||||
setMitspielerInfo(anzeige, aktiv);
|
||||
anzeige.setAufgabeText(getAnzeigeText(text, aktiv != null ? aktiv.getName() : "?", passiv != null ? passiv.getName() : "?"));
|
||||
anzeige.setTimer(120);
|
||||
return anzeige;
|
||||
}
|
||||
|
||||
@@ -84,16 +96,21 @@ public class BdsmGameDurchfuehren {
|
||||
mitspieler.stream().filter(m -> geschlecht == m.getGeschlecht()).toList().forEach(cumming -> {
|
||||
var partner = findeMitspielerMitRolle(RolleEnum.AUFGABE_PASSIV, cumming);
|
||||
var finishers = aufgabenList.getFinisher().stream()
|
||||
.filter(finisher -> geschlecht == finisher.getGeschlecht())
|
||||
.filter(finisher -> geschlecht == finisher.getGeschlecht() && finisher.isAufgabePassend(partner, cumming))
|
||||
.toList();
|
||||
if (!finishers.isEmpty()) {
|
||||
var aufgabe = finishers.get(new Random().nextInt(list.size()));
|
||||
AufgabeAnzeige anzeige = new AufgabeAnzeige();
|
||||
var aufgabe = finishers.get(new Random().nextInt(finishers.size()));
|
||||
var anzeige = new AufgabeAnzeige();
|
||||
anzeige.setNameAktiverMitspieler(cumming.getName());
|
||||
setMitspielerInfo(anzeige, cumming);
|
||||
anzeige.setAufgabeText(getAnzeigeText(aufgabe.getText(),
|
||||
cumming.getName(), partner != null ? partner.getName() : ""));
|
||||
list.add(anzeige);
|
||||
} else {
|
||||
var anzeige = new AufgabeAnzeige();
|
||||
anzeige.setNameAktiverMitspieler(cumming.getName());
|
||||
anzeige.setAufgabeText(cumming.getName() + "geht heute leider leer aus...");
|
||||
list.add(anzeige);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -177,7 +194,7 @@ public class BdsmGameDurchfuehren {
|
||||
}
|
||||
}
|
||||
}
|
||||
return findeStrafe();
|
||||
return null;
|
||||
}
|
||||
|
||||
private AufgabeAnzeige findeStrafe() {
|
||||
@@ -205,7 +222,7 @@ public class BdsmGameDurchfuehren {
|
||||
}
|
||||
}
|
||||
}
|
||||
return findeSperre();
|
||||
return null;
|
||||
}
|
||||
|
||||
private AufgabeAnzeige findeSperre() {
|
||||
|
||||
@@ -13,6 +13,7 @@ public class Mitspieler {
|
||||
private UUID id;
|
||||
private UUID userId;
|
||||
private boolean eigenesGeraet;
|
||||
private boolean sperrenVorFinaleAufloesen = true;
|
||||
private String name;
|
||||
private GeschlechtEnum geschlecht;
|
||||
private List<GeschlechtEnum> spieltMit;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.oaa.xxx.games.bdsm.aufgaben;
|
||||
|
||||
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
|
||||
import de.oaa.xxx.games.bdsm.Mitspieler;
|
||||
import de.oaa.xxx.games.bdsm.Werkzeug;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -23,4 +24,24 @@ public class Finisher {
|
||||
public String toString() {
|
||||
return "Finisher[id=" + finisherId + ", kurzText=" + kurzText + ", geschlecht=" + geschlecht + "]";
|
||||
}
|
||||
|
||||
public boolean isAufgabePassend(Mitspieler aktiv, Mitspieler passiv) {
|
||||
if (benoetigtPassiv != null) {
|
||||
for (Werkzeug werkzeug : benoetigtPassiv) {
|
||||
if (!passiv.isVerfuegbar(werkzeug)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (benoetigtAktiv == null || benoetigtAktiv.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
for (Werkzeug werkzeug : benoetigtAktiv) {
|
||||
if (aktiv.isVerfuegbar(werkzeug)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package de.oaa.xxx.games.bdsm.controller;
|
||||
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity.Status;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -20,12 +19,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity.Status;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.social.repository.FriendshipRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/bdsm/einladung")
|
||||
@@ -49,6 +49,7 @@ public class BdsmEinladungController {
|
||||
|
||||
record EinladungRequest(UUID setupId, int slotIndex, UUID inviteeId) {}
|
||||
record AntwortRequest(boolean accepted, String mode) {} // mode: OWN_DEVICE | HOST_DEVICE
|
||||
record SpielerDatenRequest(String spielerDatenJson) {}
|
||||
|
||||
private UUID currentUserId(Principal principal) {
|
||||
return userRepository.findByEmail(principal.getName())
|
||||
@@ -66,6 +67,16 @@ public class BdsmEinladungController {
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
|
||||
// Prüfen ob Person bereits aktiv eingeladen oder Teil des Spiels
|
||||
boolean alreadyInvited = einladungRepository.findBySetupId(req.setupId()).stream()
|
||||
.anyMatch(e -> req.inviteeId().equals(e.getInviteeId())
|
||||
&& (e.getStatus() == Status.PENDING
|
||||
|| e.getStatus() == Status.ACCEPTED_OWN
|
||||
|| e.getStatus() == Status.ACCEPTED_HOST));
|
||||
if (alreadyInvited) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
|
||||
// Alte Einladung für diesen Slot canceln
|
||||
einladungRepository.findBySetupId(req.setupId()).stream()
|
||||
.filter(e -> e.getSlotIndex() == req.slotIndex() && e.getStatus() == Status.PENDING)
|
||||
@@ -114,6 +125,16 @@ public class BdsmEinladungController {
|
||||
return ResponseEntity.ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/meine-aktive")
|
||||
public ResponseEntity<Map<String, Object>> getAktive(Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
return einladungRepository.findByInviteeIdAndStatus(userId, Status.ACCEPTED_OWN)
|
||||
.stream().findFirst()
|
||||
.map(e -> ResponseEntity.ok(toMap(e)))
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
@GetMapping("/pending")
|
||||
public ResponseEntity<List<Map<String, Object>>> getPending(Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
@@ -163,6 +184,19 @@ public class BdsmEinladungController {
|
||||
return ResponseEntity.ok(m);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/spielerdaten")
|
||||
public ResponseEntity<Void> spielerDatenEinreichen(@PathVariable UUID id, @RequestBody SpielerDatenRequest req, Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);
|
||||
if (e == null) return ResponseEntity.notFound().build();
|
||||
if (!e.getInviteeId().equals(userId)) return ResponseEntity.status(403).build();
|
||||
if (e.getStatus() != Status.ACCEPTED_OWN) return ResponseEntity.badRequest().build();
|
||||
e.setSpielerDatenJson(req.spielerDatenJson());
|
||||
e.setBereit(true);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/antwort")
|
||||
public ResponseEntity<Void> antwort(@PathVariable UUID id, @RequestBody AntwortRequest req, Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
@@ -190,6 +224,9 @@ public class BdsmEinladungController {
|
||||
m.put("inviterId", e.getInviterId());
|
||||
m.put("status", e.getStatus().name());
|
||||
m.put("sessionId", e.getSessionId());
|
||||
m.put("bereit", e.isBereit());
|
||||
m.put("spielerDatenJson", e.getSpielerDatenJson());
|
||||
userRepository.findById(e.getInviteeId()).ifPresent(u -> m.put("inviteeName", u.getName()));
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
package de.oaa.xxx.games.bdsm.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
|
||||
import de.oaa.xxx.games.bdsm.Mitspieler;
|
||||
import de.oaa.xxx.games.bdsm.GeschlechtEnum;
|
||||
import de.oaa.xxx.games.bdsm.Werkzeug;
|
||||
import de.oaa.xxx.games.bdsm.BdsmGame;
|
||||
import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren;
|
||||
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
|
||||
import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten;
|
||||
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
|
||||
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
|
||||
import de.oaa.xxx.games.history.GameHistoryEntity;
|
||||
import de.oaa.xxx.games.history.GameHistoryRepository;
|
||||
import de.oaa.xxx.games.history.GameRole;
|
||||
import de.oaa.xxx.games.history.GameType;
|
||||
import de.oaa.xxx.social.SystemMessageService;
|
||||
import de.oaa.xxx.social.entity.MessageCause;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -37,9 +48,13 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
import java.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@@ -48,6 +63,8 @@ import java.util.UUID;
|
||||
public class BdsmGameController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class);
|
||||
/** Kurzlebiger In-Memory-Marker: Sessions die ordentlich über spielAbgeschlossen beendet wurden. */
|
||||
private static final Set<UUID> ORDENTLICH_BEENDET = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private final BdsmGameRepository sessionRepository;
|
||||
private final MitspielerRepository mitspielerRepository;
|
||||
@@ -56,11 +73,14 @@ public class BdsmGameController {
|
||||
private final GameHistoryRepository gameHistoryRepository;
|
||||
private final BdsmEinladungRepository einladungRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SystemMessageService systemMessageService;
|
||||
private final CardlockRepository cardlockRepository;
|
||||
|
||||
public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
|
||||
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
|
||||
GameHistoryRepository gameHistoryRepository, BdsmEinladungRepository einladungRepository,
|
||||
ObjectMapper objectMapper) {
|
||||
ObjectMapper objectMapper, SystemMessageService systemMessageService,
|
||||
CardlockRepository cardlockRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.mitspielerRepository = mitspielerRepository;
|
||||
this.aktiveSperreRepository = aktiveSperreRepository;
|
||||
@@ -68,6 +88,8 @@ public class BdsmGameController {
|
||||
this.gameHistoryRepository = gameHistoryRepository;
|
||||
this.einladungRepository = einladungRepository;
|
||||
this.objectMapper = objectMapper;
|
||||
this.systemMessageService = systemMessageService;
|
||||
this.cardlockRepository = cardlockRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/{sessionId}")
|
||||
@@ -89,6 +111,15 @@ public class BdsmGameController {
|
||||
String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
UUID userId = userRepository.findByEmail(email).map(u -> u.getUserId()).orElse(null);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
var existingOpt = sessionRepository.findByUserId(userId);
|
||||
if (existingOpt.isPresent()) {
|
||||
BdsmGameEntity existing = existingOpt.get();
|
||||
if (existing.getAufgaben() != null) return ResponseEntity.status(409).build();
|
||||
// Unvollständige Session (aufgaben=null) bereinigen
|
||||
aktiveSperreRepository.deleteAll(existing.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(existing.getMitspieler());
|
||||
sessionRepository.delete(existing);
|
||||
}
|
||||
BdsmGameEntity entity = new BdsmGameEntity();
|
||||
entity.setSessionId(UUID.randomUUID());
|
||||
entity.setUserId(userId);
|
||||
@@ -103,13 +134,6 @@ public class BdsmGameController {
|
||||
entity.setLevel(1);
|
||||
entity.setSetupId(session.getSetupId());
|
||||
sessionRepository.save(entity);
|
||||
// Akzeptierte Einladungen mit der neuen Session verknüpfen
|
||||
if (session.getSetupId() != null) {
|
||||
einladungRepository.findBySetupId(session.getSetupId()).stream()
|
||||
.filter(e -> e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_OWN
|
||||
|| e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_HOST)
|
||||
.forEach(e -> e.setSessionId(entity.getSessionId()));
|
||||
}
|
||||
LOGGER.debug("BdsmGame gestartet [sessionId={}, userId={}, aufgabenProLevel={}, wahrscheinlichkeitStrafe={}%, wahrscheinlichkeitSperre={}%, zeitfaktorZeitstrafen={}]",
|
||||
entity.getSessionId(), entity.getUserId(), entity.getAufgabenProLevel(),
|
||||
entity.getWahrscheinlichkeitStrafe(), entity.getWahrscheinlichkeitSperre(),
|
||||
@@ -123,17 +147,6 @@ public class BdsmGameController {
|
||||
public ResponseEntity<Void> deleteSession(@RequestBody BdsmGame session) {
|
||||
return sessionRepository.findById(session.getSessionId())
|
||||
.map(entity -> {
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
|
||||
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameType(GameType.BDSM);
|
||||
entry.setStartTime(entity.getStartZeit());
|
||||
entry.setEndTime(endTime);
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
|
||||
gameHistoryRepository.save(entry);
|
||||
|
||||
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(entity.getMitspieler());
|
||||
sessionRepository.delete(entity);
|
||||
@@ -142,6 +155,87 @@ public class BdsmGameController {
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
@PostMapping("/{sessionId}/abgeschlossen")
|
||||
public ResponseEntity<Void> spielAbgeschlossen(@PathVariable UUID sessionId) {
|
||||
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
|
||||
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameName("BDSM Game");
|
||||
entry.setGameType(GameType.BDSM);
|
||||
entry.setStartTime(entity.getStartZeit());
|
||||
entry.setEndTime(endTime);
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER));
|
||||
gameHistoryRepository.save(entry);
|
||||
|
||||
// BDSM-XP für alle Teilnehmer gutschreiben (Minuten = XP)
|
||||
int xp = (int) durationMinutes;
|
||||
userRepository.findById(entity.getUserId()).ifPresent(u -> {
|
||||
u.setBdsmXp(u.getBdsmXp() + xp);
|
||||
userRepository.save(u);
|
||||
});
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> {
|
||||
u.setBdsmXp(u.getBdsmXp() + xp);
|
||||
userRepository.save(u);
|
||||
}));
|
||||
|
||||
// Eigene-Gerät-Gäste über ordentliches Spielende benachrichtigen
|
||||
ORDENTLICH_BEENDET.add(sessionId);
|
||||
String endNachricht = "Das BDSM-Spiel wurde erfolgreich beendet. Danke fürs Mitspielen! 🎉";
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.isEigenesGeraet() && m.getUserId() != null)
|
||||
.forEach(m -> systemMessageService.send(entity.getUserId(), m.getUserId(),
|
||||
endNachricht, "/userhome.html", MessageCause.GAME_STATE));
|
||||
|
||||
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(entity.getMitspieler());
|
||||
sessionRepository.delete(entity);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
/** Prüft ob eine Session ordentlich (nicht abgebrochen) beendet wurde. */
|
||||
@GetMapping("/{sessionId}/beendet")
|
||||
public ResponseEntity<Void> istBeendet(@PathVariable UUID sessionId) {
|
||||
if (ORDENTLICH_BEENDET.remove(sessionId)) return ResponseEntity.ok().build();
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{sessionId}/verlassen")
|
||||
public ResponseEntity<Void> verlasseSpiel(@PathVariable UUID sessionId, Principal principal) {
|
||||
UUID userId = userRepository.findByEmail(principal.getName()).map(u -> u.getUserId()).orElse(null);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
|
||||
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (session == null) return ResponseEntity.notFound().build();
|
||||
|
||||
MitspielerEntity self = session.getMitspieler().stream()
|
||||
.filter(m -> userId.equals(m.getUserId()))
|
||||
.findFirst().orElse(null);
|
||||
if (self == null) return ResponseEntity.status(403).build();
|
||||
|
||||
String name = self.getName();
|
||||
String nachricht = name + " hat das BDSM-Spiel verlassen. Das Spiel wurde abgebrochen.";
|
||||
|
||||
systemMessageService.send(userId, session.getUserId(), nachricht, "/userhome.html", MessageCause.GAME_STATE);
|
||||
session.getMitspieler().stream()
|
||||
.filter(m -> m.isEigenesGeraet() && m.getUserId() != null && !userId.equals(m.getUserId()))
|
||||
.forEach(m -> systemMessageService.send(userId, m.getUserId(), nachricht, "/userhome.html", MessageCause.GAME_STATE));
|
||||
|
||||
aktiveSperreRepository.deleteAll(session.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(session.getMitspieler());
|
||||
sessionRepository.delete(session);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{sessionId}/aufgaben")
|
||||
public ResponseEntity<Void> setAufgaben(@RequestBody AufgabenList list, @PathVariable UUID sessionId) {
|
||||
try {
|
||||
@@ -155,6 +249,13 @@ public class BdsmGameController {
|
||||
}
|
||||
session.setAufgaben(aufgaben);
|
||||
sessionRepository.save(session);
|
||||
// Erst jetzt Einladungen mit der Session verknüpfen – Gäste werden nur weitergeleitet wenn aufgaben bereit sind
|
||||
if (session.getSetupId() != null) {
|
||||
einladungRepository.findBySetupId(session.getSetupId()).stream()
|
||||
.filter(e -> e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_OWN
|
||||
|| e.getStatus() == BdsmEinladungEntity.Status.ACCEPTED_HOST)
|
||||
.forEach(e -> e.setSessionId(session.getSessionId()));
|
||||
}
|
||||
return ResponseEntity.accepted().build();
|
||||
} catch (Exception exception) {
|
||||
LOGGER.error(exception.getMessage(), exception);
|
||||
@@ -166,7 +267,7 @@ public class BdsmGameController {
|
||||
public ResponseEntity<AufgabeAnzeige> getNextAufgabe(@PathVariable UUID sessionId) {
|
||||
try {
|
||||
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (session == null) {
|
||||
if (session == null || session.getAufgaben() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
session.setLetzteAktivitaet(LocalDateTime.now());
|
||||
@@ -210,11 +311,41 @@ public class BdsmGameController {
|
||||
entity.setName(mitspieler.getName());
|
||||
entity.setRollen(mitspieler.getRollen());
|
||||
entity.setSpieltMit(mitspieler.getSpieltMit());
|
||||
entity.setWerkzeuge(mitspieler.getVerfuegbareWerkzeuge());
|
||||
entity.setWerkzeuge(new ArrayList<>(mitspieler.getVerfuegbareWerkzeuge()));
|
||||
entity.setUserId(mitspieler.getUserId());
|
||||
entity.setEigenesGeraet(mitspieler.isEigenesGeraet());
|
||||
entity.setSperrenVorFinaleAufloesen(mitspieler.isSperrenVorFinaleAufloesen());
|
||||
entity.setSession(session);
|
||||
mitspielerRepository.save(entity);
|
||||
|
||||
// Aktive Chastity-Lockees: 365-Tage-Zeitstrafe auf das gesperrte Körperteil
|
||||
if (mitspieler.getUserId() != null
|
||||
&& cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(mitspieler.getUserId())) {
|
||||
List<Werkzeug> locked = new ArrayList<>();
|
||||
if (mitspieler.getGeschlecht() == GeschlechtEnum.WEIBLICH) locked.add(Werkzeug.VAGINA);
|
||||
else if (mitspieler.getGeschlecht() == GeschlechtEnum.MAENNLICH) locked.add(Werkzeug.PENIS);
|
||||
else { locked.add(Werkzeug.VAGINA); locked.add(Werkzeug.PENIS); }
|
||||
|
||||
if (!locked.isEmpty()) {
|
||||
// Gesperrte Werkzeuge force-hinzufügen (auch wenn Checkbox nicht angekreuzt)
|
||||
locked.forEach(w -> { if (!entity.getWerkzeuge().contains(w)) entity.getWerkzeuge().add(w); });
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
AktiveSperreEntity chastitySperre = new AktiveSperreEntity();
|
||||
chastitySperre.setAktiveSperreId(UUID.randomUUID());
|
||||
chastitySperre.setMitspieler(entity);
|
||||
chastitySperre.setSession(session);
|
||||
chastitySperre.setFuer(locked);
|
||||
chastitySperre.setMinuten(1440);
|
||||
chastitySperre.setStartzeit(now);
|
||||
chastitySperre.setEndzeit(now.plusHours(24));
|
||||
chastitySperre.setReleaseText(entity.getName() + " hat die Keuschheit durchgehalten – das Schloss ist ab sofort offen.");
|
||||
aktiveSperreRepository.save(chastitySperre);
|
||||
// Werkzeug für die Spieldauer durch die Zeitstrafe sperren
|
||||
locked.forEach(entity.getWerkzeuge()::remove);
|
||||
mitspielerRepository.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@@ -267,6 +398,55 @@ public class BdsmGameController {
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
record AbschliessenRequest(boolean sperreAnwenden) {}
|
||||
record SperreFreigabe(String text, UUID mitspielerId, boolean eigenesGeraet) {}
|
||||
record AbschliessenResponse(List<SperreFreigabe> abgelaufeneSperren) {}
|
||||
|
||||
@PostMapping("/{sessionId}/active-task/abschliessen")
|
||||
public ResponseEntity<AbschliessenResponse> activeTaskAbschliessen(
|
||||
@PathVariable UUID sessionId, @RequestBody AbschliessenRequest req) {
|
||||
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (session == null) return ResponseEntity.notFound().build();
|
||||
|
||||
SperreVerarbeiten sperreVerarbeiten = new SperreVerarbeiten();
|
||||
|
||||
if (req.sperreAnwenden() && session.getActiveTaskJson() != null) {
|
||||
try {
|
||||
JsonNode task = objectMapper.readTree(session.getActiveTaskJson());
|
||||
JsonNode cb = task.get("callback");
|
||||
if (cb != null && !cb.isNull()) {
|
||||
if (cb.has("sperreId") && !cb.get("sperreId").isNull()) {
|
||||
SperreCallback callback = objectMapper.treeToValue(cb, SperreCallback.class);
|
||||
callback.setSessionId(sessionId);
|
||||
sperreVerarbeiten.sperreAnwenden(callback, sessionRepository, mitspielerRepository, aktiveSperreRepository);
|
||||
LOGGER.info("Zeitstrafe via abschliessen angewandt [session={}, spieler={}]", sessionId, callback.getSpielerId());
|
||||
} else if (cb.has("faktor") && !cb.get("faktor").isNull()) {
|
||||
SperrenVerlaengernCallback callback = objectMapper.treeToValue(cb, SperrenVerlaengernCallback.class);
|
||||
List<AktiveSperreEntity> locks = aktiveSperreRepository.findAktiveLocks(callback.getSpielerId());
|
||||
locks.forEach(lock -> sperreVerarbeiten.sperreVerlaengern(lock, callback.getFaktor(), aktiveSperreRepository));
|
||||
LOGGER.info("Sperren via abschliessen verlängert [session={}, spieler={}, faktor={}]", sessionId, callback.getSpielerId(), callback.getFaktor());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Fehler beim Verarbeiten des Callbacks beim Abschließen: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
session.setActiveTaskJson(null);
|
||||
session.setTaskStartedAt(null);
|
||||
sessionRepository.save(session);
|
||||
|
||||
List<SperreFreigabe> freigaben = new ArrayList<>();
|
||||
aktiveSperreRepository.findAbgelaufene(sessionId, LocalDateTime.now()).forEach(s -> {
|
||||
UUID mitspielerId = s.getMitspieler().getMitspielerId();
|
||||
boolean eigenesGeraet = s.getMitspieler().isEigenesGeraet();
|
||||
String t = sperreVerarbeiten.sperreAufheben(s, aktiveSperreRepository, mitspielerRepository);
|
||||
if (t != null && !t.isBlank()) freigaben.add(new SperreFreigabe(t, mitspielerId, eigenesGeraet));
|
||||
});
|
||||
|
||||
return ResponseEntity.ok(new AbschliessenResponse(freigaben));
|
||||
}
|
||||
|
||||
record ActiveTaskRequest(String taskJson, LocalDateTime timerStartedAt) {}
|
||||
record ActiveTaskResponse(String taskJson, Long elapsedSeconds) {}
|
||||
|
||||
@@ -302,6 +482,238 @@ public class BdsmGameController {
|
||||
return ResponseEntity.ok(new ActiveTaskResponse(session.getActiveTaskJson(), elapsed));
|
||||
}
|
||||
|
||||
// ── Keyholder-Angebot: prüft ob am Ende eine VAGINA/PENIS-Sperre vorliegt ──
|
||||
@GetMapping("/{sessionId}/keyholder-angebot")
|
||||
public ResponseEntity<Map<String, Object>> keyholderAngebot(@PathVariable UUID sessionId) {
|
||||
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (session == null) return ResponseEntity.notFound().build();
|
||||
|
||||
// Alle noch in der DB vorhandenen VAGINA/PENIS-Sperren – auch abgelaufene,
|
||||
// da im Finale-Flow bereits abgelaufene Sperren noch nicht formal aufgehoben wurden.
|
||||
List<AktiveSperreEntity> relevantesSperren = session.getAktiveSperren().stream()
|
||||
.filter(s -> s.getFuer().contains(Werkzeug.VAGINA) || s.getFuer().contains(Werkzeug.PENIS))
|
||||
.toList();
|
||||
|
||||
for (AktiveSperreEntity sperre : relevantesSperren) {
|
||||
MitspielerEntity lockee = sperre.getMitspieler();
|
||||
if (lockee == null || lockee.getUserId() == null || lockee.getGeschlecht() == null) continue;
|
||||
// Kein Angebot wenn Lockee bereits aktiv in einem Chastity-Game gesperrt ist
|
||||
if (cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockee.getUserId())) continue;
|
||||
|
||||
for (MitspielerEntity kandidat : session.getMitspieler()) {
|
||||
if (kandidat.getMitspielerId().equals(lockee.getMitspielerId())) continue;
|
||||
if (kandidat.getUserId() == null) continue;
|
||||
if (!kandidat.getSpieltMit().contains(lockee.getGeschlecht())) continue;
|
||||
List<CardLockEntity> locks = cardlockRepository.findByKeyholderAndUnlockTimeIsNull(kandidat.getUserId());
|
||||
if (locks.isEmpty()) continue;
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("lockeeId", lockee.getMitspielerId());
|
||||
result.put("lockeeName", lockee.getName());
|
||||
result.put("lockeeUserId", lockee.getUserId());
|
||||
result.put("keyholderMitspielerId", kandidat.getMitspielerId());
|
||||
result.put("keyholderName", kandidat.getName());
|
||||
result.put("keyholderUserId", kandidat.getUserId());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/{sessionId}/keyholder-locks")
|
||||
public ResponseEntity<List<Map<String, Object>>> keyholderLocks(
|
||||
@PathVariable UUID sessionId, @RequestParam UUID keyholderUserId) {
|
||||
BdsmGameEntity session = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (session == null) return ResponseEntity.notFound().build();
|
||||
|
||||
List<Map<String, Object>> result = cardlockRepository.findByKeyholderAndUnlockTimeIsNull(keyholderUserId).stream()
|
||||
.map(l -> {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("lockId", l.getLockId());
|
||||
item.put("name", l.getName() != null ? l.getName() : "Unbenanntes Lock");
|
||||
item.put("pickEveryMinute", l.getPickEveryMinute());
|
||||
item.put("totalCards", l.getInitialCards() != null ? l.getInitialCards().size() : 0);
|
||||
item.put("active", l.getStartTime() != null && l.getUnlockTime() == null);
|
||||
return item;
|
||||
})
|
||||
.toList();
|
||||
|
||||
if (result.isEmpty()) return ResponseEntity.noContent().build();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
record ZuChastityRequest(UUID lockId, UUID lockeeUserId, UUID keyholderUserId) {}
|
||||
|
||||
@PostMapping("/{sessionId}/zu-chastity")
|
||||
public ResponseEntity<Map<String, Object>> zuChastity(
|
||||
@PathVariable UUID sessionId, @RequestBody ZuChastityRequest req) {
|
||||
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
|
||||
CardLockEntity template = cardlockRepository.findById(req.lockId()).orElse(null);
|
||||
if (template == null) return ResponseEntity.badRequest().build();
|
||||
|
||||
// Lockee darf kein aktives Chastity-Lock haben
|
||||
if (req.lockeeUserId() != null
|
||||
&& cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(req.lockeeUserId())) {
|
||||
return ResponseEntity.status(409).build();
|
||||
}
|
||||
|
||||
// Neues Lock mit Template-Einstellungen für den BDSM-Lockee erstellen
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
CardLockEntity newLock = new CardLockEntity();
|
||||
newLock.setName(template.getName());
|
||||
newLock.setLockee(req.lockeeUserId());
|
||||
newLock.setKeyholder(req.keyholderUserId());
|
||||
newLock.setInitialCards(template.getInitialCards());
|
||||
newLock.setPickEveryMinute(template.getPickEveryMinute());
|
||||
newLock.setAccumulatePicks(template.isAccumulatePicks());
|
||||
newLock.setShowRemainingCards(template.isShowRemainingCards());
|
||||
newLock.setLatestOpeningtime(template.getLatestOpeningtime());
|
||||
newLock.setHygineOpeningDurationMinutes(template.getHygineOpeningDurationMinutes());
|
||||
newLock.setHygineOpeningEveryMinites(template.getHygineOpeningEveryMinites());
|
||||
newLock.setTasks(template.getTasks());
|
||||
newLock.setRequiresVerification(template.isRequiresVerification());
|
||||
newLock.setTestLock(false);
|
||||
newLock.setTaskCardMode(template.getTaskCardMode());
|
||||
int codeLines = template.getUnlockCodeLines() != null ? template.getUnlockCodeLines() : 5;
|
||||
newLock.setUnlockCodeLines(codeLines);
|
||||
StringBuilder codeBuilder = new StringBuilder();
|
||||
java.util.Random rng = new java.util.Random();
|
||||
for (int i = 0; i < codeLines; i++) codeBuilder.append(rng.nextInt(10));
|
||||
newLock.setUnlockCode(codeBuilder.toString());
|
||||
newLock.setStartTime(now);
|
||||
newLock.setAvailableCards(template.getInitialCards() != null
|
||||
? new java.util.ArrayList<>(template.getInitialCards()) : new java.util.ArrayList<>());
|
||||
newLock.setOpenPicks(0);
|
||||
if (template.getPickEveryMinute() != null) {
|
||||
newLock.setNextCardIn(now.plusMinutes(template.getPickEveryMinute()));
|
||||
}
|
||||
if (template.getHygineOpeningEveryMinites() != null) {
|
||||
newLock.setLastHygineOpening(now);
|
||||
}
|
||||
cardlockRepository.save(newLock);
|
||||
|
||||
// Lockee benachrichtigen (falls UserAccount vorhanden)
|
||||
if (req.lockeeUserId() != null) {
|
||||
userRepository.findById(req.keyholderUserId()).ifPresent(keyholder ->
|
||||
systemMessageService.send(req.keyholderUserId(), req.lockeeUserId(),
|
||||
keyholder.getName() + " hat nach dem BDSM Game ein Chastity Lock auf dich gesetzt.",
|
||||
"/activelock.html", MessageCause.GAME_STATE));
|
||||
}
|
||||
|
||||
// Spielabschluss-Logik (wie spielAbgeschlossen, aber ohne eigenen Delete)
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
long durationMinutes = Duration.between(entity.getStartZeit(), endTime).toMinutes();
|
||||
GameHistoryEntity entry = new GameHistoryEntity();
|
||||
entry.setGameName("BDSM Game");
|
||||
entry.setGameType(GameType.BDSM);
|
||||
entry.setStartTime(entity.getStartZeit());
|
||||
entry.setEndTime(endTime);
|
||||
entry.setDurationMinutes(durationMinutes);
|
||||
entry.addParticipant(entity.getUserId(), GameRole.PLAYER);
|
||||
entity.getMitspieler().stream()
|
||||
.filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> entry.addParticipant(m.getUserId(), GameRole.PLAYER));
|
||||
gameHistoryRepository.save(entry);
|
||||
|
||||
int xp = (int) durationMinutes;
|
||||
userRepository.findById(entity.getUserId()).ifPresent(u -> { u.setBdsmXp(u.getBdsmXp() + xp); userRepository.save(u); });
|
||||
entity.getMitspieler().stream().filter(m -> m.getUserId() != null)
|
||||
.forEach(m -> userRepository.findById(m.getUserId()).ifPresent(u -> { u.setBdsmXp(u.getBdsmXp() + xp); userRepository.save(u); }));
|
||||
|
||||
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
|
||||
mitspielerRepository.deleteAll(entity.getMitspieler());
|
||||
sessionRepository.delete(entity);
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("lockId", newLock.getLockId().toString());
|
||||
response.put("unlockCode", newLock.getUnlockCode());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/** Gibt zurück welches Werkzeug für einen User durch ein aktives Chastity-Lock blockiert ist. */
|
||||
@GetMapping("/chastity-constraint")
|
||||
public ResponseEntity<Map<String, Object>> chastityConstraint(@RequestParam UUID userId) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
if (!cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(userId)) {
|
||||
result.put("lockedWerkzeug", null);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
return userRepository.findById(userId).map(u -> {
|
||||
String werkzeug = null;
|
||||
if (u.getGeschlecht() != null) {
|
||||
werkzeug = switch (u.getGeschlecht().name()) {
|
||||
case "WEIBLICH" -> "VAGINA";
|
||||
case "MAENNLICH" -> "PENIS";
|
||||
default -> "BOTH";
|
||||
};
|
||||
}
|
||||
result.put("lockedWerkzeug", werkzeug);
|
||||
return ResponseEntity.ok(result);
|
||||
}).orElseGet(() -> {
|
||||
result.put("lockedWerkzeug", null);
|
||||
return ResponseEntity.ok(result);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Debug-Endpoint: vollständiger Entity-Zustand ──
|
||||
@GetMapping("/{sessionId}/debug")
|
||||
public ResponseEntity<Map<String, Object>> debug(@PathVariable UUID sessionId) {
|
||||
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null);
|
||||
if (entity == null) return ResponseEntity.notFound().build();
|
||||
|
||||
Map<String, Object> session = new LinkedHashMap<>();
|
||||
session.put("sessionId", entity.getSessionId());
|
||||
session.put("userId", entity.getUserId());
|
||||
session.put("setupId", entity.getSetupId());
|
||||
session.put("startZeit", entity.getStartZeit());
|
||||
session.put("letzteAktivitaet", entity.getLetzteAktivitaet());
|
||||
session.put("level", entity.getLevel());
|
||||
session.put("aufgabenAufAktuellemLevel", entity.getAufgabenAufAktuellemLevel());
|
||||
session.put("aufgabenProLevel", entity.getAufgabenProLevel());
|
||||
session.put("wahrscheinlichkeitSperre", entity.getWahrscheinlichkeitSperre());
|
||||
session.put("wahrscheinlichkeitStrafe", entity.getWahrscheinlichkeitStrafe());
|
||||
session.put("zeitfaktorZeitstrafen", entity.getZeitfaktorZeitstrafen());
|
||||
session.put("taskStartedAt", entity.getTaskStartedAt());
|
||||
session.put("hatAufgaben", entity.getAufgaben() != null);
|
||||
session.put("hatActiveTask", entity.getActiveTaskJson() != null);
|
||||
|
||||
List<Map<String, Object>> mitspielerList = entity.getMitspieler().stream().map(m -> {
|
||||
Map<String, Object> mp = new LinkedHashMap<>();
|
||||
mp.put("mitspielerId", m.getMitspielerId());
|
||||
mp.put("name", m.getName());
|
||||
mp.put("userId", m.getUserId());
|
||||
mp.put("geschlecht", m.getGeschlecht());
|
||||
mp.put("rollen", m.getRollen());
|
||||
mp.put("werkzeuge", m.getWerkzeuge());
|
||||
mp.put("spieltMit", m.getSpieltMit());
|
||||
mp.put("eigenesGeraet", m.isEigenesGeraet());
|
||||
mp.put("sperrenVorFinaleAufloesen", m.isSperrenVorFinaleAufloesen());
|
||||
return mp;
|
||||
}).toList();
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<Map<String, Object>> sperrenList = entity.getAktiveSperren().stream().map(s -> {
|
||||
Map<String, Object> sp = new LinkedHashMap<>();
|
||||
sp.put("aktiveSperreId", s.getAktiveSperreId());
|
||||
sp.put("mitspielerName", s.getMitspieler() != null ? s.getMitspieler().getName() : null);
|
||||
sp.put("fuer", s.getFuer());
|
||||
sp.put("minuten", s.getMinuten());
|
||||
sp.put("startzeit", s.getStartzeit());
|
||||
sp.put("endzeit", s.getEndzeit());
|
||||
sp.put("abgelaufen", s.getEndzeit() != null && s.getEndzeit().isBefore(now));
|
||||
sp.put("releaseText", s.getReleaseText());
|
||||
return sp;
|
||||
}).toList();
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("session", session);
|
||||
result.put("mitspieler", mitspielerList);
|
||||
result.put("aktiveSperren", sperrenList);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
private BdsmGame toSession(BdsmGameEntity entity) {
|
||||
BdsmGame session = new BdsmGame();
|
||||
session.setSessionId(entity.getSessionId());
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package de.oaa.xxx.games.bdsm.controller;
|
||||
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmSetupDraftEntity;
|
||||
import de.oaa.xxx.games.bdsm.repository.BdsmSetupDraftRepository;
|
||||
import de.oaa.xxx.user.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/bdsm/setup-draft")
|
||||
@Transactional
|
||||
public class BdsmSetupDraftController {
|
||||
|
||||
private final BdsmSetupDraftRepository draftRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public BdsmSetupDraftController(BdsmSetupDraftRepository draftRepository, UserRepository userRepository) {
|
||||
this.draftRepository = draftRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
record DraftRequest(String setupId, String settingsJson, String setupJson, String gruppenJson) {}
|
||||
|
||||
private UUID currentUserId(Principal principal) {
|
||||
return userRepository.findByEmail(principal.getName()).map(u -> u.getUserId()).orElse(null);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getDraft(Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
return draftRepository.findByUserId(userId)
|
||||
.map(d -> {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("setupId", d.getSetupId());
|
||||
m.put("settingsJson", d.getSettingsJson());
|
||||
m.put("setupJson", d.getSetupJson());
|
||||
m.put("gruppenJson", d.getGruppenJson());
|
||||
return ResponseEntity.ok(m);
|
||||
})
|
||||
.orElse(ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public ResponseEntity<Void> saveDraft(@RequestBody DraftRequest req, Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
BdsmSetupDraftEntity d = draftRepository.findByUserId(userId)
|
||||
.orElseGet(() -> { BdsmSetupDraftEntity n = new BdsmSetupDraftEntity(); n.setUserId(userId); return n; });
|
||||
if (req.setupId() != null) d.setSetupId(req.setupId());
|
||||
if (req.settingsJson() != null) d.setSettingsJson(req.settingsJson());
|
||||
if (req.setupJson() != null) d.setSetupJson(req.setupJson());
|
||||
if (req.gruppenJson() != null) d.setGruppenJson(req.gruppenJson());
|
||||
d.setUpdatedAt(LocalDateTime.now());
|
||||
draftRepository.save(d);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity<Void> deleteDraft(Principal principal) {
|
||||
UUID userId = currentUserId(principal);
|
||||
if (userId == null) return ResponseEntity.status(401).build();
|
||||
draftRepository.findByUserId(userId).ifPresent(draftRepository::delete);
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,10 @@ public class BdsmEinladungEntity {
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String spielerDatenJson;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT false")
|
||||
private boolean bereit;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.UUID;
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "session")
|
||||
@Table(name = "bdsm_game")
|
||||
public class BdsmGameEntity {
|
||||
|
||||
@Id
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.oaa.xxx.games.bdsm.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "bdsm_setup_draft")
|
||||
public class BdsmSetupDraftEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "user_id")
|
||||
private UUID userId;
|
||||
|
||||
@Column(length = 36)
|
||||
private String setupId;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String settingsJson;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String setupJson;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String gruppenJson;
|
||||
|
||||
@Column
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -56,6 +56,8 @@ public class MitspielerEntity {
|
||||
@CollectionTable(name = "mitspieler_rollen", joinColumns = @JoinColumn(name = "mitspielerId"))
|
||||
@Column(name = "rolle")
|
||||
private List<RolleEnum> rollen = new ArrayList<>();
|
||||
@Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT true")
|
||||
private boolean sperrenVorFinaleAufloesen = true;
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "sessionId", nullable = false)
|
||||
private BdsmGameEntity session;
|
||||
@@ -78,6 +80,7 @@ public class MitspielerEntity {
|
||||
mitspieler.setRollen(rollen);
|
||||
mitspieler.setSpieltMit(spieltMit);
|
||||
mitspieler.setVerfuegbareWerkzeuge(new ArrayList<>(werkzeuge));
|
||||
mitspieler.setSperrenVorFinaleAufloesen(sperrenVorFinaleAufloesen);
|
||||
return mitspieler;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.oaa.xxx.games.bdsm.repository;
|
||||
|
||||
import de.oaa.xxx.games.bdsm.entity.BdsmSetupDraftEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface BdsmSetupDraftRepository extends JpaRepository<BdsmSetupDraftEntity, UUID> {
|
||||
Optional<BdsmSetupDraftEntity> findByUserId(UUID userId);
|
||||
}
|
||||
@@ -349,6 +349,7 @@ public class SocialController {
|
||||
showGrunddaten ? user.getBeschreibung() : null,
|
||||
showXp ? user.getLockeeXp() : 0,
|
||||
showXp ? user.getKeyholderXp() : 0,
|
||||
showXp ? user.getBdsmXp() : 0,
|
||||
user.getSichtbarkeitGrunddaten(),
|
||||
user.getSichtbarkeitGalerie(),
|
||||
user.getSichtbarkeitFreunde(),
|
||||
|
||||
@@ -22,6 +22,7 @@ public record UserProfile(
|
||||
String beschreibung,
|
||||
int lockeeXp,
|
||||
int keyholderXp,
|
||||
int bdsmXp,
|
||||
// Datenschutz-Einstellungen
|
||||
Sichtbarkeit sichtbarkeitGrunddaten,
|
||||
Sichtbarkeit sichtbarkeitGalerie,
|
||||
@@ -34,7 +35,7 @@ public record UserProfile(
|
||||
/** Compact constructor for contexts where profile details are not needed (friend list etc.) */
|
||||
public UserProfile(UUID userId, String name, String profilePicture, String profilePictureHq, String friendStatus) {
|
||||
this(userId, name, profilePicture, profilePictureHq, friendStatus,
|
||||
null, null, null, null, null, null, null, 0, 0,
|
||||
null, null, null, null, null, null, null, 0, 0, 0,
|
||||
null, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -255,6 +256,21 @@ public class UserController {
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}/bdsm-defaults")
|
||||
public ResponseEntity<Map<String, Object>> getBdsmDefaultsForUser(@PathVariable UUID userId) {
|
||||
var userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) return ResponseEntity.notFound().build();
|
||||
UserEntity user = userOpt.get();
|
||||
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
|
||||
.orElse(new BdsmDefaultsEntity());
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
result.put("geschlecht", user.getGeschlecht() != null ? user.getGeschlecht().name() : null);
|
||||
result.put("spieltMit", splitOrEmpty(d.getSpieltMit()));
|
||||
result.put("rollen", splitOrEmpty(d.getRollen()));
|
||||
result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge()));
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PutMapping("/me/bdsm-defaults")
|
||||
public ResponseEntity<Void> updateBdsmDefaults(@RequestBody BdsmDefaultsRequest request, Principal principal) {
|
||||
var userOpt = userRepository.findByEmail(principal.getName());
|
||||
|
||||
@@ -60,6 +60,9 @@ public class UserEntity {
|
||||
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
|
||||
private int keyholderXp;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "INT DEFAULT 0")
|
||||
private int bdsmXp;
|
||||
|
||||
// ── Datenschutz / Sichtbarkeit ──
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(length = 20, nullable = false, columnDefinition = "VARCHAR(20) DEFAULT 'ALLE'")
|
||||
|
||||
BIN
xxxthegame/src/main/resources/static/audio/alarm.mp3
Normal file
BIN
xxxthegame/src/main/resources/static/audio/alarm.mp3
Normal file
Binary file not shown.
BIN
xxxthegame/src/main/resources/static/audio/lvlup.mp3
Normal file
BIN
xxxthegame/src/main/resources/static/audio/lvlup.mp3
Normal file
Binary file not shown.
BIN
xxxthegame/src/main/resources/static/audio/ping.mp3
Normal file
BIN
xxxthegame/src/main/resources/static/audio/ping.mp3
Normal file
Binary file not shown.
BIN
xxxthegame/src/main/resources/static/audio/release.mp3
Normal file
BIN
xxxthegame/src/main/resources/static/audio/release.mp3
Normal file
Binary file not shown.
@@ -126,7 +126,7 @@
|
||||
<label for="sldAufgaben">Aufgaben pro Level</label>
|
||||
<span class="setting-value" id="valAufgaben">5</span>
|
||||
</div>
|
||||
<input type="range" id="sldAufgaben" min="4" max="20" value="5"
|
||||
<input type="range" id="sldAufgaben" min="1" max="20" value="5"
|
||||
oninput="document.getElementById('valAufgaben').textContent=this.value">
|
||||
</div>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function weiter() {
|
||||
async function weiter() {
|
||||
hideMessage();
|
||||
const strafe = parseInt(document.getElementById('sldStrafe').value);
|
||||
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
|
||||
@@ -174,12 +174,26 @@
|
||||
showMessage('Die kombinierten Wahrscheinlichkeiten dürfen 98 % nicht überschreiten.', 'error');
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem('bdsm-session-settings', JSON.stringify({
|
||||
const settings = {
|
||||
wahrscheinlichkeitStrafe: strafe,
|
||||
wahrscheinlichkeitSperre: zeitstrafe,
|
||||
aufgabenProLevel: parseInt(document.getElementById('sldAufgaben').value),
|
||||
zeitfaktorZeitstrafen: parseInt(document.getElementById('sldZeit').value) / 10,
|
||||
}));
|
||||
};
|
||||
// Immer neue Setup-ID → Mitspieler-Konfig und Einladungen gehören zur neuen Runde
|
||||
const newSetupId = crypto.randomUUID();
|
||||
sessionStorage.setItem('bdsm-setup-id', newSetupId);
|
||||
sessionStorage.setItem('bdsm-session-settings', JSON.stringify(settings));
|
||||
sessionStorage.removeItem('bdsm-session-setup');
|
||||
sessionStorage.removeItem('bdsm-session-gruppen');
|
||||
sessionStorage.removeItem('bdsm-session-toys');
|
||||
// Alten Draft löschen, neuen mit frischer Setup-ID anlegen
|
||||
try { await fetch('/bdsm/setup-draft', { method: 'DELETE' }); } catch (_) {}
|
||||
fetch('/bdsm/setup-draft', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ setupId: newSetupId, settingsJson: JSON.stringify(settings) }),
|
||||
}).catch(() => {});
|
||||
window.location.href = '/bdsmplayers.html';
|
||||
}
|
||||
|
||||
@@ -248,6 +262,7 @@
|
||||
});
|
||||
} catch (_) { /* ignorieren */ }
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
|
||||
(async function checkAktiveSession() {
|
||||
@@ -256,42 +271,86 @@
|
||||
if (!meRes.ok) return;
|
||||
const user = await meRes.json();
|
||||
|
||||
// 1. Prüfen ob User selbst eine aktive Host-Session hat
|
||||
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
if (sessionRes.status === 204) return;
|
||||
if (!sessionRes.ok) return;
|
||||
if (sessionRes.ok) {
|
||||
const session = await sessionRes.json();
|
||||
zeigeModal(
|
||||
'Aktive Session vorhanden',
|
||||
'Du hast noch eine laufende Session. Möchtest du fortfahren?',
|
||||
[
|
||||
{ label: 'Ja, fortfahren', primary: true, onClick: () => sessionFortfahren(session.sessionId) },
|
||||
{ label: 'Nein', onClick: () => sessionBeendenFragen(session.sessionId) },
|
||||
]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await sessionRes.json();
|
||||
zeigeModal(
|
||||
'Aktive Session vorhanden',
|
||||
'Du hast noch eine laufende Session. Möchtest du fortfahren?',
|
||||
[
|
||||
{ label: 'Ja, fortfahren', primary: true, onClick: () => sessionFortfahren(session.sessionId) },
|
||||
{ label: 'Nein', onClick: () => sessionBeendenFragen(session.sessionId) },
|
||||
]
|
||||
);
|
||||
// 2. Prüfen ob User als Mitspieler (ACCEPTED_OWN) eingeladen wurde
|
||||
const einladungRes = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (!einladungRes.ok) return;
|
||||
const einladung = await einladungRes.json();
|
||||
|
||||
if (!einladung.sessionId) {
|
||||
// Spiel noch nicht gestartet → Warteseite
|
||||
window.location.replace(`/bdsmwarten.html?id=${einladung.einladungId}`);
|
||||
} else {
|
||||
// Spiel läuft bereits → direkt als Gast rein
|
||||
const mRes = await fetch(`/bdsm/${einladung.sessionId}/mitspieler/me`);
|
||||
if (mRes.ok) {
|
||||
const mData = await mRes.json();
|
||||
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
|
||||
sessionStorage.setItem('bdsm-guest-name', mData.name);
|
||||
}
|
||||
sessionStorage.setItem('bdsm-session-id', einladung.sessionId);
|
||||
sessionStorage.setItem('bdsm-is-guest', 'true');
|
||||
window.location.replace('/bdsmingame.html');
|
||||
}
|
||||
} catch (_) { /* ignorieren */ }
|
||||
})();
|
||||
|
||||
// Gespeicherte Einstellungen wiederherstellen
|
||||
(function restore() {
|
||||
const saved = sessionStorage.getItem('bdsm-session-settings');
|
||||
if (!saved) return;
|
||||
const s = JSON.parse(saved);
|
||||
|
||||
function applySettings(s) {
|
||||
function setSlider(id, displayId, value, transform) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.value = value;
|
||||
document.getElementById(displayId).textContent = transform ? transform(value) : value;
|
||||
}
|
||||
|
||||
setSlider('sldStrafe', 'valStrafe', s.wahrscheinlichkeitStrafe);
|
||||
setSlider('sldZeitstrafe', 'valZeitstrafe', s.wahrscheinlichkeitSperre);
|
||||
setSlider('sldAufgaben', 'valAufgaben', s.aufgabenProLevel);
|
||||
setSlider('sldZeit', 'valZeit', Math.round(s.zeitfaktorZeitstrafen * 10),
|
||||
v => (v / 10).toFixed(1).replace('.', ','));
|
||||
|
||||
updateWarnung();
|
||||
}
|
||||
|
||||
(async function restore() {
|
||||
const saved = sessionStorage.getItem('bdsm-session-settings');
|
||||
if (saved) {
|
||||
applySettings(JSON.parse(saved));
|
||||
return;
|
||||
}
|
||||
// Fallback: Draft aus DB laden
|
||||
try {
|
||||
const res = await fetch('/bdsm/setup-draft');
|
||||
if (!res.ok) return;
|
||||
const draft = await res.json();
|
||||
if (draft.setupId && !sessionStorage.getItem('bdsm-setup-id')) {
|
||||
sessionStorage.setItem('bdsm-setup-id', draft.setupId);
|
||||
}
|
||||
if (draft.settingsJson) {
|
||||
const s = JSON.parse(draft.settingsJson);
|
||||
sessionStorage.setItem('bdsm-session-settings', JSON.stringify(s));
|
||||
applySettings(s);
|
||||
}
|
||||
if (draft.setupJson) {
|
||||
sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
|
||||
}
|
||||
if (draft.gruppenJson) {
|
||||
sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -189,6 +189,96 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ── Debug-Panel ── */
|
||||
.debug-panel {
|
||||
margin-top: 2rem;
|
||||
border: 1px dashed var(--color-secondary);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.debug-toggle {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-muted);
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.debug-toggle:hover { background: transparent; color: var(--color-text); }
|
||||
.debug-toggle .arrow { transition: transform 0.2s; display: inline-block; }
|
||||
.debug-toggle.open .arrow { transform: rotate(90deg); }
|
||||
.debug-body {
|
||||
display: none;
|
||||
padding: 1rem;
|
||||
border-top: 1px dashed var(--color-secondary);
|
||||
}
|
||||
.debug-body.open { display: block; }
|
||||
.debug-section { margin-bottom: 1.25rem; }
|
||||
.debug-section:last-child { margin-bottom: 0; }
|
||||
.debug-section-title {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.debug-kv { font-size: 0.8rem; line-height: 1.8; }
|
||||
.debug-kv span.k { color: var(--color-muted); display: inline-block; min-width: 190px; }
|
||||
.debug-kv span.v { color: var(--color-text); font-family: monospace; }
|
||||
.debug-card {
|
||||
background: var(--color-secondary);
|
||||
border-radius: 7px;
|
||||
padding: 0.6rem 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.debug-card:last-child { margin-bottom: 0; }
|
||||
.debug-card-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.debug-badge {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 0.65rem;
|
||||
border-radius: 5px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
}
|
||||
.debug-badge.warn { background: #b45309; }
|
||||
.debug-tag-row { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.2rem; }
|
||||
.debug-tag {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-radius: 4px;
|
||||
padding: 0.1rem 0.45rem;
|
||||
font-size: 0.72rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
.debug-tag.locked { background: rgba(239,68,68,0.2); color: #fca5a5; }
|
||||
.debug-tag.expired { background: rgba(251,191,36,0.15); color: #fcd34d; }
|
||||
.debug-refresh {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-muted);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
font-weight: normal;
|
||||
}
|
||||
.debug-refresh:hover { color: var(--color-text); background: transparent; }
|
||||
|
||||
/* ── Level-Anzeige ── */
|
||||
.level-display {
|
||||
display: flex;
|
||||
@@ -227,6 +317,17 @@
|
||||
<div class="message" id="taskMessage" style="display:none; margin-top:0.75rem;"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── Debug-Perspektive ── -->
|
||||
<div class="debug-panel" id="debugPanel">
|
||||
<button class="debug-toggle" id="debugToggle" onclick="toggleDebug()">
|
||||
<span class="arrow">▶</span> DEBUG – Entity-Zustand
|
||||
<button class="debug-refresh" onclick="event.stopPropagation(); ladeDebug()">⟳ Aktualisieren</button>
|
||||
</button>
|
||||
<div class="debug-body" id="debugBody">
|
||||
<div id="debugContent" style="color:var(--color-muted);font-size:0.8rem;">Wird geladen…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -239,8 +340,8 @@
|
||||
if (!sessionId) window.location.replace('/bdsm.html');
|
||||
|
||||
// Multi-Device: bin ich Gast?
|
||||
const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true';
|
||||
const myMitspielerId = sessionStorage.getItem('bdsm-guest-mitspieler-id') || null;
|
||||
const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true';
|
||||
let myMitspielerId = sessionStorage.getItem('bdsm-guest-mitspieler-id') || null;
|
||||
let guestPollInterval = null;
|
||||
|
||||
// ── Modal ──
|
||||
@@ -274,7 +375,12 @@
|
||||
return name ? `<div class="task-player-badge">${name} ist dran</div>` : '';
|
||||
}
|
||||
|
||||
const SESSION_BEENDEN_BTN = `<button class="btn-session-beenden" onclick="sessionBeendenFragen()">Session beenden</button>`;
|
||||
const SESSION_BEENDEN_BTN = `<button class="btn-session-beenden" onclick="sessionBeendenFragen()">Session beenden</button>`;
|
||||
const SPIEL_VERLASSEN_BTN = `<button class="btn-session-beenden" onclick="spielVerlassenFragen()">Spiel verlassen</button>`;
|
||||
|
||||
function playSound(src) {
|
||||
try { new Audio(src).play().catch(() => {}); } catch (_) {}
|
||||
}
|
||||
|
||||
// ── Aufgaben-Logik ──
|
||||
let currentTask = null;
|
||||
@@ -318,15 +424,22 @@
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/aufgaben/next`);
|
||||
if (res.status === 204) { zeigeFinaleDialog(); return; }
|
||||
if (res.status === 400) { window.location.replace('/bdsmtoys.html'); return; }
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
currentTask = await res.json();
|
||||
await saveAktiveAufgabe(currentTask, null);
|
||||
if (currentTask.level) {
|
||||
document.getElementById('levelImg').src = `/img/lvl${currentTask.level}.png`;
|
||||
document.getElementById('levelDisplay').style.display = '';
|
||||
}
|
||||
const name = currentTask.nameAktiverMitspieler || '';
|
||||
const title = name ? `${name}, du bist an der Reihe` : 'Du bist an der Reihe';
|
||||
zeigeModal(title, '', [{ label: 'OK', primary: true, onClick: zeigeAufgabe }]);
|
||||
if (currentTask.eigenesGeraet && currentTask.mitspielerId) {
|
||||
zeigeAufgabe();
|
||||
} else {
|
||||
playSound('/audio/ping.mp3');
|
||||
const name = currentTask.nameAktiverMitspieler || '';
|
||||
const title = name ? `${name}, du bist an der Reihe` : 'Du bist an der Reihe';
|
||||
zeigeModal(title, '', [{ label: 'OK', primary: true, onClick: zeigeAufgabe }]);
|
||||
}
|
||||
} catch (e) {
|
||||
zeigeTaskFehler('Aufgabe konnte nicht geladen werden: ' + e.message);
|
||||
}
|
||||
@@ -340,13 +453,7 @@
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ taskJson: JSON.stringify(task), timerStartedAt }),
|
||||
});
|
||||
} catch (_) { /* ignorieren */ }
|
||||
}
|
||||
|
||||
async function clearAktiveAufgabe() {
|
||||
try {
|
||||
await fetch(`/bdsm/${sessionId}/active-task`, { method: 'DELETE' });
|
||||
} catch (_) { /* ignorieren */ }
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function checkAktiveAufgabe() {
|
||||
@@ -362,8 +469,7 @@
|
||||
if (data.elapsedSeconds !== null && data.elapsedSeconds !== undefined) {
|
||||
const remaining = currentTask.timer - data.elapsedSeconds;
|
||||
if (remaining <= 0) {
|
||||
await clearAktiveAufgabe();
|
||||
aufgabeAbgeschlossen();
|
||||
aufgabeAbschliessen(false);
|
||||
} else {
|
||||
restoreTimer(Math.floor(remaining));
|
||||
}
|
||||
@@ -375,6 +481,10 @@
|
||||
|
||||
function restoreTimer(remaining) {
|
||||
const task = currentTask;
|
||||
if (task.level) {
|
||||
document.getElementById('levelImg').src = `/img/lvl${task.level}.png`;
|
||||
document.getElementById('levelDisplay').style.display = '';
|
||||
}
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card';
|
||||
card.innerHTML = `
|
||||
@@ -394,31 +504,101 @@
|
||||
if (rem <= 0) {
|
||||
clearTimer();
|
||||
if (el) { el.textContent = formatTime(0); el.classList.add('expired'); }
|
||||
aufgabeAbgeschlossen();
|
||||
playSound('/audio/alarm.mp3');
|
||||
aufgabeAbschliessen(false);
|
||||
} else {
|
||||
if (el) el.textContent = formatTime(rem);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ── Gast-Polling: wartet auf aktive Aufgabe für mein Gerät ──
|
||||
function startGastPoll() {
|
||||
if (guestPollInterval) return;
|
||||
// ── Zentrale Abschluss-Funktion: Callback + abgelaufene Sperren im Backend verarbeiten ──
|
||||
async function aufgabeAbschliessen(sperreAnwenden = false) {
|
||||
clearTimer();
|
||||
let allFreigaben = [];
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/active-task/abschliessen`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sperreAnwenden }),
|
||||
});
|
||||
if (res.ok) allFreigaben = (await res.json()).abgelaufeneSperren || [];
|
||||
} catch (_) {}
|
||||
|
||||
const danach = isGuest ? startGastPoll : ladeAufgabe;
|
||||
if (isGuest) {
|
||||
// Guest: alle Freigaben lokal zeigen (betreffen den eigenen Spieler)
|
||||
const texte = allFreigaben.map(f => f.text);
|
||||
if (texte.length > 0) zeigeAbgelaufeneSperre(texte, 0, danach);
|
||||
else danach();
|
||||
} else {
|
||||
// Host: nur non-eigenesGeraet lokal zeigen; für eigenesGeraet per active-task weiterleiten
|
||||
const lokal = allFreigaben.filter(f => !f.eigenesGeraet).map(f => f.text);
|
||||
const gast = allFreigaben.filter(f => f.eigenesGeraet);
|
||||
const weiter = () => pushGastFreigaben(gast, 0, danach);
|
||||
if (lokal.length > 0) zeigeAbgelaufeneSperre(lokal, 0, weiter);
|
||||
else weiter();
|
||||
}
|
||||
}
|
||||
|
||||
// Sendet Freigabe-Benachrichtigungen nacheinander an eigenesGeraet-Spieler
|
||||
async function pushGastFreigaben(freigaben, index, danach) {
|
||||
if (index >= freigaben.length) { danach(); return; }
|
||||
const f = freigaben[index];
|
||||
await saveAktiveAufgabe({
|
||||
aufgabeText: f.text,
|
||||
mitspielerId: f.mitspielerId,
|
||||
eigenesGeraet: true,
|
||||
isReleaseNotification: true,
|
||||
}, null);
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = 'Warte auf Aufgabe…';
|
||||
card.innerHTML = `<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--color-muted);">Zeitstrafe-Benachrichtigung wird zugestellt…</div>`;
|
||||
startHostPoll(() => pushGastFreigaben(freigaben, index + 1, danach));
|
||||
}
|
||||
|
||||
function zeigeAbgelaufeneSperre(texte, index, danach) {
|
||||
if (index >= texte.length) { danach(); return; }
|
||||
playSound('/audio/release.mp3');
|
||||
zeigeModal(
|
||||
'Zeitstrafe abgelaufen',
|
||||
texte[index],
|
||||
[{ label: 'OK', primary: true, onClick: () => zeigeAbgelaufeneSperre(texte, index + 1, danach) }]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Gast-Polling: wartet auf aktive Aufgabe für mein Gerät ──
|
||||
async function startGastPoll() {
|
||||
if (guestPollInterval) return;
|
||||
if (!myMitspielerId) {
|
||||
try {
|
||||
const r = await fetch(`/bdsm/${sessionId}/mitspieler/me`);
|
||||
if (r.status === 200) {
|
||||
const d = await r.json();
|
||||
myMitspielerId = d.mitspielerId;
|
||||
sessionStorage.setItem('bdsm-guest-mitspieler-id', myMitspielerId);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = `
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--color-muted);">Warte auf Aufgabe…</div>
|
||||
<div class="task-footer"><div class="task-btns"></div>${SPIEL_VERLASSEN_BTN}</div>`;
|
||||
guestPollInterval = setInterval(pollGastAufgabe, 2500);
|
||||
}
|
||||
|
||||
async function pollGastAufgabe() {
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/active-task`);
|
||||
if (res.status === 404) { await spielSessionEnde(); return; }
|
||||
if (res.status === 204) {
|
||||
// Keine aktive Aufgabe – warten
|
||||
const card = document.getElementById('taskCard');
|
||||
if (!card.classList.contains('loading')) {
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = 'Warte auf Aufgabe…';
|
||||
card.innerHTML = `
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--color-muted);">Warte auf Aufgabe…</div>
|
||||
<div class="task-footer"><div class="task-btns"></div>${SPIEL_VERLASSEN_BTN}</div>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -427,8 +607,16 @@
|
||||
const task = JSON.parse(data.taskJson);
|
||||
|
||||
if (task.mitspielerId && task.mitspielerId === myMitspielerId) {
|
||||
// Meine Aufgabe!
|
||||
clearInterval(guestPollInterval); guestPollInterval = null;
|
||||
|
||||
// Freigabe-Benachrichtigung: Zeitstrafe abgelaufen
|
||||
if (task.isReleaseNotification) {
|
||||
playSound('/audio/release.mp3');
|
||||
zeigeModal('Zeitstrafe abgelaufen', task.aufgabeText,
|
||||
[{ label: 'OK', primary: true, onClick: () => aufgabeAbschliessen(false) }]);
|
||||
return;
|
||||
}
|
||||
|
||||
currentTask = task;
|
||||
if (task.level) {
|
||||
document.getElementById('levelImg').src = `/img/lvl${task.level}.png`;
|
||||
@@ -436,43 +624,37 @@
|
||||
}
|
||||
if (data.elapsedSeconds !== null && data.elapsedSeconds !== undefined && task.timer != null) {
|
||||
const remaining = task.timer - data.elapsedSeconds;
|
||||
if (remaining <= 0) { await clearAktiveAufgabe(); gastAufgabeAbgeschlossen(); }
|
||||
if (remaining <= 0) { aufgabeAbschliessen(false); }
|
||||
else restoreTimer(Math.floor(remaining));
|
||||
} else {
|
||||
zeigeGastAufgabe(task);
|
||||
playSound('/audio/ping.mp3');
|
||||
const name = task.nameAktiverMitspieler || '';
|
||||
const title = name ? `${name}, du bist dran` : 'Du bist dran';
|
||||
zeigeModal(title, '', [{ label: 'OK', primary: true, onClick: () => zeigeGastAufgabe(task) }]);
|
||||
}
|
||||
} else {
|
||||
// Jemand anderes ist dran
|
||||
const card = document.getElementById('taskCard');
|
||||
const name = task.nameAktiverMitspieler || 'Jemand';
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = `<div style="font-size:1rem;color:var(--color-muted);">${name} ist dran…</div>`;
|
||||
card.innerHTML = `<div style="font-size:1rem;color:var(--color-muted);">${name} ist an der Reihe…</div>`;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function zeigeGastAufgabe(task) {
|
||||
// Gast sieht seine Aufgabe mit eigenem "Erledigt"-Button
|
||||
const cb = task.callback;
|
||||
if (task.timer != null && !data?.elapsedSeconds) zeigeTimerAufgabe(task);
|
||||
if (task.timer != null) zeigeTimerAufgabe(task);
|
||||
else if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
|
||||
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
|
||||
else zeigeEinfacheAufgabe(task);
|
||||
}
|
||||
|
||||
async function gastAufgabeAbgeschlossen() {
|
||||
clearTimer();
|
||||
await clearAktiveAufgabe();
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = 'Aufgabe erledigt – warte auf nächste Aufgabe…';
|
||||
startGastPoll();
|
||||
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
|
||||
else zeigeEinfacheAufgabe(task);
|
||||
}
|
||||
|
||||
// ── Host: wenn Aufgabe einem eigenem-Gerät-Spieler gehört ──
|
||||
let hostPollInterval = null;
|
||||
let _hostPollComplete = null;
|
||||
|
||||
function startHostPoll() {
|
||||
function startHostPoll(onComplete) {
|
||||
_hostPollComplete = onComplete || ladeAufgabe;
|
||||
if (hostPollInterval) return;
|
||||
hostPollInterval = setInterval(pollHostAktiv, 2500);
|
||||
}
|
||||
@@ -484,28 +666,29 @@
|
||||
async function pollHostAktiv() {
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/active-task`);
|
||||
if (res.status === 404) { await spielSessionEnde(); return; }
|
||||
if (res.status === 204) {
|
||||
// Gast hat Aufgabe erledigt → Host lädt nächste
|
||||
// Gast hat Aufgabe inkl. Sperre/Abgelaufene über abschliessen verarbeitet
|
||||
stopHostPoll();
|
||||
ladeAufgabe();
|
||||
const next = _hostPollComplete || ladeAufgabe;
|
||||
_hostPollComplete = null;
|
||||
next();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function zeigeAufgabe() {
|
||||
async function zeigeAufgabe() {
|
||||
const task = currentTask;
|
||||
const cb = task.callback;
|
||||
saveAktiveAufgabe(task, null);
|
||||
await saveAktiveAufgabe(task, null);
|
||||
|
||||
// Wenn Aufgabe für eigenes-Gerät-Spieler: Host zeigt nur Hinweis
|
||||
if (!isGuest && task.eigenesGeraet && task.mitspielerId) {
|
||||
const name = task.nameAktiverMitspieler || 'Jemand';
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card';
|
||||
card.innerHTML = `
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:0.5rem;">
|
||||
<div style="font-size:1.8rem;">📱</div>
|
||||
<div style="font-size:1rem;color:var(--color-muted);text-align:center;">${name} spielt gerade auf dem eigenen Gerät.</div>
|
||||
<div style="font-size:1rem;color:var(--color-muted);text-align:center;">${name} ist an der Reihe…</div>
|
||||
</div>
|
||||
<div class="task-footer">
|
||||
<div class="task-btns"></div>
|
||||
@@ -515,10 +698,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
|
||||
if (cb && cb.sperreId != null) zeigeSperreAufgabe(task);
|
||||
else if (cb && cb.faktor != null) zeigeVerlaengernAufgabe(task);
|
||||
else if (task.timer != null) zeigeTimerAufgabe(task);
|
||||
else zeigeEinfacheAufgabe(task);
|
||||
else if (task.timer != null) zeigeTimerAufgabe(task);
|
||||
else zeigeEinfacheAufgabe(task);
|
||||
}
|
||||
|
||||
function renderCard(task, footerInner) {
|
||||
@@ -529,13 +712,13 @@
|
||||
<div class="task-text" title="${escapeAttr(task.aufgabeText)}">${task.aufgabeText}</div>
|
||||
<div class="task-footer">
|
||||
<div class="task-btns" id="taskActions">${footerInner}</div>
|
||||
${SESSION_BEENDEN_BTN}
|
||||
${isGuest ? SPIEL_VERLASSEN_BTN : SESSION_BEENDEN_BTN}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function zeigeSperreAufgabe(task) {
|
||||
renderCard(task, `
|
||||
<button onclick="sperreAnwenden()">Zeitstrafe anwenden</button>
|
||||
<button onclick="aufgabeAbschliessen(true)">Zeitstrafe anwenden</button>
|
||||
<button class="secondary" onclick="gnadeErweisen()">Gnade erweisen</button>`);
|
||||
}
|
||||
|
||||
@@ -543,15 +726,15 @@
|
||||
const name = currentTask?.nameAktiverMitspieler;
|
||||
const title = name ? `${name}, erweist du wirklich Gnade?` : 'Wirklich Gnade erweisen?';
|
||||
zeigeModal(title, 'Die Zeitstrafe wird nicht eingetragen und das Spiel geht weiter.', [
|
||||
{ label: 'Ja, Gnade erweisen', primary: true, onClick: aufgabeAbgeschlossen },
|
||||
{ label: 'Nein', onClick: versteckeModal },
|
||||
{ label: 'Ja, Gnade erweisen', primary: true, onClick: () => aufgabeAbschliessen(false) },
|
||||
{ label: 'Nein', onClick: versteckeModal },
|
||||
]);
|
||||
}
|
||||
|
||||
function zeigeVerlaengernAufgabe(task) {
|
||||
renderCard(task, `
|
||||
<button onclick="sperrenVerlaengern()">Ja, verlängern</button>
|
||||
<button class="secondary" onclick="aufgabeAbgeschlossen()">Nein</button>`);
|
||||
<button onclick="aufgabeAbschliessen(true)">Ja, verlängern</button>
|
||||
<button class="secondary" onclick="aufgabeAbschliessen(false)">Nein</button>`);
|
||||
}
|
||||
|
||||
function zeigeTimerAufgabe(task) {
|
||||
@@ -574,7 +757,8 @@
|
||||
if (remaining <= 0) {
|
||||
clearTimer();
|
||||
if (el) { el.textContent = formatTime(0); el.classList.add('expired'); }
|
||||
aufgabeAbgeschlossen();
|
||||
playSound('/audio/alarm.mp3');
|
||||
aufgabeAbschliessen(false);
|
||||
} else {
|
||||
if (el) el.textContent = formatTime(remaining);
|
||||
}
|
||||
@@ -583,67 +767,11 @@
|
||||
|
||||
function timerAbbrechen() {
|
||||
clearTimer();
|
||||
aufgabeAbgeschlossen();
|
||||
aufgabeAbschliessen(false);
|
||||
}
|
||||
|
||||
function zeigeEinfacheAufgabe(task) {
|
||||
renderCard(task, `<button onclick="aufgabeAbgeschlossen()">Erledigt</button>`);
|
||||
}
|
||||
|
||||
async function aufgabeAbgeschlossen() {
|
||||
clearTimer();
|
||||
await clearAktiveAufgabe();
|
||||
if (isGuest) { gastAufgabeAbgeschlossen(); return; }
|
||||
try {
|
||||
const res = await fetch(`/bdsm/sperre/abgelaufene?sessionId=${sessionId}`);
|
||||
if (res.ok) {
|
||||
const text = await res.text();
|
||||
const texte = text.split(';').map(t => t.trim()).filter(t => t.length > 0);
|
||||
if (texte.length > 0) { zeigeAbgelaufeneSperre(texte, 0); return; }
|
||||
}
|
||||
} catch (_) { /* ignorieren */ }
|
||||
ladeAufgabe();
|
||||
}
|
||||
|
||||
function zeigeAbgelaufeneSperre(texte, index) {
|
||||
if (index >= texte.length) { ladeAufgabe(); return; }
|
||||
zeigeModal(
|
||||
'Zeitstrafe abgelaufen',
|
||||
texte[index],
|
||||
[{ label: 'OK', primary: true, onClick: () => zeigeAbgelaufeneSperre(texte, index + 1) }]
|
||||
);
|
||||
}
|
||||
|
||||
async function sperreAnwenden() {
|
||||
const cb = currentTask?.callback;
|
||||
if (!cb) return;
|
||||
try {
|
||||
const res = await fetch('/bdsm/sperre', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...cb, sessionId }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
aufgabeAbgeschlossen();
|
||||
} catch (e) {
|
||||
zeigeTaskFehler('Zeitstrafe konnte nicht verhängt werden: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sperrenVerlaengern() {
|
||||
const cb = currentTask?.callback;
|
||||
if (!cb) return;
|
||||
try {
|
||||
const res = await fetch('/bdsm/sperre/verlaengern', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...cb, sessionId }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
aufgabeAbgeschlossen();
|
||||
} catch (e) {
|
||||
zeigeTaskFehler('Sperren verlängern fehlgeschlagen: ' + e.message);
|
||||
}
|
||||
renderCard(task, `<button onclick="aufgabeAbschliessen(false)">Erledigt</button>`);
|
||||
}
|
||||
|
||||
// ── Finale ──
|
||||
@@ -677,7 +805,10 @@
|
||||
const res = await fetch(`/bdsm/sperre/aktive?sessionId=${sessionId}`);
|
||||
if (res.ok) {
|
||||
const sperren = await res.json();
|
||||
const texte = (sperren || []).map(s => s.releaseText).filter(t => t);
|
||||
const texte = (sperren || [])
|
||||
.filter(s => s.mitspieler?.sperrenVorFinaleAufloesen !== false)
|
||||
.map(s => s.releaseText)
|
||||
.filter(t => t);
|
||||
if (texte.length > 0) { zeigeFinaleSperre(texte, 0); return; }
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -686,6 +817,7 @@
|
||||
|
||||
function zeigeFinaleSperre(texte, index) {
|
||||
if (index >= texte.length) { ladeFinisher(); return; }
|
||||
playSound('/audio/release.mp3');
|
||||
zeigeModal(
|
||||
'Zeitstrafe aufgelöst',
|
||||
texte[index],
|
||||
@@ -711,11 +843,7 @@
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card loading';
|
||||
card.innerHTML = '';
|
||||
zeigeModal(
|
||||
'Das Finale ist abgeschlossen!',
|
||||
'Wir hoffen, ihr hattet viel Spaß! 🎉',
|
||||
[{ label: 'Session beenden', primary: true, onClick: sessionLoeschen }]
|
||||
);
|
||||
pruefeKeyholderAngebot();
|
||||
return;
|
||||
}
|
||||
const finisher = liste[index];
|
||||
@@ -727,8 +855,25 @@
|
||||
);
|
||||
}
|
||||
|
||||
function zeigeFinisherAufgabe() {
|
||||
async function zeigeFinisherAufgabe() {
|
||||
const finisher = _finisherListe[_finisherIndex];
|
||||
|
||||
// Finisher für eigenesGeraet-Spieler: per active-task ans Gast-Gerät übertragen
|
||||
if (!isGuest && finisher.eigenesGeraet && finisher.mitspielerId) {
|
||||
await saveAktiveAufgabe(finisher, null);
|
||||
const name = finisher.nameAktiverMitspieler || 'Jemand';
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card';
|
||||
card.innerHTML = `
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:0.5rem;">
|
||||
<div style="font-size:1.8rem;">📱</div>
|
||||
<div style="font-size:1rem;color:var(--color-muted);text-align:center;">${name} spielt Finale auf dem eigenen Gerät.</div>
|
||||
</div>
|
||||
<div class="task-footer"><div class="task-btns"></div></div>`;
|
||||
startHostPoll(() => naechsterFinisher(_finisherListe, _finisherIndex + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
const card = document.getElementById('taskCard');
|
||||
card.className = 'task-card';
|
||||
card.innerHTML = `
|
||||
@@ -738,9 +883,170 @@
|
||||
<div class="task-btns">
|
||||
<button onclick="naechsterFinisher(_finisherListe, _finisherIndex + 1)">Erledigt</button>
|
||||
</div>
|
||||
${isGuest ? SPIEL_VERLASSEN_BTN : SESSION_BEENDEN_BTN}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Keyholder-Angebot ──
|
||||
// _keyholderOnDecline: was passiert wenn das Angebot abgelehnt/übersprungen wird
|
||||
let _keyholderOnDecline = null;
|
||||
|
||||
async function pruefeKeyholderAngebot(onKeinAngebot) {
|
||||
_keyholderOnDecline = onKeinAngebot || zeigeFinaleAbgeschlossen;
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/keyholder-angebot`);
|
||||
if (res.ok) {
|
||||
const angebot = await res.json();
|
||||
zeigeKeyholderAngebot(angebot);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
_keyholderOnDecline();
|
||||
}
|
||||
|
||||
function zeigeFinaleAbgeschlossen() {
|
||||
zeigeModal(
|
||||
'Das Finale ist abgeschlossen!',
|
||||
'Wir hoffen, ihr hattet viel Spaß! 🎉',
|
||||
[{ label: 'Session beenden', primary: true, onClick: spielAbschliessen }]
|
||||
);
|
||||
}
|
||||
|
||||
function zeigeKeyholderAngebot(angebot) {
|
||||
zeigeModal(
|
||||
'Keyholder-Angebot',
|
||||
`${angebot.keyholderName}, möchtest du die Keyholder-Rolle für ${angebot.lockeeName} übernehmen?\n\nDu kannst dafür eines deiner vorhandenen Locks auswählen.`,
|
||||
[
|
||||
{ label: 'Ja, Keyholder werden', primary: true, onClick: () => ladeKeyholderLocks(angebot) },
|
||||
{ label: 'Nein', onClick: () => _keyholderOnDecline() },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async function ladeKeyholderLocks(angebot) {
|
||||
versteckeModal();
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/keyholder-locks?keyholderUserId=${angebot.keyholderUserId}`);
|
||||
if (!res.ok) { _keyholderOnDecline(); return; }
|
||||
const locks = await res.json();
|
||||
zeigeKeyholderLockAuswahl(angebot, locks);
|
||||
} catch (_) {
|
||||
_keyholderOnDecline();
|
||||
}
|
||||
}
|
||||
|
||||
function zeigeKeyholderLockAuswahl(angebot, locks) {
|
||||
const overlay = document.getElementById('modal');
|
||||
const card = overlay.querySelector('.modal-card');
|
||||
const escapedLockeeName = escapeAttr(angebot.lockeeName);
|
||||
const optionen = locks.map(l =>
|
||||
`<button class="secondary" style="width:100%;margin-bottom:0.5rem;text-align:left;" onclick="keyholderLockGewaehlt('${l.lockId}','${angebot.lockeeUserId}','${angebot.keyholderUserId}','${escapedLockeeName}')">` +
|
||||
`<strong>${escapeHtml(l.name)}</strong>` +
|
||||
`<span style="font-size:0.8rem;color:var(--color-muted);margin-left:0.5rem;">${l.totalCards} Karten · alle ${l.pickEveryMinute} Min.${l.active ? ' · aktiv' : ''}</span>` +
|
||||
`</button>`
|
||||
).join('');
|
||||
card.innerHTML = `
|
||||
<div class="modal-title">Lock auswählen</div>
|
||||
<div class="modal-text" style="text-align:left;margin-bottom:1rem;">Welches Lock soll für <strong>${escapeHtml(angebot.lockeeName)}</strong> verwendet werden?</div>
|
||||
<div style="max-height:50vh;overflow-y:auto;">${optionen}</div>
|
||||
<div class="modal-actions" style="margin-top:1rem;">
|
||||
<button class="secondary" onclick="_keyholderOnDecline()">Abbrechen</button>
|
||||
</div>`;
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
|
||||
async function keyholderLockGewaehlt(lockId, lockeeUserId, keyholderUserId, lockeeName) {
|
||||
versteckeModal();
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/zu-chastity`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ lockId, lockeeUserId, keyholderUserId }),
|
||||
});
|
||||
if (res.status === 409) {
|
||||
zeigeModal('Bereits gesperrt', `${escapeHtml(lockeeName)} ist bereits aktiv in einem Chastity Game gesperrt. Eine zweite Sperre ist nicht möglich.`,
|
||||
[{ label: 'OK', primary: true, onClick: _keyholderOnDecline }]);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
zeigeUnlockCodeModal(data.unlockCode, lockeeName);
|
||||
} catch (_) {
|
||||
zeigeModal('Fehler', 'Die Überführung ins Chastity Game ist fehlgeschlagen.',
|
||||
[{ label: 'OK', primary: true, onClick: spielAbschliessen }]);
|
||||
}
|
||||
}
|
||||
|
||||
function zeigeUnlockCodeModal(code, lockeeName) {
|
||||
const overlay = document.getElementById('modal');
|
||||
const card = overlay.querySelector('.modal-card');
|
||||
card.innerHTML = `
|
||||
<div style="font-size:2rem;">🔒</div>
|
||||
<div class="modal-title" id="unlockCodeTitle">Entsperrcode für ${escapeHtml(lockeeName)}</div>
|
||||
<div class="modal-text" id="unlockCodeHint" style="font-size:0.85rem;">
|
||||
Stelle die Kombination des Tresors auf diesen Code ein und verschließe den Schlüssel darin.
|
||||
</div>
|
||||
<div id="unlockCodeDisplay" style="
|
||||
font-family: monospace; font-size: 2rem; letter-spacing: 0.3em;
|
||||
background: var(--color-secondary); border-radius: 8px;
|
||||
padding: 1rem 1.5rem; text-align: center; color: var(--color-primary);
|
||||
line-height: 1.8; word-break: break-all; margin-bottom: 0.75rem;
|
||||
">${escapeHtml(code)}</div>
|
||||
<div id="unlockCodeCountdown" style="display:none;font-size:0.82rem;color:var(--color-muted);text-align:center;font-family:monospace;margin-bottom:0.5rem;"></div>
|
||||
<button id="unlockCodeBtn" style="width:100%;">Code vergessen & weiter</button>`;
|
||||
overlay.style.display = 'flex';
|
||||
document.getElementById('unlockCodeBtn').onclick = () => starteChastityCodeScramble(code);
|
||||
}
|
||||
|
||||
function starteChastityCodeScramble(realCode) {
|
||||
const display = document.getElementById('unlockCodeDisplay');
|
||||
const cdEl = document.getElementById('unlockCodeCountdown');
|
||||
const hintEl = document.getElementById('unlockCodeHint');
|
||||
const btn = document.getElementById('unlockCodeBtn');
|
||||
const titleEl = document.getElementById('unlockCodeTitle');
|
||||
const len = realCode.length;
|
||||
const DURATION = 3 * 60;
|
||||
let remaining = DURATION;
|
||||
let stopped = false;
|
||||
|
||||
function randomCode() {
|
||||
return Array.from({ length: len }, () => Math.floor(Math.random() * 10)).join('');
|
||||
}
|
||||
function finish() {
|
||||
stopped = true;
|
||||
clearInterval(scrambleInt);
|
||||
clearInterval(countdownInt);
|
||||
versteckeModal();
|
||||
window.location.href = '/keyholder.html';
|
||||
}
|
||||
|
||||
hintEl.style.display = 'none';
|
||||
cdEl.style.display = '';
|
||||
titleEl.textContent = 'Nun vergessen wir den Code…';
|
||||
btn.textContent = 'Abbrechen';
|
||||
btn.onclick = finish;
|
||||
|
||||
function updateCd() {
|
||||
const m = Math.floor(remaining / 60);
|
||||
const s = remaining % 60;
|
||||
cdEl.textContent = `${m}:${String(s).padStart(2, '0')}`;
|
||||
}
|
||||
updateCd();
|
||||
|
||||
const scrambleInt = setInterval(() => { if (!stopped) display.textContent = randomCode(); }, 80);
|
||||
const countdownInt = setInterval(() => {
|
||||
if (stopped) return;
|
||||
remaining--;
|
||||
updateCd();
|
||||
if (remaining <= 0) finish();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ── Session beenden ──
|
||||
const BDSM_STORAGE_KEYS = [
|
||||
'bdsm-session-id', 'bdsm-session-settings', 'bdsm-session-setup',
|
||||
@@ -752,12 +1058,21 @@
|
||||
'Wirklich beenden?',
|
||||
'Möchtest du die aktive Session wirklich beenden?',
|
||||
[
|
||||
{ label: 'Ja, beenden', primary: true, onClick: sessionLoeschen },
|
||||
{ label: 'Ja, beenden', primary: true, onClick: () => { versteckeModal(); pruefeKeyholderAngebot(sessionLoeschen); } },
|
||||
{ label: 'Nein', onClick: versteckeModal },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async function spielAbschliessen() {
|
||||
versteckeModal();
|
||||
try {
|
||||
await fetch(`/bdsm/${sessionId}/abgeschlossen`, { method: 'POST' });
|
||||
} catch (_) {}
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
|
||||
async function sessionLoeschen() {
|
||||
versteckeModal();
|
||||
try {
|
||||
@@ -771,12 +1086,209 @@
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
|
||||
// ── Spiel verlassen (Mitspieler) ──
|
||||
function spielVerlassenFragen() {
|
||||
zeigeModal(
|
||||
'Spiel wirklich verlassen?',
|
||||
'Du verlässt das Spiel. Alle Mitspieler werden benachrichtigt und das Spiel wird für alle beendet.',
|
||||
[
|
||||
{ label: 'Ja, verlassen', primary: true, onClick: spielVerlassen },
|
||||
{ label: 'Nein', onClick: versteckeModal },
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async function spielVerlassen() {
|
||||
versteckeModal();
|
||||
try {
|
||||
await fetch(`/bdsm/${sessionId}/verlassen`, { method: 'DELETE' });
|
||||
} catch (_) {}
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
|
||||
async function spielSessionEnde() {
|
||||
if (guestPollInterval) { clearInterval(guestPollInterval); guestPollInterval = null; }
|
||||
stopHostPoll();
|
||||
stopLevelPoll();
|
||||
clearTimer();
|
||||
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
|
||||
// Unterscheide: ordentlich beendet vs. abgebrochen
|
||||
let ordentlich = false;
|
||||
try {
|
||||
const r = await fetch(`/bdsm/${sessionId}/beendet`);
|
||||
ordentlich = r.ok;
|
||||
} catch (_) {}
|
||||
if (ordentlich) {
|
||||
zeigeModal(
|
||||
'Spiel beendet',
|
||||
'Das Spiel wurde erfolgreich abgeschlossen. Danke fürs Mitspielen! 🎉',
|
||||
[{ label: 'Zur Startseite', primary: true, onClick: () => window.location.href = '/userhome.html' }]
|
||||
);
|
||||
} else {
|
||||
zeigeModal(
|
||||
'Spiel abgebrochen',
|
||||
'Das Spiel wurde vorzeitig beendet.',
|
||||
[{ label: 'Zur Startseite', primary: true, onClick: () => window.location.href = '/userhome.html' }]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Level regelmäßig aktualisieren ──
|
||||
let levelPollInterval = null;
|
||||
let lastKnownLevel = null;
|
||||
|
||||
async function fetchAndShowLevel() {
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (data.level) {
|
||||
document.getElementById('levelImg').src = `/img/lvl${data.level}.png`;
|
||||
document.getElementById('levelDisplay').style.display = '';
|
||||
if (lastKnownLevel !== null && data.level > lastKnownLevel) {
|
||||
playSound('/audio/lvlup.mp3');
|
||||
}
|
||||
lastKnownLevel = data.level;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function startLevelPoll() {
|
||||
if (levelPollInterval) return;
|
||||
fetchAndShowLevel();
|
||||
levelPollInterval = setInterval(fetchAndShowLevel, 10000);
|
||||
}
|
||||
|
||||
function stopLevelPoll() {
|
||||
if (levelPollInterval) { clearInterval(levelPollInterval); levelPollInterval = null; }
|
||||
}
|
||||
|
||||
// ── Debug-Perspektive ──
|
||||
let debugOpen = false;
|
||||
let debugAutoRefresh = null;
|
||||
|
||||
function toggleDebug() {
|
||||
debugOpen = !debugOpen;
|
||||
const toggle = document.getElementById('debugToggle');
|
||||
const body = document.getElementById('debugBody');
|
||||
toggle.classList.toggle('open', debugOpen);
|
||||
body.classList.toggle('open', debugOpen);
|
||||
if (debugOpen) {
|
||||
ladeDebug();
|
||||
debugAutoRefresh = setInterval(ladeDebug, 5000);
|
||||
} else {
|
||||
if (debugAutoRefresh) { clearInterval(debugAutoRefresh); debugAutoRefresh = null; }
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(val) {
|
||||
if (val === null || val === undefined) return '<span style="color:var(--color-muted)">—</span>';
|
||||
if (typeof val === 'boolean') return val ? '<span style="color:#86efac">true</span>' : '<span style="color:#fca5a5">false</span>';
|
||||
return String(val);
|
||||
}
|
||||
|
||||
function kv(key, val) {
|
||||
return `<div class="debug-kv"><span class="k">${key}</span><span class="v">${fmt(val)}</span></div>`;
|
||||
}
|
||||
|
||||
async function ladeDebug() {
|
||||
if (!debugOpen) return;
|
||||
try {
|
||||
const res = await fetch(`/bdsm/${sessionId}/debug`);
|
||||
if (!res.ok) { document.getElementById('debugContent').textContent = `Fehler: HTTP ${res.status}`; return; }
|
||||
const data = await res.json();
|
||||
renderDebug(data);
|
||||
} catch (e) {
|
||||
document.getElementById('debugContent').textContent = 'Fehler: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDebug(data) {
|
||||
const s = data.session;
|
||||
let html = '';
|
||||
|
||||
// ── Session ──
|
||||
html += `<div class="debug-section">
|
||||
<div class="debug-section-title">BdsmGameEntity – Session</div>
|
||||
${kv('sessionId', s.sessionId)}
|
||||
${kv('userId', s.userId)}
|
||||
${kv('setupId', s.setupId)}
|
||||
${kv('startZeit', s.startZeit)}
|
||||
${kv('letzteAktivitaet', s.letzteAktivitaet)}
|
||||
${kv('level', s.level)}
|
||||
${kv('aufgabenAufAktuellemLevel', s.aufgabenAufAktuellemLevel + ' / ' + s.aufgabenProLevel)}
|
||||
${kv('wahrscheinlichkeit Sperre', s.wahrscheinlichkeitSperre + '%')}
|
||||
${kv('wahrscheinlichkeit Strafe', s.wahrscheinlichkeitStrafe + '%')}
|
||||
${kv('zeitfaktorZeitstrafen', s.zeitfaktorZeitstrafen)}
|
||||
${kv('hatAufgaben', s.hatAufgaben)}
|
||||
${kv('hatActiveTask', s.hatActiveTask)}
|
||||
${kv('taskStartedAt', s.taskStartedAt)}
|
||||
</div>`;
|
||||
|
||||
// ── Mitspieler ──
|
||||
html += `<div class="debug-section"><div class="debug-section-title">MitspielerEntity (${data.mitspieler.length})</div>`;
|
||||
if (data.mitspieler.length === 0) {
|
||||
html += `<div style="color:var(--color-muted);font-size:0.8rem;">Keine Mitspieler</div>`;
|
||||
}
|
||||
data.mitspieler.forEach(m => {
|
||||
const werkzeugTags = (m.werkzeuge || []).map(w => `<span class="debug-tag">${w}</span>`).join('');
|
||||
const rollenTags = (m.rollen || []).map(r => `<span class="debug-tag">${r}</span>`).join('');
|
||||
const spieltMitTags= (m.spieltMit || []).map(g => `<span class="debug-tag">${g}</span>`).join('');
|
||||
html += `<div class="debug-card">
|
||||
<div class="debug-card-name">
|
||||
${escapeHtml(m.name)}
|
||||
<span class="debug-badge">${m.geschlecht || '?'}</span>
|
||||
${m.eigenesGeraet ? '<span class="debug-badge warn">eigenesGerät</span>' : ''}
|
||||
${!m.sperrenVorFinaleAufloesen ? '<span class="debug-badge warn">keine Auflösung</span>' : ''}
|
||||
</div>
|
||||
<div class="debug-kv"><span class="k">mitspielerId</span><span class="v">${m.mitspielerId}</span></div>
|
||||
<div class="debug-kv"><span class="k">userId</span><span class="v">${fmt(m.userId)}</span></div>
|
||||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">Rollen</span></div>
|
||||
<div class="debug-tag-row">${rollenTags || '<span class="debug-tag" style="opacity:0.4">—</span>'}</div>
|
||||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">Werkzeuge (verfügbar)</span></div>
|
||||
<div class="debug-tag-row">${werkzeugTags || '<span class="debug-tag locked">alle gesperrt</span>'}</div>
|
||||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">spieltMit</span></div>
|
||||
<div class="debug-tag-row">${spieltMitTags}</div>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
|
||||
// ── Aktive Sperren ──
|
||||
html += `<div class="debug-section"><div class="debug-section-title">AktiveSperreEntity (${data.aktiveSperren.length})</div>`;
|
||||
if (data.aktiveSperren.length === 0) {
|
||||
html += `<div style="color:var(--color-muted);font-size:0.8rem;">Keine aktiven Sperren</div>`;
|
||||
}
|
||||
data.aktiveSperren.forEach(sp => {
|
||||
const fuerTags = (sp.fuer || []).map(w => `<span class="debug-tag locked">${w}</span>`).join('');
|
||||
const abgelaufenBadge = sp.abgelaufen
|
||||
? '<span class="debug-badge warn">ABGELAUFEN (nicht gepolllt)</span>'
|
||||
: '<span class="debug-badge">aktiv</span>';
|
||||
html += `<div class="debug-card">
|
||||
<div class="debug-card-name">
|
||||
${escapeHtml(sp.mitspielerName || '?')} ${abgelaufenBadge}
|
||||
</div>
|
||||
<div class="debug-kv"><span class="k">aktiveSperreId</span><span class="v">${sp.aktiveSperreId}</span></div>
|
||||
<div class="debug-kv"><span class="k">minuten</span><span class="v">${fmt(sp.minuten)}</span></div>
|
||||
<div class="debug-kv"><span class="k">startzeit</span><span class="v">${fmt(sp.startzeit)}</span></div>
|
||||
<div class="debug-kv"><span class="k">endzeit</span><span class="v ${sp.abgelaufen ? 'expired' : ''}">${fmt(sp.endzeit)}</span></div>
|
||||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">gesperrt für</span></div>
|
||||
<div class="debug-tag-row">${fuerTags}</div>
|
||||
<div class="debug-kv" style="margin-top:0.3rem;"><span class="k">releaseText</span><span class="v" style="word-break:break-word;">${escapeHtml(sp.releaseText || '—')}</span></div>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
|
||||
document.getElementById('debugContent').innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Start ──
|
||||
if (isGuest) {
|
||||
startGastPoll();
|
||||
} else {
|
||||
checkAktiveAufgabe();
|
||||
}
|
||||
startLevelPoll();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -208,6 +208,15 @@
|
||||
</head>
|
||||
<body class="app">
|
||||
|
||||
<div class="modal-overlay" id="errorModal" style="display:none;">
|
||||
<div class="modal-card" style="text-align:center;">
|
||||
<div style="font-size:1.5rem;margin-bottom:0.75rem;">⚠️</div>
|
||||
<div class="modal-title" id="errorModalTitle"></div>
|
||||
<div class="modal-text" id="errorModalText" style="font-size:0.9rem;color:var(--color-muted);line-height:1.5;margin-bottom:1.25rem;"></div>
|
||||
<button onclick="document.getElementById('errorModal').style.display='none'">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="friendModal" style="display:none;">
|
||||
<div class="modal-card">
|
||||
<div class="modal-title">Freund einladen</div>
|
||||
@@ -244,15 +253,33 @@
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
if (!sessionStorage.getItem('bdsm-session-settings')) {
|
||||
window.location.replace('/bdsm.html');
|
||||
}
|
||||
|
||||
// SetupId erzeugen (persistent über sessionStorage)
|
||||
if (!sessionStorage.getItem('bdsm-setup-id')) {
|
||||
sessionStorage.setItem('bdsm-setup-id', crypto.randomUUID());
|
||||
}
|
||||
const setupId = sessionStorage.getItem('bdsm-setup-id');
|
||||
let setupId = sessionStorage.getItem('bdsm-setup-id');
|
||||
|
||||
// Draft aus DB laden wenn sessionStorage leer
|
||||
async function ladeSessionOderDraft() {
|
||||
if (sessionStorage.getItem('bdsm-session-settings')) return true;
|
||||
try {
|
||||
const res = await fetch('/bdsm/setup-draft');
|
||||
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
|
||||
const draft = await res.json();
|
||||
if (draft.setupId) {
|
||||
sessionStorage.setItem('bdsm-setup-id', draft.setupId);
|
||||
setupId = draft.setupId;
|
||||
}
|
||||
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
|
||||
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
|
||||
if (draft.gruppenJson) sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
|
||||
if (!draft.settingsJson) { window.location.replace('/bdsm.html'); return false; }
|
||||
return true;
|
||||
} catch (_) {
|
||||
window.location.replace('/bdsm.html');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const GESCHLECHTER = [
|
||||
{ value: 'MAENNLICH', label: 'Männlich' },
|
||||
@@ -288,6 +315,7 @@
|
||||
let playerInvitations = {};
|
||||
let pollIntervalId = null;
|
||||
let myUserId = null;
|
||||
let selfPlayerId = null;
|
||||
let freundeListe = [];
|
||||
|
||||
function buildCheckItems(name, items, type, disabled = false) {
|
||||
@@ -349,12 +377,24 @@
|
||||
<label>Verfügbar</label>
|
||||
<div class="check-group check-group--two-col">${buildCheckItems('p' + id + '-werkzeuge', WERKZEUGE, 'checkbox')}</div>
|
||||
<div class="field-error" id="p${id}-werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
|
||||
<div id="p${id}-chastity-hint" style="display:none;font-size:0.78rem;color:var(--color-muted);margin-top:0.4rem;font-style:italic;line-height:1.4;"></div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Finale</label>
|
||||
<label class="check-item is-checked" id="p${id}-sperre-label">
|
||||
<input type="checkbox" id="p${id}-sperrenAufloesen" checked onchange="toggleSperreWarning(${id})">
|
||||
<span class="check-item-label">Zeitstrafen vor dem Finale auflösen</span>
|
||||
</label>
|
||||
<div style="display:none; margin-top:0.4rem; font-size:0.78rem; color:var(--color-primary);" id="p${id}-sperre-warn">
|
||||
⚠️ Hinweis: Zeitstrafen werden nicht aufgelöst. Diese Person könnte im Finale leer ausgehen.
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function addPlayer(prefillName = '', isSelf = false) {
|
||||
playerSeq++;
|
||||
const id = playerSeq;
|
||||
if (isSelf) selfPlayerId = id;
|
||||
playerIds.push(id);
|
||||
playerInvitations[id] = null;
|
||||
document.getElementById('playersContainer')
|
||||
@@ -365,9 +405,25 @@
|
||||
|
||||
function removePlayer(id) {
|
||||
const inv = playerInvitations[id];
|
||||
// Einladung serverseitig canceln
|
||||
if (inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST')) {
|
||||
cancelEinladung(id);
|
||||
fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
playerInvitations[id] = null;
|
||||
|
||||
if (playerIds.length <= 2) {
|
||||
// Letzter Slot: nur leeren, nicht entfernen
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
if (header) {
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
const invBtn = header.querySelector('.btn-invite');
|
||||
if (invBtn) invBtn.style.display = '';
|
||||
}
|
||||
const body = document.getElementById(`p${id}-body`);
|
||||
if (body) body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('player-' + id)?.remove();
|
||||
playerIds = playerIds.filter(x => x !== id);
|
||||
delete playerInvitations[id];
|
||||
@@ -383,10 +439,9 @@
|
||||
}
|
||||
|
||||
function refreshRemoveButtons() {
|
||||
const canRemove = playerIds.length > 2;
|
||||
playerIds.forEach(id => {
|
||||
playerIds.forEach((id, idx) => {
|
||||
const btn = document.querySelector(`#player-${id} .player-remove`);
|
||||
if (btn) btn.style.display = canRemove ? '' : 'none';
|
||||
if (btn) btn.style.display = idx === 0 ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,12 +456,22 @@
|
||||
const prefix = input.name.slice(0, -'-geschlecht'.length);
|
||||
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
|
||||
document.querySelectorAll(`input[name="${prefix}-werkzeuge"]`).forEach(cb => {
|
||||
if (cb.closest('.check-item')?.dataset.chastitylocked) return;
|
||||
cb.checked = defaults.includes(cb.value);
|
||||
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
input.closest('.check-item')?.classList.toggle('is-checked', input.checked);
|
||||
const label = input.closest('.check-item');
|
||||
if (label?.dataset.chastitylocked) {
|
||||
// Revert immediately and flash hint
|
||||
input.checked = false;
|
||||
label.classList.remove('is-checked');
|
||||
const card = label.closest('[id^="player-"]');
|
||||
if (card) flashChastityHint(card.id.replace('player-', ''));
|
||||
return;
|
||||
}
|
||||
label?.classList.toggle('is-checked', input.checked);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -414,6 +479,15 @@
|
||||
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
|
||||
}
|
||||
|
||||
function toggleSperreWarning(id) {
|
||||
const cb = document.getElementById(`p${id}-sperrenAufloesen`);
|
||||
const warn = document.getElementById(`p${id}-sperre-warn`);
|
||||
const label = document.getElementById(`p${id}-sperre-label`);
|
||||
if (!cb) return;
|
||||
if (warn) warn.style.display = cb.checked ? 'none' : 'block';
|
||||
if (label) label.classList.toggle('is-checked', cb.checked);
|
||||
}
|
||||
|
||||
function setFieldError(id, show) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = show ? 'block' : 'none';
|
||||
@@ -449,7 +523,12 @@
|
||||
dropdown.innerHTML = '';
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) { dropdown.style.display = 'none'; return; }
|
||||
const matches = freundeListe.filter(f => (f.user.name || '').toLowerCase().includes(q));
|
||||
const invitedIds = new Set(
|
||||
Object.values(playerInvitations)
|
||||
.filter(inv => inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST'))
|
||||
.map(inv => inv.inviteeId)
|
||||
);
|
||||
const matches = freundeListe.filter(f => (f.user.name || '').toLowerCase().includes(q) && !invitedIds.has(f.user.userId));
|
||||
if (!matches.length) {
|
||||
dropdown.innerHTML = '<div style="padding:0.6rem 0.75rem;color:var(--color-muted);font-size:0.9rem;">Keine Treffer.</div>';
|
||||
dropdown.style.display = 'block';
|
||||
@@ -499,6 +578,12 @@
|
||||
selectedFriend = null;
|
||||
}
|
||||
|
||||
function zeigePopup(title, text) {
|
||||
document.getElementById('errorModalTitle').textContent = title;
|
||||
document.getElementById('errorModalText').textContent = text;
|
||||
document.getElementById('errorModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function einladen(inviteeId, inviteeName) {
|
||||
const id = currentInvitePlayerId;
|
||||
schliesseFriendModal();
|
||||
@@ -509,13 +594,17 @@
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ setupId, slotIndex: id, inviteeId }),
|
||||
});
|
||||
if (res.status === 409) {
|
||||
zeigePopup('Bereits eingeladen', `${inviteeName} ist bereits eingeladen oder nimmt schon am Spiel teil.`);
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
playerInvitations[id] = { einladungId: data.einladungId, status: 'PENDING', inviteeId, inviteeName };
|
||||
renderPending(id);
|
||||
startPoll();
|
||||
} catch (_) {
|
||||
showMessage('Einladung konnte nicht gesendet werden.', 'error');
|
||||
zeigePopup('Fehler', 'Einladung konnte nicht gesendet werden. Bitte versuche es erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,25 +617,33 @@
|
||||
if (headerInvBtn) headerInvBtn.style.display = 'none';
|
||||
|
||||
if (inv.status === 'PENDING') {
|
||||
// Badge
|
||||
// Badge nach "Spieler X" einfügen
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
header.insertAdjacentHTML('afterbegin', `<span class="player-badge-pending">Ausstehend</span>`);
|
||||
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-pending">Ausstehend</span>`);
|
||||
body.innerHTML = `
|
||||
<div class="pending-info">
|
||||
<div class="pending-name">${inv.inviteeName}</div>
|
||||
<div>Einladung wurde gesendet – warte auf Antwort…</div>
|
||||
<button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button>
|
||||
</div>`;
|
||||
} else if (inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST') {
|
||||
} else if (inv.status === 'ACCEPTED_OWN') {
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
const modeLabel = inv.status === 'ACCEPTED_OWN' ? 'Eigenes Gerät' : 'Host-Gerät';
|
||||
header.insertAdjacentHTML('afterbegin', `<span class="player-badge-accepted">✓ ${modeLabel}</span>`);
|
||||
// Name readonly, Geschlecht gesperrt (kommt vom Profil der eingeladenen Person)
|
||||
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-accepted">✓ Eigenes Gerät</span>`);
|
||||
body.innerHTML = `
|
||||
<div class="pending-info">
|
||||
<div class="pending-name">${inv.inviteeName}</div>
|
||||
<div class="pending-mode">Spieler konfiguriert Präferenzen auf dem eigenen Gerät.</div>
|
||||
<button class="btn-cancel-invite" style="margin-top:1rem;" onclick="cancelEinladung(${id})">Einladung abbrechen</button>
|
||||
</div>`;
|
||||
} else if (inv.status === 'ACCEPTED_HOST') {
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
|
||||
header.querySelector('.player-title').insertAdjacentHTML('afterend', `<span class="player-badge-accepted">✓ Host-Gerät</span>`);
|
||||
const nameField = `<input type="text" id="p${id}-name" value="${inv.inviteeName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`;
|
||||
body.innerHTML = buildPlayerBody(id, nameField, true);
|
||||
// Defaults laden falls verfügbar
|
||||
const hasGeschlecht = inv.defaults && inv.defaults.geschlecht;
|
||||
body.innerHTML = buildPlayerBody(id, nameField, hasGeschlecht);
|
||||
if (inv.defaults) {
|
||||
restorePlayer(id, {
|
||||
geschlecht: inv.defaults.geschlecht,
|
||||
@@ -555,6 +652,7 @@
|
||||
werkzeuge: inv.defaults.werkzeuge || [],
|
||||
});
|
||||
}
|
||||
pruefeChastityConstraint(id, inv.inviteeId);
|
||||
} else if (inv.status === 'DECLINED' || inv.status === 'CANCELLED') {
|
||||
// Slot wieder freigeben
|
||||
const header = document.querySelector(`#player-${id} .player-card-header`);
|
||||
@@ -607,8 +705,11 @@
|
||||
const inv = playerInvitations[id];
|
||||
if (!inv || inv.status === e.status) continue;
|
||||
inv.status = e.status;
|
||||
if (e.status === 'DECLINED' || e.status === 'CANCELLED') {
|
||||
showMessage(`${inv.inviteeName} hat die Einladung abgelehnt oder abgebrochen.`, 'error');
|
||||
}
|
||||
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
|
||||
// Defaults der eingeladenen Person laden
|
||||
// Profil + Defaults der eingeladenen Person laden
|
||||
try {
|
||||
const dRes = await fetch(`/user/${inv.inviteeId}/bdsm-defaults`);
|
||||
if (dRes.ok) inv.defaults = await dRes.json();
|
||||
@@ -640,6 +741,21 @@
|
||||
|
||||
const mitspieler = playerIds.map(id => {
|
||||
const inv = playerInvitations[id];
|
||||
const isOwnDevice = inv && inv.status === 'ACCEPTED_OWN';
|
||||
|
||||
if (isOwnDevice) {
|
||||
// Daten werden vom Spieler selbst auf dem eigenen Gerät konfiguriert
|
||||
return {
|
||||
name: inv.inviteeName,
|
||||
geschlecht: null,
|
||||
spieltMit: [], rollen: [], werkzeuge: [],
|
||||
userId: inv.inviteeId,
|
||||
eigenesGeraet: true,
|
||||
einladungId: inv.einladungId,
|
||||
sperrenVorFinaleAufloesen: true, // Wird auf dem eigenen Gerät konfiguriert
|
||||
};
|
||||
}
|
||||
|
||||
const name = document.getElementById(`p${id}-name`)?.value.trim() || '';
|
||||
const geschlecht = getChecked(`p${id}-geschlecht`);
|
||||
const spieltMit = getChecked(`p${id}-spieltmit`);
|
||||
@@ -656,35 +772,58 @@
|
||||
valid = false;
|
||||
}
|
||||
|
||||
const sperrenAufloesen = document.getElementById(`p${id}-sperrenAufloesen`);
|
||||
return {
|
||||
name,
|
||||
geschlecht: geschlecht[0] || null,
|
||||
spieltMit, rollen, werkzeuge,
|
||||
userId: inv ? inv.inviteeId : null,
|
||||
eigenesGeraet: inv ? inv.status === 'ACCEPTED_OWN' : false,
|
||||
userId: inv ? inv.inviteeId : (id === selfPlayerId ? myUserId : null),
|
||||
eigenesGeraet: false,
|
||||
sperrenVorFinaleAufloesen: sperrenAufloesen ? sperrenAufloesen.checked : true,
|
||||
};
|
||||
});
|
||||
|
||||
if (!valid) { showMessage('Bitte alle Felder für jeden Spieler ausfüllen.', 'error'); return; }
|
||||
|
||||
const allRoles = new Set(mitspieler.flatMap(p => p.rollen));
|
||||
const missingRoles = Object.keys(ROLE_LABELS).filter(r => !allRoles.has(r));
|
||||
if (missingRoles.length > 0) {
|
||||
showMessage('Folgende Rollen müssen mindestens einmal vergeben sein: ' +
|
||||
missingRoles.map(r => ROLE_LABELS[r]).join(', '), 'error');
|
||||
return;
|
||||
const configuredMitspieler = mitspieler.filter(p => !p.eigenesGeraet);
|
||||
const hasOwnDeviceInRoleCheck = mitspieler.some(p => p.eigenesGeraet);
|
||||
if (!hasOwnDeviceInRoleCheck) {
|
||||
const allRoles = new Set(configuredMitspieler.flatMap(p => p.rollen));
|
||||
const missingRoles = Object.keys(ROLE_LABELS).filter(r => !allRoles.has(r));
|
||||
if (missingRoles.length > 0) {
|
||||
showMessage('Folgende Rollen müssen mindestens einmal vergeben sein: ' +
|
||||
missingRoles.map(r => ROLE_LABELS[r]).join(', '), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let partnerFehler = false;
|
||||
mitspieler.forEach((player, i) => {
|
||||
const andereGeschlechter = mitspieler.filter((_, j) => j !== i).map(p => p.geschlecht);
|
||||
const hatPartner = player.spieltMit.some(g => andereGeschlechter.includes(g));
|
||||
if (!hatPartner) { setFieldError(`p${playerIds[i]}-partner-err`, true); partnerFehler = true; }
|
||||
});
|
||||
if (partnerFehler) { showMessage('Mindestens ein Spieler hat keinen kompatiblen Mitspieler.', 'error'); return; }
|
||||
const hasOwnDevicePlayers = mitspieler.some(p => p.eigenesGeraet);
|
||||
if (!hasOwnDevicePlayers) {
|
||||
let partnerFehler = false;
|
||||
configuredMitspieler.forEach((player, i) => {
|
||||
const andereGeschlechter = configuredMitspieler.filter((_, j) => j !== i).map(p => p.geschlecht);
|
||||
const hatPartner = player.spieltMit.some(g => andereGeschlechter.includes(g));
|
||||
if (!hatPartner) {
|
||||
const globalIdx = mitspieler.indexOf(player);
|
||||
setFieldError(`p${playerIds[globalIdx]}-partner-err`, true);
|
||||
partnerFehler = true;
|
||||
}
|
||||
});
|
||||
if (partnerFehler) { showMessage('Mindestens ein Spieler hat keinen kompatiblen Mitspieler.', 'error'); return; }
|
||||
}
|
||||
|
||||
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
|
||||
sessionStorage.setItem('bdsm-session-setup', JSON.stringify({ settings, mitspieler }));
|
||||
const sessionSetup = JSON.stringify({ settings, mitspieler });
|
||||
sessionStorage.setItem('bdsm-session-setup', sessionSetup);
|
||||
// Draft in DB aktualisieren
|
||||
fetch('/bdsm/setup-draft', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
setupId,
|
||||
setupJson: sessionSetup,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
window.location.href = '/bdsmtasks.html';
|
||||
}
|
||||
|
||||
@@ -694,6 +833,55 @@
|
||||
}
|
||||
function hideMessage() { document.getElementById('message').style.display = 'none'; }
|
||||
|
||||
// ── Chastity-Constraint: gesperrtes Werkzeug angehakt + disabled ──
|
||||
async function pruefeChastityConstraint(playerId, userId) {
|
||||
if (!userId) return;
|
||||
try {
|
||||
const res = await fetch(`/bdsm/chastity-constraint?userId=${userId}`);
|
||||
if (!res.ok) return;
|
||||
const { lockedWerkzeug } = await res.json();
|
||||
if (!lockedWerkzeug) return;
|
||||
const locked = lockedWerkzeug === 'BOTH' ? ['VAGINA', 'PENIS'] : [lockedWerkzeug];
|
||||
locked.forEach(w => sperrWerkzeugCheckbox(playerId, w));
|
||||
// Hinweis-Text im hidden div speichern, Popup beim Klick auf Checkbox
|
||||
const hintEl = document.getElementById(`p${playerId}-chastity-hint`);
|
||||
if (hintEl) {
|
||||
hintEl.dataset.hintText = '🔒 Das System weiß aus halbwegs verlässlicher Quelle, dass diese Person dieses Körperteil gerade nicht einsetzen kann – und lässt sich hier auch nicht austricksen.';
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function sperrWerkzeugCheckbox(playerId, werkzeug) {
|
||||
const cb = document.querySelector(`input[name="p${playerId}-werkzeuge"][value="${werkzeug}"]`);
|
||||
if (!cb) return;
|
||||
cb.checked = false;
|
||||
cb.disabled = false;
|
||||
const label = cb.closest('.check-item');
|
||||
if (label) {
|
||||
label.classList.remove('is-checked', 'is-disabled');
|
||||
label.dataset.chastitylocked = '1';
|
||||
}
|
||||
}
|
||||
|
||||
function flashChastityHint(playerId) {
|
||||
const hintEl = document.getElementById(`p${playerId}-chastity-hint`);
|
||||
const text = hintEl?.dataset.hintText;
|
||||
if (!text) return;
|
||||
document.getElementById('errorModalTitle').textContent = 'Nicht verfügbar';
|
||||
document.getElementById('errorModalText').textContent = text;
|
||||
document.getElementById('errorModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Klick auf gesperrte Checkbox: Hint aufblinken lassen
|
||||
document.addEventListener('click', e => {
|
||||
const label = e.target.closest('.check-item[data-chastitylocked]');
|
||||
if (!label) return;
|
||||
const card = label.closest('[id^="player-"]');
|
||||
if (!card) return;
|
||||
const playerId = card.id.replace('player-', '');
|
||||
flashChastityHint(playerId);
|
||||
});
|
||||
|
||||
function restorePlayer(id, data) {
|
||||
if (data.geschlecht) {
|
||||
const radio = document.querySelector(`input[name="p${id}-geschlecht"][value="${data.geschlecht}"]`);
|
||||
@@ -711,22 +899,72 @@
|
||||
const cb = document.querySelector(`input[name="p${id}-werkzeuge"][value="${val}"]`);
|
||||
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
|
||||
});
|
||||
if (data.sperrenVorFinaleAufloesen === false) {
|
||||
const cb = document.getElementById(`p${id}-sperrenAufloesen`);
|
||||
if (cb) { cb.checked = false; toggleSperreWarning(id); }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Einladungen aus DB wiederherstellen ──
|
||||
async function ladeEinladungenAusDb(userIdToInfo) {
|
||||
// userIdToInfo: { [userId]: { playerId, name } } oder null (dann Matching per slotIndex)
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`);
|
||||
if (!res.ok) return;
|
||||
const einladungen = await res.json();
|
||||
const aktive = einladungen.filter(e =>
|
||||
e.status === 'PENDING' || e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST');
|
||||
for (const e of aktive) {
|
||||
let playerId;
|
||||
if (userIdToInfo && userIdToInfo[e.inviteeId]) {
|
||||
playerId = userIdToInfo[e.inviteeId].playerId;
|
||||
} else {
|
||||
// Fallback: slotIndex direkt als playerId nutzen (wenn playerIds das enthält)
|
||||
playerId = playerIds.find(pid => pid === e.slotIndex);
|
||||
}
|
||||
if (!playerId) continue;
|
||||
const inviteeName = (userIdToInfo?.[e.inviteeId]?.name) || e.inviteeName || '';
|
||||
playerInvitations[playerId] = {
|
||||
einladungId: e.einladungId,
|
||||
status: e.status,
|
||||
inviteeId: e.inviteeId,
|
||||
inviteeName,
|
||||
};
|
||||
if (e.status === 'ACCEPTED_OWN' || e.status === 'ACCEPTED_HOST') {
|
||||
try {
|
||||
const dRes = await fetch(`/user/${e.inviteeId}/bdsm-defaults`);
|
||||
if (dRes.ok) playerInvitations[playerId].defaults = await dRes.json();
|
||||
} catch (_) {}
|
||||
}
|
||||
renderPending(playerId);
|
||||
}
|
||||
if (aktive.some(e => e.status === 'PENDING')) startPoll();
|
||||
updateWeiterBtn();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── Init ──
|
||||
const savedSetup = sessionStorage.getItem('bdsm-session-setup');
|
||||
if (savedSetup) {
|
||||
const { mitspieler } = JSON.parse(savedSetup);
|
||||
mitspieler.forEach((p, i) => {
|
||||
const id = addPlayer(p.name, i === 0);
|
||||
restorePlayer(id, p);
|
||||
});
|
||||
} else {
|
||||
Promise.all([
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null),
|
||||
fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}))
|
||||
]).then(([user, defaults]) => {
|
||||
myUserId = user?.userId || null;
|
||||
async function init() {
|
||||
const ok = await ladeSessionOderDraft();
|
||||
if (!ok) return;
|
||||
|
||||
// myUserId immer laden (wird für userId des Host-Spielers benötigt)
|
||||
const user = await fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null);
|
||||
myUserId = user?.userId || null;
|
||||
|
||||
const savedSetup = sessionStorage.getItem('bdsm-session-setup');
|
||||
if (savedSetup) {
|
||||
const { mitspieler } = JSON.parse(savedSetup);
|
||||
const userIdToInfo = {};
|
||||
mitspieler.forEach((p, i) => {
|
||||
const id = addPlayer(p.name, i === 0);
|
||||
restorePlayer(id, p);
|
||||
if (p.userId) userIdToInfo[p.userId] = { playerId: id, name: p.name };
|
||||
});
|
||||
mitspieler.forEach((p, i) => { if (p.userId) pruefeChastityConstraint(playerIds[i], p.userId); });
|
||||
await ladeEinladungenAusDb(userIdToInfo);
|
||||
} else {
|
||||
const defaults = await fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : {}).catch(() => ({}));
|
||||
addPlayer(user ? user.name : '', true);
|
||||
addPlayer();
|
||||
const selfId = playerIds[0];
|
||||
@@ -736,8 +974,12 @@
|
||||
rollen: defaults.rollen || [],
|
||||
werkzeuge: defaults.werkzeuge || [],
|
||||
});
|
||||
});
|
||||
if (myUserId) pruefeChastityConstraint(selfId, myUserId);
|
||||
// Auch für frischen Start: evtl. noch offene Einladungen aus vorheriger Session
|
||||
await ladeEinladungenAusDb(null);
|
||||
}
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -129,11 +129,25 @@
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
if (!sessionStorage.getItem('bdsm-session-setup')) {
|
||||
window.location.replace('/bdsm.html');
|
||||
}
|
||||
let savedGruppen = new Set();
|
||||
|
||||
const savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]'));
|
||||
async function ladeSessionOderDraft() {
|
||||
if (sessionStorage.getItem('bdsm-session-setup')) return true;
|
||||
try {
|
||||
const res = await fetch('/bdsm/setup-draft');
|
||||
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
|
||||
const draft = await res.json();
|
||||
if (draft.setupId) sessionStorage.setItem('bdsm-setup-id', draft.setupId);
|
||||
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
|
||||
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
|
||||
if (draft.gruppenJson) sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson);
|
||||
if (!draft.setupJson) { window.location.replace('/bdsm.html'); return false; }
|
||||
return true;
|
||||
} catch (_) {
|
||||
window.location.replace('/bdsm.html');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let warnungsAkzeptiert = false;
|
||||
|
||||
@@ -308,19 +322,31 @@
|
||||
}
|
||||
|
||||
sessionStorage.setItem('bdsm-session-gruppen', JSON.stringify(selected));
|
||||
// Draft in DB aktualisieren
|
||||
fetch('/bdsm/setup-draft', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gruppenJson: JSON.stringify(selected) }),
|
||||
}).catch(() => {});
|
||||
window.location.href = '/bdsmtoys.html';
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] user HTTP', r.status), { content: [] })),
|
||||
fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] abo HTTP', r.status), { content: [] })),
|
||||
fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : (console.warn('[bdsmtasks] system HTTP', r.status), { content: [] })),
|
||||
]).then(([own, abo, system]) => {
|
||||
console.log('[bdsmtasks] own:', own, 'abo:', abo, 'system:', system);
|
||||
renderList('listOwn', own.content || []);
|
||||
renderList('listSubscribed', abo.content || []);
|
||||
renderList('listSystem', system.content || []);
|
||||
}).catch(err => console.error('[bdsmtasks] Fehler beim Laden:', err));
|
||||
(async function init() {
|
||||
const ok = await ladeSessionOderDraft();
|
||||
if (!ok) return;
|
||||
savedGruppen = new Set(JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || '[]'));
|
||||
|
||||
try {
|
||||
const [own, abo, system] = await Promise.all([
|
||||
fetch('/gruppe/list/user?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
|
||||
fetch('/abo/list?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
|
||||
fetch('/gruppe/list/system?page=0&size=500').then(r => r.ok ? r.json() : { content: [] }),
|
||||
]);
|
||||
renderList('listOwn', own.content || []);
|
||||
renderList('listSubscribed', abo.content || []);
|
||||
renderList('listSystem', system.content || []);
|
||||
} catch (err) { console.error('[bdsmtasks] Fehler beim Laden:', err); }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -84,8 +84,25 @@
|
||||
|
||||
<script src="/js/sidebar.js"></script>
|
||||
<script>
|
||||
const savedGruppen = JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || 'null');
|
||||
if (!savedGruppen) window.location.replace('/bdsm.html');
|
||||
let savedGruppen = JSON.parse(sessionStorage.getItem('bdsm-session-gruppen') || 'null');
|
||||
|
||||
async function ladeSessionOderDraft() {
|
||||
if (savedGruppen) return true;
|
||||
try {
|
||||
const res = await fetch('/bdsm/setup-draft');
|
||||
if (!res.ok) { window.location.replace('/bdsm.html'); return false; }
|
||||
const draft = await res.json();
|
||||
if (draft.setupId) sessionStorage.setItem('bdsm-setup-id', draft.setupId);
|
||||
if (draft.settingsJson) sessionStorage.setItem('bdsm-session-settings', draft.settingsJson);
|
||||
if (draft.setupJson) sessionStorage.setItem('bdsm-session-setup', draft.setupJson);
|
||||
if (draft.gruppenJson) { sessionStorage.setItem('bdsm-session-gruppen', draft.gruppenJson); savedGruppen = JSON.parse(draft.gruppenJson); }
|
||||
if (!savedGruppen) { window.location.replace('/bdsm.html'); return false; }
|
||||
return true;
|
||||
} catch (_) {
|
||||
window.location.replace('/bdsm.html');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Previously saved toy selection (when navigating back from game page)
|
||||
const savedToysRaw = sessionStorage.getItem('bdsm-session-toys');
|
||||
@@ -220,25 +237,57 @@
|
||||
|
||||
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
|
||||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
|
||||
|
||||
const btn = document.querySelector('button[onclick="spielStarten()"]');
|
||||
btn.disabled = true;
|
||||
|
||||
// Für ACCEPTED_OWN Spieler: bereit prüfen und spielerDaten laden
|
||||
const hasOwnDevice = (setup?.mitspieler || []).some(p => p.eigenesGeraet);
|
||||
if (hasOwnDevice) {
|
||||
const setupId = sessionStorage.getItem('bdsm-setup-id');
|
||||
const einladungRes = await fetch(`/bdsm/einladung?setupId=${setupId}`);
|
||||
if (einladungRes.ok) {
|
||||
const einladungen = await einladungRes.json();
|
||||
const nichtBereit = einladungen.filter(e => e.status === 'ACCEPTED_OWN' && !e.bereit);
|
||||
if (nichtBereit.length > 0) {
|
||||
showMessage('Noch nicht alle Mitspieler auf eigenem Gerät haben sich bereit erklärt.', 'error');
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
// Spielerdaten der ACCEPTED_OWN Spieler in setup übernehmen
|
||||
for (const p of setup.mitspieler) {
|
||||
if (!p.eigenesGeraet || !p.einladungId) continue;
|
||||
const inv = einladungen.find(e => e.einladungId === p.einladungId);
|
||||
if (inv && inv.spielerDatenJson) {
|
||||
const daten = JSON.parse(inv.spielerDatenJson);
|
||||
p.geschlecht = daten.geschlecht;
|
||||
p.spieltMit = daten.spieltMit || [];
|
||||
p.rollen = daten.rollen || [];
|
||||
p.werkzeuge = daten.werkzeuge || [];
|
||||
p.sperrenVorFinaleAufloesen = daten.sperrenVorFinaleAufloesen !== false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { errors, warnings } = validateContent(gameContent, settings, setup?.mitspieler || []);
|
||||
|
||||
if (errors.length > 0) {
|
||||
showValidation(errors, warnings, false);
|
||||
warnungsAkzeptiert = false;
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (warnings.length > 0 && !warnungsAkzeptiert) {
|
||||
showValidation([], warnings, true);
|
||||
warnungsAkzeptiert = true;
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('bdsm-session-game', JSON.stringify(gameContent));
|
||||
|
||||
const btn = document.querySelector('button[onclick="spielStarten()"]');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
// 1. Session anlegen
|
||||
const sessionRes = await fetch('/bdsm', {
|
||||
@@ -252,24 +301,25 @@
|
||||
setupId: sessionStorage.getItem('bdsm-setup-id'),
|
||||
}),
|
||||
});
|
||||
if (sessionRes.status === 409) throw new Error('Du hast bereits ein laufendes BDSM-Spiel. Bitte beende es zuerst.');
|
||||
if (!sessionRes.ok) throw new Error('Session konnte nicht angelegt werden.');
|
||||
const location = sessionRes.headers.get('Location');
|
||||
const sessionId = location.split('/').pop();
|
||||
|
||||
// 2. Mitspieler hinzufügen
|
||||
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
|
||||
// 2. Mitspieler hinzufügen (setup bereits oben geladen und mit eigenesGeraet-Daten befüllt)
|
||||
for (const p of setup.mitspieler) {
|
||||
const res = await fetch(`/bdsm/${sessionId}/mitspieler`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: p.name,
|
||||
geschlecht: p.geschlecht,
|
||||
spieltMit: p.spieltMit,
|
||||
rollen: p.rollen,
|
||||
verfuegbareWerkzeuge: p.werkzeuge,
|
||||
userId: p.userId || null,
|
||||
eigenesGeraet: p.eigenesGeraet || false,
|
||||
name: p.name,
|
||||
geschlecht: p.geschlecht,
|
||||
spieltMit: p.spieltMit,
|
||||
rollen: p.rollen,
|
||||
verfuegbareWerkzeuge: p.werkzeuge,
|
||||
userId: p.userId || null,
|
||||
eigenesGeraet: p.eigenesGeraet || false,
|
||||
sperrenVorFinaleAufloesen: p.sperrenVorFinaleAufloesen !== false,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
|
||||
@@ -284,6 +334,8 @@
|
||||
if (!aufgabenRes.ok) throw new Error('Aufgaben konnten nicht gespeichert werden.');
|
||||
|
||||
sessionStorage.setItem('bdsm-session-id', sessionId);
|
||||
// Draft löschen, da Spiel jetzt läuft
|
||||
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
|
||||
window.location.href = '/bdsmingame.html';
|
||||
} catch (e) {
|
||||
showMessage(e.message, 'error');
|
||||
@@ -292,9 +344,13 @@
|
||||
}
|
||||
|
||||
// Load all selected groups, collect content and unique toys
|
||||
Promise.all(
|
||||
savedGruppen.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
|
||||
).then(gruppen => {
|
||||
(async function init() {
|
||||
const ok = await ladeSessionOderDraft();
|
||||
if (!ok) return;
|
||||
|
||||
const gruppen = await Promise.all(
|
||||
savedGruppen.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
|
||||
);
|
||||
gruppen.filter(Boolean).forEach(g => {
|
||||
allContent.aufgaben.push(...(g.aufgaben || []));
|
||||
allContent.strafen.push(...(g.strafen || []));
|
||||
@@ -310,7 +366,7 @@
|
||||
});
|
||||
|
||||
renderToys([...toyMap.values()].sort((a, b) => a.name.localeCompare(b.name)));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,11 +16,86 @@
|
||||
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
|
||||
.wait-title { font-size: 1.3rem; font-weight: 700; margin-bottom: 0.75rem; }
|
||||
.wait-sub { font-size: 0.9rem; color: var(--color-muted); line-height: 1.6; margin-bottom: 2rem; }
|
||||
|
||||
.setup-section { margin-bottom: 2rem; }
|
||||
.setup-section h2 {
|
||||
color: var(--color-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.card-field { margin-bottom: 1rem; }
|
||||
.card-field > label { font-size: 0.8rem; color: #aaa; margin: 0 0 0.5rem 0; display: block; }
|
||||
.check-group { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.check-group--two-col { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.check-item {
|
||||
display: inline-flex; align-items: flex-start; gap: 0.45rem;
|
||||
background: var(--color-secondary); border: 1px solid transparent;
|
||||
border-radius: 6px; padding: 0.4rem 0.7rem;
|
||||
cursor: pointer; transition: border-color 0.15s; user-select: none;
|
||||
}
|
||||
.check-item.is-checked { border-color: var(--color-primary); }
|
||||
.check-item input { accent-color: var(--color-primary); width: auto; margin-top: 0.15rem; cursor: pointer; flex-shrink: 0; }
|
||||
.check-item-label { font-size: 0.88rem; color: var(--color-text); line-height: 1.3; }
|
||||
.check-item-desc { display: block; font-size: 0.72rem; color: var(--color-muted); margin-top: 0.1rem; }
|
||||
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app">
|
||||
<div class="main">
|
||||
<div class="content wait-card">
|
||||
<!-- Konfigurations-Ansicht (für ACCEPTED_OWN Spieler) -->
|
||||
<div class="content" id="configView" style="display:none;">
|
||||
<h1>BDSM Game</h1>
|
||||
<p style="margin-bottom:2rem;">Bitte konfiguriere deine Präferenzen, bevor das Spiel startet.</p>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Deine Daten</h2>
|
||||
<div class="card-field">
|
||||
<label>Geschlecht</label>
|
||||
<div class="check-group" id="geschlechtGroup"></div>
|
||||
<div class="field-error" id="geschlecht-err">Bitte Geschlecht auswählen.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Spielt mit</label>
|
||||
<div class="check-group" id="spieltMitGroup"></div>
|
||||
<div class="field-error" id="spieltmit-err">Bitte mindestens eine Option wählen.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Rollen</label>
|
||||
<div class="check-group" id="rollenGroup"></div>
|
||||
<div class="field-error" id="rollen-err">Bitte mindestens eine Rolle wählen.</div>
|
||||
</div>
|
||||
<div class="card-field">
|
||||
<label>Verfügbar</label>
|
||||
<div class="check-group check-group--two-col" id="werkzeugeGroup"></div>
|
||||
<div class="field-error" id="werkzeuge-err">Bitte mindestens ein Werkzeug wählen.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Finale</h2>
|
||||
<div class="card-field">
|
||||
<label class="check-item is-checked" id="sperreLabel">
|
||||
<input type="checkbox" id="sperrenAufloesen" checked onchange="toggleSperreWarn()">
|
||||
<span class="check-item-label">Zeitstrafen vor dem Finale auflösen</span>
|
||||
</label>
|
||||
<div style="display:none; margin-top:0.4rem; font-size:0.78rem; color:var(--color-primary);" id="sperreWarn">
|
||||
⚠️ Hinweis: Zeitstrafen werden nicht aufgelöst. Du könntest im Finale leer ausgehen.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message" id="configMessage" style="display:none;"></div>
|
||||
<button onclick="bereitMachen()" style="width:100%;">Bereit</button>
|
||||
<button class="secondary" style="width:100%; margin-top:0.75rem;" onclick="abbrechen()">Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<!-- Warte-Ansicht -->
|
||||
<div class="content wait-card" id="waitView" style="display:none;">
|
||||
<div class="wait-icon">⏳</div>
|
||||
<div class="wait-title">Warte auf Spielstart…</div>
|
||||
<div class="wait-sub" id="sub">Der Host startet das Spiel in Kürze. Diese Seite aktualisiert sich automatisch.</div>
|
||||
@@ -34,6 +109,159 @@
|
||||
const einladungId = params.get('id');
|
||||
if (!einladungId) window.location.replace('/userhome.html');
|
||||
|
||||
const GESCHLECHTER = [
|
||||
{ value: 'MAENNLICH', label: 'Männlich' },
|
||||
{ value: 'WEIBLICH', label: 'Weiblich' },
|
||||
{ value: 'DIVERS', label: 'Divers' },
|
||||
];
|
||||
const ROLLEN = [
|
||||
{ value: 'AUFGABE_AKTIV', label: 'Aufgabe – Aktiv' },
|
||||
{ value: 'AUFGABE_PASSIV', label: 'Aufgabe – Passiv' },
|
||||
{ value: 'BESTRAFUNG_AKTIV', label: 'Bestrafung – Aktiv' },
|
||||
{ value: 'BESTRAFUNG_PASSIV', label: 'Bestrafung – Passiv' },
|
||||
];
|
||||
const WERKZEUGE = [
|
||||
{ value: 'MUND', label: 'Mund', desc: 'Gewillt den Mund einzusetzen' },
|
||||
{ value: 'VAGINA', label: 'Vagina', desc: 'Verfügt über eine Vagina und setzt sie ein' },
|
||||
{ value: 'PENIS', label: 'Penis', desc: 'Verfügt über einen Penis und setzt ihn ein' },
|
||||
{ value: 'ANUS', label: 'Anus', desc: 'Gewillt den Anus einzusetzen' },
|
||||
{ value: 'UMSCHNALLDILDO', label: 'Umschnall-Dildo', desc: 'Verfügt über einen Umschnall-Dildo' },
|
||||
];
|
||||
const WERKZEUGE_DEFAULTS = {
|
||||
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
|
||||
};
|
||||
|
||||
function buildCheckItems(containerId, items, type) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = items.map(({ value, label, desc }) => `
|
||||
<label class="check-item">
|
||||
<input type="${type}" name="${containerId}" value="${value}">
|
||||
<span>
|
||||
<span class="check-item-label">${label}</span>
|
||||
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
|
||||
</span>
|
||||
</label>`).join('');
|
||||
}
|
||||
|
||||
function initForm() {
|
||||
buildCheckItems('geschlechtGroup', GESCHLECHTER, 'radio');
|
||||
buildCheckItems('spieltMitGroup', GESCHLECHTER, 'checkbox');
|
||||
buildCheckItems('rollenGroup', ROLLEN, 'checkbox');
|
||||
buildCheckItems('werkzeugeGroup', WERKZEUGE, 'checkbox');
|
||||
|
||||
document.addEventListener('change', e => {
|
||||
const input = e.target;
|
||||
if (input.type !== 'checkbox' && input.type !== 'radio') return;
|
||||
if (input.type === 'radio') {
|
||||
document.querySelectorAll(`input[name="${input.name}"]`).forEach(r =>
|
||||
r.closest('.check-item')?.classList.toggle('is-checked', r.checked));
|
||||
if (input.checked && input.name === 'geschlechtGroup') {
|
||||
const defaults = WERKZEUGE_DEFAULTS[input.value] || [];
|
||||
document.querySelectorAll('input[name="werkzeugeGroup"]').forEach(cb => {
|
||||
cb.checked = defaults.includes(cb.value);
|
||||
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
input.closest('.check-item')?.classList.toggle('is-checked', input.checked);
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-fill from profile + bdsm-defaults
|
||||
Promise.all([
|
||||
fetch('/login/me').then(r => r.ok ? r.json() : null).catch(() => null),
|
||||
fetch('/user/me/bdsm-defaults').then(r => r.ok ? r.json() : null).catch(() => null),
|
||||
]).then(([user, bdsmDefaults]) => {
|
||||
if (user?.geschlecht) {
|
||||
const radio = document.querySelector(`input[name="geschlechtGroup"][value="${user.geschlecht}"]`);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
radio.closest('.check-item')?.classList.add('is-checked');
|
||||
const defaults = (bdsmDefaults?.werkzeuge?.length > 0)
|
||||
? bdsmDefaults.werkzeuge
|
||||
: (WERKZEUGE_DEFAULTS[user.geschlecht] || []);
|
||||
document.querySelectorAll('input[name="werkzeugeGroup"]').forEach(cb => {
|
||||
cb.checked = defaults.includes(cb.value);
|
||||
cb.closest('.check-item')?.classList.toggle('is-checked', cb.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bdsmDefaults?.spieltMit?.length > 0) {
|
||||
bdsmDefaults.spieltMit.forEach(val => {
|
||||
const cb = document.querySelector(`input[name="spieltMitGroup"][value="${val}"]`);
|
||||
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
|
||||
});
|
||||
}
|
||||
if (bdsmDefaults?.rollen?.length > 0) {
|
||||
bdsmDefaults.rollen.forEach(val => {
|
||||
const cb = document.querySelector(`input[name="rollenGroup"][value="${val}"]`);
|
||||
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getChecked(name) {
|
||||
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(el => el.value);
|
||||
}
|
||||
|
||||
function toggleSperreWarn() {
|
||||
const cb = document.getElementById('sperrenAufloesen');
|
||||
const warn = document.getElementById('sperreWarn');
|
||||
const label = document.getElementById('sperreLabel');
|
||||
if (warn) warn.style.display = cb.checked ? 'none' : 'block';
|
||||
if (label) label.classList.toggle('is-checked', cb.checked);
|
||||
}
|
||||
|
||||
function setFieldError(id, show) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function bereitMachen() {
|
||||
const geschlecht = getChecked('geschlechtGroup');
|
||||
const spieltMit = getChecked('spieltMitGroup');
|
||||
const rollen = getChecked('rollenGroup');
|
||||
const werkzeuge = getChecked('werkzeugeGroup');
|
||||
|
||||
setFieldError('geschlecht-err', geschlecht.length === 0);
|
||||
setFieldError('spieltmit-err', spieltMit.length === 0);
|
||||
setFieldError('rollen-err', rollen.length === 0);
|
||||
setFieldError('werkzeuge-err', werkzeuge.length === 0);
|
||||
|
||||
if (!geschlecht.length || !spieltMit.length || !rollen.length || !werkzeuge.length) return;
|
||||
|
||||
const sperrenAufloesen = document.getElementById('sperrenAufloesen');
|
||||
const spielerDatenJson = JSON.stringify({
|
||||
geschlecht: geschlecht[0],
|
||||
spieltMit,
|
||||
rollen,
|
||||
werkzeuge,
|
||||
sperrenVorFinaleAufloesen: sperrenAufloesen ? sperrenAufloesen.checked : true,
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung/${einladungId}/spielerdaten`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ spielerDatenJson }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
document.getElementById('configView').style.display = 'none';
|
||||
document.getElementById('waitView').style.display = 'block';
|
||||
// Sofort prüfen + Polling starten
|
||||
pruefen();
|
||||
pollInterval = setInterval(pruefen, 3000);
|
||||
} catch (_) {
|
||||
const el = document.getElementById('configMessage');
|
||||
el.textContent = 'Fehler beim Speichern. Bitte erneut versuchen.';
|
||||
el.className = 'message error';
|
||||
el.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
let pollInterval = null;
|
||||
|
||||
async function pruefen() {
|
||||
@@ -49,19 +277,18 @@
|
||||
}
|
||||
|
||||
if (data.sessionId) {
|
||||
stopPoll();
|
||||
// Meinen Mitspieler laden
|
||||
try {
|
||||
const mRes = await fetch(`/bdsm/${data.sessionId}/mitspieler/me`);
|
||||
if (mRes.ok) {
|
||||
if (mRes.status === 200) {
|
||||
const mData = await mRes.json();
|
||||
sessionStorage.setItem('bdsm-guest-mitspieler-id', mData.mitspielerId);
|
||||
sessionStorage.setItem('bdsm-guest-name', mData.name);
|
||||
sessionStorage.setItem('bdsm-session-id', data.sessionId);
|
||||
sessionStorage.setItem('bdsm-is-guest', 'true');
|
||||
stopPoll();
|
||||
window.location.replace('/bdsmingame.html');
|
||||
}
|
||||
} catch (_) {}
|
||||
sessionStorage.setItem('bdsm-session-id', data.sessionId);
|
||||
sessionStorage.setItem('bdsm-is-guest', 'true');
|
||||
window.location.replace('/bdsmingame.html');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -71,6 +298,8 @@
|
||||
}
|
||||
|
||||
function zeigeFehler(text) {
|
||||
document.getElementById('waitView').style.display = 'block';
|
||||
document.getElementById('configView').style.display = 'none';
|
||||
document.getElementById('sub').style.display = 'none';
|
||||
const el = document.getElementById('message');
|
||||
el.textContent = text;
|
||||
@@ -84,9 +313,35 @@
|
||||
window.location.href = '/userhome.html';
|
||||
}
|
||||
|
||||
// Sofort prüfen, dann alle 3 Sekunden
|
||||
pruefen();
|
||||
pollInterval = setInterval(pruefen, 3000);
|
||||
// Init: check if already bereit or if config needed
|
||||
async function init() {
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung/${einladungId}`);
|
||||
if (!res.ok) { window.location.replace('/userhome.html'); return; }
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'CANCELLED') {
|
||||
document.getElementById('waitView').style.display = 'block';
|
||||
zeigeFehler('Die Einladung wurde abgebrochen.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 'ACCEPTED_OWN' && !data.bereit) {
|
||||
// Show config form
|
||||
initForm();
|
||||
document.getElementById('configView').style.display = 'block';
|
||||
// Don't start polling yet – user must submit form first
|
||||
} else {
|
||||
// Already bereit or ACCEPTED_HOST → show waiting screen + start poll
|
||||
document.getElementById('waitView').style.display = 'block';
|
||||
pruefen();
|
||||
pollInterval = setInterval(pruefen, 3000);
|
||||
}
|
||||
} catch (_) {
|
||||
window.location.replace('/userhome.html');
|
||||
}
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -410,7 +410,7 @@
|
||||
<div class="profil-tabs" style="margin-top:1.25rem;">
|
||||
<button class="profil-tab-btn active" id="tabBtnPosts" onclick="switchProfilTab('posts', this)">Feed</button>
|
||||
<button class="profil-tab-btn" id="tabBtnPinnwand" onclick="switchProfilTab('pinnwand', this)">Pinnwand</button>
|
||||
<button class="profil-tab-btn" id="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this); loadGameHistory()">Spielhistorie</button>
|
||||
<button class="profil-tab-btn" id="tabBtnGameHistory" onclick="switchProfilTab('gamehistory', this)">Spielhistorie</button>
|
||||
</div>
|
||||
|
||||
<!-- Feed Tab (vorausgewählt) -->
|
||||
@@ -559,6 +559,7 @@
|
||||
|
||||
// ── Tabs: Feed, Pinnwand, Spielhistorie ──
|
||||
applyTabPrivacy(profile, isFriend);
|
||||
activateTabFromUrl();
|
||||
|
||||
if (canSee(profile.sichtbarkeitPinnwand, isFriend, isOwnProfile)) {
|
||||
await loadPinnwand();
|
||||
@@ -606,6 +607,7 @@
|
||||
const xpVisible = canSee(profile.sichtbarkeitXp, profile.friendStatus === 'FRIEND', isOwnProfile);
|
||||
if (xpVisible && profile.lockeeXp > 0) addTag('🔒 Lockee XP', profile.lockeeXp + ' XP');
|
||||
if (xpVisible && profile.keyholderXp > 0) addTag('🔑 Keyholder XP', profile.keyholderXp + ' XP');
|
||||
if (xpVisible && profile.bdsmXp > 0) addTag('⛓ BDSM XP', profile.bdsmXp + ' XP');
|
||||
|
||||
// Action buttons
|
||||
const actions = document.getElementById('profileActions');
|
||||
@@ -671,11 +673,33 @@
|
||||
}
|
||||
|
||||
// ── Tab switching ──
|
||||
let _gameHistoryLoaded = false;
|
||||
function switchProfilTab(name, btn) {
|
||||
document.querySelectorAll('.profil-tab-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.querySelectorAll('.profil-tab-panel').forEach(p => p.classList.remove('active'));
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
if (name === 'gamehistory' && !_gameHistoryLoaded) {
|
||||
_gameHistoryLoaded = true;
|
||||
loadGameHistory();
|
||||
}
|
||||
// URL-QueryParam aktualisieren, damit der Tab nach F5 erhalten bleibt
|
||||
const url = new URL(window.location.href);
|
||||
if (name === 'posts') {
|
||||
url.searchParams.delete('tab');
|
||||
} else {
|
||||
url.searchParams.set('tab', name);
|
||||
}
|
||||
history.replaceState(null, '', url.toString());
|
||||
}
|
||||
|
||||
function activateTabFromUrl() {
|
||||
const tab = new URLSearchParams(window.location.search).get('tab');
|
||||
if (!tab || tab === 'posts') return;
|
||||
const btn = document.querySelector(`[id^="tabBtn"][onclick*="'${tab}'"]`);
|
||||
if (btn && btn.style.display !== 'none') {
|
||||
switchProfilTab(tab, btn);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gallery Carousel ──
|
||||
@@ -902,7 +926,7 @@
|
||||
<div style="flex-shrink:0;width:3rem;text-align:center;">${gameIcon}</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-weight:600;font-size:0.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${esc(e.gameName) || 'Unbenannt'}</div>
|
||||
<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.15rem;">⏱ ${dur} · ${new Date(e.endTime).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'})}</div>
|
||||
<div style="font-size:0.78rem;color:var(--color-muted);margin-top:0.15rem;">⏱ ${dur} · ${new Date(e.unlockTime).toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric'})}</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.35rem;align-items:center;flex-shrink:0;">${participants}</div>
|
||||
</div>`;
|
||||
|
||||
@@ -255,6 +255,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BDSM-Einladungs-Dialog -->
|
||||
<div class="lockee-dialog-bg" id="bdsmInviteDialog">
|
||||
<div class="lockee-dialog-overlay" onclick="closeBdsmInviteDialog()"></div>
|
||||
<div class="lockee-dialog-box">
|
||||
<button onclick="closeBdsmInviteDialog()" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--color-muted);font-size:1.3rem;cursor:pointer;padding:0.2rem 0.5rem;line-height:1;" title="Schließen">✕</button>
|
||||
<div class="lockee-dialog-header">
|
||||
<div class="lockee-dialog-avatar" id="bdsmDialogAvatar">⛓️</div>
|
||||
<div>
|
||||
<div class="lockee-dialog-title" id="bdsmDialogTitle"></div>
|
||||
<div class="lockee-dialog-sub">BDSM Game – Einladung</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:0.88rem;color:var(--color-muted);line-height:1.5;margin:0;">
|
||||
Du wurdest zu einem BDSM Game eingeladen. Wie möchtest du mitspielen?
|
||||
</p>
|
||||
<div class="lockee-dialog-error" id="bdsmDialogError"></div>
|
||||
<div class="lockee-dialog-actions" style="flex-direction:column;gap:0.5rem;">
|
||||
<button class="btn-accept" style="width:100%;" onclick="acceptBdsmOwnDevice()">Am eigenen Gerät mitspielen</button>
|
||||
<button class="btn-accept" style="width:100%;background:#1a5c8a!important;" onclick="acceptBdsmHostDevice()">Am Gerät des Hosts mitspielen</button>
|
||||
<button class="btn-decline" style="width:100%;" onclick="declineBdsmFromDialog()">Einladung ablehnen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lockee-Einladungs-Dialog -->
|
||||
<div class="lockee-dialog-bg" id="lockeeInviteDialog">
|
||||
<div class="lockee-dialog-overlay" onclick="closeLockeeInviteDialog()"></div>
|
||||
@@ -330,7 +354,7 @@
|
||||
}
|
||||
|
||||
function buildAvatarHtml(picBase64, type) {
|
||||
const badge = type === 'keyholder' ? '🔑' : '🔒';
|
||||
const badge = type === 'keyholder' ? '🔑' : type === 'bdsm' ? '⛓️' : '🔒';
|
||||
const inner = picBase64
|
||||
? `<div class="inv-avatar"><img src="data:image/jpeg;base64,${picBase64}" alt=""></div>`
|
||||
: `<div class="inv-avatar">👤</div>`;
|
||||
@@ -350,17 +374,20 @@
|
||||
// ── Empfangen laden ──
|
||||
async function loadReceivedInvitations() {
|
||||
try {
|
||||
const [lockeeRes, khRes] = await Promise.all([
|
||||
const [lockeeRes, khRes, bdsmRes] = await Promise.all([
|
||||
fetch('/lockee/invitations/mine'),
|
||||
fetch('/keyholder/invitations/mine')
|
||||
fetch('/keyholder/invitations/mine'),
|
||||
fetch('/bdsm/einladung/pending'),
|
||||
]);
|
||||
const lockeeInvs = lockeeRes.ok ? await lockeeRes.json() : [];
|
||||
const khInvs = khRes.ok ? await khRes.json() : [];
|
||||
const bdsmInvs = bdsmRes.ok ? await bdsmRes.json() : [];
|
||||
|
||||
lockeeInvs.forEach(inv => { inv._type = 'lockee'; inv._otherName = inv.keyholderName; inv._otherPic = inv.keyholderProfilePic; });
|
||||
khInvs.forEach(inv => { inv._type = 'keyholder'; inv._otherName = inv.lockeeName; inv._otherPic = inv.lockeeProfilePic; });
|
||||
lockeeInvs.forEach(inv => { inv._type = 'lockee'; inv._key = inv.token; inv._otherName = inv.keyholderName; inv._otherPic = inv.keyholderProfilePic; });
|
||||
khInvs.forEach(inv => { inv._type = 'keyholder'; inv._key = inv.token; inv._otherName = inv.lockeeName; inv._otherPic = inv.lockeeProfilePic; });
|
||||
bdsmInvs.forEach(inv => { inv._type = 'bdsm'; inv._key = inv.einladungId; inv._otherName = inv.inviterName; inv._otherPic = inv.inviterAvatar; });
|
||||
|
||||
recvItems = [...lockeeInvs, ...khInvs].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
recvItems = [...lockeeInvs, ...khInvs, ...bdsmInvs].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
recvPage = 0;
|
||||
renderRecvPage();
|
||||
} catch(e) { console.error(e); }
|
||||
@@ -386,31 +413,40 @@
|
||||
const av = buildAvatarHtml(inv._otherPic, inv._type);
|
||||
const card = document.createElement('div');
|
||||
card.className = 'inv-card';
|
||||
card.id = 'recvinv-' + inv.token;
|
||||
card.id = 'recvinv-' + inv._key;
|
||||
if (inv._type === 'lockee') card.dataset.detailsVisible = inv.detailsVisible ? '1' : '0';
|
||||
|
||||
const typeLabel = inv._type === 'lockee' ? 'Lockee-Einladung' : 'Keyholder-Einladung';
|
||||
let actions;
|
||||
let typeLabel, line2, actions;
|
||||
if (inv._type === 'lockee') {
|
||||
typeLabel = 'Lockee-Einladung';
|
||||
line2 = 'Lockee: ' + esc(inv.lockName);
|
||||
actions = `
|
||||
<div style="display:flex;flex-direction:column;gap:0.4rem;flex-shrink:0;">
|
||||
<button onclick="declineLockeeInvitation('${esc(inv.token)}', this)" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:#c0392b;border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">✕ Ablehnen</button>
|
||||
<button onclick="openLockeeInviteDialog('${esc(inv.token)}')" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:var(--color-success,#27ae60);border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">✓ Details</button>
|
||||
</div>`;
|
||||
} else {
|
||||
} else if (inv._type === 'keyholder') {
|
||||
typeLabel = 'Keyholder-Einladung';
|
||||
line2 = 'Keyholder: ' + esc(inv.lockName);
|
||||
actions = `
|
||||
<div style="display:flex;flex-direction:column;gap:0.4rem;flex-shrink:0;">
|
||||
<button onclick="declineKhInvitation('${esc(inv.token)}', this)" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:#c0392b;border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">✕ Ablehnen</button>
|
||||
<a href="/keyholder/invitation/${esc(inv.token)}" style="display:block;text-align:center;padding:0.45rem 1rem;font-size:0.85rem;background:var(--color-success);color:#fff;border-radius:6px;text-decoration:none;font-weight:600;">✓ Annehmen</a>
|
||||
</div>`;
|
||||
} else {
|
||||
typeLabel = 'BDSM Game';
|
||||
line2 = 'Spieleinladung';
|
||||
actions = `
|
||||
<div style="display:flex;flex-direction:column;gap:0.4rem;flex-shrink:0;">
|
||||
<button onclick="openBdsmInviteDialog('${esc(inv.einladungId)}', '${esc(inv._otherName)}', '${esc(inv._otherPic || '')}')" style="margin:0;padding:0.45rem 1rem;font-size:0.85rem;width:auto;background:var(--color-success,#27ae60);border:none;color:#fff;border-radius:6px;font-weight:600;cursor:pointer;">⛓️ Details</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const rolePrefix = inv._type === 'lockee' ? 'Lockee: ' : 'Keyholder: ';
|
||||
card.innerHTML = `
|
||||
${av}
|
||||
<div class="inv-body">
|
||||
<div class="inv-line1">${esc(inv._otherName)}</div>
|
||||
<div class="inv-line2">${rolePrefix}${esc(inv.lockName)}</div>
|
||||
<div class="inv-line2">${line2}</div>
|
||||
<div class="inv-line3">${typeLabel} · ${fmtDate(inv.createdAt)}</div>
|
||||
</div>
|
||||
${actions}`;
|
||||
@@ -428,8 +464,8 @@
|
||||
document.getElementById('recvList').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function removeRecvItem(token) {
|
||||
recvItems = recvItems.filter(i => i.token !== token);
|
||||
function removeRecvItem(key) {
|
||||
recvItems = recvItems.filter(i => i._key !== key);
|
||||
const total = Math.ceil(recvItems.length / PAGE_SIZE);
|
||||
if (recvPage >= total && recvPage > 0) recvPage = total - 1;
|
||||
renderRecvPage();
|
||||
@@ -766,10 +802,62 @@
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ── BDSM-Einladungs-Dialog ──
|
||||
let activeBdsmEinladungId = null;
|
||||
|
||||
function openBdsmInviteDialog(einladungId, inviterName, inviterPic) {
|
||||
activeBdsmEinladungId = einladungId;
|
||||
document.getElementById('bdsmDialogTitle').textContent = inviterName + ' lädt dich ein';
|
||||
document.getElementById('bdsmDialogError').style.display = 'none';
|
||||
const avatarEl = document.getElementById('bdsmDialogAvatar');
|
||||
avatarEl.innerHTML = inviterPic
|
||||
? `<img src="data:image/jpeg;base64,${inviterPic}" alt="" style="width:100%;height:100%;object-fit:cover;">`
|
||||
: '⛓️';
|
||||
document.getElementById('bdsmInviteDialog').classList.add('open');
|
||||
}
|
||||
|
||||
function closeBdsmInviteDialog() {
|
||||
document.getElementById('bdsmInviteDialog').classList.remove('open');
|
||||
activeBdsmEinladungId = null;
|
||||
}
|
||||
|
||||
async function _bdsmAntworten(mode) {
|
||||
if (!activeBdsmEinladungId) return;
|
||||
const accepted = mode !== null;
|
||||
const errEl = document.getElementById('bdsmDialogError');
|
||||
errEl.style.display = 'none';
|
||||
try {
|
||||
const res = await fetch(`/bdsm/einladung/${activeBdsmEinladungId}/antwort`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accepted, mode }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const key = activeBdsmEinladungId;
|
||||
closeBdsmInviteDialog();
|
||||
removeRecvItem(key);
|
||||
if (mode === 'OWN_DEVICE') {
|
||||
window.location.href = `/bdsmwarten.html?id=${key}`;
|
||||
}
|
||||
} catch (_) {
|
||||
errEl.textContent = 'Fehler beim Speichern der Antwort.';
|
||||
errEl.style.display = '';
|
||||
}
|
||||
}
|
||||
|
||||
function acceptBdsmOwnDevice() { _bdsmAntworten('OWN_DEVICE'); }
|
||||
function acceptBdsmHostDevice() { _bdsmAntworten('HOST_DEVICE'); }
|
||||
|
||||
async function declineBdsmFromDialog() {
|
||||
if (!await showConfirm('Einladung ablehnen', 'Möchtest du diese BDSM-Game-Einladung wirklich ablehnen?')) return;
|
||||
_bdsmAntworten(null);
|
||||
}
|
||||
|
||||
// ── Esc schließt Dialog ──
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && document.getElementById('lockeeInviteDialog').classList.contains('open')) {
|
||||
closeLockeeInviteDialog();
|
||||
if (e.key === 'Escape') {
|
||||
if (document.getElementById('bdsmInviteDialog').classList.contains('open')) closeBdsmInviteDialog();
|
||||
if (document.getElementById('lockeeInviteDialog').classList.contains('open')) closeLockeeInviteDialog();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>xXx Games</title>
|
||||
<link rel="stylesheet" href="/css/variables.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="icon" type="image/png" href="icon.png">
|
||||
<link rel="icon" href="/img/icon.png" type="image/png">
|
||||
</head>
|
||||
<body>
|
||||
<img src="img/icon.png" alt="Icon">
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
label: 'BDSM Game',
|
||||
icon: '◆',
|
||||
items: [
|
||||
{ href: '/bdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/bdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
|
||||
{ href: '#', icon: '⏳', label: 'Aktive Session', id: 'navBdsmAktiv' },
|
||||
{ href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' },
|
||||
{ href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' },
|
||||
{ href: '/toys.html', icon: '◈', label: 'Toys' },
|
||||
{ href: '/entdecken.html', icon: '⊙', label: 'Entdecken' },
|
||||
@@ -90,10 +91,12 @@
|
||||
});
|
||||
});
|
||||
|
||||
// "Im Spiel" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
|
||||
// "Im Spiel" und "Aktive Session" standardmäßig ausblenden; wird nach Session-Check ggf. wieder eingeblendet
|
||||
const navNeu = document.getElementById('navBdsmNeu');
|
||||
const navAktiv = document.getElementById('navBdsmAktiv');
|
||||
const navImSpiel = document.getElementById('navBdsmImSpiel');
|
||||
const navCAktiv = document.getElementById('navChastityAktiv');
|
||||
if (navAktiv) navAktiv.style.display = 'none';
|
||||
if (navImSpiel) navImSpiel.style.display = 'none';
|
||||
if (navCAktiv) navCAktiv.style.display = 'none';
|
||||
|
||||
@@ -105,10 +108,26 @@
|
||||
|
||||
// BDSM Session-Status
|
||||
try {
|
||||
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
const hasSession = sessionRes.status === 200;
|
||||
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
|
||||
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';
|
||||
// Zuerst aktive Einladung prüfen (eigenesGeraet-Spieler)
|
||||
const aktivRes = await fetch('/bdsm/einladung/meine-aktive');
|
||||
if (aktivRes.ok) {
|
||||
const aktiv = await aktivRes.json();
|
||||
if (navNeu) navNeu.style.display = 'none';
|
||||
if (navImSpiel) navImSpiel.style.display = 'none';
|
||||
if (navAktiv) {
|
||||
navAktiv.style.display = '';
|
||||
const ziel = aktiv.sessionId
|
||||
? '/bdsmingame.html'
|
||||
: `/bdsmwarten.html?id=${aktiv.einladungId}`;
|
||||
navAktiv.querySelector('a').href = ziel;
|
||||
}
|
||||
} else {
|
||||
// Dann laufende Host-Session prüfen
|
||||
const sessionRes = await fetch(`/bdsm?userId=${user.userId}`);
|
||||
const hasSession = sessionRes.status === 200;
|
||||
if (navNeu) navNeu.style.display = hasSession ? 'none' : '';
|
||||
if (navImSpiel) navImSpiel.style.display = hasSession ? '' : 'none';
|
||||
}
|
||||
} catch (_) { /* Menü bleibt im Standardzustand */ }
|
||||
|
||||
// Chastity Lock-Status
|
||||
|
||||
@@ -150,9 +150,10 @@
|
||||
|
||||
Promise.all([
|
||||
fetch('/keyholder/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
|
||||
fetch('/lockee/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => [])
|
||||
]).then(([khInvs, lockeeInvs]) =>
|
||||
setBadge(['socialInvBadge', 'socialMobileInvBadge'], khInvs.length + lockeeInvs.length)
|
||||
fetch('/lockee/invitations/mine').then(r => r.ok ? r.json() : []).catch(() => []),
|
||||
fetch('/bdsm/einladung/pending').then(r => r.ok ? r.json() : []).catch(() => [])
|
||||
]).then(([khInvs, lockeeInvs, bdsmInvs]) =>
|
||||
setBadge(['socialInvBadge', 'socialMobileInvBadge'], khInvs.length + lockeeInvs.length + bdsmInvs.length)
|
||||
).catch(() => {});
|
||||
|
||||
// ── SSE: Echtzeit-Push vom Server ──
|
||||
|
||||
Reference in New Issue
Block a user