Shadows
GPT 5.6 terra · typescript

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 25,880 bytes · SHA-256 8322497760a2
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shadows of the Abyss</title>
<style>
/* ── css: core ── */
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600;700;800&family=Inter:wght@400;500;600;700&display=swap');
:root{--bg:#090b13;--panel:#101421;--panel2:#151a2a;--line:#30374d;--text:#e7e4dc;--muted:#9297a6;--gold:#dfb969;--red:#cf5266;--purple:#9276c9;--blue:#5f9bc9;--green:#73b58a;--ink:#05060b}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 50% -20%,#26213f 0%,#0b0d16 47%,#05060a 100%);color:var(--text);font-family:Inter,Arial,sans-serif;min-height:100vh}body:before{content:"";position:fixed;inset:0;pointer-events:none;opacity:.35;background-image:linear-gradient(rgba(255,255,255,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.012) 1px,transparent 1px);background-size:28px 28px}button{font:inherit;color:inherit;cursor:pointer}#game{position:relative;min-height:100vh}#topbar{height:76px;padding:0 5.5vw;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #292d3d;background:rgba(9,11,19,.84);backdrop-filter:blur(12px)}.brand{display:flex;align-items:center;gap:13px}.brand-mark{font-size:28px;color:var(--gold);text-shadow:0 0 20px #db9a4d}h1{font:700 17px Cinzel,serif;letter-spacing:2px;margin:0}h1 em{font-style:normal;color:#a98ad8}#subtitle{margin:4px 0 0;color:var(--muted);font-size:11px;letter-spacing:.5px}#run-stats{display:flex;align-items:center;gap:18px;font-size:13px}.stat{display:flex;gap:7px;align-items:center;color:#c8cad0}.stat b{color:var(--text)}.stat .heart{color:var(--red)}.stat .coin{color:var(--gold)}#stage{width:min(1180px,94vw);margin:0 auto;padding:42px 0 70px}#toast{position:fixed;top:92px;left:50%;transform:translateX(-50%);padding:10px 16px;background:#1e2435;border:1px solid #49506a;border-radius:5px;font-size:13px;opacity:0;transition:.25s;pointer-events:none;z-index:30}.screen-title{text-align:center;font:700 30px Cinzel,serif;letter-spacing:2px;margin:10px 0;color:#f2ede2}.screen-lead{max-width:620px;text-align:center;color:var(--muted);margin:0 auto 32px;font-size:14px;line-height:1.7}.choice-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;max-width:1010px;margin:auto}.class-card,.panel{background:linear-gradient(145deg,rgba(26,31,48,.98),rgba(13,16,28,.98));border:1px solid #343b52;box-shadow:0 15px 50px rgba(0,0,0,.3)}.class-card{min-height:375px;padding:26px;position:relative;text-align:left;overflow:hidden;transition:.2s}.class-card:hover{transform:translateY(-5px);border-color:#9078bf}.class-card:after{content:"";position:absolute;width:190px;height:190px;border-radius:50%;right:-58px;bottom:-72px;background:radial-gradient(circle,var(--accent),transparent 68%);opacity:.13}.class-icon{font-size:53px;color:var(--accent);filter:drop-shadow(0 0 13px var(--accent));height:72px}.class-card h2{font:700 20px Cinzel,serif;letter-spacing:1px;margin:16px 0 8px}.class-card p{font-size:13px;color:var(--muted);line-height:1.65;min-height:64px}.tag{display:inline-block;font-size:10px;letter-spacing:1px;color:var(--accent);border:1px solid color-mix(in srgb,var(--accent),transparent 45%);padding:4px 7px;margin:9px 0 18px}.deck-mini{border-top:1px solid #33394d;padding-top:15px;color:#babdca;font-size:12px;line-height:1.8}.deck-mini b{color:#eee}.btn{border:1px solid #5b6380;background:#252c41;padding:10px 16px;border-radius:3px;font-weight:700;font-size:12px;letter-spacing:.5px;transition:.15s}.btn:hover{background:#363f5c;border-color:#8794bf}.btn.primary{background:linear-gradient(135deg,#6d4b9b,#473467);border-color:#a887d2}.btn.danger{background:#572d3a;border-color:#a75062}.btn.gold{background:#624d29;border-color:#b9934f}.btn:disabled{opacity:.4;cursor:not-allowed}.class-card .btn{width:100%;position:absolute;bottom:24px;left:26px;width:calc(100% - 52px)}
@media(max-width:760px){#topbar{padding:0 3vw}.choice-grid{grid-template-columns:1fr}.class-card{min-height:280px}#stage{padding-top:22px}.hide-mobile{display:none}}
/* ── css: game ── */
.map-wrap{display:grid;grid-template-columns:250px 1fr 250px;gap:18px;align-items:start}.side-card{padding:17px}.side-card h3,.panel h3{font:700 13px Cinzel,serif;letter-spacing:1px;color:#c9b493;margin:0 0 14px}.relic-row{padding:9px 0;border-top:1px solid #292f42;font-size:12px;color:#c5c9d4}.relic-row span{color:var(--gold);margin-right:7px}.map-panel{min-height:570px;padding:28px;position:relative}.act-label{text-align:center;color:#a894c8;font:600 12px Cinzel,serif;letter-spacing:2px}.map-title{text-align:center;font:700 24px Cinzel,serif;margin:6px 0 25px}.map-list{display:flex;flex-direction:column-reverse;gap:7px;max-width:540px;margin:auto}.map-row{display:flex;justify-content:center;gap:10px;min-height:37px;position:relative}.map-row:after{content:"";position:absolute;height:12px;width:1px;background:#56607a;bottom:-12px;left:50%;z-index:0}.map-row:first-child:after{display:none}.node{z-index:1;width:38px;height:34px;border:1px solid #4e5773;background:#1b2031;display:grid;place-items:center;clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);font-size:14px;color:#bdc2d3;transition:.15s}.node.available{cursor:pointer;border-color:#c69d58;color:#f0cc7c;background:#453824;box-shadow:0 0 14px #bd873e77}.node.available:hover{transform:scale(1.18)}.node.current{color:#fff;background:#6b4a94;border-color:#c7a1e9;box-shadow:0 0 15px #9366cd}.node.done{opacity:.35}.legend{display:flex;justify-content:center;gap:12px;margin-top:22px;font-size:11px;color:#8f96aa}.legend span{white-space:nowrap}.potion{border:1px solid #4a526c;background:#1a2031;padding:8px;margin-bottom:7px;font-size:12px;color:#d6d8df}.potion button{float:right;border:0;background:transparent;color:var(--gold);font-size:11px}.combat{display:grid;grid-template-columns:1fr 310px;gap:19px}.battlefield{min-height:487px;padding:24px;position:relative;background:radial-gradient(ellipse at 50% 55%,#30283c 0%,#151927 46%,#0d101a 100%)}.enemies{display:flex;gap:15px;justify-content:center;align-items:flex-start;min-height:180px}.enemy{width:165px;text-align:center;padding:12px 9px;border:1px solid #3e4256;background:rgba(25,22,34,.72);position:relative}.enemy.target{border-color:#d8b36c;box-shadow:0 0 14px #b8844244}.enemy.dead{opacity:.22}.enemy-icon{font-size:47px;filter:drop-shadow(0 0 8px #8d5c9e)}.unit-name{font:700 12px Cinzel,serif;margin:4px 0 7px}.hpbar{height:7px;background:#292b37;border:1px solid #44495a}.hpbar i{display:block;height:100%;background:linear-gradient(90deg,#b43d51,#e46d75)}.tiny{font-size:10px;color:#aeb2c0;margin-top:4px}.intent{margin:8px auto 0;background:#252b3e;border:1px solid #4a5169;padding:6px;font-size:11px;color:#d7d9e0}.player-zone{position:absolute;bottom:18px;left:24px;right:24px;border-top:1px solid #3a4053;padding-top:12px}.player-summary{display:flex;align-items:center;justify-content:space-between;font-size:13px}.energy{font-weight:700;color:#8bbcf2}.block{color:#76a8d5}.statusline{font-size:11px;color:#c8afdb;margin-top:6px}.hand{display:flex;gap:8px;overflow-x:auto;padding:15px 3px 3px}.card{flex:0 0 125px;min-height:169px;border:1px solid #606980;background:linear-gradient(160deg,#252c3c,#141827);padding:10px;position:relative;text-align:left;transition:.15s}.card:hover:not(.disabled){transform:translateY(-9px);border-color:#e3c578}.card.attack{border-top:4px solid #bc5664}.card.skill{border-top:4px solid #5d91c5}.card.power{border-top:4px solid #a37cca}.card.disabled{opacity:.52}.cost{position:absolute;top:8px;right:8px;border-radius:50%;width:22px;height:22px;background:#325a87;display:grid;place-items:center;font-weight:700;font-size:12px}.card-name{font:700 11px Cinzel,serif;padding-right:22px}.card-type{font-size:9px;color:#a7aebd;text-transform:uppercase;letter-spacing:.8px;margin:5px 0 13px}.card-desc{font-size:11px;line-height:1.45;color:#d6d8df}.combat-side{display:flex;flex-direction:column;gap:14px}.turn-box{padding:19px}.turn-box .btn{width:100%;margin-top:13px}.log{padding:16px;height:300px;overflow:auto}.log p{margin:0;padding:7px 0;border-bottom:1px solid #292e3d;font-size:11px;color:#afb5c5}.log p:first-letter{color:#d4ab61}.modal{max-width:720px;margin:20px auto;padding:28px}.offer-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.offer{min-height:195px;padding:15px;text-align:left;border:1px solid #434b62;background:#191e2d}.offer h3{margin:0;color:#ede2cb}.offer p{font-size:12px;color:#c6cad4;line-height:1.55}.modal-actions{text-align:center;margin-top:18px;display:flex;gap:10px;justify-content:center}.rest-actions{display:grid;grid-template-columns:repeat(3,1fr);gap:13px}.rest-actions .btn{padding:24px 12px;min-height:110px}.selection-list{max-height:290px;overflow:auto;display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:16px}.select-card{padding:10px;background:#202638;border:1px solid #424b63;text-align:left;font-size:12px}.select-card:hover{border-color:#d0a967}.event-text{color:#bfc3cf;line-height:1.7;max-width:580px;margin:15px auto 24px}.shop-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.shop-item{padding:13px;border:1px solid #3c455d;background:#1a1f2e;min-height:130px}.shop-item h4{margin:0 0 8px;font:600 13px Cinzel,serif}.shop-item p{font-size:11px;color:#b9bfcd;line-height:1.4}.shop-item .btn{padding:7px 9px;font-size:11px}@media(max-width:900px){.map-wrap,.combat{grid-template-columns:1fr}.side-card{display:none}.offer-grid,.shop-grid{grid-template-columns:1fr}.battlefield{min-height:455px}.card{flex-basis:115px}}
</style>
</head>
<body>
<div id="game"><header id="topbar"><div class="brand"><span class="brand-mark">✦</span><div><h1>SHADOWS <em>OF THE ABYSS</em></h1><p id="subtitle">A cursed ascent awaits</p></div></div><div id="run-stats"></div></header><main id="stage"></main><div id="toast" aria-live="polite"></div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {};
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";
Object.defineProperty(exports, "__esModule", { value: true });
const $ = (s) => document.querySelector(s);
const stage = $('#stage'), stats = $('#run-stats'), toast = $('#toast');
const icons = { Enemy: '⚔', Elite: '♛', Event: '?', Shop: '♜', Rest: '✚', Boss: '☠' };
const layout = ['Start', 'Enemy', 'Enemy', 'Event', 'Shop', 'Enemy', 'Rest', 'Enemy', 'Elite', 'Event', 'Enemy', 'Elite', 'Enemy', 'Rest', 'Boss'];
let cls, hp = 70, maxHp = 70, gold = 99, act = 1, floor = 0, deck = [], relics = [], potions = [], tempEnergy = 0;
let enemies = [], hand = [], draw = [], discard = [], exhaust = [], energy = 3, block = 0, strength = 0, dex = 0, intangible = 0, powers = [], logs = [], target = 0, combatType = '', rewardGold = 0;
const base = (n) => n === 'Strike' ? { id: crypto.randomUUID(), name: 'Strike', type: 'Attack', cost: 1, desc: 'Deal 6 damage.' } : { id: crypto.randomUUID(), name: 'Defend', type: 'Skill', cost: 1, desc: 'Gain 5 Block.' };
const pools = {
'Blood Reaper': [c('Sanguine Strike', 'Attack', 1, 'Deal 8 damage. Heal 2 HP.'), c('Hemorrhage', 'Attack', 1, 'Deal 5 damage. Apply 3 Bleed.'), c('Bloodletting', 'Attack', 1, 'Lose 3 HP. Deal 12 damage.'), c('Crimson Shield', 'Skill', 1, 'Gain 6 Block. Lose 2 HP.'), c('Blood Ward', 'Skill', 1, 'Gain 3 Block. Gain 1 Regeneration.'), c('Life Tap', 'Skill', 0, 'Draw 2 cards. Lose 3 HP.'), c('Rend Flesh', 'Skill', 1, 'Apply 5 Bleed to ALL enemies. Lose 2 HP.'), c('Soul Siphon', 'Power', 2, 'Each turn: +1 Energy, lose 1 HP.'), c('Blood Pact', 'Power', 1, 'HP loss from cards grants Strength.')],
'Shadowmancer': [c('Shadow Bolt', 'Attack', 1, 'Deal 6 damage. Apply 1 Weak.'), c('Grasp of Despair', 'Attack', 1, 'Deal 4 damage. Apply 1 Vulnerable.'), c('Nightmare Lash', 'Attack', 2, 'Deal 7 damage. If Weak, deal 7 more.'), c('Cloak of Shadows', 'Skill', 1, 'Gain 5 Block. Draw 1 card.'), c('Haunting Mist', 'Skill', 1, 'Gain 7 Block. Apply 1 Weak to ALL enemies.'), c('Phantom Armor', 'Skill', 1, 'Gain 8 Block. Next turn gain 2 Dexterity.'), c('Shadowform', 'Skill', 2, 'Gain 2 Intangible. Exhaust.', true), c('Dark Ritual', 'Skill', 0, 'Gain 2 Energy. Add Dazed to draw pile.'), c('Umbral Mastery', 'Power', 2, 'Each turn: apply 1 Weak to ALL enemies.')],
'Hellforged': [c('Firebolt', 'Attack', 1, 'Deal 7 damage. Apply 2 Burn.'), c('Hellfire Blast', 'Attack', 2, 'Deal 9 damage to ALL enemies.'), c('Searing Strike', 'Attack', 1, 'Deal 6 damage. Next turn +1 Energy.'), c('Immolate', 'Attack', 2, 'Deal 12 damage. Exhaust.', true), c('Obsidian Skin', 'Skill', 1, 'Gain 7 Block.'), c('Flame Ward', 'Skill', 1, 'Gain 5 Block. Apply 2 Burn.'), c('Demonic Resilience', 'Skill', 1, 'Gain 4 Block. Heal 2 HP.'), c('Summon Imp', 'Skill', 1, 'Add an Imp Claw to hand. Exhaust.', true), c('Infernal Pact', 'Skill', 0, 'Lose 2 HP. Gain 2 Energy. Add Imp Claw.'), c('Soul Burn', 'Skill', 1, 'Apply 8 Burn. Draw 1 card.'), c('Hellfire Aura', 'Power', 2, 'Each turn: deal 3 damage to ALL enemies.')]
};
function c(name, type, cost, desc, exhaust = false) { return { id: crypto.randomUUID(), name, type, cost, desc, rarity: 'Common', exhaust }; }
function shuffle(a) { return [...a].sort(() => Math.random() - .5); }
function log(x) { logs.unshift(x); logs = logs.slice(0, 18); }
function say(x) { toast.textContent = x; toast.style.opacity = '1'; setTimeout(() => toast.style.opacity = '0', 1600); }
function top() { stats.innerHTML = cls ? `<span class="stat"><span class="heart">♥</span><b>${hp}/${maxHp}</b></span><span class="stat"><span class="coin">●</span><b>${gold}</b> Gold</span><span class="stat">Act <b>${act}</b> · Floor <b>${floor || 1}</b></span>` : ''; }
function startScreen() { stage.innerHTML = `<section><h2 class="screen-title">CHOOSE YOUR CURSE</h2><p class="screen-lead">Three fallen paths descend into the Crypts. Your deck is your covenant; every choice becomes another shadow.</p><div class="choice-grid">${[['Blood Reaper', '♜', 'Sacrifice vitality for brutal power, bleed and forbidden recovery.', '#c85d66'], ['Shadowmancer', '☽', 'Twist enemy intent with weakness, vulnerability and spectral wards.', '#9980cf'], ['Hellforged', '♨', 'Channel hellfire, burning enemies and calling impish claws.', '#dc8656']].map(x => `<article class="class-card" style="--accent:${x[3]}"><div class="class-icon">${x[1]}</div><h2>${x[0]}</h2><span class="tag">70 HP · 3 ENERGY</span><p>${x[2]}</p><div class="deck-mini"><b>Starting deck</b><br>5 × Strike · 4 × Defend<br>1 × signature card · 99 Gold</div><button class="btn primary" data-class="${x[0]}">BEGIN DESCENT</button></article>`).join('')}</div></section>`; document.querySelectorAll('[data-class]').forEach(b => b.addEventListener('click', () => begin(b.dataset.class))); }
function begin(x) { cls = x; deck = [...Array(5)].map(() => base('Strike')).concat([...Array(4)].map(() => base('Defend')), [structuredClone(pools[cls][0])]); top(); map(); }
function map() { top(); let rows = layout.map((kind, i) => ({ kind, i })); stage.innerHTML = `<div class="map-wrap"><aside class="panel side-card"><h3>RELICS</h3>${relics.length ? relics.map(r => `<div class="relic-row"><span>✦</span>${r}</div>`).join('') : '<div class="relic-row">No relics claimed</div>'}<h3 style="margin-top:22px">POTIONS</h3>${potions.map((p, i) => `<div class="potion">${p}<button data-pot="${i}">Use</button></div>`).join('') || '<div class="relic-row">Empty vials</div>'}</aside><section class="panel map-panel"><div class="act-label">ACT ${act}</div><div class="map-title">${act === 1 ? 'THE CRYPTS' : act === 2 ? 'BLIGHTED WOODS' : 'ABYSSAL SPIRE'}</div><div class="map-list">${rows.map(r => { let avail = r.i === floor + 1; let done = r.i <= floor; if (r.i === 0)
return `<div class="map-row"><div class="node ${done ? 'done' : ''}">⌂</div></div>`; let opts = (r.kind === 'Elite' && avail) ? ['Elite', 'Enemy'] : [r.kind]; return `<div class="map-row">${opts.map(k => `<button class="node ${avail ? 'available' : ''} ${r.i === floor ? 'current' : ''} ${done ? 'done' : ''}" ${avail ? `data-node="${k}"` : ''} title="${k}">${icons[k] || '⌂'}</button>`).join('')}</div>`; }).join('')}</div><div class="legend"><span>⚔ Enemy</span><span>♛ Elite</span><span>? Event</span><span>♜ Shop</span><span>✚ Rest</span></div></section><aside class="panel side-card"><h3>THE RUN</h3><div class="relic-row">Deck <b>${deck.length}</b> cards</div><div class="relic-row">Defeat the Act III Archdemon to escape.</div><div class="relic-row" style="color:#aa91d2">${cls}</div></aside></div>`; document.querySelectorAll('[data-node]').forEach(b => b.addEventListener('click', () => visit(b.dataset.node))); document.querySelectorAll('[data-pot]').forEach(b => b.addEventListener('click', () => { potions.splice(+b.dataset.pot, 1); say('Potions can be used in combat.'); map(); })); }
function visit(kind) { floor++; if (kind === 'Enemy' || kind === 'Elite' || kind === 'Boss')
combat(kind);
else if (kind === 'Rest')
rest();
else if (kind === 'Shop')
shop();
else
eventScreen(); }
function combat(kind) { const names = kind === 'Boss' ? [act === 1 ? 'Lich King' : act === 2 ? 'Eldritch Treant' : 'Archdemon'] : kind === 'Elite' ? [act === 1 ? 'Tomb Guardian' : act === 2 ? 'Treant Protector' : 'Dread Knight'] : [act === 1 ? 'Skeleton Warrior' : act === 2 ? 'Wraith' : 'Hellhound']; const max = kind === 'Boss' ? (act === 1 ? 65 : act === 2 ? 75 : 85) : kind === 'Elite' ? 48 : 28; let eh = max, eb = 0, turn = 0, hand = shuffle(deck).slice(0, 5), energy = 3, pblock = 0, target = 0; const render = () => { top(); stage.innerHTML = `<div class="combat"><section class="panel battlefield"><div class="enemies"><button class="enemy target"><div class="enemy-icon">${kind === 'Boss' ? '♛' : '☠'}</div><div class="unit-name">${names}</div><div class="hpbar"><i style="width:${Math.max(0, eh / max * 100)}%"></i></div><div class="tiny">${Math.max(0, eh)}/${max} HP ${eb ? ' · ' + eb + ' Block' : ''}</div><div class="intent">Intent: ${turn % 3 === 2 ? 'Defend 8' : 'Attack ' + (kind === 'Boss' ? 12 : kind === 'Elite' ? 9 : 7)}</div></button></div><div class="player-zone"><div class="player-summary"><span><b>YOU</b> · ♥ ${hp}/${maxHp} <span class="block">${pblock ? ' ◈ ' + pblock : ''}</span></span><span class="energy">◆ ${energy} ENERGY</span></div><div class="hand">${hand.map((x, i) => `<button class="card ${x.type.toLowerCase()} ${energy < x.cost ? 'disabled' : ''}" data-card="${i}"><b class="cost">${x.cost}</b><div class="card-name">${x.name}</div><div class="card-type">${x.type}</div><div class="card-desc">${x.desc}</div></button>`).join('')}</div></div></section><aside class="combat-side"><section class="panel turn-box"><h3>YOUR TURN</h3><div class="tiny">Play cards, then endure the enemy intent.</div><button class="btn danger" id="end">END TURN</button></section><section class="panel log"><h3>BATTLE LOG</h3><p>Battle in the abyss.</p><p>Defeat your foe to claim a card.</p></section></aside></div>`; document.querySelectorAll('[data-card]').forEach(b => b.addEventListener('click', () => { let i = +b.dataset.card, x = hand[i]; if (!x || energy < x.cost)
return; energy -= x.cost; hand.splice(i, 1); let damage = x.name.includes('Strike') ? x.name === 'Sanguine Strike' ? 8 : 6 : x.name === 'Firebolt' ? 7 : x.name === 'Shadow Bolt' ? 6 : x.name === 'Bloodletting' ? 12 : x.name === 'Immolate' ? 12 : 0; if (x.name === 'Defend' || x.name.includes('Shield') || x.name === 'Obsidian Skin')
pblock += x.name === 'Defend' ? 5 : 7; if (x.name === 'Bloodletting') {
hp -= 3;
damage = 12;
} if (x.name === 'Sanguine Strike')
hp = Math.min(maxHp, hp + 2); if (x.name === 'Demonic Resilience')
hp = Math.min(maxHp, hp + 2); if (x.name === 'Hellfire Blast')
damage = 9; if (damage) {
let d = Math.max(0, damage - eb);
eb = Math.max(0, eb - damage);
eh -= d;
} if (eh <= 0)
reward(kind);
else
render(); })); $('#end').onclick = () => { if (turn % 3 === 2)
eb = 8;
else {
let d = kind === 'Boss' ? 12 : kind === 'Elite' ? 9 : 7;
d = Math.max(0, d - pblock);
hp -= d;
} pblock = 0; turn++; if (hp <= 0) {
gameOver();
return;
} hand = shuffle(deck).slice(0, 5); energy = 3; render(); }; }; render(); }
function reward(kind) { let gain = kind === 'Boss' ? 50 : kind === 'Elite' ? 30 : 15; gold += gain; if (kind === 'Elite')
relics.push(['Bloodstone', 'Cursed Amulet', 'Ashen Armor'][Math.floor(Math.random() * 3)]); if (kind === 'Boss') {
if (act === 3) {
victory();
return;
}
act++;
floor = 0;
hp = maxHp;
} let offers = shuffle(pools[cls]).slice(0, 3); stage.innerHTML = `<section class="panel modal"><h2 class="screen-title">SPOILS OF BATTLE</h2><p class="screen-lead">+${gain} Gold · Choose a card or skip.</p><div class="offer-grid">${offers.map((x, i) => `<button class="offer" data-offer="${i}"><h3>${x.name}</h3><p>${x.desc}</p></button>`).join('')}</div><div class="modal-actions"><button class="btn" id="skip">SKIP</button></div></section>`; document.querySelectorAll('[data-offer]').forEach(b => b.addEventListener('click', () => { deck.push(structuredClone(offers[+b.dataset.offer])); map(); })); $('#skip').onclick = map; }
function rest() { stage.innerHTML = `<section class="panel modal"><h2 class="screen-title">A MOMENT OF STILLNESS</h2><p class="screen-lead">Choose one ritual.</p><div class="rest-actions"><button class="btn gold" id="heal">REST<br><small>Heal 17 HP</small></button><button class="btn primary" id="forge">FORGE<br><small>Upgrade a card</small></button><button class="btn" id="remove">MEDITATE<br><small>Remove a card</small></button></div></section>`; $('#heal').onclick = () => { hp = Math.min(maxHp, hp + 17); map(); }; $('#forge').onclick = () => pick('Forge a card', x => { x.name += '+'; x.desc = x.desc.replace(/\d+/, m => String(+m + 2)); map(); }); $('#remove').onclick = () => pick('Remove a card', x => { deck.splice(deck.indexOf(x), 1); map(); }); }
function pick(title, fn) { stage.innerHTML = `<section class="panel modal"><h2 class="screen-title">${title}</h2><div class="selection-list">${deck.map((x, i) => `<button class="select-card" data-pick="${i}"><b>${x.name}</b><br><small>${x.desc}</small></button>`).join('')}</div></section>`; document.querySelectorAll('[data-pick]').forEach(b => b.addEventListener('click', () => fn(deck[+b.dataset.pick]))); }
function shop() { let wares = shuffle(pools[cls]).slice(0, 3); stage.innerHTML = `<section class="panel modal"><h2 class="screen-title">THE UMBRAL MARKET</h2><p class="screen-lead">Gold: ${gold}</p><div class="shop-grid">${wares.map((x, i) => `<div class="shop-item"><h4>${x.name}</h4><p>${x.desc}</p><button class="btn gold" data-buy="${i}">BUY · 50g</button></div>`).join('')}<div class="shop-item"><h4>Card Removal</h4><p>Purge a card for 75 Gold.</p><button class="btn" id="purge">PURGE</button></div></div><div class="modal-actions"><button class="btn" id="leave">LEAVE</button></div></section>`; document.querySelectorAll('[data-buy]').forEach(b => b.addEventListener('click', () => { if (gold < 50) {
say('Not enough gold.');
return;
} gold -= 50; deck.push(structuredClone(wares[+b.dataset.buy])); shop(); })); $('#purge').onclick = () => { if (gold < 75) {
say('Not enough gold.');
return;
} gold -= 75; pick('Purge a card', x => { deck.splice(deck.indexOf(x), 1); map(); }); }; $('#leave').onclick = map; }
function eventScreen() { stage.innerHTML = `<section class="panel modal" style="text-align:center"><h2 class="screen-title">THE FORGOTTEN ALTAR</h2><p class="event-text">A blood-stained altar hums with power beneath the dust.</p><div class="rest-actions"><button class="btn danger" id="offer">OFFER 15 HP<br><small>Gain a relic</small></button><button class="btn primary" id="pray">PRAY<br><small>Heal 10 HP, gain Doubt</small></button><button class="btn" id="leave">LEAVE</button></div></section>`; $('#offer').onclick = () => { hp -= 15; relics.push('Shadow Cloak'); map(); }; $('#pray').onclick = () => { hp = Math.min(maxHp, hp + 10); deck.push(c('Doubt', 'Curse', 99, 'Unplayable. End turn lose 1 HP.')); map(); }; $('#leave').onclick = map; }
function gameOver() { stage.innerHTML = `<section class="panel modal" style="text-align:center"><h2 class="screen-title">THE ABYSS CLAIMS ANOTHER</h2><p class="event-text">Your cursed ascent ends in darkness.</p><button class="btn primary" id="again">BEGIN ANEW</button></section>`; $('#again').onclick = () => location.reload(); }
function victory() { stage.innerHTML = `<section class="panel modal" style="text-align:center"><div class="class-icon" style="color:#dfb969">✦</div><h2 class="screen-title">THE ABYSS YIELDS</h2><p class="event-text">The Archdemon falls silent. You endured the final shadow.</p><button class="btn primary" id="again">BEGIN ANEW</button></section>`; $('#again').onclick = () => location.reload(); }
startScreen();
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-5.6-terra","provider":"openrouter","persona":"typescript","sessionId":"eff1ced6-5fe3-4739-9e8e-1ca8b20cd6e6","tokensIn":497907,"tokensOut":14309,"tokensTotal":512216,"turns":22,"toolCalls":21,"failedToolCalls":1,"timestamp":"2026-07-31T17:58:23.852Z"} -->