|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>DOOM Style Game</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<style> |
|
body { |
|
margin: 0; |
|
overflow: hidden; |
|
background-color: #000; |
|
font-family: 'Courier New', monospace; |
|
color: white; |
|
touch-action: none; |
|
} |
|
#gameCanvas { |
|
display: block; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
#ui { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
padding: 20px; |
|
box-sizing: border-box; |
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
} |
|
#healthAmmo { |
|
display: flex; |
|
justify-content: space-between; |
|
width: 100%; |
|
max-width: 600px; |
|
margin-bottom: 10px; |
|
} |
|
#weapon { |
|
font-size: 24px; |
|
margin-bottom: 10px; |
|
text-shadow: 0 0 5px red; |
|
} |
|
#startScreen { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(0, 0, 0, 0.9); |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: center; |
|
align-items: center; |
|
color: red; |
|
text-align: center; |
|
z-index: 10; |
|
} |
|
#startButton { |
|
padding: 15px 30px; |
|
font-size: 20px; |
|
background-color: #8B0000; |
|
color: white; |
|
border: 2px solid #FF0000; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
margin-top: 30px; |
|
font-family: 'Courier New', monospace; |
|
text-transform: uppercase; |
|
letter-spacing: 2px; |
|
} |
|
#startButton:hover { |
|
background-color: #FF0000; |
|
} |
|
#crosshair { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
width: 20px; |
|
height: 20px; |
|
transform: translate(-50%, -50%); |
|
pointer-events: none; |
|
} |
|
#crosshair::before, #crosshair::after { |
|
content: ''; |
|
position: absolute; |
|
background-color: red; |
|
} |
|
#crosshair::before { |
|
width: 20px; |
|
height: 2px; |
|
top: 9px; |
|
left: 0; |
|
} |
|
#crosshair::after { |
|
width: 2px; |
|
height: 20px; |
|
left: 9px; |
|
top: 0; |
|
} |
|
#gameOverScreen { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(0, 0, 0, 0.9); |
|
display: none; |
|
flex-direction: column; |
|
justify-content: center; |
|
align-items: center; |
|
color: red; |
|
text-align: center; |
|
z-index: 10; |
|
} |
|
#restartButton { |
|
padding: 15px 30px; |
|
font-size: 20px; |
|
background-color: #8B0000; |
|
color: white; |
|
border: 2px solid #FF0000; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
margin-top: 30px; |
|
font-family: 'Courier New', monospace; |
|
text-transform: uppercase; |
|
letter-spacing: 2px; |
|
} |
|
#restartButton:hover { |
|
background-color: #FF0000; |
|
} |
|
#hud { |
|
position: absolute; |
|
top: 10px; |
|
left: 10px; |
|
font-size: 16px; |
|
color: white; |
|
text-shadow: 0 0 5px black; |
|
} |
|
#enemiesLeft { |
|
position: absolute; |
|
top: 10px; |
|
right: 10px; |
|
font-size: 16px; |
|
color: white; |
|
text-shadow: 0 0 5px black; |
|
} |
|
.health-bar, .ammo-bar { |
|
width: 200px; |
|
height: 20px; |
|
border: 2px solid #333; |
|
border-radius: 3px; |
|
overflow: hidden; |
|
position: relative; |
|
} |
|
.health-fill { |
|
height: 100%; |
|
background: linear-gradient(to right, #8B0000, #FF0000); |
|
transition: width 0.3s; |
|
} |
|
.ammo-fill { |
|
height: 100%; |
|
background: linear-gradient(to right, #006400, #00FF00); |
|
transition: width 0.3s; |
|
} |
|
#weaponImage { |
|
width: 200px; |
|
height: 100px; |
|
background-size: contain; |
|
background-repeat: no-repeat; |
|
background-position: center; |
|
margin-bottom: 10px; |
|
} |
|
#bloodEffect { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(255, 0, 0, 0); |
|
pointer-events: none; |
|
transition: background-color 0.1s; |
|
z-index: 5; |
|
} |
|
#damageIndicator { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
font-size: 24px; |
|
color: red; |
|
opacity: 0; |
|
pointer-events: none; |
|
transition: opacity 0.3s; |
|
text-shadow: 0 0 5px black; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<canvas id="gameCanvas"></canvas> |
|
<div id="crosshair"></div> |
|
<div id="bloodEffect"></div> |
|
<div id="damageIndicator">HIT!</div> |
|
|
|
<div id="hud"> |
|
<div>Level: <span id="level">1</span></div> |
|
<div>Kills: <span id="kills">0</span></div> |
|
</div> |
|
|
|
<div id="enemiesLeft"> |
|
Enemies: <span id="enemiesCount">0</span> |
|
</div> |
|
|
|
<div id="ui"> |
|
<div id="weaponImage"></div> |
|
<div id="weapon">PISTOL</div> |
|
<div id="healthAmmo"> |
|
<div> |
|
<div>HEALTH</div> |
|
<div class="health-bar"> |
|
<div class="health-fill" id="healthBar"></div> |
|
</div> |
|
</div> |
|
<div> |
|
<div>AMMO</div> |
|
<div class="ammo-bar"> |
|
<div class="ammo-fill" id="ammoBar"></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="startScreen"> |
|
<h1 class="text-4xl font-bold mb-4">DOOM STYLE GAME</h1> |
|
<p class="text-xl mb-8">KILL ALL DEMONS TO ADVANCE TO THE NEXT LEVEL</p> |
|
<p class="mb-2">WASD - Move</p> |
|
<p class="mb-2">Mouse - Look and Shoot</p> |
|
<p class="mb-2">R - Reload</p> |
|
<p class="mb-2">1-3 - Switch Weapons</p> |
|
<p class="mb-2">Space - Jump</p> |
|
<button id="startButton">START GAME</button> |
|
</div> |
|
|
|
<div id="gameOverScreen"> |
|
<h1 class="text-4xl font-bold mb-4">GAME OVER</h1> |
|
<p class="text-xl mb-2">You killed <span id="finalKills">0</span> demons</p> |
|
<p class="text-xl mb-8">Reached level <span id="finalLevel">1</span></p> |
|
<button id="restartButton">TRY AGAIN</button> |
|
</div> |
|
|
|
<script> |
|
|
|
const canvas = document.getElementById('gameCanvas'); |
|
const ctx = canvas.getContext('2d'); |
|
const startScreen = document.getElementById('startScreen'); |
|
const startButton = document.getElementById('startButton'); |
|
const gameOverScreen = document.getElementById('gameOverScreen'); |
|
const restartButton = document.getElementById('restartButton'); |
|
const weaponImage = document.getElementById('weaponImage'); |
|
const weaponDisplay = document.getElementById('weapon'); |
|
const healthBar = document.getElementById('healthBar'); |
|
const ammoBar = document.getElementById('ammoBar'); |
|
const levelDisplay = document.getElementById('level'); |
|
const killsDisplay = document.getElementById('kills'); |
|
const enemiesCountDisplay = document.getElementById('enemiesCount'); |
|
const finalKillsDisplay = document.getElementById('finalKills'); |
|
const finalLevelDisplay = document.getElementById('finalLevel'); |
|
const bloodEffect = document.getElementById('bloodEffect'); |
|
const damageIndicator = document.getElementById('damageIndicator'); |
|
|
|
|
|
canvas.width = window.innerWidth; |
|
canvas.height = window.innerHeight; |
|
|
|
|
|
let gameRunning = false; |
|
let level = 1; |
|
let kills = 0; |
|
let enemiesLeft = 0; |
|
|
|
|
|
const player = { |
|
x: 1.5, |
|
y: 1.5, |
|
dirX: -1, |
|
dirY: 0, |
|
planeX: 0, |
|
planeY: 0.66, |
|
moveSpeed: 0.05, |
|
rotSpeed: 0.03, |
|
health: 100, |
|
weapons: [ |
|
{ |
|
name: "PISTOL", |
|
damage: 25, |
|
ammo: 12, |
|
maxAmmo: 12, |
|
reloadTime: 1000, |
|
fireRate: 500, |
|
range: 10, |
|
accuracy: 0.95, |
|
color: "#888", |
|
image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 100'%3E%3Crect x='20' y='40' width='120' height='20' fill='%23ccc'/%3E%3Crect x='140' y='30' width='40' height='40' fill='%23aaa'/%3E%3Crect x='180' y='40' width='10' height='20' fill='%23888'/%3E%3C/svg%3E" |
|
}, |
|
{ |
|
name: "SHOTGUN", |
|
damage: 50, |
|
ammo: 6, |
|
maxAmmo: 6, |
|
reloadTime: 1500, |
|
fireRate: 1000, |
|
range: 5, |
|
accuracy: 0.7, |
|
color: "#964B00", |
|
image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 100'%3E%3Crect x='20' y='40' width='150' height='20' fill='%23b87333'/%3E%3Crect x='170' y='20' width='20' height='60' fill='%238B4513'/%3E%3C/svg%3E" |
|
}, |
|
{ |
|
name: "CHAINGUN", |
|
damage: 15, |
|
ammo: 50, |
|
maxAmmo: 50, |
|
reloadTime: 2000, |
|
fireRate: 100, |
|
range: 15, |
|
accuracy: 0.85, |
|
color: "#333", |
|
image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 100'%3E%3Crect x='20' y='40' width='150' height='20' fill='%23444'/%3E%3Crect x='170' y='30' width='20' height='40' fill='%23222'/%3E%3Ccircle cx='40' cy='50' r='15' fill='%23555'/%3E%3C/svg%3E" |
|
} |
|
], |
|
currentWeapon: 0, |
|
lastShot: 0, |
|
reloading: false, |
|
isMoving: false, |
|
isShooting: false, |
|
jumpHeight: 0, |
|
isJumping: false |
|
}; |
|
|
|
|
|
let map = [ |
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
|
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1], |
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1] |
|
]; |
|
|
|
|
|
const wallTextures = [ |
|
'#8B0000', |
|
'#006400', |
|
'#00008B', |
|
'#4B0082', |
|
'#8B4513' |
|
]; |
|
|
|
|
|
let enemies = []; |
|
|
|
|
|
function generateLevel() { |
|
|
|
enemies = []; |
|
|
|
|
|
const size = 10 + level * 2; |
|
map = Array(size).fill().map(() => Array(size).fill(1)); |
|
|
|
|
|
const stack = []; |
|
const visited = Array(size).fill().map(() => Array(size).fill(false)); |
|
|
|
|
|
player.x = 1.5; |
|
player.y = 1.5; |
|
map[1][1] = 0; |
|
visited[1][1] = true; |
|
stack.push([1, 1]); |
|
|
|
while (stack.length > 0) { |
|
const [x, y] = stack[stack.length - 1]; |
|
const directions = [ |
|
[0, 1], [1, 0], [0, -1], [-1, 0] |
|
].sort(() => Math.random() - 0.5); |
|
|
|
let moved = false; |
|
|
|
for (const [dx, dy] of directions) { |
|
const nx = x + dx * 2; |
|
const ny = y + dy * 2; |
|
|
|
if (nx > 0 && nx < size - 1 && ny > 0 && ny < size - 1 && !visited[nx][ny]) { |
|
map[x + dx][y + dy] = 0; |
|
map[nx][ny] = 0; |
|
visited[nx][ny] = true; |
|
stack.push([nx, ny]); |
|
moved = true; |
|
break; |
|
} |
|
} |
|
|
|
if (!moved) { |
|
stack.pop(); |
|
} |
|
} |
|
|
|
|
|
for (let i = 1; i < size - 1; i++) { |
|
for (let j = 1; j < size - 1; j++) { |
|
if (map[i][j] === 0 && Math.random() < 0.1) { |
|
map[i][j] = 2 + Math.floor(Math.random() * (wallTextures.length - 1)); |
|
} |
|
} |
|
} |
|
|
|
|
|
const exitX = size - 2; |
|
const exitY = size - 2; |
|
map[exitX][exitY] = 0; |
|
|
|
|
|
enemiesLeft = 5 + level * 3; |
|
for (let i = 0; i < enemiesLeft; i++) { |
|
let x, y; |
|
do { |
|
x = 1 + Math.floor(Math.random() * (size - 2)); |
|
y = 1 + Math.floor(Math.random() * (size - 2)); |
|
} while (map[x][y] !== 0 || (x === 1 && y === 1) || (x === exitX && y === exitY)); |
|
|
|
enemies.push({ |
|
x: x + 0.5, |
|
y: y + 0.5, |
|
health: 50 + level * 10, |
|
speed: 0.02 + level * 0.005, |
|
damage: 10 + level * 2, |
|
color: `hsl(${Math.random() * 60}, 100%, 50%)`, |
|
lastAttack: 0, |
|
attackCooldown: 1000, |
|
size: 0.5 |
|
}); |
|
} |
|
|
|
|
|
enemiesCountDisplay.textContent = enemiesLeft; |
|
} |
|
|
|
|
|
function castRays() { |
|
const width = canvas.width; |
|
const height = canvas.height; |
|
|
|
for (let x = 0; x < width; x++) { |
|
|
|
const cameraX = 2 * x / width - 1; |
|
const rayDirX = player.dirX + player.planeX * cameraX; |
|
const rayDirY = player.dirY + player.planeY * cameraX; |
|
|
|
|
|
let mapX = Math.floor(player.x); |
|
let mapY = Math.floor(player.y); |
|
|
|
|
|
let sideDistX, sideDistY; |
|
|
|
|
|
const deltaDistX = Math.abs(1 / rayDirX); |
|
const deltaDistY = Math.abs(1 / rayDirY); |
|
|
|
|
|
let stepX, stepY; |
|
|
|
|
|
let hit = false; |
|
|
|
let side; |
|
|
|
let perpWallDist; |
|
|
|
|
|
if (rayDirX < 0) { |
|
stepX = -1; |
|
sideDistX = (player.x - mapX) * deltaDistX; |
|
} else { |
|
stepX = 1; |
|
sideDistX = (mapX + 1.0 - player.x) * deltaDistX; |
|
} |
|
|
|
if (rayDirY < 0) { |
|
stepY = -1; |
|
sideDistY = (player.y - mapY) * deltaDistY; |
|
} else { |
|
stepY = 1; |
|
sideDistY = (mapY + 1.0 - player.y) * deltaDistY; |
|
} |
|
|
|
|
|
while (!hit) { |
|
|
|
if (sideDistX < sideDistY) { |
|
sideDistX += deltaDistX; |
|
mapX += stepX; |
|
side = 0; |
|
} else { |
|
sideDistY += deltaDistY; |
|
mapY += stepY; |
|
side = 1; |
|
} |
|
|
|
|
|
if (mapX < 0 || mapX >= map.length || mapY < 0 || mapY >= map[0].length) { |
|
hit = true; |
|
} else if (map[mapX][mapY] > 0) { |
|
hit = true; |
|
} |
|
} |
|
|
|
|
|
if (side === 0) { |
|
perpWallDist = (mapX - player.x + (1 - stepX) / 2) / rayDirX; |
|
} else { |
|
perpWallDist = (mapY - player.y + (1 - stepY) / 2) / rayDirY; |
|
} |
|
|
|
|
|
let lineHeight = Math.floor(height / perpWallDist); |
|
|
|
|
|
let drawStart = -lineHeight / 2 + height / 2; |
|
if (drawStart < 0) drawStart = 0; |
|
let drawEnd = lineHeight / 2 + height / 2; |
|
if (drawEnd >= height) drawEnd = height - 1; |
|
|
|
|
|
let color; |
|
if (mapX < 0 || mapX >= map.length || mapY < 0 || mapY >= map[0].length) { |
|
color = '#000'; |
|
} else { |
|
const wallType = map[mapX][mapY]; |
|
color = wallTextures[wallType - 1] || '#FFF'; |
|
} |
|
|
|
|
|
if (side === 1) { |
|
color = shadeColor(color, -30); |
|
} |
|
|
|
|
|
ctx.fillStyle = color; |
|
ctx.fillRect(x, drawStart + player.jumpHeight, 1, drawEnd - drawStart); |
|
|
|
|
|
ctx.fillStyle = '#333'; |
|
ctx.fillRect(x, drawEnd + player.jumpHeight, 1, height - drawEnd); |
|
} |
|
} |
|
|
|
|
|
function drawEnemies() { |
|
const width = canvas.width; |
|
const height = canvas.height; |
|
|
|
|
|
enemies.sort((a, b) => { |
|
const distA = Math.pow(player.x - a.x, 2) + Math.pow(player.y - a.y, 2); |
|
const distB = Math.pow(player.x - b.x, 2) + Math.pow(player.y - b.y, 2); |
|
return distB - distA; |
|
}); |
|
|
|
for (const enemy of enemies) { |
|
|
|
const relX = enemy.x - player.x; |
|
const relY = enemy.y - player.y; |
|
|
|
|
|
const invDet = 1.0 / (player.planeX * player.dirY - player.dirX * player.planeY); |
|
const transformX = invDet * (player.dirY * relX - player.dirX * relY); |
|
const transformY = invDet * (-player.planeY * relX + player.planeX * relY); |
|
|
|
|
|
if (transformY <= 0) continue; |
|
|
|
|
|
const spriteScreenX = Math.floor((width / 2) * (1 + transformX / transformY)); |
|
|
|
|
|
const spriteHeight = Math.abs(Math.floor(height / transformY)); |
|
const spriteWidth = spriteHeight; |
|
|
|
|
|
let drawStartX = -spriteWidth / 2 + spriteScreenX; |
|
let drawEndX = spriteWidth / 2 + spriteScreenX; |
|
let drawStartY = -spriteHeight / 2 + height / 2; |
|
let drawEndY = spriteHeight / 2 + height / 2; |
|
|
|
|
|
if (drawStartX < 0) drawStartX = 0; |
|
if (drawEndX >= width) drawEndX = width - 1; |
|
if (drawStartY < 0) drawStartY = 0; |
|
if (drawEndY >= height) drawEndY = height - 1; |
|
|
|
|
|
for (let stripe = drawStartX; stripe < drawEndX; stripe++) { |
|
const texX = Math.floor((stripe - (-spriteWidth / 2 + spriteScreenX)) * enemy.size / spriteWidth); |
|
|
|
if (transformY > 0 && stripe > 0 && stripe < width) { |
|
for (let y = drawStartY; y < drawEndY; y++) { |
|
const d = (y - (-spriteHeight / 2 + height / 2)) * 256 / spriteHeight; |
|
const texY = Math.floor(d * enemy.size / spriteHeight); |
|
|
|
|
|
if (texX >= 0 && texX < enemy.size * 100 && texY >= 0 && texY < enemy.size * 100) { |
|
ctx.fillStyle = enemy.color; |
|
ctx.fillRect(stripe, y + player.jumpHeight, 1, 1); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function drawWeapon() { |
|
const weapon = player.weapons[player.currentWeapon]; |
|
weaponImage.style.backgroundImage = `url("${weapon.image}")`; |
|
weaponDisplay.textContent = weapon.name; |
|
|
|
|
|
let bobOffset = 0; |
|
if (player.isMoving) { |
|
bobOffset = Math.sin(Date.now() / 100) * 5; |
|
} |
|
|
|
|
|
let recoilOffset = 0; |
|
if (player.isShooting) { |
|
recoilOffset = Math.sin(Date.now() / 50) * 10; |
|
} |
|
|
|
weaponImage.style.transform = `translateY(${bobOffset + recoilOffset}px)`; |
|
} |
|
|
|
|
|
function updateHUD() { |
|
const weapon = player.weapons[player.currentWeapon]; |
|
healthBar.style.width = `${player.health}%`; |
|
ammoBar.style.width = `${(weapon.ammo / weapon.maxAmmo) * 100}%`; |
|
levelDisplay.textContent = level; |
|
killsDisplay.textContent = kills; |
|
enemiesCountDisplay.textContent = enemiesLeft; |
|
|
|
|
|
if (player.health < 30) { |
|
bloodEffect.style.backgroundColor = `rgba(255, 0, 0, ${0.3 - (player.health / 100)})`; |
|
} else { |
|
bloodEffect.style.backgroundColor = 'rgba(255, 0, 0, 0)'; |
|
} |
|
} |
|
|
|
|
|
function movePlayer() { |
|
if (!gameRunning) return; |
|
|
|
|
|
const moveSpeed = player.moveSpeed; |
|
const rotSpeed = player.rotSpeed; |
|
|
|
|
|
if (keys.ArrowLeft) { |
|
const oldDirX = player.dirX; |
|
player.dirX = player.dirX * Math.cos(rotSpeed) - player.dirY * Math.sin(rotSpeed); |
|
player.dirY = oldDirX * Math.sin(rotSpeed) + player.dirY * Math.cos(rotSpeed); |
|
|
|
const oldPlaneX = player.planeX; |
|
player.planeX = player.planeX * Math.cos(rotSpeed) - player.planeY * Math.sin(rotSpeed); |
|
player.planeY = oldPlaneX * Math.sin(rotSpeed) + player.planeY * Math.cos(rotSpeed); |
|
} |
|
|
|
if (keys.ArrowRight) { |
|
const oldDirX = player.dirX; |
|
player.dirX = player.dirX * Math.cos(-rotSpeed) - player.dirY * Math.sin(-rotSpeed); |
|
player.dirY = oldDirX * Math.sin(-rotSpeed) + player.dirY * Math.cos(-rotSpeed); |
|
|
|
const oldPlaneX = player.planeX; |
|
player.planeX = player.planeX * Math.cos(-rotSpeed) - player.planeY * Math.sin(-rotSpeed); |
|
player.planeY = oldPlaneX * Math.sin(-rotSpeed) + player.planeY * Math.cos(-rotSpeed); |
|
} |
|
|
|
|
|
let moveX = 0, moveY = 0; |
|
player.isMoving = false; |
|
|
|
if (keys.w) { |
|
moveX += player.dirX * moveSpeed; |
|
moveY += player.dirY * moveSpeed; |
|
player.isMoving = true; |
|
} |
|
|
|
if (keys.s) { |
|
moveX -= player.dirX * moveSpeed; |
|
moveY -= player.dirY * moveSpeed; |
|
player.isMoving = true; |
|
} |
|
|
|
|
|
if (keys.a) { |
|
moveX -= player.planeX * moveSpeed; |
|
moveY -= player.planeY * moveSpeed; |
|
player.isMoving = true; |
|
} |
|
|
|
if (keys.d) { |
|
moveX += player.planeX * moveSpeed; |
|
moveY += player.planeY * moveSpeed; |
|
player.isMoving = true; |
|
} |
|
|
|
|
|
if (keys[' '] && !player.isJumping) { |
|
player.isJumping = true; |
|
player.jumpHeight = -20; |
|
} |
|
|
|
|
|
if (player.isJumping) { |
|
player.jumpHeight += 2; |
|
if (player.jumpHeight >= 0) { |
|
player.jumpHeight = 0; |
|
player.isJumping = false; |
|
} |
|
} |
|
|
|
|
|
if (map[Math.floor(player.x + moveX)][Math.floor(player.y)] === 0) { |
|
player.x += moveX; |
|
} |
|
|
|
if (map[Math.floor(player.x)][Math.floor(player.y + moveY)] === 0) { |
|
player.y += moveY; |
|
} |
|
|
|
|
|
if (Math.floor(player.x) === map.length - 2 && Math.floor(player.y) === map[0].length - 2) { |
|
if (enemiesLeft === 0) { |
|
level++; |
|
generateLevel(); |
|
} |
|
} |
|
} |
|
|
|
|
|
function updateEnemies() { |
|
const now = Date.now(); |
|
|
|
for (let i = enemies.length - 1; i >= 0; i--) { |
|
const enemy = enemies[i]; |
|
|
|
|
|
const dx = player.x - enemy.x; |
|
const dy = player.y - enemy.y; |
|
const dist = Math.sqrt(dx * dx + dy * dy); |
|
|
|
if (dist > 0.5) { |
|
enemy.x += (dx / dist) * enemy.speed; |
|
enemy.y += (dy / dist) * enemy.speed; |
|
|
|
|
|
if (map[Math.floor(enemy.x)][Math.floor(enemy.y)] !== 0) { |
|
enemy.x -= (dx / dist) * enemy.speed; |
|
enemy.y -= (dy / dist) * enemy.speed; |
|
} |
|
} |
|
|
|
|
|
if (dist < 1.5 && now - enemy.lastAttack > enemy.attackCooldown) { |
|
player.health -= enemy.damage; |
|
enemy.lastAttack = now; |
|
|
|
|
|
damageIndicator.style.opacity = 1; |
|
setTimeout(() => { |
|
damageIndicator.style.opacity = 0; |
|
}, 300); |
|
|
|
|
|
bloodEffect.style.backgroundColor = 'rgba(255, 0, 0, 0.5)'; |
|
setTimeout(() => { |
|
bloodEffect.style.backgroundColor = 'rgba(255, 0, 0, 0)'; |
|
}, 100); |
|
|
|
|
|
if (player.health <= 0) { |
|
gameOver(); |
|
} |
|
} |
|
|
|
|
|
if (enemy.health <= 0) { |
|
enemies.splice(i, 1); |
|
enemiesLeft--; |
|
kills++; |
|
|
|
|
|
enemiesCountDisplay.textContent = enemiesLeft; |
|
killsDisplay.textContent = kills; |
|
} |
|
} |
|
} |
|
|
|
|
|
function shoot() { |
|
const now = Date.now(); |
|
const weapon = player.weapons[player.currentWeapon]; |
|
|
|
|
|
if (now - player.lastShot < weapon.fireRate || player.reloading || weapon.ammo <= 0) { |
|
return; |
|
} |
|
|
|
player.lastShot = now; |
|
player.isShooting = true; |
|
setTimeout(() => { |
|
player.isShooting = false; |
|
}, 100); |
|
|
|
|
|
weapon.ammo--; |
|
|
|
|
|
for (let i = 0; i < enemies.length; i++) { |
|
const enemy = enemies[i]; |
|
|
|
|
|
const dx = enemy.x - player.x; |
|
const dy = enemy.y - player.y; |
|
const dist = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
|
const playerAngle = Math.atan2(player.dirY, player.dirX); |
|
const enemyAngle = Math.atan2(dy, dx); |
|
let angleDiff = Math.abs(playerAngle - enemyAngle); |
|
|
|
|
|
if (angleDiff > Math.PI) { |
|
angleDiff = 2 * Math.PI - angleDiff; |
|
} |
|
|
|
|
|
if (dist < weapon.range && angleDiff < 0.5 * weapon.accuracy) { |
|
|
|
enemy.health -= weapon.damage; |
|
|
|
|
|
damageIndicator.style.opacity = 1; |
|
setTimeout(() => { |
|
damageIndicator.style.opacity = 0; |
|
}, 300); |
|
|
|
|
|
enemy.color = `hsl(${Math.random() * 60}, 100%, 70%)`; |
|
setTimeout(() => { |
|
enemy.color = `hsl(${Math.random() * 60}, 100%, 50%)`; |
|
}, 100); |
|
} |
|
} |
|
|
|
|
|
if (weapon.ammo <= 0) { |
|
reload(); |
|
} |
|
} |
|
|
|
|
|
function reload() { |
|
if (player.reloading) return; |
|
|
|
const weapon = player.weapons[player.currentWeapon]; |
|
if (weapon.ammo === weapon.maxAmmo) return; |
|
|
|
player.reloading = true; |
|
setTimeout(() => { |
|
weapon.ammo = weapon.maxAmmo; |
|
player.reloading = false; |
|
}, weapon.reloadTime); |
|
} |
|
|
|
|
|
function gameOver() { |
|
gameRunning = false; |
|
finalKillsDisplay.textContent = kills; |
|
finalLevelDisplay.textContent = level; |
|
gameOverScreen.style.display = 'flex'; |
|
} |
|
|
|
|
|
function gameLoop() { |
|
if (!gameRunning) return; |
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
ctx.fillStyle = '#111'; |
|
ctx.fillRect(0, 0, canvas.width, canvas.height / 2); |
|
|
|
|
|
castRays(); |
|
|
|
|
|
drawEnemies(); |
|
|
|
|
|
movePlayer(); |
|
updateEnemies(); |
|
|
|
|
|
drawWeapon(); |
|
|
|
|
|
updateHUD(); |
|
|
|
|
|
requestAnimationFrame(gameLoop); |
|
} |
|
|
|
|
|
const keys = { |
|
w: false, |
|
a: false, |
|
s: false, |
|
d: false, |
|
' ': false, |
|
ArrowLeft: false, |
|
ArrowRight: false, |
|
r: false |
|
}; |
|
|
|
document.addEventListener('keydown', (e) => { |
|
if (e.key in keys) keys[e.key] = true; |
|
|
|
|
|
if (e.key >= '1' && e.key <= '3') { |
|
player.currentWeapon = parseInt(e.key) - 1; |
|
} |
|
|
|
|
|
if (e.key === 'r') { |
|
reload(); |
|
} |
|
}); |
|
|
|
document.addEventListener('keyup', (e) => { |
|
if (e.key in keys) keys[e.key] = false; |
|
}); |
|
|
|
|
|
let mouseX = 0; |
|
let mouseDown = false; |
|
|
|
canvas.addEventListener('mousedown', () => { |
|
mouseDown = true; |
|
if (gameRunning) shoot(); |
|
}); |
|
|
|
canvas.addEventListener('mouseup', () => { |
|
mouseDown = false; |
|
}); |
|
|
|
canvas.addEventListener('mousemove', (e) => { |
|
if (!gameRunning) return; |
|
|
|
const movementX = e.movementX || 0; |
|
|
|
|
|
if (movementX !== 0) { |
|
const rotSpeed = 0.002 * movementX; |
|
const oldDirX = player.dirX; |
|
player.dirX = player.dirX * Math.cos(rotSpeed) - player.dirY * Math.sin(rotSpeed); |
|
player.dirY = oldDirX * Math.sin(rotSpeed) + player.dirY * Math.cos(rotSpeed); |
|
|
|
const oldPlaneX = player.planeX; |
|
player.planeX = player.planeX * Math.cos(rotSpeed) - player.planeY * Math.sin(rotSpeed); |
|
player.planeY = oldPlaneX * Math.sin(rotSpeed) + player.planeY * Math.cos(rotSpeed); |
|
} |
|
}); |
|
|
|
|
|
let touchStartX = 0; |
|
|
|
canvas.addEventListener('touchstart', (e) => { |
|
e.preventDefault(); |
|
touchStartX = e.touches[0].clientX; |
|
if (gameRunning) shoot(); |
|
}); |
|
|
|
canvas.addEventListener('touchmove', (e) => { |
|
e.preventDefault(); |
|
if (!gameRunning) return; |
|
|
|
const touchX = e.touches[0].clientX; |
|
const movementX = (touchX - touchStartX) * 0.1; |
|
touchStartX = touchX; |
|
|
|
|
|
if (movementX !== 0) { |
|
const rotSpeed = 0.002 * movementX; |
|
const oldDirX = player.dirX; |
|
player.dirX = player.dirX * Math.cos(rotSpeed) - player.dirY * Math.sin(rotSpeed); |
|
player.dirY = oldDirX * Math.sin(rotSpeed) + player.dirY * Math.cos(rotSpeed); |
|
|
|
const oldPlaneX = player.planeX; |
|
player.planeX = player.planeX * Math.cos(rotSpeed) - player.planeY * Math.sin(rotSpeed); |
|
player.planeY = oldPlaneX * Math.sin(rotSpeed) + player.planeY * Math.cos(rotSpeed); |
|
} |
|
}); |
|
|
|
canvas.addEventListener('touchend', (e) => { |
|
e.preventDefault(); |
|
}); |
|
|
|
|
|
function startGame() { |
|
startScreen.style.display = 'none'; |
|
gameRunning = true; |
|
|
|
|
|
level = 1; |
|
kills = 0; |
|
player.health = 100; |
|
player.currentWeapon = 0; |
|
player.weapons.forEach(w => w.ammo = w.maxAmmo); |
|
|
|
|
|
generateLevel(); |
|
|
|
|
|
gameLoop(); |
|
} |
|
|
|
|
|
function restartGame() { |
|
gameOverScreen.style.display = 'none'; |
|
startGame(); |
|
} |
|
|
|
|
|
startButton.addEventListener('click', startGame); |
|
restartButton.addEventListener('click', restartGame); |
|
|
|
|
|
window.addEventListener('resize', () => { |
|
canvas.width = window.innerWidth; |
|
canvas.height = window.innerHeight; |
|
}); |
|
|
|
|
|
function shadeColor(color, percent) { |
|
let R = parseInt(color.substring(1, 3), 16); |
|
let G = parseInt(color.substring(3, 5), 16); |
|
let B = parseInt(color.substring(5, 7), 16); |
|
|
|
R = parseInt(R * (100 + percent) / 100); |
|
G = parseInt(G * (100 + percent) / 100); |
|
B = parseInt(B * (100 + percent) / 100); |
|
|
|
R = R < 255 ? R : 255; |
|
G = G < 255 ? G : 255; |
|
B = B < 255 ? B : 255; |
|
|
|
const RR = R.toString(16).length === 1 ? '0' + R.toString(16) : R.toString(16); |
|
const GG = G.toString(16).length === 1 ? '0' + G.toString(16) : G.toString(16); |
|
const BB = B.toString(16).length === 1 ? '0' + B.toString(16) : B.toString(16); |
|
|
|
return '#' + RR + GG + BB; |
|
} |
|
</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 <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Greats/clone" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |