← DOOM results

DOOM

nex agi Nex N2.5 mini GGUF · typescript

Initial view of DOOM

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 37,840 bytes · SHA-256 38baaff40555
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Doom</title>
<style>
/* ── css: game ── */
:root {
  --bg: #090b12;
  --panel: #111722;
  --panel-2: #171f2c;
  --line: #2b364a;
  --ink: #d9e8ed;
  --dim: #708291;
  --cyan: #59f1ff;
  --green: #9cff9c;
  --yellow: #ffd45a;
  --red: #ff4964;
  --purple: #c678ff;
  --shadow: #000;
  --mono: "Courier New", Courier, monospace;
}

* { box-sizing: border-box; }
html, body { width: 100%; min-width: 320px; height: 100%; margin: 0; background: radial-gradient(circle at 50% 20%, #121b27 0%, var(--bg) 63%); color: var(--ink); font-family: var(--mono); }
body { min-height: 100%; overflow: hidden; }
button { font: inherit; }

#game-shell { width: 100%; min-height: 100%; display: flex; flex-direction: column; background: #090b12; }
.topbar { height: 54px; flex: 0 0 54px; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; border-bottom: 1px solid #253147; background: linear-gradient(90deg, #101623, #111825 45%, #0d131f); box-shadow: 0 4px 18px #0009; letter-spacing: .13em; }
.brand { display: flex; gap: 10px; align-items: center; color: #eef8f6; font-size: 15px; font-weight: 900; text-transform: uppercase; text-shadow: 0 0 12px #59f1ff77; }
.brand-mark { width: 12px; height: 12px; border: 2px solid var(--cyan); transform: rotate(45deg); box-shadow: 0 0 12px var(--cyan), inset 0 0 5px var(--cyan); }
.brand-sub { color: var(--dim); font-size: 11px; font-weight: 500; }
.top-actions { display: flex; align-items: center; gap: 18px; color: var(--dim); font-size: 11px; letter-spacing: .15em; }
#fps-counter { color: #6d7d8c; min-width: 48px; text-align: right; }
#pause-button { border: 1px solid #33435b; background: #131b28; color: #a9bac6; padding: 6px 11px; font-size: 10px; letter-spacing: .12em; cursor: pointer; }
#pause-button:hover { border-color: var(--cyan); color: var(--cyan); box-shadow: 0 0 13px #59f1ff33; }

.play-area { position: relative; flex: 1 1 auto; min-height: 0; display: grid; place-items: center; padding: 22px 26px 14px; }
#game-canvas { width: min(100%, 960px); aspect-ratio: 3 / 2; max-height: 100%; object-fit: contain; display: block; border: 2px solid #2d3a4c; background: #070a10; box-shadow: 0 0 0 5px #0e1420, 0 12px 42px #000b, 0 0 38px #59f1ff11; image-rendering: pixelated; }
#game-canvas::after { content: ""; position: fixed; pointer-events: none; inset: 0; z-index: 3; background: repeating-linear-gradient(0deg, transparent 0 3px, #00000020 4px); mix-blend-mode: multiply; opacity: .38; }
#game-canvas { position: relative; }
#game-canvas::before { content: ""; position: absolute; z-index: 2; pointer-events: none; inset: 0; background: radial-gradient(ellipse at center, transparent 57%, #0007 100%); box-shadow: inset 0 0 44px #000; }

.overlay { position: absolute; z-index: 4; inset: 0; display: grid; place-items: center; padding: 24px; background: radial-gradient(circle at 50% 45%, #172336 0%, #090d16 62%, #05070c 100%); }
.overlay::before { content: ""; position: absolute; inset: 0; opacity: .17; background-image: linear-gradient(#59f1ff22 1px, transparent 1px), linear-gradient(90deg, #59f1ff18 1px, transparent 1px); background-size: 38px 38px; transform: perspective(600px) rotateX(58deg) scale(1.5) translateY(16%); transform-origin: center 80%; }
.overlay-card { position: relative; width: min(580px, 90%); padding: 36px 42px 32px; border: 1px solid #344358; background: #0d1420ee; box-shadow: 0 0 0 5px #080c13, 0 0 48px #59f1ff22, inset 0 0 30px #59f1ff10; }
.overlay-card::before, .overlay-card::after { content: ""; position: absolute; width: 16px; height: 16px; border-color: var(--cyan); }
.overlay-card::before { top: -5px; left: -5px; border-top: 2px solid; border-left: 2px solid; }
.overlay-card::after { right: -5px; bottom: -5px; border-right: 2px solid; border-bottom: 2px solid; }
.overlay-kicker { color: var(--cyan); font-size: 10px; letter-spacing: .22em; margin-bottom: 14px; }
.overlay h1 { margin: 0 0 18px; color: #eafffa; font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif; font-size: clamp(62px, 11vw, 104px); line-height: .76; letter-spacing: -.025em; text-transform: uppercase; text-shadow: 4px 4px 0 #d44f81, 8px 8px 0 #2a1642, 0 0 20px #59f1ff66; }
.overlay h1 em { color: var(--yellow); font-style: normal; text-shadow: 4px 4px 0 #7c3d18, 8px 8px 0 #26180e, 0 0 22px #ffd45a88; }
.overlay-copy { max-width: 430px; margin: 0 0 24px; color: #91a2ae; font-size: 12px; line-height: 1.65; letter-spacing: .08em; }
.overlay-copy span { color: var(--green); text-shadow: 0 0 10px #9cff9c88; }
.overlay-controls { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-bottom: 25px; color: #8293a0; font-size: 10px; letter-spacing: .09em; }
.overlay-controls b { color: #e5f2f1; }
.start-button { width: 100%; padding: 12px 18px; border: 2px solid var(--yellow); background: #4d3a16; color: #fff4c3; cursor: pointer; box-shadow: 5px 5px 0 #6d3f0b, 0 0 20px #ffd45a33; transition: transform .12s, box-shadow .12s, background .12s; }
.start-button span { float: right; font-weight: 900; }
.start-button:hover { background: #ffcf3d; color: #201500; transform: translate(2px, 2px); box-shadow: 3px 3px 0 #6d3f0b; }

.toast { position: absolute; z-index: 5; top: 17px; left: 50%; transform: translate(-50%, -12px); padding: 8px 14px; border: 1px solid #506077; background: #0b111bdd; color: #c6d4db; opacity: 0; pointer-events: none; font-size: 10px; letter-spacing: .14em; transition: opacity .18s, transform .18s; }
.toast.show { opacity: 1; transform: translate(-50%, 0); }

.hud { flex: 0 0 auto; min-height: 78px; display: grid; grid-template-columns: minmax(145px, 1fr) minmax(170px, 1.25fr) minmax(160px, 1fr) minmax(260px, 1.45fr); align-items: stretch; gap: 8px; padding: 9px 14px 11px; background: linear-gradient(#101623, #0c111b); border-top: 2px solid #2c3a50; box-shadow: 0 -4px 16px #0008; }
.hud-panel { display: flex; flex-direction: column; justify-content: center; min-width: 0; padding: 7px 12px; border: 1px solid #253147; background: #111824cc; }
.hud-label { margin-bottom: 5px; color: var(--dim); font-size: 9px; letter-spacing: .19em; }
.stat-row { display: flex; align-items: center; gap: 10px; }
#health-stat { color: var(--red); font-size: 27px; font-weight: 900; line-height: 1; text-shadow: 0 0 8px #ff496488; }
.meter { width: min(160px, 28vw); height: 9px; border: 1px solid #40506a; background: #06090e; box-shadow: inset 0 0 7px #000; }
.meter i { display: block; width: 100%; height: 100%; background: linear-gradient(90deg, #ff3652, #ff8a55 45%, #9cff9c 75%); box-shadow: 0 0 9px #ff496488; transition: width .18s; }
.health-icon { color: var(--red); font-size: 20px; text-shadow: 0 0 10px var(--red); }
.weapon-name { color: #f2fbf8; font-size: 13px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ammo-row { display: flex; align-items: baseline; gap: 4px; margin-top: 2px; }
#ammo-count { color: var(--cyan); font-size: 27px; font-weight: 900; text-shadow: 0 0 9px #59f1ff88; }
.ammo-total { color: #617281; font-size: 12px; }
.weapon-hint { margin-top: 3px; color: #607182; font-size: 9px; letter-spacing: .12em; }
.objective-stat { color: var(--green); font-size: 12px; font-weight: 900; letter-spacing: .08em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.objective-stat.dim { color: #627282; font-weight: 500; }
.hud-controls { display: flex; align-items: center; justify-content: flex-end; gap: 7px; color: #6f8090; font-size: 9px; letter-spacing: .1em; white-space: nowrap; overflow: hidden; }
.key { display: inline-block; min-width: 22px; padding: 3px 5px; margin-left: 6px; border: 1px solid #344358; color: #c0ccd2; text-align: center; font-size: 8px; background: #090e17; }

@media (max-width: 940px) {
  .topbar { padding: 0 12px; }
  .play-area { padding: 12px 12px 8px; }
  .hud { grid-template-columns: repeat(3, 1fr); }
  .hud-controls { grid-column: 1 / -1; justify-content: flex-start; padding-top: 5px; border-top: 1px solid #202b3c; }
  .hud-controls span:not(.key) { display: none; }
}
@media (max-width: 620px) {
  .brand-sub, #fps-counter { display: none; }
  .overlay-card { padding: 26px 22px 23px; }
  .overlay-copy { font-size: 10px; }
  .hud { grid-template-columns: 1fr 1fr; }
  .objective-panel, .hud-controls { grid-column: 1 / -1; }
}
</style>
</head>
<body>
<div id="app"><div id="game-shell">
  <header class="topbar">
    <div class="brand"><span class="brand-mark"></span><span>NEON&nbsp;ABYSS</span><span class="brand-sub">/ RAYCASTER</span></div>
    <div class="top-actions"><span id="fps-counter"></span><button id="pause-button" type="button">II PAUSE</button></div>
  </header>
  <main class="play-area">
    <canvas id="game-canvas" width="960" height="640" aria-label="Retro raycaster game viewport"></canvas>
    <div id="screen-overlay" class="overlay">
      <div class="overlay-card">
        <div class="overlay-kicker">CLASSIFIED OPERATION // SECTOR 7G</div>
        <h1>NEON<br><em>ABYSS</em></h1>
        <p class="overlay-copy">THE STATIC IS WATCHING. FIND THE <span>EXIT GATE</span> BEFORE IT FINDS YOU.</p>
        <div class="overlay-controls"><span><b>WASD</b> MOVE</span><span><b>MOUSE</b> LOOK</span><span><b>LEFT CLICK</b> FIRE</span><span><b>RMB / Q</b> ROCKETS</span></div>
        <button id="start-button" class="start-button" type="button">INSERT COIN <span>▶ START</span></button>
      </div>
    </div>
    <div id="toast" class="toast" aria-live="polite"></div>
  </main>
  <footer class="hud" aria-label="player status">
    <div class="hud-panel health-panel"><div class="hud-label">VITALS</div><div class="stat-row"><span id="health-stat">100</span><div class="meter"><i id="health-bar"></i></div></div><div id="health-icon" class="health-icon">♥</div></div>
    <div class="hud-panel ammo-panel"><div class="hud-label">ARMORY</div><div id="weapon-name" class="weapon-name">PLASMA PISTOL</div><div class="ammo-row"><span id="ammo-count">240</span><span class="ammo-total">/ <span id="ammo-total">999</span></span></div><div id="weapon-hint" class="weapon-hint">click / space to fire</div></div>
    <div class="hud-panel objective-panel"><div class="hud-label">MISSION</div><div id="level-stat" class="objective-stat">LEVEL 01 // KILL 0/0</div><div id="exit-stat" class="objective-stat dim">EXIT GATE UNKNOWN</div></div>
    <div class="hud-controls"><span class="key">1</span> PLASMA <span class="key">2</span> CHAINSAW <span class="key">3</span> ROCKET <span class="key">SPACE</span> FIRE</div>
  </footer>
</div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.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;
const game_1 = require("./game");
function start() {
    (0, game_1.startGame)();
}
start();
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startGame = startGame;
const app = document.querySelector('#app');
const canvas = document.querySelector('#game-canvas');
const ctx = canvas.getContext('2d');
const overlay = document.querySelector('#screen-overlay');
const startButton = document.querySelector('#start-button');
const pauseButton = document.querySelector('#pause-button');
const fpsCounter = document.querySelector('#fps-counter');
const healthStat = document.querySelector('#health-stat');
const healthBar = document.querySelector('#health-bar');
const healthIcon = document.querySelector('#health-icon');
const weaponName = document.querySelector('#weapon-name');
const ammoCount = document.querySelector('#ammo-count');
const ammoTotal = document.querySelector('#ammo-total');
const weaponHint = document.querySelector('#weapon-hint');
const levelStat = document.querySelector('#level-stat');
const exitStat = document.querySelector('#exit-stat');
const toastEl = document.querySelector('#toast');
const WEAPONS = {
    pistol: { kind: 'pistol', name: 'PLASMA PISTOL', icon: '[::]', ammoLabel: 'CELLS', color: '#59f1ff', damage: 30, delay: 300, spread: 0.075, maxAmmo: 500, kick: 0.016 },
    chainsaw: { kind: 'chainsaw', name: 'CHAINSAW', icon: '[==]', ammoLabel: 'FUEL', color: '#ff9b38', damage: 12, delay: 65, spread: 0.025, maxAmmo: 300, kick: 0.008 },
    rocket: { kind: 'rocket', name: 'ROCKET LAUNCHER', icon: '[=>]', ammoLabel: 'ROCKETS', color: '#c678ff', damage: 90, delay: 720, spread: 0.028, maxAmmo: 80, kick: 0.06 },
};
const MAP_W = 8;
const MAP_H = 7;
const HORIZON_RATIO = 0.5;
const FOV = 1.42;
let state = initialState();
let keys = {};
let mouseShot = false;
let raf = 0;
let fpsFrames = 0;
let fpsLast = performance.now();
function initialState() {
    return {
        running: false,
        paused: false,
        player: { x: 1.55, y: 1.55, angle: -Math.PI / 2, health: 100, maxHealth: 100, ammo: 240, weapon: 'pistol', move: 0, strafe: 0, firing: false, fireCd: 0, invuln: 1.2, dead: false, respawnT: 0, frame: 0 },
        enemies: [],
        pickups: [],
        walls: [],
        kills: 0,
        killsTotal: 12,
        exitFound: false,
        toast: '',
        toastSeconds: 0,
        lastTs: performance.now(),
        frame: 0,
    };
}
function makeEnemy(x, y, id) {
    return { id, x, y, hp: 46 + id * 4, maxHp: 46 + id * 4, speed: 0.56 + (id % 3) * 0.07, alive: true, fireT: 2.2 + id * 0.28, attackCd: 1.2, flash: 0 };
}
function makePickup(x, y, kind) {
    return { id: state.pickups.length + 1, x, y, kind, bob: Math.random() * 8 };
}
function buildLevel() {
    const walls = [];
    const add = (x, y, w, h, kind) => walls.push({ x, y, w, h, kind });
    for (let i = 0; i < 6; i++)
        add(0.72 + i * 0.57, 0.35, 0.57, 0.18, i % 2 ? 'metal' : 'stone');
    for (let i = 0; i < 6; i++)
        add(0.72 + i * 0.57, 5.27, 0.57, 0.18, i % 2 ? 'stone' : 'metal');
    add(0.28, 2.85, 0.2, 1.9, 'stone');
    add(6.0, 1.2, 0.2, 1.4, 'metal');
    add(6.0, 4.4, 0.2, 0.8, 'stone');
    add(3.15, 2.72, 0.3, 0.4, 'hazard');
    add(1.35, 3.72, 0.3, 0.5, 'metal');
    add(4.35, 0.92, 0.6, 0.2, 'metal');
    add(4.35, 5.1, 0.6, 0.2, 'stone');
    add(0.55, 4.2, 0.6, 0.18, 'stone');
    add(6.25, 4.85, 0.5, 0.18, 'metal');
    add(2.2, 2.2, 0.45, 0.08, 'hazard');
    add(5.3, 3.05, 0.45, 0.08, 'hazard');
    const spawns = [
        { x: 5.75, y: 1.35 }, { x: 5.75, y: 4.2 }, { x: 3.65, y: 5.45 },
        { x: 1.2, y: 4.95 }, { x: 2.7, y: 1.75 }, { x: 4.9, y: 2.35 },
        { x: 1.2, y: 2.2 }, { x: 5.7, y: 3.25 }, { x: 3.75, y: 2.85 },
        { x: 1.1, y: 5.4 }, { x: 4.75, y: 5.4 }, { x: 5.85, y: 5.5 },
    ];
    const enemies = spawns.map((p, i) => makeEnemy(p.x, p.y, i + 1));
    const pickups = [
        makePickup(3.25, 5.0, 'ammo'), makePickup(5.75, 1.7, 'health'), makePickup(2.0, 3.45, 'ammo'),
        makePickup(5.25, 4.85, 'health'), makePickup(1.7, 1.75, 'ammo'),
    ];
    pickups.push({ id: 99, x: 3.15, y: 5.05, kind: 'exit', bob: 0 });
    state.walls = walls;
    state.enemies = enemies;
    state.pickups = pickups;
    state.kills = 0;
    state.killsTotal = enemies.length;
    state.exitFound = false;
    const p = state.player;
    p.x = 1.55;
    p.y = 1.55;
    p.angle = -Math.PI / 2;
    p.health = p.maxHealth;
    p.ammo = 240;
    p.weapon = 'pistol';
    p.dead = false;
    p.respawnT = 0;
    p.invuln = 1.5;
    p.fireCd = 0;
}
function beginRun() {
    state.running = true;
    state.paused = false;
    state.lastTs = performance.now();
    state.player.invuln = 1.2;
    overlay.classList.add('hidden');
    pauseButton.textContent = 'II PAUSE';
    toast('SECTOR 7G // BREACH IN PROGRESS', 2600);
    updateHud();
}
function togglePause() {
    if (!state.running || state.player.dead)
        return;
    state.paused = !state.paused;
    pauseButton.textContent = state.paused ? '▶ RESUME' : 'II PAUSE';
    toast(state.paused ? 'GAME PAUSED // TAKE A BREATH' : 'RESUMED // STAY SHARP', 1300);
}
function onKeyDown(event) {
    if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.code))
        event.preventDefault();
    keys[event.code] = true;
    if (event.code === 'Space')
        mouseShot = true;
    if (event.code === 'KeyQ')
        fireWeapon('rocket');
    if (event.code === 'Digit1')
        changeWeapon('pistol');
    if (event.code === 'Digit2')
        changeWeapon('chainsaw');
    if (event.code === 'Digit3')
        changeWeapon('rocket');
}
function onKeyUp(event) { keys[event.code] = false; }
function clearInput() { keys = {}; mouseShot = false; state.player.firing = false; }
function changeWeapon(kind) {
    if (!state.running || state.paused || state.player.dead)
        return;
    state.player.weapon = kind;
    const w = WEAPONS[kind];
    toast(`${w.icon} ${w.name} // ${w.ammoLabel}`, 1100);
    updateHud();
}
function fireWeapon(kind) {
    if (!state.running || state.paused || state.player.dead)
        return;
    const w = WEAPONS[kind];
    if (state.player.fireCd > 0)
        return;
    if (kind === 'chainsaw' && state.player.ammo <= 0) {
        toast('CHAINSAW OUT OF FUEL // FIND FUEL', 1500);
        return;
    }
    if (kind === 'pistol' && state.player.ammo <= 0) {
        toast('PLASMA EMPTY // PRESS 1 OR FIND CELLS', 1500);
        return;
    }
    if (kind === 'rocket' && state.player.ammo <= 0) {
        toast('ROCKET EMPTY // PRESS 3 OR FIND ROCKETS', 1500);
        return;
    }
    state.player.ammo = Math.max(0, state.player.ammo - (kind === 'rocket' ? 4 : 1));
    hitTarget(w.damage, w.spread, kind === 'rocket');
    state.player.fireCd = w.delay;
    state.player.firing = true;
    updateHud();
}
function hitTarget(damage, spread, explode) {
    const p = state.player;
    for (let i = 0; i < state.enemies.length; i++) {
        const e = state.enemies[i];
        if (!e.alive)
            continue;
        const dx = e.x - p.x;
        const dy = e.y - p.y;
        const dist = Math.hypot(dx, dy);
        if (dist > 6.6)
            continue;
        const target = Math.atan2(dy, dx);
        const facing = angleDiff(p.angle, target);
        const tolerance = spread + 0.07 + Math.min(0.2, dist * 0.015);
        if (Math.abs(facing) > tolerance)
            continue;
        e.hp -= damage;
        e.flash = 0.12;
        e.attackCd = 0.4;
        const knock = explode ? 0.2 : 0.08;
        e.x += Math.cos(facing) * knock;
        e.y -= Math.sin(facing) * knock;
        if (e.hp <= 0) {
            e.alive = false;
            state.kills += 1;
            if (explode)
                explodeNearby(e.x, e.y, i);
            if (state.kills === state.killsTotal)
                toast('SECTOR CLEARED // EXIT GATE OPEN', 3600);
            break;
        }
        if (!explode)
            break;
    }
}
function explodeNearby(x, y, index) {
    state.enemies.forEach((e, i) => {
        if (i === index || !e.alive)
            return;
        if (Math.hypot(e.x - x, e.y - y) < 1.45) {
            e.hp -= 28;
            e.flash = 0.12;
            if (e.hp <= 0) {
                e.alive = false;
                state.kills += 1;
                if (state.kills === state.killsTotal)
                    toast('SECTOR CLEARED // EXIT GATE OPEN', 3600);
            }
        }
    });
}
function angleDiff(a, b) {
    let d = (b - a) % (Math.PI * 2);
    if (d > Math.PI)
        d -= Math.PI * 2;
    if (d < -Math.PI)
        d += Math.PI * 2;
    return d;
}
function update(dt) {
    if (!state.running || state.paused)
        return;
    const p = state.player;
    p.frame = state.frame++;
    p.fireCd = Math.max(0, p.fireCd - dt);
    p.invuln = Math.max(0, p.invuln - dt);
    p.firing = false;
    if (p.dead) {
        p.respawnT -= dt;
        if (p.respawnT <= 0) {
            p.dead = false;
            p.health = p.maxHealth;
            p.x = 1.55;
            p.y = 1.55;
            p.angle = -Math.PI / 2;
            p.invuln = 2.0;
            toast('RESPAWNED // THE STATIC REFORMED', 1700);
        }
        updateHud();
        return;
    }
    p.move = (keys.KeyW || keys.ArrowUp ? 1 : 0) - (keys.KeyS || keys.ArrowDown ? 1 : 0);
    p.strafe = (keys.KeyD ? 1 : 0) - (keys.KeyA ? 1 : 0);
    p.angle += ((keys.KeyD ? 1 : 0) - (keys.KeyA ? 1 : 0)) * 2.05 * dt;
    const speed = 2.35;
    const sin = Math.sin(p.angle);
    const cos = Math.cos(p.angle);
    if (p.move || p.strafe) {
        const vx = (cos * p.move + sin * p.strafe) * speed;
        const vy = (sin * p.move - cos * p.strafe) * speed;
        if (!hitWall(p.x + vx * dt, p.y))
            p.x += vx * dt;
        if (!hitWall(p.x, p.y + vy * dt))
            p.y += vy * dt;
    }
    if (mouseShot) {
        fireWeapon(p.weapon);
        mouseShot = false;
    }
    if (keys.Space) {
        fireWeapon(p.weapon);
        p.firing = true;
    }
    for (const enemy of state.enemies)
        updateEnemy(enemy, dt);
    for (const pickup of state.pickups) {
        if (pickup.kind !== 'used')
            pickup.bob += dt * 5;
    }
    collectPickups();
    checkExit();
    if (p.health <= 0) {
        p.dead = true;
        p.respawnT = 2.1;
        state.paused = false;
        pauseButton.textContent = 'II PAUSE';
        toast('SIGNAL LOST // REBOOTING...', 1800);
    }
    updateHud();
}
function updateEnemy(e, dt) {
    if (!e.alive)
        return;
    const p = state.player;
    const dx = p.x - e.x;
    const dy = p.y - e.y;
    const dist = Math.hypot(dx, dy);
    if (dist < 0.01)
        return;
    e.fireT -= dt;
    e.attackCd -= dt;
    if (dist > 1.05) {
        const nx = dx / dist;
        const ny = dy / dist;
        if (!hitWall(e.x + nx * e.speed * dt, e.y))
            e.x += nx * e.speed * dt;
        if (!hitWall(e.x, e.y + ny * e.speed * dt))
            e.y += ny * e.speed * dt;
    }
    if (dist < 1.35 && e.attackCd <= 0 && e.fireT <= 0) {
        e.fireT = 1.35;
        e.attackCd = 0.75;
        p.health = Math.max(0, p.health - (12 + dist * 6));
        p.invuln = 0.7;
        toast('STATIC CLAW // VITALS LOSS', 900);
    }
}
function collectPickups() {
    const p = state.player;
    for (const pickup of state.pickups) {
        if (pickup.kind === 'exit')
            continue;
        if (Math.hypot(pickup.x - p.x, pickup.y - p.y) < 0.48) {
            if (pickup.kind === 'health') {
                p.health = Math.min(p.maxHealth, p.health + 30);
                pickup.kind = 'used';
                toast('+30 VITALS // MED-KIT SECURED', 1500);
            }
            else {
                const current = WEAPONS[p.weapon];
                p.ammo = Math.min(current.maxAmmo, p.ammo + 70);
                pickup.kind = 'used';
                toast(`+70 ${current.ammoLabel} // RESUPPLIED`, 1500);
            }
            updateHud();
        }
    }
}
function checkExit() {
    const exit = state.pickups.find((item) => item.kind === 'exit');
    const p = state.player;
    if (!exit || !state.exitFound && state.kills !== state.killsTotal)
        return;
    if (!state.exitFound) {
        state.exitFound = true;
        toast('EXIT GATE OPEN // REACH THE GREEN SIGNAL', 2700);
    }
    if (Math.hypot(exit.x - p.x, exit.y - p.y) < 0.68) {
        state.running = false;
        showEnd(true);
    }
}
function hitWall(x, y) {
    if (x < 0.14 || y < 0.14 || x > MAP_W - 0.14 || y > MAP_H - 0.14)
        return true;
    return state.walls.some((wall) => x > wall.x - 0.12 && x < wall.x + wall.w + 0.12 && y > wall.y - 0.12 && y < wall.y + wall.h + 0.12);
}
function loop(now) {
    const dt = Math.min(0.05, Math.max(0, now - state.lastTs) / 1000 || 0);
    state.lastTs = now;
    fpsFrames++;
    if (now - fpsLast >= 500) {
        fpsCounter.textContent = `${Math.round(fpsFrames * 1000 / (now - fpsLast))} FPS`;
        fpsFrames = 0;
        fpsLast = now;
    }
    update(dt);
    draw();
    raf = requestAnimationFrame(loop);
}
function draw() {
    if (!ctx)
        return;
    const p = state.player;
    const width = canvas.width;
    const height = canvas.height;
    const horizon = height * HORIZON_RATIO;
    ctx.clearRect(0, 0, width, height);
    const floor = ctx.createLinearGradient(0, horizon, 0, height);
    floor.addColorStop(0, '#162030');
    floor.addColorStop(1, '#070a12');
    ctx.fillStyle = floor;
    ctx.fillRect(0, horizon, width, height - horizon);
    const ceiling = ctx.createLinearGradient(0, 0, 0, horizon);
    ceiling.addColorStop(0, '#02040a');
    ceiling.addColorStop(1, '#101722');
    ctx.fillStyle = ceiling;
    ctx.fillRect(0, 0, width, horizon);
    // scanline glow and distant signal
    ctx.save();
    ctx.globalCompositeOperation = 'screen';
    const glow = ctx.createRadialGradient(width / 2, horizon, 20, width / 2, horizon, width * 0.72);
    glow.addColorStop(0, '#59f1ff14');
    glow.addColorStop(1, '#00000000');
    ctx.fillStyle = glow;
    ctx.fillRect(0, 0, width, height);
    ctx.restore();
    drawWorld(p, width, height, horizon);
    drawWeapons(p, width, height);
    drawVignette(width, height);
    if (p.invuln > 0 && state.frame % 8 < 4) {
        ctx.fillStyle = '#59f1ff12';
        ctx.fillRect(0, 0, width, height);
    }
}
function drawWorld(p, width, height, horizon) {
    const sin = Math.sin(p.angle);
    const cos = Math.cos(p.angle);
    const projected = [];
    const corners = [];
    for (const wall of state.walls) {
        corners.length = 0;
        for (const [x, y] of [[wall.x, wall.y], [wall.x + wall.w, wall.y], [wall.x + wall.w, wall.y + wall.h], [wall.x, wall.y + wall.h]]) {
            const dx = x - p.x;
            const dy = y - p.y;
            const view = dx * cos + dy * sin;
            if (view < 0.16)
                continue;
            const side = -dx * sin + dy * cos;
            corners.push({ x: width / 2 + (side / view) * width * 0.48, y: horizon - (330 / view) });
        }
        if (corners.length < 3)
            continue;
        const centerDx = wall.x + wall.w / 2 - p.x;
        const centerDy = wall.y + wall.h / 2 - p.y;
        const dist = Math.hypot(centerDx, centerDy);
        projected.push({ wall, pts: corners, dist, shade: shadeFor(wall.kind) });
    }
    projected.sort((a, b) => b.dist - a.dist);
    for (const item of projected)
        drawWallQuad(item, width, height, horizon);
    drawThings(p, width, height, horizon);
}
function drawWallQuad(item, width, height, horizon) {
    const { wall, pts } = item;
    const top = Math.min(...pts.map((p) => p.y));
    const bottom = Math.max(...pts.map((p) => p.y));
    const left = Math.min(...pts.map((p) => p.x));
    const right = Math.max(...pts.map((p) => p.x));
    ctx.fillStyle = shadeColor(wall.kind, item.shade);
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for (let i = 1; i < pts.length; i++)
        ctx.lineTo(pts[i].x, pts[i].y);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = wall.kind === 'hazard' ? '#ffca4d' : '#344359';
    ctx.globalAlpha = 0.55;
    ctx.stroke();
    ctx.globalAlpha = 1;
    // neon edge on nearby walls
    ctx.strokeStyle = wall.kind === 'metal' ? '#59f1ff20' : wall.kind === 'hazard' ? '#ffd45a28' : '#ffffff0b';
    ctx.stroke();
    if (wall.kind === 'hazard' && bottom - top > 5) {
        ctx.fillStyle = '#ffd45a22';
        ctx.fillRect(left + 2, bottom - 4, right - left - 4, 3);
    }
    if (wall.kind === 'gate') {
        const pulse = 0.5 + Math.sin(state.frame * 0.08) * 0.3;
        ctx.fillStyle = `rgba(156,255,156,${pulse})`;
        ctx.fillRect(left + 4, top + 8, Math.max(0, right - left - 8), Math.max(1, bottom - top - 16));
    }
    // simple depth striping keeps the texture readable at range
    const bands = Math.max(1, Math.floor((bottom - top) / 44));
    ctx.fillStyle = wall.kind === 'metal' ? '#59f1ff11' : '#ffffff08';
    for (let i = 1; i < bands; i++) {
        const y = top + (bottom - top) * (i / bands);
        ctx.fillRect(left, y, right - left, 1);
    }
}
function drawThings(p, width, height, horizon) {
    const things = [];
    for (const pickup of state.pickups) {
        if (pickup.kind === 'used' || (pickup.kind === 'exit' && !state.exitFound))
            continue;
        const dist = Math.hypot(pickup.x - p.x, pickup.y - p.y);
        if (dist > 7)
            continue;
        const size = Math.min(1, 2.1 / dist);
        things.push({ x: pickup.x, y: pickup.y, size, dist, draw: () => drawPickup(pickup, p, size, width, height, horizon) });
    }
    for (const enemy of state.enemies) {
        if (!enemy.alive)
            continue;
        const dist = Math.hypot(enemy.x - p.x, enemy.y - p.y);
        if (dist > 7)
            continue;
        const size = Math.min(1, 1.8 / dist);
        things.push({ x: enemy.x, y: enemy.y, size, dist, draw: () => drawEnemy(enemy, p, size, width, height, horizon) });
    }
    things.sort((a, b) => b.dist - a.dist);
    for (const thing of things)
        thing.draw();
}
function worldToScreen(x, y, p, width, height, horizon) {
    const dx = x - p.x;
    const dy = y - p.y;
    const dist = Math.hypot(dx, dy);
    const side = -dx * Math.sin(p.angle) + dy * Math.cos(p.angle);
    const view = dx * Math.cos(p.angle) + dy * Math.sin(p.angle);
    return { x: width / 2 + (side / Math.max(0.18, view)) * width * 0.62, y: horizon - dist * 31, dist };
}
function drawPickup(pickup, p, size, width, height, horizon) {
    const pos = worldToScreen(pickup.x, pickup.y, p, width, height, horizon);
    const radius = 6 * size + 4;
    ctx.save();
    ctx.shadowColor = pickup.kind === 'exit' ? '#9cff9c' : pickup.kind === 'health' ? '#ff4964' : '#59f1ff';
    ctx.shadowBlur = 12;
    if (pickup.kind === 'health') {
        ctx.fillStyle = '#ff4964';
        ctx.beginPath();
        ctx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#fff';
        ctx.fillRect(pos.x - 3 * size, pos.y - 1.5 * size, 6 * size, 3 * size);
        ctx.fillRect(pos.x - 1.5 * size, pos.y - 3 * size, 3 * size, 6 * size);
    }
    else if (pickup.kind === 'ammo') {
        ctx.fillStyle = '#59f1ff';
        ctx.fillRect(pos.x - radius, pos.y - radius * 0.65, radius * 2, radius * 1.3);
        ctx.fillStyle = '#dffcff';
        ctx.fillRect(pos.x - radius * 0.35, pos.y - radius * 0.35, radius * 0.7, radius * 0.7);
    }
    else {
        ctx.strokeStyle = '#9cff9c';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(pos.x, pos.y, radius + 5 + Math.sin(pickup.bob) * 2, 0, Math.PI * 2);
        ctx.stroke();
        ctx.fillStyle = '#9cff9c';
        ctx.fillRect(pos.x - 2, pos.y - radius, 4, radius * 2);
        ctx.fillRect(pos.x - radius, pos.y - 2, radius * 2, 4);
    }
    ctx.restore();
}
function drawEnemy(enemy, p, size, width, height, horizon) {
    const pos = worldToScreen(enemy.x, enemy.y, p, width, height, horizon);
    const s = size;
    const h = 39 * s + 10;
    const bodyW = 30 * s + 7;
    ctx.save();
    ctx.translate(pos.x, pos.y);
    ctx.fillStyle = enemy.flash > 0 ? '#fff3f3' : '#93365f';
    ctx.fillRect(-bodyW / 2, -h * 0.48, bodyW, h * 0.62);
    ctx.fillStyle = enemy.flash > 0 ? '#fff' : '#232d42';
    ctx.fillRect(-bodyW * 0.33, -h * 0.72, bodyW * 0.66, h * 0.38);
    ctx.fillStyle = '#ffd45a';
    ctx.fillRect(-7 * s - 3, -h * 0.62, 4 * s + 2, 4 * s + 2);
    ctx.fillRect(3, -h * 0.62, 4 * s + 2, 4 * s + 2);
    ctx.fillStyle = '#ff4964';
    ctx.fillRect(-bodyW / 2, h * 0.12, bodyW, h * 0.1);
    ctx.restore();
    const hpW = 34 * s;
    ctx.fillStyle = '#251520';
    ctx.fillRect(pos.x - hpW / 2, pos.y - h - 15 * s, hpW, 4);
    ctx.fillStyle = '#ff4964';
    ctx.fillRect(pos.x - hpW / 2, pos.y - h - 15 * s, hpW * Math.max(0, enemy.hp / enemy.maxHp), 4);
}
function drawWeapons(p, width, height) {
    const kind = WEAPONS[p.weapon];
    const bob = p.firing ? 3 + Math.sin(state.frame * 0.8) * 2 : Math.sin(state.frame * 0.07) * 2;
    const x = width * 0.5 + kind.kick * 18;
    const y = height * 0.78 + bob;
    ctx.save();
    ctx.shadowColor = kind.color;
    ctx.shadowBlur = 16;
    ctx.fillStyle = '#0c121e';
    ctx.strokeStyle = kind.color;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(x - 100, y - 25);
    ctx.lineTo(x + 100, y - 25);
    ctx.lineTo(x + 116, y + 26);
    ctx.lineTo(x - 116, y + 26);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
    ctx.fillStyle = kind.color;
    ctx.fillRect(x - 82, y - 17, 164, 8);
    ctx.fillStyle = '#101a28';
    ctx.fillRect(x - 64, y - 13, 128, 16);
    ctx.fillStyle = kind.color;
    ctx.font = '900 12px Courier New';
    ctx.textAlign = 'center';
    ctx.fillText(kind.icon, x, y - 4);
    ctx.restore();
    if (p.weapon === 'rocket') {
        ctx.fillStyle = '#c678ff';
        ctx.font = '900 13px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText('>>', x + 112, y + 7);
    }
}
function drawVignette(width, height) {
    const g = ctx.createRadialGradient(width / 2, height / 2, height * 0.34, width / 2, height / 2, height * 0.8);
    g.addColorStop(0, '#00000000');
    g.addColorStop(1, '#000000b8');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, width, height);
}
function shadeFor(kind) {
    if (kind === 'stone')
        return 1;
    if (kind === 'metal')
        return -0.12;
    if (kind === 'hazard')
        return -0.25;
    return 0.3;
}
function shadeColor(kind, shade) {
    const base = kind === 'stone' ? [54, 70, 86] : kind === 'metal' ? [64, 87, 111] : kind === 'hazard' ? [106, 84, 43] : [46, 103, 84];
    const f = 1 + shade;
    return `rgb(${Math.round(base[0] * f)},${Math.round(base[1] * f)},${Math.round(base[2] * f)})`;
}
function updateHud() {
    const p = state.player;
    const w = WEAPONS[p.weapon];
    healthStat.textContent = String(Math.round(p.health));
    healthBar.style.width = `${Math.max(0, p.health)}%`;
    healthIcon.textContent = p.health <= 35 ? '♥' : '♥';
    weaponName.textContent = w.name;
    ammoCount.textContent = String(p.ammo);
    ammoTotal.textContent = String(w.maxAmmo);
    weaponHint.textContent = p.weapon === 'pistol' ? 'click / space to fire' : p.weapon === 'chainsaw' ? 'hold space // close range' : 'q / right click // blast';
    levelStat.textContent = `LEVEL 01 // KILL ${state.kills}/${state.killsTotal}`;
    exitStat.textContent = state.exitFound ? 'EXIT GATE // OPEN' : state.kills === state.killsTotal ? 'EXIT GATE // OPEN' : 'EXIT GATE HIDDEN // CLEAR THE SECTOR';
    exitStat.classList.toggle('dim', !(state.exitFound || state.kills === state.killsTotal));
}
function toast(message, duration = 1800) {
    state.toast = message;
    state.toastSeconds = duration / 1000;
    toastEl.textContent = message;
    toastEl.classList.add('show');
    const timer = setTimeout(() => toastEl.classList.remove('show'), duration);
    toastEl.dataTimer = timer;
}
function showEnd(won) {
    overlay.classList.remove('hidden');
    overlay.innerHTML = `<div class="overlay-card"><div class="overlay-kicker">${won ? 'MISSION COMPLETE // GATE SECURED' : 'SIGNAL LOST // RETRY READY'}</div><h1>${won ? 'GATE<br><em>OPEN</em>' : 'WASTED'}</h1><p class="overlay-copy">${won ? `SECTOR 7G IS YOURS. ${state.kills}/${state.killsTotal} HOSTILES PURGED. THE STATIC SCREAMS NO MORE.` : 'YOUR SIGNAL REBOOTED. THE SECTOR REMAINS HOSTILE. PRESS START TO RETRY.'}</p><button id="retry-button" class="start-button" type="button">RESTART MISSION <span>↻ RETRY</span></button></div>`;
    document.querySelector('#retry-button')?.addEventListener('click', () => {
        buildLevel();
        overlay.innerHTML = `<div class="overlay-card"><div class="overlay-kicker">CLASSIFIED OPERATION // SECTOR 7G</div><h1>NEON<br><em>ABYSS</em></h1><p class="overlay-copy">THE STATIC IS WATCHING. FIND THE <span>EXIT GATE</span> BEFORE IT FINDS YOU.</p><div class="overlay-controls"><span><b>WASD</b> MOVE</span><span><b>MOUSE</b> LOOK</span><span><b>LEFT CLICK</b> FIRE</span><span><b>RMB / Q</b> ROCKETS</span></div><button id="start-button" class="start-button" type="button">INSERT COIN <span>▶ START</span></button></div>`;
        document.querySelector('#start-button')?.addEventListener('click', beginRun);
        overlay.classList.add('hidden');
        toast('SECTOR 7G // BREACH IN PROGRESS', 2600);
        updateHud();
    });
}
function startGame() {
    startGameInternal();
}
function startGameInternal() {
    buildLevel();
    startButton.addEventListener('click', beginRun);
    pauseButton.addEventListener('click', togglePause);
    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);
    window.addEventListener('blur', clearInput);
    canvas.addEventListener('click', () => { mouseShot = true; });
    canvas.addEventListener('contextmenu', (event) => { event.preventDefault(); fireWeapon('rocket'); });
    raf = requestAnimationFrame(loop);
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>