Deckbuilder
Qwen 3.6 27b MTP · typescript

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 86,974 bytes · SHA-256 e198818f5ca3
<!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; background: #0a0a0f; font-family: 'Georgia', serif; color: #d4c5a9; }
#game-container { width: 100vw; height: 100vh; position: relative; overflow: hidden; }
.screen { display: none; width: 100%; height: 100%; flex-direction: column; align-items: center; justify-content: center; position: absolute; top: 0; left: 0; z-index: 1; }
.screen.active { display: flex; }
.overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 100; display: flex; align-items: center; justify-content: center; }
.overlay.hidden { display: none; }
/* Class Select */
#screen-class-select { background: radial-gradient(ellipse at center, #1a1520 0%, #0a0a0f 70%); }
#screen-class-select h1 { font-size: 3em; color: #c9a84c; text-shadow: 0 0 20px rgba(201,168,76,0.5), 0 2px 4px rgba(0,0,0,0.8); margin-bottom: 0.2em; letter-spacing: 3px; }
.subtitle { font-size: 1.1em; color: #8a7e6b; margin-bottom: 2em; font-style: italic; }
#class-cards { display: flex; gap: 2em; flex-wrap: wrap; justify-content: center; padding: 0 1em; }
.class-card { width: 240px; height: 360px; border-radius: 12px; cursor: pointer; transition: transform 0.3s, box-shadow 0.3s; position: relative; overflow: hidden; display: flex; flex-direction: column; align-items: center; padding: 1em; }
.class-card:hover { transform: translateY(-8px) scale(1.05); }
.class-card .class-name { font-size: 1.4em; margin-top: 0.5em; text-shadow: 0 2px 4px rgba(0,0,0,0.8); }
.class-card .class-theme { font-style: italic; color: #aaa; margin-bottom: 0.5em; font-size: 0.9em; }
.class-card .class-stats { font-size: 0.85em; color: #bbb; text-align: center; line-height: 1.6; }
/* Combat Screen */
#screen-combat { background: radial-gradient(ellipse at bottom, #1a1215 0%, #0d0b10 50%, #08070e 100%); justify-content: flex-start; padding-top: 0; }
#combat-hud-top { width: 100%; display: flex; justify-content: space-between; padding: 8px 20px; background: linear-gradient(180deg, rgba(30,25,20,0.9) 0%, transparent 100%); font-size: 0.9em; z-index: 10; }
#combat-hud-top span { padding: 4px 12px; border-radius: 4px; background: rgba(0,0,0,0.5); border: 1px solid #3a3020; }
#combat-arena { width: 100%; flex: 1; display: flex; justify-content: space-between; align-items: center; padding: 0 60px; position: relative; min-height: 0; }
#enemy-area { display: flex; flex-direction: column; align-items: center; gap: 8px; }
#player-area { width: 100%; display: flex; justify-content: center; padding-bottom: 10px; }
#enemy-sprite { width: 120px; height: 140px; border-radius: 8px; position: relative; transition: transform 0.1s; }
#enemy-sprite.hit { animation: enemyHit 0.3s ease-out; }
@keyframes enemyHit { 0% { filter: brightness(3); transform: scale(1.1); } 100% { filter: none; transform: scale(1); } }
#enemy-info, #player-info { text-align: center; width: 200px; }
#enemy-name { font-size: 1em; color: #c9a84c; margin-bottom: 4px; }
#enemy-hp-bar, #player-hp-bar { width: 100%; height: 16px; background: #2a1515; border-radius: 8px; overflow: hidden; border: 1px solid #4a3030; position: relative; }
#enemy-hp-fill, #player-hp-fill { height: 100%; background: linear-gradient(90deg, #8b2500, #cc3300); transition: width 0.4s ease-out; border-radius: 8px; }
#player-hp-fill { background: linear-gradient(90deg, #1a6b1a, #2ecc40); }
#enemy-statuses, #player-statuses { display: flex; gap: 4px; justify-content: center; margin-top: 4px; min-height: 20px; flex-wrap: wrap; }
.status-badge { font-size: 0.7em; padding: 2px 6px; border-radius: 3px; background: rgba(0,0,0,0.6); border: 1px solid #555; }
#player-defense { color: #4a9eff; font-weight: bold; margin-top: 2px; }
#hud-energy { color: #ffcc00; font-size: 1em; padding: 4px 12px; border-radius: 4px; background: rgba(0,0,0,0.5); border: 1px solid #3a3020; }
/* Hand Area */
#combat-hand-area { width: 100%; min-height: 200px; display: flex; justify-content: center; align-items: flex-end; padding-bottom: 8px; position: relative; z-index: 5; }
#hand-container { display: flex; gap: -30px; justify-content: center; align-items: flex-end; perspective: 800px; min-height: 170px; padding: 0 20px; }
/* Cards */
.game-card { width: 140px; height: 195px; border-radius: 8px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s, margin-top 0.2s; position: relative; flex-shrink: 0; user-select: none; }
.game-card:hover { transform: translateY(-30px) scale(1.08); z-index: 20; box-shadow: 0 8px 25px rgba(0,0,0,0.6); margin-top: -20px; }
.game-card.playable { filter: drop-shadow(0 0 4px rgba(255,200,0,0.3)); }
.game-card.unplayable { opacity: 0.5; cursor: not-allowed; }
.game-card.unplayable:hover { transform: none; box-shadow: none; margin-top: 0; }
/* Card Detail Overlay */
#card-detail-content { background: #1a1520; border: 2px solid #c9a84c; border-radius: 12px; padding: 2em; max-width: 360px; text-align: center; }
#card-detail-content h3 { color: #c9a84c; margin-bottom: 0.5em; font-size: 1.3em; }
#card-detail-content p { line-height: 1.6; color: #bbb; }
/* Combat Controls */
#combat-controls { width: 100%; display: flex; justify-content: center; align-items: center; gap: 20px; padding: 8px 20px 12px; background: linear-gradient(0deg, rgba(30,25,20,0.9) 0%, transparent 100%); }
#btn-end-turn { padding: 10px 30px; font-size: 1em; font-family: 'Georgia', serif; background: linear-gradient(180deg, #4a3520, #2a1a10); color: #d4c5a9; border: 2px solid #6a5030; border-radius: 6px; cursor: pointer; transition: all 0.2s; letter-spacing: 1px; }
#btn-end-turn:hover { background: linear-gradient(180deg, #5a4530, #3a2a20); box-shadow: 0 0 15px rgba(201,168,76,0.3); border-color: #c9a84c; }
#btn-end-turn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Shop Screen */
#screen-shop { background: radial-gradient(ellipse at center, #1a1520 0%, #0a0a0f 70%); padding: 2em; }
#screen-shop h2 { color: #c9a84c; font-size: 2em; margin-bottom: 0.3em; text-shadow: 0 0 15px rgba(201,168,76,0.4); }
#shop-gold-display { color: #ffd700; font-size: 1.2em; margin-bottom: 1.5em; }
#shop-cards { display: flex; gap: 1.5em; flex-wrap: wrap; justify-content: center; max-width: 900px; margin-bottom: 2em; }
.shop-card-wrapper { text-align: center; }
.shop-card-price { color: #ffd700; font-size: 0.85em; margin-top: 6px; }
.shop-card-count { color: #888; font-size: 0.75em; }
/* Buttons */
button { padding: 12px 30px; font-size: 1em; font-family: 'Georgia', serif; background: linear-gradient(180deg, #4a3520, #2a1a10); color: #d4c5a9; border: 2px solid #6a5030; border-radius: 6px; cursor: pointer; transition: all 0.2s; letter-spacing: 1px; }
button:hover { background: linear-gradient(180deg, #5a4530, #3a2a20); box-shadow: 0 0 15px rgba(201,168,76,0.3); border-color: #c9a84c; }
/* Boss Intro */
#screen-boss-intro { background: radial-gradient(ellipse at center, #2a0a0a 0%, #0a0a0f 70%); }
#screen-boss-intro h2 { color: #cc3300; font-size: 2.5em; text-shadow: 0 0 30px rgba(204,51,0,0.6); margin-bottom: 0.5em; }
#boss-desc { color: #aa8866; font-style: italic; margin-bottom: 2em; max-width: 500px; text-align: center; line-height: 1.6; }
/* Game Over */
#screen-game-over { background: radial-gradient(ellipse at center, #1a0a0a 0%, #0a0a0f 70%); }
#screen-game-over h2 { font-size: 3em; margin-bottom: 0.5em; text-shadow: 0 0 30px rgba(204,51,0,0.6); }
#game-over-text { color: #aa8866; margin-bottom: 2em; font-size: 1.1em; }
/* Damage/Heal Float */
.float-text { position: absolute; pointer-events: none; font-weight: bold; font-size: 1.4em; animation: floatUp 1s ease-out forwards; z-index: 50; text-shadow: 0 2px 4px rgba(0,0,0,0.8); }
@keyframes floatUp { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-60px) scale(0.7); } }
/* Card play animation */
.card-play-anim { position: absolute; z-index: 30; pointer-events: none; transition: all 0.4s ease-in-out; }
/* Turn indicator */
#turn-indicator { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 2em; color: #c9a84c; text-shadow: 0 0 20px rgba(201,168,76,0.6); opacity: 0; pointer-events: none; z-index: 40; transition: opacity 0.3s; }
#turn-indicator.show { opacity: 1; animation: fadeOut 1s ease-out forwards; }
@keyframes fadeOut { 0% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; } }
/* Room transition */
#room-transition { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #0a0a0f; z-index: 200; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity 0.5s; }
#room-transition.active { opacity: 1; pointer-events: all; }
#room-transition h2 { color: #c9a84c; font-size: 2em; text-shadow: 0 0 20px rgba(201,168,76,0.5); }
/* Card type colors */
.card-type-attack { border-color: #cc3300 !important; }
.card-type-defense { border-color: #4a9eff !important; }
.card-type-other { border-color: #aa44ff !important; }
/* ── css: ambient ── */
/* Ambient particles */
#particles-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; overflow: hidden; }
.particle { position: absolute; border-radius: 50%; opacity: 0.3; animation: particleFloat linear infinite; }
@keyframes particleFloat {
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 0.4; }
90% { opacity: 0.4; }
100% { transform: translateY(-20px) rotate(360deg); opacity: 0; }
}
/* Vignette overlay */
#vignette { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.6) 100%); }
/* Combat arena atmosphere */
#combat-arena::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(180deg, transparent 60%, rgba(20,10,30,0.5) 100%); pointer-events: none; }
/* Card hover glow effects */
.game-card.playable:hover { box-shadow: 0 0 20px rgba(201,168,76,0.4), 0 8px 25px rgba(0,0,0,0.6); }
.card-type-attack.playable:hover { box-shadow: 0 0 20px rgba(204,51,0,0.5), 0 8px 25px rgba(0,0,0,0.6); }
.card-type-defense.playable:hover { box-shadow: 0 0 20px rgba(74,158,255,0.5), 0 8px 25px rgba(0,0,0,0.6); }
.card-type-other.playable:hover { box-shadow: 0 0 20px rgba(170,68,255,0.5), 0 8px 25px rgba(0,0,0,0.6); }
/* Enemy idle animation */
#enemy-sprite { animation: enemyIdle 3s ease-in-out infinite; }
@keyframes enemyIdle {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
/* Player area subtle glow */
#player-area::before { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 200px; height: 40px; background: radial-gradient(ellipse, rgba(100,200,100,0.1) 0%, transparent 70%); pointer-events: none; }
/* Room transition overlay */
#room-transition { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #0a0a0f; z-index: 200; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity 0.5s ease-in-out; }
#room-transition.active { opacity: 1; pointer-events: all; }
#room-transition h2 { color: #c9a84c; font-size: 2em; text-shadow: 0 0 20px rgba(201,168,76,0.5); animation: pulseGlow 2s ease-in-out infinite; }
@keyframes pulseGlow { 0%, 100% { text-shadow: 0 0 20px rgba(201,168,76,0.3); } 50% { text-shadow: 0 0 40px rgba(201,168,76,0.7); } }
/* Shop ambient */
#screen-shop::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center bottom, rgba(201,168,76,0.05) 0%, transparent 60%); pointer-events: none; }
/* Card play flash */
.card-play-flash { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); pointer-events: none; animation: flashFade 0.3s ease-out forwards; z-index: 25; }
@keyframes flashFade { from { opacity: 1; } to { opacity: 0; } }
/* Energy pips */
.energy-pip { display: inline-block; width: 14px; height: 14px; border-radius: 50%; margin-right: 3px; transition: all 0.2s; }
.energy-pip.filled { background: radial-gradient(circle, #ffdd44, #cc9900); box-shadow: 0 0 6px rgba(255,221,68,0.5); }
.energy-pip.empty { background: #3a3020; border: 1px solid #5a4a30; }
/* HP bar glow */
#player-hp-fill::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 50%; background: linear-gradient(180deg, rgba(255,255,255,0.2) 0%, transparent 100%); border-radius: 8px 8px 0 0; }
#enemy-hp-fill::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 50%; background: linear-gradient(180deg, rgba(255,255,255,0.15) 0%, transparent 100%); border-radius: 8px 8px 0 0; }
/* Scrollbar styling */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0a0a0f; }
::-webkit-scrollbar-thumb { background: #3a3020; border-radius: 3px; }
</style>
</head>
<body>
<div id="game-container"><div id="particles-container"></div>
<div id="vignette"></div>
<div id="room-transition"><h2></h2></div>
<div id="screen-class-select" class="screen active">
<h1>Shadows of Aethelgard</h1>
<p class="subtitle">Choose your champion to descend into the Void</p>
<div id="class-cards"></div>
</div>
<div id="screen-combat" class="screen">
<div id="combat-hud-top">
<span id="hud-floor"></span>
<span id="hud-room"></span>
<span id="hud-gold"></span>
</div>
<div id="combat-arena">
<div id="enemy-area">
<div id="enemy-sprite"></div>
<div id="enemy-info">
<div id="enemy-name"></div>
<div id="enemy-hp-bar"><div id="enemy-hp-fill"></div></div>
<span id="enemy-hp-text"></span>
<span id="enemy-statuses"></span>
</div>
</div>
<div id="player-area">
<div id="player-info">
<span id="player-hp-text"></span>
<div id="player-hp-bar"><div id="player-hp-fill"></div></div>
<span id="player-defense"></span>
<span id="player-statuses"></span>
</div>
</div>
</div>
<div id="combat-hand-area">
<div id="hand-container"></div>
</div>
<div id="combat-controls">
<button id="btn-end-turn">End Turn</button>
<span id="hud-energy"></span>
</div>
</div>
<div id="screen-shop" class="screen">
<h2>The Merchant's Tent</h2>
<p id="shop-gold-display"></p>
<div id="shop-cards"></div>
<button id="btn-leave-shop">Leave Shop</button>
</div>
<div id="screen-boss-intro" class="screen">
<h2 id="boss-name"></h2>
<p id="boss-desc"></p>
<button id="btn-enter-combat">Enter Combat</button>
</div>
<div id="screen-game-over" class="screen">
<h2 id="game-over-title"></h2>
<p id="game-over-text"></p>
<button id="btn-restart">Restart</button>
</div>
<div id="card-detail-overlay" class="overlay hidden">
<div id="card-detail-content"></div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./ui":"src/ui.ts"},"src/cards.ts":{"./types":"src/types.ts"},"src/enemies.ts":{"./types":"src/types.ts"},"src/card-svg.ts":{"./types":"src/types.ts"},"src/game-state.ts":{"./types":"src/types.ts","./cards":"src/cards.ts","./enemies":"src/enemies.ts"},"src/combat.ts":{"./types":"src/types.ts","./game-state":"src/game-state.ts"},"src/ui.ts":{"./types":"src/types.ts","./game-state":"src/game-state.ts","./cards":"src/cards.ts","./combat":"src/combat.ts","./card-svg":"src/card-svg.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";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const UI = __importStar(require("./ui"));
// Initialize the game
UI.cacheElements();
UI.renderClassSelect();
// Wire up end turn button
const btn = document.getElementById('btn-end-turn');
if (btn) {
btn.addEventListener('click', () => UI.onEndTurn());
}
// Keyboard shortcut: Space/Enter to end turn
document.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
const gs = UI.getGameState();
if (gs && gs.combatActive && gs.turnPhase === 'player') {
UI.onEndTurn();
}
}
});
console.log('%c Shadows of Aethelgard ', 'background: #1a1520; color: #c9a84c; font-size: 16px; padding: 4px 8px; border-radius: 4px;');
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/cards.ts ──
__mods["src/cards.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CLASS_INFO = exports.CLASS_STARTER_DECKS = exports.ALL_CARDS = void 0;
exports.createCardInstance = createCardInstance;
exports.getCardById = getCardById;
exports.getCardsByClass = getCardsByClass;
exports.ALL_CARDS = [
// === BASTION (12 cards) ===
{ id: 'BA-A01', name: 'Slam', type: 'Attack', cost: 2, attack: 3, defense: 0, ability: 'If you have ≥30 HP, draw 1 card.', description: 'Deals 3 damage. If you have 30+ HP, draw 1 card.', classId: 'bastion' },
{ id: 'BA-A02', name: 'Earthquake', type: 'Attack', cost: 3, attack: 4, defense: 0, ability: 'Hits all enemies.', description: 'Deals 4 damage to the enemy. (AoE in multi-enemy combat.)', classId: 'bastion' },
{ id: 'BA-A03', name: 'Stone Fist', type: 'Attack', cost: 1, attack: 2, defense: 0, ability: '', description: 'Deals 2 damage.', classId: 'bastion' },
{ id: 'BA-A04', name: 'Boulder', type: 'Attack', cost: 4, attack: 6, defense: 0, ability: '', description: 'Deals 6 damage. Costs 4 Energy.', classId: 'bastion' },
{ id: 'BA-D01', name: 'Shield Wall', type: 'Defense', cost: 0, attack: 0, defense: 4, ability: '', description: 'Gain 4 Defense for this turn.', classId: 'bastion' },
{ id: 'BA-D02', name: 'Iron Skin', type: 'Defense', cost: 2, attack: 0, defense: 5, ability: 'Permanent. Discard 1 card.', description: 'Gain 5 Defense permanently (base). Discard 1 card.', classId: 'bastion' },
{ id: 'BA-D03', name: 'Stand Firm', type: 'Defense', cost: 1, attack: 0, defense: 2, ability: '', description: 'Gain 2 Defense for this turn.', classId: 'bastion' },
{ id: 'BA-D04', name: 'Taunt', type: 'Defense', cost: 2, attack: 0, defense: 3, ability: 'Stun enemy 1 turn.', description: 'Gain 3 Defense. Stun the enemy for 1 turn.', classId: 'bastion' },
{ id: 'BA-U01', name: 'Search', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Draw 1. Discard if >5.', description: 'Draw 1 card. If hand > 5 cards, discard down to 5.', classId: 'bastion' },
{ id: 'BA-U02', name: 'Repair', type: 'Other', cost: 3, attack: 0, defense: 0, ability: 'Heal 6 HP.', description: 'Restore 6 HP.', classId: 'bastion' },
{ id: 'BA-U03', name: 'Rally', type: 'Other', cost: 1, attack: 0, defense: 0, ability: 'Draw 1. Gain 1 Energy.', description: 'Draw 1 card and gain 1 Energy.', classId: 'bastion' },
{ id: 'BA-U04', name: 'Loot', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Gain 5 Gold.', description: 'Gain 5 Gold.', classId: 'bastion' },
// === WITHER (12 cards) ===
{ id: 'WI-A01', name: 'Fireball', type: 'Attack', cost: 2, attack: 3, defense: 0, ability: 'Apply 1 Burn.', description: 'Deals 3 damage. Apply 1 Burn to target.', classId: 'wither' },
{ id: 'WI-A02', name: 'Inferno', type: 'Attack', cost: 3, attack: 4, defense: 0, ability: 'Apply 3 Burn.', description: 'Deals 4 damage. Applies 3 Burn to target.', classId: 'wither' },
{ id: 'WI-A03', name: 'Ember', type: 'Attack', cost: 1, attack: 2, defense: 0, ability: 'Apply 1 Burn/turn.', description: 'Deals 2 damage. Deals 1 damage every turn to target (Burn).', classId: 'wither' },
{ id: 'WI-A04', name: 'Pyre', type: 'Attack', cost: 4, attack: 5, defense: 0, ability: 'All enemies take 1 dmg next turn.', description: 'Deals 5 damage. All enemies take 1 extra damage next turn.', classId: 'wither' },
{ id: 'WI-D01', name: 'Scorch Shield', type: 'Defense', cost: 2, attack: 0, defense: 3, ability: '', description: 'Gain 3 Defense for this turn.', classId: 'wither' },
{ id: 'WI-D02', name: 'Heat Resist', type: 'Defense', cost: 1, attack: 0, defense: 1, ability: 'Gain 1 Energy.', description: 'Gain 1 Defense. Gain 1 Energy.', classId: 'wither' },
{ id: 'WI-D03', name: 'Smoke', type: 'Defense', cost: 2, attack: 0, defense: 4, ability: '', description: 'Gain 4 Defense for this turn.', classId: 'wither' },
{ id: 'WI-D04', name: 'Endure', type: 'Defense', cost: 3, attack: 0, defense: 2, ability: 'Heal 2 HP.', description: 'Gain 2 Defense. Heal 2 HP.', classId: 'wither' },
{ id: 'WI-U01', name: 'Poison', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Apply 1 Poison.', description: 'Apply 1 Poison to target (1 dmg/turn).', classId: 'wither' },
{ id: 'WI-U02', name: 'Burn Wave', type: 'Other', cost: 3, attack: 0, defense: 0, ability: '3 dmg + 2 Burn all enemies.', description: 'Deal 3 damage and apply 2 Burn to the enemy.', classId: 'wither' },
{ id: 'WI-U03', name: 'Slow', type: 'Other', cost: 1, attack: 0, defense: 0, ability: 'Draw 1. Stun target.', description: 'Draw 1 card. Target is stunned next turn.', classId: 'wither' },
{ id: 'WI-U04', name: 'Wither', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Heal 5 HP.', description: 'Restore 5 HP.', classId: 'wither' },
// === SHROUD (12 cards) ===
{ id: 'SH-A01', name: 'Backstab', type: 'Attack', cost: 2, attack: 4, defense: 0, ability: '+1 dmg if target has no Defense.', description: 'Deals 4 damage. +1 Damage if target has no Defense.', classId: 'shroud' },
{ id: 'SH-A02', name: 'Shadow Bolt', type: 'Attack', cost: 3, attack: 5, defense: 0, ability: 'Cannot be blocked.', description: 'Deals 5 damage. Target cannot block this damage.', classId: 'shroud' },
{ id: 'SH-A03', name: 'Silence', type: 'Attack', cost: 1, attack: 3, defense: 0, ability: 'Stun target 1 turn.', description: 'Deals 3 damage. Target is stunned for 1 turn.', classId: 'shroud' },
{ id: 'SH-A04', name: 'Vengeance', type: 'Attack', cost: 3, attack: 6, defense: 0, ability: 'Gain 2 Energy.', description: 'Deals 6 damage. Gain 2 Energy after playing.', classId: 'shroud' },
{ id: 'SH-D01', name: 'Blur', type: 'Defense', cost: 3, attack: 0, defense: 2, ability: '', description: 'Gain 2 Defense for this turn.', classId: 'shroud' },
{ id: 'SH-D02', name: 'Step', type: 'Defense', cost: 2, attack: 0, defense: 3, ability: 'Gain 1 Energy.', description: 'Gain 3 Defense. Gain 1 Energy.', classId: 'shroud' },
{ id: 'SH-D03', name: 'Evasion', type: 'Defense', cost: 1, attack: 0, defense: 1, ability: '', description: 'Gain 1 Defense for this turn.', classId: 'shroud' },
{ id: 'SH-D04', name: 'Phantom', type: 'Defense', cost: 2, attack: 0, defense: 4, ability: '', description: 'Gain 4 Defense. Ghost form for 1 turn.', classId: 'shroud' },
{ id: 'SH-U01', name: 'Dash', type: 'Other', cost: 1, attack: 0, defense: 0, ability: 'Gain 1 Energy.', description: 'Gain 1 Energy.', classId: 'shroud' },
{ id: 'SH-U02', name: 'Teleport', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Deal 2 damage.', description: 'Deal 2 damage to the enemy.', classId: 'shroud' },
{ id: 'SH-U03', name: 'Hide', type: 'Other', cost: 2, attack: 0, defense: 0, ability: 'Gain 1 Def. Discard 1. Draw 2.', description: 'Gain 1 Defense. Discard 1 card. Draw 2 cards.', classId: 'shroud' },
{ id: 'SH-U04', name: 'Assassinate', type: 'Other', cost: 3, attack: 0, defense: 0, ability: 'Deal 8 unblockable damage.', description: 'Deal 8 damage. Cannot be blocked this turn.', classId: 'shroud' },
];
exports.CLASS_STARTER_DECKS = {
bastion: ['BA-A01', 'BA-A03', 'BA-A03', 'BA-D01', 'BA-D01', 'BA-D03', 'BA-U02', 'BA-U03', 'BA-U04', 'BA-A02'],
wither: ['WI-A01', 'WI-A03', 'WI-A03', 'WI-D01', 'WI-D02', 'WI-U01', 'WI-U02', 'WI-U03', 'WI-U04', 'WI-A02'],
shroud: ['SH-A01', 'SH-A03', 'SH-A03', 'SH-D01', 'SH-D03', 'SH-U01', 'SH-U02', 'SH-U03', 'SH-U04', 'SH-A02'],
};
exports.CLASS_INFO = {
bastion: { name: 'The Bastion', theme: 'Immovable Object', hp: 35, energy: 3, handSize: 4 },
wither: { name: 'The Wither', theme: 'Rot and Burn', hp: 30, energy: 3, handSize: 4 },
shroud: { name: 'The Shroud', theme: 'Shadow and Speed', hp: 28, energy: 3, handSize: 4 },
};
let uidCounter = 0;
function createCardInstance(def) {
return { def, uid: `card_${def.id}_${uidCounter++}` };
}
function getCardById(id) {
return exports.ALL_CARDS.find(c => c.id === id);
}
function getCardsByClass(classId) {
return exports.ALL_CARDS.filter(c => c.classId === classId);
}
};
// ── module: src/enemies.ts ──
__mods["src/enemies.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BOSS_DESCRIPTIONS = exports.BOSS_TYPES = exports.BOSS_DEFS = exports.FLOOR_ENEMIES = void 0;
exports.getRandomEnemy = getRandomEnemy;
exports.FLOOR_ENEMIES = {
1: [
{ name: 'Void Goblin', hp: 20, damage: 3, spriteColor: '#4a7a3a', spriteShape: 'goblin' },
{ name: 'Shadow Rat', hp: 15, damage: 2, spriteColor: '#6a5a4a', spriteShape: 'goblin' },
{ name: 'Dark Imp', hp: 18, damage: 3, spriteColor: '#7a3a3a', spriteShape: 'goblin' },
],
2: [
{ name: 'Vampire Knight', hp: 30, damage: 5, spriteColor: '#6a2a4a', spriteShape: 'knight' },
{ name: 'Void Ogre', hp: 35, damage: 6, spriteColor: '#4a4a6a', spriteShape: 'knight' },
{ name: 'Cursed Warrior', hp: 28, damage: 5, spriteColor: '#5a3a5a', spriteShape: 'knight' },
],
3: [
{ name: 'Void Warrior', hp: 40, damage: 8, spriteColor: '#3a2a6a', spriteShape: 'warrior' },
{ name: 'Dark Cultist', hp: 35, damage: 7, spriteColor: '#5a2a5a', spriteShape: 'warrior' },
{ name: 'Abyssal Knight', hp: 45, damage: 9, spriteColor: '#2a3a6a', spriteShape: 'warrior' },
],
};
exports.BOSS_DEFS = {
1: { name: 'The Gatekeeper', hp: 80, damage: 15, spriteColor: '#8b7355', spriteShape: 'boss' },
2: { name: 'The Corrupted', hp: 80, damage: 15, spriteColor: '#6a1a3a', spriteShape: 'boss' },
3: { name: 'The Void Lord', hp: 80, damage: 15, spriteColor: '#2a1a4a', spriteShape: 'boss' },
};
exports.BOSS_TYPES = {
1: 'gatekeeper',
2: 'corrupted',
3: 'voidlord',
};
exports.BOSS_DESCRIPTIONS = {
1: '"Blocks 5 damage per turn until HP drops below 20. An immovable force guarding the descent."',
2: '"Deals devastating 20 damage if you have less than 3 Energy. A corrupted knight consumed by darkness."',
3: '"Regenerates 10 HP per turn and cannot be blocked. The final horror that consumes all of Aethelgard."',
};
function getRandomEnemy(floor) {
const enemies = exports.FLOOR_ENEMIES[floor] || exports.FLOOR_ENEMIES[1];
return enemies[Math.floor(Math.random() * enemies.length)];
}
};
// ── module: src/card-svg.ts ──
__mods["src/card-svg.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCardSVG = createCardSVG;
exports.createCardBackSVG = createCardBackSVG;
exports.createEnemySVG = createEnemySVG;
const CLASS_COLORS = {
bastion: { primary: '#c9a84c', secondary: '#8b7355', accent: '#d4af37', glow: 'rgba(201,168,76,0.4)' },
wither: { primary: '#cc3300', secondary: '#8b2500', accent: '#ff6600', glow: 'rgba(204,51,0,0.4)' },
shroud: { primary: '#7a3abf', secondary: '#4a1a7a', accent: '#aa44ff', glow: 'rgba(122,58,191,0.4)' },
};
const TYPE_COLORS = {
Attack: '#cc3300',
Defense: '#4a9eff',
Other: '#aa44ff',
};
function createCardSVG(cardDef, faceUp = true) {
const colors = CLASS_COLORS[cardDef.classId];
if (!faceUp)
return createCardBackSVG(cardDef.classId);
const typeColor = TYPE_COLORS[cardDef.type] || '#888';
const statsText = cardDef.attack > 0 ? `⚔${cardDef.attack}` : (cardDef.defense > 0 ? `🛡${cardDef.defense}` : '');
return `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="195" viewBox="0 0 140 195">
<defs>
<linearGradient id="bg_${cardDef.id}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1a1520"/>
<stop offset="100%" style="stop-color:#0d0b10"/>
</linearGradient>
<filter id="glow_${cardDef.id}">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Card background -->
<rect x="1" y="1" width="138" height="193" rx="8" fill="url(#bg_${cardDef.id})"/>
<!-- Outer border with class color -->
<rect x="2" y="2" width="136" height="191" rx="7" fill="none" stroke="${colors.primary}" stroke-width="2.5" filter="url(#glow_${cardDef.id})"/>
<!-- Inner decorative border -->
<rect x="6" y="6" width="128" height="183" rx="4" fill="none" stroke="${colors.secondary}" stroke-width="0.7" opacity="0.5"/>
${getClassFrameDecoration(cardDef.classId, colors)}
<!-- Cost badge -->
<circle cx="22" cy="22" r="13" fill="#1a1520" stroke="${colors.primary}" stroke-width="1.5"/>
<text x="22" y="27" text-anchor="middle" font-size="14" font-weight="bold" fill="${colors.accent}" font-family="Georgia">${cardDef.cost}</text>
<!-- Card name -->
<text x="70" y="28" text-anchor="middle" font-size="11" font-weight="bold" fill="${colors.primary}" font-family="Georgia">${cardDef.name}</text>
<!-- Type label -->
<rect x="50" y="34" width="40" height="12" rx="3" fill="${typeColor}" opacity="0.3"/>
<text x="70" y="43" text-anchor="middle" font-size="8" fill="${typeColor}" font-family="Georgia">${cardDef.type}</text>
<!-- Card image placeholder area -->
<rect x="12" y="52" width="116" height="70" rx="4" fill="#0a0810" stroke="${colors.secondary}" stroke-width="0.5"/>
${getClassImagePlaceholder(cardDef.classId, colors)}
<!-- Stats -->
<text x="70" y="132" text-anchor="middle" font-size="16" font-weight="bold" fill="${typeColor}" font-family="Georgia">${statsText}</text>
<!-- Divider line -->
<line x1="15" y1="140" x2="125" y2="140" stroke="${colors.secondary}" stroke-width="0.5" opacity="0.6"/>
<!-- Ability text -->
<text x="70" y="152" text-anchor="middle" font-size="7.5" fill="#bbb" font-family="Georgia">${cardDef.description.length > 40 ? cardDef.description.substring(0, 38) + '...' : cardDef.description}</text>
<text x="70" y="162" text-anchor="middle" font-size="7.5" fill="#999" font-family="Georgia">${cardDef.description.length > 40 ? cardDef.description.substring(38) : ''}</text>
<!-- Bottom decorative element -->
<text x="70" y="182" text-anchor="middle" font-size="6" fill="${colors.secondary}" opacity="0.5">${cardDef.id}</text>
</svg>`;
}
function getClassFrameDecoration(classId, colors) {
switch (classId) {
case 'bastion':
return `
<!-- Bastion: Shield corner ornaments -->
<path d="M6,12 L14,6 L20,12" fill="none" stroke="${colors.primary}" stroke-width="1.5"/>
<path d="M134,12 L126,6 L120,12" fill="none" stroke="${colors.primary}" stroke-width="1.5"/>
<!-- Shield motif top center -->
<polygon points="70,8 74,12 74,18 70,22 66,18 66,12" fill="none" stroke="${colors.primary}" stroke-width="0.8"/>
`;
case 'wither':
return `
<!-- Wither: Flame corner ornaments -->
<path d="M6,14 Q10,8 14,14 Q12,10 10,14" fill="${colors.primary}" opacity="0.5"/>
<path d="M134,14 Q130,8 126,14 Q128,10 130,14" fill="${colors.primary}" opacity="0.5"/>
<!-- Flame motif top center -->
<circle cx="70" cy="12" r="3" fill="${colors.primary}" opacity="0.3"/>
`;
case 'shroud':
return `
<!-- Shroud: Shadow corner ornaments -->
<path d="M6,14 L10,8 L14,14 Z" fill="${colors.primary}" opacity="0.3"/>
<path d="M134,14 L130,8 L126,14 Z" fill="${colors.primary}" opacity="0.3"/>
<!-- Eye motif top center -->
<ellipse cx="70" cy="12" rx="5" ry="3" fill="none" stroke="${colors.primary}" stroke-width="0.8"/>
<circle cx="70" cy="12" r="1.5" fill="${colors.primary}" opacity="0.6"/>
`;
}
}
function getClassImagePlaceholder(classId, colors) {
switch (classId) {
case 'bastion':
return `
<!-- Bastion: Shield emblem -->
<polygon points="70,62 85,72 85,90 70,100 55,90 55,72" fill="none" stroke="${colors.primary}" stroke-width="1.5" opacity="0.4"/>
<polygon points="70,68 80,75 80,88 70,95 60,88 60,75" fill="${colors.secondary}" opacity="0.2"/>
<line x1="70" y1="68" x2="70" y2="95" stroke="${colors.primary}" stroke-width="0.8" opacity="0.3"/>
`;
case 'wither':
return `
<!-- Wither: Flame/skull emblem -->
<circle cx="70" cy="85" r="12" fill="none" stroke="${colors.primary}" stroke-width="1.5" opacity="0.4"/>
<path d="M62,90 Q65,75 70,82 Q75,75 78,90" fill="${colors.accent}" opacity="0.3"/>
<circle cx="66" cy="83" r="2" fill="${colors.primary}" opacity="0.4"/>
<circle cx="74" cy="83" r="2" fill="${colors.primary}" opacity="0.4"/>
`;
case 'shroud':
return `
<!-- Shroud: Shadow/eye emblem -->
<ellipse cx="70" cy="85" rx="16" ry="10" fill="none" stroke="${colors.primary}" stroke-width="1.5" opacity="0.4"/>
<circle cx="70" cy="85" r="6" fill="${colors.secondary}" opacity="0.3"/>
<circle cx="70" cy="85" r="3" fill="${colors.primary}" opacity="0.5"/>
<!-- Shadow wisps -->
<path d="M48,85 Q52,78 56,85" fill="none" stroke="${colors.primary}" stroke-width="0.8" opacity="0.3"/>
<path d="M84,85 Q88,92 92,85" fill="none" stroke="${colors.primary}" stroke-width="0.8" opacity="0.3"/>
`;
}
}
function createCardBackSVG(classId) {
const colors = CLASS_COLORS[classId];
switch (classId) {
case 'bastion':
return `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="195" viewBox="0 0 140 195">
<rect x="1" y="1" width="138" height="193" rx="8" fill="#1a1520"/>
<rect x="2" y="2" width="136" height="191" rx="7" fill="none" stroke="${colors.primary}" stroke-width="2.5"/>
<!-- Bastion card back: Shield pattern -->
<polygon points="70,25 110,45 110,100 70,130 30,100 30,45" fill="none" stroke="${colors.primary}" stroke-width="2"/>
<polygon points="70,35 100,50 100,95 70,118 40,95 40,50" fill="${colors.secondary}" opacity="0.15"/>
<!-- Cross pattern -->
<line x1="70" y1="35" x2="70" y2="118" stroke="${colors.primary}" stroke-width="1" opacity="0.4"/>
<line x1="40" y1="76" x2="100" y2="76" stroke="${colors.primary}" stroke-width="1" opacity="0.4"/>
<!-- Corner ornaments -->
<circle cx="35" cy="35" r="4" fill="none" stroke="${colors.secondary}" stroke-width="0.8"/>
<circle cx="105" cy="35" r="4" fill="none" stroke="${colors.secondary}" stroke-width="0.8"/>
<circle cx="35" cy="160" r="4" fill="none" stroke="${colors.secondary}" stroke-width="0.8"/>
<circle cx="105" cy="160" r="4" fill="none" stroke="${colors.secondary}" stroke-width="0.8"/>
</svg>`;
case 'wither':
return `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="195" viewBox="0 0 140 195">
<rect x="1" y="1" width="138" height="193" rx="8" fill="#1a1520"/>
<rect x="2" y="2" width="136" height="191" rx="7" fill="none" stroke="${colors.primary}" stroke-width="2.5"/>
<!-- Wither card back: Flame pattern -->
<path d="M70,30 Q85,50 80,70 Q90,65 85,80 Q95,75 88,95 Q100,90 90,110 Q80,120 70,130" fill="none" stroke="${colors.primary}" stroke-width="2"/>
<path d="M70,30 Q55,50 60,70 Q50,65 55,80 Q45,75 52,95 Q40,90 50,110 Q60,120 70,130" fill="none" stroke="${colors.primary}" stroke-width="2"/>
<!-- Inner flame glow -->
<path d="M70,45 Q80,60 76,75 Q82,72 78,85 Q86,82 80,98 Q88,95 82,108" fill="${colors.accent}" opacity="0.1"/>
<path d="M70,45 Q60,60 64,75 Q58,72 62,85 Q54,82 60,98 Q52,95 58,108" fill="${colors.accent}" opacity="0.1"/>
<!-- Ember dots -->
<circle cx="70" cy="35" r="2" fill="${colors.accent}" opacity="0.4"/>
<circle cx="60" cy="40" r="1.5" fill="${colors.primary}" opacity="0.3"/>
<circle cx="80" cy="40" r="1.5" fill="${colors.primary}" opacity="0.3"/>
</svg>`;
case 'shroud':
return `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="195" viewBox="0 0 140 195">
<rect x="1" y="1" width="138" height="193" rx="8" fill="#1a1520"/>
<rect x="2" y="2" width="136" height="191" rx="7" fill="none" stroke="${colors.primary}" stroke-width="2.5"/>
<!-- Shroud card back: Shadow eye pattern -->
<ellipse cx="70" cy="80" rx="35" ry="22" fill="none" stroke="${colors.primary}" stroke-width="2"/>
<circle cx="70" cy="80" r="14" fill="${colors.secondary}" opacity="0.2"/>
<circle cx="70" cy="80" r="8" fill="${colors.primary}" opacity="0.3"/>
<circle cx="70" cy="80" r="4" fill="#0a0810"/>
<!-- Shadow wisps -->
<path d="M35,80 Q25,65 30,50" fill="none" stroke="${colors.primary}" stroke-width="1.5" opacity="0.3"/>
<path d="M105,80 Q115,95 110,110" fill="none" stroke="${colors.primary}" stroke-width="1.5" opacity="0.3"/>
<path d="M42,62 Q35,55 38,45" fill="none" stroke="${colors.secondary}" stroke-width="1" opacity="0.2"/>
<path d="M98,98 Q105,105 102,115" fill="none" stroke="${colors.secondary}" stroke-width="1" opacity="0.2"/>
</svg>`;
}
}
function createEnemySVG(enemyDef, isBoss) {
const scale = isBoss ? 1.3 : 1;
const w = Math.round(80 * scale);
const h = Math.round(100 * scale);
let bodyPath = '';
switch (enemyDef.spriteShape) {
case 'goblin':
bodyPath = `<ellipse cx="${w / 2}" cy="${h * 0.65}" rx="${w * 0.35}" ry="${h * 0.3}" fill="${enemyDef.spriteColor}"/>
<circle cx="${w / 2}" cy="${h * 0.35}" r="${w * 0.22}" fill="${enemyDef.spriteColor}"/>
<polygon points="${w * 0.3},${h * 0.28} ${w * 0.4},${h * 0.15} ${w * 0.45},${h * 0.3}" fill="${enemyDef.spriteColor}"/>
<polygon points="${w * 0.7},${h * 0.28} ${w * 0.6},${h * 0.15} ${w * 0.55},${h * 0.3}" fill="${enemyDef.spriteColor}"/>`;
break;
case 'knight':
bodyPath = `<rect x="${w * 0.25}" y="${h * 0.4}" width="${w * 0.5}" height="${h * 0.45}" rx="8" fill="${enemyDef.spriteColor}"/>
<circle cx="${w / 2}" cy="${h * 0.3}" r="${w * 0.25}" fill="${enemyDef.spriteColor}"/>
<rect x="${w * 0.15}" y="${h * 0.45}" width="${w * 0.15}" height="${h * 0.3}" rx="4" fill="${enemyDef.spriteColor}"/>
<rect x="${w * 0.7}" y="${h * 0.45}" width="${w * 0.15}" height="${h * 0.3}" rx="4" fill="${enemyDef.spriteColor}"/>`;
break;
case 'warrior':
bodyPath = `<polygon points="${w / 2},${h * 0.15} ${w * 0.85},${h * 0.9} ${w * 0.15},${h * 0.9}" fill="${enemyDef.spriteColor}"/>
<circle cx="${w / 2}" cy="${h * 0.3}" r="${w * 0.2}" fill="${enemyDef.spriteColor}"/>`;
break;
case 'boss':
bodyPath = `<rect x="${w * 0.2}" y="${h * 0.35}" width="${w * 0.6}" height="${h * 0.5}" rx="10" fill="${enemyDef.spriteColor}"/>
<circle cx="${w / 2}" cy="${h * 0.25}" r="${w * 0.28}" fill="${enemyDef.spriteColor}"/>
<rect x="${w * 0.1}" y="${h * 0.4}" width="${w * 0.18}" height="${h * 0.35}" rx="6" fill="${enemyDef.spriteColor}"/>
<rect x="${w * 0.72}" y="${h * 0.4}" width="${w * 0.18}" height="${h * 0.35}" rx="6" fill="${enemyDef.spriteColor}"/>`;
break;
}
const eyeColor = isBoss ? '#ff3300' : '#ffcc00';
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
<defs>
<filter id="enemyGlow"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
</defs>
<g filter="url(#enemyGlow)">
${bodyPath}
<!-- Eyes -->
<circle cx="${w * 0.4}" cy="${h * 0.32}" r="${isBoss ? 5 : 3}" fill="${eyeColor}"/>
<circle cx="${w * 0.6}" cy="${h * 0.32}" r="${isBoss ? 5 : 3}" fill="${eyeColor}"/>
</g>
${isBoss ? `<text x="${w / 2}" y="${h + 14}" text-anchor="middle" font-size="8" fill="#cc3300" font-family="Georgia">★ BOSS ★</text>` : ''}
</svg>`;
}
};
// ── module: src/game-state.ts ──
__mods["src/game-state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createPlayer = createPlayer;
exports.createEnemyState = createEnemyState;
exports.createInitialState = createInitialState;
exports.shuffle = shuffle;
exports.drawCards = drawCards;
exports.discardHand = discardHand;
exports.applyEnemyStatusEffects = applyEnemyStatusEffects;
exports.applyPlayerStatusEffects = applyPlayerStatusEffects;
exports.tickStatusEffects = tickStatusEffects;
exports.getBossDamage = getBossDamage;
exports.applyBossStartTurn = applyBossStartTurn;
exports.canPlayCard = canPlayCard;
exports.getGoldReward = getGoldReward;
const cards_1 = require("./cards");
const enemies_1 = require("./enemies");
function cloneStatus(s) {
return { burn: s.burn, poison: s.poison, stun: s.stun, silence: s.silence };
}
function createPlayer(classId) {
const info = cards_1.CLASS_INFO[classId];
const starterIds = cards_1.CLASS_STARTER_DECKS[classId];
// Build inventory count
const inventory = {};
for (const id of starterIds) {
inventory[id] = (inventory[id] || 0) + 1;
}
// Create deck instances
const deck = [];
for (const id of starterIds) {
const def = (0, cards_1.getCardById)(id);
if (def)
deck.push((0, cards_1.createCardInstance)(def));
}
return {
classId,
hp: info.hp,
maxHp: info.hp,
energy: info.energy,
maxEnergy: info.energy,
handSize: info.handSize,
gold: 0,
defense: 0,
status: { burn: 0, poison: 0, stun: 0, silence: 0 },
deck,
discardPile: [],
inventory,
};
}
function createEnemyState(floor, isBoss) {
const def = isBoss ? enemies_1.BOSS_DEFS[floor] : (0, enemies_1.getRandomEnemy)(floor);
return {
def,
currentHp: def.hp,
maxHp: def.hp,
defense: 0,
status: { burn: 0, poison: 0, stun: 0, silence: 0 },
isBoss,
bossType: isBoss ? enemies_1.BOSS_TYPES[floor] : undefined,
};
}
function createInitialState(classId) {
return {
player: createPlayer(classId),
enemy: null,
hand: [],
floor: 1,
room: 0, // 0 = before first room
isBossRoom: false,
turnPhase: 'player',
combatActive: false,
gameOver: false,
};
}
// Shuffle array in place (Fisher-Yates)
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
function drawCards(state, count) {
// Move cards from discard to deck if needed
if (state.player.deck.length === 0 && state.player.discardPile.length > 0) {
state.player.deck.push(...state.player.discardPile);
state.player.discardPile = [];
shuffle(state.player.deck);
}
for (let i = 0; i < count; i++) {
if (state.player.deck.length === 0)
break;
const spaceLeft = state.player.handSize - state.hand.length;
if (spaceLeft <= 0)
break;
const card = state.player.deck.pop();
state.hand.push(card);
}
}
function discardHand(state) {
state.player.discardPile.push(...state.hand);
state.hand = [];
}
// Apply status effects at start of enemy turn (burn/poison damage to enemy)
function applyEnemyStatusEffects(enemy) {
let totalDmg = 0;
if (enemy.status.burn > 0) {
totalDmg += enemy.status.burn;
enemy.currentHp -= enemy.status.burn;
enemy.status.burn--;
}
if (enemy.status.poison > 0) {
totalDmg += enemy.status.poison;
enemy.currentHp -= enemy.status.poison;
enemy.status.poison--;
}
return totalDmg;
}
// Apply status effects at start of player turn (burn/poison damage to player)
function applyPlayerStatusEffects(player) {
let totalDmg = 0;
if (player.status.burn > 0) {
totalDmg += player.status.burn;
player.hp -= player.status.burn;
player.status.burn--;
}
if (player.status.poison > 0) {
totalDmg += player.status.poison;
player.hp -= player.status.poison;
player.status.poison--;
}
return totalDmg;
}
// Decrease stun/silence counters at end of turn
function tickStatusEffects(status) {
if (status.stun > 0)
status.stun--;
if (status.silence > 0)
status.silence--;
}
// Boss-specific behavior during enemy action
function getBossDamage(enemy, playerEnergy) {
switch (enemy.bossType) {
case 'gatekeeper':
// Normal damage
return enemy.def.damage;
case 'corrupted':
// Deals 20 if player has < 3 energy
return playerEnergy < 3 ? 20 : enemy.def.damage;
case 'voidlord':
// Normal damage, but also regenerates
return enemy.def.damage;
default:
return enemy.def.damage;
}
}
// Boss-specific behavior at start of their turn
function applyBossStartTurn(enemy) {
switch (enemy.bossType) {
case 'gatekeeper':
// Blocks 5 damage per turn until HP < 20
if (enemy.currentHp >= 20) {
enemy.defense = Math.max(enemy.defense, 5);
}
break;
case 'voidlord':
// Regenerates 10 HP per turn
enemy.currentHp = Math.min(enemy.maxHp + 40, enemy.currentHp + 10);
if (enemy.currentHp > enemy.maxHp) {
enemy.maxHp = enemy.currentHp;
}
break;
case 'corrupted':
// No special start-of-turn behavior
break;
}
}
// Check if a card can be played
function canPlayCard(card, player) {
return card.def.cost <= player.energy;
}
// Get gold reward for killing an enemy
function getGoldReward(enemy) {
return enemy.isBoss ? 15 : 5;
}
};
// ── module: src/combat.ts ──
__mods["src/combat.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.playCard = playCard;
exports.executeEnemyTurn = executeEnemyTurn;
exports.startCombat = startCombat;
exports.endPlayerTurn = endPlayerTurn;
exports.handleCardSideEffects = handleCardSideEffects;
const GS = __importStar(require("./game-state"));
// Play a card and resolve its effects
function playCard(card, player, enemy) {
const logs = [];
// Deduct energy
if (card.def.cost > player.energy)
return [{ type: 'message', text: 'Not enough energy!' }];
player.energy -= card.def.cost;
switch (card.def.type) {
case 'Attack':
resolveAttackCard(card, player, enemy, logs);
break;
case 'Defense':
resolveDefenseCard(card, player, enemy, logs);
break;
case 'Other':
resolveOtherCard(card, player, enemy, logs);
break;
}
return logs;
}
function resolveAttackCard(card, _player, enemy, logs) {
let damage = card.def.attack;
let unblockable = false;
// Special abilities per card
switch (card.def.id) {
case 'BA-A01': // Slam: If player has >=30 HP, draw 1
break; // Draw handled in UI after combat resolution
case 'SH-A01': // Backstab: +1 if target has no defense
if (enemy.defense <= 0)
damage += 1;
break;
case 'SH-A02': // Shadow Bolt: Cannot be blocked
unblockable = true;
break;
case 'SH-A03': // Silence: Stun target
enemy.status.stun = Math.max(enemy.status.stun, 1);
logs.push({ type: 'status', text: 'Stunned!', target: 'enemy' });
break;
case 'WI-A01': // Fireball: Apply 1 Burn
enemy.status.burn += 1;
logs.push({ type: 'status', text: '+1 Burn', target: 'enemy' });
break;
case 'WI-A02': // Inferno: Apply 3 Burn
enemy.status.burn += 3;
logs.push({ type: 'status', text: '+3 Burn', target: 'enemy' });
break;
case 'WI-A03': // Ember: Apply 1 Burn/turn (same as burn)
enemy.status.burn += 1;
logs.push({ type: 'status', text: '+1 Burn', target: 'enemy' });
break;
case 'WI-A04': // Pyre: All enemies take 1 extra next turn (simplified to +2 burn)
enemy.status.burn += 2;
logs.push({ type: 'status', text: '+2 Burn', target: 'enemy' });
break;
}
// Apply damage
if (!unblockable && enemy.defense > 0) {
const blocked = Math.min(damage, enemy.defense);
enemy.defense -= blocked;
damage -= blocked;
if (blocked > 0)
logs.push({ type: 'defense', text: `${blocked} Blocked`, target: 'enemy' });
}
if (damage > 0) {
enemy.currentHp -= damage;
logs.push({ type: 'damage', text: `-${damage}`, target: 'enemy' });
}
else if (!unblockable) {
logs.push({ type: 'message', text: 'Fully blocked!' });
}
else {
// Unblockable still deals full damage
enemy.currentHp -= card.def.attack;
logs.push({ type: 'damage', text: `-${card.def.attack} (unblockable)`, target: 'enemy' });
}
if (enemy.currentHp < 0)
enemy.currentHp = 0;
}
function resolveDefenseCard(card, player, _enemy, logs) {
switch (card.def.id) {
case 'BA-D02': // Iron Skin: Permanent defense + discard 1 (handled by UI)
player.maxHp += card.def.defense; // Simplified: permanent = increase max HP as buffer
player.hp += card.def.defense;
logs.push({ type: 'defense', text: `+${card.def.defense} Permanent`, target: 'player' });
logs.push({ type: 'discard', text: 'Discarded 1 card', target: 'player' });
break;
case 'BA-D04': // Taunt: Stun enemy
player.defense += card.def.defense;
logs.push({ type: 'defense', text: `+${card.def.defense} Def`, target: 'player' });
// Stun handled in UI
break;
case 'WI-D04': // Endure: Heal 2 HP
player.defense += card.def.defense;
logs.push({ type: 'defense', text: `+${card.def.defense} Def`, target: 'player' });
player.hp = Math.min(player.maxHp, player.hp + 2);
logs.push({ type: 'heal', text: '+2 HP', target: 'player' });
break;
case 'WI-D02': // Heat Resist: Gain 1 Energy
player.defense += card.def.defense;
logs.push({ type: 'defense', text: `+${card.def.defense} Def`, target: 'player' });
player.energy += 1;
logs.push({ type: 'energy', text: '+1 Energy', target: 'player' });
break;
case 'SH-D02': // Step: Gain 1 Energy
player.defense += card.def.defense;
logs.push({ type: 'defense', text: `+${card.def.defense} Def`, target: 'player' });
player.energy += 1;
logs.push({ type: 'energy', text: '+1 Energy', target: 'player' });
break;
default:
// Standard defense card
player.defense += card.def.defense;
logs.push({ type: 'defense', text: `+${card.def.defense} Def`, target: 'player' });
}
}
function resolveOtherCard(card, player, enemy, logs) {
switch (card.def.id) {
case 'BA-U01': // Search: Draw 1, discard if >5
logs.push({ type: 'draw', text: '+1 Draw', target: 'player' });
break;
case 'BA-U02': // Repair: Heal 6 HP
player.hp = Math.min(player.maxHp, player.hp + 6);
logs.push({ type: 'heal', text: '+6 HP', target: 'player' });
break;
case 'BA-U03': // Rally: Draw 1, Gain 1 Energy
logs.push({ type: 'draw', text: '+1 Draw', target: 'player' });
player.energy += 1;
logs.push({ type: 'energy', text: '+1 Energy', target: 'player' });
break;
case 'BA-U04': // Loot: Gain 5 Gold
player.gold += 5;
logs.push({ type: 'gold', text: '+5 Gold', target: 'player' });
break;
case 'WI-U01': // Poison: Apply 1 Poison
enemy.status.poison += 1;
logs.push({ type: 'status', text: '+1 Poison', target: 'enemy' });
break;
case 'WI-U02': // Burn Wave: 3 dmg + 2 Burn
let dmg = 3;
if (enemy.defense > 0) {
const blocked = Math.min(dmg, enemy.defense);
enemy.defense -= blocked;
dmg -= blocked;
}
if (dmg > 0) {
enemy.currentHp -= dmg;
logs.push({ type: 'damage', text: `-${dmg}`, target: 'enemy' });
}
enemy.status.burn += 2;
logs.push({ type: 'status', text: '+2 Burn', target: 'enemy' });
break;
case 'WI-U03': // Slow: Draw 1, Stun target
logs.push({ type: 'draw', text: '+1 Draw', target: 'player' });
enemy.status.stun = Math.max(enemy.status.stun, 1);
logs.push({ type: 'status', text: 'Stunned!', target: 'enemy' });
break;
case 'WI-U04': // Wither: Heal 5 HP
player.hp = Math.min(player.maxHp, player.hp + 5);
logs.push({ type: 'heal', text: '+5 HP', target: 'player' });
break;
case 'SH-U01': // Dash: Gain 1 Energy
player.energy += 1;
logs.push({ type: 'energy', text: '+1 Energy', target: 'player' });
break;
case 'SH-U02': // Teleport: Deal 2 damage
let tDmg = 2;
if (enemy.defense > 0) {
const blocked = Math.min(tDmg, enemy.defense);
enemy.defense -= blocked;
tDmg -= blocked;
}
if (tDmg > 0) {
enemy.currentHp -= tDmg;
logs.push({ type: 'damage', text: `-${tDmg}`, target: 'enemy' });
}
break;
case 'SH-U03': // Hide: Gain 1 Def, Discard 1, Draw 2
player.defense += 1;
logs.push({ type: 'defense', text: '+1 Def', target: 'player' });
logs.push({ type: 'discard', text: 'Discarded 1 card', target: 'player' });
logs.push({ type: 'draw', text: '+2 Draw', target: 'player' });
break;
case 'SH-U04': // Assassinate: Deal 8 unblockable damage
enemy.currentHp -= 8;
logs.push({ type: 'damage', text: '-8 (unblockable)', target: 'enemy' });
break;
default:
logs.push({ type: 'message', text: 'Card played.' });
}
if (enemy.currentHp < 0)
enemy.currentHp = 0;
}
// Execute the enemy's turn
function executeEnemyTurn(player, enemy) {
const logs = [];
// Check stun
if (enemy.status.stun > 0) {
GS.tickStatusEffects(enemy.status);
logs.push({ type: 'message', text: `${enemy.def.name} is stunned!` });
return logs;
}
// Boss start-of-turn effects
GS.applyBossStartTurn(enemy);
// Calculate damage
let damage = GS.getBossDamage(enemy, player.energy);
// Apply damage to player (defense absorbs first)
if (player.defense > 0) {
const blocked = Math.min(damage, player.defense);
player.defense -= blocked;
damage -= blocked;
logs.push({ type: 'defense', text: `${blocked} Blocked`, target: 'player' });
}
if (damage > 0) {
player.hp -= damage;
logs.push({ type: 'damage', text: `-${damage}`, target: 'player' });
}
else {
logs.push({ type: 'message', text: 'Attack fully blocked!' });
}
if (player.hp < 0)
player.hp = 0;
// Tick enemy status effects
GS.tickStatusEffects(enemy.status);
return logs;
}
// Start a new combat encounter
function startCombat(state) {
state.enemy = GS.createEnemyState(state.floor, state.isBossRoom);
state.combatActive = true;
state.turnPhase = 'player';
// Clear hand from previous combat
state.hand = [];
// Reshuffle discard into deck if needed
if (state.player.deck.length === 0 && state.player.discardPile.length > 0) {
state.player.deck.push(...state.player.discardPile);
state.player.discardPile = [];
}
// Shuffle and draw starting hand
GS.shuffle(state.player.deck);
const drawCount = Math.min(state.player.handSize, state.player.deck.length);
for (let i = 0; i < drawCount; i++) {
if (state.player.deck.length > 0) {
state.hand.push(state.player.deck.pop());
}
}
// Reset per-turn values
state.player.defense = 0;
state.player.energy = state.player.maxEnergy;
}
// End player turn, start enemy turn
function endPlayerTurn(state) {
const result = { logs: [], enemyDead: false, playerDead: false };
// Apply burn/poison to enemy from previous turns
const statusDmg = GS.applyEnemyStatusEffects(state.enemy);
if (statusDmg > 0) {
result.logs.push({ type: 'damage', text: `-${statusDmg} Status`, target: 'enemy' });
}
// Check enemy death from status damage
if (state.enemy.currentHp <= 0) {
state.enemy.currentHp = 0;
const gold = GS.getGoldReward(state.enemy);
state.player.gold += gold;
result.logs.push({ type: 'gold', text: `+${gold} Gold`, target: 'player' });
result.enemyDead = true;
return result;
}
// Enemy turn
const enemyLogs = executeEnemyTurn(state.player, state.enemy);
result.logs.push(...enemyLogs);
// Check player death
if (state.player.hp <= 0) {
state.player.hp = 0;
result.playerDead = true;
return result;
}
// Tick player status effects
GS.tickStatusEffects(state.player.status);
// Start new player turn
state.turnPhase = 'player';
state.player.defense = 0; // Defense expires at end of turn
state.player.energy = Math.min(state.player.maxEnergy, state.player.energy + 1);
// Discard hand and draw new cards
GS.discardHand(state);
// Reshuffle discard pile into deck if needed
if (state.player.deck.length === 0 && state.player.discardPile.length > 0) {
state.player.deck.push(...state.player.discardPile);
state.player.discardPile = [];
GS.shuffle(state.player.deck);
}
const drawCount = Math.min(state.player.handSize, state.player.deck.length);
for (let i = 0; i < drawCount; i++) {
if (state.player.deck.length > 0) {
state.hand.push(state.player.deck.pop());
}
}
return result;
}
// Handle special card effects that need game state access
function handleCardSideEffects(card, player, enemy) {
const logs = [];
switch (card.def.id) {
case 'BA-A01': // Slam: If >=30 HP, draw 1
if (player.hp >= 30) {
logs.push({ type: 'draw', text: '+1 Draw (Slam)', target: 'player' });
}
break;
case 'BA-D04': // Taunt: Stun enemy
enemy.status.stun = Math.max(enemy.status.stun, 1);
logs.push({ type: 'status', text: 'Enemy Stunned!', target: 'enemy' });
break;
case 'SH-A04': // Vengeance: Gain 2 Energy (already handled in defense)
player.energy += 2;
logs.push({ type: 'energy', text: '+2 Energy', target: 'player' });
break;
}
return logs;
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.cacheElements = cacheElements;
exports.renderClassSelect = renderClassSelect;
exports.enterNextRoom = enterNextRoom;
exports.renderCombat = renderCombat;
exports.onEndTurn = onEndTurn;
exports.renderShop = renderShop;
exports.getGameState = getGameState;
exports.setEndTurnHandler = setEndTurnHandler;
const GS = __importStar(require("./game-state"));
const Cards = __importStar(require("./cards"));
const Combat = __importStar(require("./combat"));
const SVG = __importStar(require("./card-svg"));
// DOM element references
let elements = {};
function cacheElements() {
const ids = [
'screen-class-select', 'screen-combat', 'screen-shop', 'screen-boss-intro',
'screen-game-over', 'class-cards', 'hand-container', 'enemy-sprite',
'enemy-name', 'enemy-hp-fill', 'enemy-hp-text', 'enemy-statuses',
'player-hp-text', 'player-hp-fill', 'player-defense', 'player-statuses',
'hud-floor', 'hud-room', 'hud-gold', 'hud-energy', 'btn-end-turn',
'shop-cards', 'shop-gold-display', 'btn-leave-shop',
'boss-name', 'boss-desc', 'btn-enter-combat',
'game-over-title', 'game-over-text', 'btn-restart',
'card-detail-overlay', 'card-detail-content',
];
for (const id of ids) {
elements[id] = document.getElementById(id);
}
// Create ambient particles
createParticles();
}
function createParticles() {
const container = document.getElementById('particles-container');
if (!container)
return;
const colors = ['rgba(201,168,76,0.3)', 'rgba(150,130,100,0.2)', 'rgba(100,80,60,0.2)'];
for (let i = 0; i < 20; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
const size = Math.random() * 3 + 1;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${Math.random() * 100}%`;
particle.style.background = colors[Math.floor(Math.random() * colors.length)];
particle.style.animationDuration = `${Math.random() * 15 + 10}s`;
particle.style.animationDelay = `${Math.random() * 10}s`;
container.appendChild(particle);
}
}
function showScreen(name) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const el = elements[name];
if (el)
el.classList.add('active');
}
// ===== CLASS SELECT SCREEN =====
function renderClassSelect() {
showScreen('screen-class-select');
const container = elements['class-cards'];
container.innerHTML = '';
const classes = [
{ id: 'bastion', name: 'The Bastion', theme: 'Immovable Object', hp: 35, energy: 3 },
{ id: 'wither', name: 'The Wither', theme: 'Rot and Burn', hp: 30, energy: 3 },
{ id: 'shroud', name: 'The Shroud', theme: 'Shadow and Speed', hp: 28, energy: 3 },
];
const classColors = {
bastion: '#c9a84c', wither: '#cc3300', shroud: '#7a3abf',
};
for (const cls of classes) {
const card = document.createElement('div');
card.className = 'class-card';
card.style.border = `2px solid ${classColors[cls.id]}`;
card.style.background = `linear-gradient(180deg, rgba(${hexToRgb(classColors[cls.id])},0.15) 0%, #1a1520 60%)`;
// Show a sample card back for visual flair
const sampleCardDef = { id: 'SAMPLE', name: cls.name, type: 'Attack', cost: 1, attack: 3, defense: 0, ability: '', description: '', classId: cls.id };
const svgStr = SVG.createCardBackSVG(cls.id);
card.innerHTML = `
<div style="width:120px;height:165px;margin-bottom:8px;transform:scale(0.7);">${svgStr}</div>
<div class="class-name" style="color:${classColors[cls.id]}">${cls.name}</div>
<div class="class-theme">"${cls.theme}"</div>
<div class="class-stats">HP: ${cls.hp} | Energy: ${cls.energy}<br/>Hand Size: 4<br/>Deck: 10 cards</div>
`;
card.addEventListener('click', () => {
startGame(cls.id);
});
container.appendChild(card);
}
}
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `${r},${g},${b}`;
}
// ===== GAME START =====
let gameState = null;
let drawQueue = 0; // Cards to draw after current play
function startGame(classId) {
gameState = GS.createInitialState(classId);
enterNextRoom();
}
// ===== ROOM MANAGEMENT =====
let firstRoom = true;
function enterNextRoom() {
if (!gameState)
return;
gameState.room++;
// Boss room at the end of each floor (room 9 per floor)
const roomsPerFloor = 9;
if (gameState.room % roomsPerFloor === 0) {
gameState.isBossRoom = true;
showBossIntro();
}
else {
startCombatEncounter();
}
firstRoom = false;
}
function showBossIntro() {
if (!gameState)
return;
showScreen('screen-boss-intro');
const bossNames = ['', 'The Gatekeeper', 'The Corrupted', 'The Void Lord'];
const bossDescs = [
'',
'"Blocks 5 damage per turn until HP drops below 20. An immovable force guarding the descent."',
'"Deals devastating 20 damage if you have less than 3 Energy. A corrupted knight consumed by darkness."',
'"Regenerates 10 HP per turn and cannot be blocked. The final horror that consumes all of Aethelgard."',
];
const nameEl = elements['boss-name'];
const descEl = elements['boss-desc'];
if (nameEl)
nameEl.textContent = `Floor ${gameState.floor} Boss: ${bossNames[gameState.floor]}`;
if (descEl)
descEl.textContent = bossDescs[gameState.floor];
const btn = elements['btn-enter-combat'];
if (btn) {
btn.onclick = () => startCombatEncounter();
}
}
function startCombatEncounter() {
if (!gameState)
return;
const gs = gameState; // capture for callback
// Skip transition on first room for faster startup
if (firstRoom) {
Combat.startCombat(gs);
showScreen('screen-combat');
renderCombat();
return;
}
// Show room transition
const isBoss = gs.isBossRoom;
const roomsPerFloor = 9;
const roomInFloor = ((gs.room - 1) % roomsPerFloor) + 1;
showRoomTransition(isBoss ? `⚔ Floor ${gs.floor} Boss ⚔` : `Floor ${gs.floor} — Room ${roomInFloor}`, () => {
Combat.startCombat(gs);
showScreen('screen-combat');
renderCombat();
});
}
function showRoomTransition(text, callback) {
const overlay = document.getElementById('room-transition');
if (!overlay)
return;
const h2 = overlay.querySelector('h2');
if (h2)
h2.textContent = text;
overlay.classList.add('active');
setTimeout(() => {
overlay.classList.remove('active');
setTimeout(callback, 150);
}, 600);
}
// ===== COMBAT RENDERING =====
function renderCombat() {
if (!gameState || !gameState.enemy)
return;
const p = gameState.player;
const e = gameState.enemy;
// HUD top
setText('hud-floor', `Floor ${gameState.floor}`);
setText('hud-room', `Room ${gameState.room}`);
setText('hud-gold', `💰 ${p.gold}`);
// Energy pips
let energyHTML = '<span style="color:#cc9900;margin-right:4px">⚡</span>';
for (let i = 0; i < p.maxEnergy; i++) {
const filled = i < p.energy;
energyHTML += `<span class="energy-pip ${filled ? 'filled' : 'empty'}"></span>`;
}
setHTML('hud-energy', energyHTML);
// Enemy
const enemySvg = SVG.createEnemySVG(e.def, e.isBoss);
setHTML('enemy-sprite', enemySvg);
setText('enemy-name', `${e.def.name}${e.isBoss ? ' ★' : ''}`);
const hpPercent = Math.max(0, (e.currentHp / e.maxHp) * 100);
setStyle('enemy-hp-fill', 'width', `${hpPercent}%`);
setText('enemy-hp-text', `${e.currentHp}/${e.maxHp} HP`);
// Enemy statuses
renderStatuses('enemy-statuses', e.status, e.defense > 0 ? [`🛡${e.defense}`] : []);
// Player
const playerHpPercent = Math.max(0, (p.hp / p.maxHp) * 100);
setStyle('player-hp-fill', 'width', `${playerHpPercent}%`);
setText('player-hp-text', `❤️ ${p.hp}/${p.maxHp}`);
setText('player-defense', p.defense > 0 ? `🛡 ${p.defense} Defense` : '');
renderStatuses('player-statuses', p.status, []);
// Hand
renderHand();
// End turn button state
const btn = elements['btn-end-turn'];
if (btn) {
btn.disabled = gameState.turnPhase !== 'player';
}
}
function renderStatuses(containerId, status, extras) {
const container = elements[containerId];
if (!container)
return;
let html = '';
if (status.burn > 0)
html += `<span class="status-badge" style="border-color:#cc6600;color:#ff9944">🔥 Burn ${status.burn}</span>`;
if (status.poison > 0)
html += `<span class="status-badge" style="border-color:#44aa00;color:#88dd44">☠ Poison ${status.poison}</span>`;
if (status.stun > 0)
html += `<span class="status-badge" style="border-color:#ffcc00;color:#ffee66">💫 Stunned</span>`;
if (status.silence > 0)
html += `<span class="status-badge" style="border-color:#aa44ff;color:#cc88ff">🔇 Silenced</span>`;
for (const ex of extras) {
html += `<span class="status-badge">${ex}</span>`;
}
container.innerHTML = html;
}
function renderHand() {
if (!gameState)
return;
const container = elements['hand-container'];
if (!container)
return;
container.innerHTML = '';
for (let i = 0; i < gameState.hand.length; i++) {
const card = gameState.hand[i];
const playable = GS.canPlayCard(card, gameState.player);
const div = document.createElement('div');
div.className = `game-card ${playable ? 'playable' : 'unplayable'} card-type-${card.def.type.toLowerCase()}`;
div.innerHTML = SVG.createCardSVG(card.def);
// Stagger cards slightly
const offset = (i - gameState.hand.length / 2) * 15;
div.style.marginLeft = `${offset}px`;
if (playable) {
div.addEventListener('click', () => playCardFromHand(i));
}
container.appendChild(div);
}
}
// ===== CARD PLAYING =====
function playCardFromHand(index) {
if (!gameState || gameState.turnPhase !== 'player')
return;
const card = gameState.hand[index];
if (!GS.canPlayCard(card, gameState.player))
return;
// Remove from hand and add to discard pile (played cards go to discard)
gameState.hand.splice(index, 1);
gameState.player.discardPile.push(card);
// Play the card
const logs = Combat.playCard(card, gameState.player, gameState.enemy);
// Handle side effects (draws, discards)
const sideEffects = Combat.handleCardSideEffects(card, gameState.player, gameState.enemy);
// Process draws from side effects
for (const log of [...logs, ...sideEffects]) {
if (log.type === 'draw') {
const count = parseInt(log.text.match(/\+(\d+)/)?.[1] || '0');
drawQueue += count;
}
if (log.type === 'discard') {
// Will discard from hand after card play resolves
if (gameState.hand.length > 0) {
const discarded = gameState.hand.pop();
gameState.player.discardPile.push(discarded);
}
}
}
// Process draws
for (let i = 0; i < drawQueue; i++) {
if (gameState.player.deck.length > 0 && gameState.hand.length < gameState.player.handSize + 2) {
const drawn = gameState.player.deck.pop();
gameState.hand.push(drawn);
}
else if (gameState.player.deck.length === 0 && gameState.player.discardPile.length > 0) {
GS.shuffle(gameState.player.discardPile);
gameState.player.deck.push(...gameState.player.discardPile);
gameState.player.discardPile = [];
if (gameState.player.deck.length > 0 && gameState.hand.length < gameState.player.handSize + 2) {
const drawn = gameState.player.deck.pop();
gameState.hand.push(drawn);
}
}
}
drawQueue = 0;
// Show card play flash effect
const arena = document.getElementById('combat-arena');
if (arena) {
const flash = document.createElement('div');
flash.className = 'card-play-flash';
arena.appendChild(flash);
setTimeout(() => flash.remove(), 300);
}
// Show float text for damage/heal
showCombatFeedback(logs, sideEffects);
// Animate enemy hit if damage dealt
const hasDamage = logs.some(l => l.type === 'damage' && l.target === 'enemy');
if (hasDamage) {
const spriteEl = elements['enemy-sprite'];
if (spriteEl) {
spriteEl.classList.add('hit');
setTimeout(() => spriteEl.classList.remove('hit'), 300);
}
}
// Check enemy death
if (gameState.enemy.currentHp <= 0) {
gameState.enemy.currentHp = 0;
const gold = GS.getGoldReward(gameState.enemy);
gameState.player.gold += gold;
setTimeout(() => {
showScreen('screen-shop');
renderShop();
}, 600);
return;
}
// Check player death (shouldn't happen from card play, but just in case)
if (gameState.player.hp <= 0) {
gameState.gameOver = true;
setTimeout(() => showGameOver(false), 500);
return;
}
renderCombat();
}
function showCombatFeedback(logs, sideEffects) {
const allLogs = [...logs, ...sideEffects];
const arena = document.getElementById('combat-arena');
if (!arena)
return;
for (const log of allLogs) {
if (log.type === 'damage' || log.type === 'heal') {
const floatEl = document.createElement('div');
floatEl.className = 'float-text';
floatEl.textContent = log.text;
if (log.target === 'enemy') {
floatEl.style.color = '#ff4444';
floatEl.style.left = '50%';
floatEl.style.top = '30%';
}
else {
floatEl.style.color = log.type === 'heal' ? '#44ff44' : '#ff4444';
floatEl.style.left = '50%';
floatEl.style.bottom = '20%';
}
arena.appendChild(floatEl);
setTimeout(() => floatEl.remove(), 1000);
}
}
}
// ===== END TURN =====
function onEndTurn() {
if (!gameState || gameState.turnPhase !== 'player')
return;
const gs = gameState; // capture for async callbacks
// Show turn indicator
showTurnIndicator('Enemy Turn');
const result = Combat.endPlayerTurn(gs);
renderCombat();
// Process results
setTimeout(() => {
if (result.enemyDead) {
setTimeout(() => {
showScreen('screen-shop');
renderShop();
}, 500);
}
else if (result.playerDead) {
gs.gameOver = true;
setTimeout(() => showGameOver(false), 500);
}
else {
// Show player turn indicator after enemy action
setTimeout(() => showTurnIndicator('Your Turn'), 800);
}
}, 300);
}
function showTurnIndicator(text) {
let indicator = document.getElementById('turn-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'turn-indicator';
document.getElementById('game-container')?.appendChild(indicator);
}
indicator.textContent = text;
indicator.classList.remove('show');
void indicator.offsetWidth; // Force reflow
indicator.classList.add('show');
}
// ===== SHOP =====
function renderShop() {
if (!gameState)
return;
const p = gameState.player;
// Gold display with heal option
const goldEl = elements['shop-gold-display'];
if (goldEl) {
const healCost = Math.max(1, Math.floor((p.maxHp - p.hp) * 2));
const canHeal = p.gold >= healCost && p.hp < p.maxHp;
goldEl.innerHTML = `Gold: ${p.gold} | ` +
`<button id="btn-heal" style="padding:4px 12px;font-size:0.85em;" ${canHeal ? '' : 'disabled'}>❤️ Heal (${healCost}g)</button>`;
const healBtn = document.getElementById('btn-heal');
if (healBtn && canHeal) {
healBtn.onclick = () => {
p.gold -= healCost;
p.hp = Math.min(p.maxHp, p.hp + Math.floor(healCost / 2));
renderShop();
};
}
}
const container = elements['shop-cards'];
if (!container)
return;
container.innerHTML = '';
// Generate shop items - random cards from player's class
const availableCards = generateShopItems(p.classId, p.inventory);
for (const item of availableCards) {
const wrapper = document.createElement('div');
wrapper.className = 'shop-card-wrapper';
const canAfford = p.gold >= item.cost;
const maxOwned = item.ownedCount >= 5;
const cardDiv = document.createElement('div');
cardDiv.className = `game-card ${canAfford && !maxOwned ? 'playable' : 'unplayable'}`;
cardDiv.innerHTML = SVG.createCardSVG(item.cardDef);
if (!canAfford || maxOwned) {
cardDiv.style.opacity = '0.5';
}
const priceEl = document.createElement('div');
priceEl.className = 'shop-card-price';
priceEl.textContent = maxOwned ? 'MAX' : `💰 ${item.cost}`;
const countEl = document.createElement('div');
countEl.className = 'shop-card-count';
countEl.textContent = `Owned: ${item.ownedCount}/5`;
wrapper.appendChild(cardDiv);
wrapper.appendChild(priceEl);
wrapper.appendChild(countEl);
if (canAfford && !maxOwned) {
cardDiv.addEventListener('click', () => buyCard(item));
}
container.appendChild(wrapper);
}
const leaveBtn = elements['btn-leave-shop'];
if (leaveBtn) {
leaveBtn.onclick = () => leaveShop();
}
}
function buyCard(item) {
if (!gameState)
return;
if (gameState.player.gold < item.cost || item.ownedCount >= 5)
return;
gameState.player.gold -= item.cost;
const instance = Cards.createCardInstance(item.cardDef);
gameState.player.deck.push(instance);
gameState.player.inventory[item.cardDef.id] = (gameState.player.inventory[item.cardDef.id] || 0) + 1;
renderShop();
}
function leaveShop() {
if (!gameState)
return;
const roomsPerFloor = 9;
// Check if we just defeated a boss (room is multiple of 9)
if (gameState.room % roomsPerFloor === 0 && gameState.floor < 3) {
// Move to next floor - start fresh room count
gameState.floor++;
gameState.isBossRoom = false;
enterNextRoom();
}
else if (gameState.room % roomsPerFloor === 0 && gameState.floor === 3) {
// Victory! Defeated Floor 3 boss
showGameOver(true);
}
else {
// Normal room progression
enterNextRoom();
}
}
// ===== GAME OVER =====
function showGameOver(victory) {
const gs = gameState; // capture before potential null issues
showScreen('screen-game-over');
const titleEl = elements['game-over-title'];
const textEl = elements['game-over-text'];
if (victory) {
titleEl.textContent = 'Victory!';
titleEl.style.color = '#c9a84c';
textEl.textContent = 'You have defeated the Void Lord and saved Aethelgard!';
}
else {
titleEl.textContent = 'Defeated...';
titleEl.style.color = '#cc3300';
textEl.textContent = `The Void consumes you on Floor ${gs?.floor ?? '?'}, Room ${gs?.room ?? '?'}.`;
}
const btn = elements['btn-restart'];
if (btn) {
btn.onclick = () => renderClassSelect();
}
}
// ===== SHOP GENERATION =====
function generateShopItems(classId, inventory) {
const classCards = Cards.getCardsByClass(classId);
// Pick 4-6 random cards for the shop
const shuffled = [...classCards];
GS.shuffle(shuffled);
const selected = shuffled.slice(0, 5);
return selected.map(cardDef => {
const ownedCount = inventory[cardDef.id] || 0;
let cost;
if (ownedCount >= 3) {
cost = 10 + (ownedCount - 2) * 5; // Duplication penalty
}
else {
cost = cardDef.cost + ownedCount; // Base cost scales with ownership
}
cost = Math.min(cost, 50); // Cap at 50
return { cardDef, cost, ownedCount };
});
}
// ===== UTILITY FUNCTIONS =====
function setText(id, text) {
const el = elements[id];
if (el)
el.textContent = text;
}
function setHTML(id, html) {
const el = elements[id];
if (el)
el.innerHTML = html;
}
function setStyle(id, prop, value) {
const el = elements[id];
if (el)
el.style.setProperty(prop, value);
}
// Export for main.ts to call
function getGameState() {
return gameState;
}
function setEndTurnHandler(handler) {
const btn = elements['btn-end-turn'];
if (btn)
btn.onclick = handler;
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"d7b3f5f8-e130-44db-930f-af1f837722c8","tokensIn":4699497,"tokensOut":56768,"tokensTotal":4756265,"turns":85,"toolCalls":84,"failedToolCalls":2,"timestamp":"2026-07-30T19:42:56.515Z"} -->