feat: Hex Wars - Kundschaften, Mine-Alert auto-clear, Spielliste live
This commit is contained in:
@@ -141,9 +141,11 @@ function Leaderboard({ reloadKey }) {
|
||||
}
|
||||
|
||||
// ── Spielfeld ─────────────────────────────────────────────────────────────────
|
||||
function GameBoard({ game, myId, onMove, toast }) {
|
||||
const [pending, setPending] = useState(null);
|
||||
const [moving, setMoving] = useState(false);
|
||||
function GameBoard({ game, myId, onMove, onScout, toast }) {
|
||||
const [pending, setPending] = useState(null);
|
||||
const [moving, setMoving] = useState(false);
|
||||
const [scoutMode, setScoutMode] = useState(false); // Scout-Modus aktiv
|
||||
const [scoutPend, setScoutPend] = useState(null); // gewählter Scout-Mittelpunkt
|
||||
const [mineAlert, setMineAlert] = useState(null); // letztes Mine-Event
|
||||
|
||||
const grid = JSON.parse(game.grid);
|
||||
@@ -158,14 +160,18 @@ function GameBoard({ game, myId, onMove, toast }) {
|
||||
|
||||
// Mine-Event anzeigen wenn frisch
|
||||
const [blastHighlight, setBlastHighlight] = useState(new Set());
|
||||
const [alertShownAt, setAlertShownAt] = useState(null); // move_count beim Anzeigen
|
||||
|
||||
useEffect(() => {
|
||||
if (!game.last_event) return;
|
||||
if (!game.last_event) {
|
||||
setMineAlert(null); // Event wurde vom Server geclearet (nächster Zug)
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const ev = JSON.parse(game.last_event);
|
||||
if (ev.type === 'mine') {
|
||||
setMineAlert(ev);
|
||||
// Blast-Zellen kurz highlighten
|
||||
setAlertShownAt(game.move_count);
|
||||
if (ev.blastCells?.length) {
|
||||
const keys = new Set(ev.blastCells.map(([r,c]) => `${r}-${c}`));
|
||||
setBlastHighlight(keys);
|
||||
@@ -173,7 +179,7 @@ function GameBoard({ game, myId, onMove, toast }) {
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}, [game.last_event]);
|
||||
}, [game.last_event, game.move_count]);
|
||||
|
||||
// Schlangen-Köpfe aus Server-Response
|
||||
const myHeadStr = game.myHead;
|
||||
@@ -190,12 +196,11 @@ function GameBoard({ game, myId, onMove, toast }) {
|
||||
};
|
||||
|
||||
const canClick = (r, c) => {
|
||||
if (scoutMode) return false; // im Scout-Modus kein normaler Zug
|
||||
if (!isMyTurn || game.status !== 'active') return false;
|
||||
if (grid[r][c] !== 0) return false;
|
||||
if (hasTerrain && terrain[r][c] === T_ROCK) return false;
|
||||
// Schlangen-Regel: nur Felder an den Kopf
|
||||
if (myHead) return isAdjacentToHead(r, c);
|
||||
// Fallback für alte Spiele ohne head
|
||||
return DIRS8.some(([dr,dc]) => {
|
||||
const nr=r+dr, nc=c+dc;
|
||||
return nr>=0&&nr<GRID&&nc>=0&&nc<GRID&&grid[nr][nc]===myId;
|
||||
@@ -256,14 +261,34 @@ function GameBoard({ game, myId, onMove, toast }) {
|
||||
}, []);
|
||||
|
||||
// Zell-Rendering
|
||||
const handleScoutClick = (r, c) => {
|
||||
if (!scoutPend) return;
|
||||
setScoutPend({ row:r, col:c, confirmed: true });
|
||||
};
|
||||
|
||||
const confirmScout = async () => {
|
||||
if (!scoutPend || moving) return;
|
||||
setMoving(true);
|
||||
try {
|
||||
await onScout(scoutPend.row, scoutPend.col);
|
||||
setScoutMode(false);
|
||||
setScoutPend(null);
|
||||
} catch(e) {
|
||||
toast?.(e.message || 'Fehler', 'error');
|
||||
} finally { setMoving(false); }
|
||||
};
|
||||
|
||||
const renderCell = (r, c) => {
|
||||
const cell = grid[r][c];
|
||||
const t = hasTerrain ? terrain[r][c] : T_NORMAL;
|
||||
const clic = canClick(r, c);
|
||||
const isPend = pending?.row === r && pending?.col === c;
|
||||
const isBlast = blastHighlight.has(`${r}-${c}`);
|
||||
const isHead = myHR === r && myHC === c; // eigener Kopf
|
||||
const isOppHead = game.status === 'finished' && oppHR === r && oppHC === c; // Gegner-Kopf nur bei Spielende
|
||||
const isHead = myHR === r && myHC === c;
|
||||
const isOppHead = game.status === 'finished' && oppHR === r && oppHC === c;
|
||||
// Scout-Preview: 3x3 um Maus-Position im Scout-Modus
|
||||
const isScoutPreview = scoutPend &&
|
||||
Math.abs(r - scoutPend.row) <= 1 && Math.abs(c - scoutPend.col) <= 1;
|
||||
const isFog = t === T_FOG;
|
||||
const isRock = t === T_ROCK;
|
||||
const isGold = t === T_GOLD;
|
||||
@@ -293,14 +318,17 @@ function GameBoard({ game, myId, onMove, toast }) {
|
||||
|
||||
if (isBlast) { bg = '#ff440066'; border = '2px solid #ff4400'; }
|
||||
if (isOppHead) { border = `2px solid ${oppColor}`; }
|
||||
if (isHead && !isPend) { border = `2px solid ${myColor}`; } // eigener Kopf
|
||||
if (isScoutPreview && !isHead) { bg = `rgba(255,230,109,0.2)`; border = `1px solid rgba(255,230,109,0.5)`; }
|
||||
if (isHead && !isPend) { border = `2px solid ${myColor}`; }
|
||||
if (isPend) { bg = myColor; border = `2px solid ${myColor}`; }
|
||||
else if (clic && !isPend && !isBlast && !isHead) border = `1px solid ${myColor}44`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${r}-${c}`}
|
||||
onClick={() => handleCellClick(r, c)}
|
||||
onClick={() => scoutMode ? setScoutPend({row:r,col:c}) : handleCellClick(r, c)}
|
||||
onMouseEnter={() => scoutMode ? setScoutPend({row:r,col:c}) : setHover(`${r}-${c}`)}
|
||||
onMouseLeave={() => { if(!scoutMode) setHover(null); }}
|
||||
style={{
|
||||
width:cellSize, height:cellSize, background:bg,
|
||||
borderRadius:1, cursor:clic?'pointer':'default',
|
||||
@@ -362,12 +390,35 @@ function GameBoard({ game, myId, onMove, toast }) {
|
||||
⏳ {oppName} ist dran…
|
||||
</div>
|
||||
)}
|
||||
{isMyTurn && !pending && (
|
||||
<div style={{ color:myColor, fontFamily:'monospace', fontSize:11, textAlign:'center', padding:'4px 0' }}>
|
||||
▶ Baue vom Kopf aus — Vorsicht vor Minen! 💣
|
||||
{isMyTurn && !pending && !scoutMode && (
|
||||
<div style={{ display:'flex', alignItems:'center', gap:6 }}>
|
||||
<div style={{ color:myColor, fontFamily:'monospace', fontSize:11, flex:1, textAlign:'center', padding:'4px 0' }}>
|
||||
▶ Baue vom Kopf aus — Vorsicht vor Minen! 💣
|
||||
</div>
|
||||
<button onClick={() => { setScoutMode(true); setPending(null); }}
|
||||
style={{ ...S.btn('#ffe66d', true), fontSize:10, flexShrink:0 }} title="3x3 Bereich aufdecken (kostet Zug)">
|
||||
🔭 Kundschaften
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{pending && (
|
||||
{isMyTurn && scoutMode && !scoutPend && (
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8, background:'rgba(255,230,109,0.1)', border:'1px solid rgba(255,230,109,0.4)', borderRadius:8, padding:'7px 10px' }}>
|
||||
<span style={{ color:'#ffe66d', fontFamily:'monospace', fontSize:11, flex:1 }}>
|
||||
🔭 Klicke einen Mittelpunkt zum Aufdecken (3×3)
|
||||
</span>
|
||||
<button onClick={() => { setScoutMode(false); setScoutPend(null); }} style={S.btn('#666666', true)}>✕ Abbrechen</button>
|
||||
</div>
|
||||
)}
|
||||
{isMyTurn && scoutMode && scoutPend && (
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8, background:'rgba(255,230,109,0.1)', border:'1px solid rgba(255,230,109,0.44)', borderRadius:8, padding:'7px 10px' }}>
|
||||
<span style={{ color:'#ffe66d', fontFamily:'monospace', fontSize:11, flex:1 }}>
|
||||
🔭 R{scoutPend.row+1} S{scoutPend.col+1} aufdecken — kostet einen Zug
|
||||
</span>
|
||||
<button onClick={() => setScoutPend(null)} style={S.btn('#666666', true)}>✕</button>
|
||||
<button onClick={confirmScout} disabled={moving} style={S.btn('#ffe66d', true)}>{moving ? '…' : '✓ Aufdecken'}</button>
|
||||
</div>
|
||||
)}
|
||||
{pending && !scoutMode && (
|
||||
<div style={{ display:'flex', alignItems:'center', gap:8, background:`${myColor}12`, border:`1px solid ${myColor}44`, borderRadius:8, padding:'7px 10px' }}>
|
||||
<span style={{ color:myColor, fontFamily:'monospace', fontSize:11, flex:1 }}>
|
||||
✦ R{pending.row+1} S{pending.col+1} — bestätigen?
|
||||
@@ -504,7 +555,13 @@ export default function HexWars({ toast }) {
|
||||
const handleMove = async (row, col) => {
|
||||
const updated = await api(`/tools/gebietseroberung/${activeId}/move`, { body: { row, col } });
|
||||
setGame(updated);
|
||||
setGames(prev => prev.map(g => g.id === updated.id ? { ...g, ...updated } : g));
|
||||
loadGames(); // Spielliste aktualisieren damit current_turn stimmt
|
||||
};
|
||||
|
||||
const handleScout = async (row, col) => {
|
||||
const updated = await api(`/tools/gebietseroberung/${activeId}/scout`, { body: { row, col } });
|
||||
setGame(updated);
|
||||
loadGames();
|
||||
};
|
||||
|
||||
const handleResign = async () => {
|
||||
@@ -547,7 +604,7 @@ export default function HexWars({ toast }) {
|
||||
{!activeId
|
||||
? <GameList games={games} myId={myId} isAdmin={isAdmin} onSelect={setActiveId} onNew={() => setShowNew(true)} onDelete={handleDelete} reloadKey={reloadKey} />
|
||||
: game && myId
|
||||
? <GameBoard game={game} myId={myId} onMove={handleMove} toast={toast} />
|
||||
? <GameBoard game={game} myId={myId} onMove={handleMove} onScout={handleScout} toast={toast} />
|
||||
: <div style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', textAlign:'center', paddingTop:40 }}>Lade Spiel…</div>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user