DOOM
Qwen 3.6 27B MTP GGUF UD Q5 K XL · 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 74,741 bytes · SHA-256 0461ec8eae60
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOMED CORRIDORS - Retro FPS</title>
<style>
/* ── css: base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%; overflow: hidden;
background: #000;
font-family: 'Courier New', monospace;
}
canvas {
display: block;
width: 100%; height: 100%;
image-rendering: pixelated;
cursor: none;
}
/* ── css: hud ── */
#hud {
position: fixed; bottom: 0; left: 0; right: 0;
height: 72px;
background: linear-gradient(to top, #1a1a1a 0%, #2a2a2a 60%, transparent 100%);
border-top: 2px solid #444;
display: flex; justify-content: space-between; align-items: center;
padding: 8px 24px;
z-index: 10;
pointer-events: none;
color: #fff;
font-size: 16px;
font-weight: bold;
text-shadow: 1px 1px 2px #000;
font-family: 'Courier New', monospace;
}
#hud-left, #hud-right, #hud-center {
display: flex; flex-direction: column; gap: 2px;
}
#hud-center { align-items: center; }
#hud-right { align-items: flex-end; }
#weapon-name {
font-size: 22px;
color: #ff8844;
text-shadow: 0 0 10px rgba(255, 136, 68, 0.5);
}
#crosshair {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 24px; height: 24px;
z-index: 11;
pointer-events: none;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute;
background: rgba(255, 255, 255, 0.7);
}
#crosshair::before {
width: 2px; height: 24px; left: 11px; top: 0;
}
#crosshair::after {
width: 24px; height: 2px; top: 11px; left: 0;
}
#message-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 72px;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
z-index: 12;
pointer-events: none;
background: rgba(0,0,0,0.5);
color: #ff4444;
font-size: 40px;
font-weight: bold;
text-shadow: 2px 2px 4px #000;
opacity: 0;
transition: opacity 0.3s;
text-align: center;
line-height: 1.4;
font-family: 'Courier New', monospace;
}
#message-overlay.visible { opacity: 1; }
#controls-hint {
position: fixed; top: 8px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.4);
font-size: 12px;
z-index: 10;
pointer-events: none;
font-family: 'Courier New', monospace;
}
#damage-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 9;
border: 8px solid transparent;
transition: border-color 0.1s;
}
#damage-overlay.hit { border-color: rgba(255,0,0,0.6); }
#game-over-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
z-index: 50;
pointer-events: none;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
background: rgba(80, 0, 0, 0.4);
opacity: 0;
transition: opacity 0.5s;
}
#game-over-overlay.visible { opacity: 1; }
#game-over-overlay .respawn-timer {
font-size: 24px;
color: #ffaaaa;
font-family: 'Courier New', monospace;
margin-top: 10px;
}
/* ── css: title ── */
#title-screen {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(to bottom, #000 0%, #1a0000 50%, #000 100%);
display: flex; flex-direction: column;
justify-content: center; align-items: center;
z-index: 200;
cursor: pointer;
}
#title-text {
font-size: 64px;
font-weight: bold;
color: #cc0000;
text-shadow: 0 0 20px #ff0000, 0 0 40px #880000, 3px 3px 0 #440000;
letter-spacing: 8px;
margin-bottom: 10px;
}
#subtitle {
font-size: 20px;
color: #884444;
letter-spacing: 12px;
margin-bottom: 60px;
}
#start-prompt {
font-size: 24px;
color: #ff4444;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
#controls-info {
font-size: 14px;
color: #666;
margin-top: 30px;
letter-spacing: 2px;
}
#vignette {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 8;
}
/* ── css: gameover ── */
#game-over-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
z-index: 50;
pointer-events: none;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
background: rgba(80, 0, 0, 0.3);
opacity: 0;
transition: opacity 0.5s;
}
#game-over-overlay.visible { opacity: 1; }
#death-text {
font-size: 64px;
color: #cc0000;
font-weight: bold;
text-shadow: 0 0 20px #ff0000, 3px 3px 0 #440000;
font-family: 'Courier New', monospace;
letter-spacing: 6px;
}
#game-over-overlay .respawn-timer {
font-size: 20px;
color: #ffaaaa;
font-family: 'Courier New', monospace;
margin-top: 20px;
}
</style>
</head>
<body>
<div id="damage-overlay"></div><div id="game-over-overlay">
<div id="death-text">YOU DIED</div>
<div class="respawn-timer" id="respawn-timer"></div>
</div>
<canvas id="game"></canvas>
<div id="hud">
<div id="hud-left">
<div id="health-display">❤️ 100</div>
<div id="armor-display">🛡️ 0</div>
</div>
<div id="hud-center">
<div id="weapon-name">PISTOL</div>
</div>
<div id="hud-right">
<div id="ammo-display">🔫 20 / 99</div>
<div id="score-display">💀 0 / 20</div>
</div>
</div>
<div id="crosshair"></div>
<div id="message-overlay"></div>
<div id="controls-hint">WASD: Move | Mouse: Look | Click: Shoot | 1-3: Weapons | R: Reload</div>
<div id="title-screen">
<div id="title-text">DOOMED CORRIDORS</div>
<div id="subtitle">A Retro FPS</div>
<div id="start-prompt">Click to Play</div>
<div id="controls-info">WASD: Move | Mouse: Look | Click: Shoot | 1-3: Weapons</div>
</div>
<canvas id="vignette"></canvas>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./renderer":"src/renderer.ts","./sprites":"src/sprites.ts","./weapons":"src/weapons.ts","./input":"src/input.ts","./audio":"src/audio.ts","./ai":"src/ai.ts","./map":"src/map.ts","./player":"src/player.ts"},"src/player.ts":{"./types":"src/types.ts"},"src/game.ts":{"./player":"src/player.ts","./types":"src/types.ts","./map":"src/map.ts"},"src/renderer.ts":{"./game":"src/game.ts","./map":"src/map.ts"},"src/sprites.ts":{"./game":"src/game.ts","./types":"src/types.ts"},"src/weapons.ts":{"./game":"src/game.ts","./types":"src/types.ts","./map":"src/map.ts","./audio":"src/audio.ts"},"src/input.ts":{"./game":"src/game.ts","./audio":"src/audio.ts"},"src/ai.ts":{"./game":"src/game.ts","./types":"src/types.ts","./map":"src/map.ts","./player":"src/player.ts","./audio":"src/audio.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 });
exports.start = start;
const game_1 = require("./game");
const renderer_1 = require("./renderer");
const sprites_1 = require("./sprites");
const weapons_1 = require("./weapons");
const input_1 = require("./input");
const audio_1 = require("./audio");
const ai_1 = require("./ai");
const map_1 = require("./map");
const player_1 = require("./player");
let g;
let ws;
function start() {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
if (g) {
g.width = canvas.width;
g.height = canvas.height;
g.zBuffer = new Array(g.width);
}
}
window.addEventListener('resize', resize);
resize();
g = (0, game_1.createGame)(canvas, ctx, canvas.width, canvas.height);
ws = (0, weapons_1.createWeaponState)();
(0, input_1.setupInput)(g);
g.running = true;
showMessage('DOOMED CORRIDORS', 3000);
requestAnimationFrame(gameLoop);
}
function gameLoop(timestamp) {
const dt = Math.min(50, timestamp - g.lastTime);
g.lastTime = timestamp;
if (g.running) {
update(dt, timestamp);
}
renderFrame();
requestAnimationFrame(gameLoop);
}
function update(dt, now) {
g.levelTime += dt;
// Handle weapon switch
if (g._weaponSwitchTarget !== null) {
(0, weapons_1.trySwitchWeapon)(g, ws, g._weaponSwitchTarget, now);
g._weaponSwitchTarget = null;
}
(0, weapons_1.updateSwitch)(g, ws, now);
(0, weapons_1.updateReload)(g, ws, now);
// Mouse look
const mouseSensitivity = 0.002;
g.player.angle += g.mouse.dx * mouseSensitivity;
g.player.pitch -= g.mouse.dy * mouseSensitivity * 0.7;
g.player.pitch = Math.max(-0.6, Math.min(0.6, g.player.pitch));
g.mouse.dx = 0;
g.mouse.dy = 0;
if (!g.player.alive) {
g.player.deadTimer -= dt;
const goOverlay = document.getElementById('game-over-overlay');
const respawnTimer = document.getElementById('respawn-timer');
if (goOverlay)
goOverlay.classList.add('visible');
if (respawnTimer)
respawnTimer.textContent = `Respawning in ${Math.ceil(g.player.deadTimer / 1000)}...`;
if (g.player.deadTimer <= 0) {
(0, player_1.respawnPlayer)(g.player, 1.5, 1.5);
if (goOverlay)
goOverlay.classList.remove('visible');
showMessage('RESPAWNED', 1500);
}
else if (g.player.deadTimer < 2800 && g.player.deadTimer > 2700) {
(0, audio_1.playSound)('death');
}
return;
}
// Hide death overlay when alive
const deathOverlay = document.getElementById('game-over-overlay');
if (deathOverlay)
deathOverlay.classList.remove('visible');
if (g.won)
return;
// Movement
const moveSpeed = g.keys.shift ? g.player.moveSpeed * 1.6 : g.player.moveSpeed;
let moving = false;
let moveX = 0, moveY = 0;
if (g.keys.w) {
moveX += Math.cos(g.player.angle);
moveY += Math.sin(g.player.angle);
moving = true;
}
if (g.keys.s) {
moveX -= Math.cos(g.player.angle);
moveY -= Math.sin(g.player.angle);
moving = true;
}
if (g.keys.a) {
moveX += Math.cos(g.player.angle - Math.PI / 2);
moveY += Math.sin(g.player.angle - Math.PI / 2);
moving = true;
}
if (g.keys.d) {
moveX += Math.cos(g.player.angle + Math.PI / 2);
moveY += Math.sin(g.player.angle + Math.PI / 2);
moving = true;
}
// Normalize
const mag = Math.sqrt(moveX * moveX + moveY * moveY);
if (mag > 0) {
moveX = (moveX / mag) * moveSpeed * (dt / 1000);
moveY = (moveY / mag) * moveSpeed * (dt / 1000);
}
// Collision and movement
const slideRadius = 0.2;
// Try X movement
const newX = g.player.x + moveX;
let blockedX = false;
for (let dy = -slideRadius; dy <= slideRadius; dy += slideRadius * 2) {
const tileX = Math.floor(newX + dy);
const tileY = Math.floor(g.player.y);
if (tileX < 0 || tileX >= map_1.MAP_W || tileY < 0 || tileY >= map_1.MAP_H || map_1.MAP[tileY][tileX] > 0) {
blockedX = true;
break;
}
}
if (!blockedX)
g.player.x = newX;
// Try Y movement
const newY = g.player.y + moveY;
let blockedY = false;
for (let dx = -slideRadius; dx <= slideRadius; dx += slideRadius * 2) {
const tileX = Math.floor(g.player.x + dx);
const tileY = Math.floor(newY);
if (tileX < 0 || tileX >= map_1.MAP_W || tileY < 0 || tileY >= map_1.MAP_H || map_1.MAP[tileY][tileX] > 0) {
blockedY = true;
break;
}
}
if (!blockedY)
g.player.y = newY;
// Check exit tile (win condition)
const pTileX = Math.floor(g.player.x);
const pTileY = Math.floor(g.player.y);
if (pTileX >= 0 && pTileX < map_1.MAP_W && pTileY >= 0 && pTileY < map_1.MAP_H && map_1.MAP[pTileY][pTileX] === 3) {
if ((0, game_1.countAliveEnemies)(g) <= 0) {
g.won = true;
showMessage('YOU ESCAPED!\nAll enemies eliminated!\nTime: ' + Math.floor(g.levelTime / 1000) + 's', 999999);
return;
}
}
// Head bob
if (moving) {
g.player.bobPhase += dt * 0.008 * (g.keys.shift ? 1.3 : 1);
g.player.bobAmount = Math.min(1, g.player.bobAmount + dt * 0.005);
}
else {
g.player.bobAmount = Math.max(0, g.player.bobAmount - dt * 0.003);
}
// Muzzle flash
g.player.muzzleFlash = Math.max(0, g.player.muzzleFlash - dt * 0.05);
// Weapon anim
if (g.weaponAnim > 0)
g.weaponAnim = Math.max(0, g.weaponAnim - dt * 0.03);
// Screen shake decay
if (g.screenShake > 0) {
g.screenShake *= 0.9;
if (g.screenShake < 0.1)
g.screenShake = 0;
}
// Manual reload on R
if (g.keys.r && !ws.isReloading) {
const w = weapons_1.WEAPONS[g.player.currentWeapon];
// Give some ammo (from infinite reserve)
const ammoType = w.ammoType;
const maxAmmo = ammoType === 'pistol' ? 99 : ammoType === 'shotgun' ? 32 : 50;
const reloadAmount = ammoType === 'pistol' ? 12 : ammoType === 'shotgun' ? 6 : 20;
if (g.player.ammo[ammoType] < maxAmmo) {
ws.isReloading = true;
ws.reloadStart = now;
}
}
// Mouse fire
if (g.keys.mouse) {
const w = weapons_1.WEAPONS[g.player.currentWeapon];
if (w.autoFire || true) {
(0, weapons_1.tryFire)(g, ws, now);
if (!w.autoFire)
g.keys.mouse = false;
}
}
// Pickup check
const pickupRange = 0.5;
for (const p of g.pickups) {
if (!p.active)
continue;
p.bobPhase += dt * 0.003;
const dx = g.player.x - p.x;
const dy = g.player.y - p.y;
if (dx * dx + dy * dy < pickupRange * pickupRange) {
p.active = false;
(0, audio_1.playSound)('pickup');
switch (p.type) {
case 'health':
g.player.health = Math.min(g.player.maxHealth, g.player.health + 25);
break;
case 'armor':
g.player.armor = Math.min(100, g.player.armor + 50);
break;
case 'ammo_pistol':
g.player.ammo.pistol = Math.min(99, g.player.ammo.pistol + 12);
break;
case 'ammo_shotgun':
g.player.ammo.shotgun = Math.min(32, g.player.ammo.shotgun + 6);
break;
case 'ammo_rifle':
g.player.ammo.rifle = Math.min(50, g.player.ammo.rifle + 20);
break;
}
}
}
// Update enemies
(0, ai_1.updateEnemies)(g, dt);
// Update particles
update: for (let i = g.particles.length - 1; i >= 0; i--) {
const p = g.particles[i];
p.x += p.vx;
p.y += p.vy;
p.z += p.vz;
p.vz -= 0.0001;
p.life -= dt;
if (p.life <= 0) {
g.particles.splice(i, 1);
}
}
// Damage flash
g.damageFlash = Math.max(0, g.damageFlash - dt);
// Message timer
if (g.messageTimer > 0) {
g.messageTimer -= dt;
if (g.messageTimer <= 0)
clearMessage();
}
// Update HUD
updateHUD();
// Check win (all dead + at exit)
if ((0, game_1.countAliveEnemies)(g) === 0 && !g.won && !g.allDeadMsgShown) {
g.allDeadMsgShown = true;
showMessage('ALL ENEMIES DEAD!\nFind the exit door!', 4000);
}
}
function renderFrame() {
const { ctx, width, height } = g;
ctx.save();
// Screen shake
if (g.screenShake > 0) {
const shakeX = (Math.random() - 0.5) * g.screenShake * 2;
const shakeY = (Math.random() - 0.5) * g.screenShake * 2;
ctx.translate(shakeX, shakeY);
g.screenShake *= 0.85;
if (g.screenShake < 0.1)
g.screenShake = 0;
}
ctx.clearRect(-10, -10, width + 20, height + 20);
// Render walls
(0, renderer_1.render)(g);
// Render sprites
(0, sprites_1.renderSprites)(g);
// Render particles
renderParticles(g);
// Render weapon
(0, weapons_1.drawWeapon)(g, ws);
// Damage overlay
const dmgOverlay = document.getElementById('damage-overlay');
if (g.damageFlash > 0) {
dmgOverlay?.classList.add('hit');
}
else {
dmgOverlay?.classList.remove('hit');
}
// Minimap
drawMinimap(g);
// Vignette
drawVignette();
ctx.restore();
}
function renderParticles(g) {
const { ctx, width, height, player, zBuffer } = g;
for (const p of g.particles) {
const dx = p.x - player.x;
const dy = p.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
let angle = Math.atan2(dy, dx) - player.angle;
while (angle < -Math.PI)
angle += 2 * Math.PI;
while (angle > Math.PI)
angle -= 2 * Math.PI;
if (Math.abs(angle) > Math.PI / 2.2)
continue;
if (dist < 0.1)
continue;
const perpDist = dist * Math.cos(angle - player.angle);
const screenX = width / 2 + (angle / (Math.PI / 3)) * width / 2;
const screenY = height / 2 - (p.z - 0.3) * height / perpDist + player.pitch * height;
const size = Math.max(1, p.size * height / perpDist / 100);
const alpha = p.life / p.maxLife;
const c = p.color.startsWith('#') ? p.color : p.color;
ctx.globalAlpha = alpha;
ctx.fillStyle = c;
ctx.fillRect(screenX - size / 2, screenY - size / 2, size, size);
}
ctx.globalAlpha = 1;
}
function drawMinimap(g) {
const { ctx, width, height, player } = g;
const scale = 6;
const mapPx = map_1.MAP_W * scale;
const mapPy = map_1.MAP_H * scale;
const offsetX = 10;
const offsetY = 10;
// Background
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(offsetX - 2, offsetY - 2, mapPx + 4, mapPy + 4);
// Walls
for (let y = 0; y < map_1.MAP_H; y++) {
for (let x = 0; x < map_1.MAP_W; x++) {
if (map_1.MAP[y][x] === 0)
continue;
if (map_1.MAP[y][x] === 3)
ctx.fillStyle = '#335533';
else if (map_1.MAP[y][x] === 1)
ctx.fillStyle = '#664433';
else
ctx.fillStyle = '#444455';
ctx.fillRect(offsetX + x * scale, offsetY + y * scale, scale, scale);
}
}
// Enemies
for (const e of g.enemies) {
if (e.state === 'dead')
continue;
ctx.fillStyle = e.type === 'boss' ? '#ff44ff' : e.type === 'soldier' ? '#4466ff' : '#ff4444';
ctx.fillRect(offsetX + e.x * scale - 2, offsetY + e.y * scale - 2, 4, 4);
}
// Pickups
for (const p of g.pickups) {
if (!p.active)
continue;
ctx.fillStyle = p.type === 'health' ? '#ff3333' : p.type === 'armor' ? '#3366ff' : '#ffaa00';
ctx.fillRect(offsetX + p.x * scale - 1, offsetY + p.y * scale - 1, 3, 3);
}
// Player
ctx.fillStyle = '#00ff00';
ctx.beginPath();
ctx.arc(offsetX + player.x * scale, offsetY + player.y * scale, 3, 0, Math.PI * 2);
ctx.fill();
// Player direction
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(offsetX + player.x * scale, offsetY + player.y * scale);
ctx.lineTo(offsetX + (player.x + Math.cos(player.angle) * 1.5) * scale, offsetY + (player.y + Math.sin(player.angle) * 1.5) * scale);
ctx.stroke();
}
function updateHUD() {
const p = g.player;
const w = weapons_1.WEAPONS[p.currentWeapon];
document.getElementById('health-display').textContent = `❤️ ${Math.ceil(p.health)}`;
document.getElementById('armor-display').textContent = `🛡️ ${Math.ceil(p.armor)}`;
document.getElementById('weapon-name').textContent = w.name + (ws.isReloading ? ' [RELOADING]' : '') + (ws.switching ? ' ...' : '');
const ammoCount = p.ammo[w.ammoType] || 0;
document.getElementById('ammo-display').textContent = `🔫 ${ammoCount}`;
const alive = (0, game_1.countAliveEnemies)(g);
document.getElementById('score-display').textContent = `💀 ${g.enemiesKilled} / ${g.totalEnemies}`;
// Health color
const healthEl = document.getElementById('health-display');
if (p.health < 25)
healthEl.style.color = '#ff0000';
else if (p.health < 50)
healthEl.style.color = '#ff8800';
else
healthEl.style.color = '#fff';
}
function showMessage(text, duration) {
g.message = text;
g.messageTimer = duration;
const overlay = document.getElementById('message-overlay');
overlay.textContent = text;
overlay.classList.add('visible');
}
function clearMessage() {
g.message = '';
g.messageTimer = 0;
const overlay = document.getElementById('message-overlay');
overlay.classList.remove('visible');
}
function drawVignette() {
const vignette = document.getElementById('vignette');
if (!vignette)
return;
// Resize if needed
if (vignette.width !== g.width || vignette.height !== g.height) {
vignette.width = g.width;
vignette.height = g.height;
}
const vCtx = vignette.getContext('2d');
vCtx.clearRect(0, 0, vignette.width, vignette.height);
// Vignette gradient
const cx = vignette.width / 2;
const cy = vignette.height / 2;
const radius = Math.max(vignette.width, vignette.height) * 0.7;
const grad = vCtx.createRadialGradient(cx, cy, radius * 0.4, cx, cy, radius);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(0.6, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,0.6)');
vCtx.fillStyle = grad;
vCtx.fillRect(0, 0, vignette.width, vignette.height);
// Red flash when low health
if (g.player.health < 30 && g.player.alive) {
const intensity = (1 - g.player.health / 30) * 0.3;
const pulse = Math.sin(g.levelTime * 0.005) * 0.5 + 0.5;
vCtx.fillStyle = `rgba(255,0,0,${intensity * pulse * 0.3})`;
vCtx.fillRect(0, 0, vignette.width, vignette.height);
}
}
start();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CEILING_COLOR = exports.FLOOR_COLOR = exports.WALL_COLORS = exports.MAP_H = exports.MAP_W = exports.MAP = exports.TILE = void 0;
// Tile types
exports.TILE = {
EMPTY: 0,
WALL: 1,
WALL_DARK: 2,
WALL_EXIT: 3,
};
// 24x18 dungeon map - more rooms and corridors
exports.MAP = [
[2, 2, 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, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 2],
[2, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2],
[2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 2],
[2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 3, 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, 2, 2],
];
exports.MAP_W = exports.MAP[0].length;
exports.MAP_H = exports.MAP.length;
// Wall colors per tile type [bright, dark]
exports.WALL_COLORS = {
1: ['#885533', '#553322'], // brown brick
2: ['#444455', '#222233'], // dark gray stone
3: ['#335533', '#223322'], // green exit
};
// Floor/ceiling colors
exports.FLOOR_COLOR = '#333340';
exports.CEILING_COLOR = '#222228';
};
// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createPlayer = createPlayer;
exports.respawnPlayer = respawnPlayer;
exports.takeDamage = takeDamage;
function createPlayer() {
return {
x: 1.5,
y: 1.5,
angle: 0,
pitch: 0,
health: 100,
maxHealth: 100,
armor: 0,
ammo: { pistol: 20, shotgun: 8, rifle: 0 },
alive: true,
deadTimer: 0,
speed: 0,
moveSpeed: 3.5,
rotSpeed: 2.0,
bobPhase: 0,
bobAmount: 0,
muzzleFlash: 0,
weaponSwitching: false,
currentWeapon: 0,
keys: [],
};
}
function respawnPlayer(p, x, y) {
p.x = x;
p.y = y;
p.health = 100;
p.armor = 0;
p.alive = true;
p.deadTimer = 0;
p.ammo.pistol = 20;
p.ammo.shotgun = 8;
p.ammo.rifle = 12;
p.currentWeapon = 0;
p.speed = 0;
}
function takeDamage(p, amount) {
if (!p.alive)
return 0;
let remaining = amount;
if (p.armor > 0) {
const armorAbsorb = Math.min(p.armor, remaining * 0.7);
p.armor -= armorAbsorb;
remaining -= armorAbsorb * 0.5;
}
p.health -= remaining;
if (p.health <= 0) {
p.health = 0;
p.alive = false;
p.deadTimer = 3000;
}
return amount;
}
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGame = createGame;
exports.countAliveEnemies = countAliveEnemies;
const player_1 = require("./player");
function spawnEnemies() {
const enemies = [];
const spawns = [
// Grunts (front rooms)
{ x: 4.5, y: 2.5, type: 'grunt' },
{ x: 10.5, y: 2.5, type: 'grunt' },
{ x: 17.5, y: 2.5, type: 'grunt' },
// Grunts (middle)
{ x: 4.5, y: 5.5, type: 'grunt' },
{ x: 12.5, y: 4.5, type: 'grunt' },
{ x: 20.5, y: 5.5, type: 'grunt' },
// Soldiers (back rooms)
{ x: 8.5, y: 10.5, type: 'soldier' },
{ x: 14.5, y: 11.5, type: 'soldier' },
{ x: 4.5, y: 12.5, type: 'soldier' },
// Soldiers (late game)
{ x: 10.5, y: 15.5, type: 'soldier' },
{ x: 19.5, y: 14.5, type: 'soldier' },
// Boss (near exit)
{ x: 20.5, y: 16.5, type: 'boss' },
// Extra grunt
{ x: 15.5, y: 8.5, type: 'grunt' },
];
let id = 0;
for (const s of spawns) {
const base = s.type === 'grunt'
? { health: 30, maxHealth: 30, speed: 1.2, damage: 8, attackCooldown: 1000, attackRange: 1.5 }
: s.type === 'soldier'
? { health: 60, maxHealth: 60, speed: 1.0, damage: 15, attackCooldown: 800, attackRange: 3.0 }
: { health: 150, maxHealth: 150, speed: 0.8, damage: 25, attackCooldown: 600, attackRange: 2.0 };
enemies.push({
id: id++, x: s.x, y: s.y, type: s.type,
...base,
state: 'patrol',
animFrame: 0, animTimer: 0, hitTimer: 0,
patrolAngle: Math.random() * Math.PI * 2,
patrolTimer: Math.random() * 3000,
z: 0,
});
}
return enemies;
}
function spawnPickups() {
return [
{ x: 1.5, y: 1.5, type: 'ammo_pistol', active: true, bobPhase: 0 },
{ x: 3.5, y: 2.5, type: 'health', active: true, bobPhase: 0 },
{ x: 6.5, y: 2.5, type: 'ammo_shotgun', active: true, bobPhase: Math.PI },
{ x: 11.5, y: 2.5, type: 'health', active: true, bobPhase: 0 },
{ x: 16.5, y: 2.5, type: 'ammo_pistol', active: true, bobPhase: Math.PI },
{ x: 3.5, y: 7.5, type: 'armor', active: true, bobPhase: 0 },
{ x: 8.5, y: 7.5, type: 'ammo_shotgun', active: true, bobPhase: Math.PI },
{ x: 15.5, y: 7.5, type: 'health', active: true, bobPhase: 0 },
{ x: 21.5, y: 7.5, type: 'ammo_pistol', active: true, bobPhase: Math.PI },
{ x: 5.5, y: 11.5, type: 'ammo_rifle', active: true, bobPhase: 0 },
{ x: 13.5, y: 11.5, type: 'health', active: true, bobPhase: Math.PI },
{ x: 20.5, y: 12.5, type: 'armor', active: true, bobPhase: 0 },
{ x: 8.5, y: 15.5, type: 'ammo_shotgun', active: true, bobPhase: Math.PI },
{ x: 15.5, y: 15.5, type: 'health', active: true, bobPhase: 0 },
{ x: 20.5, y: 15.5, type: 'ammo_rifle', active: true, bobPhase: Math.PI },
];
}
function createGame(canvas, ctx, width, height) {
const textureCanvas = document.createElement('canvas');
textureCanvas.width = 64;
textureCanvas.height = 64;
const textureCtx = textureCanvas.getContext('2d');
return {
player: (0, player_1.createPlayer)(),
enemies: spawnEnemies(),
pickups: spawnPickups(),
particles: [],
running: false,
won: false,
totalEnemies: 13,
enemiesKilled: 0,
message: '',
messageTimer: 0,
levelTime: 0,
damageFlash: 0,
reloading: 0,
weaponAnim: 0,
lastTime: performance.now(),
keys: { w: false, a: false, s: false, d: false, shift: false, mouse: false, r: false },
mouse: { x: 0, y: 0, dx: 0, dy: 0 },
canvas, ctx, width, height,
textureCanvas, textureCtx,
zBuffer: new Array(width),
minimapScale: 4,
pointerLocked: false,
_weaponSwitchTarget: null,
allDeadMsgShown: false,
screenShake: 0,
};
}
function countAliveEnemies(g) {
return g.enemies.filter(e => e.state !== 'dead').length;
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
const map_1 = require("./map");
const FOV = Math.PI / 3;
const HALF_FOV = FOV / 2;
const MAX_DEPTH = 22;
function render(g) {
const { player, ctx, width, height, zBuffer } = g;
const numRays = Math.min(width, 640);
const stripW = width / numRays;
for (let i = 0; i < numRays; i++) {
const rayAngle = player.angle - HALF_FOV + (i / numRays) * FOV;
const sin = Math.sin(rayAngle);
const cos = Math.cos(rayAngle);
// DDA
let mapX = Math.floor(player.x);
let mapY = Math.floor(player.y);
const deltaDistX = Math.abs(1 / cos) || 1e10;
const deltaDistY = Math.abs(1 / sin) || 1e10;
let stepX, stepY;
let sideDistX, sideDistY;
if (cos < 0) {
stepX = -1;
sideDistX = (player.x - mapX) * deltaDistX;
}
else {
stepX = 1;
sideDistX = (mapX + 1 - player.x) * deltaDistX;
}
if (sin < 0) {
stepY = -1;
sideDistY = (player.y - mapY) * deltaDistY;
}
else {
stepY = 1;
sideDistY = (mapY + 1 - player.y) * deltaDistY;
}
let side = 0;
let dist = MAX_DEPTH;
let tileType = 0;
let hit = false;
for (let step = 0; step < 64; step++) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapX < 0 || mapX >= map_1.MAP_W || mapY < 0 || mapY >= map_1.MAP_H)
break;
if (map_1.MAP[mapY][mapX] > 0) {
hit = true;
tileType = map_1.MAP[mapY][mapX];
break;
}
}
if (hit) {
dist = side === 0
? (mapX - player.x + (1 - stepX) / 2) / cos
: (mapY - player.y + (1 - stepY) / 2) / sin;
}
const perpDist = dist * Math.cos(rayAngle - player.angle);
zBuffer[i] = perpDist;
const wallHeight = Math.min(height, height / perpDist);
const pitchOffset = player.pitch * height;
const drawStart = Math.floor((height - wallHeight) / 2 + pitchOffset);
const drawEnd = Math.min(height, Math.floor((height + wallHeight) / 2 + pitchOffset));
// Wall X for texture
let wallX;
if (side === 0)
wallX = player.y + dist * sin;
else
wallX = player.x + dist * cos;
wallX = ((wallX % 1) + 1) % 1;
// Fog
const fog = Math.min(0.8, perpDist / MAX_DEPTH);
// Draw ceiling as single gradient strip
if (drawStart > 0) {
const cGrad = ctx.createLinearGradient(0, 0, 0, drawStart);
const cFog = Math.min(0.5, perpDist / MAX_DEPTH);
cGrad.addColorStop(0, `rgb(${Math.floor(15 * (1 - cFog))},${Math.floor(15 * (1 - cFog))},${Math.floor(20 * (1 - cFog))})`);
cGrad.addColorStop(1, `rgb(${Math.floor(22 * (1 - cFog))},${Math.floor(22 * (1 - cFog))},${Math.floor(28 * (1 - cFog))})`);
ctx.fillStyle = cGrad;
ctx.fillRect(Math.floor(i * stripW), 0, Math.ceil(stripW), drawStart);
}
// Draw wall strip
for (let y = drawStart; y < drawEnd; y++) {
const d = (y - (height - wallHeight) / 2 - pitchOffset) / wallHeight;
const texY = Math.max(0, Math.min(63, Math.floor(d * 64)));
const texX = Math.floor(wallX * 64);
let r, gr, b;
if (tileType === 1) {
// Brick wall with mortar lines
const brickH = 8;
const brickW = 16;
const row = Math.floor(texY / brickH);
const brickX = ((texX + (row % 2) * 8) % brickW);
const brickY = texY % brickH;
const isMortar = brickX < 1 || brickY < 1;
if (isMortar) {
r = 100;
gr = 90;
b = 80;
}
else {
const variation = ((texX * 3 + row * 7) % 15) - 7;
r = 136 + variation;
gr = 85 + variation / 2;
b = 51;
}
// Slight horizontal line highlight at top
if (texY < 2) {
r += 10;
gr += 10;
b += 10;
}
}
else if (tileType === 2) {
// Stone wall
const n = ((texX * 7 + texY * 13) % 31) / 31;
const n2 = ((texX * 13 + texY * 7) % 17) / 17;
r = 68 + n * 20 + n2 * 10;
gr = 68 + n * 18;
b = 85 + n * 15;
}
else if (tileType === 3) {
// Exit door - glowing
const isFrame = texX < 4 || texX > 60 || texY < 4 || texY > 60;
if (isFrame) {
r = 40;
gr = 70;
b = 40;
}
else {
const stripeX = Math.floor(texX / 8);
const pulse = Math.sin(g.levelTime * 0.003) * 30 + 30;
r = 180 + pulse;
gr = 220 + pulse * 0.5;
b = 30;
}
}
else {
r = 128;
gr = 128;
b = 128;
}
if (side === 1) {
r = Math.floor(r * 0.65);
gr = Math.floor(gr * 0.65);
b = Math.floor(b * 0.65);
}
r = Math.floor(r * (1 - fog));
gr = Math.floor(gr * (1 - fog));
b = Math.floor(b * (1 - fog));
ctx.fillStyle = `rgb(${r},${gr},${b})`;
ctx.fillRect(Math.floor(i * stripW), y, Math.ceil(stripW), 1);
}
// Draw floor as gradient strip
if (drawEnd < height) {
const fGrad = ctx.createLinearGradient(0, drawEnd, 0, height);
const fFog = Math.min(0.5, perpDist / MAX_DEPTH);
fGrad.addColorStop(0, `rgb(${Math.floor(51 * (1 - fFog))},${Math.floor(51 * (1 - fFog))},${Math.floor(64 * (1 - fFog))})`);
fGrad.addColorStop(1, `rgb(${Math.floor(30 * (1 - fFog))},${Math.floor(30 * (1 - fFog))},${Math.floor(38 * (1 - fFog))})`);
ctx.fillStyle = fGrad;
ctx.fillRect(Math.floor(i * stripW), drawEnd, Math.ceil(stripW), height - drawEnd);
}
}
}
};
// ── module: src/sprites.ts ──
__mods["src/sprites.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderSprites = renderSprites;
// Draw a sprite (enemy or pickup) using billboard technique
function drawSprite(g, spriteX, spriteY, size, color, fog) {
const { ctx, width, height, player, zBuffer } = g;
const dx = spriteX - player.x;
const dy = spriteY - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Angle from player
let spriteAngle = Math.atan2(dy, dx) - player.angle;
while (spriteAngle < -Math.PI)
spriteAngle += 2 * Math.PI;
while (spriteAngle > Math.PI)
spriteAngle -= 2 * Math.PI;
// Off screen?
if (Math.abs(spriteAngle) > Math.PI / 2.2)
return;
const perpDist = dist * Math.cos(spriteAngle);
if (perpDist < 0.1)
return;
const screenX = width / 2 + (spriteAngle / (Math.PI / 3)) * width / 2;
const spriteHeight = Math.min(height, Math.floor(height / perpDist * size));
const spriteWidth = spriteHeight;
const pitchOffset = player.pitch * height;
const drawStart = Math.floor((height - spriteHeight) / 2 + pitchOffset);
const drawEnd = Math.floor((height + spriteHeight) / 2 + pitchOffset);
const startStripe = Math.floor(screenX - spriteWidth / 2);
const endStripe = Math.floor(screenX + spriteWidth / 2);
const fogFactor = Math.min(0.7, perpDist / 20);
// Parse color
const c = parseColor(color);
const r = Math.floor(c.r * (1 - fogFactor));
const gr = Math.floor(c.g * (1 - fogFactor));
const b = Math.floor(c.b * (1 - fogFactor));
// Draw sprite with z-buffer check
const stride = g.width / Math.min(g.width, 640);
for (let stripe = startStripe; stripe < endStripe; stripe++) {
if (stripe < 0 || stripe >= width)
continue;
const rayIdx = Math.floor(stripe / stride);
if (rayIdx < 0 || rayIdx >= zBuffer.length)
continue;
if (zBuffer[rayIdx] < perpDist)
continue;
// Position within sprite
const spriteNX = (stripe - startStripe) / (endStripe - startStripe);
for (let y = drawStart; y < drawEnd; y++) {
const spriteNY = (y - drawStart) / (drawEnd - drawStart);
// Simple circle shape
const cx = spriteNX - 0.5;
const cy = spriteNY - 0.5;
const d = Math.sqrt(cx * cx + cy * cy);
if (d > 0.5)
continue;
ctx.fillStyle = `rgb(${r},${gr},${b})`;
ctx.fillRect(stripe, y, Math.ceil(stride), 1);
}
}
}
function parseColor(color) {
// Simple hex color parser
if (color.startsWith('#')) {
const v = parseInt(color.slice(1), 16);
return { r: (v >> 16) & 255, g: (v >> 8) & 255, b: v & 255 };
}
const m = color.match(/(\d+)/g);
if (m && m.length >= 3)
return { r: +m[0], g: +m[1], b: +m[2] };
return { r: 255, g: 0, b: 0 };
}
function renderSprites(g) {
const sprites = [];
for (const enemy of g.enemies) {
if (enemy.state === 'dead')
continue;
const dx = enemy.x - g.player.x;
const dy = enemy.y - g.player.y;
sprites.push({
dist: dx * dx + dy * dy,
render: () => renderEnemy(g, enemy),
});
}
for (const pickup of g.pickups) {
if (!pickup.active)
continue;
const dx = pickup.x - g.player.x;
const dy = pickup.y - g.player.y;
sprites.push({
dist: dx * dx + dy * dy,
render: () => renderPickup(g, pickup),
});
}
// Back to front
sprites.sort((a, b) => b.dist - a.dist);
for (const s of sprites)
s.render();
}
function renderEnemy(g, e) {
const { ctx, width, height, player, zBuffer } = g;
const dx = e.x - player.x;
const dy = e.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
let spriteAngle = Math.atan2(dy, dx) - player.angle;
while (spriteAngle < -Math.PI)
spriteAngle += 2 * Math.PI;
while (spriteAngle > Math.PI)
spriteAngle -= 2 * Math.PI;
if (Math.abs(spriteAngle) > Math.PI / 2.2)
return;
const perpDist = dist * Math.cos(spriteAngle);
if (perpDist < 0.1)
return;
const screenX = width / 2 + (spriteAngle / (Math.PI / 3)) * width / 2;
const scale = e.type === 'boss' ? 1.4 : e.type === 'soldier' ? 1.1 : 1.0;
const spriteHeight = Math.min(height, Math.floor(height / perpDist * 0.8 * scale));
const spriteWidth = spriteHeight * 0.6;
const pitchOffset = player.pitch * height;
const drawStart = Math.floor((height - spriteHeight * 0.8) / 2 + pitchOffset - spriteHeight * 0.1);
const drawEnd = Math.floor((height + spriteHeight * 0.8) / 2 + pitchOffset + spriteHeight * 0.2);
const startStripe = Math.floor(screenX - spriteWidth / 2);
const endStripe = Math.floor(screenX + spriteWidth / 2);
const fogFactor = Math.min(0.7, perpDist / 20);
const hitFlash = e.hitTimer > 0;
// Colors per type
let bodyColor, eyeColor;
if (e.type === 'grunt') {
bodyColor = hitFlash ? '#ffffff' : '#aa4444';
eyeColor = '#ffff00';
}
else if (e.type === 'soldier') {
bodyColor = hitFlash ? '#ffffff' : '#4466aa';
eyeColor = '#ff4400';
}
else {
bodyColor = hitFlash ? '#ffffff' : '#884488';
eyeColor = '#ff0000';
}
const c = parseColor(bodyColor);
const ec = parseColor(eyeColor);
const r = Math.floor(c.r * (1 - fogFactor));
const gr = Math.floor(c.g * (1 - fogFactor));
const b = Math.floor(c.b * (1 - fogFactor));
const er = Math.floor(ec.r * (1 - fogFactor));
const eg = Math.floor(ec.g * (1 - fogFactor));
const eb = Math.floor(ec.b * (1 - fogFactor));
const stride = g.width / Math.min(g.width, 640);
for (let stripe = startStripe; stripe < endStripe; stripe++) {
if (stripe < 0 || stripe >= width)
continue;
const rayIdx = Math.floor(stripe / stride);
if (rayIdx < 0 || rayIdx >= g.zBuffer.length)
continue;
if (g.zBuffer[rayIdx] < perpDist)
continue;
const spriteNX = (stripe - startStripe) / (endStripe - startStripe);
for (let y = drawStart; y < drawEnd; y++) {
const spriteNY = (y - drawStart) / (drawEnd - drawStart);
// Head region (top 25%)
const headTop = 0;
const headBottom = 0.28;
// Body region (28% to 70%)
const bodyTop = 0.25;
const bodyBottom = 0.72;
// Legs (70% to 100%)
const cx = spriteNX - 0.5;
// Draw body as a rough shape
let fill = false;
if (spriteNY >= headTop && spriteNY < headBottom) {
// Head - circular
const headCX = cx;
const headCY = spriteNY - 0.14;
const headR = 0.28;
if (headCX * headCX + headCY * headCY < headR * headR)
fill = true;
}
if (spriteNY >= bodyTop && spriteNY < bodyBottom) {
const bodyW = 0.35 + (spriteNY - bodyTop) * 0.15;
if (Math.abs(cx) < bodyW)
fill = true;
}
if (spriteNY >= bodyBottom) {
// Legs - split
const legSpread = 0.12 + (spriteNY - bodyBottom) * 0.3;
if (Math.abs(cx) < legSpread + 0.06)
fill = true;
}
if (!fill)
continue;
// Eyes
if (spriteNY > 0.12 && spriteNY < 0.2) {
const eyeW = 0.06;
const eyeLX = -0.12, eyeRX = 0.12;
if (Math.abs(cx - eyeLX) < eyeW || Math.abs(cx - eyeRX) < eyeW) {
ctx.fillStyle = `rgb(${er},${eg},${eb})`;
ctx.fillRect(stripe, y, Math.ceil(stride), 1);
continue;
}
}
// Mouth for boss
if (e.type === 'boss' && spriteNY > 0.22 && spriteNY < 0.26 && Math.abs(cx) < 0.1) {
ctx.fillStyle = `rgb(${Math.floor(200 * (1 - fogFactor))},${Math.floor(200 * (1 - fogFactor))},${Math.floor(200 * (1 - fogFactor))})`;
ctx.fillRect(stripe, y, Math.ceil(stride), 1);
continue;
}
// Body color
// Add some shading
const shade = 0.85 + Math.abs(cx) * 0.3;
ctx.fillStyle = `rgb(${Math.floor(r * shade)},${Math.floor(gr * shade)},${Math.floor(b * shade)})`;
ctx.fillRect(stripe, y, Math.ceil(stride), 1);
}
}
// Health bar above enemy
if (e.health < e.maxHealth && perpDist < 12) {
const barWidth = spriteWidth * 0.8;
const barHeight = Math.max(3, spriteHeight * 0.03);
const barY = drawStart - barHeight - 4;
const healthPct = e.health / e.maxHealth;
if (barY > 0) {
ctx.fillStyle = '#333';
ctx.fillRect(Math.floor(screenX - barWidth / 2), barY, barWidth, barHeight);
ctx.fillStyle = healthPct > 0.5 ? '#0f0' : healthPct > 0.25 ? '#ff0' : '#f00';
ctx.fillRect(Math.floor(screenX - barWidth / 2), barY, Math.floor(barWidth * healthPct), barHeight);
}
}
}
function renderPickup(g, p) {
const { ctx, width, height, player, zBuffer } = g;
const dx = p.x - player.x;
const dy = p.y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
let spriteAngle = Math.atan2(dy, dx) - player.angle;
while (spriteAngle < -Math.PI)
spriteAngle += 2 * Math.PI;
while (spriteAngle > Math.PI)
spriteAngle -= 2 * Math.PI;
if (Math.abs(spriteAngle) > Math.PI / 2.2)
return;
const perpDist = dist * Math.cos(spriteAngle);
if (perpDist < 0.1)
return;
const screenX = width / 2 + (spriteAngle / (Math.PI / 3)) * width / 2;
const bob = Math.sin(p.bobPhase) * 4;
const spriteHeight = Math.floor(height / perpDist * 0.25);
const spriteWidth = spriteHeight * 0.7;
const pitchOffset = player.pitch * height;
const drawStart = Math.floor((height) / 2 + pitchOffset - spriteHeight / 2 + bob);
const drawEnd = Math.floor((height) / 2 + pitchOffset + spriteHeight / 2 + bob);
const startStripe = Math.floor(screenX - spriteWidth / 2);
const endStripe = Math.floor(screenX + spriteWidth / 2);
const fogFactor = Math.min(0.7, perpDist / 20);
let color;
switch (p.type) {
case 'health':
color = '#ff3333';
break;
case 'armor':
color = '#3366ff';
break;
case 'ammo_pistol':
color = '#ffaa00';
break;
case 'ammo_shotgun':
color = '#aa6600';
break;
case 'ammo_rifle':
color = '#00cc00';
break;
default: color = '#ffff00';
}
const c = parseColor(color);
const r = Math.floor(c.r * (1 - fogFactor));
const gr = Math.floor(c.g * (1 - fogFactor));
const b = Math.floor(c.b * (1 - fogFactor));
const stride = g.width / Math.min(g.width, 640);
// Glow effect
const glowStart = startStripe - 2;
const glowEnd = endStripe + 2;
const glowHeight = (drawEnd - drawStart) + 2;
for (let stripe = glowStart; stripe < glowEnd; stripe++) {
if (stripe < 0 || stripe >= width)
continue;
const rayIdx = Math.floor(stripe / stride);
if (rayIdx < 0 || rayIdx >= zBuffer.length)
continue;
if (zBuffer[rayIdx] < perpDist)
continue;
const spriteNX = (stripe - glowStart) / (glowEnd - glowStart);
for (let y = drawStart - 1; y < drawEnd + 1; y++) {
const spriteNY = (y - drawStart + 1) / glowHeight;
const cx = spriteNX - 0.5;
const cy = spriteNY - 0.5;
const d = Math.sqrt(cx * cx + cy * cy);
if (d > 0.5)
continue;
const alpha = 1 - d / 0.5;
ctx.fillStyle = `rgba(${r},${gr},${b},${alpha * 0.5})`;
ctx.fillRect(stripe, y, Math.ceil(stride), 1);
}
}
// Core
for (let stripe = startStripe; stripe < endStripe; stripe++) {
if (stripe < 0 || stripe >= width)
continue;
const rayIdx = Math.floor(stripe / stride);
if (rayIdx < 0 || rayIdx >= zBuffer.length)
continue;
if (zBuffer[rayIdx] < perpDist)
continue;
const spriteNX = (stripe - startStripe) / (endStripe - startStripe);
for (let y = drawStart; y < drawEnd; y++) {
const spriteNY = (y - drawStart) / (drawEnd - drawStart);
const cx = spriteNX - 0.5;
const cy = spriteNY - 0.5;
const d = Math.sqrt(cx * cx + cy * cy);
if (d > 0.45)
continue;
ctx.fillStyle = `rgb(${r},${gr},${b})`;
ctx.fillRect(stripe, y, Math.ceil(stride), 1);
}
}
}
};
// ── module: src/weapons.ts ──
__mods["src/weapons.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WEAPONS = void 0;
exports.createWeaponState = createWeaponState;
exports.tryFire = tryFire;
exports.tryReload = tryReload;
exports.updateReload = updateReload;
exports.trySwitchWeapon = trySwitchWeapon;
exports.updateSwitch = updateSwitch;
exports.drawWeapon = drawWeapon;
const map_1 = require("./map");
const audio_1 = require("./audio");
exports.WEAPONS = [
{
name: 'PISTOL',
ammoType: 'pistol',
damage: 15,
fireRate: 400,
spread: 0.02,
pellets: 1,
ammoPerShot: 1,
reloadTime: 1000,
maxClip: 99,
melee: false,
autoFire: false,
color: '#ffaa00',
size: 0.15,
},
{
name: 'SHOTGUN',
ammoType: 'shotgun',
damage: 8,
fireRate: 900,
spread: 0.08,
pellets: 5,
ammoPerShot: 1,
reloadTime: 1500,
maxClip: 32,
melee: false,
autoFire: false,
color: '#aa6600',
size: 0.2,
},
{
name: 'LASER RIFLE',
ammoType: 'rifle',
damage: 25,
fireRate: 150,
spread: 0.03,
pellets: 1,
ammoPerShot: 1,
reloadTime: 2000,
maxClip: 50,
melee: false,
autoFire: true,
color: '#00ff00',
size: 0.18,
},
];
function createWeaponState() {
return {
lastShot: 0,
lastSwitch: 0,
switching: false,
switchTarget: 0,
isReloading: false,
reloadStart: 0,
};
}
function tryFire(g, ws, now) {
const p = g.player;
if (!p.alive || ws.switching || ws.isReloading)
return false;
const w = exports.WEAPONS[p.currentWeapon];
const ammoCount = p.ammo[w.ammoType] || 0;
if (now - ws.lastShot < w.fireRate)
return false;
if (ammoCount < w.ammoPerShot) {
tryReload(g, ws, now);
return false;
}
ws.lastShot = now;
p.ammo[w.ammoType] -= w.ammoPerShot;
p.muzzleFlash = 100;
g.weaponAnim = 5;
// Sound
if (w.ammoType === 'pistol')
(0, audio_1.playSound)('pistol');
else if (w.ammoType === 'shotgun')
(0, audio_1.playSound)('shotgun');
else
(0, audio_1.playSound)('rifle');
// Fire pellets
g.screenShake = w.ammoType === 'shotgun' ? 6 : w.ammoType === 'rifle' ? 2 : 3;
for (let i = 0; i < w.pellets; i++) {
const spread = (Math.random() - 0.5) * w.spread;
const angle = p.angle + spread;
// Raycast for hit
fireBullet(g, angle, w.damage, w.ammoType);
}
return true;
}
function fireBullet(g, angle, damage, ammoType) {
const p = g.player;
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// Check enemies in order of distance
let closestHit = null;
let closestDist = Infinity;
for (const e of g.enemies) {
if (e.state === 'dead')
continue;
const dx = e.x - p.x;
const dy = e.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist >= closestDist)
continue;
// Angle to enemy
let eAngle = Math.atan2(dy, dx) - angle;
while (eAngle < -Math.PI)
eAngle += 2 * Math.PI;
while (eAngle > Math.PI)
eAngle -= 2 * Math.PI;
// Enemy hit radius (approximate)
const hitRadius = e.type === 'boss' ? 0.5 : 0.35;
if (Math.abs(eAngle) < hitRadius / dist) {
// Check wall occlusion
if (!wallBetween(p.x, p.y, e.x, e.y)) {
closestHit = e;
closestDist = dist;
}
}
}
if (closestHit) {
closestHit.health -= damage;
closestHit.hitTimer = 150;
closestHit.state = 'chase';
(0, audio_1.playSound)('hit');
// Blood particles
for (let i = 0; i < 3; i++) {
g.particles.push({
x: closestHit.x + (Math.random() - 0.5) * 0.3,
y: closestHit.y + (Math.random() - 0.5) * 0.3,
z: 0.3 + Math.random() * 0.4,
vx: (Math.random() - 0.5) * 0.02,
vy: (Math.random() - 0.5) * 0.02,
vz: Math.random() * 0.02,
life: 500,
maxLife: 500,
color: '#ff0000',
size: 2 + Math.random() * 2,
});
}
if (closestHit.health <= 0) {
closestHit.state = 'dead';
g.enemiesKilled++;
(0, audio_1.playSound)('kill');
// Death particles
for (let i = 0; i < 8; i++) {
g.particles.push({
x: closestHit.x + (Math.random() - 0.5) * 0.4,
y: closestHit.y + (Math.random() - 0.5) * 0.4,
z: Math.random() * 0.5,
vx: (Math.random() - 0.5) * 0.04,
vy: (Math.random() - 0.5) * 0.04,
vz: Math.random() * 0.03,
life: 800,
maxLife: 800,
color: closestHit.type === 'boss' ? '#ff00ff' : '#ff4400',
size: 3 + Math.random() * 3,
});
}
}
}
}
function wallBetween(x1, y1, x2, y2) {
const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
const steps = Math.ceil(dist * 10);
for (let i = 1; i < steps; i++) {
const t = i / steps;
const cx = x1 + (x2 - x1) * t;
const cy = y1 + (y2 - y1) * t;
const mx = Math.floor(cx);
const my = Math.floor(cy);
if (mx >= 0 && mx < map_1.MAP_W && my >= 0 && my < map_1.MAP_H && map_1.MAP[my][mx] > 0) {
return true;
}
}
return false;
}
function tryReload(g, ws, now) {
const p = g.player;
if (ws.isReloading || ws.switching || !p.alive)
return;
if ((p.ammo[exports.WEAPONS[p.currentWeapon].ammoType] || 0) > 0)
return; // has ammo, can't reload
ws.isReloading = true;
ws.reloadStart = now;
}
function updateReload(g, ws, now) {
if (!ws.isReloading)
return;
const w = exports.WEAPONS[g.player.currentWeapon];
if (now - ws.reloadStart >= w.reloadTime) {
ws.isReloading = false;
// Give some ammo
const ammoType = w.ammoType;
const maxAmmo = ammoType === 'pistol' ? 99 : ammoType === 'shotgun' ? 32 : 50;
g.player.ammo[ammoType] = Math.min(maxAmmo, (g.player.ammo[ammoType] || 0) + (ammoType === 'pistol' ? 12 : ammoType === 'shotgun' ? 4 : 20));
}
}
function trySwitchWeapon(g, ws, target, now) {
if (ws.switching || target === g.player.currentWeapon)
return;
if (target < 0 || target >= exports.WEAPONS.length)
return;
// Check if player has this weapon
const ammoType = exports.WEAPONS[target].ammoType;
if (ammoType === 'rifle' && (g.player.ammo.rifle || 0) === 0 && target !== g.player.currentWeapon) {
// Can only switch to rifle if they have ammo or already have it
// Give rifle on first pickup
return;
}
ws.switching = true;
ws.switchTarget = target;
ws.lastSwitch = now;
}
function updateSwitch(g, ws, now) {
if (!ws.switching)
return;
if (now - ws.lastSwitch >= 300) {
g.player.currentWeapon = ws.switchTarget;
ws.switching = false;
ws.isReloading = false;
}
}
function drawWeapon(g, ws) {
const { ctx, width, height, player } = g;
const w = exports.WEAPONS[player.currentWeapon];
const bobX = Math.sin(player.bobPhase) * player.bobAmount * 20;
const bobY = Math.abs(Math.cos(player.bobPhase)) * player.bobAmount * 15;
const fireOffset = g.weaponAnim > 0 ? -20 * (g.weaponAnim / 5) : 0;
const switchOffset = ws.switching ? 50 : 0;
const baseX = width / 2 + width * 0.2 + bobX;
const baseY = height - height * 0.2 + bobY + fireOffset + switchOffset;
const scale = Math.min(width, height) / 600;
ctx.save();
ctx.translate(baseX, baseY);
ctx.scale(scale, scale);
// Draw weapon based on type
if (player.currentWeapon === 0) {
drawPistol(ctx);
}
else if (player.currentWeapon === 1) {
drawShotgun(ctx);
}
else {
drawRifle(ctx);
}
// Muzzle flash
if (player.muzzleFlash > 0) {
const flashSize = 30 + Math.random() * 20;
ctx.fillStyle = `rgba(255, 255, 100, ${player.muzzleFlash / 100})`;
ctx.beginPath();
ctx.arc(-40, -60, flashSize, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = `rgba(255, 150, 50, ${player.muzzleFlash / 150})`;
ctx.beginPath();
ctx.arc(-40, -60, flashSize * 1.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawPistol(ctx) {
// Handle
ctx.fillStyle = '#553322';
ctx.fillRect(-10, 0, 20, 60);
// Barrel
ctx.fillStyle = '#444';
ctx.fillRect(-8, -50, 16, 55);
// Barrel tip
ctx.fillStyle = '#333';
ctx.fillRect(-6, -65, 12, 18);
// Sight
ctx.fillStyle = '#666';
ctx.fillRect(-2, -70, 4, 8);
}
function drawShotgun(ctx) {
// Stock
ctx.fillStyle = '#553322';
ctx.fillRect(-15, 10, 30, 50);
// Body
ctx.fillStyle = '#444';
ctx.fillRect(-12, -40, 24, 55);
// Barrel
ctx.fillStyle = '#555';
ctx.fillRect(-10, -70, 20, 35);
// Pump
ctx.fillStyle = '#553322';
ctx.fillRect(-12, -35, 24, 12);
// Sight
ctx.fillStyle = '#666';
ctx.fillRect(-2, -75, 4, 8);
}
function drawRifle(ctx) {
// Body
ctx.fillStyle = '#333344';
ctx.fillRect(-14, -20, 28, 65);
// Barrel
ctx.fillStyle = '#444466';
ctx.fillRect(-8, -65, 16, 50);
// Energy core glow
ctx.fillStyle = '#00ff44';
ctx.fillRect(-6, -5, 12, 8);
ctx.fillStyle = '#00ff88';
ctx.fillRect(-4, -3, 8, 4);
// Sight
ctx.fillStyle = '#00ff44';
ctx.fillRect(-3, -70, 6, 8);
// Magazine
ctx.fillStyle = '#222233';
ctx.fillRect(-16, 10, 8, 30);
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupInput = setupInput;
const audio_1 = require("./audio");
function setupInput(g) {
// Keyboard
window.addEventListener('keydown', (e) => {
switch (e.code) {
case 'KeyW':
case 'ArrowUp':
g.keys.w = true;
break;
case 'KeyA':
case 'ArrowLeft':
g.keys.a = true;
break;
case 'KeyS':
case 'ArrowDown':
g.keys.s = true;
break;
case 'KeyD':
case 'ArrowRight':
g.keys.d = true;
break;
case 'ShiftLeft':
case 'ShiftRight':
g.keys.shift = true;
break;
case 'KeyR':
g.keys.r = true;
break;
case 'Digit1':
g._weaponSwitchTarget = 0;
break;
case 'Digit2':
g._weaponSwitchTarget = 1;
break;
case 'Digit3':
g._weaponSwitchTarget = 2;
break;
}
});
window.addEventListener('keyup', (e) => {
switch (e.code) {
case 'KeyW':
case 'ArrowUp':
g.keys.w = false;
break;
case 'KeyA':
case 'ArrowLeft':
g.keys.a = false;
break;
case 'KeyS':
case 'ArrowDown':
g.keys.s = false;
break;
case 'KeyD':
case 'ArrowRight':
g.keys.d = false;
break;
case 'ShiftLeft':
case 'ShiftRight':
g.keys.shift = false;
break;
case 'KeyR':
g.keys.r = false;
break;
}
});
// Mouse
g.canvas.addEventListener('click', () => {
(0, audio_1.initAudio)();
if (!document.pointerLockElement) {
g.canvas.requestPointerLock();
return;
}
g.keys.mouse = true;
});
document.addEventListener('pointerlockchange', () => {
g.pointerLocked = !!document.pointerLockElement;
});
document.addEventListener('mousemove', (e) => {
if (document.pointerLockElement === g.canvas) {
g.mouse.dx += e.movementX;
g.mouse.dy += e.movementY;
}
});
// Title screen
const titleScreen = document.getElementById('title-screen');
const titleHide = () => {
if (titleScreen)
titleScreen.style.display = 'none';
document.removeEventListener('keydown', titleKeyHandler);
g.canvas.removeEventListener('click', titleClickHandler);
};
const titleKeyHandler = (e) => {
if (e.code === 'Enter' || e.code === 'Space')
titleHide();
};
const titleClickHandler = () => {
(0, audio_1.initAudio)();
titleHide();
g.canvas.requestPointerLock();
};
document.addEventListener('keydown', titleKeyHandler);
g.canvas.addEventListener('click', titleClickHandler);
}
};
// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateEnemies = updateEnemies;
const map_1 = require("./map");
const player_1 = require("./player");
const audio_1 = require("./audio");
function updateEnemies(g, dt) {
for (const e of g.enemies) {
if (e.state === 'dead')
continue;
e.hitTimer = Math.max(0, e.hitTimer - dt);
e.animTimer += dt;
if (e.animTimer > 300) {
e.animTimer = 0;
e.animFrame = (e.animFrame + 1) % 4;
}
const dx = g.player.x - e.x;
const dy = g.player.y - e.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// State machine
switch (e.state) {
case 'idle':
case 'patrol':
if (dist < 8 && g.player.alive) {
e.state = 'chase';
}
else {
// Patrol behavior
e.patrolTimer -= dt;
if (e.patrolTimer <= 0) {
e.patrolAngle += (Math.random() - 0.5) * Math.PI;
e.patrolTimer = 2000 + Math.random() * 3000;
}
tryMove(e, Math.cos(e.patrolAngle), Math.sin(e.patrolAngle), e.speed * 0.3, dt);
}
break;
case 'chase':
if (!g.player.alive || dist > 15) {
e.state = 'patrol';
e.patrolTimer = 0;
}
else if (dist < e.attackRange) {
e.state = 'attack';
}
else {
// Move toward player
const nx = dx / dist;
const ny = dy / dist;
tryMove(e, nx, ny, e.speed, dt);
}
break;
case 'attack':
if (!g.player.alive || dist > e.attackRange * 2) {
e.state = 'chase';
}
else {
// Attack
e.attackCooldown -= dt;
if (e.attackCooldown <= 0 && g.player.alive) {
// Check line of sight
if (!wallBlocksLOS(e.x, e.y, g.player.x, g.player.y)) {
const accuracy = e.type === 'soldier' ? 0.85 : 0.9;
if (Math.random() < accuracy) {
const dmg = e.damage * (0.7 + Math.random() * 0.6);
(0, player_1.takeDamage)(g.player, dmg);
g.damageFlash = 200;
g.screenShake = 4;
(0, audio_1.playSound)('hurt');
}
}
e.attackCooldown = e.type === 'boss' ? 500 : e.type === 'soldier' ? 600 : 1000 + Math.random() * 500;
}
// Boss and soldiers move slowly toward player when in range
if (dist > e.attackRange * 0.8) {
const nx = dx / dist;
const ny = dy / dist;
tryMove(e, nx, ny, e.speed * 0.2, dt);
}
else if (dist < e.attackRange * 0.5) {
// Back away
const nx = -dx / dist;
const ny = -dy / dist;
tryMove(e, nx, ny, e.speed * 0.2, dt);
}
}
break;
}
}
}
function tryMove(e, vx, vy, speed, dt) {
const moveX = vx * speed * (dt / 1000);
const moveY = vy * speed * (dt / 1000);
// Try X
const newX = e.x + moveX;
const tileX = Math.floor(newX);
const tileY = Math.floor(e.y);
if (tileX >= 0 && tileX < map_1.MAP_W && tileY >= 0 && tileY < map_1.MAP_H && map_1.MAP[tileY][tileX] === 0) {
e.x = newX;
}
// Try Y
const newTileX = Math.floor(e.x);
const newTileY = Math.floor(e.y + moveY);
if (newTileX >= 0 && newTileX < map_1.MAP_W && newTileY >= 0 && newTileY < map_1.MAP_H && map_1.MAP[newTileY][newTileX] === 0) {
e.y = e.y + moveY;
}
}
function wallBlocksLOS(x1, y1, x2, y2) {
const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
const steps = Math.ceil(dist * 8);
for (let i = 1; i < steps; i++) {
const t = i / steps;
const cx = x1 + (x2 - x1) * t;
const cy = y1 + (y2 - y1) * t;
const mx = Math.floor(cx);
const my = Math.floor(cy);
if (mx >= 0 && mx < map_1.MAP_W && my >= 0 && my < map_1.MAP_H && map_1.MAP[my][mx] > 0) {
return true;
}
}
return false;
}
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.playSound = playSound;
// Procedural sound effects using Web Audio API
let audioCtx = null;
let audioEnabled = false;
function initAudio() {
if (audioEnabled)
return;
audioEnabled = true;
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
catch (e) { /* Audio not supported */ }
}
function getAudioCtx() {
if (!audioCtx)
return null;
if (audioCtx.state === 'suspended')
audioCtx.resume();
return audioCtx;
}
function playSound(type) {
try {
const ctx = getAudioCtx();
if (!ctx)
return;
if (ctx.state === 'suspended')
ctx.resume();
switch (type) {
case 'pistol':
fireWeaponSound(ctx, 800, 0.08, 'sawtooth');
break;
case 'shotgun':
fireWeaponSound(ctx, 300, 0.15, 'square');
fireWeaponSound(ctx, 200, 0.2, 'sawtooth');
break;
case 'rifle':
fireWeaponSound(ctx, 1200, 0.04, 'sawtooth');
break;
case 'hit':
hitSound(ctx);
break;
case 'kill':
killSound(ctx);
break;
case 'pickup':
pickupSound(ctx);
break;
case 'hurt':
hurtSound(ctx);
break;
case 'death':
deathSound(ctx);
break;
}
}
catch (e) {
// Audio not available
}
}
function fireWeaponSound(ctx, freq, duration, type) {
if (!ctx)
return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(freq * 0.1, ctx.currentTime + duration);
gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + duration);
}
function hitSound(ctx) {
if (!ctx)
return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(400, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.1);
}
function killSound(ctx) {
if (!ctx)
return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(600, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(50, ctx.currentTime + 0.4);
gain.gain.setValueAtTime(0.12, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.4);
}
function pickupSound(ctx) {
if (!ctx)
return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(600, ctx.currentTime);
osc.frequency.setValueAtTime(800, ctx.currentTime + 0.05);
osc.frequency.setValueAtTime(1000, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.15);
}
function hurtSound(ctx) {
if (!ctx)
return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(150, ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.1, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.2);
}
function deathSound(ctx) {
if (!ctx)
return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(400, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(40, ctx.currentTime + 1.0);
gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.0);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 1.0);
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-UD-Q5_K_XL","provider":"brigid","persona":"typescript","sessionId":"c04c96af-8498-4776-82eb-ce2eb6e0a294","tokensIn":5053787,"tokensOut":52393,"tokensTotal":5106180,"cost":0,"turns":110,"toolCalls":117,"failedToolCalls":4,"timestamp":"2026-08-04T07:34:54.408Z"} -->