Add major game features and improvements

- Add interval management system to fix memory leaks and cleanup bugs
- Implement win condition (survive 3 minutes) with victory screen
- Add pause functionality (ESC/P key) with pause overlay menu
- Add progressive difficulty (enemy speed/spawn rate increase over time)
- Implement enemy variety: Normal, Fast (green), and Tank (larger, 2 HP)
- Add boss fights every 60 seconds with health bar
- Add power-ups: Extra Life, Triple Shot, Shield (10-15s spawn interval)
- Implement Hardcore Mode (1 life, faster enemies, no power-ups)
- Add high score system with localStorage persistence
- Add background music support with volume control
- Add visual feedback (player hit flash, enemy death animation, shield glow)
- Add mobile touch controls (auto-detected on touch devices)
- Improve collision detection using getBoundingClientRect
- Add responsive styles for mobile devices
This commit is contained in:
Richard Nixon 2026-01-24 21:44:47 +00:00
parent 19276b11fe
commit d49bed1541
3 changed files with 1161 additions and 144 deletions

View file

@ -10,10 +10,15 @@
<body>
<main>
<div id="game-intro">
<img src="./images/logo.png" alt="" class="logo-img" />
<img src="./images/logo.png" alt="" class="logo-img" />
<br />
<button id="start-button">Start Game</button>
<div class="button-container">
<button id="start-button">Start Game</button>
<button id="hardcore-button">Hardcore Mode</button>
</div>
<p id="high-score-display">High Score: 0<br>Hardcore High: 0</p>
<h2>Use the arrows keys to move and spacebar to shoot, try survive if you are able.</h2>
<p class="controls-hint">Press ESC or P to pause</p>
<img src="./images/arrows.png" />
</div>
@ -22,20 +27,48 @@
<label for="volume">Volume:</label>
<input type="range" id="volume" min="0" max="1" step="0.1" value="0.4">
</div>
<div id ="stats">
<div id="stats">
<h2>Stats</h2>
<h2 id="timer"></h2>
<h2>Lives: <span id="lives">3</span></h2>
<h2>Score: <span id="score">0</span></h2>
</div>
<!-- Pause Overlay -->
<div id="pause-overlay">
<div class="pause-content">
<h1>PAUSED</h1>
<button id="resume-button">Resume</button>
<button id="main-menu-button">Main Menu</button>
</div>
</div>
</div>
<div id="game-end">
<div id="game-end">
<h1>Game Over</h1>
<p>Final Score: <span id="final-score">0</span></p>
<p><span id="final-time">Time elapsed: 0:00</span></p>
<p id="final-high-score">High Score: 0</p>
<button id="restart-button">Restart</button>
</div>
</div>
<div id="game-victory">
<h1>VICTORY!</h1>
<p>You survived the apocalypse!</p>
<p>Final Score: <span id="victory-score">0</span></p>
<p><span id="victory-time">Time survived: 3:00</span></p>
<p id="victory-high-score">High Score: 0</p>
<button id="victory-restart-button">Play Again</button>
</div>
<!-- Touch Controls for Mobile -->
<div id="touch-controls">
<div class="touch-move">
<button id="touch-up" class="touch-btn">UP</button>
<button id="touch-down" class="touch-btn">DOWN</button>
</div>
<button id="touch-shoot" class="touch-btn touch-shoot-btn">SHOOT</button>
</div>
</main>
<script type="text/javascript" src="script.js"></script>
</body>

843
script.js
View file

