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: 120 100% 50%;
|
||||||
--primary-foreground: 0 0% 0%;
|
--primary-foreground: 0 0% 0%;
|
||||||
|
|
||||||
--secondary: 120 100% 15%;
|
--secondary: 0 100% 50%;
|
||||||
--secondary-foreground: 120 100% 50%;
|
--secondary-foreground: 0 0% 0%;
|
||||||
|
|
||||||
--muted: 120 20% 10%;
|
--muted: 120 20% 10%;
|
||||||
--muted-foreground: 120 30% 60%;
|
--muted-foreground: 120 30% 60%;
|
||||||
@@ -82,8 +82,8 @@ html {
|
|||||||
--primary: 0 100% 50%;
|
--primary: 0 100% 50%;
|
||||||
--primary-foreground: 0 0% 0%;
|
--primary-foreground: 0 0% 0%;
|
||||||
|
|
||||||
--secondary: 0 100% 15%;
|
--secondary: 120 100% 50%;
|
||||||
--secondary-foreground: 0 100% 50%;
|
--secondary-foreground: 0 0% 0%;
|
||||||
|
|
||||||
--muted: 0 20% 10%;
|
--muted: 0 20% 10%;
|
||||||
--muted-foreground: 0 30% 60%;
|
--muted-foreground: 0 30% 60%;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import GlitchText from '@/components/GlitchText';
|
|||||||
import MessageContent from '@/components/MessageContent';
|
import MessageContent from '@/components/MessageContent';
|
||||||
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 { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||||
|
import MatrixCursor from '@/components/MatrixCursor';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -406,6 +407,8 @@ const AIChat = () => {
|
|||||||
: 'flex flex-col h-full'
|
: '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">
|
<div className="flex items-center justify-between gap-2 mb-3 flex-shrink-0">
|
||||||
<GlitchText
|
<GlitchText
|
||||||
text="AI Terminal"
|
text="AI Terminal"
|
||||||
|
|||||||
+22
-3
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
@@ -8,6 +9,7 @@ import { Maximize2, Minimize2 } from 'lucide-react';
|
|||||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
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 MatrixCursor from '@/components/MatrixCursor';
|
||||||
|
|
||||||
const PADDLE_HEIGHT = 14;
|
const PADDLE_HEIGHT = 14;
|
||||||
const BRICK_ROWS = 6;
|
const BRICK_ROWS = 6;
|
||||||
@@ -132,6 +134,15 @@ const Breakout = () => {
|
|||||||
if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); }
|
if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); }
|
||||||
}, [gameStarted, isMobile, isFullscreen, 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 () => {
|
const toggleFullscreen = async () => {
|
||||||
if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); }
|
if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); }
|
||||||
else { setIsFullscreen(false); await exitFullscreen(); }
|
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 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 };
|
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;
|
paddleRefToReset.current = canvasSize.width / 2 - paddleWidth / 2;
|
||||||
}, [canvasSize.width, canvasSize.height, paddleWidth]);
|
}, [canvasSize.width, canvasSize.height, paddleWidth]);
|
||||||
@@ -538,9 +550,11 @@ const Breakout = () => {
|
|||||||
|
|
||||||
if (showGlitchCrash) return <GlitchCrash onComplete={() => window.location.reload()} />;
|
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 }}
|
<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 justify-between mb-2">
|
||||||
<div className="flex items-center gap-4">
|
<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>
|
<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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Portal to body for true fullscreen, escaping MainLayout transforms
|
||||||
|
return isFullscreen && typeof document !== 'undefined'
|
||||||
|
? createPortal(gameUi, document.body)
|
||||||
|
: gameUi;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Breakout;
|
export default Breakout;
|
||||||
|
|||||||
+78
-3
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
@@ -9,6 +10,7 @@ import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
|||||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||||
import { useSwipeControls } from '@/hooks/useSwipeControls';
|
import { useSwipeControls } from '@/hooks/useSwipeControls';
|
||||||
import GameTouchButton from '@/components/GameTouchButton';
|
import GameTouchButton from '@/components/GameTouchButton';
|
||||||
|
import MatrixCursor from '@/components/MatrixCursor';
|
||||||
|
|
||||||
const GRID_WIDTH = 21;
|
const GRID_WIDTH = 21;
|
||||||
const GRID_HEIGHT = 21;
|
const GRID_HEIGHT = 21;
|
||||||
@@ -90,6 +92,22 @@ const Pacman = () => {
|
|||||||
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
|
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
|
||||||
const [level, setLevel] = useState(1);
|
const [level, setLevel] = useState(1);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
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 gameRef = useRef<HTMLDivElement>(null);
|
||||||
const powerTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const powerTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
@@ -137,6 +155,15 @@ const Pacman = () => {
|
|||||||
if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); }
|
if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); }
|
||||||
}, [gameStarted, isMobile, isFullscreen, 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 () => {
|
const toggleFullscreen = async () => {
|
||||||
if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); }
|
if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); }
|
||||||
else { setIsFullscreen(false); await exitFullscreen(); }
|
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);
|
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 = () => {
|
const startGame = () => {
|
||||||
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
|
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
|
||||||
|
if (fruitTimerRef.current) clearTimeout(fruitTimerRef.current);
|
||||||
setPacman({ x: 10, y: 15 }); setDirection('right'); setNextDirection('right'); setMouthOpen(true);
|
setPacman({ x: 10, y: 15 }); setDirection('right'); setNextDirection('right'); setMouthOpen(true);
|
||||||
setPacman2({ x: 10, y: 15 }); setDirection2('right'); setNextDirection2('right'); setMouthOpen2(true);
|
setPacman2({ x: 10, y: 15 }); setDirection2('right'); setNextDirection2('right'); setMouthOpen2(true);
|
||||||
setLives(3); setLives2(3);
|
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 }]);
|
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();
|
const { dots: newDots, powerPellets: newPowerPellets } = initDots();
|
||||||
setDots(newDots); setPowerPellets(newPowerPellets); setIsPowered(false); setScore(0); setScore2(0); setLevel(1);
|
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);
|
setGameOver(false); setGameComplete(false); setIsPaused(false); setGameStarted(true); setShowModeSelection(false);
|
||||||
setPlayer1Eliminated(false); setPlayer2Eliminated(false); setShowEliminationPrompt(false);
|
setPlayer1Eliminated(false); setPlayer2Eliminated(false); setShowEliminationPrompt(false);
|
||||||
playSound('success'); gameRef.current?.focus();
|
playSound('success'); gameRef.current?.focus();
|
||||||
@@ -246,6 +286,7 @@ const Pacman = () => {
|
|||||||
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
|
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
|
||||||
if (invulnerableTimerRef.current) clearTimeout(invulnerableTimerRef.current);
|
if (invulnerableTimerRef.current) clearTimeout(invulnerableTimerRef.current);
|
||||||
if (invulnerable2TimerRef.current) clearTimeout(invulnerable2TimerRef.current);
|
if (invulnerable2TimerRef.current) clearTimeout(invulnerable2TimerRef.current);
|
||||||
|
if (fruitTimerRef.current) clearTimeout(fruitTimerRef.current);
|
||||||
}; }, []);
|
}; }, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -350,6 +391,14 @@ const Pacman = () => {
|
|||||||
playSound('success');
|
playSound('success');
|
||||||
} else if (dots.has(posKey)) {
|
} else if (dots.has(posKey)) {
|
||||||
setDots(prev => { const nd = new Set(prev); nd.delete(posKey); return nd; });
|
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 => {
|
setPlayerScore(prev => {
|
||||||
const ns = Math.min(prev + 10, MAX_SCORE);
|
const ns = Math.min(prev + 10, MAX_SCORE);
|
||||||
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
||||||
@@ -358,6 +407,19 @@ const Pacman = () => {
|
|||||||
});
|
});
|
||||||
playSound('hover');
|
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 ghostIndex = ghosts.findIndex(g => g.pos.x === x && g.pos.y === y && !g.eaten);
|
||||||
const isDot = dots.has(`${x},${y}`);
|
const isDot = dots.has(`${x},${y}`);
|
||||||
const isPowerPellet = powerPellets.has(`${x},${y}`);
|
const isPowerPellet = powerPellets.has(`${x},${y}`);
|
||||||
|
const isFruitHere = fruit && fruit.x === x && fruit.y === y;
|
||||||
cells.push(
|
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 }}>
|
<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>}
|
{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>}
|
{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>}
|
{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" />}
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -663,9 +731,11 @@ const Pacman = () => {
|
|||||||
|
|
||||||
if (showGlitchCrash) return <GlitchCrash onComplete={() => window.location.reload()} />;
|
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 }}
|
<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 justify-between mb-2">
|
||||||
<div className="flex items-center gap-4">
|
<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>
|
<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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Portal to body for true fullscreen, escaping MainLayout transforms
|
||||||
|
return isFullscreen && typeof document !== 'undefined'
|
||||||
|
? createPortal(gameUi, document.body)
|
||||||
|
: gameUi;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Pacman;
|
export default Pacman;
|
||||||
|
|||||||
+20
-2
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
@@ -9,6 +10,7 @@ import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
|||||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||||
import { useSwipeControls } from '@/hooks/useSwipeControls';
|
import { useSwipeControls } from '@/hooks/useSwipeControls';
|
||||||
import GameTouchButton from '@/components/GameTouchButton';
|
import GameTouchButton from '@/components/GameTouchButton';
|
||||||
|
import MatrixCursor from '@/components/MatrixCursor';
|
||||||
|
|
||||||
const GRID_SIZE = 20;
|
const GRID_SIZE = 20;
|
||||||
const TICK_SPEED = 120;
|
const TICK_SPEED = 120;
|
||||||
@@ -117,6 +119,15 @@ const Snake = () => {
|
|||||||
}
|
}
|
||||||
}, [gameStarted, isMobile, isFullscreen, 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 () => {
|
const toggleFullscreen = async () => {
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
setIsFullscreen(true);
|
setIsFullscreen(true);
|
||||||
@@ -533,15 +544,17 @@ const Snake = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const gameUi = (
|
||||||
<motion.div
|
<motion.div
|
||||||
ref={gameRef}
|
ref={gameRef}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.5 }}
|
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 justify-between mb-2">
|
||||||
<div className="flex items-center gap-4">
|
<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>
|
<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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Portal to body for true fullscreen, escaping MainLayout transforms
|
||||||
|
return isFullscreen && typeof document !== 'undefined'
|
||||||
|
? createPortal(gameUi, document.body)
|
||||||
|
: gameUi;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Snake;
|
export default Snake;
|
||||||
+20
-2
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
@@ -9,6 +10,7 @@ import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
|||||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||||
import { useSwipeControls, SwipeDirection } from '@/hooks/useSwipeControls';
|
import { useSwipeControls, SwipeDirection } from '@/hooks/useSwipeControls';
|
||||||
import GameTouchButton from '@/components/GameTouchButton';
|
import GameTouchButton from '@/components/GameTouchButton';
|
||||||
|
import MatrixCursor from '@/components/MatrixCursor';
|
||||||
|
|
||||||
const BOARD_WIDTH = 10;
|
const BOARD_WIDTH = 10;
|
||||||
const BOARD_HEIGHT = 20;
|
const BOARD_HEIGHT = 20;
|
||||||
@@ -152,6 +154,15 @@ const Tetris = () => {
|
|||||||
}
|
}
|
||||||
}, [gameStarted, isMobile, isFullscreen, 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 () => {
|
const toggleFullscreen = async () => {
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
setIsFullscreen(true);
|
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 }}
|
<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 justify-between mb-2">
|
||||||
<div className="flex items-center gap-4">
|
<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>
|
<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>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Portal to body for true fullscreen, escaping MainLayout transforms
|
||||||
|
return isFullscreen && typeof document !== 'undefined'
|
||||||
|
? createPortal(gameUi, document.body)
|
||||||
|
: gameUi;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Tetris;
|
export default Tetris;
|
||||||
|
|||||||
Reference in New Issue
Block a user