/** * MTG Commander Simulator — ui.js * All UI rendering functions adapted for the 4-quadrant battlefield layout. * This file overrides/extends DOM-touching functions from engine.js. * ============================================================ */ 'use strict'; // ============================================================ // GLOBAL UI STATE // ============================================================ let zoneModalPlayerId = null; let activeZoneTab = 'graveyard'; let _zoneCardCtx = null; let undoHistoryVisible = false; let aiTurnActive = false; let floatingPreviewTimeout = null; // Hand-to-battlefield drag state let isDraggingFromHand = false; let handDragPlayerIdx = null; let handDragCardIdx = null; let handDragGhost = null; // ============================================================ // LOG SYSTEM (override engine stub) // ============================================================ const _logEntries = []; function addLogEntry(text, category) { const entry = { text, category: category || 'general', ts: Date.now() }; _logEntries.push(entry); // Keep last 500 entries if (_logEntries.length > 500) _logEntries.shift(); // Show as floating log entry on the battlefield addFloatingLogEntry(text, category); } function addFloatingLogEntry(text, category) { const floatingLog = document.getElementById('floating-log'); if (!floatingLog) return; const entry = document.createElement('div'); entry.className = 'floating-log-entry'; entry.innerHTML = text; floatingLog.appendChild(entry); // Remove old entries while (floatingLog.children.length > 5) { floatingLog.removeChild(floatingLog.firstChild); } // Fade out after 4 seconds setTimeout(() => { entry.style.opacity = '0'; entry.style.transition = 'opacity 0.6s'; setTimeout(() => { if (entry.parentNode) entry.remove(); }, 700); }, 4000); } // ============================================================ // LIFE COUNTER RENDERING (4-quadrant) // ============================================================ function updateLifeCounterDisplay(playerId) { if (!gameState) return; const player = gameState.players[playerId]; if (!player) return; const el = document.getElementById('life-' + playerId); if (el) el.textContent = player.life; // Also update poison const poisonEl = document.getElementById('poison-' + playerId); if (poisonEl) { poisonEl.textContent = player.poison > 0 ? player.poison + ' poison' : ''; poisonEl.style.display = player.poison > 0 ? 'block' : 'none'; } // Update player label with commander name const cmdNameEl = document.getElementById('cmd-name-' + playerId); if (cmdNameEl && player.commanderCardName) { cmdNameEl.textContent = player.commanderCardName; } } // Called by engine's setLife function setLife(playerId, newLife) { if (!gameState) return; pushUndoState(); const player = gameState.players[playerId]; if (!player) return; const oldLife = player.life; player.life = newLife; updateLifeCounterDisplay(playerId); const diff = newLife - oldLife; const sign = diff > 0 ? '+' : ''; addLogEntry('' + player.name + ' life: ' + oldLife + ' → ' + newLife + ' (' + sign + diff + ')'); if (diff > 0) fireBattlefieldTriggers('life_gain'); else if (diff < 0) fireBattlefieldTriggers('life_loss'); if (newLife <= 0 && !player.eliminated) eliminatePlayer(playerId, 'life total reached 0'); checkStateBasedActions(); // Highlight active player quadrant updateActivePlayerHighlight(); } // ============================================================ // ACTIVE PLAYER HIGHLIGHT // ============================================================ function updateActivePlayerHighlight() { if (!gameState) return; for (let i = 0; i < 4; i++) { const area = document.querySelector('.player-area.p' + (i + 1)); if (area) { area.classList.toggle('active-player', gameState.currentPlayerIndex === i); } } // Update active player indicator in top bar const indi = document.getElementById('active-player-indicator'); if (indi && gameState.players[gameState.currentPlayerIndex]) { const p = gameState.players[gameState.currentPlayerIndex]; indi.innerHTML = '' + p.name + "'s Turn"; } // Update turn display const turnEl = document.getElementById('turn-counter'); if (turnEl) turnEl.innerHTML = 'Turn ' + (gameState.turnNumber || 1) + ''; } // ============================================================ // RENDER PLAYER PANELS (adapted for 4-quadrant — updates zones/labels) // ============================================================ function renderPlayerPanels() { if (!gameState) return; gameState.players.forEach((player, idx) => { updateLifeCounterDisplay(idx); updateZoneCountsForPlayer(idx); renderCommanderZone(idx); renderHandZone(idx); }); updateActivePlayerHighlight(); } function updateManaPoolBadge(playerIdx) { // Future: show mini mana pool indicators on each player's area } function updateZoneCountsForPlayer(playerIdx) { if (!gameState) return; const player = gameState.players[playerIdx]; if (!player) return; const pNum = playerIdx + 1; // Update library count const libCount = document.getElementById('lib-count-' + pNum); if (libCount) libCount.textContent = player.zones.library.length; // Update hand count const handCount = document.getElementById('hand-count-' + pNum); if (handCount) handCount.textContent = player.zones.hand.length; // Exile + Graveyard: render last card image face-up renderLastCardInZone(playerIdx, 'exile'); renderLastCardInZone(playerIdx, 'graveyard'); } function updatePlayerZones() { if (!gameState) return; gameState.players.forEach((_, idx) => { updateZoneCountsForPlayer(idx); renderHandZone(idx); renderCommanderZone(idx); }); } // ============================================================ // COMMANDER ZONE RENDERING // ============================================================ function renderCommanderZone(playerIdx) { if (!gameState) return; const player = gameState.players[playerIdx]; if (!player) return; const pNum = playerIdx + 1; const zoneEl = document.getElementById('cmd-zone-' + pNum); if (!zoneEl) return; const cmdCards = player.zones.command || []; if (cmdCards.length > 0) { const cmdCard = cmdCards[0]; if (cmdCard.imageUrl) { zoneEl.innerHTML = '' + (cmdCard.name || 'Commander') + ''; // Count badge if (player.commanderCastCount > 0) { zoneEl.innerHTML += '' + (player.commanderCastCount) + 'x'; } } else { zoneEl.innerHTML = '👑' + 'Commander' + '' + cmdCards.length + ''; } } else { zoneEl.innerHTML = '👑' + 'Commander'; } } // ============================================================ // EXILE / GRAVEYARD — Show last card image face-up // ============================================================ function renderLastCardInZone(playerIdx, zoneName) { if (!gameState) return; const player = gameState.players[playerIdx]; if (!player) return; const pNum = playerIdx + 1; const zoneIdPrefix = (zoneName === 'exile') ? 'exile-zone-' : 'gv-zone-'; const zoneEl = document.getElementById(zoneIdPrefix + pNum); if (!zoneEl) return; const cards = player.zones[zoneName] || []; const lastCard = cards[cards.length - 1]; // most recently added card if (lastCard && lastCard.imageUrl) { const safeImg = (lastCard.imageUrl || '').replace(/'/g, "\\'"); const safeName = (lastCard.name || '').replace(/'/g, "\\'"); const safeType = (lastCard.typeLine || '').replace(/'/g, "\\'"); zoneEl.innerHTML = '' + (lastCard.name || zoneName) + ''; // Always show count badge on top if (cards.length > 0) { zoneEl.innerHTML += '' + cards.length + ''; } } else { // Fallback: icon + name + count (original look) const icon = (zoneName === 'exile') ? '⬡' : '☠'; const label = (zoneName === 'exile') ? 'Exile' : 'Grave'; const countId = (zoneName === 'exile') ? 'exile-count-' : 'gv-count-'; zoneEl.innerHTML = '' + cards.length + '' + '' + icon + '' + '' + label + ''; } } // ============================================================ // HAND ZONE RENDERING (thumbnails in zone.hand) // ============================================================ function renderHandZone(playerIdx) { if (!gameState) return; const player = gameState.players[playerIdx]; if (!player) return; const pNum = playerIdx + 1; const handEl = document.getElementById('hand-zone-' + pNum); if (!handEl) return; const hand = player.zones.hand || []; // Update count badge const countEl = document.getElementById('hand-count-' + pNum); if (countEl) countEl.textContent = hand.length; if (hand.length === 0) { handEl.innerHTML = '🃏' + 'Hand (0)'; return; } // Render thumbnails let thumbsHtml = ''; const maxShow = Math.min(hand.length, 7); // limit thumbnails for (let i = 0; i < maxShow; i++) { const card = hand[i]; const imgSrc = card.imageUrl || ''; const escapedName = (card.name || '').replace(/'/g, "\\'"); const escapedType = (card.typeLine || '').replace(/'/g, "\\'"); if (imgSrc) { thumbsHtml += '
' + '' + (card.name || '') + '' + '
'; } else { thumbsHtml += '
' + (card.name || '?').substring(0, 4) + '
'; } } if (hand.length > maxShow) { thumbsHtml += '
+' + (hand.length - maxShow) + '
'; } handEl.innerHTML = thumbsHtml; } // ============================================================ // RENDER BATTLEFIELD CARD (adapted for quadrant battle-zones) // ============================================================ function renderBattlefieldCard(cardData) { // Target the player's battle-zone div const pNum = cardData.ownerIndex + 1; const battleZone = document.getElementById('battle-zone-' + pNum); if (!battleZone) { // Fallback: try old-style battlefield const bf = document.getElementById('battlefield'); if (!bf) return; } const container = battleZone || document.getElementById('battlefield'); // Remove existing card element if re-rendering const existing = document.getElementById('bc-' + cardData.id); if (existing) existing.remove(); const card = document.createElement('div'); card.className = 'battlefield-card'; card.id = 'bc-' + cardData.id; card.style.left = (cardData.x || 10) + 'px'; card.style.top = (cardData.y || 10) + 'px'; const playerColor = (gameState.players[cardData.ownerIndex] || {}).color || 'transparent'; if (cardData.tapped) card.classList.add('tapped'); if (cardData.faceDown) card.classList.add('face-down'); if (typeof hasSummoningSickness === 'function' && hasSummoningSickness(cardData)) { card.classList.add('summoning-sick'); } if (cardData.isToken) card.classList.add('token-glow'); // Keyword badges HTML let kwBadgeHtml = ''; if (typeof getKeywordBadges === 'function') { const kwBadges = getKeywordBadges(cardData); if (kwBadges && kwBadges.length > 0) { kwBadgeHtml = '
'; kwBadges.forEach(b => { kwBadgeHtml += '' + b.icon + ''; }); kwBadgeHtml += '
'; } } // Protection badge let protBadge = ''; if (typeof getBattlefieldCardProtectionBadge === 'function') { protBadge = getBattlefieldCardProtectionBadge(cardData) || ''; } const imgUrl = cardData.imageUrl || ''; const altName = (cardData.name || 'Card').replace(/"/g, '"'); card.innerHTML = protBadge + '' + altName + '' + kwBadgeHtml; // Counter badges if (cardData.counters) { const cTypes = Object.keys(cardData.counters).filter(k => cardData.counters[k] > 0); if (cTypes.length > 0) { const strip = document.createElement('div'); strip.className = 'card-counters-strip'; cTypes.forEach(cType => { const cCount = cardData.counters[cType]; const ct = (typeof COUNTER_TYPES !== 'undefined' && COUNTER_TYPES[cType]) || { icon: '\u25CF', color: '#9ca3af', label: cType }; const badge = document.createElement('span'); badge.className = 'card-counter-badge'; badge.style.cssText = 'background:' + ct.color + ';color:#fff;'; badge.title = cCount + ' ' + ct.label; if (cType === '+1/+1') badge.textContent = '+' + cCount + '/+' + cCount; else if (cType === '-1/-1') badge.textContent = '-' + cCount + '/-' + cCount; else badge.textContent = ct.icon + cCount; strip.appendChild(badge); }); card.appendChild(strip); } } // Damage badge if (cardData.damageMarked && cardData.damageMarked > 0) { const dmgBadge = document.createElement('div'); dmgBadge.className = 'damage-badge'; dmgBadge.textContent = cardData.damageMarked; card.appendChild(dmgBadge); } // Event listeners — use both for compatibility (Comet: inline onclick already on container) card.addEventListener('mousedown', e => { if (e.button !== 0) return; if (cardData.attachedTo) return; e.preventDefault(); isDraggingBattlefieldCard = true; draggedCardId = cardData.id; const rect = card.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; card.style.zIndex = '100'; card.style.transition = 'none'; }); card.addEventListener('dblclick', e => { e.preventDefault(); toggleTap(cardData.id); }); card.addEventListener('contextmenu', e => { e.preventDefault(); showContextMenu(e.clientX, e.clientY, cardData.id); }); card.addEventListener('click', e => { if (!gameState) return; if (typeof stackPickMode !== 'undefined' && stackPickMode) { handleStackPickClick(cardData.id); e.stopPropagation(); return; } if (typeof equipPickMode !== 'undefined' && equipPickMode) { handleEquipPickClick(cardData.id); e.stopPropagation(); return; } // Combat attacker/blocker clicks const combat = gameState.combat; if (combat && combat.phase === 'declaring-attackers') { if (cardData.ownerIndex === gameState.currentPlayerIndex && !cardData.tapped) { toggleAttacker(cardData.id); e.stopPropagation(); } } else if (combat && combat.phase === 'declaring-blockers') { if (!cardData.tapped && combat.pendingBlockerFor !== null) { toggleBlocker(cardData.id, combat.pendingBlockerFor); e.stopPropagation(); } else { const asAttacker = combat.attackers && combat.attackers.find(a => a.cardId === cardData.id); if (asAttacker) { combat.pendingBlockerFor = cardData.id; refreshCombatOverlay(); e.stopPropagation(); } } } }); // Long press for context menu (touch) let _lpTimer = null; card.addEventListener('touchstart', e => { _lpTimer = setTimeout(() => { const t = e.touches[0]; showContextMenu(t.clientX, t.clientY, cardData.id); }, 600); }); card.addEventListener('touchend', () => clearTimeout(_lpTimer)); card.addEventListener('touchmove', () => clearTimeout(_lpTimer)); card.addEventListener('mouseenter', e => { if (!isDraggingBattlefieldCard) { if (cardData.stackGroupId && typeof isStackLeader === 'function' && isStackLeader(cardData)) { fanOutStack(cardData.stackGroupId); } showFloatingCardPreview(cardData.imageUrl, e, { name: cardData.name, typeLine: cardData.typeLine, oracleText: cardData.oracleText, manaCost: cardData.manaCost || '', power: cardData.power, toughness: cardData.toughness, pt: cardData.pt, loyalty: cardData.loyalty, _bfCard: cardData }); } }); card.addEventListener('mousemove', e => { if (!isDraggingBattlefieldCard) positionFloatingPreview(e); }); card.addEventListener('mouseleave', () => { hideFloatingCardPreview(); }); container.appendChild(card); } // Drag tracking for battlefield cards document.addEventListener('mousemove', e => { if (!isDraggingBattlefieldCard || draggedCardId === null) return; hideFloatingCardPreview(); const cardData = gameState && gameState.battlefieldCards.find(c => c.id === draggedCardId); if (!cardData) return; const pNum = cardData.ownerIndex + 1; const battleZone = document.getElementById('battle-zone-' + pNum); if (!battleZone) return; const rect = battleZone.getBoundingClientRect(); let newX = e.clientX - rect.left - dragOffset.x; let newY = e.clientY - rect.top - dragOffset.y; newX = Math.max(0, Math.min(rect.width - 80, newX)); newY = Math.max(0, Math.min(rect.height - 105, newY)); cardData.x = newX; cardData.y = newY; const cardEl = document.getElementById('bc-' + draggedCardId); if (cardEl) { cardEl.style.left = newX + 'px'; cardEl.style.top = newY + 'px'; } if (cardData.attachments && cardData.attachments.length > 0) { moveAttachmentsWithCreature(cardData.id); } }); document.addEventListener('mouseup', e => { if (!isDraggingBattlefieldCard) return; isDraggingBattlefieldCard = false; const droppedId = draggedCardId; const cardEl = document.getElementById('bc-' + droppedId); if (cardEl) { cardEl.style.zIndex = ''; cardEl.style.transition = ''; } draggedCardId = null; // Shift+drop stacking if (e.shiftKey && droppedId && gameState) { const droppedCard = gameState.battlefieldCards.find(c => c.id === droppedId); if (droppedCard && typeof createStack === 'function') { const threshold = 40; let closestCard = null, closestDist = Infinity; gameState.battlefieldCards.forEach(other => { if (other.id === droppedId) return; if (other.stackGroupId && !isStackLeader(other)) return; const dx = (other.x || 0) - (droppedCard.x || 0); const dy = (other.y || 0) - (droppedCard.y || 0); const dist = Math.sqrt(dx * dx + dy * dy); if (dist < threshold && dist < closestDist) { closestDist = dist; closestCard = other; } }); if (closestCard) createStack(closestCard.id, droppedId); } } }); // ============================================================ // HAND-TO-BATTLEFIELD DRAG & DROP // ============================================================ function startHandDrag(e, playerIdx, handIndex, imgSrc) { // Only left mouse button if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); isDraggingFromHand = true; handDragPlayerIdx = playerIdx; handDragCardIdx = handIndex; hideFloatingCardPreview(); // Create ghost element handDragGhost = document.createElement('div'); handDragGhost.className = 'hand-drag-ghost'; handDragGhost.innerHTML = ''; handDragGhost.style.left = e.clientX + 'px'; handDragGhost.style.top = e.clientY + 'px'; document.body.appendChild(handDragGhost); } document.addEventListener('mousemove', function(e) { if (!isDraggingFromHand || !handDragGhost) return; handDragGhost.style.left = e.clientX + 'px'; handDragGhost.style.top = e.clientY + 'px'; }); document.addEventListener('mouseup', function(e) { if (!isDraggingFromHand) return; // Clean up ghost if (handDragGhost) { handDragGhost.remove(); handDragGhost = null; } // Check if dropped over a battle-zone const pNum = handDragPlayerIdx + 1; const battleZone = document.getElementById('battle-zone-' + pNum); if (battleZone) { const rect = battleZone.getBoundingClientRect(); if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) { // Play the card if (typeof playCardFromHand === 'function') { playCardFromHand(handDragPlayerIdx, handDragCardIdx); } } } isDraggingFromHand = false; handDragPlayerIdx = null; handDragCardIdx = null; }); // ============================================================ // PHASE TRACKER // ============================================================ function renderPhaseTracker() { if (!gameState) return; const track = document.getElementById('phase-track'); if (!track) return; const currentPhase = PHASES[gameState.currentPhaseIndex] || PHASES[0]; let html = ''; PHASES.forEach((phase, idx) => { const isActive = idx === gameState.currentPhaseIndex; const isDone = idx < gameState.currentPhaseIndex; let cls = 'phase-step'; if (isActive) cls += ' active'; if (isDone) cls += ' done'; let style = ''; if (isActive) { style = 'style="background:' + hexToRgba(phase.color || '#5b8dd9', 0.25) + ';box-shadow:inset 0 0 0 1px ' + hexToRgba(phase.color || '#5b8dd9', 0.4) + '"'; } // If in combat and this is the combat phase, show current combat sub-phase let label = phase.short; if (idx === 2 && gameState.inCombat) { const combatPhase = COMBAT_PHASES[gameState.combatPhaseIndex] || COMBAT_PHASES[0]; label = combatPhase.short; } html += '
'; html += '' + (phase.icon || '') + ''; html += '' + label + ''; html += '
'; if (idx < PHASES.length - 1) { html += '
'; } }); track.innerHTML = html; } function handlePhaseClick(phaseIdx) { // Just provide info about the phase, don't jump to it if (!gameState) return; const phase = PHASES[phaseIdx]; if (phase) addLogEntry('Phase info: ' + phase.label + ' — ' + phase.tip); } function updateTurnDisplay() { if (!gameState) return; const turnEl = document.getElementById('turn-counter'); if (turnEl) turnEl.innerHTML = 'Turn ' + (gameState.turnNumber || 1) + ''; updateActivePlayerHighlight(); renderPhaseTracker(); } function updateStormCounter() { if (!gameState) return; const storm = gameState.stormCount || 0; const badge = document.getElementById('storm-badge'); if (badge) { badge.textContent = 'Storm: ' + storm; badge.style.display = storm > 0 ? 'block' : 'none'; } } // ============================================================ // MANA POOL DISPLAY // ============================================================ function renderManaPool(playerIdx) { // mana pool display removed (Phase 1) } // ============================================================ // CONTEXT MENU // ============================================================ let contextMenuTargetId = null; function showContextMenu(x, y, cardId) { contextMenuTargetId = cardId; const menu = document.getElementById('context-menu'); if (!menu) return; menu.classList.add('visible'); const cardData = gameState && gameState.battlefieldCards.find(c => c.id === cardId); const _isEquip = cardData && typeof isEquipment === 'function' && isEquipment(cardData); const _isAuraCard = cardData && typeof isAura === 'function' && isAura(cardData); const _isPlaneswalker = cardData && cardData.typeLine && /planeswalker/i.test(cardData.typeLine); const hasAbilities = cardData && typeof parseActivatedAbilities === 'function' && parseActivatedAbilities(cardData).length > 0; menu.innerHTML = '' + '' + '
' + '
' + '' + '' + '' + '
' + '
' + (hasAbilities ? '' : '') + (_isPlaneswalker ? '' : '') + (_isEquip ? '' : '') + '' + '
' + '' + '' + '' + '' + '' + '
' + '' + ''; // Position const maxX = window.innerWidth - 200; const maxY = window.innerHeight - 360; menu.style.left = Math.min(x, maxX) + 'px'; menu.style.top = Math.min(y, maxY) + 'px'; } function hideContextMenu() { const menu = document.getElementById('context-menu'); if (menu) menu.classList.remove('visible'); } // Close context menu on outside click document.addEventListener('click', e => { const menu = document.getElementById('context-menu'); if (menu && !menu.contains(e.target)) hideContextMenu(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') { hideContextMenu(); if (typeof cancelStackPickMode === 'function') cancelStackPickMode(); if (typeof cancelEquipPickMode === 'function') cancelEquipPickMode(); closeZoneModal(); } }); // ============================================================ // FLOATING CARD PREVIEW // ============================================================ function showFloatingCardPreview(imgSrc, e, cardInfo) { if (!imgSrc) return; const preview = document.getElementById('card-preview'); if (!preview) return; if (floatingPreviewTimeout) { clearTimeout(floatingPreviewTimeout); floatingPreviewTimeout = null; } const img = preview.querySelector('img'); if (img) img.src = imgSrc; const infoDiv = preview.querySelector('.card-preview-info'); if (infoDiv && cardInfo) { const nameEl = preview.querySelector('.card-preview-name'); const typeEl = preview.querySelector('.card-preview-type'); const oracleEl = preview.querySelector('.card-preview-oracle'); const ptEl = preview.querySelector('.card-preview-pt'); if (nameEl) nameEl.textContent = cardInfo.name || ''; if (typeEl) typeEl.textContent = cardInfo.typeLine || ''; if (oracleEl) oracleEl.textContent = cardInfo.oracleText || ''; if (ptEl) { const pt = cardInfo.pt || (cardInfo.power && cardInfo.toughness ? cardInfo.power + '/' + cardInfo.toughness : ''); ptEl.textContent = pt; } if (cardInfo.name || cardInfo.typeLine || cardInfo.oracleText) { infoDiv.style.display = 'block'; } else { infoDiv.style.display = 'none'; } } preview.style.display = 'block'; positionFloatingPreview(e); } function positionFloatingPreview(e) { const preview = document.getElementById('card-preview'); if (!preview || preview.style.display === 'none') return; const pw = preview.offsetWidth || 220; const ph = preview.offsetHeight || 340; const ww = window.innerWidth; const wh = window.innerHeight; let px = e.clientX + 16; let py = e.clientY - 20; if (px + pw > ww - 10) px = e.clientX - pw - 16; if (py + ph > wh - 10) py = wh - ph - 10; if (py < 50) py = 50; preview.style.left = px + 'px'; preview.style.top = py + 'px'; } function hideFloatingCardPreview() { floatingPreviewTimeout = setTimeout(() => { const preview = document.getElementById('card-preview'); if (preview) preview.style.display = 'none'; }, 80); } // ============================================================ // ZONE MODAL // ============================================================ function openZoneModal(playerId, zone) { zoneModalPlayerId = playerId; activeZoneTab = zone || 'graveyard'; renderZoneModal(); const modal = document.getElementById('zone-modal'); if (modal) modal.classList.add('visible'); } function closeZoneModal() { const modal = document.getElementById('zone-modal'); if (modal) modal.classList.remove('visible'); zoneModalPlayerId = null; if (_zoneCardCtx) _zoneCardCtx.classList.remove('visible'); } function switchZoneTab(zone) { activeZoneTab = zone; renderZoneModal(); } function renderZoneModal() { if (zoneModalPlayerId === null || !gameState) return; const player = gameState.players[zoneModalPlayerId]; if (!player) return; const titleEl = document.getElementById('zone-modal-title'); if (titleEl) titleEl.textContent = player.name + "'s Zones"; // Render tabs const tabsEl = document.getElementById('zone-modal-tabs'); if (tabsEl) { const zones = [ { key: 'graveyard', icon: '☠', label: 'Graveyard' }, { key: 'exile', icon: '⬡', label: 'Exile' }, { key: 'command', icon: '👑', label: 'Command' }, { key: 'hand', icon: '✋', label: 'Hand' }, { key: 'library', icon: '📚', label: 'Library' }, ]; tabsEl.innerHTML = zones.map(z => { const count = (player.zones[z.key] || []).length; const isActive = z.key === activeZoneTab; return ''; }).join(''); } const body = document.getElementById('zone-modal-body'); if (!body) return; const cards = player.zones[activeZoneTab] || []; let actionButtons = ''; if (activeZoneTab === 'library') { actionButtons = '' + '' + '' + ''; } if (cards.length === 0) { body.innerHTML = (actionButtons ? '
' + actionButtons + '
' : '') + '
No cards in ' + activeZoneTab + '.
'; return; } let gridHtml = '
'; cards.forEach((card, idx) => { const imgSrc = card.imageUrl || ''; const safeName = (card.name || 'Unknown').replace(/'/g, "\\'").replace(/"/g, '"'); const safeType = (card.typeLine || '').replace(/'/g, "\\'"); gridHtml += '
' + (imgSrc ? '' + safeName + '' : '
' + safeName + '
') + '
' + (card.name || 'Unknown') + '
' + '
'; }); gridHtml += '
'; const infoBar = '
' + '' + cards.length + ' card' + (cards.length !== 1 ? 's' : '') + '' + '
' + actionButtons + '
' + '
'; body.innerHTML = infoBar + gridHtml; } // Zone card context menu function showZoneCardCtx(x, y, playerId, zone, cardIndex) { if (_zoneCardCtx) _zoneCardCtx.classList.remove('visible'); if (!_zoneCardCtx) { _zoneCardCtx = document.createElement('div'); _zoneCardCtx.className = 'zone-card-ctx'; _zoneCardCtx.id = 'zone-card-ctx'; _zoneCardCtx.style.cssText = 'position:fixed;z-index:1100;background:var(--color-surface-raised);' + 'border:1px solid var(--color-border-strong);border-radius:var(--radius-lg);' + 'padding:6px;box-shadow:var(--shadow-lg);min-width:180px;'; document.body.appendChild(_zoneCardCtx); } const player = gameState && gameState.players[playerId]; const card = player && player.zones[zone] ? player.zones[zone][cardIndex] : null; if (!card) return; const items = [ { icon: '⚔', label: 'Play to Battlefield', action: 'battlefield' }, { icon: '✋', label: 'Return to Hand', action: 'hand', skip: zone === 'hand' }, { divider: true }, { icon: '☠', label: 'To Graveyard', action: 'graveyard', skip: zone === 'graveyard' }, { icon: '⬡', label: 'To Exile', action: 'exile', skip: zone === 'exile' }, { icon: '👑', label: 'To Command Zone', action: 'command', skip: zone === 'command' }, { divider: true }, { icon: '📚', label: 'Top of Library', action: 'library-top' }, { icon: '📚', label: 'Bottom of Library', action: 'library-bottom' }, { divider: true }, { icon: '✕', label: 'Remove from Game', action: 'remove', danger: true }, ]; _zoneCardCtx.innerHTML = '
' + (card.name || 'Card') + '
'; items.forEach(item => { if (item.skip) return; if (item.divider) { _zoneCardCtx.innerHTML += '
'; return; } _zoneCardCtx.innerHTML += ''; }); _zoneCardCtx.style.left = Math.min(x, window.innerWidth - 200) + 'px'; _zoneCardCtx.style.top = Math.min(y, window.innerHeight - 300) + 'px'; _zoneCardCtx.classList.add('visible'); _zoneCardCtx.style.display = 'block'; } function hideZoneCardCtxMenu() { if (_zoneCardCtx) { _zoneCardCtx.classList.remove('visible'); _zoneCardCtx.style.display = 'none'; } } function handleZmCtxAction(playerId, zone, cardIndex, action) { pushUndoState(); const player = gameState && gameState.players[playerId]; if (!player) return; const cards = player.zones[zone]; if (!cards || cardIndex < 0 || cardIndex >= cards.length) return; const card = cards[cardIndex]; if (action === 'battlefield') { cards.splice(cardIndex, 1); if (zone === 'command' && gameState.rulesEngine && gameState.rulesEngine.commanderTaxEnabled) { player.commanderCastCount = (player.commanderCastCount || 0) + 1; } addCardToBattlefield(card.name, card.imageUrl, card.typeLine, playerId, undefined, undefined, card.oracleText || '', { manaCost: card.manaCost || '', power: card.power || '', toughness: card.toughness || '', pt: card.pt || '', cmc: card.cmc || 0, loyalty: card.loyalty || '' }); addLogEntry('' + card.name + ' moved from ' + zone + ' to battlefield'); } else if (action === 'hand') { cards.splice(cardIndex, 1); player.zones.hand.push(card); addLogEntry('' + card.name + ' returned to ' + player.name + "'s hand"); } else if (['graveyard', 'exile', 'command'].includes(action)) { cards.splice(cardIndex, 1); player.zones[action].push(card); addLogEntry('' + card.name + ' moved to ' + action); } else if (action === 'library-top') { cards.splice(cardIndex, 1); player.zones.library.unshift(card); addLogEntry('' + card.name + ' put on top of library'); } else if (action === 'library-bottom') { cards.splice(cardIndex, 1); player.zones.library.push(card); addLogEntry('' + card.name + ' put on bottom of library'); } else if (action === 'remove') { cards.splice(cardIndex, 1); addLogEntry('' + card.name + ' removed from game'); } updatePlayerZones(); renderZoneModal(); renderHandArea(); } // ============================================================ // HAND AREA — full hand display (used in hand-area section, NOT the zone thumbnails) // For the new layout, hand is rendered as thumbnails in the zone.hand element // ============================================================ function renderHandArea() { if (!gameState) return; // In the quadrant layout, hand is shown as thumbnails in each player's zone gameState.players.forEach((_, idx) => renderHandZone(idx)); } // ============================================================ // SETUP SCREEN // ============================================================ function initSetupScreen() { let selectedPlayerCount = 4; let startingLife = 40; const playerColors = [ { color: '#c0392b', name: 'P1 (Crimson)' }, { color: '#2980b9', name: 'P2 (Sapphire)' }, { color: '#27ae60', name: 'P3 (Emerald)' }, { color: '#d4930d', name: 'P4 (Gold)' }, ]; const setupEl = document.getElementById('setup-screen'); if (!setupEl) return; setupEl.innerHTML = '
' + '
Commander
' + '
4-Player Battlefield Simulator
' + '
' + '
Players
' + '
' + [2, 3, 4].map(n => '' ).join('') + '
' + '
' + '
' + '
Player Names
' + '
' + playerColors.map((pc, i) => '
' + '
' + '' + '
' ).join('') + '
' + '
' + '
' + '
AI Opponents
' + '
' + '' + '
' + '
' + '
Starting Life
' + '
' + '' + '40' + '' + '
' + '
' + '
' + '
Decks
' + '
' + 'Checking backend...' + '
' + '
' + '
' + '
' + '' + '' + '' + '
' + '' + '
'; // Init deck assignment grid updateDeckAssignmentGrid(4); // Expose helpers to window // Track AI selections locally var _localAiSet = new Set(); var _localAiDiff = 'easy'; function _renderAiToggles() { var grid = document.getElementById('ai-toggle-grid'); if (!grid) return; grid.innerHTML = ''; for (var i = 0; i < selectedPlayerCount; i++) { (function(idx) { var isAi = _localAiSet.has(idx); var btn = document.createElement('button'); btn.style.cssText = 'display:flex;align-items:center;gap:5px;padding:5px 12px;border-radius:6px;cursor:pointer;font-size:12px;' + 'border:1px solid ' + (isAi ? 'rgba(91,141,217,0.4)' : 'rgba(255,255,255,0.1)') + ';' + 'background:' + (isAi ? 'rgba(91,141,217,0.15)' : 'transparent') + ';' + 'color:' + (isAi ? 'rgba(255,255,255,0.9)' : 'rgba(255,255,255,0.5)') + ';'; btn.innerHTML = '' + (isAi ? '🤖 AI' : '👤 Human') + ' P' + (idx + 1) + ''; btn.setAttribute('onclick', '(function(){})()'); btn.addEventListener('click', function() { if (!_localAiSet.has(idx)) { var humanCount = selectedPlayerCount - _localAiSet.size; if (humanCount <= 1) return; _localAiSet.add(idx); var nameInp = document.getElementById('player-name-' + idx); if (nameInp && (!nameInp.value.trim() || nameInp.value.startsWith('Player '))) { nameInp.value = 'AI Bot ' + (idx + 1); } } else { _localAiSet.delete(idx); var nameInp2 = document.getElementById('player-name-' + idx); if (nameInp2 && nameInp2.value.startsWith('AI Bot')) nameInp2.value = ''; } _renderAiToggles(); }); grid.appendChild(btn); })(i); } var diffRow = document.getElementById('ai-difficulty-row'); if (diffRow) diffRow.style.display = _localAiSet.size > 0 ? 'flex' : 'none'; } window._setAiDiff = function(el, diff) { _localAiDiff = diff; document.querySelectorAll('.ai-diff-btn').forEach(function(b) { b.style.background = b.getAttribute('data-diff') === diff ? 'rgba(91,141,217,0.2)' : 'transparent'; b.style.color = b.getAttribute('data-diff') === diff ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.5)'; b.classList.toggle('active', b.getAttribute('data-diff') === diff); }); }; window.selectPlayerCount = function(n) { selectedPlayerCount = n; document.querySelectorAll('.player-count-btn').forEach((btn, i) => { btn.classList.toggle('active', [2, 3, 4][i] === n); }); // Hide/show player fields for (let i = 0; i < 4; i++) { const field = document.getElementById('player-field-' + i); if (field) field.style.display = i < n ? '' : 'none'; } // Clear AI selections for removed players _localAiSet.forEach(function(idx) { if (idx >= n) _localAiSet.delete(idx); }); _renderAiToggles(); updateDeckAssignmentGrid(n); }; window.adjustSetupLife = function(delta) { startingLife = Math.max(10, Math.min(999, startingLife + delta)); const el = document.getElementById('starting-life-display'); if (el) el.textContent = startingLife; }; window.startGame = function() { const playerNames = []; for (let i = 0; i < selectedPlayerCount; i++) { const inp = document.getElementById('player-name-' + i); playerNames.push(inp ? inp.value.trim() || ((_localAiSet.has(i)) ? 'AI Bot ' + (i + 1) : 'Player ' + (i + 1)) : 'Player ' + (i + 1)); } // Sync AI config to engine globals setupAiPlayers = _localAiSet; setupAiDifficulty = _localAiDiff; setupPlayerCount = selectedPlayerCount; setupStartingLife = startingLife; // Sync player names to engine globals for (let i = 0; i < playerNames.length; i++) { var nameInput = document.getElementById('player-name-' + i); if (nameInput) nameInput.value = playerNames[i]; } // If any deck choices were made, use startGameWithDecks (async deck loading) var hasDecks = typeof setupDeckChoices !== 'undefined' && Object.keys(setupDeckChoices).length > 0; if (hasDecks && typeof startGameWithDecks === 'function') { startGameWithDecks(); } else { initGame(selectedPlayerCount, playerNames, startingLife); } }; // Trigger display update for player count window.selectPlayerCount(4); } function updateDeckAssignmentGrid(playerCount) { var grid = document.getElementById('deck-assignment-grid'); if (!grid) return; grid.innerHTML = ''; var playerColors = [ { color: '#c0392b', name: 'Crimson' }, { color: '#2980b9', name: 'Sapphire' }, { color: '#27ae60', name: 'Emerald' }, { color: '#d4930d', name: 'Gold' }, ]; for (var i = 0; i < playerCount; i++) { var isAi = typeof setupAiPlayers !== 'undefined' && setupAiPlayers.has ? setupAiPlayers.has(i) : (typeof _localAiSet !== 'undefined' && _localAiSet.has(i)); var nameInput = document.getElementById('player-name-' + i); var playerName = (nameInput && nameInput.value.trim()) || (isAi ? ('AI Bot ' + (i + 1)) : ('Player ' + (i + 1))); var panel = document.createElement('div'); panel.className = 'deck-select-panel'; panel.id = 'deck-panel-' + i; if (isAi) { panel.innerHTML = '
' + '
' + '' + playerName + '' + 'AI' + '
' + '
AI Deck (auto-generated)
'; } else { panel.innerHTML = '
' + '
' + '' + playerName + '' + 'No deck selected' + '
' + '
' + '' + '' + '' + '
' + '
' + _buildPreconTabContent(i) + '
'; } grid.appendChild(panel); } // If backend is connected, load precons from API _loadBackendPrecons(); } function _buildPreconTabContent(playerIdx) { // Local precons from precons.js (PRECON_DATA) var preconList = (typeof PRECON_DATA !== 'undefined') ? PRECON_DATA : []; if (preconList.length === 0) { return '
No precon data loaded.
Connect to Commander-AI-Lab backend for 163+ precons.
'; } var html = '
' + '' + '
'; // Group by set var sets = {}; preconList.forEach(function(p) { var setName = p.set || 'Other'; if (!sets[setName]) sets[setName] = []; sets[setName].push(p); }); html += '
'; Object.keys(sets).sort().forEach(function(setName) { html += '
'; html += '
'; html += '\u25b8 ' + setName + ' (' + sets[setName].length + ')'; html += '
'; html += '
'; }); html += '
'; return html; } async function _loadBackendPrecons() { if (typeof AIBridge === 'undefined' || !AIBridge.connector.connected) return; try { var precons = await AIBridge.deckSource.fetchPrecons(); if (precons && precons.length > 0) { // Merge backend precons into PRECON_DATA so the UI can render them window.PRECON_DATA = precons.map(function(p) { return { id: (p.fileName || p.name || '').replace('.dck', ''), name: p.name || p.fileName || '', commander: p.commander || (p.commanders && p.commanders[0]) || '', set: p.set || '', fileName: p.fileName || '', colors: p.colors || [], year: p.year || 0 }; }); // Re-render the precon tab content for all visible player panels var panels = document.querySelectorAll('[id^="deck-content-"]'); panels.forEach(function(panel) { var idx = parseInt(panel.id.replace('deck-content-', '')); if (!isNaN(idx)) { var activeTab = panel.closest('.deck-select-panel'); var activeBtn = activeTab ? activeTab.querySelector('.deck-tab.active') : null; // Only re-render if Precons tab is active (or no tab selected yet) if (!activeBtn || activeBtn.getAttribute('data-tab') === 'precons') { panel.innerHTML = _buildPreconTabContent(idx); } } }); // Update status indicator var indicator = document.getElementById('backend-status-indicator'); if (indicator) { indicator.textContent = '\u26a1 ' + precons.length + ' precons from backend'; indicator.className = 'backend-status connected'; } } } catch (e) { console.warn('[_loadBackendPrecons] Error:', e); } } function switchDeckTab(playerIdx, tab) { var tabs = document.querySelectorAll('#deck-tabs-' + playerIdx + ' .deck-tab'); tabs.forEach(function(t) { t.classList.toggle('active', t.getAttribute('data-tab') === tab); }); var content = document.getElementById('deck-content-' + playerIdx); if (!content) return; switch (tab) { case 'precons': content.innerHTML = _buildPreconTabContent(playerIdx); break; case 'edhrec': content.innerHTML = '
' + '
' + '' + '' + '
' + '
' + '
Enter a commander name to find EDHREC recommendations.
' + 'EDHREC integration coming soon.
' + '
' + '
'; break; case 'mydecks': content.innerHTML = '
' + '
' + '
Loading user decks...
' + '
' + '
'; _loadMyDecksTab(playerIdx); break; } } function _loadMyDecksTab(playerIdx) { var listEl = document.getElementById('mydecks-list-' + playerIdx); if (!listEl) return; var apiBase = (typeof LAB_API_BASE !== 'undefined' && LAB_API_BASE) ? LAB_API_BASE : ''; fetch(apiBase + '/api/lab/decks') .then(function(res) { return res.json(); }) .then(function(data) { var decks = (data.decks || []).filter(function(d) { return d.source === 'deckbuilder'; }); if (decks.length === 0) { listEl.innerHTML = '
No saved decks. Use the Deck Builder or Auto Gen to create one.
'; return; } var html = ''; decks.forEach(function(d) { var name = d.name || 'Untitled'; var cmdr = d.commander || d.commander_name || ''; var count = d.card_count || d.total_cards || 0; html += ''; }); listEl.innerHTML = html; }) .catch(function(e) { console.warn('[_loadMyDecksTab] Error:', e); listEl.innerHTML = '
Could not load decks. Is the server running?
'; }); } function selectUserDeck(playerIdx, deckId, deckName) { var statusEl = document.getElementById('deck-status-' + playerIdx); if (statusEl) { statusEl.textContent = 'Exporting...'; statusEl.className = 'deck-select-status'; } // Highlight selected button var listEl = document.getElementById('mydecks-list-' + playerIdx); if (listEl) { listEl.querySelectorAll('.deck-precon-item').forEach(function(b) { b.classList.remove('selected'); }); // Find the clicked button listEl.querySelectorAll('.deck-precon-item').forEach(function(b) { if (b.querySelector('.deck-precon-name') && b.querySelector('.deck-precon-name').textContent === deckName) { b.classList.add('selected'); } }); } // Export to .dck then store the choice var apiBase = (typeof LAB_API_BASE !== 'undefined' && LAB_API_BASE) ? LAB_API_BASE : ''; fetch(apiBase + '/api/decks/' + deckId + '/export-to-sim', { method: 'POST' }) .then(function(res) { return res.json(); }) .then(function(result) { if (result.success) { if (typeof setupDeckChoices !== 'undefined') { setupDeckChoices[playerIdx] = { type: 'user-deck', deckName: result.deckName, fileName: result.deckName + '.dck' }; } if (statusEl) { statusEl.textContent = deckName + ' (' + (result.totalCards || '?') + ' cards)'; statusEl.className = 'deck-select-status loaded'; } } else { if (statusEl) { statusEl.textContent = 'Export failed'; statusEl.className = 'deck-select-status'; } } }) .catch(function(e) { console.error('[selectUserDeck] Export failed:', e); if (statusEl) { statusEl.textContent = 'Export error'; statusEl.className = 'deck-select-status'; } }); } function filterPrecons(playerIdx, query) { var list = document.getElementById('precon-list-' + playerIdx); if (!list) return; var q = query.toLowerCase().trim(); list.querySelectorAll('.deck-precon-item').forEach(function(item) { var name = item.querySelector('.deck-precon-name')?.textContent?.toLowerCase() || ''; var cmdr = item.querySelector('.deck-precon-cmdr')?.textContent?.toLowerCase() || ''; var match = !q || name.includes(q) || cmdr.includes(q); item.style.display = match ? '' : 'none'; }); // Auto-expand groups that have visible items list.querySelectorAll('.deck-set-group').forEach(function(group) { var items = group.querySelector('.deck-set-items'); var hasVisible = items && Array.from(items.querySelectorAll('.deck-precon-item')) .some(function(it) { return it.style.display !== 'none'; }); if (items && q) { items.style.display = hasVisible ? '' : 'none'; } }); } function toggleDeckSet(headerEl) { var items = headerEl.nextElementSibling; if (!items) return; var isOpen = items.style.display !== 'none'; items.style.display = isOpen ? 'none' : ''; var arrow = headerEl.querySelector('.deck-set-arrow'); if (arrow) arrow.textContent = isOpen ? '\u25b8' : '\u25be'; } function selectPreconDeck(playerIdx, preconId) { var panel = document.getElementById('deck-panel-' + playerIdx); if (panel) { panel.querySelectorAll('.deck-precon-item').forEach(function(item) { item.classList.remove('selected'); }); var selected = panel.querySelector('[data-precon-id="' + preconId + '"]'); if (selected) selected.classList.add('selected'); } var status = document.getElementById('deck-status-' + playerIdx); var preconList = (typeof PRECON_DATA !== 'undefined') ? PRECON_DATA : []; var precon = preconList.find(function(p) { return p.id === preconId; }); if (status) { status.textContent = precon ? (precon.name || preconId) + ' selected' : preconId + ' selected'; status.className = 'deck-select-status selected'; } if (typeof setupDeckChoices === 'undefined') window.setupDeckChoices = {}; setupDeckChoices[playerIdx] = { type: 'precon', preconId: preconId }; } function searchEdhrec(playerIdx) { var input = document.getElementById('edhrec-search-' + playerIdx); var results = document.getElementById('edhrec-results-' + playerIdx); if (!input || !results) return; var query = input.value.trim(); if (!query) return; results.innerHTML = '
Searching EDHREC for "' + query + '"...
'; if (typeof AIBridge !== 'undefined' && AIBridge.connector.connected) { AIBridge.deckSource.fetchEdhrecDecks(query).then(function(decks) { if (decks.length === 0) { results.innerHTML = '
No results for "' + query + '".
' + 'EDHREC integration is coming soon.
'; } else { var html = ''; decks.forEach(function(d) { html += ''; }); results.innerHTML = html; } }); } else { results.innerHTML = '
EDHREC search requires Commander-AI-Lab backend.
' + 'Start the backend: python lab_api.py
'; } } function importDeckFromTextarea(playerIdx) { var textarea = document.getElementById('deck-import-' + playerIdx); if (!textarea) return; var text = textarea.value.trim(); if (!text) return; var status = document.getElementById('deck-status-' + playerIdx); if (status) { status.textContent = 'Decklist imported'; status.className = 'deck-select-status selected'; } if (typeof setupDeckChoices === 'undefined') window.setupDeckChoices = {}; setupDeckChoices[playerIdx] = { type: 'import', text: text }; } // ============================================================ // GAME INITIALIZATION // ============================================================ function initGame(playerCount, playerNames, startingLife) { gameState = createInitialState(playerCount, playerNames, startingLife); gameState.inGame = true; // Hide setup screen const setup = document.getElementById('setup-screen'); if (setup) setup.classList.add('hidden'); // Show battlefield const bf = document.getElementById('game-battlefield'); if (bf) bf.style.display = 'block'; // Update player labels and life counters in the quadrant layout for (let i = 0; i < 4; i++) { const pNum = i + 1; const player = gameState.players[i]; const area = document.querySelector('.player-area.p' + pNum); // Update player label const labelEl = area && area.querySelector('.player-label'); if (labelEl) { labelEl.textContent = player ? player.name : ''; labelEl.style.display = player ? '' : 'none'; } // Show/hide based on player count if (area) { area.style.display = i < playerCount ? '' : 'none'; } if (player) { // Set life counter const lifeEl = document.getElementById('life-' + i); if (lifeEl) lifeEl.textContent = player.life; } } // NOTE: Precon deck loading is handled by the "Begin Battle" button // which calls startGameWithDecks() when setupDeckChoices has entries. // The old deck-sel-{i} dropdown path is removed. // Mark AI players setupAiPlayers.forEach(function(idx) { if (gameState.players[idx]) { gameState.players[idx].isAI = true; } }); gameState.aiDifficulty = setupAiDifficulty; // Initialize phase tracker renderPhaseTracker(); updateTurnDisplay(); renderPlayerPanels(); renderManaPool(0); var aiCount = 0; gameState.players.forEach(function(p) { if (p.isAI) aiCount++; }); addLogEntry('🎮 Game started! ' + playerCount + ' players, ' + startingLife + ' starting life.' + (aiCount > 0 ? ' (' + aiCount + ' AI on ' + setupAiDifficulty + ')' : '')); addLogEntry('' + gameState.players[0].name + ' begins the game.'); // Build section areas for each battle zone buildQuadrantBattleZones(); // Show mana pool display const manaDisplay = document.getElementById('mana-pool-display'); if (manaDisplay) manaDisplay.style.display = 'flex'; // If first player is AI, start their turn after a short delay if (gameState.players[0] && gameState.players[0].isAI && typeof runAiTurn === 'function') { setTimeout(function() { runAiTurn(); }, 1200); } } function buildQuadrantBattleZones() { // Each player's battle-zone div is already in the HTML // Just ensure they have position:relative and overflow:hidden for (let i = 0; i < 4; i++) { const pNum = i + 1; const bz = document.getElementById('battle-zone-' + pNum); if (bz) { bz.style.position = 'relative'; bz.style.overflow = 'hidden'; } } } function loadPreconDeck(playerIdx, precon) { if (!gameState || !precon) return; const player = gameState.players[playerIdx]; if (!player) return; player.commanderCardName = precon.commander || ''; // Parse decklist text if available (PRECON_DATA format) const deckText = precon.decklist || ''; const cards = precon.cards || []; // Parse from decklist text const lines = deckText.split('\n'); const parsedCards = []; let commanderName = precon.commander || ''; let inCommanderSection = false; lines.forEach(line => { line = line.trim(); if (!line || line.startsWith('//')) { inCommanderSection = line.toLowerCase().includes('commander'); return; } const match = line.match(/^(\d+)\s+(.+)$/); if (match) { const qty = parseInt(match[1]); const name = match[2].trim(); if (inCommanderSection || name === commanderName) { commanderName = name; inCommanderSection = false; } else { for (let q = 0; q < qty; q++) parsedCards.push(name); } } }); // Fall back to precon.cards array const cardNames = parsedCards.length > 0 ? parsedCards : cards.filter(c => c !== commanderName); if (cardNames.length > 0 || commanderName) { // Commander to command zone if (commanderName) { player.zones.command.push({ name: commanderName, imageUrl: '', typeLine: 'Legendary Creature', oracleText: '', }); } // Library cardNames.forEach(cardName => { player.zones.library.push({ name: cardName, imageUrl: '', typeLine: '', oracleText: '', }); }); // Shuffle library for (let i = player.zones.library.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [player.zones.library[i], player.zones.library[j]] = [player.zones.library[j], player.zones.library[i]]; } // Draw opening hand (7 cards) for (let d = 0; d < 7 && player.zones.library.length > 0; d++) { player.zones.hand.push(player.zones.library.shift()); } updateZoneCountsForPlayer(playerIdx); renderCommanderZone(playerIdx); renderHandZone(playerIdx); addLogEntry('' + player.name + ' loaded deck: ' + precon.name + ' (' + commanderName + ')'); } } // ============================================================ // SEARCH SIDEBAR // ============================================================ let searchDebounceTimer = null; function initSearchSidebar() { const input = document.getElementById('card-search-input'); if (!input) return; input.addEventListener('input', () => { clearTimeout(searchDebounceTimer); const q = input.value.trim(); if (q.length < 2) { setSearchStatus('Type to search cards...'); document.getElementById('card-results').innerHTML = ''; return; } searchDebounceTimer = setTimeout(() => searchCards(q), 300); }); input.addEventListener('keydown', e => { if (e.key === 'Enter') { clearTimeout(searchDebounceTimer); const q = input.value.trim(); if (q.length >= 1) searchCards(q); } }); } function setSearchStatus(text) { const el = document.getElementById('search-status'); if (el) el.textContent = text; } function showSearchSpinner(show) { const el = document.getElementById('card-results'); if (!el) return; if (show) { el.innerHTML = '
'; setSearchStatus('Searching...'); } } function searchCards(query) { if (!query) return; showSearchSpinner(true); const url = 'https://api.scryfall.com/cards/search?q=' + encodeURIComponent(query) + '&unique=cards&order=name&page=1'; fetch(url) .then(r => r.json()) .then(data => { if (data.data) { renderSearchResults(data.data.slice(0, 20)); setSearchStatus(data.total_cards + ' results'); } else { setSearchStatus('No results found'); document.getElementById('card-results').innerHTML = '
No cards found for "' + query + '"
'; } }) .catch(() => { setSearchStatus('Search failed'); document.getElementById('card-results').innerHTML = '
Search failed. Check internet connection.
'; }); } function getCardImage(card) { if (card.image_uris) return card.image_uris.normal || card.image_uris.small; if (card.card_faces && card.card_faces[0] && card.card_faces[0].image_uris) { return card.card_faces[0].image_uris.normal || card.card_faces[0].image_uris.small; } return ''; } function renderSearchResults(cards) { const container = document.getElementById('card-results'); if (!container) return; container.innerHTML = ''; cards.forEach(card => { const imgUrl = getCardImage(card); const item = document.createElement('div'); item.className = 'search-card-item'; item.innerHTML = (imgUrl ? '' + card.name + '' : '
' + card.name + '
') + '
' + '
' + card.name + '
' + '
' + (card.type_line || '') + '
' + '
' + ''; // Add to battlefield on click const addCard = () => { if (!gameState) return; const ownerIdx = gameState.currentPlayerIndex; addCardToBattlefield( card.name, imgUrl, card.type_line || '', ownerIdx, undefined, undefined, card.oracle_text || card.card_faces?.[0]?.oracle_text || '', { manaCost: card.mana_cost || '', power: card.power || '', toughness: card.toughness || '', pt: (card.power && card.toughness) ? card.power + '/' + card.toughness : '', cmc: card.cmc || 0, loyalty: card.loyalty || '', } ); }; item.querySelector('.search-card-add-btn').onclick = e => { e.stopPropagation(); addCard(); }; item.addEventListener('mouseenter', e => { if (imgUrl) showFloatingCardPreview(imgUrl, e, { name: card.name, typeLine: card.type_line || '', oracleText: card.oracle_text || (card.card_faces && card.card_faces[0] ? card.card_faces[0].oracle_text : '') || '', manaCost: card.mana_cost || '', power: card.power, toughness: card.toughness, }); }); item.addEventListener('mousemove', positionFloatingPreview); item.addEventListener('mouseleave', hideFloatingCardPreview); container.appendChild(item); }); } // ============================================================ // TRIGGER BADGE & STACK PANEL // ============================================================ function updateTriggerBadge() { if (!gameState) return; const pending = (gameState.triggerStack || []).filter(t => !t.resolved); const btn = document.getElementById('btn-triggers'); if (!btn) return; btn.textContent = pending.length > 0 ? '❇ ' + pending.length : '❇'; btn.classList.toggle('has-triggers', pending.length > 0); } function openTriggerStack() { const panel = document.getElementById('trigger-stack-panel'); if (panel) { panel.classList.add('visible'); renderTriggerStack(); } } function closeTriggerStack() { const panel = document.getElementById('trigger-stack-panel'); if (panel) panel.classList.remove('visible'); } function toggleTriggerStack() { const panel = document.getElementById('trigger-stack-panel'); if (panel) { if (panel.classList.contains('visible')) closeTriggerStack(); else openTriggerStack(); } } function renderTriggerStack() { const body = document.getElementById('trigger-stack-body'); if (!body || !gameState) return; const pending = (gameState.triggerStack || []).filter(t => !t.resolved); if (pending.length === 0) { body.innerHTML = '
No pending triggers.
'; return; } body.innerHTML = pending.map((trigger, idx) => { const typeIcon = (typeof TRIGGER_TYPE_ICONS !== 'undefined' && TRIGGER_TYPE_ICONS[trigger.type]) || '❇'; const typeLabel = (typeof TRIGGER_TYPE_LABELS !== 'undefined' && TRIGGER_TYPE_LABELS[trigger.type]) || trigger.type; const ownerColor = (gameState.players[trigger.ownerIndex] || {}).color || '#888'; return '
' + '
' + typeIcon + '
' + '
' + '
' + (trigger.cardName || 'Unknown') + '
' + '
' + typeLabel + '
' + '
' + (trigger.oracleSnippet || '') + '
' + '
' + '
' + '' + '
'; }).join(''); updateTriggerBadge(); } // ============================================================ // UNDO/REDO BUTTONS // ============================================================ function updateUndoRedoButtons() { const undoBtn = document.getElementById('btn-undo'); const redoBtn = document.getElementById('btn-redo'); if (undoBtn) undoBtn.disabled = undoStack.length === 0; if (redoBtn) redoBtn.disabled = redoStack.length === 0; if (undoHistoryVisible) renderUndoHistory(); } function renderUndoHistory() { const panel = document.getElementById('undo-history-panel'); if (!panel) return; // Simple render of last 20 undo states const items = undoStack.slice(-20).reverse(); panel.innerHTML = '
' + 'Undo History
' + '
' + items.map((snap, i) => '
' + (snap._description || 'State #' + (undoStack.length - i)) + '
').join('') + '
'; } function closeUndoHistory() { undoHistoryVisible = false; const panel = document.getElementById('undo-history-panel'); if (panel) panel.classList.remove('visible'); } // ============================================================ // DICE MODAL // ============================================================ function openDiceModal() { const modal = document.getElementById('dice-modal'); if (modal) modal.classList.add('visible'); } function closeDiceModal() { const modal = document.getElementById('dice-modal'); if (modal) modal.classList.remove('visible'); } function rollDice(sides) { const result = Math.floor(Math.random() * sides) + 1; const display = document.getElementById('dice-result-display'); const label = document.getElementById('dice-label'); if (display) { display.textContent = result; display.classList.remove('rolling'); void display.offsetWidth; // Reflow display.classList.add('rolling'); } if (label) label.textContent = 'd' + sides + ' Roll'; addLogEntry('🎲 Rolled d' + sides + ': ' + result + ''); } function flipCoin() { const result = Math.random() < 0.5 ? 'Heads' : 'Tails'; const display = document.getElementById('dice-result-display'); const label = document.getElementById('dice-label'); if (display) { display.textContent = result === 'Heads' ? 'H' : 'T'; display.classList.remove('rolling'); void display.offsetWidth; display.classList.add('rolling'); } if (label) label.textContent = 'Coin Flip'; addLogEntry('🪙 Coin flip: ' + result + ''); } // ============================================================ // TOAST NOTIFICATIONS // ============================================================ function showToast(message) { const toast = document.createElement('div'); toast.className = 'toast-message'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.4s'; setTimeout(() => toast.remove(), 450); }, 2800); } // ============================================================ // MANA POOL DISPLAY BUTTON // ============================================================ function showManaPool() { // mana pool display removed (Phase 1) } // ============================================================ // SHORTCUT HINT // ============================================================ let _shortcutHintTimeout = null; function showShortcutHint() { const hint = document.querySelector('.shortcut-hint'); if (!hint) return; const hints = [ 'N next phase T next turn Space tap/untap', 'U undo D draw card S search cards', 'G dice roll Esc close panels', ]; hint.innerHTML = hints[Math.floor(Math.random() * hints.length)]; hint.classList.add('show'); clearTimeout(_shortcutHintTimeout); _shortcutHintTimeout = setTimeout(() => hint.classList.remove('show'), 4000); } // ============================================================ // KEYBOARD SHORTCUTS // ============================================================ function setupKeyboardShortcuts() { document.addEventListener('keydown', e => { // Don't fire when typing in inputs if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return; if (!gameState || !gameState.inGame) return; switch (e.key.toLowerCase()) { case 'n': if (!e.ctrlKey && !e.metaKey) { nextPhase(); e.preventDefault(); } break; case 't': if (!e.ctrlKey && !e.metaKey) { nextTurn(); e.preventDefault(); } break; case 'u': if (!e.ctrlKey) { undo(); e.preventDefault(); } break; case 'y': if (!e.ctrlKey) { redo(); e.preventDefault(); } break; case 'd': drawCard(gameState.currentPlayerIndex); break; case 's': { const sidebar = document.getElementById('card-search-sidebar'); if (sidebar) { sidebar.classList.toggle('collapsed'); const inp = document.getElementById('card-search-input'); if (inp && !sidebar.classList.contains('collapsed')) { inp.focus(); } } break; } case 'g': openDiceModal(); break; case 'escape': { hideContextMenu(); closeZoneModal(); closeDiceModal(); if (typeof cancelStackPickMode === 'function') cancelStackPickMode(); if (typeof cancelEquipPickMode === 'function') cancelEquipPickMode(); break; } } if (e.ctrlKey && e.key === 'z') { undo(); e.preventDefault(); } if (e.ctrlKey && e.key === 'y') { redo(); e.preventDefault(); } }); } // ============================================================ // COMBAT OVERLAY (SVG arrows) // ============================================================ function refreshCombatOverlay() { const svg = document.getElementById('combat-arrows-svg'); if (!svg || !gameState || !gameState.combat) { return; } svg.innerHTML = ''; const combat = gameState.combat; if (!combat.attackers || combat.attackers.length === 0) return; // Draw arrows from attacker cards to the target player quadrant combat.attackers.forEach(atk => { const attackerEl = document.getElementById('bc-' + atk.cardId); if (!attackerEl) return; const targetPNum = atk.targetPlayerId + 1; const targetArea = document.querySelector('.player-area.p' + targetPNum); if (!targetArea) return; const svgRect = svg.getBoundingClientRect(); const atkRect = attackerEl.getBoundingClientRect(); const tgtRect = targetArea.getBoundingClientRect(); const x1 = atkRect.left + atkRect.width / 2 - svgRect.left; const y1 = atkRect.top + atkRect.height / 2 - svgRect.top; const x2 = tgtRect.left + tgtRect.width / 2 - svgRect.left; const y2 = tgtRect.top + tgtRect.height / 2 - svgRect.top; const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); line.setAttribute('x1', x1); line.setAttribute('y1', y1); line.setAttribute('x2', x2); line.setAttribute('y2', y2); line.setAttribute('stroke', 'rgba(211,32,42,0.6)'); line.setAttribute('stroke-width', '2'); line.setAttribute('stroke-dasharray', '6,3'); line.setAttribute('marker-end', 'url(#arrowhead)'); svg.appendChild(line); }); } function updateCombatArrows() { refreshCombatOverlay(); } function showCombatOverlay() { refreshCombatOverlay(); } // ============================================================ // SETTINGS PANEL // ============================================================ function toggleSettingsPanel() { const panel = document.getElementById('settings-panel'); if (panel) panel.classList.toggle('visible'); if (panel && panel.classList.contains('visible')) renderSettingsPanel(); } function renderSettingsPanel() { if (!gameState) return; const body = document.getElementById('settings-body'); if (!body) return; const re = gameState.rulesEngine || {}; body.innerHTML = '
Auto Untap' + renderToggle('autoUntap', re.autoUntap) + '
' + '
Auto Draw' + renderToggle('autoDraw', re.autoDraw) + '
' + '
SBA Checks' + renderToggle('sbaChecks', re.sbaChecks) + '
' + '
Commander Tax' + renderToggle('commanderTaxEnabled', re.commanderTaxEnabled) + '
' + '
Auto Resolve Triggers' + renderToggle('autoResolveTriggers', re.autoResolveTriggers) + '
' + '
Enforce Hand Size' + renderToggle('enforceHandSize', re.enforceHandSize) + '
' + '
Auto Eliminate on SBA' + renderToggle('autoEliminateOnSBA', re.autoEliminateOnSBA) + '
'; } function renderToggle(key, value) { return ''; } function toggleSetting(key) { if (!gameState) return; if (key === 'manaAutoClear') { gameState.manaAutoClear = !gameState.manaAutoClear; } else if (gameState.rulesEngine && gameState.rulesEngine[key] !== undefined) { gameState.rulesEngine[key] = !gameState.rulesEngine[key]; } renderSettingsPanel(); addLogEntry('Setting ' + key + ' toggled'); } // ============================================================ // PLACEHOLDER STUBS for engine-called UI functions // (engine.js calls these — we provide no-op or simple implementations) // ============================================================ function renderPlayerPanelsNoOp() { renderPlayerPanels(); } function showManaShortfallWarning(cardName, shortfall, playerIndex) { showToast('Mana shortfall for ' + cardName + ': need ' + shortfall + ' more mana'); } function closeManaShortfallModal() {} function renderMulliganUI() {} // Mulligan handled by engine - UI prompt via showToast function openDeckView(playerId) { openZoneModal(playerId, 'library'); } function closeDeckView() { closeZoneModal(); } function renderDeckView() { renderZoneModal(); } function openFullLogModal() { showToast('Full log: ' + _logEntries.length + ' entries'); } function closeFullLogModal() {} function renderFullLog() {} function renderTimerDisplay() {} function updateTimerDisplay(playerId) {} function renderAllTimerDisplays() {} function flashTimerWarning(playerId) {} function renderBattlefieldZoneLabels() {} function renderZonePanel() { updatePlayerZones(); } function renderZonePanelBody() { renderZoneModal(); } function showZpContextMenu(x, y, player, zone, cardIndex) { showZoneCardCtx(x, y, player, zone, cardIndex); } function showHandContextMenu(x, y, playerId, handIndex) { // Show context menu for hand card if (!gameState) return; const player = gameState.players[playerId]; if (!player || !player.zones.hand[handIndex]) return; showZoneCardCtx(x, y, playerId, 'hand', handIndex); } function renderDeckStats() {} function updateDeckSummary(playerId) {} function updateDeckImportProgress(loaded, total, message) {} function showPreconDetail(preconId) {} function closePreconDetail() {} function showManaPickerModal() {} function closeManaPickerModal() {} function updatePlayerNameFields() {} function updateAiToggleGrid() {} function renderDeckSelectionGrid() {} function renderTrainingUI() {} function openAiTrainingModal() {} function closeAiTrainingModal() {} function renderDeckTesterProgress() {} function renderDeckTesterResults() {} function openDeckTester() {} function closeDeckTester() {} function renderDeckTesterSetup() {} function updateMonarchBadges() { renderPlayerPanels(); } function openShortcutsModal() {} function closeShortcutsModal() {} function renderShortcutsContent() {} function openGuideModal() {} function closeGuideModal() {} function renderAiThoughtBubble(playerIdx, thought) {} function showAiCastPopup(card, aiPlayerIdx, handIndex, aiTarget) {} function closeSaveLoadModal() {} function renderCollectionGrid() {} function renderWishlistGrid() {} function openCollectionAddModal() {} function closeCollectionAddModal() {} function renderDbResultGrid(cards) {} function addCardToDeck(card) {} function closeDbCtxMenu() {} function renderDbCommanderSlot() {} function renderDbList() {} function updateDbCountDisplay() {} function renderDbStats() {} function renderDbCurve() {} function renderDbColors() {} function renderDbTypes() {} function updateCardLegalityDots() {} function renderDbSavedDecks() {} function openDeckBuilderImport() {} function closeDbImportModal() {} function showImportProgress(current, total, text) {} function renderSettingsRulesTab() { renderSettingsPanel(); } function renderSettingsDisplayTab() {} function renderSettingsGameplayTab() {} function renderSettingsAITab() {} function updateDisplaySetting(key, value) {} function updateGameplaySetting(key, value) { toggleSetting(key); } function updateAIDifficulty(diff) {} function updateAISetting(key, value) {} function renderStackPanel() {} function updateUnifiedStack() {} function openPriorityWindow(reason, onAllPass) {} function closePriorityWindow() {} function renderPriorityBanner() {} function showPriorityActionMode() {} function hidePriorityBanner() {} function renderLegalActionsBar() {} function showStackBlockedWarning() { addLogEntry('⚠ Resolve all stack items before advancing.'); } function isPriorityBlocking() { return false; } function isPriorityEnabled() { return gameState && gameState.rulesEngine && gameState.rulesEngine.useStack; } function showTargetingWarningBadge(targetCardId, sourceOwnerId) {} function showColorIdentityWarning(cardName, violations) { showToast('Color identity warning: ' + cardName); } function showCommanderZonePrompt(cardId, cardData, intendedZone) { // Auto-send to command zone if (typeof moveCardToZone === 'function') moveCardToZone(cardId, 'command'); } function showDiscardPrompt(playerId, count) { addLogEntry('' + (gameState.players[playerId] || {}).name + ' must discard ' + count + ' card(s).'); } function closeDiscardPrompt() {} function toggleRulesPanel() { toggleSettingsPanel(); } function closeRulesPanel() { document.getElementById('settings-panel') && document.getElementById('settings-panel').classList.remove('visible'); } function renderTurnHistory() {} function updateBattlefieldSectionCounts() {} function buildBattlefieldSections() {} function buildOwnerFilterButtons() {} function applyBattlefieldFilter() {} function autoArrangeBattlefield() { if (!gameState) return; gameState.battlefieldCards.forEach((card, idx) => { const pNum = card.ownerIndex + 1; const bz = document.getElementById('battle-zone-' + pNum); if (!bz) return; const cols = Math.ceil(Math.sqrt(gameState.battlefieldCards.filter(c => c.ownerIndex === card.ownerIndex).length)); const col = idx % cols; const row = Math.floor(idx / cols); card.x = col * 110 + 5; card.y = row * 145 + 5; const el = document.getElementById('bc-' + card.id); if (el) { el.style.left = card.x + 'px'; el.style.top = card.y + 'px'; } }); } function setupBattlefieldDrop() {} function showDamagePreview() {} function renderDamagePreviewContent() {} function showFloatingDamage(elementId, amount, type) { const el = document.getElementById(elementId); if (!el) return; const damage = document.createElement('div'); damage.className = 'floating-damage'; damage.textContent = (type === 'loss' ? '-' : '+') + amount; damage.style.color = type === 'loss' ? '#e74c3c' : '#2ecc71'; damage.style.left = (el.offsetLeft + el.offsetWidth / 2) + 'px'; damage.style.top = (el.offsetTop - 20) + 'px'; el.parentNode.appendChild(damage); setTimeout(() => damage.remove(), 1200); } function updateDamageBadge(cardData) { const el = document.getElementById('bc-' + cardData.id); if (!el) return; let badge = el.querySelector('.damage-badge'); if (cardData.damageMarked > 0) { if (!badge) { badge = document.createElement('div'); badge.className = 'damage-badge'; el.appendChild(badge); } badge.textContent = cardData.damageMarked; badge.style.display = 'flex'; } else if (badge) { badge.style.display = 'none'; } } function showCombatRecap(logLines) { if (logLines && logLines.length > 0) { logLines.forEach(line => addLogEntry(line)); } } function showPlaneswalkerAbilities(cardId) {} function closePlaneswalkerAbilities() {} function openScryModal(playerId, count) { showScryDialog(playerId); } function closeScryModal() {} function renderScryZones() {} function renderScryZone(containerId, cards, zone) {} function openTokenModal(playerId) { const modal = document.getElementById('token-modal'); if (modal) modal.classList.add('visible'); } function closeTokenModal() { const modal = document.getElementById('token-modal'); if (modal) modal.classList.remove('visible'); } function updateTokenPreview() {} function addToTokenHistory(entry) {} function renderTokenHistory() {} function renderTokenBrowserResults(tokens) {} function updateTokenSummaryBar() {} function openDeckModal(playerId) { const modal = document.getElementById('deck-modal'); if (modal) { modal.classList.add('visible'); renderDeckModalContent(playerId); } } function closeDeckModal() { const modal = document.getElementById('deck-modal'); if (modal) modal.classList.remove('visible'); } function renderDeckModalContent(playerId) { const body = document.getElementById('deck-modal-body'); if (!body || !gameState) return; const player = gameState.players[playerId]; if (!player) return; body.innerHTML = '

