Shadows
Gemma 4 31b · 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,918 bytes · SHA-256 12e2af2e0ae9
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shadows of the Abyss</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Crimson+Text:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0a0a0c;
--panel-color: #1a1a1e;
--accent-color: #8b0000;
--text-color: #d4d4d8;
--highlight-color: #f5f5f5;
--gold-color: #ffd700;
--energy-color: #4dabff;
--block-color: #aab;
--hp-color: #e53935;
--rarity-common: #fff;
--rarity-uncommon: #4caf50;
--rarity-rare: #ff9800;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Crimson Text', serif;
overflow: hidden;
user-select: none;
}
h1, h2, h3, .cinzel {
font-family: 'Cinzel', serif;
text-transform: uppercase;
}
#game-container {
width: 100vw;
height: 100vh;
position: relative;
display: flex;
flex-direction: column;
}
.screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: radial-gradient(circle, #1a1a2e 0%, #0a0a0c 100%);
}
.screen.active { display: flex; }
/* UI Components */
.btn {
background: var(--panel-color);
color: var(--text-color);
border: 2px solid var(--accent-color);
padding: 10px 20px;
font-family: 'Cinzel', serif;
cursor: pointer;
transition: all 0.2s;
margin: 10px;
font-size: 1.1rem;
}
.btn:hover {
background: var(--accent-color);
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Class Selection */
.class-grid {
display: flex;
gap: 20px;
margin-bottom: 40px;
}
.class-card {
background: var(--panel-color);
border: 3px solid #444;
padding: 20px;
width: 200px;
text-align: center;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
}
.class-card:hover {
transform: translateY(-10px);
border-color: var(--accent-color);
}
.class-card h3 { margin-top: 0; }
/* Map Screen */
#map-canvas {
background: rgba(0,0,0,0.5);
border: 2px solid #333;
cursor: pointer;
}
.map-overlay {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0,0,0,0.8);
padding: 15px;
border: 1px solid var(--accent-color);
font-family: 'Cinzel', serif;
}
/* Combat Screen */
#combat-area {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 20px;
position: relative;
}
.entity-row {
display: flex;
justify-content: center;
gap: 50px;
width: 100%;
margin: 40px 0;
}
.entity {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
width: 150px;
}
.entity-sprite {
width: 100px;
height: 100px;
background: #333;
border-radius: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
transition: transform 0.2s;
}
.entity.attacking .entity-sprite { transform: scale(1.1) translateY(-20px); }
.stat-bar {
width: 100%;
height: 20px;
background: #222;
border: 1px solid #555;
position: relative;
margin: 5px 0;
}
.stat-fill {
height: 100%;
transition: width 0.3s ease-out;
}
.hp-fill { background: var(--hp-color); }
.block-fill { background: var(--block-color); width: 0%; }
.stat-text {
position: absolute;
width: 100%;
text-align: center;
font-size: 0.8rem;
line-height: 20px;
color: white;
font-weight: bold;
text-shadow: 1px 1px 2px black;
}
.intent-icon {
position: absolute;
top: -40px;
background: rgba(0,0,0,0.7);
padding: 5px 10px;
border-radius: 5px;
font-weight: bold;
white-space: nowrap;
font-family: 'Cinzel', serif;
color: #ffbb00;
}
.status-container {
display: flex;
gap: 5px;
margin-top: 5px;
}
.status-pill {
font-size: 0.7rem;
padding: 2px 5px;
background: #444;
border-radius: 3px;
border: 1px solid #666;
}
/* Hand and Cards */
#player-controls {
width: 100%;
height: 250px;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
#hand {
display: flex;
justify-content: center;
gap: 10px;
height: 180px;
perspective: 1000px;
}
.card {
width: 120px;
height: 170px;
background: #2a2a2e;
border: 2px solid #555;
border-radius: 10px;
padding: 10px;
display: flex;
flex-direction: column;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
position: relative;
user-select: none;
}
.card:hover {
transform: translateY(-30px) scale(1.1);
z-index: 10;
border-color: white;
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
}
.card.playable { border-color: var(--energy-color); }
.card.unplayable { opacity: 0.7; filter: grayscale(0.5); cursor: not-allowed; }
.card-cost {
position: absolute;
top: -10px;
left: -10px;
width: 25px;
height: 25px;
background: var(--energy-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
border: 2px solid white;
}
.card-name {
font-family: 'Cinzel', serif;
font-size: 0.9rem;
text-align: center;
margin-bottom: 10px;
border-bottom: 1px solid #555;
padding-bottom: 5px;
}
.card-desc {
font-size: 0.8rem;
text-align: center;
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
font-style: italic;
}
.card-rarity-common { border-color: var(--rarity-common); }
.card-rarity-uncommon { border-color: var(--rarity-uncommon); }
.card-rarity-rare { border-color: var(--rarity-rare); }
#combat-info {
display: flex;
gap: 30px;
font-family: 'Cinzel', serif;
font-size: 1.2rem;
}
.info-item {
display: flex;
align-items: center;
gap: 10px;
}
/* Rewards Screen */
.reward-options {
display: flex;
gap: 20px;
margin-top: 30px;
}
/* Event/Shop Modal */
.overlay-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.85);
display: none;
justify-content: center;
align-items: center;
z-index: 100;
}
.modal-content {
background: var(--panel-color);
border: 2px solid var(--accent-color);
padding: 30px;
max-width: 600px;
text-align: center;
border-radius: 10px;
}
.modal-text {
font-size: 1.3rem;
margin-bottom: 20px;
line-height: 1.5;
}
.shop-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.shop-item {
background: #222;
border: 1px solid #444;
padding: 10px;
cursor: pointer;
}
.shop-item:hover { border-color: var(--gold-color); }
/* Animations */
@keyframes damage-shake {
0% { transform: translateX(0); }
25% { transform: translateX(-5px); }
50% { transform: translateX(5px); }
75% { transform: translateX(-5px); }
100% { transform: translateX(0); }
}
.shake { animation: damage-shake 0.2s; }
.floating-text {
position: absolute;
color: white;
font-weight: bold;
font-size: 1.5rem;
pointer-events: none;
animation: float-up 1s forwards;
z-index: 100;
}
@keyframes float-up {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-50px); }
}
</style>
</head>
<body>
<div id="game-container">
<!-- Start Screen -->
<div id="screen-start" class="screen active">
<h1 style="font-size: 3rem; color: var(--accent-color); margin-bottom: 50px;">Shadows of the Abyss</h1>
<p>Choose your cursed archetype to begin your descent.</p>
<div class="class-grid">
<div class="class-card" onclick="game.selectClass('Blood Reaper')">
<h3>Blood Reaper</h3>
<p>Sacrifice vitality for devastating power.</p>
<small>Specialty: HP Loss / Life Steal / Bleed</small>
</div>
<div class="class-card" onclick="game.selectClass('Shadowmancer')">
<h3>Shadowmancer</h3>
<p>Manipulate the void and weaken your foes.</p>
<small>Specialty: Debuffs / Intangible / Draw</small>
</div>
<div class="class-card" onclick="game.selectClass('Hellforged')">
<h3>Hellforged</h3>
<p>Wield the fires of the pit to incinerate all.</p>
<small>Specialty: Burn / Summoning / Energy</small>
</div>
</div>
</div>
<!-- Map Screen -->
<div id="screen-map" class="screen">
<div class="map-overlay">
<div>Act: <span id="map-act">1</span> - <span id="map-act-name">The Crypts</span></div>
<div>Gold: <span id="map-gold">0</span>g</div>
<div>HP: <span id="map-hp">0</span> / <span id="map-max-hp">0</span></div>
</div>
<canvas id="map-canvas"></canvas>
</div>
<!-- Combat Screen -->
<div id="screen-combat" class="screen">
<div id="combat-area">
<div class="entity-row" id="enemy-row"></div>
<div class="entity-row">
<div class="entity" id="player-entity">
<div class="entity-sprite">🛡️</div>
<div class="stat-bar"><div id="player-hp-fill" class="stat-fill hp-fill"></div><div id="player-hp-text" class="stat-text">70/70</div></div>
<div class="stat-bar"><div id="player-block-fill" class="stat-fill block-fill"></div><div id="player-block-text" class="stat-text">0</div></div>
<div class="status-container" id="player-statuses"></div>
<div style="margin-top: 10px; font-weight: bold;">The Fallen</div>
</div>
</div>
<div id="player-controls">
<div id="combat-info">
<div class="info-item">⚡ <span id="player-energy">3</span> / 3</div>
<div class="info-item">🎴 <span id="deck-count">0</span> in deck</div>
</div>
<div id="hand"></div>
<button class="btn" onclick="game.endTurn()">End Turn</button>
</div>
</div>
</div>
<!-- Reward Screen -->
<div id="screen-reward" class="screen">
<h2 id="reward-title">Choose a Card</h2>
<div class="reward-options" id="reward-cards"></div>
<button class="btn" onclick="game.skipReward()">Skip Reward</button>
</div>
<!-- GameOver/Victory Screen -->
<div id="screen-end" class="screen">
<h1 id="end-title">Game Over</h1>
<p id="end-msg"></p>
<button class="btn" onclick="location.reload()">Try Again</button>
</div>
</div>
<!-- Modals -->
<div id="modal-overlay" class="overlay-modal">
<div class="modal-content">
<h2 id="modal-title">Event</h2>
<div id="modal-text" class="modal-text"></div>
<div id="modal-options"></div>
</div>
</div>
<script>
/** GAME DATA & CONFIG **/
const DATA = {
CLASSES: {
'Blood Reaper': {
startingDeck: ['Strike', 'Strike', 'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend', 'Defend', 'Sanguine Strike'],
pool: [
{ name: 'Sanguine Strike', type: 'Attack', rarity: 'Common', cost: 1, effect: (p, e) => { e.takeDamage(8); p.heal(2); }, upgrade: (p, e) => { e.takeDamage(10); p.heal(3); } },
{ name: 'Hemorrhage', type: 'Attack', rarity: 'Common', cost: 1, effect: (p, e) => { e.takeDamage(5); e.addStatus('Bleed', 3); }, upgrade: (p, e) => { e.takeDamage(6); e.addStatus('Bleed', 4); } },
{ name: 'Bloodletting', type: 'Attack', rarity: 'Uncommon', cost: 1, effect: (p, e) => { p.takeDamage(3); e.takeDamage(12); }, upgrade: (p, e) => { p.takeDamage(2); e.takeDamage(15); } },
{ name: 'Crimson Eruption', type: 'Attack', rarity: 'Rare', cost: 2, effect: (p, enemies) => { enemies.forEach(en => en.takeDamage(8)); p.takeDamage(2); }, upgrade: (p, enemies) => { enemies.forEach(en => en.takeDamage(10)); p.takeDamage(2); }, allEnemies: true },
{ name: 'Crimson Shield', type: 'Skill', rarity: 'Common', cost: 1, effect: (p) => { p.gainBlock(6); p.takeDamage(2); }, upgrade: (p) => { p.gainBlock(8); p.takeDamage(2); } },
{ name: 'Blood Ward', type: 'Skill', rarity: 'Common', cost: 1, effect: (p) => { p.gainBlock(3); p.addStatus('Regeneration', 1); }, upgrade: (p) => { p.gainBlock(4); p.addStatus('Regeneration', 2); } },
{ name: 'Sacrificial Barrier', type: 'Skill', rarity: 'Uncommon', cost: 2, effect: (p) => { p.gainBlock(10); p.takeDamage(5); }, upgrade: (p) => { p.gainBlock(13); p.takeDamage(5); } },
{ name: 'Vital Surge', type: 'Skill', rarity: 'Rare', cost: 0, effect: (p) => { const missing = p.maxHp - p.hp; p.gainBlock(missing); }, upgrade: (p) => { const missing = p.maxHp - p.hp; p.gainBlock(missing + 3); }, exhaust: true },
{ name: 'Life Tap', type: 'Skill', rarity: 'Common', cost: 0, effect: (p) => { p.draw(2); p.takeDamage(3); }, upgrade: (p) => { p.draw(3); p.takeDamage(3); } },
{ name: 'Rend Flesh', type: 'Skill', rarity: 'Uncommon', cost: 1, effect: (p, enemies) => { enemies.forEach(en => en.addStatus('Bleed', 5)); p.takeDamage(2); }, upgrade: (p, enemies) => { enemies.forEach(en => en.addStatus('Bleed', 7)); p.takeDamage(2); }, allEnemies: true },
{ name: 'Soul Siphon', type: 'Power', rarity: 'Uncommon', cost: 2, effect: (p) => { p.powers.push('Soul Siphon'); }, upgrade: (p) => { p.powers.push('Soul Siphon'); }, permanent: true, costUpgrade: 1 },
{ name: 'Blood Pact', type: 'Power', rarity: 'Rare', cost: 1, effect: (p) => { p.powers.push('Blood Pact'); }, upgrade: (p) => { p.powers.push('Blood Pact'); }, permanent: true },
]
},
'Shadowmancer': {
startingDeck: ['Strike', 'Strike', 'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend', 'Defend', 'Shadow Bolt'],
pool: [
{ name: 'Shadow Bolt', type: 'Attack', rarity: 'Common', cost: 1, effect: (p, e) => { e.takeDamage(6); e.addStatus('Weak', 1); }, upgrade: (p, e) => { e.takeDamage(8); e.addStatus('Weak', 2); } },
{ name: 'Grasp of Despair', type: 'Attack', rarity: 'Common', cost: 1, effect: (p, e) => { e.takeDamage(4); e.addStatus('Vulnerable', 1); }, upgrade: (p, e) => { e.takeDamage(5); e.addStatus('Vulnerable', 2); } },
{ name: 'Nightmare Lash', type: 'Attack', rarity: 'Uncommon', cost: 2, effect: (p, e) => { let dmg = 7; if(e.hasStatus('Weak')) dmg += 7; e.takeDamage(dmg); }, upgrade: (p, e) => { let dmg = 9; if(e.hasStatus('Weak')) dmg += 9; e.takeDamage(dmg); } },
{ name: 'Eternal Darkness', type: 'Attack', rarity: 'Rare', cost: 2, effect: (p, enemies) => { for(let i=0; i<3; i++) { const target = enemies[Math.floor(Math.random()*enemies.length)]; target.takeDamage(3); } }, upgrade: (p, enemies) => { for(let i=0; i<3; i++) { const target = enemies[Math.floor(Math.random()*enemies.length)]; target.takeDamage(4); } }, allEnemies: true },
{ name: 'Cloak of Shadows', type: 'Skill', rarity: 'Common', cost: 1, effect: (p) => { p.gainBlock(5); p.draw(1); }, upgrade: (p) => { p.gainBlock(7); p.draw(1); } },
{ name: 'Haunting Mist', type: 'Skill', rarity: 'Common', cost: 1, effect: (p, enemies) => { p.gainBlock(7); enemies.forEach(en => en.addStatus('Weak', 1)); }, upgrade: (p, enemies) => { p.gainBlock(9); enemies.forEach(en => en.addStatus('Weak', 1)); }, allEnemies: true },
{ name: 'Phantom Armor', type: 'Skill', rarity: 'Uncommon', cost: 1, effect: (p) => { p.gainBlock(8); p.deferredBuffs.push({type: 'Dexterity', value: 2, trigger: 'nextTurn'}); }, upgrade: (p) => { p.gainBlock(10); p.deferredBuffs.push({type: 'Dexterity', value: 3, trigger: 'nextTurn'}); } },
{ name: 'Whispering Shroud', type: 'Skill', rarity: 'Rare', cost: 1, effect: (p) => { p.gainBlock(4); p.deferredBuffs.push({type: 'Block', value: 4, trigger: 'nextTurn'}); }, upgrade: (p) => { p.gainBlock(5); p.deferredBuffs.push({type: 'Block', value: 5, trigger: 'nextTurn'}); } },
{ name: 'Shadowform', type: 'Skill', rarity: 'Uncommon', cost: 2, effect: (p) => { p.addStatus('Intangible', 2); }, upgrade: (p) => { p.addStatus('Intangible', 3); }, exhaust: true },
{ name: 'Curse of Misfortune', type: 'Skill', rarity: 'Common', cost: 1, effect: (p, e) => { e.tempStatMod('Strength', -3); e.addStatus('Vulnerable', 1); }, upgrade: (p, e) => { e.tempStatMod('Strength', -4); e.addStatus('Vulnerable', 2); } },
{ name: 'Dark Ritual', type: 'Skill', rarity: 'Uncommon', cost: 0, effect: (p) => { p.gainEnergy(2); p.addToDeck('Dazed'); }, upgrade: (p) => { p.gainEnergy(2); p.addToDeck('Dazed'); } },
{ name: 'Umbral Mastery', type: 'Power', rarity: 'Rare', cost: 2, effect: (p) => { p.powers.push('Umbral Mastery'); }, upgrade: (p) => { p.powers.push('Umbral Mastery'); }, permanent: true },
]
},
'Hellforged': {
startingDeck: ['Strike', 'Strike', 'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend', 'Defend', 'Firebolt'],
pool: [
{ name: 'Firebolt', type: 'Attack', rarity: 'Common', cost: 1, effect: (p, e) => { e.takeDamage(7); e.addStatus('Burn', 2); }, upgrade: (p, e) => { e.takeDamage(9); e.addStatus('Burn', 3); } },
{ name: 'Hellfire Blast', type: 'Attack', rarity: 'Uncommon', cost: 2, effect: (p, enemies) => { enemies.forEach(en => en.takeDamage(9)); }, upgrade: (p, enemies) => { enemies.forEach(en => en.takeDamage(12)); }, allEnemies: true },
{ name: 'Searing Strike', type: 'Attack', rarity: 'Common', cost: 1, effect: (p, e) => { e.takeDamage(6); p.deferredBuffs.push({type: 'Energy', value: 1, trigger: 'nextTurn'}); }, upgrade: (p, e) => { e.takeDamage(8); p.deferredBuffs.push({type: 'Energy', value: 1, trigger: 'nextTurn'}); } },
{ name: 'Immolate', type: 'Attack', rarity: 'Rare', cost: 2, effect: (p, e) => { e.takeDamage(12); }, upgrade: (p, e) => { e.takeDamage(16); }, exhaust: true },
{ name: 'Obsidian Skin', type: 'Skill', rarity: 'Common', cost: 1, effect: (p) => { p.gainBlock(7); }, upgrade: (p) => { p.gainBlock(10); } },
{ name: 'Flame Ward', type: 'Skill', rarity: 'Common', cost: 1, effect: (p, enemies) => { p.gainBlock(5); const e = enemies[Math.floor(Math.random()*enemies.length)]; e.addStatus('Burn', 2); }, upgrade: (p, enemies) => { p.gainBlock(6); const e = enemies[Math.floor(Math.random()*enemies.length)]; e.addStatus('Burn', 3); }, allEnemies: true },
{ name: 'Demonic Resilience', type: 'Skill', rarity: 'Uncommon', cost: 1, effect: (p) => { p.gainBlock(4); p.heal(2); }, upgrade: (p) => { p.gainBlock(5); p.heal(3); } },
{ name: 'Brimstone Shield', type: 'Skill', rarity: 'Rare', cost: 1, effect: (p) => { p.gainBlock(3); p.tempStatMod('Strength', 2); }, upgrade: (p) => { p.gainBlock(4); p.tempStatMod('Strength', 3); } },
{ name: 'Summon Imp', type: 'Skill', rarity: 'Common', cost: 1, effect: (p) => { p.addToHand('Imp Claw'); }, upgrade: (p) => { p.addToHand('Imp Claw+'); }, exhaust: true },
{ name: 'Infernal Pact', type: 'Skill', rarity: 'Uncommon', cost: 0, effect: (p) => { p.takeDamage(2); p.gainEnergy(2); p.addToHand('Imp Claw'); }, upgrade: (p) => { p.takeDamage(1); p.gainEnergy(2); p.addToHand('Imp Claw+'); } },
{ name: 'Hellfire Aura', type: 'Power', rarity: 'Rare', cost: 2, effect: (p) => { p.powers.push('Hellfire Aura'); }, upgrade: (p) => { p.powers.push('Hellfire Aura'); }, permanent: true },
{ name: 'Soul Burn', type: 'Skill', rarity: 'Uncommon', cost: 1, effect: (p, e) => { e.addStatus('Burn', 8); p.draw(1); }, upgrade: (p, e) => { e.addStatus('Burn', 10); p.draw(1); } },
]
}
},
BASICS: {
'Strike': { name: 'Strike', type: 'Attack', rarity: 'Basic', cost: 1, effect: (p, e) => { e.takeDamage(6); }, upgrade: (p, e) => { e.takeDamage(8); } },
'Defend': { name: 'Defend', type: 'Skill', rarity: 'Basic', cost: 1, effect: (p) => { p.gainBlock(5); }, upgrade: (p) => { p.gainBlock(8); } },
},
COLORLESS: [
{ name: 'Flash of Insight', type: 'Skill', rarity: 'Common', cost: 1, effect: (p) => { p.draw(2); }, upgrade: (p) => { p.draw(3); } },
{ name: 'Iron Will', type: 'Skill', rarity: 'Common', cost: 1, effect: (p) => { p.gainBlock(8); }, upgrade: (p) => { p.gainBlock(11); } },
{ name: 'Quick Slash', type: 'Attack', rarity: 'Common', cost: 1, effect: (p, e) => { e.takeDamage(7); }, upgrade: (p, e) => { e.takeDamage(9); } },
{ name: 'Potent Brew', type: 'Skill', rarity: 'Uncommon', cost: 0, effect: (p) => { p.gainEnergy(1); }, upgrade: (p) => { p.gainEnergy(2); }, exhaust: true },
],
CURSES: {
'Wound': { name: 'Wound', type: 'Curse', cost: 0, effect: () => {}, unplayable: true, ethereal: true, exhaust: true },
'Doubt': { name: 'Doubt', type: 'Curse', cost: 0, effect: (p) => { p.takeDamage(1); }, unplayable: true },
'Dazed': { name: 'Dazed', type: 'Status', cost: 0, effect: () => {}, unplayable: true, ethereal: true, exhaust: true },
},
TOKENS: {
'Imp Claw': { name: 'Imp Claw', type: 'Attack', cost: 0, effect: (p, e) => { e.takeDamage(4); }, exhaust: true },
'Imp Claw+': { name: 'Imp Claw+', type: 'Attack', cost: 0, effect: (p, e) => { e.takeDamage(6); }, exhaust: true },
},
ACTS: [
{
name: 'The Crypts',
enemies: {
'Skeleton Warrior': { hp: 22, sprite: '💀', pattern: [ {type: 'Attack', value: 6}, {type: 'Attack', value: 6}, {type: 'Defend', value: 8} ] },
'Zombie Brute': { hp: 28, sprite: '🧟', pattern: [ {type: 'Attack', value: 8}, {type: 'Debuff', status: 'Weak', value: 2}, {type: 'Attack', value: 8} ] },
'Dark Cultist': { hp: 20, sprite: '🧙', pattern: [ {type: 'Buff', stat: 'Strength', value: 3}, {type: 'Attack', value: 5}, {type: 'Attack', value: 5} ] },
'Rat Swarm': { hp: 15, sprite: '🐀', pattern: [ {type: 'Multi', count: 2, value: 3}, {type: 'Multi', count: 2, value: 3} ] },
'Tomb Guardian': { hp: 42, sprite: '🛡️', pattern: [ {type: 'Combo', attack: 6, block: 8}, {type: 'Multi', count: 3, value: 4}, {type: 'Attack', value: 12} ] },
'Blood Witch': { hp: 36, sprite: '🩸', pattern: [ {type: 'Drain', attack: 5, heal: 4}, {type: 'Debuff', status: 'Vulnerable', value: 1, status2: 'Weak', value2: 1}, {type: 'Drain', attack: 7, heal: 4} ] },
'Lich King': { hp: 65, sprite: '👑', isBoss: true }
}
}
// Acts 2 and 3 would follow similarly. For the MVP, we'll iterate on Act 1.
]
};
/** GAME ENGINE **/
class Game {
constructor() {
this.state = 'START';
this.player = null;
this.currentAct = 0;
this.currentFloor = 1;
this.map = [];
this.combat = null;
}
selectClass(className) {
const classData = DATA.CLASSES[className];
this.player = new Player(className, classData);
this.state = 'MAP';
this.currentAct = 0;
this.currentFloor = 1;
this.generateMap();
this.showScreen('screen-map');
this.updateMapUI();
}
generateMap() {
this.map = [];
const layout = [
{ type: 'START' }, // 1
{ type: 'Enemy', branch: true }, // 2
{ type: 'Enemy' }, // 3
{ type: 'Event' }, // 4
{ type: 'Shop' }, // 5
{ type: 'Enemy' }, // 6
{ type: 'Rest' }, // 7
{ type: 'Enemy', branch: true }, // 8
{ type: 'Elite', branch: true }, // 9
{ type: 'Event' }, // 10
{ type: 'Enemy' }, // 11
{ type: 'Elite', branch: true }, // 12
{ type: 'Enemy' }, // 13
{ type: 'Rest' }, // 14
{ type: 'Boss' } // 15
];
for(let i=0; i<layout.length; i++) {
this.map.push({ ...layout[i], floor: i + 1 });
}
this.drawMap();
}
drawMap() {
const canvas = document.getElementById('map-canvas');
const ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 800;
ctx.clearRect(0,0,600,800);
const nodes = this.map;
const col = 300;
const rowSpacing = 50;
const startY = 750;
// Draw paths
ctx.strokeStyle = '#555';
ctx.lineWidth = 2;
ctx.beginPath();
for(let i=0; i < nodes.length - 1; i++) {
ctx.moveTo(col, startY - i * rowSpacing);
ctx.lineTo(col, startY - (i+1) * rowSpacing);
}
ctx.stroke();
// Draw nodes
nodes.forEach((node, i) => {
const x = col;
const y = startY - i * rowSpacing;
// Highlight the current floor and previous floors
ctx.fillStyle = (node.floor <= this.currentFloor) ? varColor('--accent-color') : '#333';
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI*2);
ctx.fill();
// Edge highlight for the floor the player is currently ON
if(node.floor === this.currentFloor) {
ctx.strokeStyle = 'white';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(x, y, 13, 0, Math.PI*2);
ctx.stroke();
}
ctx.fillStyle = 'white';
ctx.font = '12px Cinzel';
ctx.textAlign = 'center';
ctx.fillText(node.type, x + 20, y + 5);
});
}
updateMapUI() {
document.getElementById('map-act').innerText = this.currentAct + 1;
document.getElementById('map-act-name').innerText = DATA.ACTS[0].name;
document.getElementById('map-gold').innerText = this.player.gold;
document.getElementById('map-hp').innerText = this.player.hp;
document.getElementById('map-max-hp').innerText = this.player.maxHp;
}
handleMapClick(e) {
const rect = document.getElementById('map-canvas').getBoundingClientRect();
const y = e.clientY - rect.top;
const rowSpacing = 50;
const startY = 750;
const floorIdx = Math.round((startY - y) / rowSpacing);
if(floorIdx === this.currentFloor) {
this.enterNode(this.map[floorIdx]);
}
}
enterNode(node) {
this.currentFloor = node.floor;
if(node.type === 'Enemy' || node.type === 'Elite' || node.type === 'Boss') {
this.startCombat(node.type);
} else if(node.type === 'Shop') {
this.openShop();
} else if(node.type === 'Rest') {
this.openRest();
} else if(node.type === 'Event') {
this.openEvent();
} else if(node.type === 'START') {
// do nothing
}
}
startCombat(type) {
this.state = 'COMBAT';
this.combat = new Combat(this.player, type);
this.showScreen('screen-combat');
this.combat.init();
this.combat.updateUI();
}
endTurn() {
this.combat.endPlayerTurn();
}
showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
skipReward() {
this.state = 'MAP';
this.showScreen('screen-map');
this.updateMapUI();
}
// Modal functions
openModal(title, text, options) {
document.getElementById('modal-title').innerText = title;
document.getElementById('modal-text').innerText = text;
const opts = document.getElementById('modal-options');
opts.innerHTML = '';
options.forEach(opt => {
const b = document.createElement('button');
b.className = 'btn';
b.innerText = opt.text;
b.onclick = () => {
opt.action();
document.getElementById('modal-overlay').style.display = 'none';
};
opts.appendChild(b);
});
document.getElementById('modal-overlay').style.display = 'flex';
}
openShop() {
this.openModal('The Dark Merchant', 'Trade your gold for forbidden power.', [
{ text: 'Browse Wares', action: () => {
// simplified shop for now
this.player.gold -= 50;
this.player.addRandomCard();
this.updateMapUI();
}},
{ text: 'Leave', action: () => {} }
]);
}
openRest() {
this.openModal('Rest Site', 'A moment of respite in the abyss.', [
{ text: 'Heal 17 HP', action: () => { this.player.heal(17); this.updateMapUI(); }},
{ text: 'Leave', action: () => {} }
]);
}
openEvent() {
this.openModal('Strange Occurrence', 'You find a blood-stained altar.', [
{ text: 'Offer 15 HP (Get Relic)', action: () => { this.player.takeDamage(15); this.updateMapUI(); }},
{ text: 'Leave', action: () => {} }
]);
}
}
class Player {
constructor(className, classData) {
this.className = className;
this.maxHp = 70;
this.hp = 70;
this.gold = 99;
this.energy = 3;
this.maxEnergy = 3;
this.strength = 0;
this.dexterity = 0;
this.block = 0;
this.statuses = {}; // name: value
this.powers = [];
this.deferredBuffs = [];
this.relics = [];
this.potions = [];
this.deck = [];
this.hand = [];
this.drawPile = [];
this.discardPile = [];
// Initial deck
classData.startingDeck.forEach(cardName => {
this.deck.push(this.createCard(cardName));
});
}
createCard(name) {
let cardDef = DATA.BASICS[name] || DATA.CLASSES[this.className]?.pool.find(c => c.name === name) || DATA.CURSES[name] || DATA.TOKENS[name];
if(!cardDef) return null;
return { ...cardDef, upgraded: false, id: Math.random().toString(36).substr(2, 9) };
}
addRandomCard() {
const pool = DATA.CLASSES[this.className].pool;
const card = this.createCard(pool[Math.floor(Math.random()*pool.length)].name);
this.deck.push(card);
}
addToDeck(name) {
this.drawPile.push(this.createCard(name));
}
addToHand(name) {
this.hand.push(this.createCard(name));
}
draw(count) {
for(let i=0; i<count; i++) {
if(this.drawPile.length === 0) {
if(this.discardPile.length === 0) break;
this.drawPile = [...this.discardPile].sort(() => Math.random() - 0.5);
this.discardPile = [];
}
this.hand.push(this.drawPile.pop());
}
}
gainBlock(val) {
const bonus = this.statuses['Dexterity'] || 0;
this.block += (val + bonus);
}
takeDamage(val) {
if(this.statuses['Intangible']) {
val = 1;
}
this.hp -= val;
if(this.hp < 0) this.hp = 0;
}
heal(val) {
this.hp = Math.min(this.maxHp, this.hp + val);
}
addStatus(name, val) {
this.statuses[name] = (this.statuses[name] || 0) + val;
}
hasStatus(name) {
return this.statuses[name] && this.statuses[name] > 0;
}
tempStatMod(stat, val) {
this[stat.toLowerCase()] += val;
}
gainEnergy(val) {
this.energy += val;
}
}
class Enemy {
constructor(name, data) {
this.name = name;
this.maxHp = data.hp;
this.hp = data.hp;
this.sprite = data.sprite;
this.block = 0;
this.strength = 0;
this.statuses = {};
this.pattern = data.pattern || [];
this.turnCount = 0;
}
takeDamage(val) {
if(this.block >= val) {
this.block -= val;
} else {
const remaining = val - this.block;
this.block = 0;
this.hp -= remaining;
}
}
addStatus(name, val) {
this.statuses[name] = (this.statuses[name] || 0) + val;
}
hasStatus(name) {
return this.statuses[name] && this.statuses[name] > 0;
}
}
class Combat {
constructor(player, type) {
this.player = player;
this.type = type;
this.enemies = [];
this.turn = 0;
this.isPlayerTurn = true;
}
init() {
const act = DATA.ACTS[0];
if(this.type === 'Boss') {
this.enemies.push(new Enemy('Lich King', act.enemies['Lich King']));
} else if(this.type === 'Elite') {
this.enemies.push(new Enemy('Tomb Guardian', act.enemies['Tomb Guardian']));
} else {
this.enemies.push(new Enemy('Skeleton Warrior', act.enemies['Skeleton Warrior']));
}
this.player.drawPile = [...this.player.deck].sort(() => Math.random() - 0.5);
this.player.discardPile = [];
this.player.hand = [];
this.startTurn();
}
startTurn() {
this.turn++;
this.isPlayerTurn = true;
this.player.energy = this.player.maxEnergy;
this.player.block = 0;
// Apply start-of-turn buffs
if(this.player.statuses['Regeneration']) {
this.player.heal(this.player.statuses['Regeneration']);
this.player.statuses['Regeneration']--;
}
this.player.draw(5);
this.updateUI();
}
playCard(cardId, targetIdx) {
if(!this.isPlayerTurn) return;
const cardIdx = this.player.hand.findIndex(c => c.id === cardId);
const card = this.player.hand[cardIdx];
if(this.player.energy < card.cost) return;
this.player.energy -= card.cost;
const target = this.enemies[targetIdx];
const effectFn = card.upgraded ? card.upgrade : card.effect;
if(card.allEnemies) {
effectFn(this.player, this.enemies);
} else {
effectFn(this.player, target);
}
if(card.exhaust) {
// remove from combat
} else {
this.player.discardPile.push(card);
}
this.player.hand.splice(cardIdx, 1);
this.updateUI();
this.checkWinLoss();
}
endPlayerTurn() {
this.isPlayerTurn = false;
this.player.discardPile.push(...this.player.hand);
this.player.hand = [];
this.updateUI();
this.enemyTurn();
}
async enemyTurn() {
for(const enemy of this.enemies) {
if(enemy.hp <= 0) continue;
const intent = this.getIntent(enemy);
this.showIntent(enemy, intent);
await new Promise(r => setTimeout(r, 600));
this.executeIntent(enemy, intent);
this.updateUI();
await new Promise(r => setTimeout(r, 600));
}
this.startTurn();
}
getIntent(enemy) {
if(enemy.pattern.length === 0) return {type: 'Attack', value: 10};
return enemy.pattern[enemy.turnCount % enemy.pattern.length];
}
showIntent(enemy, intent) {
const entityDiv = Array.from(document.querySelectorAll('.entity')).find(div => div.innerText.includes(enemy.name));
if(!entityDiv) return;
const icon = entityDiv.querySelector('.intent-icon');
if(!icon) return;
let text = '';
switch(intent.type) {
case 'Attack': text = `⚔️ ${intent.value}`; break;
case 'Defend': text = `🛡️ ${intent.value}`; break;
case 'Debuff': text = `🪄 ${intent.status}`; break;
case 'Multi': text = `⚔️x${intent.count}`; break;
case 'Combo': text = `⚔️🛡️`; break;
case 'Drain': text = `🩸`; break;
default: text = '❓';
}
icon.innerText = text;
}
executeIntent(enemy, intent) {
const p = this.player;
switch(intent.type) {
case 'Attack':
p.takeDamage(intent.value + enemy.strength);
break;
case 'Defend':
enemy.block += intent.value;
break;
case 'Debuff':
p.addStatus(intent.status, intent.value);
break;
case 'Multi':
for(let i=0; i<intent.count; i++) p.takeDamage(intent.value);
break;
case 'Combo':
p.takeDamage(intent.attack);
enemy.block += intent.block;
break;
case 'Drain':
p.takeDamage(intent.attack);
enemy.heal ? enemy.heal(intent.heal) : enemy.hp += intent.heal;
break;
}
enemy.turnCount++;
}
checkWinLoss() {
if(this.enemies.every(e => e.hp <= 0)) {
game.showScreen('screen-reward');
this.player.gold += 20;
return true;
}
if(this.player.hp <= 0) {
document.getElementById('end-title').innerText = 'You Perished';
document.getElementById('end-msg').innerText = 'The abyss consumes another soul.';
game.showScreen('screen-end');
return true;
}
return false;
}
updateUI() {
const playerEntity = document.getElementById('player-entity');
document.getElementById('player-hp-fill').style.width = (this.player.hp / this.player.maxHp * 100) + '%';
document.getElementById('player-hp-text').innerText = `${this.player.hp}/${this.player.maxHp}`;
document.getElementById('player-block-fill').style.width = (this.player.block > 0 ? 100 : 0) + '%';
document.getElementById('player-block-text').innerText = this.player.block;
document.getElementById('player-energy').innerText = this.player.energy;
document.getElementById('deck-count').innerText = this.player.drawPile.length + this.player.discardPile.length;
const statusDiv = document.getElementById('player-statuses');
statusDiv.innerHTML = '';
for(let s in this.player.statuses) {
if(this.player.statuses[s] > 0) {
const pill = document.createElement('div');
pill.className = 'status-pill';
pill.innerText = `${s} ${this.player.statuses[s]}`;
statusDiv.appendChild(pill);
}
}
const enemyRow = document.getElementById('enemy-row');
enemyRow.innerHTML = '';
this.enemies.forEach((e, idx) => {
const intent = this.getIntent(e);
let intentText = '❓';
switch(intent.type) {
case 'Attack': intentText = `⚔️ ${intent.value}`; break;
case 'Defend': intentText = `🛡️ ${intent.value}`; break;
case 'Debuff': intentText = `🪄 ${intent.status}`; break;
case 'Multi': intentText = `⚔️x${intent.count}`; break;
case 'Combo': intentText = `⚔️🛡️`; break;
case 'Drain': intentText = `🩸`; break;
}
const div = document.createElement('div');
div.className = 'entity';
div.innerHTML = `
<div class="intent-icon">${intentText}</div>
<div class="entity-sprite">${e.sprite}</div>
<div class="stat-bar"><div class="stat-fill hp-fill" style="width:${(e.hp/e.maxHp)*100}%"></div><div class="stat-text">${e.hp}/${e.maxHp}</div></div>
<div class="stat-bar"><div class="stat-fill block-fill" style="width:${e.block > 0 ? 100 : 0}%"></div><div class="stat-text">${e.block}</div></div>
<div style="margin-top: 10px; font-weight: bold;">${e.name}</div>
`;
enemyRow.appendChild(div);
});
const handDiv = document.getElementById('hand');
handDiv.innerHTML = '';
this.player.hand.forEach(card => {
const cDiv = document.createElement('div');
cDiv.className = `card ${this.player.energy >= card.cost ? 'playable' : 'unplayable'} card-rarity-${card.rarity.toLowerCase()}`;
cDiv.innerHTML = `
<div class="card-cost">${card.cost}</div>
<div class="card-name">${card.name}</div>
<div class="card-desc">${card.type}</div>
`;
cDiv.onclick = () => this.playCard(card.id, 0);
handDiv.appendChild(cDiv);
});
}
}
function varColor(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
const game = new Game();
// Map click listener
document.getElementById('map-canvas').onclick = (e) => game.handleMapClick(e);
</script>
</body>
</html>
<!-- agent-meta {"model":"gemma-4-31b","provider":"cerebras","persona":"full","sessionId":"0361acf7-0ce6-4c9f-9926-f57b13c300eb","tokensIn":194800,"tokensOut":16282,"tokensTotal":211082,"turns":7,"toolCalls":6,"failedToolCalls":0,"timestamp":"2026-07-14T16:28:26.499Z"} -->