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>
1001 lines
31 KiB
JavaScript
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; |