Fix fullscreen to cover screen
- Update MainLayout animation from scale to consistent fade - Adjust AI Chat fullscreen to use in-app portal and ensure full coverage - Align Tetris 2P layout and fullscreen behavior with 1P - Minor UI tweaks to remove extra margins on fullscreen states X-Lovable-Edit-ID: edt-5158d755-452c-4cfe-a3e7-c5332b2c2dc9
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
interface FullscreenPortalProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portals children to document.body to escape any transformed ancestors.
|
||||||
|
* This is important because CSS transforms on parents can break `position: fixed`.
|
||||||
|
*/
|
||||||
|
export const FullscreenPortal = ({ children }: FullscreenPortalProps) => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted || typeof document === 'undefined') return null;
|
||||||
|
return createPortal(children, document.body);
|
||||||
|
};
|
||||||
@@ -14,8 +14,8 @@ const MainLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className={`relative z-10 flex flex-col items-center ${isMobileGamePage ? 'pt-4' : 'pt-8 md:pt-12'} px-4 w-full ${isMobileGamePage ? 'pb-4' : 'pb-20'}`}
|
className={`relative z-10 flex flex-col items-center ${isMobileGamePage ? 'pt-4' : 'pt-8 md:pt-12'} px-4 w-full ${isMobileGamePage ? 'pb-4' : 'pb-20'}`}
|
||||||
>
|
>
|
||||||
|
|||||||
+53
-20
@@ -9,6 +9,7 @@ import { useAchievements, incrementAiMessageCount } from '@/contexts/Achievement
|
|||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import GlitchText from '@/components/GlitchText';
|
import GlitchText from '@/components/GlitchText';
|
||||||
import MessageContent from '@/components/MessageContent';
|
import MessageContent from '@/components/MessageContent';
|
||||||
|
import { FullscreenPortal } from '@/components/FullscreenPortal';
|
||||||
import { AI_PROVIDERS, AIProvider, getProvider, CUSTOM_API_STORAGE_KEY } from '@/lib/aiProviders';
|
import { AI_PROVIDERS, AIProvider, getProvider, CUSTOM_API_STORAGE_KEY } from '@/lib/aiProviders';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -372,9 +373,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 }}
|
||||||
@@ -408,14 +407,31 @@ const AIChat = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 px-2 py-1 border border-primary/30 rounded text-xs font-pixel">
|
<div className="flex items-center gap-2 px-2 py-1 border border-primary/30 rounded text-xs font-pixel">
|
||||||
<span className="text-muted-foreground">Memory:</span>
|
<span className="text-muted-foreground">Memory:</span>
|
||||||
<span className={`${
|
<span
|
||||||
(() => {
|
className={`${
|
||||||
const totalChars = messages.filter(m => m.role !== 'system').reduce((acc, m) => acc + m.content.length, 0);
|
(() => {
|
||||||
const percent = (totalChars / 5000) * 100;
|
const totalChars = messages
|
||||||
return percent > 80 ? 'text-red-400' : percent > 50 ? 'text-yellow-400' : 'text-green-400';
|
.filter(m => m.role !== 'system')
|
||||||
})()
|
.reduce((acc, m) => acc + m.content.length, 0);
|
||||||
}`}>
|
const percent = (totalChars / 5000) * 100;
|
||||||
{Math.min(100, Math.round((messages.filter(m => m.role !== 'system').reduce((acc, m) => acc + m.content.length, 0) / 5000) * 100))}%
|
return percent > 80
|
||||||
|
? 'text-red-400'
|
||||||
|
: percent > 50
|
||||||
|
? 'text-yellow-400'
|
||||||
|
: 'text-green-400';
|
||||||
|
})()
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{Math.min(
|
||||||
|
100,
|
||||||
|
Math.round(
|
||||||
|
(messages
|
||||||
|
.filter(m => m.role !== 'system')
|
||||||
|
.reduce((acc, m) => acc + m.content.length, 0) /
|
||||||
|
5000) *
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -491,7 +507,10 @@ const AIChat = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="flex-1 border border-primary/30 rounded-lg p-4 mb-4 bg-background/50" ref={scrollRef}>
|
<ScrollArea
|
||||||
|
className="flex-1 border border-primary/30 rounded-lg p-4 mb-4 bg-background/50"
|
||||||
|
ref={scrollRef}
|
||||||
|
>
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<div className="h-full flex items-center justify-center text-muted-foreground">
|
<div className="h-full flex items-center justify-center text-muted-foreground">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -502,7 +521,7 @@ const AIChat = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) =>
|
||||||
message.role === 'system' ? (
|
message.role === 'system' ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
@@ -521,7 +540,9 @@ const AIChat = () => {
|
|||||||
initial={{ opacity: 0, x: message.role === 'user' ? 20 : -20 }}
|
initial={{ opacity: 0, x: message.role === 'user' ? 20 : -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: index * 0.05 }}
|
transition={{ delay: index * 0.05 }}
|
||||||
className={`flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
className={`flex gap-3 ${
|
||||||
|
message.role === 'user' ? 'justify-end' : 'justify-start'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{message.role === 'assistant' && (
|
{message.role === 'assistant' && (
|
||||||
<div className="w-8 h-8 rounded border border-primary/50 flex items-center justify-center bg-primary/10 flex-shrink-0">
|
<div className="w-8 h-8 rounded border border-primary/50 flex items-center justify-center bg-primary/10 flex-shrink-0">
|
||||||
@@ -537,7 +558,11 @@ const AIChat = () => {
|
|||||||
>
|
>
|
||||||
<MessageContent
|
<MessageContent
|
||||||
content={message.content}
|
content={message.content}
|
||||||
isLoading={isLoading && message.role === 'assistant' && message.content === ''}
|
isLoading={
|
||||||
|
isLoading &&
|
||||||
|
message.role === 'assistant' &&
|
||||||
|
message.content === ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
@@ -546,8 +571,8 @@ const AIChat = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
),
|
||||||
))}
|
)}
|
||||||
{isLoading && messages[messages.length - 1]?.role === 'user' && (
|
{isLoading && messages[messages.length - 1]?.role === 'user' && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@@ -602,7 +627,9 @@ const AIChat = () => {
|
|||||||
id="endpoint"
|
id="endpoint"
|
||||||
placeholder="https://api.example.com/v1/chat/completions"
|
placeholder="https://api.example.com/v1/chat/completions"
|
||||||
value={tempCustomConfig.endpoint}
|
value={tempCustomConfig.endpoint}
|
||||||
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, endpoint: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setTempCustomConfig(prev => ({ ...prev, endpoint: e.target.value }))
|
||||||
|
}
|
||||||
className="font-pixel text-sm bg-background/50 border-primary/50"
|
className="font-pixel text-sm bg-background/50 border-primary/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -613,7 +640,9 @@ const AIChat = () => {
|
|||||||
type="password"
|
type="password"
|
||||||
placeholder="sk-..."
|
placeholder="sk-..."
|
||||||
value={tempCustomConfig.apiKey}
|
value={tempCustomConfig.apiKey}
|
||||||
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, apiKey: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setTempCustomConfig(prev => ({ ...prev, apiKey: e.target.value }))
|
||||||
|
}
|
||||||
className="font-pixel text-sm bg-background/50 border-primary/50"
|
className="font-pixel text-sm bg-background/50 border-primary/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -623,7 +652,9 @@ const AIChat = () => {
|
|||||||
id="model"
|
id="model"
|
||||||
placeholder="gpt-4, claude-3, etc."
|
placeholder="gpt-4, claude-3, etc."
|
||||||
value={tempCustomConfig.model}
|
value={tempCustomConfig.model}
|
||||||
onChange={(e) => setTempCustomConfig(prev => ({ ...prev, model: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setTempCustomConfig(prev => ({ ...prev, model: e.target.value }))
|
||||||
|
}
|
||||||
className="font-pixel text-sm bg-background/50 border-primary/50"
|
className="font-pixel text-sm bg-background/50 border-primary/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -658,6 +689,8 @@ const AIChat = () => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return isFullscreen ? <FullscreenPortal>{content}</FullscreenPortal> : content;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AIChat;
|
export default AIChat;
|
||||||
|
|||||||
+7
-181
@@ -8,6 +8,7 @@ import { Maximize2, Minimize2, Users, User } from 'lucide-react';
|
|||||||
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
import { useBrowserFullscreen } from '@/hooks/useGameDimensions';
|
||||||
import GameTouchButton from '@/components/GameTouchButton';
|
import GameTouchButton from '@/components/GameTouchButton';
|
||||||
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock';
|
||||||
|
import { FullscreenPortal } from '@/components/FullscreenPortal';
|
||||||
|
|
||||||
const BOARD_WIDTH = 10;
|
const BOARD_WIDTH = 10;
|
||||||
const BOARD_HEIGHT = 20;
|
const BOARD_HEIGHT = 20;
|
||||||
@@ -583,7 +584,7 @@ const Tetris = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<motion.div ref={gameRef} tabIndex={0} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}
|
<motion.div ref={gameRef} tabIndex={0} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }}
|
||||||
className={`flex flex-col outline-none ${isFullscreen ? 'fixed inset-0 z-50 bg-background p-4' : 'h-full'}`}>
|
className={`flex flex-col outline-none ${isFullscreen ? 'fixed inset-0 z-50 bg-background p-4' : 'h-full'}`}>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
@@ -619,197 +620,22 @@ const Tetris = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* ... keep existing code (rest of sidebar / stats / UI) */}
|
||||||
{/* Score Panel */}
|
|
||||||
<div className="border-2 border-primary/50 p-4 bg-background/60">
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider">Score</p>
|
|
||||||
<p className="font-minecraft text-2xl text-primary text-glow-strong">{score.toLocaleString()}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* High Score */}
|
|
||||||
<div className="border border-primary/30 p-3 bg-background/40">
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">High Score</p>
|
|
||||||
<p className="font-minecraft text-lg text-primary/80">{highScore.toLocaleString()}</p>
|
|
||||||
<p className="font-pixel text-[8px] text-foreground/30 mt-1">max: 4,294,967,296</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lines */}
|
|
||||||
<div className="border border-primary/30 p-3 bg-background/40">
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">Lines</p>
|
|
||||||
<p className="font-minecraft text-xl text-primary">{lines}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Controls */}
|
|
||||||
<div className="border border-primary/20 p-3 bg-background/30">
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider mb-2">Controls</p>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/60">← → ↑ ↓ / WASD</p>
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/60">Space: Hard Drop</p>
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/60">P: Pause</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Button */}
|
|
||||||
{!gameStarted || gameOver || gameComplete ? (
|
|
||||||
<button onClick={() => startGame('1p')} className="font-minecraft text-sm py-3 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
|
||||||
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START GAME'}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button onClick={togglePause} className="font-minecraft text-sm py-3 px-4 border-2 border-primary/60 bg-background/50 text-primary hover:bg-primary/20 transition-all">
|
|
||||||
{isPaused ? 'RESUME' : 'PAUSE'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* P1 Board */}
|
{/* ... keep existing code (2P layout) */}
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-3 rounded-sm bg-primary" />
|
|
||||||
<span className="font-pixel text-xs text-primary">P1 (WASD/Q)</span>
|
|
||||||
</div>
|
|
||||||
<div className="border-2 border-primary box-glow p-1 bg-background/80">{renderBoard2P(player1, 'border-primary/20')}</div>
|
|
||||||
<div className="flex gap-2 text-center">
|
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{player1.score}</p></div>
|
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{player1.lines}</p></div>
|
|
||||||
</div>
|
|
||||||
{player1.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* P1 Next Piece */}
|
|
||||||
<div className="border-2 border-primary/50 p-4 bg-background/60">
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider mb-3">Next</p>
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{player1.nextPiece.shape.map((row, y) => (
|
|
||||||
<div key={y} className="flex gap-1">
|
|
||||||
{row.map((val, x) => (
|
|
||||||
<div key={`${y}-${x}`} className={`w-4 h-4 ${val ? 'bg-primary box-glow' : 'bg-transparent'}`} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Center controls */}
|
|
||||||
<div className="flex flex-col gap-2 min-w-[100px] items-center">
|
|
||||||
<div className="border border-primary/50 p-2 bg-background/50 text-center">
|
|
||||||
<p className="font-pixel text-[8px] text-foreground/60 mb-1">CONTROLS</p>
|
|
||||||
<p className="font-pixel text-[8px] text-primary">P1: WASD Q</p>
|
|
||||||
<p className="font-pixel text-[8px] text-purple-400">P2: ↑↓←→ /</p>
|
|
||||||
<p className="font-pixel text-[8px] text-foreground/60 mt-1">P: Pause</p>
|
|
||||||
</div>
|
|
||||||
{!gameStarted || gameOver ? (
|
|
||||||
<button onClick={() => startGame('2p')} className="font-minecraft text-xs py-2 px-3 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
|
||||||
{gameOver ? 'AGAIN' : 'START'}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button onClick={togglePause} className="font-minecraft text-xs py-2 px-3 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all">
|
|
||||||
{isPaused ? 'RESUME' : 'PAUSE'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* P2 Board */}
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: 'hsl(280 70% 50%)' }} />
|
|
||||||
<span className="font-pixel text-xs text-purple-400">P2 (↑↓←→/)</span>
|
|
||||||
</div>
|
|
||||||
<div className="border-2 border-purple-500 p-1 bg-background/80" style={{ boxShadow: '0 0 10px hsl(280 70% 50% / 0.5)' }}>{renderBoard2P(player2, 'border-purple-500/20')}</div>
|
|
||||||
<div className="flex gap-2 text-center">
|
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-purple-400">{player2.score}</p></div>
|
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-purple-400">{player2.lines}</p></div>
|
|
||||||
</div>
|
|
||||||
{player2.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* P2 Next Piece */}
|
|
||||||
<div className="border-2 border-purple-500/50 p-4 bg-background/60">
|
|
||||||
<p className="font-pixel text-[10px] text-purple-400/70 uppercase tracking-wider mb-3">Next</p>
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{player2.nextPiece.shape.map((row, y) => (
|
|
||||||
<div key={y} className="flex gap-1">
|
|
||||||
{row.map((val, x) => (
|
|
||||||
<div key={`${y}-${x}`} className={`w-4 h-4 ${val ? 'bg-purple-400' : 'bg-transparent'}`} style={{ boxShadow: val ? '0 0 6px hsl(280 70% 50%)' : 'none' }} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isMobile && gameMode === '1p' && (
|
|
||||||
<>
|
|
||||||
<MobileControlsSpacer />
|
|
||||||
<MobileControlsDock>
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
|
||||||
<div className="flex gap-4 text-center">
|
|
||||||
<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><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</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>
|
|
||||||
|
|
||||||
{gameStarted && !gameOver && !gameComplete ? (
|
|
||||||
<div className="grid grid-cols-3 gap-1 mt-2">
|
|
||||||
<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 />
|
|
||||||
<GameTouchButton onAction={moveLeft} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
|
||||||
<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('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'}</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</MobileControlsDock>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Overlays */}
|
{/* ... keep existing code (mobile controls + overlays) */}
|
||||||
{!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && (
|
|
||||||
<div className="fixed inset-0 bg-background/80 flex items-center justify-center z-50">
|
|
||||||
<div className="border-2 border-primary box-glow-strong p-6 bg-background text-center">
|
|
||||||
<h2 className="font-minecraft text-2xl text-primary text-glow-strong mb-3">
|
|
||||||
{gameMode === '2p' && gameOver ? winner : gameComplete ? 'GAME COMPLETE!' : gameOver ? 'GAME OVER' : 'PAUSED'}
|
|
||||||
</h2>
|
|
||||||
{gameMode === '1p' && (gameOver || gameComplete) && (
|
|
||||||
<>
|
|
||||||
<p className="font-pixel text-sm text-foreground/80 mb-1">Final Score: {score.toLocaleString()}</p>
|
|
||||||
<p className="font-pixel text-xs text-foreground/60 mb-3">Lines: {lines}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{gameMode === '2p' && gameOver && (
|
|
||||||
<div className="flex gap-4 justify-center mb-3">
|
|
||||||
<div>
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/60">P1 Score</p>
|
|
||||||
<p className="font-minecraft text-lg text-primary">{player1.score}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-pixel text-[10px] text-foreground/60">P2 Score</p>
|
|
||||||
<p className="font-minecraft text-lg text-purple-400">{player2.score}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button onClick={(gameOver || gameComplete) ? () => startGame(gameMode!) : () => setIsPaused(false)} className="font-minecraft text-sm py-2 px-6 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
|
||||||
{(gameOver || gameComplete) ? 'PLAY AGAIN' : 'RESUME'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return isFullscreen ? <FullscreenPortal>{content}</FullscreenPortal> : content;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Tetris;
|
export default Tetris;
|
||||||
Reference in New Issue
Block a user