Scriptocalypse/script.js
Richard Nixon d49bed1541 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
2026-01-24 21:44:47 +00:00

818 lines
24 KiB
JavaScript

// 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');
// Game state variables
let player = null;
let activeIntervals = [];
let activeTimeouts = [];
let sec = 0;
let min = 0;
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;
// Interval management helpers
function addInterval(fn, ms, label = '') {
const id = setInterval(() => {
if (!isPaused) {
fn();
}
}, ms);
activeIntervals.push({ id, label });
return id;
}
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
});
}
function stopBackgroundMusic() {
if (backgroundMusic) {
backgroundMusic.pause();
backgroundMusic.currentTime = 0;
}
}
// Volume control
volumeSlider.addEventListener('input', (event) => {
gameVolume = parseFloat(event.target.value);
if (backgroundMusic) {
backgroundMusic.volume = gameVolume;
}
});
function playSound(src) {
const sound = new Audio(src);
sound.volume = gameVolume;
sound.play();
}
// Touch device detection
function isTouchDevice() {
return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
}
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);
});
// 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", 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";
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) + speed + "px";
}
}, 50, 'enemy');
// 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();
let health = parseInt(enemy.dataset.health);
health--;
enemy.dataset.health = health;
if (health <= 0) {
score++;
scoreElement.innerText = score;
enemyDeathEffect(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) {
endGame();
} else {
playSound("sounds/lostlife.mp3");
}
}
}
// 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() {
isGameRunning = false;
clearAllIntervals();
stopBackgroundMusic();
gameArea.style.display = 'none';
gameEnd.style.display = 'block';
stats.style.display = 'block';
if (touchControls) touchControls.style.display = 'none';
const displaySec = sec < 10 ? "0" + sec : sec;
finalScoreElement.innerText = score;
finalTimeElement.innerText = 'Time elapsed: ' + min + ':' + displaySec;
const isNewRecord = updateHighScore();
if (finalHighScore) {
finalHighScore.innerText = isNewRecord ? 'NEW HIGH SCORE!' : 'High Score: ' + getHighScore();
}
playSound("sounds/gameover.mp3");
// 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 handlers
restartButton.addEventListener('click', () => {
initGame(isHardcoreMode);
});
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();