Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Math Doom</title> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
overflow: hidden; | |
font-family: 'Courier New', monospace; | |
background-color: #000; | |
color: #fff; | |
display: flex; | |
flex-direction: column; | |
height: 100vh; | |
} | |
#gameContainer { | |
position: relative; | |
flex-grow: 1; | |
overflow: hidden; | |
} | |
#canvas { | |
display: block; | |
background-color: #000; | |
cursor: none; | |
} | |
#menu { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.8); | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 10; | |
} | |
#menu h1 { | |
font-size: 3em; | |
color: #ff0000; | |
margin-bottom: 0.5em; | |
text-shadow: 0 0 10px #ff0000; | |
} | |
.menu-button { | |
background-color: #ff0000; | |
color: #000; | |
border: none; | |
padding: 15px 30px; | |
margin: 10px; | |
font-size: 1.5em; | |
cursor: pointer; | |
font-family: 'Courier New', monospace; | |
font-weight: bold; | |
transition: all 0.3s; | |
border-radius: 5px; | |
} | |
.menu-button:hover { | |
background-color: #ffffff; | |
transform: scale(1.05); | |
} | |
#hud { | |
position: absolute; | |
bottom: 20px; | |
left: 20px; | |
font-size: 1.5em; | |
color: #fff; | |
text-shadow: 2px 2px 2px #000; | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
} | |
#health, #ammo, #weapon { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
#health span, #ammo span { | |
color: #ff0000; | |
font-weight: bold; | |
} | |
#weaponName { | |
color: #ffcc00; | |
font-weight: bold; | |
} | |
#levelInfo { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
font-size: 1.2em; | |
color: #fff; | |
text-shadow: 2px 2px 2px #000; | |
} | |
#levelInfo span { | |
color: #00ff00; | |
font-weight: bold; | |
} | |
#enemyCount { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
font-size: 1.2em; | |
color: #fff; | |
text-shadow: 2px 2px 2px #000; | |
} | |
#enemyCount span { | |
color: #ff0000; | |
font-weight: bold; | |
} | |
#mathPopup { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background-color: rgba(0, 0, 0, 0.9); | |
border: 2px solid #ff0000; | |
padding: 20px; | |
border-radius: 10px; | |
display: none; | |
flex-direction: column; | |
align-items: center; | |
z-index: 20; | |
} | |
#mathPopup p { | |
margin: 0 0 20px 0; | |
font-size: 1.5em; | |
color: #fff; | |
} | |
#mathAnswer { | |
font-size: 1.2em; | |
padding: 10px; | |
width: 200px; | |
text-align: center; | |
margin-bottom: 10px; | |
background-color: #333; | |
border: 1px solid #ff0000; | |
color: #fff; | |
} | |
#mathSubmit { | |
padding: 10px 20px; | |
background-color: #ff0000; | |
color: #000; | |
border: none; | |
font-weight: bold; | |
cursor: pointer; | |
} | |
#crosshair { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 20px; | |
height: 20px; | |
pointer-events: none; | |
z-index: 5; | |
} | |
#crosshair::before, #crosshair::after { | |
content: ''; | |
position: absolute; | |
background-color: #ff0000; | |
} | |
#crosshair::before { | |
width: 2px; | |
height: 20px; | |
left: 9px; | |
top: 0; | |
} | |
#crosshair::after { | |
width: 20px; | |
height: 2px; | |
left: 0; | |
top: 9px; | |
} | |
#deathScreen { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.8); | |
display: none; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 15; | |
} | |
#deathScreen h1 { | |
font-size: 3em; | |
color: #ff0000; | |
margin-bottom: 0.5em; | |
} | |
#winScreen { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.8); | |
display: none; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 15; | |
} | |
#winScreen h1 { | |
font-size: 3em; | |
color: #00ff00; | |
margin-bottom: 0.5em; | |
} | |
#damageIndicator { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(255, 0, 0, 0.3); | |
display: none; | |
pointer-events: none; | |
z-index: 5; | |
} | |
#fpsCounter { | |
position: absolute; | |
top: 10px; | |
right: 10px; | |
color: #00ff00; | |
font-size: 0.8em; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="gameContainer"> | |
<canvas id="canvas"></canvas> | |
<div id="crosshair"></div> | |
<div id="hud"> | |
<div id="health">HEALTH: <span id="healthValue">100</span></div> | |
<div id="ammo">AMMO: <span id="ammoValue">50</span></div> | |
<div id="weapon">WEAPON: <span id="weaponName">PISTOL</span></div> | |
</div> | |
<div id="levelInfo">LEVEL: <span id="levelValue">1</span></div> | |
<div id="enemyCount">ENEMIES: <span id="enemyCountValue">5</span></div> | |
<div id="mathPopup"> | |
<p id="mathQuestion">What is 5 + 3?</p> | |
<input type="text" id="mathAnswer" placeholder="Enter answer"> | |
<button id="mathSubmit">SUBMIT</button> | |
</div> | |
<div id="menu"> | |
<h1>MATH DOOM</h1> | |
<button class="menu-button" id="startButton">START GAME</button> | |
<button class="menu-button" id="howToButton">HOW TO PLAY</button> | |
</div> | |
<div id="deathScreen"> | |
<h1>YOU DIED</h1> | |
<button class="menu-button" id="restartButton">TRY AGAIN</button> | |
<button class="menu-button" id="menuButton">MAIN MENU</button> | |
</div> | |
<div id="winScreen"> | |
<h1>VICTORY!</h1> | |
<button class="menu-button" id="nextLevelButton">NEXT LEVEL</button> | |
<button class="menu-button" id="menuButtonWin">MAIN MENU</button> | |
</div> | |
<div id="damageIndicator"></div> | |
</div> | |
<script> | |
// Game constants | |
const GAME_WIDTH = 800; | |
const GAME_HEIGHT = 600; | |
const PLAYER_SPEED = 5; | |
const ROTATION_SPEED = 0.05; | |
const FOV = 60 * Math.PI / 180; | |
const WALL_HEIGHT = 100; | |
const CEILING_COLOR = '#333333'; | |
const FLOOR_COLOR = '#222222'; | |
const WALL_COLORS = ['#880000', '#008800', '#000088', '#888800', '#880088', '#008888']; | |
const ENEMY_COLORS = ['#ff0000', '#ff6666', '#cc0000']; | |
const PICKUP_COLORS = { | |
health: '#00ff00', | |
ammo: '#ffff00', | |
weapon: '#ffffff' | |
}; | |
// Game variables | |
let canvas, ctx; | |
let player = { | |
x: 50, | |
y: 50, | |
angle: 0, | |
health: 100, | |
maxHealth: 100, | |
ammo: 50, | |
maxAmmo: 100, | |
weapons: ['PISTOL'], | |
currentWeapon: 0, | |
damageTaken: 0, | |
visible: false | |
}; | |
let map = []; | |
let enemies = []; | |
let pickups = []; | |
let rays = []; | |
let isMouseLocked = false; | |
let isGameRunning = false; | |
let currentLevel = 1; | |
let enemiesLeft = 0; | |
let mouseSensitivity = 0.002; | |
let lastTime = 0; | |
let fps = 0; | |
let deltaTime = 0; | |
let keys = {}; | |
let showFPS = false; | |
let mathTimeout = null; | |
let currentPickup = null; | |
// Weapon stats | |
const weapons = [ | |
{ | |
name: 'PISTOL', | |
damage: 10, | |
fireRate: 500, // ms between shots | |
ammoCost: 1, | |
range: 500, | |
lastShot: 0, | |
spread: 0.05, | |
ammoType: 'bullet' | |
}, | |
{ | |
name: 'SHOTGUN', | |
damage: 30, | |
fireRate: 1000, | |
ammoCost: 5, | |
range: 300, | |
lastShot: 0, | |
spread: 0.2, | |
ammoType: 'shell' | |
}, | |
{ | |
name: 'MACHINE GUN', | |
damage: 5, | |
fireRate: 100, | |
ammoCost: 1, | |
range: 600, | |
lastShot: 0, | |
spread: 0.1, | |
ammoType: 'bullet' | |
} | |
]; | |
// Initialize game | |
function init() { | |
canvas = document.getElementById('canvas'); | |
ctx = canvas.getContext('2d'); | |
canvas.width = GAME_WIDTH; | |
canvas.height = GAME_HEIGHT; | |
setupEventListeners(); | |
generateLevel(1); | |
renderMenu(); | |
// Start game loop | |
requestAnimationFrame(gameLoop); | |
} | |
// Set up event listeners | |
function setupEventListeners() { | |
// Mouse events | |
canvas.addEventListener('click', () => { | |
if (isGameRunning && !isMouseLocked) { | |
canvas.requestPointerLock = canvas.requestPointerLock || | |
canvas.mozRequestPointerLock || | |
canvas.webkitRequestPointerLock; | |
canvas.requestPointerLock(); | |
} | |
}); | |
document.addEventListener('pointerlockchange', lockChange, false); | |
document.addEventListener('mozpointerlockchange', lockChange, false); | |
document.addEventListener('webkitpointerlockchange', lockChange, false); | |
function lockChange() { | |
isMouseLocked = document.pointerLockElement === canvas || | |
document.mozPointerLockElement === canvas || | |
document.webkitPointerLockElement === canvas; | |
} | |
document.addEventListener('mousemove', (e) => { | |
if (isMouseLocked && isGameRunning) { | |
player.angle += e.movementX * mouseSensitivity; | |
} | |
}); | |
// Keyboard events | |
document.addEventListener('keydown', (e) => { | |
keys[e.key] = true; | |
if (e.key === 'f') { | |
showFPS = !showFPS; | |
} | |
}); | |
document.addEventListener('keyup', (e) => { | |
keys[e.key] = false; | |
}); | |
// Menu buttons | |
document.getElementById('startButton').addEventListener('click', startGame); | |
document.getElementById('howToButton').addEventListener('click', showHowToPlay); | |
document.getElementById('restartButton').addEventListener('click', restartGame); | |
document.getElementById('menuButton').addEventListener('click', showMenu); | |
document.getElementById('menuButtonWin').addEventListener('click', showMenu); | |
document.getElementById('nextLevelButton').addEventListener('click', nextLevel); | |
document.getElementById('mathSubmit').addEventListener('click', checkMathAnswer); | |
} | |
// Game loop | |
function gameLoop(timestamp) { | |
deltaTime = timestamp - lastTime; | |
lastTime = timestamp; | |
fps = 1000 / deltaTime; | |
if (isGameRunning) { | |
update(); | |
render(); | |
} | |
requestAnimationFrame(gameLoop); | |
} | |
// Update game state | |
function update() { | |
// Player movement | |
if (keys['w'] || keys['ArrowUp']) { | |
const newX = player.x + Math.cos(player.angle) * PLAYER_SPEED; | |
const newY = player.y + Math.sin(player.angle) * PLAYER_SPEED; | |
if (!isWall(newX, player.y)) player.x = newX; | |
if (!isWall(player.x, newY)) player.y = newY; | |
} | |
if (keys['s'] || keys['ArrowDown']) { | |
const newX = player.x - Math.cos(player.angle) * PLAYER_SPEED; | |
const newY = player.y - Math.sin(player.angle) * PLAYER_SPEED; | |
if (!isWall(newX, player.y)) player.x = newX; | |
if (!isWall(player.x, newY)) player.y = newY; | |
} | |
if (keys['a'] || keys['ArrowLeft']) { | |
const newX = player.x - Math.cos(player.angle + Math.PI/2) * PLAYER_SPEED; | |
const newY = player.y - Math.sin(player.angle + Math.PI/2) * PLAYER_SPEED; | |
if (!isWall(newX, player.y)) player.x = newX; | |
if (!isWall(player.x, newY)) player.y = newY; | |
} | |
if (keys['d'] || keys['ArrowRight']) { | |
const newX = player.x + Math.cos(player.angle + Math.PI/2) * PLAYER_SPEED; | |
const newY = player.y + Math.sin(player.angle + Math.PI/2) * PLAYER_SPEED; | |
if (!isWall(newX, player.y)) player.x = newX; | |
if (!isWall(player.x, newY)) player.y = newY; | |
} | |
// Weapon switching | |
if (keys['1'] && player.weapons.includes('PISTOL')) { | |
player.currentWeapon = 0; | |
updateHUD(); | |
} | |
if (keys['2'] && player.weapons.includes('SHOTGUN')) { | |
player.currentWeapon = 1; | |
updateHUD(); | |
} | |
if (keys['3'] && player.weapons.includes('MACHINE GUN')) { | |
player.currentWeapon = 2; | |
updateHUD(); | |
} | |
// Shooting | |
if (keys[' '] && Date.now() - weapons[player.currentWeapon].lastShot > weapons[player.currentWeapon].fireRate) { | |
if (player.ammo >= weapons[player.currentWeapon].ammoCost) { | |
shoot(); | |
} else { | |
// Play empty sound or show message | |
} | |
} | |
// Enemy AI and actions | |
updateEnemies(); | |
// Check for pickups | |
checkPickups(); | |
// Damage indicator | |
if (player.damageTaken > 0) { | |
const damageIndicator = document.getElementById('damageIndicator'); | |
damageIndicator.style.display = 'block'; | |
damageIndicator.style.opacity = player.damageTaken / 100; | |
player.damageTaken = Math.max(0, player.damageTaken - 1); | |
} else { | |
document.getElementById('damageIndicator').style.display = 'none'; | |
} | |
// Check win/lose conditions | |
if (player.health <= 0) { | |
gameOver(); | |
} else if (enemiesLeft <= 0) { | |
levelComplete(); | |
} | |
} | |
// Render game | |
function render() { | |
// Clear canvas | |
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT); | |
// Draw ceiling and floor | |
drawCeilingAndFloor(); | |
// Cast rays and draw walls | |
castRays(); | |
// Draw enemies | |
drawEnemies(); | |
// Draw pickups | |
drawPickups(); | |
// Draw minimap (for debugging) | |
if (keys['m']) { | |
drawMinimap(); | |
} | |
// FPS counter | |
if (showFPS) { | |
ctx.fillStyle = '#ffffff'; | |
ctx.font = '16px Arial'; | |
ctx.fillText(`FPS: ${Math.round(fps)}`, 10, 20); | |
} | |
} | |
// Draw ceiling and floor | |
function drawCeilingAndFloor() { | |
// Ceiling | |
ctx.fillStyle = CEILING_COLOR; | |
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT / 2); | |
// Floor | |
ctx.fillStyle = FLOOR_COLOR; | |
ctx.fillRect(0, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT / 2); | |
} | |
// Cast rays for 3D effect | |
function castRays() { | |
rays = []; | |
const rayCount = GAME_WIDTH; | |
for (let x = 0; x < rayCount; x++) { | |
const rayAngle = player.angle - FOV / 2 + (x / rayCount) * FOV; | |
const ray = castRay(player.x, player.y, rayAngle); | |
rays.push(ray); | |
// Draw wall slice | |
const distance = ray.distance * Math.cos(ray.angle - player.angle); // Fix fisheye | |
const wallHeight = (WALL_HEIGHT / distance) * 277; // Magic number for proper scaling | |
ctx.fillStyle = WALL_COLORS[ray.colorIndex]; | |
ctx.fillRect(x, (GAME_HEIGHT - wallHeight) / 2, 1, wallHeight); | |
} | |
} | |
// Cast a single ray | |
function castRay(x, y, angle) { | |
// Check both vertical and horizontal grid lines | |
let vCollision = checkVerticalCollision(x, y, angle); | |
let hCollision = checkHorizontalCollision(x, y, angle); | |
// Use the closest collision | |
let collision; | |
if (!vCollision) { | |
collision = hCollision; | |
} else if (!hCollision) { | |
collision = vCollision; | |
} else { | |
collision = vCollision.distance < hCollision.distance ? vCollision : hCollision; | |
} | |
return { | |
x: collision.x, | |
y: collision.y, | |
distance: collision.distance, | |
angle: angle, | |
vertical: collision.vertical, | |
colorIndex: collision.colorIndex | |
}; | |
} | |
// Check for vertical grid line collisions | |
function checkVerticalCollision(x, y, angle) { | |
const right = Math.abs(Math.floor((angle - Math.PI / 2) / Math.PI) % 2) === 1; | |
const firstX = right ? | |
Math.floor(x / 50) * 50 + 50 : | |
Math.ceil(x / 50) * 50 - 50; | |
const firstY = y + (firstX - x) * Math.tan(angle); | |
const deltaX = right ? 50 : -50; | |
const deltaY = deltaX * Math.tan(angle); | |
let nextX = firstX; | |
let nextY = firstY; | |
while (nextX >= 0 && nextX <= map[0].length * 50 && | |
nextY >= 0 && nextY <= map.length * 50) { | |
const cellX = right ? Math.floor(nextX / 50) : Math.floor(nextX / 50) - 1; | |
const cellY = Math.floor(nextY / 50); | |
if (cellX >= 0 && cellX < map[0].length && | |
cellY >= 0 && cellY < map.length && | |
map[cellY][cellX] > 0) { | |
const distance = Math.sqrt(Math.pow(nextX - x, 2) + Math.pow(nextY - y, 2)); | |
return { | |
x: nextX, | |
y: nextY, | |
distance: distance, | |
vertical: true, | |
colorIndex: map[cellY][cellX] - 1 | |
}; | |
} | |
nextX += deltaX; | |
nextY += deltaY; | |
} | |
return null; | |
} | |
// Check for horizontal grid line collisions | |
function checkHorizontalCollision(x, y, angle) { | |
const up = Math.abs(Math.floor(angle / Math.PI) % 2) === 0; | |
const firstY = up ? | |
Math.floor(y / 50) * 50 - 1 : | |
Math.ceil(y / 50) * 50 + 1; | |
const firstX = x + (firstY - y) / Math.tan(angle); | |
const deltaY = up ? -50 : 50; | |
const deltaX = deltaY / Math.tan(angle); | |
let nextX = firstX; | |
let nextY = firstY; | |
while (nextX >= 0 && nextX <= map[0].length * 50 && | |
nextY >= 0 && nextY <= map.length * 50) { | |
const cellX = Math.floor(nextX / 50); | |
const cellY = up ? Math.floor(nextY / 50) - 1 : Math.floor(nextY / 50); | |
if (cellX >= 0 && cellX < map[0].length && | |
cellY >= 0 && cellY < map.length && | |
map[cellY][cellX] > 0) { | |
const distance = Math.sqrt(Math.pow(nextX - x, 2) + Math.pow(nextY - y, 2)); | |
return { | |
x: nextX, | |
y: nextY, | |
distance: distance, | |
vertical: false, | |
colorIndex: map[cellY][cellX] - 1 | |
}; | |
} | |
nextX += deltaX; | |
nextY += deltaY; | |
} | |
return null; | |
} | |
// Draw enemies | |
function drawEnemies() { | |
for (let enemy of enemies) { | |
if (enemy.health <= 0) continue; | |
// Calculate angle between player and enemy | |
const angleToEnemy = Math.atan2(enemy.y - player.y, enemy.x - player.x) - player.angle; | |
// Normalize angle | |
let normalizedAngle = angleToEnemy; | |
while (normalizedAngle > Math.PI) normalizedAngle -= 2 * Math.PI; | |
while (normalizedAngle < -Math.PI) normalizedAngle += 2 * Math.PI; | |
// Check if enemy is in player's FOV | |
if (Math.abs(normalizedAngle) < FOV / 2) { | |
// Check if there's a wall between player and enemy | |
const distToEnemy = Math.sqrt(Math.pow(enemy.x - player.x, 2) + Math.pow(enemy.y - player.y, 2)); | |
const ray = castRay(player.x, player.y, player.angle + normalizedAngle); | |
if (ray.distance > distToEnemy) { | |
// Enemy is visible, draw it | |
const enemyHeight = (WALL_HEIGHT * 1.5) / distToEnemy * 277; // Scale similarly to walls | |
const screenX = (normalizedAngle + FOV / 2) / FOV * GAME_WIDTH; | |
// Draw enemy | |
ctx.fillStyle = ENEMY_COLORS[enemy.type]; | |
ctx.fillRect( | |
screenX - enemyHeight / 4, | |
(GAME_HEIGHT - enemyHeight) / 2, | |
enemyHeight / 2, | |
enemyHeight | |
); | |
// Health bar | |
ctx.fillStyle = '#ff0000'; | |
ctx.fillRect( | |
screenX - enemyHeight / 4, | |
(GAME_HEIGHT - enemyHeight) / 2 - 10, | |
enemyHeight / 2, | |
5 | |
); | |
ctx.fillStyle = '#00ff00'; | |
ctx.fillRect( | |
screenX - enemyHeight / 4, | |
(GAME_HEIGHT - enemyHeight) / 2 - 10, | |
enemyHeight / 2 * (enemy.health / enemy.maxHealth), | |
5 | |
); | |
} | |
} | |
} | |
} | |
// Draw pickups | |
function drawPickups() { | |
for (let pickup of pickups) { | |
// Calculate angle between player and pickup | |
const angleToPickup = Math.atan2(pickup.y - player.y, pickup.x - player.x) - player.angle; | |
// Normalize angle | |
let normalizedAngle = angleToPickup; | |
while (normalizedAngle > Math.PI) normalizedAngle -= 2 * Math.PI; | |
while (normalizedAngle < -Math.PI) normalizedAngle += 2 * Math.PI; | |
// Check if pickup is in player's FOV | |
if (Math.abs(normalizedAngle) < FOV / 2) { | |
// Check if there's a wall between player and pickup | |
const distToPickup = Math.sqrt(Math.pow(pickup.x - player.x, 2) + Math.pow(pickup.y - player.y, 2)); | |
const ray = castRay(player.x, player.y, player.angle + normalizedAngle); | |
if (ray.distance > distToPickup) { | |
// Pickup is visible, draw it | |
const pickupHeight = (WALL_HEIGHT * 0.5) / distToPickup * 277; | |
const screenX = (normalizedAngle + FOV / 2) / FOV * GAME_WIDTH; | |
// Draw pickup | |
ctx.fillStyle = pickup.color; | |
ctx.beginPath(); | |
ctx.arc( | |
screenX, | |
GAME_HEIGHT / 2, | |
pickupHeight / 3, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
// Draw icon based on type | |
ctx.fillStyle = '#000000'; | |
ctx.font = `${pickupHeight / 2}px Arial`; | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
let symbol = ''; | |
if (pickup.type === 'health') symbol = '+'; | |
else if (pickup.type === 'ammo') symbol = 'A'; | |
else if (pickup.type === 'weapon') symbol = 'W'; | |
ctx.fillText(symbol, screenX, GAME_HEIGHT / 2); | |
} | |
} | |
} | |
} | |
// Draw minimap (for debugging) | |
function drawMinimap() { | |
const minimapSize = 200; | |
const cellSize = minimapSize / Math.max(map.length, map[0].length); | |
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; | |
ctx.fillRect(10, 10, minimapSize, minimapSize); | |
// Draw walls | |
for (let y = 0; y < map.length; y++) { | |
for (let x = 0; x < map[y].length; x++) { | |
if (map[y][x] > 0) { | |
ctx.fillStyle = WALL_COLORS[map[y][x] - 1]; | |
ctx.fillRect(10 + x * cellSize, 10 + y * cellSize, cellSize, cellSize); | |
} | |
} | |
} | |
// Draw player | |
ctx.fillStyle = '#ffffff'; | |
ctx.beginPath(); | |
ctx.arc( | |
10 + player.x / 50 * cellSize, | |
10 + player.y / 50 * cellSize, | |
cellSize / 2, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
// Draw player direction | |
ctx.strokeStyle = '#ffffff'; | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
ctx.moveTo( | |
10 + player.x / 50 * cellSize, | |
10 + player.y / 50 * cellSize | |
); | |
ctx.lineTo( | |
10 + (player.x + Math.cos(player.angle) * 20) / 50 * cellSize, | |
10 + (player.y + Math.sin(player.angle) * 20) / 50 * cellSize | |
); | |
ctx.stroke(); | |
// Draw enemies | |
for (let enemy of enemies) { | |
if (enemy.health <= 0) continue; | |
ctx.fillStyle = ENEMY_COLORS[enemy.type]; | |
ctx.beginPath(); | |
ctx.arc( | |
10 + enemy.x / 50 * cellSize, | |
10 + enemy.y / 50 * cellSize, | |
cellSize / 2, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
} | |
// Draw pickups | |
for (let pickup of pickups) { | |
ctx.fillStyle = pickup.color; | |
ctx.beginPath(); | |
ctx.arc( | |
10 + pickup.x / 50 * cellSize, | |
10 + pickup.y / 50 * cellSize, | |
cellSize / 3, | |
0, | |
Math.PI * 2 | |
); | |
ctx.fill(); | |
} | |
} | |
// Check if a position is a wall | |
function isWall(x, y) { | |
const cellX = Math.floor(x / 50); | |
const cellY = Math.floor(y / 50); | |
if (cellX < 0 || cellX >= map[0].length || cellY < 0 || cellY >= map.length) { | |
return true; | |
} | |
return map[cellY][cellX] > 0; | |
} | |
// Shoot weapon | |
function shoot() { | |
const weapon = weapons[player.currentWeapon]; | |
// Check if player has enough ammo | |
if (player.ammo < weapon.ammoCost) return; | |
// Consume ammo | |
player.ammo -= weapon.ammoCost; | |
weapon.lastShot = Date.now(); | |
// Add muzzle flash effect (simple for now) | |
ctx.fillStyle = '#ffff00'; | |
ctx.fillRect(GAME_WIDTH / 2 - 20, GAME_HEIGHT / 2 - 20, 40, 40); | |
setTimeout(() => { | |
// Flash disappears quickly | |
}, 50); | |
// Hit check | |
for (let i = 0; i < (player.currentWeapon === 1 ? 5 : 1); i++) { // Shotgun shoots multiple pellets | |
const spreadAngle = (Math.random() - 0.5) * weapon.spread; | |
const shootAngle = player.angle + spreadAngle; | |
// Cast ray to check for hits | |
const hit = castRay(player.x, player.y, shootAngle); | |
// Check if we hit an enemy | |
for (let enemy of enemies) { | |
if (enemy.health <= 0) continue; | |
// Calculate distance from ray to enemy | |
const distanceToEnemy = Math.sqrt(Math.pow(hit.x - enemy.x, 2) + Math.pow(hit.y - enemy.y, 2)); | |
// If enemy is close to hit point and not behind a wall | |
if (distanceToEnemy < 30 && hit.distance >= Math.sqrt(Math.pow(enemy.x - player.x, 2) + Math.pow(enemy.y - player.y, 2))) { | |
// Hit the enemy | |
enemy.health -= weapon.damage * (player.currentWeapon === 1 ? 0.8 : 1); // Shotgun pellets do less damage individually | |
// If enemy died, increase score | |
if (enemy.health <= 0) { | |
enemiesLeft--; | |
document.getElementById('enemyCountValue').textContent = enemiesLeft; | |
} | |
break; | |
} | |
} | |
} | |
updateHUD(); | |
} | |
// Update enemies | |
function updateEnemies() { | |
for (let enemy of enemies) { | |
if (enemy.health <= 0) continue; | |
// Simple AI: move toward player if visible | |
const dx = player.x - enemy.x; | |
const dy = player.y - enemy.y; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
// Check if player is visible | |
const angleToPlayer = Math.atan2(dy, dx); | |
const ray = castRay(enemy.x, enemy.y, angleToPlayer); | |
if (ray.distance >= distance - 10) { // Player is visible | |
// Move toward player | |
enemy.x += Math.cos(angleToPlayer) * enemy.speed; | |
enemy.y += Math.sin(angleToPlayer) * enemy.speed; | |
// Simple attack: damage player if close enough | |
if (distance < 50) { | |
player.health -= enemy.damage; | |
player.damageTaken = 50; // Show damage indicator | |
updateHUD(); | |
} | |
} else { | |
// Wander randomly | |
enemy.x += Math.cos(enemy.direction) * enemy.speed / 2; | |
enemy.y += Math.sin(enemy.direction) * enemy.speed / 2; | |
// Randomly change direction | |
if (Math.random() < 0.01) { | |
enemy.direction = Math.random() * Math.PI * 2; | |
} | |
} | |
// Collision with walls | |
if (isWall(enemy.x, enemy.y)) { | |
// Move away from walls | |
enemy.x -= Math.cos(angleToPlayer) * enemy.speed; | |
enemy.y -= Math.sin(angleToPlayer) * enemy.speed; | |
enemy.direction = Math.random() * Math.PI * 2; | |
} | |
} | |
} | |
// Check for pickups | |
function checkPickups() { | |
if (currentPickup) return; // Already trying to pick up something | |
for (let i = 0; i < pickups.length; i++) { | |
const pickup = pickups[i]; | |
const distance = Math.sqrt(Math.pow(pickup.x - player.x, 2) + Math.pow(pickup.y - player.y, 2)); | |
if (distance < 30) { // Close enough to pickup | |
currentPickup = pickup; | |
showMathPopup(pickup.type); | |
break; | |
} | |
} | |
} | |
// Show math popup for pickup | |
function showMathPopup(type) { | |
const mathPopup = document.getElementById('mathPopup'); | |
const mathQuestion = document.getElementById('mathQuestion'); | |
const mathAnswer = document.getElementById('mathAnswer'); | |
let question, answer; | |
const difficulty = Math.min(currentLevel, 5); | |
// Generate math question based on current level (gets harder as level increases) | |
if (difficulty <= 2) { | |
// Addition and subtraction | |
const a = Math.floor(Math.random() * 10) + 1; | |
const b = Math.floor(Math.random() * 10) + 1; | |
const op = Math.random() > 0.5 ? '+' : '-'; | |
question = `${a} ${op} ${b} = ?`; | |
answer = op === '+' ? a + b : a - b; | |
} else if (difficulty <= 4) { | |
// Multiplication | |
const a = Math.floor(Math.random() * 10) + 1; | |
const b = Math.floor(Math.random() * 5) + 1; | |
question = `${a} × ${b} = ?`; | |
answer = a * b; | |
} else { | |
// Division | |
const a = Math.floor(Math.random() * 10) + 1; | |
const b = Math.floor(Math.random() * 5) + 1; | |
const c = a * b; | |
question = `${c} ÷ ${b} = ?`; | |
answer = a; | |
} | |
mathQuestion.textContent = question; | |
mathAnswer.dataset.answer = answer; | |
mathAnswer.value = ''; | |
mathPopup.style.display = 'flex'; | |
mathAnswer.focus(); | |
// Set timeout to auto-close if player doesn't answer | |
if (mathTimeout) clearTimeout(mathTimeout); | |
mathTimeout = setTimeout(() => { | |
mathPopup.style.display = 'none'; | |
currentPickup = null; | |
}, 5000); | |
} | |
// Check math answer for pickup | |
function checkMathAnswer() { | |
const mathAnswer = document.getElementById('mathAnswer'); | |
const correctAnswer = mathAnswer.dataset.answer; | |
const userAnswer = mathAnswer.value.trim(); | |
document.getElementById('mathPopup').style.display = 'none'; | |
if (userAnswer === correctAnswer) { | |
// Correct answer - give pickup | |
const pickup = currentPickup; | |
if (pickup.type === 'health') { | |
player.health = Math.min(player.health + pickup.value, player.maxHealth); | |
} else if (pickup.type === 'ammo') { | |
player.ammo = Math.min(player.ammo + pickup.value, player.maxAmmo); | |
} else if (pickup.type === 'weapon') { | |
if (!player.weapons.includes(pickup.weaponName)) { | |
player.weapons.push(pickup.weaponName); | |
} | |
player.currentWeapon = weapons.findIndex(w => w.name === pickup.weaponName); | |
player.ammo = Math.min(player.ammo + pickup.ammoBonus, player.maxAmmo); | |
} | |
// Remove pickup | |
const index = pickups.indexOf(pickup); | |
if (index !== -1) { | |
pickups.splice(index, 1); | |
} | |
updateHUD(); | |
} | |
currentPickup = null; | |
if (mathTimeout) clearTimeout(mathTimeout); | |
} | |
// Update HUD | |
function updateHUD() { | |
document.getElementById('healthValue').textContent = player.health; | |
document.getElementById('ammoValue').textContent = player.ammo; | |
document.getElementById('weaponName').textContent = weapons[player.currentWeapon].name; | |
} | |
// Generate level | |
function generateLevel(level) { | |
// Reset game state | |
player.x = 50; | |
player.y = 50; | |
player.angle = 0; | |
player.health = 100; | |
player.ammo = 50; | |
// Default weapons | |
player.weapons = ['PISTOL']; | |
player.currentWeapon = 0; | |
enemies = []; | |
pickups = []; | |
// Create maze-like map | |
map = []; | |
const size = 10 + level * 2; // Increase size with level | |
// Initialize empty map | |
for (let y = 0; y < size; y++) { | |
map[y] = []; | |
for (let x = 0; x < size; x++) { | |
map[y][x] = 0; | |
} | |
} | |
// Add walls | |
for (let y = 0; y < size; y++) { | |
map[y][0] = 1 + Math.floor(Math.random() * 3); | |
map[y][size-1] = 1 + Math.floor(Math.random() * 3); | |
} | |
for (let x = 0; x < size; x++) { | |
map[0][x] = 1 + Math.floor(Math.random() * 3); | |
map[size-1][x] = 1 + Math.floor(Math.random() * 3); | |
} | |
// Add random walls inside | |
for (let i = 0; i < size * 2; i++) { | |
const x = Math.floor(Math.random() * (size - 2)) + 1; | |
const y = Math.floor(Math.random() * (size - 2)) + 1; | |
const length = Math.floor(Math.random() * 3) + 1; | |
const horizontal = Math.random() > 0.5; | |
for (let j = 0; j < length; j++) { | |
const nx = horizontal ? x + j : x; | |
const ny = horizontal ? y : y + j; | |
if (nx < size && ny < size) { | |
map[ny][nx] = 1 + Math.floor(Math.random() * 3); | |
} | |
} | |
} | |
// Make sure player start is open | |
map[1][1] = 0; | |
map[1][2] = 0; | |
map[2][1] = 0; | |
// Add enemies | |
enemiesLeft = 3 + level * 2; | |
for (let i = 0; i < enemiesLeft; i++) { | |
let x, y; | |
do { | |
x = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
y = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
} while (isWall(x, y)); | |
enemies.push({ | |
x: x, | |
y: y, | |
health: 30 + level * 10, | |
maxHealth: 30 + level * 10, | |
damage: 5 + level, | |
speed: 1 + level * 0.2, | |
direction: Math.random() * Math.PI * 2, | |
type: Math.min(Math.floor(Math.random() * level), ENEMY_COLORS.length - 1) | |
}); | |
} | |
// Add health pickups | |
for (let i = 0; i < 2 + level; i++) { | |
let x, y; | |
do { | |
x = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
y = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
} while (isWall(x, y)); | |
pickups.push({ | |
x: x, | |
y: y, | |
type: 'health', | |
value: 25, | |
color: PICKUP_COLORS.health | |
}); | |
} | |
// Add ammo pickups | |
for (let i = 0; i < 2 + level; i++) { | |
let x, y; | |
do { | |
x = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
y = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
} while (isWall(x, y)); | |
pickups.push({ | |
x: x, | |
y: y, | |
type: 'ammo', | |
value: 20, | |
color: PICKUP_COLORS.ammo | |
}); | |
} | |
// Add weapon pickups (only if not already has them) | |
if (level > 1 && !player.weapons.includes('SHOTGUN')) { | |
let x, y; | |
do { | |
x = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
y = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
} while (isWall(x, y)); | |
pickups.push({ | |
x: x, | |
y: y, | |
type: 'weapon', | |
weaponName: 'SHOTGUN', | |
ammoBonus: 10, | |
color: PICKUP_COLORS.weapon | |
}); | |
} | |
if (level > 3 && !player.weapons.includes('MACHINE GUN')) { | |
let x, y; | |
do { | |
x = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
y = Math.floor(Math.random() * (size - 4) + 2) * 50 + 25; | |
} while (isWall(x, y)); | |
pickups.push({ | |
x: x, | |
y: y, | |
type: 'weapon', | |
weaponName: 'MACHINE GUN', | |
ammoBonus: 30, | |
color: PICKUP_COLORS.weapon | |
}); | |
} | |
// Update HUD | |
document.getElementById('levelValue').textContent = level; | |
document.getElementById('enemyCountValue').textContent = enemiesLeft; | |
updateHUD(); | |
} | |
// Start game | |
function startGame() { | |
document.getElementById('menu').style.display = 'none'; | |
isGameRunning = true; | |
currentLevel = 1; | |
generateLevel(currentLevel); | |
// Lock mouse pointer | |
canvas.requestPointerLock = canvas.requestPointerLock || | |
canvas.mozRequestPointerLock || | |
canvas.webkitRequestPointerLock; | |
canvas.requestPointerLock(); | |
} | |
// Restart game | |
function restartGame() { | |
document.getElementById('deathScreen').style.display = 'none'; | |
isGameRunning = true; | |
generateLevel(currentLevel); | |
// Lock mouse pointer | |
canvas.requestPointerLock = canvas.requestPointerLock || | |
canvas.mozRequestPointerLock || | |
canvas.webkitRequestPointerLock; | |
canvas.requestPointerLock(); | |
} | |
// Next level | |
function nextLevel() { | |
document.getElementById('winScreen').style.display = 'none'; | |
isGameRunning = true; | |
currentLevel++; | |
generateLevel(currentLevel); | |
// Lock mouse pointer | |
canvas.requestPointerLock = canvas.requestPointerLock || | |
canvas.mozRequestPointerLock || | |
canvas.webkitRequestPointerLock; | |
canvas.requestPointerLock(); | |
} | |
// Show menu | |
function showMenu() { | |
document.getElementById('menu').style.display = 'flex'; | |
document.getElementById('deathScreen').style.display = 'none'; | |
document.getElementById('winScreen').style.display = 'none'; | |
isGameRunning = false; | |
// Unlock mouse pointer | |
document.exitPointerLock = document.exitPointerLock || | |
document.mozExitPointerLock || | |
document.webkitExitPointerLock; | |
document.exitPointerLock(); | |
} | |
// Show how to play | |
function showHowToPlay() { | |
alert( | |
"HOW TO PLAY MATH DOOM:\n" + | |
"------------------------\n" + | |
"1. Move with W, A, S, D or arrow keys\n" + | |
"2. Aim with mouse and shoot with SPACE\n" + | |
"3. Switch weapons with 1, 2, 3 keys\n" + | |
"4. To pick up items, solve math problems\n" + | |
"5. Kill all enemies to complete the level\n" + | |
"6. Math problems get harder each level\n" + | |
"\n" + | |
"TIP: Shotgun is good for close range, machine gun for long range!" | |
); | |
} | |
// Game over | |
function gameOver() { | |
document.getElementById('deathScreen').style.display = 'flex'; | |
isGameRunning = false; | |
// Unlock mouse pointer | |
document.exitPointerLock = document.exitPointerLock || | |
document.mozExitPointerLock || | |
document.webkitExitPointerLock; | |
document.exitPointerLock(); | |
} | |
// Level complete | |
function levelComplete() { | |
document.getElementById('winScreen').style.display = 'flex'; | |
isGameRunning = false; | |
// Unlock mouse pointer | |
document.exitPointerLock = document.exitPointerLock || | |
document.mozExitPointerLock || | |
document.webkitExitPointerLock; | |
document.exitPointerLock(); | |
} | |
// Initialize game when page loads | |
window.onload = init; | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> | |
</html> |