Class_generator/src/games/RiverRun.js
StillHammer 05142bdfbc Implement comprehensive AI text report/export system
- Add AIReportSystem.js for detailed AI response capture and report generation
- Add AIReportInterface.js UI component for report access and export
- Integrate AI reporting into LLMValidator and SmartPreviewOrchestrator
- Add missing modules to Application.js configuration (unifiedDRS, smartPreviewOrchestrator)
- Create missing content/chapters/sbs.json for book metadata
- Enhance Application.js with debug logging for module loading
- Add multi-format export capabilities (text, HTML, JSON)
- Implement automatic learning insights extraction from AI feedback
- Add session management and performance tracking for AI reports

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 21:24:13 +08:00

1428 lines
44 KiB
JavaScript

import Module from '../core/Module.js';
class RiverRun extends Module {
constructor(name, dependencies, config = {}) {
super(name, ['eventBus']);
if (!dependencies.eventBus || !dependencies.content) {
throw new Error('RiverRun requires eventBus and content dependencies');
}
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;
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);
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;
}
if (this._gameContainer) {
this._gameContainer.innerHTML = '';
}
if (window.currentRiverGame === this) {
delete window.currentRiverGame;
}
}
_showInitError(message) {
this._gameContainer.innerHTML = `
<div class="game-error">
<h3>❌ Loading Error</h3>
<p>${message}</p>
<p>The game requires vocabulary content with words and translations.</p>
<button onclick="window.app.getCore().router.navigate('/games')" class="back-btn">← Back to Games</button>
</div>
`;
}
_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 = `
<div class="river-run-wrapper" id="river-game">
<div class="river-run-hud">
<div class="hud-left">
<div>Score: <span id="score-display">${this._score}</span></div>
<div>Lives: <span id="lives-display">${this._lives}</span></div>
<div>Words: <span id="words-display">${this._wordsCollected}</span></div>
</div>
<div class="target-display" id="target-display">
Click to Start!
</div>
<div class="hud-right">
<div>Level: <span id="level-display">${this._level}</span></div>
<div>Speed: <span id="speed-display">${this._speed.toFixed(1)}x</span></div>
</div>
</div>
<div class="river-canvas" id="river-canvas">
<div class="river-waves"></div>
<div class="player" id="player"></div>
</div>
</div>
`;
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._gameLoop();
console.log('River Run started!');
}
_gameLoop() {
if (!this._isRunning) return;
const now = Date.now();
if (now - this._lastSpawn > this._config.spawnInterval) {
this._spawnFloatingWord();
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: <span style="color: #FF6B35;">${this._currentTarget.english}</span>`;
}
}
_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;
wordElement.style.left = `${Math.random() * 80 + 10}%`;
wordElement.style.top = '-60px';
wordElement.wordData = word;
riverCanvas.appendChild(wordElement);
this._floatingWords.push({
element: wordElement,
y: -60,
x: parseFloat(wordElement.style.left),
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 = '⚡';
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`;
if (word.y > window.innerHeight + 60) {
if (word.wordData.french === this._currentTarget.french) {
this._loseLife();
}
word.element.remove();
return false;
}
return true;
});
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);
}
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();
this._floatingWords.forEach((word, index) => {
const wordRect = this._getElementRect(word.element);
if (this._isColliding(playerRect, wordRect)) {
this._handleWordCollision(word, index);
}
});
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;
if (wordData.french === this._currentTarget.french) {
this._collectWord(wordElement, true);
} else {
this._missWord(wordElement);
}
}
_handleWordCollision(word, index) {
if (word.wordData.french === this._currentTarget.french) {
this._collectWord(word.element, true);
} else {
this._missWord(word.element);
}
this._floatingWords.splice(index, 1);
}
_collectWord(wordElement, isCorrect) {
wordElement.classList.add('collected');
if (isCorrect) {
this._score += 10 + (this._level * 2);
this._wordsCollected++;
this._eventBus.emit('game:score-update', {
gameId: 'river-run',
score: this._score,
module: this.name
});
this._setNextTarget();
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;
if (newLevel > this._level) {
this._level = newLevel;
this._speed += 0.5;
this._config.spawnInterval = Math.max(500, this._config.spawnInterval - 100);
}
}
_playSuccessSound(word) {
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(word.trim());
utterance.lang = 'fr-FR';
utterance.rate = 1.0;
speechSynthesis.speak(utterance);
}
}
_loseLife() {
this._lives--;
if (this._lives <= 0) {
this._gameOver();
}
}
_gameOver() {
this._isRunning = false;
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());
}
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() {
this._eventBus.emit('game:end', {
gameId: 'river-run',
score: this._score,
module: this.name
});
}
_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() {
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');
}
_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-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);
}
}
.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;
}
.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 = `
<div class="victory-content">
<div class="victory-header">
<div class="victory-icon">🌊</div>
<h2 class="victory-title">${gameTitle} Complete!</h2>
${isNewBest ? '<div class="new-best-badge">🎉 New Best Score!</div>' : ''}
</div>
<div class="victory-scores">
<div class="score-display">
<div class="score-label">Your Score</div>
<div class="score-value">${currentScore}</div>
</div>
<div class="score-display best-score">
<div class="score-label">Best Score</div>
<div class="score-value">${bestScore}</div>
</div>
</div>
<div class="victory-stats">
${Object.entries(stats).map(([key, value]) => `
<div class="stat-row">
<span class="stat-name">${key}</span>
<span class="stat-value">${value}</span>
</div>
`).join('')}
</div>
<div class="victory-buttons">
<button class="victory-btn primary" onclick="this.closest('.victory-popup').remove(); window.currentRiverGame._restart();">🔄 Play Again</button>
<button class="victory-btn secondary" onclick="this.closest('.victory-popup').remove(); window.app.getCore().router.navigate('/games');">🎮 Different Game</button>
<button class="victory-btn tertiary" onclick="this.closest('.victory-popup').remove(); window.app.getCore().router.navigate('/');">🏠 Main Menu</button>
</div>
</div>
`;
document.body.appendChild(popup);
// Emit completion event after showing popup
setTimeout(() => {
this._endGame();
}, 1000);
}
_removeCSS() {
const cssElement = document.getElementById('river-run-styles');
if (cssElement) {
cssElement.remove();
}
if (window.currentRiverGame === this) {
delete window.currentRiverGame;
}
}
}
export default RiverRun;