DOOM
thinkingcap qwen3.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 64,091 bytes · SHA-256 655e483d83ef
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOM-LIKE Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; user-select: none; }
#game-container { position: relative; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; }
#game-canvas { image-rendering: pixelated; background: #000; cursor: crosshair; max-width: 100%; max-height: calc(100% - 80px); }
/* ── css: hud ── */
#hud { position: absolute; bottom: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to top, #1a1a2e, transparent); display: flex; justify-content: space-between; align-items: center; padding: 10px 30px; pointer-events: none; z-index: 10; }
#health-bar { color: #ff4444; font-size: 28px; font-weight: bold; text-shadow: 0 0 10px #ff0000; display: flex; align-items: center; gap: 8px; }
#ammo-display { color: #ffaa00; font-size: 24px; font-weight: bold; text-shadow: 0 0 10px #ff6600; display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
#minimap-container { position: absolute; top: 10px; right: 10px; border: 2px solid #444; z-index: 10; pointer-events: none; }
#minimap { background: rgba(0,0,0,0.7); }
#overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 20; }
#overlay-title { color: #ff4444; font-size: 64px; font-weight: bold; text-shadow: 0 0 30px #ff0000; margin-bottom: 20px; }
#overlay-subtitle { color: #aaa; font-size: 24px; text-align: center; line-height: 1.6; }
#start-screen { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: radial-gradient(ellipse at center, #1a0a0a 0%, #000 70%); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 30; cursor: pointer; }
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="hud">
<div id="health-bar"><span id="health-icon">❤️</span><span id="health-val">100</span></div>
<div id="ammo-display"><span id="weapon-name">PISTOL</span><span id="ammo-val">50</span></div>
<div id="minimap-container"><canvas id="minimap"></canvas></div>
</div>
<div id="overlay" style="display:none;">
<div id="overlay-title"></div>
<div id="overlay-subtitle"></div>
</div><div id="start-screen">
<div style="font-size:48px;color:#ff2200;text-shadow:0 0 30px #ff0000,0 0 60px #aa0000;margin-bottom:10px;font-weight:bold;">SHADOW DUNGEON</div>
<div style="font-size:18px;color:#cc4400;text-shadow:0 0 15px #ff4400;margin-bottom:30px;">A Retro FPS Adventure</div>
<div style="color:#aaa;font-size:16px;line-height:2;text-align:center;">
WASD — Move | Mouse — Look<br>
Click / Space — Shoot | 1/2/3 — Weapons<br>
R — Respawn (when dead) | Enter — Next Level<br><br>
<span style="color:#ff6600;font-size:20px;">Click to Start</span>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./renderer":"src/renderer.ts","./game":"src/game.ts","./hud":"src/hud.ts","./minimap":"src/minimap.ts","./input":"src/input.ts"},"src/maps.ts":{"./types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts","./maps":"src/maps.ts"},"src/raycaster.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./raycaster":"src/raycaster.ts"},"src/game.ts":{"./types":"src/types.ts","./raycaster":"src/raycaster.ts","./input":"src/input.ts","./sound":"src/sound.ts"},"src/minimap.ts":{"./types":"src/types.ts"},"src/hud.ts":{"./types":"src/types.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";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const renderer_1 = require("./renderer");
const game_1 = require("./game");
const hud_1 = require("./hud");
const minimap_1 = require("./minimap");
const input_1 = require("./input");
let state = (0, state_1.initGameState)(0);
let lastTime = performance.now();
let gameStarted = false;
// Start screen handling
const startScreen = document.getElementById('start-screen');
function startGame() {
if (!gameStarted) {
gameStarted = true;
startScreen.style.display = 'none';
(0, input_1.requestPointerLock)();
}
}
startScreen.addEventListener('click', startGame);
window.addEventListener('keydown', e => {
if (e.code === 'Enter' || e.code === 'Space')
startGame();
});
// Handle game-over actions
function handleOverlayInput() {
if (!state.player.alive && !state.won) {
if (input_1.keys['KeyR']) {
(0, game_1.respawnPlayer)(state);
}
}
else if (state.won) {
if (input_1.keys['Enter'] || input_1.keys['NumpadEnter']) {
Promise.resolve().then(() => __importStar(require('./state'))).then(mod => {
const newState = mod.initGameState(state.level + 1);
Object.assign(state, newState);
});
}
}
}
function gameLoop(now) {
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
if (gameStarted) {
handleOverlayInput();
(0, game_1.update)(state, dt);
(0, renderer_1.render)(state);
(0, hud_1.updateHUD)(state);
(0, minimap_1.renderMinimap)(state);
}
else {
// Render a dark scene behind the start screen
(0, renderer_1.render)(state);
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Core types for the FPS game
Object.defineProperty(exports, "__esModule", { value: true });
exports.ENEMY_DEFS = exports.EnemyType = exports.WEAPONS = exports.WeaponType = exports.WALL_COLORS = exports.MAP_H = exports.MAP_W = exports.ROT_SPEED = exports.MOVE_SPEED = exports.FOV = exports.TILE = exports.CANVAS_H = exports.CANVAS_W = void 0;
exports.CANVAS_W = 640;
exports.CANVAS_H = 360;
exports.TILE = 1; // tile size in world units
exports.FOV = Math.PI / 3;
exports.MOVE_SPEED = 3.5;
exports.ROT_SPEED = 2.8;
// Map constants
exports.MAP_W = 24;
exports.MAP_H = 24;
// Wall colors by type (r, g, b)
exports.WALL_COLORS = [
[100, 100, 100], // 0: unused
[139, 69, 19], // 1: brown brick
[80, 80, 80], // 2: gray stone
[101, 67, 33], // 3: dark wood
[50, 50, 80], // 4: blue stone
[80, 50, 50], // 5: red brick
];
var WeaponType;
(function (WeaponType) {
WeaponType[WeaponType["PISTOL"] = 0] = "PISTOL";
WeaponType[WeaponType["SHOTGUN"] = 1] = "SHOTGUN";
WeaponType[WeaponType["PLASMA"] = 2] = "PLASMA";
})(WeaponType || (exports.WeaponType = WeaponType = {}));
exports.WEAPONS = [
{ name: "PISTOL", damage: 15, ammoCost: 1, fireRate: 400, spread: 0, pellets: 1, range: 20 },
{ name: "SHOTGUN", damage: 10, ammoCost: 2, fireRate: 800, spread: 0.15, pellets: 6, range: 12 },
{ name: "PLASMA", damage: 25, ammoCost: 3, fireRate: 150, spread: 0.04, pellets: 1, range: 18 },
];
var EnemyType;
(function (EnemyType) {
EnemyType[EnemyType["GRUNT"] = 0] = "GRUNT";
EnemyType[EnemyType["SOLDIER"] = 1] = "SOLDIER";
EnemyType[EnemyType["DEMON"] = 2] = "DEMON";
})(EnemyType || (exports.EnemyType = EnemyType = {}));
exports.ENEMY_DEFS = {
[EnemyType.GRUNT]: { health: 40, speed: 1.5, damage: 8, attackRange: 2.5, attackRate: 1500, color: [100, 160, 100] },
[EnemyType.SOLDIER]: { health: 70, speed: 1.2, damage: 15, attackRange: 8, attackRate: 2000, color: [160, 100, 100] },
[EnemyType.DEMON]: { health: 120, speed: 2.5, damage: 25, attackRange: 2, attackRate: 800, color: [180, 60, 60] },
};
};
// ── module: src/maps.ts ──
__mods["src/maps.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NUM_LEVELS = void 0;
exports.getLevel = getLevel;
// Map tiles: 0=empty, 1-5=wall types
const LEVELS = [
// Level 1 - The Dungeon
[
[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, 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, 2, 2, 2, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 1],
[1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1],
[1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 5, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 1],
[1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1],
[1, 0, 0, 3, 0, 0, 0, 0, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 5, 5, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 2, 2, 2, 0, 0, 4, 4, 0, 4, 4, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 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, 3, 3, 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
],
// Level 2 - The Fortress
[
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5],
[5, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 5],
[5, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 5],
[5, 0, 0, 0, 0, 4, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 0, 1, 1, 1, 0, 0, 5],
[5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 5],
[5, 4, 4, 4, 4, 4, 0, 0, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 5],
[5, 4, 0, 0, 0, 4, 0, 0, 3, 0, 0, 0, 3, 0, 0, 2, 2, 0, 1, 0, 1, 0, 0, 5],
[5, 4, 0, 0, 0, 4, 0, 0, 3, 0, 0, 0, 3, 0, 0, 2, 0, 0, 1, 1, 1, 0, 0, 5],
[5, 4, 0, 0, 0, 4, 0, 0, 3, 0, 0, 0, 3, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 5],
[5, 4, 4, 4, 4, 4, 0, 0, 3, 3, 0, 3, 3, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 5],
[5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 0, 5],
[5, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 4, 0, 5],
[5, 0, 0, 2, 0, 0, 2, 0, 0, 1, 1, 1, 1, 1, 0, 0, 3, 0, 4, 0, 0, 4, 0, 5],
[5, 0, 0, 2, 0, 0, 2, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3, 0, 4, 0, 0, 4, 0, 5],
[5, 0, 0, 2, 0, 0, 2, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3, 0, 4, 4, 4, 4, 0, 5],
[5, 0, 0, 2, 2, 2, 2, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3, 0, 0, 0, 0, 0, 0, 5],
[5, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 3, 0, 0, 2, 2, 2, 0, 5],
[5, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 2, 0, 2, 0, 5],
[5, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 2, 0, 2, 0, 5],
[5, 4, 0, 0, 0, 4, 0, 0, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 5],
[5, 4, 4, 4, 4, 4, 0, 0, 5, 0, 0, 0, 5, 0, 0, 3, 3, 0, 0, 2, 2, 2, 0, 5],
[5, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5],
[5, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5],
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5],
],
];
function getLevel(index) {
return LEVELS[index % LEVELS.length];
}
exports.NUM_LEVELS = LEVELS.length;
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initGameState = initGameState;
const types_1 = require("./types");
const maps_1 = require("./maps");
let nextId = 0;
function createPlayer() {
return { pos: { x: 1.5, y: 1.5 }, angle: 0, health: 100, maxHealth: 100, alive: true };
}
const ENEMY_SPAWNS = [
// Level 1
[
{ type: types_1.EnemyType.GRUNT, x: 5.5, y: 4.5 },
{ type: types_1.EnemyType.GRUNT, x: 10.5, y: 8.5 },
{ type: types_1.EnemyType.SOLDIER, x: 15.5, y: 3.5 },
{ type: types_1.EnemyType.GRUNT, x: 20.5, y: 6.5 },
{ type: types_1.EnemyType.DEMON, x: 8.5, y: 14.5 },
{ type: types_1.EnemyType.SOLDIER, x: 18.5, y: 12.5 },
{ type: types_1.EnemyType.GRUNT, x: 3.5, y: 18.5 },
{ type: types_1.EnemyType.DEMON, x: 14.5, y: 20.5 },
{ type: types_1.EnemyType.SOLDIER, x: 20.5, y: 16.5 },
{ type: types_1.EnemyType.GRUNT, x: 7.5, y: 21.5 },
],
// Level 2
[
{ type: types_1.EnemyType.SOLDIER, x: 4.5, y: 3.5 },
{ type: types_1.EnemyType.DEMON, x: 9.5, y: 6.5 },
{ type: types_1.EnemyType.GRUNT, x: 15.5, y: 2.5 },
{ type: types_1.EnemyType.SOLDIER, x: 18.5, y: 7.5 },
{ type: types_1.EnemyType.DEMON, x: 6.5, y: 13.5 },
{ type: types_1.EnemyType.GRUNT, x: 20.5, y: 11.5 },
{ type: types_1.EnemyType.SOLDIER, x: 14.5, y: 17.5 },
{ type: types_1.EnemyType.DEMON, x: 3.5, y: 20.5 },
{ type: types_1.EnemyType.GRUNT, x: 18.5, y: 21.5 },
{ type: types_1.EnemyType.SOLDIER, x: 10.5, y: 22.5 },
{ type: types_1.EnemyType.DEMON, x: 21.5, y: 4.5 },
{ type: types_1.EnemyType.GRUNT, x: 7.5, y: 19.5 },
],
];
const PICKUP_SPAWNS = [
// Level 1
[
{ x: 3.5, y: 7.5, kind: 'health', amount: 25 },
{ x: 8.5, y: 2.5, kind: 'ammo_pistol', amount: 15 },
{ x: 14.5, y: 10.5, kind: 'ammo_shotgun', amount: 8 },
{ x: 20.5, y: 13.5, kind: 'health', amount: 30 },
{ x: 6.5, y: 16.5, kind: 'ammo_plasma', amount: 10 },
{ x: 17.5, y: 20.5, kind: 'ammo_pistol', amount: 20 },
{ x: 11.5, y: 21.5, kind: 'health', amount: 25 },
],
// Level 2
[
{ x: 3.5, y: 6.5, kind: 'ammo_shotgun', amount: 8 },
{ x: 10.5, y: 4.5, kind: 'health', amount: 30 },
{ x: 17.5, y: 9.5, kind: 'ammo_plasma', amount: 12 },
{ x: 6.5, y: 15.5, kind: 'ammo_pistol', amount: 20 },
{ x: 21.5, y: 14.5, kind: 'health', amount: 25 },
{ x: 13.5, y: 20.5, kind: 'ammo_shotgun', amount: 6 },
{ x: 4.5, y: 21.5, kind: 'ammo_plasma', amount: 8 },
{ x: 19.5, y: 3.5, kind: 'health', amount: 30 },
],
];
function initGameState(level) {
nextId = 0;
const map = (0, maps_1.getLevel)(level);
const spawns = ENEMY_SPAWNS[level % ENEMY_SPAWNS.length];
const enemies = spawns.map(s => ({
id: nextId++,
type: s.type,
pos: { x: s.x, y: s.y },
health: types_1.ENEMY_DEFS[s.type].health,
maxHealth: types_1.ENEMY_DEFS[s.type].health,
alive: true,
lastAttack: 0,
alertDist: 8,
}));
const pickups = PICKUP_SPAWNS[level % PICKUP_SPAWNS.length].map(s => ({
id: nextId++,
pos: { x: s.x, y: s.y },
def: { type: s.kind, amount: s.amount },
collected: false,
}));
return {
map,
player: createPlayer(),
enemies,
pickups,
currentWeapon: types_1.WeaponType.PISTOL,
ammo: [50, 12, 30],
lastShot: 0,
shooting: false,
shootFrame: 0,
damageFlash: 0,
kills: 0,
totalEnemies: enemies.length,
level,
won: false,
};
}
};
// ── module: src/raycaster.ts ──
__mods["src/raycaster.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.castRay = castRay;
exports.isWall = isWall;
exports.hasLineOfSight = hasLineOfSight;
exports.dist = dist;
// Cast a single ray using DDA algorithm
function castRay(state, px, py, angle) {
const map = state.map;
const dirX = Math.cos(angle);
const dirY = Math.sin(angle);
let mapX = Math.floor(px);
let mapY = Math.floor(py);
const deltaDistX = Math.abs(1 / (dirX || 0.0001));
const deltaDistY = Math.abs(1 / (dirY || 0.0001));
let stepX, stepY;
let sideDistX, sideDistY;
if (dirX < 0) {
stepX = -1;
sideDistX = (px - mapX) * deltaDistX;
}
else {
stepX = 1;
sideDistX = (mapX + 1.0 - px) * deltaDistX;
}
if (dirY < 0) {
stepY = -1;
sideDistY = (py - mapY) * deltaDistY;
}
else {
stepY = 1;
sideDistY = (mapY + 1.0 - py) * deltaDistY;
}
let side = 0;
let hit = false;
while (!hit) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapX < 0 || mapX >= state.map[0].length || mapY < 0 || mapY >= state.map.length) {
break;
}
if (map[mapY][mapX] > 0) {
hit = true;
}
}
let perpDist;
let wallX;
if (side === 0) {
perpDist = sideDistX - deltaDistX;
wallX = py + perpDist * dirY;
}
else {
perpDist = sideDistY - deltaDistY;
wallX = px + perpDist * dirX;
}
wallX -= Math.floor(wallX);
const wallType = (mapX >= 0 && mapX < state.map[0].length && mapY >= 0 && mapY < state.map.length)
? map[mapY][mapX] : 1;
return { dist: perpDist, wallType, side, hitX: wallX };
}
// Check if a position is inside a wall
function isWall(map, x, y) {
const mx = Math.floor(x);
const my = Math.floor(y);
if (mx < 0 || mx >= map[0].length || my < 0 || my >= map.length)
return true;
return map[my][mx] > 0;
}
// Check line of sight between two points (simple grid check)
function hasLineOfSight(map, 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 (isWall(map, cx, cy))
return false;
}
return true;
}
// Distance between two points
function dist(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
const types_1 = require("./types");
const types_2 = require("./types");
const raycaster_1 = require("./raycaster");
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d', { alpha: false });
canvas.width = types_1.CANVAS_W;
canvas.height = types_1.CANVAS_H;
// Pre-render wall textures for each type
const TEXTURE_SIZE = 64;
const textures = [];
function generateTextures() {
for (let t = 1; t < types_1.WALL_COLORS.length; t++) {
const imgData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE);
const baseColor = types_1.WALL_COLORS[t];
for (let y = 0; y < TEXTURE_SIZE; y++) {
for (let x = 0; x < TEXTURE_SIZE; x++) {
const idx = (y * TEXTURE_SIZE + x) * 4;
// Base color with noise
let r = baseColor[0] + (Math.random() - 0.5) * 30;
let g = baseColor[1] + (Math.random() - 0.5) * 30;
let b = baseColor[2] + (Math.random() - 0.5) * 30;
// Brick pattern for types 1, 5
if (t === 1 || t === 5) {
const brickH = 8;
const brickW = 16;
const rowOffset = Math.floor(y / brickH) % 2 === 0 ? 0 : brickW / 2;
const inMortarX = (x + rowOffset) % brickW < 1 || x % brickW === 0;
const inMortarY = y % brickH < 1;
if (inMortarX || inMortarY) {
r *= 0.5;
g *= 0.5;
b *= 0.5;
}
else {
// Slight variation per brick
const brickNoise = Math.sin(x * 0.3 + y * 0.1) * 10;
r += brickNoise;
g += brickNoise;
b += brickNoise;
}
}
// Stone pattern for types 2, 4
if (t === 2 || t === 4) {
const stoneNoise = Math.sin(x * 0.5 + y * 0.3) * 15 +
Math.cos(x * 0.2 - y * 0.7) * 10;
r += stoneNoise;
g += stoneNoise;
b += stoneNoise;
}
// Wood pattern for type 3
if (t === 3) {
const grain = Math.sin(y * 0.8 + Math.sin(x * 0.15) * 3) * 12;
r += grain;
g += grain * 0.7;
b += grain * 0.4;
}
imgData.data[idx] = Math.max(0, Math.min(255, r));
imgData.data[idx + 1] = Math.max(0, Math.min(255, g));
imgData.data[idx + 2] = Math.max(0, Math.min(255, b));
imgData.data[idx + 3] = 255;
}
}
textures[t] = imgData;
}
}
generateTextures();
// Z-buffer for sprite clipping
const zBuffer = new Float64Array(types_1.CANVAS_W);
function render(state) {
const player = state.player;
// Ceiling gradient
const ceilGrad = ctx.createLinearGradient(0, 0, 0, types_1.CANVAS_H / 2);
ceilGrad.addColorStop(0, '#0a0a15');
ceilGrad.addColorStop(1, '#1a1a2e');
ctx.fillStyle = ceilGrad;
ctx.fillRect(0, 0, types_1.CANVAS_W, types_1.CANVAS_H / 2);
// Floor gradient
const floorGrad = ctx.createLinearGradient(0, types_1.CANVAS_H / 2, 0, types_1.CANVAS_H);
floorGrad.addColorStop(0, '#1a1a2e');
floorGrad.addColorStop(1, '#3d3530');
ctx.fillStyle = floorGrad;
ctx.fillRect(0, types_1.CANVAS_H / 2, types_1.CANVAS_W, types_1.CANVAS_H / 2);
// Cast rays and draw walls with textures
for (let x = 0; x < types_1.CANVAS_W; x++) {
const rayAngle = player.angle - types_1.FOV / 2 + (x / types_1.CANVAS_W) * types_1.FOV;
const hit = (0, raycaster_1.castRay)(state, player.pos.x, player.pos.y, rayAngle);
// Fix fisheye
const correctedDist = hit.dist * Math.cos(rayAngle - player.angle);
zBuffer[x] = correctedDist;
if (correctedDist < 0.1)
continue;
const wallHeight = Math.min(types_1.CANVAS_H * 2, types_1.CANVAS_H / correctedDist);
const wallTop = (types_1.CANVAS_H - wallHeight) / 2;
// Distance-based shading
let shade = Math.max(0.15, 1 - correctedDist / 16);
if (hit.side === 1)
shade *= 0.7;
// Draw textured wall strip
const tex = textures[hit.wallType];
if (tex && wallHeight > 2) {
for (let y = Math.max(0, Math.floor(wallTop)); y < Math.min(types_1.CANVAS_H, Math.ceil(wallTop + wallHeight)); y++) {
// Texture Y coordinate
const texY = Math.floor(((y - wallTop) / wallHeight) * TEXTURE_SIZE);
const idx = (texY * TEXTURE_SIZE + Math.floor(hit.hitX * TEXTURE_SIZE)) * 4;
if (idx >= 0 && idx < tex.data.length - 3) {
const r = Math.floor(tex.data[idx] * shade);
const g = Math.floor(tex.data[idx + 1] * shade);
const b = Math.floor(tex.data[idx + 2] * shade);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x, y, 1, 1);
}
}
}
else {
// Fallback: solid color with shading
const wc = types_1.WALL_COLORS[hit.wallType] || [100, 100, 100];
ctx.fillStyle = `rgb(${Math.floor(wc[0] * shade)},${Math.floor(wc[1] * shade)},${Math.floor(wc[2] * shade)})`;
ctx.fillRect(x, wallTop, 1, wallHeight);
}
// Wall edge highlights
if (wallHeight > 4) {
const edgeAlpha = shade * 0.15;
ctx.fillStyle = `rgba(255,255,255,${edgeAlpha})`;
ctx.fillRect(x, Math.floor(wallTop), 1, 2);
ctx.fillStyle = `rgba(0,0,0,${shade * 0.4})`;
ctx.fillRect(x, Math.ceil(wallTop + wallHeight) - 2, 1, 2);
}
}
// Render sprites (enemies and pickups)
renderSprites(state);
// Damage flash overlay
if (state.damageFlash > 0) {
ctx.fillStyle = `rgba(255, 0, 0, ${Math.min(0.4, state.damageFlash)})`;
ctx.fillRect(0, 0, types_1.CANVAS_W, types_1.CANVAS_H);
}
// Vignette effect
const vigGrad = ctx.createRadialGradient(types_1.CANVAS_W / 2, types_1.CANVAS_H / 2, types_1.CANVAS_H * 0.3, types_1.CANVAS_W / 2, types_1.CANVAS_H / 2, types_1.CANVAS_H * 0.8);
vigGrad.addColorStop(0, 'rgba(0,0,0,0)');
vigGrad.addColorStop(1, 'rgba(0,0,0,0.5)');
ctx.fillStyle = vigGrad;
ctx.fillRect(0, 0, types_1.CANVAS_W, types_1.CANVAS_H);
// Crosshair
drawCrosshair(ctx, types_1.CANVAS_W / 2, types_1.CANVAS_H / 2);
// Weapon rendering
renderWeapon(state);
}
function drawCrosshair(ctx, cx, cy) {
const size = 8;
const gap = 4;
ctx.strokeStyle = 'rgba(0, 255, 0, 0.7)';
ctx.lineWidth = 1.5;
// 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();
// Center dot
ctx.fillStyle = 'rgba(0, 255, 0, 0.9)';
ctx.fillRect(cx - 1, cy - 1, 2, 2);
}
function renderSprites(state) {
const player = state.player;
const sprites = [];
for (const enemy of state.enemies) {
if (!enemy.alive)
continue;
const d = (0, raycaster_1.dist)(player.pos, enemy.pos);
sprites.push({ pos: enemy.pos, dist: d, data: { type: 'enemy', enemy } });
}
for (const pickup of state.pickups) {
if (pickup.collected)
continue;
const d = (0, raycaster_1.dist)(player.pos, pickup.pos);
sprites.push({ pos: pickup.pos, dist: d, data: { type: 'pickup', pickup } });
}
// Sort back to front
sprites.sort((a, b) => b.dist - a.dist);
for (const sprite of sprites) {
const dx = sprite.pos.x - player.pos.x;
const dy = sprite.pos.y - player.pos.y;
const cosA = Math.cos(-player.angle);
const sinA = Math.sin(-player.angle);
const tx = dx * cosA - dy * sinA;
const ty = dx * sinA + dy * cosA;
if (tx <= 0.1)
continue;
const screenX = types_1.CANVAS_W / 2 * (1 + ty / tx / Math.tan(types_1.FOV / 2));
if (sprite.data.type === 'enemy') {
renderEnemy(state, sprite.data.enemy, sprite.dist, screenX);
}
else {
renderPickup(sprite.data.pickup, sprite.dist, screenX);
}
}
}
function renderEnemy(state, enemy, dist, screenX) {
const def = types_2.ENEMY_DEFS[enemy.type];
const size = Math.min(types_1.CANVAS_H * 1.5, types_1.CANVAS_H / dist);
const spriteLeft = Math.floor(screenX - size / 2);
const spriteRight = Math.floor(screenX + size / 2);
let visible = false;
for (let x = Math.max(0, spriteLeft); x < Math.min(types_1.CANVAS_W, spriteRight); x += 4) {
if (dist < zBuffer[x]) {
visible = true;
break;
}
}
if (!visible)
return;
const topY = types_1.CANVAS_H / 2 - size * 0.35;
let shade = Math.max(0.2, 1 - dist / 14);
const r = Math.floor(def.color[0] * shade);
const g = Math.floor(def.color[1] * shade);
const b = Math.floor(def.color[2] * shade);
// Draw enemy body pixel by pixel for proper z-buffering
for (let x = Math.max(0, spriteLeft); x < Math.min(types_1.CANVAS_W, spriteRight); x++) {
if (dist >= zBuffer[x])
continue;
const nx = (x - screenX) / (size / 2); // normalized: -1 to 1
// Body shape with rounded edges
const bodyWidth = 0.5 * Math.cos(nx * Math.PI / 2);
if (Math.abs(nx) > bodyWidth + 0.1)
continue;
// Shadow underneath
ctx.fillStyle = `rgba(0,0,0,${shade * 0.3})`;
ctx.fillRect(x - 1, topY + size * 0.92, 3, size * 0.08);
// Legs
if (Math.abs(nx) < 0.35) {
const legColor = `rgb(${Math.floor(r * 0.6)},${Math.floor(g * 0.6)},${Math.floor(b * 0.6)})`;
ctx.fillStyle = legColor;
ctx.fillRect(x, topY + size * 0.72, 1, size * 0.2);
}
// Body
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x, topY + size * 0.15, 1, size * 0.6);
// Body highlight
if (Math.abs(nx) < 0.2) {
ctx.fillStyle = `rgba(255,255,255,${shade * 0.1})`;
ctx.fillRect(x, topY + size * 0.2, 1, size * 0.4);
}
// Head
if (Math.abs(nx) < 0.38) {
const headR = Math.min(255, r + 30);
const headG = Math.min(255, g + 30);
const headB = Math.min(255, b + 30);
ctx.fillStyle = `rgb(${headR},${headG},${headB})`;
ctx.fillRect(x, topY - size * 0.02, 1, size * 0.22);
}
// Eyes (menacing red glow)
if ((Math.abs(nx - 0.12) < 0.06 || Math.abs(nx + 0.12) < 0.06)) {
ctx.fillStyle = `rgb(255,${Math.floor(30 * shade)},0)`;
ctx.fillRect(x, topY + size * 0.04, 1, size * 0.07);
}
// Mouth (for demons)
if (enemy.type === 2 && Math.abs(nx) < 0.15) {
ctx.fillStyle = `rgb(${Math.floor(r * 0.3)},${Math.floor(g * 0.3)},${Math.floor(b * 0.3)})`;
ctx.fillRect(x, topY + size * 0.14, 1, size * 0.03);
}
}
// Health bar above enemy
if (enemy.health < enemy.maxHealth && dist < 8) {
const barW = Math.floor(size * 0.6);
const barH = 4;
const barX = Math.floor(screenX - barW / 2);
const barY = Math.floor(topY - 12);
ctx.fillStyle = '#333';
ctx.fillRect(barX, barY, barW, barH);
const hpPct = enemy.health / enemy.maxHealth;
ctx.fillStyle = hpPct > 0.5 ? '#4f4' :
hpPct > 0.25 ? '#ff4' : '#f44';
ctx.fillRect(barX, barY, Math.floor(barW * hpPct), barH);
// Health bar border
ctx.strokeStyle = '#666';
ctx.lineWidth = 1;
ctx.strokeRect(barX, barY, barW, barH);
}
}
function renderPickup(pickup, dist, screenX) {
const size = Math.min(types_1.CANVAS_H * 0.5, types_1.CANVAS_H / dist);
const spriteLeft = Math.floor(screenX - size / 2);
const spriteRight = Math.floor(screenX + size / 2);
let visible = false;
for (let x = Math.max(0, spriteLeft); x < Math.min(types_1.CANVAS_W, spriteRight); x += 4) {
if (dist < zBuffer[x]) {
visible = true;
break;
}
}
if (!visible)
return;
const shade = Math.max(0.3, 1 - dist / 12);
let color;
switch (pickup.def.type) {
case 'health':
color = `rgb(${Math.floor(80 * shade)},${Math.floor(255 * shade)},${Math.floor(80 * shade)})`;
break;
case 'ammo_pistol':
color = `rgb(${Math.floor(255 * shade)},${Math.floor(200 * shade)},${Math.floor(50 * shade)})`;
break;
case 'ammo_shotgun':
color = `rgb(${Math.floor(255 * shade)},${Math.floor(120 * shade)},${Math.floor(30 * shade)})`;
break;
case 'ammo_plasma':
color = `rgb(${Math.floor(80 * shade)},${Math.floor(150 * shade)},${Math.floor(255 * shade)})`;
break;
default: color = '#fff';
}
const bobY = Math.sin(Date.now() / 300) * size * 0.08;
const topY = types_1.CANVAS_H / 2 + size * 0.15 + bobY;
for (let x = Math.max(0, spriteLeft); x < Math.min(types_1.CANVAS_W, spriteRight); x++) {
if (dist >= zBuffer[x])
continue;
const nx = (x - screenX) / (size / 2);
if (Math.abs(nx) > 0.45)
continue;
// Glow underneath
ctx.fillStyle = `rgba(255,255,255,${shade * 0.1})`;
ctx.fillRect(x - 1, topY + size * 0.3, 3, size * 0.08);
// Main pickup body (diamond shape)
const diamondW = 0.4 * (1 - Math.abs((nx)));
if (Math.abs(nx) < 0.45) {
ctx.fillStyle = color;
ctx.fillRect(x, topY - size * 0.05, 1, size * 0.3);
// Inner highlight
if (Math.abs(nx) < 0.25) {
ctx.fillStyle = `rgba(255,255,255,${shade * 0.4})`;
ctx.fillRect(x, topY + size * 0.05, 1, size * 0.1);
}
}
// Cross for health pickups
if (pickup.def.type === 'health' && Math.abs(nx) < 0.12) {
ctx.fillStyle = `rgba(255,255,255,${shade * 0.8})`;
ctx.fillRect(x, topY + size * 0.05, 1, size * 0.15);
}
}
}
function renderWeapon(state) {
const weaponIdx = state.currentWeapon;
const bobX = Math.sin(Date.now() / 200) * 3;
const bobY = Math.abs(Math.cos(Date.now() / 200)) * 4;
let shootOffset = 0;
if (state.shootFrame > 0) {
shootOffset = state.shootFrame * 15;
}
const baseX = types_1.CANVAS_W / 2 + bobX;
const baseY = types_1.CANVAS_H - 5 + bobY;
ctx.save();
switch (weaponIdx) {
case 0:
drawPistol(ctx, baseX, baseY - shootOffset);
break;
case 1:
drawShotgun(ctx, baseX, baseY - shootOffset);
break;
case 2:
drawPlasmaGun(ctx, baseX, baseY - shootOffset);
break;
}
ctx.restore();
// Muzzle flash
if (state.shootFrame > 0) {
const flashSize = state.shootFrame * 40;
const gradient = ctx.createRadialGradient(types_1.CANVAS_W / 2, types_1.CANVAS_H * 0.52, 0, types_1.CANVAS_W / 2, types_1.CANVAS_H * 0.52, flashSize);
gradient.addColorStop(0, `rgba(255,255,220,${state.shootFrame * 0.9})`);
gradient.addColorStop(0.3, `rgba(255,180,50,${state.shootFrame * 0.6})`);
gradient.addColorStop(1, 'rgba(255,80,0,0)');
ctx.fillStyle = gradient;
ctx.fillRect(types_1.CANVAS_W / 2 - flashSize, types_1.CANVAS_H * 0.52 - flashSize, flashSize * 2, flashSize * 2);
}
}
function drawPistol(ctx, x, y) {
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x - 8, y - 75, 16, 4);
// Barrel
ctx.fillStyle = '#555';
ctx.fillRect(x - 7, y - 82, 14, 52);
ctx.fillStyle = '#666';
ctx.fillRect(x - 5, y - 80, 10, 48);
ctx.fillStyle = '#777';
ctx.fillRect(x - 3, y - 78, 6, 44);
// Barrel tip
ctx.fillStyle = '#222';
ctx.fillRect(x - 5, y - 84, 10, 4);
// Handle
ctx.fillStyle = '#6b4226';
ctx.fillRect(x - 9, y - 32, 18, 38);
ctx.fillStyle = '#7d5230';
ctx.fillRect(x - 7, y - 30, 14, 34);
// Trigger guard
ctx.strokeStyle = '#555';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y - 26, 9, 0.1 * Math.PI, 0.9 * Math.PI);
ctx.stroke();
}
function drawShotgun(ctx, x, y) {
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x - 16, y - 85, 32, 4);
// Double barrel
ctx.fillStyle = '#4a4a4a';
ctx.fillRect(x - 15, y - 92, 11, 62);
ctx.fillRect(x + 4, y - 92, 11, 62);
// Barrel highlights
ctx.fillStyle = '#666';
ctx.fillRect(x - 13, y - 90, 7, 58);
ctx.fillRect(x + 6, y - 90, 7, 58);
// Barrel tips
ctx.fillStyle = '#222';
ctx.fillRect(x - 14, y - 94, 9, 3);
ctx.fillRect(x + 5, y - 94, 9, 3);
// Body
ctx.fillStyle = '#5a3820';
ctx.fillRect(x - 17, y - 36, 34, 42);
ctx.fillStyle = '#6b4830';
ctx.fillRect(x - 15, y - 34, 30, 38);
// Pump grip
ctx.fillStyle = '#4a2810';
ctx.fillRect(x - 19, y - 52, 38, 10);
ctx.fillStyle = '#5a3820';
ctx.fillRect(x - 17, y - 50, 34, 6);
// Stock
ctx.fillStyle = '#6b4830';
ctx.fillRect(x - 11, y - 4, 22, 16);
}
function drawPlasmaGun(ctx, x, y) {
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(x - 14, y - 70, 28, 4);
// Main body
ctx.fillStyle = '#3a5a8a';
ctx.fillRect(x - 13, y - 77, 26, 52);
ctx.fillStyle = '#4a6a9a';
ctx.fillRect(x - 11, y - 75, 22, 48);
// Energy core (pulsing glow)
const pulse = Math.sin(Date.now() / 120) * 0.3 + 0.7;
ctx.fillStyle = `rgba(0,${Math.floor(180 * pulse)},255,${pulse})`;
ctx.fillRect(x - 7, y - 72, 14, 22);
// Core bright center
ctx.fillStyle = `rgba(150,220,255,${pulse * 0.8})`;
ctx.fillRect(x - 3, y - 68, 6, 14);
// Barrel tip
ctx.fillStyle = '#5a7aaa';
ctx.fillRect(x - 9, y - 87, 18, 12);
ctx.fillStyle = '#2a3a5a';
ctx.fillRect(x - 6, y - 89, 12, 4);
// Side grips
ctx.fillStyle = '#4a6a9a';
ctx.fillRect(x - 19, y - 52, 7, 28);
ctx.fillRect(x + 12, y - 52, 7, 28);
// Handle
ctx.fillStyle = '#3a4a6a';
ctx.fillRect(x - 9, y - 28, 18, 30);
ctx.fillStyle = '#4a5a7a';
ctx.fillRect(x - 7, y - 26, 14, 26);
// Energy lines on body
ctx.strokeStyle = `rgba(0,${Math.floor(200 * pulse)},255,${pulse * 0.5})`;
ctx.lineWidth = 1;
for (let i = 0; i < 3; i++) {
const ly = y - 68 + i * 8;
ctx.beginPath();
ctx.moveTo(x - 9, ly);
ctx.lineTo(x + 9, ly);
ctx.stroke();
}
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.keys = void 0;
exports.requestPointerLock = requestPointerLock;
exports.getInput = getInput;
exports.keys = {};
let mouseDX = 0;
let mouseDown = false;
let mouseClicked = false;
window.addEventListener('keydown', e => {
exports.keys[e.code] = true;
// Prevent scrolling with space/arrows/WASD
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
e.preventDefault();
}
});
window.addEventListener('keyup', e => {
exports.keys[e.code] = false;
});
canvasEl().addEventListener('mousedown', () => { mouseDown = true; mouseClicked = true; });
window.addEventListener('mouseup', () => { mouseDown = false; });
function canvasEl() { return document.getElementById('game-canvas'); }
// Pointer lock for mouse look
function requestPointerLock() {
const c = canvasEl();
if (c && !document.pointerLockElement) {
c.requestPointerLock?.();
}
}
window.addEventListener('mousemove', e => {
if (document.pointerLockElement) {
mouseDX += e.movementX;
}
});
function getInput() {
const result = { dx: mouseDX, down: mouseDown, clicked: mouseClicked };
mouseDX = 0;
mouseClicked = false;
return result;
}
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.update = update;
exports.respawnPlayer = respawnPlayer;
exports.nextLevel = nextLevel;
const types_1 = require("./types");
const types_2 = require("./types");
const raycaster_1 = require("./raycaster");
const input_1 = require("./input");
const sound_1 = require("./sound");
const PLAYER_RADIUS = 0.25;
function update(state, dt) {
if (state.player.alive && !state.won) {
handlePlayerInput(state, dt);
updateEnemies(state, dt);
checkPickups(state);
}
// Decay effects
if (state.shootFrame > 0)
state.shootFrame -= dt * 5;
if (state.damageFlash > 0)
state.damageFlash -= dt * 3;
}
function handlePlayerInput(state, dt) {
const player = state.player;
// Rotation from mouse
const input = (0, input_1.getInput)();
player.angle += input.dx * 0.002;
if (input.clicked || input_1.keys['Space']) {
shootWeapon(state);
}
// Weapon switching
if (input_1.keys['Digit1'] || input_1.keys['Numpad1'])
state.currentWeapon = types_1.WeaponType.PISTOL;
if (input_1.keys['Digit2'] || input_1.keys['Numpad2'])
state.currentWeapon = types_1.WeaponType.SHOTGUN;
if (input_1.keys['Digit3'] || input_1.keys['Numpad3'])
state.currentWeapon = types_1.WeaponType.PLASMA;
// Movement
let moveX = 0, moveY = 0;
const speed = types_1.MOVE_SPEED * dt;
if (input_1.keys['KeyW'] || input_1.keys['ArrowUp']) {
moveX += Math.cos(player.angle) * speed;
moveY += Math.sin(player.angle) * speed;
}
if (input_1.keys['KeyS'] || input_1.keys['ArrowDown']) {
moveX -= Math.cos(player.angle) * speed;
moveY -= Math.sin(player.angle) * speed;
}
if (input_1.keys['KeyA'] || input_1.keys['ArrowLeft']) {
// Strafe left
moveX += Math.cos(player.angle - Math.PI / 2) * speed;
moveY += Math.sin(player.angle - Math.PI / 2) * speed;
}
if (input_1.keys['KeyD'] || input_1.keys['ArrowRight']) {
// Strafe right
moveX += Math.cos(player.angle + Math.PI / 2) * speed;
moveY += Math.sin(player.angle + Math.PI / 2) * speed;
}
// Keyboard rotation fallback
if (input_1.keys['KeyQ'] || input_1.keys['Numpad4'])
player.angle -= types_1.ROT_SPEED * dt;
if (input_1.keys['KeyE'] || input_1.keys['Numpad6'])
player.angle += types_1.ROT_SPEED * dt;
// Collision detection with sliding
const newX = player.pos.x + moveX;
const newY = player.pos.y + moveY;
if (!(0, raycaster_1.isWall)(state.map, newX, player.pos.y)) {
player.pos.x = newX;
}
if (!(0, raycaster_1.isWall)(state.map, player.pos.x, newY)) {
player.pos.y = newY;
}
// Extra margin check
const marginX = Math.cos(player.angle) * PLAYER_RADIUS;
const marginY = Math.sin(player.angle) * PLAYER_RADIUS;
if ((0, raycaster_1.isWall)(state.map, player.pos.x + marginX, player.pos.y + marginY)) {
player.pos.x -= moveX;
player.pos.y -= moveY;
}
}
function shootWeapon(state) {
const now = Date.now();
const weaponIdx = state.currentWeapon;
// Check ammo
if (state.ammo[weaponIdx] <= 0)
return;
// Check fire rate
const fireRate = [400, 800, 150][weaponIdx];
if (now - state.lastShot < fireRate)
return;
state.lastShot = now;
state.ammo[weaponIdx]--;
state.shooting = true;
state.shootFrame = 1.0;
(0, sound_1.playShoot)(weaponIdx);
const pellets = [1, 6, 1][weaponIdx];
const spread = [0, 0.15, 0.04][weaponIdx];
const damage = [15, 10, 25][weaponIdx];
const range = [20, 12, 18][weaponIdx];
for (let p = 0; p < pellets; p++) {
const shotAngle = state.player.angle + (Math.random() - 0.5) * spread;
// Check which enemies are hit
let closestHit = null;
for (const enemy of state.enemies) {
if (!enemy.alive)
continue;
const d = (0, raycaster_1.dist)(state.player.pos, enemy.pos);
if (d > range)
continue;
// Check angle to enemy
const dx = enemy.pos.x - state.player.pos.x;
const dy = enemy.pos.y - state.player.pos.y;
let angleToEnemy = Math.atan2(dy, dx) - shotAngle;
// Normalize angle
while (angleToEnemy > Math.PI)
angleToEnemy -= 2 * Math.PI;
while (angleToEnemy < -Math.PI)
angleToEnemy += 2 * Math.PI;
const hitWidth = Math.atan2(0.4, d); // enemy hit radius
if (Math.abs(angleToEnemy) < hitWidth && (0, raycaster_1.hasLineOfSight)(state.map, state.player.pos.x, state.player.pos.y, enemy.pos.x, enemy.pos.y)) {
if (!closestHit || d < closestHit.dist) {
closestHit = { enemy, dist: d };
}
}
}
if (closestHit) {
const dmg = damage * (0.8 + Math.random() * 0.4); // slight variance
closestHit.enemy.health -= dmg;
if (closestHit.enemy.health <= 0) {
closestHit.enemy.alive = false;
(0, sound_1.playEnemyDeath)();
state.kills++;
// Check win condition
const aliveCount = state.enemies.filter(e => e.alive).length;
if (aliveCount === 0) {
state.won = true;
}
}
}
}
}
function updateEnemies(state, dt) {
const now = Date.now();
for (const enemy of state.enemies) {
if (!enemy.alive)
continue;
const def = types_2.ENEMY_DEFS[enemy.type];
const playerPos = state.player.pos;
const d = (0, raycaster_1.dist)(playerPos, enemy.pos);
// Alert if close enough or can see player
const alertRange = enemy.type === types_1.EnemyType.DEMON ? 14 : enemy.alertDist;
if (d < alertRange || (0, raycaster_1.hasLineOfSight)(state.map, playerPos.x, playerPos.y, enemy.pos.x, enemy.pos.y)) {
// Move toward player with slight wobble for natural movement
const dx = playerPos.x - enemy.pos.x;
const dy = playerPos.y - enemy.pos.y;
let angle = Math.atan2(dy, dx);
// Add slight random wobble to movement direction
if (d > 1.5) {
angle += (Math.sin(Date.now() / 300 + enemy.id * 7) * 0.3);
}
let moveX = Math.cos(angle) * def.speed * dt;
let moveY = Math.sin(angle) * def.speed * dt;
// Collision with walls
const newX = enemy.pos.x + moveX;
const newY = enemy.pos.y + moveY;
if (!(0, raycaster_1.isWall)(state.map, newX, enemy.pos.y)) {
enemy.pos.x = newX;
}
else {
// Try sliding
if (!(0, raycaster_1.isWall)(state.map, newX, enemy.pos.y - 0.3)) {
enemy.pos.x = newX;
}
else if (!(0, raycaster_1.isWall)(state.map, newX, enemy.pos.y + 0.3)) {
enemy.pos.x = newX;
}
}
if (!(0, raycaster_1.isWall)(state.map, enemy.pos.x, newY)) {
enemy.pos.y = newY;
}
else {
// Try sliding
if (!(0, raycaster_1.isWall)(state.map, enemy.pos.x - 0.3, newY)) {
enemy.pos.y = newY;
}
else if (!(0, raycaster_1.isWall)(state.map, enemy.pos.x + 0.3, newY)) {
enemy.pos.y = newY;
}
}
// Attack player
if (d < def.attackRange && now - enemy.lastAttack > def.attackRate) {
enemy.lastAttack = now;
if (state.player.alive) {
(0, sound_1.playHit)();
state.player.health -= def.damage;
state.damageFlash = 1.0;
if (state.player.health <= 0) {
state.player.health = 0;
state.player.alive = false;
(0, sound_1.playDeath)();
}
}
}
}
}
}
function checkPickups(state) {
const playerPos = state.player.pos;
for (const pickup of state.pickups) {
if (pickup.collected)
continue;
const d = (0, raycaster_1.dist)(playerPos, pickup.pos);
if (d > 0.6)
continue;
pickup.collected = true;
(0, sound_1.playPickup)();
switch (pickup.def.type) {
case 'health':
state.player.health = Math.min(state.player.maxHealth, state.player.health + pickup.def.amount);
break;
case 'ammo_pistol':
state.ammo[types_1.WeaponType.PISTOL] += pickup.def.amount;
break;
case 'ammo_shotgun':
state.ammo[types_1.WeaponType.SHOTGUN] += pickup.def.amount;
break;
case 'ammo_plasma':
state.ammo[types_1.WeaponType.PLASMA] += pickup.def.amount;
break;
}
}
}
function respawnPlayer(state) {
state.player.health = state.player.maxHealth;
state.player.alive = true;
state.player.pos.x = 1.5;
state.player.pos.y = 1.5;
state.player.angle = 0;
state.damageFlash = 0;
}
function nextLevel(state) {
const newLevel = state.level + 1;
// Reload level data
Promise.resolve().then(() => __importStar(require('./state'))).then(mod => {
const newState = mod.initGameState(newLevel);
Object.assign(state, newState);
});
}
};
// ── module: src/minimap.ts ──
__mods["src/minimap.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderMinimap = renderMinimap;
const types_1 = require("./types");
const minimapCanvas = document.getElementById('minimap');
const mctx = minimapCanvas.getContext('2d', { alpha: false });
minimapCanvas.width = 120;
minimapCanvas.height = 120;
function renderMinimap(state) {
const map = state.map;
const mw = map[0].length;
const mh = map.length;
const scale = Math.min(120 / mw, 120 / mh);
mctx.fillStyle = '#111';
mctx.fillRect(0, 0, 120, 120);
// Draw walls
for (let y = 0; y < mh; y++) {
for (let x = 0; x < mw; x++) {
if (map[y][x] > 0) {
mctx.fillStyle = '#555';
mctx.fillRect(x * scale, y * scale, scale + 0.5, scale + 0.5);
}
}
}
// Draw pickups
for (const pickup of state.pickups) {
if (pickup.collected)
continue;
let color = '#fff';
switch (pickup.def.type) {
case 'health':
color = '#0f0';
break;
case 'ammo_pistol':
color = '#ff0';
break;
case 'ammo_shotgun':
color = '#f80';
break;
case 'ammo_plasma':
color = '#48f';
break;
}
mctx.fillStyle = color;
mctx.fillRect(pickup.pos.x * scale - 1, pickup.pos.y * scale - 1, 3, 3);
}
// Draw enemies
for (const enemy of state.enemies) {
if (!enemy.alive)
continue;
const def = types_1.ENEMY_DEFS[enemy.type];
mctx.fillStyle = `rgb(${def.color[0]},${def.color[1]},${def.color[2]})`;
mctx.fillRect(enemy.pos.x * scale - 2, enemy.pos.y * scale - 2, 4, 4);
}
// Draw player
const px = state.player.pos.x * scale;
const py = state.player.pos.y * scale;
mctx.fillStyle = '#0f0';
mctx.beginPath();
mctx.arc(px, py, 3, 0, Math.PI * 2);
mctx.fill();
// Player direction indicator
mctx.strokeStyle = '#0f0';
mctx.lineWidth = 1;
mctx.beginPath();
mctx.moveTo(px, py);
mctx.lineTo(px + Math.cos(state.player.angle) * 8, py + Math.sin(state.player.angle) * 8);
mctx.stroke();
}
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateHUD = updateHUD;
const types_1 = require("./types");
const healthVal = document.getElementById('health-val');
const weaponName = document.getElementById('weapon-name');
const ammoVal = document.getElementById('ammo-val');
// Create kills display if not exists
let killsDisplay = null;
if (!document.getElementById('kills-display')) {
const el = document.createElement('span');
el.id = 'kills-display';
el.style.cssText = 'color:#ff6600;font-size:14px;text-shadow:0 0 8px #ff3300;position:absolute;top:10px;left:10px;z-index:10;pointer-events:none;';
document.getElementById('game-container').appendChild(el);
killsDisplay = el;
}
else {
killsDisplay = document.getElementById('kills-display');
}
const overlay = document.getElementById('overlay');
const overlayTitle = document.getElementById('overlay-title');
const overlaySubtitle = document.getElementById('overlay-subtitle');
function updateHUD(state) {
healthVal.textContent = Math.max(0, state.player.health).toString();
const weaponDef = types_1.WEAPONS[state.currentWeapon];
weaponName.textContent = weaponDef.name;
ammoVal.textContent = state.ammo[state.currentWeapon].toString();
// Update kills display
if (killsDisplay) {
const remaining = state.enemies.filter(e => e.alive).length;
killsDisplay.textContent = `ENEMIES: ${remaining}`;
}
// Health bar color
const hpPct = state.player.health / state.player.maxHealth;
if (hpPct > 0.5) {
healthVal.style.color = '#ff4444';
}
else if (hpPct > 0.25) {
healthVal.style.color = '#ff8800';
}
else {
healthVal.style.color = '#ff0000';
}
// Show/hide overlay based on game state
if (!state.player.alive && !state.won) {
overlay.style.display = 'flex';
overlayTitle.textContent = 'YOU DIED';
overlaySubtitle.innerHTML = `Kills: ${state.kills} / ${state.totalEnemies}<br><br>Press R to respawn`;
}
else if (state.won) {
overlay.style.display = 'flex';
overlayTitle.textContent = 'LEVEL COMPLETE!';
overlayTitle.style.color = '#44ff44';
overlaySubtitle.innerHTML = `Kills: ${state.kills} / ${state.totalEnemies}<br><br>Press ENTER for next level`;
}
else {
overlay.style.display = 'none';
}
}
};
// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.playShoot = playShoot;
exports.playHit = playHit;
exports.playPickup = playPickup;
exports.playDeath = playDeath;
exports.playEnemyDeath = playEnemyDeath;
// Simple synthesized sound effects using Web Audio API
let audioCtx = null;
function getAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
return audioCtx;
}
function playShoot(weaponIdx) {
try {
const ctx = getAudio();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
switch (weaponIdx) {
case 0: // Pistol - sharp crack
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(50, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.15);
break;
case 1: // Shotgun - deep boom
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(80, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(20, ctx.currentTime + 0.3);
gain.gain.setValueAtTime(0.4, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.35);
// Add noise burst
const bufferSize = ctx.sampleRate * 0.2;
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) * Math.exp(-i / (bufferSize * 0.1));
}
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const nGain = ctx.createGain();
nGain.gain.setValueAtTime(0.3, ctx.currentTime);
nGain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
noise.connect(nGain);
nGain.connect(ctx.destination);
noise.start(ctx.currentTime);
break;
case 2: // Plasma - electric zap
osc.type = 'square';
osc.frequency.setValueAtTime(800, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.2, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.18);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.18);
break;
}
}
catch (e) { /* Audio not available */ }
}
function playHit() {
try {
const ctx = getAudio();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.2, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.12);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.12);
}
catch (e) { /* Audio not available */ }
}
function playPickup() {
try {
const ctx = getAudio();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(400, ctx.currentTime);
osc.frequency.linearRampToValueAtTime(800, ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.2, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.2);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.2);
}
catch (e) { /* Audio not available */ }
}
function playDeath() {
try {
const ctx = getAudio();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(400, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(30, ctx.currentTime + 1.0);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.2);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 1.2);
}
catch (e) { /* Audio not available */ }
}
function playEnemyDeath() {
try {
const ctx = getAudio();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'square';
osc.frequency.setValueAtTime(500, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, ctx.currentTime + 0.4);
gain.gain.setValueAtTime(0.25, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.45);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.45);
}
catch (e) { /* Audio not available */ }
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"d6295fc8-58d0-4f10-afe6-2808d209c8a2","tokensIn":1576901,"tokensOut":32988,"tokensTotal":1609889,"turns":54,"toolCalls":53,"failedToolCalls":3,"timestamp":"2026-07-31T18:39:21.372Z"} -->