// === STORY BUILDER GAME - STORY CONSTRUCTOR === class StoryBuilderGame { constructor(options) { this.container = options.container; this.content = options.content; this.contentEngine = options.contentEngine; this.onScoreUpdate = options.onScoreUpdate || (() => {}); this.onGameEnd = options.onGameEnd || (() => {}); // Game state this.score = 0; this.currentStory = []; this.availableElements = []; this.storyTarget = null; this.gameMode = 'vocabulary'; // 'vocabulary', 'sequence', 'dialogue', 'scenario' // Extract vocabulary using ultra-modular format this.vocabulary = this.extractVocabulary(this.content); this.wordsByType = this.groupVocabularyByType(this.vocabulary); // Configuration this.maxElements = 6; this.timeLimit = 180; // 3 minutes this.timeLeft = this.timeLimit; this.isRunning = false; // Timers this.gameTimer = null; this.init(); } init() { // Check if we have enough vocabulary if (!this.vocabulary || this.vocabulary.length < 6) { logSh('Not enough vocabulary for Story Builder', 'ERROR'); this.showInitError(); return; } this.createGameBoard(); this.setupEventListeners(); this.loadStoryContent(); } showInitError() { this.container.innerHTML = `

❌ Error loading

This content doesn't have enough vocabulary for Story Builder.

The game needs at least 6 vocabulary words with types (noun, verb, adjective, etc.).

`; } createGameBoard() { this.container.innerHTML = `

Objective:

Choose a mode and let's start!

