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>
</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">

View File

@@ -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 */}
{/* 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}
`}>
{/* 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
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]'}
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}
>
{/* 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 */}
<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} />;
})()}
<Icon size={isActive ? 14 : 12} strokeWidth={isActive ? 2.5 : 2} />
</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>
{/* 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>
{/* 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>
{/* 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>
</div>
</div>
);
};