New HTML battlefield to test features before Unity (#182)

This commit is contained in:
KoalaTrapLord authored and GitHub committed 2026-04-03 04:39:33 -04:00
1 parent 7a40b5deb8
commit eed2c7f126
7 files changed
+31727

No files matched your search

+25492
View File
File diff suppressed because it is too large. Load diff
+634
View File
@@ -0,0 +1,634 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MTG Commander Battlefield</title>
<link rel="stylesheet" href="style.css?v=20260403_0300">
</head>
<body>
<!-- ====================================================
SETUP SCREEN
==================================================== -->
<div id="setup-screen">
<!-- Populated by ui.js initSetupScreen() -->
</div>
<!-- ====================================================
TABLE SURFACE — 4-Quadrant Battlefield
==================================================== -->
<div class="table-surface" id="game-battlefield" style="display:none;">
<!-- TOP BAR: Phase tracker + controls -->
<div id="top-bar">
<span class="top-bar-logo">Commander</span>
<div class="turn-counter-badge" id="turn-counter">Turn <span>1</span></div>
<div class="active-player-indicator" id="active-player-indicator">
<span class="active-player-dot" id="active-dot" style="background:#c0392b;"></span>
Player 1's Turn
</div>
<!-- Phase track -->
<div class="phase-track-wrapper">
<div class="phase-track" id="phase-track"></div>
</div>
<!-- Phase/turn action buttons -->
<div class="top-bar-actions">
<button class="btn-phase" onclick="nextPhase()" title="Advance to next phase (N)">Next Phase</button>
<button class="btn-phase btn-next-turn" onclick="nextTurn()" title="End turn, advance to next player (T)">Next Turn</button>
</div>
<!-- Utility buttons -->
<div class="top-bar-utils">
<button class="btn-util" id="btn-undo" onclick="undo()" title="Undo (Ctrl+Z)" disabled>&#x21A9;</button>
<button class="btn-util" id="btn-redo" onclick="redo()" title="Redo (Ctrl+Y)" disabled>&#x21AA;</button>
<button class="btn-util" id="btn-triggers" onclick="toggleTriggerStack()" title="Trigger Stack">&#x2747;</button>
<button class="btn-util" onclick="openDiceModal()" title="Dice &amp; Coins (G)">&#x1F3B2;</button>
<button class="btn-util" onclick="checkStateBasedActions()" title="Check State-Based Actions">SBA</button>
<button class="btn-util" onclick="toggleSettingsPanel()" title="Settings">&#x2699;</button>
<button class="btn-util" id="btn-search-toggle" onclick="document.getElementById('card-search-sidebar').classList.toggle('collapsed')" title="Card Search (S)">&#x1F50D;</button>
</div>
<!-- Storm counter -->
<span id="storm-badge" style="display:none;font-size:10px;color:var(--mtg-gold);background:rgba(200,168,67,0.15);border:1px solid rgba(200,168,67,0.3);border-radius:4px;padding:2px 7px;white-space:nowrap;">Storm: 0</span>
</div>
<!-- DIVIDER LINES -->
<div class="divider-h"></div>
<div class="divider-v"></div>
<!-- CORNER ACCENTS -->
<div class="corner-accent tl"></div>
<div class="corner-accent tr"></div>
<div class="corner-accent bl"></div>
<div class="corner-accent br"></div>
<!-- CENTER EMBLEM -->
<div class="center-emblem">
<svg viewBox="0 0 100 100" fill="none">
<circle cx="50" cy="50" r="42" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<circle cx="50" cy="50" r="28" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>
<polygon points="50,15 58.5,37 82,37 63,51 69,74 50,60 31,74 37,51 18,37 41.5,37"
fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="0.8"/>
</svg>
</div>
<!-- ================================================
4-QUADRANT BATTLEFIELD
================================================ -->
<div class="battlefield">
<!-- PLAYER 1: Top-Left (Crimson) -->
<div class="player-area p1" id="player-area-1">
<span class="player-label" id="player-label-1">Player 1</span>
<div class="life-counter" id="life-counter-1">
<button onclick="adjustLife(0,-1)" aria-label="Decrease P1 life">&#x2212;</button>
<span class="life-counter-value" id="life-0" onclick="showSetLifeDialog(0)">40</span>
<button onclick="adjustLife(0,1)" aria-label="Increase P1 life">+</button>
</div>
<div class="poison-badge" id="poison-0" style="display:none;" onclick="adjustPoison(0,1)" title="Poison counters (click +1, right-click -1)" oncontextmenu="event.preventDefault();adjustPoison(0,-1)">0 poison</div>
<!-- Zone Row: Library, Exile, Graveyard, Hand -->
<div class="zone-row" id="zone-row-1">
<div class="zone library" id="lib-zone-1" data-player="1" data-zone="library"
onclick="openZoneModal(0,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-1">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-1" data-player="1" data-zone="exile"
onclick="openZoneModal(0,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-1" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-1" data-player="1" data-zone="graveyard"
onclick="openZoneModal(0,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-1" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-1" data-player="1" data-zone="hand"
onclick="openZoneModal(0,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-1">0</span>)</span>
</div>
</div>
<!-- Commander Row -->
<div class="commander-row" id="cmd-row-1">
<div class="zone commander" id="cmd-zone-1" data-player="1" data-zone="command"
onclick="openZoneModal(0,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<!-- Battle Zone -->
<div class="battle-zone" id="battle-zone-1">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
<!-- Battlefield cards injected here -->
</div>
</div>
<!-- PLAYER 2: Top-Right (Sapphire) -->
<div class="player-area p2" id="player-area-2">
<span class="player-label" id="player-label-2">Player 2</span>
<div class="life-counter" id="life-counter-2">
<button onclick="adjustLife(1,-1)" aria-label="Decrease P2 life">&#x2212;</button>
<span class="life-counter-value" id="life-1" onclick="showSetLifeDialog(1)">40</span>
<button onclick="adjustLife(1,1)" aria-label="Increase P2 life">+</button>
</div>
<div class="poison-badge" id="poison-1" style="display:none;" onclick="adjustPoison(1,1)" oncontextmenu="event.preventDefault();adjustPoison(1,-1)">0 poison</div>
<div class="zone-row" id="zone-row-2">
<div class="zone library" id="lib-zone-2" data-player="2" data-zone="library"
onclick="openZoneModal(1,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-2">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-2" data-player="2" data-zone="exile"
onclick="openZoneModal(1,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-2" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-2" data-player="2" data-zone="graveyard"
onclick="openZoneModal(1,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-2" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-2" data-player="2" data-zone="hand"
onclick="openZoneModal(1,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-2">0</span>)</span>
</div>
</div>
<div class="commander-row" id="cmd-row-2">
<div class="zone commander" id="cmd-zone-2" data-player="2" data-zone="command"
onclick="openZoneModal(1,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="battle-zone" id="battle-zone-2">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
</div>
<!-- PLAYER 3: Bottom-Left (Emerald) -->
<div class="player-area p3" id="player-area-3">
<!-- Battle zone on top for bottom players -->
<div class="battle-zone" id="battle-zone-3">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
<div class="commander-row" id="cmd-row-3">
<div class="zone commander" id="cmd-zone-3" data-player="3" data-zone="command"
onclick="openZoneModal(2,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="zone-row" id="zone-row-3">
<div class="zone library" id="lib-zone-3" data-player="3" data-zone="library"
onclick="openZoneModal(2,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-3">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-3" data-player="3" data-zone="exile"
onclick="openZoneModal(2,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-3" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-3" data-player="3" data-zone="graveyard"
onclick="openZoneModal(2,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-3" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-3" data-player="3" data-zone="hand"
onclick="openZoneModal(2,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-3">0</span>)</span>
</div>
</div>
<span class="player-label" id="player-label-3">Player 3</span>
<div class="life-counter" id="life-counter-3">
<button onclick="adjustLife(2,-1)" aria-label="Decrease P3 life">&#x2212;</button>
<span class="life-counter-value" id="life-2" onclick="showSetLifeDialog(2)">40</span>
<button onclick="adjustLife(2,1)" aria-label="Increase P3 life">+</button>
</div>
<div class="poison-badge" id="poison-2" style="display:none;" onclick="adjustPoison(2,1)" oncontextmenu="event.preventDefault();adjustPoison(2,-1)">0 poison</div>
</div>
<!-- PLAYER 4: Bottom-Right (Gold) -->
<div class="player-area p4" id="player-area-4">
<div class="battle-zone" id="battle-zone-4">
<div class="battle-zone-inner"></div>
<span class="battle-zone-label">Battlefield</span>
</div>
<div class="commander-row" id="cmd-row-4">
<div class="zone commander" id="cmd-zone-4" data-player="4" data-zone="command"
onclick="openZoneModal(3,'command')" data-tooltip="Command Zone">
<span class="zone-icon">&#x1F451;</span>
<span class="zone-name">Commander</span>
</div>
</div>
<div class="zone-row" id="zone-row-4">
<div class="zone library" id="lib-zone-4" data-player="4" data-zone="library"
onclick="openZoneModal(3,'library')" data-tooltip="Library">
<span class="card-count" id="lib-count-4">0</span>
<span class="zone-icon">&#x1F4DA;</span>
<span class="zone-name">Library</span>
</div>
<div class="zone exile" id="exile-zone-4" data-player="4" data-zone="exile"
onclick="openZoneModal(3,'exile')" data-tooltip="Exile">
<span class="card-count" id="exile-count-4" style="display:none;">0</span>
<span class="zone-icon">&#x2B21;</span>
<span class="zone-name">Exile</span>
</div>
<div class="zone graveyard" id="gv-zone-4" data-player="4" data-zone="graveyard"
onclick="openZoneModal(3,'graveyard')" data-tooltip="Graveyard">
<span class="card-count" id="gv-count-4" style="display:none;">0</span>
<span class="zone-icon">&#x2620;</span>
<span class="zone-name">Grave</span>
</div>
<div class="zone hand" id="hand-zone-4" data-player="4" data-zone="hand"
onclick="openZoneModal(3,'hand')" data-tooltip="Hand">
<span class="zone-icon">&#x1F0CF;</span>
<span class="zone-name">Hand (<span id="hand-count-4">0</span>)</span>
</div>
</div>
<span class="player-label" id="player-label-4">Player 4</span>
<div class="life-counter" id="life-counter-4">
<button onclick="adjustLife(3,-1)" aria-label="Decrease P4 life">&#x2212;</button>
<span class="life-counter-value" id="life-3" onclick="showSetLifeDialog(3)">40</span>
<button onclick="adjustLife(3,1)" aria-label="Increase P4 life">+</button>
</div>
<div class="poison-badge" id="poison-3" style="display:none;" onclick="adjustPoison(3,1)" oncontextmenu="event.preventDefault();adjustPoison(3,-1)">0 poison</div>
</div>
</div><!-- /.battlefield -->
<!-- COMBAT SVG OVERLAY (arrows) -->
<svg id="combat-overlay" xmlns="http://www.w3.org/2000/svg" style="position:absolute;inset:44px 0 0 0;pointer-events:none;z-index:30;">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="rgba(211,32,42,0.7)"/>
</marker>
</defs>
<g id="combat-arrows-svg"></g>
</svg>
<!-- MANA POOL DISPLAY (floating, bottom center) -->
<div id="mana-pool-display" style="display:none;">
<span class="mana-pool-label">MANA:</span>
<span class="mana-empty">Empty</span>
</div>
<!-- FLOATING LOG -->
<div id="floating-log" class="floating-log"></div>
<!-- SHORTCUT HINT -->
<div class="shortcut-hint"></div>
<!-- CARD SEARCH SIDEBAR (right side, collapsible) -->
<div id="card-search-sidebar" class="collapsed">
<div class="sidebar-header">
<span class="sidebar-title">Card Search</span>
<button class="sidebar-toggle" onclick="document.getElementById('card-search-sidebar').classList.toggle('collapsed')" title="Close search">&#x2715;</button>
</div>
<div class="search-input-wrap">
<input class="search-input" id="card-search-input" type="text" placeholder="Search Scryfall..." autocomplete="off"/>
</div>
<div class="search-status" id="search-status">Type to search cards...</div>
<div class="card-results" id="card-results"></div>
</div>
<!-- Sidebar edge tab -->
<div id="sidebar-edge-tab" class="sidebar-edge-tab" title="Toggle card search">&#x2039;</div>
</div><!-- /.table-surface / #game-battlefield -->
<!-- ====================================================
CONTEXT MENU
==================================================== -->
<div id="context-menu"></div>
<!-- ====================================================
FLOATING CARD PREVIEW
==================================================== -->
<div id="card-preview">
<img src="" alt="Card Preview"/>
<div class="card-preview-pt-overlay" id="card-preview-pt-overlay"></div>
<div class="card-preview-info">
<div class="card-preview-name"></div>
<div class="card-preview-type"></div>
<div class="card-preview-oracle"></div>
<div class="card-preview-pt"></div>
</div>
</div>
<!-- ====================================================
ZONE MODAL
==================================================== -->
<div id="zone-modal">
<div class="zone-modal-card">
<div class="zone-modal-header">
<span class="zone-modal-title" id="zone-modal-title">Player Zones</span>
<button class="btn-close" onclick="closeZoneModal()">&#x2715;</button>
</div>
<div class="zone-modal-tabs" id="zone-modal-tabs">
<!-- Populated by renderZoneModal() -->
</div>
<div class="zone-modal-body" id="zone-modal-body">
<!-- Populated by renderZoneModal() -->
</div>
</div>
</div>
<!-- ====================================================
DICE MODAL
==================================================== -->
<div id="dice-modal">
<div class="dice-modal-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<span style="font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.08em;">Dice &amp; Coins</span>
<button class="btn-close" onclick="closeDiceModal()">&#x2715;</button>
</div>
<div class="dice-result-display" id="dice-result-display">&#x2014;</div>
<div class="dice-label" id="dice-label">Choose a die</div>
<div class="dice-buttons">
<button class="btn-dice" onclick="rollDice(4)">d4</button>
<button class="btn-dice" onclick="rollDice(6)">d6</button>
<button class="btn-dice" onclick="rollDice(8)">d8</button>
<button class="btn-dice" onclick="rollDice(10)">d10</button>
<button class="btn-dice" onclick="rollDice(12)">d12</button>
<button class="btn-dice" onclick="rollDice(20)">d20</button>
<button class="btn-dice" onclick="rollDice(100)">d100</button>
<button class="btn-dice coin" onclick="flipCoin()">Coin Flip</button>
</div>
</div>
</div>
<!-- ====================================================
TOKEN MODAL
==================================================== -->
<div id="token-modal">
<div class="token-modal-card">
<div class="modal-header">
<span class="modal-title">Create Token</span>
<button class="btn-close" onclick="closeTokenModal()">&#x2715;</button>
</div>
<div class="modal-body" id="token-modal-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px;">
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Token Name</label>
<input class="game-input" id="token-name" type="text" placeholder="e.g. 1/1 White Soldier"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Type Line</label>
<input class="game-input" id="token-type" type="text" placeholder="Creature — Soldier"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Power/Toughness</label>
<input class="game-input" id="token-pt" type="text" placeholder="1/1"/>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Count</label>
<input class="game-input" id="token-count" type="number" value="1" min="1" max="20"/>
</div>
</div>
<div style="margin-bottom:10px;">
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Keywords (comma-separated)</label>
<input class="game-input" id="token-keywords" type="text" placeholder="Flying, Vigilance"/>
</div>
<div style="margin-bottom:12px;">
<label style="font-size:11px;font-weight:600;color:var(--color-text);display:block;margin-bottom:4px;">Player</label>
<select class="game-input" id="token-player-select" style="height:28px;">
<option value="0">Player 1</option>
<option value="1">Player 2</option>
<option value="2">Player 3</option>
<option value="3">Player 4</option>
</select>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-primary" onclick="createTokenFromModal()">Create Token(s)</button>
<button class="btn btn-cancel" onclick="closeTokenModal()">Cancel</button>
</div>
</div>
</div>
</div>
<!-- ====================================================
DECK MODAL
==================================================== -->
<div id="deck-modal">
<div class="deck-modal-card">
<div class="modal-header">
<span class="modal-title">Deck Manager</span>
<button class="btn-close" onclick="closeDeckModal()">&#x2715;</button>
</div>
<div class="modal-body" id="deck-modal-body">
<!-- Populated by renderDeckModalContent() -->
</div>
</div>
</div>
<!-- ====================================================
COMMANDER DAMAGE MATRIX MODAL
==================================================== -->
<div id="cmd-dmg-matrix-modal">
<!-- Populated by renderCmdDmgMatrix() -->
</div>
<!-- ====================================================
TRIGGER STACK PANEL
==================================================== -->
<div id="trigger-stack-panel">
<div class="trigger-stack-card">
<div class="trigger-stack-header">
<span class="trigger-stack-title">Trigger Stack</span>
<button class="btn-close" style="font-size:14px;width:20px;height:20px;" onclick="closeTriggerStack()">&#x2715;</button>
</div>
<div class="trigger-stack-body" id="trigger-stack-body">
<div class="trigger-stack-empty">No pending triggers.</div>
</div>
<div class="trigger-stack-actions">
<button class="btn-resolve" onclick="resolveNextTrigger()">Resolve Next</button>
<button class="btn-resolve-all" onclick="resolveAllTriggers()">Resolve All</button>
</div>
</div>
</div>
<!-- ====================================================
SETTINGS PANEL
==================================================== -->
<div id="settings-panel">
<div class="modal-header" style="flex-shrink:0;">
<span class="modal-title">Settings</span>
<button class="btn-close" onclick="toggleSettingsPanel()">&#x2715;</button>
</div>
<div class="settings-body" id="settings-body" style="flex:1;overflow-y:auto;padding:12px;">
<!-- Populated by renderSettingsPanel() -->
</div>
<div style="padding:10px 14px;border-top:1px solid var(--color-border);display:flex;gap:8px;flex-wrap:wrap;flex-shrink:0;">
<button class="btn btn-primary" onclick="openDiceModal()">&#x1F3B2; Dice</button>
<button class="btn btn-gold" onclick="openCmdDmgMatrix()">CMD Damage Matrix</button>
<button class="btn btn-cancel" onclick="checkStateBasedActions()">Check SBA</button>
<button class="btn btn-cancel" onclick="clearAllManaPools()">Clear All Mana</button>
<button class="btn btn-cancel" onclick="autoArrangeBattlefield()">Auto-Arrange</button>
</div>
</div>
<!-- ====================================================
PRIORITY BANNER
==================================================== -->
<div id="priority-banner">
<span class="priority-text" id="priority-text">Waiting for priority...</span>
<button class="btn-priority-pass" onclick="passPriority()">Pass Priority</button>
<button class="btn-close" style="font-size:13px;" onclick="closePriorityWindow()">&#x2715;</button>
</div>
<!-- ====================================================
LEGAL ACTIONS BAR
==================================================== -->
<div id="legal-actions-bar">
<!-- Populated by renderLegalActionsBar() -->
</div>
<!-- ====================================================
UNDO HISTORY PANEL
==================================================== -->
<div id="undo-history-panel">
<!-- Populated by renderUndoHistory() -->
</div>
<!-- ====================================================
MANA COLOR PICKER (dynamically appended) -->
<!-- Zone card context menu (dynamically appended) -->
<!-- SBA alert panel (dynamically appended) -->
<!-- Set-life dialog (dynamically appended) -->
<!-- ====================================================
SCRIPTS
==================================================== -->
<script src="precons.js?v=20260403_0300"></script>
<script src="engine.js?v=20260403_0300"></script>
<script src="ui.js?v=20260403_0300"></script>
<script>
// ============================================================
// STARTUP & BRIDGE FUNCTIONS
// ============================================================
// Token creation from modal
function createTokenFromModal() {
if (!gameState) return;
const name = document.getElementById('token-name').value.trim() || '1/1 Token';
const typeLine = document.getElementById('token-type').value.trim() || 'Creature';
const pt = document.getElementById('token-pt').value.trim() || '1/1';
const count = parseInt(document.getElementById('token-count').value) || 1;
const keywords = document.getElementById('token-keywords').value.trim();
const playerIdx = parseInt(document.getElementById('token-player-select').value) || 0;
const parts = pt.split('/');
const power = parts[0] ? parts[0].trim() : '1';
const toughness = parts[1] ? parts[1].trim() : '1';
for (let i = 0; i < count; i++) {
addCardToBattlefield(name, '', typeLine, playerIdx, undefined, undefined,
keywords ? keywords.replace(/,/g, ' ') : '',
{ power, toughness, pt, isToken: true });
}
addLogEntry('Created ' + count + 'x <strong>' + name + '</strong> token(s) for Player ' + (playerIdx + 1));
closeTokenModal();
}
// Pass priority
function passPriority() {
addLogEntry('Priority passed.');
closePriorityWindow();
}
// Open precon browser (stub - opens deck modal for current player)
function openPreconBrowser() {
if (gameState) openDeckModal(gameState.currentPlayerIndex);
}
// Open collection modal (stub)
function openCollectionModal() { showToast('Collection tracker available in full version.'); }
// Scry dialog stub
function showScryDialog(playerId) {
if (!gameState) return;
const player = gameState.players[playerId];
if (!player || player.zones.library.length === 0) {
addLogEntry('!! ' + (player ? player.name : 'Player') + ' has no cards to scry.');
return;
}
const count = Math.min(parseInt(prompt('Scry how many? (1-7)', '1') || '1'), 7, player.zones.library.length);
if (isNaN(count) || count < 1) return;
const topCards = player.zones.library.slice(0, count);
const names = topCards.map(c => c.name || '?').join(', ');
addLogEntry('<strong>' + player.name + '</strong> scrys ' + count + ': [' + names + ']');
}
// Mill dialog stub
function showMillDialog(playerId) {
if (!gameState) return;
const count = parseInt(prompt('Mill how many cards?', '1') || '1');
if (isNaN(count) || count < 1) return;
millCards(playerId, count);
}
// Become monarch
function becomeMonarch(playerId) {
if (!gameState) return;
gameState.monarchPlayerIndex = playerId;
const player = gameState.players[playerId];
addLogEntry('&#x1F451; <strong>' + (player ? player.name : 'Player') + '</strong> becomes the Monarch!');
updateMonarchBadges();
}
// Empower the life counter right-click on the quadrant labels
document.addEventListener('DOMContentLoaded', function() {
// Wire right-click on life values to set-life dialog
for (var i = 0; i < 4; i++) {
(function(idx) {
var lifeEl = document.getElementById('life-' + idx);
if (lifeEl) {
lifeEl.addEventListener('contextmenu', function(e) {
e.preventDefault();
showSetLifeDialog(idx);
});
}
})(i);
}
// Life counter right-click on quadrant buttons too
// (already handled via inline onclick in HTML)
});
</script>
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
unzip mtg-commander-lan.zip
cd mtg-commander-lan
npm install
node server.js
+6
View File
@@ -0,0 +1,6 @@
{
"name": "MTG Commander (HTML)",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+3028
View File
File diff suppressed because it is too large. Load diff
+495
View File
@@ -0,0 +1,495 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Commander Battlefield</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@400;500;600&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--felt: #2a2a2a;
--felt-light: #333333;
--felt-dark: #1e1e1e;
--felt-line: rgba(255,255,255,0.08);
--felt-line-strong: rgba(255,255,255,0.15);
--p1: #c0392b; --p1-soft: rgba(192,57,43,0.15); --p1-glow: rgba(192,57,43,0.35); --p1-text: #e74c3c;
--p2: #2980b9; --p2-soft: rgba(41,128,185,0.15); --p2-glow: rgba(41,128,185,0.35); --p2-text: #3498db;
--p3: #27ae60; --p3-soft: rgba(39,174,96,0.15); --p3-glow: rgba(39,174,96,0.35); --p3-text: #2ecc71;
--p4: #d4930d; --p4-soft: rgba(212,147,13,0.15); --p4-glow: rgba(212,147,13,0.35); --p4-text: #f1c40f;
--card-w: 63px;
--card-h: 88px;
--zone-radius: 8px;
}
html, body {
width: 100%; height: 100%;
overflow: hidden;
font-family: 'Inter', sans-serif;
background: var(--felt-dark);
}
body {
display: flex;
align-items: center;
justify-content: center;
}
.table-surface {
position: relative;
width: 100vw;
height: 100vh;
background: radial-gradient(ellipse at 50% 50%, var(--felt-light) 0%, var(--felt) 50%, var(--felt-dark) 100%);
overflow: hidden;
}
.table-surface::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.08'/%3E%3C/svg%3E");
background-size: 200px 200px;
pointer-events: none;
z-index: 1;
opacity: 0.7;
}
.table-surface::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.45) 100%);
pointer-events: none;
z-index: 2;
}
.battlefield {
position: absolute;
inset: 10px;
z-index: 5;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 0;
}
.divider-h, .divider-v {
position: absolute;
z-index: 10;
pointer-events: none;
}
.divider-h {
top: 50%; left: 10px; right: 10px;
height: 2px;
transform: translateY(-50%);
background: var(--felt-line-strong);
box-shadow: 0 0 12px rgba(255,255,255,0.04);
}
.divider-v {
left: 50%; top: 10px; bottom: 10px;
width: 2px;
transform: translateX(-50%);
background: var(--felt-line-strong);
box-shadow: 0 0 12px rgba(255,255,255,0.04);
}
.center-emblem {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
z-index: 15;
width: 76px; height: 76px;
border-radius: 50%;
background: radial-gradient(circle, rgba(55,55,55,0.95), rgba(25,25,25,0.98));
border: 2px solid rgba(255,255,255,0.08);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 30px rgba(0,0,0,0.6), inset 0 0 20px rgba(0,0,0,0.3);
}
.center-emblem svg { width: 40px; height: 40px; }
.player-area {
position: relative;
display: flex;
flex-direction: column;
padding: 8px;
overflow: hidden;
}
.player-area.p1 { grid-area: 1 / 1 / 2 / 2; }
.player-area.p2 { grid-area: 1 / 2 / 2 / 3; }
.player-area.p3 { grid-area: 2 / 1 / 3 / 2; }
.player-area.p4 { grid-area: 2 / 2 / 3 / 3; }
.p1, .p2 { justify-content: flex-start; }
.p3, .p4 { justify-content: flex-end; }
.player-label {
font-family: 'Cinzel', serif;
font-weight: 700;
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
padding: 3px 10px;
border-radius: 4px;
position: absolute;
z-index: 20;
white-space: nowrap;
}
.p1 .player-label { top: 8px; left: 8px; color: var(--p1-text); background: var(--p1-soft); }
.p2 .player-label { top: 8px; right: 8px; color: var(--p2-text); background: var(--p2-soft); }
.p3 .player-label { bottom: 8px; left: 8px; color: var(--p3-text); background: var(--p3-soft); }
.p4 .player-label { bottom: 8px; right: 8px; color: var(--p4-text); background: var(--p4-soft); }
.life-counter {
position: absolute;
z-index: 20;
display: flex;
align-items: center;
gap: 5px;
font-family: 'Cinzel', serif;
font-size: 20px;
font-weight: 700;
}
.life-counter button {
width: 22px; height: 22px;
border-radius: 50%;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.04);
color: rgba(255,255,255,0.45);
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-family: 'Inter', sans-serif;
line-height: 1;
}
.life-counter button:hover {
background: rgba(255,255,255,0.12);
color: rgba(255,255,255,0.8);
}
.p1 .life-counter { top: 8px; right: 8px; color: var(--p1-text); }
.p2 .life-counter { top: 8px; left: 8px; color: var(--p2-text); }
.p3 .life-counter { bottom: 8px; right: 8px; color: var(--p3-text); }
.p4 .life-counter { bottom: 8px; left: 8px; color: var(--p4-text); }
.zone-row {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
.p1 .zone-row, .p3 .zone-row { flex-direction: row; }
.p2 .zone-row, .p4 .zone-row { flex-direction: row-reverse; }
.commander-row {
display: flex;
flex-shrink: 0;
margin: 4px 0;
}
.p1 .commander-row, .p3 .commander-row { justify-content: flex-start; }
.p2 .commander-row, .p4 .commander-row { justify-content: flex-end; }
.battle-zone {
flex: 1;
position: relative;
border-radius: var(--zone-radius);
min-height: 50px;
}
.battle-zone-inner {
position: absolute;
inset: 4px;
border-radius: var(--zone-radius);
border: 1px dashed;
opacity: 0.12;
pointer-events: none;
}
.p1 .battle-zone-inner { border-color: var(--p1); }
.p2 .battle-zone-inner { border-color: var(--p2); }
.p3 .battle-zone-inner { border-color: var(--p3); }
.p4 .battle-zone-inner { border-color: var(--p4); }
.battle-zone-label {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-family: 'Cinzel', serif;
font-size: 11px;
letter-spacing: 4px;
text-transform: uppercase;
opacity: 0.06;
pointer-events: none;
white-space: nowrap;
color: #fff;
}
.zone {
position: relative;
border-radius: var(--zone-radius);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: default;
transition: all 0.25s ease;
border: 1px solid rgba(255,255,255,0.05);
background: rgba(0,0,0,0.18);
overflow: hidden;
}
.zone:hover {
border-color: rgba(255,255,255,0.12);
background: rgba(0,0,0,0.28);
}
.zone-icon {
font-size: 16px;
opacity: 0.55;
margin-bottom: 2px;
pointer-events: none;
}
.zone-name {
font-size: 7.5px;
font-weight: 600;
letter-spacing: 1.2px;
text-transform: uppercase;
color: rgba(255,255,255,0.3);
pointer-events: none;
}
.zone.library, .zone.exile, .zone.graveyard {
width: var(--card-w);
height: var(--card-h);
}
.zone.hand {
flex: 1;
height: var(--card-h);
min-width: 80px;
flex-direction: row;
gap: 4px;
}
.zone.hand .zone-icon { margin-bottom: 0; margin-right: 3px; }
.zone.commander {
width: calc(var(--card-w) + 14px);
height: calc(var(--card-h) + 6px);
border-width: 2px;
}
.p1 .zone { border-color: rgba(192,57,43,0.12); }
.p1 .zone:hover { border-color: rgba(192,57,43,0.3); box-shadow: 0 0 12px var(--p1-soft); }
.p1 .zone.commander { border-color: rgba(192,57,43,0.25); background: var(--p1-soft); }
.p1 .zone-icon { color: var(--p1-text); }
.p2 .zone { border-color: rgba(41,128,185,0.12); }
.p2 .zone:hover { border-color: rgba(41,128,185,0.3); box-shadow: 0 0 12px var(--p2-soft); }
.p2 .zone.commander { border-color: rgba(41,128,185,0.25); background: var(--p2-soft); }
.p2 .zone-icon { color: var(--p2-text); }
.p3 .zone { border-color: rgba(39,174,96,0.12); }
.p3 .zone:hover { border-color: rgba(39,174,96,0.3); box-shadow: 0 0 12px var(--p3-soft); }
.p3 .zone.commander { border-color: rgba(39,174,96,0.25); background: var(--p3-soft); }
.p3 .zone-icon { color: var(--p3-text); }
.p4 .zone { border-color: rgba(212,147,13,0.12); }
.p4 .zone:hover { border-color: rgba(212,147,13,0.3); box-shadow: 0 0 12px var(--p4-soft); }
.p4 .zone.commander { border-color: rgba(212,147,13,0.25); background: var(--p4-soft); }
.p4 .zone-icon { color: var(--p4-text); }
.zone[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.88);
color: #ccc;
font-size: 10px;
padding: 3px 8px;
border-radius: 4px;
white-space: nowrap;
z-index: 50;
pointer-events: none;
}
.card-count {
position: absolute;
top: 3px;
right: 4px;
font-size: 8px;
font-weight: 600;
color: rgba(255,255,255,0.25);
background: rgba(0,0,0,0.3);
padding: 1px 4px;
border-radius: 8px;
}
.title-bar {
position: absolute;
top: 0; left: 0; right: 0;
z-index: 30;
text-align: center;
padding: 5px;
pointer-events: none;
}
.title-bar h1 {
font-family: 'Cinzel', serif;
font-size: 12px;
font-weight: 600;
letter-spacing: 5px;
text-transform: uppercase;
color: rgba(255,255,255,0.15);
}
.corner-accent {
position: absolute;
width: 24px; height: 24px;
z-index: 6;
pointer-events: none;
opacity: 0.12;
}
.corner-accent.tl { top: 6px; left: 6px; border-top: 2px solid var(--p1); border-left: 2px solid var(--p1); }
.corner-accent.tr { top: 6px; right: 6px; border-top: 2px solid var(--p2); border-right: 2px solid var(--p2); }
.corner-accent.bl { bottom: 6px; left: 6px; border-bottom: 2px solid var(--p3); border-left: 2px solid var(--p3); }
.corner-accent.br { bottom: 6px; right: 6px; border-bottom: 2px solid var(--p4); border-right: 2px solid var(--p4); }
@media (max-width: 768px) {
:root { --card-w: 42px; --card-h: 58px; }
.player-label { font-size: 7px; letter-spacing: 1px; padding: 2px 6px; }
.life-counter { font-size: 15px; }
.life-counter button { width: 18px; height: 18px; font-size: 11px; }
.zone-name { font-size: 6px; }
.zone-icon { font-size: 12px; }
.center-emblem { width: 46px; height: 46px; }
.center-emblem svg { width: 26px; height: 26px; }
.battle-zone-label { font-size: 8px; letter-spacing: 2px; }
.zone-row { gap: 3px; }
}
</style>
</head>
<body>
<div class="table-surface">
<div class="title-bar"><h1>Commander Battlefield</h1></div>
<div class="corner-accent tl"></div>
<div class="corner-accent tr"></div>
<div class="corner-accent bl"></div>
<div class="corner-accent br"></div>
<div class="divider-h"></div>
<div class="divider-v"></div>
<div class="center-emblem">
<svg viewBox="0 0 100 100" fill="none">
<circle cx="50" cy="50" r="42" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/>
<circle cx="50" cy="50" r="28" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>
<polygon points="50,15 58.5,37 82,37 63,51 69,74 50,60 31,74 37,51 18,37 41.5,37"
fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="0.8"/>
</svg>
</div>
<div class="battlefield">
<!-- PLAYER 1: Top-Left (Crimson) -->
<div class="player-area p1">
<span class="player-label">Player 1</span>
<div class="life-counter">
<button onclick="changeLife(1,-1)" aria-label="Decrease life">−</button>
<span id="life1">40</span>
<button onclick="changeLife(1,1)" aria-label="Increase life">+</button>
</div>
<div class="zone-row">
<div class="zone library" data-tooltip="Library"><span class="card-count">99</span><span class="zone-icon">📚</span><span class="zone-name">Library</span></div>
<div class="zone exile" data-tooltip="Exile"><span class="zone-icon">✦</span><span class="zone-name">Exile</span></div>
<div class="zone graveyard" data-tooltip="Graveyard"><span class="zone-icon">💀</span><span class="zone-name">Grave</span></div>
<div class="zone hand" data-tooltip="Hand"><span class="zone-icon">🃏</span><span class="zone-name">Hand</span></div>
</div>
<div class="commander-row">
<div class="zone commander" data-tooltip="Commander Zone"><span class="zone-icon">👑</span><span class="zone-name">Commander</span></div>
</div>
<div class="battle-zone"><div class="battle-zone-inner"></div><span class="battle-zone-label">Battlefield</span></div>
</div>
<!-- PLAYER 2: Top-Right (Sapphire) -->
<div class="player-area p2">
<span class="player-label">Player 2</span>
<div class="life-counter">
<button onclick="changeLife(2,-1)" aria-label="Decrease life">−</button>
<span id="life2">40</span>
<button onclick="changeLife(2,1)" aria-label="Increase life">+</button>
</div>
<div class="zone-row">
<div class="zone library" data-tooltip="Library"><span class="card-count">99</span><span class="zone-icon">📚</span><span class="zone-name">Library</span></div>
<div class="zone exile" data-tooltip="Exile"><span class="zone-icon">✦</span><span class="zone-name">Exile</span></div>
<div class="zone graveyard" data-tooltip="Graveyard"><span class="zone-icon">💀</span><span class="zone-name">Grave</span></div>
<div class="zone hand" data-tooltip="Hand"><span class="zone-icon">🃏</span><span class="zone-name">Hand</span></div>
</div>
<div class="commander-row">
<div class="zone commander" data-tooltip="Commander Zone"><span class="zone-icon">👑</span><span class="zone-name">Commander</span></div>
</div>
<div class="battle-zone"><div class="battle-zone-inner"></div><span class="battle-zone-label">Battlefield</span></div>
</div>
<!-- PLAYER 3: Bottom-Left (Emerald) -->
<div class="player-area p3">
<div class="battle-zone"><div class="battle-zone-inner"></div><span class="battle-zone-label">Battlefield</span></div>
<div class="commander-row">
<div class="zone commander" data-tooltip="Commander Zone"><span class="zone-icon">👑</span><span class="zone-name">Commander</span></div>
</div>
<div class="zone-row">
<div class="zone library" data-tooltip="Library"><span class="card-count">99</span><span class="zone-icon">📚</span><span class="zone-name">Library</span></div>
<div class="zone exile" data-tooltip="Exile"><span class="zone-icon">✦</span><span class="zone-name">Exile</span></div>
<div class="zone graveyard" data-tooltip="Graveyard"><span class="zone-icon">💀</span><span class="zone-name">Grave</span></div>
<div class="zone hand" data-tooltip="Hand"><span class="zone-icon">🃏</span><span class="zone-name">Hand</span></div>
</div>
<span class="player-label">Player 3</span>
<div class="life-counter">
<button onclick="changeLife(3,-1)" aria-label="Decrease life">−</button>
<span id="life3">40</span>
<button onclick="changeLife(3,1)" aria-label="Increase life">+</button>
</div>
</div>
<!-- PLAYER 4: Bottom-Right (Gold) -->
<div class="player-area p4">
<div class="battle-zone"><div class="battle-zone-inner"></div><span class="battle-zone-label">Battlefield</span></div>
<div class="commander-row">
<div class="zone commander" data-tooltip="Commander Zone"><span class="zone-icon">👑</span><span class="zone-name">Commander</span></div>
</div>
<div class="zone-row">
<div class="zone library" data-tooltip="Library"><span class="card-count">99</span><span class="zone-icon">📚</span><span class="zone-name">Library</span></div>
<div class="zone exile" data-tooltip="Exile"><span class="zone-icon">✦</span><span class="zone-name">Exile</span></div>
<div class="zone graveyard" data-tooltip="Graveyard"><span class="zone-icon">💀</span><span class="zone-name">Grave</span></div>
<div class="zone hand" data-tooltip="Hand"><span class="zone-icon">🃏</span><span class="zone-name">Hand</span></div>
</div>
<span class="player-label">Player 4</span>
<div class="life-counter">
<button onclick="changeLife(4,-1)" aria-label="Decrease life">−</button>
<span id="life4">40</span>
<button onclick="changeLife(4,1)" aria-label="Increase life">+</button>
</div>
</div>
</div>
</div>
<script>
const lives = { 1: 40, 2: 40, 3: 40, 4: 40 };
function changeLife(p, d) {
lives[p] = Math.max(0, lives[p] + d);
const el = document.getElementById('life' + p);
el.textContent = lives[p];
el.style.transform = 'scale(1.25)';
el.style.transition = 'transform 0.15s ease';
setTimeout(() => { el.style.transform = 'scale(1)'; }, 150);
}
</script>
</body>
</html>
+2068
View File
File diff suppressed because it is too large. Load diff