RPG
GPT 5.6 Luna Pro · 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 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 & 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 • E INTERACT • 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"} -->