Changes
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Settings, Monitor, Volume2, Cpu, X, Contrast, Binary } from 'lucide-react';
|
import { Settings, Monitor, Volume2, Cpu, X, Contrast, Binary, Smartphone } from 'lucide-react';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings, MobileControlMode } from '@/contexts/SettingsContext';
|
||||||
import { useAchievements } from '@/contexts/AchievementsContext';
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
import CryptoConsentModal from './CryptoConsentModal';
|
import CryptoConsentModal from './CryptoConsentModal';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ interface SettingsPanelProps {
|
|||||||
const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
|
const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [showCryptoModal, setShowCryptoModal] = 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 { unlockAchievement } = useAchievements();
|
||||||
|
|
||||||
const handleToggle = (setter: (value: boolean) => void, currentValue: boolean, achievementId?: string) => {
|
const handleToggle = (setter: (value: boolean) => void, currentValue: boolean, achievementId?: string) => {
|
||||||
@@ -22,6 +22,11 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
|
|||||||
if (achievementId) unlockAchievement(achievementId);
|
if (achievementId) unlockAchievement(achievementId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMobileControlToggle = () => {
|
||||||
|
playSound('click');
|
||||||
|
setMobileControlMode(mobileControlMode === 'buttons' ? 'swipe' : 'buttons');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -149,6 +154,20 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
|
|||||||
{cryptoConsent ? 'ON' : 'OFF'}
|
{cryptoConsent ? 'ON' : 'OFF'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Controls Mode */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Smartphone size={16} className="text-primary" />
|
||||||
|
<span className="font-pixel text-sm text-foreground/90">Mobile Controls</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleMobileControlToggle}
|
||||||
|
className="px-3 py-1 text-xs font-pixel border border-primary transition-all duration-300 bg-transparent text-primary hover:bg-primary hover:text-background"
|
||||||
|
>
|
||||||
|
{mobileControlMode === 'buttons' ? 'BUTTONS' : 'SWIPE'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 pt-4 border-t border-primary/30">
|
<div className="mt-4 pt-4 border-t border-primary/30">
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useAudioAnalyzer } from './AudioAnalyzerContext';
|
|||||||
|
|
||||||
type SoundType = 'click' | 'beep' | 'hover' | 'boot' | 'success' | 'error';
|
type SoundType = 'click' | 'beep' | 'hover' | 'boot' | 'success' | 'error';
|
||||||
|
|
||||||
|
type MobileControlMode = 'swipe' | 'buttons';
|
||||||
|
|
||||||
interface SettingsContextType {
|
interface SettingsContextType {
|
||||||
crtEnabled: boolean;
|
crtEnabled: boolean;
|
||||||
setCrtEnabled: (enabled: boolean) => void;
|
setCrtEnabled: (enabled: boolean) => void;
|
||||||
@@ -26,6 +28,8 @@ interface SettingsContextType {
|
|||||||
disableAudio: () => void;
|
disableAudio: () => void;
|
||||||
userInteracted: boolean;
|
userInteracted: boolean;
|
||||||
setUserInteracted: (interacted: boolean) => void;
|
setUserInteracted: (interacted: boolean) => void;
|
||||||
|
mobileControlMode: MobileControlMode;
|
||||||
|
setMobileControlMode: (mode: MobileControlMode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
|
const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
|
||||||
@@ -66,6 +70,16 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
sessionStorage.setItem('cryptoConsentPrompted', 'true');
|
sessionStorage.setItem('cryptoConsentPrompted', 'true');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [mobileControlMode, setMobileControlModeState] = useState<MobileControlMode>(() => {
|
||||||
|
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 [audioBlocked, setAudioBlocked] = useState(false);
|
||||||
const [showAudioOverlay, setShowAudioOverlay] = useState(false);
|
const [showAudioOverlay, setShowAudioOverlay] = useState(false);
|
||||||
const [userInteracted, setUserInteracted] = useState(false);
|
const [userInteracted, setUserInteracted] = useState(false);
|
||||||
@@ -307,6 +321,8 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
disableAudio,
|
disableAudio,
|
||||||
userInteracted,
|
userInteracted,
|
||||||
setUserInteracted,
|
setUserInteracted,
|
||||||
|
mobileControlMode,
|
||||||
|
setMobileControlMode,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -314,6 +330,8 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { MobileControlMode };
|
||||||
|
|
||||||
export const useSettings = () => {
|
export const useSettings = () => {
|
||||||
const context = useContext(SettingsContext);
|
const context = useContext(SettingsContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ const AIChat = () => {
|
|||||||
sendMessage();
|
sendMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const content = (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -698,8 +698,6 @@ const AIChat = () => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return content;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AIChat;
|
export default AIChat;
|
||||||
|
|||||||
+129
-2
@@ -24,8 +24,32 @@ interface Brick {
|
|||||||
height: number;
|
height: number;
|
||||||
alive: boolean;
|
alive: boolean;
|
||||||
color: string;
|
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<PowerUpType, string> = {
|
||||||
|
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<PowerUpType, string> = {
|
||||||
|
wide: 'W',
|
||||||
|
multi: 'M',
|
||||||
|
slow: 'S',
|
||||||
|
life: '+',
|
||||||
|
};
|
||||||
|
|
||||||
const Breakout = () => {
|
const Breakout = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
@@ -40,6 +64,12 @@ const Breakout = () => {
|
|||||||
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
|
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
|
||||||
const [isTwoPlayer, setIsTwoPlayer] = useState(false);
|
const [isTwoPlayer, setIsTwoPlayer] = useState(false);
|
||||||
|
|
||||||
|
// Power-up state
|
||||||
|
const [activePowerUps, setActivePowerUps] = useState<{ type: PowerUpType; endTime: number }[]>([]);
|
||||||
|
const powerUpsRef = useRef<PowerUp[]>([]);
|
||||||
|
const extraBallsRef = useRef<{ x: number; y: number; dx: number; dy: number }[]>([]);
|
||||||
|
const basePaddleWidth = useRef(90);
|
||||||
|
|
||||||
// P2 state
|
// P2 state
|
||||||
const [score2, setScore2] = useState(0);
|
const [score2, setScore2] = useState(0);
|
||||||
const [lives2, setLives2] = useState(3);
|
const [lives2, setLives2] = useState(3);
|
||||||
@@ -91,7 +121,11 @@ const Breakout = () => {
|
|||||||
|
|
||||||
useEffect(() => { setCanvasSize(getCanvasSize()); }, [isFullscreen, getCanvasSize, isTwoPlayer]);
|
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));
|
const ballSize = isMobile ? 10 : (isTwoPlayer ? 10 : (isFullscreen ? 14 : 12));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -126,9 +160,23 @@ const Breakout = () => {
|
|||||||
const bricks: Brick[] = [];
|
const bricks: Brick[] = [];
|
||||||
const brickWidth = (width - (BRICK_COLS + 1) * BRICK_GAP) / BRICK_COLS;
|
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 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 row = 0; row < BRICK_ROWS; row++) {
|
||||||
for (let col = 0; col < BRICK_COLS; col++) {
|
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;
|
return bricks;
|
||||||
@@ -143,6 +191,9 @@ const Breakout = () => {
|
|||||||
const startGame = (twoPlayer: boolean) => {
|
const startGame = (twoPlayer: boolean) => {
|
||||||
setIsTwoPlayer(twoPlayer);
|
setIsTwoPlayer(twoPlayer);
|
||||||
setScore(0); setLevel(1); setLives(3); setGameOver(false); setIsPaused(false); setGameStarted(true);
|
setScore(0); setLevel(1); setLives(3); setGameOver(false); setIsPaused(false); setGameStarted(true);
|
||||||
|
setActivePowerUps([]);
|
||||||
|
powerUpsRef.current = [];
|
||||||
|
extraBallsRef.current = [];
|
||||||
p1DeadRef.current = false;
|
p1DeadRef.current = false;
|
||||||
p2DeadRef.current = false;
|
p2DeadRef.current = false;
|
||||||
|
|
||||||
@@ -278,6 +329,17 @@ const Breakout = () => {
|
|||||||
allDestroyed = false;
|
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) {
|
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;
|
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 => {
|
setScoreParam(prev => {
|
||||||
const ns = Math.min(prev + 10, MAX_SCORE);
|
const ns = Math.min(prev + 10, MAX_SCORE);
|
||||||
if (ns >= MAX_SCORE && playerNum === 1) setShowGlitchCrash(true);
|
if (ns >= MAX_SCORE && playerNum === 1) setShowGlitchCrash(true);
|
||||||
@@ -288,6 +350,52 @@ const Breakout = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
if (allDestroyed && bricksRefParam.current.length > 0 && !deadRef.current) nextLevel(playerNum);
|
||||||
|
|
||||||
ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0, 0, canvasSize.width, canvasSize.height);
|
ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0, 0, canvasSize.width, canvasSize.height);
|
||||||
@@ -296,6 +404,25 @@ const Breakout = () => {
|
|||||||
if (!brick.alive) continue;
|
if (!brick.alive) continue;
|
||||||
ctx.fillStyle = brick.color; ctx.fillRect(brick.x, brick.y, brick.width, brick.height);
|
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;
|
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;
|
ctx.fillStyle = primaryColor; ctx.shadowColor = primaryColor; ctx.shadowBlur = 10;
|
||||||
|
|||||||
+17
-2
@@ -8,6 +8,7 @@ import { Maximize2, Minimize2 } from 'lucide-react';
|
|||||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
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';
|
||||||
|
|
||||||
const GRID_WIDTH = 21;
|
const GRID_WIDTH = 21;
|
||||||
const GRID_HEIGHT = 21;
|
const GRID_HEIGHT = 21;
|
||||||
@@ -44,7 +45,7 @@ const MAZE_TEMPLATE = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const Pacman = () => {
|
const Pacman = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound, mobileControlMode } = useSettings();
|
||||||
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
|
|
||||||
// Game mode selection
|
// Game mode selection
|
||||||
@@ -115,9 +116,11 @@ const Pacman = () => {
|
|||||||
onTap: () => {
|
onTap: () => {
|
||||||
if (gameStarted && !gameOver && !gameComplete) setIsPaused(p => !p);
|
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(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => setCellSize(getCellSize());
|
const handleResize = () => setCellSize(getCellSize());
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
@@ -766,6 +769,17 @@ const Pacman = () => {
|
|||||||
<button onClick={() => { setGameMode('1p'); setShowModeSelection(false); }} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">START GAME</button>
|
<button onClick={() => { setGameMode('1p'); setShowModeSelection(false); }} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">START GAME</button>
|
||||||
</div>
|
</div>
|
||||||
) : gameStarted && !gameOver && !gameComplete ? (
|
) : gameStarted && !gameOver && !gameComplete ? (
|
||||||
|
useButtonControls ? (
|
||||||
|
<div className="flex flex-col items-center gap-2 mt-2">
|
||||||
|
<button onClick={() => setNextDirection('up')} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg select-none">↑</button>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button onClick={() => setNextDirection('left')} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg select-none">←</button>
|
||||||
|
<button onClick={() => setIsPaused(p => !p)} className="p-3 px-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||||
|
<button onClick={() => setNextDirection('right')} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg select-none">→</button>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setNextDirection('down')} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg select-none">↓</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className="w-full h-24 border border-primary/30 rounded-lg bg-primary/5 flex items-center justify-center touch-none mt-2"
|
className="w-full h-24 border border-primary/30 rounded-lg bg-primary/5 flex items-center justify-center touch-none mt-2"
|
||||||
{...getSwipeHandlers()}
|
{...getSwipeHandlers()}
|
||||||
@@ -775,6 +789,7 @@ const Pacman = () => {
|
|||||||
<span className="text-[10px] text-foreground/40">TAP TO PAUSE</span>
|
<span className="text-[10px] text-foreground/40">TAP TO PAUSE</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<button onClick={startGame} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
<button onClick={startGame} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||||
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}
|
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}
|
||||||
|
|||||||
+17
-2
@@ -8,6 +8,7 @@ import { Maximize2, Minimize2, Users, User } from 'lucide-react';
|
|||||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
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';
|
||||||
|
|
||||||
const GRID_SIZE = 20;
|
const GRID_SIZE = 20;
|
||||||
const TICK_SPEED = 120;
|
const TICK_SPEED = 120;
|
||||||
@@ -29,7 +30,7 @@ interface Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Snake = () => {
|
const Snake = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound, mobileControlMode } = useSettings();
|
||||||
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
|
|
||||||
// Mode selection
|
// Mode selection
|
||||||
@@ -94,9 +95,11 @@ const Snake = () => {
|
|||||||
const { getSwipeHandlers } = useSwipeControls({
|
const { getSwipeHandlers } = useSwipeControls({
|
||||||
onSwipe: (dir) => handleSwipe(dir as Direction),
|
onSwipe: (dir) => handleSwipe(dir as Direction),
|
||||||
onTap: () => setIsPaused(p => !p),
|
onTap: () => setIsPaused(p => !p),
|
||||||
enabled: isMobile && gameStarted && !gameOver && !gameComplete,
|
enabled: isMobile && gameStarted && !gameOver && !gameComplete && mobileControlMode === 'swipe',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const useButtonControls = isMobile && mobileControlMode === 'buttons';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => setCellSize(getCellSize());
|
const handleResize = () => setCellSize(getCellSize());
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
@@ -666,6 +669,17 @@ const Snake = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{gameStarted && !gameOver && !gameComplete ? (
|
{gameStarted && !gameOver && !gameComplete ? (
|
||||||
|
useButtonControls ? (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<button onClick={() => directionQueueRef.current.push('up')} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg select-none">↑</button>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button onClick={() => directionQueueRef.current.push('left')} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg select-none">←</button>
|
||||||
|
<button onClick={() => setIsPaused(p => !p)} className="p-3 px-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||||
|
<button onClick={() => directionQueueRef.current.push('right')} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg select-none">→</button>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => directionQueueRef.current.push('down')} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg select-none">↓</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className="w-full h-24 border border-primary/30 rounded-lg bg-primary/5 flex items-center justify-center touch-none"
|
className="w-full h-24 border border-primary/30 rounded-lg bg-primary/5 flex items-center justify-center touch-none"
|
||||||
{...getSwipeHandlers()}
|
{...getSwipeHandlers()}
|
||||||
@@ -675,6 +689,7 @@ const Snake = () => {
|
|||||||
<span className="text-[10px] text-foreground/40">TAP TO PAUSE</span>
|
<span className="text-[10px] text-foreground/40">TAP TO PAUSE</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||||
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}
|
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}
|
||||||
|
|||||||
+20
-2
@@ -8,6 +8,7 @@ import { Maximize2, Minimize2, Users, User } from 'lucide-react';
|
|||||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
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';
|
||||||
|
|
||||||
const BOARD_WIDTH = 10;
|
const BOARD_WIDTH = 10;
|
||||||
const BOARD_HEIGHT = 20;
|
const BOARD_HEIGHT = 20;
|
||||||
@@ -87,7 +88,7 @@ const rotate = (matrix: number[][]): number[][] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Tetris = () => {
|
const Tetris = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound, mobileControlMode } = useSettings();
|
||||||
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
|
|
||||||
const [gameMode, setGameMode] = useState<GameMode | null>(null);
|
const [gameMode, setGameMode] = useState<GameMode | null>(null);
|
||||||
@@ -329,10 +330,12 @@ const Tetris = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTap: togglePause,
|
onTap: togglePause,
|
||||||
enabled: isMobile && gameStarted && !gameOver && !gameComplete,
|
enabled: isMobile && gameStarted && !gameOver && !gameComplete && mobileControlMode === 'swipe',
|
||||||
fastSwipeThreshold: 1.2,
|
fastSwipeThreshold: 1.2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const useButtonControls = isMobile && mobileControlMode === 'buttons';
|
||||||
|
|
||||||
const movePlayer = useCallback((playerNum: 1 | 2, action: 'left' | 'right' | 'down' | 'rotate' | 'drop') => {
|
const movePlayer = useCallback((playerNum: 1 | 2, action: 'left' | 'right' | 'down' | 'rotate' | 'drop') => {
|
||||||
if (gameOver || isPaused || !gameStarted || gameMode !== '2p') return;
|
if (gameOver || isPaused || !gameStarted || gameMode !== '2p') return;
|
||||||
const setPlayer = playerNum === 1 ? setPlayer1 : setPlayer2;
|
const setPlayer = playerNum === 1 ? setPlayer1 : setPlayer2;
|
||||||
@@ -636,6 +639,20 @@ const Tetris = () => {
|
|||||||
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{lines}</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>
|
</div>
|
||||||
{gameStarted && !gameOver ? (
|
{gameStarted && !gameOver ? (
|
||||||
|
useButtonControls ? (
|
||||||
|
<div className="flex flex-col items-center gap-2 mt-1">
|
||||||
|
<GameTouchButton onAction={rotatePiece} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={150}>↻</GameTouchButton>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<GameTouchButton onAction={moveLeft} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={80}>←</GameTouchButton>
|
||||||
|
<button onClick={togglePause} className="p-3 px-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||||
|
<GameTouchButton onAction={moveRight} className="p-3 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={80}>→</GameTouchButton>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<GameTouchButton onAction={moveDown} className="p-3 px-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={50}>↓</GameTouchButton>
|
||||||
|
<button onClick={hardDrop} className="p-3 px-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">⬇</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className="w-full h-24 border border-primary/30 rounded-lg bg-primary/5 flex items-center justify-center touch-none mt-1"
|
className="w-full h-24 border border-primary/30 rounded-lg bg-primary/5 flex items-center justify-center touch-none mt-1"
|
||||||
{...getSwipeHandlers()}
|
{...getSwipeHandlers()}
|
||||||
@@ -646,6 +663,7 @@ const Tetris = () => {
|
|||||||
<span className="text-[10px] text-foreground/40">↑ ROTATE · TAP TO PAUSE</span>
|
<span className="text-[10px] text-foreground/40">↑ ROTATE · TAP TO PAUSE</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">{gameOver ? 'RETRY' : 'START'}</button>
|
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">{gameOver ? 'RETRY' : 'START'}</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user