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>
|
||||
|
||||
{/* 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 */}
|
||||
<div className="w-40 p-2 flex flex-col gap-2 items-center justify-center border-r border-white/10">
|
||||
{/* 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} />
|
||||
@@ -839,6 +839,13 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
</div>
|
||||
</DroppableZone>
|
||||
</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>
|
||||
|
||||
{/* Hand Area & Smart Button */}
|
||||
@@ -890,7 +897,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<button
|
||||
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>
|
||||
|
||||
<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}
|
||||
</div>
|
||||
<div className="flex gap-1 mt-2 justify-center">
|
||||
<div className="flex gap-1 mt-1 justify-center">
|
||||
<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 } })}
|
||||
>
|
||||
-
|
||||
</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 } })}
|
||||
>
|
||||
+
|
||||
@@ -930,7 +937,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
</div>
|
||||
|
||||
{/* 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 => {
|
||||
const count = myPlayer?.manaPool?.[color] || 0;
|
||||
const icons: Record<string, string> = {
|
||||
@@ -941,20 +948,34 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
};
|
||||
|
||||
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 className={`text-xs ${colors[color]}`}>{icons[color]}</div>
|
||||
<div className="text-sm font-mono">{count}</div>
|
||||
<div key={color} className="flex flex-col items-center">
|
||||
<div className={`text-xs ${colors[color]} font-bold flex items-center gap-1`}>
|
||||
{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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -454,10 +454,39 @@ export class RulesEngine {
|
||||
|
||||
// 2. Draw Step
|
||||
if (step === 'draw') {
|
||||
const player = this.state.players[activePlayerId];
|
||||
if (this.state.turnCount > 1 || this.state.turnOrder.length > 2) {
|
||||
// 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
|
||||
if (step === 'cleanup') {
|
||||
|
||||
@@ -126,6 +126,15 @@ export class GameManager {
|
||||
case 'MULLIGAN_DECISION':
|
||||
engine.resolveMulligan(actorId, action.keep, action.cardsToBottom);
|
||||
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
|
||||
default:
|
||||
console.warn(`Unknown strict action: ${action.type}`);
|
||||
|
||||
Reference in New Issue
Block a user