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 { useToast } from '@/hooks/use-toast';
import GlitchText from '@/components/GlitchText'; import GlitchText from '@/components/GlitchText';
import MessageContent from '@/components/MessageContent'; import MessageContent from '@/components/MessageContent';
import { FullscreenPortal } from '@/components/FullscreenPortal';
import { AI_PROVIDERS, AIProvider, getProvider, CUSTOM_API_STORAGE_KEY } from '@/lib/aiProviders'; import { AI_PROVIDERS, AIProvider, getProvider, CUSTOM_API_STORAGE_KEY } from '@/lib/aiProviders';
import { import {
DropdownMenu, DropdownMenu,
@@ -690,7 +689,7 @@ const AIChat = () => {
</motion.div> </motion.div>
); );
return isFullscreen ? <FullscreenPortal>{content}</FullscreenPortal> : content; return content;
}; };
export default AIChat; export default AIChat;
+144 -150
View File
@@ -5,10 +5,8 @@ import { useAchievements } from '@/contexts/AchievementsContext';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import GlitchCrash from '@/components/GlitchCrash'; import GlitchCrash from '@/components/GlitchCrash';
import { Maximize2, Minimize2, Users, User } from 'lucide-react'; import { Maximize2, Minimize2, Users, User } from 'lucide-react';
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
import GameTouchButton from '@/components/GameTouchButton'; import GameTouchButton from '@/components/GameTouchButton';
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock'; import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
import { FullscreenPortal } from '@/components/FullscreenPortal';
const BOARD_WIDTH = 10; const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20; 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 key = keys[Math.floor(Math.random() * keys.length)];
let tetromino = isP2 ? TETROMINOS_P2[key] : TETROMINOS[key]; 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)) { 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++) { for (let attempts = 0; attempts < 7; attempts++) {
key = keys[Math.floor(Math.random() * keys.length)]; key = keys[Math.floor(Math.random() * keys.length)];
tetromino = isP2 ? TETROMINOS_P2[key] : TETROMINOS[key]; tetromino = isP2 ? TETROMINOS_P2[key] : TETROMINOS[key];
@@ -93,10 +89,8 @@ const Tetris = () => {
const { playSound } = useSettings(); const { playSound } = useSettings();
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements(); const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
// Mode selection
const [gameMode, setGameMode] = useState<GameMode | null>(null); const [gameMode, setGameMode] = useState<GameMode | null>(null);
// 1P state
const [board, setBoard] = useState<Board>(createBoard); const [board, setBoard] = useState<Board>(createBoard);
const [piece, setPiece] = useState<Piece>(randomTetromino); const [piece, setPiece] = useState<Piece>(randomTetromino);
const [nextPiece, setNextPiece] = useState<Piece>(randomTetromino); const [nextPiece, setNextPiece] = useState<Piece>(randomTetromino);
@@ -104,34 +98,18 @@ const Tetris = () => {
const [highScore, setHighScore] = useState(0); const [highScore, setHighScore] = useState(0);
const [lines, setLines] = useState(0); const [lines, setLines] = useState(0);
// 2P state
const [player1, setPlayer1] = useState<PlayerState>(() => { const [player1, setPlayer1] = useState<PlayerState>(() => {
const p1Piece = randomTetromino(); const p1Piece = randomTetromino();
const p1Next = randomTetromino(false, p1Piece.shape); const p1Next = randomTetromino(false, p1Piece.shape);
return { return { board: createBoard(), piece: p1Piece, nextPiece: p1Next, score: 0, lines: 0, gameOver: false };
board: createBoard(),
piece: p1Piece,
nextPiece: p1Next,
score: 0,
lines: 0,
gameOver: false,
};
}); });
const [player2, setPlayer2] = useState<PlayerState>(() => { const [player2, setPlayer2] = useState<PlayerState>(() => {
const p2Piece = randomTetromino(true); const p2Piece = randomTetromino(true);
const p2Next = randomTetromino(true, p2Piece.shape); const p2Next = randomTetromino(true, p2Piece.shape);
return { return { board: createBoard(), piece: p2Piece, nextPiece: p2Next, score: 0, lines: 0, gameOver: false };
board: createBoard(),
piece: p2Piece,
nextPiece: p2Next,
score: 0,
lines: 0,
gameOver: false,
};
}); });
const [winner, setWinner] = useState<string | null>(null); const [winner, setWinner] = useState<string | null>(null);
// Common state
const [gameOver, setGameOver] = useState(false); const [gameOver, setGameOver] = useState(false);
const [gameComplete, setGameComplete] = useState(false); const [gameComplete, setGameComplete] = useState(false);
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
@@ -139,7 +117,6 @@ const Tetris = () => {
const [showGlitchCrash, setShowGlitchCrash] = useState(false); const [showGlitchCrash, setShowGlitchCrash] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const gameRef = useRef<HTMLDivElement>(null); const gameRef = useRef<HTMLDivElement>(null);
const { enterFullscreen, exitFullscreen } = useBrowserFullscreen();
const getCellSize = useCallback(() => { const getCellSize = useCallback(() => {
if (typeof window === 'undefined') return 28; if (typeof window === 'undefined') return 28;
@@ -149,9 +126,7 @@ const Tetris = () => {
const maxHeight = window.innerHeight - 440; const maxHeight = window.innerHeight - 440;
return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 24); return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 24);
} }
if (gameMode === '2p') { if (gameMode === '2p') return isFullscreen ? 26 : 22;
return isFullscreen ? 26 : 22;
}
return isFullscreen ? 34 : 28; return isFullscreen ? 34 : 28;
}, [isFullscreen, gameMode]); }, [isFullscreen, gameMode]);
@@ -164,30 +139,24 @@ const Tetris = () => {
return () => window.removeEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);
}, [getCellSize]); }, [getCellSize]);
useEffect(() => { useEffect(() => { setCellSize(getCellSize()); }, [isFullscreen, getCellSize, gameMode]);
setCellSize(getCellSize());
}, [isFullscreen, getCellSize, gameMode]);
useEffect(() => { useEffect(() => {
if (gameStarted && isMobile && !isFullscreen) { if (gameStarted && isMobile && !isFullscreen) setIsFullscreen(true);
setIsFullscreen(true); }, [gameStarted, isMobile, isFullscreen]);
enterFullscreen();
}
}, [gameStarted, isMobile, isFullscreen, enterFullscreen]);
const toggleFullscreen = async () => { const toggleFullscreen = () => {
if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); } setIsFullscreen(prev => !prev);
else { setIsFullscreen(false); await exitFullscreen(); }
playSound('click'); playSound('click');
}; };
useEffect(() => { useEffect(() => {
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isFullscreen) { setIsFullscreen(false); exitFullscreen(); } if (e.key === 'Escape' && isFullscreen) setIsFullscreen(false);
}; };
window.addEventListener('keydown', handleEscape); window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape); return () => window.removeEventListener('keydown', handleEscape);
}, [isFullscreen, exitFullscreen]); }, [isFullscreen]);
useEffect(() => { useEffect(() => {
const saved = localStorage.getItem(HIGHSCORE_KEY); const saved = localStorage.getItem(HIGHSCORE_KEY);
@@ -236,7 +205,6 @@ const Tetris = () => {
return { board: newBoard, cleared }; return { board: newBoard, cleared };
}, []); }, []);
// 1P movement functions
const moveDown = useCallback(() => { const moveDown = useCallback(() => {
if (gameOver || gameComplete || isPaused || !gameStarted || gameMode !== '1p') return; if (gameOver || gameComplete || isPaused || !gameStarted || gameMode !== '1p') return;
const newPiece = { ...piece, y: piece.y + 1 }; const newPiece = { ...piece, y: piece.y + 1 };
@@ -292,7 +260,6 @@ const Tetris = () => {
const startGame = (mode: GameMode) => { const startGame = (mode: GameMode) => {
setGameMode(mode); setGameMode(mode);
if (mode === '1p') { if (mode === '1p') {
setBoard(createBoard()); setBoard(createBoard());
setPiece(randomTetromino()); setPiece(randomTetromino());
@@ -300,25 +267,10 @@ const Tetris = () => {
setScore(0); setScore(0);
setLines(0); setLines(0);
} else { } else {
setPlayer1({ setPlayer1({ board: createBoard(), piece: randomTetromino(), nextPiece: randomTetromino(), score: 0, lines: 0, gameOver: false });
board: createBoard(), setPlayer2({ board: createBoard(), piece: randomTetromino(true), nextPiece: randomTetromino(true), score: 0, lines: 0, gameOver: false });
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); setWinner(null);
} }
setGameOver(false); setGameOver(false);
setGameComplete(false); setGameComplete(false);
setIsPaused(false); setIsPaused(false);
@@ -333,82 +285,60 @@ const Tetris = () => {
playSound('click'); playSound('click');
}; };
// 2P movement helpers
const movePlayer = useCallback((playerNum: 1 | 2, action: 'left' | 'right' | 'down' | 'rotate' | 'drop') => { const movePlayer = useCallback((playerNum: 1 | 2, action: 'left' | 'right' | 'down' | 'rotate' | 'drop') => {
if (gameOver || isPaused || !gameStarted || gameMode !== '2p') return; if (gameOver || isPaused || !gameStarted || gameMode !== '2p') return;
const setPlayer = playerNum === 1 ? setPlayer1 : setPlayer2; const setPlayer = playerNum === 1 ? setPlayer1 : setPlayer2;
const player = playerNum === 1 ? player1 : player2; const player = playerNum === 1 ? player1 : player2;
const isP2 = playerNum === 2; const isP2 = playerNum === 2;
if (player.gameOver) return; if (player.gameOver) return;
setPlayer(prev => { setPlayer(prev => {
let newPiece = { ...prev.piece }; let newPiece = { ...prev.piece };
switch (action) { switch (action) {
case 'left': case 'left':
newPiece.x--; newPiece.x--;
if (!isValidMove(newPiece, prev.board)) return prev; if (!isValidMove(newPiece, prev.board)) return prev;
playSound('hover'); playSound('hover');
return { ...prev, piece: newPiece }; return { ...prev, piece: newPiece };
case 'right': case 'right':
newPiece.x++; newPiece.x++;
if (!isValidMove(newPiece, prev.board)) return prev; if (!isValidMove(newPiece, prev.board)) return prev;
playSound('hover'); playSound('hover');
return { ...prev, piece: newPiece }; return { ...prev, piece: newPiece };
case 'rotate': case 'rotate':
newPiece.shape = rotate(prev.piece.shape); newPiece.shape = rotate(prev.piece.shape);
if (!isValidMove(newPiece, prev.board)) return prev; if (!isValidMove(newPiece, prev.board)) return prev;
playSound('hover'); playSound('hover');
return { ...prev, piece: newPiece }; return { ...prev, piece: newPiece };
case 'drop': case 'drop':
while (isValidMove({ ...newPiece, y: newPiece.y + 1 }, prev.board)) newPiece.y++; while (isValidMove({ ...newPiece, y: newPiece.y + 1 }, prev.board)) newPiece.y++;
playSound('click'); playSound('click');
return { ...prev, piece: newPiece }; return { ...prev, piece: newPiece };
case 'down': case 'down':
newPiece.y++; newPiece.y++;
if (isValidMove(newPiece, prev.board)) { if (isValidMove(newPiece, prev.board)) return { ...prev, piece: newPiece };
return { ...prev, piece: newPiece }; else {
} else {
const mergedBoard = mergePiece(prev.board, prev.piece); const mergedBoard = mergePiece(prev.board, prev.piece);
const { board: clearedBoard, cleared } = clearLines(mergedBoard); const { board: clearedBoard, cleared } = clearLines(mergedBoard);
const newScore = prev.score + (cleared > 0 ? cleared * 100 * cleared : 0); const newScore = prev.score + (cleared > 0 ? cleared * 100 * cleared : 0);
const newLines = prev.lines + cleared; const newLines = prev.lines + cleared;
if (cleared > 0) playSound('success'); if (cleared > 0) playSound('success');
else playSound('click'); else playSound('click');
const newTetromino = prev.nextPiece; const newTetromino = prev.nextPiece;
const nextNext = randomTetromino(isP2, prev.nextPiece.shape); const nextNext = randomTetromino(isP2, prev.nextPiece.shape);
if (!isValidMove(newTetromino, clearedBoard)) { if (!isValidMove(newTetromino, clearedBoard)) {
playSound('error'); playSound('error');
return { ...prev, board: clearedBoard, score: newScore, lines: newLines, gameOver: true }; 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; return prev;
}); });
}, [gameOver, isPaused, gameStarted, gameMode, player1, player2, isValidMove, mergePiece, clearLines, playSound]); }, [gameOver, isPaused, gameStarted, gameMode, player1, player2, isValidMove, mergePiece, clearLines, playSound]);
// Check 2P win condition
useEffect(() => { useEffect(() => {
if (gameMode !== '2p' || !gameStarted) return; if (gameMode !== '2p' || !gameStarted) return;
if (player1.gameOver && player2.gameOver) { if (player1.gameOver && player2.gameOver) {
setGameOver(true); setGameOver(true);
setWinner(player1.score > player2.score ? 'P1 Wins!' : player2.score > player1.score ? 'P2 Wins!' : 'Draw!'); 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]); }, [player1.gameOver, player2.gameOver, player1.score, player2.score, gameMode, gameStarted]);
// Keyboard handler
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (!gameStarted) return; if (!gameStarted) return;
if (gameMode === '1p') { if (gameMode === '1p') {
switch (e.key) { switch (e.key) {
case 'ArrowLeft': case 'a': e.preventDefault(); moveLeft(); break; case 'ArrowLeft': case 'a': e.preventDefault(); moveLeft(); break;
@@ -436,20 +364,16 @@ const Tetris = () => {
case 'p': e.preventDefault(); togglePause(); break; case 'p': e.preventDefault(); togglePause(); break;
} }
} else { } else {
// P1: WASD + Q for drop
if (e.key === 'w' || e.key === 'W') { e.preventDefault(); movePlayer(1, 'rotate'); } 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 === 'a' || e.key === 'A') { e.preventDefault(); movePlayer(1, 'left'); }
if (e.key === 's' || e.key === 'S') { e.preventDefault(); movePlayer(1, 'down'); } 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 === 'd' || e.key === 'D') { e.preventDefault(); movePlayer(1, 'right'); }
if (e.key === 'q' || e.key === 'Q') { e.preventDefault(); movePlayer(1, 'drop'); } 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 === 'ArrowUp') { e.preventDefault(); movePlayer(2, 'rotate'); }
if (e.key === 'ArrowLeft') { e.preventDefault(); movePlayer(2, 'left'); } if (e.key === 'ArrowLeft') { e.preventDefault(); movePlayer(2, 'left'); }
if (e.key === 'ArrowDown') { e.preventDefault(); movePlayer(2, 'down'); } if (e.key === 'ArrowDown') { e.preventDefault(); movePlayer(2, 'down'); }
if (e.key === 'ArrowRight') { e.preventDefault(); movePlayer(2, 'right'); } if (e.key === 'ArrowRight') { e.preventDefault(); movePlayer(2, 'right'); }
if (e.key === '/') { e.preventDefault(); movePlayer(2, 'drop'); } if (e.key === '/') { e.preventDefault(); movePlayer(2, 'drop'); }
if (e.key === 'p' || e.key === 'P') { e.preventDefault(); togglePause(); } if (e.key === 'p' || e.key === 'P') { e.preventDefault(); togglePause(); }
} }
}; };
@@ -457,14 +381,12 @@ const Tetris = () => {
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [moveLeft, moveRight, moveDown, rotatePiece, hardDrop, gameStarted, gameMode, movePlayer]); }, [moveLeft, moveRight, moveDown, rotatePiece, hardDrop, gameStarted, gameMode, movePlayer]);
// 1P tick
useEffect(() => { useEffect(() => {
if (!gameStarted || gameOver || gameComplete || isPaused || gameMode !== '1p') return; if (!gameStarted || gameOver || gameComplete || isPaused || gameMode !== '1p') return;
const interval = setInterval(moveDown, TICK_SPEED); const interval = setInterval(moveDown, TICK_SPEED);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [moveDown, gameStarted, gameOver, gameComplete, isPaused, gameMode]); }, [moveDown, gameStarted, gameOver, gameComplete, isPaused, gameMode]);
// 2P tick
useEffect(() => { useEffect(() => {
if (!gameStarted || gameOver || isPaused || gameMode !== '2p') return; if (!gameStarted || gameOver || isPaused || gameMode !== '2p') return;
const interval = setInterval(() => { const interval = setInterval(() => {
@@ -472,7 +394,7 @@ const Tetris = () => {
if (!player2.gameOver) movePlayer(2, 'down'); if (!player2.gameOver) movePlayer(2, 'down');
}, TICK_SPEED); }, TICK_SPEED);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [gameStarted, gameOver, isPaused, gameMode]); }, [gameStarted, gameOver, isPaused, gameMode, player1.gameOver, player2.gameOver, movePlayer]);
const renderBoard1P = () => { const renderBoard1P = () => {
const displayBoard = board.map(row => [...row]); const displayBoard = board.map(row => [...row]);
@@ -512,70 +434,51 @@ const Tetris = () => {
return displayBoard.map((row, y) => ( return displayBoard.map((row, y) => (
<div key={y} className="flex"> <div key={y} className="flex">
{row.map((cell, x) => ( {row.map((cell, x) => (
<div <div key={`${y}-${x}`} className={`border transition-colors duration-100 ${borderColor}`}
key={`${y}-${x}`} style={{ width: cellSize, height: cellSize, backgroundColor: cell || 'transparent', boxShadow: cell ? `0 0 5px ${cell}` : 'none' }} />
className={`border transition-colors duration-100 ${borderColor}`}
style={{
width: cellSize,
height: cellSize,
backgroundColor: cell || 'transparent',
boxShadow: cell ? `0 0 5px ${cell}` : 'none',
}}
/>
))} ))}
</div> </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()} />; if (showGlitchCrash) return <GlitchCrash onComplete={() => window.location.reload()} />;
// Mode selection screen
if (!gameMode) { if (!gameMode) {
return ( return (
<motion.div <motion.div ref={gameRef} tabIndex={0} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }} className="flex flex-col h-full">
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"> <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> <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> <h1 className="font-minecraft text-2xl md:text-3xl text-primary text-glow-strong">Tetris</h1>
</div> </div>
<div className="flex-1 flex flex-col items-center justify-center gap-6"> <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> <p className="font-pixel text-sm text-foreground/70">Select game mode</p>
<div className="flex gap-4"> <div className="flex gap-4">
<button <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">
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" /> <User size={32} className="text-primary" />
<span className="font-minecraft text-lg text-primary">1 Player</span> <span className="font-minecraft text-lg text-primary">1 Player</span>
<span className="font-pixel text-xs text-foreground/60">Solo mode</span> <span className="font-pixel text-xs text-foreground/60">Solo mode</span>
</button> </button>
{!isMobile && ( {!isMobile && (
<button <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">
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" /> <Users size={32} className="text-purple-400" />
<span className="font-minecraft text-lg text-purple-400">2 Players</span> <span className="font-minecraft text-lg text-purple-400">2 Players</span>
<span className="font-pixel text-xs text-foreground/60">Local versus</span> <span className="font-pixel text-xs text-foreground/60">Local versus</span>
</button> </button>
)} )}
</div> </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"> <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> <p className="font-pixel text-xs text-foreground/60">HIGH SCORE: <span className="text-primary">{highScore.toLocaleString()}</span></p>
</div> </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 }} <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'}`}> 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"> <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> <div className="border-2 border-primary box-glow p-1 bg-background/80">{renderBoard1P()}</div>
{!isMobile && ( {!isMobile && (
<div className="flex flex-col gap-3 min-w-[160px]"> <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"> <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> <p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider mb-3">Next</p>
<div className="flex justify-center"> <div className="flex justify-center">{renderNextPiece(nextPiece)}</div>
<div className="flex flex-col gap-1"> </div>
{nextPiece.shape.map((row, y) => ( <div className="border-2 border-primary/50 p-4 bg-background/60">
<div key={y} className="flex gap-1"> <p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider">Score</p>
{row.map((val, x) => ( <p className="font-minecraft text-2xl text-primary text-glow-strong">{score.toLocaleString()}</p>
<div key={`${y}-${x}`} className={`w-4 h-4 ${val ? 'bg-primary box-glow' : 'bg-transparent'}`} /> </div>
))} <div className="border border-primary/30 p-3 bg-background/40">
</div> <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>
</div> <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>
</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> </div>
)} )}
</> </>
) : ( ) : (
<> <div className="flex gap-4 items-start">
{/* ... keep existing code (2P layout) */} <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> </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> </motion.div>
); );
return isFullscreen ? <FullscreenPortal>{content}</FullscreenPortal> : content;
}; };
export default Tetris; export default Tetris;