Deckbuilder
Qwen 3.6 35b a3b · full

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 75,090 bytes · SHA-256 52778915f3a2
<!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');
:root {
--bg-dark: #0a0a12;
--bg-panel: #12121e;
--bg-card: #1a1a2e;
--gold: #d4a843;
--gold-dim: #8a6d2b;
--hp-red: #c0392b;
--hp-red-dark: #7b241c;
--energy-blue: #3498db;
--energy-blue-dim: #1a5276;
--block-silver: #bdc3c7;
--block-silver-dim: #7f8c8d;
--attack-red: #e74c3c;
--defense-blue: #2980b9;
--other-purple: #8e44ad;
--other-green: #27ae60;
--burn-yellow: #f39c12;
--poison-green: #2ecc71;
--void-purple: #6c3483;
--text-primary: #ecf0f1;
--text-secondary: #95a5a6;
--text-gold: #f1c40f;
--border-etched: #2c2c3e;
--shadow-glow: 0 0 20px rgba(212, 168, 67, 0.3);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg-dark);
color: var(--text-primary);
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;
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, #1a1a2e 0%, #0a0a12 70%);
position: relative;
}
#main-menu::before {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M30 0L60 30L30 60L0 30Z' fill='none' stroke='%23ffffff08' stroke-width='0.5'/%3E%3C/svg%3E");
opacity: 0.3;
}
.menu-title {
font-size: 3.5rem;
font-weight: 900;
color: var(--gold);
text-shadow: 0 0 30px rgba(212,168,67,0.5), 0 2px 4px rgba(0,0,0,0.8);
letter-spacing: 0.15em;
margin-bottom: 0.3em;
z-index: 1;
text-align: center;
}
.menu-subtitle {
font-size: 1.2rem;
color: var(--text-secondary);
margin-bottom: 2.5em;
z-index: 1;
font-style: italic;
}
.class-select {
display: flex;
gap: 1.5rem;
z-index: 1;
flex-wrap: wrap;
justify-content: center;
}
.class-card {
width: 220px;
padding: 1.5rem 1rem;
background: linear-gradient(180deg, #1a1a2e 0%, #0f0f1a 100%);
border: 2px solid var(--border-etched);
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
position: relative;
overflow: hidden;
}
.class-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
background: var(--class-accent);
opacity: 0.6;
}
.class-card:hover {
border-color: var(--class-accent);
transform: translateY(-8px);
box-shadow: 0 12px 40px rgba(0,0,0,0.5), 0 0 20px var(--class-glow);
}
.class-icon {
width: 80px; height: 80px;
margin: 0 auto 1rem;
border-radius: 50%;
border: 2px solid var(--class-accent);
display: flex;
align-items: center;
justify-content: center;
background: var(--class-bg);
}
.class-name {
font-family: 'Cinzel', serif;
font-size: 1.3rem;
font-weight: 700;
color: var(--class-accent);
margin-bottom: 0.5rem;
}
.class-theme {
font-size: 0.95rem;
color: var(--text-secondary);
margin-bottom: 0.8rem;
font-style: italic;
}
.class-stats {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.6;
}
.class-stats span {
color: var(--text-primary);
font-weight: 600;
}
/* ===== COMBAT SCREEN ===== */
#combat-screen {
flex-direction: column;
background: radial-gradient(ellipse at 50% 30%, #1a1a2e 0%, #0a0a12 60%);
position: relative;
}
#combat-screen::before {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='50' cy='50' r='0.5' fill='%23ffffff05'/%3E%3C/svg%3E");
pointer-events: none;
}
/* Top HUD Bar */
.hud-top {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1.5rem;
background: linear-gradient(180deg, rgba(18,18,30,0.95) 0%, transparent 100%);
z-index: 10;
border-bottom: 1px solid var(--border-etched);
}
.hud-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: 'Cinzel', serif;
font-size: 1rem;
font-weight: 700;
}
.hud-icon {
width: 24px; height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.hud-gold { color: var(--gold); }
.hud-floor { color: var(--text-gold); }
.hud-enemy-hp { color: var(--hp-red); }
/* Combat Arena */
.combat-arena {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
z-index: 5;
padding: 1rem;
}
/* Enemy Display */
.enemy-area {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1rem;
position: relative;
}
.enemy-sprite {
width: 160px;
height: 180px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.enemy-sprite svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 0 15px rgba(108, 52, 131, 0.4));
transition: all 0.3s;
}
.enemy-sprite.hit svg {
animation: enemyHit 0.3s ease;
}
@keyframes enemyHit {
0%, 100% { transform: translateX(0); filter: drop-shadow(0 0 15px rgba(108, 52, 131, 0.4)); }
25% { transform: translateX(-8px); filter: brightness(2) drop-shadow(0 0 20px rgba(231, 76, 60, 0.6)); }
50% { transform: translateX(8px); filter: brightness(1.5); }
75% { transform: translateX(-4px); }
}
.enemy-name {
font-family: 'Cinzel', serif;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-gold);
margin-bottom: 0.4rem;
text-shadow: 0 0 10px rgba(241, 196, 15, 0.3);
}
.enemy-hp-bar {
width: 200px;
height: 16px;
background: #1a1a2e;
border: 1px solid var(--border-etched);
border-radius: 8px;
overflow: hidden;
position: relative;
}
.enemy-hp-fill {
height: 100%;
background: linear-gradient(90deg, var(--hp-red-dark), var(--hp-red));
border-radius: 8px;
transition: width 0.5s ease;
position: relative;
}
.enemy-hp-fill::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255,255,255,0.15), transparent);
border-radius: 8px 8px 0 0;
}
.enemy-hp-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
font-family: 'Cinzel', serif;
color: white;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
}
.enemy-status-effects {
display: flex;
gap: 0.3rem;
margin-top: 0.4rem;
min-height: 20px;
}
.status-icon {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: 700;
border: 1px solid rgba(255,255,255,0.2);
}
.status-burn { background: var(--burn-yellow); color: #000; }
.status-poison { background: var(--poison-green); color: #000; }
.status-stun { background: var(--block-silver); color: #000; }
.status-silence { background: var(--void-purple); color: #fff; }
/* Player Stats (above hand) */
.player-stats {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 0.6rem 1.5rem;
background: linear-gradient(0deg, rgba(18,18,30,0.95) 0%, transparent 100%);
width: 100%;
justify-content: center;
z-index: 10;
}
.stat-bar-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
}
.stat-bar-label {
font-family: 'Cinzel', serif;
font-size: 0.7rem;
color: var(--text-secondary);
font-weight: 600;
}
.stat-bar {
width: 180px;
height: 14px;
background: #1a1a2e;
border: 1px solid var(--border-etched);
border-radius: 7px;
overflow: hidden;
position: relative;
}
.stat-bar-fill {
height: 100%;
border-radius: 7px;
transition: width 0.4s ease;
position: relative;
}
.stat-bar-fill::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255,255,255,0.12), transparent);
border-radius: 7px 7px 0 0;
}
.stat-bar-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
font-weight: 700;
font-family: 'Cinzel', serif;
color: white;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
}
.hp-fill { background: linear-gradient(90deg, var(--hp-red-dark), var(--hp-red)); }
.energy-fill { background: linear-gradient(90deg, var(--energy-blue-dim), var(--energy-blue)); }
.block-display {
display: flex;
align-items: center;
gap: 0.3rem;
font-family: 'Cinzel', serif;
font-size: 1rem;
color: var(--block-silver);
}
.block-icon {
width: 20px; height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
/* Energy Pips */
.energy-pips {
display: flex;
gap: 0.3rem;
align-items: center;
}
.energy-pip {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid var(--energy-blue);
background: transparent;
transition: all 0.3s;
}
.energy-pip.filled {
background: var(--energy-blue);
box-shadow: 0 0 8px rgba(52, 152, 219, 0.5);
}
.energy-pip.spent {
background: var(--energy-blue-dim);
border-color: var(--block-silver-dim);
box-shadow: none;
}
/* Hand Area */
.hand-area {
display: flex;
justify-content: center;
align-items: flex-end;
gap: 0.5rem;
padding: 1rem 1rem 0.5rem;
min-height: 220px;
z-index: 20;
position: relative;
flex-wrap: nowrap;
}
/* Cards */
.card {
width: 130px;
height: 190px;
border-radius: 10px;
cursor: pointer;
transition: all 0.25s ease;
position: relative;
flex-shrink: 0;
transform-origin: bottom center;
}
.card:hover {
transform: translateY(-20px) scale(1.05);
z-index: 30;
}
.card.selected {
transform: translateY(-30px) scale(1.08);
z-index: 35;
}
.card.unplayable {
opacity: 0.55;
filter: grayscale(0.3);
}
.card.unplayable:hover {
transform: translateY(-10px) scale(1.02);
}
.card-inner {
width: 100%;
height: 100%;
border-radius: 10px;
overflow: hidden;
position: relative;
border: 2px solid var(--border-etched);
}
.card-type-attack .card-inner { border-color: var(--attack-red); box-shadow: 0 0 10px rgba(231,76,60,0.2); }
.card-type-defense .card-inner { border-color: var(--defense-blue); box-shadow: 0 0 10px rgba(41,128,185,0.2); }
.card-type-other .card-inner { border-color: var(--other-purple); box-shadow: 0 0 10px rgba(142,68,173,0.2); }
.card-cost {
position: absolute;
top: -6px;
left: -6px;
width: 30px;
height: 30px;
border-radius: 50%;
background: radial-gradient(circle, var(--energy-blue), var(--energy-blue-dim));
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cinzel', serif;
font-weight: 900;
font-size: 0.9rem;
color: white;
border: 2px solid var(--energy-blue);
box-shadow: 0 0 10px rgba(52,152,219,0.4);
z-index: 2;
}
.card-art {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.card-art-bg {
position: absolute;
inset: 0;
opacity: 0.15;
}
.card-type-attack .card-art-bg { background: linear-gradient(135deg, var(--attack-red), transparent); }
.card-type-defense .card-art-bg { background: linear-gradient(135deg, var(--defense-blue), transparent); }
.card-type-other .card-art-bg { background: linear-gradient(135deg, var(--other-purple), transparent); }
.card-art-icon {
width: 60px;
height: 60px;
position: relative;
z-index: 1;
opacity: 0.7;
}
.card-art-placeholder {
width: 50px;
height: 50px;
position: relative;
z-index: 1;
opacity: 0.4;
border: 2px dashed rgba(255,255,255,0.3);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.card-art-placeholder svg {
width: 24px;
height: 24px;
opacity: 0.5;
}
.card-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0.5rem;
background: linear-gradient(0deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 70%, transparent 100%);
text-align: center;
}
.card-name {
font-family: 'Cinzel', serif;
font-size: 0.7rem;
font-weight: 700;
color: var(--text-gold);
margin-bottom: 0.15rem;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
.card-stats {
font-size: 0.65rem;
color: var(--text-secondary);
font-weight: 600;
}
.card-ability {
font-size: 0.58rem;
color: var(--text-secondary);
font-style: italic;
margin-top: 0.15rem;
line-height: 1.3;
max-height: 2.6em;
overflow: hidden;
}
/* Bottom Controls */
.controls {
display: flex;
justify-content: center;
gap: 1rem;
padding: 0.8rem 1.5rem 1rem;
background: linear-gradient(0deg, rgba(18,18,30,0.95) 0%, transparent 100%);
z-index: 10;
}
.btn {
font-family: 'Cinzel', serif;
font-weight: 700;
font-size: 1rem;
padding: 0.7rem 2rem;
border: 2px solid var(--border-etched);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 0.1em;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255,255,255,0.08), transparent);
}
.btn-primary {
background: linear-gradient(180deg, #2c2c3e, #1a1a2e);
color: var(--text-gold);
border-color: var(--gold-dim);
}
.btn-primary:hover {
background: linear-gradient(180deg, #3c3c4e, #2a2a3e);
border-color: var(--gold);
box-shadow: 0 0 15px rgba(212,168,67,0.3);
transform: translateY(-2px);
}
.btn-end-turn {
background: linear-gradient(180deg, var(--hp-red-dark), #5a1a15);
color: white;
border-color: var(--hp-red);
}
.btn-end-turn:hover {
background: linear-gradient(180deg, var(--hp-red), #7b241c);
box-shadow: 0 0 15px rgba(231,76,60,0.3);
transform: translateY(-2px);
}
.btn-shop {
background: linear-gradient(180deg, #2c2c3e, #1a1a2e);
color: var(--gold);
border-color: var(--gold-dim);
}
.btn-shop:hover {
background: linear-gradient(180deg, #3c3c4e, #2a2a3e);
border-color: var(--gold);
box-shadow: 0 0 15px rgba(212,168,67,0.3);
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
/* ===== SHOP SCREEN ===== */
#shop-screen {
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a12 70%);
}
.shop-title {
font-size: 2.5rem;
font-weight: 900;
color: var(--gold);
text-shadow: 0 0 30px rgba(212,168,67,0.5);
margin-bottom: 0.5rem;
}
.shop-subtitle {
color: var(--text-secondary);
font-style: italic;
margin-bottom: 2rem;
}
.shop-gold-display {
font-family: 'Cinzel', serif;
font-size: 1.3rem;
color: var(--gold);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.shop-cards {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 2rem;
max-width: 900px;
}
.shop-card {
width: 180px;
cursor: pointer;
transition: all 0.25s;
position: relative;
}
.shop-card:hover {
transform: translateY(-10px);
}
.shop-card-inner {
width: 100%;
border-radius: 10px;
overflow: hidden;
border: 2px solid var(--border-etched);
background: var(--bg-card);
}
.shop-card-cost {
position: absolute;
top: 8px;
left: 8px;
width: 32px;
height: 32px;
border-radius: 50%;
background: radial-gradient(circle, var(--energy-blue), var(--energy-blue-dim));
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cinzel', serif;
font-weight: 900;
font-size: 0.9rem;
color: white;
border: 2px solid var(--energy-blue);
z-index: 2;
}
.shop-card-owned {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0,0,0,0.7);
color: var(--text-secondary);
font-size: 0.65rem;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Cinzel', serif;
z-index: 2;
}
.shop-card-art {
width: 100%;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.shop-card-info {
padding: 0.7rem;
text-align: center;
}
.shop-card-name {
font-family: 'Cinzel', serif;
font-size: 0.8rem;
font-weight: 700;
color: var(--text-gold);
margin-bottom: 0.2rem;
}
.shop-card-ability {
font-size: 0.65rem;
color: var(--text-secondary);
font-style: italic;
line-height: 1.3;
}
.shop-card-price {
font-family: 'Cinzel', serif;
font-size: 0.75rem;
color: var(--gold);
margin-top: 0.3rem;
font-weight: 700;
}
.shop-actions {
display: flex;
gap: 1rem;
}
/* ===== VICTORY / DEFEAT SCREENS ===== */
#victory-screen, #defeat-screen {
flex-direction: column;
align-items: center;
justify-content: center;
background: radial-gradient(ellipse at center, #1a1a2e 0%, #0a0a12 70%);
}
#victory-screen { background: radial-gradient(ellipse at center, #1a2a1e 0%, #0a0a12 70%); }
.end-title {
font-size: 3rem;
font-weight: 900;
margin-bottom: 1rem;
text-shadow: 0 0 30px currentColor;
}
.end-subtitle {
font-size: 1.2rem;
color: var(--text-secondary);
font-style: italic;
margin-bottom: 2rem;
}
.end-stats {
font-family: 'Cinzel', serif;
font-size: 1rem;
color: var(--text-secondary);
margin-bottom: 2rem;
text-align: center;
line-height: 2;
}
.end-stats span { color: var(--text-gold); font-weight: 700; }
/* ===== COMBAT LOG ===== */
.combat-log {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 100;
}
.log-entry {
font-family: 'Cinzel', serif;
font-size: 1.2rem;
font-weight: 700;
color: var(--text-gold);
text-shadow: 0 0 10px rgba(212,168,67,0.5), 0 2px 4px rgba(0,0,0,0.8);
opacity: 0;
animation: logFloat 2s ease forwards;
white-space: nowrap;
}
.log-entry.damage { color: var(--hp-red); text-shadow: 0 0 10px rgba(231,76,60,0.5); }
.log-entry.heal { color: var(--other-green); text-shadow: 0 0 10px rgba(39,174,96,0.5); }
.log-entry.block { color: var(--block-silver); text-shadow: 0 0 10px rgba(189,195,199,0.5); }
.log-entry.energy { color: var(--energy-blue); text-shadow: 0 0 10px rgba(52,152,219,0.5); }
.log-entry.gold { color: var(--gold); text-shadow: 0 0 10px rgba(212,168,67,0.5); }
.log-entry.status { color: var(--burn-yellow); text-shadow: 0 0 10px rgba(243,156,18,0.5); }
@keyframes logFloat {
0% { opacity: 0; transform: translateY(20px); }
15% { opacity: 1; transform: translateY(0); }
70% { opacity: 1; transform: translateY(-10px); }
100% { opacity: 0; transform: translateY(-30px); }
}
/* Damage number popup */
.damage-popup {
position: absolute;
font-family: 'Cinzel', serif;
font-weight: 900;
font-size: 1.5rem;
pointer-events: none;
z-index: 200;
animation: damagePop 1s ease forwards;
}
@keyframes damagePop {
0% { opacity: 1; transform: translateY(0) scale(0.5); }
30% { transform: translateY(-20px) scale(1.2); }
100% { opacity: 0; transform: translateY(-60px) scale(0.8); }
}
/* ===== FLOOR TRANSITION ===== */
.floor-transition {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 500;
animation: fadeIn 0.5s ease;
}
.floor-transition-title {
font-size: 2.5rem;
font-weight: 900;
color: var(--gold);
text-shadow: 0 0 30px rgba(212,168,67,0.5);
margin-bottom: 0.5rem;
}
.floor-transition-sub {
font-size: 1.1rem;
color: var(--text-secondary);
font-style: italic;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* ===== TURN INDICATOR ===== */
.turn-indicator {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%);
font-family: 'Cinzel', serif;
font-size: 1rem;
font-weight: 700;
color: var(--text-gold);
text-shadow: 0 0 10px rgba(241,196,15,0.3);
opacity: 0.7;
z-index: 10;
pointer-events: none;
}
/* ===== SCROLLBAR ===== */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg-dark); }
::-webkit-scrollbar-thumb { background: var(--border-etched); border-radius: 3px; }
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.menu-title { font-size: 2rem; }
.class-card { width: 180px; padding: 1rem; }
.card { width: 100px; height: 150px; }
.card-name { font-size: 0.6rem; }
.card-ability { font-size: 0.5rem; }
.hand-area { gap: 0.3rem; }
.shop-cards { gap: 1rem; }
.shop-card { width: 150px; }
}
</style>
</head>
<body>
<!-- ===== SVG DEFS ===== -->
<svg style="display:none" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Card Backs -->
<pattern id="back-bastion" width="20" height="20" patternUnits="userSpaceOnUse">
<rect width="20" height="20" fill="#1a1a2e"/>
<path d="M10 0L20 10L10 20L0 10Z" fill="none" stroke="#8a6d2b" stroke-width="0.5" opacity="0.3"/>
<circle cx="10" cy="10" r="3" fill="none" stroke="#8a6d2b" stroke-width="0.5" opacity="0.3"/>
</pattern>
<pattern id="back-wither" width="16" height="16" patternUnits="userSpaceOnUse">
<rect width="16" height="16" fill="#1a1a2e"/>
<path d="M8 2C12 6 12 10 8 14C4 10 4 6 8 2Z" fill="none" stroke="#6c3483" stroke-width="0.5" opacity="0.3"/>
</pattern>
<pattern id="back-shroud" width="12" height="12" patternUnits="userSpaceOnUse">
<rect width="12" height="12" fill="#1a1a2e"/>
<circle cx="6" cy="6" r="4" fill="none" stroke="#2980b9" stroke-width="0.5" opacity="0.3"/>
</pattern>
<!-- Bastion Card Frame -->
<g id="frame-bastion">
<rect width="130" height="190" rx="10" fill="#1a1a2e" stroke="#8a6d2b" stroke-width="2"/>
<rect x="4" y="4" width="122" height="182" rx="7" fill="none" stroke="#d4a843" stroke-width="0.5" opacity="0.5"/>
<path d="M65 12L72 20L65 18L58 20Z" fill="#d4a843" opacity="0.6"/>
<path d="M65 178L72 170L65 172L58 170Z" fill="#d4a843" opacity="0.6"/>
<line x1="20" y1="80" x2="110" y2="80" stroke="#8a6d2b" stroke-width="0.5" opacity="0.3"/>
</g>
<!-- Wither Card Frame -->
<g id="frame-wither">
<rect width="130" height="190" rx="10" fill="#1a1a2e" stroke="#6c3483" stroke-width="2"/>
<rect x="4" y="4" width="122" height="182" rx="7" fill="none" stroke="#8e44ad" stroke-width="0.5" opacity="0.5"/>
<path d="M65 8C68 14 72 16 65 22C58 16 62 14 65 8Z" fill="#8e44ad" opacity="0.5"/>
<path d="M65 168C68 162 72 160 65 166C58 160 62 162 65 168Z" fill="#8e44ad" opacity="0.5"/>
<line x1="20" y1="80" x2="110" y2="80" stroke="#6c3483" stroke-width="0.5" opacity="0.3"/>
</g>
<!-- Shroud Card Frame -->
<g id="frame-shroud">
<rect width="130" height="190" rx="10" fill="#1a1a2e" stroke="#2980b9" stroke-width="2"/>
<rect x="4" y="4" width="122" height="182" rx="7" fill="none" stroke="#3498db" stroke-width="0.5" opacity="0.5"/>
<circle cx="65" cy="14" r="4" fill="none" stroke="#3498db" stroke-width="0.5" opacity="0.5"/>
<circle cx="65" cy="176" r="4" fill="none" stroke="#3498db" stroke-width="0.5" opacity="0.5"/>
<line x1="20" y1="80" x2="110" y2="80" stroke="#2980b9" stroke-width="0.5" opacity="0.3"/>
</g>
<!-- Bastion Card Back -->
<g id="cardback-bastion">
<rect width="130" height="190" rx="10" fill="url(#back-bastion)" stroke="#8a6d2b" stroke-width="2"/>
<rect x="8" y="8" width="114" height="174" rx="6" fill="none" stroke="#d4a843" stroke-width="1" opacity="0.4"/>
<path d="M65 40L80 65L65 90L50 65Z" fill="none" stroke="#d4a843" stroke-width="1.5" opacity="0.5"/>
<circle cx="65" cy="65" r="10" fill="none" stroke="#d4a843" stroke-width="1" opacity="0.4"/>
<circle cx="65" cy="65" r="4" fill="#d4a843" opacity="0.3"/>
<text x="65" y="155" text-anchor="middle" fill="#d4a843" font-family="Cinzel" font-size="8" opacity="0.5">BASTION</text>
</g>
<!-- Wither Card Back -->
<g id="cardback-wither">
<rect width="130" height="190" rx="10" fill="url(#back-wither)" stroke="#6c3483" stroke-width="2"/>
<rect x="8" y="8" width="114" height="174" rx="6" fill="none" stroke="#8e44ad" stroke-width="1" opacity="0.4"/>
<path d="M65 35C75 50 75 80 65 95C55 80 55 50 65 35Z" fill="none" stroke="#8e44ad" stroke-width="1.5" opacity="0.5"/>
<path d="M65 35L60 50L65 45L70 50Z" fill="#8e44ad" opacity="0.3"/>
<text x="65" y="155" text-anchor="middle" fill="#8e44ad" font-family="Cinzel" font-size="8" opacity="0.5">WITHER</text>
</g>
<!-- Shroud Card Back -->
<g id="cardback-shroud">
<rect width="130" height="190" rx="10" fill="url(#back-shroud)" stroke="#2980b9" stroke-width="2"/>
<rect x="8" y="8" width="114" height="174" rx="6" fill="none" stroke="#3498db" stroke-width="1" opacity="0.4"/>
<circle cx="65" cy="65" r="20" fill="none" stroke="#3498db" stroke-width="1" opacity="0.4"/>
<circle cx="65" cy="65" r="12" fill="none" stroke="#3498db" stroke-width="0.8" opacity="0.3"/>
<circle cx="65" cy="65" r="5" fill="#3498db" opacity="0.2"/>
<text x="65" y="155" text-anchor="middle" fill="#3498db" font-family="Cinzel" font-size="8" opacity="0.5">SHROUD</text>
</g>
<!-- Enemy Sprites -->
<g id="enemy-goblin">
<rect width="160" height="180" fill="none"/>
<ellipse cx="80" cy="140" rx="35" ry="20" fill="#2d5a27"/>
<rect x="55" y="80" width="50" height="60" rx="10" fill="#3a7a32"/>
<circle cx="68" cy="65" r="18" fill="#4a8a42"/>
<circle cx="92" cy="65" r="18" fill="#4a8a42"/>
<circle cx="68" cy="60" r="6" fill="#c0392b"/>
<circle cx="92" cy="60" r="6" fill="#c0392b"/>
<circle cx="68" cy="60" r="3" fill="#000"/>
<circle cx="92" cy="60" r="3" fill="#000"/>
<path d="M65 75L72 72L80 78L88 72L95 75" fill="none" stroke="#2d5a27" stroke-width="2"/>
<path d="M55 80L40 60L50 75Z" fill="#3a7a32"/>
<path d="M105 80L120 60L110 75Z" fill="#3a7a32"/>
</g>
<g id="enemy-rat">
<ellipse cx="80" cy="130" rx="40" ry="25" fill="#4a3728"/>
<ellipse cx="80" cy="120" rx="30" ry="20" fill="#5a4738"/>
<circle cx="60" cy="100" r="15" fill="#5a4738"/>
<circle cx="55" cy="96" r="4" fill="#c0392b"/>
<circle cx="65" cy="96" r="4" fill="#c0392b"/>
<circle cx="55" cy="96" r="2" fill="#000"/>
<circle cx="65" cy="96" r="2" fill="#000"/>
<path d="M60 108L63 105L66 108L69 105L72 108" fill="none" stroke="#3a2718" stroke-width="1.5"/>
<path d="M45 100L35 85L42 95Z" fill="#5a4738"/>
<path d="M115 100L125 85L118 95Z" fill="#5a4738"/>
<path d="M110 130L130 140L120 135Z" fill="#4a3728"/>
</g>
<g id="enemy-vampire">
<ellipse cx="80" cy="150" rx="30" ry="15" fill="#1a1a2e"/>
<rect x="55" y="70" width="50" height="80" rx="8" fill="#2c2c3e"/>
<circle cx="80" cy="55" r="22" fill="#3a3a4e"/>
<circle cx="70" cy="50" r="5" fill="#c0392b"/>
<circle cx="90" cy="50" r="5" fill="#c0392b"/>
<circle cx="70" cy="50" r="2.5" fill="#000"/>
<circle cx="90" cy="50" r="2.5" fill="#000"/>
<path d="M72 65L75 62L78 65L81 62L84 65L87 62L90 65" fill="none" stroke="#c0392b" stroke-width="1.5"/>
<path d="M60 35L55 15L65 30Z" fill="#2c2c3e"/>
<path d="M100 35L105 15L95 30Z" fill="#2c2c3e"/>
<path d="M55 70L35 50L45 70Z" fill="#2c2c3e"/>
<path d="M105 70L125 50L115 70Z" fill="#2c2c3e"/>
</g>
<g id="enemy-ogre">
<ellipse cx="80" cy="155" rx="45" ry="18" fill="#3a3a3a"/>
<rect x="45" y="60" width="70" height="90" rx="12" fill="#4a4a4a"/>
<circle cx="80" cy="45" r="28" fill="#5a5a5a"/>
<circle cx="68" cy="40" r="6" fill="#c0392b"/>
<circle cx="92" cy="40" r="6" fill="#c0392b"/>
<circle cx="68" cy="40" r="3" fill="#000"/>
<circle cx="92" cy="40" r="3" fill="#000"/>
<path d="M65 60L72 55L80 62L88 55L95 60" fill="none" stroke="#3a3a3a" stroke-width="2"/>
<path d="M45 60L25 40L35 55Z" fill="#4a4a4a"/>
<path d="M115 60L135 40L125 55Z" fill="#4a4a4a"/>
<rect x="55" y="70" width="15" height="20" rx="3" fill="#3a3a3a" opacity="0.5"/>
<rect x="90" y="70" width="15" height="20" rx="3" fill="#3a3a3a" opacity="0.5"/>
</g>
<g id="enemy-voidwarrior">
<ellipse cx="80" cy="150" rx="35" ry="15" fill="#2c1a4a"/>
<rect x="50" y="55" width="60" height="90" rx="10" fill="#3c2a5a"/>
<circle cx="80" cy="40" r="25" fill="#4c3a6a"/>
<circle cx="70" cy="35" r="5" fill="#6c3483"/>
<circle cx="90" cy="35" r="5" fill="#6c3483"/>
<circle cx="70" cy="35" r="2.5" fill="#9b59b6"/>
<circle cx="90" cy="35" r="2.5" fill="#9b59b6"/>
<path d="M68 52L75 48L80 52L85 48L92 52" fill="none" stroke="#6c3483" stroke-width="1.5"/>
<path d="M50 55L30 35L40 50Z" fill="#3c2a5a"/>
<path d="M110 55L130 35L120 50Z" fill="#3c2a5a"/>
<circle cx="80" cy="85" r="12" fill="none" stroke="#6c3483" stroke-width="1" opacity="0.5"/>
<circle cx="80" cy="85" r="6" fill="#6c3483" opacity="0.3"/>
</g>
<g id="enemy-cultist">
<ellipse cx="80" cy="150" rx="30" ry="12" fill="#1a0a2a"/>
<path d="M50 140L55 50L105 50L110 140Z" fill="#2a1a3a"/>
<circle cx="80" cy="40" r="20" fill="#3a2a4a"/>
<path d="M60 25L80 10L100 25" fill="none" stroke="#4a3a5a" stroke-width="2"/>
<circle cx="72" cy="38" r="4" fill="#6c3483"/>
<circle cx="88" cy="38" r="4" fill="#6c3483"/>
<circle cx="72" cy="38" r="2" fill="#9b59b6"/>
<circle cx="88" cy="38" r="2" fill="#9b59b6"/>
<path d="M72 50L76 47L80 50L84 47L88 50" fill="none" stroke="#6c3483" stroke-width="1"/>
<path d="M65 55L55 40L62 50Z" fill="#2a1a3a"/>
<path d="M95 55L105 40L98 50Z" fill="#2a1a3a"/>
</g>
<!-- Boss Sprites -->
<g id="boss-gatekeeper">
<ellipse cx="80" cy="160" rx="50" ry="18" fill="#2a2a2a"/>
<rect x="35" y="40" width="90" height="110" rx="15" fill="#3a3a3a"/>
<circle cx="80" cy="30" r="30" fill="#4a4a4a"/>
<circle cx="68" cy="25" r="7" fill="#c0392b"/>
<circle cx="92" cy="25" r="7" fill="#c0392b"/>
<circle cx="68" cy="25" r="3.5" fill="#000"/>
<circle cx="92" cy="25" r="3.5" fill="#000"/>
<path d="M60 45L68 40L75 46L82 40L89 46L96 40L104 45" fill="none" stroke="#c0392b" stroke-width="2"/>
<path d="M50 10L30 0L55 8Z" fill="#5a5a5a"/>
<path d="M110 10L130 0L105 8Z" fill="#5a5a5a"/>
<rect x="50" y="55" width="60" height="40" rx="5" fill="none" stroke="#c0392b" stroke-width="1.5" opacity="0.5"/>
<circle cx="80" cy="75" r="10" fill="none" stroke="#c0392b" stroke-width="1" opacity="0.4"/>
<path d="M35 40L15 20L25 40Z" fill="#3a3a3a"/>
<path d="M125 40L145 20L135 40Z" fill="#3a3a3a"/>
</g>
<g id="boss-corrupted">
<ellipse cx="80" cy="155" rx="45" ry="15" fill="#1a1a2e"/>
<rect x="40" y="45" width="80" height="105" rx="12" fill="#2a2a3e"/>
<circle cx="80" cy="35" r="28" fill="#3a3a4e"/>
<circle cx="68" cy="30" r="6" fill="#e74c3c"/>
<circle cx="92" cy="30" r="6" fill="#e74c3c"/>
<circle cx="68" cy="30" r="3" fill="#000"/>
<circle cx="92" cy="30" r="3" fill="#000"/>
<path d="M65 48L72 43L80 48L88 43L95 48" fill="none" stroke="#e74c3c" stroke-width="2"/>
<path d="M55 10L45 -5L60 5Z" fill="#4a4a5e"/>
<path d="M105 10L115 -5L100 5Z" fill="#4a4a5e"/>
<circle cx="80" cy="70" r="15" fill="none" stroke="#e74c3c" stroke-width="1.5" opacity="0.5"/>
<circle cx="80" cy="70" r="8" fill="#e74c3c" opacity="0.2"/>
<path d="M40 45L20 25L30 40Z" fill="#2a2a3e"/>
<path d="M120 45L140 25L130 40Z" fill="#2a2a3e"/>
</g>
<g id="boss-voidlord">
<ellipse cx="80" cy="155" rx="55" ry="15" fill="#0a0a1a"/>
<rect x="30" y="35" width="100" height="115" rx="15" fill="#1a1a2e"/>
<circle cx="80" cy="25" r="32" fill="#2a2a3e"/>
<circle cx="65" cy="20" r="7" fill="#6c3483"/>
<circle cx="95" cy="20" r="7" fill="#6c3483"/>
<circle cx="65" cy="20" r="3.5" fill="#9b59b6"/>
<circle cx="95" cy="20" r="3.5" fill="#9b59b6"/>
<path d="M60 38L70 32L80 38L90 32L100 38" fill="none" stroke="#6c3483" stroke-width="2"/>
<path d="M50 5L40 -10L55 0Z" fill="#3a3a4e"/>
<path d="M110 5L120 -10L105 0Z" fill="#3a3a4e"/>
<circle cx="80" cy="60" r="18" fill="none" stroke="#6c3483" stroke-width="1.5" opacity="0.4"/>
<circle cx="80" cy="60" r="10" fill="#6c3483" opacity="0.15"/>
<circle cx="80" cy="60" r="4" fill="#6c3483" opacity="0.3"/>
<path d="M30 35L10 15L20 30Z" fill="#1a1a2e"/>
<path d="M130 35L150 15L140 30Z" fill="#1a1a2e"/>
<path d="M30 35L10 15L15 25Z" fill="none" stroke="#6c3483" stroke-width="0.5" opacity="0.3"/>
<path d="M130 35L150 15L145 25Z" fill="none" stroke="#6c3483" stroke-width="0.5" opacity="0.3"/>
</g>
</defs>
</svg>
<!-- ===== MAIN MENU ===== -->
<div id="main-menu" class="screen active">
<h1 class="menu-title">SHADOWS OF AETHELGARD</h1>
<p class="menu-subtitle">The Void consumes all. Only you can stand against it.</p>
<div class="class-select">
<div class="class-card" onclick="selectClass('bastion')" style="--class-accent: #d4a843; --class-bg: #2a2a1e; --class-glow: rgba(212,168,67,0.3);">
<div class="class-icon">
<svg width="50" height="50" viewBox="0 0 50 50">
<path d="M25 5L35 15L35 35L25 45L15 35L15 15Z" fill="none" stroke="#d4a843" stroke-width="2"/>
<path d="M25 12L30 18L25 24L20 18Z" fill="#d4a843" opacity="0.6"/>
<line x1="25" y1="24" x2="25" y2="38" stroke="#d4a843" stroke-width="2"/>
</svg>
</div>
<div class="class-name">The Bastion</div>
<div class="class-theme">Immovable Object</div>
<div class="class-stats">
HP: <span>35</span><br>
Energy: <span>3</span><br>
Style: Defense & Block
</div>
</div>
<div class="class-card" onclick="selectClass('wither')" style="--class-accent: #8e44ad; --class-bg: #2a1a2e; --class-glow: rgba(142,68,173,0.3);">
<div class="class-icon">
<svg width="50" height="50" viewBox="0 0 50 50">
<path d="M25 8C32 18 32 32 25 42C18 32 18 18 25 8Z" fill="none" stroke="#8e44ad" stroke-width="2"/>
<path d="M25 15C29 22 29 30 25 37C21 30 21 22 25 15Z" fill="#8e44ad" opacity="0.4"/>
<circle cx="25" cy="25" r="4" fill="#8e44ad" opacity="0.6"/>
</svg>
</div>
<div class="class-name">The Wither</div>
<div class="class-theme">Rot and Burn</div>
<div class="class-stats">
HP: <span>28</span><br>
Energy: <span>3</span><br>
Style: Burn & Debuffs
</div>
</div>
<div class="class-card" onclick="selectClass('shroud')" style="--class-accent: #3498db; --class-bg: #1a2a3a; --class-glow: rgba(52,152,219,0.3);">
<div class="class-icon">
<svg width="50" height="50" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="18" fill="none" stroke="#3498db" stroke-width="2"/>
<circle cx="25" cy="25" r="10" fill="none" stroke="#3498db" stroke-width="1.5" opacity="0.6"/>
<circle cx="25" cy="25" r="4" fill="#3498db" opacity="0.5"/>
<path d="M25 7L28 15L25 12L22 15Z" fill="#3498db" opacity="0.4"/>
</svg>
</div>
<div class="class-name">The Shroud</div>
<div class="class-theme">Shadow and Speed</div>
<div class="class-stats">
HP: <span>30</span><br>
Energy: <span>3</span><br>
Style: Speed & Crit
</div>
</div>
</div>
</div>
<!-- ===== COMBAT SCREEN ===== -->
<div id="combat-screen" class="screen">
<div class="hud-top">
<div class="hud-item hud-gold">
<svg class="hud-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="#d4a843" stroke-width="2"/><text x="12" y="16" text-anchor="middle" fill="#d4a843" font-size="12" font-weight="bold" font-family="Cinzel">$</text></svg>
<span id="hud-gold">0</span>
</div>
<div class="hud-item hud-floor">
<span id="hud-floor">Floor 1</span>
</div>
<div class="hud-item hud-enemy-hp">
<svg class="hud-icon" viewBox="0 0 24 24"><path d="M12 21C12 21 4 15 4 9C4 5.5 7 3 12 3C17 3 20 5.5 20 9C20 15 12 21 12 21Z" fill="none" stroke="#c0392b" stroke-width="2"/><circle cx="12" cy="9" r="3" fill="#c0392b" opacity="0.5"/></svg>
<span id="hud-enemy-hp">20 / 20</span>
</div>
</div>
<div class="turn-indicator" id="turn-indicator">Your Turn</div>
<div class="combat-arena">
<div class="enemy-area" id="enemy-area">
<div class="enemy-name" id="enemy-name">Goblin</div>
<div class="enemy-hp-bar">
<div class="enemy-hp-fill" id="enemy-hp-fill" style="width:100%"></div>
<div class="enemy-hp-text" id="enemy-hp-text">20 / 20</div>
</div>
<div class="enemy-status-effects" id="enemy-status"></div>
<div class="enemy-sprite" id="enemy-sprite"></div>
</div>
</div>
<div class="player-stats">
<div class="stat-bar-container">
<div class="stat-bar-label">HP</div>
<div class="stat-bar">
<div class="stat-bar-fill hp-fill" id="player-hp-fill" style="width:100%"></div>
<div class="stat-bar-text" id="player-hp-text">35 / 35</div>
</div>
</div>
<div class="stat-bar-container">
<div class="stat-bar-label">Energy</div>
<div class="energy-pips" id="energy-pips"></div>
</div>
<div class="block-display">
<svg class="block-icon" viewBox="0 0 24 24"><path d="M12 2L4 7V12C4 17.5 8 22 12 23C16 22 20 17.5 20 12V7L12 2Z" fill="none" stroke="#bdc3c7" stroke-width="2"/></svg>
<span id="player-block">0</span>
</div>
</div>
<div class="hand-area" id="hand-area"></div>
<div class="controls">
<button class="btn btn-end-turn" id="btn-end-turn" onclick="endTurn()">End Turn</button>
<button class="btn btn-shop" id="btn-shop" onclick="openShop()" style="display:none">Visit Shop</button>
</div>
<div class="combat-log" id="combat-log"></div>
</div>
<!-- ===== SHOP SCREEN ===== -->
<div id="shop-screen" class="screen">
<h2 class="shop-title">The Dark Market</h2>
<p class="shop-subtitle">Spend your gold wisely, hero.</p>
<div class="shop-gold-display">
<svg width="28" height="28" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="#d4a843" stroke-width="2"/><text x="12" y="16" text-anchor="middle" fill="#d4a843" font-size="12" font-weight="bold" font-family="Cinzel">$</text></svg>
<span id="shop-gold">0</span> Gold
</div>
<div class="shop-cards" id="shop-cards"></div>
<div class="shop-actions">
<button class="btn btn-primary" onclick="closeShop()">Leave Shop</button>
</div>
</div>
<!-- ===== VICTORY SCREEN ===== -->
<div id="victory-screen" class="screen">
<h2 class="end-title" style="color: var(--other-green);">VICTORY</h2>
<p class="end-subtitle">The Void Lord has been vanquished. Aethelgard is saved.</p>
<div class="end-stats" id="victory-stats"></div>
<button class="btn btn-primary" onclick="location.reload()">Play Again</button>
</div>
<!-- ===== DEFEAT SCREEN ===== -->
<div id="defeat-screen" class="screen">
<h2 class="end-title" style="color: var(--hp-red);">DEFEAT</h2>
<p class="end-subtitle">The Void claims another soul...</p>
<div class="end-stats" id="defeat-stats"></div>
<button class="btn btn-primary" onclick="location.reload()">Try Again</button>
</div>
<script>
// ===== CARD DATABASE =====
const ALL_CARDS = {
// Bastion
'BA-A01': { id:'BA-A01', name:'Slam', type:'attack', cost:2, atk:3, def:0, ability:'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, ability:'Deals 4 damage to all enemies.', class:'bastion' },
'BA-A03': { id:'BA-A03', name:'Stone Fist', type:'attack', cost:1, atk:2, def:0, ability:'Deals 2 damage.', class:'bastion' },
'BA-A04': { id:'BA-A04', name:'Boulder', type:'attack', cost:4, atk:6, def:0, ability:'Deals 6 damage.', class:'bastion' },
'BA-D01': { id:'BA-D01', name:'Shield Wall', type:'defense', cost:0, atk:0, def:4, ability:'Gain 4 Defense this turn.', class:'bastion' },
'BA-D02': { id:'BA-D02', name:'Iron Skin', type:'defense', cost:2, atk:0, def:5, ability:'Gain 5 permanent Defense. Discard 1 card.', class:'bastion' },
'BA-D03': { id:'BA-D03', name:'Stand Firm', type:'defense', cost:1, atk:0, def:2, ability:'Gain 2 Defense. Blocks 1 extra damage.', class:'bastion' },
'BA-D04': { id:'BA-D04', name:'Taunt', type:'defense', cost:2, atk:0, def:3, ability:'Gain 3 Defense. Stun 1 enemy for 1 turn.', class:'bastion' },
'BA-U01': { id:'BA-U01', name:'Search', type:'other', cost:2, atk:0, def:0, ability:'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, ability:'Heal 6 HP.', class:'bastion' },
'BA-U03': { id:'BA-U03', name:'Rally', type:'other', cost:1, atk:0, def:0, ability:'Draw 1 card. Gain 1 Energy.', class:'bastion' },
'BA-U04': { id:'BA-U04', name:'Loot', type:'other', cost:1, atk:0, def:0, ability:'Gain 5 Gold.', class:'bastion' },
// Wither
'WI-A01': { id:'WI-A01', name:'Fireball', type:'attack', cost:2, atk:3, def:0, ability:'Deals 3 damage. Apply 1 Burn.', class:'wither' },
'WI-A02': { id:'WI-A02', name:'Inferno', type:'attack', cost:3, atk:4, def:0, ability:'Deals 4 damage. Applies 3 Burn.', class:'wither' },
'WI-A03': { id:'WI-A03', name:'Ember', type:'attack', cost:1, atk:2, def:0, ability:'Deals 2 damage. Applies 1 Burn.', class:'wither' },
'WI-A04': { id:'WI-A04', name:'Pyre', type:'attack', cost:4, atk:5, def:0, ability:'Deals 5 damage. All enemies take 1 damage next turn.', class:'wither' },
'WI-D01': { id:'WI-D01', name:'Scorch Shield', type:'defense', cost:2, atk:0, def:3, ability:'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, ability:'Gain 1 Defense. Gain 1 Energy.', class:'wither' },
'WI-D03': { id:'WI-D03', name:'Smoke', type:'defense', cost:2, atk:0, def:4, ability:'Gain 4 Defense. Block 1 Burn damage.', class:'wither' },
'WI-D04': { id:'WI-D04', name:'Endure', type:'defense', cost:3, atk:0, def:2, ability:'Gain 2 Defense. Heal 2 HP.', class:'wither' },
'WI-U01': { id:'WI-U01', name:'Poison', type:'other', cost:2, atk:0, def:0, ability:'Apply 1 Poison to target.', class:'wither' },
'WI-U02': { id:'WI-U02', name:'Burn', type:'other', cost:3, atk:0, def:0, ability:'Deal 3 damage + 2 Burn to all enemies.', class:'wither' },
'WI-U03': { id:'WI-U03', name:'Slow', type:'other', cost:1, atk:0, def:0, ability:'Draw 1 card. Stun target for 1 turn.', class:'wither' },
'WI-U04': { id:'WI-U04', name:'Wither', type:'other', cost:2, atk:0, def:0, ability:'Heal 5 HP. Reduce enemy Attack by 1.', class:'wither' },
// Shroud
'SH-A01': { id:'SH-A01', name:'Backstab', type:'attack', cost:2, atk:4, def:0, ability:'Deals 4 damage. +1 if target has no Defense.', class:'shroud' },
'SH-A02': { id:'SH-A02', name:'Shadow Bolt', type:'attack', cost:3, atk:5, def:0, ability:'Deals 5 damage. Target cannot block.', class:'shroud' },
'SH-A03': { id:'SH-A03', name:'Silence', type:'attack', cost:1, atk:3, def:0, ability:'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, ability:'Deals 6 damage. Gain 2 Energy.', class:'shroud' },
'SH-D01': { id:'SH-D01', name:'Blur', type:'defense', cost:3, atk:0, def:2, ability:'Gain 2 Defense. Ignore next attack.', class:'shroud' },
'SH-D02': { id:'SH-D02', name:'Step', type:'defense', cost:2, atk:0, def:3, ability:'Gain 3 Defense. Gain 1 Energy.', class:'shroud' },
'SH-D03': { id:'SH-D03', name:'Evasion', type:'defense', cost:1, atk:0, def:1, ability:'Gain 1 Defense. Skip enemy action.', class:'shroud' },
'SH-D04': { id:'SH-D04', name:'Phantom', type:'defense', cost:2, atk:0, def:4, ability:'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, ability:'Gain 1 Energy. Skip enemy attack.', class:'shroud' },
'SH-U02': { id:'SH-U02', name:'Teleport', type:'other', cost:2, atk:0, def:0, ability:'Move to back row. Deal 2 damage.', class:'shroud' },
'SH-U03': { id:'SH-U03', name:'Hide', type:'other', cost:2, atk:0, def:0, ability:'Gain 1 Defense. Discard 1 card. Draw 2.', class:'shroud' },
'SH-U04': { id:'SH-U04', name:'Assassinate', type:'other', cost:3, atk:8, def:0, ability:'Deal 8 damage. Cannot be blocked.', class:'shroud' },
};
// ===== ENEMY DEFINITIONS =====
const ENEMY_TYPES = {
floor1: [
{ id:'goblin', name:'Goblin', hp:20, dmg:3, svg:'enemy-goblin' },
{ id:'rat', name:'Rat', hp:18, dmg:4, svg:'enemy-rat' },
],
floor2: [
{ id:'vampire', name:'Vampire Knight', hp:30, dmg:5, svg:'enemy-vampire' },
{ id:'ogre', name:'Ogre', hp:35, dmg:6, svg:'enemy-ogre' },
],
floor3: [
{ id:'voidwarrior', name:'Void Warrior', hp:40, dmg:8, svg:'enemy-voidwarrior' },
{ id:'cultist', name:'Cultist', hp:35, dmg:10, svg:'enemy-cultist' },
],
bosses: [
{ id:'boss-gatekeeper', name:'The Gatekeeper', hp:80, dmg:15, svg:'boss-gatekeeper', boss:true, mechanic:'gatekeeper' },
{ id:'boss-corrupted', name:'The Corrupted', hp:80, dmg:15, svg:'boss-corrupted', boss:true, mechanic:'corrupted' },
{ id:'boss-voidlord', name:'The Void Lord', hp:80, dmg:15, svg:'boss-voidlord', boss:true, mechanic:'voidlord' },
]
};
// ===== GAME STATE =====
var game = {
class: null,
floor: 1,
room: 0,
totalRooms: 12, // 9 regular + 3 bosses
hero: {
hp: 35, maxHp: 35,
energy: 3, maxEnergy: 5,
gold: 0,
deck: [],
hand: [],
discard: [],
inventory: {}, // cardId -> count
permanentDefense: 0,
block: 0,
},
enemy: null,
turn: 'player', // 'player' or 'enemy'
isBossFight: false,
stats: { cardsPlayed: 0, damageDealt: 0, goldEarned: 0, roomsCleared: 0 },
shopAvailable: false,
skipEnemyAction: false,
};
// ===== CLASS CONFIG =====
const CLASS_CONFIG = {
bastion: { startHp: 35, startEnergy: 3, startDeck: ['BA-A01','BA-A02','BA-A03','BA-A04','BA-D01','BA-D02','BA-D03','BA-D04','BA-U01','BA-U02','BA-U03','BA-U04'] },
wither: { startHp: 28, startEnergy: 3, startDeck: ['WI-A01','WI-A02','WI-A03','WI-A04','WI-D01','WI-D02','WI-D03','WI-D04','WI-U01','WI-U02','WI-U03','WI-U04'] },
shroud: { startHp: 30, startEnergy: 3, startDeck: ['SH-A01','SH-A02','SH-A03','SH-A04','SH-D01','SH-D02','SH-D03','SH-D04','SH-U01','SH-U02','SH-U03','SH-U04'] },
};
// ===== UTILITY =====
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
function addLog(text, type = '') {
const log = document.getElementById('combat-log');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = text;
log.appendChild(entry);
setTimeout(() => entry.remove(), 2000);
}
function addDamagePopup(value, targetEl, color = '#e74c3c') {
const popup = document.createElement('div');
popup.className = 'damage-popup';
popup.textContent = value;
popup.style.color = color;
popup.style.left = targetEl.getBoundingClientRect().left + targetEl.offsetWidth/2 - 20 + 'px';
popup.style.top = targetEl.getBoundingClientRect().top + 'px';
document.body.appendChild(popup);
setTimeout(() => popup.remove(), 1000);
}
// ===== CLASS SELECTION =====
function selectClass(cls) {
game.class = cls;
const cfg = CLASS_CONFIG[cls];
game.hero.maxHp = cfg.startHp;
game.hero.hp = cfg.startHp;
game.hero.maxEnergy = 5;
game.hero.energy = cfg.startEnergy;
game.hero.deck = shuffle([...cfg.startDeck, ...cfg.startDeck]); // 2 copies of each starter
game.hero.discard = [];
game.hero.inventory = {};
game.hero.permanentDefense = 0;
game.hero.block = 0;
game.hero.gold = 0;
game.floor = 1;
game.room = 0;
game.stats = { cardsPlayed: 0, damageDealt: 0, goldEarned: 0, roomsCleared: 0 };
game.shopAvailable = false;
game.skipEnemyAction = false;
showScreen('combat-screen');
startCombat();
}
// ===== COMBAT =====
function startCombat() {
game.hero.block = 0;
game.hero.energy = game.hero.maxEnergy;
game.hero.permanentDefense = 0;
game.skipEnemyAction = false;
game.turn = 'player';
// Determine enemy
if (game.room % 3 === 2) {
// Boss fight
const bossIdx = Math.floor(game.room / 3);
const bossDef = ENEMY_TYPES.bosses[bossIdx];
game.enemy = Object.assign({}, bossDef, {
currentHp: Number(bossDef.hp),
maxHp: Number(bossDef.hp),
currentDmg: Number(bossDef.dmg),
block: 0,
statusEffects: {},
isBoss: true,
mechanicState: {},
});
game.isBossFight = true;
} else {
const floorKey = 'floor' + game.floor;
const types = ENEMY_TYPES[floorKey];
const def = types[Math.floor(Math.random() * types.length)];
game.enemy = Object.assign({}, def, {
currentHp: Number(def.hp),
maxHp: Number(def.hp),
currentDmg: Number(def.dmg),
block: 0,
statusEffects: {},
isBoss: false,
mechanicState: {},
});
game.isBossFight = false;
}
// Discard hand and refresh deck
game.hero.discard = [...game.hero.hand, ...game.hero.discard];
game.hero.hand = [];
drawCards(4);
updateHUD();
updateEnergyPips();
renderEnemy();
renderHand();
updateTurnIndicator();
document.getElementById('btn-shop').style.display = 'none';
document.getElementById('btn-end-turn').disabled = false;
}
function drawCards(count) {
let drawn = 0;
while (drawn < count && game.hero.deck.length > 0) {
if (game.hero.hand.length >= 5 + (game.class === 'shroud' ? 1 : 0)) break;
if (game.hero.deck.length === 0) {
game.hero.deck = shuffle(game.hero.discard);
game.hero.discard = [];
}
game.hero.hand.push(game.hero.deck.pop());
drawn++;
}
return drawn;
}
function updateHUD() {
// Gold
document.getElementById('hud-gold').textContent = game.hero.gold;
// Floor
document.getElementById('hud-floor').textContent = `Floor ${game.floor}`;
// Enemy HP
const hpPct = Math.max(0, (game.enemy.currentHp / game.enemy.maxHp) * 100);
document.getElementById('enemy-hp-fill').style.width = hpPct + '%';
document.getElementById('enemy-hp-text').textContent = `${Math.max(0, game.enemy.currentHp)} / ${game.enemy.maxHp}`;
document.getElementById('hud-enemy-hp').textContent = `${Math.max(0, game.enemy.currentHp)} / ${game.enemy.maxHp}`;
// Player HP
const hpPct2 = Math.max(0, (game.hero.hp / game.hero.maxHp) * 100);
document.getElementById('player-hp-fill').style.width = hpPct2 + '%';
document.getElementById('player-hp-text').textContent = `${Math.max(0, game.hero.hp)} / ${game.hero.maxHp}`;
// Block
document.getElementById('player-block').textContent = game.hero.block;
}
function updateEnergyPips() {
const container = document.getElementById('energy-pips');
container.innerHTML = '';
for (let i = 0; i < game.hero.maxEnergy; i++) {
const pip = document.createElement('div');
pip.className = 'energy-pip';
if (i < game.hero.energy) {
pip.classList.add('filled');
}
container.appendChild(pip);
}
}
function updateTurnIndicator() {
const el = document.getElementById('turn-indicator');
if (game.turn === 'player') {
el.textContent = 'Your Turn';
el.style.color = 'var(--text-gold)';
} else {
el.textContent = 'Enemy Turn';
el.style.color = 'var(--hp-red)';
}
}
function renderEnemy() {
const sprite = document.getElementById('enemy-sprite');
sprite.innerHTML = `<svg viewBox="0 0 160 180">${game.enemy.svg ? `<use href="#${game.enemy.svg}"/>` : ''}</svg>`;
document.getElementById('enemy-name').textContent = game.enemy.name;
renderEnemyStatus();
}
function renderEnemyStatus() {
const container = document.getElementById('enemy-status');
container.innerHTML = '';
for (const [type, count] of Object.entries(game.enemy.statusEffects)) {
if (count <= 0) continue;
const icon = document.createElement('div');
icon.className = `status-icon status-${type.toLowerCase()}`;
icon.textContent = count;
icon.title = `${type}: ${count} turns`;
container.appendChild(icon);
}
}
function renderHand() {
const container = document.getElementById('hand-area');
container.innerHTML = '';
game.hero.hand.forEach((cardId, idx) => {
const card = ALL_CARDS[cardId];
const canPlay = game.turn === 'player' && card.cost <= game.hero.energy;
const div = document.createElement('div');
div.className = `card card-type-${card.type} ${canPlay ? '' : 'unplayable'}`;
div.dataset.idx = idx;
div.onclick = () => playCard(idx);
const frameId = `frame-${card.class}`;
const backId = `cardback-${card.class}`;
div.innerHTML = `
<div class="card-inner">
<div class="card-cost">${card.cost}</div>
<div class="card-art">
<div class="card-art-bg"></div>
<svg class="card-art-placeholder" viewBox="0 0 50 50">
<use href="#${backId}"/>
</svg>
</div>
<div class="card-info">
<div class="card-name">${card.name}</div>
<div class="card-stats">${card.type === 'attack' ? `⚔ ${card.atk}` : card.type === 'defense' ? `🛡 ${card.def}` : '✦ Other'}</div>
<div class="card-ability">${card.ability}</div>
</div>
</div>
`;
container.appendChild(div);
});
}
// ===== CARD PLAY =====
function playCard(handIdx) {
if (game.turn !== 'player') return;
const cardId = game.hero.hand[handIdx];
const card = ALL_CARDS[cardId];
if (card.cost > game.hero.energy) return;
// Pay energy
game.hero.energy -= card.cost;
updateEnergyPips();
// Execute card effect
executeCardEffect(card, handIdx);
// Move card to discard
game.hero.discard.push(game.hero.hand.splice(handIdx, 1)[0]);
game.stats.cardsPlayed++;
updateHUD();
renderHand();
// Check if enemy is dead
if (game.enemy.currentHp <= 0) {
setTimeout(() => enemyDefeated(), 600);
}
}
function executeCardEffect(card, handIdx) {
const enemy = game.enemy;
if (card.type === 'attack') {
let damage = card.atk;
// Backstab bonus
if (card.id === 'SH-A01' && enemy.currentHp > 0 && enemy.currentHp <= enemy.maxHp) {
// Check if target has no armor - simplified: always +1 for this prototype
damage += 1;
}
// Shadow Bolt: target cannot block (handled in damage calculation)
if (card.id === 'SH-A02') {
enemy.block = Math.max(0, enemy.block); // no effect on enemy
}
// Deal damage
if (enemy.currentHp > 0) {
let blocked = 0;
if (card.id !== 'SH-U04') { // Assassinate cannot be blocked
blocked = Math.min(enemy.block, damage);
enemy.block -= blocked;
}
const actualDmg = damage - blocked;
enemy.currentHp -= actualDmg;
game.stats.damageDealt += actualDmg;
addLog(`Dealt ${actualDmg} damage!`, 'damage');
addDamagePopup(actualDmg, document.getElementById('enemy-sprite'), '#e74c3c');
// Hit animation
const sprite = document.getElementById('enemy-sprite');
sprite.classList.remove('hit');
void sprite.offsetWidth;
sprite.classList.add('hit');
}
// Status effects
if (card.id === 'WI-A01') {
enemy.statusEffects['Burn'] = (enemy.statusEffects['Burn'] || 0) + 1;
addLog('Applied 1 Burn!', 'status');
}
if (card.id === 'WI-A02') {
enemy.statusEffects['Burn'] = (enemy.statusEffects['Burn'] || 0) + 3;
addLog('Applied 3 Burn!', 'status');
}
if (card.id === 'WI-A03') {
enemy.statusEffects['Burn'] = (enemy.statusEffects['Burn'] || 0) + 1;
addLog('Applied 1 Burn!', 'status');
}
if (card.id === 'BA-D04') {
enemy.statusEffects['Stun'] = (enemy.statusEffects['Stun'] || 0) + 1;
addLog('Stunned enemy!', 'status');
}
if (card.id === 'WI-U01') {
enemy.statusEffects['Poison'] = (enemy.statusEffects['Poison'] || 0) + 1;
addLog('Applied 1 Poison!', 'status');
}
if (card.id === 'WI-U03') {
enemy.statusEffects['Stun'] = (enemy.statusEffects['Stun'] || 0) + 1;
addLog('Stunned enemy!', 'status');
}
if (card.id === 'SH-A03') {
enemy.statusEffects['Silence'] = (enemy.statusEffects['Silence'] || 0) + 1;
addLog('Silenced enemy!', 'status');
}
// Pyre: all enemies take 1 damage next turn (handled at start of next player turn)
if (card.id === 'WI-A04') {
enemy.mechanicState['pyreNextTurn'] = true;
}
} else if (card.type === 'defense') {
let blockGain = card.def;
game.hero.block += blockGain;
addLog(`Gained ${blockGain} Block!`, 'block');
// Iron Skin: permanent defense + discard
if (card.id === 'BA-D02') {
game.hero.permanentDefense += blockGain;
// Discard 1 random card
if (game.hero.hand.length > 0) {
const discardIdx = Math.floor(Math.random() * game.hero.hand.length);
game.hero.discard.push(game.hero.hand.splice(discardIdx, 1)[0]);
addLog('Discarded 1 card.', 'block');
}
}
// Stand Firm: blocks 1 extra
if (card.id === 'BA-D03') {
game.hero.block += 1;
addLog('Extra block!', 'block');
}
// Heat Resist: gain energy
if (card.id === 'WI-D02') {
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
updateEnergyPips();
addLog('+1 Energy!', 'energy');
}
// Step: gain energy
if (card.id === 'SH-D02') {
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
updateEnergyPips();
addLog('+1 Energy!', 'energy');
}
// Evasion: skip enemy action
if (card.id === 'SH-D03') {
game.skipEnemyAction = true;
addLog('Enemy action skipped!', 'energy');
}
// Phantom: ghost form (skip next attack)
if (card.id === 'SH-D04') {
game.skipEnemyAction = true;
addLog('Ghost form - next attack ignored!', 'energy');
}
// Scorch Shield / Smoke: block burn damage (passive, handled in damage calc)
} else if (card.type === 'other') {
if (card.id === 'BA-U01') {
// Search: draw 1, discard if > 5
drawCards(1);
if (game.hero.hand.length > 5) {
while (game.hero.hand.length > 5) {
game.hero.discard.push(game.hero.hand.pop());
}
addLog('Hand discarded (too large).', 'block');
}
addLog('Drew 1 card!', 'energy');
}
if (card.id === 'BA-U02') {
const heal = Math.min(6, game.hero.maxHp - game.hero.hp);
game.hero.hp += heal;
addLog(`Healed ${heal} HP!`, 'heal');
addDamagePopup(`+${heal}`, document.getElementById('player-hp-fill'), '#2ecc71');
}
if (card.id === 'BA-U03') {
drawCards(1);
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
updateEnergyPips();
addLog('+1 Energy, drew 1 card!', 'energy');
}
if (card.id === 'BA-U04') {
game.hero.gold += 5;
game.stats.goldEarned += 5;
addLog('+5 Gold!', 'gold');
}
if (card.id === 'WI-U02') {
let dmg = 3;
if (enemy.currentHp > 0) {
let blocked = Math.min(enemy.block, dmg);
enemy.block -= blocked;
enemy.currentHp -= (dmg - blocked);
game.stats.damageDealt += (dmg - blocked);
addLog(`Dealt ${dmg - blocked} damage!`, 'damage');
addDamagePopup(dmg - blocked, document.getElementById('enemy-sprite'), '#e74c3c');
}
enemy.statusEffects['Burn'] = (enemy.statusEffects['Burn'] || 0) + 2;
addLog('Applied 2 Burn to all enemies!', 'status');
}
if (card.id === 'WI-U04') {
const heal = Math.min(5, game.hero.maxHp - game.hero.hp);
game.hero.hp += heal;
game.enemy.currentDmg = Math.max(1, game.enemy.currentDmg - 1);
addLog(`Healed ${heal} HP. Enemy ATK -1.`, 'heal');
}
if (card.id === 'SH-U01') {
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
updateEnergyPips();
game.skipEnemyAction = true;
addLog('+1 Energy. Enemy attack skipped!', 'energy');
}
if (card.id === 'SH-U02') {
let dmg = 2;
if (enemy.currentHp > 0) {
enemy.currentHp -= dmg;
game.stats.damageDealt += dmg;
addLog(`Dealt ${dmg} damage!`, 'damage');
addDamagePopup(dmg, document.getElementById('enemy-sprite'), '#e74c3c');
}
}
if (card.id === 'SH-U03') {
game.hero.block += 1;
if (game.hero.hand.length > 0) {
game.hero.discard.push(game.hero.hand.splice(Math.floor(Math.random() * game.hero.hand.length), 1)[0]);
}
drawCards(2);
addLog('+1 Block. Discarded 1, drew 2!', 'block');
}
if (card.id === 'SH-U04') {
let dmg = 8;
if (enemy.currentHp > 0) {
enemy.currentHp -= dmg;
game.stats.damageDealt += dmg;
addLog(`Dealt ${dmg} damage!`, 'damage');
addDamagePopup(dmg, document.getElementById('enemy-sprite'), '#e74c3c');
}
}
}
// Update HUD after card play
updateHUD();
renderEnemyStatus();
}
// ===== END TURN =====
function endTurn() {
if (game.turn !== 'player') return;
game.turn = 'enemy';
updateTurnIndicator();
// Process burn/poison DoTs on enemy
processDoT(game.enemy, 'enemy');
// Pyre damage from WI-A04
if (game.enemy.mechanicState['pyreNextTurn']) {
game.enemy.currentHp -= 1;
game.enemy.mechanicState['pyreNextTurn'] = false;
addLog('Pyre burns the enemy!', 'status');
if (game.enemy.currentHp <= 0) {
setTimeout(() => enemyDefeated(), 400);
return;
}
}
// Boss mechanics - Gatekeeper
if (game.enemy.mechanicState['gatekeeper'] && game.enemy.currentHp < 20) {
game.enemy.mechanicState['gatekeeper'] = false;
}
if (game.enemy.mechanicState['gatekeeper']) {
game.enemy.block += 5;
addLog('Gatekeeper blocks 5 damage!', 'block');
}
// Boss mechanics - Corrupted
if (game.enemy.mechanicState['corrupted'] && game.hero.energy < 3) {
const extraDmg = 20;
const actualDmg = Math.max(0, extraDmg - game.hero.block - game.hero.permanentDefense);
game.hero.hp -= actualDmg;
game.hero.block = Math.max(0, game.hero.block - extraDmg);
addLog(`Corrupted deals ${extraDmg} burst!`, 'damage');
addDamagePopup(extraDmg, document.getElementById('player-hp-fill'), '#e74c3c');
}
// Boss mechanics - Void Lord regen
if (game.enemy.mechanicState['voidlord']) {
const regen = 10;
game.enemy.currentHp = Math.min(game.enemy.maxHp, game.enemy.currentHp + regen);
addLog(`Void Lord regenerates ${regen} HP!`, 'heal');
updateHUD();
}
// Enemy action
setTimeout(() => {
if (game.skipEnemyAction) {
game.skipEnemyAction = false;
addLog('Enemy action skipped!', 'energy');
startNewTurn();
return;
}
if (game.enemy.statusEffects['Stun'] && game.enemy.statusEffects['Stun'] > 0) {
game.enemy.statusEffects['Stun']--;
addLog(`${game.enemy.name} is stunned!`, 'status');
startNewTurn();
return;
}
if (game.enemy.statusEffects['Silence'] && game.enemy.statusEffects['Silence'] > 0) {
game.enemy.statusEffects['Silence']--;
addLog(`${game.enemy.name} is silenced!`, 'status');
startNewTurn();
return;
}
// Boss special attacks
if (game.enemy.isBoss) {
resolveBossAttack();
} else {
resolveEnemyAttack();
}
}, 800);
}
function resolveEnemyAttack() {
const enemy = game.enemy;
if (enemy.currentHp <= 0) return;
const rawDmg = enemy.currentDmg;
// Check if attack should be ignored (Blur, Phantom, etc.)
if (game.skipEnemyAction) {
game.skipEnemyAction = false;
addLog('Attack ignored!', 'block');
startNewTurn();
return;
}
// Cannot be blocked check (Assassinate effect - already handled)
const blockAmount = game.hero.block + game.hero.permanentDefense;
const actualDmg = Math.max(0, rawDmg - blockAmount);
game.hero.block = Math.max(0, game.hero.block - rawDmg);
game.hero.permanentDefense = Math.max(0, game.hero.permanentDefense - Math.max(0, rawDmg - game.hero.block));
game.hero.hp -= actualDmg;
addLog(`${enemy.name} deals ${actualDmg} damage!`, 'damage');
addDamagePopup(actualDmg, document.getElementById('player-hp-fill'), '#e74c3c');
updateHUD();
if (game.hero.hp <= 0) {
setTimeout(() => playerDefeated(), 500);
return;
}
setTimeout(() => startNewTurn(), 600);
}
function resolveBossAttack() {
const enemy = game.enemy;
if (enemy.currentHp <= 0) return;
let rawDmg = enemy.currentDmg;
// Gatekeeper special: blocks 5 per turn until HP < 20
if (enemy.mechanicState['gatekeeper'] && enemy.block > 0) {
const blocked = Math.min(enemy.block, rawDmg);
enemy.block -= blocked;
rawDmg -= blocked;
addLog(`Gatekeeper blocks ${blocked}!`, 'block');
}
// Corrupted special: if player has < 3 energy, deal 20 extra
if (enemy.mechanicState['corrupted'] && game.hero.energy < 3) {
rawDmg += 20;
addLog('Corrupted burst attacks!', 'damage');
}
// Void Lord special: cannot be blocked
if (enemy.mechanicState['voidlord']) {
game.hero.hp -= rawDmg;
addLog(`Void Lord deals ${rawDmg} (unblockable)!`, 'damage');
addDamagePopup(rawDmg, document.getElementById('player-hp-fill'), '#6c3483');
updateHUD();
if (game.hero.hp <= 0) {
setTimeout(() => playerDefeated(), 500);
return;
}
setTimeout(() => startNewTurn(), 600);
return;
}
const blockAmount = game.hero.block;
const actualDmg = Math.max(0, rawDmg - blockAmount);
game.hero.block = Math.max(0, game.hero.block - rawDmg);
game.hero.hp -= actualDmg;
addLog(`${enemy.name} deals ${actualDmg} damage!`, 'damage');
addDamagePopup(actualDmg, document.getElementById('player-hp-fill'), '#e74c3c');
updateHUD();
if (game.hero.hp <= 0) {
setTimeout(() => playerDefeated(), 500);
return;
}
setTimeout(() => startNewTurn(), 600);
}
function processDoT(target, targetType) {
if (targetType === 'enemy') {
// Burn
if (target.statusEffects['Burn'] && target.statusEffects['Burn'] > 0) {
const burnDmg = target.statusEffects['Burn'];
target.currentHp -= burnDmg;
addLog(`${burnDmg} Burn damage!`, 'status');
addDamagePopup(burnDmg, document.getElementById('enemy-sprite'), '#f39c12');
target.statusEffects['Burn']--;
if (target.currentHp <= 0) {
setTimeout(() => enemyDefeated(), 400);
}
}
// Poison
if (target.statusEffects['Poison'] && target.statusEffects['Poison'] > 0) {
const poisonDmg = target.statusEffects['Poison'];
target.currentHp -= poisonDmg;
addLog(`${poisonDmg} Poison damage!`, 'status');
addDamagePopup(poisonDmg, document.getElementById('enemy-sprite'), '#2ecc71');
target.statusEffects['Poison']--;
if (target.currentHp <= 0) {
setTimeout(() => enemyDefeated(), 400);
}
}
}
}
function startNewTurn() {
game.turn = 'player';
game.hero.energy = Math.min(game.hero.maxEnergy, game.hero.energy + 1);
updateEnergyPips();
// Draw cards
drawCards(4);
renderHand();
updateHUD();
updateTurnIndicator();
document.getElementById('btn-shop').style.display = 'none';
document.getElementById('btn-end-turn').disabled = false;
}
// ===== ENEMY DEFEATED =====
function enemyDefeated() {
const goldReward = game.enemy.isBoss ? 15 : 5;
game.hero.gold += goldReward;
game.stats.goldEarned += goldReward;
game.stats.roomsCleared++;
addLog(`${game.enemy.name} defeated! +${goldReward} Gold`, 'gold');
if (game.isBossFight) {
game.floor++;
if (game.floor > 3) {
setTimeout(() => showVictory(), 1000);
return;
}
// Floor transition
showFloorTransition(`Floor ${game.floor}`, getFloorDescription(game.floor));
setTimeout(() => {
game.room++;
startCombat();
}, 2000);
} else {
game.room++;
if (game.room >= game.totalRooms) {
setTimeout(() => showVictory(), 1000);
return;
}
// Show shop after rooms 1 and 4 (between boss fights)
if (game.room === 1 || game.room === 4) {
setTimeout(() => openShop(), 800);
} else {
setTimeout(() => startCombat(), 1000);
}
}
}
function getFloorDescription(floor) {
const descs = {
1: 'Goblins and rats lurk in the shadows.',
2: 'Vampire Knights and Ogres guard the path.',
3: 'The Void Warriors and Cultists await.',
};
return descs[floor] || '';
}
function showFloorTransition(title, subtitle) {
const el = document.createElement('div');
el.className = 'floor-transition';
el.innerHTML = `
<div class="floor-transition-title">${title}</div>
<div class="floor-transition-sub">${subtitle}</div>
`;
document.body.appendChild(el);
setTimeout(() => {
el.style.opacity = '0';
el.style.transition = 'opacity 0.5s';
setTimeout(() => el.remove(), 500);
}, 1800);
}
// ===== SHOP =====
function openShop() {
showScreen('shop-screen');
document.getElementById('shop-gold').textContent = game.hero.gold;
const container = document.getElementById('shop-cards');
container.innerHTML = '';
// Generate 3 random cards from the player's class
const classCards = Object.values(ALL_CARDS).filter(c => c.class === game.class);
const available = classCards.filter(c => (game.hero.inventory[c.id] || 0) < 5);
const shopCards = shuffle(available).slice(0, 3);
if (shopCards.length === 0) {
container.innerHTML = '<p style="color:var(--text-secondary); font-style:italic;">No cards available. You own all copies.</p>';
}
shopCards.forEach(card => {
const owned = game.hero.inventory[card.id] || 0;
const cost = Math.min(50, card.cost + owned); // Cost scaling
const div = document.createElement('div');
div.className = 'shop-card';
div.onclick = () => buyCard(card.id);
const frameId = `frame-${card.class}`;
div.innerHTML = `
<div class="shop-card-inner">
<div class="shop-card-cost">${cost}</div>
<div class="shop-card-owned">Owned: ${owned}/5</div>
<div class="shop-card-art">
<svg viewBox="0 0 130 120">
<use href="#${frameId}"/>
</svg>
</div>
<div class="shop-card-info">
<div class="shop-card-name">${card.name}</div>
<div class="shop-card-ability">${card.ability}</div>
<div class="shop-card-price">${cost} Gold</div>
</div>
</div>
`;
container.appendChild(div);
});
}
function buyCard(cardId) {
const card = ALL_CARDS[cardId];
const owned = game.hero.inventory[cardId] || 0;
const cost = Math.min(50, card.cost + owned);
if (game.hero.gold < cost) {
addLog('Not enough gold!', 'damage');
return;
}
if (owned >= 5) return;
game.hero.gold -= cost;
game.hero.inventory[cardId] = owned + 1;
game.hero.deck.push(cardId);
document.getElementById('shop-gold').textContent = game.hero.gold;
addLog(`Bought ${card.name}!`, 'gold');
openShop(); // Refresh shop
}
function closeShop() {
showScreen('combat-screen');
startCombat();
}
// ===== VICTORY / DEFEAT =====
function showVictory() {
showScreen('victory-screen');
document.getElementById('victory-stats').innerHTML = `
Class: <span>${game.class.charAt(0).toUpperCase() + game.class.slice(1)}</span><br>
Rooms Cleared: <span>${game.stats.roomsCleared}</span><br>
Damage Dealt: <span>${game.stats.damageDealt}</span><br>
Gold Earned: <span>${game.stats.goldEarned}</span><br>
Cards Played: <span>${game.stats.cardsPlayed}</span>
`;
}
function playerDefeated() {
showScreen('defeat-screen');
document.getElementById('defeat-stats').innerHTML = `
Class: <span>${game.class.charAt(0).toUpperCase() + game.class.slice(1)}</span><br>
Floor Reached: <span>${game.floor}</span><br>
Rooms Cleared: <span>${game.stats.roomsCleared}</span><br>
Damage Dealt: <span>${game.stats.damageDealt}</span><br>
Cards Played: <span>${game.stats.cardsPlayed}</span>
`;
}
// ===== INIT =====
// Auto-select first class for testing if needed
// selectClass('bastion');
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"full","sessionId":"ad2ad3aa-665f-4510-a40e-24144a71df97","tokensIn":1926234,"tokensOut":56345,"tokensTotal":1982579,"turns":40,"toolCalls":39,"failedToolCalls":1,"timestamp":"2026-07-21T02:41:29.164Z"} -->