Shadows
thinkingcap qwen3.6 27b · 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 92,094 bytes · SHA-256 b4f10f2d6029
<!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>
<style>
/* ── css: base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a0f; color: #d4c5a9; font-family: 'Georgia', serif; }
.screen { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; }
.screen.active { display: flex; flex-direction: column; align-items: center; justify-content: center; }
button { cursor: pointer; font-family: 'Georgia', serif; border: none; outline: none; transition: all 0.2s; }
button:hover { filter: brightness(1.2); transform: scale(1.03); }
button:active { transform: scale(0.97); }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #1a1a2e; }
::-webkit-scrollbar-thumb { background: #4a3f6b; border-radius: 4px; }
/* ── css: title-screen ── */
#screen-title { background: radial-gradient(ellipse at center, #1a0a2e 0%, #0a0a0f 70%); }
.title-content { text-align: center; max-width: 600px; padding: 40px; }
.game-title { font-size: 3em; color: #c9a84c; text-shadow: 0 0 20px rgba(201,168,76,0.5), 0 2px 4px rgba(0,0,0,0.8); margin-bottom: 10px; letter-spacing: 3px; }
.game-subtitle { font-size: 1.1em; color: #8a7a5c; margin-bottom: 40px; font-style: italic; }
.class-select { display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; margin-top: 30px; }
.class-card { width: 160px; padding: 20px 15px; background: linear-gradient(180deg, #1a1a2e, #0f0f1a); border: 2px solid #3a3a5c; border-radius: 10px; cursor: pointer; transition: all 0.3s; text-align: center; }
.class-card:hover { border-color: #c9a84c; box-shadow: 0 0 20px rgba(201,168,76,0.3); transform: translateY(-5px); }
.class-icon { font-size: 2.5em; margin-bottom: 10px; }
.class-name { color: #c9a84c; font-size: 1.1em; margin-bottom: 8px; font-weight: bold; }
.class-desc { color: #8a7a5c; font-size: 0.8em; line-height: 1.3; }
/* ── css: map-screen ── */
#screen-map { background: radial-gradient(ellipse at center, #1a0a2e 0%, #0a0a0f 70%); justify-content: flex-start; padding-top: 20px; }
.map-header { text-align: center; margin-bottom: 15px; width: 100%; max-width: 500px; }
.act-title { color: #c9a84c; font-size: 1.3em; letter-spacing: 2px; }
.floor-info { color: #8a7a5c; font-size: 0.9em; margin-top: 5px; }
.player-stats-bar { display: flex; gap: 15px; justify-content: center; align-items: center; background: rgba(26,26,46,0.8); padding: 8px 20px; border-radius: 8px; margin-top: 8px; font-size: 0.85em; }
.stat-item { display: flex; align-items: center; gap: 4px; }
.map-container { width: 100%; max-width: 500px; padding: 10px; position: relative; min-height: 60vh; }
.map-grid { display: flex; flex-direction: column-reverse; align-items: center; gap: 8px; padding-bottom: 20px; }
.map-row { display: flex; justify-content: center; gap: 30px; width: 100%; position: relative; }
.map-node { width: 50px; height: 50px; border-radius: 8px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; font-size: 1.4em; position: relative; }
.map-node.locked { opacity: 0.3; cursor: not-allowed; filter: grayscale(1); }
.map-node.available { animation: nodePulse 2s infinite; border-color: #c9a84c !important; box-shadow: 0 0 15px rgba(201,168,76,0.4); }
.map-node.visited { opacity: 0.5; filter: grayscale(0.5); }
@keyframes nodePulse { 0%, 100% { box-shadow: 0 0 10px rgba(201,168,76,0.3); } 50% { box-shadow: 0 0 25px rgba(201,168,76,0.6); } }
.node-enemy { background: #4a1a1a; border: 2px solid #8b3a3a; }
.node-elite { background: #4a1a3a; border: 2px solid #8b3a7a; }
.node-boss { background: #5a0a0a; border: 2px solid #c9a84c; font-size: 1.6em; }
.node-rest { background: #1a3a1a; border: 2px solid #3a7a3a; }
.node-shop { background: #3a3a1a; border: 2px solid #7a7a3a; }
.node-event { background: #1a2a4a; border: 2px solid #3a5a8b; }
.map-connector { width: 2px; height: 8px; background: #3a3a5c; margin: 0 auto; }
/* ── css: combat-screen ── */
#screen-combat { background: radial-gradient(ellipse at center, #1a0a2e 0%, #0a0a0f 70%); justify-content: flex-start; padding-top: 5px; }
.combat-header { width: 100%; max-width: 800px; display: flex; justify-content: space-between; align-items: center; padding: 5px 15px; background: rgba(26,26,46,0.9); border-radius: 8px; margin-bottom: 5px; }
.combat-player-info { display: flex; gap: 15px; align-items: center; font-size: 0.85em; }
.hp-bar-container { width: 120px; height: 14px; background: #2a1a1a; border-radius: 7px; overflow: hidden; position: relative; }
.hp-bar-fill { height: 100%; background: linear-gradient(90deg, #8b3a3a, #c9a84c); transition: width 0.3s; }
.hp-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 0.7em; color: white; text-shadow: 1px 1px 2px black; }
.combat-area { width: 100%; max-width: 800px; display: flex; gap: 10px; margin-bottom: 5px; }
.enemy-zone { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; min-height: 200px; justify-content: center; }
.player-zone { width: 140px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; }
.enemy-card { background: linear-gradient(135deg, #2a1a1a, #1a0f0f); border: 2px solid #5a3a3a; border-radius: 8px; padding: 10px; text-align: center; min-width: 100px; position: relative; }
.enemy-card.targetable:hover { border-color: #c94a4a; box-shadow: 0 0 15px rgba(201,74,74,0.3); cursor: pointer; }
.enemy-name { color: #c9a84c; font-size: 0.8em; margin-bottom: 4px; }
.enemy-hp-bar { width: 80px; height: 8px; background: #2a1a1a; border-radius: 4px; overflow: hidden; margin: 4px auto; }
.enemy-hp-fill { height: 100%; background: linear-gradient(90deg, #8b3a3a, #c94a4a); transition: width 0.3s; }
.intent-icon { font-size: 0.75em; margin-top: 4px; color: #db5a5a; }
.player-avatar { width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #3a2a5c, #1a1a3e); border: 2px solid #5a4a7c; display: flex; align-items: center; justify-content: center; font-size: 1.8em; }
.player-block { color: #6ab0de; font-size: 0.9em; margin-top: 5px; }
.buffs-row { display: flex; gap: 3px; flex-wrap: wrap; justify-content: center; max-width: 120px; }
.buff-icon { width: 22px; height: 22px; border-radius: 4px; font-size: 0.6em; display: flex; align-items: center; justify-content: center; color: white; position: relative; }
.buff-count { position: absolute; bottom: -2px; right: -2px; background: #1a1a2e; border-radius: 50%; width: 14px; height: 14px; font-size: 0.7em; display: flex; align-items: center; justify-content: center; }
.hand-area { width: 100%; max-width: 800px; min-height: 160px; padding: 5px; position: relative; }
.hand-cards { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; perspective: 800px; }
.game-card { width: 110px; min-height: 140px; background: linear-gradient(180deg, #2a2a3e, #1a1a2e); border: 2px solid #4a4a6c; border-radius: 8px; padding: 6px; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; position: relative; user-select: none; }
.game-card:hover { transform: translateY(-15px); z-index: 10; box-shadow: 0 8px 25px rgba(0,0,0,0.6); border-color: #c9a84c; }
.game-card.unplayable { opacity: 0.5; cursor: not-allowed; filter: grayscale(0.5); }
.game-card.unplayable:hover { transform: none; box-shadow: none; border-color: #4a4a6c; }
.card-cost { position: absolute; top: -8px; left: -8px; width: 24px; height: 24px; background: linear-gradient(135deg, #4a3a7c, #2a1a5e); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.8em; color: white; font-weight: bold; border: 2px solid #6a5a9c; }
.card-name { color: #c9a84c; font-size: 0.7em; text-align: center; margin-bottom: 3px; line-height: 1.1; }
.card-type-badge { position: absolute; top: -2px; right: -2px; width: 16px; height: 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.5em; }
.type-attack { background: #8b3a3a; }
.type-skill { background: #3a7a3a; }
.type-power { background: #3a3a8b; }
.card-desc { color: #9a9ab0; font-size: 0.6em; text-align: center; flex: 1; line-height: 1.2; margin-top: 5px; }
.combat-footer { width: 100%; max-width: 800px; display: flex; justify-content: space-between; align-items: center; padding: 5px 15px; gap: 10px; }
.end-turn-btn { background: linear-gradient(135deg, #4a2a2a, #2a1a1a); color: #c9a84c; border: 2px solid #6a4a4a; padding: 8px 25px; border-radius: 8px; font-size: 0.9em; }
.potion-slots { display: flex; gap: 5px; }
.potion-slot { width: 35px; height: 35px; background: #1a1a2e; border: 2px solid #4a4a6c; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.1em; }
.potion-slot:hover { border-color: #c9a84c; }
.discard-pile-info { font-size: 0.75em; color: #6a6a8c; text-align: center; margin-top: 3px; }
.enemy-debuffs { display: flex; gap: 2px; justify-content: center; margin-top: 3px; flex-wrap: wrap; }
.enemy-buff-icon { width: 18px; height: 18px; border-radius: 3px; font-size: 0.5em; display: flex; align-items: center; justify-content: center; color: white; position: relative; }
/* ── css: ui-screens ── */
#screen-rewards, #screen-shop, #screen-event, #screen-rest { background: radial-gradient(ellipse at center, #1a0a2e 0%, #0a0a0f 70%); }
.reward-title { color: #c9a84c; font-size: 1.5em; margin-bottom: 20px; text-shadow: 0 0 10px rgba(201,168,76,0.3); }
.card-choice-row { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; margin: 15px 0; }
.reward-card { width: 140px; min-height: 180px; background: linear-gradient(180deg, #2a2a3e, #1a1a2e); border: 2px solid #4a4a6c; border-radius: 10px; padding: 10px; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; }
.reward-card:hover { transform: translateY(-8px); box-shadow: 0 8px 25px rgba(0,0,0,0.6); border-color: #c9a84c; }
.gold-reward { color: #c9a84c; font-size: 1.2em; margin: 10px 0; }
.btn-primary { background: linear-gradient(135deg, #4a3a7c, #2a1a5e); color: #d4c5a9; border: 2px solid #6a5a9c; padding: 10px 30px; border-radius: 8px; font-size: 1em; margin-top: 15px; }
.btn-secondary { background: linear-gradient(135deg, #2a2a3e, #1a1a2e); color: #9a9ab0; border: 2px solid #4a4a6c; padding: 8px 20px; border-radius: 8px; font-size: 0.9em; margin-top: 10px; }
.shop-grid { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; max-width: 700px; }
.shop-item { width: 130px; background: linear-gradient(180deg, #2a2a3e, #1a1a2e); border: 2px solid #4a4a6c; border-radius: 8px; padding: 10px; text-align: center; cursor: pointer; transition: all 0.2s; }
.shop-item:hover { border-color: #c9a84c; transform: translateY(-3px); }
.shop-price { color: #c9a84c; font-size: 0.85em; margin-top: 5px; }
.event-text { max-width: 500px; text-align: center; line-height: 1.6; margin-bottom: 20px; color: #b0a890; font-size: 1em; }
.event-choices { display: flex; flex-direction: column; gap: 10px; align-items: center; }
.rest-options { display: flex; gap: 20px; justify-content: center; margin-top: 20px; }
.rest-option { width: 150px; padding: 20px; background: linear-gradient(180deg, #2a2a3e, #1a1a2e); border: 2px solid #4a4a6c; border-radius: 10px; text-align: center; cursor: pointer; transition: all 0.2s; }
.rest-option:hover { border-color: #c9a84c; transform: translateY(-5px); }
.victory-text, .defeat-text { font-size: 3em; margin-bottom: 20px; text-shadow: 0 0 30px rgba(201,168,76,0.5); }
.defeat-text { color: #8b3a3a; }
.victory-text { color: #c9a84c; }
.deck-view { max-width: 600px; margin: 10px auto; padding: 10px; background: rgba(26,26,46,0.5); border-radius: 8px; }
.deck-cards-grid { display: flex; gap: 5px; flex-wrap: wrap; justify-content: center; }
.mini-card { width: 70px; min-height: 90px; background: linear-gradient(180deg, #2a2a3e, #1a1a2e); border: 1px solid #4a4a6c; border-radius: 5px; padding: 4px; font-size: 0.55em; display: flex; flex-direction: column; cursor: pointer; }
.mini-card:hover { border-color: #c9a84c; }
.rarity-common { color: #6ab0de; }
.rarity-uncommon { color: #3adb3a; }
.rarity-rare { color: #db3adb; }
</style>
</head>
<body>
<div id="screen-title" class="screen active"></div>
<div id="screen-map" class="screen"></div>
<div id="screen-combat" class="screen"></div>
<div id="screen-rewards" class="screen"></div>
<div id="screen-shop" class="screen"></div>
<div id="screen-event" class="screen"></div>
<div id="screen-rest" class="screen"></div>
<div id="screen-victory" class="screen"></div>
<div id="screen-defeat" class="screen"></div>
<script>
'use strict';
// ── const: CARD_TYPES ──
const CARD_TYPES = { ATTACK: 'attack', SKILL: 'skill', POWER: 'power', CURSE: 'curse', STATUS: 'status' };
// ── const: RARITIES ──
const RARITIES = { BASIC: 'basic', COMMON: 'common', UNCOMMON: 'uncommon', RARE: 'rare' };
// ── const: CLASSES ──
const CLASSES = { BLOOD_REAPER: 'blood_reaper', SHADOWMANCER: 'shadowmancer', HELLFORGED: 'hellforged' };
// ── function: makeCardDef ──
function makeCardDef(id, name, type, cost, rarity, desc, upgName, upgDesc) {
return { id, name, type, cost, rarity, desc, upgraded: false, upgName: upgName || (name + '+'), upgDesc: upgDesc || desc };
}
// ── const: BASIC_STRIKE ──
const BASIC_STRIKE = makeCardDef('strike', 'Strike', CARD_TYPES.ATTACK, 1, RARITIES.BASIC, 'Deal 6 damage.', null, 'Deal 8 damage.');
// ── const: BASIC_DEFEND ──
const BASIC_DEFEND = makeCardDef('defend', 'Defend', CARD_TYPES.SKILL, 1, RARITIES.BASIC, 'Gain 5 Block.', null, 'Gain 8 Block.');
// ── const: BR_ATTACKS ──
// Blood Reaper cards
const BR_ATTACKS = [
makeCardDef('sanguine_strike', 'Sanguine Strike', CARD_TYPES.ATTACK, 1, RARITIES.COMMON, 'Deal 8 damage. Heal 2 HP.', null, 'Deal 10 damage. Heal 3 HP.'),
makeCardDef('hemorrhage', 'Hemorrhage', CARD_TYPES.ATTACK, 1, RARITIES.COMMON, 'Deal 5 damage. Apply 3 Bleed.', null, 'Deal 6 damage. Apply 4 Bleed.'),
makeCardDef('bloodletting', 'Bloodletting', CARD_TYPES.ATTACK, 1, RARITIES.UNCOMMON, 'Lose 3 HP. Deal 12 damage.', null, 'Lose 2 HP. Deal 15 damage.'),
makeCardDef('crimson_eruption', 'Crimson Eruption', CARD_TYPES.ATTACK, 2, RARITIES.RARE, 'Deal 8 damage to ALL enemies. Lose 2 HP.', null, 'Deal 10 damage to ALL enemies. Lose 2 HP.'),
];
// ── const: BR_DEFENSES ──
const BR_DEFENSES = [
makeCardDef('crimson_shield', 'Crimson Shield', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Gain 6 Block. Lose 2 HP.', null, 'Gain 8 Block. Lose 2 HP.'),
makeCardDef('blood_ward', 'Blood Ward', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Gain 3 Block. Gain 1 Regeneration.', null, 'Gain 4 Block. Gain 2 Regeneration.'),
makeCardDef('sacrificial_barrier', 'Sacrificial Barrier', CARD_TYPES.SKILL, 2, RARITIES.UNCOMMON, 'Gain 10 Block. Lose 5 HP.', null, 'Gain 13 Block. Lose 5 HP.'),
makeCardDef('vital_surge', 'Vital Surge', CARD_TYPES.SKILL, -1, RARITIES.RARE, 'Gain Block equal to your missing HP. Exhaust.', null, 'Gain Block equal to missing HP + 3. Exhaust.'),
];
// ── const: BR_OTHERS ──
const BR_OTHERS = [
makeCardDef('life_tap', 'Life Tap', CARD_TYPES.SKILL, 0, RARITIES.COMMON, 'Draw 2 cards. Lose 3 HP.', null, 'Draw 3 cards. Lose 3 HP.'),
makeCardDef('rend_flesh', 'Rend Flesh', CARD_TYPES.SKILL, 1, RARITIES.UNCOMMON, 'Apply 5 Bleed to ALL enemies. Lose 2 HP.', null, 'Apply 7 Bleed to ALL enemies. Lose 2 HP.'),
makeCardDef('soul_siphon', 'Soul Siphon', CARD_TYPES.POWER, 2, RARITIES.UNCOMMON, '(Power) At start of your turn, gain 1 Energy and lose 1 HP.', null, 'At start of your turn, gain 1 Energy and lose 1 HP.'),
makeCardDef('blood_pact', 'Blood Pact', CARD_TYPES.POWER, 1, RARITIES.RARE, '(Power) Whenever you lose HP from a card, gain 1 Strength.', null, 'Whenever you lose HP from a card, gain 2 Strength.'),
];
// ── const: SM_ATTACKS ──
// Shadowmancer cards
const SM_ATTACKS = [
makeCardDef('shadow_bolt', 'Shadow Bolt', CARD_TYPES.ATTACK, 1, RARITIES.COMMON, 'Deal 6 damage. Apply 1 Weak.', null, 'Deal 8 damage. Apply 2 Weak.'),
makeCardDef('grasp_of_despair', 'Grasp of Despair', CARD_TYPES.ATTACK, 1, RARITIES.COMMON, 'Deal 4 damage. Apply 1 Vulnerable.', null, 'Deal 5 damage. Apply 2 Vulnerable.'),
makeCardDef('nightmare_lash', 'Nightmare Lash', CARD_TYPES.ATTACK, 2, RARITIES.UNCOMMON, 'Deal 7 damage. If enemy has Weak, deal 7 more.', null, 'Deal 9 damage. If Weak, deal 9 more.'),
makeCardDef('eternal_darkness', 'Eternal Darkness', CARD_TYPES.ATTACK, 2, RARITIES.RARE, 'Deal 3 damage to a random enemy 3 times.', null, 'Deal 4 damage to a random enemy 3 times.'),
];
// ── const: SM_DEFENSES ──
const SM_DEFENSES = [
makeCardDef('cloak_of_shadows', 'Cloak of Shadows', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Gain 5 Block. Draw 1 card.', null, 'Gain 7 Block. Draw 1 card.'),
makeCardDef('haunting_mist', 'Haunting Mist', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Gain 7 Block. Apply 1 Weak to ALL enemies.', null, 'Gain 9 Block. Apply 1 Weak to ALL enemies.'),
makeCardDef('phantom_armor', 'Phantom Armor', CARD_TYPES.SKILL, 1, RARITIES.UNCOMMON, 'Gain 8 Block. Next turn, gain 2 Dexterity.', null, 'Gain 10 Block. Next turn, gain 3 Dexterity.'),
makeCardDef('whispering_shroud', 'Whispering Shroud', CARD_TYPES.SKILL, 1, RARITIES.RARE, 'Gain 4 Block. Next turn, gain 4 Block.', null, 'Gain 5 Block. Next turn, gain 5 Block.'),
];
// ── const: SM_OTHERS ──
const SM_OTHERS = [
makeCardDef('shadowform', 'Shadowform', CARD_TYPES.SKILL, 2, RARITIES.UNCOMMON, 'Gain 2 Intangible. Exhaust.', null, 'Gain 3 Intangible. Exhaust.'),
makeCardDef('curse_of_misfortune', 'Curse of Misfortune', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Enemy loses 3 Strength this turn. Apply 1 Vulnerable.', null, 'Enemy loses 4 Strength this turn. Apply 2 Vulnerable.'),
makeCardDef('dark_ritual', 'Dark Ritual', CARD_TYPES.SKILL, 0, RARITIES.UNCOMMON, 'Gain 2 Energy. Shuffle a Dazed into your discard pile.', null, 'Gain 2 Energy. Shuffle a Dazed into your discard pile.'),
makeCardDef('umbral_mastery', 'Umbral Mastery', CARD_TYPES.POWER, 2, RARITIES.RARE, '(Power) At start of your turn, apply 1 Weak to ALL enemies.', null, 'At start of your turn, apply 2 Weak to ALL enemies.'),
];
// ── const: HF_ATTACKS ──
// Hellforged cards
const HF_ATTACKS = [
makeCardDef('firebolt', 'Firebolt', CARD_TYPES.ATTACK, 1, RARITIES.COMMON, 'Deal 7 damage. Apply 2 Burn.', null, 'Deal 9 damage. Apply 3 Burn.'),
makeCardDef('hellfire_blast', 'Hellfire Blast', CARD_TYPES.ATTACK, 2, RARITIES.UNCOMMON, 'Deal 9 damage to ALL enemies.', null, 'Deal 12 damage to ALL enemies.'),
makeCardDef('searing_strike', 'Searing Strike', CARD_TYPES.ATTACK, 1, RARITIES.COMMON, 'Deal 6 damage. Gain 1 Energy next turn.', null, 'Deal 8 damage. Gain 1 Energy next turn.'),
makeCardDef('immolate', 'Immolate', CARD_TYPES.ATTACK, 2, RARITIES.RARE, 'Deal 12 damage. Exhaust.', null, 'Deal 16 damage. Exhaust.'),
];
// ── const: HF_DEFENSES ──
const HF_DEFENSES = [
makeCardDef('obsidian_skin', 'Obsidian Skin', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Gain 7 Block.', null, 'Gain 10 Block.'),
makeCardDef('flame_ward', 'Flame Ward', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Gain 5 Block. Apply 2 Burn to a random enemy.', null, 'Gain 6 Block. Apply 3 Burn to a random enemy.'),
makeCardDef('demonic_resilience', 'Demonic Resilience', CARD_TYPES.SKILL, 1, RARITIES.UNCOMMON, 'Gain 4 Block. Heal 2 HP.', null, 'Gain 5 Block. Heal 3 HP.'),
makeCardDef('brimstone_shield', 'Brimstone Shield', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Gain 3 Block. Gain 2 Strength this turn.', null, 'Gain 4 Block. Gain 3 Strength this turn.'),
];
// ── const: HF_OTHERS ──
const HF_OTHERS = [
makeCardDef('summon_imp', 'Summon Imp', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Add an Imp Claw to your hand. Exhaust.', null, 'Add an Imp Claw+ to your hand. Exhaust.'),
makeCardDef('infernal_pact', 'Infernal Pact', CARD_TYPES.SKILL, 0, RARITIES.UNCOMMON, 'Lose 2 HP. Gain 2 Energy. Add an Imp Claw to your hand.', null, 'Lose 1 HP. Gain 2 Energy. Add an Imp Claw+.'),
makeCardDef('hellfire_aura', 'Hellfire Aura', CARD_TYPES.POWER, 2, RARITIES.RARE, '(Power) At start of your turn, deal 3 damage to ALL enemies.', null, 'At start of your turn, deal 5 damage to ALL enemies.'),
makeCardDef('soul_burn', 'Soul Burn', CARD_TYPES.SKILL, 1, RARITIES.UNCOMMON, 'Apply 8 Burn to an enemy. Draw 1 card.', null, 'Apply 10 Burn to an enemy. Draw 1 card.'),
];
// ── const: COLORLESS_CARDS ──
// Colorless cards (shop only)
const COLORLESS_CARDS = [
makeCardDef('flash_of_insight', 'Flash of Insight', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Draw 2 cards.', null, 'Draw 3 cards.'),
makeCardDef('iron_will', 'Iron Will', CARD_TYPES.SKILL, 1, RARITIES.COMMON, 'Gain 8 Block.', null, 'Gain 11 Block.'),
makeCardDef('quick_slash', 'Quick Slash', CARD_TYPES.ATTACK, 1, RARITIES.COMMON, 'Deal 7 damage.', null, 'Deal 9 damage.'),
makeCardDef('potent_brew', 'Potent Brew', CARD_TYPES.SKILL, 0, RARITIES.UNCOMMON, 'Gain 1 Energy. Exhaust.', null, 'Gain 2 Energy. Exhaust.'),
];
// ── const: WOUND ──
// Curses and status cards
const WOUND = { id: 'wound', name: 'Wound', type: CARD_TYPES.CURSE, cost: -1, rarity: RARITIES.BASIC, desc: 'Unplayable. Ethereal.', upgraded: false };
// ── const: DOUBT ──
const DOUBT = { id: 'doubt', name: 'Doubt', type: CARD_TYPES.CURSE, cost: -1, rarity: RARITIES.BASIC, desc: 'Unplayable. End of turn: lose 1 HP.', upgraded: false };
// ── const: DAZED ──
const DAZED = { id: 'dazed', name: 'Dazed', type: CARD_TYPES.STATUS, cost: -1, rarity: RARITIES.BASIC, desc: 'Unplayable. Ethereal.', upgraded: false };
// ── const: CLASS_POOLS ──
// Class card pools
const CLASS_POOLS = {
[CLASSES.BLOOD_REAPER]: [...BR_ATTACKS, ...BR_DEFENSES, ...BR_OTHERS],
[CLASSES.SHADOWMANCER]: [...SM_ATTACKS, ...SM_DEFENSES, ...SM_OTHERS],
[CLASSES.HELLFORGED]: [...HF_ATTACKS, ...HF_DEFENSES, ...HF_OTHERS],
};
// ── function: getStartingDeck ──
function getStartingDeck(classId) {
const deck = [];
for (let i = 0; i < 5; i++) deck.push({ ...BASIC_STRIKE });
for (let i = 0; i < 4; i++) deck.push({ ...BASIC_DEFEND });
if (classId === CLASSES.BLOOD_REAPER) deck.push({ ...BR_ATTACKS[0] });
else if (classId === CLASSES.SHADOWMANCER) deck.push({ ...SM_ATTACKS[0] });
else deck.push({ ...HF_ATTACKS[0] });
return deck;
}
// ── function: pickRandomCard ──
function pickRandomCard(pool, rarityWeights) {
const weights = rarityWeights || { [RARITIES.COMMON]: 60, [RARITIES.UNCOMMON]: 30, [RARITIES.RARE]: 10 };
let r = Math.random() * 100;
let chosenRarity = RARITIES.COMMON;
if (r < weights[RARITIES.RARE]) chosenRarity = RARITIES.RARE;
else if (r < weights[RARITIES.RARE] + weights[RARITIES.UNCOMMON]) chosenRarity = RARITIES.UNCOMMON;
const filtered = pool.filter(c => c.rarity === chosenRarity);
if (filtered.length > 0) return { ...filtered[Math.floor(Math.random() * filtered.length)] };
// fallback to any card
return { ...pool[Math.floor(Math.random() * pool.length)] };
}
// ── function: pickRewardCards ──
function pickRewardCards(pool, isBoss, isElite) {
const weights = isBoss ? { [RARITIES.COMMON]: 40, [RARITIES.UNCOMMON]: 30, [RARITIES.RARE]: 30 } :
isElite ? { [RARITIES.COMMON]: 50, [RARITIES.UNCOMMON]: 30, [RARITIES.RARE]: 20 } :
{ [RARITIES.COMMON]: 60, [RARITIES.UNCOMMON]: 30, [RARITIES.RARE]: 10 };
const cards = [];
if (isBoss) {
// Boss always has one rare
const rares = pool.filter(c => c.rarity === RARITIES.RARE);
cards.push({ ...rares[Math.floor(Math.random() * rares.length)] });
for (let i = 1; i < 3; i++) cards.push(pickRandomCard(pool, weights));
} else {
for (let i = 0; i < 3; i++) cards.push(pickRandomCard(pool, weights));
}
return cards;
}
// ── const: ENEMIES ──
// Enemy definitions with intent patterns
const ENEMIES = {
// Act 1 - The Crypts
skeleton_warrior: { name: 'Skeleton Warrior', hp: 22, maxHp: 22, intents: [
{ type: 'attack', value: 6 }, { type: 'attack', value: 6 }, { type: 'defend', value: 8 }
]},
zombie_brute: { name: 'Zombie Brute', hp: 28, maxHp: 28, intents: [
{ type: 'attack', value: 8 }, { type: 'debuff', debuffType: 'weak', value: 2 }, { type: 'attack', value: 8 }
]},
dark_cultist: { name: 'Dark Cultist', hp: 20, maxHp: 20, intents: [
{ type: 'buff', buffType: 'strength', value: 3 }, { type: 'attack', value: 5 }, { type: 'attack', value: 5 }
]},
rat_swarm: { name: 'Rat Swarm', hp: 15, maxHp: 15, intents: [
{ type: 'multi_attack', hits: 2, value: 3 }, { type: 'multi_attack', hits: 2, value: 3 }
]},
tomb_guardian: { name: 'Tomb Guardian', hp: 42, maxHp: 42, isElite: true, intents: [
{ type: 'attack_defend', attackValue: 6, defendValue: 8 }, { type: 'multi_attack', hits: 3, value: 4 }, { type: 'attack', value: 12 }
]},
blood_witch: { name: 'Blood Witch', hp: 36, maxHp: 36, isElite: true, intents: [
{ type: 'drain', attackValue: 5, healValue: 4 }, { type: 'debuff_combo', weak: 1, vulnerable: 1 }, { type: 'drain', attackValue: 7, healValue: 4 }
]},
lich_king: { name: 'Lich King', hp: 65, maxHp: 65, isBoss: true, intents: [
{ type: 'summon', summonEnemy: 'skeleton_minion' }, { type: 'attack', value: 8 }, { type: 'defend', value: 12 }
], phase2Intents: [
{ type: 'attack', value: 10 }, { type: 'attack', value: 10 }, { type: 'defend', value: 15 }
]},
skeleton_minion: { name: 'Skeleton Minion', hp: 15, maxHp: 15, intents: [
{ type: 'attack', value: 5 }, { type: 'attack', value: 5 }, { type: 'defend', value: 4 }
]},
// Act 2 - Blighted Woods
wraith: { name: 'Wraith', hp: 30, maxHp: 30, intents: [
{ type: 'attack_debuff', attackValue: 7, debuffType: 'weak', value: 1 }, { type: 'attack', value: 7 }, { type: 'defend', value: 10 }
]},
corrupted_beast: { name: 'Corrupted Beast', hp: 38, maxHp: 38, intents: [
{ type: 'attack', value: 12 }, { type: 'self_vuln_defend', defendValue: 5 }, { type: 'attack', value: 12 }
]},
thorned_horror: { name: 'Thorned Horror', hp: 25, maxHp: 25, intents: [
{ type: 'multi_attack', hits: 3, value: 4 }, { type: 'debuff', debuffType: 'bleed', value: 3 }, { type: 'attack', value: 8 }
]},
witch_doctor: { name: 'Witch Doctor', hp: 22, maxHp: 22, intents: [
{ type: 'self_buff', defendValue: 4, regenValue: 2 }, { type: 'attack_debuff', attackValue: 6, debuffType: 'poison', value: 2 }, { type: 'attack', value: 6 }
]},
treant_protector: { name: 'Treant Protector', hp: 50, maxHp: 50, isElite: true, intents: [
{ type: 'attack_defend', attackValue: 9, defendValue: 6 }, { type: 'debuff', debuffType: 'vulnerable', value: 2 }, { type: 'multi_attack', hits: 2, value: 10 }
]},
plague_shaman: { name: 'Plague Shaman', hp: 42, maxHp: 42, isElite: true, intents: [
{ type: 'debuff', debuffType: 'poison', value: 4 }, { type: 'summon', summonEnemy: 'plague_rat' }, { type: 'attack', value: 8 }
]},
plague_rat: { name: 'Plague Rat', hp: 18, maxHp: 18, intents: [
{ type: 'attack', value: 5 }, { type: 'attack', value: 5 }, { type: 'defend', value: 3 }
]},
eldritch_treant: { name: 'Eldritch Treant', hp: 75, maxHp: 75, isBoss: true, intents: [
{ type: 'attack_debuff', attackValue: 10, debuffType: 'weak', value: 2 }, { type: 'defend_regen', defendValue: 15, regenValue: 2 }, { type: 'multi_attack', hits: 2, value: 8 }
], phase2Intents: [
{ type: 'attack_debuff', attackValue: 14, debuffType: 'vulnerable', value: 1 }, { type: 'defend_heal', defendValue: 20, healValue: 5 }, { type: 'attack_debuff', attackValue: 14, debuffType: 'vulnerable', value: 1 }
]},
// Act 3 - Abyssal Spire
hellhound: { name: 'Hellhound', hp: 26, maxHp: 26, intents: [
{ type: 'attack_debuff', attackValue: 8, debuffType: 'burn', value: 2 }, { type: 'attack', value: 8 }, { type: 'defend', value: 9 }
]},
shadow_demon: { name: 'Shadow Demon', hp: 30, maxHp: 30, intents: [
{ type: 'attack_debuff', attackValue: 7, debuffType: 'weak', value: 1 }, { type: 'attack', value: 7 }, { type: 'self_buff_intangible' }
]},
infernal_imp: { name: 'Infernal Imp', hp: 18, maxHp: 18, intents: [
{ type: 'attack', value: 5 }, { type: 'attack', value: 5 }, { type: 'summon', summonEnemy: 'infernal_imp' }
]},
abyssal_cultist: { name: 'Abyssal Cultist', hp: 24, maxHp: 24, intents: [
{ type: 'buff_all', buffType: 'strength', value: 2 }, { type: 'attack', value: 7 }, { type: 'attack', value: 7 }
]},
flamespawn: { name: 'Flamespawn', hp: 48, maxHp: 48, isElite: true, intents: [
{ type: 'attack_debuff', attackValue: 9, debuffType: 'burn', value: 3 }, { type: 'defend_buff', defendValue: 10, buffType: 'strength', value: 2 }, { type: 'multi_attack', hits: 2, value: 8 }
]},
dread_knight: { name: 'Dread Knight', hp: 55, maxHp: 55, isElite: true, intents: [
{ type: 'attack', value: 12 }, { type: 'debuff', debuffType: 'bleed', value: 3 }, { type: 'defend_heal', defendValue: 12, healValue: 4 }
]},
archdemon: { name: 'Archdemon', hp: 85, maxHp: 85, isBoss: true, intents: [
{ type: 'summon', summonEnemy: 'hellhound' }, { type: 'attack_debuff', attackValue: 12, debuffType: 'burn', value: 2 }, { type: 'defend_buff', defendValue: 15, buffType: 'strength', value: 3 }
], phase2Intents: [
{ type: 'attack', value: 16 }, { type: 'multi_attack', hits: 3, value: 7 }, { type: 'attack', value: 16 }
]},
};
// ── const: ACT_ENEMY_POOLS ──
const ACT_ENEMY_POOLS = {
1: ['skeleton_warrior', 'zombie_brute', 'dark_cultist', 'rat_swarm'],
2: ['wraith', 'corrupted_beast', 'thorned_horror', 'witch_doctor'],
3: ['hellhound', 'shadow_demon', 'infernal_imp', 'abyssal_cultist'],
};
// ── const: RELICS ──
// Relics
const RELICS = [
{ id: 'bloodstone', name: 'Bloodstone', desc: 'Whenever you lose HP from a card, heal 1 HP.', rarity: 'common' },
{ id: 'shadow_cloak', name: 'Shadow Cloak', desc: 'Start combat with 1 Intangible.', rarity: 'rare' },
{ id: 'cursed_amulet', name: 'Cursed Amulet', desc: 'Start combat: apply 1 Weak to ALL enemies.', rarity: 'common' },
{ id: 'tome_dark_rituals', name: 'Tome of Dark Rituals', desc: 'When you play a Power card, draw 1 card.', rarity: 'uncommon' },
{ id: 'bleeding_edge', name: 'Bleeding Edge', desc: 'Start turn: apply 1 Bleed to random enemy.', rarity: 'common' },
{ id: 'soul_ember', name: 'Soul Ember', desc: 'When you apply Burn, deal 2 extra damage.', rarity: 'rare' },
{ id: 'eternal_hourglass', name: 'Eternal Hourglass', desc: 'Start turn: if no cards in hand, gain 1 Energy.', rarity: 'uncommon' },
{ id: 'ashen_armor', name: 'Ashen Armor', desc: 'End combat: heal 6 HP.', rarity: 'common' },
{ id: 'greedy_imp', name: 'Greedy Imp', desc: 'Enemies drop 10 extra Gold.', rarity: 'common' },
{ id: 'demon_horns', name: 'Demon Horns', desc: 'Start combat: add random Imp Claw to hand.', rarity: 'uncommon', classSpecific: CLASSES.HELLFORGED },
{ id: 'sacrificial_dagger', name: 'Sacrificial Dagger', desc: 'First Attack costing HP each combat costs 1 less HP.', rarity: 'uncommon', classSpecific: CLASSES.BLOOD_REAPER },
];
// ── const: BOSS_RELICS ──
const BOSS_RELICS = [
{ id: 'cursed_crown', name: "Cursed Crown", desc: '+1 Energy/turn. Curses can\'t be removed.' },
{ id: 'ectoplasm', name: 'Ectoplasm', desc: '+1 Energy. Lose all Gold, gain no more.' },
{ id: 'philosophers_stone', name: "Philosopher's Stone", desc: '+1 Energy. Enemies start with +1 Strength.' },
{ id: 'black_blood', name: 'Black Blood', desc: 'Start combat: lose 10% max HP. Play Attack → heal 1 HP.' },
{ id: 'shadow_amulet', name: 'Shadow Amulet', desc: 'Start turn with 1 Intangible. -15 max HP.' },
];
// ── const: POTIONS ──
// Potions
const POTIONS = [
{ id: 'blood_vial', name: 'Blood Vial', icon: '🩸', desc: 'Heal 15 HP.' },
{ id: 'firebomb', name: 'Firebomb', icon: '💥', desc: 'Deal 10 damage to ALL enemies.' },
{ id: 'dex_tonic', name: 'Dex Tonic', icon: '⚡', desc: 'Gain 3 Dexterity for 1 turn.' },
{ id: 'energy_elixir', name: 'Energy Elixir', icon: '✨', desc: 'Gain 2 Energy this turn.' },
{ id: 'cursed_brew', name: 'Cursed Brew', icon: '☠️', desc: 'Apply 3 Vulnerable to all enemies.' },
{ id: 'bleeding_flask', name: 'Bleeding Flask', icon: '🔴', desc: 'Apply 6 Bleed to an enemy.' },
{ id: 'imp_in_bottle', name: 'Imp-in-a-Bottle', icon: '👹', desc: 'Add Imp Claw+ to hand.' },
{ id: 'smoke_bomb', name: 'Smoke Bomb', icon: '💨', desc: 'Escape combat (no rewards).' },
];
// ── const: EVENTS ──
// Events
const EVENTS = [
{
id: 'forgotten_altar', title: 'The Forgotten Altar', text: 'A blood-stained altar hums with dark power.',
choices: [
{ text: '[Offer 15 HP]', effect: (g) => { g.player.hp = Math.max(1, g.player.hp - 15); g.player.relics.push({ ...RELICS[Math.floor(Math.random() * RELICS.length)] }); }},
{ text: '[Pray]', effect: (g) => { g.player.hp = Math.min(g.player.maxHp, g.player.hp + 10); addToDeck(g, DOUBT); }},
{ text: '[Leave]', effect: () => {} },
]
},
{
id: 'shadowy_peddler', title: 'Shadowy Peddler', text: 'A hooded figure offers a trade.',
choices: [
{ text: '[Trade a card for 100 Gold]', effect: (g) => { if (g.player.deck.length > 5) g.player.deck.splice(0, 1); g.player.gold += 100; }},
{ text: '[Buy wares (-50 Gold)]', effect: (g) => { g.player.gold -= 50; g.player.potions.push({ ...POTIONS[Math.floor(Math.random() * POTIONS.length)] }); g.player.relics.push({ ...RELICS[Math.floor(Math.random() * RELICS.length)] }); }},
{ text: '[Leave]', effect: () => {} },
]
},
{
id: 'cursed_spring', title: 'Cursed Spring', text: 'A spring bubbles with dark water.',
choices: [
{ text: '[Drink (-10 HP, remove curses)]', effect: (g) => { g.player.hp = Math.max(1, g.player.hp - 10); g.player.deck = g.player.deck.filter(c => c.type !== CARD_TYPES.CURSE); }},
{ text: '[Bottle it]', effect: (g) => { if (g.player.potions.length < 3) g.player.potions.push({ id: 'cursed_brew', name: 'Cursed Brew', icon: '☠️', desc: 'Apply 3 Vulnerable to all enemies.' }); }},
{ text: '[Leave]', effect: () => {} },
]
},
{
id: 'restless_dead', title: 'Restless Dead', text: 'Ghosts offer a bargain.',
choices: [
{ text: '[Accept (-20 HP, upgrade 2 cards)]', effect: (g) => { g.player.hp = Math.max(1, g.player.hp - 20); let count = 0; for (let c of g.player.deck) { if (!c.upgraded && count < 2) { upgradeCard(c); count++; } } }},
{ text: '[Refuse]', effect: () => {} },
]
},
{
id: 'impish_gambler', title: 'Impish Gambler', text: 'A grinning imp challenges you.',
choices: [
{ text: '[Bet 30 Gold (50/50)]', effect: (g) => { g.player.gold -= 30; if (Math.random() < 0.5) g.player.gold += 60; }},
{ text: '[Wager a card]', effect: (g) => { const idx = Math.floor(Math.random() * g.player.deck.length); const c = g.player.deck.splice(idx, 1)[0]; if (c.type === CARD_TYPES.CURSE) g.player.gold += 100; }},
{ text: '[Leave]', effect: () => {} },
]
},
{
id: 'ancient_library', title: 'Ancient Library', text: 'Dusty tomes contain forgotten knowledge.',
choices: [
{ text: '[Study (-5 HP, +1 Energy next combat)]', effect: (g) => { g.player.hp = Math.max(1, g.player.hp - 5); g.player.temporaryBonusEnergy = (g.player.temporaryBonusEnergy || 0) + 1; }},
{ text: '[Read forbidden page]', effect: (g) => { const pool = CLASS_POOLS[g.player.class]; const powers = pool.filter(c => c.type === CARD_TYPES.POWER); if (powers.length > 0) addToDeck(g, { ...powers[Math.floor(Math.random() * powers.length)] }); addToDeck(g, DOUBT); }},
{ text: '[Leave]', effect: () => {} },
]
},
];
// ── function: upgradeCard ──
function upgradeCard(card) {
card.upgraded = true;
if (card.id === 'strike') { card.name = 'Strike+'; card.desc = 'Deal 8 damage.'; }
else if (card.id === 'defend') { card.name = 'Defend+'; card.desc = 'Gain 8 Block.'; }
else {
const pool = [...BR_ATTACKS, ...BR_DEFENSES, ...BR_OTHERS, ...SM_ATTACKS, ...SM_DEFENSES, ...SM_OTHERS, ...HF_ATTACKS, ...HF_DEFENSES, ...HF_OTHERS, ...COLORLESS_CARDS];
const def = pool.find(c => c.id === card.id);
if (def) { card.name = def.upgName; card.desc = def.upgDesc; }
}
}
// ── function: addToDeck ──
function addToDeck(gameState, cardDef) {
gameState.player.deck.push({ ...cardDef });
}
// ── let: G ──
// Game state and map generation
let G = null;
// ── function: createGameState ──
// Global game state
function createGameState(classId) {
return {
screen: 'title',
player: {
class: classId,
hp: 70,
maxHp: 70,
gold: 99,
deck: getStartingDeck(classId),
relics: [],
potions: [],
temporaryBonusEnergy: 0,
},
act: 1,
floor: 1,
mapNodes: [],
currentNodeIndex: -1,
combat: null,
rewards: null,
shopData: null,
eventData: null,
restData: null,
};
}
// ── function: generateMap ──
function generateMap(act) {
const nodes = [];
// Floor layout: 2=Enemy,3=Enemy,4=Event,5=Shop,6=Enemy,7=Rest,8=Enemy,9=Elite/Enemy,10=Event,11=Enemy,12=Elite/Enemy,13=Enemy,14=Rest,15=Boss
const floorTypes = [
{ type: 'enemy', hasBranch: true }, // floor 2
{ type: 'enemy' }, // floor 3
{ type: 'event' }, // floor 4
{ type: 'shop' }, // floor 5
{ type: 'enemy' }, // floor 6
{ type: 'rest' }, // floor 7
{ type: 'enemy', hasBranch: true }, // floor 8
{ type: 'elite', hasAltEnemy: true }, // floor 9
{ type: 'event' }, // floor 10
{ type: 'enemy' }, // floor 11
{ type: 'elite', hasAltEnemy: true }, // floor 12
{ type: 'enemy' }, // floor 13
{ type: 'rest' }, // floor 14
{ type: 'boss' }, // floor 15
];
const enemyPool = ACT_ENEMY_POOLS[act];
const bossKey = act === 1 ? 'lich_king' : act === 2 ? 'eldritch_treant' : 'archdemon';
const eliteKeys = act === 1 ? ['tomb_guardian', 'blood_witch'] : act === 2 ? ['treant_protector', 'plague_shaman'] : ['flamespawn', 'dread_knight'];
for (let i = 0; i < floorTypes.length; i++) {
const ft = floorTypes[i];
const node = { floor: i + 2, type: ft.type, visited: false, available: false };
if (ft.type === 'enemy') {
const eKey = enemyPool[Math.floor(Math.random() * enemyPool.length)];
node.enemyKeys = [eKey];
if (ft.hasBranch) {
const altKey = enemyPool[Math.floor(Math.random() * enemyPool.length)];
node.branchEnemyKey = altKey;
}
} else if (ft.type === 'elite') {
node.eliteKey = eliteKeys[i - 7] || eliteKeys[0]; // approximate index
const eKey = enemyPool[Math.floor(Math.random() * enemyPool.length)];
node.altEnemyKey = eKey;
} else if (ft.type === 'boss') {
node.enemyKeys = [bossKey];
}
nodes.push(node);
}
return nodes;
}
// ── function: getNodeIcon ──
function getNodeIcon(type) {
switch (type) {
case 'enemy': return '⚔️';
case 'elite': return '💀';
case 'rest': return '🏕️';
case 'shop': return '🛒';
case 'event': return '❓';
case 'boss': return '👑';
default: return '?';
}
}
// ── function: getActName ──
function getActName(act) {
switch (act) {
case 1: return 'The Crypts';
case 2: return 'Blighted Woods';
case 3: return 'Abyssal Spire';
default: return '';
}
}
// ── function: getClassName ──
function getClassName(classId) {
switch (classId) {
case CLASSES.BLOOD_REAPER: return 'Blood Reaper';
case CLASSES.SHADOWMANCER: return 'Shadowmancer';
case CLASSES.HELLFORGED: return 'Hellforged';
default: return '';
}
}
// ── function: getClassIcon ──
function getClassIcon(classId) {
switch (classId) {
case CLASSES.BLOOD_REAPER: return '🩸';
case CLASSES.SHADOWMANCER: return '🌑';
case CLASSES.HELLFORGED: return '🔥';
default: return '?';
}
}
// ── function: getClassDesc ──
function getClassDesc(classId) {
switch (classId) {
case CLASSES.BLOOD_REAPER: return 'Sacrifice HP for devastating power and life steal.';
case CLASSES.SHADOWMANCER: return 'Debuff enemies with Weak, Vulnerable, and Intangible.';
case CLASSES.HELLFORGED: return 'Fire damage, Burn effects, and summon Imps.';
default: return '';
}
}
// ── function: shuffleArray ──
function shuffleArray(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// ── function: showScreen ──
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const el = document.getElementById(id);
if (el) el.classList.add('active');
}
// ── function: renderTitleScreen ──
// UI Rendering Functions
function renderTitleScreen() {
const el = document.getElementById('screen-title');
el.innerHTML = `
<div class="title-content">
<h1 class="game-title">Shadows of the Abyss</h1>
<p class="game-subtitle">A Dark Fantasy Roguelike Deckbuilder</p>
<div class="class-select">
<div class="class-card" onclick="selectClass('${CLASSES.BLOOD_REAPER}')">
<div class="class-icon">🩸</div>
<div class="class-name">Blood Reaper</div>
<div class="class-desc">Sacrifice HP for devastating power and life steal.</div>
</div>
<div class="class-card" onclick="selectClass('${CLASSES.SHADOWMANCER}')">
<div class="class-icon">🌑</div>
<div class="class-name">Shadowmancer</div>
<div class="class-desc">Debuff enemies with Weak, Vulnerable, and Intangible.</div>
</div>
<div class="class-card" onclick="selectClass('${CLASSES.HELLFORGED}')">
<div class="class-icon">🔥</div>
<div class="class-name">Hellforged</div>
<div class="class-desc">Fire damage, Burn effects, and summon Imps.</div>
</div>
</div>
</div>
`;
}
// ── function: renderMapScreen ──
function renderMapScreen() {
const el = document.getElementById('screen-map');
let nodesHtml = '';
for (let i = G.mapNodes.length - 1; i >= 0; i--) {
const node = G.mapNodes[i];
if (!node) continue;
let cls = `map-node node-${node.type}`;
if (node.visited) cls += ' visited';
else if (node.available) cls += ' available';
else cls += ' locked';
const onclick = node.available ? `onclick="selectNode(${i})"` : '';
nodesHtml += `<div class="map-row">
<div class="${cls}" ${onclick} title="${node.type.toUpperCase()} - Floor ${node.floor}">${getNodeIcon(node.type)}</div>
</div>`;
}
el.innerHTML = `
<div class="map-header">
<div class="act-title">Act ${G.act}: ${getActName(G.act)}</div>
<div class="floor-info">Floor ${G.floor} of 15</div>
<div class="player-stats-bar">
<span class="stat-item">❤️ ${G.player.hp}/${G.player.maxHp}</span>
<span class="stat-item">💰 ${G.player.gold}</span>
<span class="stat-item">🃏 ${G.player.deck.length} cards</span>
<span class="stat-item">🏺 ${G.player.relics.length} relics</span>
</div>
</div>
<div class="map-container">
<div class="map-grid">${nodesHtml}</div>
</div>
`;
// Mark next nodes as available
updateMapAvailability();
}
// ── function: updateMapAvailability ──
function updateMapAvailability() {
const visitedCount = G.mapNodes.filter(n => n.visited).length;
if (visitedCount < G.mapNodes.length) {
const nextNode = G.mapNodes[visitedCount];
if (nextNode && !nextNode.visited) {
nextNode.available = true;
}
}
}
// ── function: renderCombatScreen ──
function renderCombatScreen() {
const c = G.combat;
const el = document.getElementById('screen-combat');
let enemiesHtml = '';
for (let i = 0; i < c.enemies.length; i++) {
const e = c.enemies[i];
if (!e.isAlive) continue;
const hpPct = Math.max(0, (e.currentHp / e.maxHp) * 100);
let intentText = '';
let intents = e.isBoss && e.inPhase2 ? e.phase2Intents : e.intents;
const intent = intents[e.intentIndex % intents.length];
switch (intent.type) {
case 'attack': intentText = `⚔️ ${intent.value + (e.buffs.strength || 0)} dmg`; break;
case 'defend': intentText = `🛡️ ${intent.value} Block`; break;
case 'multi_attack': intentText = `⚔️ ${intent.hits}×${intent.value}`; break;
case 'attack_debuff': intentText = `⚔️ ${intent.attackValue + (e.buffs.strength || 0)} + ${intent.debuffType}`; break;
case 'debuff': intentText = `☠️ ${intent.debuffType} ×${intent.value}`; break;
case 'summon': intentText = `👹 Summon`; break;
case 'drain': intentText = `💀 Drain`; break;
default: intentText = `⚔️ ?`;
}
let enemyBuffsHtml = '';
if (e.buffs.strength > 0) enemyBuffsHtml += `<div class="enemy-buff-icon" style="background:#8b3a3a;" title="Strength +${e.buffs.strength}">💪</div>`;
if (e.debuffs.weak > 0 && e.debuffs.weakDuration > 0) enemyBuffsHtml += `<div class="enemy-buff-icon" style="background:#5a3a7a;" title="Weak ${e.debuffs.weakDuration}">😰</div>`;
if (e.debuffs.vulnerable > 0 && e.debuffs.vulnerableDuration > 0) enemyBuffsHtml += `<div class="enemy-buff-icon" style="background:#7a3a3a;" title="Vulnerable ${e.debuffs.vulnerableDuration}">🔥</div>`;
if (e.debuffs.bleed > 0) enemyBuffsHtml += `<div class="enemy-buff-icon" style="background:#8b1a1a;" title="Bleed ${e.debuffs.bleed}">🩸</div>`;
if (e.debuffs.burn > 0) enemyBuffsHtml += `<div class="enemy-buff-icon" style="background:#db5a1a;" title="Burn ${e.debuffs.burn}">🔥</div>`;
enemiesHtml += `<div class="enemy-card targetable" onclick="selectTargetEnemy(${i})">
<div class="enemy-name">${e.name}</div>
<div class="enemy-hp-bar"><div class="enemy-hp-fill" style="width:${hpPct}%"></div></div>
<div style="font-size:0.7em;color:#9a9ab0;">${e.currentHp}/${e.maxHp} HP</div>
${e.block > 0 ? `<div style="font-size:0.7em;color:#6ab0de;">🛡️ ${e.block}</div>` : ''}
<div class="intent-icon">${intentText}</div>
<div class="enemy-debuffs">${enemyBuffsHtml}</div>
</div>`;
}
let playerBuffsHtml = '';
if (c.player.buffs.strength > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#8b3a3a;" title="Strength +${c.player.buffs.strength}">💪<span class="buff-count">${c.player.buffs.strength}</span></div>`;
if (c.player.buffs.dexterity > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#3a7a3a;" title="Dex +${c.player.buffs.dexterity}">🏃<span class="buff-count">${c.player.buffs.dexterity}</span></div>`;
if (c.player.buffs.regeneration > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#3adb3a;" title="Regen ${c.player.buffs.regeneration}">💚<span class="buff-count">${c.player.buffs.regeneration}</span></div>`;
if (c.player.debuffs.weak > 0 && c.player.debuffs.weakDuration > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#5a3a7a;" title="Weak ${c.player.debuffs.weakDuration}">😰<span class="buff-count">${c.player.debuffs.weakDuration}</span></div>`;
if (c.player.debuffs.vulnerable > 0 && c.player.debuffs.vulnerableDuration > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#7a3a3a;" title="Vuln ${c.player.debuffs.vulnerableDuration}">🔥<span class="buff-count">${c.player.debuffs.vulnerableDuration}</span></div>`;
if (c.player.debuffs.bleed > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#8b1a1a;" title="Bleed ${c.player.debuffs.bleed}">🩸<span class="buff-count">${c.player.debuffs.bleed}</span></div>`;
if (c.player.debuffs.burn > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#db5a1a;" title="Burn ${c.player.debuffs.burn}">🔥<span class="buff-count">${c.player.debuffs.burn}</span></div>`;
if (c.player.debuffs.poison > 0 && c.player.debuffs.poisonDuration > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#3a7a1a;" title="Poison ${c.player.debuffs.poisonDuration}">☠️<span class="buff-count">${c.player.debuffs.poisonDuration}</span></div>`;
if (c.player.debuffs.intangible > 0) playerBuffsHtml += `<div class="buff-icon" style="background:#5a5a9c;" title="Intangible ${c.player.debuffs.intangible}">👻<span class="buff-count">${c.player.debuffs.intangible}</span></div>`;
let handHtml = '';
for (let i = 0; i < c.hand.length; i++) {
const card = c.hand[i];
const playable = c.isPlayerTurn && canPlayCardInCombat(card);
const typeClass = `type-${card.type}`;
handHtml += `<div class="game-card ${playable ? '' : 'unplayable'}" onclick="${playable ? `playCardFromHand(${i})` : ''}">
<div class="card-cost">${card.cost >= 0 ? card.cost : (card.id === 'vital_surge' ? 'X' : '?')}</div>
<div class="card-type-badge ${typeClass}"></div>
<div class="card-name rarity-${card.rarity}">${card.name}</div>
<div class="card-desc">${card.desc}</div>
</div>`;
}
let potionsHtml = '';
for (let i = 0; i < G.player.potions.length; i++) {
const p = G.player.potions[i];
potionsHtml += `<div class="potion-slot" title="${p.name}: ${p.desc}" onclick="usePotionFromCombat(${i})">${p.icon}</div>`;
}
el.innerHTML = `
<div class="combat-header">
<div class="combat-player-info">
<span>❤️ ${c.player.hp}/${c.player.maxHp}</span>
<div class="hp-bar-container"><div class="hp-bar-fill" style="width:${(c.player.hp/c.player.maxHp)*100}%"></div><span class="hp-text">${c.player.hp}/${c.player.maxHp}</span></div>
</div>
<div class="combat-player-info">
<span>⚡ ${c.player.energy}/${c.player.maxEnergy}</span>
<span>🛡️ ${c.player.block}</span>
<span style="color:#6a6a8c;">Turn ${c.turn}</span>
</div>
</div>
<div class="combat-area">
<div class="enemy-zone">${enemiesHtml || '<div style="color:#4a4a6c;text-align:center;">All enemies defeated!</div>'}</div>
<div class="player-zone">
<div class="player-avatar">${getClassIcon(G.player.class)}</div>
${c.player.block > 0 ? `<div class="player-block">🛡️ ${c.player.block}</div>` : ''}
<div class="buffs-row">${playerBuffsHtml}</div>
</div>
</div>
<div class="hand-area">
<div class="hand-cards">${handHtml || '<div style="color:#4a4a6c;text-align:center;">No cards in hand</div>'}</div>
</div>
<div class="discard-pile-info">Draw: ${c.drawPile.length} | Discard: ${c.discardPile.length} | Powers: ${c.powers.length}</div>
<div class="combat-footer">
<div class="potion-slots">${potionsHtml}</div>
<button class="end-turn-btn" onclick="${c.isPlayerTurn ? 'endCombatTurn()' : ''}" ${!c.isPlayerTurn ? 'disabled style="opacity:0.3"' : ''}>End Turn</button>
</div>
`;
}
// ── function: canPlayCardInCombat ──
function canPlayCardInCombat(card) {
const c = G.combat;
if (!c.isPlayerTurn) return false;
if (card.type === CARD_TYPES.CURSE || card.type === CARD_TYPES.STATUS) return false;
if (card.cost < 0 && card.id !== 'vital_surge') return false;
if (card.cost > c.player.energy && !(card.cost < 0)) return false;
return true;
}
// ── function: renderRewardsScreen ──
function renderRewardsScreen() {
const el = document.getElementById('screen-rewards');
const r = G.rewards;
let cardsHtml = '';
for (let i = 0; i < r.cards.length; i++) {
const card = r.cards[i];
cardsHtml += `<div class="reward-card" onclick="chooseRewardCard(${i})">
<div style="text-align:center;margin-bottom:5px;">
<span style="color:#c9a84c;font-size:0.9em;">${card.name}</span>
</div>
<div class="rarity-${card.rarity}" style="font-size:0.7em;text-align:center;">${card.rarity.toUpperCase()}</div>
<div style="color:#9a9ab0;font-size:0.75em;text-align:center;margin-top:8px;flex:1;">${card.desc}</div>
</div>`;
}
el.innerHTML = `
<h2 class="reward-title">Victory!</h2>
<div class="gold-reward">💰 +${r.gold} Gold</div>
<p style="color:#8a7a5c;margin-bottom:10px;">Choose a card:</p>
<div class="card-choice-row">${cardsHtml}</div>
${r.potion ? `<button class="btn-secondary" onclick="takePotionReward()" style="margin-top:10px;">🧪 Take Potion</button>` : ''}
<br><button class="btn-secondary" onclick="skipRewards()">Skip</button>
`;
}
// ── function: renderShopScreen ──
function renderShopScreen() {
const el = document.getElementById('screen-shop');
const shop = G.shopData;
let itemsHtml = '';
// Cards for sale
for (let i = 0; i < shop.cards.length; i++) {
const card = shop.cards[i];
const price = card.rarity === RARITIES.RARE ? 150 : card.rarity === RARITIES.UNCOMMON ? 75 : 50;
itemsHtml += `<div class="shop-item" onclick="buyShopCard(${i})">
<div style="color:#c9a84c;font-size:0.8em;">${card.name}</div>
<div class="rarity-${card.rarity}" style="font-size:0.65em;">${card.rarity.toUpperCase()}</div>
<div style="color:#9a9ab0;font-size:0.65em;margin-top:4px;">${card.desc}</div>
<div class="shop-price">💰 ${price}g</div>
</div>`;
}
// Relics for sale
for (let i = 0; i < shop.relics.length; i++) {
const relic = shop.relics[i];
itemsHtml += `<div class="shop-item" onclick="buyShopRelic(${i})">
<div style="color:#c9a84c;font-size:0.8em;">${relic.name}</div>
<div style="color:#9a9ab0;font-size:0.65em;margin-top:4px;">${relic.desc}</div>
<div class="shop-price">💰 150g</div>
</div>`;
}
// Potions for sale
for (let i = 0; i < shop.potions.length; i++) {
const potion = shop.potions[i];
itemsHtml += `<div class="shop-item" onclick="buyShopPotion(${i})">
<div style="font-size:1.2em;">${potion.icon}</div>
<div style="color:#c9a84c;font-size:0.75em;">${potion.name}</div>
<div class="shop-price">💰 50g</div>
</div>`;
}
// Card removal option
if (shop.canRemove) {
itemsHtml += `<div class="shop-item" onclick="removeCardFromShop()">
<div style="font-size:1.2em;">🗑️</div>
<div style="color:#c9a84c;font-size:0.75em;">Remove Card</div>
<div class="shop-price">💰 75g</div>
</div>`;
}
el.innerHTML = `
<h2 class="reward-title">🛒 Shop</h2>
<div style="color:#c9a84c;margin-bottom:10px;">💰 ${G.player.gold} Gold</div>
<div class="shop-grid">${itemsHtml}</div>
<button class="btn-secondary" onclick="leaveShop()">Leave Shop</button>
`;
}
// ── function: renderEventScreen ──
function renderEventScreen() {
const el = document.getElementById('screen-event');
const evt = G.eventData;
let choicesHtml = '';
for (let i = 0; i < evt.choices.length; i++) {
choicesHtml += `<button class="btn-primary" onclick="chooseEvent(${i})">${evt.choices[i].text}</button>`;
}
el.innerHTML = `
<h2 class="reward-title">${evt.title}</h2>
<p class="event-text">${evt.text}</p>
<div class="event-choices">${choicesHtml}</div>
`;
}
// ── function: renderRestScreen ──
function renderRestScreen() {
const el = document.getElementById('screen-rest');
// Deck view for forge/meditate
let deckCardsHtml = '';
for (let i = 0; i < G.player.deck.length; i++) {
const card = G.player.deck[i];
deckCardsHtml += `<div class="mini-card" onclick="selectDeckCard(${i})">
<div style="color:#c9a84c;font-size:0.7em;text-align:center;">${card.name}</div>
<div style="color:#9a9ab0;font-size:0.6em;text-align:center;margin-top:2px;">${card.desc.substring(0, 30)}...</div>
</div>`;
}
el.innerHTML = `
<h2 class="reward-title">🏕️ Rest Site</h2>
<p style="color:#8a7a5c;margin-bottom:15px;">Choose an action:</p>
<div class="rest-options" id="restOptions">
<div class="rest-option" onclick="restAction('heal')">
<div style="font-size:2em;">💤</div>
<div style="color:#c9a84c;margin-top:5px;">Rest</div>
<div style="color:#8a7a5c;font-size:0.75em;">Heal 25% HP</div>
</div>
<div class="rest-option" onclick="restAction('forge')">
<div style="font-size:2em;">⚒️</div>
<div style="color:#c9a84c;margin-top:5px;">Forge</div>
<div style="color:#8a7a5c;font-size:0.75em;">Upgrade a card</div>
</div>
<div class="rest-option" onclick="restAction('meditate')">
<div style="font-size:2em;">🧘</div>
<div style="color:#c9a84c;margin-top:5px;">Meditate</div>
<div style="color:#8a7a5c;font-size:0.75em;">Remove a card</div>
</div>
</div>
<div id="restDeckView" class="deck-view" style="display:none;">
<p style="color:#8a7a5c;margin-bottom:10px;font-size:0.9em;" id="restDeckTitle">Select a card:</p>
<div class="deck-cards-grid">${deckCardsHtml}</div>
<button class="btn-secondary" onclick="cancelRestAction()">Cancel</button>
</div>
`;
}
// ── function: renderVictoryScreen ──
function renderVictoryScreen() {
const el = document.getElementById('screen-victory');
el.innerHTML = `
<h1 class="victory-text">🏆 Victory!</h1>
<p style="color:#c9a84c;font-size:1.2em;">You have defeated the Archdemon and conquered the Abyss!</p>
<p style="color:#8a7a5c;margin-top:10px;">Class: ${getClassName(G.player.class)} | Act 3 Cleared</p>
<button class="btn-primary" onclick="location.reload()" style="margin-top:20px;">Play Again</button>
`;
}
// ── function: renderDefeatScreen ──
function renderDefeatScreen() {
const el = document.getElementById('screen-defeat');
el.innerHTML = `
<h1 class="defeat-text">💀 Defeated</h1>
<p style="color:#8a7a5c;font-size:1.1em;">Your soul has been consumed by the Abyss...</p>
<p style="color:#6a6a8c;margin-top:10px;">Act ${G.act}, Floor ${G.floor}</p>
<button class="btn-primary" onclick="location.reload()" style="margin-top:20px;">Try Again</button>
`;
}
// ── let: selectedDeckCardIndex ──
// Game Flow Logic
let selectedDeckCardIndex = -1;
// ── let: currentRestAction ──
let currentRestAction = '';
// ── function: selectClass ──
function selectClass(classId) {
G = createGameState(classId);
G.mapNodes = generateMap(1);
G.screen = 'map';
showScreen('screen-map');
renderMapScreen();
}
// ── function: selectNode ──
function selectNode(index) {
const node = G.mapNodes[index];
if (!node || !node.available || node.visited) return;
node.visited = true;
node.available = false;
G.floor = node.floor;
// Mark next nodes available
updateMapAvailability();
switch (node.type) {
case 'enemy': startCombat(node.enemyKeys); break;
case 'elite': startCombat([node.eliteKey]); break;
case 'boss': startCombat(node.enemyKeys, true); break;
case 'rest': showScreen('screen-rest'); renderRestScreen(); break;
case 'shop': openShop(); break;
case 'event': triggerEvent(); break;
}
}
// ── function: startCombat ──
function startCombat(enemyKeys, isBoss = false) {
G.combat = new Combat(enemyKeys, G.player);
G.combat.isBossFight = isBoss;
// Start first player turn (draw cards, gain energy)
G.combat.startPlayerTurn();
showScreen('screen-combat');
renderCombatScreen();
}
// ── function: selectTargetEnemy ──
function selectTargetEnemy(index) {
if (!G.combat || !G.combat.isPlayerTurn) return;
G.combat.targetEnemyIndex = index;
}
// ── function: playCardFromHand ──
function playCardFromHand(index) {
if (!G.combat || !G.combat.isPlayerTurn) return;
const result = G.combat.playCard(index);
if (result) renderCombatScreen();
}
// ── function: endCombatTurn ──
function endCombatTurn() {
if (!G.combat || !G.combat.isPlayerTurn) return;
G.combat.endPlayerTurn();
// Check combat outcome after enemy turn
if (G.combat.won) {
setTimeout(() => showCombatRewards(false), 500);
} else if (G.combat.lost) {
setTimeout(() => {
showScreen('screen-defeat');
renderDefeatScreen();
}, 500);
} else if (G.combat.escaped) {
// Smoke bomb escape - no rewards, continue map
G.screen = 'map';
showScreen('screen-map');
renderMapScreen();
} else {
renderCombatScreen();
}
}
// ── function: usePotionFromCombat ──
function usePotionFromCombat(index) {
if (!G.combat || !G.combat.isPlayerTurn) return;
const result = G.combat.usePotion(index);
if (result && G.combat.escaped) {
G.screen = 'map';
showScreen('screen-map');
renderMapScreen();
} else if (result) {
renderCombatScreen();
}
}
// ── function: showCombatRewards ──
function showCombatRewards(isBoss) {
const c = G.combat;
// Calculate gold reward
let goldReward = isBoss ? 50 : Math.floor(Math.random() * 11) + 10; // 10-20 normal, 25-35 elite, 50 boss
if (c.enemies.some(e => e.isElite)) goldReward = Math.floor(Math.random() * 11) + 25;
// Greedy Imp relic bonus
for (const r of G.player.relics) {
if (r.id === 'greedy_imp') goldReward += 10;
}
// Ectoplasm: no gold gain
for (const r of G.player.relics) {
if (r.id === 'ectoplasm') goldReward = 0;
}
G.player.gold += goldReward;
// Ashen Armor relic: heal at end of combat
for (const r of G.player.relics) {
if (r.id === 'ashen_armor') G.player.hp = Math.min(G.player.maxHp, G.player.hp + 6);
}
// Card rewards
const pool = CLASS_POOLS[G.player.class];
const cards = pickRewardCards(pool, isBoss, c.enemies.some(e => e.isElite));
// Potion chance
let potionDrop = false;
if (isBoss) {
potionDrop = true;
// Boss drops 2 potions
for (let i = 0; i < 2; i++) {
if (G.player.potions.length < 3) G.player.potions.push({ ...POTIONS[Math.floor(Math.random() * POTIONS.length)] });
}
} else if (c.enemies.some(e => e.isElite)) {
potionDrop = Math.random() < 0.7;
} else {
potionDrop = Math.random() < 0.4;
}
// Relic from elite
let relicReward = null;
if (c.enemies.some(e => e.isElite)) {
const eligibleRelics = RELICS.filter(r => !r.classSpecific || r.classSpecific === G.player.class);
relicReward = { ...eligibleRelics[Math.floor(Math.random() * eligibleRelics.length)] };
G.player.relics.push(relicReward);
}
// Boss rewards: choose from boss relics
let bossRelicChoice = null;
if (isBoss) {
const available = BOSS_RELICS.filter(r => !G.player.relics.find(pr => pr.id === r.id));
bossRelicChoice = [];
for (let i = 0; i < Math.min(3, available.length); i++) {
bossRelicChoice.push({ ...available[i] });
}
}
// Full heal after boss
if (isBoss) {
G.player.hp = G.player.maxHp;
}
G.rewards = { gold: goldReward, cards, potionDrop, relicReward, bossRelicChoice };
// Sync player HP from combat back to game state
G.player.hp = c.player.hp;
showScreen('screen-rewards');
renderRewardsScreen();
}
// ── function: chooseRewardCard ──
function chooseRewardCard(index) {
const card = G.rewards.cards[index];
addToDeck(G, card);
proceedAfterReward();
}
// ── function: takePotionReward ──
function takePotionReward() {
if (G.player.potions.length < 3) {
G.player.potions.push({ ...POTIONS[Math.floor(Math.random() * POTIONS.length)] });
}
proceedAfterReward();
}
// ── function: skipRewards ──
function skipRewards() {
proceedAfterReward();
}
// ── function: proceedAfterReward ──
function proceedAfterReward() {
// Check for boss relic choice
if (G.rewards.bossRelicChoice && G.rewards.bossRelicChoice.length > 0) {
showBossRelicChoice(G.rewards.bossRelicChoice);
return;
}
advanceMap();
}
// ── function: showBossRelicChoice ──
function showBossRelicChoice(relics) {
const el = document.getElementById('screen-rewards');
let relicsHtml = '';
for (let i = 0; i < relics.length; i++) {
const r = relics[i];
relicsHtml += `<div class="reward-card" onclick="chooseBossRelic(${i})">
<div style="text-align:center;margin-bottom:5px;">
<span style="color:#c9a84c;font-size:0.9em;">${r.name}</span>
</div>
<div style="color:#9a9ab0;font-size:0.75em;text-align:center;margin-top:8px;">${r.desc}</div>
</div>`;
}
el.innerHTML = `
<h2 class="reward-title">Boss Relic</h2>
<p style="color:#8a7a5c;margin-bottom:10px;">Choose a relic:</p>
<div class="card-choice-row">${relicsHtml}</div>
<button class="btn-secondary" onclick="skipBossRelic()">Skip</button>
`;
}
// ── function: chooseBossRelic ──
function chooseBossRelic(index) {
const relics = G.rewards.bossRelicChoice;
if (!relics || !relics[index]) return;
const relic = { ...relics[index] };
G.player.relics.push(relic);
// Apply immediate effects
switch (relic.id) {
case 'ectoplasm': G.player.gold = 0; break;
case 'shadow_amulet': G.player.maxHp -= 15; G.player.hp = Math.min(G.player.hp, G.player.maxHp); break;
}
advanceMap();
}
// ── function: skipBossRelic ──
function skipBossRelic() {
advanceMap();
}
// ── function: advanceMap ──
function advanceMap() {
const visitedCount = G.mapNodes.filter(n => n.visited).length;
if (visitedCount >= G.mapNodes.length) {
// Act complete
if (G.act < 3) {
G.act++;
G.floor = 1;
G.mapNodes = generateMap(G.act);
showScreen('screen-map');
renderMapScreen();
} else {
// Victory!
showScreen('screen-victory');
renderVictoryScreen();
}
} else {
showScreen('screen-map');
renderMapScreen();
}
}
// ── function: openShop ──
// Shop functions
function openShop() {
const pool = CLASS_POOLS[G.player.class];
G.shopData = {
cards: [pickRandomCard(pool), pickRandomCard(pool), pickRandomCard(pool)],
colorlessCard: COLORLESS_CARDS[Math.floor(Math.random() * COLORLESS_CARDS.length)],
relics: [],
potions: [],
canRemove: true,
};
// Add a colorless card to the shop cards
G.shopData.cards.push({ ...G.shopData.colorlessCard });
// Random relics for sale (2)
const eligibleRelics = RELICS.filter(r => !r.classSpecific || r.classSpecific === G.player.class);
for (let i = 0; i < 2; i++) {
G.shopData.relics.push({ ...eligibleRelics[Math.floor(Math.random() * eligibleRelics.length)] });
}
// Random potions for sale (3)
for (let i = 0; i < 3; i++) {
G.shopData.potions.push({ ...POTIONS[Math.floor(Math.random() * POTIONS.length)] });
}
showScreen('screen-shop');
renderShopScreen();
}
// ── function: buyShopCard ──
function buyShopCard(index) {
const card = G.shopData.cards[index];
if (!card) return;
const price = card.rarity === RARITIES.RARE ? 150 : card.rarity === RARITIES.UNCOMMON ? 75 :
(card.id && COLORLESS_CARDS.find(c => c.id === card.id)) ? 60 : 50;
if (G.player.gold < price) return;
G.player.gold -= price;
addToDeck(G, card);
G.shopData.cards.splice(index, 1);
renderShopScreen();
}
// ── function: buyShopRelic ──
function buyShopRelic(index) {
const relic = G.shopData.relics[index];
if (!relic || G.player.gold < 150) return;
G.player.gold -= 150;
G.player.relics.push(relic);
G.shopData.relics.splice(index, 1);
renderShopScreen();
}
// ── function: buyShopPotion ──
function buyShopPotion(index) {
const potion = G.shopData.potions[index];
if (!potion || G.player.gold < 50) return;
if (G.player.potions.length >= 3) return;
G.player.gold -= 50;
G.player.potions.push(potion);
G.shopData.potions.splice(index, 1);
renderShopScreen();
}
// ── function: removeCardFromShop ──
function removeCardFromShop() {
if (G.player.gold < 75 || !G.shopData.canRemove) return;
// Show deck to choose from
const el = document.getElementById('screen-shop');
let deckCardsHtml = '';
for (let i = 0; i < G.player.deck.length; i++) {
const card = G.player.deck[i];
deckCardsHtml += `<div class="mini-card" onclick="confirmShopRemove(${i})">
<div style="color:#c9a84c;font-size:0.7em;text-align:center;">${card.name}</div>
<div style="color:#9a9ab0;font-size:0.6em;text-align:center;margin-top:2px;">${card.desc.substring(0, 30)}...</div>
</div>`;
}
el.innerHTML = `
<h2 class="reward-title">🗑️ Remove a Card</h2>
<p style="color:#8a7a5c;margin-bottom:10px;">Select a card to remove (75g):</p>
<div class="deck-view"><div class="deck-cards-grid">${deckCardsHtml}</div></div>
<button class="btn-secondary" onclick="renderShopScreen()">Cancel</button>
`;
}
// ── function: confirmShopRemove ──
function confirmShopRemove(index) {
G.player.gold -= 75;
G.player.deck.splice(index, 1);
G.shopData.canRemove = false;
renderShopScreen();
}
// ── function: leaveShop ──
function leaveShop() {
advanceMap();
}
// ── function: triggerEvent ──
// Event functions
function triggerEvent() {
const evt = EVENTS[Math.floor(Math.random() * EVENTS.length)];
G.eventData = evt;
showScreen('screen-event');
renderEventScreen();
}
// ── function: chooseEvent ──
function chooseEvent(index) {
const choice = G.eventData.choices[index];
if (choice && choice.effect) {
choice.effect(G);
}
advanceMap();
}
// ── function: restAction ──
// Rest functions
function restAction(action) {
currentRestAction = action;
if (action === 'heal') {
G.player.hp = Math.min(G.player.maxHp, G.player.hp + Math.floor(G.player.maxHp * 0.25));
advanceMap();
return;
}
// Show deck for forge/meditate
document.getElementById('restOptions').style.display = 'none';
const deckView = document.getElementById('restDeckView');
if (deckView) {
deckView.style.display = 'block';
document.getElementById('restDeckTitle').textContent = action === 'forge' ? 'Select a card to upgrade:' : 'Select a card to remove:';
}
}
// ── function: selectDeckCard ──
function selectDeckCard(index) {
selectedDeckCardIndex = index;
if (currentRestAction === 'forge') {
const card = G.player.deck[index];
if (!card.upgraded) {
upgradeCard(card);
}
} else if (currentRestAction === 'meditate') {
G.player.deck.splice(index, 1);
}
advanceMap();
}
// ── function: cancelRestAction ──
function cancelRestAction() {
currentRestAction = '';
selectedDeckCardIndex = -1;
renderRestScreen();
}
// ── class: Combat ──
class Combat {
constructor(enemyKeys, playerState) {
this.enemies = [];
for (const key of enemyKeys) {
const def = ENEMIES[key];
if (!def) continue;
this.enemies.push({ ...def, currentHp: def.hp, block: 0, intentIndex: Math.floor(Math.random() * 3), buffs: { strength: 0, dexterity: 0, regeneration: 0 }, debuffs: { weak: 0, vulnerable: 0, bleed: 0, burn: 0, poison: 0, intangible: 0, weakDuration: 0, vulnerableDuration: 0, bleedDuration: 0, burnDuration: 0, poisonDuration: 0 }, isAlive: true });
}
this.player = { hp: playerState.hp, maxHp: playerState.maxHp, block: 0, energy: 3, maxEnergy: 3, buffs: { strength: 0, dexterity: 0, regeneration: 0 }, debuffs: { weak: 0, vulnerable: 0, bleed: 0, burn: 0, poison: 0, intangible: 0, weakDuration: 0, vulnerableDuration: 0, bleedDuration: 0, burnDuration: 0, poisonDuration: 0 }, nextTurnBlock: 0, nextTurnEnergy: 0 };
this.drawPile = shuffleArray(playerState.deck.map(c => ({ ...c })));
this.discardPile = []; this.hand = []; this.powers = []; this.exhausted = [];
this.turn = 1; this.isPlayerTurn = true; this.targetEnemyIndex = null;
this.firstAttackCostsHpThisCombat = true;
this._applyStartRelics(playerState);
}
_applyStartRelics(ps) {
const relics = ps.relics || [];
for (const r of relics) {
if (r.id === 'shadow_cloak') this.player.debuffs.intangible++;
if (r.id === 'cursed_amulet') for (const e of this.enemies) { if (e.isAlive) { e.debuffs.weak++; e.debuffs.weakDuration = 99; } }
if (r.id === 'black_blood') this.player.hp -= Math.floor(this.player.maxHp * 0.1);
if (r.id === 'shadow_amulet') this.player.debuffs.intangible++;
}
for (const r of relics) {
if (r.id === 'demon_horns') this.hand.push({ id: 'imp_claw', name: 'Imp Claw', type: CARD_TYPES.ATTACK, cost: 0, rarity: RARITIES.BASIC, desc: 'Deal 4 damage. Exhaust.', exhaust: true });
if (['cursed_crown', 'ectoplasm', 'philosophers_stone'].includes(r.id)) this.player.maxEnergy = 4;
}
}
startPlayerTurn() {
this.isPlayerTurn = true; this.turn++; this.player.block = 0;
if (this.player.nextTurnBlock) { this.player.block += this.player.nextTurnBlock; this.player.nextTurnBlock = 0; }
if (this.player.debuffs.bleed > 0) { const d = this._int(this.player.debuffs.bleed); this.player.hp -= d; this.player.debuffs.bleed = Math.floor(this.player.debuffs.bleed / 2); }
if (this.player.debuffs.poison > 0 && this.player.debuffs.poisonDuration > 0) { const d = this._int(this.player.debuffs.poison); this.player.hp -= d; this.player.debuffs.poison--; this.player.debuffs.poisonDuration--; }
if (this.player.debuffs.intangible > 0) this.player.debuffs.intangible--;
for (const p of this.powers) {
if (p.id === 'soul_siphon') { this.player.energy = Math.min(this.player.maxEnergy, this.player.energy + 1); this._dmgPlayer(1, true); }
if (p.id === 'umbral_mastery') { const a = p.upgraded ? 2 : 1; for (const e of this.enemies) { if (e.isAlive) { e.debuffs.weak += a; e.debuffs.weakDuration = Math.max(e.debuffs.weakDuration, 99); } } }
if (p.id === 'hellfire_aura') { const d = p.upgraded ? 5 : 3; for (const e of this.enemies) { if (e.isAlive) this._dmgEnemy(e, d); } }
}
for (const r of G.player.relics) { if (r.id === 'bleeding_edge') { const ae = this.enemies.filter(e => e.isAlive); if (ae.length > 0) { const t = ae[Math.floor(Math.random() * ae.length)]; t.debuffs.bleed++; t.debuffs.bleedDuration = Math.max(t.debuffs.bleedDuration, 99); } } }
for (const r of G.player.relics) { if (r.id === 'eternal_hourglass' && this.hand.length === 0) this.player.energy = Math.min(this.player.maxEnergy, this.player.energy + 1); }
this.player.energy = this.player.maxEnergy;
if (this.player.nextTurnEnergy > 0) { this.player.energy += this.player.nextTurnEnergy; this.player.nextTurnEnergy = 0; }
if (G.player.temporaryBonusEnergy > 0) { this.player.energy += G.player.temporaryBonusEnergy; G.player.temporaryBonusEnergy--; }
this.drawCards(5);
for (const e of this.enemies) { if (e.isAlive && e.debuffs.bleed > 0) { e.currentHp -= e.debuffs.bleed; e.debuffs.bleed = Math.floor(e.debuffs.bleed / 2); } }
for (const e of this.enemies) { if (!e.isBoss || !e.phase2Entered) continue; if (e.currentHp <= Math.floor(e.maxHp * 0.5) && !e.inPhase2) e.inPhase2 = true; }
}
drawCards(n) { for (let i = 0; i < n; i++) { if (this.drawPile.length === 0 && this.discardPile.length > 0) { this.drawPile = shuffleArray(this.discardPile.filter(c => !c.exhaust)); this.discardPile = []; } if (this.drawPile.length > 0) this.hand.push(this.drawPile.pop()); } }
canPlayCard(card) {
if (card.type === CARD_TYPES.CURSE || card.type === CARD_TYPES.STATUS) return false;
if (card.cost < 0 && card.id !== 'vital_surge') return false;
if (card.cost > this.player.energy && !(card.cost < 0)) return false;
return true;
}
playCard(idx) {
const card = this.hand[idx];
if (!this.canPlayCard(card)) return false;
if (card.cost >= 0) this.player.energy -= card.cost;
let tIdx = this.targetEnemyIndex !== null ? this.targetEnemyIndex : 0;
const ae = this.enemies.filter(e => e.isAlive);
if (ae.length === 0) { this.hand.splice(idx, 1); return true; }
this._execCard(card, tIdx);
for (const r of G.player.relics) { if (r.id === 'tome_dark_rituals' && card.type === CARD_TYPES.POWER) this.drawCards(1); }
this.hand.splice(idx, 1);
if (card.exhaust || card.type === CARD_TYPES.STATUS) this.exhausted.push(card);
else if (card.type === CARD_TYPES.POWER) this.powers.push(card);
else this.discardPile.push(card);
return true;
}
_dmgEnemy(e, d) { let r = Math.max(0, d || 0); if (e.block > 0) { const b = Math.min(e.block, r); e.block -= b; r -= b; } e.currentHp -= r; if (e.currentHp <= 0) { e.currentHp = 0; e.isAlive = false; } }
_dmgPlayer(d, fromCard) { let f = Math.max(0, d || 0); if (this.player.debuffs.intangible > 0 && fromCard) f = 1; this.player.hp -= f; }
_int(d) { return this.player.debuffs.intangible > 0 ? 1 : Math.max(0, d || 0); }
endPlayerTurn() {
for (const c of this.hand) { if (c.id === 'doubt') this._dmgPlayer(1, false); }
if (this.player.debuffs.burn > 0 && this.player.debuffs.burnDuration > 0) { const d = this._int(this.player.debuffs.burn); this.player.hp -= d; this.player.debuffs.burn = Math.floor(this.player.debuffs.burn / 2); }
if (this.player.buffs.regeneration > 0) { this.player.hp = Math.min(this.player.maxHp, this.player.hp + this.player.buffs.regeneration); this.player.buffs.regeneration--; }
this.discardPile.push(...this.hand); this.hand = []; this.isPlayerTurn = false;
this._enemyTurn();
}
_enemyTurn() {
for (const enemy of this.enemies) {
if (!enemy.isAlive) continue;
if (enemy.isBoss && enemy.inPhase2) enemy.buffs.strength += (enemy.name === 'Archdemon' ? 2 : 1);
let intents = enemy.isBoss && enemy.inPhase2 ? enemy.phase2Intents : enemy.intents;
const intent = intents[enemy.intentIndex % intents.length];
enemy.block = 0; this._execEnemy(enemy, intent);
if (enemy.debuffs.burn > 0) { enemy.currentHp -= enemy.debuffs.burn; enemy.debuffs.burn = Math.floor(enemy.debuffs.burn / 2); }
if (enemy.buffs.regeneration > 0) { enemy.currentHp = Math.min(enemy.maxHp, enemy.currentHp + enemy.buffs.regeneration); enemy.buffs.regeneration--; }
enemy.intentIndex++;
}
if (this.enemies.filter(e => e.isAlive).length === 0) { G.combat.won = true; return; }
if (this.player.hp <= 0) { G.combat.lost = true; return; }
this.startPlayerTurn();
}
_execEnemy(enemy, intent) {
const ae = this.enemies.filter(e => e.isAlive); const s = enemy.buffs.strength || 0;
switch (intent.type) {
case 'attack': this._dmgP(s ? intent.value + s : intent.value); break;
case 'defend': enemy.block += intent.value; break;
case 'multi_attack': for (let i = 0; i < intent.hits; i++) this._dmgP(intent.value); break;
case 'attack_debuff': { this._dmgP(s ? intent.attackValue + s : intent.attackValue); if (intent.debuffType === 'weak') { this.player.debuffs.weak += intent.value; this.player.debuffs.weakDuration = Math.max(this.player.debuffs.weakDuration, 3); } if (intent.debuffType === 'burn') { this.player.debuffs.burn += intent.value; this.player.debuffs.burnDuration = Math.max(this.player.debuffs.burnDuration, 99); } if (intent.debuffType === 'vulnerable') { this.player.debuffs.vulnerable += intent.value; this.player.debuffs.vulnerableDuration = Math.max(this.player.debuffs.vulnerableDuration, 3); } break; }
case 'debuff': { if (intent.debuffType === 'weak') { this.player.debuffs.weak += intent.value; this.player.debuffs.weakDuration = Math.max(this.player.debuffs.weakDuration, 3); } if (intent.debuffType === 'vulnerable') { this.player.debuffs.vulnerable += intent.value; this.player.debuffs.vulnerableDuration = Math.max(this.player.debuffs.vulnerableDuration, 3); } if (intent.debuffType === 'bleed') { this.player.debuffs.bleed += intent.value; this.player.debuffs.bleedDuration = Math.max(this.player.debuffs.bleedDuration, 99); } if (intent.debuffType === 'poison') { this.player.debuffs.poison += intent.value; this.player.debuffs.poisonDuration = Math.max(this.player.debuffs.poisonDuration, 3); } break; }
case 'debuff_combo': { if (intent.weak) { this.player.debuffs.weak += intent.weak; this.player.debuffs.weakDuration = Math.max(this.player.debuffs.weakDuration, 3); } if (intent.vulnerable) { this.player.debuffs.vulnerable += intent.vulnerable; this.player.debuffs.vulnerableDuration = Math.max(this.player.debuffs.vulnerableDuration, 3); } break; }
case 'buff': if (intent.buffType === 'strength') enemy.buffs.strength += intent.value; break;
case 'self_buff': enemy.block += (intent.defendValue || 0); enemy.buffs.regeneration += (intent.regenValue || 0); break;
case 'self_buff_intangible': enemy.debuffs.intangible++; break;
case 'self_vuln_defend': enemy.block += intent.defendValue; enemy.debuffs.vulnerable++; break;
case 'attack_defend': this._dmgP(s ? intent.attackValue + s : intent.attackValue); enemy.block += intent.defendValue; break;
case 'defend_buff': enemy.block += intent.defendValue; if (intent.buffType === 'strength') enemy.buffs.strength += intent.value; break;
case 'drain': this._dmgP(s ? intent.attackValue + s : intent.attackValue); enemy.currentHp = Math.min(enemy.maxHp, enemy.currentHp + intent.healValue); break;
case 'defend_regen': enemy.block += intent.defendValue; enemy.buffs.regeneration += intent.regenValue; break;
case 'defend_heal': enemy.block += intent.defendValue; enemy.currentHp = Math.min(enemy.maxHp, enemy.currentHp + intent.healValue); break;
case 'summon': { const sd = ENEMIES[intent.summonEnemy]; if (sd) { let cs = true; if (enemy.isBoss && enemy.name === 'Lich King' && !enemy.inPhase2 && this.enemies.filter(e => e.name.includes('Minion') || e.name.includes('Skeleton')).length >= 2) cs = false; if (enemy.name === 'Infernal Imp' && this.enemies.filter(e => e.name === 'Infernal Imp').length >= 3) cs = false; if (cs) this.enemies.push({ ...sd, currentHp: sd.hp, block: 0, intentIndex: Math.floor(Math.random() * 3), buffs: { strength: 0, dexterity: 0, regeneration: 0 }, debuffs: { weak: 0, vulnerable: 0, bleed: 0, burn: 0, poison: 0, intangible: 0, weakDuration: 0, vulnerableDuration: 0, bleedDuration: 0, burnDuration: 0, poisonDuration: 0 }, isAlive: true }); else this._dmgP(5); } break; }
case 'buff_all': for (const e of ae) { if (intent.buffType === 'strength') e.buffs.strength += intent.value; } break;
}
}
_dmgP(d) { let r = Math.max(0, d || 0); if (this.player.block > 0) { const b = Math.min(this.player.block, r); this.player.block -= b; r -= b; } this.player.hp -= r; }
usePotion(idx) {
const p = G.player.potions[idx]; if (!p) return false;
switch (p.id) {
case 'blood_vial': this.player.hp = Math.min(this.player.maxHp, this.player.hp + 15); break;
case 'firebomb': for (const e of this.enemies) { if (e.isAlive) this._dmgEnemy(e, 10); } break;
case 'dex_tonic': this.player.buffs.dexterity += 3; break;
case 'energy_elixir': this.player.energy += 2; break;
case 'cursed_brew': for (const e of this.enemies) { if (e.isAlive) e.debuffs.vulnerable += 3; } break;
case 'bleeding_flask': { const a = this.enemies.filter(e => e.isAlive); if (a.length > 0) a[0].debuffs.bleed += 6; } break;
case 'imp_in_bottle': this.hand.push({ id: 'imp_claw_plus', name: 'Imp Claw+', type: CARD_TYPES.ATTACK, cost: 0, rarity: RARITIES.BASIC, desc: 'Deal 6 damage. Exhaust.', exhaust: true }); break;
case 'smoke_bomb': G.combat.escaped = true; return true;
}
G.player.potions.splice(idx, 1); return true;
}
_execCard(card, tIdx) {
const ae = this.enemies.filter(e => e.isAlive); if (ae.length === 0) return;
let te = null; for (const e of this.enemies) { if (e.isAlive && e.name === ae[tIdx]?.name || (this.targetEnemyIndex !== null && this.enemies[this.targetEnemyIndex] === e)) { te = e; break; } }
if (!te) te = ae[0];
const tgt = () => te; const allA = () => this.enemies.filter(e => e.isAlive);
const strB = this.player.buffs.strength || 0; const dexB = this.player.buffs.dexterity || 0;
const dd = (e, d) => { let f = Math.max(1, d + strB); if (this.player.debuffs.weak > 0 && this.player.debuffs.weakDuration > 0) for (let i = 0; i < Math.min(this.player.debuffs.weak, this.player.debuffs.weakDuration); i++) f = Math.floor(f * 0.75); if (e.debuffs.vulnerable > 0 && e.debuffs.vulnerableDuration > 0) for (let i = 0; i < Math.min(e.debuffs.vulnerable, e.debuffs.vulnerableDuration); i++) f = Math.ceil(f * 1.5); this._dmgEnemy(e, f); if (card.id && ['firebolt', 'hellfire_blast', 'flame_ward', 'soul_burn'].includes(card.id)) for (const r of G.player.relics) { if (r.id === 'soul_ember') this._dmgEnemy(e, 2); } if (card.type === CARD_TYPES.ATTACK) for (const r of G.player.relics) { if (r.id === 'black_blood') this.player.hp = Math.min(this.player.maxHp, this.player.hp + 1); } };
const dda = (d) => { allA().forEach(e => dd(e, d)); };
const gb = (a) => { this.player.block += a + dexB; };
const dc = (n) => { this.drawCards(n); };
const ade = (e, t, v) => { e.debuffs[t] += v; e.debuffs[t + 'Duration'] = Math.max(e.debuffs[t + 'Duration'], 99); };
const ada = (t, v) => { allA().forEach(e => ade(e, t, v)); };
let hpr = 0; if ((card.type === CARD_TYPES.ATTACK || card.id === 'bloodletting') && G.player.relics.some(r => r.id === 'sacrificial_dagger' && this.firstAttackCostsHpThisCombat)) { hpr = 1; this.firstAttackCostsHpThisCombat = false; }
switch (card.id) {
case 'strike': dd(tgt(), card.upgraded ? 8 : 6); break;
case 'defend': gb(card.upgraded ? 8 : 5); break;
case 'sanguine_strike': dd(tgt(), card.upgraded ? 10 : 8); this.player.hp = Math.min(this.player.maxHp, this.player.hp + (card.upgraded ? 3 : 2)); break;
case 'hemorrhage': dd(tgt(), card.upgraded ? 6 : 5); ade(tgt(), 'bleed', card.upgraded ? 4 : 3); break;
case 'bloodletting': { const hl = Math.max(1, (card.upgraded ? 2 : 3) - hpr); this._dmgPlayer(this._int(hl), true); dd(tgt(), card.upgraded ? 15 : 12); break; }
case 'crimson_eruption': { const hl = Math.max(1, 2 - hpr); this._dmgPlayer(this._int(hl), true); dda(card.upgraded ? 10 : 8); break; }
case 'crimson_shield': gb(card.upgraded ? 8 : 6); this._dmgPlayer(this._int(2), true); break;
case 'blood_ward': gb(card.upgraded ? 4 : 3); this.player.buffs.regeneration += (card.upgraded ? 2 : 1); break;
case 'sacrificial_barrier': gb(card.upgraded ? 13 : 10); this._dmgPlayer(this._int(5), true); break;
case 'vital_surge': { const m = this.player.maxHp - this.player.hp; gb(m + (card.upgraded ? 3 : 0)); card.exhaust = true; break; }
case 'life_tap': dc(card.upgraded ? 3 : 2); this._dmgPlayer(this._int(3), true); break;
case 'rend_flesh': ada('bleed', card.upgraded ? 7 : 5); this._dmgPlayer(this._int(2), true); break;
case 'soul_siphon': case 'blood_pact': break;
case 'shadow_bolt': dd(tgt(), card.upgraded ? 8 : 6); ade(tgt(), 'weak', card.upgraded ? 2 : 1); break;
case 'grasp_of_despair': dd(tgt(), card.upgraded ? 5 : 4); ade(tgt(), 'vulnerable', card.upgraded ? 2 : 1); break;
case 'nightmare_lash': { const t = tgt(); dd(t, card.upgraded ? 9 : 7); if (t.debuffs.weak > 0) dd(t, card.upgraded ? 9 : 7); break; }
case 'eternal_darkness': for (let i = 0; i < 3; i++) { const t = allA()[Math.floor(Math.random() * allA().length)]; if (t) dd(t, card.upgraded ? 4 : 3); } break;
case 'cloak_of_shadows': gb(card.upgraded ? 7 : 5); dc(1); break;
case 'haunting_mist': gb(card.upgraded ? 9 : 7); ada('weak', 1); break;
case 'phantom_armor': gb(card.upgraded ? 10 : 8); this.player.buffs.dexterity += (card.upgraded ? 3 : 2); break;
case 'whispering_shroud': gb(card.upgraded ? 5 : 4); this.player.nextTurnBlock += (card.upgraded ? 5 : 4); break;
case 'shadowform': this.player.debuffs.intangible += (card.upgraded ? 3 : 2); card.exhaust = true; break;
case 'curse_of_misfortune': { const t = tgt(); if (t.buffs.strength > 0) t.buffs.strength -= (card.upgraded ? 4 : 3); else t.buffs.strength = -(card.upgraded ? 4 : 3); ade(t, 'vulnerable', card.upgraded ? 2 : 1); break; }
case 'dark_ritual': this.player.energy += 2; this.discardPile.push({ ...DAZED }); break;
case 'umbral_mastery': break;
case 'firebolt': dd(tgt(), card.upgraded ? 9 : 7); ade(tgt(), 'burn', card.upgraded ? 3 : 2); break;
case 'hellfire_blast': dda(card.upgraded ? 12 : 9); break;
case 'searing_strike': dd(tgt(), card.upgraded ? 8 : 6); this.player.nextTurnEnergy += 1; break;
case 'immolate': dd(tgt(), card.upgraded ? 16 : 12); card.exhaust = true; break;
case 'obsidian_skin': gb(card.upgraded ? 10 : 7); break;
case 'flame_ward': gb(card.upgraded ? 6 : 5); { const t = allA()[Math.floor(Math.random() * allA().length)]; if (t) ade(t, 'burn', card.upgraded ? 3 : 2); } break;
case 'demonic_resilience': gb(card.upgraded ? 5 : 4); this.player.hp = Math.min(this.player.maxHp, this.player.hp + (card.upgraded ? 3 : 2)); break;
case 'brimstone_shield': gb(card.upgraded ? 4 : 3); this.player.buffs.strength += (card.upgraded ? 3 : 2); break;
case 'summon_imp': { const imp = card.upgraded ? { id: 'imp_claw_plus', name: 'Imp Claw+', type: CARD_TYPES.ATTACK, cost: 0, rarity: RARITIES.BASIC, desc: 'Deal 6 damage. Exhaust.', exhaust: true } : { id: 'imp_claw', name: 'Imp Claw', type: CARD_TYPES.ATTACK, cost: 0, rarity: RARITIES.BASIC, desc: 'Deal 4 damage. Exhaust.', exhaust: true }; this.hand.push(imp); card.exhaust = true; break; }
case 'infernal_pact': { const hl = Math.max(1, (card.upgraded ? 1 : 2) - hpr); this._dmgPlayer(this._int(hl), true); this.player.energy += 2; const imp = card.upgraded ? { id: 'imp_claw_plus', name: 'Imp Claw+', type: CARD_TYPES.ATTACK, cost: 0, rarity: RARITIES.BASIC, desc: 'Deal 6 damage. Exhaust.', exhaust: true } : { id: 'imp_claw', name: 'Imp Claw', type: CARD_TYPES.ATTACK, cost: 0, rarity: RARITIES.BASIC, desc: 'Deal 4 damage. Exhaust.', exhaust: true }; this.hand.push(imp); break; }
case 'hellfire_aura': break;
case 'soul_burn': ade(tgt(), 'burn', card.upgraded ? 10 : 8); dc(1); break;
case 'imp_claw': dd(tgt(), 4); card.exhaust = true; break;
case 'imp_claw_plus': dd(tgt(), 6); card.exhaust = true; break;
case 'flash_of_insight': dc(card.upgraded ? 3 : 2); break;
case 'iron_will': gb(card.upgraded ? 11 : 8); break;
case 'quick_slash': dd(tgt(), card.upgraded ? 9 : 7); break;
case 'potent_brew': this.player.energy += (card.upgraded ? 2 : 1); card.exhaust = true; break;
}
for (const r of G.player.relics) { if (r.id === 'bloodstone' && ['bloodletting', 'crimson_eruption', 'crimson_shield', 'sacrificial_barrier', 'life_tap', 'rend_flesh', 'infernal_pact'].includes(card.id)) this.player.hp = Math.min(this.player.maxHp, this.player.hp + 1); }
for (const p of this.powers) { if (p.id === 'blood_pact' && ['bloodletting', 'crimson_eruption', 'crimson_shield', 'sacrificial_barrier', 'life_tap', 'rend_flesh', 'infernal_pact'].includes(card.id)) this.player.buffs.strength += (p.upgraded ? 2 : 1); }
}
}
// ── main ──
// Initialize game
renderTitleScreen();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"639fb44e-9741-49c6-a74c-791022c97219","tokensIn":3427197,"tokensOut":67651,"tokensTotal":3494848,"turns":61,"toolCalls":60,"failedToolCalls":2,"timestamp":"2026-07-18T16:17:14.670Z"} -->