Shadows
muse spark 1.2 contributor · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 100,176 bytes · SHA-256 fe22aea384ed
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shadows of the Abyss</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Cinzel+Decorative:wght@700&family=JetBrains+Mono:wght@400;700&family=Crimson+Pro:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0a12;--panel:#14141f;--panel2:#1c1c2e;--gold:#d4a65a;--gold2:#8a6a2e;--crimson:#c0392b;--violet:#7a3cff;--teal:#2ecc71;--muted:#8a8aa0}
body{background:var(--bg);color:#e8e0d0;font-family:'Crimson Pro',serif;overflow-x:hidden;min-height:100vh}
h1,h2,h3{font-family:'Cinzel',serif}
.mono{font-family:'JetBrains Mono',monospace}
/* header */
.topbar{position:sticky;top:0;z-index:50;background:linear-gradient(180deg,#0f0f1e 0%,#0a0a12 100%);border-bottom:1px solid #2a2a40;display:flex;align-items:center;justify-content:space-between;padding:10px 18px;gap:12px;flex-wrap:wrap}
.brand{font-family:'Cinzel Decorative',serif;font-size:20px;letter-spacing:2px;color:var(--gold);text-shadow:0 0 18px rgba(212,166,90,.4)}
.stats{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.stat-pill{background:#1a1a2e;border:1px solid #2a2a40;border-radius:999px;padding:6px 12px;display:flex;align-items:center;gap:8px;font-family:'JetBrains Mono',monospace;font-size:12px}
.hp-bar{width:140px;height:10px;background:#1a1a1a;border-radius:999px;overflow:hidden;border:1px solid #333}
.hp-fill{height:100%;background:linear-gradient(90deg,#c0392b,#e74c3c);transition:width .4s}
.gold{color:var(--gold);font-weight:700}
.act-badge{background:var(--violet);color:#fff;padding:4px 10px;border-radius:999px;font-size:11px;letter-spacing:1px}
/* layout */
.wrap{max-width:1280px;margin:0 auto;padding:18px}
.view{display:none}
.view.active{display:block}
/* class select */
.hero{text-align:center;padding:30px 10px}
.hero h1{font-size:42px;color:var(--gold);text-shadow:0 0 30px rgba(212,166,90,.25)}
.hero p{color:var(--muted);max-width:640px;margin:10px auto;line-height:1.6}
.class-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:22px}
@media(max-width:900px){.class-grid{grid-template-columns:1fr}}
.class-card{background:linear-gradient(180deg,#1a1a2e 0%,#12121f 100%);border:1px solid #2a2a40;border-radius:16px;padding:18px;cursor:pointer;transition:.2s;position:relative;overflow:hidden}
.class-card:hover{transform:translateY(-4px);border-color:var(--gold);box-shadow:0 12px 40px rgba(0,0,0,.6),0 0 0 1px rgba(212,166,90,.2)}
.class-card h3{color:var(--gold);letter-spacing:1px}
.class-icon{font-size:44px;margin:10px 0}
.class-desc{color:#a8a8c0;font-size:14px;line-height:1.5;min-height:48px}
.class-deck{margin-top:10px;display:flex;flex-wrap:wrap;gap:6px}
.mini-tag{background:#0f0f1a;border:1px solid #2a2a40;border-radius:999px;padding:3px 8px;font-size:11px;font-family:'JetBrains Mono',monospace;color:#c8c8d8}
.btn{appearance:none;border:none;cursor:pointer;padding:10px 16px;border-radius:10px;font-family:'Cinzel',serif;font-weight:700;letter-spacing:1px;transition:.2s}
.btn-gold{background:linear-gradient(180deg,#d4a65a,#8a6a2e);color:#0a0a12;box-shadow:0 4px 16px rgba(212,166,90,.3)}
.btn-gold:hover{filter:brightness(1.1);transform:translateY(-1px)}
.btn-ghost{background:#1a1a2e;color:#e8e0d0;border:1px solid #2a2a40}
.btn-ghost:hover{border-color:var(--gold);color:var(--gold)}
.btn-danger{background:#c0392b;color:#fff}
.btn-small{padding:6px 10px;font-size:12px;border-radius:8px}
/* map */
.map-wrap{background:linear-gradient(180deg,#0f0f1f 0%,#0a0a12 100%);border:1px solid #2a2a40;border-radius:16px;padding:16px;position:relative;overflow:hidden}
.map-title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
.map-grid{display:flex;flex-direction:column;gap:8px;max-height:560px;overflow:auto;padding-right:6px}
.floor{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;background:rgba(255,255,255,.02);border:1px solid transparent}
.floor.current{border-color:var(--gold);background:rgba(212,166,90,.08);box-shadow:0 0 0 1px rgba(212,166,90,.15) inset}
.floor.done{opacity:.55}
.floor-label{width:70px;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.nodes{display:flex;gap:8px;flex-wrap:wrap;flex:1}
.node{flex:1;min-width:140px;max-width:260px;display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:10px;border:1px solid #2a2a40;background:#141428;cursor:pointer;transition:.2s}
.node:hover{border-color:var(--gold);transform:translateY(-1px)}
.node.disabled{opacity:.4;pointer-events:none}
.node.active-choice{border-color:var(--gold);background:rgba(212,166,90,.12);box-shadow:0 0 12px rgba(212,166,90,.25)}
.node-icon{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:16px;flex-shrink:0}
.node-enemy .node-icon{background:#2a1a1a;color:#e74c3c}
.node-elite .node-icon{background:#3a1a2a;color:#ff6b9d}
.node-rest .node-icon{background:#1a2a1a;color:#2ecc71}
.node-shop .node-icon{background:#1a1a2a;color:var(--gold)}
.node-event .node-icon{background:#1a1a2e;color:#7a3cff}
.node-boss .node-icon{background:linear-gradient(135deg,#c0392b,#7a3cff);color:#fff}
.node-name{font-family:'Cinzel',serif;font-size:12px;letter-spacing:.5px}
.node-sub{font-size:11px;color:var(--muted);font-family:'JetBrains Mono',monospace}
/* combat */
.combat-layout{display:grid;grid-template-columns:1fr 340px;gap:16px}
@media(max-width:1000px){.combat-layout{grid-template-columns:1fr}}
.battlefield{background:radial-gradient(800px 400px at 50% 0%,#1a1430 0%,#0f0f1e 55%,#0a0a12 100%);border:1px solid #2a2a40;border-radius:16px;padding:14px;position:relative;overflow:hidden}
.enemy-row{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;min-height:180px;align-items:flex-start}
.enemy-card{background:linear-gradient(180deg,#1e1e32,#141428);border:1px solid #2a2a40;border-radius:14px;padding:12px;width:210px;position:relative;transition:.2s}
.enemy-card.targetable{cursor:pointer;box-shadow:0 0 0 2px var(--gold)}
.enemy-card.dead{opacity:.35;filter:grayscale(1)}
.enemy-top{display:flex;justify-content:space-between;align-items:center}
.enemy-name{font-family:'Cinzel',serif;font-size:13px;color:#fff}
.enemy-hp{height:8px;background:#111;border-radius:999px;overflow:hidden;border:1px solid #333;margin:8px 0}
.enemy-hp-fill{height:100%;background:linear-gradient(90deg,#c0392b,#ff6b6b);transition:width .3s}
.intent{margin-top:8px;padding:7px 8px;border-radius:8px;background:#0f0f1a;border:1px solid #2a2a40;font-family:'JetBrains Mono',monospace;font-size:11px;display:flex;align-items:center;gap:6px}
.intent-atk{border-color:#c0392b;color:#ff8a80}
.intent-def{border-color:#2ecc71;color:#a5d6a7}
.intent-buff{border-color:#7a3cff;color:#b39ddb}
.intent-debuff{border-color:#ff9800;color:#ffcc80}
.block-badge{background:#2c3e50;color:#aed6f1;border:1px solid #34495e;padding:2px 6px;border-radius:999px;font-size:11px;font-family:'JetBrains Mono',monospace}
.status-row{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
.status{font-size:10px;padding:2px 6px;border-radius:999px;border:1px solid #333;background:#0f0f1a;font-family:'JetBrains Mono',monospace}
.s-buff{border-color:#2ecc71;color:#a5d6a7}
.s-debuff{border-color:#c0392b;color:#ff8a80}
.s-special{border-color:#7a3cff;color:#d1c4e9}
.player-area{margin-top:14px;background:linear-gradient(180deg,#141428,#0f0f1e);border:1px solid #2a2a40;border-radius:14px;padding:12px;display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
.player-info{display:flex;align-items:center;gap:12px}
.avatar{width:52px;height:52px;border-radius:12px;background:linear-gradient(135deg,#2a1a3a,#1a2a3a);display:grid;place-items:center;font-size:26px;border:1px solid #2a2a40}
.energy{display:flex;gap:6px;align-items:center}
.energy-orb{width:32px;height:32px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#ffd54f,#ff8f00);display:grid;place-items:center;font-weight:900;color:#3e2723;box-shadow:0 0 12px rgba(255,143,0,.5);font-family:'Cinzel',serif}
.hand{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;padding:14px 6px;min-height:170px}
.card{width:148px;min-height:188px;background:linear-gradient(180deg,#1e1e32 0%,#12121f 100%);border:1px solid #2a2a40;border-radius:12px;padding:10px;display:flex;flex-direction:column;gap:6px;cursor:pointer;transition:.18s;position:relative;overflow:hidden}
.card:hover{transform:translateY(-6px) scale(1.02);border-color:var(--gold);box-shadow:0 12px 30px rgba(0,0,0,.5)}
.card.disabled{opacity:.45;pointer-events:none;filter:grayscale(.3)}
.card.selected{border-color:var(--gold);box-shadow:0 0 0 2px var(--gold),0 12px 30px rgba(0,0,0,.5)}
.card-top{display:flex;justify-content:space-between;align-items:center}
.card-cost{width:26px;height:26px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#ffd54f,#ff8f00);display:grid;place-items:center;font-weight:900;font-size:12px;color:#3e2723;font-family:'Cinzel',serif;box-shadow:0 0 8px rgba(255,143,0,.4)}
.card-type{font-size:9px;letter-spacing:1px;text-transform:uppercase;font-family:'JetBrains Mono',monospace;padding:2px 6px;border-radius:999px;border:1px solid #2a2a40}
.type-attack{color:#ff8a80;border-color:#4a1a1a;background:rgba(192,57,43,.15)}
.type-skill{color:#82b1ff;border-color:#1a2a4a;background:rgba(52,73,94,.4)}
.type-power{color:#ce93d8;border-color:#3a1a4a;background:rgba(122,60,255,.18)}
.card-name{font-family:'Cinzel',serif;font-size:12px;color:#fff;line-height:1.2;min-height:28px}
.card-desc{font-size:11.5px;line-height:1.35;color:#c8c8d8;flex:1}
.card-up{position:absolute;top:6px;right:6px;font-size:9px;background:var(--gold);color:#0a0a12;padding:1px 5px;border-radius:999px;font-family:'JetBrains Mono',monospace;font-weight:700}
.pile-info{display:flex;gap:8px;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted)}
.combat-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.log{max-height:220px;overflow:auto;background:#0f0f1a;border:1px solid #2a2a40;border-radius:12px;padding:10px;font-family:'JetBrains Mono',monospace;font-size:11px;line-height:1.5;color:#c8c8d8}
.log b{color:#fff}
/* side panel */
.side{display:flex;flex-direction:column;gap:12px}
.panel{background:#14141f;border:1px solid #2a2a40;border-radius:14px;padding:12px}
.panel h4{font-family:'Cinzel',serif;font-size:12px;letter-spacing:1px;color:var(--gold);margin-bottom:8px}
.relic-grid{display:flex;flex-wrap:wrap;gap:6px}
.relic{width:32px;height:32px;border-radius:8px;background:#1a1a2e;border:1px solid #2a2a40;display:grid;place-items:center;font-size:14px;cursor:help}
.potion-row{display:flex;gap:8px;flex-wrap:wrap}
.potion{flex:1;min-width:90px;background:#1a1a2e;border:1px solid #2a2a40;border-radius:10px;padding:8px;text-align:center;cursor:pointer}
.potion:hover{border-color:var(--gold)}
.potion-name{font-size:11px;font-family:'Cinzel',serif;color:#fff}
.potion-desc{font-size:10px;color:var(--muted);margin-top:2px;line-height:1.3}
/* shop / rest / event / reward */
.shop-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
@media(max-width:900px){.shop-grid{grid-template-columns:1fr}}
.shop-card{background:#141428;border:1px solid #2a2a40;border-radius:12px;padding:12px}
.price{font-family:'JetBrains Mono',monospace;color:var(--gold);font-weight:700;margin-top:6px}
.reward-choices{display:flex;gap:12px;flex-wrap:wrap;justify-content:center}
.choice-card{width:170px}
.overlay{position:fixed;inset:0;background:rgba(5,5,12,.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:100;padding:18px}
.overlay.open{display:flex}
.modal{background:linear-gradient(180deg,#1a1a2e,#12121f);border:1px solid #2a2a40;border-radius:16px;padding:18px;max-width:720px;width:100%;max-height:90vh;overflow:auto;box-shadow:0 20px 60px rgba(0,0,0,.6)}
.deck-grid{display:flex;flex-wrap:wrap;gap:8px}
.tag{font-size:10px;padding:2px 6px;border-radius:999px;border:1px solid #2a2a40;background:#0f0f1a;color:var(--muted);font-family:'JetBrains Mono',monospace}
.divider{height:1px;background:#2a2a40;margin:12px 0}
.kbd{font-family:'JetBrains Mono',monospace;background:#0f0f1a;border:1px solid #2a2a40;border-bottom-width:2px;padding:2px 6px;border-radius:6px;font-size:11px}
</style>
</head>
<body>
<div class="topbar">
<div class="brand">SHADOWS OF THE ABYSS</div>
<div class="stats">
<div class="stat-pill"><span>♥</span><div class="hp-bar"><div id="hpFill" class="hp-fill" style="width:100%"></div></div><span id="hpText" class="mono">70/70</span><span id="blockText" class="block-badge" style="display:none">0 Block</span></div>
<div class="stat-pill"><span>◈</span><span id="goldText" class="gold mono">99 Gold</span></div>
<div class="stat-pill"><span id="actText" class="act-badge">ACT I — THE CRYPTS</span></div>
<div class="stat-pill mono" id="deckInfo">Deck 10</div>
<button class="btn btn-ghost btn-small" onclick="showDeck()">Deck</button>
<button class="btn btn-ghost btn-small" onclick="abandonRun()">Abandon</button>
</div>
</div>
<div class="wrap">
<!-- CLASS SELECT -->
<div id="view-class" class="view active">
<div class="hero">
<h1>Choose Your Curse</h1>
<p>Fallen heroes wielding forbidden magic. Each archetype bears a unique deck, relics and damnation. Survive 3 acts. The Archdemon awaits at the Spire's peak.</p>
<div class="class-grid" id="classGrid"></div>
<div style="margin-top:18px;color:var(--muted);font-size:13px">Energy 3 • Draw 5 • Permadeath • Predictable enemy intents</div>
</div>
</div>
<!-- MAP -->
<div id="view-map" class="view">
<div class="map-wrap">
<div class="map-title">
<h2 style="color:var(--gold)">World Map</h2>
<div style="display:flex;gap:8px;align-items:center">
<span class="tag" id="mapProgress">Floor 2 / 15</span>
<button class="btn btn-ghost btn-small" onclick="showDeck()">View Deck</button>
</div>
</div>
<div id="mapGrid" class="map-grid"></div>
<div class="divider"></div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<span class="tag">◈ Enemy</span><span class="tag" style="color:#ff6b9d">⬢ Elite (Relic)</span><span class="tag" style="color:#2ecc71">✦ Rest</span><span class="tag" style="color:var(--gold)">⌖ Shop</span><span class="tag" style="color:#7a3cff">✧ Event</span><span class="tag" style="color:#c0392b">♛ Boss</span>
</div>
</div>
</div>
<!-- COMBAT -->
<div id="view-combat" class="view">
<div class="combat-layout">
<div class="battlefield">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;gap:8px;flex-wrap:wrap">
<h3 style="color:var(--gold);font-size:14px;letter-spacing:1px" id="combatTitle">Combat</h3>
<div class="pile-info"><span id="drawCount">Draw 0</span><span>•</span><span id="discardCount">Discard 0</span><span>•</span><span id="exhaustCount">Exhaust 0</span></div>
</div>
<div id="enemyRow" class="enemy-row"></div>
<div class="player-area">
<div class="player-info">
<div class="avatar" id="playerAvatar">☠</div>
<div>
<div style="font-family:'Cinzel',serif;font-size:13px" id="playerName">Blood Reaper</div>
<div class="status-row" id="playerStatuses"></div>
<div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted)" id="playerBlockLine">Block 0 • Powers 0</div>
</div>
</div>
<div class="energy"><div class="energy-orb" id="energyOrb">3</div><div class="mono" style="font-size:12px;color:var(--muted)">Energy</div></div>
<div class="combat-actions">
<button class="btn btn-gold" onclick="endTurn()">End Turn <span class="kbd">E</span></button>
<button class="btn btn-ghost btn-small" onclick="viewDiscard()">Piles</button>
</div>
</div>
<div id="hand" class="hand"></div>
<div style="text-align:center;color:var(--muted);font-size:11px;font-family:'JetBrains Mono',monospace">Click a card, then click an enemy if it requires a target. Unplayable cards are dimmed.</div>
</div>
<div class="side">
<div class="panel">
<h4>Combat Log</h4>
<div id="combatLog" class="log"></div>
</div>
<div class="panel">
<h4>Relics & Potions</h4>
<div id="relicList" class="relic-grid"></div>
<div style="height:8px"></div>
<div id="potionList" class="potion-row"></div>
</div>
<div class="panel">
<h4>Tips</h4>
<div style="font-size:12px;color:#a8a8c0;line-height:1.5">
• Block resets at start of your turn.<br>
• Weak −25% dmg, Vulnerable +50% taken.<br>
• Bleed/Burn halve each turn, Poison −1.<br>
• Intangible reduces all damage to 1.
</div>
</div>
</div>
</div>
</div>
<!-- SHOP -->
<div id="view-shop" class="view">
<div class="map-wrap">
<div style="display:flex;justify-content:space-between;align-items:center">
<h2 style="color:var(--gold)">Shop — The Veiled Bazaar</h2>
<button class="btn btn-ghost btn-small" onclick="leaveShop()">Leave</button>
</div>
<p style="color:var(--muted);margin:8px 0">Spend gold wisely. Card removal costs 75g. Potions 50g. Relics 150g.</p>
<h4 style="color:var(--gold);margin:10px 0">Cards</h4>
<div id="shopCards" class="shop-grid"></div>
<h4 style="color:var(--gold);margin:14px 0 10px">Relics</h4>
<div id="shopRelics" class="shop-grid"></div>
<h4 style="color:var(--gold);margin:14px 0 10px">Potions</h4>
<div id="shopPotions" class="shop-grid"></div>
<div class="divider"></div>
<button class="btn btn-ghost" onclick="buyRemoval()">Remove a Card — 75g</button>
<span id="shopMsg" class="mono" style="margin-left:10px;color:var(--gold);font-size:12px"></span>
</div>
</div>
<!-- REST -->
<div id="view-rest" class="view">
<div class="map-wrap" style="text-align:center;padding:28px">
<h2 style="color:var(--gold)">Rest Site — Flickering Campfire</h2>
<p style="color:var(--muted);margin:8px 0">Choose one. The fire will not wait.</p>
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:16px">
<button class="btn btn-gold" onclick="restHeal()">Rest — Heal 25% (17 HP)</button>
<button class="btn btn-ghost" onclick="restForge()">Forge — Upgrade a Card</button>
<button class="btn btn-ghost" onclick="restMeditate()">Meditate — Remove a Card</button>
</div>
<div style="margin-top:16px"><button class="btn btn-ghost btn-small" onclick="leaveRest()">Leave</button></div>
</div>
</div>
<!-- EVENT -->
<div id="view-event" class="view">
<div class="map-wrap" style="max-width:720px;margin:0 auto">
<h2 id="eventTitle" style="color:var(--gold)"></h2>
<p id="eventText" style="color:#c8c8d8;margin:10px 0;line-height:1.6"></p>
<div id="eventChoices" style="display:flex;flex-direction:column;gap:8px;margin-top:12px"></div>
<div style="margin-top:12px"><button class="btn btn-ghost btn-small" onclick="leaveEvent()">Leave</button></div>
</div>
</div>
<!-- REWARD -->
<div id="view-reward" class="view">
<div class="map-wrap" style="text-align:center">
<h2 style="color:var(--gold)" id="rewardTitle">Victory!</h2>
<p id="rewardSubtitle" style="color:var(--muted)"></p>
<div id="rewardGold" style="margin:10px 0;font-family:'JetBrains Mono',monospace;color:var(--gold)"></div>
<h4 style="color:var(--gold);margin:12px 0">Choose a Card (or Skip)</h4>
<div id="rewardCards" class="reward-choices"></div>
<div style="margin-top:12px;display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<button class="btn btn-ghost" onclick="skipCardReward()">Skip</button>
<button class="btn btn-gold" onclick="continueFromReward()">Continue</button>
</div>
<div id="rewardExtra" style="margin-top:12px"></div>
</div>
</div>
<!-- VICTORY / DEFEAT -->
<div id="view-end" class="view">
<div class="map-wrap" style="text-align:center;padding:40px">
<h1 id="endTitle" style="font-family:'Cinzel Decorative',serif;color:var(--gold)"></h1>
<p id="endText" style="color:var(--muted);margin:10px 0"></p>
<button class="btn btn-gold" onclick="resetToClassSelect()">Return to Title</button>
</div>
</div>
</div>
<!-- MODALS -->
<div id="deckModal" class="overlay" onclick="if(event.target===this)closeDeck()">
<div class="modal">
<div style="display:flex;justify-content:space-between;align-items:center">
<h3 style="color:var(--gold)">Your Deck</h3>
<button class="btn btn-ghost btn-small" onclick="closeDeck()">Close</button>
</div>
<div id="deckModalContent" class="deck-grid" style="margin-top:12px"></div>
<div class="divider"></div>
<div style="font-size:12px;color:var(--muted)" id="deckStats"></div>
</div>
</div>
<div id="choiceModal" class="overlay">
<div class="modal">
<h3 id="choiceTitle" style="color:var(--gold)"></h3>
<p id="choiceDesc" style="color:var(--muted);margin:6px 0"></p>
<div id="choiceGrid" class="deck-grid" style="margin-top:12px"></div>
<div style="margin-top:12px;text-align:right"><button class="btn btn-ghost btn-small" onclick="closeChoice()">Cancel</button></div>
</div>
</div>
<script>
/* ===== DATA ===== */
const CLASSES={
blood:{name:'Blood Reaper',icon:'🩸',color:'#c0392b',desc:'Sacrifice HP for overwhelming power. Bleed, life-steal and Strength scaling.',sig:'Sanguine Strike'},
shadow:{name:'Shadowmancer',icon:'🌑',color:'#7a3cff',desc:'Cripple foes with Weak & Vulnerable. Intangible and card manipulation.',sig:'Shadow Bolt'},
hell:{name:'Hellforged',icon:'🔥',color:'#ff6a00',desc:'Burn enemies, summon Imps and manipulate Energy. Fire is your ally.',sig:'Firebolt'}
};
const CARD_DB={
// basics
Strike:{name:'Strike',type:'Attack',cost:1,rarity:'Basic',desc:'Deal 6 damage.',upDesc:'Deal 8 damage.',dmg:6,upDmg:8},
Defend:{name:'Defend',type:'Skill',cost:1,rarity:'Basic',desc:'Gain 5 Block.',upDesc:'Gain 8 Block.',block:5,upBlock:8},
// Blood
'Sanguine Strike':{name:'Sanguine Strike',type:'Attack',cost:1,rarity:'Common',class:'blood',desc:'Deal 8 damage. Heal 2 HP.',upDesc:'Deal 10 damage. Heal 3 HP.',dmg:8,upDmg:10,heal:2,upHeal:3},
Hemorrhage:{name:'Hemorrhage',type:'Attack',cost:1,rarity:'Common',class:'blood',desc:'Deal 5 damage. Apply 3 Bleed.',upDesc:'Deal 6 damage. Apply 4 Bleed.',dmg:5,upDmg:6,bleed:3,upBleed:4},
Bloodletting:{name:'Bloodletting',type:'Attack',cost:1,rarity:'Uncommon',class:'blood',desc:'Lose 3 HP. Deal 12 damage.',upDesc:'Lose 2 HP. Deal 15 damage.',dmg:12,upDmg:15,hpCost:3,upHpCost:2},
'Crimson Eruption':{name:'Crimson Eruption',type:'Attack',cost:2,rarity:'Rare',class:'blood',desc:'Deal 8 damage to ALL enemies. Lose 2 HP.',upDesc:'Deal 10 damage to ALL enemies. Lose 2 HP.',dmg:8,upDmg:10,aoe:true,hpCost:2},
'Crimson Shield':{name:'Crimson Shield',type:'Skill',cost:1,rarity:'Common',class:'blood',desc:'Gain 6 Block. Lose 2 HP.',upDesc:'Gain 8 Block. Lose 2 HP.',block:6,upBlock:8,hpCost:2},
'Blood Ward':{name:'Blood Ward',type:'Skill',cost:1,rarity:'Common',class:'blood',desc:'Gain 3 Block. Gain 1 Regeneration.',upDesc:'Gain 4 Block. Gain 2 Regeneration.',block:3,upBlock:4,regen:1,upRegen:2},
'Sacrificial Barrier':{name:'Sacrificial Barrier',type:'Skill',cost:2,rarity:'Uncommon',class:'blood',desc:'Gain 10 Block. Lose 5 HP.',upDesc:'Gain 13 Block. Lose 5 HP.',block:10,upBlock:13,hpCost:5},
'Vital Surge':{name:'Vital Surge',type:'Skill',cost:'X',rarity:'Rare',class:'blood',desc:'Gain Block equal to missing HP. Exhaust.',upDesc:'Gain Block equal to missing HP +3. Exhaust.',exhaust:true,isX:true},
'Life Tap':{name:'Life Tap',type:'Skill',cost:0,rarity:'Common',class:'blood',desc:'Draw 2 cards. Lose 3 HP.',upDesc:'Draw 3 cards. Lose 3 HP.',draw:2,upDraw:3,hpCost:3},
'Rend Flesh':{name:'Rend Flesh',type:'Skill',cost:1,rarity:'Uncommon',class:'blood',desc:'Apply 5 Bleed to ALL enemies. Lose 2 HP.',upDesc:'Apply 7 Bleed to ALL enemies. Lose 2 HP.',bleed:5,upBleed:7,aoe:true,hpCost:2},
'Soul Siphon':{name:'Soul Siphon',type:'Power',cost:2,rarity:'Uncommon',class:'blood',desc:'At start of your turn, gain 1 Energy and lose 1 HP.',upDesc:'At start of your turn, gain 1 Energy and lose 1 HP. (Cost 1)',upCost:1},
'Blood Pact':{name:'Blood Pact',type:'Power',cost:1,rarity:'Rare',class:'blood',desc:'Whenever you lose HP from a card, gain 1 Strength.',upDesc:'Whenever you lose HP from a card, gain 2 Strength.',strPerHp:1,upStrPerHp:2},
// Shadow
'Shadow Bolt':{name:'Shadow Bolt',type:'Attack',cost:1,rarity:'Common',class:'shadow',desc:'Deal 6 damage. Apply 1 Weak.',upDesc:'Deal 8 damage. Apply 2 Weak.',dmg:6,upDmg:8,weak:1,upWeak:2},
'Grasp of Despair':{name:'Grasp of Despair',type:'Attack',cost:1,rarity:'Common',class:'shadow',desc:'Deal 4 damage. Apply 1 Vulnerable.',upDesc:'Deal 5 damage. Apply 2 Vulnerable.',dmg:4,upDmg:5,vuln:1,upVuln:2},
'Nightmare Lash':{name:'Nightmare Lash',type:'Attack',cost:2,rarity:'Uncommon',class:'shadow',desc:'Deal 7 damage. If enemy has Weak, deal 7 more.',upDesc:'Deal 9 damage. If Weak, deal 9 more.',dmg:7,upDmg:9,bonusIfWeak:true},
'Eternal Darkness':{name:'Eternal Darkness',type:'Attack',cost:2,rarity:'Rare',class:'shadow',desc:'Deal 3 damage to random enemy 3 times.',upDesc:'Deal 4 damage to random enemy 3 times.',dmg:3,upDmg:4,hits:3,random:true},
'Cloak of Shadows':{name:'Cloak of Shadows',type:'Skill',cost:1,rarity:'Common',class:'shadow',desc:'Gain 5 Block. Draw 1 card.',upDesc:'Gain 7 Block. Draw 1 card.',block:5,upBlock:7,draw:1},
'Haunting Mist':{name:'Haunting Mist',type:'Skill',cost:1,rarity:'Common',class:'shadow',desc:'Gain 7 Block. Apply 1 Weak to ALL.',upDesc:'Gain 9 Block. Apply 1 Weak to ALL.',block:7,upBlock:9,weak:1,aoe:true},
'Phantom Armor':{name:'Phantom Armor',type:'Skill',cost:1,rarity:'Uncommon',class:'shadow',desc:'Gain 8 Block. Next turn gain 2 Dexterity.',upDesc:'Gain 10 Block. Next turn gain 3 Dexterity.',block:8,upBlock:10,nextDex:2,upNextDex:3},
'Whispering Shroud':{name:'Whispering Shroud',type:'Skill',cost:1,rarity:'Rare',class:'shadow',desc:'Gain 4 Block. Next turn gain 4 Block.',upDesc:'Gain 5 Block. Next turn gain 5 Block.',block:4,upBlock:5,nextBlock:4,upNextBlock:5},
Shadowform:{name:'Shadowform',type:'Skill',cost:2,rarity:'Uncommon',class:'shadow',desc:'Gain 2 Intangible. Exhaust.',upDesc:'Gain 3 Intangible. Exhaust.',exhaust:true,intangible:2,upIntangible:3},
'Curse of Misfortune':{name:'Curse of Misfortune',type:'Skill',cost:1,rarity:'Common',class:'shadow',desc:'Enemy loses 3 Strength this turn. Apply 1 Vulnerable.',upDesc:'Enemy loses 4 Strength, Apply 2 Vulnerable.',strDown:3,upStrDown:4,vuln:1,upVuln:2},
'Dark Ritual':{name:'Dark Ritual',type:'Skill',cost:0,rarity:'Uncommon',class:'shadow',desc:'Gain 2 Energy. Shuffle Dazed into draw pile.',upDesc:'Gain 2 Energy. Shuffle Dazed into discard pile.',energyGain:2},
'Umbral Mastery':{name:'Umbral Mastery',type:'Power',cost:2,rarity:'Rare',class:'shadow',desc:'At start of your turn, apply 1 Weak to ALL.',upDesc:'At start of your turn, apply 2 Weak to ALL.',weakAura:1,upWeakAura:2},
// Hellforged
Firebolt:{name:'Firebolt',type:'Attack',cost:1,rarity:'Common',class:'hell',desc:'Deal 7 damage. Apply 2 Burn.',upDesc:'Deal 9 damage. Apply 3 Burn.',dmg:7,upDmg:9,burn:2,upBurn:3},
'Hellfire Blast':{name:'Hellfire Blast',type:'Attack',cost:2,rarity:'Uncommon',class:'hell',desc:'Deal 9 damage to ALL enemies.',upDesc:'Deal 12 damage to ALL.',dmg:9,upDmg:12,aoe:true},
'Searing Strike':{name:'Searing Strike',type:'Attack',cost:1,rarity:'Common',class:'hell',desc:'Deal 6 damage. Gain 1 Energy next turn.',upDesc:'Deal 8 damage. Gain 1 Energy next turn.',dmg:6,upDmg:8,nextEnergy:1},
Immolate:{name:'Immolate',type:'Attack',cost:2,rarity:'Rare',class:'hell',desc:'Deal 12 damage. Exhaust.',upDesc:'Deal 16 damage. Exhaust.',dmg:12,upDmg:16,exhaust:true},
'Obsidian Skin':{name:'Obsidian Skin',type:'Skill',cost:1,rarity:'Common',class:'hell',desc:'Gain 7 Block.',upDesc:'Gain 10 Block.',block:7,upBlock:10},
'Flame Ward':{name:'Flame Ward',type:'Skill',cost:1,rarity:'Common',class:'hell',desc:'Gain 5 Block. Apply 2 Burn to random enemy.',upDesc:'Gain 6 Block. Apply 3 Burn.',block:5,upBlock:6,burn:2,upBurn:3,randomBurn:true},
'Demonic Resilience':{name:'Demonic Resilience',type:'Skill',cost:1,rarity:'Uncommon',class:'hell',desc:'Gain 4 Block. Heal 2 HP.',upDesc:'Gain 5 Block. Heal 3 HP.',block:4,upBlock:5,heal:2,upHeal:3},
'Brimstone Shield':{name:'Brimstone Shield',type:'Skill',cost:1,rarity:'Rare',class:'hell',desc:'Gain 3 Block. Gain 2 Strength this turn.',upDesc:'Gain 4 Block. Gain 3 Strength.',block:3,upBlock:4,str:2,upStr:3},
'Summon Imp':{name:'Summon Imp',type:'Skill',cost:1,rarity:'Common',class:'hell',desc:'Add Imp Claw (0-cost 4 dmg) to hand. Exhaust.',upDesc:'Add Imp Claw+ (6 dmg) instead. Exhaust.',exhaust:true,summonImp:true},
'Infernal Pact':{name:'Infernal Pact',type:'Skill',cost:0,rarity:'Uncommon',class:'hell',desc:'Lose 2 HP. Gain 2 Energy. Add Imp Claw.',upDesc:'Lose 1 HP. Gain 2 Energy. Add Imp Claw+.',hpCost:2,upHpCost:1,energyGain:2,summonImp:true},
'Hellfire Aura':{name:'Hellfire Aura',type:'Power',cost:2,rarity:'Rare',class:'hell',desc:'At start of turn, deal 3 damage to ALL.',upDesc:'At start of turn, deal 5 damage to ALL.',auraDmg:3,upAuraDmg:5},
'Soul Burn':{name:'Soul Burn',type:'Skill',cost:1,rarity:'Uncommon',class:'hell',desc:'Apply 8 Burn to an enemy. Draw 1.',upDesc:'Apply 10 Burn. Draw 1.',burn:8,upBurn:10,draw:1},
// Colorless
'Flash of Insight':{name:'Flash of Insight',type:'Skill',cost:1,rarity:'Common',desc:'Draw 2 cards.',upDesc:'Draw 3 cards.',draw:2,upDraw:3,colorless:true},
'Iron Will':{name:'Iron Will',type:'Skill',cost:1,rarity:'Common',desc:'Gain 8 Block.',upDesc:'Gain 11 Block.',block:8,upBlock:11,colorless:true},
'Quick Slash':{name:'Quick Slash',type:'Attack',cost:1,rarity:'Common',desc:'Deal 7 damage.',upDesc:'Deal 9 damage.',dmg:7,upDmg:9,colorless:true},
'Potent Brew':{name:'Potent Brew',type:'Skill',cost:0,rarity:'Uncommon',desc:'Gain 1 Energy. Exhaust.',upDesc:'Gain 2 Energy. Exhaust.',exhaust:true,energyGain:1,upEnergyGain:2,colorless:true},
// Curses/Status
Wound:{name:'Wound',type:'Curse',cost:99,rarity:'Curse',desc:'Unplayable. Ethereal. Exhaust.',unplayable:true,ethereal:true,exhaust:true},
Doubt:{name:'Doubt',type:'Curse',cost:99,rarity:'Curse',desc:'Unplayable. At end of turn, lose 1 HP.',unplayable:true,doubt:true},
Dazed:{name:'Dazed',type:'Status',cost:99,rarity:'Status',desc:'Unplayable. Ethereal. Exhaust.',unplayable:true,ethereal:true,exhaust:true},
'Imp Claw':{name:'Imp Claw',type:'Attack',cost:0,rarity:'Status',desc:'Deal 4 damage. Exhaust.',dmg:4,exhaust:true,token:true},
'Imp Claw+':{name:'Imp Claw+',type:'Attack',cost:0,rarity:'Status',desc:'Deal 6 damage. Exhaust.',dmg:6,exhaust:true,token:true},
};
const RELICS=[
{id:'Bloodstone',name:'Bloodstone',icon:'🩸',desc:'When you lose HP from a card, heal 1 HP.',rarity:'Common'},
{id:'Demon Horns',name:'Demon Horns',icon:'� horns',desc:'Start of combat: add Imp Claw to hand.',rarity:'Common',cls:'hell'},
{id:'Shadow Cloak',name:'Shadow Cloak',icon:'🥷',desc:'Start of combat: gain 1 Intangible.',rarity:'Rare'},
{id:'Cursed Amulet',name:'Cursed Amulet',icon:'📿',desc:'Start of combat: apply 1 Weak to ALL enemies.',rarity:'Common'},
{id:'Tome of Dark Rituals',name:'Tome of Dark Rituals',icon:'📖',desc:'When you play a Power, draw 1 card.',rarity:'Uncommon'},
{id:'Bleeding Edge',name:'Bleeding Edge',icon:'🗡️',desc:'Start of turn: apply 1 Bleed to random enemy.',rarity:'Common'},
{id:'Soul Ember',name:'Soul Ember',icon:'🔥',desc:'When you apply Burn, deal 2 extra damage.',rarity:'Rare'},
{id:'Eternal Hourglass',name:'Eternal Hourglass',icon:'⏳',desc:'Start of turn if hand empty: gain 1 Energy.',rarity:'Uncommon'},
{id:'Sacrificial Dagger',name:'Sacrificial Dagger',icon:'🗡️',desc:'First HP-cost Attack each combat costs 1 less HP.',rarity:'Uncommon',cls:'blood'},
{id:'Ashen Armor',name:'Ashen Armor',icon:'🛡️',desc:'End of combat: heal 6 HP.',rarity:'Common'},
{id:'Greedy Imp',name:'Greedy Imp',icon:'👹',desc:'Enemies drop +10 Gold.',rarity:'Common'},
{id:'Vial of Shadows',name:'Vial of Shadows',icon:'🧪',desc:'On pickup: remove a card from deck.',rarity:'Rare'},
];
const BOSS_RELICS=[
{id:'Cursed Crown',name:'Cursed Crown',icon:'👑',desc:'+1 Energy each turn. Curses cannot be removed.',energy:1},
{id:'Ectoplasm',name:'Ectoplasm',icon:'👻',desc:'+1 Energy. Lose all Gold and gain no more.',energy:1},
{id:'Philosopher Stone',name:'Philosopher Stone',icon:'💎',desc:'+1 Energy. Enemies start with 1 Strength.',energy:1},
{id:'Black Blood',name:'Black Blood',icon:'🖤',desc:'Start combat −10% HP, but Attacks heal 1 HP.',energy:0},
{id:'Shadow Amulet',name:'Shadow Amulet',icon:'🔮',desc:'Start each turn +1 Intangible. −15 Max HP.',energy:0},
];
const POTIONS=[
{id:'Blood Vial',name:'Blood Vial',icon:'🧴',desc:'Heal 15 HP.'},
{id:'Firebomb',name:'Firebomb',icon:'💣',desc:'Deal 10 damage to ALL.'},
{id:'Dex Tonic',name:'Dex Tonic',icon:'🍷',desc:'Gain 3 Dexterity for 2 turns.'},
{id:'Energy Elixir',name:'Energy Elixir',icon:'⚡',desc:'Gain 2 Energy this turn.'},
{id:'Cursed Brew',name:'Cursed Brew',icon:'☠️',desc:'Apply 3 Vulnerable to ALL.'},
{id:'Bleeding Flask',name:'Bleeding Flask',icon:'🧪',desc:'Apply 6 Bleed to an enemy.'},
{id:'Imp-in-a-Bottle',name:'Imp-in-a-Bottle',icon:'😈',desc:'Add Imp Claw+ to hand.'},
{id:'Smoke Bomb',name:'Smoke Bomb',icon:'💨',desc:'Escape non-boss combat.'},
];
const ENEMIES={
// Act1
Skeleton:{name:'Skeleton Warrior',hp:22,pattern:[{t:'atk',dmg:6},{t:'atk',dmg:6},{t:'def',block:8}]},
Zombie:{name:'Zombie Brute',hp:28,pattern:[{t:'atk',dmg:8},{t:'debuff',weak:2},{t:'atk',dmg:8}]},
Cultist:{name:'Dark Cultist',hp:20,pattern:[{t:'buff',str:3},{t:'atk',dmg:5},{t:'atk',dmg:5}]},
Rat:{name:'Rat Swarm',hp:15,pattern:[{t:'multi',hits:2,dmg:3},{t:'multi',hits:2,dmg:3}]},
Tomb:{name:'Tomb Guardian',hp:42,elite:true,pattern:[{t:'atk',dmg:6,block:8},{t:'multi',hits:3,dmg:4},{t:'atk',dmg:12}]},
BloodWitch:{name:'Blood Witch',hp:36,elite:true,pattern:[{t:'atk',dmg:5,heal:4},{t:'debuff',vuln:1,weak:1},{t:'atk',dmg:7,heal:4}]},
Lich:{name:'Lich King',hp:65,boss:true,pattern:[{t:'summon'},{t:'atk',dmg:8},{t:'def',block:12}]},
// Act2
Wraith:{name:'Wraith',hp:30,pattern:[{t:'atk',dmg:7,weak:1},{t:'atk',dmg:7},{t:'def',block:10}]},
Beast:{name:'Corrupted Beast',hp:38,pattern:[{t:'atk',dmg:12},{t:'def',block:5,vulnSelf:1},{t:'atk',dmg:12}]},
Thorn:{name:'Thorned Horror',hp:25,pattern:[{t:'multi',hits:3,dmg:4},{t:'debuff',bleed:3},{t:'atk',dmg:8}]},
WitchDoc:{name:'Witch Doctor',hp:22,pattern:[{t:'buff',block:4,regen:2},{t:'atk',dmg:6,poison:2},{t:'atk',dmg:6}]},
Treant:{name:'Treant Protector',hp:50,elite:true,pattern:[{t:'atk',dmg:9,block:6},{t:'debuff',vuln:2},{t:'multi',hits:2,dmg:10}]},
Shaman:{name:'Plague Shaman',hp:42,elite:true,pattern:[{t:'debuff',poison:4},{t:'summon'},{t:'atk',dmg:8}]},
Eldritch:{name:'Eldritch Treant',hp:75,boss:true,pattern:[{t:'atk',dmg:10,weak:2},{t:'def',block:15,regen:2},{t:'multi',hits:2,dmg:8}]},
// Act3
Hellhound:{name:'Hellhound',hp:26,pattern:[{t:'atk',dmg:8,burn:2},{t:'atk',dmg:8},{t:'def',block:9}]},
ShadowDemon:{name:'Shadow Demon',hp:30,pattern:[{t:'atk',dmg:7,weak:1},{t:'atk',dmg:7},{t:'buff',intangible:1}]},
ImpEnemy:{name:'Infernal Imp',hp:18,pattern:[{t:'atk',dmg:5},{t:'atk',dmg:5},{t:'summon'}]},
AbyssCult:{name:'Abyssal Cultist',hp:24,pattern:[{t:'buff',str:2,aoeStr:true},{t:'atk',dmg:7},{t:'atk',dmg:7}]},
Flamespawn:{name:'Flamespawn',hp:48,elite:true,pattern:[{t:'atk',dmg:9,burn:3},{t:'def',block:10,str:2},{t:'multi',hits:2,dmg:8}]},
DreadKnight:{name:'Dread Knight',hp:55,elite:true,pattern:[{t:'atk',dmg:12},{t:'debuff',bleed:3},{t:'def',block:12,heal:4}]},
Archdemon:{name:'Archdemon',hp:85,boss:true,pattern:[{t:'summon'},{t:'atk',dmg:12,burn:2},{t:'def',block:15,str:3}]},
};
const EVENTS=[
{title:'The Forgotten Altar',text:'A blood-stained altar hums with power. Dark veins pulse beneath the stone.',choices:[
{label:'Offer 15 HP → Random Rare Relic',act:s=>{if(s.hp<=15)return msg('Not enough HP');changeHp(-15);gainRelic(randomRelic('Rare'));logEvent('You bleed onto the altar. A relic emerges.');closeEventToMap();}},
{label:'Pray → Heal 10 HP, gain Doubt',act:s=>{changeHp(10);addCardToDeck('Doubt');logEvent('You pray. A cold doubt seeps into your deck.');closeEventToMap();}},
{label:'Leave',act:s=>{logEvent('You leave the altar untouched.');closeEventToMap();}},
]},
{title:'Shadowy Peddler',text:'A hooded figure offers a trade, eyes glinting beneath the cowl.',choices:[
{label:'Trade a card → Remove 1 card, +100 Gold',act:s=>{openChoice('Choose card to remove',s.deck, c=>{removeCard(c);s.gold+=100;updateTop();logEvent('Traded card for 100 Gold.');closeChoice();closeEventToMap();});}},
{label:'Buy wares → −50 Gold, Potion + Common Relic',act:s=>{if(s.gold<50)return msg('Need 50 Gold');s.gold-=50;gainPotion();gainRelic(randomRelic('Common'));updateTop();logEvent('You buy the peddler\'s wares.');closeEventToMap();}},
{label:'Attack → Fight Shadow Thief (+75 Gold)',act:s=>{closeEventToMap();startCombat([{key:'Cultist',hp:24}],true,75);}},
]},
{title:'Cursed Spring',text:'A spring bubbles with dark water. It smells of iron and rot.',choices:[
{label:'Drink → −10 HP, remove all Curses',act:s=>{changeHp(-10);let before=s.deck.length;s.deck=s.deck.filter(c=>!['Wound','Doubt','Dazed'].includes(baseName(c)));logEvent(`Curses purged: ${before-s.deck.length} removed.`);closeEventToMap();updateTop();}},
{label:'Bottle it → Gain Cursed Brew',act:s=>{gainPotion('Cursed Brew');logEvent('You bottle the cursed water.');closeEventToMap();}},
{label:'Leave',act:s=>{closeEventToMap();}},
]},
{title:'Restless Dead',text:'Ghosts offer a bargain: vitality for knowledge.',choices:[
{label:'Accept → −20 HP, upgrade 2 random cards',act:s=>{changeHp(-20);let upgradable=s.deck.filter(c=>!isUpgraded(c)&&CARD_DB[baseName(c)]);shuffle(upgradable);for(let i=0;i<Math.min(2,upgradable.length);i++)upgradeCard(upgradable[i]);logEvent('Ghosts upgrade your cards.');closeEventToMap();}},
{label:'Refuse',act:s=>{closeEventToMap();}},
]},
{title:'Impish Gambler',text:'A grinning imp challenges you to a game of chance.',choices:[
{label:'Bet 30 Gold → 50% to win 60',act:s=>{if(s.gold<30)return msg('Need 30 Gold');s.gold-=30;if(Math.random()<0.5){s.gold+=60;logEvent('You won the bet! +30 Gold net.');}else logEvent('You lost the bet.');updateTop();closeEventToMap();}},
{label:'Wager a card → Remove random card',act:s=>{if(!s.deck.length)return;let idx=Math.floor(Math.random()*s.deck.length);let c=s.deck[idx];let isCurse=['Wound','Doubt','Dazed'].includes(baseName(c));s.deck.splice(idx,1);if(isCurse){s.gold+=100;logEvent('Cursed card wagered! +100 Gold.');}else logEvent(`Wagered ${c} — lost.`);updateTop();closeEventToMap();}},
{label:'Leave',act:s=>{closeEventToMap();}},
]},
{title:'Ancient Library',text:'Dusty tomes contain forgotten knowledge. The air crackles.',choices:[
{label:'Study → +1 Energy next combat, −5 HP',act:s=>{s.nextCombatEnergy=(s.nextCombatEnergy||0)+1;changeHp(-5);logEvent('You study. Next combat +1 Energy.');closeEventToMap();}},
{label:'Read forbidden page → Random Power + Doubt',act:s=>{let pool=Object.keys(CARD_DB).filter(k=>CARD_DB[k].type==='Power'&&CARD_DB[k].class===s.cls);let pick=pool[Math.floor(Math.random()*pool.length)];addCardToDeck(pick);addCardToDeck('Doubt');logEvent(`Gained ${pick} and Doubt.`);closeEventToMap();}},
{label:'Leave',act:s=>{closeEventToMap();}},
]},
];
/* ===== STATE ===== */
let state={
cls:null,act:1,floor:1,maxHp:70,hp:70,gold:99,
deck:[],relics:[],potions:[],bossRelics:[],
map:[],currentFloorIdx:0,
nextCombatEnergy:0,
combat:null,
reward:null,
shop:null,
event:null,
pendingChoice:null
};
function baseName(c){return c.replace(/\+$/,'');}
function isUpgraded(c){return c.endsWith('+');}
function cardData(name){let b=baseName(name);let d=CARD_DB[b];if(!d)return null;let up=isUpgraded(name);return {...d,upgraded:up,name:name,displayName:name};}
function cardCost(d){if(d.cost==='X')return 'X';if(d.upgraded&&d.upCost!==undefined)return d.upCost;return d.cost;}
function cardDesc(d){
let b=CARD_DB[baseName(d.name)];
return d.upgraded? b.upDesc: b.desc;
}
/* ===== INIT ===== */
function initClassGrid(){
const g=document.getElementById('classGrid');
g.innerHTML='';
Object.entries(CLASSES).forEach(([k,v])=>{
let deckText={blood:'5 Strike, 4 Defend, 1 Sanguine Strike',shadow:'5 Strike, 4 Defend, 1 Shadow Bolt',hell:'5 Strike, 4 Defend, 1 Firebolt'}[k];
let el=document.createElement('div');
el.className='class-card';
el.innerHTML=`<div class="class-icon">${v.icon}</div><h3>${v.name}</h3><div class="class-desc">${v.desc}</div>
<div class="class-deck"><span class="mini-tag">${deckText}</span><span class="mini-tag">70 HP</span><span class="mini-tag">Sig: ${v.sig}</span></div>
<div style="margin-top:12px"><button class="btn btn-gold btn-small">Choose ${v.name}</button></div>`;
el.onclick=()=>chooseClass(k);
g.appendChild(el);
});
}
initClassGrid();
function chooseClass(k){
state.cls=k;state.act=1;state.floor=1;state.hp=70;state.maxHp=70;state.gold=99;state.relics=[];state.potions=[];state.bossRelics=[];state.nextCombatEnergy=0;
let sig={blood:'Sanguine Strike',shadow:'Shadow Bolt',hell:'Firebolt'}[k];
state.deck=[];
for(let i=0;i<5;i++)state.deck.push('Strike');
for(let i=0;i<4;i++)state.deck.push('Defend');
state.deck.push(sig);
generateMap();
updateTop();
showView('map');
renderMap();
logEvent(`Began run as ${CLASSES[k].name}.`);
}
function updateTop(){
document.getElementById('hpText').textContent=`${state.hp}/${state.maxHp}`;
document.getElementById('hpFill').style.width=`${Math.max(0,state.hp/state.maxHp*100)}%`;
let block = state.combat? state.combat.player.block : 0;
let bt=document.getElementById('blockText');
if(block>0){bt.style.display='inline-block';bt.textContent=`${block} Block`;}else bt.style.display='none';
document.getElementById('goldText').textContent=`${state.gold} Gold`;
document.getElementById('actText').textContent=`ACT ${state.act} — ${['THE CRYPTS','BLIGHTED WOODS','ABYSSAL SPIRE'][state.act-1]}`;
document.getElementById('deckInfo').textContent=`Deck ${state.deck.length}`;
document.getElementById('mapProgress').textContent=`Floor ${state.floor} / 15 • Act ${state.act}`;
renderRelicsPotions();
}
function showView(id){
document.querySelectorAll('.view').forEach(v=>v.classList.remove('active'));
document.getElementById('view-'+id).classList.add('active');
}
/* ===== MAP ===== */
function generateMap(){
// fixed floor types per spec
let types=['start','enemy','enemy','event','shop','enemy','rest','enemy','elite','event','enemy','elite','enemy','rest','boss'];
state.map=[];
for(let i=0;i<15;i++){
let t=types[i];
let floor={idx:i+1,type:t,options:[]};
if(t==='start'){floor.options=[{label:'Start',sub:'Begin'}];}
else if(t==='enemy'){
// branching floors 2 and 8
if(i+1===2) floor.options=[{label:'Skeleton Warrior',sub:'Enemy',enemies:[{key:'Skeleton'}]},{label:'Zombie Brute',sub:'Enemy',enemies:[{key:'Zombie'}]}];
else if(i+1===8) floor.options=[{label:'Skeleton Pair',sub:'2× Skeleton',enemies:[{key:'Skeleton'},{key:'Skeleton'}]},{label:'Zombie Brute',sub:'Enemy',enemies:[{key:'Zombie'}]}];
else if(i+1===3) floor.options=[{label:'Dark Cultist',sub:'Enemy',enemies:[{key:'Cultist'}]}];
else if(i+1===6) floor.options=[{label:'Rat Swarm',sub:'Enemy',enemies:[{key:'Rat'}]}];
else if(i+1===11) floor.options=[{label:'Cultist + Rat',sub:'Duo',enemies:[{key:'Cultist'},{key:'Rat'}]}];
else if(i+1===13) floor.options=[{label:'Cultist Duo',sub:'2× Cultist',enemies:[{key:'Cultist'},{key:'Cultist'}]}];
else floor.options=[{label:'Enemy',sub:'Combat',enemies:[{key:'Skeleton'}]}];
} else if(t==='elite'){
if(i+1===9) floor.options=[{label:'Tomb Guardian',sub:'Elite — Relic',enemies:[{key:'Tomb'}],elite:true},{label:'Skeleton + Rat',sub:'Skip Elite',enemies:[{key:'Skeleton'},{key:'Rat'}]}];
else floor.options=[{label:'Blood Witch',sub:'Elite — Relic',enemies:[{key:'BloodWitch'}],elite:true},{label:'Zombie + Skeleton',sub:'Skip Elite',enemies:[{key:'Zombie'},{key:'Skeleton'}]}];
} else if(t==='event'){ floor.options=[{label:'Mysterious Event',sub:'Choices & risk',event:true}];}
else if(t==='shop'){ floor.options=[{label:'Veiled Bazaar',sub:'Shop',shop:true}];}
else if(t==='rest'){ floor.options=[{label:'Campfire',sub:'Rest / Forge / Remove',rest:true}];}
else if(t==='boss'){
let bossKey=['Lich','Eldritch','Archdemon'][state.act-1];
floor.options=[{label:ENEMIES[bossKey].name,sub:'Boss',enemies:[{key:bossKey}],boss:true}];
}
// act 2/3 enemy remap for non-branch floors
if(state.act===2 && t==='enemy'){
const remap={Skeleton:'Wraith',Zombie:'Beast',Cultist:'WitchDoc',Rat:'Thorn'};
floor.options.forEach(o=>o.enemies=o.enemies.map(e=>({key:remap[e.key]||e.key})));
}
if(state.act===3 && t==='enemy'){
const remap={Skeleton:'Hellhound',Zombie:'ShadowDemon',Cultist:'AbyssCult',Rat:'ImpEnemy'};
floor.options.forEach(o=>o.enemies=o.enemies.map(e=>({key:remap[e.key]||e.key})));
}
if(state.act===2 && t==='elite'){
floor.options.forEach(o=>{if(o.elite){o.enemies=[{key: i+1===9?'Treant':'Shaman'}];o.label=ENEMIES[o.enemies[0].key].name;}});
}
if(state.act===3 && t==='elite'){
floor.options.forEach(o=>{if(o.elite){o.enemies=[{key: i+1===9?'Flamespawn':'DreadKnight'}];o.label=ENEMIES[o.enemies[0].key].name;}});
}
state.map.push(floor);
}
state.currentFloorIdx=1; // start at floor 2 index 1
state.floor=1;
}
function renderMap(){
const grid=document.getElementById('mapGrid');
grid.innerHTML='';
state.map.forEach((floor,idx)=>{
let row=document.createElement('div');
row.className='floor '+(idx<state.currentFloorIdx?'done':idx===state.currentFloorIdx?'current':'');
let label=document.createElement('div');
label.className='floor-label';
label.textContent=`Floor ${floor.idx}`;
row.appendChild(label);
let nodes=document.createElement('div');
nodes.className='nodes';
floor.options.forEach((opt,oi)=>{
let n=document.createElement('div');
let typeCls='node-enemy';
if(floor.type==='elite'&&opt.elite)typeCls='node-elite';
else if(floor.type==='rest')typeCls='node-rest';
else if(floor.type==='shop')typeCls='node-shop';
else if(floor.type==='event')typeCls='node-event';
else if(floor.type==='boss')typeCls='node-boss';
n.className='node '+typeCls+(idx!==state.currentFloorIdx?' disabled':'');
let icon='⚔';
if(typeCls.includes('elite'))icon='⬢';
if(typeCls.includes('rest'))icon='🔥';
if(typeCls.includes('shop'))icon='◈';
if(typeCls.includes('event'))icon='?';
if(typeCls.includes('boss'))icon='♛';
n.innerHTML=`<div class="node-icon">${icon}</div><div><div class="node-name">${opt.label}</div><div class="node-sub">${opt.sub}</div></div>`;
if(idx===state.currentFloorIdx){
n.onclick=()=>enterFloor(idx,oi);
}
nodes.appendChild(n);
});
row.appendChild(nodes);
grid.appendChild(row);
});
updateTop();
}
function enterFloor(floorIdx,optIdx){
let floor=state.map[floorIdx];
let opt=floor.options[optIdx];
state.currentFloorIdx=floorIdx;
state.floor=floor.idx;
if(floor.type==='enemy' || floor.type==='elite' || floor.type==='boss'){
let enemies=opt.enemies.map(e=>{
let base=ENEMIES[e.key];
return {key:e.key,name:base.name,hp:base.hp,maxHp:base.hp,block:0,pattern:base.pattern,idx:0,status:{},elite:!!base.elite,boss:!!base.boss};
});
let isElite=!!opt.elite;
let isBoss=!!opt.boss;
startCombat(enemies,isElite?false:false,isBoss);
// mark elite/boss flags for rewards
state._lastWasElite=isElite;
state._lastWasBoss=isBoss;
} else if(floor.type==='shop'){
openShop();
} else if(floor.type==='rest'){
showView('rest');
} else if(floor.type==='event'){
openEvent();
}
// start floor auto-advance after combat etc handled
}
function advanceMap(){
state.currentFloorIdx++;
if(state.currentFloorIdx>=state.map.length){
// act complete -> next act or victory
if(state.act<3){
state.act++;state.hp=state.maxHp; // full heal after boss per spec
generateMap();
showView('map');
renderMap();
logEvent(`Entered Act ${state.act}. HP fully restored.`);
} else {
showEnd(true);
}
} else {
state.floor=state.map[state.currentFloorIdx].idx;
showView('map');
renderMap();
}
}
/* ===== COMBAT ===== */
function startCombat(enemyDefs,isEventFight,eventGold){
// setup combat state
let drawPile=shuffle([...state.deck]);
let combat={
player:{hp:state.hp,maxHp:state.maxHp,block:0,energy:3,baseEnergy:3,status:{strength:0,dex:0,weak:0,vuln:0,intangible:0,regen:0,poison:0,bleed:0,burn:0,dexNext:0,blockNext:0,energyNext:0},powers:[],nextTurn:{dex:0,block:0,energy:0}},
enemies:enemyDefs.map(e=>({...e,block:0,status:{strength:0,weak:0,vuln:0,bleed:0,burn:0,poison:0,regen:0,intangible:0}})),
draw:drawPile,discard:[],exhaust:[],hand:[],turn:1,
selectedCard:null,selectedIdx:-1,
isBoss:enemyDefs.some(e=>e.boss),
isElite:enemyDefs.some(e=>e.elite),
eventGold:eventGold||0
};
// relic start of combat effects
if(hasRelic('Shadow Cloak')) combat.player.status.intangible+=1;
if(hasRelic('Cursed Amulet')) combat.enemies.forEach(en=>en.status.weak=(en.status.weak||0)+1);
if(hasRelic('Demon Horns') && state.cls==='hell'){combat.hand.push('Imp Claw');}
if(state.bossRelics.includes('Shadow Amulet')) combat.player.status.intangible+=1;
if(state.bossRelics.includes('Philosopher Stone')) combat.enemies.forEach(en=>en.status.strength=(en.status.strength||0)+1);
if(state.bossRelics.includes('Black Blood')) combat.player.hp=Math.max(1,combat.player.hp - Math.floor(combat.player.maxHp*0.1));
if(state.nextCombatEnergy){combat.player.energy+=state.nextCombatEnergy; combat.player.baseEnergy+=state.nextCombatEnergy; state.nextCombatEnergy=0;}
// boss relic ectoplasm etc handled elsewhere
state.combat=combat;
// apply boss phase tracking
combat.enemies.forEach(en=>en.phase=1);
showView('combat');
document.getElementById('combatTitle').textContent=(combat.isBoss?'BOSS — ':'')+combat.enemies.map(e=>e.name).join(' & ');
document.getElementById('playerName').textContent=CLASSES[state.cls].name;
document.getElementById('playerAvatar').textContent=CLASSES[state.cls].icon;
logClear();log(`Combat started vs ${combat.enemies.map(e=>e.name).join(', ')}`);
// initial draw
drawCards(5);
renderCombat();
}
function hasRelic(id){return state.relics.some(r=>r.id===id);}
function drawCards(n){
let c=state.combat;
for(let i=0;i<n;i++){
if(c.draw.length===0){
if(c.discard.length===0) break;
c.draw=shuffle([...c.discard]);c.discard=[];
log('Shuffled discard into draw pile.');
}
let card=c.draw.pop();
// ethereal check? handled on end turn
c.hand.push(card);
// Wound/Doubt no on-draw effect except Doubt end turn
}
}
function renderCombat(){
let c=state.combat; if(!c) return;
// enemies
let row=document.getElementById('enemyRow');row.innerHTML='';
c.enemies.forEach((en,idx)=>{
let div=document.createElement('div');
div.className='enemy-card'+(en.hp<=0?' dead':'')+(c.selectedCard&&needsTarget(c.selectedCard)?' targetable':'');
div.onclick=()=>enemyClicked(idx);
let intent=computeIntent(en,c);
let intentHtml='';
if(en.hp>0){
let cls='intent-def';
let txt=intent.text;
if(intent.type==='atk' || intent.type==='multi') cls='intent-atk';
else if(intent.type==='buff') cls='intent-buff';
else if(intent.type==='debuff') cls='intent-debuff';
intentHtml=`<div class="intent ${cls}"><span>${intent.icon}</span><span>${txt}</span></div>`;
} else intentHtml=`<div class="intent" style="opacity:.6">Defeated</div>`;
let statusHtml=renderStatus(en.status);
let blockHtml=en.block>0?`<span class="block-badge">${en.block} Block</span>`:'';
div.innerHTML=`
<div class="enemy-top"><span class="enemy-name">${en.name}</span>${blockHtml}</div>
<div class="enemy-hp"><div class="enemy-hp-fill" style="width:${Math.max(0,en.hp/en.maxHp*100)}%"></div></div>
<div class="mono" style="font-size:11px;color:var(--muted)">${Math.max(0,en.hp)}/${en.maxHp} HP</div>
${intentHtml}
<div class="status-row">${statusHtml}</div>
`;
row.appendChild(div);
});
// player
document.getElementById('energyOrb').textContent=c.player.energy;
document.getElementById('playerStatuses').innerHTML=renderStatus(c.player.status);
document.getElementById('playerBlockLine').textContent=`Block ${c.player.block} • Powers ${c.player.powers.length} • Turn ${c.turn}`;
document.getElementById('drawCount').textContent=`Draw ${c.draw.length}`;
document.getElementById('discardCount').textContent=`Discard ${c.discard.length}`;
document.getElementById('exhaustCount').textContent=`Exhaust ${c.exhaust.length}`;
// hand
let handEl=document.getElementById('hand');handEl.innerHTML='';
c.hand.forEach((cardName,idx)=>{
let d=cardData(cardName);
let cost=cardCost(d);
let desc=cardDesc(d);
let canPlay = cost!=='X' ? c.player.energy>=cost : true;
if(d.unplayable) canPlay=false;
let div=document.createElement('div');
div.className='card'+(!canPlay?' disabled':'')+(c.selectedIdx===idx?' selected':'');
let typeCls=d.type==='Attack'?'type-attack':d.type==='Power'?'type-power':'type-skill';
div.innerHTML=`
<div class="card-top"><div class="card-cost">${cost}</div><div class="card-type ${typeCls}">${d.type}</div></div>
<div class="card-name">${d.displayName}${d.upgraded?' +':''}</div>
<div class="card-desc">${desc}</div>
${d.upgraded?'<div class="card-up">UPGRADED</div>':''}
`;
div.onclick=()=>cardClicked(idx);
handEl.appendChild(div);
});
updateTop();
}
function renderStatus(s){
let parts=[];
if(s.strength) parts.push(`<span class="status s-buff">STR ${s.strength>0?'+':''}${s.strength}</span>`);
if(s.dex) parts.push(`<span class="status s-buff">DEX ${s.dex>0?'+':''}${s.dex}</span>`);
if(s.weak) parts.push(`<span class="status s-debuff">Weak ${s.weak}</span>`);
if(s.vuln) parts.push(`<span class="status s-debuff">Vuln ${s.vuln}</span>`);
if(s.intangible) parts.push(`<span class="status s-special">Intangible ${s.intangible}</span>`);
if(s.regen) parts.push(`<span class="status s-buff">Regen ${s.regen}</span>`);
if(s.bleed) parts.push(`<span class="status s-debuff">Bleed ${s.bleed}</span>`);
if(s.burn) parts.push(`<span class="status s-debuff">Burn ${s.burn}</span>`);
if(s.poison) parts.push(`<span class="status s-debuff">Poison ${s.poison}</span>`);
return parts.join('');
}
function computeIntent(en,combat){
if(en.hp<=0) return {type:'dead',icon:'✕',text:'Defeated'};
let base=ENEMIES[en.key];
// boss phase handling
if(en.boss){
// Lich phase 2, Eldritch phase2, Archdemon phase2
if(en.key==='Lich' && en.hp<=30 && en.phase===1){en.phase=2;en.idx=0;log(`${en.name} enrages! +2 Strength per turn.`);}
if(en.key==='Eldritch' && en.hp<=40 && en.phase===1){en.phase=2;en.idx=0;log(`${en.name} roots crack and empower!`);}
if(en.key==='Archdemon' && en.hp<=45 && en.phase===1){en.phase=2;en.idx=0;log(`${en.name} unleashes hellfire!`);}
if(en.phase===2){
if(en.key==='Lich') base={pattern:[{t:'atk',dmg:10},{t:'atk',dmg:10},{t:'def',block:15}]};
if(en.key==='Eldritch') base={pattern:[{t:'atk',dmg:14,vuln:1},{t:'def',block:20,heal:5},{t:'atk',dmg:14,vuln:1}]};
if(en.key==='Archdemon') base={pattern:[{t:'atk',dmg:16},{t:'multi',hits:3,dmg:7},{t:'atk',dmg:16}]};
}
}
let pat=base.pattern[en.idx % base.pattern.length];
if(pat.t==='atk') return {type:'atk',icon:'⚔',text:`Attack ${pat.dmg + (en.status.strength||0)}${pat.burn?' +Burn':''}${pat.weak?' +Weak':''}`};
if(pat.t==='multi') return {type:'multi',icon:'⚔',text:`Attack ${pat.hits}×${pat.dmg}`};
if(pat.t==='def') return {type:'def',icon:'🛡',text:`Defend ${pat.block||0}${pat.heal?' Heal':''}`};
if(pat.t==='buff') return {type:'buff',icon:'✦',text:`Buff${pat.str?` +${pat.str} STR`:''}${pat.block?` +${pat.block} Block`:''}${pat.intangible?' Intangible':''}`};
if(pat.t==='debuff') return {type:'debuff',icon:'☠',text:`Debuff${pat.weak?' Weak':''}${pat.vuln?' Vulnerable':''}${pat.bleed?' Bleed':''}${pat.poison?' Poison':''}`};
if(pat.t==='summon') return {type:'buff',icon:' summon',text:'Summon Minion'};
return {type:'atk',icon:'?',text:'Unknown'};
}
function needsTarget(cardName){
let d=cardData(cardName);
let b=CARD_DB[baseName(cardName)];
// single target attacks/skills that apply to one enemy
let single=['Sanguine Strike','Hemorrhage','Bloodletting','Shadow Bolt','Grasp of Despair','Nightmare Lash','Firebolt','Searing Strike','Immolate','Curse of Misfortune','Soul Burn','Bleeding Flask'];
if(b.aoe || b.random || b.randomBurn) return false;
if(d.type==='Attack' && !b.aoe && !b.random) return true;
if(['Hemorrhage','Shadow Bolt','Grasp of Despair','Firebolt','Soul Burn','Curse of Misfortune','Bleeding Flask'].includes(baseName(cardName))) return true;
return false;
}
function cardClicked(idx){
let c=state.combat;
let name=c.hand[idx];
let d=cardData(name);
if(d.unplayable){log('Unplayable card.');return;}
let cost=cardCost(d);
if(cost!=='X' && c.player.energy<cost){log('Not enough Energy.');return;}
if(needsTarget(name)){
c.selectedCard=name;c.selectedIdx=idx;
log(`Selected ${name} — choose a target.`);
renderCombat();
} else {
playCard(idx,null);
}
}
function enemyClicked(eIdx){
let c=state.combat;
if(c.selectedIdx<0) return;
let en=c.enemies[eIdx];
if(en.hp<=0){log('Target is defeated.');return;}
playCard(c.selectedIdx,eIdx);
}
function playCard(handIdx,targetIdx){
let c=state.combat;
let name=c.hand[handIdx];
let d=cardData(name);
let b=CARD_DB[baseName(name)];
let cost=cardCost(d);
let Xval=c.player.energy;
if(cost==='X'){
// Vital Surge: spend all energy
cost=Xval;
}
if(c.player.energy<cost){log('Not enough Energy.');return;}
c.player.energy-=cost;
// remove from hand
c.hand.splice(handIdx,1);
c.selectedCard=null;c.selectedIdx=-1;
// handle hp cost
let hpCost = d.upgraded && b.upHpCost!==undefined ? b.upHpCost : b.hpCost;
if(hpCost){
// Sacrificial Dagger relic
if(hasRelic('Sacrificial Dagger') && !c._daggerUsed && hpCost>0){
hpCost=Math.max(0,hpCost-1);c._daggerUsed=true;
}
let dmgHp=hpCost;
// Intangible reduces to 1
if(c.player.status.intangible>0) dmgHp=Math.min(dmgHp,1);
c.player.hp-=dmgHp;
log(`<b>${name}</b> costs ${dmgHp} HP.`);
// Blood Pact / Bloodstone
if(c.player.powers.includes('Blood Pact')||c.player.powers.includes('Blood Pact+')){
let amt = c.player.powers.includes('Blood Pact+')?2:1;
c.player.status.strength=(c.player.status.strength||0)+amt;
log(`Blood Pact grants +${amt} Strength.`);
} else if(state.relics.some(r=>r.id==='Blood Pact' || r.id==='Blood Pact Power')){}
// check relic Bloodstone heal 1
if(hasRelic('Bloodstone')){
c.player.hp=Math.min(c.player.maxHp,c.player.hp+1);
log('Bloodstone heals 1 HP.');
}
// also check Blood Pact power via combat powers array
// handle generic Blood Pact relic? already
if(c.player.hp<=0){checkPlayerDeath();return;}
}
// apply card effect
applyCardEffect(name,targetIdx,c);
// handle exhaust vs discard
let isPower = d.type==='Power';
if(isPower){
c.player.powers.push(name);
log(`<b>${name}</b> becomes a Power.`);
if(hasRelic('Tome of Dark Rituals')){drawCards(1);log('Tome draws 1 card.');}
} else if(b.exhaust){
c.exhaust.push(name);
} else {
c.discard.push(name);
}
// post-play checks
if(c.player.hp<=0){checkPlayerDeath();return;}
checkEnemiesDead();
if(c.enemies.every(e=>e.hp<=0)){
winCombat();
return;
}
renderCombat();
}
function applyCardEffect(name,targetIdx,c){
let bName=baseName(name);
let d=cardData(name);
let b=CARD_DB[bName];
let up=d.upgraded;
let dmg = up && b.upDmg!==undefined ? b.upDmg : b.dmg;
let block = up && b.upBlock!==undefined ? b.upBlock : b.block;
let heal = up && b.upHeal!==undefined ? b.upHeal : b.heal;
// helper to deal damage to enemy with calc
function dealToEnemy(en,baseDmg){
if(!en||en.hp<=0) return;
let dmgCalc=baseDmg + (c.player.status.strength||0);
if(c.player.status.weak) dmgCalc=Math.floor(dmgCalc*0.75);
if(en.status.vuln) dmgCalc=Math.floor(dmgCalc*1.5);
if(en.status.intangible) dmgCalc=Math.min(dmgCalc,1);
// block
let blocked=Math.min(en.block,dmgCalc);
en.block-=blocked; dmgCalc-=blocked;
en.hp-=dmgCalc;
log(`Dealt ${baseDmg} → ${dmgCalc+blocked} (${blocked} blocked) to ${en.name}.`);
// Black Blood heal
if(state.bossRelics.includes('Black Blood') && d.type==='Attack'){
c.player.hp=Math.min(c.player.maxHp,c.player.hp+1);
log('Black Blood heals 1 HP.');
}
}
function gainBlock(amt){
let add=amt + (c.player.status.dex||0);
if(c.player.status.dex) add=amt + c.player.status.dex;
c.player.block+=Math.max(0,add);
log(`Gained ${add} Block.`);
}
// --- effects ---
if(bName==='Strike' || bName==='Quick Slash'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
} else if(bName==='Defend' || bName==='Iron Will' || bName==='Obsidian Skin'){
gainBlock(block);
} else if(bName==='Sanguine Strike'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
c.player.hp=Math.min(c.player.maxHp,c.player.hp+(up?3:2));
log(`Healed ${up?3:2} HP.`);
} else if(bName==='Hemorrhage'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
en.status.bleed=(en.status.bleed||0)+(up?4:3);
log(`Applied ${up?4:3} Bleed.`);
} else if(bName==='Bloodletting'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
} else if(bName==='Crimson Eruption' || bName==='Hellfire Blast'){
c.enemies.forEach(en=>{if(en.hp>0)dealToEnemy(en,dmg);});
} else if(bName==='Crimson Shield'){
gainBlock(block);
} else if(bName==='Blood Ward'){
gainBlock(block);
c.player.status.regen=(c.player.status.regen||0)+(up?2:1);
log(`Gained ${up?2:1} Regeneration.`);
} else if(bName==='Sacrificial Barrier'){
gainBlock(block);
} else if(bName==='Vital Surge'){
let missing=c.player.maxHp - c.player.hp;
let amt=missing + (up?3:0);
c.player.block+=amt;
log(`Vital Surge gains ${amt} Block.`);
} else if(bName==='Life Tap'){
let dr=up?3:2;
drawCards(dr);
log(`Drew ${dr} cards.`);
} else if(bName==='Rend Flesh'){
c.enemies.forEach(en=>{if(en.hp>0) en.status.bleed=(en.status.bleed||0)+(up?7:5);});
log(`Applied ${up?7:5} Bleed to ALL.`);
} else if(bName==='Soul Siphon' || bName==='Blood Pact' || bName==='Umbral Mastery' || bName==='Hellfire Aura'){
// powers handled via turn start
log(`Power ${bName} active.`);
} else if(bName==='Shadow Bolt'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
en.status.weak=(en.status.weak||0)+(up?2:1);
} else if(bName==='Grasp of Despair'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
en.status.vuln=(en.status.vuln||0)+(up?2:1);
} else if(bName==='Nightmare Lash'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
let extra = en.status.weak? (up?9:7) : 0;
dealToEnemy(en,dmg);
if(extra) dealToEnemy(en,extra);
} else if(bName==='Eternal Darkness'){
for(let i=0;i<3;i++){
let alive=c.enemies.filter(e=>e.hp>0);
if(!alive.length) break;
let en=alive[Math.floor(Math.random()*alive.length)];
dealToEnemy(en,dmg);
}
} else if(bName==='Cloak of Shadows'){
gainBlock(block);
drawCards(1);
} else if(bName==='Haunting Mist'){
gainBlock(block);
c.enemies.forEach(en=>{if(en.hp>0) en.status.weak=(en.status.weak||0)+1;});
} else if(bName==='Phantom Armor'){
gainBlock(block);
c.player.nextTurn.dex=(c.player.nextTurn.dex||0)+(up?3:2);
log(`Next turn +${up?3:2} Dex.`);
} else if(bName==='Whispering Shroud'){
gainBlock(block);
c.player.nextTurn.block=(c.player.nextTurn.block||0)+(up?5:4);
} else if(bName==='Shadowform'){
c.player.status.intangible=(c.player.status.intangible||0)+(up?3:2);
} else if(bName==='Curse of Misfortune'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
en.status.strength=(en.status.strength||0)-(up?4:3);
en.status.vuln=(en.status.vuln||0)+(up?2:1);
log(`Enemy −${up?4:3} Strength, +${up?2:1} Vulnerable.`);
} else if(bName==='Dark Ritual'){
c.player.energy+=2;
let dazed='Dazed';
if(up) c.discard.push(dazed); else c.draw.push(dazed);
shuffle(c.draw);
log('Gained 2 Energy, shuffled Dazed.');
} else if(bName==='Firebolt'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
let br=up?3:2;
en.status.burn=(en.status.burn||0)+br;
if(hasRelic('Soul Ember')){en.hp-=2;log('Soul Ember +2 damage.');}
} else if(bName==='Searing Strike'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
c.player.nextTurn.energy=(c.player.nextTurn.energy||0)+1;
} else if(bName==='Immolate'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
} else if(bName==='Flame Ward'){
gainBlock(block);
let br=up?3:2;
let alive=c.enemies.filter(e=>e.hp>0);
if(alive.length){let en=alive[Math.floor(Math.random()*alive.length)];en.status.burn=(en.status.burn||0)+br; if(hasRelic('Soul Ember')){en.hp-=2;}}
} else if(bName==='Demonic Resilience'){
gainBlock(block);
c.player.hp=Math.min(c.player.maxHp,c.player.hp+(up?3:2));
} else if(bName==='Brimstone Shield'){
gainBlock(block);
c.player.status.strength=(c.player.status.strength||0)+(up?3:2);
} else if(bName==='Summon Imp'){
let claw=up?'Imp Claw+':'Imp Claw';
c.hand.push(claw);
log(`Added ${claw} to hand.`);
} else if(bName==='Infernal Pact'){
c.player.energy+=2;
let claw=up?'Imp Claw+':'Imp Claw';
c.hand.push(claw);
log('Gained 2 Energy and Imp Claw.');
} else if(bName==='Soul Burn'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
let br=up?10:8;
en.status.burn=(en.status.burn||0)+br;
if(hasRelic('Soul Ember')){en.hp-=2;log('Soul Ember +2 damage.');}
drawCards(1);
} else if(bName==='Flash of Insight'){
drawCards(up?3:2);
} else if(bName==='Potent Brew'){
c.player.energy+=(up?2:1);
} else if(bName==='Imp Claw' || bName==='Imp Claw+'){
let en=c.enemies[targetIdx!==null?targetIdx:c.enemies.findIndex(e=>e.hp>0)];
dealToEnemy(en,dmg);
} else if(heal){
c.player.hp=Math.min(c.player.maxHp,c.player.hp+heal);
log(`Healed ${heal} HP.`);
} else if(block){
gainBlock(block);
}
// generic draw
if(b.draw && !['Life Tap','Flash of Insight','Cloak of Shadows','Soul Burn'].includes(bName)){
drawCards(up && b.upDraw!==undefined ? b.upDraw : b.draw);
}
if(b.energyGain && !['Dark Ritual','Infernal Pact','Potent Brew'].includes(bName)){
c.player.energy+= b.energyGain;
}
// handle next turn dex/block via status
// burn/weak etc already
if(b.weak && !['Shadow Bolt','Haunting Mist'].includes(bName)){
if(b.aoe) c.enemies.forEach(en=>{if(en.hp>0) en.status.weak=(en.status.weak||0)+(up&&b.upWeak!==undefined?b.upWeak:b.weak);});
else if(targetIdx!==null){c.enemies[targetIdx].status.weak=(c.enemies[targetIdx].status.weak||0)+(up&&b.upWeak!==undefined?b.upWeak:b.weak);}
}
if(b.vuln && !['Grasp of Despair','Curse of Misfortune'].includes(bName)){
if(b.aoe) c.enemies.forEach(en=>{if(en.hp>0) en.status.vuln=(en.status.vuln||0)+(up&&b.upVuln!==undefined?b.upVuln:b.vuln);});
}
if(b.burn && !['Firebolt','Flame Ward','Soul Burn'].includes(bName)){
let br=up&&b.upBurn!==undefined?b.upBurn:b.burn;
if(b.aoe) c.enemies.forEach(en=>{if(en.hp>0){en.status.burn=(en.status.burn||0)+br; if(hasRelic('Soul Ember'))en.hp-=2;}});
else if(targetIdx!==null){c.enemies[targetIdx].status.burn=(c.enemies[targetIdx].status.burn||0)+br; if(hasRelic('Soul Ember'))c.enemies[targetIdx].hp-=2;}
}
if(b.bleed && !['Hemorrhage','Rend Flesh'].includes(bName)){
let br=up&&b.upBleed!==undefined?b.upBleed:b.bleed;
if(b.aoe) c.enemies.forEach(en=>{if(en.hp>0)en.status.bleed=(en.status.bleed||0)+br;});
}
if(b.str){
c.player.status.strength=(c.player.status.strength||0)+(up&&b.upStr!==undefined?b.upStr:b.str);
}
}
function endTurn(){
let c=state.combat; if(!c) return;
// Doubt damage
let doubtCount=c.hand.filter(x=>baseName(x)==='Doubt').length;
if(doubtCount){c.player.hp-=doubtCount;log(`Doubt deals ${doubtCount} damage.`); if(c.player.hp<=0){checkPlayerDeath();return;}}
// discard hand (ethereal handling)
c.hand.forEach(card=>{
let b=CARD_DB[baseName(card)];
if(b.ethereal){
c.exhaust.push(card);
} else {
c.discard.push(card);
}
});
c.hand=[];
c.player.block=0;
// decrement player debuff durations at start of enemy turn? We'll handle at start of player turn
// enemy turn
enemyTurn();
}
function enemyTurn(){
let c=state.combat;
// apply enemy bleed/burn/poison at start of their turn? spec: Bleed at start, Burn at end, Poison at start
c.enemies.forEach(en=>{
if(en.hp<=0) return;
// Bleed
if(en.status.bleed){
let dmg=en.status.bleed;
if(en.status.intangible) dmg=Math.min(dmg,1);
// block? bleed bypasses block but intangible already
en.hp-=dmg;
log(`${en.name} suffers ${dmg} Bleed.`);
en.status.bleed=Math.floor(en.status.bleed/2);
if(en.status.bleed===0) delete en.status.bleed;
}
if(en.status.poison){
let dmg=en.status.poison;
en.hp-=dmg;
log(`${en.name} suffers ${dmg} Poison.`);
en.status.poison-=1;
if(en.status.poison<=0) delete en.status.poison;
}
});
checkEnemiesDead();
if(c.enemies.every(e=>e.hp<=0)){winCombat();return;}
// each enemy acts
c.enemies.forEach(en=>{
if(en.hp<=0) return;
let base=ENEMIES[en.key];
// handle boss phase already
let pat=base.pattern[en.idx % base.pattern.length];
// special summon
if(pat.t==='summon'){
let alive=c.enemies.filter(e=>e.hp>0).length;
if(alive<3){
let summonKey = en.key==='Lich'?'Skeleton': en.key==='Shaman'?'Rat': en.key==='Archdemon'?'Hellhound': en.key==='ImpEnemy'?'ImpEnemy':'Rat';
// infer
if(en.key==='Lich') summonKey='Skeleton';
if(en.key==='Shaman') summonKey='Rat';
if(en.key==='Archdemon') summonKey='Hellhound';
if(en.key==='ImpEnemy') summonKey='ImpEnemy';
let sBase=ENEMIES[summonKey];
c.enemies.push({key:summonKey,name:sBase.name,hp:15,maxHp:15,block:0,pattern:sBase.pattern,idx:0,status:{}});
log(`${en.name} summons ${sBase.name}!`);
} else {
// attack instead
pat={t:'atk',dmg:5};
}
}
if(pat.t==='atk'){
let dmg=pat.dmg + (en.status.strength||0);
if(en.status.weak) dmg=Math.floor(dmg*0.75);
// player vulnerable
if(c.player.status.vuln) dmg=Math.floor(dmg*1.5);
if(c.player.status.intangible) dmg=Math.min(dmg,1);
let blocked=Math.min(c.player.block,dmg);
c.player.block-=blocked; dmg-=blocked;
c.player.hp-=dmg;
log(`${en.name} attacks for ${pat.dmg + (en.status.strength||0)} → ${dmg+blocked} (${blocked} blocked).`);
if(pat.burn){c.player.status.burn=(c.player.status.burn||0)+pat.burn; log(`Burn +${pat.burn}`);}
if(pat.weak){c.player.status.weak=(c.player.status.weak||0)+pat.weak; log(`Weak +${pat.weak}`);}
if(pat.heal){en.hp=Math.min(en.maxHp,en.hp+pat.heal);log(`${en.name} heals ${pat.heal}.`);}
} else if(pat.t==='multi'){
for(let i=0;i<pat.hits;i++){
let dmg=pat.dmg + (en.status.strength||0);
if(en.status.weak) dmg=Math.floor(dmg*0.75);
if(c.player.status.vuln) dmg=Math.floor(dmg*1.5);
if(c.player.status.intangible) dmg=Math.min(dmg,1);
let blocked=Math.min(c.player.block,dmg);
c.player.block-=blocked; dmg-=blocked;
c.player.hp-=dmg;
log(`${en.name} hits ${i+1}/${pat.hits} for ${dmg+blocked}.`);
if(c.player.hp<=0) break;
}
} else if(pat.t==='def'){
en.block+=(pat.block||0);
log(`${en.name} gains ${pat.block||0} Block.`);
if(pat.heal){en.hp=Math.min(en.maxHp,en.hp+pat.heal);log(`${en.name} heals ${pat.heal}.`);}
if(pat.regen){en.status.regen=(en.status.regen||0)+pat.regen;}
if(pat.str){en.status.strength=(en.status.strength||0)+pat.str;log(`${en.name} +${pat.str} Strength.`);}
} else if(pat.t==='buff'){
if(pat.str){en.status.strength=(en.status.strength||0)+pat.str;log(`${en.name} +${pat.str} Strength.`);}
if(pat.block){en.block+=pat.block;log(`${en.name} +${pat.block} Block.`);}
if(pat.regen){en.status.regen=(en.status.regen||0)+pat.regen;}
if(pat.intangible){en.status.intangible=(en.status.intangible||0)+pat.intangible;log(`${en.name} gains Intangible.`);}
if(pat.aoeStr){c.enemies.forEach(e=>e.status.strength=(e.status.strength||0)+pat.str);log(`All enemies +${pat.str} Strength.`);}
} else if(pat.t==='debuff'){
if(pat.weak){c.player.status.weak=(c.player.status.weak||0)+pat.weak;log(`Player Weak +${pat.weak}`);}
if(pat.vuln){c.player.status.vuln=(c.player.status.vuln||0)+pat.vuln;log(`Player Vulnerable +${pat.vuln}`);}
if(pat.bleed){c.player.status.bleed=(c.player.status.bleed||0)+pat.bleed;log(`Player Bleed +${pat.bleed}`);}
if(pat.poison){c.player.status.poison=(c.player.status.poison||0)+pat.poison;log(`Player Poison +${pat.poison}`);}
if(pat.vulnSelf){en.status.vuln=(en.status.vuln||0)+pat.vulnSelf;}
}
// Burn at end of enemy turn
if(en.status.burn){
let dmg=en.status.burn;
if(en.status.intangible) dmg=Math.min(dmg,1);
en.hp-=dmg;
log(`${en.name} suffers ${dmg} Burn.`);
en.status.burn=Math.floor(en.status.burn/2);
if(en.status.burn===0) delete en.status.burn;
}
// regen
if(en.status.regen){
en.hp=Math.min(en.maxHp,en.hp+en.status.regen);
log(`${en.name} regenerates ${en.status.regen}.`);
en.status.regen-=1;
if(en.status.regen<=0) delete en.status.regen;
}
en.idx=(en.idx+1)%base.pattern.length;
if(c.player.hp<=0) return;
});
checkEnemiesDead();
if(c.player.hp<=0){checkPlayerDeath();return;}
if(c.enemies.every(e=>e.hp<=0)){winCombat();return;}
// start player turn
startPlayerTurn();
}
function startPlayerTurn(){
let c=state.combat;
c.turn++;
// decrement statuses, apply regen, bleed, poison, etc at start of player turn
// Bleed
if(c.player.status.bleed){
let dmg=c.player.status.bleed;
if(c.player.status.intangible) dmg=Math.min(dmg,1);
c.player.hp-=dmg;
log(`You suffer ${dmg} Bleed.`);
c.player.status.bleed=Math.floor(c.player.status.bleed/2);
if(c.player.status.bleed===0) delete c.player.status.bleed;
if(c.player.hp<=0){checkPlayerDeath();return;}
}
if(c.player.status.poison){
let dmg=c.player.status.poison;
if(c.player.status.intangible) dmg=Math.min(dmg,1);
c.player.hp-=dmg;
log(`You suffer ${dmg} Poison.`);
c.player.status.poison-=1;
if(c.player.status.poison<=0) delete c.player.status.poison;
if(c.player.hp<=0){checkPlayerDeath();return;}
}
if(c.player.status.burn){
let dmg=c.player.status.burn;
if(c.player.status.intangible) dmg=Math.min(dmg,1);
c.player.hp-=dmg;
log(`You suffer ${dmg} Burn.`);
c.player.status.burn=Math.floor(c.player.status.burn/2);
if(c.player.status.burn===0) delete c.player.status.burn;
if(c.player.hp<=0){checkPlayerDeath();return;}
}
// regen
if(c.player.status.regen){
c.player.hp=Math.min(c.player.maxHp,c.player.hp+c.player.status.regen);
log(`Regeneration heals ${c.player.status.regen}.`);
c.player.status.regen-=1;
if(c.player.status.regen<=0) delete c.player.status.regen;
}
// decrement weak/vuln/intangible
['weak','vuln','intangible'].forEach(k=>{
if(c.player.status[k]){c.player.status[k]-=1; if(c.player.status[k]<=0) delete c.player.status[k];}
});
c.enemies.forEach(en=>{
['weak','vuln','intangible'].forEach(k=>{if(en.status[k]){en.status[k]-=1; if(en.status[k]<=0) delete en.status[k];}});
});
// apply next turn buffs
if(c.player.nextTurn.dex){c.player.status.dex=(c.player.status.dex||0)+c.player.nextTurn.dex;log(`Gained ${c.player.nextTurn.dex} Dexterity.`);c.player.nextTurn.dex=0;}
if(c.player.nextTurn.block){c.player.block+=c.player.nextTurn.block;log(`Gained ${c.player.nextTurn.block} Block next turn.`);c.player.nextTurn.block=0;}
if(c.player.nextTurn.energy){c.player.energy+=c.player.nextTurn.energy;log(`Gained ${c.player.nextTurn.energy} Energy next turn.`);c.player.nextTurn.energy=0;}
// reset energy
c.player.energy=c.player.baseEnergy;
if(state.bossRelics.includes('Cursed Crown')) c.player.energy+=1;
if(state.bossRelics.includes('Ectoplasm')) c.player.energy+=1;
if(state.bossRelics.includes('Philosopher Stone')) c.player.energy+=1;
// powers start of turn
if(c.player.powers.includes('Soul Siphon')||c.player.powers.includes('Soul Siphon+')){
c.player.energy+=1; c.player.hp-=1; if(c.player.status.intangible) c.player.hp+=0; // already min 1
if(c.player.status.intangible) c.player.hp=Math.min(c.player.maxHp,c.player.hp+0); // intangible reduces to 1 but we already subtracted 1
log('Soul Siphon: +1 Energy, −1 HP.');
if(c.player.hp<=0){checkPlayerDeath();return;}
}
if(c.player.powers.includes('Umbral Mastery')||c.player.powers.includes('Umbral Mastery+')){
let amt=c.player.powers.includes('Umbral Mastery+')?2:1;
c.enemies.forEach(en=>{if(en.hp>0)en.status.weak=(en.status.weak||0)+amt;});
log(`Umbral Mastery applies ${amt} Weak to all.`);
}
if(c.player.powers.includes('Hellfire Aura')||c.player.powers.includes('Hellfire Aura+')){
let dmg=c.player.powers.includes('Hellfire Aura+')?5:3;
c.enemies.forEach(en=>{if(en.hp>0){let d=dmg; if(en.status.vuln) d=Math.floor(d*1.5); en.hp-=d; log(`Hellfire Aura ${d} to ${en.name}.`);}});
checkEnemiesDead();
if(c.enemies.every(e=>e.hp<=0)){winCombat();return;}
}
if(c.player.powers.includes('Blood Pact')||c.player.powers.includes('Blood Pact+')){/* handled on hp cost */}
// Bleeding Edge relic
if(hasRelic('Bleeding Edge')){
let alive=c.enemies.filter(e=>e.hp>0);
if(alive.length){let en=alive[Math.floor(Math.random()*alive.length)];en.status.bleed=(en.status.bleed||0)+1;log('Bleeding Edge +1 Bleed.');}
}
// Eternal Hourglass
if(hasRelic('Eternal Hourglass') && c.hand.length===0){c.player.energy+=1;log('Hourglass +1 Energy (empty hand).');}
// boss relic Shadow Amulet
if(state.bossRelics.includes('Shadow Amulet')){c.player.status.intangible=(c.player.status.intangible||0)+1;log('Shadow Amulet +1 Intangible.');}
// draw
drawCards(5);
renderCombat();
}
function checkEnemiesDead(){
// nothing extra
}
function checkPlayerDeath(){
state.hp=0;
showEnd(false);
}
function winCombat(){
let c=state.combat;
// persist hp
state.hp=Math.max(1,c.player.hp);
// Ashen Armor heal
if(hasRelic('Ashen Armor')){state.hp=Math.min(state.maxHp,state.hp+6);log('Ashen Armor heals 6.');}
// gold
let goldEarned=0;
if(c.isBoss) goldEarned=50;
else if(c.isElite) goldEarned= 25+Math.floor(Math.random()*11);
else goldEarned=10+Math.floor(Math.random()*11);
if(hasRelic('Greedy Imp')) goldEarned+=10;
if(c.eventGold) goldEarned+=c.eventGold;
if(!state.bossRelics.includes('Ectoplasm')) state.gold+=goldEarned;
// potion chance
let potionChance=c.isBoss?1:c.isElite?0.7:0.4;
let gotPotion = Math.random()<potionChance;
// relic if elite
let gotRelic=null;
if(c.isElite){gotRelic=randomRelic(); state.relics.push(gotRelic); if(gotRelic.id==='Vial of Shadows'){/* handled on pickup */}}
// boss relic choice handled via reward screen
state.combat=null;
updateTop();
if(c.isBoss){
// boss rewards: card choice + boss relic choice + 2 potions
showBossReward(goldEarned,gotPotion);
} else {
showReward(goldEarned,gotPotion,gotRelic,c.isElite);
}
}
/* ===== REWARDS ===== */
function showReward(gold,gotPotion,gotRelic,isElite){
showView('reward');
document.getElementById('rewardTitle').textContent=isElite?'Elite Victory!':'Victory!';
document.getElementById('rewardSubtitle').textContent='Choose a card to add to your deck.';
document.getElementById('rewardGold').textContent=`+${gold} Gold${gotRelic?' • Relic: '+gotRelic.name:''}${gotPotion?' • Potion obtained!':''}`;
if(gotPotion) gainPotion();
if(gotRelic) logEvent(`Obtained relic: ${gotRelic.name}`);
// card choices
let choices=generateCardChoices(isElite);
state.reward={choices,chosen:false,gold,gotPotion,gotRelic};
let container=document.getElementById('rewardCards');container.innerHTML='';
choices.forEach((name,idx)=>{
let d=cardData(name);
let el=makeCardEl(d,()=>chooseRewardCard(idx));
container.appendChild(el);
});
document.getElementById('rewardExtra').innerHTML=gotRelic?`<div class="tag">Relic: ${gotRelic.icon} ${gotRelic.name} — ${gotRelic.desc}</div>`:'';
}
function showBossReward(gold,gotPotion){
showView('reward');
document.getElementById('rewardTitle').textContent='Boss Defeated!';
document.getElementById('rewardSubtitle').textContent='Choose a card and a Boss Relic. HP fully restored.';
document.getElementById('rewardGold').textContent=`+${gold} Gold • +2 Potions`;
gainPotion();gainPotion();
state.hp=state.maxHp;
let choices=generateCardChoices(true,true);
state.reward={choices,chosen:false,isBoss:true};
let container=document.getElementById('rewardCards');container.innerHTML='';
choices.forEach((name,idx)=>{
let d=cardData(name);
let el=makeCardEl(d,()=>chooseRewardCard(idx));
container.appendChild(el);
});
// boss relic choices
let extra=document.getElementById('rewardExtra');
extra.innerHTML='<h4 style="color:var(--gold);margin:10px 0">Choose a Boss Relic</h4><div id="bossRelicChoices" style="display:flex;gap:10px;flex-wrap:wrap;justify-content:center"></div>';
let pool=shuffle([...BOSS_RELICS]).slice(0,3);
state._bossChoices=pool;
let brc=document.getElementById('bossRelicChoices');
pool.forEach((r,idx)=>{
let div=document.createElement('div');
div.className='shop-card';
div.style.cursor='pointer';div.style.minWidth='180px';
div.innerHTML=`<div style="font-size:24px">${r.icon}</div><div style="font-family:'Cinzel',serif;color:#fff">${r.name}</div><div style="font-size:12px;color:var(--muted)">${r.desc}</div>`;
div.onclick=()=>chooseBossRelic(idx);
brc.appendChild(div);
});
}
function generateCardChoices(isElite,isBoss){
let pool=Object.keys(CARD_DB).filter(k=>{
let d=CARD_DB[k];
if(d.rarity==='Basic'||d.rarity==='Curse'||d.rarity==='Status'||d.token) return false;
if(d.colorless) return false;
if(d.class && d.class!==state.cls) return false;
return true;
});
let rareChance=isBoss?1:isElite?0.2:0.1;
let picks=[];
for(let i=0;i<3;i++){
let roll=Math.random();
let rarity= roll<rareChance?'Rare': roll<rareChance+0.3?'Uncommon':'Common';
let filtered=pool.filter(k=>CARD_DB[k].rarity===rarity);
if(!filtered.length) filtered=pool;
let choice=filtered[Math.floor(Math.random()*filtered.length)];
picks.push(choice);
}
return picks;
}
function makeCardEl(d,onClick){
let div=document.createElement('div');
div.className='card choice-card';
div.style.cursor='pointer';
let typeCls=d.type==='Attack'?'type-attack':d.type==='Power'?'type-power':'type-skill';
div.innerHTML=`<div class="card-top"><div class="card-cost">${cardCost(d)}</div><div class="card-type ${typeCls}">${d.type}</div></div>
<div class="card-name">${d.displayName}</div><div class="card-desc">${cardDesc(d)}</div><div class="tag" style="margin-top:6px">${d.rarity}</div>`;
div.onclick=onClick;
return div;
}
function chooseRewardCard(idx){
if(state.reward.chosen) return;
state.reward.chosen=true;
let name=state.reward.choices[idx];
addCardToDeck(name);
logEvent(`Added ${name} to deck.`);
document.getElementById('rewardCards').querySelectorAll('.card').forEach((el,i)=>{el.style.opacity=i===idx?'1':'.4';el.style.borderColor=i===idx?'var(--gold)':'';});
}
function chooseBossRelic(idx){
let r=state._bossChoices[idx];
state.bossRelics.push(r.id);
if(r.id==='Ectoplasm'){state.gold=0;logEvent('Ectoplasm: Gold lost!');}
if(r.id==='Shadow Amulet'){state.maxHp=Math.max(1,state.maxHp-15);state.hp=Math.min(state.hp,state.maxHp);}
logEvent(`Boss Relic: ${r.name}`);
document.getElementById('bossRelicChoices').querySelectorAll('.shop-card').forEach((el,i)=>{el.style.opacity=i===idx?'1':'.4';el.style.borderColor=i===idx?'var(--gold)':'';});
updateTop();
}
function skipCardReward(){state.reward.chosen=true;logEvent('Skipped card reward.');}
function continueFromReward(){
if(state.reward && state.reward.isBoss && !state.bossRelics.length){
// allow without? require
}
advanceMap();
}
/* ===== SHOP ===== */
function openShop(){
showView('shop');
let cards=shuffle(Object.keys(CARD_DB).filter(k=>{
let d=CARD_DB[k];
if(d.rarity==='Curse'||d.rarity==='Status'||d.token||d.rarity==='Basic') return false;
if(d.colorless) return false;
if(d.class && d.class!==state.cls) return false;
return true;
})).slice(0,3);
let colorlessPool=Object.keys(CARD_DB).filter(k=>CARD_DB[k].colorless);
let colorless=colorlessPool[Math.floor(Math.random()*colorlessPool.length)];
let allCards=[...cards,colorless];
state.shop={cards:allCards,relics:shuffle([...RELICS]).slice(0,2),potions:shuffle([...POTIONS]).slice(0,3)};
let cardDiv=document.getElementById('shopCards');cardDiv.innerHTML='';
allCards.forEach((name,idx)=>{
let d=cardData(name);
let price=d.colorless?60: d.rarity==='Common'?50: d.rarity==='Uncommon'?75:150;
let el=document.createElement('div');el.className='shop-card';
el.innerHTML=`${makeCardInner(d)}<div class="price">${price} Gold</div><button class="btn btn-gold btn-small" style="margin-top:6px">Buy</button>`;
el.querySelector('button').onclick=()=>buyCard(idx,price);
cardDiv.appendChild(el);
});
let relicDiv=document.getElementById('shopRelics');relicDiv.innerHTML='';
state.shop.relics.forEach((r,idx)=>{
let el=document.createElement('div');el.className='shop-card';
el.innerHTML=`<div style="font-size:28px">${r.icon}</div><div style="font-family:'Cinzel',serif;color:#fff">${r.name}</div><div style="font-size:12px;color:var(--muted)">${r.desc}</div><div class="price">150 Gold</div><button class="btn btn-gold btn-small" style="margin-top:6px">Buy</button>`;
el.querySelector('button').onclick=()=>buyRelic(idx);
relicDiv.appendChild(el);
});
let potDiv=document.getElementById('shopPotions');potDiv.innerHTML='';
state.shop.potions.forEach((p,idx)=>{
let el=document.createElement('div');el.className='shop-card';
el.innerHTML=`<div style="font-size:28px">${p.icon}</div><div style="font-family:'Cinzel',serif;color:#fff">${p.name}</div><div style="font-size:12px;color:var(--muted)">${p.desc}</div><div class="price">50 Gold</div><button class="btn btn-gold btn-small" style="margin-top:6px">Buy</button>`;
el.querySelector('button').onclick=()=>buyPotion(idx);
potDiv.appendChild(el);
});
document.getElementById('shopMsg').textContent='';
}
function makeCardInner(d){
let typeCls=d.type==='Attack'?'type-attack':d.type==='Power'?'type-power':'type-skill';
return `<div style="display:flex;justify-content:space-between"><span class="card-cost" style="width:24px;height:24px;font-size:11px">${cardCost(d)}</span><span class="card-type ${typeCls}" style="font-size:9px">${d.type}</span></div>
<div style="font-family:'Cinzel',serif;color:#fff;margin:6px 0">${d.displayName}</div><div style="font-size:12px;color:#c8c8d8">${cardDesc(d)}</div><div class="tag" style="margin-top:6px">${d.rarity}</div>`;
}
function buyCard(idx,price){
if(state.gold<price){shopMsg('Not enough Gold');return;}
if(state.shop.cards[idx]==null){shopMsg('Sold out');return;}
state.gold-=price;addCardToDeck(state.shop.cards[idx]);state.shop.cards[idx]=null;updateTop();shopMsg(`Bought ${state.deck[state.deck.length-1]}`);openShop();
}
function buyRelic(idx){
if(state.gold<150){shopMsg('Not enough Gold');return;}
let r=state.shop.relics[idx];if(!r){shopMsg('Sold out');return;}
state.gold-=150;state.relics.push(r);if(r.id==='Vial of Shadows'){openChoice('Vial of Shadows: Remove a card',state.deck,c=>{removeCard(c);closeChoice();});}
state.shop.relics[idx]=null;updateTop();shopMsg(`Bought relic ${r.name}`);openShop();
}
function buyPotion(idx){
if(state.gold<50){shopMsg('Not enough Gold');return;}
if(state.potions.length>=3){shopMsg('Potion slots full (3)');return;}
let p=state.shop.potions[idx];if(!p){shopMsg('Sold out');return;}
state.gold-=50;state.potions.push(p);state.shop.potions[idx]=null;updateTop();shopMsg(`Bought ${p.name}`);openShop();
}
function buyRemoval(){
if(state.gold<75){shopMsg('Need 75 Gold');return;}
openChoice('Choose card to remove (75g)',state.deck,c=>{
state.gold-=75;removeCard(c);updateTop();closeChoice();shopMsg(`Removed ${c}`);openShop();
});
}
function shopMsg(t){document.getElementById('shopMsg').textContent=t;setTimeout(()=>document.getElementById('shopMsg').textContent='',2000);}
function leaveShop(){advanceMap();}
/* ===== REST ===== */
function restHeal(){state.hp=Math.min(state.maxHp,state.hp+Math.floor(state.maxHp*0.25));updateTop();logEvent('Rested: healed 25%.');advanceMap();}
function restForge(){openChoice('Choose card to upgrade',state.deck,c=>{upgradeCard(c);closeChoice();advanceMap();});}
function restMeditate(){openChoice('Choose card to remove',state.deck,c=>{removeCard(c);closeChoice();advanceMap();});}
function leaveRest(){advanceMap();}
/* ===== EVENT ===== */
function openEvent(){
showView('event');
let ev=EVENTS[Math.floor(Math.random()*EVENTS.length)];
state.event=ev;
document.getElementById('eventTitle').textContent=ev.title;
document.getElementById('eventText').textContent=ev.text;
let ch=document.getElementById('eventChoices');ch.innerHTML='';
ev.choices.forEach(c=>{
let btn=document.createElement('button');
btn.className='btn btn-ghost';
btn.textContent=c.label;
btn.onclick=()=>c.act(state);
ch.appendChild(btn);
});
}
function leaveEvent(){advanceMap();}
function closeEventToMap(){advanceMap();}
/* ===== UTIL ===== */
function addCardToDeck(name){state.deck.push(name);updateTop();}
function removeCard(name){
let idx=state.deck.indexOf(name);
if(idx>=0) state.deck.splice(idx,1);
updateTop();
}
function upgradeCard(name){
let idx=state.deck.indexOf(name);
if(idx>=0 && !isUpgraded(name)){state.deck[idx]=name+'+';updateTop();logEvent(`Upgraded ${name} → ${name}+`);}
}
function randomRelic(rarity){
let pool=RELICS.filter(r=>!rarity||r.rarity===rarity);
if(state.cls==='hell') pool=RELICS; // allow class specific
// avoid duplicates? allow
return pool[Math.floor(Math.random()*pool.length)];
}
function gainRelic(r){
if(!r) r=randomRelic();
state.relics.push(r);
if(r.id==='Vial of Shadows'){
openChoice('Vial of Shadows: Remove a card',state.deck,c=>{removeCard(c);closeChoice();});
}
updateTop();
}
function gainPotion(id){
if(state.potions.length>=3) return;
let p=id? POTIONS.find(x=>x.id===id) : POTIONS[Math.floor(Math.random()*POTIONS.length)];
state.potions.push(p);
updateTop();
}
function changeHp(delta){
state.hp=Math.max(0,Math.min(state.maxHp,state.hp+delta));
if(state.combat) state.combat.player.hp=state.hp;
updateTop();
if(state.hp<=0) showEnd(false);
}
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 log(t){let el=document.getElementById('combatLog');if(el){let d=document.createElement('div');d.innerHTML=t;el.appendChild(d);el.scrollTop=el.scrollHeight;}}
function logClear(){let el=document.getElementById('combatLog');if(el)el.innerHTML='';}
function logEvent(t){log(t);}
function msg(t){log(t);}
/* deck modal */
function showDeck(){
document.getElementById('deckModal').classList.add('open');
let cont=document.getElementById('deckModalContent');cont.innerHTML='';
state.deck.forEach(name=>{
let d=cardData(name);
let div=document.createElement('div');
div.className='card';div.style.width='140px';div.style.minHeight='160px';
let typeCls=d.type==='Attack'?'type-attack':d.type==='Power'?'type-power':'type-skill';
div.innerHTML=`<div class="card-top"><div class="card-cost">${cardCost(d)}</div><div class="card-type ${typeCls}">${d.type}</div></div><div class="card-name">${d.displayName}${d.upgraded?' +':''}</div><div class="card-desc">${cardDesc(d)}</div>`;
cont.appendChild(div);
});
document.getElementById('deckStats').textContent=`${state.deck.length} cards • ${state.relics.length} relics • ${state.potions.length}/3 potions`;
}
function closeDeck(){document.getElementById('deckModal').classList.remove('open');}
function openChoice(title,list,onPick){
document.getElementById('choiceModal').classList.add('open');
document.getElementById('choiceTitle').textContent=title;
document.getElementById('choiceDesc').textContent='Click a card to choose.';
let grid=document.getElementById('choiceGrid');grid.innerHTML='';
list.forEach(name=>{
let d=cardData(name);
let div=document.createElement('div');
div.className='card';div.style.width='140px';div.style.minHeight='150px';div.style.cursor='pointer';
let typeCls=d.type==='Attack'?'type-attack':d.type==='Power'?'type-power':'type-skill';
div.innerHTML=`<div class="card-top"><div class="card-cost">${cardCost(d)}</div><div class="card-type ${typeCls}">${d.type}</div></div><div class="card-name">${d.displayName}${d.upgraded?' +':''}</div><div class="card-desc">${cardDesc(d)}</div>`;
div.onclick=()=>onPick(name);
grid.appendChild(div);
});
}
function closeChoice(){document.getElementById('choiceModal').classList.remove('open');}
function viewDiscard(){
let c=state.combat; if(!c) return;
let info=`Draw: ${c.draw.join(', ')||'empty'}\nDiscard: ${c.discard.join(', ')||'empty'}\nExhaust: ${c.exhaust.join(', ')||'empty'}\nPowers: ${c.player.powers.join(', ')||'none'}`;
alert(info);
}
function renderRelicsPotions(){
let rL=document.getElementById('relicList'); if(!rL) return;
rL.innerHTML='';
[...state.relics,...state.bossRelics.map(id=>BOSS_RELICS.find(b=>b.id===id)).filter(Boolean)].forEach(r=>{
let div=document.createElement('div');div.className='relic';div.title=`${r.name}: ${r.desc}`;div.textContent=r.icon||'◈';
rL.appendChild(div);
});
let pL=document.getElementById('potionList'); if(!pL) return;
pL.innerHTML='';
state.potions.forEach((p,idx)=>{
let div=document.createElement('div');div.className='potion';
div.innerHTML=`<div class="potion-name">${p.icon} ${p.name}</div><div class="potion-desc">${p.desc}</div>`;
div.onclick=()=>usePotion(idx);
pL.appendChild(div);
});
if(state.potions.length===0) pL.innerHTML='<span class="tag">No potions</span>';
}
function usePotion(idx){
let c=state.combat;
let p=state.potions[idx];
if(!p) return;
// allow use outside combat too for heal
if(p.id==='Blood Vial'){
let heal=15;
if(c) c.player.hp=Math.min(c.player.maxHp,c.player.hp+heal);
state.hp=Math.min(state.maxHp,state.hp+heal);
log(`Used Blood Vial: +15 HP`);
} else if(p.id==='Firebomb'){
if(c) c.enemies.forEach(en=>{if(en.hp>0)en.hp-=10;});
log('Firebomb deals 10 to ALL.');
if(c) checkEnemiesDead();
} else if(p.id==='Dex Tonic'){
if(c){c.player.status.dex=(c.player.status.dex||0)+3; setTimeout(()=>{if(state.combat)state.combat.player.status.dex-=3;},0);}
log('Dex Tonic +3 Dex.');
} else if(p.id==='Energy Elixir'){
if(c) c.player.energy+=2;
log('+2 Energy.');
} else if(p.id==='Cursed Brew'){
if(c) c.enemies.forEach(en=>{if(en.hp>0)en.status.vuln=(en.status.vuln||0)+3;});
log('Cursed Brew: +3 Vulnerable to ALL.');
} else if(p.id==='Bleeding Flask'){
if(c){
// need target - pick first alive
let en=c.enemies.find(e=>e.hp>0);
if(en) en.status.bleed=(en.status.bleed||0)+6;
log('Bleeding Flask +6 Bleed.');
}
} else if(p.id==='Imp-in-a-Bottle'){
if(c) c.hand.push('Imp Claw+');
log('Imp-in-a-Bottle: +Imp Claw+');
} else if(p.id==='Smoke Bomb'){
if(c && !c.isBoss){log('Escaped combat!');state.combat=null;advanceMap();state.potions.splice(idx,1);updateTop();return;}
else {log('Cannot escape boss!');return;}
}
state.potions.splice(idx,1);
updateTop();
if(c){
if(c.enemies.every(e=>e.hp<=0)) winCombat();
else renderCombat();
}
}
function showEnd(victory){
showView('end');
document.getElementById('endTitle').textContent=victory?'VICTORY':'DEFEAT';
document.getElementById('endText').textContent=victory?'You have defeated the Archdemon and cleansed the Abyss. The curses are lifted... for now.':'Your HP reached 0. The Abyss claims another soul. Permadeath — run over.';
}
function resetToClassSelect(){
state.combat=null;
showView('class');
updateTop();
}
function abandonRun(){if(confirm('Abandon run? Progress lost.')) resetToClassSelect();}
// keyboard
document.addEventListener('keydown',e=>{
if(e.key.toLowerCase()==='e' && state.combat) endTurn();
});
// initial top
updateTop();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"e7294f42-2eff-442b-87f5-2f366905c8f7","tokensIn":104753,"tokensOut":32737,"tokensTotal":137490,"cost":0.0170227,"turns":3,"toolCalls":2,"failedToolCalls":0,"timestamp":"2026-08-23T01:47:42.410Z"} -->