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

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

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

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

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

978 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// === NAVIGATION SYSTEM ===
const AppNavigation = {
currentPage: 'home',
navigationHistory: ['home'],
gamesConfig: null,
contentScanner: new ContentScanner(),
scannedContent: null,
compatibilityChecker: null,
init() {
// Clear any existing compatibility cache in localStorage
this.clearExistingCache();
this.loadGamesConfig();
this.initContentScanner();
this.initCompatibilityChecker();
this.setupEventListeners();
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');
this.gamesConfig = this.getDefaultConfig();
},
async initContentScanner() {
try {
logSh('🔍 Initializing content scanner...', 'INFO');
this.scannedContent = await this.contentScanner.scanAllContent();
logSh(`${this.scannedContent.found.length} content modules detected automatically`, 'INFO');
} catch (error) {
logSh('Content scan error:', error, 'ERROR');
}
},
initCompatibilityChecker() {
if (window.ContentGameCompatibility) {
this.compatibilityChecker = new ContentGameCompatibility();
logSh('🎯 Content-Game compatibility checker initialized', 'INFO');
} else {
logSh('⚠️ ContentGameCompatibility not found, compatibility checks disabled', 'WARN');
}
},
getDefaultConfig() {
return {
games: {
'whack-a-mole': {
enabled: true,
name: 'Whack-a-Mole',
icon: '🔨',
description: 'Hit the right answers!'
},
'whack-a-mole-hard': {
enabled: true,
name: 'Whack-a-Mole Hard',
icon: '💥',
description: '3 moles at once, 5x3 grid, harder!'
},
'memory-match': {
enabled: true,
name: 'Memory Match',
icon: '🧠',
description: 'Find matching English-French pairs!'
},
'quiz-game': {
enabled: true,
name: 'Quiz Game',
icon: '❓',
description: 'Answer vocabulary questions!'
},
'fill-the-blank': {
enabled: true,
name: 'Fill the Blank',
icon: '📝',
description: 'Complete sentences by filling in the blanks!'
},
'adventure-reader': {
enabled: true,
name: 'Adventure Reader',
icon: '⚔️',
description: 'Zelda-style adventure with vocabulary!'
},
'story-reader': {
enabled: true,
name: 'Story Reader',
icon: '📚',
description: 'Read long stories with sentence chunking and word-by-word translation'
},
'word-storm': {
enabled: true,
name: 'Word Storm',
icon: '🌪️',
description: 'Catch falling words before they hit the ground!'
},
'word-discovery': {
enabled: true,
name: 'Word Discovery',
icon: '🔍',
description: 'Learn new words with images and interactive practice!'
},
'letter-discovery': {
enabled: true,
name: 'Letter Discovery',
icon: '🔤',
description: 'Discover letters first, then explore words that start with each letter!'
},
'river-run': {
enabled: true,
name: 'River Run',
icon: '🌊',
description: 'Navigate the river and catch your target words while avoiding obstacles!'
},
'grammar-discovery': {
enabled: true,
name: 'Grammar Discovery',
icon: '📚',
description: 'Discover and learn grammar patterns through interactive examples!'
}
},
content: {
'sbs-level-8': {
enabled: true,
name: 'SBS Level 8',
icon: '📚',
description: 'SBS textbook vocabulary'
},
'animals': {
enabled: false,
name: 'Animals',
icon: '🐱',
description: 'Animal vocabulary'
},
'colors': {
enabled: false,
name: 'Colors & Numbers',
icon: '🌈',
description: 'Colors and numbers'
},
'story-prototype-1000words': {
enabled: true,
name: 'The Magical Library (1000 words)',
icon: '✨',
description: '1000-word adventure story with sentence-by-sentence chunking'
},
'story-test': {
enabled: true,
name: 'Story Test - Short Adventure',
icon: '📖',
description: 'Simple test story for Story Reader (8 sentences)'
},
'story-complete-1000words': {
enabled: true,
name: 'The Secret Garden Adventure',
icon: '🌸',
description: 'Complete 1000-word story with full pronunciation and translation'
},
'chinese-long-story': {
enabled: true,
name: 'The Dragon\'s Pearl (Chinese)',
icon: '🐉',
description: 'Chinese story with English translation and pinyin pronunciation'
},
'chinese-beginner-story': {
enabled: true,
name: 'Le Jardin Magique (Chinese → French)',
icon: '🌸',
description: 'Simple Chinese story for beginners with French translation'
},
'story-prototype-optimized': {
enabled: true,
name: 'The Magical Library (Optimized)',
icon: '⚡',
description: 'Story with smart vocabulary matching and game compatibility'
}
}
};
},
setupEventListeners() {
// URL navigation
window.addEventListener('popstate', () => {
this.handleInitialRoute();
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.goBack();
}
});
// Scroll pour masquer/afficher la breadcrumb
this.setupScrollBehavior();
},
async handleInitialRoute() {
const params = Utils.getUrlParams();
if (params.page === 'play' && params.game && params.content) {
this.showGamePage(params.game, params.content);
} else if (params.page === 'games' && params.content) {
await this.showGamesPage(params.content);
} else if (params.page === 'levels') {
this.showLevelsPage();
} else {
this.showHomePage();
}
},
setupScrollBehavior() {
let lastScrollY = window.scrollY;
let breadcrumb = null;
const handleScroll = () => {
if (!breadcrumb) {
breadcrumb = document.querySelector('.breadcrumb');
if (!breadcrumb) return;
}
const currentScrollY = window.scrollY;
// Si on scroll vers le bas et qu'on a scrollé plus de 50px
if (currentScrollY > lastScrollY && currentScrollY > 50) {
breadcrumb.classList.add('hidden');
breadcrumb.classList.remove('visible');
}
// Si on scroll vers le haut ou qu'on est près du top
else if (currentScrollY < lastScrollY || currentScrollY <= 50) {
breadcrumb.classList.remove('hidden');
breadcrumb.classList.add('visible');
}
lastScrollY = currentScrollY;
};
// Throttle scroll event pour les performances
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
handleScroll();
ticking = false;
});
ticking = true;
}
});
},
// Navigate to a page
navigateTo(page, game = null, content = null) {
logSh(`🧭 Navigating to: ${page} ${game ? `(game: ${game})` : ''} ${content ? `(content: ${content})` : ''}`, 'INFO');
const params = { page };
if (game) params.game = game;
if (content) params.content = content;
Utils.setUrlParams(params);
// Update history
if (this.currentPage !== page) {
this.navigationHistory.push(page);
}
this.currentPage = page;
// Display appropriate page
switch(page) {
case 'games':
if (!content) {
logSh(`⚠️ Pas de contenu spécifié pour la page games, retour aux levels`, 'WARN');
this.showLevelsPage();
} else {
// Utiliser l'async pour les jeux pour le système de compatibilité
this.showGamesPage(content).catch(error => {
logSh(`❌ Erreur lors de l'affichage des jeux: ${error.message}`, 'ERROR');
// Retour aux levels en cas d'erreur
this.showLevelsPage();
});
}
break;
case 'levels':
this.showLevelsPage();
break;
case 'play':
this.showGamePage(game, content);
break;
case 'settings':
this.showSettingsPage();
break;
default:
this.showHomePage();
}
this.updateBreadcrumb();
},
// Go back
goBack() {
logSh(`⬅️ Going back from: ${this.currentPage}`, 'INFO');
if (this.navigationHistory.length > 1) {
this.navigationHistory.pop(); // Remove current page
const previousPage = this.navigationHistory[this.navigationHistory.length - 1];
logSh(`📍 Going back to: ${previousPage}`, 'DEBUG');
const params = Utils.getUrlParams();
if (previousPage === 'levels') {
this.navigateTo('levels', params.game);
} else if (previousPage === 'games') {
// Récupérer le content depuis les paramètres URL ou retourner aux levels
const urlContent = params.content;
if (urlContent) {
this.navigateTo('games', null, urlContent);
} else {
logSh(`⚠️ Pas de content pour revenir aux jeux, retour aux levels`, 'WARN');
this.navigateTo('levels');
}
} else {
this.navigateTo('home');
}
}
},
// Display home page
showHomePage() {
logSh('🏠 Displaying home page', 'INFO');
this.hideAllPages();
document.getElementById('home-page').classList.add('active');
this.currentPage = 'home';
this.updateBreadcrumb();
},
showSettingsPage() {
logSh('⚙️ Displaying settings page', 'INFO');
this.hideAllPages();
document.getElementById('settings-page').classList.add('active');
this.currentPage = 'settings';
this.updateBreadcrumb();
// Initialize settings if SettingsManager is available
if (window.SettingsManager) {
// Ensure SettingsManager is initialized for this page
setTimeout(() => {
window.SettingsManager.init();
}, 100);
}
},
// Display games selection page
async showGamesPage(contentType) {
logSh(`🎮 Displaying games selection page for content: ${contentType}`, 'INFO');
this.hideAllPages();
document.getElementById('games-page').classList.add('active');
this.currentPage = 'games';
this.selectedContent = contentType;
// Update description first
const contentInfo = this.gamesConfig?.content[contentType];
if (contentInfo) {
document.getElementById('game-description').textContent =
`Select a game to play with "${contentInfo.name}"`;
} else {
document.getElementById('game-description').textContent =
`Select a game to play with this content`;
}
this.updateBreadcrumb();
// Render games grid (async)
await this.renderGamesGrid(contentType);
},
// Display levels selection page (now the first step)
showLevelsPage() {
logSh('📚 Displaying levels selection page', 'INFO');
this.hideAllPages();
document.getElementById('levels-page').classList.add('active');
this.renderLevelsGrid();
this.currentPage = 'levels';
this.updateBreadcrumb();
},
// Display game page
async showGamePage(gameType, contentType) {
this.hideAllPages();
document.getElementById('game-page').classList.add('active');
this.currentPage = 'play';
Utils.showLoading();
try {
await GameLoader.loadGame(gameType, contentType);
this.updateBreadcrumb();
} catch (error) {
logSh('Game loading error:', error, 'ERROR');
Utils.showToast('Error loading game', 'error');
this.goBack();
} finally {
Utils.hideLoading();
}
},
// Hide all pages
hideAllPages() {
document.querySelectorAll('.page').forEach(page => {
page.classList.remove('active');
});
},
// Render games grid
async renderGamesGrid(contentType) {
logSh(`🎲 Generating games grid for content: ${contentType}...`, 'DEBUG');
const grid = document.getElementById('games-grid');
grid.innerHTML = '<div class="loading-content">🔍 Analyzing game compatibility...</div>';
if (!this.gamesConfig) {
logSh('❌ No games configuration available', 'ERROR');
return;
}
// DEBUG: Log détaillé du contenu
logSh(`🔍 DEBUG: Recherche contenu avec ID: "${contentType}"`, 'DEBUG');
logSh(`🔍 DEBUG: Contenu scanné disponible: ${this.scannedContent?.found?.length || 0} items`, 'DEBUG');
if (this.scannedContent?.found) {
this.scannedContent.found.forEach(content => {
logSh(`🔍 DEBUG: Contenu trouvé - ID: "${content.id}", Name: "${content.name}"`, 'DEBUG');
});
}
// Récupérer les informations du contenu
let contentInfo = null;
if (this.scannedContent && this.scannedContent.found) {
// Recherche directe par ID
contentInfo = this.scannedContent.found.find(content => content.id === contentType);
// Si pas trouvé, essayer de chercher par nom de fichier ou nom de module
if (!contentInfo) {
contentInfo = this.scannedContent.found.find(content =>
content.filename.replace('.js', '') === contentType ||
content.filename.replace('.js', '').toLowerCase() === contentType ||
content.id.toLowerCase() === contentType.toLowerCase()
);
}
}
if (!contentInfo) {
logSh(`⚠️ Content info not found for ${contentType}`, 'WARN');
logSh(`🔍 DEBUG: IDs disponibles: ${this.scannedContent?.found?.map(c => c.id).join(', ')}`, 'DEBUG');
logSh(`🔍 DEBUG: Noms de fichiers: ${this.scannedContent?.found?.map(c => c.filename).join(', ')}`, 'DEBUG');
} else {
logSh(`✅ DEBUG: Contenu trouvé: ${contentInfo.name} (ID: ${contentInfo.id})`, 'DEBUG');
}
const enabledGames = Object.entries(this.gamesConfig.games).filter(([key, game]) => game.enabled);
logSh(`🎯 ${enabledGames.length} enabled games found`, 'INFO');
// Clear loading
grid.innerHTML = '';
// Analyser la compatibilité et séparer les jeux
const compatibleGames = [];
const incompatibleGames = [];
// Process games sequentially to avoid overwhelming the system
for (const [key, game] of enabledGames) {
let compatibility = null;
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 };
if (!compatibility || compatibility.compatible) {
compatibleGames.push(gameData);
} else {
incompatibleGames.push(gameData);
}
}
// Afficher d'abord les jeux compatibles
if (compatibleGames.length > 0) {
const compatibleSection = document.createElement('div');
compatibleSection.className = 'compatible-games-section';
if (incompatibleGames.length > 0) {
compatibleSection.innerHTML = '<h3 class="section-title">🎯 Jeux recommandés</h3>';
}
compatibleGames
.sort((a, b) => (b.compatibility?.score || 50) - (a.compatibility?.score || 50))
.forEach(({ key, game, compatibility }) => {
const card = this.createGameCard(key, game, contentType, compatibility);
compatibleSection.appendChild(card);
});
grid.appendChild(compatibleSection);
}
// Puis afficher les jeux incompatibles (avec avertissement)
if (incompatibleGames.length > 0) {
const incompatibleSection = document.createElement('div');
incompatibleSection.className = 'incompatible-games-section';
incompatibleSection.innerHTML = '<h3 class="section-title">⚠️ Jeux avec limitations</h3>';
incompatibleGames.forEach(({ key, game, compatibility }) => {
const card = this.createGameCard(key, game, contentType, compatibility);
incompatibleSection.appendChild(card);
});
grid.appendChild(incompatibleSection);
}
// Message si aucun contenu
if (compatibleGames.length === 0 && incompatibleGames.length === 0) {
grid.innerHTML = '<div class="no-games">Aucun jeu disponible</div>';
}
},
// Create a game card
createGameCard(gameKey, gameInfo, contentType, compatibility = null) {
const card = document.createElement('div');
// Classes CSS selon la compatibilité
let cardClass = 'game-card';
let compatibilityBadge = '';
let compatibilityInfo = '';
let clickable = true;
if (compatibility) {
if (compatibility.compatible) {
cardClass += ' compatible';
if (compatibility.score >= 80) {
compatibilityBadge = '<div class="compatibility-badge excellent">🎯 Excellent</div>';
} else if (compatibility.score >= 60) {
compatibilityBadge = '<div class="compatibility-badge good">✅ Recommandé</div>';
} else {
compatibilityBadge = '<div class="compatibility-badge compatible">👍 Compatible</div>';
}
} else {
cardClass += ' incompatible';
compatibilityBadge = '<div class="compatibility-badge incompatible">⚠️ Limité</div>';
clickable = false; // Désactiver le clic pour les jeux incompatibles
}
// Informations détaillées de compatibilité
compatibilityInfo = `
<div class="compatibility-info">
<div class="compatibility-score">Score: ${compatibility.score}%</div>
<div class="compatibility-reason">${compatibility.reason}</div>
</div>
`;
} else {
// Pas d'analyse de compatibilité
compatibilityBadge = '<div class="compatibility-badge unknown">❓ Non analysé</div>';
}
card.className = cardClass;
card.innerHTML = `
<div class="card-header">
<div class="icon">${gameInfo.icon}</div>
${compatibilityBadge}
</div>
<div class="title">${gameInfo.name}</div>
<div class="description">${gameInfo.description}</div>
${compatibilityInfo}
${!clickable ? '<div class="incompatible-warning">Ce jeu nécessite plus de contenu pour fonctionner correctement</div>' : ''}
`;
if (clickable) {
card.addEventListener('click', () => {
logSh(`🎮 Clicked on game card: ${gameInfo.name} (${gameKey}) with content: ${contentType}`, 'INFO');
Utils.animateElement(card, 'pulse');
this.navigateTo('play', gameKey, contentType);
});
} else {
// Pour les jeux incompatibles, montrer les suggestions d'amélioration
card.addEventListener('click', () => {
this.showCompatibilityHelp(gameKey, gameInfo, compatibility);
});
}
return card;
},
// Afficher l'aide de compatibilité pour les jeux incompatibles
showCompatibilityHelp(gameKey, gameInfo, compatibility) {
if (!this.compatibilityChecker || !compatibility) return;
const modal = document.createElement('div');
modal.className = 'compatibility-help-modal';
// Récupérer les suggestions d'amélioration
const contentInfo = this.scannedContent?.found?.find(content => content.id === this.selectedContent);
const suggestions = contentInfo ?
this.compatibilityChecker.getImprovementSuggestions(contentInfo, gameKey) :
['Enrichir le contenu du module'];
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>🎮 ${gameInfo.name} - Améliorer la compatibilité</h3>
<button class="close-btn">×</button>
</div>
<div class="modal-body">
<p><strong>Raison:</strong> ${compatibility.reason}</p>
<p><strong>Score actuel:</strong> ${compatibility.score}% (minimum requis: ${compatibility.details?.minimumScore || 'N/A'}%)</p>
<h4>💡 Suggestions d'amélioration:</h4>
<ul>
${suggestions.map(suggestion => `<li>${suggestion}</li>`).join('')}
</ul>
<div class="modal-actions">
<button class="try-anyway-btn">Essayer quand même</button>
<button class="close-modal-btn">Fermer</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Event listeners
modal.querySelector('.close-btn').addEventListener('click', () => modal.remove());
modal.querySelector('.close-modal-btn').addEventListener('click', () => modal.remove());
modal.querySelector('.try-anyway-btn').addEventListener('click', () => {
modal.remove();
logSh(`🎮 Forcing game launch: ${gameInfo.name} despite compatibility issues`, 'WARN');
this.navigateTo('play', gameKey, this.selectedContent);
});
// Fermer avec ESC
const handleEscape = (e) => {
if (e.key === 'Escape') {
modal.remove();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
},
// Render levels grid
async renderLevelsGrid() {
const grid = document.getElementById('levels-grid');
grid.innerHTML = '<div class="loading-content">🔍 Searching for available content...</div>';
try {
// Get all available content automatically
const availableContent = await this.contentScanner.getAvailableContent();
if (availableContent.length === 0) {
grid.innerHTML = '<div class="no-content">No content found</div>';
return;
}
// Clear loading
grid.innerHTML = '';
logSh(`📋 Displaying ${availableContent.length} content modules`, 'INFO');
// Create cards for each found content
availableContent.forEach(content => {
const card = this.createLevelCard(content.id, content);
grid.appendChild(card);
});
} catch (error) {
logSh('Levels render error:', error, 'ERROR');
grid.innerHTML = '<div class="error-content">❌ Error loading content</div>';
}
},
// Convertir contentType vers nom de module JavaScript
getModuleName(contentType) {
const mapping = {
'sbs-level-7-8-new': 'SBSLevel78New',
'basic-chinese': 'BasicChinese',
'english-class-demo': 'EnglishClassDemo',
'chinese-long-story': 'ChineseLongStory',
'chinese-beginner-story': 'ChineseBeginnerStory',
'test-minimal': 'TestMinimal',
'test-rich': 'TestRich'
};
return mapping[contentType] || this.toPascalCase(contentType);
},
// Convertir kebab-case vers PascalCase
toPascalCase(str) {
return str.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join('');
},
// Create a level card
createLevelCard(contentKey, contentInfo) {
const card = document.createElement('div');
card.className = 'level-card';
// Calculate stats to display
const stats = [];
if (contentInfo.stats) {
if (contentInfo.stats.vocabularyCount > 0) {
stats.push(`📚 ${contentInfo.stats.vocabularyCount} words`);
}
if (contentInfo.stats.sentenceCount > 0) {
stats.push(`💬 ${contentInfo.stats.sentenceCount} sentences`);
}
if (contentInfo.stats.dialogueCount > 0) {
stats.push(`🎭 ${contentInfo.stats.dialogueCount} dialogues`);
}
}
card.innerHTML = `
<div class="card-header">
<div class="icon">${contentInfo.icon}</div>
</div>
<div class="title">${contentInfo.name}</div>
<div class="description">${contentInfo.description}</div>
<div class="content-stats">
<span class="difficulty-badge difficulty-${contentInfo.difficulty}">${contentInfo.difficulty}</span>
<span class="items-count">${contentInfo.metadata.totalItems} items</span>
<span class="time-estimate">~${contentInfo.metadata.estimatedTime}min</span>
</div>
${stats.length > 0 ? `<div class="detailed-stats">${stats.join(' • ')}</div>` : ''}
`;
card.addEventListener('click', () => {
logSh(`📚 Clicked on content card: ${contentInfo.name} (${contentKey})`, 'INFO');
Utils.animateElement(card, 'pulse');
this.navigateTo('games', null, contentKey);
});
return card;
},
// Update breadcrumb
updateBreadcrumb() {
logSh(`🍞 Updating breadcrumb for page: ${this.currentPage}`, 'DEBUG');
const breadcrumb = document.getElementById('breadcrumb');
breadcrumb.innerHTML = '';
const params = Utils.getUrlParams();
// Add back button if not on home
if (this.currentPage !== 'home' && this.navigationHistory.length > 1) {
const backButton = document.createElement('button');
backButton.className = 'back-button';
backButton.innerHTML = '← Back';
backButton.onclick = () => this.goBack();
breadcrumb.appendChild(backButton);
}
// Home
const homeItem = this.createBreadcrumbItem('🏠 Home', 'home',
this.currentPage === 'home');
breadcrumb.appendChild(homeItem);
// Levels
if (['levels', 'games', 'play'].includes(this.currentPage)) {
const levelsItem = this.createBreadcrumbItem('📚 Levels', 'levels',
this.currentPage === 'levels');
breadcrumb.appendChild(levelsItem);
}
// Games (pour un contenu spécifique)
if (['games', 'play'].includes(this.currentPage) && params.content) {
// Récupérer le nom du contenu depuis les résultats du scan
let contentName = params.content;
if (this.scannedContent?.found) {
const foundContent = this.scannedContent.found.find(c => c.id === params.content);
if (foundContent) {
contentName = foundContent.name;
}
}
const gamesItem = this.createBreadcrumbItem(`🎮 ${contentName}`, 'games',
this.currentPage === 'games', params.content);
breadcrumb.appendChild(gamesItem);
}
// Current game
if (this.currentPage === 'play' && params.content) {
const contentInfo = this.gamesConfig?.content[params.content];
const playText = contentInfo ? `🎯 ${contentInfo.name}` : 'Game';
const playItem = this.createBreadcrumbItem(playText, 'play', true);
breadcrumb.appendChild(playItem);
}
},
// Create a breadcrumb element
createBreadcrumbItem(text, page, isActive, content = null) {
const item = document.createElement('button');
item.className = `breadcrumb-item ${isActive ? 'active' : ''}`;
item.textContent = text;
item.dataset.page = page;
if (!isActive) {
item.addEventListener('click', () => {
logSh(`🍞 Clicked on breadcrumb: ${text}${page}`, 'INFO');
if (page === 'home') {
this.navigateTo('home');
} else if (page === 'games' && content) {
this.navigateTo('games', null, content);
} else if (page === 'levels') {
this.navigateTo('levels');
}
});
}
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);
});
}
};
// Global functions for HTML
window.navigateTo = (page, game, content) => AppNavigation.navigateTo(page, game, content);
window.goBack = () => AppNavigation.goBack();
// Export
window.AppNavigation = AppNavigation;