Shadows
Gemma 4 31b · composer

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 33,788 bytes · SHA-256 a3023054feb6
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shadows of the Abyss</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.min.js"></script>
<style>
/* ── css: layout ── */
body {
margin: 0;
padding: 0;
background: #0a0a0a;
color: #e0e0e0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
user-select: none;
}
#game-container {
width: 100vw;
height: 100vh;
position: relative;
}
.screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease;
}
.hidden {
display: none !important;
opacity: 0;
}
h1 {
font-size: 4rem;
color: #8b0000;
text-shadow: 0 0 20px rgba(139, 0, 0, 0.5);
margin-bottom: 1rem;
}
.class-selection {
display: flex;
gap: 20px;
margin-top: 2rem;
}
.class-card {
background: #1a1a1a;
border: 2px solid #333;
padding: 2rem;
width: 250px;
text-align: center;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
border-radius: 10px;
}
.class-card:hover {
transform: translateY(-10px);
border-color: #8b0000;
}
.class-card button {
background: #8b0000;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
font-size: 1.1rem;
margin-top: 1rem;
}
#map-header {
position: absolute;
top: 20px;
width: 90%;
display: flex;
justify-content: space-between;
font-size: 1.5rem;
font-weight: bold;
pointer-events: none;
}
#map-canvas-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#map-canvas {
background: #111;
box-shadow: 0 0 50px rgba(0,0,0,0.8);
}
#combat-hud {
position: absolute;
top: 0;
width: 100%;
height: 60%;
display: flex;
justify-content: space-around;
align-items: center;
padding: 0 50px;
}
.stat-bar {
width: 200px;
height: 24px;
background: #333;
border: 2px solid #555;
position: relative;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: #c00;
width: 100%;
transition: width 0.3s ease;
}
#player-hp-text {
position: absolute;
width: 100%;
text-align: center;
font-weight: bold;
font-size: 14px;
line-height: 24px;
color: white;
text-shadow: 1px 1px 2px black;
}
.block-value {
font-size: 2rem;
color: #4a9eff;
font-weight: bold;
text-align: center;
}
#player-energy {
font-size: 1.5rem;
text-align: center;
}
#enemy-container {
display: flex;
gap: 30px;
}
.enemy-unit {
text-align: center;
width: 180px;
}
.enemy-hp-bar {
width: 100%;
height: 15px;
background: #333;
border: 1px solid #777;
margin-bottom: 5px;
}
.enemy-hp-fill {
height: 100%;
background: #a00;
transition: width 0.3s ease;
}
.enemy-intent {
background: #222;
border: 1px solid #444;
padding: 5px;
margin-bottom: 10px;
font-size: 0.9rem;
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
#combat-area {
position: absolute;
bottom: 0;
width: 100%;
height: 40%;
background: linear-gradient(to top, #000, transparent);
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 50px;
}
#hand {
display: flex;
gap: 10px;
justify-content: center;
align-items: flex-end;
}
.card {
width: 140px;
height: 200px;
background: #2a2a2a;
border: 2px solid #666;
border-radius: 8px;
padding: 10px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
justify-content: space-between;
text-align: center;
font-size: 0.85rem;
}
.card:hover {
transform: translateY(-20px) scale(1.05);
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
}
.card.unplayable {
opacity: 0.6;
cursor: not-allowed;
filter: grayscale(1);
}
.card-cost {
position: absolute;
top: -10px;
left: -10px;
width: 30px;
height: 30px;
background: #4a9eff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
border: 2px solid white;
}
.card-title {
font-weight: bold;
font-size: 1rem;
margin-bottom: 10px;
}
.card-desc {
font-style: italic;
font-size: 0.8rem;
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
}
#end-turn-btn {
position: absolute;
right: 50px;
bottom: 80px;
padding: 15px 30px;
background: #444;
color: white;
border: 2px solid #888;
cursor: pointer;
font-size: 1.2rem;
}
#end-turn-btn:hover {
background: #666;
}
#reward-cards, #shop-items {
display: flex;
gap: 20px;
margin: 2rem;
justify-content: center;
}
.reward-option {
background: #222;
border: 2px solid #444;
padding: 1rem;
text-align: center;
cursor: pointer;
width: 160px;
transition: border-color 0.2s;
}
.reward-option:hover {
border-color: #8b0000;
}
#event-content {
background: #1a1a1a;
padding: 2rem;
border: 2px solid #333;
max-width: 600px;
text-align: center;
border-radius: 15px;
}
#event-choices {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 2rem;
}
.choice-btn {
padding: 10px;
background: #333;
color: white;
border: 1px solid #555;
cursor: pointer;
}
.choice-btn:hover {
background: #444;
}
#shop-header {
display: flex;
gap: 20px;
align-items: center;
margin-bottom: 1rem;
}
.rest-options {
display: flex;
flex-direction: column;
gap: 20px;
}
.rest-options button {
padding: 20px;
font-size: 1.2rem;
background: #333;
color: white;
border: 2px solid #555;
cursor: pointer;
}
</style>
</head>
<body>
<div id="game-container">
<div id="ui-layer">
<div id="screen-start" class="screen">
<h1>Shadows of the Abyss</h1>
<p>Choose your cursed archetype</p>
<div class="class-selection">
<div class="class-card" id="class-blood">
<h2>Blood Reaper</h2>
<p>Sacrifice HP for power and life steal.</p>
<button onclick="game.selectClass('blood')">Select</button>
</div>
<div class="class-card" id="class-shadow">
<h2>Shadowmancer</h2>
<p>Manipulate shadows, debuffs, and intangibility.</p>
<button onclick="game.selectClass('shadow')">Select</button>
</div>
<div class="class-card" id="class-hell">
<h2>Hellforged</h2>
<p>Wield fire, burn enemies, and summon imps.</p>
<button onclick="game.selectClass('hell')">Select</button>
</div>
</div>
</div>
<div id="screen-map" class="screen hidden">
<div id="map-header">
<span id="map-act">Act 1: The Crypts</span>
<span id="map-gold">Gold: 99</span>
</div>
<div id="map-canvas-container">
<canvas id="map-canvas"></canvas>
</div>
<div id="map-controls">
<p id="map-node-info">Select a node to proceed</p>
</div>
</div>
<div id="screen-combat" class="screen hidden">
<div id="combat-hud">
<div id="player-stats">
<div class="stat-bar">
<div id="player-hp-fill" class="bar-fill"></div>
<span id="player-hp-text">70 / 70</span>
</div>
<div id="player-block" class="block-value">0</div>
<div id="player-energy">Energy: 3 / 3</div>
</div>
<div id="enemy-side">
<div id="enemy-container"></div>
</div>
</div>
<div id="combat-area">
<div id="hand"></div>
<button id="end-turn-btn">End Turn</button>
</div>
</div>
<div id="screen-reward" class="screen hidden">
<h2 id="reward-title">Victory!</h2>
<div id="reward-cards"></div>
<div id="reward-potions"></div>
<div id="reward-relics"></div>
<button id="reward-next" class="hidden">Continue</button>
</div>
<div id="screen-event" class="screen hidden">
<div id="event-content">
<h2 id="event-title">Event</h2>
<p id="event-text"></p>
<div id="event-choices"></div>
</div>
</div>
<div id="screen-shop" class="screen hidden">
<h2>The Cursed Shop</h2>
<div id="shop-header">
<span id="shop-gold">Gold: 99</span>
<button id="shop-remove-card">Remove Card (75g)</button>
</div>
<div id="shop-items"></div>
<button id="shop-exit">Leave Shop</button>
</div>
<div id="screen-rest" class="screen hidden">
<h2>Rest Site</h2>
<div class="rest-options">
<button onclick="game.restAction('heal')">Heal (25% Max HP)</button>
<button onclick="game.restAction('upgrade')">Forge (Upgrade Card)</button>
<button onclick="game.restAction('remove')">Meditate (Remove Card)</button>
</div>
</div>
<div id="screen-gameover" class="screen hidden">
<h1>YOU DIED</h1>
<p>Your soul is lost to the abyss.</p>
<button onclick="location.reload()">Try Again</button>
</div>
<div id="screen-victory" class="screen hidden">
<h1>VICTORY</h1>
<p>You have emerged from the Abyssal Spire!</p>
<button onclick="location.reload()">Play Again</button>
</div>
</div>
</div>
<script>
'use strict';
// ── const: CARD_DATA ──
const CARD_DATA = {
basic: {
Strike: { cost: 1, type: 'Attack', desc: 'Deal 6 damage', upgrade: { desc: 'Deal 8 damage' } },
Defend: { cost: 1, type: 'Skill', desc: 'Gain 5 Block', upgrade: { desc: 'Gain 8 Block' } },
},
blood: {
SanguineStrike: { cost: 1, type: 'Attack', desc: 'Deal 8 damage. Heal 2 HP.', upgrade: { desc: 'Deal 10 damage. Heal 3 HP.' } },
Hemorrhage: { cost: 1, type: 'Attack', desc: 'Deal 5 damage. Apply 3 Bleed.', upgrade: { desc: 'Deal 6 damage. Apply 4 Bleed.' } },
Bloodletting: { cost: 1, type: 'Attack', desc: 'Lose 3 HP. Deal 12 damage.', upgrade: { desc: 'Lose 2 HP. Deal 15 damage.' } },
CrimsonEruption: { cost: 2, type: 'Attack', desc: 'Deal 8 damage to ALL. Lose 2 HP.', upgrade: { desc: 'Deal 10 damage to ALL. Lose 2 HP.' } },
CrimsonShield: { cost: 1, type: 'Skill', desc: 'Gain 6 Block. Lose 2 HP.', upgrade: { desc: 'Gain 8 Block. Lose 2 HP.' } },
BloodWard: { cost: 1, type: 'Skill', desc: 'Gain 3 Block. Gain 1 Regen.', upgrade: { desc: 'Gain 4 Block. Gain 2 Regen.' } },
SacrificialBarrier: { cost: 2, type: 'Skill', desc: 'Gain 10 Block. Lose 5 HP.', upgrade: { desc: 'Gain 13 Block. Lose 5 HP.' } },
VitalSurge: { cost: 0, type: 'Skill', desc: 'Gain Block equal to missing HP. Exhaust.', upgrade: { desc: 'Gain Block equal to missing HP + 3. Exhaust.' } },
LifeTap: { cost: 0, type: 'Skill', desc: 'Draw 2 cards. Lose 3 HP.', upgrade: { desc: 'Draw 3 cards. Lose 3 HP.' } },
RendFlesh: { cost: 1, type: 'Skill', desc: 'Apply 5 Bleed to ALL. Lose 2 HP.', upgrade: { desc: 'Apply 7 Bleed to ALL. Lose 2 HP.' } },
SoulSiphon: { cost: 2, type: 'Power', desc: 'Start of turn: Gain 1 Energy, lose 1 HP.', upgrade: { cost: 1, desc: 'Start of turn: Gain 1 Energy, lose 1 HP.' } },
BloodPact: { cost: 1, type: 'Power', desc: 'Losing HP from cards gains 1 Strength.', upgrade: { desc: 'Losing HP from cards gains 2 Strength.' } },
},
shadow: {
ShadowBolt: { cost: 1, type: 'Attack', desc: 'Deal 6 damage. Apply 1 Weak.', upgrade: { desc: 'Deal 8 damage. Apply 2 Weak.' } },
GraspOfDespair: { cost: 1, type: 'Attack', desc: 'Deal 4 damage. Apply 1 Vulnerable.', upgrade: { desc: 'Deal 5 damage. Apply 2 Vulnerable.' } },
NightmareLash: { cost: 2, type: 'Attack', desc: 'Deal 7 damage. +7 if Weak.', upgrade: { desc: 'Deal 9 damage. +9 if Weak.' } },
EternalDarkness: { cost: 2, type: 'Attack', desc: 'Deal 3 damage 3 times to random.', upgrade: { desc: 'Deal 4 damage 3 times to random.' } },
CloakOfShadows: { cost: 1, type: 'Skill', desc: 'Gain 5 Block. Draw 1.', upgrade: { desc: 'Gain 7 Block. Draw 1.' } },
HauntingMist: { cost: 1, type: 'Skill', desc: 'Gain 7 Block. All enemies Weak 1.', upgrade: { desc: 'Gain 9 Block. All enemies Weak 1.' } },
PhantomArmor: { cost: 1, type: 'Skill', desc: 'Gain 8 Block. Next turn +2 Dex.', upgrade: { desc: 'Gain 10 Block. Next turn +3 Dex.' } },
WhisperingShroud: { cost: 1, type: 'Skill', desc: 'Gain 4 Block. Next turn +4 Block.', upgrade: { desc: 'Gain 5 Block. Next turn +5 Block.' } },
Shadowform: { cost: 2, type: 'Skill', desc: 'Gain 2 Intangible. Exhaust.', upgrade: { desc: 'Gain 3 Intangible. Exhaust.' } },
CurseOfMisfortune: { cost: 1, type: 'Skill', desc: 'Enemy loses 3 Str this turn. 1 Vuln.', upgrade: { desc: 'Enemy loses 4 Str this turn. 2 Vuln.' } },
DarkRitual: { cost: 0, type: 'Skill', desc: 'Gain 2 Energy. Shuffle Dazed into Draw.', upgrade: { desc: 'Gain 2 Energy. Shuffle Dazed into Discard.' } },
UmbralMastery: { cost: 2, type: 'Power', desc: 'Start of turn: All enemies Weak 1.', upgrade: { desc: 'Start of turn: All enemies Weak 2.' } },
},
hell: {
Firebolt: { cost: 1, type: 'Attack', desc: 'Deal 7 damage. Apply 2 Burn.', upgrade: { desc: 'Deal 9 damage. Apply 3 Burn.' } },
HellfireBlast: { cost: 2, type: 'Attack', desc: 'Deal 9 damage to ALL.', upgrade: { desc: 'Deal 12 damage to ALL.' } },
SearingStrike: { cost: 1, type: 'Attack', desc: 'Deal 6 damage. +1 Energy next turn.', upgrade: { desc: 'Deal 8 damage. +1 Energy next turn.' } },
Immolate: { cost: 2, type: 'Attack', desc: 'Deal 12 damage. Exhaust.', upgrade: { desc: 'Deal 16 damage. Exhaust.' } },
ObsidianSkin: { cost: 1, type: 'Skill', desc: 'Gain 7 Block.', upgrade: { desc: 'Gain 10 Block.' } },
FlameWard: { cost: 1, type: 'Skill', desc: 'Gain 5 Block. 2 Burn to random.', upgrade: { desc: 'Gain 6 Block. 3 Burn to random.' } },
DemonicResilience: { cost: 1, type: 'Skill', desc: 'Gain 4 Block. Heal 2 HP.', upgrade: { desc: 'Gain 5 Block. Heal 3 HP.' } },
BrimstoneShield: { cost: 1, type: 'Skill', desc: 'Gain 3 Block. +2 Str this turn.', upgrade: { desc: 'Gain 4 Block. +3 Str this turn.' } },
SummonImp: { cost: 1, type: 'Skill', desc: 'Add Imp Claw to hand. Exhaust.', upgrade: { desc: 'Add Imp Claw+ to hand. Exhaust.' } },
InfernalPact: { cost: 0, type: 'Skill', desc: 'Lose 2 HP. Gain 2 Energy. Add Imp Claw.', upgrade: { desc: 'Lose 1 HP. Gain 2 Energy. Add Imp Claw+.' } },
HellfireAura: { cost: 2, type: 'Power', desc: 'Start of turn: 3 damage to ALL.', upgrade: { desc: 'Start of turn: 5 damage to ALL.' } },
SoulBurn: { cost: 1, type: 'Skill', desc: 'Apply 8 Burn to enemy. Draw 1.', upgrade: { desc: 'Apply 10 Burn to enemy. Draw 1.' } },
},
colorless: {
FlashOfInsight: { cost: 1, type: 'Skill', desc: 'Draw 2 cards.', upgrade: { desc: 'Draw 3 cards.' } },
IronWill: { cost: 1, type: 'Skill', desc: 'Gain 8 Block.', upgrade: { desc: 'Gain 11 Block.' } },
QuickSlash: { cost: 1, type: 'Attack', desc: 'Deal 7 damage.', upgrade: { desc: 'Deal 9 damage.' } },
PotentBrew: { cost: 0, type: 'Skill', desc: 'Gain 1 Energy. Exhaust.', upgrade: { desc: 'Gain 2 Energy. Exhaust.' } },
},
status: {
Wound: { cost: 1, type: 'Curse', desc: 'Unplayable. Ethereal. Exhaust.' },
Dazed: { cost: 1, type: 'Status', desc: 'Unplayable. Ethereal. Exhaust.' },
Doubt: { cost: 1, type: 'Curse', desc: 'Unplayable. Lose 1 HP at end of turn.' },
ImpClaw: { cost: 0, type: 'Attack', desc: 'Deal 4 damage. Exhaust.' },
ImpClawPlus: { cost: 0, type: 'Attack', desc: 'Deal 6 damage. Exhaust.' },
}
};
// ── const: ENEMY_DATA ──
const ENEMY_DATA = {
act1: {
Skeleton: { hp: 22, pattern: [
{ type: 'attack', val: 6, desc: 'Attack 6' },
{ type: 'attack', val: 6, desc: 'Attack 6' },
{ type: 'defend', val: 8, desc: 'Defend 8' }
]},
ZombieBrute: { hp: 28, pattern: [
{ type: 'attack', val: 8, desc: 'Attack 8' },
{ type: 'debuff', val: 2, effect: 'weak', desc: 'Apply 2 Weak' },
{ type: 'attack', val: 8, desc: 'Attack 8' }
]},
DarkCultist: { hp: 20, pattern: [
{ type: 'buff', val: 3, effect: 'strength', desc: 'Gain 3 Strength' },
{ type: 'attack', val: 5, desc: 'Attack 5' },
{ type: 'attack', val: 5, desc: 'Attack 5' }
]},
RatSwarm: { hp: 15, pattern: [
{ type: 'multi', val: 3, hits: 2, desc: 'Attack 3 x 2' },
{ type: 'multi', val: 3, hits: 2, desc: 'Attack 3 x 2' }
]},
TombGuardian: { hp: 42, pattern: [
{ type: 'special', val: 6, block: 8, desc: 'Attack 6, Gain 8 Block' },
{ type: 'multi', val: 4, hits: 3, desc: 'Attack 4 x 3' },
{ type: 'attack', val: 12, desc: 'Attack 12' }
]},
BloodWitch: { hp: 36, pattern: [
{ type: 'special', val: 5, heal: 4, desc: 'Attack 5, Heal 4' },
{ type: 'debuff', val: 1, effects: ['weak', 'vuln'], desc: 'Apply Weak & Vuln' },
{ type: 'special', val: 7, heal: 4, desc: 'Attack 7, Heal 4' }
]},
LichKing: { hp: 65, pattern: [
{ type: 'summon', enemy: 'Skeleton', desc: 'Summon Skeleton' },
{ type: 'attack', val: 8, desc: 'Attack 8' },
{ type: 'defend', val: 12, desc: 'Defend 12' }
], phase2: { hp: 30, strength: 2, pattern: [
{ type: 'attack', val: 10, desc: 'Attack 10' },
{ type: 'attack', val: 10, desc: 'Attack 10' },
{ type: 'defend', val: 15, desc: 'Defend 15' }
]}}
},
// Act 2 and 3 will be similar mappings
};
// ── function: resolveCardEffect ──
function resolveCardEffect(game, card, targetIndex = 0) {
const info = game.getCardInfo(card);
const player = game.player;
const combat = game.combat;
const target = combat.enemies[targetIndex];
// Basic parsing of description for simple numbers
const getVal = (txt) => {
const match = txt.match(/\d+/);
return match ? parseInt(match[0]) : 0;
};
// This is a simplified effect engine
if (card.id === 'Strike') {
game.damageEnemy(getVal(info.desc), targetIndex);
} else if (card.id === 'Defend') {
player.block += getVal(info.desc);
} else if (card.id === 'SanguineStrike') {
game.damageEnemy(getVal(info.desc), targetIndex);
player.hp = Math.min(player.maxHp, player.hp + 2);
} else if (card.id === 'Hemorrhage') {
game.damageEnemy(getVal(info.desc), targetIndex);
// Add bleed effect logic later
} else if (card.id === 'Bloodletting') {
player.hp -= 3;
game.damageEnemy(getVal(info.desc), targetIndex);
} else if (card.id === 'CrimsonEruption') {
const dmg = getVal(info.desc);
combat.enemies.forEach((_, idx) => game.damageEnemy(dmg, idx));
player.hp -= 2;
} else if (card.id === 'CrimsonShield') {
player.block += getVal(info.desc);
player.hp -= 2;
} else if (card.id === 'BloodWard') {
player.block += getVal(info.desc);
// Regen logic
} else if (card.id === 'SacrificialBarrier') {
player.block += getVal(info.desc);
player.hp -= 5;
} else if (card.id === 'VitalSurge') {
const missing = player.maxHp - player.hp;
player.block += missing;
} else if (card.id === 'LifeTap') {
game.drawCards(2);
player.hp -= 3;
} else if (card.id === 'RendFlesh') {
combat.enemies.forEach((_, idx) => {
// apply bleed logic
});
player.hp -= 2;
} else if (card.id === 'ShadowBolt') {
game.damageEnemy(getVal(info.desc), targetIndex);
// Weakness logic
} else if (card.id === 'GraspOfDespair') {
game.damageEnemy(getVal(info.desc), targetIndex);
// Vuln logic
} else if (card.id === 'CloakOfShadows') {
player.block += getVal(info.desc);
game.drawCards(1);
} else if (card.id === 'HauntingMist') {
player.block += getVal(info.desc);
combat.enemies.forEach(en => { /* Weak */ });
} else if (card.id === 'Firebolt') {
game.damageEnemy(getVal(info.desc), targetIndex);
// Burn logic
} else if (card.id === 'HellfireBlast') {
const dmg = getVal(info.desc);
combat.enemies.forEach((_, idx) => game.damageEnemy(dmg, idx));
} else if (card.id === 'SearingStrike') {
game.damageEnemy(getVal(info.desc), targetIndex);
// Energy next turn logic
} else if (card.id === 'Immolate') {
game.damageEnemy(getVal(info.desc), targetIndex);
} else if (card.id === 'ObsidianSkin') {
player.block += getVal(info.desc);
} else if (card.id === 'FlameWard') {
player.block += getVal(info.desc);
// random burn enemy
} else if (card.id === 'DemonicResilience') {
player.block += getVal(info.desc);
player.hp = Math.min(player.maxHp, player.hp + 2);
} else if (card.id === 'BrimstoneShield') {
player.block += getVal(info.desc);
player.strength += 2;
} else if (card.id === 'SummonImp') {
game.player.hand.push({ id: 'ImpClaw', upgraded: false });
} else if (card.id === 'InfernalPact') {
player.hp -= 2;
player.energy += 2;
game.player.hand.push({ id: 'ImpClaw', upgraded: false });
} else if (card.id === 'SoulBurn') {
// apply burn
game.drawCards(1);
} else if (card.id === 'ImpClaw') {
game.damageEnemy(4, targetIndex);
} else if (card.id === 'ImpClawPlus') {
game.damageEnemy(6, targetIndex);
}
}
// ── main ──
class Game {
constructor() {
this.state = 'START';
this.player = {
hp: 70,
maxHp: 70,
gold: 99,
energy: 3,
maxEnergy: 3,
strength: 0,
dexterity: 0,
block: 0,
deck: [],
hand: [],
drawPile: [],
discardPile: [],
relics: [],
potions: [],
class: null,
act: 1,
floor: 1,
currentPos: { x: 2, y: 0 }
};
this.combat = null;
this.init();
}
init() {
this.updateUI();
}
selectClass(cls) {
this.player.class = cls;
this.setupStartingDeck(cls);
this.state = 'MAP';
this.renderMap();
this.updateUI();
}
setupStartingDeck(cls) {
const basic = CARD_DATA.basic;
const classPool = CARD_DATA[cls];
for (let i = 0; i < 5; i++) this.player.deck.push({ id: 'Strike', upgraded: false });
for (let i = 0; i < 4; i++) this.player.deck.push({ id: 'Defend', upgraded: false });
const sigCard = Object.keys(classPool)[0];
this.player.deck.push({ id: sigCard, upgraded: false });
this.shuffle(this.player.deck);
}
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]];
}
}
updateUI() {
document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
if (this.state === 'START') {
document.getElementById('screen-start').classList.remove('hidden');
} else if (this.state === 'MAP') {
document.getElementById('screen-map').classList.remove('hidden');
document.getElementById('map-gold').innerText = `Gold: ${this.player.gold}`;
document.getElementById('map-act').innerText = `Act ${this.player.act}: The Crypts`;
} else if (this.state === 'COMBAT') {
document.getElementById('screen-combat').classList.remove('hidden');
this.updateCombatHUD();
} else if (this.state === 'REWARD') {
document.getElementById('screen-reward').classList.remove('hidden');
} else if (this.state === 'EVENT') {
document.getElementById('screen-event').classList.remove('hidden');
} else if (this.state === 'SHOP') {
document.getElementById('screen-shop').classList.remove('hidden');
} else if (this.state === 'REST') {
document.getElementById('screen-rest').classList.remove('hidden');
} else if (this.state === 'GAMEOVER') {
document.getElementById('screen-gameover').classList.remove('hidden');
} else if (this.state === 'VICTORY') {
document.getElementById('screen-victory').classList.remove('hidden');
}
}
renderMap() {
const canvas = document.getElementById('map-canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const nodes = [];
for (let y = 0; y < 15; y++) {
nodes.push({ x: 400, y: 550 - (y * 35), type: 'Enemy' });
}
ctx.strokeStyle = '#444';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(400, 550);
ctx.lineTo(400, 550 - (14 * 35));
ctx.stroke();
nodes.forEach((node, i) => {
ctx.fillStyle = (i === this.player.floor) ? '#8b0000' : '#444';
ctx.beginPath();
ctx.arc(node.x, node.y, 10, 0, Math.PI * 2);
ctx.fill();
if (i === this.player.floor) {
ctx.fillStyle = 'white';
ctx.fillText('Current Floor', node.x + 15, node.y + 5);
}
});
canvas.onclick = (e) => {
if (this.state !== 'MAP') return;
this.startCombat();
};
}
startCombat() {
this.state = 'COMBAT';
this.combat = {
turn: 1,
enemies: [
{ name: 'Skeleton Warrior', hp: 22, maxHp: 22, block: 0, pattern: ENEMY_DATA.act1.Skeleton.pattern, turnIndex: 0 }
],
playerBlock: 0
};
this.player.drawPile = [...this.player.deck];
this.shuffle(this.player.drawPile);
this.player.discardPile = [];
this.player.energy = this.player.maxEnergy;
this.startPlayerTurn();
this.updateUI();
}
startPlayerTurn() {
this.player.block = 0;
this.player.energy = this.player.maxEnergy;
this.drawCards(5);
this.updateCombatHUD();
this.renderHand();
}
drawCards(count) {
for (let i = 0; i < count; i++) {
if (this.player.drawPile.length === 0) {
if (this.player.discardPile.length === 0) break;
this.player.drawPile = [...this.player.discardPile];
this.player.discardPile = [];
this.shuffle(this.player.drawPile);
}
this.player.hand.push(this.player.drawPile.pop());
}
}
updateCombatHUD() {
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').innerText = this.player.block;
document.getElementById('player-energy').innerText = `Energy: ${this.player.energy} / ${this.player.maxEnergy}`;
const enemyCont = document.getElementById('enemy-container');
enemyCont.innerHTML = '';
this.combat.enemies.forEach(en => {
const el = document.createElement('div');
el.className = 'enemy-unit';
const intent = en.pattern[en.turnIndex % en.pattern.length];
el.innerHTML = `
<div class="enemy-intent">${intent.desc}</div>
<div class="enemy-hp-bar"><div class="enemy-hp-fill" style="width:${(en.hp / en.maxHp)*100}%"></div></div>
<div style="font-weight:bold">${en.name}</div>
<div style="color:#4a9eff">Block: ${en.block}</div>
<div>HP: ${en.hp}/${en.maxHp}</div>
`;
enemyCont.appendChild(el);
});
}
renderHand() {
const handEl = document.getElementById('hand');
handEl.innerHTML = '';
this.player.hand.forEach((card, idx) => {
const cardInfo = this.getCardInfo(card);
const el = document.createElement('div');
el.className = 'card';
if (cardInfo.cost > this.player.energy) el.classList.add('unplayable');
el.innerHTML = `
<div class="card-cost">${cardInfo.cost}</div>
<div class="card-title">${card.id}${card.upgraded ? '+' : ''}</div>
<div class="card-desc">${cardInfo.desc}</div>
`;
el.onclick = () => this.playCard(idx);
handEl.appendChild(el);
});
}
getCardInfo(card) {
let data = CARD_DATA.basic[card.id] || CARD_DATA[this.player.class][card.id] || CARD_DATA.colorless[card.id] || CARD_DATA.status[card.id];
if (card.upgraded && data.upgrade) {
return { ...data, ...data.upgrade };
}
return data;
}
playCard(idx) {
const card = this.player.hand[idx];
const info = this.getCardInfo(card);
if (info.cost > this.player.energy) return;
this.player.energy -= info.cost;
resolveCardEffect(this, card);
this.player.discardPile.push(this.player.hand.splice(idx, 1)[0]);
this.updateCombatHUD();
this.renderHand();
}
damageEnemy(amount, targetIdx = 0) {
const target = this.combat.enemies[targetIdx];
if (!target) return;
let rem = amount;
if (target.block > 0) {
const absorbed = Math.min(target.block, rem);
target.block -= absorbed;
rem -= absorbed;
}
target.hp -= rem;
if (target.hp <= 0) {
target.hp = 0;
// Check if all enemies dead
if (this.combat.enemies.every(e => e.hp <= 0)) {
this.endCombat(true);
}
}
}
endCombat(victory) {
if (victory) {
this.state = 'REWARD';
document.getElementById('reward-title').innerText = 'Victory!';
document.getElementById('reward-next').classList.remove('hidden');
document.getElementById('reward-next').onclick = () => {
this.player.floor++;
this.state = 'MAP';
this.renderMap();
this.updateUI();
};
} else {
this.state = 'GAMEOVER';
}
this.updateUI();
}
}
window.game = new Game();
document.getElementById('end-turn-btn').onclick = () => {
const player = window.game.player;
const combat = window.game.combat;
player.discardPile.push(...player.hand);
player.hand = [];
combat.enemies.forEach(en => {
if (en.hp <= 0) return;
const intent = en.pattern[en.turnIndex % en.pattern.length];
if (intent.type === 'attack') {
let dmg = intent.val;
if (player.block > 0) {
const absorbed = Math.min(player.block, dmg);
player.block -= absorbed;
dmg -= absorbed;
}
player.hp -= dmg;
} else if (intent.type === 'defend') {
en.block += intent.val;
} else if (intent.type === 'multi') {
for (let i = 0; i < intent.hits; i++) {
let dmg = intent.val;
if (player.block > 0) {
const absorbed = Math.min(player.block, dmg);
player.block -= absorbed;
dmg -= absorbed;
}
player.hp -= dmg;
}
}
en.turnIndex++;
});
if (player.hp <= 0) {
window.game.endCombat(false);
} else {
window.game.startPlayerTurn();
}
window.game.updateUI();
};
</script>
</body>
</html>
<!-- agent-meta {"model":"gemma-4-31b","provider":"cerebras","persona":"composer","sessionId":"2659e74c-6657-4534-9ad3-121cc3e5e548","tokensIn":61053,"tokensOut":14241,"tokensTotal":75294,"turns":3,"toolCalls":10,"failedToolCalls":0,"timestamp":"2026-07-14T16:29:47.526Z"} -->