Deckbuilder
GLM 5.3 Flash · 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 36,581 bytes · SHA-256 ee2ff946e90d
<!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>
:root{
--gold:#c9a227; --parch:#1a1410; --ink:#e8dcc0;
--atk:#c0392b; --def:#3b6ea5; --oth:#7d4ba0;
--bg0:#0a0908; --bg1:#141018; --metal:#2a2d33;
}
*{box-sizing:border-box;margin:0;padding:0;user-select:none}
body{background:radial-gradient(ellipse at 50% -10%, #1c1626 0%, #0a0908 65%);color:var(--ink);
font-family:'Segoe UI',Georgia,serif;min-height:100vh;overflow-x:hidden}
h1,h2,h3{font-family:Georgia,serif;letter-spacing:2px}
button{font-family:inherit;cursor:pointer}
.etched{background:linear-gradient(180deg,#3a3e46,#22252b);border:1px solid #555;
color:var(--ink);border-radius:6px;box-shadow:inset 0 1px 0 #666,0 3px 6px #000;
padding:8px 18px;font-size:14px;letter-spacing:1px;transition:all .15s}
.etched:hover:not(:disabled){filter:brightness(1.25);transform:translateY(-1px)}
.etched:disabled{opacity:.4;cursor:not-allowed}
/* ===== MENU ===== */
#menu{display:flex;flex-direction:column;align-items:center;padding-top:6vh;gap:26px;min-height:100vh}
#menu h1{font-size:52px;color:var(--gold);text-shadow:0 0 24px rgba(201,162,39,.5),0 2px 0 #000}
#menu .sub{color:#8a7f6a;font-style:italic;letter-spacing:4px;margin-top:-16px}
.classes{display:flex;gap:28px;flex-wrap:wrap;justify-content:center}
.classCard{width:250px;background:linear-gradient(170deg,#1c1720,#0d0b10);border:1px solid #3a3348;
border-radius:10px;padding:20px;text-align:center;cursor:pointer;transition:all .2s;position:relative}
.classCard:hover{transform:translateY(-6px);border-color:var(--gold);box-shadow:0 10px 30px rgba(0,0,0,.8),0 0 18px rgba(201,162,39,.25)}
.classCard svg{width:110px;height:150px;filter:drop-shadow(0 4px 8px #000)}
.classCard h3{margin:10px 0 6px;font-size:20px}
.classCard p{font-size:12.5px;color:#a99f8c;line-height:1.5;font-style:italic}
.classCard .stats{margin-top:10px;font-size:12px;color:#7fb0d8}
/* ===== HUD ===== */
#game{display:none;flex-direction:column;height:100vh}
#topbar{display:flex;justify-content:space-between;align-items:center;padding:8px 18px;
background:linear-gradient(180deg,#221c2c,#120e18);border-bottom:2px solid #3a3348;box-shadow:0 3px 10px #000}
#topbar .stat{display:flex;align-items:center;gap:8px;font-size:15px}
.pill{background:#0d0b12;border:1px solid #4a4030;border-radius:20px;padding:4px 14px;white-space:nowrap}
.pill b{color:var(--gold)}
#hpwrap{width:220px}
.barOuter{height:16px;background:#0d0b12;border:1px solid #4a4030;border-radius:9px;overflow:hidden;position:relative}
.barFill{height:100%;background:linear-gradient(90deg,#7e1f1f,#c0392b);transition:width .3s}
#hpText{position:absolute;inset:0;text-align:center;font-size:11px;line-height:16px;text-shadow:0 1px 2px #000}
#main{flex:1;display:flex;min-height:0}
#left{width:190px;padding:10px;border-right:1px solid #2a2533;overflow-y:auto}
#log{font-size:11.5px;line-height:1.5;color:#b0a890}
#log div{margin-bottom:4px;padding-bottom:4px;border-bottom:1px dotted #2a2533}
#log .good{color:#8fce8f}#log .bad{color:#e07070}#log .sys{color:#c9a227}
#center{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;position:relative}
#floorBanner{position:absolute;top:8px;font-size:13px;color:#6a6152;letter-spacing:3px}
#enemies{display:flex;gap:34px;align-items:flex-end;flex-wrap:wrap;justify-content:center}
.enemy{width:150px;text-align:center;cursor:pointer;transition:transform .15s;position:relative}
.enemy .sprite{width:110px;height:110px;margin:0 auto;filter:drop-shadow(0 6px 10px #000);transition:transform .15s}
.enemy.targetable .sprite{animation:pulse 1s infinite;cursor:crosshair}
.enemy.targetable{transform:translateY(-4px)}
@keyframes pulse{0%,100%{filter:drop-shadow(0 0 4px #c0392b)}50%{filter:drop-shadow(0 0 16px #ff5030)}}
.enemy.dead{opacity:.15;pointer-events:none;filter:grayscale(1)}
.enemy .ename{font-size:13px;letter-spacing:1px;margin-top:4px}
.enemy .ehp{font-size:11px;color:#c98}
.enemy .statusRow{font-size:10px;min-height:14px}
.stag{display:inline-block;padding:1px 6px;border-radius:8px;margin:0 2px;font-size:10px}
.st-burn{background:#3d2a05;color:#e8b33a}.st-poison{background:#1e3320;color:#7fce7f}
.st-stun{background:#33305a;color:#aab0ff}.st-silence{background:#3a2033;color:#e0a0d0}
.st-block{background:#1e2c3d;color:#7fb0d8}
.enemy .eintent{font-size:11px;color:#d88;margin-top:2px}
#right{width:170px;padding:12px;border-left:1px solid #2a2533;display:flex;flex-direction:column;align-items:center;gap:14px}
#energyOrb{width:84px;height:84px;border-radius:50%;display:flex;align-items:center;justify-content:center;
font-size:24px;font-weight:bold;color:#fff;text-shadow:0 0 8px #000;
background:radial-gradient(circle at 35% 30%,#5a7fd8,#1a2a6a 70%);border:3px solid #7fa0e0;
box-shadow:0 0 22px rgba(90,127,216,.6),inset 0 0 14px #000}
#blockBadge{font-size:13px;color:#7fb0d8}
#right .small{font-size:11px;color:#8a7f6a;text-align:center}
#bottom{padding:10px 16px;background:linear-gradient(0deg,#181320,#0e0b14);border-top:2px solid #3a3348;
display:flex;align-items:center;gap:14px;min-height:190px}
#hand{display:flex;gap:10px;flex:1;overflow-x:auto;padding:6px 4px;min-height:180px;align-items:center}
#hand:empty::after{content:"— hand empty —";color:#4a4438;font-style:italic;margin:auto}
#controls{display:flex;flex-direction:column;gap:10px;width:170px}
#endTurn{font-size:16px;padding:12px}
/* ===== CARD ===== */
.card{width:132px;height:186px;flex:0 0 auto;position:relative;cursor:pointer;transition:transform .15s,box-shadow .15s;
filter:drop-shadow(0 4px 6px rgba(0,0,0,.7))}
.card:hover{transform:translateY(-14px) scale(1.05);z-index:5}
.card.playable{box-shadow:0 0 0 2px rgba(201,162,39,.0)}
.card.playable::after{content:"";position:absolute;inset:-2px;border-radius:9px;box-shadow:0 0 10px rgba(201,162,39,.55);pointer-events:none}
.card.unplayable{filter:grayscale(.7) brightness(.65)}
.card.selected{transform:translateY(-18px) scale(1.07)}
.card svg.frameSvg{position:absolute;inset:0;width:100%;height:100%}
.card .art{position:absolute;left:14px;top:14px;width:104px;height:74px;display:flex;align-items:center;justify-content:center;
font-size:9px;color:#5a5344;letter-spacing:2px;font-style:italic}
.card .cname{position:absolute;top:90px;left:0;right:0;text-align:center;font-size:11.5px;font-weight:bold;letter-spacing:.5px;color:#e8dcc0}
.card .ctext{position:absolute;top:106px;left:12px;right:12px;bottom:24px;font-size:9px;line-height:1.35;color:#b0a68e;text-align:center;overflow:hidden}
.card .ccost{position:absolute;top:4px;left:6px;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;
font-size:14px;font-weight:bold;color:#fff;background:radial-gradient(circle at 35% 30%,#5a7fd8,#1a2a6a);border:2px solid #8fb0f0;box-shadow:0 0 6px #000}
.card .cstat{position:absolute;bottom:4px;font-size:13px;font-weight:bold;width:28px;height:22px;display:flex;align-items:center;justify-content:center;border-radius:6px}
.card .catk{left:6px;background:#3a1512;color:#ff8a70;border:1px solid #7a3020}
.card .cdef{right:6px;background:#12233a;color:#8fc0ff;border:1px solid #2a5a8a}
.card.t-attack .cname{color:#ffb090}.card.t-defense .cname{color:#a8d0ff}.card.t-other .cname{color:#d8b0ff}
/* ===== SHOP / OVERLAYS ===== */
.overlay{position:fixed;inset:0;background:rgba(5,4,8,.9);display:none;align-items:center;justify-content:center;z-index:50;flex-direction:column;gap:18px}
.overlay.show{display:flex}
.overlay h2{color:var(--gold);font-size:32px;text-shadow:0 0 16px rgba(201,162,39,.4)}
#shopCards{display:flex;gap:22px;flex-wrap:wrap;justify-content:center}
.shopSlot{display:flex;flex-direction:column;align-items:center;gap:8px}
.shopSlot .price{font-size:14px;color:var(--gold)}
.shopSlot .price.cant{color:#c05050}
.bigmsg{font-size:22px;color:#c9bfa8;max-width:560px;text-align:center;line-height:1.6}
#toast{position:fixed;bottom:200px;left:50%;transform:translateX(-50%);background:#1c1720;border:1px solid var(--gold);
padding:8px 22px;border-radius:20px;font-size:13px;color:var(--gold);opacity:0;transition:opacity .3s;pointer-events:none;z-index:99}
#toast.show{opacity:1}
::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:#3a3348;border-radius:4px}
</style>
</head>
<body>
<!-- ===== SVG DEFS: card frames + backs (one per deck/class) ===== -->
<svg width="0" height="0" style="position:absolute">
<defs>
<!-- BASTION frame: stone/gothic arch, iron-gold -->
<symbol id="frame-bastion" viewBox="0 0 132 186">
<rect x="1" y="1" width="130" height="184" rx="9" fill="#1c1712" stroke="#8a6d2f" stroke-width="2"/>
<rect x="6" y="6" width="120" height="174" rx="6" fill="none" stroke="#5a4a22" stroke-width="1"/>
<path d="M10 10 h112 v78 h-112 z" fill="#241d15" stroke="#6a5528" stroke-width="1.5"/>
<path d="M10 88 h112" stroke="#8a6d2f" stroke-width="1.5"/>
<path d="M14 96 h104 M14 168 h104" stroke="#4a3c1c" stroke-width="1"/>
<circle cx="66" cy="176" r="5" fill="#8a6d2f"/>
<path d="M66 12 l6 8 h-12 z" fill="#8a6d2f"/>
<path d="M4 40 l6 -8 v14 z M128 40 l-6 -8 v14 z" fill="#8a6d2f" opacity=".7"/>
</symbol>
<!-- WITHER frame: rot/organic thorns, sickly green-gold -->
<symbol id="frame-wither" viewBox="0 0 132 186">
<rect x="1" y="1" width="130" height="184" rx="9" fill="#141710" stroke="#5a7a2f" stroke-width="2"/>
<rect x="6" y="6" width="120" height="174" rx="6" fill="none" stroke="#3a4a1c" stroke-width="1"/>
<path d="M12 12 q54 -6 108 0 v74 q-54 6 -108 0 z" fill="#181c10" stroke="#4a6a22" stroke-width="1.5"/>
<path d="M12 92 q54 8 108 0" stroke="#5a7a2f" stroke-width="1.5" fill="none"/>
<path d="M8 100 q10 14 2 30 M124 100 q-10 14 -2 30" stroke="#5a7a2f" stroke-width="1.5" fill="none"/>
<circle cx="66" cy="176" r="5" fill="#5a7a2f"/>
<path d="M30 176 q18 -10 36 0 q18 -10 36 0" stroke="#3a4a1c" fill="none" stroke-width="1.5"/>
<circle cx="20" cy="20" r="2" fill="#5a7a2f"/><circle cx="112" cy="20" r="2" fill="#5a7a2f"/>
</symbol>
<!-- SHROUD frame: sharp angular shadow blades, violet -->
<symbol id="frame-shroud" viewBox="0 0 132 186">
<rect x="1" y="1" width="130" height="184" rx="9" fill="#131019" stroke="#6a4a9a" stroke-width="2"/>
<path d="M8 8 L66 2 L124 8 L124 92 L66 98 L8 92 Z" fill="#1a1424" stroke="#4a3a6a" stroke-width="1.5"/>
<path d="M8 98 L124 98 L124 172 L66 182 L8 172 Z" fill="#151020" stroke="#4a3a6a" stroke-width="1.5"/>
<path d="M2 30 L8 34 M130 30 l-6 4 M2 150 l6 -4 M130 150 l-6 -4" stroke="#6a4a9a" stroke-width="1.5"/>
<path d="M66 104 l8 8 -8 8 -8 -8 z" fill="#6a4a9a"/>
<path d="M14 178 l6 -6 M118 178 l-6 -6" stroke="#6a4a9a" stroke-width="1.5"/>
</symbol>
<!-- CARD BACKS -->
<symbol id="back-bastion" viewBox="0 0 132 186">
<rect x="1" y="1" width="130" height="184" rx="9" fill="#181310" stroke="#8a6d2f" stroke-width="2"/>
<rect x="10" y="10" width="112" height="166" rx="5" fill="none" stroke="#5a4a22"/>
<path d="M66 30 L96 60 L66 90 L36 60 Z" fill="none" stroke="#8a6d2f" stroke-width="2"/>
<path d="M66 44 L82 60 L66 76 L50 60 Z" fill="#8a6d2f" opacity=".6"/>
<path d="M66 96 v56 M40 124 h52" stroke="#5a4a22" stroke-width="2"/>
<circle cx="66" cy="124" r="10" fill="none" stroke="#8a6d2f"/>
<text x="66" y="170" text-anchor="middle" fill="#8a6d2f" font-size="9" letter-spacing="2">AETHELGARD</text>
</symbol>
<symbol id="back-wither" viewBox="0 0 132 186">
<rect x="1" y="1" width="130" height="184" rx="9" fill="#101408" stroke="#5a7a2f" stroke-width="2"/>
<rect x="10" y="10" width="112" height="166" rx="5" fill="none" stroke="#3a4a1c"/>
<path d="M66 24 q22 26 0 52 q-22 26 0 52 q14 16 0 34" stroke="#5a7a2f" stroke-width="2" fill="none"/>
<path d="M66 50 q-16 8 -20 24 M66 50 q16 8 20 24 M66 102 q-16 8 -20 24 M66 102 q16 8 20 24" stroke="#4a6a22" fill="none"/>
<circle cx="66" cy="76" r="6" fill="#5a7a2f" opacity=".7"/>
<text x="66" y="170" text-anchor="middle" fill="#5a7a2f" font-size="9" letter-spacing="2">AETHELGARD</text>
</symbol>
<symbol id="back-shroud" viewBox="0 0 132 186">
<rect x="1" y="1" width="130" height="184" rx="9" fill="#0e0a16" stroke="#6a4a9a" stroke-width="2"/>
<rect x="10" y="10" width="112" height="166" rx="5" fill="none" stroke="#3a2a5a"/>
<path d="M66 26 L98 93 L66 160 L34 93 Z" fill="none" stroke="#6a4a9a" stroke-width="2"/>
<path d="M66 50 L84 93 L66 136 L48 93 Z" fill="#6a4a9a" opacity=".45"/>
<circle cx="66" cy="93" r="5" fill="#a88ad0"/>
<path d="M20 20 l10 10 M112 20 l-10 10 M20 166 l10 -10 M112 166 l-10 -10" stroke="#6a4a9a"/>
<text x="66" y="176" text-anchor="middle" fill="#6a4a9a" font-size="9" letter-spacing="2">AETHELGARD</text>
</symbol>
</defs>
</svg>
<!-- ===== MENU ===== -->
<div id="menu">
<h1>SHADOWS OF AETHELGARD</h1>
<div class="sub">THE VOID CONSUMES ALL — CHOOSE YOUR HERO</div>
<div class="classes" id="classPick"></div>
</div>
<!-- ===== GAME ===== -->
<div id="game">
<div id="topbar">
<div class="stat">
<span class="pill">🪙 <b id="uiGold">0</b> Gold</span>
<span class="pill">⚔ Floor <b id="uiFloor">1</b> · Room <b id="uiRoom">1</b>/4</span>
</div>
<div class="stat" id="hpwrap">
<div style="flex:1">
<div class="barOuter"><div class="barFill" id="uiHpBar"></div><div id="hpText"></div></div>
</div>
<span class="pill" id="uiClassName"></span>
</div>
</div>
<div id="main">
<div id="left"><h3 style="font-size:13px;color:#8a7f6a;margin-bottom:8px">CHRONICLE</h3><div id="log"></div></div>
<div id="center">
<div id="floorBanner"></div>
<div id="enemies"></div>
<div id="targetHint" style="font-size:13px;color:var(--gold);min-height:18px"></div>
</div>
<div id="right">
<div id="energyOrb">5/5</div>
<div class="small">ENERGY</div>
<div id="blockBadge">🛡 Block: 0</div>
<button class="etched" id="shopBtn">🛒 Shop</button>
<div class="small">Shop opens between rooms.<br>1 card/slot · dupes cost more</div>
</div>
</div>
<div id="bottom">
<div id="hand"></div>
<div id="controls">
<button class="etched" id="endTurn">END TURN ▶</button>
<button class="etched" id="cancelBtn" style="display:none">✖ Cancel Target</button>
<div class="small" id="turnHint">Play cards, then end your turn.</div>
</div>
</div>
</div>
<!-- ===== SHOP ===== -->
<div class="overlay" id="shopOv">
<h2>THE WANDERING MERCHANT</h2>
<div class="bigmsg" id="shopGold"></div>
<div id="shopCards"></div>
<button class="etched" id="shopContinue">DESCEND ▶</button>
</div>
<!-- ===== END SCREENS ===== -->
<div class="overlay" id="endOv">
<h2 id="endTitle"></h2>
<div class="bigmsg" id="endMsg"></div>
<button class="etched" id="endBtn">RETURN TO MENU</button>
</div>
<div id="toast"></div>
<script>
/* ================= DATA ================= */
const CLASSES = {
bastion:{name:'The Bastion', hp:35, energy:3, color:'#c9a227', desc:'Immovable object. High defense, block and area damage. Slow but unbreakable.',
frame:'frame-bastion', back:'back-bastion'},
wither:{name:'The Wither', hp:25, energy:5, color:'#7fb04a', desc:'Rot and burn. Affliction, damage over time and debuffs. Fragile but cruel.',
frame:'frame-wither', back:'back-wither'},
shroud:{name:'The Shroud', hp:28, energy:5, color:'#a88ad0', desc:'Shadow and speed. Assassination, evasion and energy tricks. Strike from darkness.',
frame:'frame-shroud', back:'back-shroud'}
};
// type: A attack, D defense, U other. tgt: 'one'|'all'|'none'
const CARDS = [
{id:'BA-A01',cls:'bastion',type:'A',cost:2,atk:3,def:0,name:'Slam',tgt:'one',text:'Deal 3 damage. If you have exactly 30 HP, draw 1 card.'},
{id:'BA-A02',cls:'bastion',type:'A',cost:3,atk:4,def:0,name:'Earthquake',tgt:'all',text:'Deal 4 damage to all enemies.'},
{id:'BA-A03',cls:'bastion',type:'A',cost:1,atk:2,def:0,name:'Stone Fist',tgt:'one',text:'Deal 2 damage.'},
{id:'BA-A04',cls:'bastion',type:'A',cost:4,atk:6,def:0,name:'Boulder',tgt:'one',text:'Deal 6 damage.'},
{id:'BA-D01',cls:'bastion',type:'D',cost:0,atk:0,def:4,name:'Shield Wall',tgt:null,text:'Gain 4 Defense for this turn.'},
{id:'BA-D02',cls:'bastion',type:'D',cost:2,atk:0,def:5,name:'Iron Skin',tgt:null,text:'Gain 5 Defense permanently. Discard 1 random card.'},
{id:'BA-D03',cls:'bastion',type:'D',cost:1,atk:0,def:2,name:'Stand Firm',tgt:null,text:'Gain 2 Defense.'},
{id:'BA-D04',cls:'bastion',type:'D',cost:2,atk:0,def:3,name:'Taunt',tgt:'one',text:'Gain 3 Defense. Stun 1 enemy for 1 turn.'},
{id:'BA-U01',cls:'bastion',type:'U',cost:2,atk:0,def:0,name:'Search',tgt:null,text:'Draw 1 card. Discard down to 5 if over.'},
{id:'BA-U02',cls:'bastion',type:'U',cost:3,atk:0,def:0,name:'Repair',tgt:null,text:'Heal 6 HP.'},
{id:'BA-U03',cls:'bastion',type:'U',cost:1,atk:0,def:0,name:'Rally',tgt:null,text:'Draw 1 card. Gain 1 Energy.'},
{id:'BA-U04',cls:'bastion',type:'U',cost:2,atk:0,def:0,name:'Loot',tgt:null,text:'Gain 5 Gold.'},
{id:'WI-A01',cls:'wither',type:'A',cost:2,atk:3,def:0,name:'Fireball',tgt:'one',text:'Deal 3 damage. Apply 1 Burn.'},
{id:'WI-A02',cls:'wither',type:'A',cost:3,atk:4,def:0,name:'Inferno',tgt:'one',text:'Deal 4 damage. Apply 3 Burn.'},
{id:'WI-A03',cls:'wither',type:'A',cost:1,atk:2,def:0,name:'Ember',tgt:'one',text:'Deal 2 damage. Apply 1 Burn.'},
{id:'WI-A04',cls:'wither',type:'A',cost:4,atk:5,def:0,name:'Pyre',tgt:'one',text:'Deal 5 damage. All enemies take 1 damage next turn.'},
{id:'WI-D01',cls:'wither',type:'D',cost:2,atk:0,def:3,name:'Scorch Shield',tgt:null,text:'Gain 3 Defense.'},
{id:'WI-D02',cls:'wither',type:'D',cost:1,atk:0,def:1,name:'Heat Resist',tgt:null,text:'Gain 1 Defense. Gain 1 Energy.'},
{id:'WI-D03',cls:'wither',type:'D',cost:2,atk:0,def:4,name:'Smoke',tgt:null,text:'Gain 4 Defense.'},
{id:'WI-D04',cls:'wither',type:'D',cost:3,atk:0,def:2,name:'Endure',tgt:null,text:'Gain 2 Defense. Heal 2 HP.'},
{id:'WI-U01',cls:'wither',type:'U',cost:2,atk:0,def:0,name:'Poison',tgt:'one',text:'Apply 1 Poison to target.'},
{id:'WI-U02',cls:'wither',type:'U',cost:3,atk:3,def:0,name:'Wildfire',tgt:'all',text:'Deal 3 damage and apply 2 Burn to all enemies.'},
{id:'WI-U03',cls:'wither',type:'U',cost:1,atk:0,def:0,name:'Slow',tgt:'one',text:'Draw 1 card. Stun target for 1 turn.'},
{id:'WI-U04',cls:'wither',type:'U',cost:2,atk:0,def:0,name:'Wither',tgt:'one',text:'Heal 5 HP. Reduce target attack by 1 (min 1).'},
{id:'SH-A01',cls:'shroud',type:'A',cost:2,atk:4,def:0,name:'Backstab',tgt:'one',text:'Deal 4 damage. +1 damage if target has no Block.'},
{id:'SH-A02',cls:'shroud',type:'A',cost:3,atk:5,def:0,name:'Shadow Bolt',tgt:'one',text:'Deal 5 damage. Ignores Block.'},
{id:'SH-A03',cls:'shroud',type:'A',cost:1,atk:3,def:0,name:'Silence',tgt:'one',text:'Deal 3 damage. Silence target (boss specials off) for 1 turn.'},
{id:'SH-A04',cls:'shroud',type:'A',cost:3,atk:6,def:0,name:'Vengeance',tgt:'one',text:'Deal 6 damage. Gain 2 Energy.'},
{id:'SH-D01',cls:'shroud',type:'D',cost:3,atk:0,def:2,name:'Blur',tgt:null,text:'Gain 2 Defense. Ignore the next enemy attack.'},
{id:'SH-D02',cls:'shroud',type:'D',cost:2,atk:0,def:3,name:'Shadow Step',tgt:null,text:'Gain 3 Defense. Gain 1 Energy.'},
{id:'SH-D03',cls:'shroud',type:'D',cost:1,atk:0,def:1,name:'Evasion',tgt:null,text:'Gain 1 Defense. Skip all enemy actions this turn.'},
{id:'SH-D04',cls:'shroud',type:'D',cost:2,atk:0,def:4,name:'Phantom',tgt:null,text:'Gain 4 Defense (ghost form for 1 turn).'},
{id:'SH-U01',cls:'shroud',type:'U',cost:1,atk:0,def:0,name:'Dash',tgt:null,text:'Gain 1 Energy. Skip all enemy attacks this turn.'},
{id:'SH-U02',cls:'shroud',type:'U',cost:2,atk:2,def:0,name:'Teleport Strike',tgt:'one',text:'Deal 2 damage from the back row.'},
{id:'SH-U03',cls:'shroud',type:'U',cost:2,atk:0,def:1,name:'Hide',tgt:null,text:'Gain 1 Defense. Discard 1 random card, draw 2.'},
{id:'SH-U04',cls:'shroud',type:'U',cost:3,atk:8,def:0,name:'Assassinate',tgt:'one',text:'Deal 8 damage. Cannot be blocked.'}
];
const cardById = id => CARDS.find(c=>c.id===id);
const FLOORS = {
1:{names:['Goblin Cutter','Plague Rat','Goblin Archer'], hp:20, dmg:3, boss:{name:'The Gatekeeper',hp:80,dmg:15}},
2:{names:['Vampire Knight','Ogre Brute','Vampire Knight'], hp:30, dmg:5, boss:{name:'The Corrupted',hp:80,dmg:15}},
3:{names:['Void Warrior','Void Cultist','Void Warrior'], hp:40, dmg:8, boss:{name:'The Void Lord',hp:80,dmg:15}}
};
const ENEMY_ART = {
'Goblin Cutter':'👺','Plague Rat':'🐀','Goblin Archer':'🏹',
'Vampire Knight':'🧛','Ogre Brute':'👹',
'Void Warrior':'🛡','Void Cultist':'🕯',
'The Gatekeeper':'🗿','The Corrupted':'💀','The Void Lord':'👁'
};
/* ================= STATE ================= */
let G = null;
function newGame(clsKey){
const c = CLASSES[clsKey];
G = {
cls:clsKey, hp:c.hp, maxhp:c.hp, energy:c.energy, maxEnergy:5,
handSize:4, gold:0, block:0, permBlock:0, evade:0, skipEnemy:false,
deck:[], hand:[], inventory:{}, floor:1, room:1,
enemies:[], turn:1, over:false, pendingCard:null
};
CARDS.filter(x=>x.cls===clsKey).forEach(x=>{
G.deck.push(x.id); G.inventory[x.id]=1;
});
log(`You descend into Aethelgard as ${c.name}.`,'sys');
startRoom();
}
/* ================= ROOMS / ENEMIES ================= */
function isBossRoom(){ return G.room===4; }
function startRoom(){
G.turn=1; G.hand=[]; G.block=G.permBlock; G.evade=0; G.skipEnemy=false;
G.energy = (G.floor===1&&G.room===1) ? CLASSES[G.cls].energy : Math.min(G.maxEnergy,G.energy+1);
const f = FLOORS[G.floor];
if(isBossRoom()){
G.enemies=[mkEnemy(f.boss.name,f.boss.hp,f.boss.dmg,true)];
log(`⚠ ${f.boss.name} blocks your path!`,'bad');
} else {
const n = 1+Math.floor(Math.random()*2)+ (G.room>1?1:0); // 1-3 enemies
const pool=[...f.names]; G.enemies=[];
for(let i=0;i<Math.min(n,3);i++){
const nm=pool.splice(Math.floor(Math.random()*pool.length),1)[0]||f.names[0];
G.enemies.push(mkEnemy(nm,f.hp,f.dmg,false));
}
log(`Room ${G.room}: ${G.enemies.length} enemy approach!`,'sys');
}
drawTo(G.handSize);
render();
}
function mkEnemy(name,hp,dmg,boss){
return {name,hp,maxhp:hp,dmg,block:0,boss,burn:0,poison:0,stun:0,silence:0,dotNext:0,dmgMod:0};
}
/* ================= CARDS UI ================= */
function cardHTML(c, inShop, price){
const cl=CLASSES[c.cls];
const typeGlow = c.type==='A' ? '#c0392b' : c.type==='D' ? '#3b6ea5' : '#7d4ba0';
const statHtml = (c.type==='A'?`<div class="cstat catk">⚔${c.atk}</div>`:c.type==='D'?`<div class="cstat cdef">🛡${c.def}</div>`:'');
return `<div class="card t-${c.type==='A'?'attack':c.type==='D'?'defense':'other'}" data-card="${c.id}" ${inShop?`data-shop="1"`:''}>
<svg class="frameSvg" viewBox="0 0 132 186"><use href="#${cl.frame}"/></svg>
<div class="art">✦ ART ✦</div>
<div class="cname">${c.name}</div>
<div class="ctext">${c.text}</div>
<div class="ccost">${c.cost}</div>
${statHtml}
</div>`;
}
function render(){
if(!G) return;
document.getElementById('uiGold').textContent=G.gold;
document.getElementById('uiFloor').textContent=G.floor;
document.getElementById('uiRoom').textContent=G.room;
document.getElementById('uiClassName').textContent=CLASSES[G.cls].name;
document.getElementById('uiHpBar').style.width=(100*G.hp/G.maxhp)+'%';
document.getElementById('hpText').textContent=`❤ ${G.hp} / ${G.maxhp}`;
document.getElementById('energyOrb').textContent=`${G.energy}/${G.maxEnergy}`;
document.getElementById('blockBadge').textContent=`🛡 Block: ${G.block}`+(G.permBlock?` (+${G.permBlock} perm)`:'');
document.getElementById('floorBanner').textContent =
`FLOOR ${G.floor} — ${isBossRoom()?FLOORS[G.floor].boss.name.toUpperCase():'ROOM '+G.room}`;
// enemies
const ec=document.getElementById('enemies'); ec.innerHTML='';
G.enemies.forEach((e,i)=>{
const d=document.createElement('div');
d.className='enemy'+(e.hp<=0?' dead':'')+((G.pendingCard&&e.hp>0)?' targetable':'');
d.dataset.i=i;
let st='';
if(e.burn) st+=`<span class="stag st-burn">🔥${e.burn}</span>`;
if(e.poison) st+=`<span class="stag st-poison">☠${e.poison}</span>`;
if(e.stun) st+=`<span class="stag st-stun">💫</span>`;
if(e.silence) st+=`<span class="stag st-silence">🤐</span>`;
if(e.block) st+=`<span class="stag st-block">🛡${e.block}</span>`;
let intent = e.hp>0 ? `⚔ ${e.dmg+e.dmgMod}` : '';
if(e.boss && !e.silence){
if(e.name==='The Gatekeeper'&&e.hp>=20) intent+=` 🛡5`;
if(e.name==='The Void Lord') intent+=` ✚10`;
if(e.name==='The Corrupted'&&G.energy<3) intent+=` ‼20`;
}
d.innerHTML=`<svg class="sprite" viewBox="0 0 100 100"><text x="50" y="66" text-anchor="middle" font-size="60" filter="drop-shadow(0 0 6px #a00)">${ENEMY_ART[e.name]||'👾'}</text></svg>
<div class="ename">${e.name}</div>
<div class="barOuter" style="height:10px"><div class="barFill" style="width:${100*Math.max(0,e.hp)/e.maxhp}%"></div></div>
<div class="ehp">${Math.max(0,e.hp)}/${e.maxhp} HP</div>
<div class="statusRow">${st}</div><div class="eintent">${intent}</div>`;
d.onclick=()=>{ if(G.pendingCard && e.hp>0) chooseTarget(i); };
ec.appendChild(d);
});
// hand
const hc=document.getElementById('hand'); hc.innerHTML='';
G.hand.forEach((cid,i)=>{
const c=cardById(cid);
const el=document.createElement('div');
el.innerHTML=cardHTML(c,false);
const cardEl=el.firstElementChild;
if(c.cost<=G.energy && !G.pendingCard) cardEl.classList.add('playable');
else if(c.cost>G.energy) cardEl.classList.add('unplayable');
if(G.pendingCard===i) cardEl.classList.add('selected');
cardEl.onclick=()=>clickCard(i);
hc.appendChild(cardEl);
});
document.getElementById('endTurn').disabled = !!G.pendingCard || G.over;
document.getElementById('cancelBtn').style.display = G.pendingCard!==null?'':'none';
document.getElementById('targetHint').textContent =
G.pendingCard!==null ? `Choose a target for ${cardById(G.hand[G.pendingCard]).name}` : '';
}
/* ================= PLAY CARDS ================= */
function toast(msg){
const t=document.getElementById('toast'); t.textContent=msg; t.style.opacity=1;
clearTimeout(t._h); t._h=setTimeout(()=>t.style.opacity=0,1400);
}
function log(msg,cls){
const l=document.getElementById('log');
const d=document.createElement('div'); d.textContent=msg; if(cls)d.className=cls;
l.prepend(d);
}
function clickCard(i){
if(G.over||G.pendingCard!==null) return;
const c=cardById(G.hand[i]);
if(c.cost>G.energy){ toast('Not enough Energy!'); return; }
if(c.tgt==='one'){
const alive=G.enemies.filter(e=>e.hp>0);
if(!alive.length){ toast('No targets.'); return; }
if(alive.length===1){ playCard(i, G.enemies.indexOf(alive[0])); }
else { G.pendingCard=i; render(); }
} else playCard(i,null);
}
function chooseTarget(i){ playCard(G.pendingCard,i); }
function playCard(handIdx, targetIdx){
const c=cardById(G.hand[handIdx]);
if(c.cost>G.energy) return;
G.energy-=c.cost;
G.hand.splice(handIdx,1);
G.pendingCard=null;
const tgt = targetIdx!=null ? G.enemies[targetIdx] : null;
log(`You play ${c.name}.`,'sys');
applyCard(c,tgt);
drawCheck();
checkDeaths();
render();
}
function dealDamage(e,amt,pierce){
if(!e||e.hp<=0) return;
let d=amt;
if(!pierce && e.block>0){ const b=Math.min(e.block,d); e.block-=b; d-=b; }
e.hp-=d;
log(`${e.name} takes ${amt} damage${d<amt?` (${amt-d} blocked)`:''}.`,'good');
}
function applyCard(c,tgt){
const all=()=>G.enemies.filter(e=>e.hp>0);
switch(c.id){
// ===== BASTION =====
case 'BA-A01': dealDamage(tgt,3); if(G.hp===30){drawTo(G.handSize);log('Slam! You draw a card.','good');} break;
case 'BA-A02': all().forEach(e=>dealDamage(e,4)); break;
case 'BA-A03': dealDamage(tgt,2); break;
case 'BA-A04': dealDamage(tgt,6); break;
case 'BA-D01': G.block+=4; break;
case 'BA-D02': G.permBlock+=5; G.block+=5; discardRandom(); break;
case 'BA-D03': G.block+=2; break;
case 'BA-D04': G.block+=3; if(tgt){tgt.stun+=1;log(`${tgt.name} is stunned!`,'good');} break;
case 'BA-U01': drawTo(G.handSize+1); while(G.hand.length>5)G.hand.pop(); break;
case 'BA-U02': heal(6); break;
case 'BA-U03': drawTo(G.handSize+1); G.energy=Math.min(G.maxEnergy,G.energy+1); break;
case 'BA-U04': G.gold+=5; toast('+5 Gold'); break;
// ===== WITHER =====
case 'WI-A01': dealDamage(tgt,3); tgt.burn+=1; break;
case 'WI-A02': dealDamage(tgt,4); tgt.burn+=3; break;
case 'WI-A03': dealDamage(tgt,2); tgt.burn+=1; break;
case 'WI-A04': dealDamage(tgt,5); all().forEach(e=>e.dotNext+=1); break;
case 'WI-D01': G.block+=3; break;
case 'WI-D02': G.block+=1; G.energy=Math.min(G.maxEnergy,G.energy+1); break;
case 'WI-D03': G.block+=4; break;
case 'WI-D04': G.block+=2; heal(2); break;
case 'WI-U01': tgt.poison+=1; log(`${tgt.name} is poisoned.`,'good'); break;
case 'WI-U02': all().forEach(e=>{dealDamage(e,3);e.burn+=2;}); break;
case 'WI-U03': drawTo(G.handSize+1); tgt.stun+=1; log(`${tgt.name} is stunned!`,'good'); break;
case 'WI-U04': heal(5); tgt.dmgMod=Math.max(1-tgt.dmg,tgt.dmgMod-1); log(`${tgt.name}'s attack reduced.`,'good'); break;
// ===== SHROUD =====
case 'SH-A01': dealDamage(tgt,4+(tgt.block===0?1:0)); break;
case 'SH-A02': dealDamage(tgt,5,true); break;
case 'SH-A03': dealDamage(tgt,3); tgt.silence+=1; log(`${tgt.name} is silenced.`,'good'); break;
case 'SH-A04': dealDamage(tgt,6); G.energy=Math.min(G.maxEnergy,G.energy+2); break;
case 'SH-D01': G.block+=2; G.evade+=1; break;
case 'SH-D02': G.block+=3; G.energy=Math.min(G.maxEnergy,G.energy+1); break;
case 'SH-D03': G.block+=1; G.skipEnemy=true; log('You melt into shadow — enemies skip their turn!','good'); break;
case 'SH-D04': G.block+=4; break;
case 'SH-U01': G.energy=Math.min(G.maxEnergy,G.energy+1); G.skipEnemy=true; log('You dash — enemies skip their turn!','good'); break;
case 'SH-U02': dealDamage(tgt,2); break;
case 'SH-U03': G.block+=1; discardRandom(); drawTo(G.handSize+2); break;
case 'SH-U04': dealDamage(tgt,8,true); break;
}
}
function heal(n){ G.hp=Math.min(G.maxhp,G.hp+n); log(`You heal ${n} HP.`,'good'); }
function discardRandom(){ if(G.hand.length){ const i=Math.floor(Math.random()*G.hand.length); log(`${cardById(G.hand[i]).name} is discarded.`); G.hand.splice(i,1);} }
function drawTo(n){ while(G.hand.length<n && G.deck.length){ G.hand.push(G.deck[Math.floor(Math.random()*G.deck.length)]); } }
function drawCheck(){}
/* ================= DEATHS / PROGRESSION ================= */
function checkDeaths(){
G.enemies.forEach(e=>{
if(e.hp<=0 && !e.counted){
e.counted=true;
const g=e.boss?15:5; G.gold+=g;
log(`${e.name} slain! +${g} Gold.`,'sys');
}
});
if(G.enemies.every(e=>e.hp<=0)) setTimeout(roomCleared,600);
}
function roomCleared(){
if(G.over) return;
if(isBossRoom()){
if(G.floor===3){ endGame(true); return; }
log(`Floor ${G.floor} cleared! The descent continues...`,'sys');
G.floor++; G.room=1;
openShop();
} else {
G.room++;
openShop();
}
}
function endGame(win){
G.over=true;
document.getElementById('endTitle').textContent = win?'VICTORY':'GAME OVER';
document.getElementById('endMsg').textContent = win
? 'The Void Lord falls. Light returns to Aethelgard. The shadows recede... for now.'
: `You fall on Floor ${G.floor}. The Void claims another hero.`;
document.getElementById('endOv').classList.add('show');
}
/* ================= ENEMY TURN ================= */
function endTurn(){
if(G.over||G.pendingCard!==null) return;
// status ticks on enemies
G.enemies.forEach(e=>{
if(e.hp<=0) return;
if(e.burn){ e.hp-=e.burn; log(`${e.name} burns for ${e.burn}.`,'good'); }
if(e.poison){ e.hp-=e.poison; log(`${e.name} suffers ${e.poison} poison.`,'good'); }
if(e.dotNext){ e.hp-=e.dotNext; log(`${e.name} sears for ${e.dotNext}.`,'good'); e.dotNext=0; }
});
checkDeaths(); render();
if(G.enemies.every(e=>e.hp<=0)) return;
setTimeout(enemyTurn,500);
}
function enemyTurn(){
if(G.over) return;
if(!G.skipEnemy){
G.enemies.forEach(e=>{
if(e.hp<=0) return;
// boss specials
if(e.boss && !e.silence){
if(e.name==='The Gatekeeper'&&e.hp>=20) e.block=5;
if(e.name==='The Void Lord'){ e.hp=Math.min(e.maxhp,e.hp+10); log('The Void Lord regenerates 10 HP.','bad'); }
}
if(e.stun>0){ e.stun--; log(`${e.name} is stunned and cannot act.`,'good'); return; }
if(e.silence>0) e.silence--;
let dmg=e.dmg+e.dmgMod;
if(e.boss && e.name==='The Corrupted' && G.energy<3){ dmg=20; log('The Corrupted unleashes a devastating burst!','bad'); }
// player defense
if(G.evade>0){ G.evade--; log('You evade the attack entirely!','good'); return; }
const blocked=Math.min(G.block,dmg); G.block-=blocked; const taken=dmg-blocked;
G.hp-=taken;
log(`${e.name} hits you for ${dmg}${blocked?` (${blocked} blocked)`:''}.`, taken?'bad':'good');
});
} else { G.skipEnemy=false; }
// end of round
G.block=G.permBlock;
if(G.hp<=0){ endGame(false); render(); return; }
G.turn++;
G.energy=Math.min(G.maxEnergy,G.energy+1);
drawTo(G.handSize);
render();
}
/* ================= SHOP ================= */
let shopStock=[];
function shopPrice(c){
const owned=G.inventory[c.id]||0;
return Math.min(50, c.cost*2 + owned*2 + (owned>=3?10-owned*2+4:0));
}
function openShop(){
const pool=CARDS.filter(c=>(G.inventory[c.id]||0)<5);
shopStock=[];
const shuffled=[...pool].sort(()=>Math.random()-.5);
for(let i=0;i<4 && i<shuffled.length;i++) shopStock.push(shuffled[i]);
const sc=document.getElementById('shopCards'); sc.innerHTML='';
shopStock.forEach(c=>{
const price=shopPrice(c);
const slot=document.createElement('div'); slot.className='shopSlot';
slot.innerHTML=cardHTML(c,true)+`<div class="price ${G.gold<price?'cant':''}">🪙 ${price} Gold</div>`;
const cardEl=slot.querySelector('.card');
cardEl.onclick=()=>{
if(G.gold<price){ toast('Not enough Gold!'); return; }
G.gold-=price;
G.inventory[c.id]=(G.inventory[c.id]||0)+1;
G.deck.push(c.id);
log(`Bought ${c.name} for ${price} Gold.`,'sys');
openShop(); // refresh (re-roll remaining slots per GDD simplicity)
render();
};
sc.appendChild(slot);
});
document.getElementById('shopGold').textContent=`You carry ${G.gold} Gold. Buy cards to strengthen your deck (max 5 copies each).`;
document.getElementById('shopOv').classList.add('show');
render();
}
document.getElementById('shopContinue').onclick=()=>{
document.getElementById('shopOv').classList.remove('show');
startRoom();
};
document.getElementById('shopBtn').onclick=()=>{ if(!G.over) openShop(); };
/* ================= MENU / INIT ================= */
function buildMenu(){
const cp=document.getElementById('classPick'); cp.innerHTML='';
Object.entries(CLASSES).forEach(([k,c])=>{
const d=document.createElement('div'); d.className='classCard';
d.innerHTML=`<svg viewBox="0 0 132 186"><use href="#${c.back}"/></svg>
<h3 style="color:${c.color}">${c.name}</h3><p>${c.desc}</p>
<div class="stats">❤ ${c.hp} HP · ⚡ ${c.energy} start Energy</div>
<button class="etched" style="margin-top:12px">DESCEND</button>`;
d.querySelector('button').onclick=()=>{
document.getElementById('menu').style.display='none';
document.getElementById('game').style.display='flex';
document.getElementById('log').innerHTML='';
newGame(k);
};
cp.appendChild(d);
});
}
document.getElementById('endTurn').onclick=endTurn;
document.getElementById('cancelBtn').onclick=()=>{ G.pendingCard=null; render(); };
document.getElementById('endBtn').onclick=()=>{
document.getElementById('endOv').classList.remove('show');
document.getElementById('game').style.display='none';
document.getElementById('menu').style.display='flex';
G=null;
};
buildMenu();
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"9bea930f-5b22-4dff-933d-6f81c503819d","tokensIn":181203,"tokensOut":15723,"tokensTotal":196926,"cost":0.013425389999999999,"turns":9,"toolCalls":8,"failedToolCalls":0,"timestamp":"2026-08-28T03:57:21.322Z"} -->