Deckbuilder
longcat 2.0 · 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 45,996 bytes · SHA-256 9f7d1e551774
<!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: reset ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
body { background: #0a0a0f; color: #e0d8c8; font-family: 'Georgia', serif; }
#app { width: 100vw; height: 100vh; position: relative; }
.screen { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.hidden { display: none !important; }
/* ── css: menu ── */
#screen-menu { background: radial-gradient(ellipse at center, #1a1520 0%, #050508 100%); }
.menu-content { text-align: center; }
.game-title { font-size: 3.5rem; color: #c9a961; text-shadow: 0 0 20px #8b6914, 0 4px 8px #000; letter-spacing: 3px; margin-bottom: 8px; }
.subtitle { font-size: 1.2rem; color: #8a7e6b; margin-bottom: 30px; letter-spacing: 4px; text-transform: uppercase; }
#class-select { display: flex; gap: 20px; justify-content: center; margin-bottom: 30px; }
.class-card { width: 200px; background: linear-gradient(180deg, #2a2530 0%, #1a1520 100%); border: 2px solid #4a4050; border-radius: 10px; padding: 20px; cursor: pointer; transition: all 0.3s; }
.class-card:hover { border-color: #c9a961; transform: translateY(-5px); box-shadow: 0 8px 20px rgba(0,0,0,0.6); }
.class-card.selected { border-color: #c9a961; box-shadow: 0 0 20px #c9a961; }
.class-card h3 { font-size: 1.1rem; margin-bottom: 6px; }
.class-card p { font-size: 0.8rem; color: #8a7e6b; line-height: 1.4; }
.class-icon { font-size: 3rem; margin-bottom: 10px; }
.main-btn { background: linear-gradient(180deg, #4a3820 0%, #2a1808 100%); border: 2px solid #c9a961; color: #c9a961; padding: 12px 40px; font-size: 1.1rem; font-family: Georgia, serif; cursor: pointer; letter-spacing: 2px; text-transform: uppercase; border-radius: 6px; transition: all 0.3s; }
.main-btn:hover { background: linear-gradient(180deg, #6a5030 0%, #3a2810 100%); box-shadow: 0 0 15px rgba(201,169,97,0.4); }
.main-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* ── css: combat ── */
#screen-combat { background: radial-gradient(ellipse at center, #1a1520 0%, #050508 100%); }
#combat-hud { position: absolute; top: 0; left: 0; right: 0; height: 50px; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; background: rgba(0,0,0,0.6); border-bottom: 1px solid #333; z-index: 10; font-size: 0.9rem; }
.hud-left, .hud-right { display: flex; gap: 20px; }
#hud-gold { color: #c9a961; }
#hud-hp { color: #d44; }
#hud-energy { color: #4af; }
#enemy-area { position: absolute; top: 60px; left: 0; right: 0; height: 200px; display: flex; align-items: center; justify-content: center; }
#enemy-container { display: flex; gap: 20px; }
.enemy-slot { width: 120px; text-align: center; }
.enemy-sprite { width: 80px; height: 80px; margin: 0 auto 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 2.5rem; border: 2px solid #555; }
.enemy-hp-bar { width: 100%; height: 8px; background: #333; border-radius: 4px; overflow: hidden; margin-bottom: 4px; }
.enemy-hp-fill { height: 100%; background: linear-gradient(90deg, #d44, #f66); transition: width 0.3s; }
.enemy-name { font-size: 0.75rem; color: #aaa; }
.enemy-dmg { font-size: 0.7rem; color: #f88; }
.enemy-stunned { opacity: 0.5; filter: grayscale(0.5); }
#player-area { position: absolute; top: 270px; left: 0; right: 0; height: 80px; display: flex; align-items: center; justify-content: center; }
#player-card { display: flex; gap: 15px; align-items: center; }
.player-stat { text-align: center; padding: 8px 14px; background: rgba(0,0,0,0.5); border: 1px solid #444; border-radius: 6px; }
.player-stat .val { font-size: 1.3rem; font-weight: bold; }
.player-stat .lbl { font-size: 0.65rem; color: #888; text-transform: uppercase; }
#hand-area { position: absolute; bottom: 60px; left: 0; right: 0; height: 180px; padding: 0 20px; }
#hand-label { font-size: 0.75rem; color: #666; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px; }
#hand-cards { display: flex; gap: 10px; justify-content: center; align-items: flex-end; height: 140px; }
#controls { position: absolute; bottom: 10px; left: 0; right: 0; text-align: center; }
/* ── css: cards ── */
.play-card { width: 120px; height: 160px; border-radius: 8px; position: relative; cursor: pointer; transition: all 0.25s; overflow: hidden; flex-shrink: 0; }
.play-card:hover:not(.disabled) { transform: translateY(-15px) scale(1.05); z-index: 5; }
.play-card.disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(0.6); }
.play-card.attacking { animation: card-attack 0.4s ease; }
@keyframes card-attack { 0% { transform: translateY(0); } 50% { transform: translateY(-40px) scale(1.1); } 100% { transform: translateY(0); } }
.card-cost { position: absolute; top: 4px; left: 4px; width: 24px; height: 24px; border-radius: 50%; background: #4af; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: bold; z-index: 2; }
.card-name { position: absolute; top: 30px; left: 4px; right: 4px; text-align: center; font-size: 0.65rem; font-weight: bold; color: #fff; text-shadow: 0 1px 3px #000; z-index: 2; }
.card-icon { position: absolute; top: 55px; left: 50%; transform: translateX(-50%); font-size: 2rem; z-index: 2; }
.card-stats { position: absolute; bottom: 4px; display: flex; gap: 4px; left: 0; right: 0; justify-content: center; z-index: 2; }
.stat-badge { font-size: 0.65rem; padding: 1px 4px; border-radius: 3px; font-weight: bold; }
.stat-atk { background: #d44; color: #fff; }
.stat-def { background: #4af; color: #fff; }
.card-desc { position: absolute; bottom: 22px; left: 4px; right: 4px; text-align: center; font-size: 0.55rem; color: #ccc; line-height: 1.2; z-index: 2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-type-img { position: absolute; inset: 0; z-index: 1; }
.shop-card { width: 130px; transition: all 0.2s; cursor: pointer; }
.shop-card:hover { transform: translateY(-5px); }
.shop-card.cant-afford { opacity: 0.5; cursor: not-allowed; }
/* ── css: shop ── */
#screen-shop { background: rgba(10,5,15,0.95); z-index: 100; }
.shop-content { width: 90%; max-width: 900px; text-align: center; }
.shop-content h2 { font-size: 2rem; color: #c9a961; margin-bottom: 20px; }
#shop-cards { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; max-height: 400px; overflow-y: auto; margin-bottom: 20px; }
.shop-card .stock-info { font-size: 0.65rem; color: #888; text-align: center; margin-top: 4px; }
#screen-gameover { background: rgba(5,0,5,0.95); z-index: 200; }
.overlay-content { text-align: center; }
.overlay-content h2 { font-size: 2.5rem; color: #c9a961; margin-bottom: 20px; }
.overlay-content p { font-size: 1.1rem; margin-bottom: 20px; color: #aaa; }
#toast-container { position: fixed; top: 60px; right: 20px; z-index: 300; display: flex; flex-direction: column; gap: 6px; }
.toast { background: rgba(30,20,40,0.95); border: 1px solid #555; padding: 8px 16px; border-radius: 4px; font-size: 0.8rem; animation: toast-in 0.3s; }
@keyframes toast-in { from { opacity: 0; transform: translateX(50px); } to { opacity: 1; transform: translateX(0); } }
.damage-popup { position: absolute; font-size: 1.5rem; font-weight: bold; color: #f44; animation: dmg-pop 1s forwards; pointer-events: none; z-index: 50; text-shadow: 0 0 5px #000; }
.heal-popup { color: #4f4; }
@keyframes dmg-pop { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-40px) scale(1.3); } }
.burn-icon, .poison-icon, .stun-icon { display: inline-block; width: 16px; height: 16px; border-radius: 50%; margin: 0 2px; font-size: 10px; text-align: center; line-height: 16px; }
.burn-icon { background: #f80; }
.poison-icon { background: #8a2; }
.stun-icon { background: #cc0; }
/* ── css: shopMore ── */
.shop-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; width: 100%; }
.shop-header h2 { margin-bottom: 0; }
#shop-gold { font-size: 1.3rem; color: #c9a961; }
#shop-class-tabs { display: flex; gap: 8px; margin-bottom: 15px; flex-wrap: wrap; }
.class-tab { padding: 6px 14px; background: #2a2530; border: 1px solid #4a4050; border-radius: 4px; cursor: pointer; font-size: 0.8rem; color: #aaa; }
.class-tab.active { border-color: #c9a961; color: #c9a961; }
/* ── css: deckPile ── */
#deck-pile { display: flex; gap: 10px; margin-left: 30px; align-items: center; }
#deck-cards, #discard-pile { position: relative; width: 60px; height: 80px; }
.mini-card { position: absolute; width: 50px; height: 70px; border-radius: 6px; font-size: 0.55rem; display: flex; align-items: center; justify-content: center; }
.mini-card-back { border: 1px solid #555; }
.mini-card-back svg { width: 100%; height: 100%; border-radius: 6px; }
.pile-label { position: absolute; bottom: -14px; left: 0; right: 0; text-align: center; font-size: 0.6rem; color: #666; }
.pile-count { text-align: center; font-size: 0.65rem; color: #888; margin-top: 4px; }
</style>
</head>
<body>
<div id="app">
<div id="screen-menu" class="screen">
<div class="menu-content">
<h1 class="game-title">Shadows of Aethelgard</h1>
<p class="subtitle">Choose Your Hero</p>
<div id="class-select"></div>
<button id="btn-start" class="main-btn">Begin Descent</button>
</div>
</div>
<div id="screen-combat" class="screen hidden">
<div id="combat-hud">
<div class="hud-left">
<span id="hud-floor">Floor 1</span>
<span id="hud-gold">0 Gold</span>
</div>
<div class="hud-center">
<div id="enemy-info"></div>
</div>
<div class="hud-right">
<span id="hud-hp">HP: 30/30</span>
<span id="hud-energy">Energy: 5</span>
</div>
</div>
<div id="enemy-area">
<div id="enemy-container"></div>
</div>
<div id="player-area">
<div id="player-card"></div>
<div id="deck-pile">
<div id="deck-cards"></div>
<div id="discard-pile"></div>
</div>
</div>
<div id="hand-area">
<div id="hand-label">Your Hand</div>
<div id="hand-cards"></div>
</div>
<div id="controls">
<button id="btn-end-turn" class="main-btn">End Turn</button>
</div>
</div>
<div id="screen-shop" class="screen hidden">
<div class="shop-content">
<div class="shop-header"><h2>The Emporium</h2><div id="shop-gold"></div></div>
<div id="shop-class-tabs"></div>
<div id="shop-cards"></div>
<button id="btn-leave-shop" class="main-btn">Leave Shop</button>
</div>
</div>
<div id="screen-gameover" class="screen hidden">
<div class="overlay-content">
<h2 id="gameover-text">Game Over</h2>
<button id="btn-restart" class="main-btn">Descent Again</button>
</div>
</div>
<div id="toast-container"></div>
</div>
<script>
'use strict';
// ── function: svgCardFrame ──
function svgCardFrame(className, type) {
const colors = {
Bastion: { primary: '#4a4a5a', secondary: '#2a2a3a', accent: '#8a8aaa', label: 'Silvern' },
Wither: { primary: '#4a2a2a', secondary: '#2a1515', accent: '#aa4a2a', label: 'Ember' },
Shroud: { primary: '#2a2a3a', secondary: '#151525', accent: '#6a2aaa', label: 'Shadow' },
};
const typeAlpha = { Attack: 'A', Defense: 'D', Other: 'U' };
const c = colors[className];
return `<svg class="card-type-img" viewBox="0 0 120 160" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg_${className}_${type}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${c.primary}"/>
<stop offset="100%" stop-color="${c.secondary}"/>
</linearGradient>
<filter id="glow_${className}_${type}"><feGaussianBlur stdDeviation="2" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
</defs>
<rect width="120" height="160" rx="8" fill="url(#bg_${className}_${type})" stroke="${c.accent}" stroke-width="2"/>
<rect x="4" y="4" width="112" height="152" rx="6" fill="none" stroke="${c.accent}" stroke-width="0.5" opacity="0.4"/>
<path d="M10,20 L10,140" stroke="${c.accent}" stroke-width="0.5" opacity="0.3"/>
<path d="M110,20 L110,140" stroke="${c.accent}" stroke-width="0.5" opacity="0.3"/>
<circle cx="60" cy="80" r="30" fill="none" stroke="${c.accent}" stroke-width="1" opacity="0.5"/>
<circle cx="60" cy="80" r="25" fill="none" stroke="${c.accent}" stroke-width="0.5" opacity="0.3"/>
<text x="60" y="85" text-anchor="middle" font-size="14" fill="${c.accent}" opacity="0.6" font-family="Georgia">${typeAlpha[type] || '?'}</text>
<rect x="8" y="148" width="104" height="8" rx="2" fill="${c.accent}" opacity="0.3"/>
<text x="60" y="154" text-anchor="middle" font-size="5" fill="${c.accent}" opacity="0.6" font-family="Georgia">${c.label}</text>
</svg>`;
}
// ── function: svgCardBack ──
function svgCardBack(className) {
const colors = {
Bastion: { primary: '#3a3a4a', accent: '#aaaacc' },
Wither: { primary: '#3a1a1a', accent: '#cc4422' },
Shroud: { primary: '#1a1a2a', accent: '#8844cc' },
};
const c = colors[className];
return `<svg viewBox="0 0 120 160" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%;border-radius:8px;">
<rect width="120" height="160" rx="8" fill="${c.primary}" stroke="${c.accent}" stroke-width="2"/>
<rect x="6" y="6" width="108" height="148" rx="6" fill="none" stroke="${c.accent}" stroke-width="1" opacity="0.5"/>
<circle cx="60" cy="70" r="25" fill="none" stroke="${c.accent}" stroke-width="1.5" opacity="0.6"/>
<circle cx="60" cy="70" r="18" fill="${c.accent}" opacity="0.2"/>
<circle cx="60" cy="70" r="10" fill="${c.accent}" opacity="0.4"/>
<circle cx="60" cy="70" r="4" fill="${c.accent}" opacity="0.8"/>
<path d="M20,110 L100,110" stroke="${c.accent}" stroke-width="0.5" opacity="0.4"/>
<path d="M20,120 L100,120" stroke="${c.accent}" stroke-width="0.5" opacity="0.4"/>
<text x="60" y="130" text-anchor="middle" font-size="8" fill="${c.accent}" opacity="0.5" font-family="Georgia">${className}</text>
</svg>`;
}
// ── const: CARD_DATA ──
const CARD_DATA = {
// BASTION
'BA-A01': { id:'BA-A01', cls:'Bastion', type:'Attack', name:'Slam', cost:2, atk:3, def:0, icon:'⚔️', desc:'Deal 3 dmg. If HP>=30, draw 1.' },
'BA-A02': { id:'BA-A02', cls:'Bastion', type:'Attack', name:'Earthquake', cost:3, atk:4, def:0, icon:'🌋', desc:'Deal 4 dmg to ALL enemies.' },
'BA-A03': { id:'BA-A03', cls:'Bastion', type:'Attack', name:'Stone Fist', cost:1, atk:2, def:0, icon:'👊', desc:'Deal 2 dmg.' },
'BA-A04': { id:'BA-A04', cls:'Bastion', type:'Attack', name:'Boulder', cost:4, atk:6, def:0, icon:'🪨', desc:'Deal 6 dmg.' },
'BA-D01': { id:'BA-D01', cls:'Bastion', type:'Defense', name:'Shield Wall', cost:0, atk:0, def:4, icon:'🛡️', desc:'Gain 4 Block.' },
'BA-D02': { id:'BA-D02', cls:'Bastion', type:'Defense', name:'Iron Skin', cost:2, atk:0, def:5, icon:'🔩', desc:'Gain 5 Block. Discard 1.' },
'BA-D03': { id:'BA-D03', cls:'Bastion', type:'Defense', name:'Stand Firm', cost:1, atk:0, def:2, icon:'🧱', desc:'Gain 2 Block. Block 1 dmg.' },
'BA-D04': { id:'BA-D04', cls:'Bastion', type:'Defense', name:'Taunt', cost:2, atk:0, def:3, icon:'😤', desc:'Gain 3 Block. Stun 1 enemy.' },
'BA-U01': { id:'BA-U01', cls:'Bastion', type:'Other', name:'Search', cost:2, atk:0, def:0, icon:'🔍', desc:'Draw 1. Discard if hand>5.' },
'BA-U02': { id:'BA-U02', cls:'Bastion', type:'Other', name:'Repair', cost:3, atk:0, def:0, icon:'🔧', desc:'Heal 6 HP.' },
'BA-U03': { id:'BA-U03', cls:'Bastion', type:'Other', name:'Rally', cost:1, atk:0, def:0, icon:'📯', desc:'Draw 1. Gain 1 Energy.' },
'BA-U04': { id:'BA-U04', cls:'Bastion', type:'Other', name:'Loot', cost:2, atk:0, def:0, icon:'💰', desc:'Gain 5 Gold.' },
// WITHER
'WI-A01': { id:'WI-A01', cls:'Wither', type:'Attack', name:'Fireball', cost:2, atk:3, def:0, icon:'🔥', desc:'Deal 3 dmg. Apply 1 Burn.' },
'WI-A02': { id:'WI-A02', cls:'Wither', type:'Attack', name:'Inferno', cost:3, atk:4, def:0, icon:'💥', desc:'Deal 4 dmg. Apply 3 Burn.' },
'WI-A03': { id:'WI-A03', cls:'Wither', type:'Attack', name:'Ember', cost:1, atk:2, def:0, icon:'🕯️', desc:'Deal 2 dmg. 1 dmg/turn.' },
'WI-A04': { id:'WI-A04', cls:'Wither', type:'Attack', name:'Pyre', cost:4, atk:5, def:0, icon:'☄️', desc:'Deal 5 dmg. All take 1 next turn.' },
'WI-D01': { id:'WI-D01', cls:'Wither', type:'Defense', name:'Scorch Shield', cost:2, atk:0, def:3, icon:'🔥', desc:'Gain 3 Block. Block 1 Burn.' },
'WI-D02': { id:'WI-D02', cls:'Wither', type:'Defense', name:'Heat Resist', cost:1, atk:0, def:1, icon:'🌡️', desc:'Gain 1 Block. Gain 1 Energy.' },
'WI-D03': { id:'WI-D03', cls:'Wither', type:'Defense', name:'Smoke', cost:2, atk:0, def:4, icon:'💨', desc:'Gain 4 Block. Block 1 Burn.' },
'WI-D04': { id:'WI-D04', cls:'Wither', type:'Defense', name:'Endure', cost:3, atk:0, def:2, icon:'💪', desc:'Gain 2 Block. Heal 2 HP.' },
'WI-U01': { id:'WI-U01', cls:'Wither', type:'Other', name:'Poison', cost:2, atk:0, def:0, icon:'☠️', desc:'Apply 1 Poison to target.' },
'WI-U02': { id:'WI-U02', cls:'Wither', type:'Other', name:'Burn', cost:3, atk:0, def:0, icon:'🔥', desc:'Deal 3+2 Burn to ALL.' },
'WI-U03': { id:'WI-U03', cls:'Wither', type:'Other', name:'Slow', cost:1, atk:0, def:0, icon:'🐌', desc:'Draw 1. Stun target.' },
'WI-U04': { id:'WI-U04', cls:'Wither', type:'Other', name:'Wither', cost:2, atk:0, def:0, icon:'🥀', desc:'Heal 5. Reduce enemy Atk by 1.' },
// SHROUD
'SH-A01': { id:'SH-A01', cls:'Shroud', type:'Attack', name:'Backstab', cost:2, atk:4, def:0, icon:'🗡️', desc:'Deal 4 dmg. +1 if no armor.' },
'SH-A02': { id:'SH-A02', cls:'Shroud', type:'Attack', name:'Shadow Bolt', cost:3, atk:5, def:0, icon:'⚡', desc:'Deal 5 dmg. Cannot block.' },
'SH-A03': { id:'SH-A03', cls:'Shroud', type:'Attack', name:'Silence', cost:1, atk:3, def:0, icon:'🤫', desc:'Deal 3 dmg. Silence target.' },
'SH-A04': { id:'SH-A04', cls:'Shroud', type:'Attack', name:'Vengeance', cost:3, atk:6, def:0, icon:'⚔️', desc:'Deal 6 dmg. Gain 2 Energy.' },
'SH-D01': { id:'SH-D01', cls:'Shroud', type:'Defense', name:'Blur', cost:3, atk:0, def:2, icon:'🌫️', desc:'Gain 2 Block. Ignore next atk.' },
'SH-D02': { id:'SH-D02', cls:'Shroud', type:'Defense', name:'Step', cost:2, atk:0, def:3, icon:'👣', desc:'Gain 3 Block. Gain 1 Energy.' },
'SH-D03': { id:'SH-D03', cls:'Shroud', type:'Defense', name:'Evasion', cost:1, atk:0, def:1, icon:'💨', desc:'Gain 1 Block. Skip enemy.' },
'SH-D04': { id:'SH-D04', cls:'Shroud', type:'Defense', name:'Phantom', cost:2, atk:0, def:4, icon:'👻', desc:'Gain 4 Block. Ghost 1 turn.' },
'SH-U01': { id:'SH-U01', cls:'Shroud', type:'Other', name:'Dash', cost:1, atk:0, def:0, icon:'🏃', desc:'Gain 1 Energy. Skip enemy.' },
'SH-U02': { id:'SH-U02', cls:'Shroud', type:'Other', name:'Teleport', cost:2, atk:0, def:0, icon:'🌀', desc:'Move back. Deal 2 dmg.' },
'SH-U03': { id:'SH-U03', cls:'Shroud', type:'Other', name:'Hide', cost:2, atk:0, def:0, icon:'🫥', desc:'Gain 1 Def. Discard 1. Draw 2.' },
'SH-U04': { id:'SH-U04', cls:'Shroud', type:'Other', name:'Assassinate', cost:3, atk:0, def:0, icon:'💀', desc:'Deal 8 dmg. Unblockable.' },
};
// ── const: STARTER_DECKS ──
const STARTER_DECKS = {
Bastion: ['BA-A03','BA-A03','BA-D01','BA-D01','BA-D03','BA-A01','BA-U03','BA-D02'],
Wither: ['WI-A03','WI-A03','WI-D02','WI-D02','WI-U01','WI-A01','WI-U03','WI-D01'],
Shroud: ['SH-A03','SH-A03','SH-D03','SH-D03','SH-U01','SH-A01','SH-D02','SH-U03'],
};
// ── const: CLASS_STATS ──
const CLASS_STATS = {
Bastion: { hp: 35, energy: 3, icon: '🛡️', desc: 'High Defense, High Block. Immovable.' },
Wither: { hp: 30, energy: 5, icon: '🔥', desc: 'Burn & Poison. Low HP, High AoE.' },
Shroud: { hp: 28, energy: 5, icon: '🗡️', desc: 'Speed & Evasion. High Crit, Low Def.' },
};
// ── function: createEnemy ──
function createEnemy(floor) {
const tiers = [
{ hp: 20, dmg: 3, name: ['Goblin', 'Rat', 'Skeleton', 'Bat'], icons: ['👹','🐀','💀','🦇'] },
{ hp: 30, dmg: 5, name: ['Vampire Knight', 'Ogre', 'Wraith', 'Berserker'], icons: ['🧛','👺','👻','😈'] },
{ hp: 40, dmg: 8, name: ['Void Warrior', 'Cultist', 'Dark Mage', 'Abomination'], icon: ['👁️','🔮','🧙','🧟'] },
];
const t = tiers[Math.min(floor-1, 2)];
const i = Math.floor(Math.random() * t.name.length);
return {
name: t.name[i],
icon: t.icons[i],
hp: t.hp,
maxHp: t.hp,
dmg: t.dmg,
burn: 0,
poison: 0,
stunned: false,
debuff: false,
};
}
// ── function: createBoss ──
function createBoss(floor, room) {
const bosses = {
1: { name:'The Gatekeeper', icon:'🗿', hp:80, dmg:15, type:'tank' },
2: { name:'The Corrupted', icon:'👁️', hp:80, dmg:15, type:'burst' },
3: { name:'The Void Lord', icon:'🌑', hp:40, dmg:15, type:'regen', regen:10 },
};
const b = bosses[floor];
return { name: b.name, icon: b.icon, hp: b.hp, maxHp: b.hp, dmg: b.dmg, type: b.type, regen: b.regen||0, burn:0, poison:0, stunned:false, debuff:false };
}
// ── function: generateRoomEnemies ──
function generateRoomEnemies(floor, room) {
if (room === 3) return [createBoss(floor, room)];
const count = 1 + Math.floor(Math.random() * 2);
const enemies = [];
for (let i = 0; i < count; i++) enemies.push(createEnemy(floor));
return enemies;
}
// ── function: createGameState ──
function createGameState(heroClass) {
const stats = CLASS_STATS[heroClass];
const deck = [...STARTER_DECKS[heroClass]];
const collection = {};
deck.forEach(id => { collection[id] = (collection[id]||0) + 1; });
return {
heroClass,
maxHp: stats.hp,
hp: stats.hp,
maxEnergy: stats.energy,
energy: stats.energy,
handSize: 4,
gold: 0,
defense: 0,
floor: 1,
room: 0,
maxFloor: 3,
maxRoom: 3,
deck,
collection,
hand: [],
discard: [],
enemies: [],
turnActive: false,
selectedCard: null,
selectedEnemy: null,
usedThisTurn: [],
pyreNextTurn: 0,
ghostForm: false,
ignoreNextAttack: false,
skipEnemy: false,
};
}
// ── 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]];
}
return arr;
}
// ── function: drawCards ──
function drawCards(state, count) {
for (let i = 0; i < count; i++) {
if (state.hand.length >= state.handSize) break;
if (state.deck.length === 0) {
if (state.discard.length === 0) break;
state.deck = shuffle([...state.discard]);
state.discard = [];
}
state.hand.push(state.deck.shift());
}
}
// ── function: startCombat ──
function startCombat(state) {
state.room++;
state.enemies = generateRoomEnemies(state.floor, state.room);
state.deck = shuffle([...state.deck]);
state.discard = [];
state.hand = [];
state.defense = 0;
state.energy = state.maxEnergy;
state.turnActive = true;
state.pyreNextTurn = 0;
state.ghostForm = false;
state.ignoreNextAttack = false;
state.skipEnemy = false;
drawCards(state, state.handSize);
}
// ── function: getAliveEnemies ──
function getAliveEnemies(state) {
return state.enemies.filter(e => e.hp > 0);
}
// ── function: selectEnemyTarget ──
function selectEnemyTarget(state) {
const alive = getAliveEnemies(state);
if (alive.length === 0) return null;
return alive[0];
}
// ── function: playCard ──
function playCard(state, cardId, targetIdx) {
if (!state.turnActive) return;
const cardIndex = state.hand.findIndex(c => c === cardId);
if (cardIndex === -1) return;
const card = CARD_DATA[cardId];
if (card.cost > state.energy) return;
state.energy -= card.cost;
state.hand.splice(cardIndex, 1);
state.discard.push(cardId);
const target = targetIdx !== undefined ? state.enemies[targetIdx] : selectEnemyTarget(state);
resolveCardEffect(state, card, target);
showToast(`${card.name} played!`);
if (getAliveEnemies(state).length === 0) {
endCombatVictory(state);
return false;
}
return true;
}
// ── function: resolveCardEffect ──
function resolveCardEffect(state, card, target) {
const cid = card.id;
let dmg = card.atk || 0;
if (cid === 'BA-A01') {
if (target) dealDamage(state, target, dmg);
if (state.hp >= 30) drawCards(state, 1);
}
else if (cid === 'BA-A02') {
state.enemies.forEach(e => { if (e.hp > 0) dealDamage(state, e, dmg); });
}
else if (cid === 'BA-A03') {
if (target) dealDamage(state, target, dmg);
}
else if (cid === 'BA-A04') {
if (target) dealDamage(state, target, dmg);
}
else if (cid === 'BA-D01') {
state.defense += card.def;
}
else if (cid === 'BA-D02') {
state.defense += card.def;
if (state.hand.length > 0) {
const discarded = state.hand.shift();
state.discard.push(discarded);
showToast('Discarded a card');
}
}
else if (cid === 'BA-D03') {
state.defense += card.def;
state.defense += 1;
}
else if (cid === 'BA-D04') {
state.defense += card.def;
if (target) { target.stunned = true; showToast(`${target.name} stunned!`); }
}
else if (cid === 'BA-U01') {
drawCards(state, 1);
while (state.hand.length > 5) {
const d = state.hand.shift();
state.discard.push(d);
}
}
else if (cid === 'BA-U02') {
state.hp = Math.min(state.maxHp, state.hp + 6);
showToast('Healed 6 HP!');
}
else if (cid === 'BA-U03') {
drawCards(state, 1);
state.energy += 1;
showToast('+1 Energy');
}
else if (cid === 'BA-U04') {
state.gold += 5;
showToast('+5 Gold!');
}
else if (cid === 'WI-A01') {
if (target) { dealDamage(state, target, dmg); target.burn += 1; }
}
else if (cid === 'WI-A02') {
if (target) { dealDamage(state, target, dmg); target.burn += 3; }
}
else if (cid === 'WI-A03') {
if (target) { dealDamage(state, target, dmg); target.burn += 1; }
}
else if (cid === 'WI-A04') {
if (target) dealDamage(state, target, dmg);
state.enemies.forEach(e => { if (e.hp > 0) e.burn += 1; });
}
else if (cid === 'WI-D01') {
state.defense += card.def;
}
else if (cid === 'WI-D02') {
state.defense += card.def;
state.energy += 1;
}
else if (cid === 'WI-D03') {
state.defense += card.def;
}
else if (cid === 'WI-D04') {
state.defense += card.def;
state.hp = Math.min(state.maxHp, state.hp + 2);
showToast('Healed 2 HP');
}
else if (cid === 'WI-U01') {
if (target) { target.poison += 1; showToast(`${target.name} poisoned!`); }
}
else if (cid === 'WI-U02') {
state.enemies.forEach(e => { if (e.hp > 0) { dealDamage(state, e, 3); e.burn += 2; } });
}
else if (cid === 'WI-U03') {
drawCards(state, 1);
if (target) { target.stunned = true; showToast(`${target.name} slowed!`); }
}
else if (cid === 'WI-U04') {
state.hp = Math.min(state.maxHp, state.hp + 5);
state.enemies.forEach(e => { if (e.hp > 0) e.dmg = Math.max(0, e.dmg - 1); });
showToast('Healed 5, enemy weakened!');
}
else if (cid === 'SH-A01') {
let total = dmg;
if (target && target.defense === 0) total += 1;
if (target) dealDamage(state, target, total);
}
else if (cid === 'SH-A02') {
if (target) dealDamage(state, target, dmg, true);
}
else if (cid === 'SH-A03') {
if (target) { dealDamage(state, target, dmg); target.silenced = true; }
}
else if (cid === 'SH-A04') {
if (target) dealDamage(state, target, dmg);
state.energy += 2;
showToast('+2 Energy!');
}
else if (cid === 'SH-D01') {
state.defense += card.def;
state.ignoreNextAttack = true;
}
else if (cid === 'SH-D02') {
state.defense += card.def;
state.energy += 1;
showToast('+1 Energy');
}
else if (cid === 'SH-D03') {
state.defense += card.def;
state.skipEnemy = true;
showToast('Enemy skipped!');
}
else if (cid === 'SH-D04') {
state.defense += card.def;
state.ghostForm = true;
showToast('Ghost form!');
}
else if (cid === 'SH-U01') {
state.energy += 1;
state.skipEnemy = true;
showToast('+1 Energy, enemy skipped!');
}
else if (cid === 'SH-U02') {
if (target) dealDamage(state, target, 2);
}
else if (cid === 'SH-U03') {
state.defense += 1;
if (state.hand.length > 0) {
const discarded = state.hand.shift();
state.discard.push(discarded);
}
drawCards(state, 2);
}
else if (cid === 'SH-U04') {
if (target) dealDamage(state, target, 8, true);
}
}
// ── function: dealDamage ──
function dealDamage(state, enemy, amount, unblockable) {
let dmg = amount;
// Boss 1 tank mechanic: blocks 5 dmg until HP < 20
if (enemy.type === 'tank' && enemy.hp >= 20 && !unblockable) {
dmg = Math.max(0, dmg - 5);
}
// Ghost form absorbs
if (state.ghostForm && !unblockable) {
showToast('Ghost form absorbs attack!');
state.ghostForm = false;
return;
}
enemy.hp = Math.max(0, enemy.hp - dmg);
showDamageFloat(enemy, -dmg);
}
// ── function: endTurn ──
function endTurn(state) {
state.turnActive = false;
// Process enemy debuffs (burn/poison) at end of player turn for them
state.enemies.forEach(e => {
if (e.hp > 0) {
if (e.burn > 0) { e.hp = Math.max(0, e.hp - e.burn); e.burn = Math.max(0, e.burn - 1); showDamageFloat(e, -e.burn || 0); }
if (e.poison > 0) { e.hp = Math.max(0, e.hp - e.poison); e.poison = Math.max(0, e.poison - 1); showDamageFloat(e, -e.poison || 0); }
}
});
if (getAliveEnemies(state).length === 0) { endCombatVictory(state); return; }
// Ghost form / skip flags
if (state.skipEnemy) {
state.skipEnemy = false;
// Don't deal damage, start new turn
} else {
// Enemy attacks
state.enemies.forEach(enemy => {
if (enemy.hp <= 0) return;
if (enemy.stunned) { enemy.stunned = false; showToast(`${enemy.name} is stunned!`); return; }
let dmg = enemy.dmg;
if (enemy.debuff) dmg = Math.max(0, dmg - 1);
if (state.ignoreNextAttack) { state.ignoreNextAttack = false; showToast('Attack ignored!'); return; }
if (state.ghostForm) { state.ghostForm = false; showToast('Ghost form blocks!'); return; }
if (state.defense > 0) {
const absorbed = Math.min(state.defense, dmg);
state.defense -= absorbed;
dmg -= absorbed;
}
if (state.ghostForm) { state.ghostForm = false; return; }
state.hp -= dmg;
showToast(`${enemy.name} deals ${dmg} dmg!`);
if (state.hp <= 0) { state.hp = 0; gameOver(); return; }
});
}
// Boss regen
state.enemies.forEach(e => {
if (e.hp > 0 && e.type === 'regen' && e.regen) {
e.hp = Math.min(e.maxHp, e.hp + e.regen);
showToast(`${e.name} regenerates!`);
}
});
// Boss 1 tank mechanic
state.enemies.forEach(e => {
if (e.type === 'tank' && e.hp < 20) { e.blocks = false; }
});
// Energy recovery
state.energy = state.maxEnergy;
state.hand.forEach(c => state.discard.push(c));
state.hand = [];
state.defense = 0;
state.turnActive = true;
drawCards(state, state.handSize);
}
// ── function: endCombatVictory ──
function endCombatVictory(state) {
state.turnActive = false;
const isBoss = state.enemies.some(e => e.type);
const reward = isBoss ? 15 : (5 * state.enemies.length);
state.gold += reward;
// Add discard to deck
state.deck = shuffle([...state.deck, ...state.discard]);
state.discard = [];
showToast(`Victory! +${reward} Gold`);
setTimeout(() => {
const isBoss = state.enemies.some(e => e.type);
if (isBoss) {
if (state.floor >= state.maxFloor) {
showVictory();
} else {
state.floor++;
state.room = 0;
openShop(state);
}
} else {
openShop(state);
}
}, 800);
}
// ── function: gameOver ──
function gameOver() {
showScreen('screen-gameover');
document.getElementById('gameover-text').textContent = 'You Have Fallen';
}
// ── function: showVictory ──
function showVictory() {
showScreen('screen-gameover');
document.getElementById('gameover-text').textContent = 'Aethelgard Reclaimed!';
}
// ── function: renderCard ──
function renderCard(cardId, clickable, idx) {
const card = CARD_DATA[cardId];
const canAfford = !clickable || G.energy >= card.cost;
const cls = clickable && !canAfford ? ' disabled' : '';
return `<div class="play-card${cls}" data-card="${cardId}" data-idx="${idx}">
${svgCardFrame(card.cls, card.type)}
<div class="card-cost">${card.cost}</div>
<div class="card-name">${card.name}</div>
<div class="card-icon">${card.icon}</div>
<div class="card-desc">${card.desc}</div>
<div class="card-stats">${card.atk ? `<span class="stat-badge stat-atk">⚔️${card.atk}</span>` : ''}${card.def ? `<span class="stat-badge stat-def">🛡️${card.def}</span>` : ''}</div>
</div>`;
}
// ── function: renderHand ──
function renderHand() {
const hand = document.getElementById('hand-cards');
hand.innerHTML = G.hand.map((id, i) => renderCard(id, true, i)).join('');
hand.querySelectorAll('.play-card').forEach(el => {
el.addEventListener('click', () => {
const cid = el.dataset.card;
const card = CARD_DATA[cid];
// Auto-target for non-attack single-target cards
const needsTarget = (card.type === 'Attack' && !isAoeCard(cid));
if (needsTarget && getAliveEnemies(G).length > 1) {
G.selectedCard = cid;
showToast('Select a target');
highlightEnemies();
return;
}
playCard(G, cid, undefined);
refreshCombat();
});
});
}
// ── function: isAoeCard ──
function isAoeCard(cid) {
return ['BA-A02', 'WI-U02', 'WI-A04'].includes(cid);
}
// ── function: highlightEnemies ──
function highlightEnemies() {
document.querySelectorAll('.enemy-slot').forEach((el, i) => {
if (G.enemies[i] && G.enemies[i].hp > 0) {
el.style.boxShadow = '0 0 15px #f44';
el.style.cursor = 'pointer';
}
});
}
// ── function: renderEnemies ──
function renderEnemies() {
const container = document.getElementById('enemy-container');
container.innerHTML = G.enemies.map((e, i) => {
const hpPct = Math.max(0, (e.hp / e.maxHp) * 100);
const stunned = e.stunned ? ' enemy-stunned' : '';
const statuses = [];
if (e.burn > 0) statuses.push(`<span class="burn-icon">🔥${e.burn}</span>`);
if (e.poison > 0) statuses.push(`<span class="poison-icon">☠️${e.poison}</span>`);
if (e.stunned) statuses.push(`<span class="stun-icon">⚡</span>`);
const silhouette = e.hp > 0
? e.icon
: '<span style="opacity:0.3">💀</span>';
return `<div class="enemy-slot${stunned}" data-idx="${i}">
<div class="enemy-sprite">${silhouette}</div>
<div class="enemy-hp-bar"><div class="enemy-hp-fill" style="width:${hpPct}%"></div></div>
<div class="enemy-name">${e.name}</div>
<div class="enemy-dmg">${e.hp > 0 ? `⚔️${e.dmg}` : '☠️ Dead'}</div>
<div>${statuses.join('')}</div>
</div>`;
}).join('');
container.querySelectorAll('.enemy-slot').forEach(el => {
el.addEventListener('click', () => {
if (G.selectedCard) {
const idx = parseInt(el.dataset.idx);
playCard(G, G.selectedCard, idx);
G.selectedCard = null;
refreshCombat();
}
});
});
}
// ── function: renderHUD ──
function renderHUD() {
document.getElementById('hud-floor').textContent = `Floor ${G.floor} - Room ${G.room}`;
document.getElementById('hud-gold').textContent = `💰 ${G.gold} Gold`;
document.getElementById('hud-hp').textContent = `❤️ HP: ${G.hp}/${G.maxHp}`;
document.getElementById('hud-energy').textContent = `⚡ Energy: ${G.energy}/${G.maxEnergy}`;
}
// ── function: renderPlayerStats ──
function renderPlayerStats() {
const el = document.getElementById('player-card');
el.innerHTML = `
<div class="player-stat"><div class="val" style="color:#d44">${G.hp}</div><div class="lbl">HP</div></div>
<div class="player-stat"><div class="val" style="color:#4af">${G.energy}</div><div class="lbl">Energy</div></div>
<div class="player-stat"><div class="val" style="color:#fff">${G.defense}</div><div class="lbl">Block</div></div>
`;
// Deck pile with card backs
const deckEl = document.getElementById('deck-cards');
const backSvg = svgCardBack(G.heroClass);
const count = G.deck.length;
deckEl.innerHTML = '';
for (let i = 0; i < Math.min(count, 5); i++) {
const card = document.createElement('div');
card.className = 'mini-card mini-card-back';
card.style.top = (i * 2) + 'px';
card.style.left = (i * 2) + 'px';
card.style.zIndex = i;
card.style.background = '#111';
card.innerHTML = backSvg;
deckEl.appendChild(card);
}
const deckCountEl = document.createElement('div');
deckCountEl.className = 'pile-count';
deckCountEl.textContent = `Deck: ${count}`;
deckEl.appendChild(deckCountEl);
// Discard pile
const discEl = document.getElementById('discard-pile');
discEl.innerHTML = '';
const discCount = G.discard.length;
if (discCount > 0) {
const top = document.createElement('div');
top.className = 'mini-card mini-card-back';
top.style.background = '#111';
top.innerHTML = svgCardFrame(CARD_DATA[G.discard[discCount-1]].cls, CARD_DATA[G.discard[discCount-1]].type);
discEl.appendChild(top);
}
const discCountEl = document.createElement('div');
discCountEl.className = 'pile-count';
discCountEl.textContent = `Disc: ${discCount}`;
discEl.appendChild(discCountEl);
}
// ── function: refreshCombat ──
function refreshCombat() {
renderHUD();
renderEnemies();
renderPlayerStats();
if (G.turnActive) renderHand();
else document.getElementById('hand-cards').innerHTML = '';
}
// ── function: showToast ──
function showToast(msg) {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = msg;
container.appendChild(toast);
setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.5s'; }, 2000);
setTimeout(() => toast.remove(), 2600);
}
// ── function: showDamageFloat ──
function showDamageFloat(enemy, amount) {
const enemyEls = document.querySelectorAll('.enemy-slot');
const idx = G.enemies.indexOf(enemy);
if (idx >= 0 && enemyEls[idx]) {
const el = enemyEls[idx];
const popup = document.createElement('div');
popup.className = 'damage-popup' + (amount > 0 ? ' heal-popup' : '');
popup.textContent = amount > 0 ? `+${amount}` : amount;
popup.style.left = (el.offsetLeft + 40) + 'px';
popup.style.top = (el.offsetTop + 20) + 'px';
document.getElementById('enemy-area').appendChild(popup);
setTimeout(() => popup.remove(), 1000);
}
}
// ── function: showScreen ──
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
document.getElementById(id).classList.remove('hidden');
}
// ── function: openShop ──
function openShop(state) {
state.inShop = true;
showScreen('screen-shop');
renderShop();
}
// ── function: renderShop ──
function renderShop() {
document.getElementById('shop-gold').textContent = `💰 ${G.gold} Gold`;
const tabsEl = document.getElementById('shop-class-tabs');
const classes = ['all', 'Bastion', 'Wither', 'Shroud'];
tabsEl.innerHTML = classes.map(c =>
`<div class="class-tab ${shopTab === c ? 'active' : ''}" data-tab="${c}">${c === 'all' ? 'All Classes' : c}</div>`
).join('');
tabsEl.querySelectorAll('.class-tab').forEach(t => {
t.addEventListener('click', () => { shopTab = t.dataset.tab; renderShop(); });
});
const container = document.getElementById('shop-cards');
container.innerHTML = '';
let cards = Object.values(CARD_DATA);
if (shopTab !== 'all') cards = cards.filter(c => c.cls === shopTab);
cards.sort((a, b) => a.cost - b.cost);
cards.forEach(card => {
const owned = G.collection[card.id] || 0;
const cost = 1 + owned;
const maxed = owned >= 5;
const canAfford = G.gold >= cost;
const cls = (!canAfford || maxed) ? ' cant-afford' : '';
const el = document.createElement('div');
el.className = 'shop-card' + cls;
el.innerHTML = `
${renderCardStatic(card.id)}
<div class="stock-info">Owned: ${owned}/5 | Cost: ${maxed ? 'MAX' : cost + '💰'}</div>
`;
if (!maxed && canAfford) {
el.style.cursor = 'pointer';
el.addEventListener('click', () => {
G.gold -= cost;
G.collection[card.id] = (G.collection[card.id] || 0) + 1;
G.deck.push(card.id);
showToast(`Bought ${card.name}!`);
renderShop();
});
}
container.appendChild(el);
});
}
// ── function: renderCardStatic ──
function renderCardStatic(cardId) {
const card = CARD_DATA[cardId];
return `<div class="play-card" style="cursor:default;">
${svgCardFrame(card.cls, card.type)}
<div class="card-cost">${card.cost}</div>
<div class="card-name">${card.name}</div>
<div class="card-icon">${card.icon}</div>
<div class="card-stats">${card.atk ? `<span class="stat-badge stat-atk">⚔️${card.atk}</span>` : ''}${card.def ? `<span class="stat-badge stat-def">🛡️${card.def}</span>` : ''}</div>
</div>`;
}
// ── let: G ──
let G = null;
// ── let: selectedClass ──
let selectedClass = null;
// ── function: initMenu ──
function initMenu() {
const container = document.getElementById('class-select');
container.innerHTML = '';
Object.entries(CLASS_STATS).forEach(([name, stats]) => {
const el = document.createElement('div');
el.className = 'class-card';
el.dataset.cls = name;
el.innerHTML = `
<div class="class-icon">${stats.icon}</div>
<h3>The ${name}</h3>
<p>${stats.desc}<br>HP: ${stats.hp} | Energy: ${stats.energy}</p>
`;
el.addEventListener('click', () => {
document.querySelectorAll('.class-card').forEach(c => c.classList.remove('selected'));
el.classList.add('selected');
selectedClass = name;
});
container.appendChild(el);
});
document.getElementById('btn-start').addEventListener('click', () => {
if (!selectedClass) { showToast('Select a class first!'); return; }
startGame(selectedClass);
});
}
// ── function: startGame ──
function startGame(cls) {
G = createGameState(cls);
showScreen('screen-combat');
startCombat(G);
refreshCombat();
showToast(`Floor ${G.floor}, Room ${G.room}!`);
}
// ── function: startNextRoom ──
function startNextRoom() {
startCombat(G);
refreshCombat();
showToast(`Room ${G.room} — ${G.enemies.map(e=>e.name).join(', ')}`);
}
// ── let: shopTab ──
let shopTab = 'all';
// ── function: sleep ──
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// ── function: autoPlayTurn ──
async function autoPlayTurn(state) {
let safety = 0;
while (state.turnActive && safety < 20) {
safety++;
const playable = state.hand.filter(id => CARD_DATA[id].cost <= state.energy);
playable.sort((a, b) => (CARD_DATA[b].atk || 0) - (CARD_DATA[a].atk || 0));
if (playable.length > 0) {
const cid = playable[0];
playCard(state, cid, 0);
await sleep(100);
} else {
break;
}
}
if (state.turnActive) {
endTurn(state);
await sleep(200);
}
}
// ── function: fullGameSim ──
async function fullGameSim(heroClass) {
const results = { rooms: [], bosses: [], turns: 0, cardTypes: {} };
let state = createGameState(heroClass);
startCombat(state);
while (state.hp > 0 && state.floor <= 3 && results.turns < 100) {
results.turns++;
// Play turn
await autoPlayTurn(state);
results.rooms.push(`F${state.floor}R${state.room}`);
// Handle shop
if (state.inShop) {
state.inShop = false;
await sleep(50);
startCombat(state);
await sleep(50);
}
if (state.enemies.some(e => e.type)) {
results.bosses.push(state.enemies.find(e => e.type).name);
}
// Track card types used
state.hand.forEach(id => {
const t = CARD_DATA[id].type;
results.cardTypes[t] = (results.cardTypes[t] || 0) + 1;
});
}
return { ...results, hp: state.hp, floor: state.floor, gold: state.gold, won: state.floor > 3 };
}
// ── main ──
initMenu();
document.getElementById('btn-end-turn').addEventListener('click', () => {
if (!G || !G.turnActive) return;
endTurn(G);
refreshCombat();
});
document.getElementById('btn-leave-shop').addEventListener('click', () => {
if (!G) return;
G.inShop = false;
showScreen('screen-combat');
startNextRoom();
});
document.getElementById('btn-restart').addEventListener('click', () => {
G = null;
selectedClass = null;
initMenu();
showScreen('screen-menu');
});
</script>
</body>
</html>
<!-- agent-meta {"model":"meituan/longcat-2.0","provider":"openrouter","persona":"composer","sessionId":"055f2aa9-82fc-4622-aba1-b50c55e74eca","tokensIn":4729999,"tokensOut":51563,"tokensTotal":4781562,"turns":105,"toolCalls":104,"failedToolCalls":3,"timestamp":"2026-07-21T03:07:18.534Z"} -->