fixed turn bar
Some checks failed
Build and Deploy / build (push) Failing after 2m11s

This commit is contained in:
2025-12-22 17:49:58 +01:00
parent 8a65169d2a
commit 5b601efcb6
2 changed files with 163 additions and 139 deletions

View File

@@ -796,6 +796,21 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
</div> </div>
</DroppableZone> </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 */} {/* 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)]"> <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"> <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"> <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"> <div className="flex justify-center -space-x-12 w-full h-full items-end pb-4 perspective-500">

View File

@@ -1,12 +1,12 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { GameState, Phase, Step } from '../../types/game'; 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 { interface PhaseStripProps {
gameState: GameState; gameState: GameState;
currentPlayerId: string; currentPlayerId: string;
onAction: (type: string, payload?: any) => void; onAction: (type: string, payload?: any) => void;
contextData?: any; // For attackers/blockers context contextData?: any;
isYielding?: boolean; isYielding?: boolean;
onYieldToggle?: () => void; onYieldToggle?: () => void;
} }
@@ -25,86 +25,71 @@ export const PhaseStrip: React.FC<PhaseStripProps> = ({
const hasPriority = gameState.priorityPlayerId === currentPlayerId; const hasPriority = gameState.priorityPlayerId === currentPlayerId;
const isStackEmpty = !gameState.stack || gameState.stack.length === 0; const isStackEmpty = !gameState.stack || gameState.stack.length === 0;
// --- Action Logic --- // --- 1. Action Logic resolution ---
let actionLabel = "Wait"; let actionLabel = "Wait";
// Base style: Glassmorphism dark let actionColor = "bg-slate-700";
let baseStyle = "bg-slate-900/60 border-slate-700/50 text-slate-400";
let hoverStyle = "";
let glowStyle = "";
let actionType: string | null = null; let actionType: string | null = null;
let actionIcon = Hourglass; let ActionIcon = Hourglass;
let isActionEnabled = false;
if (isYielding) { if (isYielding) {
actionLabel = "Yielding (Cancel)"; actionLabel = "Cancel Yield";
baseStyle = "bg-sky-900/40 border-sky-500/30 text-sky-200"; actionColor = "bg-sky-600 hover:bg-sky-500";
hoverStyle = "hover:bg-sky-900/60 hover:border-sky-400/50";
glowStyle = "shadow-[0_0_20px_rgba(14,165,233,0.15)]";
actionType = 'CANCEL_YIELD'; actionType = 'CANCEL_YIELD';
actionIcon = XCircle; ActionIcon = XCircle;
isActionEnabled = true;
} else if (hasPriority) { } else if (hasPriority) {
// Interactive State: Subtle gradients, refined look isActionEnabled = true;
baseStyle = "cursor-pointer bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 border-emerald-500/40 text-emerald-100"; ActionIcon = ChevronRight;
hoverStyle = "hover:border-emerald-400/80 hover:shadow-[0_0_15px_rgba(16,185,129,0.2)]"; // Default Pass styling
actionIcon = Zap; actionColor = "bg-emerald-600 hover:bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.4)]";
if (currentStep === 'declare_attackers') { if (currentStep === 'declare_attackers') {
if (gameState.attackersDeclared) { if (gameState.attackersDeclared) {
actionLabel = "Confirm Attacks"; actionLabel = "Confirm (Blockers)";
actionType = 'PASS_PRIORITY'; 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 { } else {
const count = contextData?.attackers?.length || 0; const count = contextData?.attackers?.length || 0;
if (count > 0) { if (count > 0) {
actionLabel = `Attack with ${count}`; actionLabel = `Attack (${count})`;
actionType = 'DECLARE_ATTACKERS'; actionType = 'DECLARE_ATTACKERS';
actionIcon = Swords; 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"; actionColor = "bg-red-600 hover:bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.4)]";
hoverStyle = "hover:border-red-400 hover:shadow-[0_0_15px_rgba(239,68,68,0.25)]";
} else { } else {
actionLabel = "Skip Combat"; actionLabel = "Skip Combat";
actionType = 'DECLARE_ATTACKERS'; actionType = 'DECLARE_ATTACKERS';
actionIcon = ChevronRight; actionColor = "bg-slate-600 hover:bg-slate-500";
// 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";
} }
} }
} else if (currentStep === 'declare_blockers') { } else if (currentStep === 'declare_blockers') {
actionLabel = "Declare Blockers"; actionLabel = "Confirm Blocks";
actionType = 'DECLARE_BLOCKERS'; actionType = 'DECLARE_BLOCKERS';
actionIcon = Shield; 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"; actionColor = "bg-blue-600 hover:bg-blue-500 shadow-[0_0_10px_rgba(37,99,235,0.4)]";
hoverStyle = "hover:border-blue-400 hover:shadow-[0_0_15px_rgba(59,130,246,0.2)]";
} else if (isStackEmpty) { } else if (isStackEmpty) {
// Standard Pass // Standard Pass
actionType = 'PASS_PRIORITY'; actionType = 'PASS_PRIORITY';
actionIcon = ChevronRight;
if (gameState.phase === 'main1') actionLabel = "To Combat"; if (gameState.phase === 'main1') actionLabel = "To Combat";
else if (gameState.phase === 'main2') actionLabel = "End Turn"; else if (gameState.phase === 'main2') actionLabel = "End Turn";
else actionLabel = "Pass Turn"; else actionLabel = "Pass";
// 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 { } else {
// Resolve Check // Resolve
const topItem = gameState.stack![gameState.stack!.length - 1]; const topItem = gameState.stack![gameState.stack!.length - 1];
actionLabel = `Resolve ${topItem?.name || ''}`; actionLabel = "Resolve";
actionType = 'PASS_PRIORITY'; actionType = 'PASS_PRIORITY';
actionIcon = Zap; ActionIcon = Zap;
baseStyle = "cursor-pointer bg-amber-950/40 border-amber-500/40 text-amber-100"; actionColor = "bg-amber-600 hover:bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.4)]";
hoverStyle = "hover:border-amber-400/80 hover:shadow-[0_0_15px_rgba(245,158,11,0.2)]";
} }
} else { } else {
// Waiting State // Waiting
actionLabel = isMyTurn ? "Opponent Acting" : "Opponent's Turn"; actionLabel = "Waiting...";
actionIcon = Hand; ActionIcon = Hand;
baseStyle = "bg-black/40 border-white/5 text-slate-500"; actionColor = "bg-white/5 text-slate-500 cursor-not-allowed";
isActionEnabled = false;
} }
const handleAction = () => { const handleAction = (e: React.MouseEvent) => {
e.stopPropagation();
if (isYielding) { if (isYielding) {
onYieldToggle?.(); onYieldToggle?.();
return; return;
@@ -120,92 +105,130 @@ export const PhaseStrip: React.FC<PhaseStripProps> = ({
} }
}; };
// Phase Definitions // --- 2. Phase/Step Definitions ---
const phases: { id: Phase; icon: React.ElementType; label: string }[] = useMemo(() => [ interface VisualStep {
{ id: 'beginning', icon: Sun, label: 'Beginning' }, id: string;
{ id: 'main1', icon: Shield, label: 'Main 1' }, label: string;
{ id: 'combat', icon: Swords, label: 'Combat' }, icon: React.ElementType;
{ id: 'main2', icon: Shield, label: 'Main 2' }, phase: Phase;
{ id: 'ending', icon: Hourglass, label: 'End' }, 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 ( 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 */} {/* HUD Container */}
<div <div className={`
onClick={handleAction} relative w-full h-10 bg-transparent rounded-none
className={` flex items-center justify-between px-4 shadow-none transition-all duration-300
relative w-full h-11 rounded px-1 overflow-hidden transition-all duration-300 border-b-2
flex items-center justify-between ${themeBorder}
border backdrop-blur-md ${themeShadow}
${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}%` }}
/>
{/* Left: Phase Indicator */} {/* SECTION 1: Phase Timeline (Left) */}
<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={`flex items-center gap-0.5 px-2 border-r border-white/5 h-full overflow-x-auto no-scrollbar`}>
<div className={`p-1 rounded-sm ${isMyTurn ? 'text-slate-200' : 'text-slate-600'}`}> {stepsList.map((s, idx) => {
{(() => { const isActive = idx === safeActiveIndex;
const PhaseIcon = phases.find(p => p.id === currentPhase)?.icon || Sun; const isPast = idx < safeActiveIndex;
return <PhaseIcon size={14} strokeWidth={2.5} />; 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>
</div> </div>
{/* Center: Action Text */} {/* SECTION 3: Action Button (Right) */}
<div className="flex flex-col items-center justify-center z-10 flex-1 px-2"> <button
<span className="text-xs font-black uppercase tracking-[0.2em] drop-shadow-sm whitespace-nowrap"> onClick={handleAction}
{actionLabel} disabled={!isActionEnabled}
</span> className={`
{/* Detailed Step Subtext */} h-8 px-4 rounded flex items-center gap-2 transition-all duration-200
{hasPriority && ( font-bold text-xs uppercase tracking-wide text-white
<span className="text-[9px] uppercase tracking-wider opacity-60 font-medium"> ${actionColor}
{currentStep.replace(/_/g, ' ')} ${isActionEnabled ? 'hover:brightness-110' : 'opacity-50 grayscale'}
</span> `}
)} >
</div> <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> </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> </div>
); );
}; };