@ -1,107 +1,175 @@
// DOM Elements
const gameIntro = document.getElementById('game-intro');
const gameArea = document.getElementById("gameArea");
const gameEnd = document.getElementById('game-end');
const gameVictory = document.getElementById('game-victory');
const pauseOverlay = document.getElementById('pause-overlay');
const startButton = document.getElementById('start-button');
const hardcoreButton = document.getElementById('hardcore-button');
const restartButton = document.getElementById('restart-button');
const victoryRestartButton = document.getElementById('victory-restart-button');
const scoreElement = document.getElementById('score');
const livesElement = document.getElementById('lives');
const timerElement = document.getElementById('timer');
const finalScoreElement = document.getElementById('final-score');
const finalTimeElement = document.getElementById('final-time');
const victoryScoreElement = document.getElementById('victory-score');
const victoryTimeElement = document.getElementById('victory-time');
const gameAreaHeight = window.innerHeight * 0.8;
const gameAreaWidth = window.innerWidth * 0.8;
const stats = document.getElementById('stats');
const volumeSlider = document.getElementById('volume');
const highScoreDisplay = document.getElementById('high-score-display');
const finalHighScore = document.getElementById('final-high-score');
const victoryHighScore = document.getElementById('victory-high-score');
const touchControls = document.getElementById('touch-controls');
let player = document.createElement("img");
let enemyInterval;
//let bulletInterval;
// Game state variables
let player = null;
let activeIntervals = [];
let activeTimeouts = [];
let sec = 0;
let min = 0;
let intervalTimer;
let lives = 3;
let score = 0;
let playerY = gameAreaHeight / 2 - 25;
let gameVolume = 0.4;
let isPaused = false;
let isGameRunning = false;
let isHardcoreMode = false;
let difficultyLevel = 1;
let hasShield = false;
let hasTripleShot = false;
let tripleShotTimeout = null;
let shieldTimeout = null;
let backgroundMusic = null;
let bossSpawned = false;
let lastBossTime = 0;
let keydownHandler = null;
// Set background image
// Setup Game Intro state
function introPage() {
gameIntro.style.display = 'block';
gameArea.style.display = 'none';
gameEnd.style.display = 'none';
stats.style.display = 'none';
// Interval management helpers
function addInterval(fn, ms, label = '') {
const id = setInterval(() => {
if (!isPaused) {
fn();
}
}, ms);
activeIntervals.push({ id, label });
return id;
}
// Run the game when start button is pressed
function startGame() {
startButton.addEventListener('click', () => {
gameArea.style.width = gameAreaWidth + "px";
gameArea.style.height = gameAreaHeight + "px";
gameArea.style.position = "relative";
gameArea.style.overflow = "hidden";
gameIntro.style.display = 'none';
gameArea.style.display = 'block';
gameEnd.style.display = 'none';
stats.style.display = 'block';
sec = 0;
min = 0;
lives = 3;
score = 0;
scoreElement.innerText = score;
livesElement.innerText = lives;
clearInterval(intervalTimer);
intervalTimer = setInterval(timeGame, 1000);
spawnPlayer();
spawnEnemy();
function addTimeout(fn, ms, label = '') {
const id = setTimeout(() => {
if (!isPaused && isGameRunning) {
fn();
}
// Remove from tracking
activeTimeouts = activeTimeouts.filter(t => t.id !== id);
}, ms);
activeTimeouts.push({ id, label });
return id;
}
function clearAllIntervals() {
activeIntervals.forEach(interval => clearInterval(interval.id));
activeIntervals = [];
activeTimeouts.forEach(timeout => clearTimeout(timeout.id));
activeTimeouts = [];
}
function resetGame() {
clearAllIntervals();
// Clear power-up effects
if (tripleShotTimeout) clearTimeout(tripleShotTimeout);
if (shieldTimeout) clearTimeout(shieldTimeout);
hasShield = false;
hasTripleShot = false;
// Remove all game elements
document.querySelectorAll(".enemy").forEach(e => e.remove());
document.querySelectorAll(".bullet").forEach(b => b.remove());
document.querySelectorAll(".boss").forEach(b => b.remove());
document.querySelectorAll(".power-up").forEach(p => p.remove());
document.querySelectorAll(".boss-health-bar").forEach(h => h.remove());
// Remove player if exists
if (player && player.parentNode) {
player.remove();
}
player = null;
// Remove keydown listener
if (keydownHandler) {
document.removeEventListener("keydown", keydownHandler);
keydownHandler = null;
}
// Stop background music
stopBackgroundMusic();
// Reset state
sec = 0;
min = 0;
lives = isHardcoreMode ? 1 : 3;
score = 0;
playerY = gameAreaHeight / 2 - 25;
isPaused = false;
isGameRunning = false;
difficultyLevel = 1;
bossSpawned = false;
lastBossTime = 0;
}
// High Score functions
function getHighScore() {
const key = isHardcoreMode ? 'scriptocalypse_highscore_hardcore' : 'scriptocalypse_highscore';
return parseInt(localStorage.getItem(key)) || 0;
}
function updateHighScore() {
const key = isHardcoreMode ? 'scriptocalypse_highscore_hardcore' : 'scriptocalypse_highscore';
const currentHigh = getHighScore();
if (score > currentHigh) {
localStorage.setItem(key, score);
return true; // New record
}
return false;
}
function displayHighScores() {
const normalHigh = parseInt(localStorage.getItem('scriptocalypse_highscore')) || 0;
const hardcoreHigh = parseInt(localStorage.getItem('scriptocalypse_highscore_hardcore')) || 0;
if (highScoreDisplay) {
highScoreDisplay.innerHTML = `High Score: ${normalHigh}<br>Hardcore High: ${hardcoreHigh}`;
}
}
// Background Music
function playBackgroundMusic() {
if (!backgroundMusic) {
backgroundMusic = new Audio('sounds/music.mp3');
backgroundMusic.loop = true;
}
backgroundMusic.volume = gameVolume;
backgroundMusic.play().catch(() => {
// Autoplay blocked, will play on first interaction
});
}
// Game Time
function timeGame() {
sec++;
sec = sec < 10 ? "0" + sec : sec;
if (sec == 60) {
min++;
sec = 0;
function stopBackgroundMusic() {
if (backgroundMusic) {
backgroundMusic.pause();
backgroundMusic.currentTime = 0;
}
timerElement.innerText = 'Time elapsed: ' + min + ':' + sec;
}
function spawnPlayer() {
player.src = "images/shooter.gif";
player.classList.add("player");
gameArea.appendChild(player);
playerMove();
// Player movement and shooting
player.style.bottom = playerY + "px";
let bulletInterval = setInterval(() => {
let checkPlayerColision = document.querySelectorAll(".enemy");
checkPlayerColision.forEach(enemy => {
checkCollisionEnemy(player, enemy);
});
},20);
}
function playerMove(){
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowUp" && playerY < gameAreaHeight - 60) {
playerY += 40;
} else if (e.key === "ArrowDown" && playerY > 0) {
playerY -= 40;
} else if (e.key === " ") {
shoot();
}
player.style.bottom = playerY + "px";
});
}
// Volume control
volumeSlider.addEventListener('input', (event) => {
gameVolume = event.target.value;
gameVolume = parseFloat(event.target.value);
if (backgroundMusic) {
backgroundMusic.volume = gameVolume;
}
});
function playSound(src) {
@ -110,85 +178,544 @@ function playSound(src) {
sound.play();
}
function shoot() {
const bullet = document.createElement("div");
bullet.classList.add("bullet");
gameArea.appendChild(bullet);
bullet.style.left = "60px";
bullet.style.bottom = playerY + 20 + "px";
let bulletInterval = setInterval(() => {
let bulletX = bullet.offsetLeft;
// Touch device detection
function isTouchDevice() {
return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
}
let checkShoot = document.querySelectorAll(".enemy");
checkShoot.forEach(enemy => {
checkCollision(bullet, enemy);
function setupTouchControls() {
if (isTouchDevice() && touchControls) {
touchControls.style.display = 'flex';
const upBtn = document.getElementById('touch-up');
const downBtn = document.getElementById('touch-down');
const shootBtn = document.getElementById('touch-shoot');
if (upBtn) {
upBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
if (playerY < gameAreaHeight - 60 && !isPaused) {
playerY += 40;
player.style.bottom = playerY + "px";
}
});
}
if (downBtn) {
downBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
if (playerY > 0 && !isPaused) {
playerY -= 40;
player.style.bottom = playerY + "px";
}
});
}
if (shootBtn) {
shootBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
if (!isPaused) {
shoot();
}
});
}
}
}
// Pause functionality
function togglePause() {
if (!isGameRunning) return;
isPaused = !isPaused;
if (pauseOverlay) {
pauseOverlay.style.display = isPaused ? 'flex' : 'none';
}
if (isPaused) {
if (backgroundMusic) backgroundMusic.pause();
} else {
if (backgroundMusic) backgroundMusic.play().catch(() => {});
}
}
// Visual feedback
function flashPlayer() {
if (!player) return;
player.classList.add('player-hit');
setTimeout(() => {
if (player) player.classList.remove('player-hit');
}, 300);
}
function enemyDeathEffect(enemy) {
enemy.classList.add('enemy-death');
setTimeout(() => {
enemy.remove();
}, 200);
}
// Setup Game Intro state
function introPage() {
gameIntro.style.display = 'block';
gameArea.style.display = 'none';
gameEnd.style.display = 'none';
if (gameVictory) gameVictory.style.display = 'none';
stats.style.display = 'none';
if (touchControls) touchControls.style.display = 'none';
displayHighScores();
}
// Initialize game
function initGame(hardcore = false) {
resetGame();
isHardcoreMode = hardcore;
lives = isHardcoreMode ? 1 : 3;
gameArea.style.width = gameAreaWidth + "px";
gameArea.style.height = gameAreaHeight + "px";
gameArea.style.position = "relative";
gameArea.style.overflow = "hidden";
gameIntro.style.display = 'none';
gameArea.style.display = 'block';
gameEnd.style.display = 'none';
if (gameVictory) gameVictory.style.display = 'none';
if (pauseOverlay) pauseOverlay.style.display = 'none';
stats.style.display = 'block';
scoreElement.innerText = score;
livesElement.innerText = lives;
timerElement.innerText = 'Time elapsed: 0:00';
isGameRunning = true;
// Start game systems
addInterval(timeGame, 1000, 'timer');
spawnPlayer();
spawnEnemy();
if (!isHardcoreMode) {
// Spawn power-ups every 15-20 seconds (not in hardcore)
schedulePowerUp();
}
playBackgroundMusic();
setupTouchControls();
}
// Start button handlers
function startGame() {
startButton.addEventListener('click', () => {
initGame(false);
});
if (hardcoreButton) {
hardcoreButton.addEventListener('click', () => {
initGame(true);
});
}
}
// Game Time
function timeGame() {
if (!isGameRunning) return;
sec++;
if (sec >= 60) {
min++;
sec = 0;
}
const displaySec = sec < 10 ? "0" + sec : sec;
timerElement.innerText = 'Time elapsed: ' + min + ':' + displaySec;
// Update difficulty every 30 seconds
const totalSeconds = min * 60 + sec;
difficultyLevel = 1 + Math.floor(totalSeconds / 30) * 0.2;
// Check for boss spawn (every 60 seconds)
if (totalSeconds > 0 && totalSeconds % 60 === 0 && totalSeconds !== lastBossTime) {
lastBossTime = totalSeconds;
spawnBoss();
}
// Win condition: survive 3 minutes
if (min >= 3) {
victoryGame();
}
}
function spawnPlayer() {
player = document.createElement("img");
player.src = "images/shooter.gif";
player.classList.add("player");
gameArea.appendChild(player);
player.style.bottom = playerY + "px";
// Setup keyboard controls (only once)
keydownHandler = (e) => {
if (isPaused && e.key !== "Escape" && e.key !== "p" && e.key !== "P") return;
if (e.key === "Escape" || e.key === "p" || e.key === "P") {
togglePause();
return;
}
if (e.key === "ArrowUp" && playerY < gameAreaHeight - 60) {
playerY += 40;
} else if (e.key === "ArrowDown" && playerY > 0) {
playerY -= 40;
} else if (e.key === " ") {
e.preventDefault();
shoot();
}
player.style.bottom = playerY + "px";
};
document.addEventListener("keydown", keydownHandler);
// Collision check interval
addInterval(() => {
if (!player) return;
// Check enemy collisions
document.querySelectorAll(".enemy").forEach(enemy => {
checkCollisionEnemy(player, enemy);
});
if (bulletX > gameAreaWidth) {
bullet.remove();
clearInterval(bulletInterval);
} else {
bullet.style.left = bulletX + 10 + "px";
}
}, 20);
// Check boss collisions
document.querySelectorAll(".boss").forEach(boss => {
checkCollisionEnemy(player, boss);
});
// Check power-up collisions
document.querySelectorAll(".power-up").forEach(powerUp => {
checkPowerUpCollision(player, powerUp);
});
}, 20, 'playerCollision');
}
function shoot() {
if (isPaused || !isGameRunning) return;
if (hasTripleShot) {
// Shoot 3 bullets
createBullet(playerY + 10);
createBullet(playerY + 20);
createBullet(playerY + 30);
} else {
createBullet(playerY + 20);
}
playSound("sounds/shoot.mp3");
}
function createBullet(yPosition) {
const bullet = document.createElement("div");
bullet.classList.add("bullet");
gameArea.appendChild(bullet);
bullet.style.left = "60px";
bullet.style.bottom = yPosition + "px";
const bulletInterval = addInterval(() => {
let bulletX = bullet.offsetLeft;
// Check collision with enemies
document.querySelectorAll(".enemy").forEach(enemy => {
checkCollision(bullet, enemy);
});
// Check collision with boss
document.querySelectorAll(".boss").forEach(boss => {
checkBossCollision(bullet, boss);
});
if (bulletX > gameAreaWidth || !bullet.parentNode) {
bullet.remove();
clearInterval(bulletInterval);
activeIntervals = activeIntervals.filter(i => i.id !== bulletInterval);
} else {
bullet.style.left = bulletX + 10 + "px";
}
}, 20, 'bullet');
}
// Calculate spawn delay based on difficulty
function getSpawnDelay() {
const baseDelay = isHardcoreMode ? 1500 : 2000;
const minDelay = isHardcoreMode ? 200 : 300;
const totalSeconds = min * 60 + sec;
return Math.max(minDelay, baseDelay - (totalSeconds * 5));
}
// Calculate enemy speed based on difficulty and type
function getEnemySpeed(baseSpeed) {
const multiplier = isHardcoreMode ? 1.5 : 1;
return Math.round(baseSpeed * difficultyLevel * multiplier);
}
// Enemy types
function getRandomEnemyType() {
const rand = Math.random();
if (rand < 0.6) {
return { type: 'normal', speed: 5, health: 1, class: 'enemy-normal' };
} else if (rand < 0.85) {
return { type: 'fast', speed: 8, health: 1, class: 'enemy-fast' };
} else {
return { type: 'tank', speed: 3, health: 2, class: 'enemy-tank' };
}
}
function spawnEnemy() {
if (!isGameRunning) return;
const enemyType = getRandomEnemyType();
const enemy = document.createElement("img");
enemy.src = "images/zombie.gif";
enemy.classList.add("enemy");
enemy.classList.add("enemy", enemyType.class);
enemy.dataset.health = enemyType.health;
enemy.dataset.type = enemyType.type;
gameArea.appendChild(enemy);
enemy.style.right = "0px";
enemy.style.bottom = Math.random() * (gameAreaHeight - 50) + "px";
enemyInterval = setInterval(() => {
const speed = getEnemySpeed(enemyType.speed);
const enemyInterval = addInterval(() => {
if (!enemy.parentNode) {
clearInterval(enemyInterval);
activeIntervals = activeIntervals.filter(i => i.id !== enemyInterval);
return;
}
let enemyX = enemy.offsetLeft;
if (enemyX < 0) {
enemy.remove();
clearInterval(enemyInterval);
activeIntervals = activeIntervals.filter(i => i.id !== enemyInterval);
} else {
enemy.style.right = parseInt(enemy.style.right) + 5 + "px";
enemy.style.right = parseInt(enemy.style.right) + speed + "px";
}
}, 50);
}, 50, 'enemy');
setTimeout(spawnEnemy, Math.random() * 2000 + 100);
// Schedule next enemy spawn
addTimeout(spawnEnemy, Math.random() * getSpawnDelay() + 100, 'spawnEnemy');
}
// Boss functionality
function spawnBoss() {
if (!isGameRunning) return;
const boss = document.createElement("img");
boss.src = "images/zombie.gif";
boss.classList.add("boss");
boss.dataset.health = 10;
boss.dataset.maxHealth = 10;
gameArea.appendChild(boss);
boss.style.right = "0px";
boss.style.bottom = (gameAreaHeight / 2 - 75) + "px";
// Create health bar
const healthBar = document.createElement("div");
healthBar.classList.add("boss-health-bar");
healthBar.innerHTML = '<div class="boss-health-fill"></div>';
boss.healthBar = healthBar;
gameArea.appendChild(healthBar);
const bossInterval = addInterval(() => {
if (!boss.parentNode) {
if (healthBar.parentNode) healthBar.remove();
clearInterval(bossInterval);
activeIntervals = activeIntervals.filter(i => i.id !== bossInterval);
return;
}
let bossX = boss.offsetLeft;
// Update health bar position
healthBar.style.left = (bossX - 20) + "px";
healthBar.style.bottom = (parseInt(boss.style.bottom) + 160) + "px";
if (bossX < 0) {
boss.remove();
healthBar.remove();
clearInterval(bossInterval);
activeIntervals = activeIntervals.filter(i => i.id !== bossInterval);
} else {
boss.style.right = parseInt(boss.style.right) + 2 + "px";
}
}, 50, 'boss');
playSound("sounds/lostlife.mp3"); // Boss spawn alert
}
function checkBossCollision(bullet, boss) {
if (!bullet.parentNode || !boss.parentNode) return;
if (bullet.offsetLeft < boss.offsetLeft + boss.offsetWidth &&
bullet.offsetLeft + bullet.offsetWidth > boss.offsetLeft &&
bullet.offsetTop < boss.offsetTop + boss.offsetHeight &&
bullet.offsetTop + bullet.offsetHeight > boss.offsetTop) {
bullet.remove();
let health = parseInt(boss.dataset.health);
health--;
boss.dataset.health = health;
// Update health bar
const maxHealth = parseInt(boss.dataset.maxHealth);
const healthPercent = (health / maxHealth) * 100;
const healthFill = boss.healthBar.querySelector('.boss-health-fill');
if (healthFill) {
healthFill.style.width = healthPercent + '%';
}
if (health <= 0) {
score += 10; // Boss worth more points
scoreElement.innerText = score;
if (boss.healthBar) boss.healthBar.remove();
enemyDeathEffect(boss);
}
}
}
// Power-ups
function schedulePowerUp() {
if (!isGameRunning || isHardcoreMode) return;
const delay = 10000 + Math.random() * 5000; // 10-15 seconds
addTimeout(() => {
spawnPowerUp();
schedulePowerUp();
}, delay, 'powerUpSchedule');
}
function spawnPowerUp() {
if (!isGameRunning || isPaused) return;
const types = ['extraLife', 'tripleShot', 'shield'];
const type = types[Math.floor(Math.random() * types.length)];
const powerUp = document.createElement("div");
powerUp.classList.add("power-up", `power-up-${type}`);
powerUp.dataset.type = type;
gameArea.appendChild(powerUp);
powerUp.style.right = "0px";
powerUp.style.bottom = Math.random() * (gameAreaHeight - 40) + "px";
// Set icon based on type
switch (type) {
case 'extraLife': powerUp.innerHTML = '+'; break;
case 'tripleShot': powerUp.innerHTML = '3'; break;
case 'shield': powerUp.innerHTML = 'S'; break;
}
const powerUpInterval = addInterval(() => {
if (!powerUp.parentNode) {
clearInterval(powerUpInterval);
activeIntervals = activeIntervals.filter(i => i.id !== powerUpInterval);
return;
}
// Move faster than enemies so players can catch them (7px/50ms)
powerUp.style.right = parseInt(powerUp.style.right) + 7 + "px";
// Remove when off-screen (left side)
if (parseInt(powerUp.style.right) > gameAreaWidth + 50) {
powerUp.remove();
clearInterval(powerUpInterval);
activeIntervals = activeIntervals.filter(i => i.id !== powerUpInterval);
}
}, 50, 'powerUp');
}
function checkPowerUpCollision(player, powerUp) {
if (!player || !powerUp.parentNode) return;
// Use getBoundingClientRect for accurate collision with right-positioned elements
const playerRect = player.getBoundingClientRect();
const powerUpRect = powerUp.getBoundingClientRect();
if (playerRect.left < powerUpRect.right &&
playerRect.right > powerUpRect.left &&
playerRect.top < powerUpRect.bottom &&
playerRect.bottom > powerUpRect.top) {
collectPowerUp(powerUp.dataset.type);
powerUp.remove();
playSound("sounds/shoot.mp3");
}
}
function collectPowerUp(type) {
switch (type) {
case 'extraLife':
lives++;
livesElement.innerText = lives;
break;
case 'tripleShot':
hasTripleShot = true;
if (tripleShotTimeout) clearTimeout(tripleShotTimeout);
tripleShotTimeout = setTimeout(() => {
hasTripleShot = false;
}, 10000);
break;
case 'shield':
hasShield = true;
player.classList.add('player-shield');
if (shieldTimeout) clearTimeout(shieldTimeout);
shieldTimeout = setTimeout(() => {
hasShield = false;
if (player) player.classList.remove('player-shield');
}, 5000);
break;
}
}
function checkCollision(bullet, enemy) {
if (!bullet.parentNode || !enemy.parentNode) return;
if (bullet.offsetLeft < enemy.offsetLeft + enemy.offsetWidth &&
bullet.offsetLeft + bullet.offsetWidth > enemy.offsetLeft &&
bullet.offsetTop < enemy.offsetTop + enemy.offsetHeight &&
bullet.offsetTop + bullet.offsetHeight > enemy.offsetTop) {
bullet.remove();
enemy.remove();
score++;
scoreElement.innerText = score;
let health = parseInt(enemy.dataset.health);
health--;
enemy.dataset.health = health;
if (health <= 0) {
score++;
scoreElement.innerText = score;
enemyDeathEffect(enemy);
}
}
}
function checkPlayerCollisions(){
let checkPlayerColision = document.querySelectorAll(".enemy");
checkPlayerColision.forEach(enemy => {
checkCollisionEnemy(player, enemy);
});
}
function checkCollisionEnemy(player, enemy) {
if (!player || !enemy.parentNode) return;
if (player.offsetLeft < enemy.offsetLeft + enemy.offsetWidth &&
player.offsetLeft + player.offsetWidth > enemy.offsetLeft &&
player.offsetTop < enemy.offsetTop + enemy.offsetHeight &&
player.offsetTop + player.offsetHeight > enemy.offsetTop) {
enemy.remove();
// Shield protects from damage
if (hasShield) {
return;
}
lives--;
livesElement.innerText = lives;
flashPlayer();
if (lives === 0) {
if (lives <= 0) {
endGame();
} else {
playSound("sounds/lostlife.mp3");
@ -196,42 +723,96 @@ function checkCollisionEnemy(player, enemy) {
}
}
// Victory screen
function victoryGame() {
isGameRunning = false;
clearAllIntervals();
stopBackgroundMusic();
gameArea.style.display = 'none';
if (gameVictory) {
gameVictory.style.display = 'block';
}
stats.style.display = 'block';
if (touchControls) touchControls.style.display = 'none';
const displaySec = sec < 10 ? "0" + sec : sec;
if (victoryScoreElement) victoryScoreElement.innerText = score;
if (victoryTimeElement) victoryTimeElement.innerText = 'Time survived: ' + min + ':' + displaySec;
const isNewRecord = updateHighScore();
if (victoryHighScore) {
victoryHighScore.innerText = isNewRecord ? 'NEW HIGH SCORE!' : 'High Score: ' + getHighScore();
}
playSound("sounds/shoot.mp3"); // Victory sound
// Cleanup
document.querySelectorAll(".enemy").forEach(e => e.remove());
document.querySelectorAll(".bullet").forEach(b => b.remove());
document.querySelectorAll(".boss").forEach(b => b.remove());
document.querySelectorAll(".power-up").forEach(p => p.remove());
document.querySelectorAll(".boss-health-bar").forEach(h => h.remove());
}
// Setup End game state
function endGame() {
clearInterval(intervalTimer);
isGameRunning = false;
clearAllIntervals();
stopBackgroundMusic();
gameArea.style.display = 'none';
gameEnd.style.display = 'block';
stats.style.display = 'block';
if (touchControls) touchControls.style.display = 'none';
// stats on Game Over screen
const displaySec = sec < 10 ? "0" + sec : sec;
finalScoreElement.innerText = score;
finalTimeElement.innerText = 'Time elapsed: ' + min + ':' + sec;
finalTimeElement.innerText = 'Time elapsed: ' + min + ':' + displaySec;
const isNewRecord = updateHighScore();
if (finalHighScore) {
finalHighScore.innerText = isNewRecord ? 'NEW HIGH SCORE!' : 'High Score: ' + getHighScore();
}
playSound("sounds/gameover.mp3");
// Remove enemys and bullets
document.querySelectorAll(".enemy").forEach(enemy => enemy.remove());
document.querySelectorAll(".bullet").forEach(bullet => bullet.remove());
// Cleanup
document.querySelectorAll(".enemy").forEach(e => e.remove());
document.querySelectorAll(".bullet").forEach(b => b.remove());
document.querySelectorAll(".boss").forEach(b => b.remove());
document.querySelectorAll(".power-up").forEach(p => p.remove());
document.querySelectorAll(".boss-health-bar").forEach(h => h.remove());
}
// Restart game when the button restart is pressed
// Restart game handlers
restartButton.addEventListener('click', () => {
gameEnd.style.display = 'none';
gameArea.style.display = 'block';
stats.style.display = 'block';
sec = 0;
min = 0;
lives = 3;
score = 0;
scoreElement.innerText = score;
livesElement.innerText = lives;
clearInterval(intervalTimer); // clear time interval
clearInterval(enemyInterval); // clear enemy interval
document.querySelectorAll(".enemy").forEach(enemy => enemy.remove()); //make sure have no enemys and also fix the bug of have a ghost enemy after restart
intervalTimer = setInterval(timeGame, 1000);
//spawnEnemy();
initGame(isHardcoreMode);
});
// Start the initial function introPage() and startGame();
if (victoryRestartButton) {
victoryRestartButton.addEventListener('click', () => {
initGame(isHardcoreMode);
});
}
// Resume button in pause menu
const resumeButton = document.getElementById('resume-button');
if (resumeButton) {
resumeButton.addEventListener('click', () => {
togglePause();
});
}
// Main menu button in pause
const mainMenuButton = document.getElementById('main-menu-button');
if (mainMenuButton) {
mainMenuButton.addEventListener('click', () => {
resetGame();
introPage();
});
}
// Start the initial function
introPage();
startGame();

419
style.css
View file

@ -1,6 +1,15 @@
/* Base styles */
body {
margin: 0;
padding: 0;
text-align: center;
box-sizing: border-box;
font-family: 'Verdana', sans-serif;
}
/* Game Area */
#gameArea {
position: relative;
/*width: 100vw;*/
height: 100vh;
background: radial-gradient(rgb(230, 231, 230), rgb(223, 226, 223));
padding: 20px 0px;
@ -9,11 +18,11 @@
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
text-align: center;
box-sizing: border-box;
/* Intro Screen */
#game-intro {
position: relative;
min-height: 100vh;
padding: 20px;
}
#game-intro::before {
@ -32,9 +41,9 @@ body {
.game-intro p {
font-size: 18px;
font-family: "Verdana";
}
/* Buttons */
body button {
font-size: 30px;
background-color: #870007;
@ -44,21 +53,99 @@ body button {
box-shadow: 0;
border-radius: 5px;
margin-bottom: 20px;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}
body button:hover {
background-color: #a50009;
transform: scale(1.02);
}
body button:active {
transform: scale(0.98);
}
.button-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
#hardcore-button {
background-color: #2a0003;
font-size: 24px;
padding: 15px 30px;
}
#hardcore-button:hover {
background-color: #4a0005;
}
/* High Score Display */
#high-score-display {
font-size: 18px;
color: #fff;
background-color: rgba(0, 0, 0, 0.6);
padding: 10px 20px;
border-radius: 5px;
display: inline-block;
margin: 10px 0;
}
.controls-hint {
font-size: 14px;
color: #fff;
background-color: rgba(0, 0, 0, 0.4);
padding: 5px 15px;
border-radius: 3px;
display: inline-block;
}
/* Player */
.player {
position: absolute;
left: 20px;
bottom: 5 0%;
bottom: 50%;
width: 75px;
height: auto;
transition: bottom 0.05s ease-out;
}
/* Player hit animation */
.player-hit {
animation: playerHitFlash 0.3s ease-out;
}
@keyframes playerHitFlash {
0%, 100% { filter: none; }
25% { filter: brightness(0.5) sepia(1) hue-rotate(-50deg) saturate(5); }
50% { filter: brightness(2); }
75% { filter: brightness(0.5) sepia(1) hue-rotate(-50deg) saturate(5); }
}
/* Player shield effect */
.player-shield {
animation: shieldGlow 0.5s ease-in-out infinite alternate;
filter: drop-shadow(0 0 10px #00bfff);
}
@keyframes shieldGlow {
0% { filter: drop-shadow(0 0 5px #00bfff); }
100% { filter: drop-shadow(0 0 15px #00bfff) brightness(1.2); }
}
/* Bullet */
.bullet {
position: absolute;
width: 10px;
height: 5px;
background-color: black;
}
/* Enemy Base Styles */
.enemy {
position: absolute;
width: 75px;
@ -67,3 +154,319 @@ body button {
bottom: 50%;
transform: scaleX(-1);
}
/* Enemy Types */
.enemy-normal {
/* Default zombie appearance */
}
.enemy-fast {
filter: hue-rotate(90deg) brightness(1.2);
width: 65px;
}
.enemy-tank {
filter: hue-rotate(-30deg) saturate(1.5);
width: 90px;
}
/* Enemy death animation */
.enemy-death {
animation: enemyDeath 0.2s ease-out forwards;
}
@keyframes enemyDeath {
0% {
opacity: 1;
transform: scaleX(-1) scale(1);
}
100% {
opacity: 0;
transform: scaleX(-1) scale(1.5);
}
}
/* Boss */
.boss {
position: absolute;
width: 150px;
height: auto;
right: 0;
transform: scaleX(-1);
filter: hue-rotate(180deg) saturate(2) brightness(0.8);
z-index: 10;
}
/* Boss Health Bar */
.boss-health-bar {
position: absolute;
width: 120px;
height: 12px;
background-color: #333;
border: 2px solid #000;
border-radius: 6px;
overflow: hidden;
z-index: 11;
}
.boss-health-fill {
width: 100%;
height: 100%;
background: linear-gradient(to bottom, #ff4444, #aa0000);
transition: width 0.2s ease-out;
}
/* Power-ups */
.power-up {
position: absolute;
width: 45px;
height: 45px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
color: #fff;
animation: powerUpPulse 0.5s ease-in-out infinite alternate;
z-index: 5;
box-shadow: 0 0 15px rgba(255, 255, 255, 0.8);
}
@keyframes powerUpPulse {
0% { transform: scale(1); box-shadow: 0 0 10px currentColor; }
100% { transform: scale(1.1); box-shadow: 0 0 20px currentColor; }
}
.power-up-extraLife {
background: linear-gradient(135deg, #00ff00, #008800);
border: 3px solid #00cc00;
}
.power-up-tripleShot {
background: linear-gradient(135deg, #ffff00, #ccaa00);
border: 3px solid #ffcc00;
color: #000;
}
.power-up-shield {
background: linear-gradient(135deg, #00bfff, #0066cc);
border: 3px solid #00aaff;
}
/* Pause Overlay */
#pause-overlay {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
justify-content: center;
align-items: center;
z-index: 100;
}
.pause-content {
text-align: center;
color: #fff;
}
.pause-content h1 {
font-size: 48px;
margin-bottom: 30px;
text-shadow: 2px 2px 4px #000;
}
.pause-content button {
display: block;
margin: 10px auto;
min-width: 200px;
}
/* Game End Screen */
#game-end {
display: none;
padding: 50px;
min-height: 100vh;
background: linear-gradient(to bottom, #1a0000, #4a0000);
color: #fff;
}
#game-end h1 {
font-size: 48px;
color: #ff4444;
text-shadow: 2px 2px 4px #000;
}
#game-end p {
font-size: 24px;
margin: 15px 0;
}
#final-high-score {
color: #ffcc00;
font-weight: bold;
}
/* Victory Screen */
#game-victory {
display: none;
padding: 50px;
min-height: 100vh;
background: linear-gradient(to bottom, #001a00, #004a00);
color: #fff;
}
#game-victory h1 {
font-size: 64px;
color: #44ff44;
text-shadow: 2px 2px 4px #000;
animation: victoryPulse 1s ease-in-out infinite alternate;
}
@keyframes victoryPulse {
0% { transform: scale(1); }
100% { transform: scale(1.05); text-shadow: 0 0 30px #44ff44; }
}
#game-victory p {
font-size: 24px;
margin: 15px 0;
}
#victory-high-score {
color: #ffcc00;
font-weight: bold;
}
/* Volume Control */
#volume-control {
position: absolute;
top: 10px;
right: 10px;
z-index: 50;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
color: #fff;
}
#volume-control label {
margin-right: 5px;
}
/* Stats */
#stats {
position: absolute;
top: 10px;
left: 10px;
text-align: left;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
color: #fff;
z-index: 50;
}
#stats h2 {
margin: 5px 0;
font-size: 16px;
}
/* Touch Controls */
#touch-controls {
display: none;
position: fixed;
bottom: 20px;
left: 0;
right: 0;
justify-content: space-between;
padding: 0 20px;
z-index: 200;
pointer-events: none;
}
.touch-move {
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: auto;
}
.touch-btn {
width: 70px;
height: 70px;
font-size: 16px;
padding: 10px;
background-color: rgba(135, 0, 7, 0.8);
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 10px;
pointer-events: auto;
touch-action: manipulation;
}
.touch-btn:active {
background-color: rgba(200, 0, 10, 0.9);
transform: scale(0.95);
}
.touch-shoot-btn {
width: 100px;
height: 100px;
font-size: 20px;
border-radius: 50%;
align-self: center;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
body button {
font-size: 24px;
padding: 15px 30px;
}
#hardcore-button {
font-size: 18px;
padding: 12px 24px;
}
.player {
width: 60px;
}
.enemy {
width: 60px;
}
.enemy-fast {
width: 50px;
}
.enemy-tank {
width: 75px;
}
.boss {
width: 120px;
}
#stats h2 {
font-size: 14px;
}
#game-end h1,
#game-victory h1 {
font-size: 36px;
}
#game-end p,
#game-victory p {
font-size: 18px;
}
.pause-content h1 {
font-size: 36px;
}
}