629 lines
27 KiB
JavaScript
629 lines
27 KiB
JavaScript
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
|
||
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:12}}>
|
||
Alle haben gewürfelt — Ergebnis auswerten!
|
||
</div>
|
||
{isFirstPlayer && (
|
||
<button onClick={onEvaluate} style={{...S.btn('#ffe66d'),width:'100%'}}>
|
||
✓ Runde auswerten & Scheiben verteilen
|
||
</button>
|
||
)}
|
||
</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 }) {
|
||
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 (
|
||
<button onClick={()=>onSelect(g.id)} style={{
|
||
...S.card,width:'100%',textAlign:'left',cursor:'pointer',
|
||
padding:'12px 14px',marginBottom:8,display:'flex',alignItems:'center',gap:10,
|
||
border:'1px solid rgba(255,255,255,0.07)',
|
||
}}>
|
||
<div style={{flex:1}}>
|
||
<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
|
||
</div>
|
||
</div>
|
||
<span style={{color:g.status==='finished'?'rgba(255,255,255,0.3)':MY_COLOR,fontSize:11,fontFamily:'monospace',flexShrink:0}}>
|
||
{statusLabel(g.status)}
|
||
</span>
|
||
</button>
|
||
);
|
||
};
|
||
|
||
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>
|
||
|
||
{!activeId
|
||
? <GameList games={games} myId={myId} onSelect={setActiveId} onNew={()=>setShowNew(true)}/>
|
||
: 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>
|
||
);
|
||
}
|