Add browser-based Doom clone with raycasting engine

Built a complete FPS game playable in the browser using vanilla JS and
HTML5 Canvas. Features a raycasting renderer with 8 textured wall types,
3 weapons (pistol, shotgun, plasma gun), 3 enemy types with AI
(imp, demon, baron), 3 levels of increasing difficulty, HUD with
minimap, pickups, doors, explosive barrels, and procedural audio.
Includes GitHub Actions workflow for GitHub Pages deployment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 21:27:43 +00:00
co-authored by Claude Opus 4.6
commit c55d7a7475
15 changed files with 3123 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
// Player class
class Player {
constructor() {
this.x = 2;
this.y = 2;
this.angle = 0;
this.health = 100;
this.maxHealth = 100;
this.armor = 0;
this.maxArmor = 100;
this.ammo = 50;
this.maxAmmo = 200;
this.kills = 0;
this.totalKills = 0;
this.score = 0;
this.weaponSystem = new WeaponSystem();
this.currentMap = null;
// Input state
this.keys = {};
this.mouseDown = false;
this.mouseDX = 0;
// Head bob
this.bobPhase = 0;
this.isMoving = false;
}
reset(startPos) {
this.x = startPos.x;
this.y = startPos.y;
this.angle = startPos.angle;
this.health = 100;
this.armor = 0;
this.ammo = 50;
this.kills = 0;
this.weaponSystem = new WeaponSystem();
}
fullReset() {
this.health = 100;
this.armor = 0;
this.ammo = 50;
this.kills = 0;
this.totalKills = 0;
this.score = 0;
this.weaponSystem = new WeaponSystem();
}
update(dt, map) {
this.currentMap = map;
// Mouse look
this.angle += this.mouseDX * MOUSE_SENSITIVITY;
this.mouseDX = 0;
// Movement
let moveX = 0;
let moveY = 0;
const speed = PLAYER_SPEED * dt / 1000;
this.isMoving = false;
if (this.keys['w'] || this.keys['arrowup']) {
moveX += Math.cos(this.angle) * speed;
moveY += Math.sin(this.angle) * speed;
this.isMoving = true;
}
if (this.keys['s'] || this.keys['arrowdown']) {
moveX -= Math.cos(this.angle) * speed;
moveY -= Math.sin(this.angle) * speed;
this.isMoving = true;
}
if (this.keys['a'] || this.keys['arrowleft']) {
moveX += Math.cos(this.angle - Math.PI / 2) * speed;
moveY += Math.sin(this.angle - Math.PI / 2) * speed;
this.isMoving = true;
}
if (this.keys['d'] || this.keys['arrowright']) {
moveX -= Math.cos(this.angle - Math.PI / 2) * speed;
moveY -= Math.sin(this.angle - Math.PI / 2) * speed;
this.isMoving = true;
}
// Apply movement with collision
this.move(moveX, moveY, map);
// Head bob
if (this.isMoving) {
this.bobPhase += dt * 0.008;
}
// Weapon switching
if (this.keys['1']) this.weaponSystem.switchTo('pistol');
if (this.keys['2']) this.weaponSystem.switchTo('shotgun');
if (this.keys['3']) this.weaponSystem.switchTo('plasma');
// Weapon update
this.weaponSystem.update(dt);
}
move(moveX, moveY, map) {
const margin = PLAYER_SIZE;
// Separate X and Y collision for sliding along walls
const newX = this.x + moveX;
const newY = this.y + moveY;
// Check X
if (this.isPassable(newX + (moveX > 0 ? margin : -margin), this.y, map) &&
this.isPassable(newX + (moveX > 0 ? margin : -margin), this.y + margin, map) &&
this.isPassable(newX + (moveX > 0 ? margin : -margin), this.y - margin, map)) {
this.x = newX;
}
// Check Y
if (this.isPassable(this.x, newY + (moveY > 0 ? margin : -margin), map) &&
this.isPassable(this.x + margin, newY + (moveY > 0 ? margin : -margin), map) &&
this.isPassable(this.x - margin, newY + (moveY > 0 ? margin : -margin), map)) {
this.y = newY;
}
}
isPassable(x, y, map) {
const cellX = Math.floor(x);
const cellY = Math.floor(y);
if (cellY < 0 || cellY >= map.length || cellX < 0 || cellX >= map[0].length) return false;
const cell = map[cellY][cellX];
return cell === 0;
}
takeDamage(amount) {
// Armor absorbs some damage
if (this.armor > 0) {
const armorAbsorb = Math.min(this.armor, Math.floor(amount * 0.6));
this.armor -= armorAbsorb;
amount -= armorAbsorb;
}
this.health -= amount;
if (this.health < 0) this.health = 0;
Audio.playPlayerHurt();
}
heal(amount) {
this.health = Math.min(this.maxHealth, this.health + amount);
}
addArmor(amount) {
this.armor = Math.min(this.maxArmor, this.armor + amount);
}
addAmmo(amount) {
this.ammo = Math.min(this.maxAmmo, this.ammo + amount);
}
// Try to interact with a door nearby
tryInteract(map) {
// Check cells in front of the player
for (let dist = 0.5; dist <= 1.5; dist += 0.5) {
const checkX = Math.floor(this.x + Math.cos(this.angle) * dist);
const checkY = Math.floor(this.y + Math.sin(this.angle) * dist);
if (checkY >= 0 && checkY < map.length && checkX >= 0 && checkX < map[0].length) {
if (map[checkY][checkX] === WALL_DOOR) {
// Open door
map[checkY][checkX] = 0;
Audio.playDoorOpen();
return 'door_opened';
}
if (map[checkY][checkX] === WALL_EXIT) {
return 'exit_reached';
}
}
}
return null;
}
}