Red Mode: 10 FSK-18-Karten (Rotlicht), Host-Schalter im Raum, rotes Design
Build & Deploy / deploy (push) Successful in 19s
Build & Deploy / deploy (push) Successful in 19s
This commit is contained in:
+25
-1
@@ -1,6 +1,6 @@
|
|||||||
// ── Kategorien & Statement-Karten (aus dem offiziellen Spielregeln-PDF) ──
|
// ── 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 {
|
export interface Kategorie {
|
||||||
id: KategorieId
|
id: KategorieId
|
||||||
@@ -59,6 +59,15 @@ export const KATEGORIEN: Kategorie[] = [
|
|||||||
ring: 'ring-emerald-400/40',
|
ring: 'ring-emerald-400/40',
|
||||||
chip: 'bg-emerald-400/15 text-emerald-300 border-emerald-400/30',
|
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 =>
|
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: 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: 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?' },
|
{ 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) ──────────
|
// ── Spielregeln (Inhalt aus dem offiziellen PDF) ──────────
|
||||||
|
|
||||||
export interface RegelSchritt {
|
export interface RegelSchritt {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
/** Hängt die Design-Klasse red-mode an <body> 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])
|
||||||
|
}
|
||||||
@@ -136,6 +136,28 @@
|
|||||||
animation: glanzen 3.5s linear infinite;
|
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-Chips der Hero-Sektion */
|
||||||
.zahlen-chip {
|
.zahlen-chip {
|
||||||
@apply flex h-9 w-9 items-center justify-center rounded-xl font-display text-lg font-semibold shadow-lg;
|
@apply flex h-9 w-9 items-center justify-center rounded-xl font-display text-lg font-semibold shadow-lg;
|
||||||
|
|||||||
+4
-2
@@ -32,6 +32,7 @@ export interface RaumZustand {
|
|||||||
spieler: SpielerInfo[]
|
spieler: SpielerInfo[]
|
||||||
minSpieler: number
|
minSpieler: number
|
||||||
maxSpieler: number
|
maxSpieler: number
|
||||||
|
redMode: boolean
|
||||||
alleReady: boolean
|
alleReady: boolean
|
||||||
rankingsFertig: number
|
rankingsFertig: number
|
||||||
punkte: Record<string, number>
|
punkte: Record<string, number>
|
||||||
@@ -79,11 +80,11 @@ async function anfrage(methode: 'GET' | 'POST', pfad: string, body?: object, tok
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Raum eröffnen; liefert Session direkt mit */
|
/** 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', {
|
const res = await fetch('/api/game/room', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name, redMode }),
|
||||||
})
|
})
|
||||||
const daten = await res.json().catch(() => ({}))
|
const daten = await res.json().catch(() => ({}))
|
||||||
if (!res.ok || !daten.token) throw new ApiFehler(daten.fehler || 'Raum konnte nicht erstellt werden')
|
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 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 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 frageStellen = (s: RaumSession, frage: string) => anfrage('POST', `/${s.code}/question`, { frage }, s.token)
|
||||||
export const rankingAbgeben = (s: RaumSession, reihenfolge: string[]) =>
|
export const rankingAbgeben = (s: RaumSession, reihenfolge: string[]) =>
|
||||||
anfrage('POST', `/${s.code}/ranking`, { reihenfolge }, s.token)
|
anfrage('POST', `/${s.code}/ranking`, { reihenfolge }, s.token)
|
||||||
|
|||||||
+57
-7
@@ -1,6 +1,9 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { RefreshCw, Shuffle } from 'lucide-react'
|
import { Flame, RefreshCw, Shuffle } from 'lucide-react'
|
||||||
import { KARTEN, KATEGORIEN, kategorieById, type KategorieId, type Spielkarte } from '@/data/game'
|
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
|
type Filter = 'alle' | KategorieId
|
||||||
|
|
||||||
@@ -47,10 +50,24 @@ export default function Cards() {
|
|||||||
const [filter, setFilter] = useState<Filter>('alle')
|
const [filter, setFilter] = useState<Filter>('alle')
|
||||||
const [gezogen, setGezogen] = useState<Spielkarte | null>(null)
|
const [gezogen, setGezogen] = useState<Spielkarte | null>(null)
|
||||||
const [zaehler, setZaehler] = useState(0) // triggert Re-Flip-Animation
|
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(
|
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 = () => {
|
const ziehen = () => {
|
||||||
@@ -71,15 +88,48 @@ export default function Cards() {
|
|||||||
Runde haben soll! 🎨
|
Runde haben soll! 🎨
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Red Mode Toggle */}
|
||||||
|
<button
|
||||||
|
onClick={toggleRedMode}
|
||||||
|
className={`mt-4 flex w-full items-center justify-between rounded-2xl border p-3.5 transition-all active:scale-[0.98] ${
|
||||||
|
redMode
|
||||||
|
? 'border-red-400/50 bg-red-500/10 shadow-[0_0_20px_rgba(239,68,68,0.25)]'
|
||||||
|
: 'border-white/10 bg-white/[0.05]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2.5">
|
||||||
|
<Flame size={19} className={redMode ? 'text-red-400' : 'text-white/40'} />
|
||||||
|
<span className="text-left">
|
||||||
|
<span className={`block text-sm font-bold ${redMode ? 'text-red-300' : 'text-white/75'}`}>
|
||||||
|
🔞 Red Mode <span className="font-extrabold">(FSK 18)</span>
|
||||||
|
</span>
|
||||||
|
<span className="block text-xs text-white/45">
|
||||||
|
{redMode ? 'Aktiviert – 10 extra freche Karten im Stapel' : 'Schaltet 10 freche Karten frei'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`relative h-7 w-12 shrink-0 rounded-full transition-colors ${
|
||||||
|
redMode ? 'bg-gradient-to-r from-red-500 to-rose-600' : 'bg-white/15'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-1 h-5 w-5 rounded-full bg-white shadow transition-all ${
|
||||||
|
redMode ? 'left-6' : 'left-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Kategorie-Filter */}
|
{/* Kategorie-Filter */}
|
||||||
<div className="scrollbar-none -mx-5 mt-5 flex gap-2 overflow-x-auto px-5 pb-1">
|
<div className="scrollbar-none -mx-5 mt-4 flex gap-2 overflow-x-auto px-5 pb-1">
|
||||||
<FilterChip aktiv={filter === 'alle'} onClick={() => setFilter('alle')} label="✨ Alle" />
|
<FilterChip aktiv={filter === 'alle'} onClick={() => setFilter('alle')} label="✨ Alle" />
|
||||||
{KATEGORIEN.map((k) => (
|
{KATEGORIEN.filter((k) => k.id !== 'rotlicht' || redMode).map((k) => (
|
||||||
<FilterChip
|
<FilterChip
|
||||||
key={k.id}
|
key={k.id}
|
||||||
aktiv={filter === k.id}
|
aktiv={filter === k.id}
|
||||||
onClick={() => setFilter(k.id)}
|
onClick={() => setFilter(k.id)}
|
||||||
label={`${k.emoji} ${k.titel.split(' ')[0]}`}
|
label={k.id === 'rotlicht' ? `🌶️ Rotlicht` : `${k.emoji} ${k.titel.split(' ')[0]}`}
|
||||||
chipKlasse={k.chip}
|
chipKlasse={k.chip}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+71
-6
@@ -14,7 +14,8 @@ import {
|
|||||||
Trophy,
|
Trophy,
|
||||||
Users,
|
Users,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { KARTEN } from '@/data/game'
|
import { KARTEN, KARTEN_OHNE_ROT } from '@/data/game'
|
||||||
|
import { useRedMode } from '@/hooks/useRedMode'
|
||||||
import {
|
import {
|
||||||
ladeSession,
|
ladeSession,
|
||||||
speichereSession,
|
speichereSession,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
raumBeitreten,
|
raumBeitreten,
|
||||||
zustandLaden,
|
zustandLaden,
|
||||||
readySetzen,
|
readySetzen,
|
||||||
|
redModeSetzen,
|
||||||
frageStellen,
|
frageStellen,
|
||||||
rankingAbgeben,
|
rankingAbgeben,
|
||||||
naechstePhase,
|
naechstePhase,
|
||||||
@@ -34,6 +36,8 @@ export default function Game() {
|
|||||||
const [session, setSession] = useState<RaumSession | null>(ladeSession)
|
const [session, setSession] = useState<RaumSession | null>(ladeSession)
|
||||||
const [zustand, setZustand] = useState<RaumZustand | null>(null)
|
const [zustand, setZustand] = useState<RaumZustand | null>(null)
|
||||||
const [fehler, setFehler] = useState('')
|
const [fehler, setFehler] = useState('')
|
||||||
|
// Red Mode des Raums verdunkelt das Design (nur während des Spiels)
|
||||||
|
useRedMode(!!zustand?.redMode)
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (!session) return
|
if (!session) return
|
||||||
@@ -66,8 +70,8 @@ export default function Game() {
|
|||||||
setSession(s)
|
setSession(s)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const erstellen = useCallback(async (name: string) => {
|
const erstellen = useCallback(async (name: string, redMode: boolean) => {
|
||||||
const { session: s } = await raumErstellen(name)
|
const { session: s } = await raumErstellen(name, redMode)
|
||||||
setSession(s)
|
setSession(s)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -142,12 +146,13 @@ function Einstieg({
|
|||||||
onErstellen,
|
onErstellen,
|
||||||
onBeitreten,
|
onBeitreten,
|
||||||
}: {
|
}: {
|
||||||
onErstellen: (name: string) => Promise<void>
|
onErstellen: (name: string, redMode: boolean) => Promise<void>
|
||||||
onBeitreten: (code: string, name: string) => Promise<void>
|
onBeitreten: (code: string, name: string) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
const [modus, setModus] = useState<'start' | 'beitreten'>('start')
|
const [modus, setModus] = useState<'start' | 'beitreten'>('start')
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [code, setCode] = useState(() => new URLSearchParams(window.location.search).get('raum') ?? '')
|
const [code, setCode] = useState(() => new URLSearchParams(window.location.search).get('raum') ?? '')
|
||||||
|
const [redMode, setRedMode] = useState(false)
|
||||||
const [lade, setLade] = useState(false)
|
const [lade, setLade] = useState(false)
|
||||||
const [fehler, setFehler] = useState('')
|
const [fehler, setFehler] = useState('')
|
||||||
|
|
||||||
@@ -157,7 +162,7 @@ function Einstieg({
|
|||||||
setLade(true)
|
setLade(true)
|
||||||
setFehler('')
|
setFehler('')
|
||||||
try {
|
try {
|
||||||
if (modus === 'start') await onErstellen(name.trim())
|
if (modus === 'start') await onErstellen(name.trim(), redMode)
|
||||||
else await onBeitreten(code.trim(), name.trim())
|
else await onBeitreten(code.trim(), name.trim())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setFehler(e instanceof Error ? e.message : 'Verbindung fehlgeschlagen')
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
</Feld>
|
</Feld>
|
||||||
|
{modus === 'start' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setRedMode(!redMode)}
|
||||||
|
className={`flex w-full items-center justify-between rounded-xl border px-3.5 py-3 transition-all active:scale-[0.98] ${
|
||||||
|
redMode ? 'border-red-400/50 bg-red-500/10' : 'border-white/10 bg-white/[0.04]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2 text-sm font-bold text-white/80">
|
||||||
|
🌶️ Red Mode <span className="text-red-300">🔞 FSK 18</span>
|
||||||
|
<span className="text-xs font-normal text-white/45">– freche Karten im Spiel</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`relative h-6 w-10 shrink-0 rounded-full transition-colors ${
|
||||||
|
redMode ? 'bg-gradient-to-r from-red-500 to-rose-600' : 'bg-white/15'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-all ${
|
||||||
|
redMode ? 'left-5' : 'left-0.5'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{fehler && <p className="text-sm font-bold text-rose-300">{fehler}</p>}
|
{fehler && <p className="text-sm font-bold text-rose-300">{fehler}</p>}
|
||||||
<button
|
<button
|
||||||
onClick={los}
|
onClick={los}
|
||||||
@@ -290,6 +319,36 @@ function Lobby({
|
|||||||
</h3>
|
</h3>
|
||||||
<span className="text-xs font-bold text-white/45">Mind. {z.minSpieler}</span>
|
<span className="text-xs font-bold text-white/45">Mind. {z.minSpieler}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Red Mode: Host schaltet frei, alle sehen den Status */}
|
||||||
|
<button
|
||||||
|
onClick={() => ich?.istHost && onAct((s) => redModeSetzen(s, !z.redMode))}
|
||||||
|
disabled={!ich?.istHost}
|
||||||
|
className={`mb-3 flex w-full items-center justify-between rounded-xl border px-3.5 py-2.5 text-left transition-all ${
|
||||||
|
z.redMode ? 'border-red-400/50 bg-red-500/10' : 'border-white/10 bg-white/[0.04]'
|
||||||
|
} ${ich?.istHost ? 'active:scale-[0.98]' : 'opacity-80'}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2 text-sm font-bold text-white/80">
|
||||||
|
🌶️ Red Mode <span className="text-red-300">🔞 FSK 18</span>
|
||||||
|
<span className="text-xs font-normal text-white/45">
|
||||||
|
{z.redMode ? 'AN – freche Karten sind im Spiel' : 'aus – klassische Karten'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`relative h-6 w-10 shrink-0 rounded-full transition-colors ${
|
||||||
|
z.redMode ? 'bg-gradient-to-r from-red-500 to-rose-600' : 'bg-white/15'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-all ${
|
||||||
|
z.redMode ? 'left-5' : 'left-0.5'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{!ich?.istHost && (
|
||||||
|
<p className="-mt-2 mb-3 text-right text-[11px] text-white/35">Nur der Host kann den Red Mode ändern.</p>
|
||||||
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{z.spieler.map((s) => (
|
{z.spieler.map((s) => (
|
||||||
<div key={s.name} className="flex items-center justify-between rounded-xl bg-white/[0.05] px-3.5 py-2.5">
|
<div key={s.name} className="flex items-center justify-between rounded-xl bg-white/[0.05] px-3.5 py-2.5">
|
||||||
@@ -333,7 +392,8 @@ function FragePhase({ z, onAct }: { z: RaumZustand; onAct: (fn: (s: RaumSession)
|
|||||||
const istFragesteller = z.ich === z.fragesteller
|
const istFragesteller = z.ich === z.fragesteller
|
||||||
|
|
||||||
const ziehen = () => {
|
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)
|
setFrage(karte.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +414,11 @@ function FragePhase({ z, onAct }: { z: RaumZustand; onAct: (fn: (s: RaumSession)
|
|||||||
<div className="glass-card p-5 text-center">
|
<div className="glass-card p-5 text-center">
|
||||||
<p className="text-xs font-extrabold uppercase tracking-[0.25em] text-violet-300">Du bist dran!</p>
|
<p className="text-xs font-extrabold uppercase tracking-[0.25em] text-violet-300">Du bist dran!</p>
|
||||||
<p className="mt-1 text-sm text-white/60">Ziehe eine Karte und lies sie laut vor – oder erfinde eigene Frage.</p>
|
<p className="mt-1 text-sm text-white/60">Ziehe eine Karte und lies sie laut vor – oder erfinde eigene Frage.</p>
|
||||||
|
{z.redMode && (
|
||||||
|
<p className="mt-2 inline-block rounded-full border border-red-400/40 bg-red-500/10 px-3 py-1 text-xs font-bold text-red-300">
|
||||||
|
🌶️ Red Mode – freche Karten sind im Stapel
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="glass-card space-y-3 p-4">
|
<div className="glass-card space-y-3 p-4">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ function oeffentlicherZustand(raum, token) {
|
|||||||
})),
|
})),
|
||||||
minSpieler: MIN_SPIELER,
|
minSpieler: MIN_SPIELER,
|
||||||
maxSpieler: MAX_SPIELER,
|
maxSpieler: MAX_SPIELER,
|
||||||
|
redMode: !!raum.redMode,
|
||||||
alleReady,
|
alleReady,
|
||||||
rankingsFertig: Object.keys(raum.rankings).length,
|
rankingsFertig: Object.keys(raum.rankings).length,
|
||||||
punkte: raum.punkte,
|
punkte: raum.punkte,
|
||||||
@@ -211,6 +212,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
status: 'lobby',
|
status: 'lobby',
|
||||||
runde: 0,
|
runde: 0,
|
||||||
frage: '',
|
frage: '',
|
||||||
|
redMode: !!body.redMode,
|
||||||
spieler: [{ name, token: crypto.randomBytes(18).toString('hex'), ready: false, istHost: true }],
|
spieler: [{ name, token: crypto.randomBytes(18).toString('hex'), ready: false, istHost: true }],
|
||||||
rankings: {},
|
rankings: {},
|
||||||
punkte: { [name]: 0 },
|
punkte: { [name]: 0 },
|
||||||
@@ -260,6 +262,15 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return antwort(res, 200, { ok: true, zustand: oeffentlicherZustand(raum, token) });
|
return antwort(res, 200, { ok: true, zustand: oeffentlicherZustand(raum, token) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Red Mode umschalten (nur Host, nur vor Spielstart) */
|
||||||
|
if (req.method === 'POST' && aktion === 'redmode') {
|
||||||
|
if (raum.status !== 'lobby') return fehler(res, 409, 'Red Mode ist nur vor Spielstart aenderbar');
|
||||||
|
if (!spieler.istHost) return fehler(res, 403, 'Nur der Host kann den Red Mode aendern');
|
||||||
|
raum.redMode = !!body.redMode;
|
||||||
|
speichern();
|
||||||
|
return antwort(res, 200, { ok: true, zustand: oeffentlicherZustand(raum, token) });
|
||||||
|
}
|
||||||
|
|
||||||
/* Frage stellen */
|
/* Frage stellen */
|
||||||
if (req.method === 'POST' && aktion === 'question') {
|
if (req.method === 'POST' && aktion === 'question') {
|
||||||
if (raum.status !== 'question') return fehler(res, 409, 'Keine Fragephase');
|
if (raum.status !== 'question') return fehler(res, 409, 'Keine Fragephase');
|
||||||
|
|||||||
Reference in New Issue
Block a user