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>
1009 lines
33 KiB
JavaScript
1009 lines
33 KiB
JavaScript
// === 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 = `
|
||
<div class="game-error">
|
||
<h3>❌ Error loading</h3>
|
||
<p>This content doesn't have enough vocabulary for Story Builder.</p>
|
||
<p>The game needs at least 6 vocabulary words with types (noun, verb, adjective, etc.).</p>
|
||
<button onclick="AppNavigation.navigateTo('games')" class="back-btn">← Back</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
createGameBoard() {
|
||
this.container.innerHTML = `
|
||
<div class="story-builder-wrapper">
|
||
<!-- Mode Selection -->
|
||
<div class="mode-selector">
|
||
<button class="mode-btn active" data-mode="vocabulary">
|
||
📚 Vocabulary Story
|
||
</button>
|
||
<button class="mode-btn" data-mode="sequence">
|
||
📝 Sequence
|
||
</button>
|
||
<button class="mode-btn" data-mode="dialogue">
|
||
💬 Dialogue
|
||
</button>
|
||
<button class="mode-btn" data-mode="scenario">
|
||
🎭 Scenario
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Game Info -->
|
||
<div class="game-info">
|
||
<div class="story-objective" id="story-objective">
|
||
<h3>Objective:</h3>
|
||
<p id="objective-text">Choose a mode and let's start!</p>
|
||
</div>
|
||
<div class="game-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-value" id="time-left">${this.timeLeft}</span>
|
||
<span class="stat-label">Time</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-value" id="story-progress">0/${this.maxElements}</span>
|
||
<span class="stat-label">Progress</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Story Construction Area -->
|
||
<div class="story-construction">
|
||
<div class="story-target" id="story-target">
|
||
<!-- Story to build -->
|
||
</div>
|
||
|
||
<div class="drop-zone" id="drop-zone">
|
||
<div class="drop-hint">Drag elements here to build your story</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Available Elements -->
|
||
<div class="elements-bank" id="elements-bank">
|
||
<!-- Available elements -->
|
||
</div>
|
||
|
||
<!-- Game Controls -->
|
||
<div class="game-controls">
|
||
<button class="control-btn" id="start-btn">🎮 Start</button>
|
||
<button class="control-btn" id="check-btn" disabled>✅ Check</button>
|
||
<button class="control-btn" id="hint-btn" disabled>💡 Hint</button>
|
||
<button class="control-btn" id="restart-btn">🔄 Restart</button>
|
||
</div>
|
||
|
||
<!-- Feedback Area -->
|
||
<div class="feedback-area" id="feedback-area">
|
||
<div class="instruction">
|
||
Select a mode to start building stories!
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 = '<h4>Available elements:</h4>';
|
||
|
||
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 = `
|
||
<div class="element-content">
|
||
<div class="original">${element.original}</div>
|
||
<div class="translation">${element.translation}</div>
|
||
${element.type ? `<div class="word-type">${element.type}</div>` : ''}
|
||
</div>
|
||
`;
|
||
} else if (element.text || element.original) {
|
||
// Dialogue or sequence element
|
||
div.innerHTML = `
|
||
<div class="element-content">
|
||
<div class="original">${element.text || element.original}</div>
|
||
${element.translation ? `<div class="translation">${element.translation}</div>` : ''}
|
||
${element.speaker ? `<div class="speaker">${element.speaker}:</div>` : ''}
|
||
</div>
|
||
`;
|
||
} else if (element.word) {
|
||
// Element containing a word object
|
||
div.innerHTML = `
|
||
<div class="element-content">
|
||
<div class="original">${element.word.original}</div>
|
||
<div class="translation">${element.word.translation}</div>
|
||
${element.word.type ? `<div class="word-type">${element.word.type}</div>` : ''}
|
||
</div>
|
||
`;
|
||
} else if (typeof element === 'string') {
|
||
// Simple text
|
||
div.innerHTML = `<div class="element-content">${element}</div>`;
|
||
}
|
||
|
||
// 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 = '<div class="drop-hint">Drag elements here to build your story</div>';
|
||
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 = '<div class="drop-hint">Drag elements here to build your story</div>';
|
||
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 = `<div class="instruction ${type}">${message}</div>`;
|
||
}
|
||
|
||
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 = '';
|
||
}
|
||
|
||
// === 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
|
||
const storyBuilderStyles = `
|
||
<style>
|
||
.story-builder-wrapper {
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.story-construction {
|
||
background: #f8f9fa;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.story-target {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
border-left: 4px solid var(--primary-color);
|
||
}
|
||
|
||
.drop-zone {
|
||
min-height: 120px;
|
||
border: 3px dashed #ddd;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.drop-zone.drag-over {
|
||
border-color: var(--primary-color);
|
||
background: rgba(59, 130, 246, 0.1);
|
||
}
|
||
|
||
.drop-hint {
|
||
color: #6b7280;
|
||
font-style: italic;
|
||
}
|
||
|
||
.elements-bank {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin: 20px 0;
|
||
border: 2px solid #e5e7eb;
|
||
}
|
||
|
||
.elements-bank h4 {
|
||
margin-bottom: 15px;
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.story-element {
|
||
display: inline-block;
|
||
background: white;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin: 8px;
|
||
cursor: grab;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
min-width: 150px;
|
||
}
|
||
|
||
.story-element:hover {
|
||
border-color: var(--primary-color);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.story-element:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.story-element.in-story {
|
||
background: var(--secondary-color);
|
||
color: white;
|
||
border-color: var(--secondary-color);
|
||
cursor: default;
|
||
margin: 5px;
|
||
}
|
||
|
||
.element-content {
|
||
text-align: center;
|
||
}
|
||
|
||
.element-icon {
|
||
font-size: 1.5rem;
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.original {
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.translation {
|
||
font-size: 0.9rem;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.word-type {
|
||
font-size: 0.8rem;
|
||
color: #9ca3af;
|
||
font-style: italic;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.speaker {
|
||
font-size: 0.8rem;
|
||
color: #ef4444;
|
||
font-weight: bold;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.story-element.in-story .translation {
|
||
color: rgba(255,255,255,0.8);
|
||
}
|
||
|
||
.story-element.in-story .word-type {
|
||
color: rgba(255,255,255,0.6);
|
||
}
|
||
|
||
/* Type-based styling */
|
||
.story-element.type-noun {
|
||
border-left: 4px solid #3b82f6;
|
||
}
|
||
|
||
.story-element.type-verb {
|
||
border-left: 4px solid #10b981;
|
||
}
|
||
|
||
.story-element.type-adjective {
|
||
border-left: 4px solid #f59e0b;
|
||
}
|
||
|
||
.story-element.type-adverb {
|
||
border-left: 4px solid #8b5cf6;
|
||
}
|
||
|
||
.story-element.type-greeting {
|
||
border-left: 4px solid #ef4444;
|
||
}
|
||
|
||
.remove-element {
|
||
position: absolute;
|
||
top: -5px;
|
||
right: -5px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: var(--error-color);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.story-objective {
|
||
background: linear-gradient(135deg, #f0f9ff, #dbeafe);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
border-left: 4px solid var(--primary-color);
|
||
}
|
||
|
||
.story-objective h3 {
|
||
color: var(--primary-color);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.story-element {
|
||
min-width: 120px;
|
||
padding: 8px;
|
||
margin: 5px;
|
||
}
|
||
|
||
.drop-zone {
|
||
min-height: 100px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.elements-bank {
|
||
padding: 15px;
|
||
}
|
||
}
|
||
</style>
|
||
`;
|
||
|
||
// Ajouter les styles
|
||
document.head.insertAdjacentHTML('beforeend', storyBuilderStyles);
|
||
|
||
// Enregistrement du module
|
||
window.GameModules = window.GameModules || {};
|
||
window.GameModules.StoryBuilder = StoryBuilderGame; |