Weiter an der Konfig Maske für das BDSM Game gearbeitet, Refactoring der Controller Klassen

This commit is contained in:
2026-03-20 16:05:25 +01:00
parent dc0a3f6e85
commit 173302e0ee
52 changed files with 7060 additions and 6825 deletions

View File

@@ -25,7 +25,8 @@
"Bash(for f:*)", "Bash(for f:*)",
"Bash(ls:*)", "Bash(ls:*)",
"Bash(./gradlew compileJava)", "Bash(./gradlew compileJava)",
"Bash(./gradlew build:*)" "Bash(./gradlew build:*)",
"Bash(find /home/mario/Workspaces/xxx-thegame -type f \\\\\\(-name *bdsm* -o -name *BDSM* \\\\\\))"
] ]
} }
} }

View File

@@ -1,5 +1,5 @@
#Thu Mar 19 23:00:53 CET 2026 #Fri Mar 20 15:47:06 CET 2026
display=\:0 display=\:0
host=Mario-Linux host=Mario-Linux
process-id=50461 process-id=112524
user=mario user=mario

View File

@@ -3416,3 +3416,616 @@ java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plu
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822) at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508) at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583) at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-20 00:43:19.865
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 00:43:20.572
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!SESSION 2026-03-20 07:44:39.589 -----------------------------------------------
eclipse.buildId=4.39.0.20260305-0817
java.version=21.0.10
java.vendor=Eclipse Adoptium
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
Framework arguments: -product org.eclipse.epp.package.java.product
Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product
!ENTRY ch.qos.logback.classic 1 0 2026-03-20 07:44:40.568
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
!ENTRY ch.qos.logback.classic 1 0 2026-03-20 07:44:42.780
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
!ENTRY org.eclipse.ui 2 0 2026-03-20 07:44:42.926
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-20 07:44:42.926
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui 2 0 2026-03-20 07:44:43.066
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-20 07:44:43.066
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui.workbench 4 0 2026-03-20 07:44:54.556
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!SESSION 2026-03-20 10:34:12.527 -----------------------------------------------
eclipse.buildId=4.39.0.20260305-0817
java.version=21.0.10
java.vendor=Eclipse Adoptium
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
Framework arguments: -product org.eclipse.epp.package.java.product
Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product
!ENTRY ch.qos.logback.classic 1 0 2026-03-20 10:34:13.217
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
!ENTRY org.eclipse.core.resources 2 10035 2026-03-20 10:34:15.534
!MESSAGE The workspace exited with unsaved changes in the previous session; refreshing workspace to recover changes.
!ENTRY ch.qos.logback.classic 1 0 2026-03-20 10:34:15.774
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
!ENTRY org.eclipse.ui 2 0 2026-03-20 10:34:15.901
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-20 10:34:15.901
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui 2 0 2026-03-20 10:34:16.018
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-20 10:34:16.018
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui.workbench 4 0 2026-03-20 11:05:16.162
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!SESSION 2026-03-20 12:07:51.165 -----------------------------------------------
eclipse.buildId=4.39.0.20260305-0817
java.version=21.0.10
java.vendor=Eclipse Adoptium
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
Framework arguments: -product org.eclipse.epp.package.java.product
Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product
!ENTRY ch.qos.logback.classic 1 0 2026-03-20 12:07:51.857
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
!ENTRY org.eclipse.core.resources 2 10035 2026-03-20 12:07:59.028
!MESSAGE The workspace exited with unsaved changes in the previous session; refreshing workspace to recover changes.
!ENTRY ch.qos.logback.classic 1 0 2026-03-20 12:07:59.273
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
!ENTRY org.eclipse.ui 2 0 2026-03-20 12:07:59.406
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-20 12:07:59.406
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui 2 0 2026-03-20 12:07:59.525
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-20 12:07:59.525
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui.workbench 4 0 2026-03-20 12:57:37.530
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!ENTRY org.eclipse.ui.workbench 4 0 2026-03-20 13:10:19.604
!MESSAGE Dynamic menu contribution 'DynamicContributionItems(id=org.eclipse.terminal.connector.local.LocalLauncherDynamicContributionItems, visible=true)' threw an unexpected exception
!STACK 0
org.eclipse.swt.SWTException: i/o error (java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden))
at org.eclipse.swt.SWT.error(SWT.java:4950)
at org.eclipse.swt.SWT.error(SWT.java:4865)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:207)
at org.eclipse.swt.graphics.ImageLoader.load(ImageLoader.java:198)
at org.eclipse.terminal.view.ui.internal.local.showin.ExternalExecutablesUtils.loadImage(ExternalExecutablesUtils.java:38)
at org.eclipse.terminal.view.ui.internal.local.showin.DynamicContributionItems.getContributionItems(DynamicContributionItems.java:76)
at org.eclipse.ui.actions.CompoundContributionItem.getContributionItemsToFill(CompoundContributionItem.java:83)
at org.eclipse.ui.actions.CompoundContributionItem.fill(CompoundContributionItem.java:57)
at org.eclipse.ui.internal.menus.DynamicMenuContributionItem.fill(DynamicMenuContributionItem.java:194)
at org.eclipse.jface.action.MenuManager.doItemFill(MenuManager.java:727)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:804)
at org.eclipse.jface.action.MenuManager.update(MenuManager.java:671)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.scheduleManagerUpdate(MenuManagerRenderer.java:1149)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer.subscribeUIElementTopicVisible(MenuManagerRenderer.java:211)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.e4.core.internal.di.MethodRequestor.execute(MethodRequestor.java:58)
at org.eclipse.swt.widgets.Synchronizer.syncExec(Synchronizer.java:183)
at org.eclipse.ui.internal.UISynchronizer.syncExec(UISynchronizer.java:136)
at org.eclipse.swt.widgets.Display.syncExec(Display.java:5950)
at org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize.syncExec(DisplayUISynchronize.java:34)
at org.eclipse.e4.ui.internal.di.UIEventObjectSupplier$UIEventHandler.handleEvent(UIEventObjectSupplier.java:65)
at org.eclipse.equinox.internal.event.EventHandlerWrapper.handleEvent(EventHandlerWrapper.java:206)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:201)
at org.eclipse.equinox.internal.event.EventHandlerTracker.dispatchEvent(EventHandlerTracker.java:1)
at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:151)
at org.eclipse.equinox.internal.event.EventAdminImpl.dispatchEvent(EventAdminImpl.java:132)
at org.eclipse.equinox.internal.event.EventAdminImpl.sendEvent(EventAdminImpl.java:73)
at org.eclipse.equinox.internal.event.EventComponent.sendEvent(EventComponent.java:48)
at org.eclipse.e4.ui.services.internal.events.EventBroker.send(EventBroker.java:55)
at org.eclipse.e4.ui.internal.workbench.UIEventPublisher.notifyChanged(UIEventPublisher.java:61)
at org.eclipse.emf.common.notify.impl.BasicNotifierImpl.eNotify(BasicNotifierImpl.java:424)
at org.eclipse.e4.ui.model.application.ui.impl.UIElementImpl.setVisible(UIElementImpl.java:365)
at org.eclipse.e4.ui.workbench.renderers.swt.ContributionRecord.updateVisibility(ContributionRecord.java:110)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:169)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRendererFilter.updateElementVisibility(MenuManagerRendererFilter.java:179)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.showMenu(MenuManagerShowProcessor.java:243)
at org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerShowProcessor.menuAboutToHide(MenuManagerShowProcessor.java:111)
at org.eclipse.jface.internal.MenuManagerEventHelper.showEventPostHelper(MenuManagerEventHelper.java:89)
at org.eclipse.jface.action.MenuManager.handleAboutToShow(MenuManager.java:467)
at org.eclipse.jface.action.MenuManager$2.menuShown(MenuManager.java:493)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:297)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:91)
at org.eclipse.swt.widgets.Display.sendEvent(Display.java:5845)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1656)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1682)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1661)
at org.eclipse.swt.widgets.Menu._setVisible(Menu.java:290)
at org.eclipse.swt.widgets.Display.runPopups(Display.java:5102)
at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:4488)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine$5.run(PartRenderingEngine.java:1160)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.e4.ui.internal.workbench.swt.PartRenderingEngine.run(PartRenderingEngine.java:1051)
at org.eclipse.e4.ui.internal.workbench.E4Workbench.createAndRunUI(E4Workbench.java:153)
at org.eclipse.ui.internal.Workbench.lambda$3(Workbench.java:684)
at org.eclipse.core.databinding.observable.Realm.runWithDefault(Realm.java:339)
at org.eclipse.ui.internal.Workbench.createAndRunWorkbench(Workbench.java:583)
at org.eclipse.ui.PlatformUI.createAndRunWorkbench(PlatformUI.java:173)
at org.eclipse.ui.internal.ide.application.IDEApplication.start(IDEApplication.java:185)
at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:219)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:149)
at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:115)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:467)
at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:298)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:615)
at org.eclipse.equinox.launcher.Main.basicRun(Main.java:563)
at org.eclipse.equinox.launcher.Main.run(Main.java:1415)
at org.eclipse.equinox.launcher.Main.main(Main.java:1387)
Caused by: java.io.FileNotFoundException: C:\Program Files\Git\mingw64\share\git\git-for-windows.ico (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:106)
at org.eclipse.swt.graphics.ImageLoader.loadByZoom(ImageLoader.java:204)
... 68 more
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 14:51:12.457
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:37:34.279
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:37:39.025
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:37:48.762
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:38:19.558
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:38:43.890
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:38:51.083
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!ENTRY org.eclipse.jface 2 0 2026-03-20 15:42:01.796
!MESSAGE Keybinding conflicts occurred. They may interfere with normal accelerator operation.
!SUBENTRY 1 org.eclipse.jface 2 0 2026-03-20 15:42:01.796
!MESSAGE A conflict occurred for CTRL+SHIFT+T:
Binding(CTRL+SHIFT+T,
ParameterizedCommand(Command(org.eclipse.jdt.ui.navigate.open.type,Open Type,
Open a type in a Java editor,
Category(org.eclipse.ui.category.navigate,Navigate,null,true),
WorkbenchHandlerServiceHandler("org.eclipse.jdt.ui.navigate.open.type"),
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.ui.contexts.window,,,system)
Binding(CTRL+SHIFT+T,
ParameterizedCommand(Command(org.eclipse.lsp4e.symbolInWorkspace,Go to Symbol in Workspace,
,
Category(org.eclipse.lsp4e.category,Language Servers,null,true),
WorkbenchHandlerServiceHandler("org.eclipse.lsp4e.symbolInWorkspace"),
,,true),null),
org.eclipse.ui.defaultAcceleratorConfiguration,
org.eclipse.ui.contexts.window,,,system)
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-20 15:47:03.456
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS
!ENTRY org.eclipse.jdt.core 4 4 2026-03-20 15:47:03.843
!MESSAGE Failed to save JDT index: Index for /xxxthegame
!STACK 0
java.io.FileNotFoundException: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.jdt.core/9341915.index (Datei oder Verzeichnis nicht gefunden)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:152)
at org.eclipse.jdt.internal.core.index.FileIndexLocation.getInputStream(FileIndexLocation.java:83)
at org.eclipse.jdt.internal.core.index.DiskIndex.readAllDocumentNames(DiskIndex.java:633)
at org.eclipse.jdt.internal.core.index.DiskIndex.mergeWith(DiskIndex.java:536)
at org.eclipse.jdt.internal.core.index.Index.save(Index.java:229)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndex(IndexManager.java:1135)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.saveIndexes(IndexManager.java:1178)
at org.eclipse.jdt.internal.core.search.indexing.IndexManager.notifyIdle(IndexManager.java:822)
at org.eclipse.jdt.internal.core.search.processing.JobManager.indexerLoop(JobManager.java:508)
at java.base/java.lang.Thread.run(Thread.java:1583)
!SESSION 2026-03-20 15:47:04.884 -----------------------------------------------
eclipse.buildId=4.39.0.20260305-0817
java.version=21.0.10
java.vendor=Eclipse Adoptium
BootLoader constants: OS=linux, ARCH=x86_64, WS=gtk, NL=de_DE
Framework arguments: -product org.eclipse.epp.package.java.product
Command-line arguments: -os linux -ws gtk -arch x86_64 -product org.eclipse.epp.package.java.product -data file:/home/mario/Workspaces/xxx-thegame/
!ENTRY ch.qos.logback.classic 1 0 2026-03-20 15:47:05.724
!MESSAGE Activated before the state location was initialized. Retry after the state location is initialized.
!ENTRY ch.qos.logback.classic 1 0 2026-03-20 15:47:06.276
!MESSAGE Logback config file: /home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
!ENTRY org.eclipse.ui 2 0 2026-03-20 15:47:06.437
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-20 15:47:06.437
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.eclipse.ui 2 0 2026-03-20 15:47:06.565
!MESSAGE Warnings while parsing the commands from the 'org.eclipse.ui.commands' and 'org.eclipse.ui.actionDefinitions' extension points.
!SUBENTRY 1 org.eclipse.ui 2 0 2026-03-20 15:47:06.565
!MESSAGE Commands should really have a category: plug-in='org.springframework.tooling.boot.ls', id='spring.initializr.addStarters', categoryId='org.eclipse.lsp4e.commandCategory'
!ENTRY org.springframework.tooling.boot.ls 1 0 2026-03-20 16:01:51.334
!MESSAGE DelegatingStreamConnectionProvider - Stopping Boot LS

View File

@@ -1,7 +1,7 @@
[ { [ {
"version" : "9.4.1-20260319034812+0000", "version" : "9.5.0-20260320031124+0000",
"buildTime" : "20260319034812+0000", "buildTime" : "20260320031124+0000",
"commitId" : "2d6327017519d23b96af35865dc997fcb544fb40", "commitId" : "97faa73152fb6d4ea37edf6b3f7590dcbce8b952",
"current" : false, "current" : false,
"snapshot" : true, "snapshot" : true,
"nightly" : false, "nightly" : false,
@@ -10,15 +10,15 @@
"rcFor" : "", "rcFor" : "",
"milestoneFor" : "", "milestoneFor" : "",
"broken" : false, "broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260319034812+0000-bin.zip", "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260320031124+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260319034812+0000-bin.zip.sha256", "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260320031124+0000-bin.zip.sha256",
"checksum" : "a1f30c1e81a9e33725a213f158d5044dd305f438e539983f683f58b5860ab65e", "checksum" : "d28982b60bd15c7f3e13032152fc384f30465713b9c439bd3e159ad758461393",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.4.1-20260319034812+0000-wrapper.jar.sha256", "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260320031124+0000-wrapper.jar.sha256",
"wrapperChecksum" : "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c" "wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
}, { }, {
"version" : "9.5.0-20260319005705+0000", "version" : "9.6.0-20260319194115+0000",
"buildTime" : "20260319005705+0000", "buildTime" : "20260319194115+0000",
"commitId" : "312894732cc8829c4f69bd292c9b259a1f5bfd8f", "commitId" : "eaac62111b6cbb05984176b52e4be56d5249ebf8",
"current" : false, "current" : false,
"snapshot" : true, "snapshot" : true,
"nightly" : true, "nightly" : true,
@@ -27,11 +27,28 @@
"rcFor" : "", "rcFor" : "",
"milestoneFor" : "", "milestoneFor" : "",
"broken" : false, "broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260319005705+0000-bin.zip", "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260319194115+0000-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260319005705+0000-bin.zip.sha256", "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260319194115+0000-bin.zip.sha256",
"checksum" : "9c1f5565f97acfcbfd7b6e2a0be3eb65f366f8d522b0c82f82839d08fd8d3aaf", "checksum" : "a72c6e0f1a5ecc7d81768c65a5bdcd8f0af37ba5b05c83df4c45d08b8ce79fce",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.0-20260319005705+0000-wrapper.jar.sha256", "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260319194115+0000-wrapper.jar.sha256",
"wrapperChecksum" : "7ef3d73bd95c047814d76ec8324f72deefb96593eb9ce87aa06ecdcdaba7ffe8" "wrapperChecksum" : "f307680272dffdb8e636f1169adfbf693513005c80aa06e8d381f20390a06e6a"
}, {
"version" : "9.4.1",
"buildTime" : "20260319084628+0000",
"commitId" : "2d6327017519d23b96af35865dc997fcb544fb40",
"current" : true,
"snapshot" : false,
"nightly" : false,
"releaseNightly" : false,
"activeRc" : false,
"rcFor" : "",
"milestoneFor" : "",
"broken" : false,
"downloadUrl" : "https://services.gradle.org/distributions/gradle-9.4.1-bin.zip",
"checksumUrl" : "https://services.gradle.org/distributions/gradle-9.4.1-bin.zip.sha256",
"checksum" : "2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb",
"wrapperChecksumUrl" : "https://services.gradle.org/distributions/gradle-9.4.1-wrapper.jar.sha256",
"wrapperChecksum" : "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"
}, { }, {
"version" : "9.5.0-milestone-7", "version" : "9.5.0-milestone-7",
"buildTime" : "20260315084051+0000", "buildTime" : "20260315084051+0000",
@@ -53,7 +70,7 @@
"version" : "9.4.0", "version" : "9.4.0",
"buildTime" : "20260304103600+0000", "buildTime" : "20260304103600+0000",
"commitId" : "b631911858264c0b6e4d6603d677ff5218766cee", "commitId" : "b631911858264c0b6e4d6603d677ff5218766cee",
"current" : true, "current" : false,
"snapshot" : false, "snapshot" : false,
"nightly" : false, "nightly" : false,
"releaseNightly" : false, "releaseNightly" : false,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,13 +4,14 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
176453541.index 176453541.index
677104696.index 677104696.index
341080888.index 341080888.index
774576701.index
4134502745.index 4134502745.index
774576701.index
41199409.index 41199409.index
2217896880.index 2217896880.index
134995224.index 134995224.index
4025319337.index 4025319337.index
900586112.index 900586112.index
9341915.index
2929476459.index 2929476459.index
2065500052.index 2065500052.index
3051047092.index 3051047092.index
@@ -27,8 +28,8 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
2032345814.index 2032345814.index
3839581777.index 3839581777.index
2466743981.index 2466743981.index
13999064.index
673436610.index 673436610.index
13999064.index
3972616808.index 3972616808.index
1914043487.index 1914043487.index
3154281632.index 3154281632.index
@@ -44,10 +45,10 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
4020783879.index 4020783879.index
2900482015.index 2900482015.index
3059431983.index 3059431983.index
833027591.index
13156219.index 13156219.index
37241354.index 833027591.index
4088356365.index 4088356365.index
37241354.index
1295630681.index 1295630681.index
2701419231.index 2701419231.index
3939420913.index 3939420913.index
@@ -55,35 +56,35 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
1318022262.index 1318022262.index
773718761.index 773718761.index
2311226047.index 2311226047.index
3539841425.index
1865797976.index 1865797976.index
3539841425.index
2455962971.index 2455962971.index
836138551.index 2576972120.index
2389383899.index 2389383899.index
2226615777.index 2226615777.index
3515611559.index 3515611559.index
3728851734.index
2826242951.index
2899155238.index 2899155238.index
3763224039.index 836138551.index
2138052223.index 2138052223.index
3763224039.index
3728851734.index
2236377038.index 2236377038.index
3547251881.index 3547251881.index
371677185.index
2127778675.index
2519831052.index
1063231598.index
2874180664.index
2939623059.index
2576972120.index
2376429633.index
2628068441.index
1090991043.index
1138623861.index 1138623861.index
2376429633.index
2519831052.index
371677185.index
2874180664.index
1090991043.index
2826242951.index
2127778675.index
2628068441.index
1063231598.index
2939623059.index
1223891870.index 1223891870.index
3769604005.index 3769604005.index
3158780236.index
2237645717.index 2237645717.index
3158780236.index
2852275968.index 2852275968.index
2403041570.index 2403041570.index
1704193220.index 1704193220.index
@@ -95,12 +96,12 @@ INDEX VERSION 1.134+/home/mario/Workspaces/xxx-thegame/.metadata/.plugins/org.ec
352173590.index 352173590.index
766439048.index 766439048.index
3424266581.index 3424266581.index
2247053514.index
1765772496.index 1765772496.index
3514351073.index
3892622621.index
2494834982.index 2494834982.index
1780956574.index 3514351073.index
1022297761.index 2247053514.index
1938594271.index 1938594271.index
3892622621.index
1022297761.index
1780956574.index
1256436118.index 1256436118.index

View File

@@ -2,4 +2,6 @@
<typeInfoHistroy> <typeInfoHistroy>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben.controller{AboController.java[AboController" modifiers="1" timestamp="1773400404000"/> <typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.aufgaben.controller{AboController.java[AboController" modifiers="1" timestamp="1773400404000"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.history{GameHistoryEntity.java[GameHistoryEntity" modifiers="1" timestamp="1773860770365"/> <typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.history{GameHistoryEntity.java[GameHistoryEntity" modifiers="1" timestamp="1773860770365"/>
<typeInfo handle="=xxxthegame/src\/main\/java=/gradle_scope=/main=/=/gradle_used_by_scope=/main,test=/&lt;de.oaa.xxx.games.bdsm.controller{BdsmGameController.java[BdsmGameController" modifiers="1" timestamp="1774017499554"/>
<typeInfo handle="=xxxthegame/\/usr\/lib\/jvm\/java-21-openjdk-amd64\/lib\/jrt-fs.jar`java.base=/javadoc_location=/https:\/\/docs.oracle.com\/en\/java\/javase\/21\/docs\/api\/=/&lt;java.util(UUID.class[UUID" modifiers="49" timestamp="1769125611000"/>
</typeInfoHistroy> </typeInfoHistroy>

View File

@@ -27,3 +27,7 @@
2026-03-19 18:15:12,369 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-19 18:15:12,369 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-19 21:54:37,068 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-19 21:54:37,068 [Worker-7: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-19 23:00:56,635 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read. 2026-03-19 23:00:56,635 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-20 07:44:45,034 [Worker-2: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is out-of-date. Trying to update.
2026-03-20 10:34:17,517 [Worker-5: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-20 12:08:01,037 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.
2026-03-20 15:47:08,644 [Worker-1: Loading available Gradle versions] INFO o.e.b.c.i.u.g.PublishedGradleVersions - Gradle version information cache is up-to-date. Trying to read.

View File

@@ -1,3 +1,3 @@
#Thu Mar 19 23:00:53 CET 2026 #Fri Mar 20 15:47:06 CET 2026
org.eclipse.core.runtime=2 org.eclipse.core.runtime=2
org.eclipse.platform=4.39.0.v20260226-0420 org.eclipse.platform=4.39.0.v20260226-0420

BIN
bilder/logo_dating.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

View File

@@ -0,0 +1,190 @@
package de.oaa.xxx.aufgaben;
import de.oaa.xxx.aufgaben.entity.AufgabeEntity;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.FinisherEntity;
import de.oaa.xxx.aufgaben.entity.SperreEntity;
import de.oaa.xxx.aufgaben.entity.StrafeEntity;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.FinisherRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Service für komplexe AufgabenGruppen-Operationen.
* Kapselt die Kopier-Logik (Systemgruppe → eigene Gruppe) inkl. Toy-Mapping.
*/
@Service
public class AufgabenGruppeService {
private static final Logger LOGGER = LoggerFactory.getLogger(AufgabenGruppeService.class);
private final AufgabenGruppeRepository gruppeRepository;
private final AufgabeRepository aufgabeRepository;
private final StrafeRepository strafeRepository;
private final SperreRepository sperreRepository;
private final FinisherRepository finisherRepository;
private final ToyRepository toyRepository;
public AufgabenGruppeService(AufgabenGruppeRepository gruppeRepository,
AufgabeRepository aufgabeRepository,
StrafeRepository strafeRepository,
SperreRepository sperreRepository,
FinisherRepository finisherRepository,
ToyRepository toyRepository) {
this.gruppeRepository = gruppeRepository;
this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository;
this.sperreRepository = sperreRepository;
this.finisherRepository = finisherRepository;
this.toyRepository = toyRepository;
}
/**
* Kopiert eine öffentliche (System-)Gruppe in die eigene Sammlung des Users.
*
* @param sourceId UUID der Quellgruppe
* @param userId UUID des Ziel-Users
* @return UUID der neu erstellten Gruppe
* @throws IllegalStateException wenn das Gruppen-Limit (10) erreicht ist
* @throws IllegalArgumentException wenn die Quellgruppe nicht gefunden oder nicht kopierbar ist
*/
@Transactional
public UUID copyGruppe(UUID sourceId, UUID userId) {
if (gruppeRepository.countByUserId(userId) >= 10) {
throw new IllegalStateException("Gruppen-Limit erreicht");
}
AufgabenGruppeEntity source = gruppeRepository.findById(sourceId)
.orElseThrow(() -> new IllegalArgumentException("Gruppe nicht gefunden: " + sourceId));
if (source.isPrivateGruppe()) {
throw new IllegalArgumentException("Privat-Gruppen können nicht kopiert werden");
}
if (userId.equals(source.getUserId())) {
throw new IllegalArgumentException("Eigene Gruppen können nicht kopiert werden");
}
// Toy-Mapping aufbauen: Source-ToyId → Ziel-ToyEntity
Set<ToyEntity> allSourceToys = new HashSet<>();
source.getAufgaben().forEach(a -> { if (a.getBenoetigteToys() != null) allSourceToys.addAll(a.getBenoetigteToys()); });
source.getStrafen().forEach(s -> { if (s.getBenoetigteToys() != null) allSourceToys.addAll(s.getBenoetigteToys()); });
source.getSperren().forEach(sp -> { if (sp.getBenoetigteToys() != null) allSourceToys.addAll(sp.getBenoetigteToys()); });
source.getFinisher().forEach(f -> { if (f.getBenoetigteToys() != null) allSourceToys.addAll(f.getBenoetigteToys()); });
Map<UUID, ToyEntity> toyMapping = new HashMap<>();
for (ToyEntity sourceToy : allSourceToys) {
if (sourceToy.getUserId() == null) {
// System-Toy: direkt referenzieren
toyMapping.put(sourceToy.getToyId(), sourceToy);
} else {
// User-Toy: gleichnamiges Toy suchen oder kopieren
ToyEntity mapped = toyRepository.findByNameIgnoreCaseAndUserId(sourceToy.getName(), userId)
.orElseGet(() -> {
ToyEntity tc = new ToyEntity();
tc.setToyId(UUID.randomUUID());
tc.setName(sourceToy.getName());
tc.setBeschreibung(sourceToy.getBeschreibung());
tc.setBild(sourceToy.getBild());
tc.setUserId(userId);
return toyRepository.save(tc);
});
toyMapping.put(sourceToy.getToyId(), mapped);
}
}
// Neue Gruppe anlegen
AufgabenGruppeEntity copy = new AufgabenGruppeEntity();
copy.setGruppenId(UUID.randomUUID());
copy.setName(source.getName());
copy.setBeschreibung(source.getBeschreibung());
copy.setVon(source.getVon());
copy.setBild(source.getBild());
copy.setUserId(userId);
copy.setPrivateGruppe(true);
gruppeRepository.save(copy);
// Aufgaben kopieren
for (AufgabeEntity a : source.getAufgaben()) {
AufgabeEntity ac = new AufgabeEntity();
ac.setAufgabeId(UUID.randomUUID());
ac.setAufgabenGruppe(copy);
ac.setKurzText(a.getKurzText());
ac.setText(a.getText());
ac.setLevel(a.getLevel());
ac.setSekundenVon(a.getSekundenVon());
ac.setSekundenBis(a.getSekundenBis());
ac.setBenoetigtAktiv(a.getBenoetigtAktiv() != null ? new ArrayList<>(a.getBenoetigtAktiv()) : null);
ac.setBenoetigtPassiv(a.getBenoetigtPassiv() != null ? new ArrayList<>(a.getBenoetigtPassiv()) : null);
ac.setBenoetigteToys(mapToys(a.getBenoetigteToys(), toyMapping));
aufgabeRepository.save(ac);
}
// Strafen kopieren
for (StrafeEntity s : source.getStrafen()) {
StrafeEntity sc = new StrafeEntity();
sc.setStrafeId(UUID.randomUUID());
sc.setAufgabenGruppe(copy);
sc.setKurzText(s.getKurzText());
sc.setText(s.getText());
sc.setLevel(s.getLevel());
sc.setSekundenVon(s.getSekundenVon());
sc.setSekundenBis(s.getSekundenBis());
sc.setBenoetigtAktiv(s.getBenoetigtAktiv() != null ? new ArrayList<>(s.getBenoetigtAktiv()) : null);
sc.setBenoetigtPassiv(s.getBenoetigtPassiv() != null ? new ArrayList<>(s.getBenoetigtPassiv()) : null);
sc.setBenoetigteToys(mapToys(s.getBenoetigteToys(), toyMapping));
strafeRepository.save(sc);
}
// Sperren kopieren
for (SperreEntity sp : source.getSperren()) {
SperreEntity spc = new SperreEntity();
spc.setSperreId(UUID.randomUUID());
spc.setAufgabenGruppe(copy);
spc.setKurzText(sp.getKurzText());
spc.setText(sp.getText());
spc.setReleaseText(sp.getReleaseText());
spc.setMinutenVon(sp.getMinutenVon());
spc.setMinutenBis(sp.getMinutenBis());
spc.setSperreFuer(sp.getSperreFuer() != null ? new ArrayList<>(sp.getSperreFuer()) : null);
spc.setBenoetigteToys(mapToys(sp.getBenoetigteToys(), toyMapping));
sperreRepository.save(spc);
}
// Finisher kopieren
for (FinisherEntity f : source.getFinisher()) {
FinisherEntity fc = new FinisherEntity();
fc.setFinisherId(UUID.randomUUID());
fc.setAufgabenGruppe(copy);
fc.setKurzText(f.getKurzText());
fc.setText(f.getText());
fc.setGeschlecht(f.getGeschlecht());
fc.setBenoetigtAktiv(f.getBenoetigtAktiv() != null ? new ArrayList<>(f.getBenoetigtAktiv()) : null);
fc.setBenoetigtPassiv(f.getBenoetigtPassiv() != null ? new ArrayList<>(f.getBenoetigtPassiv()) : null);
fc.setBenoetigteToys(mapToys(f.getBenoetigteToys(), toyMapping));
finisherRepository.save(fc);
}
LOGGER.info("User {} hat AufgabenGruppe {} kopiert (Quelle: {})", userId, copy.getGruppenId(), sourceId);
return copy.getGruppenId();
}
private List<ToyEntity> mapToys(List<ToyEntity> source, Map<UUID, ToyEntity> mapping) {
if (source == null || source.isEmpty()) return new ArrayList<>();
return source.stream().map(t -> mapping.getOrDefault(t.getToyId(), t)).toList();
}
}

View File

@@ -1,23 +1,9 @@
package de.oaa.xxx.aufgaben.controller; package de.oaa.xxx.aufgaben.controller;
import de.oaa.xxx.aufgaben.AufgabenGruppe; import java.security.Principal;
import de.oaa.xxx.aufgaben.AufgabenGruppeList; import java.util.Base64;
import de.oaa.xxx.aufgaben.AufgabenGruppePage; import java.util.UUID;
import de.oaa.xxx.aufgaben.entity.AufgabeEntity;
import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import de.oaa.xxx.aufgaben.entity.FinisherEntity;
import de.oaa.xxx.aufgaben.entity.SperreEntity;
import de.oaa.xxx.aufgaben.entity.StrafeEntity;
import de.oaa.xxx.aufgaben.entity.ToyEntity;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.FinisherRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -37,15 +23,19 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal; import de.oaa.xxx.aufgaben.AufgabenGruppe;
import java.util.ArrayList; import de.oaa.xxx.aufgaben.AufgabenGruppeList;
import java.util.Base64; import de.oaa.xxx.aufgaben.AufgabenGruppePage;
import java.util.HashMap; import de.oaa.xxx.aufgaben.AufgabenGruppeService;
import java.util.HashSet; import de.oaa.xxx.aufgaben.entity.AufgabenGruppeEntity;
import java.util.List; import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import java.util.Map; import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import java.util.Set; import de.oaa.xxx.aufgaben.repository.FinisherRepository;
import java.util.UUID; import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.user.UserEntity;
import de.oaa.xxx.user.UserRepository;
@RestController @RestController
@RequestMapping("/gruppe") @RequestMapping("/gruppe")
@@ -62,7 +52,7 @@ public class AufgabenGruppeController {
private final FinisherRepository finisherRepository; private final FinisherRepository finisherRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final GruppenAboRepository aboRepository; private final GruppenAboRepository aboRepository;
private final ToyRepository toyRepository; private final AufgabenGruppeService aufgabenGruppeService;
public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository, public AufgabenGruppeController(AufgabenGruppeRepository gruppeRepository,
AufgabeRepository aufgabeRepository, AufgabeRepository aufgabeRepository,
@@ -71,7 +61,7 @@ public class AufgabenGruppeController {
FinisherRepository finisherRepository, FinisherRepository finisherRepository,
UserRepository userRepository, UserRepository userRepository,
GruppenAboRepository aboRepository, GruppenAboRepository aboRepository,
ToyRepository toyRepository) { AufgabenGruppeService aufgabenGruppeService) {
this.gruppeRepository = gruppeRepository; this.gruppeRepository = gruppeRepository;
this.aufgabeRepository = aufgabeRepository; this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository; this.strafeRepository = strafeRepository;
@@ -79,7 +69,7 @@ public class AufgabenGruppeController {
this.finisherRepository = finisherRepository; this.finisherRepository = finisherRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.aboRepository = aboRepository; this.aboRepository = aboRepository;
this.toyRepository = toyRepository; this.aufgabenGruppeService = aufgabenGruppeService;
} }
// ── Paginierte Listen ── // ── Paginierte Listen ──
@@ -190,118 +180,16 @@ public class AufgabenGruppeController {
public ResponseEntity<Void> copy(@PathVariable UUID gruppeId, Principal principal) { public ResponseEntity<Void> copy(@PathVariable UUID gruppeId, Principal principal) {
UserEntity user = resolveUser(principal); UserEntity user = resolveUser(principal);
if (user == null) return ResponseEntity.status(401).build(); if (user == null) return ResponseEntity.status(401).build();
try {
if (gruppeRepository.countByUserId(user.getUserId()) >= 10) { aufgabenGruppeService.copyGruppe(gruppeId, user.getUserId());
return ResponseEntity.status(409).build();
}
AufgabenGruppeEntity source = gruppeRepository.findById(gruppeId).orElse(null);
if (source == null) return ResponseEntity.notFound().build();
if (source.isPrivateGruppe()) return ResponseEntity.status(403).build();
if (user.getUserId().equals(source.getUserId())) return ResponseEntity.status(403).build();
// Build toy mapping: source toyId → toy entity the copy will reference
Set<ToyEntity> allSourceToys = new HashSet<>();
source.getAufgaben().forEach(a -> { if (a.getBenoetigteToys() != null) allSourceToys.addAll(a.getBenoetigteToys()); });
source.getStrafen().forEach(s -> { if (s.getBenoetigteToys() != null) allSourceToys.addAll(s.getBenoetigteToys()); });
source.getSperren().forEach(sp -> { if (sp.getBenoetigteToys() != null) allSourceToys.addAll(sp.getBenoetigteToys()); });
source.getFinisher().forEach(f -> { if (f.getBenoetigteToys() != null) allSourceToys.addAll(f.getBenoetigteToys()); });
Map<UUID, ToyEntity> toyMapping = new HashMap<>();
for (ToyEntity sourceToy : allSourceToys) {
if (sourceToy.getUserId() == null) {
// System toy reference directly
toyMapping.put(sourceToy.getToyId(), sourceToy);
} else {
// User toy find existing toy with same name in user's collection, or create a copy
ToyEntity mapped = toyRepository.findByNameIgnoreCaseAndUserId(sourceToy.getName(), user.getUserId())
.orElseGet(() -> {
ToyEntity tc = new ToyEntity();
tc.setToyId(UUID.randomUUID());
tc.setName(sourceToy.getName());
tc.setBeschreibung(sourceToy.getBeschreibung());
tc.setBild(sourceToy.getBild());
tc.setUserId(user.getUserId());
return toyRepository.save(tc);
});
toyMapping.put(sourceToy.getToyId(), mapped);
}
}
AufgabenGruppeEntity copy = new AufgabenGruppeEntity();
copy.setGruppenId(UUID.randomUUID());
copy.setName(source.getName());
copy.setBeschreibung(source.getBeschreibung());
copy.setVon(source.getVon());
copy.setBild(source.getBild());
copy.setUserId(user.getUserId());
copy.setPrivateGruppe(true);
gruppeRepository.save(copy);
for (AufgabeEntity a : source.getAufgaben()) {
AufgabeEntity ac = new AufgabeEntity();
ac.setAufgabeId(UUID.randomUUID());
ac.setAufgabenGruppe(copy);
ac.setKurzText(a.getKurzText());
ac.setText(a.getText());
ac.setLevel(a.getLevel());
ac.setSekundenVon(a.getSekundenVon());
ac.setSekundenBis(a.getSekundenBis());
ac.setBenoetigtAktiv(a.getBenoetigtAktiv() != null ? new ArrayList<>(a.getBenoetigtAktiv()) : null);
ac.setBenoetigtPassiv(a.getBenoetigtPassiv() != null ? new ArrayList<>(a.getBenoetigtPassiv()) : null);
ac.setBenoetigteToys(mapToys(a.getBenoetigteToys(), toyMapping));
aufgabeRepository.save(ac);
}
for (StrafeEntity s : source.getStrafen()) {
StrafeEntity sc = new StrafeEntity();
sc.setStrafeId(UUID.randomUUID());
sc.setAufgabenGruppe(copy);
sc.setKurzText(s.getKurzText());
sc.setText(s.getText());
sc.setLevel(s.getLevel());
sc.setSekundenVon(s.getSekundenVon());
sc.setSekundenBis(s.getSekundenBis());
sc.setBenoetigtAktiv(s.getBenoetigtAktiv() != null ? new ArrayList<>(s.getBenoetigtAktiv()) : null);
sc.setBenoetigtPassiv(s.getBenoetigtPassiv() != null ? new ArrayList<>(s.getBenoetigtPassiv()) : null);
sc.setBenoetigteToys(mapToys(s.getBenoetigteToys(), toyMapping));
strafeRepository.save(sc);
}
for (SperreEntity sp : source.getSperren()) {
SperreEntity spc = new SperreEntity();
spc.setSperreId(UUID.randomUUID());
spc.setAufgabenGruppe(copy);
spc.setKurzText(sp.getKurzText());
spc.setText(sp.getText());
spc.setReleaseText(sp.getReleaseText());
spc.setMinutenVon(sp.getMinutenVon());
spc.setMinutenBis(sp.getMinutenBis());
spc.setSperreFuer(sp.getSperreFuer() != null ? new ArrayList<>(sp.getSperreFuer()) : null);
spc.setBenoetigteToys(mapToys(sp.getBenoetigteToys(), toyMapping));
sperreRepository.save(spc);
}
for (FinisherEntity f : source.getFinisher()) {
FinisherEntity fc = new FinisherEntity();
fc.setFinisherId(UUID.randomUUID());
fc.setAufgabenGruppe(copy);
fc.setKurzText(f.getKurzText());
fc.setText(f.getText());
fc.setGeschlecht(f.getGeschlecht());
fc.setBenoetigtAktiv(f.getBenoetigtAktiv() != null ? new ArrayList<>(f.getBenoetigtAktiv()) : null);
fc.setBenoetigtPassiv(f.getBenoetigtPassiv() != null ? new ArrayList<>(f.getBenoetigtPassiv()) : null);
fc.setBenoetigteToys(mapToys(f.getBenoetigteToys(), toyMapping));
finisherRepository.save(fc);
}
LOGGER.info("User {} hat AufgabenGruppe {} kopiert (Quelle: {})", user.getUserId(), copy.getGruppenId(), gruppeId);
return ResponseEntity.status(201).build(); return ResponseEntity.status(201).build();
} catch (IllegalStateException e) {
return ResponseEntity.status(409).build();
} catch (IllegalArgumentException e) {
String msg = e.getMessage();
if (msg != null && msg.contains("nicht gefunden")) return ResponseEntity.notFound().build();
return ResponseEntity.status(403).build();
} }
private List<ToyEntity> mapToys(List<ToyEntity> source, Map<UUID, ToyEntity> mapping) {
if (source == null || source.isEmpty()) return new ArrayList<>();
return source.stream().map(t -> mapping.getOrDefault(t.getToyId(), t)).toList();
} }
// ── Löschen ── // ── Löschen ──

View File

@@ -8,6 +8,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -49,6 +51,9 @@ public class SecurityConfig {
.requestMatchers("/sessionbdsmtasks.html").authenticated() .requestMatchers("/sessionbdsmtasks.html").authenticated()
.requestMatchers("/sessionbdsmtoys.html").authenticated() .requestMatchers("/sessionbdsmtoys.html").authenticated()
.requestMatchers("/sessionbdsmingame.html").authenticated() .requestMatchers("/sessionbdsmingame.html").authenticated()
.requestMatchers("/neubdsm.html").authenticated()
.requestMatchers("/bdsmingame.html").authenticated()
.requestMatchers("/bdsmwarten.html").authenticated()
.requestMatchers("/personen-suchen.html").authenticated() .requestMatchers("/personen-suchen.html").authenticated()
.requestMatchers("/freunde.html").authenticated() .requestMatchers("/freunde.html").authenticated()
.requestMatchers("/nachrichten.html").authenticated() .requestMatchers("/nachrichten.html").authenticated()
@@ -79,6 +84,7 @@ public class SecurityConfig {
.requestMatchers("/*.svg").permitAll() .requestMatchers("/*.svg").permitAll()
.requestMatchers("/*.webp").permitAll() .requestMatchers("/*.webp").permitAll()
.requestMatchers(HttpMethod.GET, "/login").permitAll() .requestMatchers(HttpMethod.GET, "/login").permitAll()
.requestMatchers(HttpMethod.POST, "/login").permitAll()
.requestMatchers(HttpMethod.GET, "/login/publickey").permitAll() .requestMatchers(HttpMethod.GET, "/login/publickey").permitAll()
.requestMatchers(HttpMethod.GET, "/login/logout").permitAll() .requestMatchers(HttpMethod.GET, "/login/logout").permitAll()
.requestMatchers(HttpMethod.POST, "/user").permitAll() .requestMatchers(HttpMethod.POST, "/user").permitAll()
@@ -96,4 +102,9 @@ public class SecurityConfig {
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build(); return http.build();
} }
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
} }

View File

@@ -0,0 +1,208 @@
package de.oaa.xxx.games.bdsm;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
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;
/**
* Service für komplexe BDSM-Game-Operationen.
* Kapselt Spielabschluss-Logik (XP-Vergabe, History) und den BDSM→Chastity-Übergang.
*/
@Service
public class BdsmGameService {
private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameService.class);
private final BdsmGameRepository sessionRepository;
private final MitspielerRepository mitspielerRepository;
private final AktiveSperreRepository aktiveSperreRepository;
private final UserRepository userRepository;
private final GameHistoryRepository gameHistoryRepository;
private final CardlockRepository cardlockRepository;
private final SystemMessageService systemMessageService;
public BdsmGameService(BdsmGameRepository sessionRepository,
MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository,
UserRepository userRepository,
GameHistoryRepository gameHistoryRepository,
CardlockRepository cardlockRepository,
SystemMessageService systemMessageService) {
this.sessionRepository = sessionRepository;
this.mitspielerRepository = mitspielerRepository;
this.aktiveSperreRepository = aktiveSperreRepository;
this.userRepository = userRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.cardlockRepository = cardlockRepository;
this.systemMessageService = systemMessageService;
}
/**
* Beendet eine BDSM-Session ordentlich: History speichern, XP vergeben,
* Gäste auf eigenem Gerät benachrichtigen, Daten aufräumen.
*/
@Transactional
public void spielAbschliessen(BdsmGameEntity entity) {
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);
}));
// Gäste auf eigenem Gerät benachrichtigen
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));
bereinige(entity);
}
/**
* Überführt eine BDSM-Session in ein neues Chastity-Lock (BDSM→Chastity-Transition).
* History + XP werden wie beim normalen Spielabschluss vergeben.
*
* @return Das neu angelegte CardLockEntity
* @throws IllegalArgumentException wenn Session oder Template nicht gefunden
* @throws IllegalStateException wenn Lockee bereits ein aktives Lock hat
*/
@Transactional
public CardLockEntity zuChastity(UUID sessionId, UUID templateLockId, UUID lockeeUserId, UUID keyholderUserId) {
BdsmGameEntity entity = sessionRepository.findById(sessionId)
.orElseThrow(() -> new IllegalArgumentException("Session nicht gefunden: " + sessionId));
CardLockEntity template = cardlockRepository.findById(templateLockId)
.orElseThrow(() -> new IllegalArgumentException("Template-Lock nicht gefunden: " + templateLockId));
if (lockeeUserId != null
&& cardlockRepository.existsByLockeeAndStartTimeIsNotNullAndUnlockTimeIsNull(lockeeUserId)) {
throw new IllegalStateException("Lockee hat bereits ein aktives Chastity-Lock");
}
LocalDateTime now = LocalDateTime.now();
CardLockEntity newLock = new CardLockEntity();
newLock.setName(template.getName());
newLock.setLockee(lockeeUserId);
newLock.setKeyholder(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 ArrayList<>(template.getInitialCards()) : new 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
if (lockeeUserId != null) {
userRepository.findById(keyholderUserId).ifPresent(keyholder ->
systemMessageService.send(keyholderUserId, lockeeUserId,
keyholder.getName() + " hat nach dem BDSM Game ein Chastity Lock auf dich gesetzt.",
"/activelock.html", MessageCause.GAME_STATE));
}
// Spielabschluss-Logik (History + XP + Cleanup)
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);
}));
bereinige(entity);
LOGGER.info("BDSM-Session {} in Chastity-Lock {} überführt (Lockee: {}, Keyholder: {})",
sessionId, newLock.getLockId(), lockeeUserId, keyholderUserId);
return newLock;
}
/** Löscht alle Session-Daten (Sperren, Mitspieler, Session selbst). */
private void bereinige(BdsmGameEntity entity) {
aktiveSperreRepository.deleteAll(entity.getAktiveSperren());
mitspielerRepository.deleteAll(entity.getMitspieler());
sessionRepository.delete(entity);
}
}

View File

@@ -67,6 +67,8 @@ public class BdsmEinladungController {
return ResponseEntity.status(403).build(); return ResponseEntity.status(403).build();
} }
if (req.setupId() == null) return ResponseEntity.badRequest().build();
// Prüfen ob Person bereits aktiv eingeladen oder Teil des Spiels // Prüfen ob Person bereits aktiv eingeladen oder Teil des Spiels
boolean alreadyInvited = einladungRepository.findBySetupId(req.setupId()).stream() boolean alreadyInvited = einladungRepository.findBySetupId(req.setupId()).stream()
.anyMatch(e -> req.inviteeId().equals(e.getInviteeId()) .anyMatch(e -> req.inviteeId().equals(e.getInviteeId())
@@ -113,6 +115,10 @@ public class BdsmEinladungController {
if (e == null) return ResponseEntity.notFound().build(); if (e == null) return ResponseEntity.notFound().build();
if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build(); if (!e.getInviterId().equals(userId)) return ResponseEntity.status(403).build();
e.setStatus(Status.CANCELLED); e.setStatus(Status.CANCELLED);
String inviterName = userRepository.findById(userId).map(u -> u.getName()).orElse("Jemand");
systemMessageService.send(userId, e.getInviteeId(),
inviterName + " hat die BDSM-Spieleinladung zurückgezogen.",
"/einladungen.html", MessageCause.INVITATION);
return ResponseEntity.accepted().build(); return ResponseEntity.accepted().build();
} }
@@ -168,7 +174,7 @@ public class BdsmEinladungController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getById(@PathVariable UUID id, Principal principal) { public ResponseEntity<Map<String, Object>> getById(@PathVariable("id") UUID id, Principal principal) {
UUID userId = currentUserId(principal); UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build(); if (userId == null) return ResponseEntity.status(401).build();
BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null); BdsmEinladungEntity e = einladungRepository.findById(id).orElse(null);

View File

@@ -1,34 +1,17 @@
package de.oaa.xxx.games.bdsm.controller; package de.oaa.xxx.games.bdsm.controller;
import com.fasterxml.jackson.databind.JsonNode; import java.security.Principal;
import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Duration;
import de.oaa.xxx.games.bdsm.AufgabeAnzeige; import java.time.LocalDateTime;
import de.oaa.xxx.games.bdsm.Mitspieler; import java.util.ArrayList;
import de.oaa.xxx.games.bdsm.GeschlechtEnum; import java.util.Collections;
import de.oaa.xxx.games.bdsm.Werkzeug; import java.util.HashSet;
import de.oaa.xxx.games.bdsm.BdsmGame; import java.util.LinkedHashMap;
import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren; import java.util.List;
import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList; import java.util.Map;
import de.oaa.xxx.games.bdsm.sperre.SperreCallback; import java.util.Set;
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback; import java.util.UUID;
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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -45,17 +28,33 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.security.Principal; import com.fasterxml.jackson.databind.JsonNode;
import java.time.Duration; import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.util.ArrayList; import de.oaa.xxx.games.bdsm.AufgabeAnzeige;
import java.util.Collections; import de.oaa.xxx.games.bdsm.BdsmGame;
import java.util.HashSet; import de.oaa.xxx.games.bdsm.BdsmGameDurchfuehren;
import java.util.LinkedHashMap; import de.oaa.xxx.games.bdsm.BdsmGameService;
import java.util.List; import de.oaa.xxx.games.bdsm.GeschlechtEnum;
import java.util.Map; import de.oaa.xxx.games.bdsm.Mitspieler;
import java.util.Set; import de.oaa.xxx.games.bdsm.Werkzeug;
import java.util.UUID; import de.oaa.xxx.games.bdsm.aufgaben.AufgabenList;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmEinladungEntity;
import de.oaa.xxx.games.bdsm.entity.BdsmGameEntity;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmEinladungRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.games.bdsm.sperre.SperreCallback;
import de.oaa.xxx.games.bdsm.sperre.SperreVerarbeiten;
import de.oaa.xxx.games.bdsm.sperre.SperrenVerlaengernCallback;
import de.oaa.xxx.games.chastity.cardlock.CardLockEntity;
import de.oaa.xxx.games.chastity.cardlock.CardlockRepository;
import de.oaa.xxx.social.SystemMessageService;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.user.UserRepository;
@RestController @RestController
@RequestMapping("/bdsm") @RequestMapping("/bdsm")
@@ -63,33 +62,36 @@ import java.util.UUID;
public class BdsmGameController { public class BdsmGameController {
private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class); private static final Logger LOGGER = LoggerFactory.getLogger(BdsmGameController.class);
/** Kurzlebiger In-Memory-Marker: Sessions die ordentlich über spielAbgeschlossen beendet wurden. */ /**
* Kurzlebiger In-Memory-Marker: Sessions die ordentlich über spielAbgeschlossen
* beendet wurden.
*/
private static final Set<UUID> ORDENTLICH_BEENDET = Collections.synchronizedSet(new HashSet<>()); private static final Set<UUID> ORDENTLICH_BEENDET = Collections.synchronizedSet(new HashSet<>());
private final BdsmGameRepository sessionRepository; private final BdsmGameRepository sessionRepository;
private final MitspielerRepository mitspielerRepository; private final MitspielerRepository mitspielerRepository;
private final AktiveSperreRepository aktiveSperreRepository; private final AktiveSperreRepository aktiveSperreRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final GameHistoryRepository gameHistoryRepository;
private final BdsmEinladungRepository einladungRepository; private final BdsmEinladungRepository einladungRepository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final SystemMessageService systemMessageService; private final SystemMessageService systemMessageService;
private final CardlockRepository cardlockRepository; private final CardlockRepository cardlockRepository;
private final BdsmGameService bdsmGameService;
public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository, public BdsmGameController(BdsmGameRepository sessionRepository, MitspielerRepository mitspielerRepository,
AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository, AktiveSperreRepository aktiveSperreRepository, UserRepository userRepository,
GameHistoryRepository gameHistoryRepository, BdsmEinladungRepository einladungRepository, BdsmEinladungRepository einladungRepository, ObjectMapper objectMapper,
ObjectMapper objectMapper, SystemMessageService systemMessageService, SystemMessageService systemMessageService, CardlockRepository cardlockRepository,
CardlockRepository cardlockRepository) { BdsmGameService bdsmGameService) {
this.sessionRepository = sessionRepository; this.sessionRepository = sessionRepository;
this.mitspielerRepository = mitspielerRepository; this.mitspielerRepository = mitspielerRepository;
this.aktiveSperreRepository = aktiveSperreRepository; this.aktiveSperreRepository = aktiveSperreRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.einladungRepository = einladungRepository; this.einladungRepository = einladungRepository;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.systemMessageService = systemMessageService; this.systemMessageService = systemMessageService;
this.cardlockRepository = cardlockRepository; this.cardlockRepository = cardlockRepository;
this.bdsmGameService = bdsmGameService;
} }
@GetMapping("/{sessionId}") @GetMapping("/{sessionId}")
@@ -159,46 +161,8 @@ public class BdsmGameController {
public ResponseEntity<Void> spielAbgeschlossen(@PathVariable UUID sessionId) { public ResponseEntity<Void> spielAbgeschlossen(@PathVariable UUID sessionId) {
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null); BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null);
if (entity == null) return ResponseEntity.notFound().build(); 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); ORDENTLICH_BEENDET.add(sessionId);
String endNachricht = "Das BDSM-Spiel wurde erfolgreich beendet. Danke fürs Mitspielen! 🎉"; bdsmGameService.spielAbschliessen(entity);
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(); return ResponseEntity.accepted().build();
} }
@@ -547,89 +511,20 @@ public class BdsmGameController {
@PostMapping("/{sessionId}/zu-chastity") @PostMapping("/{sessionId}/zu-chastity")
public ResponseEntity<Map<String, Object>> zuChastity( public ResponseEntity<Map<String, Object>> zuChastity(
@PathVariable UUID sessionId, @RequestBody ZuChastityRequest req) { @PathVariable UUID sessionId, @RequestBody ZuChastityRequest req) {
BdsmGameEntity entity = sessionRepository.findById(sessionId).orElse(null); try {
if (entity == null) return ResponseEntity.notFound().build(); CardLockEntity newLock = bdsmGameService.zuChastity(
sessionId, req.lockId(), req.lockeeUserId(), req.keyholderUserId());
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<>(); Map<String, Object> response = new LinkedHashMap<>();
response.put("lockId", newLock.getLockId().toString()); response.put("lockId", newLock.getLockId().toString());
response.put("unlockCode", newLock.getUnlockCode()); response.put("unlockCode", newLock.getUnlockCode());
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
String msg = e.getMessage();
if (msg != null && msg.contains("Session")) return ResponseEntity.notFound().build();
return ResponseEntity.badRequest().build();
} catch (IllegalStateException e) {
return ResponseEntity.status(409).build();
}
} }
/** Gibt zurück welches Werkzeug für einen User durch ein aktives Chastity-Lock blockiert ist. */ /** Gibt zurück welches Werkzeug für einen User durch ein aktives Chastity-Lock blockiert ist. */

View File

@@ -33,10 +33,15 @@ public class BdsmSetupDraftController {
} }
@GetMapping @GetMapping
public ResponseEntity<Map<String, Object>> getDraft(Principal principal) { public ResponseEntity<Map<String, Object>> getDraft(
@RequestParam(required = false) String setupId,
Principal principal) {
UUID userId = currentUserId(principal); UUID userId = currentUserId(principal);
if (userId == null) return ResponseEntity.status(401).build(); if (userId == null) return ResponseEntity.status(401).build();
return draftRepository.findByUserId(userId) var lookup = (setupId != null && !setupId.isBlank())
? draftRepository.findBySetupId(setupId)
: draftRepository.findByUserId(userId);
return lookup
.map(d -> { .map(d -> {
Map<String, Object> m = new LinkedHashMap<>(); Map<String, Object> m = new LinkedHashMap<>();
m.put("setupId", d.getSetupId()); m.put("setupId", d.getSetupId());

View File

@@ -8,4 +8,5 @@ import java.util.UUID;
public interface BdsmSetupDraftRepository extends JpaRepository<BdsmSetupDraftEntity, UUID> { public interface BdsmSetupDraftRepository extends JpaRepository<BdsmSetupDraftEntity, UUID> {
Optional<BdsmSetupDraftEntity> findByUserId(UUID userId); Optional<BdsmSetupDraftEntity> findByUserId(UUID userId);
Optional<BdsmSetupDraftEntity> findBySetupId(String setupId);
} }

View File

@@ -0,0 +1,50 @@
package de.oaa.xxx.games.chastity.cardlock;
import de.oaa.xxx.games.history.GameHistoryRepository;
import de.oaa.xxx.games.chastity.verification.VerificationRepository;
import de.oaa.xxx.games.chastity.verification.VerificationVoteRepository;
import de.oaa.xxx.user.UserRepository;
import org.springframework.stereotype.Service;
/**
* Factory für CardLockService-Instanzen.
*
* CardLockService hält pro Instanz den Zustand eines konkreten CardLockEntity
* und kann daher kein Singleton-Bean sein. Diese Factory zentralisiert die
* Erzeugung und verwaltet alle Abhängigkeiten als injizierte Singletons.
*/
@Service
public class CardLockServiceFactory {
private final VerificationRepository verificationRepository;
private final VerificationVoteRepository verificationVoteRepository;
private final CardLockRepository cardLockRepository;
private final GameHistoryRepository gameHistoryRepository;
private final UserRepository userRepository;
public CardLockServiceFactory(VerificationRepository verificationRepository,
VerificationVoteRepository verificationVoteRepository,
CardLockRepository cardLockRepository,
GameHistoryRepository gameHistoryRepository,
UserRepository userRepository) {
this.verificationRepository = verificationRepository;
this.verificationVoteRepository = verificationVoteRepository;
this.cardLockRepository = cardLockRepository;
this.gameHistoryRepository = gameHistoryRepository;
this.userRepository = userRepository;
}
/**
* Erstellt eine neue CardLockService-Instanz für das gegebene Lock.
*/
public CardLockService create(CardLockEntity lock) {
return new CardLockService(
lock,
verificationRepository,
verificationVoteRepository,
cardLockRepository,
gameHistoryRepository,
userRepository
);
}
}

View File

@@ -1,3 +1,3 @@
package de.oaa.xxx.passwordreset; package de.oaa.xxx.passwordreset;
public record PasswordResetConfirm(String token, String passwordHash) {} public record PasswordResetConfirm(String token, String password) {}

View File

@@ -8,6 +8,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.UUID; import java.util.UUID;
@@ -25,15 +26,18 @@ public class PasswordResetController {
private final UserRepository userRepository; private final UserRepository userRepository;
private final MailService mailService; private final MailService mailService;
private final MailTemplateService mailTemplateService; private final MailTemplateService mailTemplateService;
private final PasswordEncoder passwordEncoder;
public PasswordResetController(PasswordResetRepository passwordResetRepository, public PasswordResetController(PasswordResetRepository passwordResetRepository,
UserRepository userRepository, UserRepository userRepository,
MailService mailService, MailService mailService,
MailTemplateService mailTemplateService) { MailTemplateService mailTemplateService,
PasswordEncoder passwordEncoder) {
this.passwordResetRepository = passwordResetRepository; this.passwordResetRepository = passwordResetRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.mailService = mailService; this.mailService = mailService;
this.mailTemplateService = mailTemplateService; this.mailTemplateService = mailTemplateService;
this.passwordEncoder = passwordEncoder;
} }
@PostMapping("/request") @PostMapping("/request")
@@ -67,7 +71,7 @@ public class PasswordResetController {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
userRepository.findByEmail(entity.get().getEmail()).ifPresent(user -> { userRepository.findByEmail(entity.get().getEmail()).ifPresent(user -> {
user.setPassword(confirm.passwordHash()); user.setPassword(passwordEncoder.encode(confirm.password()));
userRepository.save(user); userRepository.save(user);
LOGGER.info("Passwort zurückgesetzt für: {}", entity.get().getEmail()); LOGGER.info("Passwort zurückgesetzt für: {}", entity.get().getEmail());
}); });

View File

@@ -1,7 +1,6 @@
package de.oaa.xxx.registration; package de.oaa.xxx.registration;
import java.net.URI; import java.net.URI;
import java.util.UUID;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -9,36 +8,29 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.user.UserController;
@RestController @RestController
@RequestMapping("/activation") @RequestMapping("/activation")
public class ActivationController { public class ActivationController {
private final RegistrationRepository registrationRepository; private final RegistrationService registrationService;
private final UserController userController;
public ActivationController(RegistrationRepository registrationRepository, UserController userController) { public ActivationController(RegistrationService registrationService) {
this.registrationRepository = registrationRepository; this.registrationService = registrationService;
this.userController = userController;
} }
@GetMapping("/{uuid}") @GetMapping("/{uuid}")
public ResponseEntity<Void> activate(@PathVariable String uuid) { public ResponseEntity<Void> activate(@PathVariable String uuid) {
RegistrationEntity registration = registrationRepository.findById(UUID.fromString(uuid)).orElse(null); try {
if (registration != null && !Boolean.TRUE.equals(registration.getActivated())) { String email = registrationService.activate(uuid);
ResponseEntity<Void> response = userController.userAnlegen(registration.toRegistration()); String redirect = "/login.html?email=" + java.net.URLEncoder.encode(email, java.nio.charset.StandardCharsets.UTF_8);
if (response.getStatusCode().is2xxSuccessful()) {
registration.setActivated(Boolean.TRUE);
registrationRepository.save(registration);
String redirect = "/login.html?email=" + java.net.URLEncoder.encode(registration.getEmail(), java.nio.charset.StandardCharsets.UTF_8);
return ResponseEntity.status(302).location(URI.create(redirect)).build(); return ResponseEntity.status(302).location(URI.create(redirect)).build();
} else { } catch (IllegalStateException e) {
// Bereits aktiviert → trotzdem zum Login weiterleiten (idempotent)
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.noContent().build();
} catch (Exception e) {
return ResponseEntity.internalServerError().build(); return ResponseEntity.internalServerError().build();
} }
} else {
return ResponseEntity.noContent().build();
}
} }
} }

View File

@@ -13,7 +13,7 @@ public class Registration {
private UUID id; private UUID id;
private String name; private String name;
private String email; private String email;
private String passwordHash; private String password;
private LocalDate geburtsdatum; private LocalDate geburtsdatum;
@Override @Override

View File

@@ -13,6 +13,7 @@ import de.oaa.xxx.mail.Email;
import de.oaa.xxx.mail.MailService; import de.oaa.xxx.mail.MailService;
import de.oaa.xxx.mail.MailTemplateService; import de.oaa.xxx.mail.MailTemplateService;
import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Period; import java.time.Period;
@@ -30,13 +31,16 @@ public class RegistrationController {
private final UserRepository userRepository; private final UserRepository userRepository;
private final MailService mailService; private final MailService mailService;
private final MailTemplateService mailTemplateService; private final MailTemplateService mailTemplateService;
private final PasswordEncoder passwordEncoder;
public RegistrationController(RegistrationRepository registrationRepository, UserRepository userRepository, public RegistrationController(RegistrationRepository registrationRepository, UserRepository userRepository,
MailService mailService, MailTemplateService mailTemplateService) { MailService mailService, MailTemplateService mailTemplateService,
PasswordEncoder passwordEncoder) {
this.registrationRepository = registrationRepository; this.registrationRepository = registrationRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.mailService = mailService; this.mailService = mailService;
this.mailTemplateService = mailTemplateService; this.mailTemplateService = mailTemplateService;
this.passwordEncoder = passwordEncoder;
} }
@PostMapping @PostMapping
@@ -57,6 +61,8 @@ public class RegistrationController {
LOGGER.warn("User mit Name {} bereits vorhanden", registration.getName()); LOGGER.warn("User mit Name {} bereits vorhanden", registration.getName());
return ResponseEntity.status(409).build(); return ResponseEntity.status(409).build();
} }
// Passwort serverseitig mit BCrypt hashen
registration.setPassword(passwordEncoder.encode(registration.getPassword()));
RegistrationEntity entity = RegistrationEntity.create(registration); RegistrationEntity entity = RegistrationEntity.create(registration);
registrationRepository.save(entity); registrationRepository.save(entity);

View File

@@ -40,7 +40,7 @@ public class RegistrationEntity {
registration.setId(registrationId); registration.setId(registrationId);
registration.setEmail(email); registration.setEmail(email);
registration.setName(name); registration.setName(name);
registration.setPasswordHash(password); registration.setPassword(password);
registration.setGeburtsdatum(geburtsdatum); registration.setGeburtsdatum(geburtsdatum);
return registration; return registration;
} }
@@ -51,7 +51,7 @@ public class RegistrationEntity {
entity.setEmail(registration.getEmail()); entity.setEmail(registration.getEmail());
entity.setActivated(Boolean.FALSE); entity.setActivated(Boolean.FALSE);
entity.setName(registration.getName()); entity.setName(registration.getName());
entity.setPassword(registration.getPasswordHash()); entity.setPassword(registration.getPassword());
entity.setGeburtsdatum(registration.getGeburtsdatum()); entity.setGeburtsdatum(registration.getGeburtsdatum());
return entity; return entity;
} }

View File

@@ -0,0 +1,58 @@
package de.oaa.xxx.registration;
import de.oaa.xxx.user.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.UUID;
/**
* Koordiniert den Aktivierungsflow: liest die RegistrationEntity aus der DB
* und delegiert die User-Anlage an den UserService.
* Ersetzt den direkten Controller→Controller-Aufruf im ActivationController.
*/
@Service
public class RegistrationService {
private static final Logger LOGGER = LoggerFactory.getLogger(RegistrationService.class);
private final RegistrationRepository registrationRepository;
private final UserService userService;
public RegistrationService(RegistrationRepository registrationRepository, UserService userService) {
this.registrationRepository = registrationRepository;
this.userService = userService;
}
/**
* Aktiviert eine Registrierung: legt den User an und markiert die Registration als aktiviert.
*
* @return E-Mail des aktivierten Users (für Redirect im Controller)
* @throws IllegalArgumentException wenn UUID ungültig oder Registration nicht gefunden
* @throws IllegalStateException wenn Registration bereits aktiviert
*/
public String activate(String uuid) {
UUID registrationId;
try {
registrationId = UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Ungültige UUID: " + uuid);
}
RegistrationEntity registration = registrationRepository.findById(registrationId)
.orElseThrow(() -> new IllegalArgumentException("Registration nicht gefunden: " + uuid));
if (Boolean.TRUE.equals(registration.getActivated())) {
throw new IllegalStateException("Registration bereits aktiviert");
}
userService.createUser(registration.toRegistration());
registration.setActivated(Boolean.TRUE);
registrationRepository.save(registration);
LOGGER.info("Registration {} aktiviert, User {} angelegt", uuid, registration.getEmail());
return registration.getEmail();
}
}

View File

@@ -7,15 +7,16 @@ import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.security.Principal; import java.security.Principal;
import java.time.Duration; import java.time.Duration;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@@ -24,21 +25,25 @@ public class LoginController {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class); private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
record LoginRequest(String email, String password) {}
private final UserRepository userRepository; private final UserRepository userRepository;
private final JwtService jwtService; private final JwtService jwtService;
private final PasswordEncoder passwordEncoder;
public LoginController(UserRepository userRepository, JwtService jwtService) { public LoginController(UserRepository userRepository, JwtService jwtService, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.jwtService = jwtService; this.jwtService = jwtService;
this.passwordEncoder = passwordEncoder;
} }
@GetMapping @PostMapping
public ResponseEntity<User> login(@RequestParam String email, @RequestParam String hash, public ResponseEntity<User> login(@RequestBody LoginRequest request, HttpServletResponse response) {
HttpServletResponse response) { var userOpt = userRepository.findByEmail(request.email());
Optional<UserEntity> user = userRepository.findByEmailAndPassword(email, hash); if (userOpt.isPresent() && passwordEncoder.matches(request.password(), userOpt.get().getPassword())) {
if (user.isPresent()) { UserEntity user = userOpt.get();
LOGGER.info("User erfolgreich angemeldet: {}", email); LOGGER.info("User erfolgreich angemeldet: {}", request.email());
String token = jwtService.generateToken(user.get().getEmail(), user.get().getName()); String token = jwtService.generateToken(user.getEmail(), user.getName());
ResponseCookie cookie = ResponseCookie.from("jwt", token) ResponseCookie cookie = ResponseCookie.from("jwt", token)
.httpOnly(true) .httpOnly(true)
.sameSite("Strict") .sameSite("Strict")
@@ -46,7 +51,7 @@ public class LoginController {
.maxAge(Duration.ofHours(24)) .maxAge(Duration.ofHours(24))
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.ok(user.get().toUser()); return ResponseEntity.ok(user.toUser());
} else { } else {
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }

View File

@@ -23,34 +23,13 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity; import de.oaa.xxx.games.bdsm.entity.BdsmDefaultsEntity;
import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository; import de.oaa.xxx.games.bdsm.repository.BdsmDefaultsRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.FavoritRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.emailchange.EmailChangeRepository;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.passwordreset.PasswordResetRepository;
import de.oaa.xxx.registration.Registration; import de.oaa.xxx.registration.Registration;
import de.oaa.xxx.registration.RegistrationRepository; import de.oaa.xxx.registration.RegistrationRepository;
import de.oaa.xxx.social.entity.MessageCause; import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.NotificationPreferenceEntity; import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import de.oaa.xxx.social.repository.KommentarLikeRepository;
import de.oaa.xxx.social.repository.KommentarRepository;
import de.oaa.xxx.social.repository.NotificationPreferenceRepository; import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
import de.oaa.xxx.social.repository.PinnwandEintragRepository;
import de.oaa.xxx.social.repository.PinnwandLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageRepository;
import jakarta.transaction.Transactional;
@RestController @RestController
@RequestMapping("/user") @RequestMapping("/user")
@@ -60,71 +39,20 @@ public class UserController {
private final UserRepository userRepository; private final UserRepository userRepository;
private final RegistrationRepository registrationRepository; private final RegistrationRepository registrationRepository;
private final AufgabenGruppeRepository aufgabenGruppeRepository;
private final AufgabeRepository aufgabeRepository;
private final StrafeRepository strafeRepository;
private final SperreRepository sperreRepository;
private final ToyRepository toyRepository;
private final FavoritRepository favoritRepository;
private final GruppenAboRepository gruppenAboRepository;
private final BdsmGameRepository sessionRepository;
private final AktiveSperreRepository aktiveSperreRepository;
private final MitspielerRepository mitspielerRepository;
private final EmailChangeRepository emailChangeRepository;
private final PasswordResetRepository passwordResetRepository;
private final ProfileImageRepository profileImageRepository;
private final ProfileImageLikeRepository profileImageLikeRepository;
private final PinnwandEintragRepository pinnwandEintragRepository;
private final PinnwandLikeRepository pinnwandLikeRepository;
private final KommentarRepository kommentarRepository;
private final KommentarLikeRepository kommentarLikeRepository;
private final NotificationPreferenceRepository notificationPreferenceRepository; private final NotificationPreferenceRepository notificationPreferenceRepository;
private final BdsmDefaultsRepository bdsmDefaultsRepository; private final BdsmDefaultsRepository bdsmDefaultsRepository;
private final UserService userService;
public UserController(UserRepository userRepository, public UserController(UserRepository userRepository,
RegistrationRepository registrationRepository, RegistrationRepository registrationRepository,
AufgabenGruppeRepository aufgabenGruppeRepository,
AufgabeRepository aufgabeRepository,
StrafeRepository strafeRepository,
SperreRepository sperreRepository,
ToyRepository toyRepository,
FavoritRepository favoritRepository,
GruppenAboRepository gruppenAboRepository,
BdsmGameRepository sessionRepository,
AktiveSperreRepository aktiveSperreRepository,
MitspielerRepository mitspielerRepository,
EmailChangeRepository emailChangeRepository,
PasswordResetRepository passwordResetRepository,
ProfileImageRepository profileImageRepository,
ProfileImageLikeRepository profileImageLikeRepository,
PinnwandEintragRepository pinnwandEintragRepository,
PinnwandLikeRepository pinnwandLikeRepository,
KommentarRepository kommentarRepository,
KommentarLikeRepository kommentarLikeRepository,
NotificationPreferenceRepository notificationPreferenceRepository, NotificationPreferenceRepository notificationPreferenceRepository,
BdsmDefaultsRepository bdsmDefaultsRepository) { BdsmDefaultsRepository bdsmDefaultsRepository,
UserService userService) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.registrationRepository = registrationRepository; this.registrationRepository = registrationRepository;
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository;
this.sperreRepository = sperreRepository;
this.toyRepository = toyRepository;
this.favoritRepository = favoritRepository;
this.gruppenAboRepository = gruppenAboRepository;
this.sessionRepository = sessionRepository;
this.aktiveSperreRepository = aktiveSperreRepository;
this.mitspielerRepository = mitspielerRepository;
this.emailChangeRepository = emailChangeRepository;
this.passwordResetRepository = passwordResetRepository;
this.profileImageRepository = profileImageRepository;
this.profileImageLikeRepository = profileImageLikeRepository;
this.pinnwandEintragRepository = pinnwandEintragRepository;
this.pinnwandLikeRepository = pinnwandLikeRepository;
this.kommentarRepository = kommentarRepository;
this.kommentarLikeRepository = kommentarLikeRepository;
this.notificationPreferenceRepository = notificationPreferenceRepository; this.notificationPreferenceRepository = notificationPreferenceRepository;
this.bdsmDefaultsRepository = bdsmDefaultsRepository; this.bdsmDefaultsRepository = bdsmDefaultsRepository;
this.userService = userService;
} }
record ProfilePictureRequest(String picture, String pictureHq) {} record ProfilePictureRequest(String picture, String pictureHq) {}
@@ -250,6 +178,7 @@ public class UserController {
BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId) BdsmDefaultsEntity d = bdsmDefaultsRepository.findByUserId(userId)
.orElse(new BdsmDefaultsEntity()); .orElse(new BdsmDefaultsEntity());
Map<String, Object> result = new java.util.LinkedHashMap<>(); Map<String, Object> result = new java.util.LinkedHashMap<>();
result.put("geschlecht", userOpt.get().getGeschlecht() != null ? userOpt.get().getGeschlecht().name() : null);
result.put("spieltMit", splitOrEmpty(d.getSpieltMit())); result.put("spieltMit", splitOrEmpty(d.getSpieltMit()));
result.put("rollen", splitOrEmpty(d.getRollen())); result.put("rollen", splitOrEmpty(d.getRollen()));
result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge())); result.put("werkzeuge", splitOrEmpty(d.getWerkzeuge()));
@@ -322,84 +251,14 @@ public class UserController {
} }
@DeleteMapping("/me") @DeleteMapping("/me")
@Transactional
public ResponseEntity<Void> deleteAccount(Principal principal) { public ResponseEntity<Void> deleteAccount(Principal principal) {
var userOpt = userRepository.findByEmail(principal.getName()); var userOpt = userRepository.findByEmail(principal.getName());
if (userOpt.isEmpty()) return ResponseEntity.status(401).build(); if (userOpt.isEmpty()) return ResponseEntity.status(401).build();
var user = userOpt.get(); UUID userId = userOpt.get().getUserId();
UUID userId = user.getUserId(); String email = userOpt.get().getEmail();
String email = user.getEmail();
LOGGER.info("Lösche Konto für User {}", email); userService.deleteAccount(userId, email);
// 1. Delete user's AufgabenGruppen and all their content
var gruppen = aufgabenGruppeRepository.findByUserId(userId);
if (!gruppen.isEmpty()) {
aufgabeRepository.deleteAll(aufgabeRepository.findByAufgabenGruppeIn(gruppen));
strafeRepository.deleteAll(strafeRepository.findByAufgabenGruppeIn(gruppen));
sperreRepository.deleteAll(sperreRepository.findByAufgabenGruppeIn(gruppen));
for (var gruppe : gruppen) {
gruppenAboRepository.deleteByAufgabenGruppe(gruppe);
favoritRepository.deleteByAufgabenGruppeId(gruppe.getGruppenId());
}
aufgabenGruppeRepository.deleteAll(gruppen);
}
// 2. Delete user's Toys (join table refs already cleared above)
toyRepository.deleteAll(toyRepository.findByUserId(userId));
// 3. Delete user's own Favoriten and Gruppenabos (to other groups)
favoritRepository.deleteAll(favoritRepository.findByUserId(userId));
gruppenAboRepository.deleteAll(gruppenAboRepository.findByUserId(userId));
// 4. Delete Session with Mitspieler and AktiveSperre
var sessionOpt = sessionRepository.findByUserId(userId);
if (sessionOpt.isPresent()) {
var session = sessionOpt.get();
List<AktiveSperreEntity> sperren = session.getAktiveSperren();
List<MitspielerEntity> mitspieler = session.getMitspieler();
aktiveSperreRepository.deleteAll(sperren);
mitspielerRepository.deleteAll(mitspieler);
sessionRepository.delete(session);
}
// 5. Delete pending tokens
emailChangeRepository.findByUserEmail(email).ifPresent(emailChangeRepository::delete);
passwordResetRepository.findByEmail(email).ifPresent(passwordResetRepository::delete);
// 5b. Delete profile images and likes
var profileImages = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId);
for (var img : profileImages) {
profileImageLikeRepository.deleteByImageId(img.getImageId());
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("IMAGE", img.getImageId())
.forEach(k -> {
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
kommentarRepository.delete(k);
});
}
profileImageRepository.deleteAll(profileImages);
profileImageLikeRepository.deleteByUserId(userId);
// 5c. Delete pinnwand entries (authored by or on user's wall) + their likes/comments
var ownWallEntries = pinnwandEintragRepository.findByProfilUserIdOrderByCreatedAtDesc(userId);
for (var e : ownWallEntries) {
pinnwandLikeRepository.deleteByEintragId(e.getEintragId());
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", e.getEintragId())
.forEach(k -> {
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
kommentarRepository.delete(k);
});
}
pinnwandEintragRepository.deleteAll(ownWallEntries);
pinnwandEintragRepository.deleteByAuthorId(userId);
pinnwandLikeRepository.deleteByUserId(userId);
kommentarRepository.deleteByAuthorId(userId);
kommentarLikeRepository.deleteByUserId(userId);
// 6. Delete user
userRepository.delete(user);
// Clear JWT cookie
ResponseCookie cookie = ResponseCookie.from("jwt", "") ResponseCookie cookie = ResponseCookie.from("jwt", "")
.httpOnly(true) .httpOnly(true)
.sameSite("Strict") .sameSite("Strict")
@@ -413,30 +272,15 @@ public class UserController {
@PostMapping @PostMapping
public ResponseEntity<Void> userAnlegen(@RequestBody Registration registration) { public ResponseEntity<Void> userAnlegen(@RequestBody Registration registration) {
if (registration.getEmail() == null || registration.getPasswordHash() == null || registration.getName() == null) {
return ResponseEntity.badRequest().build();
}
if (userRepository.findByEmail(registration.getEmail()).isPresent()) {
LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail());
return ResponseEntity.status(409).build();
}
try { try {
UserEntity entity = new UserEntity(); userService.createUser(registration);
entity.setUserId(UUID.randomUUID());
entity.setEmail(registration.getEmail());
entity.setName(registration.getName());
entity.setPassword(registration.getPasswordHash());
entity.setGeburtsdatum(registration.getGeburtsdatum());
userRepository.save(entity);
for (MessageCause cause : MessageCause.values()) {
notificationPreferenceRepository.save(
NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause));
}
return ResponseEntity.status(201).build(); return ResponseEntity.status(201).build();
} catch (Exception exception) { } catch (IllegalArgumentException e) {
LOGGER.error(exception.getMessage(), exception); return ResponseEntity.badRequest().build();
} catch (IllegalStateException e) {
return ResponseEntity.status(409).build();
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return ResponseEntity.internalServerError().build(); return ResponseEntity.internalServerError().build();
} }
} }

View File

@@ -8,7 +8,6 @@ import java.util.UUID;
public interface UserRepository extends JpaRepository<UserEntity, UUID> { public interface UserRepository extends JpaRepository<UserEntity, UUID> {
Optional<UserEntity> findByEmailAndPassword(String email, String password);
Optional<UserEntity> findByEmail(String email); Optional<UserEntity> findByEmail(String email);
Optional<UserEntity> findByName(String name); Optional<UserEntity> findByName(String name);
List<UserEntity> findByNameContainingIgnoreCase(String name); List<UserEntity> findByNameContainingIgnoreCase(String name);

View File

@@ -0,0 +1,210 @@
package de.oaa.xxx.user;
import de.oaa.xxx.aufgaben.repository.AufgabeRepository;
import de.oaa.xxx.aufgaben.repository.AufgabenGruppeRepository;
import de.oaa.xxx.aufgaben.repository.FavoritRepository;
import de.oaa.xxx.aufgaben.repository.GruppenAboRepository;
import de.oaa.xxx.aufgaben.repository.SperreRepository;
import de.oaa.xxx.aufgaben.repository.StrafeRepository;
import de.oaa.xxx.aufgaben.repository.ToyRepository;
import de.oaa.xxx.emailchange.EmailChangeRepository;
import de.oaa.xxx.games.bdsm.entity.AktiveSperreEntity;
import de.oaa.xxx.games.bdsm.entity.MitspielerEntity;
import de.oaa.xxx.games.bdsm.repository.AktiveSperreRepository;
import de.oaa.xxx.games.bdsm.repository.BdsmGameRepository;
import de.oaa.xxx.games.bdsm.repository.MitspielerRepository;
import de.oaa.xxx.passwordreset.PasswordResetRepository;
import de.oaa.xxx.registration.Registration;
import de.oaa.xxx.social.entity.MessageCause;
import de.oaa.xxx.social.entity.NotificationPreferenceEntity;
import de.oaa.xxx.social.repository.KommentarLikeRepository;
import de.oaa.xxx.social.repository.KommentarRepository;
import de.oaa.xxx.social.repository.NotificationPreferenceRepository;
import de.oaa.xxx.social.repository.PinnwandEintragRepository;
import de.oaa.xxx.social.repository.PinnwandLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageLikeRepository;
import de.oaa.xxx.social.repository.ProfileImageRepository;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
@Service
public class UserService {
private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository;
private final AufgabenGruppeRepository aufgabenGruppeRepository;
private final AufgabeRepository aufgabeRepository;
private final StrafeRepository strafeRepository;
private final SperreRepository sperreRepository;
private final ToyRepository toyRepository;
private final FavoritRepository favoritRepository;
private final GruppenAboRepository gruppenAboRepository;
private final BdsmGameRepository sessionRepository;
private final AktiveSperreRepository aktiveSperreRepository;
private final MitspielerRepository mitspielerRepository;
private final EmailChangeRepository emailChangeRepository;
private final PasswordResetRepository passwordResetRepository;
private final ProfileImageRepository profileImageRepository;
private final ProfileImageLikeRepository profileImageLikeRepository;
private final PinnwandEintragRepository pinnwandEintragRepository;
private final PinnwandLikeRepository pinnwandLikeRepository;
private final KommentarRepository kommentarRepository;
private final KommentarLikeRepository kommentarLikeRepository;
private final NotificationPreferenceRepository notificationPreferenceRepository;
public UserService(UserRepository userRepository,
AufgabenGruppeRepository aufgabenGruppeRepository,
AufgabeRepository aufgabeRepository,
StrafeRepository strafeRepository,
SperreRepository sperreRepository,
ToyRepository toyRepository,
FavoritRepository favoritRepository,
GruppenAboRepository gruppenAboRepository,
BdsmGameRepository sessionRepository,
AktiveSperreRepository aktiveSperreRepository,
MitspielerRepository mitspielerRepository,
EmailChangeRepository emailChangeRepository,
PasswordResetRepository passwordResetRepository,
ProfileImageRepository profileImageRepository,
ProfileImageLikeRepository profileImageLikeRepository,
PinnwandEintragRepository pinnwandEintragRepository,
PinnwandLikeRepository pinnwandLikeRepository,
KommentarRepository kommentarRepository,
KommentarLikeRepository kommentarLikeRepository,
NotificationPreferenceRepository notificationPreferenceRepository) {
this.userRepository = userRepository;
this.aufgabenGruppeRepository = aufgabenGruppeRepository;
this.aufgabeRepository = aufgabeRepository;
this.strafeRepository = strafeRepository;
this.sperreRepository = sperreRepository;
this.toyRepository = toyRepository;
this.favoritRepository = favoritRepository;
this.gruppenAboRepository = gruppenAboRepository;
this.sessionRepository = sessionRepository;
this.aktiveSperreRepository = aktiveSperreRepository;
this.mitspielerRepository = mitspielerRepository;
this.emailChangeRepository = emailChangeRepository;
this.passwordResetRepository = passwordResetRepository;
this.profileImageRepository = profileImageRepository;
this.profileImageLikeRepository = profileImageLikeRepository;
this.pinnwandEintragRepository = pinnwandEintragRepository;
this.pinnwandLikeRepository = pinnwandLikeRepository;
this.kommentarRepository = kommentarRepository;
this.kommentarLikeRepository = kommentarLikeRepository;
this.notificationPreferenceRepository = notificationPreferenceRepository;
}
/**
* Löscht einen User-Account vollständig inklusive aller abhängigen Daten.
* Gibt die gelöschte E-Mail zurück (wird für Cookie-Clearing im Controller benötigt).
*/
@Transactional
public void deleteAccount(UUID userId, String email) {
var user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User nicht gefunden: " + userId));
LOGGER.info("Lösche Konto für User {}", email);
// 1. AufgabenGruppen und deren Inhalte löschen
var gruppen = aufgabenGruppeRepository.findByUserId(userId);
if (!gruppen.isEmpty()) {
aufgabeRepository.deleteAll(aufgabeRepository.findByAufgabenGruppeIn(gruppen));
strafeRepository.deleteAll(strafeRepository.findByAufgabenGruppeIn(gruppen));
sperreRepository.deleteAll(sperreRepository.findByAufgabenGruppeIn(gruppen));
for (var gruppe : gruppen) {
gruppenAboRepository.deleteByAufgabenGruppe(gruppe);
favoritRepository.deleteByAufgabenGruppeId(gruppe.getGruppenId());
}
aufgabenGruppeRepository.deleteAll(gruppen);
}
// 2. Toys löschen
toyRepository.deleteAll(toyRepository.findByUserId(userId));
// 3. Eigene Favoriten und Gruppenabos löschen
favoritRepository.deleteAll(favoritRepository.findByUserId(userId));
gruppenAboRepository.deleteAll(gruppenAboRepository.findByUserId(userId));
// 4. BDSM-Session mit Mitspieler und AktiveSperre löschen
var sessionOpt = sessionRepository.findByUserId(userId);
if (sessionOpt.isPresent()) {
var session = sessionOpt.get();
List<AktiveSperreEntity> sperren = session.getAktiveSperren();
List<MitspielerEntity> mitspieler = session.getMitspieler();
aktiveSperreRepository.deleteAll(sperren);
mitspielerRepository.deleteAll(mitspieler);
sessionRepository.delete(session);
}
// 5. Pending Tokens löschen
emailChangeRepository.findByUserEmail(email).ifPresent(emailChangeRepository::delete);
passwordResetRepository.findByEmail(email).ifPresent(passwordResetRepository::delete);
// 5b. Profilbilder und Likes löschen
var profileImages = profileImageRepository.findByUserIdOrderByUploadedAtDesc(userId);
for (var img : profileImages) {
profileImageLikeRepository.deleteByImageId(img.getImageId());
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("IMAGE", img.getImageId())
.forEach(k -> {
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
kommentarRepository.delete(k);
});
}
profileImageRepository.deleteAll(profileImages);
profileImageLikeRepository.deleteByUserId(userId);
// 5c. Pinnwand-Einträge und Likes/Kommentare löschen
var ownWallEntries = pinnwandEintragRepository.findByProfilUserIdOrderByCreatedAtDesc(userId);
for (var e : ownWallEntries) {
pinnwandLikeRepository.deleteByEintragId(e.getEintragId());
kommentarRepository.findByTargetTypeAndTargetIdOrderByCreatedAtAsc("PINNWAND", e.getEintragId())
.forEach(k -> {
kommentarLikeRepository.deleteByKommentarId(k.getKommentarId());
kommentarRepository.delete(k);
});
}
pinnwandEintragRepository.deleteAll(ownWallEntries);
pinnwandEintragRepository.deleteByAuthorId(userId);
pinnwandLikeRepository.deleteByUserId(userId);
kommentarRepository.deleteByAuthorId(userId);
kommentarLikeRepository.deleteByUserId(userId);
// 6. User löschen
userRepository.delete(user);
}
/**
* Legt einen neuen User aus einer bestätigten Registration an
* und erstellt die Standard-Benachrichtigungseinstellungen.
*/
public void createUser(Registration registration) {
if (registration.getEmail() == null || registration.getPassword() == null || registration.getName() == null) {
throw new IllegalArgumentException("E-Mail, Passwort und Name sind Pflichtfelder");
}
if (userRepository.findByEmail(registration.getEmail()).isPresent()) {
LOGGER.warn("User mit E-Mail {} bereits vorhanden", registration.getEmail());
throw new IllegalStateException("E-Mail bereits vorhanden");
}
UserEntity entity = new UserEntity();
entity.setUserId(UUID.randomUUID());
entity.setEmail(registration.getEmail());
entity.setName(registration.getName());
entity.setPassword(registration.getPassword());
entity.setGeburtsdatum(registration.getGeburtsdatum());
userRepository.save(entity);
for (MessageCause cause : MessageCause.values()) {
notificationPreferenceRepository.save(
NotificationPreferenceEntity.defaultFor(entity.getUserId(), cause));
}
LOGGER.info("User {} angelegt", entity.getUserId());
}
}

View File

@@ -98,7 +98,7 @@
document.getElementById('sub').textContent = 'Du hast die Einladung abgelehnt.'; document.getElementById('sub').textContent = 'Du hast die Einladung abgelehnt.';
document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>'; document.getElementById('actions').innerHTML = '<button onclick="window.location.href=\'/userhome.html\'">Zur Startseite</button>';
} else if (mode === 'OWN_DEVICE') { } else if (mode === 'OWN_DEVICE') {
window.location.replace(`/bdsmwarten.html?id=${einladungId}`); window.location.replace(`/neubdsm.html`);
} else { } else {
zeigeBestaetigt(); zeigeBestaetigt();
} }

View File

@@ -2,356 +2,10 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>BDSM Game</title>
<title>BDSM Game Neue Session XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.setting-row { margin-bottom: 1.25rem; }
.setting-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.4rem;
}
.setting-header label {
font-size: 0.85rem;
color: #aaa;
margin: 0;
}
.setting-value {
font-size: 1rem;
font-weight: 600;
color: var(--color-primary);
min-width: 3.5rem;
text-align: right;
}
input[type="range"] {
width: 100%;
accent-color: var(--color-primary);
cursor: pointer;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.modal-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 2rem;
max-width: 420px;
width: 100%;
text-align: center;
}
.modal-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 0.75rem;
}
.modal-text {
font-size: 0.9rem;
color: var(--color-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.modal-actions { display: flex; flex-direction: column; gap: 0.6rem; }
.modal-actions button { width: 100%; padding: 0.75rem; }
</style>
</head> </head>
<body class="app"> <body>
<script>window.location.replace('/neubdsm.html');</script>
<div class="modal-overlay" id="modal" style="display:none;">
<div class="modal-card">
<div class="modal-title" id="modalTitle"></div>
<div class="modal-text" id="modalText"></div>
<div class="modal-actions" id="modalActions"></div>
</div>
</div>
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 1 von 4 Session-Einstellungen</p>
<div class="setup-section">
<h2>Session-Einstellungen</h2>
<div class="setting-row">
<div class="setting-header">
<label for="sldStrafe">Wahrscheinlichkeit Strafe</label>
<span class="setting-value"><span id="valStrafe">15</span> %</span>
</div>
<input type="range" id="sldStrafe" min="0" max="100" value="15"
oninput="document.getElementById('valStrafe').textContent=this.value; updateWarnung()">
</div>
<div class="setting-row">
<div class="setting-header">
<label for="sldZeitstrafe">Wahrscheinlichkeit Zeitstrafe</label>
<span class="setting-value"><span id="valZeitstrafe">15</span> %</span>
</div>
<input type="range" id="sldZeitstrafe" min="0" max="100" value="15"
oninput="document.getElementById('valZeitstrafe').textContent=this.value; updateWarnung()">
</div>
<div class="message" id="wahrschWarnung" style="display:none; margin-top:0.75rem;"></div>
<div class="setting-row">
<div class="setting-header">
<label for="sldAufgaben">Aufgaben pro Level</label>
<span class="setting-value" id="valAufgaben">5</span>
</div>
<input type="range" id="sldAufgaben" min="1" max="20" value="5"
oninput="document.getElementById('valAufgaben').textContent=this.value">
</div>
<div class="setting-row">
<div class="setting-header">
<label for="sldZeit">Zeitfaktor Zeitstrafen</label>
<span class="setting-value" id="valZeit">1,0</span>
</div>
<input type="range" id="sldZeit" min="5" max="20" value="10"
oninput="document.getElementById('valZeit').textContent=(this.value/10).toFixed(1).replace('.',',')">
</div>
</div>
<div class="message" id="message"></div>
<button class="full-width" onclick="weiter()">Weiter</button>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
function updateWarnung() {
const strafe = parseInt(document.getElementById('sldStrafe').value);
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
const summe = strafe + zeitstrafe;
const el = document.getElementById('wahrschWarnung');
if (summe > 98) {
el.textContent = `Kombiniert ${summe} % Werte über 98 % sind nicht möglich.`;
el.className = 'message error';
el.style.display = 'block';
} else if (summe > 60) {
el.textContent = `Hinweis: Bei ${summe} % kombinierten Wahrscheinlichkeiten ist die Chance auf Vanilla-Aufgaben sehr gering.`;
el.className = 'message warning';
el.style.display = 'block';
} else {
el.style.display = 'none';
}
}
async function weiter() {
hideMessage();
const strafe = parseInt(document.getElementById('sldStrafe').value);
const zeitstrafe = parseInt(document.getElementById('sldZeitstrafe').value);
if (strafe + zeitstrafe > 98) {
showMessage('Die kombinierten Wahrscheinlichkeiten dürfen 98 % nicht überschreiten.', 'error');
return;
}
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';
}
function showMessage(text, type) {
const el = document.getElementById('message');
el.textContent = text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
// ── Aktive-Session-Check ──
function zeigeModal(title, text, actions) {
document.getElementById('modalTitle').textContent = title;
const textEl = document.getElementById('modalText');
textEl.textContent = text;
textEl.style.display = text ? '' : 'none';
const actEl = document.getElementById('modalActions');
actEl.innerHTML = '';
actions.forEach(a => {
const btn = document.createElement('button');
btn.textContent = a.label;
btn.className = a.primary ? 'full-width' : 'full-width secondary';
btn.onclick = () => a.onClick();
actEl.appendChild(btn);
});
document.getElementById('modal').style.display = 'flex';
}
function versteckeModal() {
document.getElementById('modal').style.display = 'none';
}
const BDSM_STORAGE_KEYS = [
'bdsm-session-id', 'bdsm-session-settings', 'bdsm-session-setup',
'bdsm-session-gruppen', 'bdsm-session-toys', 'bdsm-session-game',
];
function sessionFortfahren(sid) {
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
sessionStorage.setItem('bdsm-session-id', sid);
window.location.href = '/bdsmingame.html';
}
function sessionBeendenFragen(sid) {
zeigeModal(
'Session wirklich beenden?',
'Die Session und alle aktiven Sperren werden gelöscht.',
[
{ label: 'Ja, beenden', primary: true, onClick: () => sessionLoeschen(sid) },
{ label: 'Nein, fortfahren', onClick: () => sessionFortfahren(sid) },
]
);
}
async function sessionLoeschen(sid) {
versteckeModal();
try {
await fetch('/bdsm', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: sid }),
});
} catch (_) { /* ignorieren */ }
BDSM_STORAGE_KEYS.forEach(k => sessionStorage.removeItem(k));
fetch('/bdsm/setup-draft', { method: 'DELETE' }).catch(() => {});
}
(async function checkAktiveSession() {
try {
const meRes = await fetch('/login/me');
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.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;
}
// 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 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> </body>
</html> </html>

View File

@@ -337,7 +337,7 @@
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null'); const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup') || 'null');
const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]'); const toys = JSON.parse(sessionStorage.getItem('bdsm-session-toys') || '[]');
const sessionId = sessionStorage.getItem('bdsm-session-id'); const sessionId = sessionStorage.getItem('bdsm-session-id');
if (!sessionId) window.location.replace('/bdsm.html'); if (!sessionId) window.location.replace('/neubdsm.html');
// Multi-Device: bin ich Gast? // Multi-Device: bin ich Gast?
const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true'; const isGuest = sessionStorage.getItem('bdsm-is-guest') === 'true';
@@ -424,7 +424,7 @@
try { try {
const res = await fetch(`/bdsm/${sessionId}/aufgaben/next`); const res = await fetch(`/bdsm/${sessionId}/aufgaben/next`);
if (res.status === 204) { zeigeFinaleDialog(); return; } if (res.status === 204) { zeigeFinaleDialog(); return; }
if (res.status === 400) { window.location.replace('/bdsmtoys.html'); return; } if (res.status === 400) { window.location.replace('/neubdsm.html'); return; }
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
currentTask = await res.json(); currentTask = await res.json();
await saveAktiveAufgabe(currentTask, null); await saveAktiveAufgabe(currentTask, null);

View File

@@ -2,984 +2,10 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>BDSM Game</title>
<title>BDSM Game Mitspieler XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ── Player cards ── */
.player-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1rem;
}
.player-card-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1.25rem;
}
.player-title {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
}
.player-badge {
background: var(--color-primary);
color: #fff;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-badge-pending {
background: var(--color-secondary);
color: var(--color-muted);
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-badge-accepted {
background: #1a5c2a;
color: #6fcf97;
font-size: 0.7rem;
padding: 0.1rem 0.5rem;
border-radius: 10px;
font-weight: 600;
}
.player-remove {
margin-left: auto;
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.25rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.player-remove:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: transparent;
}
.btn-invite {
background: transparent;
border: 1px solid var(--color-primary);
color: var(--color-primary);
padding: 0.45rem 1rem;
font-size: 0.875rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-invite:hover { background: var(--color-primary); color: #fff; }
.btn-cancel-invite {
background: transparent;
border: 1px solid var(--color-secondary);
color: var(--color-muted);
padding: 0.2rem 0.6rem;
font-size: 0.75rem;
font-weight: normal;
border-radius: 4px;
cursor: pointer;
}
.btn-cancel-invite:hover { border-color: var(--color-primary); color: var(--color-primary); background: transparent; }
.pending-info {
text-align: center;
color: var(--color-muted);
font-size: 0.9rem;
padding: 1.5rem 0;
}
.pending-name {
font-weight: 600;
color: var(--color-text);
font-size: 1rem;
margin-bottom: 0.25rem;
}
.pending-mode {
font-size: 0.8rem;
color: var(--color-muted);
}
.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; }
.add-player-btn {
width: 100%;
background: transparent;
border: 1px dashed var(--color-secondary);
color: var(--color-muted);
padding: 0.75rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: normal;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
margin-bottom: 2rem;
}
.add-player-btn:hover { border-color: var(--color-primary); color: var(--color-text); background: transparent; }
.field-error { font-size: 0.78rem; color: var(--color-primary); margin-top: 0.3rem; display: none; }
/* ── Freunde-Modal ── */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.75);
z-index: 1000;
display: flex; align-items: center; justify-content: center;
padding: 1.5rem;
}
.modal-card {
background: var(--color-card);
border: 1px solid var(--color-secondary);
border-radius: 14px;
padding: 1.75rem;
max-width: 420px; width: 100%;
}
.modal-title { font-size: 1rem; font-weight: 700; margin-bottom: 1rem; }
.check-item.is-disabled { opacity: 0.5; pointer-events: none; cursor: default; }
.friend-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; background: var(--color-secondary); flex-shrink: 0; }
.friend-combobox { position: relative; }
.friend-dropdown {
display: none; position: absolute; top: 100%; left: 0; right: 0;
background: var(--color-card); border: 1px solid var(--color-secondary);
border-radius: 8px; max-height: 220px; overflow-y: auto; z-index: 10; margin-top: 0.25rem;
}
.friend-dropdown-item {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.6rem 0.75rem; cursor: pointer; transition: background 0.1s;
font-size: 0.9rem; font-weight: 600;
}
.friend-dropdown-item:hover { background: var(--color-secondary); }
.selected-friend-box {
display: none; margin-top: 0.75rem; padding: 0.6rem 0.75rem;
background: var(--color-secondary); border-radius: 8px;
font-size: 0.9rem; font-weight: 600;
border: 1px solid var(--color-primary); color: var(--color-text);
}
.modal-cancel { margin-top: 0.6rem; width: 100%; }
</style>
</head> </head>
<body class="app"> <body>
<script>window.location.replace('/neubdsm.html');</script>
<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>
<div class="friend-combobox">
<input type="text" id="friendSearch" placeholder="Name eingeben…" autocomplete="off" oninput="filterFreunde(this.value)">
<div class="friend-dropdown" id="friendDropdown"></div>
</div>
<div class="selected-friend-box" id="selectedFriendBox"></div>
<button id="btnEinladen" style="margin-top:1rem; width:100%;" disabled onclick="confirmedEinladen()">Einladen</button>
<button class="secondary modal-cancel" onclick="schliesseFriendModal()">Abbrechen</button>
</div>
</div>
<div class="main">
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 2 von 4 Mitspieler</p>
<div class="setup-section">
<h2>Mitspieler</h2>
<div id="playersContainer"></div>
<button class="add-player-btn" onclick="addPlayer()">+ Spieler hinzufügen</button>
</div>
<div class="message" id="message"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsm.html'">← Zurück</button>
<button style="flex:2;" id="weiterBtn" onclick="weiter()">Weiter</button>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
// SetupId erzeugen (persistent über sessionStorage)
if (!sessionStorage.getItem('bdsm-setup-id')) {
sessionStorage.setItem('bdsm-setup-id', crypto.randomUUID());
}
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' },
{ 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_DEFAULTS = {
MAENNLICH: ['MUND', 'PENIS', 'ANUS', 'UMSCHNALLDILDO'],
WEIBLICH: ['MUND', 'VAGINA', 'ANUS', 'UMSCHNALLDILDO'],
DIVERS: ['MUND', 'ANUS', 'UMSCHNALLDILDO'],
};
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 ROLE_LABELS = {
AUFGABE_AKTIV: 'Aufgabe Aktiv', AUFGABE_PASSIV: 'Aufgabe Passiv',
BESTRAFUNG_AKTIV: 'Bestrafung Aktiv', BESTRAFUNG_PASSIV: 'Bestrafung Passiv',
};
let playerSeq = 0;
let playerIds = [];
// { [playerId]: { einladungId, status, inviteeId, inviteeName, mode } | null }
let playerInvitations = {};
let pollIntervalId = null;
let myUserId = null;
let selfPlayerId = null;
let freundeListe = [];
function buildCheckItems(name, items, type, disabled = false) {
return items.map(({ value, label, desc }) => `
<label class="check-item${disabled ? ' is-disabled' : ''}">
<input type="${type}" name="${name}" value="${value}"${disabled ? ' disabled' : ''}>
<span>
<span class="check-item-label">${label}</span>
${desc ? `<span class="check-item-desc">${desc}</span>` : ''}
</span>
</label>`).join('');
}
function createCardHtml(id, prefillName, isSelf) {
const badge = isSelf ? '<span class="player-badge">Du</span>' : '';
const num = playerIds.indexOf(id) + 1;
const nameField = isSelf
? `<input type="text" id="p${id}-name" value="${prefillName}" readonly style="background:transparent;cursor:default;color:var(--color-muted);">`
: `<input type="text" id="p${id}-name" value="${prefillName}" placeholder="Name" autocomplete="off">`;
const inviteBtn = isSelf ? '' : `<button class="btn-invite" onclick="oeffneFreundeModal(${id})">👥 Einladen</button>`;
return `
<div class="player-card" id="player-${id}">
<div class="player-card-header">
<span class="player-title">Spieler ${num}</span>
${badge}
${inviteBtn}
<button class="player-remove" onclick="removePlayer(${id})">✕ Entfernen</button>
</div>
<div id="p${id}-body">
${buildPlayerBody(id, nameField, isSelf)}
</div>
</div>`;
}
function buildPlayerBody(id, nameField, genderDisabled = false) {
return `
<div class="card-field">
<label>Name</label>
${nameField}
<div class="field-error" id="p${id}-name-err">Bitte Namen eingeben.</div>
</div>
<div class="card-field">
<label>Geschlecht${genderDisabled ? ' <span style="font-size:0.75rem;color:var(--color-muted);">(unveränderlich)</span>' : ''}</label>
<div class="check-group">${buildCheckItems('p' + id + '-geschlecht', GESCHLECHTER, 'radio', genderDisabled)}</div>
<div class="field-error" id="p${id}-geschlecht-err">Bitte Geschlecht auswählen.</div>
</div>
<div class="card-field">
<label>Spielt mit</label>
<div class="check-group">${buildCheckItems('p' + id + '-spieltmit', GESCHLECHTER, 'checkbox')}</div>
<div class="field-error" id="p${id}-spieltmit-err">Bitte mindestens eine Option wählen.</div>
<div class="field-error" id="p${id}-partner-err">Kein Mitspieler mit passendem Geschlecht vorhanden.</div>
</div>
<div class="card-field">
<label>Rollen</label>
<div class="check-group">${buildCheckItems('p' + id + '-rollen', ROLLEN, 'checkbox')}</div>
<div class="field-error" id="p${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">${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')
.insertAdjacentHTML('beforeend', createCardHtml(id, prefillName, isSelf));
refreshRemoveButtons();
return id;
}
function removePlayer(id) {
const inv = playerInvitations[id];
// Einladung serverseitig canceln
if (inv && (inv.status === 'PENDING' || inv.status === 'ACCEPTED_OWN' || inv.status === 'ACCEPTED_HOST')) {
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];
refreshPlayerTitles();
refreshRemoveButtons();
}
function refreshPlayerTitles() {
playerIds.forEach((id, idx) => {
const el = document.querySelector(`#player-${id} .player-title`);
if (el) el.textContent = 'Spieler ' + (idx + 1);
});
}
function refreshRemoveButtons() {
playerIds.forEach((id, idx) => {
const btn = document.querySelector(`#player-${id} .player-remove`);
if (btn) btn.style.display = idx === 0 ? 'none' : '';
});
}
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.endsWith('-geschlecht')) {
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 {
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);
}
});
function getChecked(name) {
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';
}
// ── Freunde-Modal ──
let currentInvitePlayerId = null;
let selectedFriend = null; // { userId, name }
async function oeffneFreundeModal(playerId) {
currentInvitePlayerId = playerId;
selectedFriend = null;
document.getElementById('friendSearch').value = '';
document.getElementById('friendDropdown').style.display = 'none';
document.getElementById('friendDropdown').innerHTML = '';
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('selectedFriendBox').textContent = '';
document.getElementById('btnEinladen').disabled = true;
document.getElementById('friendModal').style.display = 'flex';
if (freundeListe.length === 0) {
try {
const res = await fetch('/social/friends');
freundeListe = res.ok ? await res.json() : [];
} catch (_) { freundeListe = []; }
}
}
function filterFreunde(query) {
selectedFriend = null;
document.getElementById('selectedFriendBox').style.display = 'none';
document.getElementById('btnEinladen').disabled = true;
const dropdown = document.getElementById('friendDropdown');
dropdown.innerHTML = '';
const q = query.trim().toLowerCase();
if (!q) { dropdown.style.display = 'none'; return; }
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';
return;
}
matches.forEach(f => {
const item = document.createElement('div');
item.className = 'friend-dropdown-item';
item.addEventListener('click', () => selectFriend(f.user.userId, f.user.name || 'Unbekannt'));
if (f.user.profilePicture) {
const img = document.createElement('img');
img.className = 'friend-avatar';
img.src = 'data:image/png;base64,' + f.user.profilePicture;
img.alt = '';
item.appendChild(img);
} else {
const av = document.createElement('div');
av.className = 'friend-avatar';
item.appendChild(av);
}
const span = document.createElement('span');
span.textContent = f.user.name || 'Unbekannt';
item.appendChild(span);
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
}
function selectFriend(userId, name) {
selectedFriend = { userId, name };
document.getElementById('friendSearch').value = name;
document.getElementById('friendDropdown').style.display = 'none';
const box = document.getElementById('selectedFriendBox');
box.textContent = '✓ ' + name;
box.style.display = 'block';
document.getElementById('btnEinladen').disabled = false;
}
async function confirmedEinladen() {
if (!selectedFriend) return;
await einladen(selectedFriend.userId, selectedFriend.name);
}
function schliesseFriendModal() {
document.getElementById('friendModal').style.display = 'none';
currentInvitePlayerId = null;
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();
if (!id) return;
try {
const res = await fetch('/bdsm/einladung', {
method: 'POST',
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 (_) {
zeigePopup('Fehler', 'Einladung konnte nicht gesendet werden. Bitte versuche es erneut.');
}
}
function renderPending(id) {
const inv = playerInvitations[id];
if (!inv) return;
const body = document.getElementById(`p${id}-body`);
if (!body) return;
const headerInvBtn = document.querySelector(`#player-${id} .btn-invite`);
if (headerInvBtn) headerInvBtn.style.display = 'none';
if (inv.status === 'PENDING') {
// 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.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') {
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">✓ 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);">`;
const hasGeschlecht = inv.defaults && inv.defaults.geschlecht;
body.innerHTML = buildPlayerBody(id, nameField, hasGeschlecht);
if (inv.defaults) {
restorePlayer(id, {
geschlecht: inv.defaults.geschlecht,
spieltMit: inv.defaults.spieltMit || [],
rollen: inv.defaults.rollen || [],
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`);
header.querySelectorAll('.player-badge-pending,.player-badge-accepted').forEach(el => el.remove());
if (headerInvBtn) headerInvBtn.style.display = '';
playerInvitations[id] = null;
body.innerHTML = buildPlayerBody(id, `<input type="text" id="p${id}-name" placeholder="Name" autocomplete="off">`);
}
}
async function cancelEinladung(id) {
const inv = playerInvitations[id];
if (!inv) return;
await fetch(`/bdsm/einladung/${inv.einladungId}`, { method: 'DELETE' }).catch(() => {});
playerInvitations[id] = null;
// UI zurücksetzen
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">`);
}
// ── Polling ──
function startPoll() {
if (pollIntervalId) return;
pollIntervalId = setInterval(pollEinladungen, 3000);
}
function stopPoll() {
if (pollIntervalId) { clearInterval(pollIntervalId); pollIntervalId = null; }
}
async function pollEinladungen() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (!hasPending) { stopPoll(); return; }
try {
const res = await fetch(`/bdsm/einladung?setupId=${setupId}`);
if (!res.ok) return;
const liste = await res.json();
for (const e of liste) {
const id = playerIds.find(pid => {
const inv = playerInvitations[pid];
return inv && inv.einladungId === e.einladungId;
});
if (!id) continue;
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') {
// Profil + Defaults der eingeladenen Person laden
try {
const dRes = await fetch(`/user/${inv.inviteeId}/bdsm-defaults`);
if (dRes.ok) inv.defaults = await dRes.json();
} catch (_) {}
}
renderPending(id);
}
} catch (_) {}
updateWeiterBtn();
}
function updateWeiterBtn() {
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
document.getElementById('weiterBtn').disabled = hasPending;
}
// ── Validation & Weiter ──
function weiter() {
hideMessage();
const hasPending = Object.values(playerInvitations).some(inv => inv && inv.status === 'PENDING');
if (hasPending) {
showMessage('Bitte warte, bis alle Einladungen beantwortet wurden.', 'error');
return;
}
let valid = true;
playerIds.forEach(id => setFieldError(`p${id}-partner-err`, false));
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`);
const rollen = getChecked(`p${id}-rollen`);
const werkzeuge = getChecked(`p${id}-werkzeuge`);
setFieldError(`p${id}-name-err`, !name);
setFieldError(`p${id}-geschlecht-err`, geschlecht.length === 0);
setFieldError(`p${id}-spieltmit-err`, spieltMit.length === 0);
setFieldError(`p${id}-rollen-err`, rollen.length === 0);
setFieldError(`p${id}-werkzeuge-err`, werkzeuge.length === 0);
if (!name || geschlecht.length === 0 || spieltMit.length === 0 || rollen.length === 0 || werkzeuge.length === 0) {
valid = false;
}
const sperrenAufloesen = document.getElementById(`p${id}-sperrenAufloesen`);
return {
name,
geschlecht: geschlecht[0] || null,
spieltMit, rollen, werkzeuge,
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 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;
}
}
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'));
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';
}
function showMessage(text, type) {
const el = document.getElementById('message');
el.textContent = text; el.className = `message ${type}`; el.style.display = 'block';
}
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}"]`);
if (radio) { radio.checked = true; radio.closest('.check-item')?.classList.add('is-checked'); }
}
(data.spieltMit || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-spieltmit"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.rollen || []).forEach(val => {
const cb = document.querySelector(`input[name="p${id}-rollen"][value="${val}"]`);
if (cb) { cb.checked = true; cb.closest('.check-item')?.classList.add('is-checked'); }
});
(data.werkzeuge || []).forEach(val => {
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 ──
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];
restorePlayer(selfId, {
geschlecht: user?.geschlecht || null,
spieltMit: defaults.spieltMit || [],
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> </body>
</html> </html>

View File

@@ -2,351 +2,10 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>BDSM Game</title>
<title>BDSM Game Aufgaben-Gruppen XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.select-all-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
}
.select-all-label input {
accent-color: var(--color-primary);
width: 14px;
height: 14px;
cursor: pointer;
flex-shrink: 0;
}
.gruppe-list { list-style: none; padding: 0; margin: 0; }
.gruppe-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 0.85rem;
border-radius: 8px;
background: var(--color-card);
border: 1px solid var(--color-secondary);
margin-bottom: 0.5rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
}
.gruppe-item.is-checked { border-color: var(--color-primary); }
.gruppe-item input {
accent-color: var(--color-primary);
flex-shrink: 0;
width: 14px;
height: 14px;
cursor: pointer;
}
.gruppe-item span { flex: 1; min-width: 0; }
.item-img {
width: 38px;
height: 38px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
}
.gruppe-item-name {
font-size: 0.95rem;
font-weight: 600;
color: var(--color-text);
}
.gruppe-item-desc {
display: block;
font-size: 0.8rem;
color: var(--color-muted);
margin-top: 0.15rem;
}
.empty-hint {
color: var(--color-muted);
font-size: 0.875rem;
font-style: italic;
padding: 0.5rem 0;
}
</style>
</head> </head>
<body class="app"> <body>
<div class="main"> <script>window.location.replace('/neubdsm.html');</script>
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 3 von 4 Aufgaben</p>
<div class="setup-section" id="sectionOwn">
<h2><label class="select-all-label">
<input type="checkbox" class="select-all-cb" data-list="listOwn">
Eigene Gruppen
</label></h2>
<ul class="gruppe-list" id="listOwn"></ul>
</div>
<div class="setup-section" id="sectionSubscribed">
<h2><label class="select-all-label">
<input type="checkbox" class="select-all-cb" data-list="listSubscribed">
Abonnierte Gruppen
</label></h2>
<ul class="gruppe-list" id="listSubscribed"></ul>
</div>
<div class="setup-section" id="sectionSystem">
<h2><label class="select-all-label">
<input type="checkbox" class="select-all-cb" data-list="listSystem">
System-Gruppen
</label></h2>
<ul class="gruppe-list" id="listSystem"></ul>
</div>
<div style="position:relative; margin-top:2rem;">
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmplayers.html'">← Zurück</button>
<button style="flex:2;" onclick="weiter()">Weiter</button>
</div>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
let savedGruppen = new Set();
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;
document.addEventListener('change', e => {
const cb = e.target;
if (cb.type !== 'checkbox') return;
if (cb.classList.contains('select-all-cb')) {
// Alle Gruppen in dieser Sektion (de-)selektieren
const list = document.getElementById(cb.dataset.list);
list.querySelectorAll('input[type="checkbox"]').forEach(itemCb => {
itemCb.checked = cb.checked;
itemCb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
});
} else {
// Einzelne Gruppe: is-checked-Klasse anpassen und Alles-Haken aktualisieren
cb.closest('.gruppe-item')?.classList.toggle('is-checked', cb.checked);
updateSelectAll(cb.closest('.gruppe-list'));
}
warnungsAkzeptiert = false;
hideMessage();
});
function updateSelectAll(list) {
if (!list) return;
const itemCbs = [...list.querySelectorAll('input[type="checkbox"]')];
if (!itemCbs.length) return;
const section = list.closest('.setup-section');
const selectAllCb = section?.querySelector('.select-all-cb');
if (!selectAllCb) return;
const checkedCount = itemCbs.filter(cb => cb.checked).length;
selectAllCb.checked = checkedCount === itemCbs.length;
selectAllCb.indeterminate = checkedCount > 0 && checkedCount < itemCbs.length;
}
function renderList(containerId, gruppen) {
const ul = document.getElementById(containerId);
const section = ul.closest('.setup-section');
const selectAllWrap = section?.querySelector('.select-all-label');
if (!gruppen.length) {
ul.innerHTML = '<li class="empty-hint">Keine Gruppen vorhanden.</li>';
if (selectAllWrap) selectAllWrap.style.visibility = 'hidden';
return;
}
ul.innerHTML = gruppen.map(g => {
const checked = savedGruppen.has(g.gruppenId);
return `
<li>
<label class="gruppe-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${g.gruppenId}"${checked ? ' checked' : ''}>
<span>
<span class="gruppe-item-name">${g.name}</span>
${g.beschreibung ? `<span class="gruppe-item-desc">${g.beschreibung}</span>` : ''}
</span>
${g.bild ? `<img class="item-img" src="data:image/png;base64,${g.bild}" alt="">` : ''}
</label>
</li>`;
}).join('');
updateSelectAll(ul);
}
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
function validateContent(content, settings, mitspieler) {
const errors = [], warnings = [];
const aufgabenByLevel = {};
content.aufgaben.forEach(a => {
const l = a.level ?? 0;
aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1;
});
for (const [level, count] of Object.entries(aufgabenByLevel)) {
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) Minimum 5 erforderlich`);
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben empfohlen ≥ 10`);
}
if (settings.wahrscheinlichkeitStrafe > 1) {
const strafenByLevel = {};
content.strafen.forEach(s => {
const l = s.level ?? 0;
strafenByLevel[l] = (strafenByLevel[l] || 0) + 1;
});
for (const level of Object.keys(aufgabenByLevel)) {
const count = strafenByLevel[level] || 0;
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) empfohlen ≥ 2`);
}
}
if (settings.wahrscheinlichkeitSperre > 1) {
const count = content.sperren.length;
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) empfohlen ≥ 5`);
}
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
for (const g of beteiligtGeschlecht) {
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
}
return { errors, warnings };
}
function showValidation(errors, warnings, mitHinweis) {
const el = document.getElementById('message');
el.innerHTML = [
...errors.map(e => `<div>✕ ${e}</div>`),
...warnings.map(w => `<div>⚠ ${w}</div>`),
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Weiter klicken um fortzufahren.</div>'] : []),
].join('');
el.className = `message ${errors.length ? 'error' : 'warning'}`;
el.style.display = 'block';
}
function showMessage(text, type) {
const el = document.getElementById('message');
const icon = type === 'error' ? '✕ ' : type === 'warning' ? '⚠ ' : '';
el.textContent = icon + text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
async function weiter() {
hideMessage();
const selected = [...document.querySelectorAll('.gruppe-list input[type="checkbox"]:checked')]
.map(cb => cb.value);
if (selected.length === 0) {
showMessage('Bitte mindestens eine Aufgaben-Gruppe auswählen.', 'error');
warnungsAkzeptiert = false;
return;
}
const btn = document.querySelector('button[onclick="weiter()"]');
btn.disabled = true;
const gruppen = await Promise.all(
selected.map(id => fetch(`/gruppe/${id}`).then(r => r.ok ? r.json() : null))
);
btn.disabled = false;
const content = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
gruppen.filter(Boolean).forEach(g => {
content.aufgaben.push(...(g.aufgaben || []));
content.strafen.push(...(g.strafen || []));
content.sperren.push(...(g.sperren || []));
content.finisher.push(...(g.finisher || []));
});
const settings = JSON.parse(sessionStorage.getItem('bdsm-session-settings'));
const setup = JSON.parse(sessionStorage.getItem('bdsm-session-setup'));
const { errors, warnings } = validateContent(content, settings, setup?.mitspieler || []);
if (errors.length > 0) {
showValidation(errors, warnings, false);
warnungsAkzeptiert = false;
return;
}
if (warnings.length > 0 && !warnungsAkzeptiert) {
showValidation([], warnings, true);
warnungsAkzeptiert = true;
return;
}
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';
}
(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> </body>
</html> </html>

View File

@@ -2,371 +2,10 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>BDSM Game</title>
<title>BDSM Game Toys XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.session-setup { }
.setup-section { margin-bottom: 2.5rem; }
.setup-section h2 {
color: var(--color-primary);
font-size: 1rem;
font-weight: 600;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.toy-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 0.85rem;
border-radius: 8px;
background: var(--color-card);
border: 1px solid var(--color-secondary);
margin-bottom: 0.5rem;
cursor: pointer;
transition: border-color 0.15s;
user-select: none;
}
.toy-item.is-checked { border-color: var(--color-primary); }
.toy-item input {
accent-color: var(--color-primary);
flex-shrink: 0;
width: 14px;
height: 14px;
cursor: pointer;
}
.toy-item span { flex: 1; min-width: 0; }
.item-img {
width: 38px;
height: 38px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
}
.toy-item-name { font-size: 0.95rem; font-weight: 600; color: var(--color-text); }
.toy-item-desc { display: block; font-size: 0.8rem; color: var(--color-muted); margin-top: 0.15rem; }
.no-toys { color: var(--color-muted); font-size: 0.875rem; font-style: italic; }
</style>
</head> </head>
<body class="app"> <body>
<div class="main"> <script>window.location.replace('/neubdsm.html');</script>
<div class="content session-setup">
<h1>BDSM Game</h1>
<p style="margin-bottom:2rem;">Schritt 4 von 4 Toys</p>
<div class="setup-section">
<h2>Benötigte Toys</h2>
<p style="font-size:0.85rem; color:var(--color-muted); margin-bottom:1.25rem;">
Deaktiviere Toys, die nicht zur Verfügung stehen. Aufgaben, die diese benötigen, werden nicht gespielt.
</p>
<div id="toyList"></div>
</div>
<div style="position:relative; margin-top:2rem;">
<div class="message" id="message" style="position:absolute; bottom:calc(100% + 0.5rem); left:0; right:0; margin:0;"></div>
<div style="display:flex; gap:1rem;">
<button style="flex:1;" class="secondary" onclick="window.location.href='/bdsmtasks.html'">← Zurück</button>
<button style="flex:2;" onclick="spielStarten()">Spiel starten</button>
</div>
</div>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
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');
const savedToyIds = savedToysRaw
? new Set(JSON.parse(savedToysRaw).map(t => t.toyId))
: null; // null = first visit → default all checked
// All content collected from selected groups
const allContent = { aufgaben: [], strafen: [], sperren: [], finisher: [] };
let warnungsAkzeptiert = false;
document.addEventListener('change', e => {
const cb = e.target;
if (cb.type !== 'checkbox') return;
cb.closest('.toy-item')?.classList.toggle('is-checked', cb.checked);
warnungsAkzeptiert = false;
hideMessage();
});
const GESCHLECHT_LABEL = { WEIBLICH: 'Weiblich', DIVERS: 'Divers', MAENNLICH: 'Männlich' };
function validateContent(content, settings, mitspieler) {
const errors = [], warnings = [];
const aufgabenByLevel = {};
content.aufgaben.forEach(a => {
const l = a.level ?? 0;
aufgabenByLevel[l] = (aufgabenByLevel[l] || 0) + 1;
});
for (const [level, count] of Object.entries(aufgabenByLevel)) {
if (count < 5) errors.push(`Level ${level}: Nur ${count} Aufgabe(n) Minimum 5 erforderlich`);
else if (count < 10) warnings.push(`Level ${level}: Nur ${count} Aufgaben empfohlen ≥ 10`);
}
if (settings.wahrscheinlichkeitStrafe > 1) {
const strafenByLevel = {};
content.strafen.forEach(s => {
const l = s.level ?? 0;
strafenByLevel[l] = (strafenByLevel[l] || 0) + 1;
});
for (const level of Object.keys(aufgabenByLevel)) {
const count = strafenByLevel[level] || 0;
if (count < 1) errors.push(`Level ${level}: Keine Strafe vorhanden`);
else if (count < 2) warnings.push(`Level ${level}: Nur ${count} Strafe(n) empfohlen ≥ 2`);
}
}
if (settings.wahrscheinlichkeitSperre > 1) {
const count = content.sperren.length;
if (count < 1) errors.push('Keine Zeitstrafen vorhanden');
else if (count < 5) warnings.push(`Nur ${count} Zeitstrafe(n) empfohlen ≥ 5`);
}
const beteiligtGeschlecht = [...new Set((mitspieler || []).map(p => p.geschlecht).filter(Boolean))];
for (const g of beteiligtGeschlecht) {
const count = (content.finisher || []).filter(f => f.geschlecht === g).length;
if (count < 1) errors.push(`Kein Finisher für ${GESCHLECHT_LABEL[g] || g} vorhanden`);
}
return { errors, warnings };
}
function showValidation(errors, warnings, mitHinweis) {
const el = document.getElementById('message');
el.innerHTML = [
...errors.map(e => `<div>✕ ${e}</div>`),
...warnings.map(w => `<div>⚠ ${w}</div>`),
...(mitHinweis ? ['<div style="margin-top:0.5rem;font-style:italic;">Nochmals auf Spiel starten klicken um fortzufahren.</div>'] : []),
].join('');
el.className = `message ${errors.length ? 'error' : 'warning'}`;
el.style.display = 'block';
}
function showMessage(text, type) {
const el = document.getElementById('message');
const icon = type === 'error' ? '✕ ' : type === 'warning' ? '⚠ ' : '';
el.textContent = icon + text;
el.className = `message ${type}`;
el.style.display = 'block';
}
function hideMessage() {
document.getElementById('message').style.display = 'none';
}
function renderToys(toys) {
const container = document.getElementById('toyList');
if (!toys.length) {
container.innerHTML = '<p class="no-toys">Keine Toys erforderlich alle Aufgaben können gespielt werden.</p>';
return;
}
container.innerHTML = toys.map(toy => {
const checked = savedToyIds === null || savedToyIds.has(toy.toyId);
return `
<label class="toy-item${checked ? ' is-checked' : ''}">
<input type="checkbox" value="${toy.toyId}"${checked ? ' checked' : ''}>
<span>
<span class="toy-item-name">${toy.name}</span>
${toy.beschreibung ? `<span class="toy-item-desc">${toy.beschreibung}</span>` : ''}
</span>
${toy.bild ? `<img class="item-img" src="data:image/png;base64,${toy.bild}" alt="">` : ''}
</label>`;
}).join('');
}
async function spielStarten() {
const checkedToyIds = new Set(
[...document.querySelectorAll('#toyList input[type="checkbox"]:checked')].map(cb => cb.value)
);
// Collect full toy objects for the checked ones (for name display on overview)
const toyMap = new Map();
[...allContent.aufgaben, ...allContent.strafen, ...allContent.sperren, ...allContent.finisher].forEach(item => {
(item.benoetigteToys || []).forEach(t => toyMap.set(t.toyId, t));
});
const checkedToys = [...checkedToyIds].map(id => toyMap.get(id)).filter(Boolean);
sessionStorage.setItem('bdsm-session-toys', JSON.stringify(checkedToys));
function toyOk(item) {
const toys = item.benoetigteToys || [];
return toys.length === 0 || toys.every(t => checkedToyIds.has(t.toyId));
}
const gameContent = {
aufgaben: allContent.aufgaben.filter(toyOk),
strafen: allContent.strafen.filter(toyOk),
sperren: allContent.sperren.filter(toyOk),
finisher: allContent.finisher.filter(toyOk),
};
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));
try {
// 1. Session anlegen
const sessionRes = await fetch('/bdsm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wahrscheinlichkeitStrafe: settings.wahrscheinlichkeitStrafe,
wahrscheinlichkeitSperre: settings.wahrscheinlichkeitSperre,
aufgabenProLevel: settings.aufgabenProLevel,
zeitfaktorZeitstrafen: settings.zeitfaktorZeitstrafen,
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 (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,
sperrenVorFinaleAufloesen: p.sperrenVorFinaleAufloesen !== false,
}),
});
if (!res.ok) throw new Error(`Mitspieler "${p.name}" konnte nicht hinzugefügt werden.`);
}
// 3. Aufgaben setzen
const aufgabenRes = await fetch(`/bdsm/${sessionId}/aufgaben`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(gameContent),
});
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');
btn.disabled = false;
}
}
// Load all selected groups, collect content and unique toys
(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 || []));
allContent.sperren.push(...(g.sperren || []));
allContent.finisher.push(...(g.finisher || []));
});
const toyMap = new Map();
[...allContent.aufgaben, ...allContent.strafen, ...allContent.sperren, ...allContent.finisher].forEach(item => {
(item.benoetigteToys || []).forEach(t => {
if (!toyMap.has(t.toyId)) toyMap.set(t.toyId, t);
});
});
renderToys([...toyMap.values()].sort((a, b) => a.name.localeCompare(b.name)));
})();
</script>
</body> </body>
</html> </html>

