Shadows
ox alpha · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 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,'<');}
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"} -->