diff --git a/src/client/src/modules/game/GameView.tsx b/src/client/src/modules/game/GameView.tsx index 699d8c6..51ace0b 100644 --- a/src/client/src/modules/game/GameView.tsx +++ b/src/client/src/modules/game/GameView.tsx @@ -498,6 +498,18 @@ export const GameView: React.FC = ({ gameState, currentPlayerId } return; } + // Handle Equipment / Ability on Battlefield + if (card.zone === 'battlefield') { + const isEquipment = card.types?.includes('Artifact') && card.subtypes?.includes('Equipment'); + + if (isEquipment && over.data.current.type === 'card') { // Equip only targets cards (creatures) + socketService.socket.emit('game_strict_action', { + action: { type: 'ACTIVATE_ABILITY', abilityIndex: 0, sourceId: cardId, targets: [targetId] } + }); + return; + } + } + // Default Cast with Target if (card.zone === 'hand') { socketService.socket.emit('game_strict_action', { @@ -702,9 +714,23 @@ export const GameView: React.FC = ({ gameState, currentPlayerId } {(() => { - const creatures = myBattlefield.filter(c => c.types?.includes('Creature')); - const allLands = myBattlefield.filter(c => c.types?.includes('Land') && !c.types?.includes('Creature')); - const others = myBattlefield.filter(c => !c.types?.includes('Creature') && !c.types?.includes('Land')); + // Separate Roots and Attachments + const attachments = myBattlefield.filter(c => c.attachedTo); + const unattached = myBattlefield.filter(c => !c.attachedTo); + + const creatures = unattached.filter(c => c.types?.includes('Creature')); + const allLands = unattached.filter(c => c.types?.includes('Land') && !c.types?.includes('Creature')); + const others = unattached.filter(c => !c.types?.includes('Creature') && !c.types?.includes('Land')); + + // Map Attachments to Hosts + const attachmentsMap = attachments.reduce((acc, c) => { + const target = c.attachedTo; + if (target) { + if (!acc[target]) acc[target] = []; + acc[target].push(c); + } + return acc; + }, {} as Record); const landGroups = allLands.reduce((acc, card) => { const key = card.name || 'Unknown Land'; @@ -713,17 +739,17 @@ export const GameView: React.FC = ({ gameState, currentPlayerId } return acc; }, {} as Record); - - const renderCard = (card: CardInstance) => { const isAttacking = proposedAttackers.has(card.instanceId); const blockingTargetId = proposedBlockers.get(card.instanceId); const isPreviewTapped = previewTappedIds.has(card.instanceId); + const attachedCards = attachmentsMap[card.instanceId] || []; + return (
= ({ gameState, currentPlayerId } }} > + {/* Render Attachments UNDER the card */} + {attachedCards.length > 0 && ( +
+ {attachedCards.map((att, idx) => ( +
+ { }} + onDragStart={() => { }} + className="w-16 h-16 opacity-90 hover:opacity-100 shadow-md border border-slate-600 rounded" + /> + {/* Allow dragging attachment off? Need separate Draggable wrapper for it OR handle logic */} + {/* For now, just visual representation. Use main logic to drag OFF if needed, but nested dragging is complex. + Ideally, we define DraggableCardWrapper around THIS too? + GameView dnd uses ID. If we use DraggableCardWrapper here, it should work. + */} +
+ ))} +
+ )} + 0 || (this.state.phase !== 'main1' && this.state.phase !== 'main2')) { + throw new Error("Equip can only be used as a sorcery."); + } + + // Target Check + if (targets.length !== 1) throw new Error("Equip requires exactly one target."); + const targetId = targets[0]; + const target = this.state.cards[targetId]; + + if (!target || target.zone !== 'battlefield' || !this.isCreature(target) || target.controllerId !== playerId) { + throw new Error("Invalid Equip target. Must be a creature you control."); + } + + // Pay Cost (Mock: {1}) + // this.payManaCost(playerId, "{1}"); + + // Resolve Immediately (Simple) or Stack (Correct)? + // Activated abilities go on stack. + this.state.stack.push({ + id: Math.random().toString(36).substr(2, 9), + sourceId: sourceId, + controllerId: playerId, + type: 'ability', + name: `Equip ${source.name} to ${target.name}`, + text: `Attach ${source.name} to ${target.name}`, + targets: [targetId] + }); + + this.resetPriority(playerId); + return true; + } + + throw new Error("Ability not implemented."); + } + // --- Mana System --- private parseManaCost(manaCost: string): { generic: number, colors: Record, hybrids: string[][] } { @@ -520,18 +580,87 @@ export class RulesEngine { ); if (isPermanent) { - this.moveCardToZone(card.instanceId, 'battlefield', false, item.resolutionPosition); + if (this.isAura(card)) { + // Aura Resolution + const targetId = item.targets[0]; + const target = this.state.cards[targetId]; + if (target && target.zone === 'battlefield' && this.canAttach(card, target)) { + this.moveCardToZone(card.instanceId, 'battlefield', false, item.resolutionPosition); + card.attachedTo = target.instanceId; + console.log(`${card.name} enters attached to ${target.name}`); + } else { + // 303.4g If an Aura is entering the battlefield and there is no legal object or player for it to enchant... it is put into its owner's graveyard. + console.log(`${card.name} failed to attach. Putting into GY.`); + this.moveCardToZone(card.instanceId, 'graveyard'); + } + } else { + // Normal Permanent + this.moveCardToZone(card.instanceId, 'battlefield', false, item.resolutionPosition); + } } else { // Instant / Sorcery this.moveCardToZone(card.instanceId, 'graveyard'); } } + } else if (item.type === 'ability') { + // Handle Ability Resolution (Equip or other Generic) + const source = this.state.cards[item.sourceId]; + if (source && source.zone === 'battlefield') { + // Equipment Logic + if (this.isEquipment(source)) { + const targetId = item.targets[0]; + const target = this.state.cards[targetId]; + + // Generic Validation Check for "Attach" + if (target && target.zone === 'battlefield' && this.validateTarget(source, target)) { + source.attachedTo = target.instanceId; + console.log(`Equipped ${source.name} to ${target.name}`); + } else { + console.log(`Equip failed: Target invalid.`); + } + } + } } // After resolution, Active Player gets priority again (Rule 117.3b) this.resetPriority(this.state.activePlayerId); } + // --- Targeting System (Generic) --- + + public validateTarget(source: any, target: any, actionType: 'cast' | 'equip' | 'ability' = 'ability'): boolean { + // 1. Basic Existence + if (!target || target.zone !== 'battlefield') return false; + + // 2. Protection (Simple Check) + if (target.pro_protection) { + // Check if source characteristics match protection + // E.g. Protection from White -> if source is White, return false. + // Not fully implemented yet, but placeholder is here. + } + + // 3. Specific Restrictions based on Source Type + if (this.isAura(source) || actionType === 'equip') { + // "Enchant/Equip Creature" is default for MVP unless specified otherwise + // Parse Text or Definition if available + // For now, strict Creature check for Equip/Aura (unless Aura says otherwise in text) + if (this.isEquipment(source) && !this.isCreature(target)) return false; + if (this.isAura(source)) { + // Example parsing: "Enchant land" + if (source.oracleText?.toLowerCase().includes('enchant land')) { + if (!target.types.includes('Land')) return false; + } else { + // Default Aura -> Creature + if (!target.types.includes('Creature')) return false; + } + } + + // Shroud / Hexproof check would go here + } + + return true; + } + private advanceStep() { // Transition Table const structure: Record = { @@ -890,6 +1019,18 @@ export class RulesEngine { } }); + // 5. Equipment Validity + Object.values(cards).forEach(c => { + if (c.zone === 'battlefield' && this.isEquipment(c) && c.attachedTo) { + const target = cards[c.attachedTo]; + if (!target || target.zone !== 'battlefield') { + console.log(`SBA: ${c.name} (Equipment) detached (Host invalid).`); + c.attachedTo = undefined; + sbaPerformed = true; + } + } + }); + return sbaPerformed; } @@ -972,15 +1113,30 @@ export class RulesEngine { }); } - // Layer 7e: Switch Power/Toughness - skipped for now - - // Final Floor rule: T cannot be less than 0 for logic? No, T can be negative for calculation, but usually treated as 0 for damage? - // Actually CR says negative numbers are real in calculation, but treated as 0 for dealing damage. - // We store true values. - card.power = p; card.toughness = t; }); } + // --- Helpers --- + + private isAura(card: any): boolean { + return card.types && card.types.includes('Enchantment') && card.subtypes && card.subtypes.includes('Aura'); + } + + private isEquipment(card: any): boolean { + return card.types && card.types.includes('Artifact') && card.subtypes && card.subtypes.includes('Equipment'); + } + + private isCreature(card: any): boolean { + return card.types && card.types.includes('Creature'); + } + + private canAttach(source: any, target: any): boolean { + if (this.isAura(source)) { + if (source.oracleText?.toLowerCase().includes('enchant land')) return target.types.includes('Land'); + return target.types.includes('Creature'); + } + return true; + } }