changes the smart button

This commit is contained in:
2025-12-22 17:28:22 +01:00
parent f17ef711da
commit 8a65169d2a
4 changed files with 208 additions and 169 deletions

View File

@@ -9,8 +9,8 @@ import { CardComponent } from './CardComponent';
import { GameContextMenu, ContextMenuRequest } from './GameContextMenu';
import { ZoneOverlay } from './ZoneOverlay';
import { PhaseStrip } from './PhaseStrip';
import { SmartButton } from './SmartButton';
import { StackVisualizer } from './StackVisualizer';
import { GestureManager } from './GestureManager';
import { MulliganView } from './MulliganView';
import { RadialMenu, RadialOption } from './RadialMenu';
@@ -801,10 +801,8 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
{/* Left Controls: Library/Grave/Exile */}
<div className="w-40 p-2 flex flex-col gap-2 items-center justify-start pt-6 border-r border-white/10">
{/* Phase Strip Integration */}
<div className="mb-2 scale-75 origin-center">
<PhaseStrip gameState={gameState} />
</div>
{/* Phase Strip Moved to Bottom Center */}
<div className="flex gap-2">
<DroppableZone
@@ -852,11 +850,12 @@ 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 Floating above Hand */}
<div className="mb-4 z-40 self-center">
<SmartButton
{/* 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}
playerId={currentPlayerId}
currentPlayerId={currentPlayerId}
onAction={(type, payload) => socketService.socket.emit(type, { action: payload })}
contextData={{
attackers: Array.from(proposedAttackers).map(id => ({ attackerId: id, targetId: opponentId })),
@@ -867,6 +866,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
/>
</div>
<div className="flex justify-center -space-x-12 w-full h-full items-end pb-4 perspective-500">
{myHand.map((card, index) => (
<div

View File

@@ -1,50 +1,211 @@
import React from 'react';
import React, { useMemo } from 'react';
import { GameState, Phase, Step } from '../../types/game';
import { Sun, Shield, Swords, Hourglass } from 'lucide-react';
import { Sun, Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Skull } from 'lucide-react';
interface PhaseStripProps {
gameState: GameState;
currentPlayerId: string;
onAction: (type: string, payload?: any) => void;
contextData?: any; // For attackers/blockers context
isYielding?: boolean;
onYieldToggle?: () => void;
}
export const PhaseStrip: React.FC<PhaseStripProps> = ({ gameState }) => {
export const PhaseStrip: React.FC<PhaseStripProps> = ({
gameState,
currentPlayerId,
onAction,
contextData,
isYielding,
onYieldToggle
}) => {
const currentPhase = gameState.phase as Phase;
const currentStep = gameState.step as Step;
const isMyTurn = gameState.activePlayerId === currentPlayerId;
const hasPriority = gameState.priorityPlayerId === currentPlayerId;
const isStackEmpty = !gameState.stack || gameState.stack.length === 0;
// --- Action Logic ---
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 actionType: string | null = null;
let actionIcon = Hourglass;
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)]";
actionType = 'CANCEL_YIELD';
actionIcon = XCircle;
} 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;
if (currentStep === 'declare_attackers') {
if (gameState.attackersDeclared) {
actionLabel = "Confirm Attacks";
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}`;
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)]";
} 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";
}
}
} else if (currentStep === 'declare_blockers') {
actionLabel = "Declare Blockers";
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)]";
} 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 {
// Resolve Check
const topItem = gameState.stack![gameState.stack!.length - 1];
actionLabel = `Resolve ${topItem?.name || ''}`;
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)]";
}
} else {
// Waiting State
actionLabel = isMyTurn ? "Opponent Acting" : "Opponent's Turn";
actionIcon = Hand;
baseStyle = "bg-black/40 border-white/5 text-slate-500";
}
const handleAction = () => {
if (isYielding) {
onYieldToggle?.();
return;
}
if (!hasPriority) return;
if (actionType) {
let payload: any = { type: actionType };
if (actionType === 'DECLARE_ATTACKERS') {
payload.attackers = contextData?.attackers || [];
}
onAction('game_strict_action', payload);
}
};
// Phase Definitions
const phases: { id: Phase; icon: React.ElementType; label: string }[] = [
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' },
];
], []);
const activePhaseIndex = phases.findIndex(p => p.id === currentPhase);
return (
<div className="flex bg-black/40 backdrop-blur-md rounded-full p-1 border border-white/10 gap-1">
{phases.map((p) => {
const isActive = p.id === currentPhase;
<div className="flex flex-col items-center gap-2 w-full max-w-[420px] mx-auto pointer-events-auto transition-all duration-300">
return (
<div
key={p.id}
className={`
relative flex items-center justify-center w-8 h-8 rounded-full transition-all duration-300
${isActive ? 'bg-emerald-500 text-white shadow-[0_0_10px_rgba(16,185,129,0.5)] scale-110 z-10' : 'text-slate-500 bg-transparent hover:bg-white/5'}
`}
title={p.label}
>
<p.icon size={16} />
{/* 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}%` }}
/>
{/* Active Step Indicator (Text below or Tooltip) */}
{isActive && (
<span className="absolute -bottom-6 left-1/2 -translate-x-1/2 text-[10px] font-bold text-white uppercase tracking-wider whitespace-nowrap bg-black/80 px-2 py-0.5 rounded border border-white/10">
{currentStep}
</span>
)}
{/* 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} />;
})()}
</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>
</div>
);
};

View File

@@ -1,129 +0,0 @@
import React, { useRef } from 'react';
import { GameState } from '../../types/game';
interface SmartButtonProps {
gameState: GameState;
playerId: string;
onAction: (type: string, payload?: any) => void;
contextData?: any;
isYielding?: boolean;
onYieldToggle?: () => void;
}
export const SmartButton: React.FC<SmartButtonProps> = ({ gameState, playerId, onAction, contextData, isYielding, onYieldToggle }) => {
const isMyPriority = gameState.priorityPlayerId === playerId;
const isStackEmpty = !gameState.stack || gameState.stack.length === 0;
let label = "Wait";
let colorClass = "bg-slate-700 text-slate-400 cursor-not-allowed";
let actionType: string | null = null;
if (isYielding) {
label = "Yielding... (Tap to Cancel)";
colorClass = "bg-sky-600 hover:bg-sky-500 text-white shadow-[0_0_15px_rgba(2,132,199,0.5)] animate-pulse";
// Tap to cancel yield
actionType = 'CANCEL_YIELD';
} else if (isMyPriority) {
if (gameState.step === 'declare_attackers') {
if (gameState.attackersDeclared) {
label = "Pass (to Blockers)";
colorClass = "bg-emerald-600 hover:bg-emerald-500 text-white shadow-[0_0_15px_rgba(16,185,129,0.5)] animate-pulse";
actionType = 'PASS_PRIORITY';
} else {
const count = contextData?.attackers?.length || 0;
label = count > 0 ? `Attack with ${count}` : "Skip Combat";
colorClass = "bg-red-600 hover:bg-red-500 text-white shadow-[0_0_15px_rgba(239,68,68,0.5)] animate-pulse";
actionType = 'DECLARE_ATTACKERS';
}
} else if (gameState.step === 'declare_blockers') {
// Todo: blockers context
label = "Declare Blockers";
colorClass = "bg-blue-600 hover:bg-blue-500 text-white shadow-[0_0_15px_rgba(37,99,235,0.5)] animate-pulse";
actionType = 'DECLARE_BLOCKERS';
} else if (isStackEmpty) {
// Pass Priority / Advance Step
// If Main Phase, could technically play land/cast, but button defaults to Pass
label = "Pass Turn/Phase";
// If we want more granular: "Move to Combat" vs "End Turn" based on phase
if (gameState.phase === 'main1') label = "Pass to Combat";
else if (gameState.phase === 'main2') label = "End Turn";
else label = "Pass";
colorClass = "bg-emerald-600 hover:bg-emerald-500 text-white shadow-[0_0_15px_rgba(16,185,129,0.5)] animate-pulse";
actionType = 'PASS_PRIORITY';
} else {
// Resolve Top Item
const topItem = gameState.stack![gameState.stack!.length - 1];
label = `Resolve ${topItem?.name || 'Item'}`;
colorClass = "bg-amber-600 hover:bg-amber-500 text-white shadow-[0_0_15px_rgba(245,158,11,0.5)]";
actionType = 'PASS_PRIORITY'; // Resolving is just passing priority when stack not empty
}
}
const timerRef = useRef<NodeJS.Timeout | null>(null);
const isLongPress = useRef(false);
const handlePointerDown = () => {
isLongPress.current = false;
timerRef.current = setTimeout(() => {
isLongPress.current = true;
if (onYieldToggle) {
// Visual feedback could be added here
onYieldToggle();
}
}, 600); // 600ms long press for Yield
};
const handlePointerUp = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
if (!isLongPress.current) {
handleClick();
}
};
const handleClick = () => {
if (isYielding) {
// Cancel logic
if (onYieldToggle) onYieldToggle();
return;
}
if (actionType) {
let payload: any = { type: actionType };
if (actionType === 'DECLARE_ATTACKERS') {
payload.attackers = contextData?.attackers || [];
}
// TODO: Blockers payload
onAction('game_strict_action', payload);
}
};
// Prevent context menu on long press
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
};
return (
<button
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onPointerLeave={() => { if (timerRef.current) clearTimeout(timerRef.current); }}
onContextMenu={handleContextMenu}
disabled={!isMyPriority && !isYielding}
className={`
px-6 py-3 rounded-xl font-bold text-lg uppercase tracking-wider transition-all duration-300
${colorClass}
border border-white/10
flex items-center justify-center
min-w-[200px] select-none
`}
>
{label}
</button>
);
};

View File

@@ -13,17 +13,24 @@ export class RulesEngine {
public passPriority(playerId: string): boolean {
if (this.state.priorityPlayerId !== playerId) return false; // Not your turn
this.state.players[playerId].hasPassed = true;
const player = this.state.players[playerId];
player.hasPassed = true;
this.state.passedPriorityCount++;
// Check if all players passed
if (this.state.passedPriorityCount >= this.state.turnOrder.length) {
const totalPlayers = this.state.turnOrder.length;
// Check if all players passed in a row
if (this.state.passedPriorityCount >= totalPlayers) {
// 1. If Stack is NOT empty, Resolve Top
if (this.state.stack.length > 0) {
this.resolveTopStack();
} else {
}
// 2. If Stack IS empty, Advance Step
else {
this.advanceStep();
}
} else {
// Pass Priority to Next Player
this.passPriorityToNext();
}
return true;