← Shadows results

Shadows

Qwen 3.6 27b MTP · composer

Initial view of Shadows

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 121,022 bytes · SHA-256 c3246bccef9f
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shadows of the Abyss</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a0f; color: #d4c8b0; font-family: 'Georgia', serif; }
#app { width: 100%; height: 100%; position: relative; }

.screen { display: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; flex-direction: column; align-items: center; justify-content: center; padding: 20px; overflow-y: auto; }
.screen.active { display: flex; }

.game-title { font-size: 3em; color: #c41e3a; text-shadow: 0 0 20px rgba(196,30,58,0.5), 0 0 40px rgba(196,30,58,0.3); margin-bottom: 10px; letter-spacing: 3px; }
.subtitle { font-size: 1.2em; color: #7a6f60; margin-bottom: 40px; font-style: italic; }

.main-btn { background: linear-gradient(180deg, #3d1c1c, #1a0e0e); border: 2px solid #5a2d2d; color: #d4c8b0; padding: 15px 40px; font-size: 1.2em; cursor: pointer; font-family: 'Georgia', serif; transition: all 0.3s; margin-top: 20px; }
.main-btn:hover { background: linear-gradient(180deg, #5a2d2d, #3d1c1c); border-color: #c41e3a; box-shadow: 0 0 15px rgba(196,30,58,0.4); }

.class-grid { display: flex; gap: 20px; margin-top: 20px; flex-wrap: wrap; justify-content: center; }
.class-card { width: 220px; padding: 20px; background: linear-gradient(180deg, #1a1520, #0d0b12); border: 2px solid #3a2d4a; cursor: pointer; transition: all 0.3s; text-align: center; }
.class-card:hover { border-color: #c41e3a; transform: translateY(-5px); box-shadow: 0 5px 20px rgba(196,30,58,0.3); }
.class-card h3 { color: #c41e3a; margin-bottom: 10px; font-size: 1.3em; }
.class-card p { font-size: 0.85em; color: #9a8f80; line-height: 1.4; }

#map-header { display: flex; justify-content: space-between; width: 800px; margin-bottom: 10px; padding: 10px; background: rgba(26,21,32,0.9); border: 1px solid #3a2d4a; }
#map-canvas { cursor: pointer; border: 1px solid #3a2d4a; }

/* Combat Screen */
#screen-combat { justify-content: flex-start; padding: 5px; }
#combat-top-bar { display: flex; gap: 15px; align-items: center; width: 95%; max-width: 900px; margin-bottom: 8px; padding: 8px 12px; background: rgba(26,21,32,0.9); border: 1px solid #3a2d4a; flex-wrap: wrap; }
#player-hp-bar { display: flex; align-items: center; gap: 5px; }
.hp-bar-bg { width: 200px; height: 20px; background: #1a0e0e; border: 1px solid #3d1c1c; position: relative; overflow: hidden; }
.hp-bar-fill { height: 100%; background: linear-gradient(90deg, #8b0000, #c41e3a); transition: width 0.3s; }
#hp-text { font-size: 0.9em; color: #ff6b6b; min-width: 70px; }
#player-block-display { color: #6baed6; font-size: 0.9em; }
#player-energy-display { display: flex; gap: 3px; align-items: center; }
.energy-gem { width: 18px; height: 18px; background: radial-gradient(circle, #ffd700, #b8860b); border-radius: 50%; border: 1px solid #daa520; display: inline-block; }
.energy-gem.used { opacity: 0.3; }
#player-gold-display { color: #ffd700; font-size: 0.9em; margin-left: auto; }

#combat-area { width: 95%; max-width: 900px; min-height: 200px; display: flex; justify-content: center; gap: 30px; padding: 10px 0; position: relative; }
#enemy-zone { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; justify-content: center; width: 100%; }

.enemy-card { background: linear-gradient(180deg, #1a1520, #0d0b12); border: 2px solid #3a2d4a; padding: 15px; min-width: 160px; text-align: center; cursor: pointer; transition: all 0.2s; position: relative; }
.enemy-card:hover { border-color: #c41e3a; }
.enemy-card.selected { border-color: #ff4444; box-shadow: 0 0 15px rgba(255,68,68,0.5); }
.enemy-card.dead { opacity: 0.3; pointer-events: none; filter: grayscale(1); }
.enemy-name { font-size: 0.9em; color: #c41e3a; margin-bottom: 5px; }
.enemy-hp-bar-bg { width: 120px; height: 12px; background: #1a0e0e; border: 1px solid #3d1c1c; margin: 5px auto; overflow: hidden; }
.enemy-hp-fill { height: 100%; background: linear-gradient(90deg, #8b0000, #c41e3a); transition: width 0.3s; }
.enemy-hp-text { font-size: 0.75em; color: #ff6b6b; }
.enemy-block-text { font-size: 0.75em; color: #6baed6; }

#intent-zone { display: flex; gap: 20px; justify-content: center; width: 100%; margin-top: 5px; flex-wrap: wrap; }
.intent-icon { background: rgba(26,21,32,0.8); border: 1px solid #3a2d4a; padding: 5px 10px; font-size: 0.75em; color: #9a8f80; text-align: center; min-width: 100px; }
.intent-icon.attack { color: #ff6b6b; border-color: #5a2d2d; }
.intent-icon.defend { color: #6baed6; border-color: #2d4a5a; }
.intent-icon.buff { color: #ffd700; border-color: #5a5a2d; }
.intent-icon.debuff { color: #9b59b6; border-color: #4a2d5a; }

#hand-zone { display: flex; gap: 8px; justify-content: center; padding: 10px; min-height: 180px; width: 95%; max-width: 900px; flex-wrap: wrap; align-items: flex-end; }
.card { width: 130px; height: 170px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 2px solid #4a3d5a; padding: 8px; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; position: relative; font-size: 0.7em; }
.card:hover { transform: translateY(-15px); z-index: 10; box-shadow: 0 5px 20px rgba(0,0,0,0.8); }
.card.unplayable { opacity: 0.4; cursor: not-allowed; }
.card.unplayable:hover { transform: none; }

.card-cost { position: absolute; top: -5px; left: -5px; width: 24px; height: 24px; background: radial-gradient(circle, #ffd700, #b8860b); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #1a0e0e; font-size: 1.2em; }
.card-cost.free { background: radial-gradient(circle, #aaa, #666); }

.card-name { font-size: 1em; font-weight: bold; margin-bottom: 3px; text-align: center; color: #d4c8b0; line-height: 1.2; }
.card-type-badge { position: absolute; top: -5px; right: -5px; width: 6px; height: 24px; border-radius: 3px; }
.type-attack .card-type-badge { background: #c41e3a; }
.type-skill .card-type-badge { background: #6baed6; }
.type-power .card-type-badge { background: #ffd700; }
.type-curse .card-type-badge { background: #9b59b6; }

.card-desc { font-size: 0.85em; color: #9a8f80; line-height: 1.3; flex-grow: 1; margin-top: 5px; overflow-y: auto; }
.card-rarity { text-align: center; margin-top: 2px; font-size: 0.75em; }
.rarity-common .card-rarity { color: #6baed6; }
.rarity-uncommon .card-rarity { color: #9b59b6; }
.rarity-rare .card-rarity { color: #ffd700; }
.rarity-basic .card-rarity { color: #aaa; }

.card.upgraded .card-name::after { content: " +"; color: #ffd700; }

#deck-info { display: flex; gap: 20px; margin-top: 5px; font-size: 0.8em; color: #7a6f60; }
.combat-btn { background: linear-gradient(180deg, #3d1c1c, #1a0e0e); border: 2px solid #5a2d2d; color: #d4c8b0; padding: 10px 30px; font-size: 1em; cursor: pointer; font-family: 'Georgia', serif; margin-top: 5px; }
.combat-btn:hover { background: linear-gradient(180deg, #5a2d2d, #3d1c1c); border-color: #c41e3a; }

#potion-bar { position: fixed; right: 10px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 8px; z-index: 100; }
.potion-btn { width: 45px; height: 45px; background: linear-gradient(135deg, #2a1530, #1a0e20); border: 2px solid #6a3d8a; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.2em; transition: all 0.2s; }
.potion-btn:hover { border-color: #c41e3a; transform: scale(1.1); }

#combat-log { position: fixed; bottom: 5px; left: 5px; max-width: 300px; max-height: 120px; overflow-y: auto; font-size: 0.7em; color: #7a6f60; background: rgba(10,10,15,0.8); padding: 5px; border-radius: 3px; z-index: 50; }

/* Event Screen */
#event-content { max-width: 600px; text-align: center; }
#event-title { color: #c41e3a; margin-bottom: 20px; font-size: 1.8em; }
#event-text { font-size: 1.1em; line-height: 1.6; margin-bottom: 30px; color: #b0a590; }
.event-choice-btn { display: block; width: 100%; max-width: 400px; margin: 8px auto; padding: 12px 20px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 1px solid #4a3d5a; color: #d4c8b0; cursor: pointer; font-family: 'Georgia', serif; font-size: 0.95em; transition: all 0.2s; text-align: left; }
.event-choice-btn:hover { border-color: #c41e3a; background: linear-gradient(180deg, #3d2d4a, #2a2035); }

/* Shop Screen */
#shop-items { display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; margin: 20px 0; max-width: 900px; }
.shop-item { width: 140px; padding: 10px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 2px solid #4a3d5a; text-align: center; cursor: pointer; transition: all 0.2s; }
.shop-item:hover { border-color: #ffd700; transform: translateY(-3px); }
.shop-item .item-price { color: #ffd700; font-size: 0.85em; margin-top: 5px; }
.shop-item.relic-shop { border-color: #9b59b6; }

/* Rest Screen */
#rest-choices { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; justify-content: center; }
.rest-btn { padding: 15px 30px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 2px solid #4a3d5a; color: #d4c8b0; cursor: pointer; font-family: 'Georgia', serif; font-size: 1em; transition: all 0.2s; min-width: 180px; }
.rest-btn:hover { border-color: #c41e3a; background: linear-gradient(180deg, #3d2d4a, #2a2035); }

/* Reward Screen */
#reward-gold { color: #ffd700; font-size: 1.2em; margin-bottom: 15px; }
#reward-cards { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; }

/* Relic choice */
#relic-choices { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; margin-top: 20px; }
.relic-choice { width: 180px; padding: 15px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 2px solid #9b59b6; text-align: center; cursor: pointer; transition: all 0.2s; }
.relic-choice:hover { border-color: #ffd700; transform: translateY(-3px); }

/* Buff/Debuff badges */
.status-badge { display: inline-block; padding: 1px 5px; font-size: 0.6em; margin: 1px; border-radius: 2px; }
.badge-buff { background: #2d4a2d; color: #90ee90; }
.badge-debuff { background: #4a2d2d; color: #ff6b6b; }

/* Damage animation */
@keyframes damageFlash { 0% { filter: brightness(1); } 50% { filter: brightness(3) hue-rotate(180deg); } 100% { filter: brightness(1); } }
.damage-flash { animation: damageFlash 0.3s ease; }

@keyframes cardPlay { from { transform: scale(1); opacity: 1; } to { transform: scale(0.5) translateY(-100px); opacity: 0; } }

/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0a0a0f; }
::-webkit-scrollbar-thumb { background: #3a2d4a; border-radius: 3px; }

h1, h2, h3 { text-align: center; }
/* ── css: polish ── */
/* Additional visual polish */

/* Card hover effects */
.card { transition: transform 0.15s ease, box-shadow 0.15s ease; }
.card:hover:not(.unplayable) { 
  transform: translateY(-20px) scale(1.05); 
  z-index: 100;
  box-shadow: 0 8px 30px rgba(0,0,0,0.9), 0 0 15px rgba(196,30,58,0.3);
}

/* Card type colors */
.type-attack { border-color: #5a2d2d; }
.type-skill { border-color: #2d4a5a; }
.type-power { border-color: #5a5a2d; }
.type-curse { border-color: #4a2d5a; background: linear-gradient(180deg, #2a1530, #1a0e20); }

/* Rarity glow effects */
.rarity-common:hover:not(.unplayable) { box-shadow: 0 8px 30px rgba(0,0,0,0.9), 0 0 10px rgba(107,174,214,0.3); }
.rarity-uncommon:hover:not(.unplayable) { box-shadow: 0 8px 30px rgba(0,0,0,0.9), 0 0 15px rgba(155,89,182,0.4); }
.rarity-rare:hover:not(.unplayable) { box-shadow: 0 8px 30px rgba(0,0,0,0.9), 0 0 20px rgba(255,215,0,0.4); }

/* Enemy card improvements */
.enemy-card { transition: all 0.2s ease; border-radius: 8px; }
.enemy-card:hover { transform: scale(1.03); }
.enemy-card.selected { 
  box-shadow: 0 0 20px rgba(255,68,68,0.4), inset 0 0 10px rgba(255,68,68,0.1);
}

/* HP bar animations */
.hp-bar-fill { transition: width 0.3s ease; }
.enemy-hp-fill { transition: width 0.3s ease; }

/* Damage number popup animation */
@keyframes damagePopup {
  0% { opacity: 1; transform: translateY(0) scale(1); }
  50% { opacity: 1; transform: translateY(-20px) scale(1.2); }
  100% { opacity: 0; transform: translateY(-40px) scale(0.8); }
}

/* Screen transitions */
.screen { transition: opacity 0.3s ease; }

/* Title screen ambient effect */
#screen-title::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: radial-gradient(ellipse at center, rgba(196,30,58,0.1) 0%, transparent 70%);
  pointer-events: none;
}

/* Combat area background */
#screen-combat::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: radial-gradient(ellipse at center top, rgba(196,30,58,0.05) 0%, transparent 60%);
  pointer-events: none;
}

/* Map node hover */
#map-canvas { transition: filter 0.2s ease; }
#map-canvas:hover { filter: brightness(1.1); }

/* Scrollbar styling for combat log */
#combat-log::-webkit-scrollbar { width: 4px; }
#combat-log::-webkit-scrollbar-thumb { background: #3a2d4a; border-radius: 2px; }

/* Energy gem animation */
@keyframes energyPulse {
  0%, 100% { box-shadow: 0 0 5px rgba(255,215,0,0.3); }
  50% { box-shadow: 0 0 10px rgba(255,215,0,0.6); }
}
.energy-gem:not(.used) { animation: energyPulse 2s ease infinite; }

/* Class card selection effect */
.class-card::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: linear-gradient(180deg, transparent 60%, rgba(196,30,58,0.1));
  pointer-events: none;
}

/* Relic display in combat */
.relic-display { position: fixed; left: 10px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 5px; z-index: 100; }
.relic-icon { width: 35px; height: 35px; background: linear-gradient(135deg, #2a1530, #1a0e20); border: 2px solid #6a3d8a; display: flex; align-items: center; justify-content: center; font-size: 0.9em; cursor: help; }

/* Buff/debuff display */
.buff-display { position: fixed; left: 55px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 3px; z-index: 100; font-size: 0.7em; }

/* Victory screen effects */
#screen-victory::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: radial-gradient(ellipse at center, rgba(255,215,0,0.1) 0%, transparent 70%);
  pointer-events: none;
}

/* Game over screen effects */
#screen-gameover::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: radial-gradient(ellipse at center, rgba(139,0,0,0.2) 0%, transparent 70%);
  pointer-events: none;
}

/* Hand card spacing */
#hand-zone .card { margin-bottom: 5px; }

/* Responsive adjustments for smaller screens */
@media (max-width: 900px) {
  .card { width: 110px; height: 145px; font-size: 0.6em; }
  #hand-zone { gap: 4px; }
}

/* Potion overlay styling */
.screen-overlay { 
  display: none !important; 
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(10,10,15,0.9); z-index: 200;
  flex-direction: column; align-items: center; justify-content: center; gap: 15px;
}

/* Event screen text styling */
#event-text { max-width: 500px; margin: 0 auto 30px; }

/* Shop item hover */
.shop-item:hover:not([style*="opacity"]) { 
  transform: translateY(-5px);
  box-shadow: 0 5px 15px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<div id="app"><!-- Title Screen -->
<div id="screen-title" class="screen active">
  <h1 class="game-title">Shadows of the Abyss</h1>
  <p class="subtitle">A Dark Fantasy Roguelike Deckbuilder</p>
  <button id="btn-start" class="main-btn">Begin Your Descent</button>
</div>

<!-- Class Selection -->
<div id="screen-class-select" class="screen">
  <h2>Choose Your Cursed Archetype</h2>
  <div id="class-cards" class="class-grid"></div>
</div>

<!-- Map Screen -->
<div id="screen-map" class="screen">
  <div id="map-header">
    <span id="map-act-name"></span>
    <span id="map-floor-num"></span>
  </div>
  <canvas id="map-canvas" width="800" height="600"></canvas>
</div>

<!-- Combat Screen -->
<div id="screen-combat" class="screen">
  <div id="combat-top-bar">
    <div id="player-hp-bar"><span id="hp-text"></span></div>
    <div id="player-block-display"></div>
    <div id="player-energy-display"></div>
    <div id="player-gold-display"></div>
  </div>
  <div id="combat-area">
    <div id="enemy-zone"></div>
    <div id="intent-zone"></div>
  </div>
  <div id="hand-zone"></div>
  <div id="deck-info">
    <span id="draw-pile-count"></span>
    <span id="discard-pile-count"></span>
  </div>
  <button id="btn-end-turn" class="combat-btn">End Turn</button>
</div>

<!-- Event Screen -->
<div id="screen-event" class="screen">
  <div id="event-content">
    <h2 id="event-title"></h2>
    <p id="event-text"></p>
    <div id="event-choices"></div>
  </div>
</div>

<!-- Shop Screen -->
<div id="screen-shop" class="screen">
  <h2>The Shadow Market</h2>
  <div id="shop-gold-display"></div>
  <div id="shop-items"></div>
  <button id="btn-leave-shop" class="main-btn">Leave Shop</button>
</div>

<!-- Rest Screen -->
<div id="screen-rest" class="screen">
  <h2>Campfire of the Damned</h2>
  <p id="rest-hp-text"></p>
  <div id="rest-choices"></div>
</div>

<!-- Reward Screen -->
<div id="screen-reward" class="screen">
  <h2>Victory!</h2>
  <div id="reward-gold"></div>
  <div id="reward-cards" class="card-grid"></div>
  <button id="btn-skip-reward" class="main-btn">Skip</button>
</div>

<!-- Game Over Screen -->
<div id="screen-gameover" class="screen">
  <h2>Your Soul is Consumed...</h2>
  <p id="gameover-stats"></p>
  <button id="btn-restart" class="main-btn">Try Again</button>
</div>

<!-- Victory Screen -->
<div id="screen-victory" class="screen">
  <h1>The Abyss is Conquered!</h1>
  <p id="victory-stats"></p>
  <button id="btn-play-again" class="main-btn">Play Again</button>
</div>

<!-- Card Remove Screen -->
<div id="screen-remove-card" class="screen">
  <h2 id="remove-title">Remove a Card</h2>
  <p id="remove-desc"></p>
  <div id="remove-cards" class="card-grid"></div>
  <button id="btn-cancel-remove" class="main-btn">Cancel</button>
</div>

<!-- Upgrade Screen -->
<div id="screen-upgrade-card" class="screen">
  <h2>The Forge</h2>
  <p>Choose a card to upgrade:</p>
  <div id="upgrade-cards" class="card-grid"></div>
  <button id="btn-cancel-upgrade" class="main-btn">Cancel</button>
</div>

<!-- Potion Use Screen -->
<div id="screen-potion-use" class="screen-overlay">
  <h3>Choose Target for Potion</h3>
  <div id="potion-targets"></div>
  <button id="btn-cancel-potion" class="main-btn">Cancel</button>
</div>

<!-- Relic Screen -->
<div id="screen-relic-choice" class="screen">
  <h2>Choose a Relic</h2>
  <div id="relic-choices"></div>
</div>

<!-- Potion buttons in combat -->
<div id="potion-bar"></div>

<!-- Combat log -->
<div id="combat-log"></div><div id="relic-display" class="relic-display"></div>
<div id="buff-display" class="buff-display"></div></div>
<script>
'use strict';

// ── const: CARD_DB ──
const CARD_DB = {
  // Basic Cards (all classes)
  strike: { name: "Strike", type: "attack", cost: 1, rarity: "basic", desc: "Deal 6 damage.", effect: { dmg: 6 }, upgrade: { dmg: 8 } },
  defend: { name: "Defend", type: "skill", cost: 1, rarity: "basic", desc: "Gain 5 Block.", effect: { block: 5 }, upgrade: { block: 8 } },

  // Blood Reaper Cards
  sanguine_strike: { name: "Sanguine Strike", type: "attack", cost: 1, rarity: "common", desc: "Deal 8 damage. Heal 2 HP.", effect: { dmg: 8, heal: 2 }, upgrade: { dmg: 10, heal: 3 } },
  hemorrhage: { name: "Hemorrhage", type: "attack", cost: 1, rarity: "common", desc: "Deal 5 damage. Apply 3 Bleed.", effect: { dmg: 5, bleed: 3 }, upgrade: { dmg: 6, bleed: 4 } },
  bloodletting: { name: "Bloodletting", type: "attack", cost: 1, rarity: "uncommon", desc: "Lose 3 HP. Deal 12 damage.", effect: { selfDmg: 3, dmg: 12 }, upgrade: { selfDmg: 2, dmg: 15 } },
  crimson_eruption: { name: "Crimson Eruption", type: "attack", cost: 2, rarity: "rare", desc: "Deal 8 damage to ALL enemies. Lose 2 HP.", effect: { dmgAll: 8, selfDmg: 2 }, upgrade: { dmgAll: 10, selfDmg: 2 } },
  crimson_shield: { name: "Crimson Shield", type: "skill", cost: 1, rarity: "common", desc: "Gain 6 Block. Lose 2 HP.", effect: { block: 6, selfDmg: 2 }, upgrade: { block: 8, selfDmg: 2 } },
  blood_ward: { name: "Blood Ward", type: "skill", cost: 1, rarity: "common", desc: "Gain 3 Block. Gain 1 Regeneration.", effect: { block: 3, regen: 1 }, upgrade: { block: 4, regen: 2 } },
  sacrificial_barrier: { name: "Sacrificial Barrier", type: "skill", cost: 2, rarity: "uncommon", desc: "Gain 10 Block. Lose 5 HP.", effect: { block: 10, selfDmg: 5 }, upgrade: { block: 13, selfDmg: 5 } },
  vital_surge: { name: "Vital Surge", type: "skill", cost: 0, rarity: "rare", desc: "Gain Block equal to your missing HP. Exhaust.", effect: { blockMissingHp: true }, exhaust: true, upgrade: { blockMissingHpPlus: 3 } },
  life_tap: { name: "Life Tap", type: "skill", cost: 0, rarity: "common", desc: "Draw 2 cards. Lose 3 HP.", effect: { draw: 2, selfDmg: 3 }, upgrade: { draw: 3, selfDmg: 3 } },
  rend_flesh: { name: "Rend Flesh", type: "skill", cost: 1, rarity: "uncommon", desc: "Apply 5 Bleed to ALL enemies. Lose 2 HP.", effect: { bleedAll: 5, selfDmg: 2 }, upgrade: { bleedAll: 7, selfDmg: 2 } },
  soul_siphon: { name: "Soul Siphon", type: "power", cost: 2, rarity: "uncommon", desc: "(Power) At start of turn, gain 1 Energy and lose 1 HP.", effect: { powerStartTurnEnergy: 1, powerSelfDmg: 1 }, upgradeCost: 1 },
  blood_pact: { name: "Blood Pact", type: "power", cost: 1, rarity: "rare", desc: "(Power) Whenever you lose HP from a card, gain 1 Strength.", effect: { strengthOnHpLoss: 1 }, upgrade: { strengthOnHpLoss: 2 } },

  // Shadowmancer Cards
  shadow_bolt: { name: "Shadow Bolt", type: "attack", cost: 1, rarity: "common", desc: "Deal 6 damage. Apply 1 Weak.", effect: { dmg: 6, weak: 1 }, upgrade: { dmg: 8, weak: 2 } },
  grasp_of_despair: { name: "Grasp of Despair", type: "attack", cost: 1, rarity: "common", desc: "Deal 4 damage. Apply 1 Vulnerable.", effect: { dmg: 4, vulnerable: 1 }, upgrade: { dmg: 5, vulnerable: 2 } },
  nightmare_lash: { name: "Nightmare Lash", type: "attack", cost: 2, rarity: "uncommon", desc: "Deal 7 damage. If enemy has Weak, deal 7 more.", effect: { dmg: 7, extraIfWeak: 7 }, upgrade: { dmg: 9, extraIfWeak: 9 } },
  eternal_darkness: { name: "Eternal Darkness", type: "attack", cost: 2, rarity: "rare", desc: "Deal 3 damage to a random enemy 3 times.", effect: { multiRandomDmg: 3, multiCount: 3 }, upgrade: { multiRandomDmg: 4, multiCount: 3 } },
  cloak_of_shadows: { name: "Cloak of Shadows", type: "skill", cost: 1, rarity: "common", desc: "Gain 5 Block. Draw 1 card.", effect: { block: 5, draw: 1 }, upgrade: { block: 7, draw: 1 } },
  haunting_mist: { name: "Haunting Mist", type: "skill", cost: 1, rarity: "common", desc: "Gain 7 Block. Apply 1 Weak to ALL enemies.", effect: { block: 7, weakAll: 1 }, upgrade: { block: 9, weakAll: 1 } },
  phantom_armor: { name: "Phantom Armor", type: "skill", cost: 1, rarity: "uncommon", desc: "Gain 8 Block. Next turn, gain 2 Dexterity.", effect: { block: 8, nextDex: 2 }, upgrade: { block: 10, nextDex: 3 } },
  whispering_shroud: { name: "Whispering Shroud", type: "skill", cost: 1, rarity: "uncommon", desc: "Gain 4 Block. Next turn, gain 4 Block.", effect: { block: 4, nextBlock: 4 }, upgrade: { block: 5, nextBlock: 5 } },
  shadowform: { name: "Shadowform", type: "skill", cost: 2, rarity: "uncommon", desc: "Gain 2 Intangible. Exhaust.", effect: { intangible: 2 }, exhaust: true, upgrade: { intangible: 3 } },
  curse_of_misfortune: { name: "Curse of Misfortune", type: "skill", cost: 1, rarity: "common", desc: "Enemy loses 3 Strength this turn. Apply 1 Vulnerable.", effect: { enemyLoseStr: 3, vulnerable: 1 }, upgrade: { enemyLoseStr: 4, vulnerable: 2 } },
  dark_ritual: { name: "Dark Ritual", type: "skill", cost: 0, rarity: "uncommon", desc: "Gain 2 Energy. Shuffle a Dazed into your discard pile.", effect: { gainEnergy: 2, shuffleDazed: true }, upgrade: {} },
  umbral_mastery: { name: "Umbral Mastery", type: "power", cost: 2, rarity: "rare", desc: "(Power) At start of turn, apply 1 Weak to ALL enemies.", effect: { powerWeakAll: 1 }, upgrade: { powerWeakAll: 2 } },

  // Hellforged Cards
  firebolt: { name: "Firebolt", type: "attack", cost: 1, rarity: "common", desc: "Deal 7 damage. Apply 2 Burn.", effect: { dmg: 7, burn: 2 }, upgrade: { dmg: 9, burn: 3 } },
  hellfire_blast: { name: "Hellfire Blast", type: "attack", cost: 2, rarity: "uncommon", desc: "Deal 9 damage to ALL enemies.", effect: { dmgAll: 9 }, upgrade: { dmgAll: 12 } },
  searing_strike: { name: "Searing Strike", type: "attack", cost: 1, rarity: "common", desc: "Deal 6 damage. Gain 1 Energy next turn.", effect: { dmg: 6, nextEnergy: 1 }, upgrade: { dmg: 8, nextEnergy: 1 } },
  immolate: { name: "Immolate", type: "attack", cost: 2, rarity: "rare", desc: "Deal 12 damage. Exhaust.", effect: { dmg: 12 }, exhaust: true, upgrade: { dmg: 16 } },
  obsidian_skin: { name: "Obsidian Skin", type: "skill", cost: 1, rarity: "common", desc: "Gain 7 Block.", effect: { block: 7 }, upgrade: { block: 10 } },
  flame_ward: { name: "Flame Ward", type: "skill", cost: 1, rarity: "common", desc: "Gain 5 Block. Apply 2 Burn to a random enemy.", effect: { block: 5, burnRandom: 2 }, upgrade: { block: 6, burnRandom: 3 } },
  demonic_resilience: { name: "Demonic Resilience", type: "skill", cost: 1, rarity: "uncommon", desc: "Gain 4 Block. Heal 2 HP.", effect: { block: 4, heal: 2 }, upgrade: { block: 5, heal: 3 } },
  brimstone_shield: { name: "Brimstone Shield", type: "skill", cost: 1, rarity: "uncommon", desc: "Gain 3 Block. Gain 2 Strength this turn.", effect: { block: 3, tempStr: 2 }, upgrade: { block: 4, tempStr: 3 } },
  summon_imp: { name: "Summon Imp", type: "skill", cost: 1, rarity: "common", desc: "Add an Imp Claw to your hand. Exhaust.", effect: { addImpClaw: true }, exhaust: true, upgrade: { addImpClawPlus: true } },
  infernal_pact: { name: "Infernal Pact", type: "skill", cost: 0, rarity: "uncommon", desc: "Lose 2 HP. Gain 2 Energy. Add an Imp Claw to your hand.", effect: { selfDmg: 2, gainEnergy: 2, addImpClaw: true }, upgrade: { selfDmg: 1, gainEnergy: 2, addImpClawPlus: true } },
  hellfire_aura: { name: "Hellfire Aura", type: "power", cost: 2, rarity: "rare", desc: "(Power) At start of turn, deal 3 damage to ALL enemies.", effect: { powerDmgAllStartTurn: 3 }, upgrade: { powerDmgAllStartTurn: 5 } },
  soul_burn: { name: "Soul Burn", type: "skill", cost: 1, rarity: "uncommon", desc: "Apply 8 Burn to an enemy. Draw 1 card.", effect: { burn: 8, draw: 1 }, upgrade: { burn: 10, draw: 1 } },

  // Colorless Cards (shop only)
  flash_of_insight: { name: "Flash of Insight", type: "skill", cost: 1, rarity: "common", desc: "Draw 2 cards.", effect: { draw: 2 }, upgrade: { draw: 3 } },
  iron_will: { name: "Iron Will", type: "skill", cost: 1, rarity: "common", desc: "Gain 8 Block.", effect: { block: 8 }, upgrade: { block: 11 } },
  quick_slash: { name: "Quick Slash", type: "attack", cost: 1, rarity: "common", desc: "Deal 7 damage.", effect: { dmg: 7 }, upgrade: { dmg: 9 } },
  potent_brew: { name: "Potent Brew", type: "skill", cost: 0, rarity: "uncommon", desc: "Gain 1 Energy. Exhaust.", effect: { gainEnergy: 1 }, exhaust: true, upgrade: { gainEnergy: 2 } },

  // Curses
  wound: { name: "Wound", type: "curse", cost: -1, rarity: "basic", desc: "Unplayable. Ethereal.", unplayable: true, ethereal: true },
  doubt: { name: "Doubt", type: "curse", cost: -1, rarity: "basic", desc: "Unplayable. At end of turn, lose 1 HP.", unplayable: true, endTurnSelfDmg: 1 },
  dazed: { name: "Dazed", type: "status", cost: -1, rarity: "basic", desc: "Unplayable. Ethereal.", unplayable: true, ethereal: true },

  // Imp Claw (generated)
  imp_claw: { name: "Imp Claw", type: "attack", cost: 0, rarity: "basic", desc: "Deal 4 damage. Exhaust.", effect: { dmg: 4 }, exhaust: true },
  imp_claw_plus: { name: "Imp Claw+", type: "attack", cost: 0, rarity: "basic", desc: "Deal 6 damage. Exhaust.", effect: { dmg: 6 }, exhaust: true }
};

// ── const: CLASS_DATA ──
const CLASS_DATA = {
  blood_reaper: {
    name: "Blood Reaper",
    theme: "Sacrifice HP for power, life steal, Bleed.",
    color: "#c41e3a",
    signatureCard: "sanguine_strike",
    cardPool: ["sanguine_strike","hemorrhage","bloodletting","crimson_eruption","crimson_shield","blood_ward","sacrificial_barrier","vital_surge","life_tap","rend_flesh","soul_siphon","blood_pact"]
  },
  shadowmancer: {
    name: "Shadowmancer",
    theme: "Debuffs (Weak, Vulnerable), Intangible, card manipulation.",
    color: "#9b59b6",
    signatureCard: "shadow_bolt",
    cardPool: ["shadow_bolt","grasp_of_despair","nightmare_lash","eternal_darkness","cloak_of_shadows","haunting_mist","phantom_armor","whispering_shroud","shadowform","curse_of_misfortune","dark_ritual","umbral_mastery"]
  },
  hellforged: {
    name: "Hellforged",
    theme: "Fire damage, Burn, summoning Imps, energy manipulation.",
    color: "#ff6b35",
    signatureCard: "firebolt",
    cardPool: ["firebolt","hellfire_blast","searing_strike","immolate","obsidian_skin","flame_ward","demonic_resilience","brimstone_shield","summon_imp","infernal_pact","hellfire_aura","soul_burn"]
  }
};

// ── const: RELIC_DB ──
const RELIC_DB = {
  bloodstone: { name: "Bloodstone", desc: "Whenever you lose HP from a card, heal 1 HP.", rarity: "common" },
  demon_horns: { name: "Demon Horns", desc: "At start of combat, add random Imp Claw to hand. (Hellforged only)", rarity: "uncommon", classReq: "hellforged" },
  shadow_cloak: { name: "Shadow Cloak", desc: "At start of each combat, gain 1 Intangible.", rarity: "rare" },
  cursed_amulet: { name: "Cursed Amulet", desc: "At start of each combat, apply 1 Weak to ALL enemies.", rarity: "common" },
  tome_of_dark_rituals: { name: "Tome of Dark Rituals", desc: "Whenever you play a Power card, draw 1 card.", rarity: "uncommon" },
  bleeding_edge: { name: "Bleeding Edge", desc: "At start of your turn, apply 1 Bleed to a random enemy.", rarity: "common" },
  soul_ember: { name: "Soul Ember", desc: "Whenever you apply Burn to an enemy, deal 2 additional damage.", rarity: "rare" },
  eternal_hourglass: { name: "Eternal Hourglass", desc: "At start of turn, if no cards in hand, gain 1 Energy.", rarity: "uncommon" },
  sacrificial_dagger: { name: "Sacrificial Dagger", desc: "Your first Attack that costs HP each combat costs 1 less HP. (Blood Reaper only)", rarity: "uncommon", classReq: "blood_reaper" },
  vial_of_shadows: { name: "Vial of Shadows", desc: "When obtained, remove a card from your deck.", rarity: "rare", unique: true },
  ashen_armor: { name: "Ashen Armor", desc: "At end of combat, heal 6 HP.", rarity: "common" },
  greedy_imp: { name: "Greedy Imp", desc: "Enemies drop 10 extra Gold.", rarity: "common" }
};

// ── const: BOSS_RELICS ──
const BOSS_RELICS = ["cursed_crown","ectoplasm","philosophers_stone","black_blood","shadow_amulet"];

// ── const: BOSS_RELIC_DB ──
const BOSS_RELIC_DB = {
  cursed_crown: { name: "Cursed Crown", desc: "Gain 1 Energy at start of each turn. Curses can no longer be removed.", rarity: "boss" },
  ectoplasm: { name: "Ectoplasm", desc: "Gain 1 Energy. Lose all Gold and gain no more Gold this run.", rarity: "boss" },
  philosophers_stone: { name: "Philosopher's Stone", desc: "Gain 1 Energy. All enemies start combat with 1 Strength.", rarity: "boss" },
  black_blood: { name: "Black Blood", desc: "At start of combat, lose 10% max HP. Whenever you play an Attack, heal 1 HP.", rarity: "boss" },
  shadow_amulet: { name: "Shadow Amulet", desc: "Start each turn with 1 Intangible. Reduce max HP by 15.", rarity: "boss" }
};

// ── const: POTION_DB ──
const POTION_DB = [
  { id: "blood_vial", name: "Blood Vial", icon: "🩸", desc: "Heal 15 HP." },
  { id: "firebomb", name: "Firebomb", icon: "💥", desc: "Deal 10 damage to ALL enemies." },
  { id: "dex_tonic", name: "Dex Tonic", icon: "⚡", desc: "Gain 3 Dexterity for 1 turn." },
  { id: "energy_elixir", name: "Energy Elixir", icon: "✨", desc: "Gain 2 Energy this turn." },
  { id: "cursed_brew", name: "Cursed Brew", icon: "🧪", desc: "Apply 3 Vulnerable to all enemies." },
  { id: "bleeding_flask", name: "Bleeding Flask", icon: "💉", desc: "Apply 6 Bleed to an enemy." },
  { id: "imp_in_bottle", name: "Imp-in-a-Bottle", icon: "👹", desc: "Add an Imp Claw+ to your hand." },
  { id: "smoke_bomb", name: "Smoke Bomb", icon: "💨", desc: "Escape from combat, no rewards." }
];

// ── const: COLORLESS_CARDS ──
const COLORLESS_CARDS = ["flash_of_insight","iron_will","quick_slash","potent_brew"];

// ── const: ENEMY_DB ──
const ENEMY_DB = {
  // Act 1 - The Crypts
  skeleton_warrior: { name: "Skeleton Warrior", hp: 22, maxHp: 22, intents: [
    { type: "attack", dmg: 6 }, { type: "attack", dmg: 6 }, { type: "defend", block: 8 }
  ]},
  zombie_brute: { name: "Zombie Brute", hp: 28, maxHp: 28, intents: [
    { type: "attack", dmg: 8 }, { type: "debuff", weak: 2 }, { type: "attack", dmg: 8 }
  ]},
  dark_cultist: { name: "Dark Cultist", hp: 20, maxHp: 20, intents: [
    { type: "buff", str: 3 }, { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }
  ]},
  rat_swarm: { name: "Rat Swarm", hp: 15, maxHp: 15, intents: [
    { type: "multiHit", hits: [{dmg:3},{dmg:3}] }, { type: "multiHit", hits: [{dmg:3},{dmg:3}] }
  ]},

  // Act 1 Elites
  tomb_guardian: { name: "Tomb Guardian", hp: 42, maxHp: 42, intents: [
    { type: "attackDefend", dmg: 6, block: 8 }, { type: "multiHit", hits: [{dmg:4},{dmg:4},{dmg:4}] }, { type: "attack", dmg: 12 }
  ]},
  blood_witch: { name: "Blood Witch", hp: 36, maxHp: 36, intents: [
    { type: "drainLife", dmg: 5, heal: 4 }, { type: "debuffAll", vulnerable: 1, weak: 1 }, { type: "drainLife", dmg: 7, heal: 4 }
  ]},

  // Act 2 - Blighted Woods
  wraith: { name: "Wraith", hp: 30, maxHp: 30, intents: [
    { type: "attackDebuff", dmg: 7, weak: 1 }, { type: "attack", dmg: 7 }, { type: "defend", block: 10 }
  ]},
  corrupted_beast: { name: "Corrupted Beast", hp: 38, maxHp: 38, intents: [
    { type: "attack", dmg: 12 }, { type: "selfVulnDefend", block: 5 }, { type: "attack", dmg: 12 }
  ]},
  thorned_horror: { name: "Thorned Horror", hp: 25, maxHp: 25, intents: [
    { type: "multiHit", hits: [{dmg:4},{dmg:4},{dmg:4}] }, { type: "debuff", bleed: 3 }, { type: "attack", dmg: 8 }
  ]},
  witch_doctor: { name: "Witch Doctor", hp: 22, maxHp: 22, intents: [
    { type: "selfBuff", block: 4, regen: 2 }, { type: "attackDebuff", dmg: 6, poison: 2 }, { type: "attack", dmg: 6 }
  ]},

  // Act 2 Elites
  treant_protector: { name: "Treant Protector", hp: 50, maxHp: 50, intents: [
    { type: "attackDefend", dmg: 9, block: 6 }, { type: "debuffAll", vulnerable: 2 }, { type: "multiHit", hits: [{dmg:10},{dmg:10}] }
  ]},
  plague_shaman: { name: "Plague Shaman", hp: 42, maxHp: 42, intents: [
    { type: "debuff", poison: 4 }, { type: "summon", summonId: "plague_rat" }, { type: "attack", dmg: 8 }
  ]},

  // Act 3 - Abyssal Spire
  hellhound: { name: "Hellhound", hp: 26, maxHp: 26, intents: [
    { type: "attackDebuff", dmg: 8, burn: 2 }, { type: "attack", dmg: 8 }, { type: "defend", block: 9 }
  ]},
  shadow_demon: { name: "Shadow Demon", hp: 30, maxHp: 30, intents: [
    { type: "attackDebuff", dmg: 7, weak: 1 }, { type: "attack", dmg: 7 }, { type: "selfBuff", intangible: 1 }
  ]},
  infernal_imp: { name: "Infernal Imp", hp: 18, maxHp: 18, intents: [
    { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }, { type: "summon", summonId: "infernal_imp" }
  ]},
  abyssal_cultist: { name: "Abyssal Cultist", hp: 24, maxHp: 24, intents: [
    { type: "buffAll", str: 2 }, { type: "attack", dmg: 7 }, { type: "attack", dmg: 7 }
  ]},

  // Act 3 Elites
  flamespawn: { name: "Flamespawn", hp: 48, maxHp: 48, intents: [
    { type: "attackDebuff", dmg: 9, burn: 3 }, { type: "defendBuff", block: 10, str: 2 }, { type: "multiHit", hits: [{dmg:8},{dmg:8}] }
  ]},
  dread_knight: { name: "Dread Knight", hp: 55, maxHp: 55, intents: [
    { type: "attack", dmg: 12 }, { type: "debuff", bleed: 3 }, { type: "defendHeal", block: 12, heal: 4 }
  ]},

  // Minions/Summons
  skeleton_minion: { name: "Skeleton Minion", hp: 15, maxHp: 15, intents: [
    { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }
  ]},
  plague_rat: { name: "Plague Rat", hp: 18, maxHp: 18, intents: [
    { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }
  ]},

  // Shadow Thief (event enemy)
  shadow_thief: { name: "Shadow Thief", hp: 30, maxHp: 30, intents: [
    { type: "attackDebuff", dmg: 6, weak: 1 }, { type: "defend", block: 8 }, { type: "attack", dmg: 7 }
  ]}
};

// ── const: BOSS_DB ──
const BOSS_DB = {
  act1: { name: "Lich King", hp: 65, maxHp: 65, isBoss: true, phase2Threshold: 30,
    phase1Intents: [
      { type: "summon", summonId: "skeleton_minion" },
      { type: "attack", dmg: 8 },
      { type: "defend", block: 12 }
    ],
    phase2Intents: [
      { type: "attackStrGain", dmg: 10, strGain: 2 },
      { type: "attackStrGain", dmg: 10, strGain: 2 },
      { type: "defendStrGain", block: 15, strGain: 2 }
    ]
  },
  act2: { name: "Eldritch Treant", hp: 75, maxHp: 75, isBoss: true, phase2Threshold: 40,
    phase1Intents: [
      { type: "attackDebuff", dmg: 10, weak: 2 },
      { type: "defendBuff", block: 15, regen: 2 },
      { type: "multiHit", hits: [{dmg:8},{dmg:8}] }
    ],
    phase2Intents: [
      { type: "attackDebuffStrGain", dmg: 14, vulnerable: 1, strGain: 1 },
      { type: "defendHealStrGain", block: 20, heal: 5, strGain: 1 },
      { type: "attackDebuffStrGain", dmg: 14, vulnerable: 1, strGain: 1 }
    ]
  },
  act3: { name: "Archdemon", hp: 85, maxHp: 85, isBoss: true, phase2Threshold: 45,
    phase1Intents: [
      { type: "summon", summonId: "hellhound" },
      { type: "attackDebuff", dmg: 12, burn: 2 },
      { type: "defendBuff", block: 15, str: 3 }
    ],
    phase2Intents: [
      { type: "attackStrGain", dmg: 16, strGain: 2 },
      { type: "multiHitStrGain", hits: [{dmg:7},{dmg:7},{dmg:7}], strGain: 2 },
      { type: "attackStrGain", dmg: 16, strGain: 2 }
    ]
  }
};

// ── const: ACT_ENEMY_POOLS ──
const ACT_ENEMY_POOLS = {
  act1: ["skeleton_warrior","zombie_brute","dark_cultist","rat_swarm"],
  act2: ["wraith","corrupted_beast","thorned_horror","witch_doctor"],
  act3: ["hellhound","shadow_demon","infernal_imp","abyssal_cultist"]
};

// ── const: ACT_ELITE_POOLS ──
const ACT_ELITE_POOLS = {
  act1: ["tomb_guardian","blood_witch"],
  act2: ["treant_protector","plague_shaman"],
  act3: ["flamespawn","dread_knight"]
};

// ── const: MAP_LAYOUT ──
// Map structure per act - floor layout with node types
const MAP_LAYOUT = [
  { floor: 2, type: "enemy", branch: true },
  { floor: 3, type: "enemy" },
  { floor: 4, type: "event" },
  { floor: 5, type: "shop" },
  { floor: 6, type: "enemy" },
  { floor: 7, type: "rest" },
  { floor: 8, type: "enemy", branch: true },
  { floor: 9, type: "elite", altType: "enemy" },
  { floor: 10, type: "event" },
  { floor: 11, type: "enemy" },
  { floor: 12, type: "elite", altType: "enemy" },
  { floor: 13, type: "enemy" },
  { floor: 14, type: "rest" },
  { floor: 15, type: "boss" }
];

// ── const: EVENTS ──
const EVENTS = [
  { id: "forgotten_altar", title: "The Forgotten Altar", text: "A blood-stained altar hums with power.", choices: [
    { text: "[Offer 15 HP] → Obtain a random Rare relic.", action: "offer_hp_relic" },
    { text: "[Pray] → Heal 10 HP, gain a Curse (Doubt).", action: "pray_heal_curse" },
    { text: "[Leave]", action: "nothing" }
  ]},
  { id: "shadowy_peddler", title: "Shadowy Peddler", text: "A hooded figure offers a trade.", choices: [
    { text: "[Trade a card] → Select a card to remove, gain 100 Gold.", action: "trade_card" },
    { text: "[Buy his wares] → Lose 50 Gold, obtain a random Potion and a random Common relic.", action: "buy_wares" },
    { text: "[Attack] → Begin combat with Shadow Thief, get 75 Gold on victory.", action: "attack_peddler" }
  ]},
  { id: "cursed_spring", title: "Cursed Spring", text: "A spring bubbles with dark water.", choices: [
    { text: "[Drink] → Lose 10 HP, remove all Curses from deck.", action: "drink_cures" },
    { text: "[Bottle it] → Obtain a potion (Cursed Brew).", action: "bottle_potion" },
    { text: "[Leave]", action: "nothing" }
  ]},
  { id: "restless_dead", title: "Restless Dead", text: "Ghosts offer a bargain.", choices: [
    { text: "[Accept] → Lose 20 HP, upgrade 2 random cards in your deck.", action: "upgrade_random" },
    { text: "[Refuse]", action: "nothing" }
  ]},
  { id: "impish_gambler", title: "Impish Gambler", text: "A grinning imp challenges you to a game of chance.", choices: [
    { text: "[Bet 30 Gold] → 50% chance to double (60 Gold) or lose it all.", action: "bet_gold" },
    { text: "[Wager a card] → Remove a random card; if it's a Curse, gain 100 Gold.", action: "wager_card" },
    { text: "[Leave]", action: "nothing" }
  ]},
  { id: "ancient_library", title: "Ancient Library", text: "Dusty tomes contain forgotten knowledge.", choices: [
    { text: "[Study] → Gain +1 Energy next combat only, lose 5 HP.", action: "study_energy" },
    { text: "[Read forbidden page] → Add a random Power card from your class to deck, gain Doubt.", action: "read_power_curse" },
    { text: "[Leave]", action: "nothing" }
  ]}
];

// ── let: G ──
let G = {};

// ── function: initGameState ──
// Global game state

function initGameState() {
  G = {
    screen: "title",
    playerClass: null,
    hp: 70, maxHp: 70, block: 0, energy: 3, maxEnergy: 3, gold: 99,
    act: 1, floor: 1,
    deck: [], hand: [], drawPile: [], discardPile: [], exhaustPile: [],
    relics: [], potions: [], potionSlots: 3,
    buffs: {}, // { strength: 0, dexterity: 0, regeneration: 0, intangible: 0 }
    nextTurnBuffs: {}, // { block: 0, energy: 0, dex: 0 }
    combatActive: false,
    enemies: [], selectedEnemyIdx: -1,
    turnNumber: 0, playerTurn: true,
    mapNodes: [], currentNodeIdx: 0, visitedNodes: new Set(),
    pendingReward: null,
    eventCombatGold: 0,
    tempEnergyBonus: 0, // from Ancient Library event
    firstHpCostAttackUsed: false, // for Sacrificial Dagger
    cursesUnremovable: false, // Cursed Crown effect
    noMoreGold: false, // Ectoplasm effect
    enemiesStartWithStr: false, // Philosopher's Stone effect
    healOnAttack: false, // Black Blood effect
    startTurnIntangible: false, // Shadow Amulet effect
    logMessages: []
  };
}

// ── function: createCardInstance ──
function createCardInstance(cardId) {
  const template = CARD_DB[cardId];
  if (!template) return null;
  return {
    id: cardId,
    name: template.name,
    type: template.type,
    cost: template.cost,
    rarity: template.rarity,
    desc: template.desc,
    effect: Object.assign({}, template.effect || {}),
    exhaust: template.exhaust || false,
    unplayable: template.unplayable || false,
    ethereal: template.ethereal || false,
    endTurnSelfDmg: template.endTurnSelfDmg || 0,
    upgraded: false,
    upgradeCost: template.upgradeCost !== undefined ? template.upgradeCost : null,
    uid: Math.random().toString(36).substr(2, 9)
  };
}

// ── function: createStartingDeck ──
function createStartingDeck(classId) {
  const deck = [];
  for (let i = 0; i < 5; i++) deck.push(createCardInstance("strike"));
  for (let i = 0; i < 4; i++) deck.push(createCardInstance("defend"));
  const sig = CLASS_DATA[classId].signatureCard;
  deck.push(createCardInstance(sig));
  return shuffleArray(deck);
}

// ── function: shuffleArray ──
function shuffleArray(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;
}

// ── function: addLog ──
function addLog(msg) {
  G.logMessages.push(msg);
  if (G.logMessages.length > 50) G.logMessages.shift();
}

// ── function: showScreen ──
function showScreen(screenId) {
  document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
  const el = document.getElementById("screen-" + screenId);
  if (el) el.classList.add('active');
  G.screen = screenId;
}

// ── function: eidIsBoss ──
function eidIsBoss(eid) {
  return ["lich_king","eldritch_treant","archdemon"].includes(eid);
}

// ── function: startPlayerTurn ──
function startPlayerTurn() {
  G.playerTurn = true;
  G.turnNumber++;
  G.block = 0; // Block resets at start of player turn

  // Apply next-turn buffs
  if (G.nextTurnBuffs.block > 0) {
    G.block += G.nextTurnBuffs.block;
    addLog(`Gain ${G.nextTurnBuffs.block} Block from previous effect.`);
  }
  if (G.nextTurnBuffs.dex > 0) {
    G.buffs.dexterity = Math.max(G.buffs.dexterity, G.nextTurnBuffs.dex);
    addLog(`Gain ${G.nextTurnBuffs.dex} Dexterity for this turn.`);
  }

  // Energy: base 3 + bonuses
  let energyBonus = G.tempEnergyBonus;
  if (G.relics.includes("cursed_crown")) energyBonus += 1;
  if (G.relics.includes("ectoplasm")) energyBonus += 1;
  if (G.relics.includes("philosophers_stone")) energyBonus += 1;
  G.energy = G.maxEnergy + energyBonus + (G.nextTurnBuffs.energy || 0);

  // Reset next-turn buffs that were consumed
  G.nextTurnBuffs.block = 0;
  G.nextTurnBuffs.dex = 0;
  G.nextTurnBuffs.energy = 0;

  // Intangible decrements at start of player turn
  if (G.buffs.intangible > 0) {
    G.buffs.intangible--;
  }

  // Shadow Amulet: start each turn with 1 Intangible
  if (G.startTurnIntangible) {
    G.buffs.intangible += 1;
  }

  // Power card effects at start of turn
  const powers = G.deck.filter(c => c.type === "power" && !G.drawPile.includes(c) && !G.hand.includes(c));
  
  // Soul Siphon: gain energy, lose HP
  const soulSiphons = powers.filter(c => c.id === "soul_siphon");
  soulSiphons.forEach(p => {
    G.energy += p.effect.powerStartTurnEnergy || 1;
    let selfDmg = p.effect.powerSelfDmg || 1;
    applyPlayerHpLoss(selfDmg, true);
  });

  // Umbral Mastery: apply Weak to all enemies
  const umbrals = powers.filter(c => c.id === "umbral_mastery");
  umbrals.forEach(p => {
    const weakAmt = p.effect.powerWeakAll || 1;
    G.enemies.forEach(e => { if (e.alive) e.debuffs.weak = (e.debuffs.weak || 0) + weakAmt; });
    addLog(`Umbral Mastery: Applied ${weakAmt} Weak to all enemies.`);
  });

  // Hellfire Aura: deal damage to all enemies at start of turn
  const hellfires = powers.filter(c => c.id === "hellfire_aura");
  hellfires.forEach(p => {
    const dmg = p.effect.powerDmgAllStartTurn || 3;
    G.enemies.forEach(e => {
      if (e.alive) dealDamageToEnemy(e, dmg);
    });
    addLog(`Hellfire Aura: Dealt ${dmg} damage to all enemies.`);
  });

  // Bleeding Edge relic: apply 1 Bleed to random enemy at start of turn
  if (G.relics.includes("bleeding_edge")) {
    const aliveEnemies = G.enemies.filter(e => e.alive);
    if (aliveEnemies.length > 0) {
      const target = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
      target.debuffs.bleed = (target.debuffs.bleed || 0) + 1;
      addLog("Bleeding Edge: Applied 1 Bleed to a random enemy.");
    }
  }

  // Eternal Hourglass: if no cards in hand, gain 1 Energy
  if (G.relics.includes("eternal_hourglass") && G.hand.length === 0) {
    G.energy += 1;
    addLog("Eternal Hourglass: Gain 1 Energy.");
  }

  // Draw 5 cards
  drawCards(5);

  renderCombat();
}

// ── function: drawCards ──
function drawCards(count) {
  for (let i = 0; i < count; i++) {
    if (G.drawPile.length === 0 && G.discardPile.length > 0) {
      // Shuffle discard into draw, excluding exhausted cards
      const toShuffle = G.discardPile.filter(c => !c.ethereal);
      G.discardPile = [];
      G.drawPile = shuffleArray(toShuffle);
    }
    if (G.drawPile.length > 0) {
      const card = G.drawPile.pop();
      
      // Ethereal cards exhaust when drawn
      if (card.ethereal) {
        G.exhaustPile.push(card);
        continue;
      }
      
      G.hand.push(card);
    }
  }
}

// ── function: playCard ──
function playCard(handIdx) {
  if (!G.playerTurn) return;
  
  const card = G.hand[handIdx];
  if (!card || card.unplayable) return;
  if (card.cost > G.energy && card.cost >= 0) return; // Can't afford

  // Pay energy cost
  if (card.cost > 0) {
    G.energy -= card.cost;
  }

  addLog(`Played ${card.name}.`);

  // Execute card effects
  executeCardEffect(card, handIdx);

  // Tome of Dark Rituals: draw 1 when playing Power
  if (G.relics.includes("tome_of_dark_rituals") && card.type === "power") {
    drawCards(1);
    addLog("Tome of Dark Rituals: Drew 1 card.");
  }

  // Black Blood: heal 1 HP when playing Attack
  if (G.relics.includes("black_blood") && card.type === "attack") {
    G.hp = Math.min(G.maxHp, G.hp + 1);
  }

  // Remove from hand
  G.hand.splice(handIdx, 1);

  // Exhaust or discard
  if (card.exhaust) {
    G.exhaustPile.push(card);
  } else {
    G.discardPile.push(card);
  }

  checkCombatEnd();
  renderCombat();
}

// ── function: executeCardEffect ──
function executeCardEffect(card, handIdx) {
  const e = card.effect;
  if (!e) return;

  // Determine target enemy
  let targetEnemy = null;
  if (G.selectedEnemyIdx >= 0 && G.enemies[G.selectedEnemyIdx] && G.enemies[G.selectedEnemyIdx].alive) {
    targetEnemy = G.enemies[G.selectedEnemyIdx];
  } else {
    const aliveEnemies = G.enemies.filter(en => en.alive);
    if (aliveEnemies.length > 0) targetEnemy = aliveEnemies[0];
  }

  // Sacrificial Dagger: first HP-costing attack costs 1 less HP
  let hpCostReduction = 0;
  if (G.relics.includes("sacrificial_dagger") && !G.firstHpCostAttackUsed && card.type === "attack" && e.selfDmg) {
    hpCostReduction = 1;
    G.firstHpCostAttackUsed = true;
  }

  // Self damage (HP cost)
  if (e.selfDmg !== undefined) {
    let dmg = Math.max(0, e.selfDmg - hpCostReduction);
    applyPlayerHpLoss(dmg, true);
  }

  // Block
  if (e.block !== undefined) {
    const dexBonus = G.buffs.dexterity || 0;
    G.block += e.block + dexBonus;
  }

  // Block equal to missing HP (Vital Surge)
  if (e.blockMissingHp) {
    let blockAmt = Math.max(0, G.maxHp - G.hp);
    if (card.upgraded && e.blockMissingHpPlus) blockAmt += e.blockMissingHpPlus;
    const dexBonus = G.buffs.dexterity || 0;
    G.block += blockAmt + dexBonus;
  }

  // Damage to single enemy
  if (e.dmg !== undefined && targetEnemy && targetEnemy.alive) {
    let dmg = e.dmg;
    
    // Nightmare Lash: extra damage if enemy has Weak
    if (card.id === "nightmare_lash" && targetEnemy.debuffs.weak > 0) {
      dmg += e.extraIfWeak || 0;
    }

    dealDamageToEnemy(targetEnemy, dmg);
  }

  // Damage to ALL enemies
  if (e.dmgAll !== undefined) {
    G.enemies.forEach(en => {
      if (en.alive) dealDamageToEnemy(en, e.dmgAll);
    });
  }

  // Multi-random damage (Eternal Darkness)
  if (e.multiRandomDmg !== undefined) {
    const aliveEnemies = G.enemies.filter(en => en.alive);
    for (let i = 0; i < (e.multiCount || 3); i++) {
      if (aliveEnemies.length > 0) {
        const target = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
        dealDamageToEnemy(target, e.multiRandomDmg);
      }
    }
  }

  // Heal
  if (e.heal !== undefined) {
    G.hp = Math.min(G.maxHp, G.hp + e.heal);
  }

  // Draw cards
  if (e.draw !== undefined) {
    drawCards(e.draw);
  }

  // Gain Energy
  if (e.gainEnergy !== undefined) {
    G.energy += e.gainEnergy;
  }

  // Apply Weak to enemy
  if (e.weak !== undefined && targetEnemy && targetEnemy.alive) {
    targetEnemy.debuffs.weak = (targetEnemy.debuffs.weak || 0) + e.weak;
  }

  // Apply Vulnerable to enemy
  if (e.vulnerable !== undefined && targetEnemy && targetEnemy.alive) {
    targetEnemy.debuffs.vulnerable = (targetEnemy.debuffs.vulnerable || 0) + e.vulnerable;
  }

  // Apply Bleed to enemy
  if (e.bleed !== undefined && targetEnemy && targetEnemy.alive) {
    let bleedAmt = e.bleed;
    targetEnemy.debuffs.bleed = (targetEnemy.debuffs.bleed || 0) + bleedAmt;
    
    // Soul Ember relic: deal 2 additional damage when applying Burn
    if (G.relics.includes("soul_ember") && targetEnemy.alive) {
      dealDamageToEnemy(targetEnemy, 2);
    }
  }

  // Apply Burn to enemy
  if (e.burn !== undefined && targetEnemy && targetEnemy.alive) {
    let burnAmt = e.burn;
    targetEnemy.debuffs.burn = (targetEnemy.debuffs.burn || 0) + burnAmt;
    
    // Soul Ember relic: deal 2 additional damage when applying Burn
    if (G.relics.includes("soul_ember") && targetEnemy.alive) {
      dealDamageToEnemy(targetEnemy, 2);
    }
  }

  // Apply Bleed to ALL enemies
  if (e.bleedAll !== undefined) {
    G.enemies.forEach(en => {
      if (en.alive) en.debuffs.bleed = (en.debuffs.bleed || 0) + e.bleedAll;
    });
  }

  // Apply Weak to ALL enemies
  if (e.weakAll !== undefined) {
    G.enemies.forEach(en => {
      if (en.alive) en.debuffs.weak = (en.debuffs.weak || 0) + e.weakAll;
    });
  }

  // Apply Burn to random enemy
  if (e.burnRandom !== undefined) {
    const aliveEnemies = G.enemies.filter(en => en.alive);
    if (aliveEnemies.length > 0) {
      const target = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
      target.debuffs.burn = (target.debuffs.burn || 0) + e.burnRandom;
      
      // Soul Ember relic
      if (G.relics.includes("soul_ember")) {
        dealDamageToEnemy(target, 2);
      }
    }
  }

  // Enemy loses Strength this turn
  if (e.enemyLoseStr !== undefined && targetEnemy && targetEnemy.alive) {
    targetEnemy.strBuff = Math.max(0, (targetEnemy.strBuff || 0) - e.enemyLoseStr);
  }

  // Gain Intangible
  if (e.intangible !== undefined) {
    G.buffs.intangible += e.intangible;
  }

  // Next turn Block
  if (e.nextBlock !== undefined) {
    G.nextTurnBuffs.block = (G.nextTurnBuffs.block || 0) + e.nextBlock;
  }

  // Next turn Dexterity
  if (e.nextDex !== undefined) {
    G.nextTurnBuffs.dex = Math.max(G.nextTurnBuffs.dex || 0, e.nextDex);
  }

  // Next turn Energy
  if (e.nextEnergy !== undefined) {
    G.nextTurnBuffs.energy = (G.nextTurnBuffs.energy || 0) + e.nextEnergy;
  }

  // Temp Strength this turn
  if (e.tempStr !== undefined) {
    G.buffs.strength += e.tempStr;
  }

  // Regeneration
  if (e.regen !== undefined) {
    G.buffs.regeneration = (G.buffs.regeneration || 0) + e.regen;
  }

  // Add Imp Claw to hand
  if (e.addImpClaw || e.addImpClawPlus) {
    const impId = e.addImpClawPlus ? "imp_claw_plus" : "imp_claw";
    G.hand.push(createCardInstance(impId));
  }

  // Shuffle Dazed into discard pile
  if (e.shuffleDazed) {
    const dazed = createCardInstance("dazed");
    G.discardPile.push(dazed);
  }

  // Blood Pact: gain Strength on HP loss from card
  if (G.relics.includes("blood_pact") && e.selfDmg !== undefined) {
    const pactPower = G.deck.find(c => c.id === "blood_pact");
    if (pactPower) {
      const strGain = pactPower.upgraded ? (pactPower.effect.strengthOnHpLoss || 2) : (pactPower.effect.strengthOnHpLoss || 1);
      G.buffs.strength += strGain;
      addLog(`Blood Pact: Gain ${strGain} Strength.`);
    }
  }

  // Bloodstone: heal 1 HP when losing HP from card
  if (G.relics.includes("bloodstone") && e.selfDmg !== undefined) {
    G.hp = Math.min(G.maxHp, G.hp + 1);
    addLog("Bloodstone: Healed 1 HP.");
  }
}

// ── function: dealDamageToEnemy ──
function dealDamageToEnemy(enemy, baseDmg) {
  if (!enemy || !enemy.alive) return;
  
  // Calculate enemy's total strength
  const enemyStr = enemy.strBuff || 0;
  let dmg = baseDmg + enemyStr;

  // Apply Weak debuff (25% less per stack, multiplicative)
  if (enemy.debuffs.weak > 0) {
    for (let i = 0; i < enemy.debuffs.weak; i++) {
      dmg = Math.floor(dmg * 0.75);
    }
  }

  // Apply Vulnerable debuff (50% more per stack, multiplicative)
  if (enemy.debuffs.vulnerable > 0) {
    for (let i = 0; i < enemy.debuffs.vulnerable; i++) {
      dmg = Math.floor(dmg * 1.5);
    }
  }

  // Block absorbs damage
  let blockAbsorbed = Math.min(enemy.block, dmg);
  enemy.block -= blockAbsorbed;
  if (enemy.block < 0) enemy.block = 0;
  
  const actualDmg = dmg - blockAbsorbed;
  enemy.hp -= actualDmg;

  addLog(`${enemy.name} takes ${actualDmg} damage${blockAbsorbed > 0 ? ` (${blockAbsorbed} blocked)` : ''}.`);

  if (enemy.hp <= 0) {
    enemy.hp = 0;
    enemy.alive = false;
    addLog(`${enemy.name} is defeated!`);
  }
}

// ── function: applyPlayerHpLoss ──
function applyPlayerHpLoss(amount, fromCard) {
  // Intangible reduces all HP loss to 1 per stack
  if (G.buffs.intangible > 0) {
    amount = Math.min(amount, G.buffs.intangible);
  }
  
  G.hp -= amount;
  if (amount > 0 && fromCard !== false) addLog(`You lose ${amount} HP.`);

  // Bloodstone: heal 1 when losing HP from card
  if (G.relics.includes("bloodstone") && fromCard) {
    G.hp = Math.min(G.maxHp, G.hp + 1);
  }

  // Blood Pact: gain Strength on HP loss from card
  if (fromCard && G.relics.includes("blood_pact")) {
    const pactPower = G.deck.find(c => c.id === "blood_pact");
    if (pactPower) {
      const strGain = pactPower.upgraded ? (pactPower.effect.strengthOnHpLoss || 2) : (pactPower.effect.strengthOnHpLoss || 1);
      G.buffs.strength += strGain;
      addLog(`Blood Pact: Gain ${strGain} Strength.`);
    }
  }

  if (G.hp <= 0) {
    G.hp = 0;
    endCombat(false);
  }
}

// ── function: dealDamageToPlayer ──
function dealDamageToPlayer(baseDmg, multiHits) {
  // Handle multi-hit attacks separately
  if (multiHits && multiHits.hits) {
    multiHits.hits.forEach(hit => {
      let dmg = hit.dmg + (hit.strBonus || 0);
      
      // Block absorbs damage per hit
      const blockAbsorbed = Math.min(G.block, dmg);
      G.block -= blockAbsorbed;
      if (G.block < 0) G.block = 0;
      
      let actualDmg = dmg - blockAbsorbed;
      
      // Intangible reduces to 1 per stack
      if (G.buffs.intangible > 0) {
        actualDmg = Math.min(actualDmg, G.buffs.intangible);
      }
      
      G.hp -= actualDmg;
      addLog(`Took ${actualDmg} damage${blockAbsorbed > 0 ? ` (${blockAbsorbed} blocked)` : ''}.`);
    });
  } else {
    let dmg = baseDmg;
    
    // Block absorbs damage
    const blockAbsorbed = Math.min(G.block, dmg);
    G.block -= blockAbsorbed;
    if (G.block < 0) G.block = 0;
    
    let actualDmg = dmg - blockAbsorbed;
    
    // Intangible reduces to 1 per stack
    if (G.buffs.intangible > 0) {
      actualDmg = Math.min(actualDmg, G.buffs.intangible);
    }
    
    G.hp -= actualDmg;
    addLog(`Took ${actualDmg} damage${blockAbsorbed > 0 ? ` (${blockAbsorbed} blocked)` : ''}.`);
  }

  if (G.hp <= 0) {
    G.hp = 0;
    endCombat(false);
  }
}

// ── function: endPlayerTurn ──
function endPlayerTurn() {
  if (!G.playerTurn || !G.combatActive) return;
  
  // Process Doubt curses in hand
  G.hand.forEach(card => {
    if (card.endTurnSelfDmg > 0) {
      applyPlayerHpLoss(card.endTurnSelfDmg, false);
    }
  });

  // Discard hand to discard pile
  while (G.hand.length > 0) {
    const card = G.hand.pop();
    if (!card.exhaust) {
      G.discardPile.push(card);
    } else {
      G.exhaustPile.push(card);
    }
  }

  // Regeneration at end of turn
  if (G.buffs.regeneration > 0) {
    const healAmt = Math.min(G.buffs.regeneration, G.maxHp - G.hp);
    G.hp += healAmt;
    G.buffs.regeneration--;
    addLog(`Regenerated ${healAmt} HP.`);
  }

  // Burn on player (if any) at end of turn
  if (G.debuffs && G.debuffs.burn > 0) {
    const burnDmg = G.debuffs.burn;
    applyPlayerHpLoss(burnDmg, false);
    G.debuffs.burn = Math.floor(G.debuffs.burn / 2);
  }

  // Enemy turn(s)
  if (G.combatActive && G.hp > 0) {
    processEnemyTurn();
  }
}

// ── function: processEnemyTurn ──
function processEnemyTurn() {
  const aliveEnemies = G.enemies.filter(e => e.alive);
  
  aliveEnemies.forEach(enemy => {
    // Process enemy debuffs at start of their turn
    if (enemy.debuffs.bleed > 0) {
      const bleedDmg = enemy.debuffs.bleed;
      enemy.hp -= bleedDmg;
      addLog(`${enemy.name} takes ${bleedDmg} Bleed damage.`);
      enemy.debuffs.bleed = Math.floor(enemy.debuffs.bleed / 2);
      if (enemy.hp <= 0) {
        enemy.hp = 0;
        enemy.alive = false;
        addLog(`${enemy.name} is defeated by Bleed!`);
      }
    }

    // Process Burn at end of enemy turn (we do it here since enemies act after player)
    if (enemy.debuffs.burn > 0) {
      const burnDmg = enemy.debuffs.burn;
      enemy.hp -= burnDmg;
      addLog(`${enemy.name} takes ${burnDmg} Burn damage.`);
      enemy.debuffs.burn = Math.floor(enemy.debuffs.burn / 2);
      if (enemy.hp <= 0) {
        enemy.hp = 0;
        enemy.alive = false;
        addLog(`${enemy.name} is defeated by Burn!`);
      }
    }

    // Poison at start of turn
    if (enemy.debuffs.poison > 0) {
      const poisonDmg = enemy.debuffs.poison;
      enemy.hp -= poisonDmg;
      addLog(`${enemy.name} takes ${poisonDmg} Poison damage.`);
      enemy.debuffs.poison--;
      if (enemy.hp <= 0) {
        enemy.hp = 0;
        enemy.alive = false;
        addLog(`${enemy.name} is defeated by Poison!`);
      }
    }

    // Check boss phase transitions
    if (enemy.isBoss && !enemy.phase2) {
      const bossDef = getBossDefinition();
      if (bossDef && enemy.hp <= bossDef.phase2Threshold) {
        enemy.phase2 = true;
        addLog(`${enemy.name} enrages!`);
      }
    }

    // Get current intent
    let intents = [];
    if (enemy.isBoss) {
      const bossDef = getBossDefinition();
      intents = enemy.phase2 ? bossDef.phase2Intents : bossDef.phase1Intents;
    } else {
      intents = enemy.intents;
    }

    const intentIdx = enemy.intentIdx % intents.length;
    const intent = intents[intentIdx];
    enemy.intentIdx++;

    // Execute intent
    executeEnemyIntent(enemy, intent);
  });

  if (G.combatActive && G.hp > 0) {
    startPlayerTurn();
  }
}

// ── function: getBossDefinition ──
function getBossDefinition() {
  const actKey = "act" + G.act;
  return BOSS_DB[actKey];
}

// ── function: executeEnemyIntent ──
function executeEnemyIntent(enemy, intent) {
  if (!enemy.alive) return;

  switch (intent.type) {
    case "attack": {
      let dmg = intent.dmg + (enemy.strBuff || 0);
      dealDamageToPlayer(dmg);
      break;
    }
    case "multiHit": {
      const hits = intent.hits.map(h => ({ ...h, strBonus: enemy.strBuff || 0 }));
      dealDamageToPlayer(0, { hits });
      break;
    }
    case "defend": {
      enemy.block += intent.block;
      addLog(`${enemy.name} gains ${intent.block} Block.`);
      break;
    }
    case "attackDefend": {
      let dmg = intent.dmg + (enemy.strBuff || 0);
      dealDamageToPlayer(dmg);
      enemy.block += intent.block;
      addLog(`${enemy.name} gains ${intent.block} Block.`);
      break;
    }
    case "debuff": {
      if (intent.weak) {
        G.debuffs = G.debuffs || {};
        G.debuffs.weak = (G.debuffs.weak || 0) + intent.weak;
        addLog(`You gain ${intent.weak} Weak.`);
      }
      if (intent.bleed) {
        G.debuffs = G.debuffs || {};
        G.debuffs.bleed = (G.debuffs.bleed || 0) + intent.bleed;
        addLog(`You gain ${intent.bleed} Bleed.`);
      }
      if (intent.poison) {
        G.debuffs = G.debuffs || {};
        G.debuffs.poison = (G.debuffs.poison || 0) + intent.poison;
        addLog(`You gain ${intent.poison} Poison.`);
      }
      break;
    }
    case "debuffAll": {
      if (intent.vulnerable) {
        G.debuffs = G.debuffs || {};
        G.debuffs.vulnerable = (G.debuffs.vulnerable || 0) + intent.vulnerable;
        addLog(`You gain ${intent.vulnerable} Vulnerable.`);
      }
      if (intent.weak) {
        G.debuffs = G.debuffs || {};
        G.debuffs.weak = (G.debuffs.weak || 0) + intent.weak;
        addLog(`You gain ${intent.weak} Weak.`);
      }
      break;
    }
    case "attackDebuff": {
      let dmg = intent.dmg + (enemy.strBuff || 0);
      dealDamageToPlayer(dmg);
      if (intent.weak) { G.debuffs = G.debuffs || {}; G.debuffs.weak = (G.debuffs.weak || 0) + intent.weak; }
      if (intent.burn) { G.debuffs = G.debuffs || {}; G.debuffs.burn = (G.debuffs.burn || 0) + intent.burn; addLog(`You gain ${intent.burn} Burn.`); }
      break;
    }
    case "selfBuff": {
      if (intent.block) enemy.block += intent.block;
      if (intent.regen) enemy.strBuff = (enemy.strBuff || 0); // regen handled separately
      if (intent.intangible) addLog(`${enemy.name} gains Intangible.`);
      break;
    }
    case "selfVulnDefend": {
      enemy.block += intent.block;
      enemy.debuffs.vulnerable = (enemy.debuffs.vulnerable || 0) + 1;
      addLog(`${enemy.name} gains ${intent.block} Block and applies Vulnerable to itself.`);
      break;
    }
    case "defendBuff": {
      enemy.block += intent.block;
      if (intent.str) enemy.strBuff = (enemy.strBuff || 0) + intent.str;
      if (intent.regen) addLog(`${enemy.name} gains Regeneration.`);
      break;
    }
    case "buffAll": {
      G.enemies.forEach(e => { if (e.alive && e !== enemy) e.strBuff = (e.strBuff || 0) + intent.str; });
      enemy.strBuff = (enemy.strBuff || 0) + intent.str;
      addLog(`${enemy.name} grants ${intent.str} Strength to all enemies.`);
      break;
    }
    case "drainLife": {
      let dmg = intent.dmg + (enemy.strBuff || 0);
      dealDamageToPlayer(dmg);
      enemy.hp = Math.min(enemy.maxHp, enemy.hp + intent.heal);
      addLog(`${enemy.name} heals ${intent.heal} HP.`);
      break;
    }
    case "summon": {
      // Check max minions for boss summons
      if (enemy.isBoss) {
        const minionCount = G.enemies.filter(e => !e.isBoss).length;
        if (minionCount >= 2) {
          addLog(`${enemy.name} cannot summon more minions.`);
          return;
        }
      }
      // Check for duplicate imp summons
      if (intent.summonId === "infernal_imp") {
        const existingImps = G.enemies.filter(e => e.id === "infernal_imp").length;
        if (existingImps >= 2) return;
      }
      
      const summonTemplate = ENEMY_DB[intent.summonId];
      if (summonTemplate) {
        const newEnemy = {
          id: intent.summonId, name: summonTemplate.name, hp: summonTemplate.hp, maxHp: summonTemplate.maxHp,
          block: 0, intentIdx: 0, intents: [...summonTemplate.intents], alive: true,
          buffs: {}, debuffs: {}, strBuff: 0, isBoss: false, phase2: false,
          idx: G.enemies.length
        };
        G.enemies.push(newEnemy);
        addLog(`${enemy.name} summons ${summonTemplate.name}!`);
      }
      break;
    }
    case "defendHeal": {
      enemy.block += intent.block;
      enemy.hp = Math.min(enemy.maxHp, enemy.hp + intent.heal);
      addLog(`${enemy.name} gains ${intent.block} Block and heals ${intent.heal} HP.`);
      break;
    }
    case "attackStrGain": {
      if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
      let dmg = intent.dmg + (enemy.strBuff || 0);
      dealDamageToPlayer(dmg);
      break;
    }
    case "multiHitStrGain": {
      if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
      const hits = intent.hits.map(h => ({ ...h, strBonus: enemy.strBuff || 0 }));
      dealDamageToPlayer(0, { hits });
      break;
    }
    case "defendStrGain": {
      if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
      enemy.block += intent.block;
      addLog(`${enemy.name} gains ${intent.block} Block.`);
      break;
    }
    case "attackDebuffStrGain": {
      if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
      let dmg = intent.dmg + (enemy.strBuff || 0);
      dealDamageToPlayer(dmg);
      if (intent.vulnerable) { G.debuffs = G.debuffs || {}; G.debuffs.vulnerable = (G.debuffs.vulnerable || 0) + intent.vulnerable; }
      break;
    }
    case "defendHealStrGain": {
      if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
      enemy.block += intent.block;
      enemy.hp = Math.min(enemy.maxHp, enemy.hp + intent.heal);
      addLog(`${enemy.name} gains ${intent.block} Block and heals ${intent.heal} HP.`);
      break;
    }
  }
}

// ── function: checkCombatEnd ──
function checkCombatEnd() {
  const aliveEnemies = G.enemies.filter(e => e.alive);
  if (aliveEnemies.length === 0) {
    endCombat(true);
  }
}

// ── function: generateCombatRewards ──
function generateCombatRewards(isElite) {
  // Gold reward
  let goldAmt;
  if (isElite) {
    goldAmt = 25 + Math.floor(Math.random() * 11); // 25-35
  } else {
    goldAmt = 10 + Math.floor(Math.random() * 11); // 10-20
  }
  
  if (G.relics.includes("greedy_imp")) goldAmt += 10;
  if (!G.noMoreGold) G.gold += goldAmt;

  // Event combat bonus gold
  if (G.eventCombatGold > 0) {
    G.gold += G.eventCombatGold;
    G.eventCombatGold = 0;
  }

  // Card reward
  const cards = generateCardRewards(isElite, false);
  
  // Potion chance
  let potionChance = isElite ? 0.7 : 0.4;
  if (Math.random() < potionChance && G.potions.length < G.potionSlots) {
    const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
    G.potions.push(potion.id);
  }

  // Show reward screen
  showRewardScreen(cards, goldAmt);
}

// ── function: generateBossRewards ──
function generateBossRewards() {
  let goldAmt = 50;
  if (G.relics.includes("greedy_imp")) goldAmt += 10;
  if (!G.noMoreGold) G.gold += goldAmt;

  // Boss card rewards - always include one rare
  const cards = generateCardRewards(false, true);

  // Always drops 2 potions
  for (let i = 0; i < 2 && G.potions.length < G.potionSlots; i++) {
    const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
    if (!G.potions.includes(potion.id)) {
      G.potions.push(potion.id);
    } else {
      // Allow duplicates of potions
      G.potions.push(potion.id);
    }
  }

  // Boss relic choice (3 from boss pool)
  const availableBossRelics = BOSS_RELICS.filter(r => !G.relics.includes(r));
  const choices = shuffleArray(availableBossRelics).slice(0, 3);
  
  showRewardScreen(cards, goldAmt, choices);
}

// ── function: generateCardRewards ──
function generateCardRewards(isElite, isBoss) {
  const classPool = CLASS_DATA[G.playerClass].cardPool;
  const cards = [];
  
  for (let i = 0; i < 3; i++) {
    let cardId;
    
    if (isBoss && i === 0) {
      // Boss rewards always include one rare
      const rares = classPool.filter(cid => CARD_DB[cid].rarity === "rare");
      cardId = rares[Math.floor(Math.random() * rares.length)];
    } else {
      // Weighted random selection
      let rarity;
      const roll = Math.random();
      if (isElite) {
        if (roll < 0.6) rarity = "common";
        else if (roll < 0.8) rarity = "uncommon";
        else rarity = "rare";
      } else {
        if (roll < 0.6) rarity = "common";
        else if (roll < 0.9) rarity = "uncommon";
        else rarity = "rare";
      }
      
      const poolByRarity = classPool.filter(cid => CARD_DB[cid].rarity === rarity);
      if (poolByRarity.length > 0) {
        cardId = poolByRarity[Math.floor(Math.random() * poolByRarity.length)];
      } else {
        cardId = classPool[Math.floor(Math.random() * classPool.length)];
      }
    }
    
    cards.push(createCardInstance(cardId));
  }
  
  return cards;
}

// ── function: showRewardScreen ──
function showRewardScreen(cards, goldAmt, bossRelicChoices) {
  G.pendingReward = { cards, goldAmt, bossRelicChoices };
  
  if (bossRelicChoices && bossRelicChoices.length > 0) {
    // Show relic choice first
    renderRelicChoice(bossRelicChoices);
    showScreen("relic-choice");
  } else {
    renderRewardScreen(cards, goldAmt);
    showScreen("reward");
  }
}

// ── function: renderRewardScreen ──
function renderRewardScreen(cards, goldAmt) {
  const container = document.getElementById("reward-cards");
  container.innerHTML = "";
  
  document.getElementById("reward-gold").textContent = `+${goldAmt} Gold`;
  
  cards.forEach(card => {
    const el = createCardElement(card);
    el.addEventListener('click', () => {
      G.deck.push(card);
      advanceToMap();
    });
    container.appendChild(el);
  });
}

// ── function: renderRelicChoice ──
function renderRelicChoice(choices) {
  const container = document.getElementById("relic-choices");
  container.innerHTML = "";
  
  choices.forEach(relicId => {
    const relic = BOSS_RELIC_DB[relicId];
    if (!relic) return;
    
    const el = document.createElement("div");
    el.className = "relic-choice";
    el.innerHTML = `<h3>${relic.name}</h3><p style="font-size:0.8em;color:#9a8f80;margin-top:5px;">${relic.desc}</p>`;
    el.addEventListener('click', () => {
      G.relics.push(relicId);
      
      // Apply immediate effects
      if (relicId === "ectoplasm") {
        G.gold = 0;
        G.noMoreGold = true;
      }
      if (relicId === "shadow_amulet") {
        G.maxHp -= 15;
        G.hp = Math.min(G.hp, G.maxHp);
        G.startTurnIntangible = true;
      }
      
      // After relic choice, show card rewards
      renderRewardScreen(G.pendingReward.cards, G.pendingReward.goldAmt);
      showScreen("reward");
    });
    container.appendChild(el);
  });
}

// ── function: advanceToMap ──
function advanceToMap() {
  G.floor++;
  
  if (G.floor > 15) {
    // Act complete - handled in endCombat for bosses
    return;
  }
  
  renderMap();
  showScreen("map");
}

// ── function: createCardElement ──
// ===== UI RENDERING =====

function createCardElement(card) {
  const el = document.createElement("div");
  let rarityClass = `rarity-${card.rarity}`;
  if (card.rarity === "basic") rarityClass = "rarity-basic";
  
  el.className = `card type-${card.type} ${rarityClass}${card.upgraded ? ' upgraded' : ''}`;
  
  const costDisplay = card.cost < 0 ? "" : card.cost === 0 ? `<div class="card-cost free">0</div>` : `<div class="card-cost">${card.cost}</div>`;
  
  el.innerHTML = `
    ${costDisplay}
    <div class="card-type-badge"></div>
    <div class="card-name">${card.name}</div>
    <div class="card-desc">${card.desc}</div>
    <div class="card-rarity">${card.rarity.toUpperCase()}</div>
  `;
  
  return el;
}

// ── function: renderHand ──
function renderHand() {
  const handZone = document.getElementById("hand-zone");
  handZone.innerHTML = "";
  
  G.hand.forEach((card, idx) => {
    const el = createCardElement(card);
    
    if (card.unplayable || card.cost > G.energy) {
      el.classList.add("unplayable");
    } else {
      el.addEventListener('click', () => playCard(idx));
    }
    
    handZone.appendChild(el);
  });
}

// ── function: renderPotionBar ──
function renderPotionBar() {
  const bar = document.getElementById("potion-bar");
  if (!bar) return;
  bar.innerHTML = "";
  
  G.potions.forEach((potionId, idx) => {
    const potionDef = POTION_DB.find(p => p.id === potionId);
    if (!potionDef) return;
    
    const btn = document.createElement("button");
    btn.className = "potion-btn";
    btn.textContent = potionDef.icon;
    btn.title = `${potionDef.name}: ${potionDef.desc}`;
    btn.addEventListener('click', () => usePotion(idx));
    bar.appendChild(btn);
  });
}

// ── function: usePotion ──
function usePotion(potionIdx) {
  if (!G.playerTurn || !G.combatActive) return;
  
  const potionId = G.potions[potionIdx];
  const potionDef = POTION_DB.find(p => p.id === potionId);
  if (!potionDef) return;
  
  // Some potions need a target
  if (["bleeding_flask"].includes(potionId)) {
    showPotionTargetScreen(potionIdx, potionId);
    return;
  }
  
  executePotionEffect(potionId);
  G.potions.splice(potionIdx, 1);
  renderCombat();
}

// ── function: executePotionEffect ──
function executePotionEffect(potionId) {
  switch (potionId) {
    case "blood_vial":
      G.hp = Math.min(G.maxHp, G.hp + 15);
      addLog("Used Blood Vial: Healed 15 HP.");
      break;
    case "firebomb":
      G.enemies.forEach(e => { if (e.alive) dealDamageToEnemy(e, 10); });
      addLog("Used Firebomb: Dealt 10 damage to all enemies.");
      checkCombatEnd();
      break;
    case "dex_tonic":
      G.buffs.dexterity = Math.max(G.buffs.dexterity || 0, 3);
      addLog("Used Dex Tonic: Gain 3 Dexterity for this turn.");
      break;
    case "energy_elixir":
      G.energy += 2;
      addLog("Used Energy Elixir: Gain 2 Energy.");
      break;
    case "cursed_brew":
      G.enemies.forEach(e => { if (e.alive) e.debuffs.vulnerable = (e.debuffs.vulnerable || 0) + 3; });
      addLog("Used Cursed Brew: Applied 3 Vulnerable to all enemies.");
      break;
    case "imp_in_bottle":
      G.hand.push(createCardInstance("imp_claw_plus"));
      addLog("Used Imp-in-a-Bottle: Added Imp Claw+ to hand.");
      break;
    case "smoke_bomb": {
      const isBossFight = G.enemies.some(e => e.isBoss);
      if (isBossFight) {
        addLog("Cannot use Smoke Bomb on a boss!");
        return;
      }
      addLog("Used Smoke Bomb: Escaped combat.");
      endCombat(false); // No rewards, but don't die
      G.combatActive = false;
      advanceToMap();
      break;
    }
  }
}

// ── function: showPotionTargetScreen ──
function showPotionTargetScreen(potionIdx, potionId) {
  const screen = document.getElementById("screen-potion-use");
  const targetsEl = document.getElementById("potion-targets");
  targetsEl.innerHTML = "";
  
  G.enemies.forEach((enemy, idx) => {
    if (!enemy.alive) return;
    
    const el = document.createElement("div");
    el.className = "enemy-card";
    el.innerHTML = `<div class="enemy-name">${enemy.name}</div><div>${enemy.hp}/${enemy.maxHp} HP</div>`;
    el.addEventListener('click', () => {
      if (potionId === "bleeding_flask") {
        enemy.debuffs.bleed = (enemy.debuffs.bleed || 0) + 6;
        addLog("Used Bleeding Flask: Applied 6 Bleed.");
      }
      G.potions.splice(potionIdx, 1);
      screen.style.display = "none";
      renderCombat();
    });
    targetsEl.appendChild(el);
  });
  
  screen.style.display = "flex";
}

// ── function: renderMap ──
// Fix renderMap to draw connections properly between nodes
function renderMap() {
  const canvas = document.getElementById("map-canvas");
  const ctx = canvas.getContext("2d");
  
  // Clear
  ctx.fillStyle = "#0a0a0f";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  // Draw subtle grid
  ctx.strokeStyle = "rgba(58,45,74,0.15)";
  ctx.lineWidth = 1;
  for (let x = 0; x < canvas.width; x += 60) {
    ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
  }
  for (let y = 0; y < canvas.height; y += 40) {
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
  }
  
  const nodePositions = getNodePositions();
  
  // Draw connections between consecutive floors
  ctx.strokeStyle = "rgba(58,45,74,0.5)";
  ctx.lineWidth = 2;
  for (let i = 0; i < G.mapNodes.length - 1; i++) {
    const fromNode = G.mapNodes[i];
    // Find nodes at next floor
    for (let j = i + 1; j < G.mapNodes.length; j++) {
      const toNode = G.mapNodes[j];
      if (toNode.floor === fromNode.floor + 1) {
        const fromPos = nodePositions[i];
        const toPos = nodePositions[j];
        if (fromPos && toPos) {
          ctx.beginPath();
          ctx.moveTo(fromPos.x, fromPos.y);
          ctx.lineTo(toPos.x, toPos.y);
          ctx.stroke();
        }
      }
    }
  }
  
  // Draw nodes
  G.mapNodes.forEach((node, idx) => {
    const pos = nodePositions[idx];
    if (!pos) return;
    
    const visited = G.visitedNodes.has(idx);
    const current = idx === G.currentNodeIdx;
    const reachable = isNodeReachable(idx);
    
    // Node color based on type
    let fillColor, strokeColor;
    switch (node.type) {
      case "enemy": fillColor = visited ? "#2d4a2d" : "#3d1c1c"; strokeColor = visited ? "#5a8a5a" : "#5a2d2d"; break;
      case "elite": fillColor = visited ? "#4a2d4a" : "#5a2d5a"; strokeColor = visited ? "#9b59b6" : "#7a3d7a"; break;
      case "boss": fillColor = "#8b0000"; strokeColor = "#ff4444"; break;
      case "rest": fillColor = visited ? "#2d4a4a" : "#1c3d3d"; strokeColor = visited ? "#5a8a8a" : "#2d5a5a"; break;
      case "shop": fillColor = visited ? "#4a4a2d" : "#3d3d1c"; strokeColor = visited ? "#8a8a5a" : "#5a5a2d"; break;
      case "event": fillColor = visited ? "#2d2d4a" : "#1c1c3d"; strokeColor = visited ? "#5a5a8a" : "#2d2d5a"; break;
      default: fillColor = "#1a1520"; strokeColor = "#3a2d4a";
    }
    
    if (!reachable && !visited) {
      fillColor = "rgba(26,21,32,0.5)";
      strokeColor = "rgba(58,45,74,0.3)";
    }
    
    // Draw node circle
    ctx.beginPath();
    ctx.arc(pos.x, pos.y, 20, 0, Math.PI * 2);
    ctx.fillStyle = fillColor;
    ctx.fill();
    ctx.strokeStyle = strokeColor;
    ctx.lineWidth = current ? 3 : 2;
    ctx.stroke();
    
    // Glow for reachable nodes
    if (reachable && !visited) {
      ctx.beginPath();
      ctx.arc(pos.x, pos.y, 25, 0, Math.PI * 2);
      ctx.strokeStyle = "rgba(196,30,58,0.4)";
      ctx.lineWidth = 2;
      ctx.stroke();
    }
    
    // Node icon
    ctx.fillStyle = reachable || visited ? "#d4c8b0" : "#4a3d5a";
    ctx.font = "16px Georgia";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    
    let icon;
    switch (node.type) {
      case "enemy": icon = node.enemies && node.enemies.length > 1 ? "⚔⚔" : "⚔"; break;
      case "elite": icon = "💀"; break;
      case "boss": icon = "👑"; break;
      case "rest": icon = "🏕"; break;
      case "shop": icon = "🛒"; break;
      case "event": icon = "❓"; break;
      default: icon = "?";
    }
    
    ctx.fillText(icon, pos.x, pos.y);
    
    // Floor number below node
    ctx.fillStyle = "#7a6f60";
    ctx.font = "10px Georgia";
    ctx.textBaseline = "top";
    ctx.fillText(`F${node.floor}`, pos.x, pos.y + 24);
  });
  
  // Update header
  const actNames = ["", "The Crypts", "Blighted Woods", "Abyssal Spire"];
  document.getElementById("map-act-name").textContent = `Act ${G.act}: ${actNames[G.act]}`;
  document.getElementById("map-floor-num").textContent = `Floor ${G.floor}/15`;
  
  // Click handler for nodes
  canvas.onclick = (e) => {
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;
    const mx = (e.clientX - rect.left) * scaleX;
    const my = (e.clientY - rect.top) * scaleY;
    
    for (let idx = 0; idx < G.mapNodes.length; idx++) {
      if (!isNodeReachable(idx)) continue;
      
      const pos = nodePositions[idx];
      if (!pos) continue;
      
      const dist = Math.sqrt((mx - pos.x) ** 2 + (my - pos.y) ** 2);
      if (dist < 25) {
        enterNode(idx);
        return;
      }
    }
  };
}

// ── function: getNodePositions ──
function getNodePositions() {
  const positions = [];
  const cols = [100, 300, 500, 700];
  
  G.mapNodes.forEach((node) => {
    // Y position: floor 2 at bottom (y=540), floor 15 at top (y=60)
    const y = 560 - ((node.floor - 1) * 38);
    positions.push({ x: cols[node.col], y });
  });
  
  return positions;
}

// ── function: isNodeReachable ──
function isNodeReachable(idx) {
  const node = G.mapNodes[idx];
  if (!node) return false;
  
  // If no nodes visited yet (start of game), show first floor options
  if (G.visitedNodes.size === 0 && G.currentNodeIdx === -1) {
    return node.floor === 2;
  }
  
  // Node must be at next floor after current
  const currentNode = G.mapNodes[G.currentNodeIdx];
  if (!currentNode) return false;
  
  if (node.floor !== currentNode.floor + 1) return false;
  
  // Column adjacency: can move to adjacent columns
  const colDiff = Math.abs(node.col - currentNode.col);
  return colDiff <= 2;
}

// ── function: getRandomEnemyForAct ──
function getRandomEnemyForAct(act) {
  const pool = ACT_ENEMY_POOLS["act" + act];
  return pool[Math.floor(Math.random() * pool.length)];
}

// ── function: getRandomEliteForAct ──
function getRandomEliteForAct(act) {
  const pool = ACT_ELITE_POOLS["act" + act];
  return pool[Math.floor(Math.random() * pool.length)];
}

// ── function: showEventScreen ──
function showEventScreen() {
  const event = EVENTS[Math.floor(Math.random() * EVENTS.length)];
  
  document.getElementById("event-title").textContent = event.title;
  document.getElementById("event-text").textContent = event.text;
  
  const choicesEl = document.getElementById("event-choices");
  choicesEl.innerHTML = "";
  
  event.choices.forEach(choice => {
    const btn = document.createElement("button");
    btn.className = "event-choice-btn";
    btn.textContent = choice.text;
    btn.addEventListener('click', () => handleEventChoice(choice.action));
    choicesEl.appendChild(btn);
  });
  
  showScreen("event");
}

// ── function: handleEventChoice ──
function handleEventChoice(action) {
  switch (action) {
    case "offer_hp_relic":
      if (G.hp > 15) {
        G.hp -= 15;
        const rareRelics = Object.keys(RELIC_DB).filter(r => RELIC_DB[r].rarity === "rare" && !G.relics.includes(r));
        if (rareRelics.length > 0) {
          G.relics.push(rareRelics[Math.floor(Math.random() * rareRelics.length)]);
        } else {
          G.gold += 50; // consolation
        }
      }
      break;
    case "pray_heal_curse":
      G.hp = Math.min(G.maxHp, G.hp + 10);
      G.deck.push(createCardInstance("doubt"));
      break;
    case "trade_card":
      showRemoveCardScreen(() => {
        G.gold += 100;
        advanceToMap();
      });
      return;
    case "buy_wares":
      if (G.gold >= 50) {
        G.gold -= 50;
        const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
        if (G.potions.length < G.potionSlots) G.potions.push(potion.id);
        const commonRelics = Object.keys(RELIC_DB).filter(r => RELIC_DB[r].rarity === "common" && !G.relics.includes(r));
        if (commonRelics.length > 0) {
          G.relics.push(commonRelics[Math.floor(Math.random() * commonRelics.length)]);
        }
      }
      break;
    case "attack_peddler":
      G.eventCombatGold = 75;
      startCombat(["shadow_thief"]);
      return;
    case "drink_cures":
      if (G.hp > 10) {
        G.hp -= 10;
        G.deck = G.deck.filter(c => c.type !== "curse" || !G.cursesUnremovable);
      }
      break;
    case "bottle_potion":
      if (G.potions.length < G.potionSlots) G.potions.push("cursed_brew");
      break;
    case "upgrade_random":
      if (G.hp > 20) {
        G.hp -= 20;
        const upgradable = G.deck.filter(c => !c.upgraded && c.type !== "curse" && c.type !== "status");
        for (let i = 0; i < Math.min(2, upgradable.length); i++) {
          upgradeCard(upgradable[i]);
        }
      }
      break;
    case "bet_gold":
      if (G.gold >= 30) {
        G.gold -= 30;
        if (Math.random() < 0.5) {
          G.gold += 60;
        }
      }
      break;
    case "wager_card": {
      const deckCopy = [...G.deck];
      if (deckCopy.length > 10) { // Don't remove from very small decks
        const idx = Math.floor(Math.random() * G.deck.length);
        const removed = G.deck.splice(idx, 1)[0];
        if (removed.type === "curse") {
          G.gold += 100;
        }
      }
      break;
    }
    case "study_energy":
      if (G.hp > 5) {
        G.hp -= 5;
        G.tempEnergyBonus++;
      }
      break;
    case "read_power_curse": {
      const classPool = CLASS_DATA[G.playerClass].cardPool;
      const powers = classPool.filter(cid => CARD_DB[cid].type === "power");
      if (powers.length > 0) {
        G.deck.push(createCardInstance(powers[Math.floor(Math.random() * powers.length)]));
      }
      G.deck.push(createCardInstance("doubt"));
      break;
    }
    case "nothing":
      // Nothing happens
      break;
  }
  
  advanceToMap();
}

// ── function: showRestScreen ──
function showRestScreen() {
  document.getElementById("rest-hp-text").textContent = `HP: ${G.hp}/${G.maxHp}`;
  
  const choicesEl = document.getElementById("rest-choices");
  choicesEl.innerHTML = "";
  
  // Rest option
  const restBtn = document.createElement("button");
  restBtn.className = "rest-btn";
  restBtn.textContent = `🏕 Rest (Heal ${Math.floor(G.maxHp * 0.25)} HP)`;
  restBtn.addEventListener('click', () => {
    G.hp = Math.min(G.maxHp, G.hp + Math.floor(G.maxHp * 0.25));
    advanceToMap();
  });
  choicesEl.appendChild(restBtn);
  
  // Forge option
  const forgeBtn = document.createElement("button");
  forgeBtn.className = "rest-btn";
  forgeBtn.textContent = "⚒ Forge (Upgrade a card)";
  forgeBtn.addEventListener('click', () => showUpgradeScreen());
  choicesEl.appendChild(forgeBtn);
  
  // Meditate option
  const meditateBtn = document.createElement("button");
  meditateBtn.className = "rest-btn";
  meditateBtn.textContent = "🧘 Meditate (Remove a card)";
  meditateBtn.addEventListener('click', () => showRemoveCardScreen(() => advanceToMap()));
  choicesEl.appendChild(meditateBtn);
  
  showScreen("rest");
}

// ── function: showUpgradeScreen ──
function showUpgradeScreen() {
  const upgradable = G.deck.filter(c => !c.upgraded && c.type !== "curse" && c.type !== "status");
  
  if (upgradable.length === 0) {
    alert("No cards to upgrade!");
    advanceToMap();
    return;
  }
  
  const container = document.getElementById("upgrade-cards");
  container.innerHTML = "";
  
  upgradable.forEach(card => {
    const el = createCardElement(card);
    el.addEventListener('click', () => {
      upgradeCard(card);
      advanceToMap();
    });
    container.appendChild(el);
  });
  
  showScreen("upgrade-card");
}

// ── function: upgradeCard ──
function upgradeCard(card) {
  const template = CARD_DB[card.id];
  if (!template || !template.upgrade) return;
  
  card.upgraded = true;
  Object.assign(card.effect, template.upgrade);
  
  // Update description to reflect upgraded values
  let newDesc = template.desc;
  if (template.upgrade.dmg !== undefined) {
    newDesc = newDesc.replace(/Deal \d+ damage/, `Deal ${template.upgrade.dmg} damage`);
  }
  if (template.upgrade.block !== undefined) {
    newDesc = newDesc.replace(/Gain \d+ Block/, `Gain ${template.upgrade.block} Block`);
  }
  card.desc = newDesc;
  
  // Handle cost reduction for Soul Siphon
  if (card.id === "soul_siphon" && template.upgradeCost !== undefined) {
    card.cost = template.upgradeCost;
  }
  
  addLog(`Upgraded ${card.name} to ${card.name}+`);
}

// ── function: showRemoveCardScreen ──
function showRemoveCardScreen(callback) {
  const removable = G.deck.filter(c => c.type !== "curse" || !G.cursesUnremovable);
  
  if (removable.length === 0) {
    callback();
    return;
  }
  
  document.getElementById("remove-title").textContent = "Remove a Card";
  document.getElementById("remove-desc").textContent = "Choose a card to remove from your deck.";
  
  const container = document.getElementById("remove-cards");
  container.innerHTML = "";
  
  removable.forEach(card => {
    const el = createCardElement(card);
    el.addEventListener('click', () => {
      const idx = G.deck.indexOf(card);
      if (idx >= 0) G.deck.splice(idx, 1);
      callback();
    });
    container.appendChild(el);
  });
  
  showScreen("remove-card");
}

// ── function: showShopScreen ──
function showShopScreen() {
  document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
  
  const itemsEl = document.getElementById("shop-items");
  itemsEl.innerHTML = "";
  
  // Generate shop cards (3 class cards + 1 colorless)
  const classPool = CLASS_DATA[G.playerClass].cardPool;
  const shopCards = [];
  
  for (let i = 0; i < 3; i++) {
    const cardId = classPool[Math.floor(Math.random() * classPool.length)];
    shopCards.push({ type: "card", data: createCardInstance(cardId) });
  }
  
  // Colorless card
  const colorlessId = COLORLESS_CARDS[Math.floor(Math.random() * COLORLESS_CARDS.length)];
  shopCards.push({ type: "card", data: createCardInstance(colorlessId), colorless: true });
  
  // Relics (2)
  const availableRelics = Object.keys(RELIC_DB).filter(r => !G.relics.includes(r));
  for (let i = 0; i < 2 && availableRelics.length > 0; i++) {
    const idx = Math.floor(Math.random() * availableRelics.length);
    shopCards.push({ type: "relic", data: availableRelics[idx] });
    availableRelics.splice(idx, 1);
  }
  
  // Potions (3)
  for (let i = 0; i < 3; i++) {
    const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
    shopCards.push({ type: "potion", data: potion });
  }
  
  // Card removal service
  shopCards.push({ type: "remove" });
  
  shopCards.forEach(item => {
    const el = document.createElement("div");
    
    if (item.type === "card") {
      const cardEl = createCardElement(item.data);
      const price = item.data.rarity === "rare" ? 150 : item.data.rarity === "uncommon" ? 75 : 50;
      
      el.className = "shop-item";
      if (item.colorless) el.style.borderColor = "#6baed6";
      
      const priceEl = document.createElement("div");
      priceEl.className = "item-price";
      priceEl.textContent = `${price}g`;
      
      el.appendChild(cardEl);
      el.appendChild(priceEl);
      
      if (G.gold >= price) {
        el.addEventListener('click', () => {
          G.gold -= price;
          G.deck.push(item.data);
          document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
          renderShopAfterPurchase();
        });
      } else {
        el.style.opacity = "0.4";
        el.style.cursor = "not-allowed";
      }
    } else if (item.type === "relic") {
      const relicDef = RELIC_DB[item.data];
      el.className = "shop-item relic-shop";
      el.innerHTML = `<div style="font-size:1.5em;">🏆</div><h3 style="color:#9b59b6;font-size:0.85em;margin-top:5px;">${relicDef.name}</h3>
        <p style="font-size:0.7em;color:#9a8f80;margin-top:3px;">${relicDef.desc}</p><div class="item-price">150g</div>`;
      
      if (G.gold >= 150) {
        el.addEventListener('click', () => {
          G.gold -= 150;
          G.relics.push(item.data);
          
          // Vial of Shadows: remove a card on pickup
          if (item.data === "vial_of_shadows") {
            showRemoveCardScreen(() => {
              document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
              renderShopAfterPurchase();
            });
            return;
          }
          
          document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
          renderShopAfterPurchase();
        });
      } else {
        el.style.opacity = "0.4";
        el.style.cursor = "not-allowed";
      }
    } else if (item.type === "potion") {
      const potionDef = item.data;
      el.className = "shop-item";
      el.innerHTML = `<div style="font-size:1.5em;">${potionDef.icon}</div><h3 style="color:#d4c8b0;font-size:0.85em;margin-top:5px;">${potionDef.name}</h3>
        <p style="font-size:0.7em;color:#9a8f80;margin-top:3px;">${potionDef.desc}</p><div class="item-price">50g</div>`;
      
      if (G.gold >= 50 && G.potions.length < G.potionSlots) {
        el.addEventListener('click', () => {
          G.gold -= 50;
          G.potions.push(potionDef.id);
          document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
          renderShopAfterPurchase();
        });
      } else if (G.potions.length >= G.potionSlots) {
        el.style.opacity = "0.4";
        el.innerHTML += `<div style="color:#ff6b6b;font-size:0.7em;">Potion slots full</div>`;
      } else {
        el.style.opacity = "0.4";
        el.style.cursor = "not-allowed";
      }
    } else if (item.type === "remove") {
      el.className = "shop-item";
      el.innerHTML = `<div style="font-size:1.5em;">🗑</div><h3 style="color:#d4c8b0;font-size:0.85em;margin-top:5px;">Remove Card</h3>
        <p style="font-size:0.7em;color:#9a8f80;margin-top:3px;">Remove 1 card from deck</p><div class="item-price">75g</div>`;
      
      if (G.gold >= 75) {
        el.addEventListener('click', () => {
          G.gold -= 75;
          showRemoveCardScreen(() => {
            document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
            renderShopAfterPurchase();
          });
        });
      } else {
        el.style.opacity = "0.4";
        el.style.cursor = "not-allowed";
      }
    }
    
    itemsEl.appendChild(el);
  });
  
  showScreen("shop");
}

// ── function: renderShopAfterPurchase ──
function renderShopAfterPurchase() {
  // Re-render shop with updated gold
  document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
  const itemsEl = document.getElementById("shop-items");
  // Simple re-render by refreshing the page - in a real game we'd be more surgical
}

// ── function: renderClassSelection ──
// ===== CLASS SELECTION & GAME INIT =====

function renderClassSelection() {
  const container = document.getElementById("class-cards");
  container.innerHTML = "";
  
  Object.entries(CLASS_DATA).forEach(([id, data]) => {
    const el = document.createElement("div");
    el.className = "class-card";
    
    const sigCard = CARD_DB[data.signatureCard];
    
    el.innerHTML = `
      <h3 style="color:${data.color}">${data.name}</h3>
      <p>${data.theme}</p>
      <hr style="border-color:#3a2d4a;margin:10px 0;">
      <p style="font-size:0.8em;color:#7a6f60;">Starting Deck:</p>
      <p style="font-size:0.75em;color:#9a8f80;">5× Strike, 4× Defend</p>
      <p style="font-size:0.75em;color:${data.color};">1× ${sigCard.name}</p>
      <hr style="border-color:#3a2d4a;margin:10px 0;">
      <p style="font-size:0.8em;color:#ffd700;">99 Gold</p>
    `;
    
    el.addEventListener('click', () => selectClass(id));
    container.appendChild(el);
  });
}

// ── function: selectClass ──
function selectClass(classId) {
  G.playerClass = classId;
  
  // Initialize player stats
  G.hp = 70;
  G.maxHp = 70;
  G.gold = 99;
  G.act = 1;
  G.floor = 2;
  G.relics = [];
  G.potions = [];
  G.deck = createStartingDeck(classId);
  
  // Shadow Amulet effect (if from previous run - shouldn't happen but safety)
  if (G.startTurnIntangible) {
    G.maxHp -= 15;
    G.hp = Math.min(G.hp, G.maxHp);
  }
  
  addLog(`Started as ${CLASS_DATA[classId].name}.`);
  
  // Generate map and show it
  generateMap();
  renderMap();
  showScreen("map");
}

// ── function: startNewGame ──
function startNewGame() {
  initGameState();
  renderClassSelection();
  showScreen("class-select");
}

// ── function: setupEventListeners ──
// ===== EVENT LISTENERS SETUP =====

function setupEventListeners() {
  // Title screen -> Class selection
  document.getElementById("btn-start").addEventListener('click', () => {
    startNewGame();
  });
  
  // End Turn button
  document.getElementById("btn-end-turn").addEventListener('click', () => {
    endPlayerTurn();
  });
  
  // Skip reward
  document.getElementById("btn-skip-reward").addEventListener('click', () => {
    advanceToMap();
  });
  
  // Leave shop
  document.getElementById("btn-leave-shop").addEventListener('click', () => {
    advanceToMap();
  });
  
  // Cancel remove card
  document.getElementById("btn-cancel-remove").addEventListener('click', () => {
    showScreen("rest");
  });
  
  // Cancel upgrade
  document.getElementById("btn-cancel-upgrade").addEventListener('click', () => {
    showScreen("rest");
  });
  
  // Cancel potion target
  document.getElementById("btn-cancel-potion").addEventListener('click', () => {
    document.getElementById("screen-potion-use").style.display = "none";
  });
  
  // Restart / Play again
  document.getElementById("btn-restart").addEventListener('click', () => {
    initGameState();
    showScreen("title");
  });
  
  document.getElementById("btn-play-again").addEventListener('click', () => {
    initGameState();
    showScreen("title");
  });
}

// ── function: getBossEnemyId ──
function getBossEnemyId(act) {
  const bossDef = BOSS_DB["act" + act];
  if (!bossDef) return "skeleton_warrior";
  return bossDef.name.toLowerCase().replace(/\s+/g, '_');
}

// ── const: originalEnterNode ──
// Override enterNode boss handling
const originalEnterNode = enterNode;

// ── function: startCombat ──
function startCombat(enemyIds) {
  G.combatActive = true;
  G.playerTurn = true;
  G.turnNumber = 0;
  G.selectedEnemyIdx = -1;
  G.firstHpCostAttackUsed = false;
  G.debuffs = {}; // Player debuffs: weak, vulnerable, bleed, burn, poison
  
  // Create enemies from IDs
  G.enemies = [];
  
  enemyIds.forEach((eid, idx) => {
    // Check if it's a boss
    const actKey = "act" + G.act;
    const bossDef = BOSS_DB[actKey];
    
    let isBossFight = false;
    if (bossDef && eid === getBossEnemyId(G.act)) {
      isBossFight = true;
      
      const bossEnemy = {
        id: eid, name: bossDef.name, hp: bossDef.hp, maxHp: bossDef.maxHp,
        block: 0, intentIdx: 0, intents: [...bossDef.phase1Intents], alive: true,
        buffs: {}, debuffs: {}, strBuff: 0, isBoss: true, phase2: false,
        idx: G.enemies.length
      };
      G.enemies.push(bossEnemy);
    } else {
      // Regular enemy from ENEMY_DB
      const template = ENEMY_DB[eid];
      if (!template) return;
      
      const enemy = {
        id: eid, name: template.name, hp: template.hp, maxHp: template.maxHp,
        block: 0, intentIdx: 0, intents: [...template.intents], alive: true,
        buffs: {}, debuffs: {}, strBuff: 0, isBoss: false, phase2: false,
        idx: G.enemies.length
      };
      G.enemies.push(enemy);
    }
  });

  // Apply Philosopher's Stone effect
  if (G.enemiesStartWithStr) {
    G.enemies.forEach(e => e.strBuff += 1);
  }

  // Reset player combat state
  G.block = 0;
  G.buffs = { strength: 0, dexterity: 0, regeneration: 0, intangible: 0 };
  G.nextTurnBuffs = { block: 0, energy: 0, dex: 0 };

  // Apply Shadow Cloak relic
  if (G.relics.includes("shadow_cloak")) {
    G.buffs.intangible += 1;
  }

  // Apply Cursed Amulet relic
  if (G.relics.includes("cursed_amulet")) {
    G.enemies.forEach(e => { e.debuffs.weak = (e.debuffs.weak || 0) + 1; });
  }

  // Black Blood: lose 10% max HP at start of combat
  if (G.relics.includes("black_blood")) {
    const loss = Math.floor(G.maxHp * 0.1);
    G.hp -= loss;
    addLog(`Black Blood: Lose ${loss} HP.`);
  }

  // Demon Horns: Hellforged only, add Imp Claw at start of combat (once per combat)
  if (G.relics.includes("demon_horns") && G.playerClass === "hellforged" && !G._demonHornsUsed) {
    const imp = createCardInstance("imp_claw");
    G.hand.push(imp);
    addLog("Demon Horns: Added an Imp Claw to your hand.");
  }

  // Setup deck for combat - separate powers from regular cards
  const powerCards = G.deck.filter(c => c.type === "power");
  const regularCards = G.deck.filter(c => c.type !== "power");
  
  G.drawPile = shuffleArray(regularCards);
  G.discardPile = [];
  G.exhaustPile = [];
  G.hand = [];

  showScreen("combat");
  startPlayerTurn();
}

// ── function: getEnemyIdsForFloor ──
function getEnemyIdsForFloor(node, act) {
  // Multi-enemy compositions based on floor
  const pool = ACT_ENEMY_POOLS["act" + act];
  
  if (node.enemyId) {
    return [node.enemyId];
  }
  
  // Default: single random enemy
  return [pool[Math.floor(Math.random() * pool.length)]];
}

// ── const: _origEndCombat ──
// Override endCombat to handle elite flag properly
const _origEndCombat = endCombat;

// ── function: endCombat ──
function endCombat(victory) {
  G.combatActive = false;
  
  if (victory) {
    addLog("Combat Victory!");
    
    // Ashen Armor: heal 6 HP at end of combat
    if (G.relics.includes("ashen_armor")) {
      G.hp = Math.min(G.maxHp, G.hp + 6);
      addLog("Ashen Armor: Healed 6 HP.");
    }

    // Check for boss victory
    const wasBoss = G.enemies.some(e => e.isBoss);
    
    if (wasBoss) {
      // Full heal after boss
      G.hp = G.maxHp;
      
      if (G.act >= 3 && G.floor >= 15) {
        showScreen("victory");
        document.getElementById("victory-stats").textContent = `Class: ${CLASS_DATA[G.playerClass].name} | Gold: ${G.gold}`;
        return;
      }
      
      // Advance to next act
      G.act++;
      if (G.act > 3) {
        showScreen("victory");
        document.getElementById("victory-stats").textContent = `Class: ${CLASS_DATA[G.playerClass].name} | Gold: ${G.gold}`;
        return;
      }
      
      // Reset floor for new act
      G.floor = 2;
      
      // Boss rewards
      generateBossRewards();
    } else if (G._isEliteFight) {
      // Elite rewards - always drops a relic
      const goldAmt = 25 + Math.floor(Math.random() * 11);
      let finalGold = goldAmt;
      if (G.relics.includes("greedy_imp")) finalGold += 10;
      if (!G.noMoreGold) G.gold += finalGold;

      // Card reward with increased rare chance
      const cards = generateCardRewards(true, false);
      
      // Potion chance: 70%
      if (Math.random() < 0.7 && G.potions.length < G.potionSlots) {
        const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
        G.potions.push(potion.id);
      }
      
      // Relic drop - random from pool
      const availableRelics = Object.keys(RELIC_DB).filter(r => !G.relics.includes(r));
      if (availableRelics.length > 0) {
        const relicId = availableRelics[Math.floor(Math.random() * availableRelics.length)];
        G.relics.push(relicId);
        addLog(`Obtained relic: ${RELIC_DB[relicId].name}`);
        
        // Vial of Shadows special handling
        if (relicId === "vial_of_shadows") {
          showRemoveCardScreen(() => {
            G.pendingReward = { cards, goldAmt };
            renderRewardScreen(cards, finalGold);
            showScreen("reward");
          });
          return;
        }
      }
      
      // Show reward screen
      G.pendingReward = { cards, goldAmt: finalGold };
      renderRewardScreen(cards, finalGold);
      showScreen("reward");
    } else {
      // Normal combat rewards
      generateCombatRewards(false);
    }
    
    G._isEliteFight = false;
  } else {
    addLog("You have fallen...");
    showScreen("gameover");
    document.getElementById("gameover-stats").textContent = `Class: ${CLASS_DATA[G.playerClass].name} | Act ${G.act}, Floor ${G.floor}`;
  }
}

// ── const: _origRenderMap ──
// Fix renderMap to draw connections properly
const _origRenderMap = renderMap;

// ── function: enterNode ──
function enterNode(idx) {
  const node = G.mapNodes[idx];
  if (!node || !isNodeReachable(idx)) return;
  
  G.visitedNodes.add(G.currentNodeIdx);
  G.currentNodeIdx = idx;
  G.floor = node.floor;
  
  switch (node.type) {
    case "enemy": {
      // Normalize enemies to array of strings
      let enemyIds = [];
      if (Array.isArray(node.enemies)) {
        enemyIds = node.enemies.filter(e => typeof e === 'string');
      } else if (typeof node.enemies === 'string') {
        enemyIds = [node.enemies];
      }
      if (enemyIds.length === 0) enemyIds = [getRandomEnemyForAct(G.act)];
      
      startCombat(enemyIds);
      break;
    }
    case "elite":
      G._isEliteFight = true;
      let eliteIds = [];
      if (Array.isArray(node.enemies)) {
        eliteIds = node.enemies.filter(e => typeof e === 'string');
      } else if (typeof node.enemies === 'string') {
        eliteIds = [node.enemies];
      }
      if (eliteIds.length === 0) eliteIds = [getRandomEliteForAct(G.act)];
      
      startCombat(eliteIds);
      break;
    case "boss": {
      const bossId = getBossEnemyId(G.act);
      startCombat([bossId]);
      break;
    }
    case "rest":
      showRestScreen();
      break;
    case "shop":
      showShopScreen();
      break;
    case "event":
      showEventScreen();
      break;
  }
}

// ── function: generateMap ──
function generateMap() {
  G.mapNodes = [];
  G.visitedNodes = new Set();
  G.currentNodeIdx = -1;
  
  const actPool = ACT_ENEMY_POOLS["act" + G.act];
  const elitePool = ACT_ELITE_POOLS["act" + G.act];
  
  function randEnemy() { return actPool[Math.floor(Math.random() * actPool.length)]; }
  function randElite() { return elitePool[Math.floor(Math.random() * elitePool.length)]; }
  
  // Floor definitions: each specifies the node type and enemy composition
  const floors = [
    { floor: 2, type: "enemy", leftEnemies: [randEnemy()], rightEnemies: [randEnemy()] },
    { floor: 3, type: "enemy", enemies: [randEnemy()] },
    { floor: 4, type: "event" },
    { floor: 5, type: "shop" },
    { floor: 6, type: "enemy", enemies: [randEnemy()] },
    { floor: 7, type: "rest" },
    { floor: 8, type: "enemy", leftEnemies: [actPool[0], actPool[1 % actPool.length]], rightEnemies: [randEnemy()] },
    { floor: 9, type: "elite", leftEnemies: [elitePool[0]], rightEnemies: [randEnemy()] },
    { floor: 10, type: "event" },
    { floor: 11, type: "enemy", enemies: [randEnemy(), randEnemy()] },
    { floor: 12, type: "elite", leftEnemies: [elitePool[1 % elitePool.length]], rightEnemies: [randEnemy(), randEnemy()] },
    { floor: 13, type: "enemy", enemies: [randEnemy(), randEnemy()] },
    { floor: 14, type: "rest" },
    { floor: 15, type: "boss" }
  ];
  
  let currentCol = 1;
  
  floors.forEach((floorDef) => {
    const isBranch = !!floorDef.leftEnemies;
    
    if (isBranch) {
      // Two options at this floor - left is the harder/elite option, right is easier
      const leftCol = Math.max(0, currentCol - 1);
      const rightCol = Math.min(3, currentCol + 1);
      
      G.mapNodes.push({
        floor: floorDef.floor, type: floorDef.type, col: leftCol,
        enemies: floorDef.leftEnemies || [floorDef.type === "elite" ? randElite() : randEnemy()]
      });
      
      G.mapNodes.push({
        floor: floorDef.floor, type: "enemy", col: rightCol, // Right branch is always enemy (easier alternative)
        enemies: floorDef.rightEnemies || [randEnemy()]
      });
    } else {
      if (floorDef.floor === 15) currentCol = 1;
      
      G.mapNodes.push({
        floor: floorDef.floor, type: floorDef.type, col: currentCol,
        enemies: floorDef.enemies || null
      });
    }
  });
}

// ── function: renderPlayerStatus ──
function renderPlayerStatus() {
  // Render relic display
  const relicEl = document.getElementById("relic-display");
  if (relicEl) {
    relicEl.innerHTML = "";
    G.relics.forEach(relicId => {
      const relicDef = RELIC_DB[relicId] || BOSS_RELIC_DB[relicId];
      if (!relicDef) return;
      
      const icon = document.createElement("div");
      icon.className = "relic-icon";
      icon.textContent = "🏆";
      icon.title = `${relicDef.name}: ${relicDef.desc}`;
      relicEl.appendChild(icon);
    });
  }
  
  // Render buff/debuff display
  const buffEl = document.getElementById("buff-display");
  if (buffEl) {
    buffEl.innerHTML = "";
    
    // Player buffs
    if (G.buffs.strength > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-buff";
      badge.textContent = `Str +${G.buffs.strength}`;
      buffEl.appendChild(badge);
    }
    if (G.buffs.dexterity > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-buff";
      badge.textContent = `Dex +${G.buffs.dexterity}`;
      buffEl.appendChild(badge);
    }
    if (G.buffs.regeneration > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-buff";
      badge.textContent = `Regen ${G.buffs.regeneration}`;
      buffEl.appendChild(badge);
    }
    if (G.buffs.intangible > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-buff";
      badge.textContent = `Intangible ${G.buffs.intangible}`;
      buffEl.appendChild(badge);
    }
    
    // Player debuffs
    if (G.debuffs && G.debuffs.weak > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-debuff";
      badge.textContent = `Weak ${G.debuffs.weak}`;
      buffEl.appendChild(badge);
    }
    if (G.debuffs && G.debuffs.vulnerable > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-debuff";
      badge.textContent = `Vuln ${G.debuffs.vulnerable}`;
      buffEl.appendChild(badge);
    }
    if (G.debuffs && G.debuffs.bleed > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-debuff";
      badge.textContent = `Bleed ${G.debuffs.bleed}`;
      buffEl.appendChild(badge);
    }
    if (G.debuffs && G.debuffs.burn > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-debuff";
      badge.textContent = `Burn ${G.debuffs.burn}`;
      buffEl.appendChild(badge);
    }
    if (G.debuffs && G.debuffs.poison > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-debuff";
      badge.textContent = `Poison ${G.debuffs.poison}`;
      buffEl.appendChild(badge);
    }
    
    // Next turn buffs
    if (G.nextTurnBuffs.block > 0) {
      const badge = document.createElement("div");
      badge.className = "status-badge badge-buff";
      badge.textContent = `Next Block ${G.nextTurnBuffs.block}`;
      buffEl.appendChild(badge);
    }
  }
}

// ── const: _origRenderCombat ──
// Update renderCombat to call this
const _origRenderCombat = renderCombat;

// ── function: renderCombat ──
function renderCombat() {
  // HP bar
  const hpPct = Math.max(0, (G.hp / G.maxHp) * 100);
  document.getElementById("hp-text").innerHTML = `<span style="color:#ff6b6b;">${G.hp}/${G.maxHp}</span>`;
  
  // Block
  document.getElementById("player-block-display").textContent = G.block > 0 ? `🛡 ${G.block}` : "";
  
  // Energy
  const energyEl = document.getElementById("player-energy-display");
  let gemsHtml = "";
  for (let i = 0; i < G.energy; i++) {
    gemsHtml += `<span class="energy-gem"></span>`;
  }
  const totalEnergy = G.maxEnergy + G.tempEnergyBonus + 
    (G.relics.includes("cursed_crown") ? 1 : 0) + 
    (G.relics.includes("ectoplasm") ? 1 : 0) + 
    (G.relics.includes("philosophers_stone") ? 1 : 0);
  for (let i = G.energy; i < totalEnergy; i++) {
    gemsHtml += `<span class="energy-gem used"></span>`;
  }
  energyEl.innerHTML = gemsHtml;
  
  // Gold
  document.getElementById("player-gold-display").textContent = `💰 ${G.gold}`;
  
  // Enemies
  const enemyZone = document.getElementById("enemy-zone");
  enemyZone.innerHTML = "";
  G.enemies.forEach((enemy, idx) => {
    if (!enemy.alive) return;
    
    const hpPct = Math.max(0, (enemy.hp / enemy.maxHp) * 100);
    const el = document.createElement("div");
    el.className = `enemy-card${G.selectedEnemyIdx === idx ? ' selected' : ''}`;
    
    let statusBadges = "";
    if (enemy.debuffs.weak > 0) statusBadges += `<span class="status-badge badge-debuff">Weak ${enemy.debuffs.weak}</span>`;
    if (enemy.debuffs.vulnerable > 0) statusBadges += `<span class="status-badge badge-debuff">Vuln ${enemy.debuffs.vulnerable}</span>`;
    if (enemy.debuffs.bleed > 0) statusBadges += `<span class="status-badge badge-debuff">Bleed ${enemy.debuffs.bleed}</span>`;
    if (enemy.debuffs.burn > 0) statusBadges += `<span class="status-badge badge-debuff">Burn ${enemy.debuffs.burn}</span>`;
    if (enemy.strBuff > 0) statusBadges += `<span class="status-badge badge-buff">Str +${enemy.strBuff}</span>`;
    
    el.innerHTML = `
      <div class="enemy-name">${enemy.name}${enemy.isBoss ? ' 👑' : ''}</div>
      <div class="enemy-hp-bar-bg"><div class="enemy-hp-fill" style="width:${hpPct}%"></div></div>
      <div class="enemy-hp-text">${enemy.hp}/${enemy.maxHp}</div>
      ${enemy.block > 0 ? `<div class="enemy-block-text">🛡 ${enemy.block}</div>` : ''}
      <div style="margin-top:3px;">${statusBadges}</div>
    `;
    
    el.addEventListener('click', () => {
      G.selectedEnemyIdx = idx;
      renderCombat();
    });
    
    enemyZone.appendChild(el);
  });
  
  // Intent zone
  const intentZone = document.getElementById("intent-zone");
  intentZone.innerHTML = "";
  G.enemies.forEach((enemy) => {
    if (!enemy.alive) return;
    
    let intents = [];
    if (enemy.isBoss) {
      const bossDef = getBossDefinition();
      intents = enemy.phase2 ? bossDef.phase2Intents : bossDef.phase1Intents;
    } else {
      intents = enemy.intents;
    }
    
    const intentIdx = enemy.intentIdx % intents.length;
    const intent = intents[intentIdx];
    
    let intentText = "";
    let intentClass = "attack";
    
    switch (intent.type) {
      case "attack": intentText = `⚔ Attack ${intent.dmg + (enemy.strBuff||0)}`; intentClass = "attack"; break;
      case "multiHit": {
        const totalDmg = intent.hits.reduce((s,h) => s + h.dmg + (enemy.strBuff||0), 0);
        intentText = `⚔ ${intent.hits.length}×${intent.hits[0].dmg + (enemy.strBuff||0)} (${totalDmg})`; intentClass = "attack"; break;
      }
      case "defend": intentText = `🛡 Defend ${intent.block}`; intentClass = "defend"; break;
      case "attackDefend": intentText = `⚔${intent.dmg + (enemy.strBuff||0)} 🛡${intent.block}`; intentClass = "attack"; break;
      case "debuff": {
        if (intent.weak) { intentText = `💜 Weak ${intent.weak}`; intentClass = "debuff"; }
        else if (intent.bleed) { intentText = `🩸 Bleed ${intent.bleed}`; intentClass = "debuff"; }
        else if (intent.poison) { intentText = `☠ Poison ${intent.poison}`; intentClass = "debuff"; }
      } break;
      case "debuffAll": intentText = `💜 Vuln+Weak`; intentClass = "debuff"; break;
      case "attackDebuff": intentText = `⚔${intent.dmg + (enemy.strBuff||0)} ${intent.weak ? '💜' : ''}${intent.burn ? '🔥' : ''}`; intentClass = "attack"; break;
      case "selfBuff": intentText = `✨ Buff`; intentClass = "buff"; break;
      case "drainLife": intentText = `⚔${intent.dmg + (enemy.strBuff||0)} ❤+${intent.heal}`; intentClass = "attack"; break;
      case "summon": intentText = `👹 Summon`; intentClass = "buff"; break;
      case "defendBuff": intentText = `🛡${intent.block} ✨Str`; intentClass = "defend"; break;
      case "buffAll": intentText = `✨ Str All`; intentClass = "buff"; break;
      case "selfVulnDefend": intentText = `🛡${intent.block}`; intentClass = "defend"; break;
      default: intentText = `⚔ Attack`; intentClass = "attack";
    }
    
    const intentEl = document.createElement("div");
    intentEl.className = `intent-icon ${intentClass}`;
    intentEl.textContent = `${enemy.name}: ${intentText}`;
    intentZone.appendChild(intentEl);
  });
  
  // Hand
  renderHand();
  
  // Deck info
  const powerCards = G.deck.filter(c => c.type === "power");
  document.getElementById("draw-pile-count").textContent = `📚 Draw: ${G.drawPile.length}`;
  document.getElementById("discard-pile-count").textContent = `🗑 Discard: ${G.discardPile.length}${powerCards.length > 0 ? ` | Powers: ${powerCards.length}` : ''}`;
  
  // Potion bar
  renderPotionBar();
  
  // Player status (relics, buffs)
  renderPlayerStatus();
  
  // Combat log
  const logEl = document.getElementById("combat-log");
  if (logEl) {
    logEl.innerHTML = G.logMessages.slice(-8).map(m => `<div>${m}</div>`).join("");
    logEl.scrollTop = logEl.scrollHeight;
  }
}

// ── main ──
// Initialize game
initGameState();
setupEventListeners();

// Show title screen
showScreen("title");

// Ensure potion overlay is hidden initially
const potionOverlay = document.getElementById("screen-potion-use");
if (potionOverlay) potionOverlay.style.display = "none";

// Add combat log element if not present
if (!document.getElementById("combat-log")) {
  const logEl = document.createElement("div");
  logEl.id = "combat-log";
  document.body.appendChild(logEl);
}

// Add potion bar if not present  
if (!document.getElementById("potion-bar")) {
  const bar = document.createElement("div");
  bar.id = "potion-bar";
  document.body.appendChild(bar);
}
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"30417ba0-bcba-4ea0-8f8c-79acfd3b68f8","tokensIn":4184343,"tokensOut":65481,"tokensTotal":4249824,"turns":64,"toolCalls":63,"failedToolCalls":1,"timestamp":"2026-07-16T03:35:22.162Z"} -->