created the tournament ui and fixed the turns sequence

This commit is contained in:
2025-12-22 19:54:01 +01:00
parent ac21657bc7
commit 937620bac1
14 changed files with 738 additions and 198 deletions

View File

@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.gg4oatbh7is"
"revision": "0.ca9afac9bpo"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { Layers, Box, Trophy, Users, Play } from 'lucide-react';
import { CubeManager } from './modules/cube/CubeManager';
import { TournamentManager } from './modules/tournament/TournamentManager';
import { LobbyManager } from './modules/lobby/LobbyManager';
import { DeckTester } from './modules/tester/DeckTester';
import { Pack } from './services/PackGeneratorService';
@@ -130,7 +129,13 @@ export const App: React.FC = () => {
)}
{activeTab === 'lobby' && <LobbyManager generatedPacks={generatedPacks} availableLands={availableLands} />}
{activeTab === 'tester' && <DeckTester />}
{activeTab === 'bracket' && <TournamentManager />}
{activeTab === 'bracket' && (
<div className="flex flex-col items-center justify-center h-full text-slate-400">
<Trophy className="w-16 h-16 mb-4 opacity-50" />
<h2 className="text-xl font-bold">Tournament Manager</h2>
<p>Tournaments are now managed within the Online Lobby.</p>
</div>
)}
</main>
<footer className="bg-slate-900 border-t border-slate-800 p-2 text-center text-xs text-slate-500 shrink-0">

View File

@@ -11,6 +11,7 @@ interface SidePanelPreviewProps {
onToggleCollapse: (collapsed: boolean) => void;
onResizeStart?: (e: React.MouseEvent | React.TouchEvent) => void;
className?: string; // For additional styling (positioning, z-index, etc)
children?: React.ReactNode;
}
export const SidePanelPreview: React.FC<SidePanelPreviewProps> = ({

View File

@@ -17,7 +17,10 @@ interface DeckBuilderViewProps {
roomId: string;
currentPlayerId: string;
initialPool: any[];
initialDeck?: any[];
availableBasicLands?: any[];
onSubmit?: (deck: any[]) => void;
submitLabel?: string;
}
const ManaCurve = ({ deck }: { deck: any[] }) => {
@@ -176,6 +179,40 @@ const ListItem: React.FC<{ card: DraftCard; onClick?: () => void; onHover?: (c:
);
};
const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) => {
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover, () => {
if (window.matchMedia('(pointer: coarse)').matches) {
onHover(card);
} else {
onCardClick(card);
}
}, card);
return (
<div
onClick={onClick}
onMouseEnter={() => onHover(card)}
onMouseLeave={() => onHover(null)}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
>
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
{isFoil && <FoilOverlay />}
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm">FOIL</div>}
{displayImage ? (
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" draggable={false} />
) : (
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
)}
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' : card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' : card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' : 'bg-black'}`} />
</div>
</div>
);
};
// Extracted Component to avoid re-mounting issues
const CardsDisplay: React.FC<{
cards: any[];
@@ -273,7 +310,7 @@ const CardsDisplay: React.FC<{
)
};
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, availableBasicLands = [] }) => {
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, initialDeck = [], availableBasicLands = [], onSubmit, submitLabel }) => {
// Unlimited Timer (Static for now)
const [timer] = useState<string>("Unlimited");
/* --- Hooks --- */
@@ -359,8 +396,17 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
useEffect(() => localStorage.setItem('deck_groupBy', groupBy), [groupBy]);
useEffect(() => localStorage.setItem('deck_cardWidth', cardWidth.toString()), [cardWidth]);
const [pool, setPool] = useState<any[]>(initialPool);
const [deck, setDeck] = useState<any[]>([]);
const [deck, setDeck] = useState<any[]>(initialDeck);
const [pool, setPool] = useState<any[]>(() => {
if (initialDeck && initialDeck.length > 0) {
// Need to be careful about IDs.
// If initialDeck cards are from the pool, they share IDs?
// Usually yes.
const deckIds = new Set(initialDeck.map(c => c.id));
return initialPool.filter(c => !deckIds.has(c.id));
}
return initialPool;
});
// const [lands, setLands] = useState(...); // REMOVED: Managed directly in deck now
const [hoveredCard, setHoveredCard] = useState<any>(null);
const [displayCard, setDisplayCard] = useState<any>(null);
@@ -498,7 +544,13 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
return cardWithDefinition;
});
socketService.socket.emit('player_ready', { deck: preparedDeck });
if (onSubmit) {
onSubmit(preparedDeck);
} else {
socketService.socket.emit('player_ready', { deck: preparedDeck });
}
};
const handleAutoBuild = async () => {
@@ -876,7 +928,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
onClick={submitDeck}
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg font-bold shadow-lg flex items-center gap-2 transition-transform hover:scale-105 text-sm"
>
<Save className="w-4 h-4" /> <span className="hidden sm:inline">Submit Deck</span><span className="sm:hidden">Save</span>
<Save className="w-4 h-4" /> <span className="hidden sm:inline">{submitLabel || 'Submit Deck'}</span><span className="sm:hidden">Save</span>
</button>
</div>
</div>
@@ -1004,36 +1056,4 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
);
};
const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) => {
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover, () => {
if (window.matchMedia('(pointer: coarse)').matches) {
onHover(card);
} else {
onCardClick(card);
}
}, card);
return (
<div
onClick={onClick}
onMouseEnter={() => onHover(card)}
onMouseLeave={() => onHover(null)}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
onTouchMove={onTouchMove}
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
>
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
{isFoil && <FoilOverlay />}
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm">FOIL</div>}
{displayImage ? (
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" draggable={false} />
) : (
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
)}
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' : card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' : card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' : 'bg-black'}`} />
</div>
</div>
);
};

