From a9548741ce408cd53c8c26628f0a7067ffd136c7 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Thu, 22 Jan 2026 23:38:57 +0000
Subject: [PATCH] Changes
---
src/components/SettingsPanel.tsx | 25 +++++-
src/contexts/SettingsContext.tsx | 18 +++++
src/pages/AIChat.tsx | 4 +-
src/pages/Breakout.tsx | 131 ++++++++++++++++++++++++++++++-
src/pages/Pacman.tsx | 37 ++++++---
src/pages/Snake.tsx | 37 ++++++---
src/pages/Tetris.tsx | 42 +++++++---
7 files changed, 252 insertions(+), 42 deletions(-)
diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx
index 03a82c7..569d6d4 100644
--- a/src/components/SettingsPanel.tsx
+++ b/src/components/SettingsPanel.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
-import { Settings, Monitor, Volume2, Cpu, X, Contrast, Binary } from 'lucide-react';
-import { useSettings } from '@/contexts/SettingsContext';
+import { Settings, Monitor, Volume2, Cpu, X, Contrast, Binary, Smartphone } from 'lucide-react';
+import { useSettings, MobileControlMode } from '@/contexts/SettingsContext';
import { useAchievements } from '@/contexts/AchievementsContext';
import CryptoConsentModal from './CryptoConsentModal';
@@ -13,7 +13,7 @@ interface SettingsPanelProps {
const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
const [isOpen, setIsOpen] = useState(false);
const [showCryptoModal, setShowCryptoModal] = useState(false);
- const { crtEnabled, setCrtEnabled, matrixEnabled, setMatrixEnabled, soundEnabled, setSoundEnabled, cryptoConsent, playSound } = useSettings();
+ const { crtEnabled, setCrtEnabled, matrixEnabled, setMatrixEnabled, soundEnabled, setSoundEnabled, cryptoConsent, playSound, mobileControlMode, setMobileControlMode } = useSettings();
const { unlockAchievement } = useAchievements();
const handleToggle = (setter: (value: boolean) => void, currentValue: boolean, achievementId?: string) => {
@@ -22,6 +22,11 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
if (achievementId) unlockAchievement(achievementId);
};
+ const handleMobileControlToggle = () => {
+ playSound('click');
+ setMobileControlMode(mobileControlMode === 'buttons' ? 'swipe' : 'buttons');
+ };
+
return (
<>
+
+ {/* Mobile Controls Mode */}
+
+
+
+ Mobile Controls
+
+
+
diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx
index 2f71346..f604b94 100644
--- a/src/contexts/SettingsContext.tsx
+++ b/src/contexts/SettingsContext.tsx
@@ -3,6 +3,8 @@ import { useAudioAnalyzer } from './AudioAnalyzerContext';
type SoundType = 'click' | 'beep' | 'hover' | 'boot' | 'success' | 'error';
+type MobileControlMode = 'swipe' | 'buttons';
+
interface SettingsContextType {
crtEnabled: boolean;
setCrtEnabled: (enabled: boolean) => void;
@@ -26,6 +28,8 @@ interface SettingsContextType {
disableAudio: () => void;
userInteracted: boolean;
setUserInteracted: (interacted: boolean) => void;
+ mobileControlMode: MobileControlMode;
+ setMobileControlMode: (mode: MobileControlMode) => void;
}
const SettingsContext = createContext
(undefined);
@@ -66,6 +70,16 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
sessionStorage.setItem('cryptoConsentPrompted', 'true');
};
+ const [mobileControlMode, setMobileControlModeState] = useState(() => {
+ const saved = localStorage.getItem('mobileControlMode');
+ return (saved as MobileControlMode) || 'buttons';
+ });
+
+ const setMobileControlMode = (mode: MobileControlMode) => {
+ setMobileControlModeState(mode);
+ localStorage.setItem('mobileControlMode', mode);
+ };
+
const [audioBlocked, setAudioBlocked] = useState(false);
const [showAudioOverlay, setShowAudioOverlay] = useState(false);
const [userInteracted, setUserInteracted] = useState(false);
@@ -307,6 +321,8 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
disableAudio,
userInteracted,
setUserInteracted,
+ mobileControlMode,
+ setMobileControlMode,
}}
>
{children}
@@ -314,6 +330,8 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
);
};
+export type { MobileControlMode };
+
export const useSettings = () => {
const context = useContext(SettingsContext);
if (context === undefined) {
diff --git a/src/pages/AIChat.tsx b/src/pages/AIChat.tsx
index 0822ebb..04d19d5 100644
--- a/src/pages/AIChat.tsx
+++ b/src/pages/AIChat.tsx
@@ -382,7 +382,7 @@ const AIChat = () => {
sendMessage();
}
};
- const content = (
+ return (
{
);
-
- return content;
};
export default AIChat;
diff --git a/src/pages/Breakout.tsx b/src/pages/Breakout.tsx
index 0027b74..78c3d15 100644
--- a/src/pages/Breakout.tsx
+++ b/src/pages/Breakout.tsx
@@ -24,8 +24,32 @@ interface Brick {
height: number;
alive: boolean;
color: string;
+ hasPowerUp?: PowerUpType;
}
+type PowerUpType = 'wide' | 'multi' | 'slow' | 'life';
+
+interface PowerUp {
+ x: number;
+ y: number;
+ type: PowerUpType;
+ active: boolean;
+}
+
+const POWER_UP_COLORS: Record = {
+ wide: 'hsl(200 80% 60%)',
+ multi: 'hsl(280 80% 60%)',
+ slow: 'hsl(45 80% 60%)',
+ life: 'hsl(0 80% 60%)',
+};
+
+const POWER_UP_LABELS: Record = {
+ wide: 'W',
+ multi: 'M',
+ slow: 'S',
+ life: '+',
+};
+
const Breakout = () => {
const { playSound } = useSettings();
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
@@ -40,6 +64,12 @@ const Breakout = () => {
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
const [isTwoPlayer, setIsTwoPlayer] = useState(false);
+ // Power-up state
+ const [activePowerUps, setActivePowerUps] = useState<{ type: PowerUpType; endTime: number }[]>([]);
+ const powerUpsRef = useRef([]);
+ const extraBallsRef = useRef<{ x: number; y: number; dx: number; dy: number }[]>([]);
+ const basePaddleWidth = useRef(90);
+
// P2 state
const [score2, setScore2] = useState(0);
const [lives2, setLives2] = useState(3);
@@ -91,7 +121,11 @@ const Breakout = () => {
useEffect(() => { setCanvasSize(getCanvasSize()); }, [isFullscreen, getCanvasSize, isTwoPlayer]);
- const paddleWidth = isMobile ? 70 : (isTwoPlayer ? 70 : (isFullscreen ? 100 : 90));
+ // Calculate paddle width based on active power-ups
+ const hasWidePaddle = activePowerUps.some(p => p.type === 'wide' && p.endTime > Date.now());
+ const hasSlowBall = activePowerUps.some(p => p.type === 'slow' && p.endTime > Date.now());
+ basePaddleWidth.current = isMobile ? 70 : (isTwoPlayer ? 70 : (isFullscreen ? 100 : 90));
+ const paddleWidth = hasWidePaddle ? basePaddleWidth.current * 1.5 : basePaddleWidth.current;
const ballSize = isMobile ? 10 : (isTwoPlayer ? 10 : (isFullscreen ? 14 : 12));
useEffect(() => {
@@ -126,9 +160,23 @@ const Breakout = () => {
const bricks: Brick[] = [];
const brickWidth = (width - (BRICK_COLS + 1) * BRICK_GAP) / BRICK_COLS;
const colors = ['hsl(0 70% 50%)', 'hsl(30 70% 50%)', 'hsl(60 70% 50%)', 'hsl(120 70% 50%)', 'hsl(200 70% 50%)', 'hsl(280 70% 50%)'];
+ const powerUpTypes: PowerUpType[] = ['wide', 'multi', 'slow', 'life'];
+
for (let row = 0; row < BRICK_ROWS; row++) {
for (let col = 0; col < BRICK_COLS; col++) {
- bricks.push({ x: BRICK_GAP + col * (brickWidth + BRICK_GAP), y: 60 + row * (BRICK_HEIGHT + BRICK_GAP), width: brickWidth, height: BRICK_HEIGHT, alive: true, color: colors[row % colors.length] });
+ // ~15% chance of having a power-up
+ const hasPowerUp = Math.random() < 0.15;
+ const powerUpType = hasPowerUp ? powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)] : undefined;
+
+ bricks.push({
+ x: BRICK_GAP + col * (brickWidth + BRICK_GAP),
+ y: 60 + row * (BRICK_HEIGHT + BRICK_GAP),
+ width: brickWidth,
+ height: BRICK_HEIGHT,
+ alive: true,
+ color: colors[row % colors.length],
+ hasPowerUp: powerUpType
+ });
}
}
return bricks;
@@ -143,6 +191,9 @@ const Breakout = () => {
const startGame = (twoPlayer: boolean) => {
setIsTwoPlayer(twoPlayer);
setScore(0); setLevel(1); setLives(3); setGameOver(false); setIsPaused(false); setGameStarted(true);
+ setActivePowerUps([]);
+ powerUpsRef.current = [];
+ extraBallsRef.current = [];
p1DeadRef.current = false;
p2DeadRef.current = false;
@@ -278,6 +329,17 @@ const Breakout = () => {
allDestroyed = false;
if (ball.x + ballSize / 2 >= brick.x && ball.x - ballSize / 2 <= brick.x + brick.width && ball.y + ballSize / 2 >= brick.y && ball.y - ballSize / 2 <= brick.y + brick.height) {
brick.alive = false; ball.dy = -ball.dy;
+
+ // Spawn power-up if brick had one (P1 only for simplicity)
+ if (brick.hasPowerUp && playerNum === 1) {
+ powerUpsRef.current.push({
+ x: brick.x + brick.width / 2,
+ y: brick.y + brick.height,
+ type: brick.hasPowerUp,
+ active: true
+ });
+ }
+
setScoreParam(prev => {
const ns = Math.min(prev + 10, MAX_SCORE);
if (ns >= MAX_SCORE && playerNum === 1) setShowGlitchCrash(true);
@@ -287,6 +349,52 @@ const Breakout = () => {
playSound('success'); break;
}
}
+
+ // Update power-ups (P1 only)
+ if (playerNum === 1) {
+ const paddleY = canvasSize.height - 30;
+ for (const pu of powerUpsRef.current) {
+ if (!pu.active) continue;
+ pu.y += 2; // Fall speed
+
+ // Check paddle collision
+ if (pu.y >= paddleY && pu.y <= paddleY + PADDLE_HEIGHT && pu.x >= paddleRefParam.current && pu.x <= paddleRefParam.current + paddleWidth) {
+ pu.active = false;
+ playSound('success');
+
+ if (pu.type === 'life') {
+ setLivesParam(prev => Math.min(prev + 1, 5));
+ } else if (pu.type === 'multi') {
+ // Add extra ball
+ const newBall = { x: ball.x, y: ball.y, dx: -ball.dx, dy: ball.dy };
+ extraBallsRef.current.push(newBall);
+ } else {
+ // Wide or Slow - timed power-up (8 seconds)
+ setActivePowerUps(prev => [...prev.filter(p => p.type !== pu.type), { type: pu.type, endTime: Date.now() + 8000 }]);
+ }
+ }
+
+ // Remove if off screen
+ if (pu.y > canvasSize.height) pu.active = false;
+ }
+ powerUpsRef.current = powerUpsRef.current.filter(pu => pu.active);
+
+ // Update extra balls
+ for (const eb of extraBallsRef.current) {
+ const speedMod = hasSlowBall ? 0.6 : 1;
+ eb.x += eb.dx * speedMod; eb.y += eb.dy * speedMod;
+ if (eb.x <= ballSize / 2 || eb.x >= canvasSize.width - ballSize / 2) eb.dx = -eb.dx;
+ if (eb.y <= ballSize / 2) eb.dy = -eb.dy;
+ if (eb.y + ballSize / 2 >= paddleY && eb.y - ballSize / 2 <= paddleY + PADDLE_HEIGHT && eb.x >= paddleRefParam.current && eb.x <= paddleRefParam.current + paddleWidth) {
+ const hitPos = (eb.x - paddleRefParam.current) / paddleWidth;
+ const angle = (hitPos - 0.5) * Math.PI * 0.7;
+ const speed = Math.sqrt(eb.dx * eb.dx + eb.dy * eb.dy);
+ eb.dx = Math.sin(angle) * speed; eb.dy = -Math.abs(Math.cos(angle) * speed);
+ eb.y = paddleY - ballSize / 2;
+ }
+ }
+ extraBallsRef.current = extraBallsRef.current.filter(eb => eb.y < canvasSize.height);
+ }
if (allDestroyed && bricksRefParam.current.length > 0 && !deadRef.current) nextLevel(playerNum);
@@ -296,6 +404,25 @@ const Breakout = () => {
if (!brick.alive) continue;
ctx.fillStyle = brick.color; ctx.fillRect(brick.x, brick.y, brick.width, brick.height);
ctx.strokeStyle = primaryColor; ctx.globalAlpha = 0.5; ctx.strokeRect(brick.x, brick.y, brick.width, brick.height); ctx.globalAlpha = 1;
+ // Draw power-up indicator
+ if (brick.hasPowerUp) {
+ ctx.fillStyle = POWER_UP_COLORS[brick.hasPowerUp];
+ ctx.font = '10px monospace';
+ ctx.textAlign = 'center';
+ ctx.fillText(POWER_UP_LABELS[brick.hasPowerUp], brick.x + brick.width / 2, brick.y + brick.height / 2 + 3);
+ }
+ }
+
+ // Draw falling power-ups
+ if (playerNum === 1) {
+ for (const pu of powerUpsRef.current) {
+ if (!pu.active) continue;
+ ctx.fillStyle = POWER_UP_COLORS[pu.type];
+ ctx.shadowColor = POWER_UP_COLORS[pu.type]; ctx.shadowBlur = 8;
+ ctx.beginPath(); ctx.arc(pu.x, pu.y, 8, 0, Math.PI * 2); ctx.fill();
+ ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center';
+ ctx.fillText(POWER_UP_LABELS[pu.type], pu.x, pu.y + 4); ctx.shadowBlur = 0;
+ }
}
ctx.fillStyle = primaryColor; ctx.shadowColor = primaryColor; ctx.shadowBlur = 10;
diff --git a/src/pages/Pacman.tsx b/src/pages/Pacman.tsx
index b200b0e..cc9ddda 100644
--- a/src/pages/Pacman.tsx
+++ b/src/pages/Pacman.tsx
@@ -8,6 +8,7 @@ import { Maximize2, Minimize2 } from 'lucide-react';
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
import { useSwipeControls } from '@/hooks/useSwipeControls';
+import GameTouchButton from '@/components/GameTouchButton';
const GRID_WIDTH = 21;
const GRID_HEIGHT = 21;
@@ -44,7 +45,7 @@ const MAZE_TEMPLATE = [
];
const Pacman = () => {
- const { playSound } = useSettings();
+ const { playSound, mobileControlMode } = useSettings();
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
// Game mode selection
@@ -115,9 +116,11 @@ const Pacman = () => {
onTap: () => {
if (gameStarted && !gameOver && !gameComplete) setIsPaused(p => !p);
},
- enabled: isMobile && gameStarted && !gameOver && !gameComplete,
+ enabled: isMobile && gameStarted && !gameOver && !gameComplete && mobileControlMode === 'swipe',
});
+ const useButtonControls = isMobile && mobileControlMode === 'buttons';
+
useEffect(() => {
const handleResize = () => setCellSize(getCellSize());
window.addEventListener('resize', handleResize);
@@ -766,15 +769,27 @@ const Pacman = () => {
) : gameStarted && !gameOver && !gameComplete ? (
-
-
- SWIPE TO MOVE
- TAP TO PAUSE
-
-
+ useButtonControls ? (
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+ SWIPE TO MOVE
+ TAP TO PAUSE
+
+
+ )
) : (