From 1094cf694810374e9fff22b08bf2380b837477c4 Mon Sep 17 00:00:00 2001
From: Kimi Work
Date: Sat, 10 Oct 2026 17:42:00 +0200
Subject: [PATCH] Red Mode: 10 FSK-18-Karten (Rotlicht), Host-Schalter im Raum,
rotes Design
---
app/src/data/game.ts | 26 ++++++++++++-
app/src/hooks/useRedMode.ts | 15 ++++++++
app/src/index.css | 22 +++++++++++
app/src/lib/api.ts | 6 ++-
app/src/pages/Cards.tsx | 64 ++++++++++++++++++++++++++----
app/src/pages/Game.tsx | 77 ++++++++++++++++++++++++++++++++++---
server/game/api/server.js | 11 ++++++
7 files changed, 205 insertions(+), 16 deletions(-)
create mode 100644 app/src/hooks/useRedMode.ts
diff --git a/app/src/data/game.ts b/app/src/data/game.ts
index 67ac2b6..dfc4e90 100644
--- a/app/src/data/game.ts
+++ b/app/src/data/game.ts
@@ -1,6 +1,6 @@
// ── Kategorien & Statement-Karten (aus dem offiziellen Spielregeln-PDF) ──
-export type KategorieId = 'abenteuer' | 'persoenlichkeit' | 'alltag' | 'party' | 'erfolg'
+export type KategorieId = 'abenteuer' | 'persoenlichkeit' | 'alltag' | 'party' | 'erfolg' | 'rotlicht'
export interface Kategorie {
id: KategorieId
@@ -59,6 +59,15 @@ export const KATEGORIEN: Kategorie[] = [
ring: 'ring-emerald-400/40',
chip: 'bg-emerald-400/15 text-emerald-300 border-emerald-400/30',
},
+ {
+ id: 'rotlicht',
+ titel: 'Rotlicht & Versuchung 🔞',
+ emoji: '🌶️',
+ farbe: 'from-red-500 to-rose-700',
+ textfarbe: 'text-red-300',
+ ring: 'ring-red-400/40',
+ chip: 'bg-red-400/15 text-red-300 border-red-400/30',
+ },
]
export const kategorieById = (id: KategorieId): Kategorie =>
@@ -96,8 +105,23 @@ export const KARTEN: Spielkarte[] = [
{ id: 18, kategorie: 'erfolg', text: 'Wer wäre der beste Kandidat für eine Reality-TV-Show?' },
{ id: 19, kategorie: 'erfolg', text: 'Wer könnte am ehesten eine 30-minütige Präsentation über ein Thema halten, von dem er eigentlich gar keine Ahnung hat?' },
{ id: 20, kategorie: 'erfolg', text: 'Wer hat das Potenzial, ein berühmter Geheimagent zu sein, weil er am besten Geheimnisse bewahren kann?' },
+ // Rotlicht & Versuchung (Red Mode / FSK 18)
+ { id: 21, kategorie: 'rotlicht', text: 'Wer aus der Gruppe würde am ehesten einen One-Night-Stand über eine Dating-App haben?' },
+ { id: 22, kategorie: 'rotlicht', text: 'Wer würde bei „Wahrheit oder Pflicht“ drei Mal hintereinander „Pflicht“ wählen – egal, was kommt?' },
+ { id: 23, kategorie: 'rotlicht', text: 'Wer würde am ehesten heimlich in der hintersten Reihe des Kinos rummachen?' },
+ { id: 24, kategorie: 'rotlicht', text: 'Wer würde nach einer durchzechten Nacht als Erster den Namen seines Dates vergessen?' },
+ { id: 25, kategorie: 'rotlicht', text: 'Wer würde am ehesten einen Strip-Poker-Abend anzetteln – und zwar gewinnen?' },
+ { id: 26, kategorie: 'rotlicht', text: 'Wer würde am ehesten mit dem Ex eines guten Freunds anbändeln und es vertuschen?' },
+ { id: 27, kategorie: 'rotlicht', text: 'Wer hat die Stimme, mit der man am Telefon am ehesten flirten könnte?' },
+ { id: 28, kategorie: 'rotlicht', text: 'Wer würde in einem Club am ehesten auf der Theke tanzen – und es auch noch gut finden?' },
+ { id: 29, kategorie: 'rotlicht', text: 'Wer würde am ehesten den ganzen Abend mit einem Fremden flirten, nur um einen Drink abzugreifen?' },
+ { id: 30, kategorie: 'rotlicht', text: 'Wer sagt „Ich melde mich noch“ – und meint damit die nächsten drei Jahre?' },
]
+/** FSK-18-Karten – nur im Red Mode verfügbar */
+export const ROT_KARTEN: Spielkarte[] = KARTEN.filter((k) => k.kategorie === 'rotlicht')
+export const KARTEN_OHNE_ROT: Spielkarte[] = KARTEN.filter((k) => k.kategorie !== 'rotlicht')
+
// ── Spielregeln (Inhalt aus dem offiziellen PDF) ──────────
export interface RegelSchritt {
diff --git a/app/src/hooks/useRedMode.ts b/app/src/hooks/useRedMode.ts
new file mode 100644
index 0000000..9ff7126
--- /dev/null
+++ b/app/src/hooks/useRedMode.ts
@@ -0,0 +1,15 @@
+import { useEffect } from 'react'
+
+/** Hängt die Design-Klasse red-mode an und passt theme-color an. */
+export function useRedMode(aktiv: boolean) {
+ useEffect(() => {
+ document.body.classList.toggle('red-mode', aktiv)
+ const meta = document.querySelector('meta[name="theme-color"]')
+ const vorher = meta?.getAttribute('content')
+ if (meta) meta.setAttribute('content', aktiv ? '#1f060b' : '#170a2e')
+ return () => {
+ document.body.classList.remove('red-mode')
+ if (meta && vorher) meta.setAttribute('content', vorher)
+ }
+ }, [aktiv])
+}
diff --git a/app/src/index.css b/app/src/index.css
index e88c60d..8b66e30 100644
--- a/app/src/index.css
+++ b/app/src/index.css
@@ -136,6 +136,28 @@
animation: glanzen 3.5s linear infinite;
}
+/* ── Red Mode (FSK 18): rote Design-Welt ────────────────── */
+body.red-mode {
+ background-image:
+ radial-gradient(ellipse 80% 50% at 50% -10%, rgba(239, 68, 68, 0.32), transparent),
+ radial-gradient(ellipse 60% 40% at 100% 0%, rgba(244, 63, 94, 0.22), transparent),
+ radial-gradient(ellipse 60% 40% at 0% 100%, rgba(190, 18, 60, 0.2), transparent),
+ linear-gradient(180deg, #1f060b 0%, #2a0710 55%, #1a0409 100%);
+}
+body.red-mode .text-gradient {
+ background-image: linear-gradient(to right, #f87171, #fb7185, #f472b6);
+}
+body.red-mode nav.fixed {
+ background-color: rgb(38 8 14 / 0.88);
+ border-color: rgba(248, 113, 113, 0.18);
+}
+body.red-mode .glanz {
+ background-image: linear-gradient(90deg, #ef4444, #e11d48, #be123c, #e11d48, #ef4444) !important;
+}
+body.red-mode .glass-card {
+ border-color: rgba(248, 113, 113, 0.14);
+}
+
/* Zahlen-Chips der Hero-Sektion */
.zahlen-chip {
@apply flex h-9 w-9 items-center justify-center rounded-xl font-display text-lg font-semibold shadow-lg;
diff --git a/app/src/lib/api.ts b/app/src/lib/api.ts
index 6100b50..d93a0d1 100644
--- a/app/src/lib/api.ts
+++ b/app/src/lib/api.ts
@@ -32,6 +32,7 @@ export interface RaumZustand {
spieler: SpielerInfo[]
minSpieler: number
maxSpieler: number
+ redMode: boolean
alleReady: boolean
rankingsFertig: number
punkte: Record
@@ -79,11 +80,11 @@ async function anfrage(methode: 'GET' | 'POST', pfad: string, body?: object, tok
}
/** Raum eröffnen; liefert Session direkt mit */
-export async function raumErstellen(name: string): Promise<{ session: RaumSession; zustand: RaumZustand }> {
+export async function raumErstellen(name: string, redMode = false): Promise<{ session: RaumSession; zustand: RaumZustand }> {
const res = await fetch('/api/game/room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name }),
+ body: JSON.stringify({ name, redMode }),
})
const daten = await res.json().catch(() => ({}))
if (!res.ok || !daten.token) throw new ApiFehler(daten.fehler || 'Raum konnte nicht erstellt werden')
@@ -108,6 +109,7 @@ export async function raumBeitreten(code: string, name: string): Promise<{ sessi
export const zustandLaden = (s: RaumSession) => anfrage('GET', `/${s.code}`, undefined, s.token)
export const readySetzen = (s: RaumSession, ready: boolean) => anfrage('POST', `/${s.code}/ready`, { ready }, s.token)
+export const redModeSetzen = (s: RaumSession, redMode: boolean) => anfrage('POST', `/${s.code}/redmode`, { redMode }, s.token)
export const frageStellen = (s: RaumSession, frage: string) => anfrage('POST', `/${s.code}/question`, { frage }, s.token)
export const rankingAbgeben = (s: RaumSession, reihenfolge: string[]) =>
anfrage('POST', `/${s.code}/ranking`, { reihenfolge }, s.token)
diff --git a/app/src/pages/Cards.tsx b/app/src/pages/Cards.tsx
index 76a2c82..9cfaa69 100644
--- a/app/src/pages/Cards.tsx
+++ b/app/src/pages/Cards.tsx
@@ -1,6 +1,9 @@
import { useMemo, useState } from 'react'
-import { RefreshCw, Shuffle } from 'lucide-react'
-import { KARTEN, KATEGORIEN, kategorieById, type KategorieId, type Spielkarte } from '@/data/game'
+import { Flame, RefreshCw, Shuffle } from 'lucide-react'
+import { KARTEN, KATEGORIEN, KARTEN_OHNE_ROT, kategorieById, type KategorieId, type Spielkarte } from '@/data/game'
+import { useRedMode } from '@/hooks/useRedMode'
+
+const REDMODE_KEY = 'einsbisacht-redmode'
type Filter = 'alle' | KategorieId
@@ -47,10 +50,24 @@ export default function Cards() {
const [filter, setFilter] = useState('alle')
const [gezogen, setGezogen] = useState(null)
const [zaehler, setZaehler] = useState(0) // triggert Re-Flip-Animation
+ const [redMode, setRedMode] = useState(() => localStorage.getItem(REDMODE_KEY) === '1')
+ useRedMode(redMode)
+
+ const toggleRedMode = () => {
+ setRedMode((alt) => {
+ localStorage.setItem(REDMODE_KEY, alt ? '0' : '1')
+ return !alt
+ })
+ }
const stapel = useMemo(
- () => (filter === 'alle' ? KARTEN : KARTEN.filter((k) => k.kategorie === filter)),
- [filter]
+ () =>
+ filter === 'alle'
+ ? redMode
+ ? KARTEN
+ : KARTEN_OHNE_ROT
+ : KARTEN.filter((k) => k.kategorie === filter),
+ [filter, redMode]
)
const ziehen = () => {
@@ -71,15 +88,48 @@ export default function Cards() {
Runde haben soll! 🎨
+ {/* Red Mode Toggle */}
+
+
{/* Kategorie-Filter */}
-
+
setFilter('alle')} label="✨ Alle" />
- {KATEGORIEN.map((k) => (
+ {KATEGORIEN.filter((k) => k.id !== 'rotlicht' || redMode).map((k) => (
setFilter(k.id)}
- label={`${k.emoji} ${k.titel.split(' ')[0]}`}
+ label={k.id === 'rotlicht' ? `🌶️ Rotlicht` : `${k.emoji} ${k.titel.split(' ')[0]}`}
chipKlasse={k.chip}
/>
))}
diff --git a/app/src/pages/Game.tsx b/app/src/pages/Game.tsx
index 06ca687..930e07d 100644
--- a/app/src/pages/Game.tsx
+++ b/app/src/pages/Game.tsx
@@ -14,7 +14,8 @@ import {
Trophy,
Users,
} from 'lucide-react'
-import { KARTEN } from '@/data/game'
+import { KARTEN, KARTEN_OHNE_ROT } from '@/data/game'
+import { useRedMode } from '@/hooks/useRedMode'
import {
ladeSession,
speichereSession,
@@ -22,6 +23,7 @@ import {
raumBeitreten,
zustandLaden,
readySetzen,
+ redModeSetzen,
frageStellen,
rankingAbgeben,
naechstePhase,
@@ -34,6 +36,8 @@ export default function Game() {
const [session, setSession] = useState(ladeSession)
const [zustand, setZustand] = useState(null)
const [fehler, setFehler] = useState('')
+ // Red Mode des Raums verdunkelt das Design (nur während des Spiels)
+ useRedMode(!!zustand?.redMode)
const refresh = useCallback(async () => {
if (!session) return
@@ -66,8 +70,8 @@ export default function Game() {
setSession(s)
}, [])
- const erstellen = useCallback(async (name: string) => {
- const { session: s } = await raumErstellen(name)
+ const erstellen = useCallback(async (name: string, redMode: boolean) => {
+ const { session: s } = await raumErstellen(name, redMode)
setSession(s)
}, [])
@@ -142,12 +146,13 @@ function Einstieg({
onErstellen,
onBeitreten,
}: {
- onErstellen: (name: string) => Promise
+ onErstellen: (name: string, redMode: boolean) => Promise
onBeitreten: (code: string, name: string) => Promise
}) {
const [modus, setModus] = useState<'start' | 'beitreten'>('start')
const [name, setName] = useState('')
const [code, setCode] = useState(() => new URLSearchParams(window.location.search).get('raum') ?? '')
+ const [redMode, setRedMode] = useState(false)
const [lade, setLade] = useState(false)
const [fehler, setFehler] = useState('')
@@ -157,7 +162,7 @@ function Einstieg({
setLade(true)
setFehler('')
try {
- if (modus === 'start') await onErstellen(name.trim())
+ if (modus === 'start') await onErstellen(name.trim(), redMode)
else await onBeitreten(code.trim(), name.trim())
} catch (e) {
setFehler(e instanceof Error ? e.message : 'Verbindung fehlgeschlagen')
@@ -207,6 +212,30 @@ function Einstieg({
className="w-full rounded-xl border border-white/10 bg-white/[0.06] px-3.5 py-3 text-[15px] font-bold text-white placeholder:font-normal placeholder:text-white/30 focus:border-pink-400/60 focus:outline-none focus:ring-2 focus:ring-pink-400/20"
/>
+ {modus === 'start' && (
+
+ )}
{fehler && {fehler}
}
+
+ {/* Red Mode: Host schaltet frei, alle sehen den Status */}
+
+ {!ich?.istHost && (
+
Nur der Host kann den Red Mode ändern.
+ )}
{z.spieler.map((s) => (
@@ -333,7 +392,8 @@ function FragePhase({ z, onAct }: { z: RaumZustand; onAct: (fn: (s: RaumSession)
const istFragesteller = z.ich === z.fragesteller
const ziehen = () => {
- const karte = KARTEN[Math.floor(Math.random() * KARTEN.length)]
+ const stapel = z.redMode ? KARTEN : KARTEN_OHNE_ROT
+ const karte = stapel[Math.floor(Math.random() * stapel.length)]
setFrage(karte.text)
}
@@ -354,6 +414,11 @@ function FragePhase({ z, onAct }: { z: RaumZustand; onAct: (fn: (s: RaumSession)
Du bist dran!
Ziehe eine Karte und lies sie laut vor – oder erfinde eigene Frage.
+ {z.redMode && (
+
+ 🌶️ Red Mode – freche Karten sind im Stapel
+
+ )}