740 lines
20 KiB
JavaScript
740 lines
20 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
// Game State
|
|
const state = {
|
|
isPlaying: false,
|
|
isPaused: false,
|
|
score: 0,
|
|
distance: 0,
|
|
gifts: 0,
|
|
boost: 0,
|
|
maxBoost: 100,
|
|
isBoostActive: false,
|
|
speed: 3,
|
|
baseSpeed: 3,
|
|
boostSpeed: 7,
|
|
playerY: 0,
|
|
playerVelocity: 0,
|
|
obstacles: [],
|
|
giftItems: [],
|
|
particles: [],
|
|
highscores: [],
|
|
lastBoostAnnounce: false
|
|
};
|
|
|
|
// DOM Elements
|
|
const canvas = document.getElementById('gameCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const startBtn = document.getElementById('startGame');
|
|
const overlayBtn = document.getElementById('overlayBtn');
|
|
const gameOverlay = document.getElementById('gameOverlay');
|
|
const overlayTitle = document.getElementById('overlayTitle');
|
|
const overlayMessage = document.getElementById('overlayMessage');
|
|
const finalStats = document.getElementById('finalStats');
|
|
const scoreDisplay = document.getElementById('scoreDisplay');
|
|
const distanceDisplay = document.getElementById('distanceDisplay');
|
|
const giftsDisplay = document.getElementById('giftsDisplay');
|
|
const boostBar = document.getElementById('boostBar');
|
|
const boostValue = document.getElementById('boostValue');
|
|
const boostBarContainer = boostBar.parentElement;
|
|
const instructionsModal = document.getElementById('instructionsModal');
|
|
const closeInstructions = document.getElementById('closeInstructions');
|
|
const highscoresBody = document.getElementById('highscoresBody');
|
|
const gameStatus = document.getElementById('gameStatus');
|
|
|
|
// Check for reduced motion preference
|
|
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
// Focus trap for modal
|
|
let focusableElements = [];
|
|
let firstFocusable = null;
|
|
let lastFocusable = null;
|
|
|
|
function setupFocusTrap(modal) {
|
|
focusableElements = modal.querySelectorAll(
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
);
|
|
firstFocusable = focusableElements[0];
|
|
lastFocusable = focusableElements[focusableElements.length - 1];
|
|
}
|
|
|
|
function handleFocusTrap(e) {
|
|
if (e.key !== 'Tab') return;
|
|
|
|
if (e.shiftKey) {
|
|
if (document.activeElement === firstFocusable) {
|
|
e.preventDefault();
|
|
lastFocusable.focus();
|
|
}
|
|
} else {
|
|
if (document.activeElement === lastFocusable) {
|
|
e.preventDefault();
|
|
firstFocusable.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Canvas setup
|
|
function resizeCanvas() {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const dpr = window.devicePixelRatio || 1;
|
|
canvas.width = rect.width * dpr;
|
|
canvas.height = rect.height * dpr;
|
|
ctx.scale(dpr, dpr);
|
|
state.playerY = rect.height / 2;
|
|
}
|
|
|
|
function getCanvasSize() {
|
|
return {
|
|
width: canvas.getBoundingClientRect().width,
|
|
height: canvas.getBoundingClientRect().height
|
|
};
|
|
}
|
|
|
|
// Boat drawing
|
|
const boatSprite = {
|
|
width: 48,
|
|
height: 32,
|
|
draw(ctx, x, y, isBoost) {
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
|
|
// Boat body
|
|
ctx.fillStyle = isBoost ? '#ff6b6b' : '#8b4513';
|
|
ctx.fillRect(0, 8, 40, 16);
|
|
ctx.fillRect(-4, 12, 8, 8);
|
|
|
|
// Boat front (pointed)
|
|
ctx.fillStyle = isBoost ? '#ff6b6b' : '#a0522d';
|
|
ctx.beginPath();
|
|
ctx.moveTo(40, 8);
|
|
ctx.lineTo(48, 16);
|
|
ctx.lineTo(40, 24);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Sail
|
|
ctx.fillStyle = isBoost ? '#ffd60a' : '#ffffff';
|
|
ctx.beginPath();
|
|
ctx.moveTo(20, 8);
|
|
ctx.lineTo(20, -12);
|
|
ctx.lineTo(35, 8);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Mast
|
|
ctx.fillStyle = '#654321';
|
|
ctx.fillRect(18, -12, 4, 20);
|
|
|
|
// Flag
|
|
ctx.fillStyle = '#f72585';
|
|
ctx.fillRect(18, -16, 8, 6);
|
|
|
|
// Wake effect when boosting
|
|
if (isBoost && !prefersReducedMotion) {
|
|
ctx.fillStyle = 'rgba(0, 245, 212, 0.6)';
|
|
for (let i = 0; i < 3; i++) {
|
|
const yOffset = Math.sin(Date.now() / 100 + i) * 2;
|
|
ctx.fillRect(-8 - i * 8, 14 + yOffset, 4, 4);
|
|
}
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
};
|
|
|
|
// Gift item
|
|
function drawGift(ctx, x, y, type) {
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
|
|
const bounce = prefersReducedMotion ? 0 : Math.sin(Date.now() / 200 + x) * 3;
|
|
ctx.translate(0, bounce);
|
|
|
|
if (type === 'boost') {
|
|
// Lightning bolt
|
|
ctx.fillStyle = '#ffd60a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, -12);
|
|
ctx.lineTo(8, -4);
|
|
ctx.lineTo(2, -4);
|
|
ctx.lineTo(8, 12);
|
|
ctx.lineTo(-4, 0);
|
|
ctx.lineTo(2, 0);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
} else {
|
|
// Gift box
|
|
ctx.fillStyle = '#f72585';
|
|
ctx.fillRect(-8, -4, 16, 12);
|
|
ctx.fillStyle = '#ffd60a';
|
|
ctx.fillRect(-2, -4, 4, 12);
|
|
ctx.fillRect(-8, 0, 16, 4);
|
|
// Bow
|
|
ctx.fillRect(-6, -10, 4, 6);
|
|
ctx.fillRect(2, -10, 4, 6);
|
|
ctx.fillRect(-2, -8, 4, 4);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// Obstacle (rock)
|
|
function drawObstacle(ctx, x, y, size) {
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
|
|
ctx.fillStyle = '#4a4a4a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-size/2, size/2);
|
|
ctx.lineTo(-size/3, -size/3);
|
|
ctx.lineTo(0, -size/2);
|
|
ctx.lineTo(size/3, -size/4);
|
|
ctx.lineTo(size/2, size/2);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Highlights
|
|
ctx.fillStyle = '#5a5a5a';
|
|
ctx.fillRect(-size/4, -size/4, size/4, size/4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// Water background
|
|
function drawWater(ctx, canvasSize) {
|
|
const { width, height } = canvasSize;
|
|
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
gradient.addColorStop(0, '#0d1b2a');
|
|
gradient.addColorStop(0.5, '#1b263b');
|
|
gradient.addColorStop(1, '#0d1b2a');
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Animated waves (skip if reduced motion or paused)
|
|
if (!prefersReducedMotion && state.isPlaying && !state.isPaused) {
|
|
ctx.strokeStyle = 'rgba(0, 245, 212, 0.1)';
|
|
ctx.lineWidth = 2;
|
|
for (let i = 0; i < 4; i++) {
|
|
ctx.beginPath();
|
|
for (let x = 0; x < width; x += 20) {
|
|
const waveY = (height / 5) * (i + 1) + Math.sin((x + state.distance * 2 + i * 50) / 50) * 8;
|
|
if (x === 0) {
|
|
ctx.moveTo(x, waveY);
|
|
} else {
|
|
ctx.lineTo(x, waveY);
|
|
}
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Particle system
|
|
function createParticle(x, y, color) {
|
|
if (prefersReducedMotion) return;
|
|
|
|
state.particles.push({
|
|
x,
|
|
y,
|
|
vx: (Math.random() - 0.5) * 4,
|
|
vy: (Math.random() - 0.5) * 4,
|
|
life: 1,
|
|
color,
|
|
size: Math.random() * 4 + 2
|
|
});
|
|
}
|
|
|
|
function updateParticles() {
|
|
for (let i = state.particles.length - 1; i >= 0; i--) {
|
|
const p = state.particles[i];
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
p.life -= 0.03;
|
|
if (p.life <= 0) {
|
|
state.particles.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawParticles(ctx) {
|
|
state.particles.forEach(p => {
|
|
ctx.fillStyle = p.color;
|
|
ctx.globalAlpha = p.life;
|
|
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
|
|
});
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// Spawn obstacles and gifts
|
|
function spawnObstacle(canvasSize) {
|
|
const { width, height } = canvasSize;
|
|
const maxY = height - 40;
|
|
const minY = 40;
|
|
|
|
state.obstacles.push({
|
|
x: width + 50,
|
|
y: Math.random() * (maxY - minY) + minY,
|
|
size: Math.random() * 20 + 30
|
|
});
|
|
}
|
|
|
|
function spawnGift(canvasSize) {
|
|
const { width, height } = canvasSize;
|
|
const maxY = height - 30;
|
|
const minY = 30;
|
|
|
|
state.giftItems.push({
|
|
x: width + 30,
|
|
y: Math.random() * (maxY - minY) + minY,
|
|
type: Math.random() < 0.3 ? 'boost' : 'gift'
|
|
});
|
|
}
|
|
|
|
// Collision detection
|
|
function checkCollision(ax, ay, aw, ah, bx, by, bw, bh) {
|
|
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
|
}
|
|
|
|
// Update game
|
|
let lastSpawnTime = 0;
|
|
let lastGiftTime = 0;
|
|
|
|
function update(timestamp) {
|
|
if (!state.isPlaying || state.isPaused) return;
|
|
|
|
const canvasSize = getCanvasSize();
|
|
const { height } = canvasSize;
|
|
|
|
// Increase difficulty over time
|
|
state.speed = state.baseSpeed + state.distance / 1200;
|
|
const currentSpeed = state.isBoostActive ? state.boostSpeed : state.speed;
|
|
|
|
// Update distance
|
|
state.distance += currentSpeed * 0.5;
|
|
|
|
// Player physics
|
|
state.playerY += state.playerVelocity;
|
|
state.playerVelocity *= 0.9;
|
|
state.playerY = Math.max(24, Math.min(height - 24, state.playerY));
|
|
|
|
// Spawn obstacles
|
|
const spawnInterval = Math.max(800, 1500 - state.distance * 0.5);
|
|
if (timestamp - lastSpawnTime > spawnInterval) {
|
|
spawnObstacle(canvasSize);
|
|
lastSpawnTime = timestamp;
|
|
}
|
|
|
|
// Spawn gifts
|
|
if (timestamp - lastGiftTime > 2200) {
|
|
spawnGift(canvasSize);
|
|
lastGiftTime = timestamp;
|
|
}
|
|
|
|
// Update obstacles
|
|
for (let i = state.obstacles.length - 1; i >= 0; i--) {
|
|
state.obstacles[i].x -= currentSpeed;
|
|
|
|
// Collision with player
|
|
if (checkCollision(
|
|
60, state.playerY - 16, 36, 28,
|
|
state.obstacles[i].x - state.obstacles[i].size/2,
|
|
state.obstacles[i].y - state.obstacles[i].size/2,
|
|
state.obstacles[i].size,
|
|
state.obstacles[i].size
|
|
)) {
|
|
gameOver();
|
|
return;
|
|
}
|
|
|
|
if (state.obstacles[i].x < -50) {
|
|
state.obstacles.splice(i, 1);
|
|
state.score += 10;
|
|
}
|
|
}
|
|
|
|
// Update gifts
|
|
for (let i = state.giftItems.length - 1; i >= 0; i--) {
|
|
state.giftItems[i].x -= currentSpeed;
|
|
|
|
// Collision with player
|
|
if (checkCollision(
|
|
60, state.playerY - 16, 36, 28,
|
|
state.giftItems[i].x - 12,
|
|
state.giftItems[i].y - 12,
|
|
24, 24
|
|
)) {
|
|
// Collect gift
|
|
const gift = state.giftItems[i];
|
|
if (gift.type === 'boost') {
|
|
state.boost = Math.min(state.boost + 35, state.maxBoost);
|
|
state.score += 25;
|
|
} else {
|
|
state.gifts++;
|
|
state.boost = Math.min(state.boost + 15, state.maxBoost);
|
|
state.score += 50;
|
|
}
|
|
|
|
// Particles
|
|
for (let j = 0; j < 8; j++) {
|
|
createParticle(gift.x, gift.y, gift.type === 'boost' ? '#ffd60a' : '#f72585');
|
|
}
|
|
|
|
state.giftItems.splice(i, 1);
|
|
updateHUD();
|
|
continue;
|
|
}
|
|
|
|
if (state.giftItems[i].x < -30) {
|
|
state.giftItems.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// Boost drain
|
|
if (state.isBoostActive) {
|
|
state.boost -= 0.6;
|
|
if (state.boost <= 0) {
|
|
state.boost = 0;
|
|
state.isBoostActive = false;
|
|
}
|
|
// Boost particles
|
|
createParticle(60, state.playerY, 'rgba(0, 245, 212, 0.8)');
|
|
}
|
|
|
|
// Announce boost ready (only once)
|
|
const boostFull = state.boost >= state.maxBoost;
|
|
if (boostFull && !state.lastBoostAnnounce) {
|
|
gameStatus.textContent = 'בוסט מוכן! לחצו רווח להפעלה';
|
|
state.lastBoostAnnounce = true;
|
|
} else if (!boostFull && state.lastBoostAnnounce) {
|
|
state.lastBoostAnnounce = false;
|
|
gameStatus.textContent = '';
|
|
}
|
|
|
|
updateParticles();
|
|
updateHUD();
|
|
}
|
|
|
|
function updateHUD() {
|
|
scoreDisplay.textContent = Math.floor(state.score);
|
|
distanceDisplay.textContent = Math.floor(state.distance) + ' מ׳';
|
|
giftsDisplay.textContent = state.gifts;
|
|
|
|
const boostPercent = Math.floor(state.boost / state.maxBoost * 100);
|
|
boostBar.style.width = boostPercent + '%';
|
|
boostValue.textContent = boostPercent + '%';
|
|
boostBarContainer.setAttribute('aria-valuenow', boostPercent);
|
|
}
|
|
|
|
// Render game
|
|
function render() {
|
|
const canvasSize = getCanvasSize();
|
|
const { width, height } = canvasSize;
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
drawWater(ctx, canvasSize);
|
|
|
|
// Draw obstacles
|
|
state.obstacles.forEach(obs => {
|
|
drawObstacle(ctx, obs.x, obs.y, obs.size);
|
|
});
|
|
|
|
// Draw gifts
|
|
state.giftItems.forEach(gift => {
|
|
drawGift(ctx, gift.x, gift.y, gift.type);
|
|
});
|
|
|
|
// Draw player
|
|
boatSprite.draw(ctx, 60, state.playerY, state.isBoostActive);
|
|
|
|
// Draw particles
|
|
drawParticles(ctx);
|
|
|
|
// Boost ready indicator
|
|
if (state.boost >= state.maxBoost && state.isPlaying) {
|
|
ctx.font = 'bold 14px Heebo, sans-serif';
|
|
ctx.fillStyle = '#ffd60a';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'top';
|
|
ctx.fillText('⚡ בוסט מוכן! לחצו רווח ⚡', width / 2, 12);
|
|
}
|
|
}
|
|
|
|
// Game loop
|
|
let animationId;
|
|
function gameLoop(timestamp) {
|
|
update(timestamp);
|
|
render();
|
|
if (state.isPlaying) {
|
|
animationId = requestAnimationFrame(gameLoop);
|
|
}
|
|
}
|
|
|
|
// Start game
|
|
function startGame() {
|
|
const canvasSize = getCanvasSize();
|
|
|
|
state.isPlaying = true;
|
|
state.isPaused = false;
|
|
state.score = 0;
|
|
state.distance = 0;
|
|
state.gifts = 0;
|
|
state.boost = 0;
|
|
state.isBoostActive = false;
|
|
state.speed = state.baseSpeed;
|
|
state.playerY = canvasSize.height / 2;
|
|
state.playerVelocity = 0;
|
|
state.obstacles = [];
|
|
state.giftItems = [];
|
|
state.particles = [];
|
|
state.lastBoostAnnounce = false;
|
|
|
|
gameOverlay.classList.add('hidden');
|
|
gameStatus.textContent = 'המשחק התחיל!';
|
|
updateHUD();
|
|
|
|
lastSpawnTime = performance.now();
|
|
lastGiftTime = performance.now();
|
|
|
|
animationId = requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Game over
|
|
function gameOver() {
|
|
state.isPlaying = false;
|
|
cancelAnimationFrame(animationId);
|
|
|
|
// Save highscore
|
|
const entry = {
|
|
score: Math.floor(state.score),
|
|
gifts: state.gifts,
|
|
distance: Math.floor(state.distance)
|
|
};
|
|
|
|
state.highscores.push(entry);
|
|
state.highscores.sort((a, b) => b.score - a.score);
|
|
state.highscores = state.highscores.slice(0, 5);
|
|
|
|
try {
|
|
localStorage.setItem('boatRaceHighscores', JSON.stringify(state.highscores));
|
|
} catch (e) {
|
|
// Storage might be unavailable
|
|
}
|
|
|
|
// Show overlay
|
|
overlayTitle.textContent = 'המשחק נגמר!';
|
|
overlayMessage.textContent = 'כל הכבוד! הגעתם למרחק ' + Math.floor(state.distance) + ' מטר';
|
|
finalStats.hidden = false;
|
|
document.getElementById('finalScore').textContent = Math.floor(state.score);
|
|
document.getElementById('finalGifts').textContent = state.gifts;
|
|
document.getElementById('finalDistance').textContent = Math.floor(state.distance) + ' מ׳';
|
|
overlayBtn.textContent = 'שחק שוב';
|
|
gameOverlay.classList.remove('hidden');
|
|
overlayBtn.focus();
|
|
|
|
gameStatus.textContent = 'המשחק נגמר. ניקוד סופי: ' + Math.floor(state.score);
|
|
|
|
renderHighscores();
|
|
}
|
|
|
|
// Render highscores
|
|
function renderHighscores() {
|
|
if (state.highscores.length === 0) {
|
|
highscoresBody.innerHTML = '<div class="table-row table-row--empty" role="row"><span role="cell">עדיין אין שיאים — היו הראשונים!</span></div>';
|
|
return;
|
|
}
|
|
|
|
highscoresBody.innerHTML = state.highscores.map((entry, i) => `
|
|
<div class="table-row" role="row">
|
|
<span role="cell">#${i + 1}</span>
|
|
<span role="cell">${entry.score}</span>
|
|
<span role="cell">${entry.gifts}</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Input handling
|
|
function handleKeyDown(e) {
|
|
if (e.code === 'Space') {
|
|
e.preventDefault();
|
|
if (!state.isPlaying) {
|
|
startGame();
|
|
} else if (state.boost >= state.maxBoost && !state.isBoostActive) {
|
|
state.isBoostActive = true;
|
|
gameStatus.textContent = 'בוסט פעיל!';
|
|
}
|
|
}
|
|
|
|
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
|
|
e.preventDefault();
|
|
state.playerVelocity = -7;
|
|
}
|
|
|
|
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
|
|
e.preventDefault();
|
|
state.playerVelocity = 7;
|
|
}
|
|
|
|
if (e.code === 'KeyP' && state.isPlaying) {
|
|
state.isPaused = !state.isPaused;
|
|
gameStatus.textContent = state.isPaused ? 'המשחק מושהה' : 'המשחק ממשיך';
|
|
if (!state.isPaused) {
|
|
animationId = requestAnimationFrame(gameLoop);
|
|
}
|
|
}
|
|
|
|
if (e.code === 'Escape') {
|
|
if (!instructionsModal.hidden) {
|
|
closeModal();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Touch handling
|
|
let touchStartY = 0;
|
|
let lastTap = 0;
|
|
|
|
function handleTouchStart(e) {
|
|
if (!state.isPlaying) {
|
|
startGame();
|
|
return;
|
|
}
|
|
|
|
touchStartY = e.touches[0].clientY;
|
|
|
|
// Double tap for boost
|
|
const now = Date.now();
|
|
if (now - lastTap < 300 && state.boost >= state.maxBoost && !state.isBoostActive) {
|
|
state.isBoostActive = true;
|
|
gameStatus.textContent = 'בוסט פעיל!';
|
|
}
|
|
lastTap = now;
|
|
}
|
|
|
|
function handleTouchMove(e) {
|
|
if (!state.isPlaying) return;
|
|
e.preventDefault();
|
|
|
|
const touchY = e.touches[0].clientY;
|
|
const diff = touchStartY - touchY;
|
|
state.playerVelocity = -diff * 0.12;
|
|
touchStartY = touchY;
|
|
}
|
|
|
|
// Modal handling
|
|
let previousActiveElement = null;
|
|
|
|
function openModal() {
|
|
previousActiveElement = document.activeElement;
|
|
instructionsModal.hidden = false;
|
|
setupFocusTrap(instructionsModal);
|
|
closeInstructions.focus();
|
|
document.body.style.overflow = 'hidden';
|
|
instructionsModal.addEventListener('keydown', handleFocusTrap);
|
|
}
|
|
|
|
function closeModal() {
|
|
instructionsModal.hidden = true;
|
|
document.body.style.overflow = '';
|
|
instructionsModal.removeEventListener('keydown', handleFocusTrap);
|
|
if (previousActiveElement) {
|
|
previousActiveElement.focus();
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
function init() {
|
|
resizeCanvas();
|
|
|
|
// Load highscores
|
|
try {
|
|
const saved = localStorage.getItem('boatRaceHighscores');
|
|
if (saved) {
|
|
state.highscores = JSON.parse(saved);
|
|
}
|
|
} catch (e) {
|
|
// Storage might be unavailable
|
|
}
|
|
|
|
renderHighscores();
|
|
|
|
// Event listeners
|
|
window.addEventListener('resize', resizeCanvas);
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
canvas.addEventListener('touchstart', handleTouchStart, { passive: true });
|
|
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
|
|
startBtn.addEventListener('click', startGame);
|
|
overlayBtn.addEventListener('click', startGame);
|
|
|
|
// Navigation
|
|
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const action = btn.dataset.action;
|
|
if (action === 'instructions') {
|
|
openModal();
|
|
} else if (action === 'highscores') {
|
|
document.getElementById('highscoresSection').scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' });
|
|
}
|
|
});
|
|
});
|
|
|
|
closeInstructions.addEventListener('click', closeModal);
|
|
|
|
instructionsModal.addEventListener('click', (e) => {
|
|
if (e.target === instructionsModal) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
// Scroll reveal animations (skip if reduced motion)
|
|
if (!prefersReducedMotion) {
|
|
const observerOptions = {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -40px 0px'
|
|
};
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('revealed');
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
document.querySelectorAll('.feature-card, .section-title').forEach(el => {
|
|
el.classList.add('reveal-on-scroll');
|
|
observer.observe(el);
|
|
});
|
|
|
|
// Add reveal styles dynamically
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.reveal-on-scroll {
|
|
opacity: 0;
|
|
transform: translateY(24px);
|
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
}
|
|
.reveal-on-scroll.revealed {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
// Initial render
|
|
render();
|
|
}
|
|
|
|
// Start
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})(); |