← RPG results

RPG

GPT 5.6 Luna Pro · typescript

Initial view of RPG

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 22,434 bytes · SHA-256 de38dd881337
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Emberfall — The Sunken Archive</title>
<style>
/* ── css: game-ui ── */
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@400;600;700&display=swap');
:root{--ink:#e8f0e8;--muted:#8da19a;--teal:#54d6b0;--amber:#ffb65c;--red:#ff6f6b;--bg:#081214}*{box-sizing:border-box}html,body,#app{margin:0;width:100%;height:100%;overflow:hidden;background:var(--bg);font-family:'Space Grotesk',sans-serif;color:var(--ink)}#game-shell{position:relative;width:100%;height:100%;background:#081214}canvas{display:block;width:100%;height:100%;image-rendering:auto}#hud{position:absolute;inset:0;pointer-events:none}.brand{position:absolute;top:24px;left:28px;display:flex;gap:12px;align-items:center;letter-spacing:.12em}.sigil{font-size:31px;color:var(--amber);text-shadow:0 0 18px #ffb65c88}.brand b{display:block;font-size:14px}.brand small{display:block;color:var(--muted);font:10px 'DM Mono';letter-spacing:.08em;margin-top:2px}.stats{position:absolute;top:24px;left:50%;transform:translateX(-50%);display:flex;gap:30px}.stat{min-width:90px}.label{display:block;font:10px 'DM Mono';color:var(--muted);letter-spacing:.12em;margin-bottom:5px}.stat span:not(.label){color:var(--red);letter-spacing:3px;font-size:18px;text-shadow:0 0 10px #ff6f6b44}.stat:nth-child(2) span:not(.label){color:var(--teal);font-size:15px}.stat strong{font-size:20px;color:var(--amber)}.quest{position:absolute;top:24px;right:28px;text-align:right;min-width:240px}.quest strong{display:block;font-size:14px}.quest #hint{display:block;font:10px 'DM Mono';color:var(--muted);margin-top:7px}.panel{position:absolute;right:28px;top:90px;width:285px;background:#0c1b1dF2;border:1px solid #3a6c62;border-radius:10px;padding:18px;box-shadow:0 18px 60px #0008;z-index:5}.hidden{display:none!important}.panel-head{display:flex;justify-content:space-between;font:12px 'DM Mono';letter-spacing:.1em;color:var(--teal)}button{background:none;border:0;color:var(--ink);font-size:24px;cursor:pointer}.muted{font:11px 'DM Mono';color:var(--muted);line-height:1.5}.item{display:flex;align-items:center;gap:11px;padding:10px 0;border-top:1px solid #ffffff12}.item .icon{font-size:22px;width:30px;text-align:center;color:var(--amber)}.item b{font-size:13px}.item small{display:block;color:var(--muted);font:10px 'DM Mono';margin-top:2px}#toast{position:absolute;left:50%;bottom:76px;transform:translateX(-50%);padding:11px 18px;border:1px solid #54d6b088;background:#081b1bf2;border-radius:5px;color:#dff8ee;font:12px 'DM Mono';letter-spacing:.04em;opacity:0;transition:opacity .2s;pointer-events:none;z-index:4;box-shadow:0 8px 30px #0007}#toast.show{opacity:1}.toast-accent{color:var(--amber)}#bossbar{position:absolute;bottom:30px;left:50%;transform:translateX(-50%);width:min(460px,60%);text-align:center;font:11px 'DM Mono';letter-spacing:.18em;color:var(--red);z-index:4}#bossbar div{margin-top:7px;height:7px;background:#31191d;border:1px solid #71343b;border-radius:5px;overflow:hidden}#bossbar i{display:block;height:100%;width:100%;background:var(--red);transition:width .2s}#help{position:absolute;left:28px;bottom:22px;color:#6e8780;font:10px 'DM Mono';letter-spacing:.05em;z-index:3}@media(max-width:700px){.stats{left:auto;right:20px;transform:none;gap:12px}.brand{top:17px;left:16px}.quest{top:92px;right:16px}.panel{right:16px;top:140px}.quest strong{font-size:12px}.stats .stat{min-width:auto}.stats .label{font-size:8px}.stats span:not(.label){font-size:14px!important}.help{left:16px}}
</style>
</head>
<body>
<main id="game-shell"><canvas id="game" width="1280" height="720"></canvas><section id="hud"><div class="brand"><span class="sigil">✦</span><div><b>EMBERFALL</b><small>THE SUNKEN ARCHIVE</small></div></div><div class="stats"><div class="stat"><span class="label">VITALITY</span><span id="hearts">♥♥♥</span></div><div class="stat"><span class="label">AETHER</span><span id="aether">▰▰▰▰▰▰</span></div><div class="stat"><span class="label">EMBER COINS</span><strong id="coins">0</strong></div></div><div class="quest"><span class="label">CURRENT THREAD</span><strong id="quest">Find the Sunken Archive</strong><span id="hint">WASD move · SPACE strike · E interact</span></div></section><aside id="inventory" class="panel hidden"><div class="panel-head"><span>PACK &amp; RELICS</span><button id="close-inv">×</button></div><p class="muted">Press I to close · discovered items persist at the next shrine</p><div id="items"></div></aside><div id="toast"></div><div id="bossbar" class="hidden"><span>THE ARCHIVIST</span><div><i></i></div></div><div id="help">I  PACK&nbsp;&nbsp; • &nbsp;&nbsp;E  INTERACT&nbsp;&nbsp; • &nbsp;&nbsp;SPACE  ATTACK</div></main>
<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 canvas = document.querySelector('#game');
const g = canvas.getContext('2d');
const W = 1280, H = 720;
const keys = new Set();
const p = { x: 170, y: 520, hp: 6, max: 6, coins: 12, weapon: 0, range: false, stamina: 100, inv: ['Copper Band'], hurt: 0, attack: 0, face: 1, walk: 0 };
const enemies = [
    { x: 455, y: 455, hp: 3, max: 3, kind: 'mireling', hit: 0, flash: 0 }, { x: 570, y: 265, hp: 4, max: 4, kind: 'mireling', hit: 0, flash: 0 },
    { x: 820, y: 460, hp: 5, max: 5, kind: 'thornback', hit: 0, flash: 0 }, { x: 1020, y: 300, hp: 6, max: 6, kind: 'thornback', hit: 0, flash: 0 }, { x: 1120, y: 500, hp: 3, max: 3, kind: 'mireling', hit: 0, flash: 0 }
];
const drops = [
    { x: 340, y: 260, kind: 'Coin cache', value: 15, note: 'A forgotten purse of ember coins' },
    { x: 650, y: 560, kind: 'Moonleaf tonic', value: 0, note: 'Restores two vitality' },
    { x: 760, y: 220, kind: 'Amber shard', value: 25, note: 'Weapon upgrade I · searing edge' },
    { x: 920, y: 580, kind: 'Ironwood charm', value: 0, note: 'Weapon upgrade II · heavy cleaver' },
    { x: 1090, y: 180, kind: 'Heart fragment', value: 0, note: 'Adds one heart container' }
];
const walls = [{ x: 0, y: 0, w: 1280, h: 92 }, { x: 0, y: 650, w: 1280, h: 70 }, { x: 0, y: 0, w: 34, h: 720 }, { x: 1246, y: 0, w: 34, h: 720 }, { x: 240, y: 135, w: 25, h: 240 }, { x: 400, y: 90, w: 25, h: 220 }, { x: 690, y: 100, w: 25, h: 260 }, { x: 910, y: 380, w: 25, h: 250 }, { x: 520, y: 500, w: 180, h: 24 }];
let mode = 'intro', progress = 0, boss = false, bossHp = 18, flash = 0, particles = [], floatTexts = [], toastTimer = 0, audio;
const $ = (s) => document.querySelector(s);
function say(t) { const el = $('#toast'); el.innerHTML = t; el.classList.add('show'); toastTimer = 170; }
function sound(freq, dur = .08, type = 'triangle') { try {
    audio ?? (audio = new AudioContext());
    const o = audio.createOscillator(), a = audio.createGain();
    o.type = type;
    o.frequency.value = freq;
    a.gain.value = .028;
    o.connect(a);
    a.connect(audio.destination);
    o.start();
    a.gain.exponentialRampToValueAtTime(.001, audio.currentTime + dur);
    o.stop(audio.currentTime + dur);
}
catch { } }
function burst(x, y, color, n = 10) { for (let i = 0; i < n; i++)
    particles.push({ x, y, color, life: 25 + Math.random() * 25, size: 2 + Math.random() * 3, vx: (Math.random() - .5) * 4, vy: (Math.random() - .5) * 4 }); }
function floatText(x, y, text, color = '#ffb65c') { floatTexts.push({ x, y, text, color, life: 46 }); }
function near(x, y, r = 58) { return Math.hypot(p.x - x, p.y - y) < r; }
function blocked(x, y) { return walls.some(w => x > w.x - 18 && x < w.x + w.w + 18 && y > w.y - 18 && y < w.y + w.h + 18); }
function move() { let dx = 0, dy = 0; if (keys.has('w') || keys.has('ArrowUp'))
    dy--; if (keys.has('s') || keys.has('ArrowDown'))
    dy++; if (keys.has('a') || keys.has('ArrowLeft'))
    dx--; if (keys.has('d') || keys.has('ArrowRight'))
    dx++; if (dx || dy) {
    const l = Math.hypot(dx, dy);
    dx /= l;
    dy /= l;
    p.face = dx || p.face;
    p.walk += .22;
    p.stamina = Math.max(0, p.stamina - .28);
    if (Math.random() < .06)
        sound(105, .025, 'sine');
}
else {
    p.walk *= .8;
    p.stamina = Math.min(100, p.stamina + .45);
} const speed = p.stamina < 15 ? 1.45 : 2.85; const nx = p.x + dx * speed, ny = p.y + dy * speed; if (nx > 55 && nx < 1225 && !blocked(nx, p.y))
    p.x = nx; if (ny > 115 && ny < 630 && !blocked(p.x, ny))
    p.y = ny; }
function attack() { if (p.attack > 0)
    return; p.attack = p.weapon === 2 ? 14 : 18; const reach = p.range ? 135 : p.weapon === 2 ? 78 : 58; const damage = p.weapon === 2 ? 3 : p.weapon === 1 ? 2 : 1; sound(p.range ? 620 : p.weapon === 2 ? 230 : 180, .12); burst(p.x + p.face * 42, p.y, p.range ? '#8bead0' : p.weapon === 1 ? '#ffb65c' : '#d7e8df', p.range ? 10 : 7); for (const e of enemies) {
    if (!e.dead && Math.hypot(e.x - p.x, e.y - p.y) < reach) {
        e.hp -= damage;
        e.hit = 10;
        e.flash = 8;
        floatText(e.x, e.y - 28, '-' + damage, '#ff6f6b');
        burst(e.x, e.y, '#ff6f6b', 12);
        if (e.hp <= 0) {
            e.dead = true;
            p.coins += 8;
            say('<b>THREAT CLEARED</b>  +8 ember coins');
            sound(840, .15);
        }
    }
} if (boss && Math.hypot(1130 - p.x, 380 - p.y) < reach) {
    bossHp -= p.range ? 2 : p.weapon === 2 ? 2 : 1;
    burst(1130, 380, '#ffb65c', 14);
    if (bossHp <= 0) {
        boss = false;
        progress = 6;
        p.coins += 60;
        say('<b>THE ARCHIVIST FALLS</b>  +60 coins · archive recovered');
        $('#quest').textContent = 'Return to the campfire';
        $('#bossbar').classList.add('hidden');
        sound(1100, .3);
    }
} }
function interact() { if (near(125, 550)) {
    save();
    progress = Math.max(progress, 1);
    say('<b>CHECKPOINT MARKED</b>  Your discoveries are safe');
    sound(400, .2);
    return;
} if (near(250, 560)) {
    if (p.coins >= 18 && !p.inv.includes('Sun tonic')) {
        p.coins -= 18;
        p.inv.push('Sun tonic');
        say('<b>SUN TONIC PURCHASED</b>  Press H to drink');
        sound(600, .15);
    }
    else
        say(p.inv.includes('Sun tonic') ? 'The shelf is empty.' : 'The shopkeeper needs 18 ember coins.');
    return;
} for (const d of drops)
    if (!d.got && near(d.x, d.y, 48)) {
        d.got = true;
        if (d.kind === 'Coin cache' || d.kind === 'Amber shard')
            p.coins += d.value;
        else if (d.kind === 'Heart fragment') {
            p.max = Math.min(10, p.max + 2);
            p.hp = p.max;
            progress = Math.max(progress, 5);
        }
        else
            p.inv.push(d.kind);
        floatText(d.x, d.y - 18, d.kind === 'Coin cache' || d.kind === 'Amber shard' ? `+${d.value} COINS` : 'FOUND', '#54d6b0');
        if (d.kind === 'Amber shard') {
            p.weapon = 1;
            progress = Math.max(progress, 2);
            say('<b>EMBER EDGE ACQUIRED</b>  Upgrade I · armor-searing strikes');
        }
        else if (d.kind === 'Ironwood charm') {
            p.weapon = 2;
            p.max = Math.min(10, p.max + 2);
            p.hp = p.max;
            progress = Math.max(progress, 4);
            say('<b>IRONWOOD CLEAVER</b>  Upgrade II · heavy reach');
        }
        else if (d.kind === 'Moonleaf tonic')
            say('<b>MOONLEAF TONIC</b> added to pack · H to restore 2 hearts');
        else if (d.kind === 'Heart fragment')
            say('<b>HEART CONTAINER +1</b>  Vitality increased');
        else
            say('<b>' + d.kind.toUpperCase() + '</b>  +' + d.value + ' ember coins');
        burst(d.x, d.y, '#54d6b0', 18);
        sound(760, .18);
        return;
    } }
function save() { const data = { x: p.x, y: p.y, hp: p.hp, max: p.max, coins: p.coins, weapon: p.weapon, range: p.range, inv: [...p.inv], progress, drops: drops.map(d => !!d.got) }; localStorage.setItem('emberfall-save', JSON.stringify(data)); }
function load() { try {
    const s = JSON.parse(localStorage.getItem('emberfall-save') || 'null');
    if (!s)
        return false;
    Object.assign(p, { x: s.x, y: s.y, hp: s.hp, max: s.max, coins: s.coins, weapon: s.weapon, range: s.range, inv: s.inv });
    progress = s.progress;
    drops.forEach((d, i) => d.got = s.drops?.[i] ?? false);
    return true;
}
catch {
    return false;
} }
function update() {
    if (mode === 'intro') {
        if (keys.has('Enter')) {
            mode = 'play';
            keys.delete('Enter');
            say(load() ? 'CHECKPOINT RESTORED  ·  descend again' : 'Explore the drowned halls. The campfire is your anchor.');
        }
        return;
    }
    move();
    if (keys.has(' ') || keys.has('Space')) {
        attack();
        keys.delete(' ');
        keys.delete('Space');
    }
    if (keys.has('e')) {
        interact();
        keys.delete('e');
    }
    if (keys.has('i')) {
        $('#inventory').classList.toggle('hidden');
        keys.delete('i');
    }
    if (keys.has('h')) {
        const i = p.inv.findIndex(x => x === 'Moonleaf tonic' || x === 'Sun tonic');
        if (i >= 0) {
            p.inv.splice(i, 1);
            p.hp = Math.min(p.max, p.hp + 2);
            say('TONIC RESTORED  +2 vitality');
            sound(520, .15);
        }
        else
            say('No tonic in your pack.');
        keys.delete('h');
    }
    if (keys.has('r')) {
        localStorage.removeItem('emberfall-save');
        location.reload();
    }
    if (p.attack > 0)
        p.attack--;
    if (p.hurt > 0)
        p.hurt--;
    for (const e of enemies) {
        if (e.dead)
            continue;
        e.hit = Math.max(0, e.hit - 1);
        e.flash = Math.max(0, e.flash - 1);
        const d = Math.hypot(p.x - e.x, p.y - e.y);
        if (d < 190 && d > 24) {
            e.x += (p.x - e.x) / d * .32;
            e.y += (p.y - e.y) / d * .32;
        }
        if (d < 30 && e.hit === 0) {
            p.hp--;
            p.hurt = 20;
            flash = 10;
            floatText(p.x, p.y - 30, 'HURT', '#ff6f6b');
            burst(p.x, p.y, '#ff6f6b', 12);
            sound(90, .2, 'sawtooth');
            if (p.hp <= 0) {
                p.hp = p.max;
                p.x = 170;
                p.y = 520;
                floatText(p.x, p.y - 28, 'REVIVED', '#54d6b0');
                say('YOU FALL · awakened at the last campfire');
            }
        }
    }
    if (progress >= 4 && !p.range) {
        p.range = true;
        progress = 5;
        p.inv.push('Aether bow');
        say('<b>MIDPOINT DISCOVERY</b>  Aether bow unlocked · SPACE fires');
        sound(900, .2);
    }
    if (progress >= 5 && !boss && enemies.every(e => e.dead || e.x < 900)) {
        boss = true;
        say('<b>SEALED DOOR OPEN</b>  Defeat the Archivist in the east');
        $('#bossbar').classList.remove('hidden');
    }
    particles = particles.filter(q => q.life-- > 0);
    for (const q of particles) {
        q.x += q.vx;
        q.y += q.vy;
        q.vy += .04;
    }
    floatTexts = floatTexts.filter(t => t.life-- > 0);
    for (const t of floatTexts)
        t.y -= .35;
    if (flash > 0)
        flash--;
    if (toastTimer-- <= 0)
        $('#toast').classList.remove('show');
    hud();
}
function hud() { const full = Math.ceil(p.hp / 2), total = p.max / 2; $('#hearts').textContent = '♥'.repeat(full) + '♡'.repeat(Math.max(0, total - full)); $('#coins').textContent = String(p.coins); const bars = Math.ceil(p.stamina / 17); $('#aether').textContent = '▰'.repeat(bars) + '▱'.repeat(6 - bars); $('#bossbar i').style.width = `${Math.max(0, bossHp / 18 * 100)}%`; $('#items').innerHTML = p.inv.map((x, i) => `<div class="item"><span class="icon">${['◈', '✚', '❖', '⬟', '☽'][i % 5]}</span><div><b>${x}</b><small>${x.includes('tonic') ? 'H to consume' : 'A relic of the drowned halls'}</small></div></div>`).join(''); const q = progress < 2 ? 'Find the Amber Shard' : progress < 4 ? 'Reach the Ironwood Shrine' : progress < 5 ? 'Cross the sealed east gate' : boss ? 'Defeat the Archivist' : progress === 6 ? 'Return to the campfire' : 'Find the Sunken Archive'; if (!boss && progress < 6)
    $('#quest').textContent = q; }
function draw() {
    g.clearRect(0, 0, W, H);
    g.fillStyle = '#081719';
    g.fillRect(0, 0, W, H);
    g.fillStyle = '#10292a';
    g.fillRect(36, 94, 1208, 554);
    g.strokeStyle = '#27534e';
    g.lineWidth = 2;
    g.strokeRect(36, 94, 1208, 554);
    g.fillStyle = '#1d4543';
    for (let x = 55; x < 1220; x += 44)
        for (let y = 110; y < 635; y += 44) {
            g.globalAlpha = .25;
            g.fillRect(x, y, 1, 1);
        }
    g.globalAlpha = 1;
    for (const w of walls) {
        g.fillStyle = '#142c2d';
        g.fillRect(w.x, w.y, w.w, w.h);
        g.strokeStyle = '#2c5650';
        g.strokeRect(w.x, w.y, w.w, w.h);
    }
    g.font = '11px DM Mono';
    g.fillStyle = '#6e9b8e';
    g.fillText('CAMPFIRE', 83, 626);
    g.fillText('TRADER', 226, 626);
    g.fillText('DROWNED HALLS', 770, 125);
    g.fillStyle = '#183b3a';
    g.beginPath();
    g.arc(125, 550, 34, 0, 7);
    g.fill();
    g.strokeStyle = '#54d6b0';
    g.stroke();
    g.fillStyle = '#ffb65c';
    g.beginPath();
    g.moveTo(125, 520);
    g.lineTo(137, 550);
    g.lineTo(125, 570);
    g.lineTo(113, 550);
    g.closePath();
    g.fill();
    g.fillStyle = '#173635';
    g.fillRect(205, 520, 95, 72);
    g.strokeStyle = '#5e8a76';
    g.strokeRect(205, 520, 95, 72);
    g.fillStyle = '#ffb65c';
    g.fillText('SHOP', 235, 610);
    for (const d of drops)
        if (!d.got) {
            g.shadowBlur = 18;
            g.shadowColor = d.kind === 'Heart fragment' ? '#ff6f6b' : '#54d6b0';
            g.fillStyle = d.kind === 'Heart fragment' ? '#ff6f6b' : d.kind === 'Amber shard' ? '#ffb65c' : '#54d6b0';
            g.beginPath();
            g.arc(d.x, d.y, 8 + Math.sin(Date.now() / 300 + d.x) * 2, 0, 7);
            g.fill();
            g.shadowBlur = 0;
            g.fillStyle = '#8da19a';
            g.font = '10px DM Mono';
            g.fillText(d.kind, d.x - 30, d.y - 18);
        }
    for (const e of enemies)
        if (!e.dead) {
            g.fillStyle = e.flash ? '#fff' : e.kind === 'thornback' ? '#a84f63' : '#a15b72';
            g.beginPath();
            g.arc(e.x, e.y, e.kind === 'thornback' ? 21 : 17, 0, 7);
            g.fill();
            if (e.kind === 'thornback') {
                g.strokeStyle = '#ffb65c';
                g.stroke();
            }
            g.fillStyle = '#201c29';
            g.fillRect(e.x - 14, e.y - 27, 28, 4);
            g.fillStyle = '#ff6f6b';
            g.fillRect(e.x - 14, e.y - 27, 28 * e.hp / e.max, 4);
        }
    if (boss) {
        g.fillStyle = '#753c63';
        g.beginPath();
        g.arc(1130, 380, 38, 0, 7);
        g.fill();
        g.strokeStyle = '#ffb65c';
        g.lineWidth = 3;
        g.stroke();
        g.lineWidth = 1;
        g.fillStyle = '#ffb65c';
        g.font = '11px DM Mono';
        g.fillText('ARCHIVIST', 1090, 430);
    }
    g.save();
    g.translate(p.x, p.y);
    g.scale(p.face < 0 ? -1 : 1, 1);
    g.globalAlpha = p.hurt % 4 < 2 ? .5 : 1;
    const bob = Math.sin(p.walk) * 2;
    g.fillStyle = '#73c7b0';
    g.beginPath();
    g.arc(0, bob, 17, 0, 7);
    g.fill();
    g.fillStyle = '#d5ece2';
    g.fillRect(-10, -22 + bob, 20, 7);
    g.fillStyle = p.weapon === 2 ? '#c68b55' : p.weapon === 1 ? '#ffb65c' : '#bddbd0';
    g.rotate(p.attack > 0 ? -.65 : 0);
    g.fillRect(10, -3 + bob, p.range ? 35 : p.weapon === 2 ? 34 : 28, 5);
    if (p.range) {
        g.strokeStyle = '#8bead0';
        g.beginPath();
        g.arc(25, -1 + bob, 12, -1.1, 1.1);
        g.stroke();
    }
    g.restore();
    g.globalAlpha = 1;
    for (const q of particles) {
        g.globalAlpha = q.life / 45;
        g.fillStyle = q.color;
        g.fillRect(q.x, q.y, q.size, q.size);
    }
    g.globalAlpha = 1;
    for (const t of floatTexts) {
        g.globalAlpha = Math.min(1, t.life / 18);
        g.fillStyle = t.color;
        g.font = 'bold 13px DM Mono';
        g.textAlign = 'center';
        g.fillText(t.text, t.x, t.y);
        g.textAlign = 'left';
    }
    g.globalAlpha = 1;
    if (flash) {
        g.fillStyle = `rgba(255,70,80,${flash / 35})`;
        g.fillRect(0, 0, W, H);
    }
    if (mode === 'intro') {
        g.fillStyle = '#081214e8';
        g.fillRect(0, 0, W, H);
        g.fillStyle = '#ffb65c';
        g.font = 'bold 46px Space Grotesk';
        g.fillText('EMBERFALL', 90, 230);
        g.fillStyle = '#e8f0e8';
        g.font = '23px Space Grotesk';
        g.fillText('The Sunken Archive', 94, 270);
        g.fillStyle = '#8da19a';
        g.font = '14px DM Mono';
        g.fillText('A compact adventure of steel, secrets, and salvage.', 96, 315);
        g.fillStyle = '#54d6b0';
        g.font = '14px DM Mono';
        g.fillText(localStorage.getItem('emberfall-save') ? 'PRESS ENTER TO CONTINUE' : 'PRESS ENTER TO DESCEND', 96, 380);
        g.fillStyle = '#6e8780';
        g.fillText('WASD MOVE   ·   SPACE ATTACK   ·   E INTERACT   ·   I PACK', 96, 420);
    }
}
function loop() { update(); draw(); requestAnimationFrame(loop); }
addEventListener('keydown', e => { keys.add(e.key); if ([' ', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key))
    e.preventDefault(); });
addEventListener('keyup', e => keys.delete(e.key));
$('#close-inv').addEventListener('click', () => $('#inventory').classList.add('hidden'));
hud();
loop();
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-5.6-luna-pro","provider":"openrouter","persona":"typescript","sessionId":"57fe263e-6b4a-4042-a481-4c5cc74e2a8d","tokensIn":833851,"tokensOut":11752,"tokensTotal":845603,"turns":19,"toolCalls":24,"failedToolCalls":2,"timestamp":"2026-07-31T03:36:44.323Z"} -->