Shadows
Qwen 3.8 27B GGUF · typescript

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 109,478 bytes · SHA-256 4f12b6377bb7
<!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 — Roguelike Deckbuilder</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&display=swap">
<style>
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/data.ts":{"./types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts","./data":"src/data.ts","./utils":"src/utils.ts"},"src/engine.ts":{"./types":"src/types.ts","./data":"src/data.ts","./state":"src/state.ts","./fx":"src/fx.ts","./utils":"src/utils.ts"},"src/events.ts":{"./types":"src/types.ts","./state":"src/state.ts","./data":"src/data.ts","./utils":"src/utils.ts"},"src/ui.ts":{"./types":"src/types.ts","./data":"src/data.ts","./state":"src/state.ts","./audio":"src/audio.ts","./utils":"src/utils.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// Entry point. Runs after the DOM is parsed and after any classic
// <script> imports, so document queries and CDN globals are available.
// Import your other modules from here.
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
function start() {
// Replace this with the real startup sequence.
}
start();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/utils.ts ──
__mods["src/utils.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.esc = void 0;
exports.uid = uid;
exports.rnd = rnd;
exports.rint = rint;
exports.pick = pick;
exports.shuffle = shuffle;
exports.chance = chance;
exports.clone = clone;
exports.clamp = clamp;
exports.plural = plural;
let uidCounter = 0;
function uid() {
uidCounter += 1;
return `u${uidCounter}_${Math.floor(Math.random() * 1e6)}`;
}
function rnd(n) { return Math.floor(Math.random() * n); }
function rint(min, max) { return min + rnd(max - min + 1); }
function pick(arr) { return arr[rnd(arr.length)]; }
function shuffle(arr) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = rnd(i + 1);
const t = a[i];
a[i] = a[j];
a[j] = t;
}
return a;
}
function chance(p) { return Math.random() < p; }
function clone(o) {
return JSON.parse(JSON.stringify(o));
}
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
exports.esc = esc;
function plural(n, s) { return n === 1 ? s : s + 's'; }
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sfx = void 0;
exports.setMuted = setMuted;
exports.isMuted = isMuted;
// Tiny WebAudio SFX synthesizer — no external assets.
let ctx = null;
let master = null;
let muted = false;
function ensure() {
try {
if (!ctx) {
const AC = window.AudioContext || window.webkitAudioContext;
ctx = new AC();
master = ctx.createGain();
master.gain.value = 0.35;
master.connect(ctx.destination);
}
if (ctx.state === 'suspended')
void ctx.resume();
return ctx;
}
catch {
return null;
}
}
function setMuted(m) { muted = m; }
function isMuted() { return muted; }
function blip(freq, dur, type, vol, slideTo, delay = 0) {
if (muted)
return;
const c = ensure();
if (!c || !master)
return;
const t0 = c.currentTime + delay;
const o = c.createOscillator();
const g = c.createGain();
o.type = type;
o.frequency.setValueAtTime(freq, t0);
if (slideTo !== undefined)
o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), t0 + dur);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(vol, t0 + 0.012);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
o.connect(g);
g.connect(master);
o.start(t0);
o.stop(t0 + dur + 0.05);
}
function noise(dur, vol, freq = 800, delay = 0) {
if (muted)
return;
const c = ensure();
if (!c || !master)
return;
const t0 = c.currentTime + delay;
const len = Math.max(1, Math.floor(c.sampleRate * dur));
const buf = c.createBuffer(1, len, c.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < len; i++)
d[i] = (Math.random() * 2 - 1) * (1 - i / len);
const src = c.createBufferSource();
src.buffer = buf;
const f = c.createBiquadFilter();
f.type = 'lowpass';
f.frequency.value = freq;
const g = c.createGain();
g.gain.setValueAtTime(vol, t0);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
src.connect(f);
f.connect(g);
g.connect(master);
src.start(t0);
}
exports.Sfx = {
click: () => blip(660, 0.06, 'square', 0.25),
hover: () => blip(880, 0.03, 'sine', 0.1),
draw: () => { blip(320, 0.05, 'triangle', 0.2); blip(420, 0.05, 'triangle', 0.15, undefined, 0.05); },
play: () => { blip(220, 0.08, 'sawtooth', 0.2, 330); noise(0.08, 0.15, 1200); },
hit: () => { noise(0.14, 0.5, 500); blip(110, 0.12, 'square', 0.3, 55); },
blocked: () => { blip(500, 0.06, 'square', 0.3, 250); noise(0.06, 0.2, 2500); },
heal: () => { blip(520, 0.09, 'sine', 0.25); blip(780, 0.12, 'sine', 0.2, undefined, 0.07); },
fire: () => { noise(0.3, 0.35, 900); blip(180, 0.25, 'sawtooth', 0.15, 60); },
poison: () => blip(200, 0.15, 'sine', 0.2, 120),
buff: () => { blip(440, 0.07, 'triangle', 0.22); blip(660, 0.09, 'triangle', 0.22, undefined, 0.06); },
debuff: () => { blip(300, 0.1, 'sawtooth', 0.2, 150); },
turn: () => { blip(240, 0.1, 'triangle', 0.2, 180); blip(360, 0.1, 'triangle', 0.18, undefined, 0.09); },
death: () => { blip(160, 0.5, 'sawtooth', 0.3, 40); noise(0.4, 0.3, 300); },
win: () => { [392, 494, 587, 784].forEach((f, i) => blip(f, 0.22, 'triangle', 0.25, undefined, i * 0.11)); },
lose: () => { [300, 240, 180, 120].forEach((f, i) => blip(f, 0.25, 'sawtooth', 0.22, f * 0.7, i * 0.14)); },
gold: () => { blip(990, 0.06, 'square', 0.2); blip(1320, 0.08, 'square', 0.18, undefined, 0.05); },
error: () => blip(140, 0.12, 'square', 0.2, 90),
summon: () => { blip(90, 0.4, 'sawtooth', 0.25, 160); noise(0.3, 0.2, 400); },
exhaust: () => noise(0.2, 0.2, 1500),
};
};
// ── module: src/data.ts ──
__mods["src/data.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ACT_NAMES = exports.ENEMIES = exports.POTIONS = exports.ALL_RELICS = exports.BOSS_RELICS = exports.RELICS = exports.ALL_CLASS_POOLS = exports.IMP_CLAW_UP = exports.IMP_CLAW = exports.CURSE_DEFS = exports.COLORLESS_POOL = exports.BASIC_POOL = exports.HELL_POOL = exports.SHADOW_POOL = exports.BLOOD_POOL = void 0;
exports.getDef = getDef;
exports.classOfCard = classOfCard;
exports.isUnplayable = isUnplayable;
exports.cardSpec = cardSpec;
exports.getRelic = getRelic;
exports.getPotion = getPotion;
exports.getEnemy = getEnemy;
exports.buildMap = buildMap;
// ---------------------------------------------------------------- CLASS CARDS
const c = (d) => d;
exports.BLOOD_POOL = [
c({ id: 'sanguine_strike', name: 'Sanguine Strike', type: 'Attack', rarity: 'Common', cost: 1, target: 'enemy', desc: 'Deal 8 damage. Heal 2 HP.', n: { dmg: 8, heal: 2 }, nu: { dmg: 10, heal: 3 } }),
c({ id: 'hemorrhage', name: 'Hemorrhage', type: 'Attack', rarity: 'Common', cost: 1, target: 'enemy', desc: 'Deal 5 damage. Apply 3 Bleed.', n: { dmg: 5, bleed: 3 }, nu: { dmg: 6, bleed: 4 } }),
c({ id: 'bloodletting', name: 'Bloodletting', type: 'Attack', rarity: 'Uncommon', cost: 1, target: 'enemy', desc: 'Lose 3 HP. Deal 12 damage.', n: { dmg: 12, costHp: 3 }, nu: { dmg: 15, costHp: 2 } }),
c({ id: 'crimson_eruption', name: 'Crimson Eruption', type: 'Attack', rarity: 'Rare', cost: 2, target: 'none', all: true, desc: 'Deal 8 damage to ALL enemies. Lose 2 HP.', n: { dmg: 8, costHp: 2 }, nu: { dmg: 10, costHp: 2 } }),
c({ id: 'crimson_shield', name: 'Crimson Shield', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', desc: 'Gain 6 Block. Lose 2 HP.', n: { block: 6, costHp: 2 }, nu: { block: 8, costHp: 2 } }),
c({ id: 'blood_ward', name: 'Blood Ward', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', desc: 'Gain 3 Block. Gain 1 Regeneration.', n: { block: 3, reg: 1 }, nu: { block: 4, reg: 2 } }),
c({ id: 'sacrificial_barrier', name: 'Sacrificial Barrier', type: 'Skill', rarity: 'Uncommon', cost: 2, target: 'none', desc: 'Gain 10 Block. Lose 5 HP.', n: { block: 10, costHp: 5 }, nu: { block: 13, costHp: 5 } }),
c({ id: 'vital_surge', name: 'Vital Surge', type: 'Skill', rarity: 'Rare', cost: 'X', target: 'none', exhaust: true, blockOnMissingHp: true, missingHpBonus: 0, desc: 'Gain Block equal to your missing HP. Exhaust.', nu: { block: 3, desc: 'Gain Block equal to your missing HP plus 3. Exhaust.' } }),
c({ id: 'life_tap', name: 'Life Tap', type: 'Skill', rarity: 'Common', cost: 0, target: 'none', desc: 'Draw 2 cards. Lose 3 HP.', n: { draw: 2, costHp: 3 }, nu: { draw: 3, costHp: 3 }, drawOnPlay: 2 }),
c({ id: 'rend_flesh', name: 'Rend Flesh', type: 'Skill', rarity: 'Uncommon', cost: 1, target: 'none', all: true, desc: 'Apply 5 Bleed to ALL enemies. Lose 2 HP.', n: { bleed: 5, costHp: 2 }, nu: { bleed: 7, costHp: 2 }, bleedAll: 5 }),
c({ id: 'soul_siphon', name: 'Soul Siphon', type: 'Power', rarity: 'Uncommon', cost: 2, target: 'none', desc: 'At the start of your turn, gain 1 Energy and lose 1 HP.', power: { kind: 'soulSiphon' }, nu: { cost: 1, desc: 'At the start of your turn, gain 1 Energy and lose 1 HP.' } }),
c({ id: 'blood_pact', name: 'Blood Pact', type: 'Power', rarity: 'Rare', cost: 1, target: 'none', desc: 'Whenever you lose HP from a card, gain 1 Strength.', power: { kind: 'bloodPact' }, nu: { str: 2, desc: 'Whenever you lose HP from a card, gain 2 Strength.' } }),
];
exports.SHADOW_POOL = [
c({ id: 'shadow_bolt', name: 'Shadow Bolt', type: 'Attack', rarity: 'Common', cost: 1, target: 'enemy', desc: 'Deal 6 damage. Apply 1 Weak.', n: { dmg: 6, weak: 1 }, nu: { dmg: 8, weak: 2 } }),
c({ id: 'grasp_of_despair', name: 'Grasp of Despair', type: 'Attack', rarity: 'Common', cost: 1, target: 'enemy', desc: 'Deal 4 damage. Apply 1 Vulnerable.', n: { dmg: 4, vuln: 1 }, nu: { dmg: 5, vuln: 2 } }),
c({ id: 'nightmare_lash', name: 'Nightmare Lash', type: 'Attack', rarity: 'Uncommon', cost: 2, target: 'enemy', desc: 'Deal 7 damage. If the enemy has Weak, deal 7 more damage.', n: { dmg: 7 }, nu: { dmg: 9 }, ifWeakExtra: 7, ifWeakExtraUp: 9 }),
c({ id: 'eternal_darkness', name: 'Eternal Darkness', type: 'Attack', rarity: 'Rare', cost: 2, target: 'none', random: true, desc: 'Deal 3 damage to a random enemy 3 times.', n: { dmg: 3 }, nu: { dmg: 4 }, randomHit: { hits: 3 } }),
c({ id: 'cloak_of_shadows', name: 'Cloak of Shadows', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', desc: 'Gain 5 Block. Draw 1 card.', n: { block: 5, draw: 1 }, nu: { block: 7, draw: 1 }, drawOnPlay: 1 }),
c({ id: 'haunting_mist', name: 'Haunting Mist', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', all: true, desc: 'Gain 7 Block. Apply 1 Weak to ALL enemies.', n: { block: 7, weak: 1 }, nu: { block: 9, weak: 1 }, weakAll: 1 }),
c({ id: 'phantom_armor', name: 'Phantom Armor', type: 'Skill', rarity: 'Uncommon', cost: 1, target: 'none', desc: 'Gain 8 Block. Next turn, gain 2 Dexterity.', n: { block: 8 }, nu: { block: 10 }, dexNextTurn: 2, dexNextTurnUp: 3 }),
c({ id: 'whispering_shroud', name: 'Whispering Shroud', type: 'Skill', rarity: 'Rare', cost: 1, target: 'none', desc: 'Gain 4 Block. Next turn, gain 4 Block.', n: { block: 4 }, nu: { block: 5 }, blockNextTurn: 4, blockNextTurnUp: 5 }),
c({ id: 'shadowform', name: 'Shadowform', type: 'Skill', rarity: 'Uncommon', cost: 2, target: 'none', exhaust: true, desc: 'Gain 2 Intangible. Exhaust.', n: { intang: 2 }, nu: { intang: 3, desc: 'Gain 3 Intangible. Exhaust.' } }),
c({ id: 'curse_of_misfortune', name: 'Curse of Misfortune', type: 'Skill', rarity: 'Common', cost: 1, target: 'enemy', desc: 'Enemy loses 3 Strength this turn. Apply 1 Vulnerable.', n: { str: -3, vuln: 1 }, nu: { str: -4, vuln: 2 } }),
c({ id: 'dark_ritual', name: 'Dark Ritual', type: 'Skill', rarity: 'Uncommon', cost: 0, target: 'none', desc: 'Gain 2 Energy. Shuffle a Dazed into your draw pile.', n: { energy: 2 }, nu: { desc: 'Gain 2 Energy. Add a Dazed to your discard pile.' }, shuffleDazed: true, shuffleDazedDiscard: true }),
c({ id: 'umbral_mastery', name: 'Umbral Mastery', type: 'Power', rarity: 'Rare', cost: 2, target: 'none', desc: 'At the start of your turn, apply 1 Weak to ALL enemies.', power: { kind: 'umbral' }, nu: { weak: 2, desc: 'At the start of your turn, apply 2 Weak to ALL enemies.' } }),
];
exports.HELL_POOL = [
c({ id: 'firebolt', name: 'Firebolt', type: 'Attack', rarity: 'Common', cost: 1, target: 'enemy', desc: 'Deal 7 damage. Apply 2 Burn.', n: { dmg: 7, burn: 2 }, nu: { dmg: 9, burn: 3 } }),
c({ id: 'hellfire_blast', name: 'Hellfire Blast', type: 'Attack', rarity: 'Uncommon', cost: 2, target: 'none', all: true, desc: 'Deal 9 damage to ALL enemies.', n: { dmg: 9 }, nu: { dmg: 12 } }),
c({ id: 'searing_strike', name: 'Searing Strike', type: 'Attack', rarity: 'Common', cost: 1, target: 'enemy', desc: 'Deal 6 damage. Gain 1 Energy next turn.', n: { dmg: 6 }, nu: { dmg: 8 }, energyNextTurn: 1 }),
c({ id: 'immolate', name: 'Immolate', type: 'Attack', rarity: 'Rare', cost: 2, target: 'enemy', exhaust: true, desc: 'Deal 12 damage. Exhaust.', n: { dmg: 12 }, nu: { dmg: 16, desc: 'Deal 16 damage. Exhaust.' } }),
c({ id: 'obsidian_skin', name: 'Obsidian Skin', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', desc: 'Gain 7 Block.', n: { block: 7 }, nu: { block: 10 } }),
c({ id: 'flame_ward', name: 'Flame Ward', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', desc: 'Gain 5 Block. Apply 2 Burn to a random enemy.', n: { block: 5, burn: 2 }, nu: { block: 6, burn: 3 } }),
c({ id: 'demonic_resilience', name: 'Demonic Resilience', type: 'Skill', rarity: 'Uncommon', cost: 1, target: 'none', desc: 'Gain 4 Block. Heal 2 HP.', n: { block: 4, heal: 2 }, nu: { block: 5, heal: 3 } }),
c({ id: 'brimstone_shield', name: 'Brimstone Shield', type: 'Skill', rarity: 'Rare', cost: 1, target: 'none', desc: 'Gain 3 Block. Gain 2 Strength this turn.', n: { block: 3, str: 2 }, nu: { block: 4, str: 3 }, strThisTurn: 2 }),
c({ id: 'summon_imp', name: 'Summon Imp', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', exhaust: true, desc: 'Add an Imp Claw (0-cost Attack, 4 damage, Exhaust) to your hand. Exhaust.', addImp: true, nu: { desc: 'Add an Imp Claw+ (6 damage) to your hand. Exhaust.' } }),
c({ id: 'infernal_pact', name: 'Infernal Pact', type: 'Skill', rarity: 'Uncommon', cost: 0, target: 'none', desc: 'Lose 2 HP. Gain 2 Energy. Add an Imp Claw to your hand.', n: { energy: 2, costHp: 2 }, nu: { costHp: 1 }, addImp: true, energyOnPlay: 2 }),
c({ id: 'hellfire_aura', name: 'Hellfire Aura', type: 'Power', rarity: 'Rare', cost: 2, target: 'none', desc: 'At the start of your turn, deal 3 damage to ALL enemies.', power: { kind: 'hellfire' }, nu: { dmg: 5, desc: 'At the start of your turn, deal 5 damage to ALL enemies.' } }),
c({ id: 'soul_burn', name: 'Soul Burn', type: 'Skill', rarity: 'Uncommon', cost: 1, target: 'enemy', desc: 'Apply 8 Burn to an enemy. Draw 1 card.', n: { burn: 8, draw: 1 }, nu: { burn: 10, draw: 1 }, drawOnPlay: 1 }),
];
exports.BASIC_POOL = [
c({ id: 'strike', name: 'Strike', type: 'Attack', rarity: 'Basic', cost: 1, target: 'enemy', desc: 'Deal 6 damage.', n: { dmg: 6 }, nu: { dmg: 8 } }),
c({ id: 'defend', name: 'Defend', type: 'Skill', rarity: 'Basic', cost: 1, target: 'none', desc: 'Gain 5 Block.', n: { block: 5 }, nu: { block: 8 } }),
];
exports.COLORLESS_POOL = [
c({ id: 'flash_of_insight', name: 'Flash of Insight', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', desc: 'Draw 2 cards.', n: { draw: 2 }, nu: { draw: 3 }, drawOnPlay: 2 }),
c({ id: 'iron_will', name: 'Iron Will', type: 'Skill', rarity: 'Common', cost: 1, target: 'none', desc: 'Gain 8 Block.', n: { block: 8 }, nu: { block: 11 } }),
c({ id: 'quick_slash', name: 'Quick Slash', type: 'Attack', rarity: 'Common', cost: 1, target: 'enemy', desc: 'Deal 7 damage.', n: { dmg: 7 }, nu: { dmg: 9 } }),
c({ id: 'potent_brew', name: 'Potent Brew', type: 'Skill', rarity: 'Uncommon', cost: 0, target: 'none', exhaust: true, desc: 'Gain 1 Energy. Exhaust.', n: { energy: 1 }, nu: { energy: 2, desc: 'Gain 2 Energy. Exhaust.' }, energyOnPlay: 1 }),
];
exports.CURSE_DEFS = [
c({ id: 'wound', name: 'Wound', type: 'Curse', rarity: 'Curse', cost: -1, target: 'none', desc: 'Unplayable. Ethereal. Exhaust.', ethereal: true, exhaust: true }),
c({ id: 'doubt', name: 'Doubt', type: 'Curse', rarity: 'Curse', cost: -1, target: 'none', desc: 'Unplayable. At the end of your turn, lose 1 HP.' }),
c({ id: 'dazed', name: 'Dazed', type: 'Status', rarity: 'Status', cost: -1, target: 'none', desc: 'Unplayable. Ethereal. Exhaust.', ethereal: true, exhaust: true }),
];
exports.IMP_CLAW = c({ id: 'imp_claw', name: 'Imp Claw', type: 'Attack', rarity: 'Token', cost: 0, target: 'enemy', exhaust: true, desc: 'Deal 4 damage. Exhaust.', n: { dmg: 4 } });
exports.IMP_CLAW_UP = c({ id: 'imp_claw_up', name: 'Imp Claw+', type: 'Attack', rarity: 'Token', cost: 0, target: 'enemy', exhaust: true, desc: 'Deal 6 damage. Exhaust.', n: { dmg: 6 } });
exports.ALL_CLASS_POOLS = {
blood: exports.BLOOD_POOL, shadow: exports.SHADOW_POOL, hell: exports.HELL_POOL,
};
function getDef(defId) {
if (defId === 'imp_claw')
return exports.IMP_CLAW;
if (defId === 'imp_claw_up')
return exports.IMP_CLAW_UP;
for (const pool of [exports.BLOOD_POOL, exports.SHADOW_POOL, exports.HELL_POOL, exports.BASIC_POOL, exports.COLORLESS_POOL, exports.CURSE_DEFS]) {
const f = pool.find(x => x.id === defId);
if (f)
return f;
}
throw new Error('unknown card ' + defId);
}
function classOfCard(def) {
if (exports.BLOOD_POOL.includes(def))
return 'blood';
if (exports.SHADOW_POOL.includes(def))
return 'shadow';
if (exports.HELL_POOL.includes(def))
return 'hell';
return null;
}
function isUnplayable(def) {
return def.type === 'Curse' || def.type === 'Status';
}
function cardSpec(def, upgraded) {
const nu = def.nu;
let name = def.name;
let desc = def.desc;
let cost = def.cost;
const n = { ...(def.n || {}) };
if (upgraded) {
name = def.name + '+';
if (nu) {
if (nu.desc)
desc = nu.desc;
if (nu.cost !== undefined)
cost = nu.cost;
const keys = Object.keys(nu);
for (const k of keys) {
if (n[k] !== undefined && nu[k] !== undefined)
n[k] = nu[k];
}
}
}
return { def, name, desc, cost, n };
}
// ---------------------------------------------------------------- RELICS
exports.RELICS = [
{ id: 'bloodstone', name: 'Bloodstone', desc: 'Whenever you lose HP from a card, heal 1 HP.', rarity: 'Common' },
{ id: 'demon_horns', name: 'Demon Horns', desc: 'At the start of each combat, add an Imp Claw to your hand (once per combat).', rarity: 'Common', cls: 'hell' },
{ id: 'shadow_cloak', name: 'Shadow Cloak', desc: 'At the start of each combat, gain 1 Intangible.', rarity: 'Rare' },
{ id: 'cursed_amulet', name: 'Cursed Amulet', desc: 'At the start of each combat, apply 1 Weak to ALL enemies.', rarity: 'Common' },
{ id: 'tome', name: 'Tome of Dark Rituals', desc: 'Whenever you play a Power card, draw 1 card.', rarity: 'Uncommon' },
{ id: 'bleeding_edge', name: 'Bleeding Edge', desc: 'At the start of your turn, apply 1 Bleed to a random enemy.', rarity: 'Common' },
{ id: 'soul_ember', name: 'Soul Ember', desc: 'Whenever you apply Burn to an enemy, deal 2 additional damage.', rarity: 'Rare' },
{ id: 'hourglass', name: 'Eternal Hourglass', desc: 'At the start of your turn, if you have no cards in hand, gain 1 Energy.', rarity: 'Uncommon' },
{ id: 'sacrificial_dagger', name: 'Sacrificial Dagger', desc: 'Your first Attack that costs HP each combat costs 1 less HP.', rarity: 'Common', cls: 'blood' },
{ id: 'vial_of_shadows', name: 'Vial of Shadows', desc: 'When obtained, remove a card from your deck. It does nothing further.', rarity: 'Rare', unique: true },
{ id: 'ashen_armor', name: 'Ashen Armor', desc: 'At the end of combat, heal 6 HP.', rarity: 'Common' },
{ id: 'greedy_imp', name: 'Greedy Imp', desc: 'Enemies drop 10 extra Gold.', rarity: 'Common' },
];
exports.BOSS_RELICS = [
{ id: 'cursed_crown', name: 'Cursed Crown', desc: 'Gain 1 Energy at the start of each turn. Curses can no longer be removed.', rarity: 'Rare', boss: true, unique: true },
{ id: 'ectoplasm', name: 'Ectoplasm', desc: 'Gain 1 Energy at the start of each turn. Lose all Gold and gain no more Gold this run.', rarity: 'Rare', boss: true, unique: true },
{ id: 'philosopher', name: "Philosopher's Stone", desc: 'Gain 1 Energy at the start of each turn. All enemies start combat with 1 Strength.', rarity: 'Rare', boss: true, unique: true },
{ id: 'black_blood', name: 'Black Blood', desc: 'At start of combat, lose 10% max HP, but whenever you play an Attack, heal 1 HP.', rarity: 'Rare', boss: true, unique: true },
{ id: 'shadow_amulet', name: 'Shadow Amulet', desc: 'Start each turn with 1 Intangible. Reduce max HP by 15.', rarity: 'Rare', boss: true, unique: true },
];
exports.ALL_RELICS = [...exports.RELICS, ...exports.BOSS_RELICS];
function getRelic(id) {
const r = exports.ALL_RELICS.find(x => x.id === id);
if (!r)
throw new Error('unknown relic ' + id);
return r;
}
// ---------------------------------------------------------------- POTIONS
exports.POTIONS = [
{ id: 'blood_vial', name: 'Blood Vial', desc: 'Heal 15 HP.' },
{ id: 'firebomb', name: 'Firebomb', desc: 'Deal 10 damage to ALL enemies.' },
{ id: 'dex_tonic', name: 'Dex Tonic', desc: 'Gain 3 Dexterity for the rest of combat (1 turn).' },
{ id: 'energy_elixir', name: 'Energy Elixir', desc: 'Gain 2 Energy this turn.' },
{ id: 'cursed_brew', name: 'Cursed Brew', desc: 'Apply 3 Vulnerable to ALL enemies.' },
{ id: 'bleeding_flask', name: 'Bleeding Flask', desc: 'Apply 6 Bleed to an enemy.' },
{ id: 'imp_bottle', name: 'Imp-in-a-Bottle', desc: 'Add an Imp Claw+ to your hand.' },
{ id: 'smoke_bomb', name: 'Smoke Bomb', desc: 'Escape from a non-boss combat, no rewards.' },
];
function getPotion(id) {
const p = exports.POTIONS.find(x => x.id === id);
if (!p)
throw new Error('unknown potion ' + id);
return p;
}
// ---------------------------------------------------------------- ENEMIES
const E = (d) => d;
const atk = (text, dmg, extra) => ({ kind: 'attack', text, dmg, ...extra });
const multi = (text, dmg, hits, extra) => ({ kind: 'multi', text, dmg, hits, ...extra });
exports.ENEMIES = {};
function reg(def) { exports.ENEMIES[def.id] = def; }
reg(E({ id: 'skeleton', name: 'Skeleton Warrior', hp: 22, art: 'skeleton', pattern: [atk('Attack 6', 6), atk('Attack 6', 6), { kind: 'defend', text: 'Defend 8', block: 8 }] }));
reg(E({ id: 'zombie', name: 'Zombie Brute', hp: 28, art: 'zombie', pattern: [atk('Attack 8', 8), { kind: 'debuff', text: 'Apply 2 Weak', weak: 2 }, atk('Attack 8', 8)] }));
reg(E({ id: 'cultist', name: 'Dark Cultist', hp: 20, art: 'cultist', pattern: [{ kind: 'buff', text: 'Gain 3 Strength', str: 3 }, atk('Attack 5', 5), atk('Attack 5', 5)] }));
reg(E({ id: 'rats', name: 'Rat Swarm', hp: 15, art: 'rats', pattern: [multi('2×3', 3, 2), multi('2×3', 3, 2)] }));
reg(E({ id: 'guardian', name: 'Tomb Guardian', hp: 42, art: 'guardian', pattern: [{ kind: 'mixed', text: 'Shield Bash: 6 dmg, +8 Block', dmg: 6, block: 8 }, multi('Flurry: 3×4', 4, 3), atk('Heavy Slam 12', 12)] }));
reg(E({ id: 'witch', name: 'Blood Witch', hp: 36, art: 'witch', pattern: [{ kind: 'drain', text: 'Drain Life: 5 dmg, heal 4', dmg: 5, heal: 4 }, { kind: 'debuff', text: 'Curse: 1 Vuln, 1 Weak', vuln: 1, weak: 1 }, { kind: 'drain', text: 'Blood Ritual: 7 dmg, heal 4', dmg: 7, heal: 4 }] }));
reg(E({ id: 'lich', name: 'Lich King', hp: 65, art: 'lich', phase2: 'lich2', pattern: [{ kind: 'summon', text: 'Summon Skeleton Minion', summon: 'minion_skeleton' }, atk('Attack 8', 8), { kind: 'defend', text: 'Defend 12', block: 12 }] }));
reg(E({ id: 'lich2', name: 'Lich King (Enraged)', hp: 65, art: 'lich', pattern: [atk('Attack 10', 10), atk('Attack 10', 10), { kind: 'defend', text: 'Defend 15', block: 15 }] }));
reg(E({ id: 'minion_skeleton', name: 'Skeleton Minion', hp: 15, art: 'minion', pattern: [atk('Attack 5', 5)] }));
reg(E({ id: 'shadow_thief', name: 'Shadow Thief', hp: 30, art: 'thief', pattern: [{ kind: 'mixed', text: 'Backstab: 7 dmg, +4 Block', dmg: 7, block: 4 }, atk('Attack 6', 6), { kind: 'debuff', text: 'Sap: 1 Vuln, 1 Weak', vuln: 1, weak: 1 }] }));
// Act 2
reg(E({ id: 'wraith', name: 'Wraith', hp: 30, art: 'wraith', scale: { hp: 1, dmg: 0.25 }, pattern: [atk('Attack 7, 1 Weak', 7, { weak: 1 }), atk('Attack 7', 7), { kind: 'defend', text: 'Defend 10', block: 10 }] }));
reg(E({ id: 'beast', name: 'Corrupted Beast', hp: 38, art: 'beast', scale: { hp: 1, dmg: 0.25 }, pattern: [atk('Attack 12', 12), { kind: 'vulnSelf', text: 'Expose: +5 Block, 1 Vuln on self', block: 5, vulnSelf: 1 }, atk('Attack 12', 12)] }));
reg(E({ id: 'horror', name: 'Thorned Horror', hp: 25, art: 'horror', scale: { hp: 1, dmg: 0.25 }, pattern: [multi('3×4', 4, 3), { kind: 'debuff', text: 'Apply 3 Bleed', bleed: 3 }, atk('Attack 8', 8)] }));
reg(E({ id: 'shaman', name: 'Witch Doctor', hp: 22, art: 'shaman', scale: { hp: 1, dmg: 0.25 }, pattern: [{ kind: 'buff', text: 'Ritual: +4 Block, +2 Regen', block: 4, regen: 2 }, { kind: 'attack', text: 'Spit: 6 dmg, 2 Poison', dmg: 6, poison: 2 }, { kind: 'attack', text: 'Spit: 6 dmg, 2 Poison', dmg: 6, poison: 2 }] }));
reg(E({ id: 'treant_prot', name: 'Treant Protector', hp: 50, art: 'treant', scale: { hp: 1, dmg: 0.25 }, pattern: [{ kind: 'mixed', text: 'Branch Smash: 9 dmg, +6 Block', dmg: 9, block: 6 }, { kind: 'debuff', text: 'Wither: 2 Vulnerable', vuln: 2 }, multi('Rend: 2×10', 10, 2)] }));
reg(E({ id: 'plague', name: 'Plague Shaman', hp: 42, art: 'plague', scale: { hp: 1, dmg: 0.25 }, pattern: [{ kind: 'debuff', text: 'Plague Cloud: 4 Poison', poison: 4 }, { kind: 'summon', text: 'Summon Plague Rat', summon: 'plague_rat' }, atk('Attack 8', 8)] }));
reg(E({ id: 'plague_rat', name: 'Plague Rat', hp: 18, art: 'rats', pattern: [atk('Attack 5', 5)] }));
reg(E({ id: 'eldritch', name: 'Eldritch Treant', hp: 75, art: 'treant_boss', phase2: 'eldritch2', pattern: [atk('Attack 10, 2 Weak', 10, { weak: 2 }), { kind: 'defend', text: 'Defend 15, +2 Regen', block: 15, regen: 2 }, multi('2×8', 8, 2)] }));
reg(E({ id: 'eldritch2', name: 'Eldritch Treant (Awakened)', hp: 75, art: 'treant_boss', pattern: [atk('Attack 14, 1 Vuln', 14, { vuln: 1 }), { kind: 'defend', text: 'Defend 20, heal 5', block: 20, heal: 5 }, atk('Attack 14, 1 Vuln', 14, { vuln: 1 })] }));
// Act 3
reg(E({ id: 'hound', name: 'Hellhound', hp: 26, art: 'hound', scale: { hp: 1, dmg: 0.5 }, pattern: [atk('Attack 8, 2 Burn', 8, { burn: 2 }), atk('Attack 8', 8), { kind: 'defend', text: 'Defend 9', block: 9 }] }));
reg(E({ id: 'sdemon', name: 'Shadow Demon', hp: 30, art: 'sdemon', scale: { hp: 1, dmg: 0.5 }, pattern: [atk('Attack 7, 1 Weak', 7, { weak: 1 }), atk('Attack 7', 7), { kind: 'buff', text: 'Gain 1 Intangible', intang: 1 }] }));
reg(E({ id: 'imp3', name: 'Infernal Imp', hp: 18, art: 'imp', scale: { hp: 1, dmg: 0.5 }, pattern: [atk('Attack 5', 5), atk('Attack 5', 5), { kind: 'summon', text: 'Summon Infernal Imp', summon: 'imp3' }] }));
reg(E({ id: 'acultist', name: 'Abyssal Cultist', hp: 24, art: 'cultist3', scale: { hp: 1, dmg: 0.5 }, pattern: [{ kind: 'buff', text: 'Empower: +2 Str to ALL enemies', str: 2 }, atk('Attack 7', 7), atk('Attack 7', 7)] }));
reg(E({ id: 'flamespawn', name: 'Flamespawn', hp: 48, art: 'flamespawn', scale: { hp: 1, dmg: 0.5 }, pattern: [atk('Attack 9, 3 Burn', 9, { burn: 3 }), { kind: 'defend', text: 'Defend 10, +2 Str', block: 10, str: 2 }, multi('2×8', 8, 2)] }));
reg(E({ id: 'dread', name: 'Dread Knight', hp: 55, art: 'dread', scale: { hp: 1, dmg: 0.5 }, pattern: [atk('Attack 12', 12), { kind: 'debuff', text: 'Cleaver: 3 Bleed', bleed: 3 }, { kind: 'defend', text: 'Defend 12, heal 4', block: 12, heal: 4 }] }));
reg(E({ id: 'archdemon', name: 'Archdemon', hp: 85, art: 'archdemon', phase2: 'archdemon2', pattern: [{ kind: 'summon', text: 'Summon Hellhound', summon: 'hound' }, atk('Attack 12, 2 Burn', 12, { burn: 2 }), { kind: 'defend', text: 'Defend 15, +3 Str', block: 15, str: 3 }] }));
reg(E({ id: 'archdemon2', name: 'Archdemon (Unbound)', hp: 85, art: 'archdemon', pattern: [atk('Attack 16', 16), multi('3×7', 7, 3), atk('Attack 16', 16)] }));
function getEnemy(id) {
const d = exports.ENEMIES[id];
if (!d)
throw new Error('unknown enemy ' + id);
return d;
}
// ---------------------------------------------------------------- MAPS
// 15 rows; columns 0..4; player starts at floor 1 col 2 and moves upward.
const COLS = [
[2], // f1 start
[1, 3], // f2
[0, 2, 4], // f3
[2], // f4 event
[1, 3], // f5 shop
[2], // f6
[0, 2, 4], // f7 rest
[2], // f8
[1, 3], // f9 elite / enemy
[2], // f10 event
[0, 2, 4], // f11
[1, 3], // f12 elite / enemy
[0, 2, 4], // f13
[2], // f14 rest
[2], // f15 boss
];
const FLOOR_TYPE = [
'start', 'enemy', 'enemy', 'event', 'shop', 'enemy', 'rest', 'enemy',
'elite', 'event', 'enemy', 'elite', 'enemy', 'rest', 'boss',
];
function buildMap(act) {
const nodes = [];
const edges = [];
for (let f = 1; f <= 15; f++) {
for (const col of COLS[f - 1]) {
nodes.push({ id: `f${f}c${col}`, type: FLOOR_TYPE[f - 1], col, floor: f });
}
}
// edges: connect adjacent floors when |colA - colB| <= 2
for (let f = 2; f <= 15; f++) {
for (const c1 of COLS[f - 2]) {
for (const c2 of COLS[f - 1]) {
if (Math.abs(c2 - c1) <= 2)
edges.push([`f${f - 1}c${c1}`, `f${f}c${c2}`]);
}
}
}
// On floors 9 and 12 the right node (col 3) is the safe enemy alternative
for (const f of [9, 12]) {
const n = nodes.find(x => x.floor === f && x.col === 3);
if (n)
n.type = 'enemy';
}
// enemy groups per act, indexed by floor
const groups = {
1: { 2: ['skeleton', 'zombie'], 3: ['cultist'], 6: ['rats'], 8: ['skeleton', 'skeleton', 'zombie'], 9: ['skeleton', 'rats'], 11: ['cultist', 'rats'], 12: ['zombie', 'skeleton'], 13: ['cultist', 'cultist'] },
2: { 2: ['wraith', 'wraith'], 3: ['horror'], 6: ['beast'], 8: ['beast', 'horror'], 9: ['horror'], 11: ['wraith', 'horror'], 12: ['beast', 'shaman'], 13: ['wraith', 'wraith'] },
3: { 2: ['hound', 'sdemon'], 3: ['acultist', 'imp3'], 6: ['sdemon', 'sdemon'], 8: ['acultist', 'acultist'], 9: ['sdemon'], 11: ['hound', 'imp3'], 12: ['hound', 'sdemon', 'acultist'], 13: ['sdemon', 'sdemon'] },
};
const elites = {
1: { 9: 'guardian', 12: 'witch' },
2: { 9: 'treant_prot', 12: 'plague' },
3: { 9: 'flamespawn', 12: 'dread' },
};
const bosses = { 1: 'lich', 2: 'eldritch', 3: 'archdemon' };
const events = { 1: ['altar', 'spring'], 2: ['peddler', 'dead'], 3: ['gambler', 'library'] };
for (let f = 2; f <= 13; f++) {
const floorNodes = nodes.filter(x => x.floor === f && (x.type === 'enemy' || x.type === 'elite')).sort((a, b) => a.col - b.col);
floorNodes.forEach((n, i) => {
if (n.type === 'enemy')
n.group = (groups[act][f] && groups[act][f][i]) || 'skeleton';
if (n.type === 'elite')
n.group = elites[act][f];
});
}
for (const n of nodes) {
if (n.type === 'boss')
n.group = bosses[act];
}
let evIdx = 0;
for (const n of nodes) {
if (n.type === 'event') {
n.group = events[act][evIdx % events[act].length];
evIdx++;
}
}
return { nodes, edges };
}
exports.ACT_NAMES = ['', 'The Crypts', 'Blighted Woods', 'Abyssal Spire'];
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ACT_NAMES = void 0;
exports.setRunRef = setRunRef;
exports.run = run;
exports.newCharStats = newCharStats;
exports.newRun = newRun;
exports.makeEnemy = makeEnemy;
exports.newCombat = newCombat;
exports.hpLossCard = hpLossCard;
exports.rollReward = rollReward;
exports.rollCardChoices = rollCardChoices;
exports.makeShop = makeShop;
exports.rollRelicPool = rollRelicPool;
exports.addCardToDeck = addCardToDeck;
exports.removeCardFromDeck = removeCardFromDeck;
exports.upgradeCardInDeck = upgradeCardInDeck;
const data_1 = require("./data");
Object.defineProperty(exports, "ACT_NAMES", { enumerable: true, get: function () { return data_1.ACT_NAMES; } });
const utils_1 = require("./utils");
let runRef = null;
function setRunRef(r) { runRef = r; }
function run() { if (!runRef)
throw new Error('no run'); return runRef; }
function newCharStats(name, hp) {
return { name, maxHp: hp, hp, block: 0, str: 0, dex: 0, regen: 0, intangible: 0, weak: 0, vulnerable: 0, bleed: 0, burn: 0, poison: 0, alive: true };
}
function newRun(cls) {
const deck = [];
for (let i = 0; i < 5; i++)
deck.push({ uid: (0, utils_1.uid)(), defId: 'strike', upgraded: false });
for (let i = 0; i < 4; i++)
deck.push({ uid: (0, utils_1.uid)(), defId: 'defend', upgraded: false });
const sig = { blood: 'sanguine_strike', shadow: 'shadow_bolt', hell: 'firebolt' };
deck.push({ uid: (0, utils_1.uid)(), defId: sig[cls], upgraded: false });
return {
s: {
cls, hp: 70, maxHp: 70, gold: 99,
deck, relics: [], potions: [],
act: 1, nodeIdx: 0, curNodeId: 'f1c2', map: (0, data_1.buildMap)(1).nodes,
nextEnergyBuff: 0, doubtInHand: false, over: false, victory: false,
},
pendingVial: false,
};
}
function makeEnemy(defId, isMinion = false) {
const def = (0, data_1.getEnemy)(defId);
const act = runRef ? runRef.s.act : 1;
const scale = def.scale || { hp: 1, dmg: 1 };
const hp = def.hp + (act - 1) * Math.round(def.hp * scale.hp * 0.4);
return {
uid: (0, utils_1.uid)(), defId,
stats: newCharStats(def.name, hp),
turnIdx: 0, enraged: false, isMinion, isBoss: false, art: def.art,
};
}
function newCombat(enemyGroup, nodeId, opts) {
const r = run();
const c = {
enemies: [], draw: [], discard: [], exhaust: [], hand: [], powers: [],
energy: 3, energyNextTurn: 0, turn: 0, blockNextTurn: 0,
player: newCharStats('Hero', r.s.hp),
bossRelic: opts?.bossRelic || null, elite: !!opts?.elite, nodeId,
eventGold: opts?.eventGold || 0, strThisTurn: 0, daggerUsed: false, impClawGiven: false,
};
c.player.maxHp = r.s.maxHp;
const group = enemyGroup.length ? enemyGroup : ['skeleton'];
for (const g of group) {
const e = makeEnemy(g);
if (opts?.boss)
e.isBoss = true;
c.enemies.push(e);
}
c.draw = (0, utils_1.shuffle)((0, utils_1.clone)(r.s.deck).map(d => ({ ...d })));
// start-of-combat relics
if (r.s.relics.includes('shadow_cloak'))
c.player.intangible += 1;
if (r.s.relics.includes('cursed_amulet'))
for (const e of c.enemies)
e.stats.weak += 1;
if (r.s.relics.includes('black_blood'))
hpLossCard(c, Math.round(r.s.maxHp * 0.1), 'black_blood');
if (r.s.relics.includes('shadow_amulet'))
c.player.intangible += 1;
if (r.s.relics.includes('philosopher'))
for (const e of c.enemies)
e.stats.str += 1;
if (r.s.relics.includes('demon_horns') && r.s.cls === 'hell')
c.hand.push({ uid: (0, utils_1.uid)(), defId: 'imp_claw', upgraded: false });
return c;
}
// Player HP loss from a card / relic effect (bypasses Block; Intangible reduces to 1).
function hpLossCard(c, raw, src) {
const p = c.player;
let dmg = raw;
if (p.intangible > 0)
dmg = 1;
p.hp -= dmg;
if (src !== 'black_blood') {
const pact = c.powers.find(cd => (0, data_1.getDef)(cd.defId).power?.kind === 'bloodPact');
if (pact)
p.str += (0, data_1.getDef)(pact.defId).nu?.str || 1;
if (run().s.relics.includes('bloodstone'))
p.hp = Math.min(p.maxHp, p.hp + 1);
}
return dmg;
}
function rollReward(kind, c) {
const r = run();
let gold = kind === 'normal' ? (0, utils_1.rint)(10, 20) : kind === 'elite' ? (0, utils_1.rint)(25, 35) : 50;
if (r.s.relics.includes('greedy_imp'))
gold += 10;
if (r.s.relics.includes('ectoplasm'))
gold = 0;
gold += c.eventGold || 0;
const cards = rollCardChoices(kind);
let potion = null;
let potionFull = false;
if (kind === 'boss') {
if (r.s.potions.length < 3)
potion = (0, utils_1.pick)(data_1.POTIONS).id;
else
potionFull = true;
}
else if ((0, utils_1.chance)(kind === 'elite' ? 0.7 : 0.4)) {
if (r.s.potions.length < 3)
potion = (0, utils_1.pick)(data_1.POTIONS).id;
else
potionFull = true;
}
const relics = [];
if (kind === 'elite') {
const pool = data_1.RELICS.filter(x => !x.boss && (!x.cls || x.cls === r.s.cls) && !r.s.relics.includes(x.id));
if (pool.length)
relics.push((0, utils_1.pick)(pool).id);
}
let bossRelicChoices = null;
if (kind === 'boss') {
const pool = data_1.BOSS_RELICS.filter(x => !r.s.relics.includes(x.id));
if (pool.length)
bossRelicChoices = (0, utils_1.shuffle)(pool.map(x => x.id)).slice(0, 3);
}
return { gold, cards, potion, potionFull, relics, bossRelicChoices, eventGold: c.eventGold || 0 };
}
function rollCardChoices(kind) {
const r = run();
const pool = data_1.ALL_CLASS_POOLS[r.s.cls].slice();
const out = [];
const rareBoost = kind === 'elite' ? 0.2 : 0.1;
const pickOne = () => {
if (kind === 'boss' && !out.some(x => x.rarity === 'Rare')) {
const rares = pool.filter(x => x.rarity === 'Rare');
return (0, utils_1.pick)(rares.length ? rares : pool);
}
const roll = Math.random();
let rarity;
if (roll < rareBoost)
rarity = 'Rare';
else if (roll < rareBoost + 0.3)
rarity = 'Uncommon';
else
rarity = 'Common';
let cands = pool.filter(x => x.rarity === rarity);
if (!cands.length)
cands = pool;
return (0, utils_1.pick)(cands);
};
for (let i = 0; i < 3; i++) {
let card = pickOne();
let tries = 0;
while (out.some(x => x.id === card.id) && tries < 20) {
card = pickOne();
tries++;
}
out.push(card);
}
return out;
}
function makeShop() {
const r = run();
const pool = data_1.ALL_CLASS_POOLS[r.s.cls];
const priceOf = (d) => d.rarity === 'Common' ? 50 : d.rarity === 'Uncommon' ? 75 : 150;
const cards = [];
for (let i = 0; i < 3; i++) {
const d = (0, utils_1.pick)(pool);
cards.push({ defId: d.id, price: priceOf(d), sold: false });
}
const cd = (0, utils_1.pick)(data_1.COLORLESS_POOL);
const relicPool = data_1.RELICS.filter(x => !x.boss && (!x.cls || x.cls === r.s.cls));
const relics = (0, utils_1.shuffle)(relicPool).slice(0, 2).map(x => ({ defId: x.id, price: 150, sold: false }));
const potions = (0, utils_1.shuffle)(data_1.POTIONS).slice(0, 3).map(x => ({ defId: x.id, price: 50, sold: false }));
return { cards, colorless: { defId: cd.id, price: 60, sold: false }, relics, potions, removalUsed: false };
}
function rollRelicPool() {
const r = run();
const pool = data_1.RELICS.filter(x => !x.boss && (!x.cls || x.cls === r.s.cls));
return (0, utils_1.shuffle)(pool.map(x => x.id)).slice(0, 3);
}
// deck helpers
function addCardToDeck(defId) {
run().s.deck.push({ uid: (0, utils_1.uid)(), defId, upgraded: false });
}
function removeCardFromDeck(card) {
const d = run().s.deck;
const i = d.findIndex(x => x.uid === card.uid);
if (i >= 0)
d.splice(i, 1);
}
function upgradeCardInDeck(card) {
const d = run().s.deck;
const i = d.findIndex(x => x.uid === card.uid);
if (i >= 0)
d[i].upgraded = true;
}
};
// ── module: src/fx.ts ──
__mods["src/fx.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fxQueue = void 0;
exports.pushFx = pushFx;
exports.clearFx = clearFx;
exports.drainFx = drainFx;
exports.fxQueue = [];
function pushFx(e) { exports.fxQueue.push(e); }
function clearFx() { exports.fxQueue.length = 0; }
function drainFx() {
const out = exports.fxQueue.splice(0, exports.fxQueue.length);
return out;
}
};
// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setCombat = setCombat;
exports.getCombat = getCombat;
exports.aliveEnemies = aliveEnemies;
exports.allTargets = allTargets;
exports.calcPlayerDamage = calcPlayerDamage;
exports.damageEnemy = damageEnemy;
exports.enemyAttack = enemyAttack;
exports.gainPlayerBlock = gainPlayerBlock;
exports.healPlayer = healPlayer;
exports.playableCost = playableCost;
exports.canPlay = canPlay;
exports.playCard = playCard;
exports.startTurn = startTurn;
exports.endTurn = endTurn;
exports.currentIntent = currentIntent;
exports.usePotion = usePotion;
exports.isPlayerDead = isPlayerDead;
exports.allDefeated = allDefeated;
exports.playerStatList = playerStatList;
exports.enemyStatList = enemyStatList;
exports.cardDamagePreview = cardDamagePreview;
exports.intentText = intentText;
exports.intentKind = intentKind;
exports.intentDamage = intentDamage;
const data_1 = require("./data");
const state_1 = require("./state");
const fx_1 = require("./fx");
const utils_1 = require("./utils");
let combat;
function setCombat(c) { combat = c; }
function getCombat() { return combat; }
function aliveEnemies() { return combat.enemies.filter(e => e.stats.hp > 0); }
function allTargets() { return aliveEnemies(); }
function log(text, tone = 'info') {
(0, fx_1.pushFx)({ kind: 'log', text, tone });
}
function float(target, text, tone = 'info') {
(0, fx_1.pushFx)({ kind: 'float', target, text, tone });
}
function sfx(name) { (0, fx_1.pushFx)({ kind: 'sfx', name }); }
// ---------- damage ----------
function calcPlayerDamage(base, enemy) {
const p = combat.player;
let d = base + p.str + combat.strThisTurn;
if (p.weak > 0)
d = Math.floor(d * Math.pow(0.75, p.weak));
if (d < 0)
d = 0;
if (enemy.stats.vulnerable > 0)
d = Math.floor(d * 1.5);
return d;
}
function damageEnemy(enemy, base, opts) {
if (enemy.stats.hp <= 0)
return 0;
let d = calcPlayerDamage(base, enemy);
let blocked = 0;
if (enemy.stats.block > 0) {
blocked = Math.min(enemy.stats.block, d);
enemy.stats.block -= blocked;
d -= blocked;
}
enemy.stats.hp -= d;
if (blocked > 0)
float(enemy.uid, `-${blocked} blk`, 'block');
if (d > 0) {
float(enemy.uid, `-${d}`, 'dmg');
sfx('hit');
}
if (enemy.stats.hp <= 0) {
enemy.stats.hp = 0;
log(`${enemy.stats.name} is defeated!`, 'good');
sfx('death');
}
void opts;
return d;
}
// enemy attacks player (1 hit)
function enemyAttack(enemy, base, multiHits = false) {
const p = combat.player;
let d = base + enemy.stats.str + (enemy.tempStr || 0);
if (d < 0)
d = 0;
let blocked = 0;
if (p.block > 0) {
blocked = Math.min(p.block, d);
p.block -= blocked;
d -= blocked;
}
if (d > 0) {
p.hp -= d;
float('player', `-${d}`, 'dmg');
(0, fx_1.pushFx)({ kind: 'hitPlayer' });
sfx('hit');
}
if (blocked > 0) {
float('player', `-${blocked} blk`, 'block');
sfx('blocked');
}
if (!multiHits && p.hp <= 0) {
p.hp = 0;
sfx('death');
}
return d;
}
function gainPlayerBlock(amount) {
const amt = amount + combat.player.dex;
combat.player.block += amt;
float('player', `+${amt} block`, 'block');
sfx('buff');
return amt;
}
function healPlayer(amount, silent = false) {
const p = combat.player;
const amt = Math.min(amount, p.maxHp - p.hp);
p.hp += amt;
if (!silent) {
float('player', `+${amt}`, 'heal');
sfx('heal');
}
return amt;
}
function applyWeak(enemy, amt) {
if (enemy.stats.hp <= 0)
return;
enemy.stats.weak += amt;
float(enemy.uid, `+${amt} Weak`, 'debuff');
}
function applyVuln(enemy, amt) {
if (enemy.stats.hp <= 0)
return;
enemy.stats.vulnerable += amt;
float(enemy.uid, `+${amt} Vuln`, 'debuff');
}
function applyBleed(enemy, amt) {
if (enemy.stats.hp <= 0)
return;
enemy.stats.bleed += amt;
float(enemy.uid, `+${amt} Bleed`, 'debuff');
if ((0, state_1.run)().s.relics.includes('soul_ember'))
damageEnemy(enemy, 2);
}
function applyBurn(enemy, amt) {
if (enemy.stats.hp <= 0)
return;
enemy.stats.burn += amt;
float(enemy.uid, `+${amt} Burn`, 'debuff');
if ((0, state_1.run)().s.relics.includes('soul_ember'))
damageEnemy(enemy, 2);
}
function applyPoison(enemy, amt) {
if (enemy.stats.hp <= 0)
return;
enemy.stats.poison += amt;
float(enemy.uid, `+${amt} Poison`, 'debuff');
}
function drawCards(n) {
const c = combat;
for (let i = 0; i < n; i++) {
if (c.draw.length === 0) {
if (c.discard.length === 0)
break;
c.draw = (0, utils_1.shuffle)(c.discard.slice());
c.discard = [];
log('Draw pile reshuffled from discard.', 'info');
}
c.hand.push(c.draw.pop());
}
sfx('draw');
}
function addImpClaw(up) {
const def = up ? data_1.IMP_CLAW_UP : data_1.IMP_CLAW;
combat.hand.push({ uid: (0, utils_1.uid)(), defId: def.id, upgraded: false });
log(`${def.name} added to hand.`, 'good');
}
// ---------- playing cards ----------
function playableCost(card) {
const spec = (0, data_1.cardSpec)((0, data_1.getDef)(card.defId), card.upgraded);
const cost = spec.cost;
if (cost === 'X')
return 0;
return cost;
}
function canPlay(card) {
const def = (0, data_1.getDef)(card.defId);
if ((0, data_1.isUnplayable)(def))
return false;
if (specTargetMissing(def))
return false;
const cost = def.cost;
if (cost === 'X')
return true;
return combat.energy >= cost;
}
function specTargetMissing(def) {
return def.target === 'enemy' && allTargets().length === 0;
}
function playCard(card, target) {
const c = combat;
if (!canPlay(card)) {
const dchk = def2(card);
if (typeof dchk.cost === 'number' && c.energy < dchk.cost) {
sfx('error');
return false;
}
if ((0, data_1.isUnplayable)(dchk)) {
sfx('error');
log(`${dchk.name} is unplayable.`, 'bad');
return false;
}
if (specTargetMissing(dchk)) {
sfx('error');
return false;
}
return false;
}
const def = def2(card);
const spec = (0, data_1.cardSpec)(def, card.upgraded);
const n = spec.n;
const dmg = n.dmg ?? 0, blk = n.block ?? 0, weak = n.weak ?? 0, vuln = n.vuln ?? 0, bleed = n.bleed ?? 0, burn = n.burn ?? 0, healA = n.heal ?? 0, costHp = n.costHp ?? 0, energyG = n.energy ?? 0, intang = n.intang ?? 0, reg = n.reg ?? 0, strN = n.str ?? 0;
const ti = c.hand.findIndex(x => x.uid === card.uid);
if (ti < 0)
return false;
c.hand.splice(ti, 1);
if (def.cost !== 'X')
c.energy -= def.cost;
else
c.energy = 0;
// HP cost (before effects; dagger relic)
let hpCost = costHp;
if (hpCost > 0 && !c.daggerUsed && def.type === 'Attack' && (0, state_1.run)().s.relics.includes('sacrificial_dagger')) {
hpCost = Math.max(0, hpCost - 1);
c.daggerUsed = true;
log('Sacrificial Dagger: reduced HP cost by 1.', 'info');
}
if (hpCost > 0) {
const lost = (0, state_1.hpLossCard)(c, hpCost, card.defId);
float('player', `-${lost} HP`, 'dmg');
log(`You lose ${lost} HP.`, 'bad');
}
// Black Blood: playing an Attack heals 1
if (def.type === 'Attack' && (0, state_1.run)().s.relics.includes('black_blood'))
healPlayer(1, true);
log(`${spec.name}${card.upgraded ? '' : ''} played.`, 'info');
// power card goes to power zone
if (def.type === 'Power') {
c.powers.push(card);
sfx('buff');
float('player', `${spec.name}`, 'buff');
if ((0, state_1.run)().s.relics.includes('tome')) {
drawCards(1);
log('Tome of Dark Rituals: draw 1.', 'good');
}
}
else if (def.exhaust) {
c.exhaust.push(card);
sfx('exhaust');
}
else if (def.ethereal) {
c.exhaust.push(card);
}
else {
c.discard.push(card);
}
// ---- effects ----
const targets = def.all ? allTargets() : (target && target.stats.hp > 0 ? [target] : []);
if (def.random) {
const hits = def.randomHit?.hits ?? 1;
for (let i = 0; i < hits; i++) {
const t = (0, utils_1.pick)(allTargets());
damageEnemy(t, dmg);
}
}
else if (dmg > 0 && targets.length) {
for (const t of targets)
damageEnemy(t, dmg);
if (def.id === 'nightmare_lash' && target && target.stats.weak > 0 && target.stats.hp > 0) {
const extra = card.upgraded ? (def.ifWeakExtraUp ?? 9) : (def.ifWeakExtra ?? 7);
log('Weakness intensifies the blow.', 'good');
damageEnemy(target, extra);
}
}
if (blk > 0)
gainPlayerBlock(blk);
if (def.blockOnMissingHp) {
const missing = c.player.maxHp - c.player.hp;
gainPlayerBlock(missing + (card.upgraded ? 3 : 0));
}
if (healA > 0)
healPlayer(healA);
if (weak > 0 && targets.length)
for (const t of targets)
applyWeak(t, weak);
if (def.weakAll)
for (const t of allTargets())
applyWeak(t, card.upgraded ? (def.nu?.weak ?? def.weakAll) : def.weakAll);
if (vuln > 0 && targets.length)
for (const t of targets)
applyVuln(t, vuln);
if (bleed > 0 && targets.length)
for (const t of targets)
applyBleed(t, bleed);
if (def.bleedAll)
for (const t of allTargets())
applyBleed(t, card.upgraded ? (def.nu?.bleed ?? def.bleedAll) : def.bleedAll);
if (burn > 0 && targets.length)
for (const t of targets)
applyBurn(t, burn);
if (strN < 0 && targets.length) {
for (const t of targets) {
t.tempStr = (t.tempStr || 0) + strN;
float(t.uid, `${strN} Str (turn)`, 'debuff');
sfx('debuff');
}
}
if (energyG > 0) {
c.energy += energyG;
float('player', `+${energyG} energy`, 'buff');
sfx('buff');
}
if (def.energyOnPlay) {
c.energy += def.energyOnPlay;
float('player', `+${def.energyOnPlay} energy`, 'buff');
}
if (def.energyNextTurn) {
c.energyNextTurn += def.energyNextTurn;
float('player', `+${def.energyNextTurn} energy next turn`, 'buff');
}
if (def.dexNextTurn) {
c.player.dex += card.upgraded ? (def.dexNextTurnUp ?? def.dexNextTurn) : def.dexNextTurn;
float('player', '+Dex (next turn)', 'buff');
}
if (def.strThisTurn) {
c.strThisTurn += card.upgraded ? (def.nu?.str ?? def.strThisTurn) : def.strThisTurn;
float('player', `+${def.strThisTurn} Str (turn)`, 'buff');
}
if (intang > 0) {
c.player.intangible += intang;
float('player', `+${intang} Intangible`, 'buff');
sfx('buff');
}
if (reg > 0) {
c.player.regen += reg;
float('player', `+${reg} Regen`, 'buff');
sfx('buff');
}
if (def.blockNextTurn) {
c.blockNextTurn += card.upgraded ? (def.blockNextTurnUp ?? def.blockNextTurn) : def.blockNextTurn;
float('player', `+${def.blockNextTurn} Block next turn`, 'buff');
}
if (def.shuffleDazed) {
const d = { uid: (0, utils_1.uid)(), defId: 'dazed', upgraded: false };
if (card.upgraded && def.shuffleDazedDiscard)
c.discard.push(d);
else {
c.draw.push(d);
c.draw = (0, utils_1.shuffle)(c.draw);
}
log('A Dazed card is shuffled into your draw pile.', 'bad');
}
if (def.addImp) {
addImpClaw(card.upgraded);
c.impClawGiven = true;
}
if (def.drawOnPlay)
drawCards(card.upgraded ? (def.nu?.draw ?? def.drawOnPlay) : def.drawOnPlay);
// check combat end
if (allTargets().length === 0)
log('All enemies defeated!', 'good');
return true;
}
function def2(card) { return (0, data_1.getDef)(card.defId); }
// ---------- turns ----------
function startTurn() {
const c = combat;
c.turn += 1;
const p = c.player;
p.bleed = 0;
p.block = 0;
if (c.blockNextTurn > 0) {
p.block += c.blockNextTurn;
log(`Whispering Shroud: gain ${c.blockNextTurn} Block.`, 'good');
c.blockNextTurn = 0;
}
if (p.intangible > 0)
p.intangible -= 1;
// powers
for (const pw of c.powers) {
const k = (0, data_1.getDef)(pw.defId).power?.kind;
if (k === 'soulSiphon') {
c.energy += 1;
const lost = (0, state_1.hpLossCard)(c, 1, 'soul_siphon');
float('player', `-${lost} HP`, 'dmg');
log('Soul Siphon: +1 Energy, lose HP.', 'info');
}
if (k === 'umbral') {
const amt = (0, data_1.getDef)(pw.defId).nu?.weak ?? 1;
for (const e of allTargets())
applyWeak(e, amt);
log(`Umbral Mastery: ${amt} Weak to ALL.`, 'info');
}
if (k === 'hellfire') {
const d = (0, data_1.getDef)(pw.defId).nu?.dmg ?? 3;
for (const e of allTargets())
damageEnemy(e, d);
log(`Hellfire Aura: ${d} damage to ALL.`, 'info');
}
if (k === 'bloodPact') { /* passive */ }
}
// relics
if ((0, state_1.run)().s.relics.includes('bleeding_edge')) {
const t = (0, utils_1.pick)(allTargets());
if (t)
applyBleed(t, 1);
}
if ((0, state_1.run)().s.relics.includes('hourglass') && c.hand.length === 0) {
c.energy += 1;
log('Eternal Hourglass: +1 Energy (empty hand).', 'good');
}
if ((0, state_1.run)().s.relics.includes('cursed_crown') || (0, state_1.run)().s.relics.includes('ectoplasm') || (0, state_1.run)().s.relics.includes('philosopher'))
c.energy += 1;
if ((0, state_1.run)().s.relics.includes('shadow_amulet'))
p.intangible += 1;
c.energy += (0, state_1.run)().s.nextEnergyBuff;
(0, state_1.run)().s.nextEnergyBuff = 0;
if (c.energyNextTurn > 0) {
c.energy += c.energyNextTurn;
c.energyNextTurn = 0;
}
drawCards(5);
(0, state_1.run)().s.doubtInHand = c.hand.some(h => (0, data_1.getDef)(h.defId).id === 'doubt');
sfx('turn');
}
function endTurn() {
const c = combat;
// Doubt
if ((0, state_1.run)().s.doubtInHand) {
const lost = (0, state_1.hpLossCard)(c, 1, 'doubt');
float('player', `-${lost} (Doubt)`, 'dmg');
log('Doubt gnaws at you: lose 1 HP.', 'bad');
}
// Regeneration (end of player turn)
if (c.player.regen > 0) {
healPlayer(c.player.regen, true);
c.player.regen -= 1;
}
// Burn end of player turn
if (c.player.burn > 0) {
c.player.hp -= c.player.burn;
float('player', `-${c.player.burn} Burn`, 'dmg');
c.player.burn = Math.floor(c.player.burn / 2);
sfx('fire');
}
// discard hand
for (const h of c.hand) {
if ((0, data_1.getDef)(h.defId).ethereal || (0, data_1.getDef)(h.defId).exhaust)
c.exhaust.push(h);
else
c.discard.push(h);
}
c.hand = [];
c.player.block = 0;
c.strThisTurn = 0;
(0, state_1.run)().s.doubtInHand = false;
enemyPhase();
}
function enemyPhase() {
const c = combat;
for (const e of c.enemies) {
if (e.stats.hp <= 0)
continue;
// Bleed at start of enemy turn
if (e.stats.bleed > 0) {
e.stats.hp -= e.stats.bleed;
float(e.uid, `-${e.stats.bleed} Bleed`, 'dmg');
e.stats.bleed = Math.floor(e.stats.bleed / 2);
if (e.stats.hp <= 0) {
e.stats.hp = 0;
log(`${e.stats.name} dies to bleeding out!`, 'good');
sfx('death');
continue;
}
}
// poison start
if (e.stats.poison > 0) {
e.stats.hp -= e.stats.poison;
float(e.uid, `-${e.stats.poison} Poison`, 'dmg');
e.stats.poison -= 1;
if (e.stats.hp <= 0) {
e.stats.hp = 0;
log(`${e.stats.name} is poisoned to death!`, 'good');
sfx('death');
continue;
}
}
// burn end of enemy turn
if (e.stats.burn > 0) {
e.stats.hp -= e.stats.burn;
float(e.uid, `-${e.stats.burn} Burn`, 'dmg');
e.stats.burn = Math.floor(e.stats.burn / 2);
if (e.stats.hp <= 0) {
e.stats.hp = 0;
log(`${e.stats.name} burns away!`, 'good');
sfx('death');
continue;
}
}
// duration ticks
if (e.stats.weak > 0)
e.stats.weak -= 1;
if (e.stats.vulnerable > 0)
e.stats.vulnerable -= 1;
if (e.stats.regen > 0) {
e.stats.hp = Math.min(e.stats.maxHp, e.stats.hp + e.stats.regen);
e.stats.regen -= 1;
}
// boss phase 2 checks
if (e.isBoss && e.phase2 && e.stats.hp > 0 && e.stats.hp <= phaseThreshold(e.defId) && !e.enraged) {
e.enraged = true;
e.defId = e.phase2;
e.stats.name = (0, data_1.getEnemy)(e.phase2).name;
e.art = (0, data_1.getEnemy)(e.phase2).art;
log(`${e.stats.name} ENRAGES!`, 'bad');
sfx('summon');
}
if (e.enraged && (e.defId === 'lich2' || e.defId === 'archdemon2'))
e.stats.str += 2;
if (e.enraged && e.defId === 'eldritch2')
e.stats.str += 1;
const intent = currentIntent(e);
doEnemyIntent(e, intent);
e.tempStr = 0;
}
// player death check
if (c.player.hp <= 0) {
c.player.hp = 0;
(0, fx_1.pushFx)({ kind: 'log', text: 'You collapse into darkness...', tone: 'bad' });
}
}
function phaseThreshold(defId) {
if (defId === 'lich')
return 30;
if (defId === 'eldritch')
return 40;
if (defId === 'archdemon')
return 45;
return 0;
}
function currentIntent(e) {
const def = (0, data_1.getEnemy)(e.defId);
let i = e.turnIdx % def.pattern.length;
// Lich summon skip when 2+ minions present
if (e.defId === 'lich') {
const minions = aliveEnemies().filter(x => x.isMinion).length;
if (def.pattern[i].kind === 'summon' && minions >= 2)
i = 1;
}
if (e.defId === 'archdemon' && e.enraged)
i = 0; // phase2 handled by defId swap
return def.pattern[i];
}
function doEnemyIntent(e, it) {
const c = combat;
switch (it.kind) {
case 'attack':
enemyAttack(e, it.dmg || 0);
if (it.weak) {
c.player.weak += it.weak;
float('player', `+${it.weak} Weak`, 'debuff');
sfx('debuff');
}
if (it.vuln) {
c.player.vulnerable += it.vuln;
float('player', `+${it.vuln} Vuln`, 'debuff');
sfx('debuff');
}
if (it.burn) {
c.player.burn += it.burn;
float('player', `+${it.burn} Burn`, 'debuff');
sfx('fire');
}
break;
case 'multi':
for (let h = 0; h < (it.hits || 1); h++)
enemyAttack(e, it.dmg || 0, true);
if (it.weak)
c.player.weak += it.weak;
if (it.burn)
c.player.burn += it.burn;
break;
case 'defend':
e.stats.block += it.block || 0;
float(e.uid, `+${it.block} block`, 'block');
if (it.regen) {
e.stats.regen += it.regen;
float(e.uid, `+${it.regen} Regen`, 'buff');
}
if (it.heal) {
e.stats.hp = Math.min(e.stats.maxHp, e.stats.hp + (it.heal || 0));
float(e.uid, `+${it.heal}`, 'heal');
}
break;
case 'mixed':
e.stats.block += it.block || 0;
float(e.uid, `+${it.block} block`, 'block');
enemyAttack(e, it.dmg || 0);
if (it.weak)
c.player.weak += it.weak;
if (it.vuln)
c.player.vulnerable += it.vuln;
if (it.burn)
c.player.burn += it.burn;
break;
case 'buff':
if (it.str) {
if (e.defId === 'acultist') {
for (const t of aliveEnemies())
t.stats.str += it.str;
float(e.uid, `Empower +${it.str} Str (all)`, 'buff');
}
else {
e.stats.str += it.str;
float(e.uid, `+${it.str} Str`, 'buff');
}
sfx('buff');
}
if (it.block) {
e.stats.block += it.block;
float(e.uid, `+${it.block} block`, 'block');
}
if (it.regen) {
e.stats.regen += it.regen;
float(e.uid, `+${it.regen} Regen`, 'buff');
}
if (it.intang) {
e.stats.intangible += it.intang;
float(e.uid, `+${it.intang} Intangible`, 'buff');
}
break;
case 'debuff':
if (it.weak) {
c.player.weak += it.weak;
float('player', `+${it.weak} Weak`, 'debuff');
sfx('debuff');
}
if (it.vuln) {
c.player.vulnerable += it.vuln;
float('player', `+${it.vuln} Vuln`, 'debuff');
sfx('debuff');
}
if (it.bleed) {
c.player.bleed += it.bleed;
float('player', `+${it.bleed} Bleed`, 'debuff');
sfx('debuff');
}
if (it.poison) {
c.player.poison += it.poison;
float('player', `+${it.poison} Poison`, 'debuff');
sfx('poison');
}
break;
case 'drain':
enemyAttack(e, it.dmg || 0);
if (it.heal) {
e.stats.hp = Math.min(e.stats.maxHp, e.stats.hp + it.heal);
float(e.uid, `+${it.heal}`, 'heal');
}
break;
case 'summon': {
const existing = aliveEnemies().filter(x => x.isMinion).length;
if (it.summon && existing < 2) {
const m = (0, state_1.makeEnemy)(it.summon, true);
c.enemies.push(m);
log(`${e.stats.name} summons ${m.stats.name}!`, 'bad');
sfx('summon');
}
else {
log(`${e.stats.name} cannot summon more.`, 'info');
}
break;
}
case 'vulnSelf':
e.stats.block += it.block || 0;
e.stats.vulnerable += it.vulnSelf || 0;
float(e.uid, `+${it.vulnSelf} Vuln (self)`, 'debuff');
break;
}
e.turnIdx += 1;
}
// ---------- potions ----------
function usePotion(potionId, target) {
const c = combat;
const r = (0, state_1.run)();
const idx = r.s.potions.indexOf(potionId);
if (idx < 0)
return false;
const enemies = allTargets();
switch (potionId) {
case 'blood_vial':
healPlayer(15);
break;
case 'firebomb':
for (const e of enemies)
damageEnemy(e, 10);
break;
case 'dex_tonic':
c.player.dex += 3;
float('player', '+3 Dex', 'buff');
break;
case 'energy_elixir':
c.energy += 2;
float('player', '+2 energy', 'buff');
break;
case 'cursed_brew':
for (const e of enemies)
applyVuln(e, 3);
break;
case 'bleeding_flask':
if (enemies.length) {
const t = target && target.stats.hp > 0 ? target : enemies[0];
applyBleed(t, 6);
}
break;
case 'imp_bottle':
addImpClaw(true);
break;
case 'smoke_bomb': {
r.s.potions.splice(idx, 1);
(0, fx_1.pushFx)({ kind: 'log', text: 'You vanish in a cloud of smoke...', tone: 'info' });
c.escaped = true;
return true;
}
}
r.s.potions.splice(idx, 1);
return true;
}
function isPlayerDead() { return combat.player.hp <= 0; }
function allDefeated() { return allTargets().length === 0; }
function playerStatList() {
const p = combat.player;
const out = [];
if (p.str > 0)
out.push({ id: 'str', label: 'Strength', value: p.str, tone: 'buff' });
if (p.dex > 0)
out.push({ id: 'dex', label: 'Dexterity', value: p.dex, tone: 'buff' });
if (p.regen > 0)
out.push({ id: 'regen', label: 'Regeneration', value: p.regen, tone: 'buff' });
if (p.intangible > 0)
out.push({ id: 'intang', label: 'Intangible', value: p.intangible, tone: 'buff' });
if (p.weak > 0)
out.push({ id: 'weak', label: 'Weak', value: p.weak, tone: 'debuff' });
if (p.vulnerable > 0)
out.push({ id: 'vuln', label: 'Vulnerable', value: p.vulnerable, tone: 'debuff' });
if (p.bleed > 0)
out.push({ id: 'bleed', label: 'Bleed', value: p.bleed, tone: 'debuff' });
if (p.burn > 0)
out.push({ id: 'burn', label: 'Burn', value: p.burn, tone: 'debuff' });
if (p.poison > 0)
out.push({ id: 'poison', label: 'Poison', value: p.poison, tone: 'debuff' });
return out;
}
function enemyStatList(e) {
const s = e.stats;
const out = [];
if (s.str > 0)
out.push({ id: 'str', label: 'Str', value: s.str, tone: 'buff' });
if (s.regen > 0)
out.push({ id: 'regen', label: 'Regen', value: s.regen, tone: 'buff' });
if (s.intangible > 0)
out.push({ id: 'intang', label: 'Intang', value: s.intangible, tone: 'buff' });
if (s.weak > 0)
out.push({ id: 'weak', label: 'Weak', value: s.weak, tone: 'debuff' });
if (s.vulnerable > 0)
out.push({ id: 'vuln', label: 'Vuln', value: s.vulnerable, tone: 'debuff' });
if (s.bleed > 0)
out.push({ id: 'bleed', label: 'Bleed', value: s.bleed, tone: 'debuff' });
if (s.burn > 0)
out.push({ id: 'burn', label: 'Burn', value: s.burn, tone: 'debuff' });
if (s.poison > 0)
out.push({ id: 'poison', label: 'Poison', value: s.poison, tone: 'debuff' });
return out;
}
function cardDamagePreview(card, enemy) {
const def = (0, data_1.getDef)(card.defId);
const spec = (0, data_1.cardSpec)(def, card.upgraded);
if ((spec.n.dmg ?? 0) > 0)
return `~${calcPlayerDamage(spec.n.dmg ?? 0, enemy)}`;
return '';
}
function intentText(e) {
return currentIntent(e).text;
}
function intentKind(e) {
return currentIntent(e).kind;
}
function intentDamage(e) {
const it = currentIntent(e);
if (it.kind === 'attack' || it.kind === 'mixed' || it.kind === 'drain')
return it.dmg || 0;
if (it.kind === 'multi')
return (it.dmg || 0) * (it.hits || 1);
return 0;
}
};
// ── module: src/events.ts ──
__mods["src/events.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVENTS = void 0;
exports.makeCtx = makeCtx;
exports.getEvent = getEvent;
exports.availableEventIds = availableEventIds;
exports.rollEventId = rollEventId;
const state_1 = require("./state");
const data_1 = require("./data");
const utils_1 = require("./utils");
function makeCtx() {
const r = (0, state_1.run)();
return {
hpLoss(n) { const h = Math.min(n, r.s.hp); r.s.hp -= h; return h; },
heal(n) { const a = Math.min(n, r.s.maxHp - r.s.hp); r.s.hp += a; return a; },
addGold(n) { r.s.gold += n; },
spendGold(n) { if (r.s.gold < n)
return false; r.s.gold -= n; return true; },
addPotion(id) { if (r.s.potions.length >= 3)
return false; r.s.potions.push(id); return true; },
addRelic(id) { r.s.relics.push(id); },
randomRareRelic() { const p = data_1.RELICS.filter(x => x.rarity === 'Rare' && !x.boss && (!x.cls || x.cls === r.s.cls)); return (0, utils_1.pick)(p.length ? p : data_1.RELICS.filter(x => !x.boss)).id; },
randomCommonRelic() { const p = data_1.RELICS.filter(x => x.rarity === 'Common' && (!x.cls || x.cls === r.s.cls)); return (0, utils_1.pick)(p.length ? p : data_1.RELICS).id; },
randomPotion() { return (0, utils_1.pick)(data_1.POTIONS).id; },
addCurse(id) { r.s.deck.push({ uid: Math.random().toString(36).slice(2), defId: id, upgraded: false }); },
upgradeRandomCards(n) {
const deck = r.s.deck;
const cands = deck.filter(x => !x.upgraded);
const out = [];
for (let i = 0; i < n && cands.length; i++) {
const c = (0, utils_1.pick)(cands);
c.upgraded = true;
out.push((0, data_1.getDef)(c.defId).name);
cands.splice(cands.indexOf(c), 1);
}
void deck;
return out;
},
nextEnergyPlus() { r.s.nextEnergyBuff += 1; },
addRandomPower() {
const pool = data_1.ALL_CLASS_POOLS[r.s.cls].filter(x => x.type === 'Power');
const d = (0, utils_1.pick)(pool);
r.s.deck.push({ uid: Math.random().toString(36).slice(2), defId: d.id, upgraded: false });
return d.name;
},
removeCurseDeck() {
const before = r.s.deck.length;
r.s.deck = r.s.deck.filter(x => (0, data_1.getDef)(x.defId).type !== 'Curse');
return before - r.s.deck.length;
},
};
}
exports.EVENTS = {
altar: {
id: 'altar', title: 'The Forgotten Altar', art: 'altar',
text: 'A blood-stained altar hums with power. Something ancient watches from the shadows, waiting to see what kind of hero you are.',
choices: [
{ label: 'Offer 15 HP', sub: 'Gain a random Rare relic', result: (ctx) => { const h = ctx.hpLoss(15); const rel = ctx.randomRareRelic(); ctx.addRelic(rel); return `You press your bleeding hand to the altar. ${15 - h >= 0 ? '' : ''}It drinks deep of your life and rewards you with a relic: ${data_1.RELICS.find(x => x.id === rel)?.name}.`; } },
{ label: 'Pray', sub: 'Heal 10 HP, gain a Doubt curse', result: (ctx) => { const h = ctx.heal(10); ctx.addCurse('doubt'); return `Whispers answer your prayer. You feel ${h} vitality return... and a new doubt take root in your mind.`; } },
{ label: 'Leave', sub: 'Nothing happens', result: () => 'You step back from the altar. The humming fades behind you.' },
],
},
peddler: {
id: 'peddler', title: 'Shadowy Peddler', art: 'peddler',
text: 'A hooded figure leans against a dead tree, a satchel bulging with curious goods. "A trade, hero? I never ask twice."',
choices: [
{ label: 'Trade a card', sub: 'Remove a card from your deck, gain 100 Gold', selectCard: { after: (ctx, card) => { (0, state_1.removeCardFromDeck)(card); ctx.addGold(100); return `The peddler takes your ${(0, data_1.getDef)(card.defId).name} and vanishes. Your purse grows by 100 gold.`; } } },
{ label: 'Buy his wares', sub: 'Lose 50 Gold: random Potion + random Common relic', disabled: (ctx) => (0, state_1.run)().s.gold < 50, result: (ctx) => { ctx.spendGold(50); const pot = ctx.randomPotion(); const rel = ctx.randomCommonRelic(); ctx.addPotion(pot); ctx.addRelic(rel); return `You pay 50 gold. The peddler slides you a ${data_1.POTIONS.find(x => x.id === pot)?.name} and a ${data_1.RELICS.find(x => x.id === rel)?.name}.`; } },
{ label: 'Attack', sub: 'Fight the Shadow Thief — +75 Gold on victory', combat: { group: ['shadow_thief'], eventGold: 75 } },
],
},
spring: {
id: 'spring', title: 'Cursed Spring', art: 'spring',
text: 'A spring bubbles with black water that never freezes. The surface reflects a world that is not this one.',
choices: [
{ label: 'Drink', sub: 'Lose 10 HP, remove ALL Curses from your deck', disabled: (ctx) => (0, state_1.run)().s.hp <= 10, result: (ctx) => { ctx.hpLoss(10); const n = ctx.removeCurseDeck(); return `The water burns as it slides down, purging ${n} Curse${n === 1 ? '' : 's'} from your deck. The spring accepts your toll.`; } },
{ label: 'Bottle it', sub: 'Obtain a Cursed Brew potion', result: (ctx) => { const ok = ctx.addPotion('cursed_brew'); return ok ? 'You bottle the dark water. A Cursed Brew is yours.' : 'Your potion satchel is full — the water slips through your fingers and evaporates.'; } },
{ label: 'Leave', sub: 'Nothing happens', result: () => 'The spring bubbles on, indifferent.' },
],
},
dead: {
id: 'dead', title: 'Restless Dead', art: 'ghosts',
text: 'Spectral hands reach from the ground. Their voices are a chorus of the unfinished: "Let us... improve you... just a little more... before the end."',
choices: [
{ label: 'Accept', sub: 'Lose 20 HP, upgrade 2 random cards in your deck', disabled: (ctx) => (0, state_1.run)().s.hp <= 20, result: (ctx) => { ctx.hpLoss(20); const ups = ctx.upgradeRandomCards(2); return ups.length ? `The ghosts swarm your deck and sharpen its edges. Upgraded: ${ups.join(', ')}. The toll is paid in blood.` : `The ghosts find nothing new to change in your deck. The toll is paid in blood anyway.`; } },
{ label: 'Refuse', sub: 'Nothing happens', result: () => 'The hands sink back into the earth. The whispers fade, unimpressed.' },
],
},
gambler: {
id: 'gambler', title: 'Impish Gambler', art: 'imp',
text: 'A grinning imp flips a gold coin that has heads on both sides. "One bet, hero. You can\'t lose. (You can lose.)"',
choices: [
{ label: 'Bet 30 Gold', sub: '50% chance to win 60, else lose it all', disabled: (ctx) => (0, state_1.run)().s.gold < 30, result: (ctx) => { ctx.spendGold(30); if ((0, utils_1.chance)(0.5)) {
ctx.addGold(60);
return 'The coin lands heads-up. The imp hisses and pays 60 gold. "Lucky fool."';
} return 'The coin lands... tails. The imp pockets your 30 gold and laughs in three octaves.'; } },
{ label: 'Wager a card', sub: 'Remove a random card — if a Curse, gain 100 Gold', result: (ctx) => { const d = (0, state_1.run)().s.deck; const card = (0, utils_1.pick)(d); (0, state_1.removeCardFromDeck)(card); const isCurse = (0, data_1.getDef)(card.defId).type === 'Curse'; if (isCurse) {
ctx.addGold(100);
return `You wager your ${(0, data_1.getDef)(card.defId).name}. The imp snatches it up, giddy, and pays 100 gold. Even imps hate curses.`;
} return `You wager your ${(0, data_1.getDef)(card.defId).name}. The imp examines it, shrugs, and keeps it. No gold. Just an empty deck space.`; } },
{ label: 'Leave', sub: 'Nothing happens', result: () => 'The imp waves you off, muttering about unprincipled tourists.' },
],
},
library: {
id: 'library', title: 'Ancient Library', art: 'library',
text: 'Tomes drift open and closed on their own, pages whispering forbidden equations. A reading nook, mercifully, sits undisturbed.',
choices: [
{ label: 'Study', sub: 'Gain 1 Energy in the next combat, lose 5 HP', disabled: (ctx) => (0, state_1.run)().s.hp <= 5, result: (ctx) => { ctx.hpLoss(5); ctx.nextEnergyPlus(); return 'The knowledge sears into your mind, costing 5 HP. Your next battle will burn brighter.'; } },
{ label: 'Read the forbidden page', sub: 'Gain a random Power card, gain a Doubt curse', result: (ctx) => { const name = ctx.addRandomPower(); ctx.addCurse('doubt'); return `You tear the page free. ${name} is yours now — as is the growing doubt of what else it changed in you.`; } },
{ label: 'Leave', sub: 'Nothing happens', result: () => 'The books close their eyes as you go.' },
],
},
};
function getEvent(id) {
return exports.EVENTS[id] || exports.EVENTS.altar;
}
function availableEventIds() { return Object.keys(exports.EVENTS); }
function rollEventId() { return (0, utils_1.pick)(Object.keys(exports.EVENTS)); }
void data_1.COLORLESS_POOL;
void utils_1.shuffle;
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.NODE_COLORS = exports.app = void 0;
exports.showScreen = showScreen;
exports.icon = icon;
exports.nodeIcon = nodeIcon;
exports.nodeLabel = nodeLabel;
exports.classArt = classArt;
exports.enemyArt = enemyArt;
exports.potionIcon = potionIcon;
exports.rarityColor = rarityColor;
exports.cardEl = cardEl;
exports.renderTopbar = renderTopbar;
exports.openDeckPanel = openDeckPanel;
exports.closeDeckPanel = closeDeckPanel;
const data_1 = require("./data");
const state_1 = require("./state");
const audio_1 = require("./audio");
const utils_1 = require("./utils");
exports.app = document.getElementById('app');
const overlays = {};
for (const id of ['menu', 'map', 'combat', 'reward', 'rest', 'shop', 'event', 'gameover', 'victory', 'deckpanel']) {
const el = document.getElementById(id);
overlays[id] = el;
}
function showScreen(name) {
for (const k of Object.keys(overlays))
overlays[k].classList.toggle('hidden', k !== name);
exports.app.dataset.screen = name;
}
// ---------------------------------------------------------------- ICONS (inline svg)
let svgSeq = 1;
const gid = (p) => `${p}${svgSeq++}`;
function icon(name, size = 18) {
const s = (body) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
switch (name) {
case 'skull': return s(`<path d="M12 2a8 8 0 0 0-8 8c0 3 1.6 5.2 4 6.4V20h2v-2h2v2h2v-2h2v2h2v-3.6c2.4-1.2 4-3.4 4-6.4a8 8 0 0 0-8-8z" fill="currentColor" stroke="none"/><circle cx="9" cy="10" r="1.6" fill="#1a1024" stroke="none"/><circle cx="15" cy="10" r="1.6" fill="#1a1024" stroke="none"/>`);
case 'crown': return s(`<path d="M3 8l4 4 5-6 5 6 4-4v9H3z" fill="currentColor" stroke="none"/>`);
case 'flame': return s(`<path d="M12 2c1 4-4 5-4 10a4 4 0 0 0 8 0c0-2-1-3-1-3s3 1 3 5a7 7 0 1 1-14 0C4 8 10 7 12 2z" fill="currentColor" stroke="none"/>`);
case 'bag': return s(`<path d="M8 7V6a4 4 0 0 1 8 0v1h3l-1 14H6L5 7h3z" fill="currentColor" stroke="none"/><circle cx="12" cy="14" r="2" fill="#1a1024" stroke="none"/>`);
case 'dice': return s(`<rect x="4" y="4" width="16" height="16" rx="3" fill="currentColor" stroke="none"/><circle cx="9" cy="9" r="1.4" fill="#1a1024" stroke="none"/><circle cx="15" cy="15" r="1.4" fill="#1a1024" stroke="none"/><circle cx="15" cy="9" r="1.4" fill="#1a1024" stroke="none"/><circle cx="9" cy="15" r="1.4" fill="#1a1024" stroke="none"/>`);
case 'tent': return s(`<path d="M12 4L2 20h20L12 4z" fill="currentColor" stroke="none"/><path d="M12 10l-4 10h8l-4-10z" fill="#1a1024" stroke="none"/>`);
case 'sword': return s(`<path d="M19 3l2 2-9 9 2 2-2 2-2-2-3 3-2-2 3-3-2-2 2-2 2 2 9-9z" fill="currentColor" stroke="none"/>`);
case 'shield': return s(`<path d="M12 2l8 3v6c0 5-3.4 9.4-8 11-4.6-1.6-8-6-8-11V5l8-3z" fill="currentColor" stroke="none"/>`);
case 'heart': return s(`<path d="M12 21C6 16 3 12.5 3 9a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 3.5-3 7-9 12z" fill="currentColor" stroke="none"/>`);
case 'coin': return s(`<circle cx="12" cy="12" r="9" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="5" fill="#1a1024" stroke="none" opacity="0.35"/>`);
case 'energy': return s(`<path d="M13 2L4 14h6l-1 8 9-12h-6l1-8z" fill="currentColor" stroke="none"/>`);
case 'potion': return s(`<path d="M10 2h4v4l3 3v11a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V9l3-3V2z" fill="currentColor" stroke="none"/><path d="M9 12h6v7H9z" fill="#1a1024" stroke="none" opacity="0.4"/>`);
case 'relic': return s(`<path d="M12 2l3 6 6 1-4.5 4.5L18 20l-6-3-6 3 1.5-6.5L3 9l6-1 3-6z" fill="currentColor" stroke="none"/>`);
case 'book': return s(`<path d="M4 4h13a3 3 0 0 1 3 3v13H7a3 3 0 0 1-3-3V4z" fill="currentColor" stroke="none"/><path d="M8 9h8M8 13h8" stroke="#1a1024" stroke-width="1.6"/>`);
case 'weak': return s(`<circle cx="12" cy="12" r="9" fill="none"/><path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="3"/>`);
case 'vuln': return s(`<path d="M12 2l10 18H2L12 2z" fill="currentColor" stroke="none"/>`);
case 'bleed': return s(`<path d="M12 3c3 5 6 8 6 11a6 6 0 1 1-12 0c0-3 3-6 6-11z" fill="currentColor" stroke="none"/>`);
case 'burn': return s(`<path d="M12 2c1 4-4 5-4 10a4 4 0 0 0 8 0c0-2-1-3-1-3s3 1 3 5a7 7 0 1 1-14 0C4 8 10 7 12 2z" fill="currentColor" stroke="none"/>`);
case 'poison': return s(`<circle cx="12" cy="13" r="7" fill="currentColor" stroke="none"/><path d="M9 3h2v4H9zM13 3h2v4h-2z" fill="currentColor" stroke="none"/>`);
case 'intang': return s(`<circle cx="12" cy="12" r="8" fill="none" stroke-dasharray="3 3"/><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/>`);
case 'regen': return s(`<path d="M12 21C6 16 3 12.5 3 9a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 3.5-3 7-9 12z" fill="currentColor" stroke="none"/><path d="M12 8v8M8 12h8" stroke="#1a1024" stroke-width="2"/>`);
case 'block': return s(`<path d="M12 2l8 3v6c0 5-3.4 9.4-8 11-4.6-1.6-8-6-8-11V5l8-3z" fill="currentColor" stroke="none"/>`);
case 'str': return s(`<path d="M6 4h4v6h4V4h4v6a4 4 0 0 1-4 4v6h3v4H11v-4h-3v4H5v-4h3v-6a4 4 0 0 1-2-4V4z" fill="currentColor" stroke="none"/>`);
case 'dex': return s(`<path d="M2 12l7-7 3 3-4 4 4 4 3-3 7 7-2 2-7-7-3 3-4-4-4-4z" fill="currentColor" stroke="none"/>`);
case 'summon': return s(`<circle cx="12" cy="12" r="4" fill="currentColor" stroke="none"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4" stroke="currentColor" stroke-width="2.5"/>`);
default: return s(`<circle cx="12" cy="12" r="8" fill="currentColor" stroke="none"/>`);
}
}
function nodeIcon(type, size = 20) {
switch (type) {
case 'enemy': return icon('skull', size);
case 'elite': return icon('crown', size);
case 'rest': return icon('flame', size);
case 'shop': return icon('bag', size);
case 'event': return icon('dice', size);
case 'boss': return icon('skull', size + 4);
case 'start': return icon('tent', size);
default: return icon('skull', size);
}
}
exports.NODE_COLORS = {
start: '#8a7a5c', enemy: '#a33d3d', elite: '#c98a2b', rest: '#e0562a',
shop: '#c9a22b', event: '#7a4bc9', boss: '#e02a4a',
};
function nodeLabel(type) {
switch (type) {
case 'enemy': return 'Battle';
case 'elite': return 'Elite';
case 'rest': return 'Rest Site';
case 'shop': return 'Shop';
case 'event': return 'Mystery';
case 'boss': return 'BOSS';
case 'start': return 'Start';
}
}
// ---------------------------------------------------------------- SVG ART
const ART_DEFS = (id, c1, c2) => `<defs><linearGradient id="${id}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${c1}"/><stop offset="1" stop-color="${c2}"/></linearGradient></defs>`;
function classArt(cls, big = false) {
const w = big ? 150 : 90;
const id = gid('ca');
const body = big ? '' : '';
switch (cls) {
case 'blood':
return `<svg width="${w}" height="${w * 1.25}" viewBox="0 0 100 125" ${body}>${ART_DEFS(id, '#ff4d5e', '#7a1020')}
<path d="M50 8c-16 0-26 12-26 28 0 10-6 16-8 26 10-4 12 2 14 6h40c2-4 4-10 14-6-2-10-8-16-8-26 0-16-10-28-26-28z" fill="url(#${id})"/>
<path d="M36 40c4 10 4 24 0 36 4-4 8-6 14-6s10 2 14 6c-4-12-4-26 0-36 0 8-6 12-14 12s-14-4-14-12z" fill="#2b0710"/>
<circle cx="42" cy="38" r="4" fill="#ffd9dd"/><circle cx="58" cy="38" r="4" fill="#ffd9dd"/>
<path d="M30 92c10 8 30 8 40 0l6 22c-12 8-40 8-52 0l6-22z" fill="#40121c"/>
<path d="M12 60l14 40 6-30-14-10z" fill="#8a1626"/></svg>`;
case 'shadow':
return `<svg width="${w}" height="${w * 1.25}" viewBox="0 0 100 125">${ART_DEFS(id, '#9a5bff', '#2d1257')}
<path d="M50 6C30 6 22 24 22 40c0 14-10 20-12 34 8-6 16-4 20 2h40c4-6 12-8 20-2-2-14-12-20-12-34 0-16-8-34-28-34z" fill="url(#${id})"/>
<path d="M34 38c2 12 2 26 0 38 4-6 10-8 16-8s12 2 16 8c-2-12-2-26 0-38-4 10-10 14-16 14s-12-4-16-14z" fill="#170a2b"/>
<ellipse cx="42" cy="38" rx="4" ry="6" fill="#e6d4ff"/><ellipse cx="58" cy="38" rx="4" ry="6" fill="#e6d4ff"/>
<path d="M28 92c12 8 32 8 44 0l4 24c-14 8-42 8-52 0l4-24z" fill="#33154f"/>
<circle cx="82" cy="30" r="3" fill="#c9a2ff" opacity="0.7"/><circle cx="20" cy="52" r="2.5" fill="#c9a2ff" opacity="0.7"/></svg>`;
case 'hell':
return `<svg width="${w}" height="${w * 1.25}" viewBox="0 0 100 125">${ART_DEFS(id, '#ff7a2f', '#8a1c07')}
<path d="M28 20C20 30 16 44 22 56c-4 2-6 6-4 10h64c2-4 0-8-4-10 6-12 2-26-6-36-4 8-6 12-8 18 2-10 0-20-4-28-4 12-10 20-20 24-10-4-16-12-20-24-4 8-6 18-4 28-2-6-4-10-8-18z" fill="url(#${id})"/>
<path d="M36 44c2 10 2 22 0 32 4-4 8-6 14-6s10 2 14 6c-2-10-2-22 0-32-2 6-8 10-14 10s-12-4-14-10z" fill="#2b0d05"/>
<circle cx="43" cy="44" r="4" fill="#ffe8b0"/><circle cx="57" cy="44" r="4" fill="#ffe8b0"/>
<path d="M32 90c10 8 26 8 36 0l4 26c-12 8-36 8-48 0l4-26z" fill="#571c07"/>
<path d="M84 20c4 6 4 14 0 20l-6-2c4-6 4-12 2-16l4-2z" fill="#ffb020"/></svg>`;
}
}
const ENEMY_ART = {
skeleton: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#e8e0d0', '#8a8070')}<circle cx="50" cy="34" r="18" fill="#ded6c4"/><path d="M38 44h24v10c0 4-3 6-6 6h-4v4h-4v-4h-4v4h-4v-4h-4c-3 0-6-2-6-6V44z" fill="#ded6c4"/><circle cx="43" cy="32" r="5" fill="#1a1024"/><circle cx="57" cy="32" r="5" fill="#1a1024"/><path d="M30 70h40M34 78h32M38 86h24" stroke="#b0a894" stroke-width="6" stroke-linecap="round"/></svg>`,
zombie: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#7a9a4b', '#3d5724')}<circle cx="50" cy="36" r="20" fill="#6d8a42"/><path d="M36 46h28v12c0 3-2 5-5 5h-18c-3 0-5-2-5-5V46z" fill="#4c6330"/><circle cx="42" cy="32" r="4" fill="#ffd23e"/><circle cx="58" cy="32" r="4" fill="#ffd23e"/><path d="M36 74h28l6 16H30l6-16z" fill="#3d5724"/><path d="M24 60l12 10M76 60l-12 10" stroke="#4c6330" stroke-width="8" stroke-linecap="round"/></svg>`,
cultist: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#5b3d8a', '#1e0f38')}<path d="M50 8C32 8 26 24 26 38c0 12-8 18-10 30 8-4 14-2 18 2h32c4-4 10-6 18-2-2-12-10-18-10-30 0-14-6-30-24-30z" fill="url(#${w === 100 ? 'g' : 'g'})"/><path d="M36 36c2 8 2 18 0 26 4-3 8-5 14-5s10 2 14 5c-2-8-2-18 0-26-6 8-10 10-14 10s-8-2-14-10z" fill="#140a26"/><circle cx="43" cy="36" r="3.5" fill="#e056e0"/><circle cx="57" cy="36" r="3.5" fill="#e056e0"/></svg>`,
rats: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#8a6a4b', '#4b3624')}<ellipse cx="30" cy="40" rx="16" ry="11" fill="#7a5c40"/><ellipse cx="68" cy="52" rx="16" ry="11" fill="#8a6a4b"/><ellipse cx="42" cy="72" rx="16" ry="11" fill="#6a4e34"/><path d="M18 36c-6-2-8-6-6-10M78 48c6-2 8-6 6-10M52 68c6-2 8-6 6-10" stroke="#5a4230" stroke-width="3" fill="none"/><circle cx="24" cy="36" r="2" fill="#ffd23e"/><circle cx="74" cy="48" r="2" fill="#ffd23e"/><circle cx="36" cy="68" r="2" fill="#ffd23e"/></svg>`,
guardian: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#8a8f9a', '#3d424d')}<rect x="30" y="14" width="40" height="34" rx="8" fill="#6d727d"/><rect x="24" y="48" width="52" height="34" rx="8" fill="#565b66"/><circle cx="42" cy="30" r="5" fill="#ffd23e"/><circle cx="58" cy="30" r="5" fill="#ffd23e"/><path d="M36 38h28" stroke="#3d424d" stroke-width="4"/><path d="M22 52l-14 20M78 52l14 20" stroke="#565b66" stroke-width="10" stroke-linecap="round"/><circle cx="14" cy="74" r="12" fill="#8a8f9a"/><path d="M14 64v20M4 74h20" stroke="#3d424d" stroke-width="3"/></svg>`,
witch: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#4b7a5b', '#1e3824')}<path d="M50 8C34 8 28 22 28 36c0 12-8 18-10 30 8-4 14-2 18 2h32c4-4 10-6 18-2-2-12-10-18-10-30 0-14-6-28-26-28z" fill="url(#${'w'})"/><path d="M38 36c2 8 2 18 0 26 4-3 8-5 12-5s8 2 12 5c-2-8-2-18 0-26-4 8-8 10-12 10s-8-2-12-10z" fill="#0f1f14"/><circle cx="44" cy="36" r="3.5" fill="#ff4d5e"/><circle cx="56" cy="36" r="3.5" fill="#ff4d5e"/><path d="M44 48c4 3 8 3 12 0" stroke="#ff4d5e" stroke-width="2" fill="none"/></svg>`,
lich: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#b8aef0', '#3d3572')}<path d="M50 2l10 12-10 8-10-8 10-12zM22 26l8 10-12 2 4-12zM78 26l-8 10 12 2-4-12z" fill="#ffd23e"/><path d="M50 22c-14 0-22 10-22 24 0 10-6 16-8 26 8-4 12-2 14 2h32c2-4 6-6 14-2-2-10-8-16-8-26 0-14-8-24-22-24z" fill="url(#${'l'})"/><path d="M38 44c2 8 2 18 0 26 4-3 8-5 12-5s8 2 12 5c-2-8-2-18 0-26-4 8-8 10-12 10s-8-2-12-10z" fill="#141026"/><circle cx="44" cy="44" r="4" fill="#b8f0ff"/><circle cx="56" cy="44" r="4" fill="#b8f0ff"/></svg>`,
minion: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#cfc8b8', '#7a7264')}<circle cx="50" cy="40" r="15" fill="#cfc8b8"/><path d="M40 48h20v8c0 3-2 5-4 5h-12c-2 0-4-2-4-5v-8z" fill="#cfc8b8"/><circle cx="45" cy="38" r="3.5" fill="#1a1024"/><circle cx="55" cy="38" r="3.5" fill="#1a1024"/><path d="M34 66h32M38 74h24M42 82h16" stroke="#a89c88" stroke-width="5" stroke-linecap="round"/></svg>`,
thief: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#4b5b8a', '#1a2440')}<path d="M50 8C34 8 28 22 28 36c0 12-8 18-10 30 8-4 14-2 18 2h32c4-4 10-6 18-2-2-12-10-18-10-30 0-14-6-28-26-28z" fill="url(#${'t'})"/><path d="M38 36c2 8 2 18 0 26 4-3 8-5 12-5s8 2 12 5c-2-8-2-18 0-26-4 8-8 10-12 10s-8-2-12-10z" fill="#0f1526"/><circle cx="44" cy="36" r="3.5" fill="#5be0ff"/><circle cx="56" cy="36" r="3.5" fill="#5be0ff"/><path d="M78 58l14 14-4 4-14-14 4-4z" fill="#c8d4e8"/></svg>`,
wraith: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#c8d4e8', '#5b6b8a')}<path d="M50 10c-16 0-24 12-24 28v44c4-6 8-8 12-4s8 2 12-2 8-2 12 2 8 6 12 4V38c0-16-8-28-24-28z" fill="url(#${'r'})"/><circle cx="42" cy="36" r="5" fill="#141a26"/><circle cx="58" cy="36" r="5" fill="#141a26"/><path d="M30 26c-6-4-10-10-8-16 6 2 10 6 12 12M70 26c6-4 10-10 8-16-6 2-10 6-12 12" stroke="#8a9ab8" stroke-width="3" fill="none"/></svg>`,
beast: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#5b8a4b', '#24401e')}<path d="M20 30l14-14 10 10h12l10-10 14 14-8 22c2 8 0 18-6 24l-10 14h-20L30 72c-6-6-8-16-6-24l6-18z" fill="url(#${'b'})"/><circle cx="38" cy="42" r="4" fill="#ffd23e"/><circle cx="62" cy="42" r="4" fill="#ffd23e"/><path d="M40 62l6 8 4-6 4 6 6-8" stroke="#14260f" stroke-width="3" fill="none"/><path d="M36 70l-6 8M64 70l6 8" stroke="#e8e0d0" stroke-width="4" stroke-linecap="round"/></svg>`,
horror: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#3d5724', '#141f0a')}<circle cx="50" cy="50" r="28" fill="url(#${'h'})"/><path d="M50 22l6-14 6 14-6-2zM50 78l6 14 6-14-6 2zM22 50l-14-6 14-6 2 6zM78 50l14-6-14-6-2 6zM30 30L18 18l14 2 2 4zM70 30l12-12-14 2-2 4zM30 70L18 82l14-2 2-4zM70 70l12 12-14-2-2-4z" fill="#2d3f1a"/><circle cx="50" cy="46" r="7" fill="#ff2a4d"/><path d="M36 64c8 6 20 6 28 0" stroke="#1a0f0a" stroke-width="4" fill="none"/></svg>`,
shaman: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#7a4b8a', '#2d1540')}<path d="M50 8C36 8 30 20 30 32c0 10-8 16-10 26 8-4 12-2 16 2h28c4-4 8-6 16-2-2-10-10-16-10-26 0-12-6-24-20-24z" fill="url(#${'s'})"/><path d="M38 32c2 6 2 14 0 20 4-2 8-4 12-4s8 2 12 4c-2-6-2-14 0-20-4 6-8 8-12 8s-8-2-12-8z" fill="#170a2b"/><circle cx="44" cy="32" r="3.5" fill="#7affc9"/><circle cx="56" cy="32" r="3.5" fill="#7affc9"/><path d="M78 44v44M70 56h16M70 70h16" stroke="#c8b8a8" stroke-width="5" stroke-linecap="round"/><circle cx="78" cy="40" r="7" fill="#7affc9" opacity="0.85"/></svg>`,
treant: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#7a5230', '#3d2814')}<circle cx="50" cy="26" r="22" fill="#4b6b2d"/><circle cx="34" cy="34" r="14" fill="#5b7d35"/><circle cx="66" cy="34" r="14" fill="#5b7d35"/><rect x="36" y="44" width="28" height="40" rx="6" fill="url(#${'t2'})"/><circle cx="44" cy="58" r="4" fill="#ffb020"/><circle cx="56" cy="58" r="4" fill="#ffb020"/><path d="M44 70c4 3 8 3 12 0" stroke="#2b1a0a" stroke-width="3" fill="none"/><path d="M36 56l-16 12M64 56l16 12" stroke="#5a3a20" stroke-width="8" stroke-linecap="round"/></svg>`,
plague: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#6b8a3d', '#2d4014')}<circle cx="50" cy="40" r="20" fill="#5b7a32"/><path d="M38 50h24v10c0 3-2 5-5 5h-14c-3 0-5-2-5-5V50z" fill="#4a6326"/><circle cx="43" cy="36" r="4" fill="#eaff7a"/><circle cx="57" cy="36" r="4" fill="#eaff7a"/><path d="M42 58h4M52 58h4" stroke="#1a260f" stroke-width="3"/><path d="M30 16c-4-4-4-8 0-12M50 12c-4-4-4-8 0-12M70 16c-4-4-4-8 0-12" stroke="#9aff5e" stroke-width="3" fill="none" opacity="0.6"/></svg>`,
treant_boss: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#8a5a2f', '#402a12')}<circle cx="50" cy="24" r="24" fill="#3d5724"/><circle cx="30" cy="34" r="15" fill="#4b6b2d"/><circle cx="70" cy="34" r="15" fill="#4b6b2d"/><rect x="32" y="40" width="36" height="46" rx="8" fill="url(#${'tb'})"/><circle cx="43" cy="56" r="5" fill="#ff4d5e"/><circle cx="57" cy="56" r="5" fill="#ff4d5e"/><path d="M42 72l8 6 8-6" stroke="#1a0f0a" stroke-width="3" fill="none"/><path d="M32 56l-20 14M68 56l20 14M32 74l-16 16M68 74l16 16" stroke="#5a3a20" stroke-width="9" stroke-linecap="round"/></svg>`,
hound: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#8a2f2f', '#3d1212')}<path d="M24 44l-10-16 16 4 8-10 8 12c14 0 22 8 22 20l10 14-12 2-4 12h-36l-4-12c-6-4-10-10-8-16z" fill="url(#${'h2'})"/><circle cx="40" cy="48" r="4" fill="#ffd23e"/><circle cx="56" cy="48" r="4" fill="#ffd23e"/><path d="M38 64c4 6 10 6 14 0" stroke="#1a0a0a" stroke-width="3" fill="none"/><path d="M40 62l4 6 4-6 4 6 4-6" fill="#e8e0d0"/></svg>`,
sdemon: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#8a3dc9', '#2d0f40')}<path d="M34 20C28 28 26 38 30 46l-12 20 16-6c2 10 8 16 16 16s14-6 16-16l16 6-12-20c4-8 2-18-4-26-4 8-8 12-16 12s-12-4-16-12z" fill="url(#${'sd'})"/><circle cx="43" cy="40" r="3.5" fill="#e0b0ff"/><circle cx="57" cy="40" r="3.5" fill="#e0b0ff"/><path d="M44 52c3 3 9 3 12 0" stroke="#2d0f40" stroke-width="2.5" fill="none"/></svg>`,
imp: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#c94b2f', '#571c07')}<path d="M38 18c-8 8-10 20-4 30l-8 14 12-4c2 8 8 12 12 12s10-4 12-12l12 4-8-14c6-10 4-22-4-30-4 6-8 8-12 8s-8-2-12-8z" fill="url(#${'i'})"/><circle cx="44" cy="36" r="3" fill="#ffd23e"/><circle cx="56" cy="36" r="3" fill="#ffd23e"/><path d="M44 48c3 2 9 2 12 0" stroke="#3d0f07" stroke-width="2" fill="none"/></svg>`,
cultist3: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#6b2f8a', '#240a38')}<path d="M50 8C34 8 28 22 28 36c0 12-8 18-10 30 8-4 14-2 18 2h32c4-4 10-6 18-2-2-12-10-18-10-30 0-14-6-28-26-28z" fill="url(#${'c3'})"/><path d="M38 36c2 8 2 18 0 26 4-3 8-5 12-5s8 2 12 5c-2-8-2-18 0-26-4 8-8 10-12 10s-8-2-12-10z" fill="#140a26"/><circle cx="44" cy="36" r="3.5" fill="#ff4d5e"/><circle cx="56" cy="36" r="3.5" fill="#ff4d5e"/><path d="M50 62l3 6-3 6-3-6 3-6z" fill="#ff4d5e" opacity="0.8"/></svg>`,
flamespawn: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#ff7a2f', '#8a1c07')}<path d="M28 30C22 40 20 52 26 62l-8 16 14-6c4 8 12 12 18 12s14-4 18-12l14 6-8-16c6-10 4-22-2-32-6 8-12 10-20 10s-14-2-20-10z" fill="url(#${'f'})"/><circle cx="42" cy="46" r="4" fill="#ffe8b0"/><circle cx="58" cy="46" r="4" fill="#ffe8b0"/><path d="M40 62c4 6 12 6 16 0l-4 8h-8l-4-8z" fill="#1a0a05"/><path d="M44 66c2 4 2 8 0 12M56 66c-2 4-2 8 0 12" stroke="#ffd23e" stroke-width="3" fill="none"/></svg>`,
dread: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#5b6b8a', '#1a2440')}<path d="M50 6l14 10v14l-6 8H42l-6-8V16l14-10z" fill="url(#${'d'})"/><rect x="34" y="40" width="32" height="34" rx="6" fill="#2d3b57"/><path d="M34 46l-16 26 8 4 12-20zM66 46l16 26-8 4-12-20z" fill="#2d3b57"/><path d="M78 10v64" stroke="#c8d4e8" stroke-width="5" stroke-linecap="round"/><path d="M70 14h16" stroke="#c8d4e8" stroke-width="5" stroke-linecap="round"/><circle cx="44" cy="24" r="3.5" fill="#ff4d5e"/><circle cx="56" cy="24" r="3.5" fill="#ff4d5e"/></svg>`,
archdemon: (w) => `<svg width="${w}" height="${w}" viewBox="0 0 100 100">${ART_DEFS(gid('ea'), '#e02a4a', '#400a14')}<path d="M20 10c2 14 8 22 18 26-4 10-4 22 4 32l-14 22 18-6c4 4 14 4 18 0l18 6-14-22c8-10 8-22 4-32 10-4 16-12 18-26l-16 8c-8-4-16-6-24-6s-16 2-24 6l-16-8z" fill="url(#${'a'})"/><circle cx="40" cy="44" r="5" fill="#ffd23e"/><circle cx="60" cy="44" r="5" fill="#ffd23e"/><path d="M38 62l6 8 6-6 6 6 6-8" stroke="#1a0508" stroke-width="3" fill="none"/></svg>`,
};
function enemyArt(artKey, w) {
const fn = ENEMY_ART[artKey] || ENEMY_ART.skeleton;
return fn(w);
}
function potionIcon(id) {
const colors = {
blood_vial: '#e02a4a', firebomb: '#ff7a2f', dex_tonic: '#5b8aff', energy_elixir: '#ffd23e',
cursed_brew: '#7a4bc9', bleeding_flask: '#a33d3d', imp_bottle: '#c94b2f', smoke_bomb: '#8a9ab8',
};
const col = colors[id] || '#8a9ab8';
return `<svg width="22" height="22" viewBox="0 0 24 24"><path d="M10 2h4v4l3 3v11a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V9l3-3V2z" fill="${col}" stroke="#1a1024" stroke-width="1.2"/><path d="M10 12c1 1 3 1 4 0" stroke="#fff" stroke-width="1" fill="none" opacity="0.5"/></svg>`;
}
// ---------------------------------------------------------------- CARDS
function rarityColor(r) {
switch (r) {
case 'Common': return '#c8c8d0';
case 'Uncommon': return '#5ba8ff';
case 'Rare': return '#e8b83d';
case 'Basic': return '#9a9aa8';
case 'Curse': return '#c94be0';
case 'Status': return '#9a9aa8';
case 'Token': return '#5be08a';
default: return '#c8c8d0';
}
}
function cardEl(card, opts) {
const def = (0, data_1.getDef)(card.defId);
const spec = (0, data_1.cardSpec)(def, card.upgraded);
const el = document.createElement('div');
el.className = 'card' + (opts?.mini ? ' mini' : '') + (opts?.dimmed ? ' dimmed' : '') + (opts?.chosen ? ' chosen' : '');
const cost = opts?.costOverride !== undefined ? opts.costOverride : (spec.cost === 'X' ? 'X' : spec.cost);
const costCls = def.type === 'Attack' ? 'at' : def.type === 'Skill' ? 'sk' : 'pw';
const name = opts?.mini ? spec.name : spec.name;
el.innerHTML = `
<div class="card-cost ${costCls}">${(0, utils_1.esc)(String(cost))}</div>
<div class="card-name">${(0, utils_1.esc)(name)}</div>
<div class="card-type">${def.type}</div>
<div class="card-art">${iconCardArt(def.type, def.id)}</div>
<div class="card-desc">${(0, utils_1.esc)(spec.desc)}</div>
<div class="card-rar" style="color:${rarityColor(def.rarity)}">${def.rarity}</div>
`;
el.dataset.defid = def.id;
if (opts?.onClick)
el.addEventListener('click', opts.onClick);
el.title = `${spec.name} (${def.type}, ${def.rarity})\n${spec.desc}`;
return el;
}
function iconCardArt(type, defId) {
if (type === 'Attack')
return icon('sword', 34);
if (type === 'Skill') {
if (defId.includes('imp') || defId === 'infernal_pact')
return icon('flame', 34);
return icon('shield', 34);
}
if (type === 'Power')
return icon('relic', 34);
if (defId === 'doubt')
return icon('book', 34);
if (defId === 'wound' || defId === 'dazed')
return icon('weak', 34);
return icon('flame', 34);
}
// ---------------------------------------------------------------- TOP BAR
function renderTopbar() {
const bar = document.getElementById('topbar');
const r = (0, state_1.run)();
if (!r) {
bar.innerHTML = '';
return;
}
const relicChips = r.s.relics.map(id => {
const rel = (0, data_1.getRelic)(id);
return `<span class="relic-chip" title="${(0, utils_1.esc)(rel.name)}: ${(0, utils_1.esc)(rel.desc)}">${icon('relic', 15)}</span>`;
}).join('');
const potionChips = r.s.potions.map(id => {
const p = (0, data_1.getPotion)(id);
return `<span class="potion-chip" title="${(0, utils_1.esc)(p.name)}: ${(0, utils_1.esc)(p.desc)}">${potionIcon(id)}</span>`;
}).join('') + `<span class="potion-chip empty" title="Potion slots: ${r.s.potions.length}/3">·</span>`;
bar.innerHTML = `
<div class="tb-left">
<span class="tb-act">${data_1.ACT_NAMES[r.s.act]}</span>
<span class="tb-hp">${icon('heart', 14)} ${r.s.hp}/${r.s.maxHp}</span>
<span class="tb-gold">${icon('coin', 14)} ${r.s.gold}</span>
</div>
<div class="tb-mid">${relicChips}</div>
<div class="tb-right">
${potionChips}
<button id="deckbtn" class="tb-btn" title="View deck (D)">${icon('book', 14)} Deck (${r.s.deck.length})</button>
<button id="mutebtn" class="tb-btn">${document.body.dataset.muted === '1' ? '🔇' : '🔊'}</button>
</div>
`;
document.getElementById('deckbtn').addEventListener('click', () => { audio_1.Sfx.click(); openDeckPanel('browse'); });
document.getElementById('mutebtn').addEventListener('click', () => {
const m = document.body.dataset.muted !== '1';
document.body.dataset.muted = m ? '1' : '0';
Promise.resolve().then(() => __importStar(require('./audio'))).then(a => a.setMuted(m));
renderTopbar();
});
}
// ---------------------------------------------------------------- DECK PANEL
let deckMode = 'browse';
let deckOnPick = null;
function openDeckPanel(mode, onPick) {
deckMode = mode;
deckOnPick = onPick || null;
const titles = {
browse: 'Your Deck', upgrade: 'Forge — choose a card to upgrade', remove: 'Meditate — choose a card to remove',
vial: 'Vial of Shadows — remove a card', shopRemove: 'Remove a card (75g)',
};
const r = (0, state_1.run)();
const panel = overlays.deckpanel;
panel.classList.remove('hidden');
const box = document.getElementById('decklist');
const can = (card) => {
const def = (0, data_1.getDef)(card.defId);
if (deckMode === 'upgrade')
return !card.upgraded && def.rarity !== 'Curse' && def.rarity !== 'Token';
if (deckMode === 'remove' || deckMode === 'vial')
return def.rarity !== 'Curse' ? true : !(r.s.relics.includes('cursed_crown'));
if (deckMode === 'shopRemove')
return !r.s.relics.includes('cursed_crown') ? def.type !== 'Curse' || true : false;
return false;
};
const list = r.s.deck.slice().sort((a, b) => (0, data_1.getDef)(a.defId).name.localeCompare((0, data_1.getDef)(b.defId).name));
box.innerHTML = '';
box.appendChild(document.createElement('div')).className = 'panel-title';
box.lastChild.textContent = titles[mode];
for (const card of list) {
const usable = can(card);
const el = cardEl(card, { mini: true, onClick: usable ? (ev) => { audio_1.Sfx.click(); if (deckOnPick) {
deckOnPick(card);
closeDeckPanel();
}
else if (deckMode === 'upgrade') {
(0, state_1.upgradeCardInDeck)(card);
audio_1.Sfx.buff();
renderTopbar();
openDeckPanel(mode);
}
else if (deckMode === 'remove' || deckMode === 'vial') {
(0, state_1.removeCardFromDeck)(card);
audio_1.Sfx.click();
renderTopbar();
openDeckPanel(mode);
} } : undefined });
if (!usable)
el.classList.add('dimmed');
box.appendChild(el);
}
const close = document.getElementById('deckclose');
close.onclick = () => closeDeckPanel();
}
function closeDeckPanel() { overlays.deckpanel.classList.add('hidden'); deckOnPick = null; }
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>