← Deckbuilder results

Deckbuilder

Qwen 3.6 27B MTP GGUF · typescript

Initial view of Deckbuilder

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 76,031 bytes · SHA-256 2cdfd7377183
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shadows of Aethelgard</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { 
  width: 100%; height: 100%; 
  overflow: hidden;
  font-family: 'Georgia', 'Times New Roman', serif;
  background: #060610;
  color: #d4c9a8;
  user-select: none;
}
#app {
  width: 100%; height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: radial-gradient(ellipse at 50% 40%, #140e1e 0%, #080810 60%, #040410 100%);
}

#game-container {
  position: relative;
  width: min(100vw, 100vh);
  height: min(100vw, 100vh);
  max-width: 900px;
  max-height: 900px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid #1a1528;
  border-radius: 4px;
  box-shadow: 0 0 60px rgba(0,0,0,0.8), inset 0 0 80px rgba(20,14,30,0.5);
}

/* Ambient background particles */
#game-container::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: 
    radial-gradient(1px 1px at 10% 20%, rgba(168,85,247,0.15) 0%, transparent 100%),
    radial-gradient(1px 1px at 30% 60%, rgba(168,85,247,0.1) 0%, transparent 100%),
    radial-gradient(1px 1px at 70% 30%, rgba(200,150,240,0.1) 0%, transparent 100%),
    radial-gradient(1px 1px at 80% 70%, rgba(100,80,160,0.12) 0%, transparent 100%),
    radial-gradient(1px 1px at 50% 80%, rgba(168,85,247,0.08) 0%, transparent 100%),
    radial-gradient(1px 1px at 20% 90%, rgba(100,80,160,0.1) 0%, transparent 100%),
    radial-gradient(1px 1px at 60% 15%, rgba(168,85,247,0.12) 0%, transparent 100%),
    radial-gradient(1px 1px at 90% 50%, rgba(200,150,240,0.08) 0%, transparent 100%);
  pointer-events: none;
  z-index: 1;
  animation: particleDrift 20s linear infinite;
}
@keyframes particleDrift {
  0% { opacity: 0.5; }
  50% { opacity: 1; }
  100% { opacity: 0.5; }
}

/* Top HUD Bar */
#hud-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 20px;
  background: linear-gradient(180deg, rgba(26,21,32,0.95) 0%, rgba(13,11,18,0.9) 100%);
  border-bottom: 1px solid #2a2040;
  height: 46px;
  flex-shrink: 0;
  z-index: 10;
  backdrop-filter: blur(4px);
}
.hud-item {
  display: flex; align-items: center; gap: 6px;
  font-size: 13px; letter-spacing: 1px;
}
.hud-icon { font-size: 16px; filter: drop-shadow(0 0 3px rgba(212,201,168,0.3)); }
#hud-gold { color: #f0c040; font-weight: bold; }
#hud-floor { color: #c8a070; }
#hud-class { color: #a098c0; }

/* Main game area */
#game-area {
  flex: 1;
  display: flex;
  position: relative;
  overflow: hidden;
  background: radial-gradient(ellipse at 50% 35%, #120e1a 0%, #0a0810 70%, #060610 100%);
}

/* Floor-specific background tints */
#game-area[data-floor="1"] { background: radial-gradient(ellipse at 50% 35%, #121a10 0%, #0a1008 70%, #061008 100%); }
#game-area[data-floor="2"] { background: radial-gradient(ellipse at 50% 35%, #1a1018 0%, #100810 70%, #0a0610 100%); }
#game-area[data-floor="3"] { background: radial-gradient(ellipse at 50% 35%, #100a18 0%, #080610 70%, #040410 100%); }

/* Enemy area (center) */
#enemy-area {
  position: absolute;
  left: 50%; top: 22%;
  transform: translate(-50%, -50%);
  text-align: center;
  z-index: 5;
}
#enemy-sprite {
  width: 130px; height: 130px;
  margin: 0 auto 8px;
  filter: drop-shadow(0 0 15px rgba(139, 0, 0, 0.4));
  transition: filter 0.2s;
}
#enemy-sprite.hurt {
  animation: enemyHurt 0.3s;
}
@keyframes enemyHurt {
  0%, 100% { filter: drop-shadow(0 0 15px rgba(139, 0, 0, 0.4)); }
  50% { filter: brightness(2) drop-shadow(0 0 25px rgba(255, 0, 0, 0.8)); }
}
#enemy-hp-bar {
  width: 160px; height: 14px;
  background: #1a1a1a;
  border: 1px solid #333;
  border-radius: 3px;
  margin: 0 auto;
  overflow: hidden;
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
}
#enemy-hp-fill {
  height: 100%;
  background: linear-gradient(90deg, #8b0000, #cc3333);
  transition: width 0.3s ease-out;
  box-shadow: 0 0 8px rgba(204,51,51,0.3);
}
#enemy-name {
  font-size: 14px;
  color: #cc8866;
  margin-bottom: 4px;
  text-shadow: 0 0 10px rgba(204, 136, 102, 0.5);
  letter-spacing: 1px;
}
#enemy-stats {
  font-size: 11px;
  color: #777;
  margin-top: 4px;
  letter-spacing: 0.5px;
}

/* Player area (bottom) */
#player-area {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  z-index: 5;
  background: linear-gradient(0deg, rgba(10,8,16,0.95) 0%, rgba(10,8,16,0.7) 60%, transparent 100%);
}

/* Card hand */
#card-hand {
  display: flex;
  justify-content: center;
  gap: 6px;
  padding: 12px 20px;
  min-height: 175px;
  flex-wrap: nowrap;
  perspective: 800px;
}
.card {
  width: 105px;
  height: 148px;
  border-radius: 8px;
  cursor: pointer;
  transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
  position: relative;
  flex-shrink: 0;
  filter: drop-shadow(0 2px 6px rgba(0,0,0,0.7));
}
.card:hover {
  transform: translateY(-24px) scale(1.1) !important;
  z-index: 100;
  filter: drop-shadow(0 8px 20px rgba(212, 191, 77, 0.4));
}
.card.playable {
  filter: drop-shadow(0 2px 10px rgba(212, 191, 77, 0.25));
}
.card:not(.playable) {
  filter: grayscale(0.6) drop-shadow(0 2px 4px rgba(0,0,0,0.4));
  cursor: not-allowed;
}
.card svg {
  width: 100%; height: 100%;
  border-radius: 8px;
}

/* Player info bar */
#player-info {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 20px 4px;
}
#player-info span {
  font-size: 12px;
  color: #998877;
}

/* Energy bar */
#energy-bar {
  display: flex; align-items: center; gap: 3px;
}
.energy-pip {
  width: 16px; height: 16px;
  border-radius: 50%;
  border: 2px solid #3a2d5a;
  background: #1a1030;
  transition: all 0.2s;
}
.energy-pip.filled {
  background: radial-gradient(circle at 40% 40%, #c084fc, #7c3aed);
  border-color: #a855f7;
  box-shadow: 0 0 8px rgba(168, 85, 247, 0.5);
}

/* HP bar player */
#player-hp-bar {
  width: 120px; height: 10px;
  background: #1a1a1a;
  border: 1px solid #333;
  border-radius: 3px;
  overflow: hidden;
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
}
#player-hp-fill {
  height: 100%;
  background: linear-gradient(90deg, #2d8a4e, #4caf50);
  transition: width 0.3s ease-out;
  box-shadow: 0 0 6px rgba(76,175,80,0.3);
}

