Local changes before pull
This commit is contained in:
commit
7d085243c1
269
CLAUDE_local.md
Normal file
269
CLAUDE_local.md
Normal file
@ -0,0 +1,269 @@
|
||||
# Architecture Cours d'Anglais Modulaire
|
||||
*Système de leçons interactives HTML pour enfants de 8-9 ans*
|
||||
|
||||
## 🎯 Objectif
|
||||
Créer une plateforme modulaire permettant de générer facilement des cours d'anglais interactifs avec différents types de jeux et contenus pédagogiques.
|
||||
|
||||
## 🏗️ Architecture en 3 Niveaux
|
||||
|
||||
### Niveau 1 : Page d'Accueil (`index.html`)
|
||||
- **Titre** : "Cours d'Anglais Interactif"
|
||||
- **Options** :
|
||||
- 🎮 "Créer une leçon personnalisée" → Niveau 2
|
||||
- 📊 "Statistiques" (futur)
|
||||
- ⚙️ "Paramètres" (futur)
|
||||
|
||||
### Niveau 2 : Sélection Type de Jeu (`game-selector.html`)
|
||||
Interface avec cards cliquables pour chaque type de jeu :
|
||||
- 🔨 **Whack-a-Mole** (Tape-taupe vocabulaire)
|
||||
- 🧠 **Memory Game** (Jeu de mémoire)
|
||||
- ❓ **Quiz Game** (Questions/Réponses)
|
||||
- 🎯 **Target Game** (Viser les bonnes réponses)
|
||||
- 📝 **Temp Games** (Module temporaire pour nouveaux jeux)
|
||||
|
||||
### Niveau 3 : Sélection Contenu/Niveau (`level-selector.html`)
|
||||
Interface dynamique qui s'adapte au jeu choisi :
|
||||
- 📚 **SBS Level 8** (votre contenu actuel)
|
||||
- 🐱 **Animals Vocabulary**
|
||||
- 🌈 **Colors & Numbers**
|
||||
- 👨👩👧👦 **Family Members**
|
||||
- 🍎 **Food & Drinks**
|
||||
- 🏠 **House & Furniture**
|
||||
|
||||
### Niveau 4 : Jeu (`game.html`)
|
||||
Page générique qui charge dynamiquement :
|
||||
- Le moteur de jeu sélectionné
|
||||
- Le contenu pédagogique choisi
|
||||
- Interface de retour/navigation
|
||||
|
||||
## 📁 Structure des Fichiers
|
||||
|
||||
```
|
||||
cours-anglais/
|
||||
├── index.html # Accueil
|
||||
├── game-selector.html # Sélection type de jeu
|
||||
├── level-selector.html # Sélection niveau/contenu
|
||||
├── game.html # Page de jeu générique
|
||||
│
|
||||
├── css/
|
||||
│ ├── main.css # Styles généraux
|
||||
│ ├── navigation.css # Styles navigation
|
||||
│ └── games.css # Styles jeux
|
||||
│
|
||||
├── js/
|
||||
│ ├── core/
|
||||
│ │ ├── navigation.js # Gestion navigation 3 niveaux
|
||||
│ │ ├── game-loader.js # Chargement dynamique des jeux
|
||||
│ │ └── utils.js # Fonctions utilitaires
|
||||
│ │
|
||||
│ ├── games/
|
||||
│ │ ├── whack-a-mole.js # Module Whack-a-Mole
|
||||
│ │ ├── memory-game.js # Module Memory Game
|
||||
│ │ ├── quiz-game.js # Module Quiz
|
||||
│ │ ├── target-game.js # Module Target Game
|
||||
│ │ └── temp-games.js # Module temporaire
|
||||
│ │
|
||||
│ └── content/
|
||||
│ ├── sbs-level-8.js # Contenu SBS Level 8
|
||||
│ ├── animals.js # Vocabulaire animaux
|
||||
│ ├── colors.js # Couleurs et nombres
|
||||
│ ├── family.js # Famille
|
||||
│ ├── food.js # Nourriture
|
||||
│ └── house.js # Maison
|
||||
│
|
||||
├── assets/
|
||||
│ ├── images/
|
||||
│ ├── sounds/
|
||||
│ └── icons/
|
||||
│
|
||||
└── config/
|
||||
└── games-config.json # Configuration des jeux disponibles
|
||||
```
|
||||
|
||||
## 🔧 Système de Modules
|
||||
|
||||
### Format des Modules de Jeu
|
||||
```javascript
|
||||
// Exemple: whack-a-mole.js
|
||||
class WhackAMoleGame {
|
||||
constructor(content, container) {
|
||||
this.content = content;
|
||||
this.container = container;
|
||||
this.score = 0;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() { /* Initialisation */ }
|
||||
start() { /* Démarrage */ }
|
||||
stop() { /* Arrêt */ }
|
||||
destroy() { /* Nettoyage */ }
|
||||
}
|
||||
|
||||
// Export pour utilisation
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.WhackAMole = WhackAMoleGame;
|
||||
```
|
||||
|
||||
### Format des Modules de Contenu
|
||||
```javascript
|
||||
// Exemple: sbs-level-8.js
|
||||
window.ContentModules = window.ContentModules || {};
|
||||
window.ContentModules.SBSLevel8 = {
|
||||
name: "SBS Level 8",
|
||||
description: "Vocabulaire du manuel SBS Level 8",
|
||||
vocabulary: [
|
||||
{ english: "cat", french: "chat", image: "cat.jpg" },
|
||||
{ english: "dog", french: "chien", image: "dog.jpg" },
|
||||
// ...
|
||||
],
|
||||
grammar: [
|
||||
// Règles de grammaire si nécessaire
|
||||
],
|
||||
difficulty: "intermediate"
|
||||
};
|
||||
```
|
||||
|
||||
## 🎮 Spécifications des Jeux
|
||||
|
||||
### Whack-a-Mole (Priorité 1)
|
||||
- **Principe** : Taupes apparaissent avec mots anglais, élève doit taper les bonnes réponses
|
||||
- **Variantes** :
|
||||
- Mot anglais affiché, taper la traduction française
|
||||
- Image affichée, taper le mot anglais
|
||||
- Son joué, taper le mot correspondant
|
||||
- **Paramètres** : Vitesse, nombre de taupes, durée
|
||||
|
||||
### Memory Game (Priorité 2)
|
||||
- **Principe** : Retourner des cartes pour faire des paires mot/traduction ou mot/image
|
||||
- **Variantes** : Tailles de grille (4x4, 6x6, 8x8)
|
||||
|
||||
### Quiz Game (Priorité 3)
|
||||
- **Principe** : Questions à choix multiples ou réponses libres
|
||||
- **Types** : QCM, Vrai/Faux, Compléter la phrase
|
||||
|
||||
### Target Game (Futur)
|
||||
- **Principe** : Viser avec la souris les bonnes réponses qui bougent
|
||||
- **Variantes** : Ballons, bulles, cibles
|
||||
|
||||
## 🛠️ Fonctionnalités Transversales
|
||||
|
||||
### Navigation
|
||||
- Boutons "Retour" sur chaque niveau
|
||||
- Breadcrumb navigation
|
||||
- URLs avec paramètres pour partage/bookmark
|
||||
- Raccourcis clavier (ESC = retour)
|
||||
|
||||
### Système de Scoring
|
||||
- Points par bonne réponse
|
||||
- Malus par erreur
|
||||
- Bonus vitesse
|
||||
- Historique des scores
|
||||
- Badges/achievements
|
||||
|
||||
### Accessibilité
|
||||
- Support clavier complet
|
||||
- Textes alternatifs pour images
|
||||
- Contrastes élevés
|
||||
- Tailles de police ajustables
|
||||
|
||||
### Responsive Design
|
||||
- Adaptation tablette/mobile
|
||||
- Interface tactile
|
||||
- Orientation portrait/paysage
|
||||
|
||||
## 📊 Configuration et Personnalisation
|
||||
|
||||
### Fichier de Configuration Globale
|
||||
```json
|
||||
{
|
||||
"games": {
|
||||
"whack-a-mole": {
|
||||
"enabled": true,
|
||||
"name": "Whack-a-Mole",
|
||||
"icon": "🔨",
|
||||
"description": "Tape sur les bonnes réponses !"
|
||||
},
|
||||
"memory-game": {
|
||||
"enabled": true,
|
||||
"name": "Memory Game",
|
||||
"icon": "🧠",
|
||||
"description": "Trouve les paires !"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"sbs-level-8": {
|
||||
"enabled": true,
|
||||
"name": "SBS Level 8",
|
||||
"icon": "📚",
|
||||
"description": "Vocabulaire manuel SBS"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"defaultDifficulty": "easy",
|
||||
"soundEnabled": true,
|
||||
"animationsEnabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Plan de Développement
|
||||
|
||||
### Phase 1 (Urgent - Demain)
|
||||
- [x] Architecture de base
|
||||
- [ ] Navigation 3 niveaux
|
||||
- [ ] Module Whack-a-Mole
|
||||
- [ ] Contenu SBS Level 8
|
||||
- [ ] Module temporaire avec 2-3 mini-jeux
|
||||
|
||||
### Phase 2 (Semaine suivante)
|
||||
- [ ] Module Memory Game
|
||||
- [ ] Contenu Animals
|
||||
- [ ] Système de scoring
|
||||
- [ ] Améliorations UI/UX
|
||||
|
||||
### Phase 3 (Semaines suivantes)
|
||||
- [ ] Module Quiz Game
|
||||
- [ ] Contenus Colors, Family, Food
|
||||
- [ ] Système de statistiques
|
||||
- [ ] Export/Import de contenus
|
||||
|
||||
### Phase 4 (Long terme)
|
||||
- [ ] Module Target Game
|
||||
- [ ] Éditeur de contenu intégré
|
||||
- [ ] Système multi-utilisateurs
|
||||
- [ ] Progression pédagogique
|
||||
|
||||
## 💡 Notes d'Implémentation
|
||||
|
||||
### URL Routing
|
||||
- `index.html` → Accueil
|
||||
- `index.html?page=games` → Sélection jeux
|
||||
- `index.html?page=levels&game=whack-a-mole` → Sélection niveau
|
||||
- `index.html?page=play&game=whack-a-mole&content=sbs-level-8` → Jeu
|
||||
|
||||
### Stockage Local
|
||||
- Scores et progression : `localStorage`
|
||||
- Préférences utilisateur : `localStorage`
|
||||
- Cache des contenus : `sessionStorage`
|
||||
|
||||
### Performance
|
||||
- Lazy loading des modules
|
||||
- Préchargement des assets critiques
|
||||
- Compression des images
|
||||
- Minification JS/CSS en production
|
||||
|
||||
## 🎨 Direction Artistique
|
||||
|
||||
### Palette de Couleurs
|
||||
- **Primaire** : Bleu (#3B82F6) - Confiance, apprentissage
|
||||
- **Secondaire** : Vert (#10B981) - Succès, validation
|
||||
- **Accent** : Orange (#F59E0B) - Énergie, attention
|
||||
- **Erreur** : Rouge (#EF4444) - Erreurs claires
|
||||
- **Neutre** : Gris (#6B7280) - Textes, backgrounds
|
||||
|
||||
### Style Visuel
|
||||
- Design moderne et épuré
|
||||
- Animations fluides et non-agressives
|
||||
- Icons emoji + Font Awesome
|
||||
- Typography lisible (16px minimum)
|
||||
- Boutons larges et tactiles (44px minimum)
|
||||
163
config/games-config.json
Normal file
163
config/games-config.json
Normal file
@ -0,0 +1,163 @@
|
||||
{
|
||||
"games": {
|
||||
"whack-a-mole": {
|
||||
"enabled": true,
|
||||
"name": "Whack-a-Mole",
|
||||
"icon": "🔨",
|
||||
"description": "Tape sur les bonnes réponses !",
|
||||
"difficulty": "easy",
|
||||
"minAge": 8,
|
||||
"maxAge": 12,
|
||||
"estimatedTime": 5
|
||||
},
|
||||
"memory-game": {
|
||||
"enabled": false,
|
||||
"name": "Memory Game",
|
||||
"icon": "🧠",
|
||||
"description": "Trouve les paires !",
|
||||
"difficulty": "medium",
|
||||
"minAge": 8,
|
||||
"maxAge": 14,
|
||||
"estimatedTime": 7
|
||||
},
|
||||
"quiz-game": {
|
||||
"enabled": false,
|
||||
"name": "Quiz Game",
|
||||
"icon": "❓",
|
||||
"description": "Réponds aux questions !",
|
||||
"difficulty": "medium",
|
||||
"minAge": 9,
|
||||
"maxAge": 15,
|
||||
"estimatedTime": 10
|
||||
},
|
||||
"temp-games": {
|
||||
"enabled": true,
|
||||
"name": "Mini-Jeux",
|
||||
"icon": "🎯",
|
||||
"description": "Jeux temporaires en développement",
|
||||
"difficulty": "easy",
|
||||
"minAge": 8,
|
||||
"maxAge": 12,
|
||||
"estimatedTime": 3
|
||||
},
|
||||
"story-builder": {
|
||||
"enabled": true,
|
||||
"name": "Story Builder",
|
||||
"icon": "📖",
|
||||
"description": "Construis des histoires en anglais !",
|
||||
"difficulty": "medium",
|
||||
"minAge": 8,
|
||||
"maxAge": 14,
|
||||
"estimatedTime": 10
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"sbs-level-8": {
|
||||
"enabled": true,
|
||||
"name": "SBS Level 8",
|
||||
"icon": "📚",
|
||||
"description": "Vocabulaire du manuel SBS Level 8",
|
||||
"difficulty": "intermediate",
|
||||
"vocabulary_count": 25,
|
||||
"topics": ["family", "daily_activities", "school_objects"]
|
||||
},
|
||||
"animals": {
|
||||
"enabled": false,
|
||||
"name": "Animals",
|
||||
"icon": "🐱",
|
||||
"description": "Vocabulaire des animaux",
|
||||
"difficulty": "easy",
|
||||
"vocabulary_count": 20,
|
||||
"topics": ["pets", "farm_animals", "wild_animals"]
|
||||
},
|
||||
"colors": {
|
||||
"enabled": false,
|
||||
"name": "Colors & Numbers",
|
||||
"icon": "🌈",
|
||||
"description": "Couleurs et nombres",
|
||||
"difficulty": "easy",
|
||||
"vocabulary_count": 15,
|
||||
"topics": ["basic_colors", "numbers_1_to_20"]
|
||||
},
|
||||
"family": {
|
||||
"enabled": false,
|
||||
"name": "Family Members",
|
||||
"icon": "👨👩👧👦",
|
||||
"description": "Membres de la famille",
|
||||
"difficulty": "easy",
|
||||
"vocabulary_count": 12,
|
||||
"topics": ["immediate_family", "extended_family"]
|
||||
},
|
||||
"food": {
|
||||
"enabled": false,
|
||||
"name": "Food & Drinks",
|
||||
"icon": "🍎",
|
||||
"description": "Nourriture et boissons",
|
||||
"difficulty": "easy",
|
||||
"vocabulary_count": 18,
|
||||
"topics": ["fruits", "vegetables", "drinks", "meals"]
|
||||
},
|
||||
"house": {
|
||||
"enabled": false,
|
||||
"name": "House & Furniture",
|
||||
"icon": "🏠",
|
||||
"description": "Maison et mobilier",
|
||||
"difficulty": "medium",
|
||||
"vocabulary_count": 22,
|
||||
"topics": ["rooms", "furniture", "household_objects"]
|
||||
},
|
||||
"demo-flexible": {
|
||||
"enabled": true,
|
||||
"name": "Demo Architecture",
|
||||
"icon": "🎯",
|
||||
"description": "Démonstration de la nouvelle architecture flexible",
|
||||
"difficulty": "mixed",
|
||||
"vocabulary_count": 12,
|
||||
"topics": ["vocabulary", "sentences", "dialogues", "sequences"]
|
||||
},
|
||||
"sbs-level-7-8": {
|
||||
"enabled": true,
|
||||
"name": "SBS Level 7-8",
|
||||
"icon": "🌍",
|
||||
"description": "Around the World - Homes, Clothing & Cultures",
|
||||
"difficulty": "intermediate",
|
||||
"vocabulary_count": 85,
|
||||
"topics": ["homes", "clothing", "neighborhoods", "grammar", "culture"]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"defaultDifficulty": "easy",
|
||||
"soundEnabled": true,
|
||||
"animationsEnabled": true,
|
||||
"autoSave": true,
|
||||
"showHints": true,
|
||||
"timeLimit": 300,
|
||||
"maxErrors": 5,
|
||||
"pointsPerCorrect": 10,
|
||||
"pointsPerError": -2
|
||||
},
|
||||
"ui": {
|
||||
"theme": "default",
|
||||
"language": "fr",
|
||||
"fontSize": "medium",
|
||||
"animations": {
|
||||
"enabled": true,
|
||||
"speed": "normal"
|
||||
},
|
||||
"sounds": {
|
||||
"enabled": true,
|
||||
"volume": 0.5,
|
||||
"effects": ["click", "success", "error", "complete"]
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"statistics": false,
|
||||
"userProfiles": false,
|
||||
"achievements": false,
|
||||
"multiplayer": false,
|
||||
"contentEditor": false,
|
||||
"exportImport": false
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"lastUpdated": "2024-01-15"
|
||||
}
|
||||
1525
css/games.css
Normal file
1525
css/games.css
Normal file
File diff suppressed because it is too large
Load Diff
394
css/main.css
Normal file
394
css/main.css
Normal file
@ -0,0 +1,394 @@
|
||||
/* === VARIABLES CSS === */
|
||||
:root {
|
||||
--primary-color: #3B82F6;
|
||||
--secondary-color: #10B981;
|
||||
--accent-color: #F59E0B;
|
||||
--error-color: #EF4444;
|
||||
--success-color: #22C55E;
|
||||
--neutral-color: #6B7280;
|
||||
--background-color: #F8FAFC;
|
||||
--background: #F8FAFC;
|
||||
--card-background: #FFFFFF;
|
||||
--text-primary: #1F2937;
|
||||
--text-secondary: #6B7280;
|
||||
--border-color: #E5E7EB;
|
||||
--primary-light: #EBF4FF;
|
||||
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
--border-radius: 12px;
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* === RESET ET BASE === */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* === LAYOUT PRINCIPAL === */
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* === PAGES === */
|
||||
.page {
|
||||
display: none;
|
||||
animation: fadeIn 0.4s ease-in-out;
|
||||
background: var(--background-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.page.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* === HERO SECTION === */
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.3rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* === PAGE HEADERS === */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* === CARDS SYSTEM === */
|
||||
.main-options {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 25px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.option-card, .game-card, .level-card {
|
||||
background: var(--card-background);
|
||||
border: 3px solid transparent;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 30px 25px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
box-shadow: var(--shadow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.option-card:hover, .game-card:hover, .level-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.option-card.primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), #2563EB);
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.option-card.primary:hover {
|
||||
background: linear-gradient(135deg, #2563EB, var(--primary-color));
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
.option-card.secondary {
|
||||
border: 2px solid var(--neutral-color);
|
||||
}
|
||||
|
||||
.option-card span {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.option-card small {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* === GAME CARDS === */
|
||||
.game-card {
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.game-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
||||
}
|
||||
|
||||
.game-card .icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.game-card .title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.game-card .description {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* === LEVEL CARDS === */
|
||||
.level-card {
|
||||
min-height: 160px;
|
||||
border: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.level-card:hover {
|
||||
border-color: var(--secondary-color);
|
||||
background: linear-gradient(135deg, #f0fdf4, #ecfdf5);
|
||||
}
|
||||
|
||||
.level-card .icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.level-card .title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.level-card .description {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* === BOUTONS === */
|
||||
button {
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
transition: var(--transition);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 12px 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: var(--neutral-color);
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 25px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #4B5563;
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
/* === MODAL === */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--card-background);
|
||||
padding: 40px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
margin: 20px;
|
||||
transform: scale(0.9);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.modal.show .modal-content {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.modal-content button {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 12px 30px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.modal-content button:hover {
|
||||
background: #2563EB;
|
||||
}
|
||||
|
||||
/* === LOADING === */
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* === RESPONSIVE === */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hero h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.option-card, .game-card, .level-card {
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 30px 20px;
|
||||
margin: 15px;
|
||||
}
|
||||
}
|
||||
262
css/navigation.css
Normal file
262
css/navigation.css
Normal file
@ -0,0 +1,262 @@
|
||||
/* === NAVIGATION BREADCRUMB === */
|
||||
.breadcrumb {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 15px 25px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
background: transparent;
|
||||
border: 2px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
background: var(--background-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.breadcrumb-item:not(:last-child)::after {
|
||||
content: '›';
|
||||
position: absolute;
|
||||
right: -15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* === ANIMATIONS DE TRANSITION === */
|
||||
.page-transition-enter {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.page-transition-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.page-transition-exit {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.page-transition-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
transition: all 0.3s ease-in;
|
||||
}
|
||||
|
||||
/* === NAVIGATION RESPONSIVE === */
|
||||
@media (max-width: 768px) {
|
||||
.breadcrumb {
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 15px;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item:not(:last-child)::after {
|
||||
right: -12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* === CONTENT SCANNING STYLES === */
|
||||
.loading-content, .no-content, .error-content {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border: 2px dashed #e5e7eb;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
color: var(--error-color);
|
||||
border-color: var(--error-color);
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.content-info {
|
||||
background: #f0f9ff;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.show-all-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.show-all-btn:hover {
|
||||
background: #2563EB;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* === ENHANCED LEVEL CARDS === */
|
||||
.level-card {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-header .icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.compatibility {
|
||||
font-size: 1.2rem;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.compatibility.high-compat {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.compatibility.medium-compat {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.compatibility.low-compat {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.content-stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 10px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.difficulty-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.difficulty-easy {
|
||||
background: #10B981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty-medium {
|
||||
background: #F59E0B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty-hard {
|
||||
background: #EF4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.difficulty-intermediate {
|
||||
background: #6366F1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.items-count, .time-estimate {
|
||||
background: #f3f4f6;
|
||||
color: var(--text-secondary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.detailed-stats {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.content-stats {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detailed-stats {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.breadcrumb {
|
||||
padding: 10px 15px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item:not(:last-child)::after {
|
||||
right: -10px;
|
||||
}
|
||||
|
||||
.content-stats {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.loading-content, .no-content, .error-content {
|
||||
padding: 30px 15px;
|
||||
}
|
||||
}
|
||||
145
index.html
Normal file
145
index.html
Normal file
@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cours d'Anglais Interactif</title>
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/navigation.css">
|
||||
<link rel="stylesheet" href="css/games.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation Breadcrumb -->
|
||||
<nav class="breadcrumb" id="breadcrumb">
|
||||
<button class="breadcrumb-item active" data-page="home">🏠 Accueil</button>
|
||||
</nav>
|
||||
|
||||
<!-- Main Container -->
|
||||
<main class="container" id="main-container">
|
||||
|
||||
<!-- Page d'Accueil -->
|
||||
<div class="page active" id="home-page">
|
||||
<div class="hero">
|
||||
<h1>🎓 Cours d'Anglais Interactif</h1>
|
||||
<p>Apprends l'anglais en t'amusant !</p>
|
||||
</div>
|
||||
|
||||
<div class="main-options">
|
||||
<button class="option-card primary" onclick="navigateTo('games')">
|
||||
🎮 <span>Créer une leçon personnalisée</span>
|
||||
</button>
|
||||
<button class="option-card secondary" onclick="showComingSoon()">
|
||||
📊 <span>Statistiques</span>
|
||||
<small>Bientôt disponible</small>
|
||||
</button>
|
||||
<button class="option-card secondary" onclick="showComingSoon()">
|
||||
⚙️ <span>Paramètres</span>
|
||||
<small>Bientôt disponible</small>
|
||||
</button>
|
||||
<button class="option-card primary" onclick="showContentCreator()">
|
||||
🏭 <span>Créateur de Contenu</span>
|
||||
<small>Créez vos propres exercices</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sélection Type de Jeu -->
|
||||
<div class="page" id="games-page">
|
||||
<div class="page-header">
|
||||
<h2>🎮 Choisis ton jeu</h2>
|
||||
<p>Sélectionne le type d'activité que tu veux faire</p>
|
||||
</div>
|
||||
|
||||
<div class="cards-grid" id="games-grid">
|
||||
<!-- Les cartes seront générées dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sélection Contenu/Niveau -->
|
||||
<div class="page" id="levels-page">
|
||||
<div class="page-header">
|
||||
<h2>📚 Choisis ton niveau</h2>
|
||||
<p id="level-description">Sélectionne le contenu à apprendre</p>
|
||||
</div>
|
||||
|
||||
<div class="cards-grid" id="levels-grid">
|
||||
<!-- Les cartes seront générées dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page de Jeu -->
|
||||
<div class="page" id="game-page">
|
||||
<div class="game-header">
|
||||
<button class="back-btn" onclick="goBack()">← Retour</button>
|
||||
<h3 id="game-title">Jeu en cours...</h3>
|
||||
<div class="score-display">
|
||||
Score: <span id="current-score">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-container" id="game-container">
|
||||
<!-- Le jeu sera chargé ici dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Modal Coming Soon -->
|
||||
<div class="modal" id="coming-soon-modal">
|
||||
<div class="modal-content">
|
||||
<h3>🚧 Bientôt disponible !</h3>
|
||||
<p>Cette fonctionnalité sera disponible dans une prochaine version.</p>
|
||||
<button onclick="closeModal()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Chargement...</p>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="js/core/utils.js"></script>
|
||||
<script src="js/core/content-engine.js"></script>
|
||||
<script src="js/core/content-factory.js"></script>
|
||||
<script src="js/core/content-parsers.js"></script>
|
||||
<script src="js/core/content-generators.js"></script>
|
||||
<script src="js/core/content-scanner.js"></script>
|
||||
<script src="js/tools/content-creator.js"></script>
|
||||
<script src="js/core/navigation.js"></script>
|
||||
<script src="js/core/game-loader.js"></script>
|
||||
<script>
|
||||
// Initialize app when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
AppNavigation.init();
|
||||
});
|
||||
|
||||
// Coming soon functionality
|
||||
function showComingSoon() {
|
||||
document.getElementById('coming-soon-modal').classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('coming-soon-modal').classList.remove('show');
|
||||
}
|
||||
|
||||
function showContentCreator() {
|
||||
// Masquer la page d'accueil
|
||||
document.getElementById('home-page').classList.remove('active');
|
||||
|
||||
// Créer et afficher le créateur de contenu
|
||||
const creator = new ContentCreator();
|
||||
creator.init();
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
AppNavigation.goBack();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
239
js/content/sbs-level-7-8-new.js
Normal file
239
js/content/sbs-level-7-8-new.js
Normal file
@ -0,0 +1,239 @@
|
||||
const content = {
|
||||
vocabulary: {
|
||||
// Housing and Places
|
||||
central: "中心的;中央的",
|
||||
avenue: "大街;林荫道",
|
||||
refrigerator: "冰箱",
|
||||
closet: "衣柜;壁橱",
|
||||
elevator: "电梯",
|
||||
building: "建筑物;大楼",
|
||||
"air conditioner": "空调",
|
||||
superintendent: "主管;负责人",
|
||||
"bus stop": "公交车站",
|
||||
jacuzzi: "按摩浴缸",
|
||||
machine: "机器;设备",
|
||||
"two and a half": "两个半",
|
||||
"in the center of": "在……中心",
|
||||
town: "城镇",
|
||||
"a lot of": "许多",
|
||||
noise: "噪音",
|
||||
sidewalks: "人行道",
|
||||
"all day and all night": "整日整夜",
|
||||
convenient: "便利的",
|
||||
upset: "失望的",
|
||||
|
||||
// Clothing and Accessories
|
||||
shirt: "衬衫",
|
||||
coat: "外套、大衣",
|
||||
dress: "连衣裙",
|
||||
skirt: "短裙",
|
||||
blouse: "女式衬衫",
|
||||
jacket: "夹克、短外套",
|
||||
sweater: "毛衣、针织衫",
|
||||
suit: "套装、西装",
|
||||
tie: "领带",
|
||||
pants: "裤子",
|
||||
jeans: "牛仔裤",
|
||||
belt: "腰带、皮带",
|
||||
hat: "帽子",
|
||||
glove: "手套",
|
||||
"purse/pocketbook": "手提包、女式小包",
|
||||
glasses: "眼镜",
|
||||
pajamas: "睡衣",
|
||||
socks: "袜子",
|
||||
shoes: "鞋子",
|
||||
bathrobe: "浴袍",
|
||||
"tee shirt": "T恤",
|
||||
scarf: "围巾",
|
||||
wallet: "钱包",
|
||||
ring: "戒指",
|
||||
sandals: "凉鞋",
|
||||
slippers: "拖鞋",
|
||||
sneakers: "运动鞋",
|
||||
shorts: "短裤",
|
||||
"sweat pants": "运动裤",
|
||||
|
||||
// Places and Areas
|
||||
"urban areas": "cities",
|
||||
"suburban areas": "places near cities",
|
||||
"rural areas": "places in the countryside, far from cities",
|
||||
farmhouse: "农舍",
|
||||
hut: "小屋",
|
||||
houseboat: "船屋",
|
||||
"mobile home": "移动房屋",
|
||||
trailer: "拖车房",
|
||||
|
||||
// Store Items
|
||||
jackets: "夹克",
|
||||
gloves: "手套",
|
||||
blouses: "女式衬衫",
|
||||
bracelets: "手镯",
|
||||
ties: "领带"
|
||||
},
|
||||
|
||||
sentences: [
|
||||
{
|
||||
english: "Amy's apartment building is in the center of town.",
|
||||
chinese: "艾米的公寓楼在城镇中心。"
|
||||
},
|
||||
{
|
||||
english: "There's a lot of noise near Amy's apartment building.",
|
||||
chinese: "艾米的公寓楼附近有很多噪音。"
|
||||
},
|
||||
{
|
||||
english: "It's a very busy place, but it's a convenient place to live.",
|
||||
chinese: "那是个非常热闹的地方,但也是个居住很方便的地方。"
|
||||
},
|
||||
{
|
||||
english: "Around the corner from the building, there are two supermarkets.",
|
||||
chinese: "从这栋楼拐个弯,就有两家超市。"
|
||||
},
|
||||
{
|
||||
english: "I'm looking for a shirt.",
|
||||
chinese: "我在找一件衬衫。"
|
||||
},
|
||||
{
|
||||
english: "Shirts are over there.",
|
||||
chinese: "衬衫在那边。"
|
||||
}
|
||||
],
|
||||
|
||||
texts: [
|
||||
{
|
||||
title: "People's Homes",
|
||||
content: "Homes are different all around the world. This family is living in a farmhouse. This family is living in a hut. This family is living in a houseboat. These people are living in a mobile home (a trailer). What different kinds of homes are there in your country?"
|
||||
},
|
||||
{
|
||||
title: "Urban, Suburban, and Rural",
|
||||
content: "urban areas = cities, suburban areas = places near cities, rural areas = places in the countryside, far from cities. About 50% (percent) of the world's population is in urban and suburban areas. About 50% (percent) of the world's population is in rural areas."
|
||||
},
|
||||
{
|
||||
title: "Global Exchange - RosieM",
|
||||
content: "My apartment is in a wonderful neighborhood. There's a big, beautiful park across from my apartment building. Around the corner, there's a bank, a post office, and a laundromat. There are also many restaurants and stores in my neighborhood. It's a noisy place, but it's a very interesting place. There are a lot of people on the sidewalks all day and all night. How about your neighborhood? Tell me about it."
|
||||
},
|
||||
{
|
||||
title: "Clothing, Colors, and Cultures",
|
||||
content: "Blue and pink aren't children's clothing colors all around the world. The meanings of colors are sometimes very different in different cultures. For example, in some cultures, blue is a common clothing color for little boys, and pink is a common clothing color for little girls. In other cultures, other colors are common for boys and girls. There are also different colors for special days in different cultures. For example, white is the traditional color of a wedding dress in some cultures, but other colors are traditional in other cultures. For some people, white is a happy color. For others, it's a sad color. For some people, red is a beautiful and lucky color. For others, it's a very sad color. What are the meanings of different colors in YOUR culture?"
|
||||
}
|
||||
],
|
||||
|
||||
grammar: {
|
||||
thereBe: {
|
||||
topic: "There be 句型的用法",
|
||||
singular: {
|
||||
form: "there is (there's) + 名词单数/不可数名词",
|
||||
explanation: "在某地方有什么人或东西",
|
||||
examples: [
|
||||
"There's a bank.",
|
||||
"There's some water.",
|
||||
"There's a book store on Main Street."
|
||||
],
|
||||
forms: {
|
||||
positive: "There's a stove in the kitchen.",
|
||||
negative: "There isn't a stove in the kitchen.",
|
||||
question: "Is there a stove in the kitchen?",
|
||||
shortAnswers: "Yes, there is. / No, there isn't."
|
||||
}
|
||||
},
|
||||
plural: {
|
||||
form: "there are (there're) + 复数名词",
|
||||
examples: [
|
||||
"There're two hospitals.",
|
||||
"There're many rooms in this apartment."
|
||||
],
|
||||
forms: {
|
||||
positive: "There're two windows in the kitchen.",
|
||||
negative: "There aren't two windows in the kitchen.",
|
||||
question: "Are there two windows in the kitchen?",
|
||||
shortAnswers: "Yes, there are. / No, there aren't."
|
||||
}
|
||||
}
|
||||
},
|
||||
plurals: {
|
||||
topic: "可数名词复数",
|
||||
pronunciation: {
|
||||
rules: [
|
||||
{
|
||||
condition: "在清辅音/-p,-k/后",
|
||||
pronunciation: "/-s/",
|
||||
example: "socks中-k是清辅音/-k/,所以-s读/-s/"
|
||||
},
|
||||
{
|
||||
condition: "在浊辅音和元音音标后",
|
||||
pronunciation: "/-z/",
|
||||
example: "jeans中-n是浊辅音/-n/, 所以-s读/-z/; tie的读音是/tai/,以元音结尾,所以-s读/-z/"
|
||||
},
|
||||
{
|
||||
condition: "以/-s,-z,-ʃ,-ʒ,-tʃ,-dʒ/发音结尾的名词",
|
||||
pronunciation: "/-iz/",
|
||||
example: "watches中-ch读/-tʃ/,所以-es读/-iz/"
|
||||
}
|
||||
]
|
||||
},
|
||||
formation: {
|
||||
regular: {
|
||||
rule: "一般在词尾加-s",
|
||||
examples: ["shirts", "shoes"]
|
||||
},
|
||||
special: {
|
||||
rule: "以-s,-sh,-ch,-x,以及辅音字母o结尾的词在词尾加-es",
|
||||
examples: ["boxes", "buses", "potatoes", "tomatoes", "heroes"]
|
||||
},
|
||||
irregular: {
|
||||
rule: "特殊的复数形式",
|
||||
examples: {
|
||||
"man": "men",
|
||||
"woman": "women",
|
||||
"child": "children",
|
||||
"tooth": "teeth",
|
||||
"mouse": "mice"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
listening: {
|
||||
jMartShopping: {
|
||||
title: "Attention, J-Mart Shoppers!",
|
||||
items: [
|
||||
{ item: "jackets", aisle: "Aisle 9" },
|
||||
{ item: "gloves", aisle: "Aisle 7" },
|
||||
{ item: "blouses", aisle: "Aisle 9" },
|
||||
{ item: "bracelets", aisle: "Aisle 11" },
|
||||
{ item: "ties", aisle: "Aisle 5" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
exercises: {
|
||||
sentenceCompletion: [
|
||||
"That's a very nice _______.",
|
||||
"Those are very nice _______."
|
||||
],
|
||||
questions: [
|
||||
"What different kinds of homes are there in your country?",
|
||||
"How about your neighborhood? Tell me about it.",
|
||||
"What are the meanings of different colors in YOUR culture?"
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Export pour le système de modules web
|
||||
window.ContentModules = window.ContentModules || {};
|
||||
window.ContentModules.SBSLevel78New = {
|
||||
name: "SBS Level 7-8 (New)",
|
||||
description: "Format simple et clair - Homes, Clothing & Cultures",
|
||||
difficulty: "intermediate",
|
||||
vocabulary: content.vocabulary,
|
||||
sentences: content.sentences,
|
||||
texts: content.texts,
|
||||
grammar: content.grammar,
|
||||
listening: content.listening,
|
||||
exercises: content.exercises
|
||||
};
|
||||
|
||||
// Export Node.js (optionnel)
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = content;
|
||||
}
|
||||
485
js/core/content-engine.js
Normal file
485
js/core/content-engine.js
Normal file
@ -0,0 +1,485 @@
|
||||
// === MOTEUR DE CONTENU FLEXIBLE ===
|
||||
|
||||
class ContentEngine {
|
||||
constructor() {
|
||||
this.loadedContent = {};
|
||||
this.migrator = new ContentMigrator();
|
||||
this.validator = new ContentValidator();
|
||||
}
|
||||
|
||||
// Charger et traiter un module de contenu
|
||||
async loadContent(contentId) {
|
||||
if (this.loadedContent[contentId]) {
|
||||
return this.loadedContent[contentId];
|
||||
}
|
||||
|
||||
try {
|
||||
// Charger le contenu brut
|
||||
const rawContent = await this.loadRawContent(contentId);
|
||||
|
||||
// Valider et migrer si nécessaire
|
||||
const processedContent = this.processContent(rawContent);
|
||||
|
||||
// Mettre en cache
|
||||
this.loadedContent[contentId] = processedContent;
|
||||
|
||||
return processedContent;
|
||||
} catch (error) {
|
||||
console.error(`Erreur chargement contenu ${contentId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadRawContent(contentId) {
|
||||
// Charger depuis le module existant
|
||||
const moduleName = this.getModuleName(contentId);
|
||||
if (window.ContentModules && window.ContentModules[moduleName]) {
|
||||
return window.ContentModules[moduleName];
|
||||
}
|
||||
|
||||
// Charger dynamiquement le script
|
||||
await this.loadScript(`js/content/${contentId}.js`);
|
||||
return window.ContentModules[moduleName];
|
||||
}
|
||||
|
||||
processContent(rawContent) {
|
||||
// Vérifier le format
|
||||
if (this.isOldFormat(rawContent)) {
|
||||
console.log('Migration ancien format vers nouveau format...');
|
||||
return this.migrator.migrateToNewFormat(rawContent);
|
||||
}
|
||||
|
||||
// Valider le nouveau format
|
||||
if (!this.validator.validate(rawContent)) {
|
||||
throw new Error('Format de contenu invalide');
|
||||
}
|
||||
|
||||
return rawContent;
|
||||
}
|
||||
|
||||
isOldFormat(content) {
|
||||
// Détecter l'ancien format (simple array vocabulary)
|
||||
return content.vocabulary && Array.isArray(content.vocabulary) &&
|
||||
!content.contentItems && !content.version;
|
||||
}
|
||||
|
||||
// Filtrer le contenu par critères
|
||||
filterContent(content, filters = {}) {
|
||||
if (!content.contentItems) return content;
|
||||
|
||||
let filtered = [...content.contentItems];
|
||||
|
||||
// Filtrer par type
|
||||
if (filters.type) {
|
||||
filtered = filtered.filter(item =>
|
||||
Array.isArray(filters.type) ?
|
||||
filters.type.includes(item.type) :
|
||||
item.type === filters.type
|
||||
);
|
||||
}
|
||||
|
||||
// Filtrer par difficulté
|
||||
if (filters.difficulty) {
|
||||
filtered = filtered.filter(item => item.difficulty === filters.difficulty);
|
||||
}
|
||||
|
||||
// Filtrer par catégorie
|
||||
if (filters.category) {
|
||||
filtered = filtered.filter(item => item.category === filters.category);
|
||||
}
|
||||
|
||||
// Filtrer par tags
|
||||
if (filters.tags) {
|
||||
filtered = filtered.filter(item =>
|
||||
item.content.tags &&
|
||||
filters.tags.some(tag => item.content.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
return { ...content, contentItems: filtered };
|
||||
}
|
||||
|
||||
// Adapter le contenu pour un jeu spécifique
|
||||
adaptForGame(content, gameType) {
|
||||
const adapter = new GameContentAdapter(gameType);
|
||||
return adapter.adapt(content);
|
||||
}
|
||||
|
||||
// Obtenir des éléments aléatoires
|
||||
getRandomItems(content, count = 10, filters = {}) {
|
||||
const filtered = this.filterContent(content, filters);
|
||||
const items = filtered.contentItems || [];
|
||||
|
||||
const shuffled = [...items].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, count);
|
||||
}
|
||||
|
||||
// Obtenir des éléments par progression
|
||||
getItemsByProgression(content, userLevel = 1, count = 10) {
|
||||
const items = content.contentItems || [];
|
||||
|
||||
// Calculer la difficulté appropriée
|
||||
const targetDifficulties = this.getDifficultiesForLevel(userLevel);
|
||||
|
||||
const appropriateItems = items.filter(item =>
|
||||
targetDifficulties.includes(item.difficulty)
|
||||
);
|
||||
|
||||
return this.shuffleArray(appropriateItems).slice(0, count);
|
||||
}
|
||||
|
||||
getDifficultiesForLevel(level) {
|
||||
if (level <= 2) return ['easy'];
|
||||
if (level <= 4) return ['easy', 'medium'];
|
||||
return ['easy', 'medium', 'hard'];
|
||||
}
|
||||
|
||||
// Utilitaires
|
||||
getModuleName(contentId) {
|
||||
const names = {
|
||||
'sbs-level-8': 'SBSLevel8',
|
||||
'animals': 'Animals',
|
||||
'colors': 'Colors',
|
||||
'family': 'Family',
|
||||
'food': 'Food',
|
||||
'house': 'House'
|
||||
};
|
||||
return names[contentId] || contentId;
|
||||
}
|
||||
|
||||
async loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const existingScript = document.querySelector(`script[src="${src}"]`);
|
||||
if (existingScript) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = () => reject(new Error(`Impossible de charger ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// === MIGRATEUR DE CONTENU ===
|
||||
|
||||
class ContentMigrator {
|
||||
migrateToNewFormat(oldContent) {
|
||||
const newContent = {
|
||||
id: oldContent.name || 'migrated-content',
|
||||
name: oldContent.name || 'Contenu Migré',
|
||||
description: oldContent.description || '',
|
||||
version: '2.0',
|
||||
format: 'unified',
|
||||
difficulty: oldContent.difficulty || 'intermediate',
|
||||
|
||||
// Métadonnées
|
||||
metadata: {
|
||||
totalItems: oldContent.vocabulary ? oldContent.vocabulary.length : 0,
|
||||
categories: this.extractCategories(oldContent),
|
||||
contentTypes: ['vocabulary'],
|
||||
migrationDate: new Date().toISOString()
|
||||
},
|
||||
|
||||
// Configuration
|
||||
config: {
|
||||
defaultInteraction: 'click',
|
||||
supportedGames: ['whack-a-mole', 'memory-game', 'temp-games'],
|
||||
adaptiveEnabled: true
|
||||
},
|
||||
|
||||
// Contenu principal
|
||||
contentItems: []
|
||||
};
|
||||
|
||||
// Migrer le vocabulaire
|
||||
if (oldContent.vocabulary) {
|
||||
oldContent.vocabulary.forEach((word, index) => {
|
||||
newContent.contentItems.push(this.migrateVocabularyItem(word, index));
|
||||
});
|
||||
}
|
||||
|
||||
// Migrer les phrases si elles existent
|
||||
if (oldContent.phrases) {
|
||||
oldContent.phrases.forEach((phrase, index) => {
|
||||
newContent.contentItems.push(this.migratePhraseItem(phrase, index));
|
||||
});
|
||||
}
|
||||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
migrateVocabularyItem(oldWord, index) {
|
||||
return {
|
||||
id: `vocab_${index + 1}`,
|
||||
type: 'vocabulary',
|
||||
difficulty: this.inferDifficulty(oldWord.english),
|
||||
category: oldWord.category || 'general',
|
||||
|
||||
content: {
|
||||
english: oldWord.english,
|
||||
french: oldWord.french,
|
||||
context: oldWord.category || '',
|
||||
tags: oldWord.category ? [oldWord.category] : []
|
||||
},
|
||||
|
||||
media: {
|
||||
image: oldWord.image || null,
|
||||
audio: null,
|
||||
icon: this.getIconForCategory(oldWord.category)
|
||||
},
|
||||
|
||||
pedagogy: {
|
||||
learningObjective: `Apprendre le mot "${oldWord.english}"`,
|
||||
prerequisites: [],
|
||||
followUp: [],
|
||||
grammarFocus: 'vocabulary'
|
||||
},
|
||||
|
||||
interaction: {
|
||||
type: 'click',
|
||||
validation: 'exact',
|
||||
hints: [oldWord.french],
|
||||
feedback: {
|
||||
correct: `Bien joué ! "${oldWord.english}" = "${oldWord.french}"`,
|
||||
incorrect: `Non, "${oldWord.english}" = "${oldWord.french}"`
|
||||
},
|
||||
alternatives: [] // Sera rempli dynamiquement
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
migratePhraseItem(oldPhrase, index) {
|
||||
return {
|
||||
id: `phrase_${index + 1}`,
|
||||
type: 'sentence',
|
||||
difficulty: 'medium',
|
||||
category: oldPhrase.category || 'general',
|
||||
|
||||
content: {
|
||||
english: oldPhrase.english,
|
||||
french: oldPhrase.french,
|
||||
context: oldPhrase.category || '',
|
||||
tags: [oldPhrase.category || 'phrase']
|
||||
},
|
||||
|
||||
media: {
|
||||
image: null,
|
||||
audio: null,
|
||||
icon: '💬'
|
||||
},
|
||||
|
||||
pedagogy: {
|
||||
learningObjective: `Comprendre la phrase "${oldPhrase.english}"`,
|
||||
prerequisites: [],
|
||||
followUp: [],
|
||||
grammarFocus: 'sentence_structure'
|
||||
},
|
||||
|
||||
interaction: {
|
||||
type: 'click',
|
||||
validation: 'exact',
|
||||
hints: [oldPhrase.french],
|
||||
feedback: {
|
||||
correct: `Parfait ! Cette phrase signifie "${oldPhrase.french}"`,
|
||||
incorrect: `Cette phrase signifie "${oldPhrase.french}"`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
inferDifficulty(englishWord) {
|
||||
if (englishWord.length <= 4) return 'easy';
|
||||
if (englishWord.length <= 8) return 'medium';
|
||||
return 'hard';
|
||||
}
|
||||
|
||||
getIconForCategory(category) {
|
||||
const icons = {
|
||||
family: '👨👩👧👦',
|
||||
animals: '🐱',
|
||||
colors: '🎨',
|
||||
food: '🍎',
|
||||
school_objects: '📚',
|
||||
daily_activities: '🌅',
|
||||
numbers: '🔢'
|
||||
};
|
||||
return icons[category] || '📝';
|
||||
}
|
||||
|
||||
extractCategories(oldContent) {
|
||||
const categories = new Set();
|
||||
|
||||
if (oldContent.vocabulary) {
|
||||
oldContent.vocabulary.forEach(word => {
|
||||
if (word.category) categories.add(word.category);
|
||||
});
|
||||
}
|
||||
|
||||
if (oldContent.categories) {
|
||||
Object.keys(oldContent.categories).forEach(cat => categories.add(cat));
|
||||
}
|
||||
|
||||
return Array.from(categories);
|
||||
}
|
||||
}
|
||||
|
||||
// === VALIDATEUR DE CONTENU ===
|
||||
|
||||
class ContentValidator {
|
||||
validate(content) {
|
||||
try {
|
||||
// Vérifications de base
|
||||
if (!content.id || !content.name) {
|
||||
console.warn('Contenu manque ID ou nom');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!content.contentItems || !Array.isArray(content.contentItems)) {
|
||||
console.warn('contentItems manquant ou invalide');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Valider chaque élément
|
||||
for (let item of content.contentItems) {
|
||||
if (!this.validateContentItem(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erreur validation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
validateContentItem(item) {
|
||||
// Champs requis
|
||||
const requiredFields = ['id', 'type', 'content'];
|
||||
|
||||
for (let field of requiredFields) {
|
||||
if (!item[field]) {
|
||||
console.warn(`Champ requis manquant: ${field}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Valider le contenu selon le type
|
||||
switch (item.type) {
|
||||
case 'vocabulary':
|
||||
return this.validateVocabulary(item);
|
||||
case 'sentence':
|
||||
return this.validateSentence(item);
|
||||
case 'dialogue':
|
||||
return this.validateDialogue(item);
|
||||
default:
|
||||
console.warn(`Type de contenu inconnu: ${item.type}`);
|
||||
return true; // Permettre types inconnus pour extensibilité
|
||||
}
|
||||
}
|
||||
|
||||
validateVocabulary(item) {
|
||||
return item.content.english && item.content.french;
|
||||
}
|
||||
|
||||
validateSentence(item) {
|
||||
return item.content.english && item.content.french;
|
||||
}
|
||||
|
||||
validateDialogue(item) {
|
||||
return item.content.conversation && Array.isArray(item.content.conversation);
|
||||
}
|
||||
}
|
||||
|
||||
// === ADAPTATEUR CONTENU/JEU ===
|
||||
|
||||
class GameContentAdapter {
|
||||
constructor(gameType) {
|
||||
this.gameType = gameType;
|
||||
}
|
||||
|
||||
adapt(content) {
|
||||
switch (this.gameType) {
|
||||
case 'whack-a-mole':
|
||||
return this.adaptForWhackAMole(content);
|
||||
case 'memory-game':
|
||||
return this.adaptForMemoryGame(content);
|
||||
case 'temp-games':
|
||||
return this.adaptForTempGames(content);
|
||||
default:
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
adaptForWhackAMole(content) {
|
||||
// Convertir vers format attendu par Whack-a-Mole
|
||||
const vocabulary = content.contentItems
|
||||
.filter(item => item.type === 'vocabulary' || item.type === 'sentence')
|
||||
.map(item => ({
|
||||
english: item.content.english,
|
||||
french: item.content.french,
|
||||
image: item.media?.image,
|
||||
category: item.category,
|
||||
difficulty: item.difficulty,
|
||||
interaction: item.interaction
|
||||
}));
|
||||
|
||||
return {
|
||||
...content,
|
||||
vocabulary: vocabulary,
|
||||
// Maintenir la compatibilité
|
||||
gameSettings: {
|
||||
whackAMole: {
|
||||
recommendedWords: Math.min(15, vocabulary.length),
|
||||
timeLimit: 60,
|
||||
maxErrors: 5
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
adaptForMemoryGame(content) {
|
||||
const pairs = content.contentItems
|
||||
.filter(item => ['vocabulary', 'sentence'].includes(item.type))
|
||||
.map(item => ({
|
||||
english: item.content.english,
|
||||
french: item.content.french,
|
||||
image: item.media?.image,
|
||||
type: item.type
|
||||
}));
|
||||
|
||||
return { ...content, pairs: pairs };
|
||||
}
|
||||
|
||||
adaptForTempGames(content) {
|
||||
// Format flexible pour les mini-jeux
|
||||
return {
|
||||
...content,
|
||||
vocabulary: content.contentItems.map(item => ({
|
||||
english: item.content.english,
|
||||
french: item.content.french,
|
||||
type: item.type,
|
||||
difficulty: item.difficulty
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export global
|
||||
window.ContentEngine = ContentEngine;
|
||||
window.ContentMigrator = ContentMigrator;
|
||||
window.ContentValidator = ContentValidator;
|
||||
window.GameContentAdapter = GameContentAdapter;
|
||||
554
js/core/content-factory.js
Normal file
554
js/core/content-factory.js
Normal file
@ -0,0 +1,554 @@
|
||||
// === CONTENT FACTORY - GÉNÉRATEUR UNIVERSEL DE CONTENU ===
|
||||
|
||||
class ContentFactory {
|
||||
constructor() {
|
||||
this.parsers = new Map();
|
||||
this.generators = new Map();
|
||||
this.templates = new Map();
|
||||
this.mediaProcessor = new MediaProcessor();
|
||||
this.validator = new ContentValidator();
|
||||
|
||||
this.initializeParsers();
|
||||
this.initializeGenerators();
|
||||
this.initializeTemplates();
|
||||
}
|
||||
|
||||
// === PARSERS - ANALYSE DE CONTENU BRUT ===
|
||||
|
||||
initializeParsers() {
|
||||
// Parser pour texte libre
|
||||
this.parsers.set('text', new TextParser());
|
||||
|
||||
// Parser pour fichiers CSV
|
||||
this.parsers.set('csv', new CSVParser());
|
||||
|
||||
// Parser pour JSON structuré
|
||||
this.parsers.set('json', new JSONParser());
|
||||
|
||||
// Parser pour dialogue/script
|
||||
this.parsers.set('dialogue', new DialogueParser());
|
||||
|
||||
// Parser pour séquences/histoires
|
||||
this.parsers.set('sequence', new SequenceParser());
|
||||
|
||||
// Parser pour média (audio/image)
|
||||
this.parsers.set('media', new MediaParser());
|
||||
}
|
||||
|
||||
// === GÉNÉRATEURS - CRÉATION D'EXERCICES ===
|
||||
|
||||
initializeGenerators() {
|
||||
// Générateur de vocabulaire
|
||||
this.generators.set('vocabulary', new VocabularyGenerator());
|
||||
|
||||
// Générateur de phrases
|
||||
this.generators.set('sentence', new SentenceGenerator());
|
||||
|
||||
// Générateur de dialogues
|
||||
this.generators.set('dialogue', new DialogueGenerator());
|
||||
|
||||
// Générateur de séquences
|
||||
this.generators.set('sequence', new SequenceGenerator());
|
||||
|
||||
// Générateur de scénarios
|
||||
this.generators.set('scenario', new ScenarioGenerator());
|
||||
|
||||
// Générateur automatique (détection de type)
|
||||
this.generators.set('auto', new AutoGenerator());
|
||||
}
|
||||
|
||||
// === TEMPLATES - MODÈLES DE CONTENU ===
|
||||
|
||||
initializeTemplates() {
|
||||
this.templates.set('vocabulary_simple', {
|
||||
name: 'Vocabulaire Simple',
|
||||
description: 'Mots avec traduction',
|
||||
requiredFields: ['english', 'french'],
|
||||
optionalFields: ['image', 'audio', 'phonetic', 'category'],
|
||||
interactions: ['click', 'drag_drop', 'type'],
|
||||
games: ['whack-a-mole', 'memory-game', 'temp-games']
|
||||
});
|
||||
|
||||
this.templates.set('dialogue_conversation', {
|
||||
name: 'Dialogue Conversationnel',
|
||||
description: 'Conversation entre personnages',
|
||||
requiredFields: ['speakers', 'conversation'],
|
||||
optionalFields: ['scenario', 'context', 'audio_files'],
|
||||
interactions: ['role_play', 'click', 'build_sentence'],
|
||||
games: ['story-builder', 'temp-games']
|
||||
});
|
||||
|
||||
this.templates.set('sequence_story', {
|
||||
name: 'Histoire Séquentielle',
|
||||
description: 'Étapes chronologiques',
|
||||
requiredFields: ['title', 'steps'],
|
||||
optionalFields: ['images', 'times', 'context'],
|
||||
interactions: ['chronological_order', 'drag_drop'],
|
||||
games: ['story-builder', 'memory-game']
|
||||
});
|
||||
|
||||
this.templates.set('scenario_context', {
|
||||
name: 'Scénario Contextuel',
|
||||
description: 'Situation réelle complexe',
|
||||
requiredFields: ['setting', 'vocabulary', 'phrases'],
|
||||
optionalFields: ['roles', 'objectives', 'media'],
|
||||
interactions: ['simulation', 'role_play', 'click'],
|
||||
games: ['story-builder', 'temp-games']
|
||||
});
|
||||
}
|
||||
|
||||
// === MÉTHODE PRINCIPALE - CRÉATION DE CONTENU ===
|
||||
|
||||
async createContent(input, options = {}) {
|
||||
try {
|
||||
console.log('🏭 Content Factory - Début création contenu');
|
||||
|
||||
// 1. Analyser l'input
|
||||
const parsedContent = await this.parseInput(input, options);
|
||||
|
||||
// 2. Détecter le type de contenu
|
||||
const contentType = this.detectContentType(parsedContent, options);
|
||||
|
||||
// 3. Générer les exercices
|
||||
const exercises = await this.generateExercises(parsedContent, contentType, options);
|
||||
|
||||
// 4. Traiter les médias
|
||||
const processedMedia = await this.processMedia(parsedContent.media || [], options);
|
||||
|
||||
// 5. Assembler le module final
|
||||
const contentModule = this.assembleModule(exercises, processedMedia, options);
|
||||
|
||||
// 6. Valider le résultat
|
||||
if (!this.validator.validate(contentModule)) {
|
||||
throw new Error('Contenu généré invalide');
|
||||
}
|
||||
|
||||
console.log('✅ Content Factory - Contenu créé avec succès');
|
||||
return contentModule;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Content Factory - Erreur:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// === PARSING - ANALYSE DES INPUTS ===
|
||||
|
||||
async parseInput(input, options) {
|
||||
const inputType = this.detectInputType(input, options);
|
||||
const parser = this.parsers.get(inputType);
|
||||
|
||||
if (!parser) {
|
||||
throw new Error(`Parser non trouvé pour le type: ${inputType}`);
|
||||
}
|
||||
|
||||
return await parser.parse(input, options);
|
||||
}
|
||||
|
||||
detectInputType(input, options) {
|
||||
// Type explicite fourni
|
||||
if (options.inputType) {
|
||||
return options.inputType;
|
||||
}
|
||||
|
||||
// Détection automatique
|
||||
if (typeof input === 'string') {
|
||||
if (input.includes(',') && input.includes('=')) {
|
||||
return 'csv';
|
||||
}
|
||||
if (input.includes(':') && (input.includes('A:') || input.includes('B:'))) {
|
||||
return 'dialogue';
|
||||
}
|
||||
if (input.includes('1.') || input.includes('First') || input.includes('Then')) {
|
||||
return 'sequence';
|
||||
}
|
||||
return 'text';
|
||||
}
|
||||
|
||||
if (typeof input === 'object') {
|
||||
if (input.contentItems) return 'json';
|
||||
if (input.conversation) return 'dialogue';
|
||||
if (input.steps) return 'sequence';
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
if (input[0]?.english && input[0]?.french) return 'vocabulary';
|
||||
if (input[0]?.speaker) return 'dialogue';
|
||||
if (input[0]?.order || input[0]?.step) return 'sequence';
|
||||
}
|
||||
|
||||
return 'text'; // Fallback
|
||||
}
|
||||
|
||||
detectContentType(parsedContent, options) {
|
||||
// Type explicite
|
||||
if (options.contentType) {
|
||||
return options.contentType;
|
||||
}
|
||||
|
||||
// Détection basée sur la structure
|
||||
if (parsedContent.vocabulary && parsedContent.vocabulary.length > 0) {
|
||||
return 'vocabulary';
|
||||
}
|
||||
|
||||
if (parsedContent.conversation || parsedContent.dialogue) {
|
||||
return 'dialogue';
|
||||
}
|
||||
|
||||
if (parsedContent.steps || parsedContent.sequence) {
|
||||
return 'sequence';
|
||||
}
|
||||
|
||||
if (parsedContent.scenario || parsedContent.setting) {
|
||||
return 'scenario';
|
||||
}
|
||||
|
||||
if (parsedContent.sentences) {
|
||||
return 'sentence';
|
||||
}
|
||||
|
||||
return 'auto'; // Génération automatique
|
||||
}
|
||||
|
||||
// === GÉNÉRATION D'EXERCICES ===
|
||||
|
||||
async generateExercises(parsedContent, contentType, options) {
|
||||
const generator = this.generators.get(contentType);
|
||||
|
||||
if (!generator) {
|
||||
throw new Error(`Générateur non trouvé pour le type: ${contentType}`);
|
||||
}
|
||||
|
||||
return await generator.generate(parsedContent, options);
|
||||
}
|
||||
|
||||
// === ASSEMBLAGE DU MODULE ===
|
||||
|
||||
assembleModule(exercises, media, options) {
|
||||
const moduleId = options.id || this.generateId();
|
||||
const moduleName = options.name || 'Contenu Généré';
|
||||
|
||||
return {
|
||||
id: moduleId,
|
||||
name: moduleName,
|
||||
description: options.description || `Contenu généré automatiquement - ${new Date().toLocaleDateString()}`,
|
||||
version: "2.0",
|
||||
format: "unified",
|
||||
difficulty: options.difficulty || this.inferDifficulty(exercises),
|
||||
|
||||
metadata: {
|
||||
totalItems: exercises.length,
|
||||
categories: this.extractCategories(exercises),
|
||||
contentTypes: this.extractContentTypes(exercises),
|
||||
generatedAt: new Date().toISOString(),
|
||||
sourceType: options.inputType || 'auto-detected'
|
||||
},
|
||||
|
||||
config: {
|
||||
defaultInteraction: options.defaultInteraction || "click",
|
||||
supportedGames: options.supportedGames || ["whack-a-mole", "memory-game", "temp-games", "story-builder"],
|
||||
adaptiveEnabled: true,
|
||||
difficultyProgression: true
|
||||
},
|
||||
|
||||
contentItems: exercises,
|
||||
|
||||
media: media,
|
||||
|
||||
categories: this.generateCategories(exercises),
|
||||
|
||||
gameSettings: this.generateGameSettings(exercises, options),
|
||||
|
||||
// Métadonnées de génération
|
||||
generation: {
|
||||
timestamp: Date.now(),
|
||||
factory_version: "1.0",
|
||||
options: options,
|
||||
stats: {
|
||||
parsing_time: 0, // À implémenter
|
||||
generation_time: 0,
|
||||
total_time: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === UTILITAIRES ===
|
||||
|
||||
generateId() {
|
||||
return 'generated_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
|
||||
}
|
||||
|
||||
inferDifficulty(exercises) {
|
||||
// Analyser la complexité des exercices
|
||||
const complexities = exercises.map(ex => {
|
||||
if (ex.type === 'vocabulary') return 1;
|
||||
if (ex.type === 'sentence') return 2;
|
||||
if (ex.type === 'dialogue') return 3;
|
||||
if (ex.type === 'scenario') return 4;
|
||||
return 2;
|
||||
});
|
||||
|
||||
const avgComplexity = complexities.reduce((a, b) => a + b, 0) / complexities.length;
|
||||
|
||||
if (avgComplexity <= 1.5) return 'easy';
|
||||
if (avgComplexity <= 2.5) return 'medium';
|
||||
return 'hard';
|
||||
}
|
||||
|
||||
extractCategories(exercises) {
|
||||
const categories = new Set();
|
||||
exercises.forEach(ex => {
|
||||
if (ex.category) categories.add(ex.category);
|
||||
if (ex.content?.tags) {
|
||||
ex.content.tags.forEach(tag => categories.add(tag));
|
||||
}
|
||||
});
|
||||
return Array.from(categories);
|
||||
}
|
||||
|
||||
extractContentTypes(exercises) {
|
||||
const types = new Set();
|
||||
exercises.forEach(ex => types.add(ex.type));
|
||||
return Array.from(types);
|
||||
}
|
||||
|
||||
generateCategories(exercises) {
|
||||
const categories = {};
|
||||
const categoryGroups = this.groupByCategory(exercises);
|
||||
|
||||
Object.keys(categoryGroups).forEach(cat => {
|
||||
categories[cat] = {
|
||||
name: this.beautifyCategory(cat),
|
||||
icon: this.getCategoryIcon(cat),
|
||||
description: `Contenu généré pour ${cat}`,
|
||||
difficulty: this.inferCategoryDifficulty(categoryGroups[cat]),
|
||||
estimatedTime: Math.ceil(categoryGroups[cat].length * 1.5)
|
||||
};
|
||||
});
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
generateGameSettings(exercises, options) {
|
||||
const types = this.extractContentTypes(exercises);
|
||||
|
||||
return {
|
||||
whackAMole: {
|
||||
recommendedWords: Math.min(15, exercises.length),
|
||||
timeLimit: 60,
|
||||
maxErrors: 5,
|
||||
supportedTypes: types.filter(t => ['vocabulary', 'sentence'].includes(t))
|
||||
},
|
||||
memoryGame: {
|
||||
recommendedPairs: Math.min(8, Math.floor(exercises.length / 2)),
|
||||
timeLimit: 120,
|
||||
maxFlips: 30,
|
||||
supportedTypes: types
|
||||
},
|
||||
storyBuilder: {
|
||||
recommendedScenes: Math.min(6, exercises.length),
|
||||
timeLimit: 180,
|
||||
supportedTypes: types.filter(t => ['dialogue', 'sequence', 'scenario'].includes(t))
|
||||
},
|
||||
tempGames: {
|
||||
recommendedItems: Math.min(10, exercises.length),
|
||||
timeLimit: 90,
|
||||
supportedTypes: types
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// === API PUBLIQUE SIMPLIFIÉE ===
|
||||
|
||||
// Créer contenu depuis texte libre
|
||||
async fromText(text, options = {}) {
|
||||
return this.createContent(text, { ...options, inputType: 'text' });
|
||||
}
|
||||
|
||||
// Créer contenu depuis liste vocabulaire
|
||||
async fromVocabulary(words, options = {}) {
|
||||
return this.createContent(words, { ...options, contentType: 'vocabulary' });
|
||||
}
|
||||
|
||||
// Créer contenu depuis dialogue
|
||||
async fromDialogue(dialogue, options = {}) {
|
||||
return this.createContent(dialogue, { ...options, contentType: 'dialogue' });
|
||||
}
|
||||
|
||||
// Créer contenu depuis séquence
|
||||
async fromSequence(steps, options = {}) {
|
||||
return this.createContent(steps, { ...options, contentType: 'sequence' });
|
||||
}
|
||||
|
||||
// Créer contenu avec médias
|
||||
async fromMediaBundle(content, mediaFiles, options = {}) {
|
||||
return this.createContent(content, {
|
||||
...options,
|
||||
media: mediaFiles,
|
||||
processMedia: true
|
||||
});
|
||||
}
|
||||
|
||||
// === TEMPLATE HELPERS ===
|
||||
|
||||
getAvailableTemplates() {
|
||||
return Array.from(this.templates.entries()).map(([key, template]) => ({
|
||||
id: key,
|
||||
...template
|
||||
}));
|
||||
}
|
||||
|
||||
async fromTemplate(templateId, data, options = {}) {
|
||||
const template = this.templates.get(templateId);
|
||||
if (!template) {
|
||||
throw new Error(`Template non trouvé: ${templateId}`);
|
||||
}
|
||||
|
||||
// Valider les données selon le template
|
||||
this.validateTemplateData(data, template);
|
||||
|
||||
return this.createContent(data, {
|
||||
...options,
|
||||
template: template,
|
||||
requiredFields: template.requiredFields
|
||||
});
|
||||
}
|
||||
|
||||
validateTemplateData(data, template) {
|
||||
for (const field of template.requiredFields) {
|
||||
if (!data[field]) {
|
||||
throw new Error(`Champ requis manquant: ${field}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
groupByCategory(exercises) {
|
||||
const groups = {};
|
||||
exercises.forEach(ex => {
|
||||
const cat = ex.category || 'general';
|
||||
if (!groups[cat]) groups[cat] = [];
|
||||
groups[cat].push(ex);
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
beautifyCategory(category) {
|
||||
const beautified = {
|
||||
'family': 'Famille',
|
||||
'animals': 'Animaux',
|
||||
'colors': 'Couleurs',
|
||||
'numbers': 'Nombres',
|
||||
'food': 'Nourriture',
|
||||
'school': 'École',
|
||||
'daily': 'Quotidien',
|
||||
'greetings': 'Salutations'
|
||||
};
|
||||
return beautified[category] || category.charAt(0).toUpperCase() + category.slice(1);
|
||||
}
|
||||
|
||||
getCategoryIcon(category) {
|
||||
const icons = {
|
||||
'family': '👨👩👧👦',
|
||||
'animals': '🐱',
|
||||
'colors': '🎨',
|
||||
'numbers': '🔢',
|
||||
'food': '🍎',
|
||||
'school': '📚',
|
||||
'daily': '🌅',
|
||||
'greetings': '👋',
|
||||
'general': '📝'
|
||||
};
|
||||
return icons[category] || '📝';
|
||||
}
|
||||
|
||||
inferCategoryDifficulty(exercises) {
|
||||
const difficulties = exercises.map(ex => {
|
||||
switch(ex.difficulty) {
|
||||
case 'easy': return 1;
|
||||
case 'medium': return 2;
|
||||
case 'hard': return 3;
|
||||
default: return 2;
|
||||
}
|
||||
});
|
||||
|
||||
const avg = difficulties.reduce((a, b) => a + b, 0) / difficulties.length;
|
||||
|
||||
if (avg <= 1.3) return 'easy';
|
||||
if (avg <= 2.3) return 'medium';
|
||||
return 'hard';
|
||||
}
|
||||
}
|
||||
|
||||
// === PROCESSEUR DE MÉDIAS ===
|
||||
|
||||
class MediaProcessor {
|
||||
constructor() {
|
||||
this.supportedAudioFormats = ['mp3', 'wav', 'ogg'];
|
||||
this.supportedImageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
}
|
||||
|
||||
async processMedia(mediaFiles, options = {}) {
|
||||
const processedMedia = {
|
||||
audio: {},
|
||||
images: {},
|
||||
metadata: {
|
||||
totalFiles: mediaFiles.length,
|
||||
processedAt: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
for (const file of mediaFiles) {
|
||||
try {
|
||||
const processed = await this.processFile(file, options);
|
||||
|
||||
if (this.isAudioFile(file.name)) {
|
||||
processedMedia.audio[file.id || file.name] = processed;
|
||||
} else if (this.isImageFile(file.name)) {
|
||||
processedMedia.images[file.id || file.name] = processed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Erreur traitement fichier ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return processedMedia;
|
||||
}
|
||||
|
||||
async processFile(file, options) {
|
||||
// Dans un environnement réel, ici on ferait :
|
||||
// - Validation du format
|
||||
// - Optimisation (compression, resize)
|
||||
// - Upload vers CDN
|
||||
// - Génération de thumbnails
|
||||
// - Extraction de métadonnées
|
||||
|
||||
return {
|
||||
originalName: file.name,
|
||||
path: file.path || `assets/${this.getFileCategory(file.name)}/${file.name}`,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
processedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
isAudioFile(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
return this.supportedAudioFormats.includes(ext);
|
||||
}
|
||||
|
||||
isImageFile(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
return this.supportedImageFormats.includes(ext);
|
||||
}
|
||||
|
||||
getFileCategory(filename) {
|
||||
return this.isAudioFile(filename) ? 'sounds' : 'images';
|
||||
}
|
||||
}
|
||||
|
||||
// Export global
|
||||
window.ContentFactory = ContentFactory;
|
||||
window.MediaProcessor = MediaProcessor;
|
||||
864
js/core/content-generators.js
Normal file
864
js/core/content-generators.js
Normal file
@ -0,0 +1,864 @@
|
||||
// === GÉNÉRATEURS AUTOMATIQUES D'EXERCICES ===
|
||||
|
||||
// === GÉNÉRATEUR DE VOCABULAIRE ===
|
||||
class VocabularyGenerator {
|
||||
async generate(parsedContent, options = {}) {
|
||||
console.log('📚 VocabularyGenerator - Génération exercices vocabulaire');
|
||||
|
||||
const exercises = [];
|
||||
const vocabulary = parsedContent.vocabulary || [];
|
||||
|
||||
vocabulary.forEach((word, index) => {
|
||||
const exercise = this.createVocabularyExercise(word, index, options);
|
||||
exercises.push(exercise);
|
||||
});
|
||||
|
||||
// Générer des exercices supplémentaires si demandé
|
||||
if (options.generateVariations) {
|
||||
exercises.push(...this.generateVariations(vocabulary, options));
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
createVocabularyExercise(word, index, options) {
|
||||
return {
|
||||
id: `vocab_${index + 1}`,
|
||||
type: 'vocabulary',
|
||||
difficulty: this.inferDifficulty(word),
|
||||
category: word.category || 'general',
|
||||
|
||||
content: {
|
||||
english: word.english,
|
||||
french: word.french,
|
||||
phonetic: word.phonetic || this.generatePhonetic(word.english),
|
||||
context: word.context || word.category || '',
|
||||
tags: this.generateTags(word)
|
||||
},
|
||||
|
||||
media: {
|
||||
image: word.image || null,
|
||||
audio: word.audio || null,
|
||||
icon: this.getIconForWord(word.english, word.category)
|
||||
},
|
||||
|
||||
pedagogy: {
|
||||
learningObjective: `Apprendre le mot "${word.english}"`,
|
||||
prerequisites: [],
|
||||
followUp: this.suggestFollowUp(word),
|
||||
grammarFocus: 'vocabulary'
|
||||
},
|
||||
|
||||
interaction: {
|
||||
type: options.interactionType || this.selectBestInteraction(word),
|
||||
validation: 'exact',
|
||||
hints: this.generateHints(word),
|
||||
feedback: {
|
||||
correct: `Parfait ! "${word.english}" = "${word.french}" ${this.getIconForWord(word.english)}`,
|
||||
incorrect: `Non, "${word.english}" signifie "${word.french}"`
|
||||
},
|
||||
alternatives: this.generateAlternatives(word)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
inferDifficulty(word) {
|
||||
const length = word.english.length;
|
||||
const complexity = this.calculateComplexity(word.english);
|
||||
|
||||
if (length <= 4 && complexity < 2) return 'easy';
|
||||
if (length <= 8 && complexity < 3) return 'medium';
|
||||
return 'hard';
|
||||
}
|
||||
|
||||
calculateComplexity(word) {
|
||||
let complexity = 0;
|
||||
if (word.includes('th')) complexity++;
|
||||
if (word.includes('gh')) complexity++;
|
||||
if (word.match(/[aeiou]{2,}/)) complexity++;
|
||||
if (word.split('').some(c => 'xyz'.includes(c))) complexity++;
|
||||
return complexity;
|
||||
}
|
||||
|
||||
generateTags(word) {
|
||||
const tags = [word.category || 'general'];
|
||||
|
||||
// Tags basés sur la longueur
|
||||
if (word.english.length <= 4) tags.push('short');
|
||||
else if (word.english.length >= 8) tags.push('long');
|
||||
|
||||
// Tags basés sur la catégorie
|
||||
if (word.category) {
|
||||
tags.push(word.category);
|
||||
if (['cat', 'dog', 'bird'].includes(word.english)) tags.push('pet');
|
||||
if (['red', 'blue', 'green'].includes(word.english)) tags.push('color');
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
getIconForWord(word, category) {
|
||||
const wordIcons = {
|
||||
cat: '🐱', dog: '🐕', bird: '🐦', fish: '🐠',
|
||||
apple: '🍎', banana: '🍌', orange: '🍊',
|
||||
car: '🚗', house: '🏠', school: '🏫',
|
||||
book: '📚', pen: '✏️', pencil: '✏️'
|
||||
};
|
||||
|
||||
if (wordIcons[word.toLowerCase()]) {
|
||||
return wordIcons[word.toLowerCase()];
|
||||
}
|
||||
|
||||
const categoryIcons = {
|
||||
animals: '🐾', food: '🍎', transport: '🚗',
|
||||
family: '👨👩👧👦', colors: '🎨', numbers: '🔢'
|
||||
};
|
||||
|
||||
return categoryIcons[category] || '📝';
|
||||
}
|
||||
|
||||
selectBestInteraction(word) {
|
||||
// Sélection intelligente du type d'interaction
|
||||
if (word.category === 'actions') return 'gesture';
|
||||
if (word.english.length > 8) return 'type';
|
||||
if (word.image) return 'drag_drop';
|
||||
return 'click';
|
||||
}
|
||||
|
||||
generateHints(word) {
|
||||
const hints = [];
|
||||
|
||||
// Hint basé sur la catégorie
|
||||
if (word.category === 'animals') hints.push("C'est un animal");
|
||||
if (word.category === 'food') hints.push("On peut le manger");
|
||||
if (word.category === 'colors') hints.push("C'est une couleur");
|
||||
|
||||
// Hint basé sur la longueur
|
||||
hints.push(`Ce mot a ${word.english.length} lettres`);
|
||||
|
||||
// Hint basé sur la première lettre
|
||||
hints.push(`Commence par la lettre "${word.english[0].toUpperCase()}"`);
|
||||
|
||||
return hints.slice(0, 3); // Max 3 hints
|
||||
}
|
||||
|
||||
generateAlternatives(word) {
|
||||
// Générer des alternatives plausibles pour QCM
|
||||
const alternatives = [];
|
||||
|
||||
// Pour l'instant, alternatives génériques
|
||||
// Dans une vraie implémentation, on utiliserait une base de données
|
||||
const commonWords = {
|
||||
animals: ['chien', 'oiseau', 'poisson', 'lapin'],
|
||||
food: ['pomme', 'banane', 'orange', 'pain'],
|
||||
colors: ['rouge', 'bleu', 'vert', 'jaune'],
|
||||
family: ['mère', 'père', 'frère', 'sœur']
|
||||
};
|
||||
|
||||
const categoryAlts = commonWords[word.category] || ['mot1', 'mot2', 'mot3'];
|
||||
|
||||
// Prendre 3 alternatives qui ne sont pas la bonne réponse
|
||||
return categoryAlts.filter(alt => alt !== word.french).slice(0, 3);
|
||||
}
|
||||
|
||||
suggestFollowUp(word) {
|
||||
const followUp = [];
|
||||
|
||||
if (word.category === 'animals') {
|
||||
followUp.push('pets', 'farm_animals', 'wild_animals');
|
||||
} else if (word.category === 'family') {
|
||||
followUp.push('family_relationships', 'family_activities');
|
||||
}
|
||||
|
||||
return followUp;
|
||||
}
|
||||
|
||||
generateVariations(vocabulary, options) {
|
||||
const variations = [];
|
||||
|
||||
// Générer des exercices de synonymes/antonymes
|
||||
// Générer des exercices de catégorisation
|
||||
// Générer des exercices de construction de phrases
|
||||
|
||||
return variations; // Pour l'instant vide
|
||||
}
|
||||
|
||||
generatePhonetic(word) {
|
||||
// Génération basique de phonétique
|
||||
// Dans une vraie implémentation, utiliser une API de phonétique
|
||||
const basicPhonetic = {
|
||||
cat: '/kæt/', dog: '/dɔg/', bird: '/bɜrd/',
|
||||
apple: '/ˈæpəl/', house: '/haʊs/', book: '/bʊk/'
|
||||
};
|
||||
|
||||
return basicPhonetic[word.toLowerCase()] || `/${word}/`;
|
||||
}
|
||||
}
|
||||
|
||||
// === GÉNÉRATEUR DE PHRASES ===
|
||||
class SentenceGenerator {
|
||||
async generate(parsedContent, options = {}) {
|
||||
console.log('📖 SentenceGenerator - Génération exercices phrases');
|
||||
|
||||
const exercises = [];
|
||||
const sentences = parsedContent.sentences || [];
|
||||
|
||||
sentences.forEach((sentence, index) => {
|
||||
const exercise = this.createSentenceExercise(sentence, index, options);
|
||||
exercises.push(exercise);
|
||||
});
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
createSentenceExercise(sentence, index, options) {
|
||||
return {
|
||||
id: `sent_${index + 1}`,
|
||||
type: 'sentence',
|
||||
difficulty: this.inferSentenceDifficulty(sentence),
|
||||
category: sentence.category || 'general',
|
||||
|
||||
content: {
|
||||
english: sentence.english,
|
||||
french: sentence.french || this.suggestTranslation(sentence.english),
|
||||
structure: this.analyzeSentenceStructure(sentence.english),
|
||||
context: sentence.context || 'general',
|
||||
tags: this.generateSentenceTags(sentence)
|
||||
},
|
||||
|
||||
media: {
|
||||
icon: this.getSentenceIcon(sentence)
|
||||
},
|
||||
|
||||
pedagogy: {
|
||||
learningObjective: `Comprendre et construire la phrase "${sentence.english}"`,
|
||||
prerequisites: this.extractPrerequisites(sentence.english),
|
||||
followUp: ['complex_sentences', 'questions'],
|
||||
grammarFocus: this.identifyGrammarFocus(sentence.english)
|
||||
},
|
||||
|
||||
interaction: {
|
||||
type: 'build_sentence',
|
||||
validation: 'word_order',
|
||||
hints: this.generateSentenceHints(sentence),
|
||||
feedback: {
|
||||
correct: `Excellent ! Tu as construit la phrase correctement ! ✨`,
|
||||
incorrect: `Essaie de remettre les mots dans le bon ordre`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
inferSentenceDifficulty(sentence) {
|
||||
const wordCount = sentence.english.split(' ').length;
|
||||
const structure = sentence.structure || {};
|
||||
|
||||
if (wordCount <= 4 && !structure.hasQuestion) return 'easy';
|
||||
if (wordCount <= 8) return 'medium';
|
||||
return 'hard';
|
||||
}
|
||||
|
||||
analyzeSentenceStructure(sentence) {
|
||||
return {
|
||||
words: sentence.split(' '),
|
||||
wordCount: sentence.split(' ').length,
|
||||
hasQuestion: sentence.includes('?'),
|
||||
hasExclamation: sentence.includes('!'),
|
||||
hasComma: sentence.includes(','),
|
||||
startsWithCapital: /^[A-Z]/.test(sentence),
|
||||
tense: this.detectTense(sentence)
|
||||
};
|
||||
}
|
||||
|
||||
detectTense(sentence) {
|
||||
if (sentence.includes(' am ') || sentence.includes(' is ') || sentence.includes(' are ')) {
|
||||
if (sentence.includes('ing')) return 'present_continuous';
|
||||
return 'present_simple';
|
||||
}
|
||||
if (sentence.includes(' was ') || sentence.includes(' were ')) return 'past_simple';
|
||||
if (sentence.includes(' will ')) return 'future_simple';
|
||||
return 'present_simple';
|
||||
}
|
||||
|
||||
extractPrerequisites(sentence) {
|
||||
const prerequisites = [];
|
||||
const words = sentence.toLowerCase().split(' ');
|
||||
|
||||
// Extraire les mots de vocabulaire importants
|
||||
const importantWords = words.filter(word =>
|
||||
word.length > 3 &&
|
||||
!['this', 'that', 'with', 'from', 'they', 'have', 'been'].includes(word)
|
||||
);
|
||||
|
||||
return importantWords.slice(0, 3);
|
||||
}
|
||||
|
||||
identifyGrammarFocus(sentence) {
|
||||
if (sentence.includes('?')) return 'questions';
|
||||
if (sentence.includes(' my ') || sentence.includes(' your ')) return 'possessives';
|
||||
if (sentence.includes(' is ') || sentence.includes(' are ')) return 'be_verb';
|
||||
if (sentence.includes('ing')) return 'present_continuous';
|
||||
return 'sentence_structure';
|
||||
}
|
||||
|
||||
generateSentenceTags(sentence) {
|
||||
const tags = ['sentence'];
|
||||
|
||||
if (sentence.english.includes('?')) tags.push('question');
|
||||
if (sentence.english.includes('!')) tags.push('exclamation');
|
||||
if (sentence.english.toLowerCase().includes('hello')) tags.push('greeting');
|
||||
if (sentence.english.toLowerCase().includes('my name')) tags.push('introduction');
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
getSentenceIcon(sentence) {
|
||||
if (sentence.english.includes('?')) return '❓';
|
||||
if (sentence.english.includes('!')) return '❗';
|
||||
if (sentence.english.toLowerCase().includes('hello')) return '👋';
|
||||
return '💬';
|
||||
}
|
||||
|
||||
generateSentenceHints(sentence) {
|
||||
const hints = [];
|
||||
|
||||
hints.push(`La phrase a ${sentence.english.split(' ').length} mots`);
|
||||
|
||||
if (sentence.english.includes('?')) {
|
||||
hints.push("C'est une question");
|
||||
}
|
||||
|
||||
const firstWord = sentence.english.split(' ')[0];
|
||||
hints.push(`Commence par "${firstWord}"`);
|
||||
|
||||
return hints;
|
||||
}
|
||||
|
||||
suggestTranslation(english) {
|
||||
// Traduction basique pour exemples
|
||||
const basicTranslations = {
|
||||
'Hello': 'Bonjour',
|
||||
'My name is': 'Je m\'appelle',
|
||||
'I am': 'Je suis',
|
||||
'How are you?': 'Comment allez-vous ?',
|
||||
'Thank you': 'Merci'
|
||||
};
|
||||
|
||||
for (const [en, fr] of Object.entries(basicTranslations)) {
|
||||
if (english.includes(en)) {
|
||||
return english.replace(en, fr);
|
||||
}
|
||||
}
|
||||
|
||||
return ''; // À traduire manuellement
|
||||
}
|
||||
}
|
||||
|
||||
// === GÉNÉRATEUR DE DIALOGUES ===
|
||||
class DialogueGenerator {
|
||||
async generate(parsedContent, options = {}) {
|
||||
console.log('💬 DialogueGenerator - Génération exercices dialogues');
|
||||
|
||||
const exercises = [];
|
||||
|
||||
if (parsedContent.dialogue) {
|
||||
const exercise = this.createDialogueExercise(parsedContent.dialogue, 0, options);
|
||||
exercises.push(exercise);
|
||||
}
|
||||
|
||||
if (parsedContent.conversations) {
|
||||
parsedContent.conversations.forEach((conversation, index) => {
|
||||
const exercise = this.createConversationExercise(conversation, index, options);
|
||||
exercises.push(exercise);
|
||||
});
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
createDialogueExercise(dialogue, index, options) {
|
||||
return {
|
||||
id: `dial_${index + 1}`,
|
||||
type: 'dialogue',
|
||||
difficulty: this.inferDialogueDifficulty(dialogue),
|
||||
category: dialogue.scenario || 'conversation',
|
||||
|
||||
content: {
|
||||
scenario: dialogue.scenario || 'conversation',
|
||||
english: this.extractDialogueTitle(dialogue),
|
||||
french: this.translateScenario(dialogue.scenario),
|
||||
conversation: dialogue.conversation.map(line => ({
|
||||
speaker: line.speaker,
|
||||
english: line.english || line.text,
|
||||
french: line.french || this.suggestTranslation(line.english || line.text),
|
||||
role: this.identifyRole(line)
|
||||
})),
|
||||
context: dialogue.scenario || 'general',
|
||||
tags: this.generateDialogueTags(dialogue)
|
||||
},
|
||||
|
||||
media: {
|
||||
icon: this.getDialogueIcon(dialogue.scenario)
|
||||
},
|
||||
|
||||
pedagogy: {
|
||||
learningObjective: `Participer à un dialogue : ${dialogue.scenario}`,
|
||||
prerequisites: this.extractDialoguePrerequisites(dialogue),
|
||||
followUp: ['advanced_dialogue', 'role_play'],
|
||||
grammarFocus: 'conversation_patterns'
|
||||
},
|
||||
|
||||
interaction: {
|
||||
type: 'role_play',
|
||||
validation: 'dialogue_flow',
|
||||
userRole: this.selectUserRole(dialogue),
|
||||
hints: this.generateDialogueHints(dialogue),
|
||||
feedback: {
|
||||
correct: `Parfait ! Tu maîtrises ce type de conversation ! 🎭`,
|
||||
incorrect: `Essaie de suivre le flow naturel de la conversation`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
createConversationExercise(conversation, index, options) {
|
||||
return {
|
||||
id: `conv_${index + 1}`,
|
||||
type: 'dialogue',
|
||||
difficulty: 'medium',
|
||||
category: conversation.scene || 'conversation',
|
||||
|
||||
content: {
|
||||
scenario: conversation.scene || `Conversation ${index + 1}`,
|
||||
english: `${conversation.speaker}: ${conversation.english}`,
|
||||
french: `${conversation.speaker}: ${conversation.french}`,
|
||||
conversation: [{
|
||||
speaker: conversation.speaker,
|
||||
english: conversation.english,
|
||||
french: conversation.french,
|
||||
role: 'statement'
|
||||
}],
|
||||
tags: ['conversation', 'dialogue']
|
||||
},
|
||||
|
||||
interaction: {
|
||||
type: 'click',
|
||||
validation: 'exact'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
inferDialogueDifficulty(dialogue) {
|
||||
const conversationLength = dialogue.conversation?.length || 1;
|
||||
const averageWordsPerLine = dialogue.conversation?.reduce((sum, line) =>
|
||||
sum + (line.english || line.text || '').split(' ').length, 0) / conversationLength || 5;
|
||||
|
||||
if (conversationLength <= 2 && averageWordsPerLine <= 5) return 'easy';
|
||||
if (conversationLength <= 4 && averageWordsPerLine <= 8) return 'medium';
|
||||
return 'hard';
|
||||
}
|
||||
|
||||
extractDialogueTitle(dialogue) {
|
||||
if (dialogue.title) return dialogue.title;
|
||||
if (dialogue.scenario) return this.beautifyScenario(dialogue.scenario);
|
||||
return 'Dialogue';
|
||||
}
|
||||
|
||||
beautifyScenario(scenario) {
|
||||
const scenarios = {
|
||||
'greeting': 'Salutations',
|
||||
'restaurant': 'Au Restaurant',
|
||||
'shopping': 'Faire les Courses',
|
||||
'school': 'À l\'École',
|
||||
'family': 'En Famille'
|
||||
};
|
||||
|
||||
return scenarios[scenario] || scenario;
|
||||
}
|
||||
|
||||
translateScenario(scenario) {
|
||||
const translations = {
|
||||
'greeting': 'Se saluer',
|
||||
'restaurant': 'Commander au restaurant',
|
||||
'shopping': 'Acheter quelque chose',
|
||||
'school': 'Parler à l\'école',
|
||||
'family': 'Conversation familiale'
|
||||
};
|
||||
|
||||
return translations[scenario] || scenario;
|
||||
}
|
||||
|
||||
identifyRole(line) {
|
||||
const text = (line.english || line.text || '').toLowerCase();
|
||||
|
||||
if (text.includes('?')) return 'question';
|
||||
if (text.includes('hello') || text.includes('hi')) return 'greeting';
|
||||
if (text.includes('thank')) return 'thanks';
|
||||
if (text.includes('goodbye') || text.includes('bye')) return 'farewell';
|
||||
return 'statement';
|
||||
}
|
||||
|
||||
generateDialogueTags(dialogue) {
|
||||
const tags = ['dialogue', 'conversation'];
|
||||
|
||||
if (dialogue.scenario) tags.push(dialogue.scenario);
|
||||
|
||||
const hasQuestion = dialogue.conversation?.some(line =>
|
||||
(line.english || line.text || '').includes('?'));
|
||||
if (hasQuestion) tags.push('questions');
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
getDialogueIcon(scenario) {
|
||||
const icons = {
|
||||
'greeting': '👋',
|
||||
'restaurant': '🍽️',
|
||||
'shopping': '🛒',
|
||||
'school': '🎒',
|
||||
'family': '👨👩👧👦',
|
||||
'friends': '👫'
|
||||
};
|
||||
|
||||
return icons[scenario] || '💬';
|
||||
}
|
||||
|
||||
selectUserRole(dialogue) {
|
||||
// Sélectionner le rôle que l'utilisateur va jouer
|
||||
const speakers = dialogue.conversation?.map(line => line.speaker) || [];
|
||||
const uniqueSpeakers = [...new Set(speakers)];
|
||||
|
||||
// Privilégier certains rôles
|
||||
if (uniqueSpeakers.includes('student')) return 'student';
|
||||
if (uniqueSpeakers.includes('child')) return 'child';
|
||||
if (uniqueSpeakers.includes('customer')) return 'customer';
|
||||
|
||||
// Sinon, prendre le premier
|
||||
return uniqueSpeakers[0] || 'person1';
|
||||
}
|
||||
|
||||
extractDialoguePrerequisites(dialogue) {
|
||||
const prerequisites = new Set();
|
||||
|
||||
dialogue.conversation?.forEach(line => {
|
||||
const text = line.english || line.text || '';
|
||||
const words = text.toLowerCase().split(' ');
|
||||
|
||||
// Extraire mots importants
|
||||
words.forEach(word => {
|
||||
if (word.length > 3 && !['this', 'that', 'with', 'they', 'have'].includes(word)) {
|
||||
prerequisites.add(word);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(prerequisites).slice(0, 5);
|
||||
}
|
||||
|
||||
generateDialogueHints(dialogue) {
|
||||
const hints = [];
|
||||
|
||||
hints.push(`Cette conversation a ${dialogue.conversation?.length || 1} répliques`);
|
||||
|
||||
if (dialogue.scenario) {
|
||||
hints.push(`Le contexte est : ${dialogue.scenario}`);
|
||||
}
|
||||
|
||||
const hasQuestions = dialogue.conversation?.some(line =>
|
||||
(line.english || line.text || '').includes('?'));
|
||||
if (hasQuestions) {
|
||||
hints.push('Il y a des questions dans cette conversation');
|
||||
}
|
||||
|
||||
return hints;
|
||||
}
|
||||
|
||||
suggestTranslation(english) {
|
||||
// Utiliser le même système que SentenceGenerator
|
||||
return new SentenceGenerator().suggestTranslation(english);
|
||||
}
|
||||
}
|
||||
|
||||
// === GÉNÉRATEUR DE SÉQUENCES ===
|
||||
class SequenceGenerator {
|
||||
async generate(parsedContent, options = {}) {
|
||||
console.log('📋 SequenceGenerator - Génération exercices séquences');
|
||||
|
||||
const exercises = [];
|
||||
|
||||
if (parsedContent.sequence) {
|
||||
const exercise = this.createSequenceExercise(parsedContent.sequence, 0, options);
|
||||
exercises.push(exercise);
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
createSequenceExercise(sequence, index, options) {
|
||||
return {
|
||||
id: `seq_${index + 1}`,
|
||||
type: 'sequence',
|
||||
difficulty: this.inferSequenceDifficulty(sequence),
|
||||
category: this.inferSequenceCategory(sequence),
|
||||
|
||||
content: {
|
||||
english: sequence.title || 'Sequence',
|
||||
french: this.translateSequenceTitle(sequence.title),
|
||||
title: sequence.title || 'Sequence',
|
||||
steps: sequence.steps.map(step => ({
|
||||
order: step.order,
|
||||
english: step.english,
|
||||
french: step.french || this.suggestStepTranslation(step.english),
|
||||
icon: this.getStepIcon(step.english),
|
||||
time: this.extractTime(step)
|
||||
})),
|
||||
context: this.inferContext(sequence),
|
||||
tags: this.generateSequenceTags(sequence)
|
||||
},
|
||||
|
||||
media: {
|
||||
icon: this.getSequenceIcon(sequence)
|
||||
},
|
||||
|
||||
pedagogy: {
|
||||
learningObjective: `Comprendre l'ordre chronologique : ${sequence.title}`,
|
||||
prerequisites: this.extractSequencePrerequisites(sequence),
|
||||
followUp: ['time_expressions', 'daily_routine'],
|
||||
grammarFocus: 'sequence_connectors'
|
||||
},
|
||||
|
||||
interaction: {
|
||||
type: 'chronological_order',
|
||||
validation: 'sequence_correct',
|
||||
hints: this.generateSequenceHints(sequence),
|
||||
feedback: {
|
||||
correct: `Parfait ! Tu connais l'ordre correct ! ⏰`,
|
||||
incorrect: `Réfléchis à l'ordre logique des étapes`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
inferSequenceDifficulty(sequence) {
|
||||
const stepCount = sequence.steps?.length || 1;
|
||||
const avgWordsPerStep = sequence.steps?.reduce((sum, step) =>
|
||||
sum + step.english.split(' ').length, 0) / stepCount || 3;
|
||||
|
||||
if (stepCount <= 3 && avgWordsPerStep <= 4) return 'easy';
|
||||
if (stepCount <= 5 && avgWordsPerStep <= 6) return 'medium';
|
||||
return 'hard';
|
||||
}
|
||||
|
||||
inferSequenceCategory(sequence) {
|
||||
const title = sequence.title?.toLowerCase() || '';
|
||||
const allText = sequence.steps?.map(s => s.english.toLowerCase()).join(' ') || '';
|
||||
|
||||
if (title.includes('morning') || allText.includes('wake up') || allText.includes('breakfast')) {
|
||||
return 'daily_routine';
|
||||
}
|
||||
if (title.includes('recipe') || allText.includes('cook') || allText.includes('mix')) {
|
||||
return 'cooking';
|
||||
}
|
||||
if (title.includes('story') || allText.includes('once upon')) {
|
||||
return 'story';
|
||||
}
|
||||
|
||||
return 'sequence';
|
||||
}
|
||||
|
||||
translateSequenceTitle(title) {
|
||||
const translations = {
|
||||
'Morning Routine': 'Routine du Matin',
|
||||
'Getting Ready': 'Se Préparer',
|
||||
'Cooking Recipe': 'Recette de Cuisine',
|
||||
'Story': 'Histoire'
|
||||
};
|
||||
|
||||
return translations[title] || title;
|
||||
}
|
||||
|
||||
suggestStepTranslation(english) {
|
||||
const stepTranslations = {
|
||||
'Wake up': 'Se réveiller',
|
||||
'Get dressed': 'S\'habiller',
|
||||
'Eat breakfast': 'Prendre le petit-déjeuner',
|
||||
'Go to school': 'Aller à l\'école',
|
||||
'Brush teeth': 'Se brosser les dents'
|
||||
};
|
||||
|
||||
return stepTranslations[english] || '';
|
||||
}
|
||||
|
||||
getStepIcon(stepText) {
|
||||
const icons = {
|
||||
'wake up': '⏰', 'get dressed': '👕', 'breakfast': '🥞',
|
||||
'school': '🎒', 'brush': '🪥', 'wash': '🧼',
|
||||
'cook': '🍳', 'mix': '🥄', 'bake': '🔥'
|
||||
};
|
||||
|
||||
const lowerText = stepText.toLowerCase();
|
||||
|
||||
for (const [key, icon] of Object.entries(icons)) {
|
||||
if (lowerText.includes(key)) return icon;
|
||||
}
|
||||
|
||||
return '📝';
|
||||
}
|
||||
|
||||
extractTime(step) {
|
||||
// Extraire indication de temps du texte
|
||||
const timeMatch = step.english.match(/\d{1,2}:\d{2}|\d{1,2}(am|pm)/i);
|
||||
return timeMatch ? timeMatch[0] : null;
|
||||
}
|
||||
|
||||
inferContext(sequence) {
|
||||
const category = this.inferSequenceCategory(sequence);
|
||||
|
||||
const contexts = {
|
||||
'daily_routine': 'routine quotidienne',
|
||||
'cooking': 'cuisine',
|
||||
'story': 'histoire',
|
||||
'sequence': 'étapes'
|
||||
};
|
||||
|
||||
return contexts[category] || 'séquence';
|
||||
}
|
||||
|
||||
generateSequenceTags(sequence) {
|
||||
const tags = ['sequence', 'chronological'];
|
||||
|
||||
const category = this.inferSequenceCategory(sequence);
|
||||
tags.push(category);
|
||||
|
||||
if (sequence.steps?.some(step => this.extractTime(step))) {
|
||||
tags.push('time');
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
getSequenceIcon(sequence) {
|
||||
const category = this.inferSequenceCategory(sequence);
|
||||
|
||||
const icons = {
|
||||
'daily_routine': '🌅',
|
||||
'cooking': '👨🍳',
|
||||
'story': '📖',
|
||||
'sequence': '📋'
|
||||
};
|
||||
|
||||
return icons[category] || '📋';
|
||||
}
|
||||
|
||||
extractSequencePrerequisites(sequence) {
|
||||
const prerequisites = new Set();
|
||||
|
||||
sequence.steps?.forEach(step => {
|
||||
const words = step.english.toLowerCase().split(' ');
|
||||
|
||||
// Verbes d'action importants
|
||||
const actionVerbs = words.filter(word =>
|
||||
['wake', 'get', 'eat', 'go', 'brush', 'wash', 'cook', 'mix'].some(verb =>
|
||||
word.includes(verb)
|
||||
)
|
||||
);
|
||||
|
||||
actionVerbs.forEach(verb => prerequisites.add(verb));
|
||||
});
|
||||
|
||||
return Array.from(prerequisites).slice(0, 4);
|
||||
}
|
||||
|
||||
generateSequenceHints(sequence) {
|
||||
const hints = [];
|
||||
|
||||
hints.push(`Cette séquence a ${sequence.steps?.length || 0} étapes`);
|
||||
|
||||
const category = this.inferSequenceCategory(sequence);
|
||||
if (category === 'daily_routine') {
|
||||
hints.push('Pense à ton ordre habituel du matin');
|
||||
} else if (category === 'cooking') {
|
||||
hints.push('Quelle est la logique de la recette ?');
|
||||
}
|
||||
|
||||
const firstStep = sequence.steps?.[0];
|
||||
if (firstStep) {
|
||||
hints.push(`La première étape est : "${firstStep.english}"`);
|
||||
}
|
||||
|
||||
return hints;
|
||||
}
|
||||
}
|
||||
|
||||
// === GÉNÉRATEUR AUTOMATIQUE ===
|
||||
class AutoGenerator {
|
||||
async generate(parsedContent, options = {}) {
|
||||
console.log('🤖 AutoGenerator - Génération automatique intelligente');
|
||||
|
||||
const exercises = [];
|
||||
|
||||
// Détecter et générer selon ce qui est disponible
|
||||
if (parsedContent.vocabulary?.length > 0) {
|
||||
const vocabGen = new VocabularyGenerator();
|
||||
const vocabExercises = await vocabGen.generate(parsedContent, options);
|
||||
exercises.push(...vocabExercises);
|
||||
}
|
||||
|
||||
if (parsedContent.sentences?.length > 0) {
|
||||
const sentGen = new SentenceGenerator();
|
||||
const sentExercises = await sentGen.generate(parsedContent, options);
|
||||
exercises.push(...sentExercises);
|
||||
}
|
||||
|
||||
if (parsedContent.dialogue) {
|
||||
const dialogGen = new DialogueGenerator();
|
||||
const dialogExercises = await dialogGen.generate(parsedContent, options);
|
||||
exercises.push(...dialogExercises);
|
||||
}
|
||||
|
||||
if (parsedContent.sequence) {
|
||||
const seqGen = new SequenceGenerator();
|
||||
const seqExercises = await seqGen.generate(parsedContent, options);
|
||||
exercises.push(...seqExercises);
|
||||
}
|
||||
|
||||
// Si rien de spécifique n'a été détecté, créer du contenu basique
|
||||
if (exercises.length === 0) {
|
||||
exercises.push(this.createFallbackExercise(parsedContent, options));
|
||||
}
|
||||
|
||||
return exercises;
|
||||
}
|
||||
|
||||
createFallbackExercise(parsedContent, options) {
|
||||
return {
|
||||
id: 'auto_001',
|
||||
type: 'vocabulary',
|
||||
difficulty: 'easy',
|
||||
category: 'general',
|
||||
|
||||
content: {
|
||||
english: 'text',
|
||||
french: 'texte',
|
||||
context: 'Contenu généré automatiquement',
|
||||
tags: ['auto-generated']
|
||||
},
|
||||
|
||||
interaction: {
|
||||
type: 'click',
|
||||
validation: 'exact',
|
||||
feedback: {
|
||||
correct: 'Bonne réponse !',
|
||||
incorrect: 'Essaie encore'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export global
|
||||
window.VocabularyGenerator = VocabularyGenerator;
|
||||
window.SentenceGenerator = SentenceGenerator;
|
||||
window.DialogueGenerator = DialogueGenerator;
|
||||
window.SequenceGenerator = SequenceGenerator;
|
||||
window.AutoGenerator = AutoGenerator;
|
||||
485
js/core/content-parsers.js
Normal file
485
js/core/content-parsers.js
Normal file
@ -0,0 +1,485 @@
|
||||
// === PARSERS UNIVERSELS POUR CONTENU ===
|
||||
|
||||
// === PARSER DE TEXTE LIBRE ===
|
||||
class TextParser {
|
||||
async parse(text, options = {}) {
|
||||
console.log('📝 TextParser - Analyse du texte libre');
|
||||
|
||||
const result = {
|
||||
rawText: text,
|
||||
vocabulary: [],
|
||||
sentences: [],
|
||||
dialogue: null,
|
||||
sequence: null,
|
||||
metadata: {
|
||||
wordCount: text.split(' ').length,
|
||||
language: this.detectLanguage(text),
|
||||
structure: this.analyzeStructure(text)
|
||||
}
|
||||
};
|
||||
|
||||
// Détecter le type de contenu
|
||||
if (this.isVocabularyList(text)) {
|
||||
result.vocabulary = this.parseVocabularyList(text);
|
||||
} else if (this.isDialogue(text)) {
|
||||
result.dialogue = this.parseDialogue(text);
|
||||
} else if (this.isSequence(text)) {
|
||||
result.sequence = this.parseSequence(text);
|
||||
} else {
|
||||
result.sentences = this.parseSentences(text);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
isVocabularyList(text) {
|
||||
// Recherche patterns: "word = translation", "word: translation", "word - translation"
|
||||
const patterns = [/\w+\s*[=:-]\s*\w+/g, /\w+\s*=\s*\w+/g];
|
||||
return patterns.some(pattern => pattern.test(text));
|
||||
}
|
||||
|
||||
parseVocabularyList(text) {
|
||||
const vocabulary = [];
|
||||
const lines = text.split('\n').filter(line => line.trim());
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const matches = line.match(/(.+?)\s*[=:-]\s*(.+?)(?:\s*\((.+?)\))?$/);
|
||||
if (matches) {
|
||||
const [, english, french, category] = matches;
|
||||
vocabulary.push({
|
||||
english: english.trim(),
|
||||
french: french.trim(),
|
||||
category: category?.trim() || 'general',
|
||||
index: index
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return vocabulary;
|
||||
}
|
||||
|
||||
isDialogue(text) {
|
||||
// Recherche patterns: "A:", "Person1:", "- Alice:", etc.
|
||||
return /^[A-Z][^:]*:|^-\s*[A-Z]/m.test(text);
|
||||
}
|
||||
|
||||
parseDialogue(text) {
|
||||
const conversation = [];
|
||||
const lines = text.split('\n').filter(line => line.trim());
|
||||
|
||||
lines.forEach(line => {
|
||||
const speakerMatch = line.match(/^(?:-\s*)?([^:]+):\s*(.+)$/);
|
||||
if (speakerMatch) {
|
||||
const [, speaker, text] = speakerMatch;
|
||||
conversation.push({
|
||||
speaker: speaker.trim(),
|
||||
text: text.trim(),
|
||||
english: text.trim() // À traduire si nécessaire
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
scenario: 'conversation',
|
||||
conversation: conversation,
|
||||
speakers: [...new Set(conversation.map(c => c.speaker))]
|
||||
};
|
||||
}
|
||||
|
||||
isSequence(text) {
|
||||
// Recherche patterns: "1.", "First", "Then", "Finally", etc.
|
||||
const sequenceIndicators = /^(\d+\.|\d+\))|first|then|next|after|finally|lastly/mi;
|
||||
return sequenceIndicators.test(text);
|
||||
}
|
||||
|
||||
parseSequence(text) {
|
||||
const steps = [];
|
||||
const lines = text.split('\n').filter(line => line.trim());
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const stepMatch = line.match(/^(?:(\d+)[\.\)]\s*)?(.+)$/);
|
||||
if (stepMatch) {
|
||||
const [, number, stepText] = stepMatch;
|
||||
steps.push({
|
||||
order: number ? parseInt(number) : index + 1,
|
||||
english: stepText.trim(),
|
||||
french: '', // À traduire
|
||||
index: index
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
title: 'Sequence',
|
||||
steps: steps.sort((a, b) => a.order - b.order)
|
||||
};
|
||||
}
|
||||
|
||||
parseSentences(text) {
|
||||
// Séparer en phrases
|
||||
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 3);
|
||||
|
||||
return sentences.map((sentence, index) => ({
|
||||
english: sentence.trim(),
|
||||
french: '', // À traduire
|
||||
index: index,
|
||||
structure: this.analyzeSentenceStructure(sentence)
|
||||
}));
|
||||
}
|
||||
|
||||
detectLanguage(text) {
|
||||
// Détection simple basée sur des mots courants
|
||||
const englishWords = ['the', 'and', 'is', 'in', 'to', 'of', 'a', 'that'];
|
||||
const frenchWords = ['le', 'et', 'est', 'dans', 'de', 'la', 'que', 'un'];
|
||||
|
||||
const words = text.toLowerCase().split(/\s+/);
|
||||
const englishCount = words.filter(w => englishWords.includes(w)).length;
|
||||
const frenchCount = words.filter(w => frenchWords.includes(w)).length;
|
||||
|
||||
if (englishCount > frenchCount) return 'english';
|
||||
if (frenchCount > englishCount) return 'french';
|
||||
return 'mixed';
|
||||
}
|
||||
|
||||
analyzeStructure(text) {
|
||||
return {
|
||||
hasNumbers: /\d+/.test(text),
|
||||
hasColons: /:/.test(text),
|
||||
hasEquals: /=/.test(text),
|
||||
hasDashes: /-/.test(text),
|
||||
lineCount: text.split('\n').length,
|
||||
avgWordsPerLine: text.split('\n').reduce((acc, line) => acc + line.split(' ').length, 0) / text.split('\n').length
|
||||
};
|
||||
}
|
||||
|
||||
analyzeSentenceStructure(sentence) {
|
||||
return {
|
||||
wordCount: sentence.split(' ').length,
|
||||
hasQuestion: sentence.includes('?'),
|
||||
hasExclamation: sentence.includes('!'),
|
||||
complexity: sentence.split(' ').length > 10 ? 'complex' : 'simple'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// === PARSER CSV ===
|
||||
class CSVParser {
|
||||
async parse(csvText, options = {}) {
|
||||
console.log('📊 CSVParser - Analyse CSV');
|
||||
|
||||
const separator = options.separator || this.detectSeparator(csvText);
|
||||
const lines = csvText.split('\n').filter(line => line.trim());
|
||||
const headers = lines[0].split(separator).map(h => h.trim());
|
||||
|
||||
const vocabulary = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const values = lines[i].split(separator).map(v => v.trim());
|
||||
const entry = {};
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
entry[header.toLowerCase()] = values[index] || '';
|
||||
});
|
||||
|
||||
vocabulary.push(entry);
|
||||
}
|
||||
|
||||
return {
|
||||
vocabulary: vocabulary,
|
||||
headers: headers,
|
||||
format: 'csv',
|
||||
separator: separator
|
||||
};
|
||||
}
|
||||
|
||||
detectSeparator(csvText) {
|
||||
const separators = [',', ';', '\t', '|'];
|
||||
const firstLine = csvText.split('\n')[0];
|
||||
|
||||
let maxCount = 0;
|
||||
let bestSeparator = ',';
|
||||
|
||||
separators.forEach(sep => {
|
||||
const count = (firstLine.match(new RegExp('\\' + sep, 'g')) || []).length;
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
bestSeparator = sep;
|
||||
}
|
||||
});
|
||||
|
||||
return bestSeparator;
|
||||
}
|
||||
}
|
||||
|
||||
// === PARSER JSON ===
|
||||
class JSONParser {
|
||||
async parse(jsonData, options = {}) {
|
||||
console.log('🔗 JSONParser - Analyse JSON');
|
||||
|
||||
let data;
|
||||
if (typeof jsonData === 'string') {
|
||||
try {
|
||||
data = JSON.parse(jsonData);
|
||||
} catch (error) {
|
||||
throw new Error('JSON invalide: ' + error.message);
|
||||
}
|
||||
} else {
|
||||
data = jsonData;
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
format: 'json',
|
||||
parsed: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// === PARSER DIALOGUE SPÉCIALISÉ ===
|
||||
class DialogueParser {
|
||||
async parse(dialogueText, options = {}) {
|
||||
console.log('💬 DialogueParser - Analyse dialogue');
|
||||
|
||||
const scenes = this.extractScenes(dialogueText);
|
||||
const characters = this.extractCharacters(dialogueText);
|
||||
const conversations = this.parseConversations(dialogueText);
|
||||
|
||||
return {
|
||||
dialogue: true,
|
||||
scenes: scenes,
|
||||
characters: characters,
|
||||
conversations: conversations,
|
||||
format: 'dialogue'
|
||||
};
|
||||
}
|
||||
|
||||
extractScenes(text) {
|
||||
// Rechercher des indications de scène: [Scene], (Scene), etc.
|
||||
const sceneMatches = text.match(/\[([^\]]+)\]|\(([^)]+)\)/g) || [];
|
||||
return sceneMatches.map(match => match.replace(/[\[\]()]/g, ''));
|
||||
}
|
||||
|
||||
extractCharacters(text) {
|
||||
// Extraire tous les noms avant ":"
|
||||
const characterMatches = text.match(/^[^:\n]+:/gm) || [];
|
||||
const characters = new Set();
|
||||
|
||||
characterMatches.forEach(match => {
|
||||
const name = match.replace(':', '').trim();
|
||||
if (name.length > 0 && name.length < 30) {
|
||||
characters.add(name);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(characters);
|
||||
}
|
||||
|
||||
parseConversations(text) {
|
||||
const conversations = [];
|
||||
const lines = text.split('\n');
|
||||
|
||||
let currentScene = 'Scene 1';
|
||||
|
||||
lines.forEach(line => {
|
||||
line = line.trim();
|
||||
|
||||
// Détection de nouvelle scène
|
||||
if (line.match(/\[([^\]]+)\]|\(([^)]+)\)/)) {
|
||||
currentScene = line.replace(/[\[\]()]/g, '');
|
||||
return;
|
||||
}
|
||||
|
||||
// Détection de dialogue
|
||||
const dialogueMatch = line.match(/^([^:]+):\s*(.+)$/);
|
||||
if (dialogueMatch) {
|
||||
const [, speaker, text] = dialogueMatch;
|
||||
conversations.push({
|
||||
scene: currentScene,
|
||||
speaker: speaker.trim(),
|
||||
english: text.trim(),
|
||||
french: '', // À traduire
|
||||
timestamp: conversations.length
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return conversations;
|
||||
}
|
||||
}
|
||||
|
||||
// === PARSER SÉQUENCE SPÉCIALISÉ ===
|
||||
class SequenceParser {
|
||||
async parse(sequenceText, options = {}) {
|
||||
console.log('📋 SequenceParser - Analyse séquence');
|
||||
|
||||
const title = this.extractTitle(sequenceText);
|
||||
const steps = this.extractSteps(sequenceText);
|
||||
const timeline = this.extractTimeline(sequenceText);
|
||||
|
||||
return {
|
||||
sequence: true,
|
||||
title: title,
|
||||
steps: steps,
|
||||
timeline: timeline,
|
||||
format: 'sequence'
|
||||
};
|
||||
}
|
||||
|
||||
extractTitle(text) {
|
||||
// Chercher un titre en début de texte
|
||||
const lines = text.split('\n');
|
||||
const firstLine = lines[0].trim();
|
||||
|
||||
// Si la première ligne ne commence pas par un numéro, c'est probablement le titre
|
||||
if (!firstLine.match(/^\d+/)) {
|
||||
return firstLine;
|
||||
}
|
||||
|
||||
return 'Sequence';
|
||||
}
|
||||
|
||||
extractSteps(text) {
|
||||
const steps = [];
|
||||
const lines = text.split('\n').filter(line => line.trim());
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// Ignorer la première ligne si c'est le titre
|
||||
if (index === 0 && !line.match(/^\d+/)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stepPatterns = [
|
||||
/^(\d+)[\.\)]\s*(.+)$/, // "1. Step text"
|
||||
/^(First|Then|Next|After|Finally|Lastly)[:.]?\s*(.+)$/i, // "First: text"
|
||||
/^(.+)$/ // Fallback: toute ligne
|
||||
];
|
||||
|
||||
for (let pattern of stepPatterns) {
|
||||
const match = line.match(pattern);
|
||||
if (match) {
|
||||
let [, indicator, stepText] = match;
|
||||
|
||||
if (!stepText) {
|
||||
stepText = indicator;
|
||||
indicator = (steps.length + 1).toString();
|
||||
}
|
||||
|
||||
steps.push({
|
||||
order: this.normalizeStepNumber(indicator, steps.length + 1),
|
||||
english: stepText.trim(),
|
||||
french: '', // À traduire
|
||||
indicator: indicator,
|
||||
rawLine: line
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return steps.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
normalizeStepNumber(indicator, fallback) {
|
||||
if (/^\d+$/.test(indicator)) {
|
||||
return parseInt(indicator);
|
||||
}
|
||||
|
||||
const wordNumbers = {
|
||||
'first': 1, 'second': 2, 'third': 3, 'fourth': 4, 'fifth': 5,
|
||||
'then': fallback, 'next': fallback, 'after': fallback,
|
||||
'finally': 999, 'lastly': 999
|
||||
};
|
||||
|
||||
return wordNumbers[indicator.toLowerCase()] || fallback;
|
||||
}
|
||||
|
||||
extractTimeline(text) {
|
||||
// Rechercher des indications de temps: "7:00", "at 8pm", "in the morning"
|
||||
const timeMatches = text.match(/\d{1,2}:\d{2}|\d{1,2}(am|pm)|morning|afternoon|evening|night/gi) || [];
|
||||
return timeMatches;
|
||||
}
|
||||
}
|
||||
|
||||
// === PARSER MÉDIA ===
|
||||
class MediaParser {
|
||||
async parse(mediaData, options = {}) {
|
||||
console.log('🎵 MediaParser - Analyse médias');
|
||||
|
||||
const result = {
|
||||
audio: [],
|
||||
images: [],
|
||||
metadata: {},
|
||||
format: 'media'
|
||||
};
|
||||
|
||||
if (Array.isArray(mediaData)) {
|
||||
mediaData.forEach(file => {
|
||||
if (this.isAudioFile(file)) {
|
||||
result.audio.push(this.parseAudioFile(file));
|
||||
} else if (this.isImageFile(file)) {
|
||||
result.images.push(this.parseImageFile(file));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
isAudioFile(file) {
|
||||
const audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'flac'];
|
||||
const extension = this.getFileExtension(file.name || file);
|
||||
return audioExtensions.includes(extension.toLowerCase());
|
||||
}
|
||||
|
||||
isImageFile(file) {
|
||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
|
||||
const extension = this.getFileExtension(file.name || file);
|
||||
return imageExtensions.includes(extension.toLowerCase());
|
||||
}
|
||||
|
||||
getFileExtension(filename) {
|
||||
return filename.split('.').pop() || '';
|
||||
}
|
||||
|
||||
parseAudioFile(file) {
|
||||
return {
|
||||
name: file.name,
|
||||
path: file.path || file.name,
|
||||
type: 'audio',
|
||||
extension: this.getFileExtension(file.name),
|
||||
associatedWord: this.extractWordFromFilename(file.name),
|
||||
metadata: {
|
||||
size: file.size,
|
||||
duration: file.duration || null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
parseImageFile(file) {
|
||||
return {
|
||||
name: file.name,
|
||||
path: file.path || file.name,
|
||||
type: 'image',
|
||||
extension: this.getFileExtension(file.name),
|
||||
associatedWord: this.extractWordFromFilename(file.name),
|
||||
metadata: {
|
||||
size: file.size,
|
||||
width: file.width || null,
|
||||
height: file.height || null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extractWordFromFilename(filename) {
|
||||
// Extraire le mot du nom de fichier: "cat.mp3" -> "cat"
|
||||
return filename.split('.')[0].replace(/[_-]/g, ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Export global
|
||||
window.TextParser = TextParser;
|
||||
window.CSVParser = CSVParser;
|
||||
window.JSONParser = JSONParser;
|
||||
window.DialogueParser = DialogueParser;
|
||||
window.SequenceParser = SequenceParser;
|
||||
window.MediaParser = MediaParser;
|
||||
377
js/core/content-scanner.js
Normal file
377
js/core/content-scanner.js
Normal file
@ -0,0 +1,377 @@
|
||||
// === SCANNER AUTOMATIQUE DE CONTENU ===
|
||||
|
||||
class ContentScanner {
|
||||
constructor() {
|
||||
this.discoveredContent = new Map();
|
||||
this.contentFiles = [
|
||||
// Liste des fichiers de contenu à scanner automatiquement
|
||||
'sbs-level-7-8-new.js'
|
||||
];
|
||||
}
|
||||
|
||||
async scanAllContent() {
|
||||
console.log('🔍 ContentScanner - Scan automatique du contenu...');
|
||||
|
||||
const results = {
|
||||
found: [],
|
||||
errors: [],
|
||||
total: 0
|
||||
};
|
||||
|
||||
for (const filename of this.contentFiles) {
|
||||
try {
|
||||
const contentInfo = await this.scanContentFile(filename);
|
||||
if (contentInfo) {
|
||||
this.discoveredContent.set(contentInfo.id, contentInfo);
|
||||
results.found.push(contentInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Erreur scan ${filename}:`, error.message);
|
||||
results.errors.push({ filename, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
results.total = results.found.length;
|
||||
console.log(`✅ Scan terminé: ${results.total} modules trouvés`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async scanContentFile(filename) {
|
||||
const contentId = this.extractContentId(filename);
|
||||
const moduleName = this.getModuleName(contentId);
|
||||
|
||||
try {
|
||||
// Charger le script si pas déjà fait
|
||||
await this.loadScript(`js/content/${filename}`);
|
||||
|
||||
// Vérifier si le module existe
|
||||
if (!window.ContentModules || !window.ContentModules[moduleName]) {
|
||||
throw new Error(`Module ${moduleName} non trouvé après chargement`);
|
||||
}
|
||||
|
||||
const module = window.ContentModules[moduleName];
|
||||
|
||||
// Extraire les métadonnées
|
||||
const contentInfo = this.extractContentInfo(module, contentId, filename);
|
||||
|
||||
console.log(`📦 Contenu découvert: ${contentInfo.name}`);
|
||||
return contentInfo;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Impossible de charger ${filename}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
extractContentInfo(module, contentId, filename) {
|
||||
return {
|
||||
id: contentId,
|
||||
filename: filename,
|
||||
name: module.name || this.beautifyContentId(contentId),
|
||||
description: module.description || 'Contenu automatiquement détecté',
|
||||
icon: this.getContentIcon(module, contentId),
|
||||
difficulty: module.difficulty || 'medium',
|
||||
enabled: true,
|
||||
|
||||
// Métadonnées détaillées
|
||||
metadata: {
|
||||
version: module.version || '1.0',
|
||||
format: module.format || 'legacy',
|
||||
totalItems: this.countItems(module),
|
||||
categories: this.extractCategories(module),
|
||||
contentTypes: this.extractContentTypes(module),
|
||||
estimatedTime: this.calculateEstimatedTime(module),
|
||||
lastScanned: new Date().toISOString()
|
||||
},
|
||||
|
||||
// Statistiques
|
||||
stats: {
|
||||
vocabularyCount: this.countByType(module, 'vocabulary'),
|
||||
sentenceCount: this.countByType(module, 'sentence'),
|
||||
dialogueCount: this.countByType(module, 'dialogue'),
|
||||
grammarCount: this.countByType(module, 'grammar')
|
||||
},
|
||||
|
||||
// Configuration pour les jeux
|
||||
gameCompatibility: this.analyzeGameCompatibility(module)
|
||||
};
|
||||
}
|
||||
|
||||
extractContentId(filename) {
|
||||
return filename.replace('.js', '').toLowerCase();
|
||||
}
|
||||
|
||||
getModuleName(contentId) {
|
||||
const mapping = {
|
||||
'sbs-level-7-8-new': 'SBSLevel78New'
|
||||
};
|
||||
return mapping[contentId] || this.toPascalCase(contentId);
|
||||
}
|
||||
|
||||
toPascalCase(str) {
|
||||
return str.split('-').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join('');
|
||||
}
|
||||
|
||||
beautifyContentId(contentId) {
|
||||
const beautified = {
|
||||
'sbs-level-7-8-new': 'SBS Level 7-8 (Simple Format)'
|
||||
};
|
||||
return beautified[contentId] || contentId.charAt(0).toUpperCase() + contentId.slice(1);
|
||||
}
|
||||
|
||||
getContentIcon(module, contentId) {
|
||||
// Icône du module si disponible
|
||||
if (module.icon) return module.icon;
|
||||
|
||||
// Icônes par défaut selon l'ID
|
||||
const defaultIcons = {
|
||||
'sbs-level-7-8-new': '✨'
|
||||
};
|
||||
|
||||
return defaultIcons[contentId] || '📝';
|
||||
}
|
||||
|
||||
countItems(module) {
|
||||
let count = 0;
|
||||
|
||||
// Format moderne (contentItems)
|
||||
if (module.contentItems && Array.isArray(module.contentItems)) {
|
||||
return module.contentItems.length;
|
||||
}
|
||||
|
||||
// Format simple (vocabulary object + sentences array)
|
||||
if (module.vocabulary && typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) {
|
||||
count += Object.keys(module.vocabulary).length;
|
||||
}
|
||||
// Format legacy (vocabulary array)
|
||||
else if (module.vocabulary && Array.isArray(module.vocabulary)) {
|
||||
count += module.vocabulary.length;
|
||||
}
|
||||
|
||||
// Autres contenus
|
||||
if (module.sentences && Array.isArray(module.sentences)) count += module.sentences.length;
|
||||
if (module.dialogues && Array.isArray(module.dialogues)) count += module.dialogues.length;
|
||||
if (module.phrases && Array.isArray(module.phrases)) count += module.phrases.length;
|
||||
if (module.texts && Array.isArray(module.texts)) count += module.texts.length;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
extractCategories(module) {
|
||||
const categories = new Set();
|
||||
|
||||
if (module.categories) {
|
||||
Object.keys(module.categories).forEach(cat => categories.add(cat));
|
||||
}
|
||||
|
||||
if (module.metadata && module.metadata.categories) {
|
||||
module.metadata.categories.forEach(cat => categories.add(cat));
|
||||
}
|
||||
|
||||
// Extraire des contenus si format moderne
|
||||
if (module.contentItems) {
|
||||
module.contentItems.forEach(item => {
|
||||
if (item.category) categories.add(item.category);
|
||||
});
|
||||
}
|
||||
|
||||
// Extraire du vocabulaire selon le format
|
||||
if (module.vocabulary) {
|
||||
// Format simple (vocabulary object)
|
||||
if (typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) {
|
||||
// Pour l'instant, pas de catégories dans le format simple
|
||||
categories.add('vocabulary');
|
||||
}
|
||||
// Format legacy (vocabulary array)
|
||||
else if (Array.isArray(module.vocabulary)) {
|
||||
module.vocabulary.forEach(word => {
|
||||
if (word.category) categories.add(word.category);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(categories);
|
||||
}
|
||||
|
||||
extractContentTypes(module) {
|
||||
const types = new Set();
|
||||
|
||||
if (module.contentItems) {
|
||||
module.contentItems.forEach(item => {
|
||||
if (item.type) types.add(item.type);
|
||||
});
|
||||
} else {
|
||||
// Format legacy - deviner les types
|
||||
if (module.vocabulary) types.add('vocabulary');
|
||||
if (module.sentences) types.add('sentence');
|
||||
if (module.dialogues || module.dialogue) types.add('dialogue');
|
||||
if (module.phrases) types.add('sentence');
|
||||
}
|
||||
|
||||
return Array.from(types);
|
||||
}
|
||||
|
||||
calculateEstimatedTime(module) {
|
||||
if (module.metadata && module.metadata.estimatedTime) {
|
||||
return module.metadata.estimatedTime;
|
||||
}
|
||||
|
||||
// Calcul basique : 1 minute par 3 éléments
|
||||
const itemCount = this.countItems(module);
|
||||
return Math.max(5, Math.ceil(itemCount / 3));
|
||||
}
|
||||
|
||||
countByType(module, type) {
|
||||
if (module.contentItems) {
|
||||
return module.contentItems.filter(item => item.type === type).length;
|
||||
}
|
||||
|
||||
// Format simple et legacy
|
||||
switch(type) {
|
||||
case 'vocabulary':
|
||||
// Format simple (vocabulary object)
|
||||
if (module.vocabulary && typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) {
|
||||
return Object.keys(module.vocabulary).length;
|
||||
}
|
||||
// Format legacy (vocabulary array)
|
||||
return module.vocabulary ? module.vocabulary.length : 0;
|
||||
|
||||
case 'sentence':
|
||||
return (module.sentences ? module.sentences.length : 0) +
|
||||
(module.phrases ? module.phrases.length : 0);
|
||||
|
||||
case 'dialogue':
|
||||
return module.dialogues ? module.dialogues.length : 0;
|
||||
|
||||
case 'grammar':
|
||||
// Format simple (grammar object avec sous-propriétés)
|
||||
if (module.grammar && typeof module.grammar === 'object') {
|
||||
return Object.keys(module.grammar).length;
|
||||
}
|
||||
return module.grammar && Array.isArray(module.grammar) ? module.grammar.length : 0;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
analyzeGameCompatibility(module) {
|
||||
const compatibility = {
|
||||
'whack-a-mole': { compatible: false, score: 0 },
|
||||
'memory-game': { compatible: false, score: 0 },
|
||||
'story-builder': { compatible: false, score: 0 },
|
||||
'temp-games': { compatible: false, score: 0 }
|
||||
};
|
||||
|
||||
const vocabCount = this.countByType(module, 'vocabulary');
|
||||
const sentenceCount = this.countByType(module, 'sentence');
|
||||
const dialogueCount = this.countByType(module, 'dialogue');
|
||||
|
||||
// Whack-a-Mole - aime le vocabulaire et phrases simples
|
||||
if (vocabCount > 5 || sentenceCount > 3) {
|
||||
compatibility['whack-a-mole'].compatible = true;
|
||||
compatibility['whack-a-mole'].score = Math.min(100, vocabCount * 5 + sentenceCount * 3);
|
||||
}
|
||||
|
||||
// Memory Game - parfait pour vocabulaire avec images
|
||||
if (vocabCount > 4) {
|
||||
compatibility['memory-game'].compatible = true;
|
||||
compatibility['memory-game'].score = Math.min(100, vocabCount * 8);
|
||||
}
|
||||
|
||||
// Story Builder - aime les dialogues et séquences
|
||||
if (dialogueCount > 0 || sentenceCount > 5) {
|
||||
compatibility['story-builder'].compatible = true;
|
||||
compatibility['story-builder'].score = Math.min(100, dialogueCount * 15 + sentenceCount * 2);
|
||||
}
|
||||
|
||||
// Temp Games - accepte tout
|
||||
if (this.countItems(module) > 3) {
|
||||
compatibility['temp-games'].compatible = true;
|
||||
compatibility['temp-games'].score = Math.min(100, this.countItems(module) * 2);
|
||||
}
|
||||
|
||||
return compatibility;
|
||||
}
|
||||
|
||||
async loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Vérifier si déjà chargé
|
||||
const existingScript = document.querySelector(`script[src="${src}"]`);
|
||||
if (existingScript) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = () => reject(new Error(`Impossible de charger ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// === API PUBLIQUE ===
|
||||
|
||||
async getAvailableContent() {
|
||||
if (this.discoveredContent.size === 0) {
|
||||
await this.scanAllContent();
|
||||
}
|
||||
return Array.from(this.discoveredContent.values());
|
||||
}
|
||||
|
||||
async getContentById(id) {
|
||||
if (this.discoveredContent.size === 0) {
|
||||
await this.scanAllContent();
|
||||
}
|
||||
return this.discoveredContent.get(id);
|
||||
}
|
||||
|
||||
async getContentByGame(gameType) {
|
||||
const allContent = await this.getAvailableContent();
|
||||
|
||||
return allContent.filter(content => {
|
||||
const compat = content.gameCompatibility[gameType];
|
||||
return compat && compat.compatible;
|
||||
}).sort((a, b) => {
|
||||
// Trier par score de compatibilité
|
||||
const scoreA = a.gameCompatibility[gameType].score;
|
||||
const scoreB = b.gameCompatibility[gameType].score;
|
||||
return scoreB - scoreA;
|
||||
});
|
||||
}
|
||||
|
||||
async refreshContent() {
|
||||
this.discoveredContent.clear();
|
||||
return await this.scanAllContent();
|
||||
}
|
||||
|
||||
getContentStats() {
|
||||
const stats = {
|
||||
totalModules: this.discoveredContent.size,
|
||||
totalItems: 0,
|
||||
categories: new Set(),
|
||||
contentTypes: new Set(),
|
||||
difficulties: new Set()
|
||||
};
|
||||
|
||||
for (const content of this.discoveredContent.values()) {
|
||||
stats.totalItems += content.metadata.totalItems;
|
||||
content.metadata.categories.forEach(cat => stats.categories.add(cat));
|
||||
content.metadata.contentTypes.forEach(type => stats.contentTypes.add(type));
|
||||
stats.difficulties.add(content.difficulty);
|
||||
}
|
||||
|
||||
return {
|
||||
...stats,
|
||||
categories: Array.from(stats.categories),
|
||||
contentTypes: Array.from(stats.contentTypes),
|
||||
difficulties: Array.from(stats.difficulties)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export global
|
||||
window.ContentScanner = ContentScanner;
|
||||
337
js/core/game-loader.js
Normal file
337
js/core/game-loader.js
Normal file
@ -0,0 +1,337 @@
|
||||
// === CHARGEUR DE JEUX DYNAMIQUE ===
|
||||
|
||||
const GameLoader = {
|
||||
currentGame: null,
|
||||
contentScanner: new ContentScanner(),
|
||||
loadedModules: {
|
||||
games: {},
|
||||
content: {}
|
||||
},
|
||||
|
||||
async loadGame(gameType, contentType) {
|
||||
try {
|
||||
// Nettoyage du jeu précédent
|
||||
this.cleanup();
|
||||
|
||||
// Chargement parallèle du module de jeu et du contenu
|
||||
const [gameModule, contentModule] = await Promise.all([
|
||||
this.loadGameModule(gameType),
|
||||
this.loadContentModule(contentType)
|
||||
]);
|
||||
|
||||
// Initialisation du jeu
|
||||
this.initGame(gameType, gameModule, contentModule);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement du jeu:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async loadGameModule(gameType) {
|
||||
// Vérifier si le module est déjà chargé
|
||||
if (this.loadedModules.games[gameType]) {
|
||||
return this.loadedModules.games[gameType];
|
||||
}
|
||||
|
||||
try {
|
||||
// Chargement dynamique du script
|
||||
await this.loadScript(`js/games/${gameType}.js`);
|
||||
|
||||
// Récupération du module depuis l'objet global
|
||||
const module = window.GameModules?.[this.getModuleName(gameType)];
|
||||
|
||||
if (!module) {
|
||||
throw new Error(`Module de jeu ${gameType} non trouvé`);
|
||||
}
|
||||
|
||||
// Cache du module
|
||||
this.loadedModules.games[gameType] = module;
|
||||
return module;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Erreur chargement module jeu ${gameType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async loadContentModule(contentType) {
|
||||
// Utiliser le ContentScanner pour récupérer le contenu découvert
|
||||
try {
|
||||
// Récupérer le contenu déjà découvert par le scanner
|
||||
const contentInfo = await this.contentScanner.getContentById(contentType);
|
||||
|
||||
if (!contentInfo) {
|
||||
throw new Error(`Contenu ${contentType} non trouvé par le scanner`);
|
||||
}
|
||||
|
||||
// Charger le module JavaScript correspondant
|
||||
await this.loadScript(`js/content/${contentInfo.filename}`);
|
||||
|
||||
// Récupérer le module depuis l'objet global
|
||||
const moduleName = this.getContentModuleName(contentType);
|
||||
const rawModule = window.ContentModules?.[moduleName];
|
||||
|
||||
if (!rawModule) {
|
||||
throw new Error(`Module ${moduleName} non trouvé après chargement`);
|
||||
}
|
||||
|
||||
// Combiner les informations du scanner avec le contenu brut
|
||||
const enrichedContent = {
|
||||
...rawModule,
|
||||
...contentInfo,
|
||||
// S'assurer que le contenu brut du module est disponible
|
||||
rawContent: rawModule
|
||||
};
|
||||
|
||||
this.loadedModules.content[contentType] = enrichedContent;
|
||||
return enrichedContent;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Erreur chargement contenu ${contentType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Vérifier si le script est déjà chargé
|
||||
const existingScript = document.querySelector(`script[src="${src}"]`);
|
||||
if (existingScript) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = resolve;
|
||||
script.onerror = () => reject(new Error(`Impossible de charger ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
},
|
||||
|
||||
initGame(gameType, GameClass, contentData) {
|
||||
const gameContainer = document.getElementById('game-container');
|
||||
const gameTitle = document.getElementById('game-title');
|
||||
const scoreDisplay = document.getElementById('current-score');
|
||||
|
||||
// Le contenu est déjà enrichi par le ContentScanner, pas besoin d'adaptation supplémentaire
|
||||
const adaptedContent = contentData;
|
||||
|
||||
// Mise à jour du titre
|
||||
const contentName = adaptedContent.name || contentType;
|
||||
gameTitle.textContent = this.getGameTitle(gameType, contentName);
|
||||
|
||||
// Réinitialisation du score
|
||||
scoreDisplay.textContent = '0';
|
||||
|
||||
// Création de l'instance de jeu avec contenu enrichi
|
||||
this.currentGame = new GameClass({
|
||||
container: gameContainer,
|
||||
content: adaptedContent,
|
||||
contentScanner: this.contentScanner, // Passer le scanner pour accès aux métadonnées
|
||||
onScoreUpdate: (score) => this.updateScore(score),
|
||||
onGameEnd: (finalScore) => this.handleGameEnd(finalScore)
|
||||
});
|
||||
|
||||
// Démarrage du jeu
|
||||
this.currentGame.start();
|
||||
},
|
||||
|
||||
updateScore(score) {
|
||||
const scoreDisplay = document.getElementById('current-score');
|
||||
scoreDisplay.textContent = score.toString();
|
||||
|
||||
// Animation du score
|
||||
Utils.animateElement(scoreDisplay, 'pulse', 200);
|
||||
},
|
||||
|
||||
handleGameEnd(finalScore) {
|
||||
// Sauvegarde du score
|
||||
this.saveScore(finalScore);
|
||||
|
||||
// Affichage du résultat
|
||||
Utils.showToast(`Jeu terminé ! Score final: ${finalScore}`, 'success');
|
||||
|
||||
// Afficher les options de fin de jeu
|
||||
this.showGameEndOptions(finalScore);
|
||||
},
|
||||
|
||||
showGameEndOptions(finalScore) {
|
||||
const gameContainer = document.getElementById('game-container');
|
||||
|
||||
// Créer l'overlay de fin de jeu
|
||||
const endOverlay = document.createElement('div');
|
||||
endOverlay.className = 'game-end-overlay';
|
||||
endOverlay.innerHTML = `
|
||||
<div class="game-end-modal">
|
||||
<h3>🎉 Jeu Terminé !</h3>
|
||||
<div class="final-score">Score final: <strong>${finalScore}</strong></div>
|
||||
<div class="best-score">Meilleur score: <strong>${this.getBestScoreForCurrentGame()}</strong></div>
|
||||
<div class="game-end-buttons">
|
||||
<button class="restart-game-btn">🔄 Rejouer</button>
|
||||
<button class="back-to-levels-btn">← Changer de niveau</button>
|
||||
<button class="back-to-games-btn">🎮 Autres jeux</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
gameContainer.appendChild(endOverlay);
|
||||
|
||||
// Ajouter les event listeners
|
||||
endOverlay.querySelector('.restart-game-btn').addEventListener('click', () => {
|
||||
this.removeGameEndOverlay();
|
||||
this.restartCurrentGame();
|
||||
});
|
||||
|
||||
endOverlay.querySelector('.back-to-levels-btn').addEventListener('click', () => {
|
||||
const params = Utils.getUrlParams();
|
||||
AppNavigation.navigateTo('levels', params.game);
|
||||
});
|
||||
|
||||
endOverlay.querySelector('.back-to-games-btn').addEventListener('click', () => {
|
||||
AppNavigation.navigateTo('games');
|
||||
});
|
||||
|
||||
// Fermer avec ESC
|
||||
const handleEscape = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.removeGameEndOverlay();
|
||||
AppNavigation.goBack();
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
},
|
||||
|
||||
removeGameEndOverlay() {
|
||||
const overlay = document.querySelector('.game-end-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
},
|
||||
|
||||
getBestScoreForCurrentGame() {
|
||||
const params = Utils.getUrlParams();
|
||||
return this.getBestScore(params.game, params.content);
|
||||
},
|
||||
|
||||
restartCurrentGame() {
|
||||
if (this.currentGame && this.currentGame.restart) {
|
||||
this.currentGame.restart();
|
||||
document.getElementById('current-score').textContent = '0';
|
||||
}
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
if (this.currentGame && this.currentGame.destroy) {
|
||||
this.currentGame.destroy();
|
||||
}
|
||||
|
||||
// Supprimer l'overlay de fin de jeu s'il existe
|
||||
this.removeGameEndOverlay();
|
||||
|
||||
const gameContainer = document.getElementById('game-container');
|
||||
gameContainer.innerHTML = '';
|
||||
|
||||
this.currentGame = null;
|
||||
},
|
||||
|
||||
saveScore(score) {
|
||||
const params = Utils.getUrlParams();
|
||||
const scoreKey = `score_${params.game}_${params.content}`;
|
||||
const currentScores = Utils.storage.get(scoreKey, []);
|
||||
|
||||
currentScores.push({
|
||||
score: score,
|
||||
date: new Date().toISOString(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Garder seulement les 10 meilleurs scores
|
||||
currentScores.sort((a, b) => b.score - a.score);
|
||||
const bestScores = currentScores.slice(0, 10);
|
||||
|
||||
Utils.storage.set(scoreKey, bestScores);
|
||||
},
|
||||
|
||||
getBestScore(gameType, contentType) {
|
||||
const scoreKey = `score_${gameType}_${contentType}`;
|
||||
const scores = Utils.storage.get(scoreKey, []);
|
||||
return scores.length > 0 ? scores[0].score : 0;
|
||||
},
|
||||
|
||||
// Utilitaires de nommage
|
||||
getModuleName(gameType) {
|
||||
const names = {
|
||||
'whack-a-mole': 'WhackAMole',
|
||||
'whack-a-mole-hard': 'WhackAMoleHard',
|
||||
'memory-match': 'MemoryMatch',
|
||||
'quiz-game': 'QuizGame',
|
||||
'temp-games': 'TempGames',
|
||||
'fill-the-blank': 'FillTheBlank',
|
||||
'text-reader': 'TextReader',
|
||||
'adventure-reader': 'AdventureReader'
|
||||
};
|
||||
return names[gameType] || gameType;
|
||||
},
|
||||
|
||||
getContentModuleName(contentType) {
|
||||
// Utilise la même logique que le ContentScanner
|
||||
const mapping = {
|
||||
'sbs-level-7-8-new': 'SBSLevel78New'
|
||||
};
|
||||
return mapping[contentType] || this.toPascalCase(contentType);
|
||||
},
|
||||
|
||||
toPascalCase(str) {
|
||||
return str.split('-').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join('');
|
||||
},
|
||||
|
||||
getGameTitle(gameType, contentName) {
|
||||
const gameNames = {
|
||||
'whack-a-mole': 'Whack-a-Mole',
|
||||
'whack-a-mole-hard': 'Whack-a-Mole Hard',
|
||||
'memory-match': 'Memory Match',
|
||||
'quiz-game': 'Quiz Game',
|
||||
'temp-games': 'Mini-Jeux',
|
||||
'fill-the-blank': 'Fill the Blank',
|
||||
'text-reader': 'Text Reader',
|
||||
'adventure-reader': 'Adventure Reader'
|
||||
};
|
||||
|
||||
const gameName = gameNames[gameType] || gameType;
|
||||
return `${gameName} - ${contentName}`;
|
||||
},
|
||||
|
||||
// API pour les jeux
|
||||
createGameAPI() {
|
||||
return {
|
||||
showFeedback: (message, type = 'info') => Utils.showToast(message, type),
|
||||
playSound: (soundFile) => this.playSound(soundFile),
|
||||
updateScore: (score) => this.updateScore(score),
|
||||
endGame: (score) => this.handleGameEnd(score),
|
||||
getBestScore: () => {
|
||||
const params = Utils.getUrlParams();
|
||||
return this.getBestScore(params.game, params.content);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
playSound(soundFile) {
|
||||
if (Utils.canPlayAudio()) {
|
||||
try {
|
||||
const audio = new Audio(`assets/sounds/${soundFile}`);
|
||||
audio.volume = 0.5;
|
||||
audio.play().catch(e => console.warn('Cannot play sound:', e));
|
||||
} catch (error) {
|
||||
console.warn('Sound error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export global
|
||||
window.GameLoader = GameLoader;
|
||||
453
js/core/navigation.js
Normal file
453
js/core/navigation.js
Normal file
@ -0,0 +1,453 @@
|
||||
// === SYSTÈME DE NAVIGATION ===
|
||||
|
||||
const AppNavigation = {
|
||||
currentPage: 'home',
|
||||
navigationHistory: ['home'],
|
||||
gamesConfig: null,
|
||||
contentScanner: new ContentScanner(),
|
||||
scannedContent: null,
|
||||
|
||||
init() {
|
||||
this.loadGamesConfig();
|
||||
this.initContentScanner();
|
||||
this.setupEventListeners();
|
||||
this.handleInitialRoute();
|
||||
},
|
||||
|
||||
async loadGamesConfig() {
|
||||
// Utilisation directe de la config par défaut (pas de fetch)
|
||||
console.log('📁 Utilisation de la configuration par défaut');
|
||||
this.gamesConfig = this.getDefaultConfig();
|
||||
},
|
||||
|
||||
async initContentScanner() {
|
||||
try {
|
||||
console.log('🔍 Initialisation du scanner de contenu...');
|
||||
this.scannedContent = await this.contentScanner.scanAllContent();
|
||||
console.log(`✅ ${this.scannedContent.found.length} modules de contenu détectés automatiquement`);
|
||||
} catch (error) {
|
||||
console.error('Erreur scan contenu:', error);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
games: {
|
||||
'whack-a-mole': {
|
||||
enabled: true,
|
||||
name: 'Whack-a-Mole',
|
||||
icon: '🔨',
|
||||
description: 'Tape sur les bonnes réponses !'
|
||||
},
|
||||
'whack-a-mole-hard': {
|
||||
enabled: true,
|
||||
name: 'Whack-a-Mole Hard',
|
||||
icon: '💥',
|
||||
description: '3 moles at once, 5x3 grid, harder!'
|
||||
},
|
||||
'memory-match': {
|
||||
enabled: true,
|
||||
name: 'Memory Match',
|
||||
icon: '🧠',
|
||||
description: 'Find matching English-French pairs!'
|
||||
},
|
||||
'quiz-game': {
|
||||
enabled: true,
|
||||
name: 'Quiz Game',
|
||||
icon: '❓',
|
||||
description: 'Answer vocabulary questions!'
|
||||
},
|
||||
'temp-games': {
|
||||
enabled: true,
|
||||
name: 'Jeux Temporaires',
|
||||
icon: '🎯',
|
||||
description: 'Mini-jeux en développement'
|
||||
},
|
||||
'fill-the-blank': {
|
||||
enabled: true,
|
||||
name: 'Fill the Blank',
|
||||
icon: '📝',
|
||||
description: 'Complète les phrases en remplissant les blancs !'
|
||||
},
|
||||
'text-reader': {
|
||||
enabled: true,
|
||||
name: 'Text Reader',
|
||||
icon: '📖',
|
||||
description: 'Read texts sentence by sentence'
|
||||
},
|
||||
'adventure-reader': {
|
||||
enabled: true,
|
||||
name: 'Adventure Reader',
|
||||
icon: '⚔️',
|
||||
description: 'Zelda-style adventure with vocabulary!'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
'sbs-level-8': {
|
||||
enabled: true,
|
||||
name: 'SBS Level 8',
|
||||
icon: '📚',
|
||||
description: 'Vocabulaire manuel SBS'
|
||||
},
|
||||
'animals': {
|
||||
enabled: false,
|
||||
name: 'Animals',
|
||||
icon: '🐱',
|
||||
description: 'Vocabulaire des animaux'
|
||||
},
|
||||
'colors': {
|
||||
enabled: false,
|
||||
name: 'Colors & Numbers',
|
||||
icon: '🌈',
|
||||
description: 'Couleurs et nombres'
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
setupEventListeners() {
|
||||
// Navigation par URL
|
||||
window.addEventListener('popstate', () => {
|
||||
this.handleInitialRoute();
|
||||
});
|
||||
|
||||
// Raccourcis clavier
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.goBack();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleInitialRoute() {
|
||||
const params = Utils.getUrlParams();
|
||||
|
||||
if (params.page === 'play' && params.game && params.content) {
|
||||
this.showGamePage(params.game, params.content);
|
||||
} else if (params.page === 'levels' && params.game) {
|
||||
this.showLevelsPage(params.game);
|
||||
} else if (params.page === 'games') {
|
||||
this.showGamesPage();
|
||||
} else {
|
||||
this.showHomePage();
|
||||
}
|
||||
},
|
||||
|
||||
// Navigation vers une page
|
||||
navigateTo(page, game = null, content = null) {
|
||||
const params = { page };
|
||||
if (game) params.game = game;
|
||||
if (content) params.content = content;
|
||||
|
||||
Utils.setUrlParams(params);
|
||||
|
||||
// Mise à jour historique
|
||||
if (this.currentPage !== page) {
|
||||
this.navigationHistory.push(page);
|
||||
}
|
||||
|
||||
this.currentPage = page;
|
||||
|
||||
// Affichage de la page appropriée
|
||||
switch(page) {
|
||||
case 'games':
|
||||
this.showGamesPage();
|
||||
break;
|
||||
case 'levels':
|
||||
this.showLevelsPage(game);
|
||||
break;
|
||||
case 'play':
|
||||
this.showGamePage(game, content);
|
||||
break;
|
||||
default:
|
||||
this.showHomePage();
|
||||
}
|
||||
|
||||
this.updateBreadcrumb();
|
||||
},
|
||||
|
||||
// Retour en arrière
|
||||
goBack() {
|
||||
if (this.navigationHistory.length > 1) {
|
||||
this.navigationHistory.pop(); // Retirer la page actuelle
|
||||
const previousPage = this.navigationHistory[this.navigationHistory.length - 1];
|
||||
|
||||
const params = Utils.getUrlParams();
|
||||
|
||||
if (previousPage === 'levels') {
|
||||
this.navigateTo('levels', params.game);
|
||||
} else if (previousPage === 'games') {
|
||||
this.navigateTo('games');
|
||||
} else {
|
||||
this.navigateTo('home');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Affichage page d'accueil
|
||||
showHomePage() {
|
||||
this.hideAllPages();
|
||||
document.getElementById('home-page').classList.add('active');
|
||||
this.currentPage = 'home';
|
||||
},
|
||||
|
||||
// Affichage page sélection jeux
|
||||
showGamesPage() {
|
||||
this.hideAllPages();
|
||||
document.getElementById('games-page').classList.add('active');
|
||||
this.renderGamesGrid();
|
||||
this.currentPage = 'games';
|
||||
},
|
||||
|
||||
// Affichage page sélection niveaux
|
||||
showLevelsPage(gameType) {
|
||||
this.hideAllPages();
|
||||
document.getElementById('levels-page').classList.add('active');
|
||||
this.renderLevelsGrid(gameType);
|
||||
this.currentPage = 'levels';
|
||||
|
||||
// Mise à jour de la description
|
||||
const gameInfo = this.gamesConfig?.games[gameType];
|
||||
if (gameInfo) {
|
||||
document.getElementById('level-description').textContent =
|
||||
`Sélectionne le contenu pour jouer à ${gameInfo.name}`;
|
||||
}
|
||||
},
|
||||
|
||||
// Affichage page de jeu
|
||||
async showGamePage(gameType, contentType) {
|
||||
this.hideAllPages();
|
||||
document.getElementById('game-page').classList.add('active');
|
||||
this.currentPage = 'play';
|
||||
|
||||
Utils.showLoading();
|
||||
|
||||
try {
|
||||
await GameLoader.loadGame(gameType, contentType);
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement jeu:', error);
|
||||
Utils.showToast('Erreur lors du chargement du jeu', 'error');
|
||||
this.goBack();
|
||||
} finally {
|
||||
Utils.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
// Masquer toutes les pages
|
||||
hideAllPages() {
|
||||
document.querySelectorAll('.page').forEach(page => {
|
||||
page.classList.remove('active');
|
||||
});
|
||||
},
|
||||
|
||||
// Rendu grille des jeux
|
||||
renderGamesGrid() {
|
||||
const grid = document.getElementById('games-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (!this.gamesConfig) return;
|
||||
|
||||
Object.entries(this.gamesConfig.games).forEach(([key, game]) => {
|
||||
if (game.enabled) {
|
||||
const card = this.createGameCard(key, game);
|
||||
grid.appendChild(card);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Création d'une carte de jeu
|
||||
createGameCard(gameKey, gameInfo) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'game-card';
|
||||
card.innerHTML = `
|
||||
<div class="icon">${gameInfo.icon}</div>
|
||||
<div class="title">${gameInfo.name}</div>
|
||||
<div class="description">${gameInfo.description}</div>
|
||||
`;
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
Utils.animateElement(card, 'pulse');
|
||||
this.navigateTo('levels', gameKey);
|
||||
});
|
||||
|
||||
return card;
|
||||
},
|
||||
|
||||
// Rendu grille des niveaux
|
||||
async renderLevelsGrid(gameType) {
|
||||
const grid = document.getElementById('levels-grid');
|
||||
grid.innerHTML = '<div class="loading-content">🔍 Recherche du contenu disponible...</div>';
|
||||
|
||||
try {
|
||||
// Obtenir tout le contenu disponible automatiquement
|
||||
const availableContent = await this.contentScanner.getAvailableContent();
|
||||
|
||||
if (availableContent.length === 0) {
|
||||
grid.innerHTML = '<div class="no-content">Aucun contenu trouvé</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Effacer le loading
|
||||
grid.innerHTML = '';
|
||||
|
||||
// Filtrer par compatibilité avec le jeu si possible
|
||||
const compatibleContent = await this.contentScanner.getContentByGame(gameType);
|
||||
const contentToShow = compatibleContent.length > 0 ? compatibleContent : availableContent;
|
||||
|
||||
console.log(`📋 Affichage de ${contentToShow.length} modules pour ${gameType}`);
|
||||
|
||||
// Créer les cartes pour chaque contenu trouvé
|
||||
contentToShow.forEach(content => {
|
||||
const card = this.createLevelCard(content.id, content, gameType);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
// Ajouter info de compatibilité si filtré
|
||||
if (compatibleContent.length > 0 && compatibleContent.length < availableContent.length) {
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'content-info';
|
||||
infoDiv.innerHTML = `
|
||||
<p><em>Affichage des contenus les plus compatibles avec ${gameType}</em></p>
|
||||
<button onclick="AppNavigation.showAllContent('${gameType}')" class="show-all-btn">
|
||||
Voir tous les contenus (${availableContent.length})
|
||||
</button>
|
||||
`;
|
||||
grid.appendChild(infoDiv);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur rendu levels:', error);
|
||||
grid.innerHTML = '<div class="error-content">❌ Erreur lors du chargement du contenu</div>';
|
||||
}
|
||||
},
|
||||
|
||||
// Méthode pour afficher tout le contenu
|
||||
async showAllContent(gameType) {
|
||||
const grid = document.getElementById('levels-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
const availableContent = await this.contentScanner.getAvailableContent();
|
||||
|
||||
availableContent.forEach(content => {
|
||||
const card = this.createLevelCard(content.id, content, gameType);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
},
|
||||
|
||||
// Création d'une carte de niveau
|
||||
createLevelCard(contentKey, contentInfo, gameType) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'level-card';
|
||||
|
||||
// Calculer les statistiques à afficher
|
||||
const stats = [];
|
||||
if (contentInfo.stats) {
|
||||
if (contentInfo.stats.vocabularyCount > 0) {
|
||||
stats.push(`📚 ${contentInfo.stats.vocabularyCount} mots`);
|
||||
}
|
||||
if (contentInfo.stats.sentenceCount > 0) {
|
||||
stats.push(`💬 ${contentInfo.stats.sentenceCount} phrases`);
|
||||
}
|
||||
if (contentInfo.stats.dialogueCount > 0) {
|
||||
stats.push(`🎭 ${contentInfo.stats.dialogueCount} dialogues`);
|
||||
}
|
||||
}
|
||||
|
||||
// Indicateur de compatibilité
|
||||
const compatibility = contentInfo.gameCompatibility?.[gameType];
|
||||
const compatScore = compatibility?.score || 0;
|
||||
const compatClass = compatScore > 70 ? 'high-compat' : compatScore > 40 ? 'medium-compat' : 'low-compat';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-header">
|
||||
<div class="icon">${contentInfo.icon}</div>
|
||||
${compatibility ? `<div class="compatibility ${compatClass}" title="Compatibilité: ${compatScore}%">
|
||||
${compatScore > 70 ? '🟢' : compatScore > 40 ? '🟡' : '🟠'}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<div class="title">${contentInfo.name}</div>
|
||||
<div class="description">${contentInfo.description}</div>
|
||||
<div class="content-stats">
|
||||
<span class="difficulty-badge difficulty-${contentInfo.difficulty}">${contentInfo.difficulty}</span>
|
||||
<span class="items-count">${contentInfo.metadata.totalItems} éléments</span>
|
||||
<span class="time-estimate">~${contentInfo.metadata.estimatedTime}min</span>
|
||||
</div>
|
||||
${stats.length > 0 ? `<div class="detailed-stats">${stats.join(' • ')}</div>` : ''}
|
||||
`;
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
Utils.animateElement(card, 'pulse');
|
||||
this.navigateTo('play', gameType, contentKey);
|
||||
});
|
||||
|
||||
return card;
|
||||
},
|
||||
|
||||
// Mise à jour du breadcrumb
|
||||
updateBreadcrumb() {
|
||||
const breadcrumb = document.getElementById('breadcrumb');
|
||||
breadcrumb.innerHTML = '';
|
||||
|
||||
const params = Utils.getUrlParams();
|
||||
|
||||
// Accueil
|
||||
const homeItem = this.createBreadcrumbItem('🏠 Accueil', 'home',
|
||||
this.currentPage === 'home');
|
||||
breadcrumb.appendChild(homeItem);
|
||||
|
||||
// Jeux
|
||||
if (['games', 'levels', 'play'].includes(this.currentPage)) {
|
||||
const gamesItem = this.createBreadcrumbItem('🎮 Jeux', 'games',
|
||||
this.currentPage === 'games');
|
||||
breadcrumb.appendChild(gamesItem);
|
||||
}
|
||||
|
||||
// Niveaux
|
||||
if (['levels', 'play'].includes(this.currentPage) && params.game) {
|
||||
const gameInfo = this.gamesConfig?.games[params.game];
|
||||
const levelText = gameInfo ? `${gameInfo.icon} ${gameInfo.name}` : 'Niveaux';
|
||||
const levelsItem = this.createBreadcrumbItem(levelText, 'levels',
|
||||
this.currentPage === 'levels');
|
||||
breadcrumb.appendChild(levelsItem);
|
||||
}
|
||||
|
||||
// Jeu en cours
|
||||
if (this.currentPage === 'play' && params.content) {
|
||||
const contentInfo = this.gamesConfig?.content[params.content];
|
||||
const playText = contentInfo ? `🎯 ${contentInfo.name}` : 'Jeu';
|
||||
const playItem = this.createBreadcrumbItem(playText, 'play', true);
|
||||
breadcrumb.appendChild(playItem);
|
||||
}
|
||||
},
|
||||
|
||||
// Création d'un élément breadcrumb
|
||||
createBreadcrumbItem(text, page, isActive) {
|
||||
const item = document.createElement('button');
|
||||
item.className = `breadcrumb-item ${isActive ? 'active' : ''}`;
|
||||
item.textContent = text;
|
||||
item.dataset.page = page;
|
||||
|
||||
if (!isActive) {
|
||||
item.addEventListener('click', () => {
|
||||
const params = Utils.getUrlParams();
|
||||
|
||||
if (page === 'home') {
|
||||
this.navigateTo('home');
|
||||
} else if (page === 'games') {
|
||||
this.navigateTo('games');
|
||||
} else if (page === 'levels') {
|
||||
this.navigateTo('levels', params.game);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonctions globales pour l'HTML
|
||||
window.navigateTo = (page, game, content) => AppNavigation.navigateTo(page, game, content);
|
||||
window.goBack = () => AppNavigation.goBack();
|
||||
|
||||
// Export
|
||||
window.AppNavigation = AppNavigation;
|
||||
176
js/core/utils.js
Normal file
176
js/core/utils.js
Normal file
@ -0,0 +1,176 @@
|
||||
// === UTILITIES GÉNÉRALES ===
|
||||
|
||||
const Utils = {
|
||||
// Gestion des paramètres URL
|
||||
getUrlParams() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
page: params.get('page') || 'home',
|
||||
game: params.get('game') || null,
|
||||
content: params.get('content') || null
|
||||
};
|
||||
},
|
||||
|
||||
setUrlParams(params) {
|
||||
const url = new URL(window.location);
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key]) {
|
||||
url.searchParams.set(key, params[key]);
|
||||
} else {
|
||||
url.searchParams.delete(key);
|
||||
}
|
||||
});
|
||||
window.history.pushState({}, '', url);
|
||||
},
|
||||
|
||||
// Affichage/masquage du loading
|
||||
showLoading() {
|
||||
document.getElementById('loading').classList.add('show');
|
||||
},
|
||||
|
||||
hideLoading() {
|
||||
document.getElementById('loading').classList.remove('show');
|
||||
},
|
||||
|
||||
// Notifications toast
|
||||
showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${type === 'success' ? 'var(--secondary-color)' :
|
||||
type === 'error' ? 'var(--error-color)' : 'var(--primary-color)'};
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 1001;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease-in';
|
||||
setTimeout(() => document.body.removeChild(toast), 300);
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
// Animation d'éléments
|
||||
animateElement(element, animation, duration = 300) {
|
||||
return new Promise(resolve => {
|
||||
element.style.animation = `${animation} ${duration}ms ease-out`;
|
||||
setTimeout(() => {
|
||||
element.style.animation = '';
|
||||
resolve();
|
||||
}, duration);
|
||||
});
|
||||
},
|
||||
|
||||
// Génération d'ID unique
|
||||
generateId() {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
},
|
||||
|
||||
// Mélange d'array (Fisher-Yates)
|
||||
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;
|
||||
},
|
||||
|
||||
// Sélection aléatoire d'éléments
|
||||
getRandomItems(array, count) {
|
||||
const shuffled = this.shuffleArray(array);
|
||||
return shuffled.slice(0, count);
|
||||
},
|
||||
|
||||
// Formatage du temps
|
||||
formatTime(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
// Debounce pour événements
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
// Vérification de support audio
|
||||
canPlayAudio() {
|
||||
return 'Audio' in window;
|
||||
},
|
||||
|
||||
// Chargement d'image avec promise
|
||||
loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
},
|
||||
|
||||
// Stockage local sécurisé
|
||||
storage: {
|
||||
set(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (e) {
|
||||
console.warn('LocalStorage not available:', e);
|
||||
}
|
||||
},
|
||||
|
||||
get(key, defaultValue = null) {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (e) {
|
||||
console.warn('LocalStorage read error:', e);
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (e) {
|
||||
console.warn('LocalStorage remove error:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Ajout de styles CSS dynamiques pour les animations
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleSheet);
|
||||
|
||||
// Export global
|
||||
window.Utils = Utils;
|
||||
950
js/games/adventure-reader.js
Normal file
950
js/games/adventure-reader.js
Normal file
@ -0,0 +1,950 @@
|
||||
// === MODULE ADVENTURE READER (ZELDA-STYLE) ===
|
||||
|
||||
class AdventureReaderGame {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.content = options.content;
|
||||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||||
this.onGameEnd = options.onGameEnd || (() => {});
|
||||
|
||||
// Game state
|
||||
this.score = 0;
|
||||
this.currentSentenceIndex = 0;
|
||||
this.currentVocabIndex = 0;
|
||||
this.potsDestroyed = 0;
|
||||
this.enemiesDefeated = 0;
|
||||
this.isGamePaused = false;
|
||||
|
||||
// Game objects
|
||||
this.pots = [];
|
||||
this.enemies = [];
|
||||
this.player = { x: 0, y: 0 }; // Will be set when map is created
|
||||
this.isPlayerMoving = false;
|
||||
this.isPlayerInvulnerable = false;
|
||||
this.invulnerabilityTimeout = null;
|
||||
|
||||
// Content extraction
|
||||
this.vocabulary = this.extractVocabulary(this.content);
|
||||
this.sentences = this.extractSentences(this.content);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if ((!this.vocabulary || this.vocabulary.length === 0) &&
|
||||
(!this.sentences || this.sentences.length === 0)) {
|
||||
console.error('No content available for Adventure Reader');
|
||||
this.showInitError();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createGameInterface();
|
||||
this.initializePlayer();
|
||||
this.setupEventListeners();
|
||||
this.generateGameObjects();
|
||||
this.generateDecorations();
|
||||
this.startGameLoop();
|
||||
}
|
||||
|
||||
showInitError() {
|
||||
this.container.innerHTML = `
|
||||
<div class="game-error">
|
||||
<h3>❌ Error loading</h3>
|
||||
<p>This content doesn't contain texts compatible with Adventure Reader.</p>
|
||||
<button onclick="AppNavigation.goBack()" class="back-btn">← Back</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
extractVocabulary(content) {
|
||||
let vocabulary = [];
|
||||
|
||||
if (content.rawContent && content.rawContent.vocabulary) {
|
||||
if (typeof content.rawContent.vocabulary === 'object' && !Array.isArray(content.rawContent.vocabulary)) {
|
||||
vocabulary = Object.entries(content.rawContent.vocabulary).map(([english, translation]) => ({
|
||||
english: english,
|
||||
translation: translation
|
||||
}));
|
||||
} else if (Array.isArray(content.rawContent.vocabulary)) {
|
||||
vocabulary = content.rawContent.vocabulary;
|
||||
}
|
||||
}
|
||||
|
||||
return vocabulary.filter(item => item && item.english && item.translation);
|
||||
}
|
||||
|
||||
extractSentences(content) {
|
||||
let sentences = [];
|
||||
|
||||
if (content.rawContent) {
|
||||
if (content.rawContent.sentences && Array.isArray(content.rawContent.sentences)) {
|
||||
sentences = content.rawContent.sentences;
|
||||
} else if (content.rawContent.texts && Array.isArray(content.rawContent.texts)) {
|
||||
// Extract sentences from texts
|
||||
content.rawContent.texts.forEach(text => {
|
||||
const textSentences = text.content.split(/[.!?]+/).filter(s => s.trim().length > 10);
|
||||
textSentences.forEach(sentence => {
|
||||
sentences.push({
|
||||
english: sentence.trim() + '.',
|
||||
translation: sentence.trim() + '.' // Fallback
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sentences.filter(item => item && item.english);
|
||||
}
|
||||
|
||||
createGameInterface() {
|
||||
this.container.innerHTML = `
|
||||
<div class="adventure-reader-wrapper">
|
||||
<!-- Game HUD -->
|
||||
<div class="game-hud">
|
||||
<div class="hud-left">
|
||||
<div class="stat-item">
|
||||
<span class="stat-icon">🏆</span>
|
||||
<span id="score-display">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-icon">🏺</span>
|
||||
<span id="pots-counter">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-icon">⚔️</span>
|
||||
<span id="enemies-counter">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hud-right">
|
||||
<div class="progress-info">
|
||||
<span id="progress-text">Start your adventure!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Map -->
|
||||
<div class="game-map" id="game-map">
|
||||
<!-- Player -->
|
||||
<div class="player" id="player">🧙♂️</div>
|
||||
|
||||
<!-- Game objects will be generated here -->
|
||||
</div>
|
||||
|
||||
<!-- Game Controls -->
|
||||
<div class="game-controls">
|
||||
<div class="instructions">
|
||||
Click 🏺 pots for vocabulary • Click 👹 enemies for sentences
|
||||
</div>
|
||||
<button class="control-btn secondary" id="restart-btn">🔄 Restart Adventure</button>
|
||||
</div>
|
||||
|
||||
<!-- Reading Modal -->
|
||||
<div class="reading-modal" id="reading-modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modal-title">Enemy Defeated!</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="reading-content" id="reading-content">
|
||||
<!-- Sentence content -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="control-btn primary" id="continue-btn">Continue Adventure →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vocab Popup -->
|
||||
<div class="vocab-popup" id="vocab-popup" style="display: none;">
|
||||
<div class="popup-content">
|
||||
<div class="vocab-word" id="vocab-word"></div>
|
||||
<div class="vocab-translation" id="vocab-translation"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
initializePlayer() {
|
||||
// Set player initial position to center of map
|
||||
const gameMap = document.getElementById('game-map');
|
||||
const mapRect = gameMap.getBoundingClientRect();
|
||||
this.player.x = mapRect.width / 2 - 20; // -20 for half player width
|
||||
this.player.y = mapRect.height / 2 - 20; // -20 for half player height
|
||||
|
||||
const playerElement = document.getElementById('player');
|
||||
playerElement.style.left = this.player.x + 'px';
|
||||
playerElement.style.top = this.player.y + 'px';
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
|
||||
document.getElementById('continue-btn').addEventListener('click', () => this.closeModal());
|
||||
|
||||
// Map click handler
|
||||
const gameMap = document.getElementById('game-map');
|
||||
gameMap.addEventListener('click', (e) => this.handleMapClick(e));
|
||||
|
||||
// Window resize handler
|
||||
window.addEventListener('resize', () => {
|
||||
setTimeout(() => this.initializePlayer(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
generateGameObjects() {
|
||||
const gameMap = document.getElementById('game-map');
|
||||
|
||||
// Clear existing objects
|
||||
gameMap.querySelectorAll('.pot, .enemy').forEach(el => el.remove());
|
||||
|
||||
this.pots = [];
|
||||
this.enemies = [];
|
||||
|
||||
// Generate pots (for vocabulary)
|
||||
const numPots = Math.min(8, this.vocabulary.length);
|
||||
for (let i = 0; i < numPots; i++) {
|
||||
const pot = this.createPot();
|
||||
this.pots.push(pot);
|
||||
gameMap.appendChild(pot.element);
|
||||
}
|
||||
|
||||
// Generate enemies (for sentences) - spawn across entire viewport
|
||||
const numEnemies = Math.min(8, this.sentences.length);
|
||||
for (let i = 0; i < numEnemies; i++) {
|
||||
const enemy = this.createEnemy();
|
||||
this.enemies.push(enemy);
|
||||
gameMap.appendChild(enemy.element);
|
||||
}
|
||||
|
||||
this.updateHUD();
|
||||
}
|
||||
|
||||
createPot() {
|
||||
const pot = document.createElement('div');
|
||||
pot.className = 'pot';
|
||||
pot.innerHTML = '🏺';
|
||||
|
||||
const position = this.getRandomPosition();
|
||||
pot.style.left = position.x + 'px';
|
||||
pot.style.top = position.y + 'px';
|
||||
|
||||
return {
|
||||
element: pot,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
destroyed: false
|
||||
};
|
||||
}
|
||||
|
||||
createEnemy() {
|
||||
const enemy = document.createElement('div');
|
||||
enemy.className = 'enemy';
|
||||
enemy.innerHTML = '👹';
|
||||
|
||||
const position = this.getRandomPosition(true); // Force away from player
|
||||
enemy.style.left = position.x + 'px';
|
||||
enemy.style.top = position.y + 'px';
|
||||
|
||||
// Random movement pattern for each enemy
|
||||
const patterns = ['patrol', 'chase', 'wander', 'circle'];
|
||||
const pattern = patterns[Math.floor(Math.random() * patterns.length)];
|
||||
|
||||
return {
|
||||
element: enemy,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
defeated: false,
|
||||
moveDirection: Math.random() * Math.PI * 2,
|
||||
speed: 0.6 + Math.random() * 0.6, // Reduced speed
|
||||
pattern: pattern,
|
||||
patrolStartX: position.x,
|
||||
patrolStartY: position.y,
|
||||
patrolDistance: 80 + Math.random() * 60,
|
||||
circleCenter: { x: position.x, y: position.y },
|
||||
circleRadius: 60 + Math.random() * 40,
|
||||
circleAngle: Math.random() * Math.PI * 2,
|
||||
changeDirectionTimer: 0,
|
||||
dashCooldown: 0,
|
||||
isDashing: false
|
||||
};
|
||||
}
|
||||
|
||||
getRandomPosition(forceAwayFromPlayer = false) {
|
||||
const gameMap = document.getElementById('game-map');
|
||||
const mapRect = gameMap.getBoundingClientRect();
|
||||
const mapWidth = mapRect.width;
|
||||
const mapHeight = mapRect.height;
|
||||
const margin = 40;
|
||||
|
||||
let x, y;
|
||||
let tooClose;
|
||||
const minDistance = forceAwayFromPlayer ? 150 : 80;
|
||||
|
||||
do {
|
||||
x = margin + Math.random() * (mapWidth - margin * 2);
|
||||
y = margin + Math.random() * (mapHeight - margin * 2);
|
||||
|
||||
// Check distance from player
|
||||
const distFromPlayer = Math.sqrt(
|
||||
Math.pow(x - this.player.x, 2) + Math.pow(y - this.player.y, 2)
|
||||
);
|
||||
tooClose = distFromPlayer < minDistance;
|
||||
|
||||
} while (tooClose);
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
handleMapClick(e) {
|
||||
if (this.isGamePaused || this.isPlayerMoving) return;
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const clickY = e.clientY - rect.top;
|
||||
|
||||
// Check pot clicks
|
||||
let targetFound = false;
|
||||
this.pots.forEach(pot => {
|
||||
if (!pot.destroyed && this.isNearPosition(clickX, clickY, pot)) {
|
||||
this.movePlayerToTarget(pot, 'pot');
|
||||
targetFound = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check enemy clicks (only if no pot was clicked)
|
||||
if (!targetFound) {
|
||||
this.enemies.forEach(enemy => {
|
||||
if (!enemy.defeated && this.isNearPosition(clickX, clickY, enemy)) {
|
||||
this.movePlayerToTarget(enemy, 'enemy');
|
||||
targetFound = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If no target found, move to empty area
|
||||
if (!targetFound) {
|
||||
this.movePlayerToPosition(clickX, clickY);
|
||||
}
|
||||
}
|
||||
|
||||
isNearPosition(clickX, clickY, object) {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(clickX - (object.x + 20), 2) + Math.pow(clickY - (object.y + 20), 2)
|
||||
);
|
||||
return distance < 60; // Larger clickable area
|
||||
}
|
||||
|
||||
movePlayerToTarget(target, type) {
|
||||
this.isPlayerMoving = true;
|
||||
const playerElement = document.getElementById('player');
|
||||
|
||||
// Grant invulnerability IMMEDIATELY when attacking an enemy
|
||||
if (type === 'enemy') {
|
||||
this.grantAttackInvulnerability();
|
||||
}
|
||||
|
||||
// Calculate target position (near the object)
|
||||
const targetX = target.x;
|
||||
const targetY = target.y;
|
||||
|
||||
// Update player position
|
||||
this.player.x = targetX;
|
||||
this.player.y = targetY;
|
||||
|
||||
// Animate player movement
|
||||
playerElement.style.left = targetX + 'px';
|
||||
playerElement.style.top = targetY + 'px';
|
||||
|
||||
// Add walking animation
|
||||
playerElement.style.transform = 'scale(1.1)';
|
||||
|
||||
// Wait for movement animation to complete, then interact
|
||||
setTimeout(() => {
|
||||
playerElement.style.transform = 'scale(1)';
|
||||
this.isPlayerMoving = false;
|
||||
|
||||
if (type === 'pot') {
|
||||
this.destroyPot(target);
|
||||
} else if (type === 'enemy') {
|
||||
this.defeatEnemy(target);
|
||||
}
|
||||
}, 800); // Match CSS transition duration
|
||||
}
|
||||
|
||||
movePlayerToPosition(targetX, targetY) {
|
||||
this.isPlayerMoving = true;
|
||||
const playerElement = document.getElementById('player');
|
||||
|
||||
// Update player position
|
||||
this.player.x = targetX - 20; // Center the player on click point
|
||||
this.player.y = targetY - 20;
|
||||
|
||||
// Keep player within bounds
|
||||
const gameMap = document.getElementById('game-map');
|
||||
const mapRect = gameMap.getBoundingClientRect();
|
||||
const margin = 20;
|
||||
|
||||
this.player.x = Math.max(margin, Math.min(mapRect.width - 60, this.player.x));
|
||||
this.player.y = Math.max(margin, Math.min(mapRect.height - 60, this.player.y));
|
||||
|
||||
// Animate player movement
|
||||
playerElement.style.left = this.player.x + 'px';
|
||||
playerElement.style.top = this.player.y + 'px';
|
||||
|
||||
// Add walking animation
|
||||
playerElement.style.transform = 'scale(1.1)';
|
||||
|
||||
// Reset animation after movement
|
||||
setTimeout(() => {
|
||||
playerElement.style.transform = 'scale(1)';
|
||||
this.isPlayerMoving = false;
|
||||
}, 800);
|
||||
}
|
||||
|
||||
destroyPot(pot) {
|
||||
pot.destroyed = true;
|
||||
pot.element.classList.add('destroyed');
|
||||
|
||||
// Animation
|
||||
pot.element.innerHTML = '💥';
|
||||
setTimeout(() => {
|
||||
pot.element.style.opacity = '0.3';
|
||||
pot.element.innerHTML = '💨';
|
||||
}, 200);
|
||||
|
||||
this.potsDestroyed++;
|
||||
this.score += 10;
|
||||
|
||||
// Show vocabulary
|
||||
if (this.currentVocabIndex < this.vocabulary.length) {
|
||||
this.showVocabPopup(this.vocabulary[this.currentVocabIndex]);
|
||||
this.currentVocabIndex++;
|
||||
}
|
||||
|
||||
this.updateHUD();
|
||||
this.checkGameComplete();
|
||||
}
|
||||
|
||||
defeatEnemy(enemy) {
|
||||
enemy.defeated = true;
|
||||
enemy.element.classList.add('defeated');
|
||||
|
||||
// Animation
|
||||
enemy.element.innerHTML = '☠️';
|
||||
setTimeout(() => {
|
||||
enemy.element.style.opacity = '0.3';
|
||||
}, 300);
|
||||
|
||||
this.enemiesDefeated++;
|
||||
this.score += 25;
|
||||
|
||||
// Invulnerability is already granted at start of movement
|
||||
// Just refresh the timer to ensure full 2 seconds from now
|
||||
this.refreshAttackInvulnerability();
|
||||
|
||||
// Show sentence (pause game)
|
||||
if (this.currentSentenceIndex < this.sentences.length) {
|
||||
this.showReadingModal(this.sentences[this.currentSentenceIndex]);
|
||||
this.currentSentenceIndex++;
|
||||
}
|
||||
|
||||
this.updateHUD();
|
||||
}
|
||||
|
||||
showVocabPopup(vocab) {
|
||||
const popup = document.getElementById('vocab-popup');
|
||||
const wordEl = document.getElementById('vocab-word');
|
||||
const translationEl = document.getElementById('vocab-translation');
|
||||
|
||||
wordEl.textContent = vocab.english;
|
||||
translationEl.textContent = vocab.translation;
|
||||
|
||||
popup.style.display = 'block';
|
||||
popup.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
popup.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
popup.style.display = 'none';
|
||||
}, 300);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
showReadingModal(sentence) {
|
||||
this.isGamePaused = true;
|
||||
const modal = document.getElementById('reading-modal');
|
||||
const content = document.getElementById('reading-content');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="sentence-content">
|
||||
<p class="english-text">${sentence.english}</p>
|
||||
${sentence.translation ? `<p class="translation-text">${sentence.translation}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
modal.classList.add('show');
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
const modal = document.getElementById('reading-modal');
|
||||
modal.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
this.isGamePaused = false;
|
||||
}, 300);
|
||||
|
||||
this.checkGameComplete();
|
||||
}
|
||||
|
||||
checkGameComplete() {
|
||||
const allPotsDestroyed = this.pots.every(pot => pot.destroyed);
|
||||
const allEnemiesDefeated = this.enemies.every(enemy => enemy.defeated);
|
||||
|
||||
if (allPotsDestroyed && allEnemiesDefeated) {
|
||||
setTimeout(() => {
|
||||
this.gameComplete();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
gameComplete() {
|
||||
// Bonus for completion
|
||||
this.score += 100;
|
||||
this.updateHUD();
|
||||
|
||||
document.getElementById('progress-text').textContent = '🏆 Adventure Complete!';
|
||||
|
||||
setTimeout(() => {
|
||||
this.onGameEnd(this.score);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
updateHUD() {
|
||||
document.getElementById('score-display').textContent = this.score;
|
||||
document.getElementById('pots-counter').textContent = this.potsDestroyed;
|
||||
document.getElementById('enemies-counter').textContent = this.enemiesDefeated;
|
||||
|
||||
const totalObjects = this.pots.length + this.enemies.length;
|
||||
const destroyedObjects = this.potsDestroyed + this.enemiesDefeated;
|
||||
|
||||
document.getElementById('progress-text').textContent =
|
||||
`Progress: ${destroyedObjects}/${totalObjects} objects`;
|
||||
|
||||
this.onScoreUpdate(this.score);
|
||||
}
|
||||
|
||||
generateDecorations() {
|
||||
const gameMap = document.getElementById('game-map');
|
||||
const mapRect = gameMap.getBoundingClientRect();
|
||||
const mapWidth = mapRect.width;
|
||||
const mapHeight = mapRect.height;
|
||||
|
||||
// Remove existing decorations
|
||||
gameMap.querySelectorAll('.decoration').forEach(el => el.remove());
|
||||
|
||||
// Generate trees (fewer, larger)
|
||||
const numTrees = 4 + Math.floor(Math.random() * 4); // 4-7 trees
|
||||
for (let i = 0; i < numTrees; i++) {
|
||||
const tree = document.createElement('div');
|
||||
tree.className = 'decoration tree';
|
||||
tree.innerHTML = Math.random() < 0.5 ? '🌳' : '🌲';
|
||||
|
||||
const position = this.getDecorationPosition(mapWidth, mapHeight, 60); // Keep away from objects
|
||||
tree.style.left = position.x + 'px';
|
||||
tree.style.top = position.y + 'px';
|
||||
tree.style.fontSize = (25 + Math.random() * 15) + 'px'; // Random size
|
||||
|
||||
gameMap.appendChild(tree);
|
||||
}
|
||||
|
||||
// Generate grass patches (many, small)
|
||||
const numGrass = 15 + Math.floor(Math.random() * 10); // 15-24 grass
|
||||
for (let i = 0; i < numGrass; i++) {
|
||||
const grass = document.createElement('div');
|
||||
grass.className = 'decoration grass';
|
||||
const grassTypes = ['🌿', '🌱', '🍀', '🌾'];
|
||||
grass.innerHTML = grassTypes[Math.floor(Math.random() * grassTypes.length)];
|
||||
|
||||
const position = this.getDecorationPosition(mapWidth, mapHeight, 30); // Smaller keepaway
|
||||
grass.style.left = position.x + 'px';
|
||||
grass.style.top = position.y + 'px';
|
||||
grass.style.fontSize = (15 + Math.random() * 8) + 'px'; // Smaller size
|
||||
|
||||
gameMap.appendChild(grass);
|
||||
}
|
||||
|
||||
// Generate rocks (medium amount)
|
||||
const numRocks = 3 + Math.floor(Math.random() * 3); // 3-5 rocks
|
||||
for (let i = 0; i < numRocks; i++) {
|
||||
const rock = document.createElement('div');
|
||||
rock.className = 'decoration rock';
|
||||
rock.innerHTML = Math.random() < 0.5 ? '🪨' : '⛰️';
|
||||
|
||||
const position = this.getDecorationPosition(mapWidth, mapHeight, 40);
|
||||
rock.style.left = position.x + 'px';
|
||||
rock.style.top = position.y + 'px';
|
||||
rock.style.fontSize = (20 + Math.random() * 10) + 'px';
|
||||
|
||||
gameMap.appendChild(rock);
|
||||
}
|
||||
}
|
||||
|
||||
getDecorationPosition(mapWidth, mapHeight, keepAwayDistance) {
|
||||
const margin = 20;
|
||||
let x, y;
|
||||
let attempts = 0;
|
||||
let validPosition = false;
|
||||
|
||||
do {
|
||||
x = margin + Math.random() * (mapWidth - margin * 2);
|
||||
y = margin + Math.random() * (mapHeight - margin * 2);
|
||||
|
||||
// Check distance from player
|
||||
const distFromPlayer = Math.sqrt(
|
||||
Math.pow(x - this.player.x, 2) + Math.pow(y - this.player.y, 2)
|
||||
);
|
||||
|
||||
// Check distance from pots and enemies
|
||||
let tooClose = distFromPlayer < keepAwayDistance;
|
||||
|
||||
if (!tooClose) {
|
||||
this.pots.forEach(pot => {
|
||||
const dist = Math.sqrt(Math.pow(x - pot.x, 2) + Math.pow(y - pot.y, 2));
|
||||
if (dist < keepAwayDistance) tooClose = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!tooClose) {
|
||||
this.enemies.forEach(enemy => {
|
||||
const dist = Math.sqrt(Math.pow(x - enemy.x, 2) + Math.pow(y - enemy.y, 2));
|
||||
if (dist < keepAwayDistance) tooClose = true;
|
||||
});
|
||||
}
|
||||
|
||||
validPosition = !tooClose;
|
||||
attempts++;
|
||||
|
||||
} while (!validPosition && attempts < 50);
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
startGameLoop() {
|
||||
const animate = () => {
|
||||
if (!this.isGamePaused) {
|
||||
this.moveEnemies();
|
||||
}
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
animate();
|
||||
}
|
||||
|
||||
moveEnemies() {
|
||||
const gameMap = document.getElementById('game-map');
|
||||
const mapRect = gameMap.getBoundingClientRect();
|
||||
const mapWidth = mapRect.width;
|
||||
const mapHeight = mapRect.height;
|
||||
|
||||
this.enemies.forEach(enemy => {
|
||||
if (enemy.defeated) return;
|
||||
|
||||
// Apply movement pattern
|
||||
this.applyMovementPattern(enemy, mapWidth, mapHeight);
|
||||
|
||||
// Bounce off walls (using dynamic map size)
|
||||
if (enemy.x < 10 || enemy.x > mapWidth - 50) {
|
||||
enemy.moveDirection = Math.PI - enemy.moveDirection;
|
||||
enemy.x = Math.max(10, Math.min(mapWidth - 50, enemy.x));
|
||||
}
|
||||
if (enemy.y < 10 || enemy.y > mapHeight - 50) {
|
||||
enemy.moveDirection = -enemy.moveDirection;
|
||||
enemy.y = Math.max(10, Math.min(mapHeight - 50, enemy.y));
|
||||
}
|
||||
|
||||
enemy.element.style.left = enemy.x + 'px';
|
||||
enemy.element.style.top = enemy.y + 'px';
|
||||
|
||||
// Check collision with player
|
||||
this.checkPlayerEnemyCollision(enemy);
|
||||
});
|
||||
}
|
||||
|
||||
applyMovementPattern(enemy, mapWidth, mapHeight) {
|
||||
enemy.changeDirectionTimer++;
|
||||
|
||||
switch (enemy.pattern) {
|
||||
case 'patrol':
|
||||
// Patrol back and forth
|
||||
const distanceFromStart = Math.sqrt(
|
||||
Math.pow(enemy.x - enemy.patrolStartX, 2) + Math.pow(enemy.y - enemy.patrolStartY, 2)
|
||||
);
|
||||
|
||||
if (distanceFromStart > enemy.patrolDistance) {
|
||||
// Turn around and head back to start
|
||||
const angleToStart = Math.atan2(
|
||||
enemy.patrolStartY - enemy.y,
|
||||
enemy.patrolStartX - enemy.x
|
||||
);
|
||||
enemy.moveDirection = angleToStart;
|
||||
}
|
||||
|
||||
if (enemy.changeDirectionTimer > 120) { // Change direction every ~2 seconds
|
||||
enemy.moveDirection += (Math.random() - 0.5) * Math.PI * 0.5;
|
||||
enemy.changeDirectionTimer = 0;
|
||||
}
|
||||
|
||||
enemy.x += Math.cos(enemy.moveDirection) * enemy.speed;
|
||||
enemy.y += Math.sin(enemy.moveDirection) * enemy.speed;
|
||||
break;
|
||||
|
||||
case 'chase':
|
||||
enemy.dashCooldown--;
|
||||
|
||||
if (enemy.isDashing) {
|
||||
// Continue dash movement with very high speed
|
||||
enemy.x += Math.cos(enemy.moveDirection) * (enemy.speed * 6);
|
||||
enemy.y += Math.sin(enemy.moveDirection) * (enemy.speed * 6);
|
||||
enemy.dashCooldown--;
|
||||
|
||||
if (enemy.dashCooldown <= 0) {
|
||||
enemy.isDashing = false;
|
||||
enemy.dashCooldown = 120 + Math.random() * 60; // Reset cooldown
|
||||
}
|
||||
} else {
|
||||
// Normal chase behavior
|
||||
const angleToPlayer = Math.atan2(
|
||||
this.player.y - enemy.y,
|
||||
this.player.x - enemy.x
|
||||
);
|
||||
|
||||
// Sometimes do a perpendicular dash
|
||||
if (enemy.dashCooldown <= 0 && Math.random() < 0.3) {
|
||||
enemy.isDashing = true;
|
||||
enemy.dashCooldown = 50; // Much longer dash duration
|
||||
|
||||
// Perpendicular angle (90 degrees from player direction)
|
||||
const perpAngle = angleToPlayer + (Math.random() < 0.5 ? Math.PI/2 : -Math.PI/2);
|
||||
enemy.moveDirection = perpAngle;
|
||||
|
||||
// Start dash with visual effect
|
||||
enemy.element.style.filter = 'drop-shadow(0 0 8px red)';
|
||||
setTimeout(() => {
|
||||
enemy.element.style.filter = 'drop-shadow(1px 1px 2px rgba(0,0,0,0.3))';
|
||||
}, 300);
|
||||
} else {
|
||||
// Mix chasing with some randomness
|
||||
enemy.moveDirection = angleToPlayer + (Math.random() - 0.5) * 0.3;
|
||||
|
||||
enemy.x += Math.cos(enemy.moveDirection) * (enemy.speed * 0.8);
|
||||
enemy.y += Math.sin(enemy.moveDirection) * (enemy.speed * 0.8);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'wander':
|
||||
// Random wandering
|
||||
if (enemy.changeDirectionTimer > 60 + Math.random() * 60) {
|
||||
enemy.moveDirection += (Math.random() - 0.5) * Math.PI;
|
||||
enemy.changeDirectionTimer = 0;
|
||||
}
|
||||
|
||||
enemy.x += Math.cos(enemy.moveDirection) * enemy.speed;
|
||||
enemy.y += Math.sin(enemy.moveDirection) * enemy.speed;
|
||||
break;
|
||||
|
||||
case 'circle':
|
||||
// Move in circular pattern
|
||||
enemy.circleAngle += 0.03 + (enemy.speed * 0.01);
|
||||
|
||||
enemy.x = enemy.circleCenter.x + Math.cos(enemy.circleAngle) * enemy.circleRadius;
|
||||
enemy.y = enemy.circleCenter.y + Math.sin(enemy.circleAngle) * enemy.circleRadius;
|
||||
|
||||
// Occasionally change circle center
|
||||
if (enemy.changeDirectionTimer > 180) {
|
||||
enemy.circleCenter.x += (Math.random() - 0.5) * 100;
|
||||
enemy.circleCenter.y += (Math.random() - 0.5) * 100;
|
||||
|
||||
// Keep circle center within bounds
|
||||
enemy.circleCenter.x = Math.max(enemy.circleRadius + 20,
|
||||
Math.min(mapWidth - enemy.circleRadius - 20, enemy.circleCenter.x));
|
||||
enemy.circleCenter.y = Math.max(enemy.circleRadius + 20,
|
||||
Math.min(mapHeight - enemy.circleRadius - 20, enemy.circleCenter.y));
|
||||
|
||||
enemy.changeDirectionTimer = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
checkPlayerEnemyCollision(enemy) {
|
||||
if (this.isPlayerInvulnerable || enemy.defeated) return;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(this.player.x - enemy.x, 2) + Math.pow(this.player.y - enemy.y, 2)
|
||||
);
|
||||
|
||||
// Collision detected
|
||||
if (distance < 35) {
|
||||
this.takeDamage();
|
||||
}
|
||||
}
|
||||
|
||||
takeDamage() {
|
||||
if (this.isPlayerInvulnerable) return;
|
||||
|
||||
// Apply damage
|
||||
this.score = Math.max(0, this.score - 20);
|
||||
this.updateHUD();
|
||||
|
||||
// Clear any existing invulnerability timeout
|
||||
if (this.invulnerabilityTimeout) {
|
||||
clearTimeout(this.invulnerabilityTimeout);
|
||||
}
|
||||
|
||||
// Start damage invulnerability
|
||||
this.isPlayerInvulnerable = true;
|
||||
const playerElement = document.getElementById('player');
|
||||
|
||||
// Visual feedback - blinking effect
|
||||
let blinkCount = 0;
|
||||
const blinkInterval = setInterval(() => {
|
||||
playerElement.style.opacity = playerElement.style.opacity === '0.3' ? '1' : '0.3';
|
||||
blinkCount++;
|
||||
|
||||
if (blinkCount >= 8) { // 4 blinks in 2 seconds
|
||||
clearInterval(blinkInterval);
|
||||
playerElement.style.opacity = '1';
|
||||
playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))';
|
||||
playerElement.style.transform = 'scale(1)';
|
||||
this.isPlayerInvulnerable = false;
|
||||
}
|
||||
}, 250);
|
||||
|
||||
// Show damage feedback
|
||||
this.showDamagePopup();
|
||||
}
|
||||
|
||||
grantAttackInvulnerability() {
|
||||
// Always grant invulnerability after attack, even if already invulnerable
|
||||
this.isPlayerInvulnerable = true;
|
||||
const playerElement = document.getElementById('player');
|
||||
|
||||
// Clear any existing timeout
|
||||
if (this.invulnerabilityTimeout) {
|
||||
clearTimeout(this.invulnerabilityTimeout);
|
||||
}
|
||||
|
||||
// Different visual effect for attack invulnerability (golden glow)
|
||||
playerElement.style.filter = 'drop-shadow(0 0 15px gold) brightness(1.4)';
|
||||
|
||||
this.invulnerabilityTimeout = setTimeout(() => {
|
||||
playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))';
|
||||
this.isPlayerInvulnerable = false;
|
||||
}, 2000);
|
||||
|
||||
// Show invulnerability feedback
|
||||
this.showInvulnerabilityPopup();
|
||||
}
|
||||
|
||||
refreshAttackInvulnerability() {
|
||||
// Refresh the invulnerability timer without changing visual state
|
||||
if (this.invulnerabilityTimeout) {
|
||||
clearTimeout(this.invulnerabilityTimeout);
|
||||
}
|
||||
|
||||
const playerElement = document.getElementById('player');
|
||||
this.isPlayerInvulnerable = true;
|
||||
|
||||
this.invulnerabilityTimeout = setTimeout(() => {
|
||||
playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))';
|
||||
this.isPlayerInvulnerable = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
showInvulnerabilityPopup() {
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'invulnerability-popup';
|
||||
popup.innerHTML = 'Protected!';
|
||||
popup.style.position = 'fixed';
|
||||
popup.style.left = '50%';
|
||||
popup.style.top = '25%';
|
||||
popup.style.transform = 'translate(-50%, -50%)';
|
||||
popup.style.color = '#FFD700';
|
||||
popup.style.fontSize = '1.5rem';
|
||||
popup.style.fontWeight = 'bold';
|
||||
popup.style.zIndex = '999';
|
||||
popup.style.pointerEvents = 'none';
|
||||
popup.style.animation = 'protectionFloat 2s ease-out forwards';
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
showDamagePopup() {
|
||||
// Create damage popup
|
||||
const damagePopup = document.createElement('div');
|
||||
damagePopup.className = 'damage-popup';
|
||||
damagePopup.innerHTML = '-20';
|
||||
damagePopup.style.position = 'fixed';
|
||||
damagePopup.style.left = '50%';
|
||||
damagePopup.style.top = '30%';
|
||||
damagePopup.style.transform = 'translate(-50%, -50%)';
|
||||
damagePopup.style.color = '#EF4444';
|
||||
damagePopup.style.fontSize = '2rem';
|
||||
damagePopup.style.fontWeight = 'bold';
|
||||
damagePopup.style.zIndex = '999';
|
||||
damagePopup.style.pointerEvents = 'none';
|
||||
damagePopup.style.animation = 'damageFloat 1.5s ease-out forwards';
|
||||
|
||||
document.body.appendChild(damagePopup);
|
||||
|
||||
setTimeout(() => {
|
||||
damagePopup.remove();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log('⚔️ Adventure Reader: Starting');
|
||||
document.getElementById('progress-text').textContent = 'Click objects to begin your adventure!';
|
||||
}
|
||||
|
||||
restart() {
|
||||
console.log('🔄 Adventure Reader: Restarting');
|
||||
this.reset();
|
||||
this.start();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.score = 0;
|
||||
this.currentSentenceIndex = 0;
|
||||
this.currentVocabIndex = 0;
|
||||
this.potsDestroyed = 0;
|
||||
this.enemiesDefeated = 0;
|
||||
this.isGamePaused = false;
|
||||
this.isPlayerMoving = false;
|
||||
this.isPlayerInvulnerable = false;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (this.invulnerabilityTimeout) {
|
||||
clearTimeout(this.invulnerabilityTimeout);
|
||||
this.invulnerabilityTimeout = null;
|
||||
}
|
||||
|
||||
this.generateGameObjects();
|
||||
this.initializePlayer();
|
||||
this.generateDecorations();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Module registration
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.AdventureReader = AdventureReaderGame;
|
||||
419
js/games/fill-the-blank.js
Normal file
419
js/games/fill-the-blank.js
Normal file
@ -0,0 +1,419 @@
|
||||
// === MODULE FILL THE BLANK ===
|
||||
|
||||
class FillTheBlankGame {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.content = options.content;
|
||||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||||
this.onGameEnd = options.onGameEnd || (() => {});
|
||||
|
||||
// État du jeu
|
||||
this.score = 0;
|
||||
this.errors = 0;
|
||||
this.currentSentenceIndex = 0;
|
||||
this.isRunning = false;
|
||||
|
||||
// Données de jeu
|
||||
this.sentences = this.extractSentences(this.content);
|
||||
this.currentSentence = null;
|
||||
this.blanks = [];
|
||||
this.userAnswers = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Vérifier que nous avons des phrases
|
||||
if (!this.sentences || this.sentences.length === 0) {
|
||||
console.error('Aucune phrase disponible pour Fill the Blank');
|
||||
this.showInitError();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createGameBoard();
|
||||
this.setupEventListeners();
|
||||
// Le jeu démarrera quand start() sera appelé
|
||||
}
|
||||
|
||||
showInitError() {
|
||||
this.container.innerHTML = `
|
||||
<div class="game-error">
|
||||
<h3>❌ Erreur de chargement</h3>
|
||||
<p>Ce contenu ne contient pas de phrases compatibles avec Fill the Blank.</p>
|
||||
<p>Le jeu nécessite des phrases avec leurs traductions.</p>
|
||||
<button onclick="AppNavigation.goBack()" class="back-btn">← Retour</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
extractSentences(content) {
|
||||
let sentences = [];
|
||||
|
||||
console.log('🔍 Extraction phrases depuis:', content?.name || 'contenu');
|
||||
|
||||
// Utiliser le contenu brut du module si disponible
|
||||
if (content.rawContent) {
|
||||
console.log('📦 Utilisation du contenu brut du module');
|
||||
return this.extractSentencesFromRaw(content.rawContent);
|
||||
}
|
||||
|
||||
// Format avec sentences array
|
||||
if (content.sentences && Array.isArray(content.sentences)) {
|
||||
console.log('📝 Format sentences détecté');
|
||||
sentences = content.sentences.filter(sentence =>
|
||||
sentence.english && sentence.english.trim() !== ''
|
||||
);
|
||||
}
|
||||
// Format moderne avec contentItems
|
||||
else if (content.contentItems && Array.isArray(content.contentItems)) {
|
||||
console.log('🆕 Format contentItems détecté');
|
||||
sentences = content.contentItems
|
||||
.filter(item => item.type === 'sentence' && item.english)
|
||||
.map(item => ({
|
||||
english: item.english,
|
||||
french: item.french || item.translation,
|
||||
chinese: item.chinese
|
||||
}));
|
||||
}
|
||||
|
||||
return this.finalizeSentences(sentences);
|
||||
}
|
||||
|
||||
extractSentencesFromRaw(rawContent) {
|
||||
console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module');
|
||||
let sentences = [];
|
||||
|
||||
// Format simple (sentences array)
|
||||
if (rawContent.sentences && Array.isArray(rawContent.sentences)) {
|
||||
sentences = rawContent.sentences.filter(sentence =>
|
||||
sentence.english && sentence.english.trim() !== ''
|
||||
);
|
||||
console.log(`📝 ${sentences.length} phrases extraites depuis sentences array`);
|
||||
}
|
||||
// Format contentItems
|
||||
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
|
||||
sentences = rawContent.contentItems
|
||||
.filter(item => item.type === 'sentence' && item.english)
|
||||
.map(item => ({
|
||||
english: item.english,
|
||||
french: item.french || item.translation,
|
||||
chinese: item.chinese
|
||||
}));
|
||||
console.log(`🆕 ${sentences.length} phrases extraites depuis contentItems`);
|
||||
}
|
||||
|
||||
return this.finalizeSentences(sentences);
|
||||
}
|
||||
|
||||
finalizeSentences(sentences) {
|
||||
// Validation et nettoyage
|
||||
sentences = sentences.filter(sentence =>
|
||||
sentence &&
|
||||
typeof sentence.english === 'string' &&
|
||||
sentence.english.trim() !== '' &&
|
||||
sentence.english.split(' ').length >= 3 // Au moins 3 mots pour créer des blanks
|
||||
);
|
||||
|
||||
if (sentences.length === 0) {
|
||||
console.error('❌ Aucune phrase valide trouvée');
|
||||
// Phrases de démonstration en dernier recours
|
||||
sentences = [
|
||||
{ english: "I am learning English.", chinese: "我正在学英语。" },
|
||||
{ english: "She goes to school every day.", chinese: "她每天都去学校。" },
|
||||
{ english: "We like to play games together.", chinese: "我们喜欢一起玩游戏。" }
|
||||
];
|
||||
console.warn('🚨 Utilisation de phrases de démonstration');
|
||||
}
|
||||
|
||||
// Mélanger les phrases
|
||||
sentences = this.shuffleArray(sentences);
|
||||
|
||||
console.log(`✅ Fill the Blank: ${sentences.length} phrases finalisées`);
|
||||
return sentences;
|
||||
}
|
||||
|
||||
createGameBoard() {
|
||||
this.container.innerHTML = `
|
||||
<div class="fill-blank-wrapper">
|
||||
<!-- Game Info -->
|
||||
<div class="game-info">
|
||||
<div class="game-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="current-question">${this.currentSentenceIndex + 1}</span>
|
||||
<span class="stat-label">/ ${this.sentences.length}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="errors-count">${this.errors}</span>
|
||||
<span class="stat-label">Erreurs</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="score-display">${this.score}</span>
|
||||
<span class="stat-label">Score</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Translation hint -->
|
||||
<div class="translation-hint" id="translation-hint">
|
||||
<!-- La traduction apparaîtra ici -->
|
||||
</div>
|
||||
|
||||
<!-- Sentence with blanks -->
|
||||
<div class="sentence-container" id="sentence-container">
|
||||
<!-- La phrase avec les blanks apparaîtra ici -->
|
||||
</div>
|
||||
|
||||
<!-- Input area -->
|
||||
<div class="input-area" id="input-area">
|
||||
<!-- Les inputs apparaîtront ici -->
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="game-controls">
|
||||
<button class="control-btn secondary" id="hint-btn">💡 Indice</button>
|
||||
<button class="control-btn primary" id="check-btn">✓ Vérifier</button>
|
||||
<button class="control-btn secondary" id="skip-btn">→ Suivant</button>
|
||||
</div>
|
||||
|
||||
<!-- Feedback Area -->
|
||||
<div class="feedback-area" id="feedback-area">
|
||||
<div class="instruction">
|
||||
Complète la phrase en remplissant les blancs !
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('check-btn').addEventListener('click', () => this.checkAnswer());
|
||||
document.getElementById('hint-btn').addEventListener('click', () => this.showHint());
|
||||
document.getElementById('skip-btn').addEventListener('click', () => this.skipSentence());
|
||||
|
||||
// Enter key to check answer
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && this.isRunning) {
|
||||
this.checkAnswer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log('🎮 Fill the Blank: Démarrage du jeu');
|
||||
this.loadNextSentence();
|
||||
}
|
||||
|
||||
restart() {
|
||||
console.log('🔄 Fill the Blank: Redémarrage du jeu');
|
||||
this.reset();
|
||||
this.start();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.score = 0;
|
||||
this.errors = 0;
|
||||
this.currentSentenceIndex = 0;
|
||||
this.isRunning = false;
|
||||
this.currentSentence = null;
|
||||
this.blanks = [];
|
||||
this.userAnswers = [];
|
||||
this.onScoreUpdate(0);
|
||||
}
|
||||
|
||||
loadNextSentence() {
|
||||
// Si on a fini toutes les phrases, recommencer depuis le début
|
||||
if (this.currentSentenceIndex >= this.sentences.length) {
|
||||
this.currentSentenceIndex = 0;
|
||||
this.sentences = this.shuffleArray(this.sentences); // Mélanger à nouveau
|
||||
this.showFeedback(`🎉 Toutes les phrases terminées ! On recommence avec un nouvel ordre.`, 'success');
|
||||
setTimeout(() => {
|
||||
this.loadNextSentence();
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
this.currentSentence = this.sentences[this.currentSentenceIndex];
|
||||
this.createBlanks();
|
||||
this.displaySentence();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
createBlanks() {
|
||||
const words = this.currentSentence.english.split(' ');
|
||||
this.blanks = [];
|
||||
|
||||
// Créer 1-3 blanks selon la longueur de la phrase
|
||||
const numBlanks = Math.min(Math.max(1, Math.floor(words.length / 4)), 3);
|
||||
const blankIndices = new Set();
|
||||
|
||||
// Sélectionner des mots aléatoires (pas les articles/prépositions courtes)
|
||||
const candidateWords = words.map((word, index) => ({ word, index }))
|
||||
.filter(item => item.word.length > 2 && !['the', 'and', 'but', 'for', 'nor', 'or', 'so', 'yet'].includes(item.word.toLowerCase()));
|
||||
|
||||
// Si pas assez de candidats, prendre n'importe quels mots
|
||||
if (candidateWords.length < numBlanks) {
|
||||
candidateWords = words.map((word, index) => ({ word, index }));
|
||||
}
|
||||
|
||||
// Sélectionner aléatoirement les indices des blanks
|
||||
const shuffledCandidates = this.shuffleArray(candidateWords);
|
||||
for (let i = 0; i < Math.min(numBlanks, shuffledCandidates.length); i++) {
|
||||
blankIndices.add(shuffledCandidates[i].index);
|
||||
}
|
||||
|
||||
// Créer la structure des blanks
|
||||
words.forEach((word, index) => {
|
||||
if (blankIndices.has(index)) {
|
||||
this.blanks.push({
|
||||
index: index,
|
||||
word: word.replace(/[.,!?;:]$/, ''), // Retirer la ponctuation
|
||||
punctuation: word.match(/[.,!?;:]$/) ? word.match(/[.,!?;:]$/)[0] : '',
|
||||
userAnswer: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displaySentence() {
|
||||
const words = this.currentSentence.english.split(' ');
|
||||
let sentenceHTML = '';
|
||||
let blankCounter = 0;
|
||||
|
||||
words.forEach((word, index) => {
|
||||
const blank = this.blanks.find(b => b.index === index);
|
||||
if (blank) {
|
||||
sentenceHTML += `<span class="blank-wrapper">
|
||||
<input type="text" class="blank-input"
|
||||
id="blank-${blankCounter}"
|
||||
placeholder="___"
|
||||
maxlength="${blank.word.length + 2}">
|
||||
${blank.punctuation}
|
||||
</span> `;
|
||||
blankCounter++;
|
||||
} else {
|
||||
sentenceHTML += `<span class="word">${word}</span> `;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('sentence-container').innerHTML = sentenceHTML;
|
||||
|
||||
// Afficher la traduction si disponible
|
||||
const translation = this.currentSentence.chinese || this.currentSentence.french || '';
|
||||
document.getElementById('translation-hint').innerHTML = translation ?
|
||||
`<em>💭 ${translation}</em>` : '';
|
||||
|
||||
// Focus sur le premier input
|
||||
const firstInput = document.getElementById('blank-0');
|
||||
if (firstInput) {
|
||||
setTimeout(() => firstInput.focus(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
checkAnswer() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
let allCorrect = true;
|
||||
let correctCount = 0;
|
||||
|
||||
// Vérifier chaque blank
|
||||
this.blanks.forEach((blank, index) => {
|
||||
const input = document.getElementById(`blank-${index}`);
|
||||
const userAnswer = input.value.trim().toLowerCase();
|
||||
const correctAnswer = blank.word.toLowerCase();
|
||||
|
||||
blank.userAnswer = input.value.trim();
|
||||
|
||||
if (userAnswer === correctAnswer) {
|
||||
input.classList.remove('incorrect');
|
||||
input.classList.add('correct');
|
||||
correctCount++;
|
||||
} else {
|
||||
input.classList.remove('correct');
|
||||
input.classList.add('incorrect');
|
||||
allCorrect = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (allCorrect) {
|
||||
// Toutes les réponses sont correctes
|
||||
this.score += 10 * this.blanks.length;
|
||||
this.showFeedback(`🎉 Parfait ! +${10 * this.blanks.length} points`, 'success');
|
||||
setTimeout(() => {
|
||||
this.currentSentenceIndex++;
|
||||
this.loadNextSentence();
|
||||
}, 1500);
|
||||
} else {
|
||||
// Quelques erreurs
|
||||
this.errors++;
|
||||
if (correctCount > 0) {
|
||||
this.score += 5 * correctCount;
|
||||
this.showFeedback(`✨ ${correctCount}/${this.blanks.length} correct ! +${5 * correctCount} points. Essaye encore.`, 'partial');
|
||||
} else {
|
||||
this.showFeedback(`❌ Essaye encore ! (${this.errors} erreurs)`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
this.onScoreUpdate(this.score);
|
||||
}
|
||||
|
||||
showHint() {
|
||||
// Afficher la première lettre de chaque blank vide
|
||||
this.blanks.forEach((blank, index) => {
|
||||
const input = document.getElementById(`blank-${index}`);
|
||||
if (!input.value.trim()) {
|
||||
input.value = blank.word[0];
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.showFeedback('💡 Première lettre ajoutée !', 'info');
|
||||
}
|
||||
|
||||
skipSentence() {
|
||||
// Révéler les bonnes réponses
|
||||
this.blanks.forEach((blank, index) => {
|
||||
const input = document.getElementById(`blank-${index}`);
|
||||
input.value = blank.word;
|
||||
input.classList.add('revealed');
|
||||
});
|
||||
|
||||
this.showFeedback('📖 Réponses révélées ! Phrase suivante...', 'info');
|
||||
setTimeout(() => {
|
||||
this.currentSentenceIndex++;
|
||||
this.loadNextSentence();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Méthode endGame supprimée - le jeu continue indéfiniment
|
||||
|
||||
showFeedback(message, type = 'info') {
|
||||
const feedbackArea = document.getElementById('feedback-area');
|
||||
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
document.getElementById('current-question').textContent = this.currentSentenceIndex + 1;
|
||||
document.getElementById('errors-count').textContent = this.errors;
|
||||
document.getElementById('score-display').textContent = this.score;
|
||||
}
|
||||
|
||||
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.isRunning = false;
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Enregistrement du module
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.FillTheBlank = FillTheBlankGame;
|
||||
404
js/games/memory-match.js
Normal file
404
js/games/memory-match.js
Normal file
@ -0,0 +1,404 @@
|
||||
// === MODULE MEMORY MATCH ===
|
||||
|
||||
class MemoryMatchGame {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.content = options.content;
|
||||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||||
this.onGameEnd = options.onGameEnd || (() => {});
|
||||
|
||||
// Game state
|
||||
this.cards = [];
|
||||
this.flippedCards = [];
|
||||
this.matchedPairs = 0;
|
||||
this.totalPairs = 8; // 4x4 grid = 16 cards = 8 pairs
|
||||
this.moves = 0;
|
||||
this.score = 0;
|
||||
this.isFlipping = false;
|
||||
|
||||
// Extract vocabulary
|
||||
this.vocabulary = this.extractVocabulary(this.content);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Check if we have enough vocabulary
|
||||
if (!this.vocabulary || this.vocabulary.length < this.totalPairs) {
|
||||
console.error('Not enough vocabulary for Memory Match');
|
||||
this.showInitError();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createGameInterface();
|
||||
this.generateCards();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
showInitError() {
|
||||
this.container.innerHTML = `
|
||||
<div class="game-error">
|
||||
<h3>❌ Error loading</h3>
|
||||
<p>This content doesn't have enough vocabulary for Memory Match.</p>
|
||||
<p>The game needs at least ${this.totalPairs} vocabulary pairs.</p>
|
||||
<button onclick="AppNavigation.goBack()" class="back-btn">← Back</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
extractVocabulary(content) {
|
||||
let vocabulary = [];
|
||||
|
||||
console.log('📝 Extracting vocabulary from:', content?.name || 'content');
|
||||
|
||||
// Use raw module content if available
|
||||
if (content.rawContent) {
|
||||
console.log('📦 Using raw module content');
|
||||
return this.extractVocabularyFromRaw(content.rawContent);
|
||||
}
|
||||
|
||||
// Modern format with contentItems
|
||||
if (content.contentItems && Array.isArray(content.contentItems)) {
|
||||
console.log('🆕 ContentItems format detected');
|
||||
const vocabItems = content.contentItems.filter(item => item.type === 'vocabulary');
|
||||
if (vocabItems.length > 0) {
|
||||
vocabulary = vocabItems[0].items || [];
|
||||
}
|
||||
}
|
||||
// Legacy format with vocabulary array
|
||||
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
|
||||
console.log('📚 Vocabulary array format detected');
|
||||
vocabulary = content.vocabulary;
|
||||
}
|
||||
|
||||
return this.finalizeVocabulary(vocabulary);
|
||||
}
|
||||
|
||||
extractVocabularyFromRaw(rawContent) {
|
||||
console.log('🔧 Extracting from raw content:', rawContent.name || 'Module');
|
||||
let vocabulary = [];
|
||||
|
||||
// Check vocabulary object format (key-value pairs)
|
||||
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
|
||||
vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({
|
||||
english: english,
|
||||
french: translation
|
||||
}));
|
||||
console.log(`📝 ${vocabulary.length} vocabulary pairs extracted from object`);
|
||||
}
|
||||
// Check vocabulary array format
|
||||
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
|
||||
vocabulary = rawContent.vocabulary;
|
||||
console.log(`📚 ${vocabulary.length} vocabulary items extracted from array`);
|
||||
}
|
||||
|
||||
return this.finalizeVocabulary(vocabulary);
|
||||
}
|
||||
|
||||
finalizeVocabulary(vocabulary) {
|
||||
// Filter and validate vocabulary
|
||||
vocabulary = vocabulary.filter(item =>
|
||||
item &&
|
||||
item.english &&
|
||||
(item.french || item.translation || item.chinese)
|
||||
).map(item => ({
|
||||
english: item.english,
|
||||
french: item.french || item.translation || item.chinese
|
||||
}));
|
||||
|
||||
if (vocabulary.length === 0) {
|
||||
console.error('❌ No valid vocabulary found');
|
||||
// Demo vocabulary as fallback
|
||||
vocabulary = [
|
||||
{ english: "cat", french: "chat" },
|
||||
{ english: "dog", french: "chien" },
|
||||
{ english: "house", french: "maison" },
|
||||
{ english: "car", french: "voiture" },
|
||||
{ english: "book", french: "livre" },
|
||||
{ english: "water", french: "eau" },
|
||||
{ english: "food", french: "nourriture" },
|
||||
{ english: "friend", french: "ami" }
|
||||
];
|
||||
console.warn('🚨 Using demo vocabulary');
|
||||
}
|
||||
|
||||
console.log(`✅ Memory Match: ${vocabulary.length} vocabulary items finalized`);
|
||||
return vocabulary;
|
||||
}
|
||||
|
||||
createGameInterface() {
|
||||
this.container.innerHTML = `
|
||||
<div class="memory-match-wrapper">
|
||||
<!-- Game Stats -->
|
||||
<div class="game-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Moves:</span>
|
||||
<span id="moves-counter">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Pairs:</span>
|
||||
<span id="pairs-counter">0 / ${this.totalPairs}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Score:</span>
|
||||
<span id="score-counter">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Grid -->
|
||||
<div class="memory-grid" id="memory-grid">
|
||||
<!-- Cards will be generated here -->
|
||||
</div>
|
||||
|
||||
<!-- Game Controls -->
|
||||
<div class="game-controls">
|
||||
<button class="control-btn secondary" id="restart-btn">🔄 Restart</button>
|
||||
<button class="control-btn secondary" id="hint-btn">💡 Hint</button>
|
||||
</div>
|
||||
|
||||
<!-- Feedback Area -->
|
||||
<div class="feedback-area" id="feedback-area">
|
||||
<div class="instruction">
|
||||
Click cards to flip them and find matching pairs!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
generateCards() {
|
||||
// Select random vocabulary pairs
|
||||
const selectedVocab = this.vocabulary
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, this.totalPairs);
|
||||
|
||||
// Create card pairs
|
||||
this.cards = [];
|
||||
selectedVocab.forEach((item, index) => {
|
||||
// English card
|
||||
this.cards.push({
|
||||
id: `en_${index}`,
|
||||
content: item.english,
|
||||
type: 'english',
|
||||
pairId: index,
|
||||
isFlipped: false,
|
||||
isMatched: false
|
||||
});
|
||||
|
||||
// French card
|
||||
this.cards.push({
|
||||
id: `fr_${index}`,
|
||||
content: item.french,
|
||||
type: 'french',
|
||||
pairId: index,
|
||||
isFlipped: false,
|
||||
isMatched: false
|
||||
});
|
||||
});
|
||||
|
||||
// Shuffle cards
|
||||
this.cards.sort(() => Math.random() - 0.5);
|
||||
|
||||
// Render cards
|
||||
this.renderCards();
|
||||
}
|
||||
|
||||
renderCards() {
|
||||
const grid = document.getElementById('memory-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
this.cards.forEach((card, index) => {
|
||||
const cardElement = document.createElement('div');
|
||||
cardElement.className = 'memory-card';
|
||||
cardElement.dataset.cardIndex = index;
|
||||
|
||||
cardElement.innerHTML = `
|
||||
<div class="card-inner">
|
||||
<div class="card-front">
|
||||
<span class="card-icon">🎯</span>
|
||||
</div>
|
||||
<div class="card-back">
|
||||
<span class="card-content">${card.content}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
cardElement.addEventListener('click', () => this.flipCard(index));
|
||||
grid.appendChild(cardElement);
|
||||
});
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
|
||||
document.getElementById('hint-btn').addEventListener('click', () => this.showHint());
|
||||
}
|
||||
|
||||
flipCard(cardIndex) {
|
||||
if (this.isFlipping) return;
|
||||
|
||||
const card = this.cards[cardIndex];
|
||||
if (card.isFlipped || card.isMatched) return;
|
||||
|
||||
// Flip the card
|
||||
card.isFlipped = true;
|
||||
this.updateCardDisplay(cardIndex);
|
||||
this.flippedCards.push(cardIndex);
|
||||
|
||||
if (this.flippedCards.length === 2) {
|
||||
this.moves++;
|
||||
this.updateStats();
|
||||
this.checkMatch();
|
||||
}
|
||||
}
|
||||
|
||||
updateCardDisplay(cardIndex) {
|
||||
const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`);
|
||||
const card = this.cards[cardIndex];
|
||||
|
||||
if (card.isFlipped || card.isMatched) {
|
||||
cardElement.classList.add('flipped');
|
||||
} else {
|
||||
cardElement.classList.remove('flipped');
|
||||
}
|
||||
|
||||
if (card.isMatched) {
|
||||
cardElement.classList.add('matched');
|
||||
}
|
||||
}
|
||||
|
||||
checkMatch() {
|
||||
this.isFlipping = true;
|
||||
|
||||
setTimeout(() => {
|
||||
const [firstIndex, secondIndex] = this.flippedCards;
|
||||
const firstCard = this.cards[firstIndex];
|
||||
const secondCard = this.cards[secondIndex];
|
||||
|
||||
if (firstCard.pairId === secondCard.pairId) {
|
||||
// Match found!
|
||||
firstCard.isMatched = true;
|
||||
secondCard.isMatched = true;
|
||||
this.updateCardDisplay(firstIndex);
|
||||
this.updateCardDisplay(secondIndex);
|
||||
|
||||
this.matchedPairs++;
|
||||
this.score += 100;
|
||||
this.showFeedback('Great match! 🎉', 'success');
|
||||
|
||||
if (this.matchedPairs === this.totalPairs) {
|
||||
this.gameComplete();
|
||||
}
|
||||
} else {
|
||||
// No match, flip back and apply penalty
|
||||
firstCard.isFlipped = false;
|
||||
secondCard.isFlipped = false;
|
||||
this.updateCardDisplay(firstIndex);
|
||||
this.updateCardDisplay(secondIndex);
|
||||
|
||||
// Apply penalty but don't go below 0
|
||||
this.score = Math.max(0, this.score - 10);
|
||||
this.showFeedback('Try again! (-10 points)', 'warning');
|
||||
}
|
||||
|
||||
this.flippedCards = [];
|
||||
this.isFlipping = false;
|
||||
this.updateStats();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
showHint() {
|
||||
if (this.flippedCards.length > 0) {
|
||||
this.showFeedback('Finish your current move first!', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find first unmatched pair
|
||||
const unmatchedCards = this.cards.filter(card => !card.isMatched);
|
||||
if (unmatchedCards.length === 0) return;
|
||||
|
||||
// Group by pairId
|
||||
const pairs = {};
|
||||
unmatchedCards.forEach((card, index) => {
|
||||
const actualIndex = this.cards.indexOf(card);
|
||||
if (!pairs[card.pairId]) {
|
||||
pairs[card.pairId] = [];
|
||||
}
|
||||
pairs[card.pairId].push(actualIndex);
|
||||
});
|
||||
|
||||
// Find first complete pair
|
||||
const completePair = Object.values(pairs).find(pair => pair.length === 2);
|
||||
if (completePair) {
|
||||
// Briefly show the pair
|
||||
completePair.forEach(cardIndex => {
|
||||
const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`);
|
||||
cardElement.classList.add('hint');
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
completePair.forEach(cardIndex => {
|
||||
const cardElement = document.querySelector(`[data-card-index="${cardIndex}"]`);
|
||||
cardElement.classList.remove('hint');
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
this.showFeedback('Hint shown for 2 seconds!', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
document.getElementById('moves-counter').textContent = this.moves;
|
||||
document.getElementById('pairs-counter').textContent = `${this.matchedPairs} / ${this.totalPairs}`;
|
||||
document.getElementById('score-counter').textContent = this.score;
|
||||
this.onScoreUpdate(this.score);
|
||||
}
|
||||
|
||||
gameComplete() {
|
||||
// Calculate bonus based on moves
|
||||
const perfectMoves = this.totalPairs;
|
||||
if (this.moves <= perfectMoves + 5) {
|
||||
this.score += 200; // Efficiency bonus
|
||||
}
|
||||
|
||||
this.updateStats();
|
||||
this.showFeedback('🎉 Congratulations! All pairs found!', 'success');
|
||||
|
||||
setTimeout(() => {
|
||||
this.onGameEnd(this.score);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
showFeedback(message, type = 'info') {
|
||||
const feedbackArea = document.getElementById('feedback-area');
|
||||
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log('🧠 Memory Match: Starting');
|
||||
this.showFeedback('Find matching English-French pairs!', 'info');
|
||||
}
|
||||
|
||||
restart() {
|
||||
console.log('🔄 Memory Match: Restarting');
|
||||
this.reset();
|
||||
this.start();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.flippedCards = [];
|
||||
this.matchedPairs = 0;
|
||||
this.moves = 0;
|
||||
this.score = 0;
|
||||
this.isFlipping = false;
|
||||
this.generateCards();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Module registration
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.MemoryMatch = MemoryMatchGame;
|
||||
355
js/games/quiz-game.js
Normal file
355
js/games/quiz-game.js
Normal file
@ -0,0 +1,355 @@
|
||||
// === MODULE QUIZ GAME ===
|
||||
|
||||
class QuizGame {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.content = options.content;
|
||||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||||
this.onGameEnd = options.onGameEnd || (() => {});
|
||||
|
||||
// Game state
|
||||
this.vocabulary = [];
|
||||
this.currentQuestion = 0;
|
||||
this.totalQuestions = 10;
|
||||
this.score = 0;
|
||||
this.correctAnswers = 0;
|
||||
this.currentQuestionData = null;
|
||||
this.hasAnswered = false;
|
||||
|
||||
// Extract vocabulary
|
||||
this.vocabulary = this.extractVocabulary(this.content);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Check if we have enough vocabulary
|
||||
if (!this.vocabulary || this.vocabulary.length < 4) {
|
||||
console.error('Not enough vocabulary for Quiz Game');
|
||||
this.showInitError();
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust total questions based on available vocabulary
|
||||
this.totalQuestions = Math.min(this.totalQuestions, this.vocabulary.length);
|
||||
|
||||
this.createGameInterface();
|
||||
this.generateQuestion();
|
||||
}
|
||||
|
||||
showInitError() {
|
||||
this.container.innerHTML = `
|
||||
<div class="game-error">
|
||||
<h3>❌ Error loading</h3>
|
||||
<p>This content doesn't have enough vocabulary for Quiz Game.</p>
|
||||
<p>The game needs at least 4 vocabulary items.</p>
|
||||
<button onclick="AppNavigation.goBack()" class="back-btn">← Back</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
extractVocabulary(content) {
|
||||
let vocabulary = [];
|
||||
|
||||
console.log('📝 Extracting vocabulary from:', content?.name || 'content');
|
||||
|
||||
// Use raw module content if available
|
||||
if (content.rawContent) {
|
||||
console.log('📦 Using raw module content');
|
||||
return this.extractVocabularyFromRaw(content.rawContent);
|
||||
}
|
||||
|
||||
// Modern format with contentItems
|
||||
if (content.contentItems && Array.isArray(content.contentItems)) {
|
||||
console.log('🆕 ContentItems format detected');
|
||||
const vocabItems = content.contentItems.filter(item => item.type === 'vocabulary');
|
||||
if (vocabItems.length > 0) {
|
||||
vocabulary = vocabItems[0].items || [];
|
||||
}
|
||||
}
|
||||
// Legacy format with vocabulary array
|
||||
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
|
||||
console.log('📚 Vocabulary array format detected');
|
||||
vocabulary = content.vocabulary;
|
||||
}
|
||||
|
||||
return this.finalizeVocabulary(vocabulary);
|
||||
}
|
||||
|
||||
extractVocabularyFromRaw(rawContent) {
|
||||
console.log('🔧 Extracting from raw content:', rawContent.name || 'Module');
|
||||
let vocabulary = [];
|
||||
|
||||
// Check vocabulary object format (key-value pairs)
|
||||
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
|
||||
vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({
|
||||
english: english,
|
||||
french: translation
|
||||
}));
|
||||
console.log(`📝 ${vocabulary.length} vocabulary pairs extracted from object`);
|
||||
}
|
||||
// Check vocabulary array format
|
||||
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
|
||||
vocabulary = rawContent.vocabulary;
|
||||
console.log(`📚 ${vocabulary.length} vocabulary items extracted from array`);
|
||||
}
|
||||
|
||||
return this.finalizeVocabulary(vocabulary);
|
||||
}
|
||||
|
||||
finalizeVocabulary(vocabulary) {
|
||||
// Filter and validate vocabulary
|
||||
vocabulary = vocabulary.filter(item =>
|
||||
item &&
|
||||
item.english &&
|
||||
(item.french || item.translation || item.chinese)
|
||||
).map(item => ({
|
||||
english: item.english,
|
||||
french: item.french || item.translation || item.chinese
|
||||
}));
|
||||
|
||||
if (vocabulary.length === 0) {
|
||||
console.error('❌ No valid vocabulary found');
|
||||
// Demo vocabulary as fallback
|
||||
vocabulary = [
|
||||
{ english: "cat", french: "chat" },
|
||||
{ english: "dog", french: "chien" },
|
||||
{ english: "house", french: "maison" },
|
||||
{ english: "car", french: "voiture" },
|
||||
{ english: "book", french: "livre" },
|
||||
{ english: "water", french: "eau" },
|
||||
{ english: "food", french: "nourriture" },
|
||||
{ english: "friend", french: "ami" }
|
||||
];
|
||||
console.warn('🚨 Using demo vocabulary');
|
||||
}
|
||||
|
||||
// Shuffle vocabulary for random questions
|
||||
vocabulary = vocabulary.sort(() => Math.random() - 0.5);
|
||||
|
||||
console.log(`✅ Quiz Game: ${vocabulary.length} vocabulary items finalized`);
|
||||
return vocabulary;
|
||||
}
|
||||
|
||||
createGameInterface() {
|
||||
this.container.innerHTML = `
|
||||
<div class="quiz-game-wrapper">
|
||||
<!-- Progress Bar -->
|
||||
<div class="quiz-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progress-fill"></div>
|
||||
</div>
|
||||
<div class="progress-text">
|
||||
<span id="question-counter">1 / ${this.totalQuestions}</span>
|
||||
<span id="score-display">Score: 0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Question Area -->
|
||||
<div class="question-area">
|
||||
<div class="question-text" id="question-text">
|
||||
Loading question...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options Area -->
|
||||
<div class="options-area" id="options-area">
|
||||
<!-- Options will be generated here -->
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="quiz-controls">
|
||||
<button class="control-btn primary" id="next-btn" style="display: none;">Next Question →</button>
|
||||
<button class="control-btn secondary" id="restart-btn">🔄 Restart</button>
|
||||
</div>
|
||||
|
||||
<!-- Feedback Area -->
|
||||
<div class="feedback-area" id="feedback-area">
|
||||
<div class="instruction">
|
||||
Choose the correct translation!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('next-btn').addEventListener('click', () => this.nextQuestion());
|
||||
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
|
||||
}
|
||||
|
||||
generateQuestion() {
|
||||
if (this.currentQuestion >= this.totalQuestions) {
|
||||
this.gameComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasAnswered = false;
|
||||
|
||||
// Get current vocabulary item
|
||||
const correctAnswer = this.vocabulary[this.currentQuestion];
|
||||
|
||||
// Generate 3 wrong answers from other vocabulary items
|
||||
const wrongAnswers = this.vocabulary
|
||||
.filter(item => item !== correctAnswer)
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 3)
|
||||
.map(item => item.french);
|
||||
|
||||
// Combine and shuffle all options
|
||||
const allOptions = [correctAnswer.french, ...wrongAnswers].sort(() => Math.random() - 0.5);
|
||||
|
||||
this.currentQuestionData = {
|
||||
question: correctAnswer.english,
|
||||
correctAnswer: correctAnswer.french,
|
||||
options: allOptions
|
||||
};
|
||||
|
||||
this.renderQuestion();
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
renderQuestion() {
|
||||
const { question, options } = this.currentQuestionData;
|
||||
|
||||
// Update question text
|
||||
document.getElementById('question-text').innerHTML = `
|
||||
What is the translation of "<strong>${question}</strong>"?
|
||||
`;
|
||||
|
||||
// Clear and generate options
|
||||
const optionsArea = document.getElementById('options-area');
|
||||
optionsArea.innerHTML = '';
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const optionButton = document.createElement('button');
|
||||
optionButton.className = 'quiz-option';
|
||||
optionButton.textContent = option;
|
||||
optionButton.addEventListener('click', () => this.selectAnswer(option, optionButton));
|
||||
optionsArea.appendChild(optionButton);
|
||||
});
|
||||
|
||||
// Hide next button
|
||||
document.getElementById('next-btn').style.display = 'none';
|
||||
}
|
||||
|
||||
selectAnswer(selectedAnswer, buttonElement) {
|
||||
if (this.hasAnswered) return;
|
||||
|
||||
this.hasAnswered = true;
|
||||
const isCorrect = selectedAnswer === this.currentQuestionData.correctAnswer;
|
||||
|
||||
// Disable all option buttons and show results
|
||||
const allOptions = document.querySelectorAll('.quiz-option');
|
||||
allOptions.forEach(btn => {
|
||||
btn.disabled = true;
|
||||
|
||||
if (btn.textContent === this.currentQuestionData.correctAnswer) {
|
||||
btn.classList.add('correct');
|
||||
} else if (btn === buttonElement && !isCorrect) {
|
||||
btn.classList.add('wrong');
|
||||
} else if (btn !== buttonElement && btn.textContent !== this.currentQuestionData.correctAnswer) {
|
||||
btn.classList.add('disabled');
|
||||
}
|
||||
});
|
||||
|
||||
// Update score and feedback
|
||||
if (isCorrect) {
|
||||
this.correctAnswers++;
|
||||
this.score += 10;
|
||||
this.showFeedback('✅ Correct! Well done!', 'success');
|
||||
} else {
|
||||
this.score = Math.max(0, this.score - 5);
|
||||
this.showFeedback(`❌ Wrong! Correct answer: "${this.currentQuestionData.correctAnswer}"`, 'error');
|
||||
}
|
||||
|
||||
this.updateScore();
|
||||
|
||||
// Show next button or finish
|
||||
if (this.currentQuestion < this.totalQuestions - 1) {
|
||||
document.getElementById('next-btn').style.display = 'block';
|
||||
} else {
|
||||
setTimeout(() => this.gameComplete(), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
nextQuestion() {
|
||||
this.currentQuestion++;
|
||||
this.generateQuestion();
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
const progressFill = document.getElementById('progress-fill');
|
||||
const progressPercent = ((this.currentQuestion + 1) / this.totalQuestions) * 100;
|
||||
progressFill.style.width = `${progressPercent}%`;
|
||||
|
||||
document.getElementById('question-counter').textContent =
|
||||
`${this.currentQuestion + 1} / ${this.totalQuestions}`;
|
||||
}
|
||||
|
||||
updateScore() {
|
||||
document.getElementById('score-display').textContent = `Score: ${this.score}`;
|
||||
this.onScoreUpdate(this.score);
|
||||
}
|
||||
|
||||
gameComplete() {
|
||||
const accuracy = Math.round((this.correctAnswers / this.totalQuestions) * 100);
|
||||
|
||||
// Bonus for high accuracy
|
||||
if (accuracy >= 90) {
|
||||
this.score += 50; // Excellence bonus
|
||||
} else if (accuracy >= 70) {
|
||||
this.score += 20; // Good performance bonus
|
||||
}
|
||||
|
||||
this.updateScore();
|
||||
this.showFeedback(
|
||||
`🎉 Quiz completed! ${this.correctAnswers}/${this.totalQuestions} correct (${accuracy}%)`,
|
||||
'success'
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
this.onGameEnd(this.score);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showFeedback(message, type = 'info') {
|
||||
const feedbackArea = document.getElementById('feedback-area');
|
||||
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log('❓ Quiz Game: Starting');
|
||||
this.showFeedback('Choose the correct translation for each word!', 'info');
|
||||
}
|
||||
|
||||
restart() {
|
||||
console.log('🔄 Quiz Game: Restarting');
|
||||
this.reset();
|
||||
this.start();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.currentQuestion = 0;
|
||||
this.score = 0;
|
||||
this.correctAnswers = 0;
|
||||
this.hasAnswered = false;
|
||||
this.currentQuestionData = null;
|
||||
|
||||
// Re-shuffle vocabulary
|
||||
this.vocabulary = this.vocabulary.sort(() => Math.random() - 0.5);
|
||||
|
||||
this.generateQuestion();
|
||||
this.updateScore();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Module registration
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.QuizGame = QuizGame;
|
||||
701
js/games/story-builder.js
Normal file
701
js/games/story-builder.js
Normal file
@ -0,0 +1,701 @@
|
||||
// === 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;
|
||||
736
js/games/temp-games.js
Normal file
736
js/games/temp-games.js
Normal file
@ -0,0 +1,736 @@
|
||||
// === MODULE JEUX TEMPORAIRES ===
|
||||
|
||||
class TempGamesModule {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.content = options.content;
|
||||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||||
this.onGameEnd = options.onGameEnd || (() => {});
|
||||
|
||||
this.currentGame = null;
|
||||
this.availableGames = [
|
||||
{
|
||||
id: 'word-match',
|
||||
name: 'Word Match',
|
||||
icon: '🎯',
|
||||
description: 'Associe les mots anglais avec leur traduction',
|
||||
difficulty: 'easy'
|
||||
},
|
||||
{
|
||||
id: 'quick-translation',
|
||||
name: 'Quick Translation',
|
||||
icon: '⚡',
|
||||
description: 'Traduis le mot le plus rapidement possible',
|
||||
difficulty: 'medium'
|
||||
},
|
||||
{
|
||||
id: 'word-builder',
|
||||
name: 'Word Builder',
|
||||
icon: '🔤',
|
||||
description: 'Reconstitue le mot lettre par lettre',
|
||||
difficulty: 'medium'
|
||||
}
|
||||
];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.showGameSelector();
|
||||
}
|
||||
|
||||
showGameSelector() {
|
||||
this.container.innerHTML = `
|
||||
<div class="temp-games-wrapper">
|
||||
<div class="game-selector-header">
|
||||
<h3>🎯 Mini-Jeux Temporaires</h3>
|
||||
<p>Sélectionne un mini-jeu pour t'amuser avec le vocabulaire !</p>
|
||||
</div>
|
||||
|
||||
<div class="mini-games-grid">
|
||||
${this.availableGames.map(game => this.createGameCard(game)).join('')}
|
||||
</div>
|
||||
|
||||
<div class="temp-games-info">
|
||||
<p><em>Ces jeux sont en développement et seront bientôt des modules complets !</em></p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.setupGameSelector();
|
||||
}
|
||||
|
||||
createGameCard(game) {
|
||||
const difficultyColor = {
|
||||
easy: '#10B981',
|
||||
medium: '#F59E0B',
|
||||
hard: '#EF4444'
|
||||
}[game.difficulty];
|
||||
|
||||
return `
|
||||
<div class="mini-game-card" data-game="${game.id}">
|
||||
<div class="mini-game-icon">${game.icon}</div>
|
||||
<h4 class="mini-game-title">${game.name}</h4>
|
||||
<p class="mini-game-description">${game.description}</p>
|
||||
<div class="mini-game-difficulty" style="color: ${difficultyColor}">
|
||||
${game.difficulty.toUpperCase()}
|
||||
</div>
|
||||
<button class="play-mini-game-btn">Jouer</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupGameSelector() {
|
||||
document.querySelectorAll('.mini-game-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const gameId = card.dataset.game;
|
||||
this.startMiniGame(gameId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
startMiniGame(gameId) {
|
||||
const game = this.availableGames.find(g => g.id === gameId);
|
||||
if (!game) return;
|
||||
|
||||
switch(gameId) {
|
||||
case 'word-match':
|
||||
this.startWordMatch();
|
||||
break;
|
||||
case 'quick-translation':
|
||||
this.startQuickTranslation();
|
||||
break;
|
||||
case 'word-builder':
|
||||
this.startWordBuilder();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// === WORD MATCH GAME ===
|
||||
startWordMatch() {
|
||||
this.container.innerHTML = `
|
||||
<div class="mini-game word-match-game">
|
||||
<div class="mini-game-header">
|
||||
<button class="back-to-selector">← Retour</button>
|
||||
<h3>🎯 Word Match</h3>
|
||||
<div class="mini-score">Score: <span id="match-score">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="word-match-board">
|
||||
<div class="english-words" id="english-words">
|
||||
<!-- Mots anglais -->
|
||||
</div>
|
||||
<div class="french-words" id="french-words">
|
||||
<!-- Mots français -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="match-feedback" id="match-feedback">
|
||||
Clique sur un mot anglais, puis sur sa traduction française !
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.setupWordMatch();
|
||||
}
|
||||
|
||||
setupWordMatch() {
|
||||
document.querySelector('.back-to-selector').addEventListener('click', () => {
|
||||
this.showGameSelector();
|
||||
});
|
||||
|
||||
const words = this.content.vocabulary.slice(0, 6).map(w => ({
|
||||
english: w.english,
|
||||
french: w.french
|
||||
}));
|
||||
|
||||
const shuffledFrench = [...words].sort(() => Math.random() - 0.5);
|
||||
|
||||
const englishContainer = document.getElementById('english-words');
|
||||
const frenchContainer = document.getElementById('french-words');
|
||||
|
||||
let selectedEnglish = null;
|
||||
let matchedPairs = 0;
|
||||
let score = 0;
|
||||
|
||||
words.forEach((word, index) => {
|
||||
const englishBtn = document.createElement('button');
|
||||
englishBtn.className = 'word-btn english-btn';
|
||||
englishBtn.textContent = word.english;
|
||||
englishBtn.dataset.word = word.english;
|
||||
englishContainer.appendChild(englishBtn);
|
||||
|
||||
englishBtn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.english-btn').forEach(btn =>
|
||||
btn.classList.remove('selected'));
|
||||
englishBtn.classList.add('selected');
|
||||
selectedEnglish = word.english;
|
||||
});
|
||||
});
|
||||
|
||||
shuffledFrench.forEach(word => {
|
||||
const frenchBtn = document.createElement('button');
|
||||
frenchBtn.className = 'word-btn french-btn';
|
||||
frenchBtn.textContent = word.french;
|
||||
frenchBtn.dataset.word = word.french;
|
||||
frenchContainer.appendChild(frenchBtn);
|
||||
|
||||
frenchBtn.addEventListener('click', () => {
|
||||
if (!selectedEnglish) {
|
||||
document.getElementById('match-feedback').textContent =
|
||||
'Sélectionne d\'abord un mot anglais !';
|
||||
return;
|
||||
}
|
||||
|
||||
const correctWord = words.find(w => w.english === selectedEnglish);
|
||||
if (correctWord && correctWord.french === word.french) {
|
||||
// Correct match
|
||||
score += 10;
|
||||
matchedPairs++;
|
||||
|
||||
document.querySelector(`[data-word="${selectedEnglish}"]`).classList.add('matched');
|
||||
frenchBtn.classList.add('matched');
|
||||
|
||||
document.getElementById('match-feedback').textContent = 'Parfait ! 🎉';
|
||||
|
||||
if (matchedPairs === words.length) {
|
||||
setTimeout(() => {
|
||||
alert(`Félicitations ! Score final: ${score}`);
|
||||
this.onGameEnd(score);
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
// Wrong match
|
||||
score = Math.max(0, score - 2);
|
||||
document.getElementById('match-feedback').textContent =
|
||||
`Non, "${selectedEnglish}" ne correspond pas à "${word.french}"`;
|
||||
}
|
||||
|
||||
document.getElementById('match-score').textContent = score;
|
||||
this.onScoreUpdate(score);
|
||||
selectedEnglish = null;
|
||||
document.querySelectorAll('.english-btn').forEach(btn =>
|
||||
btn.classList.remove('selected'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// === QUICK TRANSLATION GAME ===
|
||||
startQuickTranslation() {
|
||||
this.container.innerHTML = `
|
||||
<div class="mini-game quick-translation-game">
|
||||
<div class="mini-game-header">
|
||||
<button class="back-to-selector">← Retour</button>
|
||||
<h3>⚡ Quick Translation</h3>
|
||||
<div class="game-stats">
|
||||
<span>Score: <span id="quick-score">0</span></span>
|
||||
<span>Temps: <span id="quick-time">30</span>s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-translation-board">
|
||||
<div class="word-to-translate">
|
||||
<h2 id="current-word">---</h2>
|
||||
<p>Traduction en français :</p>
|
||||
</div>
|
||||
|
||||
<div class="translation-options" id="translation-options">
|
||||
<!-- Options de traduction -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="start-quick-game" class="start-btn">🎮 Commencer</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.setupQuickTranslation();
|
||||
}
|
||||
|
||||
setupQuickTranslation() {
|
||||
document.querySelector('.back-to-selector').addEventListener('click', () => {
|
||||
this.showGameSelector();
|
||||
});
|
||||
|
||||
let currentWordIndex = 0;
|
||||
let score = 0;
|
||||
let timeLeft = 30;
|
||||
let gameTimer = null;
|
||||
let isPlaying = false;
|
||||
|
||||
const words = this.shuffleArray([...this.content.vocabulary]).slice(0, 10);
|
||||
|
||||
document.getElementById('start-quick-game').addEventListener('click', () => {
|
||||
if (isPlaying) return;
|
||||
|
||||
isPlaying = true;
|
||||
currentWordIndex = 0;
|
||||
score = 0;
|
||||
timeLeft = 30;
|
||||
|
||||
document.getElementById('start-quick-game').style.display = 'none';
|
||||
|
||||
gameTimer = setInterval(() => {
|
||||
timeLeft--;
|
||||
document.getElementById('quick-time').textContent = timeLeft;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(gameTimer);
|
||||
alert(`Temps écoulé ! Score final: ${score}`);
|
||||
this.onGameEnd(score);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.showQuickWord();
|
||||
});
|
||||
|
||||
const showQuickWord = () => {
|
||||
if (currentWordIndex >= words.length) {
|
||||
clearInterval(gameTimer);
|
||||
alert(`Bravo ! Score final: ${score}`);
|
||||
this.onGameEnd(score);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWord = words[currentWordIndex];
|
||||
document.getElementById('current-word').textContent = currentWord.english;
|
||||
|
||||
// Créer 4 options (1 correcte + 3 fausses)
|
||||
const options = [currentWord.french];
|
||||
const otherWords = words.filter(w => w.french !== currentWord.french);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (otherWords[i]) {
|
||||
options.push(otherWords[i].french);
|
||||
}
|
||||
}
|
||||
|
||||
const shuffledOptions = this.shuffleArray(options);
|
||||
const optionsContainer = document.getElementById('translation-options');
|
||||
optionsContainer.innerHTML = '';
|
||||
|
||||
shuffledOptions.forEach(option => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'option-btn';
|
||||
btn.textContent = option;
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
if (option === currentWord.french) {
|
||||
score += 5;
|
||||
btn.classList.add('correct');
|
||||
setTimeout(() => {
|
||||
currentWordIndex++;
|
||||
this.showQuickWord();
|
||||
}, 500);
|
||||
} else {
|
||||
score = Math.max(0, score - 1);
|
||||
btn.classList.add('wrong');
|
||||
document.querySelector(`button:contains("${currentWord.french}")`);
|
||||
}
|
||||
|
||||
document.getElementById('quick-score').textContent = score;
|
||||
this.onScoreUpdate(score);
|
||||
|
||||
// Désactiver tous les boutons
|
||||
document.querySelectorAll('.option-btn').forEach(b => b.disabled = true);
|
||||
});
|
||||
|
||||
optionsContainer.appendChild(btn);
|
||||
});
|
||||
};
|
||||
|
||||
this.showQuickWord = showQuickWord;
|
||||
}
|
||||
|
||||
// === WORD BUILDER GAME ===
|
||||
startWordBuilder() {
|
||||
this.container.innerHTML = `
|
||||
<div class="mini-game word-builder-game">
|
||||
<div class="mini-game-header">
|
||||
<button class="back-to-selector">← Retour</button>
|
||||
<h3>🔤 Word Builder</h3>
|
||||
<div class="mini-score">Score: <span id="builder-score">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="word-builder-board">
|
||||
<div class="french-word">
|
||||
<p>Traduction : <strong id="french-hint">---</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="word-construction">
|
||||
<div class="word-spaces" id="word-spaces">
|
||||
<!-- Espaces pour les lettres -->
|
||||
</div>
|
||||
|
||||
<div class="available-letters" id="available-letters">
|
||||
<!-- Lettres disponibles -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="builder-controls">
|
||||
<button id="next-word-btn" style="display:none;">Mot suivant</button>
|
||||
<button id="give-up-btn">Passer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.setupWordBuilder();
|
||||
}
|
||||
|
||||
setupWordBuilder() {
|
||||
document.querySelector('.back-to-selector').addEventListener('click', () => {
|
||||
this.showGameSelector();
|
||||
});
|
||||
|
||||
let currentWordIndex = 0;
|
||||
let score = 0;
|
||||
const words = this.shuffleArray([...this.content.vocabulary]).slice(0, 8);
|
||||
|
||||
const showBuilderWord = () => {
|
||||
if (currentWordIndex >= words.length) {
|
||||
alert(`Félicitations ! Score final: ${score}`);
|
||||
this.onGameEnd(score);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWord = words[currentWordIndex];
|
||||
const wordSpaces = document.getElementById('word-spaces');
|
||||
const availableLetters = document.getElementById('available-letters');
|
||||
|
||||
document.getElementById('french-hint').textContent = currentWord.french;
|
||||
|
||||
// Créer les espaces
|
||||
wordSpaces.innerHTML = '';
|
||||
currentWord.english.split('').forEach((letter, index) => {
|
||||
const space = document.createElement('div');
|
||||
space.className = 'letter-space';
|
||||
space.dataset.index = index;
|
||||
space.dataset.letter = letter.toLowerCase();
|
||||
wordSpaces.appendChild(space);
|
||||
});
|
||||
|
||||
// Créer les lettres mélangées + quelques lettres supplémentaires
|
||||
const wordLetters = currentWord.english.toLowerCase().split('');
|
||||
const extraLetters = 'abcdefghijklmnopqrstuvwxyz'.split('')
|
||||
.filter(l => !wordLetters.includes(l))
|
||||
.slice(0, 3);
|
||||
|
||||
const allLetters = this.shuffleArray([...wordLetters, ...extraLetters]);
|
||||
|
||||
availableLetters.innerHTML = '';
|
||||
allLetters.forEach(letter => {
|
||||
const letterBtn = document.createElement('button');
|
||||
letterBtn.className = 'letter-btn';
|
||||
letterBtn.textContent = letter.toUpperCase();
|
||||
letterBtn.dataset.letter = letter;
|
||||
|
||||
letterBtn.addEventListener('click', () => {
|
||||
this.placeLetter(letter, letterBtn);
|
||||
});
|
||||
|
||||
availableLetters.appendChild(letterBtn);
|
||||
});
|
||||
|
||||
document.getElementById('next-word-btn').style.display = 'none';
|
||||
document.getElementById('give-up-btn').style.display = 'inline-block';
|
||||
};
|
||||
|
||||
const placeLetter = (letter, btn) => {
|
||||
const emptySpace = document.querySelector(
|
||||
`.letter-space[data-letter="${letter}"]:not(.filled)`
|
||||
);
|
||||
|
||||
if (emptySpace) {
|
||||
emptySpace.textContent = letter.toUpperCase();
|
||||
emptySpace.classList.add('filled');
|
||||
btn.disabled = true;
|
||||
|
||||
// Vérifier si le mot est complet
|
||||
const allSpaces = document.querySelectorAll('.letter-space');
|
||||
const filledSpaces = document.querySelectorAll('.letter-space.filled');
|
||||
|
||||
if (allSpaces.length === filledSpaces.length) {
|
||||
score += 15;
|
||||
document.getElementById('builder-score').textContent = score;
|
||||
this.onScoreUpdate(score);
|
||||
|
||||
document.getElementById('next-word-btn').style.display = 'inline-block';
|
||||
document.getElementById('give-up-btn').style.display = 'none';
|
||||
|
||||
// Désactiver toutes les lettres
|
||||
document.querySelectorAll('.letter-btn').forEach(b => b.disabled = true);
|
||||
}
|
||||
} else {
|
||||
// Mauvaise lettre
|
||||
btn.classList.add('wrong-letter');
|
||||
setTimeout(() => btn.classList.remove('wrong-letter'), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('next-word-btn').addEventListener('click', () => {
|
||||
currentWordIndex++;
|
||||
showBuilderWord();
|
||||
});
|
||||
|
||||
document.getElementById('give-up-btn').addEventListener('click', () => {
|
||||
currentWordIndex++;
|
||||
showBuilderWord();
|
||||
});
|
||||
|
||||
this.placeLetter = placeLetter;
|
||||
showBuilderWord();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
start() {
|
||||
// Interface commune - montrer le sélecteur
|
||||
this.showGameSelector();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// CSS supplémentaire pour les mini-jeux
|
||||
const tempGamesStyles = `
|
||||
<style>
|
||||
.temp-games-wrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-games-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.mini-game-card {
|
||||
background: white;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mini-game-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.mini-game-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mini-game-title {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mini-game-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mini-game-difficulty {
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.play-mini-game-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mini-game {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mini-game-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.word-match-board {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.word-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.word-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.word-btn.selected {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.word-btn.matched {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
border-color: var(--secondary-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.translation-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.option-btn {
|
||||
padding: 15px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.option-btn.correct {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.option-btn.wrong {
|
||||
background: var(--error-color);
|
||||
color: white;
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
.word-construction {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.word-spaces {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.letter-space {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.letter-space.filled {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.available-letters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.letter-btn {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.letter-btn:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.letter-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.letter-btn.wrong-letter {
|
||||
background: var(--error-color);
|
||||
color: white;
|
||||
border-color: var(--error-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.word-match-board {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.translation-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mini-game-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
// Ajouter les styles
|
||||
document.head.insertAdjacentHTML('beforeend', tempGamesStyles);
|
||||
|
||||
// Enregistrement du module
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.TempGames = TempGamesModule;
|
||||
367
js/games/text-reader.js
Normal file
367
js/games/text-reader.js
Normal file
@ -0,0 +1,367 @@
|
||||
// === MODULE TEXT READER ===
|
||||
|
||||
class TextReaderGame {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.content = options.content;
|
||||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||||
this.onGameEnd = options.onGameEnd || (() => {});
|
||||
|
||||
// État du lecteur
|
||||
this.currentTextIndex = 0;
|
||||
this.currentSentenceIndex = 0;
|
||||
this.isRunning = false;
|
||||
|
||||
// Données de lecture
|
||||
this.texts = this.extractTexts(this.content);
|
||||
this.currentText = null;
|
||||
this.sentences = [];
|
||||
this.showingFullText = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Vérifier que nous avons des textes
|
||||
if (!this.texts || this.texts.length === 0) {
|
||||
console.error('Aucun texte disponible pour Text Reader');
|
||||
this.showInitError();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createReaderInterface();
|
||||
this.setupEventListeners();
|
||||
this.loadText();
|
||||
}
|
||||
|
||||
showInitError() {
|
||||
this.container.innerHTML = `
|
||||
<div class="game-error">
|
||||
<h3>❌ Error loading</h3>
|
||||
<p>This content doesn't contain texts compatible with Text Reader.</p>
|
||||
<p>The reader needs texts to display.</p>
|
||||
<button onclick="AppNavigation.goBack()" class="back-btn">← Back</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
extractTexts(content) {
|
||||
let texts = [];
|
||||
|
||||
console.log('📖 Extracting texts from:', content?.name || 'content');
|
||||
|
||||
// Use raw module content if available
|
||||
if (content.rawContent) {
|
||||
console.log('📦 Using raw module content');
|
||||
return this.extractTextsFromRaw(content.rawContent);
|
||||
}
|
||||
|
||||
// Format with texts array
|
||||
if (content.texts && Array.isArray(content.texts)) {
|
||||
console.log('📝 Texts format detected');
|
||||
texts = content.texts.filter(text =>
|
||||
text.content && text.content.trim() !== ''
|
||||
);
|
||||
}
|
||||
// Modern format with contentItems
|
||||
else if (content.contentItems && Array.isArray(content.contentItems)) {
|
||||
console.log('🆕 ContentItems format detected');
|
||||
texts = content.contentItems
|
||||
.filter(item => item.type === 'text' && item.content)
|
||||
.map(item => ({
|
||||
title: item.title || 'Text',
|
||||
content: item.content
|
||||
}));
|
||||
}
|
||||
|
||||
return this.finalizeTexts(texts);
|
||||
}
|
||||
|
||||
extractTextsFromRaw(rawContent) {
|
||||
console.log('🔧 Extracting from raw content:', rawContent.name || 'Module');
|
||||
let texts = [];
|
||||
|
||||
// Simple format (texts array)
|
||||
if (rawContent.texts && Array.isArray(rawContent.texts)) {
|
||||
texts = rawContent.texts.filter(text =>
|
||||
text.content && text.content.trim() !== ''
|
||||
);
|
||||
console.log(`📝 ${texts.length} texts extracted from texts array`);
|
||||
}
|
||||
// ContentItems format
|
||||
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
|
||||
texts = rawContent.contentItems
|
||||
.filter(item => item.type === 'text' && item.content)
|
||||
.map(item => ({
|
||||
title: item.title || 'Text',
|
||||
content: item.content
|
||||
}));
|
||||
console.log(`🆕 ${texts.length} texts extracted from contentItems`);
|
||||
}
|
||||
|
||||
return this.finalizeTexts(texts);
|
||||
}
|
||||
|
||||
finalizeTexts(texts) {
|
||||
// Validation and cleanup
|
||||
texts = texts.filter(text =>
|
||||
text &&
|
||||
typeof text.content === 'string' &&
|
||||
text.content.trim() !== ''
|
||||
);
|
||||
|
||||
if (texts.length === 0) {
|
||||
console.error('❌ No valid texts found');
|
||||
// Demo texts as fallback
|
||||
texts = [
|
||||
{
|
||||
title: "Demo Text",
|
||||
content: "This is a demo text. It has multiple sentences. Each sentence will be displayed one by one. You can navigate using the buttons below."
|
||||
}
|
||||
];
|
||||
console.warn('🚨 Using demo texts');
|
||||
}
|
||||
|
||||
console.log(`✅ Text Reader: ${texts.length} texts finalized`);
|
||||
return texts;
|
||||
}
|
||||
|
||||
createReaderInterface() {
|
||||
this.container.innerHTML = `
|
||||
<div class="text-reader-wrapper">
|
||||
<!-- Text Selection -->
|
||||
<div class="text-selection">
|
||||
<label for="text-selector" class="text-selector-label">Choose a text:</label>
|
||||
<select id="text-selector" class="text-selector">
|
||||
<!-- Options will be generated here -->
|
||||
</select>
|
||||
<div class="text-progress">
|
||||
<span id="sentence-counter">1 / 1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reading Area -->
|
||||
<div class="reading-area" id="reading-area">
|
||||
<div class="sentence-display" id="sentence-display">
|
||||
<!-- Current sentence will appear here -->
|
||||
</div>
|
||||
|
||||
<div class="full-text-display" id="full-text-display" style="display: none;">
|
||||
<!-- Full text will appear here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Controls -->
|
||||
<div class="reader-controls">
|
||||
<button class="control-btn secondary" id="prev-sentence-btn" disabled>← Previous</button>
|
||||
<button class="control-btn primary" id="next-sentence-btn">Next →</button>
|
||||
<button class="control-btn secondary" id="show-full-btn">📄 Full Text</button>
|
||||
</div>
|
||||
|
||||
<!-- Full Text Navigation -->
|
||||
<div class="full-text-navigation" id="full-text-navigation" style="display: none;">
|
||||
<button class="control-btn secondary" id="back-to-reading-btn">📖 Back to Reading</button>
|
||||
</div>
|
||||
|
||||
<!-- Feedback Area -->
|
||||
<div class="feedback-area" id="feedback-area">
|
||||
<div class="instruction">
|
||||
Use Next/Previous buttons to navigate through sentences
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
document.getElementById('next-sentence-btn').addEventListener('click', () => this.nextSentence());
|
||||
document.getElementById('prev-sentence-btn').addEventListener('click', () => this.prevSentence());
|
||||
document.getElementById('show-full-btn').addEventListener('click', () => this.showFullText());
|
||||
|
||||
document.getElementById('back-to-reading-btn').addEventListener('click', () => this.backToReading());
|
||||
document.getElementById('text-selector').addEventListener('change', (e) => this.selectText(parseInt(e.target.value)));
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
if (this.showingFullText) {
|
||||
if (e.key === 'Escape') this.backToReading();
|
||||
} else {
|
||||
if (e.key === 'ArrowLeft') this.prevSentence();
|
||||
else if (e.key === 'ArrowRight') this.nextSentence();
|
||||
else if (e.key === 'Enter' || e.key === ' ') this.showFullText();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log('📖 Text Reader: Starting');
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
restart() {
|
||||
console.log('🔄 Text Reader: Restarting');
|
||||
this.reset();
|
||||
this.start();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.currentTextIndex = 0;
|
||||
this.currentSentenceIndex = 0;
|
||||
this.isRunning = false;
|
||||
this.showingFullText = false;
|
||||
this.loadText();
|
||||
}
|
||||
|
||||
loadText() {
|
||||
if (this.currentTextIndex >= this.texts.length) {
|
||||
this.currentTextIndex = 0;
|
||||
}
|
||||
|
||||
this.currentText = this.texts[this.currentTextIndex];
|
||||
this.sentences = this.splitIntoSentences(this.currentText.content);
|
||||
this.currentSentenceIndex = 0;
|
||||
this.showingFullText = false;
|
||||
|
||||
this.populateTextSelector();
|
||||
this.updateDisplay();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
populateTextSelector() {
|
||||
const selector = document.getElementById('text-selector');
|
||||
selector.innerHTML = '';
|
||||
|
||||
this.texts.forEach((text, index) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = index;
|
||||
option.textContent = text.title || `Text ${index + 1}`;
|
||||
if (index === this.currentTextIndex) {
|
||||
option.selected = true;
|
||||
}
|
||||
selector.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
selectText(textIndex) {
|
||||
if (textIndex >= 0 && textIndex < this.texts.length) {
|
||||
this.currentTextIndex = textIndex;
|
||||
this.currentText = this.texts[this.currentTextIndex];
|
||||
this.sentences = this.splitIntoSentences(this.currentText.content);
|
||||
this.currentSentenceIndex = 0;
|
||||
|
||||
// Always go back to sentence reading when changing text
|
||||
if (this.showingFullText) {
|
||||
this.backToReading();
|
||||
} else {
|
||||
this.updateDisplay();
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
this.showFeedback(`Switched to: ${this.currentText.title}`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
splitIntoSentences(text) {
|
||||
// Split by periods, exclamation marks, and question marks
|
||||
// Keep the punctuation with the sentence
|
||||
const sentences = text.split(/(?<=[.!?])\s+/)
|
||||
.filter(sentence => sentence.trim() !== '')
|
||||
.map(sentence => sentence.trim());
|
||||
|
||||
return sentences.length > 0 ? sentences : [text];
|
||||
}
|
||||
|
||||
nextSentence() {
|
||||
if (this.currentSentenceIndex < this.sentences.length - 1) {
|
||||
this.currentSentenceIndex++;
|
||||
this.updateDisplay();
|
||||
this.updateUI();
|
||||
} else {
|
||||
// End of sentences, show full text automatically
|
||||
this.showFullText();
|
||||
}
|
||||
}
|
||||
|
||||
prevSentence() {
|
||||
if (this.currentSentenceIndex > 0) {
|
||||
this.currentSentenceIndex--;
|
||||
this.updateDisplay();
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
showFullText() {
|
||||
this.showingFullText = true;
|
||||
document.getElementById('sentence-display').style.display = 'none';
|
||||
document.getElementById('full-text-display').style.display = 'block';
|
||||
document.getElementById('full-text-display').innerHTML = `
|
||||
<div class="full-text-content">
|
||||
<p>${this.currentText.content}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show full text navigation controls
|
||||
document.querySelector('.reader-controls').style.display = 'none';
|
||||
document.getElementById('full-text-navigation').style.display = 'flex';
|
||||
|
||||
this.showFeedback('Full text displayed. Use dropdown to change text.', 'info');
|
||||
}
|
||||
|
||||
backToReading() {
|
||||
this.showingFullText = false;
|
||||
document.getElementById('sentence-display').style.display = 'block';
|
||||
document.getElementById('full-text-display').style.display = 'none';
|
||||
|
||||
// Show sentence navigation controls
|
||||
document.querySelector('.reader-controls').style.display = 'flex';
|
||||
document.getElementById('full-text-navigation').style.display = 'none';
|
||||
|
||||
this.updateDisplay();
|
||||
this.updateUI();
|
||||
this.showFeedback('Back to sentence-by-sentence reading.', 'info');
|
||||
}
|
||||
|
||||
// Text navigation methods removed - using dropdown instead
|
||||
|
||||
updateDisplay() {
|
||||
if (this.showingFullText) return;
|
||||
|
||||
const sentenceDisplay = document.getElementById('sentence-display');
|
||||
const currentSentence = this.sentences[this.currentSentenceIndex];
|
||||
|
||||
sentenceDisplay.innerHTML = `
|
||||
<div class="current-sentence">
|
||||
${currentSentence}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
// Update counters
|
||||
document.getElementById('sentence-counter').textContent = `${this.currentSentenceIndex + 1} / ${this.sentences.length}`;
|
||||
|
||||
// Update button states
|
||||
document.getElementById('prev-sentence-btn').disabled = this.currentSentenceIndex === 0;
|
||||
document.getElementById('next-sentence-btn').disabled = false;
|
||||
document.getElementById('next-sentence-btn').textContent =
|
||||
this.currentSentenceIndex === this.sentences.length - 1 ? 'Full Text →' : 'Next →';
|
||||
}
|
||||
|
||||
// updateTextNavigation method removed - using dropdown instead
|
||||
|
||||
showFeedback(message, type = 'info') {
|
||||
const feedbackArea = document.getElementById('feedback-area');
|
||||
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isRunning = false;
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Module registration
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.TextReader = TextReaderGame;
|
||||
644
js/games/whack-a-mole-hard.js
Normal file
644
js/games/whack-a-mole-hard.js
Normal file
@ -0,0 +1,644 @@
|
||||
// === MODULE WHACK-A-MOLE HARD ===
|
||||
|
||||
class WhackAMoleHardGame {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.content = options.content;
|
||||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||||
this.onGameEnd = options.onGameEnd || (() => {});
|
||||
|
||||
// État du jeu
|
||||
this.score = 0;
|
||||
this.errors = 0;
|
||||
this.maxErrors = 5;
|
||||
this.gameTime = 60; // 60 secondes
|
||||
this.timeLeft = this.gameTime;
|
||||
this.isRunning = false;
|
||||
this.gameMode = 'translation'; // 'translation', 'image', 'sound'
|
||||
|
||||
// Configuration des taupes
|
||||
this.holes = [];
|
||||
this.activeMoles = [];
|
||||
this.moleAppearTime = 3000; // 3 secondes d'affichage (plus long)
|
||||
this.spawnRate = 2000; // Nouvelle vague toutes les 2 secondes
|
||||
this.molesPerWave = 3; // 3 taupes par vague
|
||||
|
||||
// Timers
|
||||
this.gameTimer = null;
|
||||
this.spawnTimer = null;
|
||||
|
||||
// Vocabulaire pour ce jeu - adapté pour le nouveau système
|
||||
this.vocabulary = this.extractVocabulary(this.content);
|
||||
this.currentWords = [];
|
||||
this.targetWord = null;
|
||||
|
||||
// Système de garantie pour le mot cible
|
||||
this.spawnsSinceTarget = 0;
|
||||
this.maxSpawnsWithoutTarget = 10; // Le mot cible doit apparaître dans les 10 prochaines taupes (1/10 chance)
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Vérifier que nous avons du vocabulaire
|
||||
if (!this.vocabulary || this.vocabulary.length === 0) {
|
||||
console.error('Aucun vocabulaire disponible pour Whack-a-Mole');
|
||||
this.showInitError();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createGameBoard();
|
||||
this.createGameUI();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
showInitError() {
|
||||
this.container.innerHTML = `
|
||||
<div class="game-error">
|
||||
<h3>❌ Erreur de chargement</h3>
|
||||
<p>Ce contenu ne contient pas de vocabulaire compatible avec Whack-a-Mole.</p>
|
||||
<p>Le jeu nécessite des mots avec leurs traductions.</p>
|
||||
<button onclick="AppNavigation.goBack()" class="back-btn">← Retour</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
createGameBoard() {
|
||||
this.container.innerHTML = `
|
||||
<div class="whack-game-wrapper">
|
||||
<!-- Mode Selection -->
|
||||
<div class="mode-selector">
|
||||
<button class="mode-btn active" data-mode="translation">
|
||||
🔤 Translation
|
||||
</button>
|
||||
<button class="mode-btn" data-mode="image">
|
||||
🖼️ Image (soon)
|
||||
</button>
|
||||
<button class="mode-btn" data-mode="sound">
|
||||
🔊 Sound (soon)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Game Info -->
|
||||
<div class="game-info">
|
||||
<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="errors-count">${this.errors}</span>
|
||||
<span class="stat-label">Errors</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="target-word">---</span>
|
||||
<span class="stat-label">Find</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="game-controls">
|
||||
<button class="control-btn" id="start-btn">🎮 Start</button>
|
||||
<button class="control-btn" id="pause-btn" disabled>⏸️ Pause</button>
|
||||
<button class="control-btn" id="restart-btn">🔄 Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Board -->
|
||||
<div class="whack-game-board hard-mode" id="game-board">
|
||||
<!-- Les trous seront générés ici (5x3 = 15 trous) -->
|
||||
</div>
|
||||
|
||||
<!-- Feedback Area -->
|
||||
<div class="feedback-area" id="feedback-area">
|
||||
<div class="instruction">
|
||||
Select a mode and click Start!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.createHoles();
|
||||
}
|
||||
|
||||
createHoles() {
|
||||
const gameBoard = document.getElementById('game-board');
|
||||
gameBoard.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < 15; i++) { // 5x3 = 15 trous
|
||||
const hole = document.createElement('div');
|
||||
hole.className = 'whack-hole';
|
||||
hole.dataset.holeId = i;
|
||||
|
||||
hole.innerHTML = `
|
||||
<div class="whack-mole" data-hole="${i}">
|
||||
<div class="word"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
gameBoard.appendChild(hole);
|
||||
this.holes.push({
|
||||
element: hole,
|
||||
mole: hole.querySelector('.whack-mole'),
|
||||
wordElement: hole.querySelector('.word'),
|
||||
isActive: false,
|
||||
word: null,
|
||||
timer: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createGameUI() {
|
||||
// Les éléments UI sont déjà créés dans createGameBoard
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (this.gameMode !== 'translation') {
|
||||
this.showFeedback('This mode will be available soon!', 'info');
|
||||
// Return to translation mode
|
||||
document.querySelector('.mode-btn[data-mode="translation"]').classList.add('active');
|
||||
btn.classList.remove('active');
|
||||
this.gameMode = 'translation';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Game controls
|
||||
document.getElementById('start-btn').addEventListener('click', () => this.start());
|
||||
document.getElementById('pause-btn').addEventListener('click', () => this.pause());
|
||||
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
|
||||
|
||||
// Mole clicks
|
||||
this.holes.forEach((hole, index) => {
|
||||
hole.mole.addEventListener('click', () => this.hitMole(index));
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.isRunning) return;
|
||||
|
||||
this.isRunning = true;
|
||||
this.score = 0;
|
||||
this.errors = 0;
|
||||
this.timeLeft = this.gameTime;
|
||||
|
||||
this.updateUI();
|
||||
this.setNewTarget();
|
||||
this.startTimers();
|
||||
|
||||
document.getElementById('start-btn').disabled = true;
|
||||
document.getElementById('pause-btn').disabled = false;
|
||||
|
||||
this.showFeedback(`Find the word: "${this.targetWord.french}"`, 'info');
|
||||
|
||||
// Show loaded content info
|
||||
const contentName = this.content.name || 'Content';
|
||||
console.log(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words)`);
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
this.isRunning = false;
|
||||
this.stopTimers();
|
||||
this.hideAllMoles();
|
||||
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('pause-btn').disabled = true;
|
||||
|
||||
this.showFeedback('Game paused', 'info');
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.stopWithoutEnd(); // Arrêter sans déclencher la fin de jeu
|
||||
this.resetGame();
|
||||
setTimeout(() => this.start(), 100);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopWithoutEnd();
|
||||
this.onGameEnd(this.score); // Déclencher la fin de jeu seulement ici
|
||||
}
|
||||
|
||||
stopWithoutEnd() {
|
||||
this.isRunning = false;
|
||||
this.stopTimers();
|
||||
this.hideAllMoles();
|
||||
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('pause-btn').disabled = true;
|
||||
}
|
||||
|
||||
resetGame() {
|
||||
// S'assurer que tout est complètement arrêté
|
||||
this.stopWithoutEnd();
|
||||
|
||||
// Reset de toutes les variables d'état
|
||||
this.score = 0;
|
||||
this.errors = 0;
|
||||
this.timeLeft = this.gameTime;
|
||||
this.isRunning = false;
|
||||
this.targetWord = null;
|
||||
this.activeMoles = [];
|
||||
this.spawnsSinceTarget = 0; // Reset du compteur de garantie
|
||||
|
||||
// S'assurer que tous les timers sont bien arrêtés
|
||||
this.stopTimers();
|
||||
|
||||
// Reset UI
|
||||
this.updateUI();
|
||||
this.onScoreUpdate(0);
|
||||
|
||||
// Clear feedback
|
||||
document.getElementById('target-word').textContent = '---';
|
||||
this.showFeedback('Select a mode and click Start!', 'info');
|
||||
|
||||
// Reset buttons
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('pause-btn').disabled = true;
|
||||
|
||||
// Clear all holes avec vérification
|
||||
this.holes.forEach(hole => {
|
||||
if (hole.timer) {
|
||||
clearTimeout(hole.timer);
|
||||
hole.timer = null;
|
||||
}
|
||||
hole.isActive = false;
|
||||
hole.word = null;
|
||||
if (hole.wordElement) {
|
||||
hole.wordElement.textContent = '';
|
||||
}
|
||||
if (hole.mole) {
|
||||
hole.mole.classList.remove('active', 'hit');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🔄 Game completely reset');
|
||||
}
|
||||
|
||||
startTimers() {
|
||||
// Timer principal du jeu
|
||||
this.gameTimer = setInterval(() => {
|
||||
this.timeLeft--;
|
||||
this.updateUI();
|
||||
|
||||
if (this.timeLeft <= 0 && this.isRunning) {
|
||||
this.stop();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Timer d'apparition des taupes
|
||||
this.spawnTimer = setInterval(() => {
|
||||
if (this.isRunning) {
|
||||
this.spawnMole();
|
||||
}
|
||||
}, this.spawnRate);
|
||||
|
||||
// Première taupe immédiate
|
||||
setTimeout(() => this.spawnMole(), 500);
|
||||
}
|
||||
|
||||
stopTimers() {
|
||||
if (this.gameTimer) {
|
||||
clearInterval(this.gameTimer);
|
||||
this.gameTimer = null;
|
||||
}
|
||||
if (this.spawnTimer) {
|
||||
clearInterval(this.spawnTimer);
|
||||
this.spawnTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
spawnMole() {
|
||||
// Mode Hard: Spawn 3 taupes à la fois
|
||||
this.spawnMultipleMoles();
|
||||
}
|
||||
|
||||
spawnMultipleMoles() {
|
||||
// Trouver tous les trous libres
|
||||
const availableHoles = this.holes.filter(hole => !hole.isActive);
|
||||
|
||||
// Spawn jusqu'à 3 taupes (ou moins si pas assez de trous libres)
|
||||
const molesToSpawn = Math.min(this.molesPerWave, availableHoles.length);
|
||||
|
||||
if (molesToSpawn === 0) return;
|
||||
|
||||
// Mélanger les trous disponibles
|
||||
const shuffledHoles = this.shuffleArray(availableHoles);
|
||||
|
||||
// Spawn les taupes
|
||||
for (let i = 0; i < molesToSpawn; i++) {
|
||||
const hole = shuffledHoles[i];
|
||||
const holeIndex = this.holes.indexOf(hole);
|
||||
|
||||
// Choisir un mot selon la stratégie de garantie
|
||||
const word = this.getWordWithTargetGuarantee();
|
||||
|
||||
// Activer la taupe avec un petit délai pour un effet visuel
|
||||
setTimeout(() => {
|
||||
if (this.isRunning && !hole.isActive) {
|
||||
this.activateMole(holeIndex, word);
|
||||
}
|
||||
}, i * 200); // Délai de 200ms entre chaque taupe
|
||||
}
|
||||
}
|
||||
|
||||
getWordWithTargetGuarantee() {
|
||||
// Incrémenter le compteur de spawns depuis le dernier mot cible
|
||||
this.spawnsSinceTarget++;
|
||||
|
||||
// Si on a atteint la limite, forcer le mot cible
|
||||
if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) {
|
||||
console.log(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`);
|
||||
this.spawnsSinceTarget = 0;
|
||||
return this.targetWord;
|
||||
}
|
||||
|
||||
// Sinon, 10% de chance d'avoir le mot cible (1/10 au lieu de 1/2)
|
||||
if (Math.random() < 0.1) {
|
||||
console.log('🎯 Spawn naturel du mot cible (1/10)');
|
||||
this.spawnsSinceTarget = 0;
|
||||
return this.targetWord;
|
||||
} else {
|
||||
return this.getRandomWord();
|
||||
}
|
||||
}
|
||||
|
||||
activateMole(holeIndex, word) {
|
||||
const hole = this.holes[holeIndex];
|
||||
if (hole.isActive) return;
|
||||
|
||||
hole.isActive = true;
|
||||
hole.word = word;
|
||||
hole.wordElement.textContent = word.english;
|
||||
hole.mole.classList.add('active');
|
||||
|
||||
// Ajouter à la liste des taupes actives
|
||||
this.activeMoles.push(holeIndex);
|
||||
|
||||
// Timer pour faire disparaître la taupe
|
||||
hole.timer = setTimeout(() => {
|
||||
this.deactivateMole(holeIndex);
|
||||
}, this.moleAppearTime);
|
||||
}
|
||||
|
||||
deactivateMole(holeIndex) {
|
||||
const hole = this.holes[holeIndex];
|
||||
if (!hole.isActive) return;
|
||||
|
||||
hole.isActive = false;
|
||||
hole.word = null;
|
||||
hole.wordElement.textContent = '';
|
||||
hole.mole.classList.remove('active');
|
||||
|
||||
if (hole.timer) {
|
||||
clearTimeout(hole.timer);
|
||||
hole.timer = null;
|
||||
}
|
||||
|
||||
// Retirer de la liste des taupes actives
|
||||
const activeIndex = this.activeMoles.indexOf(holeIndex);
|
||||
if (activeIndex > -1) {
|
||||
this.activeMoles.splice(activeIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
hitMole(holeIndex) {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const hole = this.holes[holeIndex];
|
||||
if (!hole.isActive || !hole.word) return;
|
||||
|
||||
const isCorrect = hole.word.french === this.targetWord.french;
|
||||
|
||||
if (isCorrect) {
|
||||
// Bonne réponse
|
||||
this.score += 10;
|
||||
this.deactivateMole(holeIndex);
|
||||
this.setNewTarget();
|
||||
this.showScorePopup(holeIndex, '+10', true);
|
||||
this.showFeedback(`Well done! Now find: "${this.targetWord.french}"`, 'success');
|
||||
|
||||
// Success animation
|
||||
hole.mole.classList.add('hit');
|
||||
setTimeout(() => hole.mole.classList.remove('hit'), 500);
|
||||
|
||||
} else {
|
||||
// Wrong answer
|
||||
this.errors++;
|
||||
this.score = Math.max(0, this.score - 2);
|
||||
this.showScorePopup(holeIndex, '-2', false);
|
||||
this.showFeedback(`Oops! "${hole.word.french}" ≠ "${this.targetWord.french}"`, 'error');
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
this.onScoreUpdate(this.score);
|
||||
|
||||
// Check game end by errors
|
||||
if (this.errors >= this.maxErrors) {
|
||||
this.showFeedback('Too many errors! Game over.', 'error');
|
||||
setTimeout(() => {
|
||||
if (this.isRunning) { // Check if game is still running
|
||||
this.stop();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
setNewTarget() {
|
||||
// Choisir un nouveau mot cible
|
||||
const availableWords = this.vocabulary.filter(word =>
|
||||
!this.activeMoles.some(moleIndex =>
|
||||
this.holes[moleIndex].word &&
|
||||
this.holes[moleIndex].word.english === word.english
|
||||
)
|
||||
);
|
||||
|
||||
if (availableWords.length > 0) {
|
||||
this.targetWord = availableWords[Math.floor(Math.random() * availableWords.length)];
|
||||
} else {
|
||||
this.targetWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)];
|
||||
}
|
||||
|
||||
// Reset du compteur pour le nouveau mot cible
|
||||
this.spawnsSinceTarget = 0;
|
||||
console.log(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`);
|
||||
|
||||
document.getElementById('target-word').textContent = this.targetWord.french;
|
||||
}
|
||||
|
||||
getRandomWord() {
|
||||
return this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)];
|
||||
}
|
||||
|
||||
hideAllMoles() {
|
||||
this.holes.forEach((hole, index) => {
|
||||
if (hole.isActive) {
|
||||
this.deactivateMole(index);
|
||||
}
|
||||
});
|
||||
this.activeMoles = [];
|
||||
}
|
||||
|
||||
showScorePopup(holeIndex, scoreText, isPositive) {
|
||||
const hole = this.holes[holeIndex];
|
||||
const popup = document.createElement('div');
|
||||
popup.className = `score-popup ${isPositive ? 'correct-answer' : 'wrong-answer'}`;
|
||||
popup.textContent = scoreText;
|
||||
|
||||
const rect = hole.element.getBoundingClientRect();
|
||||
popup.style.left = rect.left + rect.width / 2 + 'px';
|
||||
popup.style.top = rect.top + 'px';
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
setTimeout(() => {
|
||||
if (popup.parentNode) {
|
||||
popup.parentNode.removeChild(popup);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
showFeedback(message, type = 'info') {
|
||||
const feedbackArea = document.getElementById('feedback-area');
|
||||
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
document.getElementById('time-left').textContent = this.timeLeft;
|
||||
document.getElementById('errors-count').textContent = this.errors;
|
||||
}
|
||||
|
||||
extractVocabulary(content) {
|
||||
let vocabulary = [];
|
||||
|
||||
console.log('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu');
|
||||
|
||||
// Priorité 1: Utiliser le contenu brut du module (format simple)
|
||||
if (content.rawContent) {
|
||||
console.log('📦 Utilisation du contenu brut du module');
|
||||
return this.extractVocabularyFromRaw(content.rawContent);
|
||||
}
|
||||
|
||||
// Priorité 2: Format simple avec vocabulary object (nouveau format préféré)
|
||||
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
|
||||
console.log('✨ Format simple détecté (vocabulary object)');
|
||||
vocabulary = Object.entries(content.vocabulary).map(([english, translation]) => ({
|
||||
english: english,
|
||||
french: translation.split(';')[0], // Prendre la première traduction si plusieurs
|
||||
chinese: translation, // Garder la traduction complète en chinois
|
||||
category: 'general'
|
||||
}));
|
||||
}
|
||||
// Priorité 3: Format legacy avec vocabulary array
|
||||
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
|
||||
console.log('📚 Format legacy détecté (vocabulary array)');
|
||||
vocabulary = content.vocabulary.filter(word => word.english && word.french);
|
||||
}
|
||||
// Priorité 4: Format moderne avec contentItems
|
||||
else if (content.contentItems && Array.isArray(content.contentItems)) {
|
||||
console.log('🆕 Format contentItems détecté');
|
||||
vocabulary = content.contentItems
|
||||
.filter(item => item.type === 'vocabulary' && item.english && item.french)
|
||||
.map(item => ({
|
||||
english: item.english,
|
||||
french: item.french,
|
||||
image: item.image || null,
|
||||
category: item.category || 'general'
|
||||
}));
|
||||
}
|
||||
|
||||
return this.finalizeVocabulary(vocabulary);
|
||||
}
|
||||
|
||||
extractVocabularyFromRaw(rawContent) {
|
||||
console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module');
|
||||
let vocabulary = [];
|
||||
|
||||
// Format simple avec vocabulary object (PRÉFÉRÉ)
|
||||
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
|
||||
vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({
|
||||
english: english,
|
||||
french: translation.split(';')[0], // Première traduction pour le français
|
||||
chinese: translation, // Traduction complète en chinois
|
||||
category: 'general'
|
||||
}));
|
||||
console.log(`✨ ${vocabulary.length} mots extraits depuis vocabulary object (format simple)`);
|
||||
}
|
||||
// Format legacy (vocabulary array)
|
||||
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
|
||||
vocabulary = rawContent.vocabulary.filter(word => word.english && word.french);
|
||||
console.log(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`);
|
||||
}
|
||||
// Format contentItems (ancien format complexe)
|
||||
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
|
||||
vocabulary = rawContent.contentItems
|
||||
.filter(item => item.type === 'vocabulary' && item.english && item.french)
|
||||
.map(item => ({
|
||||
english: item.english,
|
||||
french: item.french,
|
||||
image: item.image || null,
|
||||
category: item.category || 'general'
|
||||
}));
|
||||
console.log(`📝 ${vocabulary.length} mots extraits depuis contentItems`);
|
||||
}
|
||||
// Fallback
|
||||
else {
|
||||
console.warn('⚠️ Format de contenu brut non reconnu');
|
||||
}
|
||||
|
||||
return this.finalizeVocabulary(vocabulary);
|
||||
}
|
||||
|
||||
finalizeVocabulary(vocabulary) {
|
||||
// Validation et nettoyage
|
||||
vocabulary = vocabulary.filter(word =>
|
||||
word &&
|
||||
typeof word.english === 'string' &&
|
||||
typeof word.french === 'string' &&
|
||||
word.english.trim() !== '' &&
|
||||
word.french.trim() !== ''
|
||||
);
|
||||
|
||||
if (vocabulary.length === 0) {
|
||||
console.error('❌ Aucun vocabulaire valide trouvé');
|
||||
// Vocabulaire de démonstration en dernier recours
|
||||
vocabulary = [
|
||||
{ english: 'hello', french: 'bonjour', category: 'greetings' },
|
||||
{ english: 'goodbye', french: 'au revoir', category: 'greetings' },
|
||||
{ english: 'thank you', french: 'merci', category: 'greetings' },
|
||||
{ english: 'cat', french: 'chat', category: 'animals' },
|
||||
{ english: 'dog', french: 'chien', category: 'animals' }
|
||||
];
|
||||
console.warn('🚨 Utilisation du vocabulaire de démonstration');
|
||||
}
|
||||
|
||||
console.log(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`);
|
||||
return this.shuffleArray(vocabulary);
|
||||
}
|
||||
|
||||
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.stop();
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Enregistrement du module
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.WhackAMoleHard = WhackAMoleHardGame;
|
||||
624
js/games/whack-a-mole.js
Normal file
624
js/games/whack-a-mole.js
Normal file
@ -0,0 +1,624 @@
|
||||
// === MODULE WHACK-A-MOLE ===
|
||||
|
||||
class WhackAMoleGame {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.content = options.content;
|
||||
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
||||
this.onGameEnd = options.onGameEnd || (() => {});
|
||||
|
||||
// État du jeu
|
||||
this.score = 0;
|
||||
this.errors = 0;
|
||||
this.maxErrors = 5;
|
||||
this.gameTime = 60; // 60 secondes
|
||||
this.timeLeft = this.gameTime;
|
||||
this.isRunning = false;
|
||||
this.gameMode = 'translation'; // 'translation', 'image', 'sound'
|
||||
|
||||
// Configuration des taupes
|
||||
this.holes = [];
|
||||
this.activeMoles = [];
|
||||
this.moleAppearTime = 2000; // 2 secondes d'affichage
|
||||
this.spawnRate = 1500; // Nouvelle taupe toutes les 1.5 secondes
|
||||
|
||||
// Timers
|
||||
this.gameTimer = null;
|
||||
this.spawnTimer = null;
|
||||
|
||||
// Vocabulaire pour ce jeu - adapté pour le nouveau système
|
||||
this.vocabulary = this.extractVocabulary(this.content);
|
||||
this.currentWords = [];
|
||||
this.targetWord = null;
|
||||
|
||||
// Système de garantie pour le mot cible
|
||||
this.spawnsSinceTarget = 0;
|
||||
this.maxSpawnsWithoutTarget = 3; // Le mot cible doit apparaître dans les 3 prochaines taupes
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Vérifier que nous avons du vocabulaire
|
||||
if (!this.vocabulary || this.vocabulary.length === 0) {
|
||||
console.error('Aucun vocabulaire disponible pour Whack-a-Mole');
|
||||
this.showInitError();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createGameBoard();
|
||||
this.createGameUI();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
showInitError() {
|
||||
this.container.innerHTML = `
|
||||
<div class="game-error">
|
||||
<h3>❌ Erreur de chargement</h3>
|
||||
<p>Ce contenu ne contient pas de vocabulaire compatible avec Whack-a-Mole.</p>
|
||||
<p>Le jeu nécessite des mots avec leurs traductions.</p>
|
||||
<button onclick="AppNavigation.goBack()" class="back-btn">← Retour</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
createGameBoard() {
|
||||
this.container.innerHTML = `
|
||||
<div class="whack-game-wrapper">
|
||||
<!-- Mode Selection -->
|
||||
<div class="mode-selector">
|
||||
<button class="mode-btn active" data-mode="translation">
|
||||
🔤 Translation
|
||||
</button>
|
||||
<button class="mode-btn" data-mode="image">
|
||||
🖼️ Image (soon)
|
||||
</button>
|
||||
<button class="mode-btn" data-mode="sound">
|
||||
🔊 Sound (soon)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Game Info -->
|
||||
<div class="game-info">
|
||||
<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="errors-count">${this.errors}</span>
|
||||
<span class="stat-label">Errors</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value" id="target-word">---</span>
|
||||
<span class="stat-label">Find</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="game-controls">
|
||||
<button class="control-btn" id="start-btn">🎮 Start</button>
|
||||
<button class="control-btn" id="pause-btn" disabled>⏸️ Pause</button>
|
||||
<button class="control-btn" id="restart-btn">🔄 Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Board -->
|
||||
<div class="whack-game-board" id="game-board">
|
||||
<!-- Les trous seront générés ici -->
|
||||
</div>
|
||||
|
||||
<!-- Feedback Area -->
|
||||
<div class="feedback-area" id="feedback-area">
|
||||
<div class="instruction">
|
||||
Select a mode and click Start!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.createHoles();
|
||||
}
|
||||
|
||||
createHoles() {
|
||||
const gameBoard = document.getElementById('game-board');
|
||||
gameBoard.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const hole = document.createElement('div');
|
||||
hole.className = 'whack-hole';
|
||||
hole.dataset.holeId = i;
|
||||
|
||||
hole.innerHTML = `
|
||||
<div class="whack-mole" data-hole="${i}">
|
||||
<div class="word"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
gameBoard.appendChild(hole);
|
||||
this.holes.push({
|
||||
element: hole,
|
||||
mole: hole.querySelector('.whack-mole'),
|
||||
wordElement: hole.querySelector('.word'),
|
||||
isActive: false,
|
||||
word: null,
|
||||
timer: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createGameUI() {
|
||||
// Les éléments UI sont déjà créés dans createGameBoard
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (this.gameMode !== 'translation') {
|
||||
this.showFeedback('This mode will be available soon!', 'info');
|
||||
// Return to translation mode
|
||||
document.querySelector('.mode-btn[data-mode="translation"]').classList.add('active');
|
||||
btn.classList.remove('active');
|
||||
this.gameMode = 'translation';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Game controls
|
||||
document.getElementById('start-btn').addEventListener('click', () => this.start());
|
||||
document.getElementById('pause-btn').addEventListener('click', () => this.pause());
|
||||
document.getElementById('restart-btn').addEventListener('click', () => this.restart());
|
||||
|
||||
// Mole clicks
|
||||
this.holes.forEach((hole, index) => {
|
||||
hole.mole.addEventListener('click', () => this.hitMole(index));
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.isRunning) return;
|
||||
|
||||
this.isRunning = true;
|
||||
this.score = 0;
|
||||
this.errors = 0;
|
||||
this.timeLeft = this.gameTime;
|
||||
|
||||
this.updateUI();
|
||||
this.setNewTarget();
|
||||
this.startTimers();
|
||||
|
||||
document.getElementById('start-btn').disabled = true;
|
||||
document.getElementById('pause-btn').disabled = false;
|
||||
|
||||
this.showFeedback(`Find the word: "${this.targetWord.french}"`, 'info');
|
||||
|
||||
// Show loaded content info
|
||||
const contentName = this.content.name || 'Content';
|
||||
console.log(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words)`);
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
this.isRunning = false;
|
||||
this.stopTimers();
|
||||
this.hideAllMoles();
|
||||
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('pause-btn').disabled = true;
|
||||
|
||||
this.showFeedback('Game paused', 'info');
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.stopWithoutEnd(); // Arrêter sans déclencher la fin de jeu
|
||||
this.resetGame();
|
||||
setTimeout(() => this.start(), 100);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopWithoutEnd();
|
||||
this.onGameEnd(this.score); // Déclencher la fin de jeu seulement ici
|
||||
}
|
||||
|
||||
stopWithoutEnd() {
|
||||
this.isRunning = false;
|
||||
this.stopTimers();
|
||||
this.hideAllMoles();
|
||||
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('pause-btn').disabled = true;
|
||||
}
|
||||
|
||||
resetGame() {
|
||||
// S'assurer que tout est complètement arrêté
|
||||
this.stopWithoutEnd();
|
||||
|
||||
// Reset de toutes les variables d'état
|
||||
this.score = 0;
|
||||
this.errors = 0;
|
||||
this.timeLeft = this.gameTime;
|
||||
this.isRunning = false;
|
||||
this.targetWord = null;
|
||||
this.activeMoles = [];
|
||||
this.spawnsSinceTarget = 0; // Reset du compteur de garantie
|
||||
|
||||
// S'assurer que tous les timers sont bien arrêtés
|
||||
this.stopTimers();
|
||||
|
||||
// Reset UI
|
||||
this.updateUI();
|
||||
this.onScoreUpdate(0);
|
||||
|
||||
// Clear feedback
|
||||
document.getElementById('target-word').textContent = '---';
|
||||
this.showFeedback('Select a mode and click Start!', 'info');
|
||||
|
||||
// Reset buttons
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('pause-btn').disabled = true;
|
||||
|
||||
// Clear all holes avec vérification
|
||||
this.holes.forEach(hole => {
|
||||
if (hole.timer) {
|
||||
clearTimeout(hole.timer);
|
||||
hole.timer = null;
|
||||
}
|
||||
hole.isActive = false;
|
||||
hole.word = null;
|
||||
if (hole.wordElement) {
|
||||
hole.wordElement.textContent = '';
|
||||
}
|
||||
if (hole.mole) {
|
||||
hole.mole.classList.remove('active', 'hit');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🔄 Game completely reset');
|
||||
}
|
||||
|
||||
startTimers() {
|
||||
// Timer principal du jeu
|
||||
this.gameTimer = setInterval(() => {
|
||||
this.timeLeft--;
|
||||
this.updateUI();
|
||||
|
||||
if (this.timeLeft <= 0 && this.isRunning) {
|
||||
this.stop();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Timer d'apparition des taupes
|
||||
this.spawnTimer = setInterval(() => {
|
||||
if (this.isRunning) {
|
||||
this.spawnMole();
|
||||
}
|
||||
}, this.spawnRate);
|
||||
|
||||
// Première taupe immédiate
|
||||
setTimeout(() => this.spawnMole(), 500);
|
||||
}
|
||||
|
||||
stopTimers() {
|
||||
if (this.gameTimer) {
|
||||
clearInterval(this.gameTimer);
|
||||
this.gameTimer = null;
|
||||
}
|
||||
if (this.spawnTimer) {
|
||||
clearInterval(this.spawnTimer);
|
||||
this.spawnTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
spawnMole() {
|
||||
// Trouver un trou libre
|
||||
const availableHoles = this.holes.filter(hole => !hole.isActive);
|
||||
if (availableHoles.length === 0) return;
|
||||
|
||||
const randomHole = availableHoles[Math.floor(Math.random() * availableHoles.length)];
|
||||
const holeIndex = this.holes.indexOf(randomHole);
|
||||
|
||||
// Choisir un mot selon la stratégie de garantie
|
||||
const word = this.getWordWithTargetGuarantee();
|
||||
|
||||
// Activer la taupe
|
||||
this.activateMole(holeIndex, word);
|
||||
}
|
||||
|
||||
getWordWithTargetGuarantee() {
|
||||
// Incrémenter le compteur de spawns depuis le dernier mot cible
|
||||
this.spawnsSinceTarget++;
|
||||
|
||||
// Si on a atteint la limite, forcer le mot cible
|
||||
if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) {
|
||||
console.log(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`);
|
||||
this.spawnsSinceTarget = 0;
|
||||
return this.targetWord;
|
||||
}
|
||||
|
||||
// Sinon, 50% de chance d'avoir le mot cible, 50% un mot aléatoire
|
||||
if (Math.random() < 0.5) {
|
||||
console.log('🎯 Spawn naturel du mot cible');
|
||||
this.spawnsSinceTarget = 0;
|
||||
return this.targetWord;
|
||||
} else {
|
||||
return this.getRandomWord();
|
||||
}
|
||||
}
|
||||
|
||||
activateMole(holeIndex, word) {
|
||||
const hole = this.holes[holeIndex];
|
||||
if (hole.isActive) return;
|
||||
|
||||
hole.isActive = true;
|
||||
hole.word = word;
|
||||
hole.wordElement.textContent = word.english;
|
||||
hole.mole.classList.add('active');
|
||||
|
||||
// Ajouter à la liste des taupes actives
|
||||
this.activeMoles.push(holeIndex);
|
||||
|
||||
// Timer pour faire disparaître la taupe
|
||||
hole.timer = setTimeout(() => {
|
||||
this.deactivateMole(holeIndex);
|
||||
}, this.moleAppearTime);
|
||||
}
|
||||
|
||||
deactivateMole(holeIndex) {
|
||||
const hole = this.holes[holeIndex];
|
||||
if (!hole.isActive) return;
|
||||
|
||||
hole.isActive = false;
|
||||
hole.word = null;
|
||||
hole.wordElement.textContent = '';
|
||||
hole.mole.classList.remove('active');
|
||||
|
||||
if (hole.timer) {
|
||||
clearTimeout(hole.timer);
|
||||
hole.timer = null;
|
||||
}
|
||||
|
||||
// Retirer de la liste des taupes actives
|
||||
const activeIndex = this.activeMoles.indexOf(holeIndex);
|
||||
if (activeIndex > -1) {
|
||||
this.activeMoles.splice(activeIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
hitMole(holeIndex) {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const hole = this.holes[holeIndex];
|
||||
if (!hole.isActive || !hole.word) return;
|
||||
|
||||
const isCorrect = hole.word.french === this.targetWord.french;
|
||||
|
||||
if (isCorrect) {
|
||||
// Bonne réponse
|
||||
this.score += 10;
|
||||
this.deactivateMole(holeIndex);
|
||||
this.setNewTarget();
|
||||
this.showScorePopup(holeIndex, '+10', true);
|
||||
this.showFeedback(`Well done! Now find: "${this.targetWord.french}"`, 'success');
|
||||
|
||||
// Success animation
|
||||
hole.mole.classList.add('hit');
|
||||
setTimeout(() => hole.mole.classList.remove('hit'), 500);
|
||||
|
||||
} else {
|
||||
// Wrong answer
|
||||
this.errors++;
|
||||
this.score = Math.max(0, this.score - 2);
|
||||
this.showScorePopup(holeIndex, '-2', false);
|
||||
this.showFeedback(`Oops! "${hole.word.french}" ≠ "${this.targetWord.french}"`, 'error');
|
||||
}
|
||||
|
||||
this.updateUI();
|
||||
this.onScoreUpdate(this.score);
|
||||
|
||||
// Check game end by errors
|
||||
if (this.errors >= this.maxErrors) {
|
||||
this.showFeedback('Too many errors! Game over.', 'error');
|
||||
setTimeout(() => {
|
||||
if (this.isRunning) { // Check if game is still running
|
||||
this.stop();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
setNewTarget() {
|
||||
// Choisir un nouveau mot cible
|
||||
const availableWords = this.vocabulary.filter(word =>
|
||||
!this.activeMoles.some(moleIndex =>
|
||||
this.holes[moleIndex].word &&
|
||||
this.holes[moleIndex].word.english === word.english
|
||||
)
|
||||
);
|
||||
|
||||
if (availableWords.length > 0) {
|
||||
this.targetWord = availableWords[Math.floor(Math.random() * availableWords.length)];
|
||||
} else {
|
||||
this.targetWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)];
|
||||
}
|
||||
|
||||
// Reset du compteur pour le nouveau mot cible
|
||||
this.spawnsSinceTarget = 0;
|
||||
console.log(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`);
|
||||
|
||||
document.getElementById('target-word').textContent = this.targetWord.french;
|
||||
}
|
||||
|
||||
getRandomWord() {
|
||||
return this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)];
|
||||
}
|
||||
|
||||
hideAllMoles() {
|
||||
this.holes.forEach((hole, index) => {
|
||||
if (hole.isActive) {
|
||||
this.deactivateMole(index);
|
||||
}
|
||||
});
|
||||
this.activeMoles = [];
|
||||
}
|
||||
|
||||
showScorePopup(holeIndex, scoreText, isPositive) {
|
||||
const hole = this.holes[holeIndex];
|
||||
const popup = document.createElement('div');
|
||||
popup.className = `score-popup ${isPositive ? 'correct-answer' : 'wrong-answer'}`;
|
||||
popup.textContent = scoreText;
|
||||
|
||||
const rect = hole.element.getBoundingClientRect();
|
||||
popup.style.left = rect.left + rect.width / 2 + 'px';
|
||||
popup.style.top = rect.top + 'px';
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
setTimeout(() => {
|
||||
if (popup.parentNode) {
|
||||
popup.parentNode.removeChild(popup);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
showFeedback(message, type = 'info') {
|
||||
const feedbackArea = document.getElementById('feedback-area');
|
||||
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
document.getElementById('time-left').textContent = this.timeLeft;
|
||||
document.getElementById('errors-count').textContent = this.errors;
|
||||
}
|
||||
|
||||
extractVocabulary(content) {
|
||||
let vocabulary = [];
|
||||
|
||||
console.log('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu');
|
||||
|
||||
// Priorité 1: Utiliser le contenu brut du module (format simple)
|
||||
if (content.rawContent) {
|
||||
console.log('📦 Utilisation du contenu brut du module');
|
||||
return this.extractVocabularyFromRaw(content.rawContent);
|
||||
}
|
||||
|
||||
// Priorité 2: Format simple avec vocabulary object (nouveau format préféré)
|
||||
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
|
||||
console.log('✨ Format simple détecté (vocabulary object)');
|
||||
vocabulary = Object.entries(content.vocabulary).map(([english, translation]) => ({
|
||||
english: english,
|
||||
french: translation.split(';')[0], // Prendre la première traduction si plusieurs
|
||||
chinese: translation, // Garder la traduction complète en chinois
|
||||
category: 'general'
|
||||
}));
|
||||
}
|
||||
// Priorité 3: Format legacy avec vocabulary array
|
||||
else if (content.vocabulary && Array.isArray(content.vocabulary)) {
|
||||
console.log('📚 Format legacy détecté (vocabulary array)');
|
||||
vocabulary = content.vocabulary.filter(word => word.english && word.french);
|
||||
}
|
||||
// Priorité 4: Format moderne avec contentItems
|
||||
else if (content.contentItems && Array.isArray(content.contentItems)) {
|
||||
console.log('🆕 Format contentItems détecté');
|
||||
vocabulary = content.contentItems
|
||||
.filter(item => item.type === 'vocabulary' && item.english && item.french)
|
||||
.map(item => ({
|
||||
english: item.english,
|
||||
french: item.french,
|
||||
image: item.image || null,
|
||||
category: item.category || 'general'
|
||||
}));
|
||||
}
|
||||
|
||||
return this.finalizeVocabulary(vocabulary);
|
||||
}
|
||||
|
||||
extractVocabularyFromRaw(rawContent) {
|
||||
console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module');
|
||||
let vocabulary = [];
|
||||
|
||||
// Format simple avec vocabulary object (PRÉFÉRÉ)
|
||||
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
|
||||
vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({
|
||||
english: english,
|
||||
french: translation.split(';')[0], // Première traduction pour le français
|
||||
chinese: translation, // Traduction complète en chinois
|
||||
category: 'general'
|
||||
}));
|
||||
console.log(`✨ ${vocabulary.length} mots extraits depuis vocabulary object (format simple)`);
|
||||
}
|
||||
// Format legacy (vocabulary array)
|
||||
else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) {
|
||||
vocabulary = rawContent.vocabulary.filter(word => word.english && word.french);
|
||||
console.log(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`);
|
||||
}
|
||||
// Format contentItems (ancien format complexe)
|
||||
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
|
||||
vocabulary = rawContent.contentItems
|
||||
.filter(item => item.type === 'vocabulary' && item.english && item.french)
|
||||
.map(item => ({
|
||||
english: item.english,
|
||||
french: item.french,
|
||||
image: item.image || null,
|
||||
category: item.category || 'general'
|
||||
}));
|
||||
console.log(`📝 ${vocabulary.length} mots extraits depuis contentItems`);
|
||||
}
|
||||
// Fallback
|
||||
else {
|
||||
console.warn('⚠️ Format de contenu brut non reconnu');
|
||||
}
|
||||
|
||||
return this.finalizeVocabulary(vocabulary);
|
||||
}
|
||||
|
||||
finalizeVocabulary(vocabulary) {
|
||||
// Validation et nettoyage
|
||||
vocabulary = vocabulary.filter(word =>
|
||||
word &&
|
||||
typeof word.english === 'string' &&
|
||||
typeof word.french === 'string' &&
|
||||
word.english.trim() !== '' &&
|
||||
word.french.trim() !== ''
|
||||
);
|
||||
|
||||
if (vocabulary.length === 0) {
|
||||
console.error('❌ Aucun vocabulaire valide trouvé');
|
||||
// Vocabulaire de démonstration en dernier recours
|
||||
vocabulary = [
|
||||
{ english: 'hello', french: 'bonjour', category: 'greetings' },
|
||||
{ english: 'goodbye', french: 'au revoir', category: 'greetings' },
|
||||
{ english: 'thank you', french: 'merci', category: 'greetings' },
|
||||
{ english: 'cat', french: 'chat', category: 'animals' },
|
||||
{ english: 'dog', french: 'chien', category: 'animals' }
|
||||
];
|
||||
console.warn('🚨 Utilisation du vocabulaire de démonstration');
|
||||
}
|
||||
|
||||
console.log(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`);
|
||||
return this.shuffleArray(vocabulary);
|
||||
}
|
||||
|
||||
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.stop();
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Enregistrement du module
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.WhackAMole = WhackAMoleGame;
|
||||
919
js/tools/content-creator.js
Normal file
919
js/tools/content-creator.js
Normal file
@ -0,0 +1,919 @@
|
||||
// === INTERFACE CRÉATEUR DE CONTENU ===
|
||||
|
||||
class ContentCreator {
|
||||
constructor() {
|
||||
this.factory = new ContentFactory();
|
||||
this.previewContainer = null;
|
||||
this.currentContent = null;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createInterface();
|
||||
this.setupEventListeners();
|
||||
this.loadExamples();
|
||||
}
|
||||
|
||||
createInterface() {
|
||||
// Créer l'interface dans le container principal
|
||||
const existingInterface = document.getElementById('content-creator');
|
||||
if (existingInterface) {
|
||||
existingInterface.remove();
|
||||
}
|
||||
|
||||
const interface = document.createElement('div');
|
||||
interface.id = 'content-creator';
|
||||
interface.className = 'content-creator-interface';
|
||||
|
||||
interface.innerHTML = `
|
||||
<div class="creator-header">
|
||||
<h2>🏭 Créateur de Contenu Universel</h2>
|
||||
<p>Transformez n'importe quel contenu en exercices interactifs</p>
|
||||
</div>
|
||||
|
||||
<div class="creator-tabs">
|
||||
<button class="tab-btn active" data-tab="text">📝 Texte Libre</button>
|
||||
<button class="tab-btn" data-tab="vocabulary">📚 Vocabulaire</button>
|
||||
<button class="tab-btn" data-tab="dialogue">💬 Dialogue</button>
|
||||
<button class="tab-btn" data-tab="sequence">📋 Séquence</button>
|
||||
</div>
|
||||
|
||||
<div class="creator-content">
|
||||
<!-- TAB: Texte Libre -->
|
||||
<div class="tab-content active" id="text-tab">
|
||||
<h3>Collez votre texte ici</h3>
|
||||
<textarea id="text-input" placeholder="Exemple:
|
||||
cat = chat
|
||||
dog = chien
|
||||
bird = oiseau
|
||||
|
||||
ou
|
||||
|
||||
1. Wake up at 7am
|
||||
2. Brush teeth
|
||||
3. Eat breakfast
|
||||
4. Go to school
|
||||
|
||||
ou
|
||||
|
||||
Alice: Hello! What's your name?
|
||||
Bob: Hi! I'm Bob. Nice to meet you!"></textarea>
|
||||
|
||||
<div class="input-options">
|
||||
<label>
|
||||
<input type="checkbox" id="auto-detect"> Détection automatique du type
|
||||
</label>
|
||||
<select id="content-type-select">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="vocabulary">Vocabulaire</option>
|
||||
<option value="dialogue">Dialogue</option>
|
||||
<option value="sequence">Séquence</option>
|
||||
<option value="sentence">Phrases</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Vocabulaire -->
|
||||
<div class="tab-content" id="vocabulary-tab">
|
||||
<h3>Liste de Vocabulaire</h3>
|
||||
<div class="vocab-builder">
|
||||
<div class="vocab-entry">
|
||||
<input type="text" placeholder="Anglais" class="english-input">
|
||||
<span>=</span>
|
||||
<input type="text" placeholder="Français" class="french-input">
|
||||
<input type="text" placeholder="Catégorie" class="category-input">
|
||||
<button class="remove-entry">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="add-vocab-entry">+ Ajouter un mot</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Dialogue -->
|
||||
<div class="tab-content" id="dialogue-tab">
|
||||
<h3>Créateur de Dialogue</h3>
|
||||
<input type="text" id="dialogue-scenario" placeholder="Scénario (ex: Au restaurant)">
|
||||
<div class="dialogue-builder">
|
||||
<div class="dialogue-line">
|
||||
<input type="text" placeholder="Personnage" class="speaker-input">
|
||||
<textarea placeholder="Réplique en anglais" class="line-input"></textarea>
|
||||
<button class="remove-line">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="add-dialogue-line">+ Ajouter une réplique</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB: Séquence -->
|
||||
<div class="tab-content" id="sequence-tab">
|
||||
<h3>Créateur de Séquence</h3>
|
||||
<input type="text" id="sequence-title" placeholder="Titre de la séquence">
|
||||
<div class="sequence-builder">
|
||||
<div class="sequence-step">
|
||||
<span class="step-number">1</span>
|
||||
<input type="text" placeholder="Étape en anglais" class="step-input">
|
||||
<input type="time" class="time-input" title="Heure (optionnel)">
|
||||
<button class="remove-step">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="add-sequence-step">+ Ajouter une étape</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="creator-actions">
|
||||
<div class="generation-options">
|
||||
<label>Nom du module:
|
||||
<input type="text" id="module-name" placeholder="Mon Contenu Personnalisé">
|
||||
</label>
|
||||
<label>Difficulté:
|
||||
<select id="difficulty-select">
|
||||
<option value="auto">Automatique</option>
|
||||
<option value="easy">Facile</option>
|
||||
<option value="medium">Moyen</option>
|
||||
<option value="hard">Difficile</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button id="preview-btn" class="btn secondary">👁️ Aperçu</button>
|
||||
<button id="generate-btn" class="btn primary">🚀 Générer Module</button>
|
||||
<button id="test-game-btn" class="btn success" style="display:none;">🎮 Tester dans un Jeu</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="creator-examples">
|
||||
<h4>💡 Exemples Rapides</h4>
|
||||
<div class="example-buttons">
|
||||
<button class="example-btn" data-example="vocabulary">Vocabulaire Animaux</button>
|
||||
<button class="example-btn" data-example="dialogue">Dialogue Restaurant</button>
|
||||
<button class="example-btn" data-example="sequence">Routine Matinale</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="creator-preview" id="content-preview" style="display:none;">
|
||||
<h4>📋 Aperçu du Contenu Généré</h4>
|
||||
<div class="preview-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Injecter dans la page
|
||||
const container = document.getElementById('game-container') || document.body;
|
||||
container.appendChild(interface);
|
||||
|
||||
this.previewContainer = interface.querySelector('#content-preview .preview-content');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Gestion des onglets
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
this.switchTab(btn.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
// Bouton générer
|
||||
document.getElementById('generate-btn').addEventListener('click', () => {
|
||||
this.generateContent();
|
||||
});
|
||||
|
||||
// Bouton aperçu
|
||||
document.getElementById('preview-btn').addEventListener('click', () => {
|
||||
this.previewContent();
|
||||
});
|
||||
|
||||
// Bouton test
|
||||
document.getElementById('test-game-btn').addEventListener('click', () => {
|
||||
this.testInGame();
|
||||
});
|
||||
|
||||
// Exemples
|
||||
document.querySelectorAll('.example-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
this.loadExample(btn.dataset.example);
|
||||
});
|
||||
});
|
||||
|
||||
// Builders dynamiques
|
||||
this.setupVocabularyBuilder();
|
||||
this.setupDialogueBuilder();
|
||||
this.setupSequenceBuilder();
|
||||
}
|
||||
|
||||
switchTab(tabName) {
|
||||
// Désactiver tous les onglets et contenu
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Activer le bon onglet
|
||||
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
||||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
||||
}
|
||||
|
||||
setupVocabularyBuilder() {
|
||||
document.getElementById('add-vocab-entry').addEventListener('click', () => {
|
||||
this.addVocabularyEntry();
|
||||
});
|
||||
|
||||
// Supprimer entries
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('remove-entry')) {
|
||||
e.target.parentElement.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addVocabularyEntry() {
|
||||
const builder = document.querySelector('.vocab-builder');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'vocab-entry';
|
||||
entry.innerHTML = `
|
||||
<input type="text" placeholder="Anglais" class="english-input">
|
||||
<span>=</span>
|
||||
<input type="text" placeholder="Français" class="french-input">
|
||||
<input type="text" placeholder="Catégorie" class="category-input">
|
||||
<button class="remove-entry">×</button>
|
||||
`;
|
||||
builder.appendChild(entry);
|
||||
}
|
||||
|
||||
setupDialogueBuilder() {
|
||||
document.getElementById('add-dialogue-line').addEventListener('click', () => {
|
||||
this.addDialogueLine();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('remove-line')) {
|
||||
e.target.parentElement.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addDialogueLine() {
|
||||
const builder = document.querySelector('.dialogue-builder');
|
||||
const line = document.createElement('div');
|
||||
line.className = 'dialogue-line';
|
||||
line.innerHTML = `
|
||||
<input type="text" placeholder="Personnage" class="speaker-input">
|
||||
<textarea placeholder="Réplique en anglais" class="line-input"></textarea>
|
||||
<button class="remove-line">×</button>
|
||||
`;
|
||||
builder.appendChild(line);
|
||||
}
|
||||
|
||||
setupSequenceBuilder() {
|
||||
document.getElementById('add-sequence-step').addEventListener('click', () => {
|
||||
this.addSequenceStep();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('remove-step')) {
|
||||
e.target.parentElement.remove();
|
||||
this.updateStepNumbers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addSequenceStep() {
|
||||
const builder = document.querySelector('.sequence-builder');
|
||||
const stepCount = builder.children.length + 1;
|
||||
const step = document.createElement('div');
|
||||
step.className = 'sequence-step';
|
||||
step.innerHTML = `
|
||||
<span class="step-number">${stepCount}</span>
|
||||
<input type="text" placeholder="Étape en anglais" class="step-input">
|
||||
<input type="time" class="time-input" title="Heure (optionnel)">
|
||||
<button class="remove-step">×</button>
|
||||
`;
|
||||
builder.appendChild(step);
|
||||
}
|
||||
|
||||
updateStepNumbers() {
|
||||
document.querySelectorAll('.step-number').forEach((num, index) => {
|
||||
num.textContent = index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
async generateContent() {
|
||||
try {
|
||||
const input = this.collectInput();
|
||||
const options = this.collectOptions();
|
||||
|
||||
console.log('🏭 Génération de contenu...', { input, options });
|
||||
|
||||
const content = await this.factory.createContent(input, options);
|
||||
this.currentContent = content;
|
||||
|
||||
this.showSuccess('Contenu généré avec succès !');
|
||||
this.displayPreview(content);
|
||||
|
||||
document.getElementById('test-game-btn').style.display = 'inline-block';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur génération:', error);
|
||||
this.showError('Erreur lors de la génération: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
collectInput() {
|
||||
const activeTab = document.querySelector('.tab-content.active').id;
|
||||
|
||||
switch (activeTab) {
|
||||
case 'text-tab':
|
||||
return document.getElementById('text-input').value;
|
||||
|
||||
case 'vocabulary-tab':
|
||||
return this.collectVocabularyData();
|
||||
|
||||
case 'dialogue-tab':
|
||||
return this.collectDialogueData();
|
||||
|
||||
case 'sequence-tab':
|
||||
return this.collectSequenceData();
|
||||
|
||||
default:
|
||||
throw new Error('Type de contenu non supporté');
|
||||
}
|
||||
}
|
||||
|
||||
collectVocabularyData() {
|
||||
const entries = document.querySelectorAll('.vocab-entry');
|
||||
const vocabulary = [];
|
||||
|
||||
entries.forEach(entry => {
|
||||
const english = entry.querySelector('.english-input').value;
|
||||
const french = entry.querySelector('.french-input').value;
|
||||
const category = entry.querySelector('.category-input').value || 'general';
|
||||
|
||||
if (english && french) {
|
||||
vocabulary.push({ english, french, category });
|
||||
}
|
||||
});
|
||||
|
||||
return { vocabulary };
|
||||
}
|
||||
|
||||
collectDialogueData() {
|
||||
const scenario = document.getElementById('dialogue-scenario').value || 'conversation';
|
||||
const lines = document.querySelectorAll('.dialogue-line');
|
||||
const conversation = [];
|
||||
|
||||
lines.forEach(line => {
|
||||
const speaker = line.querySelector('.speaker-input').value;
|
||||
const text = line.querySelector('.line-input').value;
|
||||
|
||||
if (speaker && text) {
|
||||
conversation.push({ speaker, english: text });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
dialogue: {
|
||||
scenario,
|
||||
conversation
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
collectSequenceData() {
|
||||
const title = document.getElementById('sequence-title').value || 'Sequence';
|
||||
const steps = document.querySelectorAll('.sequence-step');
|
||||
const sequence = [];
|
||||
|
||||
steps.forEach((step, index) => {
|
||||
const english = step.querySelector('.step-input').value;
|
||||
const time = step.querySelector('.time-input').value;
|
||||
|
||||
if (english) {
|
||||
sequence.push({
|
||||
order: index + 1,
|
||||
english,
|
||||
time: time || null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
sequence: {
|
||||
title,
|
||||
steps: sequence
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
collectOptions() {
|
||||
return {
|
||||
name: document.getElementById('module-name').value || 'Contenu Généré',
|
||||
difficulty: document.getElementById('difficulty-select').value === 'auto' ? null : document.getElementById('difficulty-select').value,
|
||||
contentType: document.getElementById('content-type-select').value === 'auto' ? null : document.getElementById('content-type-select').value
|
||||
};
|
||||
}
|
||||
|
||||
async previewContent() {
|
||||
try {
|
||||
const input = this.collectInput();
|
||||
const options = this.collectOptions();
|
||||
|
||||
// Génération rapide pour aperçu
|
||||
const content = await this.factory.createContent(input, options);
|
||||
this.displayPreview(content);
|
||||
|
||||
document.getElementById('content-preview').style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
this.showError('Erreur lors de l\'aperçu: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
displayPreview(content) {
|
||||
if (!this.previewContainer) return;
|
||||
|
||||
this.previewContainer.innerHTML = `
|
||||
<div class="preview-summary">
|
||||
<h5>${content.name}</h5>
|
||||
<p>${content.description}</p>
|
||||
<div class="preview-stats">
|
||||
<span class="stat">📊 ${content.contentItems.length} exercices</span>
|
||||
<span class="stat">🎯 Difficulté: ${content.difficulty}</span>
|
||||
<span class="stat">🏷️ Types: ${content.metadata.contentTypes.join(', ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-items">
|
||||
${content.contentItems.slice(0, 3).map(item => `
|
||||
<div class="preview-item">
|
||||
<span class="item-type">${item.type}</span>
|
||||
<span class="item-content">${item.content.english} = ${item.content.french}</span>
|
||||
<span class="item-interaction">${item.interaction.type}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
${content.contentItems.length > 3 ? `<div class="preview-more">... et ${content.contentItems.length - 3} autres</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('content-preview').style.display = 'block';
|
||||
}
|
||||
|
||||
async testInGame() {
|
||||
if (!this.currentContent) {
|
||||
this.showError('Aucun contenu généré à tester');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Sauvegarder temporairement le contenu
|
||||
const contentId = 'generated_test';
|
||||
window.ContentModules = window.ContentModules || {};
|
||||
window.ContentModules.GeneratedTest = this.currentContent;
|
||||
|
||||
// Naviguer vers un jeu pour tester
|
||||
this.showSuccess('Contenu prêt ! Redirection vers le jeu...');
|
||||
|
||||
setTimeout(() => {
|
||||
// Fermer l'interface
|
||||
document.getElementById('content-creator').remove();
|
||||
|
||||
// Lancer le jeu avec le contenu généré
|
||||
AppNavigation.navigateTo('play', 'whack-a-mole', 'generated-test');
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
this.showError('Erreur lors du test: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
loadExamples() {
|
||||
// Les exemples sont gérés par les boutons
|
||||
}
|
||||
|
||||
loadExample(type) {
|
||||
switch (type) {
|
||||
case 'vocabulary':
|
||||
this.switchTab('vocabulary');
|
||||
setTimeout(() => {
|
||||
// Effacer contenu existant
|
||||
document.querySelector('.vocab-builder').innerHTML = '';
|
||||
|
||||
// Ajouter exemples
|
||||
const animals = [
|
||||
{ english: 'cat', french: 'chat', category: 'animals' },
|
||||
{ english: 'dog', french: 'chien', category: 'animals' },
|
||||
{ english: 'bird', french: 'oiseau', category: 'animals' },
|
||||
{ english: 'fish', french: 'poisson', category: 'animals' }
|
||||
];
|
||||
|
||||
animals.forEach(animal => {
|
||||
this.addVocabularyEntry();
|
||||
const entries = document.querySelectorAll('.vocab-entry');
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
lastEntry.querySelector('.english-input').value = animal.english;
|
||||
lastEntry.querySelector('.french-input').value = animal.french;
|
||||
lastEntry.querySelector('.category-input').value = animal.category;
|
||||
});
|
||||
|
||||
document.getElementById('module-name').value = 'Animaux Domestiques';
|
||||
}, 100);
|
||||
break;
|
||||
|
||||
case 'dialogue':
|
||||
this.switchTab('dialogue');
|
||||
setTimeout(() => {
|
||||
document.getElementById('dialogue-scenario').value = 'Au Restaurant';
|
||||
document.querySelector('.dialogue-builder').innerHTML = '';
|
||||
|
||||
const dialogueLines = [
|
||||
{ speaker: 'Serveur', text: 'What would you like to order?' },
|
||||
{ speaker: 'Client', text: 'I would like a pizza, please.' },
|
||||
{ speaker: 'Serveur', text: 'What size?' },
|
||||
{ speaker: 'Client', text: 'Large, please.' }
|
||||
];
|
||||
|
||||
dialogueLines.forEach(line => {
|
||||
this.addDialogueLine();
|
||||
const lines = document.querySelectorAll('.dialogue-line');
|
||||
const lastLine = lines[lines.length - 1];
|
||||
lastLine.querySelector('.speaker-input').value = line.speaker;
|
||||
lastLine.querySelector('.line-input').value = line.text;
|
||||
});
|
||||
|
||||
document.getElementById('module-name').value = 'Dialogue Restaurant';
|
||||
}, 100);
|
||||
break;
|
||||
|
||||
case 'sequence':
|
||||
this.switchTab('sequence');
|
||||
setTimeout(() => {
|
||||
document.getElementById('sequence-title').value = 'Morning Routine';
|
||||
document.querySelector('.sequence-builder').innerHTML = '';
|
||||
|
||||
const steps = [
|
||||
{ text: 'Wake up', time: '07:00' },
|
||||
{ text: 'Brush teeth', time: '07:15' },
|
||||
{ text: 'Get dressed', time: '07:30' },
|
||||
{ text: 'Eat breakfast', time: '07:45' },
|
||||
{ text: 'Go to school', time: '08:00' }
|
||||
];
|
||||
|
||||
steps.forEach(step => {
|
||||
this.addSequenceStep();
|
||||
const stepElements = document.querySelectorAll('.sequence-step');
|
||||
const lastStep = stepElements[stepElements.length - 1];
|
||||
lastStep.querySelector('.step-input').value = step.text;
|
||||
lastStep.querySelector('.time-input').value = step.time;
|
||||
});
|
||||
|
||||
document.getElementById('module-name').value = 'Routine du Matin';
|
||||
}, 100);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
Utils.showToast(message, 'success');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
Utils.showToast(message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// CSS pour l'interface
|
||||
const contentCreatorStyles = `
|
||||
<style>
|
||||
.content-creator-interface {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
|
||||
margin: 20px auto;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.creator-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.creator-header h2 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.creator-tabs {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#text-input {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
padding: 15px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.input-options {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.vocab-builder, .dialogue-builder, .sequence-builder {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.vocab-entry, .dialogue-line, .sequence-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.vocab-entry input, .dialogue-line input, .sequence-step input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dialogue-line textarea {
|
||||
flex: 2;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
resize: vertical;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.remove-entry, .remove-line, .remove-step {
|
||||
background: var(--error-color);
|
||||
color: white;
|
||||
border: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.creator-actions {
|
||||
margin-top: 30px;
|
||||
padding-top: 30px;
|
||||
border-top: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.generation-options {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.generation-options label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.generation-options input, .generation-options select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: var(--neutral-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.success {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.creator-examples {
|
||||
margin-top: 25px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.example-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.example-btn {
|
||||
background: white;
|
||||
border: 2px solid var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.example-btn:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.creator-preview {
|
||||
margin-top: 25px;
|
||||
padding: 20px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.preview-summary h5 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.preview-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.stat {
|
||||
background: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.preview-items {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 8px 12px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-type {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-interaction {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-more {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-creator-interface {
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.creator-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.generation-options {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vocab-entry, .dialogue-line, .sequence-step {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.preview-stats {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
// Ajouter les styles
|
||||
document.head.insertAdjacentHTML('beforeend', contentCreatorStyles);
|
||||
|
||||
// Export global
|
||||
window.ContentCreator = ContentCreator;
|
||||
Loading…
Reference in New Issue
Block a user