first commit

This commit is contained in:
Ronnie 2025-05-27 19:05:54 -04:00
commit 568a1cae78
4 changed files with 1666 additions and 0 deletions

32
README.md Normal file
View file

@ -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.

897
game.js Normal file
View file

@ -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 = `
<div class="total-bonus">+${seconds}s</div>
<div class="bonus-breakdown">
<div class="bonus-item">Base: +${breakdown.base}s</div>
<div class="bonus-item">Speed: +${breakdown.speed}s</div>
<div class="bonus-item">Combo: +${breakdown.combo}s</div>
</div>
`;
document.body.appendChild(timeBonusPopup);
timeBonusPopup.classList.add('show');
setTimeout(() => {
timeBonusPopup.classList.remove('show');
timeBonusPopup.remove();
}, 2000);
}

92
index.html Normal file
View file

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zone Out</title>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container">
<h1>Zone Out</h1>
<div class="settings-icon">
<i class="fas fa-cog"></i>
<div class="settings-menu">
<h2>Game Settings</h2>
<div class="difficulty">
<h3>Select Difficulty</h3>
<div class="difficulty-options">
<button class="difficulty-btn" data-difficulty="easy">Easy</button>
<button class="difficulty-btn" data-difficulty="medium">Medium</button>
<button class="difficulty-btn" data-difficulty="hard">Hard</button>
</div>
<div class="difficulty-info">
<p class="info-text easy-info">Easy: 10 squares, slower speed, infinite lives</p>
<p class="info-text medium-info">Medium: 15 squares, medium speed, 5 lives</p>
<p class="info-text hard-info">Hard: 20 squares, fast speed, 3 lives</p>
</div>
</div>
<div class="game-mode">
<h3>Game Mode</h3>
<div class="mode-options">
<button class="mode-btn" data-mode="levels">Levels</button>
<button class="mode-btn" data-mode="endless">Endless</button>
</div>
<div class="mode-info">
<p class="info-text levels-info">Levels: Clear all squares to advance. Lives matter!</p>
<p class="info-text endless-info">Endless: Keep clearing squares for high score</p>
</div>
</div>
<button id="restart-game" class="restart-btn">
<i class="fas fa-redo"></i> Restart Game
</button>
<a href="https://ronniie.dev" class="attribution" target="_blank">
Made by Ronniie <i class="fas fa-heart"></i>
</a>
</div>
</div>
<div class="game-stats">
<div class="stat-item score">
<i class="fas fa-star"></i>
<span class="stat-value">0</span>
</div>
<div class="stat-item lives">
<i class="fas fa-heart"></i>
<span class="stat-value">5</span>
</div>
<div class="stat-item level">
<i class="fas fa-layer-group"></i>
<span class="stat-value">1</span>
</div>
<div class="stat-item timer">
<i class="fas fa-clock"></i>
<span class="stat-value">0:00</span>
</div>
</div>
<div class="death-screen">
<div class="death-content">
<h2>Game Over!</h2>
<div class="final-stats">
<div class="stat-row">
<i class="fas fa-star"></i>
<span>Final Score: <span class="final-score">0</span></span>
</div>
<div class="stat-row">
<i class="fas fa-layer-group"></i>
<span>Level Reached: <span class="final-level">1</span></span>
</div>
<div class="stat-row">
<i class="fas fa-clock"></i>
<span>Time Survived: <span class="final-time">0:00</span></span>
</div>
</div>
<button class="restart-btn">
<i class="fas fa-redo"></i> Play Again
</button>
</div>
</div>
</div>
<script src="game.js"></script>
</body>
</html>

645
styles.css Normal file
View file

@ -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;
}
}