Class_generator/Legacy/js/games/river-run.js
StillHammer 38920cc858 Complete architectural rewrite with ultra-modular system
Major Changes:
- Moved legacy system to Legacy/ folder for archival
- Built new modular architecture with strict separation of concerns
- Created core system: Module, EventBus, ModuleLoader, Router
- Added Application bootstrap with auto-start functionality
- Implemented development server with ES6 modules support
- Created comprehensive documentation and project context
- Converted SBS-7-8 content to JSON format
- Copied all legacy games and content to new structure

New Architecture Features:
- Sealed modules with WeakMap private data
- Strict dependency injection system
- Event-driven communication only
- Inviolable responsibility patterns
- Auto-initialization without commands
- Component-based UI foundation ready

Technical Stack:
- Vanilla JS/HTML/CSS only
- ES6 modules with proper imports/exports
- HTTP development server (no file:// protocol)
- Modular CSS with component scoping
- Comprehensive error handling and debugging

Ready for Phase 2: Converting legacy modules to new architecture

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 07:08:39 +08:00

1001 lines
31 KiB
JavaScript

// === 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 = `
<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>
`;
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: <span style="color: #FF6B35;">${this.currentTarget.english}</span>`;
}
}
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 = `
<div class="game-over-title">🌊 River Complete!</div>
<div class="game-over-stats">
Final Score: ${this.score}<br>
Words Collected: ${this.wordsCollected}<br>
Level Reached: ${this.level}<br>
Accuracy: ${accuracy}%
</div>
<div>
<button class="river-btn" onclick="window.currentRiverGame.restart()">
🔄 Sail Again
</button>
<button class="river-btn" onclick="window.currentRiverGame.onGameEnd(${this.score})">
🏠 Back to Games
</button>
</div>
`;
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;