first commit
This commit is contained in:
commit
568a1cae78
4 changed files with 1666 additions and 0 deletions
32
README.md
Normal file
32
README.md
Normal 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
897
game.js
Normal 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
92
index.html
Normal 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
645
styles.css
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue