Deckbuilder
Qwen 3.6 27b MTP · composer

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 56,414 bytes · SHA-256 73323c3bb152
<!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: base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700;900&family=Raleway:wght@300;400;600&display=swap');
body {
background: #0a0a12;
color: #d4c5a9;
font-family: 'Raleway', sans-serif;
overflow: hidden;
height: 100vh;
width: 100vw;
}
/* Atmospheric background */
body::before {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background:
radial-gradient(ellipse at 50% 30%, rgba(40, 20, 60, 0.4) 0%, transparent 70%),
radial-gradient(ellipse at 20% 80%, rgba(20, 10, 40, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 80% 60%, rgba(30, 15, 50, 0.2) 0%, transparent 50%),
linear-gradient(180deg, #0a0a14 0%, #0d0b18 50%, #0a0a12 100%);
z-index: -1;
}
/* Floating particles */
body::after {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-image:
radial-gradient(1px 1px at 20% 30%, rgba(201, 168, 76, 0.15), transparent),
radial-gradient(1px 1px at 40% 70%, rgba(201, 168, 76, 0.1), transparent),
radial-gradient(1px 1px at 60% 20%, rgba(201, 168, 76, 0.12), transparent),
radial-gradient(1px 1px at 80% 50%, rgba(201, 168, 76, 0.08), transparent),
radial-gradient(1px 1px at 30% 90%, rgba(201, 168, 76, 0.1), transparent);
z-index: -1;
animation: particleDrift 20s linear infinite;
}
@keyframes particleDrift {
0% { transform: translateY(0); }
100% { transform: translateY(-20px); }
}
.screen {
display: none;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.screen.active { display: flex; }
.game-title {
font-family: 'Cinzel', serif;
font-size: clamp(1.8em, 4vw, 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: 10px;
letter-spacing: 3px;
}
.subtitle {
font-size: clamp(0.8em, 1.5vw, 1.1em);
color: #8a7e6b;
margin-bottom: 40px;
font-style: italic;
}
.action-btn {
background: linear-gradient(180deg, #3a2f25 0%, #1a1510 100%);
border: 2px solid #6b5d45;
color: #c9a84c;
font-family: 'Cinzel', serif;
font-size: clamp(0.8em, 1.5vw, 1.1em);
padding: 12px 32px;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 2px;
}
.action-btn:hover {
background: linear-gradient(180deg, #4a3f35 0%, #2a2520 100%);
border-color: #c9a84c;
box-shadow: 0 0 15px rgba(201, 168, 76, 0.3);
}
.action-btn:active { transform: scale(0.97); }
/* ── css: class-select ── */
#class-cards {
display: flex;
gap: clamp(12px, 3vw, 30px);
justify-content: center;
flex-wrap: wrap;
}
.class-card {
width: clamp(200px, 25vw, 260px);
background: linear-gradient(180deg, #1a1520 0%, #0d0b14 100%);
border: 2px solid #3a3040;
padding: clamp(16px, 2vw, 24px);
cursor: pointer;
transition: all 0.3s;
text-align: center;
position: relative;
}
.class-card:hover {
transform: translateY(-8px);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.class-card.bastion:hover { border-color: #7a6b4f; box-shadow: 0 10px 30px rgba(122, 107, 79, 0.3); }
.class-card.wither:hover { border-color: #8b4513; box-shadow: 0 10px 30px rgba(139, 69, 19, 0.3); }
.class-card.shroud:hover { border-color: #4a3d7a; box-shadow: 0 10px 30px rgba(74, 61, 122, 0.3); }
.class-icon { font-size: clamp(2em, 5vw, 3em); margin-bottom: 8px; }
.class-name { font-family: 'Cinzel', serif; font-size: clamp(1em, 2vw, 1.5em); color: #c9a84c; margin-bottom: 4px; }
.class-theme { font-size: clamp(0.6em, 1vw, 0.85em); color: #7a6e5b; font-style: italic; margin-bottom: 8px; }
.class-desc { font-size: clamp(0.6em, 1vw, 0.85em); line-height: 1.4; color: #9a8e7b; }
.class-stats {
display: flex;
justify-content: space-around;
margin-top: clamp(8px, 1.5vw, 16px);
padding-top: clamp(8px, 1vw, 12px);
border-top: 1px solid #2a2530;
}
.class-stat { text-align: center; }
.class-stat-val { font-family: 'Cinzel', serif; font-size: clamp(0.9em, 1.5vw, 1.2em); color: #c9a84c; }
.class-stat-label { font-size: clamp(0.5em, 0.7vw, 0.7em); color: #6a5e4b; text-transform: uppercase; }
/* Card back preview on class cards */
.card-back-preview {
position: absolute;
top: -10px;
right: -10px;
width: clamp(40px, 5vw, 60px);
height: clamp(57px, 7vh, 84px);
opacity: 0.3;
transform: rotate(12deg);
pointer-events: none;
}
.card-back-preview svg { width: 100%; height: 100%; }
/* ── css: combat-layout ── */
#screen-combat { justify-content: flex-start; padding: 0; }
#combat-hud-top {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 20px;
background: linear-gradient(180deg, rgba(26, 21, 32, 0.95) 0%, transparent);
font-family: 'Cinzel', serif;
color: #c9a84c;
z-index: 10;
font-size: clamp(0.7em, 1.2vw, 0.95em);
}
#combat-arena {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 5px 20px 15px;
position: relative;
}
/* Divider line between enemy and player */
#combat-arena::before {
content: '';
position: absolute;
left: 10%; right: 10%;
top: 48%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(201, 168, 76, 0.2), transparent);
}
#enemy-area {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 5px;
position: relative;
}
#enemy-display {
width: clamp(140px, 20vw, 200px);
height: clamp(160px, 25vh, 220px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
}
#enemy-name {
font-family: 'Cinzel', serif;
color: #c9a84c;
margin-bottom: 6px;
font-size: clamp(0.7em, 1.2vw, 1em);
text-shadow: 0 0 10px rgba(201, 168, 76, 0.3);
}
.enemy-sprite {
font-size: clamp(3em, 8vw, 5em);
filter: drop-shadow(0 0 15px rgba(139, 69, 19, 0.4));
transition: transform 0.2s;
}
.enemy-sprite.hit { animation: enemyHit 0.3s ease-out; }
@keyframes enemyHit {
0% { transform: scale(1); filter: brightness(3) drop-shadow(0 0 15px red); }
25% { transform: translateX(-6px) scale(0.95); }
50% { transform: translateX(4px) scale(0.97); }
100% { transform: translateX(0) scale(1); filter: none; }
}
#enemy-hp-bar-container {
width: clamp(120px, 18vw, 200px);
height: 14px;
background: #1a1520;
border: 1px solid #3a3040;
margin-top: 6px;
position: relative;
overflow: hidden;
}
#enemy-hp-bar {
height: 100%;
background: linear-gradient(90deg, #8b2500, #cc3300);
transition: width 0.4s ease-out;
box-shadow: inset 0 -2px 4px rgba(0,0,0,0.3), 0 0 6px rgba(204, 51, 0, 0.3);
}
#enemy-hp-text {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-family: 'Cinzel', serif;
font-size: 0.6em;
color: #fff;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
}
#enemy-status-effects {
display: flex;
gap: 4px;
margin-top: 4px;
min-height: 18px;
flex-wrap: wrap;
justify-content: center;
}
.status-badge {
padding: 2px 6px;
font-size: 0.6em;
border-radius: 3px;
text-transform: uppercase;
letter-spacing: 1px;
}
.status-burn { background: #4a2500; color: #ff9900; border: 1px solid #6b3d00; }
.status-poison { background: #0a2a0a; color: #66cc66; border: 1px solid #1a4a1a; }
.status-stun { background: #2a2a0a; color: #cccc00; border: 1px solid #4a4a1a; }
#battle-log {
width: min(90%, 600px);
height: clamp(50px, 8vh, 80px);
overflow-y: auto;
background: rgba(10, 10, 18, 0.7);
border: 1px solid #2a2530;
padding: 6px 10px;
font-size: clamp(0.6em, 1vw, 0.75em);
color: #8a7e6b;
margin: 4px 0;
}
#battle-log .log-entry { padding: 2px 0; border-bottom: 1px solid rgba(42, 37, 48, 0.5); }
#battle-log .log-damage { color: #cc6644; }
#battle-log .log-defense { color: #6699cc; }
#battle-log .log-heal { color: #66cc66; }
#battle-log .log-gold { color: #c9a84c; }
#player-area {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
#player-stats {
display: flex;
gap: clamp(12px, 3vw, 24px);
margin-bottom: 6px;
font-family: 'Cinzel', serif;
font-size: clamp(0.7em, 1.2vw, 0.95em);
}
.stat-icon { margin-right: 4px; }
.hp-icon { color: #cc3300; }
.energy-icon { color: #6699ff; }
.def-icon { color: #88aacc; }
#hand-container {
display: flex;
gap: clamp(6px, 1.5vw, 12px);
justify-content: center;
align-items: flex-end;
min-height: clamp(140px, 25vh, 200px);
padding: 8px;
perspective: 800px;
width: 100%;
overflow-x: auto;
}
#combat-controls { margin-top: 6px; }
/* Turn indicator */
.turn-indicator {
font-family: 'Cinzel', serif;
color: #c9a84c;
font-size: 0.7em;
text-transform: uppercase;
letter-spacing: 2px;
opacity: 0.6;
}
/* Player hit flash */
@keyframes playerHitFlash {
0%, 100% { background: transparent; }
50% { background: rgba(255, 0, 0, 0.1); }
}
.player-hit-flash { animation: playerHitFlash 0.3s ease-out; }
/* ── css: cards ── */
.game-card {
width: clamp(100px, 14vw, 140px);
height: clamp(145px, 20vh, 200px);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease-out;
position: relative;
flex-shrink: 0;
transform-origin: bottom center;
}
.game-card:hover {
transform: translateY(-16px) scale(1.05);
z-index: 100;
}
.game-card.playable { cursor: pointer; }
.game-card.unplayable { opacity: 0.45; cursor: not-allowed; filter: grayscale(0.6); }
.game-card.unplayable:hover { transform: none; }
.card-inner {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.card-cost {
position: absolute;
top: 3px;
left: 3px;
width: clamp(20px, 3vw, 26px);
height: clamp(20px, 3vw, 26px);
background: #1a3a5c;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cinzel', serif;
font-weight: 900;
font-size: clamp(0.6em, 1vw, 0.85em);
color: #66ccff;
z-index: 2;
border: 1px solid #3a6a9c;
}
.card-type-badge {
position: absolute;
top: 3px;
right: 3px;
font-size: clamp(0.4em, 0.7vw, 0.55em);
padding: 1px 4px;
border-radius: 3px;
text-transform: uppercase;
letter-spacing: 1px;
z-index: 2;
}
.card-type-attack { background: #4a1a00; color: #ff6633; border: 1px solid #7a3a00; }
.card-type-defense { background: #0a1a3a; color: #6699cc; border: 1px solid #1a3a5c; }
.card-type-other { background: #2a0a3a; color: #aa66ff; border: 1px solid #4a1a5c; }
.card-image-area {
position: absolute;
top: clamp(28px, 5vh, 34px);
left: 6px;
right: 6px;
height: clamp(50px, 10vh, 70px);
display: flex;
align-items: center;
justify-content: center;
}
.card-image-area .card-emoji { font-size: clamp(1.8em, 4vw, 2.5em); }
.card-name {
position: absolute;
top: clamp(80px, 16vh, 110px);
left: 5px;
right: 5px;
text-align: center;
font-family: 'Cinzel', serif;
font-size: clamp(0.5em, 0.9vw, 0.7em);
color: #e8dcc8;
line-height: 1.2;
}
.card-desc {
position: absolute;
bottom: 4px;
left: 5px;
right: 5px;
text-align: center;
font-size: clamp(0.4em, 0.7vw, 0.55em);
color: #9a8e7b;
line-height: 1.3;
}
/* Card frame styles per class */
.card-frame-bastion { border: 2px solid #6b5d45; box-shadow: 0 2px 8px rgba(107, 93, 69, 0.3); }
.card-frame-wither { border: 2px solid #6b3a1f; box-shadow: 0 2px 8px rgba(107, 58, 31, 0.3); }
.card-frame-shroud { border: 2px solid #4a3d7a; box-shadow: 0 2px 8px rgba(74, 61, 122, 0.3); }
/* Playable card glow */
.game-card.playable:hover .card-inner::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
border-radius: 6px;
pointer-events: none;
}
.card-frame-bastion.playable:hover .card-inner::after { box-shadow: inset 0 0 15px rgba(201, 168, 76, 0.3); }
.card-frame-wither.playable:hover .card-inner::after { box-shadow: inset 0 0 15px rgba(204, 102, 51, 0.3); }
.card-frame-shroud.playable:hover .card-inner::after { box-shadow: inset 0 0 15px rgba(136, 102, 204, 0.3); }
/* Shop */
#screen-shop { justify-content: flex-start; padding-top: 20px; }
#screen-shop h2 { font-family: 'Cinzel', serif; color: #c9a84c; margin-bottom: 10px; font-size: clamp(1.2em, 2.5vw, 1.8em); }
#shop-gold-display { font-size: clamp(0.9em, 1.5vw, 1.2em); color: #c9a84c; margin-bottom: 16px; }
#shop-cards {
display: flex;
gap: clamp(8px, 2vw, 16px);
justify-content: center;
flex-wrap: wrap;
max-width: min(95%, 700px);
}
.shop-card-wrapper { text-align: center; }
.shop-card-price {
font-family: 'Cinzel', serif;
color: #c9a84c;
margin-top: 4px;
font-size: clamp(0.6em, 1vw, 0.85em);
}
.shop-card-count { font-size: clamp(0.5em, 0.8vw, 0.7em); color: #6a5e4b; }
.shop-buy-btn {
background: linear-gradient(180deg, #2a3f25 0%, #1a2510 100%);
border: 1px solid #4a7d35;
color: #66cc66;
font-family: 'Cinzel', serif;
font-size: clamp(0.6em, 0.9vw, 0.8em);
padding: 3px 10px;
cursor: pointer;
margin-top: 3px;
}
.shop-buy-btn:hover { border-color: #66cc66; box-shadow: 0 0 10px rgba(102, 204, 102, 0.3); }
.shop-buy-btn.cant-afford { opacity: 0.4; cursor: not-allowed; border-color: #3a3530; color: #6a5e4b; }
/* Floor clear / Game over */
#screen-floor-clear h2, #screen-game-over h2, #screen-victory h2 {
font-family: 'Cinzel', serif;
color: #c9a84c;
font-size: clamp(1.5em, 3vw, 2.5em);
margin-bottom: 16px;
}
#screen-floor-clear p, #screen-game-over p, #screen-victory p {
font-size: clamp(0.8em, 1.5vw, 1.1em);
color: #8a7e6b;
margin-bottom: 30px;
}
/* Card play animation */
@keyframes cardPlayFlash {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.1); }
100% { opacity: 0; transform: scale(0.8) translateY(-40px); }
}
.card-playing { animation: cardPlayFlash 0.3s ease-out forwards; pointer-events: none; }
/* Damage number popup */
.damage-popup {
position: absolute;
font-family: 'Cinzel', serif;
font-weight: 900;
font-size: clamp(1em, 2vw, 1.5em);
color: #ff4422;
text-shadow: 0 0 6px rgba(255, 68, 34, 0.5);
pointer-events: none;
animation: damageFloat 0.8s ease-out forwards;
}
@keyframes damageFloat {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(-40px) scale(0.7); }
}
/* ── css: shop-buttons ── */
#screen-shop .action-btn { margin-top: 20px; }
#btn-skip-shop { opacity: 0.7; font-size: 0.85em !important; padding: 8px 20px !important; }
#btn-skip-shop:hover { opacity: 1; }
/* Scrollbar styling */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0a0a12; }
::-webkit-scrollbar-thumb { background: #3a3040; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #5a4d60; }
/* Deck info in HUD */
#hud-deck-info {
font-size: clamp(0.5em, 0.8vw, 0.7em);
color: #6a5e4b;
}
/* End turn button styling */
#btn-end-turn {
min-width: clamp(120px, 15vw, 180px);
font-size: clamp(0.7em, 1.2vw, 0.95em) !important;
}
/* Player stats glow effects */
#hp-val { color: #cc3300; }
#energy-val { color: #6699ff; }
#def-val { color: #88aacc; }
/* Floor/room info styling */
#hud-floor-info span { color: #e8dcc8; }
</style>
</head>
<body>
<div id="screen-class-select" class="screen active">
<h1 class="game-title">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">
<div id="hud-floor-info">Floor <span id="floor-num">1</span> · Room <span id="room-num">1</span></div>
<div id="hud-gold">Gold: <span id="gold-val">0</span></div>
<div id="hud-deck-info">Deck: <span id="deck-count">0</span></div></div>
<div id="combat-arena">
<div id="enemy-area">
<div id="enemy-display"></div>
<div id="enemy-hp-bar-container"><div id="enemy-hp-bar"></div></div>
<div id="enemy-status-effects"></div>
</div>
<div id="battle-log"></div>
<div id="player-area">
<div id="player-stats">
<div id="hp-display"><span class="stat-icon hp-icon">♥</span> <span id="hp-val">30</span>/<span id="max-hp-val">30</span></div>
<div id="energy-display"><span class="stat-icon energy-icon">⚡</span> <span id="energy-val">5</span>/<span id="max-energy-val">5</span></div>
<div id="defense-display"><span class="stat-icon def-icon">🛡</span> <span id="def-val">0</span></div>
</div>
<div id="hand-container"></div>
<div id="combat-controls">
<button id="btn-end-turn" class="action-btn">End Turn</button>
</div>
</div>
</div>
</div>
<div id="screen-shop" class="screen">
<h2>The Merchant's Stall</h2>
<p id="shop-gold-display">Gold: <span id="shop-gold-val">0</span></p>
<div id="shop-cards"></div>
<button id="btn-skip-shop" class="action-btn" style="margin-right:10px; font-size:0.8em;">Skip Shop</button><button id="btn-leave-shop" class="action-btn">Next Room →</button>
</div>
<div id="screen-floor-clear" class="screen">
<h2>Floor Cleared!</h2>
<p id="floor-clear-text"></p>
<button id="btn-next-floor" class="action-btn">Descend Deeper</button>
</div>
<div id="screen-game-over" class="screen">
<h2 id="game-over-title">Game Over</h2>
<p id="game-over-text"></p>
<button id="btn-restart" class="action-btn">Return to Main Menu</button>
</div>
<div id="screen-victory" class="screen">
<h2>Victory!</h2>
<p>The Void has been vanquished. Aethelgard is saved.</p>
<button id="btn-play-again" class="action-btn">Play Again</button>
</div>
<script>
'use strict';
// ── const: CARD_DATA ──
const CARD_DATA = {
// BASTION CARDS
'BA-A01': { id:'BA-A01', name:'Slam', type:'attack', cost:2, atk:3, def:0, desc:'Deals 3 damage. If you have ≥30 HP, draw 1.', emoji:'🪨' },
'BA-A02': { id:'BA-A02', name:'Earthquake', type:'attack', cost:3, atk:4, def:0, desc:'Deals 4 damage to all enemies.', aoe:true, emoji:'💥' },
'BA-A03': { id:'BA-A03', name:'Stone Fist', type:'attack', cost:1, atk:2, def:0, desc:'Deals 2 damage.', emoji:'👊' },
'BA-A04': { id:'BA-A04', name:'Boulder', type:'attack', cost:4, atk:6, def:0, desc:'Deals 6 damage.', emoji:'⛰️' },
'BA-D01': { id:'BA-D01', name:'Shield Wall', type:'defense', cost:0, atk:0, def:4, desc:'Gain 4 Defense this turn.', emoji:'🛡️' },
'BA-D02': { id:'BA-D02', name:'Iron Skin', type:'defense', cost:2, atk:0, def:5, desc:'Gain 5 permanent Defense. Discard 1.', permDef:true, discardAfter:true, emoji:'🔩' },
'BA-D03': { id:'BA-D03', name:'Stand Firm', type:'defense', cost:1, atk:0, def:2, desc:'Gain 2 Defense. Blocks 1 incoming.', blockNext:true, emoji:'⚔️' },
'BA-D04': { id:'BA-D04', name:'Taunt', type:'defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Stun 1 enemy.', stunEnemy:true, emoji:'📢' },
'BA-U01': { id:'BA-U01', name:'Search', type:'other', cost:2, atk:0, def:0, desc:'Draw 1 card. Discard if >5.', drawCard:true, discardIfOver5:true, emoji:'🔍' },
'BA-U02': { id:'BA-U02', name:'Repair', type:'other', cost:3, atk:0, def:0, desc:'Heal 6 HP.', heal:6, emoji:'❤️🩹' },
'BA-U03': { id:'BA-U03', name:'Rally', type:'other', cost:1, atk:0, def:0, desc:'Draw 1 card. Gain 1 Energy.', drawCard:true, gainEnergy:1, emoji:'📯' },
'BA-U04': { id:'BA-U04', name:'Loot', type:'other', cost:2, atk:0, def:0, desc:'Gain 5 Gold.', goldGain:5, emoji:'💰' },
// WITHER CARDS
'WI-A01': { id:'WI-A01', name:'Fireball', type:'attack', cost:2, atk:3, def:0, desc:'Deals 3 damage. Apply 1 Burn.', burn:1, emoji:'🔥' },
'WI-A02': { id:'WI-A02', name:'Inferno', type:'attack', cost:3, atk:4, def:0, desc:'Deals 4 damage. Applies 3 Burn.', burn:3, aoe:true, emoji:'🌋' },
'WI-A03': { id:'WI-A03', name:'Ember', type:'attack', cost:1, atk:2, def:0, desc:'Deals 2 damage. 1 Burn/turn.', burn:1, dot:true, emoji:'✨' },
'WI-A04': { id:'WI-A04', name:'Pyre', type:'attack', cost:4, atk:5, def:0, desc:'Deals 5 dmg. All enemies take 1 next turn.', aoe:true, nextTurnDmg:1, emoji:'🔥' },
'WI-D01': { id:'WI-D01', name:'Scorch Shield', type:'defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Block 1 Burn.', blockBurn:true, emoji:'🛡️' },
'WI-D02': { id:'WI-D02', name:'Heat Resist', type:'defense', cost:1, atk:0, def:1, desc:'Gain 1 Defense. Gain 1 Energy.', gainEnergy:1, emoji:'♨️' },
'WI-D03': { id:'WI-D03', name:'Smoke', type:'defense', cost:2, atk:0, def:4, desc:'Gain 4 Defense. Block 1 Burn.', blockBurn:true, emoji:'💨' },
'WI-D04': { id:'WI-D04', name:'Endure', type:'defense', cost:3, atk:0, def:2, desc:'Gain 2 Defense. Heal 2 HP.', heal:2, emoji:'🌿' },
'WI-U01': { id:'WI-U01', name:'Poison', type:'other', cost:2, atk:0, def:0, desc:'Apply 1 Poison to target enemy.', poison:1, emoji:'☠️' },
'WI-U02': { id:'WI-U02', name:'Burn All', type:'other', cost:3, atk:0, def:0, desc:'Deal 3 dmg + 2 Burn to all enemies.', aoe:true, aoeDmg:3, burn:2, emoji:'🔥' },
'WI-U03': { id:'WI-U03', name:'Slow', type:'other', cost:1, atk:0, def:0, desc:'Draw 1 card. Stun target enemy.', drawCard:true, stunEnemy:true, emoji:'🐌' },
'WI-U04': { id:'WI-U04', name:'Wither', type:'other', cost:2, atk:0, def:0, desc:'Heal 5 HP. Reduce enemy Atk by 1.', heal:5, reduceAtk:true, emoji:'🥀' },
// SHROUD CARDS
'SH-A01': { id:'SH-A01', name:'Backstab', type:'attack', cost:2, atk:4, def:0, desc:'Deals 4 damage. +1 if target has no Armor.', bonusNoArmor:true, emoji:'🗡️' },
'SH-A02': { id:'SH-A02', name:'Shadow Bolt', type:'attack', cost:3, atk:5, def:0, desc:'Deals 5 damage. Target cannot block.', bypassBlock:true, emoji:'⚡' },
'SH-A03': { id:'SH-A03', name:'Silence', type:'attack', cost:1, atk:3, def:0, desc:'Deals 3 damage. Silence target.', silenceEnemy:true, emoji:'🤫' },
'SH-A04': { id:'SH-A04', name:'Vengeance', type:'attack', cost:3, atk:6, def:0, desc:'Deals 6 damage. Gain 2 Energy.', gainEnergy:2, emoji:'⚔️' },
'SH-D01': { id:'SH-D01', name:'Blur', type:'defense', cost:3, atk:0, def:2, desc:'Gain 2 Defense. Ignore next attack.', dodgeNext:true, emoji:'👻' },
'SH-D02': { id:'SH-D02', name:'Step', type:'defense', cost:2, atk:0, def:3, desc:'Gain 3 Defense. Gain 1 Energy.', gainEnergy:1, emoji:'👣' },
'SH-D03': { id:'SH-D03', name:'Evasion', type:'defense', cost:1, atk:0, def:1, desc:'Gain 1 Defense. Skip enemy action.', skipEnemy:true, emoji:'💨' },
'SH-D04': { id:'SH-D04', name:'Phantom', type:'defense', cost:2, atk:0, def:4, desc:'Gain 4 Defense. Ghost form 1 turn.', ghostForm:true, emoji:'👻' },
'SH-U01': { id:'SH-U01', name:'Dash', type:'other', cost:1, atk:0, def:0, desc:'Gain 1 Energy. Skip enemy attack.', gainEnergy:1, skipEnemy:true, emoji:'💨' },
'SH-U02': { id:'SH-U02', name:'Teleport', type:'other', cost:2, atk:0, def:0, desc:'Deal 2 damage to all enemies.', aoeDmg:2, aoe:true, emoji:'🌀' },
'SH-U03': { id:'SH-U03', name:'Hide', type:'other', cost:2, atk:0, def:1, desc:'Gain 1 Defense. Discard 1. Draw 2.', drawCards:2, discardAfter:true, emoji:'🌑' },
'SH-U04': { id:'SH-U04', name:'Assassinate', type:'other', cost:3, atk:0, def:0, desc:'Deal 8 damage. Cannot be blocked.', aoeDmg:8, bypassBlock:true, emoji:'💀' }
};
// ── const: CLASS_DATA ──
const CLASS_DATA = {
bastion: {
name: 'The Bastion', theme: 'Immovable Object', icon: '🏰',
desc: 'High Defense and Block. Focuses on durability.',
startHP: 35, maxEnergy: 3, handSize: 4, gold: 0,
starterDeck: ['BA-A01','BA-A02','BA-A03','BA-D01','BA-D02','BA-U01','BA-U02','BA-U03'],
color: '#7a6b4f'
},
wither: {
name: 'The Wither', theme: 'Rot and Burn', icon: '🔥',
desc: 'Burn damage, debuffs, status effects. Low HP, high AoE.',
startHP: 28, maxEnergy: 5, handSize: 4, gold: 0,
starterDeck: ['WI-A01','WI-A02','WI-A03','WI-D01','WI-U01','WI-U02','WI-U03','WI-U04'],
color: '#8b4513'
},
shroud: {
name: 'The Shroud', theme: 'Shadow and Speed', icon: '🗡️',
desc: 'High speed, evasion, assassination. High crit, low defense.',
startHP: 30, maxEnergy: 5, handSize: 4, gold: 0,
starterDeck: ['SH-A01','SH-A02','SH-A03','SH-D01','SH-U01','SH-U02','SH-U03','SH-U04'],
color: '#4a3d7a'
}
};
// ── const: ENEMY_TEMPLATES ──
const ENEMY_TEMPLATES = {
floor1: [
{ name:'Void Goblin', hp:20, dmg:3, emoji:'👺', goldReward:5 },
{ name:'Shadow Rat', hp:18, dmg:4, emoji:'🐀', goldReward:5 },
{ name:'Dark Imp', hp:22, dmg:3, emoji:'😈', goldReward:5 }
],
floor2: [
{ name:'Vampire Knight', hp:30, dmg:5, emoji:'🧛', goldReward:5 },
{ name:'Ogre Brute', hp:35, dmg:6, emoji:'👹', goldReward:5 },
{ name:'Dark Cultist', hp:28, dmg:7, emoji:'🧙', goldReward:5 }
],
floor3: [
{ name:'Void Warrior', hp:40, dmg:8, emoji:'⚔️', goldReward:5 },
{ name:'Cultist Elite', hp:38, dmg:9, emoji:'🗡️', goldReward:5 },
{ name:'Shadow Wraith', hp:42, dmg:7, emoji:'👻', goldReward:5 }
]
};
// ── const: BOSS_TEMPLATES ──
const BOSS_TEMPLATES = [
{ floor:1, name:'The Gatekeeper', hp:80, dmg:15, emoji:'🗿', goldReward:15, mechanic:'block5' },
{ floor:2, name:'The Corrupted', hp:80, dmg:15, emoji:'💀', goldReward:15, mechanic:'burst' },
{ floor:3, name:'The Void Lord', hp:40, maxHp:40, dmg:15, emoji:'🌀', goldReward:15, mechanic:'regen' }
];
// ── function: createCardSVG ──
function createCardSVG(cardId, classType) {
const card = CARD_DATA[cardId];
if (!card) return '';
const typeColors = {
attack: { border:'#7a3a00', bg:'linear-gradient(180deg,#2a1508,#1a0d04)', glow:'rgba(122,58,0,0.4)' },
defense: { border:'#1a3a5c', bg:'linear-gradient(180deg,#0a1528,#060d1a)', glow:'rgba(26,58,92,0.4)' },
other: { border:'#4a1a5c', bg:'linear-gradient(180deg,#1a0a28,#0d061a)', glow:'rgba(74,26,92,0.4)' }
};
const classBorders = {
bastion: '#6b5d45', wither: '#6b3a1f', shroud: '#4a3d7a'
};
const tc = typeColors[card.type];
const cb = classBorders[classType] || '#555';
return `<svg viewBox="0 0 140 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="cbg_${cardId}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:${classType==='bastion'?'#3a2f25':classType==='wither'?'#3a2520':'#252035'}"/>
<stop offset="100%" style="stop-color:#0d0b14"/>
</linearGradient>
</defs>
<!-- Outer frame -->
<rect x="1" y="1" width="138" height="198" rx="7" fill="none" stroke="${cb}" stroke-width="2.5"/>
<!-- Inner bg -->
<rect x="4" y="4" width="132" height="192" rx="5" fill="url(#cbg_${cardId})"/>
<!-- Type accent bar -->
<rect x="4" y="4" width="132" height="6" rx="3" fill="${tc.border}" opacity="0.7"/>
<!-- Decorative corner elements -->
${classType === 'bastion' ? `
<path d="M8,15 L15,8 M132,15 L125,8 M8,185 L15,192 M132,185 L125,192" stroke="${cb}" stroke-width="1.5" fill="none"/>
<rect x="60" y="7" width="20" height="4" rx="2" fill="${cb}" opacity="0.5"/>
` : classType === 'wither' ? `
<circle cx="8" cy="12" r="3" fill="none" stroke="#6b3a1f" stroke-width="1"/>
<circle cx="132" cy="12" r="3" fill="none" stroke="#6b3a1f" stroke-width="1"/>
<path d="M70,8 Q90,4 110,8" stroke="#6b3a1f" stroke-width="1" fill="none"/>
` : `
<polygon points="8,12 12,8 16,12" fill="none" stroke="#4a3d7a" stroke-width="1"/>
<polygon points="124,12 128,8 132,12" fill="none" stroke="#4a3d7a" stroke-width="1"/>
<line x1="65" y1="9" x2="75" y2="9" stroke="#4a3d7a" stroke-width="1"/>
`}
</svg>`;
}
// ── function: createCardBackSVG ──
function createCardBackSVG(classType) {
const colors = {
bastion: { primary:'#6b5d45', secondary:'#4a3f2e', accent:'#c9a84c' },
wither: { primary:'#6b3a1f', secondary:'#4a2510', accent:'#cc6633' },
shroud: { primary:'#4a3d7a', secondary:'#2e2550', accent:'#8866cc' }
};
const c = colors[classType] || colors.bastion;
return `<svg viewBox="0 0 140 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="cbk_${classType}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#1a1520"/>
<stop offset="100%" style="stop-color:#0d0b14"/>
</linearGradient>
</defs>
<rect x="1" y="1" width="138" height="198" rx="7" fill="url(#cbk_${classType})" stroke="${c.primary}" stroke-width="2.5"/>
<rect x="6" y="6" width="128" height="188" rx="4" fill="none" stroke="${c.secondary}" stroke-width="1"/>
${classType === 'bastion' ? `
<!-- Shield pattern -->
<path d="M70,35 L95,50 L95,85 Q95,110 70,120 Q45,110 45,85 L45,50 Z" fill="none" stroke="${c.primary}" stroke-width="2"/>
<path d="M70,45 L85,55 L85,80 Q85,95 70,103 Q55,95 55,80 L55,55 Z" fill="${c.primary}" opacity="0.2"/>
<line x1="70" y1="45" x2="70" y2="103" stroke="${c.accent}" stroke-width="1" opacity="0.5"/>
` : classType === 'wither' ? `
<!-- Flame pattern -->
<path d="M70,30 Q85,55 90,75 Q95,95 80,100 Q75,102 70,95 Q65,102 60,100 Q45,95 50,75 Q55,55 70,30Z" fill="none" stroke="${c.primary}" stroke-width="2"/>
<path d="M70,50 Q80,65 82,78 Q84,88 76,91 Q72,92 70,87 Q68,92 64,91 Q56,88 58,78 Q60,65 70,50Z" fill="${c.primary}" opacity="0.3"/>
<circle cx="70" cy="75" r="5" fill="${c.accent}" opacity="0.4"/>
` : `
<!-- Eye/shadow pattern -->
<ellipse cx="70" cy="70" rx="28" ry="16" fill="none" stroke="${c.primary}" stroke-width="2"/>
<circle cx="70" cy="70" r="10" fill="none" stroke="${c.accent}" stroke-width="1.5"/>
<circle cx="70" cy="70" r="4" fill="${c.accent}" opacity="0.5"/>
<path d="M42,70 Q70,55 98,70" fill="none" stroke="${c.primary}" stroke-width="1" opacity="0.5"/>
<path d="M42,70 Q70,85 98,70" fill="none" stroke="${c.primary}" stroke-width="1" opacity="0.5"/>
`}
<!-- Border decorations -->
${[35,65,95,125].map(y => `<line x1="12" y1="${y}" x2="18" y2="${y}" stroke="${c.secondary}" stroke-width="0.5"/>`).join('')}
${[35,65,95,125].map(y => `<line x1="122" y1="${y}" x2="128" y2="${y}" stroke="${c.secondary}" stroke-width="0.5"/>`).join('')}
</svg>`;
}
// ── function: getCardFrameClass ──
function getCardFrameClass(classType) {
return `card-frame-${classType}`;
}
// ── let: G ──
let G = {};
// ── function: initGame ──
// Global game state
function initGame(classType) {
const cls = CLASS_DATA[classType];
G = {
classType: classType,
className: cls.name,
hp: cls.startHP,
maxHp: cls.startHP,
energy: cls.maxEnergy,
maxEnergy: cls.maxEnergy,
handSize: cls.handSize,
gold: 0,
defense: 0,
permDefense: 0,
floor: 1,
room: 1,
roomsPerFloor: 3,
// Deck management
deckPool: [...cls.starterDeck], // All cards in deck (can have duplicates)
drawPile: [],
hand: [],
discardPile: [],
inventory: {}, // cardId -> count
// Combat state
enemy: null,
isBoss: false,
playerTurn: true,
turnActive: true,
// Effects
dodgeNextAttack: false,
ghostForm: false,
skipEnemyAction: false,
blockBurnDamage: 0,
nextTurnDmgToAll: 0,
// Log
log: []
};
// Count inventory
G.deckPool.forEach(id => {
G.inventory[id] = (G.inventory[id] || 0) + 1;
});
// Shuffle draw pile
shuffleArray(G.drawPile = [...G.deckPool]);
}
// ── function: shuffleArray ──
function shuffleArray(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 ──
function drawCards(count) {
for (let i = 0; i < count; i++) {
if (G.discardPile.length > 0 && G.drawPile.length === 0) {
// Reshuffle discard into draw pile
shuffleArray(G.drawPile = [...G.discardPile]);
G.discardPile = [];
}
if (G.drawPile.length > 0 && G.hand.length < G.handSize + 5) {
G.hand.push(G.drawPile.pop());
}
}
}
// ── function: discardCard ──
function discardCard(index) {
if (index >= 0 && index < G.hand.length) {
const cardId = G.hand.splice(index, 1)[0];
G.discardPile.push(cardId);
}
}
// ── function: addLog ──
function addLog(msg, cls='') {
G.log.push({ msg, cls });
if (G.log.length > 50) G.log.shift();
}
// ── function: playCard ──
function playCard(handIndex) {
if (!G.playerTurn || !G.turnActive) return false;
const cardId = G.hand[handIndex];
const card = CARD_DATA[cardId];
if (!card) return false;
// Check energy cost
if (card.cost > G.energy) {
addLog('Not enough Energy!', 'log-damage');
return false;
}
// Spend energy
G.energy -= card.cost;
// Remove from hand
G.hand.splice(handIndex, 1);
G.discardPile.push(cardId);
addLog(`Played ${card.name}`, '');
// Resolve card effects based on type
if (card.type === 'attack') {
resolveAttackCard(card);
} else if (card.type === 'defense') {
resolveDefenseCard(card);
} else if (card.type === 'other') {
resolveOtherCard(card);
}
// Check if enemy died
if (G.enemy && G.enemy.hp <= 0) {
onEnemyDefeated();
}
return true;
}
// ── function: resolveAttackCard ──
function resolveAttackCard(card) {
const enemy = G.enemy;
let dmg = card.atk || 0;
// Special: Backstab bonus if no armor
if (card.bonusNoArmor && (!enemy.defense || enemy.defense === 0)) {
dmg += 1;
}
if (card.aoe) {
// AoE damage to all enemies (single enemy in this prototype)
let actualDmg = Math.max(0, dmg - (enemy.defense || 0));
if (!card.bypassBlock) {
enemy.defense = Math.max(0, (enemy.defense || 0) - dmg);
}
applyDamageToEnemy(actualDmg);
addLog(`${card.name} deals ${actualDmg} damage!`, 'log-damage');
// Apply burn if any
if (card.burn) {
enemy.burn = (enemy.burn || 0) + card.burn;
addLog(`Applied ${card.burn} Burn to ${enemy.name}`, '');
}
// Next turn damage
if (card.nextTurnDmg) {
G.nextTurnDmgToAll += card.nextTurnDmg;
}
} else {
let actualDmg = Math.max(0, dmg - (enemy.defense || 0));
if (!card.bypassBlock) {
enemy.defense = Math.max(0, (enemy.defense || 0) - dmg);
}
applyDamageToEnemy(actualDmg);
addLog(`${card.name} deals ${actualDmg} damage!`, 'log-damage');
// Apply burn if any
if (card.burn) {
enemy.burn = (enemy.burn || 0) + card.burn;
addLog(`Applied ${card.burn} Burn to ${enemy.name}`, '');
}
// Silence enemy
if (card.silenceEnemy) {
enemy.silenced = true;
addLog(`${enemy.name} is Silenced!`, '');
}
}
// Gain energy from card effect
if (card.gainEnergy) {
G.energy += card.gainEnergy;
addLog(`Gained ${card.gainEnergy} Energy`, 'log-defense');
}
// Slam: draw if HP >= 30
if (card.id === 'BA-A01' && G.hp >= 30) {
drawCards(1);
addLog('Slam bonus: Drew 1 card', '');
}
}
// ── function: resolveDefenseCard ──
function resolveDefenseCard(card) {
// Gain defense
if (card.def > 0) {
if (card.permDef) {
G.permDefense += card.def;
addLog(`Gained ${card.def} permanent Defense!`, 'log-defense');
} else {
G.defense += card.def;
addLog(`Gained ${card.def} Defense this turn.`, 'log-defense');
}
}
// Block burn damage
if (card.blockBurn) {
G.blockBurnDamage++;
addLog('Will block next Burn damage', '');
}
// Stun enemy
if (card.stunEnemy && G.enemy) {
G.enemy.stunned = true;
addLog(`${G.enemy.name} is Stunned!`, '');
}
// Dodge next attack
if (card.dodgeNext) {
G.dodgeNextAttack = true;
addLog('Will dodge next attack!', 'log-defense');
}
// Ghost form
if (card.ghostForm) {
G.ghostForm = true;
addLog('Ghost Form active this turn', '');
}
// Skip enemy action
if (card.skipEnemy) {
G.skipEnemyAction = true;
addLog('Enemy will skip their action!', 'log-defense');
}
// Heal
if (card.heal) {
G.hp = Math.min(G.maxHp, G.hp + card.heal);
addLog(`Healed ${card.heal} HP!`, 'log-heal');
}
// Gain energy
if (card.gainEnergy) {
G.energy += card.gainEnergy;
addLog(`Gained ${card.gainEnergy} Energy`, 'log-defense');
}
// Discard after effect
if (card.discardAfter && G.hand.length > 0) {
discardCard(G.hand.length - 1);
addLog('Discarded 1 card', '');
}
}
// ── function: resolveOtherCard ──
function resolveOtherCard(card) {
// Draw cards
if (card.drawCards || card.drawCard) {
const count = card.drawCards || 1;
drawCards(count);
addLog(`Drew ${count} card(s)`, '');
// Discard if over 5
if (card.discardIfOver5 && G.hand.length > 5) {
while (G.hand.length > 5) discardCard(G.hand.length - 1);
addLog('Discarded excess cards', '');
}
}
// Heal
if (card.heal) {
G.hp = Math.min(G.maxHp, G.hp + card.heal);
addLog(`Healed ${card.heal} HP!`, 'log-heal');
}
// Gain energy
if (card.gainEnergy) {
G.energy += card.gainEnergy;
addLog(`Gained ${card.gainEnergy} Energy`, 'log-defense');
}
// Gold gain
if (card.goldGain) {
G.gold += card.goldGain;
addLog(`Gained ${card.goldGain} Gold!`, 'log-gold');
}
// AoE damage
if (card.aoe && card.aoeDmg) {
let dmg = card.aoeDmg;
let actualDmg = Math.max(0, dmg - (G.enemy.defense || 0));
if (!card.bypassBlock) {
G.enemy.defense = Math.max(0, (G.enemy.defense || 0) - dmg);
} else {
actualDmg = dmg;
}
applyDamageToEnemy(actualDmg);
addLog(`${card.name} deals ${actualDmg} damage!`, 'log-damage');
if (card.burn) {
G.enemy.burn = (G.enemy.burn || 0) + card.burn;
addLog(`Applied ${card.burn} Burn`, '');
}
}
// Single target damage (Assassinate)
if (!card.aoe && card.aoeDmg) {
let dmg = card.aoeDmg;
applyDamageToEnemy(dmg);
addLog(`${card.name} deals ${dmg} unblockable damage!`, 'log-damage');
}
// Poison enemy
if (card.poison && G.enemy) {
G.enemy.poison = (G.enemy.poison || 0) + card.poison;
addLog(`Applied ${card.poison} Poison to ${G.enemy.name}`, '');
}
// Stun enemy
if (card.stunEnemy && G.enemy) {
G.enemy.stunned = true;
addLog(`${G.enemy.name} is Stunned!`, '');
}
// Skip enemy action
if (card.skipEnemy) {
G.skipEnemyAction = true;
addLog('Enemy will skip their action!', 'log-defense');
}
// Reduce enemy attack
if (card.reduceAtk && G.enemy) {
G.enemy.dmg = Math.max(1, G.enemy.dmg - 1);
addLog(`${G.enemy.name}'s Attack reduced by 1`, '');
}
// Discard after effect
if (card.discardAfter && G.hand.length > 0) {
discardCard(G.hand.length - 1);
addLog('Discarded 1 card', '');
}
}
// ── function: applyDamageToEnemy ──
function applyDamageToEnemy(dmg) {
if (!G.enemy) return;
G.enemy.hp -= dmg;
animateEnemyHit();
}
// ── function: endPlayerTurn ──
function endPlayerTurn() {
if (!G.playerTurn || !G.turnActive) return;
G.playerTurn = false;
G.turnActive = false;
// Apply next-turn damage to all enemies
if (G.nextTurnDmgToAll > 0 && G.enemy) {
applyDamageToEnemy(G.nextTurnDmgToAll);
addLog(`Next-turn damage: ${G.nextTurnDmgToAll} to ${G.enemy.name}`, 'log-damage');
G.nextTurnDmgToAll = 0;
}
// Apply burn/poison DoT to enemy at end of player turn (before enemy acts)
applyEnemyDoT();
if (G.enemy && G.enemy.hp <= 0) {
onEnemyDefeated();
return;
}
// Enemy action after short delay
setTimeout(() => {
enemyAction();
if (G.enemy && G.enemy.hp <= 0) {
onEnemyDefeated();
return;
}
// Start new player turn
startPlayerTurn();
}, 600);
}
// ── function: applyEnemyDoT ──
function applyEnemyDoT() {
if (!G.enemy) return;
// Burn damage
if (G.enemy.burn && G.enemy.burn > 0) {
let burnDmg = G.enemy.burn;
// Check block burn
if (G.blockBurnDamage > 0) {
addLog('Blocked Burn damage!', 'log-defense');
G.blockBurnDamage--;
return;
}
applyDamageToEnemy(burnDmg);
addLog(`Burn deals ${burnDmg} to ${G.enemy.name}`, 'log-damage');
G.enemy.burn = Math.max(0, G.enemy.burn - 1); // Burn decrements each turn
}
// Poison damage (doesn't decrement)
if (G.enemy.poison && G.enemy.poison > 0) {
applyDamageToEnemy(G.enemy.poison);
addLog(`Poison deals ${G.enemy.poison} to ${G.enemy.name}`, 'log-damage');
}
}
// ── function: enemyAction ──
function enemyAction() {
const enemy = G.enemy;
if (!enemy || enemy.hp <= 0) return;
// Check stun
if (enemy.stunned) {
addLog(`${enemy.name} is Stunned and cannot act!`, '');
enemy.stunned = false;
return;
}
// Check skip enemy action
if (G.skipEnemyAction) {
addLog(`${enemy.name} skips their action!`, 'log-defense');
G.skipEnemyAction = false;
return;
}
// Boss-specific mechanics
let dmg = enemy.dmg;
let extraMsg = '';
if (G.isBoss) {
const bossIdx = BOSS_TEMPLATES.findIndex(b => b.floor === G.floor);
if (bossIdx >= 0) {
const boss = BOSS_TEMPLATES[bossIdx];
// Boss 1: Gatekeeper - blocks 5 damage per turn until HP < 20
if (boss.mechanic === 'block5' && enemy.hp > 20) {
enemy.defense = (enemy.defense || 0) + 5;
extraMsg += ' Blocks 5!';
}
// Boss 2: The Corrupted - deals 20 damage if player has < 3 energy
if (boss.mechanic === 'burst' && G.energy < 3) {
dmg = 20;
extraMsg += ' BURST!';
}
// Boss 3: The Void Lord - regen 10 HP per turn, cannot block
if (boss.mechanic === 'regen') {
enemy.hp = Math.min(enemy.maxHp || 50, enemy.hp + 10);
addLog(`${enemy.name} regenerates 10 HP!`, '');
enemy.defense = 0; // Cannot block
}
}
}
// Check dodge/ghost form
if (G.dodgeNextAttack) {
addLog(`Dodged ${enemy.name}'s attack!`, 'log-defense');
G.dodgeNextAttack = false;
return;
}
if (G.ghostForm) {
addLog(`Ghost Form absorbed the hit from ${enemy.name}!`, 'log-defense');
G.ghostForm = false;
return;
}
// Calculate damage after player defense
let totalDefense = G.defense + G.permDefense;
let actualDmg = Math.max(0, dmg - totalDefense);
if (actualDmg > 0) {
G.hp -= actualDmg;
addLog(`${enemy.name} deals ${actualDmg} damage!${extraMsg}`, 'log-damage');
animatePlayerHit();
} else {
addLog(`${enemy.name}'s attack was fully blocked!`, 'log-defense');
}
// Reset turn-based defense (permDefense stays)
G.defense = 0;
}
// ── function: startPlayerTurn ──
function startPlayerTurn() {
// Recover energy
G.energy = Math.min(G.maxEnergy, G.energy + 1);
// Draw cards up to hand size
drawCards(1);
G.playerTurn = true;
G.turnActive = true;
renderCombat();
}
// ── function: onEnemyDefeated ──
function onEnemyDefeated() {
const enemy = G.enemy;
if (!enemy) return;
addLog(`${enemy.name} defeated!`, 'log-gold');
G.gold += enemy.goldReward || 5;
addLog(`Gained ${enemy.goldReward || 5} Gold!`, 'log-gold');
// Check for boss victory
if (G.isBoss) {
setTimeout(() => {
if (G.floor >= 3) {
showScreen('screen-victory');
} else {
G.floor++;
G.room = 1;
document.getElementById('floor-clear-text').textContent =
`Floor ${G.floor - 1} cleared! Prepare to descend deeper...`;
showScreen('screen-floor-clear');
}
}, 800);
return;
}
// Next room or boss
G.room++;
if (G.room > G.roomsPerFloor) {
// Boss fight next - skip shop before boss
setTimeout(() => startBossFight(), 500);
} else {
// Show shop between rooms
setTimeout(() => openShop(), 500);
}
}
// ── function: startNextRoom ──
function startNextRoom() {
const templates = ENEMY_TEMPLATES[`floor${G.floor}`] || ENEMY_TEMPLATES.floor1;
const template = templates[Math.floor(Math.random() * templates.length)];
G.enemy = {
...template,
maxHp: template.hp, // Set maxHp for HP bar calculation
defense:0, burn:0, poison:0, stunned:false, silenced:false
};
G.isBoss = false;
G.playerTurn = true;
G.turnActive = true;
G.defense = 0;
G.dodgeNextAttack = false;
G.ghostForm = false;
G.skipEnemyAction = false;
G.blockBurnDamage = 0;
// Reshuffle discard into draw pile for new room
if (G.drawPile.length === 0) {
shuffleArray(G.drawPile = [...G.discardPile]);
G.discardPile = [];
}
// Draw initial hand up to handSize
while (G.hand.length < G.handSize && G.drawPile.length > 0) {
G.hand.push(G.drawPile.pop());
}
addLog(`--- Floor ${G.floor}, Room ${G.room} ---`, '');
addLog(`${template.name} appears!`, 'log-damage');
renderCombat();
}
// ── function: startBossFight ──
function startBossFight() {
const bossTemplate = BOSS_TEMPLATES.find(b => b.floor === G.floor);
if (!bossTemplate) return;
G.enemy = {
...bossTemplate,
maxHp: bossTemplate.hp,
defense: 0, burn:0, poison:0, stunned:false, silenced:false
};
G.isBoss = true;
G.playerTurn = true;
G.turnActive = true;
G.defense = 0;
G.dodgeNextAttack = false;
G.ghostForm = false;
G.skipEnemyAction = false;
G.blockBurnDamage = 0;
// Reshuffle discard into draw pile for boss fight
if (G.drawPile.length === 0) {
shuffleArray(G.drawPile = [...G.discardPile]);
G.discardPile = [];
}
// Draw initial hand up to handSize
while (G.hand.length < G.handSize && G.drawPile.length > 0) {
G.hand.push(G.drawPile.pop());
}
addLog(`⚠️ BOSS: ${bossTemplate.name} appears!`, 'log-damage');
renderCombat();
}
// ── function: checkGameOver ──
function checkGameOver() {
if (G.hp <= 0) {
G.hp = 0;
document.getElementById('game-over-title').textContent = 'Game Over';
document.getElementById('game-over-text').textContent =
`You fell on Floor ${G.floor}, Room ${G.room}. The Void consumes all...`;
showScreen('screen-game-over');
return true;
}
return false;
}
// ── function: showScreen ──
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const screen = document.getElementById(screenId);
if (screen) screen.classList.add('active');
}
// ── function: renderClassSelect ──
function renderClassSelect() {
const container = document.getElementById('class-cards');
container.innerHTML = '';
Object.entries(CLASS_DATA).forEach(([key, cls]) => {
const div = document.createElement('div');
div.className = `class-card ${key}`;
// Card back preview SVG
const cardBackSVG = createCardBackSVG(key);
div.innerHTML = `
<div class="card-back-preview">${cardBackSVG}</div>
<div class="class-icon">${cls.icon}</div>
<div class="class-name">${cls.name}</div>
<div class="class-theme">${cls.theme}</div>
<div class="class-desc">${cls.desc}</div>
<div class="class-stats">
<div class="class-stat"><div class="class-stat-val">${cls.startHP}</div><div class="class-stat-label">HP</div></div>
<div class="class-stat"><div class="class-stat-val">${cls.maxEnergy}</div><div class="class-stat-label">Energy</div></div>
<div class="class-stat"><div class="class-stat-val">${cls.starterDeck.length}</div><div class="class-stat-label">Cards</div></div>
</div>
`;
div.addEventListener('click', () => {
initGame(key);
showScreen('screen-combat');
startNextRoom();
});
container.appendChild(div);
});
}
// ── function: renderCombat ──
function renderCombat() {
// Update HUD
document.getElementById('floor-num').textContent = G.floor;
document.getElementById('room-num').textContent = G.isBoss ? 'BOSS' : G.room;
document.getElementById('gold-val').textContent = G.gold;
// Deck info
const deckCountEl = document.getElementById('deck-count');
if (deckCountEl) {
deckCountEl.textContent = `${G.drawPile.length} draw / ${G.discardPile.length} discard`;
}
// Player stats
document.getElementById('hp-val').textContent = G.hp;
document.getElementById('max-hp-val').textContent = G.maxHp;
document.getElementById('energy-val').textContent = G.energy;
document.getElementById('max-energy-val').textContent = G.maxEnergy;
document.getElementById('def-val').textContent = G.defense + G.permDefense;
// Enemy display
if (G.enemy) {
const enemyDisplay = document.getElementById('enemy-display');
enemyDisplay.innerHTML = `
<div id="enemy-name">${G.isBoss ? '⚠️ BOSS: ' : ''}${G.enemy.name}</div>
<div class="enemy-sprite" id="enemy-sprite">${G.enemy.emoji}</div>
`;
// HP bar with text overlay
const hpPercent = Math.max(0, (G.enemy.hp / G.enemy.maxHp) * 100);
document.getElementById('enemy-hp-bar').style.width = `${hpPercent}%`;
// Update or create HP text
let hpTextEl = document.getElementById('enemy-hp-text');
if (!hpTextEl) {
hpTextEl = document.createElement('span');
hpTextEl.id = 'enemy-hp-text';
document.getElementById('enemy-hp-bar-container').appendChild(hpTextEl);
}
hpTextEl.textContent = `${Math.max(0, G.enemy.hp)} / ${G.enemy.maxHp}`;
// Status effects
const statusContainer = document.getElementById('enemy-status-effects');
let statuses = '';
if (G.enemy.burn > 0) statuses += `<span class="status-badge status-burn">🔥 Burn ${G.enemy.burn}</span>`;
if (G.enemy.poison > 0) statuses += `<span class="status-badge status-poison">☠ Poison ${G.enemy.poison}</span>`;
if (G.enemy.stunned) statuses += `<span class="status-badge status-stun">⚡ Stunned</span>`;
if (G.enemy.silenced) statuses += `<span class="status-badge" style="background:#2a0a3a;color:#aa66ff;border:1px solid #4a1a5c;">🔇 Silenced</span>`;
statusContainer.innerHTML = statuses;
}
// Render hand
renderHand();
// Battle log
renderBattleLog();
}
// ── function: renderHand ──
function renderHand() {
const container = document.getElementById('hand-container');
container.innerHTML = '';
G.hand.forEach((cardId, index) => {
const card = CARD_DATA[cardId];
if (!card) return;
const playable = G.playerTurn && G.turnActive && card.cost <= G.energy;
const frameClass = getCardFrameClass(G.classType);
const cardEl = document.createElement('div');
cardEl.className = `game-card ${frameClass} ${playable ? 'playable' : 'unplayable'}`;
cardEl.innerHTML = `
<div class="card-inner">
${createCardSVG(cardId, G.classType)}
<div class="card-cost">${card.cost}</div>
<div class="card-type-badge card-type-${card.type}">${card.type}</div>
<div class="card-image-area"><span class="card-emoji">${card.emoji}</span></div>
<div class="card-name">${card.name}</div>
<div class="card-desc">${card.desc}</div>
</div>
`;
if (playable) {
cardEl.addEventListener('click', () => {
playCard(index);
renderCombat();
// Check game over after playing card
checkGameOver();
});
}
container.appendChild(cardEl);
});
}
// ── function: renderBattleLog ──
function renderBattleLog() {
const logContainer = document.getElementById('battle-log');
logContainer.innerHTML = G.log.slice(-8).map(entry =>
`<div class="log-entry ${entry.cls || ''}">${entry.msg}</div>`
).join('');
logContainer.scrollTop = logContainer.scrollHeight;
}
// ── function: animateEnemyHit ──
function animateEnemyHit() {
const sprite = document.getElementById('enemy-sprite');
if (sprite) {
sprite.classList.remove('hit');
void sprite.offsetWidth; // Force reflow
sprite.classList.add('hit');
}
}
// ── function: animatePlayerHit ──
function animatePlayerHit() {
const playerArea = document.getElementById('player-area');
playerArea.classList.remove('player-hit-flash');
void playerArea.offsetWidth;
playerArea.classList.add('player-hit-flash');
}
// ── function: openShop ──
function openShop() {
showScreen('screen-shop');
renderShop();
}
// ── function: renderShop ──
function renderShop() {
document.getElementById('shop-gold-val').textContent = G.gold;
const container = document.getElementById('shop-cards');
container.innerHTML = '';
// Generate random shop cards (3-5 cards)
const allCardIds = Object.keys(CARD_DATA);
const shopCount = 3 + Math.floor(Math.random() * 2);
const shopCards = [];
while (shopCards.length < shopCount && shopCards.length < allCardIds.length) {
const randomId = allCardIds[Math.floor(Math.random() * allCardIds.length)];
if (!shopCards.includes(randomId)) {
shopCards.push(randomId);
}
}
shopCards.forEach(cardId => {
const card = CARD_DATA[cardId];
const ownedCount = G.inventory[cardId] || 0;
// Calculate cost with scaling
let cost;
if (ownedCount >= 3) {
cost = Math.min(50, 10 + (ownedCount - 2));
} else {
cost = card.cost;
}
const canAfford = G.gold >= cost && ownedCount < 5;
const frameClass = getCardFrameClass(G.classType);
const wrapper = document.createElement('div');
wrapper.className = 'shop-card-wrapper';
wrapper.innerHTML = `
<div class="game-card ${frameClass}" style="cursor:default;">
<div class="card-inner">
${createCardSVG(cardId, G.classType)}
<div class="card-cost">${card.cost}</div>
<div class="card-type-badge card-type-${card.type}">${card.type}</div>
<div class="card-image-area"><span class="card-emoji">${card.emoji}</span></div>
<div class="card-name">${card.name}</div>
<div class="card-desc">${card.desc}</div>
</div>
</div>
<div class="shop-card-price">Cost: ${cost} Gold</div>
<div class="shop-card-count">Owned: ${ownedCount}/5</div>
<button class="shop-buy-btn ${canAfford ? '' : 'cant-afford'}" data-card="${cardId}" data-cost="${cost}">${ownedCount >= 5 ? 'Max' : 'Buy'}</button>
`;
const buyBtn = wrapper.querySelector('.shop-buy-btn');
if (canAfford) {
buyBtn.addEventListener('click', () => {
G.gold -= cost;
G.inventory[cardId] = ownedCount + 1;
G.deckPool.push(cardId);
addLog(`Bought ${card.name} for ${cost} Gold!`, 'log-gold');
renderShop();
});
}
container.appendChild(wrapper);
});
G._shopCards = shopCards;
}
// ── function: leaveShop ──
function leaveShop() {
showScreen('screen-combat');
startNextRoom();
}
// ── main ──
// Wire up UI buttons
document.getElementById('btn-end-turn').addEventListener('click', () => {
endPlayerTurn();
});
document.getElementById('btn-leave-shop').addEventListener('click', () => {
leaveShop();
});
document.getElementById('btn-skip-shop').addEventListener('click', () => {
leaveShop();
});
document.getElementById('btn-next-floor').addEventListener('click', () => {
showScreen('screen-combat');
startNextRoom();
});
document.getElementById('btn-restart').addEventListener('click', () => {
showScreen('screen-class-select');
renderClassSelect();
});
document.getElementById('btn-play-again').addEventListener('click', () => {
showScreen('screen-class-select');
renderClassSelect();
});
// Initialize class selection screen
renderClassSelect();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"8e0b1572-5c21-4da5-8ebe-c51e97480f5f","tokensIn":1982598,"tokensOut":39164,"tokensTotal":2021762,"turns":56,"toolCalls":55,"failedToolCalls":2,"timestamp":"2026-07-16T05:09:00.502Z"} -->