From 022f89478dca70e06965d3bd005718a1a5b51282 Mon Sep 17 00:00:00 2001 From: rgriffith Date: Fri, 3 Apr 2026 23:25:01 -0400 Subject: [PATCH] fix: restore ui.js from placeholder corruption + Phase 1 patches --- lan-client/ui.js | 2311 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 2310 insertions(+), 1 deletion(-) diff --git a/lan-client/ui.js b/lan-client/ui.js index 384eba5..af9668c 100644 --- a/lan-client/ui.js +++ b/lan-client/ui.js @@ -1 +1,2310 @@ -CONTENT_FROM_VARIABLE \ No newline at end of file +/** + * 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) { + // 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 = + '
' + + '
' + + '
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() { + // 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(); + }); +});