implemented game server sync
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import { ChevronLeft, Eye } from 'lucide-react';
|
||||
import { GameState, CardInstance } from '../../types/game';
|
||||
import { socketService } from '../../services/SocketService';
|
||||
import { CardComponent } from './CardComponent';
|
||||
@@ -12,10 +13,79 @@ interface GameViewProps {
|
||||
|
||||
export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }) => {
|
||||
const battlefieldRef = useRef<HTMLDivElement>(null);
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuRequest | null>(null);
|
||||
const [viewingZone, setViewingZone] = useState<string | null>(null);
|
||||
const [hoveredCard, setHoveredCard] = useState<CardInstance | null>(null);
|
||||
|
||||
// --- Sidebar State ---
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
||||
return localStorage.getItem('game_sidebarCollapsed') === 'true';
|
||||
});
|
||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||
const saved = localStorage.getItem('game_sidebarWidth');
|
||||
return saved ? parseInt(saved, 10) : 320;
|
||||
});
|
||||
|
||||
const resizingState = useRef<{
|
||||
startX: number,
|
||||
startWidth: number,
|
||||
active: boolean
|
||||
}>({ startX: 0, startWidth: 0, active: false });
|
||||
|
||||
// --- Persistence ---
|
||||
useEffect(() => {
|
||||
localStorage.setItem('game_sidebarCollapsed', isSidebarCollapsed.toString());
|
||||
}, [isSidebarCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('game_sidebarWidth', sidebarWidth.toString());
|
||||
}, [sidebarWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sidebarRef.current) sidebarRef.current.style.width = `${sidebarWidth}px`;
|
||||
}, []);
|
||||
|
||||
// --- Resize Handlers ---
|
||||
const handleResizeStart = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
if (e.cancelable) e.preventDefault();
|
||||
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
||||
|
||||
resizingState.current = {
|
||||
startX: clientX,
|
||||
startWidth: sidebarRef.current?.getBoundingClientRect().width || 320,
|
||||
active: true
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onResizeMove);
|
||||
document.addEventListener('touchmove', onResizeMove, { passive: false });
|
||||
document.addEventListener('mouseup', onResizeEnd);
|
||||
document.addEventListener('touchend', onResizeEnd);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
};
|
||||
|
||||
const onResizeMove = useCallback((e: MouseEvent | TouchEvent) => {
|
||||
if (!resizingState.current.active || !sidebarRef.current) return;
|
||||
if (e.cancelable) e.preventDefault();
|
||||
|
||||
const clientX = (e as TouchEvent).touches ? (e as TouchEvent).touches[0].clientX : (e as MouseEvent).clientX;
|
||||
const delta = clientX - resizingState.current.startX;
|
||||
const newWidth = Math.max(200, Math.min(600, resizingState.current.startWidth + delta));
|
||||
sidebarRef.current.style.width = `${newWidth}px`;
|
||||
}, []);
|
||||
|
||||
const onResizeEnd = useCallback(() => {
|
||||
if (resizingState.current.active && sidebarRef.current) {
|
||||
setSidebarWidth(parseInt(sidebarRef.current.style.width));
|
||||
}
|
||||
resizingState.current.active = false;
|
||||
document.removeEventListener('mousemove', onResizeMove);
|
||||
document.removeEventListener('touchmove', onResizeMove);
|
||||
document.removeEventListener('mouseup', onResizeEnd);
|
||||
document.removeEventListener('touchend', onResizeEnd);
|
||||
document.body.style.cursor = 'default';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Disable default context menu
|
||||
const handleContext = (e: MouseEvent) => e.preventDefault();
|
||||
@@ -152,52 +222,111 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
)}
|
||||
|
||||
{/* Zoom Sidebar */}
|
||||
<div className="hidden xl:flex w-80 shrink-0 flex-col items-center justify-start pt-8 border-r border-slate-800 bg-slate-950/50 z-30 p-4 relative shadow-2xl">
|
||||
{hoveredCard ? (
|
||||
<div className="animate-in fade-in slide-in-from-left-4 duration-200 sticky top-4 w-full h-[calc(100vh-2rem)] overflow-y-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
|
||||
<img
|
||||
src={hoveredCard.imageUrl}
|
||||
alt={hoveredCard.name}
|
||||
className="w-full rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
|
||||
/>
|
||||
<div className="mt-4 text-center pb-4">
|
||||
<h3 className="text-lg font-bold text-slate-200 leading-tight">{hoveredCard.name}</h3>
|
||||
{isSidebarCollapsed ? (
|
||||
<div key="collapsed" className="hidden xl:flex shrink-0 w-12 flex-col items-center py-4 bg-slate-900 border-r border-slate-800 z-30 gap-4 transition-all duration-300">
|
||||
<button
|
||||
onClick={() => setIsSidebarCollapsed(false)}
|
||||
className="p-3 rounded-xl transition-all duration-200 group relative text-slate-500 hover:text-purple-400 hover:bg-slate-800"
|
||||
title="Expand Preview"
|
||||
>
|
||||
<Eye className="w-6 h-6" />
|
||||
<span className="absolute left-full ml-3 top-1/2 -translate-y-1/2 bg-slate-800 text-white text-xs font-bold px-2 py-1 rounded shadow-xl opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none ring-1 ring-white/10 z-50">
|
||||
Card Preview
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
key="expanded"
|
||||
ref={sidebarRef}
|
||||
className="hidden xl:flex shrink-0 flex-col items-center justify-start pt-4 border-r border-slate-800 bg-slate-900 z-30 p-4 relative group/sidebar shadow-2xl"
|
||||
style={{ width: sidebarWidth }}
|
||||
>
|
||||
{/* Collapse Button */}
|
||||
<button
|
||||
onClick={() => setIsSidebarCollapsed(true)}
|
||||
className="absolute top-2 right-2 p-1.5 bg-slate-800/80 hover:bg-slate-700 text-slate-400 hover:text-white rounded-lg transition-colors z-20 opacity-0 group-hover/sidebar:opacity-100"
|
||||
title="Collapse Preview"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{hoveredCard.manaCost && (
|
||||
<p className="text-sm text-slate-400 mt-1 font-mono tracking-widest">{hoveredCard.manaCost}</p>
|
||||
)}
|
||||
|
||||
{hoveredCard.typeLine && (
|
||||
<div className="text-xs text-emerald-400 uppercase tracking-wider font-bold mt-2 border-b border-white/10 pb-2 mb-3">
|
||||
{hoveredCard.typeLine}
|
||||
<div className="w-full relative sticky top-4 flex flex-col h-full overflow-hidden">
|
||||
<div className="relative w-full aspect-[2.5/3.5] transition-all duration-300 ease-in-out shrink-0">
|
||||
<div
|
||||
className="relative w-full h-full"
|
||||
style={{
|
||||
transformStyle: 'preserve-3d',
|
||||
transform: hoveredCard ? 'rotateY(0deg)' : 'rotateY(180deg)',
|
||||
transition: 'transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1)'
|
||||
}}
|
||||
>
|
||||
{/* Front Face (Hovered Card) */}
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full bg-slate-900 rounded-xl"
|
||||
style={{ backfaceVisibility: 'hidden' }}
|
||||
>
|
||||
{hoveredCard && (
|
||||
<img
|
||||
src={hoveredCard.imageUrl}
|
||||
alt={hoveredCard.name}
|
||||
className="w-full h-full object-cover rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hoveredCard.oracleText && (
|
||||
<div className="text-sm text-slate-300 text-left bg-slate-900/50 p-3 rounded-lg border border-slate-800 whitespace-pre-wrap leading-relaxed">
|
||||
{hoveredCard.oracleText}
|
||||
{/* Back Face (Card Back) */}
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full rounded-xl shadow-2xl overflow-hidden bg-slate-900"
|
||||
style={{
|
||||
backfaceVisibility: 'hidden',
|
||||
transform: 'rotateY(180deg)'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/back.jpg"
|
||||
alt="Card Back"
|
||||
className="w-full h-full object-cover"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats for Creatures */}
|
||||
{hoveredCard.typeLine?.toLowerCase().includes('creature') && (
|
||||
<div className="mt-3 bg-slate-800/80 rounded px-2 py-1 inline-block border border-slate-600 font-bold text-lg">
|
||||
{/* Accessing raw PT might be hard if we don't have base PT, but we do have ptModification */}
|
||||
{/* We don't strictly have base PT in CardInstance yet. Assuming UI mainly uses image. */}
|
||||
{/* We'll skip P/T text for now as it needs base P/T to be passed from server. */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Oracle Text & Details - Only when card is hovered */}
|
||||
{hoveredCard && (
|
||||
<div className="mt-4 flex-1 overflow-y-auto px-1 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
|
||||
<h3 className="text-lg font-bold text-slate-200 leading-tight">{hoveredCard.name}</h3>
|
||||
|
||||
{hoveredCard.manaCost && (
|
||||
<p className="text-sm text-slate-400 mt-1 font-mono tracking-widest">{hoveredCard.manaCost}</p>
|
||||
)}
|
||||
|
||||
{hoveredCard.typeLine && (
|
||||
<div className="text-xs text-emerald-400 uppercase tracking-wider font-bold mt-2 border-b border-white/10 pb-2 mb-3">
|
||||
{hoveredCard.typeLine}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hoveredCard.oracleText && (
|
||||
<div className="text-sm text-slate-300 text-left bg-slate-900/50 p-3 rounded-lg border border-slate-800 whitespace-pre-wrap leading-relaxed shadow-inner">
|
||||
{hoveredCard.oracleText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-600 text-center opacity-50">
|
||||
<div className="w-48 h-64 border-2 border-dashed border-slate-700 rounded-xl mb-4 flex items-center justify-center">
|
||||
<span className="text-xs uppercase font-bold tracking-widest">Hover Card</span>
|
||||
</div>
|
||||
<p className="text-sm">Hover over a card to view clear details.</p>
|
||||
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
className="absolute right-0 top-0 bottom-0 w-1 bg-transparent hover:bg-emerald-500/50 cursor-col-resize z-50 flex flex-col justify-center items-center group transition-colors touch-none"
|
||||
onMouseDown={handleResizeStart}
|
||||
onTouchStart={handleResizeStart}
|
||||
>
|
||||
<div className="h-8 w-1 bg-slate-700/50 rounded-full group-hover:bg-emerald-400 transition-colors" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Game Area */}
|
||||
<div className="flex-1 flex flex-col h-full relative">
|
||||
|
||||
@@ -18,6 +18,7 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks, avai
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialDraftState, setInitialDraftState] = useState<any>(null);
|
||||
const [initialGameState, setInitialGameState] = useState<any>(null);
|
||||
|
||||
const [playerId] = useState(() => {
|
||||
const saved = localStorage.getItem('player_id');
|
||||
@@ -195,6 +196,7 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks, avai
|
||||
|
||||
if (response.success) {
|
||||
setInitialDraftState(response.draftState || null);
|
||||
setInitialGameState(response.gameState || null);
|
||||
setActiveRoom(response.room);
|
||||
} else {
|
||||
setError(response.message || 'Failed to join room');
|
||||
@@ -227,6 +229,9 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks, avai
|
||||
if (response.draftState) {
|
||||
setInitialDraftState(response.draftState);
|
||||
}
|
||||
if (response.gameState) {
|
||||
setInitialGameState(response.gameState);
|
||||
}
|
||||
} else {
|
||||
console.warn("Rejoin failed by server: ", response.message);
|
||||
localStorage.removeItem('active_room_id');
|
||||
@@ -261,11 +266,12 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks, avai
|
||||
}
|
||||
setActiveRoom(null);
|
||||
setInitialDraftState(null);
|
||||
setInitialGameState(null);
|
||||
localStorage.removeItem('active_room_id');
|
||||
};
|
||||
|
||||
if (activeRoom) {
|
||||
return <GameRoom room={activeRoom} currentPlayerId={playerId} onExit={handleExitRoom} initialDraftState={initialDraftState} />;
|
||||
return <GameRoom room={activeRoom} currentPlayerId={playerId} onExit={handleExitRoom} initialDraftState={initialDraftState} initialGameState={initialGameState} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user