Fix fullscreen across games

Restore in-app fullscreen usage for all games by removing broken portal usage, re-centering Tetris UI, and ensuring AIChat fullscreen now uses standard in-app fullscreen. Also revert Tetris file to valid non-corrupted state and reapply proper rendering for 1P/2P layouts.

X-Lovable-Edit-ID: edt-3fac9b3c-0a70-4aaf-9bfe-c412c7951e42
This commit is contained in:
gpt-engineer-app[bot]
2026-01-11 00:11:36 +00:00
3 changed files with 146 additions and 174 deletions
-21
View File
@@ -1,21 +0,0 @@
import { ReactNode, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
interface FullscreenPortalProps {
children: ReactNode;
}
/**
* Portals children to document.body to escape any transformed ancestors.
* This is important because CSS transforms on parents can break `position: fixed`.
*/
export const FullscreenPortal = ({ children }: FullscreenPortalProps) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted || typeof document === 'undefined') return null;
return createPortal(children, document.body);
};
+1 -2
View File
@@ -9,7 +9,6 @@ import { useAchievements, incrementAiMessageCount } from '@/contexts/Achievement
import { useToast } from '@/hooks/use-toast';
import GlitchText from '@/components/GlitchText';
import MessageContent from '@/components/MessageContent';
import { FullscreenPortal } from '@/components/FullscreenPortal';
import { AI_PROVIDERS, AIProvider, getProvider, CUSTOM_API_STORAGE_KEY } from '@/lib/aiProviders';
import {
DropdownMenu,
@@ -690,7 +689,7 @@ const AIChat = () => {
</motion.div>
);
return isFullscreen ? <FullscreenPortal>{content}</FullscreenPortal> : content;
return content;
};
export default AIChat;
+144 -150
View File
@@ -5,10 +5,8 @@ import { useAchievements } from '@/contexts/AchievementsContext';
import { Link } from 'react-router-dom';
import GlitchCrash from '@/components/GlitchCrash';
import { Maximize2, Minimize2, Users, User } from 'lucide-react';
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
import GameTouchButton from '@/components/GameTouchButton';
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
import { FullscreenPortal } from '@/components/FullscreenPortal';
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
@@ -64,9 +62,7 @@ const randomTetromino = (isP2 = false, excludeShape?: number[][]): Piece => {
let key = keys[Math.floor(Math.random() * keys.length)];
let tetromino = isP2 ? TETROMINOS_P2[key] : TETROMINOS[key];
// Ensure we don't generate the same shape twice in a row
if (excludeShape && JSON.stringify(tetromino.shape) === JSON.stringify(excludeShape)) {
// Try up to 7 times to get a different shape
for (let attempts = 0; attempts < 7; attempts++) {
key = keys[Math.floor(Math.random() * keys.length)];
tetromino = isP2 ? TETROMINOS_P2[key] : TETROMINOS[key];
@@ -93,10 +89,8 @@ const Tetris = () => {
const { playSound } = useSettings();
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
// Mode selection
const [gameMode, setGameMode] = useState<GameMode | null>(null);
// 1P state
const [board, setBoard] = useState<Board>(createBoard);
const [piece, setPiece] = useState<Piece>(randomTetromino);
const [nextPiece, setNextPiece] = useState<Piece>(randomTetromino);
@@ -104,34 +98,18 @@ const Tetris = () => {
const [highScore, setHighScore] = useState(0);
const [lines, setLines] = useState(0);
// 2P state
const [player1, setPlayer1] = useState<PlayerState>(() => {
const p1Piece = randomTetromino();
const p1Next = randomTetromino(false, p1Piece.shape);
return {
board: createBoard(),
piece: p1Piece,
nextPiece: p1Next,
score: 0,
lines: 0,
gameOver: false,
};
return { board: createBoard(), piece: p1Piece, nextPiece: p1Next, score: 0, lines: 0, gameOver: false };
});
const [player2, setPlayer2] = useState<PlayerState>(() => {
const p2Piece = randomTetromino(true);
const p2Next = randomTetromino(true, p2Piece.shape);
return {
board: createBoard(),
piece: p2Piece,
nextPiece: p2Next,
score: 0,
lines: 0,
gameOver: false,
};
return { board: createBoard(), piece: p2Piece, nextPiece: p2Next, score: 0, lines: 0, gameOver: false };
});
const [winner, setWinner] = useState<string | null>(null);
// Common state
const [gameOver, setGameOver] = useState(false);
const [gameComplete, setGameComplete] = useState(false);
const [isPaused, setIsPaused] = useState(false);
@@ -139,7 +117,6 @@ const Tetris = () => {
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const gameRef = useRef<HTMLDivElement>(null);
const { enterFullscreen, exitFullscreen } = useBrowserFullscreen();
const getCellSize = useCallback(() => {
if (typeof window === 'undefined') return 28;
@@ -149,9 +126,7 @@ const Tetris = () => {
const maxHeight = window.innerHeight - 440;
return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 24);
}
if (gameMode === '2p') {
return isFullscreen ? 26 : 22;
}
if (gameMode === '2p') return isFullscreen ? 26 : 22;
return isFullscreen ? 34 : 28;
}, [isFullscreen, gameMode]);
@@ -164,30 +139,24 @@ const Tetris = () => {
return () => window.removeEventListener('resize', handleResize);
}, [getCellSize]);
useEffect(() => {
setCellSize(getCellSize());
}, [isFullscreen, getCellSize, gameMode]);
useEffect(() => { setCellSize(getCellSize()); }, [isFullscreen, getCellSize, gameMode]);
useEffect(() => {
if (gameStarted && isMobile && !isFullscreen) {
setIsFullscreen(true);
enterFullscreen();
}
}, [gameStarted, isMobile, isFullscreen, enterFullscreen]);
if (gameStarted && isMobile && !isFullscreen) setIsFullscreen(true);
}, [gameStarted, isMobile, isFullscreen]);
const toggleFullscreen = async () => {
if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); }
else { setIsFullscreen(false); await exitFullscreen(); }
const toggleFullscreen = () => {
setIsFullscreen(prev => !prev);
playSound('click');
};
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isFullscreen) { setIsFullscreen(false); exitFullscreen(); }
if (e.key === 'Escape' && isFullscreen) setIsFullscreen(false);
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, [isFullscreen, exitFullscreen]);
}, [isFullscreen]);
useEffect(() => {
const saved = localStorage.getItem(HIGHSCORE_KEY);
@@ -236,7 +205,6 @@ const Tetris = () => {
return { board: newBoard, cleared };
}, []);
// 1P movement functions
const moveDown = useCallback(() => {
if (gameOver || gameComplete || isPaused || !gameStarted || gameMode !== '1p') return;
const newPiece = { ...piece, y: piece.y + 1 };
@@ -292,7 +260,6 @@ const Tetris = () => {
const startGame = (mode: GameMode) => {
setGameMode(mode);
if (mode === '1p') {
setBoard(createBoard());
setPiece(randomTetromino());
@@ -300,25 +267,10 @@ const Tetris = () => {
setScore(0);
setLines(0);
} else {
setPlayer1({
board: createBoard(),
piece: randomTetromino(),
nextPiece: randomTetromino(),
score: 0,
lines: 0,
gameOver: false,
});
setPlayer2({
board: createBoard(),
piece: randomTetromino(true),
nextPiece: randomTetromino(true),
score: 0,
lines: 0,
gameOver: false,
});
setPlayer1({ board: createBoard(), piece: randomTetromino(), nextPiece: randomTetromino(), score: 0, lines: 0, gameOver: false });
setPlayer2({ board: createBoard(), piece: randomTetromino(true), nextPiece: randomTetromino(true), score: 0, lines: 0, gameOver: false });
setWinner(null);
}
setGameOver(false);
setGameComplete(false);
setIsPaused(false);
@@ -333,82 +285,60 @@ const Tetris = () => {
playSound('click');
};
// 2P movement helpers
const movePlayer = useCallback((playerNum: 1 | 2, action: 'left' | 'right' | 'down' | 'rotate' | 'drop') => {
if (gameOver || isPaused || !gameStarted || gameMode !== '2p') return;
const setPlayer = playerNum === 1 ? setPlayer1 : setPlayer2;
const player = playerNum === 1 ? player1 : player2;
const isP2 = playerNum === 2;
if (player.gameOver) return;
setPlayer(prev => {
let newPiece = { ...prev.piece };
switch (action) {
case 'left':
newPiece.x--;
if (!isValidMove(newPiece, prev.board)) return prev;
playSound('hover');
return { ...prev, piece: newPiece };
case 'right':
newPiece.x++;
if (!isValidMove(newPiece, prev.board)) return prev;
playSound('hover');
return { ...prev, piece: newPiece };
case 'rotate':
newPiece.shape = rotate(prev.piece.shape);
if (!isValidMove(newPiece, prev.board)) return prev;
playSound('hover');
return { ...prev, piece: newPiece };
case 'drop':
while (isValidMove({ ...newPiece, y: newPiece.y + 1 }, prev.board)) newPiece.y++;
playSound('click');
return { ...prev, piece: newPiece };
case 'down':
newPiece.y++;
if (isValidMove(newPiece, prev.board)) {
return { ...prev, piece: newPiece };
} else {
if (isValidMove(newPiece, prev.board)) return { ...prev, piece: newPiece };
else {
const mergedBoard = mergePiece(prev.board, prev.piece);
const { board: clearedBoard, cleared } = clearLines(mergedBoard);
const newScore = prev.score + (cleared > 0 ? cleared * 100 * cleared : 0);
const newLines = prev.lines + cleared;
if (cleared > 0) playSound('success');
else playSound('click');
const newTetromino = prev.nextPiece;
const nextNext = randomTetromino(isP2, prev.nextPiece.shape);
if (!isValidMove(newTetromino, clearedBoard)) {
playSound('error');
return { ...prev, board: clearedBoard, score: newScore, lines: newLines, gameOver: true };
}
return {
...prev,
board: clearedBoard,
piece: newTetromino,
nextPiece: nextNext,
score: newScore,
lines: newLines,
};
return { ...prev, board: clearedBoard, piece: newTetromino, nextPiece: nextNext, score: newScore, lines: newLines };
}
}
return prev;
});
}, [gameOver, isPaused, gameStarted, gameMode, player1, player2, isValidMove, mergePiece, clearLines, playSound]);
// Check 2P win condition
useEffect(() => {
if (gameMode !== '2p' || !gameStarted) return;
if (player1.gameOver && player2.gameOver) {
setGameOver(true);
setWinner(player1.score > player2.score ? 'P1 Wins!' : player2.score > player1.score ? 'P2 Wins!' : 'Draw!');
@@ -421,11 +351,9 @@ const Tetris = () => {
}
}, [player1.gameOver, player2.gameOver, player1.score, player2.score, gameMode, gameStarted]);
// Keyboard handler
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!gameStarted) return;
if (gameMode === '1p') {
switch (e.key) {
case 'ArrowLeft': case 'a': e.preventDefault(); moveLeft(); break;
@@ -436,20 +364,16 @@ const Tetris = () => {
case 'p': e.preventDefault(); togglePause(); break;
}
} else {
// P1: WASD + Q for drop
if (e.key === 'w' || e.key === 'W') { e.preventDefault(); movePlayer(1, 'rotate'); }
if (e.key === 'a' || e.key === 'A') { e.preventDefault(); movePlayer(1, 'left'); }
if (e.key === 's' || e.key === 'S') { e.preventDefault(); movePlayer(1, 'down'); }
if (e.key === 'd' || e.key === 'D') { e.preventDefault(); movePlayer(1, 'right'); }
if (e.key === 'q' || e.key === 'Q') { e.preventDefault(); movePlayer(1, 'drop'); }
// P2: Arrows + / for drop
if (e.key === 'ArrowUp') { e.preventDefault(); movePlayer(2, 'rotate'); }
if (e.key === 'ArrowLeft') { e.preventDefault(); movePlayer(2, 'left'); }
if (e.key === 'ArrowDown') { e.preventDefault(); movePlayer(2, 'down'); }
if (e.key === 'ArrowRight') { e.preventDefault(); movePlayer(2, 'right'); }
if (e.key === '/') { e.preventDefault(); movePlayer(2, 'drop'); }
if (e.key === 'p' || e.key === 'P') { e.preventDefault(); togglePause(); }
}
};
@@ -457,14 +381,12 @@ const Tetris = () => {
return () => window.removeEventListener('keydown', handleKeyDown);
}, [moveLeft, moveRight, moveDown, rotatePiece, hardDrop, gameStarted, gameMode, movePlayer]);
// 1P tick
useEffect(() => {
if (!gameStarted || gameOver || gameComplete || isPaused || gameMode !== '1p') return;
const interval = setInterval(moveDown, TICK_SPEED);
return () => clearInterval(interval);
}, [moveDown, gameStarted, gameOver, gameComplete, isPaused, gameMode]);
// 2P tick
useEffect(() => {
if (!gameStarted || gameOver || isPaused || gameMode !== '2p') return;
const interval = setInterval(() => {
@@ -472,7 +394,7 @@ const Tetris = () => {
if (!player2.gameOver) movePlayer(2, 'down');
}, TICK_SPEED);
return () => clearInterval(interval);
}, [gameStarted, gameOver, isPaused, gameMode]);
}, [gameStarted, gameOver, isPaused, gameMode, player1.gameOver, player2.gameOver, movePlayer]);
const renderBoard1P = () => {
const displayBoard = board.map(row => [...row]);
@@ -512,70 +434,51 @@ const Tetris = () => {
return displayBoard.map((row, y) => (
<div key={y} className="flex">
{row.map((cell, x) => (
<div
key={`${y}-${x}`}
className={`border transition-colors duration-100 ${borderColor}`}
style={{
width: cellSize,
height: cellSize,
backgroundColor: cell || 'transparent',
boxShadow: cell ? `0 0 5px ${cell}` : 'none',
}}
/>
<div key={`${y}-${x}`} className={`border transition-colors duration-100 ${borderColor}`}
style={{ width: cellSize, height: cellSize, backgroundColor: cell || 'transparent', boxShadow: cell ? `0 0 5px ${cell}` : 'none' }} />
))}
</div>
));
};
const renderNextPiece = (nextP: Piece) => (
<div className="flex flex-col gap-1">
{nextP.shape.map((row, y) => (
<div key={y} className="flex gap-1">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-4 h-4`} style={{ backgroundColor: val ? nextP.color : 'transparent', boxShadow: val ? `0 0 5px ${nextP.color}` : 'none' }} />
))}
</div>
))}
</div>
);
if (showGlitchCrash) return <GlitchCrash onComplete={() => window.location.reload()} />;
// Mode selection screen
if (!gameMode) {
return (
<motion.div
ref={gameRef}
tabIndex={0}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="flex flex-col h-full"
>
<motion.div ref={gameRef} tabIndex={0} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }} className="flex flex-col h-full">
<div className="flex items-center gap-4 mb-4">
<Link to="/games" className="font-pixel text-sm text-foreground/50 hover:text-primary transition-colors">{'<'} Back</Link>
<h1 className="font-minecraft text-2xl md:text-3xl text-primary text-glow-strong">Tetris</h1>
</div>
<div className="flex-1 flex flex-col items-center justify-center gap-6">
<p className="font-pixel text-sm text-foreground/70">Select game mode</p>
<div className="flex gap-4">
<button
onClick={() => startGame('1p')}
className="flex flex-col items-center gap-2 p-6 border-2 border-primary/50 hover:border-primary bg-background/50 hover:bg-primary/10 transition-all"
>
<button onClick={() => startGame('1p')} className="flex flex-col items-center gap-2 p-6 border-2 border-primary/50 hover:border-primary bg-background/50 hover:bg-primary/10 transition-all">
<User size={32} className="text-primary" />
<span className="font-minecraft text-lg text-primary">1 Player</span>
<span className="font-pixel text-xs text-foreground/60">Solo mode</span>
</button>
{!isMobile && (
<button
onClick={() => startGame('2p')}
className="flex flex-col items-center gap-2 p-6 border-2 border-purple-500/50 hover:border-purple-500 bg-background/50 hover:bg-purple-500/10 transition-all"
>
<button onClick={() => startGame('2p')} className="flex flex-col items-center gap-2 p-6 border-2 border-purple-500/50 hover:border-purple-500 bg-background/50 hover:bg-purple-500/10 transition-all">
<Users size={32} className="text-purple-400" />
<span className="font-minecraft text-lg text-purple-400">2 Players</span>
<span className="font-pixel text-xs text-foreground/60">Local versus</span>
</button>
)}
</div>
{isMobile && (
<p className="font-pixel text-xs text-foreground/40 text-center max-w-xs">
2 Player mode requires a keyboard and is not available on mobile devices
</p>
)}
{isMobile && <p className="font-pixel text-xs text-foreground/40 text-center max-w-xs">2 Player mode requires a keyboard and is not available on mobile devices</p>}
<div className="border border-primary/30 p-3 bg-background/30 mt-4">
<p className="font-pixel text-xs text-foreground/60">HIGH SCORE: <span className="text-primary">{highScore.toLocaleString()}</span></p>
</div>
@@ -584,7 +487,7 @@ const Tetris = () => {
);
}
const content = (
return (
<motion.div ref={gameRef} tabIndex={0} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}
className={`flex flex-col outline-none ${isFullscreen ? 'fixed inset-0 z-50 bg-background p-4' : 'h-full'}`}>
<div className="flex items-center justify-between mb-2">
@@ -605,37 +508,128 @@ const Tetris = () => {
<div className="border-2 border-primary box-glow p-1 bg-background/80">{renderBoard1P()}</div>
{!isMobile && (
<div className="flex flex-col gap-3 min-w-[160px]">
{/* Next Piece Preview */}
<div className="border-2 border-primary/50 p-4 bg-background/60">
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider mb-3">Next</p>
<div className="flex justify-center">
<div className="flex flex-col gap-1">
{nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-1">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-4 h-4 ${val ? 'bg-primary box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
<div className="flex justify-center">{renderNextPiece(nextPiece)}</div>
</div>
<div className="border-2 border-primary/50 p-4 bg-background/60">
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider">Score</p>
<p className="font-minecraft text-2xl text-primary text-glow-strong">{score.toLocaleString()}</p>
</div>
<div className="border border-primary/30 p-3 bg-background/40">
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">High Score</p>
<p className="font-minecraft text-lg text-primary/80">{highScore.toLocaleString()}</p>
<p className="font-pixel text-[8px] text-foreground/30 mt-1">max: 4,294,967,296</p>
</div>
<div className="border border-primary/30 p-3 bg-background/40 text-center">
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">Lines</p>
<p className="font-minecraft text-xl text-primary">{lines}</p>
</div>
<div className="border border-primary/20 p-3 bg-background/30">
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider mb-2">Controls</p>
<div className="space-y-1">
<p className="font-pixel text-[10px] text-foreground/60"> / A D: Move</p>
<p className="font-pixel text-[10px] text-foreground/60"> / W: Rotate</p>
<p className="font-pixel text-[10px] text-foreground/60"> / S: Soft drop</p>
<p className="font-pixel text-[10px] text-foreground/60">Space: Hard drop</p>
<p className="font-pixel text-[10px] text-foreground/60">P: Pause</p>
</div>
</div>
{/* ... keep existing code (rest of sidebar / stats / UI) */}
{!gameStarted || gameOver ? (
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
{gameOver ? 'RETRY' : 'START GAME'}
</button>
) : (
<button onClick={togglePause} className="font-minecraft text-sm py-3 px-4 border-2 border-primary/60 bg-background/50 text-primary hover:bg-primary/20 transition-all">
{isPaused ? 'RESUME' : 'PAUSE'}
</button>
)}
</div>
)}
</>
) : (
<>
{/* ... keep existing code (2P layout) */}
</>
<div className="flex gap-4 items-start">
<div className="flex flex-col items-center gap-2">
<p className="font-minecraft text-lg text-primary">P1 <span className="text-xs text-foreground/60">(WASD + Q)</span></p>
<div className="border-2 border-primary box-glow p-1 bg-background/80">{renderBoard2P(player1, 'border-primary/20')}</div>
<div className="flex gap-4 text-center">
<div><p className="font-pixel text-[10px] text-foreground/60">Score</p><p className="font-minecraft text-lg text-primary">{player1.score}</p></div>
<div><p className="font-pixel text-[10px] text-foreground/60">Lines</p><p className="font-minecraft text-lg text-primary">{player1.lines}</p></div>
</div>
<div className="border border-primary/30 p-2 bg-background/40">
<p className="font-pixel text-[8px] text-foreground/40">Next</p>
{renderNextPiece(player1.nextPiece)}
</div>
</div>
<div className="flex flex-col items-center gap-2">
<p className="font-minecraft text-lg text-purple-400">P2 <span className="text-xs text-foreground/60">(Arrows + /)</span></p>
<div className="border-2 border-purple-500 p-1 bg-background/80" style={{ boxShadow: '0 0 15px hsl(280 70% 50% / 0.5)' }}>{renderBoard2P(player2, 'border-purple-500/20')}</div>
<div className="flex gap-4 text-center">
<div><p className="font-pixel text-[10px] text-foreground/60">Score</p><p className="font-minecraft text-lg text-purple-400">{player2.score}</p></div>
<div><p className="font-pixel text-[10px] text-foreground/60">Lines</p><p className="font-minecraft text-lg text-purple-400">{player2.lines}</p></div>
</div>
<div className="border border-purple-500/30 p-2 bg-background/40">
<p className="font-pixel text-[8px] text-foreground/40">Next</p>
{renderNextPiece(player2.nextPiece)}
</div>
</div>
</div>
)}
</div>
{/* ... keep existing code (mobile controls + overlays) */}
{isMobile && gameMode === '1p' && (
<>
<MobileControlsSpacer heightClassName="h-36" />
<MobileControlsDock>
<div className="flex flex-col items-center gap-2">
<div className="flex gap-4 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{lines}</p></div>
</div>
{gameStarted && !gameOver ? (
<div className="flex gap-2">
<GameTouchButton onAction={moveLeft} className="p-3 px-5 border border-primary/50 active:bg-primary/40 text-primary font-pixel" interval={100}></GameTouchButton>
<GameTouchButton onAction={moveDown} className="p-3 px-5 border border-primary/50 active:bg-primary/40 text-primary font-pixel" interval={100}></GameTouchButton>
<GameTouchButton onAction={moveRight} className="p-3 px-5 border border-primary/50 active:bg-primary/40 text-primary font-pixel" interval={100}></GameTouchButton>
<button onClick={rotatePiece} className="p-3 px-5 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none"></button>
<button onClick={hardDrop} className="p-3 px-5 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none"></button>
<button onClick={togglePause} className="p-3 px-5 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
</div>
) : (
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary">{gameOver ? 'RETRY' : 'START'}</button>
)}
</div>
</MobileControlsDock>
</>
)}
{!isMobile && (gameOver || isPaused) && gameStarted && (
<div className="fixed inset-0 bg-background/80 flex items-center justify-center z-[60]">
<div className="border-2 border-primary box-glow-strong p-6 bg-background text-center">
<h2 className="font-minecraft text-2xl text-primary text-glow-strong mb-3">{gameOver ? (winner || 'GAME OVER') : 'PAUSED'}</h2>
{gameOver && gameMode === '1p' && <p className="font-pixel text-sm text-foreground/70 mb-4">Final Score: {score.toLocaleString()}</p>}
{gameOver && gameMode === '2p' && (
<div className="flex gap-8 mb-4 justify-center">
<div><p className="font-pixel text-xs text-foreground/60">P1</p><p className="font-minecraft text-lg text-primary">{player1.score}</p></div>
<div><p className="font-pixel text-xs text-foreground/60">P2</p><p className="font-minecraft text-lg text-purple-400">{player2.score}</p></div>
</div>
)}
<div className="flex gap-4 justify-center">
{gameOver ? (
<>
<button onClick={() => startGame(gameMode)} className="font-minecraft text-sm py-2 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40">RETRY</button>
<button onClick={() => { setGameMode(null); setGameStarted(false); }} className="font-minecraft text-sm py-2 px-4 border-2 border-primary/50 text-primary/70 hover:bg-primary/20">MENU</button>
</>
) : (
<button onClick={togglePause} className="font-minecraft text-sm py-2 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40">RESUME</button>
)}
</div>
</div>
</div>
)}
</motion.div>
);
return isFullscreen ? <FullscreenPortal>{content}</FullscreenPortal> : content;
};
export default Tetris;