// === RIVER RUN GAME === // Endless runner on a river with floating words - avoid obstacles, catch target words! class RiverRun { constructor({ container, content, onScoreUpdate, onGameEnd }) { this.container = container; this.content = content; this.onScoreUpdate = onScoreUpdate; this.onGameEnd = onGameEnd; // Game state this.isRunning = false; this.score = 0; this.lives = 3; this.level = 1; this.speed = 2; // River flow speed this.wordsCollected = 0; // Player this.player = { x: 50, // Percentage from left y: 80, // Percentage from top targetX: 50, targetY: 80, size: 40 }; // Game objects this.floatingWords = []; this.currentTarget = null; this.targetQueue = []; this.powerUps = []; // River animation this.riverOffset = 0; this.particles = []; // Timing this.lastSpawn = 0; this.spawnInterval = 1000; // ms between word spawns (2x faster) this.gameStartTime = Date.now(); // Word management this.availableWords = []; this.usedTargets = []; // Target word guarantee system this.wordsSpawnedSinceTarget = 0; this.maxWordsBeforeTarget = 10; // Guarantee target within 10 words this.injectCSS(); this.extractContent(); this.init(); } injectCSS() { if (document.getElementById('river-run-styles')) return; const styleSheet = document.createElement('style'); styleSheet.id = 'river-run-styles'; styleSheet.textContent = ` .river-run-wrapper { background: linear-gradient(180deg, #87CEEB 0%, #4682B4 50%, #2F4F4F 100%); position: relative; overflow: hidden; height: 100vh; cursor: crosshair; } .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); } /* Words are neutral at spawn - styling happens at interaction */ .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); } } .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); } } .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; } .level-indicator { position: absolute; top: 70px; left: 20px; background: rgba(255,255,255,0.9); color: #333; padding: 5px 15px; border-radius: 15px; font-size: 0.9em; font-weight: bold; z-index: 100; } /* Responsive */ @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; } } `; document.head.appendChild(styleSheet); } extractContent() { logSh('🌊 River Run - Extracting vocabulary...', 'INFO'); // Extract words from various content formats if (this.content.vocabulary) { Object.keys(this.content.vocabulary).forEach(word => { const wordData = this.content.vocabulary[word]; this.availableWords.push({ french: word, english: typeof wordData === 'string' ? wordData : wordData.translation || wordData.user_language || 'unknown', pronunciation: wordData.pronunciation || wordData.prononciation }); }); } // Fallback: extract from letter structure if available if (this.content.letters && this.availableWords.length === 0) { Object.values(this.content.letters).forEach(letterWords => { letterWords.forEach(wordData => { this.availableWords.push({ french: wordData.word, english: wordData.translation, pronunciation: wordData.pronunciation }); }); }); } if (this.availableWords.length === 0) { throw new Error('No vocabulary found for River Run'); } logSh(`🎯 River Run ready: ${this.availableWords.length} words available`, 'INFO'); this.generateTargetQueue(); } generateTargetQueue() { // Create queue of targets, ensuring variety this.targetQueue = this.shuffleArray([...this.availableWords]).slice(0, Math.min(10, this.availableWords.length)); this.usedTargets = []; } init() { this.container.innerHTML = `
Score: ${this.score}
Lives: ${this.lives}
Words: ${this.wordsCollected}
Click to Start!
Level: ${this.level}
Speed: ${this.speed.toFixed(1)}x
`; this.setupEventListeners(); this.updateHUD(); } 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); }); // Handle floating word clicks 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(); // Start game loop this.gameLoop(); logSh('🌊 River Run started!', 'INFO'); } gameLoop() { if (!this.isRunning) return; const now = Date.now(); // Spawn new words if (now - this.lastSpawn > this.spawnInterval) { this.spawnFloatingWord(); this.lastSpawn = now; } // Update game objects this.updateFloatingWords(); this.updatePlayer(); this.updateParticles(); this.checkCollisions(); // Increase difficulty over time this.updateDifficulty(); // Update UI this.updateHUD(); // Continue loop requestAnimationFrame(() => this.gameLoop()); } setNextTarget() { if (this.targetQueue.length === 0) { this.generateTargetQueue(); } this.currentTarget = this.targetQueue.shift(); this.usedTargets.push(this.currentTarget); // Reset the word counter for new target 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; // Determine if we should force the target word let word; if (this.wordsSpawnedSinceTarget >= this.maxWordsBeforeTarget) { // Force target word to appear word = this.currentTarget; this.wordsSpawnedSinceTarget = 0; // Reset counter logSh(`🎯 Forcing target word: ${word.french}`, 'DEBUG'); } else { // Spawn random word word = this.getRandomWord(); this.wordsSpawnedSinceTarget++; } const wordElement = document.createElement('div'); wordElement.className = 'floating-word'; // No target/obstacle class at spawn // Add spaces based on level for increased difficulty const spacePadding = ' '.repeat(this.level * 2); // 2 spaces per level on each side wordElement.textContent = spacePadding + word.french + spacePadding; wordElement.style.left = `${Math.random() * 80 + 10}%`; wordElement.style.top = '-60px'; // Store word data only wordElement.wordData = word; riverCanvas.appendChild(wordElement); this.floatingWords.push({ element: wordElement, y: -60, x: parseFloat(wordElement.style.left), wordData: word }); // Occasional power-up spawn if (Math.random() < 0.1) { this.spawnPowerUp(); } } getRandomWord() { // Simply return any random word from available vocabulary 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 = '⚡'; powerUpElement.style.left = `${Math.random() * 80 + 10}%`; powerUpElement.style.top = '-40px'; riverCanvas.appendChild(powerUpElement); this.powerUps.push({ element: powerUpElement, y: -40, x: parseFloat(powerUpElement.style.left), type: 'slowTime' }); } updateFloatingWords() { this.floatingWords = this.floatingWords.filter(word => { word.y += this.speed; word.element.style.top = `${word.y}px`; // Remove words that went off screen if (word.y > window.innerHeight + 60) { // CHECK AT EXIT TIME: Was this the target word? if (word.wordData.french === this.currentTarget.french) { // Missed target word - lose life this.loseLife(); } word.element.remove(); return false; } return true; }); // Update power-ups this.powerUps = this.powerUps.filter(powerUp => { powerUp.y += this.speed; powerUp.element.style.top = `${powerUp.y}px`; if (powerUp.y > window.innerHeight + 40) { 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); } // Create ripple effect this.createRippleEffect(targetX, targetY); } updatePlayer() { // Smooth movement towards target 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() { // Create water particles occasionally 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 this.floatingWords.forEach((word, index) => { const wordRect = this.getElementRect(word.element); if (this.isColliding(playerRect, wordRect)) { this.handleWordCollision(word, index); } }); // Check power-up collisions this.powerUps.forEach((powerUp, index) => { const powerUpRect = this.getElementRect(powerUp.element); if (this.isColliding(playerRect, powerUpRect)) { this.handlePowerUpCollision(powerUp, index); } }); } getPlayerRect() { const playerElement = document.getElementById('player'); if (!playerElement) return { x: 0, y: 0, width: 0, height: 0 }; const rect = playerElement.getBoundingClientRect(); const canvas = document.getElementById('river-canvas').getBoundingClientRect(); return { x: rect.left - canvas.left, y: rect.top - canvas.top, width: rect.width, height: rect.height }; } getElementRect(element) { const rect = element.getBoundingClientRect(); const canvas = document.getElementById('river-canvas').getBoundingClientRect(); return { x: rect.left - canvas.left, y: rect.top - canvas.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; // CHECK AT PICK TIME: Is this the target word? if (wordData.french === this.currentTarget.french) { // Correct target word clicked this.collectWord(wordElement, true); } else { // Wrong word clicked - it's an obstacle this.missWord(wordElement); } } handleWordCollision(word, index) { // CHECK AT COLLISION TIME: Is this the target word? if (word.wordData.french === this.currentTarget.french) { this.collectWord(word.element, true); } else { // Collision with non-target word = obstacle hit this.missWord(word.element); } // Remove from array this.floatingWords.splice(index, 1); } collectWord(wordElement, isCorrect) { wordElement.classList.add('collected'); if (isCorrect) { this.score += 10 + (this.level * 2); this.wordsCollected++; this.onScoreUpdate(this.score); // Set next target this.setNextTarget(); // Play success sound this.playSuccessSound(wordElement.textContent); } setTimeout(() => { wordElement.remove(); }, 800); } missWord(wordElement) { wordElement.classList.add('missed'); this.loseLife(); setTimeout(() => { wordElement.remove(); }, 600); } handlePowerUpCollision(powerUp, index) { this.activatePowerUp(powerUp.type); powerUp.element.remove(); this.powerUps.splice(index, 1); } activatePowerUp(type) { switch (type) { case 'slowTime': this.speed *= 0.5; setTimeout(() => { this.speed *= 2; }, 3000); break; } } updateDifficulty() { const timeElapsed = Date.now() - this.gameStartTime; const newLevel = Math.floor(timeElapsed / 30000) + 1; // Level up every 30 seconds if (newLevel > this.level) { this.level = newLevel; this.speed += 0.5; this.spawnInterval = Math.max(500, this.spawnInterval - 100); // More aggressive spawn increase } } playSuccessSound(word) { if (window.SettingsManager && window.SettingsManager.speak) { window.SettingsManager.speak(word, { lang: this.content.language || 'fr-FR', rate: 1.0 }).catch(error => { console.warn('🔊 TTS failed:', error); }); } } loseLife() { this.lives--; if (this.lives <= 0) { this.gameOver(); } } gameOver() { this.isRunning = false; const riverGame = document.getElementById('river-game'); const accuracy = this.wordsCollected > 0 ? Math.round((this.wordsCollected / (this.wordsCollected + (3 - this.lives))) * 100) : 0; const gameOverModal = document.createElement('div'); gameOverModal.className = 'game-over-modal'; gameOverModal.innerHTML = `
🌊 River Complete!
Final Score: ${this.score}
Words Collected: ${this.wordsCollected}
Level Reached: ${this.level}
Accuracy: ${accuracy}%
`; riverGame.appendChild(gameOverModal); // Store reference for button callbacks window.currentRiverGame = this; setTimeout(() => { this.onGameEnd(this.score); }, 5000); } 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'; } 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; } restart() { // Reset game state this.isRunning = false; this.score = 0; this.lives = 3; this.level = 1; this.speed = 2; this.wordsCollected = 0; this.riverOffset = 0; // Reset player position this.player.x = 50; this.player.y = 80; this.player.targetX = 50; this.player.targetY = 80; // Clear game objects this.floatingWords = []; this.powerUps = []; this.particles = []; // Reset timing this.lastSpawn = 0; this.spawnInterval = 1000; // 2x faster spawn rate this.gameStartTime = Date.now(); // Reset targets and word counter this.wordsSpawnedSinceTarget = 0; this.generateTargetQueue(); // Cleanup DOM 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(); } // Reset target display const targetDisplay = document.getElementById('target-display'); if (targetDisplay) { targetDisplay.textContent = 'Click to Start!'; } this.updateHUD(); logSh('🔄 River Run restarted', 'INFO'); } destroy() { this.isRunning = false; // Cleanup if (window.currentRiverGame === this) { delete window.currentRiverGame; } const styleSheet = document.getElementById('river-run-styles'); if (styleSheet) { styleSheet.remove(); } } } // Add CSS animations const additionalCSS = ` @keyframes particleSpread { 0% { transform: scale(1) translate(0, 0); opacity: 1; } 100% { transform: scale(0) translate(${Math.random() * 100 - 50}px, ${Math.random() * 100 - 50}px); opacity: 0; } } @keyframes particleFlow { 0% { transform: translateY(0); opacity: 0.7; } 100% { transform: translateY(100vh); opacity: 0; } } `; // Inject additional CSS const additionalStyleSheet = document.createElement('style'); additionalStyleSheet.textContent = additionalCSS; document.head.appendChild(additionalStyleSheet); // Register the game module window.GameModules = window.GameModules || {}; window.GameModules.RiverRun = RiverRun;