Shadows
GPT 6 Luna · 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 58,258 bytes · SHA-256 4de2f9db2980
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Shadows of the Abyss</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600;700;800&family=Crimson+Pro:wght@400;500;600;700&display=swap');
:root{--ink:#100e13;--panel:#1b171d;--panel2:#241d25;--line:#51404a;--gold:#d6ad68;--gold2:#f0d39a;--red:#b84d4b;--muted:#a99ba0;--cream:#eee3d0;--green:#8ab28c;--violet:#9e7abc}
*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 50% 0%,#332630 0,#17131a 47%,#0c0b10 100%);color:var(--cream);font-family:'Crimson Pro',Georgia,serif;min-height:100vh}button{font:inherit;color:inherit;cursor:pointer}#app{min-height:100vh;max-width:1440px;margin:auto;padding:20px 30px 28px;position:relative}.topbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #4d3c43;padding:4px 0 14px;margin-bottom:18px}.brand{font:700 20px Cinzel,serif;letter-spacing:.12em;color:var(--gold2)}.brand small{display:block;font:500 11px 'Crimson Pro';letter-spacing:.28em;color:#9e878b;margin-top:2px}.top-actions{display:flex;gap:9px}.mini-btn{background:#241e24;border:1px solid #5b4747;border-radius:4px;padding:7px 12px;color:#c8b7a7;font-size:14px}.screen{animation:fade .35s ease}@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}.eyebrow{font:600 11px Cinzel,serif;letter-spacing:.24em;text-transform:uppercase;color:var(--gold)}h1,h2,h3{font-family:Cinzel,serif;margin:0}h1{font-size:clamp(33px,5vw,62px);line-height:1.1}h2{font-size:28px;color:var(--gold2)}p{line-height:1.45;color:#c6b9b2}.btn{background:linear-gradient(#644840,#3f2c2d);border:1px solid #ad8055;border-radius:4px;padding:11px 20px;font:600 13px Cinzel,serif;letter-spacing:.09em;color:#fff1d7;transition:.15s;box-shadow:0 4px 15px #0005}.btn:hover{filter:brightness(1.2);transform:translateY(-1px)}.btn.secondary{background:#29232a;border-color:#67525a;color:#dbc9bd}.btn.danger{background:#632f32;border-color:#a4504c}.title-screen{min-height:75vh;display:grid;place-items:center;text-align:center;position:relative;overflow:hidden}.title-screen:before{content:'✦';position:absolute;font-size:440px;line-height:1;color:#8c5c4c0b;top:3%;left:calc(50% - 220px);text-shadow:0 0 80px #bd80532c}.title-inner{position:relative;max-width:750px}.title-inner h1{color:#e9d3b3;text-shadow:0 4px 25px #9a574655}.title-inner .tag{font-size:21px;color:#b6a39a;margin:20px 0 35px}.class-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:30px auto 24px;max-width:900px}.class-card{padding:20px 16px 18px;min-height:190px;text-align:left;background:linear-gradient(145deg,#28212a,#17151a);border:1px solid #51414b;border-radius:5px;cursor:pointer;transition:.2s;position:relative;overflow:hidden}.class-card:hover,.class-card.selected{border-color:var(--gold);transform:translateY(-4px);box-shadow:0 8px 25px #0008}.class-card .sigil{font-size:35px;color:#cf7c75}.class-card h3{font-size:19px;margin:8px 0;color:#ebd9c2}.class-card p{font-size:16px;margin:0;color:#ac9fa1}.class-card:nth-child(2) .sigil{color:#a58bd1}.class-card:nth-child(3) .sigil{color:#e6a258}.muted{color:var(--muted)}.hud{display:flex;gap:10px;align-items:stretch;margin-bottom:15px}.stat{background:#1c181e;border:1px solid #4a3c43;border-radius:4px;padding:8px 14px;min-width:90px}.stat label{display:block;text-transform:uppercase;letter-spacing:.15em;font:10px Cinzel;color:#a99490}.stat strong{font:600 19px Cinzel;color:#ead8bd}.hpbar{width:120px;height:7px;background:#39272b;border-radius:5px;margin-top:5px;overflow:hidden}.hpbar i{display:block;background:linear-gradient(90deg,#9e343b,#dc6655);height:100%;transition:width .3s}.run-hud{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.run-info{display:flex;gap:8px;align-items:center}.badge{border:1px solid #695345;color:#d6b577;padding:5px 10px;border-radius:3px;font:11px Cinzel;letter-spacing:.08em}.layout{display:grid;grid-template-columns:minmax(220px,285px) 1fr;gap:16px}.sidebar,.main-panel{background:linear-gradient(150deg,#211b22eF,#151318f5);border:1px solid #44363e;border-radius:5px;box-shadow:0 12px 30px #0004}.sidebar{padding:15px}.sidebar h3{font-size:15px;color:#d9c5a6;letter-spacing:.08em;margin-bottom:12px}.deck-list{display:flex;flex-direction:column;gap:5px;max-height:370px;overflow:auto;padding-right:3px}.deckrow{display:flex;justify-content:space-between;padding:5px 7px;background:#ffffff06;border-bottom:1px solid #ffffff0c;color:#cbbdb6;font-size:15px}.deckrow small{color:#9c8a8a}.relic-row,.potion-row{display:flex;flex-wrap:wrap;gap:6px;margin:9px 0}.chip{background:#30252b;border:1px solid #67504d;border-radius:3px;padding:4px 7px;font-size:12px;color:#d8bd8c;cursor:help}.main-panel{padding:20px;min-height:490px;position:relative;overflow:hidden}.section-head{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:15px}.section-head p{margin:6px 0 0}.map-wrap{display:flex;flex-direction:column;gap:6px;max-width:880px;margin:10px auto}.map-row{display:flex;align-items:center;justify-content:center;gap:13px;min-height:32px}.map-node{width:39px;height:34px;border-radius:50%;background:#241e25;border:1px solid #69575b;color:#aa9893;font-size:16px;display:grid;place-items:center;position:relative}.map-node.available{cursor:pointer;border-color:#d2a661;color:#f1d39a;box-shadow:0 0 12px #dfad5225}.map-node.available:hover{transform:scale(1.12);background:#45322e}.map-node.current{background:#9a6546;border-color:#f0ca82;color:white}.map-node.done{opacity:.35}.map-node.boss{width:46px;height:42px;border-color:#b4474a;color:#ee8580}.map-row-label{width:54px;text-align:right;color:#75666b;font:10px Cinzel}.map-legend{display:flex;gap:12px;flex-wrap:wrap;color:#a99896;font-size:13px;margin:15px 0}.legend-item{display:flex;gap:5px;align-items:center}.legend-dot{width:10px;height:10px;border-radius:50%;background:#be8755}.map-note{color:#8c7d81;text-align:center;margin-top:8px}.combat-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:9px}.combat-arena{display:flex;justify-content:center;align-items:flex-end;gap:18px;min-height:230px;padding:10px 5px 18px;background:radial-gradient(ellipse at 50% 100%,#59343a44,transparent 64%);border-bottom:1px solid #514047}.enemy{width:min(190px,30%);text-align:center;position:relative;padding:5px 10px 0;transition:.2s;cursor:pointer}.enemy.target{filter:drop-shadow(0 0 12px #e19c4a99);transform:translateY(-4px)}.enemy.dead{opacity:.2;filter:grayscale(1)}.enemy-art{font-size:65px;line-height:1.1;text-shadow:0 10px 16px #000}.enemy-name{font:600 14px Cinzel;color:#e1c8ab;margin:3px 0}.health-track{height:8px;background:#40292d;border-radius:8px;overflow:hidden;border:1px solid #674347}.health-track span{height:100%;display:block;background:linear-gradient(90deg,#9d363e,#dc6457)}.enemy-stats{font-size:13px;color:#af9b99;margin-top:4px}.intent{display:inline-block;background:#292128;border:1px solid #5f4c4c;padding:4px 8px;border-radius:3px;margin-top:7px;color:#e3c17f;font-size:14px}.statusline{display:flex;justify-content:center;flex-wrap:wrap;gap:4px;min-height:20px;margin-top:4px}.status{font-size:11px;background:#483237;border-radius:3px;padding:2px 5px;color:#f0b5a5}.player-area{display:flex;justify-content:space-between;align-items:center;padding:10px 0 5px}.player-combat-stats{display:flex;gap:8px}.smallstat{background:#29232a;padding:5px 9px;border:1px solid #514149;border-radius:3px;font-size:14px;color:#cfbda8}.hand{display:flex;justify-content:center;gap:9px;min-height:185px;padding:7px 0 10px;overflow-x:auto}.playing-card{flex:0 0 124px;height:166px;position:relative;text-align:center;padding:22px 9px 9px;background:linear-gradient(150deg,#eee0c4,#c7b498);color:#282126;border:2px solid #876e5a;border-radius:6px;box-shadow:0 8px 14px #0008;cursor:pointer;transition:.15s;display:flex;flex-direction:column;justify-content:space-between}.playing-card:hover{transform:translateY(-12px);box-shadow:0 14px 24px #0009}.playing-card.unplayable{filter:grayscale(.6);opacity:.62;cursor:not-allowed}.playing-card .cost{position:absolute;top:-6px;left:-6px;width:29px;height:29px;border-radius:50%;background:#4a5262;border:2px solid #cbb98c;color:white;font:700 15px Cinzel;display:grid;place-items:center}.playing-card .cname{font:700 13px Cinzel;line-height:1.15}.playing-card .ctype{font:700 9px Cinzel;letter-spacing:.15em;text-transform:uppercase;color:#80614d}.playing-card .cdesc{font-size:14px;line-height:1.1}.playing-card .rarity{font-size:10px;color:#79645a;text-transform:uppercase;letter-spacing:.13em}.combat-bottom{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #46373e;padding-top:10px}.log{color:#ad9a98;font-size:14px;min-height:21px}.turn-buttons{display:flex;gap:8px}.reward-grid,.shop-grid,.choice-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(145px,1fr));gap:12px;margin:18px 0}.reward-card{min-height:180px;cursor:pointer}.reward-card:hover{transform:translateY(-5px);border-color:#d6ad68}.reward-card.picked{outline:2px solid #e5c17e}.choice-card{background:#292129;border:1px solid #59464b;border-radius:4px;padding:15px;cursor:pointer;min-height:115px;transition:.15s}.choice-card:hover{border-color:var(--gold);transform:translateY(-2px)}.choice-card h3{font-size:15px;color:#e2cba7;margin-bottom:7px}.choice-card p{margin:0;font-size:15px}.event-copy{font-size:20px;color:#d4c3b6;max-width:670px}.overlay{position:fixed;inset:0;background:#09080bdc;display:grid;place-items:center;z-index:5;padding:20px}.modal{max-width:650px;width:100%;padding:25px;background:#211b22;border:1px solid #80634f;border-radius:5px;box-shadow:0 20px 70px #000c}.modal h2{margin-bottom:12px}.modal-actions{display:flex;flex-wrap:wrap;gap:9px;margin-top:20px}.notice{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#2d2428;border:1px solid #a87955;color:#f0d7b1;padding:10px 17px;border-radius:4px;z-index:10;animation:fade .2s}.footer-hint{font-size:13px;color:#88777b}.disabled{opacity:.4;pointer-events:none}.hp-low{color:#ed7770!important}.relics-title{margin-top:16px!important}
@media(max-width:1000px){#app{padding:12px}.layout{grid-template-columns:1fr}.sidebar{order:2}.main-panel{min-height:420px;padding:14px}.class-grid{grid-template-columns:1fr;max-width:390px}.class-card{min-height:130px}.title-screen{min-height:85vh}.hud{flex-wrap:wrap}.stat{min-width:75px;padding:6px 9px}.combat-arena{min-height:190px;gap:5px}.enemy{width:32%;padding:3px}.enemy-art{font-size:48px}.hand{justify-content:flex-start;min-height:172px;flex-wrap:wrap;overflow:visible}.playing-card{flex:1 1 105px;max-width:150px;height:157px}.map-row{gap:6px}.map-node{width:32px;height:30px}.map-row-label{width:40px;font-size:9px}.brand{font-size:16px}.run-hud{align-items:flex-start;gap:10px;flex-wrap:wrap}.combat-bottom{gap:8px;flex-wrap:wrap}.player-area{gap:8px;flex-wrap:wrap}}
</style>
</head>
<body><div id="app"></div><script>
(()=>{'use strict';
const $=s=>document.querySelector(s), app=$('#app');
const classes={
blood:{name:'Blood Reaper',sigil:'☠',tag:'Sacrifice • Bleed • Life-steal',desc:'Turn your own lifeblood into relentless power, then take it back from your prey.',signature:'Sanguine Strike'},
shadow:{name:'Shadowmancer',sigil:'◈',tag:'Debuffs • Intangible • Control',desc:'Bend the dark to cripple foes and slip beyond the reach of death.',signature:'Shadow Bolt'},
hell:{name:'Hellforged',sigil:'♨',tag:'Burn • Imps • Fury',desc:'Kindle an inferno, summon clawing imps, and feed the flames.',signature:'Firebolt'}
};
const cards={
Strike:{type:'Attack',rarity:'Basic',cost:1,dmg:6,up:{dmg:8},text:'Deal 6 damage.',upText:'Deal 8 damage.'},
Defend:{type:'Skill',rarity:'Basic',cost:1,block:5,up:{block:8},text:'Gain 5 Block.',upText:'Gain 8 Block.'},
'Sanguine Strike':{type:'Attack',rarity:'Common',cost:1,dmg:8,heal:2,up:{dmg:10,heal:3},text:'Deal 8 damage. Heal 2 HP.',upText:'Deal 10 damage. Heal 3 HP.'},
Hemorrhage:{type:'Attack',rarity:'Common',cost:1,dmg:5,bleed:3,up:{dmg:6,bleed:4},text:'Deal 5 damage. Apply 3 Bleed.',upText:'Deal 6 damage. Apply 4 Bleed.'},
Bloodletting:{type:'Attack',rarity:'Uncommon',cost:1,dmg:12,self:3,up:{dmg:15,self:2},text:'Lose 3 HP. Deal 12 damage.',upText:'Lose 2 HP. Deal 15 damage.'},
'Crimson Eruption':{type:'Attack',rarity:'Rare',cost:2,dmg:8,all:true,self:2,up:{dmg:10,self:2},text:'Deal 8 damage to ALL. Lose 2 HP.',upText:'Deal 10 damage to ALL. Lose 2 HP.'},
'Crimson Shield':{type:'Skill',rarity:'Common',cost:1,block:6,self:2,up:{block:8,self:2},text:'Gain 6 Block. Lose 2 HP.',upText:'Gain 8 Block. Lose 2 HP.'},
'Blood Ward':{type:'Skill',rarity:'Common',cost:1,block:3,regen:1,up:{block:4,regen:2},text:'Gain 3 Block. Gain 1 Regeneration.',upText:'Gain 4 Block. Gain 2 Regeneration.'},
'Sacrificial Barrier':{type:'Skill',rarity:'Uncommon',cost:2,block:10,self:5,up:{block:13,self:5},text:'Gain 10 Block. Lose 5 HP.',upText:'Gain 13 Block. Lose 5 HP.'},
'Vital Surge':{type:'Skill',rarity:'Rare',cost:9,missingBlock:true,exhaust:true,up:{missingBonus:3},text:'Gain Block equal to missing HP. Exhaust.',upText:'Gain missing HP +3 Block. Exhaust.'},
'Life Tap':{type:'Skill',rarity:'Common',cost:0,draw:2,self:3,up:{draw:3,self:3},text:'Draw 2 cards. Lose 3 HP.',upText:'Draw 3 cards. Lose 3 HP.'},
'Rend Flesh':{type:'Skill',rarity:'Uncommon',cost:1,allBleed:5,self:2,up:{allBleed:7,self:2},text:'Apply 5 Bleed to ALL. Lose 2 HP.',upText:'Apply 7 Bleed to ALL. Lose 2 HP.'},
'Soul Siphon':{type:'Power',rarity:'Uncommon',cost:2,power:'siphon',up:{cost:1},text:'At turn start: +1 Energy, lose 1 HP.',upText:'At turn start: +1 Energy, lose 1 HP.'},
'Blood Pact':{type:'Power',rarity:'Rare',cost:1,power:'pact',text:'Card HP loss grants 1 Strength.',upText:'Card HP loss grants 2 Strength.'},
'Shadow Bolt':{type:'Attack',rarity:'Common',cost:1,dmg:6,weak:1,up:{dmg:8,weak:2},text:'Deal 6 damage. Apply 1 Weak.',upText:'Deal 8 damage. Apply 2 Weak.'},
'Grasp of Despair':{type:'Attack',rarity:'Common',cost:1,dmg:4,vuln:1,up:{dmg:5,vuln:2},text:'Deal 4 damage. Apply 1 Vulnerable.',upText:'Deal 5 damage. Apply 2 Vulnerable.'},
'Nightmare Lash':{type:'Attack',rarity:'Uncommon',cost:2,dmg:7,bonusWeak:7,up:{dmg:9,bonusWeak:9},text:'Deal 7 damage (+7 if Weak).',upText:'Deal 9 damage (+9 if Weak).'},
'Eternal Darkness':{type:'Attack',rarity:'Rare',cost:2,randomDmg:3,up:{randomDmg:4},text:'Deal 3 damage to a random foe 3 times.',upText:'Deal 4 damage to a random foe 3 times.'},
'Cloak of Shadows':{type:'Skill',rarity:'Common',cost:1,block:5,draw:1,up:{block:7},text:'Gain 5 Block. Draw 1.',upText:'Gain 7 Block. Draw 1.'},
'Haunting Mist':{type:'Skill',rarity:'Common',cost:1,block:7,allWeak:1,up:{block:9},text:'Gain 7 Block. Apply 1 Weak to ALL.',upText:'Gain 9 Block. Apply 1 Weak to ALL.'},
'Phantom Armor':{type:'Skill',rarity:'Uncommon',cost:1,block:8,nextDex:2,up:{block:10,nextDex:3},text:'Gain 8 Block. Next turn +2 Dexterity.',upText:'Gain 10 Block. Next turn +3 Dexterity.'},
'Whispering Shroud':{type:'Skill',rarity:'Rare',cost:1,block:4,nextBlock:4,up:{block:5,nextBlock:5},text:'Gain 4 Block. Next turn gain 4 Block.',upText:'Gain 5 Block. Next turn gain 5 Block.'},
Shadowform:{type:'Skill',rarity:'Uncommon',cost:2,intangible:2,exhaust:true,up:{intangible:3},text:'Gain 2 Intangible. Exhaust.',upText:'Gain 3 Intangible. Exhaust.'},
'Curse of Misfortune':{type:'Skill',rarity:'Common',cost:1,allWeakness:3,vuln:1,up:{allWeakness:4,vuln:2},text:'Enemy -3 Strength this turn. Apply 1 Vulnerable.',upText:'Enemy -4 Strength. Apply 2 Vulnerable.'},
'Dark Ritual':{type:'Skill',rarity:'Uncommon',cost:0,energy:2,dazed:true,up:{},text:'Gain 2 Energy. Shuffle a Dazed into draw pile.',upText:'Gain 2 Energy. Shuffle Dazed into discard.'},
'Umbral Mastery':{type:'Power',rarity:'Rare',cost:2,power:'mastery',up:{mastery:2},text:'At turn start, apply 1 Weak to ALL foes.',upText:'At turn start, apply 2 Weak to ALL foes.'},
Firebolt:{type:'Attack',rarity:'Common',cost:1,dmg:7,burn:2,up:{dmg:9,burn:3},text:'Deal 7 damage. Apply 2 Burn.',upText:'Deal 9 damage. Apply 3 Burn.'},
'Hellfire Blast':{type:'Attack',rarity:'Uncommon',cost:2,dmg:9,all:true,up:{dmg:12},text:'Deal 9 damage to ALL enemies.',upText:'Deal 12 damage to ALL.'},
'Searing Strike':{type:'Attack',rarity:'Common',cost:1,dmg:6,nextEnergy:1,up:{dmg:8},text:'Deal 6 damage. +1 Energy next turn.',upText:'Deal 8 damage. +1 Energy next turn.'},
Immolate:{type:'Attack',rarity:'Rare',cost:2,dmg:12,exhaust:true,up:{dmg:16},text:'Deal 12 damage. Exhaust.',upText:'Deal 16 damage. Exhaust.'},
'Obsidian Skin':{type:'Skill',rarity:'Common',cost:1,block:7,up:{block:10},text:'Gain 7 Block.',upText:'Gain 10 Block.'},
'Flame Ward':{type:'Skill',rarity:'Common',cost:1,block:5,randomBurn:2,up:{block:6,randomBurn:3},text:'Gain 5 Block. Apply 2 Burn to random foe.',upText:'Gain 6 Block. Apply 3 Burn.'},
'Demonic Resilience':{type:'Skill',rarity:'Uncommon',cost:1,block:4,heal:2,up:{block:5,heal:3},text:'Gain 4 Block. Heal 2 HP.',upText:'Gain 5 Block. Heal 3 HP.'},
'Brimstone Shield':{type:'Skill',rarity:'Rare',cost:1,block:3,tempStr:2,up:{block:4,tempStr:3},text:'Gain 3 Block. +2 Strength this turn.',upText:'Gain 4 Block. +3 Strength this turn.'},
'Summon Imp':{type:'Skill',rarity:'Common',cost:1,summon:true,exhaust:true,text:'Add an Imp Claw to hand. Exhaust.',upText:'Add an Imp Claw+ to hand. Exhaust.'},
'Infernal Pact':{type:'Skill',rarity:'Uncommon',cost:0,self:2,energy:2,summon:true,up:{self:1},text:'Lose 2 HP. +2 Energy. Add Imp Claw.',upText:'Lose 1 HP. +2 Energy. Add Imp Claw+.'},
'Hellfire Aura':{type:'Power',rarity:'Rare',cost:2,power:'aura',up:{aura:5},text:'At turn start, deal 3 to ALL foes.',upText:'At turn start, deal 5 to ALL foes.'},
'Soul Burn':{type:'Skill',rarity:'Uncommon',cost:1,burn:8,draw:1,up:{burn:10},text:'Apply 8 Burn. Draw 1.',upText:'Apply 10 Burn. Draw 1.'},
'Imp Claw':{type:'Attack',rarity:'Token',cost:0,dmg:4,exhaust:true,text:'Deal 4 damage. Exhaust.',upText:'Deal 6 damage. Exhaust.'},
'Flash of Insight':{type:'Skill',rarity:'Common',cost:1,draw:2,up:{draw:3},text:'Draw 2 cards.',upText:'Draw 3 cards.'},
'Iron Will':{type:'Skill',rarity:'Common',cost:1,block:8,up:{block:11},text:'Gain 8 Block.',upText:'Gain 11 Block.'},
'Quick Slash':{type:'Attack',rarity:'Common',cost:1,dmg:7,up:{dmg:9},text:'Deal 7 damage.',upText:'Deal 9 damage.'},
'Potent Brew':{type:'Skill',rarity:'Uncommon',cost:0,energy:1,exhaust:true,up:{energy:2},text:'Gain 1 Energy. Exhaust.',upText:'Gain 2 Energy. Exhaust.'},
Wound:{type:'Curse',rarity:'Curse',cost:99,unplayable:true,ethereal:true,text:'Unplayable. Ethereal.'},Dazed:{type:'Status',rarity:'Status',cost:99,unplayable:true,ethereal:true,text:'Unplayable. Ethereal.'},Doubt:{type:'Curse',rarity:'Curse',cost:99,unplayable:true,doubt:true,text:'Unplayable. End turn: lose 1 HP.'}
};
const pools={blood:['Sanguine Strike','Hemorrhage','Bloodletting','Crimson Eruption','Crimson Shield','Blood Ward','Sacrificial Barrier','Vital Surge','Life Tap','Rend Flesh','Soul Siphon','Blood Pact'],shadow:['Shadow Bolt','Grasp of Despair','Nightmare Lash','Eternal Darkness','Cloak of Shadows','Haunting Mist','Phantom Armor','Whispering Shroud','Shadowform','Curse of Misfortune','Dark Ritual','Umbral Mastery'],hell:['Firebolt','Hellfire Blast','Searing Strike','Immolate','Obsidian Skin','Flame Ward','Demonic Resilience','Brimstone Shield','Summon Imp','Infernal Pact','Hellfire Aura','Soul Burn']};
const relics=[['Bloodstone','Whenever a card costs HP, heal 1.'],['Shadow Cloak','Start combat with 1 Intangible.'],['Cursed Amulet','Start combat: all foes gain 1 Weak.'],['Tome of Dark Rituals','Play a Power: draw 1 card.'],['Bleeding Edge','Turn start: apply 1 Bleed to a random foe.'],['Soul Ember','Burn inflicts 2 extra damage on application.'],['Eternal Hourglass','Empty hand at turn start: gain 1 Energy.'],['Ashen Armor','Heal 6 HP after combat.'],['Greedy Imp','Gain 10 extra Gold from battles.'],['Demon Horns','Start combat with an Imp Claw.'],['Sacrificial Dagger','First HP-cost card each combat costs 1 less HP.'],['Cursed Crown','+1 Energy each turn. Curses cannot be removed.'],['Ectoplasm','+1 Energy each turn. No more Gold.'],['Philosopher’s Stone','+1 Energy. Enemies start with +1 Strength.'],['Black Blood','Start combat lose 7 HP; Attacks heal 1.'],['Shadow Amulet','Start turn with 1 Intangible; max HP -15.']];
const potions=['Blood Vial','Firebomb','Dex Tonic','Energy Elixir','Cursed Brew','Bleeding Flask','Imp-in-a-Bottle','Smoke Bomb'];
const nodes=['enemy','enemy','event','shop','enemy','rest','enemy','elite','event','enemy','elite','enemy','rest','boss'];
const nodeIcons={enemy:'⚔',elite:'♜',event:'?',shop:'◇',rest:'✚',boss:'☠'};
let G=null, selectedClass='blood', toastTimer;
function fresh(){return {screen:'title',cls:null,act:1,floor:0,hp:70,maxHp:70,gold:99,deck:[],relics:[],potions:[],upcomingDex:0,upcomingBlock:0,firstSac:false,runEnded:false}}
G=fresh();
function card(name,up=false){return {name,up,id:Math.random().toString(36).slice(2),upgraded:up}}
function cardDef(c){return cards[c.name]||cards.Strike}
function val(c,k){let d=cardDef(c);return c.upgraded&&d.up&&d.up[k]!==undefined?d.up[k]:d[k]}
function displayText(c){let d=cardDef(c);return c.upgraded?(d.upText||d.text):d.text}
function notify(t){let n=document.createElement('div');n.className='notice';n.textContent=t;document.body.append(n);clearTimeout(toastTimer);toastTimer=setTimeout(()=>n.remove(),2200)}
function render(){if(!G||G.screen==='title'){renderTitle();return}if(G.screen==='combat'){renderRun(true);return}if(['map','event','rest','shop','reward','bossreward','gameover','victory'].includes(G.screen)){renderRun(false);return}}
function renderTitle(){app.innerHTML=`<header class="topbar"><div class="brand">SHADOWS OF THE ABYSS<small>A CURSED DECKBUILDING ROGUELIKE</small></div><span class="badge">A SINGLE PLAYER DESCENT</span></header><section class="title-screen screen"><div class="title-inner"><div class="eyebrow">THE WORLD IS DYING. MAKE IT FEAR YOU.</div><h1>Shadows<br>of the Abyss</h1><div class="tag">Choose what remains of your soul.</div><div class="class-grid">${Object.entries(classes).map(([k,c])=>`<div class="class-card ${selectedClass===k?'selected':''}" data-class="${k}"><div class="sigil">${c.sigil}</div><h3>${c.name}</h3><p>${c.tag}</p></div>`).join('')}</div><button class="btn" id="begin">Begin the descent</button><p class="footer-hint">70 HP · 3 Energy · permadeath</p></div></section>`;
app.querySelectorAll('[data-class]').forEach(el=>el.onclick=()=>{selectedClass=el.dataset.class;renderTitle()});$('#begin').onclick=()=>startRun(selectedClass)}
function startRun(cls){G=fresh();G.screen='map';G.cls=cls;G.deck=[...Array(5)].map(()=>card('Strike')).concat([...Array(4)].map(()=>card('Defend')),[card(classes[cls].signature)]);render();}
function hpHud(){return `<div class="stat"><label>Vitality</label><strong class="${G.hp<20?'hp-low':''}">${G.hp} <small style="font-size:12px;color:#9c8989">/ ${G.maxHp}</small></strong><div class="hpbar"><i style="width:${Math.max(0,100*G.hp/G.maxHp)}%"></i></div></div><div class="stat"><label>Gold</label><strong>◈ ${G.gold}</strong></div><div class="stat"><label>Deck</label><strong>${G.deck.length}</strong></div><div class="stat"><label>Potions</label><strong>${G.potions.length}/3</strong></div>`}
function sideBar(){return `<aside class="sidebar"><h3>YOUR RELICS</h3><div class="relic-row">${G.relics.length?G.relics.map(r=>`<span class="chip" title="${r.desc}">${r.name}</span>`).join(''):'<span class="muted">No relics yet</span>'}</div><h3 class="relics-title">POTIONS</h3><div class="potion-row">${G.potions.length?G.potions.map((p,i)=>`<span class="chip" title="Click to use" data-potion="${i}">${p} ×</span>`).join(''):'<span class="muted">Empty vials</span>'}</div><h3 class="relics-title">YOUR DECK <small class="muted">(${G.deck.length})</small></h3><div class="deck-list">${G.deck.map((c,i)=>`<div class="deckrow" ${G.screen==='rest'?'data-remove="'+i+'"':''}><span>${c.name}${c.upgraded?'+':''}</span><small>${cardDef(c).cost===99?'—':cardDef(c).cost} ${G.screen==='rest'?'· remove':''}</small></div>`).join('')}</div></aside>`}
function renderRun(combat){let heading= combat?'THE ENCOUNTER':(G.screen==='map'?actName():(G.screen==='event'?'AN UNWELCOME FATE':G.screen==='rest'?'A MOMENT OF MERCY':G.screen==='shop'?'THE VEILED MARKET':G.screen==='reward'?'VICTORY, FOR NOW':G.screen==='bossreward'?'A THRONE UNCLAIMED':G.screen==='gameover'?'THE ABYSS CLAIMS YOU':'THE ABYSS IS SILENT'));
let main=combat?combatView():G.screen==='map'?mapView():G.screen==='event'?eventView():G.screen==='rest'?restView():G.screen==='shop'?shopView():G.screen==='reward'?rewardView():G.screen==='bossreward'?bossRewardView():endView();
app.innerHTML=`<header class="topbar"><div class="brand">SHADOWS OF THE ABYSS<small>${classes[G.cls].name.toUpperCase()} · ${actName().toUpperCase()}</small></div><div class="top-actions"><button class="mini-btn" id="deckBtn">Deck ${G.deck.length}</button><button class="mini-btn" id="abandon">Abandon run</button></div></header><div class="run-hud"><div class="run-info"><span class="badge">${actName()}</span><span class="badge">${combat?'FLOOR '+G.floor+' · COMBAT':'FLOOR '+(G.floor+1)+' / 15'}</span></div><div class="hud">${hpHud()}</div></div><div class="layout">${sideBar()}<main class="main-panel screen"><div class="section-head"><div><div class="eyebrow">${heading}</div>${!combat?`<p>${G.screen==='map'?'Choose your path through the ruin. There is no way back.':''}</p>`:''}</div>${combat?`<span class="badge">${G.combat.enemies.filter(e=>e.hp>0).length} FOE${G.combat.enemies.filter(e=>e.hp>0).length===1?'':'S'}</span>`:''}</div>${main}</main></div>`;
$('#abandon').onclick=()=>modal('Abandon this run?','All progress will be lost.',[{t:'Stay',fn:()=>{}},{t:'End run',danger:true,fn:()=>{G=fresh();render()}}]);$('#deckBtn').onclick=()=>showDeck();
app.querySelectorAll('[data-potion]').forEach(e=>e.onclick=()=>usePotion(+e.dataset.potion));
if(combat)bindCombat();if(G.screen==='map')bindMap();if(G.screen==='rest')bindRest();if(G.screen==='event')bindEvent();if(G.screen==='shop')bindShop();if(G.screen==='reward')bindReward();if(G.screen==='bossreward')bindBossReward();}
function actName(){return ['','The Crypts','Blighted Woods','Abyssal Spire'][G.act]||'Abyssal Spire'}
function mapView(){let rows='';for(let f=14;f>=1;f--){let type=nodes[f-1];let variants=type==='enemy'||type==='elite'?2:1;let current=G.floor===f-1;rows+=`<div class="map-row"><span class="map-row-label">${f===14?'BOSS':`FLOOR ${f+1}`}</span>${Array.from({length:variants},(_,i)=>{let available=G.floor===f-1;let actualType=type==='elite'&&i===1?'enemy':type;let icon=nodeIcons[actualType];return `<button class="map-node ${available?'available':''} ${current?'current':''} ${actualType==='boss'?'boss':''}" data-floor="${f-1}" data-variant="${i}" title="${actualType.toUpperCase()}${variants===2?(i===0?' · elite path':' · safer path'):''}">${icon}</button>`}).join('')}</div>`}
return `<div class="map-wrap">${rows}</div><div class="map-legend">${Object.keys(nodeIcons).map(k=>`<span class="legend-item"><i class="legend-dot" style="background:${k==='boss'?'#b4474a':k==='rest'?'#75a28a':k==='shop'?'#b99955':k==='event'?'#9980b3':k==='elite'?'#c66c54':'#be8755'}"></i>${k}</span>`).join('')}</div><p class="map-note">Select a glowing node on the next floor to continue.</p>`}
function bindMap(){app.querySelectorAll('.map-node.available').forEach(b=>b.onclick=()=>{let f=+b.dataset.floor,variant=+b.dataset.variant;if(f!==G.floor)return;let type=nodes[f];if(type==='elite'&&variant===1)type='enemy';G.floor++;enterNode(type,variant)})}
function enterNode(type,variant){if(type==='enemy'||type==='elite'||type==='boss'){let group=encounterFor(G.act,G.floor-1,variant,type);startCombat(group,type)}else if(type==='event'){G.screen='event';G.event=eventPool[Math.floor(Math.random()*eventPool.length)];render()}else if(type==='rest'){G.screen='rest';render()}else if(type==='shop'){G.screen='shop';makeShop();render()}}
const eventPool=[
['The Forgotten Altar','A blood-stained altar hums with power.','Offer 15 HP','Pray','Leave'],['Shadowy Peddler','A hooded figure offers a trade.','Trade a card','Buy his wares','Attack'],['Cursed Spring','A spring bubbles with dark water.','Drink','Bottle it','Leave'],['Restless Dead','Ghosts offer a bargain.','Accept','Refuse'],['Impish Gambler','A grinning imp challenges you.','Bet 30 Gold','Wager a card','Leave'],['Ancient Library','Dusty tomes contain forgotten knowledge.','Study','Read forbidden page','Leave']];
function eventView(){let e=G.event;return `<h2>${e[0]}</h2><p class="event-copy">${e[1]}</p><div class="choice-grid">${e.slice(2).map((x,i)=>`<button class="choice-card" data-choice="${i}"><h3>${x}</h3><p>${eventDesc(e[0],i)}</p></button>`).join('')}</div>`}
function eventDesc(n,i){return ({'The Forgotten Altar':['Trade 15 HP for a rare relic.','Heal 10 HP, but gain Doubt.','Walk away.'],'Shadowy Peddler':['Remove one card and gain 100 Gold.','Pay 50 Gold for a potion and relic.','Face the thief for a reward.'],'Cursed Spring':['Lose 10 HP and cleanse all Curses.','Take a Cursed Brew.','Walk away.'],'Restless Dead':['Lose 20 HP to upgrade 2 cards.','Refuse the bargain.'],'Impish Gambler':['Risk 30 Gold on a coin toss.','Remove a random card.','Walk away.'],'Ancient Library':['Lose 5 HP; +1 Energy next combat.','Add a class Power and Doubt.','Leave the books untouched.']})[n][i]}
function bindEvent(){app.querySelectorAll('[data-choice]').forEach(b=>b.onclick=()=>eventChoice(+b.dataset.choice))}
function eventChoice(i){let n=G.event[0];if(n==='The Forgotten Altar'){if(i===0){if(G.hp<=15)return notify('Not enough HP.');loseHp(15);addRelic(randomRelic(true))}if(i===1){heal(10);G.deck.push(card('Doubt'))}}
if(n==='Shadowy Peddler'){if(i===0){selectDeckCard('Choose a card to remove',idx=>{G.deck.splice(idx,1);G.gold+=100;finishEvent()});return}if(i===1){if(G.gold<50)return notify('Not enough Gold.');G.gold-=50;addPotion(randomPotion());addRelic(randomRelic());}if(i===2){G.eventFight=true;startCombat([makeEnemy('Shadow Thief')],'event');return}}
if(n==='Cursed Spring'){if(i===0){if(G.hp<=10)return notify('Not enough HP.');loseHp(10);G.deck=G.deck.filter(c=>cardDef(c).rarity!=='Curse')}if(i===1)addPotion('Cursed Brew')}
if(n==='Restless Dead'&&i===0){if(G.hp<=20)return notify('Not enough HP.');loseHp(20);selectDeckCard('Choose a card to upgrade',idx=>{G.deck[idx].upgraded=true;selectDeckCard('Choose a second card',idx2=>{G.deck[idx2].upgraded=true;finishEvent()})});return}
if(n==='Impish Gambler'){if(i===0){if(G.gold<30)return notify('Not enough Gold.');G.gold-=30;if(Math.random()<.5)G.gold+=60;notify('The imp tosses its coin…')}if(i===1){if(!G.deck.length)return;let idx=Math.floor(Math.random()*G.deck.length),curse=cardDef(G.deck[idx]).rarity==='Curse';G.deck.splice(idx,1);if(curse)G.gold+=100}}
if(n==='Ancient Library'){if(i===0){if(G.hp<=5)return notify('Not enough HP.');loseHp(5);G.nextCombatEnergy=1}if(i===1){let pool=pools[G.cls].filter(x=>cards[x].type==='Power');G.deck.push(card(pool[Math.floor(Math.random()*pool.length)]));G.deck.push(card('Doubt'))}}
finishEvent()}
function finishEvent(){if(G.hp<=0)return gameOver();G.screen='map';render()}
function restView(){return `<h2>The fire is barely warm.</h2><p>Choose one action before the climb continues.</p><div class="choice-grid"><button class="choice-card" data-rest="heal"><h3>Rest</h3><p>Recover 25% of your maximum HP.</p></button><button class="choice-card" data-rest="upgrade"><h3>Forge</h3><p>Permanently upgrade one card in your deck.</p></button><button class="choice-card" data-rest="remove"><h3>Meditate</h3><p>Remove one card from your deck.</p></button></div>`}
function bindRest(){app.querySelectorAll('[data-rest]').forEach(b=>b.onclick=()=>{let a=b.dataset.rest;if(a==='heal'){heal(Math.floor(G.maxHp*.25));G.screen='map';render()}else selectDeckCard(a==='upgrade'?'Choose a card to upgrade':'Choose a card to remove',idx=>{if(a==='upgrade')G.deck[idx].upgraded=true;else G.deck.splice(idx,1);G.screen='map';render()})})}
function makeShop(){let pool=pools[G.cls];G.shop={wares:[...Array(3)].map(()=>{let n=randomCard(pool);return {name:n,price:cards[n].rarity==='Common'?50:cards[n].rarity==='Uncommon'?75:150}}),color:{name:['Flash of Insight','Iron Will','Quick Slash','Potent Brew'][Math.floor(Math.random()*4)],price:60},relics:[randomRelic(),randomRelic()],pots:[randomPotion(),randomPotion(),randomPotion()]};G.shopRemoved=false}
function shopView(){let s=G.shop;return `<h2>What will you trade for?</h2><p>Your purse holds <b style="color:var(--gold2)">${G.gold} Gold</b>. The merchant's wares vanish when you leave.</p><h3 class="eyebrow">CLASS CARDS</h3><div class="shop-grid">${s.wares.map((w,i)=>shopItem(w.name,w.price,'card',i)).join('')}</div><h3 class="eyebrow">COLORLESS</h3><div class="shop-grid">${shopItem(s.color.name,s.color.price,'color',0)}</div><h3 class="eyebrow">RELICS & POTIONS</h3><div class="shop-grid">${s.relics.map((r,i)=>shopItem(r.name,150,'relic',i)).join('')}${s.pots.map((p,i)=>shopItem(p,50,'potion',i)).join('')}</div><div class="choice-grid"><button class="choice-card" id="shopRemove"><h3>Card removal · 75 Gold</h3><p>${G.shopRemoved?'Service already used.':'Remove one card from your deck.'}</p></button><button class="choice-card" id="leaveShop"><h3>Leave the market</h3><p>Continue your ascent.</p></button></div>`}
function shopItem(n,price,type,i){return `<button class="choice-card shopbuy" data-kind="${type}" data-index="${i}" ${G.gold<price?'':''}><h3>${n}</h3><p>${type==='card'||type==='color'?displayText(card(n)):type==='relic'?((relics.find(r=>r[0]===n)||[])[1]||'A strange relic.'):'A single-use potion.'}</p><p style="color:var(--gold)">◈ ${price}</p></button>`}
function bindShop(){app.querySelectorAll('.shopbuy').forEach(b=>b.onclick=()=>buy(b.dataset.kind,+b.dataset.index));$('#leaveShop').onclick=()=>{G.screen='map';render()};$('#shopRemove').onclick=()=>{if(G.shopRemoved||G.gold<75)return notify('Cannot use removal service.');G.gold-=75;G.shopRemoved=true;selectDeckCard('Choose a card to remove',i=>{G.deck.splice(i,1);render()})}}
function buy(kind,i){let item,price;if(kind==='card'){item=G.shop.wares[i].name;price=G.shop.wares[i].price}else if(kind==='color'){item=G.shop.color.name;price=60}else if(kind==='relic'){item=G.shop.relics[i].name;price=150}else{item=G.shop.pots[i];price=50}if(G.gold<price)return notify('Not enough Gold.');if(kind==='potion'&&G.potions.length>=3)return notify('Potion belt is full.');G.gold-=price;if(kind==='card'||kind==='color')G.deck.push(card(item));if(kind==='relic')addRelic(relics.find(r=>r[0]===item));if(kind==='potion')addPotion(item);render()}
function combatView(){let c=G.combat;return `<div class="combat-head"><div><h2>${c.title}</h2><p style="margin:4px 0">${c.turn===1?'The enemy watches your every move.':'Turn '+c.turn+' · intent is predictable.'}</p></div><div class="badge">${c.energy} / ${c.maxEnergy} ENERGY</div></div><div class="combat-arena">${c.enemies.map((e,i)=>enemyView(e,i)).join('')}</div><div class="player-area"><div class="player-combat-stats"><span class="smallstat">♥ ${G.hp}/${G.maxHp}</span><span class="smallstat">⬟ ${c.block} Block</span><span class="smallstat">✦ ${c.strength} STR</span><span class="smallstat">✧ ${c.dex} DEX</span>${c.intangible?`<span class="smallstat">◌ ${c.intangible} Intangible</span>`:''}</div><span class="smallstat">Draw pile ${c.draw.length} · Discard ${c.discard.length}</span></div><div class="hand">${c.hand.map((x,i)=>handCard(x,i)).join('')}</div><div class="combat-bottom"><div class="log">${c.log||'Choose a card, then choose its target.'}</div><div class="turn-buttons"><button class="btn secondary" id="drawBtn">Draw pile · ${c.draw.length}</button><button class="btn" id="endTurn">End turn →</button></div></div>`}
function enemyView(e,i){let intent=describeIntent(e);return `<div class="enemy ${e.hp<=0?'dead':''} ${G.combat.target===i?'target':''}" data-target="${i}"><div class="enemy-art">${e.art}</div><div class="enemy-name">${e.name}</div><div class="health-track"><span style="width:${Math.max(0,100*e.hp/e.maxHp)}%"></span></div><div class="enemy-stats">${Math.max(0,e.hp)} / ${e.maxHp} HP ${e.block?` · ⬟ ${e.block}`:''}</div><div class="statusline">${statusHTML(e)}</div><div class="intent">${intent}</div></div>`}
function handCard(c,i){let d=cardDef(c),cost=val(c,'cost')??d.cost,blocked=d.unplayable;let text=displayText(c);return `<div class="playing-card ${blocked?'unplayable':''}" data-card="${i}" title="${text}"><span class="cost">${cost===99?'—':cost}</span><span class="cname">${c.name}${c.upgraded?'+':''}</span><span class="ctype">${d.type} · ${d.rarity}</span><span class="cdesc">${text}</span><span class="rarity">${d.exhaust?'EXHAUST':''}</span></div>`}
function bindCombat(){app.querySelectorAll('[data-target]').forEach(e=>e.onclick=()=>{let i=+e.dataset.target;if(G.combat.enemies[i].hp>0){G.combat.target=i;render()}});app.querySelectorAll('[data-card]').forEach(e=>e.onclick=()=>playCard(+e.dataset.card));$('#endTurn').onclick=endTurn;$('#drawBtn').onclick=()=>modal('Draw pile',G.combat.draw.map(c=>c.name+(c.upgraded?'+':'')).join(' · ')||'Empty',[{t:'Close',fn:()=>{}}]);}
function encounterFor(act,floor,variant,type){if(type==='boss')return [makeEnemy(['','Lich King','Eldritch Treant','Archdemon'][act])];if(type==='elite'){return [makeEnemy(([['','Tomb Guardian','Blood Witch'],['','Treant Protector','Plague Shaman'],['','Flamespawn','Dread Knight']][act]||[])[Math.random()<.5?1:2],true)]}
let key=floor;if(variant===1&&floor===0)return [makeEnemy(act===1?'Zombie Brute':act===2?'Corrupted Beast':'Shadow Demon')];if(variant===1&&floor===6)return [makeEnemy(act===1?'Zombie Brute':act===2?'Wraith':'Hellhound')];if(variant===1&&floor===7)return groupByAct(act,'alt9');if(variant===1&&floor===10)return groupByAct(act,'alt12');return groupByAct(act,'f'+key)}
function groupByAct(act,key){let groups={1:{f0:['Skeleton Warrior'],f1:['Dark Cultist'],f4:['Rat Swarm'],f6:['Skeleton Warrior','Skeleton Warrior'],alt9:['Skeleton Warrior','Rat Swarm'],f9:['Dark Cultist','Rat Swarm'],alt12:['Zombie Brute','Skeleton Warrior'],f11:['Dark Cultist','Dark Cultist']},2:{f0:['Wraith'],f1:['Corrupted Beast'],f4:['Thorned Horror'],f6:['Wraith','Witch Doctor'],alt9:['Wraith','Thorned Horror'],f9:['Witch Doctor','Thorned Horror'],alt12:['Corrupted Beast','Wraith'],f11:['Witch Doctor','Witch Doctor']},3:{f0:['Hellhound'],f1:['Shadow Demon'],f4:['Infernal Imp'],f6:['Hellhound','Shadow Demon'],alt9:['Hellhound','Infernal Imp'],f9:['Abyssal Cultist','Hellhound'],alt12:['Shadow Demon','Infernal Imp'],f11:['Abyssal Cultist','Abyssal Cultist']}};let arr=groups[act][key]||groups[act].f0;return arr.map(n=>makeEnemy(n))}
function makeEnemy(name,elite=false){let base={
'Skeleton Warrior':[22,'💀',[['hit',6],['hit',6],['defend',8]]], 'Zombie Brute':[28,'🧟',[['hit',8],['weak',2],['hit',8]]], 'Dark Cultist':[20,'🕯️',[['buff',3],['hit',5],['hit',5]]], 'Rat Swarm':[15,'🐀',[['multi',2,3],['multi',2,3]]],
'Tomb Guardian':[42,'🛡️',[['bash',6,8],['multi',3,4],['hit',12]]], 'Blood Witch':[36,'🧙',[['drain',5,4],['curse',1],['drain',7,4]]],
'Wraith':[30,'👻',[['hitweak',7,1],['hit',7],['defend',10]]], 'Corrupted Beast':[38,'🐺',[['hit',12],['selfvuln',5],['hit',12]]], 'Thorned Horror':[25,'🌵',[['multi',3,4],['bleed',3],['hit',8]]], 'Witch Doctor':[22,'🪄',[['buffally',4,2],['poisonhit',6,2],['hit',6]]], 'Treant Protector':[50,'🌳',[['bash',9,6],['vuln',2],['multi',2,10]]], 'Plague Shaman':[42,'☣️',[['poison',4],['summon','Plague Rat'],['hit',8]]],
'Hellhound':[26,'🐕🦺',[['burnhit',8,2],['hit',8],['defend',9]]], 'Shadow Demon':[30,'😈',[['hitweak',7,1],['hit',7],['intangible',1]]], 'Infernal Imp':[18,'👹',[['hit',5],['hit',5],['summon','Infernal Imp']]], 'Abyssal Cultist':[24,'🔮',[['buffall',2],['hit',7],['hit',7]]], 'Flamespawn':[48,'🔥',[['burnhit',9,3],['defendbuff',10,2],['multi',2,8]]], 'Dread Knight':[55,'⚔️',[['hit',12],['bleed',3],['defendheal',12,4]]], 'Shadow Thief':[34,'🗡️',[['hitweak',8,1],['multi',2,5],['defend',8]]], 'Plague Rat':[18,'🐀',[['hit',5]]],
'Skeleton Minion':[15,'💀',[['hit',5]]], 'Lich King':[65,'👑',[['summon','Skeleton Minion'],['hit',8],['defend',12]]], 'Eldritch Treant':[75,'🌲',[['hitweak',10,2],['defendregen',15,2],['multi',2,8]]], 'Archdemon':[85,'👿',[['summon','Hellhound'],['burnhit',12,2],['defendbuff',15,3]]]
};let b=base[name]||[22,'☠️',[['hit',6]]];return {name,hp:b[0],maxHp:b[0],art:b[1],pattern:b[2],step:0,block:0,strength:0,weak:0,vuln:0,bleed:0,burn:0,poison:0,regen:0,intangible:0,elite:elite,boss:['Lich King','Eldritch Treant','Archdemon'].includes(name)}}
function startCombat(enemies,type){let c={title:enemies.map(e=>e.name).join(' & '),enemies,draw:[],discard:[],exhaust:[],hand:[],powers:[],energy:3,maxEnergy:3,block:0,strength:0,dex:0,intangible:0,weak:0,vuln:0,bleed:0,burn:0,poison:0,regen:0,turn:1,target:0,log:'The battle begins.',type,playerTurn:true,tempStrength:0};G.combat=c;G.screen='combat';if(G.relics.some(r=>r.name==='Shadow Cloak'))c.intangible++;if(G.relics.some(r=>r.name==='Cursed Amulet'))enemies.forEach(e=>e.weak++);if(G.relics.some(r=>r.name==='Philosopher’s Stone'))enemies.forEach(e=>e.strength++);if(G.relics.some(r=>r.name==='Demon Horns'))c.hand.push(card('Imp Claw',true));if(G.relics.some(r=>r.name==='Black Blood'))loseHp(7);if(G.nextCombatEnergy){c.energy+=G.nextCombatEnergy;G.nextCombatEnergy=0}c.draw=shuffle(G.deck.map(x=>({...x})));if(G.relics.some(r=>r.name==='Ectoplasm')||G.relics.some(r=>r.name==='Cursed Crown'))c.maxEnergy=4,c.energy=4;drawCards(5);G.firstSac=false;render()}
function shuffle(a){for(let i=a.length-1;i>0;i--){let j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}
function drawCards(n){let c=G.combat;for(let i=0;i<n;i++){if(!c.draw.length&&c.discard.length)c.draw=shuffle(c.discard.splice(0));if(c.draw.length)c.hand.push(c.draw.pop())}}
function playCard(i){let c=G.combat,cc=c.hand[i],d=cardDef(cc);if(!cc||d.unplayable)return notify('That card cannot be played.');let cost=val(cc,'cost')??d.cost;if(cost>c.energy)return notify('Not enough Energy.');if(d.type==='Attack'&&!d.all&&!d.randomDmg&&(!c.enemies[c.target]||c.enemies[c.target].hp<=0)){c.target=c.enemies.findIndex(e=>e.hp>0);if(c.target<0)return}
c.energy-=cost;c.hand.splice(i,1);let v=k=>val(cc,k)??d[k]??0;let self=v('self');if(self){if(G.relics.some(r=>r.name==='Sacrificial Dagger')&&!G.firstSac){self=Math.max(0,self-1);G.firstSac=true}loseHp(self);if(G.relics.some(r=>r.name==='Bloodstone'))heal(1);if(c.powers.includes('pact'))c.strength+=cc.upgraded?2:1}
if(v('block'))gainBlock(v('block'));if(v('missingBlock'))gainBlock(Math.max(0,G.maxHp-G.hp)+(cc.upgraded?3:0));if(v('heal'))heal(v('heal'));if(v('regen'))c.regen+=v('regen');if(v('intangible'))c.intangible+=v('intangible');if(v('nextDex'))G.upcomingDex=v('nextDex');if(v('nextBlock'))G.upcomingBlock=v('nextBlock');if(v('nextEnergy'))c.nextEnergy=(c.nextEnergy||0)+v('nextEnergy');if(v('tempStr'))c.tempStrength+=v('tempStr');if(v('energy'))c.energy+=v('energy');
if(v('dmg')){let targets=v('all')?alive():[c.enemies[c.target]];targets.forEach(e=>dealToEnemy(e,v('dmg'),true))}
if(v('randomDmg'))for(let z=0;z<3;z++){let t=alive();if(t.length)dealToEnemy(t[Math.floor(Math.random()*t.length)],v('randomDmg'),true)}
if(v('bleed'))applyEnemyStatus(c.enemies[c.target],'bleed',v('bleed'));if(v('allBleed'))alive().forEach(e=>applyEnemyStatus(e,'bleed',v('allBleed')));if(v('burn'))applyEnemyStatus(c.enemies[c.target],'burn',v('burn'));if(v('randomBurn')){let a=alive();if(a.length)applyEnemyStatus(a[Math.floor(Math.random()*a.length)],'burn',v('randomBurn'))}if(v('weak'))applyEnemyStatus(c.enemies[c.target],'weak',v('weak'));if(v('vuln'))applyEnemyStatus(c.enemies[c.target],'vuln',v('vuln'));if(v('allWeak'))alive().forEach(e=>applyEnemyStatus(e,'weak',v('allWeak')));if(v('allWeakness'))alive().forEach(e=>e.strength=Math.max(0,e.strength-v('allWeakness')));
if(v('bonusWeak')&&c.enemies[c.target].weak>0)dealToEnemy(c.enemies[c.target],v('bonusWeak'),true);if(d.power){c.powers.push(d.power);if(d.power==='pact')c.pactPower=cc.upgraded?2:1;if(d.power==='mastery')c.mastery=cc.upgraded?2:1;if(d.power==='aura')c.auraPower=v('aura')||3;if(G.relics.some(r=>r.name==='Tome of Dark Rituals'))drawCards(1)}if(v('draw'))drawCards(v('draw'));if(v('dazed')){let dazed=card('Dazed');if(cc.upgraded)c.discard.push(dazed);else c.draw.push(dazed)}if(v('summon'))c.hand.push(card('Imp Claw',cc.upgraded));if(d.exhaust)c.exhaust.push(cc);else c.discard.push(cc);c.log=`Played ${cc.name}${cc.upgraded?'+':''}.`;if(!alive().length){winCombat();return}if(G.hp<=0){gameOver();return}render()}
function alive(){return G.combat.enemies.filter(e=>e.hp>0)}
function gainBlock(n){G.combat.block+=Math.max(0,n+G.combat.dex)}
function dealToEnemy(e,n,attack=false){if(!e||e.hp<=0)return;let dmg=n+(attack?G.combat.strength+G.combat.tempStrength:0);if(attack&&G.combat.weak)dmg=Math.floor(dmg*.75);if(attack&&e.vuln)dmg=Math.floor(dmg*1.5);dmg=Math.max(0,dmg);let blocked=Math.min(e.block,dmg);e.block-=blocked;dmg-=blocked;if(e.intangible&&dmg>0)dmg=1;e.hp-=dmg;if(e.hp<=0)e.hp=0}
function applyEnemyStatus(e,k,n){if(!e||e.hp<=0)return;if(k==='burn'&&G.relics.some(r=>r.name==='Soul Ember')){dealToEnemy(e,2,false)}e[k]=(e[k]||0)+n}
function loseHp(n){let c=G.combat;if(c&&c.intangible>0)n=1;G.hp=Math.max(0,G.hp-n)}function heal(n){G.hp=Math.min(G.maxHp,G.hp+n)}
function usePotion(i){if(G.screen!=='combat')return notify('Potions can only be used in combat.');let p=G.potions[i],c=G.combat;if(!p)return;if(p==='Smoke Bomb'){if(c.type==='boss')return notify('Cannot flee from a boss.');G.potions.splice(i,1);G.screen='map';G.combat=null;notify('You escaped into the shadows.');render();return}G.potions.splice(i,1);if(p==='Blood Vial')heal(15);if(p==='Firebomb')alive().forEach(e=>dealToEnemy(e,10,false));if(p==='Dex Tonic')c.dex+=3;if(p==='Energy Elixir')c.energy+=2;if(p==='Cursed Brew')alive().forEach(e=>e.vuln+=3);if(p==='Bleeding Flask'){let t=c.enemies[c.target]||alive()[0];applyEnemyStatus(t,'bleed',6)}if(p==='Imp-in-a-Bottle')c.hand.push(card('Imp Claw',true));if(!alive().length){winCombat();return}c.log=`Used ${p}.`;render()}
function endTurn(){let c=G.combat;if(c.hand.some(x=>cardDef(x).doubt))loseHp(1);c.discard.push(...c.hand.filter(x=>!cardDef(x).ethereal));c.exhaust.push(...c.hand.filter(x=>cardDef(x).ethereal));c.hand=[];if(G.hp<=0)return gameOver();for(let e of alive()){if(e.bleed){dealToEnemy(e,e.bleed,false);e.bleed=Math.floor(e.bleed/2)}if(e.poison){dealToEnemy(e,e.poison,false);e.poison=Math.max(0,e.poison-1)}}if(!alive().length)return winCombat();for(let e of [...alive()]){if(e.hp<=0)continue;enemyAct(e);if(G.hp<=0)return gameOver()}c.block=0;c.tempStrength=0;if(c.nextEnergy)c.maxEnergy=3+c.nextEnergy,c.nextEnergy=0;else c.maxEnergy=G.relics.some(r=>['Ectoplasm','Cursed Crown'].includes(r.name))?4:3;c.turn++;c.energy=c.maxEnergy+(c.powers.includes('siphon')?1:0);c.block=G.upcomingBlock;G.upcomingBlock=0;if(G.upcomingDex){c.dex+=G.upcomingDex;G.upcomingDex=0}if(c.powers.includes('siphon'))loseHp(1);if(c.powers.includes('mastery'))alive().forEach(e=>e.weak+=(c.mastery||1));if(c.powers.includes('aura'))alive().forEach(e=>dealToEnemy(e,c.auraPower||3,false));if(G.relics.some(r=>r.name==='Bleeding Edge')){let a=alive();if(a.length)applyEnemyStatus(a[Math.floor(Math.random()*a.length)],'bleed',1)}if(G.relics.some(r=>r.name==='Eternal Hourglass')&&!c.hand.length)c.energy++;if(c.intangible)c.intangible--;if(c.regen){heal(c.regen);c.regen--}if(c.weak)c.weak--;if(c.vuln)c.vuln--;if(c.bleed){loseHp(c.bleed);c.bleed=Math.floor(c.bleed/2)}if(c.burn){loseHp(c.burn);c.burn=Math.floor(c.burn/2)}if(c.poison){loseHp(c.poison);c.poison--}if(G.hp<=0)return gameOver();drawCards(5);c.log='Your turn. The enemy intent is revealed.';render()}
function enemyAct(e){let c=G.combat;e.block=0;if(e.boss&&e.name==='Lich King'&&e.hp<=30){if(!e.phase2){e.phase2=true;e.step=0}e.strength+=2;let m=[['hit',10],['hit',10],['defend',15]][e.step%3];doEnemyIntent(e,m);e.step++;return}if(e.boss&&e.name==='Eldritch Treant'&&e.hp<=40){e.strength++;let m=[['vulnhit',14,1],['defendheal',20,5],['vulnhit',14,1]][e.step%3];doEnemyIntent(e,m);e.step++;return}if(e.boss&&e.name==='Archdemon'&&e.hp<=45){e.strength+=2;let m=[['hit',16],['multi',3,7],['hit',16]][e.step%3];doEnemyIntent(e,m);e.step++;return}let m=e.pattern[e.step%e.pattern.length];if(e.boss&&e.name==='Lich King'&&m[0]==='summon'&&alive().filter(x=>x.name==='Skeleton Minion').length>=2)m=['hit',8];if(e.boss&&e.name==='Archdemon'&&m[0]==='summon'&&alive().some(x=>x.name==='Hellhound'))m=['hit',12];doEnemyIntent(e,m);e.step++;if(e.weak)e.weak--;if(e.vuln)e.vuln--;if(e.regen){e.hp=Math.min(e.maxHp,e.hp+e.regen);e.regen--}}
function doEnemyIntent(e,m){let c=G.combat,[a,x,y]=m;let atk=(n)=>{let dmg=Math.max(0,n+e.strength);if(c.vuln)dmg=Math.floor(dmg*1.5);for(let j=0;j<1;j++)playerDamage(dmg)};if(a==='hit')atk(x);if(a==='hitweak'){atk(x);c.weak+=y}if(a==='multi')for(let i=0;i<x;i++)atk(y);if(a==='defend')e.block+=x;if(a==='bash'){atk(x);e.block+=y}if(a==='buff')e.strength+=x;if(a==='weak')c.weak+=x;if(a==='vuln')c.vuln+=x;if(a==='bleed')c.bleed+=x;if(a==='poison')c.poison+=x;if(a==='curse'){c.weak+=1;c.vuln+=1}if(a==='drain'){atk(x);e.hp=Math.min(e.maxHp,e.hp+y)}if(a==='selfvuln'){e.block+=x;e.vuln++}if(a==='buffally'){let ally=alive().filter(q=>q!==e)[0]||e;ally.block+=x;ally.regen+=y}if(a==='poisonhit'){atk(x);c.poison+=y}if(a==='intangible')e.intangible+=x;if(a==='summon'){if(alive().length<4)c.enemies.push(makeEnemy(x))}if(a==='buffall')alive().forEach(q=>q.strength+=x);if(a==='burnhit'){atk(x);c.burn+=y}if(a==='defendbuff'){e.block+=x;e.strength+=y}if(a==='defendheal'){e.block+=x;e.hp=Math.min(e.maxHp,e.hp+y)}if(a==='defendregen'){e.block+=x;e.regen+=y}if(a==='vulnhit'){atk(x);c.vuln+=y}}
function playerDamage(n){let c=G.combat,dmg=c.weak?Math.floor(n*.75):n;if(c.intangible)dmg=dmg>0?1:0;let block=Math.min(c.block,dmg);c.block-=block;dmg-=block;G.hp=Math.max(0,G.hp-dmg)}
function describeIntent(e){let m=e.pattern[e.step%e.pattern.length];if(e.boss&&e.name==='Lich King'&&e.hp<=30)m=[['hit',10],['hit',10],['defend',15]][e.step%3];if(e.boss&&e.name==='Archdemon'&&e.hp<=45)m=[['hit',16],['multi',3,7],['hit',16]][e.step%3];if(e.boss&&e.name==='Eldritch Treant'&&e.hp<=40)m=[['vulnhit',14],['defendheal',20],['vulnhit',14]][e.step%3];let [a,x,y]=m;return ({hit:`⚔ ${x+e.strength}`,hitweak:`⚔ ${x+e.strength} · Weak`,multi:`⚔ ${x} × ${y+e.strength}`,defend:`⬟ ${x} Block`,bash:`⚔ ${x} + ⬟ ${y}`,buff:`✦ +${x} Strength`,weak:`☾ ${x} Weak`,vuln:`☠ ${x} Vulnerable`,bleed:`🩸 ${x} Bleed`,poison:`☣ ${x} Poison`,curse:'☾ Weak + Vulnerable',drain:`⚔ ${x} · Heal ${y}`,selfvuln:`⬟ ${x} · Exposed`,buffally:'✦ Buff ally',poisonhit:`⚔ ${x} · ☣ ${y}`,intangible:'◌ Intangible',summon:`☠ Summon ${x}`,buffall:`✦ All +${x} Strength`,burnhit:`⚔ ${x} · 🔥 ${y}`,defendbuff:`⬟ ${x} · ✦ +${y}`,defendheal:`⬟ ${x} · ♥ ${y}`,defendregen:`⬟ ${x} · ✚ ${y}`,vulnhit:`⚔ ${x} · ☠`})[a]||'⚔ ???'}
function statusHTML(e){return ['strength','weak','vuln','bleed','burn','poison','regen','intangible'].filter(k=>e[k]>0).map(k=>`<span class="status">${({strength:'STR',weak:'WEAK',vuln:'VULN',bleed:'BLEED',burn:'BURN',poison:'POISON',regen:'REGEN',intangible:'INTANGIBLE'})[k]} ${e[k]}</span>`).join('')}
function winCombat(){let c=G.combat;if(!c||G.screen!=='combat')return;let boss=c.type==='boss';if(G.relics.some(r=>r.name==='Ashen Armor'))heal(6);if(G.eventFight){G.eventFight=false;G.gold+=75;G.screen='map';G.combat=null;notify('The thief falls. +75 Gold');render();return}G.gold+=boss?50:c.type==='elite'?28:Math.floor(10+Math.random()*11)+(G.relics.some(r=>r.name==='Greedy Imp')?10:0);G.reward={boss,type:c.type,options:rewardCards(c.type==='elite',boss)};if(c.type==='elite'){let r=randomRelic();addRelic(r);G.reward.relic=r.name}if(boss){heal(G.maxHp);G.reward.pots=[randomPotion(),randomPotion()];G.screen='bossreward'}else{if(Math.random()<(c.type==='elite'?.7:.4))addPotion(randomPotion());G.screen='reward'}G.combat=null;render()}
function rewardCards(elite,boss){let pool=pools[G.cls];let out=[];while(out.length<3){let n;if(boss){let rares=pool.filter(x=>cards[x].rarity==='Rare');n=out.length===0?rares[Math.floor(Math.random()*rares.length)]:randomCard(pool)}else n=randomCard(pool,elite);if(!out.includes(n))out.push(n)}return out}
function randomCard(pool,elite=false){let r=Math.random(),rarity=r<(elite?.2:.1)?'Rare':r<(elite?.5:.4)?'Uncommon':'Common';let p=pool.filter(n=>cards[n].rarity===rarity);if(!p.length)p=pool;return p[Math.floor(Math.random()*p.length)]}
function rewardView(){let r=G.reward;return `<h2>The dust settles.</h2><p>You salvage <b style="color:var(--gold2)">${G.gold} Gold</b> in total. ${r.relic?`Relic found: <b>${r.relic}</b>.`:''} Choose a card to add, or move on.</p><div class="reward-grid">${r.options.map((n,i)=>`<div class="playing-card reward-card" data-reward="${i}"><span class="cost">${cards[n].cost===99?'—':cards[n].cost}</span><span class="cname">${n}</span><span class="ctype">${cards[n].type} · ${cards[n].rarity}</span><span class="cdesc">${cards[n].text}</span></div>`).join('')}</div><button class="btn secondary" id="skipReward">Skip card</button><p class="footer-hint">A potion may have been found. Click it in your relic panel during the next battle.</p>`}
function bindReward(){app.querySelectorAll('[data-reward]').forEach(b=>b.onclick=()=>{G.deck.push(card(G.reward.options[+b.dataset.reward]));G.screen='map';render()});$('#skipReward').onclick=()=>{G.screen='map';render()}}
const bossRelics=[['Cursed Crown','Gain 1 Energy each turn. Curses can no longer be removed.'],['Ectoplasm','Gain 1 Energy. No more Gold this run.'],['Philosopher’s Stone','Gain 1 Energy. All enemies start with +1 Strength.'],['Black Blood','Start combat lose 7 HP; Attacks heal 1 HP.'],['Shadow Amulet','Start turn with 1 Intangible; max HP -15.']];
function bossRewardView(){return `<h2>The act falls silent.</h2><p>The boss is vanquished. You are restored to full vitality, and discover two potions. Choose one relic to carry into the next descent.</p><div class="choice-grid">${bossRelics.map((r,i)=>`<button class="choice-card" data-bossrel="${i}"><h3>${r[0]}</h3><p>${r[1]}</p></button>`).join('')}</div>`}
function bindBossReward(){app.querySelectorAll('[data-bossrel]').forEach(b=>b.onclick=()=>{let r=bossRelics[+b.dataset.bossrel];addRelic({name:r[0],desc:r[1]});G.potions.push(...G.reward.pots.slice(0,Math.max(0,3-G.potions.length)));if(G.act>=3){G.screen='victory';render();return}G.act++;G.floor=0;G.screen='map';render()})}
function addPotion(p){if(G.potions.length<3)G.potions.push(p);else notify('Your potion belt is full.')}
function randomPotion(){return potions[Math.floor(Math.random()*potions.length)]}
function randomRelic(rare=false){let arr=rare?[relics[1],relics[5],relics[15]]:relics.slice(0,12);if(G.cls!=='hell')arr=arr.filter(x=>x[0]!=='Demon Horns');if(G.cls!=='blood')arr=arr.filter(x=>x[0]!=='Sacrificial Dagger');let x=arr[Math.floor(Math.random()*arr.length)];return {name:x[0],desc:x[1]}}
function addRelic(r){if(!r)return;if(typeof r.length==='number'&&Array.isArray(r))r={name:r[0],desc:r[1]};if(r.name==='Shadow Amulet'&&G.maxHp>55){G.maxHp-=15;G.hp=Math.min(G.hp,G.maxHp)}G.relics.push(r);notify(`Relic acquired: ${r.name}`)}
function selectDeckCard(title,cb){let opts=G.deck.map((c,i)=>({t:c.name+(c.upgraded?'+':''),fn:()=>cb(i)}));if(!opts.length)return notify('There are no cards to choose.');modal(title,'Select one card.',opts.concat([{t:'Cancel',fn:()=>{}}]))}
function showDeck(){modal('Your deck',G.deck.map(c=>c.name+(c.upgraded?'+':'')).join(' · ')||'Empty deck',[{t:'Close',fn:()=>{}}])}
function modal(title,text,actions){let o=document.createElement('div');o.className='overlay';o.innerHTML=`<div class="modal"><h2>${title}</h2><p>${text}</p><div class="modal-actions">${actions.map((a,i)=>`<button class="btn ${a.danger?'danger':'secondary'}" data-modal="${i}">${a.t}</button>`).join('')}</div></div>`;document.body.append(o);o.querySelectorAll('[data-modal]').forEach(b=>b.onclick=()=>{let a=actions[+b.dataset.modal];o.remove();a.fn()})}
function gameOver(){G.screen='gameover';G.combat=null;render()}
function endView(){if(G.screen==='victory')return `<div style="text-align:center;padding:55px 10px"><div class="eyebrow">THE ABYSS HAS BEEN SILENCED</div><h1 style="margin:15px 0;color:#e9d3b3">You endured.</h1><p class="event-copy" style="margin:0 auto 25px">The Archdemon's throne crumbles. For one fragile moment, the world remembers the light.</p><button class="btn" id="newRun">Begin again</button></div>`;if(G.screen==='gameover')return `<div style="text-align:center;padding:55px 10px"><div class="eyebrow">YOUR DESCENT ENDS HERE</div><h1 style="margin:15px 0;color:#bd7771">Claimed by the Abyss</h1><p class="event-copy" style="margin:0 auto 25px">The run is lost. The dark is patient.</p><button class="btn" id="newRun">Rise once more</button></div>`;return ''}
// Status and utility helpers
function addRelicFromName(n){let r=relics.find(x=>x[0]===n);if(r)addRelic(r)}
function bindEnd(){let b=$('#newRun');if(b)b.onclick=()=>{G=fresh();renderTitle()}}
const oldRender=render;render=function(){oldRender();bindEnd()};
render();
})();
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6-luna","provider":"openrouter","persona":"full","sessionId":"6757e9ee-3ff4-49a2-aafe-592502940518","tokensIn":983893,"tokensOut":32137,"tokensTotal":1016030,"cost":0.03212755000000001,"turns":23,"toolCalls":22,"failedToolCalls":1,"timestamp":"2026-09-23T00:35:43.708Z"} -->