Shadows
Kimi k2.7 code · 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 122,689 bytes · SHA-256 3a3ae500b1f5
<!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: map ── */
.map-screen {
display: flex; flex-direction: column; align-items: center; gap: 10px;
padding-top: 10px;
}
.map-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
max-width: 900px;
}
.mapNode {
width: 64px; height: 64px;
border-radius: 50%;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
border: 2px solid var(--text-dim);
background: var(--panel);
cursor: pointer;
transition: transform 0.1s, box-shadow 0.15s;
font-size: 11px; text-align: center;
}
.mapNode:hover { transform: scale(1.08); }
.mapNode.current { border-color: var(--accent-gold); box-shadow: 0 0 14px rgba(201,162,39,0.5); }
.mapNode.visited { opacity: 0.3; }
.mapNode.available { border-color: var(--energy); }
.mapNode.enemy { color: #ff7777; }
.mapNode.elite { color: #ff5555; background: #2a1010; }
.mapNode.rest { color: #77ff77; }
.mapNode.shop { color: #ffdd77; }
.mapNode.event { color: #aaaaff; }
.mapNode.boss { color: #ff3333; background: #2a0505; }
.map-row {
display: flex; gap: 10px; justify-content: center;
align-items: center;
}
.floor-label {
width: 80px; color: var(--text-dim); font-size: 13px;
text-align: right;
}
.path-svg {
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: -1;
}
/* ── css: screens ── */
.class-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
max-width: 960px;
}
.class-card {
background: var(--panel);
border: 2px solid var(--text-dim);
border-radius: 12px;
padding: 24px;
cursor: pointer;
transition: transform 0.1s, border-color 0.15s;
}
.class-card:hover { transform: translateY(-4px); border-color: var(--accent-gold); }
.class-name { font-size: 24px; color: var(--accent-gold); margin-bottom: 8px; }
.class-desc { font-size: 15px; color: var(--text-dim); line-height: 1.4; }
.shop-grid, .reward-grid, .relic-grid, .potion-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
max-width: 900px;
width: 100%;
}
.shop-item, .reward-item, .relic-item, .potion-item {
background: var(--panel);
border: 1px solid var(--text-dim);
border-radius: 10px;
padding: 14px;
text-align: center;
cursor: pointer;
transition: transform 0.1s, border-color 0.15s;
}
.shop-item:hover, .reward-item:hover, .relic-item:hover, .potion-item:hover { transform: translateY(-3px); border-color: var(--accent-gold); }
.shop-price { color: var(--accent-gold); font-weight: bold; margin-top: 6px; }
.event-text {
max-width: 700px;
font-size: 18px;
background: var(--panel);
border: 1px solid var(--text-dim);
border-radius: 10px;
padding: 24px;
margin-bottom: 20px;
text-align: center;
}
.event-choices { display: flex; flex-direction: column; gap: 10px; max-width: 500px; width: 100%; }
.rest-options { display: flex; gap: 16px; }
.boss-relic-pick { display: flex; gap: 16px; justify-content: center; margin-top: 20px; }
.run-over {
text-align: center;
}
.run-over h1 { font-size: 48px; color: var(--accent); }
.victory h1 { color: var(--accent-gold); }
.fade-in { animation: fadeIn 0.3s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.shake { animation: shake 0.3s; }
@keyframes shake { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }
/* ── css: base ── */
:root {
--bg: #0d0d14;
--bg-light: #161622;
--panel: #1e1e2e;
--panel-light: #2a2a3e;
--text: #e6e6e6;
--text-dim: #9999aa;
--accent: #8b0000;
--accent-gold: #c9a227;
--hp: #cc3333;
--block: #4a90d9;
--energy: #44aa44;
--card-basic: #6b6b7c;
--card-attack: #8b2a2a;
--card-skill: #2a5a8b;
--card-power: #6b2a8b;
--card-curse: #3a3a3a;
--card-status: #4a4a4a;
--rare: #ffaa00;
--uncommon: #55aa55;
--common: #aaaaaa;
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
width: 100%; height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
#game { width: 100%; height: 100%; position: relative; }
#screens { width: 100%; height: 100%; }
.screen {
width: 100%; height: 100%;
display: flex; flex-direction: column;
align-items: center; justify-content: flex-start;
padding: 20px;
overflow-y: auto;
}
.hidden { display: none !important; }
button {
background: var(--panel-light);
color: var(--text);
border: 1px solid var(--text-dim);
padding: 10px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: background 0.15s, transform 0.05s;
}
button:hover { background: #35354a; }
button:active { transform: translateY(1px); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
.tooltip {
position: fixed;
pointer-events: none;
background: rgba(10,10,16,0.95);
border: 1px solid var(--text-dim);
border-radius: 8px;
padding: 10px 14px;
max-width: 280px;
z-index: 1000;
font-size: 14px;
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
}
.modal {
position: fixed; inset: 0;
background: rgba(0,0,0,0.75);
display: flex; align-items: center; justify-content: center;
z-index: 2000;
}
.modal-content {
background: var(--panel);
border: 1px solid var(--text-dim);
border-radius: 12px;
padding: 24px;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
min-width: 320px;
}
.title {
font-size: 42px;
text-align: center;
margin-bottom: 30px;
color: var(--accent-gold);
text-shadow: 0 2px 10px rgba(0,0,0,0.6);
}
.subtitle { font-size: 22px; color: var(--text-dim); margin-bottom: 16px; text-align: center; }
/* ── css: card ── */
.card {
width: 140px;
height: 200px;
border-radius: 10px;
background: linear-gradient(145deg, var(--panel-light), var(--panel));
border: 2px solid var(--text-dim);
display: flex;
flex-direction: column;
padding: 8px;
cursor: pointer;
user-select: none;
transition: transform 0.1s, box-shadow 0.15s;
position: relative;
box-shadow: 0 3px 10px rgba(0,0,0,0.4);
flex-shrink: 0;
}
.card:hover { transform: translateY(-8px); box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
.card.selected { border-color: var(--accent-gold); box-shadow: 0 0 14px rgba(201,162,39,0.5); }
.card.unplayable { opacity: 0.55; }
.card-cost {
position: absolute; top: 6px; left: 6px;
width: 28px; height: 28px;
background: var(--energy);
color: #000;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-weight: bold; font-size: 15px;
}
.card-name {
font-size: 15px; font-weight: bold;
margin-top: 22px;
text-align: center;
line-height: 1.1;
}
.card-type {
font-size: 11px; color: var(--text-dim);
text-align: center; text-transform: uppercase;
margin-top: 2px;
}
.card-rarity-common { border-color: var(--common); }
.card-rarity-uncommon { border-color: var(--uncommon); }
.card-rarity-rare { border-color: var(--rare); }
.card-rarity-basic { border-color: #888; }
.card-type-Attack { background: linear-gradient(145deg, #3a1a1a, #2a1010); }
.card-type-Skill { background: linear-gradient(145deg, #1a2a3a, #101a2a); }
.card-type-Power { background: linear-gradient(145deg, #2a1a3a, #1a102a); }
.card-type-Curse { background: linear-gradient(145deg, #2a2a2a, #1a1a1a); }
.card-type-Status { background: linear-gradient(145deg, #3a3a3a, #2a2a2a); }
.card-desc {
flex: 1;
font-size: 13px;
margin-top: 8px;
text-align: center;
color: var(--text);
line-height: 1.35;
overflow: hidden;
}
.card-upgrade { color: var(--accent-gold); font-weight: bold; }
.card-row {
display: flex; flex-wrap: wrap;
gap: 12px; justify-content: center;
margin: 12px 0;
}
.card-small { width: 110px; height: 156px; font-size: 12px; }
.card-small .card-name { font-size: 13px; margin-top: 18px; }
.card-small .card-desc { font-size: 11px; }
.card-pile {
width: 80px; height: 116px;
font-size: 11px;
display: flex; align-items: center; justify-content: center;
text-align: center;
color: var(--text-dim);
}
.card-token { font-style: italic; }
/* ── css: hud ── */
.hud {
display: flex; gap: 20px; flex-wrap: wrap;
justify-content: center; align-items: center;
background: var(--panel);
border-bottom: 1px solid var(--text-dim);
padding: 10px 20px;
width: 100%;
}
.hud-stat {
display: flex; align-items: center; gap: 6px;
font-size: 18px; font-weight: bold;
}
.stat-hp { color: var(--hp); }
.stat-block { color: var(--block); }
.stat-energy { color: var(--energy); }
.stat-gold { color: var(--accent-gold); }
.combat-frame {
display: flex;
flex-direction: column;
width: 100%; height: 100%;
}
.enemy-area {
flex: 1;
display: flex; align-items: center; justify-content: center;
gap: 30px;
padding: 20px;
}
.enemy {
width: 140px;
background: var(--panel);
border: 2px solid var(--text-dim);
border-radius: 12px;
padding: 12px;
text-align: center;
position: relative;
}
.enemy.active { border-color: var(--accent-gold); }
.enemy-hp-bar, .player-hp-bar {
height: 10px;
background: #333;
border-radius: 5px;
overflow: hidden;
margin: 6px 0;
}
.enemy-hp-fill, .player-hp-fill {
height: 100%;
background: var(--hp);
transition: width 0.2s;
}
.enemy-block, .player-block {
color: var(--block); font-weight: bold;
}
.enemy-intent {
font-size: 13px;
color: var(--text-dim);
margin-top: 6px;
min-height: 36px;
}
.player-zone {
display: flex; align-items: center; justify-content: center;
gap: 40px;
padding: 12px;
}
.player-avatar {
width: 180px;
background: var(--panel);
border: 2px solid var(--accent-gold);
border-radius: 12px;
padding: 14px;
text-align: center;
}
.status-list {
display: flex; flex-wrap: wrap; gap: 4px;
justify-content: center; margin-top: 6px;
}
.status-pill {
background: var(--bg-light);
border: 1px solid var(--text-dim);
border-radius: 10px;
padding: 2px 7px;
font-size: 12px;
color: var(--text);
}
.hand-zone {
display: flex; gap: 10px;
justify-content: center;
padding: 16px;
min-height: 220px;
align-items: flex-end;
flex-wrap: wrap;
}
.controls {
display: flex; gap: 12px; justify-content: center;
padding: 10px;
}
.log-box {
width: 260px; height: 140px;
background: var(--bg-light);
border: 1px solid var(--text-dim);
border-radius: 8px;
padding: 8px;
font-size: 13px;
overflow-y: auto;
}
.pile-zone {
display: flex; gap: 12px; justify-content: center;
align-items: flex-end;
}
.pile-info { text-align: center; font-size: 13px; color: var(--text-dim); }
</style>
</head>
<body>
<div id="game">
<div id="screens"></div>
<div id="tooltip" class="tooltip hidden"></div>
<div id="modal" class="modal hidden">
<div class="modal-content"></div>
</div>
</div>
<div id="modal-overlay" class="hidden"><div id="modal-content"></div></div>
<script>
'use strict';
// ── const: RNG ──
const RNG = {
int(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; },
choice(arr) { return arr[Math.floor(Math.random() * arr.length)]; },
weightedChoice(items) {
const total = items.reduce((s, i) => s + i.weight, 0);
let r = Math.random() * total;
for (const item of items) { r -= item.weight; if (r <= 0) return item.value; }
return items[items.length - 1].value;
},
shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
},
chance(p) { return Math.random() < p; }
};
// ── const: CLASSES ──
const CLASSES = {
bloodreaper: { name: "Blood Reaper", desc: "Sacrifice HP for devastating attacks and life steal." },
shadowmancer: { name: "Shadowmancer", desc: "Control the battlefield with Weak, Vulnerable, and Intangible." },
hellforged: { name: "Hellforged", desc: "Burn enemies and summon imps to swarm your foes." }
};
// ── const: CLASS_COLORS ──
const CLASS_COLORS = {
bloodreaper: "#8b0000",
shadowmancer: "#4a2a6b",
hellforged: "#8b4500"
};
// ── function: createCard ──
function createCard(id, opts) {
return {
uid: Math.random().toString(36).slice(2),
id,
name: opts.name,
type: opts.type,
cost: opts.cost,
baseCost: opts.cost,
rarity: opts.rarity || "Common",
class: opts.class || null,
upgraded: false,
ethereal: opts.ethereal || false,
exhaust: opts.exhaust || false,
unplayable: opts.unplayable || false,
target: opts.target || "enemy",
desc: opts.desc,
descFn: opts.descFn || null,
onTurnEnd: opts.onTurnEnd || null,
play: opts.play,
getDescription() {
return this.descFn ? this.descFn(this) : this.desc;
},
getCost() { return this.upgraded ? (opts.costUp ?? this.baseCost) : this.baseCost; },
clone() {
const c = createCard(this.id, opts);
c.upgraded = this.upgraded;
c.uid = Math.random().toString(36).slice(2);
return c;
}
};
}
// ── function: damageAmount ──
function damageAmount(base, attacker, defender) {
let dmg = base + (attacker.statuses.strength || 0) + (attacker.statuses.tempStrength || 0);
if (attacker.statuses.weak > 0) dmg = Math.floor(dmg * 0.75);
if (defender.statuses.vulnerable > 0) dmg = Math.floor(dmg * 1.5);
if (defender.statuses.intangible > 0) dmg = 1;
return Math.max(0, dmg);
}
// ── function: dealDamage ──
function dealDamage(ctx, source, target, amount, onHit) {
const actual = damageAmount(amount, source, target);
if (target.statuses.block > 0) {
const absorbed = Math.min(target.statuses.block, actual);
target.statuses.block -= absorbed;
const remaining = actual - absorbed;
target.hp -= remaining;
if (remaining > 0 && source === ctx.player) {
relicTrigger(ctx, "onHit", { target, damage: remaining });
}
if (remaining > 0 && onHit) onHit(remaining);
} else {
target.hp -= actual;
if (actual > 0 && source === ctx.player) relicTrigger(ctx, "onHit", { target, damage: actual });
if (actual > 0 && onHit) onHit(actual);
}
if (target.hp < 0) target.hp = 0;
}
// ── function: dealDamageAll ──
function dealDamageAll(ctx, source, amount, onHit) {
for (const enemy of ctx.enemies) {
if (enemy.hp > 0) dealDamage(ctx, source, enemy, amount, onHit);
}
}
// ── function: loseHp ──
function loseHp(ctx, target, amount) {
let actual = amount;
if (target.statuses.intangible > 0) actual = 1;
target.hp -= actual;
if (target.hp < 0) target.hp = 0;
return actual;
}
// ── function: gainBlock ──
function gainBlock(target, amount, ctx) {
let block = amount;
if (target === ctx.player) block += target.statuses.dexterity || 0;
target.statuses.block = (target.statuses.block || 0) + Math.max(0, block);
}
// ── function: applyStatus ──
function applyStatus(target, name, amount) {
target.statuses[name] = (target.statuses[name] || 0) + amount;
}
// ── function: drawCards ──
function drawCards(ctx, count) {
for (let i = 0; i < count; i++) {
if (ctx.drawPile.length === 0) {
ctx.drawPile = RNG.shuffle(ctx.discardPile);
ctx.discardPile = [];
}
if (ctx.drawPile.length > 0) {
const card = ctx.drawPile.pop();
ctx.hand.push(card);
}
}
}
// ── function: addCardToHand ──
function addCardToHand(ctx, card, place = "hand") {
if (place === "hand") ctx.hand.push(card);
else if (place === "drawTop") ctx.drawPile.push(card);
else if (place === "discard") ctx.discardPile.push(card);
}
// ── function: exhaustCard ──
function exhaustCard(ctx, card) {
const pile = ctx.exhaustPile || (ctx.exhaustPile = []);
pile.push(card);
}
// ── function: discardHand ──
function discardHand(ctx) {
const newHand = [];
for (const card of ctx.hand) {
if (card.ethereal || card.exhaust) {
exhaustCard(ctx, card);
if (card.onTurnEnd) card.onTurnEnd(ctx, card);
} else {
if (card.onTurnEnd) card.onTurnEnd(ctx, card);
ctx.discardPile.push(card);
}
}
ctx.hand = newHand;
}
// ── function: relicTrigger ──
function relicTrigger(ctx, event, data) {
if (!ctx.player || !ctx.player.relics) return;
for (const relic of ctx.player.relics) {
if (relic.onEvent && relic.onEvent[event]) {
relic.onEvent[event](ctx, relic, data);
}
}
}
// ── function: payHpCost ──
function payHpCost(ctx, amount, isFromCard = true) {
if (!isFromCard) return loseHp(ctx, ctx.player, amount);
const orig = amount;
const alreadyPaid = ctx.player.flags?.sacrificialDaggerUsed;
if (!alreadyPaid) {
const sd = ctx.player.relics.find(r => r.id === "sacrificial_dagger");
if (sd) {
amount = Math.max(0, amount - 1);
ctx.player.flags.sacrificialDaggerUsed = true;
}
}
const actual = loseHp(ctx, ctx.player, amount);
// Bloodstone relic
const bloodstone = ctx.player.relics.find(r => r.id === "bloodstone");
if (bloodstone && actual > 0) ctx.player.hp = Math.min(ctx.player.maxHp, ctx.player.hp + 1);
// Blood Pact power
const bp = (ctx.powers || []).find(p => p.name === "blood_pact");
if (bp && actual > 0) applyStatus(ctx.player, "strength", bp.upgraded ? 2 : 1);
return actual;
}
// ── function: createImpClaw ──
function createImpClaw(upgraded = false) {
return createCard("imp_claw", {
name: upgraded ? "Imp Claw+" : "Imp Claw",
type: "Attack",
cost: 0,
rarity: "Special",
class: "hellforged",
exhaust: true,
target: "enemy",
desc: `Deal ${upgraded ? 6 : 4} damage. Exhaust.`,
descFn(c) { return `Deal ${c.upgraded ? 6 : 4} damage. Exhaust.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, upgraded ? 6 : 4); }
});
}
// ── const: CARD_TEMPLATES ──
const CARD_TEMPLATES = {
strike: createCard("strike", {
name: "Strike", type: "Attack", cost: 1, rarity: "Basic", target: "enemy",
desc: "Deal 6 damage.",
descFn(c) { return `Deal ${c.upgraded ? 8 : 6} damage.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 8 : 6); }
}),
defend: createCard("defend", {
name: "Defend", type: "Skill", cost: 1, rarity: "Basic", target: "self",
desc: "Gain 5 Block.",
descFn(c) { return `Gain ${c.upgraded ? 8 : 5} Block.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 8 : 5, ctx); }
}),
// Blood Reaper
sanguine_strike: createCard("sanguine_strike", {
name: "Sanguine Strike", type: "Attack", cost: 1, rarity: "Common", class: "bloodreaper", target: "enemy",
desc: "Deal 8 damage. Heal 2 HP.",
descFn(c) { return `Deal ${c.upgraded ? 10 : 8} damage. Heal ${c.upgraded ? 3 : 2} HP.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 10 : 8); ctx.player.hp = Math.min(ctx.player.maxHp, ctx.player.hp + (this.upgraded ? 3 : 2)); }
}),
hemorrhage: createCard("hemorrhage", {
name: "Hemorrhage", type: "Attack", cost: 1, rarity: "Common", class: "bloodreaper", target: "enemy",
desc: "Deal 5 damage. Apply 3 Bleed.",
descFn(c) { return `Deal ${c.upgraded ? 6 : 5} damage. Apply ${c.upgraded ? 4 : 3} Bleed.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 6 : 5); applyStatus(target, "bleed", this.upgraded ? 4 : 3); }
}),
bloodletting: createCard("bloodletting", {
name: "Bloodletting", type: "Attack", cost: 1, rarity: "Uncommon", class: "bloodreaper", target: "enemy",
desc: "Lose 3 HP. Deal 12 damage.",
descFn(c) { return `Lose ${c.upgraded ? 2 : 3} HP. Deal ${c.upgraded ? 15 : 12} damage.`; },
play(ctx, target) { payHpCost(ctx, this.upgraded ? 2 : 3); dealDamage(ctx, ctx.player, target, this.upgraded ? 15 : 12); }
}),
crimson_eruption: createCard("crimson_eruption", {
name: "Crimson Eruption", type: "Attack", cost: 2, rarity: "Rare", class: "bloodreaper", target: "all",
desc: "Deal 8 damage to ALL enemies. Lose 2 HP.",
descFn(c) { return `Deal ${c.upgraded ? 10 : 8} damage to ALL enemies. Lose 2 HP.`; },
play(ctx) { payHpCost(ctx, 2); dealDamageAll(ctx, ctx.player, this.upgraded ? 10 : 8); }
}),
crimson_shield: createCard("crimson_shield", {
name: "Crimson Shield", type: "Skill", cost: 1, rarity: "Common", class: "bloodreaper", target: "self",
desc: "Gain 6 Block. Lose 2 HP.",
descFn(c) { return `Gain ${c.upgraded ? 8 : 6} Block. Lose 2 HP.`; },
play(ctx) { payHpCost(ctx, 2); gainBlock(ctx.player, this.upgraded ? 8 : 6, ctx); }
}),
blood_ward: createCard("blood_ward", {
name: "Blood Ward", type: "Skill", cost: 1, rarity: "Common", class: "bloodreaper", target: "self",
desc: "Gain 3 Block. Gain 1 Regeneration.",
descFn(c) { return `Gain ${c.upgraded ? 4 : 3} Block. Gain ${c.upgraded ? 2 : 1} Regeneration.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 4 : 3, ctx); applyStatus(ctx.player, "regeneration", this.upgraded ? 2 : 1); }
}),
sacrificial_barrier: createCard("sacrificial_barrier", {
name: "Sacrificial Barrier", type: "Skill", cost: 2, rarity: "Uncommon", class: "bloodreaper", target: "self",
desc: "Gain 10 Block. Lose 5 HP.",
descFn(c) { return `Gain ${c.upgraded ? 13 : 10} Block. Lose 5 HP.`; },
play(ctx) { payHpCost(ctx, 5); gainBlock(ctx.player, this.upgraded ? 13 : 10, ctx); }
}),
vital_surge: createCard("vital_surge", {
name: "Vital Surge", type: "Skill", cost: "X", rarity: "Rare", class: "bloodreaper", exhaust: true, target: "self",
desc: "Gain Block equal to your missing HP. Exhaust.",
descFn(c) { return `Gain Block equal to your missing HP${c.upgraded ? " + 3" : ""}. Exhaust.`; },
play(ctx) { gainBlock(ctx.player, (ctx.player.maxHp - ctx.player.hp) + (this.upgraded ? 3 : 0), ctx); }
}),
life_tap: createCard("life_tap", {
name: "Life Tap", type: "Skill", cost: 0, rarity: "Common", class: "bloodreaper", target: "self",
desc: "Draw 2 cards. Lose 3 HP.",
descFn(c) { return `Draw ${c.upgraded ? 3 : 2} cards. Lose 3 HP.`; },
play(ctx) { payHpCost(ctx, 3); drawCards(ctx, this.upgraded ? 3 : 2); }
}),
rend_flesh: createCard("rend_flesh", {
name: "Rend Flesh", type: "Skill", cost: 1, rarity: "Uncommon", class: "bloodreaper", target: "all",
desc: "Apply 5 Bleed to ALL enemies. Lose 2 HP.",
descFn(c) { return `Apply ${c.upgraded ? 7 : 5} Bleed to ALL enemies. Lose 2 HP.`; },
play(ctx) { payHpCost(ctx, 2); for (const e of ctx.enemies) applyStatus(e, "bleed", this.upgraded ? 7 : 5); }
}),
soul_siphon: createCard("soul_siphon", {
name: "Soul Siphon", type: "Power", cost: 2, rarity: "Uncommon", class: "bloodreaper", target: "self",
desc: "At the start of your turn, gain 1 Energy and lose 1 HP.",
costUp: 1,
descFn(c) { return "At the start of your turn, gain 1 Energy and lose 1 HP."; },
play(ctx) { ctx.powers.push({ name: "soul_siphon", onTurnStart(c2) { c2.energy += 1; payHpCost(c2, 1); } }); }
}),
blood_pact: createCard("blood_pact", {
name: "Blood Pact", type: "Power", cost: 1, rarity: "Rare", class: "bloodreaper", target: "self",
desc: "Whenever you lose HP from a card, gain 1 Strength.",
descFn(c) { return `Whenever you lose HP from a card, gain ${c.upgraded ? 2 : 1} Strength.`; },
play(ctx) { /* passive power handled by payHpCost via finding this power as relic? We mutate card to act as power */ ctx.powers.push({ name: "blood_pact", upgraded: this.upgraded }); }
}),
// Shadowmancer
shadow_bolt: createCard("shadow_bolt", {
name: "Shadow Bolt", type: "Attack", cost: 1, rarity: "Common", class: "shadowmancer", target: "enemy",
desc: "Deal 6 damage. Apply 1 Weak.",
descFn(c) { return `Deal ${c.upgraded ? 8 : 6} damage. Apply ${c.upgraded ? 2 : 1} Weak.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 8 : 6); applyStatus(target, "weak", this.upgraded ? 2 : 1); }
}),
grasp_of_despair: createCard("grasp_of_despair", {
name: "Grasp of Despair", type: "Attack", cost: 1, rarity: "Common", class: "shadowmancer", target: "enemy",
desc: "Deal 4 damage. Apply 1 Vulnerable.",
descFn(c) { return `Deal ${c.upgraded ? 5 : 4} damage. Apply ${c.upgraded ? 2 : 1} Vulnerable.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 5 : 4); applyStatus(target, "vulnerable", this.upgraded ? 2 : 1); }
}),
nightmare_lash: createCard("nightmare_lash", {
name: "Nightmare Lash", type: "Attack", cost: 2, rarity: "Uncommon", class: "shadowmancer", target: "enemy",
desc: "Deal 7 damage. If enemy has Weak, deal 7 more damage.",
descFn(c) { const d = c.upgraded ? 9 : 7; return `Deal ${d} damage. If enemy has Weak, deal ${d} more damage.`; },
play(ctx, target) { const d = this.upgraded ? 9 : 7; dealDamage(ctx, ctx.player, target, target.statuses.weak > 0 ? d * 2 : d); }
}),
eternal_darkness: createCard("eternal_darkness", {
name: "Eternal Darkness", type: "Attack", cost: 2, rarity: "Rare", class: "shadowmancer", target: "all_random",
desc: "Deal 3 damage to a random enemy 3 times.",
descFn(c) { return `Deal ${c.upgraded ? 4 : 3} damage to a random enemy 3 times.`; },
play(ctx) { const alive = ctx.enemies.filter(e => e.hp > 0); for (let i = 0; i < 3; i++) if (alive.length) dealDamage(ctx, ctx.player, RNG.choice(alive), this.upgraded ? 4 : 3); }
}),
cloak_of_shadows: createCard("cloak_of_shadows", {
name: "Cloak of Shadows", type: "Skill", cost: 1, rarity: "Common", class: "shadowmancer", target: "self",
desc: "Gain 5 Block. Draw 1 card.",
descFn(c) { return `Gain ${c.upgraded ? 7 : 5} Block. Draw 1 card.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 7 : 5, ctx); drawCards(ctx, 1); }
}),
haunting_mist: createCard("haunting_mist", {
name: "Haunting Mist", type: "Skill", cost: 1, rarity: "Common", class: "shadowmancer", target: "self",
desc: "Gain 7 Block. Apply 1 Weak to ALL enemies.",
descFn(c) { return `Gain ${c.upgraded ? 9 : 7} Block. Apply 1 Weak to ALL enemies.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 9 : 7, ctx); for (const e of ctx.enemies) applyStatus(e, "weak", 1); }
}),
phantom_armor: createCard("phantom_armor", {
name: "Phantom Armor", type: "Skill", cost: 1, rarity: "Uncommon", class: "shadowmancer", target: "self",
desc: "Gain 8 Block. Next turn, gain 2 Dexterity.",
descFn(c) { return `Gain ${c.upgraded ? 10 : 8} Block. Next turn, gain ${c.upgraded ? 3 : 2} Dexterity.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 10 : 8, ctx); ctx.player.statuses.dexterityNextTurn = (ctx.player.statuses.dexterityNextTurn || 0) + (this.upgraded ? 3 : 2); }
}),
whispering_shroud: createCard("whispering_shroud", {
name: "Whispering Shroud", type: "Skill", cost: 1, rarity: "Rare", class: "shadowmancer", target: "self",
desc: "Gain 4 Block. Next turn, gain 4 Block.",
descFn(c) { return `Gain ${c.upgraded ? 5 : 4} Block. Next turn, gain ${c.upgraded ? 5 : 4} Block.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 5 : 4, ctx); ctx.player.statuses.blockNextTurn = (ctx.player.statuses.blockNextTurn || 0) + (this.upgraded ? 5 : 4); }
}),
shadowform: createCard("shadowform", {
name: "Shadowform", type: "Skill", cost: 2, rarity: "Uncommon", class: "shadowmancer", exhaust: true, target: "self",
desc: "Gain 2 Intangible. Exhaust.",
descFn(c) { return `Gain ${c.upgraded ? 3 : 2} Intangible. Exhaust.`; },
play(ctx) { applyStatus(ctx.player, "intangible", this.upgraded ? 3 : 2); }
}),
curse_of_misfortune: createCard("curse_of_misfortune", {
name: "Curse of Misfortune", type: "Skill", cost: 1, rarity: "Common", class: "shadowmancer", target: "enemy",
desc: "Enemy loses 3 Strength this turn. Apply 1 Vulnerable.",
descFn(c) { return `Enemy loses ${c.upgraded ? 4 : 3} Strength this turn. Apply ${c.upgraded ? 2 : 1} Vulnerable.`; },
play(ctx, target) { applyStatus(target, "tempStrength", -(this.upgraded ? 4 : 3)); applyStatus(target, "vulnerable", this.upgraded ? 2 : 1); }
}),
dark_ritual: createCard("dark_ritual", {
name: "Dark Ritual", type: "Skill", cost: 0, rarity: "Uncommon", class: "shadowmancer", target: "self",
desc: "Gain 2 Energy. Shuffle a Dazed into your draw pile.",
descFn(c) { return `Gain 2 Energy. Shuffle a Dazed into your ${c.upgraded ? "discard pile" : "draw pile"}.`; },
play(ctx) { ctx.energy += 2; addCardToHand(ctx, CARD_TEMPLATES.dazed.clone(), this.upgraded ? "discard" : "drawTop"); }
}),
umbral_mastery: createCard("umbral_mastery", {
name: "Umbral Mastery", type: "Power", cost: 2, rarity: "Rare", class: "shadowmancer", target: "self",
desc: "At the start of your turn, apply 1 Weak to ALL enemies.",
descFn(c) { return `At the start of your turn, apply ${c.upgraded ? 2 : 1} Weak to ALL enemies.`; },
play(ctx) { const amt = this.upgraded ? 2 : 1; ctx.powers.push({ name: "umbral_mastery", onTurnStart(c2) { for (const e of c2.enemies) applyStatus(e, "weak", amt); } }); }
}),
// Hellforged
firebolt: createCard("firebolt", {
name: "Firebolt", type: "Attack", cost: 1, rarity: "Common", class: "hellforged", target: "enemy",
desc: "Deal 7 damage. Apply 2 Burn.",
descFn(c) { return `Deal ${c.upgraded ? 9 : 7} damage. Apply ${c.upgraded ? 3 : 2} Burn.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 9 : 7); applyStatus(target, "burn", this.upgraded ? 3 : 2); }
}),
hellfire_blast: createCard("hellfire_blast", {
name: "Hellfire Blast", type: "Attack", cost: 2, rarity: "Uncommon", class: "hellforged", target: "all",
desc: "Deal 9 damage to ALL enemies.",
descFn(c) { return `Deal ${c.upgraded ? 12 : 9} damage to ALL enemies.`; },
play(ctx) { dealDamageAll(ctx, ctx.player, this.upgraded ? 12 : 9); }
}),
searing_strike: createCard("searing_strike", {
name: "Searing Strike", type: "Attack", cost: 1, rarity: "Common", class: "hellforged", target: "enemy",
desc: "Deal 6 damage. Gain 1 Energy next turn.",
descFn(c) { return `Deal ${c.upgraded ? 8 : 6} damage. Gain 1 Energy next turn.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 8 : 6); ctx.player.statuses.energyNextTurn = (ctx.player.statuses.energyNextTurn || 0) + 1; }
}),
immolate: createCard("immolate", {
name: "Immolate", type: "Attack", cost: 2, rarity: "Rare", class: "hellforged", exhaust: true, target: "enemy",
desc: "Deal 12 damage. Exhaust.",
descFn(c) { return `Deal ${c.upgraded ? 16 : 12} damage. Exhaust.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 16 : 12); }
}),
obsidian_skin: createCard("obsidian_skin", {
name: "Obsidian Skin", type: "Skill", cost: 1, rarity: "Common", class: "hellforged", target: "self",
desc: "Gain 7 Block.",
descFn(c) { return `Gain ${c.upgraded ? 10 : 7} Block.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 10 : 7, ctx); }
}),
flame_ward: createCard("flame_ward", {
name: "Flame Ward", type: "Skill", cost: 1, rarity: "Common", class: "hellforged", target: "self",
desc: "Gain 5 Block. Apply 2 Burn to a random enemy.",
descFn(c) { return `Gain ${c.upgraded ? 6 : 5} Block. Apply ${c.upgraded ? 3 : 2} Burn to a random enemy.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 6 : 5, ctx); const alive = ctx.enemies.filter(e => e.hp > 0); if (alive.length) applyStatus(RNG.choice(alive), "burn", this.upgraded ? 3 : 2); }
}),
demonic_resilience: createCard("demonic_resilience", {
name: "Demonic Resilience", type: "Skill", cost: 1, rarity: "Uncommon", class: "hellforged", target: "self",
desc: "Gain 4 Block. Heal 2 HP.",
descFn(c) { return `Gain ${c.upgraded ? 5 : 4} Block. Heal ${c.upgraded ? 3 : 2} HP.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 5 : 4, ctx); ctx.player.hp = Math.min(ctx.player.maxHp, ctx.player.hp + (this.upgraded ? 3 : 2)); }
}),
brimstone_shield: createCard("brimstone_shield", {
name: "Brimstone Shield", type: "Skill", cost: 1, rarity: "Rare", class: "hellforged", target: "self",
desc: "Gain 3 Block. Gain 2 Strength this turn.",
descFn(c) { return `Gain ${c.upgraded ? 4 : 3} Block. Gain ${c.upgraded ? 3 : 2} Strength this turn.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 4 : 3, ctx); ctx.player.statuses.tempStrengthThisTurn = (ctx.player.statuses.tempStrengthThisTurn || 0) + (this.upgraded ? 3 : 2); }
}),
summon_imp: createCard("summon_imp", {
name: "Summon Imp", type: "Skill", cost: 1, rarity: "Common", class: "hellforged", exhaust: true, target: "self",
desc: "Add an Imp Claw to your hand. Exhaust.",
descFn(c) { return `Add an Imp Claw${c.upgraded ? "+" : ""} to your hand. Exhaust.`; },
play(ctx) { addCardToHand(ctx, createImpClaw(this.upgraded), "hand"); }
}),
infernal_pact: createCard("infernal_pact", {
name: "Infernal Pact", type: "Skill", cost: 0, rarity: "Uncommon", class: "hellforged", target: "self",
desc: "Lose 2 HP. Gain 2 Energy. Add an Imp Claw to your hand.",
descFn(c) { return `Lose ${c.upgraded ? 1 : 2} HP. Gain 2 Energy. Add an Imp Claw${c.upgraded ? "+" : ""} to your hand.`; },
play(ctx) { payHpCost(ctx, this.upgraded ? 1 : 2); ctx.energy += 2; addCardToHand(ctx, createImpClaw(this.upgraded), "hand"); }
}),
hellfire_aura: createCard("hellfire_aura", {
name: "Hellfire Aura", type: "Power", cost: 2, rarity: "Rare", class: "hellforged", target: "self",
desc: "At the start of your turn, deal 3 damage to ALL enemies.",
descFn(c) { return `At the start of your turn, deal ${c.upgraded ? 5 : 3} damage to ALL enemies.`; },
play(ctx) { const dmg = this.upgraded ? 5 : 3; ctx.powers.push({ name: "hellfire_aura", onTurnStart(c2) { dealDamageAll(c2, c2.player, dmg); } }); }
}),
soul_burn: createCard("soul_burn", {
name: "Soul Burn", type: "Skill", cost: 1, rarity: "Uncommon", class: "hellforged", target: "enemy",
desc: "Apply 8 Burn to an enemy. Draw 1 card.",
descFn(c) { return `Apply ${c.upgraded ? 10 : 8} Burn to an enemy. Draw 1 card.`; },
play(ctx, target) { applyStatus(target, "burn", this.upgraded ? 10 : 8); drawCards(ctx, 1); }
}),
// Colorless
flash_of_insight: createCard("flash_of_insight", {
name: "Flash of Insight", type: "Skill", cost: 1, rarity: "Common", class: "colorless", target: "self",
desc: "Draw 2 cards.",
descFn(c) { return `Draw ${c.upgraded ? 3 : 2} cards.`; },
play(ctx) { drawCards(ctx, this.upgraded ? 3 : 2); }
}),
iron_will: createCard("iron_will", {
name: "Iron Will", type: "Skill", cost: 1, rarity: "Common", class: "colorless", target: "self",
desc: "Gain 8 Block.",
descFn(c) { return `Gain ${c.upgraded ? 11 : 8} Block.`; },
play(ctx) { gainBlock(ctx.player, this.upgraded ? 11 : 8, ctx); }
}),
quick_slash: createCard("quick_slash", {
name: "Quick Slash", type: "Attack", cost: 1, rarity: "Common", class: "colorless", target: "enemy",
desc: "Deal 7 damage.",
descFn(c) { return `Deal ${c.upgraded ? 9 : 7} damage.`; },
play(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 9 : 7); }
}),
potent_brew: createCard("potent_brew", {
name: "Potent Brew", type: "Skill", cost: 0, rarity: "Uncommon", class: "colorless", exhaust: true, target: "self",
desc: "Gain 1 Energy. Exhaust.",
descFn(c) { return `Gain ${c.upgraded ? 2 : 1} Energy. Exhaust.`; },
play(ctx) { ctx.energy += this.upgraded ? 2 : 1; }
}),
// Curses / Status
wound: createCard("wound", {
name: "Wound", type: "Curse", cost: -1, rarity: "Curse", unplayable: true, ethereal: true,
desc: "Unplayable. Ethereal. Exhaust."
}),
dazed: createCard("dazed", {
name: "Dazed", type: "Status", cost: -1, rarity: "Curse", unplayable: true, ethereal: true,
desc: "Unplayable. Ethereal. Exhaust."
}),
doubt: createCard("doubt", {
name: "Doubt", type: "Curse", cost: -1, rarity: "Curse", unplayable: true,
desc: "Unplayable. At the end of your turn, lose 1 HP.",
onTurnEnd(ctx, card) { if (ctx.hand.includes(card)) loseHp(ctx, ctx.player, 1); }
})
};
// ── function: applyStatusWrapped ──
function applyStatusWrapped(ctx, target, name, amount) {
if (name === "burn" && target.hp !== undefined && amount > 0) {
const se = ctx.player.relics.find(r => r.id === "soul_ember");
if (se) dealDamage(ctx, ctx.player, target, 2);
}
applyStatus(target, name, amount);
}
// ── function: patchBurnCards ──
function patchBurnCards() {
CARD_TEMPLATES.firebolt.play = function(ctx, target) { dealDamage(ctx, ctx.player, target, this.upgraded ? 9 : 7); applyStatusWrapped(ctx, target, "burn", this.upgraded ? 3 : 2); };
CARD_TEMPLATES.flame_ward.play = function(ctx) {
gainBlock(ctx.player, this.upgraded ? 6 : 5, ctx);
const alive = ctx.enemies.filter(e => e.hp > 0);
if (alive.length) applyStatusWrapped(ctx, RNG.choice(alive), "burn", this.upgraded ? 3 : 2);
};
CARD_TEMPLATES.soul_burn.play = function(ctx, target) { applyStatusWrapped(ctx, target, "burn", this.upgraded ? 10 : 8); drawCards(ctx, 1); };
}
// ── function: createEnemy ──
function createEnemy(type, opts) {
const e = {
uid: Math.random().toString(36).slice(2),
type,
name: opts.name,
maxHp: opts.hp,
hp: opts.hp,
block: 0,
statuses: { strength: 0, dexterity: 0, regeneration: 0, intangible: 0, weak: 0, vulnerable: 0, bleed: 0, burn: 0, poison: 0, tempStrength: 0 },
intentIndex: 0,
intents: opts.intents,
phase: 1,
threshold: opts.threshold || 0,
phase2Intents: opts.phase2Intents || null,
enragePerTurn: opts.enragePerTurn || 0,
isMinion: opts.isMinion || false,
summonType: opts.summonType || null,
getIntent() {
const list = (this.phase === 2 && this.phase2Intents) ? this.phase2Intents : this.intents;
return list[this.intentIndex % list.length];
}
};
return e;
}
// ── function: nextIntent ──
function nextIntent(enemy) {
enemy.intentIndex++;
}
// ── function: intentText ──
function intentText(intent) {
if (!intent) return "";
if (intent.type === "attack") return `Attack ${intent.value}`;
if (intent.type === "multi") return `${intent.count}x${intent.value}`;
if (intent.type === "defend") return `Defend ${intent.value}`;
if (intent.type === "buff") return `Buff: ${intent.buff} ${intent.value}`;
if (intent.type === "debuff") return `Debuff: ${intent.debuff} ${intent.value}`;
if (intent.type === "summon") return `Summon ${intent.summonName}`;
return "";
}
// ── function: executeIntent ──
function executeIntent(ctx, enemy, intent) {
if (enemy.hp <= 0) return;
if (intent.type === "attack") {
dealDamage(ctx, enemy, ctx.player, intent.value);
} else if (intent.type === "multi") {
for (let i = 0; i < intent.count; i++) {
if (ctx.player.hp <= 0) break;
dealDamage(ctx, enemy, ctx.player, intent.value);
}
} else if (intent.type === "defend") {
enemy.block += intent.value;
} else if (intent.type === "buff") {
if (intent.buff === "strength") applyStatus(enemy, "strength", intent.value);
else if (intent.buff === "intangible") applyStatus(enemy, "intangible", intent.value);
else if (intent.buff === "block") enemy.block += intent.value;
else if (intent.buff === "regeneration") applyStatus(enemy, "regeneration", intent.value);
else if (intent.buff === "all_strength") for (const e of ctx.enemies) if (e.hp > 0) applyStatus(e, "strength", intent.value);
else if (intent.buff === "ally_block_regen") {
const allies = ctx.enemies.filter(e => e.hp > 0 && e !== enemy);
const target = allies.length ? RNG.choice(allies) : enemy;
target.block += intent.value;
applyStatus(target, "regeneration", 2);
}
} else if (intent.type === "debuff") {
const debuffTarget = intent.self ? enemy : ctx.player;
if (intent.debuff === "weak") applyStatus(debuffTarget, "weak", intent.value);
else if (intent.debuff === "vulnerable") applyStatus(debuffTarget, "vulnerable", intent.value);
else if (intent.debuff === "bleed") applyStatus(debuffTarget, "bleed", intent.value);
else if (intent.debuff === "burn") applyStatus(debuffTarget, "burn", intent.value);
else if (intent.debuff === "poison") applyStatus(debuffTarget, "poison", intent.value);
} else if (intent.type === "mixed") {
if (intent.attack) dealDamage(ctx, enemy, ctx.player, intent.attack);
if (intent.block) enemy.block += intent.block;
if (intent.heal) enemy.hp = Math.min(enemy.maxHp, enemy.hp + intent.heal);
if (intent.debuff) {
const debuffTarget = intent.debuffSelf ? enemy : ctx.player;
if (intent.debuff === "weak") applyStatus(debuffTarget, "weak", intent.debuffVal || 1);
if (intent.debuff === "vulnerable") applyStatus(debuffTarget, "vulnerable", intent.debuffVal || 1);
if (intent.debuff === "burn") applyStatus(debuffTarget, "burn", intent.debuffVal || 1);
}
if (intent.debuff2) {
const debuffTarget = intent.debuff2Self ? enemy : ctx.player;
if (intent.debuff2 === "weak") applyStatus(debuffTarget, "weak", intent.debuff2Val || 1);
if (intent.debuff2 === "vulnerable") applyStatus(debuffTarget, "vulnerable", intent.debuff2Val || 1);
}
if (intent.buff) {
if (intent.buff === "strength") applyStatus(enemy, "strength", intent.buffVal || 1);
if (intent.buff === "regeneration") applyStatus(enemy, "regeneration", intent.buffVal || 1);
}
} else if (intent.type === "summon") {
const count = ctx.enemies.filter(e => e.hp > 0 && e.isMinion).length;
const max = intent.max || 2;
if (count < max) {
const minion = ENEMY_TEMPLATES[intent.summonType]();
minion.isMinion = true;
ctx.enemies.push(minion);
ctx.log && ctx.log(`Summoned ${minion.name}`);
}
}
}
// ── const: ENEMY_TEMPLATES ──
const ENEMY_TEMPLATES = {
// Act 1
skeleton_warrior: () => createEnemy("skeleton_warrior", { name: "Skeleton Warrior", hp: 22, intents: [{ type: "attack", value: 6 }, { type: "attack", value: 6 }, { type: "defend", value: 8 }] }),
zombie_brute: () => createEnemy("zombie_brute", { name: "Zombie Brute", hp: 28, intents: [{ type: "attack", value: 8 }, { type: "debuff", debuff: "weak", value: 2 }, { type: "attack", value: 8 }] }),
dark_cultist: () => createEnemy("dark_cultist", { name: "Dark Cultist", hp: 20, intents: [{ type: "buff", buff: "strength", value: 3 }, { type: "attack", value: 5 }, { type: "attack", value: 5 }] }),
rat_swarm: () => createEnemy("rat_swarm", { name: "Rat Swarm", hp: 15, intents: [{ type: "multi", count: 2, value: 3 }, { type: "multi", count: 2, value: 3 }] }),
tomb_guardian: () => createEnemy("tomb_guardian", { name: "Tomb Guardian", hp: 42, intents: [{ type: "mixed", attack: 6, block: 8 }, { type: "multi", count: 3, value: 4 }, { type: "attack", value: 12 }] }),
blood_witch: () => createEnemy("blood_witch", { name: "Blood Witch", hp: 36, intents: [{ type: "mixed", attack: 5, heal: 4 }, { type: "mixed", debuff: "vulnerable", debuffVal: 1 }, { type: "attack", value: 7 }] }),
lich_king: () => createEnemy("lich_king", {
name: "Lich King", hp: 65,
intents: [{ type: "summon", summonName: "Skeleton Warrior", summonType: "skeleton_warrior", max: 2 }, { type: "attack", value: 8 }, { type: "defend", value: 12 }],
threshold: 30,
phase2Intents: [{ type: "attack", value: 10 }, { type: "attack", value: 10 }, { type: "defend", value: 15 }],
enragePerTurn: 2
}),
// Act 2
wraith: () => createEnemy("wraith", { name: "Wraith", hp: 30, intents: [{ type: "mixed", attack: 7, debuff: "weak", debuffVal: 1 }, { type: "attack", value: 7 }, { type: "defend", value: 10 }] }),
corrupted_beast: () => createEnemy("corrupted_beast", { name: "Corrupted Beast", hp: 38, intents: [{ type: "attack", value: 12 }, { type: "mixed", block: 5, debuff: "vulnerable", debuffVal: 1 }, { type: "attack", value: 12 }] }),
thorned_horror: () => createEnemy("thorned_horror", { name: "Thorned Horror", hp: 25, intents: [{ type: "multi", count: 3, value: 4 }, { type: "debuff", debuff: "bleed", value: 3 }, { type: "attack", value: 8 }] }),
witch_doctor: () => createEnemy("witch_doctor", {
name: "Witch Doctor", hp: 22,
intents: [{ type: "buff", buff: "ally_block_regen", value: 4 }, { type: "mixed", attack: 6, debuff: "poison", debuffVal: 2 }, { type: "attack", value: 6 }]
}),
treant_protector: () => createEnemy("treant_protector", { name: "Treant Protector", hp: 50, intents: [{ type: "mixed", attack: 9, block: 6 }, { type: "debuff", debuff: "vulnerable", value: 2 }, { type: "multi", count: 2, value: 10 }] }),
plague_shaman: () => createEnemy("plague_shaman", { name: "Plague Shaman", hp: 42, intents: [{ type: "debuff", debuff: "poison", value: 4 }, { type: "summon", summonName: "Plague Rat", summonType: "plague_rat", max: 2 }, { type: "attack", value: 8 }] }),
plague_rat: () => createEnemy("plague_rat", { name: "Plague Rat", hp: 18, intents: [{ type: "attack", value: 5 }, { type: "attack", value: 5 }] }),
eldritch_treant: () => createEnemy("eldritch_treant", {
name: "Eldritch Treant", hp: 75,
intents: [{ type: "mixed", attack: 10, debuff: "weak", debuffVal: 2 }, { type: "mixed", block: 15, buff: "regeneration", value: 2 }, { type: "multi", count: 2, value: 8 }],
threshold: 40,
phase2Intents: [{ type: "mixed", attack: 14, debuff: "vulnerable", debuffVal: 1 }, { type: "mixed", block: 20, heal: 5 }, { type: "mixed", attack: 14, debuff: "vulnerable", debuffVal: 1 }],
enragePerTurn: 1
}),
// Act 3
hellhound: () => createEnemy("hellhound", { name: "Hellhound", hp: 26, intents: [{ type: "mixed", attack: 8, debuff: "burn", debuffVal: 2 }, { type: "attack", value: 8 }, { type: "defend", value: 9 }] }),
shadow_demon: () => createEnemy("shadow_demon", { name: "Shadow Demon", hp: 30, intents: [{ type: "mixed", attack: 7, debuff: "weak", debuffVal: 1 }, { type: "attack", value: 7 }, { type: "buff", buff: "intangible", value: 1 }] }),
infernal_imp: () => createEnemy("infernal_imp", { name: "Infernal Imp", hp: 18, intents: [{ type: "attack", value: 5 }, { type: "attack", value: 5 }, { type: "summon", summonName: "Infernal Imp", summonType: "infernal_imp", max: 3 }] }),
abyssal_cultist: () => createEnemy("abyssal_cultist", { name: "Abyssal Cultist", hp: 24, intents: [{ type: "buff", buff: "all_strength", value: 2 }, { type: "attack", value: 7 }, { type: "attack", value: 7 }] }),
flamespawn: () => createEnemy("flamespawn", { name: "Flamespawn", hp: 48, intents: [{ type: "mixed", attack: 9, debuff: "burn", debuffVal: 3 }, { type: "mixed", block: 10, buff: "strength", value: 2 }, { type: "multi", count: 2, value: 8 }] }),
dread_knight: () => createEnemy("dread_knight", { name: "Dread Knight", hp: 55, intents: [{ type: "attack", value: 12 }, { type: "debuff", debuff: "bleed", value: 3 }, { type: "mixed", block: 12, heal: 4 }] }),
archdemon: () => createEnemy("archdemon", {
name: "Archdemon", hp: 85,
intents: [{ type: "summon", summonName: "Hellhound", summonType: "hellhound", max: 1 }, { type: "mixed", attack: 12, debuff: "burn", debuffVal: 2 }, { type: "mixed", block: 15, buff: "strength", value: 3 }],
threshold: 45,
phase2Intents: [{ type: "attack", value: 16 }, { type: "multi", count: 3, value: 7 }, { type: "attack", value: 16 }],
enragePerTurn: 2
})
};
// ── function: patchEnemyIntents ──
function patchEnemyIntents() {
ENEMY_TEMPLATES.corrupted_beast = () => createEnemy("corrupted_beast", { name: "Corrupted Beast", hp: 38, intents: [{ type: "attack", value: 12 }, { type: "mixed", block: 5, debuff: "vulnerable", debuffVal: 1, debuffSelf: true }, { type: "attack", value: 12 }] });
ENEMY_TEMPLATES.blood_witch = () => createEnemy("blood_witch", { name: "Blood Witch", hp: 36, intents: [{ type: "mixed", attack: 5, heal: 4 }, { type: "mixed", debuff: "vulnerable", debuffVal: 1, debuff2: "weak", debuff2Val: 1 }, { type: "mixed", attack: 7, heal: 4 }] });
}
// ── function: createRelic ──
function createRelic(id, opts) {
return { id, name: opts.name, desc: opts.desc, rarity: opts.rarity || "Common", class: opts.class || null, unique: opts.unique || false, used: false, onEvent: opts.onEvent || {} };
}
// ── const: RELIC_POOL ──
const RELIC_POOL = [
createRelic("bloodstone", {
name: "Bloodstone", desc: "Whenever you lose HP from a card, heal 1 HP.", rarity: "Common"
}),
createRelic("demon_horns", {
name: "Demon Horns", desc: "At the start of each combat, add a random Imp Claw to your hand.", rarity: "Uncommon", class: "hellforged",
onEvent: { combatStart(ctx) { addCardToHand(ctx, createImpClaw(Math.random() < 0.5), "hand"); } }
}),
createRelic("shadow_cloak", {
name: "Shadow Cloak", desc: "At the start of each combat, gain 1 Intangible.", rarity: "Rare",
onEvent: { combatStart(ctx) { applyStatus(ctx.player, "intangible", 1); } }
}),
createRelic("cursed_amulet", {
name: "Cursed Amulet", desc: "At the start of each combat, apply 1 Weak to ALL enemies.", rarity: "Common",
onEvent: { combatStart(ctx) { for (const e of ctx.enemies) applyStatus(e, "weak", 1); } }
}),
createRelic("tome_of_dark_rituals", {
name: "Tome of Dark Rituals", desc: "Whenever you play a Power card, draw 1 card.", rarity: "Uncommon"
}),
createRelic("bleeding_edge", {
name: "Bleeding Edge", desc: "At the start of your turn, apply 1 Bleed to a random enemy.", rarity: "Common",
onEvent: { playerTurnStart(ctx) { const alive = ctx.enemies.filter(e => e.hp > 0); if (alive.length) applyStatus(RNG.choice(alive), "bleed", 1); } }
}),
createRelic("soul_ember", {
name: "Soul Ember", desc: "Whenever you apply Burn to an enemy, deal 2 additional damage.", rarity: "Rare"
}),
createRelic("eternal_hourglass", {
name: "Eternal Hourglass", desc: "At the start of your turn, if you have no cards in hand, gain 1 Energy.", rarity: "Uncommon",
onEvent: { playerTurnStart(ctx) { if (ctx.hand.length === 0) ctx.energy += 1; } }
}),
createRelic("sacrificial_dagger", {
name: "Sacrificial Dagger", desc: "Your first Attack that costs HP each combat costs 1 less HP.", rarity: "Uncommon", class: "bloodreaper"
}),
createRelic("vial_of_shadows", {
name: "Vial of Shadows", desc: "Upon pickup, choose a card to remove from your deck.", rarity: "Rare", unique: true,
onEvent: { pickup(ctx, relic) { if (!relic.used) { relic.used = true; ctx.pendingReward = { type: "removeCard", relic: true }; } } }
}),
createRelic("ashen_armor", {
name: "Ashen Armor", desc: "At the end of combat, heal 6 HP.", rarity: "Common",
onEvent: { combatEnd(ctx) { ctx.player.hp = Math.min(ctx.player.maxHp, ctx.player.hp + 6); } }
}),
createRelic("greedy_imp", {
name: "Greedy Imp", desc: "Enemies drop 10 extra Gold.", rarity: "Common"
})
];
// ── const: BOSS_RELICS ──
const BOSS_RELICS = [
{ id: "cursed_crown", name: "Cursed Crown", desc: "Gain 1 Energy at the start of each turn. Curses can no longer be removed.", energyPerTurn: 1, cursesRemovable: false },
{ id: "ectoplasm", name: "Ectoplasm", desc: "Gain 1 Energy at the start of each turn. Gain no more Gold this run.", energyPerTurn: 1, noGold: true },
{ id: "philosophers_stone", name: "Philosopher's Stone", desc: "Gain 1 Energy at the start of each turn. All enemies start combat with 1 Strength.", energyPerTurn: 1, enemyStrength: 1 },
{ id: "black_blood", name: "Black Blood", desc: "At the start of combat, lose 10% max HP. Whenever you play an Attack, heal 1 HP.", combatHpLoss: 0.1, attackHeal: 1 },
{ id: "shadow_amulet", name: "Shadow Amulet", desc: "Start each turn with 1 Intangible. Reduce max HP by 15.", intangiblePerTurn: 1, maxHpMod: -15 }
];
// ── function: createPotion ──
function createPotion(id, opts) {
return { id, name: opts.name, desc: opts.desc, effect: opts.effect };
}
// ── const: POTION_POOL ──
const POTION_POOL = [
createPotion("blood_vial", { name: "Blood Vial", desc: "Heal 15 HP.", effect: (ctx) => { ctx.player.hp = Math.min(ctx.player.maxHp, ctx.player.hp + 15); } }),
createPotion("firebomb", { name: "Firebomb", desc: "Deal 10 damage to ALL enemies.", effect: (ctx) => { for (const e of ctx.enemies) if (e.hp > 0) dealDamage(ctx, ctx.player, e, 10); } }),
createPotion("dex_tonic", { name: "Dex Tonic", desc: "Gain 3 Dexterity for 1 turn.", effect: (ctx) => { applyStatus(ctx.player, "dexterity", 3); ctx.player.statuses.dexterityTemporal = true; } }),
createPotion("energy_elixir", { name: "Energy Elixir", desc: "Gain 2 Energy this turn.", effect: (ctx) => { ctx.energy += 2; } }),
createPotion("cursed_brew", { name: "Cursed Brew", desc: "Apply 3 Vulnerable to all enemies.", effect: (ctx) => { for (const e of ctx.enemies) if (e.hp > 0) applyStatus(e, "vulnerable", 3); } }),
createPotion("bleeding_flask", { name: "Bleeding Flask", desc: "Apply 6 Bleed to an enemy.", effect: (ctx, target) => { if (target) applyStatus(target, "bleed", 6); } }),
createPotion("imp_in_a_bottle", { name: "Imp-in-a-Bottle", desc: "Add an Imp Claw+ to your hand.", effect: (ctx) => { addCardToHand(ctx, createImpClaw(true), "hand"); } }),
createPotion("smoke_bomb", { name: "Smoke Bomb", desc: "Escape from a non-boss combat, no rewards.", effect: (ctx) => { if (!ctx.isBoss) ctx.escaped = true; } })
];
// ── function: createEvent ──
function createEvent(id, opts) {
return { id, name: opts.name, text: opts.text, choices: opts.choices };
}
// ── const: EVENT_POOL ──
const EVENT_POOL = [
createEvent("forgotten_altar", {
name: "The Forgotten Altar",
text: "A blood-stained altar hums with power.",
choices: [
{ text: "Offer 15 HP", effect: (ctx, cb) => { const actual = loseHp(ctx, ctx.player, 15); const relic = RNG.choice(usableRelics(ctx)); addRelic(ctx, relic); cb(); } },
{ text: "Pray", effect: (ctx, cb) => { ctx.player.hp = Math.min(ctx.player.maxHp, ctx.player.hp + 10); addCardToDeck(ctx, CARD_TEMPLATES.doubt.clone()); cb(); } },
{ text: "Leave", effect: (ctx, cb) => { cb(); } }
]
}),
createEvent("shadowy_peddler", {
name: "Shadowy Peddler",
text: "A hooded figure offers a trade.",
choices: [
{ text: "Trade a card", effect: (ctx, cb) => { ctx.pendingReward = { type: "removeCard", callback: () => { ctx.player.gold += 100; cb(); } }; } },
{ text: "Buy his wares (50 Gold)", effect: (ctx, cb) => { if (ctx.player.gold >= 50) { ctx.player.gold -= 50; addPotion(ctx, RNG.choice(POTION_POOL)); addRelic(ctx, RNG.choice(RELIC_POOL.filter(r => !r.class || r.class === ctx.player.className))); cb(); } else cb(); } },
{ text: "Attack", effect: (ctx, cb) => { ctx.pendingCombat = { enemies: [ENEMY_TEMPLATES.shadow_thief ? ENEMY_TEMPLATES.shadow_thief() : createShadowThief()], rewardGold: 75, onWin: cb }; } }
]
}),
createEvent("cursed_spring", {
name: "Cursed Spring",
text: "A spring bubbles with dark water.",
choices: [
{ text: "Drink", effect: (ctx, cb) => { loseHp(ctx, ctx.player, 10); ctx.player.deck = ctx.player.deck.filter(c => c.type !== "Curse"); cb(); } },
{ text: "Bottle it", effect: (ctx, cb) => { addPotion(ctx, POTION_POOL.find(p => p.id === "cursed_brew")); cb(); } },
{ text: "Leave", effect: (ctx, cb) => { cb(); } }
]
}),
createEvent("restless_dead", {
name: "Restless Dead",
text: "Ghosts offer a bargain.",
choices: [
{ text: "Accept", effect: (ctx, cb) => { loseHp(ctx, ctx.player, 20); const cards = RNG.shuffle(ctx.player.deck.filter(c => !c.upgraded)).slice(0, 2); for (const c of cards) c.upgraded = true; cb(); } },
{ text: "Refuse", effect: (ctx, cb) => { cb(); } }
]
}),
createEvent("impish_gambler", {
name: "Impish Gambler",
text: "A grinning imp challenges you.",
choices: [
{ text: "Bet 30 Gold", effect: (ctx, cb) => { if (ctx.player.gold >= 30) { ctx.player.gold -= 30; if (RNG.chance(0.5)) ctx.player.gold += 60; } cb(); } },
{ text: "Wager a card", effect: (ctx, cb) => { if (ctx.player.deck.length) { const c = RNG.choice(ctx.player.deck); ctx.player.deck = ctx.player.deck.filter(x => x !== c); if (c.type === "Curse") ctx.player.gold += 100; } cb(); } },
{ text: "Leave", effect: (ctx, cb) => { cb(); } }
]
}),
createEvent("ancient_library", {
name: "Ancient Library",
text: "Dusty tomes contain forgotten knowledge.",
choices: [
{ text: "Study", effect: (ctx, cb) => { loseHp(ctx, ctx.player, 5); ctx.player.statuses.energyNextTurnCombat = (ctx.player.statuses.energyNextTurnCombat || 0) + 1; cb(); } },
{ text: "Read forbidden page", effect: (ctx, cb) => { const pool = classCardPool(ctx.player.className); const powers = pool.filter(c => c.type === "Power"); if (powers.length) { const c = RNG.choice(powers).clone(); c.upgraded = false; addCardToDeck(ctx, c); } addCardToDeck(ctx, CARD_TEMPLATES.doubt.clone()); cb(); } },
{ text: "Leave", effect: (ctx, cb) => { cb(); } }
]
})
];
// ── function: createShadowThief ──
function createShadowThief() {
return createEnemy("shadow_thief", { name: "Shadow Thief", hp: 32, intents: [{ type: "attack", value: 8 }, { type: "debuff", debuff: "weak", value: 1 }, { type: "multi", count: 2, value: 5 }] });
}
// ── function: usableRelics ──
function usableRelics(ctx) {
return RELIC_POOL.filter(r => !r.class || r.class === ctx.player.className);
}
// ── function: addRelic ──
function addRelic(ctx, relic) {
ctx.player.relics.push(relic.clone ? relic.clone() : { ...relic });
}
// ── function: addPotion ──
function addPotion(ctx, potion) {
if (ctx.player.potions.length < 3) ctx.player.potions.push({ ...potion });
}
// ── function: addCardToDeck ──
function addCardToDeck(ctx, card) {
ctx.player.deck.push(card);
}
// ── const: ACT_DEFINITIONS ──
const ACT_DEFINITIONS = [
{
name: "The Crypts",
enemyPool: ["skeleton_warrior", "zombie_brute", "dark_cultist", "rat_swarm"],
floorEncounters: [
{ type: "enemy", groups: [["skeleton_warrior"], ["dark_cultist"]] },
{ type: "enemy", groups: [["zombie_brute"]] },
{ type: "event" },
{ type: "shop" },
{ type: "enemy", groups: [["rat_swarm"]] },
{ type: "rest" },
{ type: "enemy", groups: [["skeleton_warrior", "skeleton_warrior"], ["zombie_brute"]] },
{ type: "elite", groups: [["tomb_guardian"]], alt: [["skeleton_warrior", "rat_swarm"]] },
{ type: "event" },
{ type: "enemy", groups: [["dark_cultist", "rat_swarm"]] },
{ type: "elite", groups: [["blood_witch"]], alt: [["zombie_brute", "skeleton_warrior"]] },
{ type: "enemy", groups: [["dark_cultist", "dark_cultist"]] },
{ type: "rest" },
{ type: "boss", boss: "lich_king" }
]
},
{
name: "Blighted Woods",
enemyPool: ["wraith", "corrupted_beast", "thorned_horror", "witch_doctor"],
floorEncounters: [
{ type: "enemy", groups: [["wraith"], ["thorned_horror"]] },
{ type: "enemy", groups: [["corrupted_beast"]] },
{ type: "event" },
{ type: "shop" },
{ type: "enemy", groups: [["witch_doctor"]] },
{ type: "rest" },
{ type: "enemy", groups: [["wraith", "thorned_horror"], ["corrupted_beast"]] },
{ type: "elite", groups: [["treant_protector"]], alt: [["wraith", "witch_doctor"]] },
{ type: "event" },
{ type: "enemy", groups: [["thorned_horror", "witch_doctor"]] },
{ type: "elite", groups: [["plague_shaman"]], alt: [["corrupted_beast", "wraith"]] },
{ type: "enemy", groups: [["witch_doctor", "witch_doctor"]] },
{ type: "rest" },
{ type: "boss", boss: "eldritch_treant" }
]
},
{
name: "Abyssal Spire",
enemyPool: ["hellhound", "shadow_demon", "infernal_imp", "abyssal_cultist"],
floorEncounters: [
{ type: "enemy", groups: [["hellhound"], ["abyssal_cultist"]] },
{ type: "enemy", groups: [["shadow_demon"]] },
{ type: "event" },
{ type: "shop" },
{ type: "enemy", groups: [["infernal_imp"]] },
{ type: "rest" },
{ type: "enemy", groups: [["hellhound", "infernal_imp"], ["shadow_demon"]] },
{ type: "elite", groups: [["flamespawn"]], alt: [["hellhound", "abyssal_cultist"]] },
{ type: "event" },
{ type: "enemy", groups: [["shadow_demon", "infernal_imp"]] },
{ type: "elite", groups: [["dread_knight"]], alt: [["shadow_demon", "abyssal_cultist"]] },
{ type: "enemy", groups: [["abyssal_cultist", "abyssal_cultist"]] },
{ type: "rest" },
{ type: "boss", boss: "archdemon" }
]
}
];
// ── function: generateMap ──
function generateMap(actIndex) {
const act = ACT_DEFINITIONS[actIndex];
const rows = [];
// Start row
rows.push([{ floor: 0, col: 1, type: "start", label: "Start" }]);
for (let i = 0; i < act.floorEncounters.length; i++) {
const enc = act.floorEncounters[i];
const floor = i + 1;
const nodes = [];
if (enc.type === "enemy") {
for (let g = 0; g < enc.groups.length; g++) {
nodes.push({ floor, col: g * 2, type: "enemy", label: "Enemy", enemies: enc.groups[g] });
}
} else if (enc.type === "elite") {
nodes.push({ floor, col: 0, type: "elite", label: "Elite", enemies: enc.groups[0], alt: false });
if (enc.alt) nodes.push({ floor, col: 2, type: "enemy", label: "Enemy", enemies: enc.alt[0], altNode: true });
} else {
nodes.push({ floor, col: 1, type: enc.type, label: enc.type.charAt(0).toUpperCase() + enc.type.slice(1) });
}
rows.push(nodes);
}
// Connections
for (let r = 0; r < rows.length - 1; r++) {
for (const node of rows[r]) {
node.next = [];
const nextRow = rows[r + 1];
for (const next of nextRow) {
if (Math.abs(next.col - node.col) <= 2) node.next.push(next);
}
}
}
return rows;
}
// ── function: getAvailableNodes ──
function getAvailableNodes(mapRows, currentNode) {
if (!currentNode) return mapRows[1];
return currentNode.next || [];
}
// ── function: createPlayer ──
function createPlayer(className) {
const deck = [];
for (let i = 0; i < 5; i++) deck.push(CARD_TEMPLATES.strike.clone());
for (let i = 0; i < 4; i++) deck.push(CARD_TEMPLATES.defend.clone());
if (className === "bloodreaper") deck.push(CARD_TEMPLATES.sanguine_strike.clone());
else if (className === "shadowmancer") deck.push(CARD_TEMPLATES.shadow_bolt.clone());
else if (className === "hellforged") deck.push(CARD_TEMPLATES.firebolt.clone());
return {
className,
hp: 70, maxHp: 70,
gold: 99,
deck,
potions: [],
relics: [],
statuses: { strength: 0, dexterity: 0, regeneration: 0, intangible: 0, weak: 0, vulnerable: 0, bleed: 0, burn: 0, poison: 0, tempStrength: 0, thisTurnStrength: 0, blockNextTurn: 0, dexterityNextTurn: 0, energyNextTurn: 0, energyNextTurnCombat: 0 },
flags: {}
};
}
// ── function: classCardPool ──
function classCardPool(className) {
return Object.values(CARD_TEMPLATES).filter(c => c.class === className && !["Basic", "Curse", "Special"].includes(c.rarity));
}
// ── function: rewardCardPool ──
function rewardCardPool(className, isElite, isBoss) {
const pool = classCardPool(className);
if (isBoss) {
const rare = pool.filter(c => c.rarity === "Rare");
const others = pool.filter(c => c.rarity !== "Rare");
return [RNG.choice(rare), RNG.choice(others), RNG.choice(others)];
}
const result = [];
for (let i = 0; i < 3; i++) {
const rareChance = isElite ? 0.2 : 0.1;
const rarity = RNG.chance(rareChance) ? "Rare" : (RNG.chance(0.5) ? "Uncommon" : "Common");
const sub = pool.filter(c => c.rarity === rarity);
result.push(RNG.choice(sub.length ? sub : pool));
}
return result;
}
// ── function: generateRewardCards ──
function generateRewardCards(className, isElite, isBoss) {
const cards = rewardCardPool(className, isElite, isBoss);
return cards.map(c => c.clone());
}
// ── function: startRun ──
function startRun(className) {
state.player = createPlayer(className);
state.act = 0;
state.mapRows = generateMap(0);
state.currentNode = state.mapRows[0][0];
state.combat = null;
state.pending = null;
state.log = [];
renderMap();
}
// ── function: advanceAct ──
function advanceAct() {
state.act++;
if (state.act >= ACT_DEFINITIONS.length) {
renderVictory();
return;
}
state.player.hp = state.player.maxHp; // full heal after boss
state.mapRows = generateMap(state.act);
state.currentNode = state.mapRows[0][0];
renderMap();
}
// ── function: selectNode ──
function selectNode(node) {
const available = getAvailableNodes(state.mapRows, state.currentNode);
if (!available.includes(node)) return;
state.currentNode = node;
if (node.type === "enemy" || node.type === "elite" || node.type === "boss") {
startCombatFromNode(node);
} else if (node.type === "event") {
startEvent();
} else if (node.type === "rest") {
renderRest();
} else if (node.type === "shop") {
renderShop();
}
}
// ── function: startCombatFromNode ──
function startCombatFromNode(node) {
const enemyIds = node.enemies || [node.boss];
startCombat(enemyIds.map(id => ENEMY_TEMPLATES[id] ? id : null).filter(Boolean), node.type === "boss");
}
// ── function: startCombat ──
function startCombat(enemyIdList, isBoss) {
state.screen = "combat";
const enemies = enemyIdList.map(id => ENEMY_TEMPLATES[id]()).filter(e => e);
const bossRelic = state.player.relics.find(r => BOSS_RELICS.some(br => br.id === r.id));
state.combat = {
enemies,
hand: [],
drawPile: RNG.shuffle(state.player.deck.map(c => c.clone())),
discardPile: [],
exhaustPile: [],
powers: [],
energy: 0,
turn: 0,
isBoss,
escaped: false,
log: []
};
state.combat.player = state.player;
state.combat.flags = { sacrificialDaggerUsed: false };
state.player.statuses.block = 0;
state.player.flags.sacrificialDaggerUsed = false;
// Boss relic combat start
if (bossRelic) {
const br = BOSS_RELICS.find(x => x.id === bossRelic.id);
if (br.combatHpLoss) {
loseHp(state.combat, state.player, Math.floor(state.player.maxHp * br.combatHpLoss));
}
if (br.enemyStrength) {
for (const e of enemies) applyStatus(e, "strength", br.enemyStrength);
}
}
// Relic combat starts
for (const relic of state.player.relics) {
if (relic.onEvent && relic.onEvent.combatStart) relic.onEvent.combatStart(state.combat);
}
// Black Blood start hp loss handled above; max hp mod for Shadow Amulet handled at pickup
startPlayerTurn();
renderCombat();
}
// ── function: startPlayerTurn ──
function startPlayerTurn() {
const c = state.combat;
if (c.escaped) { endCombat(true); return; }
c.turn++;
c.log.push(`--- Turn ${c.turn} ---`);
// Reset block
state.player.statuses.block = 0;
// Decay duration statuses
decayStatuses(state.player, true);
// Start-of-turn poison/bleed for player
applyBleedDamage(state.player);
applyPoisonDamage(state.player);
// Next turn bonuses
if (state.player.statuses.blockNextTurn) { state.player.statuses.block += state.player.statuses.blockNextTurn; state.player.statuses.blockNextTurn = 0; }
if (state.player.statuses.dexterityNextTurn) { state.player.statuses.dexterity += state.player.statuses.dexterityNextTurn; state.player.statuses.dexterityNextTurn = 0; }
// Energy
let energyGain = 3;
const bossRelic = state.player.relics.find(r => BOSS_RELICS.some(br => br.id === r.id));
if (bossRelic) {
const br = BOSS_RELICS.find(x => x.id === bossRelic.id);
if (br.energyPerTurn) energyGain += br.energyPerTurn;
}
energyGain += state.player.statuses.energyNextTurn || 0;
state.player.statuses.energyNextTurn = 0;
c.energy = energyGain;
state.player.flags.sacrificialDaggerUsed = false;
// Relics and powers start of turn
for (const relic of state.player.relics) {
if (relic.onEvent && relic.onEvent.playerTurnStart) relic.onEvent.playerTurnStart(c);
}
c.powers.forEach(p => { if (p.onTurnStart) p.onTurnStart(c); });
// Draw
drawCards(c, 5);
// Dex Tonic temporary clear? It was applied as permanent dexterity; for one turn we'll subtract at end of turn
renderCombat();
}
// ── function: decayStatuses ──
function decayStatuses(entity, isPlayer) {
if (entity.statuses.weak > 0) entity.statuses.weak--;
if (entity.statuses.vulnerable > 0) entity.statuses.vulnerable--;
if (entity.statuses.poison > 0) {
// Handled separately
}
if (entity.statuses.bleed > 0) {
// Handled separately
}
if (entity.statuses.tempStrength !== 0) entity.statuses.tempStrength = 0;
if (isPlayer && entity.statuses.intangible > 0) entity.statuses.intangible--;
if (!isPlayer && entity.statuses.intangible > 0) entity.statuses.intangible--;
}
// ── function: applyBleedDamage ──
function applyBleedDamage(entity) {
if (entity.hp <= 0) return;
const bleed = entity.statuses.bleed || 0;
if (bleed > 0) {
loseHp(state.combat, entity, bleed);
state.combat.log.push(`${entity.name || "You"} took ${bleed} Bleed damage.`);
entity.statuses.bleed = Math.floor(bleed / 2);
}
}
// ── function: applyPoisonDamage ──
function applyPoisonDamage(entity) {
if (entity.hp <= 0) return;
const poison = entity.statuses.poison || 0;
if (poison > 0) {
loseHp(state.combat, entity, poison);
state.combat.log.push(`${entity.name || "You"} took ${poison} Poison damage.`);
entity.statuses.poison--;
}
}
// ── function: applyBurnDamage ──
function applyBurnDamage(entity) {
if (entity.hp <= 0) return;
const burn = entity.statuses.burn || 0;
if (burn > 0) {
loseHp(state.combat, entity, burn);
state.combat.log.push(`${entity.name || "You"} took ${burn} Burn damage.`);
entity.statuses.burn = Math.floor(burn / 2);
}
}
// ── function: applyRegeneration ──
function applyRegeneration(entity) {
const regen = entity.statuses.regeneration || 0;
if (regen > 0) {
entity.hp = Math.min(entity.maxHp, entity.hp + regen);
state.combat.log.push(`${entity.name || "You"} regenerated ${regen} HP.`);
entity.statuses.regeneration--;
}
}
// ── function: enemyTurn ──
function enemyTurn() {
const c = state.combat;
for (const enemy of c.enemies) {
if (enemy.hp <= 0) continue;
// Phase check
if (enemy.phase === 1 && enemy.threshold && enemy.hp <= enemy.threshold) {
enemy.phase = 2;
enemy.intentIndex = 0;
c.log.push(`${enemy.name} enrages!`);
}
// Enrage strength in phase 2
if (enemy.phase === 2 && enemy.enragePerTurn) {
applyStatus(enemy, "strength", enemy.enragePerTurn);
}
// Decay statuses and start-of-turn damage
decayStatuses(enemy, false);
applyBleedDamage(enemy);
applyPoisonDamage(enemy);
if (enemy.hp <= 0) continue;
// Execute intent
const intent = enemy.getIntent();
c.log.push(`${enemy.name} uses ${intentText(intent)}.`);
executeIntent(c, enemy, intent);
nextIntent(enemy);
applyBurnDamage(enemy);
applyRegeneration(enemy);
if (state.player.hp <= 0) break;
}
if (state.player.hp <= 0) { renderGameOver(); return; }
if (c.enemies.every(e => e.hp <= 0)) { endCombat(true); return; }
startPlayerTurn();
}
// ── function: playCard ──
function playCard(cardIndex, target) {
const c = state.combat;
const card = c.hand[cardIndex];
if (!card || card.unplayable) return false;
const cost = card.getCost();
if (cost !== "X" && c.energy < cost) return false;
if (cost === "X") {
card.xEnergy = c.energy;
c.energy = 0;
} else {
c.energy -= cost;
}
c.hand.splice(cardIndex, 1);
c.log.push(`Played ${card.name}${card.upgraded ? "+" : ""}.`);
// Tome of Dark Rituals
if (card.type === "Power") {
const tome = state.player.relics.find(r => r.id === "tome_of_dark_rituals");
if (tome) drawCards(c, 1);
}
// Black Blood
const bb = state.player.relics.find(r => r.id === "black_blood");
if (bb && card.type === "Attack") {
state.player.hp = Math.min(state.player.maxHp, state.player.hp + 1);
}
if (card.type === "Power") {
card.play(c, target);
c.powers.push({ name: card.id, upgraded: card.upgraded, onTurnStart: card.onTurnStart ? (ctx) => card.onTurnStart(ctx) : null });
// Move to powers zone - do not discard/exhaust
} else {
card.play(c, target);
if (card.exhaust) {
exhaustCard(c, card);
} else {
c.discardPile.push(card);
}
}
if (c.enemies.every(e => e.hp <= 0)) { endCombat(true); return true; }
renderCombat();
return true;
}
// ── function: endPlayerTurnAction ──
function endPlayerTurnAction() {
const c = state.combat;
// Burn at end of player turn
applyBurnDamage(state.player);
applyRegeneration(state.player);
// Doubt curse damage handled in discardHand via onTurnEnd
discardHand(c);
state.player.statuses.thisTurnStrength = 0;
c.selectedCardIndex = -1;
if (state.player.hp <= 0) { renderGameOver(); return; }
if (c.escaped) { endCombat(true); return; }
enemyTurn();
}
// ── function: endCombat ──
function endCombat(victory) {
const c = state.combat;
if (victory) {
// Ashen Armor
const ashen = state.player.relics.find(r => r.id === "ashen_armor");
if (ashen) state.player.hp = Math.min(state.player.maxHp, state.player.hp + 6);
if (c.isBoss) {
// Boss rewards
const gold = 50;
state.player.gold += gold + (state.player.relics.some(r => r.id === "greedy_imp") ? 10 : 0);
const relicChoices = RNG.shuffle(BOSS_RELICS).slice(0, 3).map(r => ({ ...r }));
const potions = [RNG.choice(POTION_POOL), RNG.choice(POTION_POOL)].map(p => ({ ...p }));
renderBossRewards(gold, relicChoices, potions);
} else {
const isElite = c.enemies.some(e => ["tomb_guardian", "blood_witch", "treant_protector", "plague_shaman", "flamespawn", "dread_knight"].includes(e.type));
const gold = isElite ? RNG.int(25, 35) : RNG.int(10, 20);
state.player.gold += gold + (state.player.relics.some(r => r.id === "greedy_imp") ? 10 : 0);
const relic = isElite ? RNG.choice(usableRelics({ player: state.player })) : null;
if (relic) addRelic({ player: state.player }, relic);
const potionChance = isElite ? 0.7 : 0.4;
const potion = RNG.chance(potionChance) ? RNG.choice(POTION_POOL) : null;
if (potion) addPotion({ player: state.player }, potion);
const cards = generateRewardCards(state.player.className, isElite, false);
renderCombatRewards(gold, cards, potion, relic);
}
} else {
renderGameOver();
}
state.player.statuses.block = 0;
}
// ── function: usePotion ──
function usePotion(index, target) {
const p = state.player.potions[index];
if (!p) return;
p.effect(state.combat, target);
state.player.potions.splice(index, 1);
if (state.combat.escaped && !state.combat.isBoss) { endCombat(true); return; }
renderCombat();
}
// ── const: state ──
const state = {
screen: "class_select",
player: null,
act: 0,
mapRows: [],
currentNode: null,
combat: null,
pending: null,
log: []
};
// ── function: initState ──
function initState() {
state.screen = "class_select";
state.player = null;
state.act = 0;
state.mapRows = [];
state.currentNode = null;
state.combat = null;
state.pending = null;
state.log = [];
}
// ── function: startEvent ──
function startEvent() {
const event = RNG.choice(EVENT_POOL);
state.pending = { type: "event", event };
renderEvent();
}
// ── function: pickEventChoice ──
function pickEventChoice(choiceIndex) {
const ev = state.pending.event;
const choice = ev.choices[choiceIndex];
choice.effect(state, () => {
if (state.pending && state.pending.type === "removeCard") {
// special case handled by render
} else if (state.pending && state.pending.type === "combat") {
const pc = state.pending;
startCombatFromPending(pc);
} else if (!state.pending || state.pending.type === "event") {
state.pending = null;
renderMap();
}
});
}
// ── function: startCombatFromPending ──
function startCombatFromPending(pc) {
state.pending = null;
startCombat(pc.enemies.map(e => e.type), false);
state.combat.pendingGold = pc.rewardGold;
}
// ── function: getCardBaseCost ──
function getCardBaseCost(card) {
if (card.id === "vital_surge") return "X";
return card.upgraded ? (card.costUp ?? card.baseCost) : card.baseCost;
}
// ── function: renderClassSelect ──
function renderClassSelect() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Shadows of the Abyss</h1><div class="subtitle">Choose your cursed archetype</div>`;
const grid = document.createElement("div");
grid.className = "class-grid";
for (const key of Object.keys(CLASSES)) {
const card = document.createElement("div");
card.className = "class-card";
card.style.borderColor = CLASS_COLORS[key];
card.innerHTML = `<div class="class-name">${CLASSES[key].name}</div><div class="class-desc">${CLASSES[key].desc}</div>`;
card.onclick = () => { startRun(key); };
grid.appendChild(card);
}
div.appendChild(grid);
screens.appendChild(div);
}
// ── function: formatStatuses ──
function formatStatuses(statuses) {
const arr = [];
if (statuses.strength) arr.push(`Str ${statuses.strength}`);
if (statuses.dexterity) arr.push(`Dex ${statuses.dexterity}`);
if (statuses.regeneration) arr.push(`Regen ${statuses.regeneration}`);
if (statuses.intangible) arr.push(`Intangible ${statuses.intangible}`);
if (statuses.weak) arr.push(`Weak ${statuses.weak}`);
if (statuses.vulnerable) arr.push(`Vuln ${statuses.vulnerable}`);
if (statuses.bleed) arr.push(`Bleed ${statuses.bleed}`);
if (statuses.burn) arr.push(`Burn ${statuses.burn}`);
if (statuses.poison) arr.push(`Poison ${statuses.poison}`);
if (statuses.blockNextTurn) arr.push(`Blk+ ${statuses.blockNextTurn}`);
if (statuses.dexterityNextTurn) arr.push(`Dex+ ${statuses.dexterityNextTurn}`);
if (statuses.energyNextTurn) arr.push(`En+ ${statuses.energyNextTurn}`);
return arr;
}
// ── function: statusPills ──
function statusPills(statuses) {
const s = formatStatuses(statuses);
if (!s.length) return "";
return `<div class="status-list">${s.map(x => `<span class="status-pill">${x}</span>`).join("")}</div>`;
}
// ── function: cardElement ──
function cardElement(card, opts = {}) {
const el = document.createElement("div");
el.className = `card ${card.upgraded ? "card-upgrade" : ""} card-type-${card.type} ${card.rarity ? "card-rarity-" + card.rarity.toLowerCase() : ""} ${opts.small ? "card-small" : ""} ${opts.selected ? "selected" : ""} ${opts.unplayable ? "unplayable" : ""}`;
const cost = card.getCost();
el.innerHTML = `
<div class="card-cost">${cost}</div>
<div class="card-name">${card.name}${card.upgraded ? "+" : ""}</div>
<div class="card-type">${card.type}</div>
<div class="card-desc">${card.getDescription()}</div>
`;
return el;
}
// ── function: renderMap ──
function renderMap() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen map-screen fade-in";
const act = ACT_DEFINITIONS[state.act];
div.innerHTML = `<h1 class="title">${act.name}</h1><div class="subtitle">Act ${state.act + 1} — Floor ${state.currentNode ? state.currentNode.floor + 1 : 1}</div>`;
const grid = document.createElement("div");
grid.className = "map-grid";
const available = getAvailableNodes(state.mapRows, state.currentNode);
for (let r = 0; r < state.mapRows.length; r++) {
const row = state.mapRows[r];
for (const node of row) {
const n = document.createElement("div");
n.className = `mapNode ${node.type} ${state.currentNode === node ? "current" : ""} ${available.includes(node) ? "available" : ""} ${state.currentNode !== node && node.visited ? "visited" : ""}`;
n.innerHTML = `<div>${node.label}</div><div style="font-size:9px">${node.floor + 1}</div>`;
if (available.includes(node)) {
n.onclick = () => selectNode(node);
}
grid.appendChild(n);
}
}
div.appendChild(grid);
if (state.player) {
const hud = document.createElement("div");
hud.className = "hud";
hud.style.marginTop = "20px";
hud.innerHTML = `
<div class="hud-stat stat-hp">HP: ${state.player.hp}/${state.player.maxHp}</div>
<div class="hud-stat stat-gold">Gold: ${state.player.gold}</div>
<div class="hud-stat">Deck: ${state.player.deck.length}</div>
<div class="hud-stat">Relics: ${state.player.relics.length}</div>
<div class="hud-stat">Potions: ${state.player.potions.length}/3</div>
`;
div.appendChild(hud);
}
screens.appendChild(div);
}
// ── function: renderCombat ──
function renderCombat() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const c = state.combat;
const div = document.createElement("div");
div.className = "screen combat-frame fade-in";
// HUD
const hud = document.createElement("div");
hud.className = "hud";
const deckCount = c.drawPile.length;
const discardCount = c.discardPile.length;
const exhaustCount = c.exhaustPile.length;
hud.innerHTML = `
<div class="hud-stat stat-hp">HP: ${state.player.hp}/${state.player.maxHp}</div>
<div class="hud-stat stat-block">Block: ${state.player.statuses.block}</div>
<div class="hud-stat stat-energy">Energy: ${c.energy}</div>
<div class="hud-stat stat-gold">Gold: ${state.player.gold}</div>
<div class="hud-stat">Deck: ${deckCount}</div>
<div class="hud-stat">Discard: ${discardCount}</div>
<div class="hud-stat">Turn: ${c.turn}</div>
`;
div.appendChild(hud);
// Enemy area
const enemyArea = document.createElement("div");
enemyArea.className = "enemy-area";
for (const enemy of c.enemies) {
const e = document.createElement("div");
e.className = "enemy" + (state.combat.selectedTarget === enemy.uid ? " active" : "");
const hpPct = Math.max(0, (enemy.hp / enemy.maxHp) * 100);
const intent = enemy.getIntent();
e.innerHTML = `
<div style="font-weight:bold">${enemy.name}</div>
<div class="enemy-hp-bar"><div class="enemy-hp-fill" style="width:${hpPct}%"></div></div>
<div>${enemy.hp}/${enemy.maxHp} HP</div>
<div class="enemy-block">Block: ${enemy.statuses.block}</div>
<div class="enemy-intent">Next: ${intentText(intent)}</div>
${statusPills(enemy.statuses)}
`;
e.onclick = () => selectTarget(enemy);
enemyArea.appendChild(e);
}
div.appendChild(enemyArea);
// Player zone
const playerZone = document.createElement("div");
playerZone.className = "player-zone";
const logBox = document.createElement("div");
logBox.className = "log-box";
logBox.innerHTML = c.log.slice(-10).map(x => `<div>${x}</div>`).join("");
const avatar = document.createElement("div");
avatar.className = "player-avatar";
const hpPct2 = Math.max(0, (state.player.hp / state.player.maxHp) * 100);
avatar.innerHTML = `
<div style="font-weight:bold;font-size:18px">${CLASSES[state.player.className].name}</div>
<div class="player-hp-bar"><div class="player-hp-fill" style="width:${hpPct2}%"></div></div>
<div>${state.player.hp}/${state.player.maxHp} HP</div>
<div class="player-block">Block: ${state.player.statuses.block}</div>
${statusPills(state.player.statuses)}
`;
const piles = document.createElement("div");
piles.className = "pile-zone";
piles.innerHTML = `
<div><div class="card card-pile">Draw ${deckCount}</div><div class="pile-info">Draw</div></div>
<div><div class="card card-pile">Discard ${discardCount}</div><div class="pile-info">Discard</div></div>
<div><div class="card card-pile">Exhaust ${exhaustCount}</div><div class="pile-info">Exhaust</div></div>
`;
playerZone.appendChild(logBox);
playerZone.appendChild(avatar);
playerZone.appendChild(piles);
div.appendChild(playerZone);
// Hand
const hand = document.createElement("div");
hand.className = "hand-zone";
c.hand.forEach((card, idx) => {
const cost = card.getCost();
const unplayable = card.unplayable || (cost !== "X" && c.energy < cost) || (cost === "X" && c.energy <= 0);
const el = cardElement(card, { selected: c.selectedCardIndex === idx, unplayable });
if (!unplayable) {
el.onclick = () => selectCard(idx);
}
hand.appendChild(el);
});
div.appendChild(hand);
// Controls
const controls = document.createElement("div");
controls.className = "controls";
const endBtn = document.createElement("button");
endBtn.textContent = "End Turn";
endBtn.onclick = endPlayerTurnAction;
controls.appendChild(endBtn);
// Potions
state.player.potions.forEach((potion, idx) => {
const btn = document.createElement("button");
btn.textContent = potion.name;
btn.title = potion.desc;
btn.onclick = () => usePotion(idx, c.enemies.find(e => e.hp > 0));
controls.appendChild(btn);
});
div.appendChild(controls);
screens.appendChild(div);
logBox.scrollTop = logBox.scrollHeight;
}
// ── function: selectCard ──
function selectCard(index) {
const c = state.combat;
const card = c.hand[index];
if (!card) return;
if (card.target === "enemy" || card.target === "all_random") {
// For enemy target, need selection
if (card.target === "enemy") {
if (c.selectedCardIndex === index) {
// Try play first enemy if only one
const alive = c.enemies.filter(e => e.hp > 0);
if (alive.length === 1) { playCard(index, alive[0]); return; }
}
c.selectedCardIndex = index;
c.selectedTarget = null;
renderCombat();
return;
}
}
playCard(index, null);
}
// ── function: selectTarget ──
function selectTarget(enemy) {
const c = state.combat;
if (c.selectedCardIndex >= 0) {
playCard(c.selectedCardIndex, enemy);
} else {
c.selectedTarget = enemy.uid;
renderCombat();
}
}
// ── function: renderEvent ──
function renderEvent() {
const ev = state.pending.event;
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">${ev.name}</h1><div class="event-text">${ev.text}</div>`;
const choices = document.createElement("div");
choices.className = "event-choices";
ev.choices.forEach((ch, idx) => {
const btn = document.createElement("button");
btn.textContent = ch.text;
btn.onclick = () => {
pickEventChoice(idx);
if (state.pending && state.pending.type === "removeCard") {
showCardPicker(state.player.deck, "Choose a card to remove", (card) => {
state.player.deck = state.player.deck.filter(c => c !== card);
if (state.pending.callback) state.pending.callback();
state.pending = null;
renderMap();
});
} else if (state.pending && state.pending.type === "combat") {
// combat start handled by pickEventChoice already
}
};
choices.appendChild(btn);
});
div.appendChild(choices);
screens.appendChild(div);
}
// ── function: renderRest ──
function renderRest() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Rest Site</h1><div class="subtitle">Choose one action</div>`;
const opts = document.createElement("div");
opts.className = "rest-options";
const restBtn = document.createElement("button");
restBtn.textContent = "Rest (Heal 25%)";
restBtn.onclick = () => { state.player.hp = Math.min(state.player.maxHp, state.player.hp + Math.floor(state.player.maxHp * 0.25)); renderMap(); };
const forgeBtn = document.createElement("button");
forgeBtn.textContent = "Forge (Upgrade card)";
forgeBtn.onclick = () => showCardPicker(state.player.deck, "Choose a card to upgrade", (card) => { card.upgraded = true; renderMap(); });
const medBtn = document.createElement("button");
medBtn.textContent = "Meditate (Remove card)";
medBtn.onclick = () => showCardPicker(state.player.deck, "Choose a card to remove", (card) => { state.player.deck = state.player.deck.filter(c => c !== card); renderMap(); });
opts.appendChild(restBtn);
opts.appendChild(forgeBtn);
opts.appendChild(medBtn);
div.appendChild(opts);
screens.appendChild(div);
}
// ── function: renderShop ──
function renderShop() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Shop</h1><div class="subtitle">Gold: ${state.player.gold}</div>`;
const grid = document.createElement("div");
grid.className = "shop-grid";
// Class cards
const pool = classCardPool(state.player.className);
const shopCards = [RNG.choice(pool), RNG.choice(pool), RNG.choice(pool)];
const colorless = [CARD_TEMPLATES.flash_of_insight, CARD_TEMPLATES.iron_will, CARD_TEMPLATES.quick_slash, CARD_TEMPLATES.potent_brew];
shopCards.push(RNG.choice(colorless));
for (const cardT of shopCards) {
const card = cardT.clone();
const price = card.rarity === "Rare" ? 150 : (card.rarity === "Uncommon" ? 75 : 50);
const item = document.createElement("div");
item.className = "shop-item";
item.appendChild(cardElement(card));
item.innerHTML += `<div class="shop-price">${price}g</div>`;
item.onclick = () => { if (state.player.gold >= price) { state.player.gold -= price; addCardToDeck({ player: state.player }, card.clone()); renderShop(); } };
grid.appendChild(item);
}
// Relics
for (let i = 0; i < 2; i++) {
const relic = RNG.choice(usableRelics({ player: state.player }));
const item = document.createElement("div");
item.className = "shop-item";
item.innerHTML = `<div style="font-weight:bold;color:var(--accent-gold)">${relic.name}</div><div style="font-size:13px;color:var(--text-dim)">${relic.desc}</div><div class="shop-price">150g</div>`;
item.onclick = () => { if (state.player.gold >= 150) { state.player.gold -= 150; addRelic({ player: state.player }, relic); renderShop(); } };
grid.appendChild(item);
}
// Potions
for (let i = 0; i < 3; i++) {
const potion = RNG.choice(POTION_POOL);
const item = document.createElement("div");
item.className = "shop-item";
item.innerHTML = `<div style="font-weight:bold">${potion.name}</div><div style="font-size:13px;color:var(--text-dim)">${potion.desc}</div><div class="shop-price">50g</div>`;
item.onclick = () => { if (state.player.gold >= 50 && state.player.potions.length < 3) { state.player.gold -= 50; addPotion({ player: state.player }, potion); renderShop(); } };
grid.appendChild(item);
}
// Remove card service
const removeBtn = document.createElement("button");
removeBtn.textContent = "Remove a card (75g)";
removeBtn.onclick = () => { if (state.player.gold >= 75) { state.player.gold -= 75; showCardPicker(state.player.deck, "Choose a card to remove", (card) => { state.player.deck = state.player.deck.filter(c => c !== card); renderMap(); }); } };
div.appendChild(grid);
div.appendChild(document.createElement("br"));
div.appendChild(removeBtn);
const leaveBtn = document.createElement("button");
leaveBtn.textContent = "Leave";
leaveBtn.onclick = () => renderMap();
div.appendChild(leaveBtn);
screens.appendChild(div);
}
// ── function: renderCombatRewards ──
function renderCombatRewards(gold, cards, potion, relic) {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Victory!</h1><div class="subtitle">Loot: ${gold}g</div>`;
const grid = document.createElement("div");
grid.className = "reward-grid";
for (const card of cards) {
const item = document.createElement("div");
item.className = "reward-item";
item.appendChild(cardElement(card));
item.onclick = () => { state.player.deck.push(card); renderMap(); };
grid.appendChild(item);
}
const skip = document.createElement("div");
skip.className = "reward-item";
skip.innerHTML = `<div style="padding-top:60px;font-weight:bold">Skip card</div>`;
skip.onclick = () => renderMap();
grid.appendChild(skip);
div.appendChild(grid);
if (potion) {
const pdiv = document.createElement("div");
pdiv.innerHTML = `<h3>Potion obtained: ${potion.name}</h3><p>${potion.desc}</p>`;
div.appendChild(pdiv);
}
if (relic) {
const rdiv = document.createElement("div");
rdiv.innerHTML = `<h3>Relic obtained: ${relic.name}</h3><p>${relic.desc}</p>`;
div.appendChild(rdiv);
}
const cont = document.createElement("button");
cont.textContent = "Continue";
cont.onclick = () => {
if (potion && !state.player.potions.some(p => p.name === potion.name && p.id === potion.id)) {
// auto-add potion if not shown
}
renderMap();
};
div.appendChild(cont);
screens.appendChild(div);
}
// ── function: renderBossRewards ──
function renderBossRewards(gold, relicChoices, potions) {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Boss Defeated!</h1><div class="subtitle">Choose a Boss Relic. Gold: ${gold}g</div>`;
const grid = document.createElement("div");
grid.className = "boss-relic-pick";
for (const relic of relicChoices) {
const item = document.createElement("div");
item.className = "reward-item";
item.style.maxWidth = "260px";
item.innerHTML = `<div style="font-weight:bold;color:var(--rare)">${relic.name}</div><div style="font-size:13px;color:var(--text-dim)">${relic.desc}</div>`;
item.onclick = () => {
state.player.relics.push({ id: relic.id, name: relic.name, desc: relic.desc, onEvent: {} });
if (relic.maxHpMod) { state.player.maxHp += relic.maxHpMod; state.player.hp = Math.min(state.player.maxHp, state.player.hp); }
for (const p of potions) addPotion({ player: state.player }, p);
state.player.gold += gold;
advanceAct();
};
grid.appendChild(item);
}
div.appendChild(grid);
screens.appendChild(div);
}
// ── function: renderGameOver ──
function renderGameOver() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen run-over fade-in";
div.innerHTML = `<h1>Run Over</h1><p>You fell in the ${ACT_DEFINITIONS[state.act].name}.</p>`;
const btn = document.createElement("button");
btn.textContent = "Try Again";
btn.onclick = () => { initState(); renderClassSelect(); };
div.appendChild(btn);
screens.appendChild(div);
}
// ── function: renderVictory ──
function renderVictory() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen victory fade-in";
div.innerHTML = `<h1>Victory</h1><p>You have defeated the Archdemon and conquered the Abyss.</p>`;
const btn = document.createElement("button");
btn.textContent = "New Run";
btn.onclick = () => { initState(); renderClassSelect(); };
div.appendChild(btn);
screens.appendChild(div);
}
// ── function: showModal ──
function showModal(title, contentHtml, buttons) {
const modal = document.getElementById("modal");
const mc = modal.querySelector(".modal-content");
mc.innerHTML = `<h2>${title}</h2>${contentHtml}`;
const btnRow = document.createElement("div");
btnRow.style.marginTop = "15px";
btnRow.style.display = "flex";
btnRow.style.gap = "10px";
for (const b of buttons) {
const btn = document.createElement("button");
btn.textContent = b.text;
btn.onclick = () => { hideModal(); if (b.action) b.action(); };
btnRow.appendChild(btn);
}
mc.appendChild(btnRow);
modal.classList.remove("hidden");
}
// ── function: hideModal ──
function hideModal() {
document.getElementById("modal").classList.add("hidden");
}
// ── function: showCardPicker ──
function showCardPicker(cards, title, onPick) {
const modal = document.getElementById("modal");
const mc = modal.querySelector(".modal-content");
mc.innerHTML = `<h2>${title}</h2><div class="card-row" id="picker-cards"></div>`;
const row = mc.querySelector("#picker-cards");
for (const card of cards) {
const el = cardElement(card);
el.onclick = () => { hideModal(); onPick(card); };
row.appendChild(el);
}
modal.classList.remove("hidden");
}
// ── function: patchGameLogic ──
function patchGameLogic() {
// Fix playCard reset selection
const originalPlayCard = playCard;
playCard = function(cardIndex, target) {
const res = originalPlayCard(cardIndex, target);
if (state.combat) state.combat.selectedCardIndex = -1;
return res;
};
// Fix end of player turn: clear temporal dexterity
const originalEndTurn = endPlayerTurnAction;
endPlayerTurnAction = function() {
if (state.player.statuses.dexterityTemporal) {
state.player.statuses.dexterity -= state.player.statuses.dexterityTemporal;
state.player.statuses.dexterityTemporal = 0;
}
originalEndTurn();
};
// Fix decayStatuses: don't reset tempStrength; reset after enemy action
decayStatuses = function(entity, isPlayer) {
if (entity.statuses.weak > 0) entity.statuses.weak--;
if (entity.statuses.vulnerable > 0) entity.statuses.vulnerable--;
if (isPlayer && entity.statuses.intangible > 0) entity.statuses.intangible--;
if (!isPlayer && entity.statuses.intangible > 0) entity.statuses.intangible--;
};
// Reset enemy tempStrength after action
const oldEnemyTurn = enemyTurn;
enemyTurn = function() {
oldEnemyTurn();
if (state.combat) {
for (const e of state.combat.enemies) e.statuses.tempStrength = 0;
}
};
// Fix generateMap to include boss id
const oldGenerateMap = generateMap;
generateMap = function(actIndex) {
const act = ACT_DEFINITIONS[actIndex];
const rows = [];
rows.push([{ floor: 0, col: 1, type: "start", label: "Start" }]);
for (let i = 0; i < act.floorEncounters.length; i++) {
const enc = act.floorEncounters[i];
const floor = i + 1;
const nodes = [];
if (enc.type === "enemy") {
for (let g = 0; g < enc.groups.length; g++) {
nodes.push({ floor, col: g * 2, type: "enemy", label: "Enemy", enemies: enc.groups[g] });
}
} else if (enc.type === "elite") {
nodes.push({ floor, col: 0, type: "elite", label: "Elite", enemies: enc.groups[0] });
if (enc.alt) nodes.push({ floor, col: 2, type: "enemy", label: "Enemy", enemies: enc.alt[0], altNode: true });
} else if (enc.type === "boss") {
nodes.push({ floor, col: 1, type: "boss", label: "Boss", boss: enc.boss, enemies: [enc.boss] });
} else {
nodes.push({ floor, col: 1, type: enc.type, label: enc.type.charAt(0).toUpperCase() + enc.type.slice(1) });
}
rows.push(nodes);
}
for (let r = 0; r < rows.length - 1; r++) {
for (const node of rows[r]) {
node.next = [];
const nextRow = rows[r + 1];
for (const next of nextRow) {
if (Math.abs(next.col - node.col) <= 2) node.next.push(next);
}
}
}
return rows;
};
// Patch event definitions to use state
EVENT_POOL[0].choices[0].effect = (ctx, cb) => { const actual = loseHp(state, state.player, 15); const relic = RNG.choice(usableRelics({ player: state.player })); addRelic({ player: state.player }, relic); cb(); };
EVENT_POOL[0].choices[1].effect = (ctx, cb) => { state.player.hp = Math.min(state.player.maxHp, state.player.hp + 10); addCardToDeck({ player: state.player }, CARD_TEMPLATES.doubt.clone()); cb(); };
EVENT_POOL[0].choices[2].effect = (ctx, cb) => { cb(); };
EVENT_POOL[1].choices[0].effect = (ctx, cb) => { state.pending = { type: "removeCard", callback: () => { state.player.gold += 100; cb(); } }; };
EVENT_POOL[1].choices[1].effect = (ctx, cb) => { if (state.player.gold >= 50) { state.player.gold -= 50; addPotion({ player: state.player }, RNG.choice(POTION_POOL)); addRelic({ player: state.player }, RNG.choice(usableRelics({ player: state.player }))); } cb(); };
EVENT_POOL[1].choices[2].effect = (ctx, cb) => { state.pending = { type: "combat", enemies: [createShadowThief()], rewardGold: 75, onWin: cb }; };
EVENT_POOL[2].choices[0].effect = (ctx, cb) => { loseHp(state, state.player, 10); state.player.deck = state.player.deck.filter(c => c.type !== "Curse"); cb(); };
EVENT_POOL[2].choices[1].effect = (ctx, cb) => { addPotion({ player: state.player }, POTION_POOL.find(p => p.id === "cursed_brew")); cb(); };
EVENT_POOL[2].choices[2].effect = (ctx, cb) => { cb(); };
EVENT_POOL[3].choices[0].effect = (ctx, cb) => { loseHp(state, state.player, 20); const cards = RNG.shuffle(state.player.deck.filter(c => !c.upgraded)).slice(0, 2); for (const c of cards) c.upgraded = true; cb(); };
EVENT_POOL[3].choices[1].effect = (ctx, cb) => { cb(); };
EVENT_POOL[4].choices[0].effect = (ctx, cb) => { if (state.player.gold >= 30) { state.player.gold -= 30; if (RNG.chance(0.5)) state.player.gold += 60; } cb(); };
EVENT_POOL[4].choices[1].effect = (ctx, cb) => { if (state.player.deck.length) { const c = RNG.choice(state.player.deck); state.player.deck = state.player.deck.filter(x => x !== c); if (c.type === "Curse") state.player.gold += 100; } cb(); };
EVENT_POOL[4].choices[2].effect = (ctx, cb) => { cb(); };
EVENT_POOL[5].choices[0].effect = (ctx, cb) => { loseHp(state, state.player, 5); state.player.statuses.energyNextTurnCombat = (state.player.statuses.energyNextTurnCombat || 0) + 1; cb(); };
EVENT_POOL[5].choices[1].effect = (ctx, cb) => { const pool = classCardPool(state.player.className).filter(c => c.type === "Power"); if (pool.length) { const c = RNG.choice(pool).clone(); addCardToDeck({ player: state.player }, c); } addCardToDeck({ player: state.player }, CARD_TEMPLATES.doubt.clone()); cb(); };
EVENT_POOL[5].choices[2].effect = (ctx, cb) => { cb(); };
}
// ── function: patchPowersAndRewards ──
function patchPowersAndRewards() {
// Fix Power double push
const originalPlayCard = playCard;
playCard = function(cardIndex, target) {
const c = state.combat;
const card = c.hand[cardIndex];
if (!card || card.unplayable) return false;
const cost = card.getCost();
if (cost !== "X" && c.energy < cost) return false;
if (cost === "X") { c.energy = 0; }
else { c.energy -= cost; }
c.hand.splice(cardIndex, 1);
c.log.push(`Played ${card.name}${card.upgraded ? "+" : ""}.`);
// Tome of Dark Rituals
if (card.type === "Power") {
const tome = state.player.relics.find(r => r.id === "tome_of_dark_rituals");
if (tome) drawCards(c, 1);
}
// Black Blood
const bb = state.player.relics.find(r => r.id === "black_blood");
if (bb && card.type === "Attack") {
state.player.hp = Math.min(state.player.maxHp, state.player.hp + 1);
}
card.play(c, target);
if (card.type !== "Power") {
if (card.exhaust) exhaustCard(c, card);
else c.discardPile.push(card);
}
if (c.enemies.every(e => e.hp <= 0)) { endCombat(true); return true; }
c.selectedCardIndex = -1;
renderCombat();
return true;
};
// Fix endCombat pending gold
const oldEndCombat = endCombat;
endCombat = function(victory) {
const pending = state.combat ? state.combat.pendingGold : 0;
oldEndCombat(victory);
};
// Actually override endCombat fully to handle pending
endCombat = function(victory) {
const c = state.combat;
if (victory) {
const ashen = state.player.relics.find(r => r.id === "ashen_armor");
if (ashen) state.player.hp = Math.min(state.player.maxHp, state.player.hp + 6);
if (c.isBoss) {
const gold = 50;
state.player.gold += gold + (state.player.relics.some(r => r.id === "greedy_imp") ? 10 : 0);
const relicChoices = RNG.shuffle(BOSS_RELICS).slice(0, 3).map(r => ({ ...r }));
const potions = [RNG.choice(POTION_POOL), RNG.choice(POTION_POOL)].map(p => ({ ...p }));
renderBossRewards(gold, relicChoices, potions);
} else {
const isElite = c.enemies.some(e => ["tomb_guardian", "blood_witch", "treant_protector", "plague_shaman", "flamespawn", "dread_knight"].includes(e.type));
const baseGold = c.pendingGold || (isElite ? RNG.int(25, 35) : RNG.int(10, 20));
state.player.gold += baseGold + (state.player.relics.some(r => r.id === "greedy_imp") ? 10 : 0);
const relic = isElite ? RNG.choice(usableRelics({ player: state.player })) : null;
if (relic) addRelic({ player: state.player }, relic);
const potionChance = isElite ? 0.7 : 0.4;
const potion = RNG.chance(potionChance) ? RNG.choice(POTION_POOL) : null;
if (potion) addPotion({ player: state.player }, potion);
const cards = generateRewardCards(state.player.className, isElite, false);
renderCombatRewards(baseGold, cards, potion, relic);
}
} else {
renderGameOver();
}
state.player.statuses.block = 0;
};
// Fix renderCombatRewards to auto add potion/relic
renderCombatRewards = function(gold, cards, potion, relic) {
if (potion) addPotion({ player: state.player }, potion);
if (relic) addRelic({ player: state.player }, relic);
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Victory!</h1><div class="subtitle">Loot: ${gold}g</div>`;
const grid = document.createElement("div");
grid.className = "reward-grid";
for (const card of cards) {
const item = document.createElement("div");
item.className = "reward-item";
item.appendChild(cardElement(card));
item.onclick = () => { state.player.deck.push(card); renderMap(); };
grid.appendChild(item);
}
const skip = document.createElement("div");
skip.className = "reward-item";
skip.innerHTML = `<div style="padding-top:60px;font-weight:bold">Skip card</div>`;
skip.onclick = () => renderMap();
grid.appendChild(skip);
div.appendChild(grid);
if (potion) {
const pdiv = document.createElement("div");
pdiv.innerHTML = `<h3>Potion obtained: ${potion.name}</h3>`;
div.appendChild(pdiv);
}
if (relic) {
const rdiv = document.createElement("div");
rdiv.innerHTML = `<h3>Relic obtained: ${relic.name}</h3>`;
div.appendChild(rdiv);
}
screens.appendChild(div);
};
}
// ── function: patchBossRelicsAndMisc ──
function patchBossRelicsAndMisc() {
// Shadow Amulet intangible each turn
const oldStartPlayerTurn = startPlayerTurn;
startPlayerTurn = function() {
const amulet = state.player.relics.find(r => r.id === "shadow_amulet");
oldStartPlayerTurn();
if (amulet) applyStatus(state.player, "intangible", 1);
};
// Boss reward setup flags
const oldRenderBossRewards = renderBossRewards;
renderBossRewards = function(gold, relicChoices, potions) {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Boss Defeated!</h1><div class="subtitle">Choose a Boss Relic. Gold: ${gold}g</div>`;
const grid = document.createElement("div");
grid.className = "boss-relic-pick";
for (const relic of relicChoices) {
const item = document.createElement("div");
item.className = "reward-item";
item.style.maxWidth = "260px";
item.innerHTML = `<div style="font-weight:bold;color:var(--rare)">${relic.name}</div><div style="font-size:13px;color:var(--text-dim)">${relic.desc}</div>`;
item.onclick = () => {
state.player.relics.push({ id: relic.id, name: relic.name, desc: relic.desc, onEvent: {} });
if (relic.maxHpMod) { state.player.maxHp += relic.maxHpMod; state.player.hp = Math.min(state.player.maxHp, state.player.hp); }
if (relic.id === "cursed_crown") state.player.cursesRemovable = false;
if (relic.id === "ectoplasm") state.player.noGold = true;
for (const p of potions) addPotion({ player: state.player }, p);
state.player.gold += gold;
if (gold > 0 && state.player.noGold) state.player.gold = 0;
advanceAct();
};
grid.appendChild(item);
}
div.appendChild(grid);
screens.appendChild(div);
};
// Prevent gold gain when Ectoplasm
const oldEndCombat = endCombat;
endCombat = function(victory) {
oldEndCombat(victory);
if (state.player.noGold) state.player.gold = 0;
};
// Vial of Shadows on pickup
const oldAddRelic = addRelic;
addRelic = function(ctx, relic) {
state.player.relics.push(relic.clone ? relic.clone() : { ...relic });
if (relic.id === "vial_of_shadows" || relic.id === "vial-of-shadows") {
setTimeout(() => showCardPicker(state.player.deck, "Vial of Shadows: remove a card", (card) => {
state.player.deck = state.player.deck.filter(c => c !== card);
}), 0);
}
};
// Cursed Crown: block curse removal
// Patch Rest/Shop remove to check cursesRemovable
// Already card picker just takes any card; add guard later if needed.
// Ensure player has cursesRemovable default
const oldCreatePlayer = createPlayer;
createPlayer = function(className) {
const p = oldCreatePlayer(className);
p.cursesRemovable = true;
p.noGold = false;
return p;
};
}
// ── function: patchEventFlow ──
function patchEventFlow() {
pickEventChoice = function(choiceIndex) {
const ev = state.pending.event;
const choice = ev.choices[choiceIndex];
choice.effect(state, () => {
if (state.pending && state.pending.type === "removeCard") {
showCardPicker(state.player.deck, "Choose a card to remove", (card) => {
state.player.deck = state.player.deck.filter(c => c !== card);
if (state.pending.callback) state.pending.callback();
state.pending = null;
renderMap();
});
} else if (state.pending && state.pending.type === "combat") {
startCombatFromPending(state.pending);
} else if (!state.pending || state.pending.type === "event") {
state.pending = null;
renderMap();
}
});
};
renderEvent = function() {
const ev = state.pending.event;
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">${ev.name}</h1><div class="event-text">${ev.text}</div>`;
const choices = document.createElement("div");
choices.className = "event-choices";
ev.choices.forEach((ch, idx) => {
const btn = document.createElement("button");
btn.textContent = ch.text;
btn.onclick = () => {
pickEventChoice(idx);
};
choices.appendChild(btn);
});
div.appendChild(choices);
screens.appendChild(div);
};
}
// ── function: patchMapVisited ──
function patchMapVisited() {
const oldSelectNode = selectNode;
selectNode = function(node) {
const available = getAvailableNodes(state.mapRows, state.currentNode);
if (!available.includes(node)) return;
if (state.currentNode) state.currentNode.visited = true;
oldSelectNode(node);
};
}
// ── function: patchCurseRemoval ──
function patchCurseRemoval() {
// Rest meditate
const oldRenderRest = renderRest;
renderRest = function() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Rest Site</h1><div class="subtitle">Choose one action</div>`;
const opts = document.createElement("div");
opts.className = "rest-options";
const restBtn = document.createElement("button");
restBtn.textContent = "Rest (Heal 25%)";
restBtn.onclick = () => { state.player.hp = Math.min(state.player.maxHp, state.player.hp + Math.floor(state.player.maxHp * 0.25)); renderMap(); };
const forgeBtn = document.createElement("button");
forgeBtn.textContent = "Forge (Upgrade card)";
forgeBtn.onclick = () => showCardPicker(state.player.deck, "Choose a card to upgrade", (card) => { card.upgraded = true; renderMap(); });
const medBtn = document.createElement("button");
medBtn.textContent = "Meditate (Remove card)";
medBtn.onclick = () => {
const deck = state.player.cursesRemovable === false ? state.player.deck.filter(c => c.type !== "Curse") : state.player.deck;
showCardPicker(deck, "Choose a card to remove", (card) => { state.player.deck = state.player.deck.filter(c => c !== card); renderMap(); });
};
opts.appendChild(restBtn);
opts.appendChild(forgeBtn);
opts.appendChild(medBtn);
div.appendChild(opts);
screens.appendChild(div);
};
// Shop remove
const oldRenderShop = renderShop;
renderShop = function() {
oldRenderShop();
const div = document.getElementById("screens").firstElementChild;
const oldBtn = Array.from(div.querySelectorAll("button")).find(b => b.textContent.includes("Remove a card"));
if (oldBtn) {
oldBtn.onclick = () => { if (state.player.gold >= 75) { state.player.gold -= 75; const deck = state.player.cursesRemovable === false ? state.player.deck.filter(c => c.type !== "Curse") : state.player.deck; showCardPicker(deck, "Choose a card to remove", (card) => { state.player.deck = state.player.deck.filter(c => c !== card); renderMap(); }); } };
}
};
// Cursed Spring respect curses
EVENT_POOL[2].choices[0].effect = (ctx, cb) => { loseHp(state, state.player, 10); if (state.player.cursesRemovable !== false) state.player.deck = state.player.deck.filter(c => c.type !== "Curse"); cb(); };
}
// ── function: patchDexTonic ──
function patchDexTonic() {
POTION_POOL[2].effect = (ctx) => { applyStatus(ctx.player, "dexterity", 3); ctx.player.statuses.dexterityTemporal = (ctx.player.statuses.dexterityTemporal || 0) + 3; };
}
// ── function: patchAncientLibrary ──
function patchAncientLibrary() {
EVENT_POOL[5].choices[0].effect = (ctx, cb) => { loseHp(state, state.player, 5); state.player.statuses.energyNextTurn = (state.player.statuses.energyNextTurn || 0) + 1; cb(); };
}
// ── function: patchEnemyBlock ──
function patchEnemyBlock() {
const oldCreateEnemy = createEnemy;
createEnemy = function(type, opts) {
const e = oldCreateEnemy(type, opts);
e.statuses.block = 0;
return e;
};
}
// ── function: patchSelectCard ──
function patchSelectCard() {
selectCard = function(index) {
const c = state.combat;
const card = c.hand[index];
if (!card) return;
if (card.target === "enemy") {
const alive = c.enemies.filter(e => e.hp > 0);
if (alive.length === 1) { playCard(index, alive[0]); return; }
if (c.selectedCardIndex === index && alive.length > 0) { playCard(index, alive[0]); return; }
c.selectedCardIndex = index;
c.selectedTarget = null;
renderCombat();
return;
}
playCard(index, null);
};
}
// ── function: patchRewardDisplay ──
function patchRewardDisplay() {
renderCombatRewards = function(gold, cards, potion, relic) {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Victory!</h1><div class="subtitle">Loot: ${gold}g</div>`;
const grid = document.createElement("div");
grid.className = "reward-grid";
for (const card of cards) {
const item = document.createElement("div");
item.className = "reward-item";
item.appendChild(cardElement(card));
item.onclick = () => { state.player.deck.push(card); renderMap(); };
grid.appendChild(item);
}
const skip = document.createElement("div");
skip.className = "reward-item";
skip.innerHTML = `<div style="padding-top:60px;font-weight:bold">Skip card</div>`;
skip.onclick = () => renderMap();
grid.appendChild(skip);
div.appendChild(grid);
if (potion) {
const pdiv = document.createElement("div");
pdiv.innerHTML = `<h3>Potion obtained: ${potion.name}</h3>`;
div.appendChild(pdiv);
}
if (relic) {
const rdiv = document.createElement("div");
rdiv.innerHTML = `<h3>Relic obtained: ${relic.name}</h3>`;
div.appendChild(rdiv);
}
screens.appendChild(div);
};
}
// ── function: patchBossRewardGold ──
function patchBossRewardGold() {
const oldRenderBossRewards = renderBossRewards;
renderBossRewards = function(gold, relicChoices, potions) {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen fade-in";
div.innerHTML = `<h1 class="title">Boss Defeated!</h1><div class="subtitle">Choose a Boss Relic. Gold: ${gold}g</div>`;
const grid = document.createElement("div");
grid.className = "boss-relic-pick";
for (const relic of relicChoices) {
const item = document.createElement("div");
item.className = "reward-item";
item.style.maxWidth = "260px";
item.innerHTML = `<div style="font-weight:bold;color:var(--rare)">${relic.name}</div><div style="font-size:13px;color:var(--text-dim)">${relic.desc}</div>`;
item.onclick = () => {
state.player.relics.push({ id: relic.id, name: relic.name, desc: relic.desc, onEvent: {} });
if (relic.maxHpMod) { state.player.maxHp += relic.maxHpMod; state.player.hp = Math.min(state.player.maxHp, state.player.hp); }
if (relic.id === "cursed_crown") state.player.cursesRemovable = false;
if (relic.id === "ectoplasm") { state.player.noGold = true; state.player.gold = 0; }
if (relic.id !== "ectoplasm") state.player.gold += gold;
for (const p of potions) addPotion({ player: state.player }, p);
advanceAct();
};
grid.appendChild(item);
}
div.appendChild(grid);
screens.appendChild(div);
};
}
// ── function: patchMapRender ──
function patchMapRender() {
renderMap = function() {
const screens = document.getElementById("screens");
screens.innerHTML = "";
const div = document.createElement("div");
div.className = "screen map-screen fade-in";
const act = ACT_DEFINITIONS[state.act];
div.innerHTML = `<h1 class="title">${act.name}</h1><div class="subtitle">Act ${state.act + 1} — Floor ${state.currentNode ? state.currentNode.floor + 1 : 1}</div>`;
const container = document.createElement("div");
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "10px";
container.style.alignItems = "center";
const available = getAvailableNodes(state.mapRows, state.currentNode);
for (let r = state.mapRows.length - 1; r >= 0; r--) {
const row = state.mapRows[r];
const rowDiv = document.createElement("div");
rowDiv.className = "map-row";
const label = document.createElement("div");
label.className = "floor-label";
label.textContent = `Floor ${r + 1}`;
rowDiv.appendChild(label);
for (const node of row) {
const n = document.createElement("div");
n.className = `mapNode ${node.type} ${state.currentNode === node ? "current" : ""} ${available.includes(node) ? "available" : ""} ${state.currentNode !== node && node.visited ? "visited" : ""}`;
n.innerHTML = `<div>${node.label}</div><div style="font-size:9px">${node.floor + 1}</div>`;
if (available.includes(node)) n.onclick = () => selectNode(node);
rowDiv.appendChild(n);
}
container.appendChild(rowDiv);
}
div.appendChild(container);
if (state.player) {
const hud = document.createElement("div");
hud.className = "hud";
hud.style.marginTop = "20px";
hud.innerHTML = `
<div class="hud-stat stat-hp">HP: ${state.player.hp}/${state.player.maxHp}</div>
<div class="hud-stat stat-gold">Gold: ${state.player.gold}</div>
<div class="hud-stat">Deck: ${state.player.deck.length}</div>
<div class="hud-stat">Relics: ${state.player.relics.length}</div>
<div class="hud-stat">Potions: ${state.player.potions.length}/3</div>
`;
div.appendChild(hud);
}
screens.appendChild(div);
};
}
// ── main ──
initState();
patchBurnCards();
patchEnemyIntents();
patchGameLogic();
patchPowersAndRewards();
patchBossRelicsAndMisc();
patchEventFlow();
patchMapVisited();
patchCurseRemoval();
patchDexTonic();
patchAncientLibrary();
patchEnemyBlock();
patchSelectCard();
patchRewardDisplay();
patchBossRewardGold();
patchMapRender();
renderClassSelect();
</script>
</body>
</html>
<!-- agent-meta {"model":"moonshotai/kimi-k2.7-code","provider":"openrouter","persona":"composer","sessionId":"71356d15-e3d4-473a-917a-04bbfb8e73ea","tokensIn":6274152,"tokensOut":75908,"tokensTotal":6350060,"turns":90,"toolCalls":91,"failedToolCalls":1,"timestamp":"2026-07-16T05:12:07.554Z"} -->