Class_generator/js/games/story-builder.js
StillHammer 30a2028da6 Remove Text Reader game and enhance Story Reader
- Enhanced Story Reader with text-to-story conversion methods
- Added support for simple texts and sentences in Story Reader
- Removed Text Reader game file (js/games/text-reader.js)
- Updated all configuration files to remove Text Reader references
- Modified game compatibility system to use Story Reader instead
- Updated test fixtures to reflect game changes
- Cleaned up debug/test HTML files

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 11:22:56 +08:00

979 lines
32 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.

// === 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 = '';
}
}
// 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;