From 568a1cae782e5b80e8a99f23c6433f6d54dbece9 Mon Sep 17 00:00:00 2001 From: Ronnie Date: Tue, 27 May 2025 19:05:54 -0400 Subject: [PATCH] first commit --- README.md | 32 ++ game.js | 897 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 92 ++++++ styles.css | 645 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 1666 insertions(+) create mode 100644 README.md create mode 100644 game.js create mode 100644 index.html create mode 100644 styles.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..8712de4 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# ZoneOut + +A fast-paced square collection game where you select and clear bouncing squares. Choose your difficulty and game mode, then try to achieve the highest score! + +## Game Modes +- **Levels**: Progress through increasingly challenging levels +- **Endless**: Play indefinitely with increasing difficulty + +## Difficulty Levels +- **Easy**: Infinite lives, slower squares, 20% red squares +- **Medium**: 5 lives, moderate speed, 30% red squares +- **Hard**: 3 lives, fast squares, 40% red squares + +## How to Play +1. Select difficulty and game mode +2. Click individual squares or drag to select multiple squares +3. Green squares: +1 point +4. Yellow squares: +3 points +5. Red squares: -1 life (in Medium and Hard modes) +6. Complete levels quickly for time bonuses +7. Maintain combos for additional bonuses + +## Features +- Smooth selection system with visual feedback +- Combo system for consecutive collections +- Time bonus system with detailed breakdown +- Particle effects and animations +- Modern UI with glassmorphism effects +- Responsive design that adapts to window size + +## Running the Game +Open `index.html` in a modern web browser to play. \ No newline at end of file diff --git a/game.js b/game.js new file mode 100644 index 0000000..bd287d8 --- /dev/null +++ b/game.js @@ -0,0 +1,897 @@ +// Game settings - tweak these to adjust difficulty and feel +const gameConfig = { + difficulties: { + easy: { + lives: Infinity, + timer: 180, // 3 min timer + baseSpeed: 2, + speedVariation: 0.8, // Makes squares move at slightly different speeds + initialSquares: 10, + maxSquares: 15, + squaresPerLevel: 2, + speedIncrease: 0.2, + redSquareRatio: 0.2, // 1 in 5 squares are red + timeBonus: 30 // 30s bonus for completing level + }, + medium: { + lives: 5, + timer: 120, // 2 min timer + baseSpeed: 3, + speedVariation: 0.8, + initialSquares: 15, + maxSquares: 20, + squaresPerLevel: 3, + speedIncrease: 0.3, + redSquareRatio: 0.3, // 1 in 3 squares are red + timeBonus: 20 + }, + hard: { + lives: 3, + timer: 90, // 1.5 min timer + baseSpeed: 4, + speedVariation: 1, + initialSquares: 20, + maxSquares: 25, + squaresPerLevel: 4, + speedIncrease: 0.4, + redSquareRatio: 0.4, // 2 in 5 squares are red + timeBonus: 15 + } + }, + colors: { + good: '#4ecca3', // Green + bad: '#ff6b6b', // Red + bonus: '#ffd93d' // Yellow + }, + trailLength: 20, // How long the square trails are + particleCount: 20, // Number of particles in explosions + trailParticleSize: 3, // Size of trail particles + sparkleCount: 8, // Number of sparkles around bonus squares + backgroundStars: 100 // Number of stars in the background +}; + +// Cookie handling - saves player preferences +function setCookie(name, value, days) { + const date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + const expires = "expires=" + date.toUTCString(); + document.cookie = name + "=" + value + ";" + expires + ";path=/"; +} + +function getCookie(name) { + const nameEQ = name + "="; + const ca = document.cookie.split(';'); + for(let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +// Game state - tracks everything that changes during gameplay +let gameState = { + selectedDifficulty: getCookie('difficulty') || 'medium', + selectedMode: getCookie('mode') || 'levels', + isPlaying: false, + score: 0, + level: 1, + lives: 5, + startTime: null, + gameTime: 0, + currentSpeed: 0, + levelStartTime: null, + levelTime: 0, + remainingTime: 0, + currentCombo: 0, + maxCombo: 0, + lastUpdateTime: null // Used for accurate timer updates +}; + +// DOM Elements +const difficultyButtons = document.querySelectorAll('.difficulty-btn'); +const modeButtons = document.querySelectorAll('.mode-btn'); +const restartButton = document.getElementById('restart-game'); + +// Add popup elements +const lifeLossPopup = document.createElement('div'); +lifeLossPopup.className = 'life-loss-popup'; +document.body.appendChild(lifeLossPopup); + +const pointGainPopup = document.createElement('div'); +pointGainPopup.className = 'point-gain-popup'; +document.body.appendChild(pointGainPopup); + +function showLifeLossPopup(livesLost) { + lifeLossPopup.textContent = `-${livesLost} ${livesLost === 1 ? 'Life' : 'Lives'}`; + lifeLossPopup.classList.add('show'); + setTimeout(() => { + lifeLossPopup.classList.remove('show'); + }, 1000); +} + +function showPointGainPopup(points) { + pointGainPopup.textContent = `+${points} ${points === 1 ? 'Point' : 'Points'}`; + pointGainPopup.classList.add('show'); + setTimeout(() => { + pointGainPopup.classList.remove('show'); + }, 1000); +} + +// Add death screen handling +const deathScreen = document.querySelector('.death-screen'); +const finalScore = document.querySelector('.final-score'); +const finalLevel = document.querySelector('.final-level'); +const finalTime = document.querySelector('.final-time'); + +function showDeathScreen() { + // Update final stats + finalScore.textContent = gameState.score; + finalLevel.textContent = gameState.level; + + // Format time + const minutes = Math.floor(gameState.gameTime / 60); + const seconds = Math.floor(gameState.gameTime % 60); + finalTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; + + // Show death screen + deathScreen.classList.add('show'); +} + +// Set initial active states +function setInitialStates() { + // Set difficulty + difficultyButtons.forEach(button => { + if (button.dataset.difficulty === gameState.selectedDifficulty) { + button.classList.add('active'); + } + }); + + // Set game mode + modeButtons.forEach(button => { + if (button.dataset.mode === gameState.selectedMode) { + button.classList.add('active'); + } + }); + + // Show initial explanations + document.querySelector(`.${gameState.selectedDifficulty}-info`).style.display = 'block'; + document.querySelector(`.${gameState.selectedMode}-info`).style.display = 'block'; +} + +// Event Listeners +difficultyButtons.forEach(button => { + button.addEventListener('click', () => { + difficultyButtons.forEach(btn => btn.classList.remove('active')); + button.classList.add('active'); + gameState.selectedDifficulty = button.dataset.difficulty; + setCookie('difficulty', gameState.selectedDifficulty, 365); + restartGame(); + }); +}); + +modeButtons.forEach(button => { + button.addEventListener('click', () => { + modeButtons.forEach(btn => btn.classList.remove('active')); + button.classList.add('active'); + gameState.selectedMode = button.dataset.mode; + setCookie('mode', gameState.selectedMode, 365); + restartGame(); + }); +}); + +restartButton.addEventListener('click', restartGame); + +// Set initial states when the page loads +setInitialStates(); + +// Start game automatically +window.addEventListener('load', () => { + startGame(); +}); + +function restartGame() { + // Clear timer interval + if (gameState.timerInterval) { + clearInterval(gameState.timerInterval); + } + + // Remove existing canvas if it exists + const existingCanvas = document.getElementById('gameCanvas'); + if (existingCanvas) { + existingCanvas.remove(); + } + + // Reset game state + gameState.score = 0; + gameState.level = 1; + gameState.lives = 5; + gameState.currentSpeed = 0; + gameState.currentCombo = 0; + gameState.maxCombo = 0; + updateStats(); + + // Reset timers + gameState.startTime = null; + gameState.levelStartTime = null; + gameState.lastUpdateTime = null; + gameState.gameTime = 0; + gameState.levelTime = 0; + gameState.remainingTime = 0; + + // Start new game + startGame(); +} + +function startGame() { + gameState.isPlaying = true; + gameState.startTime = Date.now(); + gameState.levelStartTime = Date.now(); + gameState.lastUpdateTime = Date.now(); + gameState.gameTime = 0; + gameState.levelTime = 0; + + // Initialize remaining time based on difficulty + const config = gameConfig.difficulties[gameState.selectedDifficulty]; + gameState.remainingTime = config.timer; + + // Hide death screen + deathScreen.classList.remove('show'); + + // Create game canvas + const canvas = document.createElement('canvas'); + canvas.id = 'gameCanvas'; + document.querySelector('.container').appendChild(canvas); + + // Initialize game + initializeGame(); + updateStats(); + + // Start timer update interval + startTimerUpdate(); +} + +// Creates a particle effect when squares are collected +function createParticle(x, y, color) { + const particle = document.createElement('div'); + particle.className = 'particle'; + particle.style.left = `${x}px`; + particle.style.top = `${y}px`; + particle.style.backgroundColor = color; + particle.style.width = '10px'; + particle.style.height = '10px'; + particle.style.borderRadius = '50%'; + document.body.appendChild(particle); + + // Remove particle after animation + setTimeout(() => particle.remove(), 500); +} + +// Creates sparkle effects around bonus squares +function createSparkle(x, y) { + const sparkle = document.createElement('div'); + sparkle.className = 'sparkle'; + sparkle.style.left = `${x}px`; + sparkle.style.top = `${y}px`; + sparkle.style.width = '20px'; + sparkle.style.height = '20px'; + sparkle.style.background = 'radial-gradient(circle, #fff 0%, transparent 70%)'; + document.body.appendChild(sparkle); + + setTimeout(() => sparkle.remove(), 1000); +} + +// Checks if level is complete - either no squares left or only red squares +function isLevelComplete(squares) { + if (squares.length === 0) return true; + return squares.every(square => square.color === gameConfig.colors.bad); +} + +// Calculates how many squares should be in the current level +function getSquaresForLevel() { + const config = gameConfig.difficulties[gameState.selectedDifficulty]; + return Math.min( + config.initialSquares + (gameState.level - 1) * config.squaresPerLevel, + config.maxSquares + ); +} + +// Gets the speed for the current level +function getSpeedForLevel() { + const config = gameConfig.difficulties[gameState.selectedDifficulty]; + return config.baseSpeed + (gameState.level - 1) * config.speedIncrease; +} + +// Calculates how many red squares should be in the current level +function getRedSquaresForLevel() { + const config = gameConfig.difficulties[gameState.selectedDifficulty]; + const totalSquares = getSquaresForLevel(); + const redSquares = Math.floor(totalSquares * config.redSquareRatio); + return Math.min(redSquares, totalSquares - 2); // Always leave at least 2 non-red squares +} + +// Spawns a new square with random properties +function spawnSquare(canvas, squares, isRed = false) { + const size = 30; + const config = gameConfig.difficulties[gameState.selectedDifficulty]; + const baseSpeed = getSpeedForLevel(); + + // Add some randomness to square speeds + const speedVariation = (Math.random() - 0.5) * config.speedVariation; + const speed = baseSpeed + speedVariation; + + // Random direction + const angle = Math.random() * Math.PI * 2; + const vx = Math.cos(angle) * speed; + const vy = Math.sin(angle) * speed; + + const square = { + x: Math.random() * (canvas.width - size), + y: Math.random() * (canvas.height - size), + size: size, + vx: vx, + vy: vy, + color: isRed ? gameConfig.colors.bad : + Math.random() < 0.7 ? gameConfig.colors.good : gameConfig.colors.bonus, + trail: [], + isHighlighted: false + }; + squares.push(square); +} + +// Handles level completion - calculates bonuses and spawns next level +function handleLevelCompletion(squares) { + const config = gameConfig.difficulties[gameState.selectedDifficulty]; + + // Calculate time bonuses + const baseTimeBonus = config.timeBonus; + const levelTime = (Date.now() - gameState.levelStartTime) / 1000; + const speedBonus = Math.max(0, Math.floor((config.timer - levelTime) * 0.5)); + const comboBonus = Math.min(30, gameState.maxCombo * 2); // 2s per combo, max 30s + + const finalBonus = baseTimeBonus + speedBonus + comboBonus; + + // Show bonus breakdown + showTimeBonusPopup(finalBonus, { + base: baseTimeBonus, + speed: speedBonus, + combo: comboBonus + }); + + // Clear squares with effects + squares.forEach(square => { + for (let i = 0; i < gameConfig.particleCount * 2; i++) { + const angle = (Math.PI * 2 * i) / (gameConfig.particleCount * 2); + const x = square.x + square.size / 2; + const y = square.y + square.size / 2; + createParticle(x, y, square.color); + } + }); + + squares.length = 0; + gameState.level++; + updateStats(); + + // Reset combo tracking + gameState.currentCombo = 0; + gameState.maxCombo = 0; + + // Spawn next level + const canvas = document.getElementById('gameCanvas'); + const numSquares = getSquaresForLevel(); + const numRedSquares = getRedSquaresForLevel(); + + // Spawn red squares first + for (let i = 0; i < numRedSquares; i++) { + spawnSquare(canvas, squares, true); + } + + // Spawn remaining squares + for (let i = numRedSquares; i < numSquares; i++) { + spawnSquare(canvas, squares, false); + } + + // Reset level timer with bonus + gameState.levelStartTime = Date.now() - (finalBonus * 1000); + gameState.levelTime = 0; +} + +// Handles square collection - scoring, effects, and game state updates +function handleSquareClick(square, squares) { + const index = squares.indexOf(square); + if (index > -1) { + // Handle scoring + if (square.color === gameConfig.colors.good) { + gameState.score += 1; + gameState.currentCombo++; + showPointGainPopup(1); + } else if (square.color === gameConfig.colors.bonus) { + gameState.score += 3; + gameState.currentCombo += 2; + showPointGainPopup(3); + } + + // Handle red squares + if (square.color === gameConfig.colors.bad) { + const config = gameConfig.difficulties[gameState.selectedDifficulty]; + if (config.lives !== Infinity) { + gameState.lives--; + gameState.currentCombo = 0; + showLifeLossPopup(1); + + if (gameState.lives <= 0) { + gameState.isPlaying = false; + showDeathScreen(); + } + } + } + + // Create effects + for (let i = 0; i < gameConfig.particleCount; i++) { + const angle = (Math.PI * 2 * i) / gameConfig.particleCount; + const x = square.x + square.size / 2; + const y = square.y + square.size / 2; + createParticle(x, y, square.color); + } + + // Extra sparkles for bonus squares + if (square.color === gameConfig.colors.bonus) { + for (let i = 0; i < gameConfig.sparkleCount; i++) { + const angle = (Math.PI * 2 * i) / gameConfig.sparkleCount; + const x = square.x + square.size / 2 + Math.cos(angle) * 20; + const y = square.y + square.size / 2 + Math.sin(angle) * 20; + createSparkle(x, y); + } + } + + squares.splice(index, 1); + + // Update max combo + if (gameState.currentCombo > gameState.maxCombo) { + gameState.maxCombo = gameState.currentCombo; + } + + // Check for level completion + if (isLevelComplete(squares)) { + handleLevelCompletion(squares); + } + + updateStats(); + } +} + +function initializeGame() { + const canvas = document.getElementById('gameCanvas'); + const ctx = canvas.getContext('2d'); + + // Set canvas size to fullscreen + function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + + // Create background stars + const stars = Array.from({ length: gameConfig.backgroundStars }, () => ({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + size: Math.random() * 2 + 1, + speed: Math.random() * 0.5 + 0.1, + opacity: Math.random() * 0.5 + 0.3 + })); + + // Game objects + const squares = []; + let selectionBox = null; + let isSelecting = false; + let startX, startY; + + // Spawn initial squares with correct red square ratio + const numSquares = getSquaresForLevel(); + const numRedSquares = getRedSquaresForLevel(); + + // Spawn red squares first + for (let i = 0; i < numRedSquares; i++) { + spawnSquare(canvas, squares, true); + } + + // Spawn remaining squares + for (let i = numRedSquares; i < numSquares; i++) { + spawnSquare(canvas, squares, false); + } + + // Function to handle selected squares + function handleSelectedSquares(selectionBox) { + if (!selectionBox) return; + + // Calculate selection bounds + const left = Math.min(selectionBox.x, selectionBox.x + selectionBox.width); + const right = Math.max(selectionBox.x, selectionBox.x + selectionBox.width); + const top = Math.min(selectionBox.y, selectionBox.y + selectionBox.height); + const bottom = Math.max(selectionBox.y, selectionBox.y + selectionBox.height); + + // Check for squares in selection + const selectedSquares = squares.filter(square => { + const squareCenterX = square.x + square.size / 2; + const squareCenterY = square.y + square.size / 2; + + return squareCenterX >= left && + squareCenterX <= right && + squareCenterY >= top && + squareCenterY <= bottom; + }); + + // Handle selected squares + selectedSquares.forEach(square => handleSquareClick(square, squares)); + + updateStats(); + } + + // Function to clear selection + function clearSelection() { + if (isSelecting && selectionBox) { + // Handle any squares in the selection before clearing + handleSelectedSquares(selectionBox); + } + isSelecting = false; + selectionBox = null; + } + + // Mouse event listeners + canvas.addEventListener('mousedown', (e) => { + isSelecting = true; + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + startX = (e.clientX - rect.left) * scaleX; + startY = (e.clientY - rect.top) * scaleY; + selectionBox = { + x: startX, + y: startY, + width: 0, + height: 0 + }; + }); + + canvas.addEventListener('mousemove', (e) => { + if (!isSelecting) return; + + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + const currentX = (e.clientX - rect.left) * scaleX; + const currentY = (e.clientY - rect.top) * scaleY; + + // Update selection box + selectionBox.width = currentX - startX; + selectionBox.height = currentY - startY; + + // Highlight squares that would be selected + squares.forEach(square => { + const squareCenterX = square.x + square.size / 2; + const squareCenterY = square.y + square.size / 2; + const left = Math.min(selectionBox.x, selectionBox.x + selectionBox.width); + const right = Math.max(selectionBox.x, selectionBox.x + selectionBox.width); + const top = Math.min(selectionBox.y, selectionBox.y + selectionBox.height); + const bottom = Math.max(selectionBox.y, selectionBox.y + selectionBox.height); + + square.isHighlighted = squareCenterX >= left && + squareCenterX <= right && + squareCenterY >= top && + squareCenterY <= bottom; + }); + }); + + canvas.addEventListener('mouseup', (e) => { + if (!isSelecting) return; + clearSelection(); + }); + + // Handle window blur + window.addEventListener('blur', () => { + clearSelection(); + }); + + // Handle mouse leave + canvas.addEventListener('mouseleave', () => { + clearSelection(); + }); + + // Handle mouse up outside canvas + document.addEventListener('mouseup', () => { + if (isSelecting) { + clearSelection(); + } + }); + + // Game loop + function gameLoop() { + if (!gameState.isPlaying) return; + + // Update game time + if (gameState.startTime) { + gameState.gameTime = (Date.now() - gameState.startTime) / 1000; + } + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw animated background + const time = Date.now() / 1000; + + // Draw stars + stars.forEach(star => { + star.y += star.speed; + if (star.y > canvas.height) { + star.y = 0; + star.x = Math.random() * canvas.width; + } + + const pulse = Math.sin(time * 2 + star.x) * 0.2 + 0.8; + ctx.beginPath(); + ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2); + ctx.fillStyle = `rgba(255, 255, 255, ${star.opacity * pulse})`; + ctx.fill(); + }); + + // Draw selection box + if (selectionBox) { + // Draw selection box with gradient + const gradient = ctx.createLinearGradient( + selectionBox.x, selectionBox.y, + selectionBox.x + selectionBox.width, + selectionBox.y + selectionBox.height + ); + gradient.addColorStop(0, 'rgba(96, 165, 250, 0.2)'); + gradient.addColorStop(1, 'rgba(96, 165, 250, 0.1)'); + + ctx.fillStyle = gradient; + ctx.fillRect(selectionBox.x, selectionBox.y, selectionBox.width, selectionBox.height); + + ctx.strokeStyle = '#60a5fa'; + ctx.lineWidth = 2; + ctx.strokeRect(selectionBox.x, selectionBox.y, selectionBox.width, selectionBox.height); + } + + // Update and draw squares + squares.forEach(square => { + // Update trail + const trailPoint = { + x: square.x + square.size / 2, + y: square.y + square.size / 2, + size: gameConfig.trailParticleSize, + alpha: 1, + angle: Math.atan2(square.vy, square.vx) + }; + + square.trail.unshift(trailPoint); + if (square.trail.length > gameConfig.trailLength) { + square.trail.pop(); + } + + // Update position + square.x += square.vx; + square.y += square.vy; + + // Bounce off walls + if (square.x <= 0 || square.x + square.size >= canvas.width) { + square.vx *= -1; + } + if (square.y <= 0 || square.y + square.size >= canvas.height) { + square.vy *= -1; + } + + // Draw trail + square.trail.forEach((particle, index) => { + const progress = index / gameConfig.trailLength; + const alpha = 1 - progress; + const size = particle.size * (1 - progress * 0.5); + + // Draw trail particle + ctx.save(); + ctx.translate(particle.x, particle.y); + ctx.rotate(particle.angle); + + // Draw elongated particle + ctx.beginPath(); + ctx.moveTo(-size * 2, -size); + ctx.lineTo(size * 2, -size); + ctx.lineTo(size * 2, size); + ctx.lineTo(-size * 2, size); + ctx.closePath(); + + // Add glow effect + const gradient = ctx.createLinearGradient(-size * 2, 0, size * 2, 0); + gradient.addColorStop(0, `${square.color}00`); + gradient.addColorStop(0.5, `${square.color}${Math.floor(alpha * 255).toString(16).padStart(2, '0')}`); + gradient.addColorStop(1, `${square.color}00`); + + ctx.fillStyle = gradient; + ctx.fill(); + ctx.restore(); + }); + + // Draw square with highlight effect if selected + if (square.isHighlighted) { + ctx.shadowColor = '#60a5fa'; + ctx.shadowBlur = 15; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + } + + ctx.fillStyle = square.color; + ctx.fillRect(square.x, square.y, square.size, square.size); + + // Reset shadow + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + + // Draw sparkles for bonus squares + if (square.color === gameConfig.colors.bonus) { + const time = Date.now() / 1000; + for (let i = 0; i < gameConfig.sparkleCount; i++) { + const angle = time + (Math.PI * 2 * i) / gameConfig.sparkleCount; + const distance = 25 + Math.sin(time * 3 + i) * 5; + const x = square.x + square.size / 2 + Math.cos(angle) * distance; + const y = square.y + square.size / 2 + Math.sin(angle) * distance; + + // Draw sparkle + ctx.save(); + ctx.translate(x, y); + ctx.rotate(angle); + + // Sparkle shape + ctx.beginPath(); + ctx.moveTo(0, -3); + ctx.lineTo(0, 3); + ctx.moveTo(-3, 0); + ctx.lineTo(3, 0); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 1; + ctx.stroke(); + + // Sparkle glow + ctx.beginPath(); + ctx.arc(0, 0, 2, 0, Math.PI * 2); + ctx.fillStyle = '#fff'; + ctx.fill(); + + ctx.restore(); + } + } + }); + + // Spawn new squares if needed (for endless mode) + if (gameState.selectedMode === 'endless' && + squares.length < gameConfig.difficulties[gameState.selectedDifficulty].maxSquares) { + spawnSquare(canvas, squares); + } + + requestAnimationFrame(gameLoop); + } + + // Start game loop + gameLoop(); +} + +// Add timer update function +function startTimerUpdate() { + // Clear any existing interval + if (gameState.timerInterval) { + clearInterval(gameState.timerInterval); + } + + // Update timer every 100ms for smoother display + gameState.timerInterval = setInterval(() => { + if (!gameState.isPlaying) { + clearInterval(gameState.timerInterval); + return; + } + + const currentTime = Date.now(); + const deltaTime = (currentTime - gameState.lastUpdateTime) / 1000; + gameState.lastUpdateTime = currentTime; + + // Update remaining time + if (gameState.selectedMode === 'levels') { + gameState.remainingTime = Math.max(0, gameState.remainingTime - deltaTime); + + // Check for time up + if (gameState.remainingTime <= 0) { + gameState.isPlaying = false; + showDeathScreen(); + clearInterval(gameState.timerInterval); + } + } + + updateStats(); + }, 100); +} + +// Add visibility change handler +document.addEventListener('visibilitychange', () => { + if (document.hidden) { + // Store the time when the tab becomes hidden + gameState.lastUpdateTime = Date.now(); + } else { + // Update the last update time when the tab becomes visible again + gameState.lastUpdateTime = Date.now(); + } +}); + +// Update game stats display +function updateStats() { + // Update score + document.querySelector('.stat-item.score .stat-value').textContent = gameState.score; + + // Update lives (only in levels mode or non-easy difficulties) + const livesElement = document.querySelector('.stat-item.lives'); + const config = gameConfig.difficulties[gameState.selectedDifficulty]; + + if (config.lives !== Infinity) { + livesElement.classList.add('show'); + document.querySelector('.stat-item.lives .stat-value').textContent = gameState.lives; + } else { + livesElement.classList.remove('show'); + } + + // Update level (only in levels mode) + const levelElement = document.querySelector('.stat-item.level'); + if (gameState.selectedMode === 'levels') { + levelElement.style.display = 'flex'; + document.querySelector('.stat-item.level .stat-value').textContent = gameState.level; + } else { + levelElement.style.display = 'none'; + } + + // Update timer + const timerElement = document.querySelector('.stat-item.timer'); + const timerValue = document.querySelector('.stat-item.timer .stat-value'); + + if (gameState.selectedMode === 'levels') { + timerElement.style.display = 'flex'; + + // Format time + const minutes = Math.floor(gameState.remainingTime / 60); + const seconds = Math.floor(gameState.remainingTime % 60); + timerValue.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; + + // Add warning class when time is running low + if (gameState.remainingTime <= 30) { + timerElement.classList.add('warning'); + } else { + timerElement.classList.remove('warning'); + } + } else { + timerElement.style.display = 'none'; + } +} + +// Add event listener for the death screen restart button +document.querySelector('.death-screen .restart-btn').addEventListener('click', restartGame); + +// Function to show time bonus popup +function showTimeBonusPopup(seconds, breakdown) { + const timeBonusPopup = document.createElement('div'); + timeBonusPopup.className = 'time-bonus-popup'; + + // Create detailed breakdown + timeBonusPopup.innerHTML = ` +
+${seconds}s
+
+
Base: +${breakdown.base}s
+
Speed: +${breakdown.speed}s
+
Combo: +${breakdown.combo}s
+
+ `; + + document.body.appendChild(timeBonusPopup); + + timeBonusPopup.classList.add('show'); + setTimeout(() => { + timeBonusPopup.classList.remove('show'); + timeBonusPopup.remove(); + }, 2000); +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..6890214 --- /dev/null +++ b/index.html @@ -0,0 +1,92 @@ + + + + + + Zone Out + + + + +
+

Zone Out

+
+ +
+

Game Settings

+
+

Select Difficulty

+
+ + + +
+
+

Easy: 10 squares, slower speed, infinite lives

+

Medium: 15 squares, medium speed, 5 lives

+

Hard: 20 squares, fast speed, 3 lives

+
+
+
+

Game Mode

+
+ + +
+
+

Levels: Clear all squares to advance. Lives matter!

+

Endless: Keep clearing squares for high score

+
+
+ + + Made by Ronniie + +
+
+
+
+ + 0 +
+
+ + 5 +
+
+ + 1 +
+
+ + 0:00 +
+
+
+
+

Game Over!

+
+
+ + Final Score: 0 +
+
+ + Level Reached: 1 +
+
+ + Time Survived: 0:00 +
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..675b9f1 --- /dev/null +++ b/styles.css @@ -0,0 +1,645 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Poppins', sans-serif; + background: linear-gradient(135deg, #0f172a, #1e293b); + color: #fff; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; +} + +.container { + position: relative; + width: 100vw; + height: 100vh; + background: rgba(255, 255, 255, 0.03); + backdrop-filter: blur(10px); +} + +#gameCanvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +h1 { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + text-align: center; + font-size: 2.5rem; + color: #60a5fa; + text-shadow: 0 0 20px rgba(96, 165, 250, 0.5); + background: rgba(15, 23, 42, 0.8); + padding: 15px 30px; + border-radius: 20px; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + z-index: 100; + letter-spacing: 2px; + font-weight: 600; + transition: all 0.3s ease; + border: 1px solid rgba(96, 165, 250, 0.2); +} + +h1:hover { + transform: translateX(-50%) translateY(-2px); + box-shadow: 0 12px 32px rgba(96, 165, 250, 0.3); + text-shadow: 0 0 25px rgba(96, 165, 250, 0.7); + border-color: rgba(96, 165, 250, 0.4); +} + +h2 { + margin-bottom: 1.5rem; + color: #4ecca3; +} + +h3 { + margin-bottom: 1rem; + color: #fff; +} + +.difficulty-options, .mode-options { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +button { + padding: 10px 20px; + border: none; + border-radius: 12px; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.1); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.1); + font-weight: 500; +} + +button:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-2px); + border-color: rgba(96, 165, 250, 0.3); +} + +.difficulty-btn.active, .mode-btn.active { + background: #60a5fa; + color: #0f172a; + box-shadow: 0 4px 12px rgba(96, 165, 250, 0.3); + border-color: transparent; +} + +.start-btn { + background: #4ecca3; + color: #1a1a2e; + font-size: 1.2rem; + padding: 1rem 2rem; + margin-top: 1rem; + width: 100%; +} + +.start-btn:hover { + background: #45b392; + box-shadow: 0 0 20px rgba(78, 204, 163, 0.6); +} + +/* Game-specific styles */ +.particle { + position: absolute; + pointer-events: none; + animation: fadeOut 0.5s ease-out forwards; +} + +@keyframes fadeOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0); + } +} + +.sparkle { + position: absolute; + pointer-events: none; + animation: sparkle 1s ease-out forwards; +} + +@keyframes sparkle { + 0% { + opacity: 1; + transform: scale(0) rotate(0deg); + } + 50% { + opacity: 1; + transform: scale(1) rotate(180deg); + } + 100% { + opacity: 0; + transform: scale(0) rotate(360deg); + } +} + +/* Settings Icon and Menu */ +.settings-icon { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + cursor: pointer; + background: rgba(15, 23, 42, 0.8); + padding: 12px; + border-radius: 15px; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; +} + +.settings-icon:hover { + transform: translateY(-2px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3); + border-color: rgba(96, 165, 250, 0.3); +} + +.settings-icon i { + font-size: 1.8rem; + color: #60a5fa; + transition: transform 0.3s ease; +} + +.settings-icon:hover i { + transform: rotate(90deg); +} + +.settings-menu { + position: absolute; + top: 100%; + right: 0; + background: rgba(15, 23, 42, 0.95); + padding: 25px; + border-radius: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + width: 350px; + opacity: 0; + visibility: hidden; + transform: translateY(10px); + transition: all 0.3s ease; + margin-top: 10px; + border: 1px solid rgba(96, 165, 250, 0.2); + display: flex; + flex-direction: column; + align-items: center; +} + +.settings-icon:hover .settings-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.settings-menu h2 { + margin-bottom: 20px; + color: #60a5fa; + font-size: 1.8rem; + text-align: center; + width: 100%; +} + +.settings-menu h3 { + margin-bottom: 15px; + color: #fff; + font-size: 1.2rem; + width: 100%; + text-align: center; +} + +.difficulty-options, .mode-options { + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 20px; + width: 100%; +} + +.difficulty-options button, .mode-options button { + min-width: 100px; + text-align: center; + justify-content: center; +} + +.attribution { + display: block; + text-align: center; + color: #60a5fa; + text-decoration: none; + font-size: 0.9rem; + margin-top: 20px; + padding: 10px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.05); + transition: all 0.3s ease; + width: 100%; +} + +.attribution:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-2px); +} + +.attribution i { + color: #f87171; + margin-left: 5px; + font-size: 0.8em; + animation: heartbeat 1.5s ease-in-out infinite; +} + +@keyframes heartbeat { + 0% { transform: scale(1); } + 14% { transform: scale(1.3); } + 28% { transform: scale(1); } + 42% { transform: scale(1.3); } + 70% { transform: scale(1); } +} + +.restart-btn { + background: #60a5fa; + color: #0f172a; + font-size: 1.2rem; + padding: 15px 30px; + width: 100%; + border: none; + border-radius: 15px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 600; + margin-top: 20px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.restart-btn i { + font-size: 1.2rem; + transition: transform 0.3s ease; + opacity: 1; + color: #0f172a; +} + +.restart-btn:hover { + background: #3b82f6; + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4); +} + +.restart-btn:hover i { + transform: rotate(180deg); +} + +/* Game Stats Display */ +.game-stats { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: rgba(15, 23, 42, 0.8); + padding: 15px 30px; + border-radius: 20px; + display: flex; + gap: 25px; + align-items: center; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + z-index: 100; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.stat-item { + display: flex; + align-items: center; + gap: 10px; + color: #fff; + font-size: 1.1rem; +} + +.stat-item.lives { + display: none; /* Hide lives by default */ +} + +.stat-item.lives.show { + display: flex; /* Show lives only when needed */ +} + +.stat-item i { + font-size: 1.4rem; + background: rgba(255, 255, 255, 0.1); + padding: 8px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.stat-item.lives i { + color: #f87171; + background: rgba(248, 113, 113, 0.1); +} + +.stat-item.score { + font-size: 1.4rem; + font-weight: 600; + color: #60a5fa; +} + +.stat-item.level i { + color: #fbbf24; + background: rgba(251, 191, 36, 0.1); +} + +.stat-item.timer i { + color: #34d399; + background: rgba(52, 211, 153, 0.1); +} + +.stat-item.timer .stat-value { + color: #34d399; + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + font-size: 1.2rem; +} + +.stat-item.timer.warning .stat-value { + color: #f87171; + animation: pulse 1s infinite; +} + +.difficulty-info, .mode-info { + margin-top: 10px; + font-size: 0.9rem; + color: #fff; + opacity: 0.8; + text-align: center; +} + +.info-text { + display: none; + padding: 8px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.1); + margin: 5px 0; + transition: all 0.3s ease; +} + +.difficulty-btn.active + .difficulty-info .info-text, +.mode-btn.active + .mode-info .info-text { + display: block; +} + +/* Death Screen */ +.death-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(15, 23, 42, 0.95); + backdrop-filter: blur(10px); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.death-screen.show { + display: flex; + animation: fadeIn 0.5s ease forwards; +} + +.death-content { + background: rgba(30, 41, 59, 0.8); + padding: 40px; + border-radius: 30px; + text-align: center; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + transform: scale(0.9); + animation: popIn 0.5s ease forwards; + max-width: 90%; + width: 500px; + border: 1px solid rgba(96, 165, 250, 0.2); +} + +.death-content h2 { + color: #f87171; + font-size: 3rem; + margin-bottom: 30px; + text-shadow: 0 0 20px rgba(248, 113, 113, 0.5); +} + +.final-stats { + margin: 30px 0; +} + +.stat-row { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + margin: 20px 0; + font-size: 1.4rem; + color: #fff; + padding: 15px; + border-radius: 15px; + background: rgba(255, 255, 255, 0.05); +} + +.stat-row i { + font-size: 1.6rem; + background: rgba(255, 255, 255, 0.1); + padding: 10px; + border-radius: 15px; +} + +.stat-row .final-score { + color: #60a5fa; + font-weight: 600; +} + +.stat-row .final-level { + color: #fbbf24; + font-weight: 600; +} + +.stat-row .final-time { + color: #34d399; + font-weight: 600; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +/* Point Gain Popup */ +.point-gain-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 20px 40px; + border-radius: 20px; + font-size: 1.8rem; + font-weight: 600; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(52, 211, 153, 0.9); + color: #064e3b; +} + +.point-gain-popup.show { + opacity: 1; + visibility: visible; + animation: popIn 0.5s ease forwards; +} + +/* Life Loss Popup */ +.life-loss-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 20px 40px; + border-radius: 20px; + font-size: 1.8rem; + font-weight: 600; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(248, 113, 113, 0.9); + color: #7f1d1d; +} + +.life-loss-popup.show { + opacity: 1; + visibility: visible; + animation: popIn 0.5s ease forwards; +} + +@keyframes popIn { + 0% { + transform: scale(0.8); + opacity: 0; + } + 50% { + transform: scale(1.1); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.time-bonus-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); + background: rgba(15, 23, 42, 0.95); + color: #fff; + padding: 25px 40px; + border-radius: 20px; + font-size: 1.8rem; + font-weight: 600; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + opacity: 0; + transition: all 0.3s ease; + z-index: 1000; + text-align: center; + border: 1px solid rgba(96, 165, 250, 0.3); +} + +.time-bonus-popup.show { + transform: translate(-50%, -50%) scale(1); + opacity: 1; +} + +.time-bonus-popup .total-bonus { + font-size: 3rem; + margin-bottom: 15px; + color: #60a5fa; + text-shadow: 0 0 20px rgba(96, 165, 250, 0.3); +} + +.time-bonus-popup .bonus-breakdown { + font-size: 1.1rem; + color: #94a3b8; + margin-top: 15px; + border-top: 1px solid rgba(96, 165, 250, 0.2); + padding-top: 15px; +} + +.time-bonus-popup .bonus-item { + margin: 8px 0; + display: flex; + justify-content: space-between; + gap: 20px; + padding: 5px 10px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.05); +} + +.time-bonus-popup .bonus-item:before { + content: '•'; + color: #60a5fa; + margin-right: 10px; +} + +/* Animations */ +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +@keyframes popIn { + 0% { + transform: scale(0.8); + opacity: 0; + } + 50% { + transform: scale(1.1); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } +} \ No newline at end of file