Files
dickendock/frontend/src/tools/schocken.jsx

687 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback } from 'react';
import { api, S } from '../lib.js';
// Testmodus: API-Call als anderer User (nur Admin, nur Schocken)
async function apiAs(userId, path, opts = {}) {
const token = localStorage.getItem('sk_token');
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` };
if (userId) headers['X-Schocken-Test-As'] = String(userId);
const res = await fetch(`/api${path}`, {
method: opts.method || (opts.body ? 'POST' : 'GET'),
headers,
body: opts.body ? JSON.stringify(opts.body) : undefined,
});
if (!res.ok) { const e = await res.json().catch(()=>({error:'Fehler'})); throw new Error(e.error||'Fehler'); }
return res.json();
}
const MY_COLOR = '#4ecdc4';
function getMyId() {
try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).id; } catch { return null; }
}
function getMyRole() {
try { return JSON.parse(atob(localStorage.getItem('sk_token').split('.')[1])).role; } catch { return null; }
}
// ── Würfel-Visualisierung ─────────────────────────────────────────────────────
const DOTS = {
1: [[50,50]],
2: [[25,25],[75,75]],
3: [[25,25],[50,50],[75,75]],
4: [[25,25],[75,25],[25,75],[75,75]],
5: [[25,25],[75,25],[50,50],[25,75],[75,75]],
6: [[25,25],[75,25],[25,50],[75,50],[25,75],[75,75]],
};
function Die({ value, kept, selected, onClick, dark, size=56 }) {
const dots = value ? DOTS[value] : [];
const bg = dark ? 'rgba(40,40,60,0.9)' : kept ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.08)';
const border = selected ? '2px solid #4ecdc4' : kept ? '2px solid rgba(78,205,196,0.4)' : '1px solid rgba(255,255,255,0.15)';
return (
<div onClick={onClick} style={{
width:size, height:size, borderRadius:10, background:bg, border,
cursor:onClick?'pointer':'default', position:'relative', flexShrink:0,
transition:'all 0.15s', boxShadow: selected ? '0 0 12px rgba(78,205,196,0.4)' : 'none',
}}>
{dark && <div style={{position:'absolute',inset:0,display:'flex',alignItems:'center',justifyContent:'center',color:'rgba(255,255,255,0.2)',fontSize:22}}>🎲</div>}
{!dark && dots.map(([x,y],i) => (
<div key={i} style={{
position:'absolute',
left:`${x}%`, top:`${y}%`,
transform:'translate(-50%,-50%)',
width:size>40?10:7, height:size>40?10:7,
borderRadius:'50%', background:'#fff',
}}/>
))}
{!value && !dark && <div style={{position:'absolute',inset:0,display:'flex',alignItems:'center',justifyContent:'center',color:'rgba(255,255,255,0.15)',fontSize:10,fontFamily:'monospace'}}>?</div>}
</div>
);
}
// ── Spieler-Zeile ─────────────────────────────────────────────────────────────
function PlayerRow({ player, chips, has16th, isMe, isCurrent, result, drinkLosses, game }) {
const isLoserH1 = game.has_16th === player.id;
return (
<div style={{
...S.card, padding:'10px 14px', marginBottom:8,
border: isCurrent ? '1px solid rgba(78,205,196,0.4)' : '1px solid rgba(255,255,255,0.07)',
background: isMe ? 'rgba(78,205,196,0.04)' : 'rgba(255,255,255,0.02)',
}}>
<div style={{display:'flex',alignItems:'center',gap:10}}>
<div style={{flex:1}}>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:13,display:'flex',alignItems:'center',gap:6}}>
{isCurrent && <span style={{color:MY_COLOR}}></span>}
<strong>{player.username}</strong>
{isMe && <span style={{color:'rgba(255,255,255,0.3)',fontSize:10}}>(du)</span>}
{isLoserH1 && <span title="Verlierer 1. Hälfte" style={{fontSize:14}}>🪶</span>}
</div>
</div>
<div style={{display:'flex',alignItems:'center',gap:12}}>
{drinkLosses > 0 && (
<span style={{color:'#f59e0b',fontFamily:'monospace',fontSize:11}}>🍺×{drinkLosses}</span>
)}
<div style={{textAlign:'center'}}>
<div style={{color:chips>0?'#ff6b9d':MY_COLOR,fontFamily:'Space Mono,monospace',fontSize:18,fontWeight:700,lineHeight:1}}>{chips}</div>
<div style={{color:'rgba(255,255,255,0.25)',fontSize:9,fontFamily:'monospace'}}>Scheiben</div>
</div>
</div>
</div>
{result && (
<div style={{marginTop:6,padding:'4px 8px',background:'rgba(255,255,255,0.04)',borderRadius:6,
color:MY_COLOR,fontFamily:'monospace',fontSize:11}}>
{result.label}
</div>
)}
</div>
);
}
// ── Würfelbereich ─────────────────────────────────────────────────────────────
function DiceArea({ game, myId, onRoll, onReady, onEvaluate }) {
const [selectedKeep, setSelectedKeep] = useState([]);
const [goDark, setGoDark] = useState(false);
const [flipSixes, setFlipSixes] = useState(false);
const [rolling, setRolling] = useState(false);
const round = game.current_round;
const players = game.players;
const activePlayers = getActivePlayers(game);
const myState = round?.playerStates?.[myId];
const currentPlayer = activePlayers[game.current_player_idx % activePlayers.length];
const isMyTurn = currentPlayer?.id === myId;
const allReady = activePlayers.every(p => round?.ready?.[p.id]);
const allDone = round?.phase === 'reveal';
const isFirstRound = !!game.first_round;
const myDice = myState?.dice || [null, null, null];
const myRolls = myState?.roll_count || 0;
const maxRolls = round?.max_rolls || 3;
const canFlipSixes = myDice.filter(d => d===6).length >= 2 && myRolls < maxRolls - 1;
// Becher umdrehen Phase — warte bis alle ready
if (!round || !allReady) {
const iMReady = round?.ready?.[myId];
const readyCount = Object.keys(round?.ready || {}).length;
const activeCount = activePlayers.length;
return (
<div>
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:12,textAlign:'center',marginBottom:12}}>
{readyCount}/{activeCount} haben den Becher umgedreht
</div>
{!iMReady && (
<button onClick={onReady} style={{...S.btn('#4ecdc4'),width:'100%',fontSize:14,padding:'12px'}}>
🎲 Becher umdrehen
</button>
)}
{iMReady && (
<div style={{color:MY_COLOR,fontFamily:'monospace',fontSize:12,textAlign:'center',padding:'12px 0'}}>
Becher umgedreht warte auf andere
</div>
)}
{isFirstRound && iMReady && readyCount === activeCount && (
<div style={{color:'rgba(255,255,255,0.5)',fontFamily:'monospace',fontSize:11,textAlign:'center',marginTop:8}}>
Alle bereit Becher werden gleichzeitig gehoben!
</div>
)}
</div>
);
}
// Auswertungsphase — alle Würfel sichtbar
if (allDone) {
const isFirstPlayer = activePlayers[0]?.id === myId || players[0]?.id === myId;
return (
<div>
<div style={{color:MY_COLOR,fontFamily:'monospace',fontSize:12,textAlign:'center',marginBottom:14}}>
🎲 Alle Becher oben wer hat was?
</div>
{/* Alle Spieler mit Würfeln und Ergebnis */}
{activePlayers.map(p => {
const ps = round.playerStates?.[p.id];
const isP = p.id === myId;
return (
<div key={p.id} style={{
...S.card, padding:'10px 14px', marginBottom:8,
border: isP ? '1px solid rgba(78,205,196,0.3)' : '1px solid rgba(255,255,255,0.07)',
}}>
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:8}}>
<span style={{color:isP?MY_COLOR:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:12,fontWeight:700}}>
{p.username}{isP?' (du)':''}
</span>
{ps?.dark && <span style={{color:'rgba(255,255,255,0.4)',fontSize:10,fontFamily:'monospace'}}>🌑 war dunkel</span>}
{ps?.roll_count && <span style={{color:'rgba(255,255,255,0.3)',fontSize:10,fontFamily:'monospace',marginLeft:'auto'}}>{ps.roll_count}. Wurf</span>}
</div>
<div style={{display:'flex',gap:8,marginBottom:8}}>
{(ps?.dice||[null,null,null]).map((d,i) => <Die key={i} value={d} size={44}/>)}
</div>
{ps?.result && (
<div style={{
padding:'4px 10px', borderRadius:6, display:'inline-block',
background: ps.result.type==='schock_aus'?'rgba(239,68,68,0.15)':
ps.result.type==='julchen'?'rgba(255,230,109,0.15)':
ps.result.type==='general'?'rgba(78,205,196,0.12)':'rgba(255,255,255,0.05)',
border: ps.result.type==='schock_aus'?'1px solid rgba(239,68,68,0.4)':
ps.result.type==='julchen'?'1px solid rgba(255,230,109,0.4)':
ps.result.type==='general'?'1px solid rgba(78,205,196,0.3)':'1px solid rgba(255,255,255,0.08)',
color: ps.result.type==='schock_aus'?'#ef4444':
ps.result.type==='julchen'?'#ffe66d':
ps.result.type==='general'?MY_COLOR:'rgba(255,255,255,0.6)',
fontFamily:'monospace', fontSize:11,
}}>
{ps.result.label} {ps.result.scheiben > 0 ? `${ps.result.scheiben} Scheibe${ps.result.scheiben!==1?'n':''}` : ''}
</div>
)}
</div>
);
})}
{isFirstPlayer && (
<button onClick={onEvaluate} style={{...S.btn('#ffe66d'),width:'100%',marginTop:4}}>
Scheiben verteilen
</button>
)}
{!isFirstPlayer && (
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,textAlign:'center',padding:'8px 0'}}>
Warte auf {activePlayers[0]?.username} zum Auswerten
</div>
)}
</div>
);
}
// Mein Würfeln
return (
<div>
{/* Status */}
<div style={{color:isMyTurn?MY_COLOR:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:11,textAlign:'center',marginBottom:10}}>
{isMyTurn ? `▶ Dein Wurf (${myRolls}/${maxRolls||'?'})` : `${currentPlayer?.username} würfelt…`}
</div>
{/* Würfel */}
<div style={{display:'flex',gap:10,justifyContent:'center',marginBottom:14}}>
{myDice.map((d, i) => (
<Die
key={i}
value={d}
dark={myState?.dark}
kept={selectedKeep.includes(i)}
selected={selectedKeep.includes(i)}
onClick={isMyTurn && myRolls > 0 && !myState?.done ? () => {
setSelectedKeep(prev =>
prev.includes(i) ? prev.filter(x=>x!==i) : [...prev,i]
);
} : undefined}
size={64}
/>
))}
</div>
{/* Optionen */}
{isMyTurn && !myState?.done && (
<div style={{display:'flex',flexDirection:'column',gap:8}}>
{myRolls > 0 && (
<div style={{display:'flex',gap:8,fontSize:11,fontFamily:'monospace',color:'rgba(255,255,255,0.5)'}}>
{selectedKeep.length > 0 && <span>📌 {selectedKeep.length} Würfel stehen lassen</span>}
</div>
)}
{canFlipSixes && (
<label style={{display:'flex',alignItems:'center',gap:8,cursor:'pointer',fontFamily:'monospace',fontSize:11,color:'rgba(255,255,255,0.6)'}}>
<input type="checkbox" checked={flipSixes} onChange={e=>setFlipSixes(e.target.checked)}/>
Zwei Sechsen zu Einer Eins umdrehen
</label>
)}
{myRolls > 0 && myRolls < (maxRolls||3) && (
<label style={{display:'flex',alignItems:'center',gap:8,cursor:'pointer',fontFamily:'monospace',fontSize:11,color:'rgba(255,255,255,0.6)'}}>
<input type="checkbox" checked={goDark} onChange={e=>setGoDark(e.target.checked)}/>
Dunkel legen (Becher nicht heben)
</label>
)}
<button
disabled={rolling}
onClick={async () => {
setRolling(true);
try { await onRoll(selectedKeep, flipSixes, goDark); setSelectedKeep([]); setFlipSixes(false); setGoDark(false); }
finally { setRolling(false); }
}}
style={{...S.btn(MY_COLOR), padding:'10px', fontSize:13}}
>
{rolling ? '🎲 Würfle…' : myRolls === 0 ? '🎲 Becher heben' : goDark ? '🎲 Dunkel würfeln' : '🎲 Nochmal würfeln'}
</button>
</div>
)}
{myState?.done && !allDone && (
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:11,textAlign:'center',padding:'8px 0'}}>
{myState.dark ? '🎲 Du bist dunkel — warte auf andere…' : `✓ Du stehst im ${myRolls}. — warte auf andere…`}
</div>
)}
{/* Andere Spieler Status */}
<div style={{marginTop:12,display:'flex',flexWrap:'wrap',gap:6}}>
{activePlayers.filter(p => p.id !== myId).map(p => {
const ps = round.playerStates?.[p.id];
return (
<div key={p.id} style={{
padding:'4px 10px', borderRadius:20,
background: ps?.done ? 'rgba(78,205,196,0.1)' : 'rgba(255,255,255,0.04)',
border: ps?.done ? '1px solid rgba(78,205,196,0.3)' : '1px solid rgba(255,255,255,0.08)',
color: ps?.done ? MY_COLOR : 'rgba(255,255,255,0.4)',
fontFamily:'monospace', fontSize:10,
}}>
{ps?.done ? '✓' : '⏳'} {p.username} {ps?.dark && '🌑'}
</div>
);
})}
</div>
</div>
);
}
// ── Schockstock ───────────────────────────────────────────────────────────────
function Schockstock({ total=15, stock, players, chips }) {
const used = total - stock;
return (
<div style={{...S.card,padding:'10px 14px',marginBottom:12}}>
<div style={{display:'flex',alignItems:'center',gap:8,marginBottom:8}}>
<span style={{color:'rgba(255,255,255,0.55)',fontFamily:'monospace',fontSize:11,letterSpacing:1}}>SCHOCKSTOCK</span>
<span style={{color:MY_COLOR,fontFamily:'monospace',fontSize:11,marginLeft:'auto'}}>{stock} verbleibend</span>
</div>
<div style={{display:'flex',gap:3,flexWrap:'wrap'}}>
{Array.from({length:16}).map((_,i) => {
const is16th = i === 15;
const isUsed = !is16th && i >= stock;
return (
<div key={i} style={{
width:14, height:14, borderRadius:'50%',
background: is16th
? 'rgba(245,158,11,0.6)'
: isUsed
? 'rgba(255,255,255,0.1)'
: '#4ecdc4',
border: is16th ? '1px solid #f59e0b' : '1px solid rgba(255,255,255,0.1)',
title: is16th ? '16. Scheibe' : '',
}}/>
);
})}
</div>
</div>
);
}
// ── Neue Runde Button ─────────────────────────────────────────────────────────
function NewRoundButton({ game, myId, onReady }) {
// Der Beginner der neuen Runde startet mit Becher umdrehen
const isStarter = game.beginner_id === myId;
return (
<div style={{...S.card,padding:'14px',textAlign:'center',marginBottom:12,border:'1px solid rgba(78,205,196,0.2)'}}>
<div style={{color:'rgba(255,255,255,0.6)',fontFamily:'monospace',fontSize:12,marginBottom:10}}>
Neue Runde {isStarter ? 'du fängst an!' : `${game.players.find(p=>p.id===game.beginner_id)?.username} fängt an`}
</div>
<button onClick={onReady} style={{...S.btn('#4ecdc4'),width:'100%',fontSize:14,padding:'12px'}}>
🎲 Becher umdrehen
</button>
</div>
);
}
// ── Spielende ─────────────────────────────────────────────────────────────────
function GameOver({ game }) {
const dl = game.drink_losses;
const loser = game.players.find(p => dl[p.id] > 0);
const isDoppelfeige = game.loser_h1 === game.loser_h2;
return (
<div style={{...S.card,padding:'20px',textAlign:'center',border:'1px solid rgba(245,158,11,0.3)'}}>
<div style={{fontSize:48,marginBottom:12}}>{isDoppelfeige ? '🪶🪶' : '🍺'}</div>
{isDoppelfeige ? (
<div style={{color:'#f59e0b',fontFamily:'Space Mono,monospace',fontSize:16,fontWeight:700,marginBottom:8}}>
DOPPELFEIGE!
</div>
) : (
<div style={{color:'#f59e0b',fontFamily:'Space Mono,monospace',fontSize:16,fontWeight:700,marginBottom:8}}>
RUNDE VORBEI
</div>
)}
{loser && (
<div style={{color:'rgba(255,255,255,0.7)',fontFamily:'monospace',fontSize:13,marginBottom:16}}>
{loser.username} zahlt die nächste Runde! 🍺
</div>
)}
<div style={{marginTop:12}}>
<div style={{...S.head,marginBottom:8}}>GETRÄNKE-STATISTIK</div>
{game.players.map(p => (
<div key={p.id} style={{display:'flex',justifyContent:'space-between',padding:'4px 0',
borderBottom:'1px solid rgba(255,255,255,0.05)',fontFamily:'monospace',fontSize:12}}>
<span style={{color:'rgba(255,255,255,0.7)'}}>{p.username}</span>
<span style={{color:dl[p.id]>0?'#f59e0b':MY_COLOR}}>🍺 {dl[p.id]||0}</span>
</div>
))}
</div>
</div>
);
}
// ── Aktive Spieler berechnen (Frontend) ───────────────────────────────────────
function getActivePlayers(game) {
const { players, player_chips, stock, status, loser_h1, loser_h2 } = game;
if (status === 'endkampf') {
return players.filter(p => p.id === loser_h1 || p.id === loser_h2);
}
if (stock > 0) return players;
return players.filter(p => player_chips[p.id] > 0);
}
// ── Neues Spiel Modal ─────────────────────────────────────────────────────────
function NewGameModal({ onClose, onCreate, toast }) {
const [users, setUsers] = useState([]);
const [picked, setPicked] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => { api('/tools/schocken/users').then(setUsers).catch(()=>{}); }, []);
const toggle = id => setPicked(p => p.includes(id) ? p.filter(x=>x!==id) : [...p,id]);
const create = async () => {
if (!picked.length) return;
setLoading(true);
try {
const r = await api('/tools/schocken', { body: { player_ids: picked } });
toast('Spiel erstellt! 🎲');
onCreate(r.id);
} catch(e) { toast(e.message||'Fehler','error'); }
finally { setLoading(false); }
};
return (
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.85)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:1000,padding:20}}>
<div style={{...S.card,maxWidth:380,width:'100%'}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:18}}>
<span style={{color:'#fff',fontFamily:'Space Mono,monospace',fontSize:13,fontWeight:700,letterSpacing:1}}>NEUES SPIEL</span>
<button onClick={onClose} style={S.btn('#666666',true)}></button>
</div>
<div style={{...S.head,marginBottom:8}}>MITSPIELER WÄHLEN</div>
<div style={{maxHeight:240,overflowY:'auto',marginBottom:16}}>
{users.map(u => (
<div key={u.id} onClick={()=>toggle(u.id)} style={{
display:'flex',alignItems:'center',gap:10,padding:'8px 10px',
cursor:'pointer',borderRadius:8,marginBottom:4,
background: picked.includes(u.id) ? 'rgba(78,205,196,0.1)' : 'rgba(255,255,255,0.03)',
border: picked.includes(u.id) ? '1px solid rgba(78,205,196,0.3)' : '1px solid rgba(255,255,255,0.06)',
}}>
<span style={{color:picked.includes(u.id)?MY_COLOR:'rgba(255,255,255,0.5)',fontSize:14}}>
{picked.includes(u.id)?'☑':'☐'}
</span>
<span style={{color:'#fff',fontFamily:'monospace',fontSize:13}}>{u.username}</span>
</div>
))}
</div>
<div style={{display:'flex',gap:8}}>
<button onClick={onClose} style={{...S.btn('#666666'),flex:1}}>Abbrechen</button>
<button onClick={create} disabled={!picked.length||loading} style={{...S.btn('#4ecdc4'),flex:1}}>
{loading ? 'Erstelle…' : `Starten (${picked.length+1} Spieler)`}
</button>
</div>
</div>
</div>
);
}
// ── Spielliste ────────────────────────────────────────────────────────────────
function GameList({ games, myId, onSelect, onNew, onDelete, isAdmin }) {
const active = games.filter(g => g.status !== 'finished');
const finished = games.filter(g => g.status === 'finished');
const statusLabel = s => ({
lobby:'Lobby', half1:'1. Hälfte', half2:'2. Hälfte', endkampf:'Endkampf', finished:'Beendet'
}[s]||s);
const GameRow = ({ g }) => {
const others = g.players.filter(p=>p.id!==myId).map(p=>p.username).join(', ');
return (
<div style={{...S.card,padding:'12px 14px',marginBottom:8,display:'flex',alignItems:'center',gap:10,border:'1px solid rgba(255,255,255,0.07)'}}>
<button onClick={()=>onSelect(g.id)} style={{flex:1,background:'none',border:'none',cursor:'pointer',textAlign:'left',padding:0}}>
<div style={{color:'#fff',fontFamily:'monospace',fontSize:13,marginBottom:3}}>
🎲 mit <strong>{others}</strong>
</div>
<div style={{color:'rgba(255,255,255,0.35)',fontSize:11,fontFamily:'monospace'}}>
{g.players.length} Spieler · {statusLabel(g.status)}
</div>
</button>
{isAdmin && (
<button onClick={async e => {
e.stopPropagation();
if (!window.confirm('Spiel löschen?')) return;
try {
await apiAs(myId, `/tools/schocken/${g.id}/cancel`, { body:{} });
onDelete(g.id);
} catch(e) { alert(e.message||'Fehler'); }
}} style={{...S.btn('#ff6b9d',true),flexShrink:0}}>🗑</button>
)}
</div>
);
};
return (
<div>
<button onClick={onNew} style={{...S.btn('#4ecdc4'),width:'100%',marginBottom:20,padding:'10px 14px',fontSize:13}}>
+ Neues Spiel starten
</button>
{active.length > 0 && <>
<div style={{...S.head,marginBottom:8}}>LAUFEND ({active.length})</div>
{active.map(g=><GameRow key={g.id} g={g}/>)}
</>}
{finished.length > 0 && <>
<div style={{...S.head,marginTop:16,marginBottom:8}}>BEENDET</div>
{finished.map(g=><GameRow key={g.id} g={g}/>)}
</>}
{games.length===0 && (
<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:13,textAlign:'center',paddingTop:40}}>
Noch keine Spiele starte eines!
</div>
)}
</div>
);
}
// ── Spielansicht ──────────────────────────────────────────────────────────────
function GameView({ game, myId, onRefresh, toast, isAdmin=false, onSeatChange }) {
const players = game.players;
const chips = game.player_chips;
const dl = game.drink_losses;
const round = game.current_round;
const activePlayers = getActivePlayers(game);
const currentPlayer = activePlayers[game.current_player_idx % activePlayers.length];
const handleRoll = async (keepDice, flipSixes, goDark) => {
await apiAs(myId, `/tools/schocken/${game.id}/roll`, {
body: { keep_dice: keepDice, flip_sixes: flipSixes, go_dark: goDark }
});
onRefresh();
};
const handleReady = async () => {
await apiAs(myId, `/tools/schocken/${game.id}/ready`, { body: {} });
onRefresh();
};
const handleEvaluate = async () => {
const result = await apiAs(myId, `/tools/schocken/${game.id}/evaluate`, { body: {} });
if (result.round_result?.eventMsg) toast(result.round_result.eventMsg);
onRefresh();
};
const handleStart = async () => {
await apiAs(myId, `/tools/schocken/${game.id}/start`, { body: {} });
onRefresh();
};
const statusLabel = { lobby:'Lobby', half1:'1. Hälfte', half2:'2. Hälfte', endkampf:'Endkampf', finished:'Beendet' };
return (
<div>
{/* Phase-Header */}
<div style={{...S.card,padding:'8px 14px',marginBottom:12,display:'flex',alignItems:'center',gap:10}}>
<span style={{color:MY_COLOR,fontFamily:'monospace',fontSize:11,letterSpacing:1}}>{statusLabel[game.status]}</span>
<span style={{flex:1}}/>
<span style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:10}}>Runde {game.round_number}</span>
</div>
{/* Testmodus-Switcher (nur Admin) */}
{isAdmin && (
<div style={{marginBottom:12,padding:'8px 12px',background:'rgba(245,158,11,0.08)',border:'1px solid rgba(245,158,11,0.25)',borderRadius:8}}>
<div style={{color:'#f59e0b',fontFamily:'monospace',fontSize:10,letterSpacing:1,marginBottom:6}}>🧪 TESTMODUS SPIELER WECHSELN</div>
<div style={{display:'flex',gap:6,flexWrap:'wrap'}}>
{players.map(p => (
<button key={p.id} onClick={() => onSeatChange(p.id)}
style={{...S.btn(myId===p.id?'#f59e0b':'#666666',true),fontSize:10}}>
{p.username}{myId===p.id?' ✓':''}
</button>
))}
</div>
</div>
)}
{/* Schockstock */}
<Schockstock stock={game.stock} players={players} chips={chips}/>
{/* Spieler */}
<div style={{marginBottom:12}}>
{players.map(p => (
<PlayerRow
key={p.id}
player={p}
chips={chips[p.id]||0}
has16th={game.has_16th}
isMe={p.id===myId}
isCurrent={currentPlayer?.id===p.id}
result={round?.playerStates?.[p.id]?.result}
drinkLosses={dl[p.id]||0}
game={game}
/>
))}
</div>
{/* Lobby */}
{game.status==='lobby' && (
<div style={{textAlign:'center'}}>
{players[0]?.id===myId ? (
<button onClick={handleStart} style={{...S.btn('#4ecdc4'),width:'100%',fontSize:14,padding:'12px'}}>
🎲 Spiel starten
</button>
) : (
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12,padding:'20px 0'}}>
Warte auf {players[0]?.username} um das Spiel zu starten
</div>
)}
</div>
)}
{/* Spielfeld */}
{game.status!=='lobby' && game.status!=='finished' && (
<div style={{...S.card,padding:'14px',border:'1px solid rgba(78,205,196,0.15)'}}>
<DiceArea
game={game}
myId={myId}
onRoll={handleRoll}
onReady={handleReady}
onEvaluate={handleEvaluate}
/>
</div>
)}
{/* Spielende */}
{game.status==='finished' && <GameOver game={game}/>}
</div>
);
}
// ── Hauptkomponente ───────────────────────────────────────────────────────────
export default function Schocken({ toast }) {
const [games, setGames] = useState([]);
const [activeId, setActiveId] = useState(null);
const [game, setGame] = useState(null);
const [showNew, setShowNew] = useState(false);
const [loading, setLoading] = useState(true);
const [testSeat, setTestSeat] = useState(null); // null = eigener Account, sonst userId
const myRealId = getMyId();
const isAdmin = getMyRole() === 'admin';
// Im Testmodus: als anderen Spieler agieren
const myId = (isAdmin && testSeat) ? testSeat : myRealId;
const loadGames = useCallback(async () => {
try { setGames(await api('/tools/schocken')); }
catch { toast?.('Fehler beim Laden','error'); }
finally { setLoading(false); }
}, []);
const loadGame = useCallback(async () => {
if (!activeId) { setGame(null); return; }
try { setGame(await api(`/tools/schocken/${activeId}`)); }
catch {}
}, [activeId]);
useEffect(() => { loadGames(); }, [loadGames]);
useEffect(() => { loadGame(); }, [loadGame]);
// Polling wenn Spiel aktiv
useEffect(() => {
if (!activeId) return;
const iv = setInterval(loadGame, 5000);
return () => clearInterval(iv);
}, [activeId, loadGame]);
if (loading) return <div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',padding:40,textAlign:'center'}}>Lade</div>;
return (
<div style={{maxWidth:560,margin:'0 auto'}}>
<div style={{display:'flex',alignItems:'center',gap:10,marginBottom:24,flexWrap:'wrap'}}>
{activeId && (
<button onClick={()=>{setActiveId(null);setGame(null);loadGames();}} style={S.btn('#666666',true)}> Zurück</button>
)}
<h2 style={{margin:0,fontSize:15,fontFamily:'monospace',color:'rgba(255,255,255,0.55)',letterSpacing:2,fontWeight:400}}>
🎲 SCHOCKEN
</h2>
<div style={{flex:1}}/>
{activeId && game?.status !== 'finished' && isAdmin && (
<button onClick={async () => {
if (!window.confirm('Spiel wirklich abbrechen? Keine Punkte werden vergeben.')) return;
try {
await apiAs(myRealId, `/tools/schocken/${activeId}/cancel`, { body:{} });
setActiveId(null); setGame(null); loadGames();
toast('Spiel abgebrochen.');
} catch(e) { toast(e.message||'Fehler','error'); }
}} style={S.btn('#ff6b9d', true)}> Abbrechen</button>
)}
</div>
{!activeId
? <GameList games={games} myId={myId} onSelect={setActiveId} onNew={()=>setShowNew(true)} onDelete={id=>setGames(prev=>prev.filter(g=>g.id!==id))} isAdmin={isAdmin}/>
: game && myId
? <GameView game={game} myId={myId} onRefresh={loadGame} toast={toast} isAdmin={isAdmin} onSeatChange={id=>{setTestSeat(id);}} />
: <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',textAlign:'center',paddingTop:40}}>Lade</div>
}
{showNew && <NewGameModal onClose={()=>setShowNew(false)} toast={toast} onCreate={id=>{setShowNew(false);setActiveId(id);loadGames();}}/>}
</div>
);
}