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 =
- '
';
- // 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 +=
- '
' +
- '

' +
- '
';
- } 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 +
- '
' +
- '' +
- 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 + '
') +
- '
' + (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
' +
- '
' +
- '
' +
- 'Difficulty:' +
- '' +
- '' +
- '' +
- '
' +
- '
' +
-
- '
' +
- '
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 =
- '' +
- 'AI Deck (auto-generated)
';
- } else {
- panel.innerHTML =
- '' +
- '' +
- '' +
- '' +
- '' +
- '
' +
- '' +
- _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 += '
';
- sets[setName].forEach(function(p) {
- 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.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