Shadows
Ornith 1.5 35B A3B · 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 88,209 bytes · SHA-256 10f8bde52538
<!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:#0b0810; --bg2:#150e1c; --panel:#1d1526; --panel2:#261b31;
--gold:#d9b24a; --gold2:#f0d68a; --blood:#8b1a1a; --blood2:#c0392b;
--purple:#6a2c9c; --purple2:#9b59b6; --teal:#1f6f6f; --text:#e8e0f0;
--muted:#9a8fb0; --line:#3a2b4a; --block:#5b9dd5; --hp:#b02828;
}
*{box-sizing:border-box;}
html,body{margin:0; padding:0; background:radial-gradient(circle at 50% 0%, #1a1224 0%, var(--bg) 70%); color:var(--text); font-family:'Trebuchet MS','Segoe UI',system-ui,sans-serif; min-height:100vh;}
#app{max-width:1100px; margin:0 auto; padding:16px; min-height:100vh;}
h1,h2,h3{margin:0 0 8px; font-weight:700; letter-spacing:.5px;}
.title-logo{font-size:46px; text-align:center; background:linear-gradient(180deg,var(--gold2),var(--gold) 60%,#7a5a1e); -webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:0 0 24px rgba(217,178,74,.25); letter-spacing:2px; margin-bottom:2px;}
.title-sub{text-align:center; color:var(--muted); letter-spacing:6px; font-size:13px; text-transform:uppercase; margin-bottom:20px;}
button{font-family:inherit; cursor:pointer; border:1px solid var(--line); background:var(--panel2); color:var(--text); padding:10px 18px; border-radius:8px; font-size:15px; transition:.15s; }
button:hover:not(:disabled){background:#362645; border-color:var(--gold); transform:translateY(-1px);}
button:disabled{opacity:.4; cursor:not-allowed;}
button.primary{background:linear-gradient(180deg,#7a2a2a,#5a1616); border-color:#a33;}
button.primary:hover{background:linear-gradient(180deg,#9a3434,#6a1a1a);}
button.gold{background:linear-gradient(180deg,#6a5020,#4a3a14); border-color:var(--gold);}
button.gold:hover{border-color:var(--gold2);}
.btn-row{display:flex; gap:10px; flex-wrap:wrap; justify-content:center; margin-top:14px;}
.card{width:168px; min-height:232px; border-radius:12px; padding:10px; background:linear-gradient(180deg,var(--panel2),#140e1f); border:2px solid var(--line); position:relative; box-shadow:0 6px 16px rgba(0,0,0,.5); transition:.15s; user-select:none;}
.card.attack{border-color:#a33;} .card.skill{border-color:#3a6;} .card.power{border-color:var(--purple2);}
.card .cost{position:absolute; top:-8px; left:-8px; width:34px; height:34px; border-radius:50%; background:radial-gradient(circle at 35% 30%,#6ec1ff,#1f5fb0); display:flex; align-items:center; justify-content:center; font-weight:800; font-size:16px; border:2px solid #cfe6ff; color:#fff; box-shadow:0 2px 6px rgba(0,0,0,.6);}
.card .cname{font-size:14px; font-weight:700; text-align:center; margin:10px 0 2px; min-height:36px; display:flex; align-items:center; justify-content:center;}
.card .ctype{font-size:10px; text-align:center; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin-bottom:6px;}
.card .cdesc{font-size:11px; line-height:1.35; text-align:center; color:var(--text); min-height:96px; display:flex; align-items:center; justify-content:center; padding:4px;}
.card .rarity{text-align:center; font-size:9px; letter-spacing:1px; margin-top:auto; color:var(--muted); text-transform:uppercase;}
.card.playable{cursor:pointer;} .card.playable:hover{transform:translateY(-14px) scale(1.04); box-shadow:0 12px 26px rgba(217,178,74,.35); border-color:var(--gold);}
.card.targetable{cursor:crosshair; box-shadow:0 0 0 3px #ff5555, 0 8px 20px rgba(255,0,0,.4);}
.card.ethereal{opacity:.65;}
.card .up{position:absolute; top:2px; right:6px; color:var(--gold2); font-size:11px; font-weight:700;}
.panel{background:linear-gradient(180deg,var(--panel),var(--bg2)); border:1px solid var(--line); border-radius:12px; padding:18px; margin-bottom:14px; box-shadow:inset 0 0 30px rgba(0,0,0,.4);}
.stat{display:inline-flex; flex-direction:column; align-items:center; margin-right:22px; min-width:60px;}
.stat .v{font-size:24px; font-weight:800;} .stat .k{font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:1px;}
.hpbar{height:14px; background:#2a0f0f; border-radius:8px; overflow:hidden; border:1px solid #4a1a1a; margin:6px 0;}
.hpbar > div{height:100%; background:linear-gradient(90deg,#7a1414,#c0392b); transition:width .3s;}
.hpbar.enemy > div{background:linear-gradient(90deg,#5a2a2a,#a33);}
.blockbadge{display:inline-block; background:radial-gradient(circle at 35% 30%,#7fb4,e0ecff,#5b9dd5); color:#0a1626; font-weight:800; border-radius:6px; padding:1px 8px; font-size:12px; margin-left:6px;}
.statusbadge{display:inline-block; font-size:11px; padding:2px 8px; border-radius:10px; margin:2px; line-height:1.5;}
.sb-strength{background:#5a2a1a;} .sb-dexterity{background:#1a5a2a;} .sb-regen{background:#1a4a4a;} .sb-intangible{background:#3a2a5a;}
.sb-weak{background:#4a3a1a;} .sb-vulnerable{background:#5a1a3a;} .sb-bleed{background:#5a1010;} .sb-burn{background:#5a3010;} .sb-poison{background:#2a5a1a;}
.enemy-grid{display:flex; gap:20px; justify-content:center; flex-wrap:wrap; margin:14px 0;}
.enemy{background:linear-gradient(180deg,#2a1414,#160a0a); border:2px solid #5a2a2a; border-radius:14px; padding:14px; min-width:150px; text-align:center; position:relative;}
.enemy.targetable{border-color:#ff5555; box-shadow:0 0 0 3px #ff5555;}
.enemy .eicon{font-size:44px; margin:4px 0;}
.enemy .ename{font-weight:700; font-size:15px;}
.intent{position:absolute; top:-14px; left:50%; transform:translateX(-50%); background:#1a1a1a; border:1px solid var(--gold); border-radius:8px; padding:3px 9px; font-size:12px; white-space:nowrap; box-shadow:0 2px 8px rgba(0,0,0,.6);}
.intent .it{color:var(--gold2); font-weight:700;}
.powers-zone{display:flex; gap:8px; min-height:70px; align-items:center;}
.powerchip{width:60px; height:70px; border:2px solid var(--purple2); border-radius:8px; background:#2a1a3a; display:flex; flex-direction:column; align-items:center; justify-content:center; font-size:9px; text-align:center; padding:2px;}
.hand{display:flex; gap:10px; justify-content:center; flex-wrap:wrap; padding:10px 0; min-height:240px; align-items:flex-end;}
.resources{display:flex; gap:18px; align-items:center; flex-wrap:wrap;}
.potions{display:flex; gap:8px;}
.potion{width:46px; height:54px; border-radius:8px; border:2px solid var(--line); background:#2a1a2a; display:flex; align-items:center; justify-content:center; font-size:22px; cursor:pointer; position:relative;}
.potion:hover{border-color:var(--gold); transform:translateY(-4px);}
.potion .pnum{position:absolute; bottom:-4px; right:-4px; background:var(--blood); border-radius:6px; font-size:10px; padding:0 4px;}
.node{width:74px; height:74px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:28px; cursor:pointer; border:3px solid var(--line); background:var(--panel); position:relative; transition:.15s; margin:8px auto;}
.node:hover{transform:scale(1.08);}
.node.enemy{border-color:#a33; color:#e77;} .node.elite{border-color:#c84; color:#fb6; box-shadow:0 0 12px rgba(255,160,40,.4);}
.node.rest{border-color:#3c6; color:#6e9;} .node.shop{border-color:var(--gold); color:var(--gold2);} .node.event{border-color:var(--purple2); color:#c9a;}
.node.boss{border-color:#f33; color:#f66; box-shadow:0 0 18px rgba(255,0,0,.5); font-weight:800;}
.node.current{outline:3px solid var(--gold2); outline-offset:2px;}
.node.done{opacity:.45;}
.map-wrap{position:relative; min-height:520px; margin-top:10px;}
.map-row{display:flex; justify-content:center; gap:40px; margin:6px 0;}
.path-line{width:2px; height:14px; background:var(--line); margin:0 auto;}
.toast{position:fixed; top:18px; left:50%; transform:translateX(-50%); background:#1a1a1a; border:1px solid var(--gold); padding:12px 22px; border-radius:10px; z-index:99; box-shadow:0 6px 20px rgba(0,0,0,.7); animation:pop .3s;}
@keyframes pop{from{transform:translate(-50%,-10px); opacity:0;}}
.center{text-align:center;} .muted{color:var(--muted);} .gold-t{color:var(--gold2);} .red{color:#e66;} .big{font-size:20px;}
.classcard{width:220px; background:linear-gradient(180deg,var(--panel2),#140e1f); border:2px solid var(--line); border-radius:14px; padding:18px; cursor:pointer; transition:.15s; text-align:center;}
.classcard:hover{border-color:var(--gold); transform:translateY(-6px); box-shadow:0 10px 26px rgba(0,0,0,.6);}
.classcard .cicon{font-size:56px;} .classcard h3{color:var(--gold2);}
.flex{display:flex; gap:14px; flex-wrap:wrap; justify-content:center; align-items:center;}
.enemy-intent-big{font-size:16px; text-align:center; margin:8px 0; color:var(--gold2);}
.log{font-size:12px; color:var(--muted); max-height:90px; overflow-y:auto; text-align:center; line-height:1.5;}
.tag{font-size:10px; padding:1px 6px; border-radius:6px; background:#333; margin:0 2px;}
.shop-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(168px,1fr)); gap:14px; justify-content:center;}
.reward-card{transition:.15s;} .reward-card:hover{transform:translateY(-10px);}
.credits{position:fixed; bottom:10px; width:100%; text-align:center; color:var(--muted); font-size:11px;}
.banner{text-align:center; font-size:26px; font-weight:800; margin:10px 0;}
.hp-input{width:70px; padding:6px; font-size:15px; border-radius:6px; border:1px solid var(--line); background:var(--panel); color:var(--text);}
.section-head{display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;}
</style>
</head>
<body>
<div id="app"></div>
<div class="credits">Shadows of the Abyss — a roguelike deckbuilder</div>
<script>
"use strict";
// ============================================================
// SHADOWS OF THE ABYSS — game data & engine
// ============================================================
// ---------- Utility ----------
const rand = (a=1,b)=> a===b ? a : Math.floor(Math.random()*(b-a+1))+a;
const pick = arr => arr[Math.floor(Math.random()*arr.length)];
const shuffle = arr => {arr=arr.slice(); for(let i=arr.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1)); [arr[i],arr[j]]=[arr[j],arr[i]];} return arr;};
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const $ = id => document.getElementById(id);
let toastTimer;
function toast(msg){ const t=document.createElement('div'); t.className='toast'; t.innerHTML=msg; document.body.appendChild(t); clearTimeout(toastTimer); toastTimer=setTimeout(()=>t.remove(),1600); }
// ---------- Basic cards (all classes) ----------
const BASIC = {
strike:{name:"Strike",type:"Attack",cost:1,rarity:"Basic",up:"Deal 8 damage.",
play:g=>{ const c=g.curCard; damageSingle(g, g.target, 6 + (c.upgraded?2:0)); }},
defend:{name:"Defend",type:"Skill",cost:1,rarity:"Basic",up:"Gain 8 Block.",
play:g=>{ const c=g.curCard; g.player.gainBlock(5 + (c.upgraded?3:0)); }},
};
// ---------- Blood Reaper unique cards ----------
const BR = {
sanguine_strike:{name:"Sanguine Strike",type:"Attack",cost:1,rarity:"Common",up:"Deal 10 damage. Heal 3 HP.",
play:g=>{ const d=8+(g.curCard.upgraded?2:0); damageSingle(g,g.target,d); const heal=2+(g.curCard.upgraded?1:0); g.player.regenAdd(heal); }},
hemorrhage:{name:"Hemorrhage",type:"Attack",cost:1,rarity:"Common",up:"Deal 6 damage. Apply 4 Bleed.",
play:g=>{ const d=5+(g.curCard.upgraded?1:0); damageSingle(g,g.target,d); g.target.applyBleed(3+(g.curCard.upgraded?1:0)); }},
bloodletting:{name:"Bloodletting",type:"Attack",cost:1,rarity:"Uncommon",up:"Lose 2 HP. Deal 15 damage.",
play:g=>{ const lose=3-(g.curCard.upgraded?1:0); const dmg=12+(g.curCard.upgraded?3:0); g.player.loseHP(lose,false); damageSingle(g,g.target,dmg); }},
crimson_eruption:{name:"Crimson Eruption",type:"Attack",cost:2,rarity:"Rare",up:"Lose 2 HP. Deal 10 damage to ALL.",
play:g=>{ const dmg=8+(g.curCard.upgraded?2:0); g.player.loseHP(2,false); damageAll(g,dmg); }},
crimson_shield:{name:"Crimson Shield",type:"Skill",cost:1,rarity:"Common",up:"Gain 8 Block. Lose 2 HP.",
play:g=>{ const b=6+(g.curCard.upgraded?2:0); g.player.gainBlock(b); g.player.loseHP(2,false); }},
blood_ward:{name:"Blood Ward",type:"Skill",cost:1,rarity:"Common",up:"Gain 4 Block. Gain 2 Regeneration.",
play:g=>{ const b=3+(g.curCard.upgraded?1:0); g.player.gainBlock(b); g.player.regenAdd(1+(g.curCard.upgraded?1:0)); }},
sacrificial_barrier:{name:"Sacrificial Barrier",type:"Skill",cost:2,rarity:"Uncommon",up:"Gain 13 Block. Lose 5 HP.",
play:g=>{ const b=10+(g.curCard.upgraded?3:0); g.player.gainBlock(b); g.player.loseHP(5,false); }},
vital_surge:{name:"Vital Surge",type:"Skill",cost:0,rarity:"Rare",exhaust:true,up:"Gain Block equal to missing HP +3.",
play:g=>{ const missing=g.player.maxHP-g.player.hp; const gained=missing+(g.curCard.upgraded?3:0); g.player.gainBlock(gained); }},
life_tap:{name:"Life Tap",type:"Skill",cost:0,rarity:"Common",up:"Draw 3 cards. Lose 3 HP.",
play:g=>{ g.player.draw(2+(g.curCard.upgraded?1:0)); g.player.loseHP(3,false); }},
rend_flesh:{name:"Rend Flesh",type:"Skill",cost:1,rarity:"Uncommon",up:"Apply 7 Bleed to ALL. Lose 2 HP.",
play:g=>{ const bl=5+(g.curCard.upgraded?2:0); g.player.loseHP(2,false); g.enemies.forEach(e=>e.applyBleed(bl)); }},
soul_siphon:{name:"Soul Siphon",type:"Power",cost:2,rarity:"Uncommon",up:"Cost 1. Gain 1 Energy, lose 1 HP each turn.",
power:g=>{ /* handled in player turn */ }},
blood_pact:{name:"Blood Pact",type:"Power",cost:1,rarity:"Rare",up:"gain 2 Strength.",
power:g=>{}},
};
// ---------- Shadowmancer unique cards ----------
const SM = {
shadow_bolt:{name:"Shadow Bolt",type:"Attack",cost:1,rarity:"Common",up:"Deal 8 damage. Apply 2 Weak.",
play:g=>{ const d=6+(g.curCard.upgraded?2:0); damageSingle(g,g.target,d); g.target.applyWeak(1+(g.curCard.upgraded?1:0)); }},
grasp_of_despair:{name:"Grasp of Despair",type:"Attack",cost:1,rarity:"Common",up:"Deal 5 damage. Apply 2 Vulnerable.",
play:g=>{ const d=4+(g.curCard.upgraded?1:0); damageSingle(g,g.target,d); g.target.applyVulnerable(1+(g.curCard.upgraded?1:0)); }},
nightmare_lash:{name:"Nightmare Lash",type:"Attack",cost:2,rarity:"Uncommon",up:"Deal 9 damage. If Weak, deal 9 more.",
play:g=>{ const c=g.curCard; const base=7+(c.upgraded?2:0); let d=base; if(g.target.hasWeak()) d+= (c.upgraded?9:7); damageSingle(g,g.target,d); }},
eternal_darkness:{name:"Eternal Darkness",type:"Attack",cost:2,rarity:"Rare",up:"Deal 4 damage to a random enemy 3 times.",
play:g=>{ const d=g.curCard.upgraded?4:3; for(let i=0;i<3;i++){ const e=pick(g.enemies); damageSingle(g,e,d); } }},
cloak_of_shadows:{name:"Cloak of Shadows",type:"Skill",cost:1,rarity:"Common",up:"Gain 7 Block. Draw 1 card.",
play:g=>{ const b=5+(g.curCard.upgraded?2:0); g.player.gainBlock(b); g.player.draw(1); }},
haunting_mist:{name:"Haunting Mist",type:"Skill",cost:1,rarity:"Common",up:"Gain 9 Block. Apply 1 Weak to ALL.",
play:g=>{ const b=7+(g.curCard.upgraded?2:0); g.player.gainBlock(b); g.enemies.forEach(e=>e.applyWeak(1)); }},
phantom_armor:{name:"Phantom Armor",type:"Skill",cost:1,rarity:"Uncommon",up:"Gain 10 Block. Next turn gain 3 Dexterity.",
play:g=>{ const b=8+(g.curCard.upgraded?2:0); g.player.gainBlock(b); g.player.dexBuff=(g.player.dexBuff||0)+(g.curCard.upgraded?3:2); }},
whispering_shroud:{name:"Whispering Shroud",type:"Skill",cost:1,rarity:"Rare",up:"Gain 5 Block. Next turn gain 5 Block.",
play:g=>{ const b=4+(g.curCard.upgraded?1:0); g.player.gainBlock(b); g.player.blockNextTurn += b; }},
shadowform:{name:"Shadowform",type:"Skill",cost:2,rarity:"Uncommon",exhaust:true,up:"Gain 3 Intangible.",
play:g=>{ g.player.intangible += 2+(g.curCard.upgraded?1:0); }},
curse_of_misfortune:{name:"Curse of Misfortune",type:"Skill",cost:1,rarity:"Common",up:"Enemy loses 4 Strength this turn. Apply 2 Vulnerable.",
play:g=>{ const c=g.curCard; const loss=4+(c.upgraded?1:0); g.target.tmpStrengthLoss=(g.target.tmpStrengthLoss||0)+loss; g.target.applyVulnerable(1+(c.upgraded?1:0)); }},
dark_ritual:{name:"Dark Ritual",type:"Skill",cost:0,rarity:"Uncommon",up:"Gain 2 Energy. Shuffle a Dazed into discard pile.",
play:g=>{ g.player.energy += 2; if(g.curCard.upgraded){ g.player.addDazedToDiscard(); } else { g.player.addDazedToDraw(); } }},
umbral_mastery:{name:"Umbral Mastery",type:"Power",cost:2,rarity:"Rare",up:"Apply 2 Weak each turn.",
power:g=>{}},
};
// ---------- Hellforged unique cards ----------
const HF = {
firebolt:{name:"Firebolt",type:"Attack",cost:1,rarity:"Common",up:"Deal 9 damage. Apply 3 Burn.",
play:g=>{ const d=7+(g.curCard.upgraded?2:0); damageSingle(g,g.target,d); g.target.applyBurn(2+(g.curCard.upgraded?1:0)); }},
hellfire_blast:{name:"Hellfire Blast",type:"Attack",cost:2,rarity:"Uncommon",up:"Deal 12 damage to ALL.",
play:g=>{ damageAll(g, 9+(g.curCard.upgraded?3:0)); }},
searing_strike:{name:"Searing Strike",type:"Attack",cost:1,rarity:"Common",up:"Deal 8 damage. Gain 1 Energy next turn.",
play:g=>{ const d=6+(g.curCard.upgraded?2:0); damageSingle(g,g.target,d); g.player.energyNextTurn += 1; }},
immolate:{name:"Immolate",type:"Attack",cost:2,rarity:"Rare",exhaust:true,up:"Deal 16 damage.",
play:g=>{ damageSingle(g,g.target, 12+(g.curCard.upgraded?4:0)); }},
obsidian_skin:{name:"Obsidian Skin",type:"Skill",cost:1,rarity:"Common",up:"Gain 10 Block.",
play:g=>{ g.player.gainBlock(7+(g.curCard.upgraded?3:0)); }},
flame_ward:{name:"Flame Ward",type:"Skill",cost:1,rarity:"Common",up:"Gain 5 Block. Apply 3 Burn to random enemy.",
play:g=>{ const b=5+(g.curCard.upgraded?1:0); g.player.gainBlock(b); if(g.enemies.length){ const e=pick(g.enemies); e.applyBurn(g.curCard.upgraded?3:2); } }},
demonic_resilience:{name:"Demonic Resilience",type:"Skill",cost:1,rarity:"Uncommon",up:"Gain 5 Block. Heal 3 HP.",
play:g=>{ g.player.gainBlock(4+(g.curCard.upgraded?1:0)); g.player.gainHP(2+(g.curCard.upgraded?1:0)); }},
brimstone_shield:{name:"Brimstone Shield",type:"Skill",cost:1,rarity:"Rare",up:"Gain 4 Block. Gain 3 Strength this turn.",
play:g=>{ const b=3+(g.curCard.upgraded?1:0); g.player.gainBlock(b); g.player.strBuff=(g.player.strBuff||0)+(g.curCard.upgraded?3:2); }},
summon_imp:{name:"Summon Imp",type:"Skill",cost:1,rarity:"Common",exhaust:true,up:"Add Imp Claw+ to hand.",
play:g=>{ const claw = g.curCard.upgraded ? makeToken("imp_claw_plus") : makeToken("imp_claw"); g.player.hand.push(claw); }},
infernal_pact:{name:"Infernal Pact",type:"Skill",cost:0,rarity:"Uncommon",up:"Lose 1 HP. Gain 2 Energy. Add Imp Claw+.",
play:g=>{ const lose=g.curCard.upgraded?1:2; g.player.loseHP(lose,false); g.player.energy += 2; const claw = g.curCard.upgraded ? makeToken("imp_claw_plus") : makeToken("imp_claw"); g.player.hand.push(claw); }},
hellfire_aura:{name:"Hellfire Aura",type:"Power",cost:2,rarity:"Rare",up:"Deal 5 damage to ALL enemies each turn.",
power:g=>{}},
soul_burn:{name:"Soul Burn",type:"Skill",cost:1,rarity:"Uncommon",up:"Apply 10 Burn to enemy. Draw 1 card.",
play:g=>{ const b=g.curCard.upgraded?10:8; g.target.applyBurn(b); g.player.draw(1); }},
};
// ---------- Colorless cards (shop only) ----------
const COLORLESS = {
flash_of_insight:{name:"Flash of Insight",type:"Skill",cost:1,rarity:"Common",up:"Draw 3 cards.",
play:g=>{ g.player.draw(2+(g.curCard.upgraded?1:0)); }},
iron_will:{name:"Iron Will",type:"Skill",cost:1,rarity:"Common",up:"Gain 11 Block.",
play:g=>{ g.player.gainBlock(8+(g.curCard.upgraded?3:0)); }},
quick_slash:{name:"Quick Slash",type:"Attack",cost:1,rarity:"Common",up:"Deal 9 damage.",
play:g=>{ damageSingle(g,g.target, 7+(g.curCard.upgraded?2:0)); }},
potent_brew:{name:"Potent Brew",type:"Skill",cost:0,rarity:"Uncommon",exhaust:true,up:"Gain 2 Energy.",
play:g=>{ g.player.energy += 2+(g.curCard.upgraded?1:0); }},
};
// ---------- Curses / Status ----------
function makeStatus(type){
const base = {status:true, ethereal:true, exhaust:true, name: type==="dazed"?"Dazed":(type==="wound"?"Wound":"Doubt"), type:"Status", cost:0, rarity:"Curse", upgraded:false, isStatus:true};
if(type==="dazed"){ return {...base, play:()=>{} }; }
if(type==="wound"){ return {...base, play:()=>{} }; }
if(type==="doubt"){ return {...base, ethereal:false, play:()=>{}, doubt:true }; }
return base;
}
function makeToken(t){
if(t==="imp_claw"){ return {name:"Imp Claw",type:"Attack",cost:0,rarity:"Status",exhaust:true,upgraded:false,isToken:true, play:g=>damageSingle(g,g.target,4)}; }
if(t==="imp_claw_plus"){ return {name:"Imp Claw+",type:"Attack",cost:0,rarity:"Status",exhaust:true,upgraded:true,isToken:true, play:g=>damageSingle(g,g.target,6)}; }
}
// ---------- Class definitions ----------
const CLASSES = {
blood_reaper:{
name:"Blood Reaper", icon:"🧛", color:"#c0392b",
desc:"Sacrifice HP for power, life steal, and Bleed.",
deck:()=>{ const d=[]; for(let i=0;i<5;i++) d.push(makeCard("strike")); for(let i=0;i<4;i++) d.push(makeCard("defend")); d.push(makeCard("sanguine_strike")); return d; },
pool:BR,
},
shadow_mancer:{
name:"Shadowmancer", icon:"🧙", color:"#9b59b6",
desc:"Debuffs (Weak, Vulnerable), Intangible, and card manipulation.",
deck:()=>{ const d=[]; for(let i=0;i<5;i++) d.push(makeCard("strike")); for(let i=0;i<4;i++) d.push(makeCard("defend")); d.push(makeCard("shadow_bolt")); return d; },
pool:SM,
},
hellforged:{
name:"Hellforged", icon:"👹", color:"#e67e22",
desc:"Fire damage, Burn, summoning Imps, energy manipulation.",
deck:()=>{ const d=[]; for(let i=0;i<5;i++) d.push(makeCard("strike")); for(let i=0;i<4;i++) d.push(makeCard("defend")); d.push(makeCard("firebolt")); return d; },
pool:HF,
},
};
// ---------- Card factory ----------
const SINGLE_ATTACK = new Set(["strike","sanguine_strike","hemorrhage","bloodletting","shadow_bolt","grasp_of_despair","nightmare_lash","firebolt","searing_strike","immolate","quick_slash","soul_burn"]);
const ALL_TARGET = new Set(["crimson_eruption","hellfire_blast"]);
function makeCard(id){
let def;
if(BASIC[id]) def={...BASIC[id]};
else if(BR[id]) def={...BR[id]};
else if(SM[id]) def={...SM[id]};
else if(HF[id]) def={...HF[id]};
else if(COLORLESS[id]) def={...COLORLESS[id]};
const c = {id, name:def.name, type:def.type, cost:def.cost, rarity:def.rarity, upgraded:false,
exhaust:def.exhaust||false, ethereal:def.ethereal||false, status:def.status||false, isStatus:!!def.isStatus, isToken:!!def.isToken,
doubt:!!def.doubt,
desc:def.up, play:def.play, power:def.power};
if(SINGLE_ATTACK.has(id)) c.targetType="enemy";
else if(ALL_TARGET.has(id)) c.targetType="all";
return c;
}
// ---------- Relics ----------
const RELICS = {
bloodstone:{name:"Bloodstone",rarity:"Common",desc:"Whenever you lose HP from a card, heal 1 HP.",
onCardHPLoss:(g,n)=>{ g.player.gainHP(1); }},
demon_horns:{name:"Demon Horns",rarity:"Class",desc:"Start combat with a random Imp Claw in hand (once per combat).",
onCombatStart:g=>{ g.player.hand.push(makeToken("imp_claw")); }},
shadow_cloak:{name:"Shadow Cloak",rarity:"Rare",desc:"Start combat with 1 Intangible.",
onCombatStart:g=>{ g.player.intangible += 1; }},
cursed_amulet:{name:"Cursed Amulet",rarity:"Common",desc:"Start combat applying 1 Weak to ALL enemies.",
onCombatStart:g=>{ g.enemies.forEach(e=>e.applyWeak(1)); }},
tome_of_dark_rituals:{name:"Tome of Dark Rituals",rarity:"Uncommon",desc:"Whenever you play a Power card, draw 1 card.",
onPlayPower:g=>{ g.player.draw(1); }},
bleeding_edge:{name:"Bleeding Edge",rarity:"Common",desc:"Start of your turn, apply 1 Bleed to a random enemy.",
onPlayerTurnStart:g=>{ if(g.enemies.length) pick(g.enemies).applyBleed(1); }},
soul_ember:{name:"Soul Ember",rarity:"Rare",desc:"Whenever you apply Burn to an enemy, deal 2 additional damage.",
onApplyBurn:(g,amt)=>amt+2},
eternal_hourglass:{name:"Eternal Hourglass",rarity:"Uncommon",desc:"Start of turn, if no cards in hand, gain 1 Energy.",
onPlayerTurnStart:g=>{ if(g.player.hand.length===0) g.player.energy += 1; }},
sacrificial_dagger:{name:"Sacrificial Dagger",rarity:"Class",desc:"Your first Attack that costs HP each combat costs 1 less HP.",
onCardHPLoss:(g,n)=>{ if(!g.player._daggerUsed){ g.player._daggerUsed=true; return n-1; } return n; }},
vial_of_shadows:{name:"Vial of Shadows",rarity:"Rare",desc:"Obtained: remove a card from your deck (one-time).",
onObtain:g=>{ askRemoveFromDeck("Vial of Shadows: remove a card from your deck."); }},
ashen_armor:{name:"Ashen Armor",rarity:"Common",desc:"At the end of combat, heal 6 HP.",
onCombatEnd:g=>{ g.player.gainHP(6); }},
greedy_imp:{name:"Greedy Imp",rarity:"Common",desc:"Enemies drop 10 extra Gold.",
goldBonus:10},
};
// ---------- Potions ----------
const POTIONS = {
blood_vial:{name:"Blood Vial",icon:"🩸",effect:g=>{ g.player.gainHP(15); }},
firebomb:{name:"Firebomb",icon:"💥",effect:g=>{ damageAll(g,10); }},
dex_tonic:{name:"Dex Tonic",icon:"⚗️",effect:g=>{ g.player.dexBuff=(g.player.dexBuff||0)+3; }},
energy_elixir:{name:"Energy Elixir",icon:"🔋",effect:g=>{ g.player.energy += 2; }},
cursed_brew:{name:"Cursed Brew",icon:"🫗",effect:g=>{ g.enemies.forEach(e=>e.applyVulnerable(3)); }},
bleeding_flask:{name:"Bleeding Flask",icon:"🩸",effect:g=>{ if(g.enemies.length) pick(g.enemies).applyBleed(6); }},
imp_in_a_bottle:{name:"Imp-in-a-Bottle",icon:"🧪",effect:g=>{ g.player.hand.push(makeToken("imp_claw_plus")); }},
smoke_bomb:{name:"Smoke Bomb",icon:"💨",effect:g=>{ endEscapeCombat(); }},
};
// ---------- Enemy bestiary ----------
// Intent types handled by combat engine:
// attack: value = damage
// multi: value = [d1,d2,...] each reduced by block
// attack2: value = [damage, debuffCount] applies debuff to player (type depends on name)
// defend: value = block
// shieldbash: value = [damage, block]
// shieldbash2: value = [block, heal]
// drain: value = [damage, heal]
// buff: value varies (string or number)
// debuff: value varies
// summon: value = enemy id to summon
function makeEnemy(id, name, hp, icon, pattern){
return {
id, name, maxHP:hp, hp, block:0, icon, pattern,
str:0, dex:0, intangible:0, tmpStrengthLoss:0,
_intentTurn:0,
_weak:0,_weakTimer:0, _vuln:0,_vulnTimer:0, _bleed:0, _burn:0, _regen:0,
hasWeak(){return this._weak>0;}, applyWeak(n){this._weak=(this._weak||0)+n;},
hasVulnerable(){return this._vuln>0;}, applyVulnerable(n){this._vuln=(this._vuln||0)+n;},
hasBleed(){return this._bleed>0;}, applyBleed(n){this._bleed=(this._bleed||0)+n;},
hasBurn(){return this._burn>0;}, applyBurn(n){this._burn=(this._burn||0)+n;},
hasRegen(){return this._regen>0;}, applyRegen(n){this._regen=(this._regen||0)+n;},
gainBlock(n){this.block+=n;},
intent(){ return this.pattern[this._intentTurn % this.pattern.length]; },
startNewCombat(){ this.hp=this.maxHP; this.block=0; this.str=0; this.dex=0; this.intangible=0; this._intentTurn=0; this._weak=0;this._weakTimer=0;this._vuln=0;this._vulnTimer=0;this._bleed=0;this._burn=0;this._regen=0; this.tmpStrengthLoss=0; }
};
}
// Intent helpers
const itAttack=(n)=>({type:"attack",value:n, text:"Attack "+n});
const itDefend=(n)=>({type:"defend",value:n, text:"Defend "+n});
const itBuff=(t,v)=>({type:"buff",value:v, text:t});
const itDebuff=(t,v)=>({type:"debuff",value:v, text:t});
const itMulti=(arr)=>({type:"multi",value:arr, text:arr.join("×")+" Attack"});
const itAttack2=(arr)=>({type:"attack2",value:arr, text:"Attack "+arr[0]+", Apply "+arr[1]});
const itSummon=(t)=>({type:"summon",value:t||null, text:"Summon"});
const itHeal=(n)=>({type:"buff",value:"heal "+n, text:"Heal "+n});
// ---- Act 1 ----
const ENEMY_DEF = {
skeleton:{name:"Skeleton Warrior",hp:22,icon:"💀",pattern:[itAttack(6),itAttack(6),itDefend(8)]},
zombie:{name:"Zombie Brute",hp:28,icon:"🧟",pattern:[itAttack(8),itDebuff("Apply 2 Weak",2),itAttack(8)]},
cultist:{name:"Dark Cultist",hp:20,icon:"🧑🦰",pattern:[itBuff("Gain 3 Strength",3),itAttack(5),itAttack(5)]},
ratswarm:{name:"Rat Swarm",hp:15,icon:"🐀",pattern:[itMulti([3,3]),itMulti([3,3])]},
wraith:{name:"Wraith",hp:30,icon:"👻",pattern:[itAttack2([7,1]),itAttack(7),itDefend(10)]},
beast:{name:"Corrupted Beast",hp:38,icon:"🐗",pattern:[itAttack(12),itBuff("Gain 5 Block & Vulnerable",1),itAttack(12)]},
thorned:{name:"Thorned Horror",hp:25,icon:"🌿",pattern:[itMulti([4,4,4]),itDebuff("Apply 3 Bleed",3),itAttack(8)]},
witchdoctor:{name:"Witch Doctor",hp:22,icon:"🧙♀️",pattern:[itBuff("Gain 5 Block & 2 Regen",1),itAttack2([6,2]),itAttack(6)]},
hellhound:{name:"Hellhound",hp:26,icon:"🐕",pattern:[itAttack2([8,2]),itAttack(8),itDefend(9)]},
shadowdemon:{name:"Shadow Demon",hp:30,icon:"😈",pattern:[itAttack2([7,1]),itAttack(7),itBuff("Gain Intangible",1)]},
infernalimp:{name:"Infernal Imp",hp:18,icon:"🦇",pattern:[itAttack(5),itAttack(5),itSummon("infernalimp")]},
abyssalcultist:{name:"Abyssal Cultist",hp:24,icon:"🕯️",pattern:[itBuff("Gain 2 Strength to all",2),itAttack(7),itAttack(7)]},
tombguardian:{name:"Tomb Guardian",hp:42,icon:"🗿",elite:true,pattern:[{type:"shieldbash",value:[6,8],text:"Attack 6, Shield 8"},itMulti([4,4,4]),itAttack(12)]},
bloodwitch:{name:"Blood Witch",hp:36,icon:"🩰",elite:true,pattern:[{type:"drain",value:[5,4],text:"Drain 5 (heal 4)"},itDebuff("Vulnerable & Weak",1),{type:"drain",value:[7,4],text:"Blood Ritual 7 (heal 4)"}]},
treant:{name:"Treant Protector",hp:50,icon:"🌳",elite:true,pattern:[{type:"shieldbash",value:[9,6],text:"Attack 9, Shield 6"},itDebuff("Apply 2 Vulnerable",2),itMulti([10,10])]},
plagueshaman:{name:"Plague Shaman",hp:42,icon:"☠️",elite:true,pattern:[itDebuff("Apply 4 Poison",4),itSummon("plagerat"),itAttack(8)]},
flamespawn:{name:"Flamespawn",hp:48,icon:"🔥",elite:true,pattern:[itAttack2([9,3]),itBuff("Gain 2 Strength",2),itMulti([8,8])]},
dreadknight:{name:"Dread Knight",hp:55,icon:"⚔️",elite:true,pattern:[itAttack(12),itDebuff("Apply 3 Bleed",3),{type:"shieldbash2",value:[12,4],text:"Defend 12, Heal 4"}]},
plagerat:{name:"Plague Rat",hp:18,icon:"🐀",pattern:[itAttack(5)]},
shadow_thief:{name:"Shadow Thief",hp:40,icon:"🗡️",elite:true,pattern:[itAttack(8),itDebuff("Apply 1 Weak",1),itAttack(10)]},
};
function makeEnemyInstance(id){
const d=ENEMY_DEF[id]; if(!d) return null;
return makeEnemy(id, d.name, d.hp, d.icon, d.pattern);
}
// Boss definitions (phase-based)
const BOSSES = {
lichking:{name:"Lich King",hp:65,icon:"🧟♂️",boss:true,act:1},
eldritchtreant:{name:"Eldritch Treant",hp:75,icon:"🌲",boss:true,act:2},
archdemon:{name:"Archdemon",hp:85,icon:"👿",boss:true,act:3},
};
// Boss intent patterns (phase-based). Returns intent for a given phase & turn.
function bossIntent(name, isPhase2, turn){
if(name==="lichking"){
if(isPhase2){ const p=[itAttack(10),itAttack(10),itDefend(15)]; return p[turn%3]; }
return [itSummon("skeleton"),itAttack(8),itDefend(12)][turn%3];
}
if(name==="eldritchtreant"){
if(isPhase2){ const p=[{type:"attack2",value:[14,1],text:"Attack 14, Apply 1 Vulnerable"},{type:"shieldbash",value:[20,5],text:"Defend 20, Heal 5"},{type:"attack2",value:[14,1],text:"Attack 14, Apply 1 Vulnerable"}]; return p[turn%3]; }
return [itAttack2([10,2]),itBuff("Defend 15 & Regen 2",1),itMulti([8,8])][turn%3];
}
if(name==="archdemon"){
if(isPhase2){ const p=[itAttack(16),itMulti([7,7,7]),itAttack(16)]; return p[turn%3]; }
return [itSummon("hellhound"),itAttack2([12,2]),{type:"shieldbash2",value:[15,3],text:"Defend 15, Gain 3 Strength"}][turn%3];
}
return itAttack(0);
}
function makeBossInstance(id){
const d=BOSSES[id];
const e=makeEnemy(id, d.name, d.hp, d.icon, []);
e.isBoss=true; e.act=d.act; e._intentTurn=0;
e.getBossIntent=()=>bossIntent(id, e.phase2===true, e._intentTurn);
return e;
}
// ---------- Map definitions (per act) ----------
// Each floor: {type, options:[{name, enemies:[...], icon}]}
// type: enemy | elite | rest | shop | event | boss
// For elite: {type:"elite", name, icon, group:[...], alt:[...]}
const ACTS = [
{
name:"Act 1 — The Crypts",
floors:[
{type:"enemy", name:"Skeleton", icon:"💀", group:["skeleton"]},
{type:"enemy", name:"Zombie Brute", icon:"🧟", group:["zombie"]},
{type:"enemy", name:"Dark Cultist", icon:"🧑🦰", group:["cultist"]},
{type:"event", name:"The Forgotten Altar", icon:"🕯️"},
{type:"shop", name:"Shop", icon:"🏪"},
{type:"enemy", name:"Rat Swarm", icon:"🐀", group:["ratswarm"]},
{type:"rest", name:"Rest", icon:"🔥"},
{type:"enemy", name:"Skeleton Group", icon:"💀", group:["skeleton","skeleton"]},
{type:"enemy", name:"Zombie Brute", icon:"🧟", group:["zombie"]},
{type:"elite", name:"Tomb Guardian", icon:"🗿", group:["tombguardian"], alt:["skeleton","ratswarm"]},
{type:"event", name:"Shadowy Peddler", icon:"🎭"},
{type:"enemy", name:"Cultist & Rat", icon:"🧑🦰", group:["cultist","ratswarm"]},
{type:"elite", name:"Blood Witch", icon:"🩰", group:["bloodwitch"], alt:["zombie","skeleton"]},
{type:"enemy", name:"Cultist Group", icon:"🧑🦰", group:["cultist","cultist"]},
{type:"rest", name:"Rest", icon:"🔥"},
{type:"boss", name:"Lich King", icon:"🧟♂️", group:["lichking"]},
],
},
{
name:"Act 2 — Blighted Woods",
floors:[
{type:"enemy", name:"Wraith", icon:"👻", group:["wraith"]},
{type:"enemy", name:"Corrupted Beast", icon:"🐗", group:["beast"]},
{type:"enemy", name:"Thorned Horror", icon:"🌿", group:["thorned"]},
{type:"event", name:"Cursed Spring", icon:"⛲"},
{type:"shop", name:"Shop", icon:"🏪"},
{type:"enemy", name:"Witch Doctor", icon:"🧙♀️", group:["witchdoctor"]},
{type:"rest", name:"Rest", icon:"🔥"},
{type:"enemy", name:"Corrupted Beast", icon:"🐗", group:["beast"]},
{type:"enemy", name:"Wraith", icon:"👻", group:["wraith"]},
{type:"elite", name:"Treant Protector", icon:"🌳", group:["treant"], alt:["wraith","beast"]},
{type:"event", name:"Restless Dead", icon:"👁️"},
{type:"enemy", name:"Beast & Wraith", icon:"🐗", group:["beast","wraith"]},
{type:"elite", name:"Plague Shaman", icon:"☠️", group:["plagueshaman"], alt:["beast","wraith"]},
{type:"enemy", name:"Witch Doctor Group", icon:"🧙♀️", group:["witchdoctor","witchdoctor"]},
{type:"rest", name:"Rest", icon:"🔥"},
{type:"boss", name:"Eldritch Treant", icon:"🌲", group:["eldritchtreant"]},
],
},
{
name:"Act 3 — Abyssal Spire",
floors:[
{type:"enemy", name:"Hellhound", icon:"🐕", group:["hellhound"]},
{type:"enemy", name:"Shadow Demon", icon:"😈", group:["shadowdemon"]},
{type:"enemy", name:"Infernal Imp", icon:"🦇", group:["infernalimp"]},
{type:"event", name:"Impish Gambler", icon:"🃏"},
{type:"shop", name:"Shop", icon:"🏪"},
{type:"enemy", name:"Abyssal Cultist", icon:"🕯️", group:["abyssalcultist"]},
{type:"rest", name:"Rest", icon:"🔥"},
{type:"enemy", name:"Hellhound & Imp", icon:"🐕", group:["hellhound","infernalimp"]},
{type:"enemy", name:"Shadow Demon", icon:"😈", group:["shadowdemon"]},
{type:"elite", name:"Flamespawn", icon:"🔥", group:["flamespawn"], alt:["hellhound","shadowdemon"]},
{type:"event", name:"Ancient Library", icon:"📚"},
{type:"enemy", name:"Cultist & Imp", icon:"🕯️", group:["abyssalcultist","infernalimp"]},
{type:"elite", name:"Dread Knight", icon:"⚔️", group:["dreadknight"], alt:["hellhound","shadowdemon"]},
{type:"enemy", name:"Abyssal Cultist Group", icon:"🕯️", group:["abyssalcultist","abyssalcultist"]},
{type:"rest", name:"Rest", icon:"🔥"},
{type:"boss", name:"Archdemon", icon:"👿", group:["archdemon"]},
],
},
];
// ---------- Events ----------
const EVENTS = [
{
id:"altar", name:"The Forgotten Altar", icon:"🕯️",
text:"A blood-stained altar hums with forbidden power.",
choices:[
{label:"Offer 15 HP → Rare relic", run:g=>{ g.player.loseHP(15,false); const r=makeRareRelic(); grantRelic(r); g.finishEvent(); }},
{label:"Pray → Heal 10, gain Doubt", run:g=>{ g.player.gainHP(10); g.player.deck.push(makeStatus("doubt")); g.finishEvent(); }},
{label:"Leave", run:g=>{ g.finishEvent(); }},
],
},
{
id:"peddler", name:"Shadowy Peddler", icon:"🎭",
text:"A hooded figure offers you a trade.",
choices:[
{label:"Trade a card → 100 Gold", run:g=>{ askRemoveFromDeck("Shadowy Peddler: trade a card for 100 gold."); g.player.gold += 100; }},
{label:"Buy wares → -50 Gold, Potion + Relic", run:g=>{ if(g.player.gold<50){ toast("Not enough gold."); return; } g.player.gold-=50; grantPotion(randPotion()); grantRelic(makeCommonRelic()); g.finishEvent(); }},
{label:"Attack → Combat (+75 Gold on win)", run:g=>{ startCombat(["shadow_thief"], "Shadow Thief", true, ()=>{g.player.gold+=75;}); }},
],
},
{
id:"spring", name:"Cursed Spring", icon:"⛲",
text:"A spring bubbles with dark, tempting water.",
choices:[
{label:"Drink → -10 HP, remove all Curses", run:g=>{ g.player.loseHP(10,false); g.player.deck = g.player.deck.filter(c=>!c.status||c.isStatus); g.finishEvent(); }},
{label:"Bottle it → Cursed Brew", run:g=>{ grantPotion("cursed_brew"); g.finishEvent(); }},
{label:"Leave", run:g=>{ g.finishEvent(); }},
],
},
{
id:"restlessdead", name:"Restless Dead", icon:"👁️",
text:"Ghosts offer you a bargain in raspy whispers.",
choices:[
{label:"Accept → -20 HP, upgrade 2 cards", run:g=>{ g.player.loseHP(20,false); upgradeRandomCards(g.player.deck,2); g.finishEvent(); }},
{label:"Refuse", run:g=>{ g.finishEvent(); }},
],
},
{
id:"gambler", name:"Impish Gambler", icon:"🃏",
text:"A grinning imp challenges you to a game of chance.",
choices:[
{label:"Bet 30 Gold → 50% double or lose", run:g=>{ if(g.player.gold<30){ toast("Not enough gold."); return; } g.player.gold-=30; if(Math.random()<0.5){ g.player.gold+=60; toast("🎉 You won 60 gold!"); } else toast("💥 You lost 30 gold."); g.finishEvent(); }},
{label:"Wager a card → remove random card", run:g=>{ if(g.player.deck.length===0){ toast("No cards to wager."); return; } const c=pick(g.player.deck.slice()); g.player.deck = g.player.deck.filter(x=>x!==c); if(c.status){ g.player.gold+=100; toast("It was a Curse! +100 gold!"); } else toast("You lost "+c.name+"."); g.finishEvent(); }},
{label:"Leave", run:g=>{ g.finishEvent(); }},
],
},
{
id:"library", name:"Ancient Library", icon:"📚",
text:"Dusty tomes contain forgotten knowledge.",
choices:[
{label:"Study → +1 Energy (next combat), -5 HP", run:g=>{ g.player.loseHP(5,false); g.player.tempEnergyNextCombat = (g.player.tempEnergyNextCombat||0)+1; g.finishEvent(); }},
{label:"Read forbidden page → Power + Doubt", run:g=>{ const pool=g.player.classDef.pool; const keys=Object.keys(pool); const id=pick(keys); const card=makeCard(id); card.cost=0; g.player.deck.push(card); g.player.deck.push(makeStatus("doubt")); g.finishEvent(); }},
{label:"Leave", run:g=>{ g.finishEvent(); }},
],
},
];
// ============================================================
// GAME STATE & COMBAT ENGINE
// ============================================================
const state = {
mode:"classselect",
player:null,
enemies:[],
actIndex:0,
floorIndex:1, // index into ACTS[actIndex].floors (floor 2 = index 0)
gold:99,
combatLabel:"",
combatSpecial:false,
goldBonus:0,
curCard:null,
targeting:null,
rewardQueue:null,
won:false,
_pendingCb:null,
};
// ---- Player object ----
function newPlayer(classId){
const def = CLASSES[classId];
const p = {
classId, classDef:def,
deck: def.deck(),
draw:[], discard:[], hand:[], exhaust:[],
maxHP:70, hp:70, block:0, energy:0,
dexBuff:0, strBuff:0, regen:0, intangible:0,
strPower:0,
// debuffs (player)
_weak:0,_weakTimer:0, _vuln:0,_vulnTimer:0, _bleed:0, _burn:0, _poison:0,
energyNextTurn:0,
blockNextTurn:0,
tempEnergyNextCombat:0,
potions:[],
relics:[],
_daggerUsed:false,
_soulSiphon:false,
};
// Player status effects
p.applyWeak=n=>{ p._weak=(p._weak||0)+n; };
p.applyVulnerable=n=>{ p._vuln=(p._vuln||0)+n; };
p.applyBleed=n=>{ p._bleed=(p._bleed||0)+n; };
p.applyBurn=n=>{ p._burn=(p._burn||0)+n; };
p.applyPoison=n=>{ p._poison=(p._poison||0)+n; };
p.gainAdd=n=>{ p.dexBuff=(p.dexBuff||0)+n; };
p.gainBlock=n=>{ p.block += n + (p.dexBuff||0); };
p.regenAdd=n=>{ p.regen=(p.regen||0)+n; };
return p;
}
function deckNoExhaust(p){ return p.deck.filter(c=>!c.exhaust && !c.isStatus); }
// ---- Deck shuffling ----
function buildDrawPile(p){
const cards = deckNoExhaust(p).map(c=>({...c}));
// remove status/doubt from draw pile (they shouldn't be there normally)
p.draw = shuffle(cards);
p.discard = []; p.exhaust = []; p.hand = [];
}
function drawCards(p, n){
for(let i=0;i<n;i++){
if(p.draw.length===0){
if(p.discard.length===0) break;
p.draw = shuffle(p.discard);
p.discard = [];
}
if(p.draw.length>0) p.hand.push(p.draw.pop());
}
}
function addDazedToDraw(){ state.player.draw.push(makeStatus("dazed")); }
function addDazedToDiscard(){ state.player.discard.push(makeStatus("dazed")); }
// ---- HP ----
function gainHP(p, n){ p.hp = Math.min(p.maxHP, p.hp+n); }
function loseHP(p, amount, fromCard){
let amt = amount;
if(p.intangible>0) amt = 1; // Intangible reduces ALL HP loss to 1
if(fromCard){
// Sacrificial Dagger
if(p.relics.includes("sacrificial_dagger") && !p._daggerUsed){ amt = Math.max(0, amt-1); p._daggerUsed = true; }
amt = Math.max(0, Math.floor(amt));
p.hp = Math.max(0, p.hp - amt);
// Bloodstone
if(p.relics.includes("bloodstone")) gainHP(p,1);
// Blood Pact
if(p.relics.includes("blood_pact")) p.strPower += (RELICS.blood_pact.upgraded?2:1);
}
}
// ---- Damage ----
function damageSingle(g, enemy, baseDmg){
if(!enemy || enemy.hp<=0) return;
const p = g.player;
let dmg = baseDmg + p.strBuff + p.strPower;
if(p._weak>0) dmg = Math.floor(dmg*0.75);
if(enemy._vuln>0) dmg = Math.floor(dmg*1.5);
if(enemy.block>0) dmg = Math.max(0, dmg - enemy.block);
enemy.hp = Math.max(0, enemy.hp - dmg);
}
function damageAll(g, baseDmg){
g.enemies.forEach(e=>{ if(e.hp>0) damageSingle(g,e,baseDmg); });
}
function applyBurn(g, enemy, n){
enemy.applyBurn(n);
if(g.player.relics.includes("soul_ember")) enemy.hp = Math.max(0, enemy.hp - 2);
}
function applyBleedOnApply(g, enemy, n){ enemy.applyBleed(n); }
// ---- Targetable helper ----
function validTargets(g, type){
if(type==="enemy") return g.enemies.filter(e=>e.hp>0);
if(type==="all") return g.enemies.filter(e=>e.hp>0);
return [];
}
// ============================================================
// COMBAT FLOW
// ============================================================
function canPlayCard(c){
if(c.status || c.isStatus){
if(c.doubt) return true; // doubt can be played? It's unplayable per spec but stays. Actually "Unplayable". Keep unplayable.
return false;
}
if(c.cost > state.player.energy) return false;
if(c.cost>0) return true;
return true; // 0-cost cards always playable (except status)
}
function startCombat(enemyIds, label, special, cb){
state.mode = "combat";
state.enemies = enemyIds.map(id=> makeEnemyInstance(id));
if(special && enemyIds[0].indexOf!==-1){}
state.combatLabel = label;
state.combatSpecial = !!special;
state._pendingCb = cb || null;
state.goldBonus = state.player.relics.includes("greedy_imp")?10:0;
const p = state.player;
p.hp = Math.min(p.maxHP, p.hp); // ensure at max
p.block=0; p.energy=0; p.dexBuff=0; p.strBuff=0; p.regen=0; p.intangible=0;
p._weak=0;p._weakTimer=0;p._vuln=0;p._vulnTimer=0;p._bleed=0;p._burn=0;p._poison=0;
p._daggerUsed=false; p._soulSiphon=false;
p.strPower=0; p.energyNextTurn=0; p.blockNextTurn=0;
p._umbralMastery=0; p._hellfireAura=false; p._hellfireAuraUpgraded=false;
buildDrawPile(p);
if(p.tempEnergyNextCombat>0){ p.energy = p.tempEnergyNextCombat; p.tempEnergyNextCombat=0; }
// Relics on combat start
p.relics.forEach(rid=>{ const r=RELICS[rid]; if(r.onCombatStart) r.onCombatStart(state); });
// Start first player turn (draw 5, energy 3, status effects)
playerTurnStart();
// Soul Siphon power check handled via played power cards
drawCards(p, 0);
state.curCard=null; state.targeting=null;
render();
}
function endEscapeCombat(){
state.mode="map";
toast("💨 You escaped with no rewards.");
render();
}
function endPlayerTurn(){
const p = state.player;
p.energy = p.energyNextTurn + 3;
p.energyNextTurn = 0;
// discard hand
p.discard.push(...p.hand);
p.hand = [];
p.block = 0;
p.blockNextTurn = 0;
state.curCard=null; state.targeting=null;
render();
setTimeout(enemyTurn, 500);
}
// ---- Player turn start ----
function playerTurnStart(){
const p = state.player;
p.block = 0;
p.energy = p.energyNextTurn + 3;
p.energyNextTurn = 0;
// Block from next turn
if(p.blockNextTurn>0){ p.block += p.blockNextTurn; p.blockNextTurn=0; }
drawCards(p,5);
// Relic: Bleeding Edge
p.relics.forEach(rid=>{ const r=RELICS[rid]; if(r.onPlayerTurnStart) r.onPlayerTurnStart(state); });
// Powers: Soul Siphon
if(p._soulSiphon){ p.energy += 1; loseHP(p,1,true); }
// Powers: Hellfire Aura
if(p._hellfireAura){ const d = p._hellfireAuraUpgraded ? 5 : 3; state.enemies.forEach(e=>{ if(e.hp>0) damageSingle(state,e,d); }); }
// Status effects at start of player's turn: reduce durations
p._weakTimer = Math.max(0,(p._weakTimer||0)-1); if(p._weakTimer===0) p._weak=0;
p._vulnTimer = Math.max(0,(p._vulnTimer||0)-1); if(p._vulnTimer===0) p._vuln=0;
// Intangible decrements
if(p.intangible>0){ p.intangible = Math.max(0, p.intangible-1); }
// Regen: heal X then reduce by 1
if(p.regen>0){ gainHP(p, p.regen); p.regen = Math.max(0, p.regen-1); }
// Bleed: lose HP equal stacks, then halve
if(p._bleed>0){ loseHP(p, p._bleed, true); p._bleed = Math.floor(p._bleed/2); if(p._bleed===0) p._bleed=0; }
// Burn: handled at END of turn. Poison: start of turn.
if(p._poison>0){ loseHP(p, p._poison, true); p._poison = Math.max(0, p._poison-1); }
p.dexBuff = Math.max(0,(p.dexBuff||0)-1);
p.strBuff = Math.max(0,(p.strBuff||0)-1);
}
// ---- Play a card ----
function playCard(card, target){
const p = state.player;
if(state.curCard !== card) return;
if(!canPlayCard(card)) return;
// Validate target
if(card.targetType==="enemy" && (!target || target.hp<=0 || state.enemies.indexOf(target)===-1)){
toast("Select a valid enemy target."); return;
}
if(card.targetType==="all"){
// no target needed
}
// Pay cost
p.energy -= card.cost;
// Set current target
state.target = target;
// Execute
if(card.type==="Power"){
applyPower(card);
if(p.relics.includes("tome_of_dark_rituals")) p.draw(1);
} else if(card.play){
card.play(state);
}
// Exhaust / discard
if(card.exhaust){ p.exhaust.push(card); } else { p.discard.push(card); }
// remove from hand
p.hand = p.hand.filter(c=>c!==card);
state.curCard=null; state.targeting=null;
if(state.player.hp<=0){ checkPlayerDeath(); return; }
render();
}
function applyPower(card){
const p = state.player;
if(card.id==="soul_siphon"){ p._soulSiphon = true; }
if(card.id==="blood_pact"){ p.strPower += (card.upgraded?2:1); }
if(card.id==="umbral_mastery"){ p._umbralMastery = card.upgraded ? 2 : 1; }
if(card.id==="hellfire_aura"){ p._hellfireAura = true; p._hellfireAuraUpgraded = card.upgraded; }
}
// ---- End turn (player) ----
function confirmEndTurn(){
endPlayerTurn();
}
// ---- Enemy turn ----
function enemyTurn(){
const p = state.player;
// Burn at end of player's turn (before enemies act)
if(p._burn>0){ loseHP(p, p._burn, true); p._burn = Math.floor(p._burn/2); if(p._burn===0) p._burn=0; }
render();
setTimeout(()=>{
state.enemies.forEach(e=>{ if(e.hp>0) e._intentTurn++; });
// Act enemies left to right
let idx=0;
function actNext(){
if(state.mode!=="combat"){ render(); return; }
const e = state.enemies[idx];
if(e && e.hp>0){
if(e.isBoss) bossPhaseStrength(e);
resolveEnemyIntent(e);
render();
idx++;
setTimeout(actNext, 600);
} else { idx++; actNext(); }
}
actNext();
}, 300);
}
// Resolve the enemy's CURRENT intent (pattern[_intentTurn]), then advance.
function resolveEnemyIntent(e){
const p = state.player;
let intent;
if(e.isBoss) intent = e.getBossIntent();
else intent = e.intent();
const v = intent.value;
switch(intent.type){
case "attack": {
let d = v + (e.str||0);
if(p._vuln>0) d = Math.floor(d*1.5);
if(p._weak>0) d = Math.floor(d*0.75);
if(p.block>0) d = Math.max(0, d - p.block);
p.hp = Math.max(0, p.hp - d);
break;
}
case "multi": {
v.forEach(dmg=>{
let d = dmg + (e.str||0);
if(p._vuln>0) d = Math.floor(d*1.5);
if(p._weak>0) d = Math.floor(d*0.75);
if(p.block>0) d = Math.max(0, d - p.block);
p.hp = Math.max(0, p.hp - d);
});
break;
}
case "attack2": {
let d = v[0] + (e.str||0);
if(p._vuln>0) d = Math.floor(d*1.5);
if(p._weak>0) d = Math.floor(d*0.75);
if(p.block>0) d = Math.max(0, d - p.block);
p.hp = Math.max(0, p.hp - d);
if(intent.text.includes("Weak")) p.applyWeak(v[1]);
else if(intent.text.includes("Burn")) p.applyBurn(v[1]);
else if(intent.text.includes("Poison")) p.applyPoison(v[1]);
break;
}
case "defend": e.gainBlock(v); break;
case "shieldbash": {
let d = v[0] + (e.str||0);
if(p._vuln>0) d = Math.floor(d*1.5);
if(p._weak>0) d = Math.floor(d*0.75);
if(p.block>0) d = Math.max(0, d - p.block);
p.hp = Math.max(0, p.hp - d);
e.gainBlock(v[1]);
break;
}
case "shieldbash2": e.gainBlock(v[0]); if(v[1]) gainHP(e, v[1]); break;
case "drain": {
let d = v[0] + (e.str||0);
if(p._vuln>0) d = Math.floor(d*1.5);
if(p._weak>0) d = Math.floor(d*0.75);
if(p.block>0) d = Math.max(0, d - p.block);
p.hp = Math.max(0, p.hp - d);
gainHP(e, v[1]);
break;
}
case "debuff":
applyEnemyDebuff(e, intent.text);
break;
case "buff":
applyEnemyBuff(e, intent);
break;
case "summon":
summonEnemy(e, v);
break;
}
e._intentTurn++;
if(e.isBoss && !e.phase2 && e.hp <= e.maxHP/2){ e.phase2 = true; }
}
function applyEnemyDebuff(e, text){
const p = state.player;
if(text.includes("Weak")) p.applyWeak(1);
else if(text.includes("Vulnerable")) p.applyVulnerable(1);
else if(text.includes("Bleed")) p.applyBleed(1);
else if(text.includes("Poison")) p.applyPoison(1);
}
function applyEnemyBuff(e, intent){
const text = intent.text;
const v = intent.value;
if(text.includes("Strength")){ e.str += (typeof v==="number"?v:0); }
else if(text.includes("Block")) e.gainBlock(5);
else if(text.includes("Regen")) e.applyRegen(2);
else if(text.includes("Intangible")) e.intangible = 1;
else if(text.includes("Vulnerable")) e.applyVulnerable(1);
}
function summonEnemy(source, id){
state.enemies.push(makeEnemyInstance(id));
}
// ---- Boss phase strength applied at start of boss's own turn ----
function bossPhaseStrength(e){
if(!e.isBoss || !e.phase2) return;
if(e.id==="lichking" || e.id==="archdemon") e.str += 2;
else if(e.id==="eldritchtreant") e.str += 1;
}
// ---- Check combat end ----
function checkCombatEnd(){
if(state.mode!=="combat") return;
// Player dead?
if(state.player.hp<=0){ checkPlayerDeath(); return; }
// All enemies dead?
if(state.enemies.every(e=>e.hp<=0)){
endCombatVictory();
}
}
// ---- Player death ----
function checkPlayerDeath(){
if(state.mode!=="combat") return;
if(state.player.hp>0) return;
state.mode="death";
render();
}
// ---- Combat victory ----
function endCombatVictory(){
const p = state.player;
const isBoss = state.enemies.some(e=>e.isBoss);
const isElite = state.enemies.some(e=>ENEMY_DEF[e.id] && ENEMY_DEF[e.id].elite);
const cb = state._pendingCb;
state._pendingCb = null;
// Ashen Armor
if(p.relics.includes("ashen_armor")) gainHP(p,6);
// Relic on combat end
p.relics.forEach(rid=>{ const r=RELICS[rid]; if(r.onCombatEnd) r.onCombatEnd(state); });
state.mode = "reward";
buildReward(isBoss, isElite);
render();
if(cb) cb();
}
// ---- Rewards ----
function rewardChoices(kind){
// returns array of {card}
const p = state.player;
const pool = p.classDef.pool;
const keys = Object.keys(pool);
const offers = [];
const count = 3;
for(let i=0;i<count;i++){
const roll = Math.random();
let rar;
if(kind==="boss"){ rar = i===0 ? "Rare" : (Math.random()<0.5?"Common":"Uncommon"); }
else if(kind==="elite"){ rar = Math.random()<0.2 ? "Rare" : (Math.random()<0.5?"Common":"Uncommon"); }
else { rar = Math.random()<0.6?"Common":(Math.random()<0.5?"Uncommon":"Rare"); }
const avail = keys.filter(k=>pool[k].rarity===rar);
const id = pick(avail.length?avail:keys);
offers.push(makeCard(id));
}
return offers;
}
function buildReward(isBoss, isElite){
const p = state.player;
let gold = 0;
if(isBoss) gold = 50;
else if(isElite) gold = rand(25,35);
else gold = rand(10,20);
gold += state.goldBonus;
p.gold += gold;
const rewards = [];
rewards.push({kind:"gold", amount:gold});
// Card choice
if(!isBoss){
rewards.push({kind:"card", offers:rewardChoices(isElite?"elite":"normal")});
} else {
rewards.push({kind:"card", offers:rewardChoices("boss")});
}
// Potion(s)
if(isBoss){
rewards.push({kind:"potions", count:2});
} else if(isElite){
if(Math.random()<0.7) rewards.push({kind:"potion", count:1});
} else {
if(Math.random()<0.4) rewards.push({kind:"potion", count:1});
}
// Relic
if(isElite){
rewards.push({kind:"relic"});
}
// Boss relic choice
if(isBoss){
rewards.push({kind:"bossrelic"});
}
state.rewardQueue = rewards;
}
function grantPotion(id){
const p = state.player;
if(p.potions.length>=3){ toast("Potion slots full — potion lost."); return; }
p.potions.push(id);
}
function grantRelic(id){
const p = state.player;
p.relics.push(id);
const r = RELICS[id];
if(r.onObtain) r.onObtain(state);
}
function makeCommonRelic(){
const commons = ["bloodstone","cursed_amulet","bleeding_edge","ashen_armor","greedy_imp"];
return pick(commons);
}
function makeRareRelic(){
const rares = ["shadow_cloak","tome_of_dark_rituals","soul_ember","eternal_hourglass","vial_of_shadows"];
return pick(rares);
}
function upgradeRandomCards(deck, n){
const upgradable = deck.filter(c=>!c.status && !c.upgraded && !c.isToken);
for(let i=0;i<n && upgradable.length;i++){
const c = pick(upgradable);
c.upgraded = true;
upgradable.splice(upgradable.indexOf(c),1);
}
}
function nextReward(){
if(!state.rewardQueue || state.rewardQueue.length===0){
afterReward(); return;
}
const r = state.rewardQueue[0];
if(r.kind==="gold"){
toast("+" + r.amount + " Gold");
state.rewardQueue.shift(); nextReward();
} else if(r.kind==="card"){
state.curReward = {kind:"card", offers:r.offers}; render();
} else if(r.kind==="potions"){
for(let i=0;i<r.count;i++) grantPotion(randPotion());
toast("Got potions!");
state.rewardQueue.shift(); nextReward();
} else if(r.kind==="potion"){
grantPotion(randPotion());
toast("Got a potion!");
state.rewardQueue.shift(); nextReward();
} else if(r.kind==="relic"){
grantRelic(makeCommonRelic());
toast("Got a Relic!");
state.rewardQueue.shift(); nextReward();
} else if(r.kind==="bossrelic"){
state.curReward = {kind:"bossrelic"}; render();
}
}
function randPotion(){
const ids = Object.keys(POTIONS).filter(k=>k!=="smoke_bomb");
return pick(ids);
}
function afterReward(){
// boss defeated? advance act
const isBoss = state.enemies.some(e=>e.isBoss);
if(isBoss){
endAct();
} else {
state.mode = "map";
render();
}
}
function endAct(){
const p = state.player;
// Full heal
p.hp = p.maxHP;
state.actIndex++;
state.floorIndex = 0;
if(state.actIndex >= ACTS.length){
state.mode = "victory";
state.won = true;
} else {
state.mode = "map";
}
render();
}
// ============================================================
// UI RENDERING
// ============================================================
function h(){ return $("app"); }
function render(){
const app = $("app");
if(!app) return;
app.innerHTML = "";
app.appendChild(titleBar());
switch(state.mode){
case "classselect": renderClassSelect(app); break;
case "map": renderMap(app); break;
case "combat": renderCombat(app); break;
case "reward": renderReward(app); break;
case "shop": renderShop(app); break;
case "event": renderEvent(app); break;
case "rest": renderRest(app); break;
case "death": renderDeath(app); break;
case "victory": renderVictory(app); break;
default: app.innerHTML += "<p>Unknown mode</p>";
}
}
function titleBar(){
const bar = document.createElement("div");
bar.className = "title-logo";
bar.textContent = "Shadows of the Abyss";
const sub = document.createElement("div");
sub.className = "title-sub";
sub.textContent = "A Roguelike Deckbuilder";
const wrap = document.createElement("div");
wrap.appendChild(bar); wrap.appendChild(sub);
if(state.mode!=="classselect"){
const info = document.createElement("div");
info.style.textAlign="center"; info.style.marginTop="6px";
if(state.player){
info.innerHTML = `<span class="muted">Class: </span><b style="color:${state.player.classDef.color}">${state.player.classDef.name}</b>
<span class="muted"> · Gold: </span><span class="gold-t">${state.player.gold}</span>
<span class="muted"> · HP: </span><span class="red">${state.player.hp}/${state.player.maxHP}</span>`;
}
wrap.appendChild(info);
}
return wrap;
}
// ---------- Class Select ----------
function renderClassSelect(app){
const panel = el("panel", {style:"text-align:center; margin-top:20px;"});
panel.innerHTML = `<h2 class="muted">Choose your cursed fate</h2>
<p class="muted">Each begins with 70 HP, 3 energy, 99 gold, and a starter deck.</p>`;
app.appendChild(panel);
const row = el("div",{style:"display:flex; gap:24px; justify-content:center; flex-wrap:wrap; margin-top:20px;"});
// fix: CLASSES values don't have classId; pass key
row.innerHTML="";
Object.keys(CLASSES).forEach(k=>{
const c = CLASSES[k];
const card = el("div",{className:"classcard", style:`border-color:${c.color}`});
card.innerHTML = `<div class="cicon">${c.icon}</div>
<h3 style="color:${c.color}">${c.name}</h3>
<p class="muted" style="font-size:13px">${c.desc}</p>
<div style="font-size:11px; margin-top:8px; color:var(--gold2)">
Signature: ${Object.keys(c.pool).find(id=>["sanguine_strike","shadow_bolt","firebolt"].includes(id))}
</div>`;
card.onclick = ()=> startRun(k);
row.appendChild(card);
});
app.appendChild(row);
}
function startRun(classId){
const p = newPlayer(classId);
p.gold = 99;
state.player = p;
state.actIndex = 0;
state.floorIndex = 0;
state.mode = "map";
render();
}
// ---------- Map ----------
function renderMap(app){
const act = ACTS[state.actIndex];
const panel = el("panel");
const head = el("div",{className:"section-head"});
head.innerHTML = `<h2>${act.name}</h2>
<div>
<span class="muted">Floor </span><b>${state.floorIndex+2}</b><span class="muted"> / 15</span>
<span class="muted" style="margin-left:16px">Relics: ${state.player.relics.length}</span>
</div>`;
panel.appendChild(head);
app.appendChild(panel);
// Potions row
const pinfo = el("div",{style:"margin:8px 0;"});
pinfo.innerHTML = `<b>Potions (${state.player.potions.length}/3):</b> ` +
state.player.potions.map(id=>`<span class="tag">${POTIONS[id].icon} ${POTIONS[id].name}</span>`).join(" ") + " ";
panel.appendChild(pinfo);
const map = el("div",{className:"map-wrap"});
act.floors.forEach((f, i)=>{
const floorNum = i+2;
const isPast = floorNum < state.floorIndex+2;
const isCurrent = floorNum === state.floorIndex+2;
const node = el("div",{className:`node ${NODE_ICONS[f.type]} ${isCurrent?"current":""} ${isPast?"done":""}`});
node.innerHTML = f.icon;
node.title = f.name;
if(isCurrent){
node.onclick = ()=> enterFloor(i);
} else if(isPast){
node.style.cursor="default";
} else {
node.style.opacity=".5";
node.style.cursor="not-allowed";
}
const label = el("div",{style:"font-size:11px; text-align:center; color:var(--muted); margin-top:2px;"});
label.textContent = "F"+floorNum;
const col = el("div",{style:"display:flex; flex-direction:column; align-items:center;"});
col.appendChild(node); col.appendChild(label);
map.appendChild(col);
if(i < act.floors.length-1){
const line = el("div",{className:"path-line"});
map.appendChild(line);
}
});
panel.appendChild(map);
}
const NODE_ICONS = {enemy:"enemy", elite:"elite", rest:"rest", shop:"shop", event:"event", boss:"boss"};
function enterFloor(i){
const act = ACTS[state.actIndex];
const f = act.floors[i];
state.floorIndex = i;
switch(f.type){
case "enemy":
case "elite":
startCombat(f.group, f.name, false);
break;
case "boss":
startCombat(f.group, f.name, true);
break;
case "shop":
state.currentFloor = f; state.mode="shop"; render(); break;
case "event":
const ev = pick(EVENTS.filter(e=>e.id!==undefined));
state.currentEvent = ev; state.mode="event"; render(); break;
case "rest":
state.currentFloor = f; state.mode="rest"; render(); break;
default:
startCombat(f.group, f.name, false);
}
}
// ---------- Combat ----------
function renderCombat(app){
const p = state.player;
const head = el("div",{className:"panel"});
head.innerHTML = `<div class="flex" style="justify-content:space-between;">
<span><b>${state.combatLabel}</b> ${state.combatSpecial?'<span class="tag">special</span>':''}</span>
<span class="muted">Act ${state.actIndex+1}, Floor ${state.floorIndex+2}</span>
</div>`;
app.appendChild(head);
// Enemies
const eg = el("div",{className:"enemy-grid"});
state.enemies.forEach((e,idx)=>{
const intent = e.isBoss ? e.getBossIntent() : e.intent();
const targetable = state.targeting && state.targeting===e;
const div = el("div",{className:`enemy ${targetable?"targetable":""}`});
div.innerHTML = `
${e.isBoss?'<div class="tag">'+(e.phase2?"PHASE 2":"PHASE 1")+'</div>':''}
<div class="intent"><span class="it">${intentText(intent)}</span></div>
<div class="eicon">${e.icon}</div>
<div class="ename">${e.name}</div>
<div class="hpbar"><div style="width:${100*e.hp/e.maxHP}%"></div></div>
<div class="muted">${e.hp}/${e.maxHP}</div>
${e.block>0?`<span class="blockbadge">🛡 ${e.block}</span>`:''}
<div>${enemyStatusBadges(e)}</div>`;
div.onclick = ()=> onEnemyClick(e);
eg.appendChild(div);
});
app.appendChild(eg);
// Powers + status row
const info = el("div",{className:"panel"});
info.innerHTML = `<div class="flex" style="justify-content:space-between; align-items:flex-start;">
<div>
<div class="muted" style="font-size:11px; text-transform:uppercase; letter-spacing:1px">Your Status</div>
<div>${playerStatusBadges(p)}</div>
</div>
<div>
<div class="muted" style="font-size:11px; text-transform:uppercase; letter-spacing:1px">Powers</div>
<div class="powers-zone">${powersZone(p)}</div>
</div>
</div>`;
app.appendChild(info);
// Player stats
const stats = el("div",{className:"panel"});
stats.innerHTML = `<div class="resources">
<div class="stat"><span class="v" style="color:#e66">${p.hp}</span><span class="k">HP</span></div>
<div class="stat"><span class="v" style="color:#6ec1ff">${p.energy}</span><span class="k">Energy</span></div>
<div class="stat"><span class="v">${p.block}</span><span class="k">Block</span></div>
<div class="stat"><span class="v gold-t">${p.gold}</span><span class="k">Gold</span></div>
<div class="muted" style="flex:1"></div>
<div class="potions">${potionsRow(p)}</div>
</div>`;
app.appendChild(stats);
// Hand
const hand = el("div",{className:"hand"});
p.hand.forEach(card=>{
hand.appendChild(cardElement(card, p.energy>=card.cost && !card.status && !card.isStatus));
});
app.appendChild(hand);
// End turn + draw/discard counts
const foot = el("div",{className:"flex", style:"justify-content:space-between; margin-top:4px;"});
foot.innerHTML = `<span class="muted">Draw: ${p.draw.length} · Discard: ${p.discard.length} · Exhaust: ${p.exhaust.length}</span>
<div>
<button id="btnDrawInfo" class="gold" onclick="showDeck()">📖 Deck</button>
<button class="primary" id="btnEndTurn" onclick="onEndTurn()">End Turn</button>
</div>`;
app.appendChild(foot);
}
function intentText(intent){
const v = intent.value;
switch(intent.type){
case "multi": return "⚔️ " + v.join("×") + " dmg";
case "attack2": return "⚔️ " + v[0] + " + " + intent.text.split(",")[1];
case "shieldbash": return "⚔️" + v[0] + " 🛡" + v[1];
case "shieldbash2": return "🛡" + v[0] + " ❤" + v[1];
case "drain": return "⚔️ " + v[0] + " ❤" + v[1];
case "summon": return "✨ Summon";
case "debuff": return "✦ " + intent.text;
case "buff": return "✦ " + intent.text;
default: return (intent.text||"");
}
}
function enemyStatusBadges(e){
let s = "";
if(e.hasWeak()) s+=`<span class="statusbadge sb-weak">Weak ${e._weak}</span>`;
if(e.hasVulnerable()) s+=`<span class="statusbadge sb-vulnerable">Vuln ${e._vuln}</span>`;
if(e.hasBleed()) s+=`<span class="statusbadge sb-bleed">Bleed ${e._bleed}</span>`;
if(e.hasBurn()) s+=`<span class="statusbadge sb-burn">Burn ${e._burn}</span>`;
if(e.hasRegen()) s+=`<span class="statusbadge sb-regen">Regen ${e._regen}</span>`;
if(e.intangible>0) s+=`<span class="statusbadge sb-intangible">Intangible ${e.intangible}</span>`;
return s;
}
function playerStatusBadges(p){
let s = "";
if(p._weak>0) s+=`<span class="statusbadge sb-weak">Weak ${p._weak}</span>`;
if(p._vuln>0) s+=`<span class="statusbadge sb-vulnerable">Vuln ${p._vuln}</span>`;
if(p._bleed>0) s+=`<span class="statusbadge sb-bleed">Bleed ${p._bleed}</span>`;
if(p._burn>0) s+=`<span class="statusbadge sb-burn">Burn ${p._burn}</span>`;
if(p._poison>0) s+=`<span class="statusbadge sb-poison">Poison ${p._poison}</span>`;
if(p.strBuff>0) s+=`<span class="statusbadge sb-strength">Str ${p.strBuff}</span>`;
if(p.dexBuff>0) s+=`<span class="statusbadge sb-dexterity">Dex ${p.dexBuff}</span>`;
if(p.regen>0) s+=`<span class="statusbadge sb-regen">Regen ${p.regen}</span>`;
if(p.intangible>0) s+=`<span class="statusbadge sb-intangible">Intangible ${p.intangible}</span>`;
if(p._soulSiphon) s+=`<span class="statusbadge" style="background:#3a2a5a">Soul Siphon</span>`;
if(p._hellfireAura) s+=`<span class="statusbadge" style="background:#5a3010">Hellfire Aura</span>`;
return s;
}
function powersZone(p){
const chips = [];
if(p._soulSiphon) chips.push(powerChip("Soul Siphon","🔮"));
if(p._hellfireAura) chips.push(powerChip("Hellfire Aura", p._hellfireAuraUpgraded?"🔥":"🔥"));
if(p._umbralMastery) chips.push(powerChip("Umbral Mastery","🌑"));
if(p.strPower>0) chips.push(powerChip("Strength","💪"));
if(chips.length===0) return '<span class="muted" style="font-size:12px">none</span>';
return chips.join("");
}
function powerChip(name,icon){ return `<div class="powerchip"><span style="font-size:16px">${icon}</span>${name}</div>`; }
function potionsRow(p){
return p.potions.map(id=>`<div class="potion" title="${POTIONS[id].name}" onclick="onPotion('${id}')">${POTIONS[id].icon}<span class="pnum">${p.potions.length}</span></div>`).join("");
}
// ---------- Card element ----------
function cardElement(card, playable){
const div = el("div",{className:`card ${card.type.toLowerCase()}` + (playable?" playable":"") + (card.ethereal||card.isStatus?" ethereal":"")});
if(card.upgraded) div.innerHTML += `<span class="up">+</span>`;
div.innerHTML += `<div class="cost">${card.cost}</div>
<div class="cname">${card.name}${card.upgraded?"+":""}</div>
<div class="ctype">${card.type}${card.isStatus?" / Status":card.isToken?" / Token":""}</div>
<div class="cdesc">${card.desc}${card.exhaust?" <br><span class='tag'>Exhaust</span>":""}${card.doubt?" <br><span class='tag'>Doubt</span>":""}</div>
<div class="rarity">${card.rarity}${card.targetType==="enemy"?" · Target":card.targetType==="all"?" · All":""}</div>`;
if(playable){
div.onclick = ()=> onCardClick(card);
}
return div;
}
// ---------- Card / enemy / potion clicks ----------
function onCardClick(card){
const p = state.player;
if(state.mode!=="combat") return;
if(card.status || card.isStatus) return;
if(!canPlayCard(card)){ toast("Can't play that card."); return; }
if(card.cost>0){
// Already selected?
if(state.curCard===card){ state.curCard=null; state.targeting=null; render(); return; }
if(card.targetType==="enemy"){
if(state.curCard===card){
// switch selection: play it (no target) -> for single attacks, need target.
// Instead just select.
}
state.curCard = card;
state.targeting = null;
// If targeting required, show targetable enemies
render();
// Highlight enemies
highlightEnemyTargets();
} else {
// No target needed
state.curCard = card;
state.targeting = null;
playCard(card, null);
}
} else {
state.curCard = card;
state.targeting = null;
playCard(card, null);
}
}
function highlightEnemyTargets(){
// re-render and add targetable class
render();
document.querySelectorAll(".enemy.targetable").forEach(el=>{
el.onclick = ()=>{
const idx = [...document.querySelectorAll(".enemy")].indexOf(el);
const target = state.enemies[idx];
playCard(state.curCard, target);
};
});
}
function onEnemyClick(e){
if(state.targeting && state.curCard){
playCard(state.curCard, e);
}
}
function onPotion(id){
if(state.mode!=="combat") return;
const potion = POTIONS[id];
potion.effect(state);
// remove from potions
const idx = state.player.potions.indexOf(id);
if(idx>=0) state.player.potions.splice(idx,1);
toast(potion.name + " used!");
checkCombatEnd();
render();
}
function onEndTurn(){
if(state.mode!=="combat") return;
// Doubt at end of turn
const p = state.player;
p.hand.filter(c=>c.doubt).forEach(c=>{ loseHP(p,1,true); });
if(p.hp<=0){ checkPlayerDeath(); return; }
endPlayerTurn();
}
// ---------- Reward screen ----------
function renderReward(app){
const panel = el("panel",{style:"text-align:center; margin-top:20px;"});
panel.innerHTML = `<h2>Victory!</h2>`;
app.appendChild(panel);
renderRewardQueue(app);
}
function renderRewardQueue(app){
const queue = state.rewardQueue;
const panel = app.querySelector(".panel") || el("panel",{style:"text-align:center; margin-top:20px;"});
// clear existing reward children
while(panel.children.length>1) panel.removeChild(panel.lastChild);
if(!queue || queue.length===0){
// all done
const btn = el("div",{className:"btn-row"});
const go = document.createElement("button");
go.className="primary"; go.textContent = state.enemies.some(e=>e.isBoss) ? "Continue" : "Continue";
go.onclick = ()=>{ nextReward(); };
// Actually nextReward handles shifting; when empty, afterReward:
// We'll route via nextReward's afterReward. But here queue empty means done.
go.onclick = ()=> afterReward();
btn.appendChild(go);
panel.appendChild(btn);
return;
}
const r = queue[0];
if(r.kind==="gold"){
const d = el("div",{style:"font-size:22px; margin:20px 0;"});
d.innerHTML = `<span class="gold-t">💰 +${r.amount} Gold</span>`;
panel.appendChild(d);
const b = el("div",{className:"btn-row"});
const go = document.createElement("button"); go.className="primary"; go.textContent="Continue";
go.onclick=()=>{ state.rewardQueue.shift(); renderRewardQueue(app); };
b.appendChild(go); panel.appendChild(b);
} else if(r.kind==="card"){
const grid = el("div",{className:"shop-grid"});
r.offers.forEach(card=>{
const ce = cardElement(card, true);
ce.classList.add("reward-card");
const wrap = el("div",{style:"display:flex; flex-direction:column; align-items:center;"});
wrap.appendChild(ce);
const buy = document.createElement("button");
buy.className="gold"; buy.style.marginTop="8px"; buy.textContent="Take card";
buy.onclick=()=>{ state.player.deck.push(card); state.rewardQueue.shift(); renderRewardQueue(app); };
wrap.appendChild(buy);
const skip = document.createElement("button");
skip.style.marginTop="4px"; skip.style.padding="4px 12px"; skip.textContent="Skip";
skip.onclick=()=>{ state.rewardQueue.shift(); renderRewardQueue(app); };
wrap.appendChild(skip);
grid.appendChild(wrap);
});
panel.appendChild(grid);
} else if(r.kind==="potions" || r.kind==="potion"){
const d = el("div",{style:"font-size:20px; margin:20px 0;"});
d.innerHTML = `🧪 You received ${r.kind==="potions"?r.count+" potions":"a potion"}!`;
panel.appendChild(d);
const b = el("div",{className:"btn-row"});
const go = document.createElement("button"); go.className="primary"; go.textContent="Continue";
go.onclick=()=>{ state.rewardQueue.shift(); renderRewardQueue(app); };
b.appendChild(go); panel.appendChild(b);
} else if(r.kind==="relic"){
const id = makeCommonRelic();
const d = el("div",{style:"font-size:20px; margin:20px 0;"});
d.innerHTML = `🏅 You received <b>${RELICS[id].name}</b>!<br><span class="muted" style="font-size:13px">${RELICS[id].desc}</span>`;
panel.appendChild(d);
const b = el("div",{className:"btn-row"});
const go = document.createElement("button"); go.className="primary"; go.textContent="Continue";
go.onclick=()=>{ state.rewardQueue.shift(); renderRewardQueue(app); };
b.appendChild(go); panel.appendChild(b);
} else if(r.kind==="bossrelic"){
const opts = bossRelicOptions();
const grid = el("div",{className:"shop-grid"});
opts.forEach(rid=>{
const wrap = el("div",{style:"width:180px; border:2px solid var(--gold); border-radius:12px; padding:14px; text-align:center; background:var(--panel2);"});
wrap.innerHTML = `<div style="font-size:24px">👑</div><b style="color:var(--gold2)">${RELICS_BOSSES[rid].name}</b>
<div class="muted" style="font-size:12px; margin:8px 0">${RELICS_BOSSES[rid].desc}</div>`;
const take = document.createElement("button");
take.className="gold"; take.style.marginTop="6px"; take.textContent="Take relic";
take.onclick=()=>{ grantRelic(rid); state.rewardQueue.shift(); renderRewardQueue(app); };
wrap.appendChild(take);
grid.appendChild(wrap);
});
panel.appendChild(grid);
}
}
// boss relics
const RELICS_BOSSES = {
cursed_crown:{name:"Cursed Crown",desc:"Gain 1 Energy at start of each turn. Curses can no longer be removed.",downside:true},
ectoplasm:{name:"Ectoplasm",desc:"Gain 1 Energy. Lose all Gold and gain no more Gold this run.",downside:true},
philosophers_stone:{name:"Philosopher's Stone",desc:"Gain 1 Energy. All enemies start combat with 1 Strength.",downside:true},
black_blood:{name:"Black Blood",desc:"At start of combat, lose 10% max HP. Whenever you play an Attack, heal 1 HP.",downside:true},
shadow_amulet:{name:"Shadow Amulet",desc:"Start each turn with 1 Intangible. Reduce max HP by 15.",downside:true},
};
function bossRelicOptions(){
const keys = Object.keys(RELICS_BOSSES);
return shuffle(keys).slice(0,3);
}
// ---------- Helpers ----------
function el(tag, attrs){
const e = document.createElement(tag);
if(attrs){
Object.keys(attrs).forEach(k=>{
if(k==="className") e.className=attrs[k];
else if(k==="innerHTML") e.innerHTML=attrs[k];
else if(k.startsWith("on")) e.addEventListener(k.slice(2), attrs[k]);
else if(k==="style") e.style.cssText=attrs[k];
else e.setAttribute(k, attrs[k]);
});
}
return e;
}
// ---------- Deck view ----------
function showDeck(){
const p = state.player;
const panel = el("panel");
panel.style.maxWidth="900px"; panel.style.margin="10px auto";
panel.innerHTML = `<div class="section-head"><h3>Your Deck</h3>
<button onclick="render()">Close</button></div>
<div class="muted">Draw: ${p.draw.length} · Discard: ${p.discard.length} · Exhaust: ${p.exhaust.length} · Total unique cards: ${deckNoExhaust(p).length}</div>`;
const grid = el("div",{className:"shop-grid"});
deckNoExhaust(p).forEach(card=>{ grid.appendChild(cardElement(card, false)); });
document.getElementById("app").appendChild(panel);
document.getElementById("app").appendChild(grid);
}
// ---------- Remove card from deck (events/rewards) ----------
function askRemoveFromDeck(reason){
const p = state.player;
const panel = el("panel",{style:"margin-top:20px;"});
panel.style.maxWidth="900px"; panel.style.margin="20px auto";
panel.innerHTML = `<h3>${reason}</h3><p class="muted">Choose a card to remove permanently.</p>`;
const grid = el("div",{className:"shop-grid"});
const upgradable = deckNoExhaust(p);
if(upgradable.length===0){
panel.innerHTML += `<p class="muted">No cards to remove.</p>
<div class="btn-row"><button class="primary" onclick="finishRemove()">Done</button></div>`;
document.getElementById("app").appendChild(panel);
return;
}
upgradable.forEach(card=>{
const wrap = el("div",{style:"display:flex; flex-direction:column; align-items:center;"});
wrap.appendChild(cardElement(card, true));
const rm = document.createElement("button");
rm.className="primary"; rm.style.marginTop="6px"; rm.textContent="Remove";
rm.onclick=()=>{
p.deck = p.deck.filter(c=>c!==card);
toast("Removed "+card.name+".");
render();
};
wrap.appendChild(rm);
grid.appendChild(wrap);
});
panel.appendChild(grid);
document.getElementById("app").appendChild(panel);
}
// ---------- Shop ----------
function renderShop(app){
const p = state.player;
const panel = el("panel",{style:"margin-top:10px;"});
panel.innerHTML = `<div class="section-head"><h2>🏪 Shop</h2>
<div><span class="gold-t" style="font-size:20px">💰 ${p.gold}</span> · <button class="primary" onclick="leaveShop()">Leave</button></div></div>`;
app.appendChild(panel);
const wares = [];
// 3 class cards
for(let i=0;i<3;i++){ wares.push({card:pickCardFromPool(), price:cardPrice(pickCardFromPool())}); }
// 1 Colorless
const colorless = pick(Object.keys(COLORLESS));
// 2 Relics
const relics = [makeCommonRelic(), makeRareRelic()];
// 3 Potions
const pots = [randPotion(), randPotion(), randPotion()];
// Cards
const ch = el("div",{className:"section-head"});
ch.innerHTML = `<h3>Cards</h3>`;
panel.appendChild(ch);
const grid = el("div",{className:"shop-grid"});
const offered = [pickCardFromPool(), pickCardFromPool(), pickCardFromPool(), makeCard(colorless)];
offered.forEach(card=>{
const wrap = el("div",{style:"display:flex; flex-direction:column; align-items:center;"});
wrap.appendChild(cardElement(card, true));
const price = cardPrice(card);
const buy = document.createElement("button");
buy.className="gold"; buy.style.marginTop="6px"; buy.textContent=`Buy — ${price}g`;
buy.disabled = p.gold < price;
buy.onclick=()=>{ if(p.gold>=price){ p.gold-=price; p.deck.push(card); toast("Bought "+card.name+"."); render(); } };
wrap.appendChild(buy);
grid.appendChild(wrap);
});
panel.appendChild(grid);
// Relics
const rh = el("div",{className:"section-head"});
rh.innerHTML = `<h3>Relics</h3>`;
panel.appendChild(rh);
const rg = el("div",{className:"shop-grid"});
relics.forEach(rid=>{
const r = RELICS[rid];
const wrap = el("div",{style:"width:180px; border:2px solid var(--gold); border-radius:12px; padding:14px; text-align:center; background:var(--panel2);"});
wrap.innerHTML = `<div style="font-size:24px">🏅</div><b style="color:var(--gold2)">${r.name}</b>
<div class="muted" style="font-size:12px; margin:8px 0">${r.desc}</div>`;
const buy = document.createElement("button");
buy.className="gold"; buy.style.marginTop="6px"; buy.textContent="Buy — 150g";
buy.disabled = p.gold<150 || p.relics.includes(rid);
buy.onclick=()=>{ if(p.gold>=150 && !p.relics.includes(rid)){ p.gold-=150; grantRelic(rid); toast("Got "+r.name+"."); render(); } };
wrap.appendChild(buy);
rg.appendChild(wrap);
});
panel.appendChild(rg);
// Potions
const ph = el("div",{className:"section-head"});
ph.innerHTML = `<h3>Potions</h3>`;
panel.appendChild(ph);
const pg = el("div",{className:"shop-grid"});
pots.forEach(pid=>{
const pot = POTIONS[pid];
const wrap = el("div",{style:"width:150px; text-align:center;"});
wrap.innerHTML = `<div style="font-size:30px">${pot.icon}</div><b>${pot.name}</b>
<div class="muted" style="font-size:12px">${pot.desc||""}</div>`;
const buy = document.createElement("button");
buy.className="gold"; buy.style.marginTop="6px"; buy.textContent="Buy — 50g";
buy.disabled = p.gold<50 || p.potions.length>=3;
buy.onclick=()=>{ if(p.gold>=50 && p.potions.length<3){ p.gold-=50; grantPotion(pid); toast("Bought "+pot.name+"."); render(); } };
wrap.appendChild(buy);
pg.appendChild(wrap);
});
panel.appendChild(pg);
// Card removal service
const rem = el("div",{className:"panel", style:"margin-top:10px; text-align:center;"});
const canRemove = p.deck.length>10;
rem.innerHTML = `<h3>Card Removal Service</h3>
<p class="muted">Remove 1 card from your deck permanently (once per visit).</p>
<div class="btn-row"><button class="gold" ${canRemove?"":"disabled"} onclick="shopRemove()">Remove a card — 75g</button></div>`;
panel.appendChild(rem);
}
function cardPrice(card){
if(card.rarity==="Common") return 50;
if(card.rarity==="Uncommon") return 75;
if(card.rarity==="Rare") return 150;
return 60;
}
function pickCardFromPool(){
const p = state.player;
const pool = p.classDef.pool;
const keys = Object.keys(pool);
const roll = Math.random();
let rar = roll<0.6?"Common":(roll<0.9?"Uncommon":"Rare");
const avail = keys.filter(k=>pool[k].rarity===rar);
const id = pick(avail.length?avail:keys);
return makeCard(id);
}
function shopRemove(){
const p = state.player;
if(p.gold<75){ toast("Not enough gold."); return; }
p.gold-=75;
askRemoveFromDeck("Shop card removal (75g)");
}
function leaveShop(){ state.mode="map"; render(); }
// ---------- Event ----------
function renderEvent(app){
const ev = state.currentEvent;
const panel = el("panel",{style:"max-width:700px; margin:20px auto;"});
panel.innerHTML = `<div style="text-align:center; font-size:56px">${ev.icon}</div>
<h2 style="text-align:center">${ev.name}</h2>
<p class="big" style="text-align:center">${ev.text}</p>`;
app.appendChild(panel);
const b = el("div",{className:"btn-row"});
ev.choices.forEach(ch=>{
const btn = document.createElement("button");
btn.className="primary"; btn.textContent = ch.label;
btn.onclick = ()=> ch.run(state);
b.appendChild(btn);
});
panel.appendChild(b);
}
// ---------- Rest ----------
function renderRest(app){
const p = state.player;
const panel = el("panel",{style:"text-align:center;"});
const healAmt = Math.floor(p.maxHP*0.25);
panel.innerHTML = `<div style="font-size:56px">🔥</div>
<h2>Rest Site</h2>
<p class="muted">A moment of safety in the abyss.</p>
<div class="btn-row">
<button class="primary" onclick="restHeal()">Rest — Heal ${healAmt} HP (${Math.floor(p.maxHP*0.25)} HP)</button>
<button class="gold" onclick="restForge()">Forge — Upgrade a card</button>
<button onclick="restMeditate()">Meditate — Remove a card</button>
</div>
<div class="btn-row"><button onclick="leaveRest()">Leave</button></div>`;
app.appendChild(panel);
}
function restHeal(){
const p = state.player;
gainHP(p, Math.floor(p.maxHP*0.25));
toast("You rest and recover.");
state.mode="map"; render();
}
function restForge(){
const p = state.player;
const upgradable = deckNoExhaust(p).filter(c=>!c.upgraded);
if(upgradable.length===0){ toast("No cards to upgrade."); return; }
const panel = el("panel",{style:"text-align:center;"});
panel.innerHTML = `<h2>Forge — choose a card to upgrade</h2>
<p class="muted">Upgraded cards are permanently improved.</p>`;
app.appendChild(panel);
const grid = el("div",{className:"shop-grid"});
upgradable.forEach(card=>{
const wrap = el("div",{style:"display:flex; flex-direction:column; align-items:center;"});
wrap.appendChild(cardElement(card, true));
const up = document.createElement("button");
up.className="gold"; up.style.marginTop="6px"; up.textContent="Upgrade";
up.onclick=()=>{ card.upgraded=true; toast(card.name+" upgraded!"); state.mode="map"; render(); };
wrap.appendChild(up);
grid.appendChild(wrap);
});
panel.appendChild(grid);
const back = document.createElement("button");
back.className="primary"; back.style.marginTop="10px"; back.textContent="Back";
back.onclick=()=>renderRest(document.getElementById("app"));
panel.appendChild(back);
}
function restMeditate(){
askRemoveFromDeck("Meditate — remove a card from your deck");
}
function leaveRest(){ state.mode="map"; render(); }
// ---------- Death ----------
function renderDeath(app){
const panel = el("panel",{style:"text-align:center; margin-top:40px;"});
panel.innerHTML = `<div style="font-size:72px">💀</div>
<h2 class="red">You Have Died</h2>
<p class="big">The Abyss claims another soul...</p>
<p class="muted">Act ${state.actIndex+1}, Floor ${state.floorIndex+2}</p>
<div class="btn-row"><button class="primary" onclick="restart()">Start a New Run</button></div>`;
app.appendChild(panel);
}
function restart(){ state.mode="classselect"; render(); }
// ---------- Victory ----------
function renderVictory(app){
const panel = el("panel",{style:"text-align:center; margin-top:40px;"});
panel.innerHTML = `<div style="font-size:72px">👑</div>
<h2 class="gold-t">Victory!</h2>
<p class="big">You have defeated the Archdemon and conquered the Abyss.</p>
<p class="muted">Your cursed journey ends in triumph.</p>
<div class="btn-row"><button class="primary" onclick="restart()">Play Again</button></div>`;
app.appendChild(panel);
}
</script>
<script>
render();
</script>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"1c9266dd-a2e3-42fb-9ae0-f4559c20fd22","tokensIn":9847301,"tokensOut":101088,"tokensTotal":9948389,"cost":0,"turns":125,"toolCalls":124,"failedToolCalls":4,"timestamp":"2026-08-23T22:51:41.755Z"} -->