Shadows
Tiel Coder 35B A3B GGUF MTP · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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,472 bytes · SHA-256 fb18b0745c1e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shadows of the Abyss</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
:root{
--bg:#0a0710; --bg2:#140c1c; --panel:#1c1226; --panel2:#261833;
--gold:#d4af37; --blood:#a11d2a; --blood2:#c62828; --purple:#7b2ff7;
--teal:#2ec4b6; --text:#e8e0f0; --dim:#9a8bb0; --danger:#e53935;
--card-common:#3a3a4a; --card-uncommon:#2d5a3d; --card-rare:#2a3f7a;
}
html,body{ height:100%; }
body{
font-family:'Trebuchet MS','Segoe UI',sans-serif;
background:radial-gradient(circle at 50% 0%,#1a0f24 0%,var(--bg) 60%);
color:var(--text); overflow:hidden; user-select:none;
}
#app{ position:fixed; inset:0; display:flex; flex-direction:column; }
.screen{ position:absolute; inset:0; display:none; flex-direction:column; }
.screen.active{ display:flex; }
h1{ font-size:3rem; text-align:center; letter-spacing:4px;
background:linear-gradient(90deg,var(--blood2),var(--gold),var(--purple));
-webkit-background-clip:text; background-clip:text; color:transparent;
text-shadow:0 0 30px rgba(161,29,42,.3); margin:auto; }
h2{ font-size:1.5rem; color:var(--gold); letter-spacing:2px; margin-bottom:12px; }
.sub{ text-align:center; color:var(--dim); margin:10px 0 30px; font-style:italic; }
/* Class select */
#classSelect{ justify-content:center; align-items:center; gap:30px; padding:20px; }
.classGrid{ display:flex; gap:24px; flex-wrap:wrap; justify-content:center; }
.classCard{
width:260px; background:var(--panel); border:2px solid #3a2b4d; border-radius:14px;
padding:22px; cursor:pointer; transition:.2s; text-align:center; position:relative;
}
.classCard:hover{ transform:translateY(-8px); border-color:var(--gold);
box-shadow:0 10px 40px rgba(212,175,55,.25); }
.classIcon{ font-size:4rem; margin-bottom:10px; }
.classCard h3{ color:var(--gold); font-size:1.4rem; margin-bottom:8px; }
.classCard .desc{ color:var(--dim); font-size:.85rem; line-height:1.4; margin-bottom:12px; }
.classCard .start{ font-size:.8rem; color:var(--text); }
.classCard .sig{ margin-top:10px; font-size:.8rem; color:var(--teal); }
.btn{
background:linear-gradient(180deg,var(--panel2),var(--panel));
border:1px solid var(--gold); color:var(--gold); padding:10px 22px;
border-radius:8px; cursor:pointer; font-size:1rem; letter-spacing:1px;
transition:.15s; font-family:inherit;
}
.btn:hover{ background:var(--gold); color:var(--bg); }
.btn:disabled{ opacity:.4; cursor:not-allowed; }
.btn.danger{ border-color:var(--danger); color:var(--danger); }
.btn.danger:hover{ background:var(--danger); color:#fff; }
.btn.small{ padding:6px 14px; font-size:.85rem; }
/* Top HUD */
#hud{ display:flex; align-items:center; justify-content:space-between;
padding:8px 16px; background:rgba(0,0,0,.4); border-bottom:1px solid #2a1c38; }
#hud .info{ display:flex; gap:18px; align-items:center; font-size:.95rem; }
#hud .gold{ color:var(--gold); font-weight:bold; }
#hud .act{ color:var(--purple); font-weight:bold; }
/* Map */
#mapScreen{ justify-content:flex-start; }
#mapScroll{ flex:1; overflow:auto; padding:30px; position:relative; }
#mapCanvas{ position:absolute; inset:0; pointer-events:none; }
.mapNodes{ position:relative; z-index:2; display:flex; flex-direction:column;
align-items:center; gap:26px; max-width:700px; margin:0 auto; }
.mapRow{ display:flex; gap:40px; justify-content:center; align-items:center; position:relative; }
.mapNode{
width:64px; height:64px; border-radius:50%; display:flex; align-items:center;
justify-content:center; font-size:1.6rem; cursor:pointer; position:relative;
border:2px solid #444; background:var(--panel); transition:.15s;
}
.mapNode .floorNum{ position:absolute; bottom:-18px; font-size:.65rem; color:var(--dim); }
.mapNode.enemy{ border-color:var(--danger); }
.mapNode.elite{ border-color:#ff7f00; box-shadow:0 0 12px rgba(255,127,0,.5); }
.mapNode.rest{ border-color:var(--teal); }
.mapNode.shop{ border-color:var(--gold); }
.mapNode.event{ border-color:var(--purple); }
.mapNode.boss{ border-color:var(--blood2); box-shadow:0 0 20px rgba(198,40,40,.7); font-size:2rem; }
.mapNode.available{ animation:pulse 1.2s infinite; }
.mapNode.cleared{ opacity:.5; }
.mapNode.current{ box-shadow:0 0 0 3px var(--gold),0 0 20px var(--gold); transform:scale(1.15); }
.mapNode.locked{ opacity:.35; cursor:not-allowed; }
@keyframes pulse{ 0%,100%{filter:brightness(1);} 50%{filter:brightness(1.5)); }
.nodeLabel{ position:absolute; top:70px; width:120px; text-align:center;
font-size:.7rem; color:var(--dim); z-index:5; }
/* Combat */
#combatScreen{ justify-content:flex-start; }
#battlefield{ flex:1; display:flex; flex-direction:column; position:relative; overflow:hidden; }
.enemyRow{ display:flex; justify-content:center; gap:40px; flex:1; align-items:center; }
.enemy{
width:130px; text-align:center; cursor:crosshair; transition:.15s; position:relative;
}
.enemy.targetable{ outline:2px solid var(--danger); border-radius:12px; }
.enemy.dead{ opacity:0; transform:scale(.3) rotate(90deg); }
.enemyArt{ font-size:3.5rem; filter:drop-shadow(0 4px 8px rgba(0,0,0,.6)); transition:.2s; }
.enemy.hit .enemyArt{ animation:shake .3s; filter:brightness(2) hue-rotate(-30deg); }
@keyframes shake{ 0%,100%{transform:translateX(0);} 25%{transform:translateX(-8px);} 75%{transform:translateX(8px);} }
.enemyName{ font-size:.8rem; color:var(--text); margin-top:4px; }
.hpBar{ width:110px; height:12px; background:#300; border-radius:6px; margin:4px auto; overflow:hidden; border:1px solid #500; }
.hpFill{ height:100%; background:linear-gradient(90deg,var(--blood),var(--blood2)); transition:width .3s; }
.enemyStats{ font-size:.7rem; color:var(--dim); }
.intentBox{ position:absolute; top:-6px; left:50%; transform:translateX(-50%);
background:rgba(0,0,0,.7); border:1px solid var(--gold); border-radius:6px;
padding:2px 6px; font-size:.7rem; white-space:nowrap; color:var(--gold); }
.statusPips{ display:flex; gap:3px; justify-content:center; margin-top:3px; min-height:16px; }
.pip{ font-size:.6rem; background:#2a1c38; border-radius:4px; padding:1px 4px; color:var(--text); }
.blockBadge{ position:absolute; top:-6px; right:0; background:#3a5a8a; border-radius:6px;
padding:1px 6px; font-size:.75rem; border:1px solid #6a8aba; }
/* Player combat */
#playerPanel{ position:relative; height:150px; display:flex; align-items:center; padding:0 20px; gap:16px; }
#playerArt{ font-size:3.5rem; position:relative; }
#playerArt .playerBlock{ position:absolute; top:0; right:-6px; background:#3a5a8a;
border-radius:8px; padding:2px 8px; font-size:1rem; border:2px solid #6a8aba; }
#playerHUD{ display:flex; flex-direction:column; gap:6px; min-width:140px; }
#hpBar{ width:180px; height:18px; background:#300; border-radius:9px; overflow:hidden; border:2px solid #700; }
#hpFill{ height:100%; background:linear-gradient(90deg,#8b0000,var(--blood2)); transition:width .3s; }
.playerStat{ font-size:.8rem; color:var(--dim); display:flex; justify-content:space-between; }
#energyOrb{ font-size:1.4rem; color:var(--teal); font-weight:bold; }
.buffsRow{ display:flex; gap:4px; }
.buff{ font-size:.65rem; background:#2a1c38; border:1px solid #4a3a5d; border-radius:4px; padding:1px 5px; }
.buff.weak{ color:#8ec5ff; } .buff.vulnerable{ color:#ff9800; }
.buff.bleed{ color:#e53935; } .buff.burn{ color:#ff5722; } .buff.regen{ color:var(--teal); }
.buff.intangible{ color:#b39dff; } .buff.strength{ color:var(--gold); } .buff.dex{ color:#4fc3f7; }
/* Hand */
#hand{ display:flex; justify-content:center; gap:10px; padding:10px; min-height:190px; align-items:flex-end; position:relative; }
.card{
width:130px; height:185px; background:var(--panel); border:2px solid #555; border-radius:10px;
cursor:pointer; transition:.15s; position:relative; display:flex; flex-direction:column;
padding:8px; flex-shrink:0;
}
.card:hover{ transform:translateY(-16px); z-index:10; box-shadow:0 12px 30px rgba(0,0,0,.6); }
.card.common{ border-color:#777; }
.card.uncommon{ border-color:#4caf50; }
.card.rare{ border-color:#3f5efb; }
.card.curse{ border-color:#6a1b3a; background:#2a0f1a; }
.card.ethereal{ opacity:.85; }
.card.unplayable{ filter:grayscale(.6); }
.card .cost{ position:absolute; top:-8px; left:-8px; width:28px; height:28px; border-radius:50%;
background:radial-gradient(circle at 30% 30%,#4dd0e1,#00838f); color:#003; font-weight:bold;
display:flex; align-items:center; justify-content:center; font-size:1rem; border:2px solid #b2ebf2; }
.card .cname{ text-align:center; font-size:.8rem; color:var(--gold); font-weight:bold; margin:6px 0 2px; }
.card .ctype{ text-align:center; font-size:.6rem; color:var(--dim); margin-bottom:4px; }
.card .cimg{ text-align:center; font-size:2rem; flex:1; display:flex; align-items:center; justify-content:center; }
.card .cdesc{ text-align:center; font-size:.62rem; color:var(--text); line-height:1.3; }
.card .cbar{ height:6px; border-radius:3px; margin-top:6px; }
.card.common .cbar{ background:#555; } .card.uncommon .cbar{ background:#4caf50; }
.card.rare .cbar{ background:#3f5efb; } .card.curse .cbar{ background:#6a1b3a; }
.card.playing{ animation:playCard .3s forwards; }
@keyframes playCard{ to{ transform:translateY(-260px) scale(.7); opacity:0; } }
.card .exhaustTag{ position:absolute; bottom:2px; right:4px; font-size:.55rem; color:var(--blood2); }
/* Controls */
#combatControls{ position:absolute; right:16px; bottom:210px; display:flex; flex-direction:column; gap:8px; align-items:center; z-index:20; }
#endTurnBtn{ font-size:1.1rem; padding:12px 20px; }
#potionBar{ position:absolute; left:16px; bottom:210px; display:flex; gap:8px; z-index:20; }
.potion{ width:44px; height:44px; border-radius:8px; background:var(--panel); border:2px solid #666;
display:flex; align-items:center; justify-content:center; font-size:1.4rem; cursor:pointer; transition:.15s; }
.potion:hover{ transform:translateY(-4px); border-color:var(--gold); }
.floatText{ position:absolute; font-weight:bold; font-size:1.4rem; pointer-events:none;
animation:floatUp 1s forwards; z-index:30; }
@keyframes floatUp{ to{ transform:translateY(-50px); opacity:0; } }
.floatText.dmg{ color:#ff5252; } .floatText.heal{ color:#66bb6a; } .floatText.block{ color:#90caf9; }
.floatText.intangible{ color:#b39dff; }
/* Overlay panels (reward, shop, rest, event, gameover) */
.overlay{ position:absolute; inset:0; background:rgba(5,2,10,.92); z-index:50;
display:none; flex-direction:column; align-items:center; justify-content:center; padding:30px; }
.overlay.active{ display:flex; }
.overlayBox{ max-width:900px; width:100%; text-align:center; }
.rewardCards{ display:flex; gap:20px; justify-content:center; flex-wrap:wrap; margin:20px 0; }
.rewardCards .card{ opacity:.95; }
.rewardSkip{ margin-top:10px; }
.rewardPotion, .rewardRelic, .rewardGold{ display:inline-block; margin:8px; padding:10px 16px;
background:var(--panel); border:1px solid var(--gold); border-radius:8px; }
.listChoices{ display:flex; flex-direction:column; gap:12px; align-items:center; margin:20px 0; }
.listChoices .choice{ text-align:left; max-width:520px; }
.msgText{ font-size:1.1rem; line-height:1.6; color:var(--text); margin:16px 0; }
.bigText{ font-size:2.5rem; margin:20px 0; }
.victory .bigText{ background:linear-gradient(90deg,var(--gold),var(--teal));
-webkit-background-clip:text; background-clip:text; color:transparent; }
.defeat .bigText{ color:var(--blood2); }
.shopGrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:14px; margin:16px 0; }
.shopItem{ background:var(--panel2); border:1px solid #444; border-radius:10px; padding:10px; text-align:center; }
.shopItem .price{ color:var(--gold); margin-top:6px; }
.shopItem.sold{ opacity:.4; }
.itemIcon{ font-size:2rem; }
.tooltip{ font-size:.7rem; color:var(--dim); }
.center-wrap{ display:flex; flex-direction:column; align-items:center; gap:14px; }
#log{ position:absolute; left:0; right:0; bottom:195px; max-height:90px; overflow:hidden;
font-size:.72rem; color:var(--dim); text-align:center; pointer-events:none; z-index:15; }
#log div{ padding:1px 0; }
.banner{ position:absolute; top:40%; left:0; right:0; text-align:center; z-index:40;
font-size:2.5rem; color:var(--gold); text-shadow:0 0 20px #000; pointer-events:none;
animation:bannerPop 1.2s forwards; }
@keyframes bannerPop{ 0%{transform:scale(.5); opacity:0;} 30%{transform:scale(1.2); opacity:1;} 100%{transform:scale(1); opacity:0;} }
</style>
</head>
<body>
<div id="app">
<!-- Class Select -->
<div id="classSelect" class="screen active">
<div>
<h1>SHADOWS OF THE ABYSS</h1>
<p class="sub">Three cursed archetypes. One dying world. Climb. Battle. Survive.</p>
</div>
<div class="classGrid" id="classGrid"></div>
</div>
<!-- Map -->
<div id="mapScreen" class="screen">
<div id="hud">
<div class="info"><span class="act" id="hudAct">Act 1</span> <span id="hudClass">Class</span></div>
<div class="info"><span class="gold">◈ <span id="hudGold">0</span></span> <span>HP <span id="hudHp">0</span>/<span id="hudMaxHp">0</span></span></div>
</div>
<div id="mapScroll">
<canvas id="mapCanvas"></canvas>
<div class="mapNodes" id="mapNodes"></div>
</div>
</div>
<!-- Combat -->
<div id="combatScreen" class="screen">
<div id="hud">
<div class="info"><span class="act" id="combatAct">Act 1</span> <span id="combatClass">Class</span></div>
<div class="info"><span class="gold">◈ <span id="combatGold">0</span></span> <span id="combatPotions"></span></div>
</div>
<div id="battlefield">
<div class="enemyRow" id="enemyRow"></div>
<div id="log"></div>
<div id="playerPanel">
<div id="playerArt">🧙<span class="playerBlock" id="playerBlock" style="display:none">0</span></div>
<div id="playerHUD">
<div id="hpBar"><div id="hpFill" style="width:100%"></div></div>
<div class="playerStat"><span>HP</span><span id="playerHpText">70/70</span></div>
<div class="playerStat"><span id="energyOrb">⚡3/3</span></div>
<div class="playerStat"><span>Draw:<span id="drawCount">0</span> | Discard:<span id="discardCount">0</span></span></div>
<div class="buffsRow" id="playerBuffs"></div>
</div>
</div>
</div>
<div id="potionBar"></div>
<div id="hand"></div>
<div id="combatControls">
<button class="btn" id="endTurnBtn">End Turn</button>
</div>
</div>
<!-- Overlays -->
<div id="overlay" class="overlay"><div class="overlayBox" id="overlayBox"></div></div>
</div>
<script>
"use strict";
"use strict";
// ============================================================
// SHADOWS OF THE ABYSS — game engine
// ============================================================
// ---------- Utility ----------
const rand = (a,b)=>Math.floor(Math.random()*(b-a+1))+a;
const pick = arr => arr[Math.floor(Math.random()*arr.length)];
const shuffle = arr => { const a=[...arr]; for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];} return a; };
const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
// ---------- Card definitions ----------
// base effect function returns an array of actions to perform on play.
// Each card: {id, name, type, cost, rarity, img, cls, desc, upg, func, exhaust, ethereal, playable}
// func(ctx) performs effects. ctx = combat state.
const CARDS = {};
function defCard(c){ CARDS[c.id]=c; return c; }
// Basic cards (shared)
defCard({id:'strike',name:'Strike',type:'Attack',cost:1,rarity:'Basic',img:'⚔️',desc:'Deal 6 damage.',upg:'Deal 8 damage.',func:ctx=>{ ctx.damage(ctx.target,6,ctx.player); }});
defCard({id:'defend',name:'Defend',type:'Skill',cost:1,rarity:'Basic',img:'🛡️',desc:'Gain 5 Block.',upg:'Gain 8 Block.',func:ctx=>{ ctx.player.block += ctx.player.dex? ctx.player.dex:0; ctx.player.block += 5; }});
// ---- Blood Reaper ----
defCard({id:'sanguine',name:'Sanguine Strike',type:'Attack',cost:1,rarity:'Common',img:'🩸',cls:'blood',desc:'Deal 8 damage. Heal 2 HP.',upg:'Deal 10 damage. Heal 3 HP.',func:ctx=>{ const d=ctx.damage(ctx.target,8,ctx.player); ctx.player.heal(2); if(ctx.relics.bloodstone) ctx.player.heal(1); }});
defCard({id:'hemorrhage',name:'Hemorrhage',type:'Attack',cost:1,rarity:'Common',img:'🔪',cls:'blood',desc:'Deal 5 damage. Apply 3 Bleed.',upg:'Deal 6 damage. Apply 4 Bleed.',func:ctx=>{ ctx.damage(ctx.target,5,ctx.player); ctx.applyToEnemy(ctx.target,'bleed',3); }});
defCard({id:'bloodletting',name:'Bloodletting',type:'Attack',cost:1,rarity:'Uncommon',img:'🩸',cls:'blood',desc:'Lose 3 HP. Deal 12 damage.',upg:'Lose 2 HP. Deal 15 damage.',func:ctx=>{ if(ctx.player.hp>3) ctx.player.takeDamage(3); ctx.damage(ctx.target,12,ctx.player); }});
defCard({id:'crimsonEruption',name:'Crimson Eruption',type:'Attack',cost:2,rarity:'Rare',img:'🌋',cls:'blood',desc:'Deal 8 damage to ALL enemies. Lose 2 HP.',upg:'Deal 10 damage to ALL. Lose 2 HP.',func:ctx=>{ ctx.player.takeDamage(2); ctx.damageAll(8,ctx.player); }});
defCard({id:'crimsonShield',name:'Crimson Shield',type:'Skill',cost:1,rarity:'Common',img:'🛡️',cls:'blood',desc:'Gain 6 Block. Lose 2 HP.',upg:'Gain 8 Block. Lose 2 HP.',func:ctx=>{ if(ctx.player.hp>2) ctx.player.takeDamage(2); ctx.player.block += (6+ctx.player.dex); }});
defCard({id:'bloodWard',name:'Blood Ward',type:'Skill',cost:1,rarity:'Common',img:'🛡️',cls:'blood',desc:'Gain 3 Block. Gain 1 Regeneration.',upg:'Gain 4 Block. Gain 2 Regeneration.',func:ctx=>{ ctx.player.block += (3+ctx.player.dex); ctx.player.addBuff('regen',1,1); }});
defCard({id:'sacrificialBarrier',name:'Sacrificial Barrier',type:'Skill',cost:2,rarity:'Uncommon',img:'🛡️',cls:'blood',desc:'Gain 10 Block. Lose 5 HP.',upg:'Gain 13 Block. Lose 5 HP.',func:ctx=>{ if(ctx.player.hp>5) ctx.player.takeDamage(5); ctx.player.block += (10+ctx.player.dex); }});
defCard({id:'vitalSurge',name:'Vital Surge',type:'Skill',cost:0,rarity:'Rare',img:'💚',cls:'blood',desc:'Gain Block equal to missing HP. Exhaust.',upg:'Gain Block = missing HP + 3. Exhaust.',exhaust:true,func:ctx=>{ const miss=ctx.player.maxHp-ctx.player.hp; ctx.player.block += (miss+3+ctx.player.dex); }});
defCard({id:'lifeTap',name:'Life Tap',type:'Skill',cost:0,rarity:'Common',img:'📖',cls:'blood',desc:'Draw 2 cards. Lose 3 HP.',upg:'Draw 3 cards. Lose 3 HP.',func:ctx=>{ if(ctx.player.hp>3) ctx.player.takeDamage(3); ctx.drawCards(ctx.player.upgrade?3:2); }});
defCard({id:'rendFlesh',name:'Rend Flesh',type:'Skill',cost:1,rarity:'Uncommon',img:'🔪',cls:'blood',desc:'Apply 5 Bleed to ALL enemies. Lose 2 HP.',upg:'Apply 7 Bleed to ALL. Lose 2 HP.',func:ctx=>{ ctx.player.takeDamage(2); ctx.damageAll(0,ctx.player,true,7); }});
defCard({id:'soulSiphon',name:'Soul Siphon',type:'Power',cost:2,rarity:'Uncommon',img:'💀',cls:'blood',desc:'Start of turn: gain 1 Energy and lose 1 HP.',upg:'Cost 1. Same effect.',func:()=>{}});
defCard({id:'bloodPact',name:'Blood Pact',type:'Power',cost:1,rarity:'Rare',img:'🩸',cls:'blood',desc:'Lose HP from a card: gain 1 Strength.',upg:'gain 2 Strength.',func:()=>{}});
// ---- Shadowmancer ----
defCard({id:'shadowBolt',name:'Shadow Bolt',type:'Attack',cost:1,rarity:'Common',img:'🔮',cls:'shadow',desc:'Deal 6 damage. Apply 1 Weak.',upg:'Deal 8 damage. Apply 2 Weak.',func:ctx=>{ ctx.damage(ctx.target,6,ctx.player); ctx.applyToEnemy(ctx.target,'weak',1); }});
defCard({id:'graspDespair',name:'Grasp of Despair',type:'Attack',cost:1,rarity:'Common',img:'🕷️',cls:'shadow',desc:'Deal 4 damage. Apply 1 Vulnerable.',upg:'Deal 5 damage. Apply 2 Vulnerable.',func:ctx=>{ ctx.damage(ctx.target,4,ctx.player); ctx.applyToEnemy(ctx.target,'vulnerable',1); }});
defCard({id:'nightmareLash',name:'Nightmare Lash',type:'Attack',cost:2,rarity:'Uncommon',img:'🌙',cls:'shadow',desc:'Deal 7 damage. If enemy Weak, +7 damage.',upg:'Deal 9 damage. If Weak, +9.',func:ctx=>{ let d=7; if(ctx.target.statuses.weak>0) d+=7; ctx.damage(ctx.target,d,ctx.player); }});
defCard({id:'eternalDarkness',name:'Eternal Darkness',type:'Attack',cost:2,rarity:'Rare',img:'🌑',cls:'shadow',desc:'Deal 3 damage to a random enemy 3 times.',upg:'Deal 4 damage, 3 times.',func:ctx=>{ const alive=ctx.enemies.filter(e=>e.hp>0); for(let i=0;i<3;i++){ const e=pick(alive); ctx.damage(e,3,ctx.player); if(e.hp<=0){ const a=ctx.enemies.filter(x=>x.hp>0); if(a.length) alive.splice(alive.indexOf(e),1); alive.push(e);} }});
defCard({id:'cloakShadows',name:'Cloak of Shadows',type:'Skill',cost:1,rarity:'Common',img:'🌫️',cls:'shadow',desc:'Gain 5 Block. Draw 1 card.',upg:'Gain 7 Block. Draw 1 card.',func:ctx=>{ ctx.player.block += (5+ctx.player.dex); ctx.drawCards(1); }});
defCard({id:'hauntingMist',name:'Haunting Mist',type:'Skill',cost:1,rarity:'Common',img:'🌫️',cls:'shadow',desc:'Gain 7 Block. Apply 1 Weak to ALL enemies.',upg:'Gain 9 Block. Apply 1 Weak to ALL.',func:ctx=>{ ctx.player.block += (7+ctx.player.dex); for(const e of ctx.enemies) if(e.hp>0) ctx.applyToEnemy(e,'weak',1); }});
defCard({id:'phantomArmor',name:'Phantom Armor',type:'Skill',cost:1,rarity:'Uncommon',img:'🛡️',cls:'shadow',desc:'Gain 8 Block. Next turn gain 2 Dexterity.',upg:'Gain 10 Block. Next turn gain 3 Dexterity.',func:ctx=>{ ctx.player.block += (8+ctx.player.dex); ctx.player.pendingDex = (ctx.player.upgrade?3:2); }});
defCard({id:'whisperingShroud',name:'Whispering Shroud',type:'Skill',cost:1,rarity:'Rare',img:'🛡️',cls:'shadow',desc:'Gain 4 Block. Next turn gain 4 Block.',upg:'Gain 5 Block. Next turn gain 5 Block.',func:ctx=>{ ctx.player.block += (4+ctx.player.dex); ctx.player.pendingBlock = (ctx.player.upgrade?5:4); }});
defCard({id:'shadowform',name:'Shadowform',type:'Skill',cost:2,rarity:'Uncommon',img:'👤',cls:'shadow',desc:'Gain 2 Intangible. Exhaust.',upg:'Gain 3 Intangible. Exhaust.',exhaust:true,func:ctx=>{ ctx.player.addBuff('intangible',2,1); }});
defCard({id:'curseMisfortune',name:'Curse of Misfortune',type:'Skill',cost:1,rarity:'Common',img:'☠️',cls:'shadow',desc:'Enemy loses 3 Strength this turn. Apply 1 Vulnerable.',upg:'Loses 4 Strength. Apply 2 Vulnerable.',func:ctx=>{ ctx.applyToEnemy(ctx.target,'vulnerable',1); ctx.pendingStrengthDmg = (ctx.pendingStrengthDmg||0); ctx.target._tempStrengthLoss = (ctx.target._tempStrengthLoss||0)+(ctx.player.upgrade?4:3); }});
defCard({id:'darkRitual',name:'Dark Ritual',type:'Skill',cost:0,rarity:'Uncommon',img:'📖',cls:'shadow',desc:'Gain 2 Energy. Shuffle a Dazed into draw pile.',upg:'Gain 2 Energy. Shuffle Dazed into discard.',func:ctx=>{ ctx.player.energy += 2; ctx.shuffleDazed(ctx.player.upgrade?'discard':'draw'); }});
defCard({id:'umbralMastery',name:'Umbral Mastery',type:'Power',cost:2,rarity:'Rare',img:'👁️',cls:'shadow',desc:'Start of turn: apply 1 Weak to ALL enemies.',upg:'Apply 2 Weak to ALL.',func:()=>{}});
// ---- Hellforged ----
defCard({id:'firebolt',name:'Firebolt',type:'Attack',cost:1,rarity:'Common',img:'🔥',cls:'hell',desc:'Deal 7 damage. Apply 2 Burn.',upg:'Deal 9 damage. Apply 3 Burn.',func:ctx=>{ ctx.damage(ctx.target,7,ctx.player); ctx.applyToEnemy(ctx.target,'burn',2); }});
defCard({id:'hellfireBlast',name:'Hellfire Blast',type:'Attack',cost:2,rarity:'Uncommon',img:'🔥',cls:'hell',desc:'Deal 9 damage to ALL enemies.',upg:'Deal 12 damage to ALL.',func:ctx=>{ ctx.damageAll(9,ctx.player); }});
defCard({id:'searingStrike',name:'Searing Strike',type:'Attack',cost:1,rarity:'Common',img:'🗡️',cls:'hell',desc:'Deal 6 damage. Gain 1 Energy next turn.',upg:'Deal 8 damage. Gain 1 Energy next turn.',func:ctx=>{ ctx.damage(ctx.target,6,ctx.player); ctx.player.pendingEnergy = (ctx.player.pendingEnergy||0)+1; }});
defCard({id:'immolate',name:'Immolate',type:'Attack',cost:2,rarity:'Rare',img:'🔥',cls:'hell',desc:'Deal 12 damage. Exhaust.',upg:'Deal 16 damage. Exhaust.',exhaust:true,func:ctx=>{ ctx.damage(ctx.target,12,ctx.player); }});
defCard({id:'obsidianSkin',name:'Obsidian Skin',type:'Skill',cost:1,rarity:'Common',img:'🛡️',cls:'hell',desc:'Gain 7 Block.',upg:'Gain 10 Block.',func:ctx=>{ ctx.player.block += (7+ctx.player.dex); }});
defCard({id:'flameWard',name:'Flame Ward',type:'Skill',cost:1,rarity:'Common',img:'🛡️',cls:'hell',desc:'Gain 5 Block. Apply 2 Burn to a random enemy.',upg:'Gain 6 Block. Apply 3 Burn.',func:ctx=>{ const e=pick(ctx.enemies.filter(x=>x.hp>0)); ctx.player.block += (5+ctx.player.dex); ctx.applyToEnemy(e,'burn',ctx.player.upgrade?3:2); }});
defCard({id:'demonicResilience',name:'Demonic Resilience',type:'Skill',cost:1,rarity:'Uncommon',img:'💚',cls:'hell',desc:'Gain 4 Block. Heal 2 HP.',upg:'Gain 5 Block. Heal 3 HP.',func:ctx=>{ ctx.player.block += (4+ctx.player.dex); ctx.player.heal(2); }});
defCard({id:'brimstoneShield',name:'Brimstone Shield',type:'Skill',cost:1,rarity:'Rare',img:'🛡️',cls:'hell',desc:'Gain 3 Block. Gain 2 Strength this turn.',upg:'Gain 4 Block. Gain 3 Strength this turn.',func:ctx=>{ ctx.player.block += (3+ctx.player.dex); ctx.player.addBuff('strength',2,1); }});
defCard({id:'summonImp',name:'Summon Imp',type:'Skill',cost:1,rarity:'Common',img:'👺',cls:'hell',desc:'Add an Imp Claw to hand. Exhaust.',upg:'Add Imp Claw+.',exhaust:true,func:ctx=>{ const c = makeImpClaw(ctx.player.upgrade); ctx.player.hand.push(c); }});
defCard({id:'infernalPact',name:'Infernal Pact',type:'Skill',cost:0,rarity:'Uncommon',img:'🔥',cls:'hell',desc:'Lose 2 HP. Gain 2 Energy. Add Imp Claw to hand.',upg:'Lose 1 HP. Gain 2 Energy. Add Imp Claw+.',func:ctx=>{ if(ctx.player.hp>1) ctx.player.takeDamage(ctx.player.upgrade?1:2); ctx.player.energy += 2; const c = makeImpClaw(ctx.player.upgrade); ctx.player.hand.push(c); }});
defCard({id:'hellfireAura',name:'Hellfire Aura',type:'Power',cost:2,rarity:'Rare',img:'🔥',cls:'hell',desc:'Start of turn: deal 3 damage to ALL enemies.',upg:'Deal 5 damage to ALL.',func:()=>{}});
defCard({id:'soulBurn',name:'Soul Burn',type:'Skill',cost:1,rarity:'Uncommon',img:'🔥',cls:'hell',desc:'Apply 8 Burn to an enemy. Draw 1 card.',upg:'Apply 10 Burn. Draw 1 card.',func:ctx=>{ ctx.applyToEnemy(ctx.target,'burn',ctx.player.upgrade?10:8); ctx.drawCards(1); }});
// Imp Claw token
function makeImpClaw(upgraded){ return {id:'impClaw',name:upgraded?'Imp Claw+':'Imp Claw',type:'Attack',cost:0,rarity:'Basic',img:'👺',desc:upgraded?'Deal 6 damage.':'Deal 4 damage.',upg:'',exhaust:true,token:true,func:ctx=>{ ctx.damage(ctx.target, upgraded?6:4, ctx.player); }}; }
// Colorless
defCard({id:'flashOfInsight',name:'Flash of Insight',type:'Skill',cost:1,rarity:'Common',img:'📖',desc:'Draw 2 cards.',upg:'Draw 3 cards.',func:ctx=>{ ctx.drawCards(ctx.player.upgrade?3:2); }});
defCard({id:'ironWill',name:'Iron Will',type:'Skill',cost:1,rarity:'Common',img:'🛡️',desc:'Gain 8 Block.',upg:'Gain 11 Block.',func:ctx=>{ ctx.player.block += 8+ctx.player.dex; }});
defCard({id:'quickSlash',name:'Quick Slash',type:'Attack',cost:1,rarity:'Common',img:'🗡️',desc:'Deal 7 damage.',upg:'Deal 9 damage.',func:ctx=>{ ctx.damage(ctx.target,7,ctx.player); }});
defCard({id:'potentBrew',name:'Potent Brew',type:'Skill',cost:0,rarity:'Uncommon',img:'⚗️',desc:'Gain 1 Energy. Exhaust.',upg:'Gain 2 Energy. Exhaust.',exhaust:true,func:ctx=>{ ctx.player.energy += ctx.player.upgrade?2:1; }});
// Curses / status
defCard({id:'wound',name:'Wound',type:'Curse',cost:0,rarity:'Basic',img:'☠️',curse:true,ethereal:true,exhaust:true,desc:'Unplayable. Ethereal.',func:()=>{}});
defCard({id:'dazed',name:'Dazed',type:'Status',cost:0,rarity:'Basic',img:'💫',curse:true,ethereal:true,exhaust:true,desc:'Unplayable. Ethereal.',func:()=>{}});
defCard({id:'doubt',name:'Doubt',type:'Curse',cost:0,rarity:'Basic',img:'❓',curse:true,desc:'Unplayable. End of turn, lose 1 HP.',func:()=>{}});
defCard({id:'impClawCard',name:'Imp Claw',type:'Attack',cost:0,rarity:'Basic',img:'👺',desc:'Deal 4 damage. Exhaust.',func:()=>{}});
// ---------- Card pools per class ----------
const CLASS_POOLS = {
blood: ['strike','strike','strike','strike','strike','defend','defend','defend','defend','sanguine','hemorrhage','bloodletting','crimsonEruption','crimsonShield','bloodWard','sacrificialBarrier','vitalSurge','lifeTap','rendFlesh','soulSiphon','bloodPact'],
shadow: ['strike','strike','strike','strike','strike','defend','defend','defend','defend','shadowBolt','graspDespair','nightmareLash','eternalDarkness','cloakShadows','hauntingMist','phantomArmor','whisperingShroud','shadowform','curseMisfortune','darkRitual','umbralMastery'],
hell: ['strike','strike','strike','strike','strike','defend','defend','defend','defend','firebolt','hellfireBlast','searingStrike','immolate','obsidianSkin','flameWard','demonicResilience','brimstoneShield','summonImp','infernalPact','hellfireAura','soulBurn']
};
// ---------- Relics ----------
const RELICS = {
bloodstone:{name:'Bloodstone',img:'🔴',rarity:'Common',desc:'Whenever you lose HP from a card, heal 1 HP.'},
demonHorns:{name:'Demon Horns',img:'🐉',rarity:'Uncommon',desc:'Start of combat: add a random Imp Claw to hand (once per combat). Hellforged only.',cls:'hell'},
shadowCloak:{name:'Shadow Cloak',img:'🟣',rarity:'Rare',desc:'Start of combat: gain 1 Intangible.'},
cursedAmulet:{name:'Cursed Amulet',img:'📿',rarity:'Common',desc:'Start of combat: apply 1 Weak to ALL enemies.'},
tomeDark:{name:'Tome of Dark Rituals',img:'📚',rarity:'Uncommon',desc:'Whenever you play a Power card, draw 1 card.'},
bleedingEdge:{name:'Bleeding Edge',img:'🔪',rarity:'Common',desc:'Start of your turn: apply 1 Bleed to a random enemy.'},
soulEmber:{name:'Soul Ember',img:'🔥',rarity:'Rare',desc:'Whenever you apply Burn to an enemy, deal 2 additional damage.'},
eternalHourglass:{name:'Eternal Hourglass',img:'⏳',rarity:'Uncommon',desc:'Start of turn: if no cards in hand, gain 1 Energy.'},
sacrificialDagger:{name:'Sacrificial Dagger',img:'🗡️',rarity:'Rare',desc:'Your first Attack that costs HP each combat costs 1 less HP. Blood Reaper only.',cls:'blood'},
vialShadows:{name:'Vial of Shadows',img:'🧪',rarity:'Rare',desc:'Upon obtaining, remove a card from your deck (one-time).',oneTime:true},
ashenArmor:{name:'Ashen Armor',img:'🛠️',rarity:'Common',desc:'At the end of combat, heal 6 HP.'},
greedyImp:{name:'Greedy Imp',img:'🪙',rarity:'Common',desc:'Enemies drop 10 extra Gold.'},
bossCursedCrown:{name:'Cursed Crown',img:'👑',rarity:'Boss',desc:'Gain 1 Energy at start of each turn. Curses can no longer be removed.'},
bossEctoplasm:{name:'Ectoplasm',img:'👻',rarity:'Boss',desc:'Gain 1 Energy. Lose all Gold and gain no more Gold.'},
bossPhilosopher:{name:'Philosopher\'s Stone',img:'💎',rarity:'Boss',desc:'Gain 1 Energy. All enemies start combat with 1 Strength.'},
bossBlackBlood:{name:'Black Blood',img:'🩸',rarity:'Boss',desc:'Start of combat, lose 10% max HP. Playing an Attack heals 1 HP.'},
bossShadowAmulet:{name:'Shadow Amulet',img:'🌑',rarity:'Boss',desc:'Start each turn with 1 Intangible. Reduce max HP by 15.'}
};
const BOSS_RELIC_IDS = ['bossCursedCrown','bossEctoplasm','bossPhilosopher','bossBlackBlood','bossShadowAmulet'];
// ---------- Potions ----------
const POTIONS = {
bloodVial:{name:'Blood Vial',img:'❤️',effect:'heal15'},
firebomb:{name:'Firebomb',img:'💥',effect:'firebomb'},
dexTonic:{name:'Dex Tonic',img:'💪',effect:'dex'},
energyElixir:{name:'Energy Elixir',img:'🔵',effect:'energy'},
cursedBrew:{name:'Cursed Brew',img:'🟣',effect:'cursedBrew'},
bleedingFlask:{name:'Bleeding Flask',img:'🩸',effect:'bleeding'},
impInBottle:{name:'Imp-in-a-Bottle',img:'👺',effect:'impInBottle'},
smokeBomb:{name:'Smoke Bomb',img:'💨',effect:'smokeBomb'}
};
const POTION_IDS = Object.keys(POTIONS);
// ---------- Enemies ----------
// intent: function(ctx, enemy) -> {type, ...} describing next action
function makeEnemy(def, idx){
return {
id:def.id, name:def.name, hp:def.hp, maxHp:def.hp, idx,
img:def.img, alive:true,
statuses:{}, block:0, strength:0, _turn:0, _tempStrengthLoss:0,
pendingBlock:0, intent:null,
isMinion:def.isMinion||false
};
}
// Intent patterns
function enemyIntent(enemy, ctx){
const t = enemy._turn % enemy.pattern.length;
enemy._turn++;
return enemy.pattern[t];
}
function P(atk, dmg){ return {type:'attack', atk, dmg}; }
function Pdef(x){ return {type:'defend', x}; }
function Pstr(x){ return {type:'buff_strength', x}; }
function Psum(monk){ return {type:'summon', monk}; }
function Pmulti(n, d){ return {type:'multi', n, d}; }
function Pweak(x){ return {type:'apply_weak', x}; }
function Pvuln(x){ return {type:'apply_vulnerable', x}; }
function Pbled(x){ return {type:'apply_bleed', x}; }
function Pburn(x){ return {type:'apply_burn', x}; }
function Pbuff(x){ return {type:'buff', x}; }
function Pdrain(atk, heal){ return {type:'drain', atk, heal}; }
function PvulnAll(x){ return {type:'apply_vulnerable', x}; }
const ENEMY_DEFS = {
// Act 1
skeleton:{id:'skeleton',name:'Skeleton Warrior',hp:22,img:'💀',pattern:[P(6),P(6),Pdef(8)]},
zombie:{id:'zombie',name:'Zombie Brute',hp:28,img:'🧟',pattern:[P(8),Pweak(1),P(8)]},
cultist:{id:'cultist',name:'Dark Cultist',hp:20,img:'🧟♂️',pattern:[Pstr(3),P(5),P(5)]},
rat:{id:'rat',name:'Rat Swarm',hp:15,img:'🐀',pattern:[Pmulti(2,3),Pmulti(2,3)]},
tombGuardian:{id:'tombGuardian',name:'Tomb Guardian',hp:42,img:'🗿',pattern:[{type:'attack',atk:6,dmg:6,block:8},{type:'multi',n:3,d:4},{type:'attack',atk:12}]},
bloodWitch:{id:'bloodWitch',name:'Blood Witch',hp:36,img:'🧙♀️',pattern:[Pdrain(5,4),Pvuln(1),Pdrain(7,4)]},
lichKing:{id:'lichKing',name:'Lich King',hp:65,img:'🧟♂️',boss:true,
phase1:[Psum('skeletonMinion'),P(8),Pdef(12)],
phase2:[P(10),P(10),Pdef(15)]},
// Act 2
wraith:{id:'wraith',name:'Wraith',hp:30,img:'👻',pattern:[Pweak(1),P(7),Pdef(10)]},
beast:{id:'beast',name:'Corrupted Beast',hp:38,img:'🐻',pattern:[P(12),{type:'defend',x:5,type2:'vuln_self'},{type:'defend',x:5,type2:'vuln_self',atk:12}]},
thorned:{id:'thorned',name:'Thorned Horror',hp:25,img:'🌿',pattern:[Pmulti(3,4),Pbled(3),P(8)]},
witchDoc:{id:'witchDoc',name:'Witch Doctor',hp:22,img:'🧝♀️',pattern:[{type:'buff_self',block:4,regen:2},{type:'poison',atk:6,poison:2},{type:'poison',atk:6,poison:2}]},
treant:{id:'treant',name:'Treant Protector',hp:50,img:'🌳',pattern:[{type:'attack',atk:9,block:6},{type:'apply_vulnerable',x:2},{type:'multi',n:2,d:10}]},
plagueShaman:{id:'plagueShaman',name:'Plague Shaman',hp:42,img:'🧟♀️',pattern:[Pbled(4),Psum('plagueRat'),P(8)]},
eldritchTreant:{id:'eldritchTreant',name:'Eldritch Treant',hp:75,img:'🌲',boss:true,
phase1:[{type:'apply_weak',x:2,atk:10},{type:'defend',x:15,regen:2},{type:'multi',n:2,d:8}],
phase2:[{type:'apply_vulnerable',x:1,atk:14},{type:'defend',x:20,heal:5},{type:'apply_vulnerable',x:1,atk:14}]},
// Act 3
hellhound:{id:'hellhound',name:'Hellhound',hp:26,img:'🐕',pattern:[Pburn(2),P(8),Pdef(9)]},
shadowDemo:{id:'shadowDemo',name:'Shadow Demon',hp:30,img:'👹',pattern:[Pweak(1),P(8),Pdef(9)]},
infernalImp:{id:'infernalImp',name:'Infernal Imp',hp:18,img:'👺',pattern:[P(5),P(5),Psum('infernalImp')]},
abyssCultist:{id:'abyssCultist',name:'Abyssal Cultist',hp:24,img:'🕴️',pattern:[{type:'buff_all',str:2},P(7),P(7)]},
flamespawn:{id:'flamespawn',name:'Flamespawn',hp:48,img:'🔥',pattern:[Pburn(3),{type:'defend',x:10,buff_str:2},{type:'multi',n:2,d:8}]},
dreadKnight:{id:'dreadKnight',name:'Dread Knight',hp:55,img:'🛡️',pattern:[P(12),Pbled(3),{type:'defend',x:12,heal:4}]},
archdemon:{id:'archdemon',name:'Archdemon',hp:85,img:'😈',boss:true,
phase1:[Psum('hellhound'),Pburn(2),Pdef(15)],
phase2:[P(16),Pmulti(3,7),P(16)]}
};
function minionDef(kind){
if(kind==='skeletonMinion') return {id:'skeletonMinion',name:'Skeleton Minion',hp:15,img:'💀',isMinion:true,pattern:[P(5),P(5),Pdef(6)]};
if(kind==='plagueRat') return {id:'plagueRat',name:'Plague Rat',hp:18,img:'🐀',isMinion:true,pattern:[P(5),P(5),P(5)]};
if(kind==='infernalImp') return {id:'infernalImp',name:'Infernal Imp',hp:18,img:'👺',isMinion:true,pattern:[P(5),P(5),Psum('infernalImp')]};
if(kind==='hellhound') return {id:'hellhound',name:'Hellhound',hp:26,img:'🐕',isMinion:true,pattern:[Pburn(2),P(8),Pdef(9)]};
return {id:'skeletonMinion',name:'Skeleton Minion',hp:15,img:'💀',isMinion:true,pattern:[P(5),P(5)]};
}
// ---------- Events ----------
const EVENTS = {
altar:{name:'The Forgotten Altar',text:'A blood-stained altar hums with forbidden power. It hungers for a sacrifice.',
choices:[
{label:'Offer 15 HP — obtain a random Rare relic',fx:g=>{ if(g.player.hp<=15) return {fail:'Not enough HP!'}; g.player.takeDamage(15,false); const rare=RELICS[Object.keys(RELICS).find(k=>RELICS[k].rarity==='Rare')]; const id=pick(['soulEmber','shadowCloak','hellfireAura','sacrificialDagger']); /* keep relic */ g.gainRelic('soulEmber'); return {msg:'You obtain the Soul Ember!';}; },
simpleRelic:'Rare'},
{label:'Pray — heal 10 HP, gain a Curse (Doubt)',fx:g=>{ g.player.heal(10); g.addCurse('doubt'); return {msg:'The altar heals you (-10 HP curse). You gain Doubt.'}; }},
{label:'Leave',fx:g=>{ return {msg:'You leave the altar untouched.'}; }}
]},
peddler:{name:'Shadowy Peddler',text:'A hooded figure whispers: "Trade, or buy... or take."',
choices:[
{label:'Trade a card — remove a card, gain 100 Gold',fx:g=>{ const removed=g.removeCardForEvent(); if(!removed) return {fail:'No cards to trade!'}; g.gold+=100; return {msg:`You sold ${removed.name} for 100 Gold.`}; }},
{label:'Buy his wares — lose 50 Gold, get a Potion + Common relic',fx:g=>{ if(g.gold<50) return {fail:'Not enough Gold!'}; g.gold-=50; g.addPotion(pick(POTION_IDS)); g.gainRelic(pick(Object.keys(RELICS).filter(k=>RELICS[k].rarity==='Common'))); return {msg:'You acquire a Potion and a Common relic.'}; }},
{label:'Attack — combat with Shadow Thief, 75 Gold on victory',fx:g=>{ g.startCombat('shadowThief'); return {msg:'You lunge at the peddler!'}; }}
]},
spring:{name:'Cursed Spring',text:'A spring bubbles with dark, tempting water.',
choices:[
{label:'Drink — lose 10 HP, remove all Curses from deck',fx:g=>{ g.player.takeDamage(10,false); g.removeAllCurses(); return {msg:'The curses wash away. You feel weaker (-10 HP).'; }},
{label:'Bottle it — obtain a Cursed Brew',fx:g=>{ g.addPotion('cursedBrew'); return {msg:'You bottle the dark water (Cursed Brew).'; }},
{label:'Leave',fx:g=>{ return {msg:'You leave the spring alone.'}; }}
]},
restlessDead:{name:'Restless Dead',text:'Ghostly apparitions offer a bargain: "Upgrade, or be denied."',
choices:[
{label:'Accept — lose 20 HP, upgrade 2 random cards',fx:g=>{ g.player.takeDamage(20,false); const up=g.upgradeRandomCards(2); if(!up||up.length===0) return {msg:'You lose 20 HP but have no cards to upgrade.'}; return {msg:`You upgrade ${up.length} card(s): ${up.join(', ')}. (-20 HP)`}; },
{label:'Refuse',fx:g=>{ return {msg:'The ghosts fade, disappointed.'}; }}
]},
gambler:{name:'Impish Gambler',text:'A grinning imp holds up gold. "Bet, or wager a card?"',
choices:[
{label:'Bet 30 Gold — 50% to double, else lose it',fx:g=>{ if(g.gold<30) return {fail:'Not enough Gold!'}; if(Math.random()<0.5){ g.gold+=30; return {msg:'The imp flips heads! You win 60 Gold.'}; } g.gold-=30; return {msg:'The imp flips tails. You lose 30 Gold.'}; },
{label:'Wager a card — if Curse gain 100 Gold, else lose the card',fx:g=>{ const c=g.removeRandomCardForBet(); if(!c) return {fail:'No cards to wager!'}; if(c.curse){ g.gold+=100; return {msg:`You wagered ${c.name} (a Curse)! 100 Gold!`; } return {msg:`You wagered ${c.name} and lost it.`}; },
{label:'Leave',fx:g=>{ return {msg:'You decline the imp\'s game.'}; }}
]},
library:{name:'Ancient Library',text:'Dusty tomes whisper forgotten knowledge.',
choices:[
{label:'Study — gain 1 Energy next combat, lose 5 HP',fx:g=>{ g.player.nextCombatEnergy=1; g.player.takeDamage(5,false); return {msg:'You study ancient lore. Next combat: +1 Energy. (-5 HP)'; }},
{label:'Read forbidden page — add a random Power card to deck, gain Doubt',fx:g=>{ const powers=g.classPool.filter(id=>CARDS[id]&&CARDS[id].type==='Power'); if(powers.length===0) return {msg:'No power cards available.'}; const chosen=pick(powers); g.player.deck.push(makeCard(chosen)); g.addCurse('doubt'); return {msg:`You add ${CARDS[chosen].name} to your deck and gain Doubt.`}; },
{label:'Leave',fx:g=>{ return {msg:'You leave the library undisturbed.'}; }}
]}
};
// Special event enemy
ENEMY_DEFS.shadowThief={id:'shadowThief',name:'Shadow Thief',hp:30,img:'🥷',pattern:[P(6),Pweak(1),P(7),P(5)]};
// ============================================================
// GAME STATE
// ============================================================
const Game = {
player: null,
currentAct: 0,
map: null,
currentNode: null,
combat: null,
nextCombatEnergy: 0,
goldGainBlocked: false,
running: false
};
const CLASSES = {
blood:{name:'Blood Reaper',img:'🩸',desc:'Sacrifice HP for power, life steal, and Bleed.',sig:'Sanguine Strike'},
shadow:{name:'Shadowmancer',img:'🔮',desc:'Debuffs, Intangible, and card manipulation.',sig:'Shadow Bolt'},
hell:{name:'Hellforged',img:'🔥',desc:'Fire damage, Burn, and summoning Imps.',sig:'Firebolt'}
};
function makeCard(id, upgraded){
const base = CARDS[id];
return {
id: base.id, name: base.name, type: base.type, cost: base.cost,
rarity: base.rarity, img: base.img, cls: base.cls,
desc: base.desc, upg: base.upg, func: base.func,
exhaust: base.exhaust, ethereal: base.ethereal, curse: base.curse,
token: base.token, upgraded: !!upgraded, playable: true,
upgradeCard: upgraded ? {id:id+'+'} : null
};
}
function newPlayer(classId){
const cls = CLASSES[classId];
const deckIds = CLASS_POOLS[classId].slice(0,10); // 5 strike? handled below
// Build starting deck: 5 Strike, 4 Defend, 1 signature
const startDeck = [];
const sig = {blood:'sanguine',shadow:'shadowBolt',hell:'firebolt'}[classId];
for(let i=0;i<5;i++) startDeck.push(makeCard('strike'));
for(let i=0;i<4;i++) startDeck.push(makeCard('defend'));
startDeck.push(makeCard(sig));
const p = {
classId, className:cls.name, classImg:cls.img,
maxHp:70, hp:70, gold:99,
deck: startDeck,
relics: [], potions: [],
potionSlots: 3,
nextCombatEnergy: 0
};
return p;
}
// ---------- Map generation ----------
// Fixed floor layout per act. Node types: enemy(elite branch optional), event, shop, rest, boss
// Each act shares structure but different enemy pools.
function buildMap(actNum){
// Return array of floors (index by floor 2..15). Floor 1 = start.
const actNames = ['The Crypts','Blighted Woods','Abyssal Spire'];
const floors = {};
// Define fixed layout
const layout = {
2: {type:'enemy', branch:true},
3: {type:'enemy'},
4: {type:'event'},
5: {type:'shop'},
6: {type:'enemy'},
7: {type:'rest'},
8: {type:'enemy', branch:true},
9: {type:'elite', alt:'enemy'},
10:{type:'event'},
11:{type:'enemy'},
12:{type:'elite', alt:'enemy'},
13:{type:'enemy'},
14:{type:'rest'},
15:{type:'boss'}
};
for(const f in layout){ floors[+f] = layout[f]; }
return { name: actNames[actNum-1], num: actNum, floors, actName: actNames[actNum-1] };
}
// ---------- Combat state ----------
function startCombat(enemyDefs, opts){
opts = opts||{};
const c = {
enemies: [], deck: [], draw:[], discard:[], exhaust:[],
hand: [], player: null, energy:3, turn:0, ended:false,
gold:0, isBoss:false, rewardApplied:false, playerUpgraded:[],
extraGold:0
};
c.player = JSON.parse(JSON.stringify(Game.player));
c.player.deck = Game.player.deck.map(card=>makeCard(card.id, card.upgradeCard?true:card.upgraded));
// Actually rebuild from id preserving upgrade:
c.player.deck = Game.player.deck.map(card => {
const upg = card.upgradeCard ? true : card.upgraded;
return makeCard(card.id, upg);
});
c.isBoss = !!opts.boss;
// Place enemies
const list = Array.isArray(enemyDefs) ? enemyDefs : [enemyDefs];
list.forEach((def,idx)=>{
if(typeof def==='string') def = ENEMY_DEFS[def];
const e = makeEnemy(def, idx);
if(opts.boss){ e.phase1=def.phase1; e.phase2=def.phase2; }
c.enemies.push(e);
});
// Relic start-of-combat effects
applyRelicStartOfCombat(c);
// Shuffle draw
c.draw = shuffle(c.player.deck.slice());
c.discard = []; c.exhaust = [];
c.energy = 3;
c.turn = 1;
if(Game.nextCombatEnergy>0){ c.energy += Game.nextCombatEnergy; Game.nextCombatEnergy=0; }
Game.combat = c;
Game.player = c.player; // keep reference
renderCombatInit();
}
function applyRelicStartOfCombat(c){
const rel = c.player.relics;
if(rel.includes('demonHorns') && Game.player.classId==='hell'){
// add random Imp Claw to hand (done at player turn start instead since hand not drawn yet)
c.demonHornsUsed = false;
}
if(rel.includes('shadowCloak')) c.player.addBuff('intangible',1,1);
if(rel.includes('cursedAmulet')) c.enemies.forEach(e=>{ if(e.hp>0) e.statuses.weak = (e.statuses.weak||0)+1; });
if(rel.includes('bossBlackBlood')){
const loss = Math.ceil(c.player.maxHp*0.1);
c.player.hp -= loss;
}
if(rel.includes('bossEctoplasm')){ c.player.noGold=true; c.player.gold=0; c.player.goldGain=false; }
if(rel.includes('bossPhilosopher')) c.enemies.forEach(e=>{ e.strength = (e.strength||0)+1; });
if(rel.includes('vialShadows')){ /* handled at pickup */ }
if(rel.includes('demonHorns') && Game.player.classId==='hell'){
c.pendingImpClaw = true;
}
}
// ---------- Combat actions ----------
function combatCtx(){
const c = Game.combat;
return {
enemies:c.enemies, player:c.player, energy:c.energy, turn:c.turn,
target:c.selectedEnemy
};
}
// ---------- Damage / healing ----------
function targetOrSelf(){ const c=Game.combat; return c.selectedEnemy && c.selectedEnemy.hp>0 ? c.selectedEnemy : c.enemies.find(e=>e.hp>0); }
function applyBurnDamage(c, enemy){
// Soul Ember: +2 dmg when burn applied handled inline; here just apply burn tick
const stacks = enemy.statuses.burn||0;
if(stacks>0){
const dmg = stacks + (c.player.relics.includes('soulEmber')?0:0);
dealDamageToEnemy(enemy, dmg, c.player, true);
if(enemy.hp>0){ log(`${enemy.name} takes ${dmg} Burn.`); }
}
}
function dealDamageToEnemy(enemy, amount, attacker, noIntangible){
// Intangible (enemy only from... actually only player has intangible per DDD)
let dmg = amount;
if(!noIntangible && enemy.statuses.intangible && enemy.statuses.intangible>0){
dmg = 1;
enemy.statuses.intangible--;
if(enemy.statuses.intangible<=0) delete enemy.statuses.intangible;
log(`${enemy.name} is Intangible — damage reduced to 1.`);
}
if(enemy.statuses.vulnerable && enemy.statuses.vulnerable>0){
dmg = Math.floor(dmg*1.5);
}
// Soul Ember extra damage when burn applied (checked at apply time)
enemy.hp -= dmg;
if(enemy.hp<=0){ enemy.hp=0; enemy.alive=false; }
return dmg;
}
function ctxDamageToEnemy(enemy, amount){
const c = Game.combat;
const dmg = dealDamageToEnemy(enemy, amount, c.player, false);
animateHit(enemy, dmg);
return dmg;
}
// Player takes damage
function playerTakeDamage(amount, fromCard){
const c = Game.combat;
let dmg = amount;
// Intangible
if(c.player.statuses.intangible && c.player.statuses.intangible>0){
dmg = 1;
c.player.statuses.intangible--;
if(c.player.statuses.intangible<=0) delete c.player.statuses.intangible;
log('Intangible reduces damage to 1!');
floatText(document.getElementById('playerArt'), '1','intangible');
}
// Block
if(c.player.block>0){
const absorbed = Math.min(c.player.block, dmg);
c.player.block -= absorbed;
dmg -= absorbed;
}
if(dmg>0){
c.player.hp -= dmg;
floatText(document.getElementById('playerArt'), '-'+dmg,'dmg');
}
if(c.player.hp<=0){ c.player.hp=0; }
// Bloodstone / Blood Pact on HP loss from card
if(fromCard){
if(c.player.relics.includes('bloodstone')){ c.player.hp = Math.min(c.player.maxHp, c.player.hp+1); }
if(c.player.relics.includes('blackBlood')){ c.player.hp = Math.min(c.player.maxHp, c.player.hp+1); }
if(c.player.relics.includes('bloodPact') || c.player.deck.some(cd=>cd.id==='bloodPact' && cd.upgradeCard)){
// gain strength from blood pact power
}
}
renderPlayer();
}
// Player heal with intangible bypass (heal not affected by intangible)
function playerHeal(amount){
const c = Game.combat;
c.player.hp = Math.min(c.player.maxHp, c.player.hp+amount);
floatText(document.getElementById('playerArt'), '+'+amount,'heal');
renderPlayer();
}
// Enemy takes damage helper (used by card funcs)
function ctx_damage(target, amount, player){
const c = Game.combat;
// Weak on target reduces damage
if(target.statuses.weak && target.statuses.weak>0){
amount = Math.floor(amount*0.75);
}
const dmg = dealDamageToEnemy(target, amount, player, false);
animateHit(target, dmg);
return dmg;
}
function ctx_damageAll(amount, player){
const c = Game.combat;
const alive = c.enemies.filter(e=>e.hp>0);
alive.forEach(e=>{
let d = amount;
if(e.statuses.weak) d = Math.floor(d*0.75);
const dmg = dealDamageToEnemy(e, d, player, false);
animateHit(e, dmg);
});
}
function ctx_applyToEnemy(enemy, status, amount){
const c = Game.combat;
if(enemy.hp<=0) return;
enemy.statuses[status] = (enemy.statuses[status]||0) + amount;
// Soul Ember: +2 when burn applied
if(status==='burn' && c.player.relics.includes('soulEmber')){
dealDamageToEnemy(enemy, 2, c.player, true);
animateHit(enemy, 2);
}
// Bleed/Burn on enemy only; if applied to player it's fine too
log(`Applied ${status} ${amount} to ${enemy.name}.`);
}
function ctx_drawCards(n){
const c = Game.combat;
for(let i=0;i<n;i++){
if(c.draw.length===0){
if(c.discard.length===0) break;
c.draw = shuffle(c.discard);
c.discard = [];
}
if(c.draw.length===0) break;
const card = c.draw.pop();
if(card.curse && card.id!=='doubt'){
// Wound/Dazed: drawn, no effect, stays in hand (ethereal, exhaust when played/discarded)
c.hand.push(card);
} else {
c.hand.push(card);
}
}
}
function ctx_shuffleDazed(place){
const c = Game.combat;
const dazed = makeCard('dazed');
if(place==='draw') c.draw.push(dazed);
else c.discard.push(dazed);
}
// ---------- Status tick helpers ----------
function startPlayerTurn(){
const c = Game.combat;
const p = c.player;
// Intangible decrement
if(p.statuses.intangible){ p.statuses.intangible--; if(p.statuses.intangible<=0) delete p.statuses.intangible; }
// Energy from brimstone? no. pending energy
// Regeneration
if(p.statuses.regen){ playerHeal(p.statuses.regen); p.statuses.regen--; if(p.statuses.regen<=0) delete p.statuses.regen; }
// Energy next turn
if(p.pendingEnergy){ c.energy += p.pendingEnergy; p.pendingEnergy=0; }
// Pending block (phantom armor / whispering shroud)
if(p.pendingBlock){ p.block += p.pendingBlock; p.pendingBlock=0; }
if(p.pendingDex){ p.addBuff('dex', p.pendingDex, 1); p.pendingDex=0; }
// Relic start-of-turn
if(p.relics.includes('eternalHourglass') && c.hand.length===0){ c.energy+=1; }
if(p.relics.includes('bleedingEdge')){ const e=c.enemies.find(e=>e.hp>0); if(e) ctx_applyToEnemy(e,'bleed',1); }
if(p.relics.includes('bossCursedCrown')){ c.energy+=1; }
// Soul Siphon power
if(p.deck.some(cd=>cd.id==='soulSiphon')){ c.energy+=1; p.hp=Math.max(1,p.hp-1); renderPlayer(); }
// Hellfire Aura power
if(p.deck.some(cd=>cd.id==='hellfireAura')){ const dmg = p.upgradeCard && p.deck.find(cd=>cd.id==='hellfireAura')&&p.deck.find(cd=>cd.id==='hellfireAura').upgradeCard ? 5:3; c.enemies.filter(e=>e.hp>0).forEach(e=>{ const dd=dealDamageToEnemy(e,dmg,p,false); animateHit(e,dd); }); }
// Umbral Mastery power
if(p.deck.some(cd=>cd.id==='umbralMastery')){ const n = p.deck.find(cd=>cd.id==='umbralMastery')&&p.deck.find(cd=>cd.id==='umbralMastery').upgradeCard?2:1; c.enemies.filter(e=>e.hp>0).forEach(e=>{ e.statuses.weak=(e.statuses.weak||0)+n; }); }
// Blood Pact: strength already applied on HP loss
// Dazed in hand -> unplayable
// End of Energy Orb display
renderEnergy();
}
function endPlayerTurn(){
const c = Game.combat;
const p = c.player;
// Doubt curse in hand -> lose 1 HP
c.hand.forEach(cd=>{ if(cd.id==='doubt'){ playerTakeDamage(1,false); } });
// Burn tick for player
if(p.statuses.burn){
const s = p.statuses.burn;
playerTakeDamage(s,false);
p.statuses.burn = Math.floor(s/2);
if(p.statuses.burn<=0) delete p.statuses.burn;
}
// Discard hand
c.discard.push(...c.hand);
c.hand = [];
// Block resets handled next turn start (block=0)
renderPlayer();
}
function startEnemyTurn(enemy){
const c = Game.combat;
// Bleed tick
if(enemy.statuses.bleed){
const s = enemy.statuses.bleed;
const dmg = dealDamageToEnemy(enemy, s, c.player, true);
animateHit(enemy, dmg);
enemy.statuses.bleed = Math.floor(s/2);
if(enemy.statuses.bleed<=0) delete enemy.statuses.bleed;
}
// Strength loss temp
if(enemy._tempStrengthLoss){ enemy.strength = Math.max(0,(enemy.strength||0)-enemy._tempStrengthLoss); enemy._tempStrengthLoss=0; }
// Weak decrement
if(enemy.statuses.weak){ enemy.statuses.weak--; if(enemy.statuses.weak<=0) delete enemy.statuses.weak; }
if(enemy.statuses.vulnerable){ enemy.statuses.vulnerable--; if(enemy.statuses.vulnerable<=0) delete enemy.statuses.vulnerable; }
}
function executeEnemyIntent(enemy, intent){
const c = Game.combat;
switch(intent.type){
case 'attack':
enemyApplyDamage(enemy, intent.dmg, enemy.atk||0);
break;
case 'multi':
for(let i=0;i<intent.n;i++){ if(c.enemies.every(e=>e.hp<=0)) break; enemyApplyDamage(enemy, intent.dmg, 0); }
break;
case 'defend':
enemy.block += (intent.x||0);
log(`${enemy.name} gains ${intent.x} Block.`);
break;
case 'buff_strength':
enemy.strength = (enemy.strength||0)+(intent.x||0);
log(`${enemy.name} gains ${intent.x} Strength.`);
break;
case 'buff':
if(intent.x && intent.x.strength) enemy.strength=(enemy.strength||0)+intent.x.strength;
if(intent.x && intent.x.block) enemy.block+=(intent.x.block);
if(intent.x && intent.x.regen){ enemy.statuses.regen=(enemy.statuses.regen||0)+intent.x.regen; }
log(`${enemy.name} buffs.`);
break;
case 'apply_weak':
if(c.player.hp>0){ c.player.statuses.weak=(c.player.statuses.weak||0)+(intent.x||1); }
break;
case 'apply_vulnerable':
if(c.player.hp>0){ c.player.statuses.vulnerable=(c.player.statuses.vulnerable||0)+(intent.x||1); }
break;
case 'apply_bleed':
if(c.player.hp>0){ c.player.statuses.bleed=(c.player.statuses.bleed||0)+(intent.x||1); }
break;
case 'apply_burn':
if(c.player.hp>0){ c.player.statuses.burn=(c.player.statuses.burn||0)+(intent.x||1); }
break;
case 'poison':
if(c.player.hp>0){ c.player.statuses.bleed=(c.player.statuses.bleed||0)+(intent.x||0); c.player.statuses.burn=(c.player.statuses.burn||0)+(intent.x||0); c.player.statuses.poison=(c.player.statuses.poison||0)+(intent.poison||0); enemyApplyDamage(enemy, intent.atk,0); }
break;
case 'drain':
enemyApplyDamage(enemy, intent.atk, 0);
enemy.hp = Math.min(enemy.maxHp, enemy.hp + intent.heal);
log(`${enemy.name} drains ${intent.heal} HP.`);
break;
case 'summon':
summonMinion(enemy, intent.monk);
break;
case 'buff_self':
if(intent.block) enemy.block+=(intent.block);
if(intent.regen) enemy.statuses.regen=(enemy.statuses.regen||0)+intent.regen;
log(`${enemy.name} buffs itself.`);
break;
case 'buff_all':
c.enemies.filter(e=>e.hp>0).forEach(e=>{ e.strength=(e.strength||0)+(intent.str||0); });
log(`${enemy.name} buffs all enemies.`);
break;
case 'type2':
break;
}
enemy._tempStrengthLoss = 0;
}
function enemyApplyDamage(enemy, dmg, strFromIntent){
const c = Game.combat;
let total = dmg + (enemy.strength||0);
// Vulnerable on enemy
if(enemy.statuses.vulnerable){ total = Math.floor(total*1.5); }
// Block absorb
if(enemy.block>0){ const ab=Math.min(enemy.block,total); enemy.block-=ab; total-=ab; }
if(total>0){ enemy.hp -= total; if(enemy.hp<=0){ enemy.hp=0; enemy.alive=false; } }
animateHit(enemy, dmg);
log(`${enemy.name} attacks for ${dmg}.`);
}
function summonMinion(source, kind){
const c = Game.combat;
const minions = c.enemies.filter(e=>e.isMinion && e.hp>0).length;
if(minions>=2){ log('Already 2 minions; summon skipped.'); return; }
const def = minionDef(kind);
const e = makeEnemy(def, c.enemies.length);
c.enemies.push(e);
log(`A ${def.name} is summoned!`);
renderEnemies();
}
// ============================================================
// RENDERING & UI
// ============================================================
const el = id => document.getElementById(id);
function showScreen(id){ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active')); el(id).classList.add('active'); }
function log(msg){
const l = el('log');
const d = document.createElement('div');
d.textContent = msg;
l.appendChild(d);
while(l.children.length>6) l.removeChild(l.firstChild);
}
function floatText(anchor, text, cls){
const parent = anchor.parentElement;
const f = document.createElement('div');
f.className='floatText '+cls;
f.textContent = text;
f.style.left = (parent.offsetWidth/2 - 30 + rand(-30,30))+'px';
f.style.top = (parent.offsetHeight/2)+'px';
parent.appendChild(f);
setTimeout(()=>f.remove(),1000);
}
function animateHit(enemy, dmg){
const art = enemy.el && enemy.el.querySelector('.enemyArt');
if(art){ art.parentElement.classList.remove('hit'); void art.offsetWidth; art.parentElement.classList.add('hit'); }
}
// ---------- Class select ----------
function renderClassSelect(){
const grid = el('classGrid');
grid.innerHTML='';
Object.entries(CLASSES).forEach(([id,c])=>{
const d = document.createElement('div');
d.className='classCard';
d.innerHTML = `<div class="classIcon">${c.img}</div><h3>${c.name}</h3><div class="desc">${c.desc}</div>
<div class="start">Start HP 70 · 3 Energy · 5 Draws</div>
<div class="sig">Signature: ${c.sig}</div>`;
d.onclick = ()=>startRun(id);
grid.appendChild(d);
});
}
function startRun(classId){
Game.player = newPlayer(classId);
Game.currentAct = 1;
Game.map = buildMap(1);
Game.nodeIndex = 1;
renderMap();
showScreen('mapScreen');
}
// ---------- Map ----------
function nodeIcon(type){
return {enemy:'⚔️',elite:'💀',rest:'🏕️',shop:'🏪',event:'❓',boss:'☠️'}[type];
}
function nodeLabel(type){
return {enemy:'Enemy',elite:'Elite',rest:'Rest',shop:'Shop',event:'Event',boss:'Boss'}[type];
}
function renderMap(){
el('hudAct').textContent = `Act ${Game.currentAct}: ${Game.map.actName}`;
el('hudClass').textContent = Game.player.className + ' ' + Game.player.classImg;
el('hudGold').textContent = Game.player.gold;
el('hudHp').textContent = Game.player.hp;
el('hudMaxHp').textContent = Game.player.maxHp;
const container = el('mapNodes');
container.innerHTML='';
const floors = Game.map.floors;
// Floor 1 start
const startRow = document.createElement('div');
startRow.className='mapRow';
startRow.innerHTML = `<div class="mapNode current" style="cursor:default">🏕️<span class="floorNum">Start</span></div>`;
container.appendChild(startRow);
for(let f=2; f<=15; f++){
const fl = floors[f];
if(!fl) continue;
const row = document.createElement('div');
row.className='mapRow';
const type = fl.branch ? 'enemy' : fl.type;
// Determine node(s)
const nodes = [];
if(fl.branch){
nodes.push({type:'enemy', label:nodeLabel('enemy')});
nodes.push({type:'enemy', label:nodeLabel('enemy')});
} else if(fl.alt){
// elite branch + enemy alt
nodes.push({type:fl.type, label:nodeLabel(fl.type)});
nodes.push({type:fl.alt, label:nodeLabel(fl.alt)});
} else {
nodes.push({type:fl.type, label:nodeLabel(fl.type)});
}
// Special: floor 15 is boss single
if(f===15){ nodes.length=0; nodes.push({type:'boss',label:'Boss'}); }
nodes.forEach((n,i)=>{
const node = document.createElement('div');
const isCurrent = (f===Game.nodeIndex);
node.className='mapNode '+n.type + (isCurrent?' current':'') + (f<Game.nodeIndex?' cleared':'') + (f>Game.nodeIndex?' locked':'');
node.innerHTML = `${nodeIcon(n.type)}<span class="floorNum">${f}</span>`;
if(isCurrent){
node.classList.add('available');
node.onclick = ()=>enterNode(f, n.type);
}
row.appendChild(node);
});
container.appendChild(row);
}
// Scroll current into view
setTimeout(()=>{ const cur=document.querySelector('.mapNode.current'); if(cur) cur.scrollIntoView({behavior:'smooth',block:'center'}); },100);
}
function enterNode(floor, type){
if(floor>Game.nodeIndex) return;
const floors = Game.map.floors;
const fl = floors[floor];
if(type==='enemy'){
const enemyGroup = getEnemyGroup(Game.currentAct, floor);
startCombat(enemyGroup, {boss:false});
} else if(type==='elite'){
const elite = getElite(Game.currentAct);
startCombat(elite, {boss:false});
} else if(type==='boss'){
const boss = getBoss(Game.currentAct);
startCombat(boss, {boss:true});
} else if(type==='rest'){
showRest();
} else if(type==='shop'){
showShop();
} else if(type==='event'){
showEvent();
}
}
function getEnemyGroup(act, floor){
const pools = {
1:{skeleton:'skeleton',zombie:'zombie',cultist:'cultist',rat:'rat'},
2:{wraith:'wraith',beast:'beast',thorned:'thorned',witchDoc:'witchDoc'},
3:{hellhound:'hellhound',shadowDemo:'shadowDemo',infernalImp:'infernalImp',abyssCultist:'abyssCultist'}
};
const p = pools[act];
switch(floor){
case 2: return Math.random()<0.5 ? [p.skeleton] : [p.zombie];
case 3: return [p.cultist];
case 6: return [p.rat];
case 8: return Math.random()<0.5 ? [p.skeleton,p.skeleton] : [p.zombie];
case 11: return [p.cultist, p.rat];
case 13: return [p.cultist, p.cultist];
default: return [pick(Object.values(p))];
}
}
function getElite(act){
return {1:'tombGuardian',2:'treant',3:'flamespawn'}[act];
}
function getBoss(act){
return {1:'lichKing',2:'eldritchTreant',3:'archdemon'}[act];
}
// ---------- Combat rendering ----------
function renderCombatInit(){
const c = Game.combat;
el('combatAct').textContent = `Act ${Game.currentAct}: ${Game.map.actName}`;
el('combatClass').textContent = Game.player.className+' '+Game.player.classImg;
el('combatGold').textContent = Game.player.gold;
showScreen('combatScreen');
renderEnemies();
renderPlayer();
renderPotions();
// Draw initial 5
c.hand = [];
ctx_drawCards(5);
startPlayerTurn();
renderHand();
renderEnergy();
}
function renderEnemies(){
const row = el('enemyRow');
row.innerHTML='';
const c = Game.combat;
c.enemies.forEach(enemy=>{
const d = document.createElement('div');
d.className='enemy' + (enemy.hp<=0?' dead':'');
if(enemy.hp>0) d.onclick = ()=>selectEnemy(enemy, d);
d.el = d;
enemy.el = d;
const hpPct = Math.max(0,(enemy.hp/enemy.maxHp)*100);
const statusHtml = Object.entries(enemy.statuses).map(([k,v])=>`<span class="pip">${k==='weak'?'🗡️':k==='vulnerable'?'🔴':k==='bleed'?'🩸':k==='burn'?'🔥':k==='regen'?'💚':k==='intangible'?'👤':k}</span>`).join('');
let intentHtml='';
if(enemy.hp>0 && c.player.hp>0 && c.playerTurn){
intentHtml = renderIntent(enemy);
}
d.innerHTML = `
${enemy.block>0?`<div class="blockBadge">🛡️${enemy.block}</div>`:''}
<div class="intentBox">${intentHtml}</div>
<div class="enemyArt">${enemy.img}</div>
<div class="enemyName">${enemy.name}</div>
<div class="hpBar"><div class="hpFill" style="width:${hpPct}%"></div></div>
<div class="enemyStats">${enemy.hp}/${enemy.maxHp} ${enemy.strength?('💪'+enemy.strength):''}</div>
<div class="statusPips">${statusHtml}</div>
`;
row.appendChild(d);
});
}
function renderIntent(enemy){
const t = enemy._turn % enemy.pattern.length;
const intent = enemy.pattern[t];
let txt='';
if(enemy.boss){
const phase = enemy.hp<=enemy.maxHp*0.45 ? 1 : 0;
const cyc = enemy.hp<=enemy.maxHp*0.45 ? enemy.phase2 : enemy.phase1;
const ti = enemy._turn % cyc.length;
return intentText(cyc[ti]);
}
return intentText(intent);
}
function intentText(intent){
switch(intent.type){
case 'attack': return `⚔️ ${intent.dmg+(intent.atk||0)}`;
case 'multi': return `⚔️ ${intent.n}×${intent.dmg}`;
case 'defend': return `🛡️ ${intent.x}`;
case 'buff_strength': return `💪 ${intent.x}`;
case 'apply_weak': return `🗡️${intent.x} Weak`;
case 'apply_vulnerable': return `🔴${intent.x} Vuln`;
case 'apply_bleed': return `🩸${intent.x} Bleed`;
case 'apply_burn': return `🔥${intent.x} Burn`;
case 'summon': return `✨ Summon`;
case 'drain': return `🩸 ${intent.atk}+${intent.heal}`;
case 'poison': return `☠️ ${intent.atk}`;
default: return '❓';
}
}
function selectEnemy(enemy, d){
const c = Game.combat;
if(!c.playerTurn || c.ended) return;
c.selectedEnemy = enemy;
document.querySelectorAll('.enemy').forEach(e=>e.classList.remove('targetable'));
d.classList.add('targetable');
}
function renderPlayer(){
const c = Game.combat;
const p = c.player;
const hpPct = Math.max(0,(p.hp/p.maxHp)*100);
el('hpFill').style.width = hpPct+'%';
el('playerHpText').textContent = `${p.hp}/${p.maxHp}`;
const b = el('playerBlock');
if(p.block>0){ b.style.display='block'; b.textContent='🛡️'+p.block; } else b.style.display='none';
const buffs = Object.entries(p.statuses).map(([k,v])=>`<span class="buff ${k}">${k} ${v}</span>`).join('');
el('playerBuffs').innerHTML = buffs;
el('drawCount').textContent = c.draw.length;
el('discardCount').textContent = c.discard.length;
renderPotions();
}
function renderEnergy(){
const c = Game.combat;
el('energyOrb').textContent = `⚡${c.energy}/${c.maxEnergy||3}`;
el('endTurnBtn').disabled = !c.playerTurn;
}
function renderHand(){
const hand = el('hand');
hand.innerHTML='';
const c = Game.combat;
c.hand.forEach((card, idx)=>{
const d = document.createElement('div');
const playable = card.playable && c.playerTurn && !c.ended && c.energy>=card.cost;
d.className = 'card '+card.rarity.toLowerCase() + (card.ethereal?' ethereal':'') + (card.curse?' curse':'') + (playable?'':' unplayable');
const desc = card.upgraded ? card.upg : card.desc;
d.innerHTML = `
<div class="cost">${card.cost}</div>
<div class="cname">${card.name}${card.upgradeCard?'+':''}</div>
<div class="ctype">${card.type}${card.rarity!=='Basic'?' · '+card.rarity:''}</div>
<div class="cimg">${card.img}</div>
<div class="cdesc">${desc}</div>
<div class="cbar"></div>
${card.exhaust?'<div class="exhaustTag">Exhaust</div>':''}
`;
if(playable){
d.onclick = ()=>playCard(idx);
}
d._idx = idx;
hand.appendChild(d);
});
}
function renderPotions(){
const bar = el('potionBar');
bar.innerHTML='';
const c = Game.combat;
c.player.potions.forEach((pid, idx)=>{
const p = POTIONS[pid];
const d = document.createElement('div');
d.className='potion';
d.textContent = p.img;
d.title = p.name;
d.onclick = ()=>usePotion(idx);
bar.appendChild(d);
});
el('combatPotions').textContent = c.player.potions.length>0 ? c.player.potions.map(p=>POTIONS[p].img).join(' ') : '';
}
// ---------- Play card ----------
function playCard(idx){
const c = Game.combat;
const card = c.hand[idx];
if(!card || !card.playable || c.ended) return;
if(c.energy < card.cost) return;
if(!c.selectedEnemy && !cardNoTarget(card)) return;
c.energy -= card.cost;
// Blood Reaper: sacrificial dagger reduces first HP-costing attack cost
c.hand.splice(idx,1);
const ctx = combatCtx();
// Soul Siphon: start-of-turn energy handled separately; here Soul Siphon doesn't trigger on play
// Play the card
try{ card.func(ctx); }
catch(e){ console.error('Card func error', e); }
// Tome of Dark Rituals: play a Power card -> draw 1
if(c.player.relics.includes('tomeDark') && card.type==='Power'){ c.hand.length < 10 && ctx_drawCards(1); }
// Blood Pact: if HP lost from card, gain strength
if(c.player.deck.some(cd=>cd.id==='bloodPact')){ /* strength applied via relic on HP loss */ }
renderEnergy();
renderPlayer();
renderEnemies();
renderHand();
// Exhaust handling
if(card.exhaust){ c.exhaust.push(card); }
else c.discard.push(card);
// Check enemy deaths
checkCombatEnd();
renderHand();
}
function cardNoTarget(card){
// Cards that don't need a target
return ['Skill','Power'].includes(card.type) || card.type==='Status';
}
// ---------- Potions ----------
function usePotion(idx){
const c = Game.combat;
if(!c.playerTurn || c.ended) return;
const pid = c.player.potions[idx];
const p = POTIONS[pid];
c.player.potions.splice(idx,1);
switch(p.effect){
case 'heal15': playerHeal(15); break;
case 'firebomb': c.enemies.filter(e=>e.hp>0).forEach(e=>{ const d=dealDamageToEnemy(e,10,c.player,false); animateHit(e,d); }); renderEnemies(); break;
case 'dex': c.player.addBuff('dex',3,1); break;
case 'energy': c.energy += 2; break;
case 'cursedBrew': c.enemies.filter(e=>e.hp>0).forEach(e=>{ e.statuses.vulnerable=(e.statuses.vulnerable||0)+3; }); break;
case 'bleeding': { const e=pick(c.enemies.filter(x=>x.hp>0)); if(e) e.statuses.bleed=(e.statuses.bleed||0)+6; } break;
case 'impInBottle': c.hand.push(makeImpClaw(true)); break;
case 'smokeBomb': { if(!c.isBoss){ endCombatEscape(); return; } else { log('Cannot escape a boss!'); c.player.potions.push(pid); return; } }
}
renderPlayer();
renderPotions();
renderEnergy();
renderEnemies();
}
// ---------- Turn processing ----------
el('endTurnBtn').addEventListener('click', endPlayerTurnAction);
function endPlayerTurnAction(){
const c = Game.combat;
if(!c.playerTurn || c.ended) return;
c.playerTurn = false;
c.selectedEnemy = null;
endPlayerTurn();
renderHand();
renderPlayer();
el('endTurnBtn').disabled = true;
setTimeout(processEnemyTurns, 600);
}
async function processEnemyTurns(){
const c = Game.combat;
for(const enemy of c.enemies){
if(enemy.hp<=0) continue;
startEnemyTurn(enemy);
if(c.player.hp<=0){ return handlePlayerDeath(); }
enemy.intent = enemy.boss ? bossIntent(enemy) : enemyIntent(enemy);
executeEnemyIntent(enemy, enemy.intent);
renderEnemies();
renderPlayer();
if(c.player.hp<=0){ return handlePlayerDeath(); }
await sleep(500);
}
if(c.player.hp<=0){ return handlePlayerDeath(); }
// Next player turn
c.turn++;
c.playerTurn = true;
c.energy = 3;
c.selectedEnemy = null;
// Block reset
c.player.block = 0;
// Draw 5
c.hand = [];
startPlayerTurn();
ctx_drawCards(5);
renderHand();
renderEnergy();
renderPlayer();
el('endTurnBtn').disabled = false;
}
function bossIntent(enemy){
const phase = enemy.hp<=enemy.maxHp*0.45 ? enemy.phase2 : enemy.phase1;
const t = enemy._turn % phase.length;
enemy._turn++;
return phase[t];
}
function sleep(ms){ return new Promise(r=>setTimeout(r,ms)); }
function handlePlayerDeath(){
const c = Game.combat;
c.ended = true;
c.player.hp = 0;
renderPlayer();
showOverlay(`<div class="bigText">YOU DIED</div>
<div class="msgText">The Abyss claims another soul.</div>
<div class="center-wrap">
<button class="btn" onclick="gameOver()">Accept Defeat</button>
</div>`);
}
function checkCombatEnd(){
const c = Game.combat;
if(c.enemies.every(e=>e.hp<=0)){
// Boss phase transitions handled mid-fight; if all dead, end combat
setTimeout(()=>winCombat(), 400);
}
}
function endCombatEscape(){
const c = Game.combat;
c.ended = true;
Game.nodeIndex++;
hideOverlay();
renderMap();
log('You escaped with the Smoke Bomb.');
}
// ---------- Win combat ----------
function winCombat(){
const c = Game.combat;
c.ended = true;
const gold = computeGold();
c.gold = gold;
// Apply gold
if(!c.player.goldGain===false){ /* default */ }
if(!c.player.noGold){
c.player.gold += gold;
Game.player.gold = c.player.gold;
}
// Ashen Armor heal
if(c.player.relics.includes('ashenArmor')){ c.player.hp = Math.min(c.player.maxHp, c.player.hp+6); }
if(c.isBoss){
// Full heal + boss rewards then advance
showBossRewards();
} else {
showRewards();
}
}
function computeGold(){
const c = Game.combat;
let g = 0;
if(c.isBoss) g = 50;
else if(c.enemies.some(e=>e.boss)) g = 50;
else g = rand(10,20);
if(c.player.relics.includes('greedyImp')) g += 10;
return g;
}
// ---------- Rewards ----------
function showRewards(){
const c = Game.combat;
let html = `<h2>Victory!</h2>
<div class="rewardGold">+${c.gold} Gold</div>
<div class="rewardCards" id="rewardCards"></div>
<div id="rewardExtras"></div>
<div class="center-wrap">
<button class="btn rewardSkip" onclick="skipReward()">Skip</button>
</div>`;
showOverlay(html);
// Card reward: 3 cards weighted by rarity
const choices = pickCardChoices();
const rc = el('rewardCards');
choices.forEach((cardId, i)=>{
const card = makeCard(cardId, false);
const d = createRewardCard(card);
d.onclick = ()=>takeRewardCard(card);
rc.appendChild(d);
});
// Potion reward
let extras = `<div class="rewardGold">${c.gold} Gold</div>`;
const normalDrop = c.enemies.some(e=>e.statuses && false) ? false : true;
// Potion chance 40%
if(Math.random()<0.4 && c.player.potions.length < c.player.potionSlots){
const pot = pick(POTION_IDS);
extras += `<div class="rewardPotion">${POTIONS[pot].img} ${POTIONS[pot].name}</div>`;
c.player.potions.push(pot);
}
el('rewardExtras').innerHTML = extras;
}
function pickCardChoices(){
const pool = Game.player.classPool || CLASS_POOLS[Game.player.classId];
const rare = Game.map.floors[Game.nodeIndex] && Game.map.floors[Game.nodeIndex].type==='elite' ? 0.20 : 0.10;
const choices = [];
for(let i=0;i<3;i++){
const r = Math.random();
let rarity;
if(r < rare) rarity='Rare';
else if(r < rare+0.30) rarity='Uncommon';
else rarity='Common';
const candidates = pool.filter(id=>CARDS[id] && CARDS[id].rarity===rarity && CARDS[id].type!=='Basic');
if(candidates.length===0){
const fallback = pool.filter(id=>CARDS[id] && CARDS[id].type!=='Basic');
choices.push(pick(fallback));
} else choices.push(pick(candidates));
}
return choices;
}
function createRewardCard(card){
const d = document.createElement('div');
d.className='card '+card.rarity.toLowerCase();
const desc = card.upgraded ? card.upg : card.desc;
d.innerHTML = `<div class="cost">${card.cost}</div>
<div class="cname">${card.name}</div>
<div class="ctype">${card.type}${card.rarity!=='Basic'?' · '+card.rarity:''}</div>
<div class="cimg">${card.img}</div>
<div class="cdesc">${desc}</div>
<div class="cbar"></div>`;
return d;
}
function takeRewardCard(card){
const c = Game.combat;
c.player.deck.push(card);
Game.player.deck = c.player.deck;
hideOverlay();
advanceNode();
}
function skipReward(){ hideOverlay(); advanceNode(); }
// ---------- Boss rewards ----------
function showBossRewards(){
const c = Game.combat;
// Choose 1 of 3 boss relics
const relics = shuffle(BOSS_RELIC_IDS).slice(0,3);
let html = `<h2>${Game.map.actName} Cleared!</h2>
<div class="msgText">The ${c.enemies[0].name} is defeated. You are fully restored.</div>
<div class="rewardGold">+50 Gold</div>
<div class="rewardCards" id="bossRelicCards"></div>
<div class="center-wrap"><button class="btn rewardSkip" onclick="skipBossReward()">Continue</button></div>`;
showOverlay(html);
const rc = el('bossRelicCards');
relics.forEach(id=>{
const rel = RELICS[id];
const d = document.createElement('div');
d.className='card rare';
d.innerHTML = `<div class="cost">★</div><div class="cname">${rel.name}</div>
<div class="cimg">${rel.img}</div><div class="cdesc">${rel.desc}</div><div class="cbar"></div>`;
d.onclick = ()=>takeBossRelic(id);
rc.appendChild(d);
});
// Store relics for take
el('overlayBox')._bossRelics = relics;
}
function takeBossRelic(id){
const c = Game.combat;
c.player.relics.push(id);
Game.player.relics = c.player.relics;
// Full heal after boss
Game.player.hp = Game.player.maxHp;
// Advance to next act if boss was act 3, else continue
hideOverlay();
advanceNode();
}
function skipBossReward(){
// still give full heal
Game.player.hp = Game.player.maxHp;
hideOverlay();
advanceNode();
}
// ---------- Advance node ----------
function advanceNode(){
Game.nodeIndex++;
// Check for act completion (after boss floor 15)
if(Game.nodeIndex > 15){
if(Game.currentAct >= 3){
victoryScreen();
} else {
Game.currentAct++;
Game.map = buildMap(Game.currentAct);
Game.nodeIndex = 1;
// Show act banner
showBanner(`Act ${Game.currentAct}: ${Game.map.actName}`);
renderMap();
}
return;
}
renderMap();
}
function showBanner(text){
const b = document.createElement('div');
b.className='banner';
b.textContent = text;
document.getElementById('mapScroll').appendChild(b);
setTimeout(()=>b.remove(),1200);
}
function gameOver(){
showOverlay(`<div class="bigText">RUN OVER</div>
<div class="msgText">You reached Act ${Game.currentAct}. The Abyss grows stronger.</div>
<div class="center-wrap"><button class="btn" onclick="restart()">Choose Another Fate</button></div>`);
}
function victoryScreen(){
Game.player.hp = Game.player.maxHp;
showOverlay(`<div class="bigText victory">VICTORY</div>
<div class="msgText">You have defeated the Archdemon and broken the curse of the Abyss. The dying world stirs with new light. <br><br>Final Gold: ${Game.player.gold} · Relics: ${Game.player.relics.length} · Potions: ${Game.player.potions.length}</div>
<div class="center-wrap"><button class="btn" onclick="restart()">Play Again</button></div>`);
}
function restart(){ showScreen('classSelect'); }
// ---------- Overlay ----------
function showOverlay(html){
el('overlayBox').innerHTML = `<div class="overlayBox">${html}</div>`;
el('overlay').classList.add('active');
}
function hideOverlay(){ el('overlay').classList.remove('active'); }
// ---------- Rest site ----------
function showRest(){
const p = Game.player;
const healAmt = Math.floor(p.maxHp*0.25);
const html = `<h2>Rest Site</h2>
<div class="msgText">A moment of safety in the Abyss. Choose one:</div>
<div class="listChoices">
<button class="btn" onclick="restAction('heal')">🏕️ Rest — Heal ${healAmt} HP (25% max)</button>
<button class="btn" onclick="restAction('forge')">🔨 Forge — Upgrade a card</button>
<button class="btn" onclick="restAction('meditate')">🧘 Meditate — Remove a card</button>
</div>`;
showOverlay(html);
}
function restAction(choice){
const p = Game.player;
if(choice==='heal'){
const heal = Math.floor(p.maxHp*0.25);
p.hp = Math.min(p.maxHp, p.hp+heal);
hideOverlay(); advanceNode();
} else if(choice==='forge'){
upgradeFromDeck();
} else if(choice==='meditate'){
removeFromDeck();
}
}
function upgradeFromDeck(){
const p = Game.player;
const upgradable = p.deck.filter(c=>!c.upgradeCard);
if(upgradable.length===0){ showOverlay(`<h2>Forge</h2><div class="msgText">No cards to upgrade.</div><div class="center-wrap"><button class="btn" onclick="hideOverlay();renderMap();">Back</button></div>`); return; }
const html = `<h2>Forge</h2><div class="msgText">Choose a card to upgrade:</div><div class="rewardCards" id="forgeCards"></div>`;
showOverlay(html);
const rc = el('forgeCards');
upgradable.forEach(card=>{
const d = createRewardCard(card);
d.onclick = ()=>{ card.upgradeCard = {id:card.id+'+'}; card.upgraded = true; hideOverlay(); advanceNode(); };
rc.appendChild(d);
});
}
function removeFromDeck(){
const p = Game.player;
const removable = p.deck.filter(c=>!c.curse && !c.token);
if(removable.length===0){ showOverlay(`<h2>Meditate</h2><div class="msgText">No cards to remove.</div><div class="center-wrap"><button class="btn" onclick="hideOverlay();renderMap();">Back</button></div>`); return; }
const html = `<h2>Meditate</h2><div class="msgText">Choose a card to remove:</div><div class="rewardCards" id="medCards"></div>`;
showOverlay(html);
const rc = el('medCards');
removable.forEach(card=>{
const d = createRewardCard(card);
d.onclick = ()=>{ p.deck = p.deck.filter(c=>c!==card); hideOverlay(); advanceNode(); };
rc.appendChild(d);
});
}
// ---------- Shop ----------
function showShop(){
const p = Game.player;
const pool = CLASS_POOLS[p.classId];
const wares = [];
// 3 class cards
for(let i=0;i<3;i++){
const rarity = pick(['Common','Common','Uncommon','Rare']);
const candidates = pool.filter(id=>CARDS[id] && CARDS[id].rarity===rarity && CARDS[id].type!=='Basic');
if(candidates.length){ wares.push({kind:'card', card:makeCard(pick(candidates), false), price:{Common:50,Uncommon:75,Rare:150}[rarity]}); }
}
// 1 colorless
const colorless = ['flashOfInsight','ironWill','quickSlash','potentBrew'];
wares.push({kind:'card', card:makeCard(pick(colorless), false), price:60});
// 2 relics
const relicIds = shuffle(Object.keys(RELICS)).filter(k=>RELICS[k].rarity!=='Boss').slice(0,2);
relicIds.forEach(id=> wares.push({kind:'relic', id, price:150}));
// 3 potions
for(let i=0;i<3;i++){ wares.push({kind:'potion', potion:pick(POTION_IDS), price:50}); }
renderShop(wares);
}
function renderShop(wares){
const p = Game.player;
let html = `<h2>Shop</h2>
<div class="msgText">Gold: <span style="color:var(--gold)">${p.gold}◈</span></div>
<div class="shopGrid" id="shopGrid"></div>
<div class="listChoices">
<button class="btn small" id="removeCardBtn" onclick="shopRemoveCard()">Remove 1 card from deck — 75g</button>
<button class="btn small" onclick="leaveShop()">Leave Shop</button>
</div>`;
showOverlay(html);
const grid = el('shopGrid');
const saved = el('overlayBox');
saved._shopWares = wares;
wares.forEach((w, i)=>{
const d = document.createElement('div');
d.className='shopItem';
d._idx = i;
let inner='';
if(w.kind==='card'){
inner = `<div class="itemIcon">${w.card.img}</div><div>${w.card.name}</div>
<div class="tooltip">${w.card.type} · ${w.card.rarity}</div><div class="price">${w.price}◈</div>`;
} else if(w.kind==='relic'){
const rel = RELICS[w.id];
inner = `<div class="itemIcon">${rel.img}</div><div>${rel.name}</div>
<div class="tooltip">${rel.rarity}</div><div class="price">${w.price}◈</div>`;
} else if(w.kind==='potion'){
const pot = POTIONS[w.potion];
inner = `<div class="itemIcon">${pot.img}</div><div>${pot.name}</div><div class="price">${w.price}◈</div>`;
}
d.innerHTML = inner;
d.onclick = ()=>buyItem(i);
grid.appendChild(d);
});
if(p.gold<75){ el('removeCardBtn').disabled = true; }
}
function buyItem(i){
const p = Game.player;
const w = el('overlayBox')._shopWares[i];
if(!w || p.gold < w.price) return;
p.gold -= w.price;
if(w.kind==='card'){ p.deck.push(w.card); }
else if(w.kind==='relic'){ p.relics.push(w.id); if(RELICS[w.id].oneTime){ /* one-time use handled */ } }
else if(w.kind==='potion'){ if(p.potions.length<p.potionSlots) p.potions.push(w.potion); }
el('overlayBox')._shopWares[i].sold = true;
const node = el('shopGrid').children[i];
if(node) node.classList.add('sold');
renderShop(el('overlayBox')._shopWares);
}
function shopRemoveCard(){
const p = Game.player;
if(p.gold<75) return;
const removable = p.deck.filter(c=>!c.curse && !c.token);
if(removable.length===0) return;
const html = `<h2>Remove Card</h2><div class="msgText">Choose a card to remove (75g):</div><div class="rewardCards" id="remCards"></div>`;
showOverlay(html);
const rc = el('remCards');
removable.forEach(card=>{
const d = createRewardCard(card);
d.onclick = ()=>{ p.gold-=75; p.deck=p.deck.filter(c=>c!==card); hideOverlay(); renderShop(el('overlayBox')._shopWares); };
rc.appendChild(d);
});
}
function leaveShop(){ hideOverlay(); renderMap(); }
// ---------- Events ----------
function showEvent(){
const eventId = pick(Object.keys(EVENTS));
const ev = EVENTS[eventId];
const html = `<h2>${ev.name}</h2><div class="msgText">${ev.text}</div><div class="listChoices" id="eventChoices"></div>`;
showOverlay(html);
const lc = el('eventChoices');
el('overlayBox')._event = ev;
ev.choices.forEach((ch, i)=>{
const b = document.createElement('button');
b.className='btn';
b.textContent = ch.label;
b.onclick = ()=>runEventChoice(i);
lc.appendChild(b);
});
}
function runEventChoice(i){
const ev = el('overlayBox')._event;
const ch = ev.choices[i];
const g = Game;
const res = ch.fx(g);
hideOverlay();
if(res && res.fail){
showOverlay(`<div class="msgText">${res.fail}</div><div class="center-wrap"><button class="btn" onclick="hideOverlay();renderMap();">OK</button></div>`);
return;
}
if(res && res.msg){ log(res.msg); }
// Check death
if(Game.player.hp<=0){ handlePlayerDeath(); return; }
advanceNode();
}
// ---------- Relic / potion / card helpers ----------
function gainRelic(id){
const p = Game.player;
if(p.relics.includes(id)){ return; }
p.relics.push(id);
}
function addPotion(pid){
const p = Game.player;
if(p.potions.length < p.potionSlots){ p.potions.push(pid); }
}
function addCurse(id){
const p = Game.player;
p.deck.push(makeCard(id));
}
function removeCardForEvent(){
const p = Game.player;
const removable = p.deck.filter(c=>!c.curse && !c.token);
if(removable.length===0) return null;
const card = removable[0];
p.deck = p.deck.filter(c=>c!==card);
return card;
}
function removeRandomCardForBet(){
const p = Game.player;
const removable = p.deck.filter(c=>!c.curse && !c.token);
if(removable.length===0) return null;
const card = pick(removable);
p.deck = p.deck.filter(c=>c!==card);
return card;
}
function removeAllCurses(){
const p = Game.player;
p.deck = p.deck.filter(c=>!c.curse);
}
function upgradeRandomCards(n){
const p = Game.player;
const upgradable = p.deck.filter(c=>!c.upgradeCard);
const done = [];
for(let i=0;i<n && upgradable.length;i++){
const card = pick(upgradable);
card.upgradeCard = {id:card.id+'+'}; card.upgraded = true;
done.push(card.name);
upgradable.splice(upgradable.indexOf(card),1);
}
return done;
}
// addBuff helper on player
function addBuff(status, amount, dur){
this.statuses[status] = (this.statuses[status]||0) + amount;
}
Game.player && (Game.player.statuses = Game.player.statuses||{});
// ---------- Init ----------
renderClassSelect();
window.restart = restart;
window.gameOver = gameOver;
window.skipReward = skipReward;
window.skipBossReward = skipBossReward;
window.leaveShop = leaveShop;
</script>
</body>
</html>