Deckbuilder
Gemini 3.7 Flash · 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 87,351 bytes · SHA-256 60ef9f4fed93
<!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>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700;900&family=MedievalSharp&family=Metamorphous&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #08070c;
--bg-panel: rgba(18, 15, 26, 0.85);
--gold-glow: #d4af37;
--gold-bright: #ffd700;
--metal-dark: #2a2634;
--metal-border: #4a4358;
--metal-highlight: #786d8d;
--atk-color: #ff4747;
--atk-glow: rgba(255, 71, 71, 0.4);
--atk-grad: linear-gradient(135deg, #3d1414, #1f0b0b);
--def-color: #4da6ff;
--def-glow: rgba(77, 166, 255, 0.4);
--def-grad: linear-gradient(135deg, #14283d, #0b151f);
--oth-color: #b05cff;
--oth-glow: rgba(176, 92, 255, 0.4);
--oth-grad: linear-gradient(135deg, #2d143d, #14081f);
--energy-orb: #38bdf8;
--hp-bar: #ef4444;
--block-bar: #3b82f6;
--font-gothic: 'Cinzel', serif;
--font-body: 'MedievalSharp', cursive, serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
body {
background-color: var(--bg-dark);
color: #e5e0d8;
font-family: var(--font-gothic);
overflow: hidden;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
}
/* Background Atmosphere */
#world-bg {
position: fixed;
inset: 0;
background: radial-gradient(circle at 50% 40%, #1a1226 0%, #0c0814 60%, #030206 100%);
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.fog-layer {
position: absolute;
inset: -50%;
background: radial-gradient(ellipse at center, rgba(147, 51, 234, 0.08) 0%, transparent 70%);
animation: fogFloat 25s infinite alternate linear;
}
@keyframes fogFloat {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(4%, 3%) scale(1.1); }
100% { transform: translate(-3%, -2%) scale(1.05); }
}
/* APP CONTAINER */
#game-container {
position: relative;
width: 1280px;
height: 720px;
background: rgba(10, 8, 15, 0.6);
border: 2px solid #322b40;
box-shadow: 0 0 50px rgba(0, 0, 0, 0.9), inset 0 0 100px rgba(0, 0, 0, 0.8);
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 1;
}
/* SCREENS */
.screen {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
z-index: 10;
transition: opacity 0.4s ease;
}
.screen.hidden {
opacity: 0;
pointer-events: none;
}
/* MAIN MENU SCREEN */
#screen-menu {
justify-content: center;
align-items: center;
background: radial-gradient(circle, rgba(28, 18, 48, 0.95), rgba(6, 4, 10, 0.98));
text-align: center;
padding: 40px;
}
.game-title {
font-size: 54px;
font-weight: 900;
letter-spacing: 6px;
color: #f7e6c4;
text-shadow: 0 0 20px #d4af37, 0 0 40px #7928ca;
margin-bottom: 8px;
}
.game-subtitle {
font-family: var(--font-body);
font-size: 20px;
color: #a79cb8;
letter-spacing: 3px;
margin-bottom: 40px;
}
.class-select-container {
display: flex;
gap: 24px;
margin-bottom: 30px;
}
.class-card {
width: 250px;
background: linear-gradient(180deg, rgba(35, 29, 50, 0.9), rgba(16, 13, 24, 0.95));
border: 2px solid var(--metal-border);
border-radius: 8px;
padding: 20px 16px;
cursor: pointer;
transition: all 0.25s ease;
position: relative;
overflow: hidden;
}
.class-card:hover, .class-card.selected {
transform: translateY(-8px);
border-color: var(--gold-glow);
box-shadow: 0 10px 25px rgba(212, 175, 55, 0.25);
}
.class-card.selected::before {
content: 'SELECTED';
position: absolute;
top: 10px;
right: -25px;
background: var(--gold-glow);
color: #111;
font-size: 10px;
font-weight: 900;
padding: 2px 30px;
transform: rotate(45deg);
}
.class-icon {
width: 80px;
height: 80px;
margin: 0 auto 12px;
}
.class-name {
font-size: 20px;
font-weight: 700;
color: #ffeedd;
margin-bottom: 6px;
}
.class-tagline {
font-family: var(--font-body);
font-size: 13px;
color: #d4af37;
margin-bottom: 12px;
}
.class-stats {
font-size: 12px;
color: #b7b0c8;
text-align: left;
line-height: 1.6;
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 10px;
}
/* BUTTONS */
.btn-gothic {
background: linear-gradient(180deg, #3d354d 0%, #1f1a29 100%);
border: 1.5px solid var(--metal-highlight);
color: #eeddcc;
padding: 12px 36px;
font-family: var(--font-gothic);
font-size: 16px;
font-weight: 700;
letter-spacing: 2px;
cursor: pointer;
border-radius: 4px;
box-shadow: 0 4px 10px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.2);
transition: all 0.2s ease;
text-transform: uppercase;
}
.btn-gothic:hover:not(:disabled) {
background: linear-gradient(180deg, #53486a 0%, #2b243b 100%);
border-color: var(--gold-glow);
color: #fff;
box-shadow: 0 0 15px rgba(212, 175, 55, 0.4);
transform: translateY(-2px);
}
.btn-gothic:active:not(:disabled) {
transform: translateY(1px);
}
.btn-gothic:disabled {
opacity: 0.4;
cursor: not-allowed;
filter: grayscale(0.8);
}
.btn-gold {
background: linear-gradient(180deg, #a67c1e 0%, #5f440b 100%);
border-color: var(--gold-bright);
color: #fff8e7;
}
.btn-gold:hover:not(:disabled) {
background: linear-gradient(180deg, #c99829 0%, #7e5c12 100%);
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
/* TOP HUD BAR */
#top-bar {
height: 52px;
background: linear-gradient(180deg, rgba(20, 16, 30, 0.95), rgba(12, 9, 18, 0.9));
border-bottom: 1.5px solid var(--metal-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
z-index: 20;
}
.hud-section {
display: flex;
align-items: center;
gap: 20px;
}
.hud-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 700;
letter-spacing: 1px;
}
.hud-icon {
width: 22px;
height: 22px;
fill: currentColor;
}
.hud-gold { color: #facc15; }
.hud-floor { color: #c084fc; }
.hud-room { color: #94a3b8; }
.hud-deck-view {
background: rgba(255,255,255,0.06);
padding: 4px 12px;
border-radius: 4px;
border: 1px solid var(--metal-border);
cursor: pointer;
transition: 0.2s;
font-size: 12px;
color: #ddd;
}
.hud-deck-view:hover {
border-color: var(--gold-glow);
background: rgba(212, 175, 55, 0.15);
}
/* MAIN PLAY AREA LAYOUT */
#combat-layout {
flex: 1;
display: grid;
grid-template-columns: 310px 1fr 220px;
grid-template-rows: 1fr 60px;
position: relative;
overflow: hidden;
}
/* LEFT: PLAYER HAND & HERO HUD */
#player-column {
background: rgba(14, 11, 20, 0.7);
border-right: 1px solid rgba(74, 67, 88, 0.5);
display: flex;
flex-direction: column;
padding: 12px;
gap: 8px;
overflow: hidden;
}
.hero-compact-card {
background: rgba(24, 19, 36, 0.85);
border: 1px solid var(--metal-border);
border-radius: 6px;
padding: 10px;
display: flex;
gap: 12px;
align-items: center;
}
.hero-avatar {
width: 54px;
height: 54px;
border-radius: 6px;
border: 1.5px solid var(--gold-glow);
background: #110d1c;
overflow: hidden;
flex-shrink: 0;
}
.hero-vitals {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.hero-name-row {
display: flex;
justify-content: space-between;
font-size: 13px;
font-weight: 700;
color: #ffeedd;
}
.bar-wrap {
width: 100%;
height: 10px;
background: #181424;
border-radius: 5px;
border: 1px solid #3b334a;
overflow: hidden;
position: relative;
}
.bar-fill {
height: 100%;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.bar-fill.hp { background: linear-gradient(90deg, #dc2626, #ef4444); }
.bar-fill.def { background: linear-gradient(90deg, #2563eb, #60a5fa); }
.bar-text {
font-size: 10px;
font-family: var(--font-body);
color: #ccc;
display: flex;
justify-content: space-between;
}
.status-pill-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 18px;
}
.status-pill {
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
font-weight: 700;
display: inline-flex;
align-items: center;
gap: 3px;
animation: pulse 1.5s infinite alternate;
}
.status-pill.burn { background: #7c2d12; color: #fed7aa; border: 1px solid #f97316; }
.status-pill.poison { background: #14532d; color: #bbf7d0; border: 1px solid #22c55e; }
.status-pill.stun { background: #581c87; color: #f3e8ff; border: 1px solid #a855f7; }
.status-pill.silence { background: #374151; color: #e5e7eb; border: 1px solid #9ca3af; }
.status-pill.ghost { background: #1e293b; color: #93c5fd; border: 1px solid #38bdf8; }
/* HAND OF CARDS */
.hand-header {
font-size: 11px;
letter-spacing: 1.5px;
color: #9f96b0;
display: flex;
justify-content: space-between;
padding: 0 4px;
}
#hand-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
padding-right: 4px;
}
#hand-container::-webkit-scrollbar {
width: 4px;
}
#hand-container::-webkit-scrollbar-thumb {
background: var(--metal-border);
border-radius: 2px;
}
/* CARD COMPONENT */
.aethel-card {
width: 100%;
height: 94px;
border-radius: 6px;
position: relative;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.2, 0, 0, 1);
display: flex;
padding: 6px 8px;
gap: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
border: 1.5px solid transparent;
flex-shrink: 0;
}
.aethel-card:hover {
transform: translateX(6px) scale(1.02);
z-index: 5;
}
.aethel-card.unplayable {
opacity: 0.5;
filter: grayscale(0.7);
cursor: not-allowed;
}
.aethel-card.unplayable:hover {
transform: none;
}
.aethel-card.selected-for-action {
border-color: var(--gold-bright) !important;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.6) !important;
transform: translateX(10px) scale(1.04);
}
/* Card Types Colors */
.card-type-Attack {
background: var(--atk-grad);
border-color: #8f2d2d;
}
.card-type-Attack:hover {
box-shadow: 0 0 14px var(--atk-glow);
border-color: var(--atk-color);
}
.card-type-Defense {
background: var(--def-grad);
border-color: #275685;
}
.card-type-Defense:hover {
box-shadow: 0 0 14px var(--def-glow);
border-color: var(--def-color);
}
.card-type-Other {
background: var(--oth-grad);
border-color: #6d2c94;
}
.card-type-Other:hover {
box-shadow: 0 0 14px var(--oth-glow);
border-color: var(--oth-color);
}
.card-art-box {
width: 70px;
height: 100%;
border-radius: 4px;
overflow: hidden;
background: #000;
border: 1px solid rgba(255,255,255,0.15);
flex-shrink: 0;
position: relative;
}
.card-details {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.card-head {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 13px;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-cost-badge {
background: radial-gradient(circle, #0284c7, #0369a1);
color: #fff;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 900;
border: 1.5px solid #7dd3fc;
box-shadow: 0 0 8px rgba(56, 189, 248, 0.6);
flex-shrink: 0;
}
.card-desc {
font-family: var(--font-body);
font-size: 11px;
color: #cbd5e1;
line-height: 1.25;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
color: #94a3b8;
}
.card-stat-pill {
padding: 1px 5px;
border-radius: 3px;
font-weight: 700;
font-size: 10px;
}
.stat-atk { background: rgba(239, 68, 68, 0.25); color: #fca5a5; }
.stat-def { background: rgba(59, 130, 246, 0.25); color: #93c5fd; }
/* CENTER: ARENA / ENEMY DISPLAY / COMBAT LOG */
#center-column {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: radial-gradient(circle at 50% 60%, rgba(30, 21, 48, 0.4) 0%, rgba(9, 7, 15, 0.8) 80%);
}
#combat-arena {
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: space-around;
position: relative;
}
.entity-stage {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
/* HERO SPRITE STAGE */
.hero-stage {
width: 180px;
}
.hero-sprite-box {
width: 150px;
height: 190px;
position: relative;
filter: drop-shadow(0 10px 15px rgba(0,0,0,0.8));
transition: transform 0.2s ease;
}
.hero-sprite-box.hero-attack {
animation: heroLunge 0.4s ease;
}
.hero-sprite-box.hero-hit {
animation: hitShake 0.4s ease;
}
@keyframes heroLunge {
0% { transform: translateX(0); }
50% { transform: translateX(45px) scale(1.05); }
100% { transform: translateX(0); }
}
@keyframes hitShake {
0%, 100% { transform: translateX(0); filter: brightness(1); }
20% { transform: translateX(-12px); filter: brightness(2) drop-shadow(0 0 10px #ff0000); }
40% { transform: translateX(10px); }
60% { transform: translateX(-6px); }
80% { transform: translateX(4px); }
}
/* ENEMY SPRITE STAGE */
.enemy-stage {
width: 280px;
}
.enemy-intent-banner {
background: rgba(18, 14, 26, 0.9);
border: 1px solid #78350f;
color: #fde047;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
box-shadow: 0 0 12px rgba(234, 179, 8, 0.2);
animation: floatSmall 2s infinite ease-in-out;
}
@keyframes floatSmall {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.enemy-sprite-box {
width: 200px;
height: 220px;
position: relative;
filter: drop-shadow(0 12px 20px rgba(0,0,0,0.9));
transition: transform 0.2s ease;
}
.enemy-sprite-box.enemy-attack {
animation: enemyLunge 0.4s ease;
}
.enemy-sprite-box.enemy-hit {
animation: hitShake 0.4s ease;
}
@keyframes enemyLunge {
0% { transform: translateX(0); }
50% { transform: translateX(-45px) scale(1.05); }
100% { transform: translateX(0); }
}
.enemy-hud {
width: 100%;
background: rgba(18, 13, 27, 0.85);
border: 1px solid var(--metal-border);
border-radius: 6px;
padding: 8px 12px;
margin-top: 10px;
}
.enemy-name-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.enemy-name {
font-size: 15px;
font-weight: 700;
color: #fca5a5;
}
.boss-tag {
background: #7f1d1d;
border: 1px solid #ef4444;
color: #fee2e2;
font-size: 9px;
font-weight: 900;
padding: 1px 6px;
border-radius: 3px;
letter-spacing: 1px;
}
/* FLOATING COMBAT TEXT */
.floating-text {
position: absolute;
font-size: 24px;
font-weight: 900;
pointer-events: none;
animation: floatUpFade 1s cubic-bezier(0.1, 0.8, 0.3, 1) forwards;
z-index: 100;
text-shadow: 0 2px 4px #000, 0 0 10px currentColor;
}
@keyframes floatUpFade {
0% { opacity: 0; transform: translateY(0) scale(0.6); }
20% { opacity: 1; transform: translateY(-15px) scale(1.2); }
100% { opacity: 0; transform: translateY(-55px) scale(0.9); }
}
/* COMBAT LOG OVERLAY */
#combat-log-box {
width: 100%;
height: 65px;
background: rgba(10, 7, 16, 0.75);
border: 1px solid rgba(74, 67, 88, 0.4);
border-radius: 4px;
padding: 6px 12px;
overflow-y: auto;
font-family: var(--font-body);
font-size: 12px;
color: #cbd5e1;
display: flex;
flex-direction: column;
gap: 3px;
}
#combat-log-box::-webkit-scrollbar {
width: 4px;
}
#combat-log-box::-webkit-scrollbar-thumb {
background: var(--metal-border);
}
.log-entry {
line-height: 1.2;
}
.log-player { color: #93c5fd; }
.log-enemy { color: #fca5a5; }
.log-status { color: #fde047; }
.log-system { color: #c084fc; font-weight: 700; }
/* RIGHT COLUMN: ENERGY & SHOP & CONTROLS */
#right-column {
background: rgba(14, 11, 20, 0.7);
border-left: 1px solid rgba(74, 67, 88, 0.5);
display: flex;
flex-direction: column;
padding: 16px 12px;
justify-content: space-between;
align-items: center;
}
/* ENERGY ORB */
.energy-orb-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.energy-orb {
width: 86px;
height: 86px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #7dd3fc 0%, #0284c7 50%, #0f172a 100%);
border: 3px solid #38bdf8;
box-shadow: 0 0 25px rgba(56, 189, 248, 0.5), inset 0 0 15px rgba(255,255,255,0.4);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
animation: orbPulse 3s infinite alternate ease-in-out;
}
@keyframes orbPulse {
0% { box-shadow: 0 0 15px rgba(56, 189, 248, 0.4); }
100% { box-shadow: 0 0 30px rgba(56, 189, 248, 0.8), 0 0 50px rgba(56, 189, 248, 0.2); }
}
.energy-value {
font-size: 32px;
font-weight: 900;
color: #fff;
line-height: 1;
text-shadow: 0 2px 8px #000;
}
.energy-max {
font-size: 11px;
color: #bae6fd;
letter-spacing: 1px;
}
/* ACTIONS MENU IN RIGHT COLUMN */
.side-actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.shop-card-preview {
background: rgba(22, 17, 33, 0.9);
border: 1px dashed var(--gold-glow);
border-radius: 6px;
padding: 10px 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
}
.shop-card-preview:hover {
background: rgba(45, 33, 20, 0.8);
border-style: solid;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(212, 175, 55, 0.3);
}
.shop-preview-title {
font-size: 12px;
font-weight: 700;
color: var(--gold-bright);
margin-bottom: 4px;
}
.shop-preview-sub {
font-size: 10px;
font-family: var(--font-body);
color: #a8a29e;
}
/* BOTTOM CONTROL BAR */
#bottom-bar {
grid-column: 1 / -1;
height: 60px;
background: linear-gradient(0deg, rgba(14, 10, 22, 0.95), rgba(20, 16, 30, 0.9));
border-top: 1.5px solid var(--metal-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
}
.turn-status-indicator {
font-size: 14px;
font-weight: 700;
letter-spacing: 1.5px;
color: #cbd5e1;
display: flex;
align-items: center;
gap: 10px;
}
.turn-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 10px #22c55e;
}
.turn-dot.enemy {
background: #ef4444;
box-shadow: 0 0 10px #ef4444;
}
/* SHOP MODAL */
#shop-modal {
position: absolute;
inset: 0;
background: rgba(4, 3, 8, 0.88);
backdrop-filter: blur(6px);
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
#shop-modal.active {
opacity: 1;
pointer-events: auto;
}
.shop-panel {
width: 900px;
max-height: 90%;
background: linear-gradient(180deg, #1f182c 0%, #100b19 100%);
border: 2px solid var(--gold-glow);
border-radius: 10px;
padding: 24px;
box-shadow: 0 0 40px rgba(0,0,0,0.9), 0 0 20px rgba(212, 175, 55, 0.3);
display: flex;
flex-direction: column;
gap: 16px;
}
.shop-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1.5px solid rgba(212, 175, 55, 0.4);
padding-bottom: 12px;
}
.shop-title {
font-size: 24px;
font-weight: 900;
color: var(--gold-bright);
letter-spacing: 2px;
}
.shop-tabs {
display: flex;
gap: 8px;
}
.shop-tab-btn {
padding: 6px 14px;
background: rgba(255,255,255,0.05);
border: 1px solid var(--metal-border);
color: #cbd5e1;
font-size: 11px;
font-family: var(--font-gothic);
border-radius: 4px;
cursor: pointer;
transition: 0.2s;
}
.shop-tab-btn.active, .shop-tab-btn:hover {
background: rgba(212, 175, 55, 0.2);
border-color: var(--gold-glow);
color: #fff;
}
.shop-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
overflow-y: auto;
max-height: 420px;
padding-right: 6px;
}
.shop-grid::-webkit-scrollbar {
width: 6px;
}
.shop-grid::-webkit-scrollbar-thumb {
background: var(--gold-glow);
border-radius: 3px;
}
.shop-item-card {
background: rgba(27, 21, 38, 0.9);
border: 1.5px solid var(--metal-border);
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
transition: all 0.2s;
}
.shop-item-card:hover {
border-color: var(--gold-bright);
transform: translateY(-2px);
}
.shop-item-card.maxed {
opacity: 0.5;
filter: grayscale(0.8);
}
.shop-item-info {
display: flex;
gap: 10px;
}
.shop-buy-row {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid rgba(255,255,255,0.08);
padding-top: 8px;
}
.shop-owned-count {
font-size: 11px;
color: #94a3b8;
}
/* DECK VIEWER MODAL */
#deck-modal {
position: absolute;
inset: 0;
background: rgba(4, 3, 8, 0.9);
backdrop-filter: blur(6px);
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
#deck-modal.active {
opacity: 1;
pointer-events: auto;
}
.deck-panel {
width: 900px;
max-height: 85%;
background: linear-gradient(180deg, #1f182c 0%, #100b19 100%);
border: 2px solid #6366f1;
border-radius: 10px;
padding: 24px;
box-shadow: 0 0 40px rgba(0,0,0,0.9);
display: flex;
flex-direction: column;
gap: 16px;
}
/* WIN & GAME OVER SCREENS */
#screen-victory, #screen-gameover {
justify-content: center;
align-items: center;
text-align: center;
padding: 40px;
}
#screen-victory {
background: radial-gradient(circle, rgba(28, 48, 30, 0.96), rgba(4, 12, 6, 0.98));
}
#screen-gameover {
background: radial-gradient(circle, rgba(48, 12, 12, 0.96), rgba(12, 4, 4, 0.98));
}
.end-title {
font-size: 56px;
font-weight: 900;
letter-spacing: 4px;
margin-bottom: 12px;
}
.end-title.vic {
color: #86efac;
text-shadow: 0 0 30px #22c55e, 0 0 60px #16a34a;
}
.end-title.loss {
color: #fca5a5;
text-shadow: 0 0 30px #ef4444, 0 0 60px #991b1b;
}
.end-subtitle {
font-family: var(--font-body);
font-size: 20px;
color: #e2e8f0;
max-width: 600px;
margin-bottom: 30px;
line-height: 1.5;
}
/* UTILITY HELPERS */
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
</head>
<body>
<div id="world-bg">
<div class="fog-layer"></div>
</div>
<div id="game-container">
<!-- TOP BAR HUD -->
<header id="top-bar">
<div class="hud-section">
<div class="hud-item hud-gold">
<svg class="hud-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="#facc15" stroke="#713f12" stroke-width="2"/><text x="12" y="16" font-size="12" font-weight="bold" fill="#713f12" text-anchor="middle">G</text></svg>
<span id="hud-gold-text">0 Gold</span>
</div>
<div class="hud-item hud-floor">
<span>Floor <strong id="hud-floor-num">1</strong> / 3</span>
</div>
<div class="hud-item hud-room">
<span>Room <strong id="hud-room-num">1</strong> / 4</span>
</div>
</div>
<div class="hud-section">
<button class="hud-deck-view" onclick="game.toggleDeckModal(true)">View Deck (<span id="hud-deck-count">12</span>)</button>
<button class="hud-deck-view" onclick="game.toggleShopModal(true)">Gold Shop</button>
<button class="hud-deck-view" onclick="game.abandonRun()">Main Menu</button>
</div>
</header>
<!-- COMBAT & GAMEPLAY AREA -->
<main id="combat-layout">
<!-- LEFT: HERO STATUS & HAND -->
<section id="player-column">
<div class="hero-compact-card">
<div class="hero-avatar" id="hero-portrait-slot"></div>
<div class="hero-vitals">
<div class="hero-name-row">
<span id="hero-name-label">The Bastion</span>
<span id="hero-def-badge" style="color:#60a5fa; font-weight:bold;">Def: 0</span>
</div>
<div class="bar-wrap">
<div id="hero-hp-bar" class="bar-fill hp" style="width: 100%;"></div>
</div>
<div class="bar-text">
<span>HP: <strong id="hero-hp-val">35</strong>/<span id="hero-maxhp-val">35</span></span>
<span>Hand Limit: <strong id="hero-handlim-val">4</strong></span>
</div>
<div class="status-pill-list" id="hero-status-list"></div>
</div>
</div>
<div class="hand-header">
<span>HAND CARDS (<span id="hand-count-text">0</span>)</span>
<span style="color:#60a5fa; font-size:10px;">Click card to play</span>
</div>
<div id="hand-container">
<!-- Cards rendered here dynamically -->
</div>
</section>
<!-- CENTER: ARENA WITH HERO & ENEMY ANIMATIONS & COMBAT LOG -->
<section id="center-column">
<div id="combat-arena">
<!-- HERO SPRITE STAGE -->
<div class="entity-stage hero-stage">
<div class="hero-sprite-box" id="hero-sprite-container">
<!-- Hero SVG dynamically drawn based on class & state -->
</div>
</div>
<!-- VS DIVIDER / INTENT -->
<div style="display:flex; flex-direction:column; align-items:center; opacity:0.6;">
<span style="font-size: 20px; font-weight:900; color:#d4af37;">VS</span>
</div>
<!-- ENEMY SPRITE STAGE -->
<div class="entity-stage enemy-stage">
<div class="enemy-intent-banner" id="enemy-intent-box">
<span>⚔️ Intent: Attack 3 DMG</span>
</div>
<div class="enemy-sprite-box" id="enemy-sprite-container">
<!-- Enemy SVG dynamically rendered -->
</div>
<div class="enemy-hud">
<div class="enemy-name-row">
<span class="enemy-name" id="enemy-name-text">Void Goblin</span>
<span id="enemy-boss-badge" class="boss-tag" style="display:none;">BOSS</span>
</div>
<div class="bar-wrap" style="margin-bottom: 4px;">
<div id="enemy-hp-bar" class="bar-fill hp" style="width: 100%;"></div>
</div>
<div class="bar-text">
<span>HP: <strong id="enemy-hp-val">20</strong>/<span id="enemy-maxhp-val">20</span></span>
<span style="color:#60a5fa;">Def: <strong id="enemy-def-val">0</strong></span>
</div>
<div class="status-pill-list" id="enemy-status-list" style="margin-top:4px;"></div>
</div>
</div>
</div>
<!-- COMBAT MESSAGE LOG -->
<div id="combat-log-box">
<div class="log-entry log-system">Welcome to Shadows of Aethelgard. Cleanse the Void.</div>
</div>
</section>
<!-- RIGHT: ENERGY ORB & SHOP SHORTCUT -->
<section id="right-column">
<div class="energy-orb-container">
<div class="energy-orb">
<div class="energy-value" id="energy-val">3</div>
<div class="energy-max">/ <span id="energy-max-val">5</span> MAX</div>
</div>
<span style="font-size:11px; color:#93c5fd; letter-spacing:1px;">+1 Energy / Turn</span>
</div>
<div class="side-actions">
<div class="shop-card-preview" onclick="game.toggleShopModal(true)">
<div class="shop-preview-title">🛒 REINFORCEMENTS</div>
<div class="shop-preview-sub">Buy new cards for Gold</div>
</div>
<div style="background: rgba(20,15,30,0.8); border:1px solid var(--metal-border); border-radius:6px; padding:10px; text-align:center;">
<div style="font-size:11px; color:#a79cb8; margin-bottom:4px;">Floor Boss Objective</div>
<div id="floor-boss-brief" style="font-size:11px; color:#fde047; font-weight:bold;">Floor 1: The Gatekeeper</div>
</div>
</div>
</section>
<!-- BOTTOM CONTROL BAR -->
<footer id="bottom-bar">
<div class="turn-status-indicator">
<div class="turn-dot" id="turn-dot"></div>
<span id="turn-status-label">PLAYER TURN</span>
</div>
<div style="display:flex; gap:16px;">
<button id="btn-end-turn" class="btn-gothic btn-gold" onclick="game.endTurn()">End Turn</button>
</div>
</footer>
</main>
<!-- SCREEN: MAIN MENU / CLASS SELECT -->
<div id="screen-menu" class="screen">
<h1 class="game-title">SHADOWS OF AETHELGARD</h1>
<p class="game-subtitle">DESCENT INTO THE CORRUPTED REALM</p>
<div class="class-select-container">
<!-- BASTION -->
<div class="class-card selected" id="card-bastion" onclick="game.selectClass('Bastion')">
<div class="class-icon" id="bastion-icon-svg"></div>
<div class="class-name">THE BASTION</div>
<div class="class-tagline">Immovable Object</div>
<div class="class-stats">
<div>HP: <strong>35</strong> | Energy: <strong>3</strong></div>
<div>Style: High Defense, High Block, AoE</div>
</div>
</div>
<!-- WITHER -->
<div class="class-card" id="card-wither" onclick="game.selectClass('Wither')">
<div class="class-icon" id="wither-icon-svg"></div>
<div class="class-name">THE WITHER</div>
<div class="class-tagline">Rot and Burn</div>
<div class="class-stats">
<div>HP: <strong>30</strong> | Energy: <strong>5</strong></div>
<div>Style: Status Burn, Poison & Debuffs</div>
</div>
</div>
<!-- SHROUD -->
<div class="class-card" id="card-shroud" onclick="game.selectClass('Shroud')">
<div class="class-icon" id="shroud-icon-svg"></div>
<div class="class-name">THE SHROUD</div>
<div class="class-tagline">Shadow and Speed</div>
<div class="class-stats">
<div>HP: <strong>30</strong> | Energy: <strong>5</strong></div>
<div>Style: High Speed, Evasion, Assassinate</div>
</div>
</div>
</div>
<button class="btn-gothic btn-gold" style="font-size:18px; padding:14px 48px;" onclick="game.startRun()">DESCEND INTO THE VOID</button>
</div>
<!-- SCREEN: VICTORY -->
<div id="screen-victory" class="screen hidden">
<h1 class="end-title vic">THE VOID HAS FALLEN</h1>
<p class="end-subtitle">You have conquered the Void Lord on Floor 3 and reclaimed Aethelgard from eternal darkness. Your legacy is etched into the stone.</p>
<button class="btn-gothic btn-gold" onclick="game.abandonRun()">RETURN TO SANCTUARY</button>
</div>
<!-- SCREEN: GAME OVER -->
<div id="screen-gameover" class="screen hidden">
<h1 class="end-title loss">CONSUMED BY SHADOWS</h1>
<p class="end-subtitle">Your resolve was shattered by the horrors of the abyss. The Void claims another hero of Aethelgard.</p>
<button class="btn-gothic btn-gold" onclick="game.abandonRun()">TRY AGAIN</button>
</div>
<!-- MODAL: GOLD CARD SHOP -->
<div id="shop-modal">
<div class="shop-panel">
<div class="shop-header">
<div>
<div class="shop-title">GOLD CARD SHOP</div>
<div style="font-size:12px; color:#cbd5e1; font-family:var(--font-body);">Upgrade and expand your battle deck. Cost scales with owned copies.</div>
</div>
<div style="display:flex; align-items:center; gap:16px;">
<div class="hud-item hud-gold" style="font-size:18px;">
<span>Current Gold: <strong id="shop-gold-display">0</strong></span>
</div>
<button class="btn-gothic" style="padding:6px 16px;" onclick="game.toggleShopModal(false)">Close</button>
</div>
</div>
<div class="shop-tabs">
<button class="shop-tab-btn active" id="tab-all" onclick="game.filterShop('ALL')">All Classes</button>
<button class="shop-tab-btn" id="tab-bastion" onclick="game.filterShop('BA')">Bastion Cards</button>
<button class="shop-tab-btn" id="tab-wither" onclick="game.filterShop('WI')">Wither Cards</button>
<button class="shop-tab-btn" id="tab-shroud" onclick="game.filterShop('SH')">Shroud Cards</button>
</div>
<div class="shop-grid" id="shop-items-grid">
<!-- Shop items dynamically rendered -->
</div>
</div>
</div>
<!-- MODAL: DECK VIEWER -->
<div id="deck-modal">
<div class="deck-panel">
<div class="shop-header">
<div>
<div class="shop-title" style="color:#818cf8;">HERO DECK INSPECTION</div>
<div style="font-size:12px; color:#cbd5e1; font-family:var(--font-body);">Showing all cards in your current deck pool (<span id="deck-view-count">0</span> cards).</div>
</div>
<button class="btn-gothic" style="padding:6px 16px;" onclick="game.toggleDeckModal(false)">Close</button>
</div>
<div class="shop-grid" id="deck-items-grid" style="max-height:460px;">
<!-- Current deck cards rendered here -->
</div>
</div>
</div>
</div>
<script>
/* =========================================================================
SHADOWS OF AETHELGARD - FULL ENGINE & GAMEPLAY LOGIC
========================================================================= */
// --- 1. SVG GENERATORS FOR CARD FRAMES & CARD BACKS ---
const SVG_ASSETS = {
getCardFrameSVG(deckType, cardType) {
// Unique Card Frame per Deck Type (Bastion: Fortified Stone, Wither: Fiery Thorn, Shroud: Shadow Blade)
let primaryColor = "#64748b";
let accentColor = "#94a3b8";
let frameTitle = "BASTION";
let runePath = "";
if (deckType === "BA") { // Bastion
primaryColor = "#44403c";
accentColor = "#d97706";
frameTitle = "BASTION";
runePath = `<polygon points="20,8 32,16 32,32 20,40 8,32 8,16" fill="#78350f" stroke="#f59e0b" stroke-width="1.5"/>
<path d="M14 24 L26 24 M20 14 L20 34" stroke="#fef08a" stroke-width="2"/>`;
} else if (deckType === "WI") { // Wither
primaryColor = "#450a0a";
accentColor = "#ea580c";
frameTitle = "WITHER";
runePath = `<circle cx="20" cy="24" r="14" fill="#7c2d12" stroke="#f97316" stroke-width="1.5"/>
<path d="M14 28 Q20 12 26 28 Q20 34 14 28 Z" fill="#ef4444"/>`;
} else if (deckType === "SH") { // Shroud
primaryColor = "#2e1065";
accentColor = "#a855f7";
frameTitle = "SHROUD";
runePath = `<polygon points="20,10 32,24 20,38 8,24" fill="#4c1d95" stroke="#c084fc" stroke-width="1.5"/>
<circle cx="20" cy="24" r="4" fill="#38bdf8"/>`;
}
// Return Frame Overlay SVG with clear Art Placeholder
return `
<svg width="100%" height="100%" viewBox="0 0 140 190" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="frameGrad-${deckType}-${Math.floor(Math.random()*10000)}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${primaryColor}" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#0a0812" stop-opacity="0.98"/>
</linearGradient>
</defs>
<!-- Border & Background -->
<rect x="2" y="2" width="136" height="186" rx="8" fill="${primaryColor}" stroke="${accentColor}" stroke-width="2.5"/>
<rect x="6" y="6" width="128" height="178" rx="6" fill="#120e1a" stroke="#2a2438" stroke-width="1"/>
<!-- Header Ribbon -->
<rect x="12" y="10" width="116" height="18" rx="3" fill="#1c1626" stroke="${accentColor}" stroke-width="1"/>
<text x="70" y="23" font-size="9" font-family="'Cinzel', serif" font-weight="900" fill="${accentColor}" text-anchor="middle" letter-spacing="1.5">${frameTitle}</text>
<!-- Main Card Image Placeholder Box -->
<rect x="14" y="34" width="112" height="74" rx="4" fill="#1e1b29" stroke="${accentColor}" stroke-width="1.5" stroke-dasharray="4,3"/>
<text x="70" y="68" font-size="10" font-family="'Cinzel', serif" fill="${accentColor}" text-anchor="middle" font-weight="bold" opacity="0.9">✦ ART SLOT ✦</text>
<text x="70" y="82" font-size="8" font-family="sans-serif" fill="#94a3b8" text-anchor="middle" opacity="0.7">[Card Image Placeholder]</text>
<!-- Bottom Rune & Details Box -->
<g transform="translate(50, 120)">
${runePath}
</g>
<!-- Corner Ornaments -->
<circle cx="10" cy="10" r="3" fill="${accentColor}"/>
<circle cx="130" cy="10" r="3" fill="${accentColor}"/>
<circle cx="10" cy="180" r="3" fill="${accentColor}"/>
<circle cx="130" cy="180" r="3" fill="${accentColor}"/>
</svg>
`;
},
getCardBackSVG(deckType) {
let mainColor = "#334155";
let glowColor = "#94a3b8";
let crest = "";
if (deckType === "BA") {
mainColor = "#44403c";
glowColor = "#d97706";
crest = `
<polygon points="40,25 60,25 70,55 50,85 30,55" fill="#292524" stroke="#d97706" stroke-width="2"/>
<line x1="50" y1="28" x2="50" y2="80" stroke="#d97706" stroke-width="2"/>
<line x1="36" y1="45" x2="64" y2="45" stroke="#d97706" stroke-width="2"/>
`;
} else if (deckType === "WI") {
mainColor = "#450a0a";
glowColor = "#ef4444";
crest = `
<circle cx="50" cy="55" r="24" fill="#1c0606" stroke="#ef4444" stroke-width="2"/>
<path d="M50 35 C58 45 62 55 50 72 C38 55 42 45 50 35 Z" fill="#b91c1c" stroke="#fca5a5" stroke-width="1"/>
`;
} else if (deckType === "SH") {
mainColor = "#2e1065";
glowColor = "#a855f7";
crest = `
<polygon points="50,28 72,55 50,82 28,55" fill="#1e0b36" stroke="#c084fc" stroke-width="2"/>
<polygon points="50,38 62,55 50,72 38,55" fill="none" stroke="#c084fc" stroke-width="1"/>
`;
}
return `
<svg width="100%" height="100%" viewBox="0 0 100 140" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="140" rx="6" fill="${mainColor}" stroke="${glowColor}" stroke-width="2"/>
<rect x="5" y="5" width="90" height="130" rx="4" fill="none" stroke="#110d1c" stroke-width="2"/>
<line x1="5" y1="5" x2="95" y2="135" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
<line x1="95" y1="5" x2="5" y2="135" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
<g id="crest">${crest}</g>
</svg>
`;
},
getCardIllustrationSVG(card) {
// Generate inline thematic card illustration placeholder
const cardId = card.id || card.ID || "";
const cardType = card.type || card.Type || "Attack";
const isAtk = cardType === "Attack";
const isDef = cardType === "Defense";
let artColor = isAtk ? "#ef4444" : (isDef ? "#38bdf8" : "#c084fc");
let icon = "";
if (cardId.includes("A01") || cardId.includes("A03")) {
icon = `<path d="M15 50 L45 20 L55 30 L25 60 Z" fill="${artColor}" stroke="#fff" stroke-width="1.5"/><line x1="12" y1="53" x2="18" y2="59" stroke="#fff" stroke-width="2"/>`;
} else if (cardId.includes("A02") || cardId.includes("A04")) {
icon = `<circle cx="35" cy="35" r="18" fill="none" stroke="${artColor}" stroke-width="3"/><path d="M25 35 L45 35 M35 25 L35 45" stroke="#fff" stroke-width="2"/>`;
} else if (isDef) {
icon = `<path d="M20 20 L50 20 L50 42 Q35 56 20 42 Z" fill="${artColor}" opacity="0.8" stroke="#fff" stroke-width="1.5"/>`;
} else {
icon = `<polygon points="35,15 52,45 18,45" fill="${artColor}" opacity="0.8" stroke="#fff" stroke-width="1.5"/><circle cx="35" cy="35" r="6" fill="#fff"/>`;
}
return `
<svg width="100%" height="100%" viewBox="0 0 70 70" xmlns="http://www.w3.org/2000/svg">
<rect width="70" height="70" fill="#0d0a14"/>
<g transform="translate(0, 0)">${icon}</g>
</svg>
`;
},
getClassAvatarSVG(className) {
if (className === "Bastion") {
return `
<svg viewBox="0 0 100 100" width="100%" height="100%">
<rect width="100" height="100" fill="#1c1917"/>
<polygon points="50,15 80,30 80,65 50,85 20,65 20,30" fill="#44403c" stroke="#d97706" stroke-width="3"/>
<rect x="42" y="32" width="16" height="36" fill="#d97706"/>
<rect x="32" y="42" width="36" height="16" fill="#d97706"/>
</svg>
`;
} else if (className === "Wither") {
return `
<svg viewBox="0 0 100 100" width="100%" height="100%">
<rect width="100" height="100" fill="#180404"/>
<circle cx="50" cy="45" r="28" fill="#450a0a" stroke="#ef4444" stroke-width="3"/>
<path d="M35 55 Q50 25 65 55 Q50 75 35 55 Z" fill="#f97316"/>
<circle cx="42" cy="40" r="4" fill="#fef08a"/>
<circle cx="58" cy="40" r="4" fill="#fef08a"/>
</svg>
`;
} else { // Shroud
return `
<svg viewBox="0 0 100 100" width="100%" height="100%">
<rect width="100" height="100" fill="#0f0728"/>
<path d="M50 15 Q75 40 75 75 L25 75 Q25 40 50 15 Z" fill="#2e1065" stroke="#a855f7" stroke-width="3"/>
<ellipse cx="40" cy="50" rx="6" ry="3" fill="#c084fc"/>
<ellipse cx="60" cy="50" rx="6" ry="3" fill="#c084fc"/>
</svg>
`;
}
},
getHeroSpriteSVG(className, state = "idle") {
// High polish character SVG representations with 3 visual classes
if (className === "Bastion") {
return `
<svg viewBox="0 0 160 200" width="100%" height="100%">
<defs>
<radialGradient id="bastionAura" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#d97706" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#000" stop-opacity="0"/>
</radialGradient>
</defs>
<ellipse cx="80" cy="185" rx="55" ry="12" fill="#000" opacity="0.6"/>
<!-- Greatshield -->
<path d="M25 60 L65 50 L60 170 L25 150 Z" fill="#44403c" stroke="#d97706" stroke-width="3"/>
<line x1="45" y1="55" x2="42" y2="160" stroke="#f59e0b" stroke-width="2"/>
<!-- Armor Body -->
<polygon points="65,65 115,65 125,140 55,140" fill="#292524" stroke="#78716c" stroke-width="2"/>
<!-- Helmet -->
<rect x="75" y="30" width="30" height="35" rx="5" fill="#57534e" stroke="#d97706" stroke-width="2"/>
<line x1="80" y1="45" x2="100" y2="45" stroke="#fef08a" stroke-width="3"/>
<!-- Heavy Warhammer -->
<line x1="120" y1="120" x2="145" y2="40" stroke="#a8a29e" stroke-width="4"/>
<rect x="135" y="30" width="22" height="20" rx="2" fill="#d97706" stroke="#fff" stroke-width="1.5"/>
</svg>
`;
} else if (className === "Wither") {
return `
<svg viewBox="0 0 160 200" width="100%" height="100%">
<ellipse cx="80" cy="185" rx="45" ry="10" fill="#000" opacity="0.6"/>
<!-- Dark Cultist Robes -->
<path d="M50 65 L110 65 L135 180 L25 180 Z" fill="#450a0a" stroke="#ef4444" stroke-width="2"/>
<!-- Hood -->
<path d="M80 20 Q110 40 105 75 Q80 85 55 75 Q50 40 80 20 Z" fill="#1c0606" stroke="#f97316" stroke-width="2"/>
<circle cx="70" cy="52" r="3" fill="#fde047"/>
<circle cx="90" cy="52" r="3" fill="#fde047"/>
<!-- Flame Staff -->
<line x1="40" y1="180" x2="35" y2="25" stroke="#78350f" stroke-width="4"/>
<circle cx="35" cy="22" r="14" fill="#f97316" opacity="0.8"/>
<circle cx="35" cy="22" r="7" fill="#fef08a"/>
</svg>
`;
} else { // Shroud
return `
<svg viewBox="0 0 160 200" width="100%" height="100%">
<ellipse cx="80" cy="185" rx="45" ry="10" fill="#000" opacity="0.6"/>
<!-- Cloak & Daggers -->
<path d="M60 55 L100 55 L125 175 L35 175 Z" fill="#2e1065" stroke="#a855f7" stroke-width="2"/>
<!-- Mask -->
<circle cx="80" cy="45" r="22" fill="#1e1b4b" stroke="#c084fc" stroke-width="2"/>
<ellipse cx="73" cy="45" rx="5" ry="2" fill="#38bdf8"/>
<ellipse cx="87" cy="45" rx="5" ry="2" fill="#38bdf8"/>
<!-- Dual Void Daggers -->
<polygon points="120,90 145,55 140,50 115,85" fill="#38bdf8" stroke="#fff" stroke-width="1.5"/>
<polygon points="40,90 15,55 20,50 45,85" fill="#38bdf8" stroke="#fff" stroke-width="1.5"/>
</svg>
`;
}
},
getEnemySpriteSVG(enemy) {
if (enemy.isBoss) {
if (enemy.bossId === 1) { // The Gatekeeper
return `
<svg viewBox="0 0 200 220" width="100%" height="100%">
<ellipse cx="100" cy="205" rx="70" ry="15" fill="#000" opacity="0.7"/>
<!-- Huge Stone Golem Gatekeeper -->
<polygon points="40,50 160,50 180,180 20,180" fill="#1e293b" stroke="#64748b" stroke-width="4"/>
<rect x="60" y="30" width="80" height="40" rx="4" fill="#0f172a" stroke="#94a3b8" stroke-width="3"/>
<!-- Runes -->
<circle cx="80" cy="50" r="6" fill="#38bdf8"/>
<circle cx="120" cy="50" r="6" fill="#38bdf8"/>
<line x1="100" y1="70" x2="100" y2="150" stroke="#38bdf8" stroke-width="3"/>
<!-- Spikes -->
<polygon points="30,40 45,10 60,40" fill="#475569"/>
<polygon points="140,40 155,10 170,40" fill="#475569"/>
</svg>
`;
} else if (enemy.bossId === 2) { // The Corrupted
return `
<svg viewBox="0 0 200 220" width="100%" height="100%">
<ellipse cx="100" cy="205" rx="70" ry="15" fill="#000" opacity="0.7"/>
<!-- Tendril Abomination -->
<circle cx="100" cy="110" r="65" fill="#4a044e" stroke="#c026d3" stroke-width="4"/>
<circle cx="80" cy="90" r="14" fill="#facc15" stroke="#000" stroke-width="2"/>
<circle cx="80" cy="90" r="5" fill="#000"/>
<circle cx="120" cy="95" r="10" fill="#facc15" stroke="#000" stroke-width="2"/>
<circle cx="120" cy="95" r="4" fill="#000"/>
<circle cx="100" cy="140" r="8" fill="#facc15"/>
<!-- Teeth -->
<polygon points="75,130 85,150 95,130 105,150 115,130 125,150" fill="#fef08a"/>
</svg>
`;
} else { // The Void Lord
return `
<svg viewBox="0 0 200 220" width="100%" height="100%">
<ellipse cx="100" cy="205" rx="80" ry="15" fill="#000" opacity="0.8"/>
<!-- Ultimate Void Entity -->
<path d="M50 40 Q100 10 150 40 Q180 120 150 190 Q100 210 50 190 Q20 120 50 40 Z" fill="#090514" stroke="#a855f7" stroke-width="4"/>
<!-- Crown of Void Horns -->
<polygon points="70,40 50,5 90,30" fill="#7e22ce"/>
<polygon points="130,40 150,5 110,30" fill="#7e22ce"/>
<polygon points="100,30 100,-5 105,30" fill="#c084fc"/>
<!-- Void Eye -->
<ellipse cx="100" cy="100" rx="30" ry="15" fill="#6b21a8" stroke="#f43f5e" stroke-width="3"/>
<circle cx="100" cy="100" r="9" fill="#000"/>
</svg>
`;
}
}
// Standard Enemies per Floor
if (enemy.floor === 1) { // Void Goblin / Rat
return `
<svg viewBox="0 0 160 180" width="100%" height="100%">
<ellipse cx="80" cy="165" rx="45" ry="10" fill="#000" opacity="0.6"/>
<circle cx="80" cy="90" r="38" fill="#14532d" stroke="#4ade80" stroke-width="2"/>
<!-- Pointy Ears -->
<polygon points="45,80 15,50 55,65" fill="#166534" stroke="#4ade80" stroke-width="1.5"/>
<polygon points="115,80 145,50 105,65" fill="#166534" stroke="#4ade80" stroke-width="1.5"/>
<!-- Red glowing eyes -->
<circle cx="68" cy="85" r="5" fill="#ef4444"/>
<circle cx="92" cy="85" r="5" fill="#ef4444"/>
<!-- Dagger -->
<line x1="120" y1="130" x2="135" y2="90" stroke="#94a3b8" stroke-width="3"/>
</svg>
`;
} else if (enemy.floor === 2) { // Vampire Knight / Ogre
return `
<svg viewBox="0 0 180 200" width="100%" height="100%">
<ellipse cx="90" cy="185" rx="55" ry="12" fill="#000" opacity="0.6"/>
<polygon points="50,60 130,60 145,170 35,170" fill="#312e81" stroke="#818cf8" stroke-width="3"/>
<rect x="70" y="30" width="40" height="35" rx="4" fill="#1e1b4b" stroke="#e11d48" stroke-width="2"/>
<line x1="78" y1="45" x2="102" y2="45" stroke="#f43f5e" stroke-width="3"/>
<!-- Greatsword -->
<line x1="140" y1="170" x2="155" y2="25" stroke="#cbd5e1" stroke-width="4"/>
<rect x="145" y="45" width="20" height="6" fill="#f59e0b"/>
</svg>
`;
} else { // Void Warrior / Cultist
return `
<svg viewBox="0 0 180 200" width="100%" height="100%">
<ellipse cx="90" cy="185" rx="60" ry="12" fill="#000" opacity="0.7"/>
<path d="M45 60 L135 60 L155 175 L25 175 Z" fill="#1e1035" stroke="#c084fc" stroke-width="3"/>
<circle cx="90" cy="45" r="22" fill="#0f051d" stroke="#a855f7" stroke-width="2"/>
<polygon points="90,20 100,5 105,25" fill="#c084fc"/>
<circle cx="82" cy="45" r="4" fill="#a855f7"/>
<circle cx="98" cy="45" r="4" fill="#a855f7"/>
</svg>
`;
}
}
};
// --- 2. CARD DATABASE (THE 36 CORE CARDS) ---
const CARD_DB = [
// --- CLASS 1: THE BASTION (BA) ---
{ id: "BA-A01", deckType: "BA", name: "Slam", type: "Attack", cost: 2, atk: 3, def: 0, rarity: "Common", desc: "Deals 3 damage. If you have 30 HP, draw 1 card." },
{ id: "BA-A02", deckType: "BA", name: "Earthquake", type: "Attack", cost: 3, atk: 4, def: 0, rarity: "Common", desc: "Deals 4 damage to all enemies." },
{ id: "BA-A03", deckType: "BA", name: "Stone Fist", type: "Attack", cost: 1, atk: 2, def: 0, rarity: "Common", desc: "Deals 2 damage." },
{ id: "BA-A04", deckType: "BA", name: "Boulder", type: "Attack", cost: 4, atk: 6, def: 0, rarity: "Common", desc: "Deals 6 damage. Heavy impact." },
{ id: "BA-D01", deckType: "BA", name: "Shield Wall", type: "Defense", cost: 0, atk: 0, def: 4, rarity: "Common", desc: "Gain 4 Defense for this turn." },
{ id: "BA-D02", deckType: "BA", name: "Iron Skin", type: "Defense", cost: 2, atk: 0, def: 5, rarity: "Common", desc: "Gain 5 Defense permanently. Discard 1 card." },
{ id: "BA-D03", deckType: "BA", name: "Stand Firm", type: "Defense", cost: 1, atk: 0, def: 2, rarity: "Common", desc: "Gain 2 Defense. Blocks 1 incoming damage." },
{ id: "BA-D04", deckType: "BA", name: "Taunt", type: "Defense", cost: 2, atk: 0, def: 3, rarity: "Common", desc: "Gain 3 Defense. Stun 1 enemy for 1 turn." },
{ id: "BA-U01", deckType: "BA", name: "Search", type: "Other", cost: 2, atk: 0, def: 0, rarity: "Common", desc: "Draw 1 card. Discard hand if > 5 cards." },
{ id: "BA-U02", deckType: "BA", name: "Repair", type: "Other", cost: 3, atk: 0, def: 0, rarity: "Common", desc: "Heal 6 HP." },
{ id: "BA-U03", deckType: "BA", name: "Rally", type: "Other", cost: 1, atk: 0, def: 0, rarity: "Common", desc: "Draw 1 card. Gain 1 Energy." },
{ id: "BA-U04", deckType: "BA", name: "Loot", type: "Other", cost: 2, atk: 0, def: 0, rarity: "Common", desc: "Gain 5 Gold." },
// --- CLASS 2: THE WITHER (WI) ---
{ id: "WI-A01", deckType: "WI", name: "Fireball", type: "Attack", cost: 2, atk: 3, def: 0, rarity: "Common", desc: "Deals 3 damage. Apply 1 Burn to target." },
{ id: "WI-A02", deckType: "WI", name: "Inferno", type: "Attack", cost: 3, atk: 4, def: 0, rarity: "Common", desc: "Deals 4 damage. Applies 3 Burn." },
{ id: "WI-A03", deckType: "WI", name: "Ember", type: "Attack", cost: 1, atk: 2, def: 0, rarity: "Common", desc: "Deals 2 damage. Deals 1 damage every turn (Burn)." },
{ id: "WI-A04", deckType: "WI", name: "Pyre", type: "Attack", cost: 4, atk: 5, def: 0, rarity: "Common", desc: "Deals 5 damage. Apply 1 Burn next turn." },
{ id: "WI-D01", deckType: "WI", name: "Scorch Shield", type: "Defense", cost: 2, atk: 0, def: 3, rarity: "Common", desc: "Gain 3 Defense. Block 1 Burn damage." },
{ id: "WI-D02", deckType: "WI", name: "Heat Resist", type: "Defense", cost: 1, atk: 0, def: 1, rarity: "Common", desc: "Gain 1 Defense. Gain 1 Energy." },
{ id: "WI-D03", deckType: "WI", name: "Smoke", type: "Defense", cost: 2, atk: 0, def: 4, rarity: "Common", desc: "Gain 4 Defense. Block 1 Burn damage." },
{ id: "WI-D04", deckType: "WI", name: "Endure", type: "Defense", cost: 3, atk: 0, def: 2, rarity: "Common", desc: "Gain 2 Defense. Heal 2 HP." },
{ id: "WI-U01", deckType: "WI", name: "Poison", type: "Other", cost: 2, atk: 0, def: 0, rarity: "Common", desc: "Apply 1 Poison to target." },
{ id: "WI-U02", deckType: "WI", name: "Burn", type: "Other", cost: 3, atk: 0, def: 0, rarity: "Common", desc: "Deal 3 damage + 2 Burn to all enemies." },
{ id: "WI-U03", deckType: "WI", name: "Slow", type: "Other", cost: 1, atk: 0, def: 0, rarity: "Common", desc: "Draw 1 card. Target Stunned next turn." },
{ id: "WI-U04", deckType: "WI", name: "Wither", type: "Other", cost: 2, atk: 0, def: 0, rarity: "Common", desc: "Heal 5 HP. Reduce enemy Attack by 1." },
// --- CLASS 3: THE SHROUD (SH) ---
{ id: "SH-A01", deckType: "SH", name: "Backstab", type: "Attack", cost: 2, atk: 4, def: 0, rarity: "Common", desc: "Deals 4 damage (+1 extra damage if enemy has no Armor)." },
{ id: "SH-A02", deckType: "SH", name: "Shadow Bolt", type: "Attack", cost: 3, atk: 5, def: 0, rarity: "Common", desc: "Deals 5 damage. Target cannot block (Pierces)." },
{ id: "SH-A03", deckType: "SH", name: "Silence", type: "Attack", cost: 1, atk: 3, def: 0, rarity: "Common", desc: "Deals 3 damage. Silences enemy abilities." },
{ id: "SH-A04", deckType: "SH", name: "Vengeance", type: "Attack", cost: 3, atk: 6, def: 0, rarity: "Common", desc: "Deals 6 damage. Gain 2 Energy." },
{ id: "SH-D01", deckType: "SH", name: "Blur", type: "Defense", cost: 3, atk: 0, def: 2, rarity: "Common", desc: "Gain 2 Defense. Ignore next attack completely." },
{ id: "SH-D02", deckType: "SH", name: "Step", type: "Defense", cost: 2, atk: 0, def: 3, rarity: "Common", desc: "Gain 3 Defense. Gain 1 Energy." },
{ id: "SH-D03", deckType: "SH", name: "Evasion", type: "Defense", cost: 1, atk: 0, def: 1, rarity: "Common", desc: "Gain 1 Defense. Skip enemy next action." },
{ id: "SH-D04", deckType: "SH", name: "Phantom", type: "Defense", cost: 2, atk: 0, def: 4, rarity: "Common", desc: "Gain 4 Defense. Enter Ghost form for 1 turn." },
{ id: "SH-U01", deckType: "SH", name: "Dash", type: "Other", cost: 1, atk: 0, def: 0, rarity: "Common", desc: "Gain 1 Energy. Skip enemy attack." },
{ id: "SH-U02", deckType: "SH", name: "Teleport", type: "Other", cost: 2, atk: 0, def: 0, rarity: "Common", desc: "Move to back row. Deal 2 direct damage." },
{ id: "SH-U03", deckType: "SH", name: "Hide", type: "Other", cost: 2, atk: 0, def: 0, rarity: "Common", desc: "Gain 1 Defense. Discard 1 card. Draw 2 cards." },
{ id: "SH-U04", deckType: "SH", name: "Assassinate", type: "Other", cost: 3, atk: 8, def: 0, rarity: "Common", desc: "Deal 8 damage. Cannot be blocked this turn." }
];
// --- 3. MAIN GAME STATE CONTROLLER ---
class GameEngine {
constructor() {
this.selectedClass = "Bastion";
this.player = null;
this.enemy = null;
this.currentFloor = 1;
this.currentRoom = 1; // 1-3 normal encounters, 4 is Floor Boss
this.isPlayerTurn = true;
this.isProcessing = false;
this.shopFilter = "ALL";
this.initMenuIcons();
}
initMenuIcons() {
document.getElementById("bastion-icon-svg").innerHTML = SVG_ASSETS.getClassAvatarSVG("Bastion");
document.getElementById("wither-icon-svg").innerHTML = SVG_ASSETS.getClassAvatarSVG("Wither");
document.getElementById("shroud-icon-svg").innerHTML = SVG_ASSETS.getClassAvatarSVG("Shroud");
}
selectClass(className) {
this.selectedClass = className;
document.querySelectorAll(".class-card").forEach(c => c.classList.remove("selected"));
document.getElementById(`card-${className.toLowerCase()}`).classList.add("selected");
}
startRun() {
// Class starter stats per GDD
let hp = 30;
let maxEnergy = 5;
let startEnergy = 3;
let handSize = 4;
let prefix = "BA";
if (this.selectedClass === "Bastion") {
hp = 35;
startEnergy = 3;
prefix = "BA";
} else if (this.selectedClass === "Wither") {
hp = 30;
startEnergy = 5;
prefix = "WI";
} else if (this.selectedClass === "Shroud") {
hp = 30;
startEnergy = 5;
prefix = "SH";
}
// Build Starter Deck (12 unique class cards)
const starterCards = CARD_DB.filter(c => c.deckType === prefix);
const deck = starterCards.map(c => ({ ...c }));
this.player = {
className: this.selectedClass,
hp: hp,
maxHp: hp,
energy: startEnergy,
maxEnergy: maxEnergy,
defense: 0,
permanentDefense: 0,
handSize: handSize,
gold: 0,
deck: deck,
hand: [],
discardPile: [],
inventory: {}, // Map of cardId -> count owned
status: {
burn: 0,
poison: 0,
stun: false,
silence: false,
ghost: false,
ignoreNextAttack: false
}
};
// Initialize inventory count
starterCards.forEach(c => {
this.player.inventory[c.id] = 1;
});
this.currentFloor = 1;
this.currentRoom = 1;
// Switch screen
document.getElementById("screen-menu").classList.add("hidden");
document.getElementById("screen-victory").classList.add("hidden");
document.getElementById("screen-gameover").classList.add("hidden");
this.log(`Descending into Floor 1 of Aethelgard as ${this.selectedClass}...`, "log-system");
this.setupEncounter();
}
abandonRun() {
document.getElementById("screen-menu").classList.remove("hidden");
document.getElementById("screen-victory").classList.add("hidden");
document.getElementById("screen-gameover").classList.add("hidden");
this.toggleShopModal(false);
this.toggleDeckModal(false);
}
setupEncounter() {
const isBoss = (this.currentRoom === 4);
let enemyName = "";
let enemyHp = 20;
let enemyDmg = 3;
if (!isBoss) {
if (this.currentFloor === 1) {
enemyHp = 20;
enemyDmg = 3;
enemyName = Math.random() > 0.5 ? "Void Goblin" : "Shadow Rat";
} else if (this.currentFloor === 2) {
enemyHp = 30;
enemyDmg = 5;
enemyName = Math.random() > 0.5 ? "Vampire Knight" : "Void Ogre";
} else {
enemyHp = 40;
enemyDmg = 8;
enemyName = Math.random() > 0.5 ? "Void Warrior" : "Dusk Cultist";
}
} else {
// Boss Encounters
enemyHp = (this.currentFloor === 3) ? 40 : 80;
enemyDmg = 15;
if (this.currentFloor === 1) enemyName = "The Gatekeeper";
else if (this.currentFloor === 2) enemyName = "The Corrupted";
else enemyName = "The Void Lord";
}
this.enemy = {
name: enemyName,
hp: enemyHp,
maxHp: enemyHp,
damage: enemyDmg,
defense: 0,
floor: this.currentFloor,
isBoss: isBoss,
bossId: isBoss ? this.currentFloor : null,
status: {
burn: 0,
poison: 0,
stun: false,
silence: false,
skipNextTurn: false,
delayedPyre: false
}
};
// Update Floor Boss Goal text
const bossNames = ["Floor 1: The Gatekeeper (Shield Master)", "Floor 2: The Corrupted (Energy Siphon)", "Floor 3: The Void Lord (Regen Beast)"];
document.getElementById("floor-boss-brief").textContent = bossNames[this.currentFloor - 1];
// Reset Turn Defense
this.player.defense = this.player.permanentDefense;
// Draw initial hand
this.reshuffleDiscardIntoDeck();
this.player.hand = [];
this.drawCards(this.player.handSize);
this.isPlayerTurn = true;
this.isProcessing = false;
this.updateHUD();
this.renderHand();
this.renderSprites();
this.log(`Encounter started: ${this.enemy.name} (${this.enemy.hp} HP)!`, isBoss ? "log-enemy" : "log-system");
}
reshuffleDiscardIntoDeck() {
if (this.player.discardPile.length > 0) {
this.player.deck.push(...this.player.discardPile);
this.player.discardPile = [];
}
}
drawCards(count) {
for (let i = 0; i < count; i++) {
if (this.player.hand.length >= 5) break; // Hard hand limit in UI
if (this.player.deck.length === 0) {
this.reshuffleDiscardIntoDeck();
}
if (this.player.deck.length > 0) {
// Draw random card from deck pool
const randIndex = Math.floor(Math.random() * this.player.deck.length);
const card = this.player.deck.splice(randIndex, 1)[0];
this.player.hand.push(card);
}
}
}
// --- CARD PLAY RESOLUTION ---
playCard(index) {
if (!this.isPlayerTurn || this.isProcessing) return;
const card = this.player.hand[index];
if (!card) return;
if (this.player.energy < card.cost) {
this.showFloatingText("Not Enough Energy!", "#ef4444", "player");
return;
}
// Silence check for abilities
if (this.player.status.silence && card.type === "Other") {
this.showFloatingText("Silenced!", "#9ca3af", "player");
return;
}
this.player.energy -= card.cost;
// Remove from hand and move to discard
this.player.hand.splice(index, 1);
this.player.discardPile.push(card);
// Hero attack animation
const heroBox = document.getElementById("hero-sprite-container");
heroBox.classList.add("hero-attack");
setTimeout(() => heroBox.classList.remove("hero-attack"), 400);
this.log(`You played [${card.name}] (Cost ${card.cost} Energy)`, "log-player");
this.resolveCardEffect(card);
this.updateHUD();
this.renderHand();
// Check if enemy died immediately
if (this.enemy.hp <= 0) {
this.handleEnemyDeath();
}
}
resolveCardEffect(card) {
// 1. Attack Type
if (card.type === "Attack") {
let dmg = card.atk;
let bypassDefense = false;
// Special Card Logic
if (card.id === "BA-A01") { // Slam: If 30 HP, draw 1
if (this.player.hp >= 30) this.drawCards(1);
} else if (card.id === "WI-A01") { // Fireball: 1 Burn
this.enemy.status.burn += 1;
} else if (card.id === "WI-A02") { // Inferno: 3 Burn
this.enemy.status.burn += 3;
} else if (card.id === "WI-A03") { // Ember: 1 continuous Burn
this.enemy.status.burn += 1;
} else if (card.id === "WI-A04") { // Pyre: Burn next turn
this.enemy.status.delayedPyre = true;
} else if (card.id === "SH-A01") { // Backstab: +1 dmg if target has no armor
if (this.enemy.defense <= 0) dmg += 1;
} else if (card.id === "SH-A02") { // Shadow Bolt: Cannot block
bypassDefense = true;
} else if (card.id === "SH-A03") { // Silence: Target loses abilities
this.enemy.status.silence = true;
} else if (card.id === "SH-A04") { // Vengeance: Gain 2 Energy
this.player.energy = Math.min(this.player.maxEnergy, this.player.energy + 2);
}
this.dealDamageToEnemy(dmg, bypassDefense);
}
// 2. Defense Type
else if (card.type === "Defense") {
if (card.def > 0) {
if (card.id === "BA-D02") { // Iron Skin: permanent defense, discard 1
this.player.permanentDefense += card.def;
this.player.defense += card.def;
if (this.player.hand.length > 0) {
const discarded = this.player.hand.pop();
this.player.discardPile.push(discarded);
this.log(`Iron Skin permanently hardened! Discarded ${discarded.name}.`, "log-player");
}
} else {
this.player.defense += card.def;
}
}
if (card.id === "BA-D04") { // Taunt: Stun 1 enemy
this.enemy.status.stun = true;
this.log(`Enemy is Taunted & Stunned for 1 turn!`, "log-status");
} else if (card.id === "WI-D02" || card.id === "SH-D02") { // Heat Resist / Step: Gain 1 Energy
this.player.energy = Math.min(this.player.maxEnergy, this.player.energy + 1);
} else if (card.id === "WI-D04") { // Endure: Heal 2 HP
this.player.hp = Math.min(this.player.maxHp, this.player.hp + 2);
} else if (card.id === "SH-D01") { // Blur: Ignore next attack
this.player.status.ignoreNextAttack = true;
} else if (card.id === "SH-D03") { // Evasion: Skip enemy action
this.enemy.status.skipNextTurn = true;
} else if (card.id === "SH-D04") { // Phantom: Ghost form
this.player.status.ghost = true;
}
this.showFloatingText(`+${card.def} DEF`, "#38bdf8", "player");
}
// 3. Other / Utility Type
else if (card.type === "Other") {
if (card.id === "BA-U01") { // Search: Draw 1, discard if > 5
this.drawCards(1);
while (this.player.hand.length > 5) {
this.player.discardPile.push(this.player.hand.pop());
}
} else if (card.id === "BA-U02") { // Repair: Heal 6 HP
this.player.hp = Math.min(this.player.maxHp, this.player.hp + 6);
this.showFloatingText("+6 HP", "#4ade80", "player");
} else if (card.id === "BA-U03") { // Rally: Draw 1, Gain 1 Energy
this.drawCards(1);
this.player.energy = Math.min(this.player.maxEnergy, this.player.energy + 1);
} else if (card.id === "BA-U04") { // Loot: Gain 5 Gold
this.player.gold += 5;
this.showFloatingText("+5 Gold", "#facc15", "player");
} else if (card.id === "WI-U01") { // Poison: Apply 1 Poison
this.enemy.status.poison += 1;
this.showFloatingText("+1 Poison", "#22c55e", "enemy");
} else if (card.id === "WI-U02") { // Burn: 3 dmg + 2 Burn
this.dealDamageToEnemy(3, false);
this.enemy.status.burn += 2;
} else if (card.id === "WI-U03") { // Slow: Draw 1, Stun enemy
this.drawCards(1);
this.enemy.status.stun = true;
} else if (card.id === "WI-U04") { // Wither: Heal 5, reduce enemy atk
this.player.hp = Math.min(this.player.maxHp, this.player.hp + 5);
this.enemy.damage = Math.max(1, this.enemy.damage - 1);
this.showFloatingText("+5 HP", "#4ade80", "player");
} else if (card.id === "SH-U01") { // Dash: Gain 1 Energy, Skip enemy
this.player.energy = Math.min(this.player.maxEnergy, this.player.energy + 1);
this.enemy.status.skipNextTurn = true;
} else if (card.id === "SH-U02") { // Teleport: 2 direct dmg
this.dealDamageToEnemy(2, true);
} else if (card.id === "SH-U03") { // Hide: +1 Def, Discard 1, Draw 2
this.player.defense += 1;
if (this.player.hand.length > 0) this.player.discardPile.push(this.player.hand.pop());
this.drawCards(2);
} else if (card.id === "SH-U04") { // Assassinate: 8 damage unblockable
this.dealDamageToEnemy(8, true);
}
}
}
dealDamageToEnemy(dmg, bypassDefense = false) {
let finalDamage = dmg;
// Boss 1 unique mechanic: Blocks 5 damage per turn until HP < 20
if (this.enemy.isBoss && this.enemy.bossId === 1 && this.enemy.hp >= 20 && !bypassDefense) {
finalDamage = Math.max(0, dmg - 5);
this.log(`The Gatekeeper's stone bulwark absorbed 5 damage!`, "log-enemy");
}
if (!bypassDefense && this.enemy.defense > 0) {
if (this.enemy.defense >= finalDamage) {
this.enemy.defense -= finalDamage;
this.showFloatingText(`Blocked (${finalDamage})`, "#60a5fa", "enemy");
finalDamage = 0;
} else {
finalDamage -= this.enemy.defense;
this.enemy.defense = 0;
}
}
if (finalDamage > 0) {
this.enemy.hp -= finalDamage;
this.showFloatingText(`-${finalDamage}`, "#ef4444", "enemy");
const enemyBox = document.getElementById("enemy-sprite-container");
enemyBox.classList.add("enemy-hit");
setTimeout(() => enemyBox.classList.remove("enemy-hit"), 400);
}
}
// --- TURN CYCLE & ENEMY RESOLUTION ---
endTurn() {
if (!this.isPlayerTurn || this.isProcessing) return;
this.isPlayerTurn = false;
this.isProcessing = true;
this.updateHUD();
document.getElementById("turn-dot").classList.add("enemy");
document.getElementById("turn-status-label").textContent = "ENEMY ACTION";
document.getElementById("btn-end-turn").disabled = true;
// 1. Resolve Player End-Of-Turn Status (Poison / Burn)
if (this.player.status.poison > 0) {
this.player.hp -= this.player.status.poison;
this.log(`Poison inflicted ${this.player.status.poison} damage on you!`, "log-status");
this.showFloatingText(`-${this.player.status.poison} Poison`, "#22c55e", "player");
}
if (this.player.status.burn > 0) {
this.player.hp -= this.player.status.burn;
this.log(`Burn scorched you for ${this.player.status.burn} damage!`, "log-status");
this.showFloatingText(`-${this.player.status.burn} Burn`, "#f97316", "player");
}
if (this.player.hp <= 0) {
this.handleGameOver();
return;
}
// 2. Enemy Step
setTimeout(() => {
this.resolveEnemyTurn();
}, 800);
}
resolveEnemyTurn() {
if (this.enemy.hp <= 0) return;
// Apply Enemy Statuses (Burn / Poison)
if (this.enemy.status.poison > 0) {
this.enemy.hp -= this.enemy.status.poison;
this.log(`${this.enemy.name} took ${this.enemy.status.poison} Poison damage!`, "log-status");
this.showFloatingText(`-${this.enemy.status.poison} Poison`, "#22c55e", "enemy");
}
if (this.enemy.status.burn > 0) {
this.enemy.hp -= this.enemy.status.burn;
this.log(`${this.enemy.name} took ${this.enemy.status.burn} Burn damage!`, "log-status");
this.showFloatingText(`-${this.enemy.status.burn} Burn`, "#f97316", "enemy");
}
if (this.enemy.status.delayedPyre) {
this.enemy.hp -= 1;
this.enemy.status.delayedPyre = false;
this.showFloatingText("-1 Pyre", "#ea580c", "enemy");
}
// Boss 3 Unique: +10 HP per turn Regen
if (this.enemy.isBoss && this.enemy.bossId === 3) {
this.enemy.hp = Math.min(this.enemy.maxHp + 40, this.enemy.hp + 10);
this.log(`The Void Lord regenerated +10 HP from the Nether!`, "log-enemy");
this.showFloatingText("+10 Regen", "#a855f7", "enemy");
}
// Check if DoT killed enemy
if (this.enemy.hp <= 0) {
this.handleEnemyDeath();
return;
}
// Check Stuns or Skips
if (this.enemy.status.stun) {
this.log(`${this.enemy.name} is STUNNED and cannot act!`, "log-status");
this.enemy.status.stun = false;
this.finishEnemyTurn();
return;
}
if (this.enemy.status.skipNextTurn) {
this.log(`${this.enemy.name}'s attack was avoided!`, "log-status");
this.enemy.status.skipNextTurn = false;
this.finishEnemyTurn();
return;
}
// Boss 2 Unique Attack: Deals 20 damage if Player Energy < 3
let incomingDamage = this.enemy.damage;
if (this.enemy.isBoss && this.enemy.bossId === 2) {
if (this.player.energy < 3) {
incomingDamage = 20;
this.log(`The Corrupted feeds on low energy! Deals 20 Cataclysmic Damage!`, "log-enemy");
}
}
// Enemy Animation
const enemyBox = document.getElementById("enemy-sprite-container");
enemyBox.classList.add("enemy-attack");
setTimeout(() => enemyBox.classList.remove("enemy-attack"), 400);
// Apply Damage to Player (Block Math: Incoming - Defense)
if (this.player.status.ghost) {
this.log(`You are in Ghost form! Attack passed right through.`, "log-player");
this.showFloatingText("Ghosted!", "#38bdf8", "player");
this.player.status.ghost = false;
} else if (this.player.status.ignoreNextAttack) {
this.log(`Blur evaded the enemy strike!`, "log-player");
this.showFloatingText("Dodged!", "#38bdf8", "player");
this.player.status.ignoreNextAttack = false;
} else {
let finalDmg = incomingDamage;
if (this.player.defense > 0) {
if (this.player.defense >= finalDmg) {
this.player.defense -= finalDmg;
this.showFloatingText(`Blocked (${finalDmg})`, "#60a5fa", "player");
finalDmg = 0;
} else {
finalDmg -= this.player.defense;
this.player.defense = 0;
}
}
if (finalDmg > 0) {
this.player.hp -= finalDmg;
this.showFloatingText(`-${finalDmg}`, "#ef4444", "player");
this.log(`${this.enemy.name} strikes for ${finalDmg} damage!`, "log-enemy");
const heroBox = document.getElementById("hero-sprite-container");
heroBox.classList.add("hero-hit");
setTimeout(() => heroBox.classList.remove("hero-hit"), 400);
}
}
if (this.player.hp <= 0) {
this.handleGameOver();
return;
}
setTimeout(() => {
this.finishEnemyTurn();
}, 600);
}
finishEnemyTurn() {
// 3. Start Next Player Turn: Recover 1 Energy (if < max), reset temp defense, draw up to hand size
this.player.energy = Math.min(this.player.maxEnergy, this.player.energy + 1);
this.player.defense = this.player.permanentDefense; // Retain permanent defense (e.g. Iron Skin)
this.player.status.silence = false;
// Draw cards up to hand limit
const needed = this.player.handSize - this.player.hand.length;
if (needed > 0) {
this.drawCards(needed);
}
this.isPlayerTurn = true;
this.isProcessing = false;
document.getElementById("turn-dot").classList.remove("enemy");
document.getElementById("turn-status-label").textContent = "PLAYER TURN";
document.getElementById("btn-end-turn").disabled = false;
this.updateHUD();
this.renderHand();
this.renderSprites();
}
handleEnemyDeath() {
const goldReward = this.enemy.isBoss ? 15 : 5;
this.player.gold += goldReward;
this.log(`${this.enemy.name} was vanquished! Gained +${goldReward} Gold.`, "log-system");
this.showFloatingText(`+${goldReward} Gold`, "#facc15", "player");
setTimeout(() => {
if (this.enemy.isBoss && this.currentFloor === 3) {
// WIN CONDITION: Defeated Boss 3
this.handleVictory();
} else {
// Advance Room or Floor
if (this.currentRoom >= 4) {
// Boss slain -> next floor
this.currentFloor += 1;
this.currentRoom = 1;
this.log(`FLOOR CLEARED! Descending to Floor ${this.currentFloor}...`, "log-system");
} else {
this.currentRoom += 1;
}
this.setupEncounter();
}
}, 1200);
}
handleGameOver() {
this.log("You have perished in the darkness.", "log-enemy");
document.getElementById("screen-gameover").classList.remove("hidden");
}
handleVictory() {
this.log("VICTORY! The Void Lord is defeated!", "log-system");
document.getElementById("screen-victory").classList.remove("hidden");
}
// --- 4. SHOP & ECONOMY SYSTEM ---
toggleShopModal(open) {
const modal = document.getElementById("shop-modal");
if (open) {
modal.classList.add("active");
this.renderShopGrid();
} else {
modal.classList.remove("active");
}
}
toggleDeckModal(open) {
const modal = document.getElementById("deck-modal");
if (open) {
modal.classList.add("active");
this.renderDeckGrid();
} else {
modal.classList.remove("active");
}
}
filterShop(category) {
this.shopFilter = category;
document.querySelectorAll(".shop-tab-btn").forEach(b => b.classList.remove("active"));
if (category === "ALL") document.getElementById("tab-all").classList.add("active");
else if (category === "BA") document.getElementById("tab-bastion").classList.add("active");
else if (category === "WI") document.getElementById("tab-wither").classList.add("active");
else if (category === "SH") document.getElementById("tab-shroud").classList.add("active");
this.renderShopGrid();
}
getCardPurchaseCost(cardId) {
const owned = this.player.inventory[cardId] || 0;
// Per GDD: If you own 3 copies, cost for 4th is 10 Gold. Base is 1 Gold.
// Scaling formula: Cost = Base + (Count * 1). If count == 3 -> 10 Gold.
if (owned >= 3) return 10;
return Math.min(50, 1 + owned);
}
buyCard(cardId) {
const cardData = CARD_DB.find(c => c.id === cardId);
if (!cardData) return;
const owned = this.player.inventory[cardId] || 0;
if (owned >= 5) {
this.showFloatingText("Max 5 Copies!", "#ef4444", "player");
return;
}
const cost = this.getCardPurchaseCost(cardId);
if (this.player.gold < cost) {
this.showFloatingText("Not Enough Gold!", "#ef4444", "player");
return;
}
// Execute Transaction
this.player.gold -= cost;
this.player.inventory[cardId] = owned + 1;
this.player.deck.push({ ...cardData });
this.log(`Purchased [${cardData.name}] for ${cost} Gold. (Total: ${this.player.inventory[cardId]}/5)`, "log-player");
this.updateHUD();
this.renderShopGrid();
}
renderShopGrid() {
document.getElementById("shop-gold-display").textContent = `${this.player.gold} G`;
const container = document.getElementById("shop-items-grid");
container.innerHTML = "";
const filtered = CARD_DB.filter(c => {
if (this.shopFilter === "ALL") return true;
return c.deckType === this.shopFilter;
});
filtered.forEach(card => {
const owned = this.player.inventory[card.id] || 0;
const cost = this.getCardPurchaseCost(card.id);
const isMaxed = owned >= 5;
const canAfford = this.player.gold >= cost && !isMaxed;
const itemEl = document.createElement("div");
itemEl.className = `shop-item-card ${isMaxed ? 'maxed' : ''}`;
itemEl.innerHTML = `
<div class="shop-item-info">
<div style="width:50px; height:65px; flex-shrink:0;">
${SVG_ASSETS.getCardFrameSVG(card.deckType, card.type)}
</div>
<div style="flex:1;">
<div style="font-size:13px; font-weight:700; color:#fff;">${card.name}</div>
<div style="font-size:10px; color:#94a3b8; margin-bottom:4px;">Cost: ${card.cost} Energy | ${card.type}</div>
<div style="font-family:var(--font-body); font-size:11px; color:#cbd5e1; line-height:1.2;">${card.desc}</div>
</div>
</div>
<div class="shop-buy-row">
<span class="shop-owned-count">Owned: <strong>${owned}</strong>/5</span>
<button class="btn-gothic ${canAfford ? 'btn-gold' : ''}" style="padding:4px 12px; font-size:11px;"
${!canAfford ? 'disabled' : ''} onclick="game.buyCard('${card.id}')">
${isMaxed ? 'MAX LIMIT' : `Buy (${cost} Gold)`}
</button>
</div>
`;
container.appendChild(itemEl);
});
}
renderDeckGrid() {
const container = document.getElementById("deck-items-grid");
container.innerHTML = "";
document.getElementById("deck-view-count").textContent = this.player.deck.length + this.player.hand.length + this.player.discardPile.length;
// Group cards by ID
const cardMap = {};
[...this.player.deck, ...this.player.hand, ...this.player.discardPile].forEach(c => {
if (!cardMap[c.id]) cardMap[c.id] = { card: c, count: 0 };
cardMap[c.id].count++;
});
Object.values(cardMap).forEach(item => {
const card = item.card;
const itemEl = document.createElement("div");
itemEl.className = "shop-item-card";
itemEl.innerHTML = `
<div class="shop-item-info">
<div style="width:48px; height:66px; flex-shrink:0; border-radius:4px; overflow:hidden; box-shadow:0 2px 6px rgba(0,0,0,0.5);" title="Card Back: ${card.deckType}">
${SVG_ASSETS.getCardBackSVG(card.deckType)}
</div>
<div style="flex:1;">
<div style="font-size:13px; font-weight:700; color:#fff;">${card.name}</div>
<div style="font-size:10px; color:#94a3b8; margin-bottom:4px;">Energy: ${card.cost} | ${card.type}</div>
<div style="font-family:var(--font-body); font-size:11px; color:#cbd5e1; line-height:1.2;">${card.desc}</div>
</div>
</div>
<div class="shop-buy-row">
<span class="shop-owned-count" style="color:#d4af37;">Copies in deck: <strong>${item.count}</strong></span>
</div>
`;
container.appendChild(itemEl);
});
}
// --- 5. UI & HUD RENDERING ---
updateHUD() {
if (!this.player) return;
// Top Bar
document.getElementById("hud-gold-text").textContent = `${this.player.gold} Gold`;
document.getElementById("hud-floor-num").textContent = this.currentFloor;
document.getElementById("hud-room-num").textContent = this.currentRoom;
document.getElementById("hud-deck-count").textContent = this.player.deck.length;
// Hero Info
document.getElementById("hero-name-label").textContent = this.player.className;
document.getElementById("hero-hp-val").textContent = Math.max(0, this.player.hp);
document.getElementById("hero-maxhp-val").textContent = this.player.maxHp;
document.getElementById("hero-def-badge").textContent = `Def: ${this.player.defense}`;
const hpPct = Math.max(0, Math.min(100, (this.player.hp / this.player.maxHp) * 100));
document.getElementById("hero-hp-bar").style.width = `${hpPct}%`;
document.getElementById("hero-handlim-val").textContent = this.player.handSize;
// Hero Statuses
const heroStatusBox = document.getElementById("hero-status-list");
heroStatusBox.innerHTML = "";
if (this.player.status.burn > 0) heroStatusBox.innerHTML += `<span class="status-pill burn">🔥 Burn (${this.player.status.burn})</span>`;
if (this.player.status.poison > 0) heroStatusBox.innerHTML += `<span class="status-pill poison">🧪 Poison (${this.player.status.poison})</span>`;
if (this.player.status.ghost) heroStatusBox.innerHTML += `<span class="status-pill ghost">👻 Ghost</span>`;
if (this.player.status.ignoreNextAttack) heroStatusBox.innerHTML += `<span class="status-pill ghost">⚡ Blur</span>`;
// Energy Orb
document.getElementById("energy-val").textContent = this.player.energy;
document.getElementById("energy-max-val").textContent = this.player.maxEnergy;
// Enemy Info
if (this.enemy) {
document.getElementById("enemy-name-text").textContent = this.enemy.name;
document.getElementById("enemy-boss-badge").style.display = this.enemy.isBoss ? "inline-block" : "none";
document.getElementById("enemy-hp-val").textContent = Math.max(0, this.enemy.hp);
document.getElementById("enemy-maxhp-val").textContent = this.enemy.maxHp;
document.getElementById("enemy-def-val").textContent = this.enemy.defense;
const enemyHpPct = Math.max(0, Math.min(100, (this.enemy.hp / this.enemy.maxHp) * 100));
document.getElementById("enemy-hp-bar").style.width = `${enemyHpPct}%`;
// Intent Box
let intentText = `⚔️ Intent: Attack ${this.enemy.damage} DMG`;
if (this.enemy.isBoss && this.enemy.bossId === 2 && this.player.energy < 3) {
intentText = `⚠️ Intent: VOID CATACLYSM 20 DMG!`;
} else if (this.enemy.status.stun) {
intentText = `💫 Intent: STUNNED (Cannot Act)`;
} else if (this.enemy.status.skipNextTurn) {
intentText = `💨 Intent: EVADED (Skipping Action)`;
}
document.getElementById("enemy-intent-box").innerHTML = intentText;
// Enemy Statuses
const enemyStatusBox = document.getElementById("enemy-status-list");
enemyStatusBox.innerHTML = "";
if (this.enemy.status.burn > 0) enemyStatusBox.innerHTML += `<span class="status-pill burn">🔥 Burn (${this.enemy.status.burn})</span>`;
if (this.enemy.status.poison > 0) enemyStatusBox.innerHTML += `<span class="status-pill poison">🧪 Poison (${this.enemy.status.poison})</span>`;
if (this.enemy.status.stun) enemyStatusBox.innerHTML += `<span class="status-pill stun">💫 Stunned</span>`;
}
}
renderHand() {
const container = document.getElementById("hand-container");
container.innerHTML = "";
document.getElementById("hand-count-text").textContent = this.player.hand.length;
this.player.hand.forEach((card, index) => {
const isPlayable = this.player.energy >= card.cost && this.isPlayerTurn && !this.isProcessing;
const cardEl = document.createElement("div");
cardEl.className = `aethel-card card-type-${card.type} ${!isPlayable ? 'unplayable' : ''}`;
const statLabel = card.type === "Attack"
? `<span class="card-stat-pill stat-atk">⚔️ ${card.atk} ATK</span>`
: (card.type === "Defense" ? `<span class="card-stat-pill stat-def">🛡️ ${card.def} DEF</span>` : `<span class="card-stat-pill" style="background:rgba(168,85,247,0.25); color:#d8b4fe;">✨ OTHER</span>`);
cardEl.innerHTML = `
<div class="card-art-box">
${SVG_ASSETS.getCardIllustrationSVG(card)}
</div>
<div class="card-details">
<div class="card-head">
<span class="card-title">${card.name}</span>
<div class="card-cost-badge">${card.cost}</div>
</div>
<div class="card-desc">${card.desc}</div>
<div class="card-footer">
${statLabel}
<span style="font-size:9px; color:#94a3b8;">${card.deckType}</span>
</div>
</div>
`;
cardEl.onclick = () => this.playCard(index);
container.appendChild(cardEl);
});
}
renderSprites() {
if (!this.player || !this.enemy) return;
document.getElementById("hero-portrait-slot").innerHTML = SVG_ASSETS.getClassAvatarSVG(this.player.className);
document.getElementById("hero-sprite-container").innerHTML = SVG_ASSETS.getHeroSpriteSVG(this.player.className);
document.getElementById("enemy-sprite-container").innerHTML = SVG_ASSETS.getEnemySpriteSVG(this.enemy);
}
showFloatingText(text, color, target) {
const parent = document.getElementById(target === "player" ? "hero-sprite-container" : "enemy-sprite-container");
if (!parent) return;
const el = document.createElement("div");
el.className = "floating-text";
el.textContent = text;
el.style.color = color;
el.style.left = "50%";
el.style.top = "40%";
el.style.transform = "translateX(-50%)";
parent.appendChild(el);
setTimeout(() => el.remove(), 1000);
}
log(msg, styleClass = "") {
const box = document.getElementById("combat-log-box");
const entry = document.createElement("div");
entry.className = `log-entry ${styleClass}`;
entry.textContent = msg;
box.appendChild(entry);
box.scrollTop = box.scrollHeight;
}
}
// Global initialization
let game;
window.addEventListener("DOMContentLoaded", () => {
game = new GameEngine();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.7-flash","provider":"openrouter","persona":"full","sessionId":"49041d0b-f9b5-458d-8bf0-58844ef7478f","tokensIn":1635236,"tokensOut":41548,"tokensTotal":1676784,"cost":0.2862292125000001,"turns":30,"toolCalls":29,"failedToolCalls":0,"timestamp":"2026-08-15T15:32:46.191Z"} -->