changes the smart button
This commit is contained in:
@@ -9,8 +9,8 @@ import { CardComponent } from './CardComponent';
|
|||||||
import { GameContextMenu, ContextMenuRequest } from './GameContextMenu';
|
import { GameContextMenu, ContextMenuRequest } from './GameContextMenu';
|
||||||
import { ZoneOverlay } from './ZoneOverlay';
|
import { ZoneOverlay } from './ZoneOverlay';
|
||||||
import { PhaseStrip } from './PhaseStrip';
|
import { PhaseStrip } from './PhaseStrip';
|
||||||
import { SmartButton } from './SmartButton';
|
|
||||||
import { StackVisualizer } from './StackVisualizer';
|
import { StackVisualizer } from './StackVisualizer';
|
||||||
|
|
||||||
import { GestureManager } from './GestureManager';
|
import { GestureManager } from './GestureManager';
|
||||||
import { MulliganView } from './MulliganView';
|
import { MulliganView } from './MulliganView';
|
||||||
import { RadialMenu, RadialOption } from './RadialMenu';
|
import { RadialMenu, RadialOption } from './RadialMenu';
|
||||||
@@ -801,10 +801,8 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
|
|
||||||
{/* Left Controls: Library/Grave/Exile */}
|
{/* 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">
|
<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 */}
|
{/* Phase Strip Moved to Bottom Center */}
|
||||||
<div className="mb-2 scale-75 origin-center">
|
|
||||||
<PhaseStrip gameState={gameState} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<DroppableZone
|
<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">
|
<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 Floating above Hand */}
|
{/* Smart Button / Action Strip Floating above Hand */}
|
||||||
<div className="mb-4 z-40 self-center">
|
<div className="mb-4 z-40 self-center flex flex-col items-center gap-4 w-full">
|
||||||
<SmartButton
|
{/* Phase Strip Central Integration (Now acts as Smart Button) */}
|
||||||
|
<PhaseStrip
|
||||||
gameState={gameState}
|
gameState={gameState}
|
||||||
playerId={currentPlayerId}
|
currentPlayerId={currentPlayerId}
|
||||||
onAction={(type, payload) => socketService.socket.emit(type, { action: payload })}
|
onAction={(type, payload) => socketService.socket.emit(type, { action: payload })}
|
||||||
contextData={{
|
contextData={{
|
||||||
attackers: Array.from(proposedAttackers).map(id => ({ attackerId: id, targetId: opponentId })),
|
attackers: Array.from(proposedAttackers).map(id => ({ attackerId: id, targetId: opponentId })),
|
||||||
@@ -867,6 +866,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
||||||
{myHand.map((card, index) => (
|
{myHand.map((card, index) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,50 +1,211 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import { GameState, Phase, Step } from '../../types/game';
|
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 {
|
interface PhaseStripProps {
|
||||||
gameState: GameState;
|
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 currentPhase = gameState.phase as Phase;
|
||||||
const currentStep = gameState.step as Step;
|
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
|
// 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: 'beginning', icon: Sun, label: 'Beginning' },
|
||||||
{ id: 'main1', icon: Shield, label: 'Main 1' },
|
{ id: 'main1', icon: Shield, label: 'Main 1' },
|
||||||
{ id: 'combat', icon: Swords, label: 'Combat' },
|
{ id: 'combat', icon: Swords, label: 'Combat' },
|
||||||
{ id: 'main2', icon: Shield, label: 'Main 2' },
|
{ id: 'main2', icon: Shield, label: 'Main 2' },
|
||||||
{ id: 'ending', icon: Hourglass, label: 'End' },
|
{ id: 'ending', icon: Hourglass, label: 'End' },
|
||||||
];
|
], []);
|
||||||
|
|
||||||
|
const activePhaseIndex = phases.findIndex(p => p.id === currentPhase);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex bg-black/40 backdrop-blur-md rounded-full p-1 border border-white/10 gap-1">
|
<div className="flex flex-col items-center gap-2 w-full max-w-[420px] mx-auto pointer-events-auto transition-all duration-300">
|
||||||
{phases.map((p) => {
|
|
||||||
const isActive = p.id === currentPhase;
|
|
||||||
|
|
||||||
|
{/* 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}%` }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={p.id}
|
key={p.id}
|
||||||
className={`
|
className={`
|
||||||
relative flex items-center justify-center w-8 h-8 rounded-full transition-all duration-300
|
h-1 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'}
|
${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' : ''}
|
||||||
`}
|
`}
|
||||||
title={p.label}
|
/>
|
||||||
>
|
|
||||||
<p.icon size={16} />
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -13,17 +13,24 @@ export class RulesEngine {
|
|||||||
public passPriority(playerId: string): boolean {
|
public passPriority(playerId: string): boolean {
|
||||||
if (this.state.priorityPlayerId !== playerId) return false; // Not your turn
|
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++;
|
this.state.passedPriorityCount++;
|
||||||
|
|
||||||
// Check if all players passed
|
const totalPlayers = this.state.turnOrder.length;
|
||||||
if (this.state.passedPriorityCount >= 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) {
|
if (this.state.stack.length > 0) {
|
||||||
this.resolveTopStack();
|
this.resolveTopStack();
|
||||||
} else {
|
}
|
||||||
|
// 2. If Stack IS empty, Advance Step
|
||||||
|
else {
|
||||||
this.advanceStep();
|
this.advanceStep();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Pass Priority to Next Player
|
||||||
this.passPriorityToNext();
|
this.passPriorityToNext();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user