diff --git a/src/client/src/modules/game/GameView.tsx b/src/client/src/modules/game/GameView.tsx index 048b180..1650bf0 100644 --- a/src/client/src/modules/game/GameView.tsx +++ b/src/client/src/modules/game/GameView.tsx @@ -796,6 +796,21 @@ export const GameView: React.FC = ({ gameState, currentPlayerId } + {/* New Phase Control Bar - Between Battlefield and Hand */} +
+ socketService.socket.emit(type, { action: payload })} + contextData={{ + attackers: Array.from(proposedAttackers).map(id => ({ attackerId: id, targetId: opponentId })), + blockers: Array.from(proposedBlockers.entries()).map(([blockerId, attackerId]) => ({ blockerId, attackerId })) + }} + isYielding={isYielding} + onYieldToggle={() => setIsYielding(!isYielding)} + /> +
+ {/* Bottom Area: Controls & Hand */}
@@ -850,21 +865,7 @@ export const GameView: React.FC = ({ gameState, currentPlayerId }
- {/* Smart Button / Action Strip Floating above Hand */} -
- {/* Phase Strip Central Integration (Now acts as Smart Button) */} - socketService.socket.emit(type, { action: payload })} - contextData={{ - attackers: Array.from(proposedAttackers).map(id => ({ attackerId: id, targetId: opponentId })), - blockers: Array.from(proposedBlockers.entries()).map(([blockerId, attackerId]) => ({ blockerId, attackerId })) - }} - isYielding={isYielding} - onYieldToggle={() => setIsYielding(!isYielding)} - /> -
+
diff --git a/src/client/src/modules/game/PhaseStrip.tsx b/src/client/src/modules/game/PhaseStrip.tsx index 70a6b22..9b82e9a 100644 --- a/src/client/src/modules/game/PhaseStrip.tsx +++ b/src/client/src/modules/game/PhaseStrip.tsx @@ -1,12 +1,12 @@ import React, { useMemo } from 'react'; import { GameState, Phase, Step } from '../../types/game'; -import { Sun, Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Skull } from 'lucide-react'; +import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Play, RotateCcw, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react'; interface PhaseStripProps { gameState: GameState; currentPlayerId: string; onAction: (type: string, payload?: any) => void; - contextData?: any; // For attackers/blockers context + contextData?: any; isYielding?: boolean; onYieldToggle?: () => void; } @@ -25,86 +25,71 @@ export const PhaseStrip: React.FC = ({ const hasPriority = gameState.priorityPlayerId === currentPlayerId; const isStackEmpty = !gameState.stack || gameState.stack.length === 0; - // --- Action Logic --- + // --- 1. Action Logic resolution --- let actionLabel = "Wait"; - // Base style: Glassmorphism dark - let baseStyle = "bg-slate-900/60 border-slate-700/50 text-slate-400"; - let hoverStyle = ""; - let glowStyle = ""; + let actionColor = "bg-slate-700"; let actionType: string | null = null; - let actionIcon = Hourglass; + let ActionIcon = Hourglass; + let isActionEnabled = false; if (isYielding) { - actionLabel = "Yielding (Cancel)"; - baseStyle = "bg-sky-900/40 border-sky-500/30 text-sky-200"; - hoverStyle = "hover:bg-sky-900/60 hover:border-sky-400/50"; - glowStyle = "shadow-[0_0_20px_rgba(14,165,233,0.15)]"; + actionLabel = "Cancel Yield"; + actionColor = "bg-sky-600 hover:bg-sky-500"; actionType = 'CANCEL_YIELD'; - actionIcon = XCircle; + ActionIcon = XCircle; + isActionEnabled = true; } else if (hasPriority) { - // Interactive State: Subtle gradients, refined look - baseStyle = "cursor-pointer bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 border-emerald-500/40 text-emerald-100"; - hoverStyle = "hover:border-emerald-400/80 hover:shadow-[0_0_15px_rgba(16,185,129,0.2)]"; - actionIcon = Zap; + isActionEnabled = true; + ActionIcon = ChevronRight; + // Default Pass styling + actionColor = "bg-emerald-600 hover:bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.4)]"; if (currentStep === 'declare_attackers') { if (gameState.attackersDeclared) { - actionLabel = "Confirm Attacks"; + actionLabel = "Confirm (Blockers)"; actionType = 'PASS_PRIORITY'; - actionIcon = Swords; - baseStyle = "cursor-pointer bg-gradient-to-r from-orange-950/40 via-orange-900/40 to-orange-950/40 border-orange-500/50 text-orange-100"; - hoverStyle = "hover:border-orange-400 hover:shadow-[0_0_15px_rgba(249,115,22,0.2)]"; } else { const count = contextData?.attackers?.length || 0; if (count > 0) { - actionLabel = `Attack with ${count}`; + actionLabel = `Attack (${count})`; actionType = 'DECLARE_ATTACKERS'; - actionIcon = Swords; - baseStyle = "cursor-pointer bg-gradient-to-r from-red-950/60 via-red-900/60 to-red-950/60 border-red-500/50 text-red-100"; - hoverStyle = "hover:border-red-400 hover:shadow-[0_0_15px_rgba(239,68,68,0.25)]"; + ActionIcon = Swords; + actionColor = "bg-red-600 hover:bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.4)]"; } else { actionLabel = "Skip Combat"; actionType = 'DECLARE_ATTACKERS'; - actionIcon = ChevronRight; - // Neutral/Skip style - baseStyle = "cursor-pointer bg-slate-900/80 border-slate-600/50 text-slate-300"; - hoverStyle = "hover:border-slate-500 hover:bg-slate-800"; + actionColor = "bg-slate-600 hover:bg-slate-500"; } } } else if (currentStep === 'declare_blockers') { - actionLabel = "Declare Blockers"; + actionLabel = "Confirm Blocks"; actionType = 'DECLARE_BLOCKERS'; - actionIcon = Shield; - baseStyle = "cursor-pointer bg-gradient-to-r from-blue-950/60 via-blue-900/60 to-blue-950/60 border-blue-500/50 text-blue-100"; - hoverStyle = "hover:border-blue-400 hover:shadow-[0_0_15px_rgba(59,130,246,0.2)]"; + ActionIcon = Shield; + actionColor = "bg-blue-600 hover:bg-blue-500 shadow-[0_0_10px_rgba(37,99,235,0.4)]"; } else if (isStackEmpty) { // Standard Pass actionType = 'PASS_PRIORITY'; - actionIcon = ChevronRight; if (gameState.phase === 'main1') actionLabel = "To Combat"; else if (gameState.phase === 'main2') actionLabel = "End Turn"; - else actionLabel = "Pass Turn"; - - // Use a very sleek neutral/emerald gradient for standard progression - baseStyle = "cursor-pointer bg-gradient-to-b from-slate-800 to-slate-900 border-white/10 text-slate-200"; - hoverStyle = "hover:border-white/30 hover:bg-slate-800"; + else actionLabel = "Pass"; } else { - // Resolve Check + // Resolve const topItem = gameState.stack![gameState.stack!.length - 1]; - actionLabel = `Resolve ${topItem?.name || ''}`; + actionLabel = "Resolve"; actionType = 'PASS_PRIORITY'; - actionIcon = Zap; - baseStyle = "cursor-pointer bg-amber-950/40 border-amber-500/40 text-amber-100"; - hoverStyle = "hover:border-amber-400/80 hover:shadow-[0_0_15px_rgba(245,158,11,0.2)]"; + ActionIcon = Zap; + actionColor = "bg-amber-600 hover:bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.4)]"; } } else { - // Waiting State - actionLabel = isMyTurn ? "Opponent Acting" : "Opponent's Turn"; - actionIcon = Hand; - baseStyle = "bg-black/40 border-white/5 text-slate-500"; + // Waiting + actionLabel = "Waiting..."; + ActionIcon = Hand; + actionColor = "bg-white/5 text-slate-500 cursor-not-allowed"; + isActionEnabled = false; } - const handleAction = () => { + const handleAction = (e: React.MouseEvent) => { + e.stopPropagation(); if (isYielding) { onYieldToggle?.(); return; @@ -120,92 +105,130 @@ export const PhaseStrip: React.FC = ({ } }; - // Phase Definitions - const phases: { id: Phase; icon: React.ElementType; label: string }[] = useMemo(() => [ - { id: 'beginning', icon: Sun, label: 'Beginning' }, - { id: 'main1', icon: Shield, label: 'Main 1' }, - { id: 'combat', icon: Swords, label: 'Combat' }, - { id: 'main2', icon: Shield, label: 'Main 2' }, - { id: 'ending', icon: Hourglass, label: 'End' }, + // --- 2. Phase/Step Definitions --- + interface VisualStep { + id: string; + label: string; + icon: React.ElementType; + phase: Phase; + step: Step; + } + + const stepsList: VisualStep[] = useMemo(() => [ + { id: 'untap', label: 'Untap', icon: RotateCcw, phase: 'beginning', step: 'untap' }, + { id: 'upkeep', label: 'Upkeep', icon: Clock, phase: 'beginning', step: 'upkeep' }, + { id: 'draw', label: 'Draw', icon: Files, phase: 'beginning', step: 'draw' }, + { id: 'main1', label: 'Main 1', icon: Zap, phase: 'main1', step: 'main' }, + { id: 'begin_combat', label: 'Combat Start', icon: Swords, phase: 'combat', step: 'beginning_combat' }, + { id: 'attackers', label: 'Attack', icon: Crosshair, phase: 'combat', step: 'declare_attackers' }, + { id: 'blockers', label: 'Block', icon: Shield, phase: 'combat', step: 'declare_blockers' }, + { id: 'damage', label: 'Damage', icon: Skull, phase: 'combat', step: 'combat_damage' }, + { id: 'end_combat', label: 'End Combat', icon: Flag, phase: 'combat', step: 'end_combat' }, + { id: 'main2', label: 'Main 2', icon: Zap, phase: 'main2', step: 'main' }, + { id: 'end', label: 'End Step', icon: Moon, phase: 'ending', step: 'end' }, + { id: 'cleanup', label: 'Cleanup', icon: Trash2, phase: 'ending', step: 'cleanup' }, ], []); - const activePhaseIndex = phases.findIndex(p => p.id === currentPhase); + // Calculate Active Step Index + // We need to match both Phase and Step because 'main' step exists in two phases + const activeStepIndex = stepsList.findIndex(s => { + if (s.phase === 'main1' || s.phase === 'main2') { + return s.phase === currentPhase && s.step === 'main'; // Special handle for split main phases + } + return s.step === currentStep; + }); + + // Fallback if step mismatch + const safeActiveIndex = activeStepIndex === -1 ? 0 : activeStepIndex; + + + const themeBorder = isMyTurn ? 'border-emerald-500/30' : 'border-red-500/30'; + const themeShadow = isMyTurn ? 'shadow-[0_0_20px_-5px_rgba(16,185,129,0.3)]' : 'shadow-[0_0_20px_-5px_rgba(239,68,68,0.3)]'; + const themeText = isMyTurn ? 'text-emerald-400' : 'text-red-400'; + const themeBgActive = isMyTurn ? 'bg-emerald-500' : 'bg-red-500'; + const themePing = isMyTurn ? 'bg-emerald-400' : 'bg-red-400'; + const themePingSolid = isMyTurn ? 'bg-emerald-500' : 'bg-red-500'; return ( -
+
- {/* Main Action Bar */} -
- {/* Progress Line (Top) */} - {!hasPriority && ( -
- )} -
+ {/* HUD Container */} +
- {/* Left: Phase Indicator */} -
-
- {(() => { - const PhaseIcon = phases.find(p => p.id === currentPhase)?.icon || Sun; - return ; - })()} + {/* SECTION 1: Phase Timeline (Left) */} +
+ {stepsList.map((s, idx) => { + const isActive = idx === safeActiveIndex; + const isPast = idx < safeActiveIndex; + const Icon = s.icon; + + return ( +
+ {/* Connector Line - simplified to just spacing/coloring */} + {/* + {idx > 0 && ( +
+ )} + */} + + {/* Icon Node */} +
+ +
+
+ ); + })} +
+ + {/* SECTION 2: Info Panel (Center/Fill) */} +
+
+ {hasPriority && ( + + + + + )} + + {isMyTurn ? 'Your Turn' : "Opponent"} + +
+
+
+ {currentStep.replace(/_/g, ' ')}
- {/* Center: Action Text */} -
- - {actionLabel} - - {/* Detailed Step Subtext */} - {hasPriority && ( - - {currentStep.replace(/_/g, ' ')} - - )} -
+ {/* SECTION 3: Action Button (Right) */} + - {/* Right: Interaction Icon */} -
- {(() => { - const ActionIcon = actionIcon; - return ; - })()} -
- - {/* Minimal Phase Dots */} -
- {phases.map((p, idx) => { - const isActive = idx === activePhaseIndex; - const isPast = idx < activePhaseIndex; - return ( -
- ); - })} -
-
); };