Deckbuilder
inkling · 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 44,099 bytes · SHA-256 810fb855e50b
<!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 — Prototype</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700;900&family=Cinzel:wght@400;600;900&family=Cormorant+Garamond:ital,wght@0,300;0,600;1,400&display=swap');
:root{
--bg:#06060a;
--surface:#0d0d14;
--surface2:#13131f;
--gold:#d4a84b;
--red:#b83528;
--red-glow:#ff5e3a44;
--blue:#3a7db8;
--blue-glow:#3a7db844;
--green:#2e8a4e;
--green-glow:#2e8a4e44;
--purple:#7a3ab8;
--purple-glow:#7a3ab844;
--text:#e6dccf;
--text-dim:#9a8e82;
--void:#1a0f2e;
--bastion:#6b5328;
--wither:#8a2e2e;
--shroud:#18283a;
}
*{box-sizing:border-box;user-select:none;-webkit-user-select:none;}
html,body{margin:0;padding:0;height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:'Cinzel',Georgia,serif;}
/* Ambient glow behind everything */
.ambient{
position:fixed;inset:0;pointer-events:none;z-index:0;
background:
radial-gradient(circle at 20% 30%, rgba(212,168,75,0.06) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(122,58,184,0.05) 0%, transparent 50%),
radial-gradient(circle at 50% 20%, rgba(58,125,184,0.04) 0%, transparent 50%);
}
/* Main layout */
#game{
position:relative;z-index:1;width:100%;height:100vh;display:flex;flex-direction:column;
background:linear-gradient(180deg,#08080c 0%,#0b0b12 60%,#060610 100%);
}
/* Header */
.header{
display:flex;align-items:center;justify-content:space-between;padding:14px 24px;
border-bottom:1px solid rgba(212,168,75,0.12);background:rgba(6,6,10,0.75);
backdrop-filter:blur(6px);box-shadow:0 2px 24px rgba(0,0,0,0.6);
}
.header .brand h1{margin:0;font-family:'Cinzel Decorative',serif;font-size:1.35rem;color:var(--gold);letter-spacing:2px;text-shadow:0 0 20px rgba(212,168,75,0.3);}
.header .brand span{font-family:'Cormorant Garamond',serif;font-style:italic;color:var(--text-dim);font-size:0.95rem;letter-spacing:1px;}
.stats-row{display:flex;gap:18px;align-items:center;}
.stat{display:flex;align-items:center;gap:7px;font-size:0.85rem;color:var(--text-dim);}
.stat .val{color:var(--text);font-weight:700;font-size:1.05rem;}
.stat-icon{font-size:1.1rem;}
/* Main stage */
.main-stage{
flex:1;display:flex;align-items:center;justify-content:center;position:relative;padding:20px 24px;
}
/* Enemy center */
.enemy-zone{
position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;z-index:2;
transition:transform 0.3s ease, opacity 0.3s ease;
}
.enemy-zone.flicker{animation:flicker 0.15s ease 2;}
@keyframes flicker{0%{filter:brightness(1)}50%{filter:brightness(1.6)}100%{filter:brightness(1)}}
.enemy-frame{
width:220px;height:220px;border-radius:50%;border:2px solid rgba(122,58,184,0.35);
box-shadow:0 0 40px rgba(122,58,184,0.15),inset 0 0 40px rgba(0,0,0,0.7);
background:radial-gradient(circle at 50% 40%,rgba(122,58,184,0.15) 0%,rgba(10,10,20,0.9) 70%);
display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;
transition:box-shadow 0.3s;
}
.enemy-frame:hover{box-shadow:0 0 60px rgba(122,58,184,0.35),inset 0 0 40px rgba(0,0,0,0.5);}
.enemy-img{
width:140px;height:140px;filter:drop-shadow(0 0 20px rgba(122,58,184,0.35));
animation:float 4s ease-in-out infinite;
}
@keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
.enemy-hp{margin-top:10px;color:var(--red);font-weight:900;font-size:1.1rem;letter-spacing:1px;}
.enemy-name{font-family:'Cinzel Decorative',serif;color:#b8a898;font-size:1.05rem;margin-top:2px;letter-spacing:2px;}
.enemy-status{margin-top:6px;display:flex;justify-content:center;gap:8px;min-height:22px;}
/* Player / Cards area */
.player-zone{
position:absolute;bottom:0;left:0;right:0;padding:0 24px 28px;display:flex;flex-direction:column;align-items:center;z-index:3;
}
.hand{display:flex;gap:14px;justify-content:center;align-items:flex-end;min-height:200px;perspective:900px;}
/* Card base styles */
.card{
width:150px;height:210px;border-radius:14px;position:relative;cursor:pointer;
transition:transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
box-shadow:0 8px 24px rgba(0,0,0,0.7),inset 0 1px 0 rgba(255,255,255,0.05);
overflow:hidden;font-size:0.85rem;
}
.card:hover:not(.disabled){transform:translateY(-22px) rotate(-1deg);box-shadow:0 16px 40px rgba(0,0,0,0.8),0 0 0 2px rgba(212,168,75,0.25);}
.card.played{transform:scale(0.9) translateY(-40px) rotate(0);opacity:0.45;pointer-events:none;}
.card.disabled{filter:brightness(0.45) grayscale(0.8);cursor:not-allowed;}
/* Card frame SVG */
.card-frame{
position:absolute;inset:0;z-index:1;pointer-events:none;
drop-shadow:0 0 6px rgba(0,0,0,0.8);
}
/* Card content */
.card-inner{
position:relative;z-index:2;height:100%;padding:10px 8px;display:flex;flex-direction:column;gap:6px;
background:linear-gradient(180deg,#14121f 0%,#0c0b14 100%);
border-radius:14px;overflow:hidden;
}
/* Card type colors */
.card.type-attack .card-inner{border-top:3px solid var(--red);box-shadow:inset 0 0 30px rgba(184,53,40,0.12);}
.card.type-defend .card-inner{border-top:3px solid var(--blue);box-shadow:inset 0 0 30px rgba(58,125,184,0.12);}
.card.type-other .card-inner{border-top:3px solid var(--purple);box-shadow:inset 0 0 30px rgba(122,58,184,0.12);}
.card-header{display:flex;justify-content:space-between;align-items:center;padding:0 2px;}
.card-cost{
background:linear-gradient(135deg,#2a2418,#3a2e1a);border:1px solid rgba(212,168,75,0.25);
color:var(--gold);font-weight:900;font-size:0.95rem;width:28px;height:28px;border-radius:50%;
display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,0.6);
}
.card-id{color:rgba(255,255,255,0.2);font-family:'Courier New',monospace;font-size:0.65rem;letter-spacing:0.5px;}
.card-title{
font-family:'Cinzel Decorative',serif;font-size:0.95rem;color:#e8dcc8;text-align:center;
margin-top:4px;letter-spacing:1px;text-shadow:0 2px 8px rgba(0,0,0,0.8);
}
.card-desc{
font-family:'Cormorant Garamond',serif;font-size:0.82rem;color:#b5a898;line-height:1.35;text-align:center;flex:1;}
.card-stats{
display:flex;justify-content:center;gap:10px;align-items:center;
font-size:0.75rem;color:var(--text-dim);font-weight:600;
}
/* Card back */
.card-back{
position:absolute;inset:0;background:#0a0810;border-radius:14px;
display:flex;align-items:center;justify-content:center;box-shadow:inset 0 0 30px rgba(0,0,0,0.8);
border:2px solid rgba(212,168,75,0.2);
}
.card-back svg{width:90px;height:120px;filter:drop-shadow(0 0 10px rgba(212,168,75,0.15));}
/* Action area (center bottom) */
.action-bar{
display:flex;align-items:center;justify-content:center;gap:20px;margin-top:20px;
}
.btn{
background:linear-gradient(180deg,#2a2418,#1a160e);border:1px solid rgba(212,168,75,0.25);
color:var(--gold);font-family:'Cinzel',serif;font-weight:700;padding:10px 28px;border-radius:8px;cursor:pointer;
letter-spacing:2px;box-shadow:0 4px 12px rgba(0,0,0,0.6),inset 0 1px 0 rgba(255,255,255,0.05);
transition:all 0.15s ease;
}
.btn:hover:not(:disabled){background:linear-gradient(180deg,#3a2e1e,#2a1e14);border-color:rgba(212,168,75,0.45);transform:translateY(-2px);box-shadow:0 8px 20px rgba(212,168,75,0.15);}
.btn:disabled{opacity:0.35;cursor:not-allowed;}
/* Panels overlay */
.overlay{
position:fixed;inset:0;background:rgba(4,4,8,0.85);z-index:50;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity 0.3s ease;backdrop-filter:blur(4px);
}
.overlay.active{opacity:1;pointer-events:auto;}
.panel{
background:linear-gradient(180deg,#10101a 0%,#080810 100%);border:1px solid rgba(212,168,75,0.15);border-radius:16px;padding:28px 36px;max-width:520px;width:92%;box-shadow:0 30px 80px rgba(0,0,0,0.9),inset 0 1px 0 rgba(255,255,255,0.03);position:relative;
}
.panel::before{
content:'';position:absolute;top:0;left:20px;right:20px;height:2px;background:linear-gradient(90deg,transparent,rgba(212,168,75,0.5),transparent);border-radius:50%;
}
.panel h2{font-family:'Cinzel Decorative',serif;color:var(--gold);margin:0 0 16px;text-align:center;letter-spacing:3px;font-size:1.3rem;}
.panel p{font-family:'Cormorant Garamond',serif;color:var(--text-dim);font-size:1.05rem;line-height:1.5;margin:0 0 18px;text-align:center;}
/* Class selection cards */
.class-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;}
.class-card{
background:linear-gradient(180deg,#121220,#0a0a12);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:18px;text-align:center;cursor:pointer;transition:all 0.2s;
}
.class-card:hover{border-color:rgba(212,168,75,0.3);transform:translateY(-4px);box-shadow:0 10px 24px rgba(0,0,0,0.6);}
.class-card.active{border-color:var(--gold);box-shadow:0 0 20px rgba(212,168,75,0.15);}
.class-card h3{margin:0 0 8px;font-family:'Cinzel',serif;font-size:1rem;color:var(--text);}
.class-card span{color:var(--text-dim);font-family:'Cormorant Garamond',serif;font-style:italic;font-size:0.85rem;}
/* Shop cards */
.shop-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;}
.shop-card{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:14px;text-align:center;transition:all 0.2s;}
.shop-card:hover:not(.sold){border-color:rgba(212,168,75,0.25);transform:translateY(-3px);box-shadow:0 6px 16px rgba(0,0,0,0.5);}
.shop-card h4{margin:0 0 4px;font-family:'Cinzel Decorative',serif;color:var(--gold);font-size:0.85rem;letter-spacing:1px;}
.shop-card .price{font-family:'Cinzel',serif;font-weight:700;color:var(--gold);font-size:0.95rem;margin-bottom:4px;}
.shop-card .qty{font-size:0.7rem;color:var(--text-dim);}
.shop-card.sold{opacity:0.35;pointer-events:none;}
/* Status badges */
.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.7rem;font-weight:700;letter-spacing:0.5px;background:rgba(184,53,40,0.15);color:var(--red);border:1px solid rgba(184,53,40,0.2);margin:0 2px;}
.badge.def{background:rgba(58,125,184,0.15);color:var(--blue);border-color:rgba(58,125,184,0.2);}
/* Log / text */
.log-area{margin-top:14px;padding:10px 14px;background:rgba(6,6,10,0.6);border:1px solid rgba(255,255,255,0.04);border-radius:8px;font-family:'Cormorant Garamond',serif;font-size:0.9rem;color:var(--text-dim);line-height:1.5;min-height:52px;}
/* Enemy damage indicator */
.enemy-attack-indicator{
margin-top:6px;font-size:0.8rem;color:#b83528;font-family:'Cinzel',serif;font-weight:600;letter-spacing:1px;
}
/* Small decorative corner ornaments */
.corner-orn{position:absolute;width:24px;height:24px;border:2px solid rgba(212,168,75,0.15);border-radius:4px;pointer-events:none;}
.corner-orn.tl{top:10px;left:10px;border-right:0;border-bottom:0;}
.corner-orn.tr{top:10px;right:10px;border-left:0;border-bottom:0;}
.corner-orn.bl{bottom:10px;left:10px;border-right:0;border-top:0;}
.corner-orn.br{bottom:10px;right:10px;border-left:0;border-top:0;}
</style>
</head>
<body>
<div class="ambient"></div>
<div id="game">
<div class="header">
<div class="brand"><h1>SHADOWS OF AETHELGARD</h1><span>Dark Fantasy Roguelite Prototype</span></div>
<div class="stats-row">
<div class="stat"><span class="stat-icon">⚔</span> <span>Floor <span class="val" id="floor-label">1</span></span></div>
<div class="stat"><span class="stat-icon">✦</span> <span>HP <span class="val" id="hp-val">30</span></span></div>
<div class="stat"><span class="stat-icon">✶</span> <span>Def <span class="val" id="def-val">0</span></span></div>
<div class="stat"><span class="stat-icon">⚡</span> <span>Energy <span class="val" id="energy-val">5</span> / <span id="max-energy">5</span></span></div>
<div class="stat"><span class="stat-icon">✦</span> <span>Gold <span class="val" id="gold-val">0</span></span></div>
</div>
</div>
<div class="main-stage">
<!-- Enemy center -->
<div class="enemy-zone" id="enemy-zone">
<div class="enemy-frame" id="enemy-frame">
<svg class="enemy-img" viewBox="0 0 140 140" id="enemy-svg">
<defs>
<linearGradient id="evil-grad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#4a1c5a"/><stop offset="100%" stop-color="#140a1a"/></linearGradient>
</defs>
<circle cx="70" cy="60" r="22" fill="#4a1c5a" opacity="0.9"/>
<path d="M50 70 Q55 120 90 120 Q125 100 105 70 Z" fill="#2a0e2a" stroke="#7a3ab8" stroke-width="2"/>
<path d="M40 90 Q70 130 100 90" stroke="#7a3ab8" stroke-width="3" fill="none" opacity="0.7"/>
<circle cx="70" cy="58" r="10" fill="#0a0a14" stroke="#b83528" stroke-width="2"/>
<path d="M60 78 Q70 88 80 78" stroke="#b83528" stroke-width="2" fill="none"/>
<circle cx="60" cy="55" r="3" fill="#b83528" opacity="0.8"/>
<circle cx="80" cy="55" r="3" fill="#b83528" opacity="0.8"/>
</svg>
</div>
<div class="enemy-name" id="enemy-name">Void Spawn</div>
<div class="enemy-hp" id="enemy-hp">20 / 20 HP</div>
<div class="enemy-attack-indicator" id="enemy-atk">Attack: 3</div>
<div class="enemy-status" id="enemy-status"></div>
</div>
<!-- Player -->
<div class="player-zone">
<div class="hand" id="hand"></div>
<div class="action-bar">
<button class="btn" id="btn-play" onclick="startTurn()">Begin Turn</button>
<button class="btn" id="btn-shop" onclick="openShop()" disabled>Enter Shop</button>
<button class="btn" onclick="restartGame()">Restart</button>
</div>
<div class="log-area" id="log">Select a class to begin your descent.</div>
</div>
</div>
</div>
<!-- Overlays -->
<div class="overlay" id="overlay-class">
<div class="panel" style="max-width:620px;">
<h2>Choose Your Hero</h2>
<p>Three classes, each with a unique starter deck and playstyle. The Void awaits.</p>
<div class="class-grid" id="class-grid"></div>
<div style="text-align:center;margin-top:14px;color:var(--text-dim);font-size:0.8rem;font-family:'Cormorant Garamond',serif;">All cards are Common and available in the shop.</div>
</div>
</div>
<div class="overlay" id="overlay-shop">
<div class="panel" style="max-width:640px;">
<h2>The Merchant of Aethelgard</h2>
<p>Buy cards with Gold. Duplicates cost more. You may purchase up to 5 copies of each card.</p>
<div class="shop-grid" id="shop-grid"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:14px;">
<button class="btn" onclick="closeShop()">Continue</button>
<span style="color:var(--gold);font-weight:700;font-family:'Cinzel';">Gold: <span id="shop-gold">0</span></span>
</div>
</div>
</div>
<div class="overlay" id="overlay-win">
<div class="panel" style="max-width:460px;text-align:center;">
<h2 style="font-size:1.6rem;">Victory Over the Void</h2>
<p style="font-size:1.1rem;">You have slain the Void Lord and reclaimed Aethelgard. The shadows recede.</p>
<button class="btn" onclick="restartGame()" style="margin-top:10px;padding:12px 36px;font-size:1rem;">Return to Menu</button>
</div>
</div>
<div class="overlay" id="overlay-lose">
<div class="panel" style="max-width:420px;text-align:center;">
<h2 style="font-size:1.6rem;color:var(--red);">Defeat</h2>
<p>You have fallen to the Void. Aethelgard is lost to darkness.</p>
<button class="btn" onclick="restartGame()" style="margin-top:10px;padding:12px 36px;font-size:1rem;">Try Again</button>
</div>
</div>
<script>
/* ==================== DATA ==================== */
const CLASSES = {
bastion: {name:'The Bastion',desc:'High Defense · Block · AoE',hp:35,energy:3,maxEnergy:3,deck:['BA-A01','BA-A02','BA-A03','BA-D01','BA-D02','BA-U01','BA-U02','BA-A01','BA-D01','BA-U03']},
wither: {name:'The Wither',desc:'Burn · Debuff · Status',hp:30,energy:5,maxEnergy:5,deck:['WI-A01','WI-A02','WI-A03','WI-D01','WI-D02','WI-U01','WI-U02','WI-U03','WI-A01','WI-D01']},
shroud: {name:'The Shroud',desc:'Speed · Evasion · Assassination',hp:30,energy:5,maxEnergy:5,deck:['SH-A01','SH-A02','SH-A03','SH-D01','SH-D02','SH-U01','SH-U02','SH-A01','SH-D02','SH-U01']},
};
const CARD_DATA = {
'BA-A01':{id:'BA-A01',title:'Slam',type:'attack',cost:2,stats:{atk:3,def:0},desc:'Deals 3 damage. If you have 30 HP, draw 1 card.',cls:'bastion'},
'BA-A02':{id:'BA-A02',title:'Earthquake',type:'attack',cost:3,stats:{atk:4,def:0},desc:'Deals 4 damage to all enemies.',cls:'bastion'},
'BA-A03':{id:'BA-A03',title:'Stone Fist',type:'attack',cost:1,stats:{atk:2,def:0},desc:'Deals 2 damage.',cls:'bastion'},
'BA-A04':{id:'BA-A04',title:'Boulder',type:'attack',cost:4,stats:{atk:6,def:0},desc:'Deals 6 damage. Costs 4 Energy.',cls:'bastion'},
'BA-D01':{id:'BA-D01',title:'Shield Wall',type:'defend',cost:0,stats:{atk:0,def:4},desc:'Gain 4 Defense for this turn.',cls:'bastion'},
'BA-D02':{id:'BA-D02',title:'Iron Skin',type:'defend',cost:2,stats:{atk:0,def:5},desc:'Gain 5 Defense permanently. Discard 1 card.',cls:'bastion'},
'BA-D03':{id:'BA-D03',title:'Stand Firm',type:'defend',cost:1,stats:{atk:0,def:2},desc:'Gain 2 Defense. Blocks 1 incoming damage.',cls:'bastion'},
'BA-D04':{id:'BA-D04',title:'Taunt',type:'defend',cost:2,stats:{atk:0,def:3},desc:'Gain 3 Defense. Stun 1 enemy for 1 turn.',cls:'bastion'},
'BA-U01':{id:'BA-U01',title:'Search',type:'other',cost:2,stats:{atk:0,def:0},desc:'Draw 1 card. Discard hand if > 5 cards.',cls:'bastion'},
'BA-U02':{id:'BA-U02',title:'Repair',type:'other',cost:3,stats:{atk:0,def:0},desc:'Heal 6 HP.',cls:'bastion'},
'BA-U03':{id:'BA-U03',title:'Rally',type:'other',cost:1,stats:{atk:0,def:0},desc:'Draw 1 card. Gain 1 Energy.',cls:'bastion'},
'BA-U04':{id:'BA-U04',title:'Loot',type:'other',cost:2,stats:{atk:0,def:0},desc:'Gain 5 Gold.',cls:'bastion'},
'WI-A01':{id:'WI-A01',title:'Fireball',type:'attack',cost:2,stats:{atk:3,def:0},desc:'Deals 3 damage. Apply 1 Burn to target.',cls:'wither'},
'WI-A02':{id:'WI-A02',title:'Inferno',type:'attack',cost:3,stats:{atk:4,def:0},desc:'Deals 4 damage. Applies 3 Burn.',cls:'wither'},
'WI-A03':{id:'WI-A03',title:'Ember',type:'attack',cost:1,stats:{atk:2,def:0},desc:'Deals 2 damage. Deals 1 damage every turn to target.',cls:'wither'},
'WI-A04':{id:'WI-A04',title:'Pyre',type:'attack',cost:4,stats:{atk:5,def:0},desc:'Deals 5 damage. All enemies take 1 damage next turn.',cls:'wither'},
'WI-D01':{id:'WI-D01',title:'Scorch Shield',type:'defend',cost:2,stats:{atk:0,def:3},desc:'Gain 3 Defense. Block 1 Burn damage.',cls:'wither'},
'WI-D02':{id:'WI-D02',title:'Heat Resist',type:'defend',cost:1,stats:{atk:0,def:1},desc:'Gain 1 Defense. Gain 1 Energy.',cls:'wither'},
'WI-D03':{id:'WI-D03',title:'Smoke',type:'defend',cost:2,stats:{atk:0,def:4},desc:'Gain 4 Defense. Block 1 Burn damage.',cls:'wither'},
'WI-D04':{id:'WI-D04',title:'Endure',type:'defend',cost:3,stats:{atk:0,def:2},desc:'Gain 2 Defense. Heal 2 HP.',cls:'wither'},
'WI-U01':{id:'WI-U01',title:'Poison',type:'other',cost:2,stats:{atk:0,def:0},desc:'Apply 1 Poison to target.',cls:'wither'},
'WI-U02':{id:'WI-U02',title:'Burn',type:'other',cost:3,stats:{atk:0,def:0},desc:'Deal 3 damage + 2 Burn to all enemies.',cls:'wither'},
'WI-U03':{id:'WI-U03',title:'Slow',type:'other',cost:1,stats:{atk:0,def:0},desc:'Draw 1 card. Target cannot move next turn (Stun).',cls:'wither'},
'WI-U04':{id:'WI-U04',title:'Wither',type:'other',cost:2,stats:{atk:0,def:0},desc:'Heal 5 HP. Reduce enemy Attack by 1.',cls:'wither'},
'SH-A01':{id:'SH-A01',title:'Backstab',type:'attack',cost:2,stats:{atk:4,def:0},desc:'Deals 4 damage. +1 Damage if target has no Armor.',cls:'shroud'},
'SH-A02':{id:'SH-A02',title:'Shadow Bolt',type:'attack',cost:3,stats:{atk:5,def:0},desc:'Deals 5 damage. Target cannot block.',cls:'shroud'},
'SH-A03':{id:'SH-A03',title:'Silence',type:'attack',cost:1,stats:{atk:3,def:0},desc:'Deals 3 damage. Target loses ability to use cards.',cls:'shroud'},
'SH-A04':{id:'SH-A04',title:'Vengeance',type:'attack',cost:3,stats:{atk:6,def:0},desc:'Deals 6 damage. Gain 2 Energy.',cls:'shroud'},
'SH-D01':{id:'SH-D01',title:'Blur',type:'defend',cost:3,stats:{atk:0,def:2},desc:'Gain 2 Defense. Ignore next attack.',cls:'shroud'},
'SH-D02':{id:'SH-D02',title:'Step',type:'defend',cost:2,stats:{atk:0,def:3},desc:'Gain 3 Defense. Gain 1 Energy.',cls:'shroud'},
'SH-D03':{id:'SH-D03',title:'Evasion',type:'defend',cost:1,stats:{atk:0,def:1},desc:'Gain 1 Defense. Skip enemy action.',cls:'shroud'},
'SH-D04':{id:'SH-D04',title:'Phantom',type:'defend',cost:2,stats:{atk:0,def:4},desc:'Gain 4 Defense. Ghost form for 1 turn.',cls:'shroud'},
'SH-U01':{id:'SH-U01',title:'Dash',type:'other',cost:1,stats:{atk:0,def:0},desc:'Gain 1 Energy. Skip enemy attack.',cls:'shroud'},
'SH-U02':{id:'SH-U02',title:'Teleport',type:'other',cost:2,stats:{atk:0,def:0},desc:'Move to back row. Deal 2 damage.',cls:'shroud'},
'SH-U03':{id:'SH-U03',title:'Hide',type:'other',cost:2,stats:{atk:0,def:0},desc:'Gain 1 Defense. Discard 1 card. Draw 2 cards.',cls:'shroud'},
'SH-U04':{id:'SH-U04',title:'Assassinate',type:'other',cost:3,stats:{atk:0,def:0},desc:'Deal 8 damage. Cannot be blocked this turn.',cls:'shroud'},
};
const ENEMY_TYPES = [
{name:'Goblin',hp:20,dmg:3},
{name:'Rat Swarm',hp:20,dmg:3},
{name:'Vampire Knight',hp:30,dmg:5},
{name:'Ogre',hp:30,dmg:5},
{name:'Void Warrior',hp:40,dmg:8},
{name:'Cultist',hp:40,dmg:8},
];
/* ==================== STATE ==================== */
let state = {
cls:null,
hp:30,
maxHp:30,
energy:5,
maxEnergy:5,
defense:0,
gold:0,
floor:1,
room:0,
deck:[],
hand:[],
discard:[],
logText:'',
enemy:{name:'',hp:20,maxHp:20,dmg:3,status:{burn:0,poison:0,stun:0},attacksNextTurn:0,blockedNext:0},
turnPhase:'start', // start, player_action, player_done, enemy_action, end
cardsPlayed:0,
currentRoundStarted:false,
inventory:{},
selectedCards:[],
};
/* ==================== INIT ==================== */
function initGame() {
// Build class selection
const grid = document.getElementById('class-grid');
grid.innerHTML = '';
for (const [key,cls] of Object.entries(CLASSES)) {
const card = document.createElement('div');
card.className = 'class-card';
card.innerHTML = `<h3>${cls.name}</h3><span>${cls.desc}</span>`;
card.onclick = () => selectClass(key);
grid.appendChild(card);
}
openOverlay('overlay-class');
}
function selectClass(key) {
state.cls = CLASSES[key];
state.hp = state.cls.hp;
state.maxHp = state.cls.hp;
state.energy = state.cls.energy;
state.maxEnergy = state.cls.maxEnergy;
state.defense = 0;
state.gold = 5; // Starting gold
state.floor = 1;
state.room = 0;
state.deck = [...state.cls.deck];
state.hand = [];
state.discard = [];
state.enemy = {name:'',hp:0,maxHp:0,dmg:0,status:{burn:0,poison:0,stun:0},attacksNextTurn:0,blockedNext:0};
state.turnPhase = 'start';
state.cardsPlayed = 0;
state.currentRoundStarted = false;
state.inventory = {};
// Initialize inventory with starter cards
for (const c of state.cls.deck) { state.inventory[c] = (state.inventory[c]||0)+1; }
// Add extra starter copies to make it playable quickly
for (const c of state.cls.deck) { state.inventory[c] = (state.inventory[c]||0)+1; }
closeOverlay('overlay-class');
nextRoom();
refreshUI();
log('You descend into Aethelgard. Choose your cards wisely.');
}
/* ==================== GAME LOOP ==================== */
function nextRoom() {
state.room++;
if (state.room > 9 && state.floor < 3) {
state.floor++;
state.room = 1;
log(`You reach Floor ${state.floor}. The air grows thicker.`);
}
if (state.floor === 3 && state.room > 3) {
// Boss on floor 3 room 3 then room 4? Let's say rooms 1-3 standard then boss on 4.
}
if (state.room === 4 || (state.floor===1 && state.room===4) || (state.floor===2 && state.room===4) || (state.floor===3 && state.room===4)) {
spawnBoss();
} else {
spawnStandardEnemy();
}
state.currentRoundStarted = false;
state.turnPhase = 'start';
state.cardsPlayed = 0;
state.hand = [];
refreshUI();
document.getElementById('btn-play').disabled = false;
document.getElementById('btn-play').textContent = 'Begin Turn';
document.getElementById('btn-shop').disabled = false;
log(`Room ${state.room} — ${state.enemy.name} approaches.`);
}
function spawnStandardEnemy() {
const tier = Math.min(state.floor, ENEMY_TYPES.length - 2); // skip bosses from list
const et = ENEMY_TYPES[tier];
const scale = 1 + (state.room-1)*0.2;
const enemy = {
name: et.name,
hp: Math.round(et.hp * scale),
maxHp: Math.round(et.hp * scale),
dmg: Math.round(et.dmg * scale),
status: {burn:0, poison:0, stun:0},
attacksNextTurn: 0,
blockedNext: 0,
};
state.enemy = enemy;
}
function spawnBoss() {
const bossConfigs = [
{name:'The Gatekeeper',hp:80,dmg:15,mechanic:'Blocks 5 damage per turn until HP < 20.'},
{name:'The Corrupted',hp:80,dmg:15,mechanic:'Deals 20 damage if you have < 3 Energy.'},
{name:'The Void Lord',hp:120,dmg:15,mechanic:'Regenerates 10 HP each turn. Cannot block.'},
];
const idx = Math.min(state.floor-1, bossConfigs.length-1);
const b = bossConfigs[idx];
state.enemy = {
name: b.name,
hp: b.hp,
maxHp: b.hp,
dmg: b.dmg,
status: {burn:0, poison:0, stun:0},
attacksNextTurn: 0,
blockedNext: 0,
};
log(`BOSS BATTLE: ${b.name} — ${b.mechanic}`);
}
/* ==================== HAND / CARDS ==================== */
function drawHand(num = 5) {
// Draw from deck or shuffle discard
if (state.deck.length < num) {
state.deck = [...state.discard];
state.discard = [];
shuffle(state.deck);
}
for (let i = 0; i < num; i++) {
if (state.hand.length >= 5) break; // max hand 5 for simplicity
const cardId = state.deck.pop();
if (cardId) state.hand.push({id: cardId, played: false, selected: false});
}
}
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]]; }
}
/* ==================== TURN LOGIC ==================== */
function startTurn() {
if (state.turnPhase !== 'start') return;
drawHand();
state.turnPhase = 'player_action';
state.cardsPlayed = 0;
state.currentRoundStarted = true;
state.energy = Math.min(state.maxEnergy, state.energy + 1); // regen at start (simplified)
refreshUI();
document.getElementById('btn-play').disabled = true;
document.getElementById('btn-play').textContent = 'End Turn';
document.getElementById('btn-play').onclick = endTurn;
log('Draw phase — play your cards.');
}
function playCard(idx) {
if (state.turnPhase !== 'player_action') return;
const card = state.hand[idx];
if (card.played || card.selected) return;
const info = CARD_DATA[card.id];
if (!info) return;
if (state.energy < info.cost) {
log('Not enough Energy to play ' + info.title + '.');
return;
}
// Mark selected / played
card.played = true;
card.selected = true;
state.energy -= info.cost;
state.cardsPlayed++;
// Resolve effects
resolveCardEffect(info, card.id);
refreshUI();
// Auto remove after short visual delay or just keep showing played
setTimeout(() => renderHand(), 250);
}
function resolveCardEffect(info, cardId) {
const type = info.type;
const desc = info.desc.toLowerCase();
const atk = info.stats.atk || 0;
const def = info.stats.def || 0;
if (type === 'attack') {
let dmg = atk;
// Boss mechanics adjustments
if (state.enemy.name === 'The Gatekeeper' && state.enemy.hp > 20) {
dmg = Math.max(0, dmg - 5);
log('Gatekeeper blocks 5 damage while above 20 HP.');
}
if (state.enemy.name === 'The Void Lord') {
dmg = dmg; // cannot block, full dmg
}
// Apply damage
state.enemy.hp = Math.max(0, state.enemy.hp - dmg);
log(`You played ${info.title} for ${dmg} damage.`);
// Apply status from desc keywords (simplified parsing)
if (desc.includes('burn')) state.enemy.status.burn = (state.enemy.status.burn||0) + (desc.includes('3 burn')?3:1);
if (desc.includes('poison')) state.enemy.status.poison = (state.enemy.status.poison||0) + 1;
if (desc.includes('stun')) state.enemy.status.stun = (state.enemy.status.stun||0) + 1;
} else if (type === 'defend') {
state.defense += def;
if (desc.includes('permanently')) {
// Permanent defense stays across turns
} else {
log(`You gained ${def} Defense.`);
}
if (desc.includes('stun')) state.enemy.status.stun = 1;
if (desc.includes('heal')) {
// Heal from defense cards (e.g. Endure)
const healMatch = desc.match(/heal (\d+)/i);
if (healMatch) { state.hp = Math.min(state.maxHp, state.hp + parseInt(healMatch[1])); }
}
if (desc.includes('draw')) {
const drawMatch = desc.match(/draw (\d+)/i);
if (drawMatch) drawHand(parseInt(drawMatch[1]));
}
} else if (type === 'other') {
if (desc.includes('heal')) {
const healMatch = desc.match(/heal (\d+)/i);
if (healMatch) { state.hp = Math.min(state.maxHp, state.hp + parseInt(healMatch[1])); log(`Restored ${healMatch[1]} HP.`); }
}
if (desc.includes('draw 1')) {
drawHand(1);
log('Drew 1 card.');
}
if (desc.includes('gain 1 energy')) {
state.energy = Math.min(state.maxEnergy, state.energy + 1);
log('Gained 1 Energy.');
}
if (desc.includes('gain 5 gold')) {
state.gold += 5;
log('Gained 5 Gold.');
}
if (desc.includes('discard 1 card')) {
// Simulate discard: remove first non-played from hand if possible
const discardable = state.hand.find(c => !c.played);
if (discardable) { discardable.played = true; state.discard.push(discardable.id); state.hand = state.hand.filter(c=>!c.played); }
}
if (desc.includes('skip enemy') || desc.includes('skip enemy action')) {
state.enemy.status.stun = 1;
log('Enemy action skipped.');
}
if (desc.includes('deal 8 damage')) {
let dmg = 8;
if (state.enemy.name === 'The Gatekeeper' && state.enemy.hp > 20) dmg = Math.max(0, dmg - 5);
state.enemy.hp = Math.max(0, state.enemy.hp - dmg);
log('Assassinate dealt 8 damage (unblockable).');
}
if (desc.includes('deal 3 damage') && desc.includes('burn') && desc.includes('all')) {
// WI-U02 Burn
state.enemy.hp = Math.max(0, state.enemy.hp - 3);
state.enemy.status.burn = (state.enemy.status.burn||0) + 2;
log('Burn: 3 damage + 2 Burn to enemy.');
}
if (desc.includes('apply 1 poison')) { state.enemy.status.poison = (state.enemy.status.poison||0) + 1; log('Applied Poison.'); }
if (desc.includes('reduce enemy attack')) { state.enemy.dmg = Math.max(1, state.enemy.dmg - 1); log('Enemy attack reduced by 1.'); }
// Handle draw + discard from Search / Rally etc
}
// Handle card draw if description mentions draw but we haven't parsed it
if (desc.includes('draw') && !desc.includes('draw 1')) {
// Already handled by generic draw logic earlier; if missed, draw 1
}
}
/* ==================== ENEMY TURN ==================== */
function enemyTurn() {
if (state.turnPhase !== 'player_action' && state.turnPhase !== 'player_done') return;
state.turnPhase = 'enemy_action';
// Apply DoT first
let burnDmg = (state.enemy.status.burn || 0);
let poisonDmg = (state.enemy.status.poison || 0);
if (burnDmg > 0 || poisonDmg > 0) {
const total = burnDmg + poisonDmg;
state.enemy.hp = Math.max(0, state.enemy.hp - total);
log(`Status effects dealt ${total} damage.`);
state.enemy.status.burn = Math.max(0, state.enemy.status.burn - 1);
state.enemy.status.poison = Math.max(0, state.enemy.status.poison - 1);
}
// Boss mechanics
if (state.enemy.name === 'The Corrupted' && state.energy < 3) {
state.hp -= 20;
log('The Corrupted bursts: +20 damage because you have < 3 Energy!');
}
if (state.enemy.name === 'The Void Lord') {
state.enemy.hp = Math.min(state.enemy.maxHp, state.enemy.hp + 10);
log('The Void Lord regenerates 10 HP.');
}
// Enemy stunned?
if (state.enemy.status.stun > 0) {
state.enemy.status.stun = Math.max(0, state.enemy.status.stun - 1);
log('Enemy is stunned and skips its attack.');
state.turnPhase = 'end';
refreshUI();
setTimeout(endTurnSequence, 600);
return;
}
// Enemy attacks
let dmg = state.enemy.dmg;
// Check defense
if (state.defense > 0) {
const absorbed = Math.min(dmg, state.defense);
state.defense -= absorbed;
dmg -= absorbed;
log(`Your Defense absorbed ${absorbed} damage.`);
}
if (dmg > 0) {
state.hp -= dmg;
log(`Enemy deals ${dmg} damage.`);
}
// Enemy blocked?
if (state.enemy.blockedNext > 0) {
state.enemy.blockedNext = 0;
log('Enemy blocked damage this turn.');
}
state.turnPhase = 'end';
refreshUI();
if (state.hp <= 0) {
log('You have fallen.');
setTimeout(() => openOverlay('overlay-lose'), 800);
return;
}
if (state.enemy.hp <= 0) {
const goldGain = state.enemy.name.includes('Boss') ? 15 : 5;
state.gold += goldGain;
log(`${state.enemy.name} defeated! Gained ${goldGain} Gold.`);
if (state.floor >= 3 && (state.enemy.name === 'The Void Lord')) {
setTimeout(() => openOverlay('overlay-win'), 1000);
}
// Go to shop / next room
state.defense = 0; // reset defense after combat? In GDD, defense resets per turn but some cards say permanent
setTimeout(() => openShop(), 600);
return;
}
setTimeout(endTurnSequence, 600);
}
/* ==================== TURN END ==================== */
function endTurnSequence() {
// Recover energy if not exhausted (simplified)
state.energy = Math.min(state.maxEnergy, state.energy + 1);
// Discard played cards
state.discard.push(...state.hand.filter(c=>c.played).map(c=>c.id));
state.hand = state.hand.filter(c=>!c.played);
state.defense = 0; // reset temporary defense
state.turnPhase = 'start';
state.cardsPlayed = 0;
refreshUI();
document.getElementById('btn-play').textContent = 'Begin Turn';
document.getElementById('btn-play').onclick = startTurn;
document.getElementById('btn-play').disabled = false;
log('Turn ended. Energy recovered.');
// Check enemy death after effects
if (state.enemy.hp <= 0) {
const goldGain = state.enemy.name.includes('Boss') ? 15 : 5;
state.gold += goldGain;
log(`${state.enemy.name} defeated! Gained ${goldGain} Gold.`);
if (state.floor >= 3 && state.enemy.name === 'The Void Lord') {
setTimeout(() => openOverlay('overlay-win'), 800);
} else {
setTimeout(() => openShop(), 600);
}
}
}
function endTurn() {
if (state.turnPhase !== 'player_action') return;
state.turnPhase = 'player_done';
document.getElementById('btn-play').textContent = 'Enemy Turn...';
document.getElementById('btn-play').disabled = true;
refreshUI();
setTimeout(enemyTurn, 500);
}
/* ==================== SHOP ==================== */
function openShop() {
if (state.enemy.hp > 0) return;
openOverlay('overlay-shop');
renderShop();
document.getElementById('shop-gold').textContent = state.gold;
}
function closeShop() {
closeOverlay('overlay-shop');
nextRoom();
}
function renderShop() {
const grid = document.getElementById('shop-grid');
grid.innerHTML = '';
const cards = Object.values(CARD_DATA);
// Show 4 random cards from pool
const pool = cards.filter(c => (state.inventory[c.id]||0) < 5);
const picks = pool.sort(() => Math.random()-0.5).slice(0, 4);
for (const card of picks) {
const count = state.inventory[card.id] || 0;
const baseCost = card.cost; // simplified cost
const cost = Math.min(50, baseCost + count);
const sold = state.gold < cost || count >= 5;
const div = document.createElement('div');
div.className = 'shop-card' + (sold ? ' sold' : '');
div.innerHTML = `<h4>${CARD_DATA[card.id].title}</h4><div class="price">${cost}G</div><div class="qty">Owned: ${count}</div>`;
div.onclick = () => buyCard(card.id);
grid.appendChild(div);
}
}
function buyCard(id) {
const card = CARD_DATA[id];
if (!card) return;
const count = state.inventory[id] || 0;
const cost = Math.min(50, card.cost + count);
if (state.gold < cost) {
log('Not enough gold.');
return;
}
if (count >= 5) {
log('Maximum copies reached.');
return;
}
state.gold -= cost;
state.inventory[id] = count + 1;
state.deck.push(id);
log(`Bought ${card.title} for ${cost} Gold.`);
document.getElementById('shop-gold').textContent = state.gold;
renderShop();
}
/* ==================== UI ==================== */
function refreshUI() {
document.getElementById('hp-val').textContent = Math.max(0, state.hp) + ' / ' + state.maxHp;
document.getElementById('def-val').textContent = state.defense;
document.getElementById('energy-val').textContent = state.energy;
document.getElementById('max-energy').textContent = state.maxEnergy;
document.getElementById('gold-val').textContent = state.gold;
document.getElementById('floor-label').textContent = state.floor;
document.getElementById('enemy-name').textContent = state.enemy.name || '--';
document.getElementById('enemy-hp').textContent = Math.max(0, state.enemy.hp) + ' / ' + state.enemy.maxHp + ' HP';
document.getElementById('enemy-atk').textContent = 'Attack: ' + (state.enemy.dmg || 0);
const statusDiv = document.getElementById('enemy-status');
let badges = '';
if (state.enemy.status.burn > 0) badges += '<span class="badge">Burn</span>';
if (state.enemy.status.poison > 0) badges += '<span class="badge">Poison</span>';
if (state.enemy.status.stun > 0) badges += '<span class="badge def">Stunned</span>';
statusDiv.innerHTML = badges;
renderHand();
}
function renderHand() {
const container = document.getElementById('hand');
container.innerHTML = '';
for (let i = 0; i < state.hand.length; i++) {
const cardObj = state.hand[i];
const info = CARD_DATA[cardObj.id];
if (!info) continue;
const cardEl = document.createElement('div');
cardEl.className = `card type-${info.type}` + (cardObj.played ? ' played' : '') + (state.turnPhase !== 'player_action' || info.cost > state.energy ? ' disabled' : '');
cardEl.innerHTML = `
<svg class="card-frame" viewBox="0 0 150 210" preserveAspectRatio="none">
<defs>
<linearGradient id="card-grad-${info.cls}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2a2115"/>
<stop offset="50%" stop-color="#1a180e"/>
<stop offset="100%" stop-color="#0f0d14"/>
</linearGradient>
</defs>
<rect width="150" height="210" rx="14" ry="14" fill="url(#card-grad-${info.cls})" stroke="#bfa85a" stroke-width="1.5" opacity="0.9"/>
<rect x="6" y="6" width="138" height="198" rx="10" ry="10" fill="none" stroke="#6b5a2a" stroke-width="0.5"/>
<path d="M10 20 Q75 15 140 20" stroke="#bfa85a" stroke-width="0.8" fill="none" opacity="0.5"/>
<path d="M10 190 Q75 195 140 190" stroke="#bfa85a" stroke-width="0.8" fill="none" opacity="0.5"/>
</svg>
<div class="card-inner">
<div class="card-header">
<span class="card-cost">${info.cost}</span>
<span class="card-id">${info.id}</span>
</div>
<div class="card-title">${info.title}</div>
<div class="card-desc">${info.desc}</div>
<div class="card-stats">${info.stats.atk > 0 ? `ATK ${info.stats.atk}` : ''} ${info.stats.def > 0 ? `DEF ${info.stats.def}` : ''}</div>
</div>`;
cardEl.onclick = () => playCard(i);
container.appendChild(cardEl);
}
}
/* ==================== OVERLAYS ==================== */
function openOverlay(id) { document.getElementById(id).classList.add('active'); }
function closeOverlay(id) { document.getElementById(id).classList.remove('active'); }
function log(msg) {
document.getElementById('log').textContent = msg;
}
function restartGame() {
closeOverlay('overlay-win');
closeOverlay('overlay-lose');
state = {
cls:null, hp:30, maxHp:30, energy:5, maxEnergy:5, defense:0, gold:0, floor:1, room:0,
deck:[], hand:[], discard:[], logText:'', enemy:{name:'',hp:0,maxHp:0,dmg:0,status:{burn:0,poison:0,stun:0},attacksNextTurn:0,blockedNext:0},
turnPhase:'start', cardsPlayed:0, currentRoundStarted:false, inventory:{}, selectedCards:[],
};
document.getElementById('btn-play').textContent = 'Select Class';
document.getElementById('btn-play').onclick = () => { openOverlay('overlay-class'); };
initGame();
}
/* ==================== BOOT ==================== */
window.addEventListener('DOMContentLoaded', () => {
initGame();
});
</script>
<style>
.card-art{height:80px;border-radius:8px;background:linear-gradient(180deg,#0f0d14,#1a161f);border:1px solid rgba(212,168,75,0.12);margin:4px 0;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;box-shadow:inset 0 0 20px rgba(0,0,0,0.6);}
.card-art img{width:55px;height:55px;opacity:0.35;filter:drop-shadow(0 0 8px rgba(212,168,75,0.25));}
.card-back.bastion-back{border-color:rgba(107,83,40,0.35);background:linear-gradient(135deg,#1a160e,#2a2014);}
.card-back.wither-back{border-color:rgba(138,46,46,0.35);background:linear-gradient(135deg,#1a0e10,#2a1414);}
.card-back.shroud-back{border-color:rgba(24,40,58,0.35);background:linear-gradient(135deg,#0e121a,#141e2a);}
</style>
<script>
// Add placeholder art to cards and unique card back SVGs
function enhanceCards() {
const backs = {
bastion: '<svg viewBox="0 0 90 120"><rect width="90" height="120" rx="8" fill="#2a1e10" stroke="#6b5328" stroke-width="2"/><circle cx="45" cy="55" r="22" fill="#4a2e1a" stroke="#bfa85a" stroke-width="1"/><path d="M30 75 Q45 95 60 75 L45 60 Z" fill="#8a6e3e"/></svg>',
wither: '<svg viewBox="0 0 90 120"><rect width="90" height="120" rx="8" fill="#2a1014" stroke="#8a2e2e" stroke-width="2"/><circle cx="45" cy="55" r="20" fill="#5a1818" stroke="#ff5e3a" stroke-width="1"/><path d="M35 60 Q45 75 55 60 Q45 45 35 60" fill="#7a2e2e"/></svg>',
shroud: '<svg viewBox="0 0 90 120"><rect width="90" height="120" rx="8" fill="#101420" stroke="#18283a" stroke-width="2"/><circle cx="45" cy="55" r="22" fill="#182040" stroke="#3a7db8" stroke-width="1"/><path d="M35 50 Q45 65 55 50 L45 35 Z" fill="#2a3548"/></svg>',
};
// Patch renderHand to inject placeholder art
const originalRender = window.renderHand || (()=>{});
// We can't easily monkey-patch, so we'll observe mutations
const observer = new MutationObserver((muts) => {
for (const m of muts) {
for (const node of m.addedNodes) {
if (node.nodeType === 1 && node.classList && node.classList.contains('card')) {
if (!node.querySelector('.card-art')) {
const art = document.createElement('div');
art.className = 'card-art';
art.innerHTML = '<svg viewBox="0 0 60 60"><circle cx="30" cy="30" r="18" fill="#3a2e1a" stroke="#bfa85a" stroke-width="2"/><path d="M15 35 L30 15 L45 35 Z" fill="#6b4328" stroke="#bfa85a" stroke-width="1"/></svg>';
const inner = node.querySelector('.card-inner');
if (inner) {
const title = inner.querySelector('.card-title');
if (title) title.insertAdjacentElement('afterend', art);
}
}
}
}
}
});
observer.observe(document.getElementById('hand') || document.body, {childList:true, subtree:true});
}
window.addEventListener('DOMContentLoaded', enhanceCards);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"full","sessionId":"293ece5d-15d8-48a7-b95e-a3f0c756743e","tokensIn":674973,"tokensOut":19202,"tokensTotal":694175,"turns":19,"toolCalls":18,"failedToolCalls":4,"timestamp":"2026-07-18T21:55:42.803Z"} -->