<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Star Catcher Deluxe!</title>
<link rel="stylesheet" href="style.css">
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@700&display=swap" rel="stylesheet">
</head>
<body>
<div id="game-wrapper">
<h1>Star Catcher Deluxe!</h1>
<div id="game-stats">
<span>Score: <span id="score">0</span></span>
<span>Lives: <span id="lives">3</span></span>
<span>High Score: <span id="high-score">0</span></span>
<!-- Added Combo Display -->
<span id="combo-display" class="hidden">Combo: x<span id="combo-multiplier">1</span></span>
</div>
<div id="game-container">
<!-- Static background stars -->
<div class="static-star" style="top: 10%; left: 15%; font-size: 10px; opacity: 0.5;">⭐</div>
<div class="static-star" style="top: 30%; left: 80%; font-size: 12px; opacity: 0.6;">✨</div>
<div class="static-star" style="top: 60%; left: 5%; font-size: 8px; opacity: 0.4;">🌟</div>
<div class="static-star" style="top: 80%; left: 90%; font-size: 11px; opacity: 0.5;">⭐</div>
<div class="static-star" style="top: 45%; left: 45%; font-size: 9px; opacity: 0.3;">✨</div>
<!-- Falling items will be added here -->
<div id="powerup-timer" class="hidden">⏰ Slow!</div> <!-- Visual cue for slow powerup -->
</div>
<!-- Moved Controls Up slightly via CSS -->
<div id="controls">
<button id="start-button">Start Game</button>
</div>
<div id="game-over-screen" class="hidden">
<h2>Game Over!</h2>
<p>Your Score: <span id="final-score">0</span></p>
<p>High Score: <span id="final-high-score">0</span></p>
<button id="restart-button">Play Again?</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
/* General Styles (Mostly Same) */
body {
font-family: 'Nunito', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(to bottom, #001f3f, #003366, #001f3f);
color: #fff;
margin: 0;
overflow: hidden;
user-select: none;
user-select: none;
user-select: none;
user-select: none;
}
#game-wrapper {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
h1 {
color: #ffd700;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
margin-bottom: 10px; /* Reduced margin */
}
/* Game Stats */
#game-stats {
font-size: 1.2em; /* Slightly smaller to fit combo */
margin-bottom: 10px;
background-color: rgba(0, 0, 0, 0.3);
padding: 5px 10px;
border-radius: 8px;
min-width: 300px; /* Increased width */
display: flex;
justify-content: space-around;
align-items: center; /* Vertically align items */
}
#game-stats span {
margin: 0 8px; /* Adjusted margin */
}
#lives {
color: #ff4136;
font-weight: bold;
}
#high-score{
color: #39cccc;
}
/* Combo Display Style */
#combo-display {
color: #FFDC00; /* Gold color for combo */
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
transition: opacity 0.3s ease; /* Smooth hide/show */
}
/* Game Container */
#game-container {
width: 90vw;
max-width: 600px;
height: 70vh;
max-height: 500px;
border: 4px solid #7fdbff;
background-color: rgba(0, 20, 40, 0.5);
position: relative;
overflow: hidden;
cursor: crosshair;
box-shadow: 0 0 20px rgba(127, 219, 255, 0.6);
border-radius: 10px;
transition: border-color 0.3s ease; /* Added for powerup effect */
}
/* Static Background Stars */
.static-star {
position: absolute;
color: #fff;
pointer-events: none;
}
/* Powerup Timer Visual Cue */
#powerup-timer {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0, 150, 255, 0.8);
color: white;
padding: 5px 10px;
border-radius: 5px;
font-size: 0.9em;
font-weight: bold;
z-index: 5; /* Above items */
box-shadow: 0 0 10px rgba(0, 150, 255, 0.7);
}
/* Falling Items */
.item {
position: absolute;
font-size: 28px;
cursor: pointer;
transition: transform 0.1s ease-out, opacity 0.2s ease-out;
text-shadow: 1px 1px 3px rgba(0,0,0,0.4);
/* Make items slightly transparent by default for slow effect */
/* opacity: 0.9; -- Optional visual change */
}
/* Specific styles for different items if needed */
.item[data-type="golden_star"] {
/* Maybe a subtle glow? */
text-shadow: 0 0 8px gold;
}
.item[data-type="slowdown"], .item[data-type="extralife"] {
/* Maybe slightly larger or different effect */
transform: scale(1.1);
}
.item.caught {
animation: catchAnim 0.3s ease-out forwards;
}
.item.missed {
animation: missAnim 0.5s ease-in forwards;
}
.item.bomb-clicked {
animation: bombExplodeAnim 0.4s ease-out forwards;
}
/* Controls & Buttons */
#controls {
margin-top: 10px; /* Reduced margin to move button up */
}
button {
padding: 12px 25px;
font-size: 1.2em;
font-family: 'Nunito', sans-serif;
cursor: pointer;
border: none;
border-radius: 8px;
color: white;
transition: background-color 0.3s ease, transform 0.1s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
button:hover { filter: brightness(1.1); }
button:active {
transform: translateY(2px);
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.2);
}
#start-button { background-color: #2ecc40; }
#restart-button { background-color: #ff851b; }
/* Game Over Screen (Same as before) */
#game-over-screen {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(40, 40, 90, 0.9);
padding: 30px 40px;
border-radius: 15px;
border: 3px solid #ffd700;
text-align: center;
box-shadow: 0 0 25px rgba(255, 215, 0, 0.5);
z-index: 10;
}
#game-over-screen h2 { color: #ffd700; margin-bottom: 15px; }
#game-over-screen p { font-size: 1.2em; margin: 10px 0; }
/* Animations (Same as before) */
@keyframes catchAnim { /* ... */ }
@keyframes missAnim { /* ... */ }
@keyframes bombExplodeAnim { /* ... */ }
/* Existing keyframes remain the same */
@keyframes catchAnim {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.8; }
100% { transform: scale(0.5); opacity: 0; }
}
@keyframes missAnim {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(20px); }
}
@keyframes bombExplodeAnim {
0% { transform: scale(1); opacity: 1; filter: brightness(1);}
50% { transform: scale(2.5); opacity: 0.5; filter: brightness(3); }
100% { transform: scale(0.1); opacity: 0; filter: brightness(0);}
}
/* Utility */
.hidden {
display: none;
opacity: 0; /* Also fade out for combo */
}
const gameContainer = document.getElementById('game-container');
const scoreDisplay = document.getElementById('score');
const livesDisplay = document.getElementById('lives');
const highScoreDisplay = document.getElementById('high-score');
const startButton = document.getElementById('start-button');
const gameOverScreen = document.getElementById('game-over-screen');
const restartButton = document.getElementById('restart-button');
const finalScoreDisplay = document.getElementById('final-score');
const finalHighScoreDisplay = document.getElementById('final-high-score');
const controlsDiv = document.getElementById('controls');
const comboDisplay = document.getElementById('combo-display'); // Combo UI
const comboMultiplierDisplay = document.getElementById('combo-multiplier'); // Combo value UI
const powerupTimerDisplay = document.getElementById('powerup-timer'); // Slow timer UI
// --- Game State Variables ---
let score = 0;
let lives = 3;
let highScore = localStorage.getItem('starCatcherHighScore') || 0;
let gameIntervalId = null;
let moveIntervalId = null;
let fallingItems = [];
let isGameOver = true;
let baseSpeed = 1.5;
let speedMultiplier = 1.0; // Difficulty scaler
let powerUpSpeedModifier = 1.0; // For slow-down powerup
let baseCreationRate = 1200;
let creationRateMultiplier = 1.0; // Difficulty scaler
// --- New Logic Variables ---
let bombChance = 0.15; // Base chance
let powerupChance = 0.08; // Chance FOR ANY powerup to spawn instead of star/bomb
let goldenStarChance = 0.1; // Chance for a STAR to be GOLDEN
let comboCounter = 0;
let comboTimeoutId = null; // Timer to reset combo
const COMBO_WINDOW = 2000; // Milliseconds to continue combo
const MAX_LIVES = 5; // Maximum lives player can have
let slowDownActive = false;
let slowDownTimeoutId = null;
const SLOW_DOWN_DURATION = 5000; // 5 seconds
const SLOW_DOWN_FACTOR = 0.5; // Halves the speed
// --- Constants ---
const GAME_HEIGHT = gameContainer.clientHeight;
const GAME_WIDTH = gameContainer.clientWidth;
const ITEM_TYPES = {
STAR: { emojis: ['⭐', '✨'], points: 10, type: 'star' },
GOLDEN_STAR: { emojis: ['🌟'], points: 50, type: 'golden_star' }, // Higher value
BOMB: { emojis: ['💣'], points: 0, type: 'bomb' }, // Points not used on click
SLOW_DOWN: { emojis: ['⏰'], type: 'slowdown' }, // Power-up
EXTRA_LIFE: { emojis: ['❤️'], type: 'extralife' } // Power-up
};
const MOVE_RATE = 20;
// Sound Placeholders...
// --- Initialization ---
function init() {
highScoreDisplay.textContent = highScore;
gameOverScreen.classList.add('hidden');
controlsDiv.classList.remove('hidden');
startButton.classList.remove('hidden');
comboDisplay.classList.add('hidden'); // Hide combo initially
powerupTimerDisplay.classList.add('hidden'); // Hide timer initially
startButton.addEventListener('click', startGame);
restartButton.addEventListener('click', startGame);
gameContainer.addEventListener('click', handleItemClick);
gameContainer.addEventListener('touchstart', handleItemClick, { passive: true });
}
// --- Game Logic ---
function startGame() {
// Reset state
score = 0;
lives = 3;
speedMultiplier = 1.0;
powerUpSpeedModifier = 1.0; // Reset powerup effect
creationRateMultiplier = 1.0;
isGameOver = false;
fallingItems.forEach(item => item.element.remove());
fallingItems = [];
resetCombo(); // Reset combo counter and UI
resetSlowDown(); // Ensure slow down isn't active
// Update UI
scoreDisplay.textContent = score;
livesDisplay.textContent = lives;
livesDisplay.style.color = '#ff4136';
gameOverScreen.classList.add('hidden');
controlsDiv.classList.add('hidden');
powerupTimerDisplay.classList.add('hidden'); // Hide timer
// Start game loops
gameIntervalId = setTimeout(createItemLoop, calculateCreationRate());
moveIntervalId = setInterval(moveItems, MOVE_RATE);
}
function calculateSpeed() {
// Apply difficulty scaler AND powerup modifier
return baseSpeed * speedMultiplier * powerUpSpeedModifier;
}
function calculateCreationRate() {
return Math.max(200, baseCreationRate * creationRateMultiplier);
}
function createItemLoop() {
if (isGameOver) return;
createItem();
gameIntervalId = setTimeout(createItemLoop, calculateCreationRate());
}
function createItem() {
let itemData;
const randomType = Math.random();
if (randomType < bombChance) {
itemData = ITEM_TYPES.BOMB;
} else if (randomType < bombChance + powerupChance) {
// It's a powerup - choose between available ones
itemData = Math.random() < 0.5 ? ITEM_TYPES.SLOW_DOWN : ITEM_TYPES.EXTRA_LIFE;
} else {
// It's a star - decide if it's golden
if (Math.random() < goldenStarChance) {
itemData = ITEM_TYPES.GOLDEN_STAR;
} else {
itemData = ITEM_TYPES.STAR;
}
}
const itemEmoji = itemData.emojis[Math.floor(Math.random() * itemData.emojis.length)];
const itemElement = document.createElement('div');
itemElement.classList.add('item');
itemElement.textContent = itemEmoji;
itemElement.dataset.type = itemData.type; // Store type
const itemSize = 30;
const maxLeft = GAME_WIDTH - itemSize;
const randomLeft = Math.random() * maxLeft;
itemElement.style.left = `${randomLeft}px`;
itemElement.style.top = `-${itemSize}px`;
gameContainer.appendChild(itemElement);
fallingItems.push({ element: itemElement, type: itemData.type });
}
function moveItems() {
if (isGameOver) return;
const currentSpeed = calculateSpeed();
for (let i = fallingItems.length - 1; i >= 0; i--) {
const item = fallingItems[i];
if (!item || !item.element) continue;
let currentTop = parseFloat(item.element.style.top);
currentTop += currentSpeed;
item.element.style.top = `${currentTop}px`;
if (currentTop > GAME_HEIGHT) {
handleMissedItem(item, i);
}
}
updateDifficulty();
}
function handleItemClick(event) {
if (isGameOver || !event.target.classList.contains('item')) return;
const clickedElement = event.target;
const itemIndex = fallingItems.findIndex(item => item.element === clickedElement);
if (itemIndex === -1) return;
const item = fallingItems[itemIndex];
// Route based on type
switch (item.type) {
case 'star':
case 'golden_star':
catchStar(item, itemIndex);
break;
case 'bomb':
clickBomb(item, itemIndex);
break;
case 'slowdown':
case 'extralife':
catchPowerUp(item, itemIndex);
break;
}
}
function catchStar(item, index) {
const basePoints = ITEM_TYPES[item.type.toUpperCase()].points;
const currentComboMultiplier = Math.max(1, comboCounter); // Combo multiplier (at least 1)
const pointsEarned = basePoints * currentComboMultiplier;
score += pointsEarned;
scoreDisplay.textContent = score;
// catchSound.play();
item.element.classList.add('caught');
setTimeout(() => item.element.remove(), 300);
fallingItems.splice(index, 1);
increaseCombo(); // Increase or start combo
}
function clickBomb(item, index) {
// bombSound.play();
item.element.classList.add('bomb-clicked');
setTimeout(() => item.element.remove(), 400);
fallingItems.splice(index, 1);
resetCombo(); // Bomb breaks combo
loseLife();
}
function catchPowerUp(item, index) {
item.element.classList.add('caught'); // Use same catch animation
setTimeout(() => item.element.remove(), 300);
fallingItems.splice(index, 1);
if (item.type === 'slowdown') {
activateSlowDown();
// Optional: Play specific powerup sound
} else if (item.type === 'extralife') {
gainLife();
// Optional: Play specific powerup sound
}
increaseCombo(); // Powerups also contribute to combo
}
function handleMissedItem(item, index) {
if (item.type === 'star' || item.type === 'golden_star') {
// missSound.play();
item.element.classList.add('missed');
setTimeout(() => item.element.remove(), 500);
resetCombo(); // Missing a star breaks combo
loseLife();
} else {
// Bombs & Powerups disappear harmlessly if missed
item.element.remove();
}
// Always remove from array regardless of type
if (fallingItems[index] === item) { // Ensure it wasn't already removed by a click race condition
fallingItems.splice(index, 1);
}
}
// --- Combo Logic ---
function increaseCombo() {
comboCounter++;
comboMultiplierDisplay.textContent = comboCounter;
comboDisplay.classList.remove('hidden'); // Show combo display
// Add a little visual pulse to the combo display
comboDisplay.style.transform = 'scale(1.2)';
setTimeout(() => { comboDisplay.style.transform = 'scale(1)';}, 100);
// Reset combo timer
clearTimeout(comboTimeoutId);
comboTimeoutId = setTimeout(resetCombo, COMBO_WINDOW);
}
function resetCombo() {
clearTimeout(comboTimeoutId);
comboCounter = 0;
comboDisplay.classList.add('hidden'); // Hide combo display
comboMultiplierDisplay.textContent = '1'; // Reset display value
}
// --- Power-up Logic ---
function activateSlowDown() {
if (slowDownActive) {
// If already active, just reset the timer
clearTimeout(slowDownTimeoutId);
} else {
// Activate
slowDownActive = true;
powerUpSpeedModifier = SLOW_DOWN_FACTOR;
powerupTimerDisplay.classList.remove('hidden'); // Show timer UI
gameContainer.style.borderColor = '#00bfff'; // Visual cue: change border color
}
// Set timer to deactivate
slowDownTimeoutId = setTimeout(resetSlowDown, SLOW_DOWN_DURATION);
}
function resetSlowDown() {
clearTimeout(slowDownTimeoutId);
slowDownActive = false;
powerUpSpeedModifier = 1.0; // Restore normal speed modifier
powerupTimerDisplay.classList.add('hidden'); // Hide timer UI
if (!isGameOver) { // Don't change border if game over screen is showing
gameContainer.style.borderColor = '#7fdbff'; // Restore border color
}
}
function gainLife() {
if (lives < MAX_LIVES) {
lives++;
livesDisplay.textContent = lives;
// Add visual feedback for gaining life (e.g., temporary green glow on lives)
livesDisplay.style.color = '#2ECC40'; // Green
setTimeout(() => {
if (!isGameOver) livesDisplay.style.color = '#ff4136'; // Back to normal red unless game over
}, 300);
}
// Optional: Play sound effect
}
// --- Life and Game Over Logic ---
function loseLife() {
if (isGameOver) return;
lives--;
livesDisplay.textContent = lives;
livesDisplay.style.color = '#FF0000';
gameContainer.style.borderColor = '#FF0000';
setTimeout(() => {
if (!isGameOver) {
livesDisplay.style.color = '#ff4136';
gameContainer.style.borderColor = '#7fdbff';
}
}, 300);
if (lives <= 0) {
endGame();
}
}
function updateDifficulty() {
// Adjust base speed and creation rate based on score
speedMultiplier = 1.0 + Math.floor(score / 75) * 0.08; // Slower speed increase
creationRateMultiplier = 1.0 / (1.0 + Math.floor(score / 100) * 0.12); // Slower rate increase
// Optionally, slightly increase bomb/powerup chances over time?
// bombChance = Math.min(0.3, 0.15 + Math.floor(score / 200) * 0.02);
}
function endGame() {
if (isGameOver) return;
isGameOver = true;
// gameOverSound.play();
clearTimeout(gameIntervalId);
clearInterval(moveIntervalId);
resetCombo(); // Clear combo state
resetSlowDown(); // Clear slowdown state
if (score > highScore) {
highScore = score;
localStorage.setItem('starCatcherHighScore', highScore);
highScoreDisplay.textContent = highScore;
}
finalScoreDisplay.textContent = score;
finalHighScoreDisplay.textContent = highScore;
gameOverScreen.classList.remove('hidden');
gameContainer.style.borderColor = '#7fdbff'; // Ensure border is reset
}
// --- Start ---
window.addEventListener('load', init);
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.