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>
978 lines
39 KiB
JavaScript
978 lines
39 KiB
JavaScript
// === 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; |