Deckbuilder
thinkingcap qwen3.6 27b · 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 54,695 bytes · SHA-256 4458e11cef96
<!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; }
html, body { width: 100%; height: 100%; overflow: hidden; }
body {
background: #0a0806;
color: #d4c5a9;
font-family: 'Georgia', 'Times New Roman', serif;
}
/* Background atmosphere */
.screen::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 50% 30%, rgba(40,25,15,0.6) 0%, transparent 70%),
radial-gradient(ellipse at 80% 80%, rgba(60,20,20,0.15) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.screen { display: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; }
.screen.active { display: flex; flex-direction: column; align-items: center; }
.screen > * { position: relative; z-index: 1; }
/* ===== TITLE SCREEN ===== */
.game-title {
font-size: clamp(2em, 5vw, 3.5em);
color: #c9a84c;
text-shadow: 0 0 30px rgba(201,168,76,0.4), 0 0 60px rgba(201,168,76,0.15), 0 2px 4px rgba(0,0,0,0.9);
margin-top: clamp(30px, 8vh, 80px);
letter-spacing: 4px;
text-align: center;
}
.subtitle {
font-size: clamp(0.9em, 2vw, 1.15em);
color: #7a6d5b;
margin: 12px 0 35px;
font-style: italic;
text-align: center;
}
/* ===== BUTTONS ===== */
.combat-btn {
background: linear-gradient(180deg, #3a2f25 0%, #1e1812 100%);
border: 2px solid #6b5d47;
color: #c9a84c;
padding: 12px 36px;
font-size: 1.05em;
font-family: 'Georgia', serif;
cursor: pointer;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 2px;
transition: all 0.2s;
}
.combat-btn:hover:not(:disabled) {
background: linear-gradient(180deg, #4a3f35 0%, #2e2822 100%);
border-color: #c9a84c;
box-shadow: 0 0 15px rgba(201,168,76,0.3), inset 0 0 10px rgba(201,168,76,0.05);
}
.combat-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* ===== CLASS SELECT ===== */
#screen-class-select { justify-content: flex-start; }
#class-cards { display: flex; gap: 28px; flex-wrap: wrap; justify-content: center; padding: 0 20px; max-width: 1000px; }
.class-card {
width: 260px;
background: linear-gradient(180deg, #1a1510 0%, #0d0b08 100%);
border: 2px solid #3a3025;
border-radius: 8px;
padding: 24px 20px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.class-card:hover {
transform: translateY(-6px);
box-shadow: 0 12px 35px rgba(0,0,0,0.7), 0 0 25px rgba(201,168,76,0.15);
border-color: #c9a84c;
}
.class-card h3 { color: #c9a84c; font-size: 1.4em; margin-bottom: 6px; }
.class-theme { color: #8a7d6b; font-style: italic; font-size: 0.9em; margin-bottom: 12px; }
.class-stats { color: #a89880; font-size: 0.85em; line-height: 1.7; }
.class-icon { width: 80px; height: 80px; margin: 0 auto 12px; display: block; }
/* ===== COMBAT SCREEN ===== */
#screen-combat { justify-content: flex-start; padding-top: 6px; }
#hud-top {
width: 100%; display: flex; justify-content: space-between; align-items: center;
padding: 8px 24px;
background: linear-gradient(180deg, rgba(26,21,16,0.95), transparent);
font-size: 0.95em; color: #c9a84c; letter-spacing: 1px;
}
#combat-area {
flex: 1; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;
position: relative; min-height: 200px; overflow: hidden;
}
/* Enemy zone */
#enemy-zone {
display: flex; gap: 24px; align-items: flex-start; justify-content: center; flex-wrap: wrap; padding: 10px 20px;
width: 100%; max-width: 800px;
}
.enemy-sprite {
width: 130px; text-align: center; position: relative; cursor: pointer; transition: transform 0.15s, filter 0.15s;
}
.enemy-sprite:hover { transform: scale(1.06); }
.enemy-sprite.selected { filter: drop-shadow(0 0 14px rgba(201,168,76,0.9)); transform: scale(1.06); }
.enemy-sprite.dead { opacity: 0.3; pointer-events: none; filter: grayscale(1); }
.enemy-hp-bar-bg {
width: 110px; height: 12px; background: #1a1510; border: 1px solid #4a3d2f; border-radius: 3px; margin: 6px auto 2px; overflow: hidden;
}
.enemy-hp-bar {
height: 100%; background: linear-gradient(90deg, #8b2020, #cc4444); transition: width 0.4s ease; border-radius: 2px;
}
.enemy-name { color: #a89880; font-size: 0.8em; margin-top: 2px; }
.enemy-statuses { display: flex; gap: 3px; justify-content: center; margin-top: 3px; min-height: 16px; flex-wrap: wrap; }
.status-badge {
font-size: 0.7em; padding: 1px 5px; border-radius: 3px; color: #fff; white-space: nowrap;
}
.status-burn { background: rgba(204,100,20,0.8); }
.status-poison { background: rgba(100,160,50,0.8); }
.status-stun { background: rgba(160,130,200,0.8); }
/* Battlefield divider */
#battlefield-divider {
width: 70%; height: 2px; margin: 8px 0;
background: linear-gradient(90deg, transparent, #4a3d2f 30%, #6b5d47 50%, #4a3d2f 70%, transparent);
}
/* Player stats */
#player-stats-bar { display: flex; gap: 20px; padding: 6px 0; align-items: center; }
.stat-item { color: #c9a84c; font-size: 0.9em; letter-spacing: 1px; white-space: nowrap; }
.stat-hp { color: #cc6655 !important; }
.stat-energy { color: #5599cc !important; }
.stat-defense { color: #88aacc !important; }
/* Combat log */
#combat-log-area {
width: 100%; max-width: 700px; height: 60px; overflow-y: auto; margin: 4px 0;
padding: 4px 12px; background: rgba(15,12,8,0.8); border: 1px solid #2a2318; border-radius: 4px;
}
#combat-log-area::-webkit-scrollbar { width: 4px; }
#combat-log-area::-webkit-scrollbar-thumb { background: #3a3025; border-radius: 2px; }
.log-entry { font-size: 0.78em; color: #9a8d7b; padding: 1px 0; line-height: 1.4; }
.log-damage { color: #cc6655; }
.log-heal { color: #66aa66; }
.log-gold { color: #c9a84c; }
/* Hand area */
#hand-area {
width: 100%; min-height: 200px; display: flex; gap: 6px; justify-content: center; align-items: flex-end;
padding: 8px 15px 4px; flex-wrap: wrap;
}
.hand-card {
cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; position: relative;
}
.hand-card:hover:not(.unplayable) {
transform: translateY(-18px);
z-index: 10;
box-shadow: 0 8px 30px rgba(0,0,0,0.6), 0 0 15px rgba(201,168,76,0.2);
}
.hand-card.unplayable { opacity: 0.45; cursor: not-allowed; filter: saturate(0.3); }
/* End turn button */
#btn-end-turn { margin: 6px auto 12px; }
/* ===== SHOP SCREEN ===== */
#screen-shop, #screen-victory, #screen-defeat { justify-content: center; align-items: center; padding-top: 40px; }
#screen-shop h2, #screen-victory h1, #screen-defeat h1 {
color: #c9a84c; font-size: clamp(1.8em, 4vw, 2.5em); margin-bottom: 8px;
text-shadow: 0 0 20px rgba(201,168,76,0.3);
}
#shop-gold-display { color: #c9a84c; font-size: 1.1em; margin-bottom: 15px; }
#shop-cards { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; max-width: 900px; margin: 15px 0; padding: 0 20px; }
.shop-card-item { cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; position: relative; }
.shop-card-item:hover { transform: translateY(-6px); box-shadow: 0 8px 25px rgba(0,0,0,0.5), 0 0 12px rgba(201,168,76,0.2); }
.shop-cost-badge {
position: absolute; bottom: -8px; right: -8px; background: #c9a84c; color: #0d0b08; font-size: 0.75em;
padding: 2px 6px; border-radius: 3px; font-weight: bold; z-index: 5;
}
/* ===== TRANSITION SCREEN ===== */
#screen-transition { justify-content: center; align-items: center; }
.transition-text { color: #c9a84c; font-size: 1.6em; margin-bottom: 20px; text-shadow: 0 0 20px rgba(201,168,76,0.3); }
/* ===== ANIMATIONS ===== */
.damage-flash { animation: damageFlash 0.35s ease-out; }
@keyframes damageFlash {
0% { filter: brightness(4) saturate(0); transform: scale(1.08); }
100% { filter: none; transform: none; }
}
.card-play-anim { animation: cardPlay 0.35s ease-out; }
@keyframes cardPlay {
0% { transform: scale(1.4) translateY(-30px); opacity: 0.3; }
60% { transform: scale(0.95) translateY(2px); }
100% { transform: none; opacity: 1; }
}
.enemy-sprite svg { width: 100%; height: auto; display: block; margin: 0 auto; }
/* Card tooltip */
.card-tooltip {
position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
background: rgba(15,12,8,0.96); border: 1px solid #4a3d2f; border-radius: 5px;
padding: 8px 12px; width: 200px; z-index: 100; pointer-events: none;
font-size: 0.78em; color: #c9b896; line-height: 1.4; text-align: left;
box-shadow: 0 4px 20px rgba(0,0,0,0.7);
}
.card-tooltip .tt-name { font-weight: bold; color: #e8d5a3; margin-bottom: 3px; }
.card-tooltip .tt-type { font-size: 0.9em; opacity: 0.7; }
/* Deck info in shop */
#deck-info { color: #8a7d6b; font-size: 0.9em; margin-bottom: 15px; text-align: center; }
/* ── css: animations ── */
/* Card entrance animation */
@keyframes cardEnter {
from { transform: translateY(30px) scale(0.8); opacity: 0; }
to { transform: none; opacity: 1; }
}
.hand-card { animation: cardEnter 0.3s ease-out backwards; }
.hand-card:nth-child(1) { animation-delay: 0.05s; }
.hand-card:nth-child(2) { animation-delay: 0.1s; }
.hand-card:nth-child(3) { animation-delay: 0.15s; }
.hand-card:nth-child(4) { animation-delay: 0.2s; }
/* Damage number popup */
@keyframes damagePopup {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(-30px); opacity: 0; }
}
.damage-number {
position: absolute; top: -20px; left: 50%; transform: translateX(-50%);
color: #ff4444; font-size: 1.2em; font-weight: bold; pointer-events: none;
animation: damagePopup 0.8s ease-out forwards; z-index: 50;
}
/* Enemy hit flash */
@keyframes enemyHit {
0% { filter: brightness(3) saturate(0); }
100% { filter: none; }
}
.enemy-hit { animation: enemyHit 0.3s ease-out; }
/* Screen transition fade */
.screen.active { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
/* HP bar critical color */
.enemy-hp-bar.critical { background: linear-gradient(90deg, #aa2222, #cc3333) !important; }
.enemy-hp-bar.low { background: linear-gradient(90deg, #aa6622, #cc8833) !important; }
/* Player HP bar */
#player-hp-bar-bg {
width: 150px; height: 10px; background: #1a1510; border: 1px solid #4a3d2f;
border-radius: 3px; overflow: hidden; display: inline-block; vertical-align: middle; margin-left: 8px;
}
#player-hp-bar {
height: 100%; background: linear-gradient(90deg, #44aa44, #66cc66); transition: width 0.4s ease; border-radius: 2px;
}
/* Energy pips */
.energy-pip {
display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 3px;
background: #334466; border: 1px solid #5577aa; transition: all 0.2s;
}
.energy-pip.filled { background: #5599cc; box-shadow: 0 0 6px rgba(85,153,204,0.5); }
/* Card type glow on hover */
.hand-card:hover:not(.unplayable) svg rect:first-child {
stroke-width: 4 !important;
}
/* Shop card purchased animation */
@keyframes purchaseFlash {
0% { box-shadow: 0 0 0 rgba(201,168,76,0); }
50% { box-shadow: 0 0 30px rgba(201,168,76,0.6); }
100% { box-shadow: 0 0 0 rgba(201,168,76,0); }
}
.shop-card-item.purchased { animation: purchaseFlash 0.5s ease-out; }
/* Boss warning pulse */
@keyframes bossPulse {
0%, 100% { text-shadow: 0 0 10px rgba(204,68,68,0.3); }
50% { text-shadow: 0 0 25px rgba(204,68,68,0.7); }
}
.boss-warning { animation: bossPulse 1.5s ease-in-out infinite; color: #cc4433 !important; }
</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-transition" class="screen"><div class="transition-text"></div><button id="btn-continue" class="combat-btn">Continue</button></div><div id="screen-combat" class="screen">
<div id="hud-top">
<span id="hud-floor">Floor 1 - Room 1</span>
<span id="hud-gold">Gold: 0</span>
</div>
<div id="combat-area">
<div id="enemy-zone"></div>
<div id="battlefield-divider"></div>
<div id="player-stats-bar">
<span id="stat-hp">HP: 30/30</span>
<span id="stat-energy">Energy: 5/5</span>
<span id="stat-defense">Defense: 0</span>
</div>
</div>
<div id="combat-log-area"></div><div id="hand-area"></div>
<button id="btn-end-turn" class="combat-btn">End Turn</button>
</div>
<div id="screen-shop" class="screen">
<h2>Card Shop</h2>
<p id="shop-gold-display">Gold: 0</p>
<div id="shop-cards"></div>
<button id="btn-leave-shop" class="combat-btn">Leave Shop</button>
</div>
<div id="screen-victory" class="screen">
<h1>VICTORY</h1>
<p>The Void has been vanquished. Aethelgard is free.</p>
<button id="btn-restart" class="combat-btn">Play Again</button>
</div>
<div id="screen-defeat" class="screen">
<h1>DEFEAT</h1>
<p>The Void consumes all...</p>
<button id="btn-restart2" class="combat-btn">Try 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:4, def:0, desc:"Deals 4 damage. If you have ≥30 HP, draw 1 card.", classId:"bastion" },
"BA-A02": { id:"BA-A02", name:"Earthquake", type:"Attack", cost:3, atk:5, def:0, desc:"Deals 5 damage to all enemies.", aoe:true, classId:"bastion" },
"BA-A03": { id:"BA-A03", name:"Stone Fist", type:"Attack", cost:1, atk:3, def:0, desc:"Deals 3 damage.", classId:"bastion" },
"BA-A04": { id:"BA-A04", name:"Boulder", type:"Attack", cost:4, atk:7, def:0, desc:"Deals 7 damage.", classId:"bastion" },
"BA-D01": { id:"BA-D01", name:"Shield Wall", type:"Defense", cost:0, atk:0, def:5, desc:"Gain 5 Defense this turn.", classId:"bastion" },
"BA-D02": { id:"BA-D02", name:"Iron Skin", type:"Defense", cost:2, atk:0, def:6, desc:"Gain 6 permanent Defense. Discard 1 card.", permDef:true, discardAfter:true, classId:"bastion" },
"BA-D03": { id:"BA-D03", name:"Stand Firm", type:"Defense", cost:1, atk:0, def:3, desc:"Gain 3 Defense. Blocks 1 incoming damage.", blockExtra:true, classId:"bastion" },
"BA-D04": { id:"BA-D04", name:"Taunt", type:"Defense", cost:2, atk:0, def:4, desc:"Gain 4 Defense. Stun 1 enemy for 1 turn.", stunEnemy:true, classId:"bastion" },
"BA-U01": { id:"BA-U01", name:"Search", type:"Other", cost:2, atk:0, def:0, desc:"Draw 1 card. Discard hand if >5 cards.", drawCard:true, discardIfOver5:true, classId:"bastion" },
"BA-U02": { id:"BA-U02", name:"Repair", type:"Other", cost:3, atk:0, def:0, desc:"Heal 6 HP.", heal:6, classId:"bastion" },
"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, classId:"bastion" },
"BA-U04": { id:"BA-U04", name:"Loot", type:"Other", cost:2, atk:0, def:0, desc:"Gain 5 Gold.", gold:5, classId:"bastion" },
// WITHER cards
"WI-A01": { id:"WI-A01", name:"Fireball", type:"Attack", cost:2, atk:4, def:0, desc:"Deals 4 damage. Apply 1 Burn.", burn:1, classId:"wither" },
"WI-A02": { id:"WI-A02", name:"Inferno", type:"Attack", cost:3, atk:5, def:0, desc:"Deals 5 damage to all. Applies 3 Burn.", aoe:true, burnAll:3, classId:"wither" },
"WI-A03": { id:"WI-A03", name:"Ember", type:"Attack", cost:1, atk:3, def:0, desc:"Deals 3 damage. Deals 1 Burn/turn to target.", burn:1, classId:"wither" },
"WI-A04": { id:"WI-A04", name:"Pyre", type:"Attack", cost:4, atk:6, def:0, desc:"Deals 6 damage. All enemies take 2 Burn next turn.", aoe:true, burnAll:2, classId:"wither" },
"WI-D01": { id:"WI-D01", name:"Scorch Shield", type:"Defense", cost:2, atk:0, def:4, desc:"Gain 4 Defense. Block 1 Burn damage.", blockBurn:true, classId:"wither" },
"WI-D02": { id:"WI-D02", name:"Heat Resist", type:"Defense", cost:1, atk:0, def:2, desc:"Gain 2 Defense. Gain 1 Energy.", gainEnergy:1, classId:"wither" },
"WI-D03": { id:"WI-D03", name:"Smoke", type:"Defense", cost:2, atk:0, def:5, desc:"Gain 5 Defense. Block 1 Burn damage.", blockBurn:true, classId:"wither" },
"WI-D04": { id:"WI-D04", name:"Endure", type:"Defense", cost:3, atk:0, def:3, desc:"Gain 3 Defense. Heal 2 HP.", heal:2, classId:"wither" },
"WI-U01": { id:"WI-U01", name:"Poison", type:"Other", cost:2, atk:0, def:0, desc:"Apply 1 Poison to target enemy.", poison:1, classId:"wither" },
"WI-U02": { id:"WI-U02", name:"Burn", type:"Other", cost:3, atk:0, def:0, desc:"Deal 4 damage + 2 Burn to all enemies.", aoe:true, atkVal:4, burnAll:2, classId:"wither" },
"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, classId:"wither" },
"WI-U04": { id:"WI-U04", name:"Wither", type:"Other", cost:2, atk:0, def:0, desc:"Heal 5 HP. Reduce enemy Attack by 1.", heal:5, debuffAtk:true, classId:"wither" },
// SHROUD cards
"SH-A01": { id:"SH-A01", name:"Backstab", type:"Attack", cost:2, atk:5, def:0, desc:"Deals 5 damage. +1 if target has no Armor.", bonusNoArmor:true, classId:"shroud" },
"SH-A02": { id:"SH-A02", name:"Shadow Bolt", type:"Attack", cost:3, atk:6, def:0, desc:"Deals 6 damage. Target cannot block.", unblockable:true, classId:"shroud" },
"SH-A03": { id:"SH-A03", name:"Silence", type:"Attack", cost:1, atk:4, def:0, desc:"Deals 4 damage. Silence target.", silenceEnemy:true, classId:"shroud" },
"SH-A04": { id:"SH-A04", name:"Vengeance", type:"Attack", cost:3, atk:7, def:0, desc:"Deals 7 damage. Gain 2 Energy.", gainEnergy:2, classId:"shroud" },
"SH-D01": { id:"SH-D01", name:"Blur", type:"Defense", cost:3, atk:0, def:3, desc:"Gain 3 Defense. Ignore next attack.", dodgeNext:true, classId:"shroud" },
"SH-D02": { id:"SH-D02", name:"Step", type:"Defense", cost:2, atk:0, def:4, desc:"Gain 4 Defense. Gain 1 Energy.", gainEnergy:1, classId:"shroud" },
"SH-D03": { id:"SH-D03", name:"Evasion", type:"Defense", cost:1, atk:0, def:2, desc:"Gain 2 Defense. Skip enemy action.", skipEnemy:true, classId:"shroud" },
"SH-D04": { id:"SH-D04", name:"Phantom", type:"Defense", cost:2, atk:0, def:5, desc:"Gain 5 Defense. Ghost form for 1 turn.", ghostForm:true, classId:"shroud" },
"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, classId:"shroud" },
"SH-U02": { id:"SH-U02", name:"Teleport", type:"Other", cost:2, atk:0, def:0, desc:"Deal 3 damage to all enemies.", aoe:true, atkVal:3, classId:"shroud" },
"SH-U03": { id:"SH-U03", name:"Hide", type:"Other", cost:2, atk:0, def:1, desc:"Gain 1 Defense. Discard 1 card. Draw 2 cards.", drawCard:true, drawCount:2, discardAfter:true, classId:"shroud" },
"SH-U04": { id:"SH-U04", name:"Assassinate", type:"Other", cost:3, atk:0, def:0, desc:"Deal 10 damage. Cannot be blocked.", atkVal:10, unblockable:true, classId:"shroud" }
};
// ── function: startRoom ──
function startRoom() {
G.enemies = [];
G.selectedEnemyIdx = -1;
G.hero.defense = 0;
G.hero.dodgeNext = false;
G.hero.ghostForm = false;
G.hero.blockBurn = false;
G.hero.skipEnemy = false;
G.hand = [];
G.combatLog = [];
if (G.isBossRoom) {
const bossTpl = BOSS_TEMPLATES[G.floor - 1];
G.enemies.push({
name: bossTpl.name, hp: bossTpl.hp, maxHp: bossTpl.maxHp || bossTpl.hp,
dmg: bossTpl.dmg, defense: 0, burn: 0, poison: 0, stun: 0, silenced: false,
isBoss: true, floor: G.floor, id: 'boss' + G.floor
});
addLog(`⚠ BOSS: ${bossTpl.name} appears!`, 'damage');
} else {
// Floor 1: 1 enemy, Floor 2: 2 enemies, Floor 3: 2-3 enemies
const count = G.floor === 1 ? 1 : (G.floor === 2 ? 2 : (2 + Math.floor(Math.random() * 2)));
const tpl = ENEMY_TEMPLATES[G.floor - 1];
for (let i = 0; i < count; i++) {
G.enemies.push({
name: tpl.names[Math.floor(Math.random() * tpl.names.length)],
hp: tpl.hp, maxHp: tpl.hp, dmg: tpl.dmg, defense: 0, burn: 0, poison: 0, stun: 0, silenced: false,
isBoss: false, floor: G.floor, id: 'e' + i
});
}
}
// Draw starting hand
drawCards(G.hero.handSize);
addLog(`--- Floor ${G.floor}, Room ${G.isBossRoom ? 'BOSS' : G.room} ---`);
showScreen('combat');
renderCombat();
}
// ── const: CLASS_DATA ──
const CLASS_DATA = {
bastion: { name:"The Bastion", theme:"Immovable Object", hp:45, energy:3, handSize:4 },
wither: { name:"The Wither", theme:"Rot and Burn", hp:38, energy:5, handSize:4 },
shroud: { name:"The Shroud", theme:"Shadow and Speed", hp:36, energy:5, handSize:4 }
};
// ── const: ENEMY_TEMPLATES ──
const ENEMY_TEMPLATES = [
{ floor:1, names:["Goblin","Rat Swarm","Bandit"], hp:6, dmg:2 },
{ floor:2, names:["Vampire Knight","Ogre","Dark Priest"], hp:20, dmg:4 },
{ floor:3, names:["Void Warrior","Cultist","Shadow Stalker"], hp:30, dmg:7 }
];
// ── const: BOSS_TEMPLATES ──
const BOSS_TEMPLATES = [
{ name:"The Gatekeeper", type:'tank', hp:45, dmg:8 },
{ name:"The Corrupted", type:'burst', hp:55, dmg:10 },
{ name:"The Void Lord", type:'regen', hp:30, maxHp:30, dmg:12 }
];
// ── function: createCardSVG ──
function createCardSVG(cardData, isBack) {
const classColors = {
bastion: { primary:"#7a6540", secondary:"#C9A84C", accent:"#D4AF37", bg1:"#2a2015", bg2:"#0d0b08" },
wither: { primary:"#6B2D2D", secondary:"#CC6644", accent:"#FF6B35", bg1:"#2a1515", bg2:"#0d0808" },
shroud: { primary:"#2D2D6B", secondary:"#7B7BDD", accent:"#9B59B6", bg1:"#15152a", bg2:"#08080d" }
};
const typeColors = { Attack:"#cc4433", Defense:"#4488bb", Other:"#aa66cc" };
if (isBack) {
const c = classColors[cardData.classId] || classColors.bastion;
let centerArt = '';
if (cardData.classId === 'bastion') {
centerArt = `<polygon points="90,50 128,78 114,120 66,120 52,78" fill="none" stroke="${c.secondary}" stroke-width="2.5" opacity="0.7"/>
<polygon points="90,65 115,83 105,110 75,110 65,83" fill="${c.primary}" opacity="0.3"/>`;
} else if (cardData.classId === 'wither') {
centerArt = `<circle cx="90" cy="85" r="32" fill="none" stroke="${c.secondary}" stroke-width="2" opacity="0.7"/>
<circle cx="90" cy="85" r="18" fill="none" stroke="${c.accent}" stroke-width="1.5" opacity="0.6"/>
<circle cx="90" cy="85" r="6" fill="${c.accent}" opacity="0.4"/>`;
} else {
centerArt = `<path d="M55 72 Q90 38 125 72 Q90 118 55 72Z" fill="none" stroke="${c.secondary}" stroke-width="2.5" opacity="0.7"/>
<circle cx="90" cy="85" r="6" fill="${c.accent}" opacity="0.5"/>`;
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 250" width="140" height="196">
<defs>
<linearGradient id="backGrad_${cardData.classId}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:${c.bg1}"/>
<stop offset="100%" style="stop-color:${c.bg2}"/>
</linearGradient>
</defs>
<rect x="2" y="2" width="176" height="246" rx="8" fill="#0a0806" stroke="${c.secondary}" stroke-width="2.5"/>
<rect x="6" y="6" width="168" height="238" rx="5" fill="url(#backGrad_${cardData.classId})"/>
<pattern id="bp_${cardData.classId}" patternUnits="userSpaceOnUse" width="40" height="40">
<path d="M20 0 L40 20 L20 40 L0 20Z" fill="none" stroke="${c.secondary}" stroke-width="0.5" opacity="0.15"/>
</pattern>
<rect x="6" y="6" width="168" height="238" rx="5" fill="url(#bp_${cardData.classId})"/>
${centerArt}
<text x="90" y="148" text-anchor="middle" font-size="9" fill="${c.secondary}" opacity="0.5" font-family="Georgia,serif" letter-spacing="3">AETHELGARD</text>
</svg>`;
}
const c = classColors[cardData.classId] || classColors.bastion;
const tc = typeColors[cardData.type];
// Split description into lines
let descLines = [];
const words = cardData.desc.split(' ');
let line = '';
words.forEach(w => {
if ((line + ' ' + w).length > 32) { descLines.push(line); line = w; }
else line = line ? line + ' ' + w : w;
});
if (line) descLines.push(line);
let statsHTML = '';
if (cardData.atk > 0 && cardData.def > 0) {
statsHTML = `<rect x="14" y="172" width="56" height="24" rx="3" fill="#1a0a0a" stroke="${tc}" stroke-width="1.5"/>
<text x="42" y="189" text-anchor="middle" font-size="13" fill="#e44" font-weight="bold">⚔${cardData.atk}</text>
<rect x="100" y="172" width="56" height="24" rx="3" fill="#0a0a1a" stroke="${tc}" stroke-width="1.5"/>
<text x="128" y="189" text-anchor="middle" font-size="13" fill="#48e" font-weight="bold">🛡${cardData.def}</text>`;
} else if (cardData.atk > 0) {
statsHTML = `<rect x="52" y="172" width="64" height="24" rx="3" fill="#1a0a0a" stroke="${tc}" stroke-width="1.5"/>
<text x="84" y="189" text-anchor="middle" font-size="13" fill="#e44" font-weight="bold">⚔${cardData.atk}</text>`;
} else if (cardData.def > 0) {
statsHTML = `<rect x="52" y="172" width="64" height="24" rx="3" fill="#0a0a1a" stroke="${tc}" stroke-width="1.5"/>
<text x="84" y="189" text-anchor="middle" font-size="13" fill="#48e" font-weight="bold">🛡${cardData.def}</text>`;
} else {
statsHTML = `<rect x="52" y="172" width="64" height="24" rx="3" fill="#0a0a1a" stroke="${tc}" stroke-width="1.5"/>
<text x="84" y="189" text-anchor="middle" font-size="13" fill="${c.secondary}">✦</text>`;
}
let typeIcon = cardData.type === 'Attack' ? '⚔' : cardData.type === 'Defense' ? '🛡' : '✦';
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 250" width="140" height="196">
<defs>
<linearGradient id="cf_${cardData.id}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:${c.bg1}"/>
<stop offset="100%" style="stop-color:${c.bg2}"/>
</linearGradient>
</defs>
<!-- Outer border with type color -->
<rect x="1" y="1" width="178" height="248" rx="9" fill="#0a0806" stroke="${tc}" stroke-width="3"/>
<!-- Inner card body -->
<rect x="5" y="5" width="170" height="240" rx="7" fill="url(#cf_${cardData.id})"/>
<!-- Header bar -->
<rect x="8" y="8" width="164" height="32" rx="4" fill="#0a0806" opacity="0.85"/>
<text x="90" y="29" text-anchor="middle" font-size="14" fill="${c.secondary}" font-weight="bold" font-family="Georgia,serif">${cardData.name}</text>
<!-- Cost circle -->
<circle cx="26" cy="27" r="11" fill="#1a1510" stroke="${tc}" stroke-width="2"/>
<text x="26" y="32" text-anchor="middle" font-size="13" fill="${c.secondary}" font-weight="bold">${cardData.cost}</text>
<!-- Image placeholder area -->
<rect x="8" y="44" width="164" height="70" rx="4" fill="#0a0806" opacity="0.5"/>
<rect x="12" y="48" width="156" height="62" rx="3" fill="${c.bg1}" opacity="0.3" stroke="${tc}" stroke-width="0.5" stroke-dasharray="3,3"/>
<text x="90" y="84" text-anchor="middle" font-size="30" opacity="0.25">${typeIcon}</text>
<!-- Description area -->
<rect x="8" y="118" width="164" height="52" rx="4" fill="#0a0806" opacity="0.7"/>
${descLines.map((l, i) => `<text x="90" y="${134 + i * 13}" text-anchor="middle" font-size="8.5" fill="${c.secondary}" opacity="0.8">${l}</text>`).join('')}
<!-- Stats -->
${statsHTML}
<!-- Footer -->
<rect x="8" y="218" width="164" height="22" rx="3" fill="#0a0806" opacity="0.7"/>
<text x="90" y="233" text-anchor="middle" font-size="8" fill="${c.secondary}" opacity="0.5">${cardData.type} • ${cardData.classId.toUpperCase()}</text>
</svg>`;
}
// ── function: createEnemySVG ──
function createEnemySVG(enemy) {
const isBoss = enemy.isBoss;
const w = isBoss ? 130 : 100;
const h = isBoss ? 140 : 110;
let bodyColor, accentColor, eyeColor;
if (enemy.floor === 1) {
bodyColor = '#5a7a3a'; accentColor = '#8aaa5a'; eyeColor = '#ffcc00'; // Goblin green
} else if (enemy.floor === 2) {
bodyColor = '#6a2a4a'; accentColor = '#9a4a6a'; eyeColor = '#ff3333'; // Vampire purple-red
} else if (enemy.floor === 3) {
bodyColor = '#2a1a5a'; accentColor = '#5a3aaa'; eyeColor = '#cc00ff'; // Void purple
} else {
bodyColor = '#4a4a4a'; accentColor = '#7a7a7a'; eyeColor = '#ffffff';
}
if (isBoss) {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w*1.1}" height="${h*1.1}">
<defs>
<radialGradient id="bossGlow_${enemy.id || 'x'}" cx="50%" cy="40%">
<stop offset="0%" style="stop-color:${accentColor};stop-opacity:0.3"/>
<stop offset="100%" style="stop-color:transparent"/>
</radialGradient>
</defs>
<!-- Aura -->
<ellipse cx="${w/2}" cy="${h*0.5}" rx="${w*0.45}" ry="${h*0.45}" fill="url(#bossGlow_${enemy.id || 'x'})"/>
<!-- Body -->
<path d="M${w*0.3} ${h*0.85} L${w*0.2} ${h*0.5} Q${w*0.15} ${h*0.35} ${w*0.3} ${h*0.25} L${w*0.4} ${h*0.15} L${w*0.6} ${h*0.15} L${w*0.7} ${h*0.25} Q${w*0.85} ${h*0.35} ${w*0.8} ${h*0.5} L${w*0.7} ${h*0.85}" fill="${bodyColor}" stroke="${accentColor}" stroke-width="2"/>
<!-- Head -->
<circle cx="${w/2}" cy="${h*0.18}" r="${w*0.14}" fill="${bodyColor}" stroke="${accentColor}" stroke-width="2"/>
<!-- Eyes -->
<ellipse cx="${w*0.43}" cy="${h*0.17}" rx="5" ry="6" fill="${eyeColor}"/>
<ellipse cx="${w*0.57}" cy="${h*0.17}" rx="5" ry="6" fill="${eyeColor}"/>
<!-- Crown/horns -->
<path d="M${w*0.38} ${h*0.08} L${w*0.42} ${h*0.02} L${w*0.5} ${h*0.06} L${w*0.58} ${h*0.02} L${w*0.62} ${h*0.08}" fill="none" stroke="${accentColor}" stroke-width="2"/>
<!-- Arms -->
<path d="M${w*0.25} ${h*0.45} Q${w*0.1} ${h*0.55} ${w*0.15} ${h*0.7}" fill="none" stroke="${bodyColor}" stroke-width="6" stroke-linecap="round"/>
<path d="M${w*0.75} ${h*0.45} Q${w*0.9} ${h*0.55} ${w*0.85} ${h*0.7}" fill="none" stroke="${bodyColor}" stroke-width="6" stroke-linecap="round"/>
<!-- Boss label -->
<text x="${w/2}" y="${h*0.95}" text-anchor="middle" font-size="10" fill="${accentColor}" font-family="Georgia,serif" opacity="0.7">BOSS</text>
</svg>`;
}
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w*1.1}" height="${h*1.1}">
<!-- Body -->
<path d="M${w*0.35} ${h*0.85} L${w*0.28} ${h*0.5} Q${w*0.25} ${h*0.38} ${w*0.35} ${h*0.3} L${w*0.42} ${h*0.2} L${w*0.58} ${h*0.2} L${w*0.65} ${h*0.3} Q${w*0.75} ${h*0.38} ${w*0.72} ${h*0.5} L${w*0.65} ${h*0.85}" fill="${bodyColor}" stroke="${accentColor}" stroke-width="1.5"/>
<!-- Head -->
<circle cx="${w/2}" cy="${h*0.22}" r="${w*0.13}" fill="${bodyColor}" stroke="${accentColor}" stroke-width="1.5"/>
<!-- Eyes -->
<ellipse cx="${w*0.44}" cy="${h*0.21}" rx="3" ry="4" fill="${eyeColor}"/>
<ellipse cx="${w*0.56}" cy="${h*0.21}" rx="3" ry="4" fill="${eyeColor}"/>
<!-- Arms -->
<path d="M${w*0.3} ${h*0.42} Q${w*0.18} ${h*0.5} ${w*0.22} ${h*0.6}" fill="none" stroke="${bodyColor}" stroke-width="4" stroke-linecap="round"/>
<path d="M${w*0.7} ${h*0.42} Q${w*0.82} ${h*0.5} ${w*0.78} ${h*0.6}" fill="none" stroke="${bodyColor}" stroke-width="4" stroke-linecap="round"/>
</svg>`;
}
// ── function: createClassIconSVG ──
function createClassIconSVG(classId) {
const colors = { bastion: '#C9A84C', wither: '#CC6644', shroud: '#7B7BDD' };
const c = colors[classId] || '#c9a84c';
let art = '';
if (classId === 'bastion') {
art = `<polygon points="40,12 68,28 58,58 22,58 12,28" fill="none" stroke="${c}" stroke-width="3"/>
<rect x="30" y="60" width="20" height="12" rx="2" fill="${c}" opacity="0.4"/>`;
} else if (classId === 'wither') {
art = `<circle cx="40" cy="35" r="22" fill="none" stroke="${c}" stroke-width="3"/>
<path d="M30 35 Q40 18 50 35 Q40 52 30 35Z" fill="${c}" opacity="0.3"/>`;
} else {
art = `<path d="M18 30 Q40 8 62 30 Q40 60 18 30Z" fill="none" stroke="${c}" stroke-width="3"/>
<circle cx="40" cy="35" r="6" fill="${c}" opacity="0.5"/>`;
}
return `<svg viewBox="0 0 80 80" width="80" height="80">${art}</svg>`;
}
// ── let: G ──
let G = {};
// ── function: initGame ──
function initGame(classId) {
const cls = CLASS_DATA[classId];
G = {
classId,
hero: {
hp: cls.hp, maxHp: cls.hp,
energy: cls.energy, maxEnergy: cls.energy,
defense: 0, permDefense: 0,
handSize: cls.handSize, gold: 0,
dodgeNext: false, ghostForm: false, blockBurn: false, skipEnemy: false
},
deck: [], discardPile: [], hand: [],
inventory: {}, // cardId -> count
enemies: [], selectedEnemyIdx: -1,
floor: 1, room: 0, isBossRoom: false,
combatLog: [], turnCount: 0,
phase: 'combat' // combat | shop | transition | victory | defeat
};
// Build starter deck (3 copies of each class card)
const classCards = Object.values(CARD_DATA).filter(c => c.classId === classId);
classCards.forEach(card => {
G.inventory[card.id] = 3;
for (let i = 0; i < 3; i++) G.deck.push({...card});
});
shuffle(G.deck);
startRoom();
}
// ── function: shuffle ──
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 ──
function drawCards(count) {
for (let i = 0; i < count; i++) {
if (G.hand.length >= G.hero.handSize + 10) break; // soft cap
if (G.deck.length === 0) {
if (G.discardPile.length === 0) break;
G.deck = [...G.discardPile];
G.discardPile = [];
shuffle(G.deck);
addLog('Deck reshuffled from discard pile');
}
if (G.deck.length > 0 && G.hand.length < G.hero.handSize + 5) {
G.hand.push(G.deck.pop());
}
}
}
// ── function: discardHand ──
function discardHand() {
while (G.hand.length > 0) {
G.discardPile.push(G.hand.pop());
}
}
// ── function: playCard ──
function playCard(handIdx) {
const card = G.hand[handIdx];
if (!card || G.hero.energy < card.cost) return false;
G.hero.energy -= card.cost;
let logMsg = `▶ ${card.name}`;
// === ATTACK CARDS ===
if (card.type === 'Attack') {
const aliveEnemies = getAliveEnemies();
const targets = card.aoe ? [...aliveEnemies] :
(G.selectedEnemyIdx >= 0 && G.enemies[G.selectedEnemyIdx] && G.enemies[G.selectedEnemyIdx].hp > 0
? [G.enemies[G.selectedEnemyIdx]] : []);
if (targets.length === 0) {
addLog('No valid target!');
return false;
}
let dmg = card.atk;
// Backstab bonus: +1 if target has no defense
if (card.bonusNoArmor && targets[0] && targets[0].defense === 0) {
dmg += 1;
logMsg += ' (+1 crit)';
}
targets.forEach(enemy => {
if (!enemy || enemy.hp <= 0) return;
let actualDmg = Math.max(0, dmg - enemy.defense);
if (card.unblockable) actualDmg = dmg;
enemy.hp -= actualDmg;
logMsg += ` → ${actualDmg} to ${enemy.name}`;
// Apply burn
if (card.burn && !card.aoe) {
enemy.burn += card.burn;
addLog(`${enemy.name} burned (${card.burn})`, 'damage');
}
});
if (card.burnAll) {
aliveEnemies.forEach(e => { e.burn += card.burnAll; });
addLog(`All enemies burned (+${card.burnAll})`, 'damage');
}
// Slam: draw if >=30 HP
if (card.id === 'BA-A01' && G.hero.hp >= 30) {
drawCards(1);
logMsg += ' (+draw)';
}
// Silence enemy
if (card.silenceEnemy && targets[0] && targets[0].hp > 0) {
targets[0].silenced = true;
addLog(`${targets[0].name} silenced!`);
}
// Vengeance: gain energy
if (card.gainEnergy) {
G.hero.energy = Math.min(G.hero.maxEnergy, G.hero.energy + card.gainEnergy);
logMsg += ` (+${card.gainEnergy}⚡)`;
}
addLog(logMsg, 'damage');
}
// === DEFENSE CARDS ===
if (card.type === 'Defense') {
G.hero.defense += card.def;
logMsg += ` +${card.def}🛡`;
if (card.permDef) {
G.hero.permDefense += card.def;
logMsg += ' (perm)';
}
// Taunt: stun enemy
if (card.stunEnemy && G.selectedEnemyIdx >= 0 && G.enemies[G.selectedEnemyIdx] && G.enemies[G.selectedEnemyIdx].hp > 0) {
G.enemies[G.selectedEnemyIdx].stun = Math.max(G.enemies[G.selectedEnemyIdx].stun, 1);
addLog(`${G.enemies[G.selectedEnemyIdx].name} stunned!`);
}
// Blur: dodge next attack
if (card.dodgeNext) G.hero.dodgeNext = true;
// Phantom: ghost form
if (card.ghostForm) G.hero.ghostForm = true;
// Scorch Shield / Smoke: block burn damage
if (card.blockBurn) G.hero.blockBurn = true;
// Endure: heal
if (card.heal) {
G.hero.hp = Math.min(G.hero.maxHp, G.hero.hp + card.heal);
logMsg += ` (+${card.heal}❤)`;
}
// Heat Resist / Step: gain energy
if (card.gainEnergy) {
G.hero.energy = Math.min(G.hero.maxEnergy, G.hero.energy + card.gainEnergy);
logMsg += ` (+${card.gainEnergy}⚡)`;
}
addLog(logMsg);
}
// === OTHER CARDS ===
if (card.type === 'Other') {
// Heal effects
if (card.heal) {
G.hero.hp = Math.min(G.hero.maxHp, G.hero.hp + card.heal);
logMsg += ` (+${card.heal}❤)`;
addLog(logMsg, 'heal');
}
// Draw cards
if (card.drawCard) {
const count = card.drawCount || 1;
drawCards(count);
// Search: discard if over 5
if (card.discardIfOver5 && G.hand.length > 5) {
while (G.hand.length > 5) G.discardPile.push(G.hand.pop());
addLog('Hand discarded to 5');
}
}
// Gain energy
if (card.gainEnergy) {
G.hero.energy = Math.min(G.hero.maxEnergy, G.hero.energy + card.gainEnergy);
logMsg += ` (+${card.gainEnergy}⚡)`;
}
// Gold
if (card.gold) {
G.hero.gold += card.gold;
addLog(`+${card.gold} gold`, 'gold');
}
// Poison target enemy
if (card.poison && G.selectedEnemyIdx >= 0 && G.enemies[G.selectedEnemyIdx] && G.enemies[G.selectedEnemyIdx].hp > 0) {
G.enemies[G.selectedEnemyIdx].poison += card.poison;
addLog(`${G.enemies[G.selectedEnemyIdx].name} poisoned!`, 'damage');
}
// Debuff attack
if (card.debuffAtk && G.selectedEnemyIdx >= 0 && G.enemies[G.selectedEnemyIdx]) {
G.enemies[G.selectedEnemyIdx].dmg = Math.max(1, G.enemies[G.selectedEnemyIdx].dmg - 1);
addLog(`${G.enemies[G.selectedEnemyIdx].name} weakened!`);
}
// AoE damage (Burn card, Teleport)
if (card.atkVal && card.aoe) {
getAliveEnemies().forEach(enemy => {
let actualDmg = Math.max(0, card.atkVal - enemy.defense);
if (card.unblockable) actualDmg = card.atkVal;
enemy.hp -= actualDmg;
});
addLog(`${card.name}: ${card.atkVal} dmg to all!`, 'damage');
}
// Single target damage (Assassinate)
if (card.atkVal && !card.aoe) {
const aliveEnemies = getAliveEnemies();
const target = G.selectedEnemyIdx >= 0 && G.enemies[G.selectedEnemyIdx] && G.enemies[G.selectedEnemyIdx].hp > 0
? G.enemies[G.selectedEnemyIdx] : (aliveEnemies[0] || null);
if (target) {
let actualDmg = Math.max(0, card.atkVal - target.defense);
if (card.unblockable) actualDmg = card.atkVal;
target.hp -= actualDmg;
addLog(`${card.name}: ${actualDmg} dmg to ${target.name}!`, 'damage');
}
}
// Stun enemy
if (card.stunEnemy && G.selectedEnemyIdx >= 0 && G.enemies[G.selectedEnemyIdx] && G.enemies[G.selectedEnemyIdx].hp > 0) {
G.enemies[G.selectedEnemyIdx].stun = Math.max(G.enemies[G.selectedEnemyIdx].stun, 1);
addLog(`${G.enemies[G.selectedEnemyIdx].name} stunned!`);
}
// Skip enemy turn flag
if (card.skipEnemy) G.hero.skipEnemy = true;
// Defense from Hide card
if (card.def && !card.type === 'Defense') {
G.hero.defense += card.def;
}
if (!card.heal && !card.atkVal) addLog(logMsg);
}
// Discard after effects (Iron Skin, Hide)
if (card.discardAfter && G.hand.length > 0) {
const ri = Math.floor(Math.random() * G.hand.length);
G.discardPile.push(G.hand.splice(ri, 1)[0]);
addLog('Discarded 1 card');
}
// Remove played card from hand
G.hand.splice(handIdx, 1);
return true;
}
// ── function: endTurn ──
function endTurn() {
G.turnCount++;
// Discard hand to deck
discardHand();
addLog('--- Enemy Turn ---');
processEnemyTurn();
if (G.hero.hp <= 0) {
G.hero.hp = 0;
showScreen('defeat');
return;
}
// Check if all enemies dead
const aliveEnemies = getAliveEnemies();
if (aliveEnemies.length === 0) {
onEnemyDefeated();
return;
}
// Reset temporary defense AFTER enemy turn
G.hero.defense = 0;
G.hero.dodgeNext = false;
G.hero.ghostForm = false;
G.hero.blockBurn = false;
// Start new player turn: recover energy and draw
G.hero.energy = Math.min(G.hero.maxEnergy, G.hero.energy + 1);
drawCards(G.hero.handSize - G.hand.length);
addLog(`--- Your Turn (${G.hero.energy}⚡) ---`);
renderCombat();
}
// ── function: processEnemyTurn ──
function processEnemyTurn() {
const aliveEnemies = getAliveEnemies();
if (aliveEnemies.length === 0) return;
// Boss special mechanics at start of enemy turn
if (G.isBossRoom && G.enemies[0]) {
const boss = G.enemies[0];
const bossTpl = BOSS_TEMPLATES[G.floor - 1];
// Boss 1: The Gatekeeper - blocks 5 dmg until HP < 20
if (bossTpl.type === 'tank' && boss.hp >= 20) {
boss.defense = Math.max(boss.defense, 5);
}
// Boss 3: The Void Lord - regen 10 HP per turn
if (bossTpl.type === 'regen') {
const oldHp = boss.hp;
boss.hp = Math.min((boss.maxHp || boss.hp) + 20, boss.hp + 10);
addLog(`${boss.name} regenerates ${boss.hp - oldHp} HP`, 'heal');
}
}
// Skip enemy turn if flagged
if (G.hero.skipEnemy) {
G.hero.skipEnemy = false;
addLog('Enemies skip their turn!');
return;
}
aliveEnemies.forEach(enemy => {
// Status effects on enemy at start of their action
if (enemy.burn > 0 && !G.hero.blockBurn) {
enemy.hp -= enemy.burn;
addLog(`${enemy.name} takes ${enemy.burn} burn dmg`, 'damage');
enemy.burn = 0;
} else if (enemy.burn > 0 && G.hero.blockBurn) {
addLog(`Burn blocked by shield!`);
enemy.burn = 0;
}
if (enemy.poison > 0) {
enemy.hp -= enemy.poison;
addLog(`${enemy.name} takes ${enemy.poison} poison dmg`, 'damage');
enemy.poison = 0;
}
if (enemy.hp <= 0) return;
// Stun check
if (enemy.stun > 0) {
enemy.stun--;
addLog(`${enemy.name} is stunned`);
return;
}
// Enemy attacks
let dmg = enemy.dmg;
// Boss 2: The Corrupted - deals 20 if player has <3 energy
if (G.isBossRoom && BOSS_TEMPLATES[G.floor-1].type === 'burst' && G.hero.energy < 3) {
dmg = 20;
addLog(`${enemy.name} uses CORRUPTED STRIKE!`, 'damage');
}
// Dodge/ghost form check
if (G.hero.dodgeNext || G.hero.ghostForm) {
addLog(`${enemy.name}'s attack misses!`);
return;
}
// Apply defense
let totalDefense = G.hero.defense + G.hero.permDefense;
let actualDmg = Math.max(0, dmg - totalDefense);
if (actualDmg > 0) {
G.hero.hp -= actualDmg;
addLog(`${enemy.name} deals ${actualDmg} damage`, 'damage');
} else {
addLog(`${enemy.name}'s attack blocked (${totalDefense}🛡)`);
}
// Reset enemy defense for next round
enemy.defense = 0;
});
// Check player death
if (G.hero.hp <= 0) {
G.hero.hp = 0;
addLog('You have fallen...', 'damage');
}
}
// ── function: onEnemyDefeated ──
function onEnemyDefeated() {
// Gold reward
const goldReward = G.isBossRoom ? 15 : 5;
G.hero.gold += goldReward;
addLog(`+${goldReward} gold!`, 'gold');
if (G.isBossRoom) {
// Boss defeated - advance floor or win
if (G.floor >= 3) {
showScreen('victory');
return;
}
G.floor++;
G.room = 0;
G.isBossRoom = false;
// Show transition then next room
showTransition(`Floor ${G.floor - 1} cleared! Descending to Floor ${G.floor}...`, () => {
startRoom();
});
} else {
G.room++;
if (G.room >= 3) {
// Boss time after 3 rooms
G.isBossRoom = true;
showTransition(`Floor ${G.floor} cleared! The boss awaits...`, () => {
startRoom();
});
} else {
// Shop between rooms
renderShop();
showScreen('shop');
}
}
}
// ── function: showScreen ──
function showScreen(name) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const target = document.getElementById(`screen-${name}`);
if (target) target.classList.add('active');
}
// ── function: renderCombat ──
function renderCombat() {
// HUD
const floorEl = document.getElementById('hud-floor');
const goldEl = document.getElementById('hud-gold');
if (floorEl) {
floorEl.textContent = `Floor ${G.floor}${G.isBossRoom ? ' - BOSS' : ' - Room ' + G.room}`;
floorEl.className = G.isBossRoom ? 'boss-warning' : '';
}
if (goldEl) goldEl.textContent = `Gold: ${G.hero.gold}`;
// Player stats with HP bar and energy pips
const hpEl = document.getElementById('stat-hp');
const energyEl = document.getElementById('stat-energy');
const defEl = document.getElementById('stat-defense');
if (hpEl) {
const hpPct = Math.max(0, G.hero.hp / G.hero.maxHp * 100);
let hpBarHTML = `<span id="player-hp-bar-bg"><span id="player-hp-bar" style="width:${hpPct}%"></span></span>`;
hpEl.innerHTML = `HP: ${G.hero.hp}/${G.hero.maxHp}${hpBarHTML}`;
}
if (energyEl) {
let pipsHTML = '';
for (let i = 0; i < G.hero.maxEnergy; i++) {
pipsHTML += `<span class="energy-pip${i < G.hero.energy ? ' filled' : ''}"></span>`;
}
energyEl.innerHTML = `Energy: ${pipsHTML}`;
}
if (defEl) defEl.textContent = `Defense: ${G.hero.defense + G.hero.permDefense}`;
// Enemies
const enemyZone = document.getElementById('enemy-zone');
if (enemyZone) {
enemyZone.innerHTML = '';
G.enemies.forEach((enemy, idx) => {
const div = document.createElement('div');
div.className = 'enemy-sprite' + (idx === G.selectedEnemyIdx ? ' selected' : '') + (enemy.hp <= 0 ? ' dead' : '');
const hpPct = Math.max(0, enemy.hp / enemy.maxHp * 100);
let barClass = '';
if (hpPct < 25) barClass = ' critical';
else if (hpPct < 50) barClass = ' low';
let statusesHTML = '';
if (enemy.burn > 0) statusesHTML += `<span class="status-badge status-burn">🔥${enemy.burn}</span>`;
if (enemy.poison > 0) statusesHTML += `<span class="status-badge status-poison">☠${enemy.poison}</span>`;
if (enemy.stun > 0) statusesHTML += `<span class="status-badge status-stun">💫${enemy.stun}</span>`;
div.innerHTML = `
${createEnemySVG(enemy)}
<div class="enemy-hp-bar-bg"><div class="enemy-hp-bar${barClass}" style="width:${hpPct}%"></div></div>
<div class="enemy-name">${enemy.name} (${enemy.hp}/${enemy.maxHp})</div>
<div class="enemy-statuses">${statusesHTML}</div>
`;
div.addEventListener('click', () => selectEnemy(idx));
enemyZone.appendChild(div);
});
}
// Hand
const handArea = document.getElementById('hand-area');
if (handArea) {
handArea.innerHTML = '';
G.hand.forEach((card, idx) => {
const div = document.createElement('div');
div.className = 'hand-card' + (G.hero.energy < card.cost ? ' unplayable' : '');
div.innerHTML = createCardSVG(card);
// Tooltip on hover
div.addEventListener('mouseenter', () => {
if (div.querySelector('.card-tooltip')) return;
const tt = document.createElement('div');
tt.className = 'card-tooltip';
tt.innerHTML = `<div class="tt-name">${card.name}</div><div class="tt-type">${card.type} • Cost: ${card.cost}</div>${card.desc}`;
div.style.position = 'relative';
div.appendChild(tt);
});
div.addEventListener('mouseleave', () => {
const tt = div.querySelector('.card-tooltip');
if (tt) tt.remove();
});
div.addEventListener('click', () => {
if (G.hero.energy >= card.cost) {
playCard(idx);
// Check for enemy deaths after playing card
const alive = getAliveEnemies();
if (alive.length === 0) {
onEnemyDefeated();
return;
}
renderCombat();
}
});
handArea.appendChild(div);
});
}
// Combat log
const logArea = document.getElementById('combat-log-area');
if (logArea) {
logArea.innerHTML = G.combatLog.slice(-10).map(entry =>
`<div class="log-entry ${entry.cls}">${entry.msg}</div>`
).join('');
logArea.scrollTop = logArea.scrollHeight;
}
// End turn button state
const btnEndTurn = document.getElementById('btn-end-turn');
if (btnEndTurn) {
btnEndTurn.disabled = false;
}
}
// ── function: renderShop ──
function renderShop() {
const goldDisplay = document.getElementById('shop-gold-display');
if (goldDisplay) goldDisplay.textContent = `Gold: ${G.hero.gold}`;
const shopCards = document.getElementById('shop-cards');
if (!shopCards) return;
shopCards.innerHTML = '';
// Show random cards from all classes
const allCards = Object.values(CARD_DATA);
const shuffled = [...allCards].sort(() => Math.random() - 0.5).slice(0, 6);
shuffled.forEach(card => {
const cost = getShopCost(card.id);
const canBuy = G.hero.gold >= cost && G.inventory[card.id] < 5;
const div = document.createElement('div');
div.className = 'shop-card-item';
if (!canBuy) div.style.opacity = '0.4';
let copyCount = G.inventory[card.id] || 0;
div.innerHTML = createCardSVG(card);
// Cost badge
const badge = document.createElement('div');
badge.className = 'shop-cost-badge';
badge.textContent = copyCount >= 5 ? 'MAX' : `${cost}g`;
div.appendChild(badge);
// Copy count indicator
if (copyCount > 0) {
const countBadge = document.createElement('div');
countBadge.style.cssText = 'position:absolute;top:-8px;left:-8px;background:#5a4a30;color:#c9a84c;font-size:0.7em;padding:2px 5px;border-radius:3px;z-index:5;';
countBadge.textContent = `×${copyCount}`;
div.appendChild(countBadge);
}
// Tooltip
div.addEventListener('mouseenter', () => {
if (div.querySelector('.card-tooltip')) return;
const tt = document.createElement('div');
tt.className = 'card-tooltip';
tt.innerHTML = `<div class="tt-name">${card.name}</div><div class="tt-type">${card.type} • Cost: ${card.cost}</div>${card.desc}<br>Owned: ${copyCount}/5`;
div.style.position = 'relative';
div.appendChild(tt);
});
div.addEventListener('mouseleave', () => {
const tt = div.querySelector('.card-tooltip');
if (tt) tt.remove();
});
div.addEventListener('click', () => {
buyCard(card.id);
});
shopCards.appendChild(div);
});
// Deck info
let deckInfo = document.getElementById('deck-info');
if (!deckInfo) {
deckInfo = document.createElement('div');
deckInfo.id = 'deck-info';
const leaveBtn = document.getElementById('btn-leave-shop');
if (leaveBtn) leaveBtn.parentNode.insertBefore(deckInfo, leaveBtn);
}
const totalCards = G.deck.length;
deckInfo.textContent = `Deck: ${totalCards} cards | Hand Size: ${G.hero.handSize}`;
}
// ── function: renderClassSelect ──
function renderClassSelect() {
const container = document.getElementById('class-cards');
if (!container) return;
container.innerHTML = '';
Object.entries(CLASS_DATA).forEach(([id, cls]) => {
const div = document.createElement('div');
div.className = 'class-card';
div.innerHTML = `
<div class="class-icon">${createClassIconSVG(id)}</div>
<h3>${cls.name}</h3>
<div class="class-theme">"${cls.theme}"</div>
<div class="class-stats">HP: ${cls.hp} | Energy: ${cls.energy}/turn<br>Hand Size: ${cls.handSize}</div>
`;
div.addEventListener('click', () => {
initGame(id);
});
container.appendChild(div);
});
}
// ── function: addLog ──
function addLog(msg, cls) {
G.combatLog.push({ msg, cls: cls || '' });
}
// ── function: getAliveEnemies ──
function getAliveEnemies() {
return G.enemies.filter(e => e.hp > 0);
}
// ── function: selectEnemy ──
function selectEnemy(idx) {
if (idx >= 0 && idx < G.enemies.length && G.enemies[idx].hp > 0) {
G.selectedEnemyIdx = idx;
}
}
// ── function: showTransition ──
function showTransition(text, callback) {
const transEl = document.querySelector('.transition-text');
if (transEl) transEl.textContent = text;
const btn = document.getElementById('btn-continue');
if (btn) {
btn.onclick = () => {
showScreen('combat');
callback();
};
}
showScreen('transition');
}
// ── function: getShopCost ──
function getShopCost(cardId) {
const count = G.inventory[cardId] || 0;
if (count >= 5) return 999; // max copies reached
if (count >= 3) return 10 + (count - 2); // scaling cost after 3 copies
return 1; // base cost for first 3 copies
}
// ── function: buyCard ──
function buyCard(cardId) {
const cost = getShopCost(cardId);
if (G.hero.gold < cost || G.inventory[cardId] >= 5) return false;
G.hero.gold -= cost;
G.inventory[cardId]++;
const cardTpl = CARD_DATA[cardId];
if (cardTpl) {
G.deck.push({...cardTpl});
addLog(`Bought ${cardTpl.name}!`);
}
renderShop();
return true;
}
// ── function: leaveShop ──
function leaveShop() {
startRoom();
}
// ── main ──
// Wire up buttons and initial render
document.getElementById('btn-end-turn').addEventListener('click', () => endTurn());
document.getElementById('btn-leave-shop').addEventListener('click', leaveShop);
document.getElementById('btn-restart').addEventListener('click', () => showScreen('class-select'));
document.getElementById('btn-restart2').addEventListener('click', () => showScreen('class-select'));
renderClassSelect();
showScreen('class-select');
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"98686f68-5168-4c8e-9b77-79e384abea0a","tokensIn":3552409,"tokensOut":43937,"tokensTotal":3596346,"turns":79,"toolCalls":78,"failedToolCalls":0,"timestamp":"2026-07-18T04:46:45.716Z"} -->