feat: Add manual draw card action, interactive mana pool controls, and reorganize game view layout.
This commit is contained in:
@@ -797,10 +797,10 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
</DroppableZone>
|
</DroppableZone>
|
||||||
|
|
||||||
{/* Bottom Area: Controls & Hand */}
|
{/* Bottom Area: Controls & Hand */}
|
||||||
<div className="h-48 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)]">
|
||||||
|
|
||||||
{/* Left Controls: Library/Grave */}
|
{/* Left Controls: Library/Grave/Exile */}
|
||||||
<div className="w-40 p-2 flex flex-col gap-2 items-center justify-center 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 Integration */}
|
||||||
<div className="mb-2 scale-75 origin-center">
|
<div className="mb-2 scale-75 origin-center">
|
||||||
<PhaseStrip gameState={gameState} />
|
<PhaseStrip gameState={gameState} />
|
||||||
@@ -839,6 +839,13 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
</div>
|
</div>
|
||||||
</DroppableZone>
|
</DroppableZone>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DroppableZone id="exile" data={{ type: 'zone' }} className="w-full text-center border-t border-white/10 mt-2 pt-2 cursor-pointer hover:bg-white/5 rounded p-1">
|
||||||
|
<div onContextMenu={(e) => handleContextMenu(e, 'zone', undefined, 'exile')}>
|
||||||
|
<span className="text-xs text-slate-500 block">Exile</span>
|
||||||
|
<span className="text-lg font-bold text-slate-400">{myExile.length}</span>
|
||||||
|
</div>
|
||||||
|
</DroppableZone>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hand Area & Smart Button */}
|
{/* Hand Area & Smart Button */}
|
||||||
@@ -890,7 +897,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Controls: Exile / Life */}
|
{/* Right Controls: Exile / Life */}
|
||||||
<div className="w-40 p-2 flex flex-col gap-4 items-center justify-between border-l border-white/10 py-4">
|
<div className="w-52 p-2 flex flex-col gap-2 items-center justify-between border-l border-white/10 py-2">
|
||||||
<div className="text-center w-full relative">
|
<div className="text-center w-full relative">
|
||||||
<button
|
<button
|
||||||
className="absolute top-0 right-0 p-1 text-slate-600 hover:text-white transition-colors"
|
className="absolute top-0 right-0 p-1 text-slate-600 hover:text-white transition-colors"
|
||||||
@@ -910,18 +917,18 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="text-[10px] text-slate-400 uppercase tracking-wider mb-1">Your Life</div>
|
<div className="text-[10px] text-slate-400 uppercase tracking-wider mb-1">Your Life</div>
|
||||||
<div className="text-5xl font-black text-transparent bg-clip-text bg-gradient-to-b from-emerald-400 to-emerald-700 drop-shadow-[0_2px_10px_rgba(16,185,129,0.3)]">
|
<div className="text-4xl font-black text-transparent bg-clip-text bg-gradient-to-b from-emerald-400 to-emerald-700 drop-shadow-[0_2px_10px_rgba(16,185,129,0.3)]">
|
||||||
{myPlayer?.life}
|
{myPlayer?.life}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 mt-2 justify-center">
|
<div className="flex gap-1 mt-1 justify-center">
|
||||||
<button
|
<button
|
||||||
className="w-8 h-8 rounded-full bg-slate-800 hover:bg-red-500/20 text-red-500 border border-slate-700 hover:border-red-500 transition-colors flex items-center justify-center font-bold"
|
className="w-6 h-6 rounded-full bg-slate-800 hover:bg-red-500/20 text-red-500 border border-slate-700 hover:border-red-500 transition-colors flex items-center justify-center font-bold"
|
||||||
onClick={() => socketService.socket.emit('game_action', { action: { type: 'UPDATE_LIFE', amount: -1 } })}
|
onClick={() => socketService.socket.emit('game_action', { action: { type: 'UPDATE_LIFE', amount: -1 } })}
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-8 h-8 rounded-full bg-slate-800 hover:bg-emerald-500/20 text-emerald-500 border border-slate-700 hover:border-emerald-500 transition-colors flex items-center justify-center font-bold"
|
className="w-6 h-6 rounded-full bg-slate-800 hover:bg-emerald-500/20 text-emerald-500 border border-slate-700 hover:border-emerald-500 transition-colors flex items-center justify-center font-bold"
|
||||||
onClick={() => socketService.socket.emit('game_action', { action: { type: 'UPDATE_LIFE', amount: 1 } })}
|
onClick={() => socketService.socket.emit('game_action', { action: { type: 'UPDATE_LIFE', amount: 1 } })}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
@@ -930,7 +937,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mana Pool Display */}
|
{/* Mana Pool Display */}
|
||||||
<div className="w-full bg-slate-800/50 rounded-lg p-2 flex flex-wrap justify-between gap-1 border border-white/5">
|
<div className="w-full bg-slate-800/50 rounded-lg p-2 grid grid-cols-3 gap-x-1 gap-y-1 border border-white/5">
|
||||||
{['W', 'U', 'B', 'R', 'G', 'C'].map(color => {
|
{['W', 'U', 'B', 'R', 'G', 'C'].map(color => {
|
||||||
const count = myPlayer?.manaPool?.[color] || 0;
|
const count = myPlayer?.manaPool?.[color] || 0;
|
||||||
const icons: Record<string, string> = {
|
const icons: Record<string, string> = {
|
||||||
@@ -941,20 +948,34 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={color} className={`flex flex-col items-center w-[30%] ${count > 0 ? 'opacity-100 scale-110 font-bold' : 'opacity-30'} transition-all`}>
|
<div key={color} className="flex flex-col items-center">
|
||||||
<div className={`text-xs ${colors[color]}`}>{icons[color]}</div>
|
<div className={`text-xs ${colors[color]} font-bold flex items-center gap-1`}>
|
||||||
<div className="text-sm font-mono">{count}</div>
|
{icons[color]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<button
|
||||||
|
className="w-4 h-4 flex items-center justify-center rounded bg-slate-700 hover:bg-red-900/50 text-red-500 text-[10px] disabled:opacity-30 disabled:hover:bg-slate-700"
|
||||||
|
onClick={() => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', mana: { color, amount: -1 } } })}
|
||||||
|
disabled={count <= 0}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span className={`text-sm font-mono w-4 text-center ${count > 0 ? 'text-white font-bold' : 'text-slate-500'}`}>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="w-4 h-4 flex items-center justify-center rounded bg-slate-700 hover:bg-emerald-900/50 text-emerald-500 text-[10px] hover:text-emerald-400"
|
||||||
|
onClick={() => socketService.socket.emit('game_strict_action', { action: { type: 'ADD_MANA', mana: { color, amount: 1 } } })}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DroppableZone id="exile" data={{ type: 'zone' }} className="w-full text-center border-t border-white/5 pt-2 cursor-pointer hover:bg-white/5 rounded p-1">
|
|
||||||
<div onContextMenu={(e) => handleContextMenu(e, 'zone', undefined, 'exile')}>
|
|
||||||
<span className="text-xs text-slate-500 block">Exile Drop Zone</span>
|
|
||||||
<span className="text-lg font-bold text-slate-400">{myExile.length}</span>
|
|
||||||
</div>
|
|
||||||
</DroppableZone>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -454,9 +454,38 @@ export class RulesEngine {
|
|||||||
|
|
||||||
// 2. Draw Step
|
// 2. Draw Step
|
||||||
if (step === 'draw') {
|
if (step === 'draw') {
|
||||||
|
const player = this.state.players[activePlayerId];
|
||||||
if (this.state.turnCount > 1 || this.state.turnOrder.length > 2) {
|
if (this.state.turnCount > 1 || this.state.turnOrder.length > 2) {
|
||||||
this.drawCard(activePlayerId);
|
// If Bot: Auto Draw
|
||||||
|
if (player && player.isBot) {
|
||||||
|
console.log(`[Auto] Bot ${player.name} drawing card.`);
|
||||||
|
this.drawCard(activePlayerId);
|
||||||
|
// After draw, AP priority
|
||||||
|
this.resetPriority(activePlayerId);
|
||||||
|
} else {
|
||||||
|
// If Human: Wait for Manual Action
|
||||||
|
console.log(`[Manual] Waiting for Human ${player?.name} to draw.`);
|
||||||
|
// We do NOT call drawCard here.
|
||||||
|
// We DO reset priority to them so they can take the action?
|
||||||
|
// Actually, if we are in 'draw' step, strict rules say AP gets priority.
|
||||||
|
// Yet, the "Turn Based Action" of drawing usually happens *immediately* at start of step, BEFORE priority.
|
||||||
|
// 504.1. First, the active player draws a card. This turn-based action doesn't use the stack.
|
||||||
|
// 504.2. Second, the active player gets priority.
|
||||||
|
// So for "Manual" feeling, we pause BEFORE 504.1 is considered "done"?
|
||||||
|
// Effectively, we treat the "DRAW_CARD" action as the completion of 504.1.
|
||||||
|
|
||||||
|
// Ensure they are the priority player so the UI lets them act (if we key off priority)
|
||||||
|
// But strict action validation for DRAW_CARD will check if they are AP and in Draw step.
|
||||||
|
if (this.state.priorityPlayerId !== activePlayerId) {
|
||||||
|
this.state.priorityPlayerId = activePlayerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Skip draw (Turn 1 in 2p game)
|
||||||
|
console.log("Skipping Draw (Turn 1 2P).");
|
||||||
|
this.resetPriority(activePlayerId);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Cleanup Step
|
// 3. Cleanup Step
|
||||||
|
|||||||
@@ -126,6 +126,15 @@ export class GameManager {
|
|||||||
case 'MULLIGAN_DECISION':
|
case 'MULLIGAN_DECISION':
|
||||||
engine.resolveMulligan(actorId, action.keep, action.cardsToBottom);
|
engine.resolveMulligan(actorId, action.keep, action.cardsToBottom);
|
||||||
break;
|
break;
|
||||||
|
case 'DRAW_CARD':
|
||||||
|
// Strict validation: Must be Draw step, Must be Active Player
|
||||||
|
if (game.step !== 'draw') throw new Error("Can only draw in Draw Step.");
|
||||||
|
if (game.activePlayerId !== actorId) throw new Error("Only Active Player can draw.");
|
||||||
|
|
||||||
|
engine.drawCard(actorId);
|
||||||
|
// After drawing, 504.2 says AP gets priority.
|
||||||
|
engine.resetPriority(actorId);
|
||||||
|
break;
|
||||||
// TODO: Activate Ability
|
// TODO: Activate Ability
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown strict action: ${action.type}`);
|
console.warn(`Unknown strict action: ${action.type}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user