Weiter am BDSM Multiplayer gearbeitet und verknüpfung der verschiedenen games

This commit is contained in:
2026-03-20 00:42:00 +01:00
parent 655cdad796
commit dc0a3f6e85
795 changed files with 10682 additions and 7176 deletions

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -17,7 +17,7 @@ import java.util.UUID;
@Getter
@Setter
@Entity
@Table(name = "session")
@Table(name = "bdsm_game")
public class BdsmGameEntity {
@Id

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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(),

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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'")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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>

View File

@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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} &nbsp;·&nbsp; ${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} &nbsp;·&nbsp; ${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>`;

View File

@@ -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();
}
});

View File

@@ -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">

View File

@@ -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

View File

@@ -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 ──