feat: implement revolutionary drag-and-drop champion arena interface
🏟️ MAJOR FEATURE: Transform games page with innovative Champion Arena - Create ChampionArena component with drag-and-drop avatar selection - Combine "choose your champion" + player count in one intuitive interface - Visual arena where users drag avatars to set game mode (solo/battle/tournament) - Replace modal selection with always-visible arena experience - Add GameModeContext for centralized player configuration management - Enhanced animations: arena entry, champion ready, floating effects - Real-time mode indicators and champion status tracking - Click to remove champions from arena with smooth animations ✨ INNOVATION: Drag avatars into arena = instant game mode selection - 1 champion = Solo Mode - 2 champions = Battle Mode - 3+ champions = Tournament Mode 🎮 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,28 +5,18 @@ import Link from 'next/link'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { grid } from '../../../styled-system/patterns'
|
||||
import { useUserProfile } from '../../contexts/UserProfileContext'
|
||||
import { useGameMode } from '../../contexts/GameModeContext'
|
||||
import { ChampionArena } from '../../components/ChampionArena'
|
||||
|
||||
export default function GamesPage() {
|
||||
const { profile } = useUserProfile()
|
||||
const [showCharacterSelection, setShowCharacterSelection] = useState<string | null>(null)
|
||||
const [selectedGameMode, setSelectedGameMode] = useState<'single' | 'two-player'>('single')
|
||||
const { gameMode, getActivePlayer } = useGameMode()
|
||||
|
||||
const handleGameClick = (gameType: string) => {
|
||||
setShowCharacterSelection(gameType)
|
||||
}
|
||||
|
||||
const handleCharacterSelectionClose = () => {
|
||||
setShowCharacterSelection(null)
|
||||
}
|
||||
|
||||
const handleStartGame = (character: 1 | 2) => {
|
||||
console.log(`Starting ${showCharacterSelection} with character ${character}`)
|
||||
setShowCharacterSelection(null)
|
||||
|
||||
// Navigate directly to games - let them handle their own mode selection
|
||||
if (showCharacterSelection === 'memory-lightning') {
|
||||
// Navigate directly to games using the centralized game mode
|
||||
if (gameType === 'memory-lightning') {
|
||||
window.location.href = '/games/memory-quiz'
|
||||
} else if (showCharacterSelection === 'battle-arena') {
|
||||
} else if (gameType === 'battle-arena') {
|
||||
window.location.href = '/games/matching'
|
||||
}
|
||||
}
|
||||
@@ -183,6 +173,13 @@ export default function GamesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Champion Arena - Drag & Drop Interface */}
|
||||
<div className={css({
|
||||
mb: '16'
|
||||
})}>
|
||||
<ChampionArena />
|
||||
</div>
|
||||
|
||||
{/* Character Showcase Header */}
|
||||
<div className={css({
|
||||
mb: '16'
|
||||
@@ -203,7 +200,7 @@ export default function GamesPage() {
|
||||
color: 'gray.600',
|
||||
fontSize: 'lg'
|
||||
})}>
|
||||
Choose your character and dominate the leaderboards!
|
||||
Track your progress and achievements!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1877,230 +1874,6 @@ export default function GamesPage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Character Selection Modal */}
|
||||
{showCharacterSelection && (
|
||||
<div className={css({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.6)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 50,
|
||||
animation: 'fadeIn 0.3s ease-out'
|
||||
})}>
|
||||
<div className={css({
|
||||
background: 'white',
|
||||
rounded: '3xl',
|
||||
p: '8',
|
||||
maxW: '2xl',
|
||||
w: '95%',
|
||||
maxH: '90vh',
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
boxShadow: '0 25px 50px rgba(0, 0, 0, 0.2)',
|
||||
animation: 'slideInUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
||||
})}>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={handleCharacterSelectionClose}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '4',
|
||||
right: '4',
|
||||
w: '10',
|
||||
h: '10',
|
||||
rounded: 'full',
|
||||
background: 'gray.100',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 'xl',
|
||||
color: 'gray.500',
|
||||
transition: 'all 0.2s ease',
|
||||
_hover: {
|
||||
background: 'gray.200',
|
||||
color: 'gray.700'
|
||||
}
|
||||
})}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
{/* Modal Header */}
|
||||
<div className={css({
|
||||
textAlign: 'center',
|
||||
mb: '8'
|
||||
})}>
|
||||
<h2 className={css({
|
||||
fontSize: '3xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '2'
|
||||
})}>
|
||||
🎮 Choose Your Champion
|
||||
</h2>
|
||||
<p className={css({
|
||||
color: 'gray.600',
|
||||
fontSize: 'lg'
|
||||
})}>
|
||||
Select a character for {showCharacterSelection === 'memory-lightning' ? 'Memory Lightning ⚡' : 'Memory Pairs 🧠'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Character Selection */}
|
||||
<div className={css({
|
||||
mb: '8'
|
||||
})}>
|
||||
<h3 className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'semibold',
|
||||
color: 'gray.800',
|
||||
mb: '4',
|
||||
textAlign: 'center'
|
||||
})}>
|
||||
🌟 Choose Your Champion
|
||||
</h3>
|
||||
|
||||
<div className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '6'
|
||||
})}>
|
||||
{/* Player 1 Character */}
|
||||
<div
|
||||
onClick={() => handleStartGame(1)}
|
||||
className={css({
|
||||
background: 'linear-gradient(135deg, #dbeafe, #bfdbfe)',
|
||||
border: '3px solid',
|
||||
borderColor: 'blue.300',
|
||||
rounded: '2xl',
|
||||
p: '6',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
transform: 'translateY(-4px) scale(1.02)',
|
||||
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
|
||||
borderColor: 'blue.400'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className={css({
|
||||
fontSize: '5xl',
|
||||
mb: '3',
|
||||
animation: 'characterBounce 0.6s ease-in-out infinite alternate'
|
||||
})}>
|
||||
{profile.player1Emoji}
|
||||
</div>
|
||||
<h4 className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'blue.800',
|
||||
mb: '2'
|
||||
})}>
|
||||
{profile.player1Name}
|
||||
</h4>
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'blue.700',
|
||||
mb: '3'
|
||||
})}>
|
||||
Level {Math.floor(profile.gamesPlayed / 5) + 1} • {profile.totalWins} wins
|
||||
</div>
|
||||
<div className={css({
|
||||
background: 'white',
|
||||
rounded: 'lg',
|
||||
p: '3',
|
||||
fontSize: 'sm',
|
||||
color: 'blue.800',
|
||||
fontWeight: 'semibold'
|
||||
})}>
|
||||
🚀 Ready to dominate!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Player 2 Character */}
|
||||
<div
|
||||
onClick={() => handleStartGame(2)}
|
||||
className={css({
|
||||
background: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)',
|
||||
border: '3px solid',
|
||||
borderColor: 'purple.300',
|
||||
rounded: '2xl',
|
||||
p: '6',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
transform: 'translateY(-4px) scale(1.02)',
|
||||
boxShadow: '0 20px 40px rgba(139, 92, 246, 0.2)',
|
||||
borderColor: 'purple.400'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div className={css({
|
||||
fontSize: '5xl',
|
||||
mb: '3',
|
||||
animation: 'characterBounce 0.6s ease-in-out infinite alternate 0.3s'
|
||||
})}>
|
||||
{profile.player2Emoji}
|
||||
</div>
|
||||
<h4 className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'purple.800',
|
||||
mb: '2'
|
||||
})}>
|
||||
{profile.player2Name}
|
||||
</h4>
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'purple.700',
|
||||
mb: '3'
|
||||
})}>
|
||||
Level {Math.floor(profile.gamesPlayed / 5) + 1} • {Math.floor(profile.totalWins / 2)} wins
|
||||
</div>
|
||||
<div className={css({
|
||||
background: 'white',
|
||||
rounded: 'lg',
|
||||
p: '3',
|
||||
fontSize: 'sm',
|
||||
color: 'purple.800',
|
||||
fontWeight: 'semibold'
|
||||
})}>
|
||||
⚡ Bring it on!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Info */}
|
||||
<div className={css({
|
||||
background: 'gray.50',
|
||||
rounded: 'xl',
|
||||
p: '4',
|
||||
textAlign: 'center'
|
||||
})}>
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.700',
|
||||
fontWeight: 'medium'
|
||||
})}>
|
||||
💡 Tip: Each character tracks their own progress and achievements!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import { AbacusDisplayProvider } from '@/contexts/AbacusDisplayContext'
|
||||
import { UserProfileProvider } from '@/contexts/UserProfileContext'
|
||||
import { GameModeProvider } from '@/contexts/GameModeContext'
|
||||
import { AppNavBar } from '@/components/AppNavBar'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -19,8 +20,10 @@ export default function RootLayout({
|
||||
<body>
|
||||
<AbacusDisplayProvider>
|
||||
<UserProfileProvider>
|
||||
<AppNavBar />
|
||||
{children}
|
||||
<GameModeProvider>
|
||||
<AppNavBar />
|
||||
{children}
|
||||
</GameModeProvider>
|
||||
</UserProfileProvider>
|
||||
</AbacusDisplayProvider>
|
||||
</body>
|
||||
|
||||
544
apps/web/src/components/ChampionArena.tsx
Normal file
544
apps/web/src/components/ChampionArena.tsx
Normal file
@@ -0,0 +1,544 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { useUserProfile } from '../contexts/UserProfileContext'
|
||||
import { useGameMode } from '../contexts/GameModeContext'
|
||||
|
||||
interface ChampionArenaProps {
|
||||
onGameModeChange?: (mode: 'single' | 'battle' | 'tournament') => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ChampionArena({ onGameModeChange, className }: ChampionArenaProps) {
|
||||
const { profile } = useUserProfile()
|
||||
const { gameMode, players, setGameMode, updatePlayer } = useGameMode()
|
||||
const [draggedPlayer, setDraggedPlayer] = useState<number | null>(null)
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
const arenaRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const availablePlayers = players.filter(player => !player.isActive)
|
||||
const arenaPlayers = players.filter(player => player.isActive)
|
||||
|
||||
const handleDragStart = (playerId: number) => {
|
||||
setDraggedPlayer(playerId)
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragOver(true)
|
||||
}
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
if (!arenaRef.current?.contains(e.relatedTarget as Node)) {
|
||||
setIsDragOver(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragOver(false)
|
||||
|
||||
if (draggedPlayer) {
|
||||
// Activate the dragged player
|
||||
updatePlayer(draggedPlayer, { isActive: true })
|
||||
|
||||
// Determine new game mode based on active player count
|
||||
const newActiveCount = arenaPlayers.length + 1
|
||||
let newMode: 'single' | 'battle' | 'tournament' = 'single'
|
||||
|
||||
if (newActiveCount === 1) {
|
||||
newMode = 'single'
|
||||
} else if (newActiveCount === 2) {
|
||||
newMode = 'battle'
|
||||
} else {
|
||||
newMode = 'tournament'
|
||||
}
|
||||
|
||||
setGameMode(newMode)
|
||||
onGameModeChange?.(newMode)
|
||||
setDraggedPlayer(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveFromArena = (playerId: number) => {
|
||||
updatePlayer(playerId, { isActive: false })
|
||||
|
||||
// Update game mode based on remaining players
|
||||
const newActiveCount = arenaPlayers.length - 1
|
||||
let newMode: 'single' | 'battle' | 'tournament' = 'single'
|
||||
|
||||
if (newActiveCount === 0) {
|
||||
newMode = 'single'
|
||||
// Re-activate player 1 by default
|
||||
updatePlayer(1, { isActive: true })
|
||||
} else if (newActiveCount === 1) {
|
||||
newMode = 'single'
|
||||
} else if (newActiveCount === 2) {
|
||||
newMode = 'battle'
|
||||
} else {
|
||||
newMode = 'tournament'
|
||||
}
|
||||
|
||||
setGameMode(newMode)
|
||||
onGameModeChange?.(newMode)
|
||||
}
|
||||
|
||||
const getPlayerEmoji = (id: number) => {
|
||||
if (id === 1) return profile.player1Emoji
|
||||
if (id === 2) return profile.player2Emoji
|
||||
const player = players.find(p => p.id === id)
|
||||
return player?.emoji || '😀'
|
||||
}
|
||||
|
||||
const getPlayerName = (id: number) => {
|
||||
if (id === 1) return profile.player1Name
|
||||
if (id === 2) return profile.player2Name
|
||||
const player = players.find(p => p.id === id)
|
||||
return player?.name || `Player ${id}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css({
|
||||
background: 'white',
|
||||
rounded: '3xl',
|
||||
p: '8',
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.200',
|
||||
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.1)',
|
||||
transition: 'all 0.3s ease'
|
||||
}) + (className ? ` ${className}` : '')}>
|
||||
|
||||
{/* Header */}
|
||||
<div className={css({
|
||||
textAlign: 'center',
|
||||
mb: '8'
|
||||
})}>
|
||||
<h2 className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '2'
|
||||
})}>
|
||||
🏟️ Champion Arena
|
||||
</h2>
|
||||
<p className={css({
|
||||
color: 'gray.600',
|
||||
fontSize: 'lg',
|
||||
mb: '4'
|
||||
})}>
|
||||
Drag your champions into the arena to set your game mode!
|
||||
</p>
|
||||
|
||||
{/* Current Mode Indicator */}
|
||||
<div className={css({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
background: gameMode === 'single'
|
||||
? 'linear-gradient(135deg, #dbeafe, #bfdbfe)'
|
||||
: gameMode === 'battle'
|
||||
? 'linear-gradient(135deg, #e9d5ff, #ddd6fe)'
|
||||
: 'linear-gradient(135deg, #fef3c7, #fde68a)',
|
||||
px: '4',
|
||||
py: '2',
|
||||
rounded: 'full',
|
||||
border: '2px solid',
|
||||
borderColor: gameMode === 'single'
|
||||
? 'blue.300'
|
||||
: gameMode === 'battle'
|
||||
? 'purple.300'
|
||||
: 'yellow.300'
|
||||
})}>
|
||||
<span className={css({ fontSize: 'lg' })}>
|
||||
{gameMode === 'single' ? '👤' : gameMode === 'battle' ? '⚔️' : '🏆'}
|
||||
</span>
|
||||
<span className={css({
|
||||
fontWeight: 'bold',
|
||||
color: gameMode === 'single'
|
||||
? 'blue.800'
|
||||
: gameMode === 'battle'
|
||||
? 'purple.800'
|
||||
: 'yellow.800',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 'sm'
|
||||
})}>
|
||||
{gameMode === 'single' ? 'Solo Mode' : gameMode === 'battle' ? 'Battle Mode' : 'Tournament Mode'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1fr', lg: '1fr 1fr' },
|
||||
gap: '8',
|
||||
alignItems: 'start'
|
||||
})}>
|
||||
|
||||
{/* Available Champions Roster */}
|
||||
<div className={css({
|
||||
order: { base: 2, lg: 1 }
|
||||
})}>
|
||||
<h3 className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800',
|
||||
mb: '4',
|
||||
textAlign: 'center'
|
||||
})}>
|
||||
🎯 Available Champions
|
||||
</h3>
|
||||
|
||||
<div className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
|
||||
gap: '4',
|
||||
p: '6',
|
||||
background: 'linear-gradient(135deg, #f8fafc, #f1f5f9)',
|
||||
rounded: '2xl',
|
||||
border: '2px dashed',
|
||||
borderColor: 'gray.300',
|
||||
minH: '32'
|
||||
})}>
|
||||
{availablePlayers.map((player) => (
|
||||
<div
|
||||
key={player.id}
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(player.id)}
|
||||
className={css({
|
||||
background: 'white',
|
||||
rounded: '2xl',
|
||||
p: '4',
|
||||
textAlign: 'center',
|
||||
cursor: 'grab',
|
||||
border: '2px solid',
|
||||
borderColor: player.color,
|
||||
boxShadow: '0 8px 20px rgba(0, 0, 0, 0.1)',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
transform: 'translateY(-4px) scale(1.05)',
|
||||
boxShadow: '0 12px 30px rgba(0, 0, 0, 0.15)',
|
||||
'& .champion-emoji': {
|
||||
transform: 'scale(1.2) rotate(10deg)',
|
||||
animation: 'championBounce 0.6s ease-in-out'
|
||||
}
|
||||
},
|
||||
_active: {
|
||||
cursor: 'grabbing',
|
||||
transform: 'scale(0.95)'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
mb: '2',
|
||||
transition: 'all 0.3s ease'
|
||||
}) + ' champion-emoji'}
|
||||
>
|
||||
{getPlayerEmoji(player.id)}
|
||||
</div>
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800'
|
||||
})}>
|
||||
{getPlayerName(player.id)}
|
||||
</div>
|
||||
<div className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'gray.600',
|
||||
mt: '1'
|
||||
})}>
|
||||
Level {Math.floor((profile.gamesPlayed || 0) / 5) + 1}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{availablePlayers.length === 0 && (
|
||||
<div className={css({
|
||||
gridColumn: '1 / -1',
|
||||
textAlign: 'center',
|
||||
color: 'gray.500',
|
||||
fontSize: 'sm',
|
||||
fontStyle: 'italic',
|
||||
py: '8'
|
||||
})}>
|
||||
All champions are in the arena! 🎮
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arena Drop Zone */}
|
||||
<div className={css({
|
||||
order: { base: 1, lg: 2 }
|
||||
})}>
|
||||
<h3 className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800',
|
||||
mb: '4',
|
||||
textAlign: 'center'
|
||||
})}>
|
||||
🏟️ Battle Arena
|
||||
</h3>
|
||||
|
||||
<div
|
||||
ref={arenaRef}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={css({
|
||||
p: '8',
|
||||
background: isDragOver
|
||||
? 'linear-gradient(135deg, #dcfce7, #bbf7d0)'
|
||||
: 'linear-gradient(135deg, #fef3c7, #fde68a)',
|
||||
rounded: '3xl',
|
||||
border: '3px dashed',
|
||||
borderColor: isDragOver ? 'green.400' : 'yellow.400',
|
||||
minH: '64',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
})}
|
||||
>
|
||||
{/* Arena Background Pattern */}
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundImage: 'radial-gradient(circle at 25% 25%, rgba(251, 191, 36, 0.1) 0%, transparent 50%), radial-gradient(circle at 75% 75%, rgba(245, 158, 11, 0.1) 0%, transparent 50%)',
|
||||
pointerEvents: 'none'
|
||||
})} />
|
||||
|
||||
{arenaPlayers.length === 0 ? (
|
||||
<div className={css({
|
||||
textAlign: 'center',
|
||||
zIndex: 1
|
||||
})}>
|
||||
<div className={css({
|
||||
fontSize: '4xl',
|
||||
mb: '4',
|
||||
opacity: isDragOver ? 1 : 0.6,
|
||||
transition: 'all 0.3s ease'
|
||||
})}>
|
||||
{isDragOver ? '✨' : '🏟️'}
|
||||
</div>
|
||||
<p className={css({
|
||||
color: 'gray.700',
|
||||
fontWeight: 'semibold',
|
||||
fontSize: 'lg'
|
||||
})}>
|
||||
{isDragOver ? 'Drop to enter the arena!' : 'Drag champions here'}
|
||||
</p>
|
||||
<p className={css({
|
||||
color: 'gray.600',
|
||||
fontSize: 'sm',
|
||||
mt: '2'
|
||||
})}>
|
||||
1 champion = Solo • 2 = Battle • 3+ = Tournament
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))',
|
||||
gap: '4',
|
||||
w: 'full',
|
||||
zIndex: 1
|
||||
})}>
|
||||
{arenaPlayers.map((player, index) => (
|
||||
<div
|
||||
key={player.id}
|
||||
className={css({
|
||||
background: 'white',
|
||||
rounded: '2xl',
|
||||
p: '4',
|
||||
textAlign: 'center',
|
||||
border: '3px solid',
|
||||
borderColor: player.color,
|
||||
boxShadow: `0 0 20px ${player.color}40`,
|
||||
position: 'relative',
|
||||
animation: `arenaEntry 0.6s ease-out ${index * 0.1}s both`,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
transform: 'translateY(-2px) scale(1.05)',
|
||||
boxShadow: `0 8px 25px ${player.color}60`
|
||||
}
|
||||
})}
|
||||
onClick={() => handleRemoveFromArena(player.id)}
|
||||
>
|
||||
{/* Remove Button */}
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
top: '-2',
|
||||
right: '-2',
|
||||
w: '6',
|
||||
h: '6',
|
||||
background: 'red.500',
|
||||
rounded: 'full',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 'xs',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
background: 'red.600',
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
})}
|
||||
style={{
|
||||
opacity: 1 // Always show remove button
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</div>
|
||||
|
||||
{/* Champion Ready Animation */}
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
top: '-4px',
|
||||
left: '-4px',
|
||||
right: '-4px',
|
||||
bottom: '-4px',
|
||||
borderRadius: '20px',
|
||||
border: '2px solid',
|
||||
borderColor: player.color,
|
||||
animation: 'championReady 2s ease-in-out infinite',
|
||||
opacity: 0.6
|
||||
})} />
|
||||
|
||||
<div className={css({
|
||||
fontSize: '3xl',
|
||||
mb: '2',
|
||||
animation: 'championFloat 3s ease-in-out infinite'
|
||||
})}>
|
||||
{getPlayerEmoji(player.id)}
|
||||
</div>
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800'
|
||||
})}>
|
||||
{getPlayerName(player.id)}
|
||||
</div>
|
||||
<div className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'green.700',
|
||||
fontWeight: 'semibold',
|
||||
mt: '1'
|
||||
})}>
|
||||
READY! 🔥
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Arena Info */}
|
||||
{arenaPlayers.length > 0 && (
|
||||
<div className={css({
|
||||
mt: '4',
|
||||
textAlign: 'center',
|
||||
background: 'white',
|
||||
rounded: 'xl',
|
||||
p: '4',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200'
|
||||
})}>
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'semibold',
|
||||
color: 'gray.700'
|
||||
})}>
|
||||
🎮 {arenaPlayers.length} champion{arenaPlayers.length > 1 ? 's' : ''} ready to battle!
|
||||
</div>
|
||||
<div className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
mt: '1'
|
||||
})}>
|
||||
Click a champion to remove from arena
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Enhanced animations for champion arena
|
||||
const championArenaAnimations = `
|
||||
@keyframes championBounce {
|
||||
0% { transform: scale(1) rotate(0deg); }
|
||||
25% { transform: scale(1.1) rotate(5deg); }
|
||||
50% { transform: scale(1.2) rotate(0deg); }
|
||||
75% { transform: scale(1.1) rotate(-5deg); }
|
||||
100% { transform: scale(1) rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes championFloat {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
33% { transform: translateY(-4px) rotate(2deg); }
|
||||
66% { transform: translateY(-2px) rotate(-2deg); }
|
||||
}
|
||||
|
||||
@keyframes championReady {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes arenaEntry {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.8) rotate(180deg);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translateY(-5px) scale(1.1) rotate(-10deg);
|
||||
}
|
||||
80% {
|
||||
transform: translateY(2px) scale(0.95) rotate(5deg);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px) scale(1) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes arenaGlow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20px rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 30px rgba(251, 191, 36, 0.6);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Inject champion arena animations
|
||||
if (typeof document !== 'undefined' && !document.getElementById('champion-arena-animations')) {
|
||||
const style = document.createElement('style')
|
||||
style.id = 'champion-arena-animations'
|
||||
style.textContent = championArenaAnimations
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
151
apps/web/src/contexts/GameModeContext.tsx
Normal file
151
apps/web/src/contexts/GameModeContext.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'
|
||||
|
||||
export type GameMode = 'single' | 'battle' | 'tournament'
|
||||
|
||||
export interface PlayerConfig {
|
||||
id: number
|
||||
name: string
|
||||
emoji: string
|
||||
color: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export interface GameModeContextType {
|
||||
gameMode: GameMode
|
||||
players: PlayerConfig[]
|
||||
activePlayerCount: number
|
||||
setGameMode: (mode: GameMode) => void
|
||||
updatePlayer: (id: number, config: Partial<PlayerConfig>) => void
|
||||
getActivePlayer: (id: number) => PlayerConfig | undefined
|
||||
resetPlayers: () => void
|
||||
}
|
||||
|
||||
const defaultPlayers: PlayerConfig[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Player 1',
|
||||
emoji: '😀',
|
||||
color: '#3b82f6', // Blue
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Player 2',
|
||||
emoji: '😎',
|
||||
color: '#8b5cf6', // Purple
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Player 3',
|
||||
emoji: '🤠',
|
||||
color: '#10b981', // Green
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Player 4',
|
||||
emoji: '🚀',
|
||||
color: '#f59e0b', // Orange
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
|
||||
const STORAGE_KEY = 'soroban-game-mode-config'
|
||||
|
||||
const GameModeContext = createContext<GameModeContextType | null>(null)
|
||||
|
||||
export function GameModeProvider({ children }: { children: ReactNode }) {
|
||||
const [gameMode, setGameModeState] = useState<GameMode>('single')
|
||||
const [players, setPlayers] = useState<PlayerConfig[]>(defaultPlayers)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
|
||||
// Load configuration from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
const config = JSON.parse(stored)
|
||||
setGameModeState(config.gameMode || 'single')
|
||||
setPlayers(config.players || defaultPlayers)
|
||||
}
|
||||
setIsInitialized(true)
|
||||
} catch (error) {
|
||||
console.warn('Failed to load game mode config from localStorage:', error)
|
||||
setIsInitialized(true)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Save configuration to localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && isInitialized) {
|
||||
try {
|
||||
const config = { gameMode, players }
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
|
||||
} catch (error) {
|
||||
console.warn('Failed to save game mode config to localStorage:', error)
|
||||
}
|
||||
}
|
||||
}, [gameMode, players, isInitialized])
|
||||
|
||||
const setGameMode = (mode: GameMode) => {
|
||||
setGameModeState(mode)
|
||||
|
||||
// Auto-configure active players based on mode
|
||||
setPlayers(prevPlayers => prevPlayers.map(player => ({
|
||||
...player,
|
||||
isActive: mode === 'single'
|
||||
? player.id === 1
|
||||
: mode === 'battle'
|
||||
? player.id <= 2
|
||||
: player.id <= 4 // tournament mode
|
||||
})))
|
||||
}
|
||||
|
||||
const updatePlayer = (id: number, config: Partial<PlayerConfig>) => {
|
||||
setPlayers(prevPlayers =>
|
||||
prevPlayers.map(player =>
|
||||
player.id === id ? { ...player, ...config } : player
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const getActivePlayer = (id: number) => {
|
||||
return players.find(player => player.id === id && player.isActive)
|
||||
}
|
||||
|
||||
const resetPlayers = () => {
|
||||
setPlayers(defaultPlayers)
|
||||
setGameModeState('single')
|
||||
}
|
||||
|
||||
const activePlayerCount = players.filter(player => player.isActive).length
|
||||
|
||||
const contextValue: GameModeContextType = {
|
||||
gameMode,
|
||||
players,
|
||||
activePlayerCount,
|
||||
setGameMode,
|
||||
updatePlayer,
|
||||
getActivePlayer,
|
||||
resetPlayers
|
||||
}
|
||||
|
||||
return (
|
||||
<GameModeContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</GameModeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useGameMode(): GameModeContextType {
|
||||
const context = useContext(GameModeContext)
|
||||
if (!context) {
|
||||
throw new Error('useGameMode must be used within a GameModeProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user