Shadows
Qwen 3.6 27b MTP · 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 121,022 bytes · SHA-256 c3246bccef9f
<!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: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a0f; color: #d4c8b0; font-family: 'Georgia', serif; }
#app { width: 100%; height: 100%; position: relative; }
.screen { display: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; flex-direction: column; align-items: center; justify-content: center; padding: 20px; overflow-y: auto; }
.screen.active { display: flex; }
.game-title { font-size: 3em; color: #c41e3a; text-shadow: 0 0 20px rgba(196,30,58,0.5), 0 0 40px rgba(196,30,58,0.3); margin-bottom: 10px; letter-spacing: 3px; }
.subtitle { font-size: 1.2em; color: #7a6f60; margin-bottom: 40px; font-style: italic; }
.main-btn { background: linear-gradient(180deg, #3d1c1c, #1a0e0e); border: 2px solid #5a2d2d; color: #d4c8b0; padding: 15px 40px; font-size: 1.2em; cursor: pointer; font-family: 'Georgia', serif; transition: all 0.3s; margin-top: 20px; }
.main-btn:hover { background: linear-gradient(180deg, #5a2d2d, #3d1c1c); border-color: #c41e3a; box-shadow: 0 0 15px rgba(196,30,58,0.4); }
.class-grid { display: flex; gap: 20px; margin-top: 20px; flex-wrap: wrap; justify-content: center; }
.class-card { width: 220px; padding: 20px; background: linear-gradient(180deg, #1a1520, #0d0b12); border: 2px solid #3a2d4a; cursor: pointer; transition: all 0.3s; text-align: center; }
.class-card:hover { border-color: #c41e3a; transform: translateY(-5px); box-shadow: 0 5px 20px rgba(196,30,58,0.3); }
.class-card h3 { color: #c41e3a; margin-bottom: 10px; font-size: 1.3em; }
.class-card p { font-size: 0.85em; color: #9a8f80; line-height: 1.4; }
#map-header { display: flex; justify-content: space-between; width: 800px; margin-bottom: 10px; padding: 10px; background: rgba(26,21,32,0.9); border: 1px solid #3a2d4a; }
#map-canvas { cursor: pointer; border: 1px solid #3a2d4a; }
/* Combat Screen */
#screen-combat { justify-content: flex-start; padding: 5px; }
#combat-top-bar { display: flex; gap: 15px; align-items: center; width: 95%; max-width: 900px; margin-bottom: 8px; padding: 8px 12px; background: rgba(26,21,32,0.9); border: 1px solid #3a2d4a; flex-wrap: wrap; }
#player-hp-bar { display: flex; align-items: center; gap: 5px; }
.hp-bar-bg { width: 200px; height: 20px; background: #1a0e0e; border: 1px solid #3d1c1c; position: relative; overflow: hidden; }
.hp-bar-fill { height: 100%; background: linear-gradient(90deg, #8b0000, #c41e3a); transition: width 0.3s; }
#hp-text { font-size: 0.9em; color: #ff6b6b; min-width: 70px; }
#player-block-display { color: #6baed6; font-size: 0.9em; }
#player-energy-display { display: flex; gap: 3px; align-items: center; }
.energy-gem { width: 18px; height: 18px; background: radial-gradient(circle, #ffd700, #b8860b); border-radius: 50%; border: 1px solid #daa520; display: inline-block; }
.energy-gem.used { opacity: 0.3; }
#player-gold-display { color: #ffd700; font-size: 0.9em; margin-left: auto; }
#combat-area { width: 95%; max-width: 900px; min-height: 200px; display: flex; justify-content: center; gap: 30px; padding: 10px 0; position: relative; }
#enemy-zone { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; justify-content: center; width: 100%; }
.enemy-card { background: linear-gradient(180deg, #1a1520, #0d0b12); border: 2px solid #3a2d4a; padding: 15px; min-width: 160px; text-align: center; cursor: pointer; transition: all 0.2s; position: relative; }
.enemy-card:hover { border-color: #c41e3a; }
.enemy-card.selected { border-color: #ff4444; box-shadow: 0 0 15px rgba(255,68,68,0.5); }
.enemy-card.dead { opacity: 0.3; pointer-events: none; filter: grayscale(1); }
.enemy-name { font-size: 0.9em; color: #c41e3a; margin-bottom: 5px; }
.enemy-hp-bar-bg { width: 120px; height: 12px; background: #1a0e0e; border: 1px solid #3d1c1c; margin: 5px auto; overflow: hidden; }
.enemy-hp-fill { height: 100%; background: linear-gradient(90deg, #8b0000, #c41e3a); transition: width 0.3s; }
.enemy-hp-text { font-size: 0.75em; color: #ff6b6b; }
.enemy-block-text { font-size: 0.75em; color: #6baed6; }
#intent-zone { display: flex; gap: 20px; justify-content: center; width: 100%; margin-top: 5px; flex-wrap: wrap; }
.intent-icon { background: rgba(26,21,32,0.8); border: 1px solid #3a2d4a; padding: 5px 10px; font-size: 0.75em; color: #9a8f80; text-align: center; min-width: 100px; }
.intent-icon.attack { color: #ff6b6b; border-color: #5a2d2d; }
.intent-icon.defend { color: #6baed6; border-color: #2d4a5a; }
.intent-icon.buff { color: #ffd700; border-color: #5a5a2d; }
.intent-icon.debuff { color: #9b59b6; border-color: #4a2d5a; }
#hand-zone { display: flex; gap: 8px; justify-content: center; padding: 10px; min-height: 180px; width: 95%; max-width: 900px; flex-wrap: wrap; align-items: flex-end; }
.card { width: 130px; height: 170px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 2px solid #4a3d5a; padding: 8px; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; position: relative; font-size: 0.7em; }
.card:hover { transform: translateY(-15px); z-index: 10; box-shadow: 0 5px 20px rgba(0,0,0,0.8); }
.card.unplayable { opacity: 0.4; cursor: not-allowed; }
.card.unplayable:hover { transform: none; }
.card-cost { position: absolute; top: -5px; left: -5px; width: 24px; height: 24px; background: radial-gradient(circle, #ffd700, #b8860b); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #1a0e0e; font-size: 1.2em; }
.card-cost.free { background: radial-gradient(circle, #aaa, #666); }
.card-name { font-size: 1em; font-weight: bold; margin-bottom: 3px; text-align: center; color: #d4c8b0; line-height: 1.2; }
.card-type-badge { position: absolute; top: -5px; right: -5px; width: 6px; height: 24px; border-radius: 3px; }
.type-attack .card-type-badge { background: #c41e3a; }
.type-skill .card-type-badge { background: #6baed6; }
.type-power .card-type-badge { background: #ffd700; }
.type-curse .card-type-badge { background: #9b59b6; }
.card-desc { font-size: 0.85em; color: #9a8f80; line-height: 1.3; flex-grow: 1; margin-top: 5px; overflow-y: auto; }
.card-rarity { text-align: center; margin-top: 2px; font-size: 0.75em; }
.rarity-common .card-rarity { color: #6baed6; }
.rarity-uncommon .card-rarity { color: #9b59b6; }
.rarity-rare .card-rarity { color: #ffd700; }
.rarity-basic .card-rarity { color: #aaa; }
.card.upgraded .card-name::after { content: " +"; color: #ffd700; }
#deck-info { display: flex; gap: 20px; margin-top: 5px; font-size: 0.8em; color: #7a6f60; }
.combat-btn { background: linear-gradient(180deg, #3d1c1c, #1a0e0e); border: 2px solid #5a2d2d; color: #d4c8b0; padding: 10px 30px; font-size: 1em; cursor: pointer; font-family: 'Georgia', serif; margin-top: 5px; }
.combat-btn:hover { background: linear-gradient(180deg, #5a2d2d, #3d1c1c); border-color: #c41e3a; }
#potion-bar { position: fixed; right: 10px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 8px; z-index: 100; }
.potion-btn { width: 45px; height: 45px; background: linear-gradient(135deg, #2a1530, #1a0e20); border: 2px solid #6a3d8a; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.2em; transition: all 0.2s; }
.potion-btn:hover { border-color: #c41e3a; transform: scale(1.1); }
#combat-log { position: fixed; bottom: 5px; left: 5px; max-width: 300px; max-height: 120px; overflow-y: auto; font-size: 0.7em; color: #7a6f60; background: rgba(10,10,15,0.8); padding: 5px; border-radius: 3px; z-index: 50; }
/* Event Screen */
#event-content { max-width: 600px; text-align: center; }
#event-title { color: #c41e3a; margin-bottom: 20px; font-size: 1.8em; }
#event-text { font-size: 1.1em; line-height: 1.6; margin-bottom: 30px; color: #b0a590; }
.event-choice-btn { display: block; width: 100%; max-width: 400px; margin: 8px auto; padding: 12px 20px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 1px solid #4a3d5a; color: #d4c8b0; cursor: pointer; font-family: 'Georgia', serif; font-size: 0.95em; transition: all 0.2s; text-align: left; }
.event-choice-btn:hover { border-color: #c41e3a; background: linear-gradient(180deg, #3d2d4a, #2a2035); }
/* Shop Screen */
#shop-items { display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; margin: 20px 0; max-width: 900px; }
.shop-item { width: 140px; padding: 10px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 2px solid #4a3d5a; text-align: center; cursor: pointer; transition: all 0.2s; }
.shop-item:hover { border-color: #ffd700; transform: translateY(-3px); }
.shop-item .item-price { color: #ffd700; font-size: 0.85em; margin-top: 5px; }
.shop-item.relic-shop { border-color: #9b59b6; }
/* Rest Screen */
#rest-choices { display: flex; gap: 15px; margin-top: 20px; flex-wrap: wrap; justify-content: center; }
.rest-btn { padding: 15px 30px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 2px solid #4a3d5a; color: #d4c8b0; cursor: pointer; font-family: 'Georgia', serif; font-size: 1em; transition: all 0.2s; min-width: 180px; }
.rest-btn:hover { border-color: #c41e3a; background: linear-gradient(180deg, #3d2d4a, #2a2035); }
/* Reward Screen */
#reward-gold { color: #ffd700; font-size: 1.2em; margin-bottom: 15px; }
#reward-cards { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; }
/* Relic choice */
#relic-choices { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; margin-top: 20px; }
.relic-choice { width: 180px; padding: 15px; background: linear-gradient(180deg, #2a2035, #1a1520); border: 2px solid #9b59b6; text-align: center; cursor: pointer; transition: all 0.2s; }
.relic-choice:hover { border-color: #ffd700; transform: translateY(-3px); }
/* Buff/Debuff badges */
.status-badge { display: inline-block; padding: 1px 5px; font-size: 0.6em; margin: 1px; border-radius: 2px; }
.badge-buff { background: #2d4a2d; color: #90ee90; }
.badge-debuff { background: #4a2d2d; color: #ff6b6b; }
/* Damage animation */
@keyframes damageFlash { 0% { filter: brightness(1); } 50% { filter: brightness(3) hue-rotate(180deg); } 100% { filter: brightness(1); } }
.damage-flash { animation: damageFlash 0.3s ease; }
@keyframes cardPlay { from { transform: scale(1); opacity: 1; } to { transform: scale(0.5) translateY(-100px); opacity: 0; } }
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0a0a0f; }
::-webkit-scrollbar-thumb { background: #3a2d4a; border-radius: 3px; }
h1, h2, h3 { text-align: center; }
/* ── css: polish ── */
/* Additional visual polish */
/* Card hover effects */
.card { transition: transform 0.15s ease, box-shadow 0.15s ease; }
.card:hover:not(.unplayable) {
transform: translateY(-20px) scale(1.05);
z-index: 100;
box-shadow: 0 8px 30px rgba(0,0,0,0.9), 0 0 15px rgba(196,30,58,0.3);
}
/* Card type colors */
.type-attack { border-color: #5a2d2d; }
.type-skill { border-color: #2d4a5a; }
.type-power { border-color: #5a5a2d; }
.type-curse { border-color: #4a2d5a; background: linear-gradient(180deg, #2a1530, #1a0e20); }
/* Rarity glow effects */
.rarity-common:hover:not(.unplayable) { box-shadow: 0 8px 30px rgba(0,0,0,0.9), 0 0 10px rgba(107,174,214,0.3); }
.rarity-uncommon:hover:not(.unplayable) { box-shadow: 0 8px 30px rgba(0,0,0,0.9), 0 0 15px rgba(155,89,182,0.4); }
.rarity-rare:hover:not(.unplayable) { box-shadow: 0 8px 30px rgba(0,0,0,0.9), 0 0 20px rgba(255,215,0,0.4); }
/* Enemy card improvements */
.enemy-card { transition: all 0.2s ease; border-radius: 8px; }
.enemy-card:hover { transform: scale(1.03); }
.enemy-card.selected {
box-shadow: 0 0 20px rgba(255,68,68,0.4), inset 0 0 10px rgba(255,68,68,0.1);
}
/* HP bar animations */
.hp-bar-fill { transition: width 0.3s ease; }
.enemy-hp-fill { transition: width 0.3s ease; }
/* Damage number popup animation */
@keyframes damagePopup {
0% { opacity: 1; transform: translateY(0) scale(1); }
50% { opacity: 1; transform: translateY(-20px) scale(1.2); }
100% { opacity: 0; transform: translateY(-40px) scale(0.8); }
}
/* Screen transitions */
.screen { transition: opacity 0.3s ease; }
/* Title screen ambient effect */
#screen-title::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse at center, rgba(196,30,58,0.1) 0%, transparent 70%);
pointer-events: none;
}
/* Combat area background */
#screen-combat::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse at center top, rgba(196,30,58,0.05) 0%, transparent 60%);
pointer-events: none;
}
/* Map node hover */
#map-canvas { transition: filter 0.2s ease; }
#map-canvas:hover { filter: brightness(1.1); }
/* Scrollbar styling for combat log */
#combat-log::-webkit-scrollbar { width: 4px; }
#combat-log::-webkit-scrollbar-thumb { background: #3a2d4a; border-radius: 2px; }
/* Energy gem animation */
@keyframes energyPulse {
0%, 100% { box-shadow: 0 0 5px rgba(255,215,0,0.3); }
50% { box-shadow: 0 0 10px rgba(255,215,0,0.6); }
}
.energy-gem:not(.used) { animation: energyPulse 2s ease infinite; }
/* Class card selection effect */
.class-card::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(180deg, transparent 60%, rgba(196,30,58,0.1));
pointer-events: none;
}
/* Relic display in combat */
.relic-display { position: fixed; left: 10px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 5px; z-index: 100; }
.relic-icon { width: 35px; height: 35px; background: linear-gradient(135deg, #2a1530, #1a0e20); border: 2px solid #6a3d8a; display: flex; align-items: center; justify-content: center; font-size: 0.9em; cursor: help; }
/* Buff/debuff display */
.buff-display { position: fixed; left: 55px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 3px; z-index: 100; font-size: 0.7em; }
/* Victory screen effects */
#screen-victory::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse at center, rgba(255,215,0,0.1) 0%, transparent 70%);
pointer-events: none;
}
/* Game over screen effects */
#screen-gameover::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse at center, rgba(139,0,0,0.2) 0%, transparent 70%);
pointer-events: none;
}
/* Hand card spacing */
#hand-zone .card { margin-bottom: 5px; }
/* Responsive adjustments for smaller screens */
@media (max-width: 900px) {
.card { width: 110px; height: 145px; font-size: 0.6em; }
#hand-zone { gap: 4px; }
}
/* Potion overlay styling */
.screen-overlay {
display: none !important;
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(10,10,15,0.9); z-index: 200;
flex-direction: column; align-items: center; justify-content: center; gap: 15px;
}
/* Event screen text styling */
#event-text { max-width: 500px; margin: 0 auto 30px; }
/* Shop item hover */
.shop-item:hover:not([style*="opacity"]) {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<div id="app"><!-- Title Screen -->
<div id="screen-title" class="screen active">
<h1 class="game-title">Shadows of the Abyss</h1>
<p class="subtitle">A Dark Fantasy Roguelike Deckbuilder</p>
<button id="btn-start" class="main-btn">Begin Your Descent</button>
</div>
<!-- Class Selection -->
<div id="screen-class-select" class="screen">
<h2>Choose Your Cursed Archetype</h2>
<div id="class-cards" class="class-grid"></div>
</div>
<!-- Map Screen -->
<div id="screen-map" class="screen">
<div id="map-header">
<span id="map-act-name"></span>
<span id="map-floor-num"></span>
</div>
<canvas id="map-canvas" width="800" height="600"></canvas>
</div>
<!-- Combat Screen -->
<div id="screen-combat" class="screen">
<div id="combat-top-bar">
<div id="player-hp-bar"><span id="hp-text"></span></div>
<div id="player-block-display"></div>
<div id="player-energy-display"></div>
<div id="player-gold-display"></div>
</div>
<div id="combat-area">
<div id="enemy-zone"></div>
<div id="intent-zone"></div>
</div>
<div id="hand-zone"></div>
<div id="deck-info">
<span id="draw-pile-count"></span>
<span id="discard-pile-count"></span>
</div>
<button id="btn-end-turn" class="combat-btn">End Turn</button>
</div>
<!-- Event Screen -->
<div id="screen-event" class="screen">
<div id="event-content">
<h2 id="event-title"></h2>
<p id="event-text"></p>
<div id="event-choices"></div>
</div>
</div>
<!-- Shop Screen -->
<div id="screen-shop" class="screen">
<h2>The Shadow Market</h2>
<div id="shop-gold-display"></div>
<div id="shop-items"></div>
<button id="btn-leave-shop" class="main-btn">Leave Shop</button>
</div>
<!-- Rest Screen -->
<div id="screen-rest" class="screen">
<h2>Campfire of the Damned</h2>
<p id="rest-hp-text"></p>
<div id="rest-choices"></div>
</div>
<!-- Reward Screen -->
<div id="screen-reward" class="screen">
<h2>Victory!</h2>
<div id="reward-gold"></div>
<div id="reward-cards" class="card-grid"></div>
<button id="btn-skip-reward" class="main-btn">Skip</button>
</div>
<!-- Game Over Screen -->
<div id="screen-gameover" class="screen">
<h2>Your Soul is Consumed...</h2>
<p id="gameover-stats"></p>
<button id="btn-restart" class="main-btn">Try Again</button>
</div>
<!-- Victory Screen -->
<div id="screen-victory" class="screen">
<h1>The Abyss is Conquered!</h1>
<p id="victory-stats"></p>
<button id="btn-play-again" class="main-btn">Play Again</button>
</div>
<!-- Card Remove Screen -->
<div id="screen-remove-card" class="screen">
<h2 id="remove-title">Remove a Card</h2>
<p id="remove-desc"></p>
<div id="remove-cards" class="card-grid"></div>
<button id="btn-cancel-remove" class="main-btn">Cancel</button>
</div>
<!-- Upgrade Screen -->
<div id="screen-upgrade-card" class="screen">
<h2>The Forge</h2>
<p>Choose a card to upgrade:</p>
<div id="upgrade-cards" class="card-grid"></div>
<button id="btn-cancel-upgrade" class="main-btn">Cancel</button>
</div>
<!-- Potion Use Screen -->
<div id="screen-potion-use" class="screen-overlay">
<h3>Choose Target for Potion</h3>
<div id="potion-targets"></div>
<button id="btn-cancel-potion" class="main-btn">Cancel</button>
</div>
<!-- Relic Screen -->
<div id="screen-relic-choice" class="screen">
<h2>Choose a Relic</h2>
<div id="relic-choices"></div>
</div>
<!-- Potion buttons in combat -->
<div id="potion-bar"></div>
<!-- Combat log -->
<div id="combat-log"></div><div id="relic-display" class="relic-display"></div>
<div id="buff-display" class="buff-display"></div></div>
<script>
'use strict';
// ── const: CARD_DB ──
const CARD_DB = {
// Basic Cards (all classes)
strike: { name: "Strike", type: "attack", cost: 1, rarity: "basic", desc: "Deal 6 damage.", effect: { dmg: 6 }, upgrade: { dmg: 8 } },
defend: { name: "Defend", type: "skill", cost: 1, rarity: "basic", desc: "Gain 5 Block.", effect: { block: 5 }, upgrade: { block: 8 } },
// Blood Reaper Cards
sanguine_strike: { name: "Sanguine Strike", type: "attack", cost: 1, rarity: "common", desc: "Deal 8 damage. Heal 2 HP.", effect: { dmg: 8, heal: 2 }, upgrade: { dmg: 10, heal: 3 } },
hemorrhage: { name: "Hemorrhage", type: "attack", cost: 1, rarity: "common", desc: "Deal 5 damage. Apply 3 Bleed.", effect: { dmg: 5, bleed: 3 }, upgrade: { dmg: 6, bleed: 4 } },
bloodletting: { name: "Bloodletting", type: "attack", cost: 1, rarity: "uncommon", desc: "Lose 3 HP. Deal 12 damage.", effect: { selfDmg: 3, dmg: 12 }, upgrade: { selfDmg: 2, dmg: 15 } },
crimson_eruption: { name: "Crimson Eruption", type: "attack", cost: 2, rarity: "rare", desc: "Deal 8 damage to ALL enemies. Lose 2 HP.", effect: { dmgAll: 8, selfDmg: 2 }, upgrade: { dmgAll: 10, selfDmg: 2 } },
crimson_shield: { name: "Crimson Shield", type: "skill", cost: 1, rarity: "common", desc: "Gain 6 Block. Lose 2 HP.", effect: { block: 6, selfDmg: 2 }, upgrade: { block: 8, selfDmg: 2 } },
blood_ward: { name: "Blood Ward", type: "skill", cost: 1, rarity: "common", desc: "Gain 3 Block. Gain 1 Regeneration.", effect: { block: 3, regen: 1 }, upgrade: { block: 4, regen: 2 } },
sacrificial_barrier: { name: "Sacrificial Barrier", type: "skill", cost: 2, rarity: "uncommon", desc: "Gain 10 Block. Lose 5 HP.", effect: { block: 10, selfDmg: 5 }, upgrade: { block: 13, selfDmg: 5 } },
vital_surge: { name: "Vital Surge", type: "skill", cost: 0, rarity: "rare", desc: "Gain Block equal to your missing HP. Exhaust.", effect: { blockMissingHp: true }, exhaust: true, upgrade: { blockMissingHpPlus: 3 } },
life_tap: { name: "Life Tap", type: "skill", cost: 0, rarity: "common", desc: "Draw 2 cards. Lose 3 HP.", effect: { draw: 2, selfDmg: 3 }, upgrade: { draw: 3, selfDmg: 3 } },
rend_flesh: { name: "Rend Flesh", type: "skill", cost: 1, rarity: "uncommon", desc: "Apply 5 Bleed to ALL enemies. Lose 2 HP.", effect: { bleedAll: 5, selfDmg: 2 }, upgrade: { bleedAll: 7, selfDmg: 2 } },
soul_siphon: { name: "Soul Siphon", type: "power", cost: 2, rarity: "uncommon", desc: "(Power) At start of turn, gain 1 Energy and lose 1 HP.", effect: { powerStartTurnEnergy: 1, powerSelfDmg: 1 }, upgradeCost: 1 },
blood_pact: { name: "Blood Pact", type: "power", cost: 1, rarity: "rare", desc: "(Power) Whenever you lose HP from a card, gain 1 Strength.", effect: { strengthOnHpLoss: 1 }, upgrade: { strengthOnHpLoss: 2 } },
// Shadowmancer Cards
shadow_bolt: { name: "Shadow Bolt", type: "attack", cost: 1, rarity: "common", desc: "Deal 6 damage. Apply 1 Weak.", effect: { dmg: 6, weak: 1 }, upgrade: { dmg: 8, weak: 2 } },
grasp_of_despair: { name: "Grasp of Despair", type: "attack", cost: 1, rarity: "common", desc: "Deal 4 damage. Apply 1 Vulnerable.", effect: { dmg: 4, vulnerable: 1 }, upgrade: { dmg: 5, vulnerable: 2 } },
nightmare_lash: { name: "Nightmare Lash", type: "attack", cost: 2, rarity: "uncommon", desc: "Deal 7 damage. If enemy has Weak, deal 7 more.", effect: { dmg: 7, extraIfWeak: 7 }, upgrade: { dmg: 9, extraIfWeak: 9 } },
eternal_darkness: { name: "Eternal Darkness", type: "attack", cost: 2, rarity: "rare", desc: "Deal 3 damage to a random enemy 3 times.", effect: { multiRandomDmg: 3, multiCount: 3 }, upgrade: { multiRandomDmg: 4, multiCount: 3 } },
cloak_of_shadows: { name: "Cloak of Shadows", type: "skill", cost: 1, rarity: "common", desc: "Gain 5 Block. Draw 1 card.", effect: { block: 5, draw: 1 }, upgrade: { block: 7, draw: 1 } },
haunting_mist: { name: "Haunting Mist", type: "skill", cost: 1, rarity: "common", desc: "Gain 7 Block. Apply 1 Weak to ALL enemies.", effect: { block: 7, weakAll: 1 }, upgrade: { block: 9, weakAll: 1 } },
phantom_armor: { name: "Phantom Armor", type: "skill", cost: 1, rarity: "uncommon", desc: "Gain 8 Block. Next turn, gain 2 Dexterity.", effect: { block: 8, nextDex: 2 }, upgrade: { block: 10, nextDex: 3 } },
whispering_shroud: { name: "Whispering Shroud", type: "skill", cost: 1, rarity: "uncommon", desc: "Gain 4 Block. Next turn, gain 4 Block.", effect: { block: 4, nextBlock: 4 }, upgrade: { block: 5, nextBlock: 5 } },
shadowform: { name: "Shadowform", type: "skill", cost: 2, rarity: "uncommon", desc: "Gain 2 Intangible. Exhaust.", effect: { intangible: 2 }, exhaust: true, upgrade: { intangible: 3 } },
curse_of_misfortune: { name: "Curse of Misfortune", type: "skill", cost: 1, rarity: "common", desc: "Enemy loses 3 Strength this turn. Apply 1 Vulnerable.", effect: { enemyLoseStr: 3, vulnerable: 1 }, upgrade: { enemyLoseStr: 4, vulnerable: 2 } },
dark_ritual: { name: "Dark Ritual", type: "skill", cost: 0, rarity: "uncommon", desc: "Gain 2 Energy. Shuffle a Dazed into your discard pile.", effect: { gainEnergy: 2, shuffleDazed: true }, upgrade: {} },
umbral_mastery: { name: "Umbral Mastery", type: "power", cost: 2, rarity: "rare", desc: "(Power) At start of turn, apply 1 Weak to ALL enemies.", effect: { powerWeakAll: 1 }, upgrade: { powerWeakAll: 2 } },
// Hellforged Cards
firebolt: { name: "Firebolt", type: "attack", cost: 1, rarity: "common", desc: "Deal 7 damage. Apply 2 Burn.", effect: { dmg: 7, burn: 2 }, upgrade: { dmg: 9, burn: 3 } },
hellfire_blast: { name: "Hellfire Blast", type: "attack", cost: 2, rarity: "uncommon", desc: "Deal 9 damage to ALL enemies.", effect: { dmgAll: 9 }, upgrade: { dmgAll: 12 } },
searing_strike: { name: "Searing Strike", type: "attack", cost: 1, rarity: "common", desc: "Deal 6 damage. Gain 1 Energy next turn.", effect: { dmg: 6, nextEnergy: 1 }, upgrade: { dmg: 8, nextEnergy: 1 } },
immolate: { name: "Immolate", type: "attack", cost: 2, rarity: "rare", desc: "Deal 12 damage. Exhaust.", effect: { dmg: 12 }, exhaust: true, upgrade: { dmg: 16 } },
obsidian_skin: { name: "Obsidian Skin", type: "skill", cost: 1, rarity: "common", desc: "Gain 7 Block.", effect: { block: 7 }, upgrade: { block: 10 } },
flame_ward: { name: "Flame Ward", type: "skill", cost: 1, rarity: "common", desc: "Gain 5 Block. Apply 2 Burn to a random enemy.", effect: { block: 5, burnRandom: 2 }, upgrade: { block: 6, burnRandom: 3 } },
demonic_resilience: { name: "Demonic Resilience", type: "skill", cost: 1, rarity: "uncommon", desc: "Gain 4 Block. Heal 2 HP.", effect: { block: 4, heal: 2 }, upgrade: { block: 5, heal: 3 } },
brimstone_shield: { name: "Brimstone Shield", type: "skill", cost: 1, rarity: "uncommon", desc: "Gain 3 Block. Gain 2 Strength this turn.", effect: { block: 3, tempStr: 2 }, upgrade: { block: 4, tempStr: 3 } },
summon_imp: { name: "Summon Imp", type: "skill", cost: 1, rarity: "common", desc: "Add an Imp Claw to your hand. Exhaust.", effect: { addImpClaw: true }, exhaust: true, upgrade: { addImpClawPlus: true } },
infernal_pact: { name: "Infernal Pact", type: "skill", cost: 0, rarity: "uncommon", desc: "Lose 2 HP. Gain 2 Energy. Add an Imp Claw to your hand.", effect: { selfDmg: 2, gainEnergy: 2, addImpClaw: true }, upgrade: { selfDmg: 1, gainEnergy: 2, addImpClawPlus: true } },
hellfire_aura: { name: "Hellfire Aura", type: "power", cost: 2, rarity: "rare", desc: "(Power) At start of turn, deal 3 damage to ALL enemies.", effect: { powerDmgAllStartTurn: 3 }, upgrade: { powerDmgAllStartTurn: 5 } },
soul_burn: { name: "Soul Burn", type: "skill", cost: 1, rarity: "uncommon", desc: "Apply 8 Burn to an enemy. Draw 1 card.", effect: { burn: 8, draw: 1 }, upgrade: { burn: 10, draw: 1 } },
// Colorless Cards (shop only)
flash_of_insight: { name: "Flash of Insight", type: "skill", cost: 1, rarity: "common", desc: "Draw 2 cards.", effect: { draw: 2 }, upgrade: { draw: 3 } },
iron_will: { name: "Iron Will", type: "skill", cost: 1, rarity: "common", desc: "Gain 8 Block.", effect: { block: 8 }, upgrade: { block: 11 } },
quick_slash: { name: "Quick Slash", type: "attack", cost: 1, rarity: "common", desc: "Deal 7 damage.", effect: { dmg: 7 }, upgrade: { dmg: 9 } },
potent_brew: { name: "Potent Brew", type: "skill", cost: 0, rarity: "uncommon", desc: "Gain 1 Energy. Exhaust.", effect: { gainEnergy: 1 }, exhaust: true, upgrade: { gainEnergy: 2 } },
// Curses
wound: { name: "Wound", type: "curse", cost: -1, rarity: "basic", desc: "Unplayable. Ethereal.", unplayable: true, ethereal: true },
doubt: { name: "Doubt", type: "curse", cost: -1, rarity: "basic", desc: "Unplayable. At end of turn, lose 1 HP.", unplayable: true, endTurnSelfDmg: 1 },
dazed: { name: "Dazed", type: "status", cost: -1, rarity: "basic", desc: "Unplayable. Ethereal.", unplayable: true, ethereal: true },
// Imp Claw (generated)
imp_claw: { name: "Imp Claw", type: "attack", cost: 0, rarity: "basic", desc: "Deal 4 damage. Exhaust.", effect: { dmg: 4 }, exhaust: true },
imp_claw_plus: { name: "Imp Claw+", type: "attack", cost: 0, rarity: "basic", desc: "Deal 6 damage. Exhaust.", effect: { dmg: 6 }, exhaust: true }
};
// ── const: CLASS_DATA ──
const CLASS_DATA = {
blood_reaper: {
name: "Blood Reaper",
theme: "Sacrifice HP for power, life steal, Bleed.",
color: "#c41e3a",
signatureCard: "sanguine_strike",
cardPool: ["sanguine_strike","hemorrhage","bloodletting","crimson_eruption","crimson_shield","blood_ward","sacrificial_barrier","vital_surge","life_tap","rend_flesh","soul_siphon","blood_pact"]
},
shadowmancer: {
name: "Shadowmancer",
theme: "Debuffs (Weak, Vulnerable), Intangible, card manipulation.",
color: "#9b59b6",
signatureCard: "shadow_bolt",
cardPool: ["shadow_bolt","grasp_of_despair","nightmare_lash","eternal_darkness","cloak_of_shadows","haunting_mist","phantom_armor","whispering_shroud","shadowform","curse_of_misfortune","dark_ritual","umbral_mastery"]
},
hellforged: {
name: "Hellforged",
theme: "Fire damage, Burn, summoning Imps, energy manipulation.",
color: "#ff6b35",
signatureCard: "firebolt",
cardPool: ["firebolt","hellfire_blast","searing_strike","immolate","obsidian_skin","flame_ward","demonic_resilience","brimstone_shield","summon_imp","infernal_pact","hellfire_aura","soul_burn"]
}
};
// ── const: RELIC_DB ──
const RELIC_DB = {
bloodstone: { name: "Bloodstone", desc: "Whenever you lose HP from a card, heal 1 HP.", rarity: "common" },
demon_horns: { name: "Demon Horns", desc: "At start of combat, add random Imp Claw to hand. (Hellforged only)", rarity: "uncommon", classReq: "hellforged" },
shadow_cloak: { name: "Shadow Cloak", desc: "At start of each combat, gain 1 Intangible.", rarity: "rare" },
cursed_amulet: { name: "Cursed Amulet", desc: "At start of each combat, apply 1 Weak to ALL enemies.", rarity: "common" },
tome_of_dark_rituals: { name: "Tome of Dark Rituals", desc: "Whenever you play a Power card, draw 1 card.", rarity: "uncommon" },
bleeding_edge: { name: "Bleeding Edge", desc: "At start of your turn, apply 1 Bleed to a random enemy.", rarity: "common" },
soul_ember: { name: "Soul Ember", desc: "Whenever you apply Burn to an enemy, deal 2 additional damage.", rarity: "rare" },
eternal_hourglass: { name: "Eternal Hourglass", desc: "At start of turn, if no cards in hand, gain 1 Energy.", rarity: "uncommon" },
sacrificial_dagger: { name: "Sacrificial Dagger", desc: "Your first Attack that costs HP each combat costs 1 less HP. (Blood Reaper only)", rarity: "uncommon", classReq: "blood_reaper" },
vial_of_shadows: { name: "Vial of Shadows", desc: "When obtained, remove a card from your deck.", rarity: "rare", unique: true },
ashen_armor: { name: "Ashen Armor", desc: "At end of combat, heal 6 HP.", rarity: "common" },
greedy_imp: { name: "Greedy Imp", desc: "Enemies drop 10 extra Gold.", rarity: "common" }
};
// ── const: BOSS_RELICS ──
const BOSS_RELICS = ["cursed_crown","ectoplasm","philosophers_stone","black_blood","shadow_amulet"];
// ── const: BOSS_RELIC_DB ──
const BOSS_RELIC_DB = {
cursed_crown: { name: "Cursed Crown", desc: "Gain 1 Energy at start of each turn. Curses can no longer be removed.", rarity: "boss" },
ectoplasm: { name: "Ectoplasm", desc: "Gain 1 Energy. Lose all Gold and gain no more Gold this run.", rarity: "boss" },
philosophers_stone: { name: "Philosopher's Stone", desc: "Gain 1 Energy. All enemies start combat with 1 Strength.", rarity: "boss" },
black_blood: { name: "Black Blood", desc: "At start of combat, lose 10% max HP. Whenever you play an Attack, heal 1 HP.", rarity: "boss" },
shadow_amulet: { name: "Shadow Amulet", desc: "Start each turn with 1 Intangible. Reduce max HP by 15.", rarity: "boss" }
};
// ── const: POTION_DB ──
const POTION_DB = [
{ 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 an Imp Claw+ to your hand." },
{ id: "smoke_bomb", name: "Smoke Bomb", icon: "💨", desc: "Escape from combat, no rewards." }
];
// ── const: COLORLESS_CARDS ──
const COLORLESS_CARDS = ["flash_of_insight","iron_will","quick_slash","potent_brew"];
// ── const: ENEMY_DB ──
const ENEMY_DB = {
// Act 1 - The Crypts
skeleton_warrior: { name: "Skeleton Warrior", hp: 22, maxHp: 22, intents: [
{ type: "attack", dmg: 6 }, { type: "attack", dmg: 6 }, { type: "defend", block: 8 }
]},
zombie_brute: { name: "Zombie Brute", hp: 28, maxHp: 28, intents: [
{ type: "attack", dmg: 8 }, { type: "debuff", weak: 2 }, { type: "attack", dmg: 8 }
]},
dark_cultist: { name: "Dark Cultist", hp: 20, maxHp: 20, intents: [
{ type: "buff", str: 3 }, { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }
]},
rat_swarm: { name: "Rat Swarm", hp: 15, maxHp: 15, intents: [
{ type: "multiHit", hits: [{dmg:3},{dmg:3}] }, { type: "multiHit", hits: [{dmg:3},{dmg:3}] }
]},
// Act 1 Elites
tomb_guardian: { name: "Tomb Guardian", hp: 42, maxHp: 42, intents: [
{ type: "attackDefend", dmg: 6, block: 8 }, { type: "multiHit", hits: [{dmg:4},{dmg:4},{dmg:4}] }, { type: "attack", dmg: 12 }
]},
blood_witch: { name: "Blood Witch", hp: 36, maxHp: 36, intents: [
{ type: "drainLife", dmg: 5, heal: 4 }, { type: "debuffAll", vulnerable: 1, weak: 1 }, { type: "drainLife", dmg: 7, heal: 4 }
]},
// Act 2 - Blighted Woods
wraith: { name: "Wraith", hp: 30, maxHp: 30, intents: [
{ type: "attackDebuff", dmg: 7, weak: 1 }, { type: "attack", dmg: 7 }, { type: "defend", block: 10 }
]},
corrupted_beast: { name: "Corrupted Beast", hp: 38, maxHp: 38, intents: [
{ type: "attack", dmg: 12 }, { type: "selfVulnDefend", block: 5 }, { type: "attack", dmg: 12 }
]},
thorned_horror: { name: "Thorned Horror", hp: 25, maxHp: 25, intents: [
{ type: "multiHit", hits: [{dmg:4},{dmg:4},{dmg:4}] }, { type: "debuff", bleed: 3 }, { type: "attack", dmg: 8 }
]},
witch_doctor: { name: "Witch Doctor", hp: 22, maxHp: 22, intents: [
{ type: "selfBuff", block: 4, regen: 2 }, { type: "attackDebuff", dmg: 6, poison: 2 }, { type: "attack", dmg: 6 }
]},
// Act 2 Elites
treant_protector: { name: "Treant Protector", hp: 50, maxHp: 50, intents: [
{ type: "attackDefend", dmg: 9, block: 6 }, { type: "debuffAll", vulnerable: 2 }, { type: "multiHit", hits: [{dmg:10},{dmg:10}] }
]},
plague_shaman: { name: "Plague Shaman", hp: 42, maxHp: 42, intents: [
{ type: "debuff", poison: 4 }, { type: "summon", summonId: "plague_rat" }, { type: "attack", dmg: 8 }
]},
// Act 3 - Abyssal Spire
hellhound: { name: "Hellhound", hp: 26, maxHp: 26, intents: [
{ type: "attackDebuff", dmg: 8, burn: 2 }, { type: "attack", dmg: 8 }, { type: "defend", block: 9 }
]},
shadow_demon: { name: "Shadow Demon", hp: 30, maxHp: 30, intents: [
{ type: "attackDebuff", dmg: 7, weak: 1 }, { type: "attack", dmg: 7 }, { type: "selfBuff", intangible: 1 }
]},
infernal_imp: { name: "Infernal Imp", hp: 18, maxHp: 18, intents: [
{ type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }, { type: "summon", summonId: "infernal_imp" }
]},
abyssal_cultist: { name: "Abyssal Cultist", hp: 24, maxHp: 24, intents: [
{ type: "buffAll", str: 2 }, { type: "attack", dmg: 7 }, { type: "attack", dmg: 7 }
]},
// Act 3 Elites
flamespawn: { name: "Flamespawn", hp: 48, maxHp: 48, intents: [
{ type: "attackDebuff", dmg: 9, burn: 3 }, { type: "defendBuff", block: 10, str: 2 }, { type: "multiHit", hits: [{dmg:8},{dmg:8}] }
]},
dread_knight: { name: "Dread Knight", hp: 55, maxHp: 55, intents: [
{ type: "attack", dmg: 12 }, { type: "debuff", bleed: 3 }, { type: "defendHeal", block: 12, heal: 4 }
]},
// Minions/Summons
skeleton_minion: { name: "Skeleton Minion", hp: 15, maxHp: 15, intents: [
{ type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }
]},
plague_rat: { name: "Plague Rat", hp: 18, maxHp: 18, intents: [
{ type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }, { type: "attack", dmg: 5 }
]},
// Shadow Thief (event enemy)
shadow_thief: { name: "Shadow Thief", hp: 30, maxHp: 30, intents: [
{ type: "attackDebuff", dmg: 6, weak: 1 }, { type: "defend", block: 8 }, { type: "attack", dmg: 7 }
]}
};
// ── const: BOSS_DB ──
const BOSS_DB = {
act1: { name: "Lich King", hp: 65, maxHp: 65, isBoss: true, phase2Threshold: 30,
phase1Intents: [
{ type: "summon", summonId: "skeleton_minion" },
{ type: "attack", dmg: 8 },
{ type: "defend", block: 12 }
],
phase2Intents: [
{ type: "attackStrGain", dmg: 10, strGain: 2 },
{ type: "attackStrGain", dmg: 10, strGain: 2 },
{ type: "defendStrGain", block: 15, strGain: 2 }
]
},
act2: { name: "Eldritch Treant", hp: 75, maxHp: 75, isBoss: true, phase2Threshold: 40,
phase1Intents: [
{ type: "attackDebuff", dmg: 10, weak: 2 },
{ type: "defendBuff", block: 15, regen: 2 },
{ type: "multiHit", hits: [{dmg:8},{dmg:8}] }
],
phase2Intents: [
{ type: "attackDebuffStrGain", dmg: 14, vulnerable: 1, strGain: 1 },
{ type: "defendHealStrGain", block: 20, heal: 5, strGain: 1 },
{ type: "attackDebuffStrGain", dmg: 14, vulnerable: 1, strGain: 1 }
]
},
act3: { name: "Archdemon", hp: 85, maxHp: 85, isBoss: true, phase2Threshold: 45,
phase1Intents: [
{ type: "summon", summonId: "hellhound" },
{ type: "attackDebuff", dmg: 12, burn: 2 },
{ type: "defendBuff", block: 15, str: 3 }
],
phase2Intents: [
{ type: "attackStrGain", dmg: 16, strGain: 2 },
{ type: "multiHitStrGain", hits: [{dmg:7},{dmg:7},{dmg:7}], strGain: 2 },
{ type: "attackStrGain", dmg: 16, strGain: 2 }
]
}
};
// ── const: ACT_ENEMY_POOLS ──
const ACT_ENEMY_POOLS = {
act1: ["skeleton_warrior","zombie_brute","dark_cultist","rat_swarm"],
act2: ["wraith","corrupted_beast","thorned_horror","witch_doctor"],
act3: ["hellhound","shadow_demon","infernal_imp","abyssal_cultist"]
};
// ── const: ACT_ELITE_POOLS ──
const ACT_ELITE_POOLS = {
act1: ["tomb_guardian","blood_witch"],
act2: ["treant_protector","plague_shaman"],
act3: ["flamespawn","dread_knight"]
};
// ── const: MAP_LAYOUT ──
// Map structure per act - floor layout with node types
const MAP_LAYOUT = [
{ floor: 2, type: "enemy", branch: true },
{ floor: 3, type: "enemy" },
{ floor: 4, type: "event" },
{ floor: 5, type: "shop" },
{ floor: 6, type: "enemy" },
{ floor: 7, type: "rest" },
{ floor: 8, type: "enemy", branch: true },
{ floor: 9, type: "elite", altType: "enemy" },
{ floor: 10, type: "event" },
{ floor: 11, type: "enemy" },
{ floor: 12, type: "elite", altType: "enemy" },
{ floor: 13, type: "enemy" },
{ floor: 14, type: "rest" },
{ floor: 15, type: "boss" }
];
// ── const: EVENTS ──
const EVENTS = [
{ id: "forgotten_altar", title: "The Forgotten Altar", text: "A blood-stained altar hums with power.", choices: [
{ text: "[Offer 15 HP] → Obtain a random Rare relic.", action: "offer_hp_relic" },
{ text: "[Pray] → Heal 10 HP, gain a Curse (Doubt).", action: "pray_heal_curse" },
{ text: "[Leave]", action: "nothing" }
]},
{ id: "shadowy_peddler", title: "Shadowy Peddler", text: "A hooded figure offers a trade.", choices: [
{ text: "[Trade a card] → Select a card to remove, gain 100 Gold.", action: "trade_card" },
{ text: "[Buy his wares] → Lose 50 Gold, obtain a random Potion and a random Common relic.", action: "buy_wares" },
{ text: "[Attack] → Begin combat with Shadow Thief, get 75 Gold on victory.", action: "attack_peddler" }
]},
{ id: "cursed_spring", title: "Cursed Spring", text: "A spring bubbles with dark water.", choices: [
{ text: "[Drink] → Lose 10 HP, remove all Curses from deck.", action: "drink_cures" },
{ text: "[Bottle it] → Obtain a potion (Cursed Brew).", action: "bottle_potion" },
{ text: "[Leave]", action: "nothing" }
]},
{ id: "restless_dead", title: "Restless Dead", text: "Ghosts offer a bargain.", choices: [
{ text: "[Accept] → Lose 20 HP, upgrade 2 random cards in your deck.", action: "upgrade_random" },
{ text: "[Refuse]", action: "nothing" }
]},
{ id: "impish_gambler", title: "Impish Gambler", text: "A grinning imp challenges you to a game of chance.", choices: [
{ text: "[Bet 30 Gold] → 50% chance to double (60 Gold) or lose it all.", action: "bet_gold" },
{ text: "[Wager a card] → Remove a random card; if it's a Curse, gain 100 Gold.", action: "wager_card" },
{ text: "[Leave]", action: "nothing" }
]},
{ id: "ancient_library", title: "Ancient Library", text: "Dusty tomes contain forgotten knowledge.", choices: [
{ text: "[Study] → Gain +1 Energy next combat only, lose 5 HP.", action: "study_energy" },
{ text: "[Read forbidden page] → Add a random Power card from your class to deck, gain Doubt.", action: "read_power_curse" },
{ text: "[Leave]", action: "nothing" }
]}
];
// ── let: G ──
let G = {};
// ── function: initGameState ──
// Global game state
function initGameState() {
G = {
screen: "title",
playerClass: null,
hp: 70, maxHp: 70, block: 0, energy: 3, maxEnergy: 3, gold: 99,
act: 1, floor: 1,
deck: [], hand: [], drawPile: [], discardPile: [], exhaustPile: [],
relics: [], potions: [], potionSlots: 3,
buffs: {}, // { strength: 0, dexterity: 0, regeneration: 0, intangible: 0 }
nextTurnBuffs: {}, // { block: 0, energy: 0, dex: 0 }
combatActive: false,
enemies: [], selectedEnemyIdx: -1,
turnNumber: 0, playerTurn: true,
mapNodes: [], currentNodeIdx: 0, visitedNodes: new Set(),
pendingReward: null,
eventCombatGold: 0,
tempEnergyBonus: 0, // from Ancient Library event
firstHpCostAttackUsed: false, // for Sacrificial Dagger
cursesUnremovable: false, // Cursed Crown effect
noMoreGold: false, // Ectoplasm effect
enemiesStartWithStr: false, // Philosopher's Stone effect
healOnAttack: false, // Black Blood effect
startTurnIntangible: false, // Shadow Amulet effect
logMessages: []
};
}
// ── function: createCardInstance ──
function createCardInstance(cardId) {
const template = CARD_DB[cardId];
if (!template) return null;
return {
id: cardId,
name: template.name,
type: template.type,
cost: template.cost,
rarity: template.rarity,
desc: template.desc,
effect: Object.assign({}, template.effect || {}),
exhaust: template.exhaust || false,
unplayable: template.unplayable || false,
ethereal: template.ethereal || false,
endTurnSelfDmg: template.endTurnSelfDmg || 0,
upgraded: false,
upgradeCost: template.upgradeCost !== undefined ? template.upgradeCost : null,
uid: Math.random().toString(36).substr(2, 9)
};
}
// ── function: createStartingDeck ──
function createStartingDeck(classId) {
const deck = [];
for (let i = 0; i < 5; i++) deck.push(createCardInstance("strike"));
for (let i = 0; i < 4; i++) deck.push(createCardInstance("defend"));
const sig = CLASS_DATA[classId].signatureCard;
deck.push(createCardInstance(sig));
return shuffleArray(deck);
}
// ── 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: addLog ──
function addLog(msg) {
G.logMessages.push(msg);
if (G.logMessages.length > 50) G.logMessages.shift();
}
// ── function: showScreen ──
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
const el = document.getElementById("screen-" + screenId);
if (el) el.classList.add('active');
G.screen = screenId;
}
// ── function: eidIsBoss ──
function eidIsBoss(eid) {
return ["lich_king","eldritch_treant","archdemon"].includes(eid);
}
// ── function: startPlayerTurn ──
function startPlayerTurn() {
G.playerTurn = true;
G.turnNumber++;
G.block = 0; // Block resets at start of player turn
// Apply next-turn buffs
if (G.nextTurnBuffs.block > 0) {
G.block += G.nextTurnBuffs.block;
addLog(`Gain ${G.nextTurnBuffs.block} Block from previous effect.`);
}
if (G.nextTurnBuffs.dex > 0) {
G.buffs.dexterity = Math.max(G.buffs.dexterity, G.nextTurnBuffs.dex);
addLog(`Gain ${G.nextTurnBuffs.dex} Dexterity for this turn.`);
}
// Energy: base 3 + bonuses
let energyBonus = G.tempEnergyBonus;
if (G.relics.includes("cursed_crown")) energyBonus += 1;
if (G.relics.includes("ectoplasm")) energyBonus += 1;
if (G.relics.includes("philosophers_stone")) energyBonus += 1;
G.energy = G.maxEnergy + energyBonus + (G.nextTurnBuffs.energy || 0);
// Reset next-turn buffs that were consumed
G.nextTurnBuffs.block = 0;
G.nextTurnBuffs.dex = 0;
G.nextTurnBuffs.energy = 0;
// Intangible decrements at start of player turn
if (G.buffs.intangible > 0) {
G.buffs.intangible--;
}
// Shadow Amulet: start each turn with 1 Intangible
if (G.startTurnIntangible) {
G.buffs.intangible += 1;
}
// Power card effects at start of turn
const powers = G.deck.filter(c => c.type === "power" && !G.drawPile.includes(c) && !G.hand.includes(c));
// Soul Siphon: gain energy, lose HP
const soulSiphons = powers.filter(c => c.id === "soul_siphon");
soulSiphons.forEach(p => {
G.energy += p.effect.powerStartTurnEnergy || 1;
let selfDmg = p.effect.powerSelfDmg || 1;
applyPlayerHpLoss(selfDmg, true);
});
// Umbral Mastery: apply Weak to all enemies
const umbrals = powers.filter(c => c.id === "umbral_mastery");
umbrals.forEach(p => {
const weakAmt = p.effect.powerWeakAll || 1;
G.enemies.forEach(e => { if (e.alive) e.debuffs.weak = (e.debuffs.weak || 0) + weakAmt; });
addLog(`Umbral Mastery: Applied ${weakAmt} Weak to all enemies.`);
});
// Hellfire Aura: deal damage to all enemies at start of turn
const hellfires = powers.filter(c => c.id === "hellfire_aura");
hellfires.forEach(p => {
const dmg = p.effect.powerDmgAllStartTurn || 3;
G.enemies.forEach(e => {
if (e.alive) dealDamageToEnemy(e, dmg);
});
addLog(`Hellfire Aura: Dealt ${dmg} damage to all enemies.`);
});
// Bleeding Edge relic: apply 1 Bleed to random enemy at start of turn
if (G.relics.includes("bleeding_edge")) {
const aliveEnemies = G.enemies.filter(e => e.alive);
if (aliveEnemies.length > 0) {
const target = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
target.debuffs.bleed = (target.debuffs.bleed || 0) + 1;
addLog("Bleeding Edge: Applied 1 Bleed to a random enemy.");
}
}
// Eternal Hourglass: if no cards in hand, gain 1 Energy
if (G.relics.includes("eternal_hourglass") && G.hand.length === 0) {
G.energy += 1;
addLog("Eternal Hourglass: Gain 1 Energy.");
}
// Draw 5 cards
drawCards(5);
renderCombat();
}
// ── function: drawCards ──
function drawCards(count) {
for (let i = 0; i < count; i++) {
if (G.drawPile.length === 0 && G.discardPile.length > 0) {
// Shuffle discard into draw, excluding exhausted cards
const toShuffle = G.discardPile.filter(c => !c.ethereal);
G.discardPile = [];
G.drawPile = shuffleArray(toShuffle);
}
if (G.drawPile.length > 0) {
const card = G.drawPile.pop();
// Ethereal cards exhaust when drawn
if (card.ethereal) {
G.exhaustPile.push(card);
continue;
}
G.hand.push(card);
}
}
}
// ── function: playCard ──
function playCard(handIdx) {
if (!G.playerTurn) return;
const card = G.hand[handIdx];
if (!card || card.unplayable) return;
if (card.cost > G.energy && card.cost >= 0) return; // Can't afford
// Pay energy cost
if (card.cost > 0) {
G.energy -= card.cost;
}
addLog(`Played ${card.name}.`);
// Execute card effects
executeCardEffect(card, handIdx);
// Tome of Dark Rituals: draw 1 when playing Power
if (G.relics.includes("tome_of_dark_rituals") && card.type === "power") {
drawCards(1);
addLog("Tome of Dark Rituals: Drew 1 card.");
}
// Black Blood: heal 1 HP when playing Attack
if (G.relics.includes("black_blood") && card.type === "attack") {
G.hp = Math.min(G.maxHp, G.hp + 1);
}
// Remove from hand
G.hand.splice(handIdx, 1);
// Exhaust or discard
if (card.exhaust) {
G.exhaustPile.push(card);
} else {
G.discardPile.push(card);
}
checkCombatEnd();
renderCombat();
}
// ── function: executeCardEffect ──
function executeCardEffect(card, handIdx) {
const e = card.effect;
if (!e) return;
// Determine target enemy
let targetEnemy = null;
if (G.selectedEnemyIdx >= 0 && G.enemies[G.selectedEnemyIdx] && G.enemies[G.selectedEnemyIdx].alive) {
targetEnemy = G.enemies[G.selectedEnemyIdx];
} else {
const aliveEnemies = G.enemies.filter(en => en.alive);
if (aliveEnemies.length > 0) targetEnemy = aliveEnemies[0];
}
// Sacrificial Dagger: first HP-costing attack costs 1 less HP
let hpCostReduction = 0;
if (G.relics.includes("sacrificial_dagger") && !G.firstHpCostAttackUsed && card.type === "attack" && e.selfDmg) {
hpCostReduction = 1;
G.firstHpCostAttackUsed = true;
}
// Self damage (HP cost)
if (e.selfDmg !== undefined) {
let dmg = Math.max(0, e.selfDmg - hpCostReduction);
applyPlayerHpLoss(dmg, true);
}
// Block
if (e.block !== undefined) {
const dexBonus = G.buffs.dexterity || 0;
G.block += e.block + dexBonus;
}
// Block equal to missing HP (Vital Surge)
if (e.blockMissingHp) {
let blockAmt = Math.max(0, G.maxHp - G.hp);
if (card.upgraded && e.blockMissingHpPlus) blockAmt += e.blockMissingHpPlus;
const dexBonus = G.buffs.dexterity || 0;
G.block += blockAmt + dexBonus;
}
// Damage to single enemy
if (e.dmg !== undefined && targetEnemy && targetEnemy.alive) {
let dmg = e.dmg;
// Nightmare Lash: extra damage if enemy has Weak
if (card.id === "nightmare_lash" && targetEnemy.debuffs.weak > 0) {
dmg += e.extraIfWeak || 0;
}
dealDamageToEnemy(targetEnemy, dmg);
}
// Damage to ALL enemies
if (e.dmgAll !== undefined) {
G.enemies.forEach(en => {
if (en.alive) dealDamageToEnemy(en, e.dmgAll);
});
}
// Multi-random damage (Eternal Darkness)
if (e.multiRandomDmg !== undefined) {
const aliveEnemies = G.enemies.filter(en => en.alive);
for (let i = 0; i < (e.multiCount || 3); i++) {
if (aliveEnemies.length > 0) {
const target = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
dealDamageToEnemy(target, e.multiRandomDmg);
}
}
}
// Heal
if (e.heal !== undefined) {
G.hp = Math.min(G.maxHp, G.hp + e.heal);
}
// Draw cards
if (e.draw !== undefined) {
drawCards(e.draw);
}
// Gain Energy
if (e.gainEnergy !== undefined) {
G.energy += e.gainEnergy;
}
// Apply Weak to enemy
if (e.weak !== undefined && targetEnemy && targetEnemy.alive) {
targetEnemy.debuffs.weak = (targetEnemy.debuffs.weak || 0) + e.weak;
}
// Apply Vulnerable to enemy
if (e.vulnerable !== undefined && targetEnemy && targetEnemy.alive) {
targetEnemy.debuffs.vulnerable = (targetEnemy.debuffs.vulnerable || 0) + e.vulnerable;
}
// Apply Bleed to enemy
if (e.bleed !== undefined && targetEnemy && targetEnemy.alive) {
let bleedAmt = e.bleed;
targetEnemy.debuffs.bleed = (targetEnemy.debuffs.bleed || 0) + bleedAmt;
// Soul Ember relic: deal 2 additional damage when applying Burn
if (G.relics.includes("soul_ember") && targetEnemy.alive) {
dealDamageToEnemy(targetEnemy, 2);
}
}
// Apply Burn to enemy
if (e.burn !== undefined && targetEnemy && targetEnemy.alive) {
let burnAmt = e.burn;
targetEnemy.debuffs.burn = (targetEnemy.debuffs.burn || 0) + burnAmt;
// Soul Ember relic: deal 2 additional damage when applying Burn
if (G.relics.includes("soul_ember") && targetEnemy.alive) {
dealDamageToEnemy(targetEnemy, 2);
}
}
// Apply Bleed to ALL enemies
if (e.bleedAll !== undefined) {
G.enemies.forEach(en => {
if (en.alive) en.debuffs.bleed = (en.debuffs.bleed || 0) + e.bleedAll;
});
}
// Apply Weak to ALL enemies
if (e.weakAll !== undefined) {
G.enemies.forEach(en => {
if (en.alive) en.debuffs.weak = (en.debuffs.weak || 0) + e.weakAll;
});
}
// Apply Burn to random enemy
if (e.burnRandom !== undefined) {
const aliveEnemies = G.enemies.filter(en => en.alive);
if (aliveEnemies.length > 0) {
const target = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
target.debuffs.burn = (target.debuffs.burn || 0) + e.burnRandom;
// Soul Ember relic
if (G.relics.includes("soul_ember")) {
dealDamageToEnemy(target, 2);
}
}
}
// Enemy loses Strength this turn
if (e.enemyLoseStr !== undefined && targetEnemy && targetEnemy.alive) {
targetEnemy.strBuff = Math.max(0, (targetEnemy.strBuff || 0) - e.enemyLoseStr);
}
// Gain Intangible
if (e.intangible !== undefined) {
G.buffs.intangible += e.intangible;
}
// Next turn Block
if (e.nextBlock !== undefined) {
G.nextTurnBuffs.block = (G.nextTurnBuffs.block || 0) + e.nextBlock;
}
// Next turn Dexterity
if (e.nextDex !== undefined) {
G.nextTurnBuffs.dex = Math.max(G.nextTurnBuffs.dex || 0, e.nextDex);
}
// Next turn Energy
if (e.nextEnergy !== undefined) {
G.nextTurnBuffs.energy = (G.nextTurnBuffs.energy || 0) + e.nextEnergy;
}
// Temp Strength this turn
if (e.tempStr !== undefined) {
G.buffs.strength += e.tempStr;
}
// Regeneration
if (e.regen !== undefined) {
G.buffs.regeneration = (G.buffs.regeneration || 0) + e.regen;
}
// Add Imp Claw to hand
if (e.addImpClaw || e.addImpClawPlus) {
const impId = e.addImpClawPlus ? "imp_claw_plus" : "imp_claw";
G.hand.push(createCardInstance(impId));
}
// Shuffle Dazed into discard pile
if (e.shuffleDazed) {
const dazed = createCardInstance("dazed");
G.discardPile.push(dazed);
}
// Blood Pact: gain Strength on HP loss from card
if (G.relics.includes("blood_pact") && e.selfDmg !== undefined) {
const pactPower = G.deck.find(c => c.id === "blood_pact");
if (pactPower) {
const strGain = pactPower.upgraded ? (pactPower.effect.strengthOnHpLoss || 2) : (pactPower.effect.strengthOnHpLoss || 1);
G.buffs.strength += strGain;
addLog(`Blood Pact: Gain ${strGain} Strength.`);
}
}
// Bloodstone: heal 1 HP when losing HP from card
if (G.relics.includes("bloodstone") && e.selfDmg !== undefined) {
G.hp = Math.min(G.maxHp, G.hp + 1);
addLog("Bloodstone: Healed 1 HP.");
}
}
// ── function: dealDamageToEnemy ──
function dealDamageToEnemy(enemy, baseDmg) {
if (!enemy || !enemy.alive) return;
// Calculate enemy's total strength
const enemyStr = enemy.strBuff || 0;
let dmg = baseDmg + enemyStr;
// Apply Weak debuff (25% less per stack, multiplicative)
if (enemy.debuffs.weak > 0) {
for (let i = 0; i < enemy.debuffs.weak; i++) {
dmg = Math.floor(dmg * 0.75);
}
}
// Apply Vulnerable debuff (50% more per stack, multiplicative)
if (enemy.debuffs.vulnerable > 0) {
for (let i = 0; i < enemy.debuffs.vulnerable; i++) {
dmg = Math.floor(dmg * 1.5);
}
}
// Block absorbs damage
let blockAbsorbed = Math.min(enemy.block, dmg);
enemy.block -= blockAbsorbed;
if (enemy.block < 0) enemy.block = 0;
const actualDmg = dmg - blockAbsorbed;
enemy.hp -= actualDmg;
addLog(`${enemy.name} takes ${actualDmg} damage${blockAbsorbed > 0 ? ` (${blockAbsorbed} blocked)` : ''}.`);
if (enemy.hp <= 0) {
enemy.hp = 0;
enemy.alive = false;
addLog(`${enemy.name} is defeated!`);
}
}
// ── function: applyPlayerHpLoss ──
function applyPlayerHpLoss(amount, fromCard) {
// Intangible reduces all HP loss to 1 per stack
if (G.buffs.intangible > 0) {
amount = Math.min(amount, G.buffs.intangible);
}
G.hp -= amount;
if (amount > 0 && fromCard !== false) addLog(`You lose ${amount} HP.`);
// Bloodstone: heal 1 when losing HP from card
if (G.relics.includes("bloodstone") && fromCard) {
G.hp = Math.min(G.maxHp, G.hp + 1);
}
// Blood Pact: gain Strength on HP loss from card
if (fromCard && G.relics.includes("blood_pact")) {
const pactPower = G.deck.find(c => c.id === "blood_pact");
if (pactPower) {
const strGain = pactPower.upgraded ? (pactPower.effect.strengthOnHpLoss || 2) : (pactPower.effect.strengthOnHpLoss || 1);
G.buffs.strength += strGain;
addLog(`Blood Pact: Gain ${strGain} Strength.`);
}
}
if (G.hp <= 0) {
G.hp = 0;
endCombat(false);
}
}
// ── function: dealDamageToPlayer ──
function dealDamageToPlayer(baseDmg, multiHits) {
// Handle multi-hit attacks separately
if (multiHits && multiHits.hits) {
multiHits.hits.forEach(hit => {
let dmg = hit.dmg + (hit.strBonus || 0);
// Block absorbs damage per hit
const blockAbsorbed = Math.min(G.block, dmg);
G.block -= blockAbsorbed;
if (G.block < 0) G.block = 0;
let actualDmg = dmg - blockAbsorbed;
// Intangible reduces to 1 per stack
if (G.buffs.intangible > 0) {
actualDmg = Math.min(actualDmg, G.buffs.intangible);
}
G.hp -= actualDmg;
addLog(`Took ${actualDmg} damage${blockAbsorbed > 0 ? ` (${blockAbsorbed} blocked)` : ''}.`);
});
} else {
let dmg = baseDmg;
// Block absorbs damage
const blockAbsorbed = Math.min(G.block, dmg);
G.block -= blockAbsorbed;
if (G.block < 0) G.block = 0;
let actualDmg = dmg - blockAbsorbed;
// Intangible reduces to 1 per stack
if (G.buffs.intangible > 0) {
actualDmg = Math.min(actualDmg, G.buffs.intangible);
}
G.hp -= actualDmg;
addLog(`Took ${actualDmg} damage${blockAbsorbed > 0 ? ` (${blockAbsorbed} blocked)` : ''}.`);
}
if (G.hp <= 0) {
G.hp = 0;
endCombat(false);
}
}
// ── function: endPlayerTurn ──
function endPlayerTurn() {
if (!G.playerTurn || !G.combatActive) return;
// Process Doubt curses in hand
G.hand.forEach(card => {
if (card.endTurnSelfDmg > 0) {
applyPlayerHpLoss(card.endTurnSelfDmg, false);
}
});
// Discard hand to discard pile
while (G.hand.length > 0) {
const card = G.hand.pop();
if (!card.exhaust) {
G.discardPile.push(card);
} else {
G.exhaustPile.push(card);
}
}
// Regeneration at end of turn
if (G.buffs.regeneration > 0) {
const healAmt = Math.min(G.buffs.regeneration, G.maxHp - G.hp);
G.hp += healAmt;
G.buffs.regeneration--;
addLog(`Regenerated ${healAmt} HP.`);
}
// Burn on player (if any) at end of turn
if (G.debuffs && G.debuffs.burn > 0) {
const burnDmg = G.debuffs.burn;
applyPlayerHpLoss(burnDmg, false);
G.debuffs.burn = Math.floor(G.debuffs.burn / 2);
}
// Enemy turn(s)
if (G.combatActive && G.hp > 0) {
processEnemyTurn();
}
}
// ── function: processEnemyTurn ──
function processEnemyTurn() {
const aliveEnemies = G.enemies.filter(e => e.alive);
aliveEnemies.forEach(enemy => {
// Process enemy debuffs at start of their turn
if (enemy.debuffs.bleed > 0) {
const bleedDmg = enemy.debuffs.bleed;
enemy.hp -= bleedDmg;
addLog(`${enemy.name} takes ${bleedDmg} Bleed damage.`);
enemy.debuffs.bleed = Math.floor(enemy.debuffs.bleed / 2);
if (enemy.hp <= 0) {
enemy.hp = 0;
enemy.alive = false;
addLog(`${enemy.name} is defeated by Bleed!`);
}
}
// Process Burn at end of enemy turn (we do it here since enemies act after player)
if (enemy.debuffs.burn > 0) {
const burnDmg = enemy.debuffs.burn;
enemy.hp -= burnDmg;
addLog(`${enemy.name} takes ${burnDmg} Burn damage.`);
enemy.debuffs.burn = Math.floor(enemy.debuffs.burn / 2);
if (enemy.hp <= 0) {
enemy.hp = 0;
enemy.alive = false;
addLog(`${enemy.name} is defeated by Burn!`);
}
}
// Poison at start of turn
if (enemy.debuffs.poison > 0) {
const poisonDmg = enemy.debuffs.poison;
enemy.hp -= poisonDmg;
addLog(`${enemy.name} takes ${poisonDmg} Poison damage.`);
enemy.debuffs.poison--;
if (enemy.hp <= 0) {
enemy.hp = 0;
enemy.alive = false;
addLog(`${enemy.name} is defeated by Poison!`);
}
}
// Check boss phase transitions
if (enemy.isBoss && !enemy.phase2) {
const bossDef = getBossDefinition();
if (bossDef && enemy.hp <= bossDef.phase2Threshold) {
enemy.phase2 = true;
addLog(`${enemy.name} enrages!`);
}
}
// Get current intent
let intents = [];
if (enemy.isBoss) {
const bossDef = getBossDefinition();
intents = enemy.phase2 ? bossDef.phase2Intents : bossDef.phase1Intents;
} else {
intents = enemy.intents;
}
const intentIdx = enemy.intentIdx % intents.length;
const intent = intents[intentIdx];
enemy.intentIdx++;
// Execute intent
executeEnemyIntent(enemy, intent);
});
if (G.combatActive && G.hp > 0) {
startPlayerTurn();
}
}
// ── function: getBossDefinition ──
function getBossDefinition() {
const actKey = "act" + G.act;
return BOSS_DB[actKey];
}
// ── function: executeEnemyIntent ──
function executeEnemyIntent(enemy, intent) {
if (!enemy.alive) return;
switch (intent.type) {
case "attack": {
let dmg = intent.dmg + (enemy.strBuff || 0);
dealDamageToPlayer(dmg);
break;
}
case "multiHit": {
const hits = intent.hits.map(h => ({ ...h, strBonus: enemy.strBuff || 0 }));
dealDamageToPlayer(0, { hits });
break;
}
case "defend": {
enemy.block += intent.block;
addLog(`${enemy.name} gains ${intent.block} Block.`);
break;
}
case "attackDefend": {
let dmg = intent.dmg + (enemy.strBuff || 0);
dealDamageToPlayer(dmg);
enemy.block += intent.block;
addLog(`${enemy.name} gains ${intent.block} Block.`);
break;
}
case "debuff": {
if (intent.weak) {
G.debuffs = G.debuffs || {};
G.debuffs.weak = (G.debuffs.weak || 0) + intent.weak;
addLog(`You gain ${intent.weak} Weak.`);
}
if (intent.bleed) {
G.debuffs = G.debuffs || {};
G.debuffs.bleed = (G.debuffs.bleed || 0) + intent.bleed;
addLog(`You gain ${intent.bleed} Bleed.`);
}
if (intent.poison) {
G.debuffs = G.debuffs || {};
G.debuffs.poison = (G.debuffs.poison || 0) + intent.poison;
addLog(`You gain ${intent.poison} Poison.`);
}
break;
}
case "debuffAll": {
if (intent.vulnerable) {
G.debuffs = G.debuffs || {};
G.debuffs.vulnerable = (G.debuffs.vulnerable || 0) + intent.vulnerable;
addLog(`You gain ${intent.vulnerable} Vulnerable.`);
}
if (intent.weak) {
G.debuffs = G.debuffs || {};
G.debuffs.weak = (G.debuffs.weak || 0) + intent.weak;
addLog(`You gain ${intent.weak} Weak.`);
}
break;
}
case "attackDebuff": {
let dmg = intent.dmg + (enemy.strBuff || 0);
dealDamageToPlayer(dmg);
if (intent.weak) { G.debuffs = G.debuffs || {}; G.debuffs.weak = (G.debuffs.weak || 0) + intent.weak; }
if (intent.burn) { G.debuffs = G.debuffs || {}; G.debuffs.burn = (G.debuffs.burn || 0) + intent.burn; addLog(`You gain ${intent.burn} Burn.`); }
break;
}
case "selfBuff": {
if (intent.block) enemy.block += intent.block;
if (intent.regen) enemy.strBuff = (enemy.strBuff || 0); // regen handled separately
if (intent.intangible) addLog(`${enemy.name} gains Intangible.`);
break;
}
case "selfVulnDefend": {
enemy.block += intent.block;
enemy.debuffs.vulnerable = (enemy.debuffs.vulnerable || 0) + 1;
addLog(`${enemy.name} gains ${intent.block} Block and applies Vulnerable to itself.`);
break;
}
case "defendBuff": {
enemy.block += intent.block;
if (intent.str) enemy.strBuff = (enemy.strBuff || 0) + intent.str;
if (intent.regen) addLog(`${enemy.name} gains Regeneration.`);
break;
}
case "buffAll": {
G.enemies.forEach(e => { if (e.alive && e !== enemy) e.strBuff = (e.strBuff || 0) + intent.str; });
enemy.strBuff = (enemy.strBuff || 0) + intent.str;
addLog(`${enemy.name} grants ${intent.str} Strength to all enemies.`);
break;
}
case "drainLife": {
let dmg = intent.dmg + (enemy.strBuff || 0);
dealDamageToPlayer(dmg);
enemy.hp = Math.min(enemy.maxHp, enemy.hp + intent.heal);
addLog(`${enemy.name} heals ${intent.heal} HP.`);
break;
}
case "summon": {
// Check max minions for boss summons
if (enemy.isBoss) {
const minionCount = G.enemies.filter(e => !e.isBoss).length;
if (minionCount >= 2) {
addLog(`${enemy.name} cannot summon more minions.`);
return;
}
}
// Check for duplicate imp summons
if (intent.summonId === "infernal_imp") {
const existingImps = G.enemies.filter(e => e.id === "infernal_imp").length;
if (existingImps >= 2) return;
}
const summonTemplate = ENEMY_DB[intent.summonId];
if (summonTemplate) {
const newEnemy = {
id: intent.summonId, name: summonTemplate.name, hp: summonTemplate.hp, maxHp: summonTemplate.maxHp,
block: 0, intentIdx: 0, intents: [...summonTemplate.intents], alive: true,
buffs: {}, debuffs: {}, strBuff: 0, isBoss: false, phase2: false,
idx: G.enemies.length
};
G.enemies.push(newEnemy);
addLog(`${enemy.name} summons ${summonTemplate.name}!`);
}
break;
}
case "defendHeal": {
enemy.block += intent.block;
enemy.hp = Math.min(enemy.maxHp, enemy.hp + intent.heal);
addLog(`${enemy.name} gains ${intent.block} Block and heals ${intent.heal} HP.`);
break;
}
case "attackStrGain": {
if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
let dmg = intent.dmg + (enemy.strBuff || 0);
dealDamageToPlayer(dmg);
break;
}
case "multiHitStrGain": {
if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
const hits = intent.hits.map(h => ({ ...h, strBonus: enemy.strBuff || 0 }));
dealDamageToPlayer(0, { hits });
break;
}
case "defendStrGain": {
if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
enemy.block += intent.block;
addLog(`${enemy.name} gains ${intent.block} Block.`);
break;
}
case "attackDebuffStrGain": {
if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
let dmg = intent.dmg + (enemy.strBuff || 0);
dealDamageToPlayer(dmg);
if (intent.vulnerable) { G.debuffs = G.debuffs || {}; G.debuffs.vulnerable = (G.debuffs.vulnerable || 0) + intent.vulnerable; }
break;
}
case "defendHealStrGain": {
if (intent.strGain) enemy.strBuff = (enemy.strBuff || 0) + intent.strGain;
enemy.block += intent.block;
enemy.hp = Math.min(enemy.maxHp, enemy.hp + intent.heal);
addLog(`${enemy.name} gains ${intent.block} Block and heals ${intent.heal} HP.`);
break;
}
}
}
// ── function: checkCombatEnd ──
function checkCombatEnd() {
const aliveEnemies = G.enemies.filter(e => e.alive);
if (aliveEnemies.length === 0) {
endCombat(true);
}
}
// ── function: generateCombatRewards ──
function generateCombatRewards(isElite) {
// Gold reward
let goldAmt;
if (isElite) {
goldAmt = 25 + Math.floor(Math.random() * 11); // 25-35
} else {
goldAmt = 10 + Math.floor(Math.random() * 11); // 10-20
}
if (G.relics.includes("greedy_imp")) goldAmt += 10;
if (!G.noMoreGold) G.gold += goldAmt;
// Event combat bonus gold
if (G.eventCombatGold > 0) {
G.gold += G.eventCombatGold;
G.eventCombatGold = 0;
}
// Card reward
const cards = generateCardRewards(isElite, false);
// Potion chance
let potionChance = isElite ? 0.7 : 0.4;
if (Math.random() < potionChance && G.potions.length < G.potionSlots) {
const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
G.potions.push(potion.id);
}
// Show reward screen
showRewardScreen(cards, goldAmt);
}
// ── function: generateBossRewards ──
function generateBossRewards() {
let goldAmt = 50;
if (G.relics.includes("greedy_imp")) goldAmt += 10;
if (!G.noMoreGold) G.gold += goldAmt;
// Boss card rewards - always include one rare
const cards = generateCardRewards(false, true);
// Always drops 2 potions
for (let i = 0; i < 2 && G.potions.length < G.potionSlots; i++) {
const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
if (!G.potions.includes(potion.id)) {
G.potions.push(potion.id);
} else {
// Allow duplicates of potions
G.potions.push(potion.id);
}
}
// Boss relic choice (3 from boss pool)
const availableBossRelics = BOSS_RELICS.filter(r => !G.relics.includes(r));
const choices = shuffleArray(availableBossRelics).slice(0, 3);
showRewardScreen(cards, goldAmt, choices);
}
// ── function: generateCardRewards ──
function generateCardRewards(isElite, isBoss) {
const classPool = CLASS_DATA[G.playerClass].cardPool;
const cards = [];
for (let i = 0; i < 3; i++) {
let cardId;
if (isBoss && i === 0) {
// Boss rewards always include one rare
const rares = classPool.filter(cid => CARD_DB[cid].rarity === "rare");
cardId = rares[Math.floor(Math.random() * rares.length)];
} else {
// Weighted random selection
let rarity;
const roll = Math.random();
if (isElite) {
if (roll < 0.6) rarity = "common";
else if (roll < 0.8) rarity = "uncommon";
else rarity = "rare";
} else {
if (roll < 0.6) rarity = "common";
else if (roll < 0.9) rarity = "uncommon";
else rarity = "rare";
}
const poolByRarity = classPool.filter(cid => CARD_DB[cid].rarity === rarity);
if (poolByRarity.length > 0) {
cardId = poolByRarity[Math.floor(Math.random() * poolByRarity.length)];
} else {
cardId = classPool[Math.floor(Math.random() * classPool.length)];
}
}
cards.push(createCardInstance(cardId));
}
return cards;
}
// ── function: showRewardScreen ──
function showRewardScreen(cards, goldAmt, bossRelicChoices) {
G.pendingReward = { cards, goldAmt, bossRelicChoices };
if (bossRelicChoices && bossRelicChoices.length > 0) {
// Show relic choice first
renderRelicChoice(bossRelicChoices);
showScreen("relic-choice");
} else {
renderRewardScreen(cards, goldAmt);
showScreen("reward");
}
}
// ── function: renderRewardScreen ──
function renderRewardScreen(cards, goldAmt) {
const container = document.getElementById("reward-cards");
container.innerHTML = "";
document.getElementById("reward-gold").textContent = `+${goldAmt} Gold`;
cards.forEach(card => {
const el = createCardElement(card);
el.addEventListener('click', () => {
G.deck.push(card);
advanceToMap();
});
container.appendChild(el);
});
}
// ── function: renderRelicChoice ──
function renderRelicChoice(choices) {
const container = document.getElementById("relic-choices");
container.innerHTML = "";
choices.forEach(relicId => {
const relic = BOSS_RELIC_DB[relicId];
if (!relic) return;
const el = document.createElement("div");
el.className = "relic-choice";
el.innerHTML = `<h3>${relic.name}</h3><p style="font-size:0.8em;color:#9a8f80;margin-top:5px;">${relic.desc}</p>`;
el.addEventListener('click', () => {
G.relics.push(relicId);
// Apply immediate effects
if (relicId === "ectoplasm") {
G.gold = 0;
G.noMoreGold = true;
}
if (relicId === "shadow_amulet") {
G.maxHp -= 15;
G.hp = Math.min(G.hp, G.maxHp);
G.startTurnIntangible = true;
}
// After relic choice, show card rewards
renderRewardScreen(G.pendingReward.cards, G.pendingReward.goldAmt);
showScreen("reward");
});
container.appendChild(el);
});
}
// ── function: advanceToMap ──
function advanceToMap() {
G.floor++;
if (G.floor > 15) {
// Act complete - handled in endCombat for bosses
return;
}
renderMap();
showScreen("map");
}
// ── function: createCardElement ──
// ===== UI RENDERING =====
function createCardElement(card) {
const el = document.createElement("div");
let rarityClass = `rarity-${card.rarity}`;
if (card.rarity === "basic") rarityClass = "rarity-basic";
el.className = `card type-${card.type} ${rarityClass}${card.upgraded ? ' upgraded' : ''}`;
const costDisplay = card.cost < 0 ? "" : card.cost === 0 ? `<div class="card-cost free">0</div>` : `<div class="card-cost">${card.cost}</div>`;
el.innerHTML = `
${costDisplay}
<div class="card-type-badge"></div>
<div class="card-name">${card.name}</div>
<div class="card-desc">${card.desc}</div>
<div class="card-rarity">${card.rarity.toUpperCase()}</div>
`;
return el;
}
// ── function: renderHand ──
function renderHand() {
const handZone = document.getElementById("hand-zone");
handZone.innerHTML = "";
G.hand.forEach((card, idx) => {
const el = createCardElement(card);
if (card.unplayable || card.cost > G.energy) {
el.classList.add("unplayable");
} else {
el.addEventListener('click', () => playCard(idx));
}
handZone.appendChild(el);
});
}
// ── function: renderPotionBar ──
function renderPotionBar() {
const bar = document.getElementById("potion-bar");
if (!bar) return;
bar.innerHTML = "";
G.potions.forEach((potionId, idx) => {
const potionDef = POTION_DB.find(p => p.id === potionId);
if (!potionDef) return;
const btn = document.createElement("button");
btn.className = "potion-btn";
btn.textContent = potionDef.icon;
btn.title = `${potionDef.name}: ${potionDef.desc}`;
btn.addEventListener('click', () => usePotion(idx));
bar.appendChild(btn);
});
}
// ── function: usePotion ──
function usePotion(potionIdx) {
if (!G.playerTurn || !G.combatActive) return;
const potionId = G.potions[potionIdx];
const potionDef = POTION_DB.find(p => p.id === potionId);
if (!potionDef) return;
// Some potions need a target
if (["bleeding_flask"].includes(potionId)) {
showPotionTargetScreen(potionIdx, potionId);
return;
}
executePotionEffect(potionId);
G.potions.splice(potionIdx, 1);
renderCombat();
}
// ── function: executePotionEffect ──
function executePotionEffect(potionId) {
switch (potionId) {
case "blood_vial":
G.hp = Math.min(G.maxHp, G.hp + 15);
addLog("Used Blood Vial: Healed 15 HP.");
break;
case "firebomb":
G.enemies.forEach(e => { if (e.alive) dealDamageToEnemy(e, 10); });
addLog("Used Firebomb: Dealt 10 damage to all enemies.");
checkCombatEnd();
break;
case "dex_tonic":
G.buffs.dexterity = Math.max(G.buffs.dexterity || 0, 3);
addLog("Used Dex Tonic: Gain 3 Dexterity for this turn.");
break;
case "energy_elixir":
G.energy += 2;
addLog("Used Energy Elixir: Gain 2 Energy.");
break;
case "cursed_brew":
G.enemies.forEach(e => { if (e.alive) e.debuffs.vulnerable = (e.debuffs.vulnerable || 0) + 3; });
addLog("Used Cursed Brew: Applied 3 Vulnerable to all enemies.");
break;
case "imp_in_bottle":
G.hand.push(createCardInstance("imp_claw_plus"));
addLog("Used Imp-in-a-Bottle: Added Imp Claw+ to hand.");
break;
case "smoke_bomb": {
const isBossFight = G.enemies.some(e => e.isBoss);
if (isBossFight) {
addLog("Cannot use Smoke Bomb on a boss!");
return;
}
addLog("Used Smoke Bomb: Escaped combat.");
endCombat(false); // No rewards, but don't die
G.combatActive = false;
advanceToMap();
break;
}
}
}
// ── function: showPotionTargetScreen ──
function showPotionTargetScreen(potionIdx, potionId) {
const screen = document.getElementById("screen-potion-use");
const targetsEl = document.getElementById("potion-targets");
targetsEl.innerHTML = "";
G.enemies.forEach((enemy, idx) => {
if (!enemy.alive) return;
const el = document.createElement("div");
el.className = "enemy-card";
el.innerHTML = `<div class="enemy-name">${enemy.name}</div><div>${enemy.hp}/${enemy.maxHp} HP</div>`;
el.addEventListener('click', () => {
if (potionId === "bleeding_flask") {
enemy.debuffs.bleed = (enemy.debuffs.bleed || 0) + 6;
addLog("Used Bleeding Flask: Applied 6 Bleed.");
}
G.potions.splice(potionIdx, 1);
screen.style.display = "none";
renderCombat();
});
targetsEl.appendChild(el);
});
screen.style.display = "flex";
}
// ── function: renderMap ──
// Fix renderMap to draw connections properly between nodes
function renderMap() {
const canvas = document.getElementById("map-canvas");
const ctx = canvas.getContext("2d");
// Clear
ctx.fillStyle = "#0a0a0f";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw subtle grid
ctx.strokeStyle = "rgba(58,45,74,0.15)";
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 60) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
const nodePositions = getNodePositions();
// Draw connections between consecutive floors
ctx.strokeStyle = "rgba(58,45,74,0.5)";
ctx.lineWidth = 2;
for (let i = 0; i < G.mapNodes.length - 1; i++) {
const fromNode = G.mapNodes[i];
// Find nodes at next floor
for (let j = i + 1; j < G.mapNodes.length; j++) {
const toNode = G.mapNodes[j];
if (toNode.floor === fromNode.floor + 1) {
const fromPos = nodePositions[i];
const toPos = nodePositions[j];
if (fromPos && toPos) {
ctx.beginPath();
ctx.moveTo(fromPos.x, fromPos.y);
ctx.lineTo(toPos.x, toPos.y);
ctx.stroke();
}
}
}
}
// Draw nodes
G.mapNodes.forEach((node, idx) => {
const pos = nodePositions[idx];
if (!pos) return;
const visited = G.visitedNodes.has(idx);
const current = idx === G.currentNodeIdx;
const reachable = isNodeReachable(idx);
// Node color based on type
let fillColor, strokeColor;
switch (node.type) {
case "enemy": fillColor = visited ? "#2d4a2d" : "#3d1c1c"; strokeColor = visited ? "#5a8a5a" : "#5a2d2d"; break;
case "elite": fillColor = visited ? "#4a2d4a" : "#5a2d5a"; strokeColor = visited ? "#9b59b6" : "#7a3d7a"; break;
case "boss": fillColor = "#8b0000"; strokeColor = "#ff4444"; break;
case "rest": fillColor = visited ? "#2d4a4a" : "#1c3d3d"; strokeColor = visited ? "#5a8a8a" : "#2d5a5a"; break;
case "shop": fillColor = visited ? "#4a4a2d" : "#3d3d1c"; strokeColor = visited ? "#8a8a5a" : "#5a5a2d"; break;
case "event": fillColor = visited ? "#2d2d4a" : "#1c1c3d"; strokeColor = visited ? "#5a5a8a" : "#2d2d5a"; break;
default: fillColor = "#1a1520"; strokeColor = "#3a2d4a";
}
if (!reachable && !visited) {
fillColor = "rgba(26,21,32,0.5)";
strokeColor = "rgba(58,45,74,0.3)";
}
// Draw node circle
ctx.beginPath();
ctx.arc(pos.x, pos.y, 20, 0, Math.PI * 2);
ctx.fillStyle = fillColor;
ctx.fill();
ctx.strokeStyle = strokeColor;
ctx.lineWidth = current ? 3 : 2;
ctx.stroke();
// Glow for reachable nodes
if (reachable && !visited) {
ctx.beginPath();
ctx.arc(pos.x, pos.y, 25, 0, Math.PI * 2);
ctx.strokeStyle = "rgba(196,30,58,0.4)";
ctx.lineWidth = 2;
ctx.stroke();
}
// Node icon
ctx.fillStyle = reachable || visited ? "#d4c8b0" : "#4a3d5a";
ctx.font = "16px Georgia";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let icon;
switch (node.type) {
case "enemy": icon = node.enemies && node.enemies.length > 1 ? "⚔⚔" : "⚔"; break;
case "elite": icon = "💀"; break;
case "boss": icon = "👑"; break;
case "rest": icon = "🏕"; break;
case "shop": icon = "🛒"; break;
case "event": icon = "❓"; break;
default: icon = "?";
}
ctx.fillText(icon, pos.x, pos.y);
// Floor number below node
ctx.fillStyle = "#7a6f60";
ctx.font = "10px Georgia";
ctx.textBaseline = "top";
ctx.fillText(`F${node.floor}`, pos.x, pos.y + 24);
});
// Update header
const actNames = ["", "The Crypts", "Blighted Woods", "Abyssal Spire"];
document.getElementById("map-act-name").textContent = `Act ${G.act}: ${actNames[G.act]}`;
document.getElementById("map-floor-num").textContent = `Floor ${G.floor}/15`;
// Click handler for nodes
canvas.onclick = (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
for (let idx = 0; idx < G.mapNodes.length; idx++) {
if (!isNodeReachable(idx)) continue;
const pos = nodePositions[idx];
if (!pos) continue;
const dist = Math.sqrt((mx - pos.x) ** 2 + (my - pos.y) ** 2);
if (dist < 25) {
enterNode(idx);
return;
}
}
};
}
// ── function: getNodePositions ──
function getNodePositions() {
const positions = [];
const cols = [100, 300, 500, 700];
G.mapNodes.forEach((node) => {
// Y position: floor 2 at bottom (y=540), floor 15 at top (y=60)
const y = 560 - ((node.floor - 1) * 38);
positions.push({ x: cols[node.col], y });
});
return positions;
}
// ── function: isNodeReachable ──
function isNodeReachable(idx) {
const node = G.mapNodes[idx];
if (!node) return false;
// If no nodes visited yet (start of game), show first floor options
if (G.visitedNodes.size === 0 && G.currentNodeIdx === -1) {
return node.floor === 2;
}
// Node must be at next floor after current
const currentNode = G.mapNodes[G.currentNodeIdx];
if (!currentNode) return false;
if (node.floor !== currentNode.floor + 1) return false;
// Column adjacency: can move to adjacent columns
const colDiff = Math.abs(node.col - currentNode.col);
return colDiff <= 2;
}
// ── function: getRandomEnemyForAct ──
function getRandomEnemyForAct(act) {
const pool = ACT_ENEMY_POOLS["act" + act];
return pool[Math.floor(Math.random() * pool.length)];
}
// ── function: getRandomEliteForAct ──
function getRandomEliteForAct(act) {
const pool = ACT_ELITE_POOLS["act" + act];
return pool[Math.floor(Math.random() * pool.length)];
}
// ── function: showEventScreen ──
function showEventScreen() {
const event = EVENTS[Math.floor(Math.random() * EVENTS.length)];
document.getElementById("event-title").textContent = event.title;
document.getElementById("event-text").textContent = event.text;
const choicesEl = document.getElementById("event-choices");
choicesEl.innerHTML = "";
event.choices.forEach(choice => {
const btn = document.createElement("button");
btn.className = "event-choice-btn";
btn.textContent = choice.text;
btn.addEventListener('click', () => handleEventChoice(choice.action));
choicesEl.appendChild(btn);
});
showScreen("event");
}
// ── function: handleEventChoice ──
function handleEventChoice(action) {
switch (action) {
case "offer_hp_relic":
if (G.hp > 15) {
G.hp -= 15;
const rareRelics = Object.keys(RELIC_DB).filter(r => RELIC_DB[r].rarity === "rare" && !G.relics.includes(r));
if (rareRelics.length > 0) {
G.relics.push(rareRelics[Math.floor(Math.random() * rareRelics.length)]);
} else {
G.gold += 50; // consolation
}
}
break;
case "pray_heal_curse":
G.hp = Math.min(G.maxHp, G.hp + 10);
G.deck.push(createCardInstance("doubt"));
break;
case "trade_card":
showRemoveCardScreen(() => {
G.gold += 100;
advanceToMap();
});
return;
case "buy_wares":
if (G.gold >= 50) {
G.gold -= 50;
const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
if (G.potions.length < G.potionSlots) G.potions.push(potion.id);
const commonRelics = Object.keys(RELIC_DB).filter(r => RELIC_DB[r].rarity === "common" && !G.relics.includes(r));
if (commonRelics.length > 0) {
G.relics.push(commonRelics[Math.floor(Math.random() * commonRelics.length)]);
}
}
break;
case "attack_peddler":
G.eventCombatGold = 75;
startCombat(["shadow_thief"]);
return;
case "drink_cures":
if (G.hp > 10) {
G.hp -= 10;
G.deck = G.deck.filter(c => c.type !== "curse" || !G.cursesUnremovable);
}
break;
case "bottle_potion":
if (G.potions.length < G.potionSlots) G.potions.push("cursed_brew");
break;
case "upgrade_random":
if (G.hp > 20) {
G.hp -= 20;
const upgradable = G.deck.filter(c => !c.upgraded && c.type !== "curse" && c.type !== "status");
for (let i = 0; i < Math.min(2, upgradable.length); i++) {
upgradeCard(upgradable[i]);
}
}
break;
case "bet_gold":
if (G.gold >= 30) {
G.gold -= 30;
if (Math.random() < 0.5) {
G.gold += 60;
}
}
break;
case "wager_card": {
const deckCopy = [...G.deck];
if (deckCopy.length > 10) { // Don't remove from very small decks
const idx = Math.floor(Math.random() * G.deck.length);
const removed = G.deck.splice(idx, 1)[0];
if (removed.type === "curse") {
G.gold += 100;
}
}
break;
}
case "study_energy":
if (G.hp > 5) {
G.hp -= 5;
G.tempEnergyBonus++;
}
break;
case "read_power_curse": {
const classPool = CLASS_DATA[G.playerClass].cardPool;
const powers = classPool.filter(cid => CARD_DB[cid].type === "power");
if (powers.length > 0) {
G.deck.push(createCardInstance(powers[Math.floor(Math.random() * powers.length)]));
}
G.deck.push(createCardInstance("doubt"));
break;
}
case "nothing":
// Nothing happens
break;
}
advanceToMap();
}
// ── function: showRestScreen ──
function showRestScreen() {
document.getElementById("rest-hp-text").textContent = `HP: ${G.hp}/${G.maxHp}`;
const choicesEl = document.getElementById("rest-choices");
choicesEl.innerHTML = "";
// Rest option
const restBtn = document.createElement("button");
restBtn.className = "rest-btn";
restBtn.textContent = `🏕 Rest (Heal ${Math.floor(G.maxHp * 0.25)} HP)`;
restBtn.addEventListener('click', () => {
G.hp = Math.min(G.maxHp, G.hp + Math.floor(G.maxHp * 0.25));
advanceToMap();
});
choicesEl.appendChild(restBtn);
// Forge option
const forgeBtn = document.createElement("button");
forgeBtn.className = "rest-btn";
forgeBtn.textContent = "⚒ Forge (Upgrade a card)";
forgeBtn.addEventListener('click', () => showUpgradeScreen());
choicesEl.appendChild(forgeBtn);
// Meditate option
const meditateBtn = document.createElement("button");
meditateBtn.className = "rest-btn";
meditateBtn.textContent = "🧘 Meditate (Remove a card)";
meditateBtn.addEventListener('click', () => showRemoveCardScreen(() => advanceToMap()));
choicesEl.appendChild(meditateBtn);
showScreen("rest");
}
// ── function: showUpgradeScreen ──
function showUpgradeScreen() {
const upgradable = G.deck.filter(c => !c.upgraded && c.type !== "curse" && c.type !== "status");
if (upgradable.length === 0) {
alert("No cards to upgrade!");
advanceToMap();
return;
}
const container = document.getElementById("upgrade-cards");
container.innerHTML = "";
upgradable.forEach(card => {
const el = createCardElement(card);
el.addEventListener('click', () => {
upgradeCard(card);
advanceToMap();
});
container.appendChild(el);
});
showScreen("upgrade-card");
}
// ── function: upgradeCard ──
function upgradeCard(card) {
const template = CARD_DB[card.id];
if (!template || !template.upgrade) return;
card.upgraded = true;
Object.assign(card.effect, template.upgrade);
// Update description to reflect upgraded values
let newDesc = template.desc;
if (template.upgrade.dmg !== undefined) {
newDesc = newDesc.replace(/Deal \d+ damage/, `Deal ${template.upgrade.dmg} damage`);
}
if (template.upgrade.block !== undefined) {
newDesc = newDesc.replace(/Gain \d+ Block/, `Gain ${template.upgrade.block} Block`);
}
card.desc = newDesc;
// Handle cost reduction for Soul Siphon
if (card.id === "soul_siphon" && template.upgradeCost !== undefined) {
card.cost = template.upgradeCost;
}
addLog(`Upgraded ${card.name} to ${card.name}+`);
}
// ── function: showRemoveCardScreen ──
function showRemoveCardScreen(callback) {
const removable = G.deck.filter(c => c.type !== "curse" || !G.cursesUnremovable);
if (removable.length === 0) {
callback();
return;
}
document.getElementById("remove-title").textContent = "Remove a Card";
document.getElementById("remove-desc").textContent = "Choose a card to remove from your deck.";
const container = document.getElementById("remove-cards");
container.innerHTML = "";
removable.forEach(card => {
const el = createCardElement(card);
el.addEventListener('click', () => {
const idx = G.deck.indexOf(card);
if (idx >= 0) G.deck.splice(idx, 1);
callback();
});
container.appendChild(el);
});
showScreen("remove-card");
}
// ── function: showShopScreen ──
function showShopScreen() {
document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
const itemsEl = document.getElementById("shop-items");
itemsEl.innerHTML = "";
// Generate shop cards (3 class cards + 1 colorless)
const classPool = CLASS_DATA[G.playerClass].cardPool;
const shopCards = [];
for (let i = 0; i < 3; i++) {
const cardId = classPool[Math.floor(Math.random() * classPool.length)];
shopCards.push({ type: "card", data: createCardInstance(cardId) });
}
// Colorless card
const colorlessId = COLORLESS_CARDS[Math.floor(Math.random() * COLORLESS_CARDS.length)];
shopCards.push({ type: "card", data: createCardInstance(colorlessId), colorless: true });
// Relics (2)
const availableRelics = Object.keys(RELIC_DB).filter(r => !G.relics.includes(r));
for (let i = 0; i < 2 && availableRelics.length > 0; i++) {
const idx = Math.floor(Math.random() * availableRelics.length);
shopCards.push({ type: "relic", data: availableRelics[idx] });
availableRelics.splice(idx, 1);
}
// Potions (3)
for (let i = 0; i < 3; i++) {
const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
shopCards.push({ type: "potion", data: potion });
}
// Card removal service
shopCards.push({ type: "remove" });
shopCards.forEach(item => {
const el = document.createElement("div");
if (item.type === "card") {
const cardEl = createCardElement(item.data);
const price = item.data.rarity === "rare" ? 150 : item.data.rarity === "uncommon" ? 75 : 50;
el.className = "shop-item";
if (item.colorless) el.style.borderColor = "#6baed6";
const priceEl = document.createElement("div");
priceEl.className = "item-price";
priceEl.textContent = `${price}g`;
el.appendChild(cardEl);
el.appendChild(priceEl);
if (G.gold >= price) {
el.addEventListener('click', () => {
G.gold -= price;
G.deck.push(item.data);
document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
renderShopAfterPurchase();
});
} else {
el.style.opacity = "0.4";
el.style.cursor = "not-allowed";
}
} else if (item.type === "relic") {
const relicDef = RELIC_DB[item.data];
el.className = "shop-item relic-shop";
el.innerHTML = `<div style="font-size:1.5em;">🏆</div><h3 style="color:#9b59b6;font-size:0.85em;margin-top:5px;">${relicDef.name}</h3>
<p style="font-size:0.7em;color:#9a8f80;margin-top:3px;">${relicDef.desc}</p><div class="item-price">150g</div>`;
if (G.gold >= 150) {
el.addEventListener('click', () => {
G.gold -= 150;
G.relics.push(item.data);
// Vial of Shadows: remove a card on pickup
if (item.data === "vial_of_shadows") {
showRemoveCardScreen(() => {
document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
renderShopAfterPurchase();
});
return;
}
document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
renderShopAfterPurchase();
});
} else {
el.style.opacity = "0.4";
el.style.cursor = "not-allowed";
}
} else if (item.type === "potion") {
const potionDef = item.data;
el.className = "shop-item";
el.innerHTML = `<div style="font-size:1.5em;">${potionDef.icon}</div><h3 style="color:#d4c8b0;font-size:0.85em;margin-top:5px;">${potionDef.name}</h3>
<p style="font-size:0.7em;color:#9a8f80;margin-top:3px;">${potionDef.desc}</p><div class="item-price">50g</div>`;
if (G.gold >= 50 && G.potions.length < G.potionSlots) {
el.addEventListener('click', () => {
G.gold -= 50;
G.potions.push(potionDef.id);
document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
renderShopAfterPurchase();
});
} else if (G.potions.length >= G.potionSlots) {
el.style.opacity = "0.4";
el.innerHTML += `<div style="color:#ff6b6b;font-size:0.7em;">Potion slots full</div>`;
} else {
el.style.opacity = "0.4";
el.style.cursor = "not-allowed";
}
} else if (item.type === "remove") {
el.className = "shop-item";
el.innerHTML = `<div style="font-size:1.5em;">🗑</div><h3 style="color:#d4c8b0;font-size:0.85em;margin-top:5px;">Remove Card</h3>
<p style="font-size:0.7em;color:#9a8f80;margin-top:3px;">Remove 1 card from deck</p><div class="item-price">75g</div>`;
if (G.gold >= 75) {
el.addEventListener('click', () => {
G.gold -= 75;
showRemoveCardScreen(() => {
document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
renderShopAfterPurchase();
});
});
} else {
el.style.opacity = "0.4";
el.style.cursor = "not-allowed";
}
}
itemsEl.appendChild(el);
});
showScreen("shop");
}
// ── function: renderShopAfterPurchase ──
function renderShopAfterPurchase() {
// Re-render shop with updated gold
document.getElementById("shop-gold-display").textContent = `Gold: ${G.gold}`;
const itemsEl = document.getElementById("shop-items");
// Simple re-render by refreshing the page - in a real game we'd be more surgical
}
// ── function: renderClassSelection ──
// ===== CLASS SELECTION & GAME INIT =====
function renderClassSelection() {
const container = document.getElementById("class-cards");
container.innerHTML = "";
Object.entries(CLASS_DATA).forEach(([id, data]) => {
const el = document.createElement("div");
el.className = "class-card";
const sigCard = CARD_DB[data.signatureCard];
el.innerHTML = `
<h3 style="color:${data.color}">${data.name}</h3>
<p>${data.theme}</p>
<hr style="border-color:#3a2d4a;margin:10px 0;">
<p style="font-size:0.8em;color:#7a6f60;">Starting Deck:</p>
<p style="font-size:0.75em;color:#9a8f80;">5× Strike, 4× Defend</p>
<p style="font-size:0.75em;color:${data.color};">1× ${sigCard.name}</p>
<hr style="border-color:#3a2d4a;margin:10px 0;">
<p style="font-size:0.8em;color:#ffd700;">99 Gold</p>
`;
el.addEventListener('click', () => selectClass(id));
container.appendChild(el);
});
}
// ── function: selectClass ──
function selectClass(classId) {
G.playerClass = classId;
// Initialize player stats
G.hp = 70;
G.maxHp = 70;
G.gold = 99;
G.act = 1;
G.floor = 2;
G.relics = [];
G.potions = [];
G.deck = createStartingDeck(classId);
// Shadow Amulet effect (if from previous run - shouldn't happen but safety)
if (G.startTurnIntangible) {
G.maxHp -= 15;
G.hp = Math.min(G.hp, G.maxHp);
}
addLog(`Started as ${CLASS_DATA[classId].name}.`);
// Generate map and show it
generateMap();
renderMap();
showScreen("map");
}
// ── function: startNewGame ──
function startNewGame() {
initGameState();
renderClassSelection();
showScreen("class-select");
}
// ── function: setupEventListeners ──
// ===== EVENT LISTENERS SETUP =====
function setupEventListeners() {
// Title screen -> Class selection
document.getElementById("btn-start").addEventListener('click', () => {
startNewGame();
});
// End Turn button
document.getElementById("btn-end-turn").addEventListener('click', () => {
endPlayerTurn();
});
// Skip reward
document.getElementById("btn-skip-reward").addEventListener('click', () => {
advanceToMap();
});
// Leave shop
document.getElementById("btn-leave-shop").addEventListener('click', () => {
advanceToMap();
});
// Cancel remove card
document.getElementById("btn-cancel-remove").addEventListener('click', () => {
showScreen("rest");
});
// Cancel upgrade
document.getElementById("btn-cancel-upgrade").addEventListener('click', () => {
showScreen("rest");
});
// Cancel potion target
document.getElementById("btn-cancel-potion").addEventListener('click', () => {
document.getElementById("screen-potion-use").style.display = "none";
});
// Restart / Play again
document.getElementById("btn-restart").addEventListener('click', () => {
initGameState();
showScreen("title");
});
document.getElementById("btn-play-again").addEventListener('click', () => {
initGameState();
showScreen("title");
});
}
// ── function: getBossEnemyId ──
function getBossEnemyId(act) {
const bossDef = BOSS_DB["act" + act];
if (!bossDef) return "skeleton_warrior";
return bossDef.name.toLowerCase().replace(/\s+/g, '_');
}
// ── const: originalEnterNode ──
// Override enterNode boss handling
const originalEnterNode = enterNode;
// ── function: startCombat ──
function startCombat(enemyIds) {
G.combatActive = true;
G.playerTurn = true;
G.turnNumber = 0;
G.selectedEnemyIdx = -1;
G.firstHpCostAttackUsed = false;
G.debuffs = {}; // Player debuffs: weak, vulnerable, bleed, burn, poison
// Create enemies from IDs
G.enemies = [];
enemyIds.forEach((eid, idx) => {
// Check if it's a boss
const actKey = "act" + G.act;
const bossDef = BOSS_DB[actKey];
let isBossFight = false;
if (bossDef && eid === getBossEnemyId(G.act)) {
isBossFight = true;
const bossEnemy = {
id: eid, name: bossDef.name, hp: bossDef.hp, maxHp: bossDef.maxHp,
block: 0, intentIdx: 0, intents: [...bossDef.phase1Intents], alive: true,
buffs: {}, debuffs: {}, strBuff: 0, isBoss: true, phase2: false,
idx: G.enemies.length
};
G.enemies.push(bossEnemy);
} else {
// Regular enemy from ENEMY_DB
const template = ENEMY_DB[eid];
if (!template) return;
const enemy = {
id: eid, name: template.name, hp: template.hp, maxHp: template.maxHp,
block: 0, intentIdx: 0, intents: [...template.intents], alive: true,
buffs: {}, debuffs: {}, strBuff: 0, isBoss: false, phase2: false,
idx: G.enemies.length
};
G.enemies.push(enemy);
}
});
// Apply Philosopher's Stone effect
if (G.enemiesStartWithStr) {
G.enemies.forEach(e => e.strBuff += 1);
}
// Reset player combat state
G.block = 0;
G.buffs = { strength: 0, dexterity: 0, regeneration: 0, intangible: 0 };
G.nextTurnBuffs = { block: 0, energy: 0, dex: 0 };
// Apply Shadow Cloak relic
if (G.relics.includes("shadow_cloak")) {
G.buffs.intangible += 1;
}
// Apply Cursed Amulet relic
if (G.relics.includes("cursed_amulet")) {
G.enemies.forEach(e => { e.debuffs.weak = (e.debuffs.weak || 0) + 1; });
}
// Black Blood: lose 10% max HP at start of combat
if (G.relics.includes("black_blood")) {
const loss = Math.floor(G.maxHp * 0.1);
G.hp -= loss;
addLog(`Black Blood: Lose ${loss} HP.`);
}
// Demon Horns: Hellforged only, add Imp Claw at start of combat (once per combat)
if (G.relics.includes("demon_horns") && G.playerClass === "hellforged" && !G._demonHornsUsed) {
const imp = createCardInstance("imp_claw");
G.hand.push(imp);
addLog("Demon Horns: Added an Imp Claw to your hand.");
}
// Setup deck for combat - separate powers from regular cards
const powerCards = G.deck.filter(c => c.type === "power");
const regularCards = G.deck.filter(c => c.type !== "power");
G.drawPile = shuffleArray(regularCards);
G.discardPile = [];
G.exhaustPile = [];
G.hand = [];
showScreen("combat");
startPlayerTurn();
}
// ── function: getEnemyIdsForFloor ──
function getEnemyIdsForFloor(node, act) {
// Multi-enemy compositions based on floor
const pool = ACT_ENEMY_POOLS["act" + act];
if (node.enemyId) {
return [node.enemyId];
}
// Default: single random enemy
return [pool[Math.floor(Math.random() * pool.length)]];
}
// ── const: _origEndCombat ──
// Override endCombat to handle elite flag properly
const _origEndCombat = endCombat;
// ── function: endCombat ──
function endCombat(victory) {
G.combatActive = false;
if (victory) {
addLog("Combat Victory!");
// Ashen Armor: heal 6 HP at end of combat
if (G.relics.includes("ashen_armor")) {
G.hp = Math.min(G.maxHp, G.hp + 6);
addLog("Ashen Armor: Healed 6 HP.");
}
// Check for boss victory
const wasBoss = G.enemies.some(e => e.isBoss);
if (wasBoss) {
// Full heal after boss
G.hp = G.maxHp;
if (G.act >= 3 && G.floor >= 15) {
showScreen("victory");
document.getElementById("victory-stats").textContent = `Class: ${CLASS_DATA[G.playerClass].name} | Gold: ${G.gold}`;
return;
}
// Advance to next act
G.act++;
if (G.act > 3) {
showScreen("victory");
document.getElementById("victory-stats").textContent = `Class: ${CLASS_DATA[G.playerClass].name} | Gold: ${G.gold}`;
return;
}
// Reset floor for new act
G.floor = 2;
// Boss rewards
generateBossRewards();
} else if (G._isEliteFight) {
// Elite rewards - always drops a relic
const goldAmt = 25 + Math.floor(Math.random() * 11);
let finalGold = goldAmt;
if (G.relics.includes("greedy_imp")) finalGold += 10;
if (!G.noMoreGold) G.gold += finalGold;
// Card reward with increased rare chance
const cards = generateCardRewards(true, false);
// Potion chance: 70%
if (Math.random() < 0.7 && G.potions.length < G.potionSlots) {
const potion = POTION_DB[Math.floor(Math.random() * POTION_DB.length)];
G.potions.push(potion.id);
}
// Relic drop - random from pool
const availableRelics = Object.keys(RELIC_DB).filter(r => !G.relics.includes(r));
if (availableRelics.length > 0) {
const relicId = availableRelics[Math.floor(Math.random() * availableRelics.length)];
G.relics.push(relicId);
addLog(`Obtained relic: ${RELIC_DB[relicId].name}`);
// Vial of Shadows special handling
if (relicId === "vial_of_shadows") {
showRemoveCardScreen(() => {
G.pendingReward = { cards, goldAmt };
renderRewardScreen(cards, finalGold);
showScreen("reward");
});
return;
}
}
// Show reward screen
G.pendingReward = { cards, goldAmt: finalGold };
renderRewardScreen(cards, finalGold);
showScreen("reward");
} else {
// Normal combat rewards
generateCombatRewards(false);
}
G._isEliteFight = false;
} else {
addLog("You have fallen...");
showScreen("gameover");
document.getElementById("gameover-stats").textContent = `Class: ${CLASS_DATA[G.playerClass].name} | Act ${G.act}, Floor ${G.floor}`;
}
}
// ── const: _origRenderMap ──
// Fix renderMap to draw connections properly
const _origRenderMap = renderMap;
// ── function: enterNode ──
function enterNode(idx) {
const node = G.mapNodes[idx];
if (!node || !isNodeReachable(idx)) return;
G.visitedNodes.add(G.currentNodeIdx);
G.currentNodeIdx = idx;
G.floor = node.floor;
switch (node.type) {
case "enemy": {
// Normalize enemies to array of strings
let enemyIds = [];
if (Array.isArray(node.enemies)) {
enemyIds = node.enemies.filter(e => typeof e === 'string');
} else if (typeof node.enemies === 'string') {
enemyIds = [node.enemies];
}
if (enemyIds.length === 0) enemyIds = [getRandomEnemyForAct(G.act)];
startCombat(enemyIds);
break;
}
case "elite":
G._isEliteFight = true;
let eliteIds = [];
if (Array.isArray(node.enemies)) {
eliteIds = node.enemies.filter(e => typeof e === 'string');
} else if (typeof node.enemies === 'string') {
eliteIds = [node.enemies];
}
if (eliteIds.length === 0) eliteIds = [getRandomEliteForAct(G.act)];
startCombat(eliteIds);
break;
case "boss": {
const bossId = getBossEnemyId(G.act);
startCombat([bossId]);
break;
}
case "rest":
showRestScreen();
break;
case "shop":
showShopScreen();
break;
case "event":
showEventScreen();
break;
}
}
// ── function: generateMap ──
function generateMap() {
G.mapNodes = [];
G.visitedNodes = new Set();
G.currentNodeIdx = -1;
const actPool = ACT_ENEMY_POOLS["act" + G.act];
const elitePool = ACT_ELITE_POOLS["act" + G.act];
function randEnemy() { return actPool[Math.floor(Math.random() * actPool.length)]; }
function randElite() { return elitePool[Math.floor(Math.random() * elitePool.length)]; }
// Floor definitions: each specifies the node type and enemy composition
const floors = [
{ floor: 2, type: "enemy", leftEnemies: [randEnemy()], rightEnemies: [randEnemy()] },
{ floor: 3, type: "enemy", enemies: [randEnemy()] },
{ floor: 4, type: "event" },
{ floor: 5, type: "shop" },
{ floor: 6, type: "enemy", enemies: [randEnemy()] },
{ floor: 7, type: "rest" },
{ floor: 8, type: "enemy", leftEnemies: [actPool[0], actPool[1 % actPool.length]], rightEnemies: [randEnemy()] },
{ floor: 9, type: "elite", leftEnemies: [elitePool[0]], rightEnemies: [randEnemy()] },
{ floor: 10, type: "event" },
{ floor: 11, type: "enemy", enemies: [randEnemy(), randEnemy()] },
{ floor: 12, type: "elite", leftEnemies: [elitePool[1 % elitePool.length]], rightEnemies: [randEnemy(), randEnemy()] },
{ floor: 13, type: "enemy", enemies: [randEnemy(), randEnemy()] },
{ floor: 14, type: "rest" },
{ floor: 15, type: "boss" }
];
let currentCol = 1;
floors.forEach((floorDef) => {
const isBranch = !!floorDef.leftEnemies;
if (isBranch) {
// Two options at this floor - left is the harder/elite option, right is easier
const leftCol = Math.max(0, currentCol - 1);
const rightCol = Math.min(3, currentCol + 1);
G.mapNodes.push({
floor: floorDef.floor, type: floorDef.type, col: leftCol,
enemies: floorDef.leftEnemies || [floorDef.type === "elite" ? randElite() : randEnemy()]
});
G.mapNodes.push({
floor: floorDef.floor, type: "enemy", col: rightCol, // Right branch is always enemy (easier alternative)
enemies: floorDef.rightEnemies || [randEnemy()]
});
} else {
if (floorDef.floor === 15) currentCol = 1;
G.mapNodes.push({
floor: floorDef.floor, type: floorDef.type, col: currentCol,
enemies: floorDef.enemies || null
});
}
});
}
// ── function: renderPlayerStatus ──
function renderPlayerStatus() {
// Render relic display
const relicEl = document.getElementById("relic-display");
if (relicEl) {
relicEl.innerHTML = "";
G.relics.forEach(relicId => {
const relicDef = RELIC_DB[relicId] || BOSS_RELIC_DB[relicId];
if (!relicDef) return;
const icon = document.createElement("div");
icon.className = "relic-icon";
icon.textContent = "🏆";
icon.title = `${relicDef.name}: ${relicDef.desc}`;
relicEl.appendChild(icon);
});
}
// Render buff/debuff display
const buffEl = document.getElementById("buff-display");
if (buffEl) {
buffEl.innerHTML = "";
// Player buffs
if (G.buffs.strength > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-buff";
badge.textContent = `Str +${G.buffs.strength}`;
buffEl.appendChild(badge);
}
if (G.buffs.dexterity > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-buff";
badge.textContent = `Dex +${G.buffs.dexterity}`;
buffEl.appendChild(badge);
}
if (G.buffs.regeneration > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-buff";
badge.textContent = `Regen ${G.buffs.regeneration}`;
buffEl.appendChild(badge);
}
if (G.buffs.intangible > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-buff";
badge.textContent = `Intangible ${G.buffs.intangible}`;
buffEl.appendChild(badge);
}
// Player debuffs
if (G.debuffs && G.debuffs.weak > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-debuff";
badge.textContent = `Weak ${G.debuffs.weak}`;
buffEl.appendChild(badge);
}
if (G.debuffs && G.debuffs.vulnerable > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-debuff";
badge.textContent = `Vuln ${G.debuffs.vulnerable}`;
buffEl.appendChild(badge);
}
if (G.debuffs && G.debuffs.bleed > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-debuff";
badge.textContent = `Bleed ${G.debuffs.bleed}`;
buffEl.appendChild(badge);
}
if (G.debuffs && G.debuffs.burn > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-debuff";
badge.textContent = `Burn ${G.debuffs.burn}`;
buffEl.appendChild(badge);
}
if (G.debuffs && G.debuffs.poison > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-debuff";
badge.textContent = `Poison ${G.debuffs.poison}`;
buffEl.appendChild(badge);
}
// Next turn buffs
if (G.nextTurnBuffs.block > 0) {
const badge = document.createElement("div");
badge.className = "status-badge badge-buff";
badge.textContent = `Next Block ${G.nextTurnBuffs.block}`;
buffEl.appendChild(badge);
}
}
}
// ── const: _origRenderCombat ──
// Update renderCombat to call this
const _origRenderCombat = renderCombat;
// ── function: renderCombat ──
function renderCombat() {
// HP bar
const hpPct = Math.max(0, (G.hp / G.maxHp) * 100);
document.getElementById("hp-text").innerHTML = `<span style="color:#ff6b6b;">${G.hp}/${G.maxHp}</span>`;
// Block
document.getElementById("player-block-display").textContent = G.block > 0 ? `🛡 ${G.block}` : "";
// Energy
const energyEl = document.getElementById("player-energy-display");
let gemsHtml = "";
for (let i = 0; i < G.energy; i++) {
gemsHtml += `<span class="energy-gem"></span>`;
}
const totalEnergy = G.maxEnergy + G.tempEnergyBonus +
(G.relics.includes("cursed_crown") ? 1 : 0) +
(G.relics.includes("ectoplasm") ? 1 : 0) +
(G.relics.includes("philosophers_stone") ? 1 : 0);
for (let i = G.energy; i < totalEnergy; i++) {
gemsHtml += `<span class="energy-gem used"></span>`;
}
energyEl.innerHTML = gemsHtml;
// Gold
document.getElementById("player-gold-display").textContent = `💰 ${G.gold}`;
// Enemies
const enemyZone = document.getElementById("enemy-zone");
enemyZone.innerHTML = "";
G.enemies.forEach((enemy, idx) => {
if (!enemy.alive) return;
const hpPct = Math.max(0, (enemy.hp / enemy.maxHp) * 100);
const el = document.createElement("div");
el.className = `enemy-card${G.selectedEnemyIdx === idx ? ' selected' : ''}`;
let statusBadges = "";
if (enemy.debuffs.weak > 0) statusBadges += `<span class="status-badge badge-debuff">Weak ${enemy.debuffs.weak}</span>`;
if (enemy.debuffs.vulnerable > 0) statusBadges += `<span class="status-badge badge-debuff">Vuln ${enemy.debuffs.vulnerable}</span>`;
if (enemy.debuffs.bleed > 0) statusBadges += `<span class="status-badge badge-debuff">Bleed ${enemy.debuffs.bleed}</span>`;
if (enemy.debuffs.burn > 0) statusBadges += `<span class="status-badge badge-debuff">Burn ${enemy.debuffs.burn}</span>`;
if (enemy.strBuff > 0) statusBadges += `<span class="status-badge badge-buff">Str +${enemy.strBuff}</span>`;
el.innerHTML = `
<div class="enemy-name">${enemy.name}${enemy.isBoss ? ' 👑' : ''}</div>
<div class="enemy-hp-bar-bg"><div class="enemy-hp-fill" style="width:${hpPct}%"></div></div>
<div class="enemy-hp-text">${enemy.hp}/${enemy.maxHp}</div>
${enemy.block > 0 ? `<div class="enemy-block-text">🛡 ${enemy.block}</div>` : ''}
<div style="margin-top:3px;">${statusBadges}</div>
`;
el.addEventListener('click', () => {
G.selectedEnemyIdx = idx;
renderCombat();
});
enemyZone.appendChild(el);
});
// Intent zone
const intentZone = document.getElementById("intent-zone");
intentZone.innerHTML = "";
G.enemies.forEach((enemy) => {
if (!enemy.alive) return;
let intents = [];
if (enemy.isBoss) {
const bossDef = getBossDefinition();
intents = enemy.phase2 ? bossDef.phase2Intents : bossDef.phase1Intents;
} else {
intents = enemy.intents;
}
const intentIdx = enemy.intentIdx % intents.length;
const intent = intents[intentIdx];
let intentText = "";
let intentClass = "attack";
switch (intent.type) {
case "attack": intentText = `⚔ Attack ${intent.dmg + (enemy.strBuff||0)}`; intentClass = "attack"; break;
case "multiHit": {
const totalDmg = intent.hits.reduce((s,h) => s + h.dmg + (enemy.strBuff||0), 0);
intentText = `⚔ ${intent.hits.length}×${intent.hits[0].dmg + (enemy.strBuff||0)} (${totalDmg})`; intentClass = "attack"; break;
}
case "defend": intentText = `🛡 Defend ${intent.block}`; intentClass = "defend"; break;
case "attackDefend": intentText = `⚔${intent.dmg + (enemy.strBuff||0)} 🛡${intent.block}`; intentClass = "attack"; break;
case "debuff": {
if (intent.weak) { intentText = `💜 Weak ${intent.weak}`; intentClass = "debuff"; }
else if (intent.bleed) { intentText = `🩸 Bleed ${intent.bleed}`; intentClass = "debuff"; }
else if (intent.poison) { intentText = `☠ Poison ${intent.poison}`; intentClass = "debuff"; }
} break;
case "debuffAll": intentText = `💜 Vuln+Weak`; intentClass = "debuff"; break;
case "attackDebuff": intentText = `⚔${intent.dmg + (enemy.strBuff||0)} ${intent.weak ? '💜' : ''}${intent.burn ? '🔥' : ''}`; intentClass = "attack"; break;
case "selfBuff": intentText = `✨ Buff`; intentClass = "buff"; break;
case "drainLife": intentText = `⚔${intent.dmg + (enemy.strBuff||0)} ❤+${intent.heal}`; intentClass = "attack"; break;
case "summon": intentText = `👹 Summon`; intentClass = "buff"; break;
case "defendBuff": intentText = `🛡${intent.block} ✨Str`; intentClass = "defend"; break;
case "buffAll": intentText = `✨ Str All`; intentClass = "buff"; break;
case "selfVulnDefend": intentText = `🛡${intent.block}`; intentClass = "defend"; break;
default: intentText = `⚔ Attack`; intentClass = "attack";
}
const intentEl = document.createElement("div");
intentEl.className = `intent-icon ${intentClass}`;
intentEl.textContent = `${enemy.name}: ${intentText}`;
intentZone.appendChild(intentEl);
});
// Hand
renderHand();
// Deck info
const powerCards = G.deck.filter(c => c.type === "power");
document.getElementById("draw-pile-count").textContent = `📚 Draw: ${G.drawPile.length}`;
document.getElementById("discard-pile-count").textContent = `🗑 Discard: ${G.discardPile.length}${powerCards.length > 0 ? ` | Powers: ${powerCards.length}` : ''}`;
// Potion bar
renderPotionBar();
// Player status (relics, buffs)
renderPlayerStatus();
// Combat log
const logEl = document.getElementById("combat-log");
if (logEl) {
logEl.innerHTML = G.logMessages.slice(-8).map(m => `<div>${m}</div>`).join("");
logEl.scrollTop = logEl.scrollHeight;
}
}
// ── main ──
// Initialize game
initGameState();
setupEventListeners();
// Show title screen
showScreen("title");
// Ensure potion overlay is hidden initially
const potionOverlay = document.getElementById("screen-potion-use");
if (potionOverlay) potionOverlay.style.display = "none";
// Add combat log element if not present
if (!document.getElementById("combat-log")) {
const logEl = document.createElement("div");
logEl.id = "combat-log";
document.body.appendChild(logEl);
}
// Add potion bar if not present
if (!document.getElementById("potion-bar")) {
const bar = document.createElement("div");
bar.id = "potion-bar";
document.body.appendChild(bar);
}
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"30417ba0-bcba-4ea0-8f8c-79acfd3b68f8","tokensIn":4184343,"tokensOut":65481,"tokensTotal":4249824,"turns":64,"toolCalls":63,"failedToolCalls":1,"timestamp":"2026-07-16T03:35:22.162Z"} -->