View File

@@ -77,7 +77,10 @@ export const MulliganView: React.FC<MulliganViewProps> = ({ hand, mulliganCount,
{/* Controls */}
<div className="flex gap-8">
<button
onClick={() => onDecision(false, [])}
onClick={() => {
console.log("Mulligan Clicked");
onDecision(false, []);
}}
className="px-8 py-4 bg-red-600/20 hover:bg-red-600/40 border border-red-500 text-red-100 rounded-xl font-bold text-lg transition-all flex flex-col items-center gap-1 group"
>
<span>Mulligan</span>
@@ -85,7 +88,12 @@ export const MulliganView: React.FC<MulliganViewProps> = ({ hand, mulliganCount,
</button>
<button
onClick={() => isSelectionValid && onDecision(true, Array.from(selectedToBottom))}
onClick={() => {
if (isSelectionValid) {
console.log("Keep Hand Clicked", Array.from(selectedToBottom));
onDecision(true, Array.from(selectedToBottom));
}
}}
disabled={!isSelectionValid}
className={`px-8 py-4 rounded-xl font-bold text-lg transition-all flex flex-col items-center gap-1 min-w-[200px] ${isSelectionValid
? 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-[0_0_20px_rgba(16,185,129,0.4)]'

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { GameState, Phase, Step } from '../../types/game';
import { ManaIcon } from '../../components/ManaIcon';
import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Play, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react';
import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react';
interface PhaseStripProps {
gameState: GameState;
@@ -75,7 +75,7 @@ export const PhaseStrip: React.FC<PhaseStripProps> = ({
else actionLabel = "Pass";
} else {
// Resolve
const topItem = gameState.stack![gameState.stack!.length - 1];
// const topItem = gameState.stack![gameState.stack!.length - 1]; // Unused
actionLabel = "Resolve";
actionType = 'PASS_PRIORITY';
ActionIcon = Zap;

View File

@@ -6,6 +6,7 @@ import { Modal } from '../../components/Modal';
import { useToast } from '../../components/Toast';
import { GameView } from '../game/GameView';
import { DraftView } from '../draft/DraftView';
import { TournamentManager as TournamentView } from '../tournament/TournamentManager';
import { DeckBuilderView } from '../draft/DeckBuilderView';
interface Player {
@@ -71,6 +72,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
const messagesEndRef = useRef<HTMLDivElement>(null);
const [gameState, setGameState] = useState<any>(initialGameState || null);
const [draftState, setDraftState] = useState<any>(initialDraftState || null);
const [tournamentState, setTournamentState] = useState<any>((initialRoom as any).tournament || null);
const [preparingMatchId, setPreparingMatchId] = useState<string | null>(null);
const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game'); // Keep for mobile
// Derived State
@@ -180,14 +183,32 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
setGameState(data);
};
const handleTournamentUpdate = (data: any) => {
setTournamentState(data);
};
// Also handle finish
const handleTournamentFinished = (data: any) => {
showToast(`Tournament Winner: ${data.winner.name}!`, 'success');
};
socket.on('draft_update', handleDraftUpdate);
socket.on('draft_error', handleDraftError);
socket.on('game_update', handleGameUpdate);
socket.on('tournament_update', handleTournamentUpdate);
socket.on('tournament_finished', handleTournamentFinished);
socket.on('match_start', () => {
setPreparingMatchId(null);
});
return () => {
socket.off('draft_update', handleDraftUpdate);
socket.off('draft_error', handleDraftError);
socket.off('game_update', handleGameUpdate);
socket.off('tournament_update', handleTournamentUpdate);
socket.off('tournament_finished', handleTournamentFinished);
socket.off('match_start');
};
}, []);
@@ -271,6 +292,29 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
return <DeckBuilderView roomId={room.id} currentPlayerId={currentPlayerId} initialPool={myPool} availableBasicLands={room.basicLands} />;
}
if (room.status === 'tournament' && tournamentState) {
if (preparingMatchId) {
const myTournamentPlayer = tournamentState.players.find((p: any) => p.id === currentPlayerId);
const myPool = draftState?.players[currentPlayerId]?.pool || [];
const myDeck = myTournamentPlayer?.deck || [];
return <DeckBuilderView
roomId={room.id}
currentPlayerId={currentPlayerId}
initialPool={myPool}
initialDeck={myDeck}
availableBasicLands={room.basicLands}
onSubmit={(deck) => {
socketService.socket.emit('match_ready', { matchId: preparingMatchId, deck });
setPreparingMatchId(null);
showToast("Deck ready! Waiting for game to start...", 'success');
}}
submitLabel="Ready for Match"
/>;
}
return <TournamentView tournament={tournamentState} currentPlayerId={currentPlayerId} onJoinMatch={setPreparingMatchId} />;
}
return (
<div className="flex-1 bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl flex flex-col items-center justify-center">
<h2 className="text-3xl font-bold text-white mb-4">Waiting for Players...</h2>

View File

@@ -1,97 +1,119 @@
import React, { useState } from 'react';
import { Users } from 'lucide-react';
import { useToast } from '../../components/Toast';
import React from 'react';
import { Trophy, Play } from 'lucide-react';
import { socketService } from '../../services/SocketService';
interface TournamentPlayer {
id: string;
name: string;
isBot: boolean;
}
interface Match {
id: number;
p1: string;
p2: string;
id: string;
round: number;
matchIndex: number;
player1: TournamentPlayer | null;
player2: TournamentPlayer | null;
winnerId?: string;
status: 'pending' | 'ready' | 'in_progress' | 'finished';
}
interface Bracket {
round1: Match[];
totalPlayers: number;
interface Tournament {
id: string;
players: TournamentPlayer[];
rounds: Match[][];
currentRound: number;
status: 'setup' | 'active' | 'finished';
winner?: TournamentPlayer;
}
export const TournamentManager: React.FC = () => {
const [playerInput, setPlayerInput] = useState('');
const [bracket, setBracket] = useState<Bracket | null>(null);
const { showToast } = useToast();
interface TournamentManagerProps {
tournament: Tournament;
currentPlayerId: string;
onJoinMatch: (matchId: string) => void;
}
const shuffleArray = (array: any[]) => {
let currentIndex = array.length, randomIndex;
const newArray = [...array];
while (currentIndex !== 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[newArray[currentIndex], newArray[randomIndex]] = [newArray[randomIndex], newArray[currentIndex]];
}
return newArray;
};
export const TournamentManager: React.FC<TournamentManagerProps> = ({ tournament, currentPlayerId, onJoinMatch }) => {
const { rounds, winner } = tournament;
const generateBracket = () => {
if (!playerInput.trim()) return;
const names = playerInput.split('\n').filter(n => n.trim() !== '').map(n => n.trim());
if (names.length < 2) {
showToast("Enter at least 2 players.", 'error');
return;
}
const shuffled = shuffleArray(names);
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(shuffled.length)));
const byesNeeded = nextPowerOf2 - shuffled.length;
const fullRoster = [...shuffled];
for (let i = 0; i < byesNeeded; i++) fullRoster.push("BYE");
const pairings: Match[] = [];
for (let i = 0; i < fullRoster.length; i += 2) {
pairings.push({ id: i, p1: fullRoster[i], p2: fullRoster[i + 1] });
}
setBracket({ round1: pairings, totalPlayers: names.length });
const handleJoinMatch = (matchId: string) => {
socketService.socket.emit('join_match', { matchId }, (response: any) => {
if (!response.success) {
console.error(response.message);
// Ideally show toast
alert(response.message); // Fallback
} else {
onJoinMatch(matchId);
}
});
};
return (
<div className="h-full overflow-y-auto max-w-4xl mx-auto p-4 md:p-6">
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl mb-8">
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
<Users className="w-5 h-5 text-blue-400" /> Players
</h2>
<p className="text-sm text-slate-400 mb-2">Enter one name per line</p>
<textarea
className="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-slate-300 focus:ring-2 focus:ring-blue-500 outline-none resize-none mb-4"
placeholder={`Player 1\nPlayer 2...`}
value={playerInput}
onChange={(e) => setPlayerInput(e.target.value)}
/>
<button
onClick={generateBracket}
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded-lg font-bold w-full md:w-auto transition-colors"
>
Generate Bracket
</button>
<div className="h-full overflow-y-auto max-w-6xl mx-auto p-4 md:p-6 text-slate-100">
{/* Header */}
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl mb-8 flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
<Trophy className="w-6 h-6 text-yellow-500" /> Tournament Bracket
</h2>
<p className="text-slate-400 text-sm mt-1">Round {tournament.currentRound}</p>
</div>
{winner && (
<div className="bg-yellow-500/20 border border-yellow-500 text-yellow-200 px-6 py-3 rounded-xl flex items-center gap-3 animate-pulse">
<Trophy className="w-8 h-8" />
<div>
<div className="text-xs uppercase font-bold tracking-wider">Winner</div>
<div className="text-xl font-bold">{winner.name}</div>
</div>
</div>
)}
</div>
{bracket && (
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl overflow-x-auto">
<h3 className="text-lg font-bold text-white mb-6 border-b border-slate-700 pb-2">Round 1 (Single Elimination)</h3>
<div className="flex flex-col gap-4 min-w-[300px]">
{bracket.round1.map((match, i) => (
<div key={i} className="bg-slate-900 border border-slate-700 rounded-lg p-4 flex flex-col gap-2 relative">
<div className="absolute -left-3 top-1/2 w-3 h-px bg-slate-600"></div>
<div className="flex justify-between items-center bg-slate-800/50 p-2 rounded border border-slate-700/50">
<span className={match.p1 === 'BYE' ? 'text-slate-500 italic' : 'font-bold text-white'}>{match.p1}</span>
</div>
<div className="text-xs text-center text-slate-500">VS</div>
<div className="flex justify-between items-center bg-slate-800/50 p-2 rounded border border-slate-700/50">
<span className={match.p2 === 'BYE' ? 'text-slate-500 italic' : 'font-bold text-white'}>{match.p2}</span>
</div>
</div>
))}
<div className="flex gap-8 overflow-x-auto pb-8 snap-x">
{rounds.map((roundMatches, roundIndex) => (
<div key={roundIndex} className="flex flex-col justify-center gap-16 min-w-[280px] snap-center">
<h3 className="text-center font-bold text-slate-500 uppercase tracking-widest text-sm mb-4">
{roundIndex === rounds.length - 1 ? "Finals" : `Round ${roundIndex + 1}`}
</h3>
<div className="flex flex-col gap-8 justify-center flex-1">
{roundMatches.map((match) => {
const isMyMatch = (match.player1?.id === currentPlayerId || match.player2?.id === currentPlayerId);
const isPlayable = isMyMatch && match.status === 'ready' && !match.winnerId;
return (
<div key={match.id} className={`bg-slate-900 border ${isMyMatch ? 'border-blue-500 ring-1 ring-blue-500/50' : 'border-slate-700'} rounded-lg p-0 overflow-hidden relative shadow-lg`}>
{/* Status Indicator */}
{match.status === 'in_progress' && <div className="absolute top-0 right-0 bg-green-500 text-xs text-black font-bold px-2 py-0.5">LIVE</div>}
<div className={`p-3 border-b border-slate-800 flex justify-between items-center ${match.winnerId === match.player1?.id ? 'bg-emerald-900/30' : ''}`}>
<span className={match.player1 ? 'font-bold' : 'text-slate-600 italic'}>
{match.player1 ? match.player1.name : 'Waiting...'}
</span>
{match.winnerId === match.player1?.id && <Trophy className="w-4 h-4 text-emerald-500" />}
</div>
<div className={`p-3 flex justify-between items-center ${match.winnerId === match.player2?.id ? 'bg-emerald-900/30' : ''}`}>
<span className={match.player2 ? 'font-bold' : 'text-slate-600 italic'}>
{match.player2 ? match.player2.name : 'Waiting...'}
</span>
{match.winnerId === match.player2?.id && <Trophy className="w-4 h-4 text-emerald-500" />}
</div>
{isPlayable && (
<button
onClick={() => handleJoinMatch(match.id)}
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 flex items-center justify-center gap-2 transition-colors"
>
<Play className="w-4 h-4" /> Play Match
</button>
)}
</div>
);
})}
</div>
</div>
</div>
)}
))}
</div>
</div>
);
};

View File

@@ -11,6 +11,13 @@ class SocketService {
this.socket = io(URL, {
autoConnect: false
});
// Debug Wrapper
const originalEmit = this.socket.emit;
this.socket.emit = (event: string, ...args: any[]) => {
console.log(`[Socket] 📤 Emitting: ${event}`, args);
return originalEmit.apply(this.socket, [event, ...args]);
};
}
connect() {