Deckbuilder
inkling · basic

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 48,143 bytes · SHA-256 efeb0aef9519
<!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 — Prototype</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700&family=Cinzel:wght@600&family=Spectral:ital,wght@0,400;1,400&display=swap');
:root {
--void-dark: #06040a;
--parchment: #1a1210;
--gold-text: #caa84d;
--gold-line: #8a6e2f;
--steel: #6a7a8a;
--red-attack: #9e2a2b;
--blue-def: #2a4a6e;
--purple-other: #3a1a4a;
--burn: #c98a2b;
--health: #5ab85a;
--energy: #3a6ea5;
--danger: #b12a2a;
}
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }
html, body { height: 100vh; overflow: hidden; background: linear-gradient(180deg, #04020a 0%, #0f0b16 60%, #16101a 100%); color: #e8dcc9; font-family: 'Spectral', serif; }
body {
display: flex;
flex-direction: column;
background-image:
radial-gradient(circle at 20% 30%, rgba(70,40,80,0.15) 0%, transparent 40%),
radial-gradient(circle at 80% 70%, rgba(100,70,30,0.1) 0%, transparent 40%),
repeating-linear-gradient(0deg, transparent 0px, transparent 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px);
}
h1, h2, h3 { font-family: 'Cinzel Decorative', serif; letter-spacing: 2px; text-shadow: 0 2px 0 #000, 0 0 20px rgba(255,220,180,0.15); color: var(--gold-text); }
/* Header Bar */
header {
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
border-bottom: 2px solid var(--gold-line);
background: linear-gradient(180deg, #111016 0%, #0a0810 100%);
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
z-index: 10;
position: relative;
}
.header-group {
display: flex;
align-items: center;
gap: 24px;
}
.stat-badge {
background: linear-gradient(135deg, #121018 0%, #1a1125 100%);
border: 1px solid #3a2a4a;
border-radius: 10px;
padding: 8px 16px;
box-shadow: inset 0 0 8px rgba(255,255,255,0.03);
min-width: 110px;
}
.stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; color: #8a7a90; display: block; margin-bottom: 2px; }
.stat-value { font-family: 'Cinzel', serif; font-size: 18px; color: var(--gold-text); text-shadow: 0 1px 2px #000; }
.stat-value.danger { color: #e85555; }
.stat-value.green { color: #5ab85a; }
.stat-value.blue { color: #5aa3d5; }
.stat-badge .icon { width: 18px; height: 18px; display: inline-block; vertical-align: middle; margin-right: 6px; }
/* Center Arena */
main {
flex: 1;
display: grid;
grid-template-columns: 1fr 2.4fr 1fr;
gap: 20px;
padding: 24px;
overflow: hidden;
}
/* Player / Enemy Panels */
.side-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.panel-header {
text-align: center;
padding-bottom: 8px;
border-bottom: 1px solid var(--gold-line);
margin-bottom: 8px;
}
.panel-card {
background: linear-gradient(160deg, #161018 0%, #0d0a12 100%);
border: 1px solid #2a2236;
border-radius: 14px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.05);
}
.panel-card h3 { font-size: 16px; margin-bottom: 10px; color: var(--gold-text); }
.entity-preview {
display: flex;
flex-direction: column;
align-items: center;
}
.entity-sprite {
width: 160px;
height: 160px;
position: relative;
margin-bottom: 12px;
}
.entity-sprite svg { width: 100%; height: 100%; filter: drop-shadow(0 0 12px rgba(255,220,180,0.15)); }
.hp-bar-container {
width: 100%;
height: 14px;
background: #0d0a12;
border-radius: 7px;
border: 1px solid #2a2236;
overflow: hidden;
margin-bottom: 6px;
box-shadow: inset 0 1px 3px #000;
}
.hp-bar-fill {
height: 100%;
background: linear-gradient(90deg, #8b1a1a 0%, #d93a3a 50%, #5ab85a 100%);
width: 60%;
transition: width 0.3s ease;
}
.entity-stats {
display: flex;
justify-content: space-around;
font-size: 12px;
color: #b8a8c0;
}
.defense-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: linear-gradient(135deg, #1a2a4a, #142a4e);
padding: 4px 10px;
border-radius: 20px;
border: 1px solid #3a4a6a;
font-weight: bold;
color: #8ec3f0;
font-size: 13px;
}
.defense-badge .shield-icon { width: 14px; height: 14px; }
.defense-badge.empty { color: #555; border-color: #2a2236; background: linear-gradient(135deg, #151015, #0d0a12); }
/* Center Arena */
.center-arena {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
}
.enemy-display {
background: linear-gradient(160deg, rgba(30,10,25,0.9), rgba(10,5,15,0.95));
border: 2px solid #3a243a;
border-radius: 20px;
padding: 28px 40px;
box-shadow: 0 12px 40px rgba(0,0,0,0.8), inset 0 0 40px rgba(100,0,50,0.1);
text-align: center;
min-width: 320px;
position: relative;
}
.enemy-display::before {
content: "";
position: absolute;
top: -2px; left: -2px; right: -2px; bottom: -2px;
background: linear-gradient(45deg, transparent 40%, rgba(255,255,255,0.03) 50%, transparent 60%);
border-radius: 20px;
pointer-events: none;
}
.enemy-title {
font-family: 'Cinzel Decorative', serif;
font-size: 22px;
color: var(--gold-text);
text-shadow: 0 2px 10px rgba(255,220,180,0.15);
margin-bottom: 14px;
}
.enemy-sprite-area {
width: 200px;
height: 200px;
margin: 0 auto 16px;
position: relative;
}
.enemy-sprite-area svg { filter: drop-shadow(0 0 15px rgba(200,50,70,0.3)); }
.boss-tag {
display: inline-block;
background: linear-gradient(135deg, #6a1a3a, #3a0a20);
border: 1px solid #9a2a4a;
color: #f0a0b0;
padding: 2px 10px;
border-radius: 12px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 4px;
box-shadow: 0 0 8px rgba(200,50,70,0.3);
}
/* Card Hand Area */
.hand-area {
display: flex;
gap: 14px;
justify-content: center;
min-height: 200px;
}
.card-wrapper {
position: relative;
width: 140px;
height: 190px;
perspective: 800px;
transition: transform 0.15s ease;
cursor: pointer;
}
.card-wrapper:hover { transform: translateY(-10px); }
.card-wrapper.playable { filter: drop-shadow(0 0 10px rgba(255,255,255,0.15)); }
.card-wrapper.unplayable { filter: grayscale(0.6) brightness(0.6); }
.card-inner {
width: 100%;
height: 100%;
position: relative;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0,0,0,0.8), inset 0 0 20px rgba(0,0,0,0.5);
background: var(--parchment);
border: 1px solid #3a2a2a;
display: flex;
flex-direction: column;
padding: 8px;
transition: border-color 0.2s;
}
.card-wrapper.playable .card-inner { border-color: var(--gold-line); box-shadow: 0 4px 16px rgba(0,0,0,0.8), 0 0 0 1px var(--gold-line), inset 0 0 20px rgba(200,170,100,0.1); }
.card-type-header {
display: flex;
justify-content: space-between;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: bold;
margin-bottom: 6px;
}
.card-cost {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #1a1210, #0d0808);
border: 1px solid var(--gold-line);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cinzel', serif;
font-size: 14px;
color: var(--gold-text);
box-shadow: 0 0 6px rgba(200,170,100,0.2);
}
.card-title {
font-family: 'Cinzel', serif;
font-size: 13px;
color: #f0e6c8;
text-align: center;
margin: 4px 0;
line-height: 1.2;
text-shadow: 0 1px 3px #000;
}
.card-art {
flex: 1;
border-radius: 6px;
overflow: hidden;
border: 1px solid #2a1a1a;
background: linear-gradient(160deg, #1a1010 0%, #0d0808 100%);
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin: 4px 0;
}
.card-art img { max-width: 90%; max-height: 60px; opacity: 0.9; filter: drop-shadow(0 0 6px rgba(255,255,255,0.1)); }
.card-ability {
font-size: 11px;
line-height: 1.3;
color: #c8b8a8;
padding-top: 6px;
border-top: 1px solid #2a2222;
}
.card-desc { font-style: italic; color: #a89880; }
.card-stats-row {
display: flex;
justify-content: center;
gap: 6px;
margin-top: 4px;
font-size: 10px;
}
/* Back of card */
.card-back {
width: 100%;
height: 100%;
position: absolute;
top: 0; left: 0;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0,0,0,0.8), inset 0 0 20px rgba(0,0,0,0.5);
border: 1px solid #3a2a2a;
background: linear-gradient(160deg, #0f0815, #06030a);
display: flex;
align-items: center;
justify-content: center;
}
.card-back svg { width: 80%; height: 80%; opacity: 0.9; filter: drop-shadow(0 0 8px rgba(200,170,100,0.15)); }
/* Bottom Controls */
.controls-bar {
height: 90px;
border-top: 2px solid var(--gold-line);
background: linear-gradient(180deg, #0a0810 0%, #05030a 100%);
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
box-shadow: 0 -4px 20px rgba(0,0,0,0.6);
}
.control-btn {
font-family: 'Cinzel', serif;
font-size: 16px;
letter-spacing: 1.5px;
padding: 14px 36px;
border-radius: 6px;
border: 1px solid var(--gold-line);
background: linear-gradient(180deg, #1a1510 0%, #0f0a08 100%);
color: var(--gold-text);
box-shadow: 0 4px 12px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.05);
cursor: pointer;
text-shadow: 0 1px 2px #000;
transition: all 0.2s;
}
.control-btn:hover:not(:disabled) {
background: linear-gradient(180deg, #2a1f10 0%, #16110d 100%);
box-shadow: 0 6px 16px rgba(0,0,0,0.6), 0 0 10px rgba(200,170,100,0.15), inset 0 1px 0 rgba(255,255,255,0.08);
transform: translateY(-2px);
}
.control-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.control-btn.red { border-color: #4a1a1a; color: #e8a0a0; }
.control-btn.green { border-color: #1a4a2a; color: #a0e8a0; }
/* Shop Overlay */
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.85);
z-index: 100;
display: none;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.overlay.active { display: flex; }
.shop-modal {
width: 920px;
max-width: 96vw;
max-height: 92vh;
background: linear-gradient(180deg, #121018 0%, #0a0810 100%);
border: 2px solid var(--gold-line);
border-radius: 16px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.9), inset 0 0 30px rgba(200,170,100,0.05);
overflow-y: auto;
}
.shop-header {
text-align: center;
margin-bottom: 24px;
border-bottom: 1px solid var(--gold-line);
padding-bottom: 16px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
}
.shop-card {
background: linear-gradient(160deg, #161018 0%, #0d0a12 100%);
border: 1px solid #2a2236;
border-radius: 10px;
padding: 14px;
transition: transform 0.15s;
cursor: pointer;
}
.shop-card:hover { transform: translateY(-4px); border-color: var(--gold-line); }
.shop-card .card-cost { margin-bottom: 8px; }
.shop-card h4 { font-family: 'Cinzel', serif; font-size: 14px; color: var(--gold-text); margin-bottom: 6px; }
.shop-card .ability { font-size: 11px; color: #b8a8c0; line-height: 1.3; }
.shop-card .price-tag { margin-top: 8px; font-size: 13px; color: #caa84d; font-weight: bold; }
.shop-card.disabled { opacity: 0.3; pointer-events: none; }
/* Utility */
.energy-meter {
display: flex;
gap: 4px;
}
.energy-orb {
width: 26px; height: 26px;
border-radius: 50%;
background: linear-gradient(135deg, #3a6ea5, #1a3a5a);
border: 1px solid #8ec3f0;
box-shadow: 0 0 8px rgba(138,195,240,0.3), inset 0 -2px 3px rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-family: 'Cinzel', serif; color: #fff;
}
.energy-orb.empty { background: linear-gradient(135deg, #1a1a2a, #0d0d15); border-color: #333; box-shadow: inset 0 2px 4px #000; color: #555; }
/* Class badges */
.class-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: bold;
border: 1px solid;
}
.bastion { background: #1a1a0a; color: #caa84d; border-color: #8a6e2f; }
.wither { background: #1a0a0a; color: #e8a0a0; border-color: #8a2a2a; }
.shroud { background: #0a0a1a; color: #b0a0e0; border-color: #4a2a6e; }
/* Small text for turn log */
.log-area {
height: 80px;
overflow-y: auto;
background: #06030a;
border: 1px solid #1a1020;
border-radius: 8px;
padding: 8px;
font-size: 12px;
color: #a89888;
line-height: 1.4;
}
.log-area .entry { margin-bottom: 2px; }
.log-area .entry.dmg { color: #e85555; }
.log-area .entry.heal { color: #5ab85a; }
.log-area .entry.gold { color: var(--gold-text); }
.log-area .entry.block { color: #5aa3d5; }
/* Responsive adjustments */
@media (max-width: 1200px) {
main { grid-template-columns: 1fr; grid-template-rows: auto auto auto; gap: 10px; }
.hand-area { order: 3; }
}
</style>
</head>
<body>
<header>
<div class="header-group">
<div>
<h1 style="font-size: 22px;">Shadows of Aethelgard</h1>
</div>
<div style="font-size: 11px; color: #8a7a90;">Roguelite Deckbuilder — Prototype</div>
</div>
<div class="header-group" style="gap: 16px;">
<div class="stat-badge"><span class="stat-label">Gold</span><span class="stat-value" id="hud-gold">0</span></div>
<div class="stat-badge"><span class="stat-label">Floor</span><span class="stat-value" id="hud-floor">1 / 1</span></div>
<div class="stat-badge"><span class="stat-label">Boss HP</span><span class="stat-value" id="hud-boss-hp">20</span></div>
</div>
</header>
<main>
<!-- Player Side -->
<aside class="side-panel" style="align-items: flex-end; text-align: right;">
<div style="display: flex; justify-content: flex-end; gap: 8px; align-items: center;">
<span class="class-badge bastion">Bastion</span>
<span class="class-badge wither">Wither</span>
<span class="class-badge shroud">Shroud</span>
<span style="font-size: 12px; color: #888;">Class: <strong id="hero-class">The Bastion</strong></span>
</div>
<div class="panel-card">
<h3>Your Hero</h3>
<div class="entity-preview">
<div class="entity-sprite">
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="heroGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#d0b060"/>
<stop offset="100%" stop-color="#8a6e2f"/>
</linearGradient>
<filter id="heroGlow">
<feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#d0b060" flood-opacity="0.4"/>
</filter>
</defs>
<g filter="url(#heroGlow)">
<!-- Armor body -->
<rect x="30" y="40" width="40" height="40" rx="6" fill="url(#heroGrad)" stroke="#5a4520" stroke-width="2"/>
<!-- Helmet -->
<path d="M25 35 L50 15 L75 35 L70 45 L30 45 Z" fill="#a08040" stroke="#6a5230" stroke-width="2"/>
<!-- Shield -->
<polygon points="10,30 35,50 35,80 10,65" fill="#2a4a6e" stroke="#8ec3f0" stroke-width="2"/>
<polygon points="15,35 30,55 30,72 15,58" fill="#3a6ea5"/>
<!-- Sword -->
<line x1="65" y1="55" x2="90" y2="30" stroke="#e0c080" stroke-width="3" stroke-linecap="round"/>
<rect x="62" y="55" width="8" height="12" fill="#5a3a20"/>
</g>
</svg>
</div>
<div style="font-size: 14px; margin-bottom: 8px;"><strong>HP</strong> <span id="hero-hp" style="color: #e85555;">30</span> / <span id="hero-max-hp">30</span></div>
<div style="font-size: 14px; margin-bottom: 8px;"><strong>Energy</strong> <span id="hero-energy" style="color: #5aa3d5;">3</span> / <span id="hero-max-energy">3</span></div>
<div style="font-size: 14px; margin-bottom: 8px;"><strong>Defense</strong> <span id="hero-defense" style="color: #8ec3f0;">0</span></div>
<div style="font-size: 14px;"><strong>Hand Size</strong> <span id="hero-handsize">4</span></div>
</div>
<div style="margin-top: 12px;">
<h4 style="font-family:'Cinzel',serif; font-size: 14px; color: var(--gold-text); margin-bottom: 6px;">Energy</h4>
<div class="energy-meter" id="energy-meter">
<!-- populated -->
</div>
</div>
</div>
</aside>
<!-- Center Arena -->
<section class="center-arena">
<div class="enemy-display">
<div class="boss-tag" id="boss-tag" style="display:none;">Boss</div>
<h2 class="enemy-title" id="enemy-name">Goblin</h2>
<div class="enemy-sprite-area">
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="enemyGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#5a2a2a"/>
<stop offset="100%" stop-color="#2a1010"/>
</linearGradient>
<filter id="enemyShadow">
<feDropShadow dx="0" dy="2" stdDeviation="5" flood-color="#000" flood-opacity="0.8"/>
</filter>
</defs>
<g filter="url(#enemyShadow)">
<circle cx="60" cy="60" r="35" fill="url(#enemyGrad)" stroke="#8a3a3a" stroke-width="3"/>
<circle cx="50" cy="55" r="4" fill="#ffe8a0"/>
<circle cx="70" cy="55" r="4" fill="#ffe8a0"/>
<path d="M50 70 Q60 80 70 70" stroke="#ffe8a0" stroke-width="2" fill="none"/>
<path d="M40 45 L25 20 L35 30 Z" fill="#8a3a3a" stroke="#a05050"/>
<path d="M80 45 L95 20 L85 30 Z" fill="#8a3a3a" stroke="#a05050"/>
</g>
</svg>
</div>
<div style="font-size: 18px; color: #e85555; font-family:'Cinzel',serif;" id="enemy-hp-display">20 / 20</div>
<div style="margin-top: 6px; color: #aaa; font-size: 12px;">Attack: <strong id="enemy-dmg">3</strong> | Defense: <strong id="enemy-def" style="color:#8ec3f0;">0</strong></div>
</div>
<div class="hand-area" id="hand-container">
<!-- Cards injected -->
</div>
</section>
<!-- Player Shop / Stats Side -->
<aside class="side-panel">
<div class="panel-card">
<h3>Combat Log</h3>
<div class="log-area" id="log"></div>
</div>
<div class="panel-card">
<h3>Shop</h3>
<div style="text-align: center; margin-bottom: 10px;">
<button class="control-btn" onclick="openShop()" id="shop-btn" style="padding: 10px 20px; font-size: 14px;">Open Shop</button>
</div>
<div style="font-size: 12px; color: #a89888; line-height: 1.5;">
<p>Buy cards: 1 Gold each. If you own 3 copies, 4th costs <strong>10 Gold</strong>. Max 5 copies per card.</p>
<p style="margin-top: 6px;">Floor <span id="shop-floor">1</span> — <span id="shop-enemy-type">Enemies</span></p>
</div>
</div>
</aside>
</main>
<div style="position: absolute; bottom: 14px; left: 0; right: 0; display: flex; justify-content: center; gap: 30px; z-index: 5; pointer-events: none;">
<button class="control-btn green" onclick="endTurn()" id="end-turn-btn" style="pointer-events: auto;">End Turn</button>
<button class="control-btn red" onclick="playHand()" id="play-btn" style="pointer-events: auto;">Play Cards</button>
</div>
<!-- Shop Modal -->
<div class="overlay" id="shop-overlay" onclick="closeShop(event)">
<div class="shop-modal" onclick="event.stopPropagation()">
<div class="shop-header">
<h2>Card Shop <span style="font-size: 14px; color: #8a7a90;">— 1 Gold per Card</span></h2>
<p style="font-size: 12px; color: #aaa;">Select cards to add to your deck. Close to continue.</p>
</div>
<div class="card-grid" id="shop-grid"></div>
<div style="text-align: center; margin-top: 20px;">
<button class="control-btn" onclick="closeShop()">Continue Adventure</button>
</div>
</div>
</div>
<script>
/* =========================================================
GAME STATE
========================================================= */
const STATE = {
hero: {
class: 'bastion', // bastion | wither | shroud
hp: 35,
maxHp: 35,
energy: 3,
maxEnergy: 3,
defense: 0,
gold: 0,
handSize: 4,
hand: [],
deck: [],
inventory: {}, // card id count
energyRegen: 3,
floor: 1,
room: 1,
maxCopy: 5,
burn: 0,
poison: 0,
stunned: false
},
enemy: {
name: 'Goblin',
hp: 20,
maxHp: 20,
dmg: 3,
defense: 0,
stun: 0,
burn: 0,
poison: 0,
isBoss: false,
floor: 1
},
turnPhase: 'player', // player | enemy | shop
selectedCards: [],
log: []
};
/* =========================================================
CARD DATABASE (36 cards)
========================================================= */
const CARDS = {
'BA-A01': { id:'BA-A01', name:'Slam', class:'bastion', type:'Attack', cost:2, atk:3, def:0, desc:'Deals 3 damage. If you have 30 HP, draw 1 card.', color:'red' },
'BA-A02': { id:'BA-A02', name:'Earthquake', class:'bastion', type:'Attack', cost:3, atk:4, def:0, desc:'Deals 4 damage to all enemies.', color:'red' },
'BA-A03': { id:'BA-A03', name:'Stone Fist', class:'bastion', type:'Attack', cost:1, atk:2, def:0, desc:'Deals 2 damage.', color:'red' },
'BA-A04': { id:'BA-A04', name:'Boulder', class:'bastion', type:'Attack', cost:4, atk:6, def:0, desc:'Deals 6 damage. Costs 4 Energy.', color:'red' },
'BA-D01': { id:'BA-D01', name:'Shield Wall', class:'bastion', type:'Defense', cost:0, atk:0, def:4, desc:'Gain 4 Defense for this turn.', color:'blue' },
'BA-D02': { id:'BA-D02', name:'Iron Skin', class:'bastion', type:'Defense', cost:2, atk:0, def:5, desc:'Gain 5 Defense permanently. Discard 1 card.', color:'blue' },
'BA-D03': { id:'BA-D03', name:'Stand Firm', class:'bastion', type:'Defense', cost:1, atk:0, def:2, desc:'Gain 2 Defense. Blocks 1 incoming damage.', color:'blue' },
'BA-D04': { id:'BA-D04', name:'Taunt', class:'bastion', type:'Defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Stun 1 enemy for 1 turn.', color:'blue' },
'BA-U01': { id:'BA-U01', name:'Search', class:'bastion', type:'Other', cost:2, atk:0, def:0, desc:'Draw 1 card. Discard hand if > 5 cards.', color:'purple' },
'BA-U02': { id:'BA-U02', name:'Repair', class:'bastion', type:'Other', cost:3, atk:0, def:0, desc:'Heal 6 HP.', color:'purple' },
'BA-U03': { id:'BA-U03', name:'Rally', class:'bastion', type:'Other', cost:1, atk:0, def:0, desc:'Draw 1 card. Gain 1 Energy.', color:'purple' },
'BA-U04': { id:'BA-U04', name:'Loot', class:'bastion', type:'Other', cost:2, atk:0, def:0, desc:'Gain 5 Gold.', color:'purple' },
'WI-A01': { id:'WI-A01', name:'Fireball', class:'wither', type:'Attack', cost:2, atk:3, def:0, desc:'Deals 3 damage. Apply 1 Burn.', color:'red' },
'WI-A02': { id:'WI-A02', name:'Inferno', class:'wither', type:'Attack', cost:3, atk:4, def:0, desc:'Deals 4 damage. Applies 3 Burn.', color:'red' },
'WI-A03': { id:'WI-A03', name:'Ember', class:'wither', type:'Attack', cost:1, atk:2, def:0, desc:'Deals 2 damage. Deals 1 damage every turn.', color:'red' },
'WI-A04': { id:'WI-A04', name:'Pyre', class:'wither', type:'Attack', cost:4, atk:5, def:0, desc:'Deals 5 damage. All enemies take 1 damage next turn.', color:'red' },
'WI-D01': { id:'WI-D01', name:'Scorch Shield', class:'wither', type:'Defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Block 1 Burn damage.', color:'blue' },
'WI-D02': { id:'WI-D02', name:'Heat Resist', class:'wither', type:'Defense', cost:1, atk:0, def:1, desc:'Gain 1 Defense. Gain 1 Energy.', color:'blue' },
'WI-D03': { id:'WI-D03', name:'Smoke', class:'wither', type:'Defense', cost:2, atk:0, def:4, desc:'Gain 4 Defense. Block 1 Burn damage.', color:'blue' },
'WI-D04': { id:'WI-D04', name:'Endure', class:'wither', type:'Defense', cost:3, atk:0, def:2, desc:'Gain 2 Defense. Heal 2 HP.', color:'blue' },
'WI-U01': { id:'WI-U01', name:'Poison', class:'wither', type:'Other', cost:2, atk:0, def:0, desc:'Apply 1 Poison to target.', color:'purple' },
'WI-U02': { id:'WI-U02', name:'Burn', class:'wither', type:'Other', cost:3, atk:0, def:0, desc:'Deal 3 damage + 2 Burn to all enemies.', color:'purple' },
'WI-U03': { id:'WI-U03', name:'Slow', class:'wither', type:'Other', cost:1, atk:0, def:0, desc:'Draw 1 card. Target cannot move next turn (Stun).', color:'purple' },
'WI-U04': { id:'WI-U04', name:'Wither', class:'wither', type:'Other', cost:2, atk:0, def:0, desc:'Heal 5 HP. Reduce enemy Attack by 1.', color:'purple' },
'SH-A01': { id:'SH-A01', name:'Backstab', class:'shroud', type:'Attack', cost:2, atk:4, def:0, desc:'Deals 4 damage. +1 Damage if target has no Armor.', color:'red' },
'SH-A02': { id:'SH-A02', name:'Shadow Bolt', class:'shroud', type:'Attack', cost:3, atk:5, def:0, desc:'Deals 5 damage. Target cannot block.', color:'red' },
'SH-A03': { id:'SH-A03', name:'Silence', class:'shroud', type:'Attack', cost:1, atk:3, def:0, desc:'Deals 3 damage. Target loses ability to use cards.', color:'red' },
'SH-A04': { id:'SH-A04', name:'Vengeance', class:'shroud', type:'Attack', cost:3, atk:6, def:0, desc:'Deals 6 damage. Gain 2 Energy.', color:'red' },
'SH-D01': { id:'SH-D01', name:'Blur', class:'shroud', type:'Defense', cost:3, atk:0, def:2, desc:'Gain 2 Defense. Ignore next attack.', color:'blue' },
'SH-D02': { id:'SH-D02', name:'Step', class:'shroud', type:'Defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Gain 1 Energy.', color:'blue' },
'SH-D03': { id:'SH-D03', name:'Evasion', class:'shroud', type:'Defense', cost:1, atk:0, def:1, desc:'Gain 1 Defense. Skip enemy action.', color:'blue' },
'SH-D04': { id:'SH-D04', name:'Phantom', class:'shroud', type:'Defense', cost:2, atk:0, def:4, desc:'Gain 4 Defense. Ghost form for 1 turn.', color:'blue' },
'SH-U01': { id:'SH-U01', name:'Dash', class:'shroud', type:'Other', cost:1, atk:0, def:0, desc:'Gain 1 Energy. Skip enemy attack.', color:'purple' },
'SH-U02': { id:'SH-U02', name:'Teleport', class:'shroud', type:'Other', cost:2, atk:0, def:0, desc:'Move to back row. Deal 2 damage.', color:'purple' },
'SH-U03': { id:'SH-U03', name:'Hide', class:'shroud', type:'Other', cost:2, atk:0, def:0, desc:'Gain 1 Defense. Discard 1 card. Draw 2 cards.', color:'purple' },
'SH-U04': { id:'SH-U04', name:'Assassinate', class:'shroud', type:'Other', cost:3, atk:0, def:0, desc:'Deal 8 damage. Cannot be blocked this turn.', color:'purple' }
};
/* =========================================================
INITIALIZATION
========================================================= */
function initGame() {
STATE.hero.class = 'bastion';
STATE.hero.hp = 35;
STATE.hero.maxHp = 35;
STATE.hero.energy = 3;
STATE.hero.maxEnergy = 3;
STATE.hero.defense = 0;
STATE.hero.gold = 0;
STATE.hero.hand = [];
STATE.hero.deck = [];
STATE.hero.inventory = {};
STATE.hero.handSize = 4;
STATE.hero.burn = 0;
STATE.hero.poison = 0;
STATE.hero.stunned = false;
STATE.hero.floor = 1;
STATE.hero.room = 1;
STATE.enemy = { name:'Goblin', hp:20, maxHp:20, dmg:3, defense:0, stun:0, burn:0, poison:0, isBoss:false, floor:1 };
STATE.selectedCards = [];
STATE.log = [];
// Build starter deck
STATE.hero.deck = [
'BA-A03','BA-D01','BA-U03',
'BA-A01','BA-D03','BA-U01',
'BA-A02','BA-U02','BA-U04',
'BA-D02','BA-A04','BA-D04'
];
for (const id of STATE.hero.deck) addToInventory(id);
STATE.turnPhase = 'player';
drawHand();
updateUI();
addLog("A new journey begins in the shadows of Aethelgard...");
openShop(); // Force first shop
}
function addToInventory(id) {
STATE.hero.inventory[id] = (STATE.hero.inventory[id] || 0) + 1;
if (STATE.hero.inventory[id] > STATE.hero.maxCopy) STATE.hero.inventory[id] = STATE.hero.maxCopy;
}
function drawHand() {
while (STATE.hero.hand.length < STATE.hero.handSize && STATE.hero.deck.length > 0) {
const idx = Math.floor(Math.random() * STATE.hero.deck.length);
const cardId = STATE.hero.deck.splice(idx, 1)[0];
STATE.hero.hand.push(CARDS[cardId]);
}
renderHand();
}
function renderHand() {
const container = document.getElementById('hand-container');
container.innerHTML = '';
STATE.hero.hand.forEach((card, i) => {
const el = document.createElement('div');
el.className = 'card-wrapper playable';
el.innerHTML = buildCardHTML(card, i);
el.onclick = () => toggleSelectCard(i);
if (STATE.hero.energy < card.cost) el.classList.remove('playable');
else if (STATE.hero.stunned) el.classList.remove('playable');
else el.classList.add('playable');
container.appendChild(el);
});
}
function buildCardHTML(card, idx) {
const className = card.class;
const artSVG = cardSVGInner(card);
let statRow = '';
if (card.atk > 0 || card.def > 0) {
statRow = `<div class="card-stats-row">
<span>${card.atk ? '⚔ ' + card.atk : ''}</span>
<span>${card.def ? '🛡 ' + card.def : ''}</span>
</div>`;
}
// Placeholder art for main card image (unique frame per class)
const frameColor = className === 'bastion' ? '#8a6e2f' : className === 'wither' ? '#6e2f2f' : '#3a2a6e';
const artPlaceholder = `
<svg viewBox="0 0 100 60" xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%;">
<rect width="100" height="60" fill="#0a0808" stroke="${frameColor}" stroke-width="2"/>
<text x="50" y="32" text-anchor="middle" font-size="8" fill="#aaa" font-family="serif">CARD ART</text>
<text x="50" y="42" text-anchor="middle" font-size="6" fill="#555" font-family="sans-serif">PLACEHOLDER</text>
<rect x="10" y="10" width="80" height="40" fill="none" stroke="${frameColor}" stroke-width="1" stroke-dasharray="2 2"/>
</svg>
`;
return `
<div class="card-inner" style="border-top: 3px solid ${frameColor};">
<div class="card-type-header">
<span style="color:${card.color === 'red' ? '#e85555' : card.color === 'blue' ? '#5aa3d5' : '#b87ad5'};">${card.type}</span>
<span class="card-cost">${card.cost}</span>
</div>
<div class="card-title">${card.name}</div>
<div class="card-art">${artPlaceholder}</div>
<div class="card-ability">
<div class="card-desc">${card.desc}</div>
${statRow}
</div>
</div>
`;
}
function cardSVGInner(card) {
// Unique art placeholder could vary; we keep it simple
return '';
}
function toggleSelectCard(i) {
if (STATE.turnPhase !== 'player') return;
if (STATE.hero.stunned) { addLog("You are stunned and cannot play cards!"); return; }
const card = STATE.hero.hand[i];
if (STATE.hero.energy < card.cost) {
addLog("Not enough Energy to play " + card.name + "!");
return;
}
if (STATE.selectedCards.includes(i)) {
STATE.selectedCards = STATE.selectedCards.filter(x => x !== i);
} else {
STATE.selectedCards.push(i);
}
renderHand();
updateUI();
}
function buildBackHTML(className) {
let color = '#4a2236'; // bastion default
let text = 'B';
if (className === 'wither') { color = '#4a2a1a'; text = 'W'; }
if (className === 'shroud') { color = '#2a1a4a'; text = 'S'; }
return `
<div class="card-back">
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" fill="#080510" stroke="${color}" stroke-width="3"/>
<circle cx="50" cy="50" r="30" fill="none" stroke="${color}" stroke-width="2"/>
<text x="50" y="58" text-anchor="middle" font-size="36" fill="${color}" font-family="Cinzel Decorative" font-weight="bold">${text}</text>
<path d="M30 70 L50 40 L70 70" stroke="${color}" stroke-width="4" fill="none"/>
<circle cx="50" cy="50" r="8" fill="${color}"/>
</svg>
</div>
`;
}
function renderBackCards() {
// Not needed for play area, but could be used in deck view
return;
}
/* =========================================================
GAME ACTIONS
========================================================= */
function playHand() {
if (STATE.turnPhase !== 'player') return;
if (STATE.selectedCards.length === 0) {
addLog("Select cards to play.");
return;
}
// Resolve selected cards in order
STATE.selectedCards.sort((a,b) => a - b);
const cardsToPlay = STATE.selectedCards.map(i => STATE.hero.hand[i]);
STATE.hero.energy -= cardsToPlay.reduce((s, c) => s + c.cost, 0);
for (const card of cardsToPlay) {
resolveCardEffect(card);
}
// Remove played cards
STATE.hero.hand = STATE.hero.hand.filter((_, i) => !STATE.selectedCards.includes(i));
STATE.selectedCards = [];
// Enemy turn after player actions
STATE.turnPhase = 'enemy';
addLog("--- Enemy Turn ---");
setTimeout(() => {
resolveEnemyTurn();
STATE.turnPhase = 'player';
STATE.hero.stunned = false;
STATE.enemy.stun = Math.max(0, STATE.enemy.stun - 1);
STATE.hero.defense = 0; // Defense resets per turn
STATE.hero.energy = Math.min(STATE.hero.maxEnergy, STATE.hero.energy + 1);
drawHand();
renderHand();
updateUI();
addLog("Your turn begins.");
// Check win/loss
if (STATE.enemy.hp <= 0) {
defeatEnemy();
}
if (STATE.hero.hp <= 0) {
gameOver();
}
}, 600);
updateUI();
}
function resolveCardEffect(card) {
let dmg = card.atk || 0;
// Bastion Slam bonus
if (card.id === 'BA-A01' && STATE.hero.hp >= 30) {
drawHand(); // draw 1 card simulated by adding extra draw attempt
// Actually draw 1 immediately
if (STATE.hero.deck.length > 0) {
const c = STATE.hero.deck.splice(Math.floor(Math.random()*STATE.hero.deck.length),1)[0];
STATE.hero.hand.push(CARDS[c]);
addLog("Slam bonus: Draw a card!");
}
}
// Shroud Backstab +1 if enemy has 0 defense
if (card.id === 'SH-A01' && STATE.enemy.defense <= 0) dmg += 1;
if (card.type === 'Attack') {
addLog(`Played ${card.name}: Deals ${dmg} damage.`);
let finalDmg = Math.max(0, dmg - STATE.enemy.defense);
STATE.enemy.hp -= finalDmg;
STATE.enemy.hp = Math.max(0, STATE.enemy.hp);
addLog(`Enemy takes ${finalDmg} damage. (HP: ${STATE.enemy.hp}/${STATE.enemy.maxHp})`);
// Apply burn / status
if (card.id === 'WI-A01') { STATE.enemy.burn += 1; addLog("Applied 1 Burn."); }
if (card.id === 'WI-A03') { STATE.enemy.burn += 1; STATE.enemy.poison += 1; addLog("Applied 1 Burn + Poison."); }
if (card.id === 'WI-A02') { STATE.enemy.burn += 3; addLog("Applied 3 Burn."); }
if (card.id === 'WI-A04') { STATE.enemy.burn += 1; /* All enemies next turn handled globally */ }
if (card.id === 'WI-U02') { STATE.enemy.hp -= 3; STATE.enemy.burn += 2; addLog("Burn: 3 dmg + 2 Burn to all."); }
if (card.id === 'SH-A03') { STATE.enemy.stun += 1; addLog("Silenced / Stunned enemy for 1 turn!"); }
if (card.id === 'BA-D04') { STATE.enemy.stun += 1; addLog("Taunted enemy: Stunned 1 turn."); }
if (card.id === 'BA-A02') { STATE.enemy.hp -= 4; addLog("Earthquake hits for 4!"); }
if (card.id === 'BA-A04') { STATE.enemy.hp -= 6; addLog("Boulder smashes for 6!"); }
if (card.id === 'BA-A03') { STATE.enemy.hp -= 2; addLog("Stone Fist hits for 2!"); }
if (card.id === 'SH-A02') { STATE.enemy.hp -= 5; STATE.enemy.defense = 0; addLog("Shadow Bolt ignores block for 5!"); }
if (card.id === 'SH-A04') { STATE.enemy.hp -= 6; STATE.hero.energy += 2; addLog("Vengeance: 6 dmg, gain 2 Energy!"); }
if (card.id === 'SH-U04') { STATE.enemy.hp -= 8; addLog("Assassinate: 8 unblockable damage!"); }
if (card.id === 'SH-A03') { STATE.enemy.hp -= 3; STATE.enemy.stun += 1; addLog("Silence: 3 dmg + Stun!"); }
if (card.id === 'SH-U02') { STATE.enemy.hp -= 2; addLog("Teleport: 2 dmg!"); }
if (card.id === 'WI-U03') { STATE.enemy.stun += 1; addLog("Slow: Target stunned!"); drawHand(); }
}
if (card.type === 'Defense') {
let def = card.def;
STATE.hero.defense += def;
addLog(`Played ${card.name}: Gain ${def} Defense.`);
// Specific triggers
if (card.id === 'BA-D01') { addLog("Shield Wall active."); }
if (card.id === 'BA-D02') { STATE.hero.defense += 5; addLog("Iron Skin: Permanent +5 Defense!"); STATE.hero.defense = 5; }
if (card.id === 'BA-D03') { STATE.hero.defense += 2; addLog("Stand Firm: +1 Block."); STATE.enemy.defense = Math.max(0, STATE.enemy.defense - 1); }
if (card.id === 'BA-U01') {
// Search: Draw 1, discard if > 5
if (STATE.hero.deck.length > 0) {
const c = STATE.hero.deck.splice(Math.floor(Math.random()*STATE.hero.deck.length),1)[0];
STATE.hero.hand.push(CARDS[c]);
}
if (STATE.hero.hand.length > 5) {
STATE.hero.hand = STATE.hero.hand.slice(0, 5);
addLog("Search: Drawn, but discarded to 5.");
}
addLog("Search: Draw 1 card.");
}
if (card.id === 'WI-D01') { addLog("Scorch Shield: Block 1 Burn damage next turn!"); }
if (card.id === 'WI-D03') { addLog("Smoke: Block 1 Burn!"); }
if (card.id === 'WI-D04') { STATE.hero.defense += 2; STATE.hero.hp = Math.min(STATE.hero.maxHp, STATE.hero.hp + 2); addLog("Endure: +2 Def, Heal 2."); }
if (card.id === 'WI-U04') { STATE.hero.hp = Math.min(STATE.hero.maxHp, STATE.hero.hp + 5); STATE.enemy.dmg = Math.max(1, STATE.enemy.dmg - 1); addLog("Wither: Heal 5, reduce enemy Atk by 1!"); }
if (card.id === 'SH-D01') { addLog("Blur: Ignore next attack!"); STATE.hero.defense += 10; /* temporary boost for turn */ }
if (card.id === 'SH-D02') { STATE.hero.defense += 3; STATE.hero.energy += 1; addLog("Step: +3 Def, +1 Energy!"); }
if (card.id === 'SH-D03') { STATE.hero.defense += 1; STATE.turnPhase = 'skip'; addLog("Evasion: Skip enemy action this turn!"); STATE.enemy.stun += 1; }
if (card.id === 'SH-D04') { STATE.hero.defense += 4; addLog("Phantom: Ghost form active!"); }
}
if (card.type === 'Other') {
if (card.id === 'BA-U02') { STATE.hero.hp = Math.min(STATE.hero.maxHp, STATE.hero.hp + 6); addLog("Repair: Heal 6 HP!"); }
if (card.id === 'BA-U03') { STATE.hero.energy += 1; if (STATE.hero.deck.length > 0) { const c = STATE.hero.deck.splice(Math.floor(Math.random()*STATE.hero.deck.length),1)[0]; STATE.hero.hand.push(CARDS[c]); } addLog("Rally: Gain 1 Energy, Draw 1!"); }
if (card.id === 'BA-U04') { STATE.hero.gold += 5; addLog("Loot: Gain 5 Gold!"); }
if (card.id === 'WI-U01') { STATE.enemy.poison += 1; addLog("Poison: Apply 1 Poison!"); }
if (card.id === 'WI-D02') { STATE.hero.defense += 1; STATE.hero.energy += 1; addLog("Heat Resist: +1 Def, +1 Energy!"); }
if (card.id === 'WI-D04') { STATE.hero.hp = Math.min(STATE.hero.maxHp, STATE.hero.hp + 2); STATE.hero.defense += 2; addLog("Endure: +2 Def, Heal 2!"); }
if (card.id === 'SH-U01') { STATE.hero.energy += 1; STATE.turnPhase = 'skip'; STATE.enemy.stun += 1; addLog("Dash: Gain 1 Energy, Skip enemy attack!"); }
if (card.id === 'SH-U03') { STATE.hero.defense += 1; if (STATE.hero.hand.length > 1) STATE.hero.hand.splice(STATE.hero.hand.length-1, 1); addLog("Hide: +1 Def, discard 1, draw 2..."); STATE.hero.hand.push(CARDS['BA-A01']); STATE.hero.hand.push(CARDS['BA-D01']); }
}
// Status effects applied after effects
if (STATE.enemy.burn > 0) {
STATE.enemy.hp -= STATE.enemy.burn;
STATE.enemy.hp = Math.max(0, STATE.enemy.hp);
addLog(`Burn: Enemy takes ${STATE.enemy.burn} damage.`);
}
if (STATE.enemy.poison > 0) {
STATE.enemy.hp -= STATE.enemy.poison;
STATE.enemy.hp = Math.max(0, STATE.enemy.hp);
addLog(`Poison: Enemy takes ${STATE.enemy.poison} damage.`);
}
}
function resolveEnemyTurn() {
if (STATE.enemy.stun > 0) {
addLog("Enemy is stunned and skips its turn!");
STATE.enemy.stun = Math.max(0, STATE.enemy.stun - 1);
return;
}
// Enemy damage calculation
let dmg = STATE.enemy.dmg;
// Boss mechanics
if (STATE.enemy.isBoss) {
if (STATE.enemy.floor === 1 && STATE.enemy.hp > STATE.enemy.maxHp * 0.25) {
dmg = Math.max(0, dmg - 5);
addLog("Boss 1 blocks 5 damage until below 25% HP!");
}
if (STATE.enemy.floor === 2 && STATE.hero.energy < 3) {
dmg += 5;
addLog("Boss 2 deals +5 damage when you have < 3 Energy!");
}
if (STATE.enemy.floor === 3) {
STATE.enemy.hp = Math.min(State.enemy.maxHp, STATE.enemy.hp + 10);
addLog("Boss 3 regenerates 10 HP!");
}
}
let finalDmg = Math.max(0, dmg - STATE.hero.defense);
STATE.hero.defense = Math.max(0, STATE.hero.defense - dmg);
STATE.hero.hp -= finalDmg;
addLog(`Enemy attacks for ${finalDmg} damage! Your HP: ${STATE.hero.hp}/${STATE.hero.maxHp}`);
STATE.hero.hp = Math.max(0, STATE.hero.hp);
}
function defeatEnemy() {
STATE.hero.room++;
let goldGain = STATE.enemy.isBoss ? 15 : 5;
STATE.hero.gold += goldGain;
// Boss kill advances floor
if (STATE.enemy.isBoss) {
addLog(`*** BOSS DEFEATED! +${goldGain} Gold ***`);
STATE.hero.floor++;
STATE.hero.room = 1;
STATE.enemy.isBoss = false;
STATE.enemy.hp = 40 + STATE.hero.floor * 10;
STATE.enemy.dmg = 3 + STATE.hero.floor * 2;
STATE.enemy.maxHp = STATE.enemy.hp;
} else {
addLog(`Enemy defeated! +${goldGain} Gold.`);
}
addLog("Moving to next encounter...");
spawnNextEnemy();
STATE.turnPhase = 'player';
STATE.hero.defense = 0;
STATE.hero.energy = Math.min(STATE.hero.maxEnergy, STATE.hero.energy + 1);
drawHand();
renderHand();
updateUI();
openShop(); // Auto open shop after combat
}
function spawnNextEnemy() {
// Random enemy based on floor
const floor = STATE.hero.floor;
if (floor === 1) {
STATE.enemy = { name: 'Goblin', hp: 20, maxHp: 20, dmg: 3, defense: 0, stun: 0, burn: 0, poison: 0, isBoss: false, floor: 1 };
document.getElementById('boss-tag').style.display = 'none';
} else if (floor === 2) {
STATE.enemy = { name: 'Vampire Knight', hp: 30, maxHp: 30, dmg: 5, defense: 0, stun: 0, burn: 0, poison: 0, isBoss: false, floor: 2 };
document.getElementById('boss-tag').style.display = 'none';
} else if (floor === 3) {
if (STATE.hero.room === 3 || STATE.hero.room === 6 || STATE.hero.room === 9) {
// Bosses at end of floors
if (STATE.hero.room === 3) STATE.enemy = { name: 'The Gatekeeper', hp: 80, maxHp: 80, dmg: 15, defense: 5, stun: 0, burn: 0, poison: 0, isBoss: true, floor: 1 };
if (STATE.hero.room === 6) STATE.enemy = { name: 'The Corrupted', hp: 80, maxHp: 80, dmg: 20, defense: 0, stun: 0, burn: 0, poison: 0, isBoss: true, floor: 2 };
if (STATE.hero.room === 9) STATE.enemy = { name: 'The Void Lord', hp: 80, maxHp: 80, dmg: 15, defense: 0, stun: 0, burn: 0, poison: 0, isBoss: true, floor: 3 };
document.getElementById('boss-tag').style.display = 'inline-block';
} else {
STATE.enemy = { name: 'Void Warrior', hp: 40, maxHp: 40, dmg: 8, defense: 0, stun: 0, burn: 0, poison: 0, isBoss: false, floor: 3 };
document.getElementById('boss-tag').style.display = 'none';
}
}
// Ensure floor label is correct for boss mechanics tracking
STATE.enemy.floor = STATE.hero.floor;
}
function endTurn() {
if (STATE.turnPhase !== 'player') return;
STATE.hero.defense = 0;
STATE.hero.stunned = false;
STATE.enemy.stun = Math.max(0, STATE.enemy.stun - 1);
STATE.hero.energy = Math.min(STATE.hero.maxEnergy, STATE.hero.energy + 1);
STATE.turnPhase = 'enemy';
setTimeout(() => {
resolveEnemyTurn();
STATE.turnPhase = 'player';
if (STATE.enemy.hp <= 0) { defeatEnemy(); return; }
drawHand();
renderHand();
updateUI();
addLog("Your turn begins.");
}, 400);
drawHand();
renderHand();
updateUI();
addLog("You end your turn.");
}
function openShop() {
document.getElementById('shop-overlay').classList.add('active');
renderShop();
}
function closeShop(e) {
document.getElementById('shop-overlay').classList.remove('active');
}
function renderShop() {
const grid = document.getElementById('shop-grid');
grid.innerHTML = '';
const cards = Object.values(CARDS);
for (const card of cards) {
if (card.class !== STATE.hero.class) continue; // For simplicity, only show class cards (prototype scope)
const count = STATE.hero.inventory[card.id] || 0;
if (count >= STATE.hero.maxCopy) continue;
const cost = count >= 3 ? 10 : 1; // Scaling simplified: 1 each, 4th = 10
const btn = document.createElement('div');
btn.className = 'shop-card';
btn.innerHTML = `
<div style="display:flex; justify-content:space-between;">
<span style="font-size:10px; text-transform:uppercase; color:#888;">${card.type}</span>
<span class="card-cost">${card.cost}</span>
</div>
<h4>${card.name}</h4>
<div class="ability">${card.desc}</div>
<div class="price-tag">${cost} Gold <span style="font-size:10px; color:#777;">(Owned: ${count}/5)</span></div>
`;
btn.onclick = () => buyCard(card);
if (STATE.hero.gold < cost) btn.classList.add('disabled');
grid.appendChild(btn);
}
}
function buyCard(card) {
const count = STATE.hero.inventory[card.id] || 0;
const cost = count >= 3 ? 10 : 1;
if (STATE.hero.gold < cost) {
addLog("Not enough gold to buy " + card.name);
return;
}
STATE.hero.gold -= cost;
STATE.hero.inventory[card.id] = (STATE.hero.inventory[card.id] || 0) + 1;
STATE.hero.deck.push(card.id);
addLog(`Bought ${card.name} for ${cost} Gold!`);
renderShop();
updateUI();
}
function addLog(msg) {
STATE.log.push(msg);
const logEl = document.getElementById('log');
logEl.innerHTML = STATE.log.slice(-12).map(m => `<div class="entry">${m}</div>`).join('');
logEl.scrollTop = logEl.scrollHeight;
}
function updateUI() {
document.getElementById('hud-gold').textContent = STATE.hero.gold;
document.getElementById('hud-floor').textContent = STATE.hero.floor + ' / ' + STATE.hero.room;
document.getElementById('hero-hp').textContent = STATE.hero.hp;
document.getElementById('hero-max-hp').textContent = STATE.hero.maxHp;
document.getElementById('hero-energy').textContent = STATE.hero.energy;
document.getElementById('hero-max-energy').textContent = STATE.hero.maxEnergy;
document.getElementById('hero-defense').textContent = STATE.hero.defense;
document.getElementById('hero-handsize').textContent = STATE.hero.handSize;
document.getElementById('hero-class').textContent = 'The ' + STATE.hero.class.charAt(0).toUpperCase() + STATE.hero.class.slice(1);
const energyContainer = document.getElementById('energy-meter');
energyContainer.innerHTML = '';
for (let i = 0; i < STATE.hero.maxEnergy; i++) {
const orb = document.createElement('div');
orb.className = 'energy-orb' + (i < STATE.hero.energy ? '' : ' empty');
orb.textContent = i + 1;
energyContainer.appendChild(orb);
}
document.getElementById('enemy-name').textContent = STATE.enemy.name;
document.getElementById('enemy-hp-display').textContent = STATE.enemy.hp + ' / ' + STATE.enemy.maxHp;
document.getElementById('enemy-dmg').textContent = STATE.enemy.dmg;
document.getElementById('enemy-def').textContent = STATE.enemy.defense;
document.getElementById('shop-floor').textContent = STATE.hero.floor;
document.getElementById('shop-enemy-type').textContent = STATE.enemy.isBoss ? 'Boss Room' : (STATE.hero.room % 3 === 0 ? 'Boss Room' : 'Enemy Room');
if (STATE.enemy.hp < STATE.enemy.maxHp * 0.25 && STATE.enemy.isBoss && STATE.enemy.floor === 1) {
document.getElementById('enemy-def').textContent = '0';
addLog("Boss 1 defense broken!");
}
document.getElementById('hud-boss-hp').textContent = STATE.enemy.hp;
}
function gameOver() {
document.getElementById('hand-container').innerHTML = `<div style="color:#e85555; font-size:18px; text-align:center;">GAME OVER</div>`;
document.getElementById('log').innerHTML = `<div class="entry dmg">Hero defeated. The shadows consume all.</div>`;
}
window.addEventListener('load', () => {
initGame();
});
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"3d89977d-970d-495a-b00c-12b18965e288","tokensIn":3572,"tokensOut":15886,"tokensTotal":19458,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T20:43:50.825Z"} -->