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

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

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

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

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

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