Unified fullscreen across arcade games with Portal; added cursor rendering, pacman fruit bonuses, adjusted breakout speed, and 2P theming tweaks.
- Made arcade fullscreen use portal approach and render MatrixCursor inside fullscreen - Added Pac-Man fruit bonuses spawning at center after certain dot counts - Slowed Breakout ball speed for classic feel - Adjusted 2P theming: swap secondary color with theme and ensure correct coloring on 2P UI X-Lovable-Edit-ID: edt-2a44cfa1-a1f4-40ad-98df-f3491c2c6dc0
This commit is contained in:
+4
-4
@@ -40,8 +40,8 @@ html {
|
||||
--primary: 120 100% 50%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
|
||||
--secondary: 120 100% 15%;
|
||||
--secondary-foreground: 120 100% 50%;
|
||||
--secondary: 0 100% 50%;
|
||||
--secondary-foreground: 0 0% 0%;
|
||||
|
||||
--muted: 120 20% 10%;
|
||||
--muted-foreground: 120 30% 60%;
|
||||
@@ -82,8 +82,8 @@ html {
|
||||
--primary: 0 100% 50%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
|
||||
--secondary: 0 100% 15%;
|
||||
--secondary-foreground: 0 100% 50%;
|
||||
--secondary: 120 100% 50%;
|
||||
--secondary-foreground: 0 0% 0%;
|
||||
|
||||
--muted: 0 20% 10%;
|
||||
--muted-foreground: 0 30% 60%;
|
||||
|
||||
@@ -12,6 +12,7 @@ import GlitchText from '@/components/GlitchText';
|
||||
import MessageContent from '@/components/MessageContent';
|
||||
import { AI_PROVIDERS, AIProvider, getProvider, CUSTOM_API_STORAGE_KEY } from '@/lib/aiProviders';
|
||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||
import MatrixCursor from '@/components/MatrixCursor';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -406,6 +407,8 @@ const AIChat = () => {
|
||||
: 'flex flex-col h-full'
|
||||
}
|
||||
>
|
||||
{/* Render cursor inside portal for fullscreen */}
|
||||
{isFullscreen && <MatrixCursor />}
|
||||
<div className="flex items-center justify-between gap-2 mb-3 flex-shrink-0">
|
||||
<GlitchText
|
||||
text="AI Terminal"
|
||||
|
||||
+22
-3
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useSettings } from '@/contexts/SettingsContext';
|
||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||
@@ -8,6 +9,7 @@ import { Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||
import GameTouchButton from '@/components/GameTouchButton';
|
||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||
import MatrixCursor from '@/components/MatrixCursor';
|
||||
|
||||
const PADDLE_HEIGHT = 14;
|
||||
const BRICK_ROWS = 6;
|
||||
@@ -132,6 +134,15 @@ const Breakout = () => {
|
||||
if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); }
|
||||
}, [gameStarted, isMobile, isFullscreen, enterFullscreen]);
|
||||
|
||||
// Prevent page scroll while in fullscreen mode (portal overlays the viewport)
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (!isFullscreen) return;
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => { document.body.style.overflow = prevOverflow; };
|
||||
}, [isFullscreen]);
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); }
|
||||
else { setIsFullscreen(false); await exitFullscreen(); }
|
||||
@@ -183,7 +194,8 @@ const Breakout = () => {
|
||||
}, []);
|
||||
|
||||
const resetBall = useCallback((ballRefToReset: React.MutableRefObject<{x: number, y: number, dx: number, dy: number}>, paddleRefToReset: React.MutableRefObject<number>, lvl: number) => {
|
||||
const speed = 2.5 + lvl * 0.3;
|
||||
// Slower base speed for classic Breakout feel (was 2.5)
|
||||
const speed = 1.8 + lvl * 0.2;
|
||||
ballRefToReset.current = { x: canvasSize.width / 2, y: canvasSize.height - 60, dx: (Math.random() > 0.5 ? 1 : -1) * speed, dy: -speed };
|
||||
paddleRefToReset.current = canvasSize.width / 2 - paddleWidth / 2;
|
||||
}, [canvasSize.width, canvasSize.height, paddleWidth]);
|
||||
@@ -538,9 +550,11 @@ const Breakout = () => {
|
||||
|
||||
if (showGlitchCrash) return <GlitchCrash onComplete={() => window.location.reload()} />;
|
||||
|
||||
return (
|
||||
const gameUi = (
|
||||
<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-[100] bg-background p-4 w-screen h-screen' : 'h-full'}`}>
|
||||
{/* Render cursor inside portal for fullscreen */}
|
||||
{isFullscreen && <MatrixCursor />}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/games" className="font-pixel text-sm text-foreground/50 hover:text-primary transition-colors">{'<'} Back</Link>
|
||||
@@ -712,6 +726,11 @@ const Breakout = () => {
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// Portal to body for true fullscreen, escaping MainLayout transforms
|
||||
return isFullscreen && typeof document !== 'undefined'
|
||||
? createPortal(gameUi, document.body)
|
||||
: gameUi;
|
||||
};
|
||||
|
||||
export default Breakout;
|
||||
|
||||
+78
-3
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useSettings } from '@/contexts/SettingsContext';
|
||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||
@@ -9,6 +10,7 @@ import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||
import { useSwipeControls } from '@/hooks/useSwipeControls';
|
||||
import GameTouchButton from '@/components/GameTouchButton';
|
||||
import MatrixCursor from '@/components/MatrixCursor';
|
||||
|
||||
const GRID_WIDTH = 21;
|
||||
const GRID_HEIGHT = 21;
|
||||
@@ -90,6 +92,22 @@ const Pacman = () => {
|
||||
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
|
||||
const [level, setLevel] = useState(1);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
// Fruit bonus system (spawns at center after eating certain dot counts)
|
||||
const [fruit, setFruit] = useState<{ x: number; y: number; type: string; points: number } | null>(null);
|
||||
const [dotsEaten, setDotsEaten] = useState(0);
|
||||
const fruitTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const FRUIT_TYPES = [
|
||||
{ type: 'cherry', points: 100, emoji: '🍒' },
|
||||
{ type: 'strawberry', points: 300, emoji: '🍓' },
|
||||
{ type: 'orange', points: 500, emoji: '🍊' },
|
||||
{ type: 'apple', points: 700, emoji: '🍎' },
|
||||
{ type: 'melon', points: 1000, emoji: '🍈' },
|
||||
{ type: 'galaxian', points: 2000, emoji: '🚀' },
|
||||
{ type: 'bell', points: 3000, emoji: '🔔' },
|
||||
{ type: 'key', points: 5000, emoji: '🔑' },
|
||||
];
|
||||
const gameRef = useRef<HTMLDivElement>(null);
|
||||
const powerTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
@@ -137,6 +155,15 @@ const Pacman = () => {
|
||||
if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); }
|
||||
}, [gameStarted, isMobile, isFullscreen, enterFullscreen]);
|
||||
|
||||
// Prevent page scroll while in fullscreen mode (portal overlays the viewport)
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (!isFullscreen) return;
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => { document.body.style.overflow = prevOverflow; };
|
||||
}, [isFullscreen]);
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); }
|
||||
else { setIsFullscreen(false); await exitFullscreen(); }
|
||||
@@ -198,14 +225,27 @@ const Pacman = () => {
|
||||
powerTimerRef.current = setTimeout(() => { setIsPowered(false); setGhosts(prev => prev.map(g => ({ ...g, eaten: false }))); }, POWER_DURATION);
|
||||
};
|
||||
|
||||
// Spawn fruit at center of maze
|
||||
const spawnFruit = useCallback(() => {
|
||||
if (fruitTimerRef.current) clearTimeout(fruitTimerRef.current);
|
||||
// Get fruit type based on level (capped at available types)
|
||||
const fruitIndex = Math.min(level - 1, FRUIT_TYPES.length - 1);
|
||||
const fruitType = FRUIT_TYPES[fruitIndex];
|
||||
setFruit({ x: 10, y: 9, type: fruitType.emoji, points: fruitType.points });
|
||||
// Fruit disappears after 10 seconds if not eaten
|
||||
fruitTimerRef.current = setTimeout(() => setFruit(null), 10000);
|
||||
}, [level, FRUIT_TYPES]);
|
||||
|
||||
const startGame = () => {
|
||||
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
|
||||
if (fruitTimerRef.current) clearTimeout(fruitTimerRef.current);
|
||||
setPacman({ x: 10, y: 15 }); setDirection('right'); setNextDirection('right'); setMouthOpen(true);
|
||||
setPacman2({ x: 10, y: 15 }); setDirection2('right'); setNextDirection2('right'); setMouthOpen2(true);
|
||||
setLives(3); setLives2(3);
|
||||
setGhosts([{ pos: { x: 9, y: 9 }, dir: 'left', eaten: false }, { pos: { x: 10, y: 9 }, dir: 'up', eaten: false }, { pos: { x: 11, y: 9 }, dir: 'right', eaten: false }]);
|
||||
const { dots: newDots, powerPellets: newPowerPellets } = initDots();
|
||||
setDots(newDots); setPowerPellets(newPowerPellets); setIsPowered(false); setScore(0); setScore2(0); setLevel(1);
|
||||
setDotsEaten(0); setFruit(null);
|
||||
setGameOver(false); setGameComplete(false); setIsPaused(false); setGameStarted(true); setShowModeSelection(false);
|
||||
setPlayer1Eliminated(false); setPlayer2Eliminated(false); setShowEliminationPrompt(false);
|
||||
playSound('success'); gameRef.current?.focus();
|
||||
@@ -246,6 +286,7 @@ const Pacman = () => {
|
||||
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
|
||||
if (invulnerableTimerRef.current) clearTimeout(invulnerableTimerRef.current);
|
||||
if (invulnerable2TimerRef.current) clearTimeout(invulnerable2TimerRef.current);
|
||||
if (fruitTimerRef.current) clearTimeout(fruitTimerRef.current);
|
||||
}; }, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -350,6 +391,14 @@ const Pacman = () => {
|
||||
playSound('success');
|
||||
} else if (dots.has(posKey)) {
|
||||
setDots(prev => { const nd = new Set(prev); nd.delete(posKey); return nd; });
|
||||
// Track dots eaten and spawn fruit at 70 and 170 dots
|
||||
setDotsEaten(prev => {
|
||||
const newCount = prev + 1;
|
||||
if (newCount === 70 || newCount === 170) {
|
||||
spawnFruit();
|
||||
}
|
||||
return newCount;
|
||||
});
|
||||
setPlayerScore(prev => {
|
||||
const ns = Math.min(prev + 10, MAX_SCORE);
|
||||
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
||||
@@ -358,6 +407,19 @@ const Pacman = () => {
|
||||
});
|
||||
playSound('hover');
|
||||
}
|
||||
|
||||
// Check for fruit pickup
|
||||
if (fruit && newPos.x === fruit.x && newPos.y === fruit.y) {
|
||||
setPlayerScore(prev => {
|
||||
const ns = Math.min(prev + fruit.points, MAX_SCORE);
|
||||
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
||||
if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); }
|
||||
return ns;
|
||||
});
|
||||
setFruit(null);
|
||||
if (fruitTimerRef.current) clearTimeout(fruitTimerRef.current);
|
||||
playSound('success');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -647,12 +709,18 @@ const Pacman = () => {
|
||||
const ghostIndex = ghosts.findIndex(g => g.pos.x === x && g.pos.y === y && !g.eaten);
|
||||
const isDot = dots.has(`${x},${y}`);
|
||||
const isPowerPellet = powerPellets.has(`${x},${y}`);
|
||||
const isFruitHere = fruit && fruit.x === x && fruit.y === y;
|
||||
cells.push(
|
||||
<div key={`${x}-${y}`} className={`flex items-center justify-center transition-all duration-200 ease-out ${isWall ? 'bg-primary/20 border border-primary/40' : isTunnel ? 'bg-background/30' : 'bg-background/50 border border-primary/5'}`} style={{ width: cellSize, height: cellSize }}>
|
||||
{isPacmanHere && <div className={`drop-shadow-lg ${p1Invulnerable ? 'animate-pulse' : ''}`} style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: 'drop-shadow(0 0 4px hsl(var(--primary)))', opacity: p1Invulnerable ? 0.5 : 1 }}>{renderPacman()}</div>}
|
||||
{isPacman2Here && <div className={`drop-shadow-lg ${p2Invulnerable ? 'animate-pulse' : ''}`} style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: 'drop-shadow(0 0 4px hsl(120 70% 50%))', opacity: p2Invulnerable ? 0.5 : 1 }}>{renderPacman2()}</div>}
|
||||
{ghostIndex !== -1 && !isPacmanHere && !isPacman2Here && <div style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out' }}>{renderGhost(ghostIndex, ghosts[ghostIndex].eaten)}</div>}
|
||||
{isDot && !isPacmanHere && !isPacman2Here && ghostIndex === -1 && <div className="w-1.5 h-1.5 bg-primary/80 rounded-full transition-all duration-200 ease-out animate-pulse" style={{ animationDuration: '2s' }} />}
|
||||
{isFruitHere && !isPacmanHere && !isPacman2Here && ghostIndex === -1 && (
|
||||
<div className="animate-bounce text-lg" style={{ fontSize: `${Math.max(cellSize * 0.7, 14)}px` }} title={`${fruit.points} points`}>
|
||||
{fruit.type}
|
||||
</div>
|
||||
)}
|
||||
{isDot && !isPacmanHere && !isPacman2Here && ghostIndex === -1 && !isFruitHere && <div className="w-1.5 h-1.5 bg-primary/80 rounded-full transition-all duration-200 ease-out animate-pulse" style={{ animationDuration: '2s' }} />}
|
||||
{isPowerPellet && !isPacmanHere && !isPacman2Here && ghostIndex === -1 && <div className="w-3 h-3 bg-primary rounded-full animate-pulse box-glow transition-all duration-200 ease-out" />}
|
||||
</div>
|
||||
);
|
||||
@@ -663,9 +731,11 @@ const Pacman = () => {
|
||||
|
||||
if (showGlitchCrash) return <GlitchCrash onComplete={() => window.location.reload()} />;
|
||||
|
||||
return (
|
||||
const gameUi = (
|
||||
<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-[100] bg-background p-4 w-screen h-screen' : 'h-full'}`}>
|
||||
{/* Render cursor inside portal for fullscreen */}
|
||||
{isFullscreen && <MatrixCursor />}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/games" className="font-pixel text-sm text-foreground/50 hover:text-primary transition-colors">{'<'} Back</Link>
|
||||
@@ -892,6 +962,11 @@ const Pacman = () => {
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// Portal to body for true fullscreen, escaping MainLayout transforms
|
||||
return isFullscreen && typeof document !== 'undefined'
|
||||
? createPortal(gameUi, document.body)
|
||||
: gameUi;
|
||||
};
|
||||
|
||||
export default Pacman;
|
||||
|
||||
+20
-2
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useSettings } from '@/contexts/SettingsContext';
|
||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||
@@ -9,6 +10,7 @@ import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||
import { useSwipeControls } from '@/hooks/useSwipeControls';
|
||||
import GameTouchButton from '@/components/GameTouchButton';
|
||||
import MatrixCursor from '@/components/MatrixCursor';
|
||||
|
||||
const GRID_SIZE = 20;
|
||||
const TICK_SPEED = 120;
|
||||
@@ -117,6 +119,15 @@ const Snake = () => {
|
||||
}
|
||||
}, [gameStarted, isMobile, isFullscreen, enterFullscreen]);
|
||||
|
||||
// Prevent page scroll while in fullscreen mode (portal overlays the viewport)
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (!isFullscreen) return;
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => { document.body.style.overflow = prevOverflow; };
|
||||
}, [isFullscreen]);
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
if (!isFullscreen) {
|
||||
setIsFullscreen(true);
|
||||
@@ -533,15 +544,17 @@ const Snake = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
const gameUi = (
|
||||
<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-[100] bg-background p-4 w-screen h-screen' : 'h-full'}`}
|
||||
>
|
||||
{/* Render cursor inside portal for fullscreen */}
|
||||
{isFullscreen && <MatrixCursor />}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => { setGameMode(null); setGameStarted(false); }} className="font-pixel text-sm text-foreground/50 hover:text-primary transition-colors">{'<'} Back</button>
|
||||
@@ -739,6 +752,11 @@ const Snake = () => {
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// Portal to body for true fullscreen, escaping MainLayout transforms
|
||||
return isFullscreen && typeof document !== 'undefined'
|
||||
? createPortal(gameUi, document.body)
|
||||
: gameUi;
|
||||
};
|
||||
|
||||
export default Snake;
|
||||
+20
-2
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useSettings } from '@/contexts/SettingsContext';
|
||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||
@@ -9,6 +10,7 @@ import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||
import { useSwipeControls, SwipeDirection } from '@/hooks/useSwipeControls';
|
||||
import GameTouchButton from '@/components/GameTouchButton';
|
||||
import MatrixCursor from '@/components/MatrixCursor';
|
||||
|
||||
const BOARD_WIDTH = 10;
|
||||
const BOARD_HEIGHT = 20;
|
||||
@@ -152,6 +154,15 @@ const Tetris = () => {
|
||||
}
|
||||
}, [gameStarted, isMobile, isFullscreen, enterFullscreen]);
|
||||
|
||||
// Prevent page scroll while in fullscreen mode (portal overlays the viewport)
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (!isFullscreen) return;
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => { document.body.style.overflow = prevOverflow; };
|
||||
}, [isFullscreen]);
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
if (!isFullscreen) {
|
||||
setIsFullscreen(true);
|
||||
@@ -530,9 +541,11 @@ const Tetris = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
const gameUi = (
|
||||
<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-[100] bg-background p-4 w-screen h-screen' : 'h-full'}`}>
|
||||
{/* Render cursor inside portal for fullscreen */}
|
||||
{isFullscreen && <MatrixCursor />}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => { setGameMode(null); setGameStarted(false); }} className="font-pixel text-sm text-foreground/50 hover:text-primary transition-colors">{'<'} Back</button>
|
||||
@@ -701,6 +714,11 @@ const Tetris = () => {
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// Portal to body for true fullscreen, escaping MainLayout transforms
|
||||
return isFullscreen && typeof document !== 'undefined'
|
||||
? createPortal(gameUi, document.body)
|
||||
: gameUi;
|
||||
};
|
||||
|
||||
export default Tetris;
|
||||
|
||||
Reference in New Issue
Block a user