' + player.name + "'s Deck Manager

" + '
' + '
Commander: ' + (player.commanderCardName || 'Not set') + '
' + '
Library: ' + player.zones.library.length + ' cards | Hand: ' + player.zones.hand.length + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
'; } window.importDeckFromTextarea = function(playerId) { const textarea = document.getElementById('deck-import-text-' + playerId); if (!textarea || !gameState) return; if (typeof importDeckList === 'function') { importDeckList(playerId, textarea.value); } else { showToast('Deck import processing...'); } }; function openPreconBrowser() { showToast('Precon browser: select a deck from the setup screen dropdown.'); } // ============================================================ // DOM UTILITY — updateCardCounterBadge // ============================================================ function updateCardCounterBadge(cardId) { if (!gameState) return; const card = gameState.battlefieldCards.find(c => c.id === cardId); if (!card) return; const el = document.getElementById('bc-' + cardId); if (!el) return; // Re-render counter badges const existingStrip = el.querySelector('.card-counters-strip'); if (existingStrip) existingStrip.remove(); if (card.counters) { const cTypes = Object.keys(card.counters).filter(k => card.counters[k] > 0); if (cTypes.length > 0) { const strip = document.createElement('div'); strip.className = 'card-counters-strip'; cTypes.forEach(cType => { const cCount = card.counters[cType]; const ct = (typeof COUNTER_TYPES !== 'undefined' && COUNTER_TYPES[cType]) || { icon: '\u25CF', color: '#9ca3af', label: cType }; const badge = document.createElement('span'); badge.className = 'card-counter-badge'; badge.style.cssText = 'background:' + ct.color + ';color:#fff;'; if (cType === '+1/+1') badge.textContent = '+' + cCount + '/+' + cCount; else if (cType === '-1/-1') badge.textContent = '-' + cCount + '/-' + cCount; else badge.textContent = ct.icon + cCount; strip.appendChild(badge); }); el.appendChild(strip); } } } // ============================================================ // DOM — toggleTap visual update // ============================================================ function _refreshCardEl(cardId) { if (!gameState) return; const card = gameState.battlefieldCards.find(c => c.id === cardId); if (!card) return; const el = document.getElementById('bc-' + cardId); if (!el) return; if (card.tapped) el.classList.add('tapped'); else el.classList.remove('tapped'); } // ============================================================ // DOMContentLoaded — wire up everything // ============================================================ document.addEventListener('DOMContentLoaded', () => { initSetupScreen(); initSearchSidebar(); setupKeyboardShortcuts(); // Sidebar toggle const sidebarTab = document.getElementById('sidebar-edge-tab'); if (sidebarTab) { sidebarTab.onclick = () => { const sidebar = document.getElementById('card-search-sidebar'); if (sidebar) sidebar.classList.toggle('collapsed'); }; } // Close modal clicks document.getElementById('zone-modal') && document.getElementById('zone-modal').addEventListener('click', e => { if (e.target.id === 'zone-modal') closeZoneModal(); }); document.getElementById('dice-modal') && document.getElementById('dice-modal').addEventListener('click', e => { if (e.target.id === 'dice-modal') closeDiceModal(); }); document.getElementById('token-modal') && document.getElementById('token-modal').addEventListener('click', e => { if (e.target.id === 'token-modal') closeTokenModal(); }); document.getElementById('deck-modal') && document.getElementById('deck-modal').addEventListener('click', e => { if (e.target.id === 'deck-modal') closeDeckModal(); }); });