Deckbuilder
muse spark 1.2 · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 51,170 bytes · SHA-256 b1c17203b38e
<!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 href="https://fonts.googleapis.com/css2?family=Cinzel:wght@700;900&family=Cinzel+Decorative:wght@900&family=JetBrains+Mono:wght@600&family=Crimson+Pro:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
<style>
:root{
--bg:#07090f; --panel:#11131c; --line:#262a3a; --gold:#d8b75a; --gold2:#8a6b2a;
--red:#ff3b3b; --blue:#5fb0ff; --purple:#c57aff; --green:#4ade80;
--text:#e8e6df; --muted:#8a8ea3;
}
*{box-sizing:border-box}
body{margin:0;background:radial-gradient(1200px 600px at 50% -10%, #1a1f35 0%, #07090f 60%), #07090f;color:var(--text);font-family:'Crimson Pro',serif;overflow-x:hidden}
button{font-family:inherit}
.topbar{height:54px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;background:linear-gradient(180deg, #151826, #0e101a);border-bottom:1px solid #2a2e45;position:sticky;top:0;z-index:20}
.brand{font-family:'Cinzel Decorative',serif;letter-spacing:.12em;font-size:18px;color:var(--gold);text-shadow:0 0 12px rgba(216,183,90,.4)}
.topStats{display:flex;gap:10px;align-items:center}
.pill{display:flex;align-items:center;gap:8px;background:#1a1d2d;border:1px solid #2c314e;border-radius:999px;padding:6px 14px;font-family:'JetBrains Mono',monospace;font-size:12px}
.pill b{color:var(--gold)}
.bossHp{width:220px;height:8px;background:#1a1d2d;border-radius:999px;overflow:hidden;border:1px solid #2c314e;display:none}
.bossHp i{display:block;height:100%;background:linear-gradient(90deg,#ff3b3b,#ff7a3b);width:100%}
.layout{display:grid;grid-template-columns:360px 1fr 320px;gap:14px;padding:14px;max-width:1440px;margin:0 auto}
.panel{background:linear-gradient(180deg, rgba(20,23,38,.96), rgba(12,13,20,.98));border:1px solid #242840;border-radius:16px;box-shadow:0 10px 40px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.06);overflow:hidden}
.panelHd{padding:12px 14px;border-bottom:1px solid #242840;display:flex;align-items:center;justify-content:space-between;font-family:'Cinzel',serif;font-size:12px;letter-spacing:.14em;color:#cbd0e6}
.playerArt{height:190px;position:relative;background: radial-gradient(400px 200px at 50% 10%, rgba(216,183,90,.15), transparent 70%), linear-gradient(180deg, #0f1220, #090a12);display:flex;align-items:center;justify-content:center;overflow:hidden;border-bottom:1px solid #242840}
.playerArt::after{content:'';position:absolute;inset:0;background:linear-gradient(180deg, transparent 60%, rgba(0,0,0,.55))}
.statRow{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px}
.statBox{background:#0e101c;border:1px solid #272c44;border-radius:12px;padding:10px}
.statBox label{font-size:10px;letter-spacing:.16em;color:var(--muted);font-family:'JetBrains Mono',monospace}
.bar{height:10px;background:#1a1d2d;border-radius:999px;overflow:hidden;margin-top:6px;border:1px solid #2a2f4f}
.bar i{display:block;height:100%;transition:width .4s}
.hp i{background:linear-gradient(90deg,#ff3b3b,#ff6b6b)}
.en i{background:linear-gradient(90deg,#5fb0ff,#9ad1ff)}
.defBadge{display:inline-flex;align-items:center;gap:6px;background:rgba(95,176,255,.12);border:1px solid rgba(95,176,255,.4);color:#9ad1ff;padding:4px 8px;border-radius:999px;font-size:11px;font-family:'JetBrains Mono',monospace;margin-top:6px}
.handArea{padding:12px}
.handGrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.centerArena{padding:14px;display:flex;flex-direction:column;gap:12px}
.enemyStage{height:340px;background: radial-gradient(600px 300px at 50% 0%, rgba(120,80,255,.18), transparent 70%), linear-gradient(180deg, #0c0f1d 0%, #080a14 100%);border:1px solid #242840;border-radius:16px;position:relative;overflow:hidden;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:18px}
.floorLabel{position:absolute;top:12px;left:14px;font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.18em;color:var(--gold);background:rgba(216,183,90,.1);border:1px solid rgba(216,183,90,.3);padding:4px 8px;border-radius:999px}
.roomDots{position:absolute;top:12px;right:14px;display:flex;gap:6px}
.dot{width:8px;height:8px;border-radius:50%;background:#1e2236;border:1px solid #2e3352}
.dot.on{background:var(--gold);box-shadow:0 0 8px var(--gold)}
.enemySprite{width:140px;height:140px;filter:drop-shadow(0 12px 30px rgba(0,0,0,.8));transition:transform .2s}
.enemySprite.hit{transform:scale(.92) translateY(4px);filter:brightness(1.6)}
.enemyName{font-family:'Cinzel',serif;letter-spacing:.12em;font-size:14px;margin-top:8px}
.enemyHpWrap{width:280px;margin-top:8px}
.enemyHpWrap .bar{height:14px}
.dmgPopup{position:absolute;top:42%;left:50%;transform:translate(-50%,-50%);font-family:'Cinzel',serif;font-size:34px;color:#ff3b3b;text-shadow:0 2px 10px rgba(0,0,0,.8);opacity:0;pointer-events:none}
.dmgPopup.show{animation:pop .6s ease}
@keyframes pop{0%{opacity:0;transform:translate(-50%,0) scale(.8)}20%{opacity:1}100%{opacity:0;transform:translate(-50%,-40px) scale(1.1)}}
.log{height:120px;background:#0b0d18;border:1px solid #242840;border-radius:12px;padding:10px;font-size:13px;line-height:1.4;overflow:auto;color:#b8beda}
.log b{color:var(--gold)}
.actionBar{display:flex;gap:10px}
.btn{flex:1;padding:12px 14px;border-radius:12px;border:1px solid #3a3f5e;background:linear-gradient(180deg, #2a2e4a, #1a1d2d);color:#e8e6df;font-family:'Cinzel',serif;letter-spacing:.1em;font-size:12px;cursor:pointer;transition:.15s}
.btn:hover{transform:translateY(-1px);border-color:var(--gold)}
.btn.primary{background:linear-gradient(180deg, #d8b75a, #8a6b2a);color:#0a0a0f;border-color:#d8b75a;box-shadow:0 6px 20px rgba(216,183,90,.35)}
.btn:disabled{opacity:.4;cursor:not-allowed;transform:none}
.btnSm{padding:8px 12px;border-radius:999px;border:1px solid #2c314e;background:#1a1d2d;color:#cbd0e6;font-family:'JetBrains Mono',monospace;font-size:11px;cursor:pointer}
.energyPanel{padding:14px;display:flex;flex-direction:column;gap:12px}
.energyMeter{display:flex;gap:6px;align-items:end;justify-content:center;padding:10px;background:#0b0d18;border:1px solid #242840;border-radius:12px}
.eOrb{width:22px;border-radius:999px;border:1px solid #3a5a8a;background:linear-gradient(180deg, #5fb0ff, #1e3a5a);transition:height .3s}
.eOrb.off{background:#1a1d2d;border-color:#2c314e;opacity:.5}
.shopBtn{width:100%}
.deckMeta{display:grid;grid-template-columns:1fr 1fr;gap:8px;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted)}
.card{position:relative;height:172px;border-radius:14px;overflow:hidden;cursor:pointer;border:1px solid rgba(255,255,255,.08);background:#0f1220;transition:transform .15s, box-shadow .15s, opacity .15s}
.card:hover{transform:translateY(-4px) scale(1.02);box-shadow:0 12px 30px rgba(0,0,0,.6)}
.card.disabled{opacity:.45;filter:grayscale(.3);cursor:not-allowed}
.card.disabled:hover{transform:none;box-shadow:none}
.cardInner{position:absolute;inset:0;padding:10px;display:flex;flex-direction:column}
.cardTop{display:flex;justify-content:space-between;align-items:center}
.cardCost{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;font-family:'Cinzel',serif;font-weight:900;font-size:13px;border:2px solid rgba(255,255,255,.9);box-shadow:0 2px 8px rgba(0,0,0,.5)}
.cardId{font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:.1em;color:rgba(255,255,255,.7);background:rgba(0,0,0,.35);padding:2px 6px;border-radius:999px}
.cardArt{flex:1;margin:8px 0;background:rgba(0,0,0,.22);border-radius:10px;border:1px solid rgba(255,255,255,.08);display:grid;place-items:center;position:relative;overflow:hidden}
.cardArt span{font-size:30px;filter:drop-shadow(0 2px 6px rgba(0,0,0,.6))}
.cardName{font-family:'Cinzel',serif;font-size:11px;letter-spacing:.06em;line-height:1.1;text-align:center}
.cardDesc{font-size:11px;line-height:1.25;text-align:center;color:rgba(255,255,255,.85);margin-top:4px;min-height:28px}
.cardStats{display:flex;justify-content:center;gap:6px;margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:10px}
.statPill{padding:2px 6px;border-radius:999px;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.15)}
.typeAttack{box-shadow:inset 0 0 0 1px rgba(255,59,59,.5), 0 0 18px rgba(255,59,59,.25)}
.typeDefense{box-shadow:inset 0 0 0 1px rgba(95,176,255,.5), 0 0 18px rgba(95,176,255,.25)}
.typeOther{box-shadow:inset 0 0 0 1px rgba(197,122,255,.5), 0 0 18px rgba(197,122,255,.25)}
.selectScreen{max-width:1100px;margin:22px auto;padding:0 14px}
.heroPick{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:16px}
.pickCard{padding:16px;border-radius:16px;background:linear-gradient(180deg, #14172a, #0d0f1d);border:1px solid #2a2e45;cursor:pointer;transition:.15s;text-align:center}
.pickCard:hover{transform:translateY(-4px);border-color:var(--gold);box-shadow:0 12px 30px rgba(0,0,0,.5)}
.pickCard h3{font-family:'Cinzel',serif;margin:8px 0 4px;letter-spacing:.1em}
.pickCard p{font-size:13px;color:var(--muted);line-height:1.4}
.badge{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.12em;padding:4px 8px;border-radius:999px;border:1px solid}
.modal{position:fixed;inset:0;background:rgba(3,5,12,.78);backdrop-filter:blur(6px);display:grid;place-items:center;z-index:50;padding:16px}
.modalBox{width:min(1100px,100%);max-height:90vh;overflow:auto;background:linear-gradient(180deg, #151826, #0b0d18);border:1px solid #2e3352;border-radius:18px;box-shadow:0 20px 60px rgba(0,0,0,.7)}
.shopGrid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;padding:14px}
@media(max-width:1000px){.layout{grid-template-columns:1fr}.heroPick{grid-template-columns:1fr}.shopGrid{grid-template-columns:repeat(2,1fr)}.topbar{flex-wrap:wrap;height:auto;padding:10px;gap:8px}}
.cardBack{width:100%;height:100%;display:grid;place-items:center;background:radial-gradient(300px 200px at 50% 0%, rgba(216,183,90,.12), transparent), #0a0e1a;border:1px solid #2a2e45;border-radius:14px}
</style>
</head>
<body>
<div class="topbar">
<div class="brand">SHADOWS OF AETHELGARD</div>
<div class="topStats">
<div class="pill">◉ GOLD <b id="goldTxt">0</b></div>
<div class="pill">⧉ FLOOR <b id="floorTxt">1</b>-<span id="roomTxt">1</span></div>
<div class="pill" id="bossPill" style="display:none">☠ BOSS <div class="bossHp" id="bossHpWrap"><i id="bossHpFill"></i></div></div>
<button class="btnSm" onclick="openShop()">SHOP [G]</button>
<button class="btnSm" onclick="resetGame()">RESET</button>
</div>
</div>
<section id="selectScreen" class="selectScreen">
<div style="text-align:center;padding:18px 0 6px">
<div style="font-family:'Cinzel',serif;letter-spacing:.18em;color:var(--gold);font-size:12px">CHOOSE YOUR VESSEL</div>
<h1 style="font-family:'Cinzel Decorative',serif;font-size:38px;margin:8px 0 6px;letter-spacing:.06em">ENTER THE VOID</h1>
<p style="color:var(--muted);max-width:680px;margin:0 auto">Three floors. Nine chambers. Three Void Lords. Master block math, burn ticks and energy tempo to survive.</p>
</div>
<div class="heroPick">
<div class="pickCard" onclick="startGame('bastion')">
<div style="width:100%;height:140px;border-radius:12px;background:linear-gradient(180deg,#2a2a30,#1a1a1f);display:grid;place-items:center;border:1px solid #3a352a">
<!-- Bastion SVG Frame preview -->
<svg width="110" height="110" viewBox="0 0 100 100"><rect x="6" y="6" width="88" height="88" rx="6" fill="#1c1a16" stroke="#d8b75a" stroke-width="2"/><path d="M6 18 L18 6 L26 6 L18 18 Z M74 6 L94 6 L94 26 L82 18 Z M6 82 L18 94 L6 94 Z M94 74 L94 94 L82 94 Z" fill="#d8b75a"/><circle cx="50" cy="50" r="28" fill="none" stroke="#8a6b2a" stroke-width="1.2" stroke-dasharray="3 3"/><text x="50" y="58" text-anchor="middle" font-size="32">🛡️</text></svg>
</div>
<h3>THE BASTION</h3><div class="badge" style="color:#d8b75a;border-color:#d8b75a">IMMUTABLE • 35 HP • 3 ENERGY</div>
<p>High defense, permanent Iron Skin and AoE Earthquake. Taunt stuns. Forgives mistakes.</p>
<div style="margin-top:10px;display:flex;gap:6px;justify-content:center;flex-wrap:wrap"><span class="statPill">Starter: Slam, Stone Fist, Shield Wall x2</span></div>
</div>
<div class="pickCard" onclick="startGame('wither')">
<div style="width:100%;height:140px;border-radius:12px;background:linear-gradient(180deg,#1a1f1a,#111610);display:grid;place-items:center;border:1px solid #2a3a2a">
<svg width="110" height="110" viewBox="0 0 100 100"><rect x="6" y="6" width="88" height="88" rx="14" fill="#141a14" stroke="#4ade80" stroke-width="2"/><path d="M50 6 C70 18 88 30 88 50 C88 74 70 88 50 94 C30 88 12 74 12 50 C12 30 30 18 50 6Z" fill="none" stroke="#4ade80" stroke-width="1.2" opacity=".6"/><text x="50" y="58" text-anchor="middle" font-size="32">🔥</text></svg>
</div>
<h3>THE WITHER</h3><div class="badge" style="color:#4ade80;border-color:#4ade80">ROT & BURN • 30 HP • 5 ENERGY</div>
<p>Burn & Poison tick each turn. Inferno & Pyre spread rot. Control the clock.</p>
<div style="margin-top:10px;display:flex;gap:6px;justify-content:center;flex-wrap:wrap"><span class="statPill">Starter: Fireball, Ember, Scorch Shield</span></div>
</div>
<div class="pickCard" onclick="startGame('shroud')">
<div style="width:100%;height:140px;border-radius:12px;background:linear-gradient(180deg,#1a1830,#0f0e1a);display:grid;place-items:center;border:1px solid #3a2e5a">
<svg width="110" height="110" viewBox="0 0 100 100"><rect x="6" y="6" width="88" height="88" rx="2" fill="#181525" stroke="#c57aff" stroke-width="2"/><path d="M50 8 L88 22 L88 78 L50 92 L12 78 L12 22 Z" fill="none" stroke="#c57aff" stroke-width="1.2"/><circle cx="50" cy="50" r="26" fill="none" stroke="#c57aff" stroke-dasharray="2 4"/><text x="50" y="58" text-anchor="middle" font-size="32">🗡️</text></svg>
</div>
<h3>THE SHROUD</h3><div class="badge" style="color:#c57aff;border-color:#c57aff">SHADOW • 28 HP • 5 ENERGY</div>
<p>Backstab crit vs no armor, Evasion skip, Vengeance refunds energy. Kill before you are hit.</p>
<div style="margin-top:10px;display:flex;gap:6px;justify-content:center;flex-wrap:wrap"><span class="statPill">Starter: Backstab, Step, Dash</span></div>
</div>
</div>
<div style="margin-top:16px;background:#0f1220;border:1px solid #2a2e45;border-radius:12px;padding:12px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;font-size:13px;color:#b8beda">
<div><b style="color:var(--gold)">Combat Math</b><br>Damage = Card Attack. Block = Incoming − Defense. Energy 1-3 per card.</div>
<div><b style="color:var(--gold)">Economy</b><br>5 Gold per kill, 15 per Boss. Shop 1 Gold; 4th copy costs 10 (max 5).</div>
<div><b style="color:var(--gold)">Status</b><br>Burn/Poison 1 dmg/turn, Stun skip attack, Silence block.</div>
</div>
</section>
<section id="gameScreen" style="display:none">
<div class="layout">
<div class="panel">
<div class="panelHd"><span>HERO</span><span id="classLabel" style="color:var(--gold)">BASTION</span></div>
<div class="playerArt" id="playerArt">
<svg width="200" height="150" viewBox="0 0 200 150" style="z-index:1">
<ellipse cx="100" cy="135" rx="55" ry="10" fill="rgba(0,0,0,.4)"/>
<g id="heroSvg"></g>
</svg>
<div style="position:absolute;bottom:10px;left:10px;right:10px;display:flex;gap:8px;z-index:2">
<div class="pill" style="padding:4px 8px;font-size:11px">HAND <b id="handCount">0</b>/ <span id="handSizeTxt">4</span></div>
<div class="pill" style="padding:4px 8px;font-size:11px">DECK <b id="deckCount">0</b></div>
<div class="pill" style="padding:4px 8px;font-size:11px">DISCARD <b id="discardCount">0</b></div>
</div>
</div>
<div class="statRow">
<div class="statBox"><label>HEALTH</label><div class="bar hp"><i id="hpFill" style="width:100%"></i></div><div style="display:flex;justify-content:space-between;margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:12px"><span id="hpTxt">35 / 35</span><span id="defTxt" class="defBadge" style="display:none">🛡️ 0</span></div></div>
<div class="statBox"><label>ENERGY</label><div class="bar en"><i id="enFill" style="width:60%"></i></div><div style="margin-top:6px;font-family:'JetBrains Mono',monospace;font-size:12px"><span id="enTxt">3 / 3</span><span style="color:var(--muted);font-size:10px"> +1 per turn</span></div></div>
</div>
<div class="handArea">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px"><span style="font-family:'Cinzel',serif;letter-spacing:.12em;font-size:11px;color:#cbd0e6">HAND — CLICK TO PLAY</span><span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted)" id="permDefTxt">PERM DEF 0</span></div>
<div class="handGrid" id="handGrid"></div>
<div style="margin-top:8px;display:flex;gap:8px"><button class="btnSm" onclick="redrawHand()">MULLIGAN (1E)</button><button class="btnSm" onclick="openShop()">VIEW DECK</button></div>
</div>
</div>
<div class="centerArena panel" style="padding:0">
<div class="enemyStage" id="enemyStage">
<div class="floorLabel" id="floorLabel">FLOOR 1 — CHAMBER 1/4</div>
<div class="roomDots" id="roomDots"></div>
<div id="dmgPopup" class="dmgPopup">-6</div>
<div id="enemySprite" class="enemySprite"></div>
<div class="enemyName" id="enemyName">GOBLIN SCOUT</div>
<div style="display:flex;gap:6px;margin-top:6px" id="statusRow"></div>
<div class="enemyHpWrap">
<div style="display:flex;justify-content:space-between;font-family:'JetBrains Mono',monospace;font-size:11px;margin-bottom:4px"><span id="enemyHpTxt">20 / 20</span><span id="enemyDmgTxt" style="color:#ff7a7a">ATK 3</span></div>
<div class="bar"><i id="enemyHpFill" style="width:100%;background:linear-gradient(90deg,#ff3b3b,#ff9a3b)"></i></div>
</div>
<div id="bossMechanic" style="margin-top:8px;font-size:12px;color:#ffcc66;text-align:center;min-height:16px"></div>
</div>
<div style="padding:12px;display:flex;flex-direction:column;gap:10px">
<div class="log" id="log"></div>
<div class="actionBar">
<button class="btn primary" id="endTurnBtn" onclick="endTurn()">END TURN ↵</button>
<button class="btn" onclick="openShop()">SHOP</button>
</div>
</div>
</div>
<div class="panel">
<div class="panelHd"><span>ENERGY & ACTIONS</span><span style="color:var(--muted)">TURN <b id="turnTxt" style="color:var(--text)">1</b></span></div>
<div class="energyPanel">
<div class="energyMeter" id="energyMeter"></div>
<button class="btn shopBtn" onclick="openShop()">CARDS SHOP — 1G</button>
<div class="deckMeta">
<div>Gold<br><b id="goldSide" style="color:var(--gold);font-size:16px">0</b></div>
<div>Copies<br><b id="copyInfo" style="color:#cbd0e6">max 5 / card</b></div>
</div>
<div style="background:#0b0d18;border:1px solid #242840;border-radius:12px;padding:10px">
<div style="font-family:'Cinzel',serif;font-size:11px;letter-spacing:.1em;color:var(--gold)">NEXT REWARD</div>
<div id="rewardTxt" style="font-size:13px;color:#b8beda;margin-top:4px">Defeat foe → +5 Gold</div>
<div style="height:1px;background:#242840;margin:8px 0"></div>
<div style="font-size:12px;color:var(--muted)">Tip: Defense is spent first. Burn/Poison ticks at start of enemy turn. Stun skips attack.</div>
</div>
<button class="btn" onclick="nextRoom()" id="nextBtn" style="display:none">NEXT CHAMBER →</button>
</div>
</div>
</div>
</section>
<div id="shopModal" class="modal" style="display:none">
<div class="modalBox">
<div style="padding:14px 16px;border-bottom:1px solid #2a2e45;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;background:#151826;z-index:2">
<div style="font-family:'Cinzel',serif;letter-spacing:.14em;color:var(--gold)">AETHELGARD BAZAAR — <span id="shopGold">0G</span></div>
<div style="display:flex;gap:8px"><button class="btnSm" onclick="closeShop()">CLOSE ✕</button></div>
</div>
<div style="padding:10px 14px;display:flex;gap:8px;flex-wrap:wrap">
<button class="btnSm" onclick="shopFilter='all';renderShop()">ALL</button>
<button class="btnSm" onclick="shopFilter='Attack';renderShop()">ATTACK</button>
<button class="btnSm" onclick="shopFilter='Defense';renderShop()">DEFENSE</button>
<button class="btnSm" onclick="shopFilter='Other';renderShop()">OTHER</button>
<span style="margin-left:auto;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted)">4th copy = 10G • Max 5</span>
</div>
<div class="shopGrid" id="shopGrid"></div>
</div>
</div>
<div id="endModal" class="modal" style="display:none">
<div style="background:linear-gradient(180deg,#1a1d2d,#0b0d18);border:1px solid #d8b75a;border-radius:16px;padding:24px;text-align:center;max-width:520px;width:90%">
<div id="endTitle" style="font-family:'Cinzel Decorative',serif;font-size:32px;color:var(--gold)">VICTORY</div>
<p id="endDesc" style="color:#b8beda">You cleared the Void.</p>
<div style="display:flex;gap:10px;justify-content:center;margin-top:16px"><button class="btn primary" onclick="resetGame()">PLAY AGAIN</button><button class="btn" onclick="closeEnd()">CLOSE</button></div>
</div>
</div>
<script>
const CARD_DB = [
// Bastion
{id:'BA-A01', cls:'bastion', type:'Attack', cost:2, atk:3, def:0, name:'SLAM', desc:'Deal 3 dmg. If HP ≥30, draw 1.', icon:'🔨'},
{id:'BA-A02', cls:'bastion', type:'Attack', cost:3, atk:4, def:0, name:'EARTHQUAKE', desc:'Deal 4 dmg to all enemies.', icon:'🌋'},
{id:'BA-A03', cls:'bastion', type:'Attack', cost:1, atk:2, def:0, name:'STONE FIST', desc:'Deal 2 dmg.', icon:'🪨'},
{id:'BA-A04', cls:'bastion', type:'Attack', cost:4, atk:6, def:0, name:'BOULDER', desc:'Deal 6 dmg. Heavy impact.', icon:'🗿'},
{id:'BA-D01', cls:'bastion', type:'Defense', cost:0, atk:0, def:4, name:'SHIELD WALL', desc:'Gain 4 Block this turn.', icon:'🛡️'},
{id:'BA-D02', cls:'bastion', type:'Defense', cost:2, atk:0, def:5, name:'IRON SKIN', desc:'Gain 5 Block permanently. Discard 1.', icon:'⚙️'},
{id:'BA-D03', cls:'bastion', type:'Defense', cost:1, atk:0, def:2, name:'STAND FIRM', desc:'Gain 2 Block.', icon:'🦶'},
{id:'BA-D04', cls:'bastion', type:'Defense', cost:2, atk:0, def:3, name:'TAUNT', desc:'Gain 3 Block. Stun enemy 1 turn.', icon:'📢'},
{id:'BA-U01', cls:'bastion', type:'Other', cost:2, atk:0, def:0, name:'SEARCH', desc:'Draw 1. Discard if hand >5.', icon:'🔍'},
{id:'BA-U02', cls:'bastion', type:'Other', cost:3, atk:0, def:0, name:'REPAIR', desc:'Heal 6 HP.', icon:'🔧'},
{id:'BA-U03', cls:'bastion', type:'Other', cost:1, atk:0, def:0, name:'RALLY', desc:'Draw 1. Gain 1 Energy.', icon:'⚡'},
{id:'BA-U04', cls:'bastion', type:'Other', cost:2, atk:0, def:0, name:'LOOT', desc:'Gain 5 Gold.', icon:'💰'},
// Wither
{id:'WI-A01', cls:'wither', type:'Attack', cost:2, atk:3, def:0, name:'FIREBALL', desc:'Deal 3 + 1 Burn.', icon:'🔥'},
{id:'WI-A02', cls:'wither', type:'Attack', cost:3, atk:4, def:0, name:'INFERNO', desc:'Deal 4 + 3 Burn.', icon:'🌞'},
{id:'WI-A03', cls:'wither', type:'Attack', cost:1, atk:2, def:0, name:'EMBER', desc:'Deal 2 + 1 Burn/turn.', icon:'✨'},
{id:'WI-A04', cls:'wither', type:'Attack', cost:4, atk:5, def:0, name:'PYRE', desc:'Deal 5. All foes +1 Burn next turn.', icon:'🕯️'},
{id:'WI-D01', cls:'wither', type:'Defense', cost:2, atk:0, def:3, name:'SCORCH SHIELD', desc:'Gain 3 Block. Block Burn.', icon:'🔥🛡️'},
{id:'WI-D02', cls:'wither', type:'Defense', cost:1, atk:0, def:1, name:'HEAT RESIST', desc:'Gain 1 Block. Gain 1 Energy.', icon:'♨️'},
{id:'WI-D03', cls:'wither', type:'Defense', cost:2, atk:0, def:4, name:'SMOKE', desc:'Gain 4 Block.', icon:'💨'},
{id:'WI-D04', cls:'wither', type:'Defense', cost:3, atk:0, def:2, name:'ENDURE', desc:'Gain 2 Block. Heal 2 HP.', icon:'❤️🩹'},
{id:'WI-U01', cls:'wither', type:'Other', cost:2, atk:0, def:0, name:'POISON', desc:'Apply 1 Poison.', icon:'🧪'},
{id:'WI-U02', cls:'wither', type:'Other', cost:3, atk:0, def:0, name:'BURN', desc:'Deal 3 + 2 Burn to all.', icon:'☠️'},
{id:'WI-U03', cls:'wither', type:'Other', cost:1, atk:0, def:0, name:'SLOW', desc:'Draw 1. Stun target.', icon:'🕸️'},
{id:'WI-U04', cls:'wither', type:'Other', cost:2, atk:0, def:0, name:'WITHER', desc:'Heal 5. Enemy -1 ATK.', icon:'🥀'},
// Shroud
{id:'SH-A01', cls:'shroud', type:'Attack', cost:2, atk:4, def:0, name:'BACKSTAB', desc:'Deal 4. +1 if foe has no Block.', icon:'🗡️'},
{id:'SH-A02', cls:'shroud', type:'Attack', cost:3, atk:5, def:0, name:'SHADOW BOLT', desc:'Deal 5. Unblockable.', icon:'🌑'},
{id:'SH-A03', cls:'shroud', type:'Attack', cost:1, atk:3, def:0, name:'SILENCE', desc:'Deal 3. Foe silenced.', icon:'🤫'},
{id:'SH-A04', cls:'shroud', type:'Attack', cost:3, atk:6, def:0, name:'VENGEANCE', desc:'Deal 6. Gain 2 Energy.', icon:'⚔️'},
{id:'SH-D01', cls:'shroud', type:'Defense', cost:3, atk:0, def:2, name:'BLUR', desc:'Gain 2 Block. Ignore next attack.', icon:'👁️'},
{id:'SH-D02', cls:'shroud', type:'Defense', cost:2, atk:0, def:3, name:'STEP', desc:'Gain 3 Block. +1 Energy.', icon:'👟'},
{id:'SH-D03', cls:'shroud', type:'Defense', cost:1, atk:0, def:1, name:'EVASION', desc:'Gain 1 Block. Skip enemy turn.', icon:'💨'},
{id:'SH-D04', cls:'shroud', type:'Defense', cost:2, atk:0, def:4, name:'PHANTOM', desc:'Gain 4 Block. Ghost 1 turn.', icon:'👻'},
{id:'SH-U01', cls:'shroud', type:'Other', cost:1, atk:0, def:0, name:'DASH', desc:'Gain 1 Energy. Skip attack.', icon:'🏃'},
{id:'SH-U02', cls:'shroud', type:'Other', cost:2, atk:0, def:0, name:'TELEPORT', desc:'Deal 2. Reposition.', icon:'🌀'},
{id:'SH-U03', cls:'shroud', type:'Other', cost:2, atk:0, def:0, name:'HIDE', desc:'+1 Block. Discard 1, Draw 2.', icon:'🎭'},
{id:'SH-U04', cls:'shroud', type:'Other', cost:3, atk:0, def:0, name:'ASSASSINATE', desc:'Deal 8. Unblockable.', icon:'☠️'},
];
const cardMap={}; CARD_DB.forEach(c=>cardMap[c.id]=c);
let state={
cls:null, hp:35, maxHp:35, energy:3, maxEnergy:3, handSize:4, gold:0, permDef:0, tempDef:0,
deck:[], drawPile:[], discard:[], hand:[], inventory:{},
floor:1, room:1, turn:1, enemy:null, skipEnemy:false, ghost:false, log:[],
};
let shopFilter='all';
function makeDeck(cls){
let starter=[];
if(cls==='bastion') starter=['BA-A01','BA-A03','BA-D01','BA-D01','BA-U03','BA-D03','BA-U02','BA-A03'];
if(cls==='wither') starter=['WI-A01','WI-A03','WI-D02','WI-U01','WI-U04','WI-A02','WI-D03','WI-U03'];
if(cls==='shroud') starter=['SH-A01','SH-A03','SH-D02','SH-U01','SH-D03','SH-A04','SH-U03','SH-D04'];
starter.forEach(id=>state.inventory[id]=(state.inventory[id]||0)+1);
return starter;
}
function shuffle(a){for(let i=a.length-1;i>0;i--){let j=Math.floor(Math.random()* (i+1));[a[i],a[j]]=[a[j],a[i]]}return a}
function startGame(cls){
state.cls=cls;
if(cls==='bastion'){state.maxHp=35;state.hp=35;state.maxEnergy=3;state.energy=3}
if(cls==='wither'){state.maxHp=30;state.hp=30;state.maxEnergy=5;state.energy=5}
if(cls==='shroud'){state.maxHp=28;state.hp=28;state.maxEnergy=5;state.energy=5}
state.handSize=4; state.gold=0; state.permDef=0; state.tempDef=0; state.floor=1; state.room=1; state.turn=1;
state.inventory={}; state.deck=makeDeck(cls); state.drawPile=shuffle([...state.deck]); state.discard=[]; state.hand=[];
document.getElementById('selectScreen').style.display='none';
document.getElementById('gameScreen').style.display='block';
document.getElementById('classLabel').textContent=cls.toUpperCase();
spawnEnemy();
drawUpToHand();
updateUI(); logMsg(`Entered as <b>${cls.toUpperCase()}</b>. Floor 1 awaits.`);
}
function enemyForFloor(f, isBoss){
if(isBoss){
if(f===1) return {name:'THE GATEKEEPER', hp:80, maxHp:80, dmg:15, isBoss:true, block:5, id:'boss1'};
if(f===2) return {name:'THE CORRUPTED', hp:80, maxHp:80, dmg:15, isBoss:true, id:'boss2'};
return {name:'THE VOID LORD', hp:40, maxHp:40, dmg:15, isBoss:true, id:'boss3', regen:10};
}
if(f===1) return {name:['GOBLIN SCOUT','VOID RAT','CULT ACOLYTE'][Math.floor(Math.random()*3)], hp:20, maxHp:20, dmg:3, burn:0, poison:0, stun:0};
if(f===2) return {name:['VAMPIRE KNIGHT','OGRE BRUTE','VOID SENTINEL'][Math.floor(Math.random()*3)], hp:30, maxHp:30, dmg:5, burn:0, poison:0, stun:0};
return {name:['VOID WARRIOR','CULTIST LORD','ABYSS SPAWN'][Math.floor(Math.random()*3)], hp:40, maxHp:40, dmg:8, burn:0, poison:0, stun:0};
}
function spawnEnemy(){
const isBoss = state.room===4;
state.enemy=enemyForFloor(state.floor, isBoss);
state.enemy.burn=state.enemy.burn||0; state.enemy.poison=state.enemy.poison||0; state.enemy.stun=0;
state.skipEnemy=false; state.ghost=false;
document.getElementById('nextBtn').style.display='none';
document.getElementById('endTurnBtn').disabled=false;
updateEnemyUI(true);
}
function drawCard(){
if(state.drawPile.length===0){
if(state.discard.length===0) return null;
state.drawPile=shuffle([...state.discard]); state.discard=[];
}
return state.drawPile.pop();
}
function drawUpToHand(){
while(state.hand.length < state.handSize){
const c=drawCard(); if(!c) break; state.hand.push(c);
}
}
function cardCost(id){
const count=state.inventory[id]||0;
if(count>=5) return 99;
if(count>=3) return 10;
return 1;
}
function getDefense(){return state.permDef + state.tempDef}
function playCard(idx){
const cid = state.hand[idx];
const card = cardMap[cid];
if(state.energy < card.cost){ logMsg(`Not enough energy for ${card.name}.`); return; }
if(state.enemy.hp<=0) return;
state.energy -= card.cost;
state.hand.splice(idx,1);
state.discard.push(cid);
resolveCard(card);
if(state.enemy.hp<=0){ onEnemyDefeat(); }
updateUI();
}
function resolveCard(c){
logMsg(`Played <b>${c.name}</b> — ${c.desc}`);
// Attack
if(c.type==='Attack'){
let dmg=c.atk;
// specific bonuses
if(c.id==='SH-A01' && getDefense()===0){ /* bastion check? actually if target has no armor = enemy block 0 */ if((state.enemy.block||0)===0) dmg+=1; }
if(c.id==='SH-A02' || c.id==='SH-U04'){ // unblockable
state.enemy.hp -= dmg;
} else {
let block = state.enemy.block||0;
if(block>0){ let absorbed=Math.min(block,dmg); block-=absorbed; dmg-=absorbed; state.enemy.block=block; if(dmg>0) state.enemy.hp-=dmg; }
else state.enemy.hp-=dmg;
}
// burn applications
if(c.id==='WI-A01'){ state.enemy.burn=(state.enemy.burn||0)+1 }
if(c.id==='WI-A02'){ state.enemy.burn=(state.enemy.burn||0)+3 }
if(c.id==='WI-A03'){ state.enemy.burn=(state.enemy.burn||0)+1 }
if(c.id==='WI-A04'){ state.enemy.burn=(state.enemy.burn||0)+1 }
if(c.id==='WI-U02'){ state.enemy.burn=(state.enemy.burn||0)+2; // also deal 3 already counted? we did atk 0, so add
state.enemy.hp-=3; dmg+=3; }
if(c.id==='WI-U01'){ state.enemy.poison=(state.enemy.poison||0)+1 }
if(c.id==='BA-A01' && state.hp>=30){ const cc=drawCard(); if(cc) state.hand.push(cc); logMsg(`Slam draws 1!`)}
if(c.id==='BA-A02'){ /* aoe already single */ }
if(c.id==='SH-A04'){ state.energy = Math.min(state.maxEnergy, state.energy+2) }
if(c.id==='SH-U02'){ state.enemy.hp-=2 }
if(c.id==='SH-U04'){ /* 8 dmg unblockable already */ }
showDmg(dmg);
animHit();
}
// Defense
if(c.type==='Defense'){
if(c.id==='BA-D02'){ state.permDef+=c.def; // discard 1
if(state.hand.length>0){ let disc=state.hand.pop(); state.discard.push(disc); logMsg(`Iron Skin discarded ${cardMap[disc].name}`)}
} else {
state.tempDef+=c.def;
}
if(c.id==='BA-D04'){ state.enemy.stun=1; logMsg(`Enemy stunned!`)}
if(c.id==='WI-D02'){ state.energy=Math.min(state.maxEnergy, state.energy+1)}
if(c.id==='WI-D04'){ state.hp=Math.min(state.maxHp, state.hp+2)}
if(c.id==='SH-D01'){ state.ghost=true; logMsg(`Blur: next attack ignored.`)}
if(c.id==='SH-D02'){ state.energy=Math.min(state.maxEnergy, state.energy+1)}
if(c.id==='SH-D03' || c.id==='SH-U01'){ state.skipEnemy=true; logMsg(`Evasion: enemy will skip.`)}
if(c.id==='SH-D04'){ state.ghost=true }
}
if(c.type==='Other'){
if(c.id==='BA-U01'){ const cc=drawCard(); if(cc) state.hand.push(cc); if(state.hand.length>5){ state.hand.pop(); logMsg(`Search discarded excess.`)}}
if(c.id==='BA-U02'){ state.hp=Math.min(state.maxHp, state.hp+6)}
if(c.id==='BA-U03'){ const cc=drawCard(); if(cc) state.hand.push(cc); state.energy=Math.min(state.maxEnergy, state.energy+1)}
if(c.id==='BA-U04'){ state.gold+=5}
if(c.id==='WI-U03'){ const cc=drawCard(); if(cc) state.hand.push(cc); state.enemy.stun=1}
if(c.id==='WI-U04'){ state.hp=Math.min(state.maxHp, state.hp+5); state.enemy.dmg=Math.max(1, state.enemy.dmg-1)}
if(c.id==='WI-D02'){} // handled
if(c.id==='SH-U01'){ state.energy=Math.min(state.maxEnergy, state.energy+1); state.skipEnemy=true}
if(c.id==='SH-U03'){ state.tempDef+=1; if(state.hand.length>0){ let d=state.hand.pop(); state.discard.push(d)}; for(let i=0;i<2;i++){let cc=drawCard(); if(cc) state.hand.push(cc)}}
if(c.id==='SH-U02'){ /* dmg handled */ }
if(c.id==='WI-U02'){ /* already */ }
}
updateEnemyUI();
}
function showDmg(n){ const el=document.getElementById('dmgPopup'); el.textContent='-'+n; el.classList.remove('show'); void el.offsetWidth; el.classList.add('show')}
function animHit(){ const s=document.getElementById('enemySprite'); s.classList.add('hit'); setTimeout(()=>s.classList.remove('hit'),220)}
function endTurn(){
if(state.enemy.hp<=0) return;
// enemy start tick: burn/poison damage to enemy
let tick = (state.enemy.burn||0) + (state.enemy.poison||0);
if(tick>0){ state.enemy.hp-=tick; logMsg(`Burn/Poison ticks ${tick} dmg.`); showDmg(tick); if(state.enemy.hp<=0){onEnemyDefeat(); updateUI(); return;} }
// enemy action
if(state.enemy.stun>0){ logMsg(`${state.enemy.name} is stunned.`); state.enemy.stun--; }
else if(state.skipEnemy){ logMsg(`Enemy attack evaded!`); state.skipEnemy=false; }
else if(state.ghost){ logMsg(`Ghost form ignores attack.`); state.ghost=false; }
else {
let dmg = state.enemy.dmg;
// boss2 mechanic
if(state.enemy.id==='boss2' && state.energy<3){ dmg=20; logMsg(`Corrupted bursts for 20! (<3 Energy)`)}
// boss1 block
if(state.enemy.id==='boss1' && state.enemy.hp>=20){ dmg=Math.max(0, dmg-0); /* boss blocks 5 dmg per turn is enemy block, handled on player attack */ }
let defense=getDefense();
let taken=Math.max(0, dmg - defense);
// consume temp defense first
if(state.tempDef>0){ let use=Math.min(state.tempDef, dmg); state.tempDef-=use; }
// perm defense stays? Actually perm should also absorb but not disappear? Spec says permanent defense remains. So don't reduce perm.
// For simplicity, perm absorbs but remains.
if(defense>0 && taken < dmg) logMsg(`Blocked ${dmg-taken} damage.`);
state.hp-=taken;
if(taken>0) logShake();
logMsg(`Enemy strikes for ${taken} damage.`);
if(state.hp<=0){ gameOver(false); return; }
}
// boss3 regen
if(state.enemy.id==='boss3'){ state.enemy.hp=Math.min(state.enemy.maxHp+100, state.enemy.hp+10); logMsg(`Void Lord regenerates +10 HP.`)}
// reset temp def if not permanent? Keep only perm
// In our model temp should reset each turn
state.tempDef=0;
// regen energy
state.energy=Math.min(state.maxEnergy, state.energy+1);
state.turn++;
// draw
drawUpToHand();
// boss1 block refresh
if(state.enemy.id==='boss1' && state.enemy.hp>=20) state.enemy.block=5;
updateUI(); updateEnemyUI();
}
function logShake(){ document.getElementById('enemyStage').animate([{transform:'translateX(0)'},{transform:'translateX(-6px)'},{transform:'translateX(6px)'},{transform:'translateX(0)'}],{duration:200})}
function onEnemyDefeat(){
let gold = state.enemy.isBoss ? 15 : 5;
state.gold+=gold;
logMsg(`<b>Victory!</b> +${gold} Gold.`);
state.enemy.hp=0;
updateEnemyUI();
document.getElementById('nextBtn').style.display='block';
document.getElementById('endTurnBtn').disabled=true;
if(state.enemy.isBoss && state.floor===3){ setTimeout(()=>gameOver(true),600)}
}
function nextRoom(){
if(state.enemy.isBoss){
if(state.floor<3){ state.floor++; state.room=1; logMsg(`Ascending to Floor ${state.floor}...`)}
else{ gameOver(true); return}
} else {
state.room++;
if(state.room>4){ state.room=4 } // boss
}
state.turn=1;
spawnEnemy();
// keep deck/hand but draw up
drawUpToHand();
updateUI();
}
function gameOver(win){
const m=document.getElementById('endModal');
m.style.display='grid';
document.getElementById('endTitle').textContent = win ? 'FLOOR CLEARED' : 'YOU HAVE FALLEN';
document.getElementById('endTitle').style.color = win ? 'var(--gold)' : '#ff5a5a';
document.getElementById('endDesc').innerHTML = win ? `You defeated the Void Lord on Floor 3!<br>Gold: ${state.gold} • Turns: ${state.turn}` : `The Void consumes you on Floor ${state.floor} Room ${state.room}.<br>Gold: ${state.gold}`;
}
function closeEnd(){document.getElementById('endModal').style.display='none'}
function resetGame(){ location.reload() }
function logMsg(t){ state.log.unshift(t); if(state.log.length>30) state.log.pop(); document.getElementById('log').innerHTML = state.log.map(x=>`• ${x}`).join('<br>')}
function renderHand(){
const g=document.getElementById('handGrid'); g.innerHTML='';
state.hand.forEach((cid,i)=>{
const c=cardMap[cid];
const can = state.energy >= c.cost && state.enemy.hp>0;
const div=document.createElement('div');
div.className=`card ${can?'':'disabled'} ${c.type==='Attack'?'typeAttack':c.type==='Defense'?'typeDefense':'typeOther'}`;
// frame color per class + type glow already
let bg='';
if(c.cls==='bastion') bg='linear-gradient(180deg,#2b2216,#1a150f)';
if(c.cls==='wither') bg='linear-gradient(180deg,#1a2520,#111a14)';
if(c.cls==='shroud') bg='linear-gradient(180deg,#211a33,#151022)';
div.style.background=bg;
div.innerHTML=`
<div class="cardInner">
<div class="cardTop"><span class="cardCost" style="background:${c.type==='Attack'?'linear-gradient(180deg,#ff6b6b,#8a1a1a)':c.type==='Defense'?'linear-gradient(180deg,#7ac1ff,#1a3a5a)':'linear-gradient(180deg,#d19cff,#4a2a6a)'};color:#fff">${c.cost}</span><span class="cardId">${c.id}</span></div>
<div class="cardArt" style="background:radial-gradient(180px 80px at 50% 0%, rgba(255,255,255,.08), transparent)">
<span>${c.icon}</span>
<svg style="position:absolute;inset:0;width:100%;height:100%;opacity:.18" viewBox="0 0 100 60">
${c.cls==='bastion'?'<path d="M0 6 L12 0 L24 6 L24 54 L12 60 L0 54 Z" fill="none" stroke="#d8b75a" stroke-width=".8"/>':''}
${c.cls==='wither'?'<path d="M50 4 C70 14 90 28 70 48 C60 60 40 60 30 48 C10 28 30 14 50 4Z" fill="none" stroke="#4ade80" stroke-width=".8"/>':''}
${c.cls==='shroud'?'<path d="M50 3 L97 18 L97 42 L50 57 L3 42 L3 18 Z" fill="none" stroke="#c57aff" stroke-width=".8"/>':''}
</svg>
</div>
<div class="cardName" style="color:${c.type==='Attack'?'#ffb3b3':c.type==='Defense'?'#b3d8ff':'#e2c2ff'}">${c.name}</div>
<div class="cardDesc">${c.desc}</div>
<div class="cardStats">${c.atk?`<span class="statPill" style="color:#ff6b6b">ATK ${c.atk}</span>`:''}${c.def?`<span class="statPill" style="color:#7ac1ff">DEF ${c.def}</span>`:''}</div>
</div>`;
div.onclick=()=>{ if(can) playCard(i) };
g.appendChild(div);
});
if(state.hand.length===0) g.innerHTML='<div style="grid-column:1/-1;text-align:center;color:var(--muted);padding:12px;border:1px dashed #2c314e;border-radius:12px">No cards — end turn to draw</div>';
}
function updateUI(){
document.getElementById('goldTxt').textContent=state.gold;
document.getElementById('goldSide').textContent=state.gold;
document.getElementById('floorTxt').textContent=state.floor;
document.getElementById('roomTxt').textContent=state.room;
document.getElementById('hpTxt').textContent=`${Math.max(0,state.hp)} / ${state.maxHp}`;
document.getElementById('hpFill').style.width=(Math.max(0,state.hp)/state.maxHp*100)+'%';
document.getElementById('enTxt').textContent=`${state.energy} / ${state.maxEnergy}`;
document.getElementById('enFill').style.width=(state.energy/state.maxEnergy*100)+'%';
document.getElementById('handCount').textContent=state.hand.length;
document.getElementById('handSizeTxt').textContent=state.handSize;
document.getElementById('deckCount').textContent=state.drawPile.length;
document.getElementById('discardCount').textContent=state.discard.length;
document.getElementById('turnTxt').textContent=state.turn;
document.getElementById('permDefTxt').textContent=`PERM DEF ${state.permDef} • BLOCK ${getDefense()}`;
const defEl=document.getElementById('defTxt');
if(getDefense()>0){defEl.style.display='inline-flex'; defEl.textContent='🛡️ '+getDefense()} else defEl.style.display='none';
document.getElementById('rewardTxt').textContent = state.enemy.isBoss ? 'Boss → +15 Gold' : 'Victory → +5 Gold';
// energy orbs
const em=document.getElementById('energyMeter'); em.innerHTML='';
for(let i=0;i<state.maxEnergy;i++){ const o=document.createElement('div'); o.className='eOrb'+(i<state.energy?'':' off'); o.style.height=(18+i*4)+'px'; o.style.opacity=i<state.energy?1:.3; em.appendChild(o)}
// dots
const dots=document.getElementById('roomDots'); dots.innerHTML='';
for(let i=1;i<=4;i++){ const d=document.createElement('div'); d.className='dot'+(i<=state.room?' on':''); if(i===4) d.style.background = i<=state.room ? '#ff3b3b':'#1e2236'; d.title=i===4?'Boss':`Room ${i}`; dots.appendChild(d)}
document.getElementById('floorLabel').textContent=`FLOOR ${state.floor} — ${state.room===4?'BOSS':`CHAMBER ${state.room}/3`}`;
renderHand();
renderHero();
updateEnemyUI();
}
function renderHero(){
const g=document.getElementById('heroSvg');
let svg='';
if(state.cls==='bastion') svg=`<g transform="translate(100,75)"><rect x="-28" y="-50" width="56" height="70" rx="8" fill="#2e2a22" stroke="#d8b75a" stroke-width="2"/><rect x="-20" y="-40" width="40" height="40" rx="6" fill="#1a1610" stroke="#8a6b2a"/><circle cx="0" cy="-18" r="14" fill="#d8b75a" stroke="#8a6b2a" stroke-width="2"/><path d="M-10 20 L0 35 L10 20" stroke="#d8b75a" fill="none" stroke-width="2"/><rect x="-36" y="-30" width="12" height="40" rx="6" fill="#3a352a" stroke="#d8b75a"/></g>`;
if(state.cls==='wither') svg=`<g transform="translate(100,75)"><path d="M0 -55 C30 -40 40 -10 20 20 C10 35 -10 35 -20 20 C-40 -10 -30 -40 0 -55Z" fill="#1a2a1e" stroke="#4ade80" stroke-width="2"/><circle cx="0" cy="-16" r="13" fill="#0f1a12" stroke="#4ade80" stroke-width="2"/><path d="M-8 -20 Q0 -28 8 -20" stroke="#4ade80" fill="none"/><path d="M-18 10 Q0 22 18 10" stroke="#4ade80" fill="none" opacity=".6"/></g>`;
if(state.cls==='shroud') svg=`<g transform="translate(100,75)"><path d="M0 -55 L38 -20 L28 25 L0 40 L-28 25 L-38 -20 Z" fill="#1a1530" stroke="#c57aff" stroke-width="2"/><circle cx="0" cy="-16" r="12" fill="#0f0a1a" stroke="#c57aff"/><path d="M-6 -18 L6 -14 M-6 -12 L6 -8" stroke="#c57aff" stroke-width="1.2"/><path d="M-12 18 L0 28 L12 18" stroke="#c57aff" fill="none"/></g>`;
g.innerHTML=svg;
}
function updateEnemyUI(first){
if(!state.enemy) return;
const e=state.enemy;
const pct=Math.max(0, e.hp/(e.maxHp + (e.id==='boss3'?0:0))*100);
document.getElementById('enemyName').textContent=e.name;
document.getElementById('enemyHpTxt').textContent=`${Math.max(0,Math.ceil(e.hp))} / ${e.maxHp}`;
document.getElementById('enemyHpFill').style.width=pct+'%';
document.getElementById('enemyDmgTxt').textContent=`ATK ${e.dmg}`;
document.getElementById('bossHpWrap').style.display = e.isBoss ? 'block' : 'none';
document.getElementById('bossPill').style.display = e.isBoss ? 'flex' : 'none';
if(e.isBoss){ document.getElementById('bossHpFill').style.width=pct+'%'; }
const statusRow=document.getElementById('statusRow'); statusRow.innerHTML='';
if(e.burn) statusRow.innerHTML+=`<span style="background:rgba(255,180,0,.15);border:1px solid #ffb400;color:#ffd166;padding:2px 8px;border-radius:999px;font-family:'JetBrains Mono',monospace;font-size:11px">🔥 BURN ${e.burn}</span>`;
if(e.poison) statusRow.innerHTML+=`<span style="background:rgba(100,255,120,.12);border:1px solid #4ade80;color:#4ade80;padding:2px 8px;border-radius:999px;font-family:'JetBrains Mono',monospace;font-size:11px">🧪 POISON ${e.poison}</span>`;
if(e.stun) statusRow.innerHTML+=`<span style="background:rgba(120,120,255,.15);border:1px solid #8b8bff;color:#aab0ff;padding:2px 8px;border-radius:999px;font-family:'JetBrains Mono',monospace;font-size:11px">💫 STUN</span>`;
if(e.block) statusRow.innerHTML+=`<span style="background:rgba(95,176,255,.15);border:1px solid #5fb0ff;color:#9ad1ff;padding:2px 8px;border-radius:999px;font-family:'JetBrains Mono',monospace;font-size:11px">🛡️ BLOCK ${e.block}</span>`;
document.getElementById('bossMechanic').textContent =
e.id==='boss1' ? (e.hp>=20 ? 'Gatekeeper blocks 5 damage per turn until 20 HP.' : 'Gatekeeper exposed!') :
e.id==='boss2' ? 'Corrupted deals 20 damage if you end turn with <3 Energy.' :
e.id==='boss3' ? 'Void Lord regenerates +10 HP each turn. Cannot block.' : '';
// sprite
const sp=document.getElementById('enemySprite');
let icon='👹';
if(e.name.includes('GOBLIN')) icon='👺';
if(e.name.includes('RAT')) icon='🐀';
if(e.name.includes('VAMPIRE')) icon='🧛';
if(e.name.includes('OGRE')) icon='👹';
if(e.name.includes('VOID')) icon='👁️';
if(e.isBoss) icon='🐲';
const size = e.isBoss? 120: 90;
sp.innerHTML=`<svg width="${size}" height="${size}" viewBox="0 0 100 100"><rect x="10" y="10" width="80" height="80" rx="16" fill="${e.isBoss?'#2a0f14':'#1a1d2d'}" stroke="${e.isBoss?'#ff3b3b':'#2e3352'}" stroke-width="2"/><text x="50" y="62" text-anchor="middle" font-size="${e.isBoss?44:38}">${icon}</text>${e.isBoss?'<circle cx="50" cy="50" r="38" fill="none" stroke="#ff3b3b" stroke-width="1" stroke-dasharray="4 4" opacity=".5"/>':''}</svg>`;
}
function openShop(){ document.getElementById('shopModal').style.display='grid'; document.getElementById('shopGold').textContent=state.gold+'G'; renderShop(); }
function closeShop(){ document.getElementById('shopModal').style.display='none'; }
function renderShop(){
const grid=document.getElementById('shopGrid'); grid.innerHTML='';
let list = CARD_DB.filter(c=> shopFilter==='all' || c.type===shopFilter);
list.forEach(c=>{
const owned=state.inventory[c.id]||0;
const cost=cardCost(c.id);
const canBuy = state.gold>=cost && owned<5;
const div=document.createElement('div');
div.className='card '+(c.type==='Attack'?'typeAttack':c.type==='Defense'?'typeDefense':'typeOther');
div.style.opacity = owned>=5 ? .4 : 1;
let bg=c.cls==='bastion'?'linear-gradient(180deg,#2b2216,#1a150f)':c.cls==='wither'?'linear-gradient(180deg,#1a2520,#111a14)':'linear-gradient(180deg,#211a33,#151022)';
div.style.background=bg;
div.innerHTML=`
<div class="cardInner">
<div class="cardTop"><span class="cardCost" style="background:${c.type==='Attack'?'#7a1a1a':c.type==='Defense'?'#1a3a5a':'#4a2a6a'};color:#fff;border-color:#fff">${c.cost}</span><span class="cardId">${c.id} • ${owned}/5</span></div>
<div class="cardArt"><span>${c.icon}</span></div>
<div class="cardName" style="color:${c.type==='Attack'?'#ffb3b3':c.type==='Defense'?'#b3d8ff':'#e2c2ff'}">${c.name}</div>
<div class="cardDesc">${c.desc}</div>
<div style="margin-top:6px;display:flex;gap:6px;align-items:center;justify-content:center">
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--gold)">${cost===99?'MAX':cost+'G'}</span>
<button class="btnSm" ${canBuy?'':'disabled'} style="padding:6px 10px">${owned>=5?'OWNED':canBuy?'BUY':'NEED '+cost+'G'}</button>
</div>
</div>`;
div.querySelector('button').onclick=(e)=>{ e.stopPropagation(); if(!canBuy) return; state.gold-=cost; state.inventory[c.id]=owned+1; state.deck.push(c.id); state.discard.push(c.id); logMsg(`Bought <b>${c.name}</b> for ${cost}G.`); openShop(); updateUI(); };
grid.appendChild(div);
});
// card backs display
const backs=document.createElement('div'); backs.style.gridColumn='1/-1'; backs.innerHTML=`
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:4px">
<div class="cardBack"><svg width="100" height="100" viewBox="0 0 100 100"><rect x="8" y="8" width="84" height="84" rx="8" fill="#1c1a16" stroke="#d8b75a" stroke-width="2"/><path d="M50 18 L78 32 L78 68 L50 82 L22 68 L22 32 Z" fill="none" stroke="#d8b75a"/><text x="50" y="56" text-anchor="middle" font-size="10" fill="#d8b75a" font-family="Cinzel">BASTION</text></svg></div>
<div class="cardBack"><svg width="100" height="100" viewBox="0 0 100 100"><rect x="8" y="8" width="84" height="84" rx="18" fill="#0f1a12" stroke="#4ade80" stroke-width="2"/><circle cx="50" cy="50" r="28" fill="none" stroke="#4ade80" stroke-dasharray="3 3"/><text x="50" y="56" text-anchor="middle" font-size="10" fill="#4ade80" font-family="Cinzel">WITHER</text></svg></div>
<div class="cardBack"><svg width="100" height="100" viewBox="0 0 100 100"><rect x="8" y="8" width="84" height="84" rx="4" fill="#151022" stroke="#c57aff" stroke-width="2"/><path d="M50 16 L80 28 L80 72 L50 84 L20 72 L20 28 Z" fill="none" stroke="#c57aff"/><text x="50" y="56" text-anchor="middle" font-size="10" fill="#c57aff" font-family="Cinzel">SHROUD</text></svg></div>
</div>
<div style="text-align:center;color:var(--muted);font-size:11px;margin-top:6px;letter-spacing:.1em">UNIQUE CARD BACKS PER DECK • SVG FRAMES PER CLASS</div>
`; grid.appendChild(backs);
}
function redrawHand(){
if(state.energy<1){ logMsg(`Need 1 Energy to mulligan.`); return; }
state.energy-=1;
state.discard.push(...state.hand); state.hand=[];
drawUpToHand();
logMsg(`Mulligan: new hand drawn.`);
updateUI();
}
document.addEventListener('keydown',e=>{
if(e.key.toLowerCase()==='g') openShop();
if(e.key==='Enter') endTurn();
if(e.key==='Escape') closeShop();
});
updateUI();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"8c67e1dc-2c44-4f64-902a-14c2a998b8e9","tokensIn":3532,"tokensOut":18464,"tokensTotal":21996,"cost":0.0827627,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:22:17.570Z"} -->