Deckbuilder
thinkingcap qwen3.6 27b · full

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 47,153 bytes · SHA-256 b275ff87f2b4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shadows of Aethelgard</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;font-family:'Segoe UI',Georgia,serif;background:#0a0a0f;color:#d4c9a8;user-select:none}
::-webkit-scrollbar{width:6px}
::-webkit-scrollbar-track{background:#1a1a2e}
::-webkit-scrollbar-thumb{background:#4a3f6b;border-radius:3px}
.screen{position:absolute;top:0;left:0;width:100%;height:100%;display:none;flex-direction:column;align-items:center;justify-content:center;z-index:1}
.screen.active{display:flex}
/* ===== PARTICLES BG ===== */
#particles{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}
/* ===== MAIN MENU ===== */
#mainMenu{background:radial-gradient(ellipse at center,#1a1a2e 0%,#0a0a0f 70%);z-index:2}
.menu-title{font-size:3.5em;color:#c9a84c;text-shadow:0 0 30px rgba(201,168,76,0.5),0 0 60px rgba(201,168,76,0.2);letter-spacing:4px;margin-bottom:8px;font-weight:bold}
.menu-subtitle{font-size:1.1em;color:#7a6f9b;margin-bottom:50px;letter-spacing:2px}
.class-select{display:flex;gap:30px;margin-top:20px}
.class-card{width:240px;padding:28px 20px;background:linear-gradient(180deg,#1e1e3a,#151528);border:2px solid #3a3a5c;border-radius:12px;cursor:pointer;text-align:center;transition:all .3s}
.class-card:hover{transform:translateY(-6px)}
.class-card.bastion:hover{border-color:#c9a84c;box-shadow:0 8px 30px rgba(201,168,76,0.4)}
.class-card.wither:hover{border-color:#e85d3a;box-shadow:0 8px 30px rgba(232,93,58,0.4)}
.class-card.shroud:hover{border-color:#7b5ea7;box-shadow:0 8px 30px rgba(123,94,167,0.4)}
.class-icon{font-size:3em;margin-bottom:10px}
.class-name{font-size:1.3em;font-weight:bold;margin-bottom:6px;color:#e8dcc8}
.class-desc{font-size:.85em;color:#8a7faa;line-height:1.4}
/* ===== GAME SCREEN ===== */
#gameScreen{background:#0d0d18;justify-content:flex-start;padding:0;z-index:2}
.hud-bar{width:100%;height:52px;background:linear-gradient(180deg,#1a1a2e,#141425);border-bottom:1px solid #2a2a4a;display:flex;align-items:center;padding:0 20px;gap:24px;z-index:10;flex-shrink:0}
.hud-item{display:flex;align-items:center;gap:6px;font-size:.95em;color:#b8ad8f}
.hud-icon{font-size:1.2em}
.hud-val{color:#e8dcc8;font-weight:bold}
.combat-area{flex:1;width:100%;display:flex;position:relative;overflow:hidden;background:radial-gradient(ellipse at 50% 60%,#1a1a2e 0%,#0d0d18 70%)}
.combat-area::before{content:'';position:absolute;top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml,%3Csvg width='60' height='60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M30 5 L35 25 L30 30 L25 25Z' fill='%23ffffff' opacity='0.01'/%3E%3C/svg%3E");pointer-events:none}
/* Enemy */
.enemy-zone{position:absolute;top:40px;left:50%;transform:translateX(-50%);text-align:center;z-index:5;transition:opacity .3s}
.enemy-sprite-wrap{width:160px;height:180px;margin:0 auto 8px;position:relative}
.enemy-name{font-size:1.1em;color:#c9a84c;margin-bottom:4px;font-weight:bold;text-shadow:0 0 10px rgba(201,168,76,.3)}
.hp-bar-container{width:220px;height:18px;background:#1a1a2e;border:1px solid #3a3a5c;border-radius:9px;overflow:hidden;margin:0 auto 4px}
.hp-bar-fill{height:100%;background:linear-gradient(90deg,#8b0000,#cc3333);transition:width .5s ease-out;border-radius:8px}
.hp-text{font-size:.8em;color:#aa9a6f}
.enemy-statuses{display:flex;gap:6px;justify-content:center;margin-top:4px;min-height:20px}
.status-badge{padding:2px 8px;border-radius:10px;font-size:.7em;font-weight:bold;text-transform:uppercase}
.status-burn{background:#5a3a00;color:#e8c84c;border:1px solid #8a6a1a}
.status-poison{background:#2a4a00;color:#8ac84c;border:1px solid #4a7a1a}
.status-stun{background:#3a005a;color:#c84ce8;border:1px solid #6a1a8a}
/* Player Info */
.player-info{position:absolute;bottom:260px;left:20px;z-index:5;display:flex;flex-direction:column;gap:6px}
.player-hp-bar{width:180px;height:14px;background:#1a1a2e;border:1px solid #3a3a5c;border-radius:7px;overflow:hidden}
.player-hp-fill{height:100%;background:linear-gradient(90deg,#2a6a2a,#4caf50);transition:width .5s ease-out;border-radius:6px}
.player-stats{font-size:.8em;color:#aa9a6f;display:flex;gap:12px}
/* Energy */
.energy-display{position:absolute;top:30px;right:20px;z-index:5;text-align:center}
.energy-orbs{display:flex;gap:4px;margin-top:4px}
.energy-orb{width:24px;height:24px;border-radius:50%;border:2px solid #4a6aaa;background:#1a2a3a;transition:all .3s}
.energy-orb.filled{background:radial-gradient(circle,#7ab8ff,#3a6acc);box-shadow:0 0 8px rgba(100,150,255,.5)}
/* Deck Panel */
.deck-panel{position:absolute;top:60px;left:20px;z-index:9;max-height:calc(100vh - 370px);overflow-y:auto;padding:8px;background:rgba(20,20,40,.75);border-radius:8px;border:1px solid #2a2a4a;width:160px}
.deck-panel-title{font-size:.8em;color:#7a6f9b;margin-bottom:6px;text-transform:uppercase;letter-spacing:1px}
/* Hand */
.hand-area{position:absolute;bottom:0;left:0;width:100%;height:250px;display:flex;align-items:flex-end;justify-content:center;padding:0 20px 16px;z-index:8;gap:4px}
/* ===== CARDS ===== */
.game-card{width:130px;height:190px;border-radius:8px;cursor:pointer;position:relative;transition:transform .2s,box-shadow .2s;flex-shrink:0}
.game-card:hover{transform:translateY(-40px) scale(1.08);z-index:20}
.game-card.disabled{opacity:.4;cursor:not-allowed}
.game-card.disabled:hover{transform:none;z-index:auto}
.card-inner{width:100%;height:100%;border-radius:8px;overflow:hidden;position:relative;background:#1a1a2e}
.card-frame-svg{position:absolute;top:0;left:0;width:100%;height:100%}
.card-image-placeholder{position:absolute;top:36px;left:14px;right:14px;height:75px;background:#12121f;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:2em;color:#3a3a5c}
.card-cost-badge{position:absolute;top:6px;left:6px;width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:bold;font-size:.95em;z-index:2}
.card-type-badge{position:absolute;top:6px;right:6px;padding:1px 7px;border-radius:4px;font-size:.6em;text-transform:uppercase;font-weight:bold;z-index:2}
.card-name-text{position:absolute;bottom:68px;left:8px;right:8px;text-align:center;font-size:.75em;font-weight:bold;color:#e8dcc8;line-height:1.2}
.card-desc-text{position:absolute;bottom:30px;left:8px;right:8px;text-align:center;font-size:.6em;color:#9a8fba;line-height:1.3}
.card-stats-text{position:absolute;bottom:14px;left:8px;right:8px;text-align:center;font-size:.7em;font-weight:bold}
.card-attack .card-cost-badge{background:#cc3333;color:#fff}
.card-defense .card-cost-badge{background:#3a6acc;color:#fff}
.card-other .card-cost-badge{background:#7b5ea7;color:#fff}
.card-attack .card-type-badge{background:rgba(204,51,51,.3);color:#e87070;border:1px solid #cc3333}
.card-defense .card-type-badge{background:rgba(58,106,204,.3);color:#7ab0ff;border:1px solid #3a6acc}
.card-other .card-type-badge{background:rgba(123,94,167,.3);color:#b89ae8;border:1px solid #7b5ea7}
/* Card glow on hover */
.game-card:hover .card-inner{box-shadow:0 0 20px rgba(201,168,76,.3)}
.card-attack:hover .card-inner{box-shadow:0 0 20px rgba(204,51,51,.4)!important}
.card-defense:hover .card-inner{box-shadow:0 0 20px rgba(58,106,204,.4)!important}
.card-other:hover .card-inner{box-shadow:0 0 20px rgba(123,94,167,.4)!important}
/* ===== BUTTONS ===== */
.btn{padding:10px 24px;background:linear-gradient(180deg,#3a3a5c,#2a2a44);border:1px solid #4a4a6c;color:#d4c9a8;border-radius:6px;cursor:pointer;font-size:.9em;font-family:inherit;transition:all .2s}
.btn:hover{background:linear-gradient(180deg,#4a4a6c,#3a3a5c);box-shadow:0 0 12px rgba(100,80,180,.3)}
.btn:active{transform:scale(.97)}
.btn-primary{background:linear-gradient(180deg,#5a4a2c,#3a2a1c);border-color:#6a5a3c}
.btn-primary:hover{background:linear-gradient(180deg,#6a5a3c,#4a3a2c)}
.end-turn-btn{position:absolute;bottom:260px;right:20px;z-index:9;padding:12px 28px;font-size:1em}
/* ===== SHOP ===== */
#shopScreen{background:#0d0d18;justify-content:flex-start;padding-top:60px;z-index:2}
.shop-title{font-size:2em;color:#c9a84c;margin-bottom:30px;text-shadow:0 0 20px rgba(201,168,76,.3)}
.shop-grid{display:flex;gap:16px;flex-wrap:wrap;justify-content:center;max-width:900px;padding:0 20px}
.shop-card-wrapper{text-align:center}
.shop-price{font-size:.85em;color:#c9a84c;margin-top:6px;font-weight:bold}
/* ===== OVERLAYS ===== */
.overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(5,5,10,.92);display:none;flex-direction:column;align-items:center;justify-content:center;z-index:100}
.overlay.active{display:flex}
.overlay-title{font-size:2.5em;margin-bottom:16px;text-shadow:0 0 30px rgba(201,168,76,.4)}
.overlay-subtitle{font-size:1.1em;color:#8a7faa;margin-bottom:30px}
/* Damage Numbers */
.damage-number{position:absolute;font-weight:bold;font-size:1.5em;pointer-events:none;z-index:50;animation:dmgFloat .8s ease-out forwards}
@keyframes dmgFloat{0%{opacity:1;transform:translateY(0) scale(1)}50%{transform:translateY(-25px) scale(1.2)}100%{opacity:0;transform:translateY(-50px) scale(.8)}}
/* Room Transition */
.room-transition{position:absolute;top:0;left:0;width:100%;height:100%;background:#0a0a0f;display:none;flex-direction:column;align-items:center;justify-content:center;z-index:50}
.room-transition.active{display:flex}
.room-text{font-size:2em;color:#c9a84c;margin-bottom:10px;text-shadow:0 0 20px rgba(201,168,76,.3)}
/* Animations */
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
.pulse{animation:pulse 1.5s infinite}
@keyframes enemyHit{0%{filter:brightness(3);transform:scale(1.08)}100%{filter:none;transform:scale(1)}}
/* Turn indicator */
.turn-indicator{position:absolute;top:60px;left:50%;transform:translateX(-50%);font-size:.9em;color:#7a6f9b;z-index:10;padding:4px 12px;background:rgba(20,20,40,.8);border-radius:4px;border:1px solid #2a2a4a}
/* Tooltip */
.card-tooltip{position:absolute;bottom:210px;left:50%;transform:translateX(-50%);background:#1a1a2e;border:1px solid #3a3a5c;padding:8px 12px;border-radius:6px;font-size:.75em;color:#b8ad8f;pointer-events:none;z-index:30;display:none;max-width:200px;text-align:center;line-height:1.4}
.game-card:hover .card-tooltip{display:block}
/* Floor progress */
.floor-progress{position:absolute;top:60px;left:50%;transform:translateX(-50%);z-index:10;display:flex;gap:8px;align-items:center}
.floor-dot{width:12px;height:12px;border-radius:50%;background:#2a2a4a;border:1px solid #3a3a5c;transition:all .3s}
.floor-dot.completed{background:#4caf50;border-color:#6a9f6a}
.floor-dot.current{background:#c9a84c;border-color:#e8c84c;box-shadow:0 0 8px rgba(201,168,76,.5)}
/* Enemy attack flash */
.enemy-attack-flash{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(204,51,51,.1);pointer-events:none;z-index:3;opacity:0;transition:opacity .1s}
</style>
</head>
<body>
<canvas id="particles"></canvas>
<!-- ===== MAIN MENU ===== -->
<div id="mainMenu" class="screen active">
<div class="menu-title">Shadows of Aethelgard</div>
<div class="menu-subtitle">Choose Your Fate</div>
<div class="class-select">
<div class="class-card bastion" onclick="startGame('bastion')">
<div class="class-icon">🛡️</div>
<div class="class-name">The Bastion</div>
<div class="class-desc">Immovable Object. High Defense, Block & AoE.<br><b style="color:#c9a84c">HP: 35 | Energy: 3</b></div>
</div>
<div class="class-card wither" onclick="startGame('wither')">
<div class="class-icon">🔥</div>
<div class="class-name">The Wither</div>
<div class="class-desc">Rot and Burn. Status Effects, Debuff Control.<br><b style="color:#e85d3a">HP: 30 | Energy: 5</b></div>
</div>
<div class="class-card shroud" onclick="startGame('shroud')">
<div class="class-icon">🗡️</div>
<div class="class-name">The Shroud</div>
<div class="class-desc">Shadow and Speed. Evasion, Assassination.<br><b style="color:#7b5ea7">HP: 30 | Energy: 5</b></div>
</div>
</div>
</div>
<!-- ===== GAME SCREEN ===== -->
<div id="gameScreen" class="screen">
<div class="hud-bar">
<div class="hud-item"><span class="hud-icon">💰</span><span class="hud-val" id="hudGold">0</span></div>
<div class="hud-item"><span class="hud-icon">🏰</span>Floor <span class="hud-val" id="hudFloor">1</span></div>
<div class="hud-item"><span class="hud-icon">⚔️</span><span class="hud-val" id="hudRoom">1/3</span></div>
<div style="flex:1"></div>
<div class="floor-progress" id="floorProgress">
<div class="floor-dot current" id="fdot1"></div>
<div class="floor-dot" id="fdot2"></div>
<div class="floor-dot" id="fdot3"></div>
</div>
<div style="width:20px"></div>
<div class="hud-item"><span class="hud-icon">❤️</span><span class="hud-val" id="hudHP">30/30</span></div>
</div>
<div class="combat-area" id="combatArea">
<div class="enemy-attack-flash" id="attackFlash"></div>
<!-- Enemy -->
<div class="enemy-zone" id="enemyZone">
<div class="enemy-sprite-wrap">
<canvas id="enemyCanvas" width="160" height="180"></canvas>
</div>
<div class="enemy-name" id="enemyName">Goblin</div>
<div class="hp-bar-container"><div class="hp-bar-fill" id="enemyHPBar" style="width:100%"></div></div>
<div class="hp-text" id="enemyHPText">20/20</div>
<div class="enemy-statuses" id="enemyStatuses"></div>
</div>
<!-- Player Info -->
<div class="player-info">
<div style="font-size:.85em;color:#c9a84c;font-weight:bold" id="playerClassName">The Bastion</div>
<div class="player-hp-bar"><div class="player-hp-fill" id="playerHPBar" style="width:100%"></div></div>
<div class="player-stats">
<span>🛡️ Block: <b id="playerBlock">0</b></span>
<span>🃏 Hand: <b id="playerHandSize">4</b></span>
</div>
</div>
<!-- Energy -->
<div class="energy-display">
<div style="font-size:.8em;color:#7ab0ff;font-weight:bold">ENERGY</div>
<div class="energy-orbs" id="energyOrbs"></div>
</div>
<!-- Deck Panel -->
<div class="deck-panel" id="deckPanel">
<div class="deck-panel-title">Deck (<span id="deckCount">0</span>)</div>
<div id="deckList"></div>
</div>
<!-- End Turn Button -->
<button class="btn btn-primary end-turn-btn" onclick="endTurn()">End Turn ⏭️</button>
<!-- Hand Area -->
<div class="hand-area" id="handArea"></div>
</div>
</div>
<!-- ===== SHOP SCREEN ===== -->
<div id="shopScreen" class="screen">
<div class="hud-bar">
<div class="hud-item"><span class="hud-icon">💰</span><span class="hud-val" id="shopGold">0</span></div>
<div class="hud-item"><span class="hud-icon">🏰</span>Floor <span class="hud-val" id="shopFloor">1</span></div>
</div>
<div style="flex:1;display:flex;flex-direction:column;align-items:center;overflow-y:auto;padding:20px;width:100%">
<div class="shop-title">⚒️ Card Shop ⚒️</div>
<div class="shop-grid" id="shopGrid"></div>
<button class="btn btn-primary" style="margin-top:30px;padding:14px 40px;font-size:1.1em" onclick="leaveShop()">Continue →</button>
</div>
</div>
<!-- ===== OVERLAYS ===== -->
<div class="room-transition" id="roomTransition">
<div class="room-text" id="roomTransText">Floor 1 - Room 1</div>
<div style="color:#7a6f9b;margin-top:8px" id="roomTransSub"></div>
</div>
<div class="overlay" id="gameOverOverlay">
<div class="overlay-title" style="color:#cc3333">Defeated</div>
<div class="overlay-subtitle">The Void consumes all...</div>
<button class="btn btn-primary" onclick="location.reload()">Return to Menu</button>
</div>
<div class="overlay" id="victoryOverlay">
<div class="overlay-title" style="color:#c9a84c">Victory!</div>
<div class="overlay-subtitle">Aethelgard is reclaimed from the Void.</div>
<button class="btn btn-primary" onclick="location.reload()">Play Again</button>
</div>
<script>
// ============================================================
// SHADOWS OF AETHELGARD - Complete Game Engine v2
// ============================================================
// ===== CARD DATABASE =====
const CARDS = {
'BA-A01':{id:'BA-A01',name:'Slam',type:'attack',cost:2,atk:3,def:0,desc:'Deals 3 damage. Draw 1 if at 30+ HP.',ability:'slam'},
'BA-A02':{id:'BA-A02',name:'Earthquake',type:'attack',cost:3,atk:4,def:0,desc:'Deals 4 damage to all enemies.',ability:'aoe'},
'BA-A03':{id:'BA-A03',name:'Stone Fist',type:'attack',cost:1,atk:2,def:0,desc:'Deals 2 damage.',ability:null},
'BA-A04':{id:'BA-A04',name:'Boulder',type:'attack',cost:4,atk:6,def:0,desc:'Deals 6 damage.',ability:null},
'BA-D01':{id:'BA-D01',name:'Shield Wall',type:'defense',cost:0,atk:0,def:4,desc:'Gain 4 Defense this turn.',ability:'turnBlock'},
'BA-D02':{id:'BA-D02',name:'Iron Skin',type:'defense',cost:2,atk:0,def:5,desc:'Gain 5 permanent Defense. Discard 1.',ability:'permDef'},
'BA-D03':{id:'BA-D03',name:'Stand Firm',type:'defense',cost:1,atk:0,def:2,desc:'Gain 2 Defense.',ability:null},
'BA-D04':{id:'BA-D04',name:'Taunt',type:'defense',cost:2,atk:0,def:3,desc:'Gain 3 Defense. Stun enemy 1 turn.',ability:'stun'},
'BA-U01':{id:'BA-U01',name:'Search',type:'other',cost:2,atk:0,def:0,desc:'Draw 1 card. Discard if >5.',ability:'drawDiscard'},
'BA-U02':{id:'BA-U02',name:'Repair',type:'other',cost:3,atk:0,def:0,desc:'Heal 6 HP.',ability:'heal6'},
'BA-U03':{id:'BA-U03',name:'Rally',type:'other',cost:1,atk:0,def:0,desc:'Draw 1 card. Gain 1 Energy.',ability:'drawEnergy'},
'BA-U04':{id:'BA-U04',name:'Loot',type:'other',cost:2,atk:0,def:0,desc:'Gain 5 Gold.',ability:'gold5'},
'WI-A01':{id:'WI-A01',name:'Fireball',type:'attack',cost:2,atk:3,def:0,desc:'Deals 3 damage. Apply 1 Burn.',ability:'burn1'},
'WI-A02':{id:'WI-A02',name:'Inferno',type:'attack',cost:3,atk:4,def:0,desc:'Deals 4 damage. Applies 3 Burn.',ability:'burn3'},
'WI-A03':{id:'WI-A03',name:'Ember',type:'attack',cost:1,atk:2,def:0,desc:'Deals 2 damage + 1 Burn/turn.',ability:'dotBurn'},
'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.',ability:'aoeNextTurn'},
'WI-D01':{id:'WI-D01',name:'Scorch Shield',type:'defense',cost:2,atk:0,def:3,desc:'Gain 3 Defense. Block Burn damage.',ability:'burnResist'},
'WI-D02':{id:'WI-D02',name:'Heat Resist',type:'defense',cost:1,atk:0,def:1,desc:'Gain 1 Defense. Gain 1 Energy.',ability:'defEnergy'},
'WI-D03':{id:'WI-D03',name:'Smoke',type:'defense',cost:2,atk:0,def:4,desc:'Gain 4 Defense. Block Burn damage.',ability:'burnResist'},
'WI-D04':{id:'WI-D04',name:'Endure',type:'defense',cost:3,atk:0,def:2,desc:'Gain 2 Defense. Heal 2 HP.',ability:'heal2'},
'WI-U01':{id:'WI-U01',name:'Poison',type:'other',cost:2,atk:0,def:0,desc:'Apply 1 Poison to target.',ability:'poison1'},
'WI-U02':{id:'WI-U02',name:'Burn Wave',type:'other',cost:3,atk:0,def:0,desc:'Deal 3 dmg + 2 Burn to all.',ability:'aoeBurn'},
'WI-U03':{id:'WI-U03',name:'Slow',type:'other',cost:1,atk:0,def:0,desc:'Draw 1 card. Stun enemy.',ability:'drawStun'},
'WI-U04':{id:'WI-U04',name:'Wither',type:'other',cost:2,atk:0,def:0,desc:'Heal 5 HP. Reduce enemy Atk by 1.',ability:'healDebuff'},
'SH-A01':{id:'SH-A01',name:'Backstab',type:'attack',cost:2,atk:4,def:0,desc:'Deals 4 dmg. +1 if target has no Armor.',ability:'backstab'},
'SH-A02':{id:'SH-A02',name:'Shadow Bolt',type:'attack',cost:3,atk:5,def:0,desc:'Deals 5 damage. Cannot be blocked.',ability:'unblockable'},
'SH-A03':{id:'SH-A03',name:'Silence',type:'attack',cost:1,atk:3,def:0,desc:'Deals 3 dmg. Target silenced.',ability:'silence'},
'SH-A04':{id:'SH-A04',name:'Vengeance',type:'attack',cost:3,atk:6,def:0,desc:'Deals 6 damage. Gain 2 Energy.',ability:'vengeance'},
'SH-D01':{id:'SH-D01',name:'Blur',type:'defense',cost:3,atk:0,def:2,desc:'Gain 2 Defense. Ignore next attack.',ability:'dodgeNext'},
'SH-D02':{id:'SH-D02',name:'Step',type:'defense',cost:2,atk:0,def:3,desc:'Gain 3 Defense. Gain 1 Energy.',ability:'defEnergy'},
'SH-D03':{id:'SH-D03',name:'Evasion',type:'defense',cost:1,atk:0,def:1,desc:'Gain 1 Defense. Skip enemy action.',ability:'skipEnemy'},
'SH-D04':{id:'SH-D04',name:'Phantom',type:'defense',cost:2,atk:0,def:4,desc:'Gain 4 Defense. Ghost form 1 turn.',ability:'ghostForm'},
'SH-U01':{id:'SH-U01',name:'Dash',type:'other',cost:1,atk:0,def:0,desc:'Gain 1 Energy. Skip enemy attack.',ability:'energySkip'},
'SH-U02':{id:'SH-U02',name:'Teleport',type:'other',cost:2,atk:0,def:0,desc:'Deal 2 damage to all enemies.',ability:'aoe2'},
'SH-U03':{id:'SH-U03',name:'Hide',type:'other',cost:2,atk:0,def:0,desc:'Gain 1 Defense. Discard 1, Draw 2.',ability:'discardDraw'},
'SH-U04':{id:'SH-U04',name:'Assassinate',type:'other',cost:3,atk:0,def:0,desc:'Deal 8 damage. Unblockable.',ability:'assassinate'}
};
const STARTER_DECKS = {
bastion:['BA-A01','BA-A02','BA-A03','BA-D01','BA-D01','BA-D03','BA-U01','BA-U02','BA-U03','BA-U04'],
wither:['WI-A01','WI-A02','WI-A03','WI-D01','WI-D02','WI-D03','WI-U01','WI-U02','WI-U03','WI-U04'],
shroud:['SH-A01','SH-A02','SH-A03','SH-D01','SH-D02','SH-D03','SH-U01','SH-U02','SH-U03','SH-U04']
};
const CLASS_INFO = {
bastion:{name:'The Bastion',maxHP:35,maxEnergy:3,handSize:4,color:'#c9a84c'},
wither:{name:'The Wither',maxHP:30,maxEnergy:5,handSize:4,color:'#e85d3a'},
shroud:{name:'The Shroud',maxHP:30,maxEnergy:5,handSize:4,color:'#7b5ea7'}
};
const ENEMIES = {
floor1:[{name:'Goblin',hp:20,dmg:3},{name:'Rat Swarm',hp:18,dmg:2},{name:'Dark Rat',hp:22,dmg:4}],
floor2:[{name:'Vampire Knight',hp:30,dmg:5},{name:'Ogre',hp:35,dmg:6},{name:'Shadow Wolf',hp:28,dmg:4}],
floor3:[{name:'Void Warrior',hp:40,dmg:8},{name:'Cultist',hp:38,dmg:7},{name:'Abyssal Guard',hp:45,dmg:9}]
};
const BOSSES = [
{name:'The Gatekeeper',hp:80,dmg:15,mechanic:'blocks5'},
{name:'The Corrupted',hp:80,dmg:15,mechanic:'energyDrain'},
{name:'The Void Lord',hp:40,maxHP:40,dmg:15,mechanic:'regen'}
];
let G = {};
function initState(classId) {
const ci = CLASS_INFO[classId];
G = {
classId, className:ci.name, maxHP:ci.maxHP, hp:ci.maxHP,
maxEnergy:ci.maxEnergy, energy:ci.maxEnergy, handSize:ci.handSize,
gold:0, floor:1, room:1, isBoss:false,
deck:[], drawPile:[], discardPile:[], hand:[],
block:0, permDef:0, dodgeNext:false, ghostForm:false, burnResist:false,
enemy:null, turnCount:0, playerTurn:true, combatActive:false,
aoeNextTurnDmg:0, skipEnemyAction:false,
deckCounts:{}
};
STARTER_DECKS[classId].forEach(cid => {
G.deck.push(cid);
G.deckCounts[cid] = (G.deckCounts[cid]||0) + 1;
});
}
// ===== SVG CARD FRAMES =====
function getCardFrameSVG(card, deckType) {
const colors = {
bastion:{frame:'#c9a84c',inner:'#2a2518',accent:'#e8c84c'},
wither:{frame:'#e85d3a',inner:'#2a1810',accent:'#ff7040'},
shroud:{frame:'#7b5ea7',inner:'#1a1828',accent:'#9a7ec8'}
};
const c = colors[deckType] || colors.bastion;
return `<svg viewBox="0 0 130 190" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="fg_${card.id}_${G.turnCount}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:${c.frame};stop-opacity:0.9"/>
<stop offset="100%" style="stop-color:${c.accent};stop-opacity:0.6"/>
</linearGradient>
</defs>
<rect x="2" y="2" width="126" height="186" rx="7" fill="${c.inner}" stroke="url(#fg_${card.id}_${G.turnCount})" stroke-width="2.5"/>
<rect x="6" y="6" width="118" height="178" rx="4" fill="none" stroke="${c.frame}" stroke-width="0.5" opacity="0.3"/>
${deckType==='bastion'?'<polygon points="65,8 72,20 84,20 74,28 78,40 65,32 52,40 56,28 46,20 58,20" fill="${c.frame}" opacity="0.15"/>':''}
${deckType==='wither'?'<circle cx="65" cy="18" r="8" fill="none" stroke="'+c.frame+'" stroke-width="1" opacity="0.3"/><circle cx="65" cy="18" r="4" fill="'+c.frame+'" opacity="0.15"/>':''}
${deckType==='shroud'?'<path d="M40,12 L65,28 L90,12" fill="none" stroke="'+c.frame+'" stroke-width="1" opacity="0.3"/><path d="M45,22 L65,34 L85,22" fill="none" stroke="'+c.frame+'" stroke-width="0.7" opacity="0.2"/>':''}
</svg>`;
}
function getCardBackSVG(deckType) {
const colors = {
bastion:{bg:'#1a1810',frame:'#c9a84c',accent:'#e8c84c'},
wither:{bg:'#1a1008',frame:'#e85d3a',accent:'#ff7040'},
shroud:{bg:'#12101a',frame:'#7b5ea7',accent:'#9a7ec8'}
};
const c = colors[deckType] || colors.bastion;
return `<svg viewBox="0 0 130 190" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="126" height="186" rx="7" fill="${c.bg}" stroke="${c.frame}" stroke-width="2.5"/>
<rect x="10" y="10" width="110" height="170" rx="4" fill="none" stroke="${c.frame}" stroke-width="1" opacity="0.3"/>
${deckType==='bastion'?`<polygon points="65,40 85,95 65,150 45,95" fill="none" stroke="${c.frame}" stroke-width="2" opacity="0.4"/><polygon points="65,55 78,95 65,135 52,95" fill="${c.frame}" opacity="0.08"/>`:''}
${deckType==='wither'?`<circle cx="65" cy="95" r="35" fill="none" stroke="${c.frame}" stroke-width="1.5" opacity="0.4"/><circle cx="65" cy="95" r="25" fill="none" stroke="${c.accent}" stroke-width="1" opacity="0.3"/><circle cx="65" cy="95" r="15" fill="${c.frame}" opacity="0.08"/>`:''}
${deckType==='shroud'?`<path d="M40,70 Q65,50 90,70 Q95,95 65,120 Q35,95 40,70Z" fill="none" stroke="${c.frame}" stroke-width="1.5" opacity="0.4"/><path d="M50,80 Q65,65 80,80 Q83,95 65,112 Q47,95 50,80Z" fill="${c.frame}" opacity="0.08"/>`:''}
<line x1="20" y1="170" x2="110" y2="170" stroke="${c.frame}" stroke-width="0.5" opacity="0.2"/>
</svg>`;
}
// ===== ENEMY RENDERING =====
function drawEnemy(canvas, enemy, isBoss) {
const ctx = canvas.getContext('2d');
const w=canvas.width, h=canvas.height;
ctx.clearRect(0,0,w,h);
// Shadow
ctx.fillStyle='rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.ellipse(w/2,h-18,45,10,0,0,Math.PI*2);
ctx.fill();
const hpRatio = enemy.hp / (enemy.maxHP||enemy.hp);
if(isBoss){
// Boss body - larger, menacing
const r=Math.floor(160*(1-hpRatio)+30), g=Math.floor(25+20*hpRatio), b=Math.floor(50+40*hpRatio);
ctx.fillStyle=`rgb(${r},${g},${b})`;
// Main body shape (low poly)
ctx.beginPath();
ctx.moveTo(w/2, 25);ctx.lineTo(w/2-38, h-30);ctx.lineTo(w/2+38, h-30);ctx.closePath();
ctx.fill();
// Armor plates
ctx.strokeStyle='#4a3a6a';ctx.lineWidth=2;
ctx.beginPath();ctx.moveTo(w/2-22,50);ctx.lineTo(w/2+22,50);ctx.stroke();
ctx.beginPath();ctx.moveTo(w/2-28,72);ctx.lineTo(w/2+28,72);ctx.stroke();
ctx.beginPath();ctx.moveTo(w/2-30,94);ctx.lineTo(w/2+30,94);ctx.stroke();
// Head
ctx.fillStyle=`rgb(${r},${g},${b})`;
ctx.beginPath();ctx.arc(w/2, 38, 18, 0, Math.PI*2);ctx.fill();
// Eyes - glowing red
ctx.fillStyle='#ff3333';ctx.shadowColor='#ff0000';ctx.shadowBlur=12;
ctx.beginPath();ctx.arc(w/2-9,35,4.5,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(w/2+9,35,4.5,0,Math.PI*2);ctx.fill();
ctx.shadowBlur=0;
// Crown/horns
ctx.strokeStyle='#c9a84c';ctx.lineWidth=2.5;
ctx.beginPath();ctx.moveTo(w/2-16,25);ctx.lineTo(w/2-22,5);ctx.stroke();
ctx.beginPath();ctx.moveTo(w/2+16,25);ctx.lineTo(w/2+22,5);ctx.stroke();
ctx.beginPath();ctx.moveTo(w/2,25);ctx.lineTo(w/2,3);ctx.stroke();
} else {
// Regular enemy - low poly style
const r=Math.floor(100*(1-hpRatio)+40), g=Math.floor(50+30*hpRatio), b=Math.floor(40+20*hpRatio);
ctx.fillStyle=`rgb(${r},${g},${b})`;
// Body
ctx.beginPath();ctx.moveTo(w/2, 40);ctx.lineTo(w/2-30, h-35);ctx.lineTo(w/2+30, h-35);ctx.closePath();
ctx.fill();
// Head
ctx.beginPath();ctx.arc(w/2, 48, 17, 0, Math.PI*2);ctx.fill();
// Eyes
ctx.fillStyle='#ffaa33';ctx.shadowColor='#ff8800';ctx.shadowBlur=6;
ctx.beginPath();ctx.arc(w/2-7,45,3.5,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(w/2+7,45,3.5,0,Math.PI*2);ctx.fill();
ctx.shadowBlur=0;
// Detail lines
ctx.strokeStyle='rgba(100,80,120,0.4)';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(w/2-16,65);ctx.lineTo(w/2+16,65);ctx.stroke();
}
// Status indicators on sprite
if(enemy.statuses){
let sy = h - 38;
if(enemy.statuses.burn > 0) { ctx.fillStyle='#e8c84c';ctx.font='bold 14px serif';ctx.fillText('🔥', w/2-16, sy); sy-=18; }
if(enemy.statuses.poison > 0) { ctx.fillStyle='#8ac84c';ctx.font='bold 14px serif';ctx.fillText('☠️', w/2+4, sy); sy-=18; }
if(enemy.statuses.stun > 0) { ctx.fillStyle='#c84ce8';ctx.font='bold 14px serif';ctx.fillText('💫', w/2-6, sy); }
}
}
// ===== DECK MANAGEMENT =====
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]];}
return arr;
}
function drawCards(count) {
while(G.hand.length < G.handSize && G.drawPile.length > 0 && count > 0) {
const cid = G.drawPile.pop();
G.hand.push(cid);
count--;
}
if(G.drawPile.length === 0 && (G.discardPile.length > 0 || G.hand.length < G.handSize)) {
G.drawPile = shuffle([...G.discardPile]);
G.discardPile = [];
drawCards(count);
}
}
function discardHand() {
G.discardPile.push(...G.hand);
G.hand = [];
}
// ===== CARD PLAY LOGIC =====
function playCard(handIndex) {
if(!G.playerTurn || !G.combatActive) return;
const cid = G.hand[handIndex];
const card = CARDS[cid];
if(!card || G.energy < card.cost) return;
G.energy -= card.cost;
G.hand.splice(handIndex, 1);
resolveCard(card);
animateCardPlay();
updateUI();
}
function resolveCard(card) {
const e = G.enemy;
if(!e || e.hp <= 0) return;
switch(card.ability) {
case null:
if(card.type==='attack') dealDamageToEnemy(card.atk);
else if(card.type==='defense') addBlock(card.def);
break;
// BASTION
case 'slam': dealDamageToEnemy(card.atk); if(G.hp >= 30) drawCards(1); break;
case 'aoe': dealDamageToEnemy(card.atk); break;
case 'turnBlock': addBlock(card.def); break;
case 'permDef': G.permDef += card.def; if(G.hand.length > 0){const d=G.hand.pop();G.discardPile.push(d);} break;
case 'stun': addBlock(card.def); e.statuses.stun = Math.max(e.statuses.stun||0, 1); break;
case 'drawDiscard': drawCards(1); if(G.hand.length > 5){const d=G.hand.pop();G.discardPile.push(d);} break;
case 'heal6': G.hp = Math.min(G.maxHP, G.hp + 6); break;
case 'drawEnergy': drawCards(1); G.energy = Math.min(G.maxEnergy, G.energy + 1); break;
case 'gold5': G.gold += 5; break;
// WITHER
case 'burn1': dealDamageToEnemy(card.atk); e.statuses.burn = Math.max(e.statuses.burn||0, 1); break;
case 'burn3': dealDamageToEnemy(card.atk); e.statuses.burn = Math.max(e.statuses.burn||0, 3); break;
case 'dotBurn': dealDamageToEnemy(card.atk); e.statuses.dotBurn = (e.statuses.dotBurn||0) + 1; break;
case 'aoeNextTurn': dealDamageToEnemy(card.atk); G.aoeNextTurnDmg += 1; break;
case 'burnResist': addBlock(card.def); G.burnResist = true; break;
case 'defEnergy': addBlock(card.def); G.energy = Math.min(G.maxEnergy, G.energy + 1); break;
case 'heal2': addBlock(card.def); G.hp = Math.min(G.maxHP, G.hp + 2); break;
case 'poison1': e.statuses.poison = Math.max(e.statuses.poison||0, 1); break;
case 'aoeBurn': dealDamageToEnemy(3); e.statuses.burn = Math.max(e.statuses.burn||0, 2); break;
case 'drawStun': drawCards(1); e.statuses.stun = Math.max(e.statuses.stun||0, 1); break;
case 'healDebuff': G.hp = Math.min(G.maxHP, G.hp + 5); e.dmgReduction = (e.dmgReduction||0) + 1; break;
// SHROUD
case 'backstab': dealDamageToEnemy(card.atk + (G.block > 0 ? 0 : 1)); break;
case 'unblockable': dealDamageToEnemy(card.atk, true); break;
case 'silence': dealDamageToEnemy(card.atk); e.silenced = true; break;
case 'vengeance': dealDamageToEnemy(card.atk); G.energy = Math.min(G.maxEnergy, G.energy + 2); break;
case 'dodgeNext': addBlock(card.def); G.dodgeNext = true; break;
case 'skipEnemy': addBlock(card.def); G.skipEnemyAction = true; break;
case 'ghostForm': addBlock(card.def); G.ghostForm = true; break;
case 'energySkip': G.energy = Math.min(G.maxEnergy, G.energy + 1); G.skipEnemyAction = true; break;
case 'aoe2': dealDamageToEnemy(2); break;
case 'discardDraw': if(G.hand.length > 0){const d=G.hand.pop();G.discardPile.push(d);} drawCards(2); break;
case 'assassinate': dealDamageToEnemy(8, true); break;
}
G.discardPile.push(card.id);
}
function dealDamageToEnemy(amount, unblockable=false) {
const e = G.enemy;
if(!e || e.hp <= 0) return;
let dmg = amount;
if(e.block && !unblockable) {
const absorbed = Math.min(e.block, dmg);
e.block -= absorbed;
dmg -= absorbed;
}
e.hp -= dmg;
showDamageNumber(dmg, '#ff4444');
flashEnemy();
}
function addBlock(amount) { G.block += amount; }
// ===== ENEMY TURN =====
function enemyTurn() {
const e = G.enemy;
if(!e || e.hp <= 0) return;
// Check stun
if(e.statuses.stun > 0) {
e.statuses.stun--;
showStatusText('Enemy Stunned!');
finishTurn();
return;
}
if(G.skipEnemyAction) {
G.skipEnemyAction = false;
showStatusText('Enemy Skipped!');
finishTurn();
return;
}
let dmg = e.dmg - (e.dmgReduction||0);
if(dmg < 1) dmg = 1;
// Boss mechanics
if(G.isBoss && G.floor === 1 && e.hp >= 20) {
e.block += 5;
}
if(G.isBoss && G.floor === 2 && G.energy < 3) {
dmg = 20;
}
// Dodge/ghost check
if(G.dodgeNext || G.ghostForm) {
showStatusText('Attack Dodged!');
G.dodgeNext = false;
finishTurn();
return;
}
// Flash screen red for enemy attack
const flashEl = document.getElementById('attackFlash');
flashEl.style.opacity='1';
setTimeout(()=>flashEl.style.opacity='0',200);
let actualDmg = dmg;
if(G.block > 0) {
const absorbed = Math.min(G.block, actualDmg);
G.block -= absorbed;
actualDmg -= absorbed;
}
G.hp -= actualDmg;
// Boss 3 regen
if(G.isBoss && G.floor === 3) {
e.hp = Math.min(80, (e.hp||40) + 10);
}
updateUI();
if(G.hp <= 0) {
G.hp = 0;
setTimeout(() => gameOver(), 500);
return;
}
finishTurn();
}
function finishTurn() {
const e = G.enemy;
if(e && e.hp > 0) {
let dotDmg = 0;
if(e.statuses.burn > 0) { dotDmg += e.statuses.burn; e.statuses.burn--; }
if(e.statuses.poison > 0) { dotDmg += e.statuses.poison; e.statuses.poison--; }
if(e.statuses.dotBurn > 0) { dotDmg += e.statuses.dotBurn; }
if(G.aoeNextTurnDmg > 0) { dotDmg += G.aoeNextTurnDmg; }
G.aoeNextTurnDmg = 0;
if(dotDmg > 0) {
e.hp -= dotDmg;
showDamageNumber(dotDmg, '#e8c84c');
}
}
if(e && e.hp <= 0) { enemyDefeated(); return; }
G.playerTurn = true;
G.block = 0;
G.energy = Math.min(G.maxEnergy, G.energy + 1);
discardHand();
drawCards(G.handSize);
updateUI();
}
function endTurn() {
if(!G.playerTurn || !G.combatActive) return;
G.playerTurn = false;
discardHand();
enemyTurn();
}
// ===== ENEMY DEFEATED =====
function enemyDefeated() {
G.combatActive = false;
const goldReward = G.isBoss ? 15 : 5;
G.gold += goldReward;
setTimeout(() => {
if(G.isBoss) {
if(G.floor >= 3) { victory(); }
else { G.floor++; G.room = 1; showRoomTransition(); }
} else {
G.room++;
if(G.room > 3) { G.isBoss = true; showRoomTransition(); }
else { openShop(); }
}
}, 800);
}
// ===== COMBAT SETUP =====
function setupCombat() {
const floorKey = 'floor' + G.floor;
if(G.isBoss) {
const bossData = BOSSES[G.floor - 1];
G.enemy = {
name: bossData.name, hp: bossData.hp, maxHP: bossData.maxHP||bossData.hp,
dmg: bossData.dmg, block:0, statuses:{burn:0,poison:0,stun:0},
dmgReduction:0, silenced:false
};
} else {
const pool = ENEMIES[floorKey];
if(!pool) return; // safety check
const data = pool[Math.floor(Math.random()*pool.length)];
G.enemy = {
name: data.name, hp: data.hp, maxHP: data.hp,
dmg: data.dmg, block:0, statuses:{burn:0,poison:0,stun:0},
dmgReduction:0, silenced:false
};
}
G.block = 0;
G.energy = G.maxEnergy;
G.playerTurn = true;
G.combatActive = true;
G.dodgeNext = false;
G.ghostForm = false;
G.burnResist = false;
G.skipEnemyAction = false;
G.aoeNextTurnDmg = 0;
G.drawPile = shuffle([...G.deck]);
G.discardPile = [];
G.hand = [];
drawCards(G.handSize);
updateUI();
}
// ===== SHOP =====
function openShop() {
document.getElementById('gameScreen').classList.remove('active');
document.getElementById('shopScreen').classList.add('active');
renderShop();
}
function leaveShop() {
document.getElementById('shopScreen').classList.remove('active');
G.isBoss = true;
showRoomTransition();
}
function renderShop() {
const grid = document.getElementById('shopGrid');
grid.innerHTML = '';
document.getElementById('shopGold').textContent = G.gold;
document.getElementById('shopFloor').textContent = G.floor;
const classPrefix = G.classId === 'bastion' ? 'BA' : G.classId === 'wither' ? 'WI' : 'SH';
const allCards = Object.values(CARDS).filter(c => c.id.startsWith(classPrefix));
const shopCards = shuffle([...allCards]).slice(0, 5);
shopCards.forEach(card => {
const count = G.deckCounts[card.id] || 0;
let cost = count >= 3 ? 10 : 1;
const wrapper = document.createElement('div');
wrapper.className = 'shop-card-wrapper';
const cardEl = createCardElement(card, G.classId);
if(count >= 5) { cardEl.classList.add('disabled'); }
else if(G.gold < cost) { cardEl.style.opacity = '0.6'; }
cardEl.onclick = () => buyCard(card.id, cost);
const priceLabel = document.createElement('div');
priceLabel.className = 'shop-price';
priceLabel.textContent = `${count}/5 owned — ${cost} Gold`;
wrapper.appendChild(cardEl);
wrapper.appendChild(priceLabel);
grid.appendChild(wrapper);
});
}
function buyCard(cardId, cost) {
if(G.gold < cost || (G.deckCounts[cardId]||0) >= 5) return;
G.gold -= cost;
G.deck.push(cardId);
G.deckCounts[cardId] = (G.deckCounts[cardId]||0) + 1;
renderShop();
}
// ===== UI RENDERING =====
function updateUI() {
document.getElementById('hudGold').textContent = G.gold;
document.getElementById('hudFloor').textContent = G.floor;
document.getElementById('hudRoom').textContent = G.isBoss ? 'BOSS' : `${G.room}/3`;
document.getElementById('hudHP').textContent = `${G.hp}/${G.maxHP}`;
const hpPct = Math.max(0, (G.hp / G.maxHP) * 100);
const phpBar = document.getElementById('playerHPBar');
phpBar.style.width = hpPct + '%';
phpBar.style.background = hpPct < 30 ? 'linear-gradient(90deg,#8b0000,#cc3333)' : 'linear-gradient(90deg,#2a6a2a,#4caf50)';
document.getElementById('playerBlock').textContent = G.block;
document.getElementById('playerHandSize').textContent = G.hand.length + '/' + G.handSize;
// Energy orbs
const orbsEl = document.getElementById('energyOrbs');
orbsEl.innerHTML = '';
for(let i=0;i<G.maxEnergy;i++){
const orb = document.createElement('div');
orb.className = 'energy-orb' + (i < G.energy ? ' filled' : '');
orbsEl.appendChild(orb);
}
// Floor progress dots
for(let f=1;f<=3;f++){
const dot=document.getElementById('fdot'+f);
dot.className='floor-dot';
if(f<G.floor)dot.classList.add('completed');
else if(f===G.floor)dot.classList.add('current');
}
// Enemy
if(G.enemy) {
document.getElementById('enemyName').textContent = G.enemy.name;
const ehpPct = Math.max(0, (G.enemy.hp / (G.enemy.maxHP||G.enemy.hp)) * 100);
document.getElementById('enemyHPBar').style.width = ehpPct + '%';
document.getElementById('enemyHPText').textContent = `${Math.max(0,G.enemy.hp)}/${G.enemy.maxHP||G.enemy.hp}`;
const statEl = document.getElementById('enemyStatuses');
statEl.innerHTML = '';
if(G.enemy.statuses.burn > 0){const b=document.createElement('span');b.className='status-badge status-burn';b.textContent=`🔥${G.enemy.statuses.burn}`;statEl.appendChild(b);}
if(G.enemy.statuses.poison > 0){const p=document.createElement('span');p.className='status-badge status-poison';p.textContent=`☠️${G.enemy.statuses.poison}`;statEl.appendChild(p);}
if(G.enemy.statuses.stun > 0){const s=document.createElement('span');s.className='status-badge status-stun';s.textContent=`💫${G.enemy.statuses.stun}`;statEl.appendChild(s);}
drawEnemy(document.getElementById('enemyCanvas'), G.enemy, G.isBoss);
}
document.getElementById('deckCount').textContent = G.deck.length;
renderDeckList();
renderHand();
}
function renderHand() {
const area = document.getElementById('handArea');
area.innerHTML = '';
G.hand.forEach((cid, i) => {
const card = CARDS[cid];
if(!card) return;
const el = createCardElement(card, G.classId);
if(G.energy < card.cost || !G.playerTurn || !G.combatActive) {
el.classList.add('disabled');
}
el.onclick = () => playCard(i);
area.appendChild(el);
});
}
function createCardElement(card, deckType) {
const el = document.createElement('div');
el.className = `game-card card-${card.type}`;
el.innerHTML = `
<div class="card-inner">
${getCardFrameSVG(card, deckType)}
<div class="card-image-placeholder">${getTypeIcon(card.type)}</div>
<div class="card-cost-badge">${card.cost}</div>
<div class="card-type-badge">${card.type}</div>
<div class="card-name-text">${card.name}</div>
<div class="card-desc-text">${card.desc}</div>
<div class="card-stats-text" style="color:${card.atk?'#e87070':'#7ab0ff'}">
${card.atk ? `⚔️${card.atk}` : ''}${card.def ? ` 🛡️${card.def}` : ''}
</div>
</div>`;
return el;
}
function getTypeIcon(type) {
if(type==='attack')return'⚔️';if(type==='defense')return'🛡️';return'✨';
}
function renderDeckList() {
const list = document.getElementById('deckList');
list.innerHTML = '';
const counts = {};
G.deck.forEach(cid => { counts[cid] = (counts[cid]||0)+1; });
Object.entries(counts).forEach(([cid, count]) => {
const card = CARDS[cid]; if(!card) return;
const row = document.createElement('div');
row.style.cssText='display:flex;align-items:center;gap:4px;padding:2px 0;font-size:.7em;color:#aa9a6f;border-bottom:1px solid #1a1a2e';
row.innerHTML=`<span style="color:${card.type==='attack'?'#e87070':card.type==='defense'?'#7ab0ff':'#b89ae8'}">${getTypeIcon(card.type)}</span>${card.name}<span style="margin-left:auto;opacity:.6">×${count}</span>`;
list.appendChild(row);
});
}
// ===== VISUAL EFFECTS =====
function showDamageNumber(amount, color) {
const area = document.getElementById('combatArea');
const dmgEl = document.createElement('div');
dmgEl.className = 'damage-number';
dmgEl.style.cssText = `left:50%;top:38%;color:${color};transform:translateX(-50%)`;
dmgEl.textContent = `-${amount}`;
area.appendChild(dmgEl);
setTimeout(() => dmgEl.remove(), 900);
}
function flashEnemy() {
const canvas = document.getElementById('enemyCanvas');
canvas.style.animation='none';
void canvas.offsetWidth; // reflow
canvas.style.animation='enemyHit .3s ease-out';
}
function showStatusText(text) {
const area = document.getElementById('combatArea');
const el = document.createElement('div');
el.style.cssText='position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:1.3em;color:#c9a84c;font-weight:bold;z-index:60;pointer-events:none;text-shadow:0 0 20px rgba(201,168,76,.5)';
el.textContent = text;
area.appendChild(el);
setTimeout(() => {el.style.transition='opacity .5s';el.style.opacity='0';setTimeout(()=>el.remove(),500)}, 800);
}
function animateCardPlay() {
const handArea = document.getElementById('handArea');
if(handArea.children.length > 0) {
const firstChild = handArea.firstElementChild;
firstChild.style.transition='transform .2s, opacity .2s';
firstChild.style.transform='scale(1.3) translateY(-60px)';
firstChild.style.opacity='0';
setTimeout(()=>firstChild.remove(), 250);
}
}
// ===== ROOM TRANSITIONS =====
function showRoomTransition() {
const overlay = document.getElementById('roomTransition');
overlay.classList.add('active');
document.getElementById('roomTransText').textContent = `Floor ${G.floor}${G.isBoss?' — Boss':' — Room '+G.room}`;
document.getElementById('roomTransSub').textContent = G.isBoss ? BOSSES[G.floor-1].name : 'Prepare for battle...';
setTimeout(() => {
overlay.classList.remove('active');
document.getElementById('gameScreen').classList.add('active');
setupCombat();
}, 1500);
}
// ===== GAME OVER / VICTORY =====
function gameOver() { G.combatActive = false; document.getElementById('gameOverOverlay').classList.add('active'); }
function victory() { G.combatActive = false; document.getElementById('victoryOverlay').classList.add('active'); }
// ===== START GAME =====
function startGame(classId) {
initState(classId);
document.getElementById('mainMenu').classList.remove('active');
document.getElementById('playerClassName').textContent = CLASS_INFO[classId].name;
G.isBoss = false;
showRoomTransition();
}
// ===== KEYBOARD SHORTCUTS =====
document.addEventListener('keydown', (e) => {
if(!G.combatActive || !G.playerTurn) return;
const num = parseInt(e.key);
if(num >= 1 && num <= G.hand.length) playCard(num - 1);
if(e.key === 'Enter' || e.key === ' ') endTurn();
});
// ===== PARTICLE BACKGROUND =====
(function initParticles(){
const canvas=document.getElementById('particles');
const ctx=canvas.getContext('2d');
let particles=[];
function resize(){canvas.width=window.innerWidth;canvas.height=window.innerHeight;}
window.addEventListener('resize',resize);resize();
for(let i=0;i<40;i++){
particles.push({x:Math.random()*canvas.width,y:Math.random()*canvas.height,
r:Math.random()*2+0.5,speedY:-Math.random()*0.3-0.1,speedX:(Math.random()-0.5)*0.2,
opacity:Math.random()*0.3+0.05});
}
function animate(){
ctx.clearRect(0,0,canvas.width,canvas.height);
particles.forEach(p=>{
p.y+=p.speedY;p.x+=p.speedX;
if(p.y<-10){p.y=canvas.height+10;p.x=Math.random()*canvas.width;}
ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);
ctx.fillStyle=`rgba(201,168,76,${p.opacity})`;ctx.fill();
});
requestAnimationFrame(animate);
}
animate();
})();
// ===== INIT =====
window.addEventListener('load', () => {
const c = document.getElementById('enemyCanvas');
const ctx = c.getContext('2d');
ctx.fillStyle='#1a1a2e';ctx.fillRect(0,0,c.width,c.height);
});
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"full","sessionId":"15dc5813-0db8-42e4-bbc4-70b7253756be","tokensIn":1922829,"tokensOut":41113,"tokensTotal":1963942,"turns":50,"toolCalls":49,"failedToolCalls":1,"timestamp":"2026-07-18T20:59:39.368Z"} -->