import Module from '../core/Module.js'; import { soundSystem } from '../gameHelpers/MarioEducational/SoundSystem.js'; class RiverRun extends Module { constructor(name, dependencies, config = {}) { super(name || 'river-run', ['eventBus']); if (!dependencies.eventBus || !dependencies.content) { throw new Error('RiverRun requires eventBus and content dependencies'); } // Ensure name is always defined (fallback to gameId) if (!this.name) { this.name = 'river-run'; } this._eventBus = dependencies.eventBus; this._content = dependencies.content; this._config = { container: null, difficulty: 'medium', initialSpeed: 2, initialLives: 3, spawnInterval: 1000, ...config }; this._isRunning = false; this._score = 0; this._lives = this._config.initialLives; this._level = 1; this._speed = this._config.initialSpeed; this._wordsCollected = 0; this._player = { x: 50, y: 80, targetX: 50, targetY: 80, size: 40 }; this._floatingWords = []; this._currentTarget = null; this._targetQueue = []; this._powerUps = []; this._particles = []; this._availableWords = []; this._usedTargets = []; this._riverOffset = 0; this._lastSpawn = 0; this._gameStartTime = 0; this._wordsSpawnedSinceTarget = 0; this._maxWordsBeforeTarget = 10; this._gameContainer = null; this._animationFrame = null; // Background music this._audioContext = null; this._backgroundMusicNodes = []; this._isMusicPlaying = false; this._musicLoopTimeout = null; // Power-up state this._slowTimeActive = false; this._slowTimeTimeout = null; Object.seal(this); } static getMetadata() { return { id: 'river-run', name: 'River Run', description: 'Navigate down a river collecting target vocabulary words while avoiding obstacles', version: '2.0.0', author: 'Class Generator', category: 'action', tags: ['vocabulary', 'action', 'reflex', 'collection'], difficulty: { min: 1, max: 4, default: 2 }, estimatedDuration: 8, requiredContent: ['vocabulary'] }; } static getCompatibilityScore(content) { if (!content || !content.vocabulary) { return 0; } let score = 50; if (typeof content.vocabulary === 'object') { const vocabCount = Object.keys(content.vocabulary).length; if (vocabCount >= 10) score += 25; if (vocabCount >= 20) score += 15; if (vocabCount >= 30) score += 10; } else if (content.letters) { score += 20; } return Math.min(score, 100); } async init() { this._validateNotDestroyed(); // Validate container if (!this._config.container) { throw new Error('Game container is required'); } this._eventBus.on('game:start', this._handleGameStart.bind(this), this.name); this._eventBus.on('game:stop', this._handleGameStop.bind(this), this.name); this._eventBus.on('navigation:change', this._handleNavigationChange.bind(this), this.name); soundSystem.initialize(); this._injectCSS(); // Start game immediately try { this._gameContainer = this._config.container; const content = this._content; if (!content) { throw new Error('No content available'); } this._extractContent(content); if (this._availableWords.length === 0) { throw new Error('No vocabulary found for River Run'); } this._generateTargetQueue(); this._createGameBoard(); this._setupEventListeners(); this._updateHUD(); // Emit game ready event this._eventBus.emit('game:ready', { gameId: 'river-run', instanceId: this.name, vocabulary: this._availableWords.length }, this.name); } catch (error) { console.error('Error starting River Run:', error); this._showInitError(error.message); } this._setInitialized(); } async destroy() { this._validateNotDestroyed(); this._cleanup(); this._removeCSS(); this._eventBus.off('game:start', this.name); this._eventBus.off('game:stop', this.name); this._eventBus.off('navigation:change', this.name); this._setDestroyed(); } _handleGameStart(event) { this._validateInitialized(); if (event.gameId === 'river-run') { this._startGame(); } } _handleGameStop(event) { this._validateInitialized(); if (event.gameId === 'river-run') { this._stopGame(); } } _handleNavigationChange(event) { this._validateInitialized(); if (event.from === '/games/river-run') { this._cleanup(); } } async _startGame() { try { this._gameContainer = document.getElementById('game-content'); if (!this._gameContainer) { throw new Error('Game container not found'); } const content = await this._content.getCurrentContent(); if (!content) { throw new Error('No content available'); } this._extractContent(content); if (this._availableWords.length === 0) { throw new Error('No vocabulary found for River Run'); } this._generateTargetQueue(); this._createGameBoard(); this._setupEventListeners(); this._updateHUD(); } catch (error) { console.error('Error starting River Run:', error); this._showInitError(error.message); } } _stopGame() { this._cleanup(); } _cleanup() { this._isRunning = false; if (this._animationFrame) { cancelAnimationFrame(this._animationFrame); this._animationFrame = null; } // Clear slow time timeout if active if (this._slowTimeTimeout) { clearTimeout(this._slowTimeTimeout); this._slowTimeTimeout = null; } this._slowTimeActive = false; this._stopBackgroundMusic(); if (this._gameContainer) { this._gameContainer.innerHTML = ''; } if (window.currentRiverGame === this) { delete window.currentRiverGame; } } _showInitError(message) { this._gameContainer.innerHTML = `

❌ Loading Error

${message}

The game requires vocabulary content with words and translations.

`; } _extractContent(content) { this._availableWords = []; if (content.vocabulary) { Object.keys(content.vocabulary).forEach(word => { const wordData = content.vocabulary[word]; this._availableWords.push({ french: word, english: typeof wordData === 'string' ? wordData : wordData.translation || wordData.user_language || 'unknown', pronunciation: wordData.pronunciation || wordData.prononciation }); }); } if (content.letters && this._availableWords.length === 0) { Object.values(content.letters).forEach(letterWords => { letterWords.forEach(wordData => { this._availableWords.push({ french: wordData.word, english: wordData.translation, pronunciation: wordData.pronunciation }); }); }); } console.log(`River Run: ${this._availableWords.length} words loaded`); } _generateTargetQueue() { this._targetQueue = this._shuffleArray([...this._availableWords]).slice(0, Math.min(10, this._availableWords.length)); this._usedTargets = []; } _createGameBoard() { this._gameContainer.innerHTML = `
Score: ${this._score}
Lives: ${this._lives}
Words: ${this._wordsCollected}
Click to Start!
Level: ${this._level}
Speed: ${this._speed.toFixed(1)}x
`; window.currentRiverGame = this; } _setupEventListeners() { const riverGame = document.getElementById('river-game'); riverGame.addEventListener('click', (e) => { if (!this._isRunning) { this._start(); return; } const rect = riverGame.getBoundingClientRect(); const clickX = ((e.clientX - rect.left) / rect.width) * 100; const clickY = ((e.clientY - rect.top) / rect.height) * 100; this._movePlayer(clickX, clickY); }); riverGame.addEventListener('click', (e) => { if (e.target.classList.contains('floating-word')) { e.stopPropagation(); this._handleWordClick(e.target); } }); } _start() { if (this._isRunning) return; this._isRunning = true; this._gameStartTime = Date.now(); this._setNextTarget(); this._startBackgroundMusic(); this._gameLoop(); console.log('River Run started!'); } _gameLoop() { if (!this._isRunning) return; const now = Date.now(); if (now - this._lastSpawn > this._config.spawnInterval) { // Spawn multiple words based on speed: sqrt(speed) words per spawn cycle // The decimal part is treated as probability for an additional word const speedSqrt = Math.sqrt(this._speed); const baseWords = Math.floor(speedSqrt); const probability = speedSqrt - baseWords; // Decimal part (e.g., 2.7 -> 0.7) // Always spawn at least the base number of words let wordsToSpawn = Math.max(1, baseWords); // Add one more word based on probability if (Math.random() < probability) { wordsToSpawn++; } for (let i = 0; i < wordsToSpawn; i++) { // Add slight delay between each word spawn for visual variety setTimeout(() => { if (this._isRunning) { this._spawnFloatingWord(); } }, i * 100); // 100ms delay between each word } this._lastSpawn = now; } this._updateFloatingWords(); this._updatePlayer(); this._updateParticles(); this._checkCollisions(); this._updateDifficulty(); this._updateHUD(); this._animationFrame = requestAnimationFrame(() => this._gameLoop()); } _setNextTarget() { if (this._targetQueue.length === 0) { this._generateTargetQueue(); } this._currentTarget = this._targetQueue.shift(); this._usedTargets.push(this._currentTarget); this._wordsSpawnedSinceTarget = 0; const targetDisplay = document.getElementById('target-display'); if (targetDisplay) { targetDisplay.innerHTML = `Find: ${this._currentTarget.english}`; } } _spawnFloatingWord() { const riverCanvas = document.getElementById('river-canvas'); if (!riverCanvas) return; let word; if (this._wordsSpawnedSinceTarget >= this._maxWordsBeforeTarget) { word = this._currentTarget; this._wordsSpawnedSinceTarget = 0; } else { word = this._getRandomWord(); this._wordsSpawnedSinceTarget++; } const wordElement = document.createElement('div'); wordElement.className = 'floating-word'; const spacePadding = ' '.repeat(this._level * 2); wordElement.textContent = spacePadding + word.french + spacePadding; // More random positioning with different strategies let xPosition; const strategy = Math.random(); if (strategy < 0.4) { // Random across full width (with margins) xPosition = Math.random() * 80 + 10; } else if (strategy < 0.6) { // Prefer left side xPosition = Math.random() * 40 + 10; } else if (strategy < 0.8) { // Prefer right side xPosition = Math.random() * 40 + 50; } else { // Prefer center xPosition = Math.random() * 30 + 35; } // Add slight random variation to starting Y position for staggered effect const yStart = -60 - Math.random() * 40; wordElement.style.left = `${xPosition}%`; wordElement.style.top = `${yStart}px`; wordElement.wordData = word; riverCanvas.appendChild(wordElement); this._floatingWords.push({ element: wordElement, y: yStart, x: xPosition, wordData: word }); if (Math.random() < 0.1) { this._spawnPowerUp(); } } _getRandomWord() { return this._availableWords[Math.floor(Math.random() * this._availableWords.length)]; } _spawnPowerUp() { const riverCanvas = document.getElementById('river-canvas'); if (!riverCanvas) return; const powerUpElement = document.createElement('div'); powerUpElement.className = 'power-up'; powerUpElement.innerHTML = '⚡'; // Random positioning similar to words const xPosition = Math.random() * 80 + 10; const yStart = -40 - Math.random() * 30; powerUpElement.style.left = `${xPosition}%`; powerUpElement.style.top = `${yStart}px`; riverCanvas.appendChild(powerUpElement); this._powerUps.push({ element: powerUpElement, y: yStart, x: xPosition, type: 'slowTime' }); } _updateFloatingWords() { const riverCanvas = document.getElementById('river-canvas'); const canvasHeight = riverCanvas ? riverCanvas.offsetHeight : window.innerHeight; this._floatingWords = this._floatingWords.filter(word => { word.y += this._speed; word.element.style.top = `${word.y}px`; // Check if word has gone below the visible game area if (word.y > canvasHeight - 50) { // Note: No longer losing life when target word escapes // Life loss only happens on collision with wrong words word.element.remove(); return false; } return true; }); this._powerUps = this._powerUps.filter(powerUp => { powerUp.y += this._speed; powerUp.element.style.top = `${powerUp.y}px`; // Check if power-up has gone below the visible game area if (powerUp.y > canvasHeight - 50) { powerUp.element.remove(); return false; } return true; }); } _movePlayer(targetX, targetY) { this._player.targetX = Math.max(5, Math.min(95, targetX)); this._player.targetY = Math.max(10, Math.min(90, targetY)); const playerElement = document.getElementById('player'); if (playerElement) { playerElement.classList.add('moving'); setTimeout(() => { playerElement.classList.remove('moving'); }, 500); } this._createRippleEffect(targetX, targetY); } _updatePlayer() { const speed = 0.1; this._player.x += (this._player.targetX - this._player.x) * speed; this._player.y += (this._player.targetY - this._player.y) * speed; const playerElement = document.getElementById('player'); if (playerElement) { playerElement.style.left = `calc(${this._player.x}% - 20px)`; playerElement.style.top = `calc(${this._player.y}% - 20px)`; } } _createRippleEffect(x, y) { for (let i = 0; i < 5; i++) { setTimeout(() => { const particle = document.createElement('div'); particle.className = 'particle'; particle.style.left = `${x}%`; particle.style.top = `${y}%`; particle.style.animation = `particleSpread 1s ease-out forwards`; const riverCanvas = document.getElementById('river-canvas'); if (riverCanvas) { riverCanvas.appendChild(particle); setTimeout(() => { particle.remove(); }, 1000); } }, i * 100); } } _updateParticles() { if (Math.random() < 0.1) { const particle = document.createElement('div'); particle.className = 'particle'; particle.style.left = `${Math.random() * 100}%`; particle.style.top = '-5px'; particle.style.animation = `particleFlow 3s linear forwards`; const riverCanvas = document.getElementById('river-canvas'); if (riverCanvas) { riverCanvas.appendChild(particle); setTimeout(() => { particle.remove(); }, 3000); } } } _checkCollisions() { const playerRect = this._getPlayerRect(); // Check word collisions (iterate backwards to safely splice) for (let i = this._floatingWords.length - 1; i >= 0; i--) { const word = this._floatingWords[i]; // Skip if word was already collected if (word.element.dataset.collected === 'true') continue; const wordRect = this._getElementRect(word.element); if (this._isColliding(playerRect, wordRect)) { this._handleWordCollision(word, i); } } // Check power-up collisions (iterate backwards to safely splice) for (let i = this._powerUps.length - 1; i >= 0; i--) { const powerUp = this._powerUps[i]; const powerUpRect = this._getElementRect(powerUp.element); if (this._isColliding(playerRect, powerUpRect)) { this._handlePowerUpCollision(powerUp, i); } } } _getPlayerRect() { const playerElement = document.getElementById('player'); if (!playerElement) return { x: 0, y: 0, width: 0, height: 0 }; const canvas = document.getElementById('river-canvas'); if (!canvas) return { x: 0, y: 0, width: 0, height: 0 }; const rect = playerElement.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect(); return { x: rect.left - canvasRect.left, y: rect.top - canvasRect.top, width: rect.width, height: rect.height }; } _getElementRect(element) { if (!element) return { x: 0, y: 0, width: 0, height: 0 }; const canvas = document.getElementById('river-canvas'); if (!canvas) return { x: 0, y: 0, width: 0, height: 0 }; const rect = element.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect(); return { x: rect.left - canvasRect.left, y: rect.top - canvasRect.top, width: rect.width, height: rect.height }; } _isColliding(rect1, rect2) { return rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y; } _handleWordClick(wordElement) { const wordData = wordElement.wordData; if (wordData.french === this._currentTarget.french) { this._collectWord(wordElement, true); } else { this._missWord(wordElement); } } _handleWordCollision(word, index) { // Handle collision for ALL words: // - TARGET word: auto-collect (points) // - WRONG word: lose life if (word.wordData && this._currentTarget) { if (word.wordData.french === this._currentTarget.french) { // Correct word - collect it this._collectWord(word.element, true); } else { // Wrong word - lose life this._missWord(word.element); } // Remove the word from the array to prevent multiple collisions this._floatingWords.splice(index, 1); // Also mark as collected to prevent further processing word.element.dataset.collected = 'true'; } } _collectWord(wordElement, isCorrect) { wordElement.classList.add('collected'); if (isCorrect) { soundSystem.play('coin'); // Base points increased with level, multiplied by sqrt of speed const basePoints = 10 + (this._level * 2); const speedMultiplier = Math.sqrt(this._speed); const pointsEarned = Math.round(basePoints * speedMultiplier); this._score += pointsEarned; this._wordsCollected++; // Show points earned (visual feedback) this._showPointsPopup(wordElement, pointsEarned); this._eventBus.emit('game:score-update', { gameId: 'river-run', score: this._score, module: this.name }, this.name); this._setNextTarget(); this._playSuccessSound(wordElement.textContent); } setTimeout(() => { wordElement.remove(); }, 800); } _missWord(wordElement) { soundSystem.play('enemy_defeat'); wordElement.classList.add('missed'); // Add shaking animation to the wrong word wordElement.style.animation = 'wordShake 0.4s ease-in-out, wordMissed 0.6s ease-out forwards'; // Shake the entire screen this._shakeScreen(); this._loseLife(); setTimeout(() => { wordElement.remove(); }, 600); } _shakeScreen() { const riverGame = document.getElementById('river-game'); if (!riverGame) return; riverGame.classList.add('screen-shake'); setTimeout(() => { riverGame.classList.remove('screen-shake'); }, 500); } _handlePowerUpCollision(powerUp, index) { this._activatePowerUp(powerUp.type); powerUp.element.remove(); this._powerUps.splice(index, 1); } _activatePowerUp(type) { switch (type) { case 'slowTime': // Clear any existing slowTime effect if (this._slowTimeTimeout) { clearTimeout(this._slowTimeTimeout); } // Activate slow time effect this._slowTimeActive = true; soundSystem.play('powerup'); // Visual feedback - add a slow-time indicator to HUD this._showSlowTimeIndicator(); // Reset after 3 seconds this._slowTimeTimeout = setTimeout(() => { this._slowTimeActive = false; this._slowTimeTimeout = null; this._hideSlowTimeIndicator(); }, 3000); break; } } _updateDifficulty() { const timeElapsed = Date.now() - this._gameStartTime; const secondsElapsed = timeElapsed / 1000; // Progressive speed increase: starts at initialSpeed, increases gradually // Speed increases by 0.1 every 5 seconds (0.02 per second) const speedIncrease = secondsElapsed * 0.02; const baseSpeed = this._config.initialSpeed + speedIncrease; // Apply slow-time multiplier if power-up is active this._speed = this._slowTimeActive ? baseSpeed * 0.5 : baseSpeed; // Update level every 30 seconds const newLevel = Math.floor(timeElapsed / 30000) + 1; if (newLevel > this._level) { this._level = newLevel; // Decrease spawn interval with each level (spawn words more frequently) this._config.spawnInterval = Math.max(500, 1000 - (this._level - 1) * 100); } } _showSlowTimeIndicator() { const hud = document.querySelector('.river-run-hud .hud-right'); if (!hud) return; // Remove any existing indicator const existing = document.getElementById('slowtime-indicator'); if (existing) existing.remove(); const indicator = document.createElement('div'); indicator.id = 'slowtime-indicator'; indicator.innerHTML = '⏱️ SLOW TIME'; indicator.style.cssText = ` background: linear-gradient(45deg, #FF6B35, #F7931E); color: white; padding: 5px 15px; border-radius: 15px; font-size: 0.9em; font-weight: bold; animation: slowTimePulse 0.5s ease-in-out infinite alternate; box-shadow: 0 0 15px rgba(255,107,53,0.8); `; hud.appendChild(indicator); } _hideSlowTimeIndicator() { const indicator = document.getElementById('slowtime-indicator'); if (indicator) { indicator.remove(); } } _showPointsPopup(wordElement, points) { const rect = wordElement.getBoundingClientRect(); const riverCanvas = document.getElementById('river-canvas'); if (!riverCanvas) return; const canvasRect = riverCanvas.getBoundingClientRect(); const popup = document.createElement('div'); popup.className = 'points-popup'; popup.textContent = `+${points}`; popup.style.left = `${rect.left - canvasRect.left + rect.width / 2}px`; popup.style.top = `${rect.top - canvasRect.top}px`; riverCanvas.appendChild(popup); setTimeout(() => { popup.remove(); }, 1000); } async _playSuccessSound(word) { if ('speechSynthesis' in window) { // Cancel any ongoing speech speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(word.trim()); // Get language from content, fallback to zh-CN (Chinese) for vocabulary const contentLanguage = this._content?.language || 'zh-CN'; utterance.lang = contentLanguage; utterance.rate = 0.8; utterance.pitch = 1.0; utterance.volume = 1.0; // Wait for voices to be loaded and select the best one const voices = await this._getVoices(); if (voices.length > 0) { const langPrefix = contentLanguage.split('-')[0]; const matchingVoice = voices.find(voice => voice.lang === contentLanguage ) || voices.find(voice => voice.lang.startsWith(langPrefix) ); if (matchingVoice) { utterance.voice = matchingVoice; console.log(`🔊 RiverRun using voice: ${matchingVoice.name} (${matchingVoice.lang})`); } else { console.warn(`🔊 No voice found for: ${contentLanguage}`); } } speechSynthesis.speak(utterance); } } _getVoices() { return new Promise((resolve) => { let voices = window.speechSynthesis.getVoices(); if (voices.length > 0) { resolve(voices); return; } const voicesChangedHandler = () => { voices = window.speechSynthesis.getVoices(); if (voices.length > 0) { window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler); resolve(voices); } }; window.speechSynthesis.addEventListener('voiceschanged', voicesChangedHandler); setTimeout(() => { window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler); resolve(window.speechSynthesis.getVoices()); }, 1000); }); } _loseLife() { this._lives--; if (this._lives <= 0) { this._gameOver(); } } _gameOver() { this._isRunning = false; this._stopBackgroundMusic(); const accuracy = this._wordsCollected > 0 ? Math.round((this._wordsCollected / (this._wordsCollected + (3 - this._lives))) * 100) : 0; // Handle localStorage best score const currentScore = this._score; const bestScore = parseInt(localStorage.getItem('river-run-best-score') || '0'); const isNewBest = currentScore > bestScore; if (isNewBest) { localStorage.setItem('river-run-best-score', currentScore.toString()); } // Emit game end event BEFORE showing popup this._endGame(); this._showVictoryPopup({ gameTitle: 'River Run', currentScore, bestScore: isNewBest ? currentScore : bestScore, isNewBest, stats: { 'Words Collected': this._wordsCollected, 'Level Reached': this._level, 'Accuracy': `${accuracy}%`, 'Lives Remaining': this._lives } }); } _endGame() { // Use gameId instead of this.name which might be undefined this._eventBus.emit('game:end', { gameId: 'river-run', score: this._score, module: this.name || 'river-run' }, this.name || 'river-run'); } _updateHUD() { const scoreDisplay = document.getElementById('score-display'); const livesDisplay = document.getElementById('lives-display'); const wordsDisplay = document.getElementById('words-display'); const levelDisplay = document.getElementById('level-display'); const speedDisplay = document.getElementById('speed-display'); if (scoreDisplay) scoreDisplay.textContent = this._score; if (livesDisplay) livesDisplay.textContent = this._lives; if (wordsDisplay) wordsDisplay.textContent = this._wordsCollected; if (levelDisplay) levelDisplay.textContent = this._level; if (speedDisplay) speedDisplay.textContent = this._speed.toFixed(1) + 'x'; } _restart() { // Stop any playing music before restarting this._stopBackgroundMusic(); this._isRunning = false; this._score = 0; this._lives = this._config.initialLives; this._level = 1; this._speed = this._config.initialSpeed; this._wordsCollected = 0; this._riverOffset = 0; this._player.x = 50; this._player.y = 80; this._player.targetX = 50; this._player.targetY = 80; this._floatingWords = []; this._powerUps = []; this._particles = []; this._lastSpawn = 0; this._config.spawnInterval = 1000; this._gameStartTime = Date.now(); this._wordsSpawnedSinceTarget = 0; this._generateTargetQueue(); const riverCanvas = document.getElementById('river-canvas'); if (riverCanvas) { const words = riverCanvas.querySelectorAll('.floating-word'); const powerUps = riverCanvas.querySelectorAll('.power-up'); const particles = riverCanvas.querySelectorAll('.particle'); words.forEach(word => word.remove()); powerUps.forEach(powerUp => powerUp.remove()); particles.forEach(particle => particle.remove()); } const gameOverModal = document.querySelector('.game-over-modal'); if (gameOverModal) { gameOverModal.remove(); } const targetDisplay = document.getElementById('target-display'); if (targetDisplay) { targetDisplay.textContent = 'Click to Start!'; } this._updateHUD(); console.log('River Run restarted'); } _startBackgroundMusic() { if (this._isMusicPlaying) return; try { // Create audio context this._audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Create master gain for volume control (quiet background music) const masterGain = this._audioContext.createGain(); masterGain.gain.value = 0.15; // Very quiet, 15% volume masterGain.connect(this._audioContext.destination); // River-like pentatonic scale (C D E G A) - peaceful and flowing const frequencies = [261.63, 293.66, 329.63, 392.00, 440.00]; // C4, D4, E4, G4, A4 // Create multiple oscillators for a richer sound const createNote = (freq, startTime, duration, gainValue) => { const oscillator = this._audioContext.createOscillator(); const gainNode = this._audioContext.createGain(); oscillator.type = 'sine'; // Soft sine wave oscillator.frequency.setValueAtTime(freq, this._audioContext.currentTime); // Envelope: fade in and fade out gainNode.gain.setValueAtTime(0, startTime); gainNode.gain.linearRampToValueAtTime(gainValue, startTime + 0.1); gainNode.gain.linearRampToValueAtTime(gainValue * 0.7, startTime + duration - 0.3); gainNode.gain.linearRampToValueAtTime(0, startTime + duration); oscillator.connect(gainNode); gainNode.connect(masterGain); oscillator.start(startTime); oscillator.stop(startTime + duration); return { oscillator, gainNode }; }; // Create a flowing melodic pattern const playMelody = () => { if (!this._isMusicPlaying || !this._audioContext) return; const now = this._audioContext.currentTime; const noteDuration = 1.5; // Longer notes for a relaxed feel // Play a sequence of notes with random variation (like water flowing) for (let i = 0; i < 4; i++) { const randomIndex = Math.floor(Math.random() * frequencies.length); const freq = frequencies[randomIndex]; const startTime = now + (i * noteDuration); const gainValue = 0.3 + Math.random() * 0.2; // Vary volume slightly createNote(freq, startTime, noteDuration * 1.2, gainValue); } // Schedule next melody and store timeout ID this._musicLoopTimeout = setTimeout(() => playMelody(), noteDuration * 4 * 1000); }; // Add subtle low drone (like distant river sound) const bassDrone = this._audioContext.createOscillator(); const bassGain = this._audioContext.createGain(); bassDrone.type = 'sine'; bassDrone.frequency.value = 65.41; // C2 - very low bassGain.gain.value = 0.08; // Very subtle bassDrone.connect(bassGain); bassGain.connect(masterGain); bassDrone.start(); this._backgroundMusicNodes.push({ oscillator: bassDrone, gainNode: bassGain }); this._isMusicPlaying = true; // Start the melody playMelody(); console.log('🎵 River background music started'); } catch (error) { console.warn('Failed to start background music:', error); } } _stopBackgroundMusic() { if (!this._isMusicPlaying) return; try { // Clear the melody loop timeout if (this._musicLoopTimeout) { clearTimeout(this._musicLoopTimeout); this._musicLoopTimeout = null; } // Stop all oscillators this._backgroundMusicNodes.forEach(node => { if (node.oscillator) { try { node.oscillator.stop(); } catch (e) { // Oscillator might already be stopped } } }); // Close audio context if (this._audioContext) { this._audioContext.close(); this._audioContext = null; } this._backgroundMusicNodes = []; this._isMusicPlaying = false; console.log('🎵 River background music stopped'); } catch (error) { console.warn('Failed to stop background music:', error); } } _shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } _injectCSS() { const cssId = 'river-run-styles'; if (document.getElementById(cssId)) return; const style = document.createElement('style'); style.id = cssId; style.textContent = ` .river-run-wrapper { background: linear-gradient(180deg, #87CEEB 0%, #4682B4 50%, #2F4F4F 100%); position: relative; overflow: hidden; height: 100vh; cursor: crosshair; } .river-run-wrapper.screen-shake { animation: screenShake 0.5s ease-in-out; } @keyframes screenShake { 0%, 100% { transform: translate(0, 0); } 10%, 30%, 50%, 70%, 90% { transform: translate(-10px, 5px); } 20%, 40%, 60%, 80% { transform: translate(10px, -5px); } } .river-run-hud { position: absolute; top: 20px; left: 20px; right: 20px; display: flex; justify-content: space-between; z-index: 100; color: white; font-weight: bold; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } .hud-left, .hud-right { display: flex; gap: 20px; align-items: center; } .target-display { background: rgba(255,255,255,0.9); color: #333; padding: 10px 20px; border-radius: 25px; font-size: 1.2em; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.2); animation: targetGlow 2s ease-in-out infinite alternate; } @keyframes targetGlow { from { box-shadow: 0 4px 15px rgba(0,0,0,0.2); } to { box-shadow: 0 4px 20px rgba(255,215,0,0.6); } } .river-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center top, rgba(135,206,235,0.3) 0%, transparent 70%), linear-gradient(0deg, rgba(70,130,180,0.1) 0%, rgba(135,206,235,0.05) 50%, rgba(173,216,230,0.1) 100% ); } .river-waves { position: absolute; width: 120%; height: 100%; background: repeating-linear-gradient( 0deg, transparent 0px, rgba(255,255,255,0.1) 2px, transparent 4px, transparent 20px ); animation: riverFlow 3s linear infinite; } @keyframes riverFlow { from { transform: translateY(-20px); } to { transform: translateY(0px); } } .player { position: absolute; width: 40px; height: 40px; background: linear-gradient(45deg, #8B4513, #A0522D); border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; box-shadow: 0 2px 10px rgba(0,0,0,0.3), inset 0 2px 5px rgba(255,255,255,0.3); transition: all 0.3s ease-out; z-index: 50; transform-origin: center; } .player::before { content: '🛶'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 20px; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); } .player.moving { animation: playerRipple 0.5s ease-out; } @keyframes playerRipple { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } .floating-word { position: absolute; background: rgba(255,255,255,0.95); border: 3px solid #4682B4; border-radius: 15px; padding: 8px 15px; font-size: 1.1em; font-weight: bold; color: #333; cursor: pointer; transition: all 0.2s ease; z-index: 40; box-shadow: 0 4px 15px rgba(0,0,0,0.2), 0 0 0 0 rgba(70,130,180,0.4); animation: wordFloat 3s ease-in-out infinite alternate; } @keyframes wordFloat { from { transform: translateY(0px) rotate(-1deg); } to { transform: translateY(-5px) rotate(1deg); } } .floating-word:hover { transform: scale(1.1) translateY(-3px); box-shadow: 0 6px 20px rgba(0,0,0,0.3), 0 0 20px rgba(70,130,180,0.6); } .floating-word.collected { animation: wordCollected 0.8s ease-out forwards; } @keyframes wordCollected { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.8; } 100% { transform: scale(0) translateY(-50px); opacity: 0; } } .floating-word.missed { animation: wordMissed 0.6s ease-out forwards; } @keyframes wordMissed { 0% { transform: scale(1); opacity: 1; background: rgba(255,255,255,0.95); } 100% { transform: scale(0.8); opacity: 0; background: rgba(220,20,60,0.8); } } @keyframes wordShake { 0%, 100% { transform: translateX(0) scale(1); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-10px) scale(1.05); } 20%, 40%, 60%, 80% { transform: translateX(10px) scale(1.05); } } .power-up { position: absolute; width: 35px; height: 35px; border-radius: 50%; background: linear-gradient(45deg, #FF6B35, #F7931E); display: flex; align-items: center; justify-content: center; font-size: 1.2em; cursor: pointer; z-index: 45; animation: powerUpFloat 2s ease-in-out infinite alternate; box-shadow: 0 4px 15px rgba(255,107,53,0.4); } @keyframes powerUpFloat { from { transform: translateY(0px) scale(1); } to { transform: translateY(-8px) scale(1.05); } } @keyframes slowTimePulse { from { transform: scale(1); box-shadow: 0 0 15px rgba(255,107,53,0.8); } to { transform: scale(1.05); box-shadow: 0 0 25px rgba(255,107,53,1); } } .game-over-modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255,255,255,0.95); padding: 40px; border-radius: 20px; text-align: center; z-index: 200; box-shadow: 0 10px 30px rgba(0,0,0,0.3); backdrop-filter: blur(10px); } .game-over-title { font-size: 2.5em; margin-bottom: 20px; color: #4682B4; text-shadow: 0 2px 4px rgba(0,0,0,0.1); } .game-over-stats { font-size: 1.3em; margin-bottom: 30px; line-height: 1.6; color: #333; } .river-btn { background: linear-gradient(45deg, #4682B4, #5F9EA0); color: white; border: none; padding: 15px 30px; border-radius: 25px; font-size: 1.1em; font-weight: bold; cursor: pointer; margin: 0 10px; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(70,130,180,0.3); } .river-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(70,130,180,0.4); } .particle { position: absolute; width: 4px; height: 4px; background: rgba(255,255,255,0.7); border-radius: 50%; pointer-events: none; z-index: 30; } .points-popup { position: absolute; color: #FFD700; font-size: 1.5em; font-weight: bold; pointer-events: none; z-index: 100; text-shadow: 0 0 10px rgba(255,215,0,0.8), 0 2px 4px rgba(0,0,0,0.5); animation: pointsFloat 1s ease-out forwards; transform: translate(-50%, 0); } @keyframes pointsFloat { 0% { opacity: 1; transform: translate(-50%, 0) scale(1); } 50% { opacity: 1; transform: translate(-50%, -30px) scale(1.2); } 100% { opacity: 0; transform: translate(-50%, -60px) scale(0.8); } } .game-error { background: rgba(239, 68, 68, 0.1); border: 2px solid #ef4444; border-radius: 15px; padding: 30px; text-align: center; color: #374151; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 500px; } .game-error h3 { color: #ef4444; margin-bottom: 15px; } .back-btn { background: linear-gradient(135deg, #6b7280, #4b5563); color: white; border: none; padding: 12px 25px; border-radius: 25px; font-size: 1.1em; font-weight: bold; cursor: pointer; margin-top: 20px; transition: all 0.3s ease; } .back-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(107, 114, 128, 0.4); } @keyframes particleSpread { 0% { transform: scale(1) translate(0, 0); opacity: 1; } 100% { transform: scale(0) translate(50px, 50px); opacity: 0; } } @keyframes particleFlow { 0% { transform: translateY(0); opacity: 0.7; } 100% { transform: translateY(100vh); opacity: 0; } } @media (max-width: 768px) { .river-run-hud { flex-direction: column; gap: 10px; } .floating-word { font-size: 1em; padding: 6px 12px; } .target-display { font-size: 1em; padding: 8px 15px; } .hud-left, .hud-right { justify-content: center; } } /* Victory Popup Styles */ .victory-popup { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 10000; animation: fadeIn 0.3s ease-out; } .victory-content { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; padding: 40px; text-align: center; color: white; max-width: 500px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); animation: slideUp 0.4s ease-out; } .victory-header { margin-bottom: 30px; } .victory-icon { font-size: 4rem; margin-bottom: 15px; animation: bounce 0.6s ease-out; } .victory-title { font-size: 2rem; font-weight: bold; margin: 0 0 10px 0; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .new-best-badge { background: linear-gradient(45deg, #f093fb 0%, #f5576c 100%); color: white; padding: 8px 20px; border-radius: 25px; font-size: 0.9rem; font-weight: bold; display: inline-block; margin-top: 10px; animation: glow 1s ease-in-out infinite alternate; } .victory-scores { display: flex; justify-content: space-around; margin: 30px 0; gap: 20px; } .score-display { background: rgba(255, 255, 255, 0.1); border-radius: 15px; padding: 20px; flex: 1; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2); } .score-label { font-size: 0.9rem; opacity: 0.9; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; } .score-value { font-size: 2rem; font-weight: bold; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .victory-stats { background: rgba(255, 255, 255, 0.1); border-radius: 15px; padding: 20px; margin: 30px 0; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2); } .stat-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .stat-row:last-child { border-bottom: none; } .stat-name { font-size: 0.95rem; opacity: 0.9; } .stat-value { font-weight: bold; font-size: 1rem; } .victory-buttons { display: flex; flex-direction: column; gap: 12px; margin-top: 30px; } .victory-btn { padding: 15px 30px; border: none; border-radius: 25px; font-size: 1rem; font-weight: bold; cursor: pointer; transition: all 0.3s ease; text-transform: uppercase; letter-spacing: 1px; } .victory-btn.primary { background: linear-gradient(45deg, #4facfe 0%, #00f2fe 100%); color: white; box-shadow: 0 8px 25px rgba(79, 172, 254, 0.3); } .victory-btn.primary:hover { transform: translateY(-2px); box-shadow: 0 12px 35px rgba(79, 172, 254, 0.4); } .victory-btn.secondary { background: linear-gradient(45deg, #a8edea 0%, #fed6e3 100%); color: #333; box-shadow: 0 8px 25px rgba(168, 237, 234, 0.3); } .victory-btn.secondary:hover { transform: translateY(-2px); box-shadow: 0 12px 35px rgba(168, 237, 234, 0.4); } .victory-btn.tertiary { background: rgba(255, 255, 255, 0.2); color: white; border: 2px solid rgba(255, 255, 255, 0.3); backdrop-filter: blur(10px); } .victory-btn.tertiary:hover { background: rgba(255, 255, 255, 0.3); transform: translateY(-2px); } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUp { from { opacity: 0; transform: translateY(30px) scale(0.9); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes bounce { 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(-10px); } 60% { transform: translateY(-5px); } } @keyframes glow { from { box-shadow: 0 0 20px rgba(245, 87, 108, 0.5); } to { box-shadow: 0 0 30px rgba(245, 87, 108, 0.8); } } @media (max-width: 768px) { .victory-content { padding: 30px 20px; width: 95%; } .victory-scores { flex-direction: column; gap: 15px; } .victory-icon { font-size: 3rem; } .victory-title { font-size: 1.5rem; } .victory-buttons { gap: 10px; } .victory-btn { padding: 12px 25px; font-size: 0.9rem; } } `; document.head.appendChild(style); } _showVictoryPopup({ gameTitle, currentScore, bestScore, isNewBest, stats }) { const popup = document.createElement('div'); popup.className = 'victory-popup'; popup.innerHTML = `
🌊

${gameTitle} Complete!

${isNewBest ? '
🎉 New Best Score!
' : ''}
Your Score
${currentScore}
Best Score
${bestScore}
${Object.entries(stats).map(([key, value]) => `
${key} ${value}
`).join('')}
`; document.body.appendChild(popup); } _removeCSS() { const cssElement = document.getElementById('river-run-styles'); if (cssElement) { cssElement.remove(); } if (window.currentRiverGame === this) { delete window.currentRiverGame; } } } export default RiverRun;