← Shadows results

Shadows

ox alpha · 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 70,875 bytes · SHA-256 d2efea88c30c
<!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>
:root{
  --bg:#0b0a12; --panel:#16142280; --gold:#c9a227; --blood:#a02030; --purple:#6a4a9c;
  --text:#d8d2e8; --dim:#7a7391; --green:#4a9c5f; --red:#c04050; --blue:#4a7ac0;
}
*{box-sizing:border-box; margin:0; padding:0; user-select:none;}
body{background:radial-gradient(ellipse at 50% -10%, #1c1530 0%, #0b0a12 60%); color:var(--text);
  font-family:'Segoe UI', Georgia, serif; min-height:100vh; overflow-x:hidden;}
#app{max-width:1200px; margin:0 auto; padding:12px; min-height:100vh; display:flex; flex-direction:column;}
h1{font-variant:small-caps; letter-spacing:3px; color:var(--gold); text-shadow:0 0 18px #c9a22766;}
h2{font-variant:small-caps; letter-spacing:2px; color:var(--gold); margin-bottom:10px;}
button{background:linear-gradient(#2a2440,#1a1628); color:var(--text); border:1px solid #4a3f6a;
  border-radius:6px; padding:8px 18px; font-family:inherit; font-size:15px; cursor:pointer; transition:all .15s;}
button:hover:not(:disabled){border-color:var(--gold); color:#fff; box-shadow:0 0 10px #c9a22744; transform:translateY(-1px);}
button:disabled{opacity:.4; cursor:default;}
.panel{background:var(--panel); border:1px solid #3a3355; border-radius:10px; padding:16px; backdrop-filter:blur(4px);}
.screen{flex:1; display:flex; flex-direction:column;}
.hidden{display:none !important;}

/* class select */
.class-row{display:flex; gap:20px; justify-content:center; flex-wrap:wrap; margin-top:30px;}
.class-card{width:280px; padding:22px; text-align:center; cursor:pointer; transition:all .2s; border-radius:12px;
  background:linear-gradient(160deg,#221c38,#120f1e); border:1px solid #3a3355;}
.class-card:hover{transform:translateY(-6px) scale(1.02); border-color:var(--gold); box-shadow:0 8px 30px #000a, 0 0 20px #6a4a9c44;}
.class-card h3{font-variant:small-caps; font-size:22px; letter-spacing:2px; margin:10px 0;}
.class-icon{font-size:52px;}
.class-card p{color:var(--dim); font-size:13px; margin-top:8px; line-height:1.5;}

/* top bar */
#topbar{display:flex; justify-content:space-between; align-items:center; padding:8px 16px; margin-bottom:10px;
  background:linear-gradient(#1c1830,#141020); border:1px solid #3a3355; border-radius:8px; font-size:15px;}
#topbar .stat{margin-right:18px; white-space:nowrap;}
.hpbar{display:inline-block; width:130px; height:14px; background:#2a0d12; border:1px solid #000; border-radius:7px; overflow:hidden; vertical-align:middle; margin:0 6px;}
.hpfill{display:block;height:100%; background:linear-gradient(#c04050,#802030); transition:width .3s;}
.gold{color:var(--gold);}

/* map */
#map{display:flex; flex-direction:column-reverse; gap:6px; padding:20px 0; overflow-y:auto; flex:1;}
.mapfloor{display:flex; justify-content:center; gap:60px; position:relative;}
.mapfloor::before{content:''; position:absolute; left:15%; right:15%; top:50%; height:1px; background:#2a2440;}
.mnode{width:120px; text-align:center; position:relative; z-index:1;}
.mnode .dot{width:52px; height:52px; margin:0 auto; border-radius:50%; display:flex; align-items:center; justify-content:center;
  font-size:24px; background:#1a1628; border:2px solid #3a3355; transition:all .2s;}
.mnode.available .dot{border-color:var(--gold); box-shadow:0 0 14px #c9a22766; cursor:pointer; animation:pulse 1.6s infinite;}
.mnode.available .dot:hover{transform:scale(1.15);}
.mnode.done .dot{opacity:.35;}
.mnode .lbl{font-size:11px; color:var(--dim); margin-top:3px;}
.mnode.current .dot{border-color:#fff;}
@keyframes pulse{0%,100%{box-shadow:0 0 8px #c9a22744}50%{box-shadow:0 0 20px #c9a227aa}}

/* combat */
#combat{flex:1; display:flex; flex-direction:column;}
#enemies{display:flex; justify-content:center; gap:40px; padding:20px; min-height:190px; flex-wrap:wrap;}
.enemy{width:170px; text-align:center; cursor:pointer; position:relative; transition:transform .15s;}
.enemy.targetable:hover{transform:scale(1.06);}
.enemy .ebody{font-size:56px; filter:drop-shadow(0 4px 8px #000); transition:all .2s;}
.enemy.dead{opacity:0; transform:translateY(20px) scale(.5); pointer-events:none; transition:all .5s;}
.enemy .ehp{width:100%; height:12px; background:#2a0d12; border:1px solid #000; border-radius:6px; overflow:hidden; margin-top:4px;}
.enemy .ehpfill{height:100%; background:linear-gradient(#c04050,#802030); transition:width .3s;}
.intent{font-size:13px; margin-top:4px; color:#e8c0c0; min-height:18px;}
.eblock{color:var(--blue); font-size:13px;}
.statusrow{min-height:20px; font-size:12px; margin-top:2px;}
.st{display:inline-block; padding:1px 6px; margin:1px; border-radius:8px; font-size:11px; background:#2a2440; border:1px solid #4a3f6a;}
.st.buff{color:#8fd0a0; border-color:#3a6a4a;}
.st.debuff{color:#e08090; border-color:#6a3a4a;}
.st.str{color:#e0a050;}
#playerzone{display:flex; justify-content:space-between; align-items:flex-end; padding:6px 10px;}
#hand{display:flex; justify-content:center; gap:-20px; min-height:190px; padding:8px 0; flex-wrap:wrap;}
.card{width:132px; height:180px; border-radius:10px; padding:8px; cursor:pointer; position:relative;
  background:linear-gradient(160deg,#2c2545,#171226); border:2px solid #4a3f6a; transition:transform .15s, box-shadow .15s;
  display:flex; flex-direction:column; flex-shrink:0;}
.card:hover{transform:translateY(-16px) scale(1.05); z-index:5; box-shadow:0 10px 24px #000c;}
.card.unplayable{filter:grayscale(.8) brightness(.6); cursor:not-allowed;}
.card.selected{outline:3px solid var(--gold); transform:translateY(-20px);}
.card .cname{font-size:13px; font-weight:bold; text-align:center; color:var(--gold); min-height:32px;}
.card .cost{position:absolute; top:-10px; left:-10px; width:30px; height:30px; border-radius:50%; background:radial-gradient(#5a8fd0,#2a4a80);
  border:2px solid #0a1a30; display:flex; align-items:center; justify-content:center; font-weight:bold; color:#fff;}
.card .ctype{text-align:center; font-size:10px; color:var(--dim); text-transform:uppercase; letter-spacing:1px;}
.card .ctext{font-size:11.5px; text-align:center; margin-top:6px; line-height:1.35; flex:1;}
.card.atk{border-color:#7a3040;} .card.skl{border-color:#3a5a7a;} .card.pwr{border-color:#6a4a9c;} .card.crs{border-color:#555; background:linear-gradient(160deg,#2a2a2a,#111);}
.card .up{color:#7fe090;}
#combat-controls{display:flex; justify-content:center; gap:14px; padding:6px;}
#energyorb{width:56px; height:56px; border-radius:50%; background:radial-gradient(#e8d060,#a08020); color:#2a2000;
  display:flex; flex-direction:column; align-items:center; justify-content:center; font-weight:bold; font-size:18px;
  box-shadow:0 0 16px #c9a22766; flex-shrink:0;}
#energyorb small{font-size:9px;}
#pstats{font-size:13px; min-width:200px;}
#deckinfo{font-size:12px; color:var(--dim);}
#log{height:44px; overflow-y:auto; font-size:12px; color:var(--dim); padding:4px 12px; text-align:center;}
#log div{animation:fadein .3s;}
@keyframes fadein{from{opacity:0}to{opacity:1}}
.floating{position:absolute; font-weight:bold; font-size:22px; pointer-events:none; z-index:50;
  animation:floatup 1s forwards; text-shadow:0 0 6px #000;}
@keyframes floatup{from{opacity:1; transform:translateY(0)}to{opacity:0; transform:translateY(-50px)}}
.shake{animation:shake .3s;}
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}

/* overlays */
#overlay{position:fixed; inset:0; background:#0b0a12e6; z-index:100; display:flex; align-items:center; justify-content:center; padding:20px;}
#overlay .panel{max-width:900px; max-height:88vh; overflow-y:auto; width:100%;}
.reward-row{display:flex; gap:16px; justify-content:center; flex-wrap:wrap; margin:14px 0;}
.choice-btn{display:block; width:100%; text-align:left; margin:8px 0; padding:12px 16px; font-size:15px;}
.potion{display:inline-flex; align-items:center; gap:6px; background:#221c38; border:1px solid #4a3f6a; border-radius:8px;
  padding:5px 10px; cursor:pointer; font-size:13px;}
.potion:hover{border-color:var(--gold);}
.potion .picon{font-size:20px;}
#potionslots{display:flex; gap:8px; align-items:center;}
.pslot{width:40px; height:40px; border-radius:8px; border:1px dashed #4a3f6a; display:flex; align-items:center; justify-content:center;
  font-size:22px; cursor:pointer; background:#16142280;}
.pslot:hover{border-color:var(--gold);}
.relic{display:inline-flex; align-items:center; gap:5px; background:#2a2440; border:1px solid #6a5a2a; border-radius:8px;
  padding:3px 8px; font-size:12px; cursor:help; margin:2px;}
.shopitem{display:flex; flex-direction:column; align-items:center; width:150px; padding:10px;}
.shopitem .price{color:var(--gold); font-size:14px; margin:6px 0;}
.sold{opacity:.35; pointer-events:none;}
.eventtext{font-size:16px; line-height:1.6; color:var(--text); margin:14px 0; font-style:italic;}
.bigmsg{text-align:center; padding:60px 20px;}
.bigmsg h1{font-size:44px; margin-bottom:20px;}
.tooltip{position:fixed; background:#1a1628; border:1px solid var(--gold); border-radius:6px; padding:8px 12px;
  font-size:12px; max-width:240px; z-index:200; pointer-events:none; line-height:1.4;}
::-webkit-scrollbar{width:8px} ::-webkit-scrollbar-thumb{background:#3a3355; border-radius:4px}
</style>
</head>
<body>
<div id="app"></div>
<div id="overlay" class="hidden"></div>
<script>
'use strict';
/* ============================ DATA ============================ */
const CLASSES = {
  reaper:{name:'Blood Reaper', icon:'🩸', desc:'Sacrifice HP for power. Life steal and Bleed.',
    sig:'r_sanguine'},
  shadow:{name:'Shadowmancer', icon:'🌑', desc:'Curses and shadows. Weak, Vulnerable, Intangible.',
    sig:'s_shadowbolt'},
  hell:{name:'Hellforged', icon:'🔥', desc:'Fire and brimstone. Burn, imps, raw energy.',
    sig:'h_firebolt'},
};
function mk(id,name,cls,type,cost,rarity,text,upText,fx,up,opts){
  return Object.assign({id,name,cls,type,cost,rarity,text,upText,fx,up:up||{}},opts||{});
}
// fx: array of effect ops. op: {t:'dmg',v,target:'one'|'all'|'rand',n:hits} {t:'blk'} {t:'heal'} {t:'hploss'}
// {t:'bleed'|'weak'|'vuln'|'burn'|'poison',v,target:'one'|'all'|'rand'} {t:'draw'} {t:'energy'} {t:'regen'} {t:'intangible'} {t:'dex'}
// {t:'str',v,temp?} {t:'strdown',v} {t:'nextblk'} {t:'nextdex'} {t:'nextenergy'} {t:'exhaust'} {t:'addcard',id,to:'hand'|'draw'|'discard'}
// custom(id) handles specials. Powers use onTurnStart/onPlay hooks via custom field.
const CARDS = {};
function def(...a){const c=mk(...a); CARDS[c.id]=c; return c;}
// Basic
def('strike','Strike','basic','Attack',1,'Basic','Deal 6 damage.','Deal 8 damage.',[{t:'dmg',v:6}],{fx:[{t:'dmg',v:8}]});
def('defend','Defend','basic','Skill',1,'Basic','Gain 5 Block.','Gain 8 Block.',[{t:'blk',v:5}],{fx:[{t:'blk',v:8}]});
// Blood Reaper
def('r_sanguine','Sanguine Strike','reaper','Attack',1,'Common','Deal 8 damage. Heal 2 HP.','Deal 10 damage. Heal 3 HP.',[{t:'dmg',v:8},{t:'heal',v:2}],{fx:[{t:'dmg',v:10},{t:'heal',v:3}]});
def('r_hemorrhage','Hemorrhage','reaper','Attack',1,'Common','Deal 5 damage. Apply 3 Bleed.','Deal 6 damage. Apply 4 Bleed.',[{t:'dmg',v:5},{t:'bleed',v:3}],{fx:[{t:'dmg',v:6},{t:'bleed',v:4}]});
def('r_bloodletting','Bloodletting','reaper','Attack',1,'Uncommon','Lose 3 HP. Deal 12 damage.','Lose 2 HP. Deal 15 damage.',[{t:'hploss',v:3},{t:'dmg',v:12}],{fx:[{t:'hploss',v:2},{t:'dmg',v:15}]});
def('r_crimson_eruption','Crimson Eruption','reaper','Attack',2,'Rare','Deal 8 damage to ALL enemies. Lose 2 HP.','Deal 10 damage to ALL enemies. Lose 2 HP.',[{t:'hploss',v:2},{t:'dmg',v:8,target:'all'}],{fx:[{t:'hploss',v:2},{t:'dmg',v:10,target:'all'}]});
def('r_crimson_shield','Crimson Shield','reaper','Skill',1,'Common','Gain 6 Block. Lose 2 HP.','Gain 8 Block. Lose 2 HP.',[{t:'blk',v:6},{t:'hploss',v:2}],{fx:[{t:'blk',v:8},{t:'hploss',v:2}]});
def('r_blood_ward','Blood Ward','reaper','Skill',1,'Common','Gain 3 Block. Gain 1 Regeneration.','Gain 4 Block. Gain 2 Regeneration.',[{t:'blk',v:3},{t:'regen',v:1}],{fx:[{t:'blk',v:4},{t:'regen',v:2}]});
def('r_sacrificial_barrier','Sacrificial Barrier','reaper','Skill',2,'Uncommon','Gain 10 Block. Lose 5 HP.','Gain 13 Block. Lose 5 HP.',[{t:'blk',v:10},{t:'hploss',v:5}],{fx:[{t:'blk',v:13},{t:'hploss',v:5}]});
def('r_vital_surge','Vital Surge','reaper','Skill','X','Rare','Gain Block equal to your missing HP. Exhaust.','Gain Block equal to missing HP +3. Exhaust.',[{t:'xcost'}],{upblk:3},{exhaust:true,custom:'vital_surge'});
def('r_lifetap','Life Tap','reaper','Skill',0,'Common','Draw 2 cards. Lose 3 HP.','Draw 3 cards. Lose 3 HP.',[{t:'draw',v:2},{t:'hploss',v:3}],{fx:[{t:'draw',v:3},{t:'hploss',v:3}]});
def('r_rend_flesh','Rend Flesh','reaper','Skill',1,'Uncommon','Apply 5 Bleed to ALL enemies. Lose 2 HP.','Apply 7 Bleed to ALL enemies. Lose 2 HP.',[{t:'bleed',v:5,target:'all'},{t:'hploss',v:2}],{fx:[{t:'bleed',v:7,target:'all'},{t:'hploss',v:2}]});
def('r_soul_siphon','Soul Siphon','reaper','Power',2,'Uncommon','At the start of your turn, gain 1 Energy and lose 1 HP.','Cost 1. At the start of your turn, gain 1 Energy and lose 1 HP.',[],{cost:1},{power:'soul_siphon'});
def('r_blood_pact','Blood Pact','reaper','Power',1,'Rare','Whenever you lose HP from a card, gain 1 Strength.','Whenever you lose HP from a card, gain 2 Strength.',[],{amt:2},{power:'blood_pact'});
// Shadowmancer
def('s_shadowbolt','Shadow Bolt','shadow','Attack',1,'Common','Deal 6 damage. Apply 1 Weak.','Deal 8 damage. Apply 2 Weak.',[{t:'dmg',v:6},{t:'weak',v:1}],{fx:[{t:'dmg',v:8},{t:'weak',v:2}]});
def('s_grasp','Grasp of Despair','shadow','Attack',1,'Common','Deal 4 damage. Apply 1 Vulnerable.','Deal 5 damage. Apply 2 Vulnerable.',[{t:'dmg',v:4},{t:'vuln',v:1}],{fx:[{t:'dmg',v:5},{t:'vuln',v:2}]});
def('s_nightmare','Nightmare Lash','shadow','Attack',2,'Uncommon','Deal 7 damage. If enemy has Weak, deal 7 more.','Deal 9 damage. If Weak, deal 9 more.',[{t:'dmg',v:7}],{fx:[{t:'dmg',v:9}]},{custom:'nightmare'});
def('s_eternal_darkness','Eternal Darkness','shadow','Attack',2,'Rare','Deal 3 damage to a random enemy 3 times.','Deal 4 damage to a random enemy 3 times.',[{t:'dmg',v:3,target:'rand',n:3}],{fx:[{t:'dmg',v:4,target:'rand',n:3}]});
def('s_cloak','Cloak of Shadows','shadow','Skill',1,'Common','Gain 5 Block. Draw 1 card.','Gain 7 Block. Draw 1 card.',[{t:'blk',v:5},{t:'draw',v:1}],{fx:[{t:'blk',v:7},{t:'draw',v:1}]});
def('s_mist','Haunting Mist','shadow','Skill',1,'Common','Gain 7 Block. Apply 1 Weak to ALL enemies.','Gain 9 Block. Apply 1 Weak to ALL enemies.',[{t:'blk',v:7},{t:'weak',v:1,target:'all'}],{fx:[{t:'blk',v:9},{t:'weak',v:1,target:'all'}]});
def('s_phantom','Phantom Armor','shadow','Skill',1,'Uncommon','Gain 8 Block. Next turn, gain 2 Dexterity.','Gain 10 Block. Next turn, gain 3 Dexterity.',[{t:'blk',v:8},{t:'nextdex',v:2}],{fx:[{t:'blk',v:10},{t:'nextdex',v:3}]});
def('s_shroud','Whispering Shroud','shadow','Skill',1,'Rare','Gain 4 Block. Next turn, gain 4 Block.','Gain 5 Block. Next turn, gain 5 Block.',[{t:'blk',v:4},{t:'nextblk',v:4}],{fx:[{t:'blk',v:5},{t:'nextblk',v:5}]});
def('s_shadowform','Shadowform','shadow','Skill',2,'Uncommon','Gain 2 Intangible. Exhaust.','Gain 3 Intangible. Exhaust.',[{t:'intangible',v:2}],{fx:[{t:'intangible',v:3}]},{exhaust:true});
def('s_misfortune','Curse of Misfortune','shadow','Skill',1,'Common','Enemy loses 3 Strength this turn. Apply 1 Vulnerable.','Enemy loses 4 Strength this turn. Apply 2 Vulnerable.',[{t:'strdown',v:3},{t:'vuln',v:1}],{fx:[{t:'strdown',v:4},{t:'vuln',v:2}]});
def('s_dark_ritual','Dark Ritual','shadow','Skill',0,'Uncommon','Gain 2 Energy. Shuffle a Dazed into your draw pile.','Gain 2 Energy. Shuffle a Dazed into your discard pile.',[{t:'energy',v:2},{t:'addcard',id:'dazed',to:'draw'}],{fx:[{t:'energy',v:2},{t:'addcard',id:'dazed',to:'discard'}]});
def('s_umbral','Umbral Mastery','shadow','Power',2,'Rare','At the start of your turn, apply 1 Weak to ALL enemies.','At the start of your turn, apply 2 Weak to ALL enemies.',[],{amt:2},{power:'umbral'});
// Hellforged
def('h_firebolt','Firebolt','hell','Attack',1,'Common','Deal 7 damage. Apply 2 Burn.','Deal 9 damage. Apply 3 Burn.',[{t:'dmg',v:7},{t:'burn',v:2}],{fx:[{t:'dmg',v:9},{t:'burn',v:3}]});
def('h_hellfire','Hellfire Blast','hell','Attack',2,'Uncommon','Deal 9 damage to ALL enemies.','Deal 12 damage to ALL enemies.',[{t:'dmg',v:9,target:'all'}],{fx:[{t:'dmg',v:12,target:'all'}]});
def('h_searing','Searing Strike','hell','Attack',1,'Common','Deal 6 damage. Gain 1 Energy next turn.','Deal 8 damage. Gain 1 Energy next turn.',[{t:'dmg',v:6},{t:'nextenergy',v:1}],{fx:[{t:'dmg',v:8},{t:'nextenergy',v:1}]});
def('h_immolate','Immolate','hell','Attack',2,'Rare','Deal 12 damage. Exhaust.','Deal 16 damage. Exhaust.',[{t:'dmg',v:12}],{fx:[{t:'dmg',v:16}]},{exhaust:true});
def('h_obsidian','Obsidian Skin','hell','Skill',1,'Common','Gain 7 Block.','Gain 10 Block.',[{t:'blk',v:7}],{fx:[{t:'blk',v:10}]});
def('h_flame_ward','Flame Ward','hell','Skill',1,'Common','Gain 5 Block. Apply 2 Burn to a random enemy.','Gain 6 Block. Apply 3 Burn to a random enemy.',[{t:'blk',v:5},{t:'burn',v:2,target:'rand'}],{fx:[{t:'blk',v:6},{t:'burn',v:3,target:'rand'}]});
def('h_resilience','Demonic Resilience','hell','Skill',1,'Uncommon','Gain 4 Block. Heal 2 HP.','Gain 5 Block. Heal 3 HP.',[{t:'blk',v:4},{t:'heal',v:2}],{fx:[{t:'blk',v:5},{t:'heal',v:3}]});
def('h_brimstone','Brimstone Shield','hell','Skill',1,'Rare','Gain 3 Block. Gain 2 Strength this turn.','Gain 4 Block. Gain 3 Strength this turn.',[{t:'blk',v:3},{t:'str',v:2,temp:true}],{fx:[{t:'blk',v:4},{t:'str',v:3,temp:true}]});
def('h_summon_imp','Summon Imp','hell','Skill',1,'Common','Add an Imp Claw to your hand. Exhaust.','Add an Imp Claw+ to your hand. Exhaust.',[{t:'addcard',id:'imp_claw',to:'hand'}],{fx:[{t:'addcard',id:'imp_claw_up',to:'hand'}]},{exhaust:true});
def('h_pact','Infernal Pact','hell','Skill',0,'Uncommon','Lose 2 HP. Gain 2 Energy. Add an Imp Claw to your hand.','Lose 1 HP. Gain 2 Energy. Add an Imp Claw+ to your hand.',[{t:'hploss',v:2},{t:'energy',v:2},{t:'addcard',id:'imp_claw',to:'hand'}],{fx:[{t:'hploss',v:1},{t:'energy',v:2},{t:'addcard',id:'imp_claw_up',to:'hand'}]});
def('h_aura','Hellfire Aura','hell','Power',2,'Rare','At the start of your turn, deal 3 damage to ALL enemies.','At the start of your turn, deal 5 damage to ALL enemies.',[],{amt:5},{power:'hellfire_aura'});
def('h_soul_burn','Soul Burn','hell','Skill',1,'Uncommon','Apply 8 Burn to an enemy. Draw 1 card.','Apply 10 Burn to an enemy. Draw 1 card.',[{t:'burn',v:8},{t:'draw',v:1}],{fx:[{t:'burn',v:10},{t:'draw',v:1}]});
// Tokens
def('imp_claw','Imp Claw','token','Attack',0,'Special','Deal 4 damage. Exhaust.','Deal 6 damage. Exhaust.',[{t:'dmg',v:4}],{fx:[{t:'dmg',v:6}]},{exhaust:true,token:true});
def('imp_claw_up','Imp Claw+','token','Attack',0,'Special','Deal 6 damage. Exhaust.','',[{t:'dmg',v:6}],{},{exhaust:true,token:true});
// Colorless
def('c_insight','Flash of Insight','colorless','Skill',1,'Common','Draw 2 cards.','Draw 3 cards.',[{t:'draw',v:2}],{fx:[{t:'draw',v:3}]});
def('c_ironwill','Iron Will','colorless','Skill',1,'Common','Gain 8 Block.','Gain 11 Block.',[{t:'blk',v:8}],{fx:[{t:'blk',v:11}]});
def('c_slash','Quick Slash','colorless','Attack',1,'Common','Deal 7 damage.','Deal 9 damage.',[{t:'dmg',v:7}],{fx:[{t:'dmg',v:9}]});
def('c_brew','Potent Brew','colorless','Skill',0,'Uncommon','Gain 1 Energy. Exhaust.','Gain 2 Energy. Exhaust.',[{t:'energy',v:1}],{fx:[{t:'energy',v:2}]},{exhaust:true});
// Curses / Status
def('wound','Wound','curse','Curse','-','Special','Unplayable. Ethereal. Exhaust.','',[],{},{unplayable:true,ethereal:true,exhaust:true});
def('dazed','Dazed','curse','Status','-','Special','Unplayable. Ethereal. Exhaust.','',[],{},{unplayable:true,ethereal:true,exhaust:true});
def('doubt','Doubt','curse','Curse','-','Special','Unplayable. At the end of your turn, lose 1 HP.','',[],{},{unplayable:true,curse:true});

const CLASS_POOL = {
  reaper:['r_sanguine','r_hemorrhage','r_crimson_shield','r_blood_ward','r_lifetap','r_bloodletting','r_sacrificial_barrier','r_rend_flesh','r_soul_siphon','r_crimson_eruption','r_vital_surge','r_blood_pact'],
  shadow:['s_shadowbolt','s_grasp','s_cloak','s_mist','s_misfortune','s_nightmare','s_phantom','s_dark_ritual','s_shadowform','s_eternal_darkness','s_shroud','s_umbral'],
  hell:['h_firebolt','h_obsidian','h_flame_ward','h_resilience','h_searing','h_soul_burn','h_hellfire','h_pact','h_summon_imp','h_immolate','h_brimstone','h_aura'],
};
const COLORLESS_POOL=['c_insight','c_ironwill','c_slash','c_brew'];
const RARITY_W={Common:60,Uncommon:30,Rare:10};

const RELICS={
  bloodstone:{name:'Bloodstone',icon:'🩸',rarity:'Common',desc:'Whenever you lose HP from a card, heal 1 HP.'},
  demon_horns:{name:'Demon Horns',icon:'😈',rarity:'Special',desc:'At the start of each combat, add a random Imp Claw to your hand (once per combat).',cls:'hell'},
  shadow_cloak:{name:'Shadow Cloak',icon:'🌑',rarity:'Rare',desc:'At the start of each combat, gain 1 Intangible.'},
  cursed_amulet:{name:'Cursed Amulet',icon:'📿',rarity:'Common',desc:'At the start of each combat, apply 1 Weak to ALL enemies.'},
  tome:{name:'Tome of Dark Rituals',icon:'📖',rarity:'Uncommon',desc:'Whenever you play a Power card, draw 1 card.'},
  bleeding_edge:{name:'Bleeding Edge',icon:'🗡️',rarity:'Common',desc:'At the start of your turn, apply 1 Bleed to a random enemy.'},
  soul_ember:{name:'Soul Ember',icon:'🔥',rarity:'Rare',desc:'Whenever you apply Burn to an enemy, deal 2 additional damage.'},
  hourglass:{name:'Eternal Hourglass',icon:'⏳',rarity:'Uncommon',desc:'At the start of your turn, if you have no cards in hand, gain 1 Energy.'},
  sacrificial_dagger:{name:'Sacrificial Dagger',icon:'🔪',rarity:'Special',desc:'Your first Attack that costs HP each combat costs 1 less HP.',cls:'reaper'},
  vial_of_shadows:{name:'Vial of Shadows',icon:'🧪',rarity:'Rare',desc:'When obtained, remove a card from your deck. Then this relic does nothing.',unique:true},
  ashen_armor:{name:'Ashen Armor',icon:'🛡️',rarity:'Common',desc:'At the end of combat, heal 6 HP.'},
  greedy_imp:{name:'Greedy Imp',icon:'💰',rarity:'Common',desc:'Enemies drop 10 extra Gold.'},
};
const BOSS_RELICS={
  cursed_crown:{name:'Cursed Crown',icon:'👑',desc:'Gain 1 Energy at start of each turn. Curses can no longer be removed.'},
  ectoplasm:{name:'Ectoplasm',icon:'👻',desc:'Gain 1 Energy. Lose all Gold and gain no more Gold this run.'},
  philosophers:{name:"Philosopher's Stone",icon:'💎',desc:'Gain 1 Energy. All enemies start combat with 1 Strength.'},
  black_blood:{name:'Black Blood',icon:'🖤',desc:'At start of combat, lose 10% max HP. Whenever you play an Attack, heal 1 HP.'},
  shadow_amulet:{name:'Shadow Amulet',icon:'🔮',desc:'Start each turn with 1 Intangible. Reduce max HP by 15.'},
};
const POTIONS={
  blood_vial:{name:'Blood Vial',icon:'🧰',desc:'Heal 15 HP.'},
  firebomb:{name:'Firebomb',icon:'💣',desc:'Deal 10 damage to ALL enemies.'},
  dex_tonic:{name:'Dex Tonic',icon:'🌀',desc:'Gain 3 Dexterity (until end of next turn).'},
  energy_elixir:{name:'Energy Elixir',icon:'⚡',desc:'Gain 2 Energy this turn.'},
  cursed_brew:{name:'Cursed Brew',icon:'☠️',desc:'Apply 3 Vulnerable to all enemies.'},
  bleeding_flask:{name:'Bleeding Flask',icon:'🩹',desc:'Apply 6 Bleed to an enemy.'},
  imp_bottle:{name:'Imp-in-a-Bottle',icon:'🍾',desc:'Add an Imp Claw+ to your hand.'},
  smoke_bomb:{name:'Smoke Bomb',icon:'💨',desc:'Escape from a non-boss combat, no rewards.'},
};
const POTION_KEYS=Object.keys(POTIONS);

/* ---- Enemies ---- */
function E(id,name,hp,icon,steps,opts){return Object.assign({id,name,hp,icon,steps},opts||{});}
const ENEMIES={
  // Act 1
  skeleton:E('skeleton','Skeleton Warrior',22,'💀',[{t:'atk',v:6},{t:'atk',v:6},{t:'def',v:8}]),
  zombie:E('zombie','Zombie Brute',28,'🧟',[{t:'atk',v:8},{t:'deb',weak:2},{t:'atk',v:8}]),
  cultist:E('cultist','Dark Cultist',20,'🕯️',[{t:'buf',str:3},{t:'atk',v:5},{t:'atk',v:5}]),
  rat:E('rat','Rat Swarm',15,'🐀',[{t:'atk',v:3,n:2},{t:'atk',v:3,n:2}]),
  tomb_guardian:E('tomb_guardian','Tomb Guardian',42,'🗿',[{t:'atk',v:6,blk:8},{t:'atk',v:4,n:3},{t:'atk',v:12}],{elite:true}),
  blood_witch:E('blood_witch','Blood Witch',36,'🧝‍♀️',[{t:'atk',v:5,heal:4},{t:'deb',vuln:1,weak:1},{t:'atk',v:7,heal:4}],{elite:true}),
  lich_minion:E('lich_minion','Skeleton Minion',15,'💀',[{t:'atk',v:5}],{minion:true}),
  lich_king:E('lich_king','Lich King',65,'👑',
    [{t:'sum',id:'lich_minion',max:2},{t:'atk',v:8},{t:'def',v:12}],{boss:true,
    phase:{hp:30,strPerTurn:2,steps:[{t:'atk',v:10},{t:'atk',v:10},{t:'def',v:15}]}}),
  // Act 2
  wraith:E('wraith','Wraith',30,'👻',[{t:'atk',v:7,weak:1},{t:'atk',v:7},{t:'def',v:10}]),
  beast:E('beast','Corrupted Beast',38,'🐺',[{t:'atk',v:12},{t:'def',v:5,selfvuln:1},{t:'atk',v:12}]),
  thorned:E('thorned','Thorned Horror',25,'🌵',[{t:'atk',v:4,n:3},{t:'deb',bleed:3},{t:'atk',v:8}]),
  witchdoctor:E('witchdoctor','Witch Doctor',22,'🥁',[{t:'buf',blk:4,regen:2},{t:'atk',v:6,poison:2},{t:'atk',v:6}]),
  treant_protector:E('treant_protector','Treant Protector',50,'🌳',[{t:'atk',v:9,blk:6},{t:'deb',vuln:2},{t:'atk',v:10,n:2}],{elite:true}),
  plague_shaman:E('plague_shaman','Plague Shaman',42,'🐸',[{t:'deb',poison:4},{t:'sum',id:'plague_rat',max:2},{t:'atk',v:8}],{elite:true}),
  plague_rat:E('plague_rat','Plague Rat',18,'🐀',[{t:'atk',v:5}],{minion:true}),
  eldritch_treant:E('eldritch_treant','Eldritch Treant',75,'🌲',
    [{t:'atk',v:10,weak:2},{t:'def',v:15,regen:2},{t:'atk',v:8,n:2}],{boss:true,
    phase:{hp:40,strPerTurn:1,steps:[{t:'atk',v:14,vuln:1},{t:'def',v:20,heal:5},{t:'atk',v:14,vuln:1}]}}),
  // Act 3
  hellhound:E('hellhound','Hellhound',26,'🐕',[{t:'atk',v:8,burn:2},{t:'atk',v:8},{t:'def',v:9}]),
  shadow_demon:E('shadow_demon','Shadow Demon',30,'👤',[{t:'atk',v:7,weak:1},{t:'atk',v:7},{t:'buf',intangible:1}]),
  infernal_imp:E('infernal_imp','Infernal Imp',18,'👺',[{t:'atk',v:5},{t:'atk',v:5},{t:'sum',id:'infernal_imp',max:3}]),
  abyssal_cultist:E('abyssal_cultist','Abyssal Cultist',24,'🕯️',[{t:'buf',strAll:2},{t:'atk',v:7},{t:'atk',v:7}]),
  flamespawn:E('flamespawn','Flamespawn',48,'🌋',[{t:'atk',v:9,burn:3},{t:'def',v:10,str:2},{t:'atk',v:8,n:2}],{elite:true}),
  dread_knight:E('dread_knight','Dread Knight',55,'⚔️',[{t:'atk',v:12},{t:'deb',bleed:3},{t:'def',v:12,heal:4}],{elite:true}),
  archdemon:E('archdemon','Archdemon',85,'😈',
    [{t:'sum',id:'hellhound',max:1},{t:'atk',v:12,burn:2},{t:'def',v:15,str:3}],{boss:true,
    phase:{hp:45,strPerTurn:2,steps:[{t:'atk',v:16},{t:'atk',v:7,n:3},{t:'atk',v:16}]}}),
  shadow_thief:E('shadow_thief','Shadow Thief',30,'🥷',[{t:'atk',v:7},{t:'atk',v:7},{t:'def',v:8}]),
};

/* ---- Map layout (same for all acts, enemy pools differ) ---- */
const MAP_LAYOUT=[
  null, // floor 1 = start
  {f:2,nodes:[{t:'enemy',g:['skeleton']},{t:'enemy',g:['zombie']}]},
  {f:3,nodes:[{t:'enemy',g:['cultist']}]},
  {f:4,nodes:[{t:'event'}]},
  {f:5,nodes:[{t:'shop'}]},
  {f:6,nodes:[{t:'enemy',g:['rat']}]},
  {f:7,nodes:[{t:'rest'}]},
  {f:8,nodes:[{t:'enemy',g:['skeleton','skeleton']},{t:'enemy',g:['zombie']}]},
  {f:9,nodes:[{t:'enemy',g:['tomb_guardian'],elite:true},{t:'enemy',g:['skeleton','rat']}]},
  {f:10,nodes:[{t:'event'}]},
  {f:11,nodes:[{t:'enemy',g:['cultist','rat']}]},
  {f:12,nodes:[{t:'enemy',g:['blood_witch'],elite:true},{t:'enemy',g:['zombie','skeleton']}]},
  {f:13,nodes:[{t:'enemy',g:['cultist','cultist']}]},
  {f:14,nodes:[{t:'rest'}]},
  {f:15,nodes:[{t:'boss'}]},
];
const ACT_POOLS={
  1:{g:{2:[['skeleton'],['zombie']],3:[['cultist']],6:[['rat']],8:[['skeleton','skeleton'],['zombie']],9:[['E'],['skeleton','rat']],11:[['cultist','rat']],12:[['E'],['zombie','skeleton']],13:[['cultist','cultist']]},
     elites:['tomb_guardian','blood_witch'],boss:'lich_king'},
  2:{g:{2:[['wraith'],['thorned']],3:[['witchdoctor']],6:[['beast']],8:[['wraith','thorned'],['beast']],9:[['E'],['thorned','witchdoctor']],11:[['witchdoctor','wraith']],12:[['E'],['beast','thorned']],13:[['wraith','witchdoctor']]},
     elites:['treant_protector','plague_shaman'],boss:'eldritch_treant'},
  3:{g:{2:[['hellhound'],['infernal_imp']],3:[['abyssal_cultist']],6:[['shadow_demon']],8:[['hellhound','infernal_imp'],['shadow_demon']],9:[['E'],['infernal_imp','abyssal_cultist']],11:[['shadow_demon','hellhound']],12:[['E'],['abyssal_cultist','infernal_imp']],13:[['shadow_demon','abyssal_cultist']]},
     elites:['flamespawn','dread_knight'],boss:'archdemon'},
};
const ACT_NAMES={1:'Act 1: The Crypts',2:'Act 2: Blighted Woods',3:'Act 3: Abyssal Spire'};
const EVENTS=[
  {id:'altar',name:'The Forgotten Altar',icon:'🗿',
   text:'A blood-stained altar hums with power. The carvings seem to watch you.',
   choices:[
    {label:'Offer 15 HP',can:s=>s.hp>15,do:s=>{loseHpOut(s,15);giveRelic(pickRelic('Rare'));}},
    {label:'Pray',can:()=>true,do:s=>{healOut(s,10);addCardToDeck(s,makeCard('doubt'));msg('You feel a Doubt creep into your deck.');}},
    {label:'Leave',can:()=>true,do:()=>{}},
   ]},
  {id:'peddler',name:'Shadowy Peddler',icon:'🥷',
   text:'A hooded figure beckons. "Everything has a price, hero."',
   choices:[
    {label:'Trade a card for 100 Gold',can:s=>s.deck.length>0,do:s=>{pickCardFromDeck(s,'Choose a card to sacrifice',c=>{removeFromDeck(s,c);s.gold+=100;msg('You gain 100 Gold.');});}},
    {label:'Buy his wares (50 Gold)',can:s=>s.gold>=50,do:s=>{s.gold-=50;addPotion(S);giveRelic(pickRelic('Common'));}},
    {label:'Attack him! (combat, 75 Gold reward)',can:()=>true,do:s=>{startCombat([{id:'shadow_thief'}],{bonusGold:75});}},
   ]},
  {id:'spring',name:'Cursed Spring',icon:'⛲',
   text:'A spring bubbles with dark water. Whispers rise from its depths.',
   choices:[
    {label:'Drink (Lose 10 HP, remove all Curses)',can:s=>s.hp>10,do:s=>{loseHpOut(s,10);const n=s.deck.length;s.deck=s.deck.filter(c=>!(CARDS[c.id].cls==='curse'&&c.id!=='dazed'&&c.id!=='wound'));msg((n-s.deck.length)+' curses washed away.');}},
    {label:'Bottle it (gain Cursed Brew)',can:()=>true,do:s=>{addPotion(s,'cursed_brew');}},
    {label:'Leave',can:()=>true,do:()=>{}},
   ]},
  {id:'dead',name:'Restless Dead',icon:'💀',
   text:'Ghosts circle you. "Flesh for power," they moan.',
   choices:[
    {label:'Accept (Lose 20 HP, upgrade 2 random cards)',can:s=>s.hp>20,do:s=>{loseHpOut(s,20);const upgradable=s.deck.filter(c=>!c.upgraded&&CARDS[c.id].upText);shuffle(upgradable);upgradable.slice(0,2).forEach(c=>{c.upgraded=true;});msg('Two cards upgraded.');}},
    {label:'Refuse',can:()=>true,do:()=>{}},
   ]},
  {id:'gambler',name:'Impish Gambler',icon:'👺',
   text:'A grinning imp rattles dice made of bone. "Feeling lucky?"',
   choices:[
    {label:'Bet 30 Gold (50% to double)',can:s=>s.gold>=30,do:s=>{s.gold-=30;if(Math.random()<0.5){s.gold+=60;msg('You win! +60 Gold.');}else msg('The imp cackles and vanishes with your gold.');}},
    {label:'Wager a card (random; Curse pays 100 Gold)',can:s=>s.deck.length>0,do:s=>{const i=Math.floor(Math.random()*s.deck.length);const c=s.deck[i];const was=CARDS[c.id].cls==='curse';s.deck.splice(i,1);if(was){s.gold+=100;msg('The removed card was a Curse! +100 Gold.');}else msg(cardName(c)+' is gone. The imp shrugs.');}},
    {label:'Leave',can:()=>true,do:()=>{}},
   ]},
  {id:'library',name:'Ancient Library',icon:'📚',
   text:'Dusty tomes contain forgotten knowledge. Some pages should stay unread.',
   choices:[
    {label:'Study (Lose 5 HP, +1 Energy next combat)',can:s=>s.hp>5,do:s=>{loseHpOut(s,5);s.nextCombatEnergy=(s.nextCombatEnergy||0)+1;msg('You will begin your next combat with +1 Energy.');}},
    {label:'Read forbidden page (random Power card, gain Doubt)',can:()=>true,do:s=>{const pool=CLASS_POOL[s.cls].filter(id=>CARDS[id].type==='Power');if(pool.length)addCardToDeck(s,makeCard(pick(pool)));addCardToDeck(s,makeCard('doubt'));}},
    {label:'Leave',can:()=>true,do:()=>{}},
   ]},
];
</script>
<script>
/* ============================ STATE & HELPERS ============================ */
const app=document.getElementById('app');
const overlayEl=document.getElementById('overlay');
let S=null; // run state
let C=null; // combat state
const rnd=n=>Math.floor(Math.random()*n);
const pick=a=>a[rnd(a.length)];
function shuffle(a){for(let i=a.length-1;i>0;i--){const j=rnd(i+1);[a[i],a[j]]=[a[j],a[i]];}return a;}
function makeCard(id,upgraded){return {id,upgraded:!!upgraded};}
function cardDef(c){return CARDS[c.id];}
function cardName(c){return cardDef(c).name+(c.upgraded?'+':'');}
function cardCost(c){const d=cardDef(c);let cost=d.cost;if(c.upgraded&&d.up.cost!==undefined)cost=d.up.cost;return cost;}
function cardText(c){const d=cardDef(c);return c.upgraded?d.upText:d.text;}
function cardFx(c){const d=cardDef(c);return (c.upgraded&&d.up.fx)?d.up.fx:d.fx;}
function cardTypeClass(c){const t=cardDef(c).type;return t==='Attack'?'atk':t==='Skill'?'skl':t==='Power'?'pwr':'crs';}
function esc(s){return s.replace(/</g,'&lt;');}
function msg(t){if(C){C.log.push(t);const l=document.getElementById('log');if(l){l.innerHTML=C.log.slice(-4).map(x=>'<div>'+esc(x)+'</div>').join('');l.scrollTop=999;}}}
function floatText(el,txt,color){if(!el)return;const f=document.createElement('div');f.className='floating';f.textContent=txt;f.style.color=color||'#fff';f.style.left=(el.offsetWidth/2-20)+'px';f.style.top='10px';el.appendChild(f);setTimeout(()=>f.remove(),1000);}
function showOverlay(html){overlayEl.innerHTML='<div class="panel">'+html+'</div>';overlayEl.classList.remove('hidden');}
function hideOverlay(){overlayEl.classList.add('hidden');overlayEl.innerHTML='';}
function tooltipAttach(el,html){el.addEventListener('mousemove',e=>{let t=document.querySelector('.tooltip');if(!t){t=document.createElement('div');t.className='tooltip';document.body.appendChild(t);}t.innerHTML=html;t.style.display='block';t.style.left=Math.min(e.clientX+14,window.innerWidth-260)+'px';t.style.top=(e.clientY+14)+'px';});el.addEventListener('mouseleave',()=>{const t=document.querySelector('.tooltip');if(t)t.style.display='none';});}

function newRun(cls){
  S={cls,hp:70,maxhp:70,gold:99,deck:[],relics:[],potions:[],act:1,floor:1,
     mapDone:{},bossRelicsTaken:[],removedUsed:false,energyNextCombat:0};
  ['strike','strike','strike','strike','strike','defend','defend','defend','defend',CLASSES[cls].sig].forEach(id=>S.deck.push(makeCard(id)));
  // act map instance
  buildMap();
  renderMap();
}
function buildMap(){
  const pool=ACT_POOLS[S.act];
  S.map=MAP_LAYOUT.map(row=>{
    if(!row)return null;
    return {f:row.f,nodes:row.nodes.map((n,idx)=>{
      const node={t:n.t,done:false};
      if(n.t==='enemy'){
        node.elite=!!n.elite;
        node.group=n.elite?[pick(pool.elites)]:pool.g[row.f][idx];
      }
      if(n.t==='boss')node.group=[pool.boss];
      return node;
    })};
  });
  S.floor=2;
}
function currentNode(){const row=S.map.find(r=>r&&r.f===S.floor);return row;}
function availableNodes(){
  const row=currentNode();if(!row)return[];
  return row.nodes.filter(n=>!n.done);
}
function relicCount(id){return S.relics.filter(r=>r===id).length;}
function hasRelic(id){return S.relics.includes(id);}
function pickRelic(rarity){
  let pool=Object.keys(RELICS).filter(id=>{
    const r=RELICS[id];
    if(r.cls&&r.cls!==S.cls)return false;
    if(id==='vial_of_shadows'&&hasRelic(id))return false;
    if(rarity&&r.rarity!==rarity)return false;
    if(!rarity&&r.rarity==='Special')return false;
    return true;
  });
  return pick(pool);
}
function giveRelic(id){
  if(!id)return;
  if(RELICS[id].unique&&hasRelic(id))return;
  S.relics.push(id);
  msg('Obtained relic: '+RELICS[id].name);
  if(id==='vial_of_shadows'){pickCardFromDeck(S,'Vial of Shadows: remove a card',c=>{removeFromDeck(S,c);});}
  if(id==='ectoplasm'){S.gold=0;}
  if(id==='shadow_amulet'){S.maxhp=Math.max(1,S.maxhp-15);S.hp=Math.min(S.hp,S.maxhp);}
}
function addPotion(s,id){
  if(s.potions.length>=3){msg('Potion slots full!');return false;}
  s.potions.push(id||pick(POTION_KEYS));return true;
}
function addCardToDeck(s,c){s.deck.push(c);}
function removeFromDeck(s,c){const i=s.deck.indexOf(c);if(i>=0)s.deck.splice(i,1);}
function healOut(s,n){s.hp=Math.min(s.maxhp,s.hp+n);}
function loseHpOut(s,n){s.hp=Math.max(0,s.hp-n);if(s.hp<=0){gameOver(false);}}
function gainGold(s,n){if(!hasRelic('ectoplasm'))s.gold+=n;}
</script>
<script>
/* ============================ COMBAT ============================ */
function mkEnemy(id){
  const d=ENEMIES[id];
  return {def:d,hp:d.hp,maxhp:d.hp,block:0,str:0,weak:0,vuln:0,bleed:0,burn:0,poison:0,regen:0,intangible:0,step:0,phase2:false,dead:false,uid:Math.random()};
}
function startCombat(groupIds,opts){
  opts=opts||{};
  C={enemies:groupIds.map(g=>mkEnemy(g.id||g)),draw:[],discard:[],exhaust:[],hand:[],powers:[],energy:3,
     turn:0,p:{block:0,str:0,dex:0,weak:0,vuln:0,bleed:0,burn:0,poison:0,regen:0,intangible:0},
     pending:null,targeting:false,over:false,rewards:opts,log:[],nextTurn:{blk:0,dex:0,energy:0},
     daggerUsed:false,tempStr:0};
  shuffle(S.deck.slice()).forEach(c=>C.draw.push(c));
  msg('Combat begins!');
  if(hasRelic('shadow_cloak')){C.p.intangible+=1;msg('Shadow Cloak: 1 Intangible.');}
  if(hasRelic('cursed_amulet')){aliveEnemies().forEach(e=>e.weak+=1);msg('Cursed Amulet: Weak to all enemies.');}
  if(hasRelic('demon_horns')){addCardToHand(makeCard(Math.random()<0.5?'imp_claw':'imp_claw_up'));msg('Demon Horns: Imp Claw added.');}
  if(hasRelic('philosophers')){aliveEnemies().forEach(e=>e.str+=1);}
  if(hasRelic('black_blood')){const d=Math.floor(S.maxhp*0.1);S.hp-=d;msg('Black Blood: lose '+d+' HP.');if(S.hp<=0){gameOver(false);return;}}
  if(S.nextCombatEnergy){C.energy+=S.nextCombatEnergy;msg('Studied knowledge: +'+S.nextCombatEnergy+' Energy.');S.nextCombatEnergy=0;}
  renderCombat();
  startPlayerTurn();
}
function aliveEnemies(){return C.enemies.filter(e=>!e.dead);}
function addCardToHand(c){C.hand.push(c);renderCombat();}
function enemyStep(e){
  const steps=e.phase2?e.def.phase.steps:e.def.steps;
  return steps[e.step%steps.length];
}
function intentLabel(e){
  if(e.dead)return '';
  const s=enemyStep(e);
  let parts=[];
  const str=e.str;
  if(s.t==='atk'||s.atk){const v=(s.v||s.atk)+(s.t==='atk'?str:0);parts.push(s.n?'Attack '+v+'×'+s.n:'Attack '+v);}
  else{
    if(s.blk)parts.push('Defend '+s.blk);
    if(s.heal)parts.push('Heal');
    if(s.str||s.strAll||s.regen||s.intangible)parts.push('Buff');
    if(s.weak||s.vuln||s.bleed||s.poison||s.burn)parts.push('Debuff');
    if(s.t==='sum')parts.push('Summon');
    if(!parts.length)parts.push('???');
  }
  return '🎯 '+parts.join(' & ');
}
function calcAtk(base,target){
  let d=base+C.p.str;
  if(C.p.weak>0)d=Math.floor(d*0.75);
  if(target&&target.vuln>0)d=Math.floor(d*1.5);
  if(target&&target.intangible>0)d=1;
  return Math.max(0,d);
}
function dmgEnemy(e,base,fromCard){
  if(e.dead)return;
  let d=calcAtk(base,e);
  const b=e.block;dmgAnim(document.querySelector('[data-uid="'+e.uid+'"]'),'-'+d,'#ff8080');
  if(b>0){const abs=Math.min(b,d);e.block-=abs;d-=abs;}
  e.hp-=d;
  if(hasRelic('black_blood')&&fromCard){}
  if(e.hp<=0){e.hp=0;e.dead=true;msg(e.def.name+' dies!');}
}
function playerTakeDamage(amount,srcEl){
  if(C.p.intangible>0)amount=1;
  else if(C.p.vuln>0)amount=Math.floor(amount*1.5);
  amount=Math.max(0,amount);
  const b=C.p.block;
  if(b>0){const abs=Math.min(b,amount);C.p.block-=abs;amount-=abs;}
  if(amount>0){S.hp-=amount;dmgAnim(document.getElementById('pzone'),'-'+amount,'#ff6060');document.getElementById('pzone').classList.add('shake');setTimeout(()=>{const z=document.getElementById('pzone');if(z)z.classList.remove('shake');},350);}
  renderTopbar();renderCombatStats();
  if(S.hp<=0){S.hp=0;endCombatLoss();}
  return amount;
}
function playerHpLoss(n,fromCard){
  if(C.p.intangible>0)n=1;
  S.hp-=n;
  if(fromCard){
    if(hasRelic('bloodstone')){healOut(S,1);}
    const bp=C.powers.find(p=>p.id==='r_blood_pact');
    if(bp){const amt=bp.upgraded?2:1;C.p.str+=amt;msg('Blood Pact: +'+amt+' Strength.');}
  }
  renderTopbar();
  if(S.hp<=0){S.hp=0;endCombatLoss();}
}
function healPlayer(n){healOut(S,n);renderTopbar();}
function drawCards(n){
  for(let i=0;i<n;i++){
    if(!C.draw.length){
      if(!C.discard.length)break;
      C.draw=shuffle(C.discard.splice(0));
    }
    C.hand.push(C.draw.pop());
  }
  renderCombat();
}
function startPlayerTurn(){
  if(C.over)return;
  C.turn++;
  C.p.block=0;
  if(C.tempStr){C.p.str-=C.tempStr;C.tempStr=0;}
  // duration ticks
  if(C.p.intangible>0)C.p.intangible--;
  if(C.p.bleed>0){const d=C.p.bleed;msg('Bleed: you lose '+d+' HP.');playerHpLoss(d,false);C.p.bleed=Math.floor(d/2);if(C.over)return;}
  if(C.p.poison>0){msg('Poison: you lose '+C.p.poison+' HP.');playerHpLoss(C.p.poison,false);C.p.poison--;if(C.over)return;}
  if(C.p.regen>0){healPlayer(C.p.regen);C.p.regen--;}
  let energy=3;
  if(S.relics.includes('cursed_crown'))energy++;
  if(C.nextTurn.energy){energy+=C.nextTurn.energy;C.nextTurn.energy=0;}
  const ss=C.powers.find(p=>p.id==='r_soul_siphon');
  if(ss){energy++;playerHpLoss(1,true);}
  if(hasRelic('hourglass')&&C.hand.length===0)energy++;
  if(hasRelic('shadow_amulet'))C.p.intangible+=1;
  if(hasRelic('bleeding_edge')&&aliveEnemies().length){pick(aliveEnemies()).bleed+=1;}
  const um=C.powers.find(p=>p.id==='s_umbral');
  if(um){aliveEnemies().forEach(e=>e.weak+=(um.upgraded?2:1));}
  const ha=C.powers.find(p=>p.id==='h_aura');
  if(ha){const d=ha.upgraded?5:3;aliveEnemies().slice().forEach(e=>{e.block=0;dmgEnemy(e,d,true);});msg('Hellfire Aura burns all enemies for '+d+'.');if(checkCombatEnd())return;}
  if(C.nextTurn.dex){C.p.dex+=C.nextTurn.dex;C.nextTurn.dex=0;}
  C.energy=energy;
  drawCards(5);
  renderCombat();
}
function renderCombatStats(){if(C&&!C.over&&document.getElementById('hand'))renderCombat();}
function gainBlock(n){
  n+=C.p.dex;if(n<0)n=0;
  C.p.block+=n;renderCombatStats();
}
function applyBurn(e,n){
  e.burn+=n;
  if(hasRelic('soul_ember')){dmgEnemy(e,2,true);}
}
function playCard(card,targetIdx){
  const d=cardDef(card);
  if(d.unplayable)return;
  let cost=cardCost(card);
  if(cost==='X'){cost=C.energy;}
  if(cost>C.energy)return;
  if(cost>0)C.energy-=cost;
  const fx=cardFx(card).slice();
  // Sacrificial Dagger
  if(hasRelic('sacrificial_dagger')&&!C.daggerUsed&&d.type==='Attack'&&fx.some(f=>f.t==='hploss')){
    C.daggerUsed=true;
    fx.forEach(f=>{if(f.t==='hploss')f.v=Math.max(0,f.v-1);});
    msg('Sacrificial Dagger reduces HP cost.');
  }
  const needsTarget=f=>['one'].includes(f.target);
  let target=null;
  if(targetIdx!==undefined&&targetIdx!==null)target=C.enemies[targetIdx];
  const alive=aliveEnemies();
  if(!target&&alive.length)target=alive[0];
  let played=false;
  if(d.custom==='vital_surge'){
    gainBlock((S.maxhp-S.hp)+(card.upgraded?3:0));
    played=true;
  }
  for(const f of fx){
    switch(f.t){
      case 'xcost':break;
      case 'dmg':{
        const hits=f.n||1;
        for(let i=0;i<hits;i++){
          const t=(f.target==='all')?aliveEnemies().slice():(f.target==='rand'?[pick(aliveEnemies())]:[target]);
          t.filter(x=>x&&!x.dead).forEach(en=>{
            dmgEnemy(en,f.v,true);
          });
        }
        played=true;break;
      }
      case 'blk':gainBlock(f.v);played=true;break;
      case 'heal':healPlayer(f.v);break;
      case 'hploss':msg('You lose '+f.v+' HP.');playerHpLoss(f.v,true);break;
      case 'bleed':applyDebuff(f,e=>e.bleed+=f.v);break;
      case 'weak':applyDebuff(f,e=>e.weak+=f.v);break;
      case 'vuln':applyDebuff(f,e=>e.vuln+=f.v);break;
      case 'burn':applyDebuff(f,e=>applyBurn(e,f.v));break;
      case 'poison':applyDebuff(f,e=>e.poison+=f.v);break;
      case 'draw':drawCards(f.v||1);break;
      case 'energy':C.energy+=f.v;break;
      case 'regen':C.p.regen+=f.v;break;
      case 'intangible':C.p.intangible+=f.v;break;
      case 'dex':C.p.dex+=f.v;break;
      case 'str':C.p.str+=f.v;if(f.temp)C.tempStr+=f.v;break;
      case 'strdown':if(target&&!target.dead){target.str=Math.max(0,target.str-f.v);}break;
      case 'nextblk':C.nextTurn.blk+=f.v;break;
      case 'nextdex':C.nextTurn.dex+=f.v;break;
      case 'nextenergy':C.nextTurn.energy+=f.v;break;
      case 'addcard':{
        const c=makeCard(f.id);
        if(f.to==='hand')addCardToHand(c);
        else if(f.to==='discard')C.discard.push(c);
        else C.draw.splice(rnd(C.draw.length+1),0,c);
        break;
      }
    }
    if(C.over)return;
  }
  function applyDebuff(f,fn){
    let ts=(f.target==='all')?aliveEnemies().slice():(f.target==='rand'?(aliveEnemies().length?[pick(aliveEnemies())]:[]):[target]);
    ts.filter(x=>x&&!x.dead).forEach(fn);
  }
  if(d.type!=='Power'){
    if(d.exhaust||(cardDef(card).exhaust))C.exhaust.push(card);
    else C.discard.push(card);
  }else{
    C.powers.push(card);
    if(hasRelic('tome'))drawCards(1);
  }
  C.hand=C.hand.filter(c=>c!==card);
  if(d.custom==='nightmare'&&target&&!target.dead&&target.weak>0){
    const v=card.upgraded?9:7;
    dmgEnemy(target,v,true);
  }
  if(d.type==='Attack'&&hasRelic('black_blood'))healPlayer(1);
  msg('You play '+cardName(card)+'.');
  checkCombatEnd();
  if(!C.over)renderCombat();
}
// custom card: Vital Surge (X cost block = missing hp)
function vitalSurge(){const miss=S.maxhp-S.hp;gainBlock(miss+(C.playingUpgraded?3:0));}

function endTurn(){
  if(C.over)return;
  C.pending=null;C.targeting=false;
  // Doubt curse
  C.hand.forEach(c=>{if(c.id==='doubt'){msg('Doubt gnaws at you.');playerHpLoss(1,false);}});
  if(C.over)return;
  // discard hand (ethereal/exhaust -> exhaust)
  C.hand.forEach(c=>{const d=cardDef(c);if(d.ethereal||d.exhaust)C.exhaust.push(c);else C.discard.push(c);});
  C.hand=[];
  // player burn
  if(C.p.burn>0){msg('Burn: you lose '+C.p.burn+' HP.');playerHpLoss(C.p.burn,false);C.p.burn=Math.floor(C.p.burn/2);if(C.over)return;}
  enemyTurn();
}
function enemyTurn(){
  for(const e of aliveEnemies()){
    if(C.over)return;
    // start of enemy turn ticks
    if(e.bleed>0){const d=e.bleed;msg(e.def.name+' bleeds for '+d+'.');e.block=0;rawDmgEnemy(e,d);if(e.dead)continue;e.bleed=Math.floor(d/2);}
    if(e.poison>0){msg(e.def.name+' suffers '+e.poison+' poison.');rawDmgEnemy(e,e.poison);e.poison--;if(e.dead)continue;}
    if(e.regen>0){e.hp=Math.min(e.maxhp,e.hp+e.regen);e.regen--;}
    if(e.weak>0)e.weak--;
    if(e.vuln>0)e.vuln--;
    if(e.intangible>0)e.intangible--;
    // boss phase
    const ph=e.def.phase;
    if(ph&&!e.phase2&&e.hp<=ph.hp){e.phase2=true;e.step=0;msg(e.def.name+' ENRAGES!');}
    if(ph&&e.phase2&&ph.strPerTurn)e.str+=ph.strPerTurn;
    const s=enemyStep(e);
    execEnemyStep(e,s);
    e.step++;
    if(C.over)return;
    if(checkCombatEnd())return;
  }
  startPlayerTurn();
}
function rawDmgEnemy(e,n){ // true damage (bleed/poison), intangible applies
  if(e.intangible>0)n=1;
  e.hp-=n;dmgAnim(document.querySelector('[data-uid="'+e.uid+'"]'),'-'+n,'#ffb060');
  if(e.hp<=0){e.hp=0;e.dead=true;msg(e.def.name+' dies!');}
}
function execEnemyStep(e,s){
  const el=document.querySelector('[data-uid="'+e.uid+'"]');
  if(s.t==='atk'||s.atk){
    const hits=s.n||1;const base=(s.v||s.atk)+e.str;
    for(let i=0;i<hits;i++){
      if(e.dead||C.over)return;
      msg(e.def.name+' attacks for '+base+'.');
      playerTakeDamage(base,el);
    }
  }
  if(s.blk){e.block+=s.blk;msg(e.def.name+' gains '+s.blk+' Block.');}
  if(s.heal){e.hp=Math.min(e.maxhp,e.hp+s.heal);msg(e.def.name+' heals '+s.heal+'.');}
  if(s.weak){C.p.weak+=s.weak;msg(e.def.name+' applies '+s.weak+' Weak to you.');}
  if(s.vuln){if(s.selfvuln){e.vuln+=s.vuln;}else{C.p.vuln+=s.vuln;msg(e.def.name+' applies '+s.vuln+' Vulnerable to you.');}}
  if(s.bleed){C.p.bleed+=s.bleed;msg(e.def.name+' applies '+s.bleed+' Bleed to you.');}
  if(s.poison){C.p.poison+=s.poison;msg(e.def.name+' applies '+s.poison+' Poison to you.');}
  if(s.burn){C.p.burn+=s.burn;msg(e.def.name+' sets you ablaze ('+s.burn+' Burn).');}
  if(s.str){e.str+=s.str;msg(e.def.name+' gains '+s.str+' Strength.');}
  if(s.strAll){aliveEnemies().forEach(x=>x.str+=s.strAll);msg('All enemies gain '+s.strAll+' Strength.');}
  if(s.regen){e.regen+=s.regen;msg(e.def.name+' gains Regeneration.');}
  if(s.intangible){e.intangible+=s.intangible;msg(e.def.name+' becomes Intangible.');}
  if(s.t==='sum'){
    const minions=C.enemies.filter(x=>!x.dead&&x.def.minion).length;
    const same=C.enemies.filter(x=>!x.dead&&x.def.id===s.id).length;
    if(minions<s.max&&same<s.max){
      const m=mkEnemy(s.id);C.enemies.push(m);msg(e.def.name+' summons '+m.def.name+'!');
    }else{
      msg(e.def.name+' cannot summon — attacks instead!');
      playerTakeDamage(6+e.str,el);
    }
  }
  renderCombat();
}
function checkCombatEnd(){
  if(C.over)return true;
  if(S.hp<=0){endCombatLoss();return true;}
  if(aliveEnemies().length===0){endCombatWin();return true;}
  return false;
}
function endCombatLoss(){
  if(C.over)return;C.over=true;
  gameOver(false);
}
function endCombatWin(){
  if(C.over)return;C.over=true;
  msg('Victory!');
  if(hasRelic('ashen_armor'))healOut(S,6);
  const r=C.rewards||{};
  setTimeout(()=>showRewards(r),600);
}
function dmgAnim(el,txt,color){
  if(el)floatText(el,txt,color);
  renderCombat();
}
/* ============================ UI ============================ */
const NODE_INFO={enemy:{icon:'⚔️',label:'Enemy'},event:{icon:'❓',label:'Event'},shop:{icon:'🛒',label:'Shop'},
  rest:{icon:'🔥',label:'Campfire'},boss:{icon:'👑',label:'BOSS'}};
function renderTopbar(){
  const pct=Math.max(0,S.hp/S.maxhp*100);
  let relics=S.relics.map(id=>'<span class="relic" data-rel="'+id+'">'+RELICS[id].icon+'</span>').join('');
  app.innerHTML=`<div id="topbar">
    <div><span class="stat">${CLASSES[S.cls].icon} <b>${CLASSES[S.cls].name}</b></span>
    <span class="stat">❤️<span class="hpbar"><span class="hpfill" style="width:${pct}%"></span></span>${S.hp}/${S.maxhp}</span>
    <span class="stat gold">💰 ${S.gold}</span>
    <span class="stat">${ACT_NAMES[S.act]} — Floor ${S.floor}</span></div>
    <div><span id="relics">${relics}</span>
    <button onclick="viewDeck()">Deck (${S.deck.length})</button>
    <span id="potionslots">${S.potions.map((p,i)=>`<span class="pslot" data-pot="${i}" title="${POTIONS[p].name}: ${POTIONS[p].desc}">${POTIONS[p].icon}</span>`).join('')}</span></div>
  </div><div class="screen" id="screen"></div>`;
  document.querySelectorAll('.relic').forEach(el=>{
    const r=RELICS[el.dataset.rel];tooltipAttach(el,'<b>'+r.name+'</b><br>'+r.desc);});
  document.querySelectorAll('.pslot').forEach(el=>{
    el.addEventListener('click',()=>usePotion(+el.dataset.pot));});
}
function viewDeck(){
  showOverlay('<h2>Your Deck ('+S.deck.length+')</h2><div class="reward-row">'+
    S.deck.map(c=>cardHTML(c,true)).join('')+'</div>'+
    '<div style="text-align:center;margin-top:12px"><button onclick="hideOverlay()">Close</button></div>');
}
function cardHTML(c,noClick){
  const d=cardDef(c);
  return `<div class="card ${cardTypeClass(c)}${d.unplayable?' unplayable':''}" data-cid="${c.id}" data-up="${c.upgraded?1:0}">
    <div class="cost">${cardCost(c)}</div>
    <div class="cname">${esc(cardName(c))}</div>
    <div class="ctype">${d.type}${c.upgraded?' <span class="up">+</span>':''}</div>
    <div class="ctext">${esc(cardText(c))}</div></div>`;
}
/* ---- Class select ---- */
function renderClassSelect(){
  app.innerHTML=`<div style="text-align:center;padding-top:40px">
    <h1 style="font-size:42px">Shadows of the Abyss</h1>
    <p style="color:var(--dim);margin:10px 0 30px">Choose your cursed archetype</p>
    <div class="class-row">${Object.keys(CLASSES).map(k=>{
      const c=CLASSES[k];
      return `<div class="class-card" data-cls="${k}">
        <div class="class-icon">${c.icon}</div><h3>${c.name}</h3><p>${c.desc}</p>
        <p style="margin-top:14px;color:#9a90b8;font-size:12px">Signature: ${CARDS[c.sig].name}</p></div>`;
    }).join('')}</div></div>`;
  document.querySelectorAll('.class-card').forEach(el=>el.addEventListener('click',()=>newRun(el.dataset.cls)));
}
/* ---- Map ---- */
function renderMap(){
  renderTopbar();
  const scr=document.getElementById('screen');
  scr.innerHTML='<h2 style="text-align:center;margin-top:8px">'+ACT_NAMES[S.act]+'</h2><div id="map"></div>';
  const mapEl=document.getElementById('map');
  S.map.forEach(row=>{
    if(!row)return;
    const fl=document.createElement('div');fl.className='mapfloor';
    row.nodes.forEach(n=>{
      const done=n.done, avail=(row.f===S.floor);
      const info=n.t==='enemy'?(n.elite?{icon:'💀',label:'Elite'}:{icon:'⚔️',label:'Enemy'}):NODE_INFO[n.t];
      const d=document.createElement('div');
      d.className='mnode'+(done?' done':avail&&n!==undefined?' available':'');
      d.innerHTML=`<div class="dot">${info.icon}</div><div class="lbl">${info.label}</div>`;
      if(avail&&!done)d.querySelector('.dot').addEventListener('click',()=>enterNode(n));
      fl.appendChild(d);
    });
    mapEl.appendChild(fl);
  });
}
function enterNode(n){
  n.done=true;
  n._floor=S.floor;
  if(n.t==='enemy'){
    startCombat(n.group.map(id=>({id})),{node:n});
  }else if(n.t==='rest'){renderRest(n);}
  else if(n.t==='shop'){renderShop(n);}
  else if(n.t==='event'){renderEvent(n);}
  else if(n.t==='boss'){startCombat(n.group.map(id=>({id})),{node:n,boss:true});}
}
function finishNode(node){
  hideOverlay();
  if(node && node.t==='boss'){
    if(S.act>=3){gameOver(true);return;}
    S.act++;buildMap();
  }else{
    S.floor++;
  }
  renderMap();
}
/* ---- Combat screen ---- */
function renderCombat(){
  if(!C)return;
  renderTopbar();
  const scr=document.getElementById('screen');
  scr.innerHTML=`<div id="combat">
    <div id="enemies"></div>
    <div id="log"></div>
    <div id="playerzone" >
      <div id="pstats"></div>
      <div id="hand"></div>
      <div id="energyorb">${C.energy}<small>/${maxEnergy()} ENERGY</small></div>
    </div>
    <div id="combat-controls">
      <button id="endturn">End Turn</button>
      ${C.pending?'<span style="color:var(--gold)">Choose a target…</span>':''}
    </div>
  </div>`;
  const enWrap=document.getElementById('enemies');
  C.enemies.forEach((e,i)=>{
    if(e.dead&&e.hp<=0){const dd=document.createElement('div');dd.className='enemy dead';enWrap.appendChild(dd);return;}
    const el=document.createElement('div');
    el.className='enemy'+(C.targeting?' targetable':'');
    el.dataset.uid=e.uid;
    const st=[];
    if(e.str)st.push(`<span class="st str">💪${e.str}</span>`);
    if(e.block)st.push(`<span class="st">🛡️${e.block}</span>`);
    if(e.weak)st.push(`<span class="st debuff">😵${e.weak}</span>`);
    if(e.vuln)st.push(`<span class="st debuff">💔${e.vuln}</span>`);
    if(e.bleed)st.push(`<span class="st debuff">🩸${e.bleed}</span>`);
    if(e.burn)st.push(`<span class="st debuff">🔥${e.burn}</span>`);
    if(e.poison)st.push(`<span class="st debuff">☠️${e.poison}</span>`);
    if(e.regen)st.push(`<span class="st buff">💚${e.regen}</span>`);
    if(e.intangible)st.push(`<span class="st buff">✨${e.intangible}</span>`);
    el.innerHTML=`<div class="intent">${intentLabel(e)}</div>
      <div class="ebody">${e.def.icon}</div>
      <div><b>${e.def.name}</b></div>
      <div class="ehp"><div class="ehpfill" style="width:${e.hp/e.maxhp*100}%"></div></div>
      <div style="font-size:12px">${e.hp}/${e.maxhp}</div>
      <div class="statusrow">${st.join('')}</div>`;
    el.addEventListener('click',()=>{
      if(C.targeting&&C.pending){playCard(C.pending,i);C.pending=null;C.targeting=false;}
    });
    enWrap.appendChild(el);
  });
  // player stats
  const p=C.p;const pst=[];
  if(p.block)pst.push(`<span class="st">🛡️${p.block}</span>`);
  if(p.str)pst.push(`<span class="st str">💪${p.str}</span>`);
  if(p.dex)pst.push(`<span class="st str">🌀${p.dex}</span>`);
  if(p.weak)pst.push(`<span class="st debuff">😵${p.weak}</span>`);
  if(p.vuln)pst.push(`<span class="st debuff">💔${p.vuln}</span>`);
  if(p.bleed)pst.push(`<span class="st debuff">🩸${p.bleed}</span>`);
  if(p.burn)pst.push(`<span class="st debuff">🔥${p.burn}</span>`);
  if(p.poison)pst.push(`<span class="st debuff">☠️${p.poison}</span>`);
  if(p.regen)pst.push(`<span class="st buff">💚${p.regen}</span>`);
  if(p.intangible)pst.push(`<span class="st buff">✨${p.intangible}</span>`);
  document.getElementById('pstats').innerHTML=
    `<div><b>You</b> — ${S.hp}/${S.maxhp} HP ${pst.join('')}</div>
     <div id="deckinfo">Draw ${C.draw.length} · Discard ${C.discard.length} · Exhaust ${C.exhaust.length}${C.powers.length?' · Powers: '+C.powers.map(x=>cardName(x)).join(', '):''}</div>`;
  document.getElementById('playerzone').id='pzone';
  const hand=document.getElementById('hand');
  C.hand.forEach(c=>{
    const d=cardDef(c);
    const elw=document.createElement('div');
    elw.innerHTML=cardHTML(c);
    const el=elw.firstChild;
    const cost=cardCost(c);
    const un=d.unplayable||(cost!=='X'&&cost>C.energy);
    if(un)el.classList.add('unplayable');
    el.addEventListener('click',()=>{
      if(d.unplayable||(!un===false))return;
      if(un)return;
      const needsTarget=cardFx(c).some(f=>['dmg','bleed','weak','vuln','burn','poison','strdown'].includes(f.t)&&!f.target)&&aliveEnemies().length>1;
      if(needsTarget){C.pending=c;C.targeting=true;renderCombat();}
      else playCard(c,null);
    });
    tooltipAttach(el,'<b>'+cardName(c)+'</b>');
    hand.appendChild(el);
  });
  document.getElementById('endturn').addEventListener('click',endTurn);
  const l=document.getElementById('log');l.innerHTML=C.log.slice(-4).map(x=>'<div>'+esc(x)+'</div>').join('');
}
function maxEnergy(){return 3+(S.relics.includes('cursed_crown')?1:0);}
/* ---- Potions ---- */
function usePotion(i){
  const id=S.potions[i];if(!id)return;
  const inCombat=C&&!C.over;
  const consume=()=>{S.potions.splice(i,1);renderTopbar();if(C&&!C.over)renderCombat();};
  switch(id){
    case 'blood_vial':healOut(S,15);msg('Blood Vial: +15 HP.');consume();break;
    case 'firebomb':if(!inCombat)return alertMsg('Only in combat.');aliveEnemies().slice().forEach(e=>{e.block=0;dmgEnemy(e,10,false);});msg('Firebomb!');consume();checkCombatEnd();renderCombat();break;
    case 'dex_tonic':if(!inCombat)return alertMsg('Only in combat.');C.p.dex+=3;C.nextTurn.dex-=3;msg('+3 Dexterity.');consume();break;
    case 'energy_elixir':if(!inCombat)return alertMsg('Only in combat.');C.energy+=2;msg('+2 Energy.');consume();break;
    case 'cursed_brew':if(!inCombat)return alertMsg('Only in combat.');aliveEnemies().forEach(e=>e.vuln+=3);msg('All enemies Vulnerable!');consume();break;
    case 'bleeding_flask':{
      if(!inCombat)return alertMsg('Only in combat.');
      if(aliveEnemies().length>1){pickTarget(t=>{t.bleed+=6;consume();});}
      else{aliveEnemies()[0].bleed+=6;consume();}
      break;}
    case 'imp_bottle':if(!inCombat)return alertMsg('Only in combat.');addCardToHand(makeCard('imp_claw_up'));consume();break;
    case 'smoke_bomb':{
      if(!inCombat||C.rewards.node.t==='boss')return alertMsg('Only in non-boss combat.');
      msg('You vanish in smoke...');
      C.over=true;consume();
      setTimeout(()=>finishNode(C.rewards.node),400);break;}
  }
  renderTopbar();
}
function alertMsg(t){flashMsg(t);}
function flashMsg(t){showOverlay('<h2>'+t+'</h2><div style="text-align:center"><button onclick="hideOverlay()">OK</button></div>');}
let _targetCb=null;
function pickTarget(cb){
  _targetCb=(e)=>{_targetCb=null;cb(e);};
  C.targeting=true;
  document.querySelectorAll('.enemy').forEach(el=>el.classList.add('targetable'));
  document.querySelectorAll('.enemy').forEach(el=>{
    el.onclick=()=>{const e=C.enemies.find(x=>x.uid==el.dataset.uid);if(_targetCb&&e&&!e.dead)_targetCb(e);renderCombat();};
  });
}
/* ---- Rewards ---- */
function showRewards(r){
  const node=r.node;
  let gold=r.bonusGold||(node.t==='boss'?50:node.elite?25+rnd(11):10+rnd(11));
  if(hasRelic('greedy_imp'))gold+=10;
  gainGold(S,gold);
  let potLine='';
  if(node.t==='boss'){addPotion(S);addPotion(S);potLine='🍾 The boss drops 2 potions!';}
  else{
    const ch=node.elite?0.7:0.4;
    if(Math.random()<ch&&addPotion(S)){potLine='🍾 You found a potion: <b>'+POTIONS[S.potions[S.potions.length-1]].name+'</b>';}
  }
  const cards=rollCardChoices(node);
  showOverlay(`<h2 style="text-align:center">⚔️ Victory!</h2>
    <p style="text-align:center">💰 You gain <b class="gold">${gold} Gold</b>. ${potLine}</p>
    <h3 style="text-align:center;margin-top:10px">Choose a card</h3>
    <div class="reward-row">${cards.map(c=>cardHTML(c)).join('')}</div>
    <div style="text-align:center"><button id="skipcard">Skip</button></div>`);
  overlayEl.querySelectorAll('.card').forEach((el,i)=>{
    el.addEventListener('click',()=>{S.deck.push(cards[i]);afterCardReward(node);});
  });
  document.getElementById('skipcard').addEventListener('click',()=>afterCardReward(node));
}
function rollCardChoices(node){
  const pool=CLASS_POOL[S.cls].slice();
  const out=[];
  for(let i=0;i<3;i++){
    let roll=rnd(100);
    let rarity=roll<(node.elite?20:10)?'Rare':roll<(node.elite?50:40)?'Uncommon':'Common';
    if(node.t==='boss'&&i===0)rarity='Rare';
    let cands=pool.filter(id=>CARDS[id].rarity===rarity);
    if(!cands.length)cands=pool;
    const id=pick(cands);
    out.push(makeCard(id));
    pool.splice(pool.indexOf(id),1);
  }
  return out;
}
function afterCardReward(node){
  if(node.t==='boss'){
    S.hp=S.maxhp;
    const keys=shuffle(Object.keys(BOSS_RELICS)).slice(0,3);
    showOverlay('<h2 style="text-align:center">👑 Boss Relic — choose one</h2>'+
      '<div class="reward-row">'+keys.map(k=>{
        const b=BOSS_RELICS[k];
        return `<div class="panel shopitem" data-br="${k}" style="cursor:pointer;text-align:center">
          <div style="font-size:44px">${b.icon}</div><b>${b.name}</b>
          <p style="font-size:12px;color:var(--dim);margin-top:6px">${b.desc}</p></div>`;
      }).join('')+'</div>');
    overlayEl.querySelectorAll('[data-br]').forEach(el=>el.addEventListener('click',()=>{
      giveRelic2(el.dataset.br);finishNode(node);
    }));
  }else finishNode(node);
}
function giveRelic2(id){
  S.relics.push(id);
  if(id==='ectoplasm')S.gold=0;
  if(id==='shadow_amulet'){S.maxhp=Math.max(1,S.maxhp-15);S.hp=Math.min(S.hp,S.maxhp);}
  msg('Obtained '+BOSS_RELICS[id].name+'.');
}
/* ---- Rest ---- */
function renderRest(node){
  renderTopbar();
  document.getElementById('screen').innerHTML=`<div class="panel" style="max-width:600px;margin:30px auto;text-align:center">
    <h2>🔥 Campfire</h2><p style="color:var(--dim)">The flames ward off the darkness. What will you do?</p>
    <button class="choice-btn" id="r_rest">Rest — Heal ${Math.floor(S.maxhp*0.25)} HP (currently ${S.hp}/${S.maxhp})</button>
    <button class="choice-btn" id="r_forge">Forge — Upgrade a card</button>
    <button class="choice-btn" id="r_med">Meditate — Remove a card from your deck</button></div>`;
  document.getElementById('r_rest').addEventListener('click',()=>{healOut(S,Math.floor(S.maxhp*0.25));msgDone('You feel restored.');});
  document.getElementById('r_forge').addEventListener('click',()=>{
    const ups=S.deck.filter(c=>!c.upgraded&&CARDS[c.id].upText);
    if(!ups.length)return flashMsg('No cards can be upgraded.');
    pickCardFromDeck(S,'Choose a card to upgrade',c=>{c.upgraded=true;msgDone(cardName(c)+' upgraded!');},ups);
  });
  document.getElementById('r_med').addEventListener('click',()=>{
    pickCardFromDeck(S,'Choose a card to remove',c=>{removeFromDeck(S,c);msgDone('You cast away '+cardName(c)+'.');});
  });
  function msgDone(t){
    document.getElementById('screen').innerHTML=`<div class="panel" style="max-width:500px;margin:60px auto;text-align:center"><h2>🔥 ${t}</h2><button onclick="finishNode(null)">Continue</button></div>`;
  }
}
function pickCardFromDeck(s,title,cb,filter){
  const list=filter||s.deck;
  showOverlay('<h2>'+title+'</h2><div class="reward-row">'+list.map(c=>cardHTML(c)).join('')+
    '<div style="width:100%;text-align:center"><button id="pcancel">Cancel</button></div></div>');
  overlayEl.querySelectorAll('.card').forEach((el,i)=>{
    el.addEventListener('click',()=>{hideOverlay();cb(list[i]);});
  });
  document.getElementById('pcancel').addEventListener('click',hideOverlay);
}
/* ---- Shop ---- */
function renderShop(node){
  renderTopbar();
  if(!node.stock){
    const pool=CLASS_POOL[S.cls].slice();
    const cards=[];
    for(let i=0;i<3;i++){
      const roll=rnd(100);
      const rarity=roll<10?'Rare':roll<40?'Uncommon':'Common';
      let cands=pool.filter(id=>CARDS[id].rarity===rarity);
      if(!cands.length)cands=pool;
      const id=pick(cands);cards.push({c:makeCard(id),price:CARDS[id].rarity==='Rare'?150:CARDS[id].rarity==='Uncommon'?75:50});
      pool.splice(pool.indexOf(id),1);
    }
    node.stock={
      cards,
      colorless:{c:makeCard(pick(COLORLESS_POOL)),price:60},
      relics:shuffle(Object.keys(RELICS).filter(id=>(!RELICS[id].cls||RELICS[id].cls===S.cls)&&!hasRelic(id)).slice(0,20)).slice(0,2).map(id=>({id,price:150,sold:false})),
      potions:[pick(POTION_KEYS),pick(POTION_KEYS),pick(POTION_KEYS)].map(id=>({id,price:50,sold:false})),
      removalUsed:false};
  }
  const st=node.stock;
  const scr=document.getElementById('screen');
  scr.innerHTML=`<div class="panel" style="max-width:1000px;margin:10px auto">
    <h2>🛒 The Merchant of Shadows <span class="gold" style="float:right">💰 ${S.gold}</span></h2>
    <h3>Class Cards</h3><div class="reward-row" id="shopcards"></div>
    <h3>Colorless</h3><div class="reward-row" id="shopcol"></div>
    <h3>Relics</h3><div class="reward-row" id="shoprel"></div>
    <h3>Potions</h3><div class="reward-row" id="shoppot"></div>
    <div style="text-align:center;margin-top:10px"><button id="shopremove">Remove a card — 75g ${st.removalUsed?'(used)':''}</button>
    <button onclick="finishNode(null)" style="margin-left:10px">Leave</button></div></div>`;
  function drawShop(){
    document.getElementById('shopcards').innerHTML=st.cards.map((it,i)=>{
      if(it.sold)return '';
      return `<div class="shopitem">${cardHTML(it.c,true)}<div class="price">💰 ${it.price}</div></div>`;}).join('');
    document.getElementById('shopcol').innerHTML=st.colorless.sold?'':`<div class="shopitem">${cardHTML(st.colorless.c,true)}<div class="price">💰 ${st.colorless.price}</div></div>`;
    document.getElementById('shoprel').innerHTML=st.relics.map((it,i)=>{
      if(it.sold)return '';
      const r=RELICS[it.id];
      return `<div class="shopitem panel" data-ri="${i}" style="cursor:pointer"><div style="font-size:36px">${r.icon}</div><b>${r.name}</b>
        <p style="font-size:11px;color:var(--dim)">${r.desc}</p><div class="price">💰 ${it.price}</div></div>`;}).join('');
    document.getElementById('shoppot').innerHTML=st.potions.map((it,i)=>{
      if(it.sold)return '';
      const p=POTIONS[it.id];
      return `<div class="shopitem panel" data-pi="${i}" style="cursor:pointer"><div style="font-size:32px">${p.icon}</div><b>${p.name}</b>
        <p style="font-size:11px;color:var(--dim)">${p.desc}</p><div class="price">💰 ${it.price}</div></div>`;}).join('');
    bind();
  }
  function buy(price,fn){
    if(S.gold<price)return flashMsg('Not enough Gold!');
    S.gold-=price;fn();renderTopbar();drawShop();
  }
  function bind(){
    overlayEl.querySelectorAll('.card').forEach(()=>{});
    scr.querySelectorAll('#shopcards .shopitem').forEach((el,i)=>{
      const vis=st.cards.filter(x=>!x.sold);
      const it=vis[i];
      el.querySelector('.card').addEventListener('click',()=>buy(it.price,()=>{it.sold=true;S.deck.push(it.c);}));
    });
    const col=scr.querySelector('#shopcol .shopitem');
    if(col)col.querySelector('.card').addEventListener('click',()=>buy(st.colorless.price,()=>{st.colorless.sold=true;S.deck.push(st.colorless.c);}));
    scr.querySelectorAll('[data-ri]').forEach(el=>el.addEventListener('click',()=>{
      const it=st.relics[+el.dataset.ri];
      buy(it.price,()=>{it.sold=true;giveRelic(it.id);renderTopbar();drawShop();});
    }));
    scr.querySelectorAll('[data-pi]').forEach(el=>el.addEventListener('click',()=>{
      const it=st.potions[+el.dataset.pi];
      buy(it.price,()=>{it.sold=true;if(addPotion(S,it.id)){}else S.gold+=it.price;renderTopbar();drawShop();});
    }));
    document.getElementById('shopremove').onclick=()=>{
      if(st.removalUsed)return flashMsg('Already used.');
      buy(75,()=>{st.removalUsed=true;pickCardFromDeck(S,'Remove a card',c=>{removeFromDeck(S,c);drawShop();});});
    };
  }
  drawShop();
}
/* ---- Events ---- */
function renderEvent(node){
  renderTopbar();
  const ev=pick(EVENTS);
  node.event=ev;
  document.getElementById('screen').innerHTML=`<div class="panel" style="max-width:640px;margin:40px auto;text-align:center">
    <div style="font-size:56px">${ev.icon}</div><h2>${ev.name}</h2>
    <p class="eventtext">${ev.text}</p>
    <div id="evchoices">${ev.choices.map((c,i)=>`<button class="choice-btn" data-i="${i}" ${c.can(S)?'':'disabled'}>${c.label}</button>`).join('')}</div></div>`;
  document.querySelectorAll('#evchoices button').forEach(btn=>{
    btn.addEventListener('click',()=>{
      const ch=ev.choices[+btn.dataset.i];
      hideOverlay();ch.do(S);
      if(document.getElementById('overlay')&&!overlayEl.classList.contains('hidden'))return; // waiting for card pick
      renderTopbar();
      document.getElementById('screen').innerHTML=`<div class="panel" style="max-width:500px;margin:60px auto;text-align:center"><h2>${ev.icon} ${ev.name}</h2><button onclick="finishNode(null)">Continue</button></div>`;
    });
  });
}
/* ---- End states ---- */
function gameOver(win){
  C&&(C.over=true);
  renderTopbar();
  document.getElementById('screen').innerHTML=`<div class="bigmsg">
    <h1 style="color:${win?'var(--gold)':'var(--red)'}">${win?'🏆 VICTORY!':'☠️ YOU DIED'}</h1>
    <p style="color:var(--dim);margin-bottom:24px">${win?'The Archdemon falls. The abyss releases its hold on your soul… for now.'
      :'The abyss claims another soul. Your legend ends here.'}</p>
    <button onclick="location.reload()">New Run</button></div>`;
  hideOverlay();
}
renderClassSelect();
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"aef04f25-08e9-43dc-81ab-225398fde3a1","tokensIn":3785957,"tokensOut":38069,"tokensTotal":3824026,"cost":0,"turns":52,"toolCalls":60,"failedToolCalls":5,"timestamp":"2026-08-21T18:27:40.328Z"} -->