Changes
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type MobileControlsDockProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** When false, renders nothing. */
|
||||||
|
visible?: boolean;
|
||||||
|
/** Optional override for the outer z-index. */
|
||||||
|
zIndexClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed bottom dock for mobile game controls.
|
||||||
|
* - Always visible on screen
|
||||||
|
* - Respects iOS safe-area
|
||||||
|
* - Keeps design tokens (no raw colors)
|
||||||
|
*/
|
||||||
|
export function MobileControlsDock({
|
||||||
|
children,
|
||||||
|
visible = true,
|
||||||
|
zIndexClassName = "z-[70]",
|
||||||
|
}: MobileControlsDockProps) {
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed inset-x-0 bottom-0 ${zIndexClassName} pointer-events-none pb-[env(safe-area-inset-bottom)] px-3`}
|
||||||
|
>
|
||||||
|
<div className="mx-auto w-full max-w-[520px] pointer-events-auto">
|
||||||
|
<div className="border border-primary/30 bg-background/85 backdrop-blur-md box-glow-strong rounded-lg p-3">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spacer to prevent fixed dock from covering page content in scroll containers. */
|
||||||
|
export function MobileControlsSpacer({ heightClassName = "h-40" }: { heightClassName?: string }) {
|
||||||
|
return <div className={`md:hidden ${heightClassName}`} aria-hidden="true" />;
|
||||||
|
}
|
||||||
+25
-18
@@ -7,6 +7,7 @@ import GlitchCrash from '@/components/GlitchCrash';
|
|||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
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';
|
||||||
|
|
||||||
const PADDLE_HEIGHT = 14;
|
const PADDLE_HEIGHT = 14;
|
||||||
const BRICK_ROWS = 6;
|
const BRICK_ROWS = 6;
|
||||||
@@ -53,7 +54,8 @@ const Breakout = () => {
|
|||||||
const isMobile = window.innerWidth < 768;
|
const isMobile = window.innerWidth < 768;
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
const maxWidth = window.innerWidth - 32;
|
const maxWidth = window.innerWidth - 32;
|
||||||
const maxHeight = window.innerHeight - 280;
|
// Reserve space for header + fixed controls dock on mobile
|
||||||
|
const maxHeight = window.innerHeight - 380;
|
||||||
const aspectRatio = 480 / 580;
|
const aspectRatio = 480 / 580;
|
||||||
let width = maxWidth;
|
let width = maxWidth;
|
||||||
let height = width / aspectRatio;
|
let height = width / aspectRatio;
|
||||||
@@ -279,24 +281,29 @@ const Breakout = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="mt-4 flex flex-col items-center gap-2 w-full">
|
<>
|
||||||
<div className="flex gap-4 text-center">
|
<MobileControlsSpacer heightClassName="h-36" />
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
<MobileControlsDock>
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
<div className="flex flex-col items-center gap-2">
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div>
|
<div className="flex gap-4 text-center">
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">♥</p><p className="font-minecraft text-sm text-primary">{lives}</p></div>
|
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
||||||
</div>
|
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
||||||
{gameStarted && !gameOver && (
|
<div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div>
|
||||||
<div className="flex gap-4 mt-2">
|
<div><p className="font-pixel text-[8px] text-foreground/60">♥</p><p className="font-minecraft text-sm text-primary">{lives}</p></div>
|
||||||
<GameTouchButton onAction={moveLeft} className="p-4 px-8 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}>←</GameTouchButton>
|
</div>
|
||||||
<button onClick={() => setIsPaused(p => !p)} className="p-4 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
|
||||||
<GameTouchButton onAction={moveRight} className="p-4 px-8 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}>→</GameTouchButton>
|
{gameStarted && !gameOver ? (
|
||||||
|
<div className="flex gap-3 mt-2">
|
||||||
|
<GameTouchButton onAction={moveLeft} className="p-4 px-8 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}>←</GameTouchButton>
|
||||||
|
<button onClick={() => setIsPaused(p => !p)} className="p-4 px-6 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||||
|
<GameTouchButton onAction={moveRight} className="p-4 px-8 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}>→</GameTouchButton>
|
||||||
|
</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">{gameOver ? 'RETRY' : 'START'}</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</MobileControlsDock>
|
||||||
{(!gameStarted || gameOver) && (
|
</>
|
||||||
<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">{gameOver ? 'RETRY' : 'START'}</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+32
-23
@@ -7,6 +7,7 @@ import GlitchCrash from '@/components/GlitchCrash';
|
|||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
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';
|
||||||
|
|
||||||
const GRID_WIDTH = 21;
|
const GRID_WIDTH = 21;
|
||||||
const GRID_HEIGHT = 21;
|
const GRID_HEIGHT = 21;
|
||||||
@@ -76,7 +77,8 @@ const Pacman = () => {
|
|||||||
const isMobile = window.innerWidth < 768;
|
const isMobile = window.innerWidth < 768;
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
const maxWidth = window.innerWidth - 40;
|
const maxWidth = window.innerWidth - 40;
|
||||||
const maxHeight = window.innerHeight - 300;
|
// Reserve space for header + fixed controls dock on mobile
|
||||||
|
const maxHeight = window.innerHeight - 420;
|
||||||
return Math.min(Math.floor(maxWidth / GRID_WIDTH), Math.floor(maxHeight / GRID_HEIGHT), 16);
|
return Math.min(Math.floor(maxWidth / GRID_WIDTH), Math.floor(maxHeight / GRID_HEIGHT), 16);
|
||||||
}
|
}
|
||||||
return isFullscreen ? 26 : 20;
|
return isFullscreen ? 26 : 20;
|
||||||
@@ -352,29 +354,36 @@ const Pacman = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="mt-4 flex flex-col items-center gap-2 w-full">
|
<>
|
||||||
<div className="flex gap-4 text-center">
|
<MobileControlsSpacer />
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
<MobileControlsDock>
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
<div className="flex flex-col items-center gap-2">
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div>
|
<div className="flex gap-4 text-center">
|
||||||
</div>
|
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
||||||
{gameStarted && !gameOver && !gameComplete && (
|
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
||||||
<div className="grid grid-cols-3 gap-1 mt-2">
|
<div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div>
|
||||||
<div />
|
</div>
|
||||||
<GameTouchButton onAction={() => setNextDirection('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑</GameTouchButton>
|
|
||||||
<div />
|
{gameStarted && !gameOver && !gameComplete ? (
|
||||||
<GameTouchButton onAction={() => setNextDirection('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
<div className="grid grid-cols-3 gap-1 mt-2">
|
||||||
<button onClick={() => setIsPaused(p => !p)} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
<div />
|
||||||
<GameTouchButton onAction={() => setNextDirection('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
<GameTouchButton onAction={() => setNextDirection('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑</GameTouchButton>
|
||||||
<div />
|
<div />
|
||||||
<GameTouchButton onAction={() => setNextDirection('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
<GameTouchButton onAction={() => setNextDirection('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
||||||
<div />
|
<button onClick={() => setIsPaused(p => !p)} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||||
|
<GameTouchButton onAction={() => setNextDirection('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
||||||
|
<div />
|
||||||
|
<GameTouchButton onAction={() => setNextDirection('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
||||||
|
<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">
|
||||||
|
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</MobileControlsDock>
|
||||||
{(!gameStarted || gameOver || gameComplete) && (
|
</>
|
||||||
<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'}</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+32
-25
@@ -7,6 +7,7 @@ import GlitchCrash from '@/components/GlitchCrash';
|
|||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
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';
|
||||||
|
|
||||||
const GRID_SIZE = 20;
|
const GRID_SIZE = 20;
|
||||||
const TICK_SPEED = 120;
|
const TICK_SPEED = 120;
|
||||||
@@ -42,7 +43,8 @@ const Snake = () => {
|
|||||||
const isMobile = window.innerWidth < 768;
|
const isMobile = window.innerWidth < 768;
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
const maxWidth = window.innerWidth - 40;
|
const maxWidth = window.innerWidth - 40;
|
||||||
const maxHeight = window.innerHeight - 300;
|
// Reserve space for header + fixed controls dock on mobile
|
||||||
|
const maxHeight = window.innerHeight - 420;
|
||||||
return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 18);
|
return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 18);
|
||||||
}
|
}
|
||||||
return isFullscreen ? 28 : 24;
|
return isFullscreen ? 28 : 24;
|
||||||
@@ -313,31 +315,36 @@ const Snake = () => {
|
|||||||
|
|
||||||
{/* Mobile Controls */}
|
{/* Mobile Controls */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="mt-4 flex flex-col items-center gap-2 w-full">
|
<>
|
||||||
<div className="flex gap-4 text-center">
|
<MobileControlsSpacer />
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
<MobileControlsDock>
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
<div className="flex flex-col items-center gap-2">
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">LEN</p><p className="font-minecraft text-sm text-primary">{snake.length}</p></div>
|
<div className="flex gap-4 text-center">
|
||||||
</div>
|
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
||||||
{gameStarted && !gameOver && !gameComplete && (
|
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
||||||
<div className="grid grid-cols-3 gap-1 mt-2">
|
<div><p className="font-pixel text-[8px] text-foreground/60">LEN</p><p className="font-minecraft text-sm text-primary">{snake.length}</p></div>
|
||||||
<div />
|
</div>
|
||||||
<GameTouchButton onAction={() => directionQueueRef.current.push('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑</GameTouchButton>
|
|
||||||
<div />
|
{gameStarted && !gameOver && !gameComplete ? (
|
||||||
<GameTouchButton onAction={() => directionQueueRef.current.push('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
<div className="grid grid-cols-3 gap-1 mt-2">
|
||||||
<button onClick={() => setIsPaused(p => !p)} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
<div />
|
||||||
<GameTouchButton onAction={() => directionQueueRef.current.push('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
<GameTouchButton onAction={() => directionQueueRef.current.push('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑</GameTouchButton>
|
||||||
<div />
|
<div />
|
||||||
<GameTouchButton onAction={() => directionQueueRef.current.push('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
<GameTouchButton onAction={() => directionQueueRef.current.push('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
||||||
<div />
|
<button onClick={() => setIsPaused(p => !p)} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||||
|
<GameTouchButton onAction={() => directionQueueRef.current.push('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
||||||
|
<div />
|
||||||
|
<GameTouchButton onAction={() => directionQueueRef.current.push('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
||||||
|
<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">
|
||||||
|
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</MobileControlsDock>
|
||||||
{(!gameStarted || gameOver || gameComplete) && (
|
</>
|
||||||
<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'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useSettings } from '@/contexts/SettingsContext';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||||
|
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||||
|
|
||||||
const GRID_SIZE = 20;
|
const GRID_SIZE = 20;
|
||||||
const TICK_SPEED = 120;
|
const TICK_SPEED = 120;
|
||||||
@@ -44,7 +45,8 @@ const SnakeMultiplayer = () => {
|
|||||||
const isMobile = window.innerWidth < 768;
|
const isMobile = window.innerWidth < 768;
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
const maxWidth = window.innerWidth - 40;
|
const maxWidth = window.innerWidth - 40;
|
||||||
const maxHeight = window.innerHeight - 300;
|
// Reserve space for header + fixed controls dock on mobile
|
||||||
|
const maxHeight = window.innerHeight - 420;
|
||||||
return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 16);
|
return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 16);
|
||||||
}
|
}
|
||||||
return isFullscreen ? 24 : 20;
|
return isFullscreen ? 24 : 20;
|
||||||
@@ -292,44 +294,75 @@ const SnakeMultiplayer = () => {
|
|||||||
<div className="grid" style={{ gridTemplateColumns: `repeat(${GRID_SIZE}, ${cellSize}px)` }}>{renderGrid()}</div>
|
<div className="grid" style={{ gridTemplateColumns: `repeat(${GRID_SIZE}, ${cellSize}px)` }}>{renderGrid()}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 min-w-[160px]">
|
{!isMobile && (
|
||||||
{/* Player 1 Stats */}
|
<div className="flex flex-col gap-2 min-w-[160px]">
|
||||||
<div className="border border-primary/50 p-3 bg-background/50">
|
{/* Player 1 Stats */}
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="border border-primary/50 p-3 bg-background/50">
|
||||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: players[0].color }} />
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<p className="font-pixel text-[10px] text-foreground/60">PLAYER 1 (WASD)</p>
|
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: players[0].color }} />
|
||||||
|
<p className="font-pixel text-[10px] text-foreground/60">PLAYER 1 (WASD)</p>
|
||||||
|
</div>
|
||||||
|
<p className="font-minecraft text-lg text-primary text-glow">{players[0].score}</p>
|
||||||
|
{!players[0].alive && <p className="font-pixel text-[8px] text-destructive">DEAD</p>}
|
||||||
</div>
|
</div>
|
||||||
<p className="font-minecraft text-lg text-primary text-glow">{players[0].score}</p>
|
|
||||||
{!players[0].alive && <p className="font-pixel text-[8px] text-destructive">DEAD</p>}
|
{/* Player 2 Stats */}
|
||||||
</div>
|
<div className="border border-purple-500/50 p-3 bg-background/50">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
{/* Player 2 Stats */}
|
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: players[1].color }} />
|
||||||
<div className="border border-purple-500/50 p-3 bg-background/50">
|
<p className="font-pixel text-[10px] text-foreground/60">PLAYER 2 (↑↓←→)</p>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
</div>
|
||||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: players[1].color }} />
|
<p className="font-minecraft text-lg text-purple-400">{players[1].score}</p>
|
||||||
<p className="font-pixel text-[10px] text-foreground/60">PLAYER 2 (↑↓←→)</p>
|
{!players[1].alive && <p className="font-pixel text-[8px] text-destructive">DEAD</p>}
|
||||||
</div>
|
</div>
|
||||||
<p className="font-minecraft text-lg text-purple-400">{players[1].score}</p>
|
|
||||||
{!players[1].alive && <p className="font-pixel text-[8px] text-destructive">DEAD</p>}
|
<div className="border border-primary/50 p-3 bg-background/50">
|
||||||
|
<p className="font-pixel text-[10px] text-foreground/60 mb-1">CONTROLS</p>
|
||||||
|
<p className="font-pixel text-[9px] text-primary">P1: W A S D</p>
|
||||||
|
<p className="font-pixel text-[9px] text-purple-400">P2: ↑ ↓ ← →</p>
|
||||||
|
<p className="font-pixel text-[9px] text-foreground/80 mt-1">P: Pause</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!gameStarted || gameOver ? (
|
||||||
|
<button onClick={startGame} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||||
|
{gameOver ? 'PLAY AGAIN' : 'START'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button onClick={() => setIsPaused(p => !p)} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all">
|
||||||
|
{isPaused ? 'RESUME' : 'PAUSE'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="border border-primary/50 p-3 bg-background/50">
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/60 mb-1">CONTROLS</p>
|
{isMobile && (
|
||||||
<p className="font-pixel text-[9px] text-primary">P1: W A S D</p>
|
<>
|
||||||
<p className="font-pixel text-[9px] text-purple-400">P2: ↑ ↓ ← →</p>
|
<MobileControlsSpacer />
|
||||||
<p className="font-pixel text-[9px] text-foreground/80 mt-1">P: Pause</p>
|
<MobileControlsDock>
|
||||||
</div>
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<div className="flex gap-4 text-center">
|
||||||
{!gameStarted || gameOver ? (
|
<div><p className="font-pixel text-[8px] text-foreground/60">P1</p><p className="font-minecraft text-sm text-primary">{players[0].score}</p></div>
|
||||||
<button onClick={startGame} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
<div><p className="font-pixel text-[8px] text-foreground/60">P2</p><p className="font-minecraft text-sm text-purple-400">{players[1].score}</p></div>
|
||||||
{gameOver ? 'PLAY AGAIN' : 'START'}
|
</div>
|
||||||
</button>
|
|
||||||
) : (
|
{!gameStarted || gameOver ? (
|
||||||
<button onClick={() => setIsPaused(p => !p)} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all">
|
<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">
|
||||||
{isPaused ? 'RESUME' : 'PAUSE'}
|
{gameOver ? 'PLAY AGAIN' : 'START'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
) : (
|
||||||
</div>
|
<button onClick={() => setIsPaused(p => !p)} 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">
|
||||||
|
{isPaused ? 'RESUME' : 'PAUSE'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3 font-pixel text-[10px] text-foreground/70">
|
||||||
|
<span className="text-primary">P1: WASD</span>
|
||||||
|
<span className="text-purple-400">P2: ↑↓←→</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MobileControlsDock>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Game Over / Pause Overlay */}
|
{/* Game Over / Pause Overlay */}
|
||||||
|
|||||||
+30
-23
@@ -7,6 +7,7 @@ import GlitchCrash from '@/components/GlitchCrash';
|
|||||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
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';
|
||||||
|
|
||||||
const BOARD_WIDTH = 10;
|
const BOARD_WIDTH = 10;
|
||||||
const BOARD_HEIGHT = 20;
|
const BOARD_HEIGHT = 20;
|
||||||
@@ -79,7 +80,8 @@ const Tetris = () => {
|
|||||||
const isMobile = window.innerWidth < 768;
|
const isMobile = window.innerWidth < 768;
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
const maxWidth = window.innerWidth - 40;
|
const maxWidth = window.innerWidth - 40;
|
||||||
const maxHeight = window.innerHeight - 320;
|
// Reserve space for header + fixed controls dock on mobile
|
||||||
|
const maxHeight = window.innerHeight - 440;
|
||||||
return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 22);
|
return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 22);
|
||||||
}
|
}
|
||||||
return isFullscreen ? 30 : 24;
|
return isFullscreen ? 30 : 24;
|
||||||
@@ -326,29 +328,34 @@ const Tetris = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="mt-4 flex flex-col items-center gap-2 w-full">
|
<>
|
||||||
<div className="flex gap-4 text-center">
|
<MobileControlsSpacer />
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
<MobileControlsDock>
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
<div className="flex flex-col items-center gap-2">
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{lines}</p></div>
|
<div className="flex gap-4 text-center">
|
||||||
</div>
|
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
||||||
{gameStarted && !gameOver && !gameComplete && (
|
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
||||||
<div className="grid grid-cols-3 gap-1 mt-2">
|
<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>
|
||||||
<GameTouchButton onAction={rotatePiece} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={200}>↑</GameTouchButton>
|
|
||||||
<div />
|
{gameStarted && !gameOver && !gameComplete ? (
|
||||||
<GameTouchButton onAction={moveLeft} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
<div className="grid grid-cols-3 gap-1 mt-2">
|
||||||
<GameTouchButton onAction={hardDrop} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px]" interval={500}>DROP</GameTouchButton>
|
<div />
|
||||||
<GameTouchButton onAction={moveRight} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
<GameTouchButton onAction={rotatePiece} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={200}>↑</GameTouchButton>
|
||||||
<button onClick={togglePause} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
<div />
|
||||||
<GameTouchButton onAction={moveDown} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
<GameTouchButton onAction={moveLeft} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
||||||
<div />
|
<GameTouchButton onAction={hardDrop} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px]" interval={500}>DROP</GameTouchButton>
|
||||||
|
<GameTouchButton onAction={moveRight} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
||||||
|
<button onClick={togglePause} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||||
|
<GameTouchButton onAction={moveDown} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
||||||
|
<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">{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</MobileControlsDock>
|
||||||
{(!gameStarted || gameOver || gameComplete) && (
|
</>
|
||||||
<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'}</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && (
|
{!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && (
|
||||||
|
|||||||
Reference in New Issue
Block a user