commit b004382ceebbca9fd8006f63a4252b2fba9c6ce7 Author: StillHammer Date: Mon Sep 15 14:25:13 2025 +0800 Initial commit: Interactive English Learning Platform - Complete SPA architecture with dynamic module loading - 9 different educational games (whack-a-mole, memory, quiz, etc.) - Rich content system supporting multimedia (audio, images, video) - Chinese study mode with character recognition - Adaptive game system based on available content - Content types: vocabulary, grammar, poems, fill-blanks, corrections - AI-powered text evaluation for open-ended answers - Flexible content schema with backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbc5150 --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ +*.tgz +*.tar.gz + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Audio files (can be large) +audio/*.mp3 +audio/*.wav +audio/*.ogg +audio/*.m4a + +# Image files (can be large) +images/*.jpg +images/*.jpeg +images/*.png +images/*.gif +images/*.bmp +images/*.svg + +# Video files +videos/*.mp4 +videos/*.avi +videos/*.mov +videos/*.wmv + +# Temporary files +tmp/ +temp/ +*.tmp + +# Backup files +*.bak +*.backup +*.old + +# Cache directories +.cache/ +.parcel-cache/ + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# ESLint cache +.eslintcache + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# User-specific content that shouldn't be shared +user-content/ +personal-notes.txt +config/user-settings.json + +# AI models and large datasets (if any) +models/ +datasets/ +*.model +*.dat \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5fc2bd7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,330 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Interactive English learning platform for children (8-9 years old) built as a modular Single Page Application. The system provides 9 different educational games that work with various content modules through a flexible architecture. + +## Key Architecture Patterns + +### Core System Flow +1. **AppNavigation** (`js/core/navigation.js`) - Central SPA navigation controller +2. **ContentScanner** (`js/core/content-scanner.js`) - Auto-discovers available content modules +3. **GameLoader** (`js/core/game-loader.js`) - Dynamically loads game and content modules +4. **Content Engine** (`js/core/content-engine.js`) - Processes and adapts content for games + +### Module Loading System +- Games and content are loaded dynamically via `GameLoader.loadGame(gameType, contentType)` +- All modules register themselves on global objects: `window.GameModules` and `window.ContentModules` +- Content is discovered automatically by `ContentScanner` scanning `js/content/` directory +- Games follow consistent constructor pattern: `new GameClass({ container, content, onScoreUpdate, onGameEnd })` + +### URL-Based Navigation +- Single HTML file (`index.html`) handles all navigation via URL parameters +- Routes: `?page=home|games|levels|play&game=&content=` +- Browser back/forward supported through `popstate` events +- Navigation history maintained in `AppNavigation.navigationHistory` +- Breadcrumb navigation with clickable path elements +- Keyboard shortcuts (ESC = go back) + +## Content Module Format + +### Rich Content Schema (New Architecture) + +Content modules support rich, multimedia educational content with optional properties. The system adapts games and exercises based on available content features: + +```javascript +window.ContentModules.ModuleName = { + name: "Display Name", + description: "Description text", + difficulty: "easy|medium|hard|beginner|intermediate|advanced", + language: "chinese|english|french|spanish", // Target learning language + hskLevel: "HSK1|HSK2|HSK3|HSK4|HSK5|HSK6", // Chinese proficiency level + + // Rich vocabulary with optional multimedia + vocabulary: { + "word_or_character": { + translation: "English translation", + pinyin: "pronunciation guide", // Optional: Chinese pinyin + type: "noun|verb|adjective|greeting|number", // Word classification + pronunciation: "audio/word.mp3", // Optional: audio file + difficulty: "HSK1|HSK2|...", // Optional: individual word difficulty + strokeOrder: ["stroke1", "stroke2"], // Optional: character writing order + examples: ["example sentence 1"], // Optional: usage examples + grammarNotes: "special usage rules" // Optional: grammar context + } + // OR simple format for basic content: + // "word": "simple translation" + }, + + // Grammar rules and explanations + grammar: { + topic_name: { + title: "Grammar Rule Title", + explanation: "Detailed explanation", + examples: [ + { chinese: "中文例子", english: "English example", pinyin: "zhōng wén lì zi" } + ], + exercises: [/* grammar-specific exercises */] + } + }, + + // Audio content with/without text + audio: { + withText: [ + { + title: "Audio Lesson Title", + audioFile: "audio/lesson1.mp3", + transcript: "Full text transcript", + translation: "English translation", + timestamps: [{ time: 5.2, text: "specific segment" }] // Optional + } + ], + withoutText: [ + { + title: "Listening Challenge", + audioFile: "audio/challenge1.mp3", + questions: [ + { question: "What did they say?", type: "ai_interpreted" } + ] + } + ] + }, + + // Poetry and cultural content + poems: [ + { + title: "Poem Title", + content: "Full poem text", + translation: "English translation", + audioFile: "audio/poem1.mp3", // Optional + culturalContext: "Historical background" + } + ], + + // Fill-in-the-blank exercises + fillInBlanks: [ + { + sentence: "I _____ to school every day", + options: ["go", "goes", "going", "went"], // Multiple choice options + correctAnswer: "go", + explanation: "Present tense with 'I'" + }, + { + sentence: "The weather is _____ today", + type: "open_ended", // AI-interpreted answers + acceptedAnswers: ["nice", "good", "beautiful", "sunny"], + aiPrompt: "Evaluate if answer describes weather positively" + } + ], + + // Sentence correction exercises + corrections: [ + { + incorrect: "I are happy today", + correct: "I am happy today", + explanation: "Use 'am' with pronoun 'I'", + type: "grammar_correction" + } + ], + + // Reading comprehension with AI evaluation + comprehension: [ + { + text: "Long reading passage...", + questions: [ + { + question: "What is the main idea?", + type: "ai_interpreted", + evaluationPrompt: "Check if answer captures main theme" + }, + { + question: "Multiple choice question?", + type: "multiple_choice", + options: ["A", "B", "C", "D"], + correctAnswer: "B" + } + ] + } + ], + + // Matching exercises (connect lines between columns) + matching: [ + { + title: "Match Words to Meanings", + leftColumn: ["apple", "book", "car"], + rightColumn: ["苹果", "书", "车"], + correctPairs: [ + { left: "apple", right: "苹果" }, + { left: "book", right: "书" }, + { left: "car", right: "车" } + ] + } + ], + + // Standard content (backward compatibility) + sentences: [{ english: "...", chinese: "...", pinyin: "..." }], + texts: [{ title: "...", content: "...", translation: "..." }], + dialogues: [{ conversation: [...] }] +}; +``` + +### Content Adaptivity System + +The platform automatically adapts available games and exercises based on content richness: + +**Content Analysis:** +- System scans each content module for available features +- Generates compatibility scores for each game type +- Recommends optimal learning activities +- Handles graceful degradation when content is incomplete + +**Adaptive Game Selection:** +- **Rich vocabulary** → Enable advanced matching games, pronunciation practice +- **Audio files present** → Enable listening exercises, pronunciation challenges +- **Grammar rules** → Enable correction exercises, structured lessons +- **Fill-in-blanks data** → Enable cloze tests with multiple choice or AI evaluation +- **Minimal content** → Fall back to basic vocabulary games + +**Missing Content Handling:** +- Display helpful messages: "Add audio files to enable pronunciation practice" +- Suggest content enrichment opportunities +- Gracefully disable incompatible game modes +- Provide content creation tools for missing elements + +**Example Adaptive Behavior:** +```javascript +// Content with only basic vocabulary +{ vocabulary: { "hello": "你好" } } +→ Enable: Basic matching, simple quiz +→ Disable: Audio practice, grammar exercises +→ Suggest: "Add pinyin and audio for pronunciation practice" + +// Rich multimedia content +{ vocabulary: { "hello": { translation: "你好", pinyin: "nǐ hǎo", pronunciation: "audio/hello.mp3" } } } +→ Enable: All vocabulary games, audio practice, pronunciation scoring +→ Unlock: Advanced difficulty levels, speed challenges +``` + +## Game Module Format + +Game modules must export to `window.GameModules` with this pattern: +```javascript +class GameName { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.container = container; + this.content = content; + this.onScoreUpdate = onScoreUpdate; + this.onGameEnd = onGameEnd; + } + + start() { /* Initialize game */ } + destroy() { /* Cleanup */ } + restart() { /* Reset game state */ } +} + +window.GameModules = window.GameModules || {}; +window.GameModules.GameName = GameName; +``` + +## Configuration System + +- Main config: `config/games-config.json` - defines available games and content +- Games can be enabled/disabled via `games.{gameType}.enabled` +- Content modules auto-detected but can be configured in `content` section +- UI settings, scoring rules, and feature flags also in main config + +## Development Workflow + +### Running the Application +Open `index.html` in a web browser - no build process required. All modules load dynamically. + +### Adding New Games +1. Create `js/games/{game-name}.js` with proper module export +2. Add game configuration to `config/games-config.json` +3. Update `AppNavigation.getDefaultConfig()` if needed + +### Adding New Content +1. Create `js/content/{content-name}.js` with proper module export +2. Add filename to `ContentScanner.contentFiles` array +3. Content will be auto-discovered on next app load + +### Content Creation Tool +- Built-in content creator at `js/tools/content-creator.js` +- Accessible via "Créateur de Contenu" button on home page +- Generates properly formatted content modules + +## Key Files by Function + +**Navigation & Loading:** +- `js/core/navigation.js` - SPA navigation controller (452 lines) +- `js/core/game-loader.js` - Dynamic module loading (336 lines) +- `js/core/content-scanner.js` - Auto content discovery (376 lines) + +**Content Processing:** +- `js/core/content-engine.js` - Content processing engine (484 lines) +- `js/core/content-factory.js` - Exercise generation (553 lines) +- `js/core/content-parsers.js` - Content parsing utilities (484 lines) + +**Game Implementations:** +- `js/games/whack-a-mole.js` - Standard version (623 lines) +- `js/games/whack-a-mole-hard.js` - Difficult version (643 lines) +- `js/games/memory-match.js` - Memory pairs game (403 lines) +- `js/games/quiz-game.js` - Quiz system (354 lines) +- `js/games/fill-the-blank.js` - Sentence completion (418 lines) +- `js/games/text-reader.js` - Guided text reading (366 lines) +- `js/games/adventure-reader.js` - RPG-style adventure (949 lines) + +## Important Implementation Details + +### Scoring System +- Games call `this.onScoreUpdate(score)` to update display +- Final scores saved to localStorage with key pattern: `score_{gameType}_{contentType}` +- Best scores tracked and displayed in game-end modal +- Points per correct answer, malus per error, speed bonus +- Score history and achievement badges + +### Content Compatibility +- `ContentScanner` evaluates content compatibility with each game type +- Compatibility scoring helps recommend best content for each game +- Games should handle various content formats gracefully + +### Memory Management +- `GameLoader.cleanup()` called before loading new games +- Games should implement `destroy()` method for proper cleanup +- Previous game instances must be cleaned up to prevent memory leaks + +### Error Handling +- Content loading errors logged but don't crash the application +- Fallback mechanisms for missing content or games +- User-friendly error messages via `Utils.showToast()` + +## Design Guidelines + +### Visual Design Principles +- Modern, clean design optimized for children (8-9 years old) +- Large, tactile buttons (minimum 44px for touch interfaces) +- High contrast colors for accessibility +- Smooth, non-aggressive animations +- Emoji icons combined with text labels + +### Color Palette +- **Primary**: Blue (#3B82F6) - Trust, learning +- **Secondary**: Green (#10B981) - Success, validation +- **Accent**: Orange (#F59E0B) - Energy, attention +- **Error**: Red (#EF4444) - Clear error indication +- **Neutral**: Gray (#6B7280) - Text, backgrounds + +### Accessibility Features +- Full keyboard navigation support +- Alternative text for all images +- Adjustable font sizes +- High contrast mode compatibility +- Screen reader friendly markup + +### Responsive Design +- Mobile/tablet adaptation +- Touch-friendly interface +- Portrait/landscape orientation support +- Fluid layouts that work on various screen sizes \ No newline at end of file diff --git a/config/games-config.json b/config/games-config.json new file mode 100644 index 0000000..1b33ea0 --- /dev/null +++ b/config/games-config.json @@ -0,0 +1,191 @@ +{ + "games": { + "whack-a-mole": { + "enabled": true, + "name": "Whack-a-Mole", + "icon": "🔨", + "description": "Tape sur les bonnes réponses !", + "difficulty": "easy", + "minAge": 8, + "maxAge": 12, + "estimatedTime": 5 + }, + "memory-game": { + "enabled": false, + "name": "Memory Game", + "icon": "🧠", + "description": "Trouve les paires !", + "difficulty": "medium", + "minAge": 8, + "maxAge": 14, + "estimatedTime": 7 + }, + "quiz-game": { + "enabled": false, + "name": "Quiz Game", + "icon": "❓", + "description": "Réponds aux questions !", + "difficulty": "medium", + "minAge": 9, + "maxAge": 15, + "estimatedTime": 10 + }, + "temp-games": { + "enabled": true, + "name": "Mini-Jeux", + "icon": "🎯", + "description": "Jeux temporaires en développement", + "difficulty": "easy", + "minAge": 8, + "maxAge": 12, + "estimatedTime": 3 + }, + "story-builder": { + "enabled": true, + "name": "Story Builder", + "icon": "📖", + "description": "Construis des histoires en anglais !", + "difficulty": "medium", + "minAge": 8, + "maxAge": 14, + "estimatedTime": 10 + }, + "chinese-study": { + "enabled": true, + "name": "Chinese Study", + "icon": "🇨🇳", + "description": "Learn Chinese characters, pinyin and vocabulary", + "difficulty": "medium", + "minAge": 12, + "maxAge": 99, + "estimatedTime": 15 + } + }, + "content": { + "sbs-level-8": { + "enabled": true, + "name": "SBS Level 8", + "icon": "📚", + "description": "Vocabulaire du manuel SBS Level 8", + "difficulty": "intermediate", + "vocabulary_count": 25, + "topics": ["family", "daily_activities", "school_objects"] + }, + "animals": { + "enabled": false, + "name": "Animals", + "icon": "🐱", + "description": "Vocabulaire des animaux", + "difficulty": "easy", + "vocabulary_count": 20, + "topics": ["pets", "farm_animals", "wild_animals"] + }, + "colors": { + "enabled": false, + "name": "Colors & Numbers", + "icon": "🌈", + "description": "Couleurs et nombres", + "difficulty": "easy", + "vocabulary_count": 15, + "topics": ["basic_colors", "numbers_1_to_20"] + }, + "family": { + "enabled": false, + "name": "Family Members", + "icon": "👨‍👩‍👧‍👦", + "description": "Membres de la famille", + "difficulty": "easy", + "vocabulary_count": 12, + "topics": ["immediate_family", "extended_family"] + }, + "food": { + "enabled": false, + "name": "Food & Drinks", + "icon": "🍎", + "description": "Nourriture et boissons", + "difficulty": "easy", + "vocabulary_count": 18, + "topics": ["fruits", "vegetables", "drinks", "meals"] + }, + "house": { + "enabled": false, + "name": "House & Furniture", + "icon": "🏠", + "description": "Maison et mobilier", + "difficulty": "medium", + "vocabulary_count": 22, + "topics": ["rooms", "furniture", "household_objects"] + }, + "demo-flexible": { + "enabled": true, + "name": "Demo Architecture", + "icon": "🎯", + "description": "Démonstration de la nouvelle architecture flexible", + "difficulty": "mixed", + "vocabulary_count": 12, + "topics": ["vocabulary", "sentences", "dialogues", "sequences"] + }, + "sbs-level-7-8": { + "enabled": true, + "name": "SBS Level 7-8", + "icon": "🌍", + "description": "Around the World - Homes, Clothing & Cultures", + "difficulty": "intermediate", + "vocabulary_count": 85, + "topics": ["homes", "clothing", "neighborhoods", "grammar", "culture"] + }, + "basic-chinese": { + "enabled": true, + "name": "Basic Chinese", + "icon": "🇨🇳", + "description": "Essential Chinese characters, pinyin and vocabulary", + "difficulty": "beginner", + "vocabulary_count": 25, + "topics": ["greetings", "numbers", "family", "basic_characters"] + }, + "english-class-demo": { + "enabled": true, + "name": "English Class Demo", + "icon": "🇬🇧", + "description": "Complete rich content example with all exercise types", + "difficulty": "mixed", + "vocabulary_count": 50, + "topics": ["vocabulary", "grammar", "audio", "poems", "exercises", "ai_evaluation"] + } + }, + "settings": { + "defaultDifficulty": "easy", + "soundEnabled": true, + "animationsEnabled": true, + "autoSave": true, + "showHints": true, + "timeLimit": 300, + "maxErrors": 5, + "pointsPerCorrect": 10, + "pointsPerError": -2 + }, + "ui": { + "theme": "default", + "language": "fr", + "fontSize": "medium", + "animations": { + "enabled": true, + "speed": "normal" + }, + "sounds": { + "enabled": true, + "volume": 0.5, + "effects": ["click", "success", "error", "complete"] + } + }, + "features": { + "statistics": false, + "userProfiles": false, + "achievements": false, + "multiplayer": false, + "contentEditor": false, + "exportImport": false + }, + "version": "1.0.0", + "lastUpdated": "2024-01-15" +} \ No newline at end of file diff --git a/css/games.css b/css/games.css new file mode 100644 index 0000000..134bdd3 --- /dev/null +++ b/css/games.css @@ -0,0 +1,1818 @@ +/* === GAME PAGE LAYOUT === */ +.game-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 30px; + padding: 20px; + background: var(--card-background); + border-radius: var(--border-radius); + box-shadow: var(--shadow); +} + +.game-header h3 { + color: var(--primary-color); + font-size: 1.5rem; + font-weight: 600; + margin: 0; +} + +.score-display { + background: var(--secondary-color); + color: white; + padding: 8px 16px; + border-radius: 20px; + font-weight: 600; + font-size: 1.1rem; +} + +/* === GAME CONTAINER === */ +.game-container { + background: var(--card-background); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + padding: 30px; + min-height: 500px; + position: relative; + overflow: hidden; +} + +/* === GAME ERROR DISPLAY === */ +.game-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + min-height: 400px; + text-align: center; + padding: 40px 20px; + color: var(--text-secondary); +} + +/* === MEMORY MATCH GAME === */ +.memory-match-wrapper { + display: flex; + flex-direction: column; + gap: 20px; + max-width: 600px; + margin: 0 auto; +} + +.game-stats { + display: flex; + justify-content: space-around; + background: var(--card-background); + padding: 15px; + border-radius: var(--border-radius); + box-shadow: var(--shadow-light); +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; +} + +.stat-label { + font-size: 0.9rem; + color: var(--text-secondary); + font-weight: 500; +} + +.stat-item span:last-child { + font-size: 1.2rem; + font-weight: 600; + color: var(--primary-color); +} + +.memory-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 15px; + padding: 20px; + background: var(--background); + border-radius: var(--border-radius); +} + +.memory-card { + aspect-ratio: 1; + perspective: 1000px; + cursor: pointer; + border-radius: 8px; + transition: transform 0.2s ease; +} + +.memory-card:hover { + transform: scale(1.05); +} + +.card-inner { + position: relative; + width: 100%; + height: 100%; + transform-style: preserve-3d; + transition: transform 0.6s ease; + border-radius: 8px; +} + +.memory-card.flipped .card-inner { + transform: rotateY(180deg); +} + +.card-front, +.card-back { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: var(--shadow); + border: 2px solid transparent; +} + +.card-front { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + color: white; +} + +.card-back { + background: var(--card-background); + color: var(--text-primary); + transform: rotateY(180deg); + padding: 10px; +} + +.card-icon { + font-size: 2rem; +} + +.card-content { + font-size: 1rem; + font-weight: 600; + text-align: center; + word-break: break-word; + line-height: 1.2; +} + +.card-type { + font-size: 1.2rem; + margin-top: 5px; +} + +.memory-card.matched .card-back { + background: var(--success-color); + color: white; + border-color: var(--success-color); +} + +.memory-card.hint .card-inner { + transform: rotateY(180deg); + animation: hint-pulse 0.5s ease-in-out; +} + +@keyframes hint-pulse { + 0%, 100% { transform: rotateY(180deg) scale(1); } + 50% { transform: rotateY(180deg) scale(1.1); } +} + +/* === ANIMATIONS DE RÉUSSITE === */ +@keyframes success-match { + 0% { transform: rotateY(180deg) scale(1); } + 25% { transform: rotateY(180deg) scale(1.15); } + 50% { transform: rotateY(180deg) scale(1.05); } + 75% { transform: rotateY(180deg) scale(1.1); } + 100% { transform: rotateY(180deg) scale(1); } +} + +@keyframes success-glow { + 0% { box-shadow: 0 0 5px #22C55E; } + 50% { box-shadow: 0 0 20px #22C55E, 0 0 30px #22C55E; } + 100% { box-shadow: 0 0 5px #22C55E; } +} + +@keyframes success-sparkle { + 0% { opacity: 0; transform: scale(0) rotate(0deg); } + 50% { opacity: 1; transform: scale(1) rotate(180deg); } + 100% { opacity: 0; transform: scale(0) rotate(360deg); } +} + +.memory-card.success-animation .card-inner { + animation: success-match 0.8s ease-in-out; +} + +.memory-card.success-animation .card-back { + animation: success-glow 1s ease-in-out; +} + +/* Particules de réussite */ +.success-particle { + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + background: #22C55E; + border-radius: 50%; + pointer-events: none; + z-index: 100; + animation: success-sparkle 1.2s ease-out; +} + +.success-particle::before { + content: '✨'; + position: absolute; + top: -5px; + left: -5px; + font-size: 20px; + animation: success-sparkle 1s ease-out; +} + +.success-particle.particle-1 { animation-delay: 0s; transform: translate(-20px, -20px); } +.success-particle.particle-2 { animation-delay: 0.1s; transform: translate(20px, -20px); } +.success-particle.particle-3 { animation-delay: 0.2s; transform: translate(-20px, 20px); } +.success-particle.particle-4 { animation-delay: 0.3s; transform: translate(20px, 20px); } + +.game-controls { + display: flex; + gap: 15px; + justify-content: center; +} + +/* Responsive design */ +@media (max-width: 600px) { + .memory-grid { + gap: 10px; + padding: 15px; + } + + .card-content { + font-size: 0.9rem; + } + + .game-stats { + flex-direction: column; + gap: 15px; + } +} + +/* === QUIZ GAME === */ +.quiz-game-wrapper { + display: flex; + flex-direction: column; + gap: 25px; + max-width: 700px; + margin: 0 auto; +} + +.quiz-progress { + background: var(--card-background); + padding: 20px; + border-radius: var(--border-radius); + box-shadow: var(--shadow-light); +} + +.progress-bar { + width: 100%; + height: 8px; + background: var(--background); + border-radius: 4px; + overflow: hidden; + margin-bottom: 15px; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + border-radius: 4px; + transition: width 0.3s ease; + width: 10%; +} + +.progress-text { + display: flex; + justify-content: space-between; + font-weight: 600; + color: var(--text-primary); +} + +.question-area { + background: var(--card-background); + padding: 30px; + border-radius: var(--border-radius); + box-shadow: var(--shadow); + text-align: center; +} + +.question-text { + font-size: 1.4rem; + color: var(--text-primary); + line-height: 1.5; + margin: 0; +} + +.question-text strong { + color: var(--primary-color); + font-size: 1.6rem; +} + +.options-area { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + padding: 20px; +} + +.quiz-option { + background: var(--card-background); + border: 2px solid var(--border-color); + border-radius: var(--border-radius); + padding: 20px; + font-size: 1.1rem; + font-weight: 500; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s ease; + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.quiz-option:hover:not(:disabled) { + border-color: var(--primary-color); + background: var(--primary-light); + transform: translateY(-2px); + box-shadow: var(--shadow); +} + +.quiz-option:disabled { + cursor: not-allowed; +} + +.quiz-option.correct { + background: var(--success-color); + border-color: var(--success-color); + color: white; + animation: success-pulse 0.6s ease; +} + +.quiz-option.wrong { + background: var(--error-color); + border-color: var(--error-color); + color: white; + animation: error-shake 0.6s ease; +} + +.quiz-option.disabled { + opacity: 0.5; + background: var(--text-secondary); + border-color: var(--text-secondary); +} + +@keyframes success-pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +@keyframes error-shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +.quiz-controls { + display: flex; + gap: 15px; + justify-content: center; + align-items: center; +} + +#next-btn { + font-size: 1.1rem; + padding: 12px 24px; +} + +/* Responsive design */ +@media (max-width: 600px) { + .options-area { + grid-template-columns: 1fr; + gap: 12px; + padding: 15px; + } + + .quiz-option { + padding: 15px; + font-size: 1rem; + min-height: 50px; + } + + .question-text { + font-size: 1.2rem; + } + + .question-text strong { + font-size: 1.4rem; + } + + .progress-text { + flex-direction: column; + gap: 8px; + text-align: center; + } +} + +/* === ADVENTURE READER GAME === */ +.adventure-reader-wrapper { + display: flex; + flex-direction: column; + gap: 15px; + max-width: 1000px; + margin: 0 auto; + user-select: none; +} + +.game-hud { + display: flex; + justify-content: space-between; + align-items: center; + background: var(--card-background); + padding: 15px 20px; + border-radius: var(--border-radius); + box-shadow: var(--shadow-light); + border: 2px solid var(--primary-color); +} + +.hud-left { + display: flex; + gap: 20px; +} + +.stat-item { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 1.1rem; +} + +.stat-icon { + font-size: 1.3rem; +} + +.progress-info { + font-weight: 500; + color: var(--primary-color); +} + +.game-map { + position: relative; + width: 70vw; + height: 55vh; + max-width: 800px; + max-height: 500px; + min-width: 400px; + min-height: 300px; + background: linear-gradient(135deg, #8fbc8f, #90ee90); + border-radius: var(--border-radius); + border: 3px solid var(--primary-color); + overflow: hidden; + cursor: crosshair; + margin: 0 auto; +} + +.player { + position: absolute; + width: 40px; + height: 40px; + font-size: 30px; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.3)); + transition: all 0.8s ease; +} + +.pot, .enemy { + position: absolute; + width: 40px; + height: 40px; + font-size: 30px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3)); +} + +/* Invisible larger clickable area */ +.pot::before, .enemy::before { + content: ''; + position: absolute; + top: -15px; + left: -15px; + right: -15px; + bottom: -15px; + border-radius: 50%; + background: transparent; + pointer-events: auto; +} + +.pot:hover, .enemy:hover { + transform: scale(1.2); + filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.5)); +} + +.pot.destroyed, .enemy.defeated { + pointer-events: none; + animation: destruction 0.5s ease; +} + +@keyframes destruction { + 0% { transform: scale(1); } + 50% { transform: scale(1.5) rotate(180deg); } + 100% { transform: scale(0.8); } +} + +.enemy { + animation: enemyFloat 2s ease-in-out infinite alternate; +} + +@keyframes enemyFloat { + 0% { transform: translateY(0px); } + 100% { transform: translateY(-5px); } +} + +.game-controls { + text-align: center; + background: var(--card-background); + padding: 15px; + border-radius: var(--border-radius); + box-shadow: var(--shadow-light); +} + +.instructions { + margin-bottom: 15px; + color: var(--text-secondary); + font-style: italic; +} + +/* Reading Modal */ +.reading-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; +} + +.reading-modal.show { + opacity: 1; +} + +.modal-content { + background: var(--card-background); + border-radius: var(--border-radius); + max-width: 500px; + width: 90%; + max-height: 80%; + overflow-y: auto; + transform: translateY(20px); + transition: transform 0.3s ease; +} + +.reading-modal.show .modal-content { + transform: translateY(0); +} + +.modal-header { + background: var(--primary-color); + color: white; + padding: 20px; + border-radius: var(--border-radius) var(--border-radius) 0 0; + text-align: center; +} + +.modal-header h3 { + margin: 0; + font-size: 1.5rem; +} + +.modal-body { + padding: 30px; +} + +.sentence-content .english-text { + font-size: 1.3rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 15px; + line-height: 1.5; +} + +.sentence-content .translation-text { + font-size: 1.1rem; + color: var(--text-secondary); + font-style: italic; + line-height: 1.4; +} + +.modal-footer { + padding: 20px; + text-align: center; + background: var(--background); + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +/* Vocab Popup */ +.vocab-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.8); + background: var(--success-color); + color: white; + padding: 20px 30px; + border-radius: var(--border-radius); + z-index: 500; + opacity: 0; + transition: all 0.3s ease; + box-shadow: var(--shadow-lg); +} + +.vocab-popup.show { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +.popup-content { + text-align: center; +} + +.vocab-word { + font-size: 1.4rem; + font-weight: 600; + margin-bottom: 8px; +} + +.vocab-translation { + font-size: 1.1rem; + opacity: 0.9; +} + +/* Responsive design */ +@media (max-width: 600px) { + .game-map { + width: 95vw; + height: 50vh; + max-width: none; + } + + .hud-left { + flex-direction: column; + gap: 10px; + } + + .game-hud { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .pot, .enemy, .player { + width: 35px; + height: 35px; + font-size: 25px; + } + + .modal-content { + width: 95%; + } + + .modal-body { + padding: 20px; + } +} + +/* === DECORATIVE ELEMENTS === */ +.decoration { + position: absolute; + pointer-events: none; + z-index: 1; + opacity: 0.8; + user-select: none; +} + +.decoration.tree { + z-index: 5; + filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.2)); +} + +.decoration.grass { + z-index: 1; + opacity: 0.6; + animation: grassSway 3s ease-in-out infinite; +} + +.decoration.rock { + z-index: 2; + opacity: 0.7; + filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3)); +} + +@keyframes grassSway { + 0%, 100% { transform: rotate(-2deg); } + 50% { transform: rotate(2deg); } +} + +/* Player and interactive elements should be above decorations */ +.player { + z-index: 10 !important; +} + +.pot, .enemy { + z-index: 8 !important; +} + +/* Damage animation */ +@keyframes damageFloat { + 0% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(-50%, -80px) scale(1.2); + } +} + +/* Protection animation */ +@keyframes protectionFloat { + 0% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 50% { + opacity: 0.7; + transform: translate(-50%, -40px) scale(1.1); + } + 100% { + opacity: 0; + transform: translate(-50%, -60px) scale(0.9); + } +} + +.game-error h3 { + color: var(--error-color); + font-size: 1.8rem; + margin-bottom: 20px; + font-weight: 600; +} + +.game-error p { + font-size: 1.1rem; + line-height: 1.6; + margin-bottom: 15px; + max-width: 500px; +} + +.game-error .back-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 12px 24px; + border-radius: 25px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + margin-top: 20px; + transition: all 0.3s ease; +} + +.game-error .back-btn:hover { + background: #2563EB; + transform: translateY(-1px); +} + +/* === FILL THE BLANK SPECIFIC STYLES === */ +.fill-blank-wrapper { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +.translation-hint { + background: #f0f9ff; + border: 2px solid #3b82f6; + border-radius: 12px; + padding: 15px; + margin-bottom: 20px; + text-align: center; + font-size: 1.1rem; + color: #1e40af; +} + +.translation-hint:empty { + display: none; +} + +.sentence-container { + background: white; + border: 3px solid #e5e7eb; + border-radius: 15px; + padding: 25px; + margin-bottom: 25px; + font-size: 1.4rem; + line-height: 1.8; + text-align: center; + min-height: 80px; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 8px; +} + +.word { + color: var(--text-primary); + font-weight: 500; +} + +.blank-wrapper { + display: inline-flex; + align-items: center; + gap: 2px; +} + +.blank-input { + background: #fef3c7; + border: 2px solid #f59e0b; + border-radius: 8px; + padding: 8px 12px; + font-size: 1.3rem; + font-weight: 600; + text-align: center; + min-width: 60px; + width: auto; + color: var(--text-primary); + transition: all 0.3s ease; +} + +.blank-input:focus { + outline: none; + border-color: var(--primary-color); + background: #dbeafe; + transform: scale(1.05); +} + +.blank-input.correct { + background: #d1fae5; + border-color: #10b981; + color: #065f46; +} + +.blank-input.incorrect { + background: #fee2e2; + border-color: #ef4444; + color: #991b1b; + animation: shake 0.5s ease-in-out; +} + +.blank-input.revealed { + background: #f3f4f6; + border-color: #6b7280; + color: #374151; + font-style: italic; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } +} + +.input-area { + margin-bottom: 25px; + min-height: 50px; +} + +.game-controls { + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 20px; +} + +.control-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 12px 24px; + border-radius: 25px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + min-width: 120px; +} + +.control-btn:hover { + background: #2563EB; + transform: translateY(-2px); +} + +.control-btn.secondary { + background: #6b7280; +} + +.control-btn.secondary:hover { + background: #4b5563; +} + +.instruction.success { + background: #d1fae5; + color: #065f46; + border: 2px solid #10b981; +} + +.instruction.partial { + background: #fef3c7; + color: #92400e; + border: 2px solid #f59e0b; +} + +.instruction.error { + background: #fee2e2; + color: #991b1b; + border: 2px solid #ef4444; +} + +.instruction.info { + background: #dbeafe; + color: #1e40af; + border: 2px solid #3b82f6; +} + +/* Responsive pour Fill the Blank */ +@media (max-width: 768px) { + .fill-blank-wrapper { + padding: 15px; + } + + .sentence-container { + font-size: 1.2rem; + padding: 20px 15px; + } + + .blank-input { + font-size: 1.1rem; + padding: 6px 10px; + min-width: 50px; + } + + .game-controls { + flex-direction: column; + align-items: center; + } + + .control-btn { + width: 100%; + max-width: 200px; + } +} + +/* === WHACK-A-MOLE SPECIFIC STYLES === */ +.whack-game-board { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + max-width: 600px; + margin: 0 auto; + padding: 20px 0; +} + +/* === TEXT READER STYLES === */ +.text-reader-wrapper { + max-width: 900px; + margin: 0 auto; + padding: 20px; +} + +.text-selection { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 25px; + border-radius: 15px; + text-align: center; + margin-bottom: 30px; + box-shadow: 0 8px 32px rgba(0,0,0,0.1); +} + +.text-selector-label { + display: block; + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 15px; + text-shadow: 0 2px 4px rgba(0,0,0,0.3); +} + +.text-selector { + background: white; + color: var(--text-primary); + border: none; + padding: 12px 20px; + border-radius: 10px; + font-size: 1.1rem; + font-weight: 500; + margin-bottom: 15px; + width: 100%; + max-width: 400px; + cursor: pointer; + box-shadow: 0 4px 15px rgba(0,0,0,0.2); + transition: all 0.3s ease; +} + +.text-selector:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(255,255,255,0.3); + transform: translateY(-1px); +} + +.text-selector:hover { + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(0,0,0,0.25); +} + +.text-progress { + font-size: 1rem; + opacity: 0.9; + font-weight: 500; +} + +.reading-area { + background: white; + border: 3px solid #e5e7eb; + border-radius: 20px; + padding: 40px; + margin-bottom: 30px; + min-height: 200px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 20px rgba(0,0,0,0.08); +} + +.sentence-display { + text-align: center; + width: 100%; +} + +.current-sentence { + font-size: 1.6rem; + line-height: 1.8; + color: var(--text-primary); + font-weight: 500; + animation: fadeInUp 0.5s ease-out; +} + +.full-text-display { + width: 100%; + text-align: left; +} + +.full-text-content { + font-size: 1.3rem; + line-height: 1.8; + color: var(--text-primary); +} + +.full-text-content p { + margin-bottom: 20px; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.reader-controls, .text-navigation { + display: flex; + justify-content: center; + gap: 20px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.reader-controls .control-btn, +.text-navigation .control-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 12px 24px; + border-radius: 25px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + min-width: 140px; +} + +.reader-controls .control-btn:hover, +.text-navigation .control-btn:hover { + background: #2563EB; + transform: translateY(-2px); +} + +.reader-controls .control-btn.secondary, +.text-navigation .control-btn.secondary { + background: #6b7280; +} + +.reader-controls .control-btn.secondary:hover, +.text-navigation .control-btn.secondary:hover { + background: #4b5563; +} + +.reader-controls .control-btn:disabled, +.text-navigation .control-btn:disabled { + background: #d1d5db; + color: #9ca3af; + cursor: not-allowed; + transform: none; +} + +.reader-controls .control-btn:disabled:hover, +.text-navigation .control-btn:disabled:hover { + background: #d1d5db; + transform: none; +} + +/* Responsive pour Text Reader */ +@media (max-width: 768px) { + .text-reader-wrapper { + padding: 15px; + } + + .text-title { + font-size: 1.4rem; + } + + .reading-area { + padding: 25px 20px; + min-height: 150px; + } + + .current-sentence { + font-size: 1.3rem; + } + + .full-text-content { + font-size: 1.1rem; + } + + .reader-controls, .text-navigation { + flex-direction: column; + align-items: center; + gap: 10px; + } + + .reader-controls .control-btn, + .text-navigation .control-btn { + width: 100%; + max-width: 250px; + } +} + +@media (max-width: 480px) { + .text-info { + padding: 15px; + } + + .text-progress { + font-size: 0.8rem; + } + + .current-sentence { + font-size: 1.2rem; + } + + .reading-area { + padding: 20px 15px; + } +} + +/* === WHACK-A-MOLE HARD MODE STYLES === */ +.whack-game-board.hard-mode { + grid-template-columns: repeat(5, 1fr); + max-width: 800px; + gap: 15px; +} + +.whack-hole { + aspect-ratio: 1; + background: radial-gradient(circle, #8B4513 0%, #654321 70%, #3D2817 100%); + border-radius: 50%; + position: relative; + cursor: pointer; + transition: var(--transition); + box-shadow: inset 0 4px 8px rgba(0,0,0,0.3); + border: 4px solid #654321; +} + +.whack-hole:hover { + transform: scale(1.05); +} + +.whack-mole { + position: absolute; + bottom: -20px; + left: 50%; + transform: translateX(-50%); + width: 80%; + height: 80%; + background: #8B4513; + border-radius: 50% 50% 40% 40%; + transition: all 0.3s ease-out; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + +.whack-mole.active { + bottom: 10px; + animation: pop-up 0.3s ease-out; +} + +.whack-mole.hit { + animation: hit-animation 0.5s ease-out; +} + +.whack-mole .word { + color: white; + font-weight: bold; + font-size: 0.9rem; + text-align: center; + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); + padding: 5px; +} + +@keyframes pop-up { + 0% { bottom: -20px; transform: translateX(-50%) scale(0.8); } + 50% { transform: translateX(-50%) scale(1.1); } + 100% { bottom: 10px; transform: translateX(-50%) scale(1); } +} + +@keyframes hit-animation { + 0% { transform: translateX(-50%) scale(1); } + 25% { transform: translateX(-50%) scale(1.2) rotate(5deg); } + 50% { transform: translateX(-50%) scale(0.9) rotate(-3deg); } + 75% { transform: translateX(-50%) scale(1.1) rotate(2deg); } + 100% { transform: translateX(-50%) scale(1) rotate(0deg); } +} + +/* === GAME UI ELEMENTS === */ +.game-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 15px 20px; + background: linear-gradient(135deg, #f8fafc, #e2e8f0); + border-radius: var(--border-radius); +} + +.game-stats { + display: flex; + gap: 20px; +} + +.stat-item { + text-align: center; +} + +.stat-value { + display: block; + font-size: 1.5rem; + font-weight: bold; + color: var(--primary-color); +} + +.stat-label { + font-size: 0.9rem; + color: var(--text-secondary); +} + +.game-controls { + display: flex; + gap: 10px; +} + +.control-btn { + background: var(--primary-color); + color: white; + padding: 10px 20px; + border-radius: 25px; + font-size: 0.9rem; + transition: var(--transition); +} + +.control-btn:hover { + background: #2563EB; + transform: translateY(-2px); +} + +.control-btn:disabled { + background: var(--neutral-color); + cursor: not-allowed; + transform: none; +} + +/* === FEEDBACK ANIMATIONS === */ +.score-popup { + position: absolute; + font-weight: bold; + font-size: 1.2rem; + color: var(--secondary-color); + pointer-events: none; + animation: score-float 1s ease-out forwards; + z-index: 10; +} + +@keyframes score-float { + 0% { + opacity: 1; + transform: translateY(0) scale(1); + } + 100% { + opacity: 0; + transform: translateY(-50px) scale(1.2); + } +} + +.wrong-answer { + color: var(--error-color) !important; +} + +.correct-answer { + color: var(--secondary-color) !important; +} + +/* === GAME MODES === */ +.mode-selector { + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 20px; + padding: 15px; + background: var(--background-color); + border-radius: var(--border-radius); +} + +.mode-btn { + background: white; + border: 2px solid var(--neutral-color); + color: var(--text-primary); + padding: 10px 20px; + border-radius: 25px; + font-size: 0.9rem; + transition: var(--transition); +} + +.mode-btn.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +.mode-btn:hover { + border-color: var(--primary-color); +} + +/* === RESPONSIVE GAMES === */ +@media (max-width: 768px) { + .game-header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .game-container { + padding: 20px; + min-height: 400px; + } + + .whack-game-board { + grid-template-columns: repeat(2, 1fr); + gap: 15px; + max-width: 400px; + } + + .game-info { + flex-direction: column; + gap: 15px; + } + + .game-stats { + justify-content: center; + } + + .mode-selector { + flex-wrap: wrap; + gap: 10px; + } +} + +/* === GAME END OVERLAY === */ +.game-end-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.3s ease-out; +} + +.game-end-modal { + background: white; + border-radius: var(--border-radius); + padding: 40px; + text-align: center; + max-width: 400px; + margin: 20px; + box-shadow: var(--shadow-lg); + animation: slideUp 0.3s ease-out; +} + +.game-end-modal h3 { + color: var(--primary-color); + font-size: 2rem; + margin-bottom: 20px; +} + +.final-score { + font-size: 1.5rem; + color: var(--secondary-color); + margin-bottom: 10px; +} + +.best-score { + font-size: 1.1rem; + color: var(--text-secondary); + margin-bottom: 30px; +} + +.game-end-buttons { + display: flex; + flex-direction: column; + gap: 12px; +} + +.restart-game-btn, .back-to-levels-btn, .back-to-games-btn { + padding: 12px 20px; + border: none; + border-radius: 25px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition); +} + +.restart-game-btn { + background: var(--secondary-color); + color: white; +} + +.restart-game-btn:hover { + background: #059669; + transform: translateY(-2px); +} + +.back-to-levels-btn { + background: var(--primary-color); + color: white; +} + +.back-to-levels-btn:hover { + background: #2563EB; + transform: translateY(-2px); +} + +.back-to-games-btn { + background: var(--neutral-color); + color: white; +} + +.back-to-games-btn:hover { + background: #4B5563; + transform: translateY(-2px); +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 480px) { + .whack-game-board { + grid-template-columns: 1fr 1fr; + gap: 10px; + max-width: 300px; + } + + .whack-mole .word { + font-size: 0.8rem; + } + + .game-header h3 { + font-size: 1.2rem; + } + + .score-display { + font-size: 1rem; + padding: 6px 12px; + } + + .game-end-modal { + padding: 30px 20px; + margin: 15px; + } + + .game-end-modal h3 { + font-size: 1.5rem; + } + + .game-end-buttons { + gap: 10px; + } +} + +/* === OPTIMISATION POUR ÉCRANS LARGES 1920x1080+ === */ +@media (min-width: 1440px) { + /* Memory Match - laisser le CSS original fonctionner */ + + /* Autres jeux en mode horizontal */ + .quiz-game-wrapper, + .fill-blank-wrapper, + .text-reader-wrapper { + display: flex; + justify-content: center; + gap: 40px; + max-width: 1400px; + margin: 0 auto; + align-items: flex-start; + } + + /* Whack-a-mole en layout horizontal simple */ + .game-container:has(.whack-game-board) { + display: flex; + justify-content: center; + gap: 40px; + align-items: flex-start; + } + + /* Board du whack-a-mole centré */ + .whack-game-board { + flex: 0 0 auto; + order: 1; + } + + /* Infos whack-a-mole sur le côté */ + .game-container:has(.whack-game-board) .game-info { + flex: 0 0 300px; + order: 2; + margin: 0; + position: sticky; + top: 80px; + } + + /* Zone principale du jeu à gauche */ + .game-main-area { + min-width: 0; /* Pour éviter overflow */ + } + + /* Panneau latéral à droite */ + .game-sidebar { + background: var(--card-background); + border-radius: var(--border-radius); + padding: 25px; + box-shadow: var(--shadow); + position: sticky; + top: 80px; + max-height: calc(100vh - 100px); + overflow-y: auto; + } + + /* Memory Match - juste augmenter la grille pour 1920x1080 */ + .memory-match-wrapper { + max-width: 900px; + } + + .memory-grid { + gap: 20px; + padding: 25px; + } + + /* Forcer la visibilité des cartes */ + .memory-card { + min-width: 100px !important; + min-height: 100px !important; + width: auto !important; + height: auto !important; + } + + .card-front, + .card-back { + border: 2px solid #e5e7eb !important; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1) !important; + background: white !important; + } + + .card-front { + background: linear-gradient(135deg, #3B82F6, #10B981) !important; + } + + /* Quiz - Options en 2x2 */ + .options-area { + grid-template-columns: repeat(2, 1fr); + gap: 20px; + padding: 30px; + } + + .quiz-option { + padding: 25px; + font-size: 1.3rem; + min-height: 80px; + } + + /* Questions plus grandes */ + .question-text { + font-size: 1.8rem; + line-height: 1.6; + } + + /* Fill the Blank - Phrase plus grande */ + .sentence-container { + font-size: 1.6rem; + line-height: 2; + padding: 40px; + min-height: 120px; + } + + .blank-input { + font-size: 1.5rem; + padding: 10px 15px; + min-width: 80px; + } + + /* Text Reader - Zone de lecture plus large */ + .current-sentence { + font-size: 2rem; + line-height: 2.2; + } + + .reading-area { + padding: 50px; + min-height: 250px; + } + + /* Adventure Reader - Carte plus grande */ + .adventure-reader-wrapper { + max-width: 1400px; + flex-direction: row; + gap: 30px; + } + + .game-map { + width: calc(100vw - 450px); + height: 70vh; + max-width: 1000px; + max-height: 700px; + } + + .adventure-sidebar { + min-width: 350px; + display: flex; + flex-direction: column; + gap: 20px; + } + + /* Whack-a-mole - Trous plus petits pour mieux voir l'ensemble */ + .whack-hole { + max-width: 100px; + max-height: 100px; + } + + .whack-mole .word { + font-size: 0.8rem; + } + + .whack-game-board { + max-width: 500px; + gap: 15px; + flex: 0 0 auto; + } + + .whack-game-board.hard-mode { + max-width: 650px; + gap: 12px; + } + + /* Game header plus compact horizontalement */ + .game-header { + padding: 20px 40px; + margin-bottom: 20px; + } + + .game-header h3 { + font-size: 1.8rem; + } + + .score-display { + font-size: 1.3rem; + padding: 10px 20px; + } +} + +/* Layout spécifique pour les wrappers de jeux */ +@media (min-width: 1440px) { + /* Headers restent en haut, pleine largeur */ + .memory-match-wrapper, + .quiz-game-wrapper, + .fill-blank-wrapper, + .text-reader-wrapper { + flex-direction: column; + } + + .memory-match-wrapper > .game-main-content, + .quiz-game-wrapper > .game-main-content, + .fill-blank-wrapper > .game-main-content, + .text-reader-wrapper > .game-main-content { + display: flex; + flex-direction: row; + justify-content: center; + gap: 40px; + width: 100%; + } + + /* Zone principale du jeu - centrée */ + .game-main-content { + flex: 0 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + } + + /* Sidebar pour les infos - à droite */ + .game-sidebar-info { + flex: 0 0 300px; + display: flex; + flex-direction: column; + gap: 20px; + } + + /* Regroupement des éléments de jeu */ + .memory-grid, + .question-area, + .options-area, + .sentence-container, + .reading-area, + .whack-game-board { + margin: 0 auto; + } +} \ No newline at end of file diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..7c3a306 --- /dev/null +++ b/css/main.css @@ -0,0 +1,402 @@ +/* === VARIABLES CSS === */ +:root { + --primary-color: #3B82F6; + --secondary-color: #10B981; + --accent-color: #F59E0B; + --error-color: #EF4444; + --success-color: #22C55E; + --neutral-color: #6B7280; + --background-color: #F8FAFC; + --background: #F8FAFC; + --card-background: #FFFFFF; + --text-primary: #1F2937; + --text-secondary: #6B7280; + --border-color: #E5E7EB; + --primary-light: #EBF4FF; + --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.05); + --border-radius: 12px; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --transition: all 0.3s ease; +} + +/* === RESET ET BASE === */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + font-size: 16px; + line-height: 1.6; + color: var(--text-primary); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + overflow-x: hidden; +} + +/* === LAYOUT PRINCIPAL === */ +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; + min-height: 100vh; + position: relative; +} + +/* Optimisation pour écrans larges */ +@media (min-width: 1440px) { + .container { + max-width: 1600px; + padding: 20px 40px; + } +} + +/* === PAGES === */ +.page { + display: none; + animation: fadeIn 0.4s ease-in-out; + background: var(--background-color); + border-radius: var(--border-radius); + box-shadow: var(--shadow-lg); + padding: 30px; + margin-top: 20px; +} + +.page.active { + display: block; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +/* === HERO SECTION === */ +.hero { + text-align: center; + margin-bottom: 40px; + padding: 40px 20px; +} + +.hero h1 { + font-size: 3rem; + font-weight: 700; + color: var(--primary-color); + margin-bottom: 15px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.1); +} + +.hero p { + font-size: 1.3rem; + color: var(--text-secondary); + font-weight: 300; +} + +/* === PAGE HEADERS === */ +.page-header { + text-align: center; + margin-bottom: 40px; +} + +.page-header h2 { + font-size: 2.5rem; + color: var(--primary-color); + margin-bottom: 10px; +} + +.page-header p { + font-size: 1.1rem; + color: var(--text-secondary); +} + +/* === CARDS SYSTEM === */ +.main-options { + display: grid; + gap: 20px; + max-width: 600px; + margin: 0 auto; +} + +.cards-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 25px; + margin-top: 30px; +} + +.option-card, .game-card, .level-card { + background: var(--card-background); + border: 3px solid transparent; + border-radius: var(--border-radius); + padding: 30px 25px; + text-align: center; + cursor: pointer; + transition: var(--transition); + box-shadow: var(--shadow); + position: relative; + overflow: hidden; +} + +.option-card:hover, .game-card:hover, .level-card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); + border-color: var(--primary-color); +} + +.option-card.primary { + background: linear-gradient(135deg, var(--primary-color), #2563EB); + color: white; + font-size: 1.2rem; + font-weight: 600; +} + +.option-card.primary:hover { + background: linear-gradient(135deg, #2563EB, var(--primary-color)); + border-color: white; +} + +.option-card.secondary { + border: 2px solid var(--neutral-color); +} + +.option-card span { + display: block; + margin-top: 10px; +} + +.option-card small { + display: block; + margin-top: 8px; + font-size: 0.85rem; + opacity: 0.7; +} + +/* === GAME CARDS === */ +.game-card { + min-height: 180px; + display: flex; + flex-direction: column; + justify-content: center; + border: 2px solid #e5e7eb; +} + +.game-card:hover { + border-color: var(--primary-color); + background: linear-gradient(135deg, #f8fafc, #f1f5f9); +} + +.game-card .icon { + font-size: 3rem; + margin-bottom: 15px; +} + +.game-card .title { + font-size: 1.4rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; +} + +.game-card .description { + font-size: 0.95rem; + color: var(--text-secondary); + line-height: 1.4; +} + +/* === LEVEL CARDS === */ +.level-card { + min-height: 160px; + border: 2px solid #e5e7eb; +} + +.level-card:hover { + border-color: var(--secondary-color); + background: linear-gradient(135deg, #f0fdf4, #ecfdf5); +} + +.level-card .icon { + font-size: 2.5rem; + margin-bottom: 12px; +} + +.level-card .title { + font-size: 1.2rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 6px; +} + +.level-card .description { + font-size: 0.9rem; + color: var(--text-secondary); +} + +/* === BOUTONS === */ +button { + border: none; + outline: none; + cursor: pointer; + font-family: inherit; + font-size: inherit; + transition: var(--transition); + border-radius: var(--border-radius); + padding: 12px 24px; + font-weight: 500; +} + +.back-btn { + background: var(--neutral-color); + color: white; + padding: 10px 20px; + border-radius: 25px; + font-size: 0.9rem; +} + +.back-btn:hover { + background: #4B5563; + transform: translateX(-2px); +} + +/* === MODAL === */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: var(--transition); +} + +.modal.show { + opacity: 1; + visibility: visible; +} + +.modal-content { + background: var(--card-background); + padding: 40px; + border-radius: var(--border-radius); + box-shadow: var(--shadow-lg); + text-align: center; + max-width: 400px; + margin: 20px; + transform: scale(0.9); + transition: var(--transition); +} + +.modal.show .modal-content { + transform: scale(1); +} + +.modal-content h3 { + color: var(--primary-color); + margin-bottom: 15px; + font-size: 1.5rem; +} + +.modal-content p { + color: var(--text-secondary); + margin-bottom: 25px; +} + +.modal-content button { + background: var(--primary-color); + color: white; + padding: 12px 30px; + border-radius: 25px; +} + +.modal-content button:hover { + background: #2563EB; +} + +/* === LOADING === */ +.loading { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 999; + opacity: 0; + visibility: hidden; + transition: var(--transition); +} + +.loading.show { + opacity: 1; + visibility: visible; +} + +.spinner { + width: 50px; + height: 50px; + border: 4px solid #f3f3f3; + border-top: 4px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* === RESPONSIVE === */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .hero h1 { + font-size: 2.2rem; + } + + .hero p { + font-size: 1.1rem; + } + + .page-header h2 { + font-size: 2rem; + } + + .cards-grid { + grid-template-columns: 1fr; + gap: 20px; + } + + .page { + padding: 20px; + } +} + +@media (max-width: 480px) { + .hero h1 { + font-size: 1.8rem; + } + + .option-card, .game-card, .level-card { + padding: 20px 15px; + } + + .modal-content { + padding: 30px 20px; + margin: 15px; + } +} \ No newline at end of file diff --git a/css/navigation.css b/css/navigation.css new file mode 100644 index 0000000..65ebc5d --- /dev/null +++ b/css/navigation.css @@ -0,0 +1,286 @@ +/* === NAVIGATION BREADCRUMB === */ +.breadcrumb { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + padding: 8px 20px; + border-radius: var(--border-radius); + box-shadow: var(--shadow); + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + transform: translateY(0); + transition: transform 0.3s ease-in-out; +} + +.breadcrumb.hidden { + transform: translateY(-100%); +} + +.breadcrumb.visible { + transform: translateY(0); +} + +.breadcrumb-item { + background: transparent; + border: 2px solid transparent; + color: var(--text-secondary); + padding: 6px 12px; + border-radius: 16px; + font-size: 0.85rem; + font-weight: 500; + transition: var(--transition); + position: relative; +} + +.breadcrumb-item:hover { + background: var(--background-color); + color: var(--text-primary); +} + +.breadcrumb-item.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +.breadcrumb-item:not(:last-child)::after { + content: '›'; + position: absolute; + right: -15px; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); + font-weight: 600; + pointer-events: none; +} + +/* === ANIMATIONS DE TRANSITION === */ +.page-transition-enter { + opacity: 0; + transform: translateX(30px); +} + +.page-transition-enter-active { + opacity: 1; + transform: translateX(0); + transition: all 0.3s ease-out; +} + +.page-transition-exit { + opacity: 1; + transform: translateX(0); +} + +.page-transition-exit-active { + opacity: 0; + transform: translateX(-30px); + transition: all 0.3s ease-in; +} + +/* === BODY PADDING FOR FIXED BREADCRUMB === */ +body { + padding-top: 50px; +} + +/* === NAVIGATION RESPONSIVE === */ +@media (max-width: 768px) { + .breadcrumb { + padding: 6px 15px; + margin-bottom: 10px; + } + + .breadcrumb-item { + padding: 4px 10px; + font-size: 0.8rem; + } + + .breadcrumb-item:not(:last-child)::after { + right: -10px; + font-size: 0.75rem; + } + + body { + padding-top: 40px; + } +} + +/* === CONTENT SCANNING STYLES === */ +.loading-content, .no-content, .error-content { + text-align: center; + padding: 40px 20px; + color: var(--text-secondary); + font-style: italic; + background: #f8f9fa; + border-radius: 12px; + border: 2px dashed #e5e7eb; +} + +.error-content { + color: var(--error-color); + border-color: var(--error-color); + background: #fef2f2; +} + +.content-info { + background: #f0f9ff; + padding: 15px; + border-radius: 8px; + text-align: center; + margin-top: 20px; + border-left: 4px solid var(--primary-color); +} + +.show-all-btn { + background: var(--primary-color); + color: white; + border: none; + padding: 8px 16px; + border-radius: 20px; + margin-top: 10px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s ease; +} + +.show-all-btn:hover { + background: #2563EB; + transform: translateY(-1px); +} + +/* === ENHANCED LEVEL CARDS === */ +.level-card { + position: relative; + overflow: visible; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 10px; +} + +.card-header .icon { + font-size: 2.5rem; +} + +.compatibility { + font-size: 1.2rem; + padding: 2px; + border-radius: 4px; +} + +.compatibility.high-compat { + background: rgba(16, 185, 129, 0.1); +} + +.compatibility.medium-compat { + background: rgba(245, 158, 11, 0.1); +} + +.compatibility.low-compat { + background: rgba(239, 68, 68, 0.1); +} + +.content-stats { + display: flex; + gap: 8px; + margin: 10px 0; + flex-wrap: wrap; +} + +.difficulty-badge { + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: bold; + text-transform: uppercase; +} + +.difficulty-easy { + background: #10B981; + color: white; +} + +.difficulty-medium { + background: #F59E0B; + color: white; +} + +.difficulty-hard { + background: #EF4444; + color: white; +} + +.difficulty-intermediate { + background: #6366F1; + color: white; +} + +.items-count, .time-estimate { + background: #f3f4f6; + color: var(--text-secondary); + padding: 2px 6px; + border-radius: 8px; + font-size: 0.75rem; +} + +.detailed-stats { + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 8px; + border-top: 1px solid #f3f4f6; + padding-top: 8px; +} + +@media (max-width: 768px) { + .card-header { + flex-direction: column; + align-items: center; + gap: 5px; + } + + .content-stats { + justify-content: center; + } + + .detailed-stats { + text-align: center; + } +} + +@media (max-width: 480px) { + .breadcrumb { + padding: 5px 12px; + gap: 6px; + } + + .breadcrumb-item { + padding: 4px 8px; + font-size: 0.75rem; + } + + .breadcrumb-item:not(:last-child)::after { + right: -8px; + } + + body { + padding-top: 35px; + } + + .content-stats { + flex-direction: column; + gap: 5px; + } + + .loading-content, .no-content, .error-content { + padding: 30px 15px; + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..046efa7 --- /dev/null +++ b/index.html @@ -0,0 +1,145 @@ + + + + + + Cours d'Anglais Interactif + + + + + + + + + +
+ + +
+
+

🎓 Cours d'Anglais Interactif

+

Apprends l'anglais en t'amusant !

+
+ +
+ + + + +
+
+ + +
+ + +
+ +
+
+ + +
+ + +
+ +
+
+ + +
+
+ +

Jeu en cours...

+
+ Score: 0 +
+
+ +
+ +
+
+ +
+ + + + + +
+
+

Chargement...

+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/content/basic-chinese.js b/js/content/basic-chinese.js new file mode 100644 index 0000000..94923ba --- /dev/null +++ b/js/content/basic-chinese.js @@ -0,0 +1,115 @@ +// Basic Chinese content for Chinese Study Mode + +const basicChineseContent = { + vocabulary: { + // Basic greetings and common words + "你好": "hello (nǐ hǎo)", + "再见": "goodbye (zài jiàn)", + "谢谢": "thank you (xiè xiè)", + "对不起": "sorry (duì bu qǐ)", + "请": "please (qǐng)", + "是": "yes/to be (shì)", + "不": "no/not (bù)", + "我": "I/me (wǒ)", + "你": "you (nǐ)", + "他": "he/him (tā)", + "她": "she/her (tā)", + + // Numbers 1-10 + "一": "one (yī)", + "二": "two (èr)", + "三": "three (sān)", + "四": "four (sì)", + "五": "five (wǔ)", + "六": "six (liù)", + "七": "seven (qī)", + "八": "eight (bā)", + "九": "nine (jiǔ)", + "十": "ten (shí)", + + // Basic family + "家": "family/home (jiā)", + "爸爸": "father (bà ba)", + "妈妈": "mother (mā ma)", + "儿子": "son (ér zi)", + "女儿": "daughter (nǚ ér)" + }, + + sentences: [ + { + chinese: "你好!", + english: "Hello!", + pinyin: "Nǐ hǎo!" + }, + { + chinese: "我是学生。", + english: "I am a student.", + pinyin: "Wǒ shì xué shēng." + }, + { + chinese: "谢谢你!", + english: "Thank you!", + pinyin: "Xiè xiè nǐ!" + }, + { + chinese: "这是我的家。", + english: "This is my home.", + pinyin: "Zhè shì wǒ de jiā." + } + ], + + dialogues: [ + { + title: "Basic Greeting", + conversation: [ + { speaker: "A", chinese: "你好!", english: "Hello!", pinyin: "Nǐ hǎo!" }, + { speaker: "B", chinese: "你好!你叫什么名字?", english: "Hello! What's your name?", pinyin: "Nǐ hǎo! Nǐ jiào shén me míng zi?" }, + { speaker: "A", chinese: "我叫小明。你呢?", english: "My name is Xiaoming. And you?", pinyin: "Wǒ jiào Xiǎo Míng. Nǐ ne?" }, + { speaker: "B", chinese: "我叫小红。很高兴认识你!", english: "My name is Xiaohong. Nice to meet you!", pinyin: "Wǒ jiào Xiǎo Hóng. Hěn gāo xìng rèn shi nǐ!" } + ] + } + ], + + texts: [ + { + title: "Learning Chinese", + content: "Chinese is one of the most spoken languages in the world. It uses characters instead of letters. Each character can represent a word or part of a word. Learning Chinese characters, their pronunciation (pinyin), and meanings is the foundation of studying Chinese.", + chinese: "学习中文是很有趣的。中文使用汉字,不是字母。每个汉字都有意思。" + } + ], + + culturalNotes: [ + { + topic: "Chinese Characters", + note: "Chinese characters are logograms, where each character represents a word or morpheme. There are thousands of characters, but you only need about 2000-3000 to read most modern Chinese texts." + }, + { + topic: "Pinyin", + note: "Pinyin is the romanization system used to help learn Chinese pronunciation. It uses the Latin alphabet with tone marks to indicate the four main tones in Mandarin Chinese." + }, + { + topic: "Tones", + note: "Mandarin Chinese has four main tones plus a neutral tone. The tone changes the meaning of the word, so it's important to learn them correctly." + } + ] +}; + +// Export for web module system +window.ContentModules = window.ContentModules || {}; +window.ContentModules.BasicChinese = { + name: "Basic Chinese", + description: "Essential Chinese characters, pinyin and vocabulary for beginners", + difficulty: "beginner", + vocabulary: basicChineseContent.vocabulary, + sentences: basicChineseContent.sentences, + dialogues: basicChineseContent.dialogues, + texts: basicChineseContent.texts, + culturalNotes: basicChineseContent.culturalNotes, + language: "chinese", + hskLevel: "HSK1" +}; + +// Node.js export (optional) +if (typeof module !== 'undefined' && module.exports) { + module.exports = basicChineseContent; +} \ No newline at end of file diff --git a/js/content/english-class-demo.js b/js/content/english-class-demo.js new file mode 100644 index 0000000..62ee1d4 --- /dev/null +++ b/js/content/english-class-demo.js @@ -0,0 +1,287 @@ +// Demo English Class - Rich Content Example +// Shows all possible content types with dummy data + +const englishClassDemoContent = { + // Rich vocabulary with all optional features + vocabulary: { + "apple": { + translation: "pomme", + type: "noun", + pronunciation: "audio/apple.mp3", + difficulty: "beginner", + examples: ["I eat an apple every day", "The apple is red and sweet"], + grammarNotes: "Count noun - can be singular or plural" + }, + "run": { + translation: "courir", + type: "verb", + pronunciation: "audio/run.mp3", + difficulty: "beginner", + examples: ["I run in the park", "She runs very fast"], + grammarNotes: "Regular verb: run, runs, running, ran" + }, + "beautiful": { + translation: "beau/belle", + type: "adjective", + pronunciation: "audio/beautiful.mp3", + difficulty: "intermediate", + examples: ["The sunset is beautiful", "She has beautiful eyes"], + grammarNotes: "Can be used before noun or after 'be'" + }, + "hello": { + translation: "bonjour", + type: "greeting", + pronunciation: "audio/hello.mp3", + difficulty: "beginner", + examples: ["Hello, how are you?", "Hello everyone!"], + grammarNotes: "Common greeting used anytime" + }, + // Simple format examples (backward compatibility) + "cat": "chat", + "dog": "chien", + "house": "maison" + }, + + // Grammar rules and explanations + grammar: { + presentSimple: { + title: "Present Simple Tense", + explanation: "Used for habits, facts, and general truths. Form: Subject + base verb (+ s for he/she/it)", + examples: [ + { english: "I walk to school", french: "Je marche à l'école" }, + { english: "She walks to school", french: "Elle marche à l'école" }, + { english: "They walk to school", french: "Ils marchent à l'école" } + ], + exercises: [ + "Complete: I _____ (play) tennis every Sunday", + "Transform: He walk to work → He _____ to work" + ] + }, + articles: { + title: "Articles: A, An, The", + explanation: "A/An = indefinite articles (one of many). The = definite article (specific one)", + examples: [ + { english: "I see a cat", french: "Je vois un chat" }, + { english: "I see an elephant", french: "Je vois un éléphant" }, + { english: "I see the cat from yesterday", french: "Je vois le chat d'hier" } + ] + } + }, + + // Audio content with transcripts + audio: { + withText: [ + { + title: "Daily Routine Conversation", + audioFile: "audio/daily_routine.mp3", + transcript: "A: What time do you wake up? B: I usually wake up at 7 AM. A: That's early! I wake up at 8:30. B: I like to exercise before work. A: That's a good habit!", + translation: "A: À quelle heure te réveilles-tu? B: Je me réveille habituellement à 7h. A: C'est tôt! Je me réveille à 8h30. B: J'aime faire de l'exercice avant le travail. A: C'est une bonne habitude!", + timestamps: [ + { time: 0.5, text: "What time do you wake up?" }, + { time: 3.2, text: "I usually wake up at 7 AM" }, + { time: 6.8, text: "That's early! I wake up at 8:30" }, + { time: 11.1, text: "I like to exercise before work" }, + { time: 14.5, text: "That's a good habit!" } + ] + }, + { + title: "Weather Report", + audioFile: "audio/weather.mp3", + transcript: "Today's weather: It's sunny and warm with a high of 25 degrees. Light winds from the south. Perfect day for outdoor activities!", + translation: "Météo d'aujourd'hui: Il fait ensoleillé et chaud avec un maximum de 25 degrés. Vents légers du sud. Journée parfaite pour les activités extérieures!" + } + ], + withoutText: [ + { + title: "Mystery Conversation", + audioFile: "audio/mystery.mp3", + questions: [ + { question: "How many people are speaking?", type: "ai_interpreted" }, + { question: "What are they talking about?", type: "ai_interpreted" }, + { question: "What is the mood of the conversation?", type: "ai_interpreted" } + ] + } + ] + }, + + // Poetry with cultural context + poems: [ + { + title: "Roses Are Red", + content: "Roses are red,\nViolets are blue,\nSugar is sweet,\nAnd so are you.", + translation: "Les roses sont rouges,\nLes violettes sont bleues,\nLe sucre est doux,\nEt toi aussi.", + audioFile: "audio/roses_poem.mp3", + culturalContext: "Traditional English nursery rhyme pattern, often used to teach basic rhyming and poetry structure to children." + }, + { + title: "Twinkle, Twinkle", + content: "Twinkle, twinkle, little star,\nHow I wonder what you are.\nUp above the world so high,\nLike a diamond in the sky.", + audioFile: "audio/twinkle.mp3", + culturalContext: "Famous children's lullaby, one of the most recognizable songs in English-speaking countries." + } + ], + + // Fill-in-the-blank exercises + fillInBlanks: [ + { + sentence: "I _____ to school every day", + options: ["go", "goes", "going", "went"], + correctAnswer: "go", + explanation: "Present simple with 'I' uses base form of verb" + }, + { + sentence: "She _____ a book right now", + options: ["read", "reads", "reading", "is reading"], + correctAnswer: "is reading", + explanation: "Present continuous for actions happening now" + }, + { + sentence: "The weather is _____ today", + type: "open_ended", + acceptedAnswers: ["nice", "good", "beautiful", "sunny", "warm", "pleasant", "lovely"], + aiPrompt: "Evaluate if the answer is a positive adjective that could describe good weather" + }, + { + sentence: "I feel _____ when I listen to music", + type: "open_ended", + acceptedAnswers: ["happy", "relaxed", "calm", "peaceful", "good", "better"], + aiPrompt: "Check if the answer describes a positive emotion or feeling" + } + ], + + // Sentence correction exercises + corrections: [ + { + incorrect: "I are happy today", + correct: "I am happy today", + explanation: "Use 'am' with pronoun 'I', not 'are'", + type: "grammar_correction" + }, + { + incorrect: "She don't like apples", + correct: "She doesn't like apples", + explanation: "Use 'doesn't' with he/she/it, not 'don't'", + type: "grammar_correction" + }, + { + incorrect: "I can to swim", + correct: "I can swim", + explanation: "After modal verbs like 'can', use base form without 'to'", + type: "grammar_correction" + } + ], + + // Reading comprehension with AI evaluation + comprehension: [ + { + text: "Sarah is a 25-year-old teacher who lives in London. Every morning, she wakes up at 6:30 AM and goes for a jog in the park near her house. After jogging, she has breakfast and reads the news. She loves her job because she enjoys working with children and helping them learn. On weekends, Sarah likes to visit museums and try new restaurants with her friends.", + questions: [ + { + question: "What is Sarah's profession?", + type: "multiple_choice", + options: ["Doctor", "Teacher", "Engineer", "Artist"], + correctAnswer: "Teacher" + }, + { + question: "What does Sarah do every morning?", + type: "ai_interpreted", + evaluationPrompt: "Check if answer mentions waking up early, jogging, and having breakfast" + }, + { + question: "Why does Sarah love her job?", + type: "ai_interpreted", + evaluationPrompt: "Verify answer mentions working with children and helping them learn" + }, + { + question: "How would you describe Sarah's lifestyle?", + type: "ai_interpreted", + evaluationPrompt: "Accept answers mentioning active, healthy, social, or organized lifestyle" + } + ] + } + ], + + // Matching exercises + matching: [ + { + title: "Match Animals to Their Sounds", + leftColumn: ["Cat", "Dog", "Cow", "Bird"], + rightColumn: ["Woof", "Meow", "Tweet", "Moo"], + correctPairs: [ + { left: "Cat", right: "Meow" }, + { left: "Dog", right: "Woof" }, + { left: "Cow", right: "Moo" }, + { left: "Bird", right: "Tweet" } + ] + }, + { + title: "Match Colors in English and French", + leftColumn: ["Red", "Blue", "Green", "Yellow"], + rightColumn: ["Bleu", "Vert", "Rouge", "Jaune"], + correctPairs: [ + { left: "Red", right: "Rouge" }, + { left: "Blue", right: "Bleu" }, + { left: "Green", right: "Vert" }, + { left: "Yellow", right: "Jaune" } + ] + } + ], + + // Standard content (backward compatibility) + sentences: [ + { english: "Hello, how are you?", french: "Bonjour, comment allez-vous?" }, + { english: "I like to read books", french: "J'aime lire des livres" }, + { english: "The weather is nice today", french: "Il fait beau aujourd'hui" }, + { english: "Can you help me please?", french: "Pouvez-vous m'aider s'il vous plaît?" } + ], + + texts: [ + { + title: "My Daily Routine", + content: "I wake up at 7 AM every day. First, I brush my teeth and take a shower. Then I have breakfast with my family. After breakfast, I go to work by bus. I work from 9 AM to 5 PM. In the evening, I cook dinner and watch TV. I go to bed at 10 PM.", + translation: "Je me réveille à 7h tous les jours. D'abord, je me brosse les dents et prends une douche. Ensuite je prends le petit déjeuner avec ma famille. Après le petit déjeuner, je vais au travail en bus. Je travaille de 9h à 17h. Le soir, je cuisine le dîner et regarde la télé. Je me couche à 22h." + }, + { + title: "The Four Seasons", + content: "There are four seasons in a year: spring, summer, autumn, and winter. Spring is warm and flowers bloom. Summer is hot and sunny. Autumn is cool and leaves change colors. Winter is cold and it sometimes snows.", + translation: "Il y a quatre saisons dans une année: le printemps, l'été, l'automne et l'hiver. Le printemps est chaud et les fleurs fleurissent. L'été est chaud et ensoleillé. L'automne est frais et les feuilles changent de couleur. L'hiver est froid et il neige parfois." + } + ], + + dialogues: [ + { + title: "At the Restaurant", + conversation: [ + { speaker: "Waiter", english: "Good evening! Welcome to our restaurant.", french: "Bonsoir! Bienvenue dans notre restaurant." }, + { speaker: "Customer", english: "Thank you. Can I see the menu please?", french: "Merci. Puis-je voir le menu s'il vous plaît?" }, + { speaker: "Waiter", english: "Of course! Here you are. What would you like to drink?", french: "Bien sûr! Voici. Que voulez-vous boire?" }, + { speaker: "Customer", english: "I'll have a glass of water, please.", french: "Je prendrai un verre d'eau, s'il vous plaît." } + ] + } + ] +}; + +// Export for web module system +window.ContentModules = window.ContentModules || {}; +window.ContentModules.EnglishClassDemo = { + name: "English Class Demo", + description: "Complete example with all content types - vocabulary, grammar, audio, poems, exercises", + difficulty: "mixed", + language: "english", + vocabulary: englishClassDemoContent.vocabulary, + grammar: englishClassDemoContent.grammar, + audio: englishClassDemoContent.audio, + poems: englishClassDemoContent.poems, + fillInBlanks: englishClassDemoContent.fillInBlanks, + corrections: englishClassDemoContent.corrections, + comprehension: englishClassDemoContent.comprehension, + matching: englishClassDemoContent.matching, + sentences: englishClassDemoContent.sentences, + texts: englishClassDemoContent.texts, + dialogues: englishClassDemoContent.dialogues +}; + +// Node.js export (optional) +if (typeof module !== 'undefined' && module.exports) { + module.exports = englishClassDemoContent; +} \ No newline at end of file diff --git a/js/content/sbs-level-7-8-new.js b/js/content/sbs-level-7-8-new.js new file mode 100644 index 0000000..d670653 --- /dev/null +++ b/js/content/sbs-level-7-8-new.js @@ -0,0 +1,239 @@ +const content = { + vocabulary: { + // Housing and Places + central: "中心的;中央的", + avenue: "大街;林荫道", + refrigerator: "冰箱", + closet: "衣柜;壁橱", + elevator: "电梯", + building: "建筑物;大楼", + "air conditioner": "空调", + superintendent: "主管;负责人", + "bus stop": "公交车站", + jacuzzi: "按摩浴缸", + machine: "机器;设备", + "two and a half": "两个半", + "in the center of": "在……中心", + town: "城镇", + "a lot of": "许多", + noise: "噪音", + sidewalks: "人行道", + "all day and all night": "整日整夜", + convenient: "便利的", + upset: "失望的", + + // Clothing and Accessories + shirt: "衬衫", + coat: "外套、大衣", + dress: "连衣裙", + skirt: "短裙", + blouse: "女式衬衫", + jacket: "夹克、短外套", + sweater: "毛衣、针织衫", + suit: "套装、西装", + tie: "领带", + pants: "裤子", + jeans: "牛仔裤", + belt: "腰带、皮带", + hat: "帽子", + glove: "手套", + "purse/pocketbook": "手提包、女式小包", + glasses: "眼镜", + pajamas: "睡衣", + socks: "袜子", + shoes: "鞋子", + bathrobe: "浴袍", + "tee shirt": "T恤", + scarf: "围巾", + wallet: "钱包", + ring: "戒指", + sandals: "凉鞋", + slippers: "拖鞋", + sneakers: "运动鞋", + shorts: "短裤", + "sweat pants": "运动裤", + + // Places and Areas + "urban areas": "cities", + "suburban areas": "places near cities", + "rural areas": "places in the countryside, far from cities", + farmhouse: "农舍", + hut: "小屋", + houseboat: "船屋", + "mobile home": "移动房屋", + trailer: "拖车房", + + // Store Items + jackets: "夹克", + gloves: "手套", + blouses: "女式衬衫", + bracelets: "手镯", + ties: "领带" + }, + + sentences: [ + { + english: "Amy's apartment building is in the center of town.", + chinese: "艾米的公寓楼在城镇中心。" + }, + { + english: "There's a lot of noise near Amy's apartment building.", + chinese: "艾米的公寓楼附近有很多噪音。" + }, + { + english: "It's a very busy place, but it's a convenient place to live.", + chinese: "那是个非常热闹的地方,但也是个居住很方便的地方。" + }, + { + english: "Around the corner from the building, there are two supermarkets.", + chinese: "从这栋楼拐个弯,就有两家超市。" + }, + { + english: "I'm looking for a shirt.", + chinese: "我在找一件衬衫。" + }, + { + english: "Shirts are over there.", + chinese: "衬衫在那边。" + } + ], + + texts: [ + { + title: "People's Homes", + content: "Homes are different all around the world. This family is living in a farmhouse. This family is living in a hut. This family is living in a houseboat. These people are living in a mobile home (a trailer). What different kinds of homes are there in your country?" + }, + { + title: "Urban, Suburban, and Rural", + content: "urban areas = cities, suburban areas = places near cities, rural areas = places in the countryside, far from cities. About 50% (percent) of the world's population is in urban and suburban areas. About 50% (percent) of the world's population is in rural areas." + }, + { + title: "Global Exchange - RosieM", + content: "My apartment is in a wonderful neighborhood. There's a big, beautiful park across from my apartment building. Around the corner, there's a bank, a post office, and a laundromat. There are also many restaurants and stores in my neighborhood. It's a noisy place, but it's a very interesting place. There are a lot of people on the sidewalks all day and all night. How about your neighborhood? Tell me about it." + }, + { + title: "Clothing, Colors, and Cultures", + content: "Blue and pink aren't children's clothing colors all around the world. The meanings of colors are sometimes very different in different cultures. For example, in some cultures, blue is a common clothing color for little boys, and pink is a common clothing color for little girls. In other cultures, other colors are common for boys and girls. There are also different colors for special days in different cultures. For example, white is the traditional color of a wedding dress in some cultures, but other colors are traditional in other cultures. For some people, white is a happy color. For others, it's a sad color. For some people, red is a beautiful and lucky color. For others, it's a very sad color. What are the meanings of different colors in YOUR culture?" + } + ], + + grammar: { + thereBe: { + topic: "There be 句型的用法", + singular: { + form: "there is (there's) + 名词单数/不可数名词", + explanation: "在某地方有什么人或东西", + examples: [ + "There's a bank.", + "There's some water.", + "There's a book store on Main Street." + ], + forms: { + positive: "There's a stove in the kitchen.", + negative: "There isn't a stove in the kitchen.", + question: "Is there a stove in the kitchen?", + shortAnswers: "Yes, there is. / No, there isn't." + } + }, + plural: { + form: "there are (there're) + 复数名词", + examples: [ + "There're two hospitals.", + "There're many rooms in this apartment." + ], + forms: { + positive: "There're two windows in the kitchen.", + negative: "There aren't two windows in the kitchen.", + question: "Are there two windows in the kitchen?", + shortAnswers: "Yes, there are. / No, there aren't." + } + } + }, + plurals: { + topic: "可数名词复数", + pronunciation: { + rules: [ + { + condition: "在清辅音/-p,-k/后", + pronunciation: "/-s/", + example: "socks中-k是清辅音/-k/,所以-s读/-s/" + }, + { + condition: "在浊辅音和元音音标后", + pronunciation: "/-z/", + example: "jeans中-n是浊辅音/-n/, 所以-s读/-z/; tie的读音是/tai/,以元音结尾,所以-s读/-z/" + }, + { + condition: "以/-s,-z,-ʃ,-ʒ,-tʃ,-dʒ/发音结尾的名词", + pronunciation: "/-iz/", + example: "watches中-ch读/-tʃ/,所以-es读/-iz/" + } + ] + }, + formation: { + regular: { + rule: "一般在词尾加-s", + examples: ["shirts", "shoes"] + }, + special: { + rule: "以-s,-sh,-ch,-x,以及辅音字母o结尾的词在词尾加-es", + examples: ["boxes", "buses", "potatoes", "tomatoes", "heroes"] + }, + irregular: { + rule: "特殊的复数形式", + examples: { + "man": "men", + "woman": "women", + "child": "children", + "tooth": "teeth", + "mouse": "mice" + } + } + } + } + }, + + listening: { + jMartShopping: { + title: "Attention, J-Mart Shoppers!", + items: [ + { item: "jackets", aisle: "Aisle 9" }, + { item: "gloves", aisle: "Aisle 7" }, + { item: "blouses", aisle: "Aisle 9" }, + { item: "bracelets", aisle: "Aisle 11" }, + { item: "ties", aisle: "Aisle 5" } + ] + } + }, + + exercises: { + sentenceCompletion: [ + "That's a very nice _______.", + "Those are very nice _______." + ], + questions: [ + "What different kinds of homes are there in your country?", + "How about your neighborhood? Tell me about it.", + "What are the meanings of different colors in YOUR culture?" + ] + } +}; + +// Export pour le système de modules web +window.ContentModules = window.ContentModules || {}; +window.ContentModules.SBSLevel78New = { + name: "SBS Level 7-8 (New)", + description: "Format simple et clair - Homes, Clothing & Cultures", + difficulty: "intermediate", + vocabulary: content.vocabulary, + sentences: content.sentences, + texts: content.texts, + grammar: content.grammar, + listening: content.listening, + exercises: content.exercises +}; + +// Export Node.js (optionnel) +if (typeof module !== 'undefined' && module.exports) { + module.exports = content; +} \ No newline at end of file diff --git a/js/core/content-engine.js b/js/core/content-engine.js new file mode 100644 index 0000000..36ed9a9 --- /dev/null +++ b/js/core/content-engine.js @@ -0,0 +1,485 @@ +// === MOTEUR DE CONTENU FLEXIBLE === + +class ContentEngine { + constructor() { + this.loadedContent = {}; + this.migrator = new ContentMigrator(); + this.validator = new ContentValidator(); + } + + // Charger et traiter un module de contenu + async loadContent(contentId) { + if (this.loadedContent[contentId]) { + return this.loadedContent[contentId]; + } + + try { + // Charger le contenu brut + const rawContent = await this.loadRawContent(contentId); + + // Valider et migrer si nécessaire + const processedContent = this.processContent(rawContent); + + // Mettre en cache + this.loadedContent[contentId] = processedContent; + + return processedContent; + } catch (error) { + console.error(`Erreur chargement contenu ${contentId}:`, error); + throw error; + } + } + + async loadRawContent(contentId) { + // Charger depuis le module existant + const moduleName = this.getModuleName(contentId); + if (window.ContentModules && window.ContentModules[moduleName]) { + return window.ContentModules[moduleName]; + } + + // Charger dynamiquement le script + await this.loadScript(`js/content/${contentId}.js`); + return window.ContentModules[moduleName]; + } + + processContent(rawContent) { + // Vérifier le format + if (this.isOldFormat(rawContent)) { + console.log('Migration ancien format vers nouveau format...'); + return this.migrator.migrateToNewFormat(rawContent); + } + + // Valider le nouveau format + if (!this.validator.validate(rawContent)) { + throw new Error('Format de contenu invalide'); + } + + return rawContent; + } + + isOldFormat(content) { + // Détecter l'ancien format (simple array vocabulary) + return content.vocabulary && Array.isArray(content.vocabulary) && + !content.contentItems && !content.version; + } + + // Filtrer le contenu par critères + filterContent(content, filters = {}) { + if (!content.contentItems) return content; + + let filtered = [...content.contentItems]; + + // Filtrer par type + if (filters.type) { + filtered = filtered.filter(item => + Array.isArray(filters.type) ? + filters.type.includes(item.type) : + item.type === filters.type + ); + } + + // Filtrer par difficulté + if (filters.difficulty) { + filtered = filtered.filter(item => item.difficulty === filters.difficulty); + } + + // Filtrer par catégorie + if (filters.category) { + filtered = filtered.filter(item => item.category === filters.category); + } + + // Filtrer par tags + if (filters.tags) { + filtered = filtered.filter(item => + item.content.tags && + filters.tags.some(tag => item.content.tags.includes(tag)) + ); + } + + return { ...content, contentItems: filtered }; + } + + // Adapter le contenu pour un jeu spécifique + adaptForGame(content, gameType) { + const adapter = new GameContentAdapter(gameType); + return adapter.adapt(content); + } + + // Obtenir des éléments aléatoires + getRandomItems(content, count = 10, filters = {}) { + const filtered = this.filterContent(content, filters); + const items = filtered.contentItems || []; + + const shuffled = [...items].sort(() => Math.random() - 0.5); + return shuffled.slice(0, count); + } + + // Obtenir des éléments par progression + getItemsByProgression(content, userLevel = 1, count = 10) { + const items = content.contentItems || []; + + // Calculer la difficulté appropriée + const targetDifficulties = this.getDifficultiesForLevel(userLevel); + + const appropriateItems = items.filter(item => + targetDifficulties.includes(item.difficulty) + ); + + return this.shuffleArray(appropriateItems).slice(0, count); + } + + getDifficultiesForLevel(level) { + if (level <= 2) return ['easy']; + if (level <= 4) return ['easy', 'medium']; + return ['easy', 'medium', 'hard']; + } + + // Utilitaires + getModuleName(contentId) { + const names = { + 'sbs-level-8': 'SBSLevel8', + 'animals': 'Animals', + 'colors': 'Colors', + 'family': 'Family', + 'food': 'Food', + 'house': 'House' + }; + return names[contentId] || contentId; + } + + async loadScript(src) { + return new Promise((resolve, reject) => { + const existingScript = document.querySelector(`script[src="${src}"]`); + if (existingScript) { + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = src; + script.onload = resolve; + script.onerror = () => reject(new Error(`Impossible de charger ${src}`)); + document.head.appendChild(script); + }); + } + + 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; + } +} + +// === MIGRATEUR DE CONTENU === + +class ContentMigrator { + migrateToNewFormat(oldContent) { + const newContent = { + id: oldContent.name || 'migrated-content', + name: oldContent.name || 'Contenu Migré', + description: oldContent.description || '', + version: '2.0', + format: 'unified', + difficulty: oldContent.difficulty || 'intermediate', + + // Métadonnées + metadata: { + totalItems: oldContent.vocabulary ? oldContent.vocabulary.length : 0, + categories: this.extractCategories(oldContent), + contentTypes: ['vocabulary'], + migrationDate: new Date().toISOString() + }, + + // Configuration + config: { + defaultInteraction: 'click', + supportedGames: ['whack-a-mole', 'memory-game', 'temp-games'], + adaptiveEnabled: true + }, + + // Contenu principal + contentItems: [] + }; + + // Migrer le vocabulaire + if (oldContent.vocabulary) { + oldContent.vocabulary.forEach((word, index) => { + newContent.contentItems.push(this.migrateVocabularyItem(word, index)); + }); + } + + // Migrer les phrases si elles existent + if (oldContent.phrases) { + oldContent.phrases.forEach((phrase, index) => { + newContent.contentItems.push(this.migratePhraseItem(phrase, index)); + }); + } + + return newContent; + } + + migrateVocabularyItem(oldWord, index) { + return { + id: `vocab_${index + 1}`, + type: 'vocabulary', + difficulty: this.inferDifficulty(oldWord.english), + category: oldWord.category || 'general', + + content: { + english: oldWord.english, + french: oldWord.french, + context: oldWord.category || '', + tags: oldWord.category ? [oldWord.category] : [] + }, + + media: { + image: oldWord.image || null, + audio: null, + icon: this.getIconForCategory(oldWord.category) + }, + + pedagogy: { + learningObjective: `Apprendre le mot "${oldWord.english}"`, + prerequisites: [], + followUp: [], + grammarFocus: 'vocabulary' + }, + + interaction: { + type: 'click', + validation: 'exact', + hints: [oldWord.french], + feedback: { + correct: `Bien joué ! "${oldWord.english}" = "${oldWord.french}"`, + incorrect: `Non, "${oldWord.english}" = "${oldWord.french}"` + }, + alternatives: [] // Sera rempli dynamiquement + } + }; + } + + migratePhraseItem(oldPhrase, index) { + return { + id: `phrase_${index + 1}`, + type: 'sentence', + difficulty: 'medium', + category: oldPhrase.category || 'general', + + content: { + english: oldPhrase.english, + french: oldPhrase.french, + context: oldPhrase.category || '', + tags: [oldPhrase.category || 'phrase'] + }, + + media: { + image: null, + audio: null, + icon: '💬' + }, + + pedagogy: { + learningObjective: `Comprendre la phrase "${oldPhrase.english}"`, + prerequisites: [], + followUp: [], + grammarFocus: 'sentence_structure' + }, + + interaction: { + type: 'click', + validation: 'exact', + hints: [oldPhrase.french], + feedback: { + correct: `Parfait ! Cette phrase signifie "${oldPhrase.french}"`, + incorrect: `Cette phrase signifie "${oldPhrase.french}"` + } + } + }; + } + + inferDifficulty(englishWord) { + if (englishWord.length <= 4) return 'easy'; + if (englishWord.length <= 8) return 'medium'; + return 'hard'; + } + + getIconForCategory(category) { + const icons = { + family: '👨‍👩‍👧‍👦', + animals: '🐱', + colors: '🎨', + food: '🍎', + school_objects: '📚', + daily_activities: '🌅', + numbers: '🔢' + }; + return icons[category] || '📝'; + } + + extractCategories(oldContent) { + const categories = new Set(); + + if (oldContent.vocabulary) { + oldContent.vocabulary.forEach(word => { + if (word.category) categories.add(word.category); + }); + } + + if (oldContent.categories) { + Object.keys(oldContent.categories).forEach(cat => categories.add(cat)); + } + + return Array.from(categories); + } +} + +// === VALIDATEUR DE CONTENU === + +class ContentValidator { + validate(content) { + try { + // Vérifications de base + if (!content.id || !content.name) { + console.warn('Contenu manque ID ou nom'); + return false; + } + + if (!content.contentItems || !Array.isArray(content.contentItems)) { + console.warn('contentItems manquant ou invalide'); + return false; + } + + // Valider chaque élément + for (let item of content.contentItems) { + if (!this.validateContentItem(item)) { + return false; + } + } + + return true; + } catch (error) { + console.error('Erreur validation:', error); + return false; + } + } + + validateContentItem(item) { + // Champs requis + const requiredFields = ['id', 'type', 'content']; + + for (let field of requiredFields) { + if (!item[field]) { + console.warn(`Champ requis manquant: ${field}`); + return false; + } + } + + // Valider le contenu selon le type + switch (item.type) { + case 'vocabulary': + return this.validateVocabulary(item); + case 'sentence': + return this.validateSentence(item); + case 'dialogue': + return this.validateDialogue(item); + default: + console.warn(`Type de contenu inconnu: ${item.type}`); + return true; // Permettre types inconnus pour extensibilité + } + } + + validateVocabulary(item) { + return item.content.english && item.content.french; + } + + validateSentence(item) { + return item.content.english && item.content.french; + } + + validateDialogue(item) { + return item.content.conversation && Array.isArray(item.content.conversation); + } +} + +// === ADAPTATEUR CONTENU/JEU === + +class GameContentAdapter { + constructor(gameType) { + this.gameType = gameType; + } + + adapt(content) { + switch (this.gameType) { + case 'whack-a-mole': + return this.adaptForWhackAMole(content); + case 'memory-game': + return this.adaptForMemoryGame(content); + case 'temp-games': + return this.adaptForTempGames(content); + default: + return content; + } + } + + adaptForWhackAMole(content) { + // Convertir vers format attendu par Whack-a-Mole + const vocabulary = content.contentItems + .filter(item => item.type === 'vocabulary' || item.type === 'sentence') + .map(item => ({ + english: item.content.english, + french: item.content.french, + image: item.media?.image, + category: item.category, + difficulty: item.difficulty, + interaction: item.interaction + })); + + return { + ...content, + vocabulary: vocabulary, + // Maintenir la compatibilité + gameSettings: { + whackAMole: { + recommendedWords: Math.min(15, vocabulary.length), + timeLimit: 60, + maxErrors: 5 + } + } + }; + } + + adaptForMemoryGame(content) { + const pairs = content.contentItems + .filter(item => ['vocabulary', 'sentence'].includes(item.type)) + .map(item => ({ + english: item.content.english, + french: item.content.french, + image: item.media?.image, + type: item.type + })); + + return { ...content, pairs: pairs }; + } + + adaptForTempGames(content) { + // Format flexible pour les mini-jeux + return { + ...content, + vocabulary: content.contentItems.map(item => ({ + english: item.content.english, + french: item.content.french, + type: item.type, + difficulty: item.difficulty + })) + }; + } +} + +// Export global +window.ContentEngine = ContentEngine; +window.ContentMigrator = ContentMigrator; +window.ContentValidator = ContentValidator; +window.GameContentAdapter = GameContentAdapter; \ No newline at end of file diff --git a/js/core/content-factory.js b/js/core/content-factory.js new file mode 100644 index 0000000..a40f40b --- /dev/null +++ b/js/core/content-factory.js @@ -0,0 +1,554 @@ +// === CONTENT FACTORY - GÉNÉRATEUR UNIVERSEL DE CONTENU === + +class ContentFactory { + constructor() { + this.parsers = new Map(); + this.generators = new Map(); + this.templates = new Map(); + this.mediaProcessor = new MediaProcessor(); + this.validator = new ContentValidator(); + + this.initializeParsers(); + this.initializeGenerators(); + this.initializeTemplates(); + } + + // === PARSERS - ANALYSE DE CONTENU BRUT === + + initializeParsers() { + // Parser pour texte libre + this.parsers.set('text', new TextParser()); + + // Parser pour fichiers CSV + this.parsers.set('csv', new CSVParser()); + + // Parser pour JSON structuré + this.parsers.set('json', new JSONParser()); + + // Parser pour dialogue/script + this.parsers.set('dialogue', new DialogueParser()); + + // Parser pour séquences/histoires + this.parsers.set('sequence', new SequenceParser()); + + // Parser pour média (audio/image) + this.parsers.set('media', new MediaParser()); + } + + // === GÉNÉRATEURS - CRÉATION D'EXERCICES === + + initializeGenerators() { + // Générateur de vocabulaire + this.generators.set('vocabulary', new VocabularyGenerator()); + + // Générateur de phrases + this.generators.set('sentence', new SentenceGenerator()); + + // Générateur de dialogues + this.generators.set('dialogue', new DialogueGenerator()); + + // Générateur de séquences + this.generators.set('sequence', new SequenceGenerator()); + + // Générateur de scénarios + this.generators.set('scenario', new ScenarioGenerator()); + + // Générateur automatique (détection de type) + this.generators.set('auto', new AutoGenerator()); + } + + // === TEMPLATES - MODÈLES DE CONTENU === + + initializeTemplates() { + this.templates.set('vocabulary_simple', { + name: 'Vocabulaire Simple', + description: 'Mots avec traduction', + requiredFields: ['english', 'french'], + optionalFields: ['image', 'audio', 'phonetic', 'category'], + interactions: ['click', 'drag_drop', 'type'], + games: ['whack-a-mole', 'memory-game', 'temp-games'] + }); + + this.templates.set('dialogue_conversation', { + name: 'Dialogue Conversationnel', + description: 'Conversation entre personnages', + requiredFields: ['speakers', 'conversation'], + optionalFields: ['scenario', 'context', 'audio_files'], + interactions: ['role_play', 'click', 'build_sentence'], + games: ['story-builder', 'temp-games'] + }); + + this.templates.set('sequence_story', { + name: 'Histoire Séquentielle', + description: 'Étapes chronologiques', + requiredFields: ['title', 'steps'], + optionalFields: ['images', 'times', 'context'], + interactions: ['chronological_order', 'drag_drop'], + games: ['story-builder', 'memory-game'] + }); + + this.templates.set('scenario_context', { + name: 'Scénario Contextuel', + description: 'Situation réelle complexe', + requiredFields: ['setting', 'vocabulary', 'phrases'], + optionalFields: ['roles', 'objectives', 'media'], + interactions: ['simulation', 'role_play', 'click'], + games: ['story-builder', 'temp-games'] + }); + } + + // === MÉTHODE PRINCIPALE - CRÉATION DE CONTENU === + + async createContent(input, options = {}) { + try { + console.log('🏭 Content Factory - Début création contenu'); + + // 1. Analyser l'input + const parsedContent = await this.parseInput(input, options); + + // 2. Détecter le type de contenu + const contentType = this.detectContentType(parsedContent, options); + + // 3. Générer les exercices + const exercises = await this.generateExercises(parsedContent, contentType, options); + + // 4. Traiter les médias + const processedMedia = await this.processMedia(parsedContent.media || [], options); + + // 5. Assembler le module final + const contentModule = this.assembleModule(exercises, processedMedia, options); + + // 6. Valider le résultat + if (!this.validator.validate(contentModule)) { + throw new Error('Contenu généré invalide'); + } + + console.log('✅ Content Factory - Contenu créé avec succès'); + return contentModule; + + } catch (error) { + console.error('❌ Content Factory - Erreur:', error); + throw error; + } + } + + // === PARSING - ANALYSE DES INPUTS === + + async parseInput(input, options) { + const inputType = this.detectInputType(input, options); + const parser = this.parsers.get(inputType); + + if (!parser) { + throw new Error(`Parser non trouvé pour le type: ${inputType}`); + } + + return await parser.parse(input, options); + } + + detectInputType(input, options) { + // Type explicite fourni + if (options.inputType) { + return options.inputType; + } + + // Détection automatique + if (typeof input === 'string') { + if (input.includes(',') && input.includes('=')) { + return 'csv'; + } + if (input.includes(':') && (input.includes('A:') || input.includes('B:'))) { + return 'dialogue'; + } + if (input.includes('1.') || input.includes('First') || input.includes('Then')) { + return 'sequence'; + } + return 'text'; + } + + if (typeof input === 'object') { + if (input.contentItems) return 'json'; + if (input.conversation) return 'dialogue'; + if (input.steps) return 'sequence'; + } + + if (Array.isArray(input)) { + if (input[0]?.english && input[0]?.french) return 'vocabulary'; + if (input[0]?.speaker) return 'dialogue'; + if (input[0]?.order || input[0]?.step) return 'sequence'; + } + + return 'text'; // Fallback + } + + detectContentType(parsedContent, options) { + // Type explicite + if (options.contentType) { + return options.contentType; + } + + // Détection basée sur la structure + if (parsedContent.vocabulary && parsedContent.vocabulary.length > 0) { + return 'vocabulary'; + } + + if (parsedContent.conversation || parsedContent.dialogue) { + return 'dialogue'; + } + + if (parsedContent.steps || parsedContent.sequence) { + return 'sequence'; + } + + if (parsedContent.scenario || parsedContent.setting) { + return 'scenario'; + } + + if (parsedContent.sentences) { + return 'sentence'; + } + + return 'auto'; // Génération automatique + } + + // === GÉNÉRATION D'EXERCICES === + + async generateExercises(parsedContent, contentType, options) { + const generator = this.generators.get(contentType); + + if (!generator) { + throw new Error(`Générateur non trouvé pour le type: ${contentType}`); + } + + return await generator.generate(parsedContent, options); + } + + // === ASSEMBLAGE DU MODULE === + + assembleModule(exercises, media, options) { + const moduleId = options.id || this.generateId(); + const moduleName = options.name || 'Contenu Généré'; + + return { + id: moduleId, + name: moduleName, + description: options.description || `Contenu généré automatiquement - ${new Date().toLocaleDateString()}`, + version: "2.0", + format: "unified", + difficulty: options.difficulty || this.inferDifficulty(exercises), + + metadata: { + totalItems: exercises.length, + categories: this.extractCategories(exercises), + contentTypes: this.extractContentTypes(exercises), + generatedAt: new Date().toISOString(), + sourceType: options.inputType || 'auto-detected' + }, + + config: { + defaultInteraction: options.defaultInteraction || "click", + supportedGames: options.supportedGames || ["whack-a-mole", "memory-game", "temp-games", "story-builder"], + adaptiveEnabled: true, + difficultyProgression: true + }, + + contentItems: exercises, + + media: media, + + categories: this.generateCategories(exercises), + + gameSettings: this.generateGameSettings(exercises, options), + + // Métadonnées de génération + generation: { + timestamp: Date.now(), + factory_version: "1.0", + options: options, + stats: { + parsing_time: 0, // À implémenter + generation_time: 0, + total_time: 0 + } + } + }; + } + + // === UTILITAIRES === + + generateId() { + return 'generated_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); + } + + inferDifficulty(exercises) { + // Analyser la complexité des exercices + const complexities = exercises.map(ex => { + if (ex.type === 'vocabulary') return 1; + if (ex.type === 'sentence') return 2; + if (ex.type === 'dialogue') return 3; + if (ex.type === 'scenario') return 4; + return 2; + }); + + const avgComplexity = complexities.reduce((a, b) => a + b, 0) / complexities.length; + + if (avgComplexity <= 1.5) return 'easy'; + if (avgComplexity <= 2.5) return 'medium'; + return 'hard'; + } + + extractCategories(exercises) { + const categories = new Set(); + exercises.forEach(ex => { + if (ex.category) categories.add(ex.category); + if (ex.content?.tags) { + ex.content.tags.forEach(tag => categories.add(tag)); + } + }); + return Array.from(categories); + } + + extractContentTypes(exercises) { + const types = new Set(); + exercises.forEach(ex => types.add(ex.type)); + return Array.from(types); + } + + generateCategories(exercises) { + const categories = {}; + const categoryGroups = this.groupByCategory(exercises); + + Object.keys(categoryGroups).forEach(cat => { + categories[cat] = { + name: this.beautifyCategory(cat), + icon: this.getCategoryIcon(cat), + description: `Contenu généré pour ${cat}`, + difficulty: this.inferCategoryDifficulty(categoryGroups[cat]), + estimatedTime: Math.ceil(categoryGroups[cat].length * 1.5) + }; + }); + + return categories; + } + + generateGameSettings(exercises, options) { + const types = this.extractContentTypes(exercises); + + return { + whackAMole: { + recommendedWords: Math.min(15, exercises.length), + timeLimit: 60, + maxErrors: 5, + supportedTypes: types.filter(t => ['vocabulary', 'sentence'].includes(t)) + }, + memoryGame: { + recommendedPairs: Math.min(8, Math.floor(exercises.length / 2)), + timeLimit: 120, + maxFlips: 30, + supportedTypes: types + }, + storyBuilder: { + recommendedScenes: Math.min(6, exercises.length), + timeLimit: 180, + supportedTypes: types.filter(t => ['dialogue', 'sequence', 'scenario'].includes(t)) + }, + tempGames: { + recommendedItems: Math.min(10, exercises.length), + timeLimit: 90, + supportedTypes: types + } + }; + } + + // === API PUBLIQUE SIMPLIFIÉE === + + // Créer contenu depuis texte libre + async fromText(text, options = {}) { + return this.createContent(text, { ...options, inputType: 'text' }); + } + + // Créer contenu depuis liste vocabulaire + async fromVocabulary(words, options = {}) { + return this.createContent(words, { ...options, contentType: 'vocabulary' }); + } + + // Créer contenu depuis dialogue + async fromDialogue(dialogue, options = {}) { + return this.createContent(dialogue, { ...options, contentType: 'dialogue' }); + } + + // Créer contenu depuis séquence + async fromSequence(steps, options = {}) { + return this.createContent(steps, { ...options, contentType: 'sequence' }); + } + + // Créer contenu avec médias + async fromMediaBundle(content, mediaFiles, options = {}) { + return this.createContent(content, { + ...options, + media: mediaFiles, + processMedia: true + }); + } + + // === TEMPLATE HELPERS === + + getAvailableTemplates() { + return Array.from(this.templates.entries()).map(([key, template]) => ({ + id: key, + ...template + })); + } + + async fromTemplate(templateId, data, options = {}) { + const template = this.templates.get(templateId); + if (!template) { + throw new Error(`Template non trouvé: ${templateId}`); + } + + // Valider les données selon le template + this.validateTemplateData(data, template); + + return this.createContent(data, { + ...options, + template: template, + requiredFields: template.requiredFields + }); + } + + validateTemplateData(data, template) { + for (const field of template.requiredFields) { + if (!data[field]) { + throw new Error(`Champ requis manquant: ${field}`); + } + } + } + + // === MÉTHODES UTILITAIRES === + + groupByCategory(exercises) { + const groups = {}; + exercises.forEach(ex => { + const cat = ex.category || 'general'; + if (!groups[cat]) groups[cat] = []; + groups[cat].push(ex); + }); + return groups; + } + + beautifyCategory(category) { + const beautified = { + 'family': 'Famille', + 'animals': 'Animaux', + 'colors': 'Couleurs', + 'numbers': 'Nombres', + 'food': 'Nourriture', + 'school': 'École', + 'daily': 'Quotidien', + 'greetings': 'Salutations' + }; + return beautified[category] || category.charAt(0).toUpperCase() + category.slice(1); + } + + getCategoryIcon(category) { + const icons = { + 'family': '👨‍👩‍👧‍👦', + 'animals': '🐱', + 'colors': '🎨', + 'numbers': '🔢', + 'food': '🍎', + 'school': '📚', + 'daily': '🌅', + 'greetings': '👋', + 'general': '📝' + }; + return icons[category] || '📝'; + } + + inferCategoryDifficulty(exercises) { + const difficulties = exercises.map(ex => { + switch(ex.difficulty) { + case 'easy': return 1; + case 'medium': return 2; + case 'hard': return 3; + default: return 2; + } + }); + + const avg = difficulties.reduce((a, b) => a + b, 0) / difficulties.length; + + if (avg <= 1.3) return 'easy'; + if (avg <= 2.3) return 'medium'; + return 'hard'; + } +} + +// === PROCESSEUR DE MÉDIAS === + +class MediaProcessor { + constructor() { + this.supportedAudioFormats = ['mp3', 'wav', 'ogg']; + this.supportedImageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + } + + async processMedia(mediaFiles, options = {}) { + const processedMedia = { + audio: {}, + images: {}, + metadata: { + totalFiles: mediaFiles.length, + processedAt: new Date().toISOString() + } + }; + + for (const file of mediaFiles) { + try { + const processed = await this.processFile(file, options); + + if (this.isAudioFile(file.name)) { + processedMedia.audio[file.id || file.name] = processed; + } else if (this.isImageFile(file.name)) { + processedMedia.images[file.id || file.name] = processed; + } + } catch (error) { + console.warn(`Erreur traitement fichier ${file.name}:`, error); + } + } + + return processedMedia; + } + + async processFile(file, options) { + // Dans un environnement réel, ici on ferait : + // - Validation du format + // - Optimisation (compression, resize) + // - Upload vers CDN + // - Génération de thumbnails + // - Extraction de métadonnées + + return { + originalName: file.name, + path: file.path || `assets/${this.getFileCategory(file.name)}/${file.name}`, + size: file.size, + type: file.type, + processedAt: new Date().toISOString() + }; + } + + isAudioFile(filename) { + const ext = filename.split('.').pop().toLowerCase(); + return this.supportedAudioFormats.includes(ext); + } + + isImageFile(filename) { + const ext = filename.split('.').pop().toLowerCase(); + return this.supportedImageFormats.includes(ext); + } + + getFileCategory(filename) { + return this.isAudioFile(filename) ? 'sounds' : 'images'; + } +} + +// Export global +window.ContentFactory = ContentFactory; +window.MediaProcessor = MediaProcessor; \ No newline at end of file diff --git a/js/core/content-generators.js b/js/core/content-generators.js new file mode 100644 index 0000000..6366b12 --- /dev/null +++ b/js/core/content-generators.js @@ -0,0 +1,864 @@ +// === GÉNÉRATEURS AUTOMATIQUES D'EXERCICES === + +// === GÉNÉRATEUR DE VOCABULAIRE === +class VocabularyGenerator { + async generate(parsedContent, options = {}) { + console.log('📚 VocabularyGenerator - Génération exercices vocabulaire'); + + const exercises = []; + const vocabulary = parsedContent.vocabulary || []; + + vocabulary.forEach((word, index) => { + const exercise = this.createVocabularyExercise(word, index, options); + exercises.push(exercise); + }); + + // Générer des exercices supplémentaires si demandé + if (options.generateVariations) { + exercises.push(...this.generateVariations(vocabulary, options)); + } + + return exercises; + } + + createVocabularyExercise(word, index, options) { + return { + id: `vocab_${index + 1}`, + type: 'vocabulary', + difficulty: this.inferDifficulty(word), + category: word.category || 'general', + + content: { + english: word.english, + french: word.french, + phonetic: word.phonetic || this.generatePhonetic(word.english), + context: word.context || word.category || '', + tags: this.generateTags(word) + }, + + media: { + image: word.image || null, + audio: word.audio || null, + icon: this.getIconForWord(word.english, word.category) + }, + + pedagogy: { + learningObjective: `Apprendre le mot "${word.english}"`, + prerequisites: [], + followUp: this.suggestFollowUp(word), + grammarFocus: 'vocabulary' + }, + + interaction: { + type: options.interactionType || this.selectBestInteraction(word), + validation: 'exact', + hints: this.generateHints(word), + feedback: { + correct: `Parfait ! "${word.english}" = "${word.french}" ${this.getIconForWord(word.english)}`, + incorrect: `Non, "${word.english}" signifie "${word.french}"` + }, + alternatives: this.generateAlternatives(word) + } + }; + } + + inferDifficulty(word) { + const length = word.english.length; + const complexity = this.calculateComplexity(word.english); + + if (length <= 4 && complexity < 2) return 'easy'; + if (length <= 8 && complexity < 3) return 'medium'; + return 'hard'; + } + + calculateComplexity(word) { + let complexity = 0; + if (word.includes('th')) complexity++; + if (word.includes('gh')) complexity++; + if (word.match(/[aeiou]{2,}/)) complexity++; + if (word.split('').some(c => 'xyz'.includes(c))) complexity++; + return complexity; + } + + generateTags(word) { + const tags = [word.category || 'general']; + + // Tags basés sur la longueur + if (word.english.length <= 4) tags.push('short'); + else if (word.english.length >= 8) tags.push('long'); + + // Tags basés sur la catégorie + if (word.category) { + tags.push(word.category); + if (['cat', 'dog', 'bird'].includes(word.english)) tags.push('pet'); + if (['red', 'blue', 'green'].includes(word.english)) tags.push('color'); + } + + return tags; + } + + getIconForWord(word, category) { + const wordIcons = { + cat: '🐱', dog: '🐕', bird: '🐦', fish: '🐠', + apple: '🍎', banana: '🍌', orange: '🍊', + car: '🚗', house: '🏠', school: '🏫', + book: '📚', pen: '✏️', pencil: '✏️' + }; + + if (wordIcons[word.toLowerCase()]) { + return wordIcons[word.toLowerCase()]; + } + + const categoryIcons = { + animals: '🐾', food: '🍎', transport: '🚗', + family: '👨‍👩‍👧‍👦', colors: '🎨', numbers: '🔢' + }; + + return categoryIcons[category] || '📝'; + } + + selectBestInteraction(word) { + // Sélection intelligente du type d'interaction + if (word.category === 'actions') return 'gesture'; + if (word.english.length > 8) return 'type'; + if (word.image) return 'drag_drop'; + return 'click'; + } + + generateHints(word) { + const hints = []; + + // Hint basé sur la catégorie + if (word.category === 'animals') hints.push("C'est un animal"); + if (word.category === 'food') hints.push("On peut le manger"); + if (word.category === 'colors') hints.push("C'est une couleur"); + + // Hint basé sur la longueur + hints.push(`Ce mot a ${word.english.length} lettres`); + + // Hint basé sur la première lettre + hints.push(`Commence par la lettre "${word.english[0].toUpperCase()}"`); + + return hints.slice(0, 3); // Max 3 hints + } + + generateAlternatives(word) { + // Générer des alternatives plausibles pour QCM + const alternatives = []; + + // Pour l'instant, alternatives génériques + // Dans une vraie implémentation, on utiliserait une base de données + const commonWords = { + animals: ['chien', 'oiseau', 'poisson', 'lapin'], + food: ['pomme', 'banane', 'orange', 'pain'], + colors: ['rouge', 'bleu', 'vert', 'jaune'], + family: ['mère', 'père', 'frère', 'sœur'] + }; + + const categoryAlts = commonWords[word.category] || ['mot1', 'mot2', 'mot3']; + + // Prendre 3 alternatives qui ne sont pas la bonne réponse + return categoryAlts.filter(alt => alt !== word.french).slice(0, 3); + } + + suggestFollowUp(word) { + const followUp = []; + + if (word.category === 'animals') { + followUp.push('pets', 'farm_animals', 'wild_animals'); + } else if (word.category === 'family') { + followUp.push('family_relationships', 'family_activities'); + } + + return followUp; + } + + generateVariations(vocabulary, options) { + const variations = []; + + // Générer des exercices de synonymes/antonymes + // Générer des exercices de catégorisation + // Générer des exercices de construction de phrases + + return variations; // Pour l'instant vide + } + + generatePhonetic(word) { + // Génération basique de phonétique + // Dans une vraie implémentation, utiliser une API de phonétique + const basicPhonetic = { + cat: '/kæt/', dog: '/dɔg/', bird: '/bɜrd/', + apple: '/ˈæpəl/', house: '/haʊs/', book: '/bʊk/' + }; + + return basicPhonetic[word.toLowerCase()] || `/${word}/`; + } +} + +// === GÉNÉRATEUR DE PHRASES === +class SentenceGenerator { + async generate(parsedContent, options = {}) { + console.log('📖 SentenceGenerator - Génération exercices phrases'); + + const exercises = []; + const sentences = parsedContent.sentences || []; + + sentences.forEach((sentence, index) => { + const exercise = this.createSentenceExercise(sentence, index, options); + exercises.push(exercise); + }); + + return exercises; + } + + createSentenceExercise(sentence, index, options) { + return { + id: `sent_${index + 1}`, + type: 'sentence', + difficulty: this.inferSentenceDifficulty(sentence), + category: sentence.category || 'general', + + content: { + english: sentence.english, + french: sentence.french || this.suggestTranslation(sentence.english), + structure: this.analyzeSentenceStructure(sentence.english), + context: sentence.context || 'general', + tags: this.generateSentenceTags(sentence) + }, + + media: { + icon: this.getSentenceIcon(sentence) + }, + + pedagogy: { + learningObjective: `Comprendre et construire la phrase "${sentence.english}"`, + prerequisites: this.extractPrerequisites(sentence.english), + followUp: ['complex_sentences', 'questions'], + grammarFocus: this.identifyGrammarFocus(sentence.english) + }, + + interaction: { + type: 'build_sentence', + validation: 'word_order', + hints: this.generateSentenceHints(sentence), + feedback: { + correct: `Excellent ! Tu as construit la phrase correctement ! ✨`, + incorrect: `Essaie de remettre les mots dans le bon ordre` + } + } + }; + } + + inferSentenceDifficulty(sentence) { + const wordCount = sentence.english.split(' ').length; + const structure = sentence.structure || {}; + + if (wordCount <= 4 && !structure.hasQuestion) return 'easy'; + if (wordCount <= 8) return 'medium'; + return 'hard'; + } + + analyzeSentenceStructure(sentence) { + return { + words: sentence.split(' '), + wordCount: sentence.split(' ').length, + hasQuestion: sentence.includes('?'), + hasExclamation: sentence.includes('!'), + hasComma: sentence.includes(','), + startsWithCapital: /^[A-Z]/.test(sentence), + tense: this.detectTense(sentence) + }; + } + + detectTense(sentence) { + if (sentence.includes(' am ') || sentence.includes(' is ') || sentence.includes(' are ')) { + if (sentence.includes('ing')) return 'present_continuous'; + return 'present_simple'; + } + if (sentence.includes(' was ') || sentence.includes(' were ')) return 'past_simple'; + if (sentence.includes(' will ')) return 'future_simple'; + return 'present_simple'; + } + + extractPrerequisites(sentence) { + const prerequisites = []; + const words = sentence.toLowerCase().split(' '); + + // Extraire les mots de vocabulaire importants + const importantWords = words.filter(word => + word.length > 3 && + !['this', 'that', 'with', 'from', 'they', 'have', 'been'].includes(word) + ); + + return importantWords.slice(0, 3); + } + + identifyGrammarFocus(sentence) { + if (sentence.includes('?')) return 'questions'; + if (sentence.includes(' my ') || sentence.includes(' your ')) return 'possessives'; + if (sentence.includes(' is ') || sentence.includes(' are ')) return 'be_verb'; + if (sentence.includes('ing')) return 'present_continuous'; + return 'sentence_structure'; + } + + generateSentenceTags(sentence) { + const tags = ['sentence']; + + if (sentence.english.includes('?')) tags.push('question'); + if (sentence.english.includes('!')) tags.push('exclamation'); + if (sentence.english.toLowerCase().includes('hello')) tags.push('greeting'); + if (sentence.english.toLowerCase().includes('my name')) tags.push('introduction'); + + return tags; + } + + getSentenceIcon(sentence) { + if (sentence.english.includes('?')) return '❓'; + if (sentence.english.includes('!')) return '❗'; + if (sentence.english.toLowerCase().includes('hello')) return '👋'; + return '💬'; + } + + generateSentenceHints(sentence) { + const hints = []; + + hints.push(`La phrase a ${sentence.english.split(' ').length} mots`); + + if (sentence.english.includes('?')) { + hints.push("C'est une question"); + } + + const firstWord = sentence.english.split(' ')[0]; + hints.push(`Commence par "${firstWord}"`); + + return hints; + } + + suggestTranslation(english) { + // Traduction basique pour exemples + const basicTranslations = { + 'Hello': 'Bonjour', + 'My name is': 'Je m\'appelle', + 'I am': 'Je suis', + 'How are you?': 'Comment allez-vous ?', + 'Thank you': 'Merci' + }; + + for (const [en, fr] of Object.entries(basicTranslations)) { + if (english.includes(en)) { + return english.replace(en, fr); + } + } + + return ''; // À traduire manuellement + } +} + +// === GÉNÉRATEUR DE DIALOGUES === +class DialogueGenerator { + async generate(parsedContent, options = {}) { + console.log('💬 DialogueGenerator - Génération exercices dialogues'); + + const exercises = []; + + if (parsedContent.dialogue) { + const exercise = this.createDialogueExercise(parsedContent.dialogue, 0, options); + exercises.push(exercise); + } + + if (parsedContent.conversations) { + parsedContent.conversations.forEach((conversation, index) => { + const exercise = this.createConversationExercise(conversation, index, options); + exercises.push(exercise); + }); + } + + return exercises; + } + + createDialogueExercise(dialogue, index, options) { + return { + id: `dial_${index + 1}`, + type: 'dialogue', + difficulty: this.inferDialogueDifficulty(dialogue), + category: dialogue.scenario || 'conversation', + + content: { + scenario: dialogue.scenario || 'conversation', + english: this.extractDialogueTitle(dialogue), + french: this.translateScenario(dialogue.scenario), + conversation: dialogue.conversation.map(line => ({ + speaker: line.speaker, + english: line.english || line.text, + french: line.french || this.suggestTranslation(line.english || line.text), + role: this.identifyRole(line) + })), + context: dialogue.scenario || 'general', + tags: this.generateDialogueTags(dialogue) + }, + + media: { + icon: this.getDialogueIcon(dialogue.scenario) + }, + + pedagogy: { + learningObjective: `Participer à un dialogue : ${dialogue.scenario}`, + prerequisites: this.extractDialoguePrerequisites(dialogue), + followUp: ['advanced_dialogue', 'role_play'], + grammarFocus: 'conversation_patterns' + }, + + interaction: { + type: 'role_play', + validation: 'dialogue_flow', + userRole: this.selectUserRole(dialogue), + hints: this.generateDialogueHints(dialogue), + feedback: { + correct: `Parfait ! Tu maîtrises ce type de conversation ! 🎭`, + incorrect: `Essaie de suivre le flow naturel de la conversation` + } + } + }; + } + + createConversationExercise(conversation, index, options) { + return { + id: `conv_${index + 1}`, + type: 'dialogue', + difficulty: 'medium', + category: conversation.scene || 'conversation', + + content: { + scenario: conversation.scene || `Conversation ${index + 1}`, + english: `${conversation.speaker}: ${conversation.english}`, + french: `${conversation.speaker}: ${conversation.french}`, + conversation: [{ + speaker: conversation.speaker, + english: conversation.english, + french: conversation.french, + role: 'statement' + }], + tags: ['conversation', 'dialogue'] + }, + + interaction: { + type: 'click', + validation: 'exact' + } + }; + } + + inferDialogueDifficulty(dialogue) { + const conversationLength = dialogue.conversation?.length || 1; + const averageWordsPerLine = dialogue.conversation?.reduce((sum, line) => + sum + (line.english || line.text || '').split(' ').length, 0) / conversationLength || 5; + + if (conversationLength <= 2 && averageWordsPerLine <= 5) return 'easy'; + if (conversationLength <= 4 && averageWordsPerLine <= 8) return 'medium'; + return 'hard'; + } + + extractDialogueTitle(dialogue) { + if (dialogue.title) return dialogue.title; + if (dialogue.scenario) return this.beautifyScenario(dialogue.scenario); + return 'Dialogue'; + } + + beautifyScenario(scenario) { + const scenarios = { + 'greeting': 'Salutations', + 'restaurant': 'Au Restaurant', + 'shopping': 'Faire les Courses', + 'school': 'À l\'École', + 'family': 'En Famille' + }; + + return scenarios[scenario] || scenario; + } + + translateScenario(scenario) { + const translations = { + 'greeting': 'Se saluer', + 'restaurant': 'Commander au restaurant', + 'shopping': 'Acheter quelque chose', + 'school': 'Parler à l\'école', + 'family': 'Conversation familiale' + }; + + return translations[scenario] || scenario; + } + + identifyRole(line) { + const text = (line.english || line.text || '').toLowerCase(); + + if (text.includes('?')) return 'question'; + if (text.includes('hello') || text.includes('hi')) return 'greeting'; + if (text.includes('thank')) return 'thanks'; + if (text.includes('goodbye') || text.includes('bye')) return 'farewell'; + return 'statement'; + } + + generateDialogueTags(dialogue) { + const tags = ['dialogue', 'conversation']; + + if (dialogue.scenario) tags.push(dialogue.scenario); + + const hasQuestion = dialogue.conversation?.some(line => + (line.english || line.text || '').includes('?')); + if (hasQuestion) tags.push('questions'); + + return tags; + } + + getDialogueIcon(scenario) { + const icons = { + 'greeting': '👋', + 'restaurant': '🍽️', + 'shopping': '🛒', + 'school': '🎒', + 'family': '👨‍👩‍👧‍👦', + 'friends': '👫' + }; + + return icons[scenario] || '💬'; + } + + selectUserRole(dialogue) { + // Sélectionner le rôle que l'utilisateur va jouer + const speakers = dialogue.conversation?.map(line => line.speaker) || []; + const uniqueSpeakers = [...new Set(speakers)]; + + // Privilégier certains rôles + if (uniqueSpeakers.includes('student')) return 'student'; + if (uniqueSpeakers.includes('child')) return 'child'; + if (uniqueSpeakers.includes('customer')) return 'customer'; + + // Sinon, prendre le premier + return uniqueSpeakers[0] || 'person1'; + } + + extractDialoguePrerequisites(dialogue) { + const prerequisites = new Set(); + + dialogue.conversation?.forEach(line => { + const text = line.english || line.text || ''; + const words = text.toLowerCase().split(' '); + + // Extraire mots importants + words.forEach(word => { + if (word.length > 3 && !['this', 'that', 'with', 'they', 'have'].includes(word)) { + prerequisites.add(word); + } + }); + }); + + return Array.from(prerequisites).slice(0, 5); + } + + generateDialogueHints(dialogue) { + const hints = []; + + hints.push(`Cette conversation a ${dialogue.conversation?.length || 1} répliques`); + + if (dialogue.scenario) { + hints.push(`Le contexte est : ${dialogue.scenario}`); + } + + const hasQuestions = dialogue.conversation?.some(line => + (line.english || line.text || '').includes('?')); + if (hasQuestions) { + hints.push('Il y a des questions dans cette conversation'); + } + + return hints; + } + + suggestTranslation(english) { + // Utiliser le même système que SentenceGenerator + return new SentenceGenerator().suggestTranslation(english); + } +} + +// === GÉNÉRATEUR DE SÉQUENCES === +class SequenceGenerator { + async generate(parsedContent, options = {}) { + console.log('📋 SequenceGenerator - Génération exercices séquences'); + + const exercises = []; + + if (parsedContent.sequence) { + const exercise = this.createSequenceExercise(parsedContent.sequence, 0, options); + exercises.push(exercise); + } + + return exercises; + } + + createSequenceExercise(sequence, index, options) { + return { + id: `seq_${index + 1}`, + type: 'sequence', + difficulty: this.inferSequenceDifficulty(sequence), + category: this.inferSequenceCategory(sequence), + + content: { + english: sequence.title || 'Sequence', + french: this.translateSequenceTitle(sequence.title), + title: sequence.title || 'Sequence', + steps: sequence.steps.map(step => ({ + order: step.order, + english: step.english, + french: step.french || this.suggestStepTranslation(step.english), + icon: this.getStepIcon(step.english), + time: this.extractTime(step) + })), + context: this.inferContext(sequence), + tags: this.generateSequenceTags(sequence) + }, + + media: { + icon: this.getSequenceIcon(sequence) + }, + + pedagogy: { + learningObjective: `Comprendre l'ordre chronologique : ${sequence.title}`, + prerequisites: this.extractSequencePrerequisites(sequence), + followUp: ['time_expressions', 'daily_routine'], + grammarFocus: 'sequence_connectors' + }, + + interaction: { + type: 'chronological_order', + validation: 'sequence_correct', + hints: this.generateSequenceHints(sequence), + feedback: { + correct: `Parfait ! Tu connais l'ordre correct ! ⏰`, + incorrect: `Réfléchis à l'ordre logique des étapes` + } + } + }; + } + + inferSequenceDifficulty(sequence) { + const stepCount = sequence.steps?.length || 1; + const avgWordsPerStep = sequence.steps?.reduce((sum, step) => + sum + step.english.split(' ').length, 0) / stepCount || 3; + + if (stepCount <= 3 && avgWordsPerStep <= 4) return 'easy'; + if (stepCount <= 5 && avgWordsPerStep <= 6) return 'medium'; + return 'hard'; + } + + inferSequenceCategory(sequence) { + const title = sequence.title?.toLowerCase() || ''; + const allText = sequence.steps?.map(s => s.english.toLowerCase()).join(' ') || ''; + + if (title.includes('morning') || allText.includes('wake up') || allText.includes('breakfast')) { + return 'daily_routine'; + } + if (title.includes('recipe') || allText.includes('cook') || allText.includes('mix')) { + return 'cooking'; + } + if (title.includes('story') || allText.includes('once upon')) { + return 'story'; + } + + return 'sequence'; + } + + translateSequenceTitle(title) { + const translations = { + 'Morning Routine': 'Routine du Matin', + 'Getting Ready': 'Se Préparer', + 'Cooking Recipe': 'Recette de Cuisine', + 'Story': 'Histoire' + }; + + return translations[title] || title; + } + + suggestStepTranslation(english) { + const stepTranslations = { + 'Wake up': 'Se réveiller', + 'Get dressed': 'S\'habiller', + 'Eat breakfast': 'Prendre le petit-déjeuner', + 'Go to school': 'Aller à l\'école', + 'Brush teeth': 'Se brosser les dents' + }; + + return stepTranslations[english] || ''; + } + + getStepIcon(stepText) { + const icons = { + 'wake up': '⏰', 'get dressed': '👕', 'breakfast': '🥞', + 'school': '🎒', 'brush': '🪥', 'wash': '🧼', + 'cook': '🍳', 'mix': '🥄', 'bake': '🔥' + }; + + const lowerText = stepText.toLowerCase(); + + for (const [key, icon] of Object.entries(icons)) { + if (lowerText.includes(key)) return icon; + } + + return '📝'; + } + + extractTime(step) { + // Extraire indication de temps du texte + const timeMatch = step.english.match(/\d{1,2}:\d{2}|\d{1,2}(am|pm)/i); + return timeMatch ? timeMatch[0] : null; + } + + inferContext(sequence) { + const category = this.inferSequenceCategory(sequence); + + const contexts = { + 'daily_routine': 'routine quotidienne', + 'cooking': 'cuisine', + 'story': 'histoire', + 'sequence': 'étapes' + }; + + return contexts[category] || 'séquence'; + } + + generateSequenceTags(sequence) { + const tags = ['sequence', 'chronological']; + + const category = this.inferSequenceCategory(sequence); + tags.push(category); + + if (sequence.steps?.some(step => this.extractTime(step))) { + tags.push('time'); + } + + return tags; + } + + getSequenceIcon(sequence) { + const category = this.inferSequenceCategory(sequence); + + const icons = { + 'daily_routine': '🌅', + 'cooking': '👨‍🍳', + 'story': '📖', + 'sequence': '📋' + }; + + return icons[category] || '📋'; + } + + extractSequencePrerequisites(sequence) { + const prerequisites = new Set(); + + sequence.steps?.forEach(step => { + const words = step.english.toLowerCase().split(' '); + + // Verbes d'action importants + const actionVerbs = words.filter(word => + ['wake', 'get', 'eat', 'go', 'brush', 'wash', 'cook', 'mix'].some(verb => + word.includes(verb) + ) + ); + + actionVerbs.forEach(verb => prerequisites.add(verb)); + }); + + return Array.from(prerequisites).slice(0, 4); + } + + generateSequenceHints(sequence) { + const hints = []; + + hints.push(`Cette séquence a ${sequence.steps?.length || 0} étapes`); + + const category = this.inferSequenceCategory(sequence); + if (category === 'daily_routine') { + hints.push('Pense à ton ordre habituel du matin'); + } else if (category === 'cooking') { + hints.push('Quelle est la logique de la recette ?'); + } + + const firstStep = sequence.steps?.[0]; + if (firstStep) { + hints.push(`La première étape est : "${firstStep.english}"`); + } + + return hints; + } +} + +// === GÉNÉRATEUR AUTOMATIQUE === +class AutoGenerator { + async generate(parsedContent, options = {}) { + console.log('🤖 AutoGenerator - Génération automatique intelligente'); + + const exercises = []; + + // Détecter et générer selon ce qui est disponible + if (parsedContent.vocabulary?.length > 0) { + const vocabGen = new VocabularyGenerator(); + const vocabExercises = await vocabGen.generate(parsedContent, options); + exercises.push(...vocabExercises); + } + + if (parsedContent.sentences?.length > 0) { + const sentGen = new SentenceGenerator(); + const sentExercises = await sentGen.generate(parsedContent, options); + exercises.push(...sentExercises); + } + + if (parsedContent.dialogue) { + const dialogGen = new DialogueGenerator(); + const dialogExercises = await dialogGen.generate(parsedContent, options); + exercises.push(...dialogExercises); + } + + if (parsedContent.sequence) { + const seqGen = new SequenceGenerator(); + const seqExercises = await seqGen.generate(parsedContent, options); + exercises.push(...seqExercises); + } + + // Si rien de spécifique n'a été détecté, créer du contenu basique + if (exercises.length === 0) { + exercises.push(this.createFallbackExercise(parsedContent, options)); + } + + return exercises; + } + + createFallbackExercise(parsedContent, options) { + return { + id: 'auto_001', + type: 'vocabulary', + difficulty: 'easy', + category: 'general', + + content: { + english: 'text', + french: 'texte', + context: 'Contenu généré automatiquement', + tags: ['auto-generated'] + }, + + interaction: { + type: 'click', + validation: 'exact', + feedback: { + correct: 'Bonne réponse !', + incorrect: 'Essaie encore' + } + } + }; + } +} + +// Export global +window.VocabularyGenerator = VocabularyGenerator; +window.SentenceGenerator = SentenceGenerator; +window.DialogueGenerator = DialogueGenerator; +window.SequenceGenerator = SequenceGenerator; +window.AutoGenerator = AutoGenerator; \ No newline at end of file diff --git a/js/core/content-parsers.js b/js/core/content-parsers.js new file mode 100644 index 0000000..cbc0107 --- /dev/null +++ b/js/core/content-parsers.js @@ -0,0 +1,485 @@ +// === PARSERS UNIVERSELS POUR CONTENU === + +// === PARSER DE TEXTE LIBRE === +class TextParser { + async parse(text, options = {}) { + console.log('📝 TextParser - Analyse du texte libre'); + + const result = { + rawText: text, + vocabulary: [], + sentences: [], + dialogue: null, + sequence: null, + metadata: { + wordCount: text.split(' ').length, + language: this.detectLanguage(text), + structure: this.analyzeStructure(text) + } + }; + + // Détecter le type de contenu + if (this.isVocabularyList(text)) { + result.vocabulary = this.parseVocabularyList(text); + } else if (this.isDialogue(text)) { + result.dialogue = this.parseDialogue(text); + } else if (this.isSequence(text)) { + result.sequence = this.parseSequence(text); + } else { + result.sentences = this.parseSentences(text); + } + + return result; + } + + isVocabularyList(text) { + // Recherche patterns: "word = translation", "word: translation", "word - translation" + const patterns = [/\w+\s*[=:-]\s*\w+/g, /\w+\s*=\s*\w+/g]; + return patterns.some(pattern => pattern.test(text)); + } + + parseVocabularyList(text) { + const vocabulary = []; + const lines = text.split('\n').filter(line => line.trim()); + + lines.forEach((line, index) => { + const matches = line.match(/(.+?)\s*[=:-]\s*(.+?)(?:\s*\((.+?)\))?$/); + if (matches) { + const [, english, french, category] = matches; + vocabulary.push({ + english: english.trim(), + french: french.trim(), + category: category?.trim() || 'general', + index: index + }); + } + }); + + return vocabulary; + } + + isDialogue(text) { + // Recherche patterns: "A:", "Person1:", "- Alice:", etc. + return /^[A-Z][^:]*:|^-\s*[A-Z]/m.test(text); + } + + parseDialogue(text) { + const conversation = []; + const lines = text.split('\n').filter(line => line.trim()); + + lines.forEach(line => { + const speakerMatch = line.match(/^(?:-\s*)?([^:]+):\s*(.+)$/); + if (speakerMatch) { + const [, speaker, text] = speakerMatch; + conversation.push({ + speaker: speaker.trim(), + text: text.trim(), + english: text.trim() // À traduire si nécessaire + }); + } + }); + + return { + scenario: 'conversation', + conversation: conversation, + speakers: [...new Set(conversation.map(c => c.speaker))] + }; + } + + isSequence(text) { + // Recherche patterns: "1.", "First", "Then", "Finally", etc. + const sequenceIndicators = /^(\d+\.|\d+\))|first|then|next|after|finally|lastly/mi; + return sequenceIndicators.test(text); + } + + parseSequence(text) { + const steps = []; + const lines = text.split('\n').filter(line => line.trim()); + + lines.forEach((line, index) => { + const stepMatch = line.match(/^(?:(\d+)[\.\)]\s*)?(.+)$/); + if (stepMatch) { + const [, number, stepText] = stepMatch; + steps.push({ + order: number ? parseInt(number) : index + 1, + english: stepText.trim(), + french: '', // À traduire + index: index + }); + } + }); + + return { + title: 'Sequence', + steps: steps.sort((a, b) => a.order - b.order) + }; + } + + parseSentences(text) { + // Séparer en phrases + const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 3); + + return sentences.map((sentence, index) => ({ + english: sentence.trim(), + french: '', // À traduire + index: index, + structure: this.analyzeSentenceStructure(sentence) + })); + } + + detectLanguage(text) { + // Détection simple basée sur des mots courants + const englishWords = ['the', 'and', 'is', 'in', 'to', 'of', 'a', 'that']; + const frenchWords = ['le', 'et', 'est', 'dans', 'de', 'la', 'que', 'un']; + + const words = text.toLowerCase().split(/\s+/); + const englishCount = words.filter(w => englishWords.includes(w)).length; + const frenchCount = words.filter(w => frenchWords.includes(w)).length; + + if (englishCount > frenchCount) return 'english'; + if (frenchCount > englishCount) return 'french'; + return 'mixed'; + } + + analyzeStructure(text) { + return { + hasNumbers: /\d+/.test(text), + hasColons: /:/.test(text), + hasEquals: /=/.test(text), + hasDashes: /-/.test(text), + lineCount: text.split('\n').length, + avgWordsPerLine: text.split('\n').reduce((acc, line) => acc + line.split(' ').length, 0) / text.split('\n').length + }; + } + + analyzeSentenceStructure(sentence) { + return { + wordCount: sentence.split(' ').length, + hasQuestion: sentence.includes('?'), + hasExclamation: sentence.includes('!'), + complexity: sentence.split(' ').length > 10 ? 'complex' : 'simple' + }; + } +} + +// === PARSER CSV === +class CSVParser { + async parse(csvText, options = {}) { + console.log('📊 CSVParser - Analyse CSV'); + + const separator = options.separator || this.detectSeparator(csvText); + const lines = csvText.split('\n').filter(line => line.trim()); + const headers = lines[0].split(separator).map(h => h.trim()); + + const vocabulary = []; + + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(separator).map(v => v.trim()); + const entry = {}; + + headers.forEach((header, index) => { + entry[header.toLowerCase()] = values[index] || ''; + }); + + vocabulary.push(entry); + } + + return { + vocabulary: vocabulary, + headers: headers, + format: 'csv', + separator: separator + }; + } + + detectSeparator(csvText) { + const separators = [',', ';', '\t', '|']; + const firstLine = csvText.split('\n')[0]; + + let maxCount = 0; + let bestSeparator = ','; + + separators.forEach(sep => { + const count = (firstLine.match(new RegExp('\\' + sep, 'g')) || []).length; + if (count > maxCount) { + maxCount = count; + bestSeparator = sep; + } + }); + + return bestSeparator; + } +} + +// === PARSER JSON === +class JSONParser { + async parse(jsonData, options = {}) { + console.log('🔗 JSONParser - Analyse JSON'); + + let data; + if (typeof jsonData === 'string') { + try { + data = JSON.parse(jsonData); + } catch (error) { + throw new Error('JSON invalide: ' + error.message); + } + } else { + data = jsonData; + } + + return { + ...data, + format: 'json', + parsed: true + }; + } +} + +// === PARSER DIALOGUE SPÉCIALISÉ === +class DialogueParser { + async parse(dialogueText, options = {}) { + console.log('💬 DialogueParser - Analyse dialogue'); + + const scenes = this.extractScenes(dialogueText); + const characters = this.extractCharacters(dialogueText); + const conversations = this.parseConversations(dialogueText); + + return { + dialogue: true, + scenes: scenes, + characters: characters, + conversations: conversations, + format: 'dialogue' + }; + } + + extractScenes(text) { + // Rechercher des indications de scène: [Scene], (Scene), etc. + const sceneMatches = text.match(/\[([^\]]+)\]|\(([^)]+)\)/g) || []; + return sceneMatches.map(match => match.replace(/[\[\]()]/g, '')); + } + + extractCharacters(text) { + // Extraire tous les noms avant ":" + const characterMatches = text.match(/^[^:\n]+:/gm) || []; + const characters = new Set(); + + characterMatches.forEach(match => { + const name = match.replace(':', '').trim(); + if (name.length > 0 && name.length < 30) { + characters.add(name); + } + }); + + return Array.from(characters); + } + + parseConversations(text) { + const conversations = []; + const lines = text.split('\n'); + + let currentScene = 'Scene 1'; + + lines.forEach(line => { + line = line.trim(); + + // Détection de nouvelle scène + if (line.match(/\[([^\]]+)\]|\(([^)]+)\)/)) { + currentScene = line.replace(/[\[\]()]/g, ''); + return; + } + + // Détection de dialogue + const dialogueMatch = line.match(/^([^:]+):\s*(.+)$/); + if (dialogueMatch) { + const [, speaker, text] = dialogueMatch; + conversations.push({ + scene: currentScene, + speaker: speaker.trim(), + english: text.trim(), + french: '', // À traduire + timestamp: conversations.length + }); + } + }); + + return conversations; + } +} + +// === PARSER SÉQUENCE SPÉCIALISÉ === +class SequenceParser { + async parse(sequenceText, options = {}) { + console.log('📋 SequenceParser - Analyse séquence'); + + const title = this.extractTitle(sequenceText); + const steps = this.extractSteps(sequenceText); + const timeline = this.extractTimeline(sequenceText); + + return { + sequence: true, + title: title, + steps: steps, + timeline: timeline, + format: 'sequence' + }; + } + + extractTitle(text) { + // Chercher un titre en début de texte + const lines = text.split('\n'); + const firstLine = lines[0].trim(); + + // Si la première ligne ne commence pas par un numéro, c'est probablement le titre + if (!firstLine.match(/^\d+/)) { + return firstLine; + } + + return 'Sequence'; + } + + extractSteps(text) { + const steps = []; + const lines = text.split('\n').filter(line => line.trim()); + + lines.forEach((line, index) => { + // Ignorer la première ligne si c'est le titre + if (index === 0 && !line.match(/^\d+/)) { + return; + } + + const stepPatterns = [ + /^(\d+)[\.\)]\s*(.+)$/, // "1. Step text" + /^(First|Then|Next|After|Finally|Lastly)[:.]?\s*(.+)$/i, // "First: text" + /^(.+)$/ // Fallback: toute ligne + ]; + + for (let pattern of stepPatterns) { + const match = line.match(pattern); + if (match) { + let [, indicator, stepText] = match; + + if (!stepText) { + stepText = indicator; + indicator = (steps.length + 1).toString(); + } + + steps.push({ + order: this.normalizeStepNumber(indicator, steps.length + 1), + english: stepText.trim(), + french: '', // À traduire + indicator: indicator, + rawLine: line + }); + break; + } + } + }); + + return steps.sort((a, b) => a.order - b.order); + } + + normalizeStepNumber(indicator, fallback) { + if (/^\d+$/.test(indicator)) { + return parseInt(indicator); + } + + const wordNumbers = { + 'first': 1, 'second': 2, 'third': 3, 'fourth': 4, 'fifth': 5, + 'then': fallback, 'next': fallback, 'after': fallback, + 'finally': 999, 'lastly': 999 + }; + + return wordNumbers[indicator.toLowerCase()] || fallback; + } + + extractTimeline(text) { + // Rechercher des indications de temps: "7:00", "at 8pm", "in the morning" + const timeMatches = text.match(/\d{1,2}:\d{2}|\d{1,2}(am|pm)|morning|afternoon|evening|night/gi) || []; + return timeMatches; + } +} + +// === PARSER MÉDIA === +class MediaParser { + async parse(mediaData, options = {}) { + console.log('🎵 MediaParser - Analyse médias'); + + const result = { + audio: [], + images: [], + metadata: {}, + format: 'media' + }; + + if (Array.isArray(mediaData)) { + mediaData.forEach(file => { + if (this.isAudioFile(file)) { + result.audio.push(this.parseAudioFile(file)); + } else if (this.isImageFile(file)) { + result.images.push(this.parseImageFile(file)); + } + }); + } + + return result; + } + + isAudioFile(file) { + const audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'flac']; + const extension = this.getFileExtension(file.name || file); + return audioExtensions.includes(extension.toLowerCase()); + } + + isImageFile(file) { + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']; + const extension = this.getFileExtension(file.name || file); + return imageExtensions.includes(extension.toLowerCase()); + } + + getFileExtension(filename) { + return filename.split('.').pop() || ''; + } + + parseAudioFile(file) { + return { + name: file.name, + path: file.path || file.name, + type: 'audio', + extension: this.getFileExtension(file.name), + associatedWord: this.extractWordFromFilename(file.name), + metadata: { + size: file.size, + duration: file.duration || null + } + }; + } + + parseImageFile(file) { + return { + name: file.name, + path: file.path || file.name, + type: 'image', + extension: this.getFileExtension(file.name), + associatedWord: this.extractWordFromFilename(file.name), + metadata: { + size: file.size, + width: file.width || null, + height: file.height || null + } + }; + } + + extractWordFromFilename(filename) { + // Extraire le mot du nom de fichier: "cat.mp3" -> "cat" + return filename.split('.')[0].replace(/[_-]/g, ' ').trim(); + } +} + +// Export global +window.TextParser = TextParser; +window.CSVParser = CSVParser; +window.JSONParser = JSONParser; +window.DialogueParser = DialogueParser; +window.SequenceParser = SequenceParser; +window.MediaParser = MediaParser; \ No newline at end of file diff --git a/js/core/content-scanner.js b/js/core/content-scanner.js new file mode 100644 index 0000000..8bb86b6 --- /dev/null +++ b/js/core/content-scanner.js @@ -0,0 +1,379 @@ +// === SCANNER AUTOMATIQUE DE CONTENU === + +class ContentScanner { + constructor() { + this.discoveredContent = new Map(); + this.contentFiles = [ + // Liste des fichiers de contenu à scanner automatiquement + 'sbs-level-7-8-new.js', + 'basic-chinese.js', + 'english-class-demo.js' + ]; + } + + async scanAllContent() { + console.log('🔍 ContentScanner - Scan automatique du contenu...'); + + const results = { + found: [], + errors: [], + total: 0 + }; + + for (const filename of this.contentFiles) { + try { + const contentInfo = await this.scanContentFile(filename); + if (contentInfo) { + this.discoveredContent.set(contentInfo.id, contentInfo); + results.found.push(contentInfo); + } + } catch (error) { + console.warn(`⚠️ Erreur scan ${filename}:`, error.message); + results.errors.push({ filename, error: error.message }); + } + } + + results.total = results.found.length; + console.log(`✅ Scan terminé: ${results.total} modules trouvés`); + + return results; + } + + async scanContentFile(filename) { + const contentId = this.extractContentId(filename); + const moduleName = this.getModuleName(contentId); + + try { + // Charger le script si pas déjà fait + await this.loadScript(`js/content/${filename}`); + + // Vérifier si le module existe + if (!window.ContentModules || !window.ContentModules[moduleName]) { + throw new Error(`Module ${moduleName} non trouvé après chargement`); + } + + const module = window.ContentModules[moduleName]; + + // Extraire les métadonnées + const contentInfo = this.extractContentInfo(module, contentId, filename); + + console.log(`📦 Contenu découvert: ${contentInfo.name}`); + return contentInfo; + + } catch (error) { + throw new Error(`Impossible de charger ${filename}: ${error.message}`); + } + } + + extractContentInfo(module, contentId, filename) { + return { + id: contentId, + filename: filename, + name: module.name || this.beautifyContentId(contentId), + description: module.description || 'Contenu automatiquement détecté', + icon: this.getContentIcon(module, contentId), + difficulty: module.difficulty || 'medium', + enabled: true, + + // Métadonnées détaillées + metadata: { + version: module.version || '1.0', + format: module.format || 'legacy', + totalItems: this.countItems(module), + categories: this.extractCategories(module), + contentTypes: this.extractContentTypes(module), + estimatedTime: this.calculateEstimatedTime(module), + lastScanned: new Date().toISOString() + }, + + // Statistiques + stats: { + vocabularyCount: this.countByType(module, 'vocabulary'), + sentenceCount: this.countByType(module, 'sentence'), + dialogueCount: this.countByType(module, 'dialogue'), + grammarCount: this.countByType(module, 'grammar') + }, + + // Configuration pour les jeux + gameCompatibility: this.analyzeGameCompatibility(module) + }; + } + + extractContentId(filename) { + return filename.replace('.js', '').toLowerCase(); + } + + getModuleName(contentId) { + const mapping = { + 'sbs-level-7-8-new': 'SBSLevel78New' + }; + return mapping[contentId] || this.toPascalCase(contentId); + } + + toPascalCase(str) { + return str.split('-').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(''); + } + + beautifyContentId(contentId) { + const beautified = { + 'sbs-level-7-8-new': 'SBS Level 7-8 (Simple Format)' + }; + return beautified[contentId] || contentId.charAt(0).toUpperCase() + contentId.slice(1); + } + + getContentIcon(module, contentId) { + // Icône du module si disponible + if (module.icon) return module.icon; + + // Icônes par défaut selon l'ID + const defaultIcons = { + 'sbs-level-7-8-new': '✨' + }; + + return defaultIcons[contentId] || '📝'; + } + + countItems(module) { + let count = 0; + + // Format moderne (contentItems) + if (module.contentItems && Array.isArray(module.contentItems)) { + return module.contentItems.length; + } + + // Format simple (vocabulary object + sentences array) + if (module.vocabulary && typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) { + count += Object.keys(module.vocabulary).length; + } + // Format legacy (vocabulary array) + else if (module.vocabulary && Array.isArray(module.vocabulary)) { + count += module.vocabulary.length; + } + + // Autres contenus + if (module.sentences && Array.isArray(module.sentences)) count += module.sentences.length; + if (module.dialogues && Array.isArray(module.dialogues)) count += module.dialogues.length; + if (module.phrases && Array.isArray(module.phrases)) count += module.phrases.length; + if (module.texts && Array.isArray(module.texts)) count += module.texts.length; + + return count; + } + + extractCategories(module) { + const categories = new Set(); + + if (module.categories) { + Object.keys(module.categories).forEach(cat => categories.add(cat)); + } + + if (module.metadata && module.metadata.categories) { + module.metadata.categories.forEach(cat => categories.add(cat)); + } + + // Extraire des contenus si format moderne + if (module.contentItems) { + module.contentItems.forEach(item => { + if (item.category) categories.add(item.category); + }); + } + + // Extraire du vocabulaire selon le format + if (module.vocabulary) { + // Format simple (vocabulary object) + if (typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) { + // Pour l'instant, pas de catégories dans le format simple + categories.add('vocabulary'); + } + // Format legacy (vocabulary array) + else if (Array.isArray(module.vocabulary)) { + module.vocabulary.forEach(word => { + if (word.category) categories.add(word.category); + }); + } + } + + return Array.from(categories); + } + + extractContentTypes(module) { + const types = new Set(); + + if (module.contentItems) { + module.contentItems.forEach(item => { + if (item.type) types.add(item.type); + }); + } else { + // Format legacy - deviner les types + if (module.vocabulary) types.add('vocabulary'); + if (module.sentences) types.add('sentence'); + if (module.dialogues || module.dialogue) types.add('dialogue'); + if (module.phrases) types.add('sentence'); + } + + return Array.from(types); + } + + calculateEstimatedTime(module) { + if (module.metadata && module.metadata.estimatedTime) { + return module.metadata.estimatedTime; + } + + // Calcul basique : 1 minute par 3 éléments + const itemCount = this.countItems(module); + return Math.max(5, Math.ceil(itemCount / 3)); + } + + countByType(module, type) { + if (module.contentItems) { + return module.contentItems.filter(item => item.type === type).length; + } + + // Format simple et legacy + switch(type) { + case 'vocabulary': + // Format simple (vocabulary object) + if (module.vocabulary && typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) { + return Object.keys(module.vocabulary).length; + } + // Format legacy (vocabulary array) + return module.vocabulary ? module.vocabulary.length : 0; + + case 'sentence': + return (module.sentences ? module.sentences.length : 0) + + (module.phrases ? module.phrases.length : 0); + + case 'dialogue': + return module.dialogues ? module.dialogues.length : 0; + + case 'grammar': + // Format simple (grammar object avec sous-propriétés) + if (module.grammar && typeof module.grammar === 'object') { + return Object.keys(module.grammar).length; + } + return module.grammar && Array.isArray(module.grammar) ? module.grammar.length : 0; + + default: + return 0; + } + } + + analyzeGameCompatibility(module) { + const compatibility = { + 'whack-a-mole': { compatible: false, score: 0 }, + 'memory-game': { compatible: false, score: 0 }, + 'story-builder': { compatible: false, score: 0 }, + 'temp-games': { compatible: false, score: 0 } + }; + + const vocabCount = this.countByType(module, 'vocabulary'); + const sentenceCount = this.countByType(module, 'sentence'); + const dialogueCount = this.countByType(module, 'dialogue'); + + // Whack-a-Mole - aime le vocabulaire et phrases simples + if (vocabCount > 5 || sentenceCount > 3) { + compatibility['whack-a-mole'].compatible = true; + compatibility['whack-a-mole'].score = Math.min(100, vocabCount * 5 + sentenceCount * 3); + } + + // Memory Game - parfait pour vocabulaire avec images + if (vocabCount > 4) { + compatibility['memory-game'].compatible = true; + compatibility['memory-game'].score = Math.min(100, vocabCount * 8); + } + + // Story Builder - aime les dialogues et séquences + if (dialogueCount > 0 || sentenceCount > 5) { + compatibility['story-builder'].compatible = true; + compatibility['story-builder'].score = Math.min(100, dialogueCount * 15 + sentenceCount * 2); + } + + // Temp Games - accepte tout + if (this.countItems(module) > 3) { + compatibility['temp-games'].compatible = true; + compatibility['temp-games'].score = Math.min(100, this.countItems(module) * 2); + } + + return compatibility; + } + + async loadScript(src) { + return new Promise((resolve, reject) => { + // Vérifier si déjà chargé + const existingScript = document.querySelector(`script[src="${src}"]`); + if (existingScript) { + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = src; + script.onload = resolve; + script.onerror = () => reject(new Error(`Impossible de charger ${src}`)); + document.head.appendChild(script); + }); + } + + // === API PUBLIQUE === + + async getAvailableContent() { + if (this.discoveredContent.size === 0) { + await this.scanAllContent(); + } + return Array.from(this.discoveredContent.values()); + } + + async getContentById(id) { + if (this.discoveredContent.size === 0) { + await this.scanAllContent(); + } + return this.discoveredContent.get(id); + } + + async getContentByGame(gameType) { + const allContent = await this.getAvailableContent(); + + return allContent.filter(content => { + const compat = content.gameCompatibility[gameType]; + return compat && compat.compatible; + }).sort((a, b) => { + // Trier par score de compatibilité + const scoreA = a.gameCompatibility[gameType].score; + const scoreB = b.gameCompatibility[gameType].score; + return scoreB - scoreA; + }); + } + + async refreshContent() { + this.discoveredContent.clear(); + return await this.scanAllContent(); + } + + getContentStats() { + const stats = { + totalModules: this.discoveredContent.size, + totalItems: 0, + categories: new Set(), + contentTypes: new Set(), + difficulties: new Set() + }; + + for (const content of this.discoveredContent.values()) { + stats.totalItems += content.metadata.totalItems; + content.metadata.categories.forEach(cat => stats.categories.add(cat)); + content.metadata.contentTypes.forEach(type => stats.contentTypes.add(type)); + stats.difficulties.add(content.difficulty); + } + + return { + ...stats, + categories: Array.from(stats.categories), + contentTypes: Array.from(stats.contentTypes), + difficulties: Array.from(stats.difficulties) + }; + } +} + +// Export global +window.ContentScanner = ContentScanner; \ No newline at end of file diff --git a/js/core/game-loader.js b/js/core/game-loader.js new file mode 100644 index 0000000..2a01995 --- /dev/null +++ b/js/core/game-loader.js @@ -0,0 +1,340 @@ +// === CHARGEUR DE JEUX DYNAMIQUE === + +const GameLoader = { + currentGame: null, + contentScanner: new ContentScanner(), + loadedModules: { + games: {}, + content: {} + }, + + async loadGame(gameType, contentType) { + try { + // Nettoyage du jeu précédent + this.cleanup(); + + // Chargement parallèle du module de jeu et du contenu + const [gameModule, contentModule] = await Promise.all([ + this.loadGameModule(gameType), + this.loadContentModule(contentType) + ]); + + // Initialisation du jeu + this.initGame(gameType, gameModule, contentModule); + + } catch (error) { + console.error('Erreur lors du chargement du jeu:', error); + throw error; + } + }, + + async loadGameModule(gameType) { + // Vérifier si le module est déjà chargé + if (this.loadedModules.games[gameType]) { + return this.loadedModules.games[gameType]; + } + + try { + // Chargement dynamique du script + await this.loadScript(`js/games/${gameType}.js`); + + // Récupération du module depuis l'objet global + const module = window.GameModules?.[this.getModuleName(gameType)]; + + if (!module) { + throw new Error(`Module de jeu ${gameType} non trouvé`); + } + + // Cache du module + this.loadedModules.games[gameType] = module; + return module; + + } catch (error) { + console.error(`Erreur chargement module jeu ${gameType}:`, error); + throw error; + } + }, + + async loadContentModule(contentType) { + // Utiliser le ContentScanner pour récupérer le contenu découvert + try { + // Récupérer le contenu déjà découvert par le scanner + const contentInfo = await this.contentScanner.getContentById(contentType); + + if (!contentInfo) { + throw new Error(`Contenu ${contentType} non trouvé par le scanner`); + } + + // Charger le module JavaScript correspondant + await this.loadScript(`js/content/${contentInfo.filename}`); + + // Récupérer le module depuis l'objet global + const moduleName = this.getContentModuleName(contentType); + const rawModule = window.ContentModules?.[moduleName]; + + if (!rawModule) { + throw new Error(`Module ${moduleName} non trouvé après chargement`); + } + + // Combiner les informations du scanner avec le contenu brut + const enrichedContent = { + ...rawModule, + ...contentInfo, + // S'assurer que le contenu brut du module est disponible + rawContent: rawModule + }; + + this.loadedModules.content[contentType] = enrichedContent; + return enrichedContent; + + } catch (error) { + console.error(`Erreur chargement contenu ${contentType}:`, error); + throw error; + } + }, + + loadScript(src) { + return new Promise((resolve, reject) => { + // Vérifier si le script est déjà chargé + const existingScript = document.querySelector(`script[src="${src}"]`); + if (existingScript) { + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = src; + script.onload = resolve; + script.onerror = () => reject(new Error(`Impossible de charger ${src}`)); + document.head.appendChild(script); + }); + }, + + initGame(gameType, GameClass, contentData) { + const gameContainer = document.getElementById('game-container'); + const gameTitle = document.getElementById('game-title'); + const scoreDisplay = document.getElementById('current-score'); + + // Le contenu est déjà enrichi par le ContentScanner, pas besoin d'adaptation supplémentaire + const adaptedContent = contentData; + + // Mise à jour du titre + const contentName = adaptedContent.name || contentType; + gameTitle.textContent = this.getGameTitle(gameType, contentName); + + // Réinitialisation du score + scoreDisplay.textContent = '0'; + + // Création de l'instance de jeu avec contenu enrichi + this.currentGame = new GameClass({ + container: gameContainer, + content: adaptedContent, + contentScanner: this.contentScanner, // Passer le scanner pour accès aux métadonnées + onScoreUpdate: (score) => this.updateScore(score), + onGameEnd: (finalScore) => this.handleGameEnd(finalScore) + }); + + // Démarrage du jeu + this.currentGame.start(); + }, + + updateScore(score) { + const scoreDisplay = document.getElementById('current-score'); + scoreDisplay.textContent = score.toString(); + + // Animation du score + Utils.animateElement(scoreDisplay, 'pulse', 200); + }, + + handleGameEnd(finalScore) { + // Sauvegarde du score + this.saveScore(finalScore); + + // Affichage du résultat + Utils.showToast(`Jeu terminé ! Score final: ${finalScore}`, 'success'); + + // Afficher les options de fin de jeu + this.showGameEndOptions(finalScore); + }, + + showGameEndOptions(finalScore) { + const gameContainer = document.getElementById('game-container'); + + // Créer l'overlay de fin de jeu + const endOverlay = document.createElement('div'); + endOverlay.className = 'game-end-overlay'; + endOverlay.innerHTML = ` +
+

🎉 Jeu Terminé !

+
Score final: ${finalScore}
+
Meilleur score: ${this.getBestScoreForCurrentGame()}
+
+ + + +
+
+ `; + + gameContainer.appendChild(endOverlay); + + // Ajouter les event listeners + endOverlay.querySelector('.restart-game-btn').addEventListener('click', () => { + this.removeGameEndOverlay(); + this.restartCurrentGame(); + }); + + endOverlay.querySelector('.back-to-levels-btn').addEventListener('click', () => { + const params = Utils.getUrlParams(); + AppNavigation.navigateTo('levels', params.game); + }); + + endOverlay.querySelector('.back-to-games-btn').addEventListener('click', () => { + AppNavigation.navigateTo('games'); + }); + + // Fermer avec ESC + const handleEscape = (e) => { + if (e.key === 'Escape') { + this.removeGameEndOverlay(); + AppNavigation.goBack(); + document.removeEventListener('keydown', handleEscape); + } + }; + document.addEventListener('keydown', handleEscape); + }, + + removeGameEndOverlay() { + const overlay = document.querySelector('.game-end-overlay'); + if (overlay) { + overlay.remove(); + } + }, + + getBestScoreForCurrentGame() { + const params = Utils.getUrlParams(); + return this.getBestScore(params.game, params.content); + }, + + restartCurrentGame() { + if (this.currentGame && this.currentGame.restart) { + this.currentGame.restart(); + document.getElementById('current-score').textContent = '0'; + } + }, + + cleanup() { + if (this.currentGame && this.currentGame.destroy) { + this.currentGame.destroy(); + } + + // Supprimer l'overlay de fin de jeu s'il existe + this.removeGameEndOverlay(); + + const gameContainer = document.getElementById('game-container'); + gameContainer.innerHTML = ''; + + this.currentGame = null; + }, + + saveScore(score) { + const params = Utils.getUrlParams(); + const scoreKey = `score_${params.game}_${params.content}`; + const currentScores = Utils.storage.get(scoreKey, []); + + currentScores.push({ + score: score, + date: new Date().toISOString(), + timestamp: Date.now() + }); + + // Garder seulement les 10 meilleurs scores + currentScores.sort((a, b) => b.score - a.score); + const bestScores = currentScores.slice(0, 10); + + Utils.storage.set(scoreKey, bestScores); + }, + + getBestScore(gameType, contentType) { + const scoreKey = `score_${gameType}_${contentType}`; + const scores = Utils.storage.get(scoreKey, []); + return scores.length > 0 ? scores[0].score : 0; + }, + + // Utilitaires de nommage + getModuleName(gameType) { + const names = { + 'whack-a-mole': 'WhackAMole', + 'whack-a-mole-hard': 'WhackAMoleHard', + 'memory-match': 'MemoryMatch', + 'quiz-game': 'QuizGame', + 'temp-games': 'TempGames', + 'fill-the-blank': 'FillTheBlank', + 'text-reader': 'TextReader', + 'adventure-reader': 'AdventureReader', + 'chinese-study': 'ChineseStudy' + }; + return names[gameType] || gameType; + }, + + getContentModuleName(contentType) { + // Utilise la même logique que le ContentScanner + const mapping = { + 'sbs-level-7-8-new': 'SBSLevel78New', + 'basic-chinese': 'BasicChinese', + 'english-class-demo': 'EnglishClassDemo' + }; + return mapping[contentType] || this.toPascalCase(contentType); + }, + + toPascalCase(str) { + return str.split('-').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(''); + }, + + getGameTitle(gameType, contentName) { + const gameNames = { + 'whack-a-mole': 'Whack-a-Mole', + 'whack-a-mole-hard': 'Whack-a-Mole Hard', + 'memory-match': 'Memory Match', + 'quiz-game': 'Quiz Game', + 'temp-games': 'Mini-Jeux', + 'fill-the-blank': 'Fill the Blank', + 'text-reader': 'Text Reader', + 'adventure-reader': 'Adventure Reader' + }; + + const gameName = gameNames[gameType] || gameType; + return `${gameName} - ${contentName}`; + }, + + // API pour les jeux + createGameAPI() { + return { + showFeedback: (message, type = 'info') => Utils.showToast(message, type), + playSound: (soundFile) => this.playSound(soundFile), + updateScore: (score) => this.updateScore(score), + endGame: (score) => this.handleGameEnd(score), + getBestScore: () => { + const params = Utils.getUrlParams(); + return this.getBestScore(params.game, params.content); + } + }; + }, + + playSound(soundFile) { + if (Utils.canPlayAudio()) { + try { + const audio = new Audio(`assets/sounds/${soundFile}`); + audio.volume = 0.5; + audio.play().catch(e => console.warn('Cannot play sound:', e)); + } catch (error) { + console.warn('Sound error:', error); + } + } + } +}; + +// Export global +window.GameLoader = GameLoader; \ No newline at end of file diff --git a/js/core/navigation.js b/js/core/navigation.js new file mode 100644 index 0000000..ee5d911 --- /dev/null +++ b/js/core/navigation.js @@ -0,0 +1,495 @@ +// === SYSTÈME DE NAVIGATION === + +const AppNavigation = { + currentPage: 'home', + navigationHistory: ['home'], + gamesConfig: null, + contentScanner: new ContentScanner(), + scannedContent: null, + + init() { + this.loadGamesConfig(); + this.initContentScanner(); + this.setupEventListeners(); + this.handleInitialRoute(); + }, + + async loadGamesConfig() { + // Utilisation directe de la config par défaut (pas de fetch) + console.log('📁 Utilisation de la configuration par défaut'); + this.gamesConfig = this.getDefaultConfig(); + }, + + async initContentScanner() { + try { + console.log('🔍 Initialisation du scanner de contenu...'); + this.scannedContent = await this.contentScanner.scanAllContent(); + console.log(`✅ ${this.scannedContent.found.length} modules de contenu détectés automatiquement`); + } catch (error) { + console.error('Erreur scan contenu:', error); + } + }, + + getDefaultConfig() { + return { + games: { + 'whack-a-mole': { + enabled: true, + name: 'Whack-a-Mole', + icon: '🔨', + description: 'Tape sur les bonnes réponses !' + }, + 'whack-a-mole-hard': { + enabled: true, + name: 'Whack-a-Mole Hard', + icon: '💥', + description: '3 moles at once, 5x3 grid, harder!' + }, + 'memory-match': { + enabled: true, + name: 'Memory Match', + icon: '🧠', + description: 'Find matching English-French pairs!' + }, + 'quiz-game': { + enabled: true, + name: 'Quiz Game', + icon: '❓', + description: 'Answer vocabulary questions!' + }, + 'temp-games': { + enabled: true, + name: 'Jeux Temporaires', + icon: '🎯', + description: 'Mini-jeux en développement' + }, + 'fill-the-blank': { + enabled: true, + name: 'Fill the Blank', + icon: '📝', + description: 'Complète les phrases en remplissant les blancs !' + }, + 'text-reader': { + enabled: true, + name: 'Text Reader', + icon: '📖', + description: 'Read texts sentence by sentence' + }, + 'adventure-reader': { + enabled: true, + name: 'Adventure Reader', + icon: '⚔️', + description: 'Zelda-style adventure with vocabulary!' + } + }, + content: { + 'sbs-level-8': { + enabled: true, + name: 'SBS Level 8', + icon: '📚', + description: 'Vocabulaire manuel SBS' + }, + 'animals': { + enabled: false, + name: 'Animals', + icon: '🐱', + description: 'Vocabulaire des animaux' + }, + 'colors': { + enabled: false, + name: 'Colors & Numbers', + icon: '🌈', + description: 'Couleurs et nombres' + } + } + }; + }, + + setupEventListeners() { + // Navigation par URL + window.addEventListener('popstate', () => { + this.handleInitialRoute(); + }); + + // Raccourcis clavier + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + this.goBack(); + } + }); + + // Scroll pour masquer/afficher la breadcrumb + this.setupScrollBehavior(); + }, + + handleInitialRoute() { + const params = Utils.getUrlParams(); + + if (params.page === 'play' && params.game && params.content) { + this.showGamePage(params.game, params.content); + } else if (params.page === 'levels' && params.game) { + this.showLevelsPage(params.game); + } else if (params.page === 'games') { + this.showGamesPage(); + } else { + this.showHomePage(); + } + }, + + setupScrollBehavior() { + let lastScrollY = window.scrollY; + let breadcrumb = null; + + const handleScroll = () => { + if (!breadcrumb) { + breadcrumb = document.querySelector('.breadcrumb'); + if (!breadcrumb) return; + } + + const currentScrollY = window.scrollY; + + // Si on scroll vers le bas et qu'on a scrollé plus de 50px + if (currentScrollY > lastScrollY && currentScrollY > 50) { + breadcrumb.classList.add('hidden'); + breadcrumb.classList.remove('visible'); + } + // Si on scroll vers le haut ou qu'on est près du top + else if (currentScrollY < lastScrollY || currentScrollY <= 50) { + breadcrumb.classList.remove('hidden'); + breadcrumb.classList.add('visible'); + } + + lastScrollY = currentScrollY; + }; + + // Throttle scroll event pour les performances + let ticking = false; + window.addEventListener('scroll', () => { + if (!ticking) { + requestAnimationFrame(() => { + handleScroll(); + ticking = false; + }); + ticking = true; + } + }); + }, + + // Navigation vers une page + navigateTo(page, game = null, content = null) { + const params = { page }; + if (game) params.game = game; + if (content) params.content = content; + + Utils.setUrlParams(params); + + // Mise à jour historique + if (this.currentPage !== page) { + this.navigationHistory.push(page); + } + + this.currentPage = page; + + // Affichage de la page appropriée + switch(page) { + case 'games': + this.showGamesPage(); + break; + case 'levels': + this.showLevelsPage(game); + break; + case 'play': + this.showGamePage(game, content); + break; + default: + this.showHomePage(); + } + + this.updateBreadcrumb(); + }, + + // Retour en arrière + goBack() { + if (this.navigationHistory.length > 1) { + this.navigationHistory.pop(); // Retirer la page actuelle + const previousPage = this.navigationHistory[this.navigationHistory.length - 1]; + + const params = Utils.getUrlParams(); + + if (previousPage === 'levels') { + this.navigateTo('levels', params.game); + } else if (previousPage === 'games') { + this.navigateTo('games'); + } else { + this.navigateTo('home'); + } + } + }, + + // Affichage page d'accueil + showHomePage() { + this.hideAllPages(); + document.getElementById('home-page').classList.add('active'); + this.currentPage = 'home'; + }, + + // Affichage page sélection jeux + showGamesPage() { + this.hideAllPages(); + document.getElementById('games-page').classList.add('active'); + this.renderGamesGrid(); + this.currentPage = 'games'; + }, + + // Affichage page sélection niveaux + showLevelsPage(gameType) { + this.hideAllPages(); + document.getElementById('levels-page').classList.add('active'); + this.renderLevelsGrid(gameType); + this.currentPage = 'levels'; + + // Mise à jour de la description + const gameInfo = this.gamesConfig?.games[gameType]; + if (gameInfo) { + document.getElementById('level-description').textContent = + `Sélectionne le contenu pour jouer à ${gameInfo.name}`; + } + }, + + // Affichage page de jeu + async showGamePage(gameType, contentType) { + this.hideAllPages(); + document.getElementById('game-page').classList.add('active'); + this.currentPage = 'play'; + + Utils.showLoading(); + + try { + await GameLoader.loadGame(gameType, contentType); + } catch (error) { + console.error('Erreur chargement jeu:', error); + Utils.showToast('Erreur lors du chargement du jeu', 'error'); + this.goBack(); + } finally { + Utils.hideLoading(); + } + }, + + // Masquer toutes les pages + hideAllPages() { + document.querySelectorAll('.page').forEach(page => { + page.classList.remove('active'); + }); + }, + + // Rendu grille des jeux + renderGamesGrid() { + const grid = document.getElementById('games-grid'); + grid.innerHTML = ''; + + if (!this.gamesConfig) return; + + Object.entries(this.gamesConfig.games).forEach(([key, game]) => { + if (game.enabled) { + const card = this.createGameCard(key, game); + grid.appendChild(card); + } + }); + }, + + // Création d'une carte de jeu + createGameCard(gameKey, gameInfo) { + const card = document.createElement('div'); + card.className = 'game-card'; + card.innerHTML = ` +
${gameInfo.icon}
+
${gameInfo.name}
+
${gameInfo.description}
+ `; + + card.addEventListener('click', () => { + Utils.animateElement(card, 'pulse'); + this.navigateTo('levels', gameKey); + }); + + return card; + }, + + // Rendu grille des niveaux + async renderLevelsGrid(gameType) { + const grid = document.getElementById('levels-grid'); + grid.innerHTML = '
🔍 Recherche du contenu disponible...
'; + + try { + // Obtenir tout le contenu disponible automatiquement + const availableContent = await this.contentScanner.getAvailableContent(); + + if (availableContent.length === 0) { + grid.innerHTML = '
Aucun contenu trouvé
'; + return; + } + + // Effacer le loading + grid.innerHTML = ''; + + // Filtrer par compatibilité avec le jeu si possible + const compatibleContent = await this.contentScanner.getContentByGame(gameType); + const contentToShow = compatibleContent.length > 0 ? compatibleContent : availableContent; + + console.log(`📋 Affichage de ${contentToShow.length} modules pour ${gameType}`); + + // Créer les cartes pour chaque contenu trouvé + contentToShow.forEach(content => { + const card = this.createLevelCard(content.id, content, gameType); + grid.appendChild(card); + }); + + // Ajouter info de compatibilité si filtré + if (compatibleContent.length > 0 && compatibleContent.length < availableContent.length) { + const infoDiv = document.createElement('div'); + infoDiv.className = 'content-info'; + infoDiv.innerHTML = ` +

Affichage des contenus les plus compatibles avec ${gameType}

+ + `; + grid.appendChild(infoDiv); + } + + } catch (error) { + console.error('Erreur rendu levels:', error); + grid.innerHTML = '
❌ Erreur lors du chargement du contenu
'; + } + }, + + // Méthode pour afficher tout le contenu + async showAllContent(gameType) { + const grid = document.getElementById('levels-grid'); + grid.innerHTML = ''; + + const availableContent = await this.contentScanner.getAvailableContent(); + + availableContent.forEach(content => { + const card = this.createLevelCard(content.id, content, gameType); + grid.appendChild(card); + }); + }, + + // Création d'une carte de niveau + createLevelCard(contentKey, contentInfo, gameType) { + const card = document.createElement('div'); + card.className = 'level-card'; + + // Calculer les statistiques à afficher + const stats = []; + if (contentInfo.stats) { + if (contentInfo.stats.vocabularyCount > 0) { + stats.push(`📚 ${contentInfo.stats.vocabularyCount} mots`); + } + if (contentInfo.stats.sentenceCount > 0) { + stats.push(`💬 ${contentInfo.stats.sentenceCount} phrases`); + } + if (contentInfo.stats.dialogueCount > 0) { + stats.push(`🎭 ${contentInfo.stats.dialogueCount} dialogues`); + } + } + + // Indicateur de compatibilité + const compatibility = contentInfo.gameCompatibility?.[gameType]; + const compatScore = compatibility?.score || 0; + const compatClass = compatScore > 70 ? 'high-compat' : compatScore > 40 ? 'medium-compat' : 'low-compat'; + + card.innerHTML = ` +
+
${contentInfo.icon}
+ ${compatibility ? `
+ ${compatScore > 70 ? '🟢' : compatScore > 40 ? '🟡' : '🟠'} +
` : ''} +
+
${contentInfo.name}
+
${contentInfo.description}
+
+ ${contentInfo.difficulty} + ${contentInfo.metadata.totalItems} éléments + ~${contentInfo.metadata.estimatedTime}min +
+ ${stats.length > 0 ? `
${stats.join(' • ')}
` : ''} + `; + + card.addEventListener('click', () => { + Utils.animateElement(card, 'pulse'); + this.navigateTo('play', gameType, contentKey); + }); + + return card; + }, + + // Mise à jour du breadcrumb + updateBreadcrumb() { + const breadcrumb = document.getElementById('breadcrumb'); + breadcrumb.innerHTML = ''; + + const params = Utils.getUrlParams(); + + // Accueil + const homeItem = this.createBreadcrumbItem('🏠 Accueil', 'home', + this.currentPage === 'home'); + breadcrumb.appendChild(homeItem); + + // Jeux + if (['games', 'levels', 'play'].includes(this.currentPage)) { + const gamesItem = this.createBreadcrumbItem('🎮 Jeux', 'games', + this.currentPage === 'games'); + breadcrumb.appendChild(gamesItem); + } + + // Niveaux + if (['levels', 'play'].includes(this.currentPage) && params.game) { + const gameInfo = this.gamesConfig?.games[params.game]; + const levelText = gameInfo ? `${gameInfo.icon} ${gameInfo.name}` : 'Niveaux'; + const levelsItem = this.createBreadcrumbItem(levelText, 'levels', + this.currentPage === 'levels'); + breadcrumb.appendChild(levelsItem); + } + + // Jeu en cours + if (this.currentPage === 'play' && params.content) { + const contentInfo = this.gamesConfig?.content[params.content]; + const playText = contentInfo ? `🎯 ${contentInfo.name}` : 'Jeu'; + const playItem = this.createBreadcrumbItem(playText, 'play', true); + breadcrumb.appendChild(playItem); + } + }, + + // Création d'un élément breadcrumb + createBreadcrumbItem(text, page, isActive) { + const item = document.createElement('button'); + item.className = `breadcrumb-item ${isActive ? 'active' : ''}`; + item.textContent = text; + item.dataset.page = page; + + if (!isActive) { + item.addEventListener('click', () => { + const params = Utils.getUrlParams(); + + if (page === 'home') { + this.navigateTo('home'); + } else if (page === 'games') { + this.navigateTo('games'); + } else if (page === 'levels') { + this.navigateTo('levels', params.game); + } + }); + } + + return item; + } +}; + +// Fonctions globales pour l'HTML +window.navigateTo = (page, game, content) => AppNavigation.navigateTo(page, game, content); +window.goBack = () => AppNavigation.goBack(); + +// Export +window.AppNavigation = AppNavigation; \ No newline at end of file diff --git a/js/core/utils.js b/js/core/utils.js new file mode 100644 index 0000000..e8c3852 --- /dev/null +++ b/js/core/utils.js @@ -0,0 +1,176 @@ +// === UTILITIES GÉNÉRALES === + +const Utils = { + // Gestion des paramètres URL + getUrlParams() { + const params = new URLSearchParams(window.location.search); + return { + page: params.get('page') || 'home', + game: params.get('game') || null, + content: params.get('content') || null + }; + }, + + setUrlParams(params) { + const url = new URL(window.location); + Object.keys(params).forEach(key => { + if (params[key]) { + url.searchParams.set(key, params[key]); + } else { + url.searchParams.delete(key); + } + }); + window.history.pushState({}, '', url); + }, + + // Affichage/masquage du loading + showLoading() { + document.getElementById('loading').classList.add('show'); + }, + + hideLoading() { + document.getElementById('loading').classList.remove('show'); + }, + + // Notifications toast + showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.className = `toast toast-${type}`; + toast.textContent = message; + + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${type === 'success' ? 'var(--secondary-color)' : + type === 'error' ? 'var(--error-color)' : 'var(--primary-color)'}; + color: white; + padding: 15px 20px; + border-radius: var(--border-radius); + box-shadow: var(--shadow); + z-index: 1001; + animation: slideIn 0.3s ease-out; + `; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.animation = 'slideOut 0.3s ease-in'; + setTimeout(() => document.body.removeChild(toast), 300); + }, 3000); + }, + + // Animation d'éléments + animateElement(element, animation, duration = 300) { + return new Promise(resolve => { + element.style.animation = `${animation} ${duration}ms ease-out`; + setTimeout(() => { + element.style.animation = ''; + resolve(); + }, duration); + }); + }, + + // Génération d'ID unique + generateId() { + return Math.random().toString(36).substr(2, 9); + }, + + // Mélange d'array (Fisher-Yates) + 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; + }, + + // Sélection aléatoire d'éléments + getRandomItems(array, count) { + const shuffled = this.shuffleArray(array); + return shuffled.slice(0, count); + }, + + // Formatage du temps + formatTime(seconds) { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }, + + // Debounce pour événements + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + // Vérification de support audio + canPlayAudio() { + return 'Audio' in window; + }, + + // Chargement d'image avec promise + loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); + }, + + // Stockage local sécurisé + storage: { + set(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (e) { + console.warn('LocalStorage not available:', e); + } + }, + + get(key, defaultValue = null) { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (e) { + console.warn('LocalStorage read error:', e); + return defaultValue; + } + }, + + remove(key) { + try { + localStorage.removeItem(key); + } catch (e) { + console.warn('LocalStorage remove error:', e); + } + } + } +}; + +// Ajout de styles CSS dynamiques pour les animations +const styleSheet = document.createElement('style'); +styleSheet.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + + @keyframes slideOut { + from { transform: translateX(0); opacity: 1; } + to { transform: translateX(100%); opacity: 0; } + } +`; +document.head.appendChild(styleSheet); + +// Export global +window.Utils = Utils; \ No newline at end of file diff --git a/js/games/adventure-reader.js b/js/games/adventure-reader.js new file mode 100644 index 0000000..82a3cce --- /dev/null +++ b/js/games/adventure-reader.js @@ -0,0 +1,950 @@ +// === MODULE ADVENTURE READER (ZELDA-STYLE) === + +class AdventureReaderGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Game state + this.score = 0; + this.currentSentenceIndex = 0; + this.currentVocabIndex = 0; + this.potsDestroyed = 0; + this.enemiesDefeated = 0; + this.isGamePaused = false; + + // Game objects + this.pots = []; + this.enemies = []; + this.player = { x: 0, y: 0 }; // Will be set when map is created + this.isPlayerMoving = false; + this.isPlayerInvulnerable = false; + this.invulnerabilityTimeout = null; + + // Content extraction + this.vocabulary = this.extractVocabulary(this.content); + this.sentences = this.extractSentences(this.content); + + this.init(); + } + + init() { + if ((!this.vocabulary || this.vocabulary.length === 0) && + (!this.sentences || this.sentences.length === 0)) { + console.error('No content available for Adventure Reader'); + this.showInitError(); + return; + } + + this.createGameInterface(); + this.initializePlayer(); + this.setupEventListeners(); + this.generateGameObjects(); + this.generateDecorations(); + this.startGameLoop(); + } + + showInitError() { + this.container.innerHTML = ` +
+

❌ Error loading

+

This content doesn't contain texts compatible with Adventure Reader.

+ +
+ `; + } + + extractVocabulary(content) { + let vocabulary = []; + + if (content.rawContent && content.rawContent.vocabulary) { + if (typeof content.rawContent.vocabulary === 'object' && !Array.isArray(content.rawContent.vocabulary)) { + vocabulary = Object.entries(content.rawContent.vocabulary).map(([english, translation]) => ({ + english: english, + translation: translation + })); + } else if (Array.isArray(content.rawContent.vocabulary)) { + vocabulary = content.rawContent.vocabulary; + } + } + + return vocabulary.filter(item => item && item.english && item.translation); + } + + extractSentences(content) { + let sentences = []; + + if (content.rawContent) { + if (content.rawContent.sentences && Array.isArray(content.rawContent.sentences)) { + sentences = content.rawContent.sentences; + } else if (content.rawContent.texts && Array.isArray(content.rawContent.texts)) { + // Extract sentences from texts + content.rawContent.texts.forEach(text => { + const textSentences = text.content.split(/[.!?]+/).filter(s => s.trim().length > 10); + textSentences.forEach(sentence => { + sentences.push({ + english: sentence.trim() + '.', + translation: sentence.trim() + '.' // Fallback + }); + }); + }); + } + } + + return sentences.filter(item => item && item.english); + } + + createGameInterface() { + this.container.innerHTML = ` +
+ +
+
+
+ 🏆 + 0 +
+
+ 🏺 + 0 +
+
+ ⚔️ + 0 +
+
+
+
+ Start your adventure! +
+
+
+ + +
+ +
🧙‍♂️
+ + +
+ + +
+
+ Click 🏺 pots for vocabulary • Click 👹 enemies for sentences +
+ +
+ + + + + + +
+ `; + } + + initializePlayer() { + // Set player initial position to center of map + const gameMap = document.getElementById('game-map'); + const mapRect = gameMap.getBoundingClientRect(); + this.player.x = mapRect.width / 2 - 20; // -20 for half player width + this.player.y = mapRect.height / 2 - 20; // -20 for half player height + + const playerElement = document.getElementById('player'); + playerElement.style.left = this.player.x + 'px'; + playerElement.style.top = this.player.y + 'px'; + } + + setupEventListeners() { + document.getElementById('restart-btn').addEventListener('click', () => this.restart()); + document.getElementById('continue-btn').addEventListener('click', () => this.closeModal()); + + // Map click handler + const gameMap = document.getElementById('game-map'); + gameMap.addEventListener('click', (e) => this.handleMapClick(e)); + + // Window resize handler + window.addEventListener('resize', () => { + setTimeout(() => this.initializePlayer(), 100); + }); + } + + generateGameObjects() { + const gameMap = document.getElementById('game-map'); + + // Clear existing objects + gameMap.querySelectorAll('.pot, .enemy').forEach(el => el.remove()); + + this.pots = []; + this.enemies = []; + + // Generate pots (for vocabulary) + const numPots = Math.min(8, this.vocabulary.length); + for (let i = 0; i < numPots; i++) { + const pot = this.createPot(); + this.pots.push(pot); + gameMap.appendChild(pot.element); + } + + // Generate enemies (for sentences) - spawn across entire viewport + const numEnemies = Math.min(8, this.sentences.length); + for (let i = 0; i < numEnemies; i++) { + const enemy = this.createEnemy(); + this.enemies.push(enemy); + gameMap.appendChild(enemy.element); + } + + this.updateHUD(); + } + + createPot() { + const pot = document.createElement('div'); + pot.className = 'pot'; + pot.innerHTML = '🏺'; + + const position = this.getRandomPosition(); + pot.style.left = position.x + 'px'; + pot.style.top = position.y + 'px'; + + return { + element: pot, + x: position.x, + y: position.y, + destroyed: false + }; + } + + createEnemy() { + const enemy = document.createElement('div'); + enemy.className = 'enemy'; + enemy.innerHTML = '👹'; + + const position = this.getRandomPosition(true); // Force away from player + enemy.style.left = position.x + 'px'; + enemy.style.top = position.y + 'px'; + + // Random movement pattern for each enemy + const patterns = ['patrol', 'chase', 'wander', 'circle']; + const pattern = patterns[Math.floor(Math.random() * patterns.length)]; + + return { + element: enemy, + x: position.x, + y: position.y, + defeated: false, + moveDirection: Math.random() * Math.PI * 2, + speed: 0.6 + Math.random() * 0.6, // Reduced speed + pattern: pattern, + patrolStartX: position.x, + patrolStartY: position.y, + patrolDistance: 80 + Math.random() * 60, + circleCenter: { x: position.x, y: position.y }, + circleRadius: 60 + Math.random() * 40, + circleAngle: Math.random() * Math.PI * 2, + changeDirectionTimer: 0, + dashCooldown: 0, + isDashing: false + }; + } + + getRandomPosition(forceAwayFromPlayer = false) { + const gameMap = document.getElementById('game-map'); + const mapRect = gameMap.getBoundingClientRect(); + const mapWidth = mapRect.width; + const mapHeight = mapRect.height; + const margin = 40; + + let x, y; + let tooClose; + const minDistance = forceAwayFromPlayer ? 150 : 80; + + do { + x = margin + Math.random() * (mapWidth - margin * 2); + y = margin + Math.random() * (mapHeight - margin * 2); + + // Check distance from player + const distFromPlayer = Math.sqrt( + Math.pow(x - this.player.x, 2) + Math.pow(y - this.player.y, 2) + ); + tooClose = distFromPlayer < minDistance; + + } while (tooClose); + + return { x, y }; + } + + handleMapClick(e) { + if (this.isGamePaused || this.isPlayerMoving) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + + // Check pot clicks + let targetFound = false; + this.pots.forEach(pot => { + if (!pot.destroyed && this.isNearPosition(clickX, clickY, pot)) { + this.movePlayerToTarget(pot, 'pot'); + targetFound = true; + } + }); + + // Check enemy clicks (only if no pot was clicked) + if (!targetFound) { + this.enemies.forEach(enemy => { + if (!enemy.defeated && this.isNearPosition(clickX, clickY, enemy)) { + this.movePlayerToTarget(enemy, 'enemy'); + targetFound = true; + } + }); + } + + // If no target found, move to empty area + if (!targetFound) { + this.movePlayerToPosition(clickX, clickY); + } + } + + isNearPosition(clickX, clickY, object) { + const distance = Math.sqrt( + Math.pow(clickX - (object.x + 20), 2) + Math.pow(clickY - (object.y + 20), 2) + ); + return distance < 60; // Larger clickable area + } + + movePlayerToTarget(target, type) { + this.isPlayerMoving = true; + const playerElement = document.getElementById('player'); + + // Grant invulnerability IMMEDIATELY when attacking an enemy + if (type === 'enemy') { + this.grantAttackInvulnerability(); + } + + // Calculate target position (near the object) + const targetX = target.x; + const targetY = target.y; + + // Update player position + this.player.x = targetX; + this.player.y = targetY; + + // Animate player movement + playerElement.style.left = targetX + 'px'; + playerElement.style.top = targetY + 'px'; + + // Add walking animation + playerElement.style.transform = 'scale(1.1)'; + + // Wait for movement animation to complete, then interact + setTimeout(() => { + playerElement.style.transform = 'scale(1)'; + this.isPlayerMoving = false; + + if (type === 'pot') { + this.destroyPot(target); + } else if (type === 'enemy') { + this.defeatEnemy(target); + } + }, 800); // Match CSS transition duration + } + + movePlayerToPosition(targetX, targetY) { + this.isPlayerMoving = true; + const playerElement = document.getElementById('player'); + + // Update player position + this.player.x = targetX - 20; // Center the player on click point + this.player.y = targetY - 20; + + // Keep player within bounds + const gameMap = document.getElementById('game-map'); + const mapRect = gameMap.getBoundingClientRect(); + const margin = 20; + + this.player.x = Math.max(margin, Math.min(mapRect.width - 60, this.player.x)); + this.player.y = Math.max(margin, Math.min(mapRect.height - 60, this.player.y)); + + // Animate player movement + playerElement.style.left = this.player.x + 'px'; + playerElement.style.top = this.player.y + 'px'; + + // Add walking animation + playerElement.style.transform = 'scale(1.1)'; + + // Reset animation after movement + setTimeout(() => { + playerElement.style.transform = 'scale(1)'; + this.isPlayerMoving = false; + }, 800); + } + + destroyPot(pot) { + pot.destroyed = true; + pot.element.classList.add('destroyed'); + + // Animation + pot.element.innerHTML = '💥'; + setTimeout(() => { + pot.element.style.opacity = '0.3'; + pot.element.innerHTML = '💨'; + }, 200); + + this.potsDestroyed++; + this.score += 10; + + // Show vocabulary + if (this.currentVocabIndex < this.vocabulary.length) { + this.showVocabPopup(this.vocabulary[this.currentVocabIndex]); + this.currentVocabIndex++; + } + + this.updateHUD(); + this.checkGameComplete(); + } + + defeatEnemy(enemy) { + enemy.defeated = true; + enemy.element.classList.add('defeated'); + + // Animation + enemy.element.innerHTML = '☠️'; + setTimeout(() => { + enemy.element.style.opacity = '0.3'; + }, 300); + + this.enemiesDefeated++; + this.score += 25; + + // Invulnerability is already granted at start of movement + // Just refresh the timer to ensure full 2 seconds from now + this.refreshAttackInvulnerability(); + + // Show sentence (pause game) + if (this.currentSentenceIndex < this.sentences.length) { + this.showReadingModal(this.sentences[this.currentSentenceIndex]); + this.currentSentenceIndex++; + } + + this.updateHUD(); + } + + showVocabPopup(vocab) { + const popup = document.getElementById('vocab-popup'); + const wordEl = document.getElementById('vocab-word'); + const translationEl = document.getElementById('vocab-translation'); + + wordEl.textContent = vocab.english; + translationEl.textContent = vocab.translation; + + popup.style.display = 'block'; + popup.classList.add('show'); + + setTimeout(() => { + popup.classList.remove('show'); + setTimeout(() => { + popup.style.display = 'none'; + }, 300); + }, 2000); + } + + showReadingModal(sentence) { + this.isGamePaused = true; + const modal = document.getElementById('reading-modal'); + const content = document.getElementById('reading-content'); + + content.innerHTML = ` +
+

${sentence.english}

+ ${sentence.translation ? `

${sentence.translation}

` : ''} +
+ `; + + modal.style.display = 'flex'; + modal.classList.add('show'); + } + + closeModal() { + const modal = document.getElementById('reading-modal'); + modal.classList.remove('show'); + setTimeout(() => { + modal.style.display = 'none'; + this.isGamePaused = false; + }, 300); + + this.checkGameComplete(); + } + + checkGameComplete() { + const allPotsDestroyed = this.pots.every(pot => pot.destroyed); + const allEnemiesDefeated = this.enemies.every(enemy => enemy.defeated); + + if (allPotsDestroyed && allEnemiesDefeated) { + setTimeout(() => { + this.gameComplete(); + }, 1000); + } + } + + gameComplete() { + // Bonus for completion + this.score += 100; + this.updateHUD(); + + document.getElementById('progress-text').textContent = '🏆 Adventure Complete!'; + + setTimeout(() => { + this.onGameEnd(this.score); + }, 2000); + } + + updateHUD() { + document.getElementById('score-display').textContent = this.score; + document.getElementById('pots-counter').textContent = this.potsDestroyed; + document.getElementById('enemies-counter').textContent = this.enemiesDefeated; + + const totalObjects = this.pots.length + this.enemies.length; + const destroyedObjects = this.potsDestroyed + this.enemiesDefeated; + + document.getElementById('progress-text').textContent = + `Progress: ${destroyedObjects}/${totalObjects} objects`; + + this.onScoreUpdate(this.score); + } + + generateDecorations() { + const gameMap = document.getElementById('game-map'); + const mapRect = gameMap.getBoundingClientRect(); + const mapWidth = mapRect.width; + const mapHeight = mapRect.height; + + // Remove existing decorations + gameMap.querySelectorAll('.decoration').forEach(el => el.remove()); + + // Generate trees (fewer, larger) + const numTrees = 4 + Math.floor(Math.random() * 4); // 4-7 trees + for (let i = 0; i < numTrees; i++) { + const tree = document.createElement('div'); + tree.className = 'decoration tree'; + tree.innerHTML = Math.random() < 0.5 ? '🌳' : '🌲'; + + const position = this.getDecorationPosition(mapWidth, mapHeight, 60); // Keep away from objects + tree.style.left = position.x + 'px'; + tree.style.top = position.y + 'px'; + tree.style.fontSize = (25 + Math.random() * 15) + 'px'; // Random size + + gameMap.appendChild(tree); + } + + // Generate grass patches (many, small) + const numGrass = 15 + Math.floor(Math.random() * 10); // 15-24 grass + for (let i = 0; i < numGrass; i++) { + const grass = document.createElement('div'); + grass.className = 'decoration grass'; + const grassTypes = ['🌿', '🌱', '🍀', '🌾']; + grass.innerHTML = grassTypes[Math.floor(Math.random() * grassTypes.length)]; + + const position = this.getDecorationPosition(mapWidth, mapHeight, 30); // Smaller keepaway + grass.style.left = position.x + 'px'; + grass.style.top = position.y + 'px'; + grass.style.fontSize = (15 + Math.random() * 8) + 'px'; // Smaller size + + gameMap.appendChild(grass); + } + + // Generate rocks (medium amount) + const numRocks = 3 + Math.floor(Math.random() * 3); // 3-5 rocks + for (let i = 0; i < numRocks; i++) { + const rock = document.createElement('div'); + rock.className = 'decoration rock'; + rock.innerHTML = Math.random() < 0.5 ? '🪨' : '⛰️'; + + const position = this.getDecorationPosition(mapWidth, mapHeight, 40); + rock.style.left = position.x + 'px'; + rock.style.top = position.y + 'px'; + rock.style.fontSize = (20 + Math.random() * 10) + 'px'; + + gameMap.appendChild(rock); + } + } + + getDecorationPosition(mapWidth, mapHeight, keepAwayDistance) { + const margin = 20; + let x, y; + let attempts = 0; + let validPosition = false; + + do { + x = margin + Math.random() * (mapWidth - margin * 2); + y = margin + Math.random() * (mapHeight - margin * 2); + + // Check distance from player + const distFromPlayer = Math.sqrt( + Math.pow(x - this.player.x, 2) + Math.pow(y - this.player.y, 2) + ); + + // Check distance from pots and enemies + let tooClose = distFromPlayer < keepAwayDistance; + + if (!tooClose) { + this.pots.forEach(pot => { + const dist = Math.sqrt(Math.pow(x - pot.x, 2) + Math.pow(y - pot.y, 2)); + if (dist < keepAwayDistance) tooClose = true; + }); + } + + if (!tooClose) { + this.enemies.forEach(enemy => { + const dist = Math.sqrt(Math.pow(x - enemy.x, 2) + Math.pow(y - enemy.y, 2)); + if (dist < keepAwayDistance) tooClose = true; + }); + } + + validPosition = !tooClose; + attempts++; + + } while (!validPosition && attempts < 50); + + return { x, y }; + } + + startGameLoop() { + const animate = () => { + if (!this.isGamePaused) { + this.moveEnemies(); + } + requestAnimationFrame(animate); + }; + animate(); + } + + moveEnemies() { + const gameMap = document.getElementById('game-map'); + const mapRect = gameMap.getBoundingClientRect(); + const mapWidth = mapRect.width; + const mapHeight = mapRect.height; + + this.enemies.forEach(enemy => { + if (enemy.defeated) return; + + // Apply movement pattern + this.applyMovementPattern(enemy, mapWidth, mapHeight); + + // Bounce off walls (using dynamic map size) + if (enemy.x < 10 || enemy.x > mapWidth - 50) { + enemy.moveDirection = Math.PI - enemy.moveDirection; + enemy.x = Math.max(10, Math.min(mapWidth - 50, enemy.x)); + } + if (enemy.y < 10 || enemy.y > mapHeight - 50) { + enemy.moveDirection = -enemy.moveDirection; + enemy.y = Math.max(10, Math.min(mapHeight - 50, enemy.y)); + } + + enemy.element.style.left = enemy.x + 'px'; + enemy.element.style.top = enemy.y + 'px'; + + // Check collision with player + this.checkPlayerEnemyCollision(enemy); + }); + } + + applyMovementPattern(enemy, mapWidth, mapHeight) { + enemy.changeDirectionTimer++; + + switch (enemy.pattern) { + case 'patrol': + // Patrol back and forth + const distanceFromStart = Math.sqrt( + Math.pow(enemy.x - enemy.patrolStartX, 2) + Math.pow(enemy.y - enemy.patrolStartY, 2) + ); + + if (distanceFromStart > enemy.patrolDistance) { + // Turn around and head back to start + const angleToStart = Math.atan2( + enemy.patrolStartY - enemy.y, + enemy.patrolStartX - enemy.x + ); + enemy.moveDirection = angleToStart; + } + + if (enemy.changeDirectionTimer > 120) { // Change direction every ~2 seconds + enemy.moveDirection += (Math.random() - 0.5) * Math.PI * 0.5; + enemy.changeDirectionTimer = 0; + } + + enemy.x += Math.cos(enemy.moveDirection) * enemy.speed; + enemy.y += Math.sin(enemy.moveDirection) * enemy.speed; + break; + + case 'chase': + enemy.dashCooldown--; + + if (enemy.isDashing) { + // Continue dash movement with very high speed + enemy.x += Math.cos(enemy.moveDirection) * (enemy.speed * 6); + enemy.y += Math.sin(enemy.moveDirection) * (enemy.speed * 6); + enemy.dashCooldown--; + + if (enemy.dashCooldown <= 0) { + enemy.isDashing = false; + enemy.dashCooldown = 120 + Math.random() * 60; // Reset cooldown + } + } else { + // Normal chase behavior + const angleToPlayer = Math.atan2( + this.player.y - enemy.y, + this.player.x - enemy.x + ); + + // Sometimes do a perpendicular dash + if (enemy.dashCooldown <= 0 && Math.random() < 0.3) { + enemy.isDashing = true; + enemy.dashCooldown = 50; // Much longer dash duration + + // Perpendicular angle (90 degrees from player direction) + const perpAngle = angleToPlayer + (Math.random() < 0.5 ? Math.PI/2 : -Math.PI/2); + enemy.moveDirection = perpAngle; + + // Start dash with visual effect + enemy.element.style.filter = 'drop-shadow(0 0 8px red)'; + setTimeout(() => { + enemy.element.style.filter = 'drop-shadow(1px 1px 2px rgba(0,0,0,0.3))'; + }, 300); + } else { + // Mix chasing with some randomness + enemy.moveDirection = angleToPlayer + (Math.random() - 0.5) * 0.3; + + enemy.x += Math.cos(enemy.moveDirection) * (enemy.speed * 0.8); + enemy.y += Math.sin(enemy.moveDirection) * (enemy.speed * 0.8); + } + } + break; + + case 'wander': + // Random wandering + if (enemy.changeDirectionTimer > 60 + Math.random() * 60) { + enemy.moveDirection += (Math.random() - 0.5) * Math.PI; + enemy.changeDirectionTimer = 0; + } + + enemy.x += Math.cos(enemy.moveDirection) * enemy.speed; + enemy.y += Math.sin(enemy.moveDirection) * enemy.speed; + break; + + case 'circle': + // Move in circular pattern + enemy.circleAngle += 0.03 + (enemy.speed * 0.01); + + enemy.x = enemy.circleCenter.x + Math.cos(enemy.circleAngle) * enemy.circleRadius; + enemy.y = enemy.circleCenter.y + Math.sin(enemy.circleAngle) * enemy.circleRadius; + + // Occasionally change circle center + if (enemy.changeDirectionTimer > 180) { + enemy.circleCenter.x += (Math.random() - 0.5) * 100; + enemy.circleCenter.y += (Math.random() - 0.5) * 100; + + // Keep circle center within bounds + enemy.circleCenter.x = Math.max(enemy.circleRadius + 20, + Math.min(mapWidth - enemy.circleRadius - 20, enemy.circleCenter.x)); + enemy.circleCenter.y = Math.max(enemy.circleRadius + 20, + Math.min(mapHeight - enemy.circleRadius - 20, enemy.circleCenter.y)); + + enemy.changeDirectionTimer = 0; + } + break; + } + } + + checkPlayerEnemyCollision(enemy) { + if (this.isPlayerInvulnerable || enemy.defeated) return; + + const distance = Math.sqrt( + Math.pow(this.player.x - enemy.x, 2) + Math.pow(this.player.y - enemy.y, 2) + ); + + // Collision detected + if (distance < 35) { + this.takeDamage(); + } + } + + takeDamage() { + if (this.isPlayerInvulnerable) return; + + // Apply damage + this.score = Math.max(0, this.score - 20); + this.updateHUD(); + + // Clear any existing invulnerability timeout + if (this.invulnerabilityTimeout) { + clearTimeout(this.invulnerabilityTimeout); + } + + // Start damage invulnerability + this.isPlayerInvulnerable = true; + const playerElement = document.getElementById('player'); + + // Visual feedback - blinking effect + let blinkCount = 0; + const blinkInterval = setInterval(() => { + playerElement.style.opacity = playerElement.style.opacity === '0.3' ? '1' : '0.3'; + blinkCount++; + + if (blinkCount >= 8) { // 4 blinks in 2 seconds + clearInterval(blinkInterval); + playerElement.style.opacity = '1'; + playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))'; + playerElement.style.transform = 'scale(1)'; + this.isPlayerInvulnerable = false; + } + }, 250); + + // Show damage feedback + this.showDamagePopup(); + } + + grantAttackInvulnerability() { + // Always grant invulnerability after attack, even if already invulnerable + this.isPlayerInvulnerable = true; + const playerElement = document.getElementById('player'); + + // Clear any existing timeout + if (this.invulnerabilityTimeout) { + clearTimeout(this.invulnerabilityTimeout); + } + + // Different visual effect for attack invulnerability (golden glow) + playerElement.style.filter = 'drop-shadow(0 0 15px gold) brightness(1.4)'; + + this.invulnerabilityTimeout = setTimeout(() => { + playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))'; + this.isPlayerInvulnerable = false; + }, 2000); + + // Show invulnerability feedback + this.showInvulnerabilityPopup(); + } + + refreshAttackInvulnerability() { + // Refresh the invulnerability timer without changing visual state + if (this.invulnerabilityTimeout) { + clearTimeout(this.invulnerabilityTimeout); + } + + const playerElement = document.getElementById('player'); + this.isPlayerInvulnerable = true; + + this.invulnerabilityTimeout = setTimeout(() => { + playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))'; + this.isPlayerInvulnerable = false; + }, 2000); + } + + showInvulnerabilityPopup() { + const popup = document.createElement('div'); + popup.className = 'invulnerability-popup'; + popup.innerHTML = 'Protected!'; + popup.style.position = 'fixed'; + popup.style.left = '50%'; + popup.style.top = '25%'; + popup.style.transform = 'translate(-50%, -50%)'; + popup.style.color = '#FFD700'; + popup.style.fontSize = '1.5rem'; + popup.style.fontWeight = 'bold'; + popup.style.zIndex = '999'; + popup.style.pointerEvents = 'none'; + popup.style.animation = 'protectionFloat 2s ease-out forwards'; + + document.body.appendChild(popup); + + setTimeout(() => { + popup.remove(); + }, 2000); + } + + showDamagePopup() { + // Create damage popup + const damagePopup = document.createElement('div'); + damagePopup.className = 'damage-popup'; + damagePopup.innerHTML = '-20'; + damagePopup.style.position = 'fixed'; + damagePopup.style.left = '50%'; + damagePopup.style.top = '30%'; + damagePopup.style.transform = 'translate(-50%, -50%)'; + damagePopup.style.color = '#EF4444'; + damagePopup.style.fontSize = '2rem'; + damagePopup.style.fontWeight = 'bold'; + damagePopup.style.zIndex = '999'; + damagePopup.style.pointerEvents = 'none'; + damagePopup.style.animation = 'damageFloat 1.5s ease-out forwards'; + + document.body.appendChild(damagePopup); + + setTimeout(() => { + damagePopup.remove(); + }, 1500); + } + + start() { + console.log('⚔️ Adventure Reader: Starting'); + document.getElementById('progress-text').textContent = 'Click objects to begin your adventure!'; + } + + restart() { + console.log('🔄 Adventure Reader: Restarting'); + this.reset(); + this.start(); + } + + reset() { + this.score = 0; + this.currentSentenceIndex = 0; + this.currentVocabIndex = 0; + this.potsDestroyed = 0; + this.enemiesDefeated = 0; + this.isGamePaused = false; + this.isPlayerMoving = false; + this.isPlayerInvulnerable = false; + + // Clear any existing timeout + if (this.invulnerabilityTimeout) { + clearTimeout(this.invulnerabilityTimeout); + this.invulnerabilityTimeout = null; + } + + this.generateGameObjects(); + this.initializePlayer(); + this.generateDecorations(); + } + + destroy() { + this.container.innerHTML = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.AdventureReader = AdventureReaderGame; \ No newline at end of file diff --git a/js/games/chinese-study.js b/js/games/chinese-study.js new file mode 100644 index 0000000..09d7bf6 --- /dev/null +++ b/js/games/chinese-study.js @@ -0,0 +1,279 @@ +// === CHINESE STUDY MODE === + +class ChineseStudyGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + this.score = 0; + this.isRunning = false; + + this.init(); + } + + init() { + this.createInterface(); + this.setupEventListeners(); + } + + createInterface() { + this.container.innerHTML = ` +
+
+

🇨🇳 Chinese Study Mode

+
Score: 0
+
+ +
+
+
📝
+

Character Recognition

+

Learn Chinese characters and their meanings

+ +
+ +
+
🗣️
+

Pinyin Practice

+

Master Chinese pronunciation with pinyin

+ +
+ +
+
📚
+

Vocabulary Cards

+

Study vocabulary with flashcards

+ +
+ +
+
✍️
+

Stroke Order

+

Learn proper character writing

+ +
+
+ +
+

🚧 Coming Soon!

+

This is a placeholder for the Chinese study mode. Different learning modules will be implemented here.

+

Available content: ${this.getContentInfo()}

+
+ +
+ +
+
+ `; + + this.addStyles(); + } + + setupEventListeners() { + const modeCards = this.container.querySelectorAll('.mode-card'); + modeCards.forEach(card => { + card.addEventListener('click', (e) => { + const mode = card.dataset.mode; + this.showPlaceholder(mode); + }); + }); + } + + showPlaceholder(mode) { + const modeNames = { + characters: 'Character Recognition', + pinyin: 'Pinyin Practice', + vocabulary: 'Vocabulary Cards', + writing: 'Stroke Order' + }; + + const placeholderDiv = this.container.querySelector('.placeholder-content'); + placeholderDiv.innerHTML = ` +

📱 ${modeNames[mode]} Mode

+

This learning module is being developed!

+

It will include interactive exercises for ${mode} practice.

+ + `; + + // Add some score for interaction + this.score += 5; + this.onScoreUpdate(this.score); + this.container.querySelector('#score').textContent = this.score; + } + + getContentInfo() { + if (!this.content) return 'No content loaded'; + + let info = []; + if (this.content.vocabulary) info.push(`${Object.keys(this.content.vocabulary).length} vocabulary items`); + if (this.content.sentences) info.push(`${this.content.sentences.length} sentences`); + if (this.content.texts) info.push(`${this.content.texts.length} texts`); + + return info.length > 0 ? info.join(', ') : 'Basic Chinese content'; + } + + addStyles() { + const style = document.createElement('style'); + style.textContent = ` + .chinese-study-container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; + font-family: 'Arial', sans-serif; + } + + .game-header { + text-align: center; + margin-bottom: 30px; + border-bottom: 2px solid #e5e7eb; + padding-bottom: 20px; + } + + .game-header h2 { + color: #dc2626; + font-size: 2.2em; + margin-bottom: 10px; + } + + .score-display { + font-size: 1.2em; + color: #059669; + font-weight: bold; + } + + .study-modes { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 40px; + } + + .mode-card { + background: linear-gradient(135deg, #fff 0%, #f8fafc 100%); + border: 2px solid #e5e7eb; + border-radius: 16px; + padding: 24px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + } + + .mode-card:hover { + border-color: #dc2626; + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(220, 38, 38, 0.15); + } + + .mode-icon { + font-size: 3em; + margin-bottom: 12px; + } + + .mode-card h3 { + color: #374151; + margin-bottom: 8px; + font-size: 1.3em; + } + + .mode-card p { + color: #6b7280; + margin-bottom: 16px; + line-height: 1.5; + } + + .mode-btn { + background: #dc2626; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s ease; + } + + .mode-btn:hover { + background: #b91c1c; + } + + .placeholder-content { + background: #fef3c7; + border: 2px solid #f59e0b; + border-radius: 12px; + padding: 24px; + text-align: center; + margin-bottom: 30px; + } + + .placeholder-content h3 { + color: #92400e; + margin-bottom: 12px; + } + + .placeholder-content p { + color: #78350f; + margin-bottom: 8px; + } + + .game-controls { + text-align: center; + } + + .back-btn, .retry-btn { + background: #6b7280; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-size: 1em; + font-weight: bold; + transition: background 0.3s ease; + } + + .back-btn:hover, .retry-btn:hover { + background: #4b5563; + } + + @media (max-width: 768px) { + .study-modes { + grid-template-columns: 1fr; + } + + .chinese-study-container { + padding: 15px; + } + + .game-header h2 { + font-size: 1.8em; + } + } + `; + document.head.appendChild(style); + } + + start() { + this.isRunning = true; + console.log('Chinese Study Mode initialized'); + } + + destroy() { + this.isRunning = false; + // Clean up any intervals, event listeners, etc. + } + + restart() { + this.score = 0; + this.onScoreUpdate(this.score); + this.container.querySelector('#score').textContent = this.score; + this.createInterface(); + this.setupEventListeners(); + } +} + +// Export to global scope +window.GameModules = window.GameModules || {}; +window.GameModules.ChineseStudy = ChineseStudyGame; \ No newline at end of file diff --git a/js/games/fill-the-blank.js b/js/games/fill-the-blank.js new file mode 100644 index 0000000..e19350c --- /dev/null +++ b/js/games/fill-the-blank.js @@ -0,0 +1,419 @@ +// === MODULE FILL THE BLANK === + +class FillTheBlankGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // État du jeu + this.score = 0; + this.errors = 0; + this.currentSentenceIndex = 0; + this.isRunning = false; + + // Données de jeu + this.sentences = this.extractSentences(this.content); + this.currentSentence = null; + this.blanks = []; + this.userAnswers = []; + + this.init(); + } + + init() { + // Vérifier que nous avons des phrases + if (!this.sentences || this.sentences.length === 0) { + console.error('Aucune phrase disponible pour Fill the Blank'); + this.showInitError(); + return; + } + + this.createGameBoard(); + this.setupEventListeners(); + // Le jeu démarrera quand start() sera appelé + } + + showInitError() { + this.container.innerHTML = ` +
+

❌ Erreur de chargement

+

Ce contenu ne contient pas de phrases compatibles avec Fill the Blank.

+

Le jeu nécessite des phrases avec leurs traductions.

+ +
+ `; + } + + extractSentences(content) { + let sentences = []; + + console.log('🔍 Extraction phrases depuis:', content?.name || 'contenu'); + + // Utiliser le contenu brut du module si disponible + if (content.rawContent) { + console.log('📦 Utilisation du contenu brut du module'); + return this.extractSentencesFromRaw(content.rawContent); + } + + // Format avec sentences array + if (content.sentences && Array.isArray(content.sentences)) { + console.log('📝 Format sentences détecté'); + sentences = content.sentences.filter(sentence => + sentence.english && sentence.english.trim() !== '' + ); + } + // Format moderne avec contentItems + else if (content.contentItems && Array.isArray(content.contentItems)) { + console.log('🆕 Format contentItems détecté'); + sentences = content.contentItems + .filter(item => item.type === 'sentence' && item.english) + .map(item => ({ + english: item.english, + french: item.french || item.translation, + chinese: item.chinese + })); + } + + return this.finalizeSentences(sentences); + } + + extractSentencesFromRaw(rawContent) { + console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module'); + let sentences = []; + + // Format simple (sentences array) + if (rawContent.sentences && Array.isArray(rawContent.sentences)) { + sentences = rawContent.sentences.filter(sentence => + sentence.english && sentence.english.trim() !== '' + ); + console.log(`📝 ${sentences.length} phrases extraites depuis sentences array`); + } + // Format contentItems + else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) { + sentences = rawContent.contentItems + .filter(item => item.type === 'sentence' && item.english) + .map(item => ({ + english: item.english, + french: item.french || item.translation, + chinese: item.chinese + })); + console.log(`🆕 ${sentences.length} phrases extraites depuis contentItems`); + } + + return this.finalizeSentences(sentences); + } + + finalizeSentences(sentences) { + // Validation et nettoyage + sentences = sentences.filter(sentence => + sentence && + typeof sentence.english === 'string' && + sentence.english.trim() !== '' && + sentence.english.split(' ').length >= 3 // Au moins 3 mots pour créer des blanks + ); + + if (sentences.length === 0) { + console.error('❌ Aucune phrase valide trouvée'); + // Phrases de démonstration en dernier recours + sentences = [ + { english: "I am learning English.", chinese: "我正在学英语。" }, + { english: "She goes to school every day.", chinese: "她每天都去学校。" }, + { english: "We like to play games together.", chinese: "我们喜欢一起玩游戏。" } + ]; + console.warn('🚨 Utilisation de phrases de démonstration'); + } + + // Mélanger les phrases + sentences = this.shuffleArray(sentences); + + console.log(`✅ Fill the Blank: ${sentences.length} phrases finalisées`); + return sentences; + } + + createGameBoard() { + this.container.innerHTML = ` +
+ +
+
+
+ ${this.currentSentenceIndex + 1} + / ${this.sentences.length} +
+
+ ${this.errors} + Erreurs +
+
+ ${this.score} + Score +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + + +
+ `; + } + + setupEventListeners() { + document.getElementById('check-btn').addEventListener('click', () => this.checkAnswer()); + document.getElementById('hint-btn').addEventListener('click', () => this.showHint()); + document.getElementById('skip-btn').addEventListener('click', () => this.skipSentence()); + + // Enter key to check answer + document.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && this.isRunning) { + this.checkAnswer(); + } + }); + } + + start() { + console.log('🎮 Fill the Blank: Démarrage du jeu'); + this.loadNextSentence(); + } + + restart() { + console.log('🔄 Fill the Blank: Redémarrage du jeu'); + this.reset(); + this.start(); + } + + reset() { + this.score = 0; + this.errors = 0; + this.currentSentenceIndex = 0; + this.isRunning = false; + this.currentSentence = null; + this.blanks = []; + this.userAnswers = []; + this.onScoreUpdate(0); + } + + loadNextSentence() { + // Si on a fini toutes les phrases, recommencer depuis le début + if (this.currentSentenceIndex >= this.sentences.length) { + this.currentSentenceIndex = 0; + this.sentences = this.shuffleArray(this.sentences); // Mélanger à nouveau + this.showFeedback(`🎉 Toutes les phrases terminées ! On recommence avec un nouvel ordre.`, 'success'); + setTimeout(() => { + this.loadNextSentence(); + }, 1500); + return; + } + + this.isRunning = true; + this.currentSentence = this.sentences[this.currentSentenceIndex]; + this.createBlanks(); + this.displaySentence(); + this.updateUI(); + } + + createBlanks() { + const words = this.currentSentence.english.split(' '); + this.blanks = []; + + // Créer 1-3 blanks selon la longueur de la phrase + const numBlanks = Math.min(Math.max(1, Math.floor(words.length / 4)), 3); + const blankIndices = new Set(); + + // Sélectionner des mots aléatoires (pas les articles/prépositions courtes) + const candidateWords = words.map((word, index) => ({ word, index })) + .filter(item => item.word.length > 2 && !['the', 'and', 'but', 'for', 'nor', 'or', 'so', 'yet'].includes(item.word.toLowerCase())); + + // Si pas assez de candidats, prendre n'importe quels mots + if (candidateWords.length < numBlanks) { + candidateWords = words.map((word, index) => ({ word, index })); + } + + // Sélectionner aléatoirement les indices des blanks + const shuffledCandidates = this.shuffleArray(candidateWords); + for (let i = 0; i < Math.min(numBlanks, shuffledCandidates.length); i++) { + blankIndices.add(shuffledCandidates[i].index); + } + + // Créer la structure des blanks + words.forEach((word, index) => { + if (blankIndices.has(index)) { + this.blanks.push({ + index: index, + word: word.replace(/[.,!?;:]$/, ''), // Retirer la ponctuation + punctuation: word.match(/[.,!?;:]$/) ? word.match(/[.,!?;:]$/)[0] : '', + userAnswer: '' + }); + } + }); + } + + displaySentence() { + const words = this.currentSentence.english.split(' '); + let sentenceHTML = ''; + let blankCounter = 0; + + words.forEach((word, index) => { + const blank = this.blanks.find(b => b.index === index); + if (blank) { + sentenceHTML += ` + + ${blank.punctuation} + `; + blankCounter++; + } else { + sentenceHTML += `${word} `; + } + }); + + document.getElementById('sentence-container').innerHTML = sentenceHTML; + + // Afficher la traduction si disponible + const translation = this.currentSentence.chinese || this.currentSentence.french || ''; + document.getElementById('translation-hint').innerHTML = translation ? + `💭 ${translation}` : ''; + + // Focus sur le premier input + const firstInput = document.getElementById('blank-0'); + if (firstInput) { + setTimeout(() => firstInput.focus(), 100); + } + } + + checkAnswer() { + if (!this.isRunning) return; + + let allCorrect = true; + let correctCount = 0; + + // Vérifier chaque blank + this.blanks.forEach((blank, index) => { + const input = document.getElementById(`blank-${index}`); + const userAnswer = input.value.trim().toLowerCase(); + const correctAnswer = blank.word.toLowerCase(); + + blank.userAnswer = input.value.trim(); + + if (userAnswer === correctAnswer) { + input.classList.remove('incorrect'); + input.classList.add('correct'); + correctCount++; + } else { + input.classList.remove('correct'); + input.classList.add('incorrect'); + allCorrect = false; + } + }); + + if (allCorrect) { + // Toutes les réponses sont correctes + this.score += 10 * this.blanks.length; + this.showFeedback(`🎉 Parfait ! +${10 * this.blanks.length} points`, 'success'); + setTimeout(() => { + this.currentSentenceIndex++; + this.loadNextSentence(); + }, 1500); + } else { + // Quelques erreurs + this.errors++; + if (correctCount > 0) { + this.score += 5 * correctCount; + this.showFeedback(`✨ ${correctCount}/${this.blanks.length} correct ! +${5 * correctCount} points. Essaye encore.`, 'partial'); + } else { + this.showFeedback(`❌ Essaye encore ! (${this.errors} erreurs)`, 'error'); + } + } + + this.updateUI(); + this.onScoreUpdate(this.score); + } + + showHint() { + // Afficher la première lettre de chaque blank vide + this.blanks.forEach((blank, index) => { + const input = document.getElementById(`blank-${index}`); + if (!input.value.trim()) { + input.value = blank.word[0]; + input.focus(); + } + }); + + this.showFeedback('💡 Première lettre ajoutée !', 'info'); + } + + skipSentence() { + // Révéler les bonnes réponses + this.blanks.forEach((blank, index) => { + const input = document.getElementById(`blank-${index}`); + input.value = blank.word; + input.classList.add('revealed'); + }); + + this.showFeedback('📖 Réponses révélées ! Phrase suivante...', 'info'); + setTimeout(() => { + this.currentSentenceIndex++; + this.loadNextSentence(); + }, 2000); + } + + // Méthode endGame supprimée - le jeu continue indéfiniment + + showFeedback(message, type = 'info') { + const feedbackArea = document.getElementById('feedback-area'); + feedbackArea.innerHTML = `
${message}
`; + } + + updateUI() { + document.getElementById('current-question').textContent = this.currentSentenceIndex + 1; + document.getElementById('errors-count').textContent = this.errors; + document.getElementById('score-display').textContent = this.score; + } + + 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; + } + + destroy() { + this.isRunning = false; + this.container.innerHTML = ''; + } +} + +// Enregistrement du module +window.GameModules = window.GameModules || {}; +window.GameModules.FillTheBlank = FillTheBlankGame; \ No newline at end of file diff --git a/js/games/memory-match.js b/js/games/memory-match.js new file mode 100644 index 0000000..1954ec7 --- /dev/null +++ b/js/games/memory-match.js @@ -0,0 +1,455 @@ +// === MODULE MEMORY MATCH === + +class MemoryMatchGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Game state + this.cards = []; + this.flippedCards = []; + this.matchedPairs = 0; + this.totalPairs = 8; // 4x4 grid = 16 cards = 8 pairs + this.moves = 0; + this.score = 0; + this.isFlipping = false; + + // Extract vocabulary + this.vocabulary = this.extractVocabulary(this.content); + + this.init(); + } + + init() { + // Check if we have enough vocabulary + if (!this.vocabulary || this.vocabulary.length < this.totalPairs) { + console.error('Not enough vocabulary for Memory Match'); + this.showInitError(); + return; + } + + this.createGameInterface(); + this.generateCards(); + this.setupEventListeners(); + } + + showInitError() { + this.container.innerHTML = ` +
+

❌ Error loading

+

This content doesn't have enough vocabulary for Memory Match.

+

The game needs at least ${this.totalPairs} vocabulary pairs.

+ +
+ `; + } + + extractVocabulary(content) { + let vocabulary = []; + + console.log('📝 Extracting vocabulary from:', content?.name || 'content'); + + // Use raw module content if available + if (content.rawContent) { + console.log('📦 Using raw module content'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Modern format with contentItems + if (content.contentItems && Array.isArray(content.contentItems)) { + console.log('🆕 ContentItems format detected'); + const vocabItems = content.contentItems.filter(item => item.type === 'vocabulary'); + if (vocabItems.length > 0) { + vocabulary = vocabItems[0].items || []; + } + } + // Legacy format with vocabulary array + else if (content.vocabulary && Array.isArray(content.vocabulary)) { + console.log('📚 Vocabulary array format detected'); + vocabulary = content.vocabulary; + } + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + console.log('🔧 Extracting from raw content:', rawContent.name || 'Module'); + let vocabulary = []; + + // Check vocabulary object format (key-value pairs) + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({ + english: english, + french: translation + })); + console.log(`📝 ${vocabulary.length} vocabulary pairs extracted from object`); + } + // Check vocabulary array format + else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) { + vocabulary = rawContent.vocabulary; + console.log(`📚 ${vocabulary.length} vocabulary items extracted from array`); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Filter and validate vocabulary + vocabulary = vocabulary.filter(item => + item && + item.english && + (item.french || item.translation || item.chinese) + ).map(item => ({ + english: item.english, + french: item.french || item.translation || item.chinese + })); + + if (vocabulary.length === 0) { + console.error('❌ No valid vocabulary found'); + // Demo vocabulary as fallback + vocabulary = [ + { english: "cat", french: "chat" }, + { english: "dog", french: "chien" }, + { english: "house", french: "maison" }, + { english: "car", french: "voiture" }, + { english: "book", french: "livre" }, + { english: "water", french: "eau" }, + { english: "food", french: "nourriture" }, + { english: "friend", french: "ami" } + ]; + console.warn('🚨 Using demo vocabulary'); + } + + console.log(`✅ Memory Match: ${vocabulary.length} vocabulary items finalized`); + return vocabulary; + } + + createGameInterface() { + this.container.innerHTML = ` +
+ +
+
+ Moves: + 0 +
+
+ Pairs: + 0 / ${this.totalPairs} +
+
+ Score: + 0 +
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ `; + } + + generateCards() { + // Select random vocabulary pairs + const selectedVocab = this.vocabulary + .sort(() => Math.random() - 0.5) + .slice(0, this.totalPairs); + + // Create card pairs + this.cards = []; + selectedVocab.forEach((item, index) => { + // English card + this.cards.push({ + id: `en_${index}`, + content: item.english, + type: 'english', + pairId: index, + isFlipped: false, + isMatched: false + }); + + // French card + this.cards.push({ + id: `fr_${index}`, + content: item.french, + type: 'french', + pairId: index, + isFlipped: false, + isMatched: false + }); + }); + + // Shuffle cards + this.cards.sort(() => Math.random() - 0.5); + + // Render cards + this.renderCards(); + } + + renderCards() { + const grid = document.getElementById('memory-grid'); + grid.innerHTML = ''; + + this.cards.forEach((card, index) => { + const cardElement = document.createElement('div'); + cardElement.className = 'memory-card'; + cardElement.dataset.cardIndex = index; + + cardElement.innerHTML = ` +
+
+ 🎯 +
+
+ ${card.content} +
+
+ `; + + cardElement.addEventListener('click', () => this.flipCard(index)); + grid.appendChild(cardElement); + }); + } + + setupEventListeners() { + document.getElementById('restart-btn').addEventListener('click', () => this.restart()); + document.getElementById('hint-btn').addEventListener('click', () => this.showHint()); + } + + flipCard(cardIndex) { + if (this.isFlipping) return; + + const card = this.cards[cardIndex]; + if (card.isFlipped || card.isMatched) return; + + // Flip the card + card.isFlipped = true; + this.updateCardDisplay(cardIndex); + this.flippedCards.push(cardIndex); + + if (this.flippedCards.length === 2) { + this.moves++; + this.updateStats(); + this.checkMatch(); + } + } + + updateCardDisplay(cardIndex) { + const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`); + const card = this.cards[cardIndex]; + + if (card.isFlipped || card.isMatched) { + cardElement.classList.add('flipped'); + } else { + cardElement.classList.remove('flipped'); + } + + if (card.isMatched) { + cardElement.classList.add('matched'); + } + } + + checkMatch() { + this.isFlipping = true; + + setTimeout(() => { + const [firstIndex, secondIndex] = this.flippedCards; + const firstCard = this.cards[firstIndex]; + const secondCard = this.cards[secondIndex]; + + if (firstCard.pairId === secondCard.pairId) { + // Match found! + firstCard.isMatched = true; + secondCard.isMatched = true; + this.updateCardDisplay(firstIndex); + this.updateCardDisplay(secondIndex); + + this.matchedPairs++; + this.score += 100; + this.showFeedback('Great match! 🎉', 'success'); + + // Trigger success animation + this.triggerSuccessAnimation(firstIndex, secondIndex); + + if (this.matchedPairs === this.totalPairs) { + setTimeout(() => this.gameComplete(), 800); + } + } else { + // No match, flip back and apply penalty + firstCard.isFlipped = false; + secondCard.isFlipped = false; + this.updateCardDisplay(firstIndex); + this.updateCardDisplay(secondIndex); + + // Apply penalty but don't go below 0 + this.score = Math.max(0, this.score - 10); + this.showFeedback('Try again! (-10 points)', 'warning'); + } + + this.flippedCards = []; + this.isFlipping = false; + this.updateStats(); + }, 1000); + } + + showHint() { + if (this.flippedCards.length > 0) { + this.showFeedback('Finish your current move first!', 'warning'); + return; + } + + // Find first unmatched pair + const unmatchedCards = this.cards.filter(card => !card.isMatched); + if (unmatchedCards.length === 0) return; + + // Group by pairId + const pairs = {}; + unmatchedCards.forEach((card, index) => { + const actualIndex = this.cards.indexOf(card); + if (!pairs[card.pairId]) { + pairs[card.pairId] = []; + } + pairs[card.pairId].push(actualIndex); + }); + + // Find first complete pair + const completePair = Object.values(pairs).find(pair => pair.length === 2); + if (completePair) { + // Briefly show the pair + completePair.forEach(cardIndex => { + const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`); + cardElement.classList.add('hint'); + }); + + setTimeout(() => { + completePair.forEach(cardIndex => { + const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`); + cardElement.classList.remove('hint'); + }); + }, 2000); + + this.showFeedback('Hint shown for 2 seconds!', 'info'); + } + } + + updateStats() { + document.getElementById('moves-counter').textContent = this.moves; + document.getElementById('pairs-counter').textContent = `${this.matchedPairs} / ${this.totalPairs}`; + document.getElementById('score-counter').textContent = this.score; + this.onScoreUpdate(this.score); + } + + gameComplete() { + // Calculate bonus based on moves + const perfectMoves = this.totalPairs; + if (this.moves <= perfectMoves + 5) { + this.score += 200; // Efficiency bonus + } + + this.updateStats(); + this.showFeedback('🎉 Congratulations! All pairs found!', 'success'); + + setTimeout(() => { + this.onGameEnd(this.score); + }, 2000); + } + + showFeedback(message, type = 'info') { + const feedbackArea = document.getElementById('feedback-area'); + feedbackArea.innerHTML = `
${message}
`; + } + + start() { + console.log('🧠 Memory Match: Starting'); + this.showFeedback('Find matching English-French pairs!', 'info'); + } + + restart() { + console.log('🔄 Memory Match: Restarting'); + this.reset(); + this.start(); + } + + reset() { + this.flippedCards = []; + this.matchedPairs = 0; + this.moves = 0; + this.score = 0; + this.isFlipping = false; + this.generateCards(); + this.updateStats(); + } + + triggerSuccessAnimation(cardIndex1, cardIndex2) { + // Get card elements + const card1 = document.querySelector(`[data-card-index="${cardIndex1}"]`); + const card2 = document.querySelector(`[data-card-index="${cardIndex2}"]`); + + if (!card1 || !card2) return; + + // Add success animation class + card1.classList.add('success-animation'); + card2.classList.add('success-animation'); + + // Create sparkle particles for both cards + this.createSparkleParticles(card1); + this.createSparkleParticles(card2); + + // Remove animation class after animation completes + setTimeout(() => { + card1.classList.remove('success-animation'); + card2.classList.remove('success-animation'); + }, 800); + } + + createSparkleParticles(cardElement) { + const rect = cardElement.getBoundingClientRect(); + + // Create 4 sparkle particles around the card + for (let i = 1; i <= 4; i++) { + const particle = document.createElement('div'); + particle.className = `success-particle particle-${i}`; + + // Position relative to card + particle.style.position = 'fixed'; + particle.style.left = (rect.left + rect.width / 2) + 'px'; + particle.style.top = (rect.top + rect.height / 2) + 'px'; + particle.style.pointerEvents = 'none'; + particle.style.zIndex = '1000'; + + document.body.appendChild(particle); + + // Remove particle after animation + setTimeout(() => { + if (particle.parentNode) { + particle.parentNode.removeChild(particle); + } + }, 1200); + } + } + + destroy() { + this.container.innerHTML = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.MemoryMatch = MemoryMatchGame; \ No newline at end of file diff --git a/js/games/quiz-game.js b/js/games/quiz-game.js new file mode 100644 index 0000000..f1ef498 --- /dev/null +++ b/js/games/quiz-game.js @@ -0,0 +1,355 @@ +// === MODULE QUIZ GAME === + +class QuizGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Game state + this.vocabulary = []; + this.currentQuestion = 0; + this.totalQuestions = 10; + this.score = 0; + this.correctAnswers = 0; + this.currentQuestionData = null; + this.hasAnswered = false; + + // Extract vocabulary + this.vocabulary = this.extractVocabulary(this.content); + + this.init(); + } + + init() { + // Check if we have enough vocabulary + if (!this.vocabulary || this.vocabulary.length < 4) { + console.error('Not enough vocabulary for Quiz Game'); + this.showInitError(); + return; + } + + // Adjust total questions based on available vocabulary + this.totalQuestions = Math.min(this.totalQuestions, this.vocabulary.length); + + this.createGameInterface(); + this.generateQuestion(); + } + + showInitError() { + this.container.innerHTML = ` +
+

❌ Error loading

+

This content doesn't have enough vocabulary for Quiz Game.

+

The game needs at least 4 vocabulary items.

+ +
+ `; + } + + extractVocabulary(content) { + let vocabulary = []; + + console.log('📝 Extracting vocabulary from:', content?.name || 'content'); + + // Use raw module content if available + if (content.rawContent) { + console.log('📦 Using raw module content'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Modern format with contentItems + if (content.contentItems && Array.isArray(content.contentItems)) { + console.log('🆕 ContentItems format detected'); + const vocabItems = content.contentItems.filter(item => item.type === 'vocabulary'); + if (vocabItems.length > 0) { + vocabulary = vocabItems[0].items || []; + } + } + // Legacy format with vocabulary array + else if (content.vocabulary && Array.isArray(content.vocabulary)) { + console.log('📚 Vocabulary array format detected'); + vocabulary = content.vocabulary; + } + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + console.log('🔧 Extracting from raw content:', rawContent.name || 'Module'); + let vocabulary = []; + + // Check vocabulary object format (key-value pairs) + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({ + english: english, + french: translation + })); + console.log(`📝 ${vocabulary.length} vocabulary pairs extracted from object`); + } + // Check vocabulary array format + else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) { + vocabulary = rawContent.vocabulary; + console.log(`📚 ${vocabulary.length} vocabulary items extracted from array`); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Filter and validate vocabulary + vocabulary = vocabulary.filter(item => + item && + item.english && + (item.french || item.translation || item.chinese) + ).map(item => ({ + english: item.english, + french: item.french || item.translation || item.chinese + })); + + if (vocabulary.length === 0) { + console.error('❌ No valid vocabulary found'); + // Demo vocabulary as fallback + vocabulary = [ + { english: "cat", french: "chat" }, + { english: "dog", french: "chien" }, + { english: "house", french: "maison" }, + { english: "car", french: "voiture" }, + { english: "book", french: "livre" }, + { english: "water", french: "eau" }, + { english: "food", french: "nourriture" }, + { english: "friend", french: "ami" } + ]; + console.warn('🚨 Using demo vocabulary'); + } + + // Shuffle vocabulary for random questions + vocabulary = vocabulary.sort(() => Math.random() - 0.5); + + console.log(`✅ Quiz Game: ${vocabulary.length} vocabulary items finalized`); + return vocabulary; + } + + createGameInterface() { + this.container.innerHTML = ` +
+ +
+
+
+
+
+ 1 / ${this.totalQuestions} + Score: 0 +
+
+ + +
+
+ Loading question... +
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ `; + + this.setupEventListeners(); + } + + setupEventListeners() { + document.getElementById('next-btn').addEventListener('click', () => this.nextQuestion()); + document.getElementById('restart-btn').addEventListener('click', () => this.restart()); + } + + generateQuestion() { + if (this.currentQuestion >= this.totalQuestions) { + this.gameComplete(); + return; + } + + this.hasAnswered = false; + + // Get current vocabulary item + const correctAnswer = this.vocabulary[this.currentQuestion]; + + // Generate 3 wrong answers from other vocabulary items + const wrongAnswers = this.vocabulary + .filter(item => item !== correctAnswer) + .sort(() => Math.random() - 0.5) + .slice(0, 3) + .map(item => item.french); + + // Combine and shuffle all options + const allOptions = [correctAnswer.french, ...wrongAnswers].sort(() => Math.random() - 0.5); + + this.currentQuestionData = { + question: correctAnswer.english, + correctAnswer: correctAnswer.french, + options: allOptions + }; + + this.renderQuestion(); + this.updateProgress(); + } + + renderQuestion() { + const { question, options } = this.currentQuestionData; + + // Update question text + document.getElementById('question-text').innerHTML = ` + What is the translation of "${question}"? + `; + + // Clear and generate options + const optionsArea = document.getElementById('options-area'); + optionsArea.innerHTML = ''; + + options.forEach((option, index) => { + const optionButton = document.createElement('button'); + optionButton.className = 'quiz-option'; + optionButton.textContent = option; + optionButton.addEventListener('click', () => this.selectAnswer(option, optionButton)); + optionsArea.appendChild(optionButton); + }); + + // Hide next button + document.getElementById('next-btn').style.display = 'none'; + } + + selectAnswer(selectedAnswer, buttonElement) { + if (this.hasAnswered) return; + + this.hasAnswered = true; + const isCorrect = selectedAnswer === this.currentQuestionData.correctAnswer; + + // Disable all option buttons and show results + const allOptions = document.querySelectorAll('.quiz-option'); + allOptions.forEach(btn => { + btn.disabled = true; + + if (btn.textContent === this.currentQuestionData.correctAnswer) { + btn.classList.add('correct'); + } else if (btn === buttonElement && !isCorrect) { + btn.classList.add('wrong'); + } else if (btn !== buttonElement && btn.textContent !== this.currentQuestionData.correctAnswer) { + btn.classList.add('disabled'); + } + }); + + // Update score and feedback + if (isCorrect) { + this.correctAnswers++; + this.score += 10; + this.showFeedback('✅ Correct! Well done!', 'success'); + } else { + this.score = Math.max(0, this.score - 5); + this.showFeedback(`❌ Wrong! Correct answer: "${this.currentQuestionData.correctAnswer}"`, 'error'); + } + + this.updateScore(); + + // Show next button or finish + if (this.currentQuestion < this.totalQuestions - 1) { + document.getElementById('next-btn').style.display = 'block'; + } else { + setTimeout(() => this.gameComplete(), 2000); + } + } + + nextQuestion() { + this.currentQuestion++; + this.generateQuestion(); + } + + updateProgress() { + const progressFill = document.getElementById('progress-fill'); + const progressPercent = ((this.currentQuestion + 1) / this.totalQuestions) * 100; + progressFill.style.width = `${progressPercent}%`; + + document.getElementById('question-counter').textContent = + `${this.currentQuestion + 1} / ${this.totalQuestions}`; + } + + updateScore() { + document.getElementById('score-display').textContent = `Score: ${this.score}`; + this.onScoreUpdate(this.score); + } + + gameComplete() { + const accuracy = Math.round((this.correctAnswers / this.totalQuestions) * 100); + + // Bonus for high accuracy + if (accuracy >= 90) { + this.score += 50; // Excellence bonus + } else if (accuracy >= 70) { + this.score += 20; // Good performance bonus + } + + this.updateScore(); + this.showFeedback( + `🎉 Quiz completed! ${this.correctAnswers}/${this.totalQuestions} correct (${accuracy}%)`, + 'success' + ); + + setTimeout(() => { + this.onGameEnd(this.score); + }, 3000); + } + + showFeedback(message, type = 'info') { + const feedbackArea = document.getElementById('feedback-area'); + feedbackArea.innerHTML = `
${message}
`; + } + + start() { + console.log('❓ Quiz Game: Starting'); + this.showFeedback('Choose the correct translation for each word!', 'info'); + } + + restart() { + console.log('🔄 Quiz Game: Restarting'); + this.reset(); + this.start(); + } + + reset() { + this.currentQuestion = 0; + this.score = 0; + this.correctAnswers = 0; + this.hasAnswered = false; + this.currentQuestionData = null; + + // Re-shuffle vocabulary + this.vocabulary = this.vocabulary.sort(() => Math.random() - 0.5); + + this.generateQuestion(); + this.updateScore(); + } + + destroy() { + this.container.innerHTML = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.QuizGame = QuizGame; \ No newline at end of file diff --git a/js/games/story-builder.js b/js/games/story-builder.js new file mode 100644 index 0000000..b1ba124 --- /dev/null +++ b/js/games/story-builder.js @@ -0,0 +1,701 @@ +// === STORY BUILDER GAME - CONSTRUCTEUR D'HISTOIRES === + +class StoryBuilderGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.contentEngine = options.contentEngine; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // État du jeu + this.score = 0; + this.currentStory = []; + this.availableElements = []; + this.storyTarget = null; + this.gameMode = 'sequence'; // 'sequence', 'dialogue', 'scenario' + + // Configuration + this.maxElements = 6; + this.timeLimit = 180; // 3 minutes + this.timeLeft = this.timeLimit; + this.isRunning = false; + + // Timers + this.gameTimer = null; + + this.init(); + } + + init() { + this.createGameBoard(); + this.setupEventListeners(); + this.loadStoryContent(); + } + + createGameBoard() { + this.container.innerHTML = ` +
+ +
+ + + +
+ + +
+
+

Objectif:

+

Choisis un mode et commençons !

+
+
+
+ ${this.timeLeft} + Temps +
+
+ 0/${this.maxElements} + Progrès +
+
+
+ + +
+
+ +
+ +
+
Glisse les éléments ici pour construire ton histoire
+
+
+ + +
+ +
+ + +
+ + + + +
+ + + +
+ `; + } + + setupEventListeners() { + // Mode selection + document.querySelectorAll('.mode-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + if (this.isRunning) return; + + document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this.gameMode = btn.dataset.mode; + + this.loadStoryContent(); + }); + }); + + // Game controls + document.getElementById('start-btn').addEventListener('click', () => this.start()); + document.getElementById('check-btn').addEventListener('click', () => this.checkStory()); + document.getElementById('hint-btn').addEventListener('click', () => this.showHint()); + document.getElementById('restart-btn').addEventListener('click', () => this.restart()); + + // Drag and Drop setup + this.setupDragAndDrop(); + } + + loadStoryContent() { + if (!this.contentEngine) { + console.warn('ContentEngine non disponible, utilisation du contenu de base'); + this.setupBasicContent(); + return; + } + + // Filtrer le contenu selon le mode + const filters = this.getModeFilters(); + const filteredContent = this.contentEngine.filterContent(this.content, filters); + + this.setupContentForMode(filteredContent); + } + + getModeFilters() { + switch (this.gameMode) { + case 'sequence': + return { type: ['sequence', 'vocabulary'] }; + case 'dialogue': + return { type: ['dialogue', 'sentence'] }; + case 'scenario': + return { type: ['scenario', 'dialogue', 'sequence'] }; + default: + return { type: ['vocabulary', 'sentence'] }; + } + } + + setupContentForMode(filteredContent) { + const contentItems = filteredContent.contentItems || []; + + switch (this.gameMode) { + case 'sequence': + this.setupSequenceMode(contentItems); + break; + case 'dialogue': + this.setupDialogueMode(contentItems); + break; + case 'scenario': + this.setupScenarioMode(contentItems); + break; + } + } + + setupSequenceMode(contentItems) { + const sequences = contentItems.filter(item => item.type === 'sequence'); + + if (sequences.length > 0) { + this.storyTarget = sequences[Math.floor(Math.random() * sequences.length)]; + this.availableElements = this.shuffleArray([...this.storyTarget.content.steps]); + + document.getElementById('objective-text').textContent = + `Remets en ordre l'histoire: "${this.storyTarget.content.title}"`; + } else { + this.setupBasicSequence(); + } + } + + setupDialogueMode(contentItems) { + const dialogues = contentItems.filter(item => item.type === 'dialogue'); + + if (dialogues.length > 0) { + this.storyTarget = dialogues[Math.floor(Math.random() * dialogues.length)]; + this.availableElements = this.shuffleArray([...this.storyTarget.content.conversation]); + + document.getElementById('objective-text').textContent = + `Reconstitue le dialogue: "${this.storyTarget.content.english}"`; + } else { + this.setupBasicDialogue(); + } + } + + setupScenarioMode(contentItems) { + const scenarios = contentItems.filter(item => item.type === 'scenario'); + + if (scenarios.length > 0) { + this.storyTarget = scenarios[Math.floor(Math.random() * scenarios.length)]; + // Mélanger vocabulaire et phrases du scénario + const vocabElements = this.storyTarget.content.vocabulary || []; + const phraseElements = this.storyTarget.content.phrases || []; + + this.availableElements = this.shuffleArray([...vocabElements, ...phraseElements]); + + document.getElementById('objective-text').textContent = + `Crée une histoire dans le contexte: "${this.storyTarget.content.english}"`; + } else { + this.setupBasicScenario(); + } + } + + setupBasicContent() { + // Fallback pour l'ancien format + const vocabulary = this.content.vocabulary || []; + this.availableElements = vocabulary.slice(0, 6); + this.gameMode = 'vocabulary'; + + document.getElementById('objective-text').textContent = + 'Construis une histoire avec ces mots !'; + } + + start() { + if (this.isRunning || this.availableElements.length === 0) return; + + this.isRunning = true; + this.score = 0; + this.currentStory = []; + this.timeLeft = this.timeLimit; + + this.renderElements(); + this.startTimer(); + this.updateUI(); + + document.getElementById('start-btn').disabled = true; + document.getElementById('check-btn').disabled = false; + document.getElementById('hint-btn').disabled = false; + + this.showFeedback('Glisse les éléments dans l\'ordre pour construire ton histoire !', 'info'); + } + + renderElements() { + const elementsBank = document.getElementById('elements-bank'); + elementsBank.innerHTML = '

Éléments disponibles:

'; + + this.availableElements.forEach((element, index) => { + const elementDiv = this.createElement(element, index); + elementsBank.appendChild(elementDiv); + }); + } + + createElement(element, index) { + const div = document.createElement('div'); + div.className = 'story-element'; + div.draggable = true; + div.dataset.index = index; + + // Adapter l'affichage selon le type d'élément + if (element.english && element.french) { + // Vocabulaire ou phrase + div.innerHTML = ` +
+
${element.english}
+
${element.french}
+
+ `; + } else if (element.text || element.english) { + // Dialogue ou séquence + div.innerHTML = ` +
+
${element.text || element.english}
+ ${element.french ? `
${element.french}
` : ''} +
+ `; + } else if (typeof element === 'string') { + // Texte simple + div.innerHTML = `
${element}
`; + } + + if (element.icon) { + div.innerHTML = `${element.icon}` + div.innerHTML; + } + + return div; + } + + setupDragAndDrop() { + let draggedElement = null; + + document.addEventListener('dragstart', (e) => { + if (e.target.classList.contains('story-element')) { + draggedElement = e.target; + e.target.style.opacity = '0.5'; + } + }); + + document.addEventListener('dragend', (e) => { + if (e.target.classList.contains('story-element')) { + e.target.style.opacity = '1'; + draggedElement = null; + } + }); + + const dropZone = document.getElementById('drop-zone'); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('drag-over'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('drag-over'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('drag-over'); + + if (draggedElement && this.isRunning) { + this.addToStory(draggedElement); + } + }); + } + + addToStory(elementDiv) { + const index = parseInt(elementDiv.dataset.index); + const element = this.availableElements[index]; + + // Ajouter à l'histoire + this.currentStory.push({ element, originalIndex: index }); + + // Créer élément dans la zone de construction + const storyElement = elementDiv.cloneNode(true); + storyElement.classList.add('in-story'); + storyElement.draggable = false; + + // Ajouter bouton de suppression + const removeBtn = document.createElement('button'); + removeBtn.className = 'remove-element'; + removeBtn.innerHTML = '×'; + removeBtn.onclick = () => this.removeFromStory(storyElement, element); + storyElement.appendChild(removeBtn); + + document.getElementById('drop-zone').appendChild(storyElement); + + // Masquer l'élément original + elementDiv.style.display = 'none'; + + this.updateProgress(); + } + + removeFromStory(storyElement, element) { + // Supprimer de l'histoire + this.currentStory = this.currentStory.filter(item => item.element !== element); + + // Supprimer visuellement + storyElement.remove(); + + // Réafficher l'élément original + const originalElement = document.querySelector(`[data-index="${this.availableElements.indexOf(element)}"]`); + if (originalElement) { + originalElement.style.display = 'block'; + } + + this.updateProgress(); + } + + checkStory() { + if (this.currentStory.length === 0) { + this.showFeedback('Ajoute au moins un élément à ton histoire !', 'error'); + return; + } + + const isCorrect = this.validateStory(); + + if (isCorrect) { + this.score += this.currentStory.length * 10; + this.showFeedback('Bravo ! Histoire parfaite ! 🎉', 'success'); + this.onScoreUpdate(this.score); + + setTimeout(() => { + this.nextChallenge(); + }, 2000); + } else { + this.score = Math.max(0, this.score - 5); + this.showFeedback('Presque ! Vérifie l\'ordre de ton histoire 🤔', 'warning'); + this.onScoreUpdate(this.score); + } + } + + validateStory() { + switch (this.gameMode) { + case 'sequence': + return this.validateSequence(); + case 'dialogue': + return this.validateDialogue(); + case 'scenario': + return this.validateScenario(); + default: + return true; // Mode libre + } + } + + validateSequence() { + if (!this.storyTarget?.content?.steps) return true; + + const expectedOrder = this.storyTarget.content.steps.sort((a, b) => a.order - b.order); + + if (this.currentStory.length !== expectedOrder.length) return false; + + return this.currentStory.every((item, index) => { + const expected = expectedOrder[index]; + return item.element.order === expected.order; + }); + } + + validateDialogue() { + // Validation flexible du dialogue (ordre logique des répliques) + return this.currentStory.length >= 2; + } + + validateScenario() { + // Validation flexible du scénario (cohérence contextuelle) + return this.currentStory.length >= 3; + } + + showHint() { + if (!this.storyTarget) { + this.showFeedback('Astuce : Pense à l\'ordre logique des événements !', 'info'); + return; + } + + switch (this.gameMode) { + case 'sequence': + if (this.storyTarget.content?.steps) { + const nextStep = this.storyTarget.content.steps.find(step => + !this.currentStory.some(item => item.element.order === step.order) + ); + if (nextStep) { + this.showFeedback(`Prochaine étape : "${nextStep.english}"`, 'info'); + } + } + break; + case 'dialogue': + this.showFeedback('Pense à l\'ordre naturel d\'une conversation !', 'info'); + break; + case 'scenario': + this.showFeedback('Crée une histoire cohérente dans ce contexte !', 'info'); + break; + } + } + + nextChallenge() { + // Charger un nouveau défi + this.loadStoryContent(); + this.currentStory = []; + document.getElementById('drop-zone').innerHTML = '
Glisse les éléments ici pour construire ton histoire
'; + this.renderElements(); + this.updateProgress(); + } + + startTimer() { + this.gameTimer = setInterval(() => { + this.timeLeft--; + this.updateUI(); + + if (this.timeLeft <= 0) { + this.endGame(); + } + }, 1000); + } + + endGame() { + this.isRunning = false; + if (this.gameTimer) { + clearInterval(this.gameTimer); + this.gameTimer = null; + } + + document.getElementById('start-btn').disabled = false; + document.getElementById('check-btn').disabled = true; + document.getElementById('hint-btn').disabled = true; + + this.onGameEnd(this.score); + } + + restart() { + this.endGame(); + this.score = 0; + this.currentStory = []; + this.timeLeft = this.timeLimit; + this.onScoreUpdate(0); + + document.getElementById('drop-zone').innerHTML = '
Glisse les éléments ici pour construire ton histoire
'; + this.loadStoryContent(); + this.updateUI(); + } + + updateProgress() { + document.getElementById('story-progress').textContent = + `${this.currentStory.length}/${this.maxElements}`; + } + + updateUI() { + document.getElementById('time-left').textContent = this.timeLeft; + } + + showFeedback(message, type = 'info') { + const feedbackArea = document.getElementById('feedback-area'); + feedbackArea.innerHTML = `
${message}
`; + } + + 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; + } + + destroy() { + this.endGame(); + this.container.innerHTML = ''; + } +} + +// CSS pour Story Builder +const storyBuilderStyles = ` + +`; + +// Ajouter les styles +document.head.insertAdjacentHTML('beforeend', storyBuilderStyles); + +// Enregistrement du module +window.GameModules = window.GameModules || {}; +window.GameModules.StoryBuilder = StoryBuilderGame; \ No newline at end of file diff --git a/js/games/temp-games.js b/js/games/temp-games.js new file mode 100644 index 0000000..4d962be --- /dev/null +++ b/js/games/temp-games.js @@ -0,0 +1,736 @@ +// === MODULE JEUX TEMPORAIRES === + +class TempGamesModule { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + this.currentGame = null; + this.availableGames = [ + { + id: 'word-match', + name: 'Word Match', + icon: '🎯', + description: 'Associe les mots anglais avec leur traduction', + difficulty: 'easy' + }, + { + id: 'quick-translation', + name: 'Quick Translation', + icon: '⚡', + description: 'Traduis le mot le plus rapidement possible', + difficulty: 'medium' + }, + { + id: 'word-builder', + name: 'Word Builder', + icon: '🔤', + description: 'Reconstitue le mot lettre par lettre', + difficulty: 'medium' + } + ]; + + this.init(); + } + + init() { + this.showGameSelector(); + } + + showGameSelector() { + this.container.innerHTML = ` +
+
+

🎯 Mini-Jeux Temporaires

+

Sélectionne un mini-jeu pour t'amuser avec le vocabulaire !

+
+ +
+ ${this.availableGames.map(game => this.createGameCard(game)).join('')} +
+ +
+

Ces jeux sont en développement et seront bientôt des modules complets !

+
+
+ `; + + this.setupGameSelector(); + } + + createGameCard(game) { + const difficultyColor = { + easy: '#10B981', + medium: '#F59E0B', + hard: '#EF4444' + }[game.difficulty]; + + return ` +
+
${game.icon}
+

${game.name}

+

${game.description}

+
+ ${game.difficulty.toUpperCase()} +
+ +
+ `; + } + + setupGameSelector() { + document.querySelectorAll('.mini-game-card').forEach(card => { + card.addEventListener('click', () => { + const gameId = card.dataset.game; + this.startMiniGame(gameId); + }); + }); + } + + startMiniGame(gameId) { + const game = this.availableGames.find(g => g.id === gameId); + if (!game) return; + + switch(gameId) { + case 'word-match': + this.startWordMatch(); + break; + case 'quick-translation': + this.startQuickTranslation(); + break; + case 'word-builder': + this.startWordBuilder(); + break; + } + } + + // === WORD MATCH GAME === + startWordMatch() { + this.container.innerHTML = ` +
+
+ +

🎯 Word Match

+
Score: 0
+
+ +
+
+ +
+
+ +
+
+ +
+ Clique sur un mot anglais, puis sur sa traduction française ! +
+
+ `; + + this.setupWordMatch(); + } + + setupWordMatch() { + document.querySelector('.back-to-selector').addEventListener('click', () => { + this.showGameSelector(); + }); + + const words = this.content.vocabulary.slice(0, 6).map(w => ({ + english: w.english, + french: w.french + })); + + const shuffledFrench = [...words].sort(() => Math.random() - 0.5); + + const englishContainer = document.getElementById('english-words'); + const frenchContainer = document.getElementById('french-words'); + + let selectedEnglish = null; + let matchedPairs = 0; + let score = 0; + + words.forEach((word, index) => { + const englishBtn = document.createElement('button'); + englishBtn.className = 'word-btn english-btn'; + englishBtn.textContent = word.english; + englishBtn.dataset.word = word.english; + englishContainer.appendChild(englishBtn); + + englishBtn.addEventListener('click', () => { + document.querySelectorAll('.english-btn').forEach(btn => + btn.classList.remove('selected')); + englishBtn.classList.add('selected'); + selectedEnglish = word.english; + }); + }); + + shuffledFrench.forEach(word => { + const frenchBtn = document.createElement('button'); + frenchBtn.className = 'word-btn french-btn'; + frenchBtn.textContent = word.french; + frenchBtn.dataset.word = word.french; + frenchContainer.appendChild(frenchBtn); + + frenchBtn.addEventListener('click', () => { + if (!selectedEnglish) { + document.getElementById('match-feedback').textContent = + 'Sélectionne d\'abord un mot anglais !'; + return; + } + + const correctWord = words.find(w => w.english === selectedEnglish); + if (correctWord && correctWord.french === word.french) { + // Correct match + score += 10; + matchedPairs++; + + document.querySelector(`[data-word="${selectedEnglish}"]`).classList.add('matched'); + frenchBtn.classList.add('matched'); + + document.getElementById('match-feedback').textContent = 'Parfait ! 🎉'; + + if (matchedPairs === words.length) { + setTimeout(() => { + alert(`Félicitations ! Score final: ${score}`); + this.onGameEnd(score); + }, 1000); + } + } else { + // Wrong match + score = Math.max(0, score - 2); + document.getElementById('match-feedback').textContent = + `Non, "${selectedEnglish}" ne correspond pas à "${word.french}"`; + } + + document.getElementById('match-score').textContent = score; + this.onScoreUpdate(score); + selectedEnglish = null; + document.querySelectorAll('.english-btn').forEach(btn => + btn.classList.remove('selected')); + }); + }); + } + + // === QUICK TRANSLATION GAME === + startQuickTranslation() { + this.container.innerHTML = ` +
+
+ +

⚡ Quick Translation

+
+ Score: 0 + Temps: 30s +
+
+ +
+
+

---

+

Traduction en français :

+
+ +
+ +
+
+ + +
+ `; + + this.setupQuickTranslation(); + } + + setupQuickTranslation() { + document.querySelector('.back-to-selector').addEventListener('click', () => { + this.showGameSelector(); + }); + + let currentWordIndex = 0; + let score = 0; + let timeLeft = 30; + let gameTimer = null; + let isPlaying = false; + + const words = this.shuffleArray([...this.content.vocabulary]).slice(0, 10); + + document.getElementById('start-quick-game').addEventListener('click', () => { + if (isPlaying) return; + + isPlaying = true; + currentWordIndex = 0; + score = 0; + timeLeft = 30; + + document.getElementById('start-quick-game').style.display = 'none'; + + gameTimer = setInterval(() => { + timeLeft--; + document.getElementById('quick-time').textContent = timeLeft; + + if (timeLeft <= 0) { + clearInterval(gameTimer); + alert(`Temps écoulé ! Score final: ${score}`); + this.onGameEnd(score); + } + }, 1000); + + this.showQuickWord(); + }); + + const showQuickWord = () => { + if (currentWordIndex >= words.length) { + clearInterval(gameTimer); + alert(`Bravo ! Score final: ${score}`); + this.onGameEnd(score); + return; + } + + const currentWord = words[currentWordIndex]; + document.getElementById('current-word').textContent = currentWord.english; + + // Créer 4 options (1 correcte + 3 fausses) + const options = [currentWord.french]; + const otherWords = words.filter(w => w.french !== currentWord.french); + + for (let i = 0; i < 3; i++) { + if (otherWords[i]) { + options.push(otherWords[i].french); + } + } + + const shuffledOptions = this.shuffleArray(options); + const optionsContainer = document.getElementById('translation-options'); + optionsContainer.innerHTML = ''; + + shuffledOptions.forEach(option => { + const btn = document.createElement('button'); + btn.className = 'option-btn'; + btn.textContent = option; + + btn.addEventListener('click', () => { + if (option === currentWord.french) { + score += 5; + btn.classList.add('correct'); + setTimeout(() => { + currentWordIndex++; + this.showQuickWord(); + }, 500); + } else { + score = Math.max(0, score - 1); + btn.classList.add('wrong'); + document.querySelector(`button:contains("${currentWord.french}")`); + } + + document.getElementById('quick-score').textContent = score; + this.onScoreUpdate(score); + + // Désactiver tous les boutons + document.querySelectorAll('.option-btn').forEach(b => b.disabled = true); + }); + + optionsContainer.appendChild(btn); + }); + }; + + this.showQuickWord = showQuickWord; + } + + // === WORD BUILDER GAME === + startWordBuilder() { + this.container.innerHTML = ` +
+
+ +

🔤 Word Builder

+
Score: 0
+
+ +
+
+

Traduction : ---

+
+ +
+
+ +
+ +
+ +
+
+ +
+ + +
+
+
+ `; + + this.setupWordBuilder(); + } + + setupWordBuilder() { + document.querySelector('.back-to-selector').addEventListener('click', () => { + this.showGameSelector(); + }); + + let currentWordIndex = 0; + let score = 0; + const words = this.shuffleArray([...this.content.vocabulary]).slice(0, 8); + + const showBuilderWord = () => { + if (currentWordIndex >= words.length) { + alert(`Félicitations ! Score final: ${score}`); + this.onGameEnd(score); + return; + } + + const currentWord = words[currentWordIndex]; + const wordSpaces = document.getElementById('word-spaces'); + const availableLetters = document.getElementById('available-letters'); + + document.getElementById('french-hint').textContent = currentWord.french; + + // Créer les espaces + wordSpaces.innerHTML = ''; + currentWord.english.split('').forEach((letter, index) => { + const space = document.createElement('div'); + space.className = 'letter-space'; + space.dataset.index = index; + space.dataset.letter = letter.toLowerCase(); + wordSpaces.appendChild(space); + }); + + // Créer les lettres mélangées + quelques lettres supplémentaires + const wordLetters = currentWord.english.toLowerCase().split(''); + const extraLetters = 'abcdefghijklmnopqrstuvwxyz'.split('') + .filter(l => !wordLetters.includes(l)) + .slice(0, 3); + + const allLetters = this.shuffleArray([...wordLetters, ...extraLetters]); + + availableLetters.innerHTML = ''; + allLetters.forEach(letter => { + const letterBtn = document.createElement('button'); + letterBtn.className = 'letter-btn'; + letterBtn.textContent = letter.toUpperCase(); + letterBtn.dataset.letter = letter; + + letterBtn.addEventListener('click', () => { + this.placeLetter(letter, letterBtn); + }); + + availableLetters.appendChild(letterBtn); + }); + + document.getElementById('next-word-btn').style.display = 'none'; + document.getElementById('give-up-btn').style.display = 'inline-block'; + }; + + const placeLetter = (letter, btn) => { + const emptySpace = document.querySelector( + `.letter-space[data-letter="${letter}"]:not(.filled)` + ); + + if (emptySpace) { + emptySpace.textContent = letter.toUpperCase(); + emptySpace.classList.add('filled'); + btn.disabled = true; + + // Vérifier si le mot est complet + const allSpaces = document.querySelectorAll('.letter-space'); + const filledSpaces = document.querySelectorAll('.letter-space.filled'); + + if (allSpaces.length === filledSpaces.length) { + score += 15; + document.getElementById('builder-score').textContent = score; + this.onScoreUpdate(score); + + document.getElementById('next-word-btn').style.display = 'inline-block'; + document.getElementById('give-up-btn').style.display = 'none'; + + // Désactiver toutes les lettres + document.querySelectorAll('.letter-btn').forEach(b => b.disabled = true); + } + } else { + // Mauvaise lettre + btn.classList.add('wrong-letter'); + setTimeout(() => btn.classList.remove('wrong-letter'), 1000); + } + }; + + document.getElementById('next-word-btn').addEventListener('click', () => { + currentWordIndex++; + showBuilderWord(); + }); + + document.getElementById('give-up-btn').addEventListener('click', () => { + currentWordIndex++; + showBuilderWord(); + }); + + this.placeLetter = placeLetter; + showBuilderWord(); + } + + 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; + } + + start() { + // Interface commune - montrer le sélecteur + this.showGameSelector(); + } + + destroy() { + this.container.innerHTML = ''; + } +} + +// CSS supplémentaire pour les mini-jeux +const tempGamesStyles = ` + +`; + +// Ajouter les styles +document.head.insertAdjacentHTML('beforeend', tempGamesStyles); + +// Enregistrement du module +window.GameModules = window.GameModules || {}; +window.GameModules.TempGames = TempGamesModule; \ No newline at end of file diff --git a/js/games/text-reader.js b/js/games/text-reader.js new file mode 100644 index 0000000..de5d825 --- /dev/null +++ b/js/games/text-reader.js @@ -0,0 +1,367 @@ +// === MODULE TEXT READER === + +class TextReaderGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // État du lecteur + this.currentTextIndex = 0; + this.currentSentenceIndex = 0; + this.isRunning = false; + + // Données de lecture + this.texts = this.extractTexts(this.content); + this.currentText = null; + this.sentences = []; + this.showingFullText = false; + + this.init(); + } + + init() { + // Vérifier que nous avons des textes + if (!this.texts || this.texts.length === 0) { + console.error('Aucun texte disponible pour Text Reader'); + this.showInitError(); + return; + } + + this.createReaderInterface(); + this.setupEventListeners(); + this.loadText(); + } + + showInitError() { + this.container.innerHTML = ` +
+

❌ Error loading

+

This content doesn't contain texts compatible with Text Reader.

+

The reader needs texts to display.

+ +
+ `; + } + + extractTexts(content) { + let texts = []; + + console.log('📖 Extracting texts from:', content?.name || 'content'); + + // Use raw module content if available + if (content.rawContent) { + console.log('📦 Using raw module content'); + return this.extractTextsFromRaw(content.rawContent); + } + + // Format with texts array + if (content.texts && Array.isArray(content.texts)) { + console.log('📝 Texts format detected'); + texts = content.texts.filter(text => + text.content && text.content.trim() !== '' + ); + } + // Modern format with contentItems + else if (content.contentItems && Array.isArray(content.contentItems)) { + console.log('🆕 ContentItems format detected'); + texts = content.contentItems + .filter(item => item.type === 'text' && item.content) + .map(item => ({ + title: item.title || 'Text', + content: item.content + })); + } + + return this.finalizeTexts(texts); + } + + extractTextsFromRaw(rawContent) { + console.log('🔧 Extracting from raw content:', rawContent.name || 'Module'); + let texts = []; + + // Simple format (texts array) + if (rawContent.texts && Array.isArray(rawContent.texts)) { + texts = rawContent.texts.filter(text => + text.content && text.content.trim() !== '' + ); + console.log(`📝 ${texts.length} texts extracted from texts array`); + } + // ContentItems format + else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) { + texts = rawContent.contentItems + .filter(item => item.type === 'text' && item.content) + .map(item => ({ + title: item.title || 'Text', + content: item.content + })); + console.log(`🆕 ${texts.length} texts extracted from contentItems`); + } + + return this.finalizeTexts(texts); + } + + finalizeTexts(texts) { + // Validation and cleanup + texts = texts.filter(text => + text && + typeof text.content === 'string' && + text.content.trim() !== '' + ); + + if (texts.length === 0) { + console.error('❌ No valid texts found'); + // Demo texts as fallback + texts = [ + { + title: "Demo Text", + content: "This is a demo text. It has multiple sentences. Each sentence will be displayed one by one. You can navigate using the buttons below." + } + ]; + console.warn('🚨 Using demo texts'); + } + + console.log(`✅ Text Reader: ${texts.length} texts finalized`); + return texts; + } + + createReaderInterface() { + this.container.innerHTML = ` +
+ +
+ + +
+ 1 / 1 +
+
+ + +
+
+ +
+ + +
+ + +
+ + + +
+ + + + + + +
+ `; + } + + setupEventListeners() { + document.getElementById('next-sentence-btn').addEventListener('click', () => this.nextSentence()); + document.getElementById('prev-sentence-btn').addEventListener('click', () => this.prevSentence()); + document.getElementById('show-full-btn').addEventListener('click', () => this.showFullText()); + + document.getElementById('back-to-reading-btn').addEventListener('click', () => this.backToReading()); + document.getElementById('text-selector').addEventListener('change', (e) => this.selectText(parseInt(e.target.value))); + + // Keyboard navigation + document.addEventListener('keydown', (e) => { + if (!this.isRunning) return; + + if (this.showingFullText) { + if (e.key === 'Escape') this.backToReading(); + } else { + if (e.key === 'ArrowLeft') this.prevSentence(); + else if (e.key === 'ArrowRight') this.nextSentence(); + else if (e.key === 'Enter' || e.key === ' ') this.showFullText(); + } + }); + } + + start() { + console.log('📖 Text Reader: Starting'); + this.isRunning = true; + } + + restart() { + console.log('🔄 Text Reader: Restarting'); + this.reset(); + this.start(); + } + + reset() { + this.currentTextIndex = 0; + this.currentSentenceIndex = 0; + this.isRunning = false; + this.showingFullText = false; + this.loadText(); + } + + loadText() { + if (this.currentTextIndex >= this.texts.length) { + this.currentTextIndex = 0; + } + + this.currentText = this.texts[this.currentTextIndex]; + this.sentences = this.splitIntoSentences(this.currentText.content); + this.currentSentenceIndex = 0; + this.showingFullText = false; + + this.populateTextSelector(); + this.updateDisplay(); + this.updateUI(); + } + + populateTextSelector() { + const selector = document.getElementById('text-selector'); + selector.innerHTML = ''; + + this.texts.forEach((text, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = text.title || `Text ${index + 1}`; + if (index === this.currentTextIndex) { + option.selected = true; + } + selector.appendChild(option); + }); + } + + selectText(textIndex) { + if (textIndex >= 0 && textIndex < this.texts.length) { + this.currentTextIndex = textIndex; + this.currentText = this.texts[this.currentTextIndex]; + this.sentences = this.splitIntoSentences(this.currentText.content); + this.currentSentenceIndex = 0; + + // Always go back to sentence reading when changing text + if (this.showingFullText) { + this.backToReading(); + } else { + this.updateDisplay(); + this.updateUI(); + } + + this.showFeedback(`Switched to: ${this.currentText.title}`, 'info'); + } + } + + splitIntoSentences(text) { + // Split by periods, exclamation marks, and question marks + // Keep the punctuation with the sentence + const sentences = text.split(/(?<=[.!?])\s+/) + .filter(sentence => sentence.trim() !== '') + .map(sentence => sentence.trim()); + + return sentences.length > 0 ? sentences : [text]; + } + + nextSentence() { + if (this.currentSentenceIndex < this.sentences.length - 1) { + this.currentSentenceIndex++; + this.updateDisplay(); + this.updateUI(); + } else { + // End of sentences, show full text automatically + this.showFullText(); + } + } + + prevSentence() { + if (this.currentSentenceIndex > 0) { + this.currentSentenceIndex--; + this.updateDisplay(); + this.updateUI(); + } + } + + showFullText() { + this.showingFullText = true; + document.getElementById('sentence-display').style.display = 'none'; + document.getElementById('full-text-display').style.display = 'block'; + document.getElementById('full-text-display').innerHTML = ` +
+

${this.currentText.content}

+
+ `; + + // Show full text navigation controls + document.querySelector('.reader-controls').style.display = 'none'; + document.getElementById('full-text-navigation').style.display = 'flex'; + + this.showFeedback('Full text displayed. Use dropdown to change text.', 'info'); + } + + backToReading() { + this.showingFullText = false; + document.getElementById('sentence-display').style.display = 'block'; + document.getElementById('full-text-display').style.display = 'none'; + + // Show sentence navigation controls + document.querySelector('.reader-controls').style.display = 'flex'; + document.getElementById('full-text-navigation').style.display = 'none'; + + this.updateDisplay(); + this.updateUI(); + this.showFeedback('Back to sentence-by-sentence reading.', 'info'); + } + + // Text navigation methods removed - using dropdown instead + + updateDisplay() { + if (this.showingFullText) return; + + const sentenceDisplay = document.getElementById('sentence-display'); + const currentSentence = this.sentences[this.currentSentenceIndex]; + + sentenceDisplay.innerHTML = ` +
+ ${currentSentence} +
+ `; + } + + updateUI() { + // Update counters + document.getElementById('sentence-counter').textContent = `${this.currentSentenceIndex + 1} / ${this.sentences.length}`; + + // Update button states + document.getElementById('prev-sentence-btn').disabled = this.currentSentenceIndex === 0; + document.getElementById('next-sentence-btn').disabled = false; + document.getElementById('next-sentence-btn').textContent = + this.currentSentenceIndex === this.sentences.length - 1 ? 'Full Text →' : 'Next →'; + } + + // updateTextNavigation method removed - using dropdown instead + + showFeedback(message, type = 'info') { + const feedbackArea = document.getElementById('feedback-area'); + feedbackArea.innerHTML = `
${message}
`; + } + + destroy() { + this.isRunning = false; + this.container.innerHTML = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.TextReader = TextReaderGame; \ No newline at end of file diff --git a/js/games/whack-a-mole-hard.js b/js/games/whack-a-mole-hard.js new file mode 100644 index 0000000..a53178e --- /dev/null +++ b/js/games/whack-a-mole-hard.js @@ -0,0 +1,644 @@ +// === MODULE WHACK-A-MOLE HARD === + +class WhackAMoleHardGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // État du jeu + this.score = 0; + this.errors = 0; + this.maxErrors = 5; + this.gameTime = 60; // 60 secondes + this.timeLeft = this.gameTime; + this.isRunning = false; + this.gameMode = 'translation'; // 'translation', 'image', 'sound' + + // Configuration des taupes + this.holes = []; + this.activeMoles = []; + this.moleAppearTime = 3000; // 3 secondes d'affichage (plus long) + this.spawnRate = 2000; // Nouvelle vague toutes les 2 secondes + this.molesPerWave = 3; // 3 taupes par vague + + // Timers + this.gameTimer = null; + this.spawnTimer = null; + + // Vocabulaire pour ce jeu - adapté pour le nouveau système + this.vocabulary = this.extractVocabulary(this.content); + this.currentWords = []; + this.targetWord = null; + + // Système de garantie pour le mot cible + this.spawnsSinceTarget = 0; + this.maxSpawnsWithoutTarget = 10; // Le mot cible doit apparaître dans les 10 prochaines taupes (1/10 chance) + + this.init(); + } + + init() { + // Vérifier que nous avons du vocabulaire + if (!this.vocabulary || this.vocabulary.length === 0) { + console.error('Aucun vocabulaire disponible pour Whack-a-Mole'); + this.showInitError(); + return; + } + + this.createGameBoard(); + this.createGameUI(); + this.setupEventListeners(); + } + + showInitError() { + this.container.innerHTML = ` +
+

❌ Erreur de chargement

+

Ce contenu ne contient pas de vocabulaire compatible avec Whack-a-Mole.

+

Le jeu nécessite des mots avec leurs traductions.

+ +
+ `; + } + + createGameBoard() { + this.container.innerHTML = ` +
+ +
+ + + +
+ + +
+
+
+ ${this.timeLeft} + Time +
+
+ ${this.errors} + Errors +
+
+ --- + Find +
+
+
+ + + +
+
+ + +
+ +
+ + + +
+ `; + + this.createHoles(); + } + + createHoles() { + const gameBoard = document.getElementById('game-board'); + gameBoard.innerHTML = ''; + + for (let i = 0; i < 15; i++) { // 5x3 = 15 trous + const hole = document.createElement('div'); + hole.className = 'whack-hole'; + hole.dataset.holeId = i; + + hole.innerHTML = ` +
+
+
+ `; + + gameBoard.appendChild(hole); + this.holes.push({ + element: hole, + mole: hole.querySelector('.whack-mole'), + wordElement: hole.querySelector('.word'), + isActive: false, + word: null, + timer: null + }); + } + } + + createGameUI() { + // Les éléments UI sont déjà créés dans createGameBoard + } + + setupEventListeners() { + // Mode selection + document.querySelectorAll('.mode-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + if (this.isRunning) return; + + document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this.gameMode = btn.dataset.mode; + + if (this.gameMode !== 'translation') { + this.showFeedback('This mode will be available soon!', 'info'); + // Return to translation mode + document.querySelector('.mode-btn[data-mode="translation"]').classList.add('active'); + btn.classList.remove('active'); + this.gameMode = 'translation'; + } + }); + }); + + // Game controls + document.getElementById('start-btn').addEventListener('click', () => this.start()); + document.getElementById('pause-btn').addEventListener('click', () => this.pause()); + document.getElementById('restart-btn').addEventListener('click', () => this.restart()); + + // Mole clicks + this.holes.forEach((hole, index) => { + hole.mole.addEventListener('click', () => this.hitMole(index)); + }); + } + + start() { + if (this.isRunning) return; + + this.isRunning = true; + this.score = 0; + this.errors = 0; + this.timeLeft = this.gameTime; + + this.updateUI(); + this.setNewTarget(); + this.startTimers(); + + document.getElementById('start-btn').disabled = true; + document.getElementById('pause-btn').disabled = false; + + this.showFeedback(`Find the word: "${this.targetWord.french}"`, 'info'); + + // Show loaded content info + const contentName = this.content.name || 'Content'; + console.log(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words)`); + } + + pause() { + if (!this.isRunning) return; + + this.isRunning = false; + this.stopTimers(); + this.hideAllMoles(); + + document.getElementById('start-btn').disabled = false; + document.getElementById('pause-btn').disabled = true; + + this.showFeedback('Game paused', 'info'); + } + + restart() { + this.stopWithoutEnd(); // Arrêter sans déclencher la fin de jeu + this.resetGame(); + setTimeout(() => this.start(), 100); + } + + stop() { + this.stopWithoutEnd(); + this.onGameEnd(this.score); // Déclencher la fin de jeu seulement ici + } + + stopWithoutEnd() { + this.isRunning = false; + this.stopTimers(); + this.hideAllMoles(); + + document.getElementById('start-btn').disabled = false; + document.getElementById('pause-btn').disabled = true; + } + + resetGame() { + // S'assurer que tout est complètement arrêté + this.stopWithoutEnd(); + + // Reset de toutes les variables d'état + this.score = 0; + this.errors = 0; + this.timeLeft = this.gameTime; + this.isRunning = false; + this.targetWord = null; + this.activeMoles = []; + this.spawnsSinceTarget = 0; // Reset du compteur de garantie + + // S'assurer que tous les timers sont bien arrêtés + this.stopTimers(); + + // Reset UI + this.updateUI(); + this.onScoreUpdate(0); + + // Clear feedback + document.getElementById('target-word').textContent = '---'; + this.showFeedback('Select a mode and click Start!', 'info'); + + // Reset buttons + document.getElementById('start-btn').disabled = false; + document.getElementById('pause-btn').disabled = true; + + // Clear all holes avec vérification + this.holes.forEach(hole => { + if (hole.timer) { + clearTimeout(hole.timer); + hole.timer = null; + } + hole.isActive = false; + hole.word = null; + if (hole.wordElement) { + hole.wordElement.textContent = ''; + } + if (hole.mole) { + hole.mole.classList.remove('active', 'hit'); + } + }); + + console.log('🔄 Game completely reset'); + } + + startTimers() { + // Timer principal du jeu + this.gameTimer = setInterval(() => { + this.timeLeft--; + this.updateUI(); + + if (this.timeLeft <= 0 && this.isRunning) { + this.stop(); + } + }, 1000); + + // Timer d'apparition des taupes + this.spawnTimer = setInterval(() => { + if (this.isRunning) { + this.spawnMole(); + } + }, this.spawnRate); + + // Première taupe immédiate + setTimeout(() => this.spawnMole(), 500); + } + + stopTimers() { + if (this.gameTimer) { + clearInterval(this.gameTimer); + this.gameTimer = null; + } + if (this.spawnTimer) { + clearInterval(this.spawnTimer); + this.spawnTimer = null; + } + } + + spawnMole() { + // Mode Hard: Spawn 3 taupes à la fois + this.spawnMultipleMoles(); + } + + spawnMultipleMoles() { + // Trouver tous les trous libres + const availableHoles = this.holes.filter(hole => !hole.isActive); + + // Spawn jusqu'à 3 taupes (ou moins si pas assez de trous libres) + const molesToSpawn = Math.min(this.molesPerWave, availableHoles.length); + + if (molesToSpawn === 0) return; + + // Mélanger les trous disponibles + const shuffledHoles = this.shuffleArray(availableHoles); + + // Spawn les taupes + for (let i = 0; i < molesToSpawn; i++) { + const hole = shuffledHoles[i]; + const holeIndex = this.holes.indexOf(hole); + + // Choisir un mot selon la stratégie de garantie + const word = this.getWordWithTargetGuarantee(); + + // Activer la taupe avec un petit délai pour un effet visuel + setTimeout(() => { + if (this.isRunning && !hole.isActive) { + this.activateMole(holeIndex, word); + } + }, i * 200); // Délai de 200ms entre chaque taupe + } + } + + getWordWithTargetGuarantee() { + // Incrémenter le compteur de spawns depuis le dernier mot cible + this.spawnsSinceTarget++; + + // Si on a atteint la limite, forcer le mot cible + if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) { + console.log(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`); + this.spawnsSinceTarget = 0; + return this.targetWord; + } + + // Sinon, 10% de chance d'avoir le mot cible (1/10 au lieu de 1/2) + if (Math.random() < 0.1) { + console.log('🎯 Spawn naturel du mot cible (1/10)'); + this.spawnsSinceTarget = 0; + return this.targetWord; + } else { + return this.getRandomWord(); + } + } + + activateMole(holeIndex, word) { + const hole = this.holes[holeIndex]; + if (hole.isActive) return; + + hole.isActive = true; + hole.word = word; + hole.wordElement.textContent = word.english; + hole.mole.classList.add('active'); + + // Ajouter à la liste des taupes actives + this.activeMoles.push(holeIndex); + + // Timer pour faire disparaître la taupe + hole.timer = setTimeout(() => { + this.deactivateMole(holeIndex); + }, this.moleAppearTime); + } + + deactivateMole(holeIndex) { + const hole = this.holes[holeIndex]; + if (!hole.isActive) return; + + hole.isActive = false; + hole.word = null; + hole.wordElement.textContent = ''; + hole.mole.classList.remove('active'); + + if (hole.timer) { + clearTimeout(hole.timer); + hole.timer = null; + } + + // Retirer de la liste des taupes actives + const activeIndex = this.activeMoles.indexOf(holeIndex); + if (activeIndex > -1) { + this.activeMoles.splice(activeIndex, 1); + } + } + + hitMole(holeIndex) { + if (!this.isRunning) return; + + const hole = this.holes[holeIndex]; + if (!hole.isActive || !hole.word) return; + + const isCorrect = hole.word.french === this.targetWord.french; + + if (isCorrect) { + // Bonne réponse + this.score += 10; + this.deactivateMole(holeIndex); + this.setNewTarget(); + this.showScorePopup(holeIndex, '+10', true); + this.showFeedback(`Well done! Now find: "${this.targetWord.french}"`, 'success'); + + // Success animation + hole.mole.classList.add('hit'); + setTimeout(() => hole.mole.classList.remove('hit'), 500); + + } else { + // Wrong answer + this.errors++; + this.score = Math.max(0, this.score - 2); + this.showScorePopup(holeIndex, '-2', false); + this.showFeedback(`Oops! "${hole.word.french}" ≠ "${this.targetWord.french}"`, 'error'); + } + + this.updateUI(); + this.onScoreUpdate(this.score); + + // Check game end by errors + if (this.errors >= this.maxErrors) { + this.showFeedback('Too many errors! Game over.', 'error'); + setTimeout(() => { + if (this.isRunning) { // Check if game is still running + this.stop(); + } + }, 1500); + } + } + + setNewTarget() { + // Choisir un nouveau mot cible + const availableWords = this.vocabulary.filter(word => + !this.activeMoles.some(moleIndex => + this.holes[moleIndex].word && + this.holes[moleIndex].word.english === word.english + ) + ); + + if (availableWords.length > 0) { + this.targetWord = availableWords[Math.floor(Math.random() * availableWords.length)]; + } else { + this.targetWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; + } + + // Reset du compteur pour le nouveau mot cible + this.spawnsSinceTarget = 0; + console.log(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`); + + document.getElementById('target-word').textContent = this.targetWord.french; + } + + getRandomWord() { + return this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; + } + + hideAllMoles() { + this.holes.forEach((hole, index) => { + if (hole.isActive) { + this.deactivateMole(index); + } + }); + this.activeMoles = []; + } + + showScorePopup(holeIndex, scoreText, isPositive) { + const hole = this.holes[holeIndex]; + const popup = document.createElement('div'); + popup.className = `score-popup ${isPositive ? 'correct-answer' : 'wrong-answer'}`; + popup.textContent = scoreText; + + const rect = hole.element.getBoundingClientRect(); + popup.style.left = rect.left + rect.width / 2 + 'px'; + popup.style.top = rect.top + 'px'; + + document.body.appendChild(popup); + + setTimeout(() => { + if (popup.parentNode) { + popup.parentNode.removeChild(popup); + } + }, 1000); + } + + showFeedback(message, type = 'info') { + const feedbackArea = document.getElementById('feedback-area'); + feedbackArea.innerHTML = `
${message}
`; + } + + updateUI() { + document.getElementById('time-left').textContent = this.timeLeft; + document.getElementById('errors-count').textContent = this.errors; + } + + extractVocabulary(content) { + let vocabulary = []; + + console.log('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu'); + + // Priorité 1: Utiliser le contenu brut du module (format simple) + if (content.rawContent) { + console.log('📦 Utilisation du contenu brut du module'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Priorité 2: Format simple avec vocabulary object (nouveau format préféré) + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + console.log('✨ Format simple détecté (vocabulary object)'); + vocabulary = Object.entries(content.vocabulary).map(([english, translation]) => ({ + english: english, + french: translation.split(';')[0], // Prendre la première traduction si plusieurs + chinese: translation, // Garder la traduction complète en chinois + category: 'general' + })); + } + // Priorité 3: Format legacy avec vocabulary array + else if (content.vocabulary && Array.isArray(content.vocabulary)) { + console.log('📚 Format legacy détecté (vocabulary array)'); + vocabulary = content.vocabulary.filter(word => word.english && word.french); + } + // Priorité 4: Format moderne avec contentItems + else if (content.contentItems && Array.isArray(content.contentItems)) { + console.log('🆕 Format contentItems détecté'); + vocabulary = content.contentItems + .filter(item => item.type === 'vocabulary' && item.english && item.french) + .map(item => ({ + english: item.english, + french: item.french, + image: item.image || null, + category: item.category || 'general' + })); + } + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module'); + let vocabulary = []; + + // Format simple avec vocabulary object (PRÉFÉRÉ) + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({ + english: english, + french: translation.split(';')[0], // Première traduction pour le français + chinese: translation, // Traduction complète en chinois + category: 'general' + })); + console.log(`✨ ${vocabulary.length} mots extraits depuis vocabulary object (format simple)`); + } + // Format legacy (vocabulary array) + else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) { + vocabulary = rawContent.vocabulary.filter(word => word.english && word.french); + console.log(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`); + } + // Format contentItems (ancien format complexe) + else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) { + vocabulary = rawContent.contentItems + .filter(item => item.type === 'vocabulary' && item.english && item.french) + .map(item => ({ + english: item.english, + french: item.french, + image: item.image || null, + category: item.category || 'general' + })); + console.log(`📝 ${vocabulary.length} mots extraits depuis contentItems`); + } + // Fallback + else { + console.warn('⚠️ Format de contenu brut non reconnu'); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Validation et nettoyage + vocabulary = vocabulary.filter(word => + word && + typeof word.english === 'string' && + typeof word.french === 'string' && + word.english.trim() !== '' && + word.french.trim() !== '' + ); + + if (vocabulary.length === 0) { + console.error('❌ Aucun vocabulaire valide trouvé'); + // Vocabulaire de démonstration en dernier recours + vocabulary = [ + { english: 'hello', french: 'bonjour', category: 'greetings' }, + { english: 'goodbye', french: 'au revoir', category: 'greetings' }, + { english: 'thank you', french: 'merci', category: 'greetings' }, + { english: 'cat', french: 'chat', category: 'animals' }, + { english: 'dog', french: 'chien', category: 'animals' } + ]; + console.warn('🚨 Utilisation du vocabulaire de démonstration'); + } + + console.log(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`); + return this.shuffleArray(vocabulary); + } + + 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; + } + + destroy() { + this.stop(); + this.container.innerHTML = ''; + } +} + +// Enregistrement du module +window.GameModules = window.GameModules || {}; +window.GameModules.WhackAMoleHard = WhackAMoleHardGame; \ No newline at end of file diff --git a/js/games/whack-a-mole.js b/js/games/whack-a-mole.js new file mode 100644 index 0000000..8dc4a44 --- /dev/null +++ b/js/games/whack-a-mole.js @@ -0,0 +1,624 @@ +// === MODULE WHACK-A-MOLE === + +class WhackAMoleGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // État du jeu + this.score = 0; + this.errors = 0; + this.maxErrors = 5; + this.gameTime = 60; // 60 secondes + this.timeLeft = this.gameTime; + this.isRunning = false; + this.gameMode = 'translation'; // 'translation', 'image', 'sound' + + // Configuration des taupes + this.holes = []; + this.activeMoles = []; + this.moleAppearTime = 2000; // 2 secondes d'affichage + this.spawnRate = 1500; // Nouvelle taupe toutes les 1.5 secondes + + // Timers + this.gameTimer = null; + this.spawnTimer = null; + + // Vocabulaire pour ce jeu - adapté pour le nouveau système + this.vocabulary = this.extractVocabulary(this.content); + this.currentWords = []; + this.targetWord = null; + + // Système de garantie pour le mot cible + this.spawnsSinceTarget = 0; + this.maxSpawnsWithoutTarget = 3; // Le mot cible doit apparaître dans les 3 prochaines taupes + + this.init(); + } + + init() { + // Vérifier que nous avons du vocabulaire + if (!this.vocabulary || this.vocabulary.length === 0) { + console.error('Aucun vocabulaire disponible pour Whack-a-Mole'); + this.showInitError(); + return; + } + + this.createGameBoard(); + this.createGameUI(); + this.setupEventListeners(); + } + + showInitError() { + this.container.innerHTML = ` +
+

❌ Erreur de chargement

+

Ce contenu ne contient pas de vocabulaire compatible avec Whack-a-Mole.

+

Le jeu nécessite des mots avec leurs traductions.

+ +
+ `; + } + + createGameBoard() { + this.container.innerHTML = ` +
+ +
+ + + +
+ + +
+
+
+ ${this.timeLeft} + Time +
+
+ ${this.errors} + Errors +
+
+ --- + Find +
+
+
+ + + +
+
+ + +
+ +
+ + + +
+ `; + + this.createHoles(); + } + + createHoles() { + const gameBoard = document.getElementById('game-board'); + gameBoard.innerHTML = ''; + + for (let i = 0; i < 9; i++) { + const hole = document.createElement('div'); + hole.className = 'whack-hole'; + hole.dataset.holeId = i; + + hole.innerHTML = ` +
+
+
+ `; + + gameBoard.appendChild(hole); + this.holes.push({ + element: hole, + mole: hole.querySelector('.whack-mole'), + wordElement: hole.querySelector('.word'), + isActive: false, + word: null, + timer: null + }); + } + } + + createGameUI() { + // Les éléments UI sont déjà créés dans createGameBoard + } + + setupEventListeners() { + // Mode selection + document.querySelectorAll('.mode-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + if (this.isRunning) return; + + document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this.gameMode = btn.dataset.mode; + + if (this.gameMode !== 'translation') { + this.showFeedback('This mode will be available soon!', 'info'); + // Return to translation mode + document.querySelector('.mode-btn[data-mode="translation"]').classList.add('active'); + btn.classList.remove('active'); + this.gameMode = 'translation'; + } + }); + }); + + // Game controls + document.getElementById('start-btn').addEventListener('click', () => this.start()); + document.getElementById('pause-btn').addEventListener('click', () => this.pause()); + document.getElementById('restart-btn').addEventListener('click', () => this.restart()); + + // Mole clicks + this.holes.forEach((hole, index) => { + hole.mole.addEventListener('click', () => this.hitMole(index)); + }); + } + + start() { + if (this.isRunning) return; + + this.isRunning = true; + this.score = 0; + this.errors = 0; + this.timeLeft = this.gameTime; + + this.updateUI(); + this.setNewTarget(); + this.startTimers(); + + document.getElementById('start-btn').disabled = true; + document.getElementById('pause-btn').disabled = false; + + this.showFeedback(`Find the word: "${this.targetWord.french}"`, 'info'); + + // Show loaded content info + const contentName = this.content.name || 'Content'; + console.log(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words)`); + } + + pause() { + if (!this.isRunning) return; + + this.isRunning = false; + this.stopTimers(); + this.hideAllMoles(); + + document.getElementById('start-btn').disabled = false; + document.getElementById('pause-btn').disabled = true; + + this.showFeedback('Game paused', 'info'); + } + + restart() { + this.stopWithoutEnd(); // Arrêter sans déclencher la fin de jeu + this.resetGame(); + setTimeout(() => this.start(), 100); + } + + stop() { + this.stopWithoutEnd(); + this.onGameEnd(this.score); // Déclencher la fin de jeu seulement ici + } + + stopWithoutEnd() { + this.isRunning = false; + this.stopTimers(); + this.hideAllMoles(); + + document.getElementById('start-btn').disabled = false; + document.getElementById('pause-btn').disabled = true; + } + + resetGame() { + // S'assurer que tout est complètement arrêté + this.stopWithoutEnd(); + + // Reset de toutes les variables d'état + this.score = 0; + this.errors = 0; + this.timeLeft = this.gameTime; + this.isRunning = false; + this.targetWord = null; + this.activeMoles = []; + this.spawnsSinceTarget = 0; // Reset du compteur de garantie + + // S'assurer que tous les timers sont bien arrêtés + this.stopTimers(); + + // Reset UI + this.updateUI(); + this.onScoreUpdate(0); + + // Clear feedback + document.getElementById('target-word').textContent = '---'; + this.showFeedback('Select a mode and click Start!', 'info'); + + // Reset buttons + document.getElementById('start-btn').disabled = false; + document.getElementById('pause-btn').disabled = true; + + // Clear all holes avec vérification + this.holes.forEach(hole => { + if (hole.timer) { + clearTimeout(hole.timer); + hole.timer = null; + } + hole.isActive = false; + hole.word = null; + if (hole.wordElement) { + hole.wordElement.textContent = ''; + } + if (hole.mole) { + hole.mole.classList.remove('active', 'hit'); + } + }); + + console.log('🔄 Game completely reset'); + } + + startTimers() { + // Timer principal du jeu + this.gameTimer = setInterval(() => { + this.timeLeft--; + this.updateUI(); + + if (this.timeLeft <= 0 && this.isRunning) { + this.stop(); + } + }, 1000); + + // Timer d'apparition des taupes + this.spawnTimer = setInterval(() => { + if (this.isRunning) { + this.spawnMole(); + } + }, this.spawnRate); + + // Première taupe immédiate + setTimeout(() => this.spawnMole(), 500); + } + + stopTimers() { + if (this.gameTimer) { + clearInterval(this.gameTimer); + this.gameTimer = null; + } + if (this.spawnTimer) { + clearInterval(this.spawnTimer); + this.spawnTimer = null; + } + } + + spawnMole() { + // Trouver un trou libre + const availableHoles = this.holes.filter(hole => !hole.isActive); + if (availableHoles.length === 0) return; + + const randomHole = availableHoles[Math.floor(Math.random() * availableHoles.length)]; + const holeIndex = this.holes.indexOf(randomHole); + + // Choisir un mot selon la stratégie de garantie + const word = this.getWordWithTargetGuarantee(); + + // Activer la taupe + this.activateMole(holeIndex, word); + } + + getWordWithTargetGuarantee() { + // Incrémenter le compteur de spawns depuis le dernier mot cible + this.spawnsSinceTarget++; + + // Si on a atteint la limite, forcer le mot cible + if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) { + console.log(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`); + this.spawnsSinceTarget = 0; + return this.targetWord; + } + + // Sinon, 50% de chance d'avoir le mot cible, 50% un mot aléatoire + if (Math.random() < 0.5) { + console.log('🎯 Spawn naturel du mot cible'); + this.spawnsSinceTarget = 0; + return this.targetWord; + } else { + return this.getRandomWord(); + } + } + + activateMole(holeIndex, word) { + const hole = this.holes[holeIndex]; + if (hole.isActive) return; + + hole.isActive = true; + hole.word = word; + hole.wordElement.textContent = word.english; + hole.mole.classList.add('active'); + + // Ajouter à la liste des taupes actives + this.activeMoles.push(holeIndex); + + // Timer pour faire disparaître la taupe + hole.timer = setTimeout(() => { + this.deactivateMole(holeIndex); + }, this.moleAppearTime); + } + + deactivateMole(holeIndex) { + const hole = this.holes[holeIndex]; + if (!hole.isActive) return; + + hole.isActive = false; + hole.word = null; + hole.wordElement.textContent = ''; + hole.mole.classList.remove('active'); + + if (hole.timer) { + clearTimeout(hole.timer); + hole.timer = null; + } + + // Retirer de la liste des taupes actives + const activeIndex = this.activeMoles.indexOf(holeIndex); + if (activeIndex > -1) { + this.activeMoles.splice(activeIndex, 1); + } + } + + hitMole(holeIndex) { + if (!this.isRunning) return; + + const hole = this.holes[holeIndex]; + if (!hole.isActive || !hole.word) return; + + const isCorrect = hole.word.french === this.targetWord.french; + + if (isCorrect) { + // Bonne réponse + this.score += 10; + this.deactivateMole(holeIndex); + this.setNewTarget(); + this.showScorePopup(holeIndex, '+10', true); + this.showFeedback(`Well done! Now find: "${this.targetWord.french}"`, 'success'); + + // Success animation + hole.mole.classList.add('hit'); + setTimeout(() => hole.mole.classList.remove('hit'), 500); + + } else { + // Wrong answer + this.errors++; + this.score = Math.max(0, this.score - 2); + this.showScorePopup(holeIndex, '-2', false); + this.showFeedback(`Oops! "${hole.word.french}" ≠ "${this.targetWord.french}"`, 'error'); + } + + this.updateUI(); + this.onScoreUpdate(this.score); + + // Check game end by errors + if (this.errors >= this.maxErrors) { + this.showFeedback('Too many errors! Game over.', 'error'); + setTimeout(() => { + if (this.isRunning) { // Check if game is still running + this.stop(); + } + }, 1500); + } + } + + setNewTarget() { + // Choisir un nouveau mot cible + const availableWords = this.vocabulary.filter(word => + !this.activeMoles.some(moleIndex => + this.holes[moleIndex].word && + this.holes[moleIndex].word.english === word.english + ) + ); + + if (availableWords.length > 0) { + this.targetWord = availableWords[Math.floor(Math.random() * availableWords.length)]; + } else { + this.targetWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; + } + + // Reset du compteur pour le nouveau mot cible + this.spawnsSinceTarget = 0; + console.log(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`); + + document.getElementById('target-word').textContent = this.targetWord.french; + } + + getRandomWord() { + return this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; + } + + hideAllMoles() { + this.holes.forEach((hole, index) => { + if (hole.isActive) { + this.deactivateMole(index); + } + }); + this.activeMoles = []; + } + + showScorePopup(holeIndex, scoreText, isPositive) { + const hole = this.holes[holeIndex]; + const popup = document.createElement('div'); + popup.className = `score-popup ${isPositive ? 'correct-answer' : 'wrong-answer'}`; + popup.textContent = scoreText; + + const rect = hole.element.getBoundingClientRect(); + popup.style.left = rect.left + rect.width / 2 + 'px'; + popup.style.top = rect.top + 'px'; + + document.body.appendChild(popup); + + setTimeout(() => { + if (popup.parentNode) { + popup.parentNode.removeChild(popup); + } + }, 1000); + } + + showFeedback(message, type = 'info') { + const feedbackArea = document.getElementById('feedback-area'); + feedbackArea.innerHTML = `
${message}
`; + } + + updateUI() { + document.getElementById('time-left').textContent = this.timeLeft; + document.getElementById('errors-count').textContent = this.errors; + } + + extractVocabulary(content) { + let vocabulary = []; + + console.log('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu'); + + // Priorité 1: Utiliser le contenu brut du module (format simple) + if (content.rawContent) { + console.log('📦 Utilisation du contenu brut du module'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Priorité 2: Format simple avec vocabulary object (nouveau format préféré) + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + console.log('✨ Format simple détecté (vocabulary object)'); + vocabulary = Object.entries(content.vocabulary).map(([english, translation]) => ({ + english: english, + french: translation.split(';')[0], // Prendre la première traduction si plusieurs + chinese: translation, // Garder la traduction complète en chinois + category: 'general' + })); + } + // Priorité 3: Format legacy avec vocabulary array + else if (content.vocabulary && Array.isArray(content.vocabulary)) { + console.log('📚 Format legacy détecté (vocabulary array)'); + vocabulary = content.vocabulary.filter(word => word.english && word.french); + } + // Priorité 4: Format moderne avec contentItems + else if (content.contentItems && Array.isArray(content.contentItems)) { + console.log('🆕 Format contentItems détecté'); + vocabulary = content.contentItems + .filter(item => item.type === 'vocabulary' && item.english && item.french) + .map(item => ({ + english: item.english, + french: item.french, + image: item.image || null, + category: item.category || 'general' + })); + } + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module'); + let vocabulary = []; + + // Format simple avec vocabulary object (PRÉFÉRÉ) + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({ + english: english, + french: translation.split(';')[0], // Première traduction pour le français + chinese: translation, // Traduction complète en chinois + category: 'general' + })); + console.log(`✨ ${vocabulary.length} mots extraits depuis vocabulary object (format simple)`); + } + // Format legacy (vocabulary array) + else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) { + vocabulary = rawContent.vocabulary.filter(word => word.english && word.french); + console.log(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`); + } + // Format contentItems (ancien format complexe) + else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) { + vocabulary = rawContent.contentItems + .filter(item => item.type === 'vocabulary' && item.english && item.french) + .map(item => ({ + english: item.english, + french: item.french, + image: item.image || null, + category: item.category || 'general' + })); + console.log(`📝 ${vocabulary.length} mots extraits depuis contentItems`); + } + // Fallback + else { + console.warn('⚠️ Format de contenu brut non reconnu'); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Validation et nettoyage + vocabulary = vocabulary.filter(word => + word && + typeof word.english === 'string' && + typeof word.french === 'string' && + word.english.trim() !== '' && + word.french.trim() !== '' + ); + + if (vocabulary.length === 0) { + console.error('❌ Aucun vocabulaire valide trouvé'); + // Vocabulaire de démonstration en dernier recours + vocabulary = [ + { english: 'hello', french: 'bonjour', category: 'greetings' }, + { english: 'goodbye', french: 'au revoir', category: 'greetings' }, + { english: 'thank you', french: 'merci', category: 'greetings' }, + { english: 'cat', french: 'chat', category: 'animals' }, + { english: 'dog', french: 'chien', category: 'animals' } + ]; + console.warn('🚨 Utilisation du vocabulaire de démonstration'); + } + + console.log(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`); + return this.shuffleArray(vocabulary); + } + + 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; + } + + destroy() { + this.stop(); + this.container.innerHTML = ''; + } +} + +// Enregistrement du module +window.GameModules = window.GameModules || {}; +window.GameModules.WhackAMole = WhackAMoleGame; \ No newline at end of file diff --git a/js/tools/content-creator.js b/js/tools/content-creator.js new file mode 100644 index 0000000..6bf0ff2 --- /dev/null +++ b/js/tools/content-creator.js @@ -0,0 +1,919 @@ +// === INTERFACE CRÉATEUR DE CONTENU === + +class ContentCreator { + constructor() { + this.factory = new ContentFactory(); + this.previewContainer = null; + this.currentContent = null; + } + + init() { + this.createInterface(); + this.setupEventListeners(); + this.loadExamples(); + } + + createInterface() { + // Créer l'interface dans le container principal + const existingInterface = document.getElementById('content-creator'); + if (existingInterface) { + existingInterface.remove(); + } + + const interface = document.createElement('div'); + interface.id = 'content-creator'; + interface.className = 'content-creator-interface'; + + interface.innerHTML = ` +
+

🏭 Créateur de Contenu Universel

+

Transformez n'importe quel contenu en exercices interactifs

+
+ +
+ + + + +
+ +
+ +
+

Collez votre texte ici

+ + +
+ + +
+
+ + +
+

Liste de Vocabulaire

+
+
+ + = + + + +
+
+ +
+ + +
+

Créateur de Dialogue

+ +
+
+ + + +
+
+ +
+ + +
+

Créateur de Séquence

+ +
+
+ 1 + + + +
+
+ +
+
+ +
+
+ + +
+ +
+ + + +
+
+ +
+

💡 Exemples Rapides

+
+ + + +
+
+ + + `; + + // Injecter dans la page + const container = document.getElementById('game-container') || document.body; + container.appendChild(interface); + + this.previewContainer = interface.querySelector('#content-preview .preview-content'); + } + + setupEventListeners() { + // Gestion des onglets + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + this.switchTab(btn.dataset.tab); + }); + }); + + // Bouton générer + document.getElementById('generate-btn').addEventListener('click', () => { + this.generateContent(); + }); + + // Bouton aperçu + document.getElementById('preview-btn').addEventListener('click', () => { + this.previewContent(); + }); + + // Bouton test + document.getElementById('test-game-btn').addEventListener('click', () => { + this.testInGame(); + }); + + // Exemples + document.querySelectorAll('.example-btn').forEach(btn => { + btn.addEventListener('click', () => { + this.loadExample(btn.dataset.example); + }); + }); + + // Builders dynamiques + this.setupVocabularyBuilder(); + this.setupDialogueBuilder(); + this.setupSequenceBuilder(); + } + + switchTab(tabName) { + // Désactiver tous les onglets et contenu + document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); + + // Activer le bon onglet + document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); + document.getElementById(`${tabName}-tab`).classList.add('active'); + } + + setupVocabularyBuilder() { + document.getElementById('add-vocab-entry').addEventListener('click', () => { + this.addVocabularyEntry(); + }); + + // Supprimer entries + document.addEventListener('click', (e) => { + if (e.target.classList.contains('remove-entry')) { + e.target.parentElement.remove(); + } + }); + } + + addVocabularyEntry() { + const builder = document.querySelector('.vocab-builder'); + const entry = document.createElement('div'); + entry.className = 'vocab-entry'; + entry.innerHTML = ` + + = + + + + `; + builder.appendChild(entry); + } + + setupDialogueBuilder() { + document.getElementById('add-dialogue-line').addEventListener('click', () => { + this.addDialogueLine(); + }); + + document.addEventListener('click', (e) => { + if (e.target.classList.contains('remove-line')) { + e.target.parentElement.remove(); + } + }); + } + + addDialogueLine() { + const builder = document.querySelector('.dialogue-builder'); + const line = document.createElement('div'); + line.className = 'dialogue-line'; + line.innerHTML = ` + + + + `; + builder.appendChild(line); + } + + setupSequenceBuilder() { + document.getElementById('add-sequence-step').addEventListener('click', () => { + this.addSequenceStep(); + }); + + document.addEventListener('click', (e) => { + if (e.target.classList.contains('remove-step')) { + e.target.parentElement.remove(); + this.updateStepNumbers(); + } + }); + } + + addSequenceStep() { + const builder = document.querySelector('.sequence-builder'); + const stepCount = builder.children.length + 1; + const step = document.createElement('div'); + step.className = 'sequence-step'; + step.innerHTML = ` + ${stepCount} + + + + `; + builder.appendChild(step); + } + + updateStepNumbers() { + document.querySelectorAll('.step-number').forEach((num, index) => { + num.textContent = index + 1; + }); + } + + async generateContent() { + try { + const input = this.collectInput(); + const options = this.collectOptions(); + + console.log('🏭 Génération de contenu...', { input, options }); + + const content = await this.factory.createContent(input, options); + this.currentContent = content; + + this.showSuccess('Contenu généré avec succès !'); + this.displayPreview(content); + + document.getElementById('test-game-btn').style.display = 'inline-block'; + + } catch (error) { + console.error('Erreur génération:', error); + this.showError('Erreur lors de la génération: ' + error.message); + } + } + + collectInput() { + const activeTab = document.querySelector('.tab-content.active').id; + + switch (activeTab) { + case 'text-tab': + return document.getElementById('text-input').value; + + case 'vocabulary-tab': + return this.collectVocabularyData(); + + case 'dialogue-tab': + return this.collectDialogueData(); + + case 'sequence-tab': + return this.collectSequenceData(); + + default: + throw new Error('Type de contenu non supporté'); + } + } + + collectVocabularyData() { + const entries = document.querySelectorAll('.vocab-entry'); + const vocabulary = []; + + entries.forEach(entry => { + const english = entry.querySelector('.english-input').value; + const french = entry.querySelector('.french-input').value; + const category = entry.querySelector('.category-input').value || 'general'; + + if (english && french) { + vocabulary.push({ english, french, category }); + } + }); + + return { vocabulary }; + } + + collectDialogueData() { + const scenario = document.getElementById('dialogue-scenario').value || 'conversation'; + const lines = document.querySelectorAll('.dialogue-line'); + const conversation = []; + + lines.forEach(line => { + const speaker = line.querySelector('.speaker-input').value; + const text = line.querySelector('.line-input').value; + + if (speaker && text) { + conversation.push({ speaker, english: text }); + } + }); + + return { + dialogue: { + scenario, + conversation + } + }; + } + + collectSequenceData() { + const title = document.getElementById('sequence-title').value || 'Sequence'; + const steps = document.querySelectorAll('.sequence-step'); + const sequence = []; + + steps.forEach((step, index) => { + const english = step.querySelector('.step-input').value; + const time = step.querySelector('.time-input').value; + + if (english) { + sequence.push({ + order: index + 1, + english, + time: time || null + }); + } + }); + + return { + sequence: { + title, + steps: sequence + } + }; + } + + collectOptions() { + return { + name: document.getElementById('module-name').value || 'Contenu Généré', + difficulty: document.getElementById('difficulty-select').value === 'auto' ? null : document.getElementById('difficulty-select').value, + contentType: document.getElementById('content-type-select').value === 'auto' ? null : document.getElementById('content-type-select').value + }; + } + + async previewContent() { + try { + const input = this.collectInput(); + const options = this.collectOptions(); + + // Génération rapide pour aperçu + const content = await this.factory.createContent(input, options); + this.displayPreview(content); + + document.getElementById('content-preview').style.display = 'block'; + + } catch (error) { + this.showError('Erreur lors de l\'aperçu: ' + error.message); + } + } + + displayPreview(content) { + if (!this.previewContainer) return; + + this.previewContainer.innerHTML = ` +
+
${content.name}
+

${content.description}

+
+ 📊 ${content.contentItems.length} exercices + 🎯 Difficulté: ${content.difficulty} + 🏷️ Types: ${content.metadata.contentTypes.join(', ')} +
+
+ +
+ ${content.contentItems.slice(0, 3).map(item => ` +
+ ${item.type} + ${item.content.english} = ${item.content.french} + ${item.interaction.type} +
+ `).join('')} + ${content.contentItems.length > 3 ? `
... et ${content.contentItems.length - 3} autres
` : ''} +
+ `; + + document.getElementById('content-preview').style.display = 'block'; + } + + async testInGame() { + if (!this.currentContent) { + this.showError('Aucun contenu généré à tester'); + return; + } + + try { + // Sauvegarder temporairement le contenu + const contentId = 'generated_test'; + window.ContentModules = window.ContentModules || {}; + window.ContentModules.GeneratedTest = this.currentContent; + + // Naviguer vers un jeu pour tester + this.showSuccess('Contenu prêt ! Redirection vers le jeu...'); + + setTimeout(() => { + // Fermer l'interface + document.getElementById('content-creator').remove(); + + // Lancer le jeu avec le contenu généré + AppNavigation.navigateTo('play', 'whack-a-mole', 'generated-test'); + }, 1500); + + } catch (error) { + this.showError('Erreur lors du test: ' + error.message); + } + } + + loadExamples() { + // Les exemples sont gérés par les boutons + } + + loadExample(type) { + switch (type) { + case 'vocabulary': + this.switchTab('vocabulary'); + setTimeout(() => { + // Effacer contenu existant + document.querySelector('.vocab-builder').innerHTML = ''; + + // Ajouter exemples + const animals = [ + { english: 'cat', french: 'chat', category: 'animals' }, + { english: 'dog', french: 'chien', category: 'animals' }, + { english: 'bird', french: 'oiseau', category: 'animals' }, + { english: 'fish', french: 'poisson', category: 'animals' } + ]; + + animals.forEach(animal => { + this.addVocabularyEntry(); + const entries = document.querySelectorAll('.vocab-entry'); + const lastEntry = entries[entries.length - 1]; + lastEntry.querySelector('.english-input').value = animal.english; + lastEntry.querySelector('.french-input').value = animal.french; + lastEntry.querySelector('.category-input').value = animal.category; + }); + + document.getElementById('module-name').value = 'Animaux Domestiques'; + }, 100); + break; + + case 'dialogue': + this.switchTab('dialogue'); + setTimeout(() => { + document.getElementById('dialogue-scenario').value = 'Au Restaurant'; + document.querySelector('.dialogue-builder').innerHTML = ''; + + const dialogueLines = [ + { speaker: 'Serveur', text: 'What would you like to order?' }, + { speaker: 'Client', text: 'I would like a pizza, please.' }, + { speaker: 'Serveur', text: 'What size?' }, + { speaker: 'Client', text: 'Large, please.' } + ]; + + dialogueLines.forEach(line => { + this.addDialogueLine(); + const lines = document.querySelectorAll('.dialogue-line'); + const lastLine = lines[lines.length - 1]; + lastLine.querySelector('.speaker-input').value = line.speaker; + lastLine.querySelector('.line-input').value = line.text; + }); + + document.getElementById('module-name').value = 'Dialogue Restaurant'; + }, 100); + break; + + case 'sequence': + this.switchTab('sequence'); + setTimeout(() => { + document.getElementById('sequence-title').value = 'Morning Routine'; + document.querySelector('.sequence-builder').innerHTML = ''; + + const steps = [ + { text: 'Wake up', time: '07:00' }, + { text: 'Brush teeth', time: '07:15' }, + { text: 'Get dressed', time: '07:30' }, + { text: 'Eat breakfast', time: '07:45' }, + { text: 'Go to school', time: '08:00' } + ]; + + steps.forEach(step => { + this.addSequenceStep(); + const stepElements = document.querySelectorAll('.sequence-step'); + const lastStep = stepElements[stepElements.length - 1]; + lastStep.querySelector('.step-input').value = step.text; + lastStep.querySelector('.time-input').value = step.time; + }); + + document.getElementById('module-name').value = 'Routine du Matin'; + }, 100); + break; + } + } + + showSuccess(message) { + Utils.showToast(message, 'success'); + } + + showError(message) { + Utils.showToast(message, 'error'); + } +} + +// CSS pour l'interface +const contentCreatorStyles = ` + +`; + +// Ajouter les styles +document.head.insertAdjacentHTML('beforeend', contentCreatorStyles); + +// Export global +window.ContentCreator = ContentCreator; \ No newline at end of file