Implement dynamic percentage compatibility system across all games
Major architectural update to replace fixed 50%/100% scoring with true dynamic percentages based on content volume: • Replace old interpolation system with Math.min(100, (count/optimal)*100) formula • Add embedded compatibility methods to all 14 game modules with static requirements • Remove compatibility cache system for real-time calculation • Fix content loading to pass complete modules with vocabulary (not just metadata) • Clean up duplicate syntax errors in adventure-reader and grammar-discovery • Update navigation.js module mapping to match actual exported class names Examples of new dynamic scoring: - 15 words / 20 optimal = 75% (was 87.5% with old interpolation) - 5 words / 10 minimum = 50% (was 25% with old linear system) - 30 words / 20 optimal = 100% (unchanged) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
30fb6cd46c
commit
24362165ab
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user