feat: Hex Wars - Kundschaften, Mine-Alert auto-clear, Spielliste live
This commit is contained in:
@@ -25,11 +25,13 @@ const router = express.Router();
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
for (const [col, def] of [
|
for (const [col, def] of [
|
||||||
['move_count', 'INTEGER NOT NULL DEFAULT 0'],
|
['move_count', 'INTEGER NOT NULL DEFAULT 0'],
|
||||||
['terrain', 'TEXT'],
|
['terrain', 'TEXT'],
|
||||||
['last_event', 'TEXT'],
|
['last_event', 'TEXT'],
|
||||||
['owner_head', 'TEXT'],
|
['owner_head', 'TEXT'],
|
||||||
['opp_head', 'TEXT'],
|
['opp_head', 'TEXT'],
|
||||||
|
['owner_scouts', 'TEXT'],
|
||||||
|
['opp_scouts', 'TEXT'],
|
||||||
]) {
|
]) {
|
||||||
if (!cols.includes(col)) db.exec(`ALTER TABLE geo_games ADD COLUMN ${col} ${def}`);
|
if (!cols.includes(col)) db.exec(`ALTER TABLE geo_games ADD COLUMN ${col} ${def}`);
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,7 @@ function generateTerrain(ownerHead, oppHead) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Fog of War ────────────────────────────────────────────────────────────────
|
// ── Fog of War ────────────────────────────────────────────────────────────────
|
||||||
function computeFog(grid, playerId) {
|
function computeFog(grid, playerId, scouts = []) {
|
||||||
const visible = Array(GRID).fill(null).map(() => Array(GRID).fill(false));
|
const visible = Array(GRID).fill(null).map(() => Array(GRID).fill(false));
|
||||||
for (let r=0;r<GRID;r++) for (let c=0;c<GRID;c++) {
|
for (let r=0;r<GRID;r++) for (let c=0;c<GRID;c++) {
|
||||||
if (grid[r][c] === playerId) {
|
if (grid[r][c] === playerId) {
|
||||||
@@ -80,6 +82,13 @@ function computeFog(grid, playerId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Scout-Aufdeckungen: 3x3 um jeden Scout-Mittelpunkt
|
||||||
|
for (const [sr,sc] of scouts) {
|
||||||
|
for (let dr=-1;dr<=1;dr++) for (let dc=-1;dc<=1;dc++) {
|
||||||
|
const nr=sr+dr, nc=sc+dc;
|
||||||
|
if (nr>=0&&nr<GRID&&nc>=0&&nc<GRID) visible[nr][nc] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return visible;
|
return visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,25 +237,30 @@ function getGameForUser(id, requesterId) {
|
|||||||
return { ...game, myScore:0, oppScore:0, myHead, oppHead };
|
return { ...game, myScore:0, oppScore:0, myHead, oppHead };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scouts des anfragenden Spielers laden
|
||||||
|
const myScoutsRaw = requesterId === game.owner_id ? game.owner_scouts : game.opp_scouts;
|
||||||
|
const myScouts = myScoutsRaw ? JSON.parse(myScoutsRaw) : [];
|
||||||
|
|
||||||
// Bei beendetem Spiel: vollständiges Grid ohne Fog
|
// Bei beendetem Spiel: vollständiges Grid ohne Fog
|
||||||
let maskedGrid, maskedTerrain;
|
let maskedGrid, maskedTerrain;
|
||||||
if (game.status === 'finished') {
|
if (game.status === 'finished') {
|
||||||
maskedGrid = grid;
|
maskedGrid = grid;
|
||||||
maskedTerrain = terrain;
|
maskedTerrain = terrain;
|
||||||
} else {
|
} else {
|
||||||
const visible = computeFog(grid, requesterId);
|
const visible = computeFog(grid, requesterId, myScouts);
|
||||||
maskedGrid = grid.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : (cell===requesterId ? cell : 0)));
|
maskedGrid = grid.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : (cell===requesterId ? cell : 0)));
|
||||||
maskedTerrain = terrain.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : -1));
|
maskedTerrain = terrain.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : -1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...game,
|
...game,
|
||||||
grid: JSON.stringify(maskedGrid),
|
grid: JSON.stringify(maskedGrid),
|
||||||
terrain: JSON.stringify(maskedTerrain),
|
terrain: JSON.stringify(maskedTerrain),
|
||||||
myScore,
|
myScore,
|
||||||
oppScore: oppSc,
|
oppScore: oppSc,
|
||||||
myHead,
|
myHead,
|
||||||
oppHead,
|
oppHead,
|
||||||
|
myScouts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,10 +456,14 @@ router.post('/:id/move', authenticate, (req, res) => {
|
|||||||
winnerId = ms > os ? me : (os > ms ? opponent : null);
|
winnerId = ms > os ? me : (os > ms ? opponent : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// last_event: nach jedem Zug das vorherige Event clearen (war vom letzten Zug)
|
||||||
|
// Nur das neue Event (falls Mine) bleibt stehen
|
||||||
|
const finalEvent = lastEvent; // null wenn kein Mine-Treffer
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE geo_games SET grid=?, terrain=?, current_turn=?, status=?, winner_id=?, move_count=?, last_event=?,
|
UPDATE geo_games SET grid=?, terrain=?, current_turn=?, status=?, winner_id=?, move_count=?, last_event=?,
|
||||||
owner_head=?, opp_head=?, updated_at=datetime('now','localtime') WHERE id=?
|
owner_head=?, opp_head=?, updated_at=datetime('now','localtime') WHERE id=?
|
||||||
`).run(JSON.stringify(grid), JSON.stringify(terrain), nextTurn, status, winnerId, newMoves, lastEvent, newOwnerHead, newOppHead, game.id);
|
`).run(JSON.stringify(grid), JSON.stringify(terrain), nextTurn, status, winnerId, newMoves, finalEvent, newOwnerHead, newOppHead, game.id);
|
||||||
|
|
||||||
// Pushover
|
// Pushover
|
||||||
if (status === 'finished') {
|
if (status === 'finished') {
|
||||||
@@ -474,6 +492,58 @@ router.post('/:id/move', authenticate, (req, res) => {
|
|||||||
res.json(getGameForUser(game.id, me));
|
res.json(getGameForUser(game.id, me));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Scout: 3x3 Bereich aufdecken (kostet Zug) ───────────────────────────────
|
||||||
|
router.post('/:id/scout', authenticate, (req, res) => {
|
||||||
|
const me = uid(req);
|
||||||
|
const { row, col } = req.body;
|
||||||
|
if (row === undefined || col === undefined) return res.status(400).json({ error: 'row/col fehlt' });
|
||||||
|
|
||||||
|
const game = db.prepare('SELECT * FROM geo_games WHERE id=?').get(req.params.id);
|
||||||
|
if (!game) return res.status(404).json({ error: 'Nicht gefunden' });
|
||||||
|
if (game.owner_id !== me && game.opponent_id !== me) return res.status(403).json({ error: 'Kein Zugriff' });
|
||||||
|
if (game.status !== 'active') return res.status(400).json({ error: 'Spiel beendet' });
|
||||||
|
if (game.current_turn !== me) return res.status(400).json({ error: 'Nicht dein Zug' });
|
||||||
|
|
||||||
|
const opponent = game.owner_id === me ? game.opponent_id : game.owner_id;
|
||||||
|
const newMoves = (game.move_count || 0) + 1;
|
||||||
|
|
||||||
|
// Scouts des Spielers laden und neuen hinzufügen
|
||||||
|
const isOwner = game.owner_id === me;
|
||||||
|
const scoutsKey = isOwner ? 'owner_scouts' : 'opp_scouts';
|
||||||
|
const existingRaw = game[scoutsKey];
|
||||||
|
const existing = existingRaw ? JSON.parse(existingRaw) : [];
|
||||||
|
existing.push([row, col]);
|
||||||
|
const newScoutsStr = JSON.stringify(existing);
|
||||||
|
|
||||||
|
// Zug weitergeben (Scout kostet Zug)
|
||||||
|
const grid = JSON.parse(game.grid);
|
||||||
|
const terrain = JSON.parse(game.terrain || '[]');
|
||||||
|
let status = 'active', winnerId = null;
|
||||||
|
|
||||||
|
// Schnellsieg-Check
|
||||||
|
if (newMoves >= 10 && terrain.length) {
|
||||||
|
const ms = calcScore(grid, terrain, me);
|
||||||
|
const os = calcScore(grid, terrain, opponent);
|
||||||
|
if (ms >= os*2 && os > 0) status = 'finished';
|
||||||
|
else if (os >= ms*2 && ms > 0) status = 'finished';
|
||||||
|
if (status === 'finished') {
|
||||||
|
winnerId = ms > os ? me : (os > ms ? opponent : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateFields = isOwner
|
||||||
|
? 'owner_scouts=?, current_turn=?, status=?, winner_id=?, move_count=?, last_event=NULL'
|
||||||
|
: 'opp_scouts=?, current_turn=?, status=?, winner_id=?, move_count=?, last_event=NULL';
|
||||||
|
|
||||||
|
db.prepare(`UPDATE geo_games SET ${updateFields}, updated_at=datetime('now','localtime') WHERE id=?`)
|
||||||
|
.run(newScoutsStr, opponent, status, winnerId, newMoves, game.id);
|
||||||
|
|
||||||
|
const myName = db.prepare('SELECT username FROM users WHERE id=?').get(me)?.username || 'Jemand';
|
||||||
|
sendPush(opponent, '⬡ Hex Wars — Du bist dran!', `${myName} hat gekundschaftet — jetzt bist du dran!`);
|
||||||
|
|
||||||
|
res.json(getGameForUser(game.id, me));
|
||||||
|
});
|
||||||
|
|
||||||
// ── Aufgeben ──────────────────────────────────────────────────────────────────
|
// ── Aufgeben ──────────────────────────────────────────────────────────────────
|
||||||
router.post('/:id/resign', authenticate, (req, res) => {
|
router.post('/:id/resign', authenticate, (req, res) => {
|
||||||
const me = uid(req);
|
const me = uid(req);
|
||||||
|
|||||||
@@ -141,9 +141,11 @@ function Leaderboard({ reloadKey }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Spielfeld ─────────────────────────────────────────────────────────────────
|
// ── Spielfeld ─────────────────────────────────────────────────────────────────
|
||||||
function GameBoard({ game, myId, onMove, toast }) {
|
function GameBoard({ game, myId, onMove, onScout, toast }) {
|
||||||
const [pending, setPending] = useState(null);
|
const [pending, setPending] = useState(null);
|
||||||
const [moving, setMoving] = useState(false);
|
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 [mineAlert, setMineAlert] = useState(null); // letztes Mine-Event
|
||||||
|
|
||||||
const grid = JSON.parse(game.grid);
|
const grid = JSON.parse(game.grid);
|
||||||
@@ -158,14 +160,18 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
|
|
||||||
// Mine-Event anzeigen wenn frisch
|
// Mine-Event anzeigen wenn frisch
|
||||||
const [blastHighlight, setBlastHighlight] = useState(new Set());
|
const [blastHighlight, setBlastHighlight] = useState(new Set());
|
||||||
|
const [alertShownAt, setAlertShownAt] = useState(null); // move_count beim Anzeigen
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!game.last_event) return;
|
if (!game.last_event) {
|
||||||
|
setMineAlert(null); // Event wurde vom Server geclearet (nächster Zug)
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const ev = JSON.parse(game.last_event);
|
const ev = JSON.parse(game.last_event);
|
||||||
if (ev.type === 'mine') {
|
if (ev.type === 'mine') {
|
||||||
setMineAlert(ev);
|
setMineAlert(ev);
|
||||||
// Blast-Zellen kurz highlighten
|
setAlertShownAt(game.move_count);
|
||||||
if (ev.blastCells?.length) {
|
if (ev.blastCells?.length) {
|
||||||
const keys = new Set(ev.blastCells.map(([r,c]) => `${r}-${c}`));
|
const keys = new Set(ev.blastCells.map(([r,c]) => `${r}-${c}`));
|
||||||
setBlastHighlight(keys);
|
setBlastHighlight(keys);
|
||||||
@@ -173,7 +179,7 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}, [game.last_event]);
|
}, [game.last_event, game.move_count]);
|
||||||
|
|
||||||
// Schlangen-Köpfe aus Server-Response
|
// Schlangen-Köpfe aus Server-Response
|
||||||
const myHeadStr = game.myHead;
|
const myHeadStr = game.myHead;
|
||||||
@@ -190,12 +196,11 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canClick = (r, c) => {
|
const canClick = (r, c) => {
|
||||||
|
if (scoutMode) return false; // im Scout-Modus kein normaler Zug
|
||||||
if (!isMyTurn || game.status !== 'active') return false;
|
if (!isMyTurn || game.status !== 'active') return false;
|
||||||
if (grid[r][c] !== 0) return false;
|
if (grid[r][c] !== 0) return false;
|
||||||
if (hasTerrain && terrain[r][c] === T_ROCK) return false;
|
if (hasTerrain && terrain[r][c] === T_ROCK) return false;
|
||||||
// Schlangen-Regel: nur Felder an den Kopf
|
|
||||||
if (myHead) return isAdjacentToHead(r, c);
|
if (myHead) return isAdjacentToHead(r, c);
|
||||||
// Fallback für alte Spiele ohne head
|
|
||||||
return DIRS8.some(([dr,dc]) => {
|
return DIRS8.some(([dr,dc]) => {
|
||||||
const nr=r+dr, nc=c+dc;
|
const nr=r+dr, nc=c+dc;
|
||||||
return nr>=0&&nr<GRID&&nc>=0&&nc<GRID&&grid[nr][nc]===myId;
|
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
|
// 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 renderCell = (r, c) => {
|
||||||
const cell = grid[r][c];
|
const cell = grid[r][c];
|
||||||
const t = hasTerrain ? terrain[r][c] : T_NORMAL;
|
const t = hasTerrain ? terrain[r][c] : T_NORMAL;
|
||||||
const clic = canClick(r, c);
|
const clic = canClick(r, c);
|
||||||
const isPend = pending?.row === r && pending?.col === c;
|
const isPend = pending?.row === r && pending?.col === c;
|
||||||
const isBlast = blastHighlight.has(`${r}-${c}`);
|
const isBlast = blastHighlight.has(`${r}-${c}`);
|
||||||
const isHead = myHR === r && myHC === c; // eigener Kopf
|
const isHead = myHR === r && myHC === c;
|
||||||
const isOppHead = game.status === 'finished' && oppHR === r && oppHC === c; // Gegner-Kopf nur bei Spielende
|
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 isFog = t === T_FOG;
|
||||||
const isRock = t === T_ROCK;
|
const isRock = t === T_ROCK;
|
||||||
const isGold = t === T_GOLD;
|
const isGold = t === T_GOLD;
|
||||||
@@ -293,14 +318,17 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
|
|
||||||
if (isBlast) { bg = '#ff440066'; border = '2px solid #ff4400'; }
|
if (isBlast) { bg = '#ff440066'; border = '2px solid #ff4400'; }
|
||||||
if (isOppHead) { border = `2px solid ${oppColor}`; }
|
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}`; }
|
if (isPend) { bg = myColor; border = `2px solid ${myColor}`; }
|
||||||
else if (clic && !isPend && !isBlast && !isHead) border = `1px solid ${myColor}44`;
|
else if (clic && !isPend && !isBlast && !isHead) border = `1px solid ${myColor}44`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${r}-${c}`}
|
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={{
|
style={{
|
||||||
width:cellSize, height:cellSize, background:bg,
|
width:cellSize, height:cellSize, background:bg,
|
||||||
borderRadius:1, cursor:clic?'pointer':'default',
|
borderRadius:1, cursor:clic?'pointer':'default',
|
||||||
@@ -362,12 +390,35 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
⏳ {oppName} ist dran…
|
⏳ {oppName} ist dran…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isMyTurn && !pending && (
|
{isMyTurn && !pending && !scoutMode && (
|
||||||
<div style={{ color:myColor, fontFamily:'monospace', fontSize:11, textAlign:'center', padding:'4px 0' }}>
|
<div style={{ display:'flex', alignItems:'center', gap:6 }}>
|
||||||
▶ Baue vom Kopf aus — Vorsicht vor Minen! 💣
|
<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>
|
</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' }}>
|
<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 }}>
|
<span style={{ color:myColor, fontFamily:'monospace', fontSize:11, flex:1 }}>
|
||||||
✦ R{pending.row+1} S{pending.col+1} — bestätigen?
|
✦ 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 handleMove = async (row, col) => {
|
||||||
const updated = await api(`/tools/gebietseroberung/${activeId}/move`, { body: { row, col } });
|
const updated = await api(`/tools/gebietseroberung/${activeId}/move`, { body: { row, col } });
|
||||||
setGame(updated);
|
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 () => {
|
const handleResign = async () => {
|
||||||
@@ -547,7 +604,7 @@ export default function HexWars({ toast }) {
|
|||||||
{!activeId
|
{!activeId
|
||||||
? <GameList games={games} myId={myId} isAdmin={isAdmin} onSelect={setActiveId} onNew={() => setShowNew(true)} onDelete={handleDelete} reloadKey={reloadKey} />
|
? <GameList games={games} myId={myId} isAdmin={isAdmin} onSelect={setActiveId} onNew={() => setShowNew(true)} onDelete={handleDelete} reloadKey={reloadKey} />
|
||||||
: game && myId
|
: 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>
|
: <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