Deckbuilder
thinkingcap qwen3.6 27b · basic

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 50,984 bytes · SHA-256 89174cd2a8c5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shadows of Aethelgard</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a0f;
color: #d4c5a9;
font-family: 'Crimson Text', serif;
overflow: hidden;
height: 100vh;
width: 100vw;
}
#game-container {
width: 100vw;
height: 100vh;
position: relative;
background: radial-gradient(ellipse at center, #1a1520 0%, #0a0a0f 70%);
}
.screen { display: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; }
.screen.active { display: flex; }
/* Main Menu */
#main-menu {
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #1a1520 0%, #050508 100%);
}
.title {
font-family: 'Cinzel', serif;
font-size: 3.5em;
font-weight: 900;
color: #c9a84c;
text-shadow: 0 0 20px rgba(201,168,76,0.5), 0 0 40px rgba(201,168,76,0.2);
margin-bottom: 10px;
letter-spacing: 4px;
}
.subtitle {
font-family: 'Cinzel', serif;
font-size: 1.2em;
color: #8a7a5a;
margin-bottom: 50px;
letter-spacing: 6px;
}
.menu-btn {
font-family: 'Cinzel', serif;
font-size: 1.3em;
padding: 14px 50px;
margin: 8px;
background: linear-gradient(180deg, #2a2530, #1a1520);
border: 2px solid #4a3f5c;
color: #c9a84c;
cursor: pointer;
letter-spacing: 3px;
transition: all 0.3s;
min-width: 280px;
}
.menu-btn:hover {
background: linear-gradient(180deg, #3a3540, #2a2530);
border-color: #c9a84c;
box-shadow: 0 0 15px rgba(201,168,76,0.3);
}
/* Class Selection */
#class-select {
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #1a1520 0%, #050508 100%);
}
.class-cards {
display: flex;
gap: 30px;
margin-top: 30px;
}
.class-card {
width: 260px;
padding: 25px;
background: linear-gradient(180deg, #1e1a25, #12101a);
border: 2px solid #3a3545;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.class-card:hover {
transform: translateY(-8px);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.class-card.bastion:hover { border-color: #a0b0c0; box-shadow: 0 10px 30px rgba(160,176,192,0.3); }
.class-card.wither:hover { border-color: #d45a2a; box-shadow: 0 10px 30px rgba(212,90,42,0.3); }
.class-card.shroud:hover { border-color: #8a6aaa; box-shadow: 0 10px 30px rgba(138,106,170,0.3); }
.class-name {
font-family: 'Cinzel', serif;
font-size: 1.5em;
font-weight: 700;
margin-bottom: 8px;
}
.bastion .class-name { color: #a0b0c0; }
.wither .class-name { color: #d45a2a; }
.shroud .class-name { color: #8a6aaa; }
.class-desc { font-size: 0.9em; color: #8a7a6a; margin-bottom: 12px; line-height: 1.4; }
.class-stats { font-size: 0.85em; color: #6a6a7a; }
/* Combat Screen */
#combat-screen {
flex-direction: column;
background: radial-gradient(ellipse at center, #1a1520 0%, #0a0a0f 100%);
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background: linear-gradient(180deg, rgba(30,25,40,0.95), rgba(20,16,30,0.9));
border-bottom: 1px solid #2a2535;
height: 45px;
flex-shrink: 0;
}
.top-bar-section { display: flex; align-items: center; gap: 15px; }
.stat-label { font-family: 'Cinzel', serif; font-size: 0.8em; color: #6a6a7a; letter-spacing: 2px; }
.stat-value { font-family: 'Cinzel', serif; font-size: 1em; color: #c9a84c; }
.gold-icon { color: #f0c040; }
.floor-icon { color: #7ab; }
.combat-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
min-height: 0;
}
.enemy-zone {
text-align: center;
position: relative;
}
.enemy-sprite {
width: 120px;
height: 140px;
margin: 0 auto 10px;
position: relative;
}
.enemy-hp-bar-container {
width: 160px;
height: 18px;
background: #1a1520;
border: 1px solid #3a3545;
margin: 0 auto;
position: relative;
}
.enemy-hp-bar {
height: 100%;
background: linear-gradient(90deg, #8b2020, #c0392b);
transition: width 0.5s;
}
.enemy-hp-text {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cinzel', serif;
font-size: 0.7em;
color: #fff;
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
}
.enemy-name {
font-family: 'Cinzel', serif;
font-size: 1em;
color: #a09080;
margin-bottom: 5px;
}
.enemy-statuses {
display: flex;
gap: 5px;
justify-content: center;
margin-top: 5px;
}
.status-badge {
font-size: 0.7em;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Cinzel', serif;
}
.status-burn { background: #4a3510; color: #d4a020; border: 1px solid #6a5020; }
.status-poison { background: #1a3a1a; color: #60c060; border: 1px solid #306030; }
.status-stun { background: #2a2a4a; color: #8080d0; border: 1px solid #404080; }
.player-zone {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
}
.player-stats-bar {
display: flex;
align-items: center;
gap: 20px;
padding: 8px 20px;
background: linear-gradient(180deg, rgba(30,25,40,0.9), rgba(20,16,30,0.8));
border: 1px solid #2a2535;
margin-bottom: 8px;
}
.player-hp-bar-container {
width: 180px;
height: 14px;
background: #1a1520;
border: 1px solid #3a3545;
}
.player-hp-bar {
height: 100%;
background: linear-gradient(90deg, #1a6b1a, #2ecc40);
transition: width 0.5s;
}
.energy-display {
display: flex;
gap: 3px;
}
.energy-pip {
width: 18px;
height: 18px;
border-radius: 50%;
background: #2a2540;
border: 1px solid #4a4560;
transition: all 0.3s;
}
.energy-pip.filled {
background: radial-gradient(circle, #7080e0, #4050a0);
box-shadow: 0 0 6px rgba(100,120,220,0.5);
}
.defense-display {
font-family: 'Cinzel', serif;
color: #7ab8d4;
font-size: 0.9em;
}
/* Card Hand */
.card-hand-area {
height: 220px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
position: relative;
}
.card-hand {
display: flex;
gap: 8px;
perspective: 800px;
}
.game-card {
width: 130px;
height: 190px;
cursor: pointer;
transition: all 0.25s ease;
position: relative;
flex-shrink: 0;
}
.game-card:hover {
transform: translateY(-20px) scale(1.08);
z-index: 10;
}
.game-card.disabled {
opacity: 0.45;
cursor: not-allowed;
}
.game-card.disabled:hover {
transform: none;
}
.card-svg-frame {
width: 100%;
height: 100%;
}
/* Shop Screen */
#shop-screen {
flex-direction: column;
align-items: center;
background: radial-gradient(ellipse at center, #1a1520 0%, #050508 100%);
}
.shop-title {
font-family: 'Cinzel', serif;
font-size: 2em;
color: #c9a84c;
margin-top: 30px;
letter-spacing: 4px;
}
.shop-gold {
font-family: 'Cinzel', serif;
font-size: 1.2em;
color: #f0c040;
margin: 10px 0 20px;
}
.shop-cards {
display: flex;
gap: 15px;
flex-wrap: wrap;
justify-content: center;
max-width: 900px;
padding: 20px;
}
.shop-card-wrapper {
text-align: center;
}
.shop-card-price {
font-family: 'Cinzel', serif;
font-size: 0.85em;
color: #f0c040;
margin-top: 6px;
}
.shop-card-count {
font-size: 0.7em;
color: #6a6a7a;
}
/* Game Over / Victory */
#game-over, #victory-screen {
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #1a1520 0%, #050508 100%);
}
.game-over-title {
font-family: 'Cinzel', serif;
font-size: 3em;
color: #c0392b;
text-shadow: 0 0 20px rgba(192,57,43,0.5);
margin-bottom: 20px;
}
.victory-title {
font-family: 'Cinzel', serif;
font-size: 3em;
color: #c9a84c;
text-shadow: 0 0 20px rgba(201,168,76,0.5);
margin-bottom: 20px;
}
/* Damage numbers */
.damage-number {
position: absolute;
font-family: 'Cinzel', serif;
font-weight: 900;
pointer-events: none;
animation: floatUp 1s ease-out forwards;
z-index: 100;
}
@keyframes floatUp {
0% { opacity: 1; transform: translateY(0) scale(1.2); }
100% { opacity: 0; transform: translateY(-60px) scale(0.8); }
}
/* Turn indicator */
.turn-indicator {
font-family: 'Cinzel', serif;
font-size: 0.9em;
color: #7ab;
letter-spacing: 2px;
padding: 4px 15px;
border: 1px solid #3a3545;
background: rgba(20,16,30,0.8);
}
/* End Turn Button */
.end-turn-btn {
font-family: 'Cinzel', serif;
font-size: 1em;
padding: 10px 40px;
background: linear-gradient(180deg, #3a2520, #2a1a15);
border: 2px solid #6a4a3a;
color: #d4a070;
cursor: pointer;
letter-spacing: 2px;
transition: all 0.3s;
}
.end-turn-btn:hover {
background: linear-gradient(180deg, #4a3530, #3a2520);
border-color: #d4a070;
box-shadow: 0 0 10px rgba(212,160,112,0.3);
}
.end-turn-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Room transition */
#room-transition {
flex-direction: column;
align-items: center;
justify-content: center;
background: #050508;
z-index: 200;
}
.room-text {
font-family: 'Cinzel', serif;
font-size: 1.8em;
color: #c9a84c;
letter-spacing: 3px;
animation: fadeInText 1s ease-in;
}
@keyframes fadeInText {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Card tooltip */
.card-tooltip {
display: none;
position: absolute;
bottom: 105%;
left: 50%;
transform: translateX(-50%);
width: 200px;
padding: 10px;
background: rgba(15,12,20,0.95);
border: 1px solid #4a3f5c;
z-index: 50;
pointer-events: none;
}
.game-card:hover .card-tooltip { display: block; }
.tooltip-name {
font-family: 'Cinzel', serif;
font-size: 0.85em;
color: #c9a84c;
margin-bottom: 4px;
}
.tooltip-type {
font-size: 0.7em;
letter-spacing: 1px;
margin-bottom: 6px;
}
.tooltip-desc {
font-size: 0.75em;
color: #a09888;
line-height: 1.3;
}
/* Enemy attack animation */
.enemy-attack-anim {
animation: enemyAttack 0.4s ease-in-out;
}
@keyframes enemyAttack {
0% { transform: translateX(0); }
30% { transform: translateX(-30px) scale(1.1); }
60% { transform: translateX(20px); }
100% { transform: translateX(0) scale(1); }
}
.player-hit-anim {
animation: playerHit 0.3s ease-in-out;
}
@keyframes playerHit {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(2) hue-rotate(20deg); }
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0a0a0f; }
::-webkit-scrollbar-thumb { background: #3a3545; border-radius: 3px; }
.shop-scroll-area {
flex: 1;
overflow-y: auto;
width: 100%;
display: flex;
justify-content: center;
padding-bottom: 20px;
}
/* Card play animation */
.card-play-anim {
animation: cardPlay 0.4s ease-out forwards;
}
@keyframes cardPlay {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.3) translateY(-40px); opacity: 0.8; }
100% { transform: scale(0.5) translateY(-80px); opacity: 0; }
}
/* Boss indicator */
.boss-indicator {
font-family: 'Cinzel', serif;
color: #c0392b;
font-size: 0.75em;
letter-spacing: 2px;
text-shadow: 0 0 8px rgba(192,57,43,0.5);
}
/* Deck info */
.deck-info {
font-family: 'Cinzel', serif;
font-size: 0.75em;
color: #6a6a7a;
}
</style>
</head>
<body>
<div id="game-container">
<!-- Main Menu -->
<div id="main-menu" class="screen active">
<div class="title">SHADOWS OF AETHELGARD</div>
<div class="subtitle">A ROGUELIKE CARD BATTLER</div>
<button class="menu-btn" onclick="showClassSelect()">BEGIN DESCENT</button>
</div>
<!-- Class Selection -->
<div id="class-select" class="screen">
<div class="title" style="font-size:2em;">CHOOSE YOUR HERO</div>
<div class="class-cards">
<div class="class-card bastion" onclick="selectClass('bastion')">
<div class="class-name">THE BASTION</div>
<div class="class-desc">Immovable Object. High Defense, High Block, Low Speed.</div>
<div class="class-stats">HP: 35 | Energy: 3</div>
</div>
<div class="class-card wither" onclick="selectClass('wither')">
<div class="class-name">THE WITHER</div>
<div class="class-desc">Rot and Burn. Status Effects, Debuff Control.</div>
<div class="class-stats">HP: 30 | Energy: 5</div>
</div>
<div class="class-card shroud" onclick="selectClass('shroud')">
<div class="class-name">THE SHROUD</div>
<div class="class-desc">Shadow and Speed. Evasion, Assassination.</div>
<div class="class-stats">HP: 28 | Energy: 5</div>
</div>
</div>
</div>
<!-- Room Transition -->
<div id="room-transition" class="screen">
<div class="room-text" id="room-text"></div>
</div>
<!-- Combat Screen -->
<div id="combat-screen" class="screen">
<div class="top-bar">
<div class="top-bar-section">
<span class="stat-label">GOLD</span>
<span class="stat-value gold-icon" id="gold-display">0</span>
</div>
<div class="top-bar-section">
<span class="turn-indicator" id="turn-indicator">TURN 1</span>
<span class="boss-indicator" id="boss-indicator"></span>
</div>
<div class="top-bar-section">
<span class="stat-label">FLOOR</span>
<span class="stat-value floor-icon" id="floor-display">1</span>
<span class="deck-info" id="deck-count"></span>
</div>
</div>
<div class="combat-area">
<div class="enemy-zone" id="enemy-zone">
<div class="enemy-name" id="enemy-name">Goblin</div>
<svg class="enemy-sprite" id="enemy-svg" viewBox="0 0 120 140"></svg>
<div class="enemy-hp-bar-container">
<div class="enemy-hp-bar" id="enemy-hp-bar" style="width:100%"></div>
<div class="enemy-hp-text" id="enemy-hp-text">20/20</div>
</div>
<div class="enemy-statuses" id="enemy-statuses"></div>
</div>
<div class="player-zone">
<div class="player-stats-bar">
<span style="font-family:'Cinzel',serif;font-size:0.85em;color:#a09080;">HP</span>
<div class="player-hp-bar-container">
<div class="player-hp-bar" id="player-hp-bar" style="width:100%"></div>
</div>
<span id="player-hp-text" style="font-family:'Cinzel',serif;font-size:0.85em;color:#d4c5a9;">30/30</span>
<span style="color:#6a6a7a;margin:0 5px;">|</span>
<span style="font-family:'Cinzel',serif;font-size:0.85em;color:#7ab;">ENERGY</span>
<div class="energy-display" id="energy-display"></div>
<span style="color:#6a6a7a;margin:0 5px;">|</span>
<span class="defense-display" id="defense-display">DEF: 0</span>
</div>
</div>
</div>
<div class="card-hand-area">
<div class="card-hand" id="card-hand"></div>
</div>
<div style="text-align:center;padding-bottom:15px;">
<button class="end-turn-btn" id="end-turn-btn" onclick="endPlayerTurn()">END TURN</button>
</div>
</div>
<!-- Shop Screen -->
<div id="shop-screen" class="screen">
<div class="shop-title">CARD SHOP</div>
<div class="shop-gold" id="shop-gold-display">Gold: 0</div>
<div class="shop-scroll-area">
<div class="shop-cards" id="shop-cards"></div>
</div>
<button class="menu-btn" onclick="nextRoom()" style="margin-top:15px;">CONTINUE DESCENT</button>
</div>
<!-- Game Over -->
<div id="game-over" class="screen">
<div class="game-over-title">YOU HAVE FALLEN</div>
<div style="color:#8a7a6a;margin-bottom:30px;">The Void consumes all...</div>
<button class="menu-btn" onclick="returnToMenu()">RETURN TO MENU</button>
</div>
<!-- Victory -->
<div id="victory-screen" class="screen">
<div class="victory-title">VICTORY</div>
<div style="color:#c9a84c;margin-bottom:10px;font-family:'Cinzel',serif;">The Void has been vanquished!</div>
<div style="color:#8a7a6a;margin-bottom:30px;">Aethelgard is reclaimed.</div>
<button class="menu-btn" onclick="returnToMenu()">RETURN TO MENU</button>
</div>
</div>
<script>
// ==================== CARD DATA ====================
const ALL_CARDS = {
// BASTION CARDS
'BA-A01': { id:'BA-A01', name:'Slam', type:'Attack', cost:2, atk:3, def:0, desc:'Deals 3 damage. If you have 30+ HP, draw 1 card.', class:'bastion' },
'BA-A02': { id:'BA-A02', name:'Earthquake', type:'Attack', cost:3, atk:4, def:0, desc:'Deals 4 damage to all enemies.', class:'bastion', aoe:true },
'BA-A03': { id:'BA-A03', name:'Stone Fist', type:'Attack', cost:1, atk:2, def:0, desc:'Deals 2 damage.', class:'bastion' },
'BA-A04': { id:'BA-A04', name:'Boulder', type:'Attack', cost:4, atk:6, def:0, desc:'Deals 6 damage. Costs 4 Energy.', class:'bastion' },
'BA-D01': { id:'BA-D01', name:'Shield Wall', type:'Defense', cost:0, atk:0, def:4, desc:'Gain 4 Defense for this turn.', class:'bastion' },
'BA-D02': { id:'BA-D02', name:'Iron Skin', type:'Defense', cost:2, atk:0, def:5, desc:'Gain 5 Defense permanently. Discard 1 card.', class:'bastion', permanent:true },
'BA-D03': { id:'BA-D03', name:'Stand Firm', type:'Defense', cost:1, atk:0, def:2, desc:'Gain 2 Defense.', class:'bastion' },
'BA-D04': { id:'BA-D04', name:'Taunt', type:'Defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Stun 1 enemy for 1 turn.', class:'bastion', stun:true },
'BA-U01': { id:'BA-U01', name:'Search', type:'Other', cost:2, atk:0, def:0, desc:'Draw 1 card. Discard hand if > 5 cards.', class:'bastion' },
'BA-U02': { id:'BA-U02', name:'Repair', type:'Other', cost:3, atk:0, def:0, desc:'Heal 6 HP.', class:'bastion', heal:6 },
'BA-U03': { id:'BA-U03', name:'Rally', type:'Other', cost:1, atk:0, def:0, desc:'Draw 1 card. Gain 1 Energy.', class:'bastion' },
'BA-U04': { id:'BA-U04', name:'Loot', type:'Other', cost:2, atk:0, def:0, desc:'Gain 5 Gold.', class:'bastion', gold:5 },
// WITHER CARDS
'WI-A01': { id:'WI-A01', name:'Fireball', type:'Attack', cost:2, atk:3, def:0, desc:'Deals 3 damage. Apply 1 Burn to target.', class:'wither', burn:1 },
'WI-A02': { id:'WI-A02', name:'Inferno', type:'Attack', cost:3, atk:4, def:0, desc:'Deals 4 damage. Applies 3 Burn.', class:'wither', burn:3 },
'WI-A03': { id:'WI-A03', name:'Ember', type:'Attack', cost:1, atk:2, def:0, desc:'Deals 2 damage. Deals 1 damage every turn to target.', class:'wither', burn:1 },
'WI-A04': { id:'WI-A04', name:'Pyre', type:'Attack', cost:4, atk:5, def:0, desc:'Deals 5 damage. All enemies take 1 damage next turn.', class:'wither', aoe:true, burn:1 },
'WI-D01': { id:'WI-D01', name:'Scorch Shield', type:'Defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Block 1 Burn damage.', class:'wither' },
'WI-D02': { id:'WI-D02', name:'Heat Resist', type:'Defense', cost:1, atk:0, def:1, desc:'Gain 1 Defense. Gain 1 Energy.', class:'wither' },
'WI-D03': { id:'WI-D03', name:'Smoke', type:'Defense', cost:2, atk:0, def:4, desc:'Gain 4 Defense. Block 1 Burn damage.', class:'wither' },
'WI-D04': { id:'WI-D04', name:'Endure', type:'Defense', cost:3, atk:0, def:2, desc:'Gain 2 Defense. Heal 2 HP.', class:'wither', heal:2 },
'WI-U01': { id:'WI-U01', name:'Poison', type:'Other', cost:2, atk:0, def:0, desc:'Apply 1 Poison to target.', class:'wither', poison:1 },
'WI-U02': { id:'WI-U02', name:'Burn', type:'Other', cost:3, atk:0, def:0, desc:'Deal 3 damage + 2 Burn to all enemies.', class:'wither', aoe:true, burn:2, atkVal:3 },
'WI-U03': { id:'WI-U03', name:'Slow', type:'Other', cost:1, atk:0, def:0, desc:'Draw 1 card. Target cannot move next turn (Stun).', class:'wither', stun:true },
'WI-U04': { id:'WI-U04', name:'Wither', type:'Other', cost:2, atk:0, def:0, desc:'Heal 5 HP. Reduce enemy Attack by 1.', class:'wither', heal:5 },
// SHROUD CARDS
'SH-A01': { id:'SH-A01', name:'Backstab', type:'Attack', cost:2, atk:4, def:0, desc:'Deals 4 damage. +1 Damage if target has no Armor.', class:'shroud' },
'SH-A02': { id:'SH-A02', name:'Shadow Bolt', type:'Attack', cost:3, atk:5, def:0, desc:'Deals 5 damage. Target cannot block.', class:'shroud', unblockable:true },
'SH-A03': { id:'SH-A03', name:'Silence', type:'Attack', cost:1, atk:3, def:0, desc:'Deals 3 damage. Target loses ability to use cards.', class:'shroud' },
'SH-A04': { id:'SH-A04', name:'Vengeance', type:'Attack', cost:3, atk:6, def:0, desc:'Deals 6 damage. Gain 2 Energy.', class:'shroud' },
'SH-D01': { id:'SH-D01', name:'Blur', type:'Defense', cost:3, atk:0, def:2, desc:'Gain 2 Defense. Ignore next attack.', class:'shroud', dodge:true },
'SH-D02': { id:'SH-D02', name:'Step', type:'Defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Gain 1 Energy.', class:'shroud' },
'SH-D03': { id:'SH-D03', name:'Evasion', type:'Defense', cost:1, atk:0, def:1, desc:'Gain 1 Defense. Skip enemy action.', class:'shroud', skipEnemy:true },
'SH-D04': { id:'SH-D04', name:'Phantom', type:'Defense', cost:2, atk:0, def:4, desc:'Gain 4 Defense. Ghost form for 1 turn.', class:'shroud' },
'SH-U01': { id:'SH-U01', name:'Dash', type:'Other', cost:1, atk:0, def:0, desc:'Gain 1 Energy. Skip enemy attack.', class:'shroud', skipEnemy:true },
'SH-U02': { id:'SH-U02', name:'Teleport', type:'Other', cost:2, atk:0, def:0, desc:'Move to back row. Deal 2 damage.', class:'shroud', atkVal:2 },
'SH-U03': { id:'SH-U03', name:'Hide', type:'Other', cost:2, atk:0, def:1, desc:'Gain 1 Defense. Discard 1 card. Draw 2 cards.', class:'shroud' },
'SH-U04': { id:'SH-U04', name:'Assassinate', type:'Other', cost:3, atk:0, def:0, desc:'Deal 8 damage. Cannot be blocked this turn.', class:'shroud', atkVal:8, unblockable:true }
};
// ==================== GAME STATE ====================
let game = {
screen: 'main-menu',
heroClass: null,
hero: null,
enemy: null,
floor: 1,
room: 0,
turn: 1,
playerTurn: true,
combatActive: false,
deckCounts: {},
shopCards: [],
bossDefeated: [false, false, false]
};
// ==================== SVG CARD FRAMES ====================
function getCardFrameSVG(cardData) {
const cls = cardData.class;
const type = cardData.type;
let borderColor, glowColor, accentColor, innerBorder;
if (cls === 'bastion') {
borderColor = '#8a9aaa';
glowColor = 'rgba(138,154,170,0.3)';
accentColor = '#6a7a8a';
innerBorder = '#5a6a7a';
} else if (cls === 'wither') {
borderColor = '#c05020';
glowColor = 'rgba(192,80,32,0.3)';
accentColor = '#a04018';
innerBorder = '#803010';
} else {
borderColor = '#7a5aaa';
glowColor = 'rgba(122,90,170,0.3)';
accentColor = '#6040a0';
innerBorder = '#503080';
}
let typeGlow;
if (type === 'Attack') typeGlow = '#d44a2a';
else if (type === 'Defense') typeGlow = '#6ab0d4';
else typeGlow = '#9a7ac0';
return `<svg viewBox="0 0 130 190" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow-${cls}"><feGaussianBlur stdDeviation="2" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<linearGradient id="bg-${cls}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#1e1a25"/>
<stop offset="100%" style="stop-color:#12101a"/>
</linearGradient>
</defs>
<!-- Outer glow -->
<rect x="2" y="2" width="126" height="186" rx="8" fill="none" stroke="${glowColor}" stroke-width="3" filter="url(#glow-${cls})"/>
<!-- Main border -->
<rect x="4" y="4" width="122" height="182" rx="7" fill="url(#bg-${cls})" stroke="${borderColor}" stroke-width="2"/>
<!-- Inner decorative border -->
<rect x="9" y="9" width="112" height="172" rx="4" fill="none" stroke="${innerBorder}" stroke-width="0.5"/>
${cls === 'bastion' ? `
<!-- Bastion: Shield emblem top -->
<polygon points="65,14 58,22 65,30 72,22" fill="${accentColor}" opacity="0.6"/>
<line x1="9" y1="35" x2="121" y2="35" stroke="${innerBorder}" stroke-width="0.5"/>
` : cls === 'wither' ? `
<!-- Wither: Flame emblem top -->
<path d="M65,14 Q60,22 65,30 Q70,22 65,14" fill="${accentColor}" opacity="0.6"/>
<line x1="9" y1="35" x2="121" y2="35" stroke="${innerBorder}" stroke-width="0.5"/>
` : `
<!-- Shroud: Eye emblem top -->
<ellipse cx="65" cy="22" rx="8" ry="4" fill="none" stroke="${accentColor}" stroke-width="1" opacity="0.6"/>
<circle cx="65" cy="22" r="2" fill="${accentColor}" opacity="0.6"/>
<line x1="9" y1="35" x2="121" y2="35" stroke="${innerBorder}" stroke-width="0.5"/>
`}
<!-- Cost circle -->
<circle cx="22" cy="22" r="10" fill="#1a1520" stroke="${borderColor}" stroke-width="1.5"/>
<text x="22" y="26" text-anchor="middle" font-family="Cinzel,serif" font-size="13" font-weight="bold" fill="${typeGlow}">${cardData.cost}</text>
<!-- Card name -->
<text x="65" y="48" text-anchor="middle" font-family="Cinzel,serif" font-size="9" font-weight="bold" fill="#c9a84c">${cardData.name}</text>
<!-- Type label -->
<text x="65" y="58" text-anchor="middle" font-family="Cinzel,serif" font-size="6" letter-spacing="1" fill="${typeGlow}" opacity="0.8">${type.toUpperCase()}</text>
<!-- Image placeholder area -->
<rect x="14" y="63" width="102" height="70" rx="3" fill="#0e0c15" stroke="${innerBorder}" stroke-width="0.5"/>
${cls === 'bastion' ? `
<!-- Bastion placeholder: stone texture lines -->
<line x1="20" y1="80" x2="110" y2="80" stroke="#2a2535" stroke-width="0.5"/>
<line x1="20" y1="95" x2="110" y2="95" stroke="#2a2535" stroke-width="0.5"/>
<line x1="20" y1="110" x2="110" y2="110" stroke="#2a2535" stroke-width="0.5"/>
<text x="65" y="102" text-anchor="middle" font-size="7" fill="#3a3545">STONE</text>
` : cls === 'wither' ? `
<!-- Wither placeholder: flame pattern -->
<path d="M65,85 Q55,95 60,105 Q50,100 55,112" fill="none" stroke="#3a2520" stroke-width="1"/>
<path d="M65,85 Q75,95 70,105 Q80,100 75,112" fill="none" stroke="#3a2520" stroke-width="1"/>
<text x="65" y="102" text-anchor="middle" font-size="7" fill="#3a2520">FLAME</text>
` : `
<!-- Shroud placeholder: shadow pattern -->
<circle cx="65" cy="98" r="15" fill="none" stroke="#2a2540" stroke-width="0.5"/>
<circle cx="65" cy="98" r="8" fill="none" stroke="#2a2540" stroke-width="0.5"/>
<text x="65" y="102" text-anchor="middle" font-size="7" fill="#2a2540">SHADOW</text>
`}
<!-- Description area -->
<rect x="14" y="138" width="102" height="42" rx="2" fill="#0e0c15" stroke="${innerBorder}" stroke-width="0.5"/>
<text x="65" y="152" text-anchor="middle" font-size="5.5" fill="#a09888">${cardData.desc.substring(0,35)}${cardData.desc.length>35?'...':''}</text>
${cardData.atk > 0 ? `<text x="65" y="162" text-anchor="middle" font-size="6" fill="#d44a2a">ATK: ${cardData.atk}</text>` : ''}
${cardData.def > 0 ? `<text x="65" y="${cardData.atk>0?170:162}" text-anchor="middle" font-size="6" fill="#6ab0d4">DEF: ${cardData.def}</text>` : ''}
<!-- Bottom decorative line -->
<line x1="9" y1="185" x2="121" y2="185" stroke="${innerBorder}" stroke-width="0.5"/>
</svg>`;
}
function getCardBackSVG(cls) {
let borderColor, accentColor, patternColor;
if (cls === 'bastion') {
borderColor = '#6a7a8a'; accentColor = '#4a5a6a'; patternColor = '#3a4a5a';
} else if (cls === 'wither') {
borderColor = '#803010'; accentColor = '#602008'; patternColor = '#501808';
} else {
borderColor = '#503080'; accentColor = '#402070'; patternColor = '#301860';
}
return `<svg viewBox="0 0 130 190" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="back-${cls}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1520"/>
<stop offset="100%" style="stop-color:#0e0c15"/>
</linearGradient>
</defs>
<rect x="4" y="4" width="122" height="182" rx="7" fill="url(#back-${cls})" stroke="${borderColor}" stroke-width="2"/>
<rect x="9" y="9" width="112" height="172" rx="4" fill="none" stroke="${accentColor}" stroke-width="0.5"/>
${cls === 'bastion' ? `
<!-- Bastion back: Shield pattern -->
<polygon points="65,30 45,50 45,100 65,120 85,100 85,50" fill="none" stroke="${patternColor}" stroke-width="1.5"/>
<polygon points="65,40 52,55 52,90 65,105 78,90 78,55" fill="none" stroke="${accentColor}" stroke-width="0.8"/>
<line x1="65" y1="30" x2="65" y2="120" stroke="${patternColor}" stroke-width="0.5"/>
<line x1="45" y1="75" x2="85" y2="75" stroke="${patternColor}" stroke-width="0.5"/>
` : cls === 'wither' ? `
<!-- Wither back: Flame pattern -->
<path d="M65,35 Q45,60 55,90 Q40,100 50,120" fill="none" stroke="${patternColor}" stroke-width="1.5"/>
<path d="M65,35 Q85,60 75,90 Q90,100 80,120" fill="none" stroke="${patternColor}" stroke-width="1.5"/>
<circle cx="65" cy="75" r="12" fill="none" stroke="${accentColor}" stroke-width="0.8"/>
<path d="M65,55 Q58,70 65,85 Q72,70 65,55" fill="${patternColor}" opacity="0.3"/>
` : `
<!-- Shroud back: Eye pattern -->
<ellipse cx="65" cy="75" rx="30" ry="18" fill="none" stroke="${patternColor}" stroke-width="1.5"/>
<circle cx="65" cy="75" r="12" fill="none" stroke="${accentColor}" stroke-width="1"/>
<circle cx="65" cy="75" r="5" fill="${patternColor}" opacity="0.4"/>
<line x1="35" y1="75" x2="95" y2="75" stroke="${patternColor}" stroke-width="0.5"/>
<path d="M65,57 Q50,75 65,93 Q80,75 65,57" fill="none" stroke="${accentColor}" stroke-width="0.5"/>
`}
<!-- Corner decorations -->
<circle cx="18" cy="18" r="2" fill="${patternColor}" opacity="0.5"/>
<circle cx="112" cy="18" r="2" fill="${patternColor}" opacity="0.5"/>
<circle cx="18" cy="172" r="2" fill="${patternColor}" opacity="0.5"/>
<circle cx="112" cy="172" r="2" fill="${patternColor}" opacity="0.5"/>
</svg>`;
}
// ==================== ENEMY SVG SPRITES ====================
function getEnemySVG(type, floor) {
let color1, color2, bodyShape;
if (floor === 1) {
color1 = '#4a6a30'; color2 = '#3a5a20';
bodyShape = `<ellipse cx="60" cy="80" rx="25" ry="30" fill="${color1}"/>
<circle cx="60" cy="45" r="18" fill="${color1}"/>
<circle cx="52" cy="42" r="3" fill="#ffcc00"/>
<circle cx="68" cy="42" r="3" fill="#ffcc00"/>
<path d="M50,50 Q60,58 70,50" fill="none" stroke="${color2}" stroke-width="1.5"/>
<rect x="35" y="90" width="10" height="25" rx="3" fill="${color2}"/>
<rect x="75" y="90" width="10" height="25" rx="3" fill="${color2}"/>
<rect x="40" y="110" width="8" height="20" rx="3" fill="${color2}"/>
<rect x="72" y="110" width="8" height="20" rx="3" fill="${color2}"/>`;
} else if (floor === 2) {
color1 = '#6a2040'; color2 = '#5a1830';
bodyShape = `<ellipse cx="60" cy="75" rx="28" ry="35" fill="${color1}"/>
<circle cx="60" cy="40" r="20" fill="${color1}"/>
<polygon points="45,25 40,10 55,22" fill="${color2}"/>
<polygon points="75,25 80,10 65,22" fill="${color2}"/>
<circle cx="52" cy="38" r="3" fill="#ff4444"/>
<circle cx="68" cy="38" r="3" fill="#ff4444"/>
<path d="M48,48 Q60,55 72,48" fill="none" stroke="${color2}" stroke-width="1.5"/>
<rect x="30" y="95" width="12" height="28" rx="4" fill="${color2}"/>
<rect x="78" y="95" width="12" height="28" rx="4" fill="${color2}"/>
<rect x="38" y="118" width="10" height="22" rx="4" fill="${color2}"/>
<rect x="72" y="118" width="10" height="22" rx="4" fill="${color2}"/>`;
} else {
color1 = '#3a1060'; color2 = '#2a0850';
bodyShape = `<ellipse cx="60" cy="75" rx="30" ry="38" fill="${color1}"/>
<circle cx="60" cy="38" r="22" fill="${color1}"/>
<path d="M40,20 Q35,5 50,18" fill="${color2}"/>
<path d="M80,20 Q85,5 70,18" fill="${color2}"/>
<circle cx="50" cy="35" r="4" fill="#aa44ff"/>
<circle cx="70" cy="35" r="4" fill="#aa44ff"/>
<path d="M46,48 Q60,58 74,48" fill="none" stroke="${color2}" stroke-width="2"/>
<rect x="28" y="98" width="14" height="30" rx="4" fill="${color2}"/>
<rect x="78" y="98" width="14" height="30" rx="4" fill="${color2}"/>
<rect x="36" y="122" width="12" height="25" rx="4" fill="${color2}"/>
<rect x="72" y="122" width="12" height="25" rx="4" fill="${color2}"/>`;
}
return `<svg viewBox="0 0 120 140" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="enemyGlow"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
</defs>
${bodyShape}
</svg>`;
}
// ==================== GAME FUNCTIONS ====================
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
game.screen = id;
}
function returnToMenu() {
showScreen('main-menu');
}
function showClassSelect() {
showScreen('class-select');
}
function selectClass(cls) {
game.heroClass = cls;
const hpMap = { bastion: 35, wither: 30, shroud: 28 };
const energyMap = { bastion: 3, wither: 5, shroud: 5 };
// Build starter deck (1 copy of each class card)
let starterDeck = [];
Object.values(ALL_CARDS).forEach(c => {
if (c.class === cls) starterDeck.push(c.id);
});
game.hero = {
hp: hpMap[cls],
maxHp: hpMap[cls],
energy: energyMap[cls],
maxEnergy: energyMap[cls],
handSize: 4,
gold: 0,
defense: 0,
permanentDefense: 0,
deck: [...starterDeck],
hand: [],
discardPile: [],
dodgeNextAttack: false,
skipEnemyAction: false
};
game.deckCounts = {};
starterDeck.forEach(id => { game.deckCounts[id] = (game.deckCounts[id] || 0) + 1; });
game.floor = 1;
game.room = 0;
game.turn = 1;
game.bossDefeated = [false, false, false];
showRoomTransition();
}
function showRoomTransition() {
const isBoss = (game.room === 3);
let enemyName;
if (isBoss) {
const bossNames = ['The Gatekeeper', 'The Corrupted', 'The Void Lord'];
enemyName = bossNames[game.floor - 1];
} else {
const floorEnemies = [
['Goblin Scout', 'Rat Swarm', 'Cave Goblin'],
['Vampire Knight', 'Ogre Brute', 'Dark Cultist'],
['Void Warrior', 'Shadow Priest', 'Abyssal Guard']
];
enemyName = floorEnemies[game.floor - 1][Math.min(game.room, 2)];
}
const text = isBoss ? `⚔ BOSS: ${enemyName} ⚔` : `Floor ${game.floor} — Room ${game.room + 1}`;
document.getElementById('room-text').textContent = text;
showScreen('room-transition');
setTimeout(() => {
startCombat(enemyName, isBoss);
}, 1500);
}
function createEnemy(name, isBoss) {
let hp, dmg;
if (isBoss) {
hp = 80;
dmg = 15;
} else {
const floorStats = [
{ hp: 20, dmg: 3 },
{ hp: 30, dmg: 5 },
{ hp: 40, dmg: 8 }
];
hp = floorStats[game.floor - 1].hp;
dmg = floorStats[game.floor - 1].dmg;
}
return {
name: name,
hp: hp,
maxHp: hp,
damage: dmg,
defense: 0,
isBoss: isBoss,
burn: 0,
poison: 0,
stun: 0,
attackReduction: 0
};
}
function startCombat(enemyName, isBoss) {
game.enemy = createEnemy(enemyName, isBoss);
game.turn = 1;
game.playerTurn = true;
game.combatActive = true;
game.hero.defense = game.hero.permanentDefense;
game.hero.dodgeNextAttack = false;
game.hero.skipEnemyAction = false;
// Reset deck from discard
game.hero.deck.push(...game.hero.discardPile);
game.hero.discardPile = [];
shuffleArray(game.hero.deck);
showScreen('combat-screen');
updateCombatUI();
drawCardsToHand();
}
function shuffleArray(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
function drawCardsToHand() {
while (game.hero.hand.length < game.hero.handSize && game.hero.deck.length > 0) {
game.hero.hand.push(game.hero.deck.pop());
}
}
function updateCombatUI() {
// Gold
document.getElementById('gold-display').textContent = game.hero.gold;
// Floor
document.getElementById('floor-display').textContent = game.floor;
// Turn
document.getElementById('turn-indicator').textContent = `TURN ${game.turn}`;
// Boss indicator
document.getElementById('boss-indicator').textContent = game.enemy.isBoss ? '⚔ BOSS ⚔' : '';
// Deck count
document.getElementById('deck-count').textContent = `${game.hero.deck.length} in deck`;
// Enemy
document.getElementById('enemy-name').textContent = game.enemy.name;
const hpPct = Math.max(0, (game.enemy.hp / game.enemy.maxHp) * 100);
document.getElementById('enemy-hp-bar').style.width = hpPct + '%';
document.getElementById('enemy-hp-text').textContent = `${Math.max(0, game.enemy.hp)}/${game.enemy.maxHp}`;
// Enemy SVG
document.getElementById('enemy-svg').innerHTML = getEnemySVG(game.enemy.name, game.floor);
// Enemy statuses
let statusHTML = '';
if (game.enemy.burn > 0) statusHTML += `<span class="status-badge status-burn">Burn ${game.enemy.burn}</span>`;
if (game.enemy.poison > 0) statusHTML += `<span class="status-badge status-poison">Poison ${game.enemy.poison}</span>`;
if (game.enemy.stun > 0) statusHTML += `<span class="status-badge status-stun">Stun</span>`;
document.getElementById('enemy-statuses').innerHTML = statusHTML;
// Player HP
const playerHpPct = Math.max(0, (game.hero.hp / game.hero.maxHp) * 100);
document.getElementById('player-hp-bar').style.width = playerHpPct + '%';
document.getElementById('player-hp-text').textContent = `${Math.max(0, game.hero.hp)}/${game.hero.maxHp}`;
// Energy pips
let energyHTML = '';
for (let i = 0; i < game.hero.maxEnergy; i++) {
energyHTML += `<div class="energy-pip ${i < game.hero.energy ? 'filled' : ''}"></div>`;
}
document.getElementById('energy-display').innerHTML = energyHTML;
// Defense
document.getElementById('defense-display').textContent = `DEF: ${game.hero.defense}`;
// Render hand
renderHand();
}
function renderHand() {
const handEl = document.getElementById('card-hand');
handEl.innerHTML = '';
game.hero.hand.forEach((cardId, idx) => {
const cardData = ALL_CARDS[cardId];
if (!cardData) return;
const canPlay = game.playerTurn && game.hero.energy >= cardData.cost;
const cardDiv = document.createElement('div');
cardDiv.className = `game-card ${canPlay ? '' : 'disabled'}`;
cardDiv.innerHTML = getCardFrameSVG(cardData);
// Tooltip
const tooltip = document.createElement('div');
tooltip.className = 'card-tooltip';
tooltip.innerHTML = `
<div class="tooltip-name">${cardData.name}</div>
<div class="tooltip-type" style="color:${cardData.type==='Attack'?'#d44a2a':cardData.type==='Defense'?'#6ab0d4':'#9a7ac0'}">${cardData.type} | Cost: ${cardData.cost}</div>
<div class="tooltip-desc">${cardData.desc}</div>
`;
cardDiv.appendChild(tooltip);
if (canPlay) {
cardDiv.onclick = () => playCard(idx);
}
handEl.appendChild(cardDiv);
});
}
function playCard(handIdx) {
if (!game.playerTurn || !game.combatActive) return;
const cardId = game.hero.hand[handIdx];
const cardData = ALL_CARDS[cardId];
if (!cardData || game.hero.energy < cardData.cost) return;
// Pay energy cost
game.hero.energy -= cardData.cost;
// Remove from hand
game.hero.hand.splice(handIdx, 1);
// Animate card play
const cards = document.querySelectorAll('.game-card');
if (cards[handIdx]) {
cards[handIdx].classList.add('card-play-anim');
}
// Resolve card effects
resolveCardEffect(cardData);
// Move to discard
game.hero.discardPile.push(cardId);
setTimeout(() => {
updateCombatUI();
checkEnemyDeath();
}, 400);
}
function resolveCardEffect(card) {
const enemy = game.enemy;
if (card.type === 'Attack') {
let dmg = card.atk;
// Bastion Slam: +draw if HP >= 30
if (card.id === 'BA-A01' && game.hero.hp >= 30) {
drawCardsToHand();
}
// Shroud Backstab: +1 if no armor
if (card.id === 'SH-A01' && enemy.defense <= 0) {
dmg += 1;
}
// Apply damage to enemy
let actualDmg = applyDamageToEnemy(dmg, card.unblockable);
showDamageNumber(actualDmg, '#d44a2a');
// Burn effects
if (card.burn) {
enemy.burn += card.burn;
}
// Shroud Vengeance: gain 2 energy
if (card.id === 'SH-A04') {
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 2);
}
}
if (card.type === 'Defense') {
let defAmount = card.def;
// Permanent defense
if (card.permanent) {
game.hero.permanentDefense += defAmount;
// Discard 1 card
if (game.hero.hand.length > 0) {
const discarded = game.hero.hand.pop();
game.hero.discardPile.push(discarded);
}
} else {
game.hero.defense += defAmount;
}
// Stun enemy
if (card.stun) {
enemy.stun = 1;
}
// Heal effects on defense cards
if (card.heal) {
game.hero.hp = Math.min(game.hero.maxHp, game.hero.hp + card.heal);
}
// Energy gain
if (card.id === 'WI-D02' || card.id === 'SH-D02') {
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
}
// Dodge / skip enemy
if (card.dodge) {
game.hero.dodgeNextAttack = true;
}
if (card.skipEnemy) {
game.hero.skipEnemyAction = true;
}
}
if (card.type === 'Other') {
// Search: draw 1, discard if > 5
if (card.id === 'BA-U01') {
drawCardsToHand();
while (game.hero.hand.length > 5) {
const d = game.hero.hand.pop();
game.hero.discardPile.push(d);
}
}
// Rally: draw 1, gain 1 energy
if (card.id === 'BA-U03') {
drawCardsToHand();
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
}
// Loot: gain gold
if (card.gold) {
game.hero.gold += card.gold;
}
// Heal
if (card.heal) {
game.hero.hp = Math.min(game.hero.maxHp, game.hero.hp + card.heal);
}
// Poison
if (card.poison) {
enemy.poison += card.poison;
}
// Burn AoE
if (card.id === 'WI-U02') {
let dmg = applyDamageToEnemy(card.atkVal || 3, false);
showDamageNumber(dmg, '#d4a020');
enemy.burn += card.burn;
}
// Slow: draw + stun
if (card.id === 'WI-U03') {
drawCardsToHand();
enemy.stun = 1;
}
// Wither: heal + reduce attack
if (card.id === 'WI-U04') {
game.hero.hp = Math.min(game.hero.maxHp, game.hero.hp + card.heal);
enemy.attackReduction += 1;
}
// Dash: energy + skip enemy
if (card.id === 'SH-U01') {
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
game.hero.skipEnemyAction = true;
}
// Teleport: deal damage
if (card.id === 'SH-U02') {
let dmg = applyDamageToEnemy(card.atkVal || 2, false);
showDamageNumber(dmg, '#8a6aaa');
}
// Hide: defense + discard + draw
if (card.id === 'SH-U03') {
game.hero.defense += card.def;
if (game.hero.hand.length > 0) {
const d = game.hero.hand.pop();
game.hero.discardPile.push(d);
}
drawCardsToHand();
drawCardsToHand();
}
// Assassinate: unblockable damage
if (card.id === 'SH-U04') {
let dmg = applyDamageToEnemy(card.atkVal || 8, true);
showDamageNumber(dmg, '#c0392b');
}
// Skip enemy action
if (card.skipEnemy) {
game.hero.skipEnemyAction = true;
}
}
}
function applyDamageToEnemy(amount, unblockable) {
let dmg = amount;
if (!unblockable && enemyHasDefense()) {
const blocked = Math.min(dmg, game.enemy.defense);
game.enemy.defense -= blocked;
dmg -= blocked;
}
game.enemy.hp -= dmg;
return dmg;
}
function enemyHasDefense() {
// Boss 1 blocks 5 per turn until HP < 20
if (game.enemy.isBoss && game.floor === 1 && game.enemy.hp >= 20) {
return true;
}
return false;
}
function showDamageNumber(amount, color) {
const zone = document.getElementById('enemy-zone');
const num = document.createElement('div');
num.className = 'damage-number';
num.textContent = `-${amount}`;
num.style.color = color;
num.style.fontSize = '1.5em';
num.style.left = '50%';
num.style.top = '30%';
num.style.transform = 'translateX(-50%)';
zone.appendChild(num);
setTimeout(() => num.remove(), 1000);
}
function showPlayerDamageNumber(amount) {
const zone = document.querySelector('.player-zone');
if (!zone) return;
const num = document.createElement('div');
num.className = 'damage-number';
num.textContent = `-${amount}`;
num.style.color = '#c0392b';
num.style.fontSize = '1.5em';
num.style.left = '50%';
num.style.top = '-20px';
num.style.transform = 'translateX(-50%)';
zone.appendChild(num);
setTimeout(() => num.remove(), 1000);
}
function checkEnemyDeath() {
if (game.enemy.hp <= 0) {
game.combatActive = false;
let goldReward = game.enemy.isBoss ? 15 : 5;
game.hero.gold += goldReward;
if (game.enemy.isBoss) {
game.bossDefeated[game.floor - 1] = true;
}
setTimeout(() => {
if (game.floor === 3 && game.room >= 3) {
showScreen('victory-screen');
} else {
openShop();
}
}, 800);
}
}
function endPlayerTurn() {
if (!game.playerTurn || !game.combatActive) return;
game.playerTurn = false;
// Discard hand to discard pile
while (game.hero.hand.length > 0) {
game.hero.discardPile.push(game.hero.hand.pop());
}
updateCombatUI();
setTimeout(() => enemyTurn(), 600);
}
function enemyTurn() {
if (!game.combatActive) return;
const enemy = game.enemy;
// Apply burn damage to enemy
if (enemy.burn > 0) {
enemy.hp -= enemy.burn;
showDamageNumber(enemy.burn, '#d4a020');
enemy.burn--;
}
// Apply poison damage to enemy
if (enemy.poison > 0) {
enemy.hp -= enemy.poison;
showDamageNumber(enemy.poison, '#60c060');
enemy.poison--;
}
// Boss 3 regen: +10 HP per turn
if (enemy.isBoss && game.floor === 3) {
enemy.hp = Math.min(enemy.maxHp + 40, enemy.hp + 10);
}
// Check enemy death from DoT
if (enemy.hp <= 0) {
checkEnemyDeath();
return;
}
// Boss 1: gain defense each turn until HP < 20
if (enemy.isBoss && game.floor === 1 && enemy.hp >= 20) {
enemy.defense += 5;
} else if (!enemy.isBoss || enemy.hp < 20) {
// Reset non-boss defense each turn
if (!enemy.isBoss) enemy.defense = 0;
}
// Stun check
if (enemy.stun > 0) {
enemy.stun--;
updateCombatUI();
startPlayerTurn();
return;
}
// Skip enemy action check
if (game.hero.skipEnemyAction) {
game.hero.skipEnemyAction = false;
updateCombatUI();
startPlayerTurn();
return;
}
// Enemy attacks
let dmg = Math.max(0, enemy.damage - enemy.attackReduction);
// Boss 2: deals 20 damage if player has < 3 energy
if (enemy.isBoss && game.floor === 2 && game.hero.energy < 3) {
dmg = 20;
}
// Dodge check
if (game.hero.dodgeNextAttack) {
game.hero.dodgeNextAttack = false;
showDamageNumber(0, '#7ab');
updateCombatUI();
startPlayerTurn();
return;
}
// Apply damage: first to defense, then to HP
let remainingDmg = dmg;
if (game.hero.defense > 0) {
const blocked = Math.min(remainingDmg, game.hero.defense);
game.hero.defense -= blocked;
remainingDmg -= blocked;
}
if (remainingDmg > 0) {
game.hero.hp -= remainingDmg;
showPlayerDamageNumber(remainingDmg);
// Hit animation
const enemySvg = document.getElementById('enemy-svg');
enemySvg.classList.add('enemy-attack-anim');
setTimeout(() => enemySvg.classList.remove('enemy-attack-anim'), 400);
}
updateCombatUI();
if (game.hero.hp <= 0) {
game.combatActive = false;
setTimeout(() => showScreen('game-over'), 500);
return;
}
startPlayerTurn();
}
function startPlayerTurn() {
game.turn++;
game.playerTurn = true;
// Reset temporary defense (permanent stays)
game.hero.defense = game.hero.permanentDefense;
// Recover energy
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
// Draw cards
drawCardsToHand();
updateCombatUI();
}
// ==================== SHOP ====================
function openShop() {
showScreen('shop-screen');
document.getElementById('shop-gold-display').textContent = `Gold: ${game.hero.gold}`;
const shopEl = document.getElementById('shop-cards');
shopEl.innerHTML = '';
// Generate random shop cards (5-7 cards)
game.shopCards = [];
const numShopCards = 5 + Math.floor(Math.random() * 3);
const allCardIds = Object.keys(ALL_CARDS);
for (let i = 0; i < numShopCards; i++) {
const randomId = allCardIds[Math.floor(Math.random() * allCardIds.length)];
game.shopCards.push(randomId);
}
game.shopCards.forEach(cardId => {
const cardData = ALL_CARDS[cardId];
const count = game.deckCounts[cardId] || 0;
let cost = 1 + Math.floor(count / 3) * 9; // 1 gold normally, 10 for 4th+ copy
if (count >= 5) cost = 999; // Max copies reached
const wrapper = document.createElement('div');
wrapper.className = 'shop-card-wrapper';
const cardDiv = document.createElement('div');
cardDiv.className = `game-card ${cost > game.hero.gold || count >= 5 ? 'disabled' : ''}`;
cardDiv.innerHTML = getCardFrameSVG(cardData);
if (cost <= game.hero.gold && count < 5) {
cardDiv.onclick = () => buyCard(cardId, cost);
}
const priceLabel = document.createElement('div');
priceLabel.className = 'shop-card-price';
priceLabel.textContent = count >= 5 ? 'MAX COPIES' : `${cost} Gold`;
const countLabel = document.createElement('div');
countLabel.className = 'shop-card-count';
countLabel.textContent = `Owned: ${count}/5`;
wrapper.appendChild(cardDiv);
wrapper.appendChild(priceLabel);
wrapper.appendChild(countLabel);
shopEl.appendChild(wrapper);
});
}
function buyCard(cardId, cost) {
if (game.hero.gold < cost || (game.deckCounts[cardId] || 0) >= 5) return;
game.hero.gold -= cost;
game.deckCounts[cardId] = (game.deckCounts[cardId] || 0) + 1;
game.hero.deck.push(cardId);
document.getElementById('shop-gold-display').textContent = `Gold: ${game.hero.gold}`;
openShop(); // Refresh shop
}
function nextRoom() {
game.room++;
if (game.room > 3) {
// Floor complete, move to next floor
game.floor++;
game.room = 0;
if (game.floor > 3) {
showScreen('victory-screen');
return;
}
}
showRoomTransition();
}
// ==================== INIT ====================
document.addEventListener('DOMContentLoaded', () => {
// Preload fonts and initialize
});
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"0ffe2772-66a1-4672-8d1d-b9c929aa73e9","tokensIn":3609,"tokensOut":19107,"tokensTotal":22716,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T02:07:34.910Z"} -->