From 147b8cb43fa70dc92fe14cb1a387001c2c8e2413 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 08:49:30 +0000 Subject: [PATCH] Changes --- src/index.css | 8 ++--- src/pages/AIChat.tsx | 3 ++ src/pages/Breakout.tsx | 25 +++++++++++-- src/pages/Pacman.tsx | 81 ++++++++++++++++++++++++++++++++++++++++-- src/pages/Snake.tsx | 22 ++++++++++-- src/pages/Tetris.tsx | 22 ++++++++++-- 6 files changed, 147 insertions(+), 14 deletions(-) diff --git a/src/index.css b/src/index.css index a6cd5f6..d756323 100644 --- a/src/index.css +++ b/src/index.css @@ -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%; diff --git a/src/pages/AIChat.tsx b/src/pages/AIChat.tsx index 14f7aa1..b910acc 100644 --- a/src/pages/AIChat.tsx +++ b/src/pages/AIChat.tsx @@ -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 && }
{ 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, 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 window.location.reload()} />; - return ( + const gameUi = ( + 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 && }
{'<'} Back @@ -712,6 +726,11 @@ const Breakout = () => { )} ); + + // Portal to body for true fullscreen, escaping MainLayout transforms + return isFullscreen && typeof document !== 'undefined' + ? createPortal(gameUi, document.body) + : gameUi; }; export default Breakout; diff --git a/src/pages/Pacman.tsx b/src/pages/Pacman.tsx index 44667bb..6d0ae67 100644 --- a/src/pages/Pacman.tsx +++ b/src/pages/Pacman.tsx @@ -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(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(null); const powerTimerRef = useRef(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(
{isPacmanHere &&
{renderPacman()}
} {isPacman2Here &&
{renderPacman2()}
} {ghostIndex !== -1 && !isPacmanHere && !isPacman2Here &&
{renderGhost(ghostIndex, ghosts[ghostIndex].eaten)}
} - {isDot && !isPacmanHere && !isPacman2Here && ghostIndex === -1 &&
} + {isFruitHere && !isPacmanHere && !isPacman2Here && ghostIndex === -1 && ( +
+ {fruit.type} +
+ )} + {isDot && !isPacmanHere && !isPacman2Here && ghostIndex === -1 && !isFruitHere &&
} {isPowerPellet && !isPacmanHere && !isPacman2Here && ghostIndex === -1 &&
}
); @@ -663,9 +731,11 @@ const Pacman = () => { if (showGlitchCrash) return window.location.reload()} />; - return ( + const gameUi = ( + 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 && }
{'<'} Back @@ -892,6 +962,11 @@ const Pacman = () => { )} ); + + // Portal to body for true fullscreen, escaping MainLayout transforms + return isFullscreen && typeof document !== 'undefined' + ? createPortal(gameUi, document.body) + : gameUi; }; export default Pacman; diff --git a/src/pages/Snake.tsx b/src/pages/Snake.tsx index f2f93f2..38e2036 100644 --- a/src/pages/Snake.tsx +++ b/src/pages/Snake.tsx @@ -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 = ( + {/* Render cursor inside portal for fullscreen */} + {isFullscreen && }
@@ -739,6 +752,11 @@ const Snake = () => { )} ); + + // Portal to body for true fullscreen, escaping MainLayout transforms + return isFullscreen && typeof document !== 'undefined' + ? createPortal(gameUi, document.body) + : gameUi; }; export default Snake; \ No newline at end of file diff --git a/src/pages/Tetris.tsx b/src/pages/Tetris.tsx index f3e73d3..040c3cf 100644 --- a/src/pages/Tetris.tsx +++ b/src/pages/Tetris.tsx @@ -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 = ( + 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 && }
@@ -701,6 +714,11 @@ const Tetris = () => { )} ); + + // Portal to body for true fullscreen, escaping MainLayout transforms + return isFullscreen && typeof document !== 'undefined' + ? createPortal(gameUi, document.body) + : gameUi; }; export default Tetris;