Deckbuilder
thinkingcap qwen3.6 27b · middle

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,174 bytes · SHA-256 f929039507e2
<!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; }
:root {
--bg-dark: #0a0a0f;
--bg-mid: #151520;
--bg-light: #1e1e30;
--gold: #c9a84c;
--gold-dim: #8a7030;
--red-attack: #d44a2a;
--blue-defense: #3a6fd4;
--purple-other: #8b3ad4;
--green-heal: #3ad45c;
--burn-orange: #e8961e;
--poison-green: #2ecc71;
--hp-red: #cc2222;
--hp-bg: #331111;
--text-main: #d4cfc0;
--text-dim: #8a8578;
}
body {
background: var(--bg-dark);
color: var(--text-main);
font-family: 'Crimson Text', Georgia, serif;
overflow: hidden;
height: 100vh;
width: 100vw;
user-select: none;
}
h1, h2, h3, .title-font { font-family: 'Cinzel', serif; }
/* ===== SCREENS ===== */
.screen { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; flex-direction: column; align-items: center; justify-content: center; z-index: 1; }
.screen.active { display: flex; }
/* ===== TITLE SCREEN ===== */
#title-screen {
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a0f 70%);
}
#title-screen h1 {
font-size: 3.5em;
color: var(--gold);
text-shadow: 0 0 30px rgba(201,168,76,0.4), 0 2px 4px rgba(0,0,0,0.8);
letter-spacing: 4px;
margin-bottom: 8px;
}
#title-screen .subtitle {
font-size: 1.2em;
color: var(--text-dim);
margin-bottom: 50px;
font-style: italic;
}
/* ===== BUTTONS ===== */
.btn {
background: linear-gradient(180deg, #2a2a3e, #1a1a28);
border: 1px solid var(--gold-dim);
color: var(--gold);
padding: 14px 40px;
font-family: 'Cinzel', serif;
font-size: 1.1em;
cursor: pointer;
letter-spacing: 2px;
transition: all 0.2s;
margin: 6px;
min-width: 200px;
text-align: center;
}
.btn:hover { background: linear-gradient(180deg, #3a3a5e, #2a2a40); border-color: var(--gold); box-shadow: 0 0 15px rgba(201,168,76,0.2); }
.btn:active { transform: scale(0.97); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* ===== CLASS SELECT ===== */
#class-select { background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a0f 70%); }
.class-cards { display: flex; gap: 30px; margin-top: 30px; }
.class-card {
width: 220px; padding: 25px 18px; text-align: center; cursor: pointer;
background: linear-gradient(180deg, #1e1e30, #12121c);
border: 2px solid var(--gold-dim);
transition: all 0.3s;
}
.class-card:hover { border-color: var(--gold); transform: translateY(-5px); box-shadow: 0 8px 25px rgba(201,168,76,0.15); }
.class-card h3 { color: var(--gold); font-size: 1.3em; margin-bottom: 10px; }
.class-card p { color: var(--text-dim); font-size: 0.9em; line-height: 1.4; }
.class-card .stats-preview { margin-top: 12px; font-size: 0.85em; color: #aaa; }
/* ===== COMBAT SCREEN ===== */
#combat-screen { justify-content: flex-start; padding: 0; background: radial-gradient(ellipse at 50% 40%, #1a1a2e, #0a0a0f); }
.top-bar {
width: 100%; height: 56px; display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; background: linear-gradient(180deg, #1a1a2e, transparent);
border-bottom: 1px solid rgba(201,168,76,0.15);
z-index: 10; flex-shrink: 0;
}
.top-bar .stat { display: flex; align-items: center; gap: 8px; font-family: 'Cinzel', serif; font-size: 0.95em; }
.top-bar .gold-icon { color: var(--gold); }
.combat-area {
flex: 1; width: 100%; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;
}
/* Enemy */
.enemy-container { text-align: center; z-index: 5; transition: all 0.3s; }
.enemy-sprite { width: 160px; height: 180px; position: relative; margin-bottom: 12px; }
.enemy-hp-bar-container { width: 180px; margin: 0 auto; }
.hp-bar-bg { background: var(--hp-bg); border-radius: 4px; height: 16px; overflow: hidden; position: relative; border: 1px solid #553333; }
.hp-bar-fill { height: 100%; background: linear-gradient(90deg, #cc2222, #ee4444); transition: width 0.4s ease; border-radius: 3px; }
.hp-text { position: absolute; top: 0; left: 50%; transform: translateX(-50%); font-size: 10px; color: #fff; line-height: 16px; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
.enemy-name { margin-top: 6px; font-family: 'Cinzel', serif; color: var(--gold-dim); font-size: 0.9em; }
.enemy-statuses { display: flex; gap: 4px; justify-content: center; margin-top: 4px; min-height: 20px; }
.status-badge { font-size: 10px; padding: 2px 6px; border-radius: 3px; color: #fff; }
.status-burn { background: var(--burn-orange); }
.status-poison { background: var(--poison-green); }
.status-stun { background: #555; }
/* Player info */
.player-info-bar {
width: 100%; padding: 8px 24px; display: flex; align-items: center; justify-content: space-between;
background: linear-gradient(0deg, rgba(10,10,15,0.9), transparent);
z-index: 10; flex-shrink: 0;
}
.player-hp-section { display: flex; align-items: center; gap: 12px; }
.player-hp-bar-container { width: 200px; }
.energy-display { display: flex; gap: 4px; align-items: center; }
.energy-pip { width: 18px; height: 18px; border-radius: 50%; background: #3a6fd4; box-shadow: 0 0 6px rgba(58,111,212,0.5); transition: all 0.2s; }
.energy-pip.empty { background: #1a1a2e; box-shadow: none; border: 1px solid #333; }
/* Hand */
.hand-area {
width: 100%; padding: 10px 20px 16px; display: flex; justify-content: center; gap: 8px;
min-height: 200px; align-items: flex-end; z-index: 10; flex-shrink: 0;
perspective: 800px;
}
/* Cards */
.card {
width: 130px; height: 185px; position: relative; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; flex-shrink: 0;
border-radius: 6px; overflow: hidden;
}
.card:hover { transform: translateY(-18px) scale(1.05); z-index: 20; }
.card.unplayable { opacity: 0.45; cursor: not-allowed; filter: grayscale(0.3); }
.card.unplayable:hover { transform: none; }
.card-inner {
width: 100%; height: 100%; position: relative;
border-radius: 6px; overflow: hidden;
}
.card-cost {
position: absolute; top: 4px; left: 4px; width: 28px; height: 28px;
background: radial-gradient(circle, #3a6fd4, #1a3a7d);
border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-family: 'Cinzel', serif; font-weight: 900; font-size: 14px; color: #fff;
z-index: 3; box-shadow: 0 2px 6px rgba(0,0,0,0.5);
}
.card-image {
width: 100%; height: 70px; margin-top: 34px; position: relative; overflow: hidden;
}
.card-body {
padding: 6px 8px; font-size: 11px; line-height: 1.3; color: #ccc;
height: calc(100% - 70px - 34px); display: flex; flex-direction: column; justify-content: space-between;
}
.card-name { font-family: 'Cinzel', serif; font-size: 12px; color: #fff; text-align: center; margin-bottom: 2px; }
.card-type-label { font-size: 9px; text-transform: uppercase; letter-spacing: 1px; text-align: center; opacity: 0.7; }
.card-desc { font-size: 10px; color: #aaa; line-height: 1.3; flex: 1; }
/* Card type glows */
.card[data-type="Attack"] .card-inner { border: 2px solid var(--red-attack); box-shadow: inset 0 0 20px rgba(212,74,42,0.15), 0 2px 8px rgba(0,0,0,0.5); }
.card[data-type="Defense"] .card-inner { border: 2px solid var(--blue-defense); box-shadow: inset 0 0 20px rgba(58,111,212,0.15), 0 2px 8px rgba(0,0,0,0.5); }
.card[data-type="Other"] .card-inner { border: 2px solid var(--purple-other); box-shadow: inset 0 0 20px rgba(139,58,212,0.15), 0 2px 8px rgba(0,0,0,0.5); }
/* Controls */
.controls-bar {
width: 100%; padding: 6px 24px; display: flex; justify-content: center; gap: 16px;
background: linear-gradient(0deg, #0a0a0f, transparent); z-index: 10; flex-shrink: 0;
}
/* ===== SHOP SCREEN ===== */
#shop-screen { background: radial-gradient(ellipse at center, #1e1e30, #0a0a0f); padding-top: 40px; }
.shop-title { font-size: 2em; color: var(--gold); margin-bottom: 8px; }
.shop-gold { color: var(--text-dim); margin-bottom: 30px; font-size: 1.1em; }
.shop-cards { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; max-width: 800px; margin-bottom: 30px; }
.shop-card { cursor: pointer; transition: all 0.2s; position: relative; }
.shop-card:hover { transform: translateY(-5px); }
.shop-card .card-cost-overlay {
position: absolute; bottom: -8px; left: 50%; transform: translateX(-50%);
background: var(--gold-dim); color: #fff; padding: 2px 10px; border-radius: 4px;
font-size: 11px; font-family: 'Cinzel', serif; white-space: nowrap; z-index: 5;
}
/* ===== GAME OVER / VICTORY ===== */
#gameover-screen, #victory-screen { background: radial-gradient(ellipse at center, #1a0a0a, #0a0a0f); }
#victory-screen { background: radial-gradient(ellipse at center, #1a1a2e, #0a0a0f); }
.end-title { font-size: 3em; margin-bottom: 16px; text-shadow: 0 0 30px currentColor; }
/* ===== DAMAGE NUMBERS ===== */
.damage-number {
position: absolute; pointer-events: none; z-index: 100;
font-family: 'Cinzel', serif; font-weight: 900; font-size: 28px;
animation: floatUp 1s ease-out forwards;
}
@keyframes floatUp {
0% { opacity: 1; transform: translateY(0) scale(1.2); }
100% { opacity: 0; transform: translateY(-60px) scale(0.8); }
}
/* ===== ROOM TRANSITION ===== */
.room-transition {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: #0a0a0f; z-index: 200; display: flex; align-items: center; justify-content: center;
animation: fadeInOut 1.5s ease-in-out forwards; pointer-events: none;
}
@keyframes fadeInOut {
0% { opacity: 0; } 30% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; }
}
/* ===== DEFENSE INDICATOR ===== */
.defense-indicator {
display: flex; align-items: center; gap: 4px; font-family: 'Cinzel', serif; font-size: 0.9em; color: var(--blue-defense);
}
/* ===== ENEMY SVG STYLES ===== */
.enemy-svg-container svg { width: 100%; height: 100%; }
/* ===== PARTICLES ===== */
.particle-bg {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; overflow: hidden;
}
.particle {
position: absolute; width: 2px; height: 2px; background: rgba(201,168,76,0.3); border-radius: 50%;
animation: drift linear infinite;
}
@keyframes drift {
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-20px) rotate(360deg); opacity: 0; }
}
/* ===== TURN INDICATOR ===== */
.turn-indicator {
position: absolute; top: 70px; left: 50%; transform: translateX(-50%);
font-family: 'Cinzel', serif; color: var(--gold-dim); font-size: 0.85em; letter-spacing: 2px;
opacity: 0.6; z-index: 10;
}
/* ===== DECK INFO ===== */
.deck-info { display: flex; gap: 16px; align-items: center; font-size: 0.85em; color: var(--text-dim); }
</style>
</head>
<body>
<!-- SVG DEFINITIONS (hidden) -->
<svg style="display:none">
<!-- BASTION CARD FRAME -->
<defs id="bastion-frame-defs">
<linearGradient id="bastion-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2a2530"/>
<stop offset="100%" style="stop-color:#1a1820"/>
</linearGradient>
</defs>
<!-- WITHER CARD FRAME -->
<defs id="wither-frame-defs">
<linearGradient id="wither-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2a1a18"/>
<stop offset="100%" style="stop-color:#1a1010"/>
</linearGradient>
</defs>
<!-- SHROUD CARD FRAME -->
<defs id="shroud-frame-defs">
<linearGradient id="shroud-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1830"/>
<stop offset="100%" style="stop-color:#0e0c1a"/>
</linearGradient>
</defs>
</svg>
<!-- TITLE SCREEN -->
<div id="title-screen" class="screen active">
<div class="particle-bg" id="particles"></div>
<h1>Shadows of Aethelgard</h1>
<p class="subtitle">The Void consumes all. Only the brave descend.</p>
<button class="btn" onclick="showClassSelect()">Begin Descent</button>
</div>
<!-- CLASS SELECT -->
<div id="class-select" class="screen">
<h2 style="color:var(--gold); font-size:1.8em; margin-bottom:6px;">Choose Your Champion</h2>
<p style="color:var(--text-dim); margin-bottom:10px;">Each hero carries a unique deck into the darkness.</p>
<div class="class-cards">
<div class="class-card" onclick="selectClass('bastion')">
<h3>The Bastion</h3>
<p>Immovable object. High defense and block, low speed.</p>
<div class="stats-preview">HP: 35 | Energy: 3</div>
</div>
<div class="class-card" onclick="selectClass('wither')">
<h3>The Wither</h3>
<p>Rot and burn. Status effects, debuffs, AoE damage.</p>
<div class="stats-preview">HP: 28 | Energy: 4</div>
</div>
<div class="class-card" onclick="selectClass('shroud')">
<h3>The Shroud</h3>
<p>Shadow and speed. Evasion, assassination, high crit.</p>
<div class="stats-preview">HP: 28 | Energy: 4</div>
</div>
</div>
</div>
<!-- COMBAT SCREEN -->
<div id="combat-screen" class="screen">
<div class="top-bar">
<div class="stat"><span class="gold-icon">◆</span> <span id="ui-gold">0</span></div>
<div class="stat title-font" style="color:var(--gold)">Floor <span id="ui-floor">1</span> — Room <span id="ui-room">1</span></div>
<div class="deck-info">Deck: <span id="ui-deck-count">0</span> | Hand: <span id="ui-hand-size">4</span></div>
</div>
<div class="turn-indicator" id="turn-indicator">YOUR TURN</div>
<div class="combat-area" id="combat-area">
<div class="enemy-container" id="enemy-container">
<div class="enemy-sprite" id="enemy-sprite"></div>
<div class="enemy-hp-bar-container">
<div class="hp-bar-bg"><div class="hp-bar-fill" id="enemy-hp-fill" style="width:100%"></div><span class="hp-text" id="enemy-hp-text">20/20</span></div>
</div>
<div class="enemy-name" id="enemy-name">Goblin Scout</div>
<div class="enemy-statuses" id="enemy-statuses"></div>
</div>
</div>
<div class="player-info-bar">
<div class="player-hp-section">
<span style="font-family:'Cinzel',serif; font-size:0.9em;">HP</span>
<div class="player-hp-bar-container">
<div class="hp-bar-bg"><div class="hp-bar-fill" id="player-hp-fill" style="width:100%"></div><span class="hp-text" id="player-hp-text">35/35</span></div>
</div>
</div>
<div class="defense-indicator">🛡️ <span id="ui-defense">0</span></div>
<div class="energy-display" id="energy-display"></div>
</div>
<div class="hand-area" id="hand-area"></div>
<div class="controls-bar">
<button class="btn" id="end-turn-btn" onclick="endTurn()">End Turn</button>
</div>
</div>
<!-- SHOP SCREEN -->
<div id="shop-screen" class="screen">
<h2 class="shop-title title-font">Merchant's Tent</h2>
<p class="shop-gold">Gold: <span id="shop-gold-display" style="color:var(--gold)">0</span></p>
<div class="shop-cards" id="shop-cards"></div>
<button class="btn" onclick="leaveShop()">Continue Deeper</button>
</div>
<!-- GAME OVER -->
<div id="gameover-screen" class="screen">
<h1 class="end-title title-font" style="color:var(--hp-red)">Defeated</h1>
<p style="color:var(--text-dim); margin-bottom:30px;">The Void claims another soul...</p>
<button class="btn" onclick="location.reload()">Try Again</button>
</div>
<!-- VICTORY -->
<div id="victory-screen" class="screen">
<h1 class="end-title title-font" style="color:var(--gold)">Victory!</h1>
<p style="color:var(--text-dim); margin-bottom:30px;">Aethelgard is freed from the Void.</p>
<button class="btn" onclick="location.reload()">Play Again</button>
</div>
<script>
// ============================================================
// GAME DATA
// ============================================================
const CARD_DATA = {
// 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.', aoe:true, class:'bastion' },
'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.', class:'bastion' },
'BA-D01': { id:'BA-D01', name:'Shield Wall', type:'Defense', cost:0, atk:0, def:4, desc:'Gain 4 Defense this turn.', class:'bastion' },
'BA-D02': { id:'BA-D02', name:'Iron Skin', type:'Defense', cost:2, atk:0, def:5, desc:'Gain 5 permanent Defense. Discard 1 card.', permDef:true, discardAfter:true, class:'bastion' },
'BA-D03': { id:'BA-D03', name:'Stand Firm', type:'Defense', cost:1, atk:0, def:2, desc:'Gain 2 Defense. Blocks 1 incoming damage.', blockTrigger:true, 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.', stunEnemy:true, class:'bastion' },
'BA-U01': { id:'BA-U01', name:'Search', type:'Other', cost:2, atk:0, def:0, desc:'Draw 1 card. Discard hand if >5 cards.', drawCard:true, discardIfOver5:true, class:'bastion' },
'BA-U02': { id:'BA-U02', name:'Repair', type:'Other', cost:3, atk:0, def:0, desc:'Heal 6 HP.', heal:6, class:'bastion' },
'BA-U03': { id:'BA-U03', name:'Rally', type:'Other', cost:1, atk:0, def:0, desc:'Draw 1 card. Gain 1 Energy.', drawCard:true, gainEnergy:1, class:'bastion' },
'BA-U04': { id:'BA-U04', name:'Loot', type:'Other', cost:2, atk:0, def:0, desc:'Gain 5 Gold.', gold:5, class:'bastion' },
// WITHER CARDS
'WI-A01': { id:'WI-A01', name:'Fireball', type:'Attack', cost:2, atk:3, def:0, desc:'Deals 3 damage. Apply 1 Burn.', burn:1, class:'wither' },
'WI-A02': { id:'WI-A02', name:'Inferno', type:'Attack', cost:3, atk:4, def:0, desc:'Deals 4 damage. Applies 3 Burn.', burn:3, aoe:true, class:'wither' },
'WI-A03': { id:'WI-A03', name:'Ember', type:'Attack', cost:1, atk:2, def:0, desc:'Deals 2 damage. Deals 1 dmg/turn to target.', burn:1, class:'wither' },
'WI-A04': { id:'WI-A04', name:'Pyre', type:'Attack', cost:4, atk:5, def:0, desc:'Deals 5 damage. All enemies take 1 dmg next turn.', aoe:true, class:'wither' },
'WI-D01': { id:'WI-D01', name:'Scorch Shield', type:'Defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Block 1 Burn damage.', blockBurn:true, 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.', gainEnergy:1, 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.', blockBurn:true, class:'wither' },
'WI-D04': { id:'WI-D04', name:'Endure', type:'Defense', cost:3, atk:0, def:2, desc:'Gain 2 Defense. Heal 2 HP.', heal:2, class:'wither' },
'WI-U01': { id:'WI-U01', name:'Poison', type:'Other', cost:2, atk:0, def:0, desc:'Apply 1 Poison to target enemy.', poison:1, class:'wither' },
'WI-U02': { id:'WI-U02', name:'Burn', type:'Other', cost:3, atk:0, def:0, desc:'Deal 3 damage + 2 Burn to all enemies.', aoe:true, burn:2, otherDmg:3, class:'wither' },
'WI-U03': { id:'WI-U03', name:'Slow', type:'Other', cost:1, atk:0, def:0, desc:'Draw 1 card. Stun target enemy.', drawCard:true, stunEnemy:true, class:'wither' },
'WI-U04': { id:'WI-U04', name:'Wither', type:'Other', cost:2, atk:0, def:0, desc:'Heal 5 HP. Reduce enemy Attack by 1.', heal:5, debuffAtk:true, class:'wither' },
// SHROUD CARDS
'SH-A01': { id:'SH-A01', name:'Backstab', type:'Attack', cost:2, atk:4, def:0, desc:'Deals 4 damage. +1 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.', unblockable:true, class:'shroud' },
'SH-A03': { id:'SH-A03', name:'Silence', type:'Attack', cost:1, atk:3, def:0, desc:'Deals 3 damage. Target silenced.', silenceEnemy:true, class:'shroud' },
'SH-A04': { id:'SH-A04', name:'Vengeance', type:'Attack', cost:3, atk:6, def:0, desc:'Deals 6 damage. Gain 2 Energy.', gainEnergy:2, class:'shroud' },
'SH-D01': { id:'SH-D01', name:'Blur', type:'Defense', cost:3, atk:0, def:2, desc:'Gain 2 Defense. Ignore next attack.', dodgeNext:true, class:'shroud' },
'SH-D02': { id:'SH-D02', name:'Step', type:'Defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Gain 1 Energy.', gainEnergy:1, class:'shroud' },
'SH-D03': { id:'SH-D03', name:'Evasion', type:'Defense', cost:1, atk:0, def:1, desc:'Gain 1 Defense. Skip enemy action.', skipEnemy:true, class:'shroud' },
'SH-D04': { id:'SH-D04', name:'Phantom', type:'Defense', cost:2, atk:0, def:4, desc:'Gain 4 Defense. Ghost form for 1 turn.', dodgeNext:true, class:'shroud' },
'SH-U01': { id:'SH-U01', name:'Dash', type:'Other', cost:1, atk:0, def:0, desc:'Gain 1 Energy. Skip enemy attack.', gainEnergy:1, skipEnemy:true, class:'shroud' },
'SH-U02': { id:'SH-U02', name:'Teleport', type:'Other', cost:2, atk:0, def:0, desc:'Deal 2 damage to all enemies.', aoe:true, otherDmg:2, class:'shroud' },
'SH-U03': { id:'SH-U03', name:'Hide', type:'Other', cost:2, atk:0, def:0, desc:'Gain 1 Defense. Discard 1 card. Draw 2 cards.', drawCard:true, drawCount:2, discardAfter:true, class:'shroud' },
'SH-U04': { id:'SH-U04', name:'Assassinate', type:'Attack', cost:3, atk:8, def:0, desc:'Deals 8 damage. Cannot be blocked.', unblockable:true, class:'shroud' }
};
const CLASS_CONFIG = {
bastion: { hp:35, energy:3, handSize:4, name:'The Bastion', starterDeck:['BA-A01','BA-A02','BA-A03','BA-D01','BA-D02','BA-U01'] },
wither: { hp:28, energy:4, handSize:4, name:'The Wither', starterDeck:['WI-A01','WI-A02','WI-A03','WI-D01','WI-D03','WI-U01'] },
shroud: { hp:28, energy:4, handSize:4, name:'The Shroud', starterDeck:['SH-A01','SH-A02','SH-A03','SH-D01','SH-D02','SH-U01'] }
};
const ENEMY_TYPES = {
floor1: [
{ name:'Goblin Scout', hp:20, dmg:3 },
{ name:'Rat Swarm', hp:18, dmg:2 },
{ name:'Goblin Brute', hp:25, dmg:4 }
],
floor2: [
{ name:'Vampire Knight', hp:30, dmg:5 },
{ name:'Ogre', hp:35, dmg:6 },
{ name:'Dark Archer', hp:28, dmg:5 }
],
floor3: [
{ name:'Void Warrior', hp:40, dmg:8 },
{ name:'Cultist', hp:38, dmg:7 },
{ name:'Abyssal Guard', hp:45, dmg:9 }
]
};
const BOSS_TYPES = [
{ name:'The Gatekeeper', hp:80, dmg:12, blockPerTurn:5, desc:'Blocks 5 damage/turn until HP<20' },
{ name:'The Corrupted', hp:80, dmg:10, burstDmg:20, desc:'Deals 20 damage if you have <3 Energy' },
{ name:'The Void Lord', hp:40, dmg:15, regenPerTurn:10, desc:'Regenerates 10 HP/turn. Cannot block.' }
];
// ============================================================
// GAME STATE
// ============================================================
let G = {}; // game state
function initGame(classId) {
const cfg = CLASS_CONFIG[classId];
G = {
class: classId,
hp: cfg.hp,
maxHp: cfg.hp,
energy: cfg.energy,
maxEnergy: cfg.energy,
handSize: cfg.handSize,
gold: 0,
defense: 0,
permDefense: 0,
floor: 1,
room: 1,
deck: [...cfg.starterDeck],
hand: [],
discardPile: [],
enemy: null,
isBoss: false,
turnNumber: 1,
dodgeNextAttack: false,
skipEnemyAction: false,
blockBurnDamage: false,
cardCounts: {},
combatActive: true
};
// Count cards in deck
G.deck.forEach(id => { G.cardCounts[id] = (G.cardCounts[id]||0) + 1; });
shuffleDeck();
}
function shuffleDeck() {
for (let i = G.discardPile.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[G.discardPile[i], G.discardPile[j]] = [G.discardPile[j], G.discardPile[i]];
}
// If deck is empty, reshuffle discard
if (G.deck.length === 0 && G.discardPile.length > 0) {
G.deck = [...G.discardPile];
G.discardPile = [];
shuffleArray(G.deck);
}
}
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]];
}
}
// ============================================================
// SVG CARD GENERATION
// ============================================================
function getCardSVG(cardId, faceUp) {
const card = CARD_DATA[cardId];
if (!faceUp) return getCardBackSVG(G.class);
let frameColor, accentColor, innerGlow;
switch (G.class) {
case 'bastion':
frameColor = '#5a4a3a'; accentColor = '#8a7060'; innerGlow = 'rgba(138,112,96,0.15)'; break;
case 'wither':
frameColor = '#5a2a2a'; accentColor = '#8a4040'; innerGlow = 'rgba(138,64,64,0.15)'; break;
case 'shroud':
frameColor = '#2a2a5a'; accentColor = '#40408a'; innerGlow = 'rgba(64,64,138,0.15)'; break;
}
let typeColor;
switch (card.type) {
case 'Attack': typeColor = '#d44a2a'; break;
case 'Defense': typeColor = '#3a6fd4'; break;
case 'Other': typeColor = '#8b3ad4'; break;
}
// Unique frame decorations per class
let frameDecorations = '';
if (G.class === 'bastion') {
// Shield/armor pattern
frameDecorations = `
<path d="M10,25 L60,15 L110,25" stroke="${accentColor}" fill="none" stroke-width="1.5"/>
<path d="M10,160 L60,170 L110,160" stroke="${accentColor}" fill="none" stroke-width="1.5"/>
<rect x="8" y="28" width="4" height="130" rx="1" fill="${frameColor}" opacity="0.5"/>
<rect x="118" y="28" width="4" height="130" rx="1" fill="${frameColor}" opacity="0.5"/>
`;
} else if (G.class === 'wither') {
// Flame/rot pattern
frameDecorations = `
<path d="M20,28 Q60,45 100,28" stroke="${accentColor}" fill="none" stroke-width="1.5"/>
<path d="M20,157 Q60,140 100,157" stroke="${accentColor}" fill="none" stroke-width="1.5"/>
<circle cx="30" cy="35" r="2" fill="${frameColor}" opacity="0.4"/>
<circle cx="90" cy="35" r="2" fill="${frameColor}" opacity="0.4"/>
<circle cx="30" cy="152" r="2" fill="${frameColor}" opacity="0.4"/>
<circle cx="90" cy="152" r="2" fill="${frameColor}" opacity="0.4"/>
`;
} else {
// Shadow/mist pattern
frameDecorations = `
<path d="M15,30 Q60,20 105,30" stroke="${accentColor}" fill="none" stroke-width="1.5"/>
<path d="M15,155 Q60,165 105,155" stroke="${accentColor}" fill="none" stroke-width="1.5"/>
<line x1="25" y1="38" x2="25" y2="147" stroke="${frameColor}" opacity="0.3" stroke-width="1"/>
<line x1="95" y1="38" x2="95" y2="147" stroke="${frameColor}" opacity="0.3" stroke-width="1"/>
`;
}
// Placeholder image area decoration per class
let placeholderDecor = '';
if (G.class === 'bastion') {
placeholderDecor = `<rect x="25" y="48" width="80" height="44" rx="3" fill="#1a1820" stroke="${frameColor}" stroke-width="1"/>
<text x="65" y="72" text-anchor="middle" font-size="10" fill="${accentColor}" opacity="0.5">⚔</text>`;
} else if (G.class === 'wither') {
placeholderDecor = `<rect x="25" y="48" width="80" height="44" rx="3" fill="#1a1010" stroke="${frameColor}" stroke-width="1"/>
<text x="65" y="72" text-anchor="middle" font-size="10" fill="${accentColor}" opacity="0.5">🔥</text>`;
} else {
placeholderDecor = `<rect x="25" y="48" width="80" height="44" rx="3" fill="#0e0c1a" stroke="${frameColor}" stroke-width="1"/>
<text x="65" y="72" text-anchor="middle" font-size="10" fill="${accentColor}" opacity="0.5">👁</text>`;
}
return `<svg viewBox="0 0 130 185" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="cbg_${cardId}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2a2530"/>
<stop offset="100%" style="stop-color:#141218"/>
</linearGradient>
</defs>
<rect x="2" y="2" width="126" height="181" rx="6" fill="url(#cbg_${cardId})" stroke="${frameColor}" stroke-width="2"/>
${frameDecorations}
<!-- Cost circle -->
<circle cx="20" cy="20" r="13" fill="#1a3a7d" stroke="#4a8aff" stroke-width="1.5"/>
<text x="20" y="24" text-anchor="middle" font-family="'Cinzel',serif" font-weight="900" font-size="14" fill="#fff">${card.cost}</text>
<!-- Image placeholder -->
${placeholderDecor}
<!-- Card name -->
<text x="65" y="112" text-anchor="middle" font-family="'Cinzel',serif" font-weight="700" font-size="11" fill="#fff">${card.name}</text>
<!-- Type label -->
<text x="65" y="124" text-anchor="middle" font-size="8" fill="${typeColor}" letter-spacing="1.5">${card.type.toUpperCase()}</text>
<!-- Description -->
<text x="65" y="138" text-anchor="middle" font-size="7.5" fill="#aaa" font-family="'Crimson Text',serif">
${card.desc.length > 40 ? card.desc.substring(0,40)+'…' : card.desc}
</text>
<!-- Stats -->
<text x="65" y="172" text-anchor="middle" font-family="'Cinzel',serif" font-size="9" fill="${typeColor}">
${card.atk > 0 ? '⚔'+card.atk : ''}${card.def > 0 ? (card.atk>0?' | ':'')+'🛡'+card.def : ''}
</text>
</svg>`;
}
function getCardBackSVG(classId) {
let colors, pattern;
switch (classId) {
case 'bastion':
colors = { bg:'#1a1820', frame:'#5a4a3a', accent:'#8a7060' };
pattern = `
<rect x="20" y="20" width="90" height="145" rx="4" fill="none" stroke="${colors.accent}" stroke-width="1.5"/>
<path d="M65,35 L80,70 L65,60 L50,70 Z" fill="${colors.frame}" opacity="0.6"/>
<path d="M65,150 L80,115 L65,125 L50,115 Z" fill="${colors.frame}" opacity="0.6"/>
<circle cx="65" cy="92" r="18" fill="none" stroke="${colors.accent}" stroke-width="2"/>
<text x="65" y="97" text-anchor="middle" font-size="14" fill="${colors.accent}">⛊</text>
`;
break;
case 'wither':
colors = { bg:'#1a1010', frame:'#5a2a2a', accent:'#8a4040' };
pattern = `
<rect x="20" y="20" width="90" height="145" rx="4" fill="none" stroke="${colors.accent}" stroke-width="1.5"/>
<path d="M65,30 Q85,70 65,92 Q45,70 65,30Z" fill="${colors.frame}" opacity="0.4"/>
<path d="M65,155 Q85,115 65,92 Q45,115 65,155Z" fill="${colors.frame}" opacity="0.4"/>
<circle cx="65" cy="92" r="15" fill="none" stroke="${colors.accent}" stroke-width="2"/>
<text x="65" y="97" text-anchor="middle" font-size="14" fill="${colors.accent}">🔥</text>
`;
break;
case 'shroud':
colors = { bg:'#0e0c1a', frame:'#2a2a5a', accent:'#40408a' };
pattern = `
<rect x="20" y="20" width="90" height="145" rx="4" fill="none" stroke="${colors.accent}" stroke-width="1.5"/>
<path d="M65,35 L78,80 L65,70 L52,80 Z" fill="${colors.frame}" opacity="0.5"/>
<path d="M65,150 L78,105 L65,115 L52,105 Z" fill="${colors.frame}" opacity="0.5"/>
<circle cx="65" cy="92" r="18" fill="none" stroke="${colors.accent}" stroke-width="2"/>
<text x="65" y="97" text-anchor="middle" font-size="14" fill="${colors.accent}">👁</text>
`;
break;
}
return `<svg viewBox="0 0 130 185" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="126" height="181" rx="6" fill="${colors.bg}" stroke="${colors.frame}" stroke-width="2"/>
${pattern}
</svg>`;
}
// ============================================================
// ENEMY SVG GENERATION
// ============================================================
function getEnemySVG(enemy) {
let svg = '';
if (G.isBoss) {
// Boss - larger, more menacing
const bossColors = ['#8b0000','#4a0080','#1a0030'];
const c = bossColors[G.floor - 1] || '#8b0000';
svg = `<svg viewBox="0 0 160 180" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="bossGlow"><stop offset="0%" style="stop-color:${c};stop-opacity:0.4"/>
<stop offset="100%" style="stop-color:#0a0a0f;stop-opacity:0"/></radialGradient>
</defs>
<circle cx="80" cy="90" r="75" fill="url(#bossGlow)"/>
<!-- Body -->
<path d="M50,160 L60,100 L45,80 L55,60 L80,35 L105,60 L115,80 L100,100 L110,160 Z" fill="#2a1a1a" stroke="${c}" stroke-width="2"/>
<!-- Eyes -->
<ellipse cx="70" cy="70" rx="6" ry="4" fill="${c}">
<animate attributeName="opacity" values="1;0.3;1" dur="2s" repeatCount="indefinite"/>
</ellipse>
<ellipse cx="90" cy="70" rx="6" ry="4" fill="${c}">
<animate attributeName="opacity" values="1;0.3;1" dur="2s" repeatCount="indefinite"/>
</ellipse>
<!-- Crown/Horns -->
<path d="M55,60 L48,30 L65,50 M105,60 L112,30 L95,50" stroke="${c}" fill="none" stroke-width="2"/>
<!-- Aura particles -->
<circle cx="40" cy="50" r="2" fill="${c}" opacity="0.5">
<animate attributeName="cy" values="50;30;50" dur="3s" repeatCount="indefinite"/>
</circle>
<circle cx="120" cy="60" r="2" fill="${c}" opacity="0.5">
<animate attributeName="cy" values="60;40;60" dur="3.5s" repeatCount="indefinite"/>
</circle>
</svg>`;
} else {
// Regular enemy
const floorColors = ['#4a7a2a','#8b3a3a','#5a1a6a'];
const c = floorColors[G.floor - 1] || '#4a7a2a';
svg = `<svg viewBox="0 0 160 180" xmlns="http://www.w3.org/2000/svg">
<!-- Body -->
<path d="M55,155 L62,105 L50,90 L58,72 L80,50 L102,72 L110,90 L98,105 L105,155 Z" fill="#1a1a1a" stroke="${c}" stroke-width="1.5"/>
<!-- Eyes -->
<ellipse cx="73" cy="68" rx="4" ry="3" fill="${c}"/>
<ellipse cx="87" cy="68" rx="4" ry="3" fill="${c}"/>
<!-- Weapon hint -->
<line x1="105" y1="90" x2="125" y2="60" stroke="${c}" stroke-width="2" opacity="0.7"/>
</svg>`;
}
return svg;
}
// ============================================================
// SCREEN MANAGEMENT
// ============================================================
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
function showClassSelect() { showScreen('class-select'); }
function selectClass(classId) {
initGame(classId);
startCombat();
}
// ============================================================
// COMBAT SYSTEM
// ============================================================
function startCombat() {
G.combatActive = true;
G.turnNumber = 1;
G.defense = G.permDefense; // permanent defense carries over
G.dodgeNextAttack = false;
G.skipEnemyAction = false;
G.blockBurnDamage = false;
// Generate enemy
if (G.room > 9) {
// Boss fight
G.isBoss = true;
const bossIdx = Math.min(G.floor - 1, BOSS_TYPES.length - 1);
const boss = BOSS_TYPES[bossIdx];
G.enemy = {
name: boss.name, hp: boss.hp, maxHp: boss.hp, dmg: boss.dmg,
burn: 0, poison: 0, stunned: false, silenced: false,
blockPerTurn: boss.blockPerTurn || 0, burstDmg: boss.burstDmg || 0, regenPerTurn: boss.regenPerTurn || 0,
currentBlock: 0
};
} else {
G.isBoss = false;
const floorKey = 'floor' + G.floor;
const types = ENEMY_TYPES[floorKey];
const type = types[Math.floor(Math.random() * types.length)];
G.enemy = {
name: type.name, hp: type.hp, maxHp: type.hp, dmg: type.dmg,
burn: 0, poison: 0, stunned: false, silenced: false,
blockPerTurn: 0, burstDmg: 0, regenPerTurn: 0, currentBlock: 0
};
}
showScreen('combat-screen');
drawCardsToHand();
updateUI();
}
function drawCardsToHand() {
while (G.hand.length < G.handSize && G.deck.length > 0) {
const cardId = G.deck.pop();
G.hand.push(cardId);
}
if (G.deck.length === 0 && G.discardPile.length > 0) {
shuffleArray(G.discardPile);
G.deck = [...G.discardPile];
G.discardPile = [];
}
}
function playCard(handIndex) {
if (!G.combatActive) return;
const cardId = G.hand[handIndex];
const card = CARD_DATA[cardId];
if (G.energy < card.cost) return;
// Pay cost
G.energy -= card.cost;
// Resolve card effects
resolveCard(card, handIndex);
// Remove from hand and add to discard
G.hand.splice(handIndex, 1);
G.discardPile.push(cardId);
updateUI();
}
function resolveCard(card, handIndex) {
const enemy = G.enemy;
if (card.type === 'Attack') {
let dmg = card.atk;
// Bastion Slam bonus
if (card.id === 'BA-A01' && G.hp >= 30) {
drawCardsToHand();
}
// Shroud Backstab bonus
if (card.id === 'SH-A01') {
dmg += 1; // simplified: always +1
}
// Unblockable attacks ignore enemy block
if (!card.unblockable && enemy.currentBlock > 0) {
const blocked = Math.min(dmg, enemy.currentBlock);
enemy.currentBlock -= blocked;
dmg -= blocked;
}
if (dmg > 0) {
dealDamageToEnemy(dmg);
showDamageNumber(dmg, 'enemy');
}
// AoE attacks hit all enemies (simplified: just the one enemy in this prototype)
if (card.aoe && card.id === 'BA-A02') {
// Already handled above for single enemy
}
// Burn effects
if (card.burn) {
enemy.burn += card.burn;
}
// Stun effect
if (card.stunEnemy) {
enemy.stunned = true;
}
// Silence effect
if (card.silenceEnemy) {
enemy.silenced = true;
}
// Energy gain
if (card.gainEnergy) {
G.energy = Math.min(G.maxEnergy, G.energy + card.gainEnergy);
}
} else if (card.type === 'Defense') {
G.defense += card.def;
if (card.permDef) {
G.permDefense += card.def;
}
// Heal
if (card.heal) {
G.hp = Math.min(G.maxHp, G.hp + card.heal);
}
// Stun enemy
if (card.stunEnemy) {
enemy.stunned = true;
}
// Skip enemy action
if (card.skipEnemy) {
G.skipEnemyAction = true;
}
// Dodge next attack
if (card.dodgeNext) {
G.dodgeNextAttack = true;
}
// Block burn damage
if (card.blockBurn) {
G.blockBurnDamage = true;
}
// Energy gain
if (card.gainEnergy) {
G.energy = Math.min(G.maxEnergy, G.energy + card.gainEnergy);
}
// Discard after effect
if (card.discardAfter && G.hand.length > 0) {
const idx = Math.floor(Math.random() * G.hand.length);
G.discardPile.push(G.hand[idx]);
G.hand.splice(idx, 1);
}
} else if (card.type === 'Other') {
// Damage effects
if (card.otherDmg) {
dealDamageToEnemy(card.otherDmg);
showDamageNumber(card.otherDmg, 'enemy');
}
// Burn on other cards
if (card.burn && !card.atk) {
G.enemy.burn += card.burn;
}
// Poison
if (card.poison) {
G.enemy.poison += card.poison;
}
// Heal
if (card.heal) {
G.hp = Math.min(G.maxHp, G.hp + card.heal);
}
// Draw cards
if (card.drawCard) {
const count = card.drawCount || 1;
for (let i = 0; i < count; i++) drawCardsToHand();
if (card.discardIfOver5 && G.hand.length > 5) {
while (G.hand.length > 5) {
G.discardPile.push(G.hand.pop());
}
}
}
// Gold
if (card.gold) {
G.gold += card.gold;
}
// Energy gain
if (card.gainEnergy) {
G.energy = Math.min(G.maxEnergy, G.energy + card.gainEnergy);
}
// Stun enemy
if (card.stunEnemy) {
G.enemy.stunned = true;
}
// Skip enemy action
if (card.skipEnemy) {
G.skipEnemyAction = true;
}
// Debuff attack
if (card.debuffAtk) {
G.enemy.dmg = Math.max(1, G.enemy.dmg - 1);
}
// Discard after effect
if (card.discardAfter && G.hand.length > 0) {
const idx = Math.floor(Math.random() * G.hand.length);
G.discardPile.push(G.hand[idx]);
G.hand.splice(idx, 1);
}
}
// Check enemy death
if (G.enemy.hp <= 0) {
enemyDefeated();
}
}
function dealDamageToEnemy(dmg) {
G.enemy.hp = Math.max(0, G.enemy.hp - dmg);
}
function endTurn() {
if (!G.combatActive) return;
// Discard hand to discard pile
while (G.hand.length > 0) {
G.discardPile.push(G.hand.pop());
}
// Apply burn/poison to enemy at start of their turn
applyStatusToEnemy();
// Enemy action
if (!G.skipEnemyAction && !G.enemy.stunned) {
enemyAttack();
} else {
G.skipEnemyAction = false;
}
// Boss regen
if (G.isBoss && G.enemy.regenPerTurn > 0 && G.enemy.hp > 0) {
G.enemy.hp = Math.min(G.enemy.maxHp, G.enemy.hp + G.enemy.regenPerTurn);
}
// Boss block
if (G.isBoss && G.enemy.blockPerTurn > 0 && G.enemy.hp >= 20 && G.enemy.hp > 0) {
G.enemy.currentBlock += G.enemy.blockPerTurn;
}
// Reset per-turn defense
G.defense = G.permDefense;
G.dodgeNextAttack = false;
G.skipEnemyAction = false;
G.blockBurnDamage = false;
// Enemy stun wears off
if (G.enemy.stunned) {
G.enemy.stunned = false;
}
// Check player death
if (G.hp <= 0) {
G.combatActive = false;
updateUI();
setTimeout(() => showScreen('gameover-screen'), 500);
return;
}
// Next turn - draw cards and recover energy
G.turnNumber++;
G.energy = Math.min(G.maxEnergy, G.energy + 1);
drawCardsToHand();
updateUI();
}
function applyStatusToEnemy() {
const enemy = G.enemy;
if (enemy.burn > 0) {
let burnDmg = enemy.burn;
if (G.blockBurnDamage) burnDmg = Math.max(0, burnDmg - 1);
dealDamageToEnemy(burnDmg);
showDamageNumber(burnDmg, 'enemy', '#e8961e');
enemy.burn = Math.max(0, enemy.burn - 1);
}
if (enemy.poison > 0) {
dealDamageToEnemy(enemy.poison);
showDamageNumber(enemy.poison, 'enemy', '#2ecc71');
enemy.poison = Math.max(0, enemy.poison - 1);
}
}
function enemyAttack() {
const enemy = G.enemy;
let dmg = enemy.dmg;
// Boss 2 burst damage check
if (G.isBoss && enemy.burstDmg > 0 && G.energy < 3) {
dmg = enemy.burstDmg;
}
// Dodge
if (G.dodgeNextAttack) {
showDamageNumber(0, 'player', '#888');
return;
}
// Apply defense
let actualDmg = Math.max(0, dmg - G.defense);
G.defense = Math.max(0, G.defense - dmg);
if (actualDmg > 0) {
G.hp -= actualDmg;
showDamageNumber(actualDmg, 'player', '#cc2222');
}
}
function enemyDefeated() {
G.combatActive = false;
const goldReward = G.isBoss ? 15 : 5;
G.gold += goldReward;
updateUI();
setTimeout(() => {
if (G.isBoss) {
// Boss defeated
if (G.floor >= 3 && G.room > 9) {
showScreen('victory-screen');
} else {
G.floor++;
G.room = 1;
transitionRoom();
}
} else {
G.room++;
if (G.room > 9) {
// Boss room
transitionRoom();
} else if (G.room % 3 === 0) {
// Shop after every 3 rooms
showShop();
} else {
transitionRoom();
}
}
}, 800);
}
function transitionRoom() {
const overlay = document.createElement('div');
overlay.className = 'room-transition';
overlay.innerHTML = `<h2 class="title-font" style="color:var(--gold)">Floor ${G.floor} — Room ${G.room}</h2>`;
document.body.appendChild(overlay);
setTimeout(() => {
overlay.remove();
startCombat();
}, 1500);
}
// ============================================================
// SHOP SYSTEM
// ============================================================
function showShop() {
showScreen('shop-screen');
updateShopUI();
}
function getCardCost(cardId) {
const count = G.cardCounts[cardId] || 0;
if (count >= 3) return 10; // duplication cost
return Math.min(50, 1 + count);
}
function updateShopUI() {
document.getElementById('shop-gold-display').textContent = G.gold;
const container = document.getElementById('shop-cards');
container.innerHTML = '';
// Show random cards from all classes (not just player's class)
const allCards = Object.values(CARD_DATA);
const shuffled = [...allCards].sort(() => Math.random() - 0.5).slice(0, 6);
shuffled.forEach(card => {
const cost = getCardCost(card.id);
const canAfford = G.gold >= cost;
const maxed = (G.cardCounts[card.id] || 0) >= 5;
const div = document.createElement('div');
div.className = 'shop-card';
if (!canAfford || maxed) div.style.opacity = '0.4';
div.innerHTML = getCardSVG(card.id, true);
if (maxed) {
div.innerHTML += `<div class="card-cost-overlay" style="background:#555">MAX</div>`;
} else {
div.innerHTML += `<div class="card-cost-overlay">${cost} Gold</div>`;
}
if (canAfford && !maxed) {
div.onclick = () => buyCard(card.id, cost);
}
container.appendChild(div);
});
}
function buyCard(cardId, cost) {
if (G.gold < cost || (G.cardCounts[cardId]||0) >= 5) return;
G.gold -= cost;
G.deck.push(cardId);
G.cardCounts[cardId] = (G.cardCounts[cardId]||0) + 1;
updateShopUI();
}
function leaveShop() {
transitionRoom();
}
// ============================================================
// UI UPDATE
// ============================================================
function updateUI() {
// Top bar
document.getElementById('ui-gold').textContent = G.gold;
document.getElementById('ui-floor').textContent = G.floor;
document.getElementById('ui-room').textContent = G.room > 9 ? 'BOSS' : G.room;
document.getElementById('ui-deck-count').textContent = G.deck.length + G.discardPile.length;
document.getElementById('ui-hand-size').textContent = G.handSize;
// Player HP
const hpPct = Math.max(0, (G.hp / G.maxHp) * 100);
document.getElementById('player-hp-fill').style.width = hpPct + '%';
document.getElementById('player-hp-text').textContent = `${Math.max(0,G.hp)}/${G.maxHp}`;
// Defense
document.getElementById('ui-defense').textContent = G.defense;
// Energy pips
const energyDiv = document.getElementById('energy-display');
energyDiv.innerHTML = '';
for (let i = 0; i < G.maxEnergy; i++) {
const pip = document.createElement('div');
pip.className = 'energy-pip' + (i >= G.energy ? ' empty' : '');
energyDiv.appendChild(pip);
}
// Enemy
if (G.enemy) {
const ehpPct = Math.max(0, (G.enemy.hp / G.enemy.maxHp) * 100);
document.getElementById('enemy-hp-fill').style.width = ehpPct + '%';
document.getElementById('enemy-hp-text').textContent = `${Math.max(0,G.enemy.hp)}/${G.enemy.maxHp}`;
document.getElementById('enemy-name').textContent = G.enemy.name;
// Enemy sprite
document.getElementById('enemy-sprite').innerHTML = getEnemySVG(G.enemy);
// Statuses
const statusDiv = document.getElementById('enemy-statuses');
statusDiv.innerHTML = '';
if (G.enemy.burn > 0) {
statusDiv.innerHTML += `<span class="status-badge status-burn">🔥${G.enemy.burn}</span>`;
}
if (G.enemy.poison > 0) {
statusDiv.innerHTML += `<span class="status-badge status-poison">☠${G.enemy.poison}</span>`;
}
if (G.enemy.stunned) {
statusDiv.innerHTML += `<span class="status-badge status-stun">💫Stunned</span>`;
}
}
// Hand
renderHand();
// Turn indicator
document.getElementById('turn-indicator').textContent = G.combatActive ? 'YOUR TURN' : (G.enemy && G.enemy.hp <= 0 ? 'ENEMY DEFEATED!' : '');
}
function renderHand() {
const handArea = document.getElementById('hand-area');
handArea.innerHTML = '';
G.hand.forEach((cardId, idx) => {
const card = CARD_DATA[cardId];
if (!card) return;
const div = document.createElement('div');
div.className = 'card';
div.dataset.type = card.type;
if (G.energy < card.cost || !G.combatActive) div.classList.add('unplayable');
div.innerHTML = `<div class="card-inner">${getCardSVG(cardId, true)}</div>`;
// Stagger cards slightly
const offset = Math.max(-15, 15 - idx * 3);
div.style.transform = `translateY(${offset}px)`;
if (G.combatActive && G.energy >= card.cost) {
div.onclick = () => playCard(idx);
}
handArea.appendChild(div);
});
}
// ============================================================
// DAMAGE NUMBERS
// ============================================================
function showDamageNumber(amount, target, color) {
const area = document.getElementById('combat-area');
const num = document.createElement('div');
num.className = 'damage-number';
num.textContent = amount > 0 ? `-${amount}` : 'MISS';
num.style.color = color || (target === 'enemy' ? '#ff4444' : '#cc2222');
if (target === 'enemy') {
num.style.left = '50%';
num.style.top = '35%';
num.style.transform = 'translateX(-50%)';
} else {
num.style.left = '50%';
num.style.bottom = '25%';
num.style.transform = 'translateX(-50%)';
}
area.appendChild(num);
setTimeout(() => num.remove(), 1000);
}
// ============================================================
// PARTICLES (Title Screen)
// ============================================================
function initParticles() {
const container = document.getElementById('particles');
for (let i = 0; i < 30; i++) {
const p = document.createElement('div');
p.className = 'particle';
p.style.left = Math.random() * 100 + '%';
p.style.animationDuration = (5 + Math.random() * 10) + 's';
p.style.animationDelay = Math.random() * 10 + 's';
p.style.width = (1 + Math.random() * 2) + 'px';
p.style.height = p.style.width;
container.appendChild(p);
}
}
// ============================================================
// INIT
// ============================================================
initParticles();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"4cc40e53-dc1f-40aa-bde7-88a87d14b162","tokensIn":27327,"tokensOut":18683,"tokensTotal":46010,"turns":2,"toolCalls":1,"failedToolCalls":0,"timestamp":"2026-07-18T23:08:27.829Z"} -->