/* Buttons */
.btn {
  padding: 8px 24px;
  border: 1px solid #3a3050;
  border-radius: 4px;
  background: linear-gradient(180deg, #251f3a 0%, #151020 100%);
  color: #d4c9a8;
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  letter-spacing: 1px;
  transition: all 0.2s;
  text-transform: uppercase;
}
.btn:hover {
  background: linear-gradient(180deg, #352f4a 0%, #252030 100%);
  border-color: #6a5a8a;
  box-shadow: 0 0 15px rgba(106,90,138,0.3);
  transform: translateY(-1px);
}
.btn:active { transform: translateY(0); }
.btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}
.btn-end-turn {
  border-color: #5a4a2a;
  background: linear-gradient(180deg, #3a2f1a 0%, #1a1510 100%);
}
.btn-end-turn:hover {
  border-color: #8a7a4a;
  box-shadow: 0 0 15px rgba(212, 191, 77, 0.3);
}

/* Overlay screens */
.overlay {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(6, 6, 16, 0.95);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 50;
  animation: fadeIn 0.4s ease-out;
  backdrop-filter: blur(4px);
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

.overlay h1 {
  font-size: 32px;
  color: #d4c9a8;
  text-shadow: 0 0 30px rgba(168, 85, 247, 0.5), 0 0 60px rgba(168, 85, 247, 0.2);
  margin-bottom: 16px;
  letter-spacing: 4px;
  text-transform: uppercase;
}
.overlay h2 {
  font-size: 20px;
  color: #cc8866;
  margin-bottom: 16px;
  letter-spacing: 2px;
}
.overlay p {
  font-size: 13px;
  color: #887766;
  max-width: 380px;
  text-align: center;
  margin-bottom: 10px;
  line-height: 1.6;
}

/* Class selection */
.class-select {
  display: flex; gap: 14px; margin: 16px 0;
}
.class-card {
  width: 155px;
  padding: 16px;
  border: 1px solid #2a2040;
  border-radius: 6px;
  background: rgba(16, 13, 24, 0.9);
  cursor: pointer;
  text-align: center;
  transition: all 0.25s;
}
.class-card:hover {
  border-color: #6a5a8a;
  transform: translateY(-4px);
  box-shadow: 0 4px 20px rgba(106,90,138,0.3);
  background: rgba(26, 21, 32, 0.95);
}
.class-card h3 { font-size: 15px; color: #d4c9a8; margin-bottom: 6px; letter-spacing: 1px; }
.class-card p { font-size: 11px; color: #776655; line-height: 1.4; }

/* Shop */
.shop-container {
  display: flex; gap: 10px; flex-wrap: wrap;
  justify-content: center;
  max-width: 580px;
  padding: 8px;
}
.shop-card {
  position: relative;
  cursor: pointer;
  transition: transform 0.2s;
}
.shop-card:hover:not(.no-hover) { transform: scale(1.06); }
.shop-card .card { width: 95px; height: 132px; }
.shop-price {
  position: absolute;
  bottom: -6px; right: -2px;
  background: #1a1028;
  border: 1px solid #a855f7;
  border-radius: 50%;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; color: #e9d5ff;
  font-weight: bold;
  box-shadow: 0 0 8px rgba(168,85,247,0.3);
}

/* Battle log */
#battle-log {
  position: absolute;
  top: 48px;
  left: 50%;
  transform: translateX(-50%);
  width: 320px;
  max-height: 60px;
  overflow: hidden;
  text-align: center;
  font-size: 12px;
  color: #cc8866;
  pointer-events: none;
  z-index: 6;
}
.log-entry {
  padding: 2px 8px;
  animation: logFade 2.5s forwards;
  text-shadow: 0 0 8px rgba(204,136,102,0.3);
  letter-spacing: 0.5px;
}
@keyframes logFade {
  0% { opacity: 0; transform: translateY(5px); }
  15% { opacity: 1; transform: translateY(0); }
  70% { opacity: 1; }
  100% { opacity: 0; }
}

/* Tooltip */
.card-tooltip {
  position: absolute;
  bottom: 175px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(10, 8, 18, 0.96);
  border: 1px solid #3a3050;
  border-radius: 6px;
  padding: 10px 14px;
  width: 210px;
  z-index: 200;
  pointer-events: none;
  display: none;
  box-shadow: 0 4px 20px rgba(0,0,0,0.6);
}
.card-tooltip .tt-name { color: #d4c9a8; font-size: 13px; font-weight: bold; margin-bottom: 2px; }
.card-tooltip .tt-cost { color: #c084fc; font-size: 11px; }
.card-tooltip .tt-type { color: #666; font-size: 10px; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 1px; }
.card-tooltip .tt-desc { color: #887766; font-size: 11px; line-height: 1.4; }

/* Status effects */
.status-icons {
  display: flex; gap: 4px; margin-top: 3px;
  justify-content: center;
}
.status-icon {
  font-size: 13px;
  filter: drop-shadow(0 0 3px rgba(255,100,0,0.4));
}

/* Floor transition */
.floor-banner {
  font-size: 26px;
  color: #d4c9a8;
  text-shadow: 0 0 30px rgba(168, 85, 247, 0.6);
  animation: bannerPulse 0.8s ease-out;
  letter-spacing: 3px;
}
@keyframes bannerPulse {
  0% { opacity: 0; transform: scale(0.8); }
  50% { opacity: 1; transform: scale(1.05); }
  100% { opacity: 1; transform: scale(1); }
}

.deck-count {
  font-size: 9px;
  color: #555;
  text-align: center;
  margin-top: 2px;
}

/* Deck/discard stats in player info */
#player-info > div {
  display: flex;
  align-items: center;
  gap: 8px;
}
</style>
</head>
<body>
<div id="game-container">
  <!-- Top HUD -->
  <div id="hud-top">
    <div class="hud-item"><span class="hud-icon">💰</span><span id="hud-gold">0</span></div>
    <div class="hud-item"><span class="hud-icon">⚔️</span><span id="hud-floor">Floor 1 - Room 1</span></div>
    <div class="hud-item"><span class="hud-icon">🛡️</span><span id="hud-class">Bastion</span></div>
  </div>
  
  <!-- Main game area -->
  <div id="game-area">
    <!-- Battle log -->
    <div id="battle-log"></div>
    
    <!-- Enemy -->
    <div id="enemy-area">
      <div id="enemy-name">Goblin Scout</div>
      <svg id="enemy-sprite" viewBox="0 0 120 120"></svg>
      <div id="enemy-hp-bar"><div id="enemy-hp-fill" style="width:100%"></div></div>
      <div class="status-icons" id="enemy-statuses"></div>
      <div id="enemy-stats">HP: 20/20 | ATK: 3</div>
    </div>
    
    <!-- Player area -->
    <div id="player-area">
      <div id="card-hand"></div>
      <div id="player-info">
        <div style="display:flex;align-items:center;gap:12px;">
          <span>HP:</span>
          <div id="player-hp-bar"><div id="player-hp-fill" style="width:100%"></div></div>
          <span id="player-hp-text">30/30</span>
        </div>
        <div style="display:flex;align-items:center;gap:12px;">
          <span>Energy:</span>
          <div id="energy-bar"></div>
          <span id="energy-text">3/3</span>
        </div>
        <div style="display:flex;align-items:center;gap:12px;">
          <span>Block: <span id="block-text">0</span></span>
          <span>Deck: <span id="deck-count">20</span></span>
          <span>Discard: <span id="discard-count">0</span></span>
        </div>
      </div>
      <div style="display:flex;justify-content:center;gap:12px;padding:8px;">
        <button class="btn btn-end-turn" id="btn-end-turn">End Turn</button>
      </div>
    </div>
    
    <!-- Card tooltip -->
    <div class="card-tooltip" id="card-tooltip">
      <div class="tt-name"></div>
      <div class="tt-cost"></div>
      <div class="tt-type"></div>
      <div class="tt-desc"></div>
    </div>
    
    <!-- Overlays -->
    <div class="overlay" id="overlay-start">
      <h1>Shadows of Aethelgard</h1>
      <p>The Void consumes all. Choose your champion and descend into the abyss.</p>
      <h2>Select Your Class</h2>
      <div class="class-select">
        <div class="class-card" data-class="bastion">
          <h3>🛡️ The Bastion</h3>
          <p>High Defense, Immovable<br>HP: 35 | Energy: 3</p>
        </div>
        <div class="class-card" data-class="wither">
          <h3>🔥 The Wither</h3>
          <p>Burn Damage, Status Effects<br>HP: 28 | Energy: 3</p>
        </div>
        <div class="class-card" data-class="shroud">
          <h3>🗡️ The Shroud</h3>
          <p>Speed, Assassination<br>HP: 30 | Energy: 3</p>
        </div>
      </div>
    </div>
    
    <div class="overlay" id="overlay-shop" style="display:none;">
      <h2>🏪 Card Shop</h2>
      <p id="shop-gold-display">Gold: 10</p>
      <div class="shop-container" id="shop-container"></div>
      <div style="margin-top:16px;display:flex;gap:12px;">
        <button class="btn" id="btn-leave-shop">Leave Shop</button>
      </div>
      <p style="margin-top:12px;font-size:11px;color:#665;">Click a card to purchase. Max 5 copies per card.</p>
    </div>
    
    <div class="overlay" id="overlay-gameover" style="display:none;">
      <h1 style="color:#8b0000;">Game Over</h1>
      <p id="gameover-msg">You have fallen to the Void.</p>
      <button class="btn" id="btn-restart" style="margin-top:16px;">Restart</button>
    </div>
    
    <div class="overlay" id="overlay-victory" style="display:none;">
      <h1 style="color:#d4af37;">Victory!</h1>
      <p>The Void Lord has been vanquished. Aethelgard is saved.</p>
      <button class="btn" id="btn-play-again" style="margin-top:16px;">Play Again</button>
    </div>
    
    <div class="overlay" id="overlay-floor-clear" style="display:none;">
      <h1 class="floor-banner" id="floor-banner-text">Floor Cleared!</h1>
      <p id="floor-clear-msg"></p>
      <button class="btn" id="btn-next-floor" style="margin-top:16px;">Descend Deeper</button>
    </div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./ui":"src/ui.ts","./game":"src/game.ts"},"src/cards.ts":{"./types":"src/types.ts"},"src/svg.ts":{"./types":"src/types.ts"},"src/enemies.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts","./cards":"src/cards.ts","./enemies":"src/enemies.ts"},"src/ui.ts":{"./types":"src/types.ts","./svg":"src/svg.ts","./game":"src/game.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ui_1 = require("./ui");
const game_1 = require("./game");
let gameState = null;
window.gameState = null;
function gs(state) {
    gameState = state;
    window.gameState = state;
}
function main() {
    (0, ui_1.cacheElements)();
    // Initial render (menu screen)
    gs({
        screen: 'menu',
        floor: 1,
        room: 1,
        player: {},
        enemy: null,
        isPlayerTurn: false,
        log: [],
        shopCards: [],
        turnStarted: false,
    });
    (0, ui_1.render)(gameState);
    // Class selection
    const classCards = document.querySelectorAll('.class-card');
    classCards.forEach(card => {
        card.addEventListener('click', () => {
            const cls = card.getAttribute('data-class');
            if (!cls)
                return;
            gs((0, game_1.initGameState)(cls));
            (0, game_1.startPlayerTurn)(gameState);
            (0, ui_1.render)(gameState);
        });
    });
    // End turn button
    document.getElementById('btn-end-turn').addEventListener('click', () => {
        if (!gameState)
            return;
        (0, game_1.endPlayerTurn)(gameState);
        (0, ui_1.render)(gameState);
    });
    // Leave shop / next floor
    document.getElementById('btn-leave-shop').addEventListener('click', () => {
        if (!gameState)
            return;
        (0, game_1.startNextCombat)(gameState);
        (0, game_1.startPlayerTurn)(gameState);
        (0, ui_1.render)(gameState);
    });
    // Auto-advance shop after buying all cards or leaving
    document.getElementById('btn-leave-shop').addEventListener('dblclick', () => {
        if (!gameState)
            return;
        (0, game_1.startNextCombat)(gameState);
        (0, game_1.startPlayerTurn)(gameState);
        (0, ui_1.render)(gameState);
    });
    document.getElementById('btn-next-floor').addEventListener('click', () => {
        if (!gameState)
            return;
        (0, game_1.advanceFloor)(gameState);
        (0, game_1.startPlayerTurn)(gameState);
        (0, ui_1.render)(gameState);
    });
    // Restart / play again
    document.getElementById('btn-restart').addEventListener('click', () => resetToMenu());
    document.getElementById('btn-play-again').addEventListener('click', () => resetToMenu());
}
function resetToMenu() {
    gs({
        screen: 'menu',
        floor: 1,
        room: 1,
        player: {},
        enemy: null,
        isPlayerTurn: false,
        log: [],
        shopCards: [],
        turnStarted: false,
    });
    (0, ui_1.render)(gameState);
}
main();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CLASS_INFO = void 0;
exports.CLASS_INFO = {
    bastion: { name: 'The Bastion', hp: 35, energy: 3, icon: '🛡️', themeColor: '#7a9ec2' },
    wither: { name: 'The Wither', hp: 28, energy: 3, icon: '🔥', themeColor: '#c27a3a' },
    shroud: { name: 'The Shroud', hp: 30, energy: 3, icon: '🗡️', themeColor: '#9b7ac2' },
};
};

// ── module: src/cards.ts ──
__mods["src/cards.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.STARTER_DECKS = exports.CARD_ROSTER = void 0;
exports.createCardInstance = createCardInstance;
exports.getCardById = getCardById;
exports.getByClass = getByClass;
exports.CARD_ROSTER = [
    // ===== BASTION (12 cards) =====
    { id: 'BA-A01', name: 'Slam', type: 'Attack', cost: 2, attack: 3, defense: 0, ability: 'If HP >= 30, draw 1', desc: 'Deals 3 damage. If you have 30+ HP, draw 1 card.', cls: 'bastion' },
    { id: 'BA-A02', name: 'Earthquake', type: 'Attack', cost: 3, attack: 4, defense: 0, ability: 'AoE', desc: 'Deals 4 damage to all enemies.', cls: 'bastion' },
    { id: 'BA-A03', name: 'Stone Fist', type: 'Attack', cost: 1, attack: 2, defense: 0, ability: '', desc: 'Deals 2 damage.', cls: 'bastion' },
    { id: 'BA-A04', name: 'Boulder', type: 'Attack', cost: 4, attack: 6, defense: 0, ability: '', desc: 'Deals 6 damage. Costs 4 Energy.', cls: 'bastion' },
    { id: 'BA-D01', name: 'Shield Wall', type: 'Defense', cost: 0, attack: 0, defense: 4, ability: '', desc: 'Gain 4 Defense for this turn.', cls: 'bastion' },
    { id: 'BA-D02', name: 'Iron Skin', type: 'Defense', cost: 2, attack: 0, defense: 5, ability: 'Discard 1', desc: 'Gain 5 Defense permanently. Discard 1 card.', cls: 'bastion' },
    { id: 'BA-D03', name: 'Stand Firm', type: 'Defense', cost: 1, attack: 0, defense: 2, ability: '', desc: 'Gain 2 Defense.', cls: 'bastion' },
    { id: 'BA-D04', name: 'Taunt', type: 'Defense', cost: 2, attack: 0, defense: 3, ability: 'Stun 1 enemy', desc: 'Gain 3 Defense. Stun 1 enemy for 1 turn.', cls: 'bastion' },
    { id: 'BA-U01', name: 'Search', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Draw', desc: 'Draw 1 card. Discard hand if > 5 cards.', cls: 'bastion' },
    { id: 'BA-U02', name: 'Repair', type: 'Other', cost: 3, attack: 0, defense: 0, ability: 'Heal', desc: 'Heal 6 HP.', cls: 'bastion' },
    { id: 'BA-U03', name: 'Rally', type: 'Other', cost: 1, attack: 0, defense: 0, ability: 'Draw + Energy', desc: 'Draw 1 card. Gain 1 Energy.', cls: 'bastion' },
    { id: 'BA-U04', name: 'Loot', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Gold', desc: 'Gain 5 Gold.', cls: 'bastion' },
    // ===== WITHER (12 cards) =====
    { id: 'WI-A01', name: 'Fireball', type: 'Attack', cost: 2, attack: 3, defense: 0, ability: '1 Burn', desc: 'Deals 3 damage. Apply 1 Burn to target.', cls: 'wither' },
    { id: 'WI-A02', name: 'Inferno', type: 'Attack', cost: 3, attack: 4, defense: 0, ability: '3 Burn', desc: 'Deals 4 damage. Applies 3 Burn.', cls: 'wither' },
    { id: 'WI-A03', name: 'Ember', type: 'Attack', cost: 1, attack: 2, defense: 0, ability: '1 Burn/t', desc: 'Deals 2 damage. Applies 1 Burn per turn.', cls: 'wither' },
    { id: 'WI-A04', name: 'Pyre', type: 'Attack', cost: 4, attack: 5, defense: 0, ability: '1 dmg/t all', desc: 'Deals 5 damage. All enemies take 1 dmg next turn.', cls: 'wither' },
    { id: 'WI-D01', name: 'Scorch Shield', type: 'Defense', cost: 2, attack: 0, defense: 3, ability: 'Block Burn', desc: 'Gain 3 Defense. Blocks 1 Burn damage.', cls: 'wither' },
    { id: 'WI-D02', name: 'Heat Resist', type: 'Defense', cost: 1, attack: 0, defense: 1, ability: '+1 Energy', desc: 'Gain 1 Defense. Gain 1 Energy.', cls: 'wither' },
    { id: 'WI-D03', name: 'Smoke', type: 'Defense', cost: 2, attack: 0, defense: 4, ability: 'Block Burn', desc: 'Gain 4 Defense. Blocks 1 Burn damage.', cls: 'wither' },
    { id: 'WI-D04', name: 'Endure', type: 'Defense', cost: 3, attack: 0, defense: 2, ability: 'Heal 2', desc: 'Gain 2 Defense. Heal 2 HP.', cls: 'wither' },
    { id: 'WI-U01', name: 'Poison', type: 'Other', cost: 2, attack: 0, defense: 0, ability: '1 Poison', desc: 'Apply 1 Poison to target enemy.', cls: 'wither' },
    { id: 'WI-U02', name: 'Burn All', type: 'Other', cost: 3, attack: 0, defense: 0, ability: '3 dmg + 2 Burn', desc: 'Deal 3 damage + 2 Burn to all enemies.', cls: 'wither' },
    { id: 'WI-U03', name: 'Slow', type: 'Other', cost: 1, attack: 0, defense: 0, ability: 'Stun', desc: 'Draw 1 card. Target enemy stunned next turn.', cls: 'wither' },
    { id: 'WI-U04', name: 'Wither', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Heal 5, -ATK', desc: 'Heal 5 HP. Reduce enemy Attack by 1.', cls: 'wither' },
    // ##### SPROUD (12 cards) =====
    { id: 'SH-A01', name: 'Backstab', type: 'Attack', cost: 2, attack: 4, defense: 0, ability: '+1 if no block', desc: 'Deals 4 damage. +1 if target has no block.', cls: 'shroud' },
    { id: 'SH-A02', name: 'Shadow Bolt', type: 'Attack', cost: 3, attack: 5, defense: 0, ability: 'No block', desc: 'Deals 5 damage. Target cannot block this damage.', cls: 'shroud' },
    { id: 'SH-A03', name: 'Silence', type: 'Attack', cost: 1, attack: 3, defense: 0, ability: 'Silence', desc: 'Deals 3 damage. Enemy silenced 1 turn.', cls: 'shroud' },
    { id: 'SH-A04', name: 'Vengeance', type: 'Attack', cost: 3, attack: 6, defense: 0, ability: '+2 Energy', desc: 'Deals 6 damage. Gain 2 Energy.', cls: 'shroud' },
    { id: 'SH-D01', name: 'Blur', type: 'Defense', cost: 3, attack: 0, defense: 2, ability: 'Dodge next', desc: 'Gain 2 Defense. Ignore next attack.', cls: 'shroud' },
    { id: 'SH-D02', name: 'Step', type: 'Defense', cost: 2, attack: 0, defense: 3, ability: '+1 Energy', desc: 'Gain 3 Defense. Gain 1 Energy.', cls: 'shroud' },
    { id: 'SH-D03', name: 'Evasion', type: 'Defense', cost: 1, attack: 0, defense: 1, ability: 'Skip enemy', desc: 'Gain 1 Defense. Skip enemy action.', cls: 'shroud' },
    { id: 'SH-D04', name: 'Phantom', type: 'Defense', cost: 2, attack: 0, defense: 4, ability: 'Ghost 1t', desc: 'Gain 4 Defense. Ghost form for 1 turn.', cls: 'shroud' },
    { id: 'SH-U01', name: 'Dash', type: 'Other', cost: 1, attack: 0, defense: 0, ability: '+1 Energy, skip enemy', desc: 'Gain 1 Energy. Skip enemy attack.', cls: 'shroud' },
    { id: 'SH-U02', name: 'Teleport', type: 'Other', cost: 2, attack: 0, defense: 0, ability: '2 dmg', desc: 'Move to back row. Deal 2 damage.', cls: 'shroud' },
    { id: 'SH-U03', name: 'Hide', type: 'Other', cost: 2, attack: 0, defense: 0, ability: '+1 Def, draw 2', desc: 'Gain 1 Defense. Discard 1 card. Draw 2 cards.', cls: 'shroud' },
    { id: 'SH-U04', name: 'Assassinate', type: 'Other', cost: 3, attack: 0, defense: 0, ability: '8 dmg unblockable', desc: 'Deal 8 damage. Cannot be blocked this turn.', cls: 'shroud' },
];
// Starter deck for each class (card IDs)
exports.STARTER_DECKS = {
    bastion: ['BA-A01', 'BA-A03', 'BA-A03', 'BA-D01', 'BA-D01', 'BA-D03', 'BA-D03', 'BA-U01', 'BA-U02', 'BA-U03', 'BA-U04', 'BA-A03', 'BA-D03', 'BA-A01'],
    wither: ['WI-A01', 'WI-A03', 'WI-A03', 'WI-D01', 'WI-D02', 'WI-D02', 'WI-U01', 'WI-U02', 'WI-U03', 'WI-A01', 'WI-D02', 'WI-A03', 'WI-D01', 'WI-U03'],
    shroud: ['SH-A01', 'SH-A03', 'SH-A03', 'SH-D01', 'SH-D02', 'SH-D02', 'SH-U01', 'SH-U01', 'SH-U02', 'SH-A03', 'SH-D02', 'SH-A01', 'SH-U01', 'SH-D01'],
};
let _uid = 0;
function createCardInstance(def) {
    return { def, uuid: `${def.id}_${_uid++}` };
}
function getCardById(id) {
    return exports.CARD_ROSTER.find(c => c.id === id);
}
function getByClass(cls) {
    return exports.CARD_ROSTER.filter(c => c.cls === cls);
}
};

// ── module: src/svg.ts ──
__mods["src/svg.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderCardSVG = renderCardSVG;
exports.renderCardBackSVG = renderCardBackSVG;
exports.renderEnemySVG = renderEnemySVG;
const THEMES = {
    bastion: { bg1: '#0d0a14', bg2: '#1e1628', accent: '#6a9fd8', border: '#8ab4e8', cardBack1: '#0a0818', cardBack2: '#181530', glow: 'rgba(106,159,216,0.3)' },
    wither: { bg1: '#140a08', bg2: '#281610', accent: '#e89a3a', border: '#f0b860', cardBack1: '#180c08', cardBack2: '#301a10', glow: 'rgba(232,154,58,0.3)' },
    shroud: { bg1: '#0c0814', bg2: '#1e1430', accent: '#b07ae8', border: '#c898f0', cardBack1: '#100818', cardBack2: '#201535', glow: 'rgba(176,122,232,0.3)' },
};
const TYPE_COLORS = {
    Attack: { fill: '#8b2020', border: '#c44' },
    Defense: { fill: '#204080', border: '#44a' },
    Other: { fill: '#4a2060', border: '#84a' },
};
function renderCardSVG(card, hover = false, dimmed = false) {
    const t = THEMES[card.cls];
    const tc = TYPE_COLORS[card.type];
    const glow = hover ? ` drop-shadow(0 0 12px ${t.glow}) drop-shadow(0 0 4px ${t.accent})` : '';
    const op = dimmed ? '0.45' : '1';
    const gId = card.id.replace(/[^a-zA-Z0-9]/g, '');
    return `<svg viewBox="0 0 110 155" xmlns="http://www.w3.org/2000/svg" style="opacity:${op};${glow}">
    <defs>
      <linearGradient id="cBg${gId}" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="${t.bg2}"/>
        <stop offset="100%" stop-color="${t.bg1}"/>
      </linearGradient>
      <linearGradient id="cBr${gId}" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0%" stop-color="${t.border}"/>
        <stop offset="50%" stop-color="${t.accent}"/>
        <stop offset="100%" stop-color="${t.border}"/>
      </linearGradient>
      <radialGradient id="costGlow${gId}" cx="50%" cy="50%">
        <stop offset="0%" stop-color="#a855f7"/>
        <stop offset="100%" stop-color="#5b21b6"/>
      </radialGradient>
    </defs>
    <!-- Outer frame -->
    <rect x="1" y="1" width="108" height="153" rx="8" fill="url(#cBr${gId})" />
    <rect x="3" y="3" width="104" height="149" rx="6" fill="url(#cBg${gId})" />
    <!-- Inner decorative border -->
    <rect x="6" y="6" width="98" height="143" rx="4" fill="none" stroke="${t.accent}" stroke-width="0.4" opacity="0.25"/>
    <rect x="8" y="8" width="94" height="139" rx="3" fill="none" stroke="${t.accent}" stroke-width="0.2" opacity="0.15"/>

    <!-- Corner flourishes per class -->
    ${getClassCorners(card.cls)}

    <!-- Top bar: type badge + name + cost -->
    <rect x="8" y="10" width="32" height="12" rx="2" fill="${tc.fill}" stroke="${tc.border}" stroke-width="0.5"/>
    <text x="24" y="19" text-anchor="middle" fill="#fff" font-size="7" font-family="Georgia" font-weight="bold">${card.type}</text>

    <!-- Name -->
    <text x="55" y="19" text-anchor="middle" fill="${t.border}" font-size="9" font-family="Georgia" font-weight="bold" letter-spacing="0.5">${card.name}</text>

    <!-- Cost orb -->
    <circle cx="98" cy="16" r="9" fill="url(#costGlow${gId})" stroke="#c084fc" stroke-width="1"/>
    <circle cx="98" cy="16" r="7" fill="#1a1028"/>
    <text x="98" y="20" text-anchor="middle" fill="#e9d5ff" font-size="9" font-family="Georgia" font-weight="bold">${card.cost}</text>

    <!-- Art placeholder area -->
    <rect x="14" y="30" width="82" height="56" rx="3" fill="none" stroke="${t.accent}" stroke-width="0.5" opacity="0.2"/>
    ${getClassArt(card.cls, gId)}

    <!-- Divider line -->
    <line x1="12" y1="90" x2="98" y2="90" stroke="${t.accent}" stroke-width="0.5" opacity="0.3"/>
    <line x1="12" y1="91" x2="98" y2="91" stroke="${t.accent}" stroke-width="0.3" opacity="0.15"/>

    <!-- Stats row -->
    <text x="16" y="102" fill="#c44" font-size="9" font-family="Georgia" font-weight="bold">${card.attack > 0 ? '⚔ ' + card.attack : ''}</text>
    <text x="62" y="102" fill="#68a" font-size="9" font-family="Georgia" font-weight="bold">${card.defense > 0 ? '🛡 ' + card.defense : ''}</text>

    <!-- Ability tag -->
    ${card.ability ? `<rect x="30" y="106" width="50" height="10" rx="3" fill="${t.accent}" opacity="0.1"/>
    <text x="55" y="114" text-anchor="middle" fill="${t.accent}" font-size="6" font-family="Georgia" font-style="italic">${card.ability}</text>` : ''}

    <!-- Description -->
    <text x="55" y="125" text-anchor="middle" fill="#7a6f60" font-size="5.5" font-family="Georgia">
      <tspan x="55" dy="0">${card.desc.substring(0, 35)}</tspan>
      <tspan x="55" dy="8">${card.desc.substring(35, 70)}</tspan>
    </text>

    <!-- Bottom decorative line -->
    <line x1="30" y1="142" x2="80" y2="142" stroke="${t.accent}" stroke-width="0.3" opacity="0.2"/>
    <text x="55" y="149" text-anchor="middle" fill="${t.accent}" font-size="4.5" font-family="monospace" opacity="0.3">${card.id}</text>
  </svg>`;
}
function getClassCorners(cls) {
    const t = THEMES[cls];
    switch (cls) {
        case 'bastion':
            return `
        <path d="M8 8 L20 8 L20 10 L10 10 L10 20 L8 20 Z" fill="${t.border}" opacity="0.2"/>
        <path d="M102 8 L90 8 L90 10 L100 10 L100 20 L102 20 Z" fill="${t.border}" opacity="0.2"/>
        <path d="M8 147 L20 147 L20 145 L10 145 L10 135 L8 135 Z" fill="${t.border}" opacity="0.2"/>
        <path d="M102 147 L90 147 L90 145 L100 145 L100 135 L102 135 Z" fill="${t.border}" opacity="0.2"/>
      `;
        case 'wither':
            return `
        <path d="M10 10 Q16 6 20 14 Q14 12 10 16Z" fill="${t.border}" opacity="0.2"/>
        <path d="M100 10 Q94 6 90 14 Q96 12 100 16Z" fill="${t.border}" opacity="0.2"/>
        <path d="M10 145 Q16 149 20 141 Q14 143 10 139Z" fill="${t.border}" opacity="0.2"/>
        <path d="M100 145 Q94 149 90 141 Q96 143 100 139Z" fill="${t.border}" opacity="0.2"/>
      `;
        default:
            return `
        <line x1="8" y1="18" x2="18" y2="8" stroke="${t.border}" stroke-width="1.5" opacity="0.25"/>
        <line x1="102" y1="18" x2="92" y2="8" stroke="${t.border}" stroke-width="1.5" opacity="0.25"/>
        <line x1="8" y1="137" x2="18" y2="147" stroke="${t.border}" stroke-width="1.5" opacity="0.25"/>
        <line x1="102" y1="137" x2="92" y2="147" stroke="${t.border}" stroke-width="1.5" opacity="0.25"/>
      `;
    }
}
function getClassArt(cls, gid) {
    const t = THEMES[cls];
    switch (cls) {
        case 'bastion':
            return `
        <path d="M55 36 L70 43 L70 62 L55 72 L40 62 L40 43 Z" fill="none" stroke="${t.accent}" stroke-width="1.5" opacity="0.25"/>
        <path d="M55 42 L64 47 L64 58 L55 64 L46 58 L46 47 Z" fill="${t.accent}" opacity="0.08" stroke="${t.accent}" stroke-width="0.5"/>
        <path d="M55 48 L59 51 L59 56 L55 58 L51 56 L51 51 Z" fill="${t.accent}" opacity="0.12"/>
      `;
        case 'wither':
            return `
        <path d="M55 38 Q48 50 55 58 Q48 66 55 74 Q62 66 55 58 Q62 50 55 38Z" fill="${t.accent}" opacity="0.08" stroke="${t.accent}" stroke-width="1"/>
        <path d="M55 44 Q51 52 55 56 Q59 52 55 44Z" fill="${t.accent}" opacity="0.12"/>
        <circle cx="55" cy="56" r="2" fill="${t.accent}" opacity="0.2"/>
      `;
        default:
            return `
        <ellipse cx="55" cy="58" rx="18" ry="11" fill="none" stroke="${t.accent}" stroke-width="0.8" opacity="0.2"/>
        <ellipse cx="55" cy="58" rx="10" ry="6" fill="none" stroke="${t.accent}" stroke-width="0.5" opacity="0.15"/>
        <circle cx="55" cy="58" r="4" fill="${t.accent}" opacity="0.08"/>
        <circle cx="55" cy="58" r="2" fill="${t.accent}" opacity="0.15"/>
        <line x1="37" y1="58" x2="43" y2="58" stroke="${t.accent}" stroke-width="0.3" opacity="0.15"/>
        <line x1="67" y1="58" x2="73" y2="58" stroke="${t.accent}" stroke-width="0.3" opacity="0.15"/>
      `;
    }
}
function renderCardBackSVG(cls) {
    const t = THEMES[cls];
    const gId = 'cb_' + cls;
    return `<svg viewBox="0 0 110 155" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <linearGradient id="cbg${gId}" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="${t.cardBack2}"/>
        <stop offset="100%" stop-color="${t.cardBack1}"/>
      </linearGradient>
    </defs>
    <rect x="1" y="1" width="108" height="153" rx="8" fill="${t.border}" opacity="0.4"/>
    <rect x="3" y="3" width="104" height="149" rx="6" fill="url(#cbg${gId})"/>
    <rect x="8" y="8" width="94" height="139" rx="4" fill="none" stroke="${t.accent}" stroke-width="0.5" opacity="0.2"/>
    ${getBackPattern(cls, gId)}
  </svg>`;
}
function getBackPattern(cls, gid) {
    const t = THEMES[cls];
    switch (cls) {
        case 'bastion':
            return `
        <g opacity="0.15">
          <path d="M55 25 L72 38 L72 65 L55 78 L38 65 L38 38 Z" fill="none" stroke="${t.border}" stroke-width="1.5"/>
          <path d="M55 35 L65 42 L65 58 L55 66 L45 58 L45 42 Z" fill="none" stroke="${t.border}" stroke-width="1"/>
          <path d="M55 44 L60 48 L60 55 L55 59 L50 55 L50 48 Z" fill="${t.accent}" opacity="0.3"/>
        </g>
        <rect x="14" y="14" width="82" height="127" rx="3" fill="none" stroke="${t.accent}" stroke-width="0.3" opacity="0.1"/>
      `;
        case 'wither':
            return `
        <g opacity="0.15">
          <path d="M55 25 Q42 40 55 55 Q42 70 55 85 Q68 70 55 55 Q68 40 55 25Z" fill="none" stroke="${t.border}" stroke-width="1.5"/>
          <path d="M55 35 Q48 45 55 55 Q48 65 55 75" fill="none" stroke="${t.accent}" stroke-width="1"/>
          <circle cx="55" cy="55" r="4" fill="${t.accent}" opacity="0.3"/>
        </g>
        <rect x="14" y="14" width="82" height="127" rx="3" fill="none" stroke="${t.accent}" stroke-width="0.3" opacity="0.1"/>
      `;
        default:
            return `
        <g opacity="0.15">
          <ellipse cx="55" cy="55" rx="22" ry="14" fill="none" stroke="${t.border}" stroke-width="1.5"/>
          <ellipse cx="55" cy="55" rx="14" ry="9" fill="none" stroke="${t.accent}" stroke-width="1"/>
          <circle cx="55" cy="55" r="5" fill="${t.accent}" opacity="0.3"/>
          <circle cx="55" cy="55" r="2" fill="${t.border}" opacity="0.5"/>
        </g>
        <rect x="14" y="14" width="82" height="127" rx="3" fill="none" stroke="${t.accent}" stroke-width="0.3" opacity="0.1"/>
      `;
    }
}
function renderEnemySVG(type, hurt = false) {
    const palette = {
        goblin: { body: '#4a7a2a', light: '#6aaa4a', eye: '#ff4444', accent: '#88cc55', glow: 'rgba(80,160,40,0.4)' },
        rat: { body: '#6a5a4a', light: '#8a7a6a', eye: '#ff6600', accent: '#9a8a5a', glow: 'rgba(140,120,80,0.3)' },
        vampire: { body: '#2a1a4a', light: '#4a2a7a', eye: '#ff0040', accent: '#7a3aaa', glow: 'rgba(80,40,120,0.4)' },
        ogre: { body: '#3a5a2a', light: '#5a8a3a', eye: '#ffaa00', accent: '#6a8a4a', glow: 'rgba(60,120,40,0.3)' },
        voidwarrior: { body: '#1a0a2a', light: '#3a1a5a', eye: '#aa00ff', accent: '#5a2a8a', glow: 'rgba(100,20,180,0.5)' },
        cultist: { body: '#2a0a1a', light: '#5a1a3a', eye: '#ff44ff', accent: '#7a2a5a', glow: 'rgba(150,20,100,0.4)' },
        boss1: { body: '#3a2a4a', light: '#5a4a7a', eye: '#ffcc00', accent: '#8a6aaa', glow: 'rgba(120,80,160,0.5)' },
        boss2: { body: '#4a0a1a', light: '#7a1a2a', eye: '#ff0030', accent: '#aa2a4a', glow: 'rgba(160,20,40,0.5)' },
        boss3: { body: '#0a0018', light: '#2a0a40', eye: '#cc00ff', accent: '#4a1a6a', glow: 'rgba(120,0,180,0.6)' },
    };
    const p = palette[type] || palette.goblin;
    const body = hurt ? '#cc2222' : p.body;
    const light = hurt ? '#ff6644' : p.light;
    const eye = hurt ? '#fff' : p.eye;
    let svg = '';
    switch (type) {
        case 'goblin':
            svg = `
        <ellipse cx="60" cy="72" rx="22" ry="26" fill="${body}" stroke="${light}" stroke-width="0.5"/>
        <ellipse cx="60" cy="44" rx="18" ry="16" fill="${body}"/>
        <ellipse cx="60" cy="44" rx="16" ry="14" fill="${body}"/>
        <circle cx="52" cy="41" r="3.5" fill="${eye}"/><circle cx="52" cy="41" r="1.5" fill="#000"/>
        <circle cx="68" cy="41" r="3.5" fill="${eye}"/><circle cx="68" cy="41" r="1.5" fill="#000"/>
        <path d="M55 53 Q60 57 65 53" fill="none" stroke="${p.accent}" stroke-width="1.5"/>
        <path d="M42 36 L32 22 L40 32Z" fill="${body}"/><path d="M78 36 L88 22 L80 32Z" fill="${body}"/>
        <path d="M46 96 L43 115 L50 115 L53 96" fill="${body}"/><path d="M67 96 L64 115 L71 115 L74 96" fill="${body}"/>
        <path d="M38 58 L26 65 L30 72 L42 65Z" fill="${body}"/>
        <path d="M82 58 L94 65 L90 72 L78 65Z" fill="${body}"/>
      `;
            break;
        case 'rat':
            svg = `
        <ellipse cx="60" cy="68" rx="28" ry="20" fill="${body}"/>
        <ellipse cx="35" cy="55" rx="14" ry="11" fill="${body}"/>
        <circle cx="28" cy="49" r="4.5" fill="${eye}"/><circle cx="28" cy="49" r="2" fill="#000"/>
        <path d="M20 52 Q14 44 16 36 Q20 40 26 46Z" fill="${body}"/>
        <path d="M20 52 Q14 56 16 64 Q20 58 26 54Z" fill="${body}"/>
        <path d="M88 68 Q100 62 102 72 Q104 78 94 80" fill="none" stroke="${body}" stroke-width="5" stroke-linecap="round"/>
        <rect x="40" y="86" width="7" height="16" rx="2" fill="${body}"/>
        <rect x="50" y="86" width="7" height="16" rx="2" fill="${body}"/>
        <rect x="60" y="86" width="7" height="16" rx="2" fill="${body}"/>
        <rect x="70" y="86" width="7" height="16" rx="2" fill="${body}"/>
      `;
            break;
        case 'vampire':
            svg = `
        <path d="M44 100 L44 60 L54 42 L60 34 L66 42 L76 60 L76 100Z" fill="${body}"/>
        <ellipse cx="60" cy="38" rx="13" ry="15" fill="${body}"/>
        <circle cx="53" cy="36" r="3.5" fill="${eye}"/><circle cx="67" cy="36" r="3.5" fill="${eye}"/>
        <path d="M55 49 L60 53 L65 49" fill="none" stroke="${eye}" stroke-width="1.5"/>
        <path d="M44 55 L28 70 L42 64Z" fill="${body}"/><path d="M76 55 L92 70 L78 64Z" fill="${body}"/>
        <path d="M46 100 L44 118 L52 118 L54 100" fill="${body}"/><path d="M66 100 L64 118 L72 118 L74 100" fill="${body}"/>
        <path d="M50 25 L55 32 L57 22 L63 22 L65 32 L70 25" fill="none" stroke="${p.accent}" stroke-width="1"/>
      `;
            break;
        case 'ogre':
            svg = `
        <ellipse cx="60" cy="74" rx="30" ry="24" fill="${body}"/>
        <ellipse cx="60" cy="48" rx="22" ry="17" fill="${body}"/>
        <circle cx="50" cy="44" r="4.5" fill="${eye}"/><circle cx="70" cy="44" r="4.5" fill="${eye}"/>
        <circle cx="50" cy="44" r="2" fill="#000"/><circle cx="70" cy="44" r="2" fill="#000"/>
        <path d="M52 56 Q60 62 68 56" fill="none" stroke="${p.accent}" stroke-width="2"/>
        <rect x="28" y="96" width="14" height="20" rx="3" fill="${body}"/><rect x="78" y="96" width="14" height="20" rx="3" fill="${body}"/>
        <rect x="36" y="93" width="10" height="12" rx="2" fill="${p.accent}" opacity="0.4"/>
        <rect x="74" y="93" width="10" height="12" rx="2" fill="${p.accent}" opacity="0.4"/>
        <path d="M38 44 L32 34 L42 38Z" fill="${body}"/><path d="M82 44 L88 34 L78 38Z" fill="${body}"/>
      `;
            break;
        case 'voidwarrior':
            svg = `
        <path d="M42 100 L40 60 L52 38 L60 28 L68 38 L80 60 L78 100Z" fill="${body}"/>
        <ellipse cx="60" cy="34" rx="15" ry="13" fill="${body}"/>
        <circle cx="53" cy="32" r="5" fill="${eye}"/><circle cx="67" cy="32" r="5" fill="${eye}"/>
        <circle cx="53" cy="32" r="2" fill="#fff"/><circle cx="67" cy="32" r="2" fill="#fff"/>
        <path d="M42 58 L24 42 L28 52Z" fill="${body}"/><path d="M78 58 L96 42 L92 52Z" fill="${body}"/>
        <rect x="46" y="100" width="12" height="18" rx="3" fill="${body}"/><rect x="62" y="100" width="12" height="18" rx="3" fill="${body}"/>
        <circle cx="60" cy="62" r="6" fill="none" stroke="${eye}" stroke-width="0.8" opacity="0.4"/>
        <line x1="60" y1="28" x2="60" y2="20" stroke="${eye}" stroke-width="1" opacity="0.3"/>
      `;
            break;
        case 'cultist':
            svg = `
        <path d="M44 100 L44 56 L56 34 L60 26 L64 34 L76 56 L76 100Z" fill="${body}"/>
        <ellipse cx="60" cy="34" rx="11" ry="13" fill="${body}"/>
        <circle cx="54" cy="32" r="3.5" fill="${eye}"/><circle cx="66" cy="32" r="3.5" fill="${eye}"/>
        <path d="M55 46 Q60 50 65 46" fill="none" stroke="${p.accent}" stroke-width="1.5"/>
        <path d="M34 28 L44 38 L40 30Z" fill="${p.accent}" opacity="0.5"/>
        <path d="M86 28 L76 38 L80 30Z" fill="${p.accent}" opacity="0.5"/>
        <path d="M55 14 L60 20 L65 14 L62 8 L58 8Z" fill="${p.accent}" opacity="0.6"/>
        <rect x="46" y="100" width="12" height="18" rx="3" fill="${body}"/><rect x="62" y="100" width="12" height="18" rx="3" fill="${body}"/>
      `;
            break;
        case 'boss1':
            svg = `
        <path d="M36 110 L34 56 L48 30 L60 18 L72 30 L86 56 L84 110Z" fill="${body}"/>
        <ellipse cx="60" cy="32" rx="19" ry="16" fill="${body}"/>
        <circle cx="50" cy="29" r="5.5" fill="${eye}"/><circle cx="70" cy="29" r="5.5" fill="${eye}"/>
        <circle cx="50" cy="29" r="2.5" fill="#fff"/><circle cx="70" cy="29" r="2.5" fill="#fff"/>
        <path d="M42 47 L55 55 L60 50 L65 55 L78 47" fill="none" stroke="${eye}" stroke-width="2.5"/>
        <path d="M36 54 L18 34 L24 46Z" fill="${body}"/><path d="M84 54 L102 34 L96 46Z" fill="${body}"/>
        <rect x="40" y="110" width="14" height="12" rx="3" fill="${body}"/><rect x="66" y="110" width="14" height="12" rx="3" fill="${body}"/>
        <rect x="38" y="56" width="44" height="10" rx="3" fill="${p.accent}" opacity="0.25"/>
        <path d="M38 22 L60 10 L82 22" fill="none" stroke="${eye}" stroke-width="1.5" opacity="0.4"/>
        <path d="M44 70 L60 62 L76 70 L72 85 L60 92 L48 85Z" fill="${p.accent}" opacity="0.15" stroke="${p.accent}" stroke-width="0.5"/>
      `;
            break;
        case 'boss2':
            svg = `
        <path d="M38 110 L34 50 L48 24 L60 12 L72 24 L86 50 L82 110Z" fill="${body}"/>
        <ellipse cx="60" cy="30" rx="17" ry="15" fill="${body}"/>
        <circle cx="50" cy="28" r="5.5" fill="${eye}"/><circle cx="70" cy="28" r="5.5" fill="${eye}"/>
        <circle cx="50" cy="28" r="2.5" fill="#fff"/><circle cx="70" cy="28" r="2.5" fill="#fff"/>
        <path d="M52 43 Q60 50 68 43" fill="none" stroke="${eye}" stroke-width="2"/>
        <path d="M34 48 L14 36 L20 50Z" fill="${body}"/><path d="M86 48 L106 36 L100 50Z" fill="${body}"/>
        <rect x="44" y="110" width="14" height="12" rx="3" fill="${body}"/><rect x="62" y="110" width="14" height="12" rx="3" fill="${body}"/>
        <circle cx="60" cy="62" r="14" fill="none" stroke="${eye}" stroke-width="0.8" opacity="0.3">
          <animateTransform attributeName="transform" type="rotate" values="0 60 62;360 60 62" dur="3s" repeatCount="indefinite"/>
        </circle>
        <circle cx="60" cy="62" r="8" fill="${eye}" opacity="0.1">
          <animate attributeName="r" values="6;10;6" dur="2s" repeatCount="indefinite"/>
        </circle>
      `;
            break;
        case 'boss3':
            svg = `
        <path d="M40 110 L34 48 L48 18 L60 4 L72 18 L86 48 L80 110Z" fill="${body}"/>
        <ellipse cx="60" cy="26" rx="15" ry="13" fill="${body}"/>
        <circle cx="53" cy="24" r="5.5" fill="${eye}"/><circle cx="67" cy="24" r="5.5" fill="${eye}"/>
        <circle cx="53" cy="24" r="2.5" fill="#fff"/><circle cx="67" cy="24" r="2.5" fill="#fff"/>
        <path d="M48 38 L60 33 L72 38" fill="none" stroke="${eye}" stroke-width="1.5"/>
        <path d="M34 46 L10 30 L16 44Z" fill="${body}"/><path d="M86 46 L110 30 L104 44Z" fill="${body}"/>
        <path d="M40 108 L34 125 L52 125 L52 108" fill="${body}"/><path d="M68 108 L68 125 L86 125 L80 108" fill="${body}"/>
        <path d="M48 14 L60 4 L72 14" fill="none" stroke="${eye}" stroke-width="1" opacity="0.5"/>
        <circle cx="60" cy="58" r="16" fill="none" stroke="${eye}" stroke-width="0.5" opacity="0.25">
          <animateTransform attributeName="transform" type="rotate" values="0 60 58;-360 60 58" dur="5s" repeatCount="indefinite"/>
        </circle>
        <circle cx="60" cy="58" r="9" fill="${eye}" opacity="0.08">
          <animate attributeName="r" values="7;11;7" dur="3s" repeatCount="indefinite"/>
        </circle>
        <circle cx="60" cy="58" r="3" fill="${eye}" opacity="0.15"/>
      `;
            break;
    }
    return `<svg viewBox="0 0 120 130" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <filter id="eglow_${type}">
        <feGaussianBlur stdDeviation="2" result="blur"/>
        <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
    </defs>
    <g filter="url(#eglow_${type})">${svg}</g>
  </svg>`;
}
};

// ── module: src/enemies.ts ──
__mods["src/enemies.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BOSS_ROSTER = exports.ENEMY_ROSTER = void 0;
exports.getRandomEnemy = getRandomEnemy;
exports.getBoss = getBoss;
exports.ENEMY_ROSTER = {
    1: [
        { name: 'Goblin Scout', hp: 20, maxHp: 20, damage: 3, block: 0, floor: 1, isBoss: false, sprite: 'goblin' },
        { name: 'Goblin Brute', hp: 20, maxHp: 20, damage: 3, block: 0, floor: 1, isBoss: false, sprite: 'goblin' },
        { name: 'Sewer Rat', hp: 20, maxHp: 20, damage: 3, block: 0, floor: 1, isBoss: false, sprite: 'rat' },
    ],
    2: [
        { name: 'Vampire Knight', hp: 30, maxHp: 30, damage: 5, block: 0, floor: 2, isBoss: false, sprite: 'vampire' },
        { name: 'Ogre Warrior', hp: 30, maxHp: 30, damage: 5, block: 0, floor: 2, isBoss: false, sprite: 'ogre' },
    ],
    3: [
        { name: 'Void Warrior', hp: 40, maxHp: 40, damage: 8, block: 0, floor: 3, isBoss: false, sprite: 'voidwarrior' },
        { name: 'Void Cultist', hp: 40, maxHp: 40, damage: 8, block: 0, floor: 3, isBoss: false, sprite: 'cultist' },
    ],
};
exports.BOSS_ROSTER = {
    1: { name: '⚔ The Gatekeeper', hp: 80, maxHp: 80, damage: 15, block: 0, floor: 1, isBoss: true, bossId: 1, sprite: 'boss1' },
    2: { name: '☠ The Corrupted', hp: 80, maxHp: 80, damage: 15, block: 0, floor: 2, isBoss: true, bossId: 2, sprite: 'boss2' },
    3: { name: '🌀 The Void Lord', hp: 80, maxHp: 80, damage: 15, block: 0, floor: 3, isBoss: true, bossId: 3, sprite: 'boss3' },
};
function getRandomEnemy(floor) {
    const enemies = exports.ENEMY_ROSTER[floor] || exports.ENEMY_ROSTER[1];
    return enemies[Math.floor(Math.random() * enemies.length)];
}
function getBoss(floor) {
    return exports.BOSS_ROSTER[floor] || exports.BOSS_ROSTER[1];
}
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createPlayer = createPlayer;
exports.initGameState = initGameState;
exports.createEnemyInstance = createEnemyInstance;
exports.startNextCombat = startNextCombat;
exports.startPlayerTurn = startPlayerTurn;
exports.playCard = playCard;
exports.endPlayerTurn = endPlayerTurn;
exports.showShop = showShop;
exports.buyCard = buyCard;
exports.getShopPrice = getShopPrice;
exports.advanceFloor = advanceFloor;
const types_1 = require("./types");
const cards_1 = require("./cards");
const enemies_1 = require("./enemies");
function createPlayer(cls) {
    const info = types_1.CLASS_INFO[cls];
    const starterIds = cards_1.STARTER_DECKS[cls];
    const instances = starterIds.map(id => {
        const def = (0, cards_1.getCardById)(id);
        return (0, cards_1.createCardInstance)(def);
    });
    const inventory = {};
    for (const inst of instances) {
        inventory[inst.def.id] = (inventory[inst.def.id] || 1);
    }
    return {
        cls,
        hp: info.hp,
        maxHp: info.hp,
        energy: info.energy,
        maxEnergy: info.energy,
        handSize: 4,
        gold: 0,
        block: 0,
        deck: starterIds.map(id => (0, cards_1.getCardById)(id)),
        inventory,
        hand: [],
        discard: [],
        drawPile: shuffle([...instances]),
    };
}
function shuffle(arr) {
    const a = [...arr];
    for (let i = a.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [a[i], a[j]] = [a[j], a[i]];
    }
    return a;
}
function initGameState(cls) {
    const player = createPlayer(cls);
    const enemyDef = (0, enemies_1.getRandomEnemy)(1);
    return {
        screen: 'combat',
        floor: 1,
        room: 1,
        player,
        enemy: createEnemyInstance(enemyDef),
        isPlayerTurn: true,
        log: ['Encountered ' + enemyDef.name + '!'],
        shopCards: [],
        turnStarted: false,
    };
}
function createEnemyInstance(def) {
    return {
        def,
        hp: def.hp,
        maxHp: def.maxHp,
        damage: def.damage,
        block: 0,
        statuses: [],
    };
}
// Start a new combat encounter
function startNextCombat(state) {
    // Room 4 = boss room
    if (state.room === 4) {
        const bossDef = (0, enemies_1.getBoss)(state.floor);
        state.enemy = createEnemyInstance(bossDef);
        state.log = [`BOSS: ${bossDef.name} appears!`];
    }
    else {
        const enemyDef = (0, enemies_1.getRandomEnemy)(state.floor);
        state.enemy = createEnemyInstance(enemyDef);
        state.log = [`Encountered ${enemyDef.name}!`];
    }
    state.screen = 'combat';
    state.isPlayerTurn = true;
    state.turnStarted = false;
    state.player.block = 0;
}
// Begin a player turn: draw cards, recover energy
function startPlayerTurn(state) {
    state.isPlayerTurn = true;
    state.turnStarted = true;
    state.player.block = 0; // Block expires at turn start
    state.player.energy = Math.min(state.player.energy + 1, state.player.maxEnergy);
    // Draw cards up to hand size
    while (state.player.hand.length < state.player.handSize && state.player.drawPile.length > 0) {
        state.player.hand.push(state.player.drawPile.pop());
    }
    // If draw pile empty, shuffle discard
    if (state.player.drawPile.length === 0 && state.player.discard.length > 0) {
        state.player.drawPile = shuffle([...state.player.discard]);
        state.player.discard = [];
        while (state.player.hand.length < state.player.handSize && state.player.drawPile.length > 0) {
            state.player.hand.push(state.player.drawPile.pop());
        }
    }
}
function playCard(state, cardIdx) {
    if (!state.isPlayerTurn || !state.enemy || state.enemy.hp <= 0)
        return false;
    const card = state.player.hand[cardIdx];
    if (!card)
        return false;
    if (state.player.energy < card.def.cost) {
        state.log.push('Not enough energy!');
        return false;
    }
    state.player.energy -= card.def.cost;
    state.player.hand.splice(cardIdx, 1);
    switch (card.def.type) {
        case 'Attack':
            resolveAttack(state, card.def);
            break;
        case 'Defense':
            resolveDefense(state, card.def);
            break;
        case 'Other':
            resolveOther(state, card.def);
            break;
    }
    state.player.discard.push(card);
    state.log.push(`Played ${card.def.name}`);
    if (state.enemy.hp <= 0) {
        onEnemyDeath(state);
    }
    return true;
}
function resolveAttack(state, card) {
    const enemy = state.enemy;
    let damage = card.attack;
    // Backstab bonus
    if (card.id === 'SH-A01' && enemy.block <= 0) {
        damage += 1;
        state.log.push('Backstab bonus!');
    }
    // Unblockable damage
    const unblocked = card.id === 'SH-A02' || card.id === 'SH-U04';
    if (unblocked) {
        enemy.hp -= damage;
        state.log.push(`${card.name} deals ${damage} unblockable damage!`);
    }
    else {
        if (enemy.block > 0) {
            const blocked = Math.min(damage, enemy.block);
            enemy.block -= blocked;
            damage -= blocked;
            state.log.push(`${enemy.def.name} blocks ${blocked} damage!`);
        }
        enemy.hp -= damage;
        if (damage > 0)
            state.log.push(`${card.name} deals ${damage} damage!`);
    }
    // Burn from attack cards
    if (card.ability.includes('Burn') || card.ability.includes('burn')) {
        const burnVal = parseInt(card.ability.match(/\d+/)?.[0] || '1');
        const existingBurn = enemy.statuses.find(s => s.type === 'burn');
        if (existingBurn) {
            existingBurn.turns = (existingBurn.turns || 0) + burnVal;
        }
        else {
            enemy.statuses.push({ type: 'burn', value: 1, turns: burnVal });
        }
        state.log.push(`Applied ${burnVal} burn to ${enemy.def.name}!`);
    }
    // Silence
    if (card.ability.includes('Silence')) {
        if (!enemy.statuses.find(s => s.type === 'silence')) {
            enemy.statuses.push({ type: 'silence', value: 1, turns: 1 });
            state.log.push(`${enemy.def.name} silenced!`);
        }
    }
    // Vengeance energy
    if (card.id === 'SH-A04') {
        state.player.energy += 2;
        state.log.push('Gained 2 Energy!');
    }
}
function resolveDefense(state, card) {
    state.player.block += card.defense;
    state.log.push(`Gained ${card.defense} Block!`);
    // Iron Skin: discard 1
    if (card.id === 'BA-D02' && state.player.hand.length > 0) {
        const d = state.player.hand.pop();
        state.player.discard.push(d);
        state.log.push(`Discarded ${d.def.name}`);
    }
    // Taunt: stun
    if (card.ability.includes('Stun') && state.enemy) {
        if (!state.enemy.statuses.find(s => s.type === 'stun')) {
            state.enemy.statuses.push({ type: 'stun', value: 1, turns: 1 });
            state.log.push(`${state.enemy.def.name} stunned!`);
        }
    }
    // Energy gain
    if (card.ability.includes('+1 Energy') || card.ability.includes('Energy')) {
        state.player.energy += 1;
        state.log.push('Gained 1 Energy!');
    }
    // Heal
    if (card.ability.includes('Heal 2')) {
        state.player.hp = Math.min(state.player.maxHp, state.player.hp + 2);
        state.log.push('Healed 2 HP!');
    }
}
function resolveOther(state, card) {
    const enemy = state.enemy;
    if (card.name === 'Search') {
        if (state.player.drawPile.length > 0) {
            state.player.hand.push(state.player.drawPile.pop());
            state.log.push('Drew 1 card!');
        }
    }
    if (card.name === 'Rally') {
        if (state.player.drawPile.length > 0) {
            state.player.hand.push(state.player.drawPile.pop());
            state.log.push('Drew 1 card!');
        }
        state.player.energy += 1;
        state.log.push('Gained 1 Energy!');
    }
    if (card.name === 'Repair') {
        state.player.hp = Math.min(state.player.maxHp, state.player.hp + 6);
        state.log.push('Healed 6 HP!');
    }
    if (card.name === 'Wither') {
        state.player.hp = Math.min(state.player.maxHp, state.player.hp + 5);
        state.log.push('Healed 5 HP!');
        enemy.damage = Math.max(0, enemy.damage - 1);
        state.log.push(`${enemy.def.name} ATK reduced!`);
    }
    if (card.name === 'Loot') {
        state.player.gold += 5;
        state.log.push('Gained 5 Gold!');
    }
    if (card.name === 'Poison') {
        enemy.statuses.push({ type: 'poison', value: 1, turns: 3 });
        state.log.push('Applied Poison!');
    }
    if (card.name === 'Burn All') {
        enemy.hp -= 3;
        enemy.statuses.push({ type: 'burn', value: 2, turns: 2 });
        state.log.push('Dealt 3 + 2 Burn!');
    }
    if (card.name === 'Slow') {
        if (!enemy.statuses.find(s => s.type === 'stun')) {
            enemy.statuses.push({ type: 'stun', value: 1, turns: 1 });
            state.log.push(`${enemy.def.name} stunned!`);
        }
        if (state.player.drawPile.length > 0) {
            state.player.hand.push(state.player.drawPile.pop());
            state.log.push('Drew 1 card!');
        }
    }
    if (card.name === 'Dash') {
        state.player.energy += 1;
        enemy.statuses.push({ type: 'stun', value: 1, turns: 1 });
        state.log.push('Gained 1 Energy, enemy skipped!');
    }
    if (card.name === 'Evasion') {
        state.player.block += 1;
        enemy.statuses.push({ type: 'stun', value: 1, turns: 1 });
        state.log.push('Gained 1 Block, enemy skipped!');
    }
    if (card.name === 'Teleport') {
        enemy.hp -= 2;
        state.log.push('Teleported! Dealt 2 damage!');
    }
    if (card.name === 'Hide') {
        state.player.block += 1;
        if (state.player.hand.length > 0) {
            const d = state.player.hand.pop();
            state.player.discard.push(d);
        }
        for (let i = 0; i < 2; i++) {
            if (state.player.drawPile.length > 0) {
                state.player.hand.push(state.player.drawPile.pop());
            }
        }
        state.log.push('Hidden! Drew 2 cards!');
    }
    if (card.name === 'Assassinate') {
        enemy.hp -= 8;
        state.log.push('Assassinate deals 8 damage!');
    }
    if (card.name === 'Blur') {
        // Ignore next attack = stun enemy
        enemy.statuses.push({ type: 'stun', value: 1, turns: 1 });
    }
    if (card.name === 'Phantom') {
        // Ghost form = stun enemy for 1 turn
        enemy.statuses.push({ type: 'stun', value: 1, turns: 1 });
    }
}
function onEnemyDeath(state) {
    const enemy = state.enemy;
    const goldReward = enemy.def.isBoss ? 15 : 5;
    state.player.gold += goldReward;
    state.log.push(`${enemy.def.name} defeated! +${goldReward} Gold`);
    state.enemy = null;
    state.room++;
    // Room 5 = floor cleared (boss at room 4 defeated)
    if (state.room > 4) {
        if (state.floor >= 3) {
            state.screen = 'victory';
            state.log.push('The Void Lord has been vanquished!');
        }
        else {
            state.screen = 'floorClear';
            state.log.push(`Floor ${state.floor} cleared!`);
        }
        return;
    }
    // After defeating a boss (room 4 was boss), show shop
    if (enemy.def.isBoss) {
        showShop(state);
        return;
    }
    // Room 2 beaten -> shop
    if (state.room === 3) {
        showShop(state);
        return;
    }
    // Otherwise: next combat encounter
    startNextCombat(state);
    startPlayerTurn(state);
}
function endPlayerTurn(state) {
    if (!state.isPlayerTurn || !state.enemy)
        return;
    state.isPlayerTurn = false;
    // Discard hand
    state.player.discard.push(...state.player.hand);
    state.player.hand = [];
    // Enemy turn
    executeEnemyTurn(state);
    if (state.player.hp <= 0) {
        state.screen = 'gameover';
        state.log.push('You have fallen!');
        return;
    }
    // Start new player turn
    startPlayerTurn(state);
}
function executeEnemyTurn(state) {
    const enemy = state.enemy;
    const player = state.player;
    // Apply burn/poison DoT on enemy
    for (const s of enemy.statuses) {
        if (s.type === 'burn' && (s.turns || 0) > 0) {
            enemy.hp -= 1;
            state.log.push(`${enemy.def.name} takes 1 burn damage!`);
        }
        if (s.type === 'poison' && (s.turns || 0) > 0) {
            enemy.hp -= 1;
            state.log.push(`${enemy.def.name} takes 1 poison damage!`);
        }
    }
    // Tick down statuses
    for (const s of enemy.statuses) {
        if (s.turns !== undefined)
            s.turns -= 1;
    }
    enemy.statuses = enemy.statuses.filter(s => (s.turns || 0) > 0);
    // Check DoT death
    if (enemy.hp <= 0) {
        onEnemyDeath(state);
        return;
    }
    // Boss mechanics
    if (enemy.def.isBoss) {
        if (enemy.def.bossId === 1) {
            if (enemy.hp >= 20) {
                enemy.block += 5;
                state.log.push('The Gatekeeper raises shields! +5 Block');
            }
        }
        else if (enemy.def.bossId === 2) {
            if (player.energy < 3) {
                let dmg = 20;
                if (player.block > 0) {
                    const blocked = Math.min(dmg, player.block);
                    player.block -= blocked;
                    dmg -= blocked;
                }
                player.hp -= dmg;
                state.log.push(`The Corrupted deals ${dmg} burst damage!`);
                return; // Corrupted used its special, skip normal attack
            }
        }
        else if (enemy.def.bossId === 3) {
            enemy.hp = Math.min(enemy.maxHp, enemy.hp + 10);
            state.log.push('The Void Lord regenerates 10 HP!');
        }
    }
    // Stun check
    if (enemy.statuses.some(s => s.type === 'stun')) {
        state.log.push(`${enemy.def.name} is stunned!`);
        return;
    }
    // Normal attack
    let damage = enemy.damage;
    if (player.block > 0) {
        const blocked = Math.min(damage, player.block);
        player.block -= blocked;
        damage -= blocked;
        state.log.push(`Blocked ${blocked} damage!`);
    }
    if (damage > 0) {
        player.hp -= damage;
        state.log.push(`${enemy.def.name} deals ${damage} damage!`);
    }
    else {
        state.log.push(`${enemy.def.name}'s attack was blocked!`);
    }
}
function showShop(state) {
    state.screen = 'shop';
    const classCards = cards_1.CARD_ROSTER.filter(c => c.cls === state.player.cls);
    const shopCards = [];
    const used = new Set();
    while (shopCards.length < 5 && classCards.length > 0) {
        const idx = Math.floor(Math.random() * classCards.length);
        const card = classCards[idx];
        if (!used.has(card.id)) {
            used.add(card.id);
            shopCards.push(card);
        }
    }
    const otherCards = cards_1.CARD_ROSTER.filter(c => c.cls !== state.player.cls && !used.has(c.id));
    while (shopCards.length < 5 && otherCards.length > 0) {
        const idx = Math.floor(Math.random() * otherCards.length);
        const card = otherCards.splice(idx, 1)[0];
        used.add(card.id);
        shopCards.push(card);
    }
    state.shopCards = shopCards;
}
function buyCard(state, cardIdx) {
    if (state.screen !== 'shop')
        return false;
    const card = state.shopCards[cardIdx];
    if (!card)
        return false;
    const owned = state.player.inventory[card.id] || 0;
    const cost = owned >= 3 ? 10 : 1 + owned;
    if (state.player.gold < cost) {
        state.log.push('Not enough gold!');
        return false;
    }
    if (owned >= 5) {
        state.log.push('Max copies owned!');
        return false;
    }
    state.player.gold -= cost;
    state.player.inventory[card.id] = owned + 1;
    state.player.drawPile.push((0, cards_1.createCardInstance)(card));
    state.log.push(`Bought ${card.name} for ${cost} Gold!`);
    return true;
}
function getShopPrice(state, cardIdx) {
    const card = state.shopCards[cardIdx];
    if (!card)
        return 0;
    const owned = state.player.inventory[card.id] || 0;
    if (owned >= 5)
        return -1;
    return owned >= 3 ? 10 : 1 + owned;
}
function advanceFloor(state) {
    state.floor++;
    state.room = 1;
    state.screen = 'combat';
    const enemyDef = (0, enemies_1.getRandomEnemy)(state.floor);
    state.enemy = createEnemyInstance(enemyDef);
    state.isPlayerTurn = true;
    state.turnStarted = false;
    state.player.block = 0;
    state.log = [`Descended to Floor ${state.floor}! Encountered ${enemyDef.name}.`];
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cacheElements = cacheElements;
exports.render = render;
const types_1 = require("./types");
const svg_1 = require("./svg");
const game_1 = require("./game");
// DOM references
let els = {};
function cacheElements() {
    const ids = [
        'hud-gold', 'hud-floor', 'hud-class',
        'enemy-name', 'enemy-sprite', 'enemy-hp-fill', 'enemy-stats', 'enemy-statuses',
        'player-hp-fill', 'player-hp-text', 'energy-bar', 'energy-text',
        'block-text', 'deck-count', 'discard-count',
        'card-hand', 'btn-end-turn', 'battle-log',
        'card-tooltip',
        'overlay-start', 'overlay-shop', 'overlay-gameover', 'overlay-victory', 'overlay-floor-clear',
        'shop-container', 'shop-gold-display',
        'floor-banner-text', 'floor-clear-msg',
        'gameover-msg',
    ];
    for (const id of ids) {
        els[id] = document.getElementById(id);
    }
}
function el(id) {
    return els[id];
}
function render(state) {
    if (state.screen === 'menu') {
        // Only render overlays for menu, skip everything else
        renderOverlays(state);
        return;
    }
    renderHUD(state);
    renderPlayerInfo(state);
    renderHand(state);
    renderEnemy(state);
    renderOverlays(state);
    renderBattleLog(state);
}
function renderHUD(state) {
    if (state.screen === 'menu') {
        el('hud-gold').textContent = '';
        el('hud-floor').textContent = '';
        el('hud-class').textContent = '';
        return;
    }
    el('hud-gold').textContent = (state.player.gold ?? 0).toString();
    const roomLabel = state.room === 4 ? 'BOSS' : `Room ${state.room}`;
    el('hud-floor').textContent = `Floor ${state.floor} - ${roomLabel}`;
    el('hud-class').textContent = types_1.CLASS_INFO[state.player.cls]?.name ?? '';
}
function renderPlayerInfo(state) {
    if (state.screen === 'menu')
        return;
    const p = state.player;
    // HP
    const hpPct = Math.max(0, (p.hp / p.maxHp) * 100);
    el('player-hp-fill').style.width = hpPct + '%';
    el('player-hp-text').textContent = `${p.hp}/${p.maxHp}`;
    // Energy
    renderEnergyBar(p.energy, p.maxEnergy);
    el('energy-text').textContent = `${p.energy}/${p.maxEnergy}`;
    // Block
    el('block-text').textContent = p.block.toString();
    // Deck / discard
    el('deck-count').textContent = p.drawPile.length.toString();
    el('discard-count').textContent = p.discard.length.toString();
    // End turn button
    const btn = el('btn-end-turn');
    btn.disabled = !state.isPlayerTurn || !state.turnStarted;
    btn.style.display = state.screen === 'combat' ? '' : 'none';
}
function renderEnergyBar(energy, maxEnergy) {
    const bar = el('energy-bar');
    bar.innerHTML = '';
    for (let i = 0; i < maxEnergy; i++) {
        const pip = document.createElement('div');
        pip.className = 'energy-pip' + (i < energy ? ' filled' : '');
        bar.appendChild(pip);
    }
}
function renderHand(state) {
    const handEl = el('card-hand');
    handEl.innerHTML = '';
    const tooltip = el('card-tooltip');
    if (state.screen !== 'combat') {
        tooltip.style.display = 'none';
        return;
    }
    for (let i = 0; i < state.player.hand.length; i++) {
        const card = state.player.hand[i];
        const playable = state.isPlayerTurn && state.player.energy >= card.def.cost && state.enemy && state.enemy.hp > 0;
        const cardDiv = document.createElement('div');
        cardDiv.className = 'card' + (playable ? ' playable' : '');
        cardDiv.innerHTML = (0, svg_1.renderCardSVG)(card.def, playable === true, playable === false);
        cardDiv.style.cursor = playable ? 'pointer' : 'not-allowed';
        if (playable) {
            cardDiv.addEventListener('click', () => {
                (0, game_1.playCard)(state, i);
                render(state);
            });
        }
        // Tooltip
        cardDiv.addEventListener('mouseenter', () => {
            tooltip.style.display = 'block';
            tooltip.querySelector('.tt-name').textContent = card.def.name;
            tooltip.querySelector('.tt-cost').textContent = `Cost: ${card.def.cost} Energy`;
            tooltip.querySelector('.tt-type').textContent = card.def.type;
            tooltip.querySelector('.tt-desc').textContent = card.def.desc;
        });
        cardDiv.addEventListener('mouseleave', () => {
            tooltip.style.display = 'none';
        });
        handEl.appendChild(cardDiv);
    }
}
function renderEnemy(state) {
    const enemy = state.enemy;
    const area = el('enemy-area');
    if (!enemy || enemy.hp <= 0) {
        area.style.display = 'none';
        return;
    }
    area.style.display = '';
    el('enemy-name').textContent = enemy.def.name;
    el('enemy-sprite').innerHTML = (0, svg_1.renderEnemySVG)(enemy.def.sprite);
    const hpPct = Math.max(0, (enemy.hp / enemy.maxHp) * 100);
    el('enemy-hp-fill').style.width = hpPct + '%';
    el('enemy-stats').textContent = `HP: ${enemy.hp}/${enemy.maxHp} | ATK: ${enemy.damage}`;
    // Statuses
    const statusEl = el('enemy-statuses');
    statusEl.innerHTML = '';
    for (const s of enemy.statuses) {
        const icon = document.createElement('span');
        icon.className = 'status-icon';
        if (s.type === 'burn')
            icon.textContent = '🔥';
        else if (s.type === 'poison')
            icon.textContent = '☠';
        else if (s.type === 'stun')
            icon.textContent = '💫';
        else if (s.type === 'silence')
            icon.textContent = '🤫';
        statusEl.appendChild(icon);
    }
}
function renderOverlays(state) {
    const hideAll = () => {
        ['overlay-start', 'overlay-shop', 'overlay-gameover', 'overlay-victory', 'overlay-floor-clear'].forEach(id => {
            el(id).style.display = 'none';
        });
    };
    if (state.screen === 'combat') {
        hideAll();
        el('btn-end-turn').style.display = '';
        return;
    }
    if (state.screen === 'shop') {
        hideAll();
        el('overlay-shop').style.display = 'flex';
        el('btn-end-turn').style.display = 'none';
        el('shop-gold-display').textContent = `Gold: ${state.player.gold}`;
        renderShop(state);
        return;
    }
    if (state.screen === 'gameover') {
        hideAll();
        el('overlay-gameover').style.display = 'flex';
        el('btn-end-turn').style.display = 'none';
        el('gameover-msg').textContent = `You fell on Floor ${state.floor}, Room ${state.room}.`;
        return;
    }
    if (state.screen === 'victory') {
        hideAll();
        el('overlay-victory').style.display = 'flex';
        el('btn-end-turn').style.display = 'none';
        return;
    }
    if (state.screen === 'floorClear') {
        hideAll();
        el('overlay-floor-clear').style.display = 'flex';
        el('btn-end-turn').style.display = 'none';
        el('floor-banner-text').textContent = `Floor ${state.floor} Cleared!`;
        el('floor-clear-msg').textContent = `Rest, prepare, and descend deeper. Gold: ${state.player.gold}`;
        return;
    }
    // Default: show menu
    hideAll();
    el('overlay-start').style.display = 'flex';
    el('btn-end-turn').style.display = 'none';
}
function renderShop(state) {
    const container = el('shop-container');
    container.innerHTML = '';
    for (let i = 0; i < state.shopCards.length; i++) {
        const card = state.shopCards[i];
        const price = (0, game_1.getShopPrice)(state, i);
        const owned = state.player.inventory[card.id] || 0;
        const canBuy = state.player.gold >= Math.max(0, price);
        const shopDiv = document.createElement('div');
        shopDiv.className = 'shop-card';
        shopDiv.style.opacity = price < 0 || !canBuy ? '0.5' : '1';
        let cardHTML = (0, svg_1.renderCardSVG)(card, false, price < 0 || !canBuy);
        if (price < 0) {
            cardHTML += `<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#8b0000;font-size:14px;font-weight:bold;text-shadow:0 0 4px #000;">MAX</div>`;
        }
        shopDiv.innerHTML = `
      <div class="card">${cardHTML}</div>
      <div class="shop-price">${price < 0 ? '✕' : price}</div>
      <div class="deck-count">Owned: ${owned}/5</div>
    `;
        if (price >= 0 && canBuy) {
            shopDiv.addEventListener('click', () => {
                (0, game_1.buyCard)(state, i);
                el('shop-gold-display').textContent = `Gold: ${state.player.gold}`;
                renderShop(state);
            });
        }
        container.appendChild(shopDiv);
    }
}
function renderBattleLog(state) {
    const logEl = el('battle-log');
    logEl.innerHTML = '';
    const recent = state.log.slice(-3);
    for (const msg of recent) {
        const entry = document.createElement('div');
        entry.className = 'log-entry';
        entry.textContent = msg;
        logEl.appendChild(entry);
    }
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"9b055302-b337-4d88-8ece-51d9ce2ce313","tokensIn":4027206,"tokensOut":64435,"tokensTotal":4091641,"cost":0,"turns":82,"toolCalls":81,"failedToolCalls":1,"timestamp":"2026-08-02T11:45:24.660Z"} -->