DOOM
Qwen 3.6 27b MTP · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 71,840 bytes · SHA-256 4167549295f7
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SHADOW CORRIDORS - Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
#game-container { position: relative; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; }
#game { image-rendering: pixelated; cursor: none; background: #000; }
#minimap { position: absolute; top: 8px; right: 8px; border: 2px solid #555; opacity: 0.7; image-rendering: pixelated; }
/* HUD */
#hud {
position: absolute; bottom: 0; left: 0; right: 0; height: 64px;
background: linear-gradient(to top, #1a1a2e 0%, #16213e 70%, transparent 100%);
display: flex; align-items: center; justify-content: space-between; padding: 8px 20px;
border-top: 2px solid #4a4a6a; z-index: 10;
}
#health-bar { width: 200px; height: 20px; background: #333; border: 2px solid #555; border-radius: 2px; overflow: hidden; }
#health-fill { height: 100%; background: linear-gradient(to right, #ff0000, #cc0000); transition: width 0.2s; width: 100%; }
#ammo-display { color: #ffaa00; font-size: 18px; text-shadow: 0 0 8px rgba(255,170,0,0.5); letter-spacing: 2px; }
#weapon-name { color: #ffffff; font-size: 22px; font-weight: bold; text-shadow: 0 0 10px rgba(255,255,255,0.3); letter-spacing: 4px; }
#score-display { color: #00ff88; font-size: 16px; text-shadow: 0 0 8px rgba(0,255,136,0.5); }
#level-display { color: #aaaaff; font-size: 14px; letter-spacing: 2px; }
/* Overlay */
#overlay {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
justify-content: center; align-items: center; z-index: 20;
}
#overlay.hidden { display: none; }
#overlay-title { color: #ff4444; font-size: 48px; text-shadow: 0 0 20px rgba(255,68,68,0.8); margin-bottom: 10px; letter-spacing: 6px; }
#overlay-subtitle { color: #aaa; font-size: 18px; margin-bottom: 30px; text-align: center; line-height: 1.6; }
#overlay-btn {
background: #cc0000; color: white; border: none; padding: 12px 40px;
font-size: 20px; cursor: pointer; font-family: 'Courier New', monospace;
letter-spacing: 3px; text-transform: uppercase; transition: all 0.2s;
}
#overlay-btn:hover { background: #ff2222; transform: scale(1.05); box-shadow: 0 0 20px rgba(255,0,0,0.5); }
/* Crosshair */
.crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; z-index: 15; }
/* ── css: crosshair ── */
#crosshair {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
pointer-events: none; z-index: 15; color: rgba(255,255,255,0.7);
font-size: 24px; font-weight: bold; text-shadow: 0 0 4px rgba(0,0,0,0.8);
line-height: 1; user-select: none;
}
/* Scanline effect overlay */
#game-container::after {
content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none; z-index: 5;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0,0,0,0.03) 2px,
rgba(0,0,0,0.03) 4px
);
}
/* Kill feed */
#kill-feed {
position: absolute; top: 10px; left: 10px; z-index: 15; pointer-events: none;
font-family: 'Courier New', monospace; font-size: 12px; color: #ff8844;
}
.kill-msg {
opacity: 1; transition: opacity 0.5s; margin-bottom: 4px;
text-shadow: 0 0 6px rgba(0,0,0,0.8);
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game"></canvas>
<div id="hud">
<div id="health-bar"><div id="health-fill"></div></div>
<div id="ammo-display">AMMO: --</div>
<div id="weapon-name">PISTOL</div>
<div id="score-display">SCORE: 0</div>
<div id="level-display">LEVEL: 1/3</div>
</div>
<div id="overlay" class="hidden">
<div id="overlay-title"></div>
<div id="overlay-subtitle"></div>
<button id="overlay-btn">CONTINUE</button>
</div>
<canvas id="minimap"></canvas>
<div id="crosshair" class="crosshair">+</div><div id="kill-feed"></div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./input":"src/input.ts","./renderer":"src/renderer.ts","./logic":"src/logic.ts","./game":"src/game.ts","./sounds":"src/sounds.ts"},"src/maps.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts","./maps":"src/maps.ts"},"src/input.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./maps":"src/maps.ts"},"src/logic.ts":{"./types":"src/types.ts","./maps":"src/maps.ts","./sounds":"src/sounds.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";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("./types");
const input_1 = require("./input");
const renderer_1 = require("./renderer");
const logic_1 = require("./logic");
const game_1 = require("./game");
const sounds_1 = require("./sounds");
// DOM elements
const gameCanvas = document.getElementById('game');
const minimapCanvas = document.getElementById('minimap');
const healthFill = document.getElementById('health-fill');
const ammoDisplay = document.getElementById('ammo-display');
const weaponName = document.getElementById('weapon-name');
const scoreDisplay = document.getElementById('score-display');
const levelDisplay = document.getElementById('level-display');
const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlay-title');
const overlaySubtitle = document.getElementById('overlay-subtitle');
const overlayBtn = document.getElementById('overlay-btn');
// Game state
let gameState = (0, game_1.initLevel)(0);
let damageFlash = 0;
let lastTime = performance.now();
let fireHeld = false;
let prevPickupStates = [];
let prevEnemyAlive = [];
const killFeedEl = document.getElementById('kill-feed');
const killMessages = [];
function addKillMessage(type) {
const names = { grunt: 'GRUNT', soldier: 'SOLDIER', boss: 'BOSS' };
const msg = document.createElement('div');
msg.className = 'kill-msg';
msg.textContent = `☠ ${names[type] || 'ENEMY'} ELIMINATED`;
killFeedEl.appendChild(msg);
killMessages.push({ el: msg, timer: 3000 });
while (killMessages.length > 5) {
const old = killMessages.shift();
old.el.remove();
}
}
// Render target
const rt = (0, renderer_1.createRenderTarget)(gameCanvas);
function resizeCanvas() {
const container = document.getElementById('game-container');
const cw = container.clientWidth || window.innerWidth;
const ch = container.clientHeight || window.innerHeight;
const scale = Math.min(cw / types_1.RENDER_W, ch / types_1.RENDER_H);
gameCanvas.style.width = `${Math.floor(types_1.RENDER_W * scale)}px`;
gameCanvas.style.height = `${Math.floor(types_1.RENDER_H * scale)}px`;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
(0, input_1.initInput)(gameCanvas);
window.__gameKeys = input_1.keys;
function showOverlay(title, subtitle, btnText) {
overlay.classList.remove('hidden');
overlayTitle.textContent = title;
overlaySubtitle.innerHTML = subtitle;
overlayBtn.textContent = btnText;
}
function hideOverlay() {
overlay.classList.add('hidden');
}
showOverlay('SHADOW CORRIDORS', '<span style="color:#ff8844;font-size:14px">A Retro FPS Adventure</span><br><br>' +
'<b>WASD</b> — Move | <b>Mouse</b> — Look<br>' +
'<b>Left Click / Space</b> — Fire<br>' +
'<b>1</b> Pistol (∞ ammo) <b>2</b> Shotgun <b>3</b> Rifle<br><br>' +
'Eliminate enemies, collect pickups,<br>and find the exit portal in each level!', 'START GAME');
overlayBtn.addEventListener('click', () => {
(0, sounds_1.initAudio)();
if (gameState.gameState === 'dead') {
gameState.deathTimer = 0;
return;
}
gameCanvas.focus();
gameCanvas.requestPointerLock();
});
gameCanvas.addEventListener('click', () => { (0, sounds_1.initAudio)(); });
function updateHUD() {
const p = gameState.player;
healthFill.style.width = `${(p.health / p.maxHealth) * 100}%`;
if (p.health > 60)
healthFill.style.background = 'linear-gradient(to right, #44cc44, #22aa22)';
else if (p.health > 30)
healthFill.style.background = 'linear-gradient(to right, #cccc00, #aaaa00)';
else
healthFill.style.background = 'linear-gradient(to right, #ff4444, #cc0000)';
const weapon = types_1.WEAPONS[p.weaponIndex];
ammoDisplay.textContent = weapon.ammoCost === 0 ? 'AMMO: ∞' : `AMMO: ${p.ammo[p.weaponIndex]}`;
weaponName.textContent = weapon.name;
scoreDisplay.textContent = `SCORE: ${p.score}`;
levelDisplay.textContent = `LEVEL: ${gameState.level}/${gameState.totalLevels}`;
// Nearby pickup indicator
let nearbyPickup = '';
for (const pk of gameState.pickups) {
if (pk.collected)
continue;
const d = Math.sqrt((p.x - pk.x) ** 2 + (p.y - pk.y) ** 2);
if (d < 3) {
const names = { health: '♥ HEALTH', ammo_shotgun: '⊕ SHOTGUN AMMO', ammo_rifle: '⊕ RIFLE AMMO' };
nearbyPickup = `${names[pk.type]} (${Math.ceil(d)}m)`;
break;
}
}
let pickupIndicator = document.getElementById('pickup-indicator');
if (nearbyPickup) {
if (!pickupIndicator) {
pickupIndicator = document.createElement('div');
pickupIndicator.id = 'pickup-indicator';
pickupIndicator.style.cssText = 'position:absolute;bottom:70px;left:50%;transform:translateX(-50%);color:#ffcc44;font-size:13px;font-family:Courier New,monospace;text-shadow:0 0 8px rgba(255,200,68,0.5);z-index:15;pointer-events:none;';
document.getElementById('game-container').appendChild(pickupIndicator);
}
pickupIndicator.textContent = nearbyPickup;
}
else if (pickupIndicator) {
pickupIndicator.remove();
}
}
function gameLoop(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
if (gameState.gameState === 'playing') {
const mouseDelta = (0, input_1.getMouseDelta)();
(0, input_1.resetMouseDelta)();
const oldHealth = gameState.player.health;
(0, logic_1.updatePlayer)(gameState, dt, input_1.keys, mouseDelta);
if (input_1.keys.fire && !fireHeld) {
(0, logic_1.tryShoot)(gameState, now);
fireHeld = true;
}
if (!input_1.keys.fire)
fireHeld = false;
if (input_1.keys.weapon1)
(0, logic_1.switchWeapon)(gameState, 0);
if (input_1.keys.weapon2)
(0, logic_1.switchWeapon)(gameState, 1);
if (input_1.keys.weapon3)
(0, logic_1.switchWeapon)(gameState, 2);
(0, logic_1.updateEnemies)(gameState, dt, now);
if (gameState.player.health < oldHealth) {
damageFlash = 300;
}
if (prevPickupStates.length !== gameState.pickups.length) {
prevPickupStates = new Array(gameState.pickups.length).fill(false);
}
(0, logic_1.updatePickups)(gameState);
for (let i = 0; i < gameState.pickups.length; i++) {
if (!prevPickupStates[i] && !gameState.pickups[i].collected) {
(0, sounds_1.playPickup)();
}
prevPickupStates[i] = gameState.pickups[i].collected;
}
if (prevEnemyAlive.length !== gameState.enemies.length) {
prevEnemyAlive = new Array(gameState.enemies.length).fill(true);
}
for (let i = 0; i < gameState.enemies.length; i++) {
if (prevEnemyAlive[i] && !gameState.enemies[i].alive) {
addKillMessage(gameState.enemies[i].type);
}
prevEnemyAlive[i] = gameState.enemies[i].alive;
}
for (let i = killMessages.length - 1; i >= 0; i--) {
killMessages[i].timer -= dt * 1000;
if (killMessages[i].timer <= 0) {
killMessages[i].el.style.opacity = '0';
setTimeout(() => killMessages[i].el.remove(), 500);
killMessages.splice(i, 1);
}
else if (killMessages[i].timer < 1000) {
killMessages[i].el.style.opacity = String(killMessages[i].timer / 1000);
}
}
if (damageFlash > 0)
damageFlash -= dt * 1000;
}
let justDied = false;
if (gameState.gameState === 'dead') {
if (!justDied)
(0, sounds_1.playDeath)();
justDied = true;
gameState.deathTimer -= dt * 1000;
damageFlash = Math.max(damageFlash, 200);
if (gameState.deathTimer <= 0) {
(0, game_1.respawnPlayer)(gameState);
hideOverlay();
gameCanvas.requestPointerLock();
}
else {
showOverlay('YOU DIED', `Score: ${gameState.player.score}<br>Respawning in ${Math.ceil(gameState.deathTimer / 1000)}s...<br><span style="color:#888;font-size:12px">(Click to respawn faster)</span>`, 'RESPAWN');
}
}
let justCompletedLevel = false;
if (gameState.gameState === 'levelComplete' && !justCompletedLevel) {
(0, sounds_1.playLevelComplete)();
justCompletedLevel = true;
}
if (gameState.gameState === 'levelComplete') {
if (gameState.level >= gameState.totalLevels) {
gameState.gameState = 'won';
showOverlay('VICTORY!', `You escaped all ${gameState.totalLevels} levels!<br>Final Score: ${gameState.player.score}`, 'PLAY AGAIN');
overlayBtn.onclick = () => {
gameState = (0, game_1.initLevel)(0);
hideOverlay();
gameCanvas.requestPointerLock();
};
}
else {
showOverlay(`LEVEL ${gameState.level} COMPLETE`, `Score: ${gameState.player.score}<br>Get ready for the next level...`, 'NEXT LEVEL');
overlayBtn.onclick = () => {
(0, game_1.nextLevel)(gameState);
hideOverlay();
gameCanvas.requestPointerLock();
};
}
}
const playerRef = gameState.player;
let screenShake = 0;
if (playerRef.shooting && playerRef.shootTimer > 150) {
const weapon = types_1.WEAPONS[playerRef.weaponIndex];
screenShake = weapon.name === 'SHOTGUN' ? 4 : weapon.name === 'RIFLE' ? 2 : 1;
}
if (damageFlash > 100)
screenShake += damageFlash / 50;
const p = gameState.player;
(0, renderer_1.renderFrame)(rt, gameState.map, gameState.mapWidth, gameState.mapHeight, p.x + (Math.random() - 0.5) * screenShake * 0.02, p.y + (Math.random() - 0.5) * screenShake * 0.02, p.angle, gameState.enemies, gameState.pickups, gameState.exitPos, p.shooting, p.shootTimer, p.weaponIndex, damageFlash, screenShake);
(0, renderer_1.renderMinimap)(minimapCanvas, gameState.map, p.x, p.y, p.angle, gameState.enemies, gameState.pickups, gameState.exitPos);
updateHUD();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RENDER_H = exports.RENDER_W = exports.FLOOR = exports.WALL = exports.WEAPONS = exports.MAX_DEPTH = exports.FOV = exports.TILE = void 0;
// Game constants
exports.TILE = 1; // tile size in world units
exports.FOV = Math.PI / 3; // field of view (60 degrees)
exports.MAX_DEPTH = 24; // max ray cast distance
exports.WEAPONS = [
{ name: 'PISTOL', damage: 15, fireRate: 400, ammoCost: 0, spread: 0.02, pellets: 1, range: 20, reloadTime: 300 },
{ name: 'SHOTGUN', damage: 12, fireRate: 800, ammoCost: 1, spread: 0.15, pellets: 6, range: 12, reloadTime: 600 },
{ name: 'RIFLE', damage: 25, fireRate: 150, ammoCost: 1, spread: 0.03, pellets: 1, range: 30, reloadTime: 400 },
];
// Map tile types
exports.WALL = 1;
exports.FLOOR = 0;
// Render constants
exports.RENDER_W = 640;
exports.RENDER_H = 480;
};
// ── module: src/maps.ts ──
__mods["src/maps.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LEVELS = void 0;
exports.getLevel = getLevel;
exports.getWallColor = getWallColor;
exports.isWall = isWall;
exports.dist = dist;
// Map levels - 0 = empty, 1+ = wall (different numbers = different wall colors)
exports.LEVELS = [
// Level 1 - The Corridors
[
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 2, 2, 2, 0, 0, 3, 3, 0, 0, 0, 0, 4, 4, 4, 0, 0, 1],
[1, 0, 0, 2, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 4, 0, 0, 1],
[1, 0, 0, 2, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 4, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 3, 3, 3, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 1],
[1, 0, 0, 4, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 3, 0, 0, 1],
[1, 0, 0, 4, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2, 2, 2, 0, 1],
[1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1],
[1, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
],
// Level 2 - The Dungeon
[
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 3, 3, 3, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 2],
[2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 2],
[2, 1, 0, 0, 3, 3, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 4, 0, 0, 0, 2],
[2, 0, 0, 0, 3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 3, 0, 0, 2],
[2, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2],
[2, 0, 1, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 4, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 2],
[2, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 4, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 3, 3, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 2],
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
],
// Level 3 - The Fortress (final)
[
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
[3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 1, 0, 0, 3],
[3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 3],
[3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3],
[3, 0, 0, 0, 0, 1, 1, 0, 0, 4, 4, 0, 4, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 1, 1, 0, 0, 4, 4, 0, 4, 4, 0, 0, 1, 1, 0, 0, 0, 0, 0, 3],
[3, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 3],
[3, 0, 2, 2, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1, 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3],
[3, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 1, 0, 0, 3],
[3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
[3, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3],
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
],
];
function getLevel(level) {
const m = exports.LEVELS[level] || exports.LEVELS[0];
return { map: m.map(r => [...r]), width: m[0].length, height: m.length };
}
// Wall colors by tile type (for rendering)
function getWallColor(tileType) {
switch (tileType) {
case 1: return [120, 80, 60]; // brown brick
case 2: return [80, 80, 90]; // gray stone
case 3: return [60, 50, 70]; // dark purple
case 4: return [100, 70, 40]; // orange brick
default: return [100, 100, 100];
}
}
function isWall(map, x, y) {
const tx = Math.floor(x);
const ty = Math.floor(y);
if (tx < 0 || tx >= map[0].length || ty < 0 || ty >= map.length)
return true;
return map[ty][tx] !== 0;
}
function dist(x1, y1, x2, y2) {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGameState = createGameState;
exports.respawnPlayer = respawnPlayer;
exports.nextLevel = nextLevel;
exports.isExit = isExit;
exports.markExit = markExit;
exports.initLevel = initLevel;
const maps_1 = require("./maps");
const TOTAL_LEVELS = 3;
function createPlayer() {
return {
x: 1.5, y: 1.5, angle: 0,
health: 100, maxHealth: 100,
weaponIndex: 0,
ammo: [999, 20, 30], // pistol unlimited, shotgun 20, rifle 30
score: 0, alive: true,
lastShot: 0, shooting: false, shootTimer: 0,
};
}
function findEmptySpot(map, tries = 100) {
for (let i = 0; i < tries; i++) {
const x = Math.floor(Math.random() * map[0].length);
const y = Math.floor(Math.random() * map.length);
if (map[y][x] === 0)
return { x: x + 0.5, y: y + 0.5 };
}
return null;
}
function spawnEnemies(map, level) {
const enemies = [];
let id = 0;
const count = 6 + level * 4; // more enemies each level
for (let i = 0; i < count; i++) {
const pos = findEmptySpot(map);
if (!pos)
continue;
if ((0, maps_1.dist)(pos.x, pos.y, 1.5, 1.5) < 4)
continue; // not too close to spawn
let type;
if (level === 2 && i === count - 1) {
type = 'boss';
}
else if (Math.random() < 0.3 + level * 0.15) {
type = 'soldier';
}
else {
type = 'grunt';
}
const e = {
id: id++, x: pos.x, y: pos.y,
health: type === 'boss' ? 200 : type === 'soldier' ? 60 : 35,
maxHealth: type === 'boss' ? 200 : type === 'soldier' ? 60 : 35,
speed: type === 'boss' ? 1.2 : type === 'soldier' ? 1.8 : 1.0,
damage: type === 'boss' ? 20 : type === 'soldier' ? 12 : 8,
attackRange: type === 'boss' ? 3 : 2.5,
attackRate: type === 'boss' ? 600 : 1000,
lastAttack: 0, type, alive: true,
alertDist: type === 'soldier' ? 10 : 8,
textureOffset: Math.random() * 100,
};
enemies.push(e);
}
return enemies;
}
function spawnPickups(map, level) {
const pickups = [];
let id = 0;
const count = 4 + level * 2;
for (let i = 0; i < count; i++) {
const pos = findEmptySpot(map);
if (!pos)
continue;
const r = Math.random();
let type;
if (r < 0.4)
type = 'health';
else if (r < 0.7)
type = 'ammo_shotgun';
else
type = 'ammo_rifle';
pickups.push({ id: id++, x: pos.x, y: pos.y, type, collected: false });
}
return pickups;
}
function findExit(map) {
// Find a spot far from player spawn
let bestX = map[0].length - 2;
let bestY = map.length - 2;
while (map[bestY][bestX] !== 0) {
bestX--;
if (bestX < 1) {
bestX = map[0].length - 2;
bestY--;
}
}
return { x: bestX + 0.5, y: bestY + 0.5 };
}
function createGameState(level) {
const lvl = (0, maps_1.getLevel)(level);
const exitPos = findExit(lvl.map);
// Place a special tile for the exit (tile type 9)
const etx = Math.floor(exitPos.x - 0.5);
const ety = Math.floor(exitPos.y - 0.5);
if (lvl.map[ety] && lvl.map[ety][etx] !== undefined) {
// Mark exit with a special value stored separately
}
return {
player: createPlayer(),
enemies: spawnEnemies(lvl.map, level),
pickups: spawnPickups(lvl.map, level),
map: lvl.map,
mapWidth: lvl.width,
mapHeight: lvl.height,
level: level + 1,
totalLevels: TOTAL_LEVELS,
keysCollected: 0,
keysRequired: 0,
exitPos,
gameState: 'playing',
deathTimer: 0,
};
}
function respawnPlayer(state) {
state.player.health = Math.min(state.player.maxHealth, state.player.health + 50);
state.player.alive = true;
state.player.x = 1.5;
state.player.y = 1.5;
state.player.angle = 0;
state.gameState = 'playing';
}
function nextLevel(state) {
const newLvl = (0, maps_1.getLevel)(state.level);
// Preserve score and ammo
const oldScore = state.player.score;
const oldAmmo = [...state.player.ammo];
const oldWeapon = state.player.weaponIndex;
const oldHealth = Math.min(100, state.player.health + 25);
state.map = newLvl.map;
state.mapWidth = newLvl.width;
state.mapHeight = newLvl.height;
state.level++;
state.enemies = spawnEnemies(newLvl.map, state.level - 1);
state.pickups = spawnPickups(newLvl.map, state.level - 1);
state.exitPos = findExit(newLvl.map);
state.player.x = 1.5;
state.player.y = 1.5;
state.player.angle = 0;
state.player.health = oldHealth;
state.player.score = oldScore;
state.player.ammo = oldAmmo;
state.player.weaponIndex = oldWeapon;
state.gameState = 'playing';
}
function isExit(map, x, y) {
const tx = Math.floor(x);
const ty = Math.floor(y);
if (tx < 0 || tx >= map[0].length || ty < 0 || ty >= map.length)
return false;
// Exit is marked with tile value 9
return map[ty][tx] === 9;
}
// Mark exit on a map
function markExit(map, x, y) {
const tx = Math.floor(x - 0.5);
const ty = Math.floor(y - 0.5);
if (tx >= 0 && tx < map[0].length && ty >= 0 && ty < map.length) {
map[ty][tx] = 9;
}
}
// Override createGameState to mark exit
function initLevel(level) {
const state = createGameState(level);
markExit(state.map, state.exitPos.x, state.exitPos.y);
return state;
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.keys = void 0;
exports.getMouseDelta = getMouseDelta;
exports.resetMouseDelta = resetMouseDelta;
exports.initInput = initInput;
exports.keys = {
forward: false, backward: false, left: false, right: false,
strafeLeft: false, strafeRight: false, fire: false,
weapon1: false, weapon2: false, weapon3: false,
};
let mouseDX = 0;
function getMouseDelta() { return mouseDX; }
function resetMouseDelta() { mouseDX = 0; }
const keyMap = {
'KeyW': 'forward', 'ArrowUp': 'forward',
'KeyS': 'backward', 'ArrowDown': 'backward',
'KeyA': 'left', 'ArrowLeft': 'left',
'KeyD': 'right', 'ArrowRight': 'right',
'Q': 'strafeLeft', 'E': 'strafeRight',
'Space': 'fire', 'ControlLeft': 'fire', 'ControlRight': 'fire',
'Digit1': 'weapon1', 'Numpad1': 'weapon1',
'Digit2': 'weapon2', 'Numpad2': 'weapon2',
'Digit3': 'weapon3', 'Numpad3': 'weapon3',
};
window.addEventListener('keydown', (e) => {
const k = keyMap[e.code];
if (k)
exports.keys[k] = true;
});
window.addEventListener('keyup', (e) => {
const k = keyMap[e.code];
if (k)
exports.keys[k] = false;
});
// Mouse movement for looking around
let lastMouseX = 0;
function initInput(canvas) {
canvas.addEventListener('click', () => {
canvas.requestPointerLock();
});
document.addEventListener('mousemove', (e) => {
if (document.pointerLockElement === canvas) {
mouseDX += e.movementX;
}
});
// Prevent context menu on right click
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRenderTarget = createRenderTarget;
exports.renderFrame = renderFrame;
exports.renderMinimap = renderMinimap;
const types_1 = require("./types");
const maps_1 = require("./maps");
function createRenderTarget(canvas) {
const ctx = canvas.getContext('2d', { alpha: false });
canvas.width = types_1.RENDER_W;
canvas.height = types_1.RENDER_H;
const imageData = ctx.createImageData(types_1.RENDER_W, types_1.RENDER_H);
return { canvas, ctx, imageData, pixels: imageData.data };
}
function renderFrame(rt, map, mapW, mapH, px, py, angle, enemies, pickups, exitPos, shooting, shootTimer, weaponIndex, damageFlash, screenShake) {
const { pixels, ctx } = rt;
const halfH = types_1.RENDER_H / 2;
const tanHalfFov = Math.tan(types_1.FOV / 2);
// Apply screen shake offset
const shakeX = (Math.random() - 0.5) * screenShake;
const shakeY = (Math.random() - 0.5) * screenShake;
ctx.save();
ctx.translate(shakeX, shakeY);
// Clear to dark ceiling and floor
for (let y = 0; y < types_1.RENDER_H; y++) {
for (let x = 0; x < types_1.RENDER_W; x++) {
const i = (y * types_1.RENDER_W + x) << 2;
if (y < halfH) {
// Ceiling - dark gradient
const t = 1.0 - y / halfH;
pixels[i] = Math.floor(30 * (1 - t));
pixels[i + 1] = Math.floor(25 * (1 - t));
pixels[i + 2] = Math.floor(40 * (1 - t));
}
else {
// Floor - dark gradient
const t = (y - halfH) / halfH;
pixels[i] = Math.floor(40 + 30 * t);
pixels[i + 1] = Math.floor(35 + 25 * t);
pixels[i + 2] = Math.floor(30 + 20 * t);
}
pixels[i + 3] = 255;
}
}
// Raycasting for walls
const zBuffer = new Float32Array(types_1.RENDER_W);
for (let col = 0; col < types_1.RENDER_W; col++) {
const rayAngle = angle - types_1.FOV / 2 + (col / types_1.RENDER_W) * types_1.FOV;
const sinA = Math.sin(rayAngle);
const cosA = Math.cos(rayAngle);
// DDA algorithm
let mapX = Math.floor(px);
let mapY = Math.floor(py);
const deltaDistX = Math.abs(1 / cosA) || 1e10;
const deltaDistY = Math.abs(1 / sinA) || 1e10;
let stepX, stepY;
let sideDistX, sideDistY;
if (cosA < 0) {
stepX = -1;
sideDistX = (px - mapX) * deltaDistX;
}
else {
stepX = 1;
sideDistX = (mapX + 1 - px) * deltaDistX;
}
if (sinA < 0) {
stepY = -1;
sideDistY = (py - mapY) * deltaDistY;
}
else {
stepY = 1;
sideDistY = (mapY + 1 - py) * deltaDistY;
}
let hit = false;
let side = 0; // 0 = x-side, 1 = y-side
let dist = 0;
let tileType = 1;
for (let step = 0; step < types_1.MAX_DEPTH * 4; step++) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapX < 0 || mapX >= mapW || mapY < 0 || mapY >= mapH)
break;
const tile = map[mapY][mapX];
if (tile !== 0) {
hit = true;
tileType = tile;
break;
}
}
if (hit) {
// Calculate perpendicular distance to avoid fisheye
if (side === 0) {
dist = (mapX - px + (1 - stepX) / 2) / cosA;
}
else {
dist = (mapY - py + (1 - stepY) / 2) / sinA;
}
const perpDist = dist * Math.cos(rayAngle - angle);
zBuffer[col] = perpDist;
let screenPerpDist = perpDist;
if (screenPerpDist < 0.1)
screenPerpDist = 0.1;
// Wall height on screen
const lineHeight = Math.floor(types_1.RENDER_H / perpDist);
const drawStart = Math.max(0, Math.floor(halfH - lineHeight / 2));
const drawEnd = Math.min(types_1.RENDER_H - 1, Math.floor(halfH + lineHeight / 2));
// Get wall color
let [r, g, b] = (0, maps_1.getWallColor)(tileType);
// Darken y-sides
if (side === 1) {
r *= 0.7;
g *= 0.7;
b *= 0.7;
}
// Distance fog
const fogFactor = Math.min(1, perpDist / types_1.MAX_DEPTH);
r = Math.floor(r * (1 - fogFactor * 0.8));
g = Math.floor(g * (1 - fogFactor * 0.8));
b = Math.floor(b * (1 - fogFactor * 0.8));
// Draw wall strip with horizontal stripe pattern for texture effect
const texX = side === 0 ? py + dist * sinA : px + dist * cosA;
const texFrac = texX - Math.floor(texX);
for (let row = drawStart; row <= drawEnd; row++) {
const d = row - (halfH - lineHeight / 2);
const texY = d / lineHeight;
// Simple texture pattern
let shade = 1.0;
if ((Math.floor(texY * 8) + Math.floor(texX * 4)) % 3 === 0)
shade *= 0.85;
if (texY < 0.02 || texY > 0.98)
shade *= 0.7; // darker edges
const pi = (row * types_1.RENDER_W + col) << 2;
pixels[pi] = Math.floor(r * shade);
pixels[pi + 1] = Math.floor(g * shade);
pixels[pi + 2] = Math.floor(b * shade);
}
}
else {
zBuffer[col] = types_1.MAX_DEPTH;
}
}
const sprites = [];
for (const e of enemies) {
if (!e.alive)
continue;
const dx = e.x - px;
const dy = e.y - py;
sprites.push({ x: e.x, y: e.y, dist: Math.sqrt(dx * dx + dy * dy), data: e });
}
for (const p of pickups) {
if (p.collected)
continue;
const dx = p.x - px;
const dy = p.y - py;
sprites.push({ x: p.x, y: p.y, dist: Math.sqrt(dx * dx + dy * dy), data: p });
}
// Sort back to front
sprites.sort((a, b) => b.dist - a.dist);
for (const sprite of sprites) {
const dx = sprite.x - px;
const dy = sprite.y - py;
// Transform with inverse camera matrix
const invDet = 1.0 / (Math.cos(angle + Math.PI / 2) * Math.sin(angle) - Math.sin(angle + Math.PI / 2) * Math.cos(angle));
// Simplified: use angle directly
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
const planeX = Math.cos(angle + Math.PI / 2) * tanHalfFov;
const planeY = Math.sin(angle + Math.PI / 2) * tanHalfFov;
// Transform sprite position relative to camera
const transformX = cosA * dy - sinA * dx;
const transformY = -planeY * dx + cosA * dy; // simplified depth
if (transformX <= 0.1)
continue; // behind player
const spriteScreenX = Math.floor((types_1.RENDER_W / 2) * (1 + (-dx * cosA + dy * sinA) / transformX));
// Actually let me redo this properly...
// Proper sprite transformation
const camX = -dx * Math.cos(angle) + dy * Math.sin(angle); // depth
const camY = dx * Math.sin(angle) + dy * Math.cos(angle); // lateral (flipped for screen coords)
if (camX <= 0.1)
continue;
const spriteScale = types_1.RENDER_H / camX;
const spriteScreenCenterX = Math.floor((types_1.RENDER_W / 2) * (1 - camY / camX));
// Determine sprite size and color based on type
let spriteW, spriteH;
let colors;
const isEnemy = 'health' in sprite.data && 'type' in sprite.data;
if (isEnemy) {
const e = sprite.data;
const size = e.type === 'boss' ? 1.5 : 1.0;
spriteW = Math.floor(spriteScale * 0.6 * size);
spriteH = Math.floor(spriteScale * 0.9 * size);
if (e.type === 'grunt')
colors = [[180, 60, 40], [150, 50, 30]];
else if (e.type === 'soldier')
colors = [[60, 120, 60], [50, 100, 50]];
else
colors = [[180, 40, 120], [150, 30, 100]]; // boss
// Draw enemy body with detailed sprite shape
const startX = Math.max(0, spriteScreenCenterX - Math.floor(spriteW / 2));
const endX = Math.min(types_1.RENDER_W - 1, spriteScreenCenterX + Math.floor(spriteW / 2));
const startY = Math.max(0, halfH - Math.floor(spriteH / 2));
const endY = Math.min(types_1.RENDER_H - 1, halfH + Math.floor(spriteH / 2));
for (let sx = startX; sx <= endX; sx++) {
if (camX >= zBuffer[sx])
continue;
const localX = (sx - spriteScreenCenterX) / (spriteW / 2);
for (let sy = startY; sy <= endY; sy++) {
if (camX >= zBuffer[sx])
continue;
const localY = (sy - halfH) / (spriteH / 2);
let inBody = false;
let isEye = false;
let isMouth = false;
// Head region: top third
if (localY < -0.15) {
const headRadius = 0.38 + Math.max(0, -(localY + 0.15)) * 0.2;
inBody = localX * localX + (localY + 0.4) * (localY + 0.4) < headRadius * headRadius;
// Eyes - two bright spots on the face
if (inBody && localY > -0.35 && localY < -0.2) {
const eyeDist = Math.abs(Math.abs(localX) - 0.15);
if (eyeDist < 0.06)
isEye = true;
}
// Mouth
if (inBody && localY > -0.18 && localY < -0.12) {
if (Math.abs(localX) < 0.15)
isMouth = true;
}
}
// Torso: middle section
else if (localY <= 0.4) {
const torsoWidth = 0.35 + Math.abs(localY - 0.12) * 0.15;
inBody = Math.abs(localX) < torsoWidth;
// Arms on sides
if (!inBody && localY > -0.05 && localY < 0.3) {
const armDist = Math.abs(Math.abs(localX) - 0.48);
if (armDist < 0.12)
inBody = true;
}
}
// Legs: bottom section
else {
const legSpread = 0.15 + (localY - 0.4) * 0.3;
const leftLeg = Math.abs(localX + legSpread) < 0.12;
const rightLeg = Math.abs(localX - legSpread) < 0.12;
inBody = leftLeg || rightLeg;
}
if (inBody) {
const pi = (sy * types_1.RENDER_W + sx) << 2;
let [cr, cg, cb] = colors[0];
// Distance fog
const fogFactor = Math.min(1, camX / types_1.MAX_DEPTH);
if (isEye) {
// Glowing eyes
cr = 255;
cg = 255;
cb = 200;
}
else if (isMouth) {
// Dark mouth
cr = Math.floor(cr * 0.3);
cg = Math.floor(cg * 0.3);
cb = Math.floor(cb * 0.3);
}
pixels[pi] = Math.floor(cr * (1 - fogFactor * 0.8));
pixels[pi + 1] = Math.floor(cg * (1 - fogFactor * 0.8));
pixels[pi + 2] = Math.floor(cb * (1 - fogFactor * 0.8));
// Health bar on top of enemy
if (sy === startY && sx % 3 === 0) {
const healthPct = e.health / e.maxHealth;
pixels[pi] = Math.floor(255 * healthPct);
pixels[pi + 1] = Math.floor(255 * (1 - healthPct));
pixels[pi + 2] = 0;
}
}
}
}
}
else {
// Pickup rendering
const p = sprite.data;
spriteW = Math.floor(spriteScale * 0.3);
spriteH = Math.floor(spriteScale * 0.3);
let color;
if (p.type === 'health')
color = [255, 60, 60];
else if (p.type === 'ammo_shotgun')
color = [255, 180, 40];
else
color = [60, 180, 255];
const startX = Math.max(0, spriteScreenCenterX - Math.floor(spriteW / 2));
const endX = Math.min(types_1.RENDER_W - 1, spriteScreenCenterX + Math.floor(spriteW / 2));
const startY = Math.max(0, halfH + Math.floor(spriteH * 0.3) - Math.floor(spriteH / 2));
const endY = Math.min(types_1.RENDER_H - 1, halfH + Math.floor(spriteH * 0.3) + Math.floor(spriteH / 2));
// Bobbing animation
const bobOffset = Math.sin(Date.now() / 300 + p.id) * spriteScale * 0.05;
for (let sx = startX; sx <= endX; sx++) {
if (camX >= zBuffer[sx])
continue;
const bOff = Math.floor(bobOffset);
for (let sy = startY + bOff; sy <= endY + bOff; sy++) {
if (sy < 0 || sy >= types_1.RENDER_H)
continue;
const pi = (sy * types_1.RENDER_W + sx) << 2;
// Diamond shape for pickup
const cx = Math.abs(sx - spriteScreenCenterX) / (spriteW / 2);
const cy = Math.abs(sy - halfH - spriteH * 0.3) / (spriteH / 2);
if (cx + cy < 1) {
// Glow effect
const glow = 1 - (cx + cy) * 0.5;
pixels[pi] = Math.min(255, color[0] + glow * 80);
pixels[pi + 1] = Math.min(255, color[1] + glow * 40);
pixels[pi + 2] = Math.min(255, color[2] + glow * 40);
}
}
}
}
}
// Render exit portal
{
const dx = exitPos.x - px;
const dy = exitPos.y - py;
const camX = -dx * Math.cos(angle) + dy * Math.sin(angle);
const camY = dx * Math.sin(angle) + dy * Math.cos(angle);
if (camX > 0.1) {
const spriteScale = types_1.RENDER_H / camX;
const exitW = Math.floor(spriteScale * 0.5);
const exitH = Math.floor(spriteScale * 0.8);
const screenCenterX = Math.floor((types_1.RENDER_W / 2) * (1 - camY / camX));
const eHalfW = Math.floor(exitW / 2);
const eHalfH = Math.floor(exitH / 2);
for (let sx = Math.max(0, screenCenterX - eHalfW); sx < Math.min(types_1.RENDER_W - 1, screenCenterX + eHalfW); sx++) {
if (camX >= zBuffer[sx])
continue;
const localX = Math.abs(sx - screenCenterX) / (exitW / 2);
for (let sy = halfH - eHalfH; sy < halfH + eHalfH; sy++) {
if (sy < 0 || sy >= types_1.RENDER_H)
continue;
const localY = Math.abs(sy - halfH) / (exitH / 2);
// Portal effect - swirling colors
const t = Date.now() / 500 + sx * 0.1 + sy * 0.1;
if (localX < 0.9 && localY < 0.9) {
const pi = (sy * types_1.RENDER_W + sx) << 2;
pixels[pi] = Math.floor(50 + 100 * Math.sin(t));
pixels[pi + 1] = Math.floor(50 + 100 * Math.cos(t * 0.7));
pixels[pi + 2] = Math.floor(150 + 100 * Math.sin(t * 1.3));
// Bright border
if (localX > 0.8 || localY > 0.8) {
pixels[pi] = 200;
pixels[pi + 1] = 200;
pixels[pi + 2] = 255;
}
}
}
}
}
}
// Weapon rendering (bottom center)
renderWeapon(rt, weaponIndex, shooting, shootTimer);
// Damage flash overlay
if (damageFlash > 0) {
const alpha = Math.min(1, damageFlash / 300);
for (let y = 0; y < types_1.RENDER_H; y++) {
for (let x = 0; x < types_1.RENDER_W; x++) {
const i = (y * types_1.RENDER_W + x) << 2;
pixels[i] = Math.min(255, pixels[i] + Math.floor(180 * alpha));
pixels[i + 1] = Math.floor(pixels[i + 1] * (1 - alpha * 0.7));
pixels[i + 2] = Math.floor(pixels[i + 2] * (1 - alpha * 0.7));
}
}
}
// Vignette effect
for (let y = 0; y < types_1.RENDER_H; y++) {
for (let x = 0; x < types_1.RENDER_W; x++) {
const nx = (x / types_1.RENDER_W - 0.5) * 2;
const ny = (y / types_1.RENDER_H - 0.5) * 2;
const vignette = 1 - (nx * nx + ny * ny) * 0.3;
const i = (y * types_1.RENDER_W + x) << 2;
pixels[i] = Math.floor(pixels[i] * vignette);
pixels[i + 1] = Math.floor(pixels[i + 1] * vignette);
pixels[i + 2] = Math.floor(pixels[i + 2] * vignette);
}
}
ctx.restore();
rt.ctx.putImageData(rt.imageData, 0, 0);
// Draw exit proximity indicator on canvas overlay
const distToExit = Math.sqrt((exitPos.x - px) ** 2 + (exitPos.y - py) ** 2);
if (distToExit < 15 && distToExit > 1.5) {
ctx.fillStyle = `rgba(100, 200, 255, ${Math.min(0.8, (15 - distToExit) / 14)})`;
ctx.font = 'bold 16px Courier New';
ctx.textAlign = 'center';
const exitAngle = Math.atan2(exitPos.y - py, exitPos.x - px);
let angleDiff = exitAngle - angle;
while (angleDiff > Math.PI)
angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI)
angleDiff += Math.PI * 2;
const arrowX = types_1.RENDER_W / 2 + Math.sin(angleDiff) * 100;
ctx.fillText(`EXIT ${Math.ceil(distToExit)}m`, arrowX, 30);
ctx.font = '24px Courier New';
ctx.fillText('▼', arrowX, 52);
}
}
function renderWeapon(rt, weaponIndex, shooting, shootTimer) {
const { pixels } = rt;
const cx = types_1.RENDER_W / 2;
const baseY = types_1.RENDER_H;
const bobX = Math.sin(Date.now() / 300) * 4;
const bobY = Math.abs(Math.cos(Date.now() / 300)) * 3;
let recoilOffset = 0;
if (shooting && shootTimer > 0) {
recoilOffset = Math.min(20, shootTimer * 0.15);
}
const weaponY = baseY + bobY - recoilOffset;
// Draw weapon based on type
if (weaponIndex === 0) {
// Pistol
drawPistol(pixels, cx + bobX, weaponY);
}
else if (weaponIndex === 1) {
// Shotgun
drawShotgun(pixels, cx + bobX, weaponY);
}
else {
// Rifle
drawRifle(pixels, cx + bobX, weaponY);
}
// Muzzle flash
if (shooting && shootTimer > 150) {
const flashSize = Math.floor(30 * (shootTimer - 150) / 100);
for (let dy = -flashSize; dy <= flashSize; dy++) {
for (let dx = -flashSize; dx <= flashSize; dx++) {
if (dx * dx + dy * dy > flashSize * flashSize)
continue;
const sx = cx + bobX + dx;
const sy = weaponY - 100 + dy;
if (sx >= 0 && sx < types_1.RENDER_W && sy >= 0 && sy < types_1.RENDER_H) {
const i = (sy * types_1.RENDER_W + sx) << 2;
const brightness = 1 - Math.sqrt(dx * dx + dy * dy) / flashSize;
pixels[i] = 255;
pixels[i + 1] = Math.floor(200 * brightness);
pixels[i + 2] = Math.floor(50 * brightness);
}
}
}
}
}
function drawPistol(pixels, cx, baseY) {
const w = types_1.RENDER_W;
// Barrel
for (let y = baseY - 100; y < baseY - 40; y++) {
for (let x = cx - 8; x <= cx + 8; x++) {
if (y >= 0 && y < types_1.RENDER_H) {
const i = (y * w + x) << 2;
pixels[i] = 60;
pixels[i + 1] = 55;
pixels[i + 2] = 50;
}
}
}
// Handle
for (let y = baseY - 40; y < baseY; y++) {
for (let x = cx - 12; x <= cx + 6; x++) {
if (y >= 0 && y < types_1.RENDER_H) {
const i = (y * w + x) << 2;
pixels[i] = 80;
pixels[i + 1] = 50;
pixels[i + 2] = 30;
}
}
}
}
function drawShotgun(pixels, cx, baseY) {
const w = types_1.RENDER_W;
// Double barrel
for (let y = baseY - 120; y < baseY - 30; y++) {
for (let x = cx - 18; x <= cx + 18; x++) {
if (y >= 0 && y < types_1.RENDER_H) {
const i = (y * w + x) << 2;
pixels[i] = 50;
pixels[i + 1] = 45;
pixels[i + 2] = 40;
}
}
}
// Stock
for (let y = baseY - 30; y < baseY; y++) {
for (let x = cx - 20; x <= cx + 15; x++) {
if (y >= 0 && y < types_1.RENDER_H) {
const i = (y * w + x) << 2;
pixels[i] = 90;
pixels[i + 1] = 60;
pixels[i + 2] = 30;
}
}
}
}
function drawRifle(pixels, cx, baseY) {
const w = types_1.RENDER_W;
// Long barrel
for (let y = baseY - 130; y < baseY - 25; y++) {
for (let x = cx - 6; x <= cx + 6; x++) {
if (y >= 0 && y < types_1.RENDER_H) {
const i = (y * w + x) << 2;
pixels[i] = 45;
pixels[i + 1] = 50;
pixels[i + 2] = 55;
}
}
}
// Body
for (let y = baseY - 30; y < baseY; y++) {
for (let x = cx - 14; x <= cx + 14; x++) {
if (y >= 0 && y < types_1.RENDER_H) {
const i = (y * w + x) << 2;
pixels[i] = 55;
pixels[i + 1] = 60;
pixels[i + 2] = 65;
}
}
}
}
function renderMinimap(canvas, map, px, py, angle, enemies, pickups, exitPos) {
const ctx = canvas.getContext('2d');
const scale = 6;
const mw = map[0].length * scale;
const mh = map.length * scale;
canvas.width = mw;
canvas.height = mh;
// Background
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, mw, mh);
// Walls
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[0].length; x++) {
if (map[y][x] !== 0) {
const [r, g, b] = (0, maps_1.getWallColor)(map[y][x]);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x * scale, y * scale, scale, scale);
}
else if (map[y][x] === 9) {
// Exit portal on minimap
const t = Date.now() / 300;
ctx.fillStyle = `hsl(${(t * 60) % 360}, 80%, 60%)`;
ctx.fillRect(x * scale, y * scale, scale, scale);
}
}
}
// Pickups
for (const p of pickups) {
if (p.collected)
continue;
ctx.fillStyle = p.type === 'health' ? '#ff4444' : p.type === 'ammo_shotgun' ? '#ffaa00' : '#44aaff';
ctx.fillRect(p.x * scale - 2, p.y * scale - 2, 4, 4);
}
// Enemies
for (const e of enemies) {
if (!e.alive)
continue;
ctx.fillStyle = e.type === 'boss' ? '#ff00ff' : e.type === 'soldier' ? '#00ff00' : '#ff6644';
const size = e.type === 'boss' ? 5 : 3;
ctx.fillRect(e.x * scale - size / 2, e.y * scale - size / 2, size, size);
}
// Player
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(px * scale, py * scale, 3, 0, Math.PI * 2);
ctx.fill();
// Direction indicator
ctx.strokeStyle = '#ffff00';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(px * scale, py * scale);
ctx.lineTo((px + Math.cos(angle) * 2) * scale, (py + Math.sin(angle) * 2) * scale);
ctx.stroke();
}
};
// ── module: src/logic.ts ──
__mods["src/logic.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updatePlayer = updatePlayer;
exports.updateShooting = updateShooting;
exports.tryShoot = tryShoot;
exports.updateEnemies = updateEnemies;
exports.updatePickups = updatePickups;
exports.switchWeapon = switchWeapon;
const types_1 = require("./types");
const maps_1 = require("./maps");
const sounds_1 = require("./sounds");
const PLAYER_SPEED = 3.5; // tiles per second
const ROTATION_SPEED = 2.0; // radians per second
const PLAYER_RADIUS = 0.25;
function updatePlayer(state, dt, keys, mouseDelta) {
const p = state.player;
if (!p.alive)
return;
// Rotation from mouse
p.angle += mouseDelta * ROTATION_SPEED * 0.003;
// Keyboard rotation (slower)
if (keys.left)
p.angle -= ROTATION_SPEED * dt;
if (keys.right)
p.angle += ROTATION_SPEED * dt;
// Movement
let dx = 0, dy = 0;
const cosA = Math.cos(p.angle);
const sinA = Math.sin(p.angle);
if (keys.forward) {
dx += cosA;
dy += sinA;
}
if (keys.backward) {
dx -= cosA;
dy -= sinA;
}
if (keys.strafeLeft) {
dx += sinA;
dy -= cosA;
}
if (keys.strafeRight) {
dx -= sinA;
dy += cosA;
}
// Normalize diagonal movement
const len = Math.sqrt(dx * dx + dy * dy);
if (len > 0) {
dx = (dx / len) * PLAYER_SPEED * dt;
dy = (dy / len) * PLAYER_SPEED * dt;
}
// Collision detection with sliding
const newX = p.x + dx;
const newY = p.y + dy;
if (!(0, maps_1.isWall)(state.map, newX + (dx > 0 ? PLAYER_RADIUS : -PLAYER_RADIUS), p.y)) {
p.x = newX;
}
if (!(0, maps_1.isWall)(state.map, p.x, newY + (dy > 0 ? PLAYER_RADIUS : -PLAYER_RADIUS))) {
p.y = newY;
}
// Check exit
const distToExit = (0, maps_1.dist)(p.x, p.y, state.exitPos.x, state.exitPos.y);
if (distToExit < 0.8) {
state.gameState = 'levelComplete';
}
}
function updateShooting(state, now) {
const p = state.player;
if (!p.alive || state.gameState !== 'playing')
return;
const weapon = types_1.WEAPONS[p.weaponIndex];
// Update shoot timer
if (p.shootTimer > 0) {
p.shootTimer -= 16; // approximate frame time
if (p.shootTimer <= 0) {
p.shooting = false;
p.shootTimer = 0;
}
}
// Check weapon switch
const keys = window.__gameKeys;
}
function tryShoot(state, now) {
const p = state.player;
if (!p.alive)
return;
const weapon = types_1.WEAPONS[p.weaponIndex];
// Check fire rate
if (now - p.lastShot < weapon.fireRate)
return;
// Check ammo
if (weapon.ammoCost > 0 && p.ammo[p.weaponIndex] <= 0)
return;
// Fire!
(0, sounds_1.playShoot)(p.weaponIndex);
p.lastShot = now;
p.shooting = true;
p.shootTimer = weapon.reloadTime;
if (weapon.ammoCost > 0) {
p.ammo[p.weaponIndex] -= weapon.ammoCost;
}
// Hit detection - raycast for each pellet
for (let i = 0; i < weapon.pellets; i++) {
const spreadAngle = (Math.random() - 0.5) * weapon.spread;
const shootAngle = p.angle + spreadAngle;
const sinA = Math.sin(shootAngle);
const cosA = Math.cos(shootAngle);
// Find closest enemy in this direction within range
let closestDist = weapon.range;
let hitEnemy = null;
for (const e of state.enemies) {
if (!e.alive)
continue;
const dx = e.x - p.x;
const dy = e.y - p.y;
const distToE = Math.sqrt(dx * dx + dy * dy);
if (distToE > closestDist)
continue;
// Check if enemy is in the line of fire
// Project enemy position onto ray direction
const proj = dx * cosA + dy * sinA;
if (proj < 0 || proj > distToE)
continue;
// Perpendicular distance from ray to enemy center
const perpDist = Math.abs(dx * sinA - dy * cosA);
const hitRadius = e.type === 'boss' ? 0.6 : 0.4;
if (perpDist < hitRadius) {
// Check wall between player and enemy
if (!wallBetween(state.map, p.x, p.y, e.x, e.y)) {
closestDist = distToE;
hitEnemy = e;
}
}
}
if (hitEnemy) {
(0, sounds_1.playHit)();
const dmg = weapon.damage + Math.floor(Math.random() * 5);
hitEnemy.health -= dmg;
if (hitEnemy.health <= 0) {
(0, sounds_1.playEnemyDeath)();
hitEnemy.alive = false;
p.score += hitEnemy.type === 'boss' ? 500 : hitEnemy.type === 'soldier' ? 100 : 50;
}
}
}
}
function wallBetween(map, x1, y1, x2, y2) {
const steps = Math.ceil(Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) * 4);
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = x1 + (x2 - x1) * t;
const y = y1 + (y2 - y1) * t;
if ((0, maps_1.isWall)(map, x, y))
return true;
}
return false;
}
function updateEnemies(state, dt, now) {
const p = state.player;
if (!p.alive)
return;
for (const e of state.enemies) {
if (!e.alive)
continue;
const dx = p.x - e.x;
const dy = p.y - e.y;
const distToPlayer = Math.sqrt(dx * dx + dy * dy);
// Alert check
if (distToPlayer > e.alertDist)
continue;
// Move toward player
if (distToPlayer > e.attackRange * 0.7) {
const moveAngle = Math.atan2(dy, dx);
const speed = e.speed * dt;
const newX = e.x + Math.cos(moveAngle) * speed;
const newY = e.y + Math.sin(moveAngle) * speed;
// Simple collision - try to slide around walls
if (!(0, maps_1.isWall)(state.map, newX, e.y)) {
e.x = newX;
}
else if (!(0, maps_1.isWall)(state.map, e.x, newY)) {
e.y = newY;
}
else {
// Try diagonal
const diagX = e.x + Math.cos(moveAngle) * speed * 0.7;
const diagY = e.y + Math.sin(moveAngle + (Math.random() > 0.5 ? 1 : -1) * 0.5) * speed * 0.7;
if (!(0, maps_1.isWall)(state.map, diagX, diagY)) {
e.x = diagX;
e.y = diagY;
}
}
}
// Attack player
if (distToPlayer < e.attackRange && now - e.lastAttack > e.attackRate) {
e.lastAttack = now;
(0, sounds_1.playDamage)();
p.health -= e.damage + Math.floor(Math.random() * 5);
if (p.health <= 0) {
p.health = 0;
p.alive = false;
state.gameState = 'dead';
state.deathTimer = 2000; // 2 second respawn delay
}
}
}
}
function updatePickups(state) {
const p = state.player;
if (!p.alive)
return;
for (const pickup of state.pickups) {
if (pickup.collected)
continue;
const distToP = (0, maps_1.dist)(p.x, p.y, pickup.x, pickup.y);
if (distToP > 0.6)
continue;
pickup.collected = true;
switch (pickup.type) {
case 'health':
p.health = Math.min(p.maxHealth, p.health + 25);
break;
case 'ammo_shotgun':
p.ammo[1] += 8;
break;
case 'ammo_rifle':
p.ammo[2] += 10;
break;
}
}
}
function switchWeapon(state, index) {
if (index >= 0 && index < types_1.WEAPONS.length) {
state.player.weaponIndex = index;
}
}
};
// ── module: src/sounds.ts ──
__mods["src/sounds.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.playShoot = playShoot;
exports.playHit = playHit;
exports.playEnemyDeath = playEnemyDeath;
exports.playPickup = playPickup;
exports.playDamage = playDamage;
exports.playLevelComplete = playLevelComplete;
exports.playDeath = playDeath;
// Simple synthesized sound effects using Web Audio API
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
else if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function safePlay(fn) {
try {
const ctx = audioCtx;
if (!ctx || ctx.state === 'suspended')
return;
fn(ctx, ctx.currentTime);
}
catch (_e) { /* Audio not available */ }
}
function playShoot(weaponIndex) {
safePlay((ctx, now) => {
if (weaponIndex === 0) {
// Pistol - sharp crack
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(200, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.15);
const bufferSize = ctx.sampleRate * 0.05;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++)
data[i] = Math.random() * 2 - 1;
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const nGain = ctx.createGain();
nGain.gain.setValueAtTime(0.4, now);
nGain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
noise.connect(nGain).connect(ctx.destination);
noise.start(now);
}
else if (weaponIndex === 1) {
// Shotgun - loud boom with rumble
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.2);
gain.gain.setValueAtTime(0.5, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.3);
const bufferSize = ctx.sampleRate * 0.15;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++)
data[i] = Math.random() * 2 - 1;
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const nGain = ctx.createGain();
nGain.gain.setValueAtTime(0.6, now);
nGain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
noise.connect(nGain).connect(ctx.destination);
noise.start(now);
}
else {
// Rifle - rapid crack
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(1200, now);
osc.frequency.exponentialRampToValueAtTime(400, now + 0.06);
gain.gain.setValueAtTime(0.35, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.08);
const bufferSize = ctx.sampleRate * 0.03;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++)
data[i] = Math.random() * 2 - 1;
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const nGain = ctx.createGain();
nGain.gain.setValueAtTime(0.3, now);
nGain.gain.exponentialRampToValueAtTime(0.001, now + 0.04);
noise.connect(nGain).connect(ctx.destination);
noise.start(now);
}
});
}
function playHit() {
safePlay((ctx, now) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(400, now);
osc.frequency.exponentialRampToValueAtTime(150, now + 0.1);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.12);
});
}
function playEnemyDeath() {
safePlay((ctx, now) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(500, now);
osc.frequency.exponentialRampToValueAtTime(80, now + 0.4);
gain.gain.setValueAtTime(0.25, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.4);
});
}
function playPickup() {
safePlay((ctx, now) => {
[523, 659, 784].forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + i * 0.08);
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(0.2, now + i * 0.08);
gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.08 + 0.15);
osc.connect(gain).connect(ctx.destination);
osc.start(now + i * 0.08);
osc.stop(now + i * 0.08 + 0.15);
});
});
}
function playDamage() {
safePlay((ctx, now) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.15);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.15);
});
}
function playLevelComplete() {
safePlay((ctx, now) => {
[523, 659, 784, 1047].forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(freq, now + i * 0.15);
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(0.15, now + i * 0.15);
gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.15 + 0.3);
osc.connect(gain).connect(ctx.destination);
osc.start(now + i * 0.15);
osc.stop(now + i * 0.15 + 0.3);
});
});
}
function playDeath() {
safePlay((ctx, now) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(40, now + 1.0);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 1.0);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 1.0);
});
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"9ee1540e-bcd2-44b1-94b2-15e8f972a1f0","tokensIn":3722438,"tokensOut":47773,"tokensTotal":3770211,"turns":84,"toolCalls":83,"failedToolCalls":5,"timestamp":"2026-07-30T20:02:49.786Z"} -->