DOOM
Qwen 3.6 27b · 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 55,765 bytes · SHA-256 db37623e331d
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DUNGEON RAIDERS - Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
font-family: 'Courier New', monospace;
user-select: none;
}
#app {
position: relative;
width: 960px;
height: 600px;
}
#game {
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
/* ── css: hud ── */
#hud {
position: absolute;
bottom: 0;
left: 0;
width: 960px;
height: 80px;
background: linear-gradient(to bottom, #333, #111);
border-top: 3px solid #555;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 10;
}
#health-bar {
display: flex;
align-items: center;
gap: 8px;
color: #f44;
font-size: 24px;
font-weight: bold;
}
#health-bar::before {
content: '❤️';
font-size: 28px;
}
#weapon-info {
display: flex;
flex-direction: column;
align-items: center;
color: #ff0;
font-size: 18px;
font-weight: bold;
}
#weapon-name { font-size: 22px; }
#ammo-val { color: #fa0; }
#ammo-slots {
display: flex;
gap: 15px;
align-items: center;
}
.ammo-slot {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #444;
min-width: 50px;
}
.ammo-slot.active {
border-color: #ff0;
background: rgba(255,255,0,0.1);
}
.ammo-slot .slot-name {
font-size: 10px;
color: #888;
}
.ammo-slot .slot-count {
font-size: 16px;
color: #fff;
font-weight: bold;
}
#message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 36px;
font-weight: bold;
text-shadow: 2px 2px 4px #000;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
z-index: 20;
text-align: center;
}
#message.show { opacity: 1; }
#overlay {
position: absolute;
top: 0;
left: 0;
width: 960px;
height: 520px;
background: rgba(0,0,0,0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 30;
cursor: pointer;
}
#overlay-title {
font-size: 48px;
color: #f44;
font-weight: bold;
text-shadow: 3px 3px 6px #000;
margin-bottom: 20px;
}
#overlay-sub {
font-size: 24px;
color: #ff0;
animation: blink 1s infinite;
margin-bottom: 30px;
}
#overlay-instructions {
font-size: 14px;
color: #aaa;
text-align: center;
line-height: 1.8;
margin-top: 10px;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
</style>
</head>
<body>
<canvas id="game" width="960" height="600"></canvas><div id="message"></div>
<div id="hud">
<div id="health-bar"><span id="health-val">100</span></div>
<div id="ammo-slots">
<div class="ammo-slot active" id="slot-0"><span class="slot-name">PISTOL</span><span class="slot-count" id="ammo-0">12</span></div>
<div class="ammo-slot" id="slot-1"><span class="slot-name">SHOTGUN</span><span class="slot-count" id="ammo-1">8</span></div>
<div class="ammo-slot" id="slot-2"><span class="slot-name">MACHINEGUN</span><span class="slot-count" id="ammo-2">30</span></div>
</div>
<div id="weapon-info">
<span id="weapon-name">PISTOL</span>
<span id="ammo-val">Kills: 0/11</span>
</div>
</div>
<div id="overlay">
<div id="overlay-title">DUNGEON RAIDERS</div>
<div id="overlay-sub">Click to Start</div><div id="overlay-instructions">
WASD / Arrow Keys — Move & Turn<br>
Q/E — Strafe Left/Right<br>
Mouse — Look (click to lock)<br>
Space / Click — Shoot<br>
1/2/3 — Switch Weapon<br><br>
Kill all enemies, then reach the exit (yellow on minimap)!
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./map":"src/map.ts","./player":"src/player.ts","./enemies":"src/enemies.ts","./pickups":"src/pickups.ts","./input":"src/input.ts","./render":"src/render.ts","./sound":"src/sound.ts"},"src/map.ts":{"./types":"src/types.ts"},"src/raycast.ts":{"./map":"src/map.ts"},"src/player.ts":{"./types":"src/types.ts","./map":"src/map.ts"},"src/enemies.ts":{"./types":"src/types.ts","./map":"src/map.ts","./raycast":"src/raycast.ts"},"src/pickups.ts":{"./types":"src/types.ts"},"src/render.ts":{"./types":"src/types.ts","./raycast":"src/raycast.ts","./enemies":"src/enemies.ts","./pickups":"src/pickups.ts","./player":"src/player.ts","./map":"src/map.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 map_1 = require("./map");
const player_1 = require("./player");
const enemies_1 = require("./enemies");
const pickups_1 = require("./pickups");
const input_1 = require("./input");
const render_1 = require("./render");
const sound_1 = require("./sound");
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlay-title');
const overlaySub = document.getElementById('overlay-sub');
const healthVal = document.getElementById('health-val');
const weaponName = document.getElementById('weapon-name');
const ammoVal = document.getElementById('ammo-val');
const messageEl = document.getElementById('message');
let player = (0, player_1.createPlayer)();
let enemies = [];
let pickups = [];
let gameState;
let input;
let messageTimer = 0;
let currentMessage = '';
let lastTime = 0;
let running = false;
function initGame() {
player = (0, player_1.createPlayer)();
enemies = (0, map_1.spawnEnemies)();
pickups = (0, map_1.spawnPickups)();
gameState = {
phase: 'menu',
kills: 0,
totalEnemies: (0, enemies_1.countAlive)(enemies),
deathCount: 0,
level: 1,
};
currentMessage = '';
messageTimer = 0;
}
function showMessage(text, duration = 1200) {
currentMessage = text;
messageTimer = duration;
}
function updateHUD() {
healthVal.textContent = Math.ceil(player.health).toString();
weaponName.textContent = types_1.WEAPONS[player.weapon].name;
ammoVal.textContent = `Kills: ${gameState.kills}/${gameState.totalEnemies}`;
if (player.health > 50) {
healthVal.style.color = '#f44';
}
else if (player.health > 25) {
healthVal.style.color = '#fa0';
}
else {
healthVal.style.color = '#f00';
}
// Update ammo slots
for (let i = 0; i < 3; i++) {
const slot = document.getElementById(`slot-${i}`);
const count = document.getElementById(`ammo-${i}`);
if (slot && count) {
count.textContent = player.ammo[i].toString();
if (i === player.weapon) {
slot.classList.add('active');
}
else {
slot.classList.remove('active');
}
}
}
}
function handleShooting() {
if (!player.shooting)
return;
// Check ammo
if (player.ammo[player.weapon] <= 0) {
// Try to switch to a weapon with ammo
let found = false;
for (let i = 0; i < types_1.WEAPONS.length; i++) {
if (player.ammo[i] > 0) {
player.weapon = i;
found = true;
break;
}
}
if (!found) {
showMessage('No ammo!');
return;
}
}
const w = types_1.WEAPONS[player.weapon];
player.ammo[player.weapon] -= w.ammoCost;
player.weaponAnim = 1;
const pellets = w.pellets;
for (let i = 0; i < pellets; i++) {
const spread = (Math.random() - 0.5) * w.spread;
const shotAngle = player.angle + spread;
const hit = (0, enemies_1.checkShotHit)(enemies, player.x, player.y, shotAngle, w.range);
if (hit) {
if ((0, enemies_1.damageEnemy)(hit.enemy, w.damage)) {
gameState.kills++;
(0, sound_1.playSound)('enemy_die');
showMessage(`Enemy killed! (${gameState.kills}/${gameState.totalEnemies})`);
}
else {
(0, sound_1.playSound)('hit');
}
}
}
}
function handleWinCondition() {
// Win: all enemies dead AND reach the exit
if (gameState.kills >= gameState.totalEnemies) {
if ((0, map_1.isExit)(player.x, player.y)) {
(0, sound_1.playSound)('door');
gameState.phase = 'won';
running = false;
overlayTitle.textContent = 'VICTORY!';
overlayTitle.style.color = '#0f0';
overlaySub.textContent = `Deaths: ${gameState.deathCount} | Click to Play Again`;
overlay.style.display = 'flex';
}
else {
showMessage('All clear! Reach the exit (yellow on minimap)!');
}
}
}
function handleDeath() {
if (!player.alive) {
(0, sound_1.playSound)('death');
gameState.deathCount++;
gameState.phase = 'dead';
setTimeout(() => {
(0, player_1.respawnPlayer)(player);
// Respawn enemies that were killed
const freshEnemies = (0, map_1.spawnEnemies)();
for (let i = 0; i < enemies.length; i++) {
if (!enemies[i].alive && freshEnemies[i]) {
enemies[i] = freshEnemies[i];
}
}
gameState.kills = 0;
gameState.phase = 'playing';
running = true;
showMessage('Respawned!');
overlay.style.display = 'none';
}, 1500);
overlayTitle.textContent = 'YOU DIED';
overlayTitle.style.color = '#f00';
overlaySub.textContent = 'Respawning...';
overlay.style.display = 'flex';
running = false;
}
}
function gameLoop(timestamp) {
if (!running)
return;
const dt = Math.min(3, (timestamp - lastTime) / 16.67);
lastTime = timestamp;
// Input processing
let dx = 0;
let dy = 0;
if (input.forward)
dy += 1;
if (input.backward)
dy -= 1;
if (input.strafeLeft)
dx -= 1;
if (input.strafeRight)
dx += 1;
// Mouse look
const mouseDelta = (0, input_1.consumeMouseDelta)(input);
(0, player_1.rotatePlayer)(player, mouseDelta);
if (input.left)
(0, player_1.rotatePlayer)(player, -1);
if (input.right)
(0, player_1.rotatePlayer)(player, 1);
if (input.switchUp) {
(0, player_1.switchWeapon)(player, 1);
input.switchUp = false;
}
if (input.switchDown) {
(0, player_1.switchWeapon)(player, -1);
input.switchDown = false;
}
// Move player
(0, player_1.movePlayer)(player, dx, dy, dt);
// Update timers
(0, player_1.updatePlayerTimers)(player, dt);
// Weapon animation decay
if (player.weaponAnim > 0) {
player.weaponAnim -= dt * 0.15;
if (player.weaponAnim < 0)
player.weaponAnim = 0;
}
// Shooting
if (input.shoot) {
if ((0, player_1.tryShoot)(player, timestamp)) {
// Play weapon sound
const soundNames = ['pistol', 'shotgun', 'machinegun'];
(0, sound_1.playSound)(soundNames[player.weapon]);
handleShooting();
}
}
// Update enemies
(0, enemies_1.updateEnemies)(enemies, player, dt);
// Check pickups
const pickupMsg = (0, pickups_1.checkPickupCollision)(player, pickups);
if (pickupMsg) {
showMessage(pickupMsg);
(0, sound_1.playSound)('pickup');
}
// Message timer
if (messageTimer > 0) {
messageTimer -= 16.67 * dt;
}
else {
currentMessage = '';
}
// Check death
if (!player.alive) {
handleDeath();
return;
}
// Check win
handleWinCondition();
// Update HUD
updateHUD();
// Render
const msg = messageTimer > 0 ? currentMessage : null;
(0, render_1.render)(ctx, player, enemies, pickups, canvas.width, canvas.height, msg);
requestAnimationFrame(gameLoop);
}
function startGame() {
initGame();
input = (0, input_1.setupInput)(canvas);
gameState.phase = 'playing';
running = true;
overlay.style.display = 'none';
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
// Initial setup
initGame();
(0, render_1.render)(ctx, player, enemies, pickups, canvas.width, canvas.height, null);
overlay.addEventListener('click', () => {
if (gameState.phase === 'menu' || gameState.phase === 'won') {
startGame();
}
});
// Also allow keyboard start
document.addEventListener('keydown', (e) => {
if ((e.code === 'Space' || e.code === 'Enter') && (gameState.phase === 'menu' || gameState.phase === 'won')) {
startGame();
}
});
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Core types for DUNGEON RAIDERS
Object.defineProperty(exports, "__esModule", { value: true });
exports.ENEMY_TYPES = exports.WEAPONS = void 0;
exports.WEAPONS = [
{ name: 'PISTOL', damage: 15, fireRate: 15, ammoCost: 1, spread: 0.02, pellets: 1, range: 20 },
{ name: 'SHOTGUN', damage: 10, fireRate: 30, ammoCost: 1, spread: 0.15, pellets: 5, range: 15 },
{ name: 'MACHINE GUN', damage: 8, fireRate: 5, ammoCost: 1, spread: 0.05, pellets: 1, range: 18 },
];
exports.ENEMY_TYPES = {
grunt: { health: 40, speed: 0.025, damage: 8, alertRange: 8, color: '#4a4' },
soldier: { health: 80, speed: 0.02, damage: 15, alertRange: 10, color: '#a44' },
boss: { health: 200, speed: 0.015, damage: 25, alertRange: 12, color: '#a4a' },
};
};
// ── module: src/map.ts ──
__mods["src/map.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WALL_COLORS = exports.MAP_H = exports.MAP_W = exports.MAP = void 0;
exports.getTile = getTile;
exports.isWall = isWall;
exports.isExit = isExit;
exports.spawnEnemies = spawnEnemies;
exports.spawnPickups = spawnPickups;
// 24x24 map. 0=empty, 1-5=wall types, 6=exit door
exports.MAP = [
1, 1, 1, 1, 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, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 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, 0, 0, 0, 0, 1,
1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 3, 3, 3, 0, 0, 1,
1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 1,
1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 0, 1,
1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 4, 4, 4, 0, 0, 1, 1, 1, 1, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0, 4, 0, 0, 1, 0, 0, 1, 0, 0, 1,
1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1,
1, 0, 0, 0, 0, 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, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 3, 3, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1,
1, 0, 0, 3, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 2, 0, 0, 1, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 3, 3, 0, 0, 0, 1,
1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 1,
1, 0, 0, 0, 0, 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, 0, 0, 0, 6, 1,
1, 0, 0, 0, 0, 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, 1, 1, 1, 1,
];
exports.MAP_W = 24;
exports.MAP_H = 24;
// Wall colors per type [r, g, b]
exports.WALL_COLORS = [
[80, 80, 80], // 0: empty (not used)
[150, 75, 75], // 1: red brick
[75, 120, 75], // 2: green stone
[75, 75, 150], // 3: blue stone
[150, 130, 75], // 4: gold
[100, 80, 120], // 5: purple
[200, 200, 50], // 6: exit (yellow)
];
function getTile(x, y) {
const ix = Math.floor(x);
const iy = Math.floor(y);
if (ix < 0 || ix >= exports.MAP_W || iy < 0 || iy >= exports.MAP_H)
return 1;
return exports.MAP[iy * exports.MAP_W + ix];
}
function isWall(x, y) {
const t = getTile(x, y);
return t >= 1 && t <= 5;
}
function isExit(x, y) {
return getTile(x, y) === 6;
}
// Spawn enemies at fixed positions
function spawnEnemies() {
const enemies = [
// Grunts scattered around
{ x: 5.5, y: 3.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
{ x: 10.5, y: 5.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
{ x: 18.5, y: 4.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
{ x: 8.5, y: 12.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
{ x: 15.5, y: 15.5, health: 40, maxHealth: 40, type: 'grunt', alive: true, state: 'idle', attackTimer: 0, alertRange: 8, speed: 0.025, damage: 8, hitFlash: 0 },
// Soldiers in tougher spots
{ x: 4.5, y: 8.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
{ x: 12.5, y: 10.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
{ x: 20.5, y: 12.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
{ x: 10.5, y: 17.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
{ x: 18.5, y: 18.5, health: 80, maxHealth: 80, type: 'soldier', alive: true, state: 'idle', attackTimer: 0, alertRange: 10, speed: 0.02, damage: 15, hitFlash: 0 },
// Boss near the exit
{ x: 21.5, y: 21.5, health: 200, maxHealth: 200, type: 'boss', alive: true, state: 'idle', attackTimer: 0, alertRange: 12, speed: 0.015, damage: 25, hitFlash: 0 },
];
return enemies;
}
// Spawn pickups
function spawnPickups() {
return [
{ x: 3.5, y: 5.5, type: 'health', collected: false },
{ x: 7.5, y: 1.5, type: 'ammo_pistol', collected: false },
{ x: 15.5, y: 3.5, type: 'ammo_shotgun', collected: false },
{ x: 20.5, y: 7.5, type: 'health', collected: false },
{ x: 1.5, y: 10.5, type: 'ammo_machinegun', collected: false },
{ x: 11.5, y: 13.5, type: 'health', collected: false },
{ x: 5.5, y: 16.5, type: 'ammo_shotgun', collected: false },
{ x: 16.5, y: 19.5, type: 'ammo_pistol', collected: false },
{ x: 22.5, y: 15.5, type: 'health', collected: false },
{ x: 1.5, y: 20.5, type: 'ammo_machinegun', collected: false },
{ x: 10.5, y: 8.5, type: 'ammo_shotgun', collected: false },
{ x: 14.5, y: 20.5, type: 'health', collected: false },
];
}
};
// ── module: src/raycast.ts ──
__mods["src/raycast.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.castRay = castRay;
exports.getWallColor = getWallColor;
exports.getFloorCeilingColor = getFloorCeilingColor;
exports.hasLineOfSight = hasLineOfSight;
const map_1 = require("./map");
function castRay(px, py, angle, maxDist = 30) {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
// Current map cell
let mapX = Math.floor(px);
let mapY = Math.floor(py);
// Length of ray from one x-side to next x-side
const deltaDistX = Math.abs(1 / cos) || 1e10;
const deltaDistY = Math.abs(1 / sin) || 1e10;
// Direction each step goes
let stepX;
let stepY;
let sideDistX;
let sideDistY;
if (cos < 0) {
stepX = -1;
sideDistX = (px - mapX) * deltaDistX;
}
else {
stepX = 1;
sideDistX = (mapX + 1 - px) * deltaDistX;
}
if (sin < 0) {
stepY = -1;
sideDistY = (py - mapY) * deltaDistY;
}
else {
stepY = 1;
sideDistY = (mapY + 1 - py) * deltaDistY;
}
let side = 0;
let depth = 0;
for (let i = 0; i < 200; i++) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
depth++;
const tile = (0, map_1.getTile)(mapX, mapY);
if (tile >= 1 && tile <= 6) {
// Calculate actual distance
let perpDist;
let wallX;
if (side === 0) {
perpDist = (mapX - px + (1 - stepX) / 2) / cos;
wallX = py + perpDist * sin;
}
else {
perpDist = (mapY - py + (1 - stepY) / 2) / sin;
wallX = px + perpDist * cos;
}
wallX -= Math.floor(wallX);
return {
dist: perpDist,
wallType: tile,
side,
hitX: wallX,
};
}
}
return { dist: maxDist, wallType: 0, side: 0, hitX: 0 };
}
function getWallColor(wallType, side, dist) {
const base = map_1.WALL_COLORS[wallType] || map_1.WALL_COLORS[1];
const shade = side === 1 ? 0.7 : 1.0;
// Fog: darken with distance
const fog = Math.max(0.15, 1 - dist / 20);
return [
Math.floor(base[0] * shade * fog),
Math.floor(base[1] * shade * fog),
Math.floor(base[2] * shade * fog),
];
}
function getFloorCeilingColor(dist) {
const fog = Math.max(0.1, 1 - dist / 18);
const floorR = Math.floor(60 * fog);
const floorG = Math.floor(50 * fog);
const floorB = Math.floor(40 * fog);
const ceilR = Math.floor(40 * fog);
const ceilG = Math.floor(35 * fog);
const ceilB = Math.floor(50 * fog);
return [floorR, floorG, floorB, ceilR, ceilG, ceilB];
}
// Check line of sight (simplified - check if any wall blocks the path)
function hasLineOfSight(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
const dist = Math.sqrt(dx * dx + dy * dy);
const steps = Math.ceil(dist * 4);
for (let i = 0; i < steps; i++) {
const t = i / steps;
const cx = x1 + dx * t;
const cy = y1 + dy * t;
if ((0, map_1.isWall)(cx, cy))
return false;
}
return true;
}
};
// ── 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.movePlayer = movePlayer;
exports.rotatePlayer = rotatePlayer;
exports.tryShoot = tryShoot;
exports.updatePlayerTimers = updatePlayerTimers;
exports.getBobOffset = getBobOffset;
exports.switchWeapon = switchWeapon;
const types_1 = require("./types");
const map_1 = require("./map");
function createPlayer() {
return {
x: 2.5,
y: 2.5,
angle: 0.5,
health: 100,
maxHealth: 100,
alive: true,
weapon: 0,
ammo: [12, 8, 30], // pistol, shotgun, machine gun
shooting: false,
shootTimer: 0,
bobPhase: 0,
damageFlash: 0,
weaponAnim: 0,
};
}
function respawnPlayer(p) {
p.x = 2.5;
p.y = 2.5;
p.angle = 0.5;
p.health = 100;
p.alive = true;
p.weapon = 0;
p.ammo = [12, 8, 30];
p.shooting = false;
p.shootTimer = 0;
p.damageFlash = 0;
p.weaponAnim = 0;
}
function movePlayer(p, dx, dy, dt) {
if (!p.alive)
return;
const speed = 0.05 * dt;
const cos = Math.cos(p.angle);
const sin = Math.sin(p.angle);
// Forward/back
let moveX = cos * dy * speed;
let moveY = sin * dy * speed;
// Strafe
moveX += -sin * dx * speed;
moveY += cos * dx * speed;
// Collision detection with sliding
const margin = 0.2;
const newX = p.x + moveX;
const newY = p.y + moveY;
if (!(0, map_1.isWall)(newX + (moveX > 0 ? margin : -margin), p.y)) {
p.x = newX;
}
if (!(0, map_1.isWall)(p.x, newY + (moveY > 0 ? margin : -margin))) {
p.y = newY;
}
// Head bob
if (dx !== 0 || dy !== 0) {
p.bobPhase += 0.12 * dt;
}
}
function rotatePlayer(p, da) {
if (!p.alive)
return;
p.angle += da * 0.03;
}
function tryShoot(p, frame) {
if (!p.alive)
return false;
if (p.shootTimer > 0)
return false;
const w = types_1.WEAPONS[p.weapon];
p.shootTimer = w.fireRate;
p.shooting = true;
return true;
}
function updatePlayerTimers(p, dt) {
if (p.shootTimer > 0) {
p.shootTimer -= dt;
if (p.shootTimer <= 0) {
p.shooting = false;
p.shootTimer = 0;
}
}
if (p.damageFlash > 0) {
p.damageFlash -= dt * 2;
}
}
function getBobOffset(p) {
return Math.sin(p.bobPhase) * 5;
}
function switchWeapon(p, dir) {
if (!p.alive)
return;
p.weapon = (p.weapon + dir + types_1.WEAPONS.length) % types_1.WEAPONS.length;
}
};
// ── module: src/enemies.ts ──
__mods["src/enemies.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateEnemies = updateEnemies;
exports.damageEnemy = damageEnemy;
exports.getEnemyColor = getEnemyColor;
exports.countAlive = countAlive;
exports.checkShotHit = checkShotHit;
const types_1 = require("./types");
const map_1 = require("./map");
const raycast_1 = require("./raycast");
function updateEnemies(enemies, player, dt) {
for (const e of enemies) {
if (!e.alive)
continue;
// Hit flash decay
if (e.hitFlash > 0)
e.hitFlash -= dt * 3;
const dx = player.x - e.x;
const dy = player.y - e.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Detection
if (dist < e.alertRange && player.alive) {
if ((0, raycast_1.hasLineOfSight)(e.x, e.y, player.x, player.y)) {
e.state = 'chase';
}
}
if (e.state === 'idle')
continue;
// Chase player
if (dist > 1.0) {
const nx = dx / dist;
const ny = dy / dist;
const speed = e.speed * dt;
const newX = e.x + nx * speed;
const newY = e.y + ny * speed;
// Simple collision - try each axis separately
if (!(0, map_1.isWall)(newX, e.y))
e.x = newX;
if (!(0, map_1.isWall)(e.x, newY))
e.y = newY;
}
else {
e.state = 'attack';
}
// Attack player
if (e.state === 'attack' && dist < 1.5) {
e.attackTimer += dt;
if (e.attackTimer > 60) {
e.attackTimer = 0;
if (player.alive) {
player.health -= e.damage;
player.damageFlash = 1;
if (player.health <= 0) {
player.health = 0;
player.alive = false;
}
}
}
}
}
}
function damageEnemy(e, damage) {
if (!e.alive)
return false;
e.health -= damage;
e.hitFlash = 1;
if (e.health <= 0) {
e.health = 0;
e.alive = false;
return true;
}
return false;
}
function getEnemyColor(e) {
if (e.hitFlash > 0)
return '#fff';
return types_1.ENEMY_TYPES[e.type].color;
}
function countAlive(enemies) {
return enemies.filter(e => e.alive).length;
}
// Check if a shot at a given angle and distance hits an enemy
function checkShotHit(enemies, px, py, angle, maxDist) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
let hit = null;
for (const e of enemies) {
if (!e.alive)
continue;
const dx = e.x - px;
const dy = e.y - py;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > maxDist)
continue;
// Project enemy onto ray direction
const dot = dx * cos + dy * sin;
if (dot < 0)
continue; // behind
// Perpendicular distance
const perpDist = Math.abs(dx * sin - dy * cos);
// Enemy hitbox radius (smaller for grunts, larger for boss)
const radius = e.type === 'boss' ? 0.5 : 0.35;
if (perpDist < radius && dot < dist) {
if (!hit || dot < hit.dist) {
hit = { enemy: e, dist: dot };
}
}
}
return hit;
}
};
// ── module: src/pickups.ts ──
__mods["src/pickups.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PICKUP_LABELS = exports.PICKUP_COLORS = void 0;
exports.checkPickupCollision = checkPickupCollision;
exports.countRemaining = countRemaining;
exports.PICKUP_COLORS = {
health: '#f44',
ammo_pistol: '#ff0',
ammo_shotgun: '#f80',
ammo_machinegun: '#0ff',
};
exports.PICKUP_LABELS = {
health: '+HP',
ammo_pistol: 'P',
ammo_shotgun: 'S',
ammo_machinegun: 'M',
};
function checkPickupCollision(player, pickups) {
for (const p of pickups) {
if (p.collected)
continue;
const dx = player.x - p.x;
const dy = player.y - p.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.5) {
p.collected = true;
switch (p.type) {
case 'health':
player.health = Math.min(player.maxHealth, player.health + 25);
return 'Health +25!';
case 'ammo_pistol':
return 'Pistol Ammo!';
case 'ammo_shotgun':
return 'Shotgun Ammo!';
case 'ammo_machinegun':
return 'Machine Gun Ammo!';
}
}
}
return null;
}
function countRemaining(pickups) {
return pickups.filter(p => !p.collected).length;
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInput = createInput;
exports.setupInput = setupInput;
exports.consumeMouseDelta = consumeMouseDelta;
function createInput() {
return {
forward: false,
backward: false,
left: false,
right: false,
strafeLeft: false,
strafeRight: false,
shoot: false,
switchUp: false,
switchDown: false,
};
}
function setupInput(canvas) {
const input = createInput();
const keyMap = {
KeyW: 'forward',
ArrowUp: 'forward',
KeyS: 'backward',
ArrowDown: 'backward',
ArrowLeft: 'left',
KeyA: 'left',
ArrowRight: 'right',
KeyD: 'right',
KeyQ: 'strafeLeft',
KeyE: 'strafeRight',
Space: 'shoot',
Digit1: 'switchUp',
Digit2: 'switchUp',
Digit3: 'switchDown',
};
canvas.addEventListener('contextmenu', e => e.preventDefault());
canvas.addEventListener('keydown', e => {
const key = e.code;
if (key in keyMap) {
input[keyMap[key]] = true;
}
});
canvas.addEventListener('keyup', e => {
const key = e.code;
if (key in keyMap) {
input[keyMap[key]] = false;
}
});
// Mouse movement for looking
canvas.addEventListener('mousedown', (e) => {
if (e.button === 0) {
canvas.requestPointerLock?.();
input.shoot = true;
}
});
canvas.addEventListener('mouseup', (e) => {
if (e.button === 0) {
input.shoot = false;
}
});
document.addEventListener('mousemove', e => {
if (document.pointerLockElement === canvas) {
// Mouse look handled in game loop
input._mouseDX = (input._mouseDX || 0) + e.movementX;
}
});
return input;
}
function consumeMouseDelta(input) {
const dx = input._mouseDX || 0;
input._mouseDX = 0;
return dx;
}
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
const types_1 = require("./types");
const raycast_1 = require("./raycast");
const enemies_1 = require("./enemies");
const pickups_1 = require("./pickups");
const player_1 = require("./player");
const map_1 = require("./map");
// Helper: shade a hex color by a factor
function shadeColor(hex, factor) {
const num = parseInt(hex.replace('#', ''), 16);
const r = Math.min(255, Math.max(0, ((num >> 16) & 0xff) * factor));
const g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) * factor));
const b = Math.min(255, Math.max(0, (num & 0xff) * factor));
return `rgb(${Math.floor(r)},${Math.floor(g)},${Math.floor(b)})`;
}
const FOV = Math.PI / 3;
const NUM_RAYS = 480; // half the canvas width for performance
function render(ctx, player, enemies, pickups, width, height, message) {
const viewHeight = height - 80; // leave room for HUD
const halfH = viewHeight / 2;
const bob = (0, player_1.getBobOffset)(player);
// Clear sky
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, width, viewHeight);
// Cast rays and draw walls
const zBuffer = [];
for (let i = 0; i < NUM_RAYS; i++) {
const rayAngle = player.angle - FOV / 2 + (i / NUM_RAYS) * FOV;
const ray = (0, raycast_1.castRay)(player.x, player.y, rayAngle);
// Fix fisheye
const perpDist = ray.dist * Math.cos(rayAngle - player.angle);
zBuffer[i] = perpDist;
const wallHeight = Math.min(viewHeight, (viewHeight / perpDist) * 0.8);
const wallTop = halfH - wallHeight / 2 + bob;
const [r, g, b] = (0, raycast_1.getWallColor)(ray.wallType, ray.side, ray.dist);
const x = (i / NUM_RAYS) * width;
const colW = Math.ceil(width / NUM_RAYS) + 1;
// Draw wall with texture stripe pattern
const stripeSize = Math.max(2, wallHeight / 20);
for (let s = 0; s < wallHeight; s += stripeSize) {
const stripeY = wallTop + s;
const stripeH = Math.min(stripeSize, wallHeight - s);
// Alternating stripe shading for brick/stone texture
const stripeIdx = Math.floor((s - bob) / stripeSize);
const shade = (stripeIdx % 2 === 0) ? 1.0 : 0.85;
// Edge darkening for texture effect
const edgeFade = (ray.hitX < 0.05 || ray.hitX > 0.95) ? 0.7 : 1.0;
ctx.fillStyle = `rgb(${Math.floor(r * shade * edgeFade)},${Math.floor(g * shade * edgeFade)},${Math.floor(b * shade * edgeFade)})`;
ctx.fillRect(x, stripeY, colW, stripeH);
}
// Floor with distance fog
const floorFog = Math.max(0.05, 1 - perpDist / 18);
const floorR = Math.floor(55 * floorFog);
const floorG = Math.floor(45 * floorFog);
const floorB = Math.floor(35 * floorFog);
ctx.fillStyle = `rgb(${floorR},${floorG},${floorB})`;
ctx.fillRect(x, wallTop + wallHeight, colW, viewHeight - wallTop - wallHeight);
// Floor grid lines (subtle)
const gridSpacing = 0.5;
const gridY1 = player.y + (Math.floor(player.y / gridSpacing) + 1) * gridSpacing;
const gridDist1 = (gridY1 - player.y) / Math.sin(FOV / 2 + Math.abs(rayAngle - player.angle));
if (gridDist1 > 0 && gridDist1 < perpDist) {
const screenY1 = halfH + viewHeight / (2 * gridDist1 * Math.cos(rayAngle - player.angle)) + bob;
if (screenY1 > wallTop + wallHeight && screenY1 < viewHeight) {
ctx.fillStyle = `rgba(80, 70, 60, ${0.3 * floorFog})`;
ctx.fillRect(x, screenY1, colW, 1);
}
}
// Ceiling
const ceilFog = Math.max(0.05, 1 - perpDist / 15);
ctx.fillStyle = `rgb(${Math.floor(30 * ceilFog)},${Math.floor(28 * ceilFog)},${Math.floor(40 * ceilFog)})`;
ctx.fillRect(x, 0, colW, wallTop);
}
// Draw sprites (enemies and pickups)
drawSprites(ctx, player, enemies, pickups, zBuffer, width, viewHeight, halfH, bob);
// Draw crosshair
drawCrosshair(ctx, width, viewHeight);
// Draw weapon
drawWeapon(ctx, player, width, viewHeight, bob);
// Vignette effect
const gradient = ctx.createRadialGradient(width / 2, viewHeight / 2, viewHeight * 0.3, width / 2, viewHeight / 2, viewHeight * 0.9);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.5)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, viewHeight);
// Damage flash
if (player.damageFlash > 0) {
ctx.fillStyle = `rgba(255, 0, 0, ${player.damageFlash * 0.3})`;
ctx.fillRect(0, 0, width, viewHeight);
}
// Message
if (message) {
ctx.fillStyle = '#fff';
ctx.font = 'bold 24px Courier New';
ctx.textAlign = 'center';
ctx.fillText(message, width / 2, 60);
}
// Minimap
drawMinimap(ctx, player, enemies, width, viewHeight);
}
function drawCrosshair(ctx, width, viewHeight) {
const cx = width / 2;
const cy = viewHeight / 2;
const size = 12;
const gap = 4;
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 2;
ctx.shadowColor = '#0f0';
ctx.shadowBlur = 4;
// Top
ctx.beginPath();
ctx.moveTo(cx, cy - gap);
ctx.lineTo(cx, cy - size);
ctx.stroke();
// Bottom
ctx.beginPath();
ctx.moveTo(cx, cy + gap);
ctx.lineTo(cx, cy + size);
ctx.stroke();
// Left
ctx.beginPath();
ctx.moveTo(cx - gap, cy);
ctx.lineTo(cx - size, cy);
ctx.stroke();
// Right
ctx.beginPath();
ctx.moveTo(cx + gap, cy);
ctx.lineTo(cx + size, cy);
ctx.stroke();
ctx.shadowBlur = 0;
}
function drawSprites(ctx, player, enemies, pickups, zBuffer, width, viewHeight, halfH, bob) {
const sprites = [];
for (const e of enemies) {
if (!e.alive)
continue;
const dx = e.x - player.x;
const dy = e.y - player.y;
sprites.push({ x: e.x, y: e.y, dist: Math.sqrt(dx * dx + dy * dy), type: 'enemy', data: e });
}
for (const p of pickups) {
if (p.collected)
continue;
const dx = p.x - player.x;
const dy = p.y - player.y;
sprites.push({ x: p.x, y: p.y, dist: Math.sqrt(dx * dx + dy * dy), type: 'pickup', data: p });
}
// Sort by distance (far first)
sprites.sort((a, b) => b.dist - a.dist);
for (const sprite of sprites) {
const dx = sprite.x - player.x;
const dy = sprite.y - player.y;
// Transform relative to player angle
const cos = Math.cos(player.angle);
const sin = Math.sin(player.angle);
const transformX = dx * cos + dy * sin;
const transformY = -dx * sin + dy * cos;
if (transformX <= 0.1)
continue; // behind player
const screenX = (0.5 + transformY / transformX * 0.5) * width;
const spriteHeight = Math.min(viewHeight, (viewHeight / transformX) * 0.6);
const spriteWidth = spriteHeight * 0.7;
// Check z-buffer
const bufIdx = Math.floor((screenX / width) * (zBuffer.length - 1));
if (bufIdx >= 0 && bufIdx < zBuffer.length && zBuffer[bufIdx] < transformX * 0.8)
continue;
const spriteTop = halfH - spriteHeight / 2 + bob;
if (sprite.type === 'enemy') {
const enemy = sprite.data;
const color = (0, enemies_1.getEnemyColor)(enemy);
// Animated idle bob
const idleBob = Math.sin(Date.now() / 400 + sprite.x * 10) * 3;
// Legs
const legW = spriteWidth * 0.2;
const legH = spriteHeight * 0.25;
const legY = spriteTop + spriteHeight * 0.7 + idleBob;
ctx.fillStyle = shadeColor(color, 0.6);
ctx.fillRect(screenX - spriteWidth * 0.3, legY, legW, legH);
ctx.fillRect(screenX + spriteWidth * 0.1, legY, legW, legH);
// Body/torso
const bodyW = spriteWidth * 0.7;
const bodyH = spriteHeight * 0.4;
const bodyY = spriteTop + spriteHeight * 0.3 + idleBob;
ctx.fillStyle = color;
ctx.fillRect(screenX - bodyW / 2, bodyY, bodyW, bodyH);
// Body detail - belt
ctx.fillStyle = shadeColor(color, 0.5);
ctx.fillRect(screenX - bodyW / 2, bodyY + bodyH * 0.6, bodyW, 3);
// Arms
const armW = spriteWidth * 0.15;
const armH = spriteHeight * 0.35;
const armY = bodyY + 5;
ctx.fillStyle = shadeColor(color, 0.8);
ctx.fillRect(screenX - bodyW / 2 - armW, armY, armW, armH);
ctx.fillRect(screenX + bodyW / 2, armY, armW, armH);
// Head
const headR = spriteWidth * 0.25;
const headY = spriteTop + spriteHeight * 0.18 + idleBob;
ctx.fillStyle = shadeColor(color, 1.1);
ctx.beginPath();
ctx.arc(screenX, headY, headR, 0, Math.PI * 2);
ctx.fill();
// Eyes (menacing)
const eyeSize = headR * 0.25;
ctx.fillStyle = enemy.state === 'attack' ? '#f00' : '#ff0';
ctx.fillRect(screenX - headR * 0.4, headY - headR * 0.15, eyeSize, eyeSize);
ctx.fillRect(screenX + headR * 0.15, headY - headR * 0.15, eyeSize, eyeSize);
// Mouth
if (enemy.state === 'attack') {
ctx.fillStyle = '#000';
ctx.fillRect(screenX - headR * 0.2, headY + headR * 0.2, headR * 0.4, headR * 0.15);
}
// Boss crown/helmet
if (enemy.type === 'boss') {
ctx.fillStyle = '#ff0';
ctx.fillRect(screenX - headR * 0.8, headY - headR * 1.1, headR * 1.6, headR * 0.3);
ctx.fillRect(screenX - headR * 0.5, headY - headR * 1.3, headR * 0.3, headR * 0.3);
ctx.fillRect(screenX + headR * 0.2, headY - headR * 1.3, headR * 0.3, headR * 0.3);
}
// Health bar
if (enemy.health < enemy.maxHealth) {
const barW = spriteWidth * 0.8;
const barH = Math.max(3, spriteHeight * 0.03);
const barX = screenX - barW / 2;
const barY = spriteTop - 10 + idleBob;
ctx.fillStyle = '#300';
ctx.fillRect(barX, barY, barW, barH);
const hpPct = enemy.health / enemy.maxHealth;
ctx.fillStyle = hpPct > 0.5 ? '#0f0' : hpPct > 0.25 ? '#ff0' : '#f00';
ctx.fillRect(barX, barY, barW * hpPct, barH);
}
}
else {
const pickup = sprite.data;
const color = pickups_1.PICKUP_COLORS[pickup.type];
// Pickup bobbing
const bobY = Math.sin(Date.now() / 300) * 5;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(screenX, spriteTop + spriteHeight * 0.5 + bobY, spriteWidth * 0.25, 0, Math.PI * 2);
ctx.fill();
// Glow
ctx.fillStyle = color + '44';
ctx.beginPath();
ctx.arc(screenX, spriteTop + spriteHeight * 0.5 + bobY, spriteWidth * 0.4, 0, Math.PI * 2);
ctx.fill();
// Label
ctx.fillStyle = '#fff';
ctx.font = '10px Courier New';
ctx.textAlign = 'center';
ctx.fillText(pickups_1.PICKUP_LABELS[pickup.type], screenX, spriteTop + spriteHeight * 0.3);
}
}
}
function drawWeapon(ctx, player, width, viewHeight, bob) {
const w = types_1.WEAPONS[player.weapon];
const weaponX = width / 2 + Math.cos(player.bobPhase) * 8;
const weaponY = viewHeight - 80 + bob;
const recoil = player.weaponAnim * 30;
// Muzzle flash
if (player.weaponAnim > 0.5) {
const flashSize = player.weaponAnim * 30;
ctx.fillStyle = `rgba(255, 255, 100, ${player.weaponAnim})`;
ctx.beginPath();
ctx.arc(weaponX, weaponY - 70 - recoil, flashSize, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = `rgba(255, 255, 255, ${player.weaponAnim})`;
ctx.beginPath();
ctx.arc(weaponX, weaponY - 70 - recoil, flashSize * 0.5, 0, Math.PI * 2);
ctx.fill();
}
const wy = weaponY - recoil;
// Weapon body
switch (player.weapon) {
case 0: // Pistol
ctx.fillStyle = '#777';
ctx.fillRect(weaponX - 8, wy - 30, 16, 55);
ctx.fillStyle = '#555';
ctx.fillRect(weaponX - 12, wy + 10, 24, 35);
ctx.fillStyle = '#888';
ctx.fillRect(weaponX - 6, wy - 28, 12, 5);
break;
case 1: // Shotgun
ctx.fillStyle = '#666';
ctx.fillRect(weaponX - 12, wy - 50, 24, 75);
ctx.fillStyle = '#555';
ctx.fillRect(weaponX - 16, wy + 5, 32, 45);
ctx.fillStyle = '#444';
ctx.fillRect(weaponX - 6, wy - 55, 12, 18);
// Pump detail
ctx.fillStyle = '#777';
ctx.fillRect(weaponX - 10, wy - 20, 20, 8);
break;
case 2: // Machine gun
ctx.fillStyle = '#666';
ctx.fillRect(weaponX - 10, wy - 40, 20, 65);
ctx.fillStyle = '#555';
ctx.fillRect(weaponX - 14, wy + 5, 28, 40);
ctx.fillStyle = '#444';
ctx.fillRect(weaponX - 4, wy - 45, 8, 15);
// Magazine
ctx.fillStyle = '#777';
ctx.fillRect(weaponX - 16, wy - 20, 6, 25);
// Barrel
ctx.fillStyle = '#888';
ctx.fillRect(weaponX - 3, wy - 50, 6, 12);
break;
}
}
function drawMinimap(ctx, player, enemies, width, _viewHeight) {
const mapSize = 120;
const cellSize = mapSize / map_1.MAP_W;
const offsetX = width - mapSize - 10;
const offsetY = 10;
// Background
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(offsetX - 2, offsetY - 2, mapSize + 4, mapSize + 4);
// Walls
for (let y = 0; y < map_1.MAP_H; y++) {
for (let x = 0; x < map_1.MAP_W; x++) {
const tile = map_1.MAP[y * map_1.MAP_W + x];
if (tile >= 1) {
if (tile === 6) {
ctx.fillStyle = '#ff0';
}
else {
ctx.fillStyle = '#555';
}
ctx.fillRect(offsetX + x * cellSize, offsetY + y * cellSize, cellSize, cellSize);
}
}
}
// Player direction indicator
ctx.fillStyle = '#0f0';
ctx.beginPath();
ctx.arc(offsetX + player.x * cellSize, offsetY + player.y * cellSize, 3, 0, Math.PI * 2);
ctx.fill();
// Direction line
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(offsetX + player.x * cellSize, offsetY + player.y * cellSize);
ctx.lineTo(offsetX + (player.x + Math.cos(player.angle) * 1.5) * cellSize, offsetY + (player.y + Math.sin(player.angle) * 1.5) * cellSize);
ctx.stroke();
// Enemies
for (const e of enemies) {
if (!e.alive)
continue;
ctx.fillStyle = '#f00';
ctx.fillRect(offsetX + e.x * cellSize - 2, offsetY + e.y * cellSize - 2, 4, 4);
}
}
};
// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.playSound = playSound;
// Simple synthesized sound effects using Web Audio API
let audioCtx = null;
function getAudioCtx() {
if (!audioCtx) {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
catch {
return null;
}
}
return audioCtx;
}
function playSound(type) {
const ctx = getAudioCtx();
if (!ctx)
return;
const now = ctx.currentTime;
switch (type) {
case 'pistol': {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.15);
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);
// Noise burst
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) * 0.5;
const noise = ctx.createBufferSource();
const noiseGain = ctx.createGain();
noise.buffer = buffer;
noiseGain.gain.setValueAtTime(0.4, now);
noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
noise.connect(noiseGain).connect(ctx.destination);
noise.start(now);
break;
}
case 'shotgun': {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.3);
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.1;
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();
const noiseGain = ctx.createGain();
noise.buffer = buffer;
noiseGain.gain.setValueAtTime(0.6, now);
noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
noise.connect(noiseGain).connect(ctx.destination);
noise.start(now);
break;
}
case 'machinegun': {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(80, now + 0.08);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.08);
break;
}
case 'hit': {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(400, now);
osc.frequency.exponentialRampToValueAtTime(100, now + 0.1);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.1);
break;
}
case 'pickup': {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(440, now);
osc.frequency.setValueAtTime(660, now + 0.1);
osc.frequency.setValueAtTime(880, now + 0.2);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.3);
break;
}
case 'death': {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(20, now + 1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 1);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 1);
break;
}
case 'enemy_die': {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.5);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.5);
break;
}
case 'door': {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(500, now);
osc.frequency.setValueAtTime(700, now + 0.15);
osc.frequency.setValueAtTime(900, now + 0.3);
osc.frequency.setValueAtTime(1100, now + 0.45);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.6);
break;
}
}
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"946e9fa9-9fb4-4766-b3c9-c93e5a796d64","tokensIn":2438418,"tokensOut":33915,"tokensTotal":2472333,"cost":0,"turns":76,"toolCalls":75,"failedToolCalls":1,"timestamp":"2026-08-01T06:26:01.958Z"} -->