${this.timeLeft} Time
0/${this.maxElements} Progress
Drag elements here to build your story
Select a mode to start building stories!
`; } setupEventListeners() { // Mode selection document.querySelectorAll('.mode-btn').forEach(btn => { btn.addEventListener('click', (e) => { if (this.isRunning) return; document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); this.gameMode = btn.dataset.mode; this.loadStoryContent(); }); }); // Game controls document.getElementById('start-btn').addEventListener('click', () => this.start()); document.getElementById('check-btn').addEventListener('click', () => this.checkStory()); document.getElementById('hint-btn').addEventListener('click', () => this.showHint()); document.getElementById('restart-btn').addEventListener('click', () => this.restart()); // Drag and Drop setup this.setupDragAndDrop(); } loadStoryContent() { logSh('🎮 Loading story content for mode:', this.gameMode, 'INFO'); switch (this.gameMode) { case 'vocabulary': this.setupVocabularyMode(); break; case 'sequence': this.setupSequenceMode(); break; case 'dialogue': this.setupDialogueMode(); break; case 'scenario': this.setupScenarioMode(); break; default: this.setupVocabularyMode(); } } extractVocabulary(content) { let vocabulary = []; logSh('📝 Extracting vocabulary from:', content?.name || 'content', 'INFO'); // Use raw module content if available if (content.rawContent) { logSh('📦 Using raw module content', 'INFO'); return this.extractVocabularyFromRaw(content.rawContent); } // Ultra-modular format (vocabulary object) - ONLY format supported if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { logSh('✨ Ultra-modular format detected (vocabulary object)', 'INFO'); vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { // Support ultra-modular format ONLY if (typeof data === 'object' && data.user_language) { return { original: word, // Clé = original_language translation: data.user_language.split(';')[0], // First translation fullTranslation: data.user_language, // Complete translation type: data.type || 'general', audio: data.audio, image: data.image, examples: data.examples, pronunciation: data.pronunciation, category: data.type || 'general' }; } // No legacy fallback - ultra-modular only return null; }).filter(Boolean); } // No other formats supported - ultra-modular only return this.finalizeVocabulary(vocabulary); } extractVocabularyFromRaw(rawContent) { logSh('🔧 Extracting from raw content:', rawContent.name || 'Module', 'INFO'); 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, // Clé = original_language translation: data.user_language.split(';')[0], // First translation fullTranslation: data.user_language, // Complete translation type: data.type || 'general', audio: data.audio, image: data.image, examples: data.examples, pronunciation: data.pronunciation, category: data.type || 'general' }; } // No legacy fallback - ultra-modular only return null; }).filter(Boolean); } // No other formats supported - ultra-modular only return this.finalizeVocabulary(vocabulary); } finalizeVocabulary(vocabulary) { // Filter out invalid entries vocabulary = vocabulary.filter(item => item && typeof item.original === 'string' && typeof item.translation === 'string' && item.original.trim() !== '' && item.translation.trim() !== '' ); logSh(`📊 Finalized ${vocabulary.length} vocabulary items`, 'INFO'); return vocabulary; } groupVocabularyByType(vocabulary) { const grouped = {}; vocabulary.forEach(word => { const type = word.type || 'general'; if (!grouped[type]) { grouped[type] = []; } grouped[type].push(word); }); logSh('📊 Words grouped by type:', Object.keys(grouped).map(type => `${type}: ${grouped[type].length}`).join(', '), 'INFO'); return grouped; } setupVocabularyMode() { if (Object.keys(this.wordsByType).length === 0) { this.setupFallbackContent(); return; } // Create a story template using different word types this.storyTarget = this.createStoryTemplate(); this.availableElements = this.selectWordsForStory(); document.getElementById('objective-text').textContent = 'Build a coherent story using these words! Use different types: nouns, verbs, adjectives...'; } createStoryTemplate() { const types = Object.keys(this.wordsByType); // Common story templates based on available word types const templates = [ { pattern: ['noun', 'verb', 'adjective', 'noun'], name: 'Simple Story' }, { pattern: ['adjective', 'noun', 'verb', 'noun'], name: 'Descriptive Story' }, { pattern: ['noun', 'verb', 'adjective', 'noun', 'verb'], name: 'Action Story' }, { pattern: ['article', 'adjective', 'noun', 'verb', 'adverb'], name: 'Rich Story' } ]; // Find the best template based on available word types const availableTemplate = templates.find(template => template.pattern.every(type => types.includes(type) && this.wordsByType[type].length > 0 ) ); if (availableTemplate) { return { template: availableTemplate, requiredTypes: availableTemplate.pattern }; } // Fallback: use available types return { template: { pattern: types.slice(0, 4), name: 'Custom Story' }, requiredTypes: types.slice(0, 4) }; } selectWordsForStory() { const words = []; if (this.storyTarget && this.storyTarget.requiredTypes) { // Select words for each required type this.storyTarget.requiredTypes.forEach(type => { if (this.wordsByType[type] && this.wordsByType[type].length > 0) { // Add 2-3 words of each type for choice const typeWords = this.shuffleArray([...this.wordsByType[type]]).slice(0, 3); words.push(...typeWords); } }); } // Add some random extra words for distraction const allTypes = Object.keys(this.wordsByType); allTypes.forEach(type => { if (this.wordsByType[type] && this.wordsByType[type].length > 0) { const extraWords = this.shuffleArray([...this.wordsByType[type]]).slice(0, 1); words.push(...extraWords); } }); // Remove duplicates and shuffle const uniqueWords = words.filter((word, index, self) => self.findIndex(w => w.original === word.original) === index ); return this.shuffleArray(uniqueWords).slice(0, this.maxElements); } setupSequenceMode() { // Use vocabulary to create a logical sequence const actionWords = this.wordsByType.verb || []; const objectWords = this.wordsByType.noun || []; if (actionWords.length >= 2 && objectWords.length >= 2) { this.storyTarget = { type: 'sequence', steps: [ { order: 1, text: `First: ${actionWords[0].original}`, word: actionWords[0] }, { order: 2, text: `Then: ${actionWords[1].original}`, word: actionWords[1] }, { order: 3, text: `With: ${objectWords[0].original}`, word: objectWords[0] }, { order: 4, text: `Finally: ${objectWords[1].original}`, word: objectWords[1] } ] }; this.availableElements = this.shuffleArray([...this.storyTarget.steps]); document.getElementById('objective-text').textContent = 'Put these actions in logical order!'; } else { this.setupVocabularyMode(); // Fallback } } setupDialogueMode() { // Create a simple dialogue using available vocabulary const greetings = this.wordsByType.greeting || []; const nouns = this.wordsByType.noun || []; const verbs = this.wordsByType.verb || []; if (greetings.length >= 1 && (nouns.length >= 2 || verbs.length >= 2)) { const dialogue = [ { speaker: 'A', text: greetings[0].original, word: greetings[0] }, { speaker: 'B', text: greetings[0].translation, word: greetings[0] } ]; if (verbs.length >= 1) { dialogue.push({ speaker: 'A', text: verbs[0].original, word: verbs[0] }); } if (nouns.length >= 1) { dialogue.push({ speaker: 'B', text: nouns[0].original, word: nouns[0] }); } this.storyTarget = { type: 'dialogue', conversation: dialogue }; this.availableElements = this.shuffleArray([...dialogue]); document.getElementById('objective-text').textContent = 'Reconstruct this dialogue in the right order!'; } else { this.setupVocabularyMode(); // Fallback } } setupScenarioMode() { // Create a scenario using mixed vocabulary types const allWords = Object.values(this.wordsByType).flat(); if (allWords.length >= 4) { const scenario = { context: 'Daily Life', elements: this.shuffleArray(allWords).slice(0, 6) }; this.storyTarget = { type: 'scenario', scenario }; this.availableElements = [...scenario.elements]; document.getElementById('objective-text').textContent = `Create a story about: "${scenario.context}" using these words!`; } else { this.setupVocabularyMode(); // Fallback } } setupFallbackContent() { // Use any available vocabulary if (this.vocabulary.length >= 4) { this.availableElements = this.shuffleArray([...this.vocabulary]).slice(0, 6); this.gameMode = 'vocabulary'; document.getElementById('objective-text').textContent = 'Build a story with these words!'; } else { document.getElementById('objective-text').textContent = 'Not enough vocabulary available. Please select different content.'; } } start() { if (this.isRunning || this.availableElements.length === 0) return; this.isRunning = true; this.score = 0; this.currentStory = []; this.timeLeft = this.timeLimit; this.renderElements(); this.startTimer(); this.updateUI(); document.getElementById('start-btn').disabled = true; document.getElementById('check-btn').disabled = false; document.getElementById('hint-btn').disabled = false; this.showFeedback('Drag the elements in order to build your story!', 'info'); } renderElements() { const elementsBank = document.getElementById('elements-bank'); elementsBank.innerHTML = '

Available elements:

'; this.availableElements.forEach((element, index) => { const elementDiv = this.createElement(element, index); elementsBank.appendChild(elementDiv); }); } createElement(element, index) { const div = document.createElement('div'); div.className = 'story-element'; div.draggable = true; div.dataset.index = index; // Ultra-modular format display if (element.original && element.translation) { // Vocabulary word with type div.innerHTML = `
${element.original}
${element.translation}
${element.type ? `
${element.type}
` : ''}
`; } else if (element.text || element.original) { // Dialogue or sequence element div.innerHTML = `
${element.text || element.original}
${element.translation ? `
${element.translation}
` : ''} ${element.speaker ? `
${element.speaker}:
` : ''}
`; } else if (element.word) { // Element containing a word object div.innerHTML = `
${element.word.original}
${element.word.translation}
${element.word.type ? `
${element.word.type}
` : ''}
`; } else if (typeof element === 'string') { // Simple text div.innerHTML = `
${element}
`; } // Add type-based styling if (element.type) { div.classList.add(`type-${element.type}`); } return div; } setupDragAndDrop() { let draggedElement = null; document.addEventListener('dragstart', (e) => { if (e.target.classList.contains('story-element')) { draggedElement = e.target; e.target.style.opacity = '0.5'; } }); document.addEventListener('dragend', (e) => { if (e.target.classList.contains('story-element')) { e.target.style.opacity = '1'; draggedElement = null; } }); const dropZone = document.getElementById('drop-zone'); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); if (draggedElement && this.isRunning) { this.addToStory(draggedElement); } }); } addToStory(elementDiv) { const index = parseInt(elementDiv.dataset.index); const element = this.availableElements[index]; // Add to the story this.currentStory.push({ element, originalIndex: index }); // Create element in construction zone const storyElement = elementDiv.cloneNode(true); storyElement.classList.add('in-story'); storyElement.draggable = false; // Ajouter bouton de suppression const removeBtn = document.createElement('button'); removeBtn.className = 'remove-element'; removeBtn.innerHTML = '×'; removeBtn.onclick = () => this.removeFromStory(storyElement, element); storyElement.appendChild(removeBtn); document.getElementById('drop-zone').appendChild(storyElement); // Masquer l'élément original elementDiv.style.display = 'none'; this.updateProgress(); } removeFromStory(storyElement, element) { // Remove from story this.currentStory = this.currentStory.filter(item => item.element !== element); // Supprimer visuellement storyElement.remove(); // Réafficher l'élément original const originalElement = document.querySelector(`[data-index="${this.availableElements.indexOf(element)}"]`); if (originalElement) { originalElement.style.display = 'block'; } this.updateProgress(); } checkStory() { if (this.currentStory.length === 0) { this.showFeedback('Add at least one element to your story!', 'error'); return; } const isCorrect = this.validateStory(); if (isCorrect) { this.score += this.currentStory.length * 10; this.showFeedback('Bravo! Perfect story! 🎉', 'success'); this.onScoreUpdate(this.score); setTimeout(() => { this.nextChallenge(); }, 2000); } else { this.score = Math.max(0, this.score - 5); this.showFeedback('Almost! Check the order of your story 🤔', 'warning'); this.onScoreUpdate(this.score); } } validateStory() { switch (this.gameMode) { case 'vocabulary': return this.validateVocabularyStory(); case 'sequence': return this.validateSequence(); case 'dialogue': return this.validateDialogue(); case 'scenario': return this.validateScenario(); default: return true; // Free mode } } validateVocabularyStory() { if (this.currentStory.length < 3) return false; // Check for variety in word types const typesUsed = new Set(); this.currentStory.forEach(item => { const element = item.element; if (element.type) { typesUsed.add(element.type); } }); // Require at least 2 different word types for a good story return typesUsed.size >= 2; } validateSequence() { if (!this.storyTarget?.steps) return true; const expectedOrder = this.storyTarget.steps.sort((a, b) => a.order - b.order); if (this.currentStory.length !== expectedOrder.length) return false; return this.currentStory.every((item, index) => { const expected = expectedOrder[index]; return item.element.order === expected.order; }); } validateDialogue() { // Flexible dialogue validation (logical order of replies) return this.currentStory.length >= 2; } validateScenario() { // Flexible scenario validation (contextual coherence) return this.currentStory.length >= 3; } showHint() { switch (this.gameMode) { case 'vocabulary': const typesAvailable = Object.keys(this.wordsByType); this.showFeedback(`Tip: Try using different word types: ${typesAvailable.join(', ')}`, 'info'); break; case 'sequence': if (this.storyTarget?.steps) { const nextStep = this.storyTarget.steps.find(step => !this.currentStory.some(item => item.element.order === step.order) ); if (nextStep) { this.showFeedback(`Next step: "${nextStep.text}"`, 'info'); } } break; case 'dialogue': this.showFeedback('Think about the natural order of a conversation!', 'info'); break; case 'scenario': this.showFeedback('Create a coherent story in this context!', 'info'); break; default: this.showFeedback('Tip: Think about the logical order of events!', 'info'); } } nextChallenge() { // Load a new challenge this.loadStoryContent(); this.currentStory = []; document.getElementById('drop-zone').innerHTML = '
Drag elements here to build your story
'; this.renderElements(); this.updateProgress(); } startTimer() { this.gameTimer = setInterval(() => { this.timeLeft--; this.updateUI(); if (this.timeLeft <= 0) { this.endGame(); } }, 1000); } endGame() { this.isRunning = false; if (this.gameTimer) { clearInterval(this.gameTimer); this.gameTimer = null; } document.getElementById('start-btn').disabled = false; document.getElementById('check-btn').disabled = true; document.getElementById('hint-btn').disabled = true; this.onGameEnd(this.score); } restart() { this.endGame(); this.score = 0; this.currentStory = []; this.timeLeft = this.timeLimit; this.onScoreUpdate(0); document.getElementById('drop-zone').innerHTML = '
Drag elements here to build your story
'; this.loadStoryContent(); this.updateUI(); } updateProgress() { document.getElementById('story-progress').textContent = `${this.currentStory.length}/${this.maxElements}`; } updateUI() { document.getElementById('time-left').textContent = this.timeLeft; } showFeedback(message, type = 'info') { const feedbackArea = document.getElementById('feedback-area'); feedbackArea.innerHTML = `
${message}
`; } shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } destroy() { this.endGame(); this.container.innerHTML = ''; } } // CSS pour Story Builder const storyBuilderStyles = ` `; // Ajouter les styles document.head.insertAdjacentHTML('beforeend', storyBuilderStyles); // Enregistrement du module window.GameModules = window.GameModules || {}; window.GameModules.StoryBuilder = StoryBuilderGame;