Local changes before pull

This commit is contained in:
StillHammer 2025-09-15 14:43:03 +08:00
commit 7d085243c1
25 changed files with 12847 additions and 0 deletions

269
CLAUDE_local.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

394
css/main.css Normal file
View 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
View 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
View 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>

View 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
View 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
View 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;

View 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
View 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
View 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
View 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
View 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
View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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
View 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
View 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;