fix: Hex Wars - Felsen im Nebel, kein Scrollen auf Mobile, Leaderboard-Refresh
This commit is contained in:
@@ -160,8 +160,7 @@ function getGameForUser(id, requesterId) {
|
|||||||
|
|
||||||
// Grid maskieren: versteckte Gegner-Felder als 0 zeigen, Terrain nur wo sichtbar
|
// Grid maskieren: versteckte Gegner-Felder als 0 zeigen, Terrain nur wo sichtbar
|
||||||
const maskedGrid = grid.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : (cell === requesterId ? cell : 0)));
|
const maskedGrid = grid.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : (cell === requesterId ? cell : 0)));
|
||||||
// Felsen sind immer sichtbar (Frontend braucht sie für canClick)
|
const maskedTerrain = terrain.map((row,r) => row.map((cell,c) => visible[r][c] ? cell : -1));
|
||||||
const maskedTerrain = terrain.map((row,r) => row.map((cell,c) => (visible[r][c] || cell === T_ROCK) ? cell : -1));
|
|
||||||
|
|
||||||
return { ...game, grid: JSON.stringify(maskedGrid), terrain: JSON.stringify(maskedTerrain), fog: JSON.stringify(visible) };
|
return { ...game, grid: JSON.stringify(maskedGrid), terrain: JSON.stringify(maskedTerrain), fog: JSON.stringify(visible) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ function NewGameModal({ onClose, onCreated, toast }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Topliste ──────────────────────────────────────────────────────────────────
|
// ── Topliste ──────────────────────────────────────────────────────────────────
|
||||||
function Leaderboard() {
|
function Leaderboard({ reloadKey }) {
|
||||||
const [rows, setRows] = useState(null);
|
const [rows, setRows] = useState(null);
|
||||||
useEffect(() => { api('/tools/gebietseroberung/leaderboard').then(setRows).catch(() => setRows([])); }, []);
|
useEffect(() => { api('/tools/gebietseroberung/leaderboard').then(setRows).catch(() => setRows([])); }, [reloadKey]);
|
||||||
if (!rows) return null;
|
if (!rows) return null;
|
||||||
const medals = ['🥇','🥈','🥉'];
|
const medals = ['🥇','🥈','🥉'];
|
||||||
return (
|
return (
|
||||||
@@ -217,7 +217,11 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const vw = Math.min(window.innerWidth, 600);
|
const vw = Math.min(window.innerWidth, 600);
|
||||||
const cellSize = Math.max(14, Math.floor((vw - 32) / GRID));
|
// Verfügbare Höhe: Viewport minus Header (~48px) minus Scoreboard (~72px) minus Status (~40px) minus Legende (~24px) minus Puffer (20px)
|
||||||
|
const availH = window.innerHeight - 204;
|
||||||
|
const byWidth = Math.floor((vw - 32) / GRID);
|
||||||
|
const byHeight = Math.floor(availH / GRID);
|
||||||
|
const cellSize = Math.max(10, Math.min(byWidth, byHeight));
|
||||||
|
|
||||||
// Zell-Rendering
|
// Zell-Rendering
|
||||||
const renderCell = (r, c) => {
|
const renderCell = (r, c) => {
|
||||||
@@ -273,20 +277,15 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Scoreboard */}
|
{/* Scoreboard */}
|
||||||
<div style={{ display:'flex', gap:10, alignItems:'stretch', marginBottom:14 }}>
|
<div style={{ display:'flex', gap:8, alignItems:'center', marginBottom:8 }}>
|
||||||
<div style={{ ...S.card, padding:'10px 14px', flex:1 }}>
|
<div style={{ ...S.card, padding:'6px 10px', flex:1 }}>
|
||||||
<div style={{ color:myColor, fontFamily:'monospace', fontSize:10, letterSpacing:1, marginBottom:4 }}>DU</div>
|
<div style={{ color:myColor, fontFamily:'monospace', fontSize:9, letterSpacing:1, marginBottom:2 }}>DU</div>
|
||||||
<div style={{ color:'#fff', fontFamily:'Space Mono,monospace', fontSize:22, fontWeight:700, lineHeight:1 }}>{myScore}</div>
|
<div style={{ color:'#fff', fontFamily:'Space Mono,monospace', fontSize:18, fontWeight:700, lineHeight:1 }}>{myScore} <span style={{ fontSize:10, color:'rgba(255,255,255,0.3)' }}>Pkt</span></div>
|
||||||
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:10, marginTop:3 }}>{myCount} Felder</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:3, padding:'0 6px' }}>
|
<div style={{ textAlign:'center', color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:9 }}>🌫️{fogCount}</div>
|
||||||
<div style={{ color:'rgba(255,255,255,0.4)', fontFamily:'monospace', fontSize:12, fontWeight:700 }}>Pkt</div>
|
<div style={{ ...S.card, padding:'6px 10px', flex:1, textAlign:'right' }}>
|
||||||
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:9 }}>🌫️ {fogCount}</div>
|
<div style={{ color:oppColor, fontFamily:'monospace', fontSize:9, letterSpacing:1, marginBottom:2 }}>{oppName?.toUpperCase()}</div>
|
||||||
</div>
|
<div style={{ color:'#fff', fontFamily:'Space Mono,monospace', fontSize:18, fontWeight:700, lineHeight:1 }}><span style={{ fontSize:10, color:'rgba(255,255,255,0.3)' }}>Pkt </span>{oppScore}</div>
|
||||||
<div style={{ ...S.card, padding:'10px 14px', flex:1, textAlign:'right' }}>
|
|
||||||
<div style={{ color:oppColor, fontFamily:'monospace', fontSize:10, letterSpacing:1, marginBottom:4 }}>{oppName?.toUpperCase()}</div>
|
|
||||||
<div style={{ color:'#fff', fontFamily:'Space Mono,monospace', fontSize:22, fontWeight:700, lineHeight:1 }}>{oppScore}</div>
|
|
||||||
<div style={{ color:'rgba(255,255,255,0.25)', fontFamily:'monospace', fontSize:10, marginTop:3 }}>{oppCount} Felder</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -319,37 +318,31 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
|
|
||||||
{/* Zug-Status */}
|
{/* Zug-Status */}
|
||||||
{game.status === 'active' && (
|
{game.status === 'active' && (
|
||||||
<div style={{ marginBottom:10 }}>
|
<div style={{ marginBottom:6 }}>
|
||||||
{!isMyTurn && (
|
{!isMyTurn && !pending && (
|
||||||
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:12, textAlign:'center', padding:'8px 0' }}>
|
<div style={{ color:'rgba(255,255,255,0.35)', fontFamily:'monospace', fontSize:11, textAlign:'center', padding:'4px 0' }}>
|
||||||
⏳ {oppName} ist dran…
|
⏳ {oppName} ist dran…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isMyTurn && !pending && (
|
{isMyTurn && !pending && (
|
||||||
<div style={{ color:myColor, fontFamily:'monospace', fontSize:12, textAlign:'center', padding:'8px 0' }}>
|
<div style={{ color:myColor, fontFamily:'monospace', fontSize:11, textAlign:'center', padding:'4px 0' }}>
|
||||||
▶ Wähle ein Feld — Vorsicht vor Minen! 💣
|
▶ Wähle ein Feld — Vorsicht vor Minen! 💣
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isMyTurn && pending && (
|
{pending && (
|
||||||
<div style={{
|
<div style={{ display:'flex', alignItems:'center', gap:8, background:`${myColor}12`, border:`1px solid ${myColor}44`, borderRadius:8, padding:'7px 10px' }}>
|
||||||
display:'flex', alignItems:'center', gap:10,
|
<span style={{ color:myColor, fontFamily:'monospace', fontSize:11, flex:1 }}>
|
||||||
background:`${myColor}12`, border:`1px solid ${myColor}44`,
|
✦ R{pending.row+1} S{pending.col+1} — bestätigen?
|
||||||
borderRadius:8, padding:'10px 14px',
|
|
||||||
}}>
|
|
||||||
<span style={{ color:myColor, fontFamily:'monospace', fontSize:12, flex:1 }}>
|
|
||||||
✦ Zeile {pending.row+1}, Spalte {pending.col+1} — Zug bestätigen?
|
|
||||||
</span>
|
</span>
|
||||||
<button onClick={() => setPending(null)} style={S.btn('#666666', true)}>✕</button>
|
<button onClick={() => setPending(null)} style={S.btn('#666666', true)}>✕</button>
|
||||||
<button onClick={confirmMove} disabled={moving} style={S.btn(MY_COLOR, true)}>
|
<button onClick={confirmMove} disabled={moving} style={S.btn(MY_COLOR, true)}>{moving ? '…' : '✓ Ja'}</button>
|
||||||
{moving ? '…' : '✓ Ja'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Legende */}
|
{/* Legende */}
|
||||||
<div style={{ display:'flex', gap:12, marginBottom:8, flexWrap:'wrap' }}>
|
<div style={{ display:'flex', gap:8, marginBottom:6, flexWrap:'wrap' }}>
|
||||||
{[['⭐','Gold +3','#ffe66d'],['💣','Mine −3','#ff6b9d'],['🪨','Felsen','rgba(255,255,255,0.35)'],['🌫️','Nebel','rgba(255,255,255,0.25)']].map(([icon,label,color]) => (
|
{[['⭐','Gold +3','#ffe66d'],['💣','Mine −3','#ff6b9d'],['🪨','Felsen','rgba(255,255,255,0.35)'],['🌫️','Nebel','rgba(255,255,255,0.25)']].map(([icon,label,color]) => (
|
||||||
<div key={label} style={{ display:'flex', alignItems:'center', gap:4 }}>
|
<div key={label} style={{ display:'flex', alignItems:'center', gap:4 }}>
|
||||||
<span style={{ fontSize:12 }}>{icon}</span>
|
<span style={{ fontSize:12 }}>{icon}</span>
|
||||||
@@ -364,9 +357,7 @@ function GameBoard({ game, myId, onMove, toast }) {
|
|||||||
{grid.map((row, r) => row.map((_, c) => renderCell(r, c)))}
|
{grid.map((row, r) => row.map((_, c) => renderCell(r, c)))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color:'rgba(255,255,255,0.2)', fontFamily:'monospace', fontSize:10, textAlign:'center', marginTop:6 }}>
|
|
||||||
📱 Pinch zum Zoomen
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -407,7 +398,7 @@ function GameRow({ g, myId, isAdmin, onSelect, onDelete }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Spielliste ────────────────────────────────────────────────────────────────
|
// ── Spielliste ────────────────────────────────────────────────────────────────
|
||||||
function GameList({ games, myId, isAdmin, onSelect, onNew, onDelete }) {
|
function GameList({ games, myId, isAdmin, onSelect, onNew, onDelete, reloadKey }) {
|
||||||
const active = games.filter(g => g.status === 'active');
|
const active = games.filter(g => g.status === 'active');
|
||||||
const finished = games.filter(g => g.status === 'finished' && (g.move_count || 0) >= 2);
|
const finished = games.filter(g => g.status === 'finished' && (g.move_count || 0) >= 2);
|
||||||
return (
|
return (
|
||||||
@@ -428,7 +419,7 @@ function GameList({ games, myId, isAdmin, onSelect, onNew, onDelete }) {
|
|||||||
Noch keine Spiele — starte eines!
|
Noch keine Spiele — starte eines!
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Leaderboard />
|
<Leaderboard reloadKey={reloadKey} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -442,6 +433,7 @@ export default function HexWars({ toast }) {
|
|||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
const myId = getMyId();
|
const myId = getMyId();
|
||||||
const isAdmin = getMyRole() === 'admin';
|
const isAdmin = getMyRole() === 'admin';
|
||||||
|
|
||||||
@@ -491,6 +483,7 @@ export default function HexWars({ toast }) {
|
|||||||
try {
|
try {
|
||||||
await api(`/tools/gebietseroberung/${id}`, { method: 'DELETE' });
|
await api(`/tools/gebietseroberung/${id}`, { method: 'DELETE' });
|
||||||
setGames(prev => prev.filter(g => g.id !== id));
|
setGames(prev => prev.filter(g => g.id !== id));
|
||||||
|
setReloadKey(k => k + 1);
|
||||||
toast('Spiel gelöscht.');
|
toast('Spiel gelöscht.');
|
||||||
} catch(e) { toast?.(e.message || 'Fehler', 'error'); }
|
} catch(e) { toast?.(e.message || 'Fehler', 'error'); }
|
||||||
};
|
};
|
||||||
@@ -499,7 +492,7 @@ export default function HexWars({ toast }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth:600, margin:'0 auto' }}>
|
<div style={{ maxWidth:600, margin:'0 auto' }}>
|
||||||
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:24, flexWrap:'wrap' }}>
|
<div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:12, flexWrap:'wrap' }}>
|
||||||
{activeId && (
|
{activeId && (
|
||||||
<button onClick={() => { setActiveId(null); setGame(null); }} style={S.btn('#666666', true)}>← Zurück</button>
|
<button onClick={() => { setActiveId(null); setGame(null); }} style={S.btn('#666666', true)}>← Zurück</button>
|
||||||
)}
|
)}
|
||||||
@@ -514,7 +507,7 @@ export default function HexWars({ toast }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!activeId
|
{!activeId
|
||||||
? <GameList games={games} myId={myId} isAdmin={isAdmin} onSelect={setActiveId} onNew={() => setShowNew(true)} onDelete={handleDelete} />
|
? <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} 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