diff --git a/js/core/navigation.js b/js/core/navigation.js index 4542774..52448bb 100644 --- a/js/core/navigation.js +++ b/js/core/navigation.js @@ -9,6 +9,8 @@ const AppNavigation = { compatibilityChecker: null, init() { + // Clear any existing compatibility cache in localStorage + this.clearExistingCache(); this.loadGamesConfig(); this.initContentScanner(); this.initCompatibilityChecker(); @@ -16,6 +18,33 @@ const AppNavigation = { this.handleInitialRoute(); }, + // Clear existing cache from localStorage and sessionStorage + clearExistingCache() { + try { + // Clear any compatibility-related cache + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && (key.includes('compatibility') || key.includes('cache'))) { + keysToRemove.push(key); + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)); + + // Also clear sessionStorage + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + if (key && (key.includes('compatibility') || key.includes('cache'))) { + sessionStorage.removeItem(key); + } + } + + logSh('🗑️ Existing compatibility cache cleared from browser storage', 'DEBUG'); + } catch (error) { + logSh(`⚠️ Error clearing cache: ${error.message}`, 'WARN'); + } + }, + async loadGamesConfig() { // Direct use of default config (no fetch) logSh('📁 Using default configuration', 'INFO'); @@ -460,22 +489,14 @@ const AppNavigation = { const compatibleGames = []; const incompatibleGames = []; - enabledGames.forEach(([key, game]) => { + // Process games sequentially to avoid overwhelming the system + for (const [key, game] of enabledGames) { let compatibility = null; - if (contentInfo && this.compatibilityChecker) { - // Récupérer le module JavaScript réel pour le test de compatibilité - const moduleName = this.getModuleName(contentType); - const actualContentModule = window.ContentModules?.[moduleName]; - - if (actualContentModule) { - compatibility = this.compatibilityChecker.checkCompatibility(actualContentModule, key); - logSh(`🎯 ${game.name} compatibility: ${compatibility.compatible ? '✅' : '❌'} (score: ${compatibility.score}%) - ${compatibility.reason}`, 'DEBUG'); - } else { - logSh(`⚠️ Module JavaScript non trouvé: ${moduleName}`, 'WARN'); - // Pas de compatibilité = compatible par défaut (comportement de fallback) - compatibility = { compatible: true, score: 50, reason: "Module not loaded - default compatibility" }; - } + if (contentInfo) { + // Use embedded compatibility system with async loading and caching + compatibility = await this.checkGameCompatibilityEmbedded(key, contentInfo); + logSh(`🎯 ${game.name} compatibility: ${compatibility.compatible ? '✅' : '❌'} (score: ${compatibility.score}%) - ${compatibility.reason}`, 'DEBUG'); } const gameData = { key, game, compatibility }; @@ -485,7 +506,7 @@ const AppNavigation = { } else { incompatibleGames.push(gameData); } - }); + } // Afficher d'abord les jeux compatibles if (compatibleGames.length > 0) { @@ -817,6 +838,135 @@ const AppNavigation = { } return item; + }, + + // Check game compatibility using embedded system (no caching) + async checkGameCompatibilityEmbedded(gameKey, contentInfo) { + + try { + // Map game keys to module names and file paths + const moduleMapping = { + 'whack-a-mole': { name: 'WhackAMole', file: 'whack-a-mole.js' }, + 'whack-a-mole-hard': { name: 'WhackAMoleHard', file: 'whack-a-mole-hard.js' }, + 'memory-match': { name: 'MemoryMatch', file: 'memory-match.js' }, + 'quiz-game': { name: 'QuizGame', file: 'quiz-game.js' }, + 'fill-the-blank': { name: 'FillTheBlank', file: 'fill-the-blank.js' }, + 'word-discovery': { name: 'WordDiscovery', file: 'word-discovery.js' }, + 'river-run': { name: 'RiverRun', file: 'river-run.js' }, + 'story-reader': { name: 'StoryReader', file: 'story-reader.js' }, + 'adventure-reader': { name: 'AdventureReader', file: 'adventure-reader.js' }, + 'letter-discovery': { name: 'LetterDiscovery', file: 'letter-discovery.js' }, + 'chinese-study': { name: 'ChineseStudy', file: 'chinese-study.js' }, + 'grammar-discovery': { name: 'GrammarDiscovery', file: 'grammar-discovery.js' }, + 'word-storm': { name: 'WordStorm', file: 'word-storm.js' }, + 'story-builder': { name: 'StoryBuilder', file: 'story-builder.js' } + }; + + const moduleInfo = moduleMapping[gameKey]; + if (!moduleInfo) { + return { compatible: true, score: 50, reason: "Unknown game type - default compatibility" }; + } + + // Check if module is already loaded + let GameModule = window.GameModules?.[moduleInfo.name]; + + // If not loaded, load it dynamically + if (!GameModule) { + logSh(`📥 Loading game module: ${moduleInfo.file}`, 'DEBUG'); + await this.loadGameModule(moduleInfo.file); + GameModule = window.GameModules?.[moduleInfo.name]; + } + + if (!GameModule) { + return { compatible: true, score: 50, reason: "Failed to load game module - default compatibility" }; + } + + // Check if the game has embedded compatibility methods + if (!GameModule.checkContentCompatibility) { + return { compatible: true, score: 50, reason: "No embedded compatibility system - default compatibility" }; + } + + // Load the actual content module to get vocabulary, not just metadata + let actualContent = contentInfo; + if (contentInfo.filename && !contentInfo.vocabulary) { + console.log(`📥 Loading actual content module: ${contentInfo.filename}`); + try { + // Load the content module script dynamically + await this.loadContentModule(contentInfo.filename); + + // Get the actual loaded content from window.ContentModules + const moduleKey = Object.keys(window.ContentModules || {}).find(key => { + const module = window.ContentModules[key]; + return module.id === contentInfo.id || module.name === contentInfo.name; + }); + + if (moduleKey && window.ContentModules[moduleKey]) { + actualContent = window.ContentModules[moduleKey]; + console.log(`✅ Loaded actual content with vocabulary:`, actualContent.vocabulary ? 'YES' : 'NO'); + } + } catch (error) { + console.warn(`⚠️ Failed to load content module: ${error.message}`); + } + } + + // Use the game's embedded compatibility check + console.log(`🎯 Calling embedded compatibility for ${gameKey} with content:`, actualContent.name || 'Unknown'); + console.log(`🎯 Content has vocabulary:`, actualContent.vocabulary ? 'YES' : 'NO'); + const result = GameModule.checkContentCompatibility(actualContent); + console.log(`🎯 Embedded result for ${gameKey}:`, result); + + // Convert to navigation format + const minScore = 50; // Minimum score for compatibility + const finalResult = { + compatible: result.score >= minScore, + score: result.score, + reason: result.score >= minScore ? + `Compatible (${result.score}%)` : + `Incompatible (${result.score}% < ${minScore}%)`, + details: result.details, + recommendations: result.recommendations + }; + console.log(`🎯 Final result for ${gameKey}:`, finalResult); + return finalResult; + + } catch (error) { + logSh(`❌ Error checking compatibility for ${gameKey}: ${error.message}`, 'ERROR'); + return { compatible: true, score: 50, reason: "Compatibility check error - default compatibility" }; + } + }, + + // Dynamically load a game module + async loadGameModule(filename) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = `js/games/${filename}`; + script.onload = () => { + logSh(`✅ Game module loaded: ${filename}`, 'DEBUG'); + resolve(); + }; + script.onerror = () => { + logSh(`❌ Failed to load game module: ${filename}`, 'ERROR'); + reject(new Error(`Failed to load ${filename}`)); + }; + document.head.appendChild(script); + }); + }, + + // Dynamically load a content module + async loadContentModule(filename) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = `js/content/${filename}`; + script.onload = () => { + logSh(`✅ Content module loaded: ${filename}`, 'DEBUG'); + resolve(); + }; + script.onerror = () => { + logSh(`❌ Failed to load content module: ${filename}`, 'ERROR'); + reject(new Error(`Failed to load ${filename}`)); + }; + document.head.appendChild(script); + }); } }; diff --git a/js/games/adventure-reader.js b/js/games/adventure-reader.js index 6e81dca..72b8706 100644 --- a/js/games/adventure-reader.js +++ b/js/games/adventure-reader.js @@ -1280,6 +1280,200 @@ class AdventureReaderGame { } this.container.innerHTML = ''; } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + stories: 1, + vocabulary: 15 + }, + optimal: { + stories: 2, + vocabulary: 30 + }, + name: "Adventure Reader", + description: "Interactive RPG-style reading with vocabulary learning and TTS support" + }; + } + + static checkContentCompatibility(content) { + const requirements = AdventureReaderGame.getCompatibilityRequirements(); + + // Extract stories and vocabulary using same method as instance + const stories = AdventureReaderGame.extractStoriesStatic(content); + const vocabulary = AdventureReaderGame.extractVocabularyStatic(content); + + const storyCount = stories.length; + const vocabCount = vocabulary.length; + + // Calculate score based on both stories and vocabulary + + // Dynamic percentage based on optimal volumes (1→2 stories, 15→30 vocab) + // Stories: 0=0%, 1=50%, 2=100% + // Vocabulary: 0=0%, 15=50%, 30=100% + const storyScore = Math.min(100, (storyCount / requirements.optimal.stories) * 100); + const vocabScore = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + // Combined score (weighted average: 70% stories, 30% vocabulary) + const finalScore = (storyScore * 0.7) + (vocabScore * 0.3); + + const recommendations = []; + if (storyCount < requirements.optimal.stories) { + recommendations.push(`Add ${requirements.optimal.stories - storyCount} more stories for optimal experience`); + } + if (vocabCount < requirements.optimal.vocabulary) { + recommendations.push(`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for enhanced learning`); + } + + return { + score: Math.round(finalScore), + details: { + stories: { + found: storyCount, + minimum: requirements.minimum.stories, + optimal: requirements.optimal.stories, + status: storyCount >= requirements.minimum.stories ? 'sufficient' : 'insufficient' + }, + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + } + }, + recommendations: recommendations + }; + } + + static extractStoriesStatic(content) { + let stories = []; + + // Priority 1: Use raw module content + if (content.rawContent) { + // Extract from story object + if (content.rawContent.story && content.rawContent.story.chapters) { + stories.push(content.rawContent.story); + } + + // Extract from additionalStories array + if (content.rawContent.additionalStories && Array.isArray(content.rawContent.additionalStories)) { + stories.push(...content.rawContent.additionalStories); + } + } + + // Priority 2: Direct content properties + if (content.story && content.story.chapters) { + stories.push(content.story); + } + + if (content.additionalStories && Array.isArray(content.additionalStories)) { + stories.push(...content.additionalStories); + } + + // Filter valid stories + stories = stories.filter(story => + story && + typeof story === 'object' && + story.chapters && + Array.isArray(story.chapters) && + story.chapters.length > 0 + ); + + return stories; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return AdventureReaderGame.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return AdventureReaderGame.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return AdventureReaderGame.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.word === 'string' && + typeof word.translation === 'string' && + word.word.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } } // Module registration diff --git a/js/games/chinese-study.js b/js/games/chinese-study.js index 5f13f83..0fa38f1 100644 --- a/js/games/chinese-study.js +++ b/js/games/chinese-study.js @@ -1578,6 +1578,163 @@ class ChineseStudyGame { this.createGameInterface(); logSh('Chinese Study Mode restarted', 'INFO'); } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 10 + }, + optimal: { + vocabulary: 20 + }, + name: "Chinese Study", + description: "Chinese character and vocabulary learning with multiple study modes" + }; + } + + static checkContentCompatibility(content) { + const requirements = ChineseStudyGame.getCompatibilityRequirements(); + + // Extract vocabulary using same method as instance + const vocabulary = ChineseStudyGame.extractVocabularyStatic(content); + const vocabCount = vocabulary.length; + + // Dynamic percentage based on optimal volume (10 min → 20 optimal) + // 0 words = 0%, 10 words = 50%, 20 words = 100% + const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + const recommendations = []; + if (vocabCount < requirements.optimal.vocabulary) { + recommendations.push(`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`); + } + + // Count Chinese-specific features for bonus recommendations + const hasChineseCharacters = vocabulary.some(word => word.chinese || /[\u4e00-\u9fff]/.test(word.original)); + const hasPronunciation = vocabulary.some(word => word.pronunciation || word.pinyin); + + if (!hasChineseCharacters) { + recommendations.push("Add Chinese characters for authentic Chinese study experience"); + } + if (!hasPronunciation) { + recommendations.push("Add pronunciation (pinyin) for pronunciation practice"); + } + + return { + score: Math.round(score), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + }, + chineseFeatures: { + characters: hasChineseCharacters ? 'available' : 'missing', + pronunciation: hasPronunciation ? 'available' : 'missing' + } + }, + recommendations: recommendations + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return ChineseStudyGame.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + chinese: word, // Use original as Chinese if it contains Chinese characters + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + pinyin: data.pronunciation, // Use pronunciation as pinyin + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + chinese: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return ChineseStudyGame.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + chinese: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + pinyin: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + chinese: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return ChineseStudyGame.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } } // Export to global scope diff --git a/js/games/fill-the-blank.js b/js/games/fill-the-blank.js index 5628c1a..82f1678 100644 --- a/js/games/fill-the-blank.js +++ b/js/games/fill-the-blank.js @@ -562,6 +562,228 @@ class FillTheBlankGame { this.isRunning = false; this.container.innerHTML = ''; } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 5, + sentences: 3 + }, + optimal: { + vocabulary: 12, + sentences: 8 + }, + name: "Fill the Blank", + description: "Needs vocabulary and sentences/texts to create meaningful cloze exercises" + }; + } + + static checkContentCompatibility(content) { + const requirements = FillTheBlankGame.getCompatibilityRequirements(); + + // Extract vocabulary and sentences using same method as instance + const vocabulary = FillTheBlankGame.extractVocabularyStatic(content); + const sentences = FillTheBlankGame.extractSentencesStatic(content); + + const vocabCount = vocabulary.length; + const sentenceCount = sentences.length; + + // Dynamic percentage based on optimal volumes (5→12 vocab, 3→8 sentences) + // Vocabulary: 0=0%, 6=50%, 12=100% + // Sentences: 0=0%, 4=50%, 8=100% + const vocabScore = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + const sentenceScore = Math.min(100, (sentenceCount / requirements.optimal.sentences) * 100); + + // Combined score (weighted average: 60% vocabulary, 40% sentences) + const finalScore = (vocabScore * 0.6) + (sentenceScore * 0.4); + + const recommendations = []; + if (vocabCount < requirements.optimal.vocabulary) { + recommendations.push(`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words`); + } + if (sentenceCount < requirements.optimal.sentences) { + recommendations.push(`Add ${requirements.optimal.sentences - sentenceCount} more sentences/texts`); + } + + return { + score: Math.round(finalScore), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + }, + sentences: { + found: sentenceCount, + minimum: requirements.minimum.sentences, + optimal: requirements.optimal.sentences, + status: sentenceCount >= requirements.minimum.sentences ? 'sufficient' : 'insufficient' + } + }, + recommendations: recommendations + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return FillTheBlankGame.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return FillTheBlankGame.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return FillTheBlankGame.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } + + static extractSentencesStatic(content) { + let sentences = []; + + // Priority 1: Use raw module content + if (content.rawContent) { + // Extract from sentences array + if (content.rawContent.sentences && Array.isArray(content.rawContent.sentences)) { + content.rawContent.sentences.forEach(sentence => { + if (sentence.english) { + sentences.push(sentence.english); + } + if (sentence.chinese) { + sentences.push(sentence.chinese); + } + if (sentence.french) { + sentences.push(sentence.french); + } + }); + } + + // Extract from texts array + if (content.rawContent.texts && Array.isArray(content.rawContent.texts)) { + content.rawContent.texts.forEach(text => { + if (text.content) { + // Split text content into sentences + const textSentences = text.content.split(/[.!?]+/).filter(s => s.trim().length > 10); + sentences.push(...textSentences); + } + }); + } + } + + // Priority 2: Direct content properties + if (content.sentences && Array.isArray(content.sentences)) { + content.sentences.forEach(sentence => { + if (sentence.english) { + sentences.push(sentence.english); + } + if (sentence.chinese) { + sentences.push(sentence.chinese); + } + if (sentence.french) { + sentences.push(sentence.french); + } + }); + } + + if (content.texts && Array.isArray(content.texts)) { + content.texts.forEach(text => { + if (text.content) { + const textSentences = text.content.split(/[.!?]+/).filter(s => s.trim().length > 10); + sentences.push(...textSentences); + } + }); + } + + // Filter and validate sentences + sentences = sentences.filter(sentence => + sentence && + typeof sentence === 'string' && + sentence.trim().length > 5 && + sentence.split(' ').length >= 3 + ); + + return sentences; + } } // Module registration diff --git a/js/games/grammar-discovery.js b/js/games/grammar-discovery.js index b1f83d5..0b87b79 100644 --- a/js/games/grammar-discovery.js +++ b/js/games/grammar-discovery.js @@ -1178,6 +1178,135 @@ class GrammarDiscovery { delete window.currentGrammarGame; } } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + grammarRules: 1, + examples: 1 + }, + optimal: { + grammarRules: 2, + examples: 2, + exercises: 1 + }, + name: "Grammar Discovery", + description: "Grammar rule learning with examples and practice exercises" + }; + } + + static checkContentCompatibility(content) { + const requirements = GrammarDiscovery.getCompatibilityRequirements(); + + // Extract grammar data using same method as instance + const grammarData = GrammarDiscovery.extractGrammarDataStatic(content); + + const ruleCount = grammarData.length; + const hasExamples = grammarData.some(rule => rule.examples && rule.examples.length > 0); + const hasExercises = grammarData.some(rule => rule.exercises && rule.exercises.length > 0); + const avgExamples = ruleCount > 0 ? + grammarData.reduce((sum, rule) => sum + (rule.examples ? rule.examples.length : 0), 0) / ruleCount : 0; + + // Calculate score based on rules, examples, and exercises + + // Dynamic percentage based on optimal volumes (1→2 rules, 1→2 examples, exercises bonus) + // Rules: 0=0%, 1=50%, 2=100% + // Examples: 0=0%, 1=50%, 2=100% + // Exercises: 0=0%, 1=100% (binary) + const ruleScore = Math.min(100, (ruleCount / requirements.optimal.grammarRules) * 100); + const exampleScore = Math.min(100, (avgExamples / requirements.optimal.examples) * 100); + const exerciseScore = hasExercises ? 100 : 0; + + // Combined score (weighted: 50% rules, 30% examples, 20% exercises) + const finalScore = (ruleScore * 0.5) + (exampleScore * 0.3) + (exerciseScore * 0.2); + + const recommendations = []; + if (ruleCount < requirements.optimal.grammarRules) { + recommendations.push(`Add ${requirements.optimal.grammarRules - ruleCount} more grammar rules`); + } + if (avgExamples < requirements.optimal.examples) { + recommendations.push("Add more examples for each grammar rule"); + } + if (!hasExercises) { + recommendations.push("Add practice exercises for interactive learning"); + } + + return { + score: Math.round(finalScore), + details: { + grammarRules: { + found: ruleCount, + minimum: requirements.minimum.grammarRules, + optimal: requirements.optimal.grammarRules, + status: ruleCount >= requirements.minimum.grammarRules ? 'sufficient' : 'insufficient' + }, + examples: { + average: Math.round(avgExamples * 10) / 10, + minimum: requirements.minimum.examples, + optimal: requirements.optimal.examples, + status: avgExamples >= requirements.minimum.examples ? 'sufficient' : 'insufficient' + }, + exercises: { + available: hasExercises ? 'yes' : 'no', + status: hasExercises ? 'available' : 'missing' + } + }, + recommendations: recommendations + }; + } + + static extractGrammarDataStatic(content) { + let grammarRules = []; + + // Priority 1: Use raw module content + if (content.rawContent) { + // Extract from grammar object + if (content.rawContent.grammar && typeof content.rawContent.grammar === 'object') { + grammarRules = Object.entries(content.rawContent.grammar).map(([ruleKey, ruleData]) => { + if (typeof ruleData === 'object') { + return { + key: ruleKey, + title: ruleData.title || ruleKey, + explanation: ruleData.explanation || '', + examples: ruleData.examples || [], + exercises: ruleData.exercises || [], + category: ruleData.category || 'general' + }; + } + return null; + }).filter(Boolean); + } + } + + // Priority 2: Direct content properties + if (content.grammar && typeof content.grammar === 'object') { + grammarRules = Object.entries(content.grammar).map(([ruleKey, ruleData]) => { + if (typeof ruleData === 'object') { + return { + key: ruleKey, + title: ruleData.title || ruleKey, + explanation: ruleData.explanation || '', + examples: ruleData.examples || [], + exercises: ruleData.exercises || [], + category: ruleData.category || 'general' + }; + } + return null; + }).filter(Boolean); + } + + // Filter and validate grammar rules + grammarRules = grammarRules.filter(rule => + rule && + typeof rule.title === 'string' && + rule.title.trim() !== '' && + typeof rule.explanation === 'string' && + rule.explanation.trim() !== '' + ); + + return grammarRules; + } } // Export to global diff --git a/js/games/letter-discovery.js b/js/games/letter-discovery.js index 6147d52..7dfbf40 100644 --- a/js/games/letter-discovery.js +++ b/js/games/letter-discovery.js @@ -774,6 +774,164 @@ class LetterDiscovery { styleSheet.remove(); } } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + letters: 2, + wordsPerLetter: 3 + }, + optimal: { + letters: 8, + wordsPerLetter: 4 + }, + name: "Letter Discovery", + description: "Alphabet learning with words starting with each letter" + }; + } + + static checkContentCompatibility(content) { + const requirements = LetterDiscovery.getCompatibilityRequirements(); + + // Extract letter-based vocabulary using same method as instance + const letterData = LetterDiscovery.extractLetterDataStatic(content); + + const letterCount = Object.keys(letterData).length; + const avgWordsPerLetter = letterCount > 0 ? + Object.values(letterData).reduce((sum, words) => sum + words.length, 0) / letterCount : 0; + + // Dynamic percentage based on optimal volumes (2→8 letters, 3→4 words/letter) + // Letters: 0=0%, 4=50%, 8=100% + // Words per letter: 0=0%, 2=50%, 4=100% + const letterScore = Math.min(100, (letterCount / requirements.optimal.letters) * 100); + const wordsScore = Math.min(100, (avgWordsPerLetter / requirements.optimal.wordsPerLetter) * 100); + + // Combined score (weighted average: 60% letters, 40% words per letter) + const finalScore = (letterScore * 0.6) + (wordsScore * 0.4); + + const recommendations = []; + if (letterCount < requirements.optimal.letters) { + recommendations.push(`Add vocabulary for ${requirements.optimal.letters - letterCount} more letters`); + } + if (avgWordsPerLetter < requirements.optimal.wordsPerLetter) { + const wordsNeeded = Math.ceil((requirements.optimal.wordsPerLetter * letterCount) - + Object.values(letterData).reduce((sum, words) => sum + words.length, 0)); + if (wordsNeeded > 0) { + recommendations.push(`Add ${wordsNeeded} more words for better letter coverage`); + } + } + + return { + score: Math.round(finalScore), + details: { + letters: { + found: letterCount, + minimum: requirements.minimum.letters, + optimal: requirements.optimal.letters, + status: letterCount >= requirements.minimum.letters ? 'sufficient' : 'insufficient' + }, + wordsPerLetter: { + average: Math.round(avgWordsPerLetter * 10) / 10, + minimum: requirements.minimum.wordsPerLetter, + optimal: requirements.optimal.wordsPerLetter, + status: avgWordsPerLetter >= requirements.minimum.wordsPerLetter ? 'sufficient' : 'insufficient' + } + }, + recommendations: recommendations + }; + } + + static extractLetterDataStatic(content) { + const letterWords = {}; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return LetterDiscovery.extractLetterDataFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + Object.entries(content.vocabulary).forEach(([word, data]) => { + let wordData; + + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + wordData = { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + wordData = { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + + if (wordData && wordData.word && wordData.translation) { + const firstLetter = wordData.word.charAt(0).toUpperCase(); + if (!letterWords[firstLetter]) { + letterWords[firstLetter] = []; + } + letterWords[firstLetter].push(wordData); + } + }); + } + + return letterWords; + } + + static extractLetterDataFromRawStatic(rawContent) { + const letterWords = {}; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + Object.entries(rawContent.vocabulary).forEach(([word, data]) => { + let wordData; + + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + wordData = { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + wordData = { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + + if (wordData && wordData.word && wordData.translation) { + const firstLetter = wordData.word.charAt(0).toUpperCase(); + if (!letterWords[firstLetter]) { + letterWords[firstLetter] = []; + } + letterWords[firstLetter].push(wordData); + } + }); + } + + return letterWords; + } } // Register the game module diff --git a/js/games/memory-match.js b/js/games/memory-match.js index d3ceb40..dc9370e 100644 --- a/js/games/memory-match.js +++ b/js/games/memory-match.js @@ -488,6 +488,139 @@ class MemoryMatchGame { destroy() { this.container.innerHTML = ''; } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 6 + }, + optimal: { + vocabulary: 15 + }, + name: "Memory Match", + description: "Needs vocabulary pairs for card matching (8 pairs optimal, 6 minimum)" + }; + } + + static checkContentCompatibility(content) { + const requirements = MemoryMatchGame.getCompatibilityRequirements(); + + // Extract vocabulary using same method as instance + const vocabulary = MemoryMatchGame.extractVocabularyStatic(content); + const vocabCount = vocabulary.length; + + // Dynamic percentage based on optimal volume (6 min → 15 optimal) + // 0 words = 0%, 8 words = 53%, 15 words = 100% + const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + return { + score: Math.round(score), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + } + }, + recommendations: vocabCount < requirements.optimal.vocabulary ? + [`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`] : + [] + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Use raw module content if available + if (content.rawContent) { + return MemoryMatchGame.extractVocabularyFromRawStatic(content.rawContent); + } + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return MemoryMatchGame.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return MemoryMatchGame.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Filter and validate vocabulary for ultra-modular format + vocabulary = vocabulary.filter(item => + item && + typeof item.original === 'string' && + typeof item.translation === 'string' && + item.original.trim() !== '' && + item.translation.trim() !== '' + ); + + return vocabulary; + } } // Module registration diff --git a/js/games/quiz-game.js b/js/games/quiz-game.js index 7d59be5..cd90e45 100644 --- a/js/games/quiz-game.js +++ b/js/games/quiz-game.js @@ -522,6 +522,139 @@ class QuizGame { destroy() { this.container.innerHTML = ''; } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 8 + }, + optimal: { + vocabulary: 16 + }, + name: "Quiz Game", + description: "Needs vocabulary with translations for multiple choice questions" + }; + } + + static checkContentCompatibility(content) { + const requirements = QuizGame.getCompatibilityRequirements(); + + // Extract vocabulary using same method as instance + const vocabulary = QuizGame.extractVocabularyStatic(content); + const vocabCount = vocabulary.length; + + // Dynamic percentage based on optimal volume (8 min → 16 optimal) + // 0 words = 0%, 8 words = 50%, 16 words = 100% + const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + return { + score: Math.round(score), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + } + }, + recommendations: vocabCount < requirements.optimal.vocabulary ? + [`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`] : + [] + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return QuizGame.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return QuizGame.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return QuizGame.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } } // Module registration diff --git a/js/games/river-run.js b/js/games/river-run.js index aef4f7c..17521a0 100644 --- a/js/games/river-run.js +++ b/js/games/river-run.js @@ -964,6 +964,139 @@ class RiverRun { styleSheet.remove(); } } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 10 + }, + optimal: { + vocabulary: 25 + }, + name: "River Run", + description: "Vocabulary collection game where player navigates river to collect target words" + }; + } + + static checkContentCompatibility(content) { + const requirements = RiverRun.getCompatibilityRequirements(); + + // Extract vocabulary using same method as instance + const vocabulary = RiverRun.extractVocabularyStatic(content); + const vocabCount = vocabulary.length; + + // Dynamic percentage based on optimal volume (10 min → 25 optimal) + // 0 words = 0%, 12 words = 48%, 25 words = 100% + const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + return { + score: Math.round(score), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + } + }, + recommendations: vocabCount < requirements.optimal.vocabulary ? + [`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`] : + [] + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return RiverRun.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return RiverRun.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return RiverRun.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.word === 'string' && + typeof word.translation === 'string' && + word.word.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } } // Add CSS animations diff --git a/js/games/story-builder.js b/js/games/story-builder.js index 49eef8a..85ff986 100644 --- a/js/games/story-builder.js +++ b/js/games/story-builder.js @@ -766,6 +766,36 @@ class StoryBuilderGame { this.endGame(); this.container.innerHTML = ''; } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + // Story Builder is always 0% compatible - it's a mockup/prototype + }, + optimal: { + // Story Builder is always 0% compatible - it's a mockup/prototype + }, + name: "Story Builder", + description: "Interactive story construction game (prototype - not yet tested)" + }; + } + + static checkContentCompatibility(content) { + // Story Builder always returns 0% compatibility as specified + // This is a mockup/prototype that hasn't been thoroughly tested + return { + score: 0, + details: { + status: 'prototype', + reason: 'Story Builder is a prototype feature not yet ready for production use' + }, + recommendations: [ + "Story Builder is in development and not available for use yet", + "Please choose other games while this feature is being completed" + ] + }; + } } // CSS pour Story Builder diff --git a/js/games/story-reader.js b/js/games/story-reader.js index d552d6d..78f2e0d 100644 --- a/js/games/story-reader.js +++ b/js/games/story-reader.js @@ -1359,6 +1359,135 @@ class StoryReader { } this.container.innerHTML = ''; } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + texts: 1 + }, + optimal: { + texts: 1, + stories: 1 + }, + name: "Story Reader", + description: "Needs text content or stories for guided reading experience" + }; + } + + static checkContentCompatibility(content) { + const requirements = StoryReader.getCompatibilityRequirements(); + + // Extract texts and stories using same method as instance + const texts = StoryReader.extractTextsStatic(content); + const stories = StoryReader.extractStoriesStatic(content); + + const textCount = texts.length; + const storyCount = stories.length; + const totalContent = textCount + storyCount; + + // Dynamic percentage based on content quality (1 text min → 1 story optimal) + // Stories are much better than texts for reading experience + let score = 0; + if (storyCount >= 1) { + score = 100; // Perfect: has stories + } else if (textCount >= 3) { + score = 75; // Good: multiple texts + } else if (textCount >= 1) { + score = 50; // Basic: at least one text + } else { + score = 0; // No content + } + + const recommendations = []; + if (totalContent === 0) { + recommendations.push("Add text content or stories for reading"); + } else if (storyCount === 0) { + recommendations.push("Add story content for optimal reading experience"); + } + + return { + score: Math.round(score), + details: { + texts: { + found: textCount, + minimum: requirements.minimum.texts, + status: textCount >= requirements.minimum.texts ? 'sufficient' : 'insufficient' + }, + stories: { + found: storyCount, + optimal: requirements.optimal.stories, + status: storyCount >= requirements.optimal.stories ? 'available' : 'missing' + } + }, + recommendations: recommendations + }; + } + + static extractTextsStatic(content) { + let texts = []; + + // Priority 1: Use raw module content + if (content.rawContent) { + // Extract from texts array + if (content.rawContent.texts && Array.isArray(content.rawContent.texts)) { + texts.push(...content.rawContent.texts); + } + } + + // Priority 2: Direct content properties + if (content.texts && Array.isArray(content.texts)) { + texts.push(...content.texts); + } + + // Filter valid texts + texts = texts.filter(text => + text && + typeof text === 'object' && + text.content && + typeof text.content === 'string' && + text.content.trim().length > 10 + ); + + return texts; + } + + static extractStoriesStatic(content) { + let stories = []; + + // Priority 1: Use raw module content + if (content.rawContent) { + // Extract from story object + if (content.rawContent.story && content.rawContent.story.chapters) { + stories.push(content.rawContent.story); + } + + // Extract from additionalStories array + if (content.rawContent.additionalStories && Array.isArray(content.rawContent.additionalStories)) { + stories.push(...content.rawContent.additionalStories); + } + } + + // Priority 2: Direct content properties + if (content.story && content.story.chapters) { + stories.push(content.story); + } + + if (content.additionalStories && Array.isArray(content.additionalStories)) { + stories.push(...content.additionalStories); + } + + // Filter valid stories + stories = stories.filter(story => + story && + typeof story === 'object' && + story.chapters && + Array.isArray(story.chapters) && + story.chapters.length > 0 + ); + + return stories; + } } // Module registration diff --git a/js/games/whack-a-mole-hard.js b/js/games/whack-a-mole-hard.js index 6eee5dc..5cdd103 100644 --- a/js/games/whack-a-mole-hard.js +++ b/js/games/whack-a-mole-hard.js @@ -696,6 +696,139 @@ class WhackAMoleHardGame { this.stop(); this.container.innerHTML = ''; } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 12 + }, + optimal: { + vocabulary: 25 + }, + name: "Whack-a-Mole Hard", + description: "Hard mode requires more vocabulary words for increased difficulty (3 moles at once)" + }; + } + + static checkContentCompatibility(content) { + const requirements = WhackAMoleHardGame.getCompatibilityRequirements(); + + // Extract vocabulary using same method as instance + const vocabulary = WhackAMoleHardGame.extractVocabularyStatic(content); + const vocabCount = vocabulary.length; + + // Dynamic percentage based on optimal volume (12 min → 25 optimal) + // 0 words = 0%, 12 words = 48%, 25 words = 100% + const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + return { + score: Math.round(score), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + } + }, + recommendations: vocabCount < requirements.optimal.vocabulary ? + [`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`] : + [] + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return WhackAMoleHardGame.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return WhackAMoleHardGame.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return WhackAMoleHardGame.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } } // Module registration diff --git a/js/games/whack-a-mole.js b/js/games/whack-a-mole.js index fc69242..e30ee5f 100644 --- a/js/games/whack-a-mole.js +++ b/js/games/whack-a-mole.js @@ -1,6 +1,36 @@ // === MODULE WHACK-A-MOLE === class WhackAMoleGame { + // === EMBEDDED COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { vocabulary: 8 }, + optimal: { vocabulary: 20 }, + name: "Whack-a-Mole", + description: "Needs vocabulary words with translations for mole targets" + }; + } + + + static extractVocabularyStatic(content) { + // Static version of vocabulary extraction for compatibility checking + let vocabulary = []; + + if (content.rawContent?.vocabulary) { + vocabulary = Object.entries(content.rawContent.vocabulary).map(([word, data]) => ({ + original: word, + translation: typeof data === 'string' ? data : data.user_language || data.translation + })).filter(item => item.translation); + } else if (content.vocabulary) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => ({ + original: word, + translation: typeof data === 'string' ? data : data.user_language || data.translation + })).filter(item => item.translation); + } + + return vocabulary; + } + constructor(options) { this.container = options.container; this.content = options.content; @@ -678,6 +708,144 @@ class WhackAMoleGame { this.stop(); this.container.innerHTML = ''; } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 8 + }, + optimal: { + vocabulary: 20 + }, + name: "Whack-a-Mole", + description: "Needs vocabulary words with translations for mole targets" + }; + } + + static checkContentCompatibility(content) { + const requirements = WhackAMoleGame.getCompatibilityRequirements(); + + // Extract vocabulary using same method as instance + const vocabulary = WhackAMoleGame.extractVocabularyStatic(content); + const vocabCount = vocabulary.length; + + // Dynamic percentage based on optimal volume (8 min → 20 optimal) + // 0 words = 0%, 10 words = 50%, 20 words = 100% + const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + // DEBUG: Log calculation details + console.log(`🔍 WhackAMole DEBUG - Content: ${content.name || 'Unknown'}`); + console.log(`📊 Vocab found: ${vocabCount}, Min: ${requirements.minimum.vocabulary}, Optimal: ${requirements.optimal.vocabulary}`); + console.log(`🧮 Calculation: (${vocabCount} / ${requirements.optimal.vocabulary}) * 100 = ${score}`); + + return { + score: Math.round(score), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + } + }, + recommendations: vocabCount < requirements.optimal.vocabulary ? + [`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`] : + [] + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return WhackAMoleGame.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return WhackAMoleGame.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return WhackAMoleGame.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } } // Module registration diff --git a/js/games/word-discovery.js b/js/games/word-discovery.js index b16cbd4..bd1c432 100644 --- a/js/games/word-discovery.js +++ b/js/games/word-discovery.js @@ -1039,6 +1039,155 @@ class WordDiscovery { styleSheet.remove(); } } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 10 + }, + optimal: { + vocabulary: 20 + }, + name: "Word Discovery", + description: "Progressive vocabulary learning with discovery and practice phases" + }; + } + + static checkContentCompatibility(content) { + const requirements = WordDiscovery.getCompatibilityRequirements(); + + // Extract vocabulary using same method as instance + const vocabulary = WordDiscovery.extractVocabularyStatic(content); + const vocabCount = vocabulary.length; + + // Dynamic percentage based on optimal volume (10 min → 20 optimal) + // 0 words = 0%, 10 words = 50%, 20 words = 100% + const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + const recommendations = []; + if (vocabCount < requirements.optimal.vocabulary) { + recommendations.push(`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`); + } + + // Count multimedia features for bonus recommendations + const hasImages = vocabulary.some(word => word.image); + const hasAudio = vocabulary.some(word => word.audioFile || word.pronunciation); + + if (!hasImages) { + recommendations.push("Add images to vocabulary for visual learning challenges"); + } + if (!hasAudio) { + recommendations.push("Add audio files or pronunciation guides for audio challenges"); + } + + return { + score: Math.round(score), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + }, + multimedia: { + images: hasImages ? 'available' : 'missing', + audio: hasAudio ? 'available' : 'missing' + } + }, + recommendations: recommendations + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return WordDiscovery.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + image: data.image, + audioFile: data.audio, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return WordDiscovery.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + image: data.image, + audioFile: data.audio, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return WordDiscovery.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.word === 'string' && + typeof word.translation === 'string' && + word.word.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } } // Register the game module diff --git a/js/games/word-storm.js b/js/games/word-storm.js index a3add1a..2967156 100644 --- a/js/games/word-storm.js +++ b/js/games/word-storm.js @@ -649,6 +649,149 @@ class WordStormGame { logSh('Word Storm destroyed', 'INFO'); } + + // === COMPATIBILITY SYSTEM === + static getCompatibilityRequirements() { + return { + minimum: { + vocabulary: 8 + }, + optimal: { + vocabulary: 20 + }, + name: "Word Storm", + description: "Fast-paced vocabulary game with falling words to catch" + }; + } + + static checkContentCompatibility(content) { + const requirements = WordStormGame.getCompatibilityRequirements(); + + // DEBUG: Log content structure + console.log(`🔍 WordStorm DEBUG - Content received:`, content); + console.log(`🔍 WordStorm DEBUG - Content vocabulary:`, content.vocabulary); + + // Extract vocabulary using same method as instance + const vocabulary = WordStormGame.extractVocabularyStatic(content); + const vocabCount = vocabulary.length; + + // DEBUG: Log extraction results + console.log(`🔍 WordStorm DEBUG - Extracted vocabulary:`, vocabulary); + console.log(`🔍 WordStorm DEBUG - Vocab count: ${vocabCount}, Requirements:`, requirements); + + // Dynamic percentage based on optimal volume (8 min → 20 optimal) + // 0 words = 0%, 10 words = 50%, 20 words = 100% + const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100); + + console.log(`🔍 WordStorm DEBUG - Final score: ${score}%`); + + return { + score: Math.round(score), + details: { + vocabulary: { + found: vocabCount, + minimum: requirements.minimum.vocabulary, + optimal: requirements.optimal.vocabulary, + status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient' + } + }, + recommendations: vocabCount < requirements.optimal.vocabulary ? + [`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`] : + [] + }; + } + + static extractVocabularyStatic(content) { + let vocabulary = []; + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + return WordStormGame.extractVocabularyFromRawStatic(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return WordStormGame.finalizeVocabularyStatic(vocabulary); + } + + static extractVocabularyFromRawStatic(rawContent) { + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + word: word, + translation: data.user_language.split(';')[0], + fullTranslation: data.user_language, + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + word: word, + translation: data.split(';')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + + return WordStormGame.finalizeVocabularyStatic(vocabulary); + } + + static finalizeVocabularyStatic(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.word === 'string' && + typeof word.translation === 'string' && + word.word.trim() !== '' && + word.translation.trim() !== '' + ); + + return vocabulary; + } } // Export to global namespace