← Shadows results

Shadows

Qwen 3.8 · full

Initial view of Shadows

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 90,499 bytes · SHA-256 b95a6f7292fd
<!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>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden;background:#07060b;color:#d8d2c4;font-family:Georgia,'Palatino Linotype',serif}
#bg{position:fixed;inset:0;z-index:0}
#app{position:fixed;inset:0;z-index:1}
.screen{position:absolute;inset:0;display:none;flex-direction:column}
.screen.active{display:flex}
.hidden{display:none!important}
::-webkit-scrollbar{width:10px}
::-webkit-scrollbar-track{background:#0d0a14}
::-webkit-scrollbar-thumb{background:#3a2f4a;border-radius:5px}
/* ---------- buttons & panels ---------- */
.btn{background:linear-gradient(#2a2138,#181224);border:1px solid #6a5a3a;color:#d8c890;padding:10px 22px;font-family:inherit;font-size:14px;letter-spacing:2px;cursor:pointer;border-radius:4px;text-transform:uppercase;transition:all .15s}
.btn:hover{border-color:#c9a227;box-shadow:0 0 14px rgba(200,160,60,.35);color:#ffe9a0}
.btn:disabled{opacity:.35;cursor:default;box-shadow:none}
.btn:disabled:hover{border-color:#6a5a3a;color:#d8c890}
.btn.small{padding:6px 12px;font-size:12px}
.btn.danger{border-color:#7a3a3a}
.btn.danger:hover{border-color:#e05545;box-shadow:0 0 14px rgba(220,80,60,.35)}
.panel{background:rgba(14,10,20,.94);border:1px solid #3a2f4a;border-radius:10px;padding:28px;box-shadow:0 0 50px rgba(0,0,0,.8);max-width:860px;width:92%}
.panel h2{font-size:28px;letter-spacing:4px;color:#c9a227;text-align:center;margin-bottom:8px;text-transform:uppercase}
.panel h3{font-size:16px;letter-spacing:2px;color:#a898c8;margin:14px 0 10px;text-transform:uppercase}
.panel .sub{text-align:center;color:#8a8098;font-size:13px;margin-bottom:16px;letter-spacing:1px}
/* ---------- title ---------- */
#scr-title{align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 30%,rgba(80,20,30,.25),transparent 60%),radial-gradient(ellipse at 50% 90%,rgba(40,20,70,.3),transparent 60%)}
.title-sub{letter-spacing:6px;font-size:13px;color:#8a6a4a;margin-bottom:10px}
.title-main{font-size:clamp(40px,7vw,72px);letter-spacing:10px;color:#c9a227;text-shadow:0 0 40px rgba(200,60,30,.55),0 4px 6px #000;text-align:center;font-weight:normal}
.title-tag{margin:14px 0 34px;color:#9a8fa8;letter-spacing:2px;font-size:14px;font-style:italic}
.class-row{display:flex;gap:26px;justify-content:center;flex-wrap:wrap}
.class-card{width:220px;background:linear-gradient(#191225,#0e0a16);border:2px solid #3a2f4a;border-radius:12px;padding:22px 16px;text-align:center;cursor:pointer;transition:all .2s;position:relative}
.class-card:hover{transform:translateY(-8px);border-color:#c9a227;box-shadow:0 12px 30px rgba(0,0,0,.6),0 0 24px rgba(200,160,60,.25)}
.class-card .cc-art{font-size:56px;filter:drop-shadow(0 0 14px rgba(255,120,40,.35))}
.class-card .cc-name{font-size:17px;letter-spacing:2px;color:#e8dfc8;margin:12px 0 8px;text-transform:uppercase}
.class-card .cc-desc{font-size:12px;color:#9a90a8;line-height:1.5}
.class-card .cc-tag{margin-top:12px;font-size:10px;letter-spacing:2px;color:#c9a227;text-transform:uppercase;opacity:0;transition:opacity .2s}
.class-card:hover .cc-tag{opacity:1}
.title-foot{margin-top:34px;font-size:11px;letter-spacing:2px;color:#5a5068}
/* ---------- HUD ---------- */
.hud{display:flex;align-items:center;gap:14px;padding:10px 18px;background:linear-gradient(#120d1c,#0c0814);border-bottom:1px solid #2a2138;flex-wrap:wrap;z-index:5}
.hud .act-label{font-size:15px;letter-spacing:2px;color:#c9a227;text-transform:uppercase}
.hud .act-sub{font-size:11px;color:#8a8098;letter-spacing:1px}
.hud .spacer{flex:1}
.hud-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(0,0,0,.4);border:1px solid #3a2f4a;border-radius:14px;padding:4px 12px;font-size:13px}
.hud-chip b{color:#e8dfc8}
.relic-ic{font-size:17px;cursor:help}
.hpbar{width:140px;height:12px;background:#241a1a;border:1px solid #512;border-radius:6px;overflow:hidden;display:inline-block;vertical-align:middle}
.hpfill{height:100%;background:linear-gradient(#c0392b,#7b241c);transition:width .3s}
/* ---------- map ---------- */
.map-scroll{flex:1;overflow-y:auto;display:flex;justify-content:center;padding:10px 0 40px}
#mapSvg{filter:drop-shadow(0 0 30px rgba(60,20,80,.25))}
.mnode{cursor:pointer}
.mnode circle{transition:all .15s}
.mnode:hover circle{stroke:#ffe9a0;stroke-width:3}
.mnode.reach circle{animation:nodePulse 1.6s ease-in-out infinite}
@keyframes nodePulse{0%,100%{stroke-opacity:.9}50%{stroke-opacity:.25}}
.mnode.dim{opacity:.35}
.map-legend{position:absolute;bottom:10px;left:50%;transform:translateX(-50%);font-size:11px;color:#6a6080;letter-spacing:1px;background:rgba(10,8,16,.8);padding:6px 14px;border-radius:12px;border:1px solid #2a2138}
/* ---------- combat ---------- */
#scr-combat{background:radial-gradient(ellipse at 50% 20%,rgba(70,20,40,.22),transparent 55%),radial-gradient(ellipse at 85% 70%,rgba(30,15,60,.25),transparent 55%)}
.combat-hud{display:flex;align-items:center;gap:10px;padding:8px 16px;background:rgba(10,7,16,.85);border-bottom:1px solid #2a2138;z-index:5;flex-wrap:wrap}
.combat-hud .ch-label{font-size:13px;letter-spacing:2px;color:#c9a227;text-transform:uppercase}
.combat-hud .spacer{flex:1}
.pile-btn{background:rgba(0,0,0,.4);border:1px solid #3a2f4a;color:#b8b0a0;border-radius:12px;padding:4px 12px;font-size:12px;cursor:pointer;font-family:inherit;letter-spacing:1px}
.pile-btn:hover{border-color:#c9a227;color:#ffe9a0}
.combat-stage{flex:1;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden}
.enemy-row{display:flex;gap:14px;align-items:flex-end;justify-content:center;padding-top:56px}
.enemy{display:flex;flex-direction:column;align-items:center;width:150px;position:relative;padding:8px 4px;border-radius:10px;transition:opacity .4s,transform .4s}
.enemy.dead{opacity:0;transform:scale(.6) rotate(8deg);pointer-events:none}
.enemy.targetable{cursor:pointer}
.enemy.targetable .e-sprite{filter:drop-shadow(0 0 16px rgba(255,80,60,.8))}
.enemy.targetable::after{content:'';position:absolute;inset:0;border:2px solid rgba(255,90,70,.7);border-radius:10px;animation:nodePulse 1s infinite}
.e-intent{position:absolute;top:-38px;left:50%;transform:translateX(-50%);white-space:nowrap;background:rgba(12,8,18,.92);border:1px solid #5a3a5a;border-radius:14px;padding:3px 10px;font-size:12px;color:#e8c8d8;z-index:3}
.e-intent.boss-int{border-color:#c9a227;color:#ffe9a0}
.e-sprite{font-size:58px;line-height:1;filter:drop-shadow(0 8px 14px rgba(0,0,0,.7));transition:transform .1s,filter .1s}
.enemy.boss .e-sprite{font-size:76px}
.enemy.hit .e-sprite{transform:translateX(8px);filter:brightness(2.2) drop-shadow(0 0 12px #f44)}
.enemy.debuffed .e-sprite{filter:hue-rotate(80deg) brightness(.85)}
.e-name{font-size:12px;letter-spacing:1px;color:#c8b8d8;margin-top:6px;text-transform:uppercase}
.e-hpbar{width:112px;height:12px;background:#241a1a;border:1px solid #512;border-radius:6px;overflow:hidden;margin-top:4px}
.e-hpfill{height:100%;background:linear-gradient(#c0392b,#7b241c);transition:width .3s}
.e-hpnum{font-size:11px;color:#d8a8a8;margin-top:2px}
.e-block{position:absolute;right:6px;top:52%;transform:translateY(-50%);background:rgba(20,40,70,.9);border:1px solid #4a7ab0;color:#a8d0ff;border-radius:10px;padding:2px 8px;font-size:13px;font-weight:bold}
.st-row{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:5px;min-height:20px;max-width:150px}
.st{display:inline-flex;align-items:center;gap:3px;background:rgba(0,0,0,.55);border:1px solid #444;border-radius:10px;padding:1px 7px;font-size:11px;color:#d8d2c4}
.st.bad{border-color:#7a3a3a;color:#ff9a8a}
.st.good{border-color:#3a6a4a;color:#9ae0b0}
.turn-banner{position:absolute;left:50%;top:34%;transform:translate(-50%,-50%);font-size:42px;letter-spacing:12px;color:#e8d8a0;text-shadow:0 0 26px rgba(230,180,60,.6);opacity:0;pointer-events:none;z-index:20;text-transform:uppercase;white-space:nowrap}
.turn-banner.show{animation:banner 1.3s ease}
@keyframes banner{0%{opacity:0;transform:translate(-50%,-50%) scale(1.5)}18%{opacity:1;transform:translate(-50%,-50%) scale(1)}78%{opacity:1}100%{opacity:0;transform:translate(-50%,-50%) scale(1)}}
.combat-bottom{display:flex;align-items:flex-end;gap:14px;padding:4px 18px 14px;background:linear-gradient(transparent,rgba(8,5,12,.9));z-index:4}
.player-panel{width:230px;background:rgba(12,8,18,.8);border:1px solid #3a2f4a;border-radius:10px;padding:10px 12px}
.pp-top{display:flex;align-items:center;gap:10px}
.pp-avatar{font-size:38px;filter:drop-shadow(0 0 10px rgba(255,100,60,.35))}
.pp-name{font-size:12px;letter-spacing:2px;color:#c9a227;text-transform:uppercase}
.pp-hp{display:flex;align-items:center;gap:8px;margin-top:8px}
.pp-hp .hpbar{flex:1}
.pp-hpnum{font-size:13px;color:#e8a8a8;min-width:64px}
.pp-block{position:absolute;left:12px;top:-12px;background:rgba(20,40,70,.95);border:1px solid #4a7ab0;color:#a8d0ff;border-radius:12px;padding:2px 10px;font-size:14px;font-weight:bold}
.player-panel{position:relative}
.pp-potions{display:flex;gap:6px;margin-top:8px}
.pot{width:34px;height:34px;border-radius:50%;background:radial-gradient(#2a2138,#141020);border:1px solid #5a4a6a;font-size:17px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s;position:relative}
.pot:hover{border-color:#c9a227;transform:scale(1.15);box-shadow:0 0 10px rgba(200,160,60,.4)}
.pot.lock{opacity:.3;cursor:default}
.energy-orb{width:74px;height:74px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#4a3a7c,#1a1230 70%);border:2px solid #7a5fc0;display:flex;flex-direction:column;align-items:center;justify-content:center;box-shadow:0 0 22px rgba(120,80,220,.4);flex-shrink:0;margin-bottom:26px}
.energy-orb .en-num{font-size:24px;font-weight:bold;color:#d8c8ff}
.energy-orb .en-lab{font-size:9px;letter-spacing:2px;color:#9a8ac8;text-transform:uppercase}
.energy-orb.flash{animation:orbFlash .4s}
@keyframes orbFlash{50%{box-shadow:0 0 36px rgba(160,120,255,.9);transform:scale(1.12)}}
.hand-wrap{flex:1;display:flex;justify-content:center;align-items:flex-end;min-height:196px;padding-bottom:2px}
.hand{display:flex;align-items:flex-end}
.hand-slot{margin:0 -18px;transition:transform .18s;transform:rotate(var(--r,0deg)) translateY(var(--y,0px));position:relative}
.hand-slot:hover{transform:rotate(var(--r,0deg)) translateY(calc(var(--y,0px) - 38px)) scale(1.14);z-index:40}
.hand-slot .card{pointer-events:none}
.hand-slot.dim .card{opacity:.45;filter:grayscale(.5)}
.hand-slot.selected .card{box-shadow:0 0 22px rgba(255,90,70,.8)!important;border-color:#ff6a50!important}
.turn-col{width:130px;display:flex;flex-direction:column;gap:8px;align-items:center;padding-bottom:20px}
.end-turn{width:120px;padding:14px 0;font-size:14px}
.powers-row{display:flex;gap:4px;flex-wrap:wrap;justify-content:center}
.pow-ic{width:30px;height:30px;border-radius:6px;background:linear-gradient(#3a3018,#1c1808);border:1px solid #8a7a2e;font-size:15px;display:flex;align-items:center;justify-content:center;cursor:help}
/* ---------- cards ---------- */
.card{width:126px;height:174px;border-radius:8px;background:linear-gradient(#1c1726,#120e1a);border:2px solid #4a4361;position:relative;display:flex;flex-direction:column;align-items:center;padding:6px 8px 8px;cursor:pointer;transition:box-shadow .15s,border-color .15s;user-select:none;flex-shrink:0}
.card.type-attack{border-color:#8a3a3a;background:linear-gradient(#241418,#140c12)}
.card.type-skill{border-color:#2e6e7e;background:linear-gradient(#142028,#0c1218)}
.card.type-power{border-color:#8a7a2e;background:linear-gradient(#242014,#141008)}
.card.type-curse{border-color:#5a3a7e;background:linear-gradient(#1e1428,#120c18)}
.card.type-status{border-color:#555;background:linear-gradient(#1a1a1a,#101010)}
.card.rar-uncommon{box-shadow:0 0 10px rgba(70,120,220,.4)}
.card.rar-rare{box-shadow:0 0 12px rgba(220,170,60,.5)}
.card.rar-basic{box-shadow:0 0 6px rgba(160,160,160,.2)}
.card.up{border-width:3px}
.c-cost{position:absolute;top:-9px;left:-9px;width:30px;height:30px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#4a3a7c,#1a1430);border:2px solid #7a5fc0;color:#d8c8ff;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:14px;z-index:2}
.c-cost.zero{border-color:#8a8;font-size:13px}
.c-art{font-size:38px;margin-top:12px;filter:drop-shadow(0 0 8px rgba(255,140,60,.3));line-height:1}
.c-name{font-size:12.5px;font-weight:bold;color:#e8dfc8;margin-top:7px;text-align:center;line-height:1.1}
.c-type{font-size:9px;text-transform:uppercase;letter-spacing:2px;color:#8a8098;margin-top:2px}
.c-type.t-attack{color:#c87a6a}.c-type.t-skill{color:#6ab0c0}.c-type.t-power{color:#c8b060}.c-type.t-curse{color:#a07ac8}.c-type.t-status{color:#999}
.c-desc{font-size:10.5px;text-align:center;color:#b8b0a0;margin-top:6px;line-height:1.35;padding:0 2px;flex:1}
.card.small{width:104px;height:146px}
.card.small .c-art{font-size:30px;margin-top:9px}
.card.small .c-desc{font-size:9.5px}
.card-grid{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;padding:10px 0}
.card-grid .card:hover{transform:translateY(-6px);box-shadow:0 8px 20px rgba(0,0,0,.6),0 0 16px rgba(200,160,60,.3)}
.card-grid .card.disabled{opacity:.35;pointer-events:none}
.card-grid .card.pickable:hover{border-color:#c9a227}
/* ---------- overlays / modals ---------- */
.overlay{position:fixed;inset:0;background:rgba(4,2,8,.82);z-index:100;display:flex;align-items:center;justify-content:center}
.modal{max-height:88vh;overflow-y:auto;width:92%}
.modal h2{margin-bottom:14px}
.modal-close{text-align:right;margin-top:16px}
.pile-sec{margin-bottom:18px}
.pile-sec h3{color:#c9a227}
/* ---------- reward / rest / shop / event ---------- */
.center-screen{align-items:center;justify-content:center}
.reward-row{display:flex;gap:16px;justify-content:center;align-items:flex-end;flex-wrap:wrap;margin:14px 0}
.reward-gold{font-size:22px;color:#ffe9a0;text-align:center;margin:6px 0}
.reward-pot{text-align:center;font-size:14px;color:#a8d0ff;margin:4px 0}
.relic-box{display:flex;align-items:center;gap:14px;background:rgba(0,0,0,.4);border:1px solid #6a5a3a;border-radius:10px;padding:14px 18px;margin:10px auto;max-width:480px}
.relic-box .rl-ic{font-size:40px;filter:drop-shadow(0 0 10px rgba(255,180,60,.5))}
.relic-box .rl-name{font-size:16px;color:#ffe9a0;letter-spacing:1px}
.relic-box .rl-desc{font-size:12px;color:#b8b0a0;margin-top:3px;line-height:1.4}
.choice-col{display:flex;flex-direction:column;gap:10px;margin:14px 0}
.choice-col .btn{text-align:left}
.event-art{font-size:64px;text-align:center;margin:8px 0;filter:drop-shadow(0 0 18px rgba(150,80,255,.4))}
.event-text{font-size:15px;line-height:1.6;color:#c8c0b0;text-align:center;font-style:italic;margin:10px 0 18px}
.event-result{background:rgba(0,0,0,.4);border:1px solid #3a6a4a;border-radius:8px;padding:14px;margin:14px 0;font-size:14px;color:#9ae0b0;line-height:1.5;text-align:center}
.shop-grid{display:flex;flex-wrap:wrap;gap:16px;justify-content:center;margin:16px 0}
.shop-item{display:flex;flex-direction:column;align-items:center;gap:8px;width:150px}
.shop-item .price{font-size:15px;color:#ffe9a0}
.shop-item .price.cant{color:#e07060}
.shop-item .btn{width:100%}
.shop-item.sold .card{opacity:.25;filter:grayscale(1)}
.rest-opt{display:flex;align-items:center;gap:16px;background:rgba(0,0,0,.35);border:1px solid #3a2f4a;border-radius:10px;padding:14px 18px;cursor:pointer;transition:all .15s;margin-bottom:12px}
.rest-opt:hover{border-color:#c9a227;background:rgba(60,45,20,.25)}
.rest-opt .ro-ic{font-size:34px}
.rest-opt .ro-name{font-size:16px;letter-spacing:2px;color:#e8dfc8;text-transform:uppercase}
.rest-opt .ro-desc{font-size:12px;color:#9a90a8;margin-top:3px}
/* ---------- end screens ---------- */
.end-panel{text-align:center}
.end-panel .end-art{font-size:72px;margin:10px 0;filter:drop-shadow(0 0 20px rgba(255,80,40,.4))}
.end-panel h2{font-size:34px}
.end-stats{display:flex;gap:22px;justify-content:center;margin:18px 0;flex-wrap:wrap}
.end-stat{background:rgba(0,0,0,.4);border:1px solid #3a2f4a;border-radius:8px;padding:10px 18px}
.end-stat .es-num{font-size:22px;color:#ffe9a0}
.end-stat .es-lab{font-size:10px;letter-spacing:2px;color:#8a8098;text-transform:uppercase;margin-top:2px}
/* ---------- floaters & fx ---------- */
.floater{position:absolute;font-weight:bold;font-size:22px;pointer-events:none;animation:floatUp 1.1s ease-out forwards;z-index:60;text-shadow:0 2px 5px #000}
.floater.dmg{color:#ff6a50}
.floater.block{color:#a8d0ff}
.floater.heal{color:#7ae0a0}
.floater.debuff{color:#c88aff;font-size:15px}
.floater.buff{color:#ffe9a0;font-size:15px}
@keyframes floatUp{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-52px)}}
.shake{animation:shake .35s}
@keyframes shake{0%,100%{transform:translate(0,0)}20%{transform:translate(-7px,4px)}45%{transform:translate(6px,-5px)}70%{transform:translate(-4px,2px)}90%{transform:translate(3px,-1px)}}
.sound-btn{position:fixed;top:10px;right:12px;z-index:200;background:rgba(10,8,16,.7);border:1px solid #3a2f4a;border-radius:14px;padding:5px 12px;font-size:15px;cursor:pointer;color:#d8d2c4}
.sound-btn:hover{border-color:#c9a227}
.target-hint{position:absolute;bottom:210px;left:50%;transform:translateX(-50%);background:rgba(12,8,18,.9);border:1px solid #7a3a3a;border-radius:14px;padding:6px 16px;font-size:12px;color:#ff9a8a;letter-spacing:1px;z-index:30;pointer-events:none}
</style>
</head>
<body>
<canvas id="bg"></canvas>
<div id="app">
  <div id="scr-title" class="screen">
    <div style="text-align:center;max-width:900px;padding:20px">
      <div class="title-sub">A ROGUELIKE DECKBUILDER</div>
      <h1 class="title-main">SHADOWS OF THE ABYSS</h1>
      <div class="title-tag">Fallen heroes. Forbidden magic. A dying world.</div>
      <div class="class-row" id="classRow"></div>
      <div class="title-foot">TURN-BASED COMBAT &bull; PERMADEATH &bull; THREE ACTS TO THE ABYSS</div>
    </div>
  </div>
  <div id="scr-map" class="screen">
    <div class="hud" id="mapHud"></div>
    <div class="map-scroll" id="mapScroll"><svg id="mapSvg" width="480" height="980" viewBox="0 0 480 980"></svg></div>
    <div class="map-legend">⚔ Enemy &nbsp;·&nbsp; ☠ Elite &nbsp;·&nbsp; 🔥 Rest &nbsp;·&nbsp; 💰 Shop &nbsp;·&nbsp; ❓ Event &nbsp;·&nbsp; 👑 Boss</div>
  </div>
  <div id="scr-combat" class="screen">
    <div class="combat-hud" id="combatHud"></div>
    <div class="combat-stage" id="combatStage">
      <div class="enemy-row" id="enemyRow"></div>
      <div class="turn-banner" id="turnBanner"></div>
      <div class="target-hint hidden" id="targetHint">Choose a target — click an enemy (Esc to cancel)</div>
    </div>
    <div class="combat-bottom">
      <div class="player-panel" id="playerPanel"></div>
      <div class="energy-orb" id="energyOrb"><div class="en-num" id="enNum">3</div><div class="en-lab">Energy</div></div>
      <div class="hand-wrap"><div class="hand" id="hand"></div></div>
      <div class="turn-col">
        <button class="btn end-turn" id="endTurnBtn">End Turn</button>
        <div class="powers-row" id="powersRow"></div>
      </div>
    </div>
  </div>
  <div id="scr-reward" class="screen center-screen"><div class="panel" id="rewardPanel"></div></div>
  <div id="scr-rest" class="screen center-screen"><div class="panel" id="restPanel"></div></div>
  <div id="scr-shop" class="screen center-screen"><div class="panel" id="shopPanel"></div></div>
  <div id="scr-event" class="screen center-screen"><div class="panel" id="eventPanel"></div></div>
  <div id="scr-pick" class="screen center-screen"><div class="panel" id="pickPanel"></div></div>
  <div id="scr-end" class="screen center-screen"><div class="panel end-panel" id="endPanel"></div></div>
  <div id="overlay" class="overlay hidden"><div class="panel modal" id="overlayPanel"></div></div>
  <button class="sound-btn" id="soundBtn">🔊</button>
</div>
<script>
'use strict';
/* ============================== utils ============================== */
const $=s=>document.querySelector(s);
const rand=(a,b)=>a+Math.floor(Math.random()*(b-a+1));
const pick=a=>a[Math.floor(Math.random()*a.length)];
const shuffle=a=>{for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];}return a;};
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
let UID=1;
const mkCard=(id,up=false)=>({uid:UID++,id,upgraded:up});
function el(tag,cls,html){const d=document.createElement(tag);if(cls)d.className=cls;if(html!=null)d.innerHTML=html;return d;}
/* ============================== DATA: CARDS ============================== */
const CARDS={
 // basics
 strike:{name:'Strike',type:'attack',cost:1,rarity:'basic',art:'⚔️',desc:'Deal 6 damage.',up:{name:'Strike',desc:'Deal 8 damage.'}},
 defend:{name:'Defend',type:'skill',cost:1,rarity:'basic',art:'🛡️',desc:'Gain 5 Block.',up:{name:'Defend',desc:'Gain 8 Block.'}},
 // colorless
 flash:{name:'Flash of Insight',type:'skill',cost:1,rarity:'common',colorless:true,art:'💡',desc:'Draw 2 cards.',up:{name:'Flash of Insight',desc:'Draw 3 cards.'}},
 ironwill:{name:'Iron Will',type:'skill',cost:1,rarity:'common',colorless:true,art:'🪨',desc:'Gain 8 Block.',up:{name:'Iron Will',desc:'Gain 11 Block.'}},
 quickslash:{name:'Quick Slash',type:'attack',cost:1,rarity:'common',colorless:true,art:'🗡️',desc:'Deal 7 damage.',up:{name:'Quick Slash',desc:'Deal 9 damage.'}},
 potentbrew:{name:'Potent Brew',type:'skill',cost:0,rarity:'uncommon',colorless:true,exhaust:true,art:'⚗️',desc:'Gain 1 Energy. Exhaust.'},
 // blood reaper
 sanguine:{name:'Sanguine Strike',type:'attack',cost:1,rarity:'common',cls:'reaper',art:'🩸',desc:'Deal 8 damage. Heal 2 HP.',up:{name:'Sanguine Strike',desc:'Deal 10 damage. Heal 3 HP.'}},
 hemorrhage:{name:'Hemorrhage',type:'attack',cost:1,rarity:'common',cls:'reaper',art:'🔪',desc:'Deal 5 damage. Apply 3 Bleed.',up:{name:'Hemorrhage',desc:'Deal 6 damage. Apply 4 Bleed.'}},
 bloodletting:{name:'Bloodletting',type:'attack',cost:1,rarity:'uncommon',cls:'reaper',hpCost:3,art:'🩻',desc:'Lose 3 HP. Deal 12 damage.',up:{name:'Bloodletting',desc:'Lose 2 HP. Deal 15 damage.'}},
 crimson:{name:'Crimson Eruption',type:'attack',cost:2,rarity:'rare',cls:'reaper',all:true,hpCost:2,art:'🌋',desc:'Deal 8 damage to ALL enemies. Lose 2 HP.',up:{name:'Crimson Eruption',desc:'Deal 10 damage to ALL enemies. Lose 2 HP.'}},
 crimsonshield:{name:'Crimson Shield',type:'skill',cost:1,rarity:'common',cls:'reaper',hpCost:2,art:'🛡️',desc:'Gain 6 Block. Lose 2 HP.',up:{name:'Crimson Shield',desc:'Gain 8 Block. Lose 2 HP.'}},
 bloodward:{name:'Blood Ward',type:'skill',cost:1,rarity:'common',cls:'reaper',art:'🩹',desc:'Gain 3 Block. Gain 1 Regeneration.',up:{name:'Blood Ward',desc:'Gain 4 Block. Gain 2 Regeneration.'}},
 sacrifice:{name:'Sacrificial Barrier',type:'skill',cost:2,rarity:'uncommon',cls:'reaper',hpCost:5,art:'⛓️',desc:'Gain 10 Block. Lose 5 HP.',up:{name:'Sacrificial Barrier',desc:'Gain 13 Block. Lose 5 HP.'}},
 vitalsurge:{name:'Vital Surge',type:'skill',cost:'X',rarity:'rare',cls:'reaper',exhaust:true,art:'💥',desc:'Gain Block equal to your missing HP. Exhaust.',up:{name:'Vital Surge',desc:'Gain Block equal to missing HP + 3. Exhaust.'}},
 lifetap:{name:'Life Tap',type:'skill',cost:0,rarity:'common',cls:'reaper',hpCost:3,art:'🧠',desc:'Draw 2 cards. Lose 3 HP.',up:{name:'Life Tap',desc:'Draw 3 cards. Lose 3 HP.'}},
 rendflesh:{name:'Rend Flesh',type:'skill',cost:1,rarity:'uncommon',cls:'reaper',all:true,hpCost:2,art:'🦴',desc:'Apply 5 Bleed to ALL enemies. Lose 2 HP.',up:{name:'Rend Flesh',desc:'Apply 7 Bleed to ALL enemies. Lose 2 HP.'}},
 soulsiphon:{name:'Soul Siphon',type:'power',cost:2,rarity:'uncommon',cls:'reaper',art:'🌀',desc:'At the start of your turn, gain 1 Energy and lose 1 HP.',up:{name:'Soul Siphon',cost:1,desc:'At the start of your turn, gain 1 Energy and lose 1 HP.'}},
 bloodpact:{name:'Blood Pact',type:'power',cost:1,rarity:'rare',cls:'reaper',art:'📜',desc:'Whenever you lose HP from a card, gain 1 Strength.',up:{name:'Blood Pact',desc:'Whenever you lose HP from a card, gain 2 Strength.'}},
 // shadowmancer
 shadowbolt:{name:'Shadow Bolt',type:'attack',cost:1,rarity:'common',cls:'mancer',art:'🌑',desc:'Deal 6 damage. Apply 1 Weak.',up:{name:'Shadow Bolt',desc:'Deal 8 damage. Apply 2 Weak.'}},
 grasp:{name:'Grasp of Despair',type:'attack',cost:1,rarity:'common',cls:'mancer',art:'🖐️',desc:'Deal 4 damage. Apply 1 Vulnerable.',up:{name:'Grasp of Despair',desc:'Deal 5 damage. Apply 2 Vulnerable.'}},
 nightmare:{name:'Nightmare Lash',type:'attack',cost:2,rarity:'uncommon',cls:'mancer',art:'👁️',desc:'Deal 7 damage. If enemy has Weak, deal 7 more damage.',up:{name:'Nightmare Lash',desc:'Deal 9 damage. If Weak, deal 9 more.'}},
 darkness:{name:'Eternal Darkness',type:'attack',cost:2,rarity:'rare',cls:'mancer',random:true,art:'🌫️',desc:'Deal 3 damage to a random enemy 3 times.',up:{name:'Eternal Darkness',desc:'Deal 4 damage to a random enemy 3 times.'}},
 cloakshadows:{name:'Cloak of Shadows',type:'skill',cost:1,rarity:'common',cls:'mancer',art:'🧥',desc:'Gain 5 Block. Draw 1 card.',up:{name:'Cloak of Shadows',desc:'Gain 7 Block. Draw 1 card.'}},
 hauntingmist:{name:'Haunting Mist',type:'skill',cost:1,rarity:'common',cls:'mancer',all:true,art:'🌁',desc:'Gain 7 Block. Apply 1 Weak to ALL enemies.',up:{name:'Haunting Mist',desc:'Gain 9 Block. Apply 1 Weak to ALL enemies.'}},
 phantomarmor:{name:'Phantom Armor',type:'skill',cost:1,rarity:'uncommon',cls:'mancer',art:'👻',desc:'Gain 8 Block. Next turn, gain 2 Dexterity.',up:{name:'Phantom Armor',desc:'Gain 10 Block. Next turn, gain 3 Dexterity.'}},
 shroud:{name:'Whispering Shroud',type:'skill',cost:1,rarity:'rare',cls:'mancer',art:'🕸️',desc:'Gain 4 Block. Next turn, gain 4 Block.',up:{name:'Whispering Shroud',desc:'Gain 5 Block. Next turn, gain 5 Block.'}},
 shadowform:{name:'Shadowform',type:'skill',cost:2,rarity:'uncommon',cls:'mancer',exhaust:true,art:'🌘',desc:'Gain 2 Intangible. Exhaust.',up:{name:'Shadowform',desc:'Gain 3 Intangible. Exhaust.'}},
 misfortune:{name:'Curse of Misfortune',type:'skill',cost:1,rarity:'common',cls:'mancer',art:'🎭',desc:'Enemy loses 3 Strength this turn. Apply 1 Vulnerable.',up:{name:'Curse of Misfortune',desc:'Enemy loses 4 Strength this turn. Apply 2 Vulnerable.'}},
 darkritual:{name:'Dark Ritual',type:'skill',cost:0,rarity:'uncommon',cls:'mancer',art:'🕯️',desc:'Gain 2 Energy. Shuffle a Dazed into your discard pile.',up:{name:'Dark Ritual',desc:'Gain 2 Energy. Shuffle a Dazed into your discard pile.'}},
 umbral:{name:'Umbral Mastery',type:'power',cost:2,rarity:'rare',cls:'mancer',art:'😈',desc:'At the start of your turn, apply 1 Weak to ALL enemies.',up:{name:'Umbral Mastery',desc:'At the start of your turn, apply 2 Weak to ALL enemies.'}},
 // hellforged
 firebolt:{name:'Firebolt',type:'attack',cost:1,rarity:'common',cls:'forged',art:'🔥',desc:'Deal 7 damage. Apply 2 Burn.',up:{name:'Firebolt',desc:'Deal 9 damage. Apply 3 Burn.'}},
 hellfire:{name:'Hellfire Blast',type:'attack',cost:2,rarity:'uncommon',cls:'forged',all:true,art:'☄️',desc:'Deal 9 damage to ALL enemies.',up:{name:'Hellfire Blast',desc:'Deal 12 damage to ALL enemies.'}},
 searing:{name:'Searing Strike',type:'attack',cost:1,rarity:'common',cls:'forged',art:'🔱',desc:'Deal 6 damage. Gain 1 Energy next turn.',up:{name:'Searing Strike',desc:'Deal 8 damage. Gain 1 Energy next turn.'}},
 immolate:{name:'Immolate',type:'attack',cost:2,rarity:'rare',cls:'forged',exhaust:true,art:'💀',desc:'Deal 12 damage. Exhaust.',up:{name:'Immolate',desc:'Deal 16 damage. Exhaust.'}},
 obsidian:{name:'Obsidian Skin',type:'skill',cost:1,rarity:'common',cls:'forged',art:'🪨',desc:'Gain 7 Block.',up:{name:'Obsidian Skin',desc:'Gain 10 Block.'}},
 flameward:{name:'Flame Ward',type:'skill',cost:1,rarity:'common',cls:'forged',random:true,art:'🛡️',desc:'Gain 5 Block. Apply 2 Burn to a random enemy.',up:{name:'Flame Ward',desc:'Gain 6 Block. Apply 3 Burn to a random enemy.'}},
 demonic:{name:'Demonic Resilience',type:'skill',cost:1,rarity:'uncommon',cls:'forged',art:'😈',desc:'Gain 4 Block. Heal 2 HP.',up:{name:'Demonic Resilience',desc:'Gain 5 Block. Heal 3 HP.'}},
 brimstone:{name:'Brimstone Shield',type:'skill',cost:1,rarity:'rare',cls:'forged',art:'⚜️',desc:'Gain 3 Block. Gain 2 Strength this turn.',up:{name:'Brimstone Shield',desc:'Gain 4 Block. Gain 3 Strength this turn.'}},
 summonimp:{name:'Summon Imp',type:'skill',cost:1,rarity:'common',cls:'forged',exhaust:true,art:'👹',desc:'Add an Imp Claw to your hand. Exhaust.',up:{name:'Summon Imp',desc:'Add an Imp Claw+ to your hand. Exhaust.'}},
 infernalpact:{name:'Infernal Pact',type:'skill',cost:0,rarity:'uncommon',cls:'forged',hpCost:2,art:'📜',desc:'Lose 2 HP. Gain 2 Energy. Add an Imp Claw to your hand.',up:{name:'Infernal Pact',desc:'Lose 1 HP. Gain 2 Energy. Add an Imp Claw+ to your hand.'}},
 hellaura:{name:'Hellfire Aura',type:'power',cost:2,rarity:'rare',cls:'forged',art:'🌪️',desc:'At the start of your turn, deal 3 damage to ALL enemies.',up:{name:'Hellfire Aura',desc:'At the start of your turn, deal 5 damage to ALL enemies.'}},
 soulburn:{name:'Soul Burn',type:'skill',cost:1,rarity:'uncommon',cls:'forged',art:'🔮',desc:'Apply 8 Burn to an enemy. Draw 1 card.',up:{name:'Soul Burn',desc:'Apply 10 Burn to an enemy. Draw 1 card.'}},
 // tokens & curses
 impclaw:{name:'Imp Claw',type:'attack',cost:0,rarity:'status',token:true,exhaust:true,art:'👹',desc:'Deal 4 damage. Exhaust.',up:{name:'Imp Claw',desc:'Deal 6 damage. Exhaust.'}},
 wound:{name:'Wound',type:'curse',cost:-1,rarity:'status',ethereal:true,exhaust:true,art:'🤢',desc:'Unplayable. Ethereal. Exhaust.'},
 dazed:{name:'Dazed',type:'status',cost:-1,rarity:'status',ethereal:true,exhaust:true,art:'💫',desc:'Unplayable. Ethereal. Exhaust.'},
 doubt:{name:'Doubt',type:'curse',cost:-1,rarity:'curse',art:'🕳️',desc:'Unplayable. At the end of your turn, lose 1 HP.'}
};
function cardDef(c){const b=CARDS[c.id];const u=(c.upgraded&&b.up)?b.up:{};
 return {name:b.name+(c.upgraded&&b.up?'+':''),type:b.type,cost:u.cost!==undefined?u.cost:b.cost,rarity:b.rarity,art:b.art,desc:u.desc||b.desc,
  base:b,baseUp:!!(c.upgraded&&b.up),exhaust:b.exhaust||u.exhaust,ethereal:b.ethereal,all:b.all,random:b.random,hpCost:u.hpCost!==undefined?u.hpCost:b.hpCost,colorless:b.colorless,token:b.token};}
const CLASSES={
 reaper:{name:'Blood Reaper',art:'🩸',desc:'Sacrifice HP for devastating power. Bleed your foes dry.',tag:'Life steal · Bleed · HP sacrifice'},
 mancer:{name:'Shadowmancer',art:'🌑',desc:'Bend shadow to shatter wills. Weaken, blind, and vanish.',tag:'Weak · Vulnerable · Intangible'},
 forged:{name:'Hellforged',art:'🔥',desc:'Wield hellfire and hellspawn. Burn everything to ash.',tag:'Burn · Imps · Energy'}
};
const START_DECK={reaper:['strike','strike','strike','strike','strike','defend','defend','defend','defend','sanguine'],
 mancer:['strike','strike','strike','strike','strike','defend','defend','defend','defend','shadowbolt'],
 forged:['strike','strike','strike','strike','strike','defend','defend','defend','defend','firebolt']};
const RARITY_PRICE={common:50,uncommon:75,rare:150};
/* ============================== DATA: RELICS & POTIONS ============================== */
const RELICS={
 bloodstone:{name:'Bloodstone',art:'🩻',rarity:'common',desc:'Whenever you lose HP from a card, heal 1 HP.'},
 demonhorns:{name:'Demon Horns',art:'🐐',rarity:'common',cls:'forged',desc:'At the start of each combat, add a random Imp Claw to your hand (once per combat).'},
 shadowcloak:{name:'Shadow Cloak',art:'🧥',rarity:'rare',desc:'At the start of each combat, gain 1 Intangible.'},
 cursedamulet:{name:'Cursed Amulet',art:'📿',rarity:'common',desc:'At the start of each combat, apply 1 Weak to ALL enemies.'},
 tome:{name:'Tome of Dark Rituals',art:'📖',rarity:'uncommon',desc:'Whenever you play a Power card, draw 1 card.'},
 bleedingedge:{name:'Bleeding Edge',art:'🗡️',rarity:'common',desc:'At the start of your turn, apply 1 Bleed to a random enemy.'},
 soulemb:{name:'Soul Ember',art:'🔥',rarity:'rare',desc:'Whenever you apply Burn to an enemy, deal 2 additional damage.'},
 hourglass:{name:'Eternal Hourglass',art:'⏳',rarity:'uncommon',desc:'At the start of your turn, if you have no cards in hand, gain 1 Energy.'},
 dagger:{name:'Sacrificial Dagger',art:'🔪',rarity:'uncommon',cls:'reaper',desc:'Your first Attack that costs HP each combat costs 1 less HP.'},
 vialshadows:{name:'Vial of Shadows',art:'⚗️',rarity:'rare',unique:true,desc:'When obtained, remove a card from your deck. Then this relic does nothing.'},
 ashenarmor:{name:'Ashen Armor',art:'🛡️',rarity:'common',desc:'At the end of combat, heal 6 HP.'},
 greedyimp:{name:'Greedy Imp',art:'👺',rarity:'common',desc:'Enemies drop 10 extra Gold.'}
};
const BOSS_RELICS={
 crown:{name:'Cursed Crown',art:'👑',desc:'Gain 1 Energy at start of each turn. Curses can no longer be removed.'},
 ecto:{name:'Ectoplasm',art:'🫧',desc:'Gain 1 Energy. Lose all Gold and gain no more Gold this run.'},
 stone:{name:"Philosopher's Stone",art:'💎',desc:'Gain 1 Energy. All enemies start combat with 1 Strength.'},
 blood:{name:'Black Blood',art:'🖤',desc:'At start of combat, lose 10% max HP, but whenever you play an Attack, heal 1 HP.'},
 amulet:{name:'Shadow Amulet',art:'🌘',desc:'Start each turn with 1 Intangible. Reduce max HP by 15.'}
};
const POTIONS={
 blood:{name:'Blood Vial',art:'🧪',desc:'Heal 15 HP.'},
 firebomb:{name:'Firebomb',art:'💣',desc:'Deal 10 damage to ALL enemies.'},
 dex:{name:'Dex Tonic',art:'🍺',desc:'Gain 3 Dexterity for 1 turn (until end of next turn).'},
 energy:{name:'Energy Elixir',art:'⚡',desc:'Gain 2 Energy this turn.'},
 cursed:{name:'Cursed Brew',art:'🍷',desc:'Apply 3 Vulnerable to all enemies.'},
 bleeding:{name:'Bleeding Flask',art:'🫙',desc:'Apply 6 Bleed to an enemy.'},
 impbottle:{name:'Imp-in-a-Bottle',art:'🫙',desc:'Add an Imp Claw+ to your hand.'},
 smoke:{name:'Smoke Bomb',art:'💨',desc:'Escape from a non-boss combat, no rewards.'}
};
const randPotion=()=>pick(Object.keys(POTIONS));
/* ============================== DATA: ENEMIES ============================== */
const ENEMIES={
 // act 1
 skel:{name:'Skeleton Warrior',hp:22,art:'💀',pat:[{a:'atk',n:6},{a:'atk',n:6},{a:'def',n:8}]},
 zomb:{name:'Zombie Brute',hp:28,art:'🧟',pat:[{a:'atk',n:8},{a:'debuff',weak:2},{a:'atk',n:8}]},
 cult:{name:'Dark Cultist',hp:20,art:'🧙',pat:[{a:'buff',str:3},{a:'atk',n:5},{a:'atk',n:5}]},
 rats:{name:'Rat Swarm',hp:15,art:'🐀',pat:[{a:'multi',hits:2,n:3},{a:'multi',hits:2,n:3}]},
 // act 2
 wraith:{name:'Wraith',hp:30,art:'👻',pat:[{a:'atk',n:7,weak:1},{a:'atk',n:7},{a:'def',n:10}]},
 beast:{name:'Corrupted Beast',hp:38,art:'🐗',pat:[{a:'atk',n:12},{a:'def',n:5,selfvuln:1},{a:'atk',n:12}]},
 thorn:{name:'Thorned Horror',hp:25,art:'🌵',pat:[{a:'multi',hits:3,n:4},{a:'debuff',bleed:3},{a:'atk',n:8}]},
 witchdoc:{name:'Witch Doctor',hp:22,art:'🧙‍♀️',pat:[{a:'allybuff',block:4,regen:2},{a:'atk',n:6,poison:2},{a:'atk',n:6}]},
 // act 3
 hound:{name:'Hellhound',hp:26,art:'🐕',pat:[{a:'atk',n:8,burn:2},{a:'atk',n:8},{a:'def',n:9}]},
 sdemon:{name:'Shadow Demon',hp:30,art:'😈',pat:[{a:'atk',n:7,weak:1},{a:'atk',n:7},{a:'selfint',n:1}]},
 imp:{name:'Infernal Imp',hp:18,art:'👺',pat:[{a:'atk',n:5},{a:'atk',n:5},{a:'summon',id:'imp'}]},
 acult:{name:'Abyssal Cultist',hp:24,art:'🧛',pat:[{a:'allystr',str:2},{a:'atk',n:7},{a:'atk',n:7}]},
 // elites
 tomb:{name:'Tomb Guardian',hp:42,art:'🗿',pat:[{a:'atkdef',n:6,block:8},{a:'multi',hits:3,n:4},{a:'atk',n:12}]},
 bwitch:{name:'Blood Witch',hp:36,art:'🧛‍♀️',pat:[{a:'drain',n:5,heal:4},{a:'debuff',vuln:1,weak:1},{a:'drain',n:7,heal:4}]},
 treant:{name:'Treant Protector',hp:50,art:'🌳',pat:[{a:'atkdef',n:9,block:6},{a:'debuff',vuln:2},{a:'multi',hits:2,n:10}]},
 shaman:{name:'Plague Shaman',hp:42,art:'🐍',pat:[{a:'debuff',poison:4},{a:'summon',id:'prate'},{a:'atk',n:8}]},
 flames:{name:'Flamespawn',hp:48,art:'🦎',pat:[{a:'atk',n:9,burn:3},{a:'def',n:10,str:2},{a:'multi',hits:2,n:8}]},
 dknight:{name:'Dread Knight',hp:55,art:'⚔️',pat:[{a:'atk',n:12},{a:'debuff',bleed:3},{a:'def',n:12,heal:4}]},
 // special
 thief:{name:'Shadow Thief',hp:24,art:'🥷',pat:[{a:'atk',n:6},{a:'def',n:6},{a:'atk',n:9}]},
 skelmin:{name:'Skeleton Minion',hp:15,art:'☠️',pat:[{a:'atk',n:5},{a:'atk',n:5}]},
 prate:{name:'Plague Rat',hp:18,art:'🐀',pat:[{a:'atk',n:5},{a:'atk',n:5}]},
 // bosses
 lich:{name:'Lich King',hp:65,art:'👑',boss:true,
  p1:[{a:'summon',id:'skelmin',max:2},{a:'atk',n:8},{a:'def',n:12}],
  p2:[{a:'atk',n:10},{a:'atk',n:10},{a:'def',n:15}],ph:30,enrage:{str:2}},
 etreant:{name:'Eldritch Treant',hp:75,art:'🌲',boss:true,
  p1:[{a:'atk',n:10,weak:2},{a:'def',n:15,regen:2},{a:'multi',hits:2,n:8}],
  p2:[{a:'atk',n:14,vuln:1},{a:'def',n:20,heal:5},{a:'atk',n:14,vuln:1}],ph:40,enrage:{str:1}},
 arch:{name:'Archdemon',hp:85,art:'👹',boss:true,
  p1:[{a:'summon',id:'hound',max:1},{a:'atk',n:12,burn:2},{a:'def',n:15,str:3}],
  p2:[{a:'atk',n:16},{a:'multi',hits:3,n:7},{a:'atk',n:16}],ph:45,enrage:{str:2}}
};
const ACTS=[
 {name:'The Crypts',floors:'1–15',boss:'lich',
  normal:{2:{L:['skel'],R:['zomb']},3:['cult'],6:['rats'],8:{L:['skel','skel'],R:['zomb']},9:{L:['tomb'],R:['skel','rats']},11:['cult','rats'],12:{L:['bwitch'],R:['zomb','skel']},13:['cult','cult']},
  elites:['tomb','bwitch'],events:[0,1,2,3,4,5]},
 {name:'Blighted Woods',floors:'1–15',boss:'etreant',
  normal:{2:{L:['wraith'],R:['thorn']},3:['beast'],6:['witchdoc'],8:{L:['wraith','wraith'],R:['thorn']},9:{L:['treant'],R:['wraith','thorn']},11:['witchdoc','beast'],12:{L:['shaman'],R:['beast','thorn']},13:['wraith','witchdoc']},
  elites:['treant','shaman'],events:[0,1,2,3,4,5]},
 {name:'Abyssal Spire',floors:'1–15',boss:'arch',
  normal:{2:{L:['sdemon'],R:['imp','imp']},3:['acult'],6:['hound'],8:{L:['sdemon','sdemon'],R:['acult']},9:{L:['flames'],R:['sdemon','imp']},11:['acult','hound'],12:{L:['dknight'],R:['acult','sdemon']},13:['imp','imp','acult']},
  elites:['flames','dknight'],events:[0,1,2,3,4,5]}
];
const NODE_ICON={enemy:'⚔️',elite:'☠️',rest:'🔥',shop:'💰',event:'❓',boss:'👑'};
/* map template: [floor, type, variant] — variant L/R chosen by branch */
const MAP_TEMPLATE=[
 [2,'enemy','both'],[3,'enemy',''],[4,'event',''],[5,'shop',''],[6,'enemy',''],[7,'rest',''],
 [8,'enemy','both'],[9,'elite','branch'],[10,'event',''],[11,'enemy',''],[12,'elite','branch'],[13,'enemy',''],[14,'rest',''],[15,'boss','']
];
function genMap(actIdx){
 const act=ACTS[actIdx];const nodes=[];
 MAP_TEMPLATE.forEach((t,i)=>{
  const [floor,type,variant]=t;
  if(variant==='both'){
   const nL={floor,type:'enemy',comp:act.normal[floor].L,children:[],row:i+1};
   const nR={floor,type:'enemy',comp:act.normal[floor].R,children:[],row:i+1};
   nodes.push(nL,nR);
   return;
  }
  if(variant==='branch'){
   const nL={floor,type:'elite',comp:[act.elites[floor===9?0:1]],children:[],row:i+1};
   const nR={floor,type:'enemy',comp:act.normal[floor].R,children:[],row:i+1};
   nodes.push(nL,nR);
   return;
  }
  let comp=null;
  if(type==='enemy')comp=act.normal[floor];
  if(type==='boss')comp=[act.boss];
  nodes.push({floor,type,comp,children:[],row:i+1});
 });
 const rows=[];nodes.forEach(n=>{if(!rows.length||rows[rows.length-1].floor!==n.floor)rows.push([n]);rows[rows.length-1].push(n);});
 for(let r=0;r<rows.length-1;r++)rows[r].forEach(a=>rows[r+1].forEach(b=>a.children.push(b)));
 return {nodes,rows,start:rows[0]};
}
/* ============================== DATA: EVENTS ============================== */
const EVENTS=[
 {name:'The Forgotten Altar',art:'🕯️',text:'A blood-stained altar hums with power. The air tastes of iron and old prayers.',
  choices:[
   {label:'Offer 15 HP',run(){if(G.hp<=0)return 'You cannot afford the price.';G.hp-=15;const r=pick(Object.keys(RELICS));if(r==='vialshadows')return 'The altar recoils from your shadows. You leave empty-handed.';giveRelic(r);return 'Blood soaks the stone. A relic materializes: '+RELICS[r].name+'.';}},
   {label:'Pray',run(){heal(10);addCardToDeck(mkCard('doubt'));return 'Whispers answer your prayer. You heal 10 HP, but Doubt creeps into your deck.';}},
   {label:'Leave',run(){return 'You back away from the altar. Nothing happens.';}}]},
 {name:'Shadowy Peddler',art:'🎭',text:'A hooded figure leans against a pillar that shouldn\'t be there. "Trade, friend? I deal in secrets."',
  choices:[
   {label:'Trade a card (gain 100 Gold)',run(){return 'tradeCardForGold';}},
   {label:'Buy his wares (−50 Gold)',run(){if(G.gold<50)return 'You don\'t have 50 Gold.';G.gold-=50;addPotion(randPotion());const r=pick(Object.keys(RELICS).filter(k=>RELICS[k].cls===undefined));giveRelic(r);return 'The peddler grins. You receive a potion and '+RELICS[r].name+'.';}},
   {label:'Attack',run(){G.pendingThiefGold=75;startCombat({enemies:['thief'],boss:false,elite:false,thief:true});return 'combat';}}]},
 {name:'Cursed Spring',art:'⛲',text:'A spring bubbles with dark water. Something watches from beneath the surface.',
  choices:[
   {label:'Drink',run(){const n=G.deck.filter(c=>CARDS[c.id].type==='curse').length;G.hp-=10;G.deck=G.deck.filter(c=>CARDS[c.id].type!=='curse');return 'The water burns. You lose 10 HP, but '+n+' Curse'+(n===1?'':'s')+' vanish from your deck.';}},
   {label:'Bottle it',run(){addPotion('cursed');return 'You fill a flask with the dark water. (Cursed Brew)';}},
   {label:'Leave',run(){return 'You leave the spring undisturbed.';}}]},
 {name:'Restless Dead',art:'👻',text:'Spectral figures circle a bonfire that gives no heat. "Rest, hero. We will sharpen your blades."',
  choices:[
   {label:'Accept (−20 HP)',run(){if(G.hp<=20)return 'You are too wounded to risk this bargain.';G.hp-=20;const up=[];for(let i=0;i<2&&G.deck.length;i++){const c=pick(G.deck);if(!c.upgraded&&CARDS[c.id].up){c.upgraded=true;up.push(CARDS[c.id].name);}}return up.length?('You lose 20 HP. Upgraded: '+up.join(', ')+'.'):'You lose 20 HP, but your cards are already perfected.';}},
   {label:'Refuse',run(){return 'The ghosts fade into the mist, disappointed.';}}]},
 {name:'Impish Gambler',art:'🎲',text:'A grinning imp flips a coin with six sides. "Lose, and you pay. Win, and you feast."',
  choices:[
   {label:'Bet 30 Gold',run(){if(G.gold<30)return 'You don\'t have 30 Gold.';G.gold-=30;if(Math.random()<0.5){G.gold+=60;return 'The coin lands heads! You win 60 Gold.';}return 'The coin lands on the cursed side. You lose 30 Gold.';}},
   {label:'Wager a card',run(){if(!G.deck.length)return 'Your deck is empty — nothing to wager.';const i=rand(0,G.deck.length-1);const c=G.deck.splice(i,1)[0];if(CARDS[c.id].type==='curse'){G.gold+=100;return 'You wager '+CARDS[c.id].name+'. It\'s a Curse! The imp pays 100 Gold.';}return 'You wager '+CARDS[c.id].name+'. The imp keeps it and offers nothing in return.';}},
   {label:'Leave',run(){return 'You refuse the imp\'s game. It cackles as you go.';}}]},
 {name:'Ancient Library',art:'📚',text:'Dusty tomes line walls that stretch into darkness. The knowledge here is older than the abyss.',
  choices:[
   {label:'Study (−5 HP)',run(){G.hp-=5;G.nextCombatEnergy=1;return 'The words sear your mind. You lose 5 HP but gain 1 extra Energy in your next combat.';}},
   {label:'Read forbidden page',run(){const pool=Object.keys(CARDS).filter(k=>CARDS[k].type==='power'&&CARDS[k].cls===G.cls);const c=mkCard(pool.length?pick(pool):'soulsiphon');addCardToDeck(c);addCardToDeck(mkCard('doubt'));return 'Dark power floods you. Added to your deck: '+CARDS[c.id].name+'. Doubt also joins your deck.';}},
   {label:'Leave',run(){return 'You close the book and step away.';}}]}
];
/* ============================== CORE STATE ============================== */
const G={
 cls:null,act:0,floor:1,hp:70,maxHp:70,gold:99,
 deck:[],relics:[],potions:[],
 map:null,curNode:null,reachedNodes:[],
 nextCombatEnergy:0,
 stats:{kills:0,goldEarned:0,dealt:0,taken:0,upgrades:0,removed:0},
 dead:false,escaped:false
};
function addCardToDeck(c){G.deck.push(c);}
function addPotion(id){if(G.potions.length>=3)return false;G.potions.push(id);return true;}
function giveRelic(id,quiet){G.relics.push(id);if(id==='vialshadows'&&!quiet){openPick('vial');}return true;}
function heal(n){G.hp=clamp(G.hp+n,0,G.maxHp);}
function gainGold(n){if(G.relics.includes('ecto'))return;G.gold+=n;G.stats.goldEarned+=n;}
function hasRelic(id){return G.relics.includes(id);}
/* ============================== AUDIO ============================== */
let AC=null,soundOn=true;
function ac(){if(!AC){try{AC=new (window.AudioContext||window.webkitAudioContext)();}catch(e){}}if(AC&&AC.state==='suspended')AC.resume();return AC;}
function tone(f,d,type='sine',v=0.12,slide=0){if(!soundOn)return;const a=ac();if(!a)return;const o=a.createOscillator(),g=a.createGain();o.type=type;o.frequency.value=f;if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),a.currentTime+d);g.gain.value=v;g.gain.exponentialRampToValueAtTime(0.001,a.currentTime+d);o.connect(g);g.connect(a.destination);o.start();o.stop(a.currentTime+d);}
const SFX={
 click:()=>tone(660,.08,'triangle',.08),
 card:()=>{tone(520,.07,'triangle',.09);setTimeout(()=>tone(780,.06,'triangle',.07),40);},
 hit:()=>{tone(160,.12,'sawtooth',.14,-80);},
 block:()=>{tone(320,.1,'square',.09,-120);},
 heal:()=>{tone(440,.15,'sine',.1,220);},
 draw:()=>tone(880,.05,'triangle',.05),
 turn:()=>{tone(220,.12,'sine',.1);setTimeout(()=>tone(330,.12,'sine',.09),90);},
 death:()=>{tone(200,.3,'sawtooth',.12,-140);},
 win:()=>{[440,554,659,880].forEach((f,i)=>setTimeout(()=>tone(f,.18,'triangle',.1),i*110));},
 lose:()=>{[330,277,220,165].forEach((f,i)=>setTimeout(()=>tone(f,.22,'sawtooth',.09),i*140));},
 gold:()=>{tone(1200,.07,'triangle',.08);setTimeout(()=>tone(1600,.09,'triangle',.07),60);},
 err:()=>tone(140,.15,'square',.09)
};
$('#soundBtn').onclick=()=>{soundOn=!soundOn;$('#soundBtn').textContent=soundOn?'🔊':'🔇';if(soundOn)SFX.click();};
/* ============================== BACKGROUND ============================== */
(function(){
 const cv=$('#bg'),ctx=cv.getContext('2d');let W,H,parts=[];
 function rs(){W=cv.width=innerWidth;H=cv.height=innerHeight;}
 addEventListener('resize',rs);rs();
 for(let i=0;i<70;i++)parts.push({x:Math.random(),y:Math.random(),s:Math.random()*2+0.5,v:Math.random()*0.0004+0.00015,p:Math.random()*6.28});
 let t=0;
 (function loop(){t+=0.016;
  const g=ctx.createRadialGradient(W*0.5,H*0.25,50,W*0.5,H*0.5,Math.max(W,H)*0.8);
  g.addColorStop(0,'#120a18');g.addColorStop(0.5,'#0a0712');g.addColorStop(1,'#040308');
  ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
  // mist bands
  for(let i=0;i<3;i++){const y=H*(0.3+0.25*i)+Math.sin(t*0.3+i*2)*30;
   const mg=ctx.createLinearGradient(0,y-60,0,y+60);
   mg.addColorStop(0,'rgba(60,30,80,0)');mg.addColorStop(0.5,`rgba(70,35,90,${0.05+0.02*Math.sin(t*0.5+i)})`);mg.addColorStop(1,'rgba(60,30,80,0)');
   ctx.fillStyle=mg;ctx.fillRect(0,y-60,W,120);}
  // embers
  parts.forEach(p=>{p.y-=p.v*16;if(p.y<-0.02){p.y=1.02;p.x=Math.random();}
   const x=p.x*W+Math.sin(t+p.p)*14;
   ctx.globalAlpha=0.25+0.2*Math.sin(t*2+p.p);
   ctx.fillStyle=Math.random()<0.5?'#c86a3a':'#8a4ac8';
   ctx.beginPath();ctx.arc(x,p.y*H,p.s,0,6.28);ctx.fill();});
  ctx.globalAlpha=1;
  // vignette
  const vg=ctx.createRadialGradient(W/2,H/2,H*0.35,W/2,H/2,H*0.85);
  vg.addColorStop(0,'rgba(0,0,0,0)');vg.addColorStop(1,'rgba(0,0,0,0.55)');
  ctx.fillStyle=vg;ctx.fillRect(0,0,W,H);
  requestAnimationFrame(loop);})();
})();
/* ============================== SCREENS ============================== */
function show(id){document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));$('#'+id).classList.add('active');}
function openOverlay(html){$('#overlayPanel').innerHTML=html;$('#overlay').classList.remove('hidden');}
function closeOverlay(){$('#overlay').classList.add('hidden');$('#overlayPanel').innerHTML='';}
/* ============================== CARD DOM ============================== */
function cardEl(c,small){
 const d=cardDef(c);
 const div=el('div','card type-'+d.type+' rar-'+(d.rarity||'basic')+(c.upgraded?' up':'')+(small?' small':''));
 const cost=d.cost===-1?'✖':(d.cost==='X'?'X':d.cost);
 div.innerHTML=`<div class="c-cost ${d.cost===0?'zero':''}">${cost}</div>
  <div class="c-art">${d.art}</div>
  <div class="c-name">${d.name}</div>
  <div class="c-type t-${d.type}">${d.type}</div>
  <div class="c-desc">${d.desc}</div>`;
 return div;
}
/* status icons */
const ST_INFO={strength:{i:'💪',bad:false,tip:'Strength: +X attack damage'},dexterity:{i:'🌀',bad:false,tip:'Dexterity: +X Block from cards'},
 regeneration:{i:'💗',bad:false,tip:'Regeneration: heal X at end of turn, then −1'},intangible:{i:'👻',bad:false,tip:'Intangible: all damage reduced to 1 (X turns)'},
 weak:{i:'🐌',bad:true,tip:'Weak: deal 25% less damage (X turns)'},vulnerable:{i:'🎯',bad:true,tip:'Vulnerable: take 50% more damage (X turns)'},
 bleed:{i:'🩸',bad:true,tip:'Bleed: lose HP equal to stacks at start of turn, then halve'},burn:{i:'🔥',bad:true,tip:'Burn: lose HP equal to stacks at end of turn, then halve'},
 poison:{i:'☠️',bad:true,tip:'Poison: lose X HP at start of turn, then −1'}};
function statusChips(st){
 let h='';
 for(const k in st){if(st[k]>0)h+=`<span class="st ${ST_INFO[k].bad?'bad':'good'}" title="${ST_INFO[k].tip}">${ST_INFO[k].i}${st[k]}</span>`;}
 return h;
}
function playerAtk(n){
 const P=C.player;
 let d=n+P.st.strength+(P.tempStr||0);
 if(P.st.weak>0)d=Math.floor(d*0.75);
 return Math.max(0,d);
}
/* ============================== COMBAT ENGINE ============================== */
let C=null; // active combat
let busy=false; // input lock during animations
function newEnemy(id){
 const b=ENEMIES[id];
 return {id,name:b.name,hp:b.hp,maxHp:b.hp,block:0,turn:0,phase:1,enraged:false,
  st:{strength:0,dexterity:0,regeneration:0,intangible:0,weak:0,vulnerable:0,bleed:0,burn:0,poison:0},
  tempStr:0,
  pat:b.boss?b.p1:b.pat,
  boss:!!b.boss,ph:b.ph||0,art:b.art,
  summonCount:0,
  el:null};
}
function aliveEnemies(){return C?C.enemies.filter(e=>e.hp>0):[];}
function startCombat(opts){
 const enemies=opts.enemies.map(newEnemy);
 C={enemies,boss:opts.boss,elite:opts.elite,thief:opts.thief,turn:0,
  player:{hp:G.hp,block:0,energy:3,nextEnergy:0,tempStr:0,
   st:{strength:0,dexterity:0,regeneration:0,intangible:0,weak:0,vulnerable:0,bleed:0,burn:0,poison:0},
   powers:[],dexTurns:0,shroudNext:0,
   draw:[],discard:[],exhaust:[],hand:[],
   daggerUsed:false,hornUsed:false},
  scUsed:false,
  over:false};
 // relics at combat start
 if(hasRelic('cursedamulet'))enemies.forEach(e=>e.st.weak+=1);
 if(hasRelic('stone'))enemies.forEach(e=>e.st.strength+=1);
 if(hasRelic('blood')){G.hp-=Math.floor(G.maxHp*0.1);if(G.hp<=0){G.hp=1;}}
 if(hasRelic('amulet'))C.player.st.intangible+=1;
 if(hasRelic('demonhorns')&&G.cls==='forged'){C.player.hand.push(mkCard('impclaw'));C.player.hornUsed=true;}
 // build draw pile
 C.player.draw=shuffle(G.deck.map(c=>c));
 C.turn=1;
 G.escaped=false;
 show('scr-combat');
 renderCombat();
 banner('Your Turn');
 SFX.turn();
 startPlayerTurn(true);
}
function drawCards(n){
 const p=C.player;
 for(let i=0;i<n;i++){
  if(!p.draw.length){
   if(!p.discard.length)break;
   p.draw=shuffle(p.discard);p.discard=[];
  }
  const c=p.draw.pop();
  p.hand.push(c);
  const d=cardDef(c);
  if(d.ethereal){p.exhaust.push(c);p.hand.splice(p.hand.indexOf(c),1);}
 }
 SFX.draw();
}
function startPlayerTurn(first){
 const p=C.player;
 C._selTarget=null;
 p.energy=3+(p.nextEnergy||0)+(G.nextCombatEnergy>0?1:0);
 if(G.nextCombatEnergy>0)G.nextCombatEnergy=0;
 p.nextEnergy=0;
 // durations decrease at start of player's turn (weak, vulnerable, intangible)
 if(p.st.intangible>0)p.st.intangible--;
 if(p.st.weak>0)p.st.weak--;
 if(p.st.vulnerable>0)p.st.vulnerable--;
 // bleed at start of player turn
 if(p.st.bleed>0){const d=p.st.bleed;playerHurt(d);p.st.bleed=Math.floor(d/2);renderCombat();}
 // powers
 for(const pw of p.powers){
  const u=pw.upgraded;
  if(pw.id==='soulsiphon'){p.energy+=1;playerHurt(1);floaterPlayer('buff','⚡ +1');}
  if(pw.id==='umbral'){const n=u?2:1;aliveEnemies().forEach(e=>e.st.weak+=n);floaterEnemy(e=>true,'debuff','Weak +'+n);}
  if(pw.id==='hellaura'){const n=u?5:3;aliveEnemies().forEach(e=>enemyHurt(e,n));}
 }
 // relics
 if(hasRelic('crown'))p.energy+=1;
 if(hasRelic('ecto'))p.energy+=1;
 if(hasRelic('stone'))p.energy+=1;
 if(hasRelic('amulet'))p.st.intangible+=1;
 if(hasRelic('bleedingedge')){const e=pick(aliveEnemies());if(e)e.st.bleed+=1;}
 if(hasRelic('hourglass')&&!p.hand.length)p.energy+=1;
 // dex tonic expiry
 if(p.dexTurns>0){p.dexTurns--;if(p.dexTurns===0)p.st.dexterity-=3;}
 // phantom armor "next turn dexterity"
 if(p.shroudNext>0){p.st.dexterity+=p.shroudNext;p.shroudNext=0;}
 drawCards(5);
 busy=false;
 renderCombat();
 if(G.hp<=0)combatEnd(false);
}
function endPlayerTurn(){
 if(busy||!C||C.over)return;
 busy=true;
 const p=C.player;
 // doubt
 let doubts=p.hand.filter(c=>c.id==='doubt').length;
 if(doubts>0){playerHurt(doubts,'doubt');}
 // discard hand
 p.hand.forEach(c=>p.discard.push(c));
 p.hand=[];
 // regeneration
 if(p.st.regeneration>0){heal(p.st.regeneration);p.st.regeneration--;floaterPlayer('heal','+'+p.st.regeneration);}
 // burn at end of player turn
 if(p.st.burn>0){const d=p.st.burn;playerHurt(d,'burn');p.st.burn=Math.floor(d/2);}
 p.block=0;
 if(G.hp<=0){renderCombat();combatEnd(false);return;}
 C.turn++;
 renderCombat();
 banner('Enemy Turn');
 setTimeout(enemyPhase,800);
}
function enemyPhase(){
 (async()=>{
  for(const e of C.enemies){
   if(e.hp<=0||!C)continue;
   await sleep(350);
   // enemy turn start: decrement durations, bleed, poison
   for(const k of ['weak','vulnerable'])if(e.st[k]>0)e.st[k]--;
   if(e.st.bleed>0){const d=e.st.bleed;enemyHurt(e,d);e.st.bleed=Math.floor(d/2);renderCombat();await sleep(300);}
   if(e.st.poison>0){enemyHurt(e,e.st.poison);e.st.poison-=1;renderCombat();await sleep(300);}
   // boss enrage
   if(e.boss&&e.phase===1&&e.hp<=e.ph){e.phase=2;e.pat=e.id==='lich'?ENEMIES.lich.p2:e.id==='etreant'?ENEMIES.etreant.p2:ENEMIES.arch.p2;e.turn=0;banner(e.name+' Enrages!');SFX.death();}
   if(e.boss&&e.phase===2&&ENEMIES[e.id].enrage){e.st.strength+=ENEMIES[e.id].enrage.str;}
   // act intent
   const intent=e.pat[e.turn%e.pat.length];
   await doEnemyIntent(e,intent);
   e.turn++;
   // enemy turn end: regeneration, burn
   if(e.st.regeneration>0){e.hp=Math.min(e.maxHp,e.hp+e.st.regeneration);e.st.regeneration--;}
   if(e.st.burn>0){const d=e.st.burn;enemyHurt(e,d);e.st.burn=Math.floor(d/2);}
   renderCombat();
   if(aliveEnemies().length===0){combatEnd(true);return;}
  }
  if(!C||C.over)return;
  await sleep(300);
  startPlayerTurn(false);
 })();
}
function doEnemyIntent(e,it){
 return (async()=>{
  const P=C.player;
  switch(it.a){
   case 'atk':{
    await enemyAttack(e,it.n);
    if(it.weak)applyToPlayer('weak',it.weak);
    if(it.burn)applyToPlayer('burn',it.burn);
    if(it.vuln)applyToPlayer('vulnerable',it.vuln);
    break;}
   case 'multi':{
    for(let i=0;i<it.hits;i++){await enemyAttack(e,it.n);await sleep(280);}
    break;}
   case 'atkdef':{
    e.block+=it.block;renderCombat();await sleep(300);
    await enemyAttack(e,it.n);
    break;}
   case 'def':{
    e.block+=it.n;
    if(it.str)e.st.strength+=it.str;
    if(it.regen)e.st.regeneration+=it.regen;
    if(it.heal)e.hp=Math.min(e.maxHp,e.hp+it.heal);
    if(it.selfvuln)e.st.vulnerable+=it.selfvuln;
    break;}
   case 'buff':{
    e.st.strength+=it.str;
    break;}
   case 'debuff':{
    if(it.weak)applyToPlayer('weak',it.weak);
    if(it.vuln)applyToPlayer('vulnerable',it.vuln);
    if(it.bleed)applyToPlayer('bleed',it.bleed);
    if(it.poison)applyToPlayer('poison',it.poison);
    break;}
   case 'drain':{
    await enemyAttack(e,it.n);
    if(e.hp>0){e.hp=Math.min(e.maxHp,e.hp+it.heal);floaterEnemy(e,'heal','+'+it.heal);}
    break;}
   case 'allybuff':{
    const ally=aliveEnemies().length>1?pick(aliveEnemies().filter(x=>x!==e)):e;
    ally.block+=it.block;ally.st.regeneration+=it.regen;
    floaterEnemy(x=>x===ally,'buff','Buff!');
    break;}
   case 'allystr':{
    aliveEnemies().forEach(x=>x.st.strength+=it.str);
    break;}
   case 'selfint':{
    e.st.intangible+=it.n;
    break;}
   case 'summon':{
    let count=C.enemies.filter(x=>x.id===it.id&&x.hp>0).length;
    if(it.max!==undefined&&count>=it.max){await enemyAttack(e,6);break;}
    const m=newEnemy(it.id);
    C.enemies.push(m);
    renderCombat();
    banner('Summoned!');
    SFX.death();
    await sleep(500);
    break;}
  }
  renderCombat();
  await sleep(250);
 })();
}
async function enemyAttack(e,n){
 const P=C.player;
 let dmg=n+e.st.strength+e.tempStr;
 if(dmg<0)dmg=0;
 const blocked=applyDamageToPlayer(dmg);
 SFX.hit();
 shakePlayer();
 G.stats.taken+=dmg-blocked;
 await sleep(380);
 renderCombat();
 if(G.hp<=0)combatEnd(false);
}
function applyDamageToPlayer(dmg){
 const P=C.player;
 if(P.st.intangible>0)dmg=Math.min(dmg,1);
 const blocked=Math.min(P.block,dmg);
 P.block-=blocked;
 const hpLoss=dmg-blocked;
 G.hp-=hpLoss;
 if(hpLoss>0)floaterPlayer('dmg','-'+hpLoss);
 else if(blocked>0)floaterPlayer('block','🛡'+blocked);
 else floaterPlayer('dmg','0');
 return blocked;
}
function playerHurt(n){
 // HP loss (card costs, doubt, bleed, burn, powers) — Intangible reduces to 1
 const P=C.player;
 if(P.st.intangible>0)n=Math.min(n,1);
 G.hp-=n;
 if(n>0)floaterPlayer('dmg','-'+n);
}
function healPlayer(n){G.hp=clamp(G.hp+n,0,G.maxHp);floaterPlayer('heal','+'+n);SFX.heal();}
function applyToPlayer(k,n){C.player.st[k]+=n;floaterPlayer('debuff',k.toUpperCase()+' +'+n);}
function enemyHurt(e,n){
 if(n<=0)return;
 if(e.st.intangible>0)n=Math.min(n,1);
 const blocked=Math.min(e.block,n);
 e.block-=blocked;
 e.hp-=(n-blocked);
 G.stats.dealt+=(n-blocked);
 floaterEnemy(x=>x===e,'dmg',n-blocked>0?'-'+(n-blocked):'🛡');
 if(e.hp<=0){e.hp=0;e.el&&e.el.classList.add('dead');SFX.death();}
}
function applyDebuffToEnemy(e,k,n){
 e.st[k]+=n;
 if(k==='burn'&&hasRelic('soulemb'))enemyHurt(e,2);
 floaterEnemy(x=>x===e,'debuff',k.toUpperCase()+' +'+n);
}
/* ---- card playing ---- */
function canPlay(c){
 const d=cardDef(c);
 if(d.type==='curse'||d.type==='status')return false;
 if(d.cost==='X')return C.player.energy>=0;
 return C.player.energy>=d.cost;
}
function hpCostOf(c){
 const d=cardDef(c);
 let n=d.hpCost;
 if(n>0&&C.player.daggerUsed===false&&d.type==='attack'&&hasRelic('dagger')){n=Math.max(0,n-1);C.player.daggerUsed=true;}
 return n;
}
function playCard(c){
 if(busy||!C||C.over)return;
 const d=cardDef(c);
 if(!canPlay(c)){SFX.err();return;}
 busy=true;
 const P=C.player;
 const idx=P.hand.indexOf(c);
 P.hand.splice(idx,1);
 P.energy-=d.cost;
 (async()=>{
  await sleep(120);
  const hpC=hpCostOf(c);
  if(hpC>0){
   G.hp-=hpC;
   floaterPlayer('dmg','-'+hpC+' HP');
   if(hasRelic('bloodstone')){G.hp=clamp(G.hp+1,0,G.maxHp);floaterPlayer('heal','+1');}
   if(P.powers.some(p=>p.id==='bloodpact')){const s=c.upgraded?2:1;P.st.strength+=s;floaterPlayer('buff','💪 +'+s);}
   if(G.hp<=0){renderCombat();combatEnd(false);return;}
  }
  if(d.type==='attack'&&hasRelic('blood')){G.hp=clamp(G.hp+1,0,G.maxHp);floaterPlayer('heal','+1');}
  await execCard(c,d,hpC);
  if(d.type==='power'){P.powers.push(c);if(hasRelic('tome'))drawCards(1);}
  else if(d.exhaust){P.exhaust.push(c);}
  else P.discard.push(c);
  busy=false;
  renderCombat();
  if(G.hp<=0)combatEnd(false);
  else if(aliveEnemies().length===0)combatEnd(true);
 })();
}
function healBlackBlood(){if(hasRelic('blood')){G.hp=clamp(G.hp+1,0,G.maxHp);floaterPlayer('heal','+1');}}
async function execCard(c,d,hpC){
 const P=C.player;
 const up=c.upgraded;
 const tgt=C._selTarget||null;
 // per-card logic
 switch(c.id){
  case 'strike':enemyHurt(tgt,playerAtk(up?8:6));break;
  case 'defend':{const b=(up?8:5)+P.st.dexterity;P.block+=b;floaterPlayer('block','🛡+'+b);break;}
  case 'flash':drawCards(up?3:2);break;
  case 'ironwill':{const b=(up?11:8)+P.st.dexterity;P.block+=b;floaterPlayer('block','🛡+'+b);break;}
  case 'quickslash':enemyHurt(tgt,playerAtk(up?9:7));break;
  case 'potentbrew':P.energy+=up?2:1;break;
  case 'sanguine':enemyHurt(tgt,playerAtk(up?10:8));healPlayer(up?3:2);break;
  case 'hemorrhage':enemyHurt(tgt,playerAtk(up?6:5));applyDebuffToEnemy(tgt,'bleed',up?4:3);break;
  case 'bloodletting':enemyHurt(tgt,playerAtk(up?15:12));break;
  case 'crimson':aliveEnemies().forEach(e=>enemyHurt(e,playerAtk(up?10:8)));break;
  case 'crimsonshield':P.block+=up?8:6+P.st.dexterity;floaterPlayer('block','🛡');break;
  case 'bloodward':P.block+=(up?4:3)+P.st.dexterity;P.st.regeneration+=up?2:1;floaterPlayer('buff','💗');break;
  case 'sacrifice':P.block+=up?13:10+P.st.dexterity;floaterPlayer('block','🛡');break;
  case 'vitalsurge':{const m=G.maxHp-G.hp+(up?3:0);P.block+=m;floaterPlayer('block','🛡+'+m);break;}
  case 'lifetap':drawCards(up?3:2);break;
  case 'rendflesh':aliveEnemies().forEach(e=>applyDebuffToEnemy(e,'bleed',up?7:5));break;
  case 'soulsiphon':break;
  case 'bloodpact':break;
  case 'shadowbolt':enemyHurt(tgt,playerAtk(up?8:6));applyDebuffToEnemy(tgt,'weak',up?2:1);break;
  case 'grasp':enemyHurt(tgt,playerAtk(up?5:4));applyDebuffToEnemy(tgt,'vulnerable',up?2:1);break;
  case 'nightmare':{const n=playerAtk(up?9:7);enemyHurt(tgt,n);if(tgt.st.weak>0){await sleep(150);enemyHurt(tgt,n);}break;}
  case 'darkness':for(let i=0;i<3;i++){const e=pick(aliveEnemies());if(!e)break;enemyHurt(e,playerAtk(up?4:3));await sleep(160);}break;
  case 'cloakshadows':P.block+=(up?7:5)+P.st.dexterity;drawCards(1);break;
  case 'hauntingmist':P.block+=(up?9:7)+P.st.dexterity;aliveEnemies().forEach(e=>applyDebuffToEnemy(e,'weak',1));break;
  case 'phantomarmor':P.block+=(up?10:8)+P.st.dexterity;P.shroudNext=up?3:2;floaterPlayer('buff','🌀 next turn');break;
  case 'shroud':P.block+=(up?5:4)+P.st.dexterity;P.shroudNext+=up?5:4;floaterPlayer('block','🛡 later');break;
  case 'shadowform':P.st.intangible+=up?3:2;floaterPlayer('buff','👻 +'+(up?3:2));break;
  case 'misfortune':{const n=up?4:3;tgt.tempStr-=n;applyDebuffToEnemy(tgt,'vulnerable',up?2:1);floaterEnemy(e=>e===tgt,'debuff','💪 -'+n);break;}
  case 'darkritual':P.energy+=2;P.discard.push(mkCard('dazed'));break;
  case 'umbral':break;
  case 'firebolt':enemyHurt(tgt,playerAtk(up?9:7));applyDebuffToEnemy(tgt,'burn',up?3:2);break;
  case 'hellfire':aliveEnemies().forEach(e=>enemyHurt(e,playerAtk(up?12:9)));break;
  case 'searing':enemyHurt(tgt,playerAtk(up?8:6));P.nextEnergy+=1;floaterPlayer('buff','⚡ next turn');break;
  case 'immolate':enemyHurt(tgt,playerAtk(up?16:12));break;
  case 'obsidian':P.block+=up?10:7+P.st.dexterity;floaterPlayer('block','🛡');break;
  case 'flameward':P.block+=(up?6:5)+P.st.dexterity;{const e=pick(aliveEnemies());if(e)applyDebuffToEnemy(e,'burn',up?3:2);}break;
  case 'demonic':P.block+=(up?5:4)+P.st.dexterity;healPlayer(up?3:2);break;
  case 'brimstone':P.block+=(up?4:3)+P.st.dexterity;P.st.strength+=up?3:2;floaterPlayer('buff','💪 +'+(up?3:2));break;
  case 'summonimp':P.hand.push(mkCard('impclaw',up));break;
  case 'infernalpact':P.energy+=2;P.hand.push(mkCard('impclaw',up));break;
  case 'hellaura':break;
  case 'soulburn':applyDebuffToEnemy(tgt,'burn',up?10:8);drawCards(1);break;
  case 'impclaw':enemyHurt(tgt,playerAtk(up?6:4));break;
 }
 renderCombat();
 await sleep(200);
}
/* ---- targeting ---- */
let selectedUid=null;
const SINGLE_TARGET=['strike','quickslash','sanguine','hemorrhage','bloodletting','shadowbolt','grasp','nightmare','misfortune','firebolt','immolate','soulburn','impclaw'];
function needsTarget(c){
 if(!c)return false;
 const d=cardDef(c);
 if(d.type==='curse'||d.type==='status')return false;
 if(d.all||d.random)return false;
 return SINGLE_TARGET.includes(c.id);
}
function selectCard(uid){
 if(busy||!C||C.over)return;
 const c=C.player.hand.find(x=>x.uid===uid);
 if(!c)return;
 if(!canPlay(c)){SFX.err();return;}
 const d=cardDef(c);
 if(d.type==='curse'||d.type==='status'){SFX.err();return;}
 if(selectedUid===uid){selectedUid=null;renderHand();return;}
 selectedUid=uid;
 if(needsTarget(c)){
  if(C._selTarget){
   selectedUid=null;
   $('#targetHint').classList.add('hidden');
   playCardWithTarget(c,C._selTarget);
   return;
  }
  $('#targetHint').classList.remove('hidden');
  renderHand();
  return;
 }
 playCardWithTarget(c,null);
}
function playCardWithTarget(c,tgt){
 C._selTarget=tgt;
 playCard(c);
}
function clickEnemy(e){
 if(busy||!C||C.over)return;
 if(selectedUid===null)return;
 const c=C.player.hand.find(x=>x.uid===selectedUid);
 if(!c||!needsTarget(c))return;
 selectedUid=null;
 $('#targetHint').classList.add('hidden');
 playCardWithTarget(c,e);
}
/* ---- combat end ---- */
function combatEnd(won){
 if(C.over)return;
 C.over=true;busy=true;
 if(won){
  G.stats.kills++;
  SFX.win();
  banner('Victory!');
  setTimeout(()=>showRewards(),1000);
 }else{
  SFX.lose();
  banner('You Died');
  setTimeout(()=>showEnd(false),1600);
 }
}
/* ---- rendering ---- */
function renderCombat(){
 if(!C)return;
 const P=C.player;
 // hud
 const act=ACTS[G.act];
 $('#combatHud').innerHTML=`<span class="ch-label">Act ${G.act+1}: ${act.name}</span>
  <span class="hud-chip">🏰 Floor ${G.floor}</span>
  <span class="hud-chip">💰 <b>${G.gold}</b></span>
  <span class="hud-chip">❤️ <b>${G.hp}/${G.maxHp}</b></span>
  <span class="hud-chip">🃏 <b>${G.deck.length}</b></span>
  <span class="spacer"></span>
  ${P.powers.length?`<span class="hud-chip" title="Active powers">⚡ Powers: ${P.powers.length}</span>`:''}
  <button class="pile-btn" onclick="openPiles()">Piles (${P.draw.length}/${P.discard.length}/${P.exhaust.length})</button>`;
 // enemies
 const row=$('#enemyRow');
 row.innerHTML='';
 C.enemies.forEach(e=>{
  const div=el('div','enemy'+(e.boss?' boss':'')+(e.hp<=0?' dead':''));
  const it=e.pat[e.turn%e.pat.length];
  div.innerHTML=`<div class="e-intent ${e.boss?'boss-int':''}">${intentIcon(it)}</div>
   <div class="e-sprite">${e.art}</div>
   <div class="e-name">${e.name}</div>
   <div class="e-hpbar"><div class="e-hpfill" style="width:${Math.max(0,e.hp/e.maxHp*100)}%"></div></div>
   <div class="e-hpnum">${Math.max(0,e.hp)}/${e.maxHp}</div>
   ${e.block>0?`<div class="e-block">🛡${e.block}</div>`:''}
   <div class="st-row">${statusChips(e.st)}</div>`;
  div.onclick=()=>clickEnemy(e);
  const selCard=C.player.hand.find(x=>x.uid===selectedUid);
  if(selectedUid!==null&&selCard&&needsTarget(selCard)&&e.hp>0)div.classList.add('targetable');
  e.el=div;
  row.appendChild(div);
 });
 // player panel
 $('#playerPanel').innerHTML=`
  ${P.block>0?`<div class="pp-block">🛡 ${P.block}</div>`:''}
  <div class="pp-top"><div class="pp-avatar">${CLASSES[G.cls].art}</div>
   <div><div class="pp-name">${CLASSES[G.cls].name}</div><div class="st-row">${statusChips(P.st)}</div></div></div>
  <div class="pp-hp"><div class="hpbar"><div class="hpfill" style="width:${clamp(G.hp/G.maxHp*100,0,100)}%"></div></div><span class="pp-hpnum">${G.hp}/${G.maxHp}</span></div>
  <div class="pp-potions">${G.potions.map((p,i)=>`<div class="pot ${busy?'lock':''}" title="${POTIONS[p].name}: ${POTIONS[p].desc}" onclick="usePotion(${i})">${POTIONS[p].art}</div>`).join('')}${G.potions.length<3?`<div class="pot" style="opacity:.2">·</div>`:''}</div>`;
 $('#enNum').textContent=P.energy;
 $('#powersRow').innerHTML=P.powers.map(p=>`<div class="pow-ic" title="${cardDef(p).name}: ${cardDef(p).desc}">${cardDef(p).art}</div>`).join('');
 renderHand();
 $('#endTurnBtn').disabled=busy||C.over;
}
function intentIcon(it){
 switch(it.a){
  case 'atk':return `⚔️ Attack ${it.n}`;
  case 'multi':return `⚔️ ${it.hits}×${it.n} — Flurry`;
  case 'atkdef':return `⚔️ ${it.n} + 🛡 ${it.block}`;
  case 'def':return `🛡 ${it.n}${it.str?` 💪+${it.str}`:''}${it.heal?` 💗+${it.heal}`:''}`;
  case 'buff':return `💪 +${it.str} Strength`;
  case 'debuff':{let s='';if(it.weak)s+=`🐌 Weak ${it.weak}`;if(it.vuln)s+=`🎯 Vuln ${it.vuln}`;if(it.bleed)s+=`🩸 Bleed ${it.bleed}`;if(it.poison)s+=`☠️ Poison ${it.poison}`;return s||'❓';}
  case 'drain':return `🩸 ${it.n} (heals ${it.heal})`;
  case 'allybuff':return `✨ Buff ally`;
  case 'allystr':return `💪 All +${it.str} Str`;
  case 'selfint':return `👻 Intangible +${it.n}`;
  case 'summon':return `📯 Summon`;
  default:return '❓';
 }
}
function renderHand(){
 if(!C)return;
 const P=C.player;
 const hand=$('#hand');
 hand.innerHTML='';
 const n=P.hand.length;
 P.hand.forEach((c,i)=>{
  const d=cardDef(c);
  const slot=el('div','hand-slot');
  const off=i-(n-1)/2;
  slot.style.setProperty('--r',(off*3)+'deg');
  slot.style.setProperty('--y',(Math.abs(off)*9)+'px');
  const card=cardEl(c);
  slot.appendChild(card);
  const playable=canPlay(c)&&d.type!=='curse'&&d.type!=='status';
  if(!playable)slot.classList.add('dim');
  if(selectedUid===c.uid)slot.classList.add('selected');
  slot.onclick=()=>selectCard(c.uid);
  hand.appendChild(slot);
 });
}
/* ---- fx ---- */
function banner(txt){
 const b=$('#turnBanner');
 b.textContent=txt;
 b.classList.remove('show');void b.offsetWidth;b.classList.add('show');
}
function floaterPlayer(cls,txt){
 const p=$('#playerPanel');
 const f=el('div','floater '+cls,txt);
 f.style.left=(30+rand(0,80))+'px';f.style.top='10px';
 p.appendChild(f);setTimeout(()=>f.remove(),1150);
}
function floaterEnemy(pred,cls,txt){
 C&&C.enemies.forEach(e=>{if(e.hp>0&&pred(e)&&e.el){
  const f=el('div','floater '+cls,txt);
  f.style.left='50%';f.style.top='12px';f.style.transform='translateX(-50%)';
  e.el.appendChild(f);setTimeout(()=>f.remove(),1150);
 }});
}
function shakePlayer(){
 const p=$('#playerPanel');
 p.classList.remove('shake');void p.offsetWidth;p.classList.add('shake');
}
/* ============================== POTIONS ============================== */
function usePotion(i){
 if(busy||!C||C.over)return;
 const id=G.potions[i];if(!id)return;
 const P=C.player;
 if(id==='smoke'){
  if(C.boss){SFX.err();return;}
  G.potions.splice(i,1);G.escaped=true;
  (async()=>{busy=true;banner('Escaped!');await sleep(900);C.over=true;C=null;show('scr-map');renderMap();})();
  return;
 }
 G.potions.splice(i,1);
 busy=true;
 (async()=>{
  await sleep(150);
  switch(id){
   case 'blood':healPlayer(15);break;
   case 'firebomb':aliveEnemies().forEach(e=>enemyHurt(e,10));break;
   case 'dex':P.st.dexterity+=3;P.dexTurns=2;floaterPlayer('buff','🌀 +3');break;
   case 'energy':P.energy+=2;$('#energyOrb').classList.add('flash');setTimeout(()=>$('#energyOrb').classList.remove('flash'),450);break;
   case 'cursed':aliveEnemies().forEach(e=>applyDebuffToEnemy(e,'vulnerable',3));break;
   case 'bleeding':{const e=pick(aliveEnemies());if(e)applyDebuffToEnemy(e,'bleed',6);break;}
   case 'impbottle':P.hand.push(mkCard('impclaw',true));break;
  }
  busy=false;
  renderCombat();
  if(G.hp<=0)combatEnd(false);
  else if(aliveEnemies().length===0)combatEnd(true);
 })();
}
/* ============================== REWARDS ============================== */
function cardPool(){
 return Object.keys(CARDS).filter(k=>{const c=CARDS[k];
  return (c.cls===G.cls||c.colorless)&&c.rarity&&c.rarity!=='basic'&&!c.token;});
}
function rollRewardCards(elite,boss){
 const pool=cardPool();
 const byR=r=>pool.filter(k=>CARDS[k].rarity===r);
 const out=[];
 if(boss){ // always one rare
  out.push(mkCard(pick(byR('rare'))));
  const rest=['common','uncommon'];
  while(out.length<3){
   const r=pick(rest);
   const c=byR(r);if(c.length)out.push(mkCard(pick(c)));
  }
 }else{
  while(out.length<3){
   const roll=Math.random();
   let r=roll<0.6?'common':(roll<(elite?0.8:0.9)?'uncommon':'rare');
   let c=byR(r);
   if(!c.length)c=byR('common');
   out.push(mkCard(pick(c)));
  }
 }
 return out;
}
let rewardState=null;
function showRewards(){
 const boss=C.boss,elite=C.elite,thief=C.thief;
 C=null;
 let gold=0;
 if(boss)gold=50;
 else gold=rand(10,20)+(elite?15:0);
 if(hasRelic('greedyimp'))gold+=10;
 if(thief)gold+=G.pendingThiefGold||0;
 G.pendingThiefGold=0;
 gainGold(gold);
 SFX.gold();
 const pots=boss?2:(Math.random()<(elite?0.7:0.4)?1:0);
 const potIds=[];
 for(let i=0;i<pots;i++){if(addPotion(randPotion()))potIds.push(potIds.length);}
 const cards=rollRewardCards(elite,boss);
 const relics=[];
 if(elite){
  const r=rollRelic();relics.push(r);
 }
 rewardState={cards,gold,potIds,relics,boss,elite,stage:0};
 renderReward();
}
function rollRelic(){
 let pool=Object.keys(RELICS).filter(k=>RELICS[k].cls===undefined||(RELICS[k].cls===G.cls));
 pool=pool.filter(k=>!RELICS[k].unique||!hasRelic(k));
 return pick(pool);
}
function renderReward(){
 const s=rewardState;
 const p=$('#rewardPanel');
 let h=`<h2>${s.boss?'Boss Defeated!':s.elite?'Elite Defeated!':'Victory!'}</h2>
  <div class="sub">${ACTS[G.act].name} — Floor ${G.floor}</div>
  <div class="reward-gold">💰 +${s.gold} Gold</div>`;
 if(s.potIds.length)h+=`<div class="reward-pot">🧪 Potion${s.potIds.length>1?'s':''} obtained${G.potions.length>=3?' (slots!)':''}</div>`;
 if(s.relics.length)h+=s.relics.map(r=>`<div class="relic-box"><div class="rl-ic">${RELICS[r].art}</div><div><div class="rl-name">${RELICS[r].name}</div><div class="rl-desc">${RELICS[r].desc}</div></div></div>`).join('');
 if(s.boss){
  h+=`<h3>Boss Relic — choose one</h3><div class="reward-row">`;
  const br=shuffle(Object.keys(BOSS_RELICS)).slice(0,3);
  br.forEach((b,i)=>{h+=`<div class="relic-box" style="cursor:pointer;max-width:250px;flex:1" onclick="pickBossRelic('${b}')"><div class="rl-ic">${BOSS_RELICS[b].art}</div><div><div class="rl-name">${BOSS_RELICS[b].name}</div><div class="rl-desc">${BOSS_RELICS[b].desc}</div></div></div>`;});
  h+=`</div>`;
 }else{
  h+=`<h3>Choose a card (or skip)</h3><div class="card-grid">`;
  s.cards.forEach((c,i)=>{h+=cardEl(c).outerHTML;const d=cardDef(c);
   h=h.replace(`<div class="c-desc">${d.desc}</div>`,`<div class="c-desc">${d.desc}</div>`);});
  h+=`</div><div style="display:flex;gap:10px;justify-content:center;margin-top:14px">`;
  s.cards.forEach((c,i)=>{h+=`<button class="btn small" onclick="pickCard(${i})">Take ${cardDef(c).name}</button>`;});
  h+=`<button class="btn small danger" onclick="skipCard()">Skip</button></div>`;
 }
 h+=`<div class="modal-close"><button class="btn" id="rwNext" style="visibility:${s.boss?'hidden':'visible'}" onclick="rewardDone()">Continue</button></div>`;
 p.innerHTML=h;
 // make cards clickable
 p.querySelectorAll('.card-grid .card').forEach((el,i)=>{
  el.style.cursor='pointer';el.onclick=()=>pickCard(i);
 });
 show('scr-reward');
}
function pickCard(i){
 const c=rewardState.cards[i];
 addCardToDeck(c);
 SFX.card();
 rewardState.stage=1;
 rewardDone();
}
function skipCard(){rewardDone();}
function pickBossRelic(b){
 G.relics.push(b);
 applyBossRelic(b);
 SFX.gold();
 rewardState.stage=1;
 rewardDone();
}
function applyBossRelic(b){
 if(b==='amulet'){G.maxHp-=15;G.hp=clamp(G.hp,0,G.maxHp);}
 if(b==='ecto'){G.gold=0;}
}
function rewardDone(){
 if(hasRelic('ashenarmor')){heal(6);}
 const boss=C_bossFlag();
 // advance
 if(G.curNode&&G.curNode.type==='boss'){
  G.hp=G.maxHp; // full heal after boss
  if(G.act===2){showEnd(true);return;}
  G.act++;
  G.floor=1;
  G.map=genMap(G.act);
  G.curNode=null;
  show('scr-map');renderMap();
 }else{
  show('scr-map');renderMap();
 }
}
function C_bossFlag(){return rewardState&&rewardState.boss;}
/* ============================== MAP ============================== */
function reachableNodes(){
 if(!G.map)return [];
 if(!G.curNode)return G.map.start;
 return G.curNode.children||[];
}
function renderMap(){
 const act=ACTS[G.act];
 $('#mapHud').innerHTML=`<span class="act-label">Act ${G.act+1}: ${act.name}</span>
  <span class="act-sub">Floors ${act.floors}</span>
  <span class="spacer"></span>
  <span class="hud-chip">❤️ <b>${G.hp}/${G.maxHp}</b> <span class="hpbar"><span class="hpfill" style="width:${clamp(G.hp/G.maxHp*100,0,100)}%;display:block"></span></span></span>
  <span class="hud-chip">💰 <b>${G.gold}</b></span>
  <span class="hud-chip">🃏 <b>${G.deck.length}</b></span>
  <span class="hud-chip">🧪 <b>${G.potions.length}/3</b></span>
  <span class="hud-chip">${G.relics.map(r=>`<span class="relic-ic" title="${RELICS[r]?RELICS[r].name+' — '+RELICS[r].desc:BOSS_RELICS[r].name+' — '+BOSS_RELICS[r].desc}">${RELICS[r]?RELICS[r].art:BOSS_RELICS[r].art}</span>`).join(' ')}</span>
  <button class="btn small" onclick="openDeckView()">Deck</button>`;
 const svg=$('#mapSvg');
 svg.innerHTML='';
 const NS='http://www.w3.org/2000/svg';
 const rows=G.map.rows;
 const topY=940,botY=50,rowH=(topY-botY)/(rows.length-1);
 const pos={};
 rows.forEach((r,i)=>{
  const y=botY-i*rowH;
  r.forEach((n,j)=>{
   const xs=r.length===1?[240]:[150,330];
   const x=xs[j];
   pos[n]=x?{x,y}:0;
  });
 });
 // edges
 rows.forEach((r,i)=>{
  if(i===rows.length-1)return;
  r.forEach(a=>{
   rows[i+1].forEach(b=>{
    const l=document.createElementNS(NS,'line');
    l.setAttribute('x1',pos[a].x);l.setAttribute('y1',pos[a].y);
    l.setAttribute('x2',pos[b].x);l.setAttribute('y2',pos[b].y);
    const active=G.curNode===a&&b.children.length;
    l.setAttribute('stroke',active?'#c9a227':'#3a2f4a');
    l.setAttribute('stroke-width',active?2.5:1.5);
    l.setAttribute('stroke-dasharray','5 5');
    if(active)l.style.animation='dash 1s linear infinite';
    svg.appendChild(l);
   });
  });
 });
 // start marker
 const s=rows[0];
 const st=document.createElementNS(NS,'circle');
 st.setAttribute('cx',pos[s[0]].x);st.setAttribute('cy',pos[s[0]].y+26);st.setAttribute('r',7);
 st.setAttribute('fill','#c9a227');svg.appendChild(st);
 // nodes
 rows.forEach((r,i)=>{
  r.forEach(n=>{
   const g=document.createElementNS(NS,'g');
   g.setAttribute('class','mnode');
   const c=document.createElementNS(NS,'circle');
   c.setAttribute('cx',pos[n].x);c.setAttribute('cy',pos[n].y);c.setAttribute('r',20);
   const colors={enemy:'#7a4a3a',elite:'#8a3a4a',rest:'#3a6a4a',shop:'#8a7a2e',event:'#5a4a8a',boss:'#c9a227'};
   c.setAttribute('fill','#120d1c');
   c.setAttribute('stroke',colors[n.type]||'#666');
   c.setAttribute('stroke-width',n.type==='boss'?3.5:2);
   if(n.type==='boss')c.setAttribute('r',26);
   const isVisited=G.reachedNodes.includes(n);
   if(isVisited){c.setAttribute('fill','#241a30');c.setAttribute('opacity','0.55');}
   const reachable=reachableNodes().includes(n);
   if(reachable)g.setAttribute('class','mnode reach');
   const t=document.createElementNS(NS,'text');
   t.setAttribute('x',pos[n].x);t.setAttribute('y',pos[n].y+6);
   t.setAttribute('text-anchor','middle');t.setAttribute('font-size','18');
   t.textContent=NODE_ICON[n.type];
   const fl=document.createElementNS(NS,'text');
   fl.setAttribute('x',pos[n].x);fl.setAttribute('y',pos[n].y-28);
   fl.setAttribute('text-anchor','middle');fl.setAttribute('font-size','10');
   fl.setAttribute('fill','#6a6080');fl.textContent='F'+n.floor;
   g.appendChild(c);g.appendChild(t);g.appendChild(fl);
   if(reachable){
    g.onclick=()=>enterNode(n);
   }
   svg.appendChild(g);
  });
 });
 // scroll to player
 const sc=$('#mapScroll');
 const my=G.curNode?pos[G.curNode].y:pos[rows[0][0]].y;
 sc.scrollTop=sc.scrollHeight; // start at bottom
 setTimeout(()=>{sc.scrollTo({top:my-200,behavior:'smooth'});},60);
 show('scr-map');
}
function enterNode(n){
 if(!reachableNodes().includes(n))return;
 SFX.click();
 G.curNode=n;
 G.floor=n.floor;
 G.reachedNodes.push(n);
 switch(n.type){
  case 'enemy':case 'elite':startCombat({enemies:n.comp,boss:false,elite:n.type==='elite'});break;
  case 'boss':startCombat({enemies:n.comp,boss:true});break;
  case 'rest':showRest();break;
  case 'shop':showShop();break;
  case 'event':showEvent(pick(ACTS[G.act].events));break;
 }
}
/* ============================== REST ============================== */
function showRest(){
 const p=$('#restPanel');
 p.innerHTML=`<h2>Rest Site</h2><div class="sub">A moment of stillness in the dark.</div>
  <div class="choice-col">
   <div class="rest-opt" onclick="restAction('rest')"><div class="ro-ic">🔥</div><div><div class="ro-name">Rest</div><div class="ro-desc">Heal 25% of max HP (${Math.floor(G.maxHp*0.25)} HP).</div></div></div>
   <div class="rest-opt" onclick="restAction('forge')"><div class="ro-ic">🔨</div><div><div class="ro-name">Forge</div><div class="ro-desc">Upgrade one card permanently.</div></div></div>
   <div class="rest-opt" onclick="restAction('meditate')"><div class="ro-ic">🧘</div><div><div class="ro-name">Meditate</div><div class="ro-desc">Remove one card from your deck.</div></div></div>
  </div>`;
 show('scr-rest');
}
function restAction(a){
 SFX.click();
 if(a==='rest'){heal(Math.floor(G.maxHp*0.25));SFX.heal();}
 if(a==='forge'){openPick('forge');return;}
 if(a==='meditate'){openPick('meditate');return;}
 show('scr-map');renderMap();
}
/* ============================== SHOP ============================== */
let shopState=null;
function showShop(){
 const cards=rollShopCards();
 const colorless=mkCard(pick(['flash','ironwill','quickslash','potentbrew']));
 const relics=[rollRelic(),rollRelic()];
 const pots=[randPotion(),randPotion(),randPotion()];
 shopState={cards,colorless,relics,pots,sold:{},removed:false};
 renderShop();
}
function rollShopCards(){
 const pool=cardPool();
 const out=[];
 while(out.length<3){out.push(mkCard(pick(pool)));}
 return out;
}
function renderShop(){
 const s=shopState;
 const p=$('#shopPanel');
 let h=`<h2>Shop</h2><div class="sub">The merchant's eyes gleam. 💰 ${G.gold} Gold</div>
  <div class="shop-grid">`;
 s.cards.forEach((c,i)=>{
  const d=cardDef(c);
  const price=RARITY_PRICE[d.rarity]||50;
  h+=`<div class="shop-item ${s.sold['c'+i]?'sold':''}">${cardEl(c,true).outerHTML}
   <div class="price ${G.gold<price?'cant':''}">${price}g</div>
   <button class="btn small" ${s.sold['c'+i]||G.gold<price?'disabled':''} onclick="buyCard(${i})">${s.sold['c'+i]?'Sold':'Buy'}</button></div>`;
 });
 h+=`<div class="shop-item ${s.sold.colorless?'sold':''}">${cardEl(s.colorless,true).outerHTML}
   <div class="price ${G.gold<60?'cant':''}">60g</div>
   <button class="btn small" ${s.sold.colorless||G.gold<60?'disabled':''} onclick="buyColorless()">${s.sold.colorless?'Sold':'Buy'}</button></div>`;
 s.relics.forEach((r,i)=>{
  h+=`<div class="shop-item ${s.sold['r'+i]?'sold':''}"><div style="font-size:44px">${RELICS[r].art}</div>
   <div style="font-size:12px;color:#b8b0a0;text-align:center;max-width:130px">${RELICS[r].name}<br><span style="color:#8a8098;font-size:10px">${RELICS[r].desc}</span></div>
   <div class="price ${G.gold<150?'cant':''}">150g</div>
   <button class="btn small" ${s.sold['r'+i]||G.gold<150?'disabled':''} onclick="buyRelic(${i})">${s.sold['r'+i]?'Sold':'Buy'}</button></div>`;
 });
 s.pots.forEach((pt,i)=>{
  h+=`<div class="shop-item ${s.sold['p'+i]?'sold':''}"><div style="font-size:40px">${POTIONS[pt].art}</div>
   <div style="font-size:12px;color:#b8b0a0;text-align:center;max-width:130px">${POTIONS[pt].name}<br><span style="color:#8a8098;font-size:10px">${POTIONS[pt].desc}</span></div>
   <div class="price ${G.gold<50?'cant':''}">50g</div>
   <button class="btn small" ${s.sold['p'+i]||G.gold<50?'disabled':''} onclick="buyPotion(${i})">${s.sold['p'+i]?'Sold':'Buy'}</button></div>`;
 });
 h+=`</div><div style="text-align:center;margin-top:10px">
  <button class="btn small" ${s.removed||G.gold<75?'disabled':''} onclick="buyRemove()">${s.removed?'Removal Used':'Remove a Card (75g)'}</button>
  <button class="btn" onclick="leaveShop()">Leave Shop</button></div>`;
 p.innerHTML=h;
 show('scr-shop');
}
function buyCard(i){const c=shopState.cards[i];const d=cardDef(c);const price=RARITY_PRICE[d.rarity]||50;
 if(G.gold<price||shopState.sold['c'+i])return;G.gold-=price;addCardToDeck(c);shopState.sold['c'+i]=true;SFX.gold();renderShop();}
function buyColorless(){if(G.gold<60||shopState.sold.colorless)return;G.gold-=60;addCardToDeck(shopState.colorless);shopState.sold.colorless=true;SFX.gold();renderShop();}
function buyRelic(i){if(G.gold<150||shopState.sold['r'+i])return;G.gold-=150;giveRelic(shopState.relics[i]);shopState.sold['r'+i]=true;SFX.gold();renderShop();}
function buyPotion(i){if(G.gold<50||shopState.sold['p'+i])return;G.gold-=50;
 if(addPotion(shopState.pots[i])){shopState.sold['p'+i]=true;SFX.gold();}
 else SFX.err();
 renderShop();}
function buyRemove(){if(G.gold<75||shopState.removed)return;G.gold-=75;shopState.removed=true;openPick('remove');}
function leaveShop(){show('scr-map');renderMap();}
/* ============================== EVENTS ============================== */
let eventState=null;
function showEvent(idx){
 const ev=EVENTS[idx];
 eventState=ev;
 const p=$('#eventPanel');
 let h=`<h2>${ev.name}</h2><div class="event-art">${ev.art}</div><div class="event-text">${ev.text}</div><div class="choice-col">`;
 ev.choices.forEach((c,i)=>{h+=`<button class="btn" onclick="eventChoice(${i})">${c.label}</button>`;});
 h+=`</div>`;
 p.innerHTML=h;
 show('scr-event');
}
function eventChoice(i){
 SFX.click();
 const res=eventState.choices[i].run();
 if(res==='combat'){show('scr-combat');return;}
 if(res==='tradeCardForGold'){openPick('trade');return;}
 const p=$('#eventPanel');
 p.innerHTML+=`<div class="event-result">${res}</div><div style="text-align:center"><button class="btn" onclick="show('scr-map');renderMap()">Continue</button></div>`;
}
/* ============================== PICK OVERLAY ============================== */
let pickMode=null;
function openPick(mode){
 pickMode=mode;
 const p=$('#overlayPanel');
 let title='';
 if(mode==='forge')title='Forge — choose a card to upgrade';
 if(mode==='meditate')title='Meditate — choose a card to remove';
 if(mode==='vial')title='Vial of Shadows — remove a card from your deck';
 if(mode==='remove')title='Remove a card from your deck (75g)';
 if(mode==='trade')title='Peddler — trade a card for 100 Gold';
 if(mode==='deck')title='Your Deck ('+G.deck.length+' cards)';
 let h=`<h2>${title}</h2><div class="card-grid">`;
 G.deck.forEach(c=>{
  const d=cardDef(c);
  const dis=(mode==='forge'&&c.upgraded);
  h+=`<div class="pickable ${dis?'disabled':''}" data-uid="${c.uid}">${cardEl(c,true).outerHTML}</div>`;
 });
 h+=`</div><div class="modal-close"><button class="btn" onclick="closeOverlay()">Close</button></div>`;
 p.innerHTML=h;
 p.querySelectorAll('.pickable').forEach(div=>{
  const c=G.deck.find(x=>x.uid==div.dataset.uid);
  const d=cardDef(c);
  if(mode==='forge'&&c.upgraded){div.classList.add('disabled');return;}
  div.onclick=()=>{
   if(mode==='forge'){c.upgraded=true;G.stats.upgrades++;SFX.gold();}
   else{G.deck=G.deck.filter(x=>x.uid!==c.uid);G.stats.removed++;SFX.click();}
   closeOverlay();
   if(mode==='trade'){gainGold(100);}
   else if(mode==='remove'){renderShop();}
   else{show('scr-map');renderMap();}
  };
 });
 openOverlay(h);
}
function openDeckView(){openPick('deck');}
function openPiles(){
 const P=C?C.player:null;
 if(!P){openPick('deck');return;}
 const sec=(t,arr)=>`<div class="pile-sec"><h3>${t} (${arr.length})</h3><div class="card-grid">${arr.map(c=>cardEl(c,true).outerHTML).join('')||'<i style="color:#6a6080">Empty</i>'}</div></div>`;
 openOverlay(`<h2>Piles</h2>${sec('Draw',P.draw)}${sec('Discard',P.discard)}${sec('Exhaust',P.exhaust)}<div class="modal-close"><button class="btn" onclick="closeOverlay()">Close</button></div>`);
}
/* ============================== END SCREENS ============================== */
function showEnd(won){
 C=null;
 const p=$('#endPanel');
 const s=G.stats;
 p.innerHTML=won?
  `<div class="end-art">👑</div><h2 style="color:#c9a227">The Abyss Falls Silent</h2>
   <div class="sub">The Archdemon crumbles to ash. The dying world draws breath. Your legend is etched in shadow.</div>
   <div class="end-stats">${endStats()}</div>
   <button class="btn" onclick="location.reload()">Rise Again</button>`:
  `<div class="end-art">💀</div><h2 style="color:#e05545">The Run Ends</h2>
   <div class="sub">Darkness claims you on Floor ${G.floor} of Act ${G.act+1}: ${ACTS[G.act].name}. All progress is lost.</div>
   <div class="end-stats">${endStats()}</div>
   <button class="btn danger" onclick="location.reload()">Try Again</button>`;
 show('scr-end');
 if(won)SFX.win();else SFX.lose();
}
function endStats(){
 const s=G.stats;
 return `<div class="end-stat"><div class="es-num">${s.kills}</div><div class="es-lab">Kills</div></div>
  <div class="end-stat"><div class="es-num">${s.dealt}</div><div class="es-lab">Damage Dealt</div></div>
  <div class="end-stat"><div class="es-num">${s.taken}</div><div class="es-lab">Damage Taken</div></div>
  <div class="end-stat"><div class="es-num">${s.goldEarned}</div><div class="es-lab">Gold Earned</div></div>
  <div class="end-stat"><div class="es-num">${s.upgrades}</div><div class="es-lab">Upgrades</div></div>`;
}
/* ============================== TITLE / INIT ============================== */
function initTitle(){
 const row=$('#classRow');
 row.innerHTML='';
 Object.keys(CLASSES).forEach(k=>{
  const c=CLASSES[k];
  const div=el('div','class-card');
  div.innerHTML=`<div class="cc-art">${c.art}</div><div class="cc-name">${c.name}</div><div class="cc-desc">${c.desc}</div><div class="cc-tag">Choose</div>`;
  div.onclick=()=>{
   SFX.card();
   G.cls=k;
   G.deck=START_DECK[k].map(id=>mkCard(id));
   G.hp=70;G.maxHp=70;G.gold=99;
   G.act=0;G.floor=1;
   G.map=genMap(0);
   G.curNode=null;
   G.reachedNodes=[];
   G.relics=[];G.potions=[];
   G.stats={kills:0,goldEarned:0,dealt:0,taken:0,upgrades:0,removed:0};
   show('scr-map');
   renderMap();
  };
  row.appendChild(div);
 });
 show('scr-title');
}
document.addEventListener('keydown',e=>{
 if(e.key==='Escape'){
  if(selectedUid!==null){selectedUid=null;$('#targetHint').classList.add('hidden');if(C)renderHand();}
  else if(!$('#overlay').classList.contains('hidden'))closeOverlay();
 }
 if(e.key==='Enter'&&C&&!busy&&!C.over)endPlayerTurn();
});
$('#endTurnBtn').onclick=endPlayerTurn;
initTitle();
</script>
</body>
</html>