This commit is contained in:
@@ -796,6 +796,21 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
</div>
|
||||
</DroppableZone>
|
||||
|
||||
{/* New Phase Control Bar - Between Battlefield and Hand */}
|
||||
<div className="w-full z-30 bg-black border-y border-white/10 flex justify-center shrink-0 relative shadow-2xl">
|
||||
<PhaseStrip
|
||||
gameState={gameState}
|
||||
currentPlayerId={currentPlayerId}
|
||||
onAction={(type: string, payload: any) => 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)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom Area: Controls & Hand */}
|
||||
<div className="h-64 relative z-20 flex bg-gradient-to-t from-black to-slate-900/80 backdrop-blur-md shadow-[0_-5px_20px_rgba(0,0,0,0.5)]">
|
||||
|
||||
@@ -850,21 +865,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
<div className="flex-1 relative flex flex-col items-center justify-end px-4 pb-2">
|
||||
<DroppableZone id="hand" data={{ type: 'zone' }} className="flex-1 w-full h-full flex flex-col justify-end">
|
||||
|
||||
{/* Smart Button / Action Strip Floating above Hand */}
|
||||
<div className="mb-4 z-40 self-center flex flex-col items-center gap-4 w-full">
|
||||
{/* Phase Strip Central Integration (Now acts as Smart Button) */}
|
||||
<PhaseStrip
|
||||
gameState={gameState}
|
||||
currentPlayerId={currentPlayerId}
|
||||
onAction={(type, payload) => 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)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="flex justify-center -space-x-12 w-full h-full items-end pb-4 perspective-500">
|
||||
|
||||
@@ -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<PhaseStripProps> = ({
|
||||
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<PhaseStripProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col items-center gap-2 w-full max-w-[420px] mx-auto pointer-events-auto transition-all duration-300">
|
||||
<div className="w-full h-full flex flex-col items-center gap-2 pointer-events-auto">
|
||||
|
||||
{/* Main Action Bar */}
|
||||
<div
|
||||
onClick={handleAction}
|
||||
className={`
|
||||
relative w-full h-11 rounded px-1 overflow-hidden transition-all duration-300
|
||||
flex items-center justify-between
|
||||
border backdrop-blur-md
|
||||
${baseStyle}
|
||||
${hoverStyle}
|
||||
${glowStyle}
|
||||
${!hasPriority && !isYielding ? 'grayscale-[0.5] opacity-90' : 'scale-[1.02] active:scale-[0.98]'}
|
||||
`}
|
||||
>
|
||||
{/* Progress Line (Top) */}
|
||||
{!hasPriority && (
|
||||
<div className="absolute top-0 left-0 w-full h-[1px] bg-white/5" />
|
||||
)}
|
||||
<div
|
||||
className={`absolute top-0 left-0 h-[2px] transition-all duration-700 ease-out z-0 ${hasPriority ? 'bg-emerald-500/50 shadow-[0_0_10px_rgba(16,185,129,0.8)]' : 'bg-white/10'}`}
|
||||
style={{ width: `${((activePhaseIndex + 1) / phases.length) * 100}%` }}
|
||||
/>
|
||||
{/* HUD Container */}
|
||||
<div className={`
|
||||
relative w-full h-10 bg-transparent rounded-none
|
||||
flex items-center justify-between px-4 shadow-none transition-all duration-300
|
||||
border-b-2
|
||||
${themeBorder}
|
||||
${themeShadow}
|
||||
`}>
|
||||
|
||||
{/* Left: Phase Indicator */}
|
||||
<div className="flex items-center gap-3 z-10 pl-3 h-full border-r border-white/5 pr-3 bg-black/10">
|
||||
<div className={`p-1 rounded-sm ${isMyTurn ? 'text-slate-200' : 'text-slate-600'}`}>
|
||||
{(() => {
|
||||
const PhaseIcon = phases.find(p => p.id === currentPhase)?.icon || Sun;
|
||||
return <PhaseIcon size={14} strokeWidth={2.5} />;
|
||||
})()}
|
||||
{/* SECTION 1: Phase Timeline (Left) */}
|
||||
<div className={`flex items-center gap-0.5 px-2 border-r border-white/5 h-full overflow-x-auto no-scrollbar`}>
|
||||
{stepsList.map((s, idx) => {
|
||||
const isActive = idx === safeActiveIndex;
|
||||
const isPast = idx < safeActiveIndex;
|
||||
const Icon = s.icon;
|
||||
|
||||
return (
|
||||
<div key={s.id} className="relative group flex items-center justify-center min-w-[20px]">
|
||||
{/* Connector Line - simplified to just spacing/coloring */}
|
||||
{/*
|
||||
{idx > 0 && (
|
||||
<div className={`w-1 h-0.5 mx-px rounded-full ${isPast || isActive ? (isMyTurn ? 'bg-emerald-800' : 'bg-red-900') : 'bg-slate-800'}`} />
|
||||
)}
|
||||
*/}
|
||||
|
||||
{/* Icon Node */}
|
||||
<div
|
||||
className={`
|
||||
rounded flex items-center justify-center transition-all duration-300
|
||||
${isActive
|
||||
? `w-6 h-6 ${themeBgActive} text-white shadow-lg z-10 scale-110 rounded-md`
|
||||
: `w-5 h-5 ${isPast ? (isMyTurn ? 'text-emerald-800' : 'text-red-900') : 'text-slate-800'} text-opacity-80`}
|
||||
`}
|
||||
title={s.label}
|
||||
>
|
||||
<Icon size={isActive ? 14 : 12} strokeWidth={isActive ? 2.5 : 2} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* SECTION 2: Info Panel (Center/Fill) */}
|
||||
<div className="flex-1 flex items-center justify-center gap-4 px-4 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{hasPriority && (
|
||||
<span className="flex h-1.5 w-1.5 relative">
|
||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full ${themePing} opacity-75`}></span>
|
||||
<span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${themePingSolid}`}></span>
|
||||
</span>
|
||||
)}
|
||||
<span className={`text-[10px] font-bold uppercase tracking-wider ${themeText}`}>
|
||||
{isMyTurn ? 'Your Turn' : "Opponent"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-white/10" />
|
||||
<div className="text-sm font-medium text-slate-200 truncate capitalize tracking-tight">
|
||||
{currentStep.replace(/_/g, ' ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Action Text */}
|
||||
<div className="flex flex-col items-center justify-center z-10 flex-1 px-2">
|
||||
<span className="text-xs font-black uppercase tracking-[0.2em] drop-shadow-sm whitespace-nowrap">
|
||||
{actionLabel}
|
||||
</span>
|
||||
{/* Detailed Step Subtext */}
|
||||
{hasPriority && (
|
||||
<span className="text-[9px] uppercase tracking-wider opacity-60 font-medium">
|
||||
{currentStep.replace(/_/g, ' ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* SECTION 3: Action Button (Right) */}
|
||||
<button
|
||||
onClick={handleAction}
|
||||
disabled={!isActionEnabled}
|
||||
className={`
|
||||
h-8 px-4 rounded flex items-center gap-2 transition-all duration-200
|
||||
font-bold text-xs uppercase tracking-wide text-white
|
||||
${actionColor}
|
||||
${isActionEnabled ? 'hover:brightness-110' : 'opacity-50 grayscale'}
|
||||
`}
|
||||
>
|
||||
<span>{actionLabel}</span>
|
||||
<ActionIcon size={14} />
|
||||
</button>
|
||||
|
||||
{/* Right: Interaction Icon */}
|
||||
<div className="flex items-center gap-2 z-10 pr-4 pl-3 border-l border-white/5 h-full bg-black/10">
|
||||
{(() => {
|
||||
const ActionIcon = actionIcon;
|
||||
return <ActionIcon size={16} className={hasPriority ? "text-emerald-100 drop-shadow-[0_0_5px_rgba(255,255,255,0.5)]" : "opacity-40"} />;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Minimal Phase Dots */}
|
||||
<div className="flex gap-1.5 opacity-30 hover:opacity-80 transition-opacity pb-1">
|
||||
{phases.map((p, idx) => {
|
||||
const isActive = idx === activePhaseIndex;
|
||||
const isPast = idx < activePhaseIndex;
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`
|
||||
h-1 rounded-full transition-all duration-300
|
||||
${isActive ? 'w-6 bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.6)]' : 'w-1 bg-slate-400'}
|
||||
${isPast ? 'bg-slate-600' : ''}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user