Changes
This commit is contained in:
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
+143
-149
@@ -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 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>
|
||||
</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;
|
||||
Reference in New Issue
Block a user