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:
+177
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user