Class_generator/js/games/river-run.js
StillHammer 24362165ab Implement dynamic percentage compatibility system across all games
Major architectural update to replace fixed 50%/100% scoring with true dynamic percentages based on content volume:

• Replace old interpolation system with Math.min(100, (count/optimal)*100) formula
• Add embedded compatibility methods to all 14 game modules with static requirements
• Remove compatibility cache system for real-time calculation
• Fix content loading to pass complete modules with vocabulary (not just metadata)
• Clean up duplicate syntax errors in adventure-reader and grammar-discovery
• Update navigation.js module mapping to match actual exported class names

Examples of new dynamic scoring:
- 15 words / 20 optimal = 75% (was 87.5% with old interpolation)
- 5 words / 10 minimum = 50% (was 25% with old linear system)
- 30 words / 20 optimal = 100% (unchanged)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 17:00:52 +08:00

1134 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// === 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();
}
}
// === COMPATIBILITY SYSTEM ===
static getCompatibilityRequirements() {
return {
minimum: {
vocabulary: 10
},
optimal: {
vocabulary: 25
},
name: "River Run",
description: "Vocabulary collection game where player navigates river to collect target words"
};
}
static checkContentCompatibility(content) {
const requirements = RiverRun.getCompatibilityRequirements();
// Extract vocabulary using same method as instance
const vocabulary = RiverRun.extractVocabularyStatic(content);
const vocabCount = vocabulary.length;
// Dynamic percentage based on optimal volume (10 min → 25 optimal)
// 0 words = 0%, 12 words = 48%, 25 words = 100%
const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100);
return {
score: Math.round(score),
details: {
vocabulary: {
found: vocabCount,
minimum: requirements.minimum.vocabulary,
optimal: requirements.optimal.vocabulary,
status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient'
}
},
recommendations: vocabCount < requirements.optimal.vocabulary ?
[`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`] :
[]
};
}
static extractVocabularyStatic(content) {
let vocabulary = [];
// Priority 1: Use raw module content (simple format)
if (content.rawContent) {
return RiverRun.extractVocabularyFromRawStatic(content.rawContent);
}
// Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
vocabulary = Object.entries(content.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
word: word,
translation: data.user_language.split('')[0],
fullTranslation: data.user_language,
type: data.type || 'general',
audio: data.audio,
image: data.image,
examples: data.examples,
pronunciation: data.pronunciation,
category: data.type || 'general'
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
word: word,
translation: data.split('')[0],
fullTranslation: data,
type: 'general',
category: 'general'
};
}
return null;
}).filter(Boolean);
}
return RiverRun.finalizeVocabularyStatic(vocabulary);
}
static extractVocabularyFromRawStatic(rawContent) {
let vocabulary = [];
// Ultra-modular format (vocabulary object) - ONLY format supported
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
word: word,
translation: data.user_language.split('')[0],
fullTranslation: data.user_language,
type: data.type || 'general',
audio: data.audio,
image: data.image,
examples: data.examples,
pronunciation: data.pronunciation,
category: data.type || 'general'
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
word: word,
translation: data.split('')[0],
fullTranslation: data,
type: 'general',
category: 'general'
};
}
return null;
}).filter(Boolean);
}
return RiverRun.finalizeVocabularyStatic(vocabulary);
}
static finalizeVocabularyStatic(vocabulary) {
// Validation and cleanup for ultra-modular format
vocabulary = vocabulary.filter(word =>
word &&
typeof word.word === 'string' &&
typeof word.translation === 'string' &&
word.word.trim() !== '' &&
word.translation.trim() !== ''
);
return vocabulary;
}
}
// 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;