Class_generator/js/games/story-builder.js

701 lines
22 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 - CONSTRUCTEUR D'HISTOIRES ===
class StoryBuilderGame {
constructor(options) {
this.container = options.container;
this.content = options.content;
this.contentEngine = options.contentEngine;
this.onScoreUpdate = options.onScoreUpdate || (() => {});
this.onGameEnd = options.onGameEnd || (() => {});
// État du jeu
this.score = 0;
this.currentStory = [];
this.availableElements = [];
this.storyTarget = null;
this.gameMode = 'sequence'; // 'sequence', 'dialogue', 'scenario'
// Configuration
this.maxElements = 6;
this.timeLimit = 180; // 3 minutes
this.timeLeft = this.timeLimit;
this.isRunning = false;
// Timers
this.gameTimer = null;
this.init();
}
init() {
this.createGameBoard();
this.setupEventListeners();
this.loadStoryContent();
}
createGameBoard() {
this.container.innerHTML = `
<div class="story-builder-wrapper">
<!-- Mode Selection -->
<div class="mode-selector">
<button class="mode-btn active" data-mode="sequence">
📝 Séquence
</button>
<button class="mode-btn" data-mode="dialogue">
💬 Dialogue
</button>
<button class="mode-btn" data-mode="scenario">
🎭 Scénario
</button>
</div>
<!-- Game Info -->
<div class="game-info">
<div class="story-objective" id="story-objective">
<h3>Objectif:</h3>
<p id="objective-text">Choisis un mode et commençons !</p>
</div>
<div class="game-stats">
<div class="stat-item">
<span class="stat-value" id="time-left">${this.timeLeft}</span>
<span class="stat-label">Temps</span>
</div>
<div class="stat-item">
<span class="stat-value" id="story-progress">0/${this.maxElements}</span>
<span class="stat-label">Progrès</span>
</div>
</div>
</div>
<!-- Story Construction Area -->
<div class="story-construction">
<div class="story-target" id="story-target">
<!-- Histoire à construire -->
</div>
<div class="drop-zone" id="drop-zone">
<div class="drop-hint">Glisse les éléments ici pour construire ton histoire</div>
</div>
</div>
<!-- Available Elements -->
<div class="elements-bank" id="elements-bank">
<!-- Éléments disponibles -->
</div>
<!-- Game Controls -->
<div class="game-controls">
<button class="control-btn" id="start-btn">🎮 Commencer</button>
<button class="control-btn" id="check-btn" disabled>✅ Vérifier</button>
<button class="control-btn" id="hint-btn" disabled>💡 Indice</button>
<button class="control-btn" id="restart-btn">🔄 Recommencer</button>
</div>
<!-- Feedback Area -->
<div class="feedback-area" id="feedback-area">
<div class="instruction">
Sélectionne un mode pour commencer à construire des histoires !
</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() {
if (!this.contentEngine) {
console.warn('ContentEngine non disponible, utilisation du contenu de base');
this.setupBasicContent();
return;
}
// Filtrer le contenu selon le mode
const filters = this.getModeFilters();
const filteredContent = this.contentEngine.filterContent(this.content, filters);
this.setupContentForMode(filteredContent);
}
getModeFilters() {
switch (this.gameMode) {
case 'sequence':
return { type: ['sequence', 'vocabulary'] };
case 'dialogue':
return { type: ['dialogue', 'sentence'] };
case 'scenario':
return { type: ['scenario', 'dialogue', 'sequence'] };
default:
return { type: ['vocabulary', 'sentence'] };
}
}
setupContentForMode(filteredContent) {
const contentItems = filteredContent.contentItems || [];
switch (this.gameMode) {
case 'sequence':
this.setupSequenceMode(contentItems);
break;
case 'dialogue':
this.setupDialogueMode(contentItems);
break;
case 'scenario':
this.setupScenarioMode(contentItems);
break;
}
}
setupSequenceMode(contentItems) {
const sequences = contentItems.filter(item => item.type === 'sequence');
if (sequences.length > 0) {
this.storyTarget = sequences[Math.floor(Math.random() * sequences.length)];
this.availableElements = this.shuffleArray([...this.storyTarget.content.steps]);
document.getElementById('objective-text').textContent =
`Remets en ordre l'histoire: "${this.storyTarget.content.title}"`;
} else {
this.setupBasicSequence();
}
}
setupDialogueMode(contentItems) {
const dialogues = contentItems.filter(item => item.type === 'dialogue');
if (dialogues.length > 0) {
this.storyTarget = dialogues[Math.floor(Math.random() * dialogues.length)];
this.availableElements = this.shuffleArray([...this.storyTarget.content.conversation]);
document.getElementById('objective-text').textContent =
`Reconstitue le dialogue: "${this.storyTarget.content.english}"`;
} else {
this.setupBasicDialogue();
}
}
setupScenarioMode(contentItems) {
const scenarios = contentItems.filter(item => item.type === 'scenario');
if (scenarios.length > 0) {
this.storyTarget = scenarios[Math.floor(Math.random() * scenarios.length)];
// Mélanger vocabulaire et phrases du scénario
const vocabElements = this.storyTarget.content.vocabulary || [];
const phraseElements = this.storyTarget.content.phrases || [];
this.availableElements = this.shuffleArray([...vocabElements, ...phraseElements]);
document.getElementById('objective-text').textContent =
`Crée une histoire dans le contexte: "${this.storyTarget.content.english}"`;
} else {
this.setupBasicScenario();
}
}
setupBasicContent() {
// Fallback pour l'ancien format
const vocabulary = this.content.vocabulary || [];
this.availableElements = vocabulary.slice(0, 6);
this.gameMode = 'vocabulary';
document.getElementById('objective-text').textContent =
'Construis une histoire avec ces mots !';
}
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('Glisse les éléments dans l\'ordre pour construire ton histoire !', 'info');
}
renderElements() {
const elementsBank = document.getElementById('elements-bank');
elementsBank.innerHTML = '<h4>Éléments disponibles:</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;
// Adapter l'affichage selon le type d'élément
if (element.english && element.french) {
// Vocabulaire ou phrase
div.innerHTML = `
<div class="element-content">
<div class="english">${element.english}</div>
<div class="french">${element.french}</div>
</div>
`;
} else if (element.text || element.english) {
// Dialogue ou séquence
div.innerHTML = `
<div class="element-content">
<div class="english">${element.text || element.english}</div>
${element.french ? `<div class="french">${element.french}</div>` : ''}
</div>
`;
} else if (typeof element === 'string') {
// Texte simple
div.innerHTML = `<div class="element-content">${element}</div>`;
}
if (element.icon) {
div.innerHTML = `<span class="element-icon">${element.icon}</span>` + div.innerHTML;
}
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];
// Ajouter à l'histoire
this.currentStory.push({ element, originalIndex: index });
// Créer élément dans la zone de construction
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) {
// Supprimer de l'histoire
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('Ajoute au moins un élément à ton histoire !', 'error');
return;
}
const isCorrect = this.validateStory();
if (isCorrect) {
this.score += this.currentStory.length * 10;
this.showFeedback('Bravo ! Histoire parfaite ! 🎉', 'success');
this.onScoreUpdate(this.score);
setTimeout(() => {
this.nextChallenge();
}, 2000);
} else {
this.score = Math.max(0, this.score - 5);
this.showFeedback('Presque ! Vérifie l\'ordre de ton histoire 🤔', 'warning');
this.onScoreUpdate(this.score);
}
}
validateStory() {
switch (this.gameMode) {
case 'sequence':
return this.validateSequence();
case 'dialogue':
return this.validateDialogue();
case 'scenario':
return this.validateScenario();
default:
return true; // Mode libre
}
}
validateSequence() {
if (!this.storyTarget?.content?.steps) return true;
const expectedOrder = this.storyTarget.content.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() {
// Validation flexible du dialogue (ordre logique des répliques)
return this.currentStory.length >= 2;
}
validateScenario() {
// Validation flexible du scénario (cohérence contextuelle)
return this.currentStory.length >= 3;
}
showHint() {
if (!this.storyTarget) {
this.showFeedback('Astuce : Pense à l\'ordre logique des événements !', 'info');
return;
}
switch (this.gameMode) {
case 'sequence':
if (this.storyTarget.content?.steps) {
const nextStep = this.storyTarget.content.steps.find(step =>
!this.currentStory.some(item => item.element.order === step.order)
);
if (nextStep) {
this.showFeedback(`Prochaine étape : "${nextStep.english}"`, 'info');
}
}
break;
case 'dialogue':
this.showFeedback('Pense à l\'ordre naturel d\'une conversation !', 'info');
break;
case 'scenario':
this.showFeedback('Crée une histoire cohérente dans ce contexte !', 'info');
break;
}
}
nextChallenge() {
// Charger un nouveau défi
this.loadStoryContent();
this.currentStory = [];
document.getElementById('drop-zone').innerHTML = '<div class="drop-hint">Glisse les éléments ici pour construire ton histoire</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">Glisse les éléments ici pour construire ton histoire</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;
}
.english {
font-weight: 600;
margin-bottom: 4px;
}
.french {
font-size: 0.9rem;
color: #6b7280;
}
.story-element.in-story .french {
color: rgba(255,255,255,0.8);
}
.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;