View File

@@ -2,346 +2,10 @@
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/img/icon.png" type="image/png"> <meta http-equiv="refresh" content="0;url=/neubdsm.html">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>BDSM Game</title>
<title>BDSM Game Warten XXX The Game</title>
<link rel="stylesheet" href="/css/variables.css">
<link rel="stylesheet" href="/css/style.css">
<style>
.wait-card {
text-align: center;
padding: 3rem 1rem;
}
.wait-icon { font-size: 3rem; margin-bottom: 1.5rem; animation: pulse 2s ease-in-out infinite; }
@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> </head>
<body class="app"> <body>
<div class="main"> <script>window.location.replace('/neubdsm.html');</script>
<!-- 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>
<div class="message" id="message" style="display:none;"></div>
<button class="secondary" onclick="abbrechen()">Abbrechen</button>
</div>
</div>
<script src="/js/sidebar.js"></script>
<script>
const params = new URLSearchParams(location.search);
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() {
try {
const res = await fetch(`/bdsm/einladung/${einladungId}`);
if (!res.ok) return;
const data = await res.json();
if (data.status === 'CANCELLED') {
stopPoll();
zeigeFehler('Die Einladung wurde abgebrochen.');
return;
}
if (data.sessionId) {
try {
const mRes = await fetch(`/bdsm/${data.sessionId}/mitspieler/me`);
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 (_) {}
}
} catch (_) {}
}
function stopPoll() {
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
}
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;
el.className = 'message error';
el.style.display = '';
}
async function abbrechen() {
stopPoll();
await fetch(`/bdsm/einladung/${einladungId}`, { method: 'DELETE' }).catch(() => {});
window.location.href = '/userhome.html';
}
// 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> </body>
</html> </html>

