(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 = '