diff --git a/lan-client/ui.js b/lan-client/ui.js index 4cf7ab2..f82b326 100644 --- a/lan-client/ui.js +++ b/lan-client/ui.js @@ -1,2335 +1 @@ -/** - * 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; - -// ============================================================ -// 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 exile count - const exCount = document.getElementById('exile-count-' + pNum); - if (exCount) { - exCount.textContent = player.zones.exile.length; - exCount.style.display = player.zones.exile.length > 0 ? 'block' : 'none'; - } - - // Update graveyard count - const gvCount = document.getElementById('gv-count-' + pNum); - if (gvCount) { - gvCount.textContent = player.zones.graveyard.length; - gvCount.style.display = player.zones.graveyard.length > 0 ? 'block' : 'none'; - } - - // Update hand count - const handCount = document.getElementById('hand-count-' + pNum); - if (handCount) handCount.textContent = player.zones.hand.length; -} - -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'; - } -} - -// ============================================================ -// 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'; - card.style.borderColor = playerColor; - - 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); - } - } -}); - -// ============================================================ -// 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) { - if (!gameState) return; - const pool = getManaPool(playerIdx !== undefined ? playerIdx : gameState.currentPlayerIndex); - const display = document.getElementById('mana-pool-display'); - if (!display) return; - - const colors = ['W', 'U', 'B', 'R', 'G', 'C']; - let html = 'MANA:'; - let hasAny = false; - - colors.forEach(c => { - if (pool[c] > 0) { - hasAny = true; - html += '' + pool[c] + ''; - } - }); - - if (!hasAny) { - html += 'Empty'; - } - - html += ''; - display.innerHTML = html; - display.style.display = gameState.inGame ? 'flex' : 'none'; -} - -// ============================================================ -// 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 = - '
' + - '
' + - '
Import a decklist:
' + - '' + - '' + - '
' + - '
'; - break; - } -} - -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() { - const display = document.getElementById('mana-pool-display'); - if (display) { - display.style.display = display.style.display === 'none' ? 'flex' : 'none'; - } -} - -// ============================================================ -// 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) + '
' + - '
Mana Auto-Clear' + renderToggle('manaAutoClear', gameState.manaAutoClear) + '
'; -} - -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(); - }); -}); +{ "placeholder": "will be replaced" } \ No newline at end of file