Files
tcg_web_scraper/mtg_commander_life_tracker.html

764 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Commander Tracker</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #1a1520;
--bg-card: #251a2e;
--accent-gold: #d4af37;
--accent-purple: #8b5cf6;
--accent-red: #dc2626;
--text-primary: #e7e5e4;
--text-secondary: #a8a29e;
--border-color: #3f3745;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Crimson Text', serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
background-image:
radial-gradient(circle at 20% 50%, rgba(139, 92, 246, 0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 50%, rgba(212, 175, 55, 0.06) 0%, transparent 50%),
url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.02'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
overflow-x: hidden;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
margin-bottom: 3rem;
animation: fadeInDown 0.8s ease-out;
}
h1 {
font-family: 'Cinzel', serif;
font-size: 3.5rem;
font-weight: 700;
background: linear-gradient(135deg, var(--accent-gold), var(--accent-purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.subtitle {
font-size: 1.2rem;
color: var(--text-secondary);
letter-spacing: 0.15em;
text-transform: uppercase;
}
.setup-section {
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: fadeIn 0.8s ease-out 0.2s backwards;
}
.setup-controls {
display: flex;
gap: 1rem;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
label {
font-family: 'Cinzel', serif;
font-size: 1.1rem;
color: var(--accent-gold);
letter-spacing: 0.05em;
}
input[type="number"] {
background: var(--bg-card);
border: 2px solid var(--border-color);
color: var(--text-primary);
padding: 0.75rem 1rem;
border-radius: 8px;
font-size: 1.2rem;
width: 80px;
text-align: center;
font-family: 'Cinzel', serif;
transition: all 0.3s ease;
}
input[type="number"]:focus {
outline: none;
border-color: var(--accent-purple);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
}
input[type="text"] {
background: var(--bg-card);
border: 2px solid var(--border-color);
color: var(--text-primary);
padding: 0.75rem 1rem;
border-radius: 8px;
font-size: 1rem;
width: 100%;
font-family: 'Crimson Text', serif;
transition: all 0.3s ease;
}
input[type="text"]:focus {
outline: none;
border-color: var(--accent-purple);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
}
input[type="text"]::placeholder {
color: var(--text-secondary);
opacity: 0.6;
}
.player-name-input-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.player-name-label {
font-size: 0.9rem;
color: var(--text-secondary);
letter-spacing: 0.05em;
}
.btn {
background: linear-gradient(135deg, var(--accent-purple), var(--accent-gold));
border: none;
color: var(--bg-primary);
padding: 0.875rem 2rem;
border-radius: 8px;
font-family: 'Cinzel', serif;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
letter-spacing: 0.05em;
text-transform: uppercase;
transition: all 0.3s ease;
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(139, 92, 246, 0.5);
}
.btn:active {
transform: translateY(0);
}
.btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 2px solid var(--border-color);
box-shadow: none;
}
.btn-secondary:hover {
border-color: var(--accent-purple);
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.2);
}
.players-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.player-card {
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 16px;
padding: 1.5rem;
position: relative;
overflow: hidden;
transition: all 0.4s ease;
animation: scaleIn 0.5s ease-out backwards;
}
.player-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--accent-purple), var(--accent-gold));
}
.player-card.eliminated {
opacity: 0.5;
filter: grayscale(0.8);
}
.player-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.player-info {
flex: 1;
min-width: 150px;
}
.player-name {
font-family: 'Cinzel', serif;
font-size: 1.5rem;
font-weight: 600;
color: var(--accent-gold);
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.commander-deaths {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
.death-counter {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 0.25rem 0.5rem;
}
.death-display {
font-family: 'Cinzel', serif;
font-weight: 600;
color: var(--text-primary);
min-width: 20px;
text-align: center;
}
.death-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 1rem;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.death-btn:hover {
color: var(--accent-red);
transform: scale(1.2);
}
.eliminate-btn {
background: var(--accent-red);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
font-family: 'Cinzel', serif;
transition: all 0.3s ease;
}
.eliminate-btn:hover {
background: #b91c1c;
transform: scale(1.05);
}
.life-total {
text-align: center;
margin-bottom: 2rem;
}
.life-display {
font-family: 'Cinzel', serif;
font-size: 4rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 1rem;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.life-controls {
display: flex;
gap: 1rem;
justify-content: center;
}
.life-btn {
background: var(--bg-card);
border: 2px solid var(--border-color);
color: var(--text-primary);
width: 60px;
height: 60px;
border-radius: 12px;
font-family: 'Cinzel', serif;
font-size: 1.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.life-btn:hover {
border-color: var(--accent-purple);
background: var(--accent-purple);
color: var(--bg-primary);
transform: scale(1.1);
}
.life-btn:active {
transform: scale(0.95);
}
.commander-damage-section {
border-top: 1px solid var(--border-color);
padding-top: 1.5rem;
}
.section-title {
font-family: 'Cinzel', serif;
font-size: 1.1rem;
color: var(--accent-purple);
margin-bottom: 1rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.damage-grid {
display: grid;
gap: 0.75rem;
}
.damage-row {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
}
.damage-row:hover {
border-color: var(--accent-purple);
background: rgba(139, 92, 246, 0.1);
}
.damage-source {
font-size: 1rem;
color: var(--text-secondary);
}
.damage-controls {
display: flex;
gap: 0.5rem;
align-items: center;
}
.damage-display {
font-family: 'Cinzel', serif;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
min-width: 40px;
text-align: center;
}
.damage-display.lethal {
color: var(--accent-red);
animation: pulse 1s ease-in-out infinite;
}
.damage-btn {
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-primary);
width: 36px;
height: 36px;
border-radius: 6px;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.damage-btn:hover {
border-color: var(--accent-purple);
background: var(--accent-purple);
color: var(--bg-primary);
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.hidden {
display: none;
}
.save-indicator {
position: fixed;
bottom: 2rem;
right: 2rem;
background: var(--bg-secondary);
border: 2px solid var(--accent-gold);
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-family: 'Cinzel', serif;
color: var(--accent-gold);
font-size: 0.9rem;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
pointer-events: none;
z-index: 1000;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
}
.save-indicator.show {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 768px) {
h1 {
font-size: 2.5rem;
}
.players-grid {
grid-template-columns: 1fr;
}
.life-display {
font-size: 3rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Commander</h1>
<div class="subtitle">Life & Damage Tracker</div>
</header>
<div class="setup-section" id="setupSection">
<div class="setup-controls">
<label for="playerCount">Players:</label>
<input type="number" id="playerCount" min="2" max="8" value="4" onchange="updatePlayerNames()">
<label for="startingLife">Starting Life:</label>
<input type="number" id="startingLife" min="1" value="40">
</div>
<div id="playerNamesSection" style="margin-top: 2rem;">
<div class="section-title" style="text-align: center; margin-bottom: 1.5rem;">Player Names</div>
<div id="playerNamesGrid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; max-width: 800px; margin: 0 auto 1.5rem;"></div>
</div>
<div style="text-align: center;">
<button class="btn" onclick="startGame()">Begin Battle</button>
</div>
</div>
<div class="hidden" id="gameSection">
<div style="text-align: center; margin-bottom: 2rem;">
<button class="btn btn-secondary" onclick="resetGame()">New Game</button>
</div>
<div class="players-grid" id="playersGrid"></div>
</div>
</div>
<div class="save-indicator" id="saveIndicator">Game Saved</div>
<script>
let gameState = {
players: [],
startingLife: 40
};
let saveTimeout = null;
// Initialize player name inputs on page load
window.onload = function() {
loadGame();
updatePlayerNames();
};
// Auto-save function
function saveGame() {
if (gameState.players.length > 0) {
localStorage.setItem('mtgCommanderGame', JSON.stringify(gameState));
showSaveIndicator();
}
}
// Show save indicator
function showSaveIndicator() {
const indicator = document.getElementById('saveIndicator');
indicator.classList.add('show');
setTimeout(() => {
indicator.classList.remove('show');
}, 2000);
}
// Debounced save - saves 500ms after last change
function debouncedSave() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveGame, 500);
}
// Load game from localStorage
function loadGame() {
const saved = localStorage.getItem('mtgCommanderGame');
if (saved) {
try {
gameState = JSON.parse(saved);
// Show game section if there's a saved game
if (gameState.players.length > 0) {
document.getElementById('setupSection').classList.add('hidden');
document.getElementById('gameSection').classList.remove('hidden');
renderGame();
}
} catch (e) {
console.error('Error loading saved game:', e);
}
}
}
function updatePlayerNames() {
const playerCount = parseInt(document.getElementById('playerCount').value);
const grid = document.getElementById('playerNamesGrid');
grid.innerHTML = '';
for (let i = 0; i < playerCount; i++) {
const wrapper = document.createElement('div');
wrapper.className = 'player-name-input-wrapper';
wrapper.innerHTML = `
<label class="player-name-label">Player ${i + 1}</label>
<input type="text" id="playerName${i}" placeholder="Enter name..." value="Player ${i + 1}">
`;
grid.appendChild(wrapper);
}
}
function startGame() {
const playerCount = parseInt(document.getElementById('playerCount').value);
const startingLife = parseInt(document.getElementById('startingLife').value);
gameState.startingLife = startingLife;
gameState.players = [];
for (let i = 0; i < playerCount; i++) {
const nameInput = document.getElementById(`playerName${i}`);
const playerName = nameInput.value.trim() || `Player ${i + 1}`;
const player = {
id: i,
name: playerName,
life: startingLife,
commanderDamage: {},
commanderDeaths: 0,
eliminated: false
};
// Initialize commander damage tracking
for (let j = 0; j < playerCount; j++) {
if (i !== j) {
player.commanderDamage[j] = 0;
}
}
gameState.players.push(player);
}
document.getElementById('setupSection').classList.add('hidden');
document.getElementById('gameSection').classList.remove('hidden');
renderGame();
saveGame();
}
function renderGame() {
const grid = document.getElementById('playersGrid');
grid.innerHTML = '';
gameState.players.forEach((player, index) => {
const card = document.createElement('div');
card.className = `player-card ${player.eliminated ? 'eliminated' : ''}`;
card.style.animationDelay = `${index * 0.1}s`;
card.innerHTML = `
<div class="player-header">
<div class="player-info">
<div class="player-name">${player.name}</div>
<div class="commander-deaths">
<span>Commander Deaths:</span>
<div class="death-counter">
<button class="death-btn" onclick="changeCommanderDeaths(${player.id}, -1)"></button>
<span class="death-display">${player.commanderDeaths || 0}</span>
<button class="death-btn" onclick="changeCommanderDeaths(${player.id}, 1)">+</button>
</div>
</div>
</div>
<button class="eliminate-btn" onclick="toggleEliminate(${player.id})">
${player.eliminated ? 'Revive' : 'Eliminate'}
</button>
</div>
<div class="life-total">
<div class="life-display">${player.life}</div>
<div class="life-controls">
<button class="life-btn" onclick="changeLife(${player.id}, -5)">-5</button>
<button class="life-btn" onclick="changeLife(${player.id}, -1)">-1</button>
<button class="life-btn" onclick="changeLife(${player.id}, 1)">+1</button>
<button class="life-btn" onclick="changeLife(${player.id}, 5)">+5</button>
</div>
</div>
<div class="commander-damage-section">
<div class="section-title">Commander Damage Taken</div>
<div class="damage-grid">
${renderCommanderDamage(player)}
</div>
</div>
`;
grid.appendChild(card);
});
}
function renderCommanderDamage(player) {
return Object.keys(player.commanderDamage)
.map(sourceId => {
const source = gameState.players[sourceId];
const damage = player.commanderDamage[sourceId];
const isLethal = damage >= 21;
return `
<div class="damage-row">
<span class="damage-source">from ${source.name}</span>
<div class="damage-controls">
<button class="damage-btn" onclick="changeCommanderDamage(${player.id}, ${sourceId}, -1)"></button>
<span class="damage-display ${isLethal ? 'lethal' : ''}">${damage}</span>
<button class="damage-btn" onclick="changeCommanderDamage(${player.id}, ${sourceId}, 1)">+</button>
</div>
</div>
`;
})
.join('');
}
function changeLife(playerId, amount) {
const player = gameState.players[playerId];
if (player.eliminated) return;
player.life = Math.max(0, player.life + amount);
renderGame();
debouncedSave();
}
function changeCommanderDamage(playerId, sourceId, amount) {
const player = gameState.players[playerId];
if (player.eliminated) return;
player.commanderDamage[sourceId] = Math.max(0, player.commanderDamage[sourceId] + amount);
renderGame();
debouncedSave();
}
function changeCommanderDeaths(playerId, amount) {
const player = gameState.players[playerId];
if (player.eliminated) return;
player.commanderDeaths = Math.max(0, (player.commanderDeaths || 0) + amount);
renderGame();
debouncedSave();
}
function toggleEliminate(playerId) {
const player = gameState.players[playerId];
player.eliminated = !player.eliminated;
renderGame();
debouncedSave();
}
function resetGame() {
if (confirm('Are you sure you want to start a new game? Current game will be lost.')) {
localStorage.removeItem('mtgCommanderGame');
document.getElementById('setupSection').classList.remove('hidden');
document.getElementById('gameSection').classList.add('hidden');
gameState = { players: [], startingLife: 40 };
updatePlayerNames();
}
}
</script>
</body>
</html>