View File

@@ -837,7 +837,7 @@
closeBdsmInviteDialog(); closeBdsmInviteDialog();
removeRecvItem(key); removeRecvItem(key);
if (mode === 'OWN_DEVICE') { if (mode === 'OWN_DEVICE') {
window.location.href = `/bdsmwarten.html?id=${key}`; window.location.href = `/neubdsm.html`;
} }
} catch (_) { } catch (_) {
errEl.textContent = 'Fehler beim Speichern der Antwort.'; errEl.textContent = 'Fehler beim Speichern der Antwort.';

View File

@@ -13,7 +13,7 @@
label: 'BDSM Game', label: 'BDSM Game',
icon: '◆', icon: '◆',
items: [ items: [
{ href: '/bdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' }, { href: '/neubdsm.html', icon: '▷', label: 'Neue Session', id: 'navBdsmNeu' },
{ href: '#', icon: '⏳', label: 'Aktive Session', id: 'navBdsmAktiv' }, { href: '#', icon: '⏳', label: 'Aktive Session', id: 'navBdsmAktiv' },
{ href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' }, { href: '/bdsmingame.html', icon: '▶', label: 'Im Spiel', id: 'navBdsmImSpiel' },
{ href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' }, { href: '/aufgaben.html', icon: '✓', label: 'Aufgaben' },
@@ -118,7 +118,7 @@
navAktiv.style.display = ''; navAktiv.style.display = '';
const ziel = aktiv.sessionId const ziel = aktiv.sessionId
? '/bdsmingame.html' ? '/bdsmingame.html'
: `/bdsmwarten.html?id=${aktiv.einladungId}`; : `/neubdsm.html`;
navAktiv.querySelector('a').href = ziel; navAktiv.querySelector('a').href = ziel;
} }
} else { } else {

View File

@@ -50,14 +50,6 @@
if (e.key === 'Enter') login(); if (e.key === 'Enter') login();
}); });
async function sha256(text) {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
async function login() { async function login() {
const email = document.getElementById('email').value.trim(); const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
@@ -73,9 +65,11 @@
hideMessage(); hideMessage();
try { try {
const hash = await sha256(password); const response = await fetch('/login', {
const url = `/login?email=${encodeURIComponent(email)}&hash=${encodeURIComponent(hash)}`; method: 'POST',
const response = await fetch(url, { method: 'GET' }); headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.status === 200) { if (response.status === 200) {
const user = await response.json(); const user = await response.json();

File diff suppressed because it is too large Load Diff

View File

@@ -42,14 +42,6 @@
if (e.key === 'Enter') register(); if (e.key === 'Enter') register();
}); });
async function sha256(text) {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
async function register() { async function register() {
const name = document.getElementById('name').value.trim(); const name = document.getElementById('name').value.trim();
const email = document.getElementById('email').value.trim(); const email = document.getElementById('email').value.trim();
@@ -84,11 +76,10 @@
hideMessage(); hideMessage();
try { try {
const passwordHash = await sha256(password);
const response = await fetch('/registration', { const response = await fetch('/registration', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, passwordHash, geburtsdatum }) body: JSON.stringify({ name, email, password, geburtsdatum })
}); });
if (response.status === 202) { if (response.status === 202) {

View File

@@ -78,14 +78,6 @@
} }
}); });
async function sha256(text) {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
async function submit() { async function submit() {
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
const passwordConfirm = document.getElementById('passwordConfirm').value; const passwordConfirm = document.getElementById('passwordConfirm').value;
@@ -110,11 +102,10 @@
hideMessage(); hideMessage();
try { try {
const passwordHash = await sha256(password);
const response = await fetch('/password-reset/confirm', { const response = await fetch('/password-reset/confirm', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, passwordHash }) body: JSON.stringify({ token, password })
}); });
if (response.ok) { if (response.ok) {

View File

@@ -54,7 +54,7 @@
Tauche ein in strukturierte Sessions mit Aufgaben, Toys und klaren Rollen. Tauche ein in strukturierte Sessions mit Aufgaben, Toys und klaren Rollen.
Definiere Grenzen, vergib Aufgaben und erlebe intensive Momente mit deinem Partner. Definiere Grenzen, vergib Aufgaben und erlebe intensive Momente mit deinem Partner.
</p> </p>
<a href="/bdsm.html"><button class="game-card-btn">Neue Session starten</button></a> <a href="/neubdsm.html"><button class="game-card-btn">Neue Session starten</button></a>
</div> </div>
<div class="game-card"> <div class="game-card">