1270 lines
51 KiB
HTML
1270 lines
51 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Zombie Survival Shooter</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d30 100%);
|
|
font-family: 'Courier New', monospace;
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
}
|
|
|
|
canvas {
|
|
display: block;
|
|
background: linear-gradient(45deg, #3a3a3a 0%, #4a4a4a 100%);
|
|
image-rendering: pixelated;
|
|
image-rendering: -moz-crisp-edges;
|
|
image-rendering: crisp-edges;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
|
|
.ui {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: white;
|
|
font-size: 18px;
|
|
z-index: 10;
|
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
.weapon-info {
|
|
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8) 0%, rgba(20, 20, 20, 0.9) 100%);
|
|
padding: 15px 20px;
|
|
border-radius: 10px;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(5px);
|
|
}
|
|
|
|
.weapon-info div {
|
|
margin: 5px 0;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.weapon-info span {
|
|
color: #00ff88;
|
|
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
|
}
|
|
|
|
#score {
|
|
color: #ffaa00 !important;
|
|
text-shadow: 0 0 10px rgba(255, 170, 0, 0.5) !important;
|
|
}
|
|
|
|
#health {
|
|
color: #ff4444 !important;
|
|
text-shadow: 0 0 10px rgba(255, 68, 68, 0.5) !important;
|
|
}
|
|
|
|
.game-over-screen {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: rgba(0, 0, 0, 0.9);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.game-over-content {
|
|
text-align: center;
|
|
color: white;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.game-over-content h1 {
|
|
font-size: 4rem;
|
|
color: #ff4444;
|
|
text-shadow: 0 0 20px rgba(255, 68, 68, 0.8);
|
|
margin-bottom: 30px;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.7; }
|
|
}
|
|
|
|
.score-display {
|
|
font-size: 1.5rem;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.score-display div {
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.score-display span {
|
|
color: #00ff88;
|
|
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
|
|
}
|
|
|
|
.restart-btn {
|
|
background: linear-gradient(135deg, #ff4444 0%, #cc0000 100%);
|
|
border: none;
|
|
color: white;
|
|
padding: 15px 30px;
|
|
font-size: 1.2rem;
|
|
font-family: 'Courier New', monospace;
|
|
font-weight: bold;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
box-shadow: 0 4px 15px rgba(255, 68, 68, 0.3);
|
|
}
|
|
|
|
.restart-btn:hover {
|
|
background: linear-gradient(135deg, #ff6666 0%, #ff0000 100%);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(255, 68, 68, 0.5);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="gameCanvas"></canvas>
|
|
<div class="ui">
|
|
<div class="weapon-info">
|
|
<div>Weapon: <span id="weaponName">Pistol</span></div>
|
|
<div>Ammo: <span id="ammoCount">∞</span></div>
|
|
<div>Health: <span id="health">100</span></div>
|
|
<div>Score: <span id="score">0</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="gameOverScreen" class="game-over-screen" style="display: none;">
|
|
<div class="game-over-content">
|
|
<h1>GAME OVER</h1>
|
|
<div class="score-display">
|
|
<div>Final Score: <span id="finalScore">0</span></div>
|
|
<div>High Score: <span id="highScore">0</span></div>
|
|
</div>
|
|
<button id="restartButton" class="restart-btn">RESTART GAME</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Make canvas fullscreen
|
|
function resizeCanvas() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
// Game state
|
|
const game = {
|
|
player: null,
|
|
zombies: [],
|
|
bullets: [],
|
|
weapons: [],
|
|
weaponDrops: [],
|
|
score: 0,
|
|
keys: {},
|
|
mouse: { x: 0, y: 0, down: false },
|
|
map: null,
|
|
camera: { x: 0, y: 0 },
|
|
tileSize: 32,
|
|
mapWidth: 50,
|
|
mapHeight: 40,
|
|
pathfindingCache: new Map(),
|
|
maxCacheSize: 100,
|
|
pathfindingQueue: [],
|
|
maxPathfindingPerFrame: 2,
|
|
gameOver: false,
|
|
highScore: localStorage.getItem('zombieHighScore') || 0
|
|
};
|
|
|
|
// Audio loader
|
|
const sounds = {
|
|
gunshot: new Audio('386845__morganpurkis__single-gunshot-52.wav'),
|
|
zombieGrunt: new Audio('426627__mrh4hn__zombie-grunt.wav'),
|
|
playerOuch: new Audio('421877__sventhors__ouch_1.wav')
|
|
};
|
|
|
|
// Set volume levels
|
|
sounds.gunshot.volume = 0.3;
|
|
sounds.zombieGrunt.volume = 0.5;
|
|
sounds.playerOuch.volume = 0.7;
|
|
|
|
// Sprite loader
|
|
const sprites = {};
|
|
const spriteList = [
|
|
'main-character.png',
|
|
'zombie.png', 'zombie2.png', 'zombie3.png', 'zombie4.png',
|
|
'pistol.png', 'shotgun.png', 'burst-rifle.png', 'machine-gun.png',
|
|
'bullet.png', 'bullet-shotgun.png',
|
|
'zombie-dead.png'
|
|
];
|
|
|
|
let spritesLoaded = 0;
|
|
|
|
function loadSprites() {
|
|
spriteList.forEach(spriteName => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
spritesLoaded++;
|
|
if (spritesLoaded === spriteList.length) {
|
|
initGame();
|
|
}
|
|
};
|
|
img.src = spriteName;
|
|
sprites[spriteName] = img;
|
|
});
|
|
}
|
|
|
|
// Weapon definitions
|
|
const weaponTypes = {
|
|
pistol: {
|
|
name: 'Pistol',
|
|
ammo: Infinity,
|
|
damage: 25,
|
|
fireRate: 300,
|
|
spread: 0,
|
|
bulletsPerShot: 1,
|
|
range: 400,
|
|
autoFire: false,
|
|
burstCount: 1,
|
|
sprite: 'pistol.png'
|
|
},
|
|
shotgun: {
|
|
name: 'Shotgun',
|
|
ammo: 8,
|
|
damage: 40,
|
|
fireRate: 800,
|
|
spread: 0.5,
|
|
bulletsPerShot: 5,
|
|
range: 200,
|
|
autoFire: false,
|
|
burstCount: 1,
|
|
sprite: 'shotgun.png'
|
|
},
|
|
burstRifle: {
|
|
name: 'Burst Rifle',
|
|
ammo: 30,
|
|
damage: 55,
|
|
fireRate: 100,
|
|
spread: 0.1,
|
|
bulletsPerShot: 1,
|
|
range: 350,
|
|
autoFire: false,
|
|
burstCount: 3,
|
|
sprite: 'burst-rifle.png'
|
|
},
|
|
machineGun: {
|
|
name: 'Machine Gun',
|
|
ammo: 50,
|
|
damage: 45,
|
|
fireRate: 100,
|
|
spread: 0.2,
|
|
bulletsPerShot: 1,
|
|
range: 300,
|
|
autoFire: true,
|
|
burstCount: 1,
|
|
sprite: 'machine-gun.png'
|
|
}
|
|
};
|
|
|
|
// Tile system
|
|
const TILE_TYPES = {
|
|
FLOOR: 0,
|
|
WALL: 1,
|
|
BUILDING: 2,
|
|
ROAD: 3
|
|
};
|
|
|
|
function createTileSVG(type, size = 32) {
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('width', size);
|
|
svg.setAttribute('height', size);
|
|
svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
|
|
|
|
switch(type) {
|
|
case TILE_TYPES.FLOOR:
|
|
// Concrete floor with texture
|
|
svg.innerHTML = `
|
|
<rect width="${size}" height="${size}" fill="#4a4a4a"/>
|
|
<rect x="0" y="0" width="16" height="16" fill="#525252"/>
|
|
<rect x="16" y="16" width="16" height="16" fill="#525252"/>
|
|
<rect x="2" y="2" width="2" height="2" fill="#3a3a3a"/>
|
|
<rect x="28" y="6" width="2" height="2" fill="#3a3a3a"/>
|
|
<rect x="8" y="24" width="2" height="2" fill="#3a3a3a"/>
|
|
<rect x="22" y="28" width="2" height="2" fill="#3a3a3a"/>
|
|
`;
|
|
break;
|
|
case TILE_TYPES.WALL:
|
|
// Brick wall
|
|
svg.innerHTML = `
|
|
<rect width="${size}" height="${size}" fill="#8B4513"/>
|
|
<rect x="0" y="0" width="16" height="8" fill="#A0522D" stroke="#654321" stroke-width="1"/>
|
|
<rect x="16" y="0" width="16" height="8" fill="#A0522D" stroke="#654321" stroke-width="1"/>
|
|
<rect x="8" y="8" width="16" height="8" fill="#A0522D" stroke="#654321" stroke-width="1"/>
|
|
<rect x="0" y="16" width="16" height="8" fill="#A0522D" stroke="#654321" stroke-width="1"/>
|
|
<rect x="16" y="16" width="16" height="8" fill="#A0522D" stroke="#654321" stroke-width="1"/>
|
|
<rect x="8" y="24" width="16" height="8" fill="#A0522D" stroke="#654321" stroke-width="1"/>
|
|
`;
|
|
break;
|
|
case TILE_TYPES.BUILDING:
|
|
// Building tile
|
|
svg.innerHTML = `
|
|
<rect width="${size}" height="${size}" fill="#2F4F4F"/>
|
|
<rect x="2" y="2" width="${size-4}" height="${size-4}" fill="#708090"/>
|
|
<rect x="6" y="6" width="8" height="8" fill="#4682B4"/>
|
|
<rect x="18" y="6" width="8" height="8" fill="#4682B4"/>
|
|
<rect x="6" y="18" width="8" height="8" fill="#4682B4"/>
|
|
<rect x="18" y="18" width="8" height="8" fill="#4682B4"/>
|
|
<rect x="7" y="7" width="2" height="2" fill="#87CEEB"/>
|
|
<rect x="11" y="7" width="2" height="2" fill="#87CEEB"/>
|
|
<rect x="19" y="7" width="2" height="2" fill="#87CEEB"/>
|
|
<rect x="23" y="7" width="2" height="2" fill="#87CEEB"/>
|
|
<rect x="7" y="19" width="2" height="2" fill="#87CEEB"/>
|
|
<rect x="11" y="19" width="2" height="2" fill="#87CEEB"/>
|
|
<rect x="19" y="19" width="2" height="2" fill="#87CEEB"/>
|
|
<rect x="23" y="19" width="2" height="2" fill="#87CEEB"/>
|
|
`;
|
|
break;
|
|
case TILE_TYPES.ROAD:
|
|
// Asphalt road
|
|
svg.innerHTML = `
|
|
<rect width="${size}" height="${size}" fill="#2F2F2F"/>
|
|
<rect x="0" y="14" width="${size}" height="4" fill="#FFFF00"/>
|
|
<rect x="4" y="4" width="2" height="2" fill="#1F1F1F"/>
|
|
<rect x="26" y="8" width="2" height="2" fill="#1F1F1F"/>
|
|
<rect x="12" y="22" width="2" height="2" fill="#1F1F1F"/>
|
|
<rect x="20" y="26" width="2" height="2" fill="#1F1F1F"/>
|
|
`;
|
|
break;
|
|
}
|
|
|
|
return svg;
|
|
}
|
|
|
|
function generateMap() {
|
|
const map = [];
|
|
for (let y = 0; y < game.mapHeight; y++) {
|
|
const row = [];
|
|
for (let x = 0; x < game.mapWidth; x++) {
|
|
// No border walls - zombies can enter from all sides
|
|
if (false) {
|
|
row.push(TILE_TYPES.WALL);
|
|
}
|
|
// Buildings in clusters
|
|
else if (Math.random() < 0.15 && x > 5 && x < game.mapWidth - 5 && y > 5 && y < game.mapHeight - 5) {
|
|
row.push(TILE_TYPES.BUILDING);
|
|
}
|
|
// Walls for structure
|
|
else if (Math.random() < 0.08) {
|
|
row.push(TILE_TYPES.WALL);
|
|
}
|
|
// Roads
|
|
else if ((x % 8 === 0 || y % 8 === 0) && Math.random() < 0.3) {
|
|
row.push(TILE_TYPES.ROAD);
|
|
}
|
|
// Default floor
|
|
else {
|
|
row.push(TILE_TYPES.FLOOR);
|
|
}
|
|
}
|
|
map.push(row);
|
|
}
|
|
|
|
// Ensure starting area is clear
|
|
const centerX = Math.floor(game.mapWidth / 2);
|
|
const centerY = Math.floor(game.mapHeight / 2);
|
|
for (let dy = -2; dy <= 2; dy++) {
|
|
for (let dx = -2; dx <= 2; dx++) {
|
|
if (centerX + dx >= 0 && centerX + dx < game.mapWidth &&
|
|
centerY + dy >= 0 && centerY + dy < game.mapHeight) {
|
|
map[centerY + dy][centerX + dx] = TILE_TYPES.FLOOR;
|
|
}
|
|
}
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
function renderTileToCanvas(type, x, y) {
|
|
const svg = createTileSVG(type, game.tileSize);
|
|
const svgData = new XMLSerializer().serializeToString(svg);
|
|
const img = new Image();
|
|
const blob = new Blob([svgData], {type: 'image/svg+xml'});
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
img.onload = function() {
|
|
ctx.drawImage(img, x * game.tileSize, y * game.tileSize);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
img.src = url;
|
|
}
|
|
|
|
function isWalkable(tileType) {
|
|
return tileType === TILE_TYPES.FLOOR || tileType === TILE_TYPES.ROAD;
|
|
}
|
|
|
|
function getTileAt(x, y) {
|
|
const tileX = Math.floor(x / game.tileSize);
|
|
const tileY = Math.floor(y / game.tileSize);
|
|
|
|
if (tileX < 0 || tileX >= game.mapWidth || tileY < 0 || tileY >= game.mapHeight) {
|
|
return TILE_TYPES.WALL;
|
|
}
|
|
|
|
return game.map[tileY][tileX];
|
|
}
|
|
|
|
// Pathfinding system
|
|
class PathNode {
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.g = 0; // Cost from start
|
|
this.h = 0; // Heuristic cost to goal
|
|
this.f = 0; // Total cost (g + h)
|
|
this.parent = null;
|
|
}
|
|
}
|
|
|
|
function manhattanDistance(a, b) {
|
|
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
}
|
|
|
|
function getNeighbors(node) {
|
|
const neighbors = [];
|
|
const directions = [
|
|
{x: 0, y: -1}, {x: 1, y: 0}, {x: 0, y: 1}, {x: -1, y: 0}, // Cardinal
|
|
{x: -1, y: -1}, {x: 1, y: -1}, {x: 1, y: 1}, {x: -1, y: 1} // Diagonal
|
|
];
|
|
|
|
for (let dir of directions) {
|
|
const newX = node.x + dir.x;
|
|
const newY = node.y + dir.y;
|
|
|
|
// Check bounds
|
|
if (newX >= 0 && newX < game.mapWidth && newY >= 0 && newY < game.mapHeight) {
|
|
// Check if tile is walkable
|
|
if (isWalkable(game.map[newY][newX])) {
|
|
// For diagonal movement, also check adjacent tiles to prevent corner cutting
|
|
if (dir.x !== 0 && dir.y !== 0) {
|
|
if (isWalkable(game.map[node.y][newX]) && isWalkable(game.map[newY][node.x])) {
|
|
neighbors.push(new PathNode(newX, newY));
|
|
}
|
|
} else {
|
|
neighbors.push(new PathNode(newX, newY));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return neighbors;
|
|
}
|
|
|
|
function findPath(startX, startY, goalX, goalY) {
|
|
// Convert world coordinates to tile coordinates
|
|
const startTileX = Math.floor(startX / game.tileSize);
|
|
const startTileY = Math.floor(startY / game.tileSize);
|
|
const goalTileX = Math.floor(goalX / game.tileSize);
|
|
const goalTileY = Math.floor(goalY / game.tileSize);
|
|
|
|
// Check cache first (quantize coordinates for better cache hits)
|
|
const quantizedStartX = Math.floor(startTileX / 2) * 2;
|
|
const quantizedStartY = Math.floor(startTileY / 2) * 2;
|
|
const quantizedGoalX = Math.floor(goalTileX / 2) * 2;
|
|
const quantizedGoalY = Math.floor(goalTileY / 2) * 2;
|
|
const cacheKey = `${quantizedStartX},${quantizedStartY}-${quantizedGoalX},${quantizedGoalY}`;
|
|
|
|
if (game.pathfindingCache.has(cacheKey)) {
|
|
return game.pathfindingCache.get(cacheKey);
|
|
}
|
|
|
|
// Check if start and goal are valid
|
|
if (!isWalkable(game.map[startTileY]?.[startTileX]) ||
|
|
!isWalkable(game.map[goalTileY]?.[goalTileX])) {
|
|
return [];
|
|
}
|
|
|
|
const openSet = [];
|
|
const closedSet = new Set();
|
|
const startNode = new PathNode(startTileX, startTileY);
|
|
const goalNode = new PathNode(goalTileX, goalTileY);
|
|
|
|
startNode.h = manhattanDistance(startNode, goalNode);
|
|
startNode.f = startNode.h;
|
|
openSet.push(startNode);
|
|
|
|
while (openSet.length > 0) {
|
|
// Find node with lowest f cost
|
|
let currentIndex = 0;
|
|
for (let i = 1; i < openSet.length; i++) {
|
|
if (openSet[i].f < openSet[currentIndex].f) {
|
|
currentIndex = i;
|
|
}
|
|
}
|
|
|
|
const current = openSet.splice(currentIndex, 1)[0];
|
|
const currentKey = `${current.x},${current.y}`;
|
|
closedSet.add(currentKey);
|
|
|
|
// Check if we reached the goal
|
|
if (current.x === goalNode.x && current.y === goalNode.y) {
|
|
const path = [];
|
|
let node = current;
|
|
while (node) {
|
|
path.unshift({
|
|
x: node.x * game.tileSize + game.tileSize / 2,
|
|
y: node.y * game.tileSize + game.tileSize / 2
|
|
});
|
|
node = node.parent;
|
|
}
|
|
|
|
// Cache the result
|
|
if (game.pathfindingCache.size >= game.maxCacheSize) {
|
|
// Remove oldest entry
|
|
const firstKey = game.pathfindingCache.keys().next().value;
|
|
game.pathfindingCache.delete(firstKey);
|
|
}
|
|
game.pathfindingCache.set(cacheKey, path);
|
|
|
|
return path;
|
|
}
|
|
|
|
const neighbors = getNeighbors(current);
|
|
for (let neighbor of neighbors) {
|
|
const neighborKey = `${neighbor.x},${neighbor.y}`;
|
|
|
|
if (closedSet.has(neighborKey)) {
|
|
continue;
|
|
}
|
|
|
|
// Calculate movement cost (diagonal costs more)
|
|
const isDiagonal = Math.abs(neighbor.x - current.x) + Math.abs(neighbor.y - current.y) === 2;
|
|
const movementCost = isDiagonal ? 1.4 : 1.0;
|
|
const tentativeG = current.g + movementCost;
|
|
|
|
let existingNode = openSet.find(node => node.x === neighbor.x && node.y === neighbor.y);
|
|
|
|
if (!existingNode) {
|
|
neighbor.g = tentativeG;
|
|
neighbor.h = manhattanDistance(neighbor, goalNode);
|
|
neighbor.f = neighbor.g + neighbor.h;
|
|
neighbor.parent = current;
|
|
openSet.push(neighbor);
|
|
} else if (tentativeG < existingNode.g) {
|
|
existingNode.g = tentativeG;
|
|
existingNode.f = existingNode.g + existingNode.h;
|
|
existingNode.parent = current;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache empty result
|
|
const emptyPath = [];
|
|
if (game.pathfindingCache.size >= game.maxCacheSize) {
|
|
const firstKey = game.pathfindingCache.keys().next().value;
|
|
game.pathfindingCache.delete(firstKey);
|
|
}
|
|
game.pathfindingCache.set(cacheKey, emptyPath);
|
|
|
|
return emptyPath; // No path found
|
|
}
|
|
|
|
class Player {
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.width = 32;
|
|
this.height = 32;
|
|
this.speed = 3;
|
|
this.weapon = { ...weaponTypes.pistol };
|
|
this.lastShot = 0;
|
|
this.burstCount = 0;
|
|
this.burstTimer = 0;
|
|
this.health = 100;
|
|
this.maxHealth = 100;
|
|
this.lastDamage = 0;
|
|
}
|
|
|
|
update() {
|
|
// Movement with collision detection
|
|
let newX = this.x;
|
|
let newY = this.y;
|
|
|
|
if (game.keys['w'] || game.keys['W']) newY -= this.speed;
|
|
if (game.keys['s'] || game.keys['S']) newY += this.speed;
|
|
if (game.keys['a'] || game.keys['A']) newX -= this.speed;
|
|
if (game.keys['d'] || game.keys['D']) newX += this.speed;
|
|
|
|
// Check collision for X movement
|
|
if (isWalkable(getTileAt(newX - this.width/2, this.y)) &&
|
|
isWalkable(getTileAt(newX + this.width/2, this.y))) {
|
|
this.x = newX;
|
|
}
|
|
|
|
// Check collision for Y movement
|
|
if (isWalkable(getTileAt(this.x, newY - this.height/2)) &&
|
|
isWalkable(getTileAt(this.x, newY + this.height/2))) {
|
|
this.y = newY;
|
|
}
|
|
|
|
// Keep player in map bounds
|
|
this.x = Math.max(this.width/2, Math.min(game.mapWidth * game.tileSize - this.width/2, this.x));
|
|
this.y = Math.max(this.height/2, Math.min(game.mapHeight * game.tileSize - this.height/2, this.y));
|
|
|
|
// Shooting
|
|
const now = Date.now();
|
|
// Handle burst firing (remaining shots after initial click)
|
|
if (this.burstCount > 1 && now - this.burstTimer >= this.weapon.fireRate) {
|
|
this.shoot();
|
|
this.burstCount--;
|
|
this.burstTimer = now;
|
|
}
|
|
// Handle automatic weapons (hold to fire)
|
|
else if (game.mouse.down && this.weapon.autoFire && now - this.lastShot >= this.weapon.fireRate) {
|
|
this.shoot();
|
|
}
|
|
|
|
// Check weapon pickups
|
|
game.weaponDrops.forEach((drop, index) => {
|
|
const dx = this.x - drop.x;
|
|
const dy = this.y - drop.y;
|
|
if (Math.sqrt(dx * dx + dy * dy) < 30) {
|
|
this.weapon = { ...drop.weapon };
|
|
game.weaponDrops.splice(index, 1);
|
|
updateUI();
|
|
}
|
|
});
|
|
}
|
|
|
|
shoot() {
|
|
if (this.weapon.ammo <= 0) {
|
|
if (this.weapon.name !== 'Pistol') {
|
|
this.weapon = { ...weaponTypes.pistol };
|
|
updateUI();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const dx = game.mouse.x - this.x;
|
|
const dy = game.mouse.y - this.y;
|
|
const angle = Math.atan2(dy, dx);
|
|
|
|
for (let i = 0; i < this.weapon.bulletsPerShot; i++) {
|
|
const spread = (Math.random() - 0.5) * this.weapon.spread;
|
|
const bulletAngle = angle + spread;
|
|
|
|
game.bullets.push(new Bullet(
|
|
this.x, this.y, bulletAngle, this.weapon.damage, this.weapon.range
|
|
));
|
|
}
|
|
|
|
if (this.weapon.ammo !== Infinity) {
|
|
this.weapon.ammo--;
|
|
}
|
|
|
|
this.lastShot = Date.now();
|
|
|
|
// Play gunshot sound
|
|
sounds.gunshot.currentTime = 0;
|
|
sounds.gunshot.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
updateUI();
|
|
}
|
|
|
|
draw() {
|
|
ctx.drawImage(sprites['main-character.png'],
|
|
this.x - this.width/2 + game.camera.x, this.y - this.height/2 + game.camera.y, this.width, this.height);
|
|
}
|
|
}
|
|
|
|
class Zombie {
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.width = 32;
|
|
this.height = 32;
|
|
this.speed = 1 + Math.random() * 0.5;
|
|
this.hp = 50 + Math.random() * 50;
|
|
this.maxHp = this.hp;
|
|
this.sprite = ['zombie.png', 'zombie2.png', 'zombie3.png', 'zombie4.png'][Math.floor(Math.random() * 4)];
|
|
this.path = [];
|
|
this.pathIndex = 0;
|
|
this.lastPathUpdate = 0;
|
|
this.pathUpdateInterval = 1000 + Math.random() * 500; // Update path every 1-1.5 seconds
|
|
this.lastPlayerX = null;
|
|
this.lastPlayerY = null;
|
|
}
|
|
|
|
update() {
|
|
const now = Date.now();
|
|
const distanceToPlayer = Math.sqrt(
|
|
Math.pow(game.player.x - this.x, 2) + Math.pow(game.player.y - this.y, 2)
|
|
);
|
|
|
|
// If very close to player, use direct movement for responsiveness
|
|
if (distanceToPlayer < 64) {
|
|
const dx = game.player.x - this.x;
|
|
const dy = game.player.y - this.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance > 0) {
|
|
let newX = this.x + (dx / distance) * this.speed;
|
|
let newY = this.y + (dy / distance) * this.speed;
|
|
|
|
// Check collision for X movement
|
|
if (isWalkable(getTileAt(newX - this.width/2, this.y)) &&
|
|
isWalkable(getTileAt(newX + this.width/2, this.y))) {
|
|
this.x = newX;
|
|
}
|
|
|
|
// Check collision for Y movement
|
|
if (isWalkable(getTileAt(this.x, newY - this.height/2)) &&
|
|
isWalkable(getTileAt(this.x, newY + this.height/2))) {
|
|
this.y = newY;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Update path periodically or if no path exists
|
|
if (now - this.lastPathUpdate > this.pathUpdateInterval || this.path.length === 0) {
|
|
// Only recalculate if player has moved significantly or path is old
|
|
const playerMoved = this.lastPlayerX && this.lastPlayerY &&
|
|
(Math.abs(game.player.x - this.lastPlayerX) > 64 ||
|
|
Math.abs(game.player.y - this.lastPlayerY) > 64);
|
|
|
|
if (!this.lastPlayerX || playerMoved || this.path.length === 0 ||
|
|
now - this.lastPathUpdate > this.pathUpdateInterval) {
|
|
this.path = findPath(this.x, this.y, game.player.x, game.player.y);
|
|
this.pathIndex = 0;
|
|
this.lastPathUpdate = now;
|
|
this.lastPlayerX = game.player.x;
|
|
this.lastPlayerY = game.player.y;
|
|
}
|
|
}
|
|
|
|
// Follow the path
|
|
if (this.path.length > 0 && this.pathIndex < this.path.length) {
|
|
const target = this.path[this.pathIndex];
|
|
const dx = target.x - this.x;
|
|
const dy = target.y - this.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
// If we're close enough to the current waypoint, move to the next one
|
|
if (distance < 16) {
|
|
this.pathIndex++;
|
|
return;
|
|
}
|
|
|
|
// Move towards current waypoint
|
|
if (distance > 0) {
|
|
let newX = this.x + (dx / distance) * this.speed;
|
|
let newY = this.y + (dy / distance) * this.speed;
|
|
|
|
// Check collision for X movement
|
|
if (isWalkable(getTileAt(newX - this.width/2, this.y)) &&
|
|
isWalkable(getTileAt(newX + this.width/2, this.y))) {
|
|
this.x = newX;
|
|
}
|
|
|
|
// Check collision for Y movement
|
|
if (isWalkable(getTileAt(this.x, newY - this.height/2)) &&
|
|
isWalkable(getTileAt(this.x, newY + this.height/2))) {
|
|
this.y = newY;
|
|
}
|
|
}
|
|
} else {
|
|
// Fallback to direct movement if no path found
|
|
const dx = game.player.x - this.x;
|
|
const dy = game.player.y - this.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance > 0) {
|
|
let newX = this.x + (dx / distance) * this.speed * 0.5; // Slower when no path
|
|
let newY = this.y + (dy / distance) * this.speed * 0.5;
|
|
|
|
// Check collision for X movement
|
|
if (isWalkable(getTileAt(newX - this.width/2, this.y)) &&
|
|
isWalkable(getTileAt(newX + this.width/2, this.y))) {
|
|
this.x = newX;
|
|
}
|
|
|
|
// Check collision for Y movement
|
|
if (isWalkable(getTileAt(this.x, newY - this.height/2)) &&
|
|
isWalkable(getTileAt(this.x, newY + this.height/2))) {
|
|
this.y = newY;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
takeDamage(damage) {
|
|
this.hp -= damage;
|
|
return this.hp <= 0;
|
|
}
|
|
|
|
draw() {
|
|
const screenX = this.x + game.camera.x;
|
|
const screenY = this.y + game.camera.y;
|
|
|
|
ctx.drawImage(sprites[this.sprite],
|
|
screenX - this.width/2, screenY - this.height/2, this.width, this.height);
|
|
|
|
// Enhanced health bar
|
|
const barWidth = this.width + 4;
|
|
const barHeight = 6;
|
|
const healthPercent = this.hp / this.maxHp;
|
|
const barX = screenX - barWidth/2;
|
|
const barY = screenY - this.height/2 - 12;
|
|
|
|
// Health bar background (dark border)
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
|
ctx.fillRect(barX - 1, barY - 1, barWidth + 2, barHeight + 2);
|
|
|
|
// Health bar background (red)
|
|
ctx.fillStyle = '#330000';
|
|
ctx.fillRect(barX, barY, barWidth, barHeight);
|
|
|
|
// Health bar foreground with gradient effect
|
|
const gradient = ctx.createLinearGradient(barX, barY, barX, barY + barHeight);
|
|
if (healthPercent > 0.6) {
|
|
gradient.addColorStop(0, '#00ff44');
|
|
gradient.addColorStop(1, '#00cc33');
|
|
} else if (healthPercent > 0.3) {
|
|
gradient.addColorStop(0, '#ffaa00');
|
|
gradient.addColorStop(1, '#ff8800');
|
|
} else {
|
|
gradient.addColorStop(0, '#ff4444');
|
|
gradient.addColorStop(1, '#cc0000');
|
|
}
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(barX, barY, barWidth * healthPercent, barHeight);
|
|
|
|
// Health bar highlight
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
ctx.fillRect(barX, barY, barWidth * healthPercent, 2);
|
|
}
|
|
}
|
|
|
|
class Bullet {
|
|
constructor(x, y, angle, damage, range) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.startX = x;
|
|
this.startY = y;
|
|
this.vx = Math.cos(angle) * 8;
|
|
this.vy = Math.sin(angle) * 8;
|
|
this.damage = damage;
|
|
this.range = range;
|
|
this.width = 4;
|
|
this.height = 4;
|
|
}
|
|
|
|
update() {
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
|
|
// Check range
|
|
const dx = this.x - this.startX;
|
|
const dy = this.y - this.startY;
|
|
if (Math.sqrt(dx * dx + dy * dy) > this.range) {
|
|
return false;
|
|
}
|
|
|
|
// Check bounds
|
|
if (this.x < 0 || this.x > canvas.width || this.y < 0 || this.y > canvas.height) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
draw() {
|
|
// Enhanced bullet with glow effect
|
|
ctx.shadowBlur = 10;
|
|
ctx.shadowColor = '#ffff00';
|
|
ctx.fillStyle = '#ffff44';
|
|
ctx.fillRect(this.x - 2 + game.camera.x, this.y - 2 + game.camera.y, 4, 4);
|
|
|
|
// Reset shadow
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
}
|
|
|
|
class WeaponDrop {
|
|
constructor(x, y, weaponType) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.weapon = { ...weaponTypes[weaponType] };
|
|
this.sprite = this.weapon.sprite;
|
|
this.width = 24;
|
|
this.height = 24;
|
|
}
|
|
|
|
draw() {
|
|
// Weapon drop with pulsing glow effect
|
|
const time = Date.now() * 0.005;
|
|
const glowIntensity = (Math.sin(time) + 1) * 0.5;
|
|
|
|
ctx.shadowBlur = 15 + glowIntensity * 10;
|
|
ctx.shadowColor = '#00aaff';
|
|
ctx.drawImage(sprites[this.sprite],
|
|
this.x - this.width/2 + game.camera.x, this.y - this.height/2 + game.camera.y, this.width, this.height);
|
|
|
|
// Reset shadow
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
}
|
|
|
|
function spawnZombie() {
|
|
let x, y;
|
|
let attempts = 0;
|
|
const maxAttempts = 50;
|
|
|
|
// Keep trying to find a walkable spawn position
|
|
do {
|
|
x = Math.random() * (game.mapWidth * game.tileSize);
|
|
y = Math.random() * (game.mapHeight * game.tileSize);
|
|
attempts++;
|
|
} while (!isWalkable(getTileAt(x, y)) && attempts < maxAttempts);
|
|
|
|
// If we couldn't find a walkable tile, spawn on a road or floor tile
|
|
if (attempts >= maxAttempts) {
|
|
// Find any walkable tile
|
|
for (let ty = 0; ty < game.mapHeight; ty++) {
|
|
for (let tx = 0; tx < game.mapWidth; tx++) {
|
|
if (isWalkable(game.map[ty][tx])) {
|
|
x = tx * game.tileSize + game.tileSize / 2;
|
|
y = ty * game.tileSize + game.tileSize / 2;
|
|
break;
|
|
}
|
|
}
|
|
if (isWalkable(getTileAt(x, y))) break;
|
|
}
|
|
}
|
|
|
|
game.zombies.push(new Zombie(x, y));
|
|
}
|
|
|
|
function checkCollisions() {
|
|
// Bullet-zombie collisions
|
|
for (let i = game.bullets.length - 1; i >= 0; i--) {
|
|
const bullet = game.bullets[i];
|
|
let bulletHit = false;
|
|
|
|
for (let j = game.zombies.length - 1; j >= 0; j--) {
|
|
const zombie = game.zombies[j];
|
|
|
|
const dx = bullet.x - zombie.x;
|
|
const dy = bullet.y - zombie.y;
|
|
|
|
if (Math.abs(dx) < zombie.width/2 && Math.abs(dy) < zombie.height/2) {
|
|
if (zombie.takeDamage(bullet.damage)) {
|
|
// Zombie died
|
|
game.score += 10;
|
|
|
|
// Play zombie grunt sound
|
|
sounds.zombieGrunt.currentTime = 0;
|
|
sounds.zombieGrunt.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
// Random weapon drop (increased rate)
|
|
if (Math.random() < 0.4) {
|
|
const weaponKeys = Object.keys(weaponTypes);
|
|
const randomWeapon = weaponKeys[Math.floor(Math.random() * weaponKeys.length)];
|
|
if (randomWeapon !== 'pistol') {
|
|
game.weaponDrops.push(new WeaponDrop(zombie.x, zombie.y, randomWeapon));
|
|
}
|
|
}
|
|
|
|
game.zombies.splice(j, 1);
|
|
}
|
|
bulletHit = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bulletHit) {
|
|
game.bullets.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// Player-zombie collisions
|
|
const now = Date.now();
|
|
for (let i = game.zombies.length - 1; i >= 0; i--) {
|
|
const zombie = game.zombies[i];
|
|
const dx = game.player.x - zombie.x;
|
|
const dy = game.player.y - zombie.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < (game.player.width + zombie.width) / 2) {
|
|
// Damage player (once per second per zombie)
|
|
if (now - game.player.lastDamage > 1000) {
|
|
game.player.health -= 10;
|
|
game.player.lastDamage = now;
|
|
|
|
// Play player ouch sound
|
|
sounds.playerOuch.currentTime = 0;
|
|
sounds.playerOuch.play().catch(e => console.log('Audio play failed:', e));
|
|
|
|
updateUI();
|
|
|
|
if (game.player.health <= 0) {
|
|
// Game over
|
|
showGameOver();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateUI() {
|
|
document.getElementById('weaponName').textContent = game.player.weapon.name;
|
|
document.getElementById('ammoCount').textContent =
|
|
game.player.weapon.ammo === Infinity ? '∞' : game.player.weapon.ammo;
|
|
document.getElementById('health').textContent = game.player.health;
|
|
document.getElementById('score').textContent = game.score;
|
|
}
|
|
|
|
function showGameOver() {
|
|
game.gameOver = true;
|
|
|
|
// Update high score
|
|
if (game.score > game.highScore) {
|
|
game.highScore = game.score;
|
|
localStorage.setItem('zombieHighScore', game.highScore);
|
|
}
|
|
|
|
// Update game over screen
|
|
document.getElementById('finalScore').textContent = game.score;
|
|
document.getElementById('highScore').textContent = game.highScore;
|
|
document.getElementById('gameOverScreen').style.display = 'flex';
|
|
}
|
|
|
|
function restartGame() {
|
|
// Hide game over screen
|
|
document.getElementById('gameOverScreen').style.display = 'none';
|
|
|
|
// Reset game state
|
|
game.gameOver = false;
|
|
game.score = 0;
|
|
game.zombies = [];
|
|
game.bullets = [];
|
|
game.weaponDrops = [];
|
|
game.pathfindingCache.clear();
|
|
|
|
// Reset player
|
|
const centerX = Math.floor(game.mapWidth / 2) * game.tileSize;
|
|
const centerY = Math.floor(game.mapHeight / 2) * game.tileSize;
|
|
game.player.health = game.player.maxHealth;
|
|
game.player.x = centerX;
|
|
game.player.y = centerY;
|
|
game.player.weapon = { ...weaponTypes.pistol };
|
|
|
|
// Spawn initial zombies
|
|
for (let i = 0; i < 5; i++) {
|
|
spawnZombie();
|
|
}
|
|
|
|
updateUI();
|
|
}
|
|
|
|
function drawMap() {
|
|
// Pre-rendered tile canvases for performance
|
|
if (!game.tileCanvases) {
|
|
game.tileCanvases = {};
|
|
Object.values(TILE_TYPES).forEach(type => {
|
|
const tileCanvas = document.createElement('canvas');
|
|
tileCanvas.width = game.tileSize;
|
|
tileCanvas.height = game.tileSize;
|
|
const tileCtx = tileCanvas.getContext('2d');
|
|
|
|
// Draw tile directly to canvas
|
|
switch(type) {
|
|
case TILE_TYPES.FLOOR:
|
|
tileCtx.fillStyle = '#4a4a4a';
|
|
tileCtx.fillRect(0, 0, game.tileSize, game.tileSize);
|
|
tileCtx.fillStyle = '#525252';
|
|
tileCtx.fillRect(0, 0, 16, 16);
|
|
tileCtx.fillRect(16, 16, 16, 16);
|
|
tileCtx.fillStyle = '#3a3a3a';
|
|
tileCtx.fillRect(2, 2, 2, 2);
|
|
tileCtx.fillRect(28, 6, 2, 2);
|
|
tileCtx.fillRect(8, 24, 2, 2);
|
|
tileCtx.fillRect(22, 28, 2, 2);
|
|
break;
|
|
case TILE_TYPES.WALL:
|
|
tileCtx.fillStyle = '#8B4513';
|
|
tileCtx.fillRect(0, 0, game.tileSize, game.tileSize);
|
|
tileCtx.fillStyle = '#A0522D';
|
|
tileCtx.strokeStyle = '#654321';
|
|
tileCtx.lineWidth = 1;
|
|
for (let row = 0; row < 4; row++) {
|
|
for (let col = 0; col < 2; col++) {
|
|
let x = col * 16 + (row % 2) * 8;
|
|
let y = row * 8;
|
|
if (x + 16 <= game.tileSize) {
|
|
tileCtx.fillRect(x, y, 16, 8);
|
|
tileCtx.strokeRect(x, y, 16, 8);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case TILE_TYPES.BUILDING:
|
|
tileCtx.fillStyle = '#2F4F4F';
|
|
tileCtx.fillRect(0, 0, game.tileSize, game.tileSize);
|
|
tileCtx.fillStyle = '#708090';
|
|
tileCtx.fillRect(2, 2, game.tileSize-4, game.tileSize-4);
|
|
tileCtx.fillStyle = '#4682B4';
|
|
tileCtx.fillRect(6, 6, 8, 8);
|
|
tileCtx.fillRect(18, 6, 8, 8);
|
|
tileCtx.fillRect(6, 18, 8, 8);
|
|
tileCtx.fillRect(18, 18, 8, 8);
|
|
tileCtx.fillStyle = '#87CEEB';
|
|
[[7,7], [11,7], [19,7], [23,7], [7,19], [11,19], [19,19], [23,19]].forEach(([x,y]) => {
|
|
tileCtx.fillRect(x, y, 2, 2);
|
|
});
|
|
break;
|
|
case TILE_TYPES.ROAD:
|
|
tileCtx.fillStyle = '#2F2F2F';
|
|
tileCtx.fillRect(0, 0, game.tileSize, game.tileSize);
|
|
tileCtx.fillStyle = '#FFFF00';
|
|
tileCtx.fillRect(0, 14, game.tileSize, 4);
|
|
tileCtx.fillStyle = '#1F1F1F';
|
|
[[4,4], [26,8], [12,22], [20,26]].forEach(([x,y]) => {
|
|
tileCtx.fillRect(x, y, 2, 2);
|
|
});
|
|
break;
|
|
}
|
|
|
|
game.tileCanvases[type] = tileCanvas;
|
|
});
|
|
}
|
|
|
|
// Draw visible tiles
|
|
const startX = Math.max(0, Math.floor(-game.camera.x / game.tileSize));
|
|
const endX = Math.min(game.mapWidth, Math.ceil((canvas.width - game.camera.x) / game.tileSize));
|
|
const startY = Math.max(0, Math.floor(-game.camera.y / game.tileSize));
|
|
const endY = Math.min(game.mapHeight, Math.ceil((canvas.height - game.camera.y) / game.tileSize));
|
|
|
|
for (let y = startY; y < endY; y++) {
|
|
for (let x = startX; x < endX; x++) {
|
|
const tileType = game.map[y][x];
|
|
const screenX = x * game.tileSize + game.camera.x;
|
|
const screenY = y * game.tileSize + game.camera.y;
|
|
ctx.drawImage(game.tileCanvases[tileType], screenX, screenY);
|
|
}
|
|
}
|
|
}
|
|
|
|
function gameLoop() {
|
|
// Clear canvas
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Update camera to follow player
|
|
game.camera.x = canvas.width / 2 - game.player.x;
|
|
game.camera.y = canvas.height / 2 - game.player.y;
|
|
|
|
// Draw map
|
|
drawMap();
|
|
|
|
// Only update game if not game over
|
|
if (!game.gameOver) {
|
|
// Update
|
|
game.player.update();
|
|
|
|
game.zombies.forEach(zombie => zombie.update());
|
|
|
|
game.bullets = game.bullets.filter(bullet => bullet.update());
|
|
|
|
checkCollisions();
|
|
|
|
// Spawn zombies (reduced spawn rate)
|
|
if (Math.random() < 0.01 + game.score * 0.00005) {
|
|
spawnZombie();
|
|
}
|
|
}
|
|
|
|
// Draw
|
|
game.zombies.forEach(zombie => zombie.draw());
|
|
game.bullets.forEach(bullet => bullet.draw());
|
|
game.weaponDrops.forEach(drop => drop.draw());
|
|
game.player.draw();
|
|
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
function initGame() {
|
|
// Generate the map
|
|
game.map = generateMap();
|
|
|
|
// Place player at center of map
|
|
const centerX = Math.floor(game.mapWidth / 2) * game.tileSize;
|
|
const centerY = Math.floor(game.mapHeight / 2) * game.tileSize;
|
|
game.player = new Player(centerX, centerY);
|
|
|
|
// Spawn initial zombies at map edges
|
|
for (let i = 0; i < 5; i++) {
|
|
spawnZombie();
|
|
}
|
|
|
|
updateUI();
|
|
gameLoop();
|
|
}
|
|
|
|
// Event listeners
|
|
document.addEventListener('keydown', (e) => {
|
|
game.keys[e.key] = true;
|
|
});
|
|
|
|
document.addEventListener('keyup', (e) => {
|
|
game.keys[e.key] = false;
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', (e) => {
|
|
const rect = canvas.getBoundingClientRect();
|
|
game.mouse.x = e.clientX - rect.left - game.camera.x;
|
|
game.mouse.y = e.clientY - rect.top - game.camera.y;
|
|
});
|
|
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
game.mouse.down = true;
|
|
const now = Date.now();
|
|
// Always shoot on click for non-auto weapons if fire rate allows
|
|
if (!game.player.weapon.autoFire && now - game.player.lastShot >= game.player.weapon.fireRate) {
|
|
game.player.shoot(); // Shoot immediately on click
|
|
game.player.burstCount = game.player.weapon.burstCount - 1; // Set remaining burst shots
|
|
game.player.burstTimer = now;
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', (e) => {
|
|
game.mouse.down = false;
|
|
});
|
|
|
|
// Restart button event listener
|
|
document.getElementById('restartButton').addEventListener('click', restartGame);
|
|
|
|
// Start loading sprites
|
|
loadSprites();
|
|
</script>
|
|
</body>
|
|
</html> |