Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Arcane Chronicles: Battle of Realms</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Fauna+One&display=swap'); | |
body { | |
font-family: 'Fauna One', serif; | |
background-color: #0f172a; | |
color: #e2e8f0; | |
background-image: url('https://images.unsplash.com/photo-1518562180175-34a163b1a9fa?q=80&w=2070&auto=format&fit=crop'); | |
background-size: cover; | |
background-attachment: fixed; | |
} | |
.card { | |
transition: all 0.3s ease; | |
transform-style: preserve-3d; | |
perspective: 1000px; | |
} | |
.card:hover { | |
transform: translateY(-10px) scale(1.03); | |
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4); | |
} | |
.card-front { | |
backface-visibility: hidden; | |
} | |
.card-back { | |
backface-visibility: hidden; | |
transform: rotateY(180deg); | |
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); | |
} | |
.flipped { | |
transform: rotateY(180deg); | |
} | |
.health-bar { | |
height: 6px; | |
transition: width 0.3s ease; | |
} | |
.lane { | |
min-height: 250px; | |
background: rgba(30, 41, 59, 0.5); | |
border: 2px solid rgba(148, 163, 184, 0.3); | |
border-radius: 8px; | |
} | |
.attack-animation { | |
animation: attack 0.5s ease; | |
} | |
@keyframes attack { | |
0% { transform: translateX(0); } | |
50% { transform: translateX(20px); } | |
100% { transform: translateX(0); } | |
} | |
.damage-text { | |
animation: floatUp 1s ease forwards; | |
position: absolute; | |
color: #f87171; | |
font-weight: bold; | |
pointer-events: none; | |
font-size: 1.5rem; | |
} | |
@keyframes floatUp { | |
0% { transform: translateY(0); opacity: 1; } | |
100% { transform: translateY(-50px); opacity: 0; } | |
} | |
.title-font { | |
font-family: 'Cinzel', serif; | |
} | |
.card-tooltip { | |
visibility: hidden; | |
width: 250px; | |
background-color: #1e293b; | |
color: #fff; | |
text-align: center; | |
border-radius: 6px; | |
padding: 10px; | |
position: absolute; | |
z-index: 1; | |
bottom: 125%; | |
left: 50%; | |
transform: translateX(-50%); | |
opacity: 0; | |
transition: opacity 0.3s; | |
border: 1px solid #475569; | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5); | |
} | |
.card:hover .card-tooltip { | |
visibility: visible; | |
opacity: 1; | |
} | |
.end-turn-btn { | |
position: fixed; | |
bottom: 20px; | |
right: 20px; | |
z-index: 40; | |
transition: all 0.3s ease; | |
} | |
.end-turn-btn:hover { | |
transform: scale(1.05); | |
} | |
/* Larger card styles */ | |
.hand-card { | |
width: 160px; | |
height: 240px; | |
} | |
.board-card { | |
width: 180px; | |
height: 270px; | |
} | |
.card-image { | |
height: 160px; | |
background-size: cover; | |
background-position: center; | |
} | |
.board-card .card-image { | |
height: 180px; | |
} | |
.card-stats { | |
font-size: 1.1rem; | |
} | |
.card-name { | |
font-size: 1.1rem; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
} | |
.card-cost { | |
width: 28px; | |
height: 28px; | |
font-size: 1rem; | |
} | |
.card-type { | |
font-size: 0.9rem; | |
} | |
.card-keyword { | |
font-size: 0.8rem; | |
} | |
</style> | |
</head> | |
<body class="min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<!-- Header --> | |
<header class="flex flex-col items-center mb-8"> | |
<h1 class="title-font text-5xl font-bold text-amber-400 mb-2 text-center">Arcane Chronicles</h1> | |
<h2 class="title-font text-2xl text-amber-200 mb-6 text-center">Battle of Realms</h2> | |
<div class="flex space-x-4 mb-6"> | |
<button id="new-game-btn" class="bg-amber-600 hover:bg-amber-700 text-white px-6 py-2 rounded-lg font-semibold transition"> | |
<i class="fas fa-play mr-2"></i>New Game | |
</button> | |
<button id="rules-btn" class="bg-slate-700 hover:bg-slate-600 text-white px-6 py-2 rounded-lg font-semibold transition"> | |
<i class="fas fa-scroll mr-2"></i>Rules | |
</button> | |
</div> | |
<div class="flex justify-between w-full max-w-2xl bg-slate-800 p-4 rounded-lg"> | |
<div class="text-center"> | |
<div class="text-xl font-semibold text-amber-300">Player</div> | |
<div id="player-health" class="text-3xl font-bold">30</div> | |
</div> | |
<div class="text-center"> | |
<div class="text-xl font-semibold">Mana</div> | |
<div id="current-mana" class="text-3xl font-bold text-blue-400">0/10</div> | |
</div> | |
<div class="text-center"> | |
<div class="text-xl font-semibold text-red-300">Enemy</div> | |
<div id="enemy-health" class="text-3xl font-bold">30</div> | |
</div> | |
</div> | |
</header> | |
<!-- Game Board --> | |
<div class="game-board mb-8"> | |
<!-- Enemy Lane --> | |
<div class="grid grid-cols-2 gap-4 mb-6"> | |
<div class="lane" id="enemy-field-lane"> | |
<!-- Enemy cards will appear here --> | |
</div> | |
<div class="lane" id="enemy-shadow-lane"> | |
<!-- Enemy cards will appear here --> | |
</div> | |
</div> | |
<!-- Player Lane --> | |
<div class="grid grid-cols-2 gap-4 mb-6"> | |
<div class="lane" id="player-field-lane"> | |
<!-- Player cards will appear here --> | |
</div> | |
<div class="lane" id="player-shadow-lane"> | |
<!-- Player cards will appear here --> | |
</div> | |
</div> | |
</div> | |
<!-- Player Hand --> | |
<div class="player-hand-container bg-slate-900 bg-opacity-70 p-4 rounded-t-lg"> | |
<h3 class="text-xl font-semibold mb-4 text-center">Your Hand</h3> | |
<div class="flex justify-center flex-wrap gap-4 min-h-40" id="player-hand"> | |
<!-- Player cards will appear here --> | |
</div> | |
</div> | |
<!-- End Turn Button --> | |
<button id="end-turn-btn" class="end-turn-btn bg-purple-600 hover:bg-purple-700 text-white px-6 py-3 rounded-full font-semibold transition shadow-lg hidden"> | |
<i class="fas fa-hourglass-end mr-2"></i>End Turn | |
</button> | |
<!-- Modals --> | |
<div id="rules-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center hidden z-50"> | |
<div class="bg-slate-800 rounded-lg p-6 max-w-2xl max-h-[80vh] overflow-y-auto"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 class="text-2xl font-bold text-amber-300">Game Rules</h3> | |
<button id="close-rules" class="text-slate-400 hover:text-white"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="space-y-4"> | |
<div> | |
<h4 class="text-lg font-semibold text-amber-200 mb-2">Objective</h4> | |
<p>Reduce your opponent's health to zero by strategically playing creatures and using abilities.</p> | |
</div> | |
<div> | |
<h4 class="text-lg font-semibold text-amber-200 mb-2">Lanes</h4> | |
<p>The battlefield has two lanes: Field Lane and Shadow Lane.</p> | |
<ul class="list-disc pl-5 mt-2 space-y-1"> | |
<li><span class="font-semibold">Field Lane:</span> Creatures here can attack the turn they're played.</li> | |
<li><span class="font-semibold">Shadow Lane:</span> Creatures here gain Cover for one turn, protecting them from attacks.</li> | |
</ul> | |
</div> | |
<div> | |
<h4 class="text-lg font-semibold text-amber-200 mb-2">Mana System</h4> | |
<p>You start with 1 mana and gain 1 additional mana each turn (max 10).</p> | |
</div> | |
<div> | |
<h4 class="text-lg font-semibold text-amber-200 mb-2">Card Types</h4> | |
<ul class="list-disc pl-5 mt-2 space-y-1"> | |
<li><span class="font-semibold">Creatures:</span> Can attack enemy creatures or the opponent directly.</li> | |
<li><span class="font-semibold">Actions:</span> One-time effects that can deal damage, heal, or modify the board.</li> | |
<li><span class="font-semibold">Supports:</span> Ongoing effects that remain on the board until destroyed.</li> | |
</ul> | |
</div> | |
<div> | |
<h4 class="text-lg font-semibold text-amber-200 mb-2">Keywords</h4> | |
<div class="grid grid-cols-2 gap-2 mt-2"> | |
<div class="bg-slate-700 p-2 rounded"> | |
<span class="font-semibold">Breakthrough:</span> Excess damage goes to the opponent. | |
</div> | |
<div class="bg-slate-700 p-2 rounded"> | |
<span class="font-semibold">Drain:</span> Heals your health equal to damage dealt. | |
</div> | |
<div class="bg-slate-700 p-2 rounded"> | |
<span class="font-semibold">Guard:</span> Must be attacked before other targets. | |
</div> | |
<div class="bg-slate-700 p-2 rounded"> | |
<span class="font-semibold">Lethal:</span> Destroys any creature it damages. | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="game-over-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center hidden z-50"> | |
<div class="bg-slate-800 rounded-lg p-8 max-w-md text-center"> | |
<h3 id="game-result" class="text-3xl font-bold mb-4">Victory!</h3> | |
<p id="game-over-text" class="mb-6">You have defeated your opponent!</p> | |
<button id="play-again-btn" class="bg-amber-600 hover:bg-amber-700 text-white px-6 py-2 rounded-lg font-semibold transition"> | |
<i class="fas fa-redo mr-2"></i>Play Again | |
</button> | |
</div> | |
</div> | |
<div id="turn-indicator" class="fixed top-4 right-4 bg-slate-800 bg-opacity-80 px-4 py-2 rounded-lg shadow-lg hidden"> | |
<div class="flex items-center"> | |
<div id="turn-arrow" class="text-amber-400 mr-2"> | |
<i class="fas fa-arrow-right"></i> | |
</div> | |
<span id="turn-text" class="font-semibold">Your Turn</span> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Game State | |
const gameState = { | |
playerHealth: 30, | |
enemyHealth: 30, | |
playerMana: 0, | |
maxMana: 0, | |
turn: 'player', // 'player' or 'enemy' | |
turnCount: 0, | |
playerHand: [], | |
playerFieldLane: [], | |
playerShadowLane: [], | |
enemyFieldLane: [], | |
enemyShadowLane: [], | |
playerDeck: [], | |
enemyDeck: [], | |
enemyHand: [], | |
gameStarted: false | |
}; | |
// Card Database with more low-cost cards | |
const cardDatabase = [ | |
// Low cost creatures (1-3 mana) | |
{ | |
id: 1, | |
name: "Arcane Apprentice", | |
type: "creature", | |
cost: 1, | |
attack: 1, | |
health: 2, | |
keywords: [], | |
lane: "field", | |
description: "A novice mage learning the arcane arts.", | |
image: "https://images.unsplash.com/photo-1542273917363-3b1817f69a2b?q=80&w=2074&auto=format&fit=crop" | |
}, | |
{ | |
id: 2, | |
name: "Forest Sprite", | |
type: "creature", | |
cost: 1, | |
attack: 1, | |
health: 1, | |
keywords: [], | |
lane: "shadow", | |
description: "A tiny woodland spirit that moves swiftly.", | |
image: "https://images.unsplash.com/photo-1519457431-44ccd64a7b47?q=80&w=1935&auto=format&fit=crop" | |
}, | |
{ | |
id: 3, | |
name: "Town Guard", | |
type: "creature", | |
cost: 2, | |
attack: 2, | |
health: 2, | |
keywords: ["Guard"], | |
lane: "field", | |
description: "Protects the town from threats.", | |
image: "https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?q=80&w=2070&auto=format&fit=crop" | |
}, | |
{ | |
id: 4, | |
name: "Shadow Thief", | |
type: "creature", | |
cost: 2, | |
attack: 3, | |
health: 1, | |
keywords: [], | |
lane: "shadow", | |
description: "Strikes quickly from the darkness.", | |
image: "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?q=80&w=2048&auto=format&fit=crop" | |
}, | |
{ | |
id: 5, | |
name: "Arcane Sentinel", | |
type: "creature", | |
cost: 3, | |
attack: 2, | |
health: 3, | |
keywords: ["Guard"], | |
lane: "field", | |
description: "A mystical guardian that protects its allies.", | |
image: "https://images.unsplash.com/photo-1476231682828-37e571bc172f?q=80&w=1974&auto=format&fit=crop" | |
}, | |
{ | |
id: 6, | |
name: "Shadow Assassin", | |
type: "creature", | |
cost: 3, | |
attack: 4, | |
health: 2, | |
keywords: ["Lethal"], | |
lane: "shadow", | |
description: "Strikes from the darkness with deadly precision.", | |
image: "https://images.unsplash.com/photo-1519457431-44ccd64a7b47?q=80&w=1935&auto=format&fit=crop" | |
}, | |
{ | |
id: 7, | |
name: "Mountain Wolf", | |
type: "creature", | |
cost: 3, | |
attack: 3, | |
health: 3, | |
keywords: [], | |
lane: "field", | |
description: "A fierce predator of the highlands.", | |
image: "https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?q=80&w=2070&auto=format&fit=crop" | |
}, | |
// Mid cost creatures (4-6 mana) | |
{ | |
id: 8, | |
name: "Stormcaller", | |
type: "creature", | |
cost: 4, | |
attack: 3, | |
health: 4, | |
keywords: ["Breakthrough"], | |
lane: "field", | |
description: "Channels the fury of the storm into devastating attacks.", | |
image: "https://images.unsplash.com/photo-1506748686214-e9df14d4d9d0?q=80&w=2070&auto=format&fit=crop" | |
}, | |
{ | |
id: 9, | |
name: "Vampire Noble", | |
type: "creature", | |
cost: 5, | |
attack: 4, | |
health: 4, | |
keywords: ["Drain"], | |
lane: "shadow", | |
description: "Feeds on the life force of its enemies.", | |
image: "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?q=80&w=2048&auto=format&fit=crop" | |
}, | |
{ | |
id: 10, | |
name: "Ancient Treant", | |
type: "creature", | |
cost: 6, | |
attack: 2, | |
health: 6, | |
keywords: ["Guard"], | |
lane: "field", | |
description: "An ancient protector of the forests.", | |
image: "https://images.unsplash.com/photo-1476231682828-37e571bc172f?q=80&w=1974&auto=format&fit=crop" | |
}, | |
// Actions | |
{ | |
id: 11, | |
name: "Spark", | |
type: "action", | |
cost: 1, | |
effect: "dealDamage", | |
value: 2, | |
description: "Deals 2 damage to a target.", | |
image: "https://images.unsplash.com/photo-1518562180175-34a163b1a9fa?q=80&w=2070&auto=format&fit=crop" | |
}, | |
{ | |
id: 12, | |
name: "Minor Heal", | |
type: "action", | |
cost: 1, | |
effect: "heal", | |
value: 3, | |
description: "Restore 3 health to your hero.", | |
image: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=2094&auto=format&fit=crop" | |
}, | |
{ | |
id: 13, | |
name: "Fireball", | |
type: "action", | |
cost: 3, | |
effect: "dealDamage", | |
value: 4, | |
description: "Hurl a fiery projectile that deals 4 damage.", | |
image: "https://images.unsplash.com/photo-1518562180175-34a163b1a9fa?q=80&w=2070&auto=format&fit=crop" | |
}, | |
{ | |
id: 14, | |
name: "Healing Light", | |
type: "action", | |
cost: 2, | |
effect: "heal", | |
value: 5, | |
description: "Restore 5 health to your hero.", | |
image: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=2094&auto=format&fit=crop" | |
}, | |
{ | |
id: 15, | |
name: "Soul Trap", | |
type: "action", | |
cost: 4, | |
effect: "destroyCreature", | |
description: "Destroy an enemy creature with 3 or less attack.", | |
image: "https://images.unsplash.com/photo-1534447677768-be436bb09401?q=80&w=2094&auto=format&fit=crop" | |
}, | |
// Supports | |
{ | |
id: 16, | |
name: "Training Grounds", | |
type: "support", | |
cost: 2, | |
effect: "buffCreatures", | |
value: 1, | |
description: "Your creatures have +1 attack.", | |
image: "https://images.unsplash.com/photo-1535905557558-3c2747c3d7a3?q=80&w=2070&auto=format&fit=crop" | |
}, | |
{ | |
id: 17, | |
name: "Arcane Library", | |
type: "support", | |
cost: 4, | |
effect: "drawCard", | |
value: 1, | |
description: "At the start of your turn, draw an extra card.", | |
image: "https://images.unsplash.com/photo-1535905557558-3c2747c3d7a3?q=80&w=2070&auto=format&fit=crop" | |
}, | |
{ | |
id: 18, | |
name: "Ward of Protection", | |
type: "support", | |
cost: 3, | |
effect: "reduceDamage", | |
value: 1, | |
description: "Your hero takes 1 less damage from all sources.", | |
image: "https://images.unsplash.com/photo-1505506874110-0a91101d6d41?q=80&w=2070&auto=format&fit=crop" | |
} | |
]; | |
// DOM Elements | |
const playerHealthEl = document.getElementById('player-health'); | |
const enemyHealthEl = document.getElementById('enemy-health'); | |
const currentManaEl = document.getElementById('current-mana'); | |
const playerHandEl = document.getElementById('player-hand'); | |
const playerFieldLaneEl = document.getElementById('player-field-lane'); | |
const playerShadowLaneEl = document.getElementById('player-shadow-lane'); | |
const enemyFieldLaneEl = document.getElementById('enemy-field-lane'); | |
const enemyShadowLaneEl = document.getElementById('enemy-shadow-lane'); | |
const newGameBtn = document.getElementById('new-game-btn'); | |
const rulesBtn = document.getElementById('rules-btn'); | |
const closeRulesBtn = document.getElementById('close-rules'); | |
const rulesModal = document.getElementById('rules-modal'); | |
const gameOverModal = document.getElementById('game-over-modal'); | |
const gameResultEl = document.getElementById('game-result'); | |
const gameOverTextEl = document.getElementById('game-over-text'); | |
const playAgainBtn = document.getElementById('play-again-btn'); | |
const turnIndicator = document.getElementById('turn-indicator'); | |
const turnText = document.getElementById('turn-text'); | |
const turnArrow = document.getElementById('turn-arrow'); | |
const endTurnBtn = document.getElementById('end-turn-btn'); | |
// Event Listeners | |
newGameBtn.addEventListener('click', startNewGame); | |
rulesBtn.addEventListener('click', () => rulesModal.classList.remove('hidden')); | |
closeRulesBtn.addEventListener('click', () => rulesModal.classList.add('hidden')); | |
playAgainBtn.addEventListener('click', startNewGame); | |
endTurnBtn.addEventListener('click', endPlayerTurn); | |
// Game Functions | |
function startNewGame() { | |
// Reset game state | |
gameState.playerHealth = 30; | |
gameState.enemyHealth = 30; | |
gameState.playerMana = 0; | |
gameState.maxMana = 0; | |
gameState.turn = 'player'; | |
gameState.turnCount = 0; | |
gameState.playerHand = []; | |
gameState.playerFieldLane = []; | |
gameState.playerShadowLane = []; | |
gameState.enemyFieldLane = []; | |
gameState.enemyShadowLane = []; | |
gameState.playerDeck = []; | |
gameState.enemyDeck = []; | |
gameState.enemyHand = []; | |
gameState.gameStarted = true; | |
// Hide modals | |
gameOverModal.classList.add('hidden'); | |
rulesModal.classList.add('hidden'); | |
// Update UI | |
playerHealthEl.textContent = gameState.playerHealth; | |
enemyHealthEl.textContent = gameState.enemyHealth; | |
currentManaEl.textContent = `${gameState.playerMana}/${gameState.maxMana}`; | |
// Clear board | |
playerHandEl.innerHTML = ''; | |
playerFieldLaneEl.innerHTML = ''; | |
playerShadowLaneEl.innerHTML = ''; | |
enemyFieldLaneEl.innerHTML = ''; | |
enemyShadowLaneEl.innerHTML = ''; | |
// Create decks | |
createDecks(); | |
// Draw starting hands | |
drawStartingHands(); | |
// Start first turn | |
startPlayerTurn(); | |
// Show turn indicator | |
turnIndicator.classList.remove('hidden'); | |
updateTurnIndicator(); | |
// Show end turn button | |
endTurnBtn.classList.remove('hidden'); | |
} | |
function createDecks() { | |
// Create player deck (30 cards) | |
for (let i = 0; i < 30; i++) { | |
const randomCard = {...cardDatabase[Math.floor(Math.random() * cardDatabase.length)]}; | |
gameState.playerDeck.push(randomCard); | |
} | |
// Create enemy deck (30 cards) | |
for (let i = 0; i < 30; i++) { | |
const randomCard = {...cardDatabase[Math.floor(Math.random() * cardDatabase.length)]}; | |
gameState.enemyDeck.push(randomCard); | |
} | |
} | |
function drawStartingHands() { | |
// Player draws 3 cards | |
for (let i = 0; i < 3; i++) { | |
drawCard('player'); | |
} | |
// Enemy draws 3 cards | |
for (let i = 0; i < 3; i++) { | |
drawCard('enemy'); | |
} | |
} | |
function drawCard(who) { | |
if (who === 'player') { | |
if (gameState.playerDeck.length > 0) { | |
const card = gameState.playerDeck.pop(); | |
gameState.playerHand.push(card); | |
renderPlayerHand(); | |
} | |
} else { | |
if (gameState.enemyDeck.length > 0) { | |
const card = gameState.enemyDeck.pop(); | |
gameState.enemyHand.push(card); | |
} | |
} | |
} | |
function startPlayerTurn() { | |
gameState.turn = 'player'; | |
gameState.turnCount++; | |
// Increase max mana (up to 10) | |
if (gameState.maxMana < 10) { | |
gameState.maxMana++; | |
} | |
// Refill mana | |
gameState.playerMana = gameState.maxMana; | |
// Draw a card | |
drawCard('player'); | |
// Update UI | |
updateTurnIndicator(); | |
currentManaEl.textContent = `${gameState.playerMana}/${gameState.maxMana}`; | |
// Check for summoning sickness | |
updateSummoningSickness(); | |
// Enable player actions | |
enablePlayerActions(); | |
// Show end turn button | |
endTurnBtn.classList.remove('hidden'); | |
} | |
function endPlayerTurn() { | |
// Hide end turn button | |
endTurnBtn.classList.add('hidden'); | |
// Start enemy turn | |
startEnemyTurn(); | |
} | |
function startEnemyTurn() { | |
gameState.turn = 'enemy'; | |
updateTurnIndicator(); | |
// Disable player actions | |
disablePlayerActions(); | |
// Enemy draws a card | |
drawCard('enemy'); | |
// Simple AI: Play cards and attack | |
setTimeout(() => { | |
enemyPlayCards(); | |
setTimeout(() => { | |
enemyAttack(); | |
// End enemy turn after a delay | |
setTimeout(() => { | |
startPlayerTurn(); | |
}, 1500); | |
}, 1500); | |
}, 1500); | |
} | |
function enemyPlayCards() { | |
// Simple AI: Play cards if possible | |
const playableCards = gameState.enemyHand.filter(card => card.cost <= gameState.maxMana); | |
for (const card of playableCards) { | |
if (Math.random() > 0.3) { // 70% chance to play each card | |
// Determine lane | |
const lane = card.lane || (Math.random() > 0.5 ? 'field' : 'shadow'); | |
// Play the card | |
playCard('enemy', card, lane); | |
// Remove from hand | |
const cardIndex = gameState.enemyHand.findIndex(c => c.id === card.id); | |
if (cardIndex !== -1) { | |
gameState.enemyHand.splice(cardIndex, 1); | |
} | |
} | |
} | |
} | |
function enemyAttack() { | |
// Simple AI: Attack with all creatures | |
const allEnemyCreatures = [...gameState.enemyFieldLane, ...gameState.enemyShadowLane]; | |
for (const creature of allEnemyCreatures) { | |
if (!creature.hasAttacked && !creature.summoningSickness) { | |
// Find a target (simplified - just attack player directly) | |
attackTarget('enemy', creature, {type: 'player'}); | |
} | |
} | |
} | |
function playCard(who, card, lane) { | |
if (who === 'player') { | |
// Check if player has enough mana | |
if (gameState.playerMana < card.cost) { | |
alert("Not enough mana!"); | |
return; | |
} | |
// Spend mana | |
gameState.playerMana -= card.cost; | |
currentManaEl.textContent = `${gameState.playerMana}/${gameState.maxMana}`; | |
// Remove card from hand | |
const cardIndex = gameState.playerHand.findIndex(c => c.id === card.id); | |
if (cardIndex !== -1) { | |
gameState.playerHand.splice(cardIndex, 1); | |
} | |
// Add to appropriate lane | |
if (card.type === 'creature') { | |
const creature = {...card, hasAttacked: false, summoningSickness: true}; | |
if (lane === 'field') { | |
gameState.playerFieldLane.push(creature); | |
} else { | |
gameState.playerShadowLane.push(creature); | |
} | |
} | |
// Re-render | |
renderPlayerHand(); | |
renderPlayerLanes(); | |
} else { | |
// Enemy plays a card | |
if (card.type === 'creature') { | |
const creature = {...card, hasAttacked: false, summoningSickness: true}; | |
if (lane === 'field') { | |
gameState.enemyFieldLane.push(creature); | |
} else { | |
gameState.enemyShadowLane.push(creature); | |
} | |
} | |
renderEnemyLanes(); | |
} | |
} | |
function attackTarget(attackerWho, attacker, target) { | |
if (attackerWho === 'player') { | |
if (attacker.summoningSickness) { | |
alert("This creature has summoning sickness and can't attack this turn!"); | |
return; | |
} | |
if (attacker.hasAttacked) { | |
alert("This creature has already attacked this turn!"); | |
return; | |
} | |
// Mark as attacked | |
attacker.hasAttacked = true; | |
// Apply damage | |
if (target.type === 'creature') { | |
// Creature vs creature combat | |
target.health -= attacker.attack; | |
attacker.health -= target.attack; | |
// Check for deaths | |
checkForDeaths(); | |
// Show damage animation | |
showDamageAnimation(target, attacker.attack); | |
showDamageAnimation(attacker, target.attack); | |
} else if (target.type === 'player') { | |
// Attack enemy directly | |
gameState.enemyHealth -= attacker.attack; | |
enemyHealthEl.textContent = gameState.enemyHealth; | |
// Show damage animation | |
showDamageAnimation({element: enemyHealthEl}, attacker.attack); | |
// Check for game over | |
if (gameState.enemyHealth <= 0) { | |
endGame('player'); | |
} | |
} | |
// Re-render | |
renderPlayerLanes(); | |
renderEnemyLanes(); | |
} else { | |
// Enemy attacks | |
if (target.type === 'creature') { | |
// Creature vs creature combat | |
target.health -= attacker.attack; | |
attacker.health -= target.attack; | |
// Check for deaths | |
checkForDeaths(); | |
// Show damage animation | |
showDamageAnimation(target, attacker.attack); | |
showDamageAnimation(attacker, target.attack); | |
} else if (target.type === 'player') { | |
// Attack player directly | |
gameState.playerHealth -= attacker.attack; | |
playerHealthEl.textContent = gameState.playerHealth; | |
// Show damage animation | |
showDamageAnimation({element: playerHealthEl}, attacker.attack); | |
// Check for game over | |
if (gameState.playerHealth <= 0) { | |
endGame('enemy'); | |
} | |
} | |
// Re-render | |
renderPlayerLanes(); | |
renderEnemyLanes(); | |
} | |
} | |
function checkForDeaths() { | |
// Check player field lane | |
gameState.playerFieldLane = gameState.playerFieldLane.filter(creature => creature.health > 0); | |
// Check player shadow lane | |
gameState.playerShadowLane = gameState.playerShadowLane.filter(creature => creature.health > 0); | |
// Check enemy field lane | |
gameState.enemyFieldLane = gameState.enemyFieldLane.filter(creature => creature.health > 0); | |
// Check enemy shadow lane | |
gameState.enemyShadowLane = gameState.enemyShadowLane.filter(creature => creature.health > 0); | |
} | |
function endGame(winner) { | |
gameState.gameStarted = false; | |
if (winner === 'player') { | |
gameResultEl.textContent = "Victory!"; | |
gameOverTextEl.textContent = "You have defeated your opponent!"; | |
} else { | |
gameResultEl.textContent = "Defeat"; | |
gameOverTextEl.textContent = "Your opponent has defeated you!"; | |
} | |
gameOverModal.classList.remove('hidden'); | |
turnIndicator.classList.add('hidden'); | |
endTurnBtn.classList.add('hidden'); | |
} | |
function updateSummoningSickness() { | |
// Remove summoning sickness from player creatures | |
gameState.playerFieldLane.forEach(creature => { | |
if (creature.summoningSickness) { | |
creature.summoningSickness = false; | |
} | |
creature.hasAttacked = false; | |
}); | |
gameState.playerShadowLane.forEach(creature => { | |
if (creature.summoningSickness) { | |
creature.summoningSickness = false; | |
} | |
creature.hasAttacked = false; | |
}); | |
// Re-render | |
renderPlayerLanes(); | |
} | |
function enablePlayerActions() { | |
// Enable hand cards | |
const handCards = document.querySelectorAll('.hand-card'); | |
handCards.forEach(card => { | |
card.classList.remove('opacity-50', 'cursor-not-allowed'); | |
card.classList.add('cursor-pointer', 'hover:scale-105'); | |
}); | |
// Enable creatures to attack | |
const playerCreatures = document.querySelectorAll('#player-field-lane .creature, #player-shadow-lane .creature'); | |
playerCreatures.forEach(creature => { | |
if (!creature.dataset.hasAttacked && !creature.dataset.summoningSickness) { | |
creature.classList.add('cursor-pointer', 'hover:scale-105'); | |
} | |
}); | |
} | |
function disablePlayerActions() { | |
// Disable hand cards | |
const handCards = document.querySelectorAll('.hand-card'); | |
handCards.forEach(card => { | |
card.classList.add('opacity-50', 'cursor-not-allowed'); | |
card.classList.remove('cursor-pointer', 'hover:scale-105'); | |
}); | |
// Disable creatures from attacking | |
const playerCreatures = document.querySelectorAll('#player-field-lane .creature, #player-shadow-lane .creature'); | |
playerCreatures.forEach(creature => { | |
creature.classList.remove('cursor-pointer', 'hover:scale-105'); | |
}); | |
} | |
function updateTurnIndicator() { | |
if (gameState.turn === 'player') { | |
turnText.textContent = "Your Turn"; | |
turnArrow.innerHTML = '<i class="fas fa-arrow-right"></i>'; | |
} else { | |
turnText.textContent = "Enemy Turn"; | |
turnArrow.innerHTML = '<i class="fas fa-arrow-left"></i>'; | |
} | |
} | |
function showDamageAnimation(target, damage) { | |
const element = target.element || document.getElementById(`creature-${target.id}`); | |
if (!element) return; | |
const rect = element.getBoundingClientRect(); | |
const damageText = document.createElement('div'); | |
damageText.className = 'damage-text'; | |
damageText.textContent = `-${damage}`; | |
damageText.style.left = `${rect.left + rect.width / 2}px`; | |
damageText.style.top = `${rect.top}px`; | |
document.body.appendChild(damageText); | |
// Remove after animation | |
setTimeout(() => { | |
damageText.remove(); | |
}, 1000); | |
} | |
// Rendering Functions | |
function renderPlayerHand() { | |
playerHandEl.innerHTML = ''; | |
gameState.playerHand.forEach((card, index) => { | |
const cardEl = createCardElement(card, 'hand'); | |
cardEl.dataset.index = index; | |
playerHandEl.appendChild(cardEl); | |
}); | |
} | |
function renderPlayerLanes() { | |
playerFieldLaneEl.innerHTML = ''; | |
playerShadowLaneEl.innerHTML = ''; | |
// Field Lane | |
gameState.playerFieldLane.forEach((creature, index) => { | |
const creatureEl = createCreatureElement(creature, 'player', index); | |
creatureEl.id = `creature-player-${index}`; | |
creatureEl.dataset.hasAttacked = creature.hasAttacked; | |
creatureEl.dataset.summoningSickness = creature.summoningSickness; | |
playerFieldLaneEl.appendChild(creatureEl); | |
}); | |
// Shadow Lane | |
gameState.playerShadowLane.forEach((creature, index) => { | |
const creatureEl = createCreatureElement(creature, 'player', index); | |
creatureEl.id = `creature-player-${index}`; | |
creatureEl.dataset.hasAttacked = creature.hasAttacked; | |
creatureEl.dataset.summoningSickness = creature.summoningSickness; | |
playerShadowLaneEl.appendChild(creatureEl); | |
}); | |
} | |
function renderEnemyLanes() { | |
enemyFieldLaneEl.innerHTML = ''; | |
enemyShadowLaneEl.innerHTML = ''; | |
// Field Lane | |
gameState.enemyFieldLane.forEach((creature, index) => { | |
const creatureEl = createCreatureElement(creature, 'enemy', index); | |
creatureEl.id = `creature-enemy-${index}`; | |
enemyFieldLaneEl.appendChild(creatureEl); | |
}); | |
// Shadow Lane | |
gameState.enemyShadowLane.forEach((creature, index) => { | |
const creatureEl = createCreatureElement(creature, 'enemy', index); | |
creatureEl.id = `creature-enemy-${index}`; | |
enemyShadowLaneEl.appendChild(creatureEl); | |
}); | |
} | |
function createCardElement(card, location) { | |
const cardEl = document.createElement('div'); | |
cardEl.className = `card relative ${location === 'hand' ? 'hand-card' : 'board-card'} bg-slate-700 rounded-lg overflow-hidden shadow-lg`; | |
cardEl.dataset.id = card.id; | |
// Card front | |
const cardFront = document.createElement('div'); | |
cardFront.className = 'card-front w-full h-full flex flex-col'; | |
// Card image | |
const imgContainer = document.createElement('div'); | |
imgContainer.className = `card-image ${location === 'hand' ? '' : 'board-card-image'}`; | |
imgContainer.style.backgroundImage = `url(${card.image})`; | |
// Card info | |
const infoContainer = document.createElement('div'); | |
infoContainer.className = 'bg-slate-800 p-2'; | |
// Name and cost | |
const nameCost = document.createElement('div'); | |
nameCost.className = 'flex justify-between items-center mb-1'; | |
const nameEl = document.createElement('div'); | |
nameEl.className = 'font-semibold card-name text-amber-200'; | |
nameEl.textContent = card.name; | |
const costEl = document.createElement('div'); | |
costEl.className = 'bg-blue-500 text-white font-bold rounded-full card-cost flex items-center justify-center'; | |
costEl.textContent = card.cost; | |
nameCost.appendChild(nameEl); | |
nameCost.appendChild(costEl); | |
// Type and stats (if creature) | |
const typeStats = document.createElement('div'); | |
typeStats.className = 'flex justify-between items-center'; | |
const typeEl = document.createElement('div'); | |
typeEl.className = 'text-slate-300 italic card-type'; | |
typeEl.textContent = card.type; | |
typeStats.appendChild(typeEl); | |
if (card.type === 'creature') { | |
const statsEl = document.createElement('div'); | |
statsEl.className = 'flex space-x-2 card-stats'; | |
const attackEl = document.createElement('div'); | |
attackEl.className = 'bg-red-500 text-white font-bold rounded-full w-7 h-7 flex items-center justify-center'; | |
attackEl.textContent = card.attack; | |
const healthEl = document.createElement('div'); | |
healthEl.className = 'bg-green-500 text-white font-bold rounded-full w-7 h-7 flex items-center justify-center'; | |
healthEl.textContent = card.health; | |
statsEl.appendChild(attackEl); | |
statsEl.appendChild(healthEl); | |
typeStats.appendChild(statsEl); | |
} | |
infoContainer.appendChild(nameCost); | |
infoContainer.appendChild(typeStats); | |
cardFront.appendChild(imgContainer); | |
cardFront.appendChild(infoContainer); | |
// Tooltip | |
const tooltip = document.createElement('div'); | |
tooltip.className = 'card-tooltip'; | |
const tooltipName = document.createElement('div'); | |
tooltipName.className = 'font-bold text-amber-300 mb-1'; | |
tooltipName.textContent = card.name; | |
const tooltipType = document.createElement('div'); | |
tooltipType.className = 'text-xs text-slate-300 mb-2'; | |
tooltipType.textContent = `${card.type.charAt(0).toUpperCase() + card.type.slice(1)} • ${card.cost} mana`; | |
const tooltipDesc = document.createElement('div'); | |
tooltipDesc.className = 'text-sm'; | |
tooltipDesc.textContent = card.description; | |
if (card.keywords && card.keywords.length > 0) { | |
const tooltipKeywords = document.createElement('div'); | |
tooltipKeywords.className = 'mt-2 flex flex-wrap gap-1'; | |
card.keywords.forEach(keyword => { | |
const keywordEl = document.createElement('span'); | |
keywordEl.className = 'bg-slate-600 text-xs px-2 py-1 rounded card-keyword'; | |
keywordEl.textContent = keyword; | |
tooltipKeywords.appendChild(keywordEl); | |
}); | |
tooltip.appendChild(tooltipKeywords); | |
} | |
tooltip.appendChild(tooltipName); | |
tooltip.appendChild(tooltipType); | |
tooltip.appendChild(tooltipDesc); | |
cardEl.appendChild(cardFront); | |
cardEl.appendChild(tooltip); | |
// Add event listeners for hand cards | |
if (location === 'hand') { | |
cardEl.addEventListener('click', () => { | |
if (gameState.turn === 'player' && !cardEl.classList.contains('cursor-not-allowed')) { | |
// For creatures, ask which lane to play in | |
if (card.type === 'creature') { | |
const lane = confirm("Play in Field Lane? (OK for Field, Cancel for Shadow)") ? 'field' : 'shadow'; | |
playCard('player', card, lane); | |
} else { | |
// For actions/supports, just play them | |
playCard('player', card); | |
} | |
} | |
}); | |
} | |
return cardEl; | |
} | |
function createCreatureElement(creature, who, index) { | |
const creatureEl = document.createElement('div'); | |
creatureEl.className = `creature relative inline-block m-2 ${creature.summoningSickness ? 'opacity-70' : ''}`; | |
creatureEl.id = `creature-${who}-${index}`; | |
const cardEl = createCardElement(creature, 'board'); | |
// Add health bar | |
const healthBar = document.createElement('div'); | |
healthBar.className = 'health-bar bg-green-500 rounded-b'; | |
healthBar.style.width = '100%'; | |
const infoContainer = cardEl.querySelector('.bg-slate-800'); | |
infoContainer.appendChild(healthBar); | |
// Update stats if damaged | |
const originalCard = cardDatabase.find(c => c.id === creature.id); | |
if (creature.health < originalCard.health) { | |
const healthEl = cardEl.querySelector('.bg-green-500'); | |
if (healthEl) healthEl.textContent = creature.health; | |
healthBar.style.width = `${(creature.health / originalCard.health) * 100}%`; | |
if (creature.health <= 2) { | |
healthBar.className = 'health-bar bg-red-500 rounded-b'; | |
} | |
} | |
// Add summoning sickness indicator | |
if (creature.summoningSickness) { | |
const sicknessIcon = document.createElement('div'); | |
sicknessIcon.className = 'absolute top-0 right-0 bg-yellow-500 text-black text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center'; | |
sicknessIcon.title = "Summoning Sickness"; | |
sicknessIcon.innerHTML = '<i class="fas fa-hourglass-start"></i>'; | |
cardEl.appendChild(sicknessIcon); | |
} | |
// Add attack indicator if hasn't attacked | |
if (!creature.hasAttacked && !creature.summoningSickness && gameState.turn === who) { | |
const attackIcon = document.createElement('div'); | |
attackIcon.className = 'absolute top-0 left-0 bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center'; | |
attackIcon.title = "Ready to Attack"; | |
attackIcon.innerHTML = '<i class="fas fa-bolt"></i>'; | |
cardEl.appendChild(attackIcon); | |
} | |
creatureEl.appendChild(cardEl); | |
// Add event listener for attacking | |
if (who === 'player' && !creature.hasAttacked && !creature.summoningSickness && gameState.turn === 'player') { | |
creatureEl.classList.add('cursor-pointer', 'hover:scale-105'); | |
creatureEl.addEventListener('click', () => { | |
// For simplicity, just attack enemy directly | |
attackTarget('player', creature, {type: 'player'}); | |
}); | |
} | |
return creatureEl; | |
} | |
// Initialize | |
document.addEventListener('DOMContentLoaded', () => { | |
// Show welcome message | |
setTimeout(() => { | |
rulesModal.classList.remove('hidden'); | |
}, 500); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Juno360219/dnd-style-card-game" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |