From 38920cc858cb89baaadef6f268fde2a3132dc813 Mon Sep 17 00:00:00 2001 From: StillHammer Date: Mon, 22 Sep 2025 07:08:39 +0800 Subject: [PATCH] Complete architectural rewrite with ultra-modular system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major Changes: - Moved legacy system to Legacy/ folder for archival - Built new modular architecture with strict separation of concerns - Created core system: Module, EventBus, ModuleLoader, Router - Added Application bootstrap with auto-start functionality - Implemented development server with ES6 modules support - Created comprehensive documentation and project context - Converted SBS-7-8 content to JSON format - Copied all legacy games and content to new structure New Architecture Features: - Sealed modules with WeakMap private data - Strict dependency injection system - Event-driven communication only - Inviolable responsibility patterns - Auto-initialization without commands - Component-based UI foundation ready Technical Stack: - Vanilla JS/HTML/CSS only - ES6 modules with proper imports/exports - HTTP development server (no file:// protocol) - Modular CSS with component scoping - Comprehensive error handling and debugging Ready for Phase 2: Converting legacy modules to new architecture ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 1066 +++------- Legacy/CLAUDE.md | 804 +++++++ TODO.md => Legacy/TODO.md | 0 {config => Legacy/config}/games-config.json | 0 {css => Legacy/css}/games.css | 0 {css => Legacy/css}/main.css | 0 {css => Legacy/css}/navigation.css | 0 {css => Legacy/css}/settings.css | 0 .../export_logger}/EXPORT_INFO.md | 0 .../export_logger}/ErrorReporting.js | 0 .../export_logger}/README.md | 0 .../export_logger}/demo.js | 0 .../export_logger}/log-server.cjs | 0 .../export_logger}/logs-viewer.html | 0 .../export_logger}/logviewer.cjs | 0 .../export_logger}/package-lock.json | 0 .../export_logger}/package.json | 0 .../export_logger}/test-do-fetch.js | 0 .../export_logger}/trace-wrap.js | 0 .../export_logger}/trace.js | 0 .../export_logger}/websocket-server.js | 0 Legacy/index.html | 441 ++++ {js => Legacy/js}/content/NCE1-Lesson63-64.js | 0 {js => Legacy/js}/content/NCE2-Lesson3.js | 0 {js => Legacy/js}/content/NCE2-Lesson30.js | 0 {js => Legacy/js}/content/SBS-level-1.js | 0 .../js}/content/WTA1B1-documented.js | 0 {js => Legacy/js}/content/WTA1B1.js | 0 .../js}/content/chinese-long-story.js | 0 {js => Legacy/js}/content/example-minimal.js | 0 .../js}/content/example-with-images.js | 0 .../js}/content/french-beginner-story.js | 0 .../js}/content/grammar-lesson-le.js | 0 .../js}/content/sbs-level-7-8-new.js | 0 .../js}/content/story-prototype-optimized.js | 0 {js => Legacy/js}/core/browser-logger.js | 0 {js => Legacy/js}/core/content-engine.js | 0 {js => Legacy/js}/core/content-factory.js | 0 {js => Legacy/js}/core/content-generators.js | 0 {js => Legacy/js}/core/content-parsers.js | 0 {js => Legacy/js}/core/content-scanner.js | 0 {js => Legacy/js}/core/env-config.js | 0 {js => Legacy/js}/core/game-loader.js | 0 {js => Legacy/js}/core/json-content-loader.js | 0 {js => Legacy/js}/core/navigation.js | 0 {js => Legacy/js}/core/settings-manager.js | 0 {js => Legacy/js}/core/simple-logger.js | 0 {js => Legacy/js}/core/test-logger.js | 0 {js => Legacy/js}/core/utils.js | 0 {js => Legacy/js}/core/websocket-logger.js | 0 {js => Legacy/js}/games/adventure-reader.js | 0 {js => Legacy/js}/games/chinese-study.js | 0 {js => Legacy/js}/games/fill-the-blank.js | 0 {js => Legacy/js}/games/grammar-discovery.js | 0 {js => Legacy/js}/games/letter-discovery.js | 0 {js => Legacy/js}/games/memory-match.js | 0 {js => Legacy/js}/games/quiz-game.js | 0 {js => Legacy/js}/games/river-run.js | 0 {js => Legacy/js}/games/story-builder.js | 0 {js => Legacy/js}/games/story-reader.js | 0 {js => Legacy/js}/games/whack-a-mole-hard.js | 0 {js => Legacy/js}/games/whack-a-mole.js | 0 .../js}/games/wizard-spell-caster.js | 0 {js => Legacy/js}/games/word-discovery.js | 0 {js => Legacy/js}/games/word-storm.js | 0 {js => Legacy/js}/tools/content-creator.js | 0 .../js}/tools/ultra-modular-validator.js | 0 {tests => Legacy/tests}/README.md | 0 .../tests}/fixtures/content-samples.js | 0 .../tests}/fixtures/edge-case-data.js | 0 .../integration/content-loading-flow.test.js | 0 .../integration/navigation-system.test.js | 0 .../integration/proxy-digitalocean.test.js | 0 .../tests}/integration/stress-tests.test.js | 0 {tests => Legacy/tests}/package.json | 0 {tests => Legacy/tests}/run-tests.js | 0 .../tests}/unit/basic-edge-cases.test.js | 0 .../tests}/unit/content-scanner.test.js | 0 .../tests}/unit/edge-cases-simple.test.js | 0 .../tests}/unit/edge-cases.test.js | 0 .../tests}/unit/env-config.test.js | 0 .../tests}/unit/game-loader.test.js | 0 {tests => Legacy/tests}/utils/test-helpers.js | 0 README.md | 204 ++ index.html | 568 ++--- package.json | 24 + server.js | 197 ++ src/Application.js | 260 +++ src/content/NCE1-Lesson63-64.js | 850 ++++++++ src/content/NCE2-Lesson3.js | 1134 ++++++++++ src/content/NCE2-Lesson30.js | 785 +++++++ src/content/SBS-level-1.js | 479 +++++ src/content/WTA1B1-documented.js | 1252 +++++++++++ src/content/WTA1B1.js | 1188 +++++++++++ src/content/chinese-long-story.js | 575 +++++ src/content/example-minimal.js | 59 + src/content/example-with-images.js | 81 + src/content/french-beginner-story.js | 524 +++++ src/content/grammar-lesson-le.js | 293 +++ src/content/sbs-level-7-8-new.js | 168 ++ src/content/sbs-level-7-8.json | 141 ++ src/content/story-prototype-optimized.js | 325 +++ src/core/EventBus.js | 215 ++ src/core/Module.js | 104 + src/core/ModuleLoader.js | 273 +++ src/core/Router.js | 317 +++ src/core/index.js | 16 + src/games/adventure-reader.js | 1287 +++++++++++ src/games/chinese-study.js | 1585 ++++++++++++++ src/games/fill-the-blank.js | 569 +++++ src/games/grammar-discovery.js | 1185 +++++++++++ src/games/letter-discovery.js | 781 +++++++ src/games/memory-match.js | 495 +++++ src/games/quiz-game.js | 529 +++++ src/games/river-run.js | 1001 +++++++++ src/games/story-builder.js | 979 +++++++++ src/games/story-reader.js | 1366 ++++++++++++ src/games/whack-a-mole-hard.js | 703 ++++++ src/games/whack-a-mole.js | 685 ++++++ src/games/wizard-spell-caster.js | 1881 +++++++++++++++++ src/games/word-discovery.js | 1046 +++++++++ src/games/word-storm.js | 656 ++++++ src/styles/base.css | 328 +++ src/styles/components.css | 441 ++++ start.bat | 27 + 125 files changed, 26676 insertions(+), 1211 deletions(-) create mode 100644 Legacy/CLAUDE.md rename TODO.md => Legacy/TODO.md (100%) rename {config => Legacy/config}/games-config.json (100%) rename {css => Legacy/css}/games.css (100%) rename {css => Legacy/css}/main.css (100%) rename {css => Legacy/css}/navigation.css (100%) rename {css => Legacy/css}/settings.css (100%) rename {export_logger => Legacy/export_logger}/EXPORT_INFO.md (100%) rename {export_logger => Legacy/export_logger}/ErrorReporting.js (100%) rename {export_logger => Legacy/export_logger}/README.md (100%) rename {export_logger => Legacy/export_logger}/demo.js (100%) rename {export_logger => Legacy/export_logger}/log-server.cjs (100%) rename {export_logger => Legacy/export_logger}/logs-viewer.html (100%) rename {export_logger => Legacy/export_logger}/logviewer.cjs (100%) rename {export_logger => Legacy/export_logger}/package-lock.json (100%) rename {export_logger => Legacy/export_logger}/package.json (100%) rename {export_logger => Legacy/export_logger}/test-do-fetch.js (100%) rename {export_logger => Legacy/export_logger}/trace-wrap.js (100%) rename {export_logger => Legacy/export_logger}/trace.js (100%) rename {export_logger => Legacy/export_logger}/websocket-server.js (100%) create mode 100644 Legacy/index.html rename {js => Legacy/js}/content/NCE1-Lesson63-64.js (100%) rename {js => Legacy/js}/content/NCE2-Lesson3.js (100%) rename {js => Legacy/js}/content/NCE2-Lesson30.js (100%) rename {js => Legacy/js}/content/SBS-level-1.js (100%) rename {js => Legacy/js}/content/WTA1B1-documented.js (100%) rename {js => Legacy/js}/content/WTA1B1.js (100%) rename {js => Legacy/js}/content/chinese-long-story.js (100%) rename {js => Legacy/js}/content/example-minimal.js (100%) rename {js => Legacy/js}/content/example-with-images.js (100%) rename {js => Legacy/js}/content/french-beginner-story.js (100%) rename {js => Legacy/js}/content/grammar-lesson-le.js (100%) rename {js => Legacy/js}/content/sbs-level-7-8-new.js (100%) rename {js => Legacy/js}/content/story-prototype-optimized.js (100%) rename {js => Legacy/js}/core/browser-logger.js (100%) rename {js => Legacy/js}/core/content-engine.js (100%) rename {js => Legacy/js}/core/content-factory.js (100%) rename {js => Legacy/js}/core/content-generators.js (100%) rename {js => Legacy/js}/core/content-parsers.js (100%) rename {js => Legacy/js}/core/content-scanner.js (100%) rename {js => Legacy/js}/core/env-config.js (100%) rename {js => Legacy/js}/core/game-loader.js (100%) rename {js => Legacy/js}/core/json-content-loader.js (100%) rename {js => Legacy/js}/core/navigation.js (100%) rename {js => Legacy/js}/core/settings-manager.js (100%) rename {js => Legacy/js}/core/simple-logger.js (100%) rename {js => Legacy/js}/core/test-logger.js (100%) rename {js => Legacy/js}/core/utils.js (100%) rename {js => Legacy/js}/core/websocket-logger.js (100%) rename {js => Legacy/js}/games/adventure-reader.js (100%) rename {js => Legacy/js}/games/chinese-study.js (100%) rename {js => Legacy/js}/games/fill-the-blank.js (100%) rename {js => Legacy/js}/games/grammar-discovery.js (100%) rename {js => Legacy/js}/games/letter-discovery.js (100%) rename {js => Legacy/js}/games/memory-match.js (100%) rename {js => Legacy/js}/games/quiz-game.js (100%) rename {js => Legacy/js}/games/river-run.js (100%) rename {js => Legacy/js}/games/story-builder.js (100%) rename {js => Legacy/js}/games/story-reader.js (100%) rename {js => Legacy/js}/games/whack-a-mole-hard.js (100%) rename {js => Legacy/js}/games/whack-a-mole.js (100%) rename {js => Legacy/js}/games/wizard-spell-caster.js (100%) rename {js => Legacy/js}/games/word-discovery.js (100%) rename {js => Legacy/js}/games/word-storm.js (100%) rename {js => Legacy/js}/tools/content-creator.js (100%) rename {js => Legacy/js}/tools/ultra-modular-validator.js (100%) rename {tests => Legacy/tests}/README.md (100%) rename {tests => Legacy/tests}/fixtures/content-samples.js (100%) rename {tests => Legacy/tests}/fixtures/edge-case-data.js (100%) rename {tests => Legacy/tests}/integration/content-loading-flow.test.js (100%) rename {tests => Legacy/tests}/integration/navigation-system.test.js (100%) rename {tests => Legacy/tests}/integration/proxy-digitalocean.test.js (100%) rename {tests => Legacy/tests}/integration/stress-tests.test.js (100%) rename {tests => Legacy/tests}/package.json (100%) rename {tests => Legacy/tests}/run-tests.js (100%) rename {tests => Legacy/tests}/unit/basic-edge-cases.test.js (100%) rename {tests => Legacy/tests}/unit/content-scanner.test.js (100%) rename {tests => Legacy/tests}/unit/edge-cases-simple.test.js (100%) rename {tests => Legacy/tests}/unit/edge-cases.test.js (100%) rename {tests => Legacy/tests}/unit/env-config.test.js (100%) rename {tests => Legacy/tests}/unit/game-loader.test.js (100%) rename {tests => Legacy/tests}/utils/test-helpers.js (100%) create mode 100644 README.md create mode 100644 package.json create mode 100644 server.js create mode 100644 src/Application.js create mode 100644 src/content/NCE1-Lesson63-64.js create mode 100644 src/content/NCE2-Lesson3.js create mode 100644 src/content/NCE2-Lesson30.js create mode 100644 src/content/SBS-level-1.js create mode 100644 src/content/WTA1B1-documented.js create mode 100644 src/content/WTA1B1.js create mode 100644 src/content/chinese-long-story.js create mode 100644 src/content/example-minimal.js create mode 100644 src/content/example-with-images.js create mode 100644 src/content/french-beginner-story.js create mode 100644 src/content/grammar-lesson-le.js create mode 100644 src/content/sbs-level-7-8-new.js create mode 100644 src/content/sbs-level-7-8.json create mode 100644 src/content/story-prototype-optimized.js create mode 100644 src/core/EventBus.js create mode 100644 src/core/Module.js create mode 100644 src/core/ModuleLoader.js create mode 100644 src/core/Router.js create mode 100644 src/core/index.js create mode 100644 src/games/adventure-reader.js create mode 100644 src/games/chinese-study.js create mode 100644 src/games/fill-the-blank.js create mode 100644 src/games/grammar-discovery.js create mode 100644 src/games/letter-discovery.js create mode 100644 src/games/memory-match.js create mode 100644 src/games/quiz-game.js create mode 100644 src/games/river-run.js create mode 100644 src/games/story-builder.js create mode 100644 src/games/story-reader.js create mode 100644 src/games/whack-a-mole-hard.js create mode 100644 src/games/whack-a-mole.js create mode 100644 src/games/wizard-spell-caster.js create mode 100644 src/games/word-discovery.js create mode 100644 src/games/word-storm.js create mode 100644 src/styles/base.css create mode 100644 src/styles/components.css create mode 100644 start.bat diff --git a/CLAUDE.md b/CLAUDE.md index e147b9a..661a524 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,804 +1,262 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## ๐ŸŽฏ IMPORTANT: Check TODO.md First! - -**ALWAYS check `TODO.md` for the current project tasks and priorities before making any changes.** - -The `TODO.md` file contains: -- ๐Ÿ”ฅ Current tasks in progress -- ๐Ÿ“‹ Pending features to implement -- ๐Ÿšจ Known issues and blockers -- โœ… Completed work for reference - -**Make sure to update TODO.md when:** -- Starting a new task -- Completing a task -- Discovering new issues -- Planning future improvements - -## Project Overview - -Interactive English learning platform for children (8-9 years old) built as a modular Single Page Application. The system provides 9 different educational games that work with various content modules through a flexible architecture. - -## Key Architecture Patterns - -### Core System Flow -1. **AppNavigation** (`js/core/navigation.js`) - Central SPA navigation controller -2. **ContentScanner** (`js/core/content-scanner.js`) - Auto-discovers available content modules -3. **GameLoader** (`js/core/game-loader.js`) - Dynamically loads game and content modules -4. **Content Engine** (`js/core/content-engine.js`) - Processes and adapts content for games - -### Module Loading System -- Games and content are loaded dynamically via `GameLoader.loadGame(gameType, contentType)` -- All modules register themselves on global objects: `window.GameModules` and `window.ContentModules` -- Content is discovered automatically by `ContentScanner` scanning `js/content/` directory -- **JSON Content Support**: New JSON-first architecture with backward compatibility to JS modules -- **JSON Content Loader**: `js/core/json-content-loader.js` transforms JSON content to legacy game format -- **Offline-First Loading**: Content loads from local files first, with DigitalOcean Spaces fallback -- Games follow consistent constructor pattern: `new GameClass({ container, content, onScoreUpdate, onGameEnd })` - -### URL-Based Navigation -- Single HTML file (`index.html`) handles all navigation via URL parameters -- Routes: `?page=home|games|levels|play&game=&content=` -- Browser back/forward supported through `popstate` events -- Navigation history maintained in `AppNavigation.navigationHistory` -- Breadcrumb navigation with clickable path elements -- **Top Bar**: Fixed header with app title and permanent network status indicator -- **Network Status**: Real-time connectivity indicator (๐ŸŸข Online / ๐ŸŸ  Connecting / ๐Ÿ”ด Offline) -- Keyboard shortcuts (ESC = go back) - -## Content Module Format - -### Rich Content Schema (New Architecture) - -Content modules support rich, multimedia educational content with optional properties. The system adapts games and exercises based on available content features: - -```javascript -window.ContentModules.ModuleName = { - name: "Display Name", - description: "Description text", - difficulty: "easy|medium|hard|beginner|intermediate|advanced", - language: "chinese|english|french|spanish", // Target learning language - hskLevel: "HSK1|HSK2|HSK3|HSK4|HSK5|HSK6", // Chinese proficiency level - - // Rich vocabulary with optional multimedia - vocabulary: { - "word_or_character": { - translation: "English translation", - prononciation: "pronunciation guide", // Optional: pronunciation guide - type: "noun|verb|adjective|greeting|number", // Word classification - pronunciation: "audio/word.mp3", // Optional: audio file - difficulty: "HSK1|HSK2|...", // Optional: individual word difficulty - strokeOrder: ["stroke1", "stroke2"], // Optional: character writing order - examples: ["example sentence 1"], // Optional: usage examples - grammarNotes: "special usage rules" // Optional: grammar context - } - // OR simple format for basic content: - // "word": "simple translation" - }, - - // Grammar rules and explanations - grammar: { - topic_name: { - title: "Grammar Rule Title", - explanation: "Detailed explanation", - examples: [ - { chinese: "ไธญๆ–‡ไพ‹ๅญ", english: "English example", prononciation: "zhลng wรฉn lรฌ zi" } - ], - exercises: [/* grammar-specific exercises */] - } - }, - - // Audio content with/without text - audio: { - withText: [ - { - title: "Audio Lesson Title", - audioFile: "audio/lesson1.mp3", - transcript: "Full text transcript", - translation: "English translation", - timestamps: [{ time: 5.2, text: "specific segment" }] // Optional - } - ], - withoutText: [ - { - title: "Listening Challenge", - audioFile: "audio/challenge1.mp3", - questions: [ - { question: "What did they say?", type: "ai_interpreted" } - ] - } - ] - }, - - // Poetry and cultural content - poems: [ - { - title: "Poem Title", - content: "Full poem text", - translation: "English translation", - audioFile: "audio/poem1.mp3", // Optional - culturalContext: "Historical background" - } - ], - - // Fill-in-the-blank exercises - fillInBlanks: [ - { - sentence: "I _____ to school every day", - options: ["go", "goes", "going", "went"], // Multiple choice options - correctAnswer: "go", - explanation: "Present tense with 'I'" - }, - { - sentence: "The weather is _____ today", - type: "open_ended", // AI-interpreted answers - acceptedAnswers: ["nice", "good", "beautiful", "sunny"], - aiPrompt: "Evaluate if answer describes weather positively" - } - ], - - // Sentence correction exercises - corrections: [ - { - incorrect: "I are happy today", - correct: "I am happy today", - explanation: "Use 'am' with pronoun 'I'", - type: "grammar_correction" - } - ], - - // Reading comprehension with AI evaluation - comprehension: [ - { - text: "Long reading passage...", - questions: [ - { - question: "What is the main idea?", - type: "ai_interpreted", - evaluationPrompt: "Check if answer captures main theme" - }, - { - question: "Multiple choice question?", - type: "multiple_choice", - options: ["A", "B", "C", "D"], - correctAnswer: "B" - } - ] - } - ], - - // Matching exercises (connect lines between columns) - matching: [ - { - title: "Match Words to Meanings", - leftColumn: ["apple", "book", "car"], - rightColumn: ["่‹นๆžœ", "ไนฆ", "่ฝฆ"], - correctPairs: [ - { left: "apple", right: "่‹นๆžœ" }, - { left: "book", right: "ไนฆ" }, - { left: "car", right: "่ฝฆ" } - ] - } - ], - - // Standard content (backward compatibility) - sentences: [{ english: "...", chinese: "...", prononciation: "..." }], - texts: [{ title: "...", content: "...", translation: "..." }], - dialogues: [{ conversation: [...] }] -}; -``` - -### JSON Content Format (New Architecture) - -The platform now supports JSON content files for easier editing and maintenance: - -```json -{ - "name": "Content Name", - "description": "Content description", - "difficulty": "easy|medium|hard", - "vocabulary": { - "word": { - "translation": "French translation", - "prononciation": "pronunciation guide", - "type": "noun|verb|adjective" - } - }, - "sentences": [ - { - "english": "English sentence", - "chinese": "Chinese translation", - "prononciation": "pronunciation" - } - ], - "grammar": { /* grammar rules */ }, - "audio": { /* audio content */ }, - "exercises": { /* exercise definitions */ } -} -``` - -**JSON Content Loader Features:** -- Automatic transformation from JSON to legacy game format -- Backward compatibility with existing JavaScript content modules -- Support for all rich content features (vocabulary, grammar, audio, exercises) -- Offline-first loading with cloud fallback - -### Content Adaptivity System - -The platform automatically adapts available games and exercises based on content richness: - -**Content Analysis:** -- System scans each content module for available features -- Generates compatibility scores for each game type -- Recommends optimal learning activities -- Handles graceful degradation when content is incomplete - -**Adaptive Game Selection:** -- **Rich vocabulary** โ†’ Enable advanced matching games, pronunciation practice -- **Audio files present** โ†’ Enable listening exercises, pronunciation challenges -- **Grammar rules** โ†’ Enable correction exercises, structured lessons -- **Fill-in-blanks data** โ†’ Enable cloze tests with multiple choice or AI evaluation -- **Minimal content** โ†’ Fall back to basic vocabulary games - -**Missing Content Handling:** -- Display helpful messages: "Add audio files to enable pronunciation practice" -- Suggest content enrichment opportunities -- Gracefully disable incompatible game modes -- Provide content creation tools for missing elements - -**Example Adaptive Behavior:** -```javascript -// Content with only basic vocabulary -{ vocabulary: { "hello": "ไฝ ๅฅฝ" } } -โ†’ Enable: Basic matching, simple quiz -โ†’ Disable: Audio practice, grammar exercises -โ†’ Suggest: "Add pronunciation guide and audio for pronunciation practice" - -// Rich multimedia content -{ vocabulary: { "hello": { translation: "ไฝ ๅฅฝ", prononciation: "nว hวŽo", pronunciation: "audio/hello.mp3" } } } -โ†’ Enable: All vocabulary games, audio practice, pronunciation scoring -โ†’ Unlock: Advanced difficulty levels, speed challenges -``` - -## ๐Ÿšจ CRITICAL ARCHITECTURE GUIDELINES - -**NEVER violate these principles to maintain system modularity and maintainability:** - -### ๐ŸŽจ CSS Architecture Rules - -1. **NEVER modify `css/games.css` for game-specific styles** - - Global CSS is only for shared, reusable components - - Game-specific styles MUST be injected by the game itself - -2. **USE the Global CSS Base System:** - ```javascript - // โœ… CORRECT: Use global classes with specific overrides -
// Global base class -
// Global HUD structure -
// Global game area -
// Global answer panel - - // โœ… CORRECT: Inject game-specific CSS - injectCSS() { - const styleSheet = document.createElement('style'); - styleSheet.textContent = ` - .my-game-specific-element { /* Game-only styles */ } - `; - document.head.appendChild(styleSheet); - } - ``` - -3. **CSS Classes Hierarchy:** - - `.game-wrapper` - Base container (full screen) - - `.game-wrapper.compact` - Smaller viewport variant - - `.game-hud` - Top information bar - - `.game-area` - Main play zone - - `.answer-panel` - Bottom interaction zone - - `.answer-btn` - Interactive buttons - -4. **โŒ FORBIDDEN PATTERNS:** - ```css - /* โŒ NEVER add game-specific classes to games.css */ - .word-storm-wrapper { } - .my-game-specific-class { } - - /* โŒ NEVER hardcode game-specific dimensions in global CSS */ - .game-area { height: 600px; } /* This breaks other games */ - ``` - -### ๐ŸŽฎ Game Development Standards - -1. **Self-Contained Games:** - - Each game MUST inject its own CSS via `injectCSS()` - - Games MUST use global base classes where possible - - Game-specific elements get their own CSS only - -2. **Constructor Pattern (REQUIRED):** - ```javascript - class MyGame { - constructor({ container, content, onScoreUpdate, onGameEnd }) { - this.injectCSS(); // Inject game-specific styles - this.init(); // Setup interface - } - - start() { /* Start game logic */ } - destroy() { /* Cleanup */ } - } - ``` - -3. **Module Registration (REQUIRED):** - ```javascript - // MUST be at end of game file - window.GameModules = window.GameModules || {}; - window.GameModules.MyGame = MyGame; - ``` - -4. **GameLoader Integration:** - - Add game mapping in `game-loader.js` โ†’ `getModuleName()` - - Add compatibility rules in `content-game-compatibility.js` - - Add game config in `navigation.js` โ†’ game configuration - -### ๐Ÿ”ง Modification Guidelines - -**When adding new games:** -1. โœ… Use existing global CSS classes -2. โœ… Inject only game-specific CSS -3. โœ… Follow constructor pattern -4. โœ… Add to GameLoader mapping - -**When modifying existing games:** -1. โœ… Keep changes within the game file -2. โœ… Don't break global CSS compatibility -3. โœ… Test with multiple content types - -**When adding global features:** -1. โœ… Add to global CSS base classes -2. โœ… Ensure backward compatibility -3. โœ… Update this documentation - -### ๐Ÿš€ Benefits of This Architecture - -- **Modularity**: Each game is self-contained -- **Reusability**: Base classes work for all games -- **Maintainability**: Changes don't break other games -- **Performance**: Only load CSS when game is used -- **Scalability**: Easy to add new games - -### โš ๏ธ Common Mistakes to Avoid - -**CSS Architecture Violations:** -```css -/* โŒ DON'T: Adding game-specific styles to games.css */ -.word-storm-wrapper { height: 80vh; width: 90vw; } - -/* โœ… DO: Use global classes with game-specific injection */ -// In game file: -injectCSS() { - // Game-specific overrides only - styleSheet.textContent = `.falling-word { animation: wordGlow 2s; }`; -} -``` - -**Module Loading Issues:** -```javascript -// โŒ DON'T: Forget GameLoader mapping -// Game won't load because getModuleName() doesn't know about it - -// โœ… DO: Add to game-loader.js -const names = { - 'my-game': 'MyGame' // Add this mapping -}; -``` - -**HTML Structure Violations:** -```html - -
- - -
-
-
-
-``` - -**Integration Checklist:** -- [ ] Game CSS injected via `injectCSS()` -- [ ] Global classes used for structure -- [ ] GameLoader mapping added -- [ ] Compatibility rules defined -- [ ] Constructor pattern followed -- [ ] Module registration at file end - -## Game Module Format - -Game modules must export to `window.GameModules` with this pattern: -```javascript -class GameName { - constructor({ container, content, onScoreUpdate, onGameEnd }) { - this.container = container; - this.content = content; - this.onScoreUpdate = onScoreUpdate; - this.onGameEnd = onGameEnd; - } - - start() { /* Initialize game */ } - destroy() { /* Cleanup */ } - restart() { /* Reset game state */ } -} - -window.GameModules = window.GameModules || {}; -window.GameModules.GameName = GameName; -``` - -## Configuration System - -- **Main config**: `config/games-config.json` - defines available games and content -- **Environment config**: `js/core/env-config.js` - DigitalOcean Spaces configuration and offline settings -- **Content discovery**: Automatic scanning of both `.js` and `.json` content files -- Games can be enabled/disabled via `games.{gameType}.enabled` -- **Cloud Integration**: DigitalOcean Spaces endpoint configuration for remote content -- **Offline-First Strategy**: Local content prioritized, remote fallback with timeout protection -- UI settings, scoring rules, and feature flags also in main config - -## Development Workflow - -### Running the Application -Open `index.html` in a web browser - no build process required. All modules load dynamically. - -### Adding New Games -1. Create `js/games/{game-name}.js` with proper module export -2. Add game configuration to `config/games-config.json` -3. Update `AppNavigation.getDefaultConfig()` if needed - -### Adding New Content -**Option 1: JSON Format (Recommended)** -1. Create `js/content/{content-name}.json` with proper JSON structure -2. Content will be auto-discovered and loaded via JSON Content Loader -3. Easier to edit and maintain than JavaScript files - -**Option 2: JavaScript Format (Legacy)** -1. Create `js/content/{content-name}.js` with proper module export -2. Add filename to `ContentScanner.contentFiles` array -3. Content will be auto-discovered on next app load - -### Content Creation Tool -- Built-in content creator at `js/tools/content-creator.js` -- Accessible via "Crรฉateur de Contenu" button on home page -- Generates properly formatted content modules - -## Key Files by Function - -**Navigation & Loading:** -- `js/core/navigation.js` - SPA navigation controller (452 lines) -- `js/core/game-loader.js` - Dynamic module loading (336 lines) -- `js/core/content-scanner.js` - Auto content discovery (376 lines) - -**Content Processing:** -- `js/core/content-engine.js` - Content processing engine (484 lines) -- `js/core/content-factory.js` - Exercise generation (553 lines) -- `js/core/content-parsers.js` - Content parsing utilities (484 lines) -- `js/core/json-content-loader.js` - JSON to legacy format transformation -- `js/core/env-config.js` - Environment and cloud configuration - -**Game Implementations:** -- `js/games/whack-a-mole.js` - Standard version (623 lines) -- `js/games/whack-a-mole-hard.js` - Difficult version (643 lines) -- `js/games/memory-match.js` - Memory pairs game (403 lines) -- `js/games/quiz-game.js` - Quiz system (354 lines) -- `js/games/fill-the-blank.js` - Sentence completion (418 lines) -- `js/games/text-reader.js` - Guided text reading (366 lines) -- `js/games/adventure-reader.js` - RPG-style adventure (949 lines) - -## Important Implementation Details - -### Scoring System -- Games call `this.onScoreUpdate(score)` to update display -- Final scores saved to localStorage with key pattern: `score_{gameType}_{contentType}` -- Best scores tracked and displayed in game-end modal -- Points per correct answer, malus per error, speed bonus -- Score history and achievement badges - -### Content Compatibility -- `ContentScanner` evaluates content compatibility with each game type -- Compatibility scoring helps recommend best content for each game -- Games should handle various content formats gracefully - -### Memory Management -- `GameLoader.cleanup()` called before loading new games -- Games should implement `destroy()` method for proper cleanup -- Previous game instances must be cleaned up to prevent memory leaks - -### Error Handling -- Content loading errors logged but don't crash the application -- Fallback mechanisms for missing content or games -- User-friendly error messages via `Utils.showToast()` - -## Design Guidelines - -### Visual Design Principles -- Modern, clean design optimized for children (8-9 years old) -- Large, tactile buttons (minimum 44px for touch interfaces) -- High contrast colors for accessibility -- Smooth, non-aggressive animations -- Emoji icons combined with text labels - -### Color Palette -- **Primary**: Blue (#3B82F6) - Trust, learning -- **Secondary**: Green (#10B981) - Success, validation -- **Accent**: Orange (#F59E0B) - Energy, attention -- **Error**: Red (#EF4444) - Clear error indication -- **Neutral**: Gray (#6B7280) - Text, backgrounds - -### Accessibility Features -- Full keyboard navigation support -- Alternative text for all images -- Adjustable font sizes -- High contrast mode compatibility -- Screen reader friendly markup - -### Responsive Design -- Mobile/tablet adaptation -- Touch-friendly interface -- Portrait/landscape orientation support -- Fluid layouts that work on various screen sizes -- **Fixed Top Bar**: App title and network status always visible -- **Network Status**: Automatic hiding of status text on mobile devices -- **Content Margin**: Proper spacing to accommodate fixed header - -## Git Configuration - -### Repository -- **Remote**: Bitbucket repository at `AlexisTrouve/class-generator-system` -- **Port 443 Configuration**: Git is configured to use SSH over port 443 for network restrictions -- **Remote URL**: `ssh://git@altssh.bitbucket.org:443/AlexisTrouve/class-generator-system.git` - -### SSH Configuration -To push to the repository through port 443, the following SSH configuration is required in `~/.ssh/config`: -``` -Host altssh.bitbucket.org - HostName altssh.bitbucket.org - Port 443 - User git - IdentityFile ~/.ssh/bitbucket_key -``` - -### Push Commands -- Standard push: `git push` -- Set upstream: `git push --set-upstream origin master` - -## ๐Ÿšจ **Developer Guidelines & Common Pitfalls** - -**Critical information for future developers to avoid common mistakes and maintain code quality.** - -### **๐Ÿ”ฅ Template Literals Syntax Errors** - -**MOST COMMON BUG - Always check this first:** - -```javascript -// โŒ FATAL ERROR - Will break entire module -styleSheet.textContent = \`css here\`; // Backslash = SyntaxError - -// โœ… CORRECT -styleSheet.textContent = `css here`; // Backtick (grave accent) -``` - -**How to debug:** -```bash -# Test syntax before browser testing -node -c js/games/your-game.js - -# Look for "Invalid or unexpected token" errors -# Usually points to template literal issues -``` - -### **๐ŸŽฎ Game Development Best Practices** - -#### **Required Game Structure:** -```javascript -class NewGame { - constructor({ container, content, onScoreUpdate, onGameEnd }) { - this.injectCSS(); // CSS injection FIRST - this.extractContent(); // Content processing - this.init(); // UI initialization - } - - start() { - // Separate start method - NOT in constructor - this.startGameLogic(); - } - - destroy() { - // Cleanup intervals, event listeners, injected CSS - this.cleanup(); - } -} - -// REQUIRED: Global module registration -window.GameModules = window.GameModules || {}; -window.GameModules.NewGame = NewGame; -``` - -#### **CSS Architecture - Zero Tolerance Policy:** - -```javascript -// โœ… CORRECT: Inject game-specific CSS -injectCSS() { - if (document.getElementById('my-game-styles')) return; // Prevent duplicates - - const styleSheet = document.createElement('style'); - styleSheet.id = 'my-game-styles'; - styleSheet.textContent = ` - .my-game-element { color: red; } - .falling-word { animation: myCustomAnimation 2s; } - `; - document.head.appendChild(styleSheet); -} - -// โŒ FORBIDDEN: Modifying css/games.css for game-specific styles -// css/games.css should ONLY contain global reusable classes -``` - -### **๐Ÿ” Debug Templates for Quick Testing** - -#### **Isolated Game Testing:** -```html - - - - -
- - - - - - - - - -``` - -### **๐ŸŒ Interface Language Standards** - -**All UI text must be in English:** - -```javascript -// โœ… CORRECT -"Score: ${score}" -"Lives: ${lives}" -"Level Up!" -"Game Over" -"Back to Games" - -// โŒ FORBIDDEN -"Score: ${score} points" // French -"Vies: ${lives}" // French -"Niveau supรฉrieur!" // French -``` - -### **๐Ÿ“‹ Integration Checklist** - -**Before committing any new game:** - -- [ ] โœ… Syntax check: `node -c js/games/your-game.js` -- [ ] โœ… CSS injected via `injectCSS()` method -- [ ] โœ… No modifications to `css/games.css` -- [ ] โœ… Uses global classes: `.game-wrapper`, `.game-hud`, `.game-area`, `.answer-panel` -- [ ] โœ… GameLoader mapping added in `getModuleName()` -- [ ] โœ… Navigation config updated in `navigation.js` -- [ ] โœ… Constructor pattern followed exactly -- [ ] โœ… Module export at end of file -- [ ] โœ… English-only interface text -- [ ] โœ… Isolated test file created and working - -### **โšก Performance & Architecture Notes** - -**Current System Architecture:** -- **CSS:** Global base classes + per-game injection -- **Content:** Auto-discovery + JSON/JS dual support -- **Navigation:** URL-based SPA with dynamic loading -- **Modules:** Dynamic import + backward compatibility - -**Critical Files (DO NOT BREAK):** -- `js/core/game-loader.js` - Module name mapping -- `css/games.css` - Global CSS base (game-agnostic only) -- `js/core/navigation.js` - Game configuration and routing - -### **๐Ÿ”ง Common Debugging Commands** - -```bash -# Check file syntax -node -c js/games/your-game.js - -# Check for non-ASCII characters (encoding issues) -grep -P '[^\x00-\x7F]' js/games/your-game.js - -# Find template literal issues -grep -n "\\\`" js/games/your-game.js - -# Test local server -python3 -m http.server 8000 -# Then: http://localhost:8000/?page=play&game=your-game&content=available-content -``` - -### **๐Ÿš€ Quick Win Tips** - -1. **Copy working game structure** - Use existing games as templates -2. **Test isolated first** - Don't debug through the full app initially -3. **Check browser console** - JavaScript errors are usually obvious -4. **Verify content compatibility** - Make sure your content has the data your game needs -5. **Use global CSS classes** - Don't reinvent layout, build on existing structure - -**Remember: Most bugs are simple syntax errors (especially template literals) or missing module registrations. Check these first!** ๐ŸŽฏ - -## ๐Ÿค **Collaborative Development Best Practices** - -**Critical lesson learned from real debugging sessions:** - -### **โœ… Always Test Before Committing** - -**โŒ BAD WORKFLOW:** -1. Write code -2. Immediately commit -3. Discover it doesn't work -4. Debug on committed broken code - -**โœ… GOOD WORKFLOW:** -1. Write code -2. **TEST THOROUGHLY** -3. If broken โ†’ debug cooperatively -4. When working โ†’ commit - -### **๐Ÿ” Cooperative Debugging Method** - -**When user reports: "รงa marche pas" or "y'a pas de lettres":** - -1. **Get specific symptoms** - Don't assume, ask exactly what they see -2. **Add targeted debug logs** - Console.log the exact variables in question -3. **Test together** - Have user run and report console output -4. **Analyze together** - Look at debug output to find root cause -5. **Fix precisely** - Target the exact issue, don't rewrite everything - -**Real Example - Letter Discovery Issue:** -```javascript -// โŒ ASSUMPTION: "Letters not working, must rewrite everything" - -// โœ… ACTUAL DEBUG: -console.log('๐Ÿ” DEBUG this.content.letters:', this.content.letters); // undefined -console.log('๐Ÿ” DEBUG this.content.rawContent?.letters:', this.content.rawContent?.letters); // {U: Array(4), V: Array(4), T: Array(4)} - -// โœ… PRECISE FIX: Check both locations -const letters = this.content.letters || this.content.rawContent?.letters; -``` - -### **๐ŸŽฏ Key Principles** - -- **Communication > Code** - Clear problem description saves hours -- **Debug logs > Assumptions** - Add console.log to see actual data -- **Test early, test often** - Don't tunnel vision on untested code -- **Pair debugging** - Two brains spot issues faster than one -- **Patience > Speed** - Taking time to understand beats rushing broken fixes - -**"C'est mieux quand on prend notre temps en coop plutot que de tunnel vision !"** ๐ŸŽฏ \ No newline at end of file +# CLAUDE.md - Project Context & Instructions + +## ๐Ÿ“‹ Project Overview + +**Class Generator 2.0** - Complete rewrite of educational games platform with ultra-modular architecture. + +### ๐ŸŽฏ Current Mission +Building a **bulletproof modular system** with strict separation of concerns using vanilla JavaScript, HTML, and CSS. The architecture enforces inviolable responsibility patterns with sealed modules and dependency injection. + +### ๐Ÿ—๏ธ Architecture Status +**PHASE 1 COMPLETED โœ…** - Core foundation built with rigorous architectural patterns: + +- **Module.js** - Abstract base class with WeakMap privates and sealed instances +- **EventBus.js** - Strict event system with validation and module registration +- **ModuleLoader.js** - Dependency injection with proper initialization order +- **Router.js** - Navigation with guards, middleware, and state management +- **Application.js** - Auto-bootstrap system with lifecycle management +- **Development Server** - HTTP server with ES6 modules and CORS support + +## ๐Ÿ”ฅ Critical Requirements + +### Architecture Principles (NON-NEGOTIABLE) +1. **Inviolable Responsibility** - Each module has exactly one purpose +2. **Zero Direct Dependencies** - All communication via EventBus only +3. **Sealed Instances** - Modules cannot be modified after creation +4. **Private State** - Internal data completely inaccessible via WeakMap +5. **Contract Enforcement** - Abstract methods must be implemented +6. **Dependency Injection** - No globals, everything injected through constructor + +### Technical Constraints +- **Vanilla JS/HTML/CSS only** - No frameworks +- **ES6 Modules** - Import/export syntax required +- **HTTP Protocol** - Never file:// (use development server) +- **Modular CSS** - Component-scoped styling +- **Event-Driven** - No direct module coupling + +## ๐Ÿš€ Development Workflow + +### Starting the System +```bash +# Option 1: Windows batch file +start.bat + +# Option 2: Node.js directly +node server.js + +# Option 3: NPM scripts +npm start +``` + +**Access:** http://localhost:3000 + +### Development Server Features +- โœ… ES6 modules support +- โœ… CORS enabled for online communication +- โœ… Proper MIME types for all file formats +- โœ… Development-friendly caching (assets cached, HTML not cached) +- โœ… Graceful error handling with styled 404 pages + +## ๐Ÿ“ Project Structure + +``` +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ core/ # COMPLETED - Core system (sealed) +โ”‚ โ”‚ โ”œโ”€โ”€ Module.js # Abstract base class +โ”‚ โ”‚ โ”œโ”€โ”€ EventBus.js # Event communication system +โ”‚ โ”‚ โ”œโ”€โ”€ ModuleLoader.js # Dependency injection +โ”‚ โ”‚ โ”œโ”€โ”€ Router.js # Navigation system +โ”‚ โ”‚ โ””โ”€โ”€ index.js # Core exports +โ”‚ โ”œโ”€โ”€ components/ # TODO - UI components +โ”‚ โ”œโ”€โ”€ games/ # TODO - Game modules +โ”‚ โ”œโ”€โ”€ content/ # TODO - Content system +โ”‚ โ”œโ”€โ”€ styles/ # COMPLETED - Modular CSS +โ”‚ โ”‚ โ”œโ”€โ”€ base.css # Foundation styles +โ”‚ โ”‚ โ””โ”€โ”€ components.css # Reusable UI components +โ”‚ โ””โ”€โ”€ Application.js # COMPLETED - Bootstrap system +โ”œโ”€โ”€ Legacy/ # Archived old system +โ”œโ”€โ”€ index.html # COMPLETED - Entry point +โ”œโ”€โ”€ server.js # COMPLETED - Development server +โ”œโ”€โ”€ start.bat # COMPLETED - Quick start script +โ”œโ”€โ”€ package.json # COMPLETED - Node.js config +โ””โ”€โ”€ README.md # COMPLETED - Documentation +``` + +## ๐ŸŽฎ Creating New Modules + +### Game Module Template +```javascript +import Module from '../core/Module.js'; + +class GameName extends Module { + constructor(name, dependencies, config) { + super(name, ['eventBus']); // Declare dependencies + + // Validate dependencies + if (!dependencies.eventBus) { + throw new Error('GameName requires EventBus dependency'); + } + + this._eventBus = dependencies.eventBus; + this._config = config; + + Object.seal(this); // Prevent modification + } + + async init() { + this._validateNotDestroyed(); + + // Set up event listeners + this._eventBus.on('game:start', this._handleStart.bind(this), this.name); + + this._setInitialized(); + } + + async destroy() { + this._validateNotDestroyed(); + + // Cleanup logic here + + this._setDestroyed(); + } + + // Private methods + _handleStart(event) { + this._validateInitialized(); + // Game logic here + } +} + +export default GameName; +``` + +### Registration in Application.js +```javascript +modules: [ + { + name: 'gameName', + path: './games/GameName.js', + dependencies: ['eventBus'], + config: { difficulty: 'medium' } + } +] +``` + +## ๐Ÿ” Debugging & Monitoring + +### Debug Panel (F12 to toggle) +- System status and uptime +- Loaded modules list +- Event history +- Module registration status + +### Console Access +```javascript +window.app.getStatus() // Application status +window.app.getCore().eventBus // EventBus instance +window.app.getCore().moduleLoader // ModuleLoader instance +window.app.getCore().router // Router instance +``` + +### Common Commands +```bash +# Check module status +window.app.getCore().moduleLoader.getStatus() + +# View event history +window.app.getCore().eventBus.getEventHistory() + +# Navigate programmatically +window.app.getCore().router.navigate('/games') +``` + +## ๐Ÿšง Next Development Phase + +### Immediate Tasks (PHASE 2) +1. **Component-based UI System** - Reusable UI components with scoped CSS +2. **Example Game Module** - Simple memory game to validate architecture +3. **Content System Integration** - Port content loading from Legacy +4. **Testing Framework** - Validate module contracts and event flow + +### Known Legacy Issues to Fix +31 bug fixes and improvements from the old system: +- Grammar game functionality issues +- Word Storm duration and difficulty problems +- Memory card display issues +- Adventure game text repetition +- UI alignment and feedback issues +- Performance optimizations needed + +## ๐Ÿ”’ Security & Rigidity Enforcement + +### Module Protection Layers +1. **Object.seal()** - Prevents property addition/deletion +2. **Object.freeze()** - Prevents prototype modification +3. **WeakMap privates** - Internal state completely hidden +4. **Abstract enforcement** - Missing methods throw errors +5. **Validation at boundaries** - All inputs validated + +### Error Messages +The system provides explicit error messages for violations: +- "Module is abstract and cannot be instantiated directly" +- "Module name is required and must be a string" +- "EventBus requires module registration before use" +- "Module must be initialized before use" + +## ๐Ÿ“ Development Guidelines + +### DO's +- โœ… Always extend Module base class for game modules +- โœ… Use EventBus for all inter-module communication +- โœ… Validate dependencies in constructor +- โœ… Call `_setInitialized()` after successful init +- โœ… Use private methods with underscore prefix +- โœ… Seal objects to prevent modification + +### DON'Ts +- โŒ Never access another module's internals directly +- โŒ Never use global variables for communication +- โŒ Never modify Module base class or core system +- โŒ Never skip dependency validation +- โŒ Never use file:// protocol (always use HTTP server) + +## ๐ŸŽฏ Success Metrics + +### Architecture Quality +- **Zero direct coupling** between modules +- **100% sealed instances** - no external modification possible +- **Complete test coverage** of module contracts +- **Event-driven communication** only + +### Performance Targets +- **<100ms** module loading time +- **<50ms** event propagation time +- **<200ms** application startup time +- **Zero memory leaks** in module lifecycle + +## ๐Ÿ”„ Migration Notes + +### From Legacy System +The `Legacy/` folder contains the complete old system. Key architectural changes: + +**Old Approach:** +- Global variables and direct coupling +- Manual module registration +- CSS modifications in global files +- Mixed responsibilities in single files + +**New Approach:** +- Strict modules with dependency injection +- Automatic loading with dependency resolution +- Component-scoped CSS injection +- Single responsibility per module + +### Data Migration +- Content modules need adaptation to new Module base class +- Game logic needs EventBus integration +- CSS needs component scoping +- Configuration needs dependency declaration + +--- + +**This is a high-quality, maintainable system built for educational software that will scale.** \ No newline at end of file diff --git a/Legacy/CLAUDE.md b/Legacy/CLAUDE.md new file mode 100644 index 0000000..e147b9a --- /dev/null +++ b/Legacy/CLAUDE.md @@ -0,0 +1,804 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## ๐ŸŽฏ IMPORTANT: Check TODO.md First! + +**ALWAYS check `TODO.md` for the current project tasks and priorities before making any changes.** + +The `TODO.md` file contains: +- ๐Ÿ”ฅ Current tasks in progress +- ๐Ÿ“‹ Pending features to implement +- ๐Ÿšจ Known issues and blockers +- โœ… Completed work for reference + +**Make sure to update TODO.md when:** +- Starting a new task +- Completing a task +- Discovering new issues +- Planning future improvements + +## Project Overview + +Interactive English learning platform for children (8-9 years old) built as a modular Single Page Application. The system provides 9 different educational games that work with various content modules through a flexible architecture. + +## Key Architecture Patterns + +### Core System Flow +1. **AppNavigation** (`js/core/navigation.js`) - Central SPA navigation controller +2. **ContentScanner** (`js/core/content-scanner.js`) - Auto-discovers available content modules +3. **GameLoader** (`js/core/game-loader.js`) - Dynamically loads game and content modules +4. **Content Engine** (`js/core/content-engine.js`) - Processes and adapts content for games + +### Module Loading System +- Games and content are loaded dynamically via `GameLoader.loadGame(gameType, contentType)` +- All modules register themselves on global objects: `window.GameModules` and `window.ContentModules` +- Content is discovered automatically by `ContentScanner` scanning `js/content/` directory +- **JSON Content Support**: New JSON-first architecture with backward compatibility to JS modules +- **JSON Content Loader**: `js/core/json-content-loader.js` transforms JSON content to legacy game format +- **Offline-First Loading**: Content loads from local files first, with DigitalOcean Spaces fallback +- Games follow consistent constructor pattern: `new GameClass({ container, content, onScoreUpdate, onGameEnd })` + +### URL-Based Navigation +- Single HTML file (`index.html`) handles all navigation via URL parameters +- Routes: `?page=home|games|levels|play&game=&content=` +- Browser back/forward supported through `popstate` events +- Navigation history maintained in `AppNavigation.navigationHistory` +- Breadcrumb navigation with clickable path elements +- **Top Bar**: Fixed header with app title and permanent network status indicator +- **Network Status**: Real-time connectivity indicator (๐ŸŸข Online / ๐ŸŸ  Connecting / ๐Ÿ”ด Offline) +- Keyboard shortcuts (ESC = go back) + +## Content Module Format + +### Rich Content Schema (New Architecture) + +Content modules support rich, multimedia educational content with optional properties. The system adapts games and exercises based on available content features: + +```javascript +window.ContentModules.ModuleName = { + name: "Display Name", + description: "Description text", + difficulty: "easy|medium|hard|beginner|intermediate|advanced", + language: "chinese|english|french|spanish", // Target learning language + hskLevel: "HSK1|HSK2|HSK3|HSK4|HSK5|HSK6", // Chinese proficiency level + + // Rich vocabulary with optional multimedia + vocabulary: { + "word_or_character": { + translation: "English translation", + prononciation: "pronunciation guide", // Optional: pronunciation guide + type: "noun|verb|adjective|greeting|number", // Word classification + pronunciation: "audio/word.mp3", // Optional: audio file + difficulty: "HSK1|HSK2|...", // Optional: individual word difficulty + strokeOrder: ["stroke1", "stroke2"], // Optional: character writing order + examples: ["example sentence 1"], // Optional: usage examples + grammarNotes: "special usage rules" // Optional: grammar context + } + // OR simple format for basic content: + // "word": "simple translation" + }, + + // Grammar rules and explanations + grammar: { + topic_name: { + title: "Grammar Rule Title", + explanation: "Detailed explanation", + examples: [ + { chinese: "ไธญๆ–‡ไพ‹ๅญ", english: "English example", prononciation: "zhลng wรฉn lรฌ zi" } + ], + exercises: [/* grammar-specific exercises */] + } + }, + + // Audio content with/without text + audio: { + withText: [ + { + title: "Audio Lesson Title", + audioFile: "audio/lesson1.mp3", + transcript: "Full text transcript", + translation: "English translation", + timestamps: [{ time: 5.2, text: "specific segment" }] // Optional + } + ], + withoutText: [ + { + title: "Listening Challenge", + audioFile: "audio/challenge1.mp3", + questions: [ + { question: "What did they say?", type: "ai_interpreted" } + ] + } + ] + }, + + // Poetry and cultural content + poems: [ + { + title: "Poem Title", + content: "Full poem text", + translation: "English translation", + audioFile: "audio/poem1.mp3", // Optional + culturalContext: "Historical background" + } + ], + + // Fill-in-the-blank exercises + fillInBlanks: [ + { + sentence: "I _____ to school every day", + options: ["go", "goes", "going", "went"], // Multiple choice options + correctAnswer: "go", + explanation: "Present tense with 'I'" + }, + { + sentence: "The weather is _____ today", + type: "open_ended", // AI-interpreted answers + acceptedAnswers: ["nice", "good", "beautiful", "sunny"], + aiPrompt: "Evaluate if answer describes weather positively" + } + ], + + // Sentence correction exercises + corrections: [ + { + incorrect: "I are happy today", + correct: "I am happy today", + explanation: "Use 'am' with pronoun 'I'", + type: "grammar_correction" + } + ], + + // Reading comprehension with AI evaluation + comprehension: [ + { + text: "Long reading passage...", + questions: [ + { + question: "What is the main idea?", + type: "ai_interpreted", + evaluationPrompt: "Check if answer captures main theme" + }, + { + question: "Multiple choice question?", + type: "multiple_choice", + options: ["A", "B", "C", "D"], + correctAnswer: "B" + } + ] + } + ], + + // Matching exercises (connect lines between columns) + matching: [ + { + title: "Match Words to Meanings", + leftColumn: ["apple", "book", "car"], + rightColumn: ["่‹นๆžœ", "ไนฆ", "่ฝฆ"], + correctPairs: [ + { left: "apple", right: "่‹นๆžœ" }, + { left: "book", right: "ไนฆ" }, + { left: "car", right: "่ฝฆ" } + ] + } + ], + + // Standard content (backward compatibility) + sentences: [{ english: "...", chinese: "...", prononciation: "..." }], + texts: [{ title: "...", content: "...", translation: "..." }], + dialogues: [{ conversation: [...] }] +}; +``` + +### JSON Content Format (New Architecture) + +The platform now supports JSON content files for easier editing and maintenance: + +```json +{ + "name": "Content Name", + "description": "Content description", + "difficulty": "easy|medium|hard", + "vocabulary": { + "word": { + "translation": "French translation", + "prononciation": "pronunciation guide", + "type": "noun|verb|adjective" + } + }, + "sentences": [ + { + "english": "English sentence", + "chinese": "Chinese translation", + "prononciation": "pronunciation" + } + ], + "grammar": { /* grammar rules */ }, + "audio": { /* audio content */ }, + "exercises": { /* exercise definitions */ } +} +``` + +**JSON Content Loader Features:** +- Automatic transformation from JSON to legacy game format +- Backward compatibility with existing JavaScript content modules +- Support for all rich content features (vocabulary, grammar, audio, exercises) +- Offline-first loading with cloud fallback + +### Content Adaptivity System + +The platform automatically adapts available games and exercises based on content richness: + +**Content Analysis:** +- System scans each content module for available features +- Generates compatibility scores for each game type +- Recommends optimal learning activities +- Handles graceful degradation when content is incomplete + +**Adaptive Game Selection:** +- **Rich vocabulary** โ†’ Enable advanced matching games, pronunciation practice +- **Audio files present** โ†’ Enable listening exercises, pronunciation challenges +- **Grammar rules** โ†’ Enable correction exercises, structured lessons +- **Fill-in-blanks data** โ†’ Enable cloze tests with multiple choice or AI evaluation +- **Minimal content** โ†’ Fall back to basic vocabulary games + +**Missing Content Handling:** +- Display helpful messages: "Add audio files to enable pronunciation practice" +- Suggest content enrichment opportunities +- Gracefully disable incompatible game modes +- Provide content creation tools for missing elements + +**Example Adaptive Behavior:** +```javascript +// Content with only basic vocabulary +{ vocabulary: { "hello": "ไฝ ๅฅฝ" } } +โ†’ Enable: Basic matching, simple quiz +โ†’ Disable: Audio practice, grammar exercises +โ†’ Suggest: "Add pronunciation guide and audio for pronunciation practice" + +// Rich multimedia content +{ vocabulary: { "hello": { translation: "ไฝ ๅฅฝ", prononciation: "nว hวŽo", pronunciation: "audio/hello.mp3" } } } +โ†’ Enable: All vocabulary games, audio practice, pronunciation scoring +โ†’ Unlock: Advanced difficulty levels, speed challenges +``` + +## ๐Ÿšจ CRITICAL ARCHITECTURE GUIDELINES + +**NEVER violate these principles to maintain system modularity and maintainability:** + +### ๐ŸŽจ CSS Architecture Rules + +1. **NEVER modify `css/games.css` for game-specific styles** + - Global CSS is only for shared, reusable components + - Game-specific styles MUST be injected by the game itself + +2. **USE the Global CSS Base System:** + ```javascript + // โœ… CORRECT: Use global classes with specific overrides +
// Global base class +
// Global HUD structure +
// Global game area +
// Global answer panel + + // โœ… CORRECT: Inject game-specific CSS + injectCSS() { + const styleSheet = document.createElement('style'); + styleSheet.textContent = ` + .my-game-specific-element { /* Game-only styles */ } + `; + document.head.appendChild(styleSheet); + } + ``` + +3. **CSS Classes Hierarchy:** + - `.game-wrapper` - Base container (full screen) + - `.game-wrapper.compact` - Smaller viewport variant + - `.game-hud` - Top information bar + - `.game-area` - Main play zone + - `.answer-panel` - Bottom interaction zone + - `.answer-btn` - Interactive buttons + +4. **โŒ FORBIDDEN PATTERNS:** + ```css + /* โŒ NEVER add game-specific classes to games.css */ + .word-storm-wrapper { } + .my-game-specific-class { } + + /* โŒ NEVER hardcode game-specific dimensions in global CSS */ + .game-area { height: 600px; } /* This breaks other games */ + ``` + +### ๐ŸŽฎ Game Development Standards + +1. **Self-Contained Games:** + - Each game MUST inject its own CSS via `injectCSS()` + - Games MUST use global base classes where possible + - Game-specific elements get their own CSS only + +2. **Constructor Pattern (REQUIRED):** + ```javascript + class MyGame { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.injectCSS(); // Inject game-specific styles + this.init(); // Setup interface + } + + start() { /* Start game logic */ } + destroy() { /* Cleanup */ } + } + ``` + +3. **Module Registration (REQUIRED):** + ```javascript + // MUST be at end of game file + window.GameModules = window.GameModules || {}; + window.GameModules.MyGame = MyGame; + ``` + +4. **GameLoader Integration:** + - Add game mapping in `game-loader.js` โ†’ `getModuleName()` + - Add compatibility rules in `content-game-compatibility.js` + - Add game config in `navigation.js` โ†’ game configuration + +### ๐Ÿ”ง Modification Guidelines + +**When adding new games:** +1. โœ… Use existing global CSS classes +2. โœ… Inject only game-specific CSS +3. โœ… Follow constructor pattern +4. โœ… Add to GameLoader mapping + +**When modifying existing games:** +1. โœ… Keep changes within the game file +2. โœ… Don't break global CSS compatibility +3. โœ… Test with multiple content types + +**When adding global features:** +1. โœ… Add to global CSS base classes +2. โœ… Ensure backward compatibility +3. โœ… Update this documentation + +### ๐Ÿš€ Benefits of This Architecture + +- **Modularity**: Each game is self-contained +- **Reusability**: Base classes work for all games +- **Maintainability**: Changes don't break other games +- **Performance**: Only load CSS when game is used +- **Scalability**: Easy to add new games + +### โš ๏ธ Common Mistakes to Avoid + +**CSS Architecture Violations:** +```css +/* โŒ DON'T: Adding game-specific styles to games.css */ +.word-storm-wrapper { height: 80vh; width: 90vw; } + +/* โœ… DO: Use global classes with game-specific injection */ +// In game file: +injectCSS() { + // Game-specific overrides only + styleSheet.textContent = `.falling-word { animation: wordGlow 2s; }`; +} +``` + +**Module Loading Issues:** +```javascript +// โŒ DON'T: Forget GameLoader mapping +// Game won't load because getModuleName() doesn't know about it + +// โœ… DO: Add to game-loader.js +const names = { + 'my-game': 'MyGame' // Add this mapping +}; +``` + +**HTML Structure Violations:** +```html + +
+ + +
+
+
+
+``` + +**Integration Checklist:** +- [ ] Game CSS injected via `injectCSS()` +- [ ] Global classes used for structure +- [ ] GameLoader mapping added +- [ ] Compatibility rules defined +- [ ] Constructor pattern followed +- [ ] Module registration at file end + +## Game Module Format + +Game modules must export to `window.GameModules` with this pattern: +```javascript +class GameName { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.container = container; + this.content = content; + this.onScoreUpdate = onScoreUpdate; + this.onGameEnd = onGameEnd; + } + + start() { /* Initialize game */ } + destroy() { /* Cleanup */ } + restart() { /* Reset game state */ } +} + +window.GameModules = window.GameModules || {}; +window.GameModules.GameName = GameName; +``` + +## Configuration System + +- **Main config**: `config/games-config.json` - defines available games and content +- **Environment config**: `js/core/env-config.js` - DigitalOcean Spaces configuration and offline settings +- **Content discovery**: Automatic scanning of both `.js` and `.json` content files +- Games can be enabled/disabled via `games.{gameType}.enabled` +- **Cloud Integration**: DigitalOcean Spaces endpoint configuration for remote content +- **Offline-First Strategy**: Local content prioritized, remote fallback with timeout protection +- UI settings, scoring rules, and feature flags also in main config + +## Development Workflow + +### Running the Application +Open `index.html` in a web browser - no build process required. All modules load dynamically. + +### Adding New Games +1. Create `js/games/{game-name}.js` with proper module export +2. Add game configuration to `config/games-config.json` +3. Update `AppNavigation.getDefaultConfig()` if needed + +### Adding New Content +**Option 1: JSON Format (Recommended)** +1. Create `js/content/{content-name}.json` with proper JSON structure +2. Content will be auto-discovered and loaded via JSON Content Loader +3. Easier to edit and maintain than JavaScript files + +**Option 2: JavaScript Format (Legacy)** +1. Create `js/content/{content-name}.js` with proper module export +2. Add filename to `ContentScanner.contentFiles` array +3. Content will be auto-discovered on next app load + +### Content Creation Tool +- Built-in content creator at `js/tools/content-creator.js` +- Accessible via "Crรฉateur de Contenu" button on home page +- Generates properly formatted content modules + +## Key Files by Function + +**Navigation & Loading:** +- `js/core/navigation.js` - SPA navigation controller (452 lines) +- `js/core/game-loader.js` - Dynamic module loading (336 lines) +- `js/core/content-scanner.js` - Auto content discovery (376 lines) + +**Content Processing:** +- `js/core/content-engine.js` - Content processing engine (484 lines) +- `js/core/content-factory.js` - Exercise generation (553 lines) +- `js/core/content-parsers.js` - Content parsing utilities (484 lines) +- `js/core/json-content-loader.js` - JSON to legacy format transformation +- `js/core/env-config.js` - Environment and cloud configuration + +**Game Implementations:** +- `js/games/whack-a-mole.js` - Standard version (623 lines) +- `js/games/whack-a-mole-hard.js` - Difficult version (643 lines) +- `js/games/memory-match.js` - Memory pairs game (403 lines) +- `js/games/quiz-game.js` - Quiz system (354 lines) +- `js/games/fill-the-blank.js` - Sentence completion (418 lines) +- `js/games/text-reader.js` - Guided text reading (366 lines) +- `js/games/adventure-reader.js` - RPG-style adventure (949 lines) + +## Important Implementation Details + +### Scoring System +- Games call `this.onScoreUpdate(score)` to update display +- Final scores saved to localStorage with key pattern: `score_{gameType}_{contentType}` +- Best scores tracked and displayed in game-end modal +- Points per correct answer, malus per error, speed bonus +- Score history and achievement badges + +### Content Compatibility +- `ContentScanner` evaluates content compatibility with each game type +- Compatibility scoring helps recommend best content for each game +- Games should handle various content formats gracefully + +### Memory Management +- `GameLoader.cleanup()` called before loading new games +- Games should implement `destroy()` method for proper cleanup +- Previous game instances must be cleaned up to prevent memory leaks + +### Error Handling +- Content loading errors logged but don't crash the application +- Fallback mechanisms for missing content or games +- User-friendly error messages via `Utils.showToast()` + +## Design Guidelines + +### Visual Design Principles +- Modern, clean design optimized for children (8-9 years old) +- Large, tactile buttons (minimum 44px for touch interfaces) +- High contrast colors for accessibility +- Smooth, non-aggressive animations +- Emoji icons combined with text labels + +### Color Palette +- **Primary**: Blue (#3B82F6) - Trust, learning +- **Secondary**: Green (#10B981) - Success, validation +- **Accent**: Orange (#F59E0B) - Energy, attention +- **Error**: Red (#EF4444) - Clear error indication +- **Neutral**: Gray (#6B7280) - Text, backgrounds + +### Accessibility Features +- Full keyboard navigation support +- Alternative text for all images +- Adjustable font sizes +- High contrast mode compatibility +- Screen reader friendly markup + +### Responsive Design +- Mobile/tablet adaptation +- Touch-friendly interface +- Portrait/landscape orientation support +- Fluid layouts that work on various screen sizes +- **Fixed Top Bar**: App title and network status always visible +- **Network Status**: Automatic hiding of status text on mobile devices +- **Content Margin**: Proper spacing to accommodate fixed header + +## Git Configuration + +### Repository +- **Remote**: Bitbucket repository at `AlexisTrouve/class-generator-system` +- **Port 443 Configuration**: Git is configured to use SSH over port 443 for network restrictions +- **Remote URL**: `ssh://git@altssh.bitbucket.org:443/AlexisTrouve/class-generator-system.git` + +### SSH Configuration +To push to the repository through port 443, the following SSH configuration is required in `~/.ssh/config`: +``` +Host altssh.bitbucket.org + HostName altssh.bitbucket.org + Port 443 + User git + IdentityFile ~/.ssh/bitbucket_key +``` + +### Push Commands +- Standard push: `git push` +- Set upstream: `git push --set-upstream origin master` + +## ๐Ÿšจ **Developer Guidelines & Common Pitfalls** + +**Critical information for future developers to avoid common mistakes and maintain code quality.** + +### **๐Ÿ”ฅ Template Literals Syntax Errors** + +**MOST COMMON BUG - Always check this first:** + +```javascript +// โŒ FATAL ERROR - Will break entire module +styleSheet.textContent = \`css here\`; // Backslash = SyntaxError + +// โœ… CORRECT +styleSheet.textContent = `css here`; // Backtick (grave accent) +``` + +**How to debug:** +```bash +# Test syntax before browser testing +node -c js/games/your-game.js + +# Look for "Invalid or unexpected token" errors +# Usually points to template literal issues +``` + +### **๐ŸŽฎ Game Development Best Practices** + +#### **Required Game Structure:** +```javascript +class NewGame { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.injectCSS(); // CSS injection FIRST + this.extractContent(); // Content processing + this.init(); // UI initialization + } + + start() { + // Separate start method - NOT in constructor + this.startGameLogic(); + } + + destroy() { + // Cleanup intervals, event listeners, injected CSS + this.cleanup(); + } +} + +// REQUIRED: Global module registration +window.GameModules = window.GameModules || {}; +window.GameModules.NewGame = NewGame; +``` + +#### **CSS Architecture - Zero Tolerance Policy:** + +```javascript +// โœ… CORRECT: Inject game-specific CSS +injectCSS() { + if (document.getElementById('my-game-styles')) return; // Prevent duplicates + + const styleSheet = document.createElement('style'); + styleSheet.id = 'my-game-styles'; + styleSheet.textContent = ` + .my-game-element { color: red; } + .falling-word { animation: myCustomAnimation 2s; } + `; + document.head.appendChild(styleSheet); +} + +// โŒ FORBIDDEN: Modifying css/games.css for game-specific styles +// css/games.css should ONLY contain global reusable classes +``` + +### **๐Ÿ” Debug Templates for Quick Testing** + +#### **Isolated Game Testing:** +```html + + + + +
+ + + + + + + + + +``` + +### **๐ŸŒ Interface Language Standards** + +**All UI text must be in English:** + +```javascript +// โœ… CORRECT +"Score: ${score}" +"Lives: ${lives}" +"Level Up!" +"Game Over" +"Back to Games" + +// โŒ FORBIDDEN +"Score: ${score} points" // French +"Vies: ${lives}" // French +"Niveau supรฉrieur!" // French +``` + +### **๐Ÿ“‹ Integration Checklist** + +**Before committing any new game:** + +- [ ] โœ… Syntax check: `node -c js/games/your-game.js` +- [ ] โœ… CSS injected via `injectCSS()` method +- [ ] โœ… No modifications to `css/games.css` +- [ ] โœ… Uses global classes: `.game-wrapper`, `.game-hud`, `.game-area`, `.answer-panel` +- [ ] โœ… GameLoader mapping added in `getModuleName()` +- [ ] โœ… Navigation config updated in `navigation.js` +- [ ] โœ… Constructor pattern followed exactly +- [ ] โœ… Module export at end of file +- [ ] โœ… English-only interface text +- [ ] โœ… Isolated test file created and working + +### **โšก Performance & Architecture Notes** + +**Current System Architecture:** +- **CSS:** Global base classes + per-game injection +- **Content:** Auto-discovery + JSON/JS dual support +- **Navigation:** URL-based SPA with dynamic loading +- **Modules:** Dynamic import + backward compatibility + +**Critical Files (DO NOT BREAK):** +- `js/core/game-loader.js` - Module name mapping +- `css/games.css` - Global CSS base (game-agnostic only) +- `js/core/navigation.js` - Game configuration and routing + +### **๐Ÿ”ง Common Debugging Commands** + +```bash +# Check file syntax +node -c js/games/your-game.js + +# Check for non-ASCII characters (encoding issues) +grep -P '[^\x00-\x7F]' js/games/your-game.js + +# Find template literal issues +grep -n "\\\`" js/games/your-game.js + +# Test local server +python3 -m http.server 8000 +# Then: http://localhost:8000/?page=play&game=your-game&content=available-content +``` + +### **๐Ÿš€ Quick Win Tips** + +1. **Copy working game structure** - Use existing games as templates +2. **Test isolated first** - Don't debug through the full app initially +3. **Check browser console** - JavaScript errors are usually obvious +4. **Verify content compatibility** - Make sure your content has the data your game needs +5. **Use global CSS classes** - Don't reinvent layout, build on existing structure + +**Remember: Most bugs are simple syntax errors (especially template literals) or missing module registrations. Check these first!** ๐ŸŽฏ + +## ๐Ÿค **Collaborative Development Best Practices** + +**Critical lesson learned from real debugging sessions:** + +### **โœ… Always Test Before Committing** + +**โŒ BAD WORKFLOW:** +1. Write code +2. Immediately commit +3. Discover it doesn't work +4. Debug on committed broken code + +**โœ… GOOD WORKFLOW:** +1. Write code +2. **TEST THOROUGHLY** +3. If broken โ†’ debug cooperatively +4. When working โ†’ commit + +### **๐Ÿ” Cooperative Debugging Method** + +**When user reports: "รงa marche pas" or "y'a pas de lettres":** + +1. **Get specific symptoms** - Don't assume, ask exactly what they see +2. **Add targeted debug logs** - Console.log the exact variables in question +3. **Test together** - Have user run and report console output +4. **Analyze together** - Look at debug output to find root cause +5. **Fix precisely** - Target the exact issue, don't rewrite everything + +**Real Example - Letter Discovery Issue:** +```javascript +// โŒ ASSUMPTION: "Letters not working, must rewrite everything" + +// โœ… ACTUAL DEBUG: +console.log('๐Ÿ” DEBUG this.content.letters:', this.content.letters); // undefined +console.log('๐Ÿ” DEBUG this.content.rawContent?.letters:', this.content.rawContent?.letters); // {U: Array(4), V: Array(4), T: Array(4)} + +// โœ… PRECISE FIX: Check both locations +const letters = this.content.letters || this.content.rawContent?.letters; +``` + +### **๐ŸŽฏ Key Principles** + +- **Communication > Code** - Clear problem description saves hours +- **Debug logs > Assumptions** - Add console.log to see actual data +- **Test early, test often** - Don't tunnel vision on untested code +- **Pair debugging** - Two brains spot issues faster than one +- **Patience > Speed** - Taking time to understand beats rushing broken fixes + +**"C'est mieux quand on prend notre temps en coop plutot que de tunnel vision !"** ๐ŸŽฏ \ No newline at end of file diff --git a/TODO.md b/Legacy/TODO.md similarity index 100% rename from TODO.md rename to Legacy/TODO.md diff --git a/config/games-config.json b/Legacy/config/games-config.json similarity index 100% rename from config/games-config.json rename to Legacy/config/games-config.json diff --git a/css/games.css b/Legacy/css/games.css similarity index 100% rename from css/games.css rename to Legacy/css/games.css diff --git a/css/main.css b/Legacy/css/main.css similarity index 100% rename from css/main.css rename to Legacy/css/main.css diff --git a/css/navigation.css b/Legacy/css/navigation.css similarity index 100% rename from css/navigation.css rename to Legacy/css/navigation.css diff --git a/css/settings.css b/Legacy/css/settings.css similarity index 100% rename from css/settings.css rename to Legacy/css/settings.css diff --git a/export_logger/EXPORT_INFO.md b/Legacy/export_logger/EXPORT_INFO.md similarity index 100% rename from export_logger/EXPORT_INFO.md rename to Legacy/export_logger/EXPORT_INFO.md diff --git a/export_logger/ErrorReporting.js b/Legacy/export_logger/ErrorReporting.js similarity index 100% rename from export_logger/ErrorReporting.js rename to Legacy/export_logger/ErrorReporting.js diff --git a/export_logger/README.md b/Legacy/export_logger/README.md similarity index 100% rename from export_logger/README.md rename to Legacy/export_logger/README.md diff --git a/export_logger/demo.js b/Legacy/export_logger/demo.js similarity index 100% rename from export_logger/demo.js rename to Legacy/export_logger/demo.js diff --git a/export_logger/log-server.cjs b/Legacy/export_logger/log-server.cjs similarity index 100% rename from export_logger/log-server.cjs rename to Legacy/export_logger/log-server.cjs diff --git a/export_logger/logs-viewer.html b/Legacy/export_logger/logs-viewer.html similarity index 100% rename from export_logger/logs-viewer.html rename to Legacy/export_logger/logs-viewer.html diff --git a/export_logger/logviewer.cjs b/Legacy/export_logger/logviewer.cjs similarity index 100% rename from export_logger/logviewer.cjs rename to Legacy/export_logger/logviewer.cjs diff --git a/export_logger/package-lock.json b/Legacy/export_logger/package-lock.json similarity index 100% rename from export_logger/package-lock.json rename to Legacy/export_logger/package-lock.json diff --git a/export_logger/package.json b/Legacy/export_logger/package.json similarity index 100% rename from export_logger/package.json rename to Legacy/export_logger/package.json diff --git a/export_logger/test-do-fetch.js b/Legacy/export_logger/test-do-fetch.js similarity index 100% rename from export_logger/test-do-fetch.js rename to Legacy/export_logger/test-do-fetch.js diff --git a/export_logger/trace-wrap.js b/Legacy/export_logger/trace-wrap.js similarity index 100% rename from export_logger/trace-wrap.js rename to Legacy/export_logger/trace-wrap.js diff --git a/export_logger/trace.js b/Legacy/export_logger/trace.js similarity index 100% rename from export_logger/trace.js rename to Legacy/export_logger/trace.js diff --git a/export_logger/websocket-server.js b/Legacy/export_logger/websocket-server.js similarity index 100% rename from export_logger/websocket-server.js rename to Legacy/export_logger/websocket-server.js diff --git a/Legacy/index.html b/Legacy/index.html new file mode 100644 index 0000000..bbaf672 --- /dev/null +++ b/Legacy/index.html @@ -0,0 +1,441 @@ + + + + + + Interactive English Class + + + + + + + +
+
+
๐ŸŽ“ Interactive English Class
+ +
+
+
+
+ Connecting... +
+ +
+
+ + +
+ + +
+ +
+

๐ŸŽ“ Interactive English Class

+

Learn English while having fun!

+
+ +
+ + + + +
+
+ + +
+ + +
+ +
+
+ + +
+ + +
+ +
+
+ + +
+
+ +

Game in progress...

+
+ Score: 0 +
+
+ +
+ +
+
+ + +
+ + +
+ +
+

๐Ÿ”Š Audio Settings

+ +
+ + + 0.8 +
+ +
+ + + 1.0 +
+ +
+ + +
+
+ + +
+

๐Ÿ”ง TTS Debug Tools

+ +
+
+ Browser Support: + Checking... +
+
+ Available Voices: + Loading... +
+
+ English Voices: + Loading... +
+
+ +
+ + + + +
+ +
+

Debug Output:

+
+ +
+
+ + +
+

๐ŸŽค Available Voices

+
+ Loading voices... +
+
+ + +
+

๐ŸŒ Browser Information

+
+
+ User Agent: + +
+
+ Platform: + +
+
+ Language: + +
+
+
+
+
+ +
+ + + + + +
+
+

Loading...

+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/content/NCE1-Lesson63-64.js b/Legacy/js/content/NCE1-Lesson63-64.js similarity index 100% rename from js/content/NCE1-Lesson63-64.js rename to Legacy/js/content/NCE1-Lesson63-64.js diff --git a/js/content/NCE2-Lesson3.js b/Legacy/js/content/NCE2-Lesson3.js similarity index 100% rename from js/content/NCE2-Lesson3.js rename to Legacy/js/content/NCE2-Lesson3.js diff --git a/js/content/NCE2-Lesson30.js b/Legacy/js/content/NCE2-Lesson30.js similarity index 100% rename from js/content/NCE2-Lesson30.js rename to Legacy/js/content/NCE2-Lesson30.js diff --git a/js/content/SBS-level-1.js b/Legacy/js/content/SBS-level-1.js similarity index 100% rename from js/content/SBS-level-1.js rename to Legacy/js/content/SBS-level-1.js diff --git a/js/content/WTA1B1-documented.js b/Legacy/js/content/WTA1B1-documented.js similarity index 100% rename from js/content/WTA1B1-documented.js rename to Legacy/js/content/WTA1B1-documented.js diff --git a/js/content/WTA1B1.js b/Legacy/js/content/WTA1B1.js similarity index 100% rename from js/content/WTA1B1.js rename to Legacy/js/content/WTA1B1.js diff --git a/js/content/chinese-long-story.js b/Legacy/js/content/chinese-long-story.js similarity index 100% rename from js/content/chinese-long-story.js rename to Legacy/js/content/chinese-long-story.js diff --git a/js/content/example-minimal.js b/Legacy/js/content/example-minimal.js similarity index 100% rename from js/content/example-minimal.js rename to Legacy/js/content/example-minimal.js diff --git a/js/content/example-with-images.js b/Legacy/js/content/example-with-images.js similarity index 100% rename from js/content/example-with-images.js rename to Legacy/js/content/example-with-images.js diff --git a/js/content/french-beginner-story.js b/Legacy/js/content/french-beginner-story.js similarity index 100% rename from js/content/french-beginner-story.js rename to Legacy/js/content/french-beginner-story.js diff --git a/js/content/grammar-lesson-le.js b/Legacy/js/content/grammar-lesson-le.js similarity index 100% rename from js/content/grammar-lesson-le.js rename to Legacy/js/content/grammar-lesson-le.js diff --git a/js/content/sbs-level-7-8-new.js b/Legacy/js/content/sbs-level-7-8-new.js similarity index 100% rename from js/content/sbs-level-7-8-new.js rename to Legacy/js/content/sbs-level-7-8-new.js diff --git a/js/content/story-prototype-optimized.js b/Legacy/js/content/story-prototype-optimized.js similarity index 100% rename from js/content/story-prototype-optimized.js rename to Legacy/js/content/story-prototype-optimized.js diff --git a/js/core/browser-logger.js b/Legacy/js/core/browser-logger.js similarity index 100% rename from js/core/browser-logger.js rename to Legacy/js/core/browser-logger.js diff --git a/js/core/content-engine.js b/Legacy/js/core/content-engine.js similarity index 100% rename from js/core/content-engine.js rename to Legacy/js/core/content-engine.js diff --git a/js/core/content-factory.js b/Legacy/js/core/content-factory.js similarity index 100% rename from js/core/content-factory.js rename to Legacy/js/core/content-factory.js diff --git a/js/core/content-generators.js b/Legacy/js/core/content-generators.js similarity index 100% rename from js/core/content-generators.js rename to Legacy/js/core/content-generators.js diff --git a/js/core/content-parsers.js b/Legacy/js/core/content-parsers.js similarity index 100% rename from js/core/content-parsers.js rename to Legacy/js/core/content-parsers.js diff --git a/js/core/content-scanner.js b/Legacy/js/core/content-scanner.js similarity index 100% rename from js/core/content-scanner.js rename to Legacy/js/core/content-scanner.js diff --git a/js/core/env-config.js b/Legacy/js/core/env-config.js similarity index 100% rename from js/core/env-config.js rename to Legacy/js/core/env-config.js diff --git a/js/core/game-loader.js b/Legacy/js/core/game-loader.js similarity index 100% rename from js/core/game-loader.js rename to Legacy/js/core/game-loader.js diff --git a/js/core/json-content-loader.js b/Legacy/js/core/json-content-loader.js similarity index 100% rename from js/core/json-content-loader.js rename to Legacy/js/core/json-content-loader.js diff --git a/js/core/navigation.js b/Legacy/js/core/navigation.js similarity index 100% rename from js/core/navigation.js rename to Legacy/js/core/navigation.js diff --git a/js/core/settings-manager.js b/Legacy/js/core/settings-manager.js similarity index 100% rename from js/core/settings-manager.js rename to Legacy/js/core/settings-manager.js diff --git a/js/core/simple-logger.js b/Legacy/js/core/simple-logger.js similarity index 100% rename from js/core/simple-logger.js rename to Legacy/js/core/simple-logger.js diff --git a/js/core/test-logger.js b/Legacy/js/core/test-logger.js similarity index 100% rename from js/core/test-logger.js rename to Legacy/js/core/test-logger.js diff --git a/js/core/utils.js b/Legacy/js/core/utils.js similarity index 100% rename from js/core/utils.js rename to Legacy/js/core/utils.js diff --git a/js/core/websocket-logger.js b/Legacy/js/core/websocket-logger.js similarity index 100% rename from js/core/websocket-logger.js rename to Legacy/js/core/websocket-logger.js diff --git a/js/games/adventure-reader.js b/Legacy/js/games/adventure-reader.js similarity index 100% rename from js/games/adventure-reader.js rename to Legacy/js/games/adventure-reader.js diff --git a/js/games/chinese-study.js b/Legacy/js/games/chinese-study.js similarity index 100% rename from js/games/chinese-study.js rename to Legacy/js/games/chinese-study.js diff --git a/js/games/fill-the-blank.js b/Legacy/js/games/fill-the-blank.js similarity index 100% rename from js/games/fill-the-blank.js rename to Legacy/js/games/fill-the-blank.js diff --git a/js/games/grammar-discovery.js b/Legacy/js/games/grammar-discovery.js similarity index 100% rename from js/games/grammar-discovery.js rename to Legacy/js/games/grammar-discovery.js diff --git a/js/games/letter-discovery.js b/Legacy/js/games/letter-discovery.js similarity index 100% rename from js/games/letter-discovery.js rename to Legacy/js/games/letter-discovery.js diff --git a/js/games/memory-match.js b/Legacy/js/games/memory-match.js similarity index 100% rename from js/games/memory-match.js rename to Legacy/js/games/memory-match.js diff --git a/js/games/quiz-game.js b/Legacy/js/games/quiz-game.js similarity index 100% rename from js/games/quiz-game.js rename to Legacy/js/games/quiz-game.js diff --git a/js/games/river-run.js b/Legacy/js/games/river-run.js similarity index 100% rename from js/games/river-run.js rename to Legacy/js/games/river-run.js diff --git a/js/games/story-builder.js b/Legacy/js/games/story-builder.js similarity index 100% rename from js/games/story-builder.js rename to Legacy/js/games/story-builder.js diff --git a/js/games/story-reader.js b/Legacy/js/games/story-reader.js similarity index 100% rename from js/games/story-reader.js rename to Legacy/js/games/story-reader.js diff --git a/js/games/whack-a-mole-hard.js b/Legacy/js/games/whack-a-mole-hard.js similarity index 100% rename from js/games/whack-a-mole-hard.js rename to Legacy/js/games/whack-a-mole-hard.js diff --git a/js/games/whack-a-mole.js b/Legacy/js/games/whack-a-mole.js similarity index 100% rename from js/games/whack-a-mole.js rename to Legacy/js/games/whack-a-mole.js diff --git a/js/games/wizard-spell-caster.js b/Legacy/js/games/wizard-spell-caster.js similarity index 100% rename from js/games/wizard-spell-caster.js rename to Legacy/js/games/wizard-spell-caster.js diff --git a/js/games/word-discovery.js b/Legacy/js/games/word-discovery.js similarity index 100% rename from js/games/word-discovery.js rename to Legacy/js/games/word-discovery.js diff --git a/js/games/word-storm.js b/Legacy/js/games/word-storm.js similarity index 100% rename from js/games/word-storm.js rename to Legacy/js/games/word-storm.js diff --git a/js/tools/content-creator.js b/Legacy/js/tools/content-creator.js similarity index 100% rename from js/tools/content-creator.js rename to Legacy/js/tools/content-creator.js diff --git a/js/tools/ultra-modular-validator.js b/Legacy/js/tools/ultra-modular-validator.js similarity index 100% rename from js/tools/ultra-modular-validator.js rename to Legacy/js/tools/ultra-modular-validator.js diff --git a/tests/README.md b/Legacy/tests/README.md similarity index 100% rename from tests/README.md rename to Legacy/tests/README.md diff --git a/tests/fixtures/content-samples.js b/Legacy/tests/fixtures/content-samples.js similarity index 100% rename from tests/fixtures/content-samples.js rename to Legacy/tests/fixtures/content-samples.js diff --git a/tests/fixtures/edge-case-data.js b/Legacy/tests/fixtures/edge-case-data.js similarity index 100% rename from tests/fixtures/edge-case-data.js rename to Legacy/tests/fixtures/edge-case-data.js diff --git a/tests/integration/content-loading-flow.test.js b/Legacy/tests/integration/content-loading-flow.test.js similarity index 100% rename from tests/integration/content-loading-flow.test.js rename to Legacy/tests/integration/content-loading-flow.test.js diff --git a/tests/integration/navigation-system.test.js b/Legacy/tests/integration/navigation-system.test.js similarity index 100% rename from tests/integration/navigation-system.test.js rename to Legacy/tests/integration/navigation-system.test.js diff --git a/tests/integration/proxy-digitalocean.test.js b/Legacy/tests/integration/proxy-digitalocean.test.js similarity index 100% rename from tests/integration/proxy-digitalocean.test.js rename to Legacy/tests/integration/proxy-digitalocean.test.js diff --git a/tests/integration/stress-tests.test.js b/Legacy/tests/integration/stress-tests.test.js similarity index 100% rename from tests/integration/stress-tests.test.js rename to Legacy/tests/integration/stress-tests.test.js diff --git a/tests/package.json b/Legacy/tests/package.json similarity index 100% rename from tests/package.json rename to Legacy/tests/package.json diff --git a/tests/run-tests.js b/Legacy/tests/run-tests.js similarity index 100% rename from tests/run-tests.js rename to Legacy/tests/run-tests.js diff --git a/tests/unit/basic-edge-cases.test.js b/Legacy/tests/unit/basic-edge-cases.test.js similarity index 100% rename from tests/unit/basic-edge-cases.test.js rename to Legacy/tests/unit/basic-edge-cases.test.js diff --git a/tests/unit/content-scanner.test.js b/Legacy/tests/unit/content-scanner.test.js similarity index 100% rename from tests/unit/content-scanner.test.js rename to Legacy/tests/unit/content-scanner.test.js diff --git a/tests/unit/edge-cases-simple.test.js b/Legacy/tests/unit/edge-cases-simple.test.js similarity index 100% rename from tests/unit/edge-cases-simple.test.js rename to Legacy/tests/unit/edge-cases-simple.test.js diff --git a/tests/unit/edge-cases.test.js b/Legacy/tests/unit/edge-cases.test.js similarity index 100% rename from tests/unit/edge-cases.test.js rename to Legacy/tests/unit/edge-cases.test.js diff --git a/tests/unit/env-config.test.js b/Legacy/tests/unit/env-config.test.js similarity index 100% rename from tests/unit/env-config.test.js rename to Legacy/tests/unit/env-config.test.js diff --git a/tests/unit/game-loader.test.js b/Legacy/tests/unit/game-loader.test.js similarity index 100% rename from tests/unit/game-loader.test.js rename to Legacy/tests/unit/game-loader.test.js diff --git a/tests/utils/test-helpers.js b/Legacy/tests/utils/test-helpers.js similarity index 100% rename from tests/utils/test-helpers.js rename to Legacy/tests/utils/test-helpers.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca22fa1 --- /dev/null +++ b/README.md @@ -0,0 +1,204 @@ +# Class Generator 2.0 + +**Educational Games Platform with Ultra-Modular Architecture** + +A complete rewrite of the Class Generator system using strict modular design patterns, vanilla JavaScript, and rigorous separation of concerns. + +## ๐Ÿš€ Quick Start + +### Method 1: Batch File (Windows) +```bash +# Double-click or run: +start.bat +``` + +### Method 2: Command Line +```bash +# Install Node.js (if not already installed) +# Then run: +node server.js + +# Or using npm: +npm start +``` + +### Method 3: NPM Scripts +```bash +npm run dev # Start development server +npm run serve # Same as start +``` + +**Server will start on:** `http://localhost:3000` + +## ๐Ÿ—๏ธ Architecture Overview + +### Core System (Ultra-Rigid) + +- **Module.js** - Abstract base class with sealed instances and WeakMap privates +- **EventBus.js** - Strict event system with validation and type safety +- **ModuleLoader.js** - Dependency injection with proper initialization order +- **Router.js** - Navigation with guards, middleware, and state management +- **Application.js** - Bootstrap system with auto-initialization + +### Key Architecture Principles + +โœ… **Inviolable Responsibility** - Each module has exactly one purpose +โœ… **Zero Direct Dependencies** - All communication via EventBus +โœ… **Sealed Instances** - Modules cannot be modified after creation +โœ… **Private State** - Internal data completely inaccessible +โœ… **Contract Enforcement** - Abstract methods must be implemented +โœ… **Dependency Injection** - No global access, everything injected + +## ๐Ÿ“ Project Structure + +``` +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ core/ # Core system modules (sealed) +โ”‚ โ”‚ โ”œโ”€โ”€ Module.js +โ”‚ โ”‚ โ”œโ”€โ”€ EventBus.js +โ”‚ โ”‚ โ”œโ”€โ”€ ModuleLoader.js +โ”‚ โ”‚ โ”œโ”€โ”€ Router.js +โ”‚ โ”‚ โ””โ”€โ”€ index.js +โ”‚ โ”œโ”€โ”€ components/ # Reusable UI components +โ”‚ โ”œโ”€โ”€ games/ # Game modules +โ”‚ โ”œโ”€โ”€ content/ # Content modules +โ”‚ โ”œโ”€โ”€ styles/ # Modular CSS +โ”‚ โ””โ”€โ”€ Application.js # Main application bootstrap +โ”œโ”€โ”€ Legacy/ # Old system (archived) +โ”œโ”€โ”€ index.html # Entry point +โ”œโ”€โ”€ server.js # Development server +โ”œโ”€โ”€ start.bat # Windows quick start +โ””โ”€โ”€ package.json # Node.js configuration +``` + +## ๐Ÿ”ฅ Features + +### Development Server +- **ES6 Modules** - Full import/export support +- **CORS Enabled** - For online communication +- **Hot Reload Ready** - Manual refresh (planned: auto-refresh) +- **Proper MIME Types** - All file types supported +- **Security Headers** - Basic security implemented + +### Application System +- **Auto-Start** - No commands needed, just open in browser +- **Loading Screen** - Elegant initialization feedback +- **Debug Panel** - F12 to toggle, shows system status +- **Error Handling** - Graceful error recovery +- **Network Status** - Real-time connectivity indicator + +### Module System +- **Strict Validation** - Errors if contracts violated +- **Lifecycle Management** - Proper init/destroy patterns +- **Event-Driven** - Loose coupling via events +- **Dependency Resolution** - Automatic dependency loading + +## ๐ŸŽฎ Creating Game Modules + +```javascript +import Module from '../core/Module.js'; + +class MyGame extends Module { + constructor(name, dependencies, config) { + super(name, ['eventBus', 'ui']); + + this._eventBus = dependencies.eventBus; + this._ui = dependencies.ui; + this._config = config; + } + + async init() { + // Initialize game + this._eventBus.on('game:start', this._handleStart.bind(this), this.name); + this._setInitialized(); + } + + async destroy() { + // Cleanup + this._setDestroyed(); + } + + _handleStart(event) { + // Game logic here + } +} + +export default MyGame; +``` + +## ๐Ÿงฉ Adding to Application + +```javascript +// In Application.js modules config: +{ + name: 'myGame', + path: './games/MyGame.js', + dependencies: ['eventBus', 'ui'], + config: { difficulty: 'easy' } +} +``` + +## ๐Ÿ› Debugging + +### Debug Panel (F12) +- System status and uptime +- Loaded modules list +- Event history +- Performance metrics + +### Console Access +```javascript +// Global app instance available in console: +window.app.getStatus() // Application status +window.app.getCore().eventBus // Access EventBus +window.app.getCore().router // Access Router +``` + +### Common Issues + +**Module loading fails:** +- Check file paths in Application.js +- Verify module extends Module base class +- Ensure all dependencies are listed + +**Events not working:** +- Verify module is registered with EventBus +- Check event type strings match exactly +- Ensure module is initialized before emitting + +## ๐Ÿ”’ Security & Rigidity + +The architecture enforces several levels of protection: + +1. **Sealed Classes** - `Object.seal()` prevents property addition/deletion +2. **Frozen Prototypes** - `Object.freeze()` prevents method modification +3. **WeakMap Privates** - Internal state completely hidden +4. **Abstract Enforcement** - Missing methods throw errors +5. **Validation Layers** - Input validation at every boundary + +**Violation attempts will throw explicit errors with helpful messages.** + +## ๐Ÿš€ Next Steps + +1. **Create Game Modules** - Implement actual games using new architecture +2. **Add Content System** - Port content loading from Legacy system +3. **UI Components** - Build reusable component library +4. **Performance** - Add lazy loading and caching +5. **Testing** - Add automated test suite + +## ๐Ÿ“ Migration from Legacy + +The Legacy/ folder contains the complete old system. Key differences: + +- **Old:** Global variables and direct coupling +- **New:** Strict modules with dependency injection + +- **Old:** CSS modifications in global files +- **New:** Component-scoped CSS injection + +- **Old:** Manual module registration +- **New:** Automatic loading with dependency resolution + +--- + +**Built with strict architectural principles for maintainable, scalable educational software.** \ No newline at end of file diff --git a/index.html b/index.html index bbaf672..0eff6af 100644 --- a/index.html +++ b/index.html @@ -3,439 +3,193 @@ - Interactive English Class - - - - + Class Generator - Educational Games Platform + + + + + + + - -
-
-
๐ŸŽ“ Interactive English Class
- + +
+ +
+
+

Loading Class Generator...

+

Initializing educational games platform

-
-
-
- Connecting... + + +
- -
- - -
+ + - - - - - - - - - - - - - - \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5bda68d --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "class-generator", + "version": "2.0.0", + "description": "Educational games platform with modular architecture", + "type": "module", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js", + "serve": "node server.js" + }, + "keywords": [ + "education", + "games", + "learning", + "vanilla-js", + "modular" + ], + "author": "Alexis Trouvรฉ", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..4786a28 --- /dev/null +++ b/server.js @@ -0,0 +1,197 @@ +/** + * Development Server - Simple HTTP server for local development + * Handles static files, CORS, and development features + */ + +import { createServer } from 'http'; +import { readFile, stat } from 'fs/promises'; +import { join, extname } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const PORT = process.env.PORT || 3000; +const HOST = process.env.HOST || 'localhost'; + +// MIME types for different file extensions +const MIME_TYPES = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'application/javascript', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + '.mp4': 'video/mp4' +}; + +const server = createServer(async (req, res) => { + try { + // Parse URL and remove query parameters + const urlPath = new URL(req.url, `http://${req.headers.host}`).pathname; + + // Default to index.html for root requests + const filePath = urlPath === '/' ? 'index.html' : urlPath.slice(1); + const fullPath = join(__dirname, filePath); + + console.log(`${new Date().toISOString()} - ${req.method} ${urlPath}`); + + // Set CORS headers for all requests + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + + // Handle preflight requests + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // Check if file exists + try { + const stats = await stat(fullPath); + + if (stats.isDirectory()) { + // Try to serve index.html from directory + const indexPath = join(fullPath, 'index.html'); + try { + await stat(indexPath); + return serveFile(indexPath, res); + } catch { + return send404(res, `Directory listing not allowed for ${urlPath}`); + } + } + + return serveFile(fullPath, res); + + } catch (error) { + if (error.code === 'ENOENT') { + return send404(res, `File not found: ${urlPath}`); + } + throw error; + } + + } catch (error) { + console.error('Server error:', error); + + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); + } +}); + +async function serveFile(filePath, res) { + try { + const ext = extname(filePath).toLowerCase(); + const mimeType = MIME_TYPES[ext] || 'application/octet-stream'; + + // Set content type + res.setHeader('Content-Type', mimeType); + + // Set cache headers for static assets + if (['.css', '.js', '.png', '.jpg', '.gif', '.svg', '.ico', '.woff', '.woff2'].includes(ext)) { + res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour + } else { + res.setHeader('Cache-Control', 'no-cache'); // No cache for HTML and other files + } + + // Add security headers + if (ext === '.js') { + res.setHeader('X-Content-Type-Options', 'nosniff'); + } + + // Read and serve file + const content = await readFile(filePath); + + res.writeHead(200); + res.end(content); + + console.log(` โœ… Served ${filePath} (${content.length} bytes, ${mimeType})`); + + } catch (error) { + console.error(`Error serving file ${filePath}:`, error); + + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Error reading file'); + } +} + +function send404(res, message = 'Not Found') { + const html404 = ` + + + + 404 - Not Found + + + +

404

+

${message}

+

โ† Back to Class Generator

+ + + `; + + res.writeHead(404, { 'Content-Type': 'text/html' }); + res.end(html404); + + console.log(` โŒ 404: ${message}`); +} + +// Start server +server.listen(PORT, HOST, () => { + console.log('\n๐Ÿš€ Class Generator Development Server'); + console.log('====================================='); + console.log(`๐Ÿ“ Local: http://${HOST}:${PORT}/`); + console.log(`๐ŸŒ Network: http://localhost:${PORT}/`); + console.log('๐Ÿ“ Serving files from:', __dirname); + console.log('\nโœจ Features:'); + console.log(' โ€ข ES6 modules support'); + console.log(' โ€ข CORS enabled'); + console.log(' โ€ข Static file serving'); + console.log(' โ€ข Development-friendly caching'); + console.log('\n๐Ÿ”ฅ Ready for development!'); + console.log('Press Ctrl+C to stop\n'); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\n๐Ÿ‘‹ Shutting down server...'); + server.close(() => { + console.log('โœ… Server stopped'); + process.exit(0); + }); +}); + +process.on('SIGTERM', () => { + console.log('\n๐Ÿ‘‹ Received SIGTERM, shutting down...'); + server.close(() => { + console.log('โœ… Server stopped'); + process.exit(0); + }); +}); \ No newline at end of file diff --git a/src/Application.js b/src/Application.js new file mode 100644 index 0000000..923a65d --- /dev/null +++ b/src/Application.js @@ -0,0 +1,260 @@ +/** + * Application - Main application bootstrap and lifecycle manager + * Auto-initializes the entire system without external commands + */ + +import { EventBus, ModuleLoader, Router } from './core/index.js'; + +class Application { + constructor(config = {}) { + // Application state + this._isRunning = false; + this._isInitialized = false; + this._startTime = null; + + // Core system instances + this._eventBus = new EventBus(); + this._moduleLoader = new ModuleLoader(this._eventBus); + this._router = null; + + // Configuration + this._config = { + autoStart: config.autoStart !== false, // Default true + defaultRoute: config.defaultRoute || '/', + enableDebug: config.enableDebug || false, + modules: config.modules || [], + ...config + }; + + // Auto-start if enabled + if (this._config.autoStart) { + this._autoStart(); + } + + // Seal to prevent modification + Object.seal(this); + } + + /** + * Initialize and start the application + */ + async start() { + if (this._isRunning) { + console.warn('Application is already running'); + return; + } + + try { + this._startTime = Date.now(); + + console.log('๐Ÿš€ Starting Class Generator Application...'); + + // Initialize core systems + await this._initializeCore(); + + // Load and initialize modules + await this._loadModules(); + + // Start routing + await this._startRouting(); + + // Set up global error handling + this._setupErrorHandling(); + + this._isRunning = true; + this._isInitialized = true; + + const startupTime = Date.now() - this._startTime; + console.log(`โœ… Application started successfully in ${startupTime}ms`); + + // Emit application ready event + this._eventBus.emit('app:ready', { + startupTime, + modules: this._moduleLoader.getStatus() + }, 'Application'); + + } catch (error) { + console.error('โŒ Failed to start application:', error); + throw error; + } + } + + /** + * Stop the application and clean up + */ + async stop() { + if (!this._isRunning) { + return; + } + + try { + console.log('๐Ÿ›‘ Stopping application...'); + + // Emit application stopping event + this._eventBus.emit('app:stopping', {}, 'Application'); + + // Destroy all modules + const status = this._moduleLoader.getStatus(); + for (const moduleName of status.loaded) { + await this._moduleLoader.destroy(moduleName); + } + + // Destroy router + if (this._router) { + await this._moduleLoader.destroy('router'); + } + + this._isRunning = false; + this._isInitialized = false; + + console.log('โœ… Application stopped successfully'); + + } catch (error) { + console.error('โŒ Error stopping application:', error); + } + } + + /** + * Get application status + */ + getStatus() { + return { + isRunning: this._isRunning, + isInitialized: this._isInitialized, + startTime: this._startTime, + uptime: this._startTime ? Date.now() - this._startTime : 0, + modules: this._moduleLoader.getStatus(), + config: { ...this._config } + }; + } + + /** + * Get core system instances (for advanced usage) + */ + getCore() { + return { + eventBus: this._eventBus, + moduleLoader: this._moduleLoader, + router: this._router + }; + } + + // Private methods + async _autoStart() { + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => this.start()); + } else { + // DOM is already loaded + setTimeout(() => this.start(), 0); + } + } + + async _initializeCore() { + // Register router as a module + this._moduleLoader.register('router', Router, ['eventBus']); + + // Create and initialize router + this._router = await this._moduleLoader.loadAndInitialize('router', { + defaultRoute: this._config.defaultRoute, + maxHistorySize: 100 + }); + + if (this._config.enableDebug) { + console.log('๐Ÿ”ง Core systems initialized'); + } + } + + async _loadModules() { + for (const moduleConfig of this._config.modules) { + try { + const { name, path, dependencies = [], config = {} } = moduleConfig; + + // Dynamically import module + const moduleModule = await import(path); + const ModuleClass = moduleModule.default; + + // Register and initialize + this._moduleLoader.register(name, ModuleClass, dependencies); + await this._moduleLoader.loadAndInitialize(name, config); + + if (this._config.enableDebug) { + console.log(`๐Ÿ“ฆ Module ${name} loaded successfully`); + } + + } catch (error) { + console.error(`โŒ Failed to load module ${moduleConfig.name}:`, error); + + // Emit module load error + this._eventBus.emit('app:module-error', { + module: moduleConfig.name, + error: error.message + }, 'Application'); + } + } + } + + async _startRouting() { + // Register default routes + this._router.register('/', this._handleHomeRoute.bind(this)); + this._router.register('/games', this._handleGamesRoute.bind(this)); + this._router.register('/play', this._handlePlayRoute.bind(this)); + + if (this._config.enableDebug) { + console.log('๐Ÿ›ฃ๏ธ Routing system started'); + } + } + + _setupErrorHandling() { + // Global error handler + window.addEventListener('error', (event) => { + console.error('Global error:', event.error); + this._eventBus.emit('app:error', { + message: event.error.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno + }, 'Application'); + }); + + // Unhandled promise rejection handler + window.addEventListener('unhandledrejection', (event) => { + console.error('Unhandled promise rejection:', event.reason); + this._eventBus.emit('app:promise-rejection', { + reason: event.reason + }, 'Application'); + }); + + if (this._config.enableDebug) { + console.log('๐Ÿ›ก๏ธ Error handling setup complete'); + } + } + + // Default route handlers + async _handleHomeRoute(path, state) { + this._eventBus.emit('navigation:home', { path, state }, 'Application'); + } + + async _handleGamesRoute(path, state) { + this._eventBus.emit('navigation:games', { path, state }, 'Application'); + } + + async _handlePlayRoute(path, state) { + this._eventBus.emit('navigation:play', { path, state }, 'Application'); + } +} + +// Create global application instance +const app = new Application({ + enableDebug: true, + modules: [ + // Modules will be registered here + // { name: 'ui', path: './components/UI.js', dependencies: ['eventBus'] }, + // { name: 'gameEngine', path: './games/GameEngine.js', dependencies: ['eventBus', 'ui'] } + ] +}); + +// Export for manual control if needed +export default app; + +// Auto-start is handled by the constructor \ No newline at end of file diff --git a/src/content/NCE1-Lesson63-64.js b/src/content/NCE1-Lesson63-64.js new file mode 100644 index 0000000..7425258 --- /dev/null +++ b/src/content/NCE1-Lesson63-64.js @@ -0,0 +1,850 @@ +// === ENGLISH MEDICAL AND SAFETY LESSONS === +// Lessons 63-64: Doctor visit and prohibition commands with Chinese translation + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.NCE1Lesson6364 = { + id: "nce1-lesson63-64", + name: "NCE1-Lesson63-64", + description: "English medical dialogue and prohibition commands with modal verbs", + difficulty: "intermediate", + language: "en-US", + userLanguage: "zh-CN", + totalWords: 120, + + // === GRAMMAR LESSONS SYSTEM === + grammar: { + "modal-must-mustnot": { + title: "Modal Verbs: Must and Mustn't - ๆƒ…ๆ€ๅŠจ่ฏmustๅ’Œmustn't", + explanation: "English uses 'must' for strong obligation and 'mustn't' for prohibition.", + rules: [ + "must + verb - for strong necessity: You must stay in bed", + "mustn't + verb - for prohibition: You mustn't get up yet", + "Must + subject + verb? - for questions: Must he stay in bed?", + "No contraction for positive must, but mustn't = must not" + ], + examples: [ + { + english: "You must stay in bed.", + chinese: "ไฝ ๅฟ…้กปๅงๅบŠไผ‘ๆฏใ€‚", + explanation: "Use 'must' for strong obligation or medical advice", + pronunciation: "ju mสŒst steษช ษชn bed" + }, + { + english: "You mustn't get up yet.", + chinese: "ไฝ ่ฟ˜ไธ่ƒฝ่ตทๅบŠใ€‚", + explanation: "Use 'mustn't' for prohibition or things not allowed", + pronunciation: "ju mสŒsnt get สŒp jet" + }, + { + english: "Must he stay in bed?", + chinese: "ไป–ๅฟ…้กปๅงๅบŠๅ—๏ผŸ", + explanation: "Use 'Must' at the start for yes/no questions", + pronunciation: "mสŒst hi steษช ษชn bed" + }, + { + english: "He mustn't eat rich food.", + chinese: "ไป–ไธ่ƒฝๅƒๆฒน่…ป้ฃŸ็‰ฉใ€‚", + explanation: "Use 'mustn't' for medical restrictions", + pronunciation: "hi mสŒsnt it rษชtสƒ fud" + }, + { + english: "You must keep the room warm.", + chinese: "ไฝ ๅฟ…้กปไฟๆŒๆˆฟ้—ดๆธฉๆš–ใ€‚", + explanation: "Use 'must' for important instructions", + pronunciation: "ju mสŒst kip รฐษ™ rum wษ”rm" + }, + { + english: "The boy mustn't go to school yet.", + chinese: "่ฟ™ไธช็”ทๅญฉ่ฟ˜ไธ่ƒฝๅŽปไธŠๅญฆใ€‚", + explanation: "Use 'mustn't' for temporary restrictions", + pronunciation: "รฐษ™ bษ”ษช mสŒsnt gษ™สŠ tu skul jet" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "You _____ stay in bed for two days.", + options: ["must", "mustn't", "can", "can't"], + correct: "must", + explanation: "Use 'must' for medical necessity" + }, + { + type: "fill_blank", + sentence: "He _____ eat rich food when he's sick.", + options: ["must", "mustn't", "should", "can"], + correct: "mustn't", + explanation: "Use 'mustn't' for medical restrictions" + } + ] + }, + + "imperatives-commands": { + title: "Imperative Commands - ็ฅˆไฝฟๅฅ", + explanation: "English uses imperatives to give commands, instructions, or make requests.", + rules: [ + "Positive commands: Verb + object: Come upstairs", + "Negative commands: Don't + verb: Don't take medicine", + "No subject pronoun needed in commands", + "Use for instructions, warnings, and advice" + ], + examples: [ + { + english: "Come upstairs.", + chinese: "ไธŠๆฅผๆฅใ€‚", + explanation: "Positive command - just use the base verb", + pronunciation: "kสŒm สŒpstษ›rz" + }, + { + english: "Don't take any aspirins.", + chinese: "ไธ่ฆๅƒไปปไฝ•้˜ฟๅธๅŒนๆž—ใ€‚", + explanation: "Negative command - Don't + base verb", + pronunciation: "dษ™สŠnt teษชk eni รฆspษชrษชnz" + }, + { + english: "Don't play with matches.", + chinese: "ไธ่ฆ็Žฉ็ซๆŸดใ€‚", + explanation: "Safety warning using negative command", + pronunciation: "dษ™สŠnt pleษช wษชรฐ mรฆtสƒษชz" + }, + { + english: "Don't make a noise.", + chinese: "ไธ่ฆๅˆถ้€ ๅ™ช้Ÿณใ€‚", + explanation: "Polite request using negative command", + pronunciation: "dษ™สŠnt meษชk ษ™ nษ”ษชz" + }, + { + english: "Don't drive so quickly.", + chinese: "ไธ่ฆๅผ€ๅพ—่ฟ™ไนˆๅฟซใ€‚", + explanation: "Safety advice using negative command", + pronunciation: "dษ™สŠnt draษชv sษ™สŠ kwษชkli" + }, + { + english: "Don't break that vase.", + chinese: "ไธ่ฆๆ‰“็ ด้‚ฃไธช่Šฑ็“ถใ€‚", + explanation: "Warning using negative command", + pronunciation: "dษ™สŠnt breษชk รฐรฆt vษ‘หz" + } + ], + exercises: [ + { + type: "transformation", + instruction: "Make this a negative command:", + original: "Take this medicine.", + correct: "Don't take this medicine.", + explanation: "Add 'Don't' before the verb" + } + ] + }, + + "present-simple-questions": { + title: "Present Simple Questions - ไธ€่ˆฌ็Žฐๅœจๆ—ถ็–‘้—ฎๅฅ", + explanation: "English forms questions differently for 'be' verbs and other verbs.", + rules: [ + "With 'be': Be + subject: How's Jimmy? Where's Mr. Williams?", + "With other verbs: Do/Does + subject + verb: Does he have a temperature?", + "Question words come first: How, What, Where, When" + ], + examples: [ + { + english: "How's Jimmy today?", + chinese: "ๅ‰็ฑณไปŠๅคฉๆ€Žไนˆๆ ท๏ผŸ", + explanation: "Question with 'be' verb - How + is contracted", + pronunciation: "haสŠz dส’ษชmi tษ™deษช" + }, + { + english: "Where's Mr. Williams?", + chinese: "ๅจๅป‰ๅง†ๆ–ฏๅ…ˆ็”Ÿๅœจๅ“ช้‡Œ๏ผŸ", + explanation: "Question with 'be' verb - Where + is contracted", + pronunciation: "wษ›rz mษชstษ™r wษชljษ™mz" + }, + { + english: "Does he have a temperature?", + chinese: "ไป–ๅ‘็ƒงๅ—๏ผŸ", + explanation: "Question with regular verb - Does + subject + verb", + pronunciation: "dสŒz hi hรฆv ษ™ tempษ™rษ™tสƒษ™r" + }, + { + english: "Can I see him please?", + chinese: "ๆˆ‘ๅฏไปฅ็œ‹็œ‹ไป–ๅ—๏ผŸ", + explanation: "Question with modal verb - Modal + subject + verb", + pronunciation: "kรฆn aษช si hษชm pliz" + } + ], + exercises: [ + { + type: "question_formation", + statement: "He has a cold.", + correct: "Does he have a cold?", + explanation: "Use 'Does' + subject + base verb for questions" + } + ] + } + }, + + vocabulary: { + "doctor": { + "user_language": "ๅŒป็”Ÿ", + "type": "noun", + "pronunciation": "dษ”ktษ™r" + }, + "better": { + "user_language": "ๆ›ดๅฅฝ็š„", + "type": "adjective", + "pronunciation": "betษ™r" + }, + "certainly": { + "user_language": "ๅฝ“็„ถ", + "type": "adverb", + "pronunciation": "sษœrtษ™nli" + }, + "upstairs": { + "user_language": "ๆฅผไธŠ", + "type": "adverb", + "pronunciation": "สŒpstษ›rz" + }, + "bed": { + "user_language": "ๅบŠ", + "type": "noun", + "pronunciation": "bed" + }, + "yet": { + "user_language": "่ฟ˜๏ผŒไป", + "type": "adverb", + "pronunciation": "jet" + }, + "stay": { + "user_language": "ๅœ็•™", + "type": "verb", + "pronunciation": "steษช" + }, + "school": { + "user_language": "ๅญฆๆ ก", + "type": "noun", + "pronunciation": "skul" + }, + "rich": { + "user_language": "ๆฒน่…ป็š„", + "type": "adjective", + "pronunciation": "rษชtสƒ" + }, + "food": { + "user_language": "้ฃŸ็‰ฉ", + "type": "noun", + "pronunciation": "fud" + }, + "temperature": { + "user_language": "ไฝ“ๆธฉ๏ผŒๅ‘็ƒง", + "type": "noun", + "pronunciation": "tempษ™rษ™tสƒษ™r" + }, + "remain": { + "user_language": "ไฟๆŒ๏ผŒ็ปง็ปญ", + "type": "verb", + "pronunciation": "rษชmeษชn" + }, + "warm": { + "user_language": "ๆธฉๆš–็š„", + "type": "adjective", + "pronunciation": "wษ”rm" + }, + "cold": { + "user_language": "ๆ„Ÿๅ†’", + "type": "noun", + "pronunciation": "kษ™สŠld" + }, + "aspirins": { + "user_language": "้˜ฟๅธๅŒนๆž—", + "type": "noun", + "pronunciation": "รฆspษชrษชnz" + }, + "medicine": { + "user_language": "่ฏ", + "type": "noun", + "pronunciation": "medษชsษ™n" + }, + "play": { + "user_language": "็Žฉ", + "type": "verb", + "pronunciation": "pleษช" + }, + "matches": { + "user_language": "็ซๆŸด", + "type": "noun", + "pronunciation": "mรฆtสƒษชz" + }, + "talk": { + "user_language": "่ฐˆ่ฏ", + "type": "verb", + "pronunciation": "tษ”k" + }, + "library": { + "user_language": "ๅ›พไนฆ้ฆ†", + "type": "noun", + "pronunciation": "laษชbrษ™ri" + }, + "noise": { + "user_language": "ๅ™ช้Ÿณ", + "type": "noun", + "pronunciation": "nษ”ษชz" + }, + "drive": { + "user_language": "ๅผ€่ฝฆ", + "type": "verb", + "pronunciation": "draษชv" + }, + "quickly": { + "user_language": "ๅฟซๅœฐ", + "type": "adverb", + "pronunciation": "kwษชkli" + }, + "lean": { + "user_language": "ๆŽข่บซ", + "type": "verb", + "pronunciation": "lin" + }, + "window": { + "user_language": "็ช—ๆˆท", + "type": "noun", + "pronunciation": "wษชndษ™สŠ" + }, + "break": { + "user_language": "ๆ‰“็ ด", + "type": "verb", + "pronunciation": "breษชk" + }, + "vase": { + "user_language": "่Šฑ็“ถ", + "type": "noun", + "pronunciation": "vษ‘z" + } + }, + + // === SENTENCES FOR GAMES (extracted from stories) === + sentences: [ + { + english: "How's Jimmy today?", + chinese: "ๅ‰็ฑณไปŠๅคฉๆ€Žไนˆๆ ท๏ผŸ", + prononciation: "haสŠz dส’ษชmi tษ™deษช" + }, + { + english: "Better. Thank you, doctor.", + chinese: "ๅฅฝไธ€ไบ›ไบ†ใ€‚่ฐข่ฐขไฝ ๏ผŒๅŒป็”Ÿใ€‚", + prononciation: "betษ™r ฮธรฆล‹k ju dษ”ktษ™r" + }, + { + english: "Can I see him please?", + chinese: "ๆˆ‘ๅฏไปฅ็œ‹็œ‹ไป–ๅ—๏ผŸ", + prononciation: "kรฆn aษช si hษชm pliz" + }, + { + english: "You must stay in bed.", + chinese: "ไฝ ๅฟ…้กปๅงๅบŠไผ‘ๆฏใ€‚", + prononciation: "ju mสŒst steษช ษชn bed" + }, + { + english: "You mustn't get up yet.", + chinese: "ไฝ ่ฟ˜ไธ่ƒฝ่ตทๅบŠใ€‚", + prononciation: "ju mสŒsnt get สŒp jet" + }, + { + english: "Don't take any aspirins.", + chinese: "ไธ่ฆๅƒไปปไฝ•้˜ฟๅธๅŒนๆž—ใ€‚", + prononciation: "dษ™สŠnt teษชk eni รฆspษชrษชnz" + }, + { + english: "Don't play with matches.", + chinese: "ไธ่ฆ็Žฉ็ซๆŸดใ€‚", + prononciation: "dษ™สŠnt pleษช wษชรฐ mรฆtสƒษชz" + }, + { + english: "The doctor says Jimmy must rest.", + chinese: "ๅŒป็”Ÿ่ฏดๅ‰็ฑณๅฟ…้กปไผ‘ๆฏใ€‚", + prononciation: "รฐษ™ dษ”ktษ™r sez dส’ษชmi mสŒst rest" + }, + { + english: "He mustn't go outside yet.", + chinese: "ไป–่ฟ˜ไธ่ƒฝๅ‡บ้—จใ€‚", + prononciation: "hi mสŒsnt gษ™สŠ aสŠtsaษชd jet" + }, + { + english: "Mrs. Williams must keep him warm.", + chinese: "ๅจๅป‰ๅง†ๆ–ฏๅคชๅคชๅฟ…้กป่ฎฉไป–ไฟๆš–ใ€‚", + prononciation: "mษชsษชz wษชljษ™mz mสŒst kip hษชm wษ”rm" + } + ], + + story: { + title: "At the Doctor's Visit - ็œ‹ๅŒป็”Ÿ", + totalSentences: 23, + chapters: [ + { + title: "Chapter 1: Jimmy is Sick - ็ฌฌไธ€็ซ ๏ผšๅ‰็ฑณ็”Ÿ็—…ไบ†", + sentences: [ + { + id: 1, + original: "How's Jimmy today?", + translation: "ๅ‰็ฑณไปŠๅคฉๆ€Žไนˆๆ ท๏ผŸ", + words: [ + {word: "How's", translation: "ๆ€Žไนˆๆ ท", type: "question", pronunciation: "haสŠz"}, + {word: "Jimmy", translation: "ๅ‰็ฑณ", type: "noun", pronunciation: "dส’ษชmi"}, + {word: "today", translation: "ไปŠๅคฉ", type: "adverb", pronunciation: "tษ™deษช"} + ] + }, + { + id: 2, + original: "Better. Thank you, doctor.", + translation: "ๅฅฝไธ€ไบ›ไบ†ใ€‚่ฐข่ฐขไฝ ๏ผŒๅŒป็”Ÿใ€‚", + words: [ + {word: "Better", translation: "ๅฅฝไธ€ไบ›", type: "adjective", pronunciation: "betษ™r"}, + {word: "Thank", translation: "่ฐข่ฐข", type: "verb", pronunciation: "ฮธรฆล‹k"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "doctor", translation: "ๅŒป็”Ÿ", type: "noun", pronunciation: "dษ”ktษ™r"} + ] + }, + { + id: 3, + original: "Can I see him please?", + translation: "ๆˆ‘ๅฏไปฅ็œ‹็œ‹ไป–ๅ—๏ผŸ", + words: [ + {word: "Can", translation: "ๅฏไปฅ", type: "modal", pronunciation: "kรฆn"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "see", translation: "็œ‹", type: "verb", pronunciation: "si"}, + {word: "him", translation: "ไป–", type: "pronoun", pronunciation: "hษชm"}, + {word: "please", translation: "่ฏท", type: "adverb", pronunciation: "pliz"} + ] + }, + { + id: 4, + original: "Certainly, doctor. Come upstairs.", + translation: "ๅฝ“็„ถๅฏไปฅ๏ผŒๅŒป็”Ÿใ€‚ไธŠๆฅผๆฅใ€‚", + words: [ + {word: "Certainly", translation: "ๅฝ“็„ถ", type: "adverb", pronunciation: "sษœrtษ™nli"}, + {word: "doctor", translation: "ๅŒป็”Ÿ", type: "noun", pronunciation: "dษ”ktษ™r"}, + {word: "Come", translation: "ๆฅ", type: "verb", pronunciation: "kสŒm"}, + {word: "upstairs", translation: "ๆฅผไธŠ", type: "adverb", pronunciation: "สŒpstษ›rz"} + ] + }, + { + id: 5, + original: "You look very well, Jimmy.", + translation: "ไฝ ็œ‹่ตทๆฅๅพˆๅฅฝ๏ผŒๅ‰็ฑณใ€‚", + words: [ + {word: "You", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "look", translation: "็œ‹่ตทๆฅ", type: "verb", pronunciation: "lสŠk"}, + {word: "very", translation: "ๅพˆ", type: "adverb", pronunciation: "vษ›ri"}, + {word: "well", translation: "ๅฅฝ", type: "adverb", pronunciation: "wษ›l"}, + {word: "Jimmy", translation: "ๅ‰็ฑณ", type: "noun", pronunciation: "dส’ษชmi"} + ] + }, + { + id: 6, + original: "You are better now.", + translation: "ไฝ ็Žฐๅœจๅฅฝๅคšไบ†ใ€‚", + words: [ + {word: "You", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "better", translation: "ๆ›ดๅฅฝ", type: "adjective", pronunciation: "betษ™r"}, + {word: "now", translation: "็Žฐๅœจ", type: "adverb", pronunciation: "naสŠ"} + ] + }, + { + id: 7, + original: "You mustn't get up yet.", + translation: "ไฝ ่ฟ˜ไธ่ƒฝ่ตทๅบŠใ€‚", + words: [ + {word: "You", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "mustn't", translation: "ไธ่ƒฝ", type: "modal", pronunciation: "mสŒsnt"}, + {word: "get", translation: "่ตท", type: "verb", pronunciation: "get"}, + {word: "up", translation: "ๅบŠ", type: "adverb", pronunciation: "สŒp"}, + {word: "yet", translation: "่ฟ˜", type: "adverb", pronunciation: "jet"} + ] + }, + { + id: 8, + original: "You must stay in bed.", + translation: "ไฝ ๅฟ…้กปๅงๅบŠไผ‘ๆฏใ€‚", + words: [ + {word: "You", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "must", translation: "ๅฟ…้กป", type: "modal", pronunciation: "mสŒst"}, + {word: "stay", translation: "ๅœ็•™", type: "verb", pronunciation: "steษช"}, + {word: "in", translation: "ๅœจ", type: "preposition", pronunciation: "ษชn"}, + {word: "bed", translation: "ๅบŠ", type: "noun", pronunciation: "bed"} + ] + }, + { + id: 9, + original: "He mustn't go to school yet.", + translation: "ไป–่ฟ˜ไธ่ƒฝๅŽปไธŠๅญฆใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "mustn't", translation: "ไธ่ƒฝ", type: "modal", pronunciation: "mสŒsnt"}, + {word: "go", translation: "ๅŽป", type: "verb", pronunciation: "gษ™สŠ"}, + {word: "to", translation: "ๅˆฐ", type: "preposition", pronunciation: "tu"}, + {word: "school", translation: "ๅญฆๆ ก", type: "noun", pronunciation: "skul"}, + {word: "yet", translation: "่ฟ˜", type: "adverb", pronunciation: "jet"} + ] + }, + { + id: 10, + original: "He mustn't eat rich food.", + translation: "ไป–ไธ่ƒฝๅƒๆฒน่…ป้ฃŸ็‰ฉใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "mustn't", translation: "ไธ่ƒฝ", type: "modal", pronunciation: "mสŒsnt"}, + {word: "eat", translation: "ๅƒ", type: "verb", pronunciation: "it"}, + {word: "rich", translation: "ๆฒน่…ป็š„", type: "adjective", pronunciation: "rษชtสƒ"}, + {word: "food", translation: "้ฃŸ็‰ฉ", type: "noun", pronunciation: "fud"} + ] + } + ] + }, + { + title: "Chapter 2: Safety Rules - ็ฌฌไบŒ็ซ ๏ผšๅฎ‰ๅ…จ่ง„ๅˆ™", + sentences: [ + { + id: 11, + original: "Don't take any aspirins.", + translation: "ไธ่ฆๅƒไปปไฝ•้˜ฟๅธๅŒนๆž—ใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "take", translation: "ๅƒ", type: "verb", pronunciation: "teษชk"}, + {word: "any", translation: "ไปปไฝ•", type: "determiner", pronunciation: "eni"}, + {word: "aspirins", translation: "้˜ฟๅธๅŒนๆž—", type: "noun", pronunciation: "รฆspษชrษชnz"} + ] + }, + { + id: 12, + original: "Don't take this medicine.", + translation: "ไธ่ฆๅƒ่ฟ™ไธช่ฏใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "take", translation: "ๅƒ", type: "verb", pronunciation: "teษชk"}, + {word: "this", translation: "่ฟ™ไธช", type: "determiner", pronunciation: "รฐษชs"}, + {word: "medicine", translation: "่ฏ", type: "noun", pronunciation: "medษชsษ™n"} + ] + }, + { + id: 13, + original: "Don't play with matches.", + translation: "ไธ่ฆ็Žฉ็ซๆŸดใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "play", translation: "็Žฉ", type: "verb", pronunciation: "pleษช"}, + {word: "with", translation: "ๅ’Œ", type: "preposition", pronunciation: "wษชรฐ"}, + {word: "matches", translation: "็ซๆŸด", type: "noun", pronunciation: "mรฆtสƒษชz"} + ] + }, + { + id: 14, + original: "Don't talk in the library.", + translation: "ไธ่ฆๅœจๅ›พไนฆ้ฆ†่ฏด่ฏใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "talk", translation: "่ฏด่ฏ", type: "verb", pronunciation: "tษ”k"}, + {word: "in", translation: "ๅœจ", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "library", translation: "ๅ›พไนฆ้ฆ†", type: "noun", pronunciation: "laษชbrษ™ri"} + ] + }, + { + id: 15, + original: "Don't make a noise.", + translation: "ไธ่ฆๅˆถ้€ ๅ™ช้Ÿณใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "make", translation: "ๅˆถ้€ ", type: "verb", pronunciation: "meษชk"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "noise", translation: "ๅ™ช้Ÿณ", type: "noun", pronunciation: "nษ”ษชz"} + ] + }, + { + id: 16, + original: "Don't drive so quickly.", + translation: "ไธ่ฆๅผ€ๅพ—่ฟ™ไนˆๅฟซใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "drive", translation: "ๅผ€่ฝฆ", type: "verb", pronunciation: "draษชv"}, + {word: "so", translation: "ๅฆ‚ๆญค", type: "adverb", pronunciation: "sษ™สŠ"}, + {word: "quickly", translation: "ๅฟซๅœฐ", type: "adverb", pronunciation: "kwษชkli"} + ] + }, + { + id: 17, + original: "Don't lean out of the window.", + translation: "ไธ่ฆไปŽ็ช—ๆˆทๆŽขๅ‡บ่บซๅญใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "lean", translation: "ๆŽข่บซ", type: "verb", pronunciation: "lin"}, + {word: "out", translation: "ๅ‡บ", type: "adverb", pronunciation: "aสŠt"}, + {word: "of", translation: "ไปŽ", type: "preposition", pronunciation: "สŒv"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "window", translation: "็ช—ๆˆท", type: "noun", pronunciation: "wษชndษ™สŠ"} + ] + }, + { + id: 18, + original: "Don't break that vase.", + translation: "ไธ่ฆๆ‰“็ ด้‚ฃไธช่Šฑ็“ถใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "break", translation: "ๆ‰“็ ด", type: "verb", pronunciation: "breษชk"}, + {word: "that", translation: "้‚ฃไธช", type: "determiner", pronunciation: "รฐรฆt"}, + {word: "vase", translation: "่Šฑ็“ถ", type: "noun", pronunciation: "vษ‘z"} + ] + }, + { + id: 19, + original: "Does he have a temperature?", + translation: "ไป–ๅ‘็ƒงๅ—๏ผŸ", + words: [ + {word: "Does", translation: "ๅ—", type: "auxiliary", pronunciation: "dสŒz"}, + {word: "he", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "have", translation: "ๆœ‰", type: "verb", pronunciation: "hรฆv"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "temperature", translation: "ไฝ“ๆธฉ", type: "noun", pronunciation: "tempษ™rษ™tสƒษ™r"} + ] + }, + { + id: 20, + original: "He has a bad cold too.", + translation: "ไป–ไนŸๅพ—ไบ†้‡ๆ„Ÿๅ†’ใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "has", translation: "ๆœ‰", type: "verb", pronunciation: "hรฆz"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "bad", translation: "ไธฅ้‡็š„", type: "adjective", pronunciation: "bรฆd"}, + {word: "cold", translation: "ๆ„Ÿๅ†’", type: "noun", pronunciation: "kษ™สŠld"}, + {word: "too", translation: "ไนŸ", type: "adverb", pronunciation: "tu"} + ] + }, + { + id: 21, + original: "The doctor told Mrs. Williams to keep Jimmy warm and give him plenty of rest.", + translation: "ๅŒป็”Ÿๅ‘Š่ฏ‰ๅจๅป‰ๅง†ๆ–ฏๅคชๅคช่ฆ่ฎฉๅ‰็ฑณไฟๆš–ๅนถ่ฎฉไป–ๅคšไผ‘ๆฏใ€‚", + words: [ + {word: "The", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "doctor", translation: "ๅŒป็”Ÿ", type: "noun", pronunciation: "dษ”ktษ™r"}, + {word: "told", translation: "ๅ‘Š่ฏ‰", type: "verb", pronunciation: "toสŠld"}, + {word: "Mrs.", translation: "ๅคชๅคช", type: "title", pronunciation: "mษชsษชz"}, + {word: "Williams", translation: "ๅจๅป‰ๅง†ๆ–ฏ", type: "noun", pronunciation: "wษชljษ™mz"}, + {word: "to", translation: "่ฆ", type: "preposition", pronunciation: "tuห"}, + {word: "keep", translation: "ไฟๆŒ", type: "verb", pronunciation: "kiหp"}, + {word: "Jimmy", translation: "ๅ‰็ฑณ", type: "noun", pronunciation: "dส’ษชmi"}, + {word: "warm", translation: "ๆธฉๆš–", type: "adjective", pronunciation: "wษ”หrm"}, + {word: "and", translation: "ๅนถไธ”", type: "conjunction", pronunciation: "รฆnd"}, + {word: "give", translation: "็ป™", type: "verb", pronunciation: "gษชv"}, + {word: "him", translation: "ไป–", type: "pronoun", pronunciation: "hษชm"}, + {word: "plenty", translation: "ๅคง้‡", type: "noun", pronunciation: "plenti"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "rest", translation: "ไผ‘ๆฏ", type: "noun", pronunciation: "rest"} + ] + }, + { + id: 22, + original: "Jimmy must stay in bed until the doctor says he can go back to school.", + translation: "ๅ‰็ฑณๅฟ…้กปๅงๅบŠ็›ดๅˆฐๅŒป็”Ÿ่ฏดไป–ๅฏไปฅๅ›žๅญฆๆ กใ€‚", + words: [ + {word: "Jimmy", translation: "ๅ‰็ฑณ", type: "noun", pronunciation: "dส’ษชmi"}, + {word: "must", translation: "ๅฟ…้กป", type: "modal", pronunciation: "mสŒst"}, + {word: "stay", translation: "ๅœ็•™", type: "verb", pronunciation: "steษช"}, + {word: "in", translation: "ๅœจ", type: "preposition", pronunciation: "ษชn"}, + {word: "bed", translation: "ๅบŠ", type: "noun", pronunciation: "bed"}, + {word: "until", translation: "็›ดๅˆฐ", type: "conjunction", pronunciation: "ษ™nหˆtษชl"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "doctor", translation: "ๅŒป็”Ÿ", type: "noun", pronunciation: "dษ”ktษ™r"}, + {word: "says", translation: "่ฏด", type: "verb", pronunciation: "sez"}, + {word: "he", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "can", translation: "ๅฏไปฅ", type: "modal", pronunciation: "kรฆn"}, + {word: "go", translation: "ๅŽป", type: "verb", pronunciation: "goสŠ"}, + {word: "back", translation: "ๅ›ž", type: "adverb", pronunciation: "bรฆk"}, + {word: "to", translation: "ๅˆฐ", type: "preposition", pronunciation: "tuห"}, + {word: "school", translation: "ๅญฆๆ ก", type: "noun", pronunciation: "skuหl"} + ] + }, + { + id: 23, + original: "Mrs. Williams promised to follow all the doctor's instructions very carefully until Jimmy feels completely better.", + translation: "ๅจๅป‰ๅง†ๆ–ฏๅคชๅคชๆ‰ฟ่ฏบไผš้žๅธธไป”็ป†ๅœฐ้ตๅพชๅŒป็”Ÿ็š„ๆ‰€ๆœ‰ๆŒ‡็คบ็›ดๅˆฐๅ‰็ฑณๅฎŒๅ…จๅบทๅคใ€‚", + words: [ + {word: "Mrs.", translation: "ๅคชๅคช", type: "title", pronunciation: "mษชsษชz"}, + {word: "Williams", translation: "ๅจๅป‰ๅง†ๆ–ฏ", type: "noun", pronunciation: "wษชljษ™mz"}, + {word: "promised", translation: "ๆ‰ฟ่ฏบ", type: "verb", pronunciation: "prษ‘หmษชst"}, + {word: "to", translation: "่ฆ", type: "preposition", pronunciation: "tuห"}, + {word: "follow", translation: "้ตๅพช", type: "verb", pronunciation: "fษ‘หloสŠ"}, + {word: "all", translation: "ๆ‰€ๆœ‰", type: "determiner", pronunciation: "ษ”หl"}, + {word: "the", translation: "่ฟ™ไบ›", type: "article", pronunciation: "รฐษ™"}, + {word: "doctor's", translation: "ๅŒป็”Ÿ็š„", type: "noun", pronunciation: "dษ”ktษ™rz"}, + {word: "instructions", translation: "ๆŒ‡็คบ", type: "noun", pronunciation: "ษชnหˆstrสŒkสƒษ™nz"}, + {word: "very", translation: "้žๅธธ", type: "adverb", pronunciation: "veri"}, + {word: "carefully", translation: "ไป”็ป†ๅœฐ", type: "adverb", pronunciation: "kษ›rfษ™li"}, + {word: "until", translation: "็›ดๅˆฐ", type: "conjunction", pronunciation: "ษ™nหˆtษชl"}, + {word: "Jimmy", translation: "ๅ‰็ฑณ", type: "noun", pronunciation: "dส’ษชmi"}, + {word: "feels", translation: "ๆ„Ÿ่ง‰", type: "verb", pronunciation: "fiหlz"}, + {word: "completely", translation: "ๅฎŒๅ…จ", type: "adverb", pronunciation: "kษ™mหˆpliหtli"}, + {word: "better", translation: "ๆ›ดๅฅฝ", type: "adjective", pronunciation: "betษ™r"} + ] + } + ] + } + ] + }, + + // === GRAMMAR-BASED FILL IN THE BLANKS === + fillInBlanks: [ + { + sentence: "You _____ stay in bed for two days.", + options: ["must", "mustn't", "can", "don't"], + correctAnswer: "must", + explanation: "Use 'must' for strong medical advice", + grammarFocus: "modal-must-mustnot" + }, + { + sentence: "He _____ eat rich food when sick.", + options: ["must", "mustn't", "should", "can"], + correctAnswer: "mustn't", + explanation: "Use 'mustn't' for medical restrictions", + grammarFocus: "modal-must-mustnot" + }, + { + sentence: "_____ take this medicine!", + options: ["Don't", "Not", "No", "Doesn't"], + correctAnswer: "Don't", + explanation: "Use 'Don't' for negative commands", + grammarFocus: "imperatives-commands" + }, + { + sentence: "_____ he have a temperature?", + options: ["Do", "Does", "Is", "Has"], + correctAnswer: "Does", + explanation: "Use 'Does' for questions with third person singular", + grammarFocus: "present-simple-questions" + }, + { + sentence: "_____ play with matches!", + options: ["Not", "Don't", "No", "Mustn't"], + correctAnswer: "Don't", + explanation: "Use 'Don't' for safety warnings", + grammarFocus: "imperatives-commands" + }, + { + sentence: "_____ Mr. Williams this evening?", + options: ["Where", "Where's", "Where are", "Where is"], + correctAnswer: "Where's", + explanation: "Use 'Where's' (Where is) for location questions", + grammarFocus: "present-simple-questions" + } + ], + + // === GRAMMAR CORRECTION EXERCISES === + corrections: [ + { + incorrect: "You don't must get up yet.", + correct: "You mustn't get up yet.", + explanation: "Use 'mustn't' not 'don't must' for prohibition", + grammarFocus: "modal-must-mustnot" + }, + { + incorrect: "Not take this medicine!", + correct: "Don't take this medicine!", + explanation: "Use 'Don't' + verb for negative commands", + grammarFocus: "imperatives-commands" + }, + { + incorrect: "He must not to eat rich food.", + correct: "He mustn't eat rich food.", + explanation: "Don't use 'to' after modal verbs", + grammarFocus: "modal-must-mustnot" + }, + { + incorrect: "Do he have a temperature?", + correct: "Does he have a temperature?", + explanation: "Use 'Does' with third person singular subjects", + grammarFocus: "present-simple-questions" + }, + { + incorrect: "You must to stay in bed.", + correct: "You must stay in bed.", + explanation: "Don't use 'to' after 'must'", + grammarFocus: "modal-must-mustnot" + } + ], + + // === ADDITIONAL READING STORIES === + additionalStories: [ + { + title: "Doctor's Advice - ๅŒป็”Ÿ็š„ๅปบ่ฎฎ", + totalSentences: 12, + chapters: [ + { + title: "Chapter 1: Following Medical Instructions - ้ตๅพชๅŒปๅ˜ฑ", + sentences: [ + { + id: 1, + original: "The doctor says Jimmy must rest.", + translation: "ๅŒป็”Ÿ่ฏดๅ‰็ฑณๅฟ…้กปไผ‘ๆฏใ€‚", + words: [ + {word: "The", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "doctor", translation: "ๅŒป็”Ÿ", type: "noun", pronunciation: "dษ”ktษ™r"}, + {word: "says", translation: "่ฏด", type: "verb", pronunciation: "sez"}, + {word: "Jimmy", translation: "ๅ‰็ฑณ", type: "noun", pronunciation: "dส’ษชmi"}, + {word: "must", translation: "ๅฟ…้กป", type: "modal", pronunciation: "mสŒst"}, + {word: "rest", translation: "ไผ‘ๆฏ", type: "verb", pronunciation: "rest"} + ] + }, + { + id: 2, + original: "He mustn't go outside yet.", + translation: "ไป–่ฟ˜ไธ่ƒฝๅ‡บ้—จใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "mustn't", translation: "ไธ่ƒฝ", type: "modal", pronunciation: "mสŒsnt"}, + {word: "go", translation: "ๅŽป", type: "verb", pronunciation: "gษ™สŠ"}, + {word: "outside", translation: "ๅค–้ข", type: "adverb", pronunciation: "aสŠtsaษชd"}, + {word: "yet", translation: "่ฟ˜", type: "adverb", pronunciation: "jet"} + ] + }, + { + id: 3, + original: "Mrs. Williams must keep him warm.", + translation: "ๅจๅป‰ๅง†ๆ–ฏๅคชๅคชๅฟ…้กป่ฎฉไป–ไฟๆš–ใ€‚", + words: [ + {word: "Mrs.", translation: "ๅคชๅคช", type: "title", pronunciation: "mษชsษชz"}, + {word: "Williams", translation: "ๅจๅป‰ๅง†ๆ–ฏ", type: "noun", pronunciation: "wษชljษ™mz"}, + {word: "must", translation: "ๅฟ…้กป", type: "modal", pronunciation: "mสŒst"}, + {word: "keep", translation: "ไฟๆŒ", type: "verb", pronunciation: "kip"}, + {word: "him", translation: "ไป–", type: "pronoun", pronunciation: "hษชm"}, + {word: "warm", translation: "ๆธฉๆš–", type: "adjective", pronunciation: "wษ”rm"} + ] + }, + { + id: 4, + original: "Don't give him cold drinks.", + translation: "ไธ่ฆ็ป™ไป–ๅ†ท้ฅฎใ€‚", + words: [ + {word: "Don't", translation: "ไธ่ฆ", type: "auxiliary", pronunciation: "dษ™สŠnt"}, + {word: "give", translation: "็ป™", type: "verb", pronunciation: "gษชv"}, + {word: "him", translation: "ไป–", type: "pronoun", pronunciation: "hษชm"}, + {word: "cold", translation: "ๅ†ท็š„", type: "adjective", pronunciation: "kษ™สŠld"}, + {word: "drinks", translation: "้ฅฎๆ–™", type: "noun", pronunciation: "drษชล‹ks"} + ] + } + ] + } + ] + } + ] +}; + +// ============================================================================ +// CONTENT STRUCTURE SUMMARY - FOR AI REFERENCE +// ============================================================================ +// This module contains: +// - Medical dialogue vocabulary and situations +// - Modal verbs (must/mustn't) for obligations and prohibitions +// - Imperative commands for instructions and warnings +// - Present simple questions for medical inquiries +// - Safety-focused vocabulary and scenarios +// - Chinese translations for all content +// - Comprehensive grammar explanations and examples +// ============================================================================ \ No newline at end of file diff --git a/src/content/NCE2-Lesson3.js b/src/content/NCE2-Lesson3.js new file mode 100644 index 0000000..87c91fb --- /dev/null +++ b/src/content/NCE2-Lesson3.js @@ -0,0 +1,1134 @@ +// === ENGLISH PAST TENSE AND TRAVEL STORY === +// Complete English story with Chinese translation and pronunciation + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.NCE2Lesson3 = { + id: "nce2-lesson3", + name: "NCE2-Lesson3 - Please Send Me a Card", + description: "English learning story focusing on past tense verbs and travel vocabulary", + difficulty: "intermediate", + language: "en-US", + userLanguage: "zh-CN", + totalWords: 180, + + // === GRAMMAR LESSONS SYSTEM === + grammar: { + "past-tense-verbs": { + title: "Past Tense Verbs - ่ฟ‡ๅŽปๆ—ถๅŠจ่ฏ", + explanation: "English uses past tense verbs to talk about things that happened before now.", + rules: [ + "Regular verbs add -ed: visit โ†’ visited, pass โ†’ passed", + "Irregular verbs change form: go โ†’ went, think โ†’ thought", + "Past tense is the same for all persons: I went, he went, they went", + "Use past tense for completed actions in the past" + ], + examples: [ + { + english: "I went to Italy last summer.", + chinese: "ๅŽปๅนดๅคๅคฉๆˆ‘ๅŽปไบ†ๆ„ๅคงๅˆฉใ€‚", + explanation: "Use past tense for completed actions with time expressions", + pronunciation: "aษช went tuห หˆษชtษ™li lรฆst หˆsสŒmษ™r" + }, + { + english: "I visited museums and sat in gardens.", + chinese: "ๆˆ‘ๅ‚่ง‚ไบ†ๅš็‰ฉ้ฆ†๏ผŒๅๅœจ่Šฑๅ›ญ้‡Œใ€‚", + explanation: "Multiple past actions can be connected with 'and'", + pronunciation: "aษช หˆvษชzษชtษชd mjuหหˆziหษ™mz รฆnd sรฆt ษชn หˆษกษ‘หrdษ™nz" + }, + { + english: "A waiter taught me Italian words.", + chinese: "ไธ€ไธชๆœๅŠกๅ‘˜ๆ•™ไบ†ๆˆ‘ๆ„ๅคงๅˆฉ่ฏญๅ•่ฏใ€‚", + explanation: "Irregular past tense: teach โ†’ taught", + pronunciation: "ษ™ หˆweษชtษ™r tษ”หt miห ษชหˆtรฆljษ™n wษœหrdz" + }, + { + english: "I did not understand a word.", + chinese: "ๆˆ‘ไธ€ไธช่ฏ้ƒฝไธๆ‡‚ใ€‚", + explanation: "Negative past tense uses 'did not' + base verb", + pronunciation: "aษช dษชd nษ‘หt หŒสŒndษ™rหˆstรฆnd ษ™ wษœหrd" + }, + { + english: "He lent me a book.", + chinese: "ไป–ๅ€Ÿ็ป™ๆˆ‘ไธ€ๆœฌไนฆใ€‚", + explanation: "Irregular past tense: lend โ†’ lent", + pronunciation: "hiห lent miห ษ™ bสŠk" + }, + { + english: "I bought thirty-seven cards.", + chinese: "ๆˆ‘ไนฐไบ†ไธ‰ๅไธƒๅผ ๅก็‰‡ใ€‚", + explanation: "Irregular past tense: buy โ†’ bought", + pronunciation: "aษช bษ”หt หˆฮธษœหrti หˆsevษ™n kษ‘หrdz" + }, + { + english: "I spent the whole day in my room.", + chinese: "ๆˆ‘ๅœจๆˆฟ้—ด้‡Œๅพ…ไบ†ไธ€ๆ•ดๅคฉใ€‚", + explanation: "Irregular past tense: spend โ†’ spent", + pronunciation: "aษช spent รฐษ™ hoสŠl deษช ษชn maษช ruหm" + }, + { + english: "I thought about postcards every day.", + chinese: "ๆˆ‘ๆฏๅคฉ้ƒฝๆƒณ็€ๆ˜Žไฟก็‰‡ใ€‚", + explanation: "Irregular past tense: think โ†’ thought", + pronunciation: "aษช ฮธษ”หt ษ™หˆbaสŠt หˆpoสŠstkษ‘หrdz หˆevri deษช" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "Last summer, I _____ to Italy.", + options: ["go", "went", "gone", "going"], + correct: "went", + explanation: "Use 'went' (past tense of 'go') for completed past actions" + }, + { + type: "translation", + english: "I did not write a single card.", + chinese: "ๆˆ‘ไธ€ๅผ ๅก็‰‡้ƒฝๆฒกๅ†™ใ€‚", + focus: "Negative past tense structure" + } + ] + }, + + "direct-indirect-objects": { + title: "Direct and Indirect Objects - ็›ดๆŽฅๅฎพ่ฏญๅ’Œ้—ดๆŽฅๅฎพ่ฏญ", + explanation: "English can express the same meaning using two different sentence patterns with give/send/lend verbs.", + rules: [ + "Pattern 1: Subject + Verb + Indirect Object + Direct Object", + "Pattern 2: Subject + Verb + Direct Object + to/for + Indirect Object", + "Use 'to' with: give, send, lend, pass, hand, show", + "Use 'for' with: buy, make, find, get" + ], + examples: [ + { + english: "He lent me a book.", + chinese: "ไป–ๅ€Ÿ็ป™ๆˆ‘ไธ€ๆœฌไนฆใ€‚", + explanation: "Pattern 1: Verb + me (indirect) + book (direct)", + pronunciation: "hiห lent miห ษ™ bสŠk" + }, + { + english: "He lent a book to me.", + chinese: "ไป–ๅ€Ÿไบ†ไธ€ๆœฌไนฆ็ป™ๆˆ‘ใ€‚", + explanation: "Pattern 2: Verb + book (direct) + to me (indirect)", + pronunciation: "hiห lent ษ™ bสŠk tuห miห" + }, + { + english: "He sent me a card.", + chinese: "ไป–ๅฏ„็ป™ๆˆ‘ไธ€ๅผ ๅก็‰‡ใ€‚", + explanation: "Pattern 1 with 'send'", + pronunciation: "hiห sent miห ษ™ kษ‘หrd" + }, + { + english: "He sent a card to me.", + chinese: "ไป–ๅฏ„ไบ†ไธ€ๅผ ๅก็‰‡็ป™ๆˆ‘ใ€‚", + explanation: "Pattern 2 with 'send' + 'to'", + pronunciation: "hiห sent ษ™ kษ‘หrd tuห miห" + }, + { + english: "She bought me a gift.", + chinese: "ๅฅน็ป™ๆˆ‘ไนฐไบ†ไธ€ไปฝ็คผ็‰ฉใ€‚", + explanation: "Pattern 1 with 'buy'", + pronunciation: "สƒiห bษ”หt miห ษ™ ษกษชft" + }, + { + english: "She bought a gift for me.", + chinese: "ๅฅนไธบๆˆ‘ไนฐไบ†ไธ€ไปฝ็คผ็‰ฉใ€‚", + explanation: "Pattern 2 with 'buy' + 'for'", + pronunciation: "สƒiห bษ”หt ษ™ ษกษชft fษ”หr miห" + }, + { + english: "The waiter brought me the menu.", + chinese: "ๆœๅŠกๅ‘˜็ป™ๆˆ‘ๆ‹ฟๆฅไบ†่œๅ•ใ€‚", + explanation: "Pattern 1: brought + me + menu", + pronunciation: "รฐษ™ หˆweษชtษ™r brษ”หt miห รฐษ™ หˆmenjuห" + }, + { + english: "Pass the salt to me, please.", + chinese: "่ฏทๆŠŠ็›ไผ ็ป™ๆˆ‘ใ€‚", + explanation: "Pattern 2: pass + salt + to me", + pronunciation: "pรฆs รฐษ™ sษ”หlt tuห miห pliหz" + } + ], + exercises: [ + { + type: "transformation", + sentence: "He gave me the book.", + answer: "He gave the book to me.", + explanation: "Transform Pattern 1 to Pattern 2 using 'to'" + } + ] + }, + + "question-formation": { + title: "Question Formation in Past Tense - ่ฟ‡ๅŽปๆ—ถ็–‘้—ฎๅฅ", + explanation: "English forms past tense questions using 'did' + subject + base verb.", + rules: [ + "Did + subject + base verb + object?", + "Question words: What did...? Where did...? Why did...?", + "Answer with 'Yes, I did' or 'No, I didn't'", + "Don't use past tense verb after 'did'" + ], + examples: [ + { + english: "Did you see the accident?", + chinese: "ไฝ ็œ‹ๅˆฐไบ‹ๆ•…ไบ†ๅ—๏ผŸ", + explanation: "Yes/No question with 'did'", + pronunciation: "dษชd juห siห รฐi หˆรฆksษ™dษ™nt" + }, + { + english: "What happened?", + chinese: "ๅ‘็”Ÿไบ†ไป€ไนˆ๏ผŸ", + explanation: "Question word + past tense verb (no 'did' needed)", + pronunciation: "wสŒt หˆhรฆpษ™nd" + }, + { + english: "Where did he go last summer?", + chinese: "ๅŽปๅนดๅคๅคฉไป–ๅŽปๅ“ช้‡Œไบ†๏ผŸ", + explanation: "Question word + did + subject + base verb", + pronunciation: "wer dษชd hiห ษกoสŠ lรฆst หˆsสŒmษ™r" + }, + { + english: "Why did you do that?", + chinese: "ไฝ ไธบไป€ไนˆ้‚ฃๆ ทๅš๏ผŸ", + explanation: "'Why' questions ask for reasons", + pronunciation: "waษช dษชd juห duห รฐรฆt" + }, + { + english: "How many cards did he buy?", + chinese: "ไป–ไนฐไบ†ๅคšๅฐ‘ๅผ ๅก็‰‡๏ผŸ", + explanation: "Questions about quantity", + pronunciation: "haสŠ หˆmeni kษ‘หrdz dษชd hiห baษช" + }, + { + english: "Who taught you Italian?", + chinese: "่ฐๆ•™ไฝ ๆ„ๅคงๅˆฉ่ฏญ๏ผŸ", + explanation: "When 'who' is the subject, no 'did' needed", + pronunciation: "huห tษ”หt juห ษชหˆtรฆljษ™n" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "_____ you write any postcards?", + options: ["Did", "Do", "Are", "Were"], + correct: "Did", + explanation: "Use 'Did' for past tense yes/no questions" + } + ] + } + }, + + vocabulary: { + "postcard": { + "user_language": "ๆ˜Žไฟก็‰‡", + "type": "noun", + "pronunciation": "หˆpoสŠstkษ‘หrd" + }, + "send": { + "user_language": "ๅฏ„๏ผŒ้€", + "type": "verb", + "pronunciation": "send" + }, + "spoil": { + "user_language": "ๆŸๅ๏ผŒ็ ดๅ", + "type": "verb", + "pronunciation": "spษ”ษชl" + }, + "holiday": { + "user_language": "ๅ‡ๆœŸ", + "type": "noun", + "pronunciation": "หˆhษ‘หlษ™deษช" + }, + "museum": { + "user_language": "ๅš็‰ฉ้ฆ†", + "type": "noun", + "pronunciation": "mjuหหˆziหษ™m" + }, + "public": { + "user_language": "ๅ…ฌๅ…ฑ็š„", + "type": "adjective", + "pronunciation": "หˆpสŒblษชk" + }, + "garden": { + "user_language": "่Šฑๅ›ญ", + "type": "noun", + "pronunciation": "หˆษกษ‘หrdษ™n" + }, + "friendly": { + "user_language": "ๅ‹ๅฅฝ็š„", + "type": "adjective", + "pronunciation": "หˆfrendli" + }, + "waiter": { + "user_language": "ๆœๅŠกๅ‘˜", + "type": "noun", + "pronunciation": "หˆweษชtษ™r" + }, + "teach": { + "user_language": "ๆ•™", + "type": "verb", + "pronunciation": "tiหtสƒ" + }, + "lend": { + "user_language": "ๅ€Ÿ็ป™", + "type": "verb", + "pronunciation": "lend" + }, + "understand": { + "user_language": "็†่งฃ", + "type": "verb", + "pronunciation": "หŒสŒndษ™rหˆstรฆnd" + }, + "think": { + "user_language": "ๆƒณ๏ผŒๆ€่€ƒ", + "type": "verb", + "pronunciation": "ฮธษชล‹k" + }, + "pass": { + "user_language": "่ฟ‡ๅŽป๏ผŒ้€š่ฟ‡", + "type": "verb", + "pronunciation": "pรฆs" + }, + "quickly": { + "user_language": "ๅฟซ้€Ÿๅœฐ", + "type": "adverb", + "pronunciation": "หˆkwษชkli" + }, + "decision": { + "user_language": "ๅ†ณๅฎš", + "type": "noun", + "pronunciation": "dษชหˆsษชส’ษ™n" + }, + "early": { + "user_language": "ๆ—ฉ", + "type": "adverb", + "pronunciation": "หˆษœหrli" + }, + "buy": { + "user_language": "ไนฐ", + "type": "verb", + "pronunciation": "baษช" + }, + "spend": { + "user_language": "่Šฑ่ดน๏ผŒๅบฆ่ฟ‡", + "type": "verb", + "pronunciation": "spend" + }, + "whole": { + "user_language": "ๆ•ดไธช็š„", + "type": "adjective", + "pronunciation": "hoสŠl" + }, + "single": { + "user_language": "ๅ•ไธ€็š„๏ผŒๅ”ฏไธ€็š„", + "type": "adjective", + "pronunciation": "หˆsษชล‹ษกษ™l" + }, + "write": { + "user_language": "ๅ†™", + "type": "verb", + "pronunciation": "raษชt" + }, + "visit": { + "user_language": "ๅ‚่ง‚", + "type": "verb", + "pronunciation": "หˆvษชzษชt" + }, + "sit": { + "user_language": "ๅ", + "type": "verb", + "pronunciation": "sษชt" + }, + "read": { + "user_language": "่ฏป", + "type": "verb", + "pronunciation": "riหd" + }, + "word": { + "user_language": "ๅ•่ฏ", + "type": "noun", + "pronunciation": "wษœหrd" + }, + "line": { + "user_language": "่กŒ๏ผŒ็บฟ", + "type": "noun", + "pronunciation": "laษชn" + }, + "friend": { + "user_language": "ๆœ‹ๅ‹", + "type": "noun", + "pronunciation": "frend" + }, + "room": { + "user_language": "ๆˆฟ้—ด", + "type": "noun", + "pronunciation": "ruหm" + }, + "card": { + "user_language": "ๅก็‰‡", + "type": "noun", + "pronunciation": "kษ‘หrd" + } + }, + + // === SENTENCES FOR GAMES (extracted from stories) === + sentences: [ + { + english: "Postcards always spoil my holidays.", + chinese: "ๆ˜Žไฟก็‰‡ๆ€ปๆ˜ฏ็ ดๅๆˆ‘็š„ๅ‡ๆœŸใ€‚", + prononciation: "หˆpoสŠstkษ‘หrdz หˆษ”หlweษชz spษ”ษชl maษช หˆhษ‘หlษ™deษชz" + }, + { + english: "Last summer, I went to Italy.", + chinese: "ๅŽปๅนดๅคๅคฉ๏ผŒๆˆ‘ๅŽปไบ†ๆ„ๅคงๅˆฉใ€‚", + prononciation: "lรฆst หˆsสŒmษ™r aษช wษ›nt tuห หˆษชtษ™li" + }, + { + english: "I visited museums and sat in public gardens.", + chinese: "ๆˆ‘ๅ‚่ง‚ไบ†ๅš็‰ฉ้ฆ†๏ผŒๅๅœจๅ…ฌๅ›ญ้‡Œใ€‚", + prononciation: "aษช หˆvษชzษชtษชd mjuหˆziษ™mz รฆnd sรฆt ษชn หˆpสŒblษชk หˆgษ‘หrdษ™nz" + }, + { + english: "I thought about postcards every day.", + chinese: "ๆˆ‘ๆฏๅคฉ้ƒฝๆƒณ็€ๆ˜Žไฟก็‰‡ใ€‚", + prononciation: "aษช ฮธษ”หt ษ™หˆbaสŠt หˆpoสŠstkษ‘หrdz หˆษ›vri deษช" + }, + { + english: "I did not send cards to my friends.", + chinese: "ๆˆ‘ๆฒกๆœ‰็ป™ๆœ‹ๅ‹ๅฏ„ๆ˜Žไฟก็‰‡ใ€‚", + prononciation: "aษช dษชd nษ‘หt sษ›nd kษ‘หrdz tuห maษช frษ›ndz" + }, + { + english: "On the last day I made a big decision.", + chinese: "ๅœจๆœ€ๅŽไธ€ๅคฉๆˆ‘ๅšไบ†ไธ€ไธช้‡ๅคงๅ†ณๅฎšใ€‚", + prononciation: "ษ‘หn รฐษ™ lรฆst deษช aษช meษชd ษ™ bษชg dษชหˆsษชส’ษ™n" + }, + { + english: "I got up early and bought thirty-seven cards.", + chinese: "ๆˆ‘ๆ—ฉๆ—ฉ่ตทๅบŠไนฐไบ†ไธ‰ๅไธƒๅผ ๆ˜Žไฟก็‰‡ใ€‚", + prononciation: "aษช gษ‘หt สŒp หˆษœหrli รฆnd bษ”หt หˆฮธษœหrti หˆsษ›vษ™n kษ‘หrdz" + }, + { + english: "I spent the whole day in my room.", + chinese: "ๆˆ‘ๅœจๆˆฟ้—ด้‡Œๅพ…ไบ†ไธ€ๆ•ดๅคฉใ€‚", + prononciation: "aษช spษ›nt รฐษ™ hoสŠl deษช ษชn maษช ruหm" + }, + { + english: "I wrote one card to myself.", + chinese: "ๆˆ‘็ป™่‡ชๅทฑๅ†™ไบ†ไธ€ๅผ ๆ˜Žไฟก็‰‡ใ€‚", + prononciation: "aษช roสŠt wสŒn kษ‘หrd tuห maษชหˆsษ›lf" + }, + { + english: "My holidays passed quickly but I did not send any cards.", + chinese: "ๆˆ‘็š„ๅ‡ๆœŸ่ฟ‡ๅพ—ๅพˆๅฟซ๏ผŒไฝ†ๆˆ‘ๆฒกๆœ‰ๅฏ„ไปปไฝ•ๆ˜Žไฟก็‰‡ใ€‚", + prononciation: "maษช หˆhษ‘หlษ™deษชz pรฆst หˆkwษชkli bสŒt aษช dษชd nษ‘หt sษ›nd หˆษ›ni kษ‘หrdz" + } + ], + + story: { + title: "Please Send Me a Card - ่ฏท็ป™ๆˆ‘ๅฏ„ไธ€ๅผ ๆ˜Žไฟก็‰‡", + totalSentences: 16, + chapters: [ + { + title: "Chapter 1: Holiday Plans and Problems - ๅ‡ๆœŸ่ฎกๅˆ’ๅ’Œ้—ฎ้ข˜", + sentences: [ + { + id: 1, + original: "Postcards always spoil my holidays.", + translation: "ๆ˜Žไฟก็‰‡ๆ€ปๆ˜ฏ็ ดๅๆˆ‘็š„ๅ‡ๆœŸใ€‚", + words: [ + {word: "Postcards", translation: "ๆ˜Žไฟก็‰‡", type: "noun", pronunciation: "หˆpoสŠstkษ‘หrdz"}, + {word: "always", translation: "ๆ€ปๆ˜ฏ", type: "adverb", pronunciation: "หˆษ”หlweษชz"}, + {word: "spoil", translation: "็ ดๅ", type: "verb", pronunciation: "spษ”ษชl"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "holidays", translation: "ๅ‡ๆœŸ", type: "noun", pronunciation: "หˆhษ‘หlษ™deษชz"} + ] + }, + { + id: 2, + original: "Last summer, I went to Italy.", + translation: "ๅŽปๅนดๅคๅคฉ๏ผŒๆˆ‘ๅŽปไบ†ๆ„ๅคงๅˆฉใ€‚", + words: [ + {word: "Last", translation: "ๅŽปๅนด", type: "adjective", pronunciation: "lรฆst"}, + {word: "summer", translation: "ๅคๅคฉ", type: "noun", pronunciation: "หˆsสŒmษ™r"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "went", translation: "ๅŽปไบ†", type: "verb", pronunciation: "went"}, + {word: "to", translation: "ๅˆฐ", type: "preposition", pronunciation: "tuห"}, + {word: "Italy", translation: "ๆ„ๅคงๅˆฉ", type: "noun", pronunciation: "หˆษชtษ™li"} + ] + }, + { + id: 3, + original: "I visited museums and sat in public gardens.", + translation: "ๆˆ‘ๅ‚่ง‚ไบ†ๅš็‰ฉ้ฆ†๏ผŒๅๅœจๅ…ฌๅ…ฑ่Šฑๅ›ญ้‡Œใ€‚", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "visited", translation: "ๅ‚่ง‚ไบ†", type: "verb", pronunciation: "หˆvษชzษชtษชd"}, + {word: "museums", translation: "ๅš็‰ฉ้ฆ†", type: "noun", pronunciation: "mjuหหˆziหษ™mz"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "sat", translation: "ๅ", type: "verb", pronunciation: "sรฆt"}, + {word: "in", translation: "ๅœจ", type: "preposition", pronunciation: "ษชn"}, + {word: "public", translation: "ๅ…ฌๅ…ฑ็š„", type: "adjective", pronunciation: "หˆpสŒblษชk"}, + {word: "gardens", translation: "่Šฑๅ›ญ", type: "noun", pronunciation: "หˆษกษ‘หrdษ™nz"} + ] + }, + { + id: 4, + original: "A friendly waiter taught me a few words of Italian.", + translation: "ไธ€ไธชๅ‹ๅฅฝ็š„ๆœๅŠกๅ‘˜ๆ•™ไบ†ๆˆ‘ๅ‡ ไธชๆ„ๅคงๅˆฉ่ฏญๅ•่ฏใ€‚", + words: [ + {word: "A", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "friendly", translation: "ๅ‹ๅฅฝ็š„", type: "adjective", pronunciation: "หˆfrendli"}, + {word: "waiter", translation: "ๆœๅŠกๅ‘˜", type: "noun", pronunciation: "หˆweษชtษ™r"}, + {word: "taught", translation: "ๆ•™ไบ†", type: "verb", pronunciation: "tษ”หt"}, + {word: "me", translation: "ๆˆ‘", type: "pronoun", pronunciation: "miห"}, + {word: "a few", translation: "ๅ‡ ไธช", type: "determiner", pronunciation: "ษ™ fjuห"}, + {word: "words", translation: "ๅ•่ฏ", type: "noun", pronunciation: "wษœหrdz"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "Italian", translation: "ๆ„ๅคงๅˆฉ่ฏญ", type: "noun", pronunciation: "ษชหˆtรฆljษ™n"} + ] + }, + { + id: 5, + original: "Then he lent me a book.", + translation: "็„ถๅŽไป–ๅ€Ÿ็ป™ๆˆ‘ไธ€ๆœฌไนฆใ€‚", + words: [ + {word: "Then", translation: "็„ถๅŽ", type: "adverb", pronunciation: "รฐen"}, + {word: "he", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "lent", translation: "ๅ€Ÿ็ป™", type: "verb", pronunciation: "lent"}, + {word: "me", translation: "ๆˆ‘", type: "pronoun", pronunciation: "miห"}, + {word: "a", translation: "ไธ€ๆœฌ", type: "article", pronunciation: "ษ™"}, + {word: "book", translation: "ไนฆ", type: "noun", pronunciation: "bสŠk"} + ] + }, + { + id: 6, + original: "I read a few lines, but I did not understand a word.", + translation: "ๆˆ‘่ฏปไบ†ๅ‡ ่กŒ๏ผŒไฝ†ไธ€ไธช่ฏ้ƒฝไธๆ‡‚ใ€‚", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "read", translation: "่ฏปไบ†", type: "verb", pronunciation: "red"}, + {word: "a few", translation: "ๅ‡ ", type: "determiner", pronunciation: "ษ™ fjuห"}, + {word: "lines", translation: "่กŒ", type: "noun", pronunciation: "laษชnz"}, + {word: "but", translation: "ไฝ†ๆ˜ฏ", type: "conjunction", pronunciation: "bสŒt"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "did not", translation: "ๆฒกๆœ‰", type: "auxiliary", pronunciation: "dษชd nษ‘หt"}, + {word: "understand", translation: "็†่งฃ", type: "verb", pronunciation: "หŒสŒndษ™rหˆstรฆnd"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "word", translation: "่ฏ", type: "noun", pronunciation: "wษœหrd"} + ] + }, + { + id: 7, + original: "Every day I thought about postcards.", + translation: "ๆฏๅคฉๆˆ‘้ƒฝๆƒณ็€ๆ˜Žไฟก็‰‡ใ€‚", + words: [ + {word: "Every", translation: "ๆฏ", type: "adjective", pronunciation: "หˆevri"}, + {word: "day", translation: "ๅคฉ", type: "noun", pronunciation: "deษช"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "thought", translation: "ๆƒณ", type: "verb", pronunciation: "ฮธษ”หt"}, + {word: "about", translation: "ๅ…ณไบŽ", type: "preposition", pronunciation: "ษ™หˆbaสŠt"}, + {word: "postcards", translation: "ๆ˜Žไฟก็‰‡", type: "noun", pronunciation: "หˆpoสŠstkษ‘หrdz"} + ] + }, + { + id: 8, + original: "My holidays passed quickly, but I did not send cards to my friends.", + translation: "ๆˆ‘็š„ๅ‡ๆœŸ่ฟ‡ๅพ—ๅพˆๅฟซ๏ผŒไฝ†ๆˆ‘ๆฒกๆœ‰็ป™ๆœ‹ๅ‹ไปฌๅฏ„ๅก็‰‡ใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "holidays", translation: "ๅ‡ๆœŸ", type: "noun", pronunciation: "หˆhษ‘หlษ™deษชz"}, + {word: "passed", translation: "่ฟ‡ๅŽปไบ†", type: "verb", pronunciation: "pรฆst"}, + {word: "quickly", translation: "ๅฟซ้€Ÿๅœฐ", type: "adverb", pronunciation: "หˆkwษชkli"}, + {word: "but", translation: "ไฝ†ๆ˜ฏ", type: "conjunction", pronunciation: "bสŒt"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "did not", translation: "ๆฒกๆœ‰", type: "auxiliary", pronunciation: "dษชd nษ‘หt"}, + {word: "send", translation: "ๅฏ„", type: "verb", pronunciation: "send"}, + {word: "cards", translation: "ๅก็‰‡", type: "noun", pronunciation: "kษ‘หrdz"}, + {word: "to", translation: "็ป™", type: "preposition", pronunciation: "tuห"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "friends", translation: "ๆœ‹ๅ‹ไปฌ", type: "noun", pronunciation: "frends"} + ] + } + ] + }, + { + title: "Chapter 2: The Big Decision - ้‡ๅคงๅ†ณๅฎš", + sentences: [ + { + id: 9, + original: "On the last day I made a big decision.", + translation: "ๅœจๆœ€ๅŽไธ€ๅคฉ๏ผŒๆˆ‘ๅšไบ†ไธ€ไธช้‡ๅคงๅ†ณๅฎšใ€‚", + words: [ + {word: "On", translation: "ๅœจ", type: "preposition", pronunciation: "ษ‘หn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "last", translation: "ๆœ€ๅŽ็š„", type: "adjective", pronunciation: "lรฆst"}, + {word: "day", translation: "ๅคฉ", type: "noun", pronunciation: "deษช"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "made", translation: "ๅšไบ†", type: "verb", pronunciation: "meษชd"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "big", translation: "้‡ๅคง็š„", type: "adjective", pronunciation: "bษชษก"}, + {word: "decision", translation: "ๅ†ณๅฎš", type: "noun", pronunciation: "dษชหˆsษชส’ษ™n"} + ] + }, + { + id: 10, + original: "I got up early and bought thirty-seven cards.", + translation: "ๆˆ‘ๆ—ฉๆ—ฉ่ตทๅบŠ๏ผŒไนฐไบ†ไธ‰ๅไธƒๅผ ๅก็‰‡ใ€‚", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "got up", translation: "่ตทๅบŠ", type: "phrasal verb", pronunciation: "ษกษ‘หt สŒp"}, + {word: "early", translation: "ๆ—ฉ", type: "adverb", pronunciation: "หˆษœหrli"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "bought", translation: "ไนฐไบ†", type: "verb", pronunciation: "bษ”หt"}, + {word: "thirty-seven", translation: "ไธ‰ๅไธƒ", type: "number", pronunciation: "หˆฮธษœหrti หˆsevษ™n"}, + {word: "cards", translation: "ๅก็‰‡", type: "noun", pronunciation: "kษ‘หrdz"} + ] + }, + { + id: 11, + original: "I spent the whole day in my room, but I did not write a single card!", + translation: "ๆˆ‘ๅœจๆˆฟ้—ด้‡Œๅพ…ไบ†ไธ€ๆ•ดๅคฉ๏ผŒไฝ†่ฟžไธ€ๅผ ๅก็‰‡้ƒฝๆฒกๅ†™๏ผ", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "spent", translation: "ๅบฆ่ฟ‡", type: "verb", pronunciation: "spent"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "whole", translation: "ๆ•ดไธช", type: "adjective", pronunciation: "hoสŠl"}, + {word: "day", translation: "ๅคฉ", type: "noun", pronunciation: "deษช"}, + {word: "in", translation: "ๅœจ", type: "preposition", pronunciation: "ษชn"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "room", translation: "ๆˆฟ้—ด", type: "noun", pronunciation: "ruหm"}, + {word: "but", translation: "ไฝ†ๆ˜ฏ", type: "conjunction", pronunciation: "bสŒt"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "did not", translation: "ๆฒกๆœ‰", type: "auxiliary", pronunciation: "dษชd nษ‘หt"}, + {word: "write", translation: "ๅ†™", type: "verb", pronunciation: "raษชt"}, + {word: "a", translation: "ไธ€ๅผ ", type: "article", pronunciation: "ษ™"}, + {word: "single", translation: "ๅ•ไธ€็š„", type: "adjective", pronunciation: "หˆsษชล‹ษกษ™l"}, + {word: "card", translation: "ๅก็‰‡", type: "noun", pronunciation: "kษ‘หrd"} + ] + } + ] + } + ] + }, + + // === GRAMMAR-BASED FILL IN THE BLANKS === + fillInBlanks: [ + { + sentence: "Last summer, I _____ to Italy.", + options: ["go", "went", "gone", "going"], + correctAnswer: "went", + explanation: "Use 'went' (past tense of 'go') for completed past actions", + grammarFocus: "past-tense-verbs" + }, + { + sentence: "A waiter _____ me some Italian words.", + options: ["teach", "taught", "teaching", "teaches"], + correctAnswer: "taught", + explanation: "Use 'taught' (past tense of 'teach') for past actions", + grammarFocus: "past-tense-verbs" + }, + { + sentence: "I _____ not understand a word.", + options: ["do", "did", "does", "done"], + correctAnswer: "did", + explanation: "Use 'did' for negative past tense sentences", + grammarFocus: "past-tense-verbs" + }, + { + sentence: "He _____ me a book.", + options: ["lent", "lend", "lending", "lends"], + correctAnswer: "lent", + explanation: "Use 'lent' (past tense of 'lend') for completed past actions", + grammarFocus: "past-tense-verbs" + }, + { + sentence: "I _____ thirty-seven cards.", + options: ["buy", "bought", "buying", "buys"], + correctAnswer: "bought", + explanation: "Use 'bought' (past tense of 'buy') for past actions", + grammarFocus: "past-tense-verbs" + }, + { + sentence: "He lent a book _____ me.", + options: ["for", "to", "at", "in"], + correctAnswer: "to", + explanation: "Use 'to' with verbs like lend, send, give in Pattern 2", + grammarFocus: "direct-indirect-objects" + }, + { + sentence: "She bought a gift _____ me.", + options: ["for", "to", "at", "in"], + correctAnswer: "for", + explanation: "Use 'for' with verbs like buy, make, find in Pattern 2", + grammarFocus: "direct-indirect-objects" + }, + { + sentence: "_____ you visit any museums?", + options: ["Do", "Did", "Are", "Were"], + correctAnswer: "Did", + explanation: "Use 'Did' for past tense yes/no questions", + grammarFocus: "question-formation" + }, + { + sentence: "_____ happened yesterday?", + options: ["What", "Where", "When", "Why"], + correctAnswer: "What", + explanation: "Use 'What' to ask about events or things that happened", + grammarFocus: "question-formation" + }, + { + sentence: "_____ did you go last summer?", + options: ["What", "Where", "When", "Why"], + correctAnswer: "Where", + explanation: "Use 'Where' to ask about places", + grammarFocus: "question-formation" + } + ], + + // === GRAMMAR CORRECTION EXERCISES === + corrections: [ + { + incorrect: "I goed to Italy last summer.", + correct: "I went to Italy last summer.", + explanation: "Use 'went' (irregular past tense) not 'goed'", + grammarFocus: "past-tense-verbs" + }, + { + incorrect: "He teached me Italian words.", + correct: "He taught me Italian words.", + explanation: "Use 'taught' (irregular past tense) not 'teached'", + grammarFocus: "past-tense-verbs" + }, + { + incorrect: "I don't understand a word.", + correct: "I didn't understand a word.", + explanation: "Use past tense 'didn't' for past events, not present 'don't'", + grammarFocus: "past-tense-verbs" + }, + { + incorrect: "I buyed thirty-seven cards.", + correct: "I bought thirty-seven cards.", + explanation: "Use 'bought' (irregular past tense) not 'buyed'", + grammarFocus: "past-tense-verbs" + }, + { + incorrect: "He lent a book for me.", + correct: "He lent a book to me.", + explanation: "Use 'to' with 'lend', not 'for'", + grammarFocus: "direct-indirect-objects" + }, + { + incorrect: "She bought a gift to me.", + correct: "She bought a gift for me.", + explanation: "Use 'for' with 'buy', not 'to'", + grammarFocus: "direct-indirect-objects" + }, + { + incorrect: "Where you went last summer?", + correct: "Where did you go last summer?", + explanation: "Use 'did' + base verb for past tense questions", + grammarFocus: "question-formation" + }, + { + incorrect: "What you did yesterday?", + correct: "What did you do yesterday?", + explanation: "Use 'did' + base verb for past tense questions", + grammarFocus: "question-formation" + } + ], + + // === ADDITIONAL READING STORIES === + additionalStories: [ + { + title: "The Accident Story - ไบ‹ๆ•…ๆ•…ไบ‹", + totalSentences: 12, + chapters: [ + { + title: "Chapter 1: What Happened? - ๅ‘็”Ÿไบ†ไป€ไนˆ๏ผŸ", + sentences: [ + { + id: 1, + original: "Did you see the accident, sir?", + translation: "ๅ…ˆ็”Ÿ๏ผŒไฝ ็œ‹ๅˆฐ้‚ฃไธชไบ‹ๆ•…ไบ†ๅ—๏ผŸ", + words: [ + {word: "Did", translation: "...ๅ—", type: "auxiliary", pronunciation: "dษชd"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "juห"}, + {word: "see", translation: "็œ‹ๅˆฐ", type: "verb", pronunciation: "siห"}, + {word: "the", translation: "้‚ฃไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "accident", translation: "ไบ‹ๆ•…", type: "noun", pronunciation: "หˆรฆksษ™dษ™nt"}, + {word: "sir", translation: "ๅ…ˆ็”Ÿ", type: "noun", pronunciation: "sษœหr"} + ] + }, + { + id: 2, + original: "Yes, I did. The driver of that car hit that post over there.", + translation: "ๆ˜ฏ็š„๏ผŒๆˆ‘็œ‹ๅˆฐไบ†ใ€‚้‚ฃ่พ†่ฝฆ็š„ๅธๆœบๆ’žๅˆฐไบ†้‚ฃ่พน็š„ๆŸฑๅญใ€‚", + words: [ + {word: "Yes", translation: "ๆ˜ฏ็š„", type: "interjection", pronunciation: "jes"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "did", translation: "็œ‹ๅˆฐไบ†", type: "auxiliary", pronunciation: "dษชd"}, + {word: "The", translation: "้‚ฃ", type: "article", pronunciation: "รฐษ™"}, + {word: "driver", translation: "ๅธๆœบ", type: "noun", pronunciation: "หˆdraษชvษ™r"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "that", translation: "้‚ฃ่พ†", type: "determiner", pronunciation: "รฐรฆt"}, + {word: "car", translation: "่ฝฆ", type: "noun", pronunciation: "kษ‘หr"}, + {word: "hit", translation: "ๆ’žๅˆฐ", type: "verb", pronunciation: "hษชt"}, + {word: "that", translation: "้‚ฃไธช", type: "determiner", pronunciation: "รฐรฆt"}, + {word: "post", translation: "ๆŸฑๅญ", type: "noun", pronunciation: "poสŠst"}, + {word: "over there", translation: "้‚ฃ่พน", type: "phrase", pronunciation: "หˆoสŠvษ™r รฐer"} + ] + }, + { + id: 3, + original: "What happened?", + translation: "ๅ‘็”Ÿไบ†ไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "happened", translation: "ๅ‘็”Ÿไบ†", type: "verb", pronunciation: "หˆhรฆpษ™nd"} + ] + }, + { + id: 4, + original: "A dog ran across the road and the driver tried to avoid it.", + translation: "ไธ€ๅช็‹—่ท‘่ฟ‡้ฉฌ่ทฏ๏ผŒๅธๆœบ่ฏ•ๅ›พ้ฟๅผ€ๅฎƒใ€‚", + words: [ + {word: "A", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "dog", translation: "็‹—", type: "noun", pronunciation: "dษ”หษก"}, + {word: "ran", translation: "่ท‘", type: "verb", pronunciation: "rรฆn"}, + {word: "across", translation: "็ฉฟ่ฟ‡", type: "preposition", pronunciation: "ษ™หˆkrษ”หs"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "road", translation: "้ฉฌ่ทฏ", type: "noun", pronunciation: "roสŠd"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "driver", translation: "ๅธๆœบ", type: "noun", pronunciation: "หˆdraษชvษ™r"}, + {word: "tried", translation: "่ฏ•ๅ›พ", type: "verb", pronunciation: "traษชd"}, + {word: "to", translation: "ๅŽป", type: "preposition", pronunciation: "tuห"}, + {word: "avoid", translation: "้ฟๅผ€", type: "verb", pronunciation: "ษ™หˆvษ”ษชd"}, + {word: "it", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"} + ] + }, + { + id: 5, + original: "The car suddenly came towards me.", + translation: "ๆฑฝ่ฝฆ็ช็„ถๅ‘ๆˆ‘ๅ†ฒๆฅใ€‚", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "car", translation: "ๆฑฝ่ฝฆ", type: "noun", pronunciation: "kษ‘หr"}, + {word: "suddenly", translation: "็ช็„ถ", type: "adverb", pronunciation: "หˆsสŒdษ™nli"}, + {word: "came", translation: "ๆฅไบ†", type: "verb", pronunciation: "keษชm"}, + {word: "towards", translation: "ๅ‘", type: "preposition", pronunciation: "tษ”หrdz"}, + {word: "me", translation: "ๆˆ‘", type: "pronoun", pronunciation: "miห"} + ] + }, + { + id: 6, + original: "It climbed on to the pavement and crashed into that post.", + translation: "ๅฎƒ็ˆฌไธŠไบ†ไบบ่กŒ้“๏ผŒๆ’žๅˆฐไบ†้‚ฃไธชๆŸฑๅญใ€‚", + words: [ + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "climbed", translation: "็ˆฌไธŠ", type: "verb", pronunciation: "klaษชmd"}, + {word: "on to", translation: "ๅˆฐ...ไธŠ", type: "preposition", pronunciation: "ษ‘หn tuห"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "pavement", translation: "ไบบ่กŒ้“", type: "noun", pronunciation: "หˆpeษชvmษ™nt"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "crashed", translation: "ๆ’ž", type: "verb", pronunciation: "krรฆสƒt"}, + {word: "into", translation: "่ฟ›", type: "preposition", pronunciation: "หˆษชntuห"}, + {word: "that", translation: "้‚ฃไธช", type: "determiner", pronunciation: "รฐรฆt"}, + {word: "post", translation: "ๆŸฑๅญ", type: "noun", pronunciation: "poสŠst"} + ] + }, + { + id: 7, + original: "What did you do?", + translation: "ไฝ ๅšไบ†ไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "did", translation: "ๅš", type: "auxiliary", pronunciation: "dษชd"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "juห"}, + {word: "do", translation: "ไบ†", type: "verb", pronunciation: "duห"} + ] + }, + { + id: 8, + original: "I ran across the street after the dog.", + translation: "ๆˆ‘่ทŸ็€็‹—่ท‘่ฟ‡ไบ†่ก—้“ใ€‚", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "ran", translation: "่ท‘", type: "verb", pronunciation: "rรฆn"}, + {word: "across", translation: "็ฉฟ่ฟ‡", type: "preposition", pronunciation: "ษ™หˆkrษ”หs"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "street", translation: "่ก—้“", type: "noun", pronunciation: "striหt"}, + {word: "after", translation: "่ทŸ็€", type: "preposition", pronunciation: "หˆรฆftษ™r"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "dog", translation: "็‹—", type: "noun", pronunciation: "dษ”หษก"} + ] + }, + { + id: 9, + original: "Why did you do that? Were you afraid of the car?", + translation: "ไฝ ไธบไป€ไนˆ้‚ฃๆ ทๅš๏ผŸไฝ ๅฎณๆ€•ๆฑฝ่ฝฆๅ—๏ผŸ", + words: [ + {word: "Why", translation: "ไธบไป€ไนˆ", type: "adverb", pronunciation: "waษช"}, + {word: "did", translation: "ๅš", type: "auxiliary", pronunciation: "dษชd"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "juห"}, + {word: "do", translation: "ไบ†", type: "verb", pronunciation: "duห"}, + {word: "that", translation: "้‚ฃๆ ท", type: "pronoun", pronunciation: "รฐรฆt"}, + {word: "Were", translation: "ๆ˜ฏ", type: "verb", pronunciation: "wษ™r"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "juห"}, + {word: "afraid", translation: "ๅฎณๆ€•", type: "adjective", pronunciation: "ษ™หˆfreษชd"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "car", translation: "ๆฑฝ่ฝฆ", type: "noun", pronunciation: "kษ‘หr"} + ] + }, + { + id: 10, + original: "I wasn't afraid of the car. I was afraid of the driver.", + translation: "ๆˆ‘ไธๅฎณๆ€•ๆฑฝ่ฝฆใ€‚ๆˆ‘ๅฎณๆ€•ๅธๆœบใ€‚", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "wasn't", translation: "ไธ", type: "auxiliary", pronunciation: "หˆwสŒzษ™nt"}, + {word: "afraid", translation: "ๅฎณๆ€•", type: "adjective", pronunciation: "ษ™หˆfreษชd"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "car", translation: "ๆฑฝ่ฝฆ", type: "noun", pronunciation: "kษ‘หr"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "was", translation: "ๆ˜ฏ", type: "verb", pronunciation: "wสŒz"}, + {word: "afraid", translation: "ๅฎณๆ€•", type: "adjective", pronunciation: "ษ™หˆfreษชd"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "driver", translation: "ๅธๆœบ", type: "noun", pronunciation: "หˆdraษชvษ™r"} + ] + }, + { + id: 11, + original: "The driver got out of the car and began shouting at me.", + translation: "ๅธๆœบไปŽ่ฝฆ้‡Œๅ‡บๆฅ๏ผŒๅผ€ๅง‹ๅฏนๆˆ‘ๅคงๅ–Šใ€‚", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "driver", translation: "ๅธๆœบ", type: "noun", pronunciation: "หˆdraษชvษ™r"}, + {word: "got out", translation: "ๅ‡บๆฅ", type: "phrasal verb", pronunciation: "ษกษ‘หt aสŠt"}, + {word: "of", translation: "ไปŽ", type: "preposition", pronunciation: "สŒv"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "car", translation: "่ฝฆ", type: "noun", pronunciation: "kษ‘หr"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "began", translation: "ๅผ€ๅง‹", type: "verb", pronunciation: "bษชหˆษกรฆn"}, + {word: "shouting", translation: "ๅคงๅ–Š", type: "verb", pronunciation: "หˆสƒaสŠtษชล‹"}, + {word: "at", translation: "ๅฏน", type: "preposition", pronunciation: "รฆt"}, + {word: "me", translation: "ๆˆ‘", type: "pronoun", pronunciation: "miห"} + ] + }, + { + id: 12, + original: "You see, it was my dog.", + translation: "ไฝ ็œ‹๏ผŒ้‚ฃๆ˜ฏๆˆ‘็š„็‹—ใ€‚", + words: [ + {word: "You", translation: "ไฝ ", type: "pronoun", pronunciation: "juห"}, + {word: "see", translation: "็œ‹", type: "verb", pronunciation: "siห"}, + {word: "it", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "was", translation: "ๆ˜ฏ", type: "verb", pronunciation: "wสŒz"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "dog", translation: "็‹—", type: "noun", pronunciation: "dษ”หษก"} + ] + } + ] + } + ] + }, + { + title: "Roy's CD Collection - ็ฝ—ไผŠ็š„CDๆ”ถ่—", + totalSentences: 10, + chapters: [ + { + title: "Chapter 1: My Friend Roy - ๆˆ‘็š„ๆœ‹ๅ‹็ฝ—ไผŠ", + sentences: [ + { + id: 1, + original: "My friend, Roy, died last year.", + translation: "ๆˆ‘็š„ๆœ‹ๅ‹็ฝ—ไผŠๅŽปๅนดๅŽปไธ–ไบ†ใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "friend", translation: "ๆœ‹ๅ‹", type: "noun", pronunciation: "frend"}, + {word: "Roy", translation: "็ฝ—ไผŠ", type: "noun", pronunciation: "rษ”ษช"}, + {word: "died", translation: "ๅŽปไธ–ไบ†", type: "verb", pronunciation: "daษชd"}, + {word: "last", translation: "ๅŽปๅนด", type: "adjective", pronunciation: "lรฆst"}, + {word: "year", translation: "ๅนด", type: "noun", pronunciation: "jษชr"} + ] + }, + { + id: 2, + original: "He left me his CD player and his collection of CDs.", + translation: "ไป–็•™็ป™ๆˆ‘ไป–็š„CDๆ’ญๆ”พๅ™จๅ’ŒCDๆ”ถ่—ใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "left", translation: "็•™็ป™", type: "verb", pronunciation: "left"}, + {word: "me", translation: "ๆˆ‘", type: "pronoun", pronunciation: "miห"}, + {word: "his", translation: "ไป–็š„", type: "pronoun", pronunciation: "hษชz"}, + {word: "CD player", translation: "CDๆ’ญๆ”พๅ™จ", type: "noun", pronunciation: "หŒsiห หˆdiห หˆpleษชษ™r"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "his", translation: "ไป–็š„", type: "pronoun", pronunciation: "hษชz"}, + {word: "collection", translation: "ๆ”ถ่—", type: "noun", pronunciation: "kษ™หˆlekสƒษ™n"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "CDs", translation: "CD", type: "noun", pronunciation: "หŒsiห หˆdiหz"} + ] + }, + { + id: 3, + original: "Roy spent a lot of money on CDs.", + translation: "็ฝ—ไผŠๅœจCDไธŠ่Šฑไบ†ๅพˆๅคš้’ฑใ€‚", + words: [ + {word: "Roy", translation: "็ฝ—ไผŠ", type: "noun", pronunciation: "rษ”ษช"}, + {word: "spent", translation: "่Šฑไบ†", type: "verb", pronunciation: "spent"}, + {word: "a lot of", translation: "ๅพˆๅคš", type: "phrase", pronunciation: "ษ™ lษ‘หt สŒv"}, + {word: "money", translation: "้’ฑ", type: "noun", pronunciation: "หˆmสŒni"}, + {word: "on", translation: "ๅœจ...ไธŠ", type: "preposition", pronunciation: "ษ‘หn"}, + {word: "CDs", translation: "CD", type: "noun", pronunciation: "หŒsiห หˆdiหz"} + ] + }, + { + id: 4, + original: "He bought one or two new CDs every week.", + translation: "ไป–ๆฏๅ‘จไนฐไธ€ไธคๅผ ๆ–ฐCDใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "bought", translation: "ไนฐ", type: "verb", pronunciation: "bษ”หt"}, + {word: "one or two", translation: "ไธ€ไธคๅผ ", type: "phrase", pronunciation: "wสŒn ษ”หr tuห"}, + {word: "new", translation: "ๆ–ฐ็š„", type: "adjective", pronunciation: "nuห"}, + {word: "CDs", translation: "CD", type: "noun", pronunciation: "หŒsiห หˆdiหz"}, + {word: "every", translation: "ๆฏ", type: "adjective", pronunciation: "หˆevri"}, + {word: "week", translation: "ๅ‘จ", type: "noun", pronunciation: "wiหk"} + ] + }, + { + id: 5, + original: "He never went to the cinema or to the theatre.", + translation: "ไป–ไปŽไธๅŽป็”ตๅฝฑ้™ขๆˆ–ๅ‰ง้™ขใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "never", translation: "ไปŽไธ", type: "adverb", pronunciation: "หˆnevษ™r"}, + {word: "went", translation: "ๅŽป", type: "verb", pronunciation: "went"}, + {word: "to", translation: "ๅˆฐ", type: "preposition", pronunciation: "tuห"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "cinema", translation: "็”ตๅฝฑ้™ข", type: "noun", pronunciation: "หˆsษชnษ™mษ™"}, + {word: "or", translation: "ๆˆ–", type: "conjunction", pronunciation: "ษ”หr"}, + {word: "to", translation: "ๅˆฐ", type: "preposition", pronunciation: "tuห"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "theatre", translation: "ๅ‰ง้™ข", type: "noun", pronunciation: "หˆฮธiหษ™tษ™r"} + ] + }, + { + id: 6, + original: "He stayed at home every evening and listened to music.", + translation: "ไป–ๆฏๅคฉๆ™šไธŠ้ƒฝๅพ…ๅœจๅฎถ้‡Œๅฌ้Ÿณไนใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "stayed", translation: "ๅพ…", type: "verb", pronunciation: "steษชd"}, + {word: "at", translation: "ๅœจ", type: "preposition", pronunciation: "รฆt"}, + {word: "home", translation: "ๅฎถ", type: "noun", pronunciation: "hoสŠm"}, + {word: "every", translation: "ๆฏ", type: "adjective", pronunciation: "หˆevri"}, + {word: "evening", translation: "ๆ™šไธŠ", type: "noun", pronunciation: "หˆiหvnษชล‹"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "listened", translation: "ๅฌ", type: "verb", pronunciation: "หˆlษชsษ™nd"}, + {word: "to", translation: "...็š„", type: "preposition", pronunciation: "tuห"}, + {word: "music", translation: "้Ÿณไน", type: "noun", pronunciation: "หˆmjuหzษชk"} + ] + }, + { + id: 7, + original: "He often lent CDs to his friends.", + translation: "ไป–็ปๅธธๆŠŠCDๅ€Ÿ็ป™ๆœ‹ๅ‹ใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "often", translation: "็ปๅธธ", type: "adverb", pronunciation: "หˆษ”หfษ™n"}, + {word: "lent", translation: "ๅ€Ÿ็ป™", type: "verb", pronunciation: "lent"}, + {word: "CDs", translation: "CD", type: "noun", pronunciation: "หŒsiห หˆdiหz"}, + {word: "to", translation: "็ป™", type: "preposition", pronunciation: "tuห"}, + {word: "his", translation: "ไป–็š„", type: "pronoun", pronunciation: "hษชz"}, + {word: "friends", translation: "ๆœ‹ๅ‹", type: "noun", pronunciation: "frends"} + ] + }, + { + id: 8, + original: "Sometimes they kept them.", + translation: "ๆœ‰ๆ—ถไป–ไปฌไผš็•™ไธ‹่ฟ™ไบ›CDใ€‚", + words: [ + {word: "Sometimes", translation: "ๆœ‰ๆ—ถ", type: "adverb", pronunciation: "หˆsสŒmtaษชmz"}, + {word: "they", translation: "ไป–ไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "kept", translation: "็•™ไธ‹", type: "verb", pronunciation: "kept"}, + {word: "them", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐem"} + ] + }, + { + id: 9, + original: "He lost many CDs in this way.", + translation: "ไป–ๅฐฑ่ฟ™ๆ ทไธขๅคฑไบ†ๅพˆๅคšCDใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "lost", translation: "ไธขๅคฑไบ†", type: "verb", pronunciation: "lษ”หst"}, + {word: "many", translation: "ๅพˆๅคš", type: "adjective", pronunciation: "หˆmeni"}, + {word: "CDs", translation: "CD", type: "noun", pronunciation: "หŒsiห หˆdiหz"}, + {word: "in", translation: "ไปฅ", type: "preposition", pronunciation: "ษชn"}, + {word: "this", translation: "่ฟ™็ง", type: "determiner", pronunciation: "รฐษชs"}, + {word: "way", translation: "ๆ–นๅผ", type: "noun", pronunciation: "weษช"} + ] + } + ] + } + ] + } + ] +}; + +// ============================================================================ +// CONTENT STRUCTURE SUMMARY - FOR AI REFERENCE +// ============================================================================ +// +// REQUIRED SECTIONS (for basic compatibility): +// - vocabulary: Object with word keys, 25+ entries focusing on past tense verbs and travel +// - Basic metadata: id, name, description, difficulty (intermediate level) +// +// RECOMMENDED SECTIONS (for optimal experience): +// - grammar: 3 topics covering past tense, direct/indirect objects, and questions +// - story: Main postcard narrative with 11 sentences across 2 chapters +// - fillInBlanks: 10 exercises focusing on past tense and object patterns +// +// OPTIONAL SECTIONS (for enhanced features): +// - corrections: Past tense error fixing exercises +// - additionalStories: Accident story and Roy's CD collection +// +// ============================================================================ +// GAME COMPATIBILITY REFERENCE +// ============================================================================ +// +// VOCABULARY-BASED GAMES: +// - Focus on past tense irregular verbs: went, taught, lent, bought, thought, spent +// - Travel and holiday vocabulary: postcard, museum, garden, waiter, decision +// - Question formation and time expressions +// +// STORY-BASED GAMES: +// - Main story: Postcard holiday narrative with relatable vacation theme +// - Supporting stories: Accident dialogue and friend's hobby story +// +// GRAMMAR-FOCUSED GAMES: +// - Past tense verb formation (regular and irregular) +// - Direct vs indirect object patterns with give/lend/send verbs +// - Question formation with did/what/where/why +// +// ============================================================================ +// LANGUAGE LEARNING FOCUS +// ============================================================================ +// +// TARGET SKILLS: +// - Past tense narrative storytelling +// - Travel and holiday vocabulary +// - Polite conversation and questions +// - Time expressions and sequence words +// +// DIFFICULTY LEVEL: Intermediate +// - Complex sentence structures with multiple clauses +// - Irregular past tense verbs requiring memorization +// - Direct/indirect object transformations +// - Question formation with auxiliary verbs +// +// ============================================================================ \ No newline at end of file diff --git a/src/content/NCE2-Lesson30.js b/src/content/NCE2-Lesson30.js new file mode 100644 index 0000000..643c38f --- /dev/null +++ b/src/content/NCE2-Lesson30.js @@ -0,0 +1,785 @@ +// === LESSON 30: FOOTBALL OR POLO? === +// English learning story with Chinese translation - Intermediate Level + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.NCE2Lesson30 = { + id: "nce2-lesson30", + name: "NCE2-Lesson30", + description: "Football or polo? - A story about the Wayle river with grammar focus on articles and quantifiers", + difficulty: "intermediate", + language: "en-US", + userLanguage: "zh-CN", + totalWords: 200, + + // === GRAMMAR LESSONS SYSTEM === + grammar: { + "articles-usage": { + title: "Articles Usage - ๅ† ่ฏไฝฟ็”จ", + explanation: "English uses 'a', 'an', and 'the' in specific ways, especially with names and places.", + rules: [ + "the - use with rivers, seas, oceans, mountain ranges: the Thames, the Pacific", + "no article - use with most personal names and countries: John, England", + "the - use with certain countries: the United States, the United Kingdom", + "the - use with superlatives and unique things: the best, the sun" + ], + examples: [ + { + english: "The Wayle is a small river.", + chinese: "ๅจๅฐ”ๆฒณๆ˜ฏไธ€ๆกๅฐๆฒณใ€‚", + explanation: "Use 'the' with river names", + pronunciation: "รฐษ™ weษชl ษชz ษ™ smษ”หl หˆrษชvษ™r" + }, + { + english: "Paris is on the Seine.", + chinese: "ๅทด้ปŽๅœจๅกž็บณๆฒณไธŠใ€‚", + explanation: "Use 'the' with famous rivers", + pronunciation: "หˆpรฆrษชs ษชz ษ’n รฐษ™ seษชn" + }, + { + english: "London is on the Thames.", + chinese: "ไผฆๆ•ฆๅœจๆณฐๆ™คๅฃซๆฒณไธŠใ€‚", + explanation: "River names always take 'the'", + pronunciation: "หˆlสŒndษ™n ษชz ษ’n รฐษ™ temz" + }, + { + english: "He lives in England.", + chinese: "ไป–ไฝๅœจ่‹ฑๅ›ฝใ€‚", + explanation: "Most country names don't use 'the'", + pronunciation: "hiห lษชvz ษชn หˆษชล‹ษกlษ™nd" + }, + { + english: "I went to the United States.", + chinese: "ๆˆ‘ๅŽปไบ†็พŽๅ›ฝใ€‚", + explanation: "Some countries use 'the'", + pronunciation: "aษช went tuห รฐษ™ juหˆnaษชtษชd steษชts" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "_____ Thames flows through London.", + options: ["The", "A", "An", "No article"], + correct: "The", + explanation: "River names always use 'the'" + }, + { + type: "fill_blank", + sentence: "John lives in _____ England.", + options: ["the", "a", "an", "no article"], + correct: "no article", + explanation: "Most country names don't use articles" + } + ] + }, + + "some-any-usage": { + title: "Some and Any Usage - Someๅ’ŒAny็š„ไฝฟ็”จ", + explanation: "English uses 'some' and 'any' differently depending on sentence type and meaning.", + rules: [ + "some - use in positive statements: There are some people", + "any - use in questions: Are there any people?", + "any - use in negative statements: There aren't any people", + "some - use in offers and requests: Would you like some tea?" + ], + examples: [ + { + english: "Some children were playing games.", + chinese: "ไธ€ไบ›ๅญฉๅญๅœจ็Žฉๆธธๆˆใ€‚", + explanation: "Use 'some' in positive statements", + pronunciation: "sสŒm หˆtสƒษชldrษ™n wษ™r หˆpleษชษชล‹ ษกeษชmz" + }, + { + english: "There were some people rowing.", + chinese: "ๆœ‰ไธ€ไบ›ไบบๅœจๅˆ’่ˆนใ€‚", + explanation: "Use 'some' to describe what exists", + pronunciation: "รฐษ™r wษ™r sสŒm หˆpiหpษ™l หˆroสŠษชล‹" + }, + { + english: "There weren't any children in sight.", + chinese: "็œ‹ไธ่งไปปไฝ•ๅญฉๅญใ€‚", + explanation: "Use 'any' in negative statements", + pronunciation: "รฐษ™r wษ™rnt หˆeni หˆtสƒษชldrษ™n ษชn saษชt" + }, + { + english: "Are there any boats on the river?", + chinese: "ๆฒณไธŠๆœ‰่ˆนๅ—๏ผŸ", + explanation: "Use 'any' in questions", + pronunciation: "ษ™r รฐษ™r หˆeni boสŠts ษ’n รฐษ™ หˆrษชvษ™r" + }, + { + english: "Would you like some water?", + chinese: "ไฝ ๆƒณ่ฆไธ€ไบ›ๆฐดๅ—๏ผŸ", + explanation: "Use 'some' in offers", + pronunciation: "wสŠd juห laษชk sสŒm หˆwษ”หtษ™r" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "There are _____ people on the bank.", + options: ["some", "any", "a", "the"], + correct: "some", + explanation: "Use 'some' in positive statements" + }, + { + type: "fill_blank", + sentence: "There weren't _____ children in sight.", + options: ["some", "any", "a", "the"], + correct: "any", + explanation: "Use 'any' in negative statements" + } + ] + }, + + "past-continuous": { + title: "Past Continuous Tense - ่ฟ‡ๅŽป่ฟ›่กŒๆ—ถ", + explanation: "Use past continuous for actions that were in progress at a specific time in the past.", + rules: [ + "Form: was/were + verb-ing", + "Use for ongoing past actions: I was sitting by the river", + "Use for background actions in stories: Children were playing while I sat", + "Use with time expressions: at 3pm yesterday, last Sunday" + ], + examples: [ + { + english: "Some children were playing games.", + chinese: "ไธ€ไบ›ๅญฉๅญๅœจ็Žฉๆธธๆˆใ€‚", + explanation: "Action in progress in the past", + pronunciation: "sสŒm หˆtสƒษชldrษ™n wษ™r หˆpleษชษชล‹ ษกeษชmz" + }, + { + english: "People were rowing on the river.", + chinese: "ไบบไปฌๅœจๆฒณไธŠๅˆ’่ˆนใ€‚", + explanation: "Ongoing action in the past", + pronunciation: "หˆpiหpษ™l wษ™r หˆroสŠษชล‹ ษ’n รฐษ™ หˆrษชvษ™r" + }, + { + english: "I was sitting by the river.", + chinese: "ๆˆ‘ๅๅœจๆฒณ่พนใ€‚", + explanation: "Past continuous shows duration", + pronunciation: "aษช wษ™z หˆsษชtษชล‹ baษช รฐษ™ หˆrษชvษ™r" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "The children _____ playing when it happened.", + options: ["were", "was", "are", "is"], + correct: "were", + explanation: "Use 'were' with plural subjects in past continuous" + } + ] + } + }, + + vocabulary: { + "polo": { + "user_language": "ๆฐด็ƒ", + "type": "noun", + "pronunciation": "หˆpoสŠloสŠ" + }, + "river": { + "user_language": "ๆฒณๆต", + "type": "noun", + "pronunciation": "หˆrษชvษ™r" + }, + "cut": { + "user_language": "็ฉฟ่ฟ‡", + "type": "verb", + "pronunciation": "kสŒt" + }, + "park": { + "user_language": "ๅ…ฌๅ›ญ", + "type": "noun", + "pronunciation": "pษ‘หrk" + }, + "afternoon": { + "user_language": "ไธ‹ๅˆ", + "type": "noun", + "pronunciation": "หŒรฆftษ™rหˆnuหn" + }, + "bank": { + "user_language": "ๆฒณๅฒธ", + "type": "noun", + "pronunciation": "bรฆล‹k" + }, + "usual": { + "user_language": "ๅนณๅธธ็š„", + "type": "adjective", + "pronunciation": "หˆjuหส’uษ™l" + }, + "children": { + "user_language": "ๅญฉๅญไปฌ", + "type": "noun", + "pronunciation": "หˆtสƒษชldrษ™n" + }, + "games": { + "user_language": "ๆธธๆˆ", + "type": "noun", + "pronunciation": "ษกeษชmz" + }, + "people": { + "user_language": "ไบบไปฌ", + "type": "noun", + "pronunciation": "หˆpiหpษ™l" + }, + "row": { + "user_language": "ๅˆ’่ˆน", + "type": "verb", + "pronunciation": "roสŠ" + }, + "suddenly": { + "user_language": "็ช็„ถ", + "type": "adverb", + "pronunciation": "หˆsสŒdษ™nli" + }, + "kick": { + "user_language": "่ธข", + "type": "verb", + "pronunciation": "kษชk" + }, + "ball": { + "user_language": "็ƒ", + "type": "noun", + "pronunciation": "bษ”หl" + }, + "hard": { + "user_language": "็”จๅŠ›ๅœฐ", + "type": "adverb", + "pronunciation": "hษ‘หrd" + }, + "towards": { + "user_language": "ๆœๅ‘", + "type": "preposition", + "pronunciation": "tษ™หˆwษ”หrdz" + }, + "passing": { + "user_language": "็ป่ฟ‡็š„", + "type": "adjective", + "pronunciation": "หˆpรฆsษชล‹" + }, + "boat": { + "user_language": "่ˆน", + "type": "noun", + "pronunciation": "boสŠt" + }, + "called": { + "user_language": "ๅซๅ–Š", + "type": "verb", + "pronunciation": "kษ”หld" + }, + "hear": { + "user_language": "ๅฌ่ง", + "type": "verb", + "pronunciation": "hษชr" + }, + "struck": { + "user_language": "ๅ‡ปๆ‰“", + "type": "verb", + "pronunciation": "strสŒk" + }, + "nearly": { + "user_language": "ๅ‡ ไนŽ", + "type": "adverb", + "pronunciation": "หˆnษชrli" + }, + "fell": { + "user_language": "่ฝไธ‹", + "type": "verb", + "pronunciation": "fel" + }, + "water": { + "user_language": "ๆฐด", + "type": "noun", + "pronunciation": "หˆwษ”หtษ™r" + }, + "turned": { + "user_language": "่ฝฌ่บซ", + "type": "verb", + "pronunciation": "tษœหrnd" + }, + "sight": { + "user_language": "่ง†็บฟ", + "type": "noun", + "pronunciation": "saษชt" + }, + "run away": { + "user_language": "่ท‘ๅผ€", + "type": "verb", + "pronunciation": "rสŒn ษ™หˆweษช" + }, + "laughed": { + "user_language": "็ฌ‘", + "type": "verb", + "pronunciation": "lรฆft" + }, + "realized": { + "user_language": "ๆ„่ฏ†ๅˆฐ", + "type": "verb", + "pronunciation": "หˆriษ™laษชzd" + }, + "happened": { + "user_language": "ๅ‘็”Ÿ", + "type": "verb", + "pronunciation": "หˆhรฆpษ™nd" + }, + "threw": { + "user_language": "ๆ‰”", + "type": "verb", + "pronunciation": "ฮธruห" + }, + "back": { + "user_language": "ๅ›žๆฅ", + "type": "adverb", + "pronunciation": "bรฆk" + } + }, + + // === SENTENCES FOR GAMES (extracted from stories) === + sentences: [ + { + english: "The Wayle is a small river.", + chinese: "ๅจๅฐ”ๆฒณๆ˜ฏไธ€ๆกๅฐๆฒณใ€‚", + prononciation: "รฐษ™ weษชl ษชz ษ™ smษ”หl หˆrษชvษ™r" + }, + { + english: "It cuts across the park near my home.", + chinese: "ๅฎƒๆจช็ฉฟๆˆ‘ๅฎถ้™„่ฟ‘็š„ๅ…ฌๅ›ญใ€‚", + prononciation: "ษชt kสŒts ษ™หˆkrษ”หs รฐษ™ pษ‘หrk nษชr maษช hoสŠm" + }, + { + english: "I like sitting by the Wayle on fine afternoons.", + chinese: "ๆˆ‘ๅ–œๆฌขๅœจๆ™ดๆœ—็š„ไธ‹ๅˆๅๅœจๅจๅฐ”ๆฒณ่พนใ€‚", + prononciation: "aษช laษชk หˆsษชtษชล‹ baษช รฐษ™ weษชl ษ‘หn faษชn หŒรฆftษ™rหˆnuหnz" + }, + { + english: "Some children were playing games on the bank.", + chinese: "ไธ€ไบ›ๅญฉๅญๅœจๆฒณๅฒธไธŠ็Žฉๆธธๆˆใ€‚", + prononciation: "sสŒm หˆtสƒษชldrษ™n wษ™r หˆpleษชษชล‹ geษชmz ษ‘หn รฐษ™ bรฆล‹k" + }, + { + english: "There were some people rowing on the river.", + chinese: "ๆฒณไธŠๆœ‰ไธ€ไบ›ไบบๅœจๅˆ’่ˆนใ€‚", + prononciation: "รฐษ›r wษ™r sสŒm หˆpiหpษ™l หˆroสŠษชล‹ ษ‘หn รฐษ™ หˆrษชvษ™r" + }, + { + english: "The ball struck him so hard.", + chinese: "็ƒ้‡้‡ๅœฐๆ‰“ๅœจไป–่บซไธŠใ€‚", + prononciation: "รฐษ™ bษ”หl strสŒk hษชm soสŠ hษ‘หrd" + }, + { + english: "This is a pleasant surprise!", + chinese: "่ฟ™็œŸๆ˜ฏไธชๆ„ๅค–็š„ๆƒŠๅ–œ๏ผ", + prononciation: "รฐษชs ษชz ษ™ หˆplษ›zษ™nt sษ™rหˆpraษชz" + }, + { + english: "I turned to look at the children.", + chinese: "ๆˆ‘่ฝฌ่บซ็œ‹ๅ‘ๅญฉๅญไปฌใ€‚", + prononciation: "aษช tษœหrnd tuห lสŠk รฆt รฐษ™ หˆtสƒษชldrษ™n" + }, + { + english: "They were playing football, not polo!", + chinese: "ไป–ไปฌๅœจ่ธข่ถณ็ƒ๏ผŒไธๆ˜ฏๆฐด็ƒ๏ผ", + prononciation: "รฐeษช wษ™r หˆpleษชษชล‹ หˆfสŠtbษ”หl nษ‘หt หˆpoสŠloสŠ" + } + ], + + story: { + title: "Football or polo? - ่ถณ็ƒ่ฟ˜ๆ˜ฏๆฐด็ƒ๏ผŸ", + totalSentences: 12, + chapters: [ + { + title: "Chapter 1: A Day by the River - ๆฒณ่พน็š„ไธ€ๅคฉ", + sentences: [ + { + id: 1, + original: "The Wayle is a small river that cuts across the park near my home.", + translation: "ๅจๅฐ”ๆฒณๆ˜ฏๆจช็ฉฟๆˆ‘ๅฎถ้™„่ฟ‘ๅ…ฌๅ›ญ็š„ไธ€ๆกๅฐๆฒณใ€‚", + words: [ + {word: "The", translation: "่ฟ™ๆก", type: "article", pronunciation: "รฐษ™"}, + {word: "Wayle", translation: "ๅจๅฐ”ๆฒณ", type: "noun", pronunciation: "weษชl"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๆก", type: "article", pronunciation: "ษ™"}, + {word: "small", translation: "ๅฐ็š„", type: "adjective", pronunciation: "smษ”หl"}, + {word: "river", translation: "ๆฒณๆต", type: "noun", pronunciation: "หˆrษชvษ™r"}, + {word: "that", translation: "้‚ฃ", type: "pronoun", pronunciation: "รฐรฆt"}, + {word: "cuts", translation: "็ฉฟ่ฟ‡", type: "verb", pronunciation: "kสŒts"}, + {word: "across", translation: "ๆจช็ฉฟ", type: "preposition", pronunciation: "ษ™หˆkrษ”หs"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "park", translation: "ๅ…ฌๅ›ญ", type: "noun", pronunciation: "pษ‘หrk"}, + {word: "near", translation: "้ ่ฟ‘", type: "preposition", pronunciation: "nษชr"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "home", translation: "ๅฎถ", type: "noun", pronunciation: "hoสŠm"} + ] + }, + { + id: 2, + original: "I like sitting by the Wayle on fine afternoons.", + translation: "ๆˆ‘ๅ–œๆฌขๅœจๅคฉๆฐ”ๆ™ดๆœ—็š„ไธ‹ๅˆๅๅœจๅจๅฐ”ๆฒณ่พนใ€‚", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "like", translation: "ๅ–œๆฌข", type: "verb", pronunciation: "laษชk"}, + {word: "sitting", translation: "ๅ", type: "verb", pronunciation: "หˆsษชtษชล‹"}, + {word: "by", translation: "ๅœจ...ๆ—่พน", type: "preposition", pronunciation: "baษช"}, + {word: "the", translation: "่ฟ™ๆก", type: "article", pronunciation: "รฐษ™"}, + {word: "Wayle", translation: "ๅจๅฐ”ๆฒณ", type: "noun", pronunciation: "weษชl"}, + {word: "on", translation: "ๅœจ", type: "preposition", pronunciation: "ษ’n"}, + {word: "fine", translation: "ๆ™ดๆœ—็š„", type: "adjective", pronunciation: "faษชn"}, + {word: "afternoons", translation: "ไธ‹ๅˆ", type: "noun", pronunciation: "หŒรฆftษ™rหˆnuหnz"} + ] + }, + { + id: 3, + original: "It was warm last Sunday, so I went and sat on the river bank as usual.", + translation: "ไธŠๆ˜ŸๆœŸๆ—ฅๅคฉๆฐ”ๅพˆๆš–ๅ’Œ๏ผŒไบŽๆ˜ฏๆˆ‘ๅƒๅพ€ๅธธไธ€ๆ ทๅˆๅŽปๆฒณ่พนๅๅใ€‚", + words: [ + {word: "It", translation: "ๅคฉๆฐ”", type: "pronoun", pronunciation: "ษชt"}, + {word: "was", translation: "ๆ˜ฏ", type: "verb", pronunciation: "wษ™z"}, + {word: "warm", translation: "ๆš–ๅ’Œ็š„", type: "adjective", pronunciation: "wษ”หrm"}, + {word: "last", translation: "ไธŠ", type: "adjective", pronunciation: "lรฆst"}, + {word: "Sunday", translation: "ๆ˜ŸๆœŸๆ—ฅ", type: "noun", pronunciation: "หˆsสŒndeษช"}, + {word: "so", translation: "ๆ‰€ไปฅ", type: "conjunction", pronunciation: "soสŠ"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "went", translation: "ๅŽปไบ†", type: "verb", pronunciation: "went"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "sat", translation: "ๅ", type: "verb", pronunciation: "sรฆt"}, + {word: "on", translation: "ๅœจ", type: "preposition", pronunciation: "ษ’n"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "river", translation: "ๆฒณ", type: "noun", pronunciation: "หˆrษชvษ™r"}, + {word: "bank", translation: "ๅฒธ่พน", type: "noun", pronunciation: "bรฆล‹k"}, + {word: "as", translation: "ๅƒ", type: "adverb", pronunciation: "รฆz"}, + {word: "usual", translation: "ๅพ€ๅธธไธ€ๆ ท", type: "adjective", pronunciation: "หˆjuหส’uษ™l"} + ] + }, + { + id: 4, + original: "Some children were playing games on the bank and there were some people rowing on the river.", + translation: "ๆฒณๅฒธไธŠๆœ‰ไบ›ๅญฉๅญๅœจ็Žฉ่€๏ผŒๆฒณ้ขไธŠๆœ‰ไบ›ไบบๅœจๅˆ’่ˆนใ€‚", + words: [ + {word: "Some", translation: "ไธ€ไบ›", type: "determiner", pronunciation: "sสŒm"}, + {word: "children", translation: "ๅญฉๅญไปฌ", type: "noun", pronunciation: "หˆtสƒษชldrษ™n"}, + {word: "were", translation: "ๆญฃๅœจ", type: "verb", pronunciation: "wษ™r"}, + {word: "playing", translation: "็Žฉ", type: "verb", pronunciation: "หˆpleษชษชล‹"}, + {word: "games", translation: "ๆธธๆˆ", type: "noun", pronunciation: "ษกeษชmz"}, + {word: "on", translation: "ๅœจ", type: "preposition", pronunciation: "ษ’n"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "bank", translation: "ๅฒธ่พน", type: "noun", pronunciation: "bรฆล‹k"}, + {word: "and", translation: "ๅ’Œ", type: "conjunction", pronunciation: "รฆnd"}, + {word: "there", translation: "้‚ฃ้‡Œ", type: "adverb", pronunciation: "รฐer"}, + {word: "were", translation: "ๆœ‰", type: "verb", pronunciation: "wษ™r"}, + {word: "some", translation: "ไธ€ไบ›", type: "determiner", pronunciation: "sสŒm"}, + {word: "people", translation: "ไบบไปฌ", type: "noun", pronunciation: "หˆpiหpษ™l"}, + {word: "rowing", translation: "ๅˆ’่ˆน", type: "verb", pronunciation: "หˆroสŠษชล‹"}, + {word: "on", translation: "ๅœจ", type: "preposition", pronunciation: "ษ’n"}, + {word: "the", translation: "่ฟ™ๆก", type: "article", pronunciation: "รฐษ™"}, + {word: "river", translation: "ๆฒณ", type: "noun", pronunciation: "หˆrษชvษ™r"} + ] + } + ] + }, + { + title: "Chapter 2: The Ball Incident - ็ƒ็š„ไบ‹ไปถ", + sentences: [ + { + id: 5, + original: "Suddenly, one of the children kicked a ball very hard and it went towards a passing boat.", + translation: "็ช็„ถ๏ผŒไธ€ไธชๅญฉๅญ็‹ ็‹ ๅœฐ่ธขไบ†ไธ€่„š็ƒ๏ผŒ็ƒไพฟๅ‘ไธ€ๆก็ป่ฟ‡็š„ๅฐ่ˆน้ฃžๅŽปใ€‚", + words: [ + {word: "Suddenly", translation: "็ช็„ถ", type: "adverb", pronunciation: "หˆsสŒdษ™nli"}, + {word: "one", translation: "ไธ€ไธช", type: "number", pronunciation: "wสŒn"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "the", translation: "่ฟ™ไบ›", type: "article", pronunciation: "รฐษ™"}, + {word: "children", translation: "ๅญฉๅญไปฌ", type: "noun", pronunciation: "หˆtสƒษชldrษ™n"}, + {word: "kicked", translation: "่ธขไบ†", type: "verb", pronunciation: "kษชkt"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "ball", translation: "็ƒ", type: "noun", pronunciation: "bษ”หl"}, + {word: "very", translation: "้žๅธธ", type: "adverb", pronunciation: "หˆveri"}, + {word: "hard", translation: "็”จๅŠ›ๅœฐ", type: "adverb", pronunciation: "hษ‘หrd"}, + {word: "and", translation: "ๅนถไธ”", type: "conjunction", pronunciation: "รฆnd"}, + {word: "it", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "went", translation: "้ฃžๅŽป", type: "verb", pronunciation: "went"}, + {word: "towards", translation: "ๆœๅ‘", type: "preposition", pronunciation: "tษ™หˆwษ”หrdz"}, + {word: "a", translation: "ไธ€ๆก", type: "article", pronunciation: "ษ™"}, + {word: "passing", translation: "็ป่ฟ‡็š„", type: "adjective", pronunciation: "หˆpรฆsษชล‹"}, + {word: "boat", translation: "่ˆน", type: "noun", pronunciation: "boสŠt"} + ] + }, + { + id: 6, + original: "Some people on the bank called out to the man in the boat, but he did not hear them.", + translation: "ๅฒธไธŠ็š„ไธ€ไบ›ไบบๅฏน่ˆนไธŠ็š„ไบบ้ซ˜ๅ–Š๏ผŒไฝ†ไป–ๆฒกๆœ‰ๅฌ่งใ€‚", + words: [ + {word: "Some", translation: "ไธ€ไบ›", type: "determiner", pronunciation: "sสŒm"}, + {word: "people", translation: "ไบบไปฌ", type: "noun", pronunciation: "หˆpiหpษ™l"}, + {word: "on", translation: "ๅœจ", type: "preposition", pronunciation: "ษ’n"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "bank", translation: "ๅฒธไธŠ", type: "noun", pronunciation: "bรฆล‹k"}, + {word: "called", translation: "ๅซๅ–Š", type: "verb", pronunciation: "kษ”หld"}, + {word: "out", translation: "ๅ‡บๆฅ", type: "adverb", pronunciation: "aสŠt"}, + {word: "to", translation: "ๅฏน", type: "preposition", pronunciation: "tuห"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "man", translation: "ไบบ", type: "noun", pronunciation: "mรฆn"}, + {word: "in", translation: "ๅœจ", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™ๆก", type: "article", pronunciation: "รฐษ™"}, + {word: "boat", translation: "่ˆนไธŠ", type: "noun", pronunciation: "boสŠt"}, + {word: "but", translation: "ไฝ†ๆ˜ฏ", type: "conjunction", pronunciation: "bสŒt"}, + {word: "he", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "did", translation: "ๅŠฉๅŠจ่ฏ", type: "auxiliary", pronunciation: "dษชd"}, + {word: "not", translation: "ไธ", type: "adverb", pronunciation: "nษ‘หt"}, + {word: "hear", translation: "ๅฌ่ง", type: "verb", pronunciation: "hษชr"}, + {word: "them", translation: "ไป–ไปฌ", type: "pronoun", pronunciation: "รฐem"} + ] + }, + { + id: 7, + original: "The ball struck him so hard that he nearly fell into the water.", + translation: "็ƒ้‡้‡ๅœฐๆ‰“ๅœจไป–่บซไธŠ๏ผŒไฝฟไป–ๅทฎ็‚นๅ„ฟ่ฝๅ…ฅๆฐดไธญใ€‚", + words: [ + {word: "The", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "ball", translation: "็ƒ", type: "noun", pronunciation: "bษ”หl"}, + {word: "struck", translation: "ๅ‡ปๆ‰“", type: "verb", pronunciation: "strสŒk"}, + {word: "him", translation: "ไป–", type: "pronoun", pronunciation: "hษชm"}, + {word: "so", translation: "ๅฆ‚ๆญค", type: "adverb", pronunciation: "soสŠ"}, + {word: "hard", translation: "็”จๅŠ›", type: "adverb", pronunciation: "hษ‘หrd"}, + {word: "that", translation: "ไปฅ่‡ณไบŽ", type: "conjunction", pronunciation: "รฐรฆt"}, + {word: "he", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "nearly", translation: "ๅ‡ ไนŽ", type: "adverb", pronunciation: "หˆnษชrli"}, + {word: "fell", translation: "่ฝไธ‹", type: "verb", pronunciation: "fel"}, + {word: "into", translation: "่ฟ›ๅ…ฅ", type: "preposition", pronunciation: "หˆษชntuห"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "water", translation: "ๆฐดไธญ", type: "noun", pronunciation: "หˆwษ”หtษ™r"} + ] + }, + { + id: 8, + original: "I turned to look at the children, but there weren't any in sight: they had all run away!", + translation: "ๆˆ‘่ฝฌ่ฟ‡ๅคดๅŽป็œ‹้‚ฃไบ›ๅญฉๅญ๏ผŒไฝ†ไธ€ไธชไนŸไธ่ง๏ผŒๅ…จ้ƒฝ่ท‘ไบ†๏ผ", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "turned", translation: "่ฝฌ่บซ", type: "verb", pronunciation: "tษœหrnd"}, + {word: "to", translation: "ๅŽป", type: "preposition", pronunciation: "tuห"}, + {word: "look", translation: "็œ‹", type: "verb", pronunciation: "lสŠk"}, + {word: "at", translation: "็œ‹", type: "preposition", pronunciation: "รฆt"}, + {word: "the", translation: "่ฟ™ไบ›", type: "article", pronunciation: "รฐษ™"}, + {word: "children", translation: "ๅญฉๅญไปฌ", type: "noun", pronunciation: "หˆtสƒษชldrษ™n"}, + {word: "but", translation: "ไฝ†ๆ˜ฏ", type: "conjunction", pronunciation: "bสŒt"}, + {word: "there", translation: "้‚ฃ้‡Œ", type: "adverb", pronunciation: "รฐer"}, + {word: "weren't", translation: "ๆฒกๆœ‰", type: "verb", pronunciation: "wษ™rnt"}, + {word: "any", translation: "ไปปไฝ•", type: "determiner", pronunciation: "หˆeni"}, + {word: "in", translation: "ๅœจ", type: "preposition", pronunciation: "ษชn"}, + {word: "sight", translation: "่ง†็บฟไธญ", type: "noun", pronunciation: "saษชt"}, + {word: "they", translation: "ไป–ไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "had", translation: "ๅทฒ็ป", type: "auxiliary", pronunciation: "hรฆd"}, + {word: "all", translation: "ๅ…จ้ƒจ", type: "adverb", pronunciation: "ษ”หl"}, + {word: "run", translation: "่ท‘", type: "verb", pronunciation: "rสŒn"}, + {word: "away", translation: "่ตฐไบ†", type: "adverb", pronunciation: "ษ™หˆweษช"} + ] + } + ] + }, + { + title: "Chapter 3: The Happy Ending - ๆ„‰ๅฟซ็š„็ป“ๅฑ€", + sentences: [ + { + id: 9, + original: "The man laughed when he realized what had happened.", + translation: "ๅฝ“้‚ฃไธชไบบๆ˜Ž็™ฝไบ†ๅ‘็”Ÿ็š„ไบ‹ๆƒ…ๆ—ถ๏ผŒ็ฌ‘ไบ†ใ€‚", + words: [ + {word: "The", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "man", translation: "ไบบ", type: "noun", pronunciation: "mรฆn"}, + {word: "laughed", translation: "็ฌ‘ไบ†", type: "verb", pronunciation: "lรฆft"}, + {word: "when", translation: "ๅฝ“", type: "conjunction", pronunciation: "wen"}, + {word: "he", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "realized", translation: "ๆ„่ฏ†ๅˆฐ", type: "verb", pronunciation: "หˆriษ™laษชzd"}, + {word: "what", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "had", translation: "ๅทฒ็ป", type: "auxiliary", pronunciation: "hรฆd"}, + {word: "happened", translation: "ๅ‘็”Ÿไบ†", type: "verb", pronunciation: "หˆhรฆpษ™nd"} + ] + }, + { + id: 10, + original: "He called out to the children and threw the ball back to the bank.", + translation: "ไป–ๅคงๅฃฐๅซ้‚ฃไบ›ๅญฉๅญ๏ผŒๆŠŠ็ƒๆ‰”ๅ›žๅˆฐๅฒธไธŠใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hiห"}, + {word: "called", translation: "ๅซ", type: "verb", pronunciation: "kษ”หld"}, + {word: "out", translation: "ๅ‡บๆฅ", type: "adverb", pronunciation: "aสŠt"}, + {word: "to", translation: "ๅฏน", type: "preposition", pronunciation: "tuห"}, + {word: "the", translation: "่ฟ™ไบ›", type: "article", pronunciation: "รฐษ™"}, + {word: "children", translation: "ๅญฉๅญไปฌ", type: "noun", pronunciation: "หˆtสƒษชldrษ™n"}, + {word: "and", translation: "ๅนถไธ”", type: "conjunction", pronunciation: "รฆnd"}, + {word: "threw", translation: "ๆ‰”", type: "verb", pronunciation: "ฮธruห"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "ball", translation: "็ƒ", type: "noun", pronunciation: "bษ”หl"}, + {word: "back", translation: "ๅ›ž", type: "adverb", pronunciation: "bรฆk"}, + {word: "to", translation: "ๅˆฐ", type: "preposition", pronunciation: "tuห"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "bank", translation: "ๅฒธไธŠ", type: "noun", pronunciation: "bรฆล‹k"} + ] + } + ] + } + ] + }, + + // === GRAMMAR-BASED FILL IN THE BLANKS === + fillInBlanks: [ + { + sentence: "_____ Wayle is a small river.", + options: ["The", "A", "An", "No article"], + correctAnswer: "The", + explanation: "River names always use 'the'", + grammarFocus: "articles-usage" + }, + { + sentence: "There were _____ people rowing on the river.", + options: ["some", "any", "a", "the"], + correctAnswer: "some", + explanation: "Use 'some' in positive statements", + grammarFocus: "some-any-usage" + }, + { + sentence: "There weren't _____ children in sight.", + options: ["some", "any", "a", "the"], + correctAnswer: "any", + explanation: "Use 'any' in negative statements", + grammarFocus: "some-any-usage" + }, + { + sentence: "The children _____ playing games on the bank.", + options: ["were", "was", "are", "is"], + correctAnswer: "were", + explanation: "Use 'were' with plural subjects in past continuous", + grammarFocus: "past-continuous" + }, + { + sentence: "He lives in _____ England.", + options: ["the", "a", "an", "no article"], + correctAnswer: "no article", + explanation: "Most country names don't use articles", + grammarFocus: "articles-usage" + }, + { + sentence: "Are there _____ boats on the river?", + options: ["some", "any", "a", "the"], + correctAnswer: "any", + explanation: "Use 'any' in questions", + grammarFocus: "some-any-usage" + } + ], + + // === GRAMMAR CORRECTION EXERCISES === + corrections: [ + { + incorrect: "Wayle is small river.", + correct: "The Wayle is a small river.", + explanation: "River names need 'the' and countable nouns need 'a'", + grammarFocus: "articles-usage" + }, + { + incorrect: "There were any children playing.", + correct: "There were some children playing.", + explanation: "Use 'some' in positive statements, not 'any'", + grammarFocus: "some-any-usage" + }, + { + incorrect: "There wasn't some people in sight.", + correct: "There weren't any people in sight.", + explanation: "Use 'any' in negative statements, not 'some'", + grammarFocus: "some-any-usage" + }, + { + incorrect: "He goes to United States.", + correct: "He goes to the United States.", + explanation: "Some countries like 'the United States' require 'the'", + grammarFocus: "articles-usage" + } + ], + + // === COMPREHENSION QUESTIONS === + comprehension: [ + { + question: "Where does the writer like to sit?", + options: [ + "In the park", + "By the Wayle river", + "On the boat", + "In his garden" + ], + correct: "By the Wayle river", + explanation: "The writer says 'I like sitting by the Wayle on fine afternoons'" + }, + { + question: "What were the children doing?", + options: [ + "Swimming in the river", + "Playing games on the bank", + "Rowing on the river", + "Sitting by the water" + ], + correct: "Playing games on the bank", + explanation: "The text states 'Some children were playing games on the bank'" + }, + { + question: "Why didn't the man in the boat hear the people calling?", + options: [ + "He was deaf", + "The text doesn't say why", + "He was sleeping", + "The water was too loud" + ], + correct: "The text doesn't say why", + explanation: "The text only says 'he did not hear them' but doesn't give a reason" + }, + { + question: "What happened after the ball hit the man?", + options: [ + "He fell into the water", + "He got angry with the children", + "The children ran away", + "He threw the ball at the children" + ], + correct: "The children ran away", + explanation: "The text says 'there weren't any in sight: they had all run away!'" + }, + { + question: "How did the story end?", + options: [ + "The man was angry", + "The man called police", + "The man laughed and threw the ball back", + "The children came back" + ], + correct: "The man laughed and threw the ball back", + explanation: "The text ends with 'The man laughed... threw the ball back to the bank'" + } + ] +}; + +// ============================================================================ +// CONTENT STRUCTURE SUMMARY - FOR AI REFERENCE +// ============================================================================ +// +// This module represents INTERMEDIATE level English learning content for Chinese speakers: +// - Focus on past tense narrative and descriptive language +// - Grammar emphasis on articles (the/a/an) and quantifiers (some/any) +// - Rich vocabulary around outdoor activities and everyday situations +// - Complex sentence structures with subordinate clauses +// - Cultural context of British/Western recreational activities +// +// LEARNING OBJECTIVES: +// - Master use of definite/indefinite articles with different noun types +// - Understand some/any usage in different sentence types +// - Practice past continuous vs simple past tense +// - Build vocabulary around outdoor activities and emotions +// - Develop reading comprehension of narrative texts +// +// DIFFICULTY INDICATORS: +// - Longer sentences with multiple clauses +// - Past perfect and continuous tenses +// - Abstract concepts (realization, consequences) +// - Cultural references (British countryside, recreational activities) +// - Advanced vocabulary (struck, realized, rowing, etc.) +// +// ============================================================================ \ No newline at end of file diff --git a/src/content/SBS-level-1.js b/src/content/SBS-level-1.js new file mode 100644 index 0000000..3793ab0 --- /dev/null +++ b/src/content/SBS-level-1.js @@ -0,0 +1,479 @@ +// === ENGLISH LEARNING MODULE === +// Complete English learning module with Chinese translation and pronunciation + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.SBSLevel1 = { + id: "sbs-level-1", + name: "SBS-1", + description: "English introduction lessons with Chinese translation and pronunciation", + difficulty: "beginner", + language: "en-US", + userLanguage: "zh-CN", + totalWords: 150, + + // === GRAMMAR LESSONS SYSTEM === + grammar: { + "to-be-verb": { + title: "The Verb 'To Be' - ๅŠจ่ฏBe", + explanation: "The verb 'be' is one of the most important verbs in English, used to describe states, identity, and location.", + rules: [ + "I am - ๆˆ‘ๆ˜ฏ (first person singular)", + "You are - ไฝ ๆ˜ฏ/ไฝ ไปฌๆ˜ฏ (second person)", + "He/She/It is - ไป–/ๅฅน/ๅฎƒๆ˜ฏ (third person singular)", + "We are - ๆˆ‘ไปฌๆ˜ฏ (first person plural)", + "They are - ไป–ไปฌๆ˜ฏ (third person plural)" + ], + examples: [ + { + english: "My name is Maria.", + chinese: "ๆˆ‘็š„ๅๅญ—ๆ˜ฏ็Ž›ไธฝไบšใ€‚", + explanation: "Use 'is' because 'name' is third person singular", + pronunciation: "/maษช neษชm ษชz mษ™หˆriหษ™/" + }, + { + english: "I am from Mexico City.", + chinese: "ๆˆ‘ๆฅ่‡ชๅขจ่ฅฟๅ“ฅๅŸŽใ€‚", + explanation: "Use 'am' because the subject is 'I'", + pronunciation: "/aษช รฆm frสŒm หˆmeksษชkoสŠ หˆsษชti/" + }, + { + english: "Where are you from?", + chinese: "ไฝ ๆฅ่‡ชๅ“ช้‡Œ๏ผŸ", + explanation: "Use 'are' because the subject is 'you'", + pronunciation: "/wer ษ‘r ju frสŒm/" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "My address _____ 235 Main Street.", + options: ["am", "is", "are"], + correct: "is", + explanation: "Use 'is' because 'address' is third person singular" + }, + { + type: "translation", + english: "What's your phone number?", + chinese: "ไฝ ็š„็”ต่ฏๅท็ ๆ˜ฏๅคšๅฐ‘๏ผŸ", + focus: "Contraction What's = What is" + } + ] + }, + + "contractions": { + title: "Contractions - ็ผฉๅ†™ๅฝขๅผ", + explanation: "English often uses contractions to make conversation more natural and fluent.", + rules: [ + "What's = What is - ไป€ไนˆๆ˜ฏ", + "I'm = I am - ๆˆ‘ๆ˜ฏ", + "You're = You are - ไฝ ๆ˜ฏ", + "He's/She's/It's = He/She/It is - ไป–/ๅฅน/ๅฎƒๆ˜ฏ" + ], + examples: [ + { + english: "What's your name?", + chinese: "ไฝ ๅซไป€ไนˆๅๅญ—๏ผŸ", + explanation: "What's is the contraction of What is", + pronunciation: "/wสŒts jสŠr neษชm/" + }, + { + english: "I'm Nancy Lee.", + chinese: "ๆˆ‘ๆ˜ฏๅ—ๅธŒยทๆŽใ€‚", + explanation: "I'm is the contraction of I am", + pronunciation: "/aษชm หˆnรฆnsi li/" + } + ], + exercises: [ + { + type: "contraction_match", + full_form: "What is your address?", + contracted: "What's your address?", + chinese: "ไฝ ็š„ๅœฐๅ€ๆ˜ฏไป€ไนˆ๏ผŸ" + } + ] + }, + + "personal-information": { + title: "Personal Information - ไธชไบบไฟกๆฏ", + explanation: "Learn how to ask for and provide basic personal information in English.", + rules: [ + "Name - ๅง“ๅ: What's your name? My name is...", + "Address - ๅœฐๅ€: What's your address? My address is...", + "Phone - ็”ต่ฏ: What's your phone number? My phone number is...", + "Origin - ๆฅๆบ: Where are you from? I'm from..." + ], + examples: [ + { + english: "My name is David Carter.", + chinese: "ๆˆ‘็š„ๅๅญ—ๆ˜ฏๅคงๅซยทๅก็‰นใ€‚", + explanation: "Standard expression for introducing name", + pronunciation: "/maษช neษชm ษชz หˆdeษชvษชd หˆkษ‘rtษ™r/" + }, + { + english: "I'm from San Francisco.", + chinese: "ๆˆ‘ๆฅ่‡ชๆ—ง้‡‘ๅฑฑใ€‚", + explanation: "Expression for stating origin", + pronunciation: "/aษชm frสŒm sรฆn frรฆnหˆsษชskoสŠ/" + } + ], + exercises: [ + { + type: "dialogue_completion", + prompt: "A: What's your name? B: _____", + answer: "My name is [your name].", + chinese: "A: ไฝ ๅซไป€ไนˆๅๅญ—๏ผŸ B: ๆˆ‘็š„ๅๅญ—ๆ˜ฏ[ไฝ ็š„ๅๅญ—]ใ€‚" + } + ] + }, + + "meeting-people": { + title: "Meeting People - ไธŽไบบ่ง้ข", + explanation: "Common phrases and expressions used when meeting new people.", + rules: [ + "Hello - ไฝ ๅฅฝ (formal greeting)", + "Hi - ๅ—จ (informal greeting)", + "Nice to meet you - ๅพˆ้ซ˜ๅ…ด่ฎค่ฏ†ไฝ ", + "Nice to meet you, too - ๆˆ‘ไนŸๅพˆ้ซ˜ๅ…ด่ฎค่ฏ†ไฝ " + ], + examples: [ + { + english: "Hello. My name is Peter Lewis.", + chinese: "ไฝ ๅฅฝใ€‚ๆˆ‘็š„ๅๅญ—ๆ˜ฏๅฝผๅพ—ยทๅˆ˜ๆ˜“ๆ–ฏใ€‚", + explanation: "Formal introduction", + pronunciation: "/hษ™หˆloสŠ maษช neษชm ษชz หˆpitษ™r หˆluษชs/" + }, + { + english: "Hi. I'm Nancy Lee. Nice to meet you.", + chinese: "ๅ—จใ€‚ๆˆ‘ๆ˜ฏๅ—ๅธŒยทๆŽใ€‚ๅพˆ้ซ˜ๅ…ด่ฎค่ฏ†ไฝ ใ€‚", + explanation: "Informal introduction with greeting", + pronunciation: "/haษช aษชm หˆnรฆnsi li naษชs tu mit ju/" + } + ], + exercises: [ + { + type: "role_play", + scenario: "Meeting someone new", + dialogue: "A: Hello. B: Hi. A: What's your name? B: My name is ____." + } + ] + } + }, + + vocabulary: { + "name": { + "user_language": "ๅๅญ—", + "type": "noun", + "pronunciation": "/neษชm/" + }, + "address": { + "user_language": "ๅœฐๅ€", + "type": "noun", + "pronunciation": "/ษ™หˆdres/" + }, + "phone number": { + "user_language": "็”ต่ฏๅท็ ", + "type": "noun", + "pronunciation": "/foสŠn หˆnสŒmbษ™r/" + }, + "telephone number": { + "user_language": "็”ต่ฏๅท็ ", + "type": "noun", + "pronunciation": "/หˆtelษ™foสŠn หˆnสŒmbษ™r/" + }, + "apartment number": { + "user_language": "ๅ…ฌๅฏ“ๅท็ ", + "type": "noun", + "pronunciation": "/ษ™หˆpษ‘rtmษ™nt หˆnสŒmbษ™r/" + }, + "e-mail address": { + "user_language": "็”ตๅญ้‚ฎไปถๅœฐๅ€", + "type": "noun", + "pronunciation": "/หˆiหmeษชl ษ™หˆdres/" + }, + "first name": { + "user_language": "ๅ", + "type": "noun", + "pronunciation": "/fษœrst neษชm/" + }, + "last name": { + "user_language": "ๅง“", + "type": "noun", + "pronunciation": "/lรฆst neษชm/" + }, + "hello": { + "user_language": "ไฝ ๅฅฝ", + "type": "interjection", + "pronunciation": "/hษ™หˆloสŠ/" + }, + "hi": { + "user_language": "ๅ—จ", + "type": "interjection", + "pronunciation": "/haษช/" + }, + "nice to meet you": { + "user_language": "ๅพˆ้ซ˜ๅ…ด่ฎค่ฏ†ไฝ ", + "type": "phrase", + "pronunciation": "/naษชs tu mit ju/" + }, + "where": { + "user_language": "ๅ“ช้‡Œ", + "type": "adverb", + "pronunciation": "/wer/" + }, + "from": { + "user_language": "ๆฅ่‡ช", + "type": "preposition", + "pronunciation": "/frสŒm/" + }, + "alphabet": { + "user_language": "ๅญ—ๆฏ่กจ", + "type": "noun", + "pronunciation": "/หˆรฆlfษ™bet/" + }, + "numbers": { + "user_language": "ๆ•ฐๅญ—", + "type": "noun", + "pronunciation": "/หˆnสŒmbษ™rz/" + } + }, + + story: { + title: "To Be: Introduction - ๅŠจ่ฏBe็š„ไป‹็ป", + totalSentences: 50, + chapters: [ + { + title: "Chapter 1: Vocabulary Preview - ็ฌฌไธ€็ซ ๏ผš่ฏๆฑ‡้ข„่งˆ", + sentences: [ + { + id: 1, + original: "Learn the alphabet Aa Bb Cc Dd Ee Ff Gg Hh Ii Jj Kk Ll Mm Nn Oo Pp Qq Rr Ss Tt Uu Vv Ww Xx Yy Zz", + translation: "ๅญฆไน ๅญ—ๆฏ่กจ Aa Bb Cc Dd Ee Ff Gg Hh Ii Jj Kk Ll Mm Nn Oo Pp Qq Rr Ss Tt Uu Vv Ww Xx Yy Zz", + words: [ + {word: "Learn", translation: "ๅญฆไน ", type: "verb", pronunciation: "/lษœrn/"}, + {word: "alphabet", translation: "ๅญ—ๆฏ่กจ", type: "noun", pronunciation: "/หˆรฆlfษ™bet/"} + ] + }, + { + id: 2, + original: "Practice numbers 0 1 2 3 4 5 6 7 8 9 10", + translation: "็ปƒไน ๆ•ฐๅญ— 0 1 2 3 4 5 6 7 8 9 10", + words: [ + {word: "Practice", translation: "็ปƒไน ", type: "verb", pronunciation: "/หˆprรฆktษชs/"}, + {word: "numbers", translation: "ๆ•ฐๅญ—", type: "noun", pronunciation: "/หˆnสŒmbษ™rz/"} + ] + }, + { + id: 3, + original: "This is Maria's name tag.", + translation: "่ฟ™ๆ˜ฏ็Ž›ไธฝไบš็š„ๅง“ๅ็‰Œใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "/รฐษชs/"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "/ษชz/"}, + {word: "Maria's", translation: "็Ž›ไธฝไบš็š„", type: "possessive", pronunciation: "/mษ™หˆriหษ™z/"}, + {word: "name", translation: "ๅง“ๅ", type: "noun", pronunciation: "/neษชm/"}, + {word: "tag", translation: "็‰Œ", type: "noun", pronunciation: "/tรฆg/"} + ] + }, + { + id: 4, + original: "235 Main Street is an address.", + translation: "ไธป่ก—235ๅทๆ˜ฏไธ€ไธชๅœฐๅ€ใ€‚", + words: [ + {word: "235", translation: "235", type: "number", pronunciation: "/tu หˆฮธษœrti faษชv/"}, + {word: "Main", translation: "ไธป่ฆ็š„", type: "adjective", pronunciation: "/meษชn/"}, + {word: "Street", translation: "่ก—", type: "noun", pronunciation: "/strit/"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "/ษชz/"}, + {word: "an", translation: "ไธ€ไธช", type: "article", pronunciation: "/รฆn/"}, + {word: "address", translation: "ๅœฐๅ€", type: "noun", pronunciation: "/ษ™หˆdres/"} + ] + }, + { + id: 5, + original: "741-8906 is a telephone number.", + translation: "741-8906ๆ˜ฏไธ€ไธช็”ต่ฏๅท็ ใ€‚", + words: [ + {word: "741-8906", translation: "741-8906", type: "number", pronunciation: "/หˆsevษ™n fษ”r wสŒn eษชt naษชn oสŠ sษชks/"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "/ษชz/"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "/ษ™/"}, + {word: "telephone", translation: "็”ต่ฏ", type: "noun", pronunciation: "/หˆtelษ™foสŠn/"}, + {word: "number", translation: "ๅท็ ", type: "noun", pronunciation: "/หˆnสŒmbษ™r/"} + ] + } + ] + }, + { + title: "Chapter 2: What's Your Name? - ็ฌฌไบŒ็ซ ๏ผšไฝ ๅซไป€ไนˆๅๅญ—๏ผŸ", + sentences: [ + { + id: 6, + original: "What's your name?", + translation: "ไฝ ๅซไป€ไนˆๅๅญ—๏ผŸ", + words: [ + {word: "What's", translation: "ไป€ไนˆๆ˜ฏ", type: "contraction", pronunciation: "/wสŒts/"}, + {word: "your", translation: "ไฝ ็š„", type: "possessive", pronunciation: "/jสŠr/"}, + {word: "name", translation: "ๅๅญ—", type: "noun", pronunciation: "/neษชm/"} + ] + }, + { + id: 7, + original: "My name is Maria.", + translation: "ๆˆ‘็š„ๅๅญ—ๆ˜ฏ็Ž›ไธฝไบšใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "possessive", pronunciation: "/maษช/"}, + {word: "name", translation: "ๅๅญ—", type: "noun", pronunciation: "/neษชm/"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "/ษชz/"}, + {word: "Maria", translation: "็Ž›ไธฝไบš", type: "name", pronunciation: "/mษ™หˆriหษ™/"} + ] + }, + { + id: 8, + original: "What's your address?", + translation: "ไฝ ็š„ๅœฐๅ€ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What's", translation: "ไป€ไนˆๆ˜ฏ", type: "contraction", pronunciation: "/wสŒts/"}, + {word: "your", translation: "ไฝ ็š„", type: "possessive", pronunciation: "/jสŠr/"}, + {word: "address", translation: "ๅœฐๅ€", type: "noun", pronunciation: "/ษ™หˆdres/"} + ] + }, + { + id: 9, + original: "My address is 235 Main Street.", + translation: "ๆˆ‘็š„ๅœฐๅ€ๆ˜ฏไธป่ก—235ๅทใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "possessive", pronunciation: "/maษช/"}, + {word: "address", translation: "ๅœฐๅ€", type: "noun", pronunciation: "/ษ™หˆdres/"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "/ษชz/"}, + {word: "235", translation: "235", type: "number", pronunciation: "/tu หˆฮธษœrti faษชv/"}, + {word: "Main", translation: "ไธป่ฆ็š„", type: "adjective", pronunciation: "/meษชn/"}, + {word: "Street", translation: "่ก—", type: "noun", pronunciation: "/strit/"} + ] + }, + { + id: 10, + original: "I'm from Mexico City.", + translation: "ๆˆ‘ๆฅ่‡ชๅขจ่ฅฟๅ“ฅๅŸŽใ€‚", + words: [ + {word: "I'm", translation: "ๆˆ‘ๆ˜ฏ", type: "contraction", pronunciation: "/aษชm/"}, + {word: "from", translation: "ๆฅ่‡ช", type: "preposition", pronunciation: "/frสŒm/"}, + {word: "Mexico", translation: "ๅขจ่ฅฟๅ“ฅ", type: "place", pronunciation: "/หˆmeksษชkoสŠ/"}, + {word: "City", translation: "ๅŸŽ", type: "noun", pronunciation: "/หˆsษชti/"} + ] + } + ] + }, + { + title: "Chapter 3: Meeting People - ็ฌฌไธ‰็ซ ๏ผšไธŽไบบ่ง้ข", + sentences: [ + { + id: 11, + original: "Hello. My name is Peter Lewis.", + translation: "ไฝ ๅฅฝใ€‚ๆˆ‘็š„ๅๅญ—ๆ˜ฏๅฝผๅพ—ยทๅˆ˜ๆ˜“ๆ–ฏใ€‚", + words: [ + {word: "Hello", translation: "ไฝ ๅฅฝ", type: "interjection", pronunciation: "/hษ™หˆloสŠ/"}, + {word: "My", translation: "ๆˆ‘็š„", type: "possessive", pronunciation: "/maษช/"}, + {word: "name", translation: "ๅๅญ—", type: "noun", pronunciation: "/neษชm/"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "/ษชz/"}, + {word: "Peter", translation: "ๅฝผๅพ—", type: "name", pronunciation: "/หˆpitษ™r/"}, + {word: "Lewis", translation: "ๅˆ˜ๆ˜“ๆ–ฏ", type: "name", pronunciation: "/หˆluษชs/"} + ] + }, + { + id: 12, + original: "Hi. I'm Nancy Lee. Nice to meet you.", + translation: "ๅ—จใ€‚ๆˆ‘ๆ˜ฏๅ—ๅธŒยทๆŽใ€‚ๅพˆ้ซ˜ๅ…ด่ฎค่ฏ†ไฝ ใ€‚", + words: [ + {word: "Hi", translation: "ๅ—จ", type: "interjection", pronunciation: "/haษช/"}, + {word: "I'm", translation: "ๆˆ‘ๆ˜ฏ", type: "contraction", pronunciation: "/aษชm/"}, + {word: "Nancy", translation: "ๅ—ๅธŒ", type: "name", pronunciation: "/หˆnรฆnsi/"}, + {word: "Lee", translation: "ๆŽ", type: "name", pronunciation: "/li/"}, + {word: "Nice", translation: "ๅพˆๅฅฝ็š„", type: "adjective", pronunciation: "/naษชs/"}, + {word: "to", translation: "ๅˆฐ", type: "preposition", pronunciation: "/tu/"}, + {word: "meet", translation: "้‡่ง", type: "verb", pronunciation: "/mit/"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "/ju/"} + ] + }, + { + id: 13, + original: "Nice to meet you, too.", + translation: "ๆˆ‘ไนŸๅพˆ้ซ˜ๅ…ด่ฎค่ฏ†ไฝ ใ€‚", + words: [ + {word: "Nice", translation: "ๅพˆๅฅฝ็š„", type: "adjective", pronunciation: "/naษชs/"}, + {word: "to", translation: "ๅˆฐ", type: "preposition", pronunciation: "/tu/"}, + {word: "meet", translation: "้‡่ง", type: "verb", pronunciation: "/mit/"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "/ju/"}, + {word: "too", translation: "ไนŸ", type: "adverb", pronunciation: "/tu/"} + ] + } + ] + } + ] + }, + + // === GRAMMAR-BASED FILL IN THE BLANKS === + fillInBlanks: [ + { + sentence: "My name _____ David.", + options: ["am", "is", "are"], + correctAnswer: "is", + explanation: "Use 'is' because 'name' is third person singular", + grammarFocus: "to-be-verb" + }, + { + sentence: "I _____ from China.", + options: ["am", "is", "are"], + correctAnswer: "am", + explanation: "Use 'am' because the subject is 'I'", + grammarFocus: "to-be-verb" + }, + { + sentence: "_____ your phone number?", + options: ["What", "What's", "Where"], + correctAnswer: "What's", + explanation: "What's = What is, used to ask for phone number", + grammarFocus: "contractions" + }, + { + sentence: "Where _____ you from?", + options: ["am", "is", "are"], + correctAnswer: "are", + explanation: "Use 'are' because the subject is 'you'", + grammarFocus: "to-be-verb" + }, + { + sentence: "_____ to meet you.", + options: ["Nice", "Good", "Fine"], + correctAnswer: "Nice", + explanation: "Standard expression for meeting people", + grammarFocus: "meeting-people" + } + ], + + // === GRAMMAR CORRECTION EXERCISES === + corrections: [ + { + incorrect: "My name are John.", + correct: "My name is John.", + explanation: "'Name' is third person singular, so use 'is'", + grammarFocus: "to-be-verb" + }, + { + incorrect: "Where you are from?", + correct: "Where are you from?", + explanation: "In questions, the be verb comes before the subject", + grammarFocus: "to-be-verb" + }, + { + incorrect: "What is you name?", + correct: "What is your name?", + explanation: "Use possessive 'your' not subject pronoun 'you'", + grammarFocus: "personal-information" + }, + { + incorrect: "I are from Mexico.", + correct: "I am from Mexico.", + explanation: "Use 'am' with subject 'I'", + grammarFocus: "to-be-verb" + } + ] +}; \ No newline at end of file diff --git a/src/content/WTA1B1-documented.js b/src/content/WTA1B1-documented.js new file mode 100644 index 0000000..4e55e98 --- /dev/null +++ b/src/content/WTA1B1-documented.js @@ -0,0 +1,1252 @@ +// === ENGLISH LETTERS AND PETS STORY === +// Complete English story with Chinese translation and pronunciation + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.WTA1B1 = { + id: "wta1b1", + name: "WTA1B-1", + description: "English learning story with letters U, V, T and pet vocabulary", + difficulty: "beginner", + language: "en-US", + userLanguage: "zh-CN", + totalWords: 150, + + // === GRAMMAR LESSONS SYSTEM === + grammar: { + "demonstrative-pronouns": { + title: "Demonstrative Pronouns - ๆŒ‡็คบไปฃ่ฏ", + explanation: "English uses specific words to point to things that are near or far, singular or plural.", + rules: [ // Array of simple rule statements + "this - for one thing that is close", // Rule 1: Singular + proximity + "that - for one thing that is far", // Rule 2: Singular + distance + "these - for multiple things that are close", // Rule 3: Plural + proximity + "those - for multiple things that are far" // Rule 4: Plural + distance + ], + examples: [ + { + english: "What is this?", // Source sentence - used in Story Reader, Quiz Game + chinese: "่ฟ™ๆ˜ฏไป€ไนˆ๏ผŸ", // Translation - displayed in UI, used by Memory Match + explanation: "Use 'this' for one thing close to you", // Teaching note - shown in Grammar Discovery + pronunciation: "wสŒt ษชz รฐษชs" // IPA format - processed by TTS engine + }, + { + english: "What are those?", // Plural demonstrative question + chinese: "้‚ฃไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", // Chinese equivalent - cultural context preserved + explanation: "Use 'those' for multiple things far from you", // Grammar rule reinforcement + pronunciation: "wสŒt ษ‘หr รฐoสŠz" // Phonetic guide for pronunciation games + }, + { + english: "These are rabbits.", // Statement with plural demonstrative + chinese: "่ฟ™ไบ›ๆ˜ฏๅ…”ๅญใ€‚", // Direct translation - maintains sentence structure + explanation: "Use 'these' for multiple things close to you", // Pattern explanation + pronunciation: "รฐiหz ษ‘หr rรฆbษชts" // Sounds for audio-based learning + }, + { + english: "That is my pet bird.", + chinese: "้‚ฃๆ˜ฏๆˆ‘็š„ๅฎ ็‰ฉ้ธŸใ€‚", + explanation: "Use 'that' for one thing far from you", + pronunciation: "รฐรฆt ษชz maษช pet bษœหrd" + }, + { + english: "This cat is very cute.", + chinese: "่ฟ™ๅช็Œซๅพˆๅฏ็ˆฑใ€‚", + explanation: "Use 'this' when pointing to something nearby", + pronunciation: "รฐษชs kรฆt ษชz veri kjuหt" + }, + { + english: "Are these your turtles?", + chinese: "่ฟ™ไบ›ๆ˜ฏไฝ ็š„ไนŒ้พŸๅ—๏ผŸ", + explanation: "Use 'these' in questions about nearby plural things", + pronunciation: "ษ‘หr รฐiหz jสŠr tษœหrtษ™lz" + }, + { + english: "Those dogs are playing.", + chinese: "้‚ฃไบ›็‹—ๅœจ็Žฉ่€ใ€‚", + explanation: "Use 'those' for distant plural animals or things", + pronunciation: "รฐoสŠz dษ”หgz ษ‘หr pleษชษชล‹" + }, + { + english: "This is my favorite hamster.", + chinese: "่ฟ™ๆ˜ฏๆˆ‘ๆœ€ๅ–œๆฌข็š„ไป“้ผ ใ€‚", + explanation: "Use 'this' to introduce something specific and close", + pronunciation: "รฐษชs ษชz maษช feษชvษ™rษชt hรฆmstษ™r" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "_____ is a dog.", + options: ["This", "These", "That", "Those"], + correct: "This", + explanation: "Use 'This' for one thing close to you" + }, + { + type: "translation", + english: "Those are turtles.", + chinese: "้‚ฃไบ›ๆ˜ฏๆตท้พŸใ€‚", + focus: "Demonstrative pronoun for plural distant objects" + } + ] + }, + + "be-verb-usage": { + title: "Be Verb Usage - BeๅŠจ่ฏไฝฟ็”จ", + explanation: "English 'be' verbs change based on whether you're talking about one thing or many things.", + rules: [ + "is - used with singular nouns: It is a cat", + "are - used with plural nouns: They are dogs", + "Pattern: This/That + is, These/Those + are" + ], + examples: [ + { + english: "It is a bird.", + chinese: "ๅฎƒๆ˜ฏไธ€ๅช้ธŸใ€‚", + explanation: "Use 'is' with singular nouns", + pronunciation: "ษชt ษชz ษ™ bษœหrd" + }, + { + english: "They are birds.", + chinese: "ๅฎƒไปฌๆ˜ฏ้ธŸใ€‚", + explanation: "Use 'are' with plural nouns", + pronunciation: "รฐeษช ษ‘หr bษœหrdz" + }, + { + english: "Where is the cat?", + chinese: "็Œซๅœจๅ“ช้‡Œ๏ผŸ", + explanation: "Use 'is' when asking about one thing", + pronunciation: "wer ษชz รฐษ™ kรฆt" + }, + { + english: "The dog is happy.", + chinese: "็‹—ๅพˆ้ซ˜ๅ…ดใ€‚", + explanation: "Use 'is' with singular subjects and adjectives", + pronunciation: "รฐษ™ dษ”หg ษชz hรฆpi" + }, + { + english: "My pets are cute.", + chinese: "ๆˆ‘็š„ๅฎ ็‰ฉๅพˆๅฏ็ˆฑใ€‚", + explanation: "Use 'are' with plural nouns like 'pets'", + pronunciation: "maษช pets ษ‘หr kjuหt" + }, + { + english: "Where are the rabbits?", + chinese: "ๅ…”ๅญๅœจๅ“ช้‡Œ๏ผŸ", + explanation: "Use 'are' when asking about multiple things", + pronunciation: "wer ษ‘หr รฐษ™ rรฆbษชts" + }, + { + english: "This is my turtle.", + chinese: "่ฟ™ๆ˜ฏๆˆ‘็š„ไนŒ้พŸใ€‚", + explanation: "Use 'is' with demonstrative 'this'", + pronunciation: "รฐษชs ษชz maษช tษœหrtษ™l" + }, + { + english: "These are my friends.", + chinese: "่ฟ™ไบ›ๆ˜ฏๆˆ‘็š„ๆœ‹ๅ‹ใ€‚", + explanation: "Use 'are' with demonstrative 'these'", + pronunciation: "รฐiหz ษ‘หr maษช frends" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "They ____ turtles.", + options: ["is", "are", "am", "be"], + correct: "are", + explanation: "Use 'are' with plural subjects like 'they'" + } + ] + }, + + "prepositions-of-place": { + title: "Prepositions of Place - ๅœฐ็‚นไป‹่ฏ", + explanation: "English uses specific words to show where things are located.", + rules: [ + "on - things touching the top of something: on the chair", + "in - things inside something: in the box", + "under - things below something: under the table" + ], + examples: [ + { + english: "The cat is on the chair.", + chinese: "็Œซๅœจๆค…ๅญไธŠใ€‚", + explanation: "Use 'on' when something is touching the top", + pronunciation: "รฐษ™ kรฆt ษชz ษ‘หn รฐษ™ tสƒer" + }, + { + english: "The turtle is in the water.", + chinese: "ๆตท้พŸๅœจๆฐด้‡Œใ€‚", + explanation: "Use 'in' when something is inside or surrounded", + pronunciation: "รฐษ™ tษœหrtษ™l ษชz ษชn รฐษ™ wษ”หtษ™r" + }, + { + english: "The dog is under the table.", + chinese: "็‹—ๅœจๆกŒๅญไธ‹้ขใ€‚", + explanation: "Use 'under' when something is below another thing", + pronunciation: "รฐษ™ dษ”หg ษชz สŒndษ™r รฐษ™ teษชbษ™l" + }, + { + english: "The bird is on the tree.", + chinese: "้ธŸๅœจๆ ‘ไธŠใ€‚", + explanation: "Use 'on' for things resting on surfaces", + pronunciation: "รฐษ™ bษœหrd ษชz ษ‘หn รฐษ™ triห" + }, + { + english: "The rabbit is in the garden.", + chinese: "ๅ…”ๅญๅœจ่Šฑๅ›ญ้‡Œใ€‚", + explanation: "Use 'in' for enclosed or surrounded spaces", + pronunciation: "รฐษ™ rรฆbษชt ษชz ษชn รฐษ™ gษ‘หrdษ™n" + }, + { + english: "The hamster is under the bed.", + chinese: "ไป“้ผ ๅœจๅบŠไธ‹้ขใ€‚", + explanation: "Use 'under' for things below furniture", + pronunciation: "รฐษ™ hรฆmstษ™r ษชz สŒndษ™r รฐษ™ bed" + }, + { + english: "My pet is on the sofa.", + chinese: "ๆˆ‘็š„ๅฎ ็‰ฉๅœจๆฒ™ๅ‘ไธŠใ€‚", + explanation: "Use 'on' when pets sit on furniture", + pronunciation: "maษช pet ษชz ษ‘หn รฐษ™ soสŠfษ™" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "The rabbit is ____ the box.", + options: ["on", "in", "under", "at"], + correct: "in", + explanation: "Use 'in' when something is inside a container" + } + ] + }, + + "modal-can": { + title: "Modal Verb 'Can' - ๆƒ…ๆ€ๅŠจ่ฏcan", + explanation: "English uses 'can' to talk about abilities - things someone is able to do.", + rules: [ + "can + verb (base form) - expresses ability: can sing, can swim", + "can't = cannot - negative form: can't fly", + "Can + subject + verb? - question form: Can birds fly?" + ], + examples: [ + { + english: "She can sing.", + chinese: "ๅฅนไผšๅ”ฑๆญŒใ€‚", + explanation: "Use 'can' + base verb to show ability", + pronunciation: "สƒi kรฆn sษชล‹" + }, + { + english: "I can't find Ding Ding!", + chinese: "ๆˆ‘ๆ‰พไธๅˆฐไธไธ๏ผ", + explanation: "Use 'can't' for negative ability", + pronunciation: "aษช kรฆnt faษชnd dษชล‹ dษชล‹" + }, + { + english: "What can Ding Ding do?", + chinese: "ไธไธ่ƒฝๅšไป€ไนˆ๏ผŸ", + explanation: "Use 'can' in questions about ability", + pronunciation: "wสŒt kรฆn dษชล‹ dษชล‹ du" + }, + { + english: "Dogs can run very fast.", + chinese: "็‹—่ท‘ๅพ—ๅพˆๅฟซใ€‚", + explanation: "Use 'can' to describe general abilities", + pronunciation: "dษ”หgz kรฆn rสŒn veri fรฆst" + }, + { + english: "Fish can't walk on land.", + chinese: "้ฑผไธ่ƒฝๅœจ้™†ๅœฐไธŠ่ตฐ่ทฏใ€‚", + explanation: "Use 'can't' for impossible abilities", + pronunciation: "fษชสƒ kรฆnt wษ”หk ษ‘หn lรฆnd" + }, + { + english: "Can cats climb trees?", + chinese: "็Œซ่ƒฝ็ˆฌๆ ‘ๅ—๏ผŸ", + explanation: "Use 'Can' at the start of yes/no questions", + pronunciation: "kรฆn kรฆts klaษชm triหz" + }, + { + english: "Birds can fly in the sky.", + chinese: "้ธŸ่ƒฝๅœจๅคฉ็ฉบไธญ้ฃž่กŒใ€‚", + explanation: "Use 'can' for natural abilities", + pronunciation: "bษœหrdz kรฆn flaษช ษชn รฐษ™ skaษช" + }, + { + english: "I can take care of pets.", + chinese: "ๆˆ‘่ƒฝ็…ง้กพๅฎ ็‰ฉใ€‚", + explanation: "Use 'can' for learned skills", + pronunciation: "aษช kรฆn teษชk ker สŒv pets" + }, + { + english: "Turtles can't run quickly.", + chinese: "ไนŒ้พŸไธ่ƒฝ่ท‘ๅพ—ๅพˆๅฟซใ€‚", + explanation: "Use 'can't' for limited abilities", + pronunciation: "tษœหrtษ™lz kรฆnt rสŒn kwษชkli" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "Turtles ____ swim.", + options: ["can", "can't", "is", "are"], + correct: "can", + explanation: "Use 'can' to show natural ability" + } + ] + } + }, + + // === LETTERS DISCOVERY SYSTEM === + letters: { + "U": [ + { + word: "unhappy", // Target vocabulary - used in Whack-a-Mole, Word Storm + translation: "ไธๅผ€ๅฟƒ็š„", // Chinese meaning - displayed in all translation games + type: "adjective", // Grammar category - used for word type filtering + pronunciation: "สŒnhรฆpi", // Phonetic guide - enables TTS pronunciation + example: "The cat looks unhappy." // Usage context - shown in vocabulary explanations + }, + { + word: "umbrella", // Concrete noun - good for visual memory games + translation: "้›จไผž", // Common Chinese object - familiar to learners + type: "noun", // Object category - used in noun-focused exercises + pronunciation: "สŒmbrษ›lษ™", // Complex pronunciation - practice for advanced learners + example: "I need an umbrella when it rains." // Real-world usage - context building + }, + { + word: "up", // Simple directional word - beginner friendly + translation: "ๅ‘ไธŠ", // Basic direction concept - easy to visualize + type: "adverb", // Movement modifier - teaches spatial concepts + pronunciation: "สŒp", // Short sound - easy pronunciation practice + example: "The bird flies up high." // Action context - demonstrates usage in motion + }, + { + word: "under", // Spatial preposition - key for location games + translation: "ๅœจ...ไธ‹้ข", // Relational concept - important for grammar + type: "preposition", // Connecting word type - links objects and locations + pronunciation: "สŒndษ™r", // Clear consonant sounds - good for phonics practice + example: "The cat hides under the table." // Spatial relationship - concrete scenario + } + ], + "V": [ + { + word: "violet", + translation: "็ดซ่‰ฒ็š„", + type: "adjective", + pronunciation: "vaษชษ™lษ™t", + example: "She has a violet dress." + }, + { + word: "van", + translation: "้ขๅŒ…่ฝฆ", + type: "noun", + pronunciation: "vรฆn", + example: "The vet drives a white van." + }, + { + word: "vet", + translation: "ๅ…ฝๅŒป", + type: "noun", + pronunciation: "vษ›t", + example: "The vet takes care of pets." + }, + { + word: "vest", + translation: "่ƒŒๅฟƒ", + type: "noun", + pronunciation: "vษ›st", + example: "He wears a warm vest." + } + ], + "T": [ + { + word: "tall", + translation: "้ซ˜็š„", + type: "adjective", + pronunciation: "tษ”l", + example: "The teacher is very tall." + }, + { + word: "turtle", + translation: "ๆตท้พŸ", + type: "noun", + pronunciation: "tษœrtษ™l", + example: "The turtle moves slowly." + }, + { + word: "tent", + translation: "ๅธ็ฏท", + type: "noun", + pronunciation: "tษ›nt", + example: "We sleep in a tent when camping." + }, + { + word: "tiger", + translation: "่€่™Ž", + type: "noun", + pronunciation: "taษชgษ™r", + example: "The tiger is a big cat." + } + ] + }, + + vocabulary: { + "unhappy": { // Key = English word - used as primary identifier in all games + "user_language": "ไธๅผ€ๅฟƒ็š„", // Chinese translation - core data for learning + "type": "adjective", // Grammar type - enables filtering by word categories + "pronunciation": "สŒnhรฆpi" // IPA notation - supports audio generation and pronunciation scoring + }, + "umbrella": { + "user_language": "้›จไผž", // Object name - visual association possible + "type": "noun", // Concrete noun - good for memory matching + "pronunciation": "สŒmbrษ›lษ™" // Multi-syllable word - pronunciation challenge + }, + "up": { + "user_language": "ๅ‘ไธŠ", + "type": "adverb", + "pronunciation": "สŒp" + }, + "under": { + "user_language": "ๅœจ...ไธ‹้ข", + "type": "preposition", + "pronunciation": "สŒndษ™r" + }, + "uncle": { + "user_language": "ๅ”ๅ”", + "type": "noun", + "pronunciation": "สŒล‹kษ™l" + }, + "violet": { + "user_language": "็ดซ่‰ฒ็š„", + "type": "adjective", + "pronunciation": "vaษชษ™lษ™t" + }, + "van": { + "user_language": "้ขๅŒ…่ฝฆ", + "type": "noun", + "pronunciation": "vรฆn" + }, + "vet": { + "user_language": "ๅ…ฝๅŒป", + "type": "noun", + "pronunciation": "vษ›t" + }, + "vest": { + "user_language": "่ƒŒๅฟƒ", + "type": "noun", + "pronunciation": "vษ›st" + }, + "violin": { + "user_language": "ๅฐๆ็ด", + "type": "noun", + "pronunciation": "vaษชษ™lษชn" + }, + "tall": { + "user_language": "้ซ˜็š„", + "type": "adjective", + "pronunciation": "tษ”l" + }, + "turtle": { + "user_language": "ๆตท้พŸ", + "type": "noun", + "pronunciation": "tษœrtษ™l" + }, + "tent": { + "user_language": "ๅธ็ฏท", + "type": "noun", + "pronunciation": "tษ›nt" + }, + "tiger": { + "user_language": "่€่™Ž", + "type": "noun", + "pronunciation": "taษชgษ™r" + }, + "teacher": { + "user_language": "่€ๅธˆ", + "type": "noun", + "pronunciation": "titสƒษ™r" + }, + "dog": { + "user_language": "็‹—", // Simple animal - universally understood + "type": "noun", // Animal category - used in themed exercises + "pronunciation": "dษ”g" // Simple pronunciation - beginner-friendly + }, + "cat": { + "user_language": "็Œซ", // Common pet - relatable to children + "type": "noun", // Animal noun - pairs well with dog in exercises + "pronunciation": "kรฆt" // Short vowel sound - phonics practice + }, + "bird": { + "user_language": "้ธŸ", + "type": "noun", + "pronunciation": "bษœrd" + }, + "rabbit": { + "user_language": "ๅ…”ๅญ", + "type": "noun", + "pronunciation": "rรฆbษชt" + }, + "hamster": { + "user_language": "ไป“้ผ ", + "type": "noun", + "pronunciation": "hรฆmstษ™r" + }, + "sofa": { + "user_language": "ๆฒ™ๅ‘", + "type": "noun", + "pronunciation": "soสŠfษ™" + }, + "table": { + "user_language": "ๆกŒๅญ", + "type": "noun", + "pronunciation": "teษชbษ™l" + }, + "chair": { + "user_language": "ๆค…ๅญ", + "type": "noun", + "pronunciation": "tสƒษ›r" + }, + "box": { + "user_language": "็›’ๅญ", + "type": "noun", + "pronunciation": "bษ‘ks" + }, + "cupboard": { + "user_language": "ๆฉฑๆŸœ", + "type": "noun", + "pronunciation": "kสŒbษ™rd" + }, + "shelf": { + "user_language": "ๆžถๅญ", + "type": "noun", + "pronunciation": "สƒษ›lf" + } + }, + + story: { + title: "The Pet Adventure - ๅฎ ็‰ฉๅކ้™ฉ่ฎฐ", + totalSentences: 25, + chapters: [ + { + title: "Chapter 1: Choosing a Pet - ็ฌฌไธ€็ซ ๏ผš้€‰ๆ‹ฉๅฎ ็‰ฉ", + sentences: [ + { + id: 1, + original: "What is this?", + translation: "่ฟ™ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ // Word-by-word breakdown - enables detailed language analysis + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, // Question word - starts interrogative pattern + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, // Be verb - fundamental English grammar + {word: "this", translation: "่ฟ™ไธช", type: "pronoun", pronunciation: "รฐษชs"} // Demonstrative - teaches proximity concept + ] + }, + { + id: 2, + original: "It is a dog.", + translation: "่ฟ™ๆ˜ฏไธ€ๅช็‹—ใ€‚", + words: [ // Breakdown shows article usage and noun introduction + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, // Subject pronoun - sentence structure foundation + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, // Linking verb - connects subject to object + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, // Indefinite article - introduces countable nouns + {word: "dog", translation: "็‹—", type: "noun", pronunciation: "dษ”g"} // Concrete noun - vocabulary building target + ] + }, + { + id: 3, + original: "What is that?", + translation: "้‚ฃๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "that", translation: "้‚ฃไธช", type: "pronoun", pronunciation: "รฐรฆt"} + ] + }, + { + id: 4, + original: "It is a hamster.", + translation: "ๅฎƒๆ˜ฏไธ€ๅชไป“้ผ ใ€‚", + words: [ + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "hamster", translation: "ไป“้ผ ", type: "noun", pronunciation: "hรฆmstษ™r"} + ] + }, + { + id: 5, + original: "What are these?", + translation: "่ฟ™ไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "these", translation: "่ฟ™ไบ›", type: "pronoun", pronunciation: "รฐiz"} + ] + }, + { + id: 6, + original: "They are rabbits.", + translation: "ๅฎƒไปฌๆ˜ฏๅ…”ๅญใ€‚", + words: [ + {word: "They", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "rabbits", translation: "ๅ…”ๅญ", type: "noun", pronunciation: "rรฆbษชts"} + ] + }, + { + id: 7, + original: "What are those?", + translation: "้‚ฃไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"} + ] + }, + { + id: 8, + original: "They are turtles.", + translation: "ๅฎƒไปฌๆ˜ฏๆตท้พŸใ€‚", + words: [ + {word: "They", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "turtles", translation: "ๆตท้พŸ", type: "noun", pronunciation: "tษœrtษ™lz"} + ] + } + ] + }, + { + title: "Chapter 2: Dora's Pet Ding Ding - ็ฌฌไบŒ็ซ ๏ผšๅคšๆ‹‰็š„ๅฎ ็‰ฉไธไธ", + sentences: [ + { + id: 9, + original: "It is a new pet for you, Dora.", + translation: "ๅคšๆ‹‰๏ผŒ่ฟ™ๆ˜ฏ็ป™ไฝ ็š„ๆ–ฐๅฎ ็‰ฉใ€‚", + words: [ + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "new", translation: "ๆ–ฐ็š„", type: "adjective", pronunciation: "nu"}, + {word: "pet", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pษ›t"}, + {word: "for", translation: "็ป™", type: "preposition", pronunciation: "fษ”r"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "Dora", translation: "ๅคšๆ‹‰", type: "noun", pronunciation: "dษ”rษ™"} + ] + }, + { + id: 10, + original: "Oh! It is a bird. Thank you very much.", + translation: "ๅ“ฆ๏ผๆ˜ฏไธ€ๅช้ธŸใ€‚้žๅธธๆ„Ÿ่ฐขใ€‚", + words: [ + {word: "Oh", translation: "ๅ“ฆ", type: "interjection", pronunciation: "oสŠ"}, + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "bird", translation: "้ธŸ", type: "noun", pronunciation: "bษœrd"}, + {word: "Thank", translation: "ๆ„Ÿ่ฐข", type: "verb", pronunciation: "ฮธรฆล‹k"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "very", translation: "้žๅธธ", type: "adverb", pronunciation: "vษ›ri"}, + {word: "much", translation: "ๅคš", type: "adverb", pronunciation: "mสŒtสƒ"} + ] + }, + { + id: 11, + original: "This is my pet Ding Ding. She is a yellow bird. She can sing.", + translation: "่ฟ™ๆ˜ฏๆˆ‘็š„ๅฎ ็‰ฉไธไธใ€‚ๅฅนๆ˜ฏไธ€ๅช้ป„่‰ฒ็š„้ธŸใ€‚ๅฅนไผšๅ”ฑๆญŒใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "pet", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pษ›t"}, + {word: "She", translation: "ๅฅน", type: "pronoun", pronunciation: "สƒi"}, + {word: "yellow", translation: "้ป„่‰ฒ็š„", type: "adjective", pronunciation: "jษ›loสŠ"}, + {word: "can", translation: "่ƒฝ", type: "modal", pronunciation: "kรฆn"}, + {word: "sing", translation: "ๅ”ฑๆญŒ", type: "verb", pronunciation: "sษชล‹"} + ] + }, + { + id: 12, + original: "I can't find Ding Ding!", + translation: "ๆˆ‘ๆ‰พไธๅˆฐไธไธไบ†๏ผ", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "can't", translation: "ไธ่ƒฝ", type: "modal", pronunciation: "kรฆnt"}, + {word: "find", translation: "ๆ‰พๅˆฐ", type: "verb", pronunciation: "faษชnd"} + ] + }, + { + id: 13, + original: "What are those? They are birds. One is yellow. One is blue.", + translation: "้‚ฃไบ›ๆ˜ฏไป€ไนˆ๏ผŸๅฎƒไปฌๆ˜ฏ้ธŸใ€‚ไธ€ๅชๆ˜ฏ้ป„่‰ฒ็š„ใ€‚ไธ€ๅชๆ˜ฏ่“่‰ฒ็š„ใ€‚", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"}, + {word: "They", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "birds", translation: "้ธŸ", type: "noun", pronunciation: "bษœrdz"}, + {word: "One", translation: "ไธ€ๅช", type: "number", pronunciation: "wสŒn"}, + {word: "yellow", translation: "้ป„่‰ฒ็š„", type: "adjective", pronunciation: "jษ›loสŠ"}, + {word: "blue", translation: "่“่‰ฒ็š„", type: "adjective", pronunciation: "blu"} + ] + }, + { + id: 14, + original: "Now I have two pets!", + translation: "็Žฐๅœจๆˆ‘ๆœ‰ไธคๅชๅฎ ็‰ฉไบ†๏ผ", + words: [ + {word: "Now", translation: "็Žฐๅœจ", type: "adverb", pronunciation: "naสŠ"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "have", translation: "ๆœ‰", type: "verb", pronunciation: "hรฆv"}, + {word: "two", translation: "ไธค", type: "number", pronunciation: "tu"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pษ›ts"} + ] + } + ] + }, + { + title: "Chapter 3: Where Are the Pets? - ็ฌฌไธ‰็ซ ๏ผšๅฎ ็‰ฉๅœจๅ“ช้‡Œ๏ผŸ", + sentences: [ + { + id: 15, + original: "Where is the cat?", + translation: "็Œซๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wษ›r"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "the", translation: "่ฟ™ๅช", type: "article", pronunciation: "รฐษ™"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"} + ] + }, + { + id: 16, + original: "It is on the chair.", + translation: "ๅฎƒๅœจๆค…ๅญไธŠใ€‚", + words: [ + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "on", translation: "ๅœจ...ไธŠ", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "chair", translation: "ๆค…ๅญ", type: "noun", pronunciation: "tสƒษ›r"} + ] + }, + { + id: 17, + original: "Where are the turtles?", + translation: "ๆตท้พŸๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wษ›r"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "the", translation: "่ฟ™ไบ›", type: "article", pronunciation: "รฐษ™"}, + {word: "turtles", translation: "ๆตท้พŸ", type: "noun", pronunciation: "tษœrtษ™lz"} + ] + } + ] + } + ] + }, + + // === GRAMMAR-BASED FILL IN THE BLANKS === + fillInBlanks: [ + { + sentence: "_____ is a dog.", // Template with blank - player fills gap + options: ["This", "These", "That", "Those"], // Answer choices - shuffled in UI + correctAnswer: "This", // Right answer - validates player selection + explanation: "Use 'This' for one thing close to you", // Success message - reinforces rule + grammarFocus: "demonstrative-pronouns" // Links to grammar section - enables targeted practice + }, + { + sentence: "_____ are turtles.", // Plural context - tests different demonstrative + options: ["This", "These", "That", "Those"], // Same choices - different correct answer + correctAnswer: "These", // Plural demonstrative - teaches number agreement + explanation: "Use 'These' for multiple things close to you", // Explanation shows plural rule + grammarFocus: "demonstrative-pronouns" // Same focus - reinforces pattern + }, + { + sentence: "They _____ birds.", // Subject-verb agreement - core grammar concept + options: ["is", "are", "am", "be"], // Be verb options - tests understanding of forms + correctAnswer: "are", // Plural verb - matches plural subject + explanation: "Use 'are' with plural subjects", // Rule explanation - teaches subject-verb matching + grammarFocus: "be-verb-usage" // Different grammar topic - expands coverage + }, + { + sentence: "The cat is _____ the chair.", + options: ["on", "in", "under", "at"], + correctAnswer: "on", + explanation: "Use 'on' when something is on top of something else", + grammarFocus: "prepositions-of-place" + }, + { + sentence: "She _____ sing.", + options: ["can", "can't", "is", "are"], + correctAnswer: "can", + explanation: "Use 'can' to show ability", + grammarFocus: "modal-can" + }, + { + sentence: "I _____ find my pet.", + options: ["can", "can't", "is", "are"], + correctAnswer: "can't", + explanation: "Use 'can't' for negative ability", + grammarFocus: "modal-can" + } + ], + + // === GRAMMAR CORRECTION EXERCISES === + corrections: [ + { + incorrect: "This are dogs.", // Common learner error - number disagreement + correct: "These are dogs.", // Corrected version - proper plural form + explanation: "Use 'These' for multiple things, not 'This'", // Error explanation - teaches rule + grammarFocus: "demonstrative-pronouns" // Links to relevant grammar - targeted remediation + }, + { + incorrect: "They is cats.", // Subject-verb mismatch - typical mistake + correct: "They are cats.", // Fixed agreement - demonstrates correct pattern + explanation: "Use 'are' with plural subjects like 'they'", // Teaching point - reinforces rule + grammarFocus: "be-verb-usage" // Grammar connection - enables deeper practice + }, + { + incorrect: "The bird is in the chair.", // Preposition confusion - spatial relationship error + correct: "The bird is on the chair.", // Spatial correction - proper surface relationship + explanation: "Use 'on' when something is on top of furniture", // Spatial concept - visual logic + grammarFocus: "prepositions-of-place" // Category focus - systematic spatial learning + }, + { + incorrect: "She can sings.", + correct: "She can sing.", + explanation: "After 'can', use the base form of the verb", + grammarFocus: "modal-can" + } + ], + + // === ADDITIONAL READING STORIES === + additionalStories: [ + { + title: "My Uncle's Pets - ๆˆ‘ๅ”ๅ”็š„ๅฎ ็‰ฉ", + totalSentences: 13, + chapters: [ + { + title: "Chapter 1: The Vet Uncle - ๅ…ฝๅŒปๅ”ๅ”", + sentences: [ + { + id: 1, + original: "My uncle is tall.", + translation: "ๆˆ‘็š„ๅ”ๅ”ๅพˆ้ซ˜ใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "uncle", translation: "ๅ”ๅ”", type: "noun", pronunciation: "สŒล‹kษ™l"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "tall", translation: "้ซ˜็š„", type: "adjective", pronunciation: "tษ”l"} + ] + }, + { + id: 2, + original: "He is a vet.", + translation: "ไป–ๆ˜ฏไธ€ๅๅ…ฝๅŒปใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅ", type: "article", pronunciation: "ษ™"}, + {word: "vet", translation: "ๅ…ฝๅŒป", type: "noun", pronunciation: "vษ›t"} + ] + }, + { + id: 3, + original: "He can take care of pets.", + translation: "ไป–ไผš็…ง้กพๅฎ ็‰ฉใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "can", translation: "ไผš", type: "modal", pronunciation: "kรฆn"}, + {word: "take care", translation: "็…ง้กพ", type: "verb", pronunciation: "teษชk ker"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pets"} + ] + }, + { + id: 4, + original: "This is his house.", + translation: "่ฟ™ๆ˜ฏไป–็š„ๆˆฟๅญใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "his", translation: "ไป–็š„", type: "pronoun", pronunciation: "hษชz"}, + {word: "house", translation: "ๆˆฟๅญ", type: "noun", pronunciation: "haสŠs"} + ] + }, + { + id: 5, + original: "What are these?", + translation: "่ฟ™ไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "these", translation: "่ฟ™ไบ›", type: "pronoun", pronunciation: "รฐiหz"} + ] + }, + { + id: 6, + original: "These are his pets.", + translation: "่ฟ™ไบ›ๆ˜ฏไป–็š„ๅฎ ็‰ฉใ€‚", + words: [ + {word: "These", translation: "่ฟ™ไบ›", type: "pronoun", pronunciation: "รฐiหz"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "his", translation: "ไป–็š„", type: "pronoun", pronunciation: "hษชz"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pets"} + ] + }, + { + id: 7, + original: "That is a dog under the table.", + translation: "้‚ฃๆ˜ฏๆกŒๅญไธ‹้ข็š„ไธ€ๅช็‹—ใ€‚", + words: [ + {word: "That", translation: "้‚ฃ", type: "pronoun", pronunciation: "รฐรฆt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "dog", translation: "็‹—", type: "noun", pronunciation: "dษ”g"}, + {word: "under", translation: "ๅœจ...ไธ‹้ข", type: "preposition", pronunciation: "สŒndษ™r"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "table", translation: "ๆกŒๅญ", type: "noun", pronunciation: "teษชbษ™l"} + ] + }, + { + id: 8, + original: "This cat is on the chair.", + translation: "่ฟ™ๅช็Œซๅœจๆค…ๅญไธŠใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "on", translation: "ๅœจ...ไธŠ้ข", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "chair", translation: "ๆค…ๅญ", type: "noun", pronunciation: "tสƒษ›r"} + ] + }, + { + id: 9, + original: "Those rabbits are in the box.", + translation: "้‚ฃไบ›ๅ…”ๅญๅœจ็›’ๅญ้‡Œใ€‚", + words: [ + {word: "Those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"}, + {word: "rabbits", translation: "ๅ…”ๅญ", type: "noun", pronunciation: "rรฆbษชts"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "box", translation: "็›’ๅญ", type: "noun", pronunciation: "bษ‘ks"} + ] + }, + { + id: 10, + original: "The turtle is in the cupboard.", + translation: "ไนŒ้พŸๅœจๆฉฑๆŸœ้‡Œใ€‚", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "turtle", translation: "ไนŒ้พŸ", type: "noun", pronunciation: "tษœrtษ™l"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "cupboard", translation: "ๆฉฑๆŸœ", type: "noun", pronunciation: "kสŒbษ™rd"} + ] + }, + { + id: 11, + original: "Where is the bird?", + translation: "้ธŸๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wer"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "bird", translation: "้ธŸ", type: "noun", pronunciation: "bษœrd"} + ] + }, + { + id: 12, + original: "The bird is up on the shelf.", + translation: "้ธŸๅœจๆžถๅญไธŠ้ขใ€‚", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "bird", translation: "้ธŸ", type: "noun", pronunciation: "bษœrd"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "up", translation: "ๅ‘ไธŠ", type: "adverb", pronunciation: "สŒp"}, + {word: "on", translation: "ๅœจ...ไธŠ้ข", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "shelf", translation: "ๆžถๅญ", type: "noun", pronunciation: "สƒษ›lf"} + ] + }, + { + id: 13, + original: "My uncle can help unhappy pets.", + translation: "ๆˆ‘ๅ”ๅ”่ƒฝๅธฎๅŠฉไธๅผ€ๅฟƒ็š„ๅฎ ็‰ฉใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "uncle", translation: "ๅ”ๅ”", type: "noun", pronunciation: "สŒล‹kษ™l"}, + {word: "can", translation: "่ƒฝ", type: "modal", pronunciation: "kรฆn"}, + {word: "help", translation: "ๅธฎๅŠฉ", type: "verb", pronunciation: "hษ›lp"}, + {word: "unhappy", translation: "ไธๅผ€ๅฟƒ็š„", type: "adjective", pronunciation: "สŒnhรฆpi"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pets"} + ] + } + ] + } + ] + }, + { + title: "The Violet Van Adventure - ็ดซ่‰ฒ้ขๅŒ…่ฝฆๅ†’้™ฉ่ฎฐ", + totalSentences: 15, + chapters: [ + { + title: "Chapter 1: The Magic Van - ็ฅžๅฅ‡็š„้ขๅŒ…่ฝฆ", + sentences: [ + { + id: 1, + original: "This is a violet van.", + translation: "่ฟ™ๆ˜ฏไธ€่พ†็ดซ่‰ฒ็š„้ขๅŒ…่ฝฆใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€่พ†", type: "article", pronunciation: "ษ™"}, + {word: "violet", translation: "็ดซ่‰ฒ็š„", type: "adjective", pronunciation: "vaษชษ™lษ™t"}, + {word: "van", translation: "้ขๅŒ…่ฝฆ", type: "noun", pronunciation: "vรฆn"} + ] + }, + { + id: 2, + original: "My teacher can drive this van.", + translation: "ๆˆ‘็š„่€ๅธˆไผšๅผ€่ฟ™่พ†้ขๅŒ…่ฝฆใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "teacher", translation: "่€ๅธˆ", type: "noun", pronunciation: "titสƒษ™r"}, + {word: "can", translation: "ไผš", type: "modal", pronunciation: "kรฆn"}, + {word: "drive", translation: "ๅผ€", type: "verb", pronunciation: "draษชv"}, + {word: "this", translation: "่ฟ™่พ†", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "van", translation: "้ขๅŒ…่ฝฆ", type: "noun", pronunciation: "vรฆn"} + ] + }, + { + id: 3, + original: "What are those in the van?", + translation: "้ขๅŒ…่ฝฆ้‡Œ็š„้‚ฃไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "van", translation: "้ขๅŒ…่ฝฆ", type: "noun", pronunciation: "vรฆn"} + ] + }, + { + id: 4, + original: "Those are pets!", + translation: "้‚ฃไบ›ๆ˜ฏๅฎ ็‰ฉ๏ผ", + words: [ + {word: "Those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pets"} + ] + }, + { + id: 5, + original: "There is a hamster in a tent.", + translation: "ๆœ‰ไธ€ๅชไป“้ผ ๅœจๅธ็ฏท้‡Œใ€‚", + words: [ + {word: "There", translation: "ๆœ‰", type: "adverb", pronunciation: "รฐer"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "hamster", translation: "ไป“้ผ ", type: "noun", pronunciation: "hรฆmstษ™r"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "tent", translation: "ๅธ็ฏท", type: "noun", pronunciation: "tษ›nt"} + ] + }, + { + id: 6, + original: "That tiger is tall.", + translation: "้‚ฃๅช่€่™Žๅพˆ้ซ˜ใ€‚", + words: [ + {word: "That", translation: "้‚ฃๅช", type: "pronoun", pronunciation: "รฐรฆt"}, + {word: "tiger", translation: "่€่™Ž", type: "noun", pronunciation: "taษชgษ™r"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "tall", translation: "้ซ˜็š„", type: "adjective", pronunciation: "tษ”l"} + ] + }, + { + id: 7, + original: "These turtles are under the umbrella.", + translation: "่ฟ™ไบ›ไนŒ้พŸๅœจ้›จไผžไธ‹้ขใ€‚", + words: [ + {word: "These", translation: "่ฟ™ไบ›", type: "pronoun", pronunciation: "รฐiหz"}, + {word: "turtles", translation: "ไนŒ้พŸ", type: "noun", pronunciation: "tษœrtษ™lz"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "under", translation: "ๅœจ...ไธ‹้ข", type: "preposition", pronunciation: "สŒndษ™r"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "umbrella", translation: "้›จไผž", type: "noun", pronunciation: "สŒmbrษ›lษ™"} + ] + }, + { + id: 8, + original: "The bird is on the violin.", + translation: "้ธŸๅœจๅฐๆ็ดไธŠใ€‚", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "bird", translation: "้ธŸ", type: "noun", pronunciation: "bษœrd"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "on", translation: "ๅœจ...ไธŠ้ข", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "violin", translation: "ๅฐๆ็ด", type: "noun", pronunciation: "vaษชษ™lษชn"} + ] + }, + { + id: 9, + original: "Where are the rabbits?", + translation: "ๅ…”ๅญๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wer"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "rabbits", translation: "ๅ…”ๅญ", type: "noun", pronunciation: "rรฆbษชts"} + ] + }, + { + id: 10, + original: "They are in the violet vest.", + translation: "ๅฎƒไปฌๅœจ็ดซ่‰ฒ่ƒŒๅฟƒ้‡Œใ€‚", + words: [ + {word: "They", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "violet", translation: "็ดซ่‰ฒ็š„", type: "adjective", pronunciation: "vaษชษ™lษ™t"}, + {word: "vest", translation: "่ƒŒๅฟƒ", type: "noun", pronunciation: "vษ›st"} + ] + }, + { + id: 11, + original: "My teacher is unhappy.", + translation: "ๆˆ‘็š„่€ๅธˆไธๅผ€ๅฟƒใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "teacher", translation: "่€ๅธˆ", type: "noun", pronunciation: "titสƒษ™r"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "unhappy", translation: "ไธๅผ€ๅฟƒ็š„", type: "adjective", pronunciation: "สŒnhรฆpi"} + ] + }, + { + id: 12, + original: "She can't find her cat.", + translation: "ๅฅนๆ‰พไธๅˆฐๅฅน็š„็Œซใ€‚", + words: [ + {word: "She", translation: "ๅฅน", type: "pronoun", pronunciation: "สƒi"}, + {word: "can't", translation: "ไธ่ƒฝ", type: "modal", pronunciation: "kรฆnt"}, + {word: "find", translation: "ๆ‰พๅˆฐ", type: "verb", pronunciation: "faษชnd"}, + {word: "her", translation: "ๅฅน็š„", type: "pronoun", pronunciation: "hษ™r"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"} + ] + }, + { + id: 13, + original: "Where is my cat?", + translation: "ๆˆ‘็š„็Œซๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wer"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"} + ] + }, + { + id: 14, + original: "The cat is up on the van!", + translation: "็Œซๅœจ้ขๅŒ…่ฝฆไธŠ้ข๏ผ", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "up", translation: "ๅ‘ไธŠ", type: "adverb", pronunciation: "สŒp"}, + {word: "on", translation: "ๅœจ...ไธŠ้ข", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "van", translation: "้ขๅŒ…่ฝฆ", type: "noun", pronunciation: "vรฆn"} + ] + }, + { + id: 15, + original: "Now my teacher is happy.", + translation: "็Žฐๅœจๆˆ‘็š„่€ๅธˆๅผ€ๅฟƒไบ†ใ€‚", + words: [ + {word: "Now", translation: "็Žฐๅœจ", type: "adverb", pronunciation: "naสŠ"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "teacher", translation: "่€ๅธˆ", type: "noun", pronunciation: "titสƒษ™r"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "happy", translation: "ๅผ€ๅฟƒ็š„", type: "adjective", pronunciation: "hรฆpi"} + ] + } + ] + } + ] + } + ] +}; + +// ============================================================================ +// CONTENT STRUCTURE SUMMARY - FOR AI REFERENCE +// ============================================================================ +// +// REQUIRED SECTIONS (for basic compatibility): +// - vocabulary: Object with word keys, minimum 10 entries +// - Basic metadata: id, name, description, difficulty +// +// RECOMMENDED SECTIONS (for optimal experience): +// - grammar: 2-4 topics with rules and examples +// - story: Main narrative with 15-30 sentences +// - fillInBlanks: 10+ exercises linked to grammar +// - letters: 3+ letters with 3-5 words each +// +// OPTIONAL SECTIONS (for enhanced features): +// - corrections: Error fixing exercises +// - additionalStories: Extended reading material +// - audio: Audio files for pronunciation +// - comprehension: Reading questions +// - matching: Pairing exercises +// +// ============================================================================ +// GAME COMPATIBILITY REFERENCE +// ============================================================================ +// +// VOCABULARY-BASED GAMES: +// - Whack-a-Mole: Uses vocabulary object directly +// - Memory Match: Pairs words with translations +// - Quiz Game: Multiple choice from vocabulary +// - Word Storm: Falling words game +// - Word Discovery: Letter-by-letter word building +// +// STORY-BASED GAMES: +// - Story Reader: Sequential sentence reading +// - Adventure Reader: RPG-style story navigation +// - Story Builder: User creates stories from vocabulary +// +// GRAMMAR-FOCUSED GAMES: +// - Grammar Discovery: Teaches rules and patterns +// - Fill-the-Blank: Uses fillInBlanks array +// - Chinese Study: Cultural and grammar content +// +// SPECIALIZED GAMES: +// - Letter Discovery: Uses letters object +// - River Run: Vocabulary with time pressure +// +// ============================================================================ +// DATA VOLUME GUIDELINES +// ============================================================================ +// +// MINIMUM (basic functionality): +// - 10 vocabulary words +// - 1 story chapter (5-8 sentences) +// - 1 grammar topic +// +// OPTIMAL (full experience): +// - 50-100 vocabulary words +// - 3-5 story chapters (20-30 sentences) +// - 3-4 grammar topics with examples +// - 15-20 fill-in-blank exercises +// - 3-5 letters with words +// +// ============================================================================ \ No newline at end of file diff --git a/src/content/WTA1B1.js b/src/content/WTA1B1.js new file mode 100644 index 0000000..86227c2 --- /dev/null +++ b/src/content/WTA1B1.js @@ -0,0 +1,1188 @@ +// === ENGLISH LETTERS AND PETS STORY === +// Complete English story with Chinese translation and pronunciation + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.WTA1B1 = { + id: "wta1b1", + name: "WTA1B-1", + description: "English learning story with letters U, V, T and pet vocabulary", + difficulty: "beginner", + language: "en-US", + userLanguage: "zh-CN", + totalWords: 150, + + // === GRAMMAR LESSONS SYSTEM === + grammar: { + "demonstrative-pronouns": { + title: "Demonstrative Pronouns - ๆŒ‡็คบไปฃ่ฏ", + explanation: "English uses specific words to point to things that are near or far, singular or plural.", + rules: [ + "this - for one thing that is close", + "that - for one thing that is far", + "these - for multiple things that are close", + "those - for multiple things that are far" + ], + examples: [ + { + english: "What is this?", + chinese: "่ฟ™ๆ˜ฏไป€ไนˆ๏ผŸ", + explanation: "Use 'this' for one thing close to you", + pronunciation: "wสŒt ษชz รฐษชs" + }, + { + english: "What are those?", + chinese: "้‚ฃไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + explanation: "Use 'those' for multiple things far from you", + pronunciation: "wสŒt ษ‘หr รฐoสŠz" + }, + { + english: "These are rabbits.", + chinese: "่ฟ™ไบ›ๆ˜ฏๅ…”ๅญใ€‚", + explanation: "Use 'these' for multiple things close to you", + pronunciation: "รฐiหz ษ‘หr rรฆbษชts" + }, + { + english: "That is my pet bird.", + chinese: "้‚ฃๆ˜ฏๆˆ‘็š„ๅฎ ็‰ฉ้ธŸใ€‚", + explanation: "Use 'that' for one thing far from you", + pronunciation: "รฐรฆt ษชz maษช pet bษœหrd" + }, + { + english: "This cat is very cute.", + chinese: "่ฟ™ๅช็Œซๅพˆๅฏ็ˆฑใ€‚", + explanation: "Use 'this' when pointing to something nearby", + pronunciation: "รฐษชs kรฆt ษชz veri kjuหt" + }, + { + english: "Are these your turtles?", + chinese: "่ฟ™ไบ›ๆ˜ฏไฝ ็š„ไนŒ้พŸๅ—๏ผŸ", + explanation: "Use 'these' in questions about nearby plural things", + pronunciation: "ษ‘หr รฐiหz jสŠr tษœหrtษ™lz" + }, + { + english: "Those dogs are playing.", + chinese: "้‚ฃไบ›็‹—ๅœจ็Žฉ่€ใ€‚", + explanation: "Use 'those' for distant plural animals or things", + pronunciation: "รฐoสŠz dษ”หgz ษ‘หr pleษชษชล‹" + }, + { + english: "This is my favorite hamster.", + chinese: "่ฟ™ๆ˜ฏๆˆ‘ๆœ€ๅ–œๆฌข็š„ไป“้ผ ใ€‚", + explanation: "Use 'this' to introduce something specific and close", + pronunciation: "รฐษชs ษชz maษช feษชvษ™rษชt hรฆmstษ™r" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "_____ is a dog.", + options: ["This", "These", "That", "Those"], + correct: "This", + explanation: "Use 'This' for one thing close to you" + }, + { + type: "translation", + english: "Those are turtles.", + chinese: "้‚ฃไบ›ๆ˜ฏๆตท้พŸใ€‚", + focus: "Demonstrative pronoun for plural distant objects" + } + ] + }, + + "be-verb-usage": { + title: "Be Verb Usage - BeๅŠจ่ฏไฝฟ็”จ", + explanation: "English 'be' verbs change based on whether you're talking about one thing or many things.", + rules: [ + "is - used with singular nouns: It is a cat", + "are - used with plural nouns: They are dogs", + "Pattern: This/That + is, These/Those + are" + ], + examples: [ + { + english: "It is a bird.", + chinese: "ๅฎƒๆ˜ฏไธ€ๅช้ธŸใ€‚", + explanation: "Use 'is' with singular nouns", + pronunciation: "ษชt ษชz ษ™ bษœหrd" + }, + { + english: "They are birds.", + chinese: "ๅฎƒไปฌๆ˜ฏ้ธŸใ€‚", + explanation: "Use 'are' with plural nouns", + pronunciation: "รฐeษช ษ‘หr bษœหrdz" + }, + { + english: "Where is the cat?", + chinese: "็Œซๅœจๅ“ช้‡Œ๏ผŸ", + explanation: "Use 'is' when asking about one thing", + pronunciation: "wer ษชz รฐษ™ kรฆt" + }, + { + english: "The dog is happy.", + chinese: "็‹—ๅพˆ้ซ˜ๅ…ดใ€‚", + explanation: "Use 'is' with singular subjects and adjectives", + pronunciation: "รฐษ™ dษ”หg ษชz hรฆpi" + }, + { + english: "My pets are cute.", + chinese: "ๆˆ‘็š„ๅฎ ็‰ฉๅพˆๅฏ็ˆฑใ€‚", + explanation: "Use 'are' with plural nouns like 'pets'", + pronunciation: "maษช pets ษ‘หr kjuหt" + }, + { + english: "Where are the rabbits?", + chinese: "ๅ…”ๅญๅœจๅ“ช้‡Œ๏ผŸ", + explanation: "Use 'are' when asking about multiple things", + pronunciation: "wer ษ‘หr รฐษ™ rรฆbษชts" + }, + { + english: "This is my turtle.", + chinese: "่ฟ™ๆ˜ฏๆˆ‘็š„ไนŒ้พŸใ€‚", + explanation: "Use 'is' with demonstrative 'this'", + pronunciation: "รฐษชs ษชz maษช tษœหrtษ™l" + }, + { + english: "These are my friends.", + chinese: "่ฟ™ไบ›ๆ˜ฏๆˆ‘็š„ๆœ‹ๅ‹ใ€‚", + explanation: "Use 'are' with demonstrative 'these'", + pronunciation: "รฐiหz ษ‘หr maษช frends" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "They ____ turtles.", + options: ["is", "are", "am", "be"], + correct: "are", + explanation: "Use 'are' with plural subjects like 'they'" + } + ] + }, + + "prepositions-of-place": { + title: "Prepositions of Place - ๅœฐ็‚นไป‹่ฏ", + explanation: "English uses specific words to show where things are located.", + rules: [ + "on - things touching the top of something: on the chair", + "in - things inside something: in the box", + "under - things below something: under the table" + ], + examples: [ + { + english: "The cat is on the chair.", + chinese: "็Œซๅœจๆค…ๅญไธŠใ€‚", + explanation: "Use 'on' when something is touching the top", + pronunciation: "รฐษ™ kรฆt ษชz ษ‘หn รฐษ™ tสƒer" + }, + { + english: "The turtle is in the water.", + chinese: "ๆตท้พŸๅœจๆฐด้‡Œใ€‚", + explanation: "Use 'in' when something is inside or surrounded", + pronunciation: "รฐษ™ tษœหrtษ™l ษชz ษชn รฐษ™ wษ”หtษ™r" + }, + { + english: "The dog is under the table.", + chinese: "็‹—ๅœจๆกŒๅญไธ‹้ขใ€‚", + explanation: "Use 'under' when something is below another thing", + pronunciation: "รฐษ™ dษ”หg ษชz สŒndษ™r รฐษ™ teษชbษ™l" + }, + { + english: "The bird is on the tree.", + chinese: "้ธŸๅœจๆ ‘ไธŠใ€‚", + explanation: "Use 'on' for things resting on surfaces", + pronunciation: "รฐษ™ bษœหrd ษชz ษ‘หn รฐษ™ triห" + }, + { + english: "The rabbit is in the garden.", + chinese: "ๅ…”ๅญๅœจ่Šฑๅ›ญ้‡Œใ€‚", + explanation: "Use 'in' for enclosed or surrounded spaces", + pronunciation: "รฐษ™ rรฆbษชt ษชz ษชn รฐษ™ gษ‘หrdษ™n" + }, + { + english: "The hamster is under the bed.", + chinese: "ไป“้ผ ๅœจๅบŠไธ‹้ขใ€‚", + explanation: "Use 'under' for things below furniture", + pronunciation: "รฐษ™ hรฆmstษ™r ษชz สŒndษ™r รฐษ™ bed" + }, + { + english: "My pet is on the sofa.", + chinese: "ๆˆ‘็š„ๅฎ ็‰ฉๅœจๆฒ™ๅ‘ไธŠใ€‚", + explanation: "Use 'on' when pets sit on furniture", + pronunciation: "maษช pet ษชz ษ‘หn รฐษ™ soสŠfษ™" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "The rabbit is ____ the box.", + options: ["on", "in", "under", "at"], + correct: "in", + explanation: "Use 'in' when something is inside a container" + } + ] + }, + + "modal-can": { + title: "Modal Verb 'Can' - ๆƒ…ๆ€ๅŠจ่ฏcan", + explanation: "English uses 'can' to talk about abilities - things someone is able to do.", + rules: [ + "can + verb (base form) - expresses ability: can sing, can swim", + "can't = cannot - negative form: can't fly", + "Can + subject + verb? - question form: Can birds fly?" + ], + examples: [ + { + english: "She can sing.", + chinese: "ๅฅนไผšๅ”ฑๆญŒใ€‚", + explanation: "Use 'can' + base verb to show ability", + pronunciation: "สƒi kรฆn sษชล‹" + }, + { + english: "I can't find Ding Ding!", + chinese: "ๆˆ‘ๆ‰พไธๅˆฐไธไธ๏ผ", + explanation: "Use 'can't' for negative ability", + pronunciation: "aษช kรฆnt faษชnd dษชล‹ dษชล‹" + }, + { + english: "What can Ding Ding do?", + chinese: "ไธไธ่ƒฝๅšไป€ไนˆ๏ผŸ", + explanation: "Use 'can' in questions about ability", + pronunciation: "wสŒt kรฆn dษชล‹ dษชล‹ du" + }, + { + english: "Dogs can run very fast.", + chinese: "็‹—่ท‘ๅพ—ๅพˆๅฟซใ€‚", + explanation: "Use 'can' to describe general abilities", + pronunciation: "dษ”หgz kรฆn rสŒn veri fรฆst" + }, + { + english: "Fish can't walk on land.", + chinese: "้ฑผไธ่ƒฝๅœจ้™†ๅœฐไธŠ่ตฐ่ทฏใ€‚", + explanation: "Use 'can't' for impossible abilities", + pronunciation: "fษชสƒ kรฆnt wษ”หk ษ‘หn lรฆnd" + }, + { + english: "Can cats climb trees?", + chinese: "็Œซ่ƒฝ็ˆฌๆ ‘ๅ—๏ผŸ", + explanation: "Use 'Can' at the start of yes/no questions", + pronunciation: "kรฆn kรฆts klaษชm triหz" + }, + { + english: "Birds can fly in the sky.", + chinese: "้ธŸ่ƒฝๅœจๅคฉ็ฉบไธญ้ฃž่กŒใ€‚", + explanation: "Use 'can' for natural abilities", + pronunciation: "bษœหrdz kรฆn flaษช ษชn รฐษ™ skaษช" + }, + { + english: "I can take care of pets.", + chinese: "ๆˆ‘่ƒฝ็…ง้กพๅฎ ็‰ฉใ€‚", + explanation: "Use 'can' for learned skills", + pronunciation: "aษช kรฆn teษชk ker สŒv pets" + }, + { + english: "Turtles can't run quickly.", + chinese: "ไนŒ้พŸไธ่ƒฝ่ท‘ๅพ—ๅพˆๅฟซใ€‚", + explanation: "Use 'can't' for limited abilities", + pronunciation: "tษœหrtษ™lz kรฆnt rสŒn kwษชkli" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "Turtles ____ swim.", + options: ["can", "can't", "is", "are"], + correct: "can", + explanation: "Use 'can' to show natural ability" + } + ] + } + }, + + // === LETTERS DISCOVERY SYSTEM === + letters: { + "U": [ + { + word: "unhappy", + translation: "ไธๅผ€ๅฟƒ็š„", + type: "adjective", + pronunciation: "สŒnhรฆpi", + example: "The cat looks unhappy." + }, + { + word: "umbrella", + translation: "้›จไผž", + type: "noun", + pronunciation: "สŒmbrษ›lษ™", + example: "I need an umbrella when it rains." + }, + { + word: "up", + translation: "ๅ‘ไธŠ", + type: "adverb", + pronunciation: "สŒp", + example: "The bird flies up high." + }, + { + word: "under", + translation: "ๅœจ...ไธ‹้ข", + type: "preposition", + pronunciation: "สŒndษ™r", + example: "The cat hides under the table." + } + ], + "V": [ + { + word: "violet", + translation: "็ดซ่‰ฒ็š„", + type: "adjective", + pronunciation: "vaษชษ™lษ™t", + example: "She has a violet dress." + }, + { + word: "van", + translation: "้ขๅŒ…่ฝฆ", + type: "noun", + pronunciation: "vรฆn", + example: "The vet drives a white van." + }, + { + word: "vet", + translation: "ๅ…ฝๅŒป", + type: "noun", + pronunciation: "vษ›t", + example: "The vet takes care of pets." + }, + { + word: "vest", + translation: "่ƒŒๅฟƒ", + type: "noun", + pronunciation: "vษ›st", + example: "He wears a warm vest." + } + ], + "T": [ + { + word: "tall", + translation: "้ซ˜็š„", + type: "adjective", + pronunciation: "tษ”l", + example: "The teacher is very tall." + }, + { + word: "turtle", + translation: "ๆตท้พŸ", + type: "noun", + pronunciation: "tษœrtษ™l", + example: "The turtle moves slowly." + }, + { + word: "tent", + translation: "ๅธ็ฏท", + type: "noun", + pronunciation: "tษ›nt", + example: "We sleep in a tent when camping." + }, + { + word: "tiger", + translation: "่€่™Ž", + type: "noun", + pronunciation: "taษชgษ™r", + example: "The tiger is a big cat." + } + ] + }, + + vocabulary: { + "unhappy": { + "user_language": "ไธๅผ€ๅฟƒ็š„", + "type": "adjective", + "pronunciation": "สŒnhรฆpi" + }, + "umbrella": { + "user_language": "้›จไผž", + "type": "noun", + "pronunciation": "สŒmbrษ›lษ™" + }, + "up": { + "user_language": "ๅ‘ไธŠ", + "type": "adverb", + "pronunciation": "สŒp" + }, + "under": { + "user_language": "ๅœจ...ไธ‹้ข", + "type": "preposition", + "pronunciation": "สŒndษ™r" + }, + "uncle": { + "user_language": "ๅ”ๅ”", + "type": "noun", + "pronunciation": "สŒล‹kษ™l" + }, + "violet": { + "user_language": "็ดซ่‰ฒ็š„", + "type": "adjective", + "pronunciation": "vaษชษ™lษ™t" + }, + "van": { + "user_language": "้ขๅŒ…่ฝฆ", + "type": "noun", + "pronunciation": "vรฆn" + }, + "vet": { + "user_language": "ๅ…ฝๅŒป", + "type": "noun", + "pronunciation": "vษ›t" + }, + "vest": { + "user_language": "่ƒŒๅฟƒ", + "type": "noun", + "pronunciation": "vษ›st" + }, + "violin": { + "user_language": "ๅฐๆ็ด", + "type": "noun", + "pronunciation": "vaษชษ™lษชn" + }, + "tall": { + "user_language": "้ซ˜็š„", + "type": "adjective", + "pronunciation": "tษ”l" + }, + "turtle": { + "user_language": "ๆตท้พŸ", + "type": "noun", + "pronunciation": "tษœrtษ™l" + }, + "tent": { + "user_language": "ๅธ็ฏท", + "type": "noun", + "pronunciation": "tษ›nt" + }, + "tiger": { + "user_language": "่€่™Ž", + "type": "noun", + "pronunciation": "taษชgษ™r" + }, + "teacher": { + "user_language": "่€ๅธˆ", + "type": "noun", + "pronunciation": "titสƒษ™r" + }, + "dog": { + "user_language": "็‹—", + "type": "noun", + "pronunciation": "dษ”g" + }, + "cat": { + "user_language": "็Œซ", + "type": "noun", + "pronunciation": "kรฆt" + }, + "bird": { + "user_language": "้ธŸ", + "type": "noun", + "pronunciation": "bษœrd" + }, + "rabbit": { + "user_language": "ๅ…”ๅญ", + "type": "noun", + "pronunciation": "rรฆbษชt" + }, + "hamster": { + "user_language": "ไป“้ผ ", + "type": "noun", + "pronunciation": "hรฆmstษ™r" + }, + "sofa": { + "user_language": "ๆฒ™ๅ‘", + "type": "noun", + "pronunciation": "soสŠfษ™" + }, + "table": { + "user_language": "ๆกŒๅญ", + "type": "noun", + "pronunciation": "teษชbษ™l" + }, + "chair": { + "user_language": "ๆค…ๅญ", + "type": "noun", + "pronunciation": "tสƒษ›r" + }, + "box": { + "user_language": "็›’ๅญ", + "type": "noun", + "pronunciation": "bษ‘ks" + }, + "cupboard": { + "user_language": "ๆฉฑๆŸœ", + "type": "noun", + "pronunciation": "kสŒbษ™rd" + }, + "shelf": { + "user_language": "ๆžถๅญ", + "type": "noun", + "pronunciation": "สƒษ›lf" + } + }, + + story: { + title: "The Pet Adventure - ๅฎ ็‰ฉๅކ้™ฉ่ฎฐ", + totalSentences: 25, + chapters: [ + { + title: "Chapter 1: Choosing a Pet - ็ฌฌไธ€็ซ ๏ผš้€‰ๆ‹ฉๅฎ ็‰ฉ", + sentences: [ + { + id: 1, + original: "What is this?", + translation: "่ฟ™ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "this", translation: "่ฟ™ไธช", type: "pronoun", pronunciation: "รฐษชs"} + ] + }, + { + id: 2, + original: "It is a dog.", + translation: "่ฟ™ๆ˜ฏไธ€ๅช็‹—ใ€‚", + words: [ + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "dog", translation: "็‹—", type: "noun", pronunciation: "dษ”g"} + ] + }, + { + id: 3, + original: "What is that?", + translation: "้‚ฃๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "that", translation: "้‚ฃไธช", type: "pronoun", pronunciation: "รฐรฆt"} + ] + }, + { + id: 4, + original: "It is a hamster.", + translation: "ๅฎƒๆ˜ฏไธ€ๅชไป“้ผ ใ€‚", + words: [ + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "hamster", translation: "ไป“้ผ ", type: "noun", pronunciation: "hรฆmstษ™r"} + ] + }, + { + id: 5, + original: "What are these?", + translation: "่ฟ™ไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "these", translation: "่ฟ™ไบ›", type: "pronoun", pronunciation: "รฐiz"} + ] + }, + { + id: 6, + original: "They are rabbits.", + translation: "ๅฎƒไปฌๆ˜ฏๅ…”ๅญใ€‚", + words: [ + {word: "They", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "rabbits", translation: "ๅ…”ๅญ", type: "noun", pronunciation: "rรฆbษชts"} + ] + }, + { + id: 7, + original: "What are those?", + translation: "้‚ฃไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"} + ] + }, + { + id: 8, + original: "They are turtles.", + translation: "ๅฎƒไปฌๆ˜ฏๆตท้พŸใ€‚", + words: [ + {word: "They", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "turtles", translation: "ๆตท้พŸ", type: "noun", pronunciation: "tษœrtษ™lz"} + ] + } + ] + }, + { + title: "Chapter 2: Dora's Pet Ding Ding - ็ฌฌไบŒ็ซ ๏ผšๅคšๆ‹‰็š„ๅฎ ็‰ฉไธไธ", + sentences: [ + { + id: 9, + original: "It is a new pet for you, Dora.", + translation: "ๅคšๆ‹‰๏ผŒ่ฟ™ๆ˜ฏ็ป™ไฝ ็š„ๆ–ฐๅฎ ็‰ฉใ€‚", + words: [ + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "new", translation: "ๆ–ฐ็š„", type: "adjective", pronunciation: "nu"}, + {word: "pet", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pษ›t"}, + {word: "for", translation: "็ป™", type: "preposition", pronunciation: "fษ”r"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "Dora", translation: "ๅคšๆ‹‰", type: "noun", pronunciation: "dษ”rษ™"} + ] + }, + { + id: 10, + original: "Oh! It is a bird. Thank you very much.", + translation: "ๅ“ฆ๏ผๆ˜ฏไธ€ๅช้ธŸใ€‚้žๅธธๆ„Ÿ่ฐขใ€‚", + words: [ + {word: "Oh", translation: "ๅ“ฆ", type: "interjection", pronunciation: "oสŠ"}, + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "bird", translation: "้ธŸ", type: "noun", pronunciation: "bษœrd"}, + {word: "Thank", translation: "ๆ„Ÿ่ฐข", type: "verb", pronunciation: "ฮธรฆล‹k"}, + {word: "you", translation: "ไฝ ", type: "pronoun", pronunciation: "ju"}, + {word: "very", translation: "้žๅธธ", type: "adverb", pronunciation: "vษ›ri"}, + {word: "much", translation: "ๅคš", type: "adverb", pronunciation: "mสŒtสƒ"} + ] + }, + { + id: 11, + original: "This is my pet Ding Ding. She is a yellow bird. She can sing.", + translation: "่ฟ™ๆ˜ฏๆˆ‘็š„ๅฎ ็‰ฉไธไธใ€‚ๅฅนๆ˜ฏไธ€ๅช้ป„่‰ฒ็š„้ธŸใ€‚ๅฅนไผšๅ”ฑๆญŒใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "pet", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pษ›t"}, + {word: "She", translation: "ๅฅน", type: "pronoun", pronunciation: "สƒi"}, + {word: "yellow", translation: "้ป„่‰ฒ็š„", type: "adjective", pronunciation: "jษ›loสŠ"}, + {word: "can", translation: "่ƒฝ", type: "modal", pronunciation: "kรฆn"}, + {word: "sing", translation: "ๅ”ฑๆญŒ", type: "verb", pronunciation: "sษชล‹"} + ] + }, + { + id: 12, + original: "I can't find Ding Ding!", + translation: "ๆˆ‘ๆ‰พไธๅˆฐไธไธไบ†๏ผ", + words: [ + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "can't", translation: "ไธ่ƒฝ", type: "modal", pronunciation: "kรฆnt"}, + {word: "find", translation: "ๆ‰พๅˆฐ", type: "verb", pronunciation: "faษชnd"} + ] + }, + { + id: 13, + original: "What are those? They are birds. One is yellow. One is blue.", + translation: "้‚ฃไบ›ๆ˜ฏไป€ไนˆ๏ผŸๅฎƒไปฌๆ˜ฏ้ธŸใ€‚ไธ€ๅชๆ˜ฏ้ป„่‰ฒ็š„ใ€‚ไธ€ๅชๆ˜ฏ่“่‰ฒ็š„ใ€‚", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"}, + {word: "They", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "birds", translation: "้ธŸ", type: "noun", pronunciation: "bษœrdz"}, + {word: "One", translation: "ไธ€ๅช", type: "number", pronunciation: "wสŒn"}, + {word: "yellow", translation: "้ป„่‰ฒ็š„", type: "adjective", pronunciation: "jษ›loสŠ"}, + {word: "blue", translation: "่“่‰ฒ็š„", type: "adjective", pronunciation: "blu"} + ] + }, + { + id: 14, + original: "Now I have two pets!", + translation: "็Žฐๅœจๆˆ‘ๆœ‰ไธคๅชๅฎ ็‰ฉไบ†๏ผ", + words: [ + {word: "Now", translation: "็Žฐๅœจ", type: "adverb", pronunciation: "naสŠ"}, + {word: "I", translation: "ๆˆ‘", type: "pronoun", pronunciation: "aษช"}, + {word: "have", translation: "ๆœ‰", type: "verb", pronunciation: "hรฆv"}, + {word: "two", translation: "ไธค", type: "number", pronunciation: "tu"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pษ›ts"} + ] + } + ] + }, + { + title: "Chapter 3: Where Are the Pets? - ็ฌฌไธ‰็ซ ๏ผšๅฎ ็‰ฉๅœจๅ“ช้‡Œ๏ผŸ", + sentences: [ + { + id: 15, + original: "Where is the cat?", + translation: "็Œซๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wษ›r"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "the", translation: "่ฟ™ๅช", type: "article", pronunciation: "รฐษ™"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"} + ] + }, + { + id: 16, + original: "It is on the chair.", + translation: "ๅฎƒๅœจๆค…ๅญไธŠใ€‚", + words: [ + {word: "It", translation: "ๅฎƒ", type: "pronoun", pronunciation: "ษชt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "on", translation: "ๅœจ...ไธŠ", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™ไธช", type: "article", pronunciation: "รฐษ™"}, + {word: "chair", translation: "ๆค…ๅญ", type: "noun", pronunciation: "tสƒษ›r"} + ] + }, + { + id: 17, + original: "Where are the turtles?", + translation: "ๆตท้พŸๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wษ›r"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "the", translation: "่ฟ™ไบ›", type: "article", pronunciation: "รฐษ™"}, + {word: "turtles", translation: "ๆตท้พŸ", type: "noun", pronunciation: "tษœrtษ™lz"} + ] + } + ] + } + ] + }, + + // === GRAMMAR-BASED FILL IN THE BLANKS === + fillInBlanks: [ + { + sentence: "_____ is a dog.", + options: ["This", "These", "That", "Those"], + correctAnswer: "This", + explanation: "Use 'This' for one thing close to you", + grammarFocus: "demonstrative-pronouns" + }, + { + sentence: "_____ are turtles.", + options: ["This", "These", "That", "Those"], + correctAnswer: "These", + explanation: "Use 'These' for multiple things close to you", + grammarFocus: "demonstrative-pronouns" + }, + { + sentence: "They _____ birds.", + options: ["is", "are", "am", "be"], + correctAnswer: "are", + explanation: "Use 'are' with plural subjects", + grammarFocus: "be-verb-usage" + }, + { + sentence: "The cat is _____ the chair.", + options: ["on", "in", "under", "at"], + correctAnswer: "on", + explanation: "Use 'on' when something is on top of something else", + grammarFocus: "prepositions-of-place" + }, + { + sentence: "She _____ sing.", + options: ["can", "can't", "is", "are"], + correctAnswer: "can", + explanation: "Use 'can' to show ability", + grammarFocus: "modal-can" + }, + { + sentence: "I _____ find my pet.", + options: ["can", "can't", "is", "are"], + correctAnswer: "can't", + explanation: "Use 'can't' for negative ability", + grammarFocus: "modal-can" + } + ], + + // === GRAMMAR CORRECTION EXERCISES === + corrections: [ + { + incorrect: "This are dogs.", + correct: "These are dogs.", + explanation: "Use 'These' for multiple things, not 'This'", + grammarFocus: "demonstrative-pronouns" + }, + { + incorrect: "They is cats.", + correct: "They are cats.", + explanation: "Use 'are' with plural subjects like 'they'", + grammarFocus: "be-verb-usage" + }, + { + incorrect: "The bird is in the chair.", + correct: "The bird is on the chair.", + explanation: "Use 'on' when something is on top of furniture", + grammarFocus: "prepositions-of-place" + }, + { + incorrect: "She can sings.", + correct: "She can sing.", + explanation: "After 'can', use the base form of the verb", + grammarFocus: "modal-can" + } + ], + + // === ADDITIONAL READING STORIES === + additionalStories: [ + { + title: "My Uncle's Pets - ๆˆ‘ๅ”ๅ”็š„ๅฎ ็‰ฉ", + totalSentences: 13, + chapters: [ + { + title: "Chapter 1: The Vet Uncle - ๅ…ฝๅŒปๅ”ๅ”", + sentences: [ + { + id: 1, + original: "My uncle is tall.", + translation: "ๆˆ‘็š„ๅ”ๅ”ๅพˆ้ซ˜ใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "uncle", translation: "ๅ”ๅ”", type: "noun", pronunciation: "สŒล‹kษ™l"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "tall", translation: "้ซ˜็š„", type: "adjective", pronunciation: "tษ”l"} + ] + }, + { + id: 2, + original: "He is a vet.", + translation: "ไป–ๆ˜ฏไธ€ๅๅ…ฝๅŒปใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅ", type: "article", pronunciation: "ษ™"}, + {word: "vet", translation: "ๅ…ฝๅŒป", type: "noun", pronunciation: "vษ›t"} + ] + }, + { + id: 3, + original: "He can take care of pets.", + translation: "ไป–ไผš็…ง้กพๅฎ ็‰ฉใ€‚", + words: [ + {word: "He", translation: "ไป–", type: "pronoun", pronunciation: "hi"}, + {word: "can", translation: "ไผš", type: "modal", pronunciation: "kรฆn"}, + {word: "take care", translation: "็…ง้กพ", type: "verb", pronunciation: "teษชk ker"}, + {word: "of", translation: "็š„", type: "preposition", pronunciation: "สŒv"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pets"} + ] + }, + { + id: 4, + original: "This is his house.", + translation: "่ฟ™ๆ˜ฏไป–็š„ๆˆฟๅญใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "his", translation: "ไป–็š„", type: "pronoun", pronunciation: "hษชz"}, + {word: "house", translation: "ๆˆฟๅญ", type: "noun", pronunciation: "haสŠs"} + ] + }, + { + id: 5, + original: "What are these?", + translation: "่ฟ™ไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "these", translation: "่ฟ™ไบ›", type: "pronoun", pronunciation: "รฐiหz"} + ] + }, + { + id: 6, + original: "These are his pets.", + translation: "่ฟ™ไบ›ๆ˜ฏไป–็š„ๅฎ ็‰ฉใ€‚", + words: [ + {word: "These", translation: "่ฟ™ไบ›", type: "pronoun", pronunciation: "รฐiหz"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "his", translation: "ไป–็š„", type: "pronoun", pronunciation: "hษชz"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pets"} + ] + }, + { + id: 7, + original: "That is a dog under the table.", + translation: "้‚ฃๆ˜ฏๆกŒๅญไธ‹้ข็š„ไธ€ๅช็‹—ใ€‚", + words: [ + {word: "That", translation: "้‚ฃ", type: "pronoun", pronunciation: "รฐรฆt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "dog", translation: "็‹—", type: "noun", pronunciation: "dษ”g"}, + {word: "under", translation: "ๅœจ...ไธ‹้ข", type: "preposition", pronunciation: "สŒndษ™r"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "table", translation: "ๆกŒๅญ", type: "noun", pronunciation: "teษชbษ™l"} + ] + }, + { + id: 8, + original: "This cat is on the chair.", + translation: "่ฟ™ๅช็Œซๅœจๆค…ๅญไธŠใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "on", translation: "ๅœจ...ไธŠ้ข", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "chair", translation: "ๆค…ๅญ", type: "noun", pronunciation: "tสƒษ›r"} + ] + }, + { + id: 9, + original: "Those rabbits are in the box.", + translation: "้‚ฃไบ›ๅ…”ๅญๅœจ็›’ๅญ้‡Œใ€‚", + words: [ + {word: "Those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"}, + {word: "rabbits", translation: "ๅ…”ๅญ", type: "noun", pronunciation: "rรฆbษชts"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "box", translation: "็›’ๅญ", type: "noun", pronunciation: "bษ‘ks"} + ] + }, + { + id: 10, + original: "The turtle is in the cupboard.", + translation: "ไนŒ้พŸๅœจๆฉฑๆŸœ้‡Œใ€‚", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "turtle", translation: "ไนŒ้พŸ", type: "noun", pronunciation: "tษœrtษ™l"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "cupboard", translation: "ๆฉฑๆŸœ", type: "noun", pronunciation: "kสŒbษ™rd"} + ] + }, + { + id: 11, + original: "Where is the bird?", + translation: "้ธŸๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wer"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "bird", translation: "้ธŸ", type: "noun", pronunciation: "bษœrd"} + ] + }, + { + id: 12, + original: "The bird is up on the shelf.", + translation: "้ธŸๅœจๆžถๅญไธŠ้ขใ€‚", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "bird", translation: "้ธŸ", type: "noun", pronunciation: "bษœrd"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "up", translation: "ๅ‘ไธŠ", type: "adverb", pronunciation: "สŒp"}, + {word: "on", translation: "ๅœจ...ไธŠ้ข", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "shelf", translation: "ๆžถๅญ", type: "noun", pronunciation: "สƒษ›lf"} + ] + }, + { + id: 13, + original: "My uncle can help unhappy pets.", + translation: "ๆˆ‘ๅ”ๅ”่ƒฝๅธฎๅŠฉไธๅผ€ๅฟƒ็š„ๅฎ ็‰ฉใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "uncle", translation: "ๅ”ๅ”", type: "noun", pronunciation: "สŒล‹kษ™l"}, + {word: "can", translation: "่ƒฝ", type: "modal", pronunciation: "kรฆn"}, + {word: "help", translation: "ๅธฎๅŠฉ", type: "verb", pronunciation: "hษ›lp"}, + {word: "unhappy", translation: "ไธๅผ€ๅฟƒ็š„", type: "adjective", pronunciation: "สŒnhรฆpi"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pets"} + ] + } + ] + } + ] + }, + { + title: "The Violet Van Adventure - ็ดซ่‰ฒ้ขๅŒ…่ฝฆๅ†’้™ฉ่ฎฐ", + totalSentences: 15, + chapters: [ + { + title: "Chapter 1: The Magic Van - ็ฅžๅฅ‡็š„้ขๅŒ…่ฝฆ", + sentences: [ + { + id: 1, + original: "This is a violet van.", + translation: "่ฟ™ๆ˜ฏไธ€่พ†็ดซ่‰ฒ็š„้ขๅŒ…่ฝฆใ€‚", + words: [ + {word: "This", translation: "่ฟ™", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€่พ†", type: "article", pronunciation: "ษ™"}, + {word: "violet", translation: "็ดซ่‰ฒ็š„", type: "adjective", pronunciation: "vaษชษ™lษ™t"}, + {word: "van", translation: "้ขๅŒ…่ฝฆ", type: "noun", pronunciation: "vรฆn"} + ] + }, + { + id: 2, + original: "My teacher can drive this van.", + translation: "ๆˆ‘็š„่€ๅธˆไผšๅผ€่ฟ™่พ†้ขๅŒ…่ฝฆใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "teacher", translation: "่€ๅธˆ", type: "noun", pronunciation: "titสƒษ™r"}, + {word: "can", translation: "ไผš", type: "modal", pronunciation: "kรฆn"}, + {word: "drive", translation: "ๅผ€", type: "verb", pronunciation: "draษชv"}, + {word: "this", translation: "่ฟ™่พ†", type: "pronoun", pronunciation: "รฐษชs"}, + {word: "van", translation: "้ขๅŒ…่ฝฆ", type: "noun", pronunciation: "vรฆn"} + ] + }, + { + id: 3, + original: "What are those in the van?", + translation: "้ขๅŒ…่ฝฆ้‡Œ็š„้‚ฃไบ›ๆ˜ฏไป€ไนˆ๏ผŸ", + words: [ + {word: "What", translation: "ไป€ไนˆ", type: "pronoun", pronunciation: "wสŒt"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "van", translation: "้ขๅŒ…่ฝฆ", type: "noun", pronunciation: "vรฆn"} + ] + }, + { + id: 4, + original: "Those are pets!", + translation: "้‚ฃไบ›ๆ˜ฏๅฎ ็‰ฉ๏ผ", + words: [ + {word: "Those", translation: "้‚ฃไบ›", type: "pronoun", pronunciation: "รฐoสŠz"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "pets", translation: "ๅฎ ็‰ฉ", type: "noun", pronunciation: "pets"} + ] + }, + { + id: 5, + original: "There is a hamster in a tent.", + translation: "ๆœ‰ไธ€ๅชไป“้ผ ๅœจๅธ็ฏท้‡Œใ€‚", + words: [ + {word: "There", translation: "ๆœ‰", type: "adverb", pronunciation: "รฐer"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "a", translation: "ไธ€ๅช", type: "article", pronunciation: "ษ™"}, + {word: "hamster", translation: "ไป“้ผ ", type: "noun", pronunciation: "hรฆmstษ™r"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "a", translation: "ไธ€ไธช", type: "article", pronunciation: "ษ™"}, + {word: "tent", translation: "ๅธ็ฏท", type: "noun", pronunciation: "tษ›nt"} + ] + }, + { + id: 6, + original: "That tiger is tall.", + translation: "้‚ฃๅช่€่™Žๅพˆ้ซ˜ใ€‚", + words: [ + {word: "That", translation: "้‚ฃๅช", type: "pronoun", pronunciation: "รฐรฆt"}, + {word: "tiger", translation: "่€่™Ž", type: "noun", pronunciation: "taษชgษ™r"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "tall", translation: "้ซ˜็š„", type: "adjective", pronunciation: "tษ”l"} + ] + }, + { + id: 7, + original: "These turtles are under the umbrella.", + translation: "่ฟ™ไบ›ไนŒ้พŸๅœจ้›จไผžไธ‹้ขใ€‚", + words: [ + {word: "These", translation: "่ฟ™ไบ›", type: "pronoun", pronunciation: "รฐiหz"}, + {word: "turtles", translation: "ไนŒ้พŸ", type: "noun", pronunciation: "tษœrtษ™lz"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "under", translation: "ๅœจ...ไธ‹้ข", type: "preposition", pronunciation: "สŒndษ™r"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "umbrella", translation: "้›จไผž", type: "noun", pronunciation: "สŒmbrษ›lษ™"} + ] + }, + { + id: 8, + original: "The bird is on the violin.", + translation: "้ธŸๅœจๅฐๆ็ดไธŠใ€‚", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "bird", translation: "้ธŸ", type: "noun", pronunciation: "bษœrd"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "on", translation: "ๅœจ...ไธŠ้ข", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "violin", translation: "ๅฐๆ็ด", type: "noun", pronunciation: "vaษชษ™lษชn"} + ] + }, + { + id: 9, + original: "Where are the rabbits?", + translation: "ๅ…”ๅญๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wer"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "rabbits", translation: "ๅ…”ๅญ", type: "noun", pronunciation: "rรฆbษชts"} + ] + }, + { + id: 10, + original: "They are in the violet vest.", + translation: "ๅฎƒไปฌๅœจ็ดซ่‰ฒ่ƒŒๅฟƒ้‡Œใ€‚", + words: [ + {word: "They", translation: "ๅฎƒไปฌ", type: "pronoun", pronunciation: "รฐeษช"}, + {word: "are", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษ‘r"}, + {word: "in", translation: "ๅœจ...้‡Œ้ข", type: "preposition", pronunciation: "ษชn"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "violet", translation: "็ดซ่‰ฒ็š„", type: "adjective", pronunciation: "vaษชษ™lษ™t"}, + {word: "vest", translation: "่ƒŒๅฟƒ", type: "noun", pronunciation: "vษ›st"} + ] + }, + { + id: 11, + original: "My teacher is unhappy.", + translation: "ๆˆ‘็š„่€ๅธˆไธๅผ€ๅฟƒใ€‚", + words: [ + {word: "My", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "teacher", translation: "่€ๅธˆ", type: "noun", pronunciation: "titสƒษ™r"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "unhappy", translation: "ไธๅผ€ๅฟƒ็š„", type: "adjective", pronunciation: "สŒnhรฆpi"} + ] + }, + { + id: 12, + original: "She can't find her cat.", + translation: "ๅฅนๆ‰พไธๅˆฐๅฅน็š„็Œซใ€‚", + words: [ + {word: "She", translation: "ๅฅน", type: "pronoun", pronunciation: "สƒi"}, + {word: "can't", translation: "ไธ่ƒฝ", type: "modal", pronunciation: "kรฆnt"}, + {word: "find", translation: "ๆ‰พๅˆฐ", type: "verb", pronunciation: "faษชnd"}, + {word: "her", translation: "ๅฅน็š„", type: "pronoun", pronunciation: "hษ™r"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"} + ] + }, + { + id: 13, + original: "Where is my cat?", + translation: "ๆˆ‘็š„็Œซๅœจๅ“ช้‡Œ๏ผŸ", + words: [ + {word: "Where", translation: "ๅ“ช้‡Œ", type: "adverb", pronunciation: "wer"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"} + ] + }, + { + id: 14, + original: "The cat is up on the van!", + translation: "็Œซๅœจ้ขๅŒ…่ฝฆไธŠ้ข๏ผ", + words: [ + {word: "The", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "cat", translation: "็Œซ", type: "noun", pronunciation: "kรฆt"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "up", translation: "ๅ‘ไธŠ", type: "adverb", pronunciation: "สŒp"}, + {word: "on", translation: "ๅœจ...ไธŠ้ข", type: "preposition", pronunciation: "ษ‘n"}, + {word: "the", translation: "่ฟ™", type: "article", pronunciation: "รฐษ™"}, + {word: "van", translation: "้ขๅŒ…่ฝฆ", type: "noun", pronunciation: "vรฆn"} + ] + }, + { + id: 15, + original: "Now my teacher is happy.", + translation: "็Žฐๅœจๆˆ‘็š„่€ๅธˆๅผ€ๅฟƒไบ†ใ€‚", + words: [ + {word: "Now", translation: "็Žฐๅœจ", type: "adverb", pronunciation: "naสŠ"}, + {word: "my", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "maษช"}, + {word: "teacher", translation: "่€ๅธˆ", type: "noun", pronunciation: "titสƒษ™r"}, + {word: "is", translation: "ๆ˜ฏ", type: "verb", pronunciation: "ษชz"}, + {word: "happy", translation: "ๅผ€ๅฟƒ็š„", type: "adjective", pronunciation: "hรฆpi"} + ] + } + ] + } + ] + } + ] +}; \ No newline at end of file diff --git a/src/content/chinese-long-story.js b/src/content/chinese-long-story.js new file mode 100644 index 0000000..f9c25a9 --- /dev/null +++ b/src/content/chinese-long-story.js @@ -0,0 +1,575 @@ +// === CHINESE LONG STORY === +// Complete Chinese story with English translation and pinyin pronunciation + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.ChineseLongStory = { + name: "The Dragon's Pearl - ้พ™็ ไผ ่ฏด", + description: "Long story with translation and pronunciation", + difficulty: "intermediate", + language: "zh-CN", + totalWords: 1200, + + // === GRAMMAR LESSONS SYSTEM === + grammar: { + "chinese-particles": { + title: "Chinese Grammar Particles - ่ฏญๆณ•ๅŠฉ่ฏ", + explanation: "Chinese particles are essential grammatical markers that show relationships between words and add meaning to sentences.", + rules: [ + "็š„ (de) - Possessive marker and adjective connector", + "ๅœจ (zร i) - Location and time marker 'at/in/on'", + "้‡Œ (lว) - Inside/within location marker", + "ไธญ (zhลng) - In/among/middle position marker" + ], + examples: [ + { + chinese: "่€ไบบ็š„ๆ•…ไบ‹", + english: "the old man's story", + explanation: "็š„ shows possession - 'old man's'", + pronunciation: "lวŽo rรฉn de gรน shรฌ" + }, + { + chinese: "ๅœจๅฑฑ้‡Œ", + english: "in the mountains", + explanation: "ๅœจ...้‡Œ shows location 'in/inside'", + pronunciation: "zร i shฤn lว" + }, + { + chinese: "ๆ‘ๅบ„ไธญ็š„้พ™", + english: "the dragon in the village", + explanation: "ไธญ shows position 'in/among'", + pronunciation: "cลซn zhuฤng zhลng de lรณng" + } + ], + exercises: [ + { + type: "fill_blank", + sentence: "่ฟ™ๆ˜ฏๅฐๆ˜Ž___ไนฆๅŒ…", + options: ["็š„", "ๅœจ", "้‡Œ", "ไธญ"], + correct: "็š„", + explanation: "Use ็š„ for possession - 'Xiaoming's backpack'" + }, + { + type: "translation", + chinese: "้พ™ๅœจๆฐด้‡Œ", + english: "The dragon is in the water", + focus: "Location marker ๅœจ...้‡Œ" + } + ] + }, + + "chinese-word-order": { + title: "Chinese Word Order - ไธญๆ–‡่ฏญๅบ", + explanation: "Chinese follows Subject-Verb-Object order like English, but with important differences for time, place, and manner.", + rules: [ + "Basic pattern: Subject + Time + Place + Verb + Object", + "Time comes before place: 'ๆ˜จๅคฉๅœจๅฎถ' (yesterday at home)", + "Manner often comes before verb: 'ๆ…ขๆ…ขๅœฐ่ตฐ' (slowly walk)", + "Place words use specific markers: ๅœจ (at), ้‡Œ (in), ไธŠ (on)" + ], + examples: [ + { + chinese: "่€ไบบๆ˜จๅคฉๅœจๆ‘ๅบ„้‡Œ่ฎฒๆ•…ไบ‹", + english: "The old man told stories in the village yesterday", + breakdown: "่€ไบบ(S) + ๆ˜จๅคฉ(Time) + ๅœจๆ‘ๅบ„้‡Œ(Place) + ่ฎฒ(V) + ๆ•…ไบ‹(O)", + pronunciation: "lวŽo rรฉn zuรณ tiฤn zร i cลซn zhuฤng lว jiวŽng gรน shรฌ" + }, + { + chinese: "้พ™ๆ…ขๆ…ขๅœฐ้ฃžๅ‘ๅฑฑ้กถ", + english: "The dragon slowly flew toward the mountain peak", + breakdown: "้พ™(S) + ๆ…ขๆ…ขๅœฐ(Manner) + ้ฃžๅ‘(V) + ๅฑฑ้กถ(O)", + pronunciation: "lรณng mร n mร n de fฤ“i xiร ng shฤn dวng" + } + ], + exercises: [ + { + type: "word_order", + scrambled: ["ๅœจ", "ๆ˜จๅคฉ", "่€ไบบ", "ๅฎถ้‡Œ", "ไผ‘ๆฏ"], + correct: ["่€ไบบ", "ๆ˜จๅคฉ", "ๅœจ", "ๅฎถ้‡Œ", "ไผ‘ๆฏ"], + english: "The old man rested at home yesterday" + } + ] + }, + + "measure-words": { + title: "Chinese Measure Words - ้‡่ฏ", + explanation: "Chinese uses specific measure words (classifiers) between numbers and nouns, similar to 'a piece of paper' in English.", + rules: [ + "Pattern: Number + Measure Word + Noun", + "ไธช (gรจ) - Most common, used for people and general objects", + "ๆก (tiรกo) - For long, thin things like dragons, rivers, roads", + "ๅบง (zuรฒ) - For mountains, buildings, bridges", + "ๆœฌ (bฤ›n) - For books, magazines" + ], + examples: [ + { + chinese: "ไธ€ๆก้พ™", + english: "one dragon", + explanation: "ๆก is used for long creatures like dragons", + pronunciation: "yรฌ tiรกo lรณng" + }, + { + chinese: "ไธ‰ๅบงๅฑฑ", + english: "three mountains", + explanation: "ๅบง is used for large structures like mountains", + pronunciation: "sฤn zuรฒ shฤn" + }, + { + chinese: "ไธคไธช่€ไบบ", + english: "two old people", + explanation: "ไธช is the general classifier for people", + pronunciation: "liวŽng gรจ lวŽo rรฉn" + } + ], + exercises: [ + { + type: "classifier_choice", + chinese: "ไบ”___็ ๅญ", + options: ["ไธช", "ๆก", "ๅบง", "ๆœฌ"], + correct: "ไธช", + explanation: "็ ๅญ (pearls) use ไธช as the general classifier" + } + ] + }, + + "chinese-tones": { + title: "Chinese Tones - ๅฃฐ่ฐƒ", + explanation: "Mandarin Chinese has 4 main tones that change word meaning. Tone is crucial for communication.", + rules: [ + "First tone (ฤ) - High, flat tone", + "Second tone (รก) - Rising tone, like asking a question", + "Third tone (วŽ) - Falling then rising, dip tone", + "Fourth tone (ร ) - Sharp falling tone", + "Neutral tone (a) - Light, quick, no specific pitch" + ], + examples: [ + { + chinese: "ๅฆˆ (mฤ) - mother", + tone: "First tone - high and flat", + pronunciation: "mฤ" + }, + { + chinese: "้บป (mรก) - hemp/numb", + tone: "Second tone - rising", + pronunciation: "mรก" + }, + { + chinese: "้ฉฌ (mวŽ) - horse", + tone: "Third tone - dip", + pronunciation: "mวŽ" + }, + { + chinese: "้ช‚ (mร ) - to scold", + tone: "Fourth tone - falling", + pronunciation: "mร " + } + ], + exercises: [ + { + type: "tone_identification", + word: "ๅฑฑ", + pronunciation: "shฤn", + tone_options: ["First", "Second", "Third", "Fourth"], + correct: "First", + explanation: "ๅฑฑ (shฤn) uses first tone - high and flat" + } + ] + } + }, + + vocabulary: { + "้พ™": { + "user_language": "dragon", + "type": "noun", + "pronunciation": "lรณng" + }, + "็ ": { + "user_language": "pearl", + "type": "noun", + "pronunciation": "zhลซ" + }, + "ไผ ่ฏด": { + "user_language": "legend", + "type": "noun", + "pronunciation": "chuรกn shuล" + }, + "ๆ•…ไบ‹": { + "user_language": "story", + "type": "noun", + "pronunciation": "gรน shรฌ" + }, + "ๅฑฑ": { + "user_language": "mountain", + "type": "noun", + "pronunciation": "shฤn" + }, + "ๆฐด": { + "user_language": "water", + "type": "noun", + "pronunciation": "shuว" + }, + "ๆ‘ๅบ„": { + "user_language": "village", + "type": "noun", + "pronunciation": "cลซn zhuฤng" + }, + "่€ไบบ": { + "user_language": "old man", + "type": "noun", + "pronunciation": "lวŽo rรฉn" + }, + "ๅนด่ฝป": { + "user_language": "young", + "type": "adjective", + "pronunciation": "niรกn qฤซng" + }, + "็พŽไธฝ": { + "user_language": "beautiful", + "type": "adjective", + "pronunciation": "mฤ›i lรฌ" + } + }, + + story: { + title: "The Dragon's Pearl - ้พ™็ ไผ ่ฏด", + totalSentences: 150, + chapters: [ + { + title: "็ฌฌไธ€็ซ ๏ผšๅค่€็š„ไผ ่ฏด (Chapter 1: The Ancient Legend)", + sentences: [ + { + id: 1, + original: "ๅพˆไน…ๅพˆไน…ไปฅๅ‰๏ผŒๅœจไธญๅ›ฝ็š„ไธ€ไธชๅ่ฟœๅฑฑๆ‘้‡Œ๏ผŒไฝ็€ไธ€ไธชๅนด่ฝป็š„ๆธ”ๅคซๅซๆŽๆ˜Žใ€‚", + translation: "Long, long ago, in a remote mountain village in China, there lived a young fisherman named Li Ming.", + words: [ + {word: "ๅพˆไน…", translation: "long", type: "adverb", pronunciation: "hฤ›n jiว”"}, + {word: "ๅพˆไน…", translation: "long", type: "adverb", pronunciation: "hฤ›n jiว”"}, + {word: "ไปฅๅ‰", translation: "ago", type: "noun", pronunciation: "yว qiรกn"}, + {word: "ๅœจ", translation: "in", type: "preposition", pronunciation: "zร i"}, + {word: "ไธญๅ›ฝ", translation: "China", type: "noun", pronunciation: "zhลng guรณ"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ไธ€ไธช", translation: "a", type: "number", pronunciation: "yฤซ gรจ"}, + {word: "ๅ่ฟœ", translation: "remote", type: "adjective", pronunciation: "piฤn yuวŽn"}, + {word: "ๅฑฑๆ‘", translation: "mountain village", type: "noun", pronunciation: "shฤn cลซn"}, + {word: "้‡Œ", translation: "in", type: "preposition", pronunciation: "lว"}, + {word: "ไฝ็€", translation: "lived", type: "verb", pronunciation: "zhรน zhe"}, + {word: "ไธ€ไธช", translation: "a", type: "number", pronunciation: "yฤซ gรจ"}, + {word: "ๅนด่ฝป", translation: "young", type: "adjective", pronunciation: "niรกn qฤซng"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ๆธ”ๅคซ", translation: "fisherman", type: "noun", pronunciation: "yรบ fลซ"}, + {word: "ๅซ", translation: "named", type: "verb", pronunciation: "jiร o"}, + {word: "ๆŽๆ˜Ž", translation: "Li Ming", type: "noun", pronunciation: "lว mรญng"} + ] + }, + { + id: 2, + original: "ๆŽๆ˜Žๆฏๅคฉ้ƒฝๅœจ้™„่ฟ‘็š„ๆฒณ้‡Œๆ‰“้ฑผ๏ผŒ็”Ÿๆดป่™ฝ็„ถ็ฎ€ๅ•๏ผŒไฝ†ไป–ๅพˆๆปก่ถณใ€‚", + translation: "Li Ming fished in the nearby river every day, and although his life was simple, he was very content.", + words: [ + {word: "ๆŽๆ˜Ž", translation: "Li Ming", type: "noun", pronunciation: "lว mรญng"}, + {word: "ๆฏๅคฉ", translation: "every day", type: "adverb", pronunciation: "mฤ›i tiฤn"}, + {word: "้ƒฝ", translation: "all", type: "adverb", pronunciation: "dลu"}, + {word: "ๅœจ", translation: "in", type: "preposition", pronunciation: "zร i"}, + {word: "้™„่ฟ‘", translation: "nearby", type: "adjective", pronunciation: "fรน jรฌn"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ๆฒณ้‡Œ", translation: "river", type: "noun", pronunciation: "hรฉ lว"}, + {word: "ๆ‰“้ฑผ", translation: "fish", type: "verb", pronunciation: "dวŽ yรบ"}, + {word: "็”Ÿๆดป", translation: "life", type: "noun", pronunciation: "shฤ“ng huรณ"}, + {word: "่™ฝ็„ถ", translation: "although", type: "conjunction", pronunciation: "suฤซ rรกn"}, + {word: "็ฎ€ๅ•", translation: "simple", type: "adjective", pronunciation: "jiวŽn dฤn"}, + {word: "ไฝ†", translation: "but", type: "conjunction", pronunciation: "dร n"}, + {word: "ไป–", translation: "he", type: "pronoun", pronunciation: "tฤ"}, + {word: "ๅพˆ", translation: "very", type: "adverb", pronunciation: "hฤ›n"}, + {word: "ๆปก่ถณ", translation: "content", type: "adjective", pronunciation: "mวŽn zรบ"} + ] + }, + { + id: 3, + original: "ๆ‘้‡Œ็š„่€ไบบไปฌ็ปๅธธ่ฎฒ่ฟฐไธ€ไธชๅ…ณไบŽ็ฅžๅฅ‡้พ™็ ็š„ๅค่€ไผ ่ฏดใ€‚", + translation: "The elderly people in the village often told an ancient legend about a magical dragon pearl.", + words: [ + {word: "ๆ‘้‡Œ", translation: "village", type: "noun", pronunciation: "cลซn lว"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "่€ไบบไปฌ", translation: "elderly people", type: "noun", pronunciation: "lวŽo rรฉn men"}, + {word: "็ปๅธธ", translation: "often", type: "adverb", pronunciation: "jฤซng chรกng"}, + {word: "่ฎฒ่ฟฐ", translation: "tell", type: "verb", pronunciation: "jiวŽng shรน"}, + {word: "ไธ€ไธช", translation: "a", type: "number", pronunciation: "yฤซ gรจ"}, + {word: "ๅ…ณไบŽ", translation: "about", type: "preposition", pronunciation: "guฤn yรบ"}, + {word: "็ฅžๅฅ‡", translation: "magical", type: "adjective", pronunciation: "shรฉn qรญ"}, + {word: "้พ™็ ", translation: "dragon pearl", type: "noun", pronunciation: "lรณng zhลซ"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ๅค่€", translation: "ancient", type: "adjective", pronunciation: "gว” lวŽo"}, + {word: "ไผ ่ฏด", translation: "legend", type: "noun", pronunciation: "chuรกn shuล"} + ] + }, + { + id: 4, + original: "ไผ ่ฏดไธญ่ฏด๏ผŒ้พ™็ ่—ๅœจๆทฑๅฑฑ็š„็ง˜ๅฏ†ๆดž็ฉด้‡Œ๏ผŒ่ƒฝๅคŸๅฎž็ŽฐๆŒๆœ‰่€…็š„ไปปไฝ•ๆ„ฟๆœ›ใ€‚", + translation: "The legend said that the dragon pearl was hidden in a secret cave in the deep mountains and could fulfill any wish of its holder.", + words: [ + {word: "ไผ ่ฏด", translation: "legend", type: "noun", pronunciation: "chuรกn shuล"}, + {word: "ไธญ", translation: "in", type: "preposition", pronunciation: "zhลng"}, + {word: "่ฏด", translation: "said", type: "verb", pronunciation: "shuล"}, + {word: "้พ™็ ", translation: "dragon pearl", type: "noun", pronunciation: "lรณng zhลซ"}, + {word: "่—ๅœจ", translation: "hidden in", type: "verb", pronunciation: "cรกng zร i"}, + {word: "ๆทฑๅฑฑ", translation: "deep mountains", type: "noun", pronunciation: "shฤ“n shฤn"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "็ง˜ๅฏ†", translation: "secret", type: "adjective", pronunciation: "mรฌ mรฌ"}, + {word: "ๆดž็ฉด", translation: "cave", type: "noun", pronunciation: "dรฒng xuรฉ"}, + {word: "้‡Œ", translation: "in", type: "preposition", pronunciation: "lว"}, + {word: "่ƒฝๅคŸ", translation: "could", type: "verb", pronunciation: "nรฉng gรฒu"}, + {word: "ๅฎž็Žฐ", translation: "fulfill", type: "verb", pronunciation: "shรญ xiร n"}, + {word: "ๆŒๆœ‰่€…", translation: "holder", type: "noun", pronunciation: "chรญ yว’u zhฤ›"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ไปปไฝ•", translation: "any", type: "adjective", pronunciation: "rรจn hรฉ"}, + {word: "ๆ„ฟๆœ›", translation: "wish", type: "noun", pronunciation: "yuร n wร ng"} + ] + }, + { + id: 5, + original: "ไฝ†ๆ˜ฏ๏ผŒๆƒณ่ฆๆ‰พๅˆฐ้พ™็ ๅนถไธๅฎนๆ˜“๏ผŒๅ› ไธบๅฑฑไธญๅ……ๆปกไบ†ๅฑ้™ฉใ€‚", + translation: "However, finding the dragon pearl was not easy, because the mountains were full of danger.", + words: [ + {word: "ไฝ†ๆ˜ฏ", translation: "however", type: "conjunction", pronunciation: "dร n shรฌ"}, + {word: "ๆƒณ่ฆ", translation: "want to", type: "verb", pronunciation: "xiวŽng yร o"}, + {word: "ๆ‰พๅˆฐ", translation: "find", type: "verb", pronunciation: "zhวŽo dร o"}, + {word: "้พ™็ ", translation: "dragon pearl", type: "noun", pronunciation: "lรณng zhลซ"}, + {word: "ๅนถไธ", translation: "not", type: "adverb", pronunciation: "bรฌng bรน"}, + {word: "ๅฎนๆ˜“", translation: "easy", type: "adjective", pronunciation: "rรณng yรฌ"}, + {word: "ๅ› ไธบ", translation: "because", type: "conjunction", pronunciation: "yฤซn wรจi"}, + {word: "ๅฑฑไธญ", translation: "mountains", type: "noun", pronunciation: "shฤn zhลng"}, + {word: "ๅ……ๆปกไบ†", translation: "full of", type: "verb", pronunciation: "chลng mวŽn le"}, + {word: "ๅฑ้™ฉ", translation: "danger", type: "noun", pronunciation: "wฤ“i xiวŽn"} + ] + } + ] + }, + { + title: "็ฌฌไบŒ็ซ ๏ผš็ฅž็ง˜็š„ๆขฆๅขƒ (Chapter 2: The Mysterious Dream)", + sentences: [ + { + id: 6, + original: "ไธ€ๅคฉๆ™šไธŠ๏ผŒๆŽๆ˜Žๅšไบ†ไธ€ไธชๅฅ‡ๆ€ช็š„ๆขฆใ€‚", + translation: "One night, Li Ming had a strange dream.", + words: [ + {word: "ไธ€ๅคฉ", translation: "one day", type: "noun", pronunciation: "yฤซ tiฤn"}, + {word: "ๆ™šไธŠ", translation: "night", type: "noun", pronunciation: "wวŽn shร ng"}, + {word: "ๆŽๆ˜Ž", translation: "Li Ming", type: "noun", pronunciation: "lว mรญng"}, + {word: "ๅšไบ†", translation: "had", type: "verb", pronunciation: "zuรฒ le"}, + {word: "ไธ€ไธช", translation: "a", type: "number", pronunciation: "yฤซ gรจ"}, + {word: "ๅฅ‡ๆ€ช", translation: "strange", type: "adjective", pronunciation: "qรญ guร i"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ๆขฆ", translation: "dream", type: "noun", pronunciation: "mรจng"} + ] + }, + { + id: 7, + original: "ๆขฆไธญ๏ผŒไธ€ๆกๅทจๅคง็š„้‡‘้พ™ๅ‡บ็Žฐๅœจไป–้ขๅ‰๏ผŒ้พ™็š„็œผ็›ๅƒๆ˜Ÿๆ˜Ÿไธ€ๆ ท้—ช้—ชๅ‘ๅ…‰ใ€‚", + translation: "In the dream, a huge golden dragon appeared before him, its eyes sparkling like stars.", + words: [ + {word: "ๆขฆไธญ", translation: "in dream", type: "noun", pronunciation: "mรจng zhลng"}, + {word: "ไธ€ๆก", translation: "a", type: "number", pronunciation: "yฤซ tiรกo"}, + {word: "ๅทจๅคง", translation: "huge", type: "adjective", pronunciation: "jรน dร "}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "้‡‘้พ™", translation: "golden dragon", type: "noun", pronunciation: "jฤซn lรณng"}, + {word: "ๅ‡บ็Žฐ", translation: "appeared", type: "verb", pronunciation: "chลซ xiร n"}, + {word: "ๅœจ", translation: "at", type: "preposition", pronunciation: "zร i"}, + {word: "ไป–", translation: "him", type: "pronoun", pronunciation: "tฤ"}, + {word: "้ขๅ‰", translation: "before", type: "noun", pronunciation: "miร n qiรกn"}, + {word: "้พ™", translation: "dragon", type: "noun", pronunciation: "lรณng"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "็œผ็›", translation: "eyes", type: "noun", pronunciation: "yวŽn jฤซng"}, + {word: "ๅƒ", translation: "like", type: "verb", pronunciation: "xiร ng"}, + {word: "ๆ˜Ÿๆ˜Ÿ", translation: "stars", type: "noun", pronunciation: "xฤซng xฤซng"}, + {word: "ไธ€ๆ ท", translation: "same as", type: "adverb", pronunciation: "yฤซ yร ng"}, + {word: "้—ช้—ชๅ‘ๅ…‰", translation: "sparkling", type: "verb", pronunciation: "shวŽn shวŽn fฤ guฤng"} + ] + }, + { + id: 8, + original: "้‡‘้พ™ๅฏนๆŽๆ˜Ž่ฏด๏ผš'ๅ‹‡ๆ•ข็š„ๅนด่ฝปไบบ๏ผŒไฝ ๆœ‰็บฏๆด็š„ๅฟƒ๏ผŒๆˆ‘่ฆ็ป™ไฝ ไธ€ไธชๆœบไผšใ€‚'", + translation: "The golden dragon said to Li Ming: 'Brave young man, you have a pure heart, I want to give you a chance.'", + words: [ + {word: "้‡‘้พ™", translation: "golden dragon", type: "noun", pronunciation: "jฤซn lรณng"}, + {word: "ๅฏน", translation: "to", type: "preposition", pronunciation: "duรฌ"}, + {word: "ๆŽๆ˜Ž", translation: "Li Ming", type: "noun", pronunciation: "lว mรญng"}, + {word: "่ฏด", translation: "said", type: "verb", pronunciation: "shuล"}, + {word: "ๅ‹‡ๆ•ข", translation: "brave", type: "adjective", pronunciation: "yว’ng gวŽn"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ๅนด่ฝปไบบ", translation: "young man", type: "noun", pronunciation: "niรกn qฤซng rรฉn"}, + {word: "ไฝ ", translation: "you", type: "pronoun", pronunciation: "nว"}, + {word: "ๆœ‰", translation: "have", type: "verb", pronunciation: "yว’u"}, + {word: "็บฏๆด", translation: "pure", type: "adjective", pronunciation: "chรบn jiรฉ"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ๅฟƒ", translation: "heart", type: "noun", pronunciation: "xฤซn"}, + {word: "ๆˆ‘", translation: "I", type: "pronoun", pronunciation: "wว’"}, + {word: "่ฆ", translation: "want", type: "verb", pronunciation: "yร o"}, + {word: "็ป™", translation: "give", type: "verb", pronunciation: "gฤ›i"}, + {word: "ไฝ ", translation: "you", type: "pronoun", pronunciation: "nว"}, + {word: "ไธ€ไธช", translation: "a", type: "number", pronunciation: "yฤซ gรจ"}, + {word: "ๆœบไผš", translation: "chance", type: "noun", pronunciation: "jฤซ huรฌ"} + ] + }, + { + id: 9, + original: "'ๆ˜Žๅคฉๆ—ฉไธŠ๏ผŒๅฝ“ๅคช้˜ณๅ‡่ตท็š„ๆ—ถๅ€™๏ผŒ่ทŸ็€ๆฒณๆตๅ‘ๅŒ—่ตฐ๏ผŒไฝ ไผšๆ‰พๅˆฐไฝ ๅฏปๆ‰พ็š„ไธœ่ฅฟใ€‚'", + translation: "'Tomorrow morning, when the sun rises, follow the river northward, and you will find what you seek.'", + words: [ + {word: "ๆ˜Žๅคฉ", translation: "tomorrow", type: "noun", pronunciation: "mรญng tiฤn"}, + {word: "ๆ—ฉไธŠ", translation: "morning", type: "noun", pronunciation: "zวŽo shร ng"}, + {word: "ๅฝ“", translation: "when", type: "conjunction", pronunciation: "dฤng"}, + {word: "ๅคช้˜ณ", translation: "sun", type: "noun", pronunciation: "tร i yรกng"}, + {word: "ๅ‡่ตท", translation: "rises", type: "verb", pronunciation: "shฤ“ng qว"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ๆ—ถๅ€™", translation: "time", type: "noun", pronunciation: "shรญ hรฒu"}, + {word: "่ทŸ็€", translation: "follow", type: "verb", pronunciation: "gฤ“n zhe"}, + {word: "ๆฒณๆต", translation: "river", type: "noun", pronunciation: "hรฉ liรบ"}, + {word: "ๅ‘", translation: "toward", type: "preposition", pronunciation: "xiร ng"}, + {word: "ๅŒ—", translation: "north", type: "noun", pronunciation: "bฤ›i"}, + {word: "่ตฐ", translation: "walk", type: "verb", pronunciation: "zว’u"}, + {word: "ไฝ ", translation: "you", type: "pronoun", pronunciation: "nว"}, + {word: "ไผš", translation: "will", type: "verb", pronunciation: "huรฌ"}, + {word: "ๆ‰พๅˆฐ", translation: "find", type: "verb", pronunciation: "zhวŽo dร o"}, + {word: "ไฝ ", translation: "you", type: "pronoun", pronunciation: "nว"}, + {word: "ๅฏปๆ‰พ", translation: "seek", type: "verb", pronunciation: "xรบn zhวŽo"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ไธœ่ฅฟ", translation: "thing", type: "noun", pronunciation: "dลng xฤซ"} + ] + }, + { + id: 10, + original: "ๆŽๆ˜Ž้†’ๆฅๅŽ๏ผŒๅ‘็Žฐ่ฟ™ไธชๆขฆ้žๅธธๆธ…ๆ™ฐ๏ผŒๅฅฝๅƒ็œŸ็š„ๅ‘็”Ÿ่ฟ‡ไธ€ๆ ทใ€‚", + translation: "After Li Ming woke up, he found that the dream was very clear, as if it had really happened.", + words: [ + {word: "ๆŽๆ˜Ž", translation: "Li Ming", type: "noun", pronunciation: "lว mรญng"}, + {word: "้†’ๆฅ", translation: "woke up", type: "verb", pronunciation: "xวng lรกi"}, + {word: "ๅŽ", translation: "after", type: "preposition", pronunciation: "hรฒu"}, + {word: "ๅ‘็Žฐ", translation: "found", type: "verb", pronunciation: "fฤ xiร n"}, + {word: "่ฟ™ไธช", translation: "this", type: "pronoun", pronunciation: "zhรจ gรจ"}, + {word: "ๆขฆ", translation: "dream", type: "noun", pronunciation: "mรจng"}, + {word: "้žๅธธ", translation: "very", type: "adverb", pronunciation: "fฤ“i chรกng"}, + {word: "ๆธ…ๆ™ฐ", translation: "clear", type: "adjective", pronunciation: "qฤซng xฤซ"}, + {word: "ๅฅฝๅƒ", translation: "as if", type: "adverb", pronunciation: "hวŽo xiร ng"}, + {word: "็œŸ็š„", translation: "really", type: "adverb", pronunciation: "zhฤ“n de"}, + {word: "ๅ‘็”Ÿ่ฟ‡", translation: "happened", type: "verb", pronunciation: "fฤ shฤ“ng guรฒ"}, + {word: "ไธ€ๆ ท", translation: "same", type: "adverb", pronunciation: "yฤซ yร ng"} + ] + } + ] + }, + { + title: "็ฌฌไธ‰็ซ ๏ผš็ฅžๅฅ‡็š„ๅ‘็Žฐ (Chapter 3: The Amazing Discovery)", + sentences: [ + { + id: 11, + original: "็ฌฌไบŒๅคฉๆ—ฉไธŠ๏ผŒๆŽๆ˜Žๅ†ณๅฎšๆŒ‰็…งๆขฆไธญ้พ™็š„ๆŒ‡็คบๅŽปๅฏปๆ‰พ้พ™็ ใ€‚", + translation: "The next morning, Li Ming decided to follow the dragon's instructions from his dream to search for the dragon pearl.", + words: [ + {word: "็ฌฌไบŒๅคฉ", translation: "next day", type: "noun", pronunciation: "dรฌ รจr tiฤn"}, + {word: "ๆ—ฉไธŠ", translation: "morning", type: "noun", pronunciation: "zวŽo shร ng"}, + {word: "ๆŽๆ˜Ž", translation: "Li Ming", type: "noun", pronunciation: "lว mรญng"}, + {word: "ๅ†ณๅฎš", translation: "decided", type: "verb", pronunciation: "juรฉ dรฌng"}, + {word: "ๆŒ‰็…ง", translation: "according to", type: "preposition", pronunciation: "ร n zhร o"}, + {word: "ๆขฆไธญ", translation: "dream", type: "noun", pronunciation: "mรจng zhลng"}, + {word: "้พ™", translation: "dragon", type: "noun", pronunciation: "lรณng"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "ๆŒ‡็คบ", translation: "instructions", type: "noun", pronunciation: "zhว shรฌ"}, + {word: "ๅŽป", translation: "go", type: "verb", pronunciation: "qรน"}, + {word: "ๅฏปๆ‰พ", translation: "search", type: "verb", pronunciation: "xรบn zhวŽo"}, + {word: "้พ™็ ", translation: "dragon pearl", type: "noun", pronunciation: "lรณng zhลซ"} + ] + }, + { + id: 12, + original: "ไป–ๆฒฟ็€ๆฒณๆตๅ‘ๅŒ—่ตฐไบ†ๆ•ดๆ•ดไธ€ๅคฉ๏ผŒๆœ€็ปˆๅˆฐ่พพไบ†ไธ€ๅบง้ซ˜ๅฑฑ็š„่„šไธ‹ใ€‚", + translation: "He walked northward along the river for a whole day and finally reached the foot of a high mountain.", + words: [ + {word: "ไป–", translation: "he", type: "pronoun", pronunciation: "tฤ"}, + {word: "ๆฒฟ็€", translation: "along", type: "preposition", pronunciation: "yรกn zhe"}, + {word: "ๆฒณๆต", translation: "river", type: "noun", pronunciation: "hรฉ liรบ"}, + {word: "ๅ‘", translation: "toward", type: "preposition", pronunciation: "xiร ng"}, + {word: "ๅŒ—", translation: "north", type: "noun", pronunciation: "bฤ›i"}, + {word: "่ตฐไบ†", translation: "walked", type: "verb", pronunciation: "zว’u le"}, + {word: "ๆ•ดๆ•ด", translation: "whole", type: "adverb", pronunciation: "zhฤ›ng zhฤ›ng"}, + {word: "ไธ€ๅคฉ", translation: "one day", type: "noun", pronunciation: "yฤซ tiฤn"}, + {word: "ๆœ€็ปˆ", translation: "finally", type: "adverb", pronunciation: "zuรฌ zhลng"}, + {word: "ๅˆฐ่พพไบ†", translation: "reached", type: "verb", pronunciation: "dร o dรก le"}, + {word: "ไธ€ๅบง", translation: "a", type: "number", pronunciation: "yฤซ zuรฒ"}, + {word: "้ซ˜ๅฑฑ", translation: "high mountain", type: "noun", pronunciation: "gฤo shฤn"}, + {word: "็š„", translation: "of", type: "particle", pronunciation: "de"}, + {word: "่„šไธ‹", translation: "foot", type: "noun", pronunciation: "jiวŽo xiร "} + ] + } + ] + } + ] + }, + + // === GRAMMAR-BASED FILL IN THE BLANKS === + fillInBlanks: [ + { + sentence: "่ฟ™ๆ˜ฏ่€ไบบ___ๆ•…ไบ‹", + options: ["็š„", "ๅœจ", "้‡Œ", "ไธญ"], + correctAnswer: "็š„", + explanation: "Use ็š„ to show possession - 'the old man's story'", + grammarFocus: "chinese-particles" + }, + { + sentence: "้พ™___ๆฐด้‡Œๆธธๆณณ", + options: ["็š„", "ๅœจ", "้‡Œ", "ไธญ"], + correctAnswer: "ๅœจ", + explanation: "Use ๅœจ to show location - 'the dragon is swimming in the water'", + grammarFocus: "chinese-particles" + }, + { + sentence: "ไธ€___้พ™้ฃžๅ‘ๅคฉ็ฉบ", + options: ["ไธช", "ๆก", "ๅบง", "ๆœฌ"], + correctAnswer: "ๆก", + explanation: "Use ๆก for long creatures like dragons", + grammarFocus: "measure-words" + }, + { + sentence: "ไธ‰___ๅฑฑๅพˆ้ซ˜", + options: ["ไธช", "ๆก", "ๅบง", "ๆœฌ"], + correctAnswer: "ๅบง", + explanation: "Use ๅบง for large structures like mountains", + grammarFocus: "measure-words" + }, + { + sentence: "่€ไบบๆ˜จๅคฉ___ๆ‘ๅบ„้‡Œ่ฎฒๆ•…ไบ‹", + options: ["ๅœจ", "็š„", "้‡Œ", "ไธญ"], + correctAnswer: "ๅœจ", + explanation: "Word order: Subject + Time + Place (ๅœจ + location) + Verb + Object", + grammarFocus: "chinese-word-order" + }, + { + sentence: "ๅฑฑ___่ฏป้Ÿณๆ˜ฏ็ฌฌๅ‡ ๅฃฐ๏ผŸ", + options: ["็ฌฌไธ€ๅฃฐ", "็ฌฌไบŒๅฃฐ", "็ฌฌไธ‰ๅฃฐ", "็ฌฌๅ››ๅฃฐ"], + correctAnswer: "็ฌฌไธ€ๅฃฐ", + explanation: "ๅฑฑ (shฤn) uses first tone - high and flat", + grammarFocus: "chinese-tones" + } + ], + + // === GRAMMAR CORRECTION EXERCISES === + corrections: [ + { + incorrect: "้พ™้‡Œๆฐดๆธธๆณณ", + correct: "้พ™ๅœจๆฐด้‡Œๆธธๆณณ", + explanation: "Need ๅœจ (at/in) before location marker ้‡Œ", + grammarFocus: "chinese-particles" + }, + { + incorrect: "่€ไบบๅœจๆ‘ๅบ„ๆ˜จๅคฉ่ฎฒๆ•…ไบ‹", + correct: "่€ไบบๆ˜จๅคฉๅœจๆ‘ๅบ„้‡Œ่ฎฒๆ•…ไบ‹", + explanation: "Time (ๆ˜จๅคฉ) must come before place (ๅœจๆ‘ๅบ„้‡Œ)", + grammarFocus: "chinese-word-order" + }, + { + incorrect: "ไบ”้พ™้ฃžๅœจๅคฉ็ฉบ", + correct: "ไบ”ๆก้พ™้ฃžๅœจๅคฉ็ฉบไธญ", + explanation: "Need measure word ๆก for dragons and location marker ไธญ", + grammarFocus: "measure-words" + } + ] +}; \ No newline at end of file diff --git a/src/content/example-minimal.js b/src/content/example-minimal.js new file mode 100644 index 0000000..4ccd663 --- /dev/null +++ b/src/content/example-minimal.js @@ -0,0 +1,59 @@ +// Example content module with minimal data (no images, some missing pronunciation) +window.ContentModules = window.ContentModules || {}; +window.ContentModules.ExampleMinimal = { + name: "Minimal Vocabulary Test", + description: "Test content with missing images and audio", + difficulty: "easy", + language: "en-US", + + // Vocabulary with mixed availability of features + vocabulary: { + "hello": { + translation: "bonjour", + pronunciation: "hษ™หˆloสŠ", + type: "greeting" + // No image + }, + "goodbye": { + translation: "au revoir", + type: "greeting" + // No image, no pronunciation + }, + "water": { + translation: "eau", + pronunciation: "หˆwษ”หtษ™r", + type: "noun" + // No image + }, + "food": { + translation: "nourriture", + type: "noun" + // No image, no pronunciation + }, + "happy": { + translation: "heureux", + pronunciation: "หˆhรฆpi", + type: "adjective" + // No image + }, + "sad": { + translation: "triste", + type: "adjective" + // No image, no pronunciation + } + }, + + // Backward compatibility for other games + sentences: [ + { + english: "Hello, how are you?", + chinese: "Bonjour, comment allez-vous?", + prononciation: "hษ™หˆloสŠ haสŠ ษ‘r ju" + }, + { + english: "I need some water", + chinese: "J'ai besoin d'eau", + prononciation: "aษช nid sสŒm หˆwษ”หtษ™r" + } + ] +}; \ No newline at end of file diff --git a/src/content/example-with-images.js b/src/content/example-with-images.js new file mode 100644 index 0000000..dd305c2 --- /dev/null +++ b/src/content/example-with-images.js @@ -0,0 +1,81 @@ +// Example content module with image support for Word Discovery game +window.ContentModules = window.ContentModules || {}; +window.ContentModules.ExampleWithImages = { + name: "Basic Vocabulary with Images", + description: "Simple English words with visual support for beginners", + difficulty: "easy", + language: "en-US", + + // Vocabulary with image support + vocabulary: { + "apple": { + translation: "pomme", + pronunciation: "รฆpษ™l", + type: "noun", + image: "assets/images/vocabulary/apple.png", + audioFile: "assets/audio/vocabulary/apple.mp3" + }, + "cat": { + translation: "chat", + pronunciation: "kรฆt", + type: "noun", + image: "assets/images/vocabulary/cat.png", + audioFile: "assets/audio/vocabulary/cat_broken.mp3" // Broken path to test fallback + }, + "house": { + translation: "maison", + pronunciation: "haสŠs", + type: "noun", + image: "assets/images/vocabulary/house.png" + }, + "car": { + translation: "voiture", + pronunciation: "kษ‘r", + type: "noun", + image: "assets/images/vocabulary/car.png" + }, + "tree": { + translation: "arbre", + pronunciation: "tri", + type: "noun", + image: "assets/images/vocabulary/tree.png" + }, + "book": { + translation: "livre", + pronunciation: "bสŠk", + type: "noun", + image: "assets/images/vocabulary/book.png" + }, + "sun": { + translation: "soleil", + pronunciation: "sสŒn", + type: "noun", + image: "assets/images/vocabulary/sun.png" + }, + "dog": { + translation: "chien", + pronunciation: "dษ”g", + type: "noun", + image: "assets/images/vocabulary/dog.png" + } + }, + + // Backward compatibility for other games + sentences: [ + { + english: "The apple is red", + chinese: "La pomme est rouge", + prononciation: "รฐษ™ รฆpษ™l ษชz red" + }, + { + english: "The cat is sleeping", + chinese: "Le chat dort", + prononciation: "รฐษ™ kรฆt ษชz slipษชล‹" + }, + { + english: "I live in a house", + chinese: "J'habite dans une maison", + prononciation: "aษช lษชv ษชn ษ™ haสŠs" + } + ] +}; \ No newline at end of file diff --git a/src/content/french-beginner-story.js b/src/content/french-beginner-story.js new file mode 100644 index 0000000..3805de5 --- /dev/null +++ b/src/content/french-beginner-story.js @@ -0,0 +1,524 @@ +// === CHINESE BEGINNER STORY === +// Histoire chinoise pour dรฉbutants+ avec traduction franรงaise et prononciation pinyin + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.FrenchBeginnerStory = { + id: "french-beginner-story", + name: "Le Jardin Magique - The Magic Garden", + description: "Simple French story for English speakers", + difficulty: "beginner-plus", + language: "fr-FR", // Target language = franรงais + userLanguage: "en-US", // User language = anglais + totalWords: 15, + type: "story_course", + + // === GRAMMAIRE DE BASE === + grammar: { + "basic-sentence-structure": { + title: "French Basic Sentence Structure - Structure de Phrase Franรงaise", + explanation: "French follows Subject-Verb-Object order like English, but with some important differences.", + mainRules: [ + "Subject + Verb + Object (Je mange une pomme - I eat an apple)", + "French verbs conjugate according to the subject (je mange, tu manges, il mange)", + "Adjectives usually come after the noun (une fleur rouge - a red flower)", + "French nouns have gender (masculine/feminine)" + ], + examples: [ + { + french: "J'aime les fleurs", + english: "I love flowers", + pronunciation: "ส’ษ›m le flล“ส", + explanation: "Basic structure: Je(I) + aime(love) + les fleurs(flowers)" + }, + { + french: "Le jardin est trรจs beau", + english: "The garden is very beautiful", + pronunciation: "lษ™ ส’aสdษ›ฬƒ ษ› tสษ› bo", + explanation: "รชtre(to be) + adjective structure" + } + ], + detailedExplanation: { + "subject-verb-object": { + title: "Ordre Sujet-Verbe-Objet", + explanation: "Le chinois suit la mรชme logique que le franรงais pour l'ordre des mots de base.", + pattern: "Sujet + Verbe + Objet", + examples: [ + { + chinese: "ๅฐ็Œซๅƒ้ฑผ", + english: "Le petit chat mange du poisson", + pronunciation: "xiวŽo mฤo chฤซ yรบ", + breakdown: "ๅฐ็Œซ(petit chat) + ๅƒ(manger) + ้ฑผ(poisson)" + }, + { + chinese: "ๆˆ‘็œ‹ไนฆ", + english: "Je lis un livre", + pronunciation: "wว’ kร n shลซ", + breakdown: "ๆˆ‘(je) + ็œ‹(regarder/lire) + ไนฆ(livre)" + } + ] + }, + "adjectives": { + title: "Utilisation des Adjectifs", + explanation: "Les adjectifs peuvent รชtre utilisรฉs directement aprรจs ๅพˆ (trรจs) sans verbe 'รชtre'.", + pattern: "Sujet + ๅพˆ + Adjectif", + examples: [ + { + chinese: "่Šฑๅพˆ็บข", + english: "La fleur est trรจs rouge", + pronunciation: "huฤ hฤ›n hรณng", + breakdown: "่Šฑ(fleur) + ๅพˆ(trรจs) + ็บข(rouge)" + } + ] + } + }, + commonMistakes: [ + { + mistake: "Conjuguer les verbes", + wrong: "ๆˆ‘ๅƒไบ†๏ผŒไฝ ๅƒ็€๏ผŒไป–ๅƒ็š„", + correct: "ๆˆ‘ๅƒ๏ผŒไฝ ๅƒ๏ผŒไป–ๅƒ", + explanation: "Les verbes chinois ne se conjuguent pas selon la personne" + }, + { + mistake: "Oublier ๅพˆ avec les adjectifs", + wrong: "่Šฑๅ›ญ็พŽ", + correct: "่Šฑๅ›ญๅพˆ็พŽ", + explanation: "Utiliser ๅพˆ devant les adjectifs pour une phrase complรจte" + } + ], + practicePoints: [ + "Commencez par des phrases simples : Sujet + Verbe + Objet", + "Utilisez ๅพˆ + adjectif pour dรฉcrire", + "Pas de conjugaison = plus simple !", + "ร‰coutez la mรฉlodie de la langue chinoise" + ] + } + }, + + // === VOCABULAIRE FRANร‡AIS POUR APPRENANTS CHINOIS (15+ mots) === + vocabulary: { + "fleur": { + user_language: "flower", + type: "noun", + pronunciation: "flล“ส", + gender: "feminine" + }, + "jardin": { + user_language: "garden", + type: "noun", + pronunciation: "ส’aสdษ›ฬƒ", + gender: "masculine" + }, + "arbre": { + user_language: "tree", + type: "noun", + pronunciation: "aสbส", + gender: "masculine" + }, + "petit": { + user_language: "small/little", + type: "adjective", + pronunciation: "pษ™ti" + }, + "grand": { + user_language: "big/large", + type: "adjective", + pronunciation: "ษกสษ‘ฬƒ" + }, + "beau": { + user_language: "beautiful/handsome", + type: "adjective", + pronunciation: "bo" + }, + "rouge": { + user_language: "red", + type: "adjective", + pronunciation: "สuส’" + }, + "vert": { + user_language: "green", + type: "adjective", + pronunciation: "vษ›ส" + }, + "je": { + user_language: "I", + type: "pronoun", + pronunciation: "ส’ษ™" + }, + "tu": { + user_language: "you", + type: "pronoun", + pronunciation: "ty" + }, + "regarder": { + user_language: "to look/watch", + type: "verb", + pronunciation: "สษ™ษกaสde" + }, + "aimer": { + user_language: "to love/like", + type: "verb", + pronunciation: "ษ›me" + }, + "trรจs": { + user_language: "very", + type: "adverb", + pronunciation: "tสษ›" + }, + "avoir": { + user_language: "to have", + type: "verb", + pronunciation: "avwaส" + }, + "chat": { + user_language: "cat", + type: "noun", + pronunciation: "สƒa", + gender: "masculine" + }, + "mignon": { + user_language: "cute/adorable", + type: "adjective", + pronunciation: "miษฒษ”ฬƒ" + }, + "maintenant": { + user_language: "now", + type: "adverb", + pronunciation: "mษ›ฬƒtnษ‘ฬƒ" + } + }, + + // === STRUCTURE PAR LETTRES POUR LETTER DISCOVERY === + letters: { + "A": [ + { + word: "arbre", + translation: "tree", + pronunciation: "aสbส", + type: "noun", + gender: "masculine" + }, + { + word: "aimer", + translation: "to love/like", + pronunciation: "ษ›me", + type: "verb" + }, + { + word: "avoir", + translation: "to have", + pronunciation: "avwaส", + type: "verb" + } + ], + "B": [ + { + word: "beau", + translation: "beautiful/handsome", + pronunciation: "bo", + type: "adjective" + }, + { + word: "beaucoup", + translation: "a lot/much", + pronunciation: "boku", + type: "adverb" + } + ], + "C": [ + { + word: "chat", + translation: "cat", + pronunciation: "สƒa", + type: "noun", + gender: "masculine" + } + ], + "F": [ + { + word: "fleur", + translation: "flower", + pronunciation: "flล“ส", + type: "noun", + gender: "feminine" + } + ], + "G": [ + { + word: "grand", + translation: "big/large", + pronunciation: "ษกสษ‘ฬƒ", + type: "adjective" + } + ], + "J": [ + { + word: "jardin", + translation: "garden", + pronunciation: "ส’aสdษ›ฬƒ", + type: "noun", + gender: "masculine" + }, + { + word: "je", + translation: "I", + pronunciation: "ส’ษ™", + type: "pronoun" + } + ], + "M": [ + { + word: "mignon", + translation: "cute/adorable", + pronunciation: "miษฒษ”ฬƒ", + type: "adjective" + }, + { + word: "maintenant", + translation: "now", + pronunciation: "mษ›ฬƒtnษ‘ฬƒ", + type: "adverb" + } + ], + "P": [ + { + word: "petit", + translation: "small/little", + pronunciation: "pษ™ti", + type: "adjective" + } + ], + "R": [ + { + word: "rouge", + translation: "red", + pronunciation: "สuส’", + type: "adjective" + }, + { + word: "regarder", + translation: "to look/watch", + pronunciation: "สษ™ษกaสde", + type: "verb" + } + ], + "T": [ + { + word: "tu", + translation: "you", + pronunciation: "ty", + type: "pronoun" + }, + { + word: "trรจs", + translation: "very", + pronunciation: "tสษ›", + type: "adverb" + } + ], + "V": [ + { + word: "vert", + translation: "green", + pronunciation: "vษ›ส", + type: "adjective" + } + ] + }, + + // === HISTOIRE SIMPLE === + story: { + title: "Le Jardin Magique - ้ญ”ๆณ•่Šฑๅ›ญ", + totalSentences: 8, + chapters: [ + { + title: "็ฌฌไธ€็ซ ๏ผš็พŽไธฝ็š„่Šฑๅ›ญ (Chapitre 1: Le Beau Jardin)", + sentences: [ + { + id: 1, + original: "J'ai un petit jardin.", + translation: "I have a small garden.", + words: [ + {word: "J'", translation: "I", type: "pronoun", pronunciation: "ส’"}, + {word: "ai", translation: "have", type: "verb", pronunciation: "e"}, + {word: "un", translation: "a", type: "article", pronunciation: "ล“ฬƒ"}, + {word: "petit", translation: "small", type: "adjective", pronunciation: "pษ™ti"}, + {word: "jardin", translation: "garden", type: "noun", pronunciation: "ส’aสdษ›ฬƒ"} + ] + }, + { + id: 2, + original: "Dans le jardin, il y a beaucoup de belles fleurs.", + translation: "่Šฑๅ›ญ้‡Œๆœ‰ๅพˆๅคš็พŽไธฝ็š„่Šฑใ€‚", + words: [ + {word: "Dans", translation: "ๅœจ", type: "preposition", pronunciation: "dษ‘ฬƒ"}, + {word: "le", translation: "่ฟ™ไธช", type: "article", pronunciation: "lษ™"}, + {word: "jardin", translation: "่Šฑๅ›ญ", type: "noun", pronunciation: "ส’aสdษ›ฬƒ"}, + {word: "il y a", translation: "ๆœ‰", type: "verb", pronunciation: "il i a"}, + {word: "beaucoup", translation: "ๅพˆๅคš", type: "adverb", pronunciation: "boku"}, + {word: "de", translation: "็š„", type: "preposition", pronunciation: "dษ™"}, + {word: "belles", translation: "็พŽไธฝ็š„", type: "adjective", pronunciation: "bษ›l"}, + {word: "fleurs", translation: "่Šฑ", type: "noun", pronunciation: "flล“ส"} + ] + }, + { + id: 3, + original: "Il y a des fleurs rouges et des arbres verts.", + translation: "ๆœ‰็บข่Šฑๅ’Œ็ปฟๆ ‘ใ€‚", + words: [ + {word: "Il y a", translation: "ๆœ‰", type: "verb", pronunciation: "il i a"}, + {word: "des", translation: "ไธ€ไบ›", type: "article", pronunciation: "de"}, + {word: "fleurs", translation: "่Šฑ", type: "noun", pronunciation: "flล“ส"}, + {word: "rouges", translation: "็บข่‰ฒ็š„", type: "adjective", pronunciation: "สuส’"}, + {word: "et", translation: "ๅ’Œ", type: "conjunction", pronunciation: "e"}, + {word: "des", translation: "ไธ€ไบ›", type: "article", pronunciation: "de"}, + {word: "arbres", translation: "ๆ ‘", type: "noun", pronunciation: "aสbส"}, + {word: "verts", translation: "็ปฟ่‰ฒ็š„", type: "adjective", pronunciation: "vษ›ส"} + ] + }, + { + id: 4, + original: "J'aime beaucoup mon jardin.", + translation: "ๆˆ‘ๅพˆๅ–œๆฌขๆˆ‘็š„่Šฑๅ›ญใ€‚", + words: [ + {word: "J'", translation: "ๆˆ‘", type: "pronoun", pronunciation: "ส’"}, + {word: "aime", translation: "ๅ–œๆฌข", type: "verb", pronunciation: "ษ›m"}, + {word: "beaucoup", translation: "ๅพˆ", type: "adverb", pronunciation: "boku"}, + {word: "mon", translation: "ๆˆ‘็š„", type: "pronoun", pronunciation: "mษ”ฬƒ"}, + {word: "jardin", translation: "่Šฑๅ›ญ", type: "noun", pronunciation: "ส’aสdษ›ฬƒ"} + ] + } + ] + }, + { + title: "็ฌฌไบŒ็ซ ๏ผšๅฐ็Œซๆฅไบ† (Chapitre 2: Le Petit Chat Arrive)", + sentences: [ + { + id: 5, + original: "ไธ€ๅคฉ๏ผŒไธ€ๅชๅฐ็Œซๆฅๅˆฐ่Šฑๅ›ญใ€‚", + translation: "Un jour, un petit chat est venu dans le jardin.", + words: [ + {word: "ไธ€ๅคฉ", translation: "un jour", type: "noun", pronunciation: "yรฌ tiฤn"}, + {word: "ไธ€ๅช", translation: "un (classificateur)", type: "number", pronunciation: "yรฌ zhฤซ"}, + {word: "ๅฐ็Œซ", translation: "petit chat", type: "noun", pronunciation: "xiวŽo mฤo"}, + {word: "ๆฅๅˆฐ", translation: "venir ร ", type: "verb", pronunciation: "lรกi dร o"}, + {word: "่Šฑๅ›ญ", translation: "jardin", type: "noun", pronunciation: "huฤ yuรกn"} + ] + }, + { + id: 6, + original: "ๅฐ็Œซ็œ‹่Šฑ๏ผŒๆˆ‘็œ‹ๅฐ็Œซใ€‚", + translation: "Le petit chat regarde les fleurs, moi je regarde le petit chat.", + words: [ + {word: "ๅฐ็Œซ", translation: "petit chat", type: "noun", pronunciation: "xiวŽo mฤo"}, + {word: "็œ‹", translation: "regarder", type: "verb", pronunciation: "kร n"}, + {word: "่Šฑ", translation: "fleur", type: "noun", pronunciation: "huฤ"}, + {word: "ๆˆ‘", translation: "je", type: "pronoun", pronunciation: "wว’"}, + {word: "็œ‹", translation: "regarder", type: "verb", pronunciation: "kร n"}, + {word: "ๅฐ็Œซ", translation: "petit chat", type: "noun", pronunciation: "xiวŽo mฤo"} + ] + }, + { + id: 7, + original: "ๅฐ็Œซๅพˆๅฏ็ˆฑใ€‚", + translation: "Le petit chat est trรจs mignon.", + words: [ + {word: "ๅฐ็Œซ", translation: "petit chat", type: "noun", pronunciation: "xiวŽo mฤo"}, + {word: "ๅพˆ", translation: "trรจs", type: "adverb", pronunciation: "hฤ›n"}, + {word: "ๅฏ็ˆฑ", translation: "mignon", type: "adjective", pronunciation: "kฤ› ร i"} + ] + }, + { + id: 8, + original: "็Žฐๅœจ๏ผŒ่Šฑๅ›ญๆ›ด็พŽไบ†ใ€‚", + translation: "Maintenant, le jardin est encore plus beau.", + words: [ + {word: "็Žฐๅœจ", translation: "maintenant", type: "adverb", pronunciation: "xiร n zร i"}, + {word: "่Šฑๅ›ญ", translation: "jardin", type: "noun", pronunciation: "huฤ yuรกn"}, + {word: "ๆ›ด", translation: "encore plus", type: "adverb", pronunciation: "gรจng"}, + {word: "็พŽ", translation: "beau", type: "adjective", pronunciation: "mฤ›i"}, + {word: "ไบ†", translation: "particule d'aspect", type: "particle", pronunciation: "le"} + ] + } + ] + } + ] + }, + + // === EXERCICES DE COMPRร‰HENSION === + fillInBlanks: [ + { + sentence: "J'___ un petit jardin", + options: ["ai", "es", "regarde", "trรจs"], + correctAnswer: "ai", + explanation: "ไฝฟ็”จ 'ai' ่กจ็คบๆ‹ฅๆœ‰ - Use 'ai' to express possession (I have)" + }, + { + sentence: "Le jardin est ___ beau", + options: ["ai", "trรจs", "dans", "de"], + correctAnswer: "trรจs", + explanation: "ไฝฟ็”จ 'trรจs' + ๅฝขๅฎน่ฏ - Use 'trรจs' + adjective" + }, + { + sentence: "Le petit chat ___ les fleurs", + options: ["regarde", "ai", "trรจs", "de"], + correctAnswer: "regarde", + explanation: "'regarde' ๆ„ๆ€ๆ˜ฏ็œ‹/่ง‚ๅฏŸ - 'regarde' means to look/observe" + }, + { + sentence: "J'___ beaucoup mon jardin", + options: ["aime", "beau", "petit", "rouge"], + correctAnswer: "aime", + explanation: "'aime' ่กจ็คบๅ–œ็ˆฑ - 'aime' expresses liking" + }, + { + sentence: "Dans le jardin, il y ___ beaucoup de fleurs", + options: ["a", "regarde", "trรจs", "petit"], + correctAnswer: "a", + explanation: "'il y a' ่กจ็คบๅญ˜ๅœจ - 'il y a' expresses existence" + } + ], + + // === CORRECTIONS D'ERREURS === + corrections: [ + { + incorrect: "Je suis aimer le jardin", + correct: "J'aime le jardin", + explanation: "ไธ้œ€่ฆ 'suis'๏ผŒ็›ดๆŽฅ็”จๅŠจ่ฏ 'aime' - No need for 'suis', use verb 'aime' directly" + }, + { + incorrect: "Le jardin beau", + correct: "Le jardin est trรจs beau", + explanation: "้œ€่ฆๅŠจ่ฏ 'est' ๅ’Œๅ‰ฏ่ฏ 'trรจs' - Need verb 'est' and adverb 'trรจs'" + }, + { + incorrect: "Le petit chat a regardรฉ les fleurs", + correct: "Le petit chat regarde les fleurs", + explanation: "็ฎ€ๅ•ๅŠจไฝœ็”จ็Žฐๅœจๆ—ถ - Simple actions use present tense" + } + ], + + // === PHRASES D'EXEMPLE === + sentences: [ + { + french: "J'ai un beau jardin", + chinese: "ๆˆ‘ๆœ‰ไธ€ไธช็พŽไธฝ็š„่Šฑๅ›ญ", + pronunciation: "ส’e ล“ฬƒ bo ส’aสdษ›ฬƒ" + }, + { + french: "Le petit chat est trรจs mignon", + chinese: "ๅฐ็Œซๅพˆๅฏ็ˆฑ", + pronunciation: "lษ™ pษ™ti สƒa ษ› tสษ› miษฒษ”ฬƒ" + }, + { + french: "Dans le jardin il y a des fleurs rouges et des arbres verts", + chinese: "่Šฑๅ›ญ้‡Œๆœ‰็บข่Šฑๅ’Œ็ปฟๆ ‘", + pronunciation: "dษ‘ฬƒ lษ™ ส’aสdษ›ฬƒ il i a de flล“ส สuส’ e dezโ€ฟaสbส vษ›ส" + }, + { + french: "Je regarde le petit chat, le petit chat regarde les fleurs", + chinese: "ๆˆ‘็œ‹ๅฐ็Œซ๏ผŒๅฐ็Œซ็œ‹่Šฑ", + pronunciation: "ส’ษ™ สษ™ษกaสd lษ™ pษ™ti สƒa, lษ™ pษ™ti สƒa สษ™ษกaสd le flล“ส" + } + ] +}; \ No newline at end of file diff --git a/src/content/grammar-lesson-le.js b/src/content/grammar-lesson-le.js new file mode 100644 index 0000000..c2f6d7a --- /dev/null +++ b/src/content/grammar-lesson-le.js @@ -0,0 +1,293 @@ +// === GRAMMAR LESSON: ไบ† ASPECT PARTICLE === +// Dedicated grammar course focused on the ไบ† particle in Chinese + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.GrammarLessonLe = { + id: "grammar-lesson-le", + name: "Grammar Lesson: ไบ† (le) Aspect Particle", + description: "Complete lesson on the Chinese aspect particle ไบ† - completion and change of state", + difficulty: "intermediate", + language: "zh-CN", + type: "grammar_course", + + // === MAIN GRAMMAR LESSON === + grammar: { + "le-aspect-particle": { + title: "The ไบ† (le) Aspect Particle - ๅŠจๆ€ๅŠฉ่ฏไบ†", + explanation: "ไบ† is one of the most important particles in Chinese. It indicates completion of an action or a change of state. Unlike English past tense, ไบ† focuses on the aspect (how the action is viewed) rather than when it happened.", + + mainRules: [ + "ไบ† shows that an action has been completed", + "ไบ† indicates a change from one state to another", + "ไบ† can appear after the verb (ไบ†1) or at the end of sentence (ไบ†2)", + "ไบ† does NOT simply mean 'past tense' - it's about completion/change" + ], + + detailedExplanation: { + "completion": { + title: "1. Completion of Action (ๅŠจไฝœๅฎŒๆˆ)", + explanation: "ไบ† after a verb shows the action has been completed", + pattern: "Subject + Verb + ไบ† + Object", + examples: [ + { + chinese: "ๆˆ‘ๅƒไบ†้ฅญ", + english: "I ate (have eaten) the meal", + pronunciation: "wว’ chฤซ le fร n", + breakdown: "ๆˆ‘(I) + ๅƒ(eat) + ไบ†(completed) + ้ฅญ(meal)", + explanation: "The eating action is completed" + }, + { + chinese: "ไป–ไนฐไบ†ไธ€ๆœฌไนฆ", + english: "He bought a book", + pronunciation: "tฤ mวŽi le yรฌ bฤ›n shลซ", + breakdown: "ไป–(he) + ไนฐ(buy) + ไบ†(completed) + ไธ€ๆœฌไนฆ(a book)", + explanation: "The buying action is finished" + }, + { + chinese: "่€ๅธˆ่ฎฒไบ†ไธ‰ไธชๆ•…ไบ‹", + english: "The teacher told three stories", + pronunciation: "lวŽo shฤซ jiวŽng le sฤn gรจ gรน shรฌ", + breakdown: "่€ๅธˆ(teacher) + ่ฎฒ(tell) + ไบ†(completed) + ไธ‰ไธชๆ•…ไบ‹(three stories)", + explanation: "The telling action is complete" + } + ] + }, + + "change-of-state": { + title: "2. Change of State (็Šถๆ€ๅ˜ๅŒ–)", + explanation: "ไบ† at the end of a sentence shows a change in situation or state", + pattern: "Subject + Verb + Object + ไบ†", + examples: [ + { + chinese: "ๅคฉ้ป‘ไบ†", + english: "It has gotten dark / It's dark now", + pronunciation: "tiฤn hฤ“i le", + breakdown: "ๅคฉ(sky) + ้ป‘(dark) + ไบ†(change of state)", + explanation: "Change from light to dark" + }, + { + chinese: "ๆˆ‘้ฅฟไบ†", + english: "I'm hungry now / I've become hungry", + pronunciation: "wว’ รจ le", + breakdown: "ๆˆ‘(I) + ้ฅฟ(hungry) + ไบ†(change of state)", + explanation: "Change from not hungry to hungry" + }, + { + chinese: "ไธ‹้›จไบ†", + english: "It's raining now / It started to rain", + pronunciation: "xiร  yว” le", + breakdown: "ไธ‹้›จ(rain) + ไบ†(change of state)", + explanation: "Change from not raining to raining" + } + ] + }, + + "double-le": { + title: "3. Double ไบ† Construction", + explanation: "Sometimes ไบ† appears both after the verb AND at the end of sentence", + pattern: "Subject + Verb + ไบ† + Object + ไบ†", + examples: [ + { + chinese: "ๆˆ‘ไนฐไบ†ไธ‰ๆœฌไนฆไบ†", + english: "I have bought three books (and the situation has changed)", + pronunciation: "wว’ mวŽi le sฤn bฤ›n shลซ le", + breakdown: "ๆˆ‘ + ไนฐไบ†(completed buying) + ไธ‰ๆœฌไนฆ + ไบ†(new situation)", + explanation: "Action completed AND situation changed" + }, + { + chinese: "ไป–ๅƒไบ†ไธคไธช่‹นๆžœไบ†", + english: "He has eaten two apples (and is now full/satisfied)", + pronunciation: "tฤ chฤซ le liวŽng gรจ pรญng guว’ le", + breakdown: "ไป– + ๅƒไบ†(completed eating) + ไธคไธช่‹นๆžœ + ไบ†(new state)", + explanation: "Eating completed AND state changed" + } + ] + } + }, + + commonMistakes: [ + { + mistake: "Using ไบ† for all past actions", + wrong: "ๆˆ‘ๆ˜จๅคฉไบ†ๅŽปๅญฆๆ ก", + correct: "ๆˆ‘ๆ˜จๅคฉๅŽปไบ†ๅญฆๆ ก / ๆˆ‘ๆ˜จๅคฉๅŽปๅญฆๆ ก", + explanation: "ไบ† shows completion, not just past time. Don't add ไบ† randomly to past time expressions." + }, + { + mistake: "Forgetting ไบ† for completed actions", + wrong: "ๆˆ‘ๅƒ้ฅญ๏ผŒ็Žฐๅœจๅพˆ้ฅฑ", + correct: "ๆˆ‘ๅƒไบ†้ฅญ๏ผŒ็Žฐๅœจๅพˆ้ฅฑ", + explanation: "Need ไบ† to show the eating is completed before being full" + }, + { + mistake: "Using ไบ† with ongoing actions", + wrong: "ๆˆ‘ๆญฃๅœจๅƒไบ†้ฅญ", + correct: "ๆˆ‘ๆญฃๅœจๅƒ้ฅญ", + explanation: "Can't use ไบ† with ๆญฃๅœจ (ongoing) - they're contradictory" + } + ], + + practicePoints: [ + "Ask yourself: Is the action completed? Use ไบ† after verb", + "Ask yourself: Has the situation changed? Use ไบ† at end", + "Remember: ไบ† โ‰  past tense. It's about completion/change", + "Pay attention to context - sometimes ไบ† is not needed even for past actions" + ] + } + }, + + // === VOCABULARY FOR THE LESSON === + vocabulary: { + "ไบ†": { + translation: "aspect particle (completion/change)", + type: "particle", + pronunciation: "le", + usage: "Shows completed action or change of state" + }, + "ๅƒ": { + translation: "to eat", + type: "verb", + pronunciation: "chฤซ" + }, + "ไนฐ": { + translation: "to buy", + type: "verb", + pronunciation: "mวŽi" + }, + "่ฎฒ": { + translation: "to tell/speak", + type: "verb", + pronunciation: "jiวŽng" + }, + "้ฅญ": { + translation: "meal/food", + type: "noun", + pronunciation: "fร n" + }, + "ไนฆ": { + translation: "book", + type: "noun", + pronunciation: "shลซ" + }, + "ๆ•…ไบ‹": { + translation: "story", + type: "noun", + pronunciation: "gรน shรฌ" + }, + "ๅคฉ": { + translation: "sky/day", + type: "noun", + pronunciation: "tiฤn" + }, + "้ป‘": { + translation: "dark/black", + type: "adjective", + pronunciation: "hฤ“i" + }, + "้ฅฟ": { + translation: "hungry", + type: "adjective", + pronunciation: "รจ" + }, + "ไธ‹้›จ": { + translation: "to rain", + type: "verb", + pronunciation: "xiร  yว”" + } + }, + + // === FILL IN THE BLANKS EXERCISES === + fillInBlanks: [ + { + sentence: "ๆˆ‘ๅƒ___้ฅญ๏ผŒ็Žฐๅœจๅพˆ้ฅฑ", + options: ["ไบ†", "็š„", "ๅœจ", "็€"], + correctAnswer: "ไบ†", + explanation: "Use ไบ† to show the eating action is completed before being full", + grammarFocus: "completion" + }, + { + sentence: "ๅคฉ้ป‘___๏ผŒๆˆ‘ไปฌๅ›žๅฎถๅง", + options: ["ไบ†", "็š„", "ๅœจ", "็€"], + correctAnswer: "ไบ†", + explanation: "Use ไบ† to show change of state - it has become dark", + grammarFocus: "change-of-state" + }, + { + sentence: "ไป–ไนฐ___ไธ‰ๆœฌไนฆ___", + options: ["ไบ†...ไบ†", "็š„...็š„", "ๅœจ...ๅœจ", "็€...็€"], + correctAnswer: "ไบ†...ไบ†", + explanation: "Double ไบ†: action completed (ไนฐไบ†) AND situation changed (ไบ†)", + grammarFocus: "double-le" + }, + { + sentence: "ๆˆ‘ๆ˜จๅคฉ___ๅญฆๆ ก", + options: ["ๅŽปไบ†", "ไบ†ๅŽป", "ๅŽป็š„", "็š„ๅŽป"], + correctAnswer: "ๅŽปไบ†", + explanation: "ไบ† comes after the verb to show completed action", + grammarFocus: "word-order" + }, + { + sentence: "ไธ‹้›จ___๏ผŒ่ทฏๅพˆๆนฟ", + options: ["ไบ†", "็š„", "ๅœจ", "็€"], + correctAnswer: "ไบ†", + explanation: "Change of state: it has started raining (wasn't raining before)", + grammarFocus: "change-of-state" + } + ], + + // === CORRECTION EXERCISES === + corrections: [ + { + incorrect: "ๆˆ‘ๆ˜จๅคฉไบ†ๅŽปๅญฆๆ ก", + correct: "ๆˆ‘ๆ˜จๅคฉๅŽปไบ†ๅญฆๆ ก", + explanation: "ไบ† should come after the verb, not before it", + grammarFocus: "word-order" + }, + { + incorrect: "ๆˆ‘ๆญฃๅœจๅƒไบ†้ฅญ", + correct: "ๆˆ‘ๆญฃๅœจๅƒ้ฅญ", + explanation: "Cannot use ไบ† (completion) with ๆญฃๅœจ (ongoing action)", + grammarFocus: "aspect-conflict" + }, + { + incorrect: "ๆˆ‘ๅƒ้ฅญ๏ผŒ็Žฐๅœจๅพˆ้ฅฑ", + correct: "ๆˆ‘ๅƒไบ†้ฅญ๏ผŒ็Žฐๅœจๅพˆ้ฅฑ", + explanation: "Need ไบ† to show eating is completed before the result (being full)", + grammarFocus: "completion" + }, + { + incorrect: "ไป–ๅพˆ้ซ˜ไบ†็š„ไบบ", + correct: "ไป–ๆ˜ฏๅพˆ้ซ˜็š„ไบบ", + explanation: "Don't use ไบ† in descriptions with ็š„. ไบ† is for actions/changes, not permanent descriptions", + grammarFocus: "inappropriate-usage" + } + ], + + // === TRANSLATION EXERCISES === + sentences: [ + { + english: "I finished my homework", + chinese: "ๆˆ‘ๅšๅฎŒไบ†ไฝœไธš", + pronunciation: "wว’ zuรฒ wรกn le zuรฒ yรจ", + grammarFocus: "completion" + }, + { + english: "It's gotten cold", + chinese: "ๅคฉๆฐ”ๅ†ทไบ†", + pronunciation: "tiฤn qรฌ lฤ›ng le", + grammarFocus: "change-of-state" + }, + { + english: "He bought two books and now has them", + chinese: "ไป–ไนฐไบ†ไธคๆœฌไนฆไบ†", + pronunciation: "tฤ mวŽi le liวŽng bฤ›n shลซ le", + grammarFocus: "double-le" + }, + { + english: "The teacher finished the lesson", + chinese: "่€ๅธˆ่ฎฒๅฎŒไบ†่ฏพ", + pronunciation: "lวŽo shฤซ jiวŽng wรกn le kรจ", + grammarFocus: "completion" + } + ] +}; \ No newline at end of file diff --git a/src/content/sbs-level-7-8-new.js b/src/content/sbs-level-7-8-new.js new file mode 100644 index 0000000..57619de --- /dev/null +++ b/src/content/sbs-level-7-8-new.js @@ -0,0 +1,168 @@ +// === SBS LEVEL 7-8 VOCABULARY (LANGUAGE-AGNOSTIC FORMAT) === + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.SBSLevel78New = { + name: "SBS Level 7-8 New", + description: "Side by Side Level 7-8 vocabulary with language-agnostic format", + difficulty: "intermediate", + language: "en-US", + + vocabulary: { + // Housing and Places + "central": { user_language: "ไธญๅฟƒ็š„๏ผ›ไธญๅคฎ็š„", type: "adjective" }, + "avenue": { user_language: "ๅคง่ก—๏ผ›ๆž—่ซ้“", type: "noun" }, + "refrigerator": { user_language: "ๅ†ฐ็ฎฑ", type: "noun" }, + "closet": { user_language: "่กฃๆŸœ๏ผ›ๅฃๆฉฑ", type: "noun" }, + "elevator": { user_language: "็”ตๆขฏ", type: "noun" }, + "building": { user_language: "ๅปบ็ญ‘็‰ฉ๏ผ›ๅคงๆฅผ", type: "noun" }, + "air conditioner": { user_language: "็ฉบ่ฐƒ", type: "noun" }, + "superintendent": { user_language: "ไธป็ฎก๏ผ›่ดŸ่ดฃไบบ", type: "noun" }, + "bus stop": { user_language: "ๅ…ฌไบค่ฝฆ็ซ™", type: "noun" }, + "jacuzzi": { user_language: "ๆŒ‰ๆ‘ฉๆตด็ผธ", type: "noun" }, + "machine": { user_language: "ๆœบๅ™จ๏ผ›่ฎพๅค‡", type: "noun" }, + "two and a half": { user_language: "ไธคไธชๅŠ", type: "number" }, + "in the center of": { user_language: "ๅœจโ€ฆโ€ฆไธญๅฟƒ", type: "preposition" }, + "town": { user_language: "ๅŸŽ้•‡", type: "noun" }, + "a lot of": { user_language: "่ฎธๅคš", type: "determiner" }, + "noise": { user_language: "ๅ™ช้Ÿณ", type: "noun" }, + "sidewalks": { user_language: "ไบบ่กŒ้“", type: "noun" }, + "all day and all night": { user_language: "ๆ•ดๆ—ฅๆ•ดๅคœ", type: "adverb" }, + "convenient": { user_language: "ไพฟๅˆฉ็š„", type: "adjective" }, + "upset": { user_language: "ๅคฑๆœ›็š„", type: "adjective" }, + + // Clothing and Accessories + "shirt": { user_language: "่กฌ่กซ", type: "noun" }, + "coat": { user_language: "ๅค–ๅฅ—ใ€ๅคง่กฃ", type: "noun" }, + "dress": { user_language: "่ฟž่กฃ่ฃ™", type: "noun" }, + "skirt": { user_language: "็Ÿญ่ฃ™", type: "noun" }, + "blouse": { user_language: "ๅฅณๅผ่กฌ่กซ", type: "noun" }, + "jacket": { user_language: "ๅคนๅ…‹ใ€็Ÿญๅค–ๅฅ—", type: "noun" }, + "sweater": { user_language: "ๆฏ›่กฃใ€้’ˆ็ป‡่กซ", type: "noun" }, + "suit": { user_language: "ๅฅ—่ฃ…ใ€่ฅฟ่ฃ…", type: "noun" }, + "tie": { user_language: "้ข†ๅธฆ", type: "noun" }, + "pants": { user_language: "่ฃคๅญ", type: "noun" }, + "jeans": { user_language: "็‰›ไป”่ฃค", type: "noun" }, + "belt": { user_language: "่…ฐๅธฆใ€็šฎๅธฆ", type: "noun" }, + "hat": { user_language: "ๅธฝๅญ", type: "noun" }, + "glove": { user_language: "ๆ‰‹ๅฅ—", type: "noun" }, + "purse": { user_language: "ๆ‰‹ๆๅŒ…ใ€ๅฅณๅผๅฐๅŒ…", type: "noun" }, + "glasses": { user_language: "็œผ้•œ", type: "noun" }, + "pajamas": { user_language: "็ก่กฃ", type: "noun" }, + "socks": { user_language: "่ขœๅญ", type: "noun" }, + "shoes": { user_language: "้ž‹ๅญ", type: "noun" }, + "bathrobe": { user_language: "ๆตด่ข", type: "noun" }, + "tee shirt": { user_language: "Tๆค", type: "noun" }, + "scarf": { user_language: "ๅ›ดๅทพ", type: "noun" }, + "wallet": { user_language: "้’ฑๅŒ…", type: "noun" }, + "ring": { user_language: "ๆˆ’ๆŒ‡", type: "noun" }, + "sandals": { user_language: "ๅ‡‰้ž‹", type: "noun" }, + + // Body Parts and Health + "throat": { user_language: "ๅ–‰ๅ’™", type: "noun" }, + "shoulder": { user_language: "่‚ฉ่†€", type: "noun" }, + "chest": { user_language: "่ƒธ้ƒจ", type: "noun" }, + "back": { user_language: "่ƒŒ้ƒจ", type: "noun" }, + "arm": { user_language: "ๆ‰‹่‡‚", type: "noun" }, + "elbow": { user_language: "่‚˜้ƒจ", type: "noun" }, + "wrist": { user_language: "ๆ‰‹่…•", type: "noun" }, + "hip": { user_language: "้ซ‹้ƒจ", type: "noun" }, + "thigh": { user_language: "ๅคง่…ฟ", type: "noun" }, + "knee": { user_language: "่†็›–", type: "noun" }, + "shin": { user_language: "่ƒซ้ชจ", type: "noun" }, + "ankle": { user_language: "่„š่ธ", type: "noun" }, + "cough": { user_language: "ๅ’ณๅ—ฝ", type: "verb" }, + "sneeze": { user_language: "ๆ‰“ๅ–ทๅš", type: "verb" }, + "wheeze": { user_language: "ๅ–˜ๆฏ", type: "verb" }, + "feel dizzy": { user_language: "ๆ„Ÿๅˆฐๅคดๆ™•", type: "verb" }, + "feel nauseous": { user_language: "ๆ„Ÿๅˆฐๆถๅฟƒ", type: "verb" }, + "twist": { user_language: "ๆ‰ญไผค", type: "verb" }, + "burn": { user_language: "็ƒงไผค", type: "verb" }, + "hurt": { user_language: "ๅ—ไผค", type: "verb" }, + "cut": { user_language: "ๅ‰ฒไผค", type: "verb" }, + "sprain": { user_language: "ๆ‰ญไผค", type: "verb" }, + "dislocate": { user_language: "่„ฑ่‡ผ", type: "verb" }, + "break": { user_language: "้ชจๆŠ˜", type: "verb" }, + + // Actions and Verbs + "recommend": { user_language: "ๆŽจ่", type: "verb" }, + "suggest": { user_language: "ๅปบ่ฎฎ", type: "verb" }, + "insist": { user_language: "ๅšๆŒ", type: "verb" }, + "warn": { user_language: "่ญฆๅ‘Š", type: "verb" }, + "promise": { user_language: "ๆ‰ฟ่ฏบ", type: "verb" }, + "apologize": { user_language: "้“ๆญ‰", type: "verb" }, + "complain": { user_language: "ๆŠฑๆ€จ", type: "verb" }, + "discuss": { user_language: "่ฎจ่ฎบ", type: "verb" }, + "argue": { user_language: "ไบ‰่ฎบ", type: "verb" }, + "disagree": { user_language: "ไธๅŒๆ„", type: "verb" }, + "agree": { user_language: "ๅŒๆ„", type: "verb" }, + "decide": { user_language: "ๅ†ณๅฎš", type: "verb" }, + "choose": { user_language: "้€‰ๆ‹ฉ", type: "verb" }, + "prefer": { user_language: "ๅ็ˆฑ", type: "verb" }, + "enjoy": { user_language: "ไบซๅ—", type: "verb" }, + "appreciate": { user_language: "ๆฌฃ่ต", type: "verb" }, + "celebrate": { user_language: "ๅบ†็ฅ", type: "verb" }, + "congratulate": { user_language: "็ฅ่ดบ", type: "verb" }, + + // Emotions and Feelings + "worried": { user_language: "ๆ‹…ๅฟƒ็š„", type: "adjective" }, + "concerned": { user_language: "ๅ…ณๅฟƒ็š„", type: "adjective" }, + "anxious": { user_language: "็„ฆ่™‘็š„", type: "adjective" }, + "nervous": { user_language: "็ดงๅผ ็š„", type: "adjective" }, + "excited": { user_language: "ๅ…ดๅฅ‹็š„", type: "adjective" }, + "thrilled": { user_language: "ๆฟ€ๅŠจ็š„", type: "adjective" }, + "delighted": { user_language: "้ซ˜ๅ…ด็š„", type: "adjective" }, + "pleased": { user_language: "ๆปกๆ„็š„", type: "adjective" }, + "satisfied": { user_language: "ๆปก่ถณ็š„", type: "adjective" }, + "disappointed": { user_language: "ๅคฑๆœ›็š„", type: "adjective" }, + "frustrated": { user_language: "ๆฒฎไธง็š„", type: "adjective" }, + "annoyed": { user_language: "ๆผๆ€’็š„", type: "adjective" }, + "furious": { user_language: "ๆ„คๆ€’็š„", type: "adjective" }, + "exhausted": { user_language: "็ญ‹็–ฒๅŠ›ๅฐฝ็š„", type: "adjective" }, + "overwhelmed": { user_language: "ไธ็Ÿฅๆ‰€ๆŽช็š„", type: "adjective" }, + "confused": { user_language: "ๅ›ฐๆƒ‘็š„", type: "adjective" }, + "embarrassed": { user_language: "ๅฐดๅฐฌ็š„", type: "adjective" }, + "proud": { user_language: "่‡ช่ฑช็š„", type: "adjective" }, + "jealous": { user_language: "ๅซ‰ๅฆ’็š„", type: "adjective" }, + "guilty": { user_language: "ๅ†…็–š็š„", type: "adjective" }, + + // Technology and Modern Life + "website": { user_language: "็ฝ‘็ซ™", type: "noun" }, + "password": { user_language: "ๅฏ†็ ", type: "noun" }, + "username": { user_language: "็”จๆˆทๅ", type: "noun" }, + "download": { user_language: "ไธ‹่ฝฝ", type: "verb" }, + "upload": { user_language: "ไธŠไผ ", type: "verb" }, + "install": { user_language: "ๅฎ‰่ฃ…", type: "verb" }, + "update": { user_language: "ๆ›ดๆ–ฐ", type: "verb" }, + "delete": { user_language: "ๅˆ ้™ค", type: "verb" }, + "save": { user_language: "ไฟๅญ˜", type: "verb" }, + "print": { user_language: "ๆ‰“ๅฐ", type: "verb" }, + "scan": { user_language: "ๆ‰ซๆ", type: "verb" }, + "copy": { user_language: "ๅคๅˆถ", type: "verb" }, + "paste": { user_language: "็ฒ˜่ดด", type: "verb" }, + "search": { user_language: "ๆœ็ดข", type: "verb" }, + "browse": { user_language: "ๆต่งˆ", type: "verb" }, + "surf": { user_language: "็ฝ‘ไธŠๅ†ฒๆตช", type: "verb" }, + "stream": { user_language: "ๆตๅช’ไฝ“", type: "verb" }, + "tweet": { user_language: "ๅ‘ๆŽจ็‰น", type: "verb" }, + "post": { user_language: "ๅ‘ๅธƒ", type: "verb" }, + "share": { user_language: "ๅˆ†ไบซ", type: "verb" }, + "like": { user_language: "็‚น่ตž", type: "verb" }, + "follow": { user_language: "ๅ…ณๆณจ", type: "verb" }, + "unfollow": { user_language: "ๅ–ๆถˆๅ…ณๆณจ", type: "verb" }, + "block": { user_language: "ๅฑ่”ฝ", type: "verb" }, + "tag": { user_language: "ๆ ‡่ฎฐ", type: "verb" } + }, + + // Compatibility methods for different games + sentences: [], // For backward compatibility + + // For Quiz and Memory games + getVocabularyPairs() { + return Object.entries(this.vocabulary).map(([word, data]) => ({ + english: word, + translation: data.user_language, + type: data.type + })); + } +}; \ No newline at end of file diff --git a/src/content/sbs-level-7-8.json b/src/content/sbs-level-7-8.json new file mode 100644 index 0000000..b03c7e3 --- /dev/null +++ b/src/content/sbs-level-7-8.json @@ -0,0 +1,141 @@ +{ + "name": "SBS Level 7-8 New", + "description": "Side by Side Level 7-8 vocabulary with language-agnostic format", + "difficulty": "intermediate", + "language": "en-US", + "vocabulary": { + "central": { "user_language": "ไธญๅฟƒ็š„๏ผ›ไธญๅคฎ็š„", "type": "adjective" }, + "avenue": { "user_language": "ๅคง่ก—๏ผ›ๆž—่ซ้“", "type": "noun" }, + "refrigerator": { "user_language": "ๅ†ฐ็ฎฑ", "type": "noun" }, + "closet": { "user_language": "่กฃๆŸœ๏ผ›ๅฃๆฉฑ", "type": "noun" }, + "elevator": { "user_language": "็”ตๆขฏ", "type": "noun" }, + "building": { "user_language": "ๅปบ็ญ‘็‰ฉ๏ผ›ๅคงๆฅผ", "type": "noun" }, + "air conditioner": { "user_language": "็ฉบ่ฐƒ", "type": "noun" }, + "superintendent": { "user_language": "ไธป็ฎก๏ผ›่ดŸ่ดฃไบบ", "type": "noun" }, + "bus stop": { "user_language": "ๅ…ฌไบค่ฝฆ็ซ™", "type": "noun" }, + "jacuzzi": { "user_language": "ๆŒ‰ๆ‘ฉๆตด็ผธ", "type": "noun" }, + "machine": { "user_language": "ๆœบๅ™จ๏ผ›่ฎพๅค‡", "type": "noun" }, + "two and a half": { "user_language": "ไธคไธชๅŠ", "type": "number" }, + "in the center of": { "user_language": "ๅœจโ€ฆโ€ฆไธญๅฟƒ", "type": "preposition" }, + "town": { "user_language": "ๅŸŽ้•‡", "type": "noun" }, + "a lot of": { "user_language": "่ฎธๅคš", "type": "determiner" }, + "noise": { "user_language": "ๅ™ช้Ÿณ", "type": "noun" }, + "sidewalks": { "user_language": "ไบบ่กŒ้“", "type": "noun" }, + "all day and all night": { "user_language": "ๆ•ดๆ—ฅๆ•ดๅคœ", "type": "adverb" }, + "convenient": { "user_language": "ไพฟๅˆฉ็š„", "type": "adjective" }, + "upset": { "user_language": "ๅคฑๆœ›็š„", "type": "adjective" }, + "shirt": { "user_language": "่กฌ่กซ", "type": "noun" }, + "coat": { "user_language": "ๅค–ๅฅ—ใ€ๅคง่กฃ", "type": "noun" }, + "dress": { "user_language": "่ฟž่กฃ่ฃ™", "type": "noun" }, + "skirt": { "user_language": "็Ÿญ่ฃ™", "type": "noun" }, + "blouse": { "user_language": "ๅฅณๅผ่กฌ่กซ", "type": "noun" }, + "jacket": { "user_language": "ๅคนๅ…‹ใ€็Ÿญๅค–ๅฅ—", "type": "noun" }, + "sweater": { "user_language": "ๆฏ›่กฃใ€้’ˆ็ป‡่กซ", "type": "noun" }, + "suit": { "user_language": "ๅฅ—่ฃ…ใ€่ฅฟ่ฃ…", "type": "noun" }, + "tie": { "user_language": "้ข†ๅธฆ", "type": "noun" }, + "pants": { "user_language": "่ฃคๅญ", "type": "noun" }, + "jeans": { "user_language": "็‰›ไป”่ฃค", "type": "noun" }, + "belt": { "user_language": "่…ฐๅธฆใ€็šฎๅธฆ", "type": "noun" }, + "hat": { "user_language": "ๅธฝๅญ", "type": "noun" }, + "glove": { "user_language": "ๆ‰‹ๅฅ—", "type": "noun" }, + "purse": { "user_language": "ๆ‰‹ๆๅŒ…ใ€ๅฅณๅผๅฐๅŒ…", "type": "noun" }, + "glasses": { "user_language": "็œผ้•œ", "type": "noun" }, + "pajamas": { "user_language": "็ก่กฃ", "type": "noun" }, + "socks": { "user_language": "่ขœๅญ", "type": "noun" }, + "shoes": { "user_language": "้ž‹ๅญ", "type": "noun" }, + "bathrobe": { "user_language": "ๆตด่ข", "type": "noun" }, + "tee shirt": { "user_language": "Tๆค", "type": "noun" }, + "scarf": { "user_language": "ๅ›ดๅทพ", "type": "noun" }, + "wallet": { "user_language": "้’ฑๅŒ…", "type": "noun" }, + "ring": { "user_language": "ๆˆ’ๆŒ‡", "type": "noun" }, + "sandals": { "user_language": "ๅ‡‰้ž‹", "type": "noun" }, + "throat": { "user_language": "ๅ–‰ๅ’™", "type": "noun" }, + "shoulder": { "user_language": "่‚ฉ่†€", "type": "noun" }, + "chest": { "user_language": "่ƒธ้ƒจ", "type": "noun" }, + "back": { "user_language": "่ƒŒ้ƒจ", "type": "noun" }, + "arm": { "user_language": "ๆ‰‹่‡‚", "type": "noun" }, + "elbow": { "user_language": "่‚˜้ƒจ", "type": "noun" }, + "wrist": { "user_language": "ๆ‰‹่…•", "type": "noun" }, + "hip": { "user_language": "้ซ‹้ƒจ", "type": "noun" }, + "thigh": { "user_language": "ๅคง่…ฟ", "type": "noun" }, + "knee": { "user_language": "่†็›–", "type": "noun" }, + "shin": { "user_language": "่ƒซ้ชจ", "type": "noun" }, + "ankle": { "user_language": "่„š่ธ", "type": "noun" }, + "cough": { "user_language": "ๅ’ณๅ—ฝ", "type": "verb" }, + "sneeze": { "user_language": "ๆ‰“ๅ–ทๅš", "type": "verb" }, + "wheeze": { "user_language": "ๅ–˜ๆฏ", "type": "verb" }, + "feel dizzy": { "user_language": "ๆ„Ÿๅˆฐๅคดๆ™•", "type": "verb" }, + "feel nauseous": { "user_language": "ๆ„Ÿๅˆฐๆถๅฟƒ", "type": "verb" }, + "twist": { "user_language": "ๆ‰ญไผค", "type": "verb" }, + "burn": { "user_language": "็ƒงไผค", "type": "verb" }, + "hurt": { "user_language": "ๅ—ไผค", "type": "verb" }, + "cut": { "user_language": "ๅ‰ฒไผค", "type": "verb" }, + "sprain": { "user_language": "ๆ‰ญไผค", "type": "verb" }, + "dislocate": { "user_language": "่„ฑ่‡ผ", "type": "verb" }, + "break": { "user_language": "้ชจๆŠ˜", "type": "verb" }, + "recommend": { "user_language": "ๆŽจ่", "type": "verb" }, + "suggest": { "user_language": "ๅปบ่ฎฎ", "type": "verb" }, + "insist": { "user_language": "ๅšๆŒ", "type": "verb" }, + "warn": { "user_language": "่ญฆๅ‘Š", "type": "verb" }, + "promise": { "user_language": "ๆ‰ฟ่ฏบ", "type": "verb" }, + "apologize": { "user_language": "้“ๆญ‰", "type": "verb" }, + "complain": { "user_language": "ๆŠฑๆ€จ", "type": "verb" }, + "discuss": { "user_language": "่ฎจ่ฎบ", "type": "verb" }, + "argue": { "user_language": "ไบ‰่ฎบ", "type": "verb" }, + "disagree": { "user_language": "ไธๅŒๆ„", "type": "verb" }, + "agree": { "user_language": "ๅŒๆ„", "type": "verb" }, + "decide": { "user_language": "ๅ†ณๅฎš", "type": "verb" }, + "choose": { "user_language": "้€‰ๆ‹ฉ", "type": "verb" }, + "prefer": { "user_language": "ๅ็ˆฑ", "type": "verb" }, + "enjoy": { "user_language": "ไบซๅ—", "type": "verb" }, + "appreciate": { "user_language": "ๆฌฃ่ต", "type": "verb" }, + "celebrate": { "user_language": "ๅบ†็ฅ", "type": "verb" }, + "congratulate": { "user_language": "็ฅ่ดบ", "type": "verb" }, + "worried": { "user_language": "ๆ‹…ๅฟƒ็š„", "type": "adjective" }, + "concerned": { "user_language": "ๅ…ณๅฟƒ็š„", "type": "adjective" }, + "anxious": { "user_language": "็„ฆ่™‘็š„", "type": "adjective" }, + "nervous": { "user_language": "็ดงๅผ ็š„", "type": "adjective" }, + "excited": { "user_language": "ๅ…ดๅฅ‹็š„", "type": "adjective" }, + "thrilled": { "user_language": "ๆฟ€ๅŠจ็š„", "type": "adjective" }, + "delighted": { "user_language": "้ซ˜ๅ…ด็š„", "type": "adjective" }, + "pleased": { "user_language": "ๆปกๆ„็š„", "type": "adjective" }, + "satisfied": { "user_language": "ๆปก่ถณ็š„", "type": "adjective" }, + "disappointed": { "user_language": "ๅคฑๆœ›็š„", "type": "adjective" }, + "frustrated": { "user_language": "ๆฒฎไธง็š„", "type": "adjective" }, + "annoyed": { "user_language": "ๆผๆ€’็š„", "type": "adjective" }, + "furious": { "user_language": "ๆ„คๆ€’็š„", "type": "adjective" }, + "exhausted": { "user_language": "็ญ‹็–ฒๅŠ›ๅฐฝ็š„", "type": "adjective" }, + "overwhelmed": { "user_language": "ไธ็Ÿฅๆ‰€ๆŽช็š„", "type": "adjective" }, + "confused": { "user_language": "ๅ›ฐๆƒ‘็š„", "type": "adjective" }, + "embarrassed": { "user_language": "ๅฐดๅฐฌ็š„", "type": "adjective" }, + "proud": { "user_language": "่‡ช่ฑช็š„", "type": "adjective" }, + "jealous": { "user_language": "ๅซ‰ๅฆ’็š„", "type": "adjective" }, + "guilty": { "user_language": "ๅ†…็–š็š„", "type": "adjective" }, + "website": { "user_language": "็ฝ‘็ซ™", "type": "noun" }, + "password": { "user_language": "ๅฏ†็ ", "type": "noun" }, + "username": { "user_language": "็”จๆˆทๅ", "type": "noun" }, + "download": { "user_language": "ไธ‹่ฝฝ", "type": "verb" }, + "upload": { "user_language": "ไธŠไผ ", "type": "verb" }, + "install": { "user_language": "ๅฎ‰่ฃ…", "type": "verb" }, + "update": { "user_language": "ๆ›ดๆ–ฐ", "type": "verb" }, + "delete": { "user_language": "ๅˆ ้™ค", "type": "verb" }, + "save": { "user_language": "ไฟๅญ˜", "type": "verb" }, + "print": { "user_language": "ๆ‰“ๅฐ", "type": "verb" }, + "scan": { "user_language": "ๆ‰ซๆ", "type": "verb" }, + "copy": { "user_language": "ๅคๅˆถ", "type": "verb" }, + "paste": { "user_language": "็ฒ˜่ดด", "type": "verb" }, + "search": { "user_language": "ๆœ็ดข", "type": "verb" }, + "browse": { "user_language": "ๆต่งˆ", "type": "verb" }, + "surf": { "user_language": "็ฝ‘ไธŠๅ†ฒๆตช", "type": "verb" }, + "stream": { "user_language": "ๆตๅช’ไฝ“", "type": "verb" }, + "tweet": { "user_language": "ๅ‘ๆŽจ็‰น", "type": "verb" }, + "post": { "user_language": "ๅ‘ๅธƒ", "type": "verb" }, + "share": { "user_language": "ๅˆ†ไบซ", "type": "verb" }, + "like": { "user_language": "็‚น่ตž", "type": "verb" }, + "follow": { "user_language": "ๅ…ณๆณจ", "type": "verb" }, + "unfollow": { "user_language": "ๅ–ๆถˆๅ…ณๆณจ", "type": "verb" }, + "block": { "user_language": "ๅฑ่”ฝ", "type": "verb" }, + "tag": { "user_language": "ๆ ‡่ฎฐ", "type": "verb" } + }, + "sentences": [] +} \ No newline at end of file diff --git a/src/content/story-prototype-optimized.js b/src/content/story-prototype-optimized.js new file mode 100644 index 0000000..145c6a4 --- /dev/null +++ b/src/content/story-prototype-optimized.js @@ -0,0 +1,325 @@ +// === OPTIMIZED STORY PROTOTYPE WITH CENTRALIZED VOCABULARY === +// Story content with single vocabulary definition that works across all games + +window.ContentModules = window.ContentModules || {}; + +window.ContentModules.StoryPrototypeOptimized = { + name: "The Magical Library (Optimized)", + description: "Adventure story with centralized vocabulary system", + difficulty: "intermediate", + language: "en-US", + + // Centralized vocabulary - defined once, used everywhere + vocabulary: { + // Key words for the story + "library": { + translation: "bibliothรจque", + user_language: "bibliothรจque", + pronunciation: "/หˆlaษชbrษ™ri/", + type: "noun" + }, + "magical": { + translation: "magique", + user_language: "magique", + pronunciation: "/หˆmรฆdส’ษชkษ™l/", + type: "adjective" + }, + "discovered": { + translation: "dรฉcouvert", + pronunciation: "/dษชหˆskสŒvษ™rd/", + type: "verb" + }, + "ancient": { + translation: "ancien", + pronunciation: "/หˆeษชnสƒษ™nt/", + type: "adjective" + }, + "mysterious": { + translation: "mystรฉrieux", + pronunciation: "/mษชหˆstษชriษ™s/", + type: "adjective" + }, + "adventure": { + translation: "aventure", + pronunciation: "/ษ™dหˆventสƒษ™r/", + type: "noun" + }, + "explore": { + translation: "explorer", + pronunciation: "/ษชkหˆsplษ”หr/", + type: "verb" + }, + "hidden": { + translation: "cachรฉ", + pronunciation: "/หˆhษชdn/", + type: "adjective" + }, + "secret": { + translation: "secret", + pronunciation: "/หˆsiหkrษ™t/", + type: "adjective" + }, + "passage": { + translation: "passage", + pronunciation: "/หˆpรฆsษชdส’/", + type: "noun" + }, + "glowing": { + translation: "brillant", + pronunciation: "/หˆษกloสŠษชล‹/", + type: "adjective" + }, + "symbols": { + translation: "symboles", + pronunciation: "/หˆsษชmbษ™lz/", + type: "noun" + }, + "whispered": { + translation: "chuchotรฉ", + pronunciation: "/หˆwษชspษ™rd/", + type: "verb" + }, + "carefully": { + translation: "prudemment", + pronunciation: "/หˆkษ›rfษ™li/", + type: "adverb" + }, + "approached": { + translation: "approchรฉ", + pronunciation: "/ษ™หˆproสŠtสƒt/", + type: "verb" + }, + "magnificent": { + translation: "magnifique", + pronunciation: "/mรฆษกหˆnษชfษชsษ™nt/", + type: "adjective" + }, + "chamber": { + translation: "chambre", + pronunciation: "/หˆtสƒeษชmbษ™r/", + type: "noun" + }, + "floating": { + translation: "flottant", + pronunciation: "/หˆfloสŠtษชล‹/", + type: "adjective" + }, + "shelves": { + translation: "รฉtagรจres", + pronunciation: "/สƒษ›lvz/", + type: "noun" + }, + "reaching": { + translation: "atteignant", + pronunciation: "/หˆriหtสƒษชล‹/", + type: "verb" + }, + "touched": { + translation: "touchรฉ", + pronunciation: "/tสŒtสƒt/", + type: "verb" + }, + "spine": { + translation: "dos (du livre)", + pronunciation: "/spaษชn/", + type: "noun" + }, + "shimmered": { + translation: "scintillรฉ", + pronunciation: "/หˆสƒษชmษ™rd/", + type: "verb" + }, + "transformed": { + translation: "transformรฉ", + pronunciation: "/trรฆnsหˆfษ”หrmd/", + type: "verb" + }, + "portal": { + translation: "portail", + pronunciation: "/หˆpษ”หrtl/", + type: "noun" + }, + "worlds": { + translation: "mondes", + pronunciation: "/wษœหrldz/", + type: "noun" + }, + "imagination": { + translation: "imagination", + pronunciation: "/ษชหŒmรฆdส’ษชหˆneษชสƒษ™n/", + type: "noun" + }, + "realized": { + translation: "rรฉalisรฉ", + pronunciation: "/หˆriหษ™laษชzd/", + type: "verb" + }, + "ordinary": { + translation: "ordinaire", + pronunciation: "/หˆษ”หrdneri/", + type: "adjective" + }, + "gateway": { + translation: "passerelle", + pronunciation: "/หˆษกeษชtweษช/", + type: "noun" + }, + "infinite": { + translation: "infini", + pronunciation: "/หˆษชnfษชnษชt/", + type: "adjective" + }, + "possibilities": { + translation: "possibilitรฉs", + pronunciation: "/หŒpษ‘หsษ™หˆbษชlษ™tiz/", + type: "noun" + }, + "knowledge": { + translation: "connaissance", + pronunciation: "/หˆnษ‘หlษชdส’/", + type: "noun" + }, + "wisdom": { + translation: "sagesse", + pronunciation: "/หˆwษชzdษ™m/", + type: "noun" + }, + "keeper": { + translation: "gardien", + pronunciation: "/หˆkiหpษ™r/", + type: "noun" + }, + "guardian": { + translation: "gardien", + pronunciation: "/หˆษกษ‘หrdiษ™n/", + type: "noun" + }, + "appeared": { + translation: "apparu", + pronunciation: "/ษ™หˆpษชrd/", + type: "verb" + }, + "welcomed": { + translation: "accueilli", + pronunciation: "/หˆwษ›lkษ™md/", + type: "verb" + }, + "journey": { + translation: "voyage", + pronunciation: "/หˆdส’ษœหrni/", + type: "noun" + }, + "beginning": { + translation: "dรฉbut", + pronunciation: "/bษชหˆษกษชnษชล‹/", + type: "noun" + } + }, + + // Story content - just the sentences, no word-by-word translations + story: { + title: "The Magical Library", + chapters: [ + { + title: "Chapter 1: The Discovery", + sentences: [ + { + id: 1, + original: "Emma had always loved books, but she never imagined she would discover a magical library.", + translation: "Emma avait toujours aimรฉ les livres, mais elle n'avait jamais imaginรฉ qu'elle dรฉcouvrirait une bibliothรจque magique." + }, + { + id: 2, + original: "One rainy afternoon, while exploring the ancient mansion, she found a hidden door.", + translation: "Un aprรจs-midi pluvieux, en explorant l'ancien manoir, elle trouva une porte cachรฉe." + }, + { + id: 3, + original: "Behind the door was a secret passage that led to an underground chamber.", + translation: "Derriรจre la porte se trouvait un passage secret qui menait ร  une chambre souterraine." + }, + { + id: 4, + original: "The walls were covered with glowing symbols that seemed to whispered ancient secrets.", + translation: "Les murs รฉtaient couverts de symboles brillants qui semblaient chuchoter des secrets anciens." + }, + { + id: 5, + original: "Emma carefully approached the center of the room where a mysterious book floated in midair.", + translation: "Emma s'approcha prudemment du centre de la piรจce oรน un livre mystรฉrieux flottait dans les airs." + } + ] + }, + { + title: "Chapter 2: The Transformation", + sentences: [ + { + id: 6, + original: "As she entered the chamber, the room transformed into a magnificent library.", + translation: "Alors qu'elle entrait dans la chambre, la piรจce se transforma en une magnifique bibliothรจque." + }, + { + id: 7, + original: "Books were floating everywhere, and the shelves seemed to be reaching up to infinity.", + translation: "Des livres flottaient partout, et les รฉtagรจres semblaient s'รฉtendre jusqu'ร  l'infini." + }, + { + id: 8, + original: "When Emma touched the spine of one book, it shimmered and opened a portal to another world.", + translation: "Quand Emma toucha le dos d'un livre, il scintilla et ouvrit un portail vers un autre monde." + }, + { + id: 9, + original: "She realized that this was no ordinary library - it was a gateway to infinite worlds of imagination.", + translation: "Elle rรฉalisa que ce n'รฉtait pas une bibliothรจque ordinaire - c'รฉtait une passerelle vers des mondes infinis d'imagination." + }, + { + id: 10, + original: "The keeper of the library appeared and welcomed her to begin her journey through knowledge and wisdom.", + translation: "Le gardien de la bibliothรจque apparut et l'accueillit pour commencer son voyage ร  travers la connaissance et la sagesse." + } + ] + } + ] + }, + + // Compatibility functions for different games + + // For Whack-a-Mole game + getVocabularyPairs() { + return Object.entries(this.vocabulary).map(([word, data]) => ({ + english: word, + translation: data.translation, + pronunciation: data.pronunciation + })); + }, + + // For Memory Match game + getMatchingPairs() { + return Object.entries(this.vocabulary) + .slice(0, 12) // Limit to 12 pairs for memory game + .map(([word, data]) => ({ + english: word, + chinese: data.translation + })); + }, + + // For Quiz game + getQuizQuestions() { + return Object.entries(this.vocabulary).map(([word, data]) => ({ + question: `What does "${word}" mean?`, + answer: data.translation, + options: this.generateOptions(data.translation) + })); + }, + + generateOptions(correctAnswer) { + const allTranslations = Object.values(this.vocabulary) + .map(v => v.translation) + .filter(t => t !== correctAnswer); + + const shuffled = allTranslations.sort(() => Math.random() - 0.5); + const options = [correctAnswer, ...shuffled.slice(0, 3)]; + return options.sort(() => Math.random() - 0.5); + } +}; \ No newline at end of file diff --git a/src/core/EventBus.js b/src/core/EventBus.js new file mode 100644 index 0000000..0891960 --- /dev/null +++ b/src/core/EventBus.js @@ -0,0 +1,215 @@ +/** + * EventBus - Strict event-driven communication system + * Enforces type safety and prevents direct module coupling + */ + +class EventBus { + constructor() { + // Private event storage + this._listeners = new Map(); + this._moduleRegistry = new Map(); + this._eventHistory = []; + this._maxHistorySize = 1000; + + // Seal to prevent external modification + Object.seal(this); + } + + /** + * Register a module with the event bus + * @param {Module} module - Module instance + */ + registerModule(module) { + if (!module || typeof module.name !== 'string') { + throw new Error('Invalid module: must have a name property'); + } + + if (this._moduleRegistry.has(module.name)) { + throw new Error(`Module ${module.name} is already registered`); + } + + this._moduleRegistry.set(module.name, module); + } + + /** + * Unregister a module and clean up its listeners + * @param {string} moduleName - Name of module to unregister + */ + unregisterModule(moduleName) { + if (!this._moduleRegistry.has(moduleName)) { + throw new Error(`Module ${moduleName} is not registered`); + } + + // Remove all listeners for this module + for (const [eventType, listeners] of this._listeners) { + const filteredListeners = listeners.filter(listener => listener.module !== moduleName); + if (filteredListeners.length === 0) { + this._listeners.delete(eventType); + } else { + this._listeners.set(eventType, filteredListeners); + } + } + + this._moduleRegistry.delete(moduleName); + } + + /** + * Subscribe to an event type + * @param {string} eventType - Type of event to listen for + * @param {Function} callback - Function to call when event occurs + * @param {string} moduleName - Name of the subscribing module + */ + on(eventType, callback, moduleName) { + this._validateEventType(eventType); + this._validateCallback(callback); + this._validateModule(moduleName); + + if (!this._listeners.has(eventType)) { + this._listeners.set(eventType, []); + } + + const listener = { + callback, + module: moduleName, + id: this._generateId() + }; + + this._listeners.get(eventType).push(listener); + return listener.id; // Return ID for unsubscribing + } + + /** + * Unsubscribe from an event + * @param {string} eventType - Event type to unsubscribe from + * @param {string} listenerId - ID returned from on() method + */ + off(eventType, listenerId) { + this._validateEventType(eventType); + + if (!this._listeners.has(eventType)) { + return false; + } + + const listeners = this._listeners.get(eventType); + const index = listeners.findIndex(listener => listener.id === listenerId); + + if (index === -1) { + return false; + } + + listeners.splice(index, 1); + + if (listeners.length === 0) { + this._listeners.delete(eventType); + } + + return true; + } + + /** + * Emit an event to all subscribers + * @param {string} eventType - Type of event + * @param {*} data - Event data + * @param {string} sourceModule - Module emitting the event + */ + emit(eventType, data = null, sourceModule) { + this._validateEventType(eventType); + this._validateModule(sourceModule); + + const event = { + type: eventType, + data, + source: sourceModule, + timestamp: Date.now(), + id: this._generateId() + }; + + // Add to history + this._addToHistory(event); + + // Get listeners for this event type + const listeners = this._listeners.get(eventType) || []; + + // Call all listeners (async to prevent blocking) + listeners.forEach(listener => { + try { + // Prevent modules from listening to their own events (optional) + if (listener.module !== sourceModule) { + setTimeout(() => listener.callback(event), 0); + } + } catch (error) { + console.error(`Error in event listener for ${eventType}:`, error); + } + }); + + return event.id; + } + + /** + * Get event history (for debugging) + * @param {number} limit - Maximum number of events to return + */ + getEventHistory(limit = 50) { + return this._eventHistory.slice(-limit); + } + + /** + * Get registered modules (for debugging) + */ + getRegisteredModules() { + return Array.from(this._moduleRegistry.keys()); + } + + /** + * Get active listeners (for debugging) + */ + getActiveListeners() { + const result = {}; + for (const [eventType, listeners] of this._listeners) { + result[eventType] = listeners.map(l => l.module); + } + return result; + } + + // Private validation methods + _validateEventType(eventType) { + if (!eventType || typeof eventType !== 'string') { + throw new Error('Event type must be a non-empty string'); + } + } + + _validateCallback(callback) { + if (typeof callback !== 'function') { + throw new Error('Callback must be a function'); + } + } + + _validateModule(moduleName) { + if (!moduleName || typeof moduleName !== 'string') { + throw new Error('Module name must be a non-empty string'); + } + + if (!this._moduleRegistry.has(moduleName)) { + throw new Error(`Module ${moduleName} is not registered with EventBus`); + } + } + + _generateId() { + return Math.random().toString(36).substr(2, 9); + } + + _addToHistory(event) { + this._eventHistory.push(event); + + // Limit history size + if (this._eventHistory.length > this._maxHistorySize) { + this._eventHistory.shift(); + } + } +} + +// Freeze to prevent modification +Object.freeze(EventBus); +Object.freeze(EventBus.prototype); + +export default EventBus; \ No newline at end of file diff --git a/src/core/Module.js b/src/core/Module.js new file mode 100644 index 0000000..455a65d --- /dev/null +++ b/src/core/Module.js @@ -0,0 +1,104 @@ +/** + * Base Module class - Enforces strict module contracts + * Every module MUST extend this class and implement required methods + */ + +const privateData = new WeakMap(); + +class Module { + constructor(name, dependencies = []) { + if (new.target === Module) { + throw new Error('Module is abstract and cannot be instantiated directly'); + } + + if (!name || typeof name !== 'string') { + throw new Error('Module name is required and must be a string'); + } + + // Private data storage + privateData.set(this, { + name, + dependencies, + state: 'uninitialized', + initialized: false, + destroyed: false + }); + + // Seal the module to prevent external modification + Object.seal(this); + } + + // Public getters (read-only) + get name() { + return privateData.get(this).name; + } + + get dependencies() { + return [...privateData.get(this).dependencies]; // Return copy + } + + get state() { + return privateData.get(this).state; + } + + get isInitialized() { + return privateData.get(this).initialized; + } + + get isDestroyed() { + return privateData.get(this).destroyed; + } + + // Abstract methods - MUST be implemented by subclasses + init() { + throw new Error(`Module ${this.name}: init() method must be implemented`); + } + + destroy() { + throw new Error(`Module ${this.name}: destroy() method must be implemented`); + } + + // Protected methods (for internal use) + _setState(newState) { + const data = privateData.get(this); + if (data.destroyed) { + throw new Error(`Module ${this.name} is destroyed and cannot change state`); + } + data.state = newState; + } + + _setInitialized() { + const data = privateData.get(this); + if (data.initialized) { + throw new Error(`Module ${this.name} is already initialized`); + } + data.initialized = true; + data.state = 'initialized'; + } + + _setDestroyed() { + const data = privateData.get(this); + data.destroyed = true; + data.initialized = false; + data.state = 'destroyed'; + } + + // Validation helpers + _validateNotDestroyed() { + if (this.isDestroyed) { + throw new Error(`Module ${this.name} is destroyed and cannot be used`); + } + } + + _validateInitialized() { + if (!this.isInitialized) { + throw new Error(`Module ${this.name} must be initialized before use`); + } + } +} + +// Freeze the class to prevent modification +Object.freeze(Module); +Object.freeze(Module.prototype); + +export default Module; \ No newline at end of file diff --git a/src/core/ModuleLoader.js b/src/core/ModuleLoader.js new file mode 100644 index 0000000..6a0ad79 --- /dev/null +++ b/src/core/ModuleLoader.js @@ -0,0 +1,273 @@ +/** + * ModuleLoader - Strict dependency injection and module loading + * Enforces proper initialization order and dependency resolution + */ + +import EventBus from './EventBus.js'; + +class ModuleLoader { + constructor(eventBus) { + if (!(eventBus instanceof EventBus)) { + throw new Error('ModuleLoader requires an EventBus instance'); + } + + // Private state + this._eventBus = eventBus; + this._modules = new Map(); + this._loadedModules = new Map(); + this._initializationOrder = []; + this._isInitializing = false; + + // Seal to prevent modification + Object.seal(this); + } + + /** + * Register a module class for lazy loading + * @param {string} name - Module name + * @param {Function} moduleClass - Module constructor + * @param {Array} dependencies - Array of dependency names + */ + register(name, moduleClass, dependencies = []) { + this._validateModuleName(name); + this._validateModuleClass(moduleClass); + this._validateDependencies(dependencies); + + if (this._modules.has(name)) { + throw new Error(`Module ${name} is already registered`); + } + + this._modules.set(name, { + name, + moduleClass, + dependencies, + instance: null, + loaded: false, + initialized: false + }); + } + + /** + * Load and initialize a module with its dependencies + * @param {string} name - Module name to load + * @param {Object} config - Configuration object for the module + * @returns {Promise} - The loaded module instance + */ + async load(name, config = {}) { + this._validateModuleName(name); + + if (this._loadedModules.has(name)) { + return this._loadedModules.get(name); + } + + const moduleInfo = this._modules.get(name); + if (!moduleInfo) { + throw new Error(`Module ${name} is not registered`); + } + + // Resolve dependencies first + const dependencies = await this._resolveDependencies(moduleInfo.dependencies); + + // Create module instance + const moduleInstance = new moduleInfo.moduleClass(name, dependencies, config); + + // Validate that instance extends Module base class + if (!moduleInstance.name || typeof moduleInstance.init !== 'function') { + throw new Error(`Module ${name} must extend Module base class`); + } + + // Register with EventBus + this._eventBus.registerModule(moduleInstance); + + // Store the instance + this._loadedModules.set(name, moduleInstance); + moduleInfo.instance = moduleInstance; + moduleInfo.loaded = true; + + return moduleInstance; + } + + /** + * Initialize a loaded module + * @param {string} name - Module name to initialize + * @returns {Promise} - The initialized module + */ + async initialize(name) { + const moduleInfo = this._modules.get(name); + if (!moduleInfo || !moduleInfo.loaded) { + throw new Error(`Module ${name} must be loaded before initialization`); + } + + if (moduleInfo.initialized) { + return moduleInfo.instance; + } + + try { + await moduleInfo.instance.init(); + moduleInfo.instance._setInitialized(); + moduleInfo.initialized = true; + this._initializationOrder.push(name); + + // Emit module ready event + this._eventBus.emit('module:ready', { module: name }, 'ModuleLoader'); + + } catch (error) { + throw new Error(`Failed to initialize module ${name}: ${error.message}`); + } + + return moduleInfo.instance; + } + + /** + * Load and initialize a module in one step + * @param {string} name - Module name + * @param {Object} config - Module configuration + * @returns {Promise} - The ready module + */ + async loadAndInitialize(name, config = {}) { + const module = await this.load(name, config); + return await this.initialize(name); + } + + /** + * Destroy a module and clean up + * @param {string} name - Module name to destroy + */ + async destroy(name) { + const moduleInfo = this._modules.get(name); + if (!moduleInfo || !moduleInfo.loaded) { + return; // Already destroyed or never loaded + } + + try { + // Call module's destroy method + await moduleInfo.instance.destroy(); + moduleInfo.instance._setDestroyed(); + + // Unregister from EventBus + this._eventBus.unregisterModule(name); + + // Clean up references + this._loadedModules.delete(name); + moduleInfo.instance = null; + moduleInfo.loaded = false; + moduleInfo.initialized = false; + + // Remove from initialization order + const index = this._initializationOrder.indexOf(name); + if (index > -1) { + this._initializationOrder.splice(index, 1); + } + + // Emit module destroyed event + this._eventBus.emit('module:destroyed', { module: name }, 'ModuleLoader'); + + } catch (error) { + console.error(`Error destroying module ${name}:`, error); + } + } + + /** + * Get a loaded module instance + * @param {string} name - Module name + * @returns {Module|null} - Module instance or null if not loaded + */ + getModule(name) { + return this._loadedModules.get(name) || null; + } + + /** + * Check if a module is loaded + * @param {string} name - Module name + * @returns {boolean} + */ + isLoaded(name) { + const moduleInfo = this._modules.get(name); + return moduleInfo ? moduleInfo.loaded : false; + } + + /** + * Check if a module is initialized + * @param {string} name - Module name + * @returns {boolean} + */ + isInitialized(name) { + const moduleInfo = this._modules.get(name); + return moduleInfo ? moduleInfo.initialized : false; + } + + /** + * Get status information for debugging + */ + getStatus() { + const status = { + registered: [], + loaded: [], + initialized: this._initializationOrder.slice() + }; + + for (const [name, info] of this._modules) { + status.registered.push(name); + if (info.loaded) { + status.loaded.push(name); + } + } + + return status; + } + + // Private methods + async _resolveDependencies(dependencies) { + const resolved = {}; + + for (const depName of dependencies) { + if (!this._modules.has(depName)) { + throw new Error(`Dependency ${depName} is not registered`); + } + + // Load dependency if not already loaded + if (!this.isLoaded(depName)) { + await this.load(depName); + } + + // Initialize dependency if not already initialized + if (!this.isInitialized(depName)) { + await this.initialize(depName); + } + + resolved[depName] = this.getModule(depName); + } + + return resolved; + } + + _validateModuleName(name) { + if (!name || typeof name !== 'string') { + throw new Error('Module name must be a non-empty string'); + } + } + + _validateModuleClass(moduleClass) { + if (typeof moduleClass !== 'function') { + throw new Error('Module class must be a constructor function'); + } + } + + _validateDependencies(dependencies) { + if (!Array.isArray(dependencies)) { + throw new Error('Dependencies must be an array'); + } + + for (const dep of dependencies) { + if (typeof dep !== 'string') { + throw new Error('All dependencies must be strings'); + } + } + } +} + +// Freeze to prevent modification +Object.freeze(ModuleLoader); +Object.freeze(ModuleLoader.prototype); + +export default ModuleLoader; \ No newline at end of file diff --git a/src/core/Router.js b/src/core/Router.js new file mode 100644 index 0000000..d4a9e0b --- /dev/null +++ b/src/core/Router.js @@ -0,0 +1,317 @@ +/** + * Router - Strict navigation and state management + * Enforces proper route handling and prevents direct URL manipulation + */ + +import Module from './Module.js'; + +class Router extends Module { + constructor(name, dependencies, config = {}) { + super(name, ['eventBus']); + + // Validate dependencies + if (!dependencies.eventBus) { + throw new Error('Router requires EventBus dependency'); + } + + this._eventBus = dependencies.eventBus; + this._routes = new Map(); + this._currentRoute = null; + this._history = []; + this._maxHistorySize = config.maxHistorySize || 100; + this._defaultRoute = config.defaultRoute || '/'; + + // Bind methods to prevent context loss + this._handlePopState = this._handlePopState.bind(this); + + Object.seal(this); + } + + async init() { + this._validateNotDestroyed(); + + // Set up browser navigation handling + window.addEventListener('popstate', this._handlePopState); + + // Set up route change listener + this._eventBus.on('router:navigate', (event) => { + this.navigate(event.data.path, event.data.state); + }, this.name); + + // Handle initial route + this._handleCurrentRoute(); + + this._setInitialized(); + } + + async destroy() { + this._validateNotDestroyed(); + + // Clean up event listeners + window.removeEventListener('popstate', this._handlePopState); + + // Clear routes and history + this._routes.clear(); + this._history = []; + this._currentRoute = null; + + this._setDestroyed(); + } + + /** + * Register a route with its handler + * @param {string} path - Route path pattern + * @param {Function} handler - Function to handle the route + * @param {Object} options - Route options (guards, middleware, etc.) + */ + register(path, handler, options = {}) { + this._validateInitialized(); + this._validatePath(path); + this._validateHandler(handler); + + if (this._routes.has(path)) { + throw new Error(`Route ${path} is already registered`); + } + + const route = { + path, + handler, + options: { + title: options.title || '', + guards: options.guards || [], + middleware: options.middleware || [], + exact: options.exact !== false // Default to exact matching + } + }; + + this._routes.set(path, route); + + // Emit route registered event + this._eventBus.emit('router:route-registered', { path }, this.name); + } + + /** + * Unregister a route + * @param {string} path - Route path to unregister + */ + unregister(path) { + this._validateInitialized(); + + if (!this._routes.has(path)) { + return false; + } + + this._routes.delete(path); + + // Emit route unregistered event + this._eventBus.emit('router:route-unregistered', { path }, this.name); + + return true; + } + + /** + * Navigate to a specific path + * @param {string} path - Target path + * @param {Object} state - State object to pass with navigation + * @param {boolean} replace - Whether to replace current history entry + */ + navigate(path, state = {}, replace = false) { + this._validateInitialized(); + this._validatePath(path); + + const route = this._findMatchingRoute(path); + if (!route) { + throw new Error(`No route found for path: ${path}`); + } + + // Run route guards + if (!this._runGuards(route, path, state)) { + return false; + } + + // Update browser history + if (replace) { + window.history.replaceState(state, route.options.title, path); + } else { + window.history.pushState(state, route.options.title, path); + } + + // Execute navigation + this._executeRoute(route, path, state); + + return true; + } + + /** + * Go back in history + */ + back() { + this._validateInitialized(); + window.history.back(); + } + + /** + * Go forward in history + */ + forward() { + this._validateInitialized(); + window.history.forward(); + } + + /** + * Replace current route + * @param {string} path - New path + * @param {Object} state - New state + */ + replace(path, state = {}) { + this.navigate(path, state, true); + } + + /** + * Get current route information + */ + getCurrentRoute() { + return this._currentRoute ? { ...this._currentRoute } : null; + } + + /** + * Get navigation history + */ + getHistory() { + return [...this._history]; + } + + /** + * Check if a path matches the current route + * @param {string} path - Path to check + */ + isCurrentRoute(path) { + return this._currentRoute && this._currentRoute.path === path; + } + + // Private methods + _handlePopState(event) { + const path = window.location.pathname + window.location.search + window.location.hash; + const state = event.state || {}; + + const route = this._findMatchingRoute(path); + if (route) { + this._executeRoute(route, path, state); + } else { + // Navigate to default route if no match + this.navigate(this._defaultRoute); + } + } + + _handleCurrentRoute() { + const path = window.location.pathname + window.location.search + window.location.hash; + const route = this._findMatchingRoute(path); + + if (route) { + this._executeRoute(route, path, window.history.state || {}); + } else { + // Navigate to default route + this.navigate(this._defaultRoute); + } + } + + _findMatchingRoute(path) { + for (const [routePath, route] of this._routes) { + if (this._pathMatches(routePath, path, route.options.exact)) { + return route; + } + } + return null; + } + + _pathMatches(routePath, actualPath, exact = true) { + if (exact) { + return routePath === actualPath; + } else { + return actualPath.startsWith(routePath); + } + } + + _runGuards(route, path, state) { + for (const guard of route.options.guards) { + try { + if (!guard(path, state)) { + return false; + } + } catch (error) { + console.error('Route guard error:', error); + return false; + } + } + return true; + } + + async _executeRoute(route, path, state) { + // Run middleware + for (const middleware of route.options.middleware) { + try { + await middleware(path, state); + } catch (error) { + console.error('Route middleware error:', error); + return; + } + } + + // Update current route + const routeInfo = { + path, + state, + timestamp: Date.now() + }; + + this._currentRoute = routeInfo; + this._addToHistory(routeInfo); + + // Emit route change event + this._eventBus.emit('router:route-changed', { + path, + state, + previous: this._history[this._history.length - 2] || null + }, this.name); + + // Execute route handler + try { + await route.handler(path, state); + } catch (error) { + console.error('Route handler error:', error); + + // Emit route error event + this._eventBus.emit('router:route-error', { + path, + error: error.message + }, this.name); + } + } + + _addToHistory(routeInfo) { + this._history.push(routeInfo); + + // Limit history size + if (this._history.length > this._maxHistorySize) { + this._history.shift(); + } + } + + _validatePath(path) { + if (!path || typeof path !== 'string') { + throw new Error('Path must be a non-empty string'); + } + } + + _validateHandler(handler) { + if (typeof handler !== 'function') { + throw new Error('Route handler must be a function'); + } + } +} + +// Freeze to prevent modification +Object.freeze(Router); +Object.freeze(Router.prototype); + +export default Router; \ No newline at end of file diff --git a/src/core/index.js b/src/core/index.js new file mode 100644 index 0000000..9472cc1 --- /dev/null +++ b/src/core/index.js @@ -0,0 +1,16 @@ +/** + * Core Module Exports + * Central export point for all core system modules + */ + +import Module from './Module.js'; +import EventBus from './EventBus.js'; +import ModuleLoader from './ModuleLoader.js'; +import Router from './Router.js'; + +export { + Module, + EventBus, + ModuleLoader, + Router +}; \ No newline at end of file diff --git a/src/games/adventure-reader.js b/src/games/adventure-reader.js new file mode 100644 index 0000000..6e81dca --- /dev/null +++ b/src/games/adventure-reader.js @@ -0,0 +1,1287 @@ +// === 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; + + // TTS settings + this.autoPlayTTS = true; + this.ttsEnabled = true; + + // Expose content globally for SettingsManager TTS language detection + window.currentGameContent = this.content; + + // Content extraction + this.vocabulary = this.extractVocabulary(this.content); + this.sentences = this.extractSentences(this.content); + this.stories = this.extractStories(this.content); + this.dialogues = this.extractDialogues(this.content); + + this.init(); + } + + init() { + const hasVocabulary = this.vocabulary && this.vocabulary.length > 0; + const hasSentences = this.sentences && this.sentences.length > 0; + const hasStories = this.stories && this.stories.length > 0; + const hasDialogues = this.dialogues && this.dialogues.length > 0; + + if (!hasVocabulary && !hasSentences && !hasStories && !hasDialogues) { + logSh('No compatible content found for Adventure Reader', 'ERROR'); + this.showInitError(); + return; + } + + logSh(`Adventure Reader initialized with: ${this.vocabulary.length} vocab, ${this.sentences.length} sentences, ${this.stories.length} stories, ${this.dialogues.length} dialogues`, 'INFO'); + + this.createGameInterface(); + this.initializePlayer(); + this.setupEventListeners(); + this.updateContentInfo(); + this.generateGameObjects(); + this.generateDecorations(); + this.startGameLoop(); + } + + showInitError() { + this.container.innerHTML = ` +
+

โŒ No Adventure Content Found

+

This content module needs adventure-compatible content:

+
    +
  • ๐Ÿ“š texts: Stories with original_language and user_language
  • +
  • ๐Ÿ’ฌ dialogues: Character conversations with speakers
  • +
  • ๐Ÿ“ vocabulary: Words with translations for discovery
  • +
  • ๐Ÿ“– sentences: Individual phrases for reading practice
  • +
+

Add adventure content to enable this game mode.

+ +
+ `; + } + + extractVocabulary(content) { + let vocabulary = []; + + // Support pour Dragon's Pearl vocabulary structure + if (content.vocabulary && typeof content.vocabulary === 'object') { + vocabulary = Object.entries(content.vocabulary).map(([original_language, vocabData]) => { + if (typeof vocabData === 'string') { + // Simple format: "word": "translation" + return { + original_language: original_language, + user_language: vocabData, + type: 'unknown' + }; + } else if (typeof vocabData === 'object') { + // Rich format: "word": { user_language: "translation", type: "noun", ... } + return { + original_language: original_language, + user_language: vocabData.user_language || vocabData.translation || 'No translation', + type: vocabData.type || 'unknown', + pronunciation: vocabData.pronunciation, + difficulty: vocabData.difficulty + }; + } + return null; + }).filter(item => item !== null); + } + // Ultra-modular format support + else if (content.rawContent && content.rawContent.vocabulary) { + if (typeof content.rawContent.vocabulary === 'object' && !Array.isArray(content.rawContent.vocabulary)) { + vocabulary = Object.entries(content.rawContent.vocabulary).map(([original_language, vocabData]) => { + if (typeof vocabData === 'string') { + // Simple format: "word": "translation" + return { + original_language: original_language, + user_language: vocabData, + type: 'unknown' + }; + } else if (typeof vocabData === 'object') { + // Rich format: "word": { user_language: "translation", type: "noun", ... } + return { + original_language: original_language, + user_language: vocabData.user_language || vocabData.translation || 'No translation', + type: vocabData.type || 'unknown', + pronunciation: vocabData.pronunciation, + difficulty: vocabData.difficulty + }; + } + return null; + }).filter(item => item !== null); + } + } + + return vocabulary.filter(item => item && item.original_language && item.user_language); + } + + extractSentences(content) { + let sentences = []; + + logSh('๐Ÿ‰ Adventure Reader: Extracting sentences from content...', 'DEBUG'); + logSh(`๐Ÿ‰ Content structure: story=${!!content.story}, rawContent=${!!content.rawContent}`, 'DEBUG'); + + // Support pour Dragon's Pearl structure: content.story.chapters[].sentences[] + if (content.story && content.story.chapters && Array.isArray(content.story.chapters)) { + logSh(`๐Ÿ‰ Dragon's Pearl structure detected, ${content.story.chapters.length} chapters`, 'DEBUG'); + + content.story.chapters.forEach((chapter, chapterIndex) => { + logSh(`๐Ÿ‰ Processing chapter ${chapterIndex}: ${chapter.title}`, 'DEBUG'); + + if (chapter.sentences && Array.isArray(chapter.sentences)) { + logSh(`๐Ÿ‰ Chapter ${chapterIndex} has ${chapter.sentences.length} sentences`, 'DEBUG'); + + chapter.sentences.forEach((sentence, sentenceIndex) => { + if (sentence.original && sentence.translation) { + // Construire la prononciation depuis les mots si pas disponible directement + let pronunciation = sentence.pronunciation || ''; + + if (!pronunciation && sentence.words && Array.isArray(sentence.words)) { + pronunciation = sentence.words + .map(wordObj => wordObj.pronunciation || '') + .filter(p => p.trim().length > 0) + .join(' '); + } + + sentences.push({ + original_language: sentence.original, + user_language: sentence.translation, + pronunciation: pronunciation, + chapter: chapter.title || '', + id: sentence.id || sentences.length + }); + } else { + logSh(`๐Ÿ‰ WARNING: Skipping sentence ${sentenceIndex} in chapter ${chapterIndex} - missing original/translation`, 'WARN'); + } + }); + } else { + logSh(`๐Ÿ‰ WARNING: Chapter ${chapterIndex} has no sentences array`, 'WARN'); + } + }); + + logSh(`๐Ÿ‰ Dragon's Pearl extraction complete: ${sentences.length} sentences extracted`, 'INFO'); + } + // Support pour la structure ultra-modulaire existante + else if (content.rawContent) { + // Ultra-modular format: Extract from texts (stories/adventures) + if (content.rawContent.texts && Array.isArray(content.rawContent.texts)) { + content.rawContent.texts.forEach(text => { + if (text.original_language && text.user_language) { + // Split long texts into sentences for adventure reading + const originalSentences = text.original_language.split(/[.!?]+/).filter(s => s.trim().length > 10); + const userSentences = text.user_language.split(/[.!?]+/).filter(s => s.trim().length > 10); + + // Match sentences by index + originalSentences.forEach((originalSentence, index) => { + const userSentence = userSentences[index] || originalSentence; + sentences.push({ + original_language: originalSentence.trim() + '.', + user_language: userSentence.trim() + '.', + title: text.title || 'Adventure Text', + id: text.id || `text_${index}` + }); + }); + } + }); + } + + // Ultra-modular format: Extract from dialogues + if (content.rawContent.dialogues && Array.isArray(content.rawContent.dialogues)) { + content.rawContent.dialogues.forEach(dialogue => { + if (dialogue.conversation && Array.isArray(dialogue.conversation)) { + dialogue.conversation.forEach(line => { + if (line.original_language && line.user_language) { + sentences.push({ + original_language: line.original_language, + user_language: line.user_language, + speaker: line.speaker || 'Character', + title: dialogue.title || 'Dialogue', + id: line.id || dialogue.id + }); + } + }); + } + }); + } + + // Legacy format support for backward compatibility + if (content.rawContent.sentences && Array.isArray(content.rawContent.sentences)) { + content.rawContent.sentences.forEach(sentence => { + sentences.push({ + original_language: sentence.english || sentence.original_language || '', + user_language: sentence.chinese || sentence.french || sentence.user_language || sentence.translation || '', + pronunciation: sentence.prononciation || sentence.pronunciation + }); + }); + } + } + + return sentences.filter(item => item && item.original_language && item.user_language); + } + + extractStories(content) { + let stories = []; + + // Support pour Dragon's Pearl structure + if (content.story && content.story.chapters && Array.isArray(content.story.chapters)) { + // Crรฉer une histoire depuis les chapitres de Dragon's Pearl + stories.push({ + title: content.story.title || content.name || "Dragon's Pearl", + original_language: content.story.chapters.map(ch => + ch.sentences.map(s => s.original).join(' ') + ).join('\n\n'), + user_language: content.story.chapters.map(ch => + ch.sentences.map(s => s.translation).join(' ') + ).join('\n\n'), + chapters: content.story.chapters.map(chapter => ({ + title: chapter.title, + sentences: chapter.sentences + })) + }); + } + // Support pour la structure ultra-modulaire existante + else if (content.rawContent && content.rawContent.texts && Array.isArray(content.rawContent.texts)) { + stories = content.rawContent.texts.filter(text => + text.original_language && text.user_language && text.title + ).map(text => ({ + id: text.id || `story_${Date.now()}_${Math.random()}`, + title: text.title, + original_language: text.original_language, + user_language: text.user_language, + description: text.description || '', + difficulty: text.difficulty || 'medium' + })); + } + + return stories; + } + + extractDialogues(content) { + let dialogues = []; + + if (content.rawContent && content.rawContent.dialogues && Array.isArray(content.rawContent.dialogues)) { + dialogues = content.rawContent.dialogues.filter(dialogue => + dialogue.conversation && Array.isArray(dialogue.conversation) && dialogue.conversation.length > 0 + ).map(dialogue => ({ + id: dialogue.id || `dialogue_${Date.now()}_${Math.random()}`, + title: dialogue.title || 'Character Dialogue', + conversation: dialogue.conversation.filter(line => + line.original_language && line.user_language + ).map(line => ({ + id: line.id || `line_${Date.now()}_${Math.random()}`, + speaker: line.speaker || 'Character', + original_language: line.original_language, + user_language: line.user_language, + emotion: line.emotion || 'neutral' + })) + })); + } + + return dialogues.filter(dialogue => dialogue.conversation.length > 0); + } + + updateContentInfo() { + const contentInfoEl = document.getElementById('content-info'); + if (!contentInfoEl) return; + + const contentTypes = []; + + if (this.stories && this.stories.length > 0) { + contentTypes.push(`๐Ÿ“š ${this.stories.length} stories`); + } + + if (this.dialogues && this.dialogues.length > 0) { + contentTypes.push(`๐Ÿ’ฌ ${this.dialogues.length} dialogues`); + } + + if (this.vocabulary && this.vocabulary.length > 0) { + contentTypes.push(`๐Ÿ“ ${this.vocabulary.length} words`); + } + + if (this.sentences && this.sentences.length > 0) { + contentTypes.push(`๐Ÿ“– ${this.sentences.length} sentences`); + } + + if (contentTypes.length > 0) { + contentInfoEl.innerHTML = ` +
+ Adventure Content: ${contentTypes.join(' โ€ข ')} +
+ `; + } + } + + createGameInterface() { + this.container.innerHTML = ` +
+ +
+
+
+ ๐Ÿ† + 0 +
+
+ ๐Ÿบ + 0 +
+
+ โš”๏ธ + 0 +
+
+
+
+ Start your adventure! +
+
+
+ + +
+ +
๐Ÿง™โ€โ™‚๏ธ
+ + +
+ + +
+
+ Click ๐Ÿบ pots for vocabulary โ€ข Click ๐Ÿ‘น enemies for sentences +
+ + +
+ + + + + + +
+ `; + } + + 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'); + const pronunciationEl = document.getElementById('vocab-pronunciation'); + + wordEl.textContent = vocab.original_language; + translationEl.textContent = vocab.user_language; + + // Afficher la prononciation si disponible + if (vocab.pronunciation) { + pronunciationEl.textContent = `๐Ÿ—ฃ๏ธ ${vocab.pronunciation}`; + pronunciationEl.style.display = 'block'; + } else { + pronunciationEl.style.display = 'none'; + } + + popup.style.display = 'block'; + popup.classList.add('show'); + + // Auto-play TTS for vocabulary + if (this.autoPlayTTS && this.ttsEnabled) { + setTimeout(() => { + this.speakText(vocab.original_language, { rate: 0.8 }); + }, 400); // Small delay to let popup appear + } + + 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'); + const modalTitle = document.getElementById('modal-title'); + + // Determine content type and set appropriate modal title + let modalTitleText = 'Adventure Text'; + if (sentence.speaker) { + modalTitleText = `๐Ÿ’ฌ ${sentence.speaker} says...`; + } else if (sentence.title) { + modalTitleText = `๐Ÿ“š ${sentence.title}`; + } + + modalTitle.textContent = modalTitleText; + + // Create content with appropriate styling based on type + const speakerInfo = sentence.speaker ? `
๐ŸŽญ ${sentence.speaker}
` : ''; + const titleInfo = sentence.title && !sentence.speaker ? `
๐Ÿ“– ${sentence.title}
` : ''; + const emotionInfo = sentence.emotion && sentence.emotion !== 'neutral' ? `
๐Ÿ˜Š ${sentence.emotion}
` : ''; + + content.innerHTML = ` +
+ ${titleInfo} + ${speakerInfo} + ${emotionInfo} +
+

${sentence.original_language}

+

${sentence.user_language}

+ ${sentence.pronunciation ? `

๐Ÿ—ฃ๏ธ ${sentence.pronunciation}

` : ''} +
+
+ `; + + modal.style.display = 'flex'; + modal.classList.add('show'); + + // Auto-play TTS for sentence + if (this.autoPlayTTS && this.ttsEnabled) { + setTimeout(() => { + this.speakText(sentence.original_language, { rate: 0.8 }); + }, 600); // Longer delay for modal animation + } + } + + 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() { + logSh('โš”๏ธ Adventure Reader: Starting', 'INFO'); + document.getElementById('progress-text').textContent = 'Click objects to begin your adventure!'; + } + + restart() { + logSh('๐Ÿ”„ Adventure Reader: Restarting', 'INFO'); + 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(); + } + + // TTS Methods + speakText(text, options = {}) { + if (!text || !this.ttsEnabled) return; + + // Use SettingsManager if available for better language support + if (window.SettingsManager && window.SettingsManager.speak) { + const ttsOptions = { + lang: this.getContentLanguage(), + rate: options.rate || 0.8, + ...options + }; + + window.SettingsManager.speak(text, ttsOptions) + .catch(error => { + console.warn('๐Ÿ”Š SettingsManager TTS failed:', error); + this.fallbackTTS(text, ttsOptions); + }); + } else { + this.fallbackTTS(text, options); + } + } + + fallbackTTS(text, options = {}) { + if ('speechSynthesis' in window && text) { + // Cancel any ongoing speech + speechSynthesis.cancel(); + + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = this.getContentLanguage(); + utterance.rate = options.rate || 0.8; + utterance.volume = 1.0; + + speechSynthesis.speak(utterance); + } + } + + getContentLanguage() { + // Get language from content or use sensible defaults + if (this.content.language) { + const langMap = { + 'chinese': 'zh-CN', + 'english': 'en-US', + 'french': 'fr-FR', + 'spanish': 'es-ES' + }; + return langMap[this.content.language] || this.content.language; + } + return 'en-US'; // Default fallback + } + + destroy() { + // Cancel any ongoing TTS + if ('speechSynthesis' in window) { + speechSynthesis.cancel(); + } + this.container.innerHTML = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.AdventureReader = AdventureReaderGame; \ No newline at end of file diff --git a/src/games/chinese-study.js b/src/games/chinese-study.js new file mode 100644 index 0000000..5f13f83 --- /dev/null +++ b/src/games/chinese-study.js @@ -0,0 +1,1585 @@ +// === CHINESE STUDY MODE === + +class ChineseStudyGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Game state + this.vocabulary = []; + this.currentMode = null; + this.currentIndex = 0; + this.score = 0; + this.correctAnswers = 0; + this.isRunning = false; + this.studyState = 'menu'; // 'menu', 'playing', 'review' + + // Extract vocabulary + this.vocabulary = this.extractVocabulary(this.content); + + this.init(); + } + + init() { + // Check if we have enough vocabulary + if (!this.vocabulary || this.vocabulary.length === 0) { + logSh('No Chinese vocabulary found for Chinese Study Game', 'ERROR'); + this.showInitError(); + return; + } + + this.createGameInterface(); + } + + showInitError() { + this.container.innerHTML = ` +
+

โŒ Error loading

+

This content doesn't have Chinese vocabulary for the Chinese Study Game.

+

The game needs vocabulary with Chinese characters, translations, and optional pinyin.

+ +
+ `; + this.addStyles(); + } + + extractVocabulary(content) { + let vocabulary = []; + + logSh('๐Ÿ” Extracting Chinese vocabulary from:', content?.name || 'content', 'INFO'); + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + logSh('๐Ÿ“ฆ Using raw module content', 'INFO'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + logSh('โœจ Ultra-modular format detected (vocabulary object)', 'INFO'); + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + chinese: word, // Clรฉ = caractรจre chinois + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + pronunciation: data.pronunciation || '', // Pinyin + type: data.type || 'general', + hskLevel: data.hskLevel || null, + examples: data.examples || [], + strokeOrder: data.strokeOrder || [] + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + chinese: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + pronunciation: '', + type: 'general', + hskLevel: null + }; + } + return null; + }).filter(Boolean); + } + // No other formats supported - ultra-modular only + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + logSh('๐Ÿ”ง Extracting from raw content:', rawContent.name || 'Module', 'INFO'); + let vocabulary = []; + + // Extract from vocabulary object in raw content + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object') { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + if (typeof data === 'object' && data.user_language) { + return { + chinese: word, + translation: data.user_language.split('๏ผ›')[0], + fullTranslation: data.user_language, + pronunciation: data.pronunciation || '', + type: data.type || 'general', + hskLevel: data.hskLevel || null, + examples: data.examples || [], + strokeOrder: data.strokeOrder || [] + }; + } else if (typeof data === 'string') { + return { + chinese: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + pronunciation: '', + type: 'general', + hskLevel: null + }; + } + return null; + }).filter(Boolean); + } + + return vocabulary; + } + + finalizeVocabulary(vocabulary) { + if (vocabulary.length === 0) { + logSh('โš ๏ธ No valid vocabulary found', 'WARNING'); + return []; + } + + // Shuffle vocabulary + vocabulary = vocabulary.sort(() => Math.random() - 0.5); + + logSh(`โœ… Vocabulary extraction complete: ${vocabulary.length} items`, 'INFO'); + return vocabulary; + } + + createGameInterface() { + if (this.studyState === 'menu') { + this.createModeSelection(); + } else if (this.studyState === 'playing') { + this.createStudyMode(); + } + this.addStyles(); + } + + createModeSelection() { + const hasPinyin = this.vocabulary.some(item => item.pronunciation); + const hasHSK = this.vocabulary.some(item => item.hskLevel); + + this.container.innerHTML = ` +
+
+

๐Ÿ‡จ๐Ÿ‡ณ Chinese Study Mode

+
+
Score: ${this.score}
+
${this.vocabulary.length} characters available
+ ${hasHSK ? '
๐Ÿ“Š HSK levels included
' : ''} + ${hasPinyin ? '
๐Ÿ—ฃ๏ธ Pinyin available
' : ''} +
+
+ +
+
+
๐Ÿ“š
+

Flashcards

+

Study characters with flip cards

+ +
+ +
+
๐Ÿง 
+

Character Recognition

+

Match characters to their meanings

+ ${!hasPinyin ? '
Requires pinyin data
' : ''} + +
+ +
+
๐Ÿ—ฃ๏ธ
+

Pinyin Practice

+

Learn pronunciation with pinyin

+ ${!hasPinyin ? '
Requires pinyin data
' : ''} + +
+ +
+
๐Ÿ“Š
+

HSK Review

+

Study by HSK difficulty levels

+ ${!hasHSK ? '
Requires HSK level data
' : ''} + +
+
+ +
+

๐Ÿ“– Vocabulary Preview

+
+ ${this.vocabulary.slice(0, 6).map(item => ` +
+ ${item.chinese} + ${item.translation} + ${item.pronunciation ? `${item.pronunciation}` : ''} + ${item.hskLevel ? `${item.hskLevel}` : ''} +
+ `).join('')} + ${this.vocabulary.length > 6 ? `
... and ${this.vocabulary.length - 6} more
` : ''} +
+
+ +
+ +
+
+ `; + + this.setupModeListeners(); + } + + createStudyMode() { + const currentItem = this.vocabulary[this.currentIndex]; + const progress = Math.round(((this.currentIndex + 1) / this.vocabulary.length) * 100); + + let modeContent = ''; + switch (this.currentMode) { + case 'flashcards': + modeContent = this.createFlashcardMode(currentItem); + break; + case 'recognition': + modeContent = this.createRecognitionMode(currentItem); + break; + case 'pinyin': + modeContent = this.createPinyinMode(currentItem); + break; + case 'hsk': + modeContent = this.createHSKMode(currentItem); + break; + } + + this.container.innerHTML = ` +
+
+
+

${this.getModeTitle()}

+ +
+
+
+
+
+
${this.currentIndex + 1} / ${this.vocabulary.length}
+
Score: ${this.score}
+
+
+ +
+ ${modeContent} +
+ +
+ + +
+
+ `; + + this.setupStudyListeners(); + } + + setupModeListeners() { + const modeCards = this.container.querySelectorAll('.mode-card:not([data-disabled])'); + modeCards.forEach(card => { + card.addEventListener('click', (e) => { + const mode = card.dataset.mode; + this.startMode(mode); + }); + }); + } + + setupStudyListeners() { + // Bind this context to methods for onclick handlers + window.chineseStudyInstance = this; + + // Override global onclick handlers + this.container.querySelector('.back-to-menu-btn').onclick = () => this.backToMenu(); + this.container.querySelector('.prev-btn').onclick = () => this.previousItem(); + this.container.querySelector('.next-btn').onclick = () => this.nextItem(); + + // Setup mode-specific listeners + this.setupModeSpecificListeners(); + } + + setupModeSpecificListeners() { + if (this.currentMode === 'flashcards') { + const flashcard = this.container.querySelector('.flashcard'); + if (flashcard) { + flashcard.addEventListener('click', () => this.flipCard()); + } + } else if (this.currentMode === 'recognition') { + const options = this.container.querySelectorAll('.option-btn'); + options.forEach(option => { + option.addEventListener('click', (e) => this.selectOption(e.target.dataset.translation)); + }); + } else if (this.currentMode === 'pinyin') { + const pinyinInput = this.container.querySelector('.pinyin-input'); + if (pinyinInput) { + pinyinInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') this.checkPinyinAnswer(); + }); + } + const checkBtn = this.container.querySelector('.check-pinyin-btn'); + if (checkBtn) { + checkBtn.onclick = () => this.checkPinyinAnswer(); + } + } + } + + startMode(mode) { + this.currentMode = mode; + this.studyState = 'playing'; + this.currentIndex = 0; + this.correctAnswers = 0; + this.createGameInterface(); + } + + backToMenu() { + this.studyState = 'menu'; + this.currentMode = null; + this.createGameInterface(); + } + + getModeTitle() { + const titles = { + flashcards: '๐Ÿ“š Flashcards', + recognition: '๐Ÿง  Character Recognition', + pinyin: '๐Ÿ—ฃ๏ธ Pinyin Practice', + hsk: '๐Ÿ“Š HSK Review' + }; + return titles[this.currentMode] || 'Chinese Study'; + } + + createFlashcardMode(item) { + return ` +
+
+
+
${item.chinese}
+
Click to reveal translation
+
+
+
${item.translation}
+ ${item.pronunciation ? `
${item.pronunciation}
` : ''} + ${item.type ? `
${item.type}
` : ''} + ${item.hskLevel ? `
${item.hskLevel}
` : ''} +
+
+
+ + +
+
+ `; + } + + createRecognitionMode(item) { + // Create wrong options + const wrongOptions = this.vocabulary + .filter(v => v.chinese !== item.chinese) + .sort(() => Math.random() - 0.5) + .slice(0, 3) + .map(v => v.translation); + + const allOptions = [...wrongOptions, item.translation].sort(() => Math.random() - 0.5); + + return ` +
+
+
${item.chinese}
+ ${item.pronunciation ? `
${item.pronunciation}
` : ''} +
What does this character mean?
+
+
+ ${allOptions.map(option => ` + + `).join('')} +
+ +
+ `; + } + + createPinyinMode(item) { + return ` +
+
+
${item.chinese}
+
${item.translation}
+
+
+ + + +
+ + ${item.pronunciation ? `` : ''} +
+ `; + } + + createHSKMode(item) { + const hskInfo = item.hskLevel || 'No HSK level'; + return ` +
+
+
${hskInfo}
+
Chinese Character Study
+
+
+
${item.chinese}
+
+
${item.translation}
+ ${item.pronunciation ? `
${item.pronunciation}
` : ''} + ${item.type ? `
Type: ${item.type}
` : ''} + ${item.examples && item.examples.length > 0 ? ` +
+
Examples:
+ ${item.examples.slice(0, 2).map(ex => `
${ex}
`).join('')} +
+ ` : ''} +
+
+
+ + + +
+
+ `; + } + + // Navigation methods + nextItem() { + if (this.currentIndex < this.vocabulary.length - 1) { + this.currentIndex++; + this.createStudyMode(); + } + } + + previousItem() { + if (this.currentIndex > 0) { + this.currentIndex--; + this.createStudyMode(); + } + } + + // Flashcard methods + flipCard() { + const flashcard = this.container.querySelector('.flashcard'); + const isFlipped = flashcard.dataset.flipped === 'true'; + flashcard.dataset.flipped = (!isFlipped).toString(); + } + + markAsKnown(known) { + const points = known ? 10 : 5; + this.score += points; + this.correctAnswers += known ? 1 : 0; + this.onScoreUpdate(this.score); + this.updateScoreDisplay(); + + // Auto-advance after a short delay + setTimeout(() => { + if (this.currentIndex < this.vocabulary.length - 1) { + this.nextItem(); + } else { + this.endStudySession(); + } + }, 1000); + } + + // Recognition mode methods + selectOption(selectedTranslation) { + const currentItem = this.vocabulary[this.currentIndex]; + const isCorrect = selectedTranslation === currentItem.translation; + const feedback = this.container.querySelector('.result-feedback'); + + if (isCorrect) { + this.score += 15; + this.correctAnswers++; + feedback.innerHTML = 'โœ… Correct! Well done!'; + feedback.className = 'result-feedback correct'; + } else { + this.score = Math.max(0, this.score - 5); + feedback.innerHTML = `โŒ Incorrect. The correct answer is: ${currentItem.translation}`; + feedback.className = 'result-feedback incorrect'; + } + + feedback.style.display = 'block'; + this.onScoreUpdate(this.score); + this.updateScoreDisplay(); + + // Disable all option buttons + const options = this.container.querySelectorAll('.option-btn'); + options.forEach(btn => btn.disabled = true); + + // Auto-advance after a delay + setTimeout(() => { + if (this.currentIndex < this.vocabulary.length - 1) { + this.nextItem(); + } else { + this.endStudySession(); + } + }, 2000); + } + + // Pinyin mode methods + checkPinyinAnswer() { + const input = this.container.querySelector('.pinyin-input'); + const userAnswer = input.value.trim().toLowerCase(); + const currentItem = this.vocabulary[this.currentIndex]; + const correctPinyin = currentItem.pronunciation ? currentItem.pronunciation.toLowerCase() : ''; + + const feedback = this.container.querySelector('.pinyin-feedback'); + const correctAnswer = this.container.querySelector('.correct-answer'); + + if (correctPinyin && this.normalizePinyin(userAnswer) === this.normalizePinyin(correctPinyin)) { + this.score += 20; + this.correctAnswers++; + feedback.innerHTML = '๐ŸŽ‰ Excellent pronunciation!'; + feedback.className = 'pinyin-feedback correct'; + } else { + this.score = Math.max(0, this.score - 3); + feedback.innerHTML = '๐Ÿค” Not quite right. Try again or see the correct answer below.'; + feedback.className = 'pinyin-feedback incorrect'; + if (correctAnswer) correctAnswer.style.display = 'block'; + } + + feedback.style.display = 'block'; + input.disabled = true; + this.container.querySelector('.check-pinyin-btn').disabled = true; + this.onScoreUpdate(this.score); + this.updateScoreDisplay(); + + // Auto-advance after a delay + setTimeout(() => { + if (this.currentIndex < this.vocabulary.length - 1) { + this.nextItem(); + } else { + this.endStudySession(); + } + }, 3000); + } + + normalizePinyin(pinyin) { + // Remove tone marks and accents for easier comparison + return pinyin.replace(/[ฤรกวŽร ฤ“รฉฤ›รจฤซรญวรฌลรณว’รฒลซรบว”รนรผว–ว˜วšวœ]/g, (match) => { + const toneMap = { + 'ฤ': 'a', 'รก': 'a', 'วŽ': 'a', 'ร ': 'a', + 'ฤ“': 'e', 'รฉ': 'e', 'ฤ›': 'e', 'รจ': 'e', + 'ฤซ': 'i', 'รญ': 'i', 'ว': 'i', 'รฌ': 'i', + 'ล': 'o', 'รณ': 'o', 'ว’': 'o', 'รฒ': 'o', + 'ลซ': 'u', 'รบ': 'u', 'ว”': 'u', 'รน': 'u', + 'รผ': 'u', 'ว–': 'u', 'ว˜': 'u', 'วš': 'u', 'วœ': 'u' + }; + return toneMap[match] || match; + }).replace(/\s+/g, ''); + } + + // HSK mode methods + markDifficulty(difficulty) { + const points = { + 'easy': 5, + 'medium': 8, + 'hard': 12 + }; + + this.score += points[difficulty]; + this.correctAnswers++; + this.onScoreUpdate(this.score); + this.updateScoreDisplay(); + + // Visual feedback + const buttons = this.container.querySelectorAll('.difficulty-btn'); + buttons.forEach(btn => btn.disabled = true); + + const selectedBtn = this.container.querySelector(`.difficulty-btn.${difficulty}`); + selectedBtn.style.backgroundColor = '#10b981'; + selectedBtn.style.color = 'white'; + + // Auto-advance after a delay + setTimeout(() => { + if (this.currentIndex < this.vocabulary.length - 1) { + this.nextItem(); + } else { + this.endStudySession(); + } + }, 1500); + } + + updateScoreDisplay() { + const scoreElement = this.container.querySelector('#score'); + if (scoreElement) { + scoreElement.textContent = this.score; + } + } + + endStudySession() { + const accuracy = Math.round((this.correctAnswers / this.vocabulary.length) * 100); + + this.container.innerHTML = ` +
+
+

๐ŸŽ“ Study Session Complete!

+
+
+
${this.score}
+
Final Score
+
+
+
${this.correctAnswers}/${this.vocabulary.length}
+
Correct
+
+
+
${accuracy}%
+
Accuracy
+
+
+
+ ${accuracy >= 80 ? '๐ŸŒŸ Excellent work! You\'ve mastered these characters!' : + accuracy >= 60 ? '๐Ÿ‘ Good job! Keep practicing to improve further.' : + '๐Ÿ’ช Nice effort! More practice will help you improve.'} +
+
+ + + +
+
+
+ `; + + this.addStyles(); + + // Trigger game end callback + this.onGameEnd({ + score: this.score, + accuracy: accuracy, + mode: this.currentMode, + totalItems: this.vocabulary.length, + correctAnswers: this.correctAnswers + }); + } + + addStyles() { + const style = document.createElement('style'); + style.textContent = ` + .chinese-study-container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; + font-family: 'Arial', sans-serif; + } + + .game-header { + text-align: center; + margin-bottom: 30px; + border-bottom: 2px solid #e5e7eb; + padding-bottom: 20px; + } + + .game-header h2 { + color: #dc2626; + font-size: 2.2em; + margin-bottom: 15px; + } + + .game-stats { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; + margin-top: 10px; + } + + .score-display, .vocab-count, .hsk-indicator, .pinyin-indicator { + font-size: 1em; + padding: 5px 12px; + border-radius: 20px; + font-weight: bold; + } + + .score-display { + background: #10b981; + color: white; + } + + .vocab-count { + background: #3b82f6; + color: white; + } + + .hsk-indicator, .pinyin-indicator { + background: #f59e0b; + color: white; + font-size: 0.9em; + } + + .study-modes { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; + } + + .mode-card { + background: linear-gradient(135deg, #fff 0%, #f8fafc 100%); + border: 2px solid #e5e7eb; + border-radius: 16px; + padding: 24px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + position: relative; + } + + .mode-card:not([data-disabled]):hover { + border-color: #dc2626; + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(220, 38, 38, 0.15); + } + + .mode-card[data-disabled="true"] { + opacity: 0.6; + cursor: not-allowed; + background: #f9fafb; + } + + .mode-requirement { + background: #fee2e2; + color: #dc2626; + padding: 4px 8px; + border-radius: 12px; + font-size: 0.8em; + margin-bottom: 10px; + } + + .mode-icon { + font-size: 3em; + margin-bottom: 12px; + } + + .mode-card h3 { + color: #374151; + margin-bottom: 8px; + font-size: 1.3em; + } + + .mode-card p { + color: #6b7280; + margin-bottom: 16px; + line-height: 1.5; + } + + .mode-btn { + background: #dc2626; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s ease; + } + + .mode-btn:hover:not(:disabled) { + background: #b91c1c; + } + + .mode-btn:disabled { + background: #9ca3af; + cursor: not-allowed; + } + + .vocabulary-preview { + background: #f8fafc; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 20px; + margin-bottom: 30px; + } + + .vocabulary-preview h4 { + color: #374151; + margin-bottom: 15px; + text-align: center; + } + + .preview-items { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; + } + + .preview-item { + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 10px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 4px; + } + + .preview-item .chinese { + font-size: 1.4em; + font-weight: bold; + color: #dc2626; + } + + .preview-item .translation { + color: #374151; + font-size: 0.9em; + } + + .preview-item .pinyin { + color: #6b7280; + font-size: 0.8em; + font-style: italic; + } + + .preview-item .hsk-badge { + background: #f59e0b; + color: white; + padding: 2px 6px; + border-radius: 10px; + font-size: 0.7em; + font-weight: bold; + } + + .more-items { + grid-column: 1/-1; + text-align: center; + color: #6b7280; + font-style: italic; + padding: 10px; + } + + /* Study Mode Styles */ + .study-mode-active { + max-width: 800px; + } + + .study-header { + background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); + color: white; + border-radius: 12px; + padding: 20px; + margin-bottom: 30px; + } + + .mode-title { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + } + + .mode-title h2 { + margin: 0; + font-size: 1.8em; + } + + .back-to-menu-btn { + background: rgba(255,255,255,0.2); + color: white; + border: 1px solid rgba(255,255,255,0.3); + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + transition: background 0.3s ease; + } + + .back-to-menu-btn:hover { + background: rgba(255,255,255,0.3); + } + + .progress-section { + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap; + } + + .progress-bar { + flex: 1; + min-width: 200px; + height: 8px; + background: rgba(255,255,255,0.3); + border-radius: 4px; + overflow: hidden; + } + + .progress-fill { + height: 100%; + background: white; + transition: width 0.3s ease; + } + + .progress-text { + font-weight: bold; + min-width: 60px; + } + + .study-content { + background: white; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 30px; + margin-bottom: 20px; + min-height: 300px; + display: flex; + align-items: center; + justify-content: center; + } + + /* Flashcard Styles */ + .flashcard-container { + width: 100%; + max-width: 400px; + text-align: center; + } + + .flashcard { + width: 100%; + height: 250px; + position: relative; + transform-style: preserve-3d; + transition: transform 0.6s; + cursor: pointer; + margin-bottom: 20px; + } + + .flashcard[data-flipped="true"] { + transform: rotateY(180deg); + } + + .flashcard-front, .flashcard-back { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + border: 2px solid #e5e7eb; + border-radius: 12px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 20px; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + } + + .flashcard-back { + transform: rotateY(180deg); + background: #f8fafc; + } + + .chinese-character { + font-size: 4em; + font-weight: bold; + color: #dc2626; + margin-bottom: 10px; + } + + .card-hint { + color: #6b7280; + font-style: italic; + } + + .translation { + font-size: 1.5em; + color: #374151; + margin-bottom: 10px; + } + + .pronunciation { + color: #6b7280; + font-style: italic; + margin-bottom: 8px; + } + + .word-type, .hsk-level { + background: #e5e7eb; + color: #374151; + padding: 4px 8px; + border-radius: 12px; + font-size: 0.8em; + margin-bottom: 4px; + } + + .hsk-level { + background: #f59e0b; + color: white; + } + + .flashcard-actions { + display: flex; + gap: 15px; + justify-content: center; + } + + .know-btn, .dont-know-btn { + padding: 12px 24px; + border: none; + border-radius: 8px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + } + + .know-btn { + background: #10b981; + color: white; + } + + .know-btn:hover { + background: #059669; + } + + .dont-know-btn { + background: #f59e0b; + color: white; + } + + .dont-know-btn:hover { + background: #d97706; + } + + /* Recognition Mode Styles */ + .recognition-container { + width: 100%; + text-align: center; + } + + .question-section { + margin-bottom: 30px; + } + + .pronunciation-hint { + color: #6b7280; + font-style: italic; + margin-bottom: 15px; + } + + .question-text { + font-size: 1.2em; + color: #374151; + margin-bottom: 20px; + } + + .options-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + margin-bottom: 20px; + } + + .option-btn { + background: #f8fafc; + border: 2px solid #e5e7eb; + border-radius: 8px; + padding: 15px; + cursor: pointer; + font-size: 1em; + transition: all 0.3s ease; + } + + .option-btn:hover:not(:disabled) { + border-color: #dc2626; + background: #fef2f2; + } + + .option-btn:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + .result-feedback { + padding: 15px; + border-radius: 8px; + font-weight: bold; + margin-bottom: 15px; + } + + .result-feedback.correct { + background: #d1fae5; + color: #065f46; + border: 1px solid #10b981; + } + + .result-feedback.incorrect { + background: #fee2e2; + color: #991b1b; + border: 1px solid #ef4444; + } + + /* Pinyin Mode Styles */ + .pinyin-container { + width: 100%; + text-align: center; + } + + .character-section { + margin-bottom: 30px; + } + + .translation-hint { + font-size: 1.2em; + color: #6b7280; + margin-top: 10px; + } + + .pinyin-exercise { + margin-bottom: 20px; + } + + .pinyin-label { + display: block; + font-weight: bold; + color: #374151; + margin-bottom: 10px; + } + + .pinyin-input { + padding: 12px; + border: 2px solid #e5e7eb; + border-radius: 8px; + font-size: 1.1em; + width: 250px; + max-width: 100%; + margin-bottom: 15px; + margin-right: 10px; + } + + .pinyin-input:focus { + outline: none; + border-color: #dc2626; + } + + .check-pinyin-btn { + background: #dc2626; + color: white; + border: none; + padding: 12px 20px; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + } + + .check-pinyin-btn:hover:not(:disabled) { + background: #b91c1c; + } + + .check-pinyin-btn:disabled { + background: #9ca3af; + cursor: not-allowed; + } + + .pinyin-feedback { + padding: 15px; + border-radius: 8px; + font-weight: bold; + margin-bottom: 15px; + } + + .pinyin-feedback.correct { + background: #d1fae5; + color: #065f46; + border: 1px solid #10b981; + } + + .pinyin-feedback.incorrect { + background: #fee2e2; + color: #991b1b; + border: 1px solid #ef4444; + } + + .correct-answer { + background: #f0f9ff; + color: #0c4a6e; + border: 1px solid #3b82f6; + padding: 10px; + border-radius: 8px; + font-weight: bold; + } + + /* HSK Mode Styles */ + .hsk-container { + width: 100%; + text-align: center; + } + + .hsk-header { + margin-bottom: 30px; + } + + .hsk-level-badge { + background: #f59e0b; + color: white; + padding: 8px 16px; + border-radius: 20px; + font-weight: bold; + display: inline-block; + margin-bottom: 10px; + } + + .character-difficulty { + color: #6b7280; + font-size: 1em; + } + + .character-study { + margin-bottom: 30px; + } + + .character-details { + background: #f8fafc; + border-radius: 8px; + padding: 20px; + margin-top: 15px; + } + + .character-details .translation { + font-size: 1.3em; + margin-bottom: 10px; + } + + .character-details .word-type { + background: #e5e7eb; + color: #374151; + padding: 4px 12px; + border-radius: 12px; + font-size: 0.9em; + display: inline-block; + margin-bottom: 15px; + } + + .examples { + text-align: left; + margin-top: 15px; + } + + .examples h5 { + color: #374151; + margin-bottom: 8px; + text-align: center; + } + + .example { + background: white; + border: 1px solid #e5e7eb; + border-radius: 6px; + padding: 8px; + margin-bottom: 5px; + color: #6b7280; + } + + .hsk-actions { + display: flex; + gap: 15px; + justify-content: center; + flex-wrap: wrap; + } + + .difficulty-btn { + padding: 12px 20px; + border: 2px solid #e5e7eb; + border-radius: 8px; + background: white; + cursor: pointer; + font-weight: bold; + transition: all 0.3s ease; + min-width: 100px; + } + + .difficulty-btn.easy { + border-color: #10b981; + color: #10b981; + } + + .difficulty-btn.easy:hover:not(:disabled) { + background: #10b981; + color: white; + } + + .difficulty-btn.medium { + border-color: #f59e0b; + color: #f59e0b; + } + + .difficulty-btn.medium:hover:not(:disabled) { + background: #f59e0b; + color: white; + } + + .difficulty-btn.hard { + border-color: #ef4444; + color: #ef4444; + } + + .difficulty-btn.hard:hover:not(:disabled) { + background: #ef4444; + color: white; + } + + .difficulty-btn:disabled { + cursor: not-allowed; + opacity: 0.7; + } + + /* Study Controls */ + .study-controls { + display: flex; + justify-content: space-between; + gap: 15px; + } + + .prev-btn, .next-btn { + background: #6b7280; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s ease; + flex: 1; + max-width: 150px; + } + + .prev-btn:hover:not(:disabled), .next-btn:hover:not(:disabled) { + background: #4b5563; + } + + .prev-btn:disabled, .next-btn:disabled { + background: #9ca3af; + cursor: not-allowed; + } + + /* Study Complete Styles */ + .study-complete { + text-align: center; + padding: 40px 20px; + } + + .study-complete h2 { + color: #dc2626; + font-size: 2.5em; + margin-bottom: 30px; + } + + .final-stats { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 30px; + flex-wrap: wrap; + } + + .stat-item { + background: #f8fafc; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 20px; + min-width: 120px; + } + + .stat-value { + font-size: 2em; + font-weight: bold; + color: #dc2626; + margin-bottom: 5px; + } + + .stat-label { + color: #6b7280; + font-size: 0.9em; + } + + .completion-message { + background: #f0f9ff; + border: 1px solid #3b82f6; + border-radius: 12px; + padding: 20px; + color: #1e40af; + font-size: 1.1em; + font-weight: bold; + margin-bottom: 30px; + } + + .final-actions { + display: flex; + gap: 15px; + justify-content: center; + flex-wrap: wrap; + } + + .restart-btn, .menu-btn, .back-btn { + padding: 12px 24px; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: bold; + transition: background 0.3s ease; + font-size: 1em; + } + + .restart-btn { + background: #10b981; + color: white; + } + + .restart-btn:hover { + background: #059669; + } + + .menu-btn { + background: #3b82f6; + color: white; + } + + .menu-btn:hover { + background: #2563eb; + } + + .back-btn { + background: #6b7280; + color: white; + } + + .back-btn:hover { + background: #4b5563; + } + + .game-controls { + text-align: center; + } + + .game-error { + text-align: center; + padding: 40px 20px; + background: #fee2e2; + border: 1px solid #ef4444; + border-radius: 12px; + } + + .game-error h3 { + color: #991b1b; + margin-bottom: 15px; + } + + .game-error p { + color: #7f1d1d; + margin-bottom: 10px; + } + + @media (max-width: 768px) { + .chinese-study-container { + padding: 15px; + } + + .game-header h2 { + font-size: 1.8em; + } + + .study-modes { + grid-template-columns: 1fr; + } + + .preview-items { + grid-template-columns: 1fr; + } + + .game-stats { + flex-direction: column; + align-items: center; + gap: 10px; + } + + .chinese-character { + font-size: 3em; + } + + .flashcard { + height: 200px; + } + + .options-grid { + grid-template-columns: 1fr; + } + + .final-stats { + flex-direction: column; + align-items: center; + gap: 15px; + } + + .final-actions { + flex-direction: column; + align-items: center; + } + + .hsk-actions { + flex-direction: column; + align-items: center; + } + + .study-controls { + flex-direction: column; + } + + .prev-btn, .next-btn { + max-width: none; + } + + .mode-title { + flex-direction: column; + gap: 10px; + text-align: center; + } + + .progress-section { + flex-direction: column; + gap: 10px; + } + } + `; + document.head.appendChild(style); + } + + start() { + this.isRunning = true; + logSh('Chinese Study Mode initialized with ultra-modular format', 'INFO'); + } + + destroy() { + this.isRunning = false; + // Clean up global references + if (window.chineseStudyInstance === this) { + delete window.chineseStudyInstance; + } + logSh('Chinese Study Mode destroyed', 'INFO'); + } + + restart() { + this.score = 0; + this.correctAnswers = 0; + this.currentIndex = 0; + this.studyState = 'menu'; + this.currentMode = null; + this.onScoreUpdate(this.score); + + // Re-shuffle vocabulary + this.vocabulary = this.vocabulary.sort(() => Math.random() - 0.5); + + this.createGameInterface(); + logSh('Chinese Study Mode restarted', 'INFO'); + } +} + +// Export to global scope +window.GameModules = window.GameModules || {}; +window.GameModules.ChineseStudy = ChineseStudyGame; \ No newline at end of file diff --git a/src/games/fill-the-blank.js b/src/games/fill-the-blank.js new file mode 100644 index 0000000..5628c1a --- /dev/null +++ b/src/games/fill-the-blank.js @@ -0,0 +1,569 @@ +// === MODULE FILL THE BLANK === + +class FillTheBlankGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Game state + this.score = 0; + this.errors = 0; + this.currentSentenceIndex = 0; + this.isRunning = false; + + // Game data + this.vocabulary = this.extractVocabulary(this.content); + this.sentences = this.extractRealSentences(); + this.currentSentence = null; + this.blanks = []; + this.userAnswers = []; + + this.init(); + } + + init() { + // Check that we have vocabulary + if (!this.vocabulary || this.vocabulary.length === 0) { + logSh('No vocabulary available for Fill the Blank', 'ERROR'); + this.showInitError(); + return; + } + + this.createGameBoard(); + this.setupEventListeners(); + // The game will start when start() is called + } + + showInitError() { + this.container.innerHTML = ` +
+

โŒ Loading Error

+

This content does not contain vocabulary compatible with Fill the Blank.

+

The game requires words with their translations in ultra-modular format.

+ +
+ `; + } + + extractVocabulary(content) { + let vocabulary = []; + + logSh('๐Ÿ” Extracting vocabulary from:', content?.name || 'content', 'INFO'); + + // Priority 1: Use raw module content (ultra-modular format) + if (content.rawContent) { + logSh('๐Ÿ“ฆ Using raw module content', 'INFO'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + logSh('โœจ Ultra-modular format detected (vocabulary object)', 'INFO'); + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + // No other formats supported - ultra-modular only + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + logSh('๐Ÿ”ง Extracting from raw content:', rawContent.name || 'Module', 'INFO'); + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + logSh(`โœจ ${vocabulary.length} words extracted from ultra-modular vocabulary`, 'INFO'); + } + // No other formats supported - ultra-modular only + else { + logSh('โš ๏ธ Content format not supported - ultra-modular format required', 'WARN'); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + if (vocabulary.length === 0) { + logSh('โŒ No valid vocabulary found', 'ERROR'); + // Demo vocabulary as last resort + vocabulary = [ + { original: 'hello', translation: 'bonjour', category: 'greetings' }, + { original: 'goodbye', translation: 'au revoir', category: 'greetings' }, + { original: 'thank you', translation: 'merci', category: 'greetings' }, + { original: 'cat', translation: 'chat', category: 'animals' }, + { original: 'dog', translation: 'chien', category: 'animals' }, + { original: 'house', translation: 'maison', category: 'objects' }, + { original: 'school', translation: 'รฉcole', category: 'places' }, + { original: 'book', translation: 'livre', category: 'objects' } + ]; + logSh('๐Ÿšจ Using demo vocabulary', 'WARN'); + } + + logSh(`โœ… Fill the Blank: ${vocabulary.length} words finalized`, 'INFO'); + return vocabulary; + } + + extractRealSentences() { + let sentences = []; + + logSh('๐Ÿ” Extracting real sentences from content...', 'INFO'); + + // Priority 1: Extract from story chapters + if (this.content.story?.chapters) { + this.content.story.chapters.forEach(chapter => { + if (chapter.sentences) { + chapter.sentences.forEach(sentence => { + if (sentence.original && sentence.translation) { + sentences.push({ + original: sentence.original, + translation: sentence.translation, + source: 'story' + }); + } + }); + } + }); + } + + // Priority 2: Extract from rawContent story + if (this.content.rawContent?.story?.chapters) { + this.content.rawContent.story.chapters.forEach(chapter => { + if (chapter.sentences) { + chapter.sentences.forEach(sentence => { + if (sentence.original && sentence.translation) { + sentences.push({ + original: sentence.original, + translation: sentence.translation, + source: 'rawContent.story' + }); + } + }); + } + }); + } + + // Priority 3: Extract from sentences array + const directSentences = this.content.sentences || this.content.rawContent?.sentences; + if (directSentences && Array.isArray(directSentences)) { + directSentences.forEach(sentence => { + if (sentence.english && sentence.chinese) { + sentences.push({ + original: sentence.english, + translation: sentence.chinese, + source: 'sentences' + }); + } else if (sentence.original && sentence.translation) { + sentences.push({ + original: sentence.original, + translation: sentence.translation, + source: 'sentences' + }); + } + }); + } + + // Filter sentences that are suitable for fill-the-blank (min 3 words) + sentences = sentences.filter(sentence => + sentence.original && + sentence.original.split(' ').length >= 3 && + sentence.original.trim().length > 0 + ); + + // Shuffle and limit + sentences = this.shuffleArray(sentences); + + logSh(`๐Ÿ“ Extracted ${sentences.length} real sentences for fill-the-blank`, 'INFO'); + + if (sentences.length === 0) { + logSh('โŒ No suitable sentences found for fill-the-blank', 'ERROR'); + return this.createFallbackSentences(); + } + + return sentences.slice(0, 20); // Limit to 20 sentences max + } + + createFallbackSentences() { + // Simple fallback using vocabulary words in basic sentences + const fallback = []; + this.vocabulary.slice(0, 10).forEach(vocab => { + fallback.push({ + original: `This is a ${vocab.original}.`, + translation: `่ฟ™ๆ˜ฏไธ€ไธช ${vocab.translation}ใ€‚`, + source: 'fallback' + }); + }); + return fallback; + } + + createGameBoard() { + this.container.innerHTML = ` +
+ +
+
+
+ ${this.currentSentenceIndex + 1} + / ${this.sentences.length} +
+
+ ${this.errors} + Errors +
+
+ ${this.score} + Score +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ + + +
+ `; + } + + 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() { + logSh('๐ŸŽฎ Fill the Blank: Starting game', 'INFO'); + this.loadNextSentence(); + } + + restart() { + logSh('๐Ÿ”„ Fill the Blank: Restarting game', 'INFO'); + 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() { + // If we've finished all sentences, restart from the beginning + if (this.currentSentenceIndex >= this.sentences.length) { + this.currentSentenceIndex = 0; + this.sentences = this.shuffleArray(this.sentences); // Shuffle again + this.showFeedback(`๐ŸŽ‰ All sentences completed! Starting over with a new order.`, '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.original.split(' '); + this.blanks = []; + + // Create 1-2 blanks randomly (readable sentences) + const numBlanks = Math.random() < 0.5 ? 1 : 2; + const blankIndices = new Set(); + + // PRIORITY 1: Words from vocabulary (educational value) + const vocabularyWords = []; + const otherWords = []; + + words.forEach((word, index) => { + const cleanWord = word.replace(/[.,!?;:"'()[\]{}\-โ€“โ€”]/g, '').toLowerCase(); + const isVocabularyWord = this.vocabulary.some(vocab => + vocab.original.toLowerCase() === cleanWord + ); + + if (isVocabularyWord) { + vocabularyWords.push({ word, index, priority: 'vocabulary' }); + } else { + otherWords.push({ word, index, priority: 'other', length: cleanWord.length }); + } + }); + + // Select blanks: vocabulary first, then longest words + const selectedWords = []; + + // Take vocabulary words first (shuffled) + const shuffledVocab = this.shuffleArray(vocabularyWords); + for (let i = 0; i < Math.min(numBlanks, shuffledVocab.length); i++) { + selectedWords.push(shuffledVocab[i]); + } + + // If need more blanks, take longest other words + if (selectedWords.length < numBlanks) { + const sortedOthers = otherWords.sort((a, b) => b.length - a.length); + const needed = numBlanks - selectedWords.length; + for (let i = 0; i < Math.min(needed, sortedOthers.length); i++) { + selectedWords.push(sortedOthers[i]); + } + } + + // Add selected indices to blanks + selectedWords.forEach(item => blankIndices.add(item.index)); + + // Create blank structure + words.forEach((word, index) => { + if (blankIndices.has(index)) { + this.blanks.push({ + index: index, + word: word.replace(/[.,!?;:]$/, ''), // Remove punctuation + punctuation: word.match(/[.,!?;:]$/) ? word.match(/[.,!?;:]$/)[0] : '', + userAnswer: '' + }); + } + }); + } + + displaySentence() { + const words = this.currentSentence.original.split(' '); + let sentenceHTML = ''; + let blankCounter = 0; + + words.forEach((word, index) => { + const blank = this.blanks.find(b => b.index === index); + if (blank) { + sentenceHTML += ` + + ${blank.punctuation} + `; + blankCounter++; + } else { + sentenceHTML += `${word} `; + } + }); + + document.getElementById('sentence-container').innerHTML = sentenceHTML; + + // Display translation if available + const translation = this.currentSentence.translation || ''; + document.getElementById('translation-hint').innerHTML = translation ? + `๐Ÿ’ญ ${translation}` : ''; + + // Focus on first input + const firstInput = document.getElementById('blank-0'); + if (firstInput) { + setTimeout(() => firstInput.focus(), 100); + } + } + + checkAnswer() { + if (!this.isRunning) return; + + let allCorrect = true; + let correctCount = 0; + + // Check each 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) { + // All answers are correct + this.score += 10 * this.blanks.length; + this.showFeedback(`๐ŸŽ‰ Perfect! +${10 * this.blanks.length} points`, 'success'); + setTimeout(() => { + this.currentSentenceIndex++; + this.loadNextSentence(); + }, 1500); + } else { + // Some errors + this.errors++; + if (correctCount > 0) { + this.score += 5 * correctCount; + this.showFeedback(`โœจ ${correctCount}/${this.blanks.length} correct! +${5 * correctCount} points. Try again.`, 'partial'); + } else { + this.showFeedback(`โŒ Try again! (${this.errors} errors)`, 'error'); + } + } + + this.updateUI(); + this.onScoreUpdate(this.score); + } + + showHint() { + // Show first letter of each empty blank + this.blanks.forEach((blank, index) => { + const input = document.getElementById(`blank-${index}`); + if (!input.value.trim()) { + input.value = blank.word[0]; + input.focus(); + } + }); + + this.showFeedback('๐Ÿ’ก First letter added!', 'info'); + } + + skipSentence() { + // Reveal correct answers + this.blanks.forEach((blank, index) => { + const input = document.getElementById(`blank-${index}`); + input.value = blank.word; + input.classList.add('revealed'); + }); + + this.showFeedback('๐Ÿ“– Answers revealed! Next sentence...', 'info'); + setTimeout(() => { + this.currentSentenceIndex++; + this.loadNextSentence(); + }, 2000); + } + + // endGame method removed - game continues indefinitely + + showFeedback(message, type = 'info') { + const feedbackArea = document.getElementById('feedback-area'); + feedbackArea.innerHTML = `
${message}
`; + } + + 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 = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.FillTheBlank = FillTheBlankGame; \ No newline at end of file diff --git a/src/games/grammar-discovery.js b/src/games/grammar-discovery.js new file mode 100644 index 0000000..b1f83d5 --- /dev/null +++ b/src/games/grammar-discovery.js @@ -0,0 +1,1185 @@ +// === GRAMMAR DISCOVERY GAME === +// Interactive game for discovering and learning grammar patterns + +class GrammarDiscovery { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.container = container; + this.content = content; + this.onScoreUpdate = onScoreUpdate; + this.onGameEnd = onGameEnd; + + this.score = 0; + + // ROTATION SYSTEM FOR FOCUSED CONCEPT LEARNING + this.rotationSteps = [ + 'explanation-basic', // 1. Explication de base (langue originale) + 'examples-simple', // 2. Exemples simples + 'exercise-basic', // 3. Exercices de base + 'explanation-detailed', // 4. Explication dรฉtaillรฉe + 'examples-complex', // 5. Exemples complexes + 'exercise-intermediate', // 6. Exercices intermรฉdiaires + 'summary', // 7. Rรฉsumรฉ du concept + 'exercise-global' // 8. Exercice global final + ]; + + this.currentStep = 0; + this.grammarConcept = null; // Single focused concept (user selected) + this.conceptData = {}; + this.stepProgress = {}; + this.availableConcepts = []; // All available grammar concepts + this.conceptSelected = false; // Whether user has chosen a concept + + this.injectCSS(); + this.extractAvailableConcepts(); + this.init(); + } + + injectCSS() { + if (document.getElementById('grammar-discovery-styles')) return; + + const styleSheet = document.createElement('style'); + styleSheet.id = 'grammar-discovery-styles'; + styleSheet.textContent = ` + .grammar-discovery { + display: flex; + flex-direction: column; + height: 100%; + font-family: 'Arial', sans-serif; + } + + .grammar-hud { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 15px; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 10px 10px 0 0; + } + + .grammar-phase { + font-size: 18px; + font-weight: bold; + display: flex; + align-items: center; + gap: 10px; + } + + .phase-icon { + font-size: 24px; + } + + .grammar-content { + flex: 1; + display: flex; + flex-direction: column; + padding: 20px; + background: linear-gradient(145deg, #f8f9ff, #e6e9ff); + overflow-y: auto; + } + + .rule-card { + background: white; + border-radius: 15px; + padding: 25px; + margin-bottom: 20px; + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1); + border: 2px solid transparent; + transition: all 0.3s ease; + } + + .rule-card.active { + border-color: #667eea; + transform: translateY(-2px); + box-shadow: 0 12px 40px rgba(102, 126, 234, 0.2); + } + + .rule-title { + font-size: 24px; + font-weight: bold; + color: #4c51bf; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 10px; + } + + .rule-explanation { + font-size: 16px; + line-height: 1.6; + color: #4a5568; + margin-bottom: 20px; + background: #f7fafc; + padding: 15px; + border-radius: 8px; + border-left: 4px solid #667eea; + } + + .examples-section { + margin-top: 20px; + } + + .example-item { + background: #ffffff; + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: 20px; + margin-bottom: 15px; + transition: all 0.3s ease; + cursor: pointer; + } + + .example-item:hover { + border-color: #667eea; + transform: translateX(5px); + } + + .example-item.revealed { + border-color: #48bb78; + background: linear-gradient(135deg, #f0fff4, #c6f6d5); + } + + .chinese-text { + font-size: 22px; + font-weight: bold; + color: #2d3748; + margin-bottom: 8px; + } + + .english-text { + font-size: 18px; + color: #4a5568; + margin-bottom: 8px; + } + + .pronunciation { + font-size: 16px; + color: #718096; + font-style: italic; + margin-bottom: 10px; + } + + .explanation-text { + font-size: 14px; + color: #667eea; + background: #edf2f7; + padding: 10px; + border-radius: 6px; + display: none; + } + + .example-item.revealed .explanation-text { + display: block; + animation: fadeIn 0.5s ease; + } + + .discovery-controls { + display: flex; + gap: 15px; + margin-top: 20px; + justify-content: center; + } + + .discover-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + padding: 12px 25px; + border-radius: 25px; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); + } + + .discover-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); + } + + .discover-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + } + + .practice-question { + background: white; + border-radius: 15px; + padding: 25px; + margin-bottom: 20px; + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1); + } + + .question-text { + font-size: 20px; + color: #2d3748; + margin-bottom: 20px; + line-height: 1.6; + } + + .options-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 20px; + } + + .option-btn { + background: white; + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: 15px; + font-size: 16px; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + } + + .option-btn:hover { + border-color: #667eea; + background: #f7fafc; + } + + .option-btn.correct { + border-color: #48bb78; + background: linear-gradient(135deg, #f0fff4, #c6f6d5); + color: #22543d; + } + + .option-btn.incorrect { + border-color: #f56565; + background: linear-gradient(135deg, #fed7d7, #feb2b2); + color: #742a2a; + } + + .feedback { + background: #f7fafc; + border-radius: 10px; + padding: 15px; + margin-top: 15px; + border-left: 4px solid #667eea; + display: none; + } + + .feedback.show { + display: block; + animation: slideIn 0.3s ease; + } + + .progress-bar { + background: #e2e8f0; + height: 6px; + border-radius: 3px; + margin: 10px 0; + overflow: hidden; + } + + .progress-fill { + background: linear-gradient(90deg, #667eea, #764ba2); + height: 100%; + border-radius: 3px; + transition: width 0.5s ease; + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } + } + + @keyframes slideIn { + from { opacity: 0; transform: translateX(-20px); } + to { opacity: 1; transform: translateX(0); } + } + + .concept-selector { + background: white; + border-radius: 15px; + padding: 30px; + margin: 20px 0; + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1); + text-align: center; + } + + .selector-title { + font-size: 24px; + font-weight: bold; + color: #4c51bf; + margin-bottom: 20px; + } + + .selector-description { + font-size: 16px; + color: #4a5568; + margin-bottom: 25px; + line-height: 1.6; + } + + .concept-dropdown { + width: 100%; + max-width: 400px; + padding: 15px; + font-size: 16px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: white; + margin-bottom: 20px; + cursor: pointer; + transition: all 0.3s ease; + } + + .concept-dropdown:hover { + border-color: #667eea; + } + + .concept-dropdown:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + } + + .concept-preview { + background: #f7fafc; + border-radius: 10px; + padding: 20px; + margin: 20px 0; + text-align: left; + display: none; + } + + .concept-preview.show { + display: block; + animation: fadeIn 0.3s ease; + } + + .preview-title { + font-size: 18px; + font-weight: bold; + color: #2d3748; + margin-bottom: 10px; + } + + .preview-explanation { + font-size: 14px; + color: #4a5568; + margin-bottom: 15px; + } + + .preview-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 10px; + font-size: 12px; + color: #718096; + } + + .stat-item { + background: white; + padding: 8px; + border-radius: 6px; + text-align: center; + } + + .phase-complete { + text-align: center; + padding: 40px; + background: linear-gradient(135deg, #f0fff4, #c6f6d5); + border-radius: 15px; + margin: 20px 0; + } + + .complete-icon { + font-size: 48px; + margin-bottom: 20px; + } + + .complete-title { + font-size: 24px; + font-weight: bold; + color: #22543d; + margin-bottom: 15px; + } + + .tts-controls { + display: flex; + gap: 10px; + align-items: center; + margin-top: 10px; + } + + .tts-btn { + background: #667eea; + color: white; + border: none; + border-radius: 20px; + padding: 8px 15px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s ease; + } + + .tts-btn:hover { + background: #5a67d8; + transform: scale(1.05); + } + `; + document.head.appendChild(styleSheet); + } + + extractAvailableConcepts() { + if (!this.content || !this.content.grammar) { + console.error('No grammar content found for Grammar Discovery game'); + return; + } + + // GET ALL AVAILABLE CONCEPTS for selection + this.availableConcepts = Object.entries(this.content.grammar).map(([key, conceptData]) => ({ + id: key, + title: conceptData.title, + explanation: conceptData.explanation, + data: conceptData + })); + + console.log(`๐ŸŽฏ Found ${this.availableConcepts.length} grammar concepts available for selection`); + } + + selectConcept(conceptId) { + const selectedConcept = this.availableConcepts.find(c => c.id === conceptId); + if (!selectedConcept) { + console.error('Concept not found:', conceptId); + return; + } + + this.grammarConcept = conceptId; + this.conceptData = selectedConcept.data; + this.conceptSelected = true; + + console.log(`๐ŸŽฏ Selected concept: ${this.grammarConcept}`); + + // ORGANIZE CONTENT BY DIFFICULTY LEVELS + this.organizeConceptContent(); + + // Start the rotation cycle + this.startRotationCycle(); + } + + organizeConceptContent() { + const concept = this.conceptData; + + // BASIC EXAMPLES (first 2-3 simple ones) + this.simpleExamples = []; + this.complexExamples = []; + + if (concept.examples) { + this.simpleExamples = concept.examples.slice(0, 3); + this.complexExamples = concept.examples.slice(3); + } + + // Get examples from detailed explanation sections + if (concept.detailedExplanation) { + Object.values(concept.detailedExplanation).forEach(section => { + if (section.examples) { + // First 2 go to simple, rest to complex + const sectionSimple = section.examples.slice(0, 2); + const sectionComplex = section.examples.slice(2); + + this.simpleExamples.push(...sectionSimple); + this.complexExamples.push(...sectionComplex); + } + }); + } + + // ORGANIZE EXERCISES BY DIFFICULTY + this.basicExercises = []; + this.intermediateExercises = []; + this.globalExercises = []; + + if (this.content.fillInBlanks) { + this.content.fillInBlanks.forEach(exercise => { + // Accept all fillInBlanks for focused grammar lesson content + const isGrammarLesson = this.content.type === 'grammar_course'; + const isRelevant = isGrammarLesson || + exercise.grammarFocus === this.grammarConcept || + exercise.grammarFocus === 'completion' || + exercise.grammarFocus === 'change-of-state'; + + if (isRelevant) { + // Determine difficulty by sentence complexity + if (exercise.sentence.length < 15) { + this.basicExercises.push(exercise); + } else { + this.intermediateExercises.push(exercise); + } + } + }); + } + + if (this.content.corrections) { + this.content.corrections.forEach(correction => { + // Accept all corrections for focused grammar lesson content + const isGrammarLesson = this.content.type === 'grammar_course'; + const isRelevant = isGrammarLesson || + correction.grammarFocus === this.grammarConcept; + + if (isRelevant) { + this.intermediateExercises.push({ + type: 'correction', + question: 'Which sentence is correct?', + options: [correction.correct, correction.incorrect], + correctAnswer: correction.correct, + explanation: correction.explanation + }); + } + }); + } + + // Global exercises combine multiple concepts + this.globalExercises = [...this.basicExercises, ...this.intermediateExercises]; + this.globalExercises = this.shuffleArray(this.globalExercises).slice(0, 5); + + console.log(`๐Ÿ“Š Content organized: + - Simple examples: ${this.simpleExamples.length} + - Complex examples: ${this.complexExamples.length} + - Basic exercises: ${this.basicExercises.length} + - Intermediate exercises: ${this.intermediateExercises.length} + - Global exercises: ${this.globalExercises.length}`); + } + + init() { + this.container.innerHTML = ` +
+
+
+ ๐ŸŽฏ + Select Grammar Concept +
+ +
Score: 0
+
+
+ +
+
+ `; + + this.showConceptSelector(); + } + + showConceptSelector() { + if (this.availableConcepts.length === 0) { + console.error('No concepts available for selection'); + return; + } + + const contentDiv = document.getElementById('grammar-content'); + + contentDiv.innerHTML = ` +
+
+ ๐ŸŽฏ Choose Grammar Concept +
+
+ Select which grammar concept you want to focus on for intensive study with our 8-step rotation system. +
+ + + +
+ +
+ + +
+ `; + + // Store reference for global access + window.currentGrammarGame = this; + } + + previewConcept(conceptId) { + const previewDiv = document.getElementById('concept-preview'); + const startBtn = document.getElementById('start-btn'); + + if (!conceptId) { + previewDiv.classList.remove('show'); + startBtn.disabled = true; + return; + } + + const concept = this.availableConcepts.find(c => c.id === conceptId); + if (!concept) return; + + // Calculate stats for this concept + this.grammarConcept = conceptId; + this.conceptData = concept.data; + this.organizeConceptContent(); + + previewDiv.innerHTML = ` +
${concept.title}
+
${concept.explanation}
+
+
+ ${this.simpleExamples.length}
+ Simple Examples +
+
+ ${this.complexExamples.length}
+ Complex Examples +
+
+ ${this.basicExercises.length}
+ Basic Exercises +
+
+ ${this.intermediateExercises.length}
+ Intermediate Exercises +
+
+ ${this.globalExercises.length}
+ Final Test Questions +
+
+ `; + + previewDiv.classList.add('show'); + startBtn.disabled = false; + } + + startSelectedConcept() { + const dropdown = document.getElementById('concept-dropdown'); + const selectedConceptId = dropdown.value; + + if (!selectedConceptId) { + alert('Please select a grammar concept first!'); + return; + } + + // Update UI to show rotation progress + document.getElementById('step-progress').style.display = 'block'; + document.getElementById('phase-icon').textContent = '๐Ÿ“š'; + document.getElementById('phase-text').textContent = 'Basic Explanation'; + + this.selectConcept(selectedConceptId); + } + + startRotationCycle() { + this.currentStep = 0; + this.showCurrentStep(); + } + + showCurrentStep() { + const stepType = this.rotationSteps[this.currentStep]; + const stepNumber = this.currentStep + 1; + + document.getElementById('current-step').textContent = stepNumber; + + // Update phase icon and text based on step + const phaseInfo = this.getPhaseInfo(stepType); + document.getElementById('phase-icon').textContent = phaseInfo.icon; + document.getElementById('phase-text').textContent = phaseInfo.text; + + // Store reference for global access + window.currentGrammarGame = this; + + // Show content for current step + switch (stepType) { + case 'explanation-basic': + this.showBasicExplanation(); + break; + case 'examples-simple': + this.showSimpleExamples(); + break; + case 'exercise-basic': + this.showBasicExercises(); + break; + case 'explanation-detailed': + this.showDetailedExplanation(); + break; + case 'examples-complex': + this.showComplexExamples(); + break; + case 'exercise-intermediate': + this.showIntermediateExercises(); + break; + case 'summary': + this.showSummary(); + break; + case 'exercise-global': + this.showGlobalExercises(); + break; + } + } + + getPhaseInfo(stepType) { + const phaseMap = { + 'explanation-basic': { icon: '๐Ÿ“š', text: 'Basic Explanation' }, + 'examples-simple': { icon: '๐Ÿ’ก', text: 'Simple Examples' }, + 'exercise-basic': { icon: 'โœ๏ธ', text: 'Basic Practice' }, + 'explanation-detailed': { icon: '๐Ÿ”', text: 'Detailed Explanation' }, + 'examples-complex': { icon: '๐Ÿงฉ', text: 'Complex Examples' }, + 'exercise-intermediate': { icon: '๐Ÿ’ช', text: 'Intermediate Practice' }, + 'summary': { icon: '๐Ÿ“', text: 'Summary' }, + 'exercise-global': { icon: '๐Ÿ†', text: 'Final Test' } + }; + return phaseMap[stepType] || { icon: 'โ“', text: 'Unknown' }; + } + + nextStep() { + this.currentStep++; + if (this.currentStep >= this.rotationSteps.length) { + this.completeRotation(); + } else { + this.showCurrentStep(); + } + } + + completeRotation() { + const contentDiv = document.getElementById('grammar-content'); + contentDiv.innerHTML = ` +
+
๐ŸŽ‰
+
Grammar Concept Mastered!
+

You've completed the full rotation for: ${this.conceptData.title}

+

Final Score: ${this.score}

+ +
+ `; + + this.onGameEnd(this.score); + } + + // === ROTATION STEP IMPLEMENTATIONS === + + showBasicExplanation() { + const concept = this.conceptData; + const contentDiv = document.getElementById('grammar-content'); + + contentDiv.innerHTML = ` +
+
+ ๐Ÿ“š ${concept.title} +
+
+ ${concept.explanation} +
+ + ${concept.mainRules ? ` +
+

๐ŸŽฏ Key Rules:

+
    + ${concept.mainRules.slice(0, 3).map(rule => `
  • ${rule}
  • `).join('')} +
+
+ ` : ''} + +
+ +
+
+ `; + } + + showSimpleExamples() { + const contentDiv = document.getElementById('grammar-content'); + + contentDiv.innerHTML = ` +
+
+ ๐Ÿ’ก Simple Examples +
+
+ ${this.simpleExamples.map((example, index) => ` +
+
${example.chinese}
+
${example.english}
+
${example.pronunciation || ''}
+
${example.explanation || example.breakdown || ''}
+
+ + +
+
+ `).join('')} +
+ +
+ +
+
+ `; + + // Auto-play first example + if (this.simpleExamples.length > 0) { + setTimeout(() => { + this.speakChinese(this.simpleExamples[0].chinese); + }, 1000); + } + } + + showBasicExercises() { + if (this.basicExercises.length === 0) { + this.nextStep(); + return; + } + + this.currentExerciseSet = this.basicExercises; + this.currentExerciseIndex = 0; + this.showExercise('basic'); + } + + showDetailedExplanation() { + const concept = this.conceptData; + const contentDiv = document.getElementById('grammar-content'); + + let detailsHtml = ''; + if (concept.detailedExplanation) { + detailsHtml = Object.entries(concept.detailedExplanation).map(([key, section]) => ` +
+

๐Ÿ” ${section.title}

+

${section.explanation}

+ ${section.pattern ? `
Pattern: ${section.pattern}
` : ''} +
+ `).join(''); + } + + contentDiv.innerHTML = ` +
+
+ ๐Ÿ” Detailed Explanation +
+ + ${detailsHtml} + + ${concept.commonMistakes ? ` +
+

โš ๏ธ Common Mistakes:

+ ${concept.commonMistakes.map(mistake => ` +
+
โŒ ${mistake.wrong}
+
โœ… ${mistake.correct}
+
${mistake.explanation}
+
+ `).join('')} +
+ ` : ''} + +
+ +
+
+ `; + } + + showComplexExamples() { + const contentDiv = document.getElementById('grammar-content'); + + contentDiv.innerHTML = ` +
+
+ ๐Ÿงฉ Complex Examples +
+
+ ${this.complexExamples.map((example, index) => ` +
+
${example.chinese}
+
${example.english}
+
${example.pronunciation || ''}
+
${example.explanation || example.breakdown || ''}
+
+ + +
+
+ `).join('')} +
+ +
+ +
+
+ `; + } + + showIntermediateExercises() { + if (this.intermediateExercises.length === 0) { + this.nextStep(); + return; + } + + this.currentExerciseSet = this.intermediateExercises; + this.currentExerciseIndex = 0; + this.showExercise('intermediate'); + } + + showSummary() { + const concept = this.conceptData; + const contentDiv = document.getElementById('grammar-content'); + + contentDiv.innerHTML = ` +
+
+ ๐Ÿ“ Summary: ${concept.title} +
+ +
+

๐ŸŽฏ What You've Learned:

+
+
+ Basic Concept:
+ ${concept.explanation} +
+ ${concept.mainRules ? ` +
+ Key Rules:
+ ${concept.mainRules.map(rule => `โ€ข ${rule}`).join('
')} +
+ ` : ''} + ${concept.practicePoints ? ` +
+ Practice Points:
+ ${concept.practicePoints.map(point => `โ€ข ${point}`).join('
')} +
+ ` : ''} +
+
+ +
+ +
+
+ `; + } + + showGlobalExercises() { + if (this.globalExercises.length === 0) { + this.completeRotation(); + return; + } + + this.currentExerciseSet = this.globalExercises; + this.currentExerciseIndex = 0; + this.showExercise('global'); + } + + // === UNIFIED EXERCISE SYSTEM === + + showExercise(type) { + const exercise = this.currentExerciseSet[this.currentExerciseIndex]; + if (!exercise) { + this.nextStep(); + return; + } + + const contentDiv = document.getElementById('grammar-content'); + const typeInfo = { + 'basic': { title: 'โœ๏ธ Basic Practice', color: '#48bb78' }, + 'intermediate': { title: '๐Ÿ’ช Intermediate Practice', color: '#3182ce' }, + 'global': { title: '๐Ÿ† Final Test', color: '#805ad5' } + }; + + const info = typeInfo[type] || typeInfo['basic']; + + if (exercise.type === 'correction') { + contentDiv.innerHTML = ` +
+
+ ${info.title} +
+
+ ${exercise.question} +
+
+ ${exercise.options.map(option => ` + + `).join('')} +
+ +
+ Exercise ${this.currentExerciseIndex + 1} of ${this.currentExerciseSet.length} +
+
+ `; + } else { + // Fill in the blank + contentDiv.innerHTML = ` +
+
+ ${info.title} +
+
+ Fill in the blank: ${exercise.sentence} +
+
+ ${exercise.options.map(option => ` + + `).join('')} +
+ +
+ Exercise ${this.currentExerciseIndex + 1} of ${this.currentExerciseSet.length} +
+
+ `; + } + } + + selectAnswer(selected, correct, exerciseType) { + const buttons = document.querySelectorAll('.option-btn'); + const feedback = document.getElementById('feedback'); + + buttons.forEach(btn => { + btn.disabled = true; + if (btn.textContent.trim() === correct) { + btn.classList.add('correct'); + } else if (btn.textContent.trim() === selected && selected !== correct) { + btn.classList.add('incorrect'); + } + }); + + // Scoring based on exercise type + if (selected === correct) { + const points = exerciseType === 'global' ? 30 : (exerciseType === 'intermediate' ? 20 : 15); + this.score += points; + } else { + this.score = Math.max(0, this.score - 5); + } + + this.onScoreUpdate(this.score); + document.getElementById('score-value').textContent = this.score; + + feedback.classList.add('show'); + + setTimeout(() => { + this.currentExerciseIndex++; + if (this.currentExerciseIndex >= this.currentExerciseSet.length) { + // Finished this exercise set + this.nextStep(); + } else { + // Show next exercise in set + this.showExercise(exerciseType); + } + }, 2500); + } + + restart() { + this.score = 0; + this.currentStep = 0; + this.currentExerciseIndex = 0; + this.conceptSelected = false; + this.grammarConcept = null; + this.conceptData = {}; + + // Reset UI to concept selector + document.getElementById('score-value').textContent = '0'; + document.getElementById('phase-icon').textContent = '๐ŸŽฏ'; + document.getElementById('phase-text').textContent = 'Select Grammar Concept'; + document.getElementById('step-progress').style.display = 'none'; + + this.onScoreUpdate(0); + this.showConceptSelector(); + } + + // TTS Functions + speakChinese(text) { + if (window.SettingsManager && window.SettingsManager.speak) { + window.SettingsManager.speak(text, { + lang: 'zh-CN', + rate: 0.8 + }); + } + } + + speakEnglish(text) { + if (window.SettingsManager && window.SettingsManager.speak) { + window.SettingsManager.speak(text, { + lang: 'en-US', + rate: 0.9 + }); + } + } + + // Utility Functions + 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; + } + + restart() { + this.score = 0; + this.currentPhase = 'discovery'; + this.currentRule = 0; + this.currentExampleIndex = 0; + this.currentPracticeIndex = 0; + this.practiceQuestions = this.shuffleArray(this.practiceQuestions); + + document.getElementById('phase-text').innerHTML = ` + ๐Ÿ” Discovery Phase + `; + document.getElementById('score-value').textContent = '0'; + + this.onScoreUpdate(0); + this.startDiscovery(); + } + + start() { + // Game starts automatically in constructor + } + + destroy() { + // Cleanup + const styleSheet = document.getElementById('grammar-discovery-styles'); + if (styleSheet) { + styleSheet.remove(); + } + + if (window.currentGrammarGame === this) { + delete window.currentGrammarGame; + } + } +} + +// Export to global +window.GameModules = window.GameModules || {}; +window.GameModules.GrammarDiscovery = GrammarDiscovery; \ No newline at end of file diff --git a/src/games/letter-discovery.js b/src/games/letter-discovery.js new file mode 100644 index 0000000..6147d52 --- /dev/null +++ b/src/games/letter-discovery.js @@ -0,0 +1,781 @@ +// === LETTER DISCOVERY GAME === +// Discover letters first, then explore words that start with each letter + +class LetterDiscovery { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.container = container; + this.content = content; + this.onScoreUpdate = onScoreUpdate; + this.onGameEnd = onGameEnd; + + // Game state + this.currentPhase = 'letter-discovery'; // letter-discovery, word-exploration, practice + this.currentLetterIndex = 0; + this.discoveredLetters = []; + this.currentLetter = null; + this.currentWordIndex = 0; + this.discoveredWords = []; + this.score = 0; + this.lives = 3; + + // Content processing + this.letters = []; + this.letterWords = {}; // Map letter -> words starting with that letter + + // Practice system + this.practiceLevel = 1; + this.practiceRound = 0; + this.maxPracticeRounds = 8; + this.practiceCorrectAnswers = 0; + this.practiceErrors = 0; + this.currentPracticeItems = []; + + this.injectCSS(); + this.extractContent(); + this.init(); + } + + injectCSS() { + if (document.getElementById('letter-discovery-styles')) return; + + const styleSheet = document.createElement('style'); + styleSheet.id = 'letter-discovery-styles'; + styleSheet.textContent = ` + .letter-discovery-wrapper { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; + position: relative; + overflow-y: auto; + } + + .letter-discovery-hud { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(255,255,255,0.1); + padding: 15px 20px; + border-radius: 15px; + backdrop-filter: blur(10px); + margin-bottom: 20px; + flex-wrap: wrap; + gap: 10px; + } + + .hud-group { + display: flex; + align-items: center; + gap: 15px; + } + + .hud-item { + color: white; + font-weight: bold; + font-size: 1.1em; + } + + .phase-indicator { + background: rgba(255,255,255,0.2); + padding: 8px 16px; + border-radius: 20px; + font-size: 0.9em; + color: white; + backdrop-filter: blur(5px); + } + + .letter-discovery-main { + background: rgba(255,255,255,0.1); + border-radius: 20px; + padding: 30px; + backdrop-filter: blur(10px); + min-height: 70vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .game-content { + width: 100%; + max-width: 900px; + text-align: center; + } + + /* Letter Display Styles */ + .letter-card { + background: rgba(255,255,255,0.95); + border-radius: 25px; + padding: 60px 40px; + margin: 30px auto; + max-width: 400px; + box-shadow: 0 20px 40px rgba(0,0,0,0.1); + transform: scale(0.8); + animation: letterAppear 0.8s ease-out forwards; + } + + @keyframes letterAppear { + to { transform: scale(1); } + } + + .letter-display { + font-size: 8em; + font-weight: bold; + color: #667eea; + margin-bottom: 20px; + text-shadow: 0 4px 8px rgba(0,0,0,0.1); + font-family: 'Arial Black', Arial, sans-serif; + } + + .letter-info { + font-size: 1.5em; + color: #333; + margin-bottom: 15px; + } + + .letter-pronunciation { + font-size: 1.2em; + color: #666; + font-style: italic; + margin-bottom: 25px; + } + + .letter-controls { + display: flex; + gap: 15px; + justify-content: center; + margin-top: 30px; + } + + /* Word Exploration Styles */ + .word-exploration-header { + background: rgba(255,255,255,0.1); + padding: 20px; + border-radius: 15px; + margin-bottom: 30px; + backdrop-filter: blur(5px); + } + + .exploring-letter { + font-size: 3em; + color: white; + margin-bottom: 10px; + font-weight: bold; + } + + .word-progress { + color: rgba(255,255,255,0.8); + font-size: 1.1em; + } + + .word-card { + background: rgba(255,255,255,0.95); + border-radius: 20px; + padding: 40px 30px; + margin: 25px auto; + max-width: 500px; + box-shadow: 0 15px 30px rgba(0,0,0,0.1); + transform: translateY(20px); + animation: wordSlideIn 0.6s ease-out forwards; + } + + @keyframes wordSlideIn { + to { transform: translateY(0); } + } + + .word-text { + font-size: 2.5em; + color: #667eea; + margin-bottom: 15px; + font-weight: bold; + } + + .word-translation { + font-size: 1.3em; + color: #333; + margin-bottom: 10px; + } + + .word-pronunciation { + font-size: 1.1em; + color: #666; + font-style: italic; + margin-bottom: 10px; + } + + .word-type { + font-size: 0.9em; + color: #667eea; + background: rgba(102, 126, 234, 0.1); + padding: 4px 12px; + border-radius: 15px; + display: inline-block; + margin-bottom: 15px; + font-weight: 500; + } + + .word-example { + font-size: 1em; + color: #555; + font-style: italic; + padding: 10px 15px; + background: rgba(0, 0, 0, 0.05); + border-left: 3px solid #667eea; + border-radius: 0 8px 8px 0; + margin-bottom: 15px; + } + + /* Practice Challenge Styles */ + .practice-challenge { + text-align: center; + margin-bottom: 30px; + } + + .challenge-text { + font-size: 1.8em; + color: white; + margin-bottom: 25px; + text-shadow: 0 2px 4px rgba(0,0,0,0.3); + } + + .practice-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + max-width: 800px; + margin: 0 auto; + } + + .practice-option { + background: rgba(255,255,255,0.9); + border: none; + border-radius: 15px; + padding: 20px; + font-size: 1.2em; + cursor: pointer; + transition: all 0.3s ease; + color: #333; + font-weight: 500; + } + + .practice-option:hover { + background: rgba(255,255,255,1); + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(0,0,0,0.2); + } + + .practice-option.correct { + background: #4CAF50; + color: white; + animation: correctPulse 0.6s ease; + } + + .practice-option.incorrect { + background: #F44336; + color: white; + animation: incorrectShake 0.6s ease; + } + + @keyframes correctPulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } + } + + @keyframes incorrectShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5px); } + 75% { transform: translateX(5px); } + } + + .practice-stats { + display: flex; + justify-content: space-around; + margin-top: 20px; + color: white; + font-size: 1.1em; + } + + .stat-item { + text-align: center; + padding: 10px; + background: rgba(255,255,255,0.1); + border-radius: 10px; + backdrop-filter: blur(5px); + } + + /* Control Buttons */ + .discovery-btn { + background: linear-gradient(45deg, #667eea, #764ba2); + color: white; + border: none; + padding: 15px 30px; + border-radius: 25px; + font-size: 1.1em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + margin: 0 10px; + } + + .discovery-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(0,0,0,0.3); + } + + .discovery-btn:active { + transform: translateY(0); + } + + .audio-btn { + background: none; + border: none; + font-size: 2em; + cursor: pointer; + color: #667eea; + margin-left: 15px; + transition: all 0.3s ease; + } + + .audio-btn:hover { + transform: scale(1.2); + color: #764ba2; + } + + /* Completion Message */ + .completion-message { + text-align: center; + padding: 40px; + background: rgba(255,255,255,0.1); + border-radius: 20px; + backdrop-filter: blur(10px); + color: white; + } + + .completion-title { + font-size: 2.5em; + margin-bottom: 20px; + color: #00ff88; + text-shadow: 0 2px 10px rgba(0,255,136,0.3); + } + + .completion-stats { + font-size: 1.3em; + margin-bottom: 30px; + line-height: 1.6; + } + + /* Responsive Design */ + @media (max-width: 768px) { + .letter-discovery-wrapper { + padding: 15px; + } + + .letter-display { + font-size: 6em; + } + + .word-text { + font-size: 2em; + } + + .challenge-text { + font-size: 1.5em; + } + + .practice-grid { + grid-template-columns: 1fr; + } + } + `; + document.head.appendChild(styleSheet); + } + + extractContent() { + logSh('๐Ÿ” Letter Discovery - Extracting content...', 'INFO'); + + // Check for letters in content or rawContent + const letters = this.content.letters || this.content.rawContent?.letters; + + if (letters && Object.keys(letters).length > 0) { + this.letters = Object.keys(letters).sort(); + this.letterWords = letters; + logSh(`๐Ÿ“ Found ${this.letters.length} letters with words`, 'INFO'); + } else { + this.showNoLettersMessage(); + return; + } + + logSh(`๐ŸŽฏ Letter Discovery ready: ${this.letters.length} letters`, 'INFO'); + } + + showNoLettersMessage() { + this.container.innerHTML = ` +
+
+

๐Ÿ”ค Letter Discovery

+

โŒ No letter structure found in this content.

+

This game requires content with a predefined letters system.

+

Try with content that includes letter-based learning material.

+ +
+
+ `; + } + + init() { + this.container.innerHTML = ` +
+
+
+
Score: ${this.score}
+
Lives: ${this.lives}
+
+
Letter Discovery
+
+
Progress: 0/${this.letters.length}
+
+
+
+
+ +
+
+
+ `; + + this.updateHUD(); + } + + start() { + this.showLetterCard(); + } + + updateHUD() { + const scoreDisplay = document.getElementById('score-display'); + const livesDisplay = document.getElementById('lives-display'); + const progressDisplay = document.getElementById('progress-display'); + const phaseIndicator = document.getElementById('phase-indicator'); + + if (scoreDisplay) scoreDisplay.textContent = this.score; + if (livesDisplay) livesDisplay.textContent = this.lives; + + if (this.currentPhase === 'letter-discovery') { + if (progressDisplay) progressDisplay.textContent = `${this.currentLetterIndex}/${this.letters.length}`; + if (phaseIndicator) phaseIndicator.textContent = 'Letter Discovery'; + } else if (this.currentPhase === 'word-exploration') { + if (progressDisplay) progressDisplay.textContent = `${this.currentWordIndex}/${this.letterWords[this.currentLetter].length}`; + if (phaseIndicator) phaseIndicator.textContent = `Exploring Letter "${this.currentLetter}"`; + } else if (this.currentPhase === 'practice') { + if (progressDisplay) progressDisplay.textContent = `Round ${this.practiceRound + 1}/${this.maxPracticeRounds}`; + if (phaseIndicator) phaseIndicator.textContent = `Practice - Level ${this.practiceLevel}`; + } + } + + showLetterCard() { + if (this.currentLetterIndex >= this.letters.length) { + this.showCompletion(); + return; + } + + const letter = this.letters[this.currentLetterIndex]; + const gameContent = document.getElementById('game-content'); + + gameContent.innerHTML = ` +
+
${letter}
+
Letter "${letter}"
+
${this.getLetterPronunciation(letter)}
+
+ + +
+
+ `; + + // Store reference for button callbacks + window.currentLetterGame = this; + + // Auto-play letter sound + setTimeout(() => this.playLetterSound(letter), 500); + } + + getLetterPronunciation(letter) { + // Basic letter pronunciation guide + const pronunciations = { + 'A': 'ay', 'B': 'bee', 'C': 'see', 'D': 'dee', 'E': 'ee', + 'F': 'ef', 'G': 'gee', 'H': 'aych', 'I': 'eye', 'J': 'jay', + 'K': 'kay', 'L': 'el', 'M': 'em', 'N': 'en', 'O': 'oh', + 'P': 'pee', 'Q': 'cue', 'R': 'ar', 'S': 'ess', 'T': 'tee', + 'U': 'you', 'V': 'vee', 'W': 'double-you', 'X': 'ex', 'Y': 'why', 'Z': 'zee' + }; + return pronunciations[letter] || letter.toLowerCase(); + } + + playLetterSound(letter) { + if (window.SettingsManager && window.SettingsManager.speak) { + const speed = 0.8; // Slower for letters + window.SettingsManager.speak(letter, { + lang: this.content.language || 'en-US', + rate: speed + }).catch(error => { + console.warn('๐Ÿ”Š TTS failed for letter:', error); + }); + } + } + + discoverLetter() { + const letter = this.letters[this.currentLetterIndex]; + this.discoveredLetters.push(letter); + this.score += 10; + this.onScoreUpdate(this.score); + + // Start word exploration for this letter + this.currentLetter = letter; + this.currentPhase = 'word-exploration'; + this.currentWordIndex = 0; + + this.updateHUD(); + this.showWordExploration(); + } + + showWordExploration() { + const words = this.letterWords[this.currentLetter]; + + if (!words || this.currentWordIndex >= words.length) { + // Finished exploring words for this letter + this.currentPhase = 'letter-discovery'; + this.currentLetterIndex++; + this.updateHUD(); + this.showLetterCard(); + return; + } + + const word = words[this.currentWordIndex]; + const gameContent = document.getElementById('game-content'); + + gameContent.innerHTML = ` +
+
Letter "${this.currentLetter}"
+
Word ${this.currentWordIndex + 1} of ${words.length}
+
+
+
${word.word}
+
${word.translation}
+ ${word.pronunciation ? `
[${word.pronunciation}]
` : ''} + ${word.type ? `
${word.type}
` : ''} + ${word.example ? `
"${word.example}"
` : ''} +
+ + +
+
+ `; + + // Add word to discovered list + this.discoveredWords.push(word); + + // Auto-play word sound + setTimeout(() => this.playWordSound(word.word), 500); + } + + playWordSound(word) { + if (window.SettingsManager && window.SettingsManager.speak) { + const speed = 0.9; + window.SettingsManager.speak(word, { + lang: this.content.language || 'en-US', + rate: speed + }).catch(error => { + console.warn('๐Ÿ”Š TTS failed for word:', error); + }); + } + } + + nextWord() { + this.currentWordIndex++; + this.score += 5; + this.onScoreUpdate(this.score); + this.updateHUD(); + this.showWordExploration(); + } + + showCompletion() { + const gameContent = document.getElementById('game-content'); + const totalWords = Object.values(this.letterWords).reduce((sum, words) => sum + words.length, 0); + + gameContent.innerHTML = ` +
+
๐ŸŽ‰ All Letters Discovered!
+
+ Letters Discovered: ${this.discoveredLetters.length}
+ Words Learned: ${this.discoveredWords.length}
+ Final Score: ${this.score} +
+
+ + +
+
+ `; + } + + startPractice() { + this.currentPhase = 'practice'; + this.practiceLevel = 1; + this.practiceRound = 0; + this.practiceCorrectAnswers = 0; + this.practiceErrors = 0; + + // Create mixed practice from all discovered words + this.currentPracticeItems = this.shuffleArray([...this.discoveredWords]); + + this.updateHUD(); + this.showPracticeChallenge(); + } + + showPracticeChallenge() { + if (this.practiceRound >= this.maxPracticeRounds) { + this.endPractice(); + return; + } + + const currentItem = this.currentPracticeItems[this.practiceRound % this.currentPracticeItems.length]; + const gameContent = document.getElementById('game-content'); + + // Generate options (correct + 3 random) + const allWords = this.discoveredWords.filter(w => w.word !== currentItem.word); + const randomOptions = this.shuffleArray([...allWords]).slice(0, 3); + const options = this.shuffleArray([currentItem, ...randomOptions]); + + gameContent.innerHTML = ` +
+
What does "${currentItem.word}" mean?
+
+ ${options.map((option, index) => ` + + `).join('')} +
+
+
Correct: ${this.practiceCorrectAnswers}
+
Errors: ${this.practiceErrors}
+
Round: ${this.practiceRound + 1}/${this.maxPracticeRounds}
+
+
+ `; + + // Store correct answer for checking + this.currentCorrectAnswer = currentItem.word; + + // Auto-play word + setTimeout(() => this.playWordSound(currentItem.word), 500); + } + + selectPracticeAnswer(selectedIndex, selectedWord) { + const buttons = document.querySelectorAll('.practice-option'); + const isCorrect = selectedWord === this.currentCorrectAnswer; + + if (isCorrect) { + buttons[selectedIndex].classList.add('correct'); + this.practiceCorrectAnswers++; + this.score += 10; + this.onScoreUpdate(this.score); + } else { + buttons[selectedIndex].classList.add('incorrect'); + this.practiceErrors++; + // Show correct answer + buttons.forEach((btn, index) => { + if (btn.textContent.trim() === this.discoveredWords.find(w => w.word === this.currentCorrectAnswer)?.translation) { + btn.classList.add('correct'); + } + }); + } + + setTimeout(() => { + this.practiceRound++; + this.updateHUD(); + this.showPracticeChallenge(); + }, 1500); + } + + endPractice() { + const accuracy = Math.round((this.practiceCorrectAnswers / this.maxPracticeRounds) * 100); + const gameContent = document.getElementById('game-content'); + + gameContent.innerHTML = ` +
+
๐Ÿ† Practice Complete!
+
+ Accuracy: ${accuracy}%
+ Correct Answers: ${this.practiceCorrectAnswers}/${this.maxPracticeRounds}
+ Final Score: ${this.score} +
+
+ +
+
+ `; + + // End game + setTimeout(() => { + this.onGameEnd(this.score); + }, 3000); + } + + 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; + } + + restart() { + this.currentPhase = 'letter-discovery'; + this.currentLetterIndex = 0; + this.discoveredLetters = []; + this.currentLetter = null; + this.currentWordIndex = 0; + this.discoveredWords = []; + this.score = 0; + this.lives = 3; + this.practiceLevel = 1; + this.practiceRound = 0; + this.practiceCorrectAnswers = 0; + this.practiceErrors = 0; + this.currentPracticeItems = []; + + this.updateHUD(); + this.start(); + } + + destroy() { + // Cleanup + if (window.currentLetterGame === this) { + delete window.currentLetterGame; + } + + const styleSheet = document.getElementById('letter-discovery-styles'); + if (styleSheet) { + styleSheet.remove(); + } + } +} + +// Register the game module +window.GameModules = window.GameModules || {}; +window.GameModules.LetterDiscovery = LetterDiscovery; \ No newline at end of file diff --git a/src/games/memory-match.js b/src/games/memory-match.js new file mode 100644 index 0000000..d3ceb40 --- /dev/null +++ b/src/games/memory-match.js @@ -0,0 +1,495 @@ +// === 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) { + logSh('Not enough vocabulary for Memory Match', 'ERROR'); + this.showInitError(); + return; + } + + this.createGameInterface(); + this.generateCards(); + this.setupEventListeners(); + } + + showInitError() { + this.container.innerHTML = ` +
+

โŒ Error loading

+

This content doesn't have enough vocabulary for Memory Match.

+

The game needs at least ${this.totalPairs} vocabulary pairs.

+ +
+ `; + } + + extractVocabulary(content) { + let vocabulary = []; + + logSh('๐Ÿ“ Extracting vocabulary from:', content?.name || 'content', 'INFO'); + + // Use raw module content if available + if (content.rawContent) { + logSh('๐Ÿ“ฆ Using raw module content', 'INFO'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + logSh('โœจ Ultra-modular format detected (vocabulary object)', 'INFO'); + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + // No other formats supported - ultra-modular only + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + logSh('๐Ÿ”ง Extracting from raw content:', rawContent.name || 'Module', 'INFO'); + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + logSh(`โœจ ${vocabulary.length} words extracted from ultra-modular vocabulary`, 'INFO'); + } + // No other formats supported - ultra-modular only + else { + logSh('โš ๏ธ Content format not supported - ultra-modular format required', 'WARN'); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Filter and validate vocabulary for ultra-modular format + vocabulary = vocabulary.filter(item => + item && + typeof item.original === 'string' && + typeof item.translation === 'string' && + item.original.trim() !== '' && + item.translation.trim() !== '' + ); + + if (vocabulary.length === 0) { + logSh('โŒ No valid vocabulary found', 'ERROR'); + // Demo vocabulary as fallback + vocabulary = [ + { original: "cat", translation: "chat" }, + { original: "dog", translation: "chien" }, + { original: "house", translation: "maison" }, + { original: "car", translation: "voiture" }, + { original: "book", translation: "livre" }, + { original: "water", translation: "eau" }, + { original: "food", translation: "nourriture" }, + { original: "friend", translation: "ami" } + ]; + logSh('๐Ÿšจ Using demo vocabulary', 'WARN'); + } + + logSh(`โœ… Memory Match: ${vocabulary.length} vocabulary items finalized`, 'INFO'); + return vocabulary; + } + + createGameInterface() { + this.container.innerHTML = ` +
+ +
+
+ Moves: + 0 +
+
+ Pairs: + 0 / ${this.totalPairs} +
+
+ Score: + 0 +
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ `; + } + + 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.original, + type: 'english', + pairId: index, + isFlipped: false, + isMatched: false + }); + + // French card + this.cards.push({ + id: `fr_${index}`, + content: item.translation, + 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 = ` +
+
+ ๐ŸŽฏ +
+
+ ${card.content} +
+
+ `; + + 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'); + + // Trigger success animation + this.triggerSuccessAnimation(firstIndex, secondIndex); + + if (this.matchedPairs === this.totalPairs) { + setTimeout(() => this.gameComplete(), 800); + } + } 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 = `
${message}
`; + } + + start() { + logSh('๐Ÿง  Memory Match: Starting', 'INFO'); + this.showFeedback('Find matching English-French pairs!', 'info'); + } + + restart() { + logSh('๐Ÿ”„ Memory Match: Restarting', 'INFO'); + this.reset(); + this.start(); + } + + reset() { + this.flippedCards = []; + this.matchedPairs = 0; + this.moves = 0; + this.score = 0; + this.isFlipping = false; + this.generateCards(); + this.updateStats(); + } + + triggerSuccessAnimation(cardIndex1, cardIndex2) { + // Get card elements + const card1 = document.querySelector(`[data-card-index="${cardIndex1}"]`); + const card2 = document.querySelector(`[data-card-index="${cardIndex2}"]`); + + if (!card1 || !card2) return; + + // Add success animation class + card1.classList.add('success-animation'); + card2.classList.add('success-animation'); + + // Create sparkle particles for both cards + this.createSparkleParticles(card1); + this.createSparkleParticles(card2); + + // Remove animation class after animation completes + setTimeout(() => { + card1.classList.remove('success-animation'); + card2.classList.remove('success-animation'); + }, 800); + } + + createSparkleParticles(cardElement) { + const rect = cardElement.getBoundingClientRect(); + + // Create 4 sparkle particles around the card + for (let i = 1; i <= 4; i++) { + const particle = document.createElement('div'); + particle.className = `success-particle particle-${i}`; + + // Position relative to card + particle.style.position = 'fixed'; + particle.style.left = (rect.left + rect.width / 2) + 'px'; + particle.style.top = (rect.top + rect.height / 2) + 'px'; + particle.style.pointerEvents = 'none'; + particle.style.zIndex = '1000'; + + document.body.appendChild(particle); + + // Remove particle after animation + setTimeout(() => { + if (particle.parentNode) { + particle.parentNode.removeChild(particle); + } + }, 1200); + } + } + + destroy() { + this.container.innerHTML = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.MemoryMatch = MemoryMatchGame; \ No newline at end of file diff --git a/src/games/quiz-game.js b/src/games/quiz-game.js new file mode 100644 index 0000000..7d59be5 --- /dev/null +++ b/src/games/quiz-game.js @@ -0,0 +1,529 @@ +// === 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; + this.quizDirection = 'original_to_translation'; // 'original_to_translation' or 'translation_to_original' + + // Extract vocabulary and additional words from texts/stories + this.vocabulary = this.extractVocabulary(this.content); + this.allWords = this.extractAllWords(this.content); + + this.init(); + } + + init() { + // Check if we have enough vocabulary + if (!this.vocabulary || this.vocabulary.length < 6) { + logSh('Not enough vocabulary for Quiz Game', 'ERROR'); + 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 = ` +
+

โŒ Error loading

+

This content doesn't have enough vocabulary for Quiz Game.

+

The game needs at least 6 vocabulary items.

+ +
+ `; + } + + extractVocabulary(content) { + let vocabulary = []; + + logSh('๐Ÿ” Extracting vocabulary from:', content?.name || 'content', 'INFO'); + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + logSh('๐Ÿ“ฆ Using raw module content', 'INFO'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + logSh('โœจ Ultra-modular format detected (vocabulary object)', 'INFO'); + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + // No other formats supported - ultra-modular only + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + logSh('๐Ÿ”ง Extracting from raw content:', rawContent.name || 'Module', 'INFO'); + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + logSh(`โœจ ${vocabulary.length} words extracted from ultra-modular vocabulary`, 'INFO'); + } + // No other formats supported - ultra-modular only + else { + logSh('โš ๏ธ Content format not supported - ultra-modular format required', 'WARN'); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + if (vocabulary.length === 0) { + logSh('โŒ No valid vocabulary found', 'ERROR'); + // Demo vocabulary as last resort + vocabulary = [ + { original: 'hello', translation: 'bonjour', category: 'greetings' }, + { original: 'goodbye', translation: 'au revoir', category: 'greetings' }, + { original: 'thank you', translation: 'merci', category: 'greetings' }, + { original: 'cat', translation: 'chat', category: 'animals' }, + { original: 'dog', translation: 'chien', category: 'animals' }, + { original: 'house', translation: 'maison', category: 'objects' }, + { original: 'car', translation: 'voiture', category: 'objects' }, + { original: 'book', translation: 'livre', category: 'objects' } + ]; + logSh('๐Ÿšจ Using demo vocabulary', 'WARN'); + } + + // Shuffle vocabulary for random questions + vocabulary = this.shuffleArray(vocabulary); + + logSh(`โœ… Quiz Game: ${vocabulary.length} vocabulary words finalized`, 'INFO'); + return vocabulary; + } + + extractAllWords(content) { + let allWords = []; + + // Add vocabulary words first + allWords = [...this.vocabulary]; + + // Extract from stories/texts + if (content.rawContent?.story?.chapters) { + content.rawContent.story.chapters.forEach(chapter => { + if (chapter.sentences) { + chapter.sentences.forEach(sentence => { + if (sentence.words && Array.isArray(sentence.words)) { + sentence.words.forEach(wordObj => { + if (wordObj.word && wordObj.translation) { + allWords.push({ + original: wordObj.word, + translation: wordObj.translation, + type: wordObj.type || 'word', + pronunciation: wordObj.pronunciation + }); + } + }); + } + }); + } + }); + } + + // Extract from additional stories (like WTA1B1) + if (content.rawContent?.additionalStories) { + content.rawContent.additionalStories.forEach(story => { + if (story.chapters) { + story.chapters.forEach(chapter => { + if (chapter.sentences) { + chapter.sentences.forEach(sentence => { + if (sentence.words && Array.isArray(sentence.words)) { + sentence.words.forEach(wordObj => { + if (wordObj.word && wordObj.translation) { + allWords.push({ + original: wordObj.word, + translation: wordObj.translation, + type: wordObj.type || 'word', + pronunciation: wordObj.pronunciation + }); + } + }); + } + }); + } + }); + } + }); + } + + // Remove duplicates based on original word + const uniqueWords = []; + const seenWords = new Set(); + + allWords.forEach(word => { + const key = word.original.toLowerCase(); + if (!seenWords.has(key)) { + seenWords.add(key); + uniqueWords.push(word); + } + }); + + logSh(`๐Ÿ“š Extracted ${uniqueWords.length} total words for quiz options`, 'INFO'); + return uniqueWords; + } + + 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; + } + + createGameInterface() { + this.container.innerHTML = ` +
+ +
+ +
+ + +
+
+
+
+
+ 1 / ${this.totalQuestions} + Score: 0 +
+
+ + +
+
+ Loading question... +
+
+ + +
+ +
+ + +
+ +
+ + + +
+ `; + + // Add CSS for top controls + const style = document.createElement('style'); + style.textContent = ` + .quiz-top-controls { + position: absolute; + top: 10px; + left: 10px; + z-index: 10; + } + + .restart-top { + background: rgba(255, 255, 255, 0.9) !important; + border: 2px solid #ccc !important; + color: #666 !important; + font-size: 12px !important; + padding: 8px 12px !important; + border-radius: 6px !important; + box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; + } + + .restart-top:hover { + background: rgba(255, 255, 255, 1) !important; + border-color: #999 !important; + color: #333 !important; + } + `; + document.head.appendChild(style); + + 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]; + + // Randomly choose quiz direction + this.quizDirection = Math.random() < 0.5 ? 'original_to_translation' : 'translation_to_original'; + + let questionText, correctAnswerText, sourceForWrongAnswers; + + if (this.quizDirection === 'original_to_translation') { + questionText = correctAnswer.original; + correctAnswerText = correctAnswer.translation; + sourceForWrongAnswers = 'translation'; + } else { + questionText = correctAnswer.translation; + correctAnswerText = correctAnswer.original; + sourceForWrongAnswers = 'original'; + } + + // Generate 5 wrong answers from allWords (which includes story words) + const availableWords = this.allWords.length >= 6 ? this.allWords : this.vocabulary; + const wrongAnswers = availableWords + .filter(item => item !== correctAnswer) + .sort(() => Math.random() - 0.5) + .slice(0, 5) + .map(item => sourceForWrongAnswers === 'translation' ? item.translation : item.original); + + // Combine and shuffle all options (1 correct + 5 wrong = 6 total) + const allOptions = [correctAnswerText, ...wrongAnswers].sort(() => Math.random() - 0.5); + + this.currentQuestionData = { + question: questionText, + correctAnswer: correctAnswerText, + options: allOptions, + direction: this.quizDirection + }; + + this.renderQuestion(); + this.updateProgress(); + } + + renderQuestion() { + const { question, options } = this.currentQuestionData; + + // Update question text with direction indicator + const direction = this.currentQuestionData.direction; + const directionText = direction === 'original_to_translation' ? + 'What is the translation of' : 'What is the original word for'; + + document.getElementById('question-text').innerHTML = ` + ${directionText} "${question}"? + `; + + // 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(), 250); + } + } + + 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 = `
${message}
`; + } + + start() { + logSh('โ“ Quiz Game: Starting', 'INFO'); + this.showFeedback('Choose the correct translation for each word!', 'info'); + } + + restart() { + logSh('๐Ÿ”„ Quiz Game: Restarting', 'INFO'); + 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.shuffleArray(this.vocabulary); + + this.generateQuestion(); + this.updateScore(); + } + + destroy() { + this.container.innerHTML = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.QuizGame = QuizGame; \ No newline at end of file diff --git a/src/games/river-run.js b/src/games/river-run.js new file mode 100644 index 0000000..aef4f7c --- /dev/null +++ b/src/games/river-run.js @@ -0,0 +1,1001 @@ +// === RIVER RUN GAME === +// Endless runner on a river with floating words - avoid obstacles, catch target words! + +class RiverRun { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.container = container; + this.content = content; + this.onScoreUpdate = onScoreUpdate; + this.onGameEnd = onGameEnd; + + // Game state + this.isRunning = false; + this.score = 0; + this.lives = 3; + this.level = 1; + this.speed = 2; // River flow speed + this.wordsCollected = 0; + + // Player + this.player = { + x: 50, // Percentage from left + y: 80, // Percentage from top + targetX: 50, + targetY: 80, + size: 40 + }; + + // Game objects + this.floatingWords = []; + this.currentTarget = null; + this.targetQueue = []; + this.powerUps = []; + + // River animation + this.riverOffset = 0; + this.particles = []; + + // Timing + this.lastSpawn = 0; + this.spawnInterval = 1000; // ms between word spawns (2x faster) + this.gameStartTime = Date.now(); + + // Word management + this.availableWords = []; + this.usedTargets = []; + + // Target word guarantee system + this.wordsSpawnedSinceTarget = 0; + this.maxWordsBeforeTarget = 10; // Guarantee target within 10 words + + this.injectCSS(); + this.extractContent(); + this.init(); + } + + injectCSS() { + if (document.getElementById('river-run-styles')) return; + + const styleSheet = document.createElement('style'); + styleSheet.id = 'river-run-styles'; + styleSheet.textContent = ` + .river-run-wrapper { + background: linear-gradient(180deg, #87CEEB 0%, #4682B4 50%, #2F4F4F 100%); + position: relative; + overflow: hidden; + height: 100vh; + cursor: crosshair; + } + + .river-run-hud { + position: absolute; + top: 20px; + left: 20px; + right: 20px; + display: flex; + justify-content: space-between; + z-index: 100; + color: white; + font-weight: bold; + text-shadow: 0 2px 4px rgba(0,0,0,0.5); + } + + .hud-left, .hud-right { + display: flex; + gap: 20px; + align-items: center; + } + + .target-display { + background: rgba(255,255,255,0.9); + color: #333; + padding: 10px 20px; + border-radius: 25px; + font-size: 1.2em; + font-weight: bold; + box-shadow: 0 4px 15px rgba(0,0,0,0.2); + animation: targetGlow 2s ease-in-out infinite alternate; + } + + @keyframes targetGlow { + from { box-shadow: 0 4px 15px rgba(0,0,0,0.2); } + to { box-shadow: 0 4px 20px rgba(255,215,0,0.6); } + } + + .river-canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: + radial-gradient(ellipse at center top, rgba(135,206,235,0.3) 0%, transparent 70%), + linear-gradient(0deg, + rgba(70,130,180,0.1) 0%, + rgba(135,206,235,0.05) 50%, + rgba(173,216,230,0.1) 100% + ); + } + + .river-waves { + position: absolute; + width: 120%; + height: 100%; + background: + repeating-linear-gradient( + 0deg, + transparent 0px, + rgba(255,255,255,0.1) 2px, + transparent 4px, + transparent 20px + ); + animation: riverFlow 3s linear infinite; + } + + @keyframes riverFlow { + from { transform: translateY(-20px); } + to { transform: translateY(0px); } + } + + .player { + position: absolute; + width: 40px; + height: 40px; + background: linear-gradient(45deg, #8B4513, #A0522D); + border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; + box-shadow: + 0 2px 10px rgba(0,0,0,0.3), + inset 0 2px 5px rgba(255,255,255,0.3); + transition: all 0.3s ease-out; + z-index: 50; + transform-origin: center; + } + + .player::before { + content: '๐Ÿ›ถ'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 20px; + filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); + } + + .player.moving { + animation: playerRipple 0.5s ease-out; + } + + @keyframes playerRipple { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } + } + + .floating-word { + position: absolute; + background: rgba(255,255,255,0.95); + border: 3px solid #4682B4; + border-radius: 15px; + padding: 8px 15px; + font-size: 1.1em; + font-weight: bold; + color: #333; + cursor: pointer; + transition: all 0.2s ease; + z-index: 40; + box-shadow: + 0 4px 15px rgba(0,0,0,0.2), + 0 0 0 0 rgba(70,130,180,0.4); + animation: wordFloat 3s ease-in-out infinite alternate; + } + + @keyframes wordFloat { + from { transform: translateY(0px) rotate(-1deg); } + to { transform: translateY(-5px) rotate(1deg); } + } + + .floating-word:hover { + transform: scale(1.1) translateY(-3px); + box-shadow: + 0 6px 20px rgba(0,0,0,0.3), + 0 0 20px rgba(70,130,180,0.6); + } + + /* Words are neutral at spawn - styling happens at interaction */ + + .floating-word.collected { + animation: wordCollected 0.8s ease-out forwards; + } + + @keyframes wordCollected { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.3); + opacity: 0.8; + } + 100% { + transform: scale(0) translateY(-50px); + opacity: 0; + } + } + + .floating-word.missed { + animation: wordMissed 0.6s ease-out forwards; + } + + @keyframes wordMissed { + 0% { + transform: scale(1); + opacity: 1; + background: rgba(255,255,255,0.95); + } + 100% { + transform: scale(0.8); + opacity: 0; + background: rgba(220,20,60,0.8); + } + } + + .power-up { + position: absolute; + width: 35px; + height: 35px; + border-radius: 50%; + background: linear-gradient(45deg, #FF6B35, #F7931E); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2em; + cursor: pointer; + z-index: 45; + animation: powerUpFloat 2s ease-in-out infinite alternate; + box-shadow: 0 4px 15px rgba(255,107,53,0.4); + } + + @keyframes powerUpFloat { + from { transform: translateY(0px) scale(1); } + to { transform: translateY(-8px) scale(1.05); } + } + + .game-over-modal { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(255,255,255,0.95); + padding: 40px; + border-radius: 20px; + text-align: center; + z-index: 200; + box-shadow: 0 10px 30px rgba(0,0,0,0.3); + backdrop-filter: blur(10px); + } + + .game-over-title { + font-size: 2.5em; + margin-bottom: 20px; + color: #4682B4; + text-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + + .game-over-stats { + font-size: 1.3em; + margin-bottom: 30px; + line-height: 1.6; + color: #333; + } + + .river-btn { + background: linear-gradient(45deg, #4682B4, #5F9EA0); + color: white; + border: none; + padding: 15px 30px; + border-radius: 25px; + font-size: 1.1em; + font-weight: bold; + cursor: pointer; + margin: 0 10px; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(70,130,180,0.3); + } + + .river-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(70,130,180,0.4); + } + + .particle { + position: absolute; + width: 4px; + height: 4px; + background: rgba(255,255,255,0.7); + border-radius: 50%; + pointer-events: none; + z-index: 30; + } + + .level-indicator { + position: absolute; + top: 70px; + left: 20px; + background: rgba(255,255,255,0.9); + color: #333; + padding: 5px 15px; + border-radius: 15px; + font-size: 0.9em; + font-weight: bold; + z-index: 100; + } + + /* Responsive */ + @media (max-width: 768px) { + .river-run-hud { + flex-direction: column; + gap: 10px; + } + + .floating-word { + font-size: 1em; + padding: 6px 12px; + } + + .target-display { + font-size: 1em; + padding: 8px 15px; + } + } + `; + document.head.appendChild(styleSheet); + } + + extractContent() { + logSh('๐ŸŒŠ River Run - Extracting vocabulary...', 'INFO'); + + // Extract words from various content formats + if (this.content.vocabulary) { + Object.keys(this.content.vocabulary).forEach(word => { + const wordData = this.content.vocabulary[word]; + this.availableWords.push({ + french: word, + english: typeof wordData === 'string' ? wordData : + wordData.translation || wordData.user_language || 'unknown', + pronunciation: wordData.pronunciation || wordData.prononciation + }); + }); + } + + // Fallback: extract from letter structure if available + if (this.content.letters && this.availableWords.length === 0) { + Object.values(this.content.letters).forEach(letterWords => { + letterWords.forEach(wordData => { + this.availableWords.push({ + french: wordData.word, + english: wordData.translation, + pronunciation: wordData.pronunciation + }); + }); + }); + } + + if (this.availableWords.length === 0) { + throw new Error('No vocabulary found for River Run'); + } + + logSh(`๐ŸŽฏ River Run ready: ${this.availableWords.length} words available`, 'INFO'); + this.generateTargetQueue(); + } + + generateTargetQueue() { + // Create queue of targets, ensuring variety + this.targetQueue = this.shuffleArray([...this.availableWords]).slice(0, Math.min(10, this.availableWords.length)); + this.usedTargets = []; + } + + init() { + this.container.innerHTML = ` +
+
+
+
Score: ${this.score}
+
Lives: ${this.lives}
+
Words: ${this.wordsCollected}
+
+
+ Click to Start! +
+
+
Level: ${this.level}
+
Speed: ${this.speed.toFixed(1)}x
+
+
+ +
+
+
+
+
+ `; + + this.setupEventListeners(); + this.updateHUD(); + } + + setupEventListeners() { + const riverGame = document.getElementById('river-game'); + + riverGame.addEventListener('click', (e) => { + if (!this.isRunning) { + this.start(); + return; + } + + const rect = riverGame.getBoundingClientRect(); + const clickX = ((e.clientX - rect.left) / rect.width) * 100; + const clickY = ((e.clientY - rect.top) / rect.height) * 100; + + this.movePlayer(clickX, clickY); + }); + + // Handle floating word clicks + riverGame.addEventListener('click', (e) => { + if (e.target.classList.contains('floating-word')) { + e.stopPropagation(); + this.handleWordClick(e.target); + } + }); + } + + start() { + if (this.isRunning) return; + + this.isRunning = true; + this.gameStartTime = Date.now(); + this.setNextTarget(); + + // Start game loop + this.gameLoop(); + + logSh('๐ŸŒŠ River Run started!', 'INFO'); + } + + gameLoop() { + if (!this.isRunning) return; + + const now = Date.now(); + + // Spawn new words + if (now - this.lastSpawn > this.spawnInterval) { + this.spawnFloatingWord(); + this.lastSpawn = now; + } + + // Update game objects + this.updateFloatingWords(); + this.updatePlayer(); + this.updateParticles(); + this.checkCollisions(); + + // Increase difficulty over time + this.updateDifficulty(); + + // Update UI + this.updateHUD(); + + // Continue loop + requestAnimationFrame(() => this.gameLoop()); + } + + setNextTarget() { + if (this.targetQueue.length === 0) { + this.generateTargetQueue(); + } + + this.currentTarget = this.targetQueue.shift(); + this.usedTargets.push(this.currentTarget); + + // Reset the word counter for new target + this.wordsSpawnedSinceTarget = 0; + + const targetDisplay = document.getElementById('target-display'); + if (targetDisplay) { + targetDisplay.innerHTML = `Find: ${this.currentTarget.english}`; + } + } + + spawnFloatingWord() { + const riverCanvas = document.getElementById('river-canvas'); + if (!riverCanvas) return; + + // Determine if we should force the target word + let word; + if (this.wordsSpawnedSinceTarget >= this.maxWordsBeforeTarget) { + // Force target word to appear + word = this.currentTarget; + this.wordsSpawnedSinceTarget = 0; // Reset counter + logSh(`๐ŸŽฏ Forcing target word: ${word.french}`, 'DEBUG'); + } else { + // Spawn random word + word = this.getRandomWord(); + this.wordsSpawnedSinceTarget++; + } + + const wordElement = document.createElement('div'); + wordElement.className = 'floating-word'; // No target/obstacle class at spawn + + // Add spaces based on level for increased difficulty + const spacePadding = ' '.repeat(this.level * 2); // 2 spaces per level on each side + wordElement.textContent = spacePadding + word.french + spacePadding; + + wordElement.style.left = `${Math.random() * 80 + 10}%`; + wordElement.style.top = '-60px'; + + // Store word data only + wordElement.wordData = word; + + riverCanvas.appendChild(wordElement); + this.floatingWords.push({ + element: wordElement, + y: -60, + x: parseFloat(wordElement.style.left), + wordData: word + }); + + // Occasional power-up spawn + if (Math.random() < 0.1) { + this.spawnPowerUp(); + } + } + + getRandomWord() { + // Simply return any random word from available vocabulary + return this.availableWords[Math.floor(Math.random() * this.availableWords.length)]; + } + + spawnPowerUp() { + const riverCanvas = document.getElementById('river-canvas'); + if (!riverCanvas) return; + + const powerUpElement = document.createElement('div'); + powerUpElement.className = 'power-up'; + powerUpElement.innerHTML = 'โšก'; + powerUpElement.style.left = `${Math.random() * 80 + 10}%`; + powerUpElement.style.top = '-40px'; + + riverCanvas.appendChild(powerUpElement); + this.powerUps.push({ + element: powerUpElement, + y: -40, + x: parseFloat(powerUpElement.style.left), + type: 'slowTime' + }); + } + + updateFloatingWords() { + this.floatingWords = this.floatingWords.filter(word => { + word.y += this.speed; + word.element.style.top = `${word.y}px`; + + // Remove words that went off screen + if (word.y > window.innerHeight + 60) { + // CHECK AT EXIT TIME: Was this the target word? + if (word.wordData.french === this.currentTarget.french) { + // Missed target word - lose life + this.loseLife(); + } + word.element.remove(); + return false; + } + + return true; + }); + + // Update power-ups + this.powerUps = this.powerUps.filter(powerUp => { + powerUp.y += this.speed; + powerUp.element.style.top = `${powerUp.y}px`; + + if (powerUp.y > window.innerHeight + 40) { + powerUp.element.remove(); + return false; + } + + return true; + }); + } + + movePlayer(targetX, targetY) { + this.player.targetX = Math.max(5, Math.min(95, targetX)); + this.player.targetY = Math.max(10, Math.min(90, targetY)); + + const playerElement = document.getElementById('player'); + if (playerElement) { + playerElement.classList.add('moving'); + setTimeout(() => { + playerElement.classList.remove('moving'); + }, 500); + } + + // Create ripple effect + this.createRippleEffect(targetX, targetY); + } + + updatePlayer() { + // Smooth movement towards target + const speed = 0.1; + this.player.x += (this.player.targetX - this.player.x) * speed; + this.player.y += (this.player.targetY - this.player.y) * speed; + + const playerElement = document.getElementById('player'); + if (playerElement) { + playerElement.style.left = `calc(${this.player.x}% - 20px)`; + playerElement.style.top = `calc(${this.player.y}% - 20px)`; + } + } + + createRippleEffect(x, y) { + for (let i = 0; i < 5; i++) { + setTimeout(() => { + const particle = document.createElement('div'); + particle.className = 'particle'; + particle.style.left = `${x}%`; + particle.style.top = `${y}%`; + particle.style.animation = `particleSpread 1s ease-out forwards`; + + const riverCanvas = document.getElementById('river-canvas'); + if (riverCanvas) { + riverCanvas.appendChild(particle); + + setTimeout(() => { + particle.remove(); + }, 1000); + } + }, i * 100); + } + } + + updateParticles() { + // Create water particles occasionally + if (Math.random() < 0.1) { + const particle = document.createElement('div'); + particle.className = 'particle'; + particle.style.left = `${Math.random() * 100}%`; + particle.style.top = '-5px'; + particle.style.animation = `particleFlow 3s linear forwards`; + + const riverCanvas = document.getElementById('river-canvas'); + if (riverCanvas) { + riverCanvas.appendChild(particle); + + setTimeout(() => { + particle.remove(); + }, 3000); + } + } + } + + checkCollisions() { + const playerRect = this.getPlayerRect(); + + // Check word collisions + this.floatingWords.forEach((word, index) => { + const wordRect = this.getElementRect(word.element); + + if (this.isColliding(playerRect, wordRect)) { + this.handleWordCollision(word, index); + } + }); + + // Check power-up collisions + this.powerUps.forEach((powerUp, index) => { + const powerUpRect = this.getElementRect(powerUp.element); + + if (this.isColliding(playerRect, powerUpRect)) { + this.handlePowerUpCollision(powerUp, index); + } + }); + } + + getPlayerRect() { + const playerElement = document.getElementById('player'); + if (!playerElement) return { x: 0, y: 0, width: 0, height: 0 }; + + const rect = playerElement.getBoundingClientRect(); + const canvas = document.getElementById('river-canvas').getBoundingClientRect(); + + return { + x: rect.left - canvas.left, + y: rect.top - canvas.top, + width: rect.width, + height: rect.height + }; + } + + getElementRect(element) { + const rect = element.getBoundingClientRect(); + const canvas = document.getElementById('river-canvas').getBoundingClientRect(); + + return { + x: rect.left - canvas.left, + y: rect.top - canvas.top, + width: rect.width, + height: rect.height + }; + } + + isColliding(rect1, rect2) { + return rect1.x < rect2.x + rect2.width && + rect1.x + rect1.width > rect2.x && + rect1.y < rect2.y + rect2.height && + rect1.y + rect1.height > rect2.y; + } + + handleWordClick(wordElement) { + const wordData = wordElement.wordData; + + // CHECK AT PICK TIME: Is this the target word? + if (wordData.french === this.currentTarget.french) { + // Correct target word clicked + this.collectWord(wordElement, true); + } else { + // Wrong word clicked - it's an obstacle + this.missWord(wordElement); + } + } + + handleWordCollision(word, index) { + // CHECK AT COLLISION TIME: Is this the target word? + if (word.wordData.french === this.currentTarget.french) { + this.collectWord(word.element, true); + } else { + // Collision with non-target word = obstacle hit + this.missWord(word.element); + } + + // Remove from array + this.floatingWords.splice(index, 1); + } + + collectWord(wordElement, isCorrect) { + wordElement.classList.add('collected'); + + if (isCorrect) { + this.score += 10 + (this.level * 2); + this.wordsCollected++; + this.onScoreUpdate(this.score); + + // Set next target + this.setNextTarget(); + + // Play success sound + this.playSuccessSound(wordElement.textContent); + } + + setTimeout(() => { + wordElement.remove(); + }, 800); + } + + missWord(wordElement) { + wordElement.classList.add('missed'); + this.loseLife(); + + setTimeout(() => { + wordElement.remove(); + }, 600); + } + + handlePowerUpCollision(powerUp, index) { + this.activatePowerUp(powerUp.type); + powerUp.element.remove(); + this.powerUps.splice(index, 1); + } + + activatePowerUp(type) { + switch (type) { + case 'slowTime': + this.speed *= 0.5; + setTimeout(() => { + this.speed *= 2; + }, 3000); + break; + } + } + + updateDifficulty() { + const timeElapsed = Date.now() - this.gameStartTime; + const newLevel = Math.floor(timeElapsed / 30000) + 1; // Level up every 30 seconds + + if (newLevel > this.level) { + this.level = newLevel; + this.speed += 0.5; + this.spawnInterval = Math.max(500, this.spawnInterval - 100); // More aggressive spawn increase + } + } + + playSuccessSound(word) { + if (window.SettingsManager && window.SettingsManager.speak) { + window.SettingsManager.speak(word, { + lang: this.content.language || 'fr-FR', + rate: 1.0 + }).catch(error => { + console.warn('๐Ÿ”Š TTS failed:', error); + }); + } + } + + loseLife() { + this.lives--; + + if (this.lives <= 0) { + this.gameOver(); + } + } + + gameOver() { + this.isRunning = false; + + const riverGame = document.getElementById('river-game'); + const accuracy = this.wordsCollected > 0 ? Math.round((this.wordsCollected / (this.wordsCollected + (3 - this.lives))) * 100) : 0; + + const gameOverModal = document.createElement('div'); + gameOverModal.className = 'game-over-modal'; + gameOverModal.innerHTML = ` +
๐ŸŒŠ River Complete!
+
+ Final Score: ${this.score}
+ Words Collected: ${this.wordsCollected}
+ Level Reached: ${this.level}
+ Accuracy: ${accuracy}% +
+
+ + +
+ `; + + riverGame.appendChild(gameOverModal); + + // Store reference for button callbacks + window.currentRiverGame = this; + + setTimeout(() => { + this.onGameEnd(this.score); + }, 5000); + } + + updateHUD() { + const scoreDisplay = document.getElementById('score-display'); + const livesDisplay = document.getElementById('lives-display'); + const wordsDisplay = document.getElementById('words-display'); + const levelDisplay = document.getElementById('level-display'); + const speedDisplay = document.getElementById('speed-display'); + + if (scoreDisplay) scoreDisplay.textContent = this.score; + if (livesDisplay) livesDisplay.textContent = this.lives; + if (wordsDisplay) wordsDisplay.textContent = this.wordsCollected; + if (levelDisplay) levelDisplay.textContent = this.level; + if (speedDisplay) speedDisplay.textContent = this.speed.toFixed(1) + 'x'; + } + + 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; + } + + restart() { + // Reset game state + this.isRunning = false; + this.score = 0; + this.lives = 3; + this.level = 1; + this.speed = 2; + this.wordsCollected = 0; + this.riverOffset = 0; + + // Reset player position + this.player.x = 50; + this.player.y = 80; + this.player.targetX = 50; + this.player.targetY = 80; + + // Clear game objects + this.floatingWords = []; + this.powerUps = []; + this.particles = []; + + // Reset timing + this.lastSpawn = 0; + this.spawnInterval = 1000; // 2x faster spawn rate + this.gameStartTime = Date.now(); + + // Reset targets and word counter + this.wordsSpawnedSinceTarget = 0; + this.generateTargetQueue(); + + // Cleanup DOM + const riverCanvas = document.getElementById('river-canvas'); + if (riverCanvas) { + const words = riverCanvas.querySelectorAll('.floating-word'); + const powerUps = riverCanvas.querySelectorAll('.power-up'); + const particles = riverCanvas.querySelectorAll('.particle'); + + words.forEach(word => word.remove()); + powerUps.forEach(powerUp => powerUp.remove()); + particles.forEach(particle => particle.remove()); + } + + const gameOverModal = document.querySelector('.game-over-modal'); + if (gameOverModal) { + gameOverModal.remove(); + } + + // Reset target display + const targetDisplay = document.getElementById('target-display'); + if (targetDisplay) { + targetDisplay.textContent = 'Click to Start!'; + } + + this.updateHUD(); + + logSh('๐Ÿ”„ River Run restarted', 'INFO'); + } + + destroy() { + this.isRunning = false; + + // Cleanup + if (window.currentRiverGame === this) { + delete window.currentRiverGame; + } + + const styleSheet = document.getElementById('river-run-styles'); + if (styleSheet) { + styleSheet.remove(); + } + } +} + +// Add CSS animations +const additionalCSS = ` + @keyframes particleSpread { + 0% { + transform: scale(1) translate(0, 0); + opacity: 1; + } + 100% { + transform: scale(0) translate(${Math.random() * 100 - 50}px, ${Math.random() * 100 - 50}px); + opacity: 0; + } + } + + @keyframes particleFlow { + 0% { + transform: translateY(0); + opacity: 0.7; + } + 100% { + transform: translateY(100vh); + opacity: 0; + } + } +`; + +// Inject additional CSS +const additionalStyleSheet = document.createElement('style'); +additionalStyleSheet.textContent = additionalCSS; +document.head.appendChild(additionalStyleSheet); + +// Register the game module +window.GameModules = window.GameModules || {}; +window.GameModules.RiverRun = RiverRun; \ No newline at end of file diff --git a/src/games/story-builder.js b/src/games/story-builder.js new file mode 100644 index 0000000..49eef8a --- /dev/null +++ b/src/games/story-builder.js @@ -0,0 +1,979 @@ +// === STORY BUILDER GAME - STORY CONSTRUCTOR === + +class StoryBuilderGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.contentEngine = options.contentEngine; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Game state + this.score = 0; + this.currentStory = []; + this.availableElements = []; + this.storyTarget = null; + this.gameMode = 'vocabulary'; // 'vocabulary', 'sequence', 'dialogue', 'scenario' + + // Extract vocabulary using ultra-modular format + this.vocabulary = this.extractVocabulary(this.content); + this.wordsByType = this.groupVocabularyByType(this.vocabulary); + + // Configuration + this.maxElements = 6; + this.timeLimit = 180; // 3 minutes + this.timeLeft = this.timeLimit; + this.isRunning = false; + + // Timers + this.gameTimer = null; + + this.init(); + } + + init() { + // Check if we have enough vocabulary + if (!this.vocabulary || this.vocabulary.length < 6) { + logSh('Not enough vocabulary for Story Builder', 'ERROR'); + this.showInitError(); + return; + } + + this.createGameBoard(); + this.setupEventListeners(); + this.loadStoryContent(); + } + + showInitError() { + this.container.innerHTML = ` +
+

โŒ Error loading

+

This content doesn't have enough vocabulary for Story Builder.

+

The game needs at least 6 vocabulary words with types (noun, verb, adjective, etc.).

+ +
+ `; + } + + createGameBoard() { + this.container.innerHTML = ` +
+ +
+ + + + +
+ + +
+
+

Objective:

+

Choose a mode and let's start!

+
+
+
+ ${this.timeLeft} + Time +
+
+ 0/${this.maxElements} + Progress +
+
+
+ + +
+
+ +
+ +
+
Drag elements here to build your story
+
+
+ + +
+ +
+ + +
+ + + + +
+ + + +
+ `; + } + + setupEventListeners() { + // Mode selection + document.querySelectorAll('.mode-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + if (this.isRunning) return; + + document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this.gameMode = btn.dataset.mode; + + this.loadStoryContent(); + }); + }); + + // Game controls + document.getElementById('start-btn').addEventListener('click', () => this.start()); + document.getElementById('check-btn').addEventListener('click', () => this.checkStory()); + document.getElementById('hint-btn').addEventListener('click', () => this.showHint()); + document.getElementById('restart-btn').addEventListener('click', () => this.restart()); + + // Drag and Drop setup + this.setupDragAndDrop(); + } + + loadStoryContent() { + logSh('๐ŸŽฎ Loading story content for mode:', this.gameMode, 'INFO'); + + switch (this.gameMode) { + case 'vocabulary': + this.setupVocabularyMode(); + break; + case 'sequence': + this.setupSequenceMode(); + break; + case 'dialogue': + this.setupDialogueMode(); + break; + case 'scenario': + this.setupScenarioMode(); + break; + default: + this.setupVocabularyMode(); + } + } + + extractVocabulary(content) { + let vocabulary = []; + + logSh('๐Ÿ“ Extracting vocabulary from:', content?.name || 'content', 'INFO'); + + // Use raw module content if available + if (content.rawContent) { + logSh('๐Ÿ“ฆ Using raw module content', 'INFO'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + logSh('โœจ Ultra-modular format detected (vocabulary object)', 'INFO'); + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // No legacy fallback - ultra-modular only + return null; + }).filter(Boolean); + } + // No other formats supported - ultra-modular only + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + logSh('๐Ÿ”ง Extracting from raw content:', rawContent.name || 'Module', 'INFO'); + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // No legacy fallback - ultra-modular only + return null; + }).filter(Boolean); + } + // No other formats supported - ultra-modular only + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Filter out invalid entries + vocabulary = vocabulary.filter(item => + item && + typeof item.original === 'string' && + typeof item.translation === 'string' && + item.original.trim() !== '' && + item.translation.trim() !== '' + ); + + logSh(`๐Ÿ“Š Finalized ${vocabulary.length} vocabulary items`, 'INFO'); + return vocabulary; + } + + groupVocabularyByType(vocabulary) { + const grouped = {}; + + vocabulary.forEach(word => { + const type = word.type || 'general'; + if (!grouped[type]) { + grouped[type] = []; + } + grouped[type].push(word); + }); + + logSh('๐Ÿ“Š Words grouped by type:', Object.keys(grouped).map(type => `${type}: ${grouped[type].length}`).join(', '), 'INFO'); + return grouped; + } + + setupVocabularyMode() { + if (Object.keys(this.wordsByType).length === 0) { + this.setupFallbackContent(); + return; + } + + // Create a story template using different word types + this.storyTarget = this.createStoryTemplate(); + this.availableElements = this.selectWordsForStory(); + + document.getElementById('objective-text').textContent = + 'Build a coherent story using these words! Use different types: nouns, verbs, adjectives...'; + } + + createStoryTemplate() { + const types = Object.keys(this.wordsByType); + + // Common story templates based on available word types + const templates = [ + { pattern: ['noun', 'verb', 'adjective', 'noun'], name: 'Simple Story' }, + { pattern: ['adjective', 'noun', 'verb', 'noun'], name: 'Descriptive Story' }, + { pattern: ['noun', 'verb', 'adjective', 'noun', 'verb'], name: 'Action Story' }, + { pattern: ['article', 'adjective', 'noun', 'verb', 'adverb'], name: 'Rich Story' } + ]; + + // Find the best template based on available word types + const availableTemplate = templates.find(template => + template.pattern.every(type => + types.includes(type) && this.wordsByType[type].length > 0 + ) + ); + + if (availableTemplate) { + return { + template: availableTemplate, + requiredTypes: availableTemplate.pattern + }; + } + + // Fallback: use available types + return { + template: { pattern: types.slice(0, 4), name: 'Custom Story' }, + requiredTypes: types.slice(0, 4) + }; + } + + selectWordsForStory() { + const words = []; + + if (this.storyTarget && this.storyTarget.requiredTypes) { + // Select words for each required type + this.storyTarget.requiredTypes.forEach(type => { + if (this.wordsByType[type] && this.wordsByType[type].length > 0) { + // Add 2-3 words of each type for choice + const typeWords = this.shuffleArray([...this.wordsByType[type]]).slice(0, 3); + words.push(...typeWords); + } + }); + } + + // Add some random extra words for distraction + const allTypes = Object.keys(this.wordsByType); + allTypes.forEach(type => { + if (this.wordsByType[type] && this.wordsByType[type].length > 0) { + const extraWords = this.shuffleArray([...this.wordsByType[type]]).slice(0, 1); + words.push(...extraWords); + } + }); + + // Remove duplicates and shuffle + const uniqueWords = words.filter((word, index, self) => + self.findIndex(w => w.original === word.original) === index + ); + + return this.shuffleArray(uniqueWords).slice(0, this.maxElements); + } + + setupSequenceMode() { + // Use vocabulary to create a logical sequence + const actionWords = this.wordsByType.verb || []; + const objectWords = this.wordsByType.noun || []; + + if (actionWords.length >= 2 && objectWords.length >= 2) { + this.storyTarget = { + type: 'sequence', + steps: [ + { order: 1, text: `First: ${actionWords[0].original}`, word: actionWords[0] }, + { order: 2, text: `Then: ${actionWords[1].original}`, word: actionWords[1] }, + { order: 3, text: `With: ${objectWords[0].original}`, word: objectWords[0] }, + { order: 4, text: `Finally: ${objectWords[1].original}`, word: objectWords[1] } + ] + }; + + this.availableElements = this.shuffleArray([...this.storyTarget.steps]); + document.getElementById('objective-text').textContent = + 'Put these actions in logical order!'; + } else { + this.setupVocabularyMode(); // Fallback + } + } + + setupDialogueMode() { + // Create a simple dialogue using available vocabulary + const greetings = this.wordsByType.greeting || []; + const nouns = this.wordsByType.noun || []; + const verbs = this.wordsByType.verb || []; + + if (greetings.length >= 1 && (nouns.length >= 2 || verbs.length >= 2)) { + const dialogue = [ + { speaker: 'A', text: greetings[0].original, word: greetings[0] }, + { speaker: 'B', text: greetings[0].translation, word: greetings[0] } + ]; + + if (verbs.length >= 1) { + dialogue.push({ speaker: 'A', text: verbs[0].original, word: verbs[0] }); + } + if (nouns.length >= 1) { + dialogue.push({ speaker: 'B', text: nouns[0].original, word: nouns[0] }); + } + + this.storyTarget = { type: 'dialogue', conversation: dialogue }; + this.availableElements = this.shuffleArray([...dialogue]); + + document.getElementById('objective-text').textContent = + 'Reconstruct this dialogue in the right order!'; + } else { + this.setupVocabularyMode(); // Fallback + } + } + + setupScenarioMode() { + // Create a scenario using mixed vocabulary types + const allWords = Object.values(this.wordsByType).flat(); + + if (allWords.length >= 4) { + const scenario = { + context: 'Daily Life', + elements: this.shuffleArray(allWords).slice(0, 6) + }; + + this.storyTarget = { type: 'scenario', scenario }; + this.availableElements = [...scenario.elements]; + + document.getElementById('objective-text').textContent = + `Create a story about: "${scenario.context}" using these words!`; + } else { + this.setupVocabularyMode(); // Fallback + } + } + + setupFallbackContent() { + // Use any available vocabulary + if (this.vocabulary.length >= 4) { + this.availableElements = this.shuffleArray([...this.vocabulary]).slice(0, 6); + this.gameMode = 'vocabulary'; + + document.getElementById('objective-text').textContent = + 'Build a story with these words!'; + } else { + document.getElementById('objective-text').textContent = + 'Not enough vocabulary available. Please select different content.'; + } + } + + start() { + if (this.isRunning || this.availableElements.length === 0) return; + + this.isRunning = true; + this.score = 0; + this.currentStory = []; + this.timeLeft = this.timeLimit; + + this.renderElements(); + this.startTimer(); + this.updateUI(); + + document.getElementById('start-btn').disabled = true; + document.getElementById('check-btn').disabled = false; + document.getElementById('hint-btn').disabled = false; + + this.showFeedback('Drag the elements in order to build your story!', 'info'); + } + + renderElements() { + const elementsBank = document.getElementById('elements-bank'); + elementsBank.innerHTML = '

Available elements:

'; + + this.availableElements.forEach((element, index) => { + const elementDiv = this.createElement(element, index); + elementsBank.appendChild(elementDiv); + }); + } + + createElement(element, index) { + const div = document.createElement('div'); + div.className = 'story-element'; + div.draggable = true; + div.dataset.index = index; + + // Ultra-modular format display + if (element.original && element.translation) { + // Vocabulary word with type + div.innerHTML = ` +
+
${element.original}
+
${element.translation}
+ ${element.type ? `
${element.type}
` : ''} +
+ `; + } else if (element.text || element.original) { + // Dialogue or sequence element + div.innerHTML = ` +
+
${element.text || element.original}
+ ${element.translation ? `
${element.translation}
` : ''} + ${element.speaker ? `
${element.speaker}:
` : ''} +
+ `; + } else if (element.word) { + // Element containing a word object + div.innerHTML = ` +
+
${element.word.original}
+
${element.word.translation}
+ ${element.word.type ? `
${element.word.type}
` : ''} +
+ `; + } else if (typeof element === 'string') { + // Simple text + div.innerHTML = `
${element}
`; + } + + // Add type-based styling + if (element.type) { + div.classList.add(`type-${element.type}`); + } + + return div; + } + + setupDragAndDrop() { + let draggedElement = null; + + document.addEventListener('dragstart', (e) => { + if (e.target.classList.contains('story-element')) { + draggedElement = e.target; + e.target.style.opacity = '0.5'; + } + }); + + document.addEventListener('dragend', (e) => { + if (e.target.classList.contains('story-element')) { + e.target.style.opacity = '1'; + draggedElement = null; + } + }); + + const dropZone = document.getElementById('drop-zone'); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('drag-over'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('drag-over'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('drag-over'); + + if (draggedElement && this.isRunning) { + this.addToStory(draggedElement); + } + }); + } + + addToStory(elementDiv) { + const index = parseInt(elementDiv.dataset.index); + const element = this.availableElements[index]; + + // Add to the story + this.currentStory.push({ element, originalIndex: index }); + + // Create element in construction zone + const storyElement = elementDiv.cloneNode(true); + storyElement.classList.add('in-story'); + storyElement.draggable = false; + + // Ajouter bouton de suppression + const removeBtn = document.createElement('button'); + removeBtn.className = 'remove-element'; + removeBtn.innerHTML = 'ร—'; + removeBtn.onclick = () => this.removeFromStory(storyElement, element); + storyElement.appendChild(removeBtn); + + document.getElementById('drop-zone').appendChild(storyElement); + + // Masquer l'รฉlรฉment original + elementDiv.style.display = 'none'; + + this.updateProgress(); + } + + removeFromStory(storyElement, element) { + // Remove from story + this.currentStory = this.currentStory.filter(item => item.element !== element); + + // Supprimer visuellement + storyElement.remove(); + + // Rรฉafficher l'รฉlรฉment original + const originalElement = document.querySelector(`[data-index="${this.availableElements.indexOf(element)}"]`); + if (originalElement) { + originalElement.style.display = 'block'; + } + + this.updateProgress(); + } + + checkStory() { + if (this.currentStory.length === 0) { + this.showFeedback('Add at least one element to your story!', 'error'); + return; + } + + const isCorrect = this.validateStory(); + + if (isCorrect) { + this.score += this.currentStory.length * 10; + this.showFeedback('Bravo! Perfect story! ๐ŸŽ‰', 'success'); + this.onScoreUpdate(this.score); + + setTimeout(() => { + this.nextChallenge(); + }, 2000); + } else { + this.score = Math.max(0, this.score - 5); + this.showFeedback('Almost! Check the order of your story ๐Ÿค”', 'warning'); + this.onScoreUpdate(this.score); + } + } + + validateStory() { + switch (this.gameMode) { + case 'vocabulary': + return this.validateVocabularyStory(); + case 'sequence': + return this.validateSequence(); + case 'dialogue': + return this.validateDialogue(); + case 'scenario': + return this.validateScenario(); + default: + return true; // Free mode + } + } + + validateVocabularyStory() { + if (this.currentStory.length < 3) return false; + + // Check for variety in word types + const typesUsed = new Set(); + this.currentStory.forEach(item => { + const element = item.element; + if (element.type) { + typesUsed.add(element.type); + } + }); + + // Require at least 2 different word types for a good story + return typesUsed.size >= 2; + } + + validateSequence() { + if (!this.storyTarget?.steps) return true; + + const expectedOrder = this.storyTarget.steps.sort((a, b) => a.order - b.order); + + if (this.currentStory.length !== expectedOrder.length) return false; + + return this.currentStory.every((item, index) => { + const expected = expectedOrder[index]; + return item.element.order === expected.order; + }); + } + + validateDialogue() { + // Flexible dialogue validation (logical order of replies) + return this.currentStory.length >= 2; + } + + validateScenario() { + // Flexible scenario validation (contextual coherence) + return this.currentStory.length >= 3; + } + + showHint() { + switch (this.gameMode) { + case 'vocabulary': + const typesAvailable = Object.keys(this.wordsByType); + this.showFeedback(`Tip: Try using different word types: ${typesAvailable.join(', ')}`, 'info'); + break; + case 'sequence': + if (this.storyTarget?.steps) { + const nextStep = this.storyTarget.steps.find(step => + !this.currentStory.some(item => item.element.order === step.order) + ); + if (nextStep) { + this.showFeedback(`Next step: "${nextStep.text}"`, 'info'); + } + } + break; + case 'dialogue': + this.showFeedback('Think about the natural order of a conversation!', 'info'); + break; + case 'scenario': + this.showFeedback('Create a coherent story in this context!', 'info'); + break; + default: + this.showFeedback('Tip: Think about the logical order of events!', 'info'); + } + } + + nextChallenge() { + // Load a new challenge + this.loadStoryContent(); + this.currentStory = []; + document.getElementById('drop-zone').innerHTML = '
Drag elements here to build your story
'; + 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 = '
Drag elements here to build your story
'; + 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 = `
${message}
`; + } + + 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 = ` + +`; + +// Ajouter les styles +document.head.insertAdjacentHTML('beforeend', storyBuilderStyles); + +// Enregistrement du module +window.GameModules = window.GameModules || {}; +window.GameModules.StoryBuilder = StoryBuilderGame; \ No newline at end of file diff --git a/src/games/story-reader.js b/src/games/story-reader.js new file mode 100644 index 0000000..d552d6d --- /dev/null +++ b/src/games/story-reader.js @@ -0,0 +1,1366 @@ +// === STORY READER GAME === +// Prototype for reading long stories with sentence chunking and word-by-word translation + +class StoryReader { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Reading state + this.currentChapter = 0; + this.currentSentence = 0; + this.totalSentences = 0; + this.readingSessions = 0; + this.wordsRead = 0; + this.comprehensionScore = 0; + + // Story data + this.story = null; + this.availableStories = []; + this.currentStoryIndex = 0; + this.vocabulary = {}; + + // UI state + this.showTranslations = false; + this.showPronunciations = false; + this.readingMode = 'sentence'; // 'sentence' or 'paragraph' + this.fontSize = 'medium'; + + // Reading time tracking + this.startTime = Date.now(); + this.totalReadingTime = 0; + this.readingTimer = null; + + // TTS settings + this.autoPlayTTS = true; + this.ttsEnabled = true; + + // Expose content globally for SettingsManager TTS language detection + window.currentGameContent = this.content; + + this.init(); + } + + init() { + logSh(`๐Ÿ” Story Reader content received:`, this.content, 'DEBUG'); + logSh(`๐Ÿ” Story field exists: ${!!this.content.story}`, 'DEBUG'); + logSh(`๐Ÿ” RawContent exists: ${!!this.content.rawContent}`, 'DEBUG'); + + // Discover all available stories + this.discoverAvailableStories(); + + if (this.availableStories.length === 0) { + logSh('No story content found in content or rawContent', 'ERROR'); + this.showError('This content does not contain any stories for reading.'); + return; + } + + // Get URL params to check if specific story is requested + const urlParams = new URLSearchParams(window.location.search); + const requestedStory = urlParams.get('story'); + + if (requestedStory) { + const storyIndex = this.availableStories.findIndex(story => + story.id === requestedStory || story.title.toLowerCase().includes(requestedStory.toLowerCase()) + ); + if (storyIndex !== -1) { + this.currentStoryIndex = storyIndex; + } + } + + this.selectStory(this.currentStoryIndex); + this.vocabulary = this.content.rawContent?.vocabulary || this.content.vocabulary || {}; + + logSh(`๐Ÿ“– Story Reader initialized: "${this.story.title}" (${this.totalSentences} sentences)`, 'INFO'); + + this.createInterface(); + this.loadProgress(); + this.renderCurrentSentence(); + } + + discoverAvailableStories() { + this.availableStories = []; + + // Check main story field + const mainStory = this.content.rawContent?.story || this.content.story; + if (mainStory && mainStory.title) { + this.availableStories.push({ + id: 'main', + title: mainStory.title, + data: mainStory, + source: 'main' + }); + } + + // Check additionalStories field (like in WTA1B1) + const additionalStories = this.content.rawContent?.additionalStories || this.content.additionalStories; + if (additionalStories && Array.isArray(additionalStories)) { + additionalStories.forEach((story, index) => { + if (story && story.title) { + this.availableStories.push({ + id: `additional_${index}`, + title: story.title, + data: story, + source: 'additional' + }); + } + }); + } + + // NEW: Check for simple texts and convert them to stories + const texts = this.content.rawContent?.texts || this.content.texts; + if (texts && Array.isArray(texts)) { + texts.forEach((text, index) => { + if (text && (text.title || text.original_language)) { + const convertedStory = this.convertTextToStory(text, index); + this.availableStories.push({ + id: `text_${index}`, + title: text.title || `Text ${index + 1}`, + data: convertedStory, + source: 'text' + }); + } + }); + } + + // NEW: Check for sentences and create a story from them + const sentences = this.content.rawContent?.sentences || this.content.sentences; + if (sentences && Array.isArray(sentences) && sentences.length > 0 && this.availableStories.length === 0) { + const sentencesStory = this.convertSentencesToStory(sentences); + this.availableStories.push({ + id: 'sentences', + title: 'Reading Practice', + data: sentencesStory, + source: 'sentences' + }); + } + + logSh(`๐Ÿ“š Discovered ${this.availableStories.length} stories:`, this.availableStories.map(s => s.title), 'INFO'); + } + + selectStory(storyIndex) { + if (storyIndex >= 0 && storyIndex < this.availableStories.length) { + this.currentStoryIndex = storyIndex; + this.story = this.availableStories[storyIndex].data; + this.calculateTotalSentences(); + + // Reset reading position for new story + this.currentSentence = 0; + this.wordsRead = 0; + + // Update URL to include story parameter + this.updateUrlForStory(); + + logSh(`๐Ÿ“– Selected story: "${this.story.title}" (${this.totalSentences} sentences)`, 'INFO'); + } + } + + updateUrlForStory() { + const urlParams = new URLSearchParams(window.location.search); + urlParams.set('story', this.availableStories[this.currentStoryIndex].id); + const newUrl = `${window.location.pathname}?${urlParams.toString()}`; + window.history.replaceState({}, '', newUrl); + } + + showError(message) { + this.container.innerHTML = ` +
+

โŒ Error

+

${message}

+ +
+ `; + } + + calculateTotalSentences() { + this.totalSentences = 0; + this.story.chapters.forEach(chapter => { + this.totalSentences += chapter.sentences.length; + }); + } + + createInterface() { + // Create story selector dropdown if multiple stories available + const storySelector = this.availableStories.length > 1 ? ` +
+ + +
+ ` : ''; + + this.container.innerHTML = ` +
+ ${storySelector} + + +
+
+

${this.story.title}

+
+ Sentence 1 of ${this.totalSentences} +
+
+
+
+
+ +
+ + + +
+
+ + + + + +
+ Chapter 1: Loading... +
+ + +
+
+
Loading story...
+ +
+ + + +
+ + +
+ + + + +
+ + +
+
+ Words Read: + 0 +
+
+ Reading Time: + 00:00 +
+
+ Progress: + 0% +
+
+
+ `; + + this.addStyles(); + this.setupEventListeners(); + } + + addStyles() { + const style = document.createElement('style'); + style.textContent = ` + .story-reader-wrapper { + max-width: 800px; + margin: 0 auto; + padding: 20px; + font-family: 'Georgia', serif; + line-height: 1.6; + } + + .story-selector { + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 10px; + padding: 15px 20px; + margin-bottom: 25px; + display: flex; + align-items: center; + gap: 15px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } + + .story-selector label { + font-weight: 600; + color: #2d3748; + font-size: 1.1em; + min-width: 120px; + } + + .story-selector select { + flex: 1; + padding: 8px 12px; + border: 2px solid #cbd5e0; + border-radius: 6px; + background: white; + font-size: 1em; + color: #2d3748; + cursor: pointer; + transition: border-color 0.2s; + } + + .story-selector select:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + } + + .story-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 2px solid #e2e8f0; + } + + .story-title h2 { + margin: 0 0 10px 0; + color: #2d3748; + font-size: 1.8em; + } + + .reading-progress { + display: flex; + align-items: center; + gap: 10px; + } + + .progress-bar { + width: 200px; + height: 8px; + background: #e2e8f0; + border-radius: 4px; + overflow: hidden; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #3b82f6, #10b981); + width: 0%; + transition: width 0.3s ease; + } + + .story-controls { + display: flex; + gap: 10px; + flex-wrap: wrap; + } + + .control-btn { + padding: 8px 12px; + border: 2px solid #e2e8f0; + background: white; + border-radius: 6px; + cursor: pointer; + font-size: 0.9em; + transition: all 0.2s; + white-space: nowrap; + } + + .control-btn:hover { + background: #f7fafc; + border-color: #cbd5e0; + } + + .control-btn.secondary { + background: #f8fafc; + color: #4a5568; + } + + .control-btn.secondary:hover { + background: #e2e8f0; + color: #2d3748; + } + + .settings-panel { + background: #f7fafc; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 15px; + margin-bottom: 20px; + } + + .setting-group { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; + } + + .setting-group label { + font-weight: 600; + min-width: 100px; + } + + .chapter-info { + background: #edf2f7; + padding: 10px 15px; + border-radius: 6px; + margin-bottom: 20px; + font-style: italic; + color: #4a5568; + } + + .reading-area { + position: relative; + background: white; + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: 30px; + margin-bottom: 20px; + min-height: 200px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + } + + .sentence-display { + text-align: center; + } + + .original-text { + font-size: 1.2em; + color: #2d3748; + margin-bottom: 15px; + cursor: pointer; + padding: 15px; + border-radius: 8px; + transition: background-color 0.2s; + } + + .original-text:hover { + background-color: #f7fafc; + } + + .original-text.small { font-size: 1em; } + .original-text.medium { font-size: 1.2em; } + .original-text.large { font-size: 1.4em; } + .original-text.extra-large { font-size: 1.6em; } + + .translation-text { + font-style: italic; + color: #718096; + font-size: 1em; + padding: 10px; + background: #f0fff4; + border-radius: 6px; + border-left: 4px solid #10b981; + } + + .clickable-word { + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; + transition: background-color 0.2s; + position: relative; + display: inline-block; + } + + .clickable-word:hover { + background-color: #fef5e7; + color: #d69e2e; + } + + .punctuation { + color: #2d3748; + font-weight: normal; + cursor: default; + user-select: none; + } + + .word-with-pronunciation { + position: relative; + display: inline-block; + margin: 0 2px; + vertical-align: top; + line-height: 1.8; + } + + .pronunciation-text { + position: absolute; + top: -16px; + left: 50%; + transform: translateX(-50%); + font-size: 0.7em; + color: #718096; + font-style: italic; + white-space: nowrap; + pointer-events: none; + z-index: 10; + display: none; + } + + .pronunciation-text.show { + display: block; + } + + .reading-area { + position: relative; + background: white; + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: 40px 30px 30px 30px; + margin-bottom: 20px; + min-height: 200px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + line-height: 2.2; + } + + .word-popup { + position: fixed; + background: white; + border: 2px solid #3b82f6; + border-radius: 6px; + padding: 8px 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 9999; + max-width: 200px; + min-width: 120px; + font-size: 0.9em; + line-height: 1.3; + } + + .word-original { + font-weight: bold; + color: #2d3748; + font-size: 1em; + margin-bottom: 3px; + } + + .word-translation { + color: #10b981; + font-size: 0.9em; + margin-bottom: 2px; + } + + .word-type { + font-size: 0.75em; + color: #718096; + font-style: italic; + } + + .word-tts-btn { + position: absolute; + top: 5px; + right: 5px; + background: #3b82f6; + color: white; + border: none; + border-radius: 50%; + width: 24px; + height: 24px; + cursor: pointer; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + } + + .word-tts-btn:hover { + background: #2563eb; + } + + .story-navigation { + display: flex; + justify-content: center; + gap: 15px; + margin-bottom: 20px; + } + + .nav-btn { + padding: 12px 24px; + border: 2px solid #e2e8f0; + background: white; + border-radius: 8px; + cursor: pointer; + font-size: 1em; + transition: all 0.2s; + } + + .nav-btn:hover:not(:disabled) { + background: #f7fafc; + border-color: #cbd5e0; + } + + .nav-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .nav-btn.primary { + background: #3b82f6; + color: white; + border-color: #3b82f6; + } + + .nav-btn.primary:hover { + background: #2563eb; + } + + .reading-stats { + display: flex; + justify-content: space-around; + background: #f7fafc; + padding: 15px; + border-radius: 8px; + border: 1px solid #e2e8f0; + } + + .stat { + text-align: center; + } + + .stat-label { + display: block; + font-size: 0.9em; + color: #718096; + margin-bottom: 5px; + } + + .stat-value { + display: block; + font-weight: bold; + font-size: 1.1em; + color: #2d3748; + } + + @media (max-width: 768px) { + .story-reader-wrapper { + padding: 10px; + } + + .story-header { + flex-direction: column; + gap: 15px; + } + + .reading-stats { + flex-direction: column; + gap: 10px; + } + } + `; + document.head.appendChild(style); + } + + setupEventListeners() { + // Story selector (if multiple stories) + const storySelect = document.getElementById('story-select'); + if (storySelect) { + storySelect.addEventListener('change', (e) => this.changeStory(parseInt(e.target.value))); + } + + // Navigation + document.getElementById('prev-btn').addEventListener('click', () => this.previousSentence()); + document.getElementById('next-btn').addEventListener('click', () => this.nextSentence()); + document.getElementById('bookmark-btn').addEventListener('click', () => this.saveBookmark()); + + // Controls + document.getElementById('play-sentence-btn').addEventListener('click', () => this.playSentenceTTS()); + document.getElementById('settings-btn').addEventListener('click', () => this.toggleSettings()); + document.getElementById('toggle-translation-btn').addEventListener('click', () => this.toggleTranslations()); + document.getElementById('pronunciation-toggle-btn').addEventListener('click', () => this.togglePronunciations()); + + // Settings + document.getElementById('font-size-select').addEventListener('change', (e) => this.changeFontSize(e.target.value)); + document.getElementById('reading-mode-select').addEventListener('change', (e) => this.changeReadingMode(e.target.value)); + document.getElementById('auto-play-tts').addEventListener('change', (e) => this.toggleAutoPlayTTS(e.target.checked)); + document.getElementById('tts-speed-select').addEventListener('change', (e) => this.changeTTSSpeed(e.target.value)); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.key === 'ArrowLeft') this.previousSentence(); + if (e.key === 'ArrowRight') this.nextSentence(); + if (e.key === 'Space') { + e.preventDefault(); + this.nextSentence(); + } + if (e.key === 't' || e.key === 'T') this.toggleTranslations(); + if (e.key === 's' || e.key === 'S') this.playSentenceTTS(); + }); + + // Click outside to close word popup + document.addEventListener('click', (e) => { + if (!e.target.closest('.word-popup') && !e.target.closest('.clickable-word')) { + this.hideWordPopup(); + } + }); + } + + getCurrentSentenceData() { + let sentenceCount = 0; + for (let chapterIndex = 0; chapterIndex < this.story.chapters.length; chapterIndex++) { + const chapter = this.story.chapters[chapterIndex]; + if (sentenceCount + chapter.sentences.length > this.currentSentence) { + const sentenceInChapter = this.currentSentence - sentenceCount; + return { + chapter: chapterIndex, + sentence: sentenceInChapter, + data: chapter.sentences[sentenceInChapter], + chapterTitle: chapter.title + }; + } + sentenceCount += chapter.sentences.length; + } + return null; + } + + // Match words from sentence with centralized vocabulary + matchWordsWithVocabulary(sentence) { + const words = sentence.split(/(\s+|[.,!?;:"'()[\]{}\-โ€“โ€”])/); + const matchedWords = []; + + words.forEach(token => { + // Handle whitespace tokens + if (/^\s+$/.test(token)) { + matchedWords.push({ + original: token, + hasVocab: false, + isWhitespace: true + }); + return; + } + + // Handle pure punctuation tokens (preserve them as non-clickable) + if (/^[.,!?;:"'()[\]{}\-โ€“โ€”]+$/.test(token)) { + matchedWords.push({ + original: token, + hasVocab: false, + isPunctuation: true + }); + return; + } + + // Clean word (remove punctuation for matching) + const cleanWord = token.toLowerCase().replace(/[.,!?;:"'()[\]{}\-โ€“โ€”]/g, ''); + + // Skip empty tokens + if (!cleanWord) return; + + // Check if word exists in vocabulary (try exact match first, then stems) + let vocabEntry = this.content.vocabulary[cleanWord]; + + // Try common variations if exact match not found + if (!vocabEntry) { + // Try without 's' for plurals + if (cleanWord.endsWith('s')) { + vocabEntry = this.content.vocabulary[cleanWord.slice(0, -1)]; + } + // Try without 'ed' for past tense + if (!vocabEntry && cleanWord.endsWith('ed')) { + vocabEntry = this.content.vocabulary[cleanWord.slice(0, -2)]; + } + // Try without 'ing' for present participle + if (!vocabEntry && cleanWord.endsWith('ing')) { + vocabEntry = this.content.vocabulary[cleanWord.slice(0, -3)]; + } + } + + if (vocabEntry) { + // Word found in vocabulary + matchedWords.push({ + original: token, + hasVocab: true, + word: cleanWord, + translation: vocabEntry.translation || vocabEntry.user_language, + pronunciation: vocabEntry.pronunciation, + type: vocabEntry.type || 'unknown' + }); + } else { + // Word not in vocabulary - render as plain text + matchedWords.push({ + original: token, + hasVocab: false + }); + } + }); + + return matchedWords; + } + + renderCurrentSentence() { + const sentenceData = this.getCurrentSentenceData(); + if (!sentenceData) return; + + const { data, chapterTitle } = sentenceData; + + // Update chapter info + document.getElementById('chapter-info').innerHTML = ` + ${chapterTitle} + `; + + // Update progress + const progress = ((this.currentSentence + 1) / this.totalSentences) * 100; + document.getElementById('progress-fill').style.width = `${progress}%`; + document.getElementById('progress-text').textContent = `Sentence ${this.currentSentence + 1} of ${this.totalSentences}`; + document.getElementById('reading-percentage').textContent = `${Math.round(progress)}%`; + + // Check if sentence has word-by-word data (old format) or needs automatic matching + let wordsHtml; + + console.log('๐Ÿ” DEBUG: sentence data:', data); + console.log('๐Ÿ” DEBUG: data.words exists?', !!data.words); + console.log('๐Ÿ” DEBUG: data.words length:', data.words ? data.words.length : 'N/A'); + + if (data.words && data.words.length > 0) { + // Old format with word-by-word data + wordsHtml = data.words.map(wordData => { + const pronunciation = wordData.pronunciation || ''; + const pronunciationHtml = pronunciation ? + `${pronunciation}` : ''; + + return ` + ${pronunciationHtml} + ${wordData.word} + `; + }).join(' '); + } else { + // New format with centralized vocabulary - use automatic matching + const matchedWords = this.matchWordsWithVocabulary(data.original); + + wordsHtml = matchedWords.map(wordInfo => { + if (wordInfo.isWhitespace) { + return wordInfo.original; + } else if (wordInfo.isPunctuation) { + // Render punctuation as non-clickable text + return `${wordInfo.original}`; + } else if (wordInfo.hasVocab) { + const pronunciation = this.showPronunciation && wordInfo.pronunciation ? + wordInfo.pronunciation : ''; + const pronunciationHtml = pronunciation ? + `${pronunciation}` : ''; + + return ` + ${pronunciationHtml} + ${wordInfo.original} + `; + } else { + // No vocabulary entry - render as plain text + return wordInfo.original; + } + }).join(''); + } + + document.getElementById('original-text').innerHTML = wordsHtml; + document.getElementById('translation-text').textContent = data.translation; + + // Add word click listeners + document.querySelectorAll('.clickable-word').forEach(word => { + word.addEventListener('click', (e) => this.showWordPopup(e)); + }); + + // Update navigation buttons + document.getElementById('prev-btn').disabled = this.currentSentence === 0; + document.getElementById('next-btn').disabled = this.currentSentence >= this.totalSentences - 1; + + // Update stats + this.updateStats(); + + // Auto-play TTS if enabled + if (this.autoPlayTTS && this.ttsEnabled) { + // Small delay to let the sentence render + setTimeout(() => this.playSentenceTTS(), 300); + } + } + + showWordPopup(event) { + const word = event.target.dataset.word; + const translation = event.target.dataset.translation; + const type = event.target.dataset.type; + const pronunciation = event.target.dataset.pronunciation; + + logSh(`๐Ÿ” Word clicked: ${word}, translation: ${translation}`, 'DEBUG'); + + const popup = document.getElementById('word-popup'); + if (!popup) { + logSh('โŒ Word popup element not found!', 'ERROR'); + return; + } + + // Store reference to story reader for TTS button + popup.storyReader = this; + popup.currentWord = word; + + document.getElementById('popup-word').textContent = word; + document.getElementById('popup-translation').textContent = translation; + + // Show pronunciation in popup if available + const typeText = pronunciation ? `${pronunciation} (${type})` : `(${type})`; + document.getElementById('popup-type').textContent = typeText; + + // Position popup ABOVE the clicked word + const rect = event.target.getBoundingClientRect(); + popup.style.display = 'block'; + + // Center horizontally on the word, show above it + const popupLeft = rect.left + (rect.width / 2) - 100; // Center popup (200px wide / 2) + const popupTop = rect.top - 10; // Above the word with small gap + + popup.style.left = `${popupLeft}px`; + popup.style.top = `${popupTop}px`; + popup.style.transform = 'translateY(-100%)'; // Move up by its own height + + // Ensure popup stays within viewport + if (popupLeft < 10) { + popup.style.left = '10px'; + } + if (popupLeft + 200 > window.innerWidth) { + popup.style.left = `${window.innerWidth - 210}px`; + } + if (popupTop - 80 < 10) { + // If no room above, show below instead + popup.style.top = `${rect.bottom + 10}px`; + popup.style.transform = 'translateY(0)'; + } + + logSh(`๐Ÿ“ Popup positioned at: ${rect.left}px, ${rect.bottom + 10}px`, 'DEBUG'); + } + + hideWordPopup() { + document.getElementById('word-popup').style.display = 'none'; + } + + previousSentence() { + if (this.currentSentence > 0) { + this.currentSentence--; + this.renderCurrentSentence(); + this.saveProgress(); + } + } + + nextSentence() { + if (this.currentSentence < this.totalSentences - 1) { + this.currentSentence++; + this.renderCurrentSentence(); + this.saveProgress(); + } else { + this.completeReading(); + } + } + + toggleSettings() { + const panel = document.getElementById('settings-panel'); + panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; + } + + toggleTranslations() { + this.showTranslations = !this.showTranslations; + const translationText = document.getElementById('translation-text'); + translationText.style.display = this.showTranslations ? 'block' : 'none'; + + const btn = document.getElementById('toggle-translation-btn'); + btn.textContent = this.showTranslations ? '๐ŸŒ Hide Translations' : '๐ŸŒ Show Translations'; + } + + togglePronunciations() { + this.showPronunciations = !this.showPronunciations; + const pronunciations = document.querySelectorAll('.pronunciation-text'); + + pronunciations.forEach(pronunciation => { + if (this.showPronunciations) { + pronunciation.classList.add('show'); + } else { + pronunciation.classList.remove('show'); + } + }); + + const btn = document.getElementById('pronunciation-toggle-btn'); + btn.textContent = this.showPronunciations ? '๐Ÿ”‡ Hide Pronunciations' : '๐Ÿ”Š Show Pronunciations'; + } + + changeFontSize(size) { + this.fontSize = size; + document.getElementById('original-text').className = `original-text ${size}`; + } + + changeReadingMode(mode) { + this.readingMode = mode; + // Mode implementation can be extended later + } + + changeStory(storyIndex) { + if (storyIndex !== this.currentStoryIndex) { + // Save progress for current story before switching + this.saveProgress(); + + // Select new story + this.selectStory(storyIndex); + + // Load progress for new story + this.loadProgress(); + + // Update the interface title and progress + this.updateStoryTitle(); + this.renderCurrentSentence(); + + logSh(`๐Ÿ“– Switched to story: "${this.story.title}"`, 'INFO'); + } + } + + updateStoryTitle() { + const titleElement = document.querySelector('.story-title h2'); + if (titleElement) { + titleElement.textContent = this.story.title; + } + } + + // NEW: Convert simple text to story format + convertTextToStory(text, index) { + // Split text into sentences for easier reading + const sentences = this.splitTextIntoSentences(text.original_language, text.user_language); + + return { + title: text.title || `Text ${index + 1}`, + totalSentences: sentences.length, + chapters: [{ + title: "Reading Text", + sentences: sentences + }] + }; + } + + // NEW: Convert array of sentences to story format + convertSentencesToStory(sentences) { + const storyTitle = this.content.name || "Reading Practice"; + + const convertedSentences = sentences.map((sentence, index) => ({ + id: index + 1, + original: sentence.original_language || sentence.english || sentence.original || '', + translation: sentence.user_language || sentence.chinese || sentence.french || sentence.translation || '', + words: this.breakSentenceIntoWords( + sentence.original_language || sentence.english || sentence.original || '', + sentence.user_language || sentence.chinese || sentence.french || sentence.translation || '' + ) + })); + + return { + title: storyTitle, + totalSentences: convertedSentences.length, + chapters: [{ + title: "Reading Sentences", + sentences: convertedSentences + }] + }; + } + + // NEW: Split long text into manageable sentences + splitTextIntoSentences(originalText, translationText) { + // Split by sentence endings + const originalSentences = originalText.split(/[.!?]+/).filter(s => s.trim().length > 0); + const translationSentences = translationText.split(/[.!?]+/).filter(s => s.trim().length > 0); + + const sentences = []; + const maxSentences = Math.max(originalSentences.length, translationSentences.length); + + for (let i = 0; i < maxSentences; i++) { + const original = (originalSentences[i] || '').trim(); + const translation = (translationSentences[i] || '').trim(); + + if (original || translation) { + sentences.push({ + id: i + 1, + original: original + (original && !original.match(/[.!?]$/) ? '.' : ''), + translation: translation + (translation && !translation.match(/[.!?]$/) ? '.' : ''), + words: this.breakSentenceIntoWords(original, translation) + }); + } + } + + return sentences; + } + + // NEW: Break sentence into word-by-word format for Story Reader + breakSentenceIntoWords(original, translation) { + if (!original) return []; + + // First, separate punctuation from words while preserving spaces + const preprocessed = original.replace(/([.,!?;:"'()[\]{}\-โ€“โ€”])/g, ' $1 '); + const words = preprocessed.split(/\s+/).filter(word => word.trim().length > 0); + + // Do the same for translation + const translationPreprocessed = translation ? translation.replace(/([.,!?;:"'()[\]{}\-โ€“โ€”])/g, ' $1 ') : ''; + const translationWords = translationPreprocessed ? translationPreprocessed.split(/\s+/).filter(word => word.trim().length > 0) : []; + + return words.map((word, index) => { + // Clean punctuation for word lookup, but preserve punctuation in display + const cleanWord = word.replace(/[.,!?;:"'()[\]{}\-โ€“โ€”]/g, '').toLowerCase(); + + // Try to find in vocabulary + let wordTranslation = translationWords[index] || ''; + let wordType = 'word'; + let pronunciation = ''; + + // Special handling for letter pairs (like "Aa", "Bb", etc.) + if (/^[A-Za-z]{1,2}$/.test(cleanWord)) { + wordType = 'letter'; + wordTranslation = word; // Keep the letter as is + } + + // Special handling for punctuation marks + if (/^[.,!?;:"'()[\]{}]$/.test(word)) { + wordType = 'punctuation'; + wordTranslation = word; // Keep punctuation as is + } + + // Look up in content vocabulary if available + if (this.vocabulary && this.vocabulary[cleanWord]) { + const vocabEntry = this.vocabulary[cleanWord]; + wordTranslation = vocabEntry.user_language || vocabEntry.translation || wordTranslation; + wordType = vocabEntry.type || wordType; + pronunciation = vocabEntry.pronunciation || ''; + } + + return { + word: word, + translation: wordTranslation, + type: wordType, + pronunciation: pronunciation + }; + }); + } + + // TTS Methods + playSentenceTTS() { + const sentenceData = this.getCurrentSentenceData(); + if (!sentenceData || !this.ttsEnabled) return; + + const text = sentenceData.data.original; + this.speakText(text); + } + + speakText(text, options = {}) { + if (!text || !this.ttsEnabled) return; + + // Use SettingsManager if available for better language support + if (window.SettingsManager && window.SettingsManager.speak) { + const ttsOptions = { + lang: this.getContentLanguage(), + rate: parseFloat(document.getElementById('tts-speed-select')?.value || '0.8'), + ...options + }; + + window.SettingsManager.speak(text, ttsOptions) + .catch(error => { + console.warn('๐Ÿ”Š SettingsManager TTS failed:', error); + this.fallbackTTS(text, ttsOptions); + }); + } else { + this.fallbackTTS(text, options); + } + } + + fallbackTTS(text, options = {}) { + if ('speechSynthesis' in window && text) { + // Cancel any ongoing speech + speechSynthesis.cancel(); + + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = this.getContentLanguage(); + utterance.rate = options.rate || 0.8; + utterance.volume = 1.0; + + speechSynthesis.speak(utterance); + } + } + + getContentLanguage() { + // Get language from content or use sensible defaults + if (this.content.language) { + const langMap = { + 'chinese': 'zh-CN', + 'english': 'en-US', + 'french': 'fr-FR', + 'spanish': 'es-ES' + }; + return langMap[this.content.language] || this.content.language; + } + return 'en-US'; // Default fallback + } + + toggleAutoPlayTTS(enabled) { + this.autoPlayTTS = enabled; + logSh(`๐Ÿ”Š Auto-play TTS ${enabled ? 'enabled' : 'disabled'}`, 'INFO'); + } + + changeTTSSpeed(speed) { + logSh(`๐Ÿ”Š TTS speed changed to ${speed}x`, 'INFO'); + } + + speakWordFromPopup() { + const popup = document.getElementById('word-popup'); + if (popup && popup.currentWord) { + this.speakText(popup.currentWord, { rate: 0.7 }); // Slower for individual words + } + } + + updateStats() { + const sentenceData = this.getCurrentSentenceData(); + if (sentenceData) { + this.wordsRead += sentenceData.data.words.length; + document.getElementById('words-read').textContent = this.wordsRead; + } + } + + saveProgress() { + const progressData = { + currentSentence: this.currentSentence, + wordsRead: this.wordsRead, + timestamp: Date.now() + }; + const progressKey = this.getProgressKey(); + localStorage.setItem(progressKey, JSON.stringify(progressData)); + } + + loadProgress() { + const progressKey = this.getProgressKey(); + const saved = localStorage.getItem(progressKey); + if (saved) { + try { + const data = JSON.parse(saved); + this.currentSentence = data.currentSentence || 0; + this.wordsRead = data.wordsRead || 0; + } catch (error) { + logSh('Error loading progress:', error, 'WARN'); + this.currentSentence = 0; + this.wordsRead = 0; + } + } else { + // No saved progress - start fresh + this.currentSentence = 0; + this.wordsRead = 0; + } + } + + getProgressKey() { + const storyId = this.availableStories[this.currentStoryIndex]?.id || 'main'; + return `story_progress_${this.content.name}_${storyId}`; + } + + saveBookmark() { + this.saveProgress(); + const toast = document.createElement('div'); + toast.textContent = '๐Ÿ”– Bookmark saved!'; + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: #10b981; + color: white; + padding: 10px 20px; + border-radius: 6px; + z-index: 1000; + `; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2000); + } + + completeReading() { + this.onGameEnd(this.wordsRead); + + const completionMessage = ` +
+

๐ŸŽ‰ Story Complete!

+

You've finished reading "${this.story.title}"

+

Words read: ${this.wordsRead}

+

Total sentences: ${this.totalSentences}

+ + +
+ `; + + document.getElementById('reading-area').innerHTML = completionMessage; + } + + start() { + logSh('๐Ÿ“– Story Reader: Starting', 'INFO'); + this.startReadingTimer(); + } + + startReadingTimer() { + this.startTime = Date.now(); + this.readingTimer = setInterval(() => { + this.updateReadingTime(); + }, 1000); + } + + updateReadingTime() { + const currentTime = Date.now(); + this.totalReadingTime = Math.floor((currentTime - this.startTime) / 1000); + + const minutes = Math.floor(this.totalReadingTime / 60); + const seconds = this.totalReadingTime % 60; + const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + document.getElementById('reading-time').textContent = timeString; + } + + restart() { + this.currentSentence = 0; + this.wordsRead = 0; + // Restart reading timer + if (this.readingTimer) { + clearInterval(this.readingTimer); + } + this.startReadingTimer(); + this.renderCurrentSentence(); + this.saveProgress(); + } + + destroy() { + // Clean up timer + if (this.readingTimer) { + clearInterval(this.readingTimer); + } + this.container.innerHTML = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.StoryReader = StoryReader; \ No newline at end of file diff --git a/src/games/whack-a-mole-hard.js b/src/games/whack-a-mole-hard.js new file mode 100644 index 0000000..6eee5dc --- /dev/null +++ b/src/games/whack-a-mole-hard.js @@ -0,0 +1,703 @@ +// === 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 || (() => {}); + + // Game state + this.score = 0; + this.errors = 0; + this.maxErrors = 3; + this.gameTime = 60; // 60 seconds + this.timeLeft = this.gameTime; + this.isRunning = false; + this.gameMode = 'translation'; // 'translation', 'image', 'sound' + this.showPronunciation = false; // Track pronunciation display state + + // Mole configuration + this.holes = []; + this.activeMoles = []; + this.moleAppearTime = 3000; // 3 seconds display time (longer) + this.spawnRate = 2000; // New wave every 2 seconds + this.molesPerWave = 3; // 3 moles per wave + + // Timers + this.gameTimer = null; + this.spawnTimer = null; + + // Vocabulary for this game - adapted for the new system + this.vocabulary = this.extractVocabulary(this.content); + this.currentWords = []; + this.targetWord = null; + + // Target word guarantee system + this.spawnsSinceTarget = 0; + this.maxSpawnsWithoutTarget = 10; // Target word must appear in the next 10 moles (1/10 chance) + + this.init(); + } + + init() { + // Check that we have vocabulary + if (!this.vocabulary || this.vocabulary.length === 0) { + logSh('No vocabulary available for Whack-a-Mole', 'ERROR'); + this.showInitError(); + return; + } + + this.createGameBoard(); + this.createGameUI(); + this.setupEventListeners(); + } + + showInitError() { + this.container.innerHTML = ` +
+

โŒ Loading Error

+

This content does not contain vocabulary compatible with Whack-a-Mole.

+

The game requires words with their translations.

+ +
+ `; + } + + createGameBoard() { + this.container.innerHTML = ` +
+ +
+ + + +
+ + +
+
+
+ ${this.timeLeft} + Time +
+
+ ${this.errors} + Errors +
+
+ --- + Find +
+
+
+ + + + +
+
+ + +
+ +
+ + + +
+ `; + + this.createHoles(); + } + + createHoles() { + const gameBoard = document.getElementById('game-board'); + gameBoard.innerHTML = ''; + + for (let i = 0; i < 15; i++) { // 5x3 = 15 holes + const hole = document.createElement('div'); + hole.className = 'whack-hole'; + hole.dataset.holeId = i; + + hole.innerHTML = ` +
+ +
+
+ `; + + gameBoard.appendChild(hole); + this.holes.push({ + element: hole, + mole: hole.querySelector('.whack-mole'), + wordElement: hole.querySelector('.word'), + pronunciationElement: hole.querySelector('.pronunciation'), + isActive: false, + word: null, + timer: null + }); + } + } + + createGameUI() { + // UI elements are already created in 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('pronunciation-btn').addEventListener('click', () => this.togglePronunciation()); + 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.translation}"`, 'info'); + + // Show loaded content info + const contentName = this.content.name || 'Content'; + logSh(`๐ŸŽฎ Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words, 'INFO');`); + } + + 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(); // Stop without triggering game end + this.resetGame(); + setTimeout(() => this.start(), 100); + } + + togglePronunciation() { + this.showPronunciation = !this.showPronunciation; + const btn = document.getElementById('pronunciation-btn'); + + if (this.showPronunciation) { + btn.textContent = '๐Ÿ”Š Pronunciation ON'; + btn.classList.add('active'); + } else { + btn.textContent = '๐Ÿ”Š Pronunciation OFF'; + btn.classList.remove('active'); + } + + // Update currently visible moles + this.updateMoleDisplay(); + } + + updateMoleDisplay() { + // Update pronunciation display for all active moles + this.holes.forEach(hole => { + if (hole.isActive && hole.word) { + if (this.showPronunciation && hole.word.pronunciation) { + hole.pronunciationElement.textContent = hole.word.pronunciation; + hole.pronunciationElement.style.display = 'block'; + } else { + hole.pronunciationElement.style.display = 'none'; + } + } + }); + } + + stop() { + this.stopWithoutEnd(); + this.onGameEnd(this.score); // Trigger game end only here + } + + stopWithoutEnd() { + this.isRunning = false; + this.stopTimers(); + this.hideAllMoles(); + + document.getElementById('start-btn').disabled = false; + document.getElementById('pause-btn').disabled = true; + } + + resetGame() { + // Ensure everything is completely stopped + this.stopWithoutEnd(); + + // Reset all state variables + this.score = 0; + this.errors = 0; + this.timeLeft = this.gameTime; + this.isRunning = false; + this.targetWord = null; + this.activeMoles = []; + this.spawnsSinceTarget = 0; // Reset guarantee counter + + // Ensure all timers are properly stopped + 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 with verification + 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.pronunciationElement) { + hole.pronunciationElement.textContent = ''; + hole.pronunciationElement.style.display = 'none'; + } + if (hole.mole) { + hole.mole.classList.remove('active', 'hit'); + } + }); + + logSh('๐Ÿ”„ Game completely reset', 'INFO'); + } + + startTimers() { + // Main game timer + this.gameTimer = setInterval(() => { + this.timeLeft--; + this.updateUI(); + + if (this.timeLeft <= 0 && this.isRunning) { + this.stop(); + } + }, 1000); + + // Mole spawn timer + this.spawnTimer = setInterval(() => { + if (this.isRunning) { + this.spawnMole(); + } + }, this.spawnRate); + + // First immediate mole + setTimeout(() => this.spawnMole(), 500); + } + + stopTimers() { + if (this.gameTimer) { + clearInterval(this.gameTimer); + this.gameTimer = null; + } + if (this.spawnTimer) { + clearInterval(this.spawnTimer); + this.spawnTimer = null; + } + } + + spawnMole() { + // Hard mode: Spawn 3 moles at once + this.spawnMultipleMoles(); + } + + spawnMultipleMoles() { + // Find all free holes + const availableHoles = this.holes.filter(hole => !hole.isActive); + + // Spawn up to 3 moles (or fewer if not enough free holes) + const molesToSpawn = Math.min(this.molesPerWave, availableHoles.length); + + if (molesToSpawn === 0) return; + + // Shuffle available holes + const shuffledHoles = this.shuffleArray(availableHoles); + + // Spawn the moles + for (let i = 0; i < molesToSpawn; i++) { + const hole = shuffledHoles[i]; + const holeIndex = this.holes.indexOf(hole); + + // Choose a word according to guarantee strategy + const word = this.getWordWithTargetGuarantee(); + + // Activate the mole with a small delay for visual effect + setTimeout(() => { + if (this.isRunning && !hole.isActive) { + this.activateMole(holeIndex, word); + } + }, i * 200); // 200ms delay between each mole + } + } + + getWordWithTargetGuarantee() { + // Increment spawn counter since last target word + this.spawnsSinceTarget++; + + // If we've reached the limit, force the target word + if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) { + logSh(`๐ŸŽฏ Forced target word spawn after ${this.spawnsSinceTarget} attempts`, 'INFO'); + this.spawnsSinceTarget = 0; + return this.targetWord; + } + + // Otherwise, 10% chance for target word (1/10 instead of 1/2) + if (Math.random() < 0.1) { + logSh('๐ŸŽฏ Natural target word spawn (1/10)', 'INFO'); + 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.original; + + // Show pronunciation if enabled and available + if (this.showPronunciation && word.pronunciation) { + hole.pronunciationElement.textContent = word.pronunciation; + hole.pronunciationElement.style.display = 'block'; + } else { + hole.pronunciationElement.style.display = 'none'; + } + + hole.mole.classList.add('active'); + + // Add to active moles list + this.activeMoles.push(holeIndex); + + // Timer to make the mole disappear + 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.pronunciationElement.textContent = ''; + hole.pronunciationElement.style.display = 'none'; + hole.mole.classList.remove('active'); + + if (hole.timer) { + clearTimeout(hole.timer); + hole.timer = null; + } + + // Remove from active moles list + 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.translation === this.targetWord.translation; + + if (isCorrect) { + // Correct answer + this.score += 10; + this.deactivateMole(holeIndex); + this.setNewTarget(); + this.showScorePopup(holeIndex, '+10', true); + this.showFeedback(`Well done! Now find: "${this.targetWord.translation}"`, '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.translation}" โ‰  "${this.targetWord.translation}"`, '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() { + // Choose a new target word + const availableWords = this.vocabulary.filter(word => + !this.activeMoles.some(moleIndex => + this.holes[moleIndex].word && + this.holes[moleIndex].word.original === word.original + ) + ); + + 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 counter for new target word + this.spawnsSinceTarget = 0; + logSh(`๐ŸŽฏ New target word: ${this.targetWord.original} -> ${this.targetWord.translation}`, 'INFO'); + + document.getElementById('target-word').textContent = this.targetWord.translation; + } + + 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 = `
${message}
`; + } + + updateUI() { + document.getElementById('time-left').textContent = this.timeLeft; + document.getElementById('errors-count').textContent = this.errors; + } + + extractVocabulary(content) { + let vocabulary = []; + + logSh('๐Ÿ” Extracting vocabulary from:', content?.name || 'content', 'INFO'); + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + logSh('๐Ÿ“ฆ Using raw module content', 'INFO'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + logSh('โœจ Ultra-modular format detected (vocabulary object)', 'INFO'); + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + // No other formats supported - ultra-modular only + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + logSh('๐Ÿ”ง Extracting from raw content:', rawContent.name || 'Module', 'INFO'); + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format ONLY + if (typeof data === 'object' && data.user_language) { + return { + original: word, // Clรฉ = original_language + translation: data.user_language.split('๏ผ›')[0], // First translation + fullTranslation: data.user_language, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + logSh(`โœจ ${vocabulary.length} words extracted from ultra-modular vocabulary`, 'INFO'); + } + // No other formats supported - ultra-modular only + else { + logSh('โš ๏ธ Content format not supported - ultra-modular format required', 'WARN'); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + if (vocabulary.length === 0) { + logSh('โŒ No valid vocabulary found', 'ERROR'); + // Demo vocabulary as last resort + vocabulary = [ + { original: 'hello', translation: 'bonjour', category: 'greetings' }, + { original: 'goodbye', translation: 'au revoir', category: 'greetings' }, + { original: 'thank you', translation: 'merci', category: 'greetings' }, + { original: 'cat', translation: 'chat', category: 'animals' }, + { original: 'dog', translation: 'chien', category: 'animals' } + ]; + logSh('๐Ÿšจ Using demo vocabulary', 'WARN'); + } + + logSh(`โœ… Whack-a-Mole: ${vocabulary.length} vocabulary words finalized`, 'INFO'); + 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 = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.WhackAMoleHard = WhackAMoleHardGame; \ No newline at end of file diff --git a/src/games/whack-a-mole.js b/src/games/whack-a-mole.js new file mode 100644 index 0000000..fc69242 --- /dev/null +++ b/src/games/whack-a-mole.js @@ -0,0 +1,685 @@ +// === MODULE WHACK-A-MOLE === + +class WhackAMoleGame { + constructor(options) { + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Game state + this.score = 0; + this.errors = 0; + this.maxErrors = 3; + this.gameTime = 60; // 60 secondes + this.timeLeft = this.gameTime; + this.isRunning = false; + this.gameMode = 'translation'; // 'translation', 'image', 'sound' + this.showPronunciation = false; // Track pronunciation display state + + // Mole configuration + this.holes = []; + this.activeMoles = []; + this.moleAppearTime = 2000; // 2 seconds display time + this.spawnRate = 1500; // New mole every 1.5 seconds + + // Timers + this.gameTimer = null; + this.spawnTimer = null; + + // Vocabulary for this game - adapted for the new system + this.vocabulary = this.extractVocabulary(this.content); + this.currentWords = []; + this.targetWord = null; + + // Target word guarantee system + this.spawnsSinceTarget = 0; + this.maxSpawnsWithoutTarget = 3; // Target word must appear in the next 3 moles + + this.init(); + } + + init() { + // Check that we have vocabulary + if (!this.vocabulary || this.vocabulary.length === 0) { + logSh('No vocabulary available for Whack-a-Mole', 'ERROR'); + this.showInitError(); + return; + } + + this.createGameBoard(); + this.createGameUI(); + this.setupEventListeners(); + } + + showInitError() { + this.container.innerHTML = ` +
+

โŒ Loading Error

+

This content does not contain vocabulary compatible with Whack-a-Mole.

+

The game requires words with their translations.

+ +
+ `; + } + + createGameBoard() { + this.container.innerHTML = ` +
+ +
+ + + +
+ + +
+
+
+ ${this.timeLeft} + Time +
+
+ ${this.errors} + Errors +
+
+ --- + Find +
+
+
+ + + + +
+
+ + +
+ +
+ + + +
+ `; + + 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 = ` +
+ +
+
+ `; + + gameBoard.appendChild(hole); + this.holes.push({ + element: hole, + mole: hole.querySelector('.whack-mole'), + wordElement: hole.querySelector('.word'), + pronunciationElement: hole.querySelector('.pronunciation'), + isActive: false, + word: null, + timer: null + }); + } + } + + createGameUI() { + // UI elements are already created in 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('pronunciation-btn').addEventListener('click', () => this.togglePronunciation()); + 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.translation}"`, 'info'); + + // Show loaded content info + const contentName = this.content.name || 'Content'; + logSh(`๐ŸŽฎ Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words, 'INFO');`); + } + + 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(); // Stop without triggering game end + this.resetGame(); + setTimeout(() => this.start(), 100); + } + + togglePronunciation() { + this.showPronunciation = !this.showPronunciation; + const btn = document.getElementById('pronunciation-btn'); + + if (this.showPronunciation) { + btn.textContent = '๐Ÿ”Š Pronunciation ON'; + btn.classList.add('active'); + } else { + btn.textContent = '๐Ÿ”Š Pronunciation OFF'; + btn.classList.remove('active'); + } + + // Update currently visible moles + this.updateMoleDisplay(); + } + + updateMoleDisplay() { + // Update pronunciation display for all active moles + this.holes.forEach(hole => { + if (hole.isActive && hole.word) { + if (this.showPronunciation && hole.word.pronunciation) { + hole.pronunciationElement.textContent = hole.word.pronunciation; + hole.pronunciationElement.style.display = 'block'; + } else { + hole.pronunciationElement.style.display = 'none'; + } + } + }); + } + + stop() { + this.stopWithoutEnd(); + this.onGameEnd(this.score); // Trigger game end only here + } + + stopWithoutEnd() { + this.isRunning = false; + this.stopTimers(); + this.hideAllMoles(); + + document.getElementById('start-btn').disabled = false; + document.getElementById('pause-btn').disabled = true; + } + + resetGame() { + // Ensure everything is completely stopped + this.stopWithoutEnd(); + + // Reset all state variables + this.score = 0; + this.errors = 0; + this.timeLeft = this.gameTime; + this.isRunning = false; + this.targetWord = null; + this.activeMoles = []; + this.spawnsSinceTarget = 0; // Reset guarantee counter + + // Ensure all timers are properly stopped + 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 with verification + 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.pronunciationElement) { + hole.pronunciationElement.textContent = ''; + hole.pronunciationElement.style.display = 'none'; + } + if (hole.mole) { + hole.mole.classList.remove('active', 'hit'); + } + }); + + logSh('๐Ÿ”„ Game completely reset', 'INFO'); + } + + startTimers() { + // Main game timer + this.gameTimer = setInterval(() => { + this.timeLeft--; + this.updateUI(); + + if (this.timeLeft <= 0 && this.isRunning) { + this.stop(); + } + }, 1000); + + // Mole spawn timer + this.spawnTimer = setInterval(() => { + if (this.isRunning) { + this.spawnMole(); + } + }, this.spawnRate); + + // First immediate mole + setTimeout(() => this.spawnMole(), 500); + } + + stopTimers() { + if (this.gameTimer) { + clearInterval(this.gameTimer); + this.gameTimer = null; + } + if (this.spawnTimer) { + clearInterval(this.spawnTimer); + this.spawnTimer = null; + } + } + + spawnMole() { + // Find a free hole + 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); + + // Choose a word according to guarantee strategy + const word = this.getWordWithTargetGuarantee(); + + // Activate the mole + this.activateMole(holeIndex, word); + } + + getWordWithTargetGuarantee() { + // Increment spawn counter since last target word + this.spawnsSinceTarget++; + + // If we've reached the limit, force the target word + if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) { + logSh(`๐ŸŽฏ Forced target word spawn after ${this.spawnsSinceTarget} attempts`, 'INFO'); + this.spawnsSinceTarget = 0; + return this.targetWord; + } + + // Otherwise, 50% chance for target word, 50% random word + if (Math.random() < 0.5) { + logSh('๐ŸŽฏ Natural target word spawn', 'INFO'); + 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.original; + + // Show pronunciation if enabled and available + if (this.showPronunciation && word.pronunciation) { + hole.pronunciationElement.textContent = word.pronunciation; + hole.pronunciationElement.style.display = 'block'; + } else { + hole.pronunciationElement.style.display = 'none'; + } + + hole.mole.classList.add('active'); + + // Add to active moles list + this.activeMoles.push(holeIndex); + + // Timer to make the mole disappear + 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.pronunciationElement.textContent = ''; + hole.pronunciationElement.style.display = 'none'; + hole.mole.classList.remove('active'); + + if (hole.timer) { + clearTimeout(hole.timer); + hole.timer = null; + } + + // Remove from active moles list + 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.translation === this.targetWord.translation; + + if (isCorrect) { + // Correct answer + this.score += 10; + this.deactivateMole(holeIndex); + this.setNewTarget(); + this.showScorePopup(holeIndex, '+10', true); + this.showFeedback(`Well done! Now find: "${this.targetWord.translation}"`, '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.translation}" โ‰  "${this.targetWord.translation}"`, '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() { + // Choose a new target word + const availableWords = this.vocabulary.filter(word => + !this.activeMoles.some(moleIndex => + this.holes[moleIndex].word && + this.holes[moleIndex].word.original === word.original + ) + ); + + 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 counter for new target word + this.spawnsSinceTarget = 0; + logSh(`๐ŸŽฏ New target word: ${this.targetWord.original} -> ${this.targetWord.translation}`, 'INFO'); + + document.getElementById('target-word').textContent = this.targetWord.translation; + } + + 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 = `
${message}
`; + } + + updateUI() { + document.getElementById('time-left').textContent = this.timeLeft; + document.getElementById('errors-count').textContent = this.errors; + } + + extractVocabulary(content) { + let vocabulary = []; + + logSh('๐Ÿ” Extracting vocabulary from:', content?.name || 'content', 'INFO'); + + // Priority 1: Use raw module content (simple format) + if (content.rawContent) { + logSh('๐Ÿ“ฆ Using raw module content', 'INFO'); + return this.extractVocabularyFromRaw(content.rawContent); + } + + // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported + if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { + logSh('โœจ Ultra-modular format detected (vocabulary object)', 'INFO'); + vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { + // Support ultra-modular format and new centralized vocabulary format + if (typeof data === 'object' && (data.user_language || data.translation)) { + const translationText = data.user_language || data.translation; + return { + original: word, // Clรฉ = original_language + translation: translationText.split('๏ผ›')[0], // First translation + fullTranslation: translationText, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + } + // No other formats supported - ultra-modular only + + return this.finalizeVocabulary(vocabulary); + } + + extractVocabularyFromRaw(rawContent) { + logSh('๐Ÿ”ง Extracting from raw content:', rawContent.name || 'Module', 'INFO'); + let vocabulary = []; + + // Ultra-modular format (vocabulary object) - ONLY format supported + if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { + vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { + // Support ultra-modular format and new centralized vocabulary format + if (typeof data === 'object' && (data.user_language || data.translation)) { + const translationText = data.user_language || data.translation; + return { + original: word, // Clรฉ = original_language + translation: translationText.split('๏ผ›')[0], // First translation + fullTranslation: translationText, // Complete translation + type: data.type || 'general', + audio: data.audio, + image: data.image, + examples: data.examples, + pronunciation: data.pronunciation, + category: data.type || 'general' + }; + } + // Legacy fallback - simple string (temporary, will be removed) + else if (typeof data === 'string') { + return { + original: word, + translation: data.split('๏ผ›')[0], + fullTranslation: data, + type: 'general', + category: 'general' + }; + } + return null; + }).filter(Boolean); + logSh(`โœจ ${vocabulary.length} words extracted from ultra-modular vocabulary`, 'INFO'); + } + // No other formats supported - ultra-modular only + else { + logSh('โš ๏ธ Content format not supported - ultra-modular format required', 'WARN'); + } + + return this.finalizeVocabulary(vocabulary); + } + + finalizeVocabulary(vocabulary) { + // Validation and cleanup for ultra-modular format + vocabulary = vocabulary.filter(word => + word && + typeof word.original === 'string' && + typeof word.translation === 'string' && + word.original.trim() !== '' && + word.translation.trim() !== '' + ); + + if (vocabulary.length === 0) { + logSh('โŒ No valid vocabulary found', 'ERROR'); + // Demo vocabulary as last resort + vocabulary = [ + { original: 'hello', translation: 'bonjour', category: 'greetings' }, + { original: 'goodbye', translation: 'au revoir', category: 'greetings' }, + { original: 'thank you', translation: 'merci', category: 'greetings' }, + { original: 'cat', translation: 'chat', category: 'animals' }, + { original: 'dog', translation: 'chien', category: 'animals' } + ]; + logSh('๐Ÿšจ Using demo vocabulary', 'WARN'); + } + + logSh(`โœ… Whack-a-Mole: ${vocabulary.length} vocabulary words finalized`, 'INFO'); + 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 = ''; + } +} + +// Module registration +window.GameModules = window.GameModules || {}; +window.GameModules.WhackAMole = WhackAMoleGame; \ No newline at end of file diff --git a/src/games/wizard-spell-caster.js b/src/games/wizard-spell-caster.js new file mode 100644 index 0000000..b6ec306 --- /dev/null +++ b/src/games/wizard-spell-caster.js @@ -0,0 +1,1881 @@ +// === WIZARD SPELL CASTER GAME === +// Advanced game for 11+ years old - Form sentences to cast magical spells + +class WizardSpellCaster { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.container = container; + this.content = content; + this.onScoreUpdate = onScoreUpdate; + this.onGameEnd = onGameEnd; + + this.score = 0; + this.enemyHP = 100; + this.playerHP = 100; + this.currentSpells = []; + this.selectedWords = []; + + // Timer invisible pour bonus de vitesse + this.spellStartTime = null; + this.averageSpellTime = 0; + this.spellCount = 0; + + // Enemy attack system + this.enemyAttackTimer = null; + this.nextEnemyAttack = this.getRandomAttackTime(); + + this.injectCSS(); + this.extractSpells(); + this.init(); + } + + injectCSS() { + if (document.getElementById('wizard-spell-caster-styles')) return; + + const styleSheet = document.createElement('style'); + styleSheet.id = 'wizard-spell-caster-styles'; + styleSheet.textContent = ` + .wizard-game-wrapper { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); + min-height: 100vh; + color: white; + font-family: 'Fantasy', serif; + position: relative; + overflow: hidden; + } + + .wizard-hud { + display: flex; + justify-content: space-between; + padding: 15px; + background: rgba(0,0,0,0.3); + border-bottom: 2px solid #ffd700; + } + + .wizard-stats { + display: flex; + gap: 20px; + align-items: center; + } + + .health-bar { + width: 150px; + height: 20px; + background: rgba(255,255,255,0.2); + border-radius: 10px; + overflow: hidden; + border: 2px solid #ffd700; + } + + .health-fill { + height: 100%; + background: linear-gradient(90deg, #ff4757, #ff6b7a); + transition: width 0.3s ease; + } + + .battle-area { + display: flex; + height: 60vh; + padding: 20px; + } + + .wizard-side { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .enemy-side { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + .wizard-character { + width: 120px; + height: 120px; + background: linear-gradient(45deg, #6c5ce7, #a29bfe); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 48px; + margin-bottom: 20px; + animation: float 3s ease-in-out infinite; + box-shadow: 0 0 30px rgba(108, 92, 231, 0.6); + } + + .enemy-character { + width: 150px; + height: 150px; + background: linear-gradient(45deg, #ff4757, #ff6b7a); + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 64px; + margin-bottom: 20px; + animation: enemyPulse 2s ease-in-out infinite; + box-shadow: 0 0 40px rgba(255, 71, 87, 0.6); + } + + @keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } + } + + @keyframes enemyPulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } + } + + .spell-casting-area { + background: rgba(0,0,0,0.4); + border: 2px solid #ffd700; + border-radius: 15px; + padding: 20px; + margin: 20px; + } + + .spell-selection { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; + margin-bottom: 20px; + } + + .spell-card { + background: linear-gradient(135deg, #2c2c54, #40407a); + border: 2px solid #ffd700; + border-radius: 10px; + padding: 15px; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + } + + .spell-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(255, 215, 0, 0.3); + border-color: #fff; + } + + .spell-card.selected { + background: linear-gradient(135deg, #ffd700, #ffed4e); + color: #000; + transform: scale(1.05); + } + + .spell-type { + font-size: 12px; + color: #ffd700; + font-weight: bold; + margin-bottom: 5px; + } + + .spell-damage { + font-size: 14px; + color: #ff6b7a; + font-weight: bold; + } + + .sentence-builder { + background: rgba(255,255,255,0.1); + border-radius: 10px; + padding: 15px; + margin-bottom: 20px; + min-height: 80px; + border: 2px dashed #ffd700; + } + + .word-bank { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; + } + + .word-tile { + background: linear-gradient(135deg, #5f27cd, #8854d0); + color: white; + padding: 8px 15px; + border-radius: 20px; + cursor: grab; + user-select: none; + transition: all 0.3s ease; + border: 2px solid transparent; + } + + .word-tile:hover { + transform: scale(1.1); + box-shadow: 0 5px 15px rgba(95, 39, 205, 0.4); + } + + .word-tile.selected { + background: linear-gradient(135deg, #ffd700, #ffed4e); + color: #000; + border-color: #fff; + } + + .word-tile:active { + cursor: grabbing; + } + + .cast-button { + background: linear-gradient(135deg, #ff6b7a, #ff4757); + border: none; + color: white; + padding: 15px 30px; + border-radius: 25px; + font-size: 18px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 5px 15px rgba(255, 71, 87, 0.3); + width: 100%; + } + + .cast-button:hover { + transform: translateY(-3px); + box-shadow: 0 8px 25px rgba(255, 71, 87, 0.5); + } + + .cast-button:disabled { + background: #666; + cursor: not-allowed; + transform: none; + box-shadow: none; + } + + .damage-number { + position: absolute; + font-size: 36px; + font-weight: bold; + color: #ff4757; + text-shadow: 2px 2px 4px rgba(0,0,0,0.8); + pointer-events: none; + animation: damageFloat 1.5s ease-out forwards; + } + + @keyframes damageFloat { + 0% { + opacity: 1; + transform: translateY(0) scale(1); + } + 100% { + opacity: 0; + transform: translateY(-100px) scale(1.5); + } + } + + .spell-effect { + position: absolute; + width: 100px; + height: 100px; + border-radius: 50%; + pointer-events: none; + animation: spellBlast 0.8s ease-out forwards; + } + + .fire-effect { + background: radial-gradient(circle, #ff6b7a, #ff4757, transparent); + } + + .lightning-effect { + background: radial-gradient(circle, #ffd700, #ffed4e, transparent); + } + + .meteor-effect { + background: radial-gradient(circle, #a29bfe, #6c5ce7, transparent); + } + + @keyframes spellBlast { + 0% { + transform: scale(0); + opacity: 1; + } + 50% { + transform: scale(1.5); + opacity: 0.8; + } + 100% { + transform: scale(3); + opacity: 0; + } + } + + .mini-enemy { + position: absolute; + width: 60px; + height: 60px; + background: linear-gradient(45deg, #ff9ff3, #f368e0); + border-radius: 50%; + font-size: 30px; + display: flex; + align-items: center; + justify-content: center; + animation: miniEnemyFloat 3s ease-in-out infinite; + z-index: 100; + } + + @keyframes miniEnemyFloat { + 0%, 100% { transform: translateY(0px) rotate(0deg); } + 50% { transform: translateY(-15px) rotate(180deg); } + } + + .magic-quirk { + position: fixed; + width: 200px; + height: 200px; + border-radius: 50%; + background: conic-gradient(from 0deg, #ff0080, #0080ff, #ff0080); + animation: magicQuirk 2s ease-in-out; + z-index: 1000; + pointer-events: none; + } + + @keyframes magicQuirk { + 0% { + transform: translate(-50%, -50%) scale(0) rotate(0deg); + opacity: 1; + } + 50% { + transform: translate(-50%, -50%) scale(1.5) rotate(180deg); + opacity: 0.8; + } + 100% { + transform: translate(-50%, -50%) scale(0) rotate(360deg); + opacity: 0; + } + } + + .flying-bird { + position: fixed; + font-size: 48px; /* Plus gros ! */ + z-index: 500; + pointer-events: none; + width: 60px; /* Plus gros ! */ + height: 60px; /* Plus gros ! */ + display: flex; + align-items: center; + justify-content: center; + } + + .bird-path-1 { + animation: flyPath1 8s linear infinite; /* Plus rapide ! */ + } + + .bird-path-2 { + animation: flyPath2 6s linear infinite; /* Plus rapide ! */ + } + + .bird-path-3 { + animation: flyPath3 10s linear infinite; /* Plus rapide ! */ + } + + .bird-path-4 { + animation: flyPath4 12s linear infinite; /* Nouveau chemin ! */ + } + + .bird-path-5 { + animation: flyPath5 9s linear infinite; /* Encore un autre ! */ + } + + @keyframes flyPath1 { + 0% { + left: -100px; + top: 20vh; + transform: rotate(0deg) scale(1); + } + 15% { + left: 30vw; + top: 5vh; + transform: rotate(180deg) scale(1.5); /* Rotation plus douce */ + } + 30% { + left: 70vw; + top: 40vh; + transform: rotate(-90deg) scale(0.5); + } + 45% { + left: 100vw; + top: 15vh; + transform: rotate(270deg) scale(2); + } + 60% { + left: 60vw; + top: 85vh; + transform: rotate(-180deg) scale(0.8); + } + 80% { + left: 10vw; + top: 60vh; + transform: rotate(360deg) scale(1.2); + } + 100% { + left: -100px; + top: 20vh; + transform: rotate(450deg) scale(1); + } + } + + @keyframes flyPath2 { + 0% { + left: 50vw; + top: -80px; + transform: rotate(0deg) scale(0.5); + } + 20% { + left: 80vw; + top: 20vh; + transform: rotate(-225deg) scale(2.5); + } + 40% { + left: 20vw; + top: 50vh; + transform: rotate(315deg) scale(0.3); + } + 60% { + left: 90vw; + top: 80vh; + transform: rotate(-450deg) scale(3); + } + 80% { + left: 30vw; + top: 30vh; + transform: rotate(540deg) scale(0.7); + } + 100% { + left: 50vw; + top: -80px; + transform: rotate(-630deg) scale(0.5); + } + } + + @keyframes flyPath3 { + 0% { + left: 120vw; + top: 10vh; + transform: rotate(0deg) scale(1); + } + 12% { + left: 75vw; + top: 70vh; + transform: rotate(-360deg) scale(4); + } + 25% { + left: 25vw; + top: 20vh; + transform: rotate(540deg) scale(0.2); + } + 37% { + left: 85vw; + top: 90vh; + transform: rotate(-720deg) scale(3.5); + } + 50% { + left: 10vw; + top: 5vh; + transform: rotate(900deg) scale(0.4); + } + 62% { + left: 90vw; + top: 55vh; + transform: rotate(-1080deg) scale(2.8); + } + 75% { + left: 40vw; + top: 95vh; + transform: rotate(1260deg) scale(0.6); + } + 87% { + left: 70vw; + top: 25vh; + transform: rotate(-1440deg) scale(3.2); + } + 100% { + left: 120vw; + top: 10vh; + transform: rotate(1800deg) scale(1); + } + } + + @keyframes flyPath4 { + 0% { + left: 25vw; + top: 100vh; + transform: rotate(0deg) scale(1.2); + } + 20% { + left: 75vw; + top: 80vh; + transform: rotate(180deg) scale(0.7); + } + 40% { + left: 90vw; + top: 40vh; + transform: rotate(-270deg) scale(2.2); + } + 60% { + left: 40vw; + top: 20vh; + transform: rotate(360deg) scale(0.4); + } + 80% { + left: 10vw; + top: 70vh; + transform: rotate(-450deg) scale(1.8); + } + 100% { + left: 25vw; + top: 100vh; + transform: rotate(540deg) scale(1.2); + } + } + + @keyframes flyPath5 { + 0% { + left: 100vw; + top: 50vh; + transform: rotate(45deg) scale(0.8); + } + 25% { + left: 60vw; + top: 10vh; + transform: rotate(-135deg) scale(2.5); + } + 50% { + left: 20vw; + top: 80vh; + transform: rotate(225deg) scale(0.3); + } + 75% { + left: 80vw; + top: 90vh; + transform: rotate(-315deg) scale(3); + } + 100% { + left: 100vw; + top: 50vh; + transform: rotate(405deg) scale(0.8); + } + } + + .screen-shake { + animation: screenShake 0.5s ease-in-out; + } + + @keyframes screenShake { + 0%, 100% { transform: translateX(0); } + 10% { transform: translateX(-10px); } + 20% { transform: translateX(10px); } + 30% { transform: translateX(-10px); } + 40% { transform: translateX(10px); } + 50% { transform: translateX(-10px); } + 60% { transform: translateX(10px); } + 70% { transform: translateX(-10px); } + 80% { transform: translateX(10px); } + 90% { transform: translateX(-10px); } + } + + .enemy-attack-warning { + position: absolute; + top: -30px; + left: 50%; + transform: translateX(-50%); + background: #ff4757; + color: white; + padding: 5px 15px; + border-radius: 15px; + font-size: 14px; + font-weight: bold; + animation: warningPulse 1s ease-in-out infinite; + z-index: 100; + } + + @keyframes warningPulse { + 0%, 100% { opacity: 1; transform: translateX(-50%) scale(1); } + 50% { opacity: 0.6; transform: translateX(-50%) scale(1.1); } + } + + .enemy-attack-effect { + position: absolute; + width: 150px; + height: 150px; + border-radius: 50%; + background: radial-gradient(circle, #ff4757, transparent); + animation: enemyAttackBlast 1s ease-out; + pointer-events: none; + z-index: 200; + } + + @keyframes enemyAttackBlast { + 0% { + transform: scale(0); + opacity: 1; + } + 50% { + transform: scale(1.5); + opacity: 0.8; + } + 100% { + transform: scale(3); + opacity: 0; + } + } + + .enemy-charging { + animation: enemyCharging 2s ease-in-out; + } + + @keyframes enemyCharging { + 0%, 100% { + background: linear-gradient(45deg, #ff4757, #ff6b7a); + transform: scale(1); + } + 50% { + background: linear-gradient(45deg, #ff0000, #ff3333); + transform: scale(1.1); + box-shadow: 0 0 60px rgba(255, 0, 0, 0.8); + } + } + + .victory-screen, .defeat-screen { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 1000; + } + + .result-title { + font-size: 48px; + margin-bottom: 20px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.8); + } + + .victory-screen .result-title { + color: #2ed573; + } + + .defeat-screen .result-title { + color: #ff4757; + } + + .fail-message { + position: fixed; + top: 30%; + left: 50%; + transform: translateX(-50%); + background: rgba(255, 71, 87, 0.9); + color: white; + padding: 20px 30px; + border-radius: 15px; + font-size: 24px; + font-weight: bold; + z-index: 1000; + animation: failMessagePop 2s ease-out; + text-align: center; + border: 3px solid #ffd700; + } + + @keyframes failMessagePop { + 0% { + transform: translateX(-50%) scale(0); + opacity: 0; + } + 20% { + transform: translateX(-50%) scale(1.2); + opacity: 1; + } + 80% { + transform: translateX(-50%) scale(1); + opacity: 1; + } + 100% { + transform: translateX(-50%) scale(0.8); + opacity: 0; + } + } + + /* === ENHANCED SPELL EFFECTS === */ + + /* Particle animations */ + @keyframes fireParticle { + 0% { + transform: scale(1) translate(0, 0); + opacity: 1; + } + 50% { + transform: scale(1.5) translate(var(--random-x, 20px), var(--random-y, -30px)); + opacity: 0.8; + } + 100% { + transform: scale(0.5) translate(var(--random-x, 40px), var(--random-y, -60px)); + opacity: 0; + } + } + + @keyframes lightningParticle { + 0% { + transform: scale(1) translate(0, 0); + opacity: 1; + filter: brightness(2); + } + 25% { + transform: scale(2) translate(var(--random-x, 10px), var(--random-y, -20px)); + opacity: 1; + filter: brightness(3); + } + 100% { + transform: scale(0) translate(var(--random-x, 30px), var(--random-y, -50px)); + opacity: 0; + filter: brightness(1); + } + } + + @keyframes meteorParticle { + 0% { + transform: scale(0.5) translate(0, -100px); + opacity: 0.5; + } + 30% { + transform: scale(1.2) translate(var(--random-x, 0px), 0); + opacity: 1; + } + 100% { + transform: scale(0.3) translate(var(--random-x, 20px), var(--random-y, 50px)); + opacity: 0; + } + } + + /* Screen effects */ + @keyframes meteorTrail { + 0% { + opacity: 0; + transform: translateX(100px) scaleY(0); + } + 50% { + opacity: 1; + transform: translateX(0) scaleY(1); + } + 100% { + opacity: 0; + transform: translateX(-100px) scaleY(0.5); + } + } + + @keyframes lightningFlash { + 0% { + opacity: 0; + } + 50% { + opacity: 0.8; + } + 100% { + opacity: 0; + } + } + + @keyframes fireRipple { + 0% { + transform: scale(0.5); + opacity: 1; + border-width: 3px; + } + 50% { + transform: scale(1.2); + opacity: 0.6; + border-width: 2px; + } + 100% { + transform: scale(2); + opacity: 0; + border-width: 1px; + } + } + + /* Enhanced spell effect improvements */ + .fire-effect { + background: radial-gradient(circle, #ff6b7a, #ff4757, #ff3742, transparent); + filter: drop-shadow(0 0 20px #ff4757); + animation: spellBlast 0.8s ease-out forwards, fireGlow 0.8s ease-out; + } + + .lightning-effect { + background: radial-gradient(circle, #ffd700, #ffed4e, #fff200, transparent); + filter: drop-shadow(0 0 25px #ffd700); + animation: spellBlast 0.8s ease-out forwards, lightningPulse 0.8s ease-out; + } + + .meteor-effect { + background: radial-gradient(circle, #a29bfe, #6c5ce7, #5f3dc4, transparent); + filter: drop-shadow(0 0 30px #6c5ce7); + animation: spellBlast 0.8s ease-out forwards, meteorImpact 0.8s ease-out; + } + + @keyframes fireGlow { + 0%, 100% { filter: drop-shadow(0 0 20px #ff4757) hue-rotate(0deg); } + 50% { filter: drop-shadow(0 0 40px #ff4757) hue-rotate(30deg); } + } + + @keyframes lightningPulse { + 0%, 100% { filter: drop-shadow(0 0 25px #ffd700) brightness(1); } + 50% { filter: drop-shadow(0 0 50px #ffd700) brightness(2); } + } + + @keyframes meteorImpact { + 0% { filter: drop-shadow(0 0 30px #6c5ce7) contrast(1); } + 30% { filter: drop-shadow(0 0 60px #6c5ce7) contrast(1.5); } + 100% { filter: drop-shadow(0 0 30px #6c5ce7) contrast(1); } + } + + /* Spell casting enhancement */ + .spell-card.selected { + animation: spellCharging 0.5s ease-in-out infinite alternate; + } + + @keyframes spellCharging { + 0% { + box-shadow: 0 0 20px rgba(255, 215, 0, 0.5); + transform: scale(1); + } + 100% { + box-shadow: 0 0 40px rgba(255, 215, 0, 0.8); + transform: scale(1.02); + } + } + + /* Casting effect animations */ + @keyframes magicCircleForm { + 0% { + transform: scale(0.5) rotate(0deg); + opacity: 0; + } + 50% { + transform: scale(1.1) rotate(180deg); + opacity: 1; + } + 100% { + transform: scale(1) rotate(360deg); + opacity: 0; + } + } + + @keyframes castingSparkle { + 0% { + transform: scale(1) rotate(0deg); + opacity: 1; + } + 50% { + transform: scale(1.5) rotate(180deg); + opacity: 0.8; + } + 100% { + transform: scale(0.5) rotate(360deg); + opacity: 0; + } + } + `; + document.head.appendChild(styleSheet); + } + + extractSpells() { + // Extract sentences from content and categorize by length + this.spells = { + short: [], // 3-4 words + medium: [], // 5-6 words + long: [] // 7+ words + }; + + // Process story sentences + if (this.content.story && this.content.story.chapters) { + this.content.story.chapters.forEach(chapter => { + chapter.sentences.forEach(sentence => { + const wordCount = sentence.words.length; + const spellData = { + english: sentence.original, + translation: sentence.translation, + words: sentence.words, + damage: this.calculateDamage(wordCount), + castTime: this.calculateCastTime(wordCount) + }; + + if (wordCount <= 4) { + this.spells.short.push(spellData); + } else if (wordCount <= 6) { + this.spells.medium.push(spellData); + } else { + this.spells.long.push(spellData); + } + }); + }); + } + + console.log('Spells extracted:', this.spells); + } + + calculateDamage(wordCount) { + // Augmenter significativement les points pour les phrases longues + if (wordCount <= 3) return Math.floor(Math.random() * 10) + 15; // 15-25 (phrases courtes) + if (wordCount <= 5) return Math.floor(Math.random() * 15) + 30; // 30-45 (phrases moyennes) + if (wordCount <= 7) return Math.floor(Math.random() * 20) + 50; // 50-70 (phrases longues) + return Math.floor(Math.random() * 30) + 70; // 70-100 (phrases trรจs longues) + } + + calculateCastTime(wordCount) { + if (wordCount <= 4) return 1000; // 1 second + if (wordCount <= 6) return 2000; // 2 seconds + return 3000; // 3 seconds + } + + init() { + this.container.innerHTML = ` +
+
+
+
+
Wizard HP
+
+
+
+
+
+
Score: 0
+
+
+
+
+
Enemy HP
+
+
+
+
+
+
+ +
+
+
๐Ÿง™โ€โ™‚๏ธ
+
Wizard Master
+
+
+
๐Ÿ‘น
+
Grammar Demon
+
+
+ +
+
+ +
+ +
+
Form your spell incantation:
+
+
+ +
+ +
+ + +
+
+ `; + + this.setupEventListeners(); + this.generateNewSpells(); + this.startEnemyAttackSystem(); + } + + setupEventListeners() { + document.getElementById('cast-button').addEventListener('click', () => this.castSpell()); + } + + generateNewSpells() { + this.currentSpells = []; + + // Get one spell of each type + if (this.spells.short.length > 0) { + this.currentSpells.push({ + ...this.spells.short[Math.floor(Math.random() * this.spells.short.length)], + type: 'short', + name: 'Fireball', + icon: '๐Ÿ”ฅ' + }); + } + + if (this.spells.medium.length > 0) { + this.currentSpells.push({ + ...this.spells.medium[Math.floor(Math.random() * this.spells.medium.length)], + type: 'medium', + name: 'Lightning', + icon: 'โšก' + }); + } + + if (this.spells.long.length > 0) { + this.currentSpells.push({ + ...this.spells.long[Math.floor(Math.random() * this.spells.long.length)], + type: 'long', + name: 'Meteor', + icon: 'โ˜„๏ธ' + }); + } + + this.renderSpellCards(); + this.selectedSpell = null; + this.selectedWords = []; + this.updateWordBank(); + this.updateSentenceBuilder(); + } + + renderSpellCards() { + const container = document.getElementById('spell-selection'); + container.innerHTML = this.currentSpells.map((spell, index) => ` +
+
${spell.icon} ${spell.name}
+
${spell.translation}
+
${spell.damage} damage
+
+ `).join(''); + + // Add click listeners + container.querySelectorAll('.spell-card').forEach(card => { + card.addEventListener('click', (e) => { + const spellIndex = parseInt(e.currentTarget.dataset.spellIndex); + this.selectSpell(spellIndex); + }); + }); + } + + selectSpell(index) { + // Remove previous selection + document.querySelectorAll('.spell-card').forEach(card => card.classList.remove('selected')); + + // Select new spell + this.selectedSpell = this.currentSpells[index]; + document.querySelector(`[data-spell-index="${index}"]`).classList.add('selected'); + + // Dรฉmarrer le timer invisible pour le bonus de vitesse + this.spellStartTime = Date.now(); + + // Reset word selection + this.selectedWords = []; + this.updateWordBank(); + this.updateSentenceBuilder(); + } + + updateWordBank() { + const container = document.getElementById('word-bank'); + + if (!this.selectedSpell) { + container.innerHTML = '
Select a spell first
'; + return; + } + + // Extract the complete sentence including ALL punctuation + const originalSentence = this.selectedSpell.english; + const words = [...this.selectedSpell.words]; + + // Extract ALL punctuation from the original sentence + const punctuationRegex = /[.!?,;:]/g; + const punctuationMarks = originalSentence.match(punctuationRegex) || []; + + // Add all punctuation marks as separate word tiles with unique IDs + punctuationMarks.forEach((punctuation, index) => { + words.push({ + word: punctuation, + translation: punctuation, + type: 'punctuation', + pronunciation: '', + uniqueId: `punct_${index}_${Date.now()}_${Math.random()}` + }); + }); + + // Shuffle the words including punctuation + const shuffledWords = [...words].sort(() => Math.random() - 0.5); + + container.innerHTML = shuffledWords.map((wordData, index) => { + const uniqueId = wordData.uniqueId || `word_${index}_${wordData.word}`; + return ` +
+ ${wordData.word} +
+ `; + }).join(''); + + // Add click listeners + container.querySelectorAll('.word-tile').forEach(tile => { + tile.addEventListener('click', (e) => { + const word = e.currentTarget.dataset.word; + const uniqueId = e.currentTarget.dataset.uniqueId; + this.toggleWord(word, e.currentTarget, uniqueId); + }); + }); + } + + toggleWord(word, element, uniqueId) { + // Find the word by unique ID instead of just the word text + const wordIndex = this.selectedWords.findIndex(selectedWord => + selectedWord.uniqueId === uniqueId + ); + + if (wordIndex > -1) { + // Remove word + this.selectedWords.splice(wordIndex, 1); + element.classList.remove('selected'); + } else { + // Add word with unique ID + this.selectedWords.push({ + word: word, + uniqueId: uniqueId + }); + element.classList.add('selected'); + } + + this.updateSentenceBuilder(); + this.updateCastButton(); + } + + updateSentenceBuilder() { + const container = document.getElementById('current-sentence'); + const sentence = this.buildSentenceFromWords(this.selectedWords); + container.textContent = sentence; + } + + buildSentenceFromWords(words) { + // Join words and handle punctuation correctly (no space before punctuation) + let sentence = ''; + for (let i = 0; i < words.length; i++) { + const wordText = typeof words[i] === 'string' ? words[i] : words[i].word; + const isPunctuation = ['.', '!', '?', ',', ';', ':'].includes(wordText); + + if (i === 0) { + sentence = wordText; + } else if (isPunctuation) { + sentence += wordText; // No space before punctuation + } else { + sentence += ' ' + wordText; // Space before regular words + } + } + return sentence; + } + + updateCastButton() { + const button = document.getElementById('cast-button'); + + // Always enable the button - let players try and fail! + button.disabled = false; + + // Always show the same text - don't reveal if the spell is correct + if (this.selectedSpell) { + button.textContent = `๐Ÿ”ฅ CAST ${this.selectedSpell.name.toUpperCase()} ๐Ÿ”ฅ`; + } else { + button.textContent = '๐Ÿ”ฅ CAST SPELL ๐Ÿ”ฅ'; + } + } + + castSpell() { + if (!this.selectedSpell) { + this.showFailEffect('noSpell'); + return; + } + + // Check if spell is correctly formed (including punctuation) + const expectedSentence = this.selectedSpell.english; + const playerSentence = this.buildSentenceFromWords(this.selectedWords); + + console.log('๐Ÿ” Spell check:'); + console.log('Expected:', expectedSentence); + console.log('Player:', playerSentence); + console.log('Selected words:', this.selectedWords); + + const isCorrect = playerSentence === expectedSentence; + + if (isCorrect) { + // Successful cast! + this.showCastingEffect(this.selectedSpell.type); + + // Delay the main spell effect for dramatic timing + setTimeout(() => { + this.showSpellEffect(this.selectedSpell.type); + }, 500); + + // Deal damage + this.enemyHP = Math.max(0, this.enemyHP - this.selectedSpell.damage); + this.updateEnemyHealth(); + this.showDamageNumber(this.selectedSpell.damage); + + // Update score - bonus multiplicateur pour phrases longues + const wordCount = this.selectedWords.length; + let scoreMultiplier = 10; + if (wordCount >= 7) scoreMultiplier = 20; // x2 pour phrases trรจs longues + else if (wordCount >= 5) scoreMultiplier = 15; // x1.5 pour phrases longues + + // Calculer le bonus de vitesse (invisible) + let speedBonus = 0; + if (this.spellStartTime) { + const spellTime = (Date.now() - this.spellStartTime) / 1000; // en secondes + this.spellCount++; + this.averageSpellTime = ((this.averageSpellTime * (this.spellCount - 1)) + spellTime) / this.spellCount; + + // Bonus de vitesse : plus c'est rapide, plus de points + if (spellTime < 10) speedBonus = Math.floor((10 - spellTime) * 50); // Jusqu'ร  500 bonus + if (spellTime < 5) speedBonus += 300; // Bonus extra pour super rapide + if (spellTime < 3) speedBonus += 500; // Bonus รฉnorme pour trรจs rapide + } + + this.score += (this.selectedSpell.damage * scoreMultiplier) + speedBonus; + this.onScoreUpdate(this.score); + document.getElementById('current-score').textContent = this.score; + + // Check win condition + if (this.enemyHP <= 0) { + this.handleVictory(); + return; + } + + // Generate new spells for next round + setTimeout(() => { + this.generateNewSpells(); + // Reset timer pour nouveau round + this.spellStartTime = Date.now(); + }, 1000); + } else { + // Spell failed! Random funny effect + this.showFailEffect(); + } + } + + showSpellEffect(type) { + const enemyChar = document.querySelector('.enemy-character'); + const rect = enemyChar.getBoundingClientRect(); + + // Main spell effect + const effect = document.createElement('div'); + effect.className = `spell-effect ${type}-effect`; + effect.style.position = 'fixed'; + effect.style.left = rect.left + rect.width/2 - 50 + 'px'; + effect.style.top = rect.top + rect.height/2 - 50 + 'px'; + document.body.appendChild(effect); + + // Add spell-specific enhanced effects + this.createSpellParticles(type, rect); + this.triggerSpellAnimation(type, enemyChar); + + setTimeout(() => { + effect.remove(); + }, 800); + } + + createSpellParticles(type, enemyRect) { + const particleCount = type === 'meteor' ? 15 : type === 'lightning' ? 12 : 8; + + for (let i = 0; i < particleCount; i++) { + const particle = document.createElement('div'); + particle.className = `spell-particle ${type}-particle`; + + // Random position around enemy + const offsetX = (Math.random() - 0.5) * 200; + const offsetY = (Math.random() - 0.5) * 200; + + particle.style.position = 'fixed'; + particle.style.left = enemyRect.left + enemyRect.width/2 + offsetX + 'px'; + particle.style.top = enemyRect.top + enemyRect.height/2 + offsetY + 'px'; + particle.style.width = '6px'; + particle.style.height = '6px'; + particle.style.borderRadius = '50%'; + particle.style.pointerEvents = 'none'; + particle.style.zIndex = '1000'; + + // Spell-specific particle colors and animations + if (type === 'fire') { + particle.style.background = 'radial-gradient(circle, #ff6b7a, #ff4757)'; + particle.style.animation = 'fireParticle 1.2s ease-out forwards'; + particle.style.boxShadow = '0 0 10px #ff4757'; + } else if (type === 'lightning') { + particle.style.background = 'radial-gradient(circle, #ffd700, #ffed4e)'; + particle.style.animation = 'lightningParticle 0.8s ease-out forwards'; + particle.style.boxShadow = '0 0 15px #ffd700'; + } else if (type === 'meteor') { + particle.style.background = 'radial-gradient(circle, #a29bfe, #6c5ce7)'; + particle.style.animation = 'meteorParticle 1.5s ease-out forwards'; + particle.style.boxShadow = '0 0 20px #6c5ce7'; + } + + document.body.appendChild(particle); + + setTimeout(() => { + particle.remove(); + }, 1500); + } + } + + triggerSpellAnimation(type, enemyChar) { + // Screen effects based on spell type + if (type === 'meteor') { + // Meteor causes screen shake + document.body.classList.add('screen-shake'); + setTimeout(() => document.body.classList.remove('screen-shake'), 500); + + // Create meteor trail effect + this.createMeteorTrail(); + } else if (type === 'lightning') { + // Lightning flash effect + this.createLightningFlash(); + } else if (type === 'fire') { + // Fire ripple effect + this.createFireRipple(enemyChar); + } + + // Enemy hit reaction + enemyChar.style.transform = 'scale(1.1)'; + enemyChar.style.filter = type === 'fire' ? 'hue-rotate(30deg)' : + type === 'lightning' ? 'brightness(1.5)' : + 'contrast(1.3)'; + + setTimeout(() => { + enemyChar.style.transform = ''; + enemyChar.style.filter = ''; + }, 300); + } + + createMeteorTrail() { + const trail = document.createElement('div'); + trail.className = 'meteor-trail'; + trail.style.position = 'fixed'; + trail.style.top = '0'; + trail.style.right = '0'; + trail.style.width = '4px'; + trail.style.height = '100vh'; + trail.style.background = 'linear-gradient(180deg, #a29bfe, transparent)'; + trail.style.animation = 'meteorTrail 0.6s ease-out forwards'; + trail.style.pointerEvents = 'none'; + trail.style.zIndex = '999'; + + document.body.appendChild(trail); + setTimeout(() => trail.remove(), 600); + } + + createLightningFlash() { + const flash = document.createElement('div'); + flash.style.position = 'fixed'; + flash.style.top = '0'; + flash.style.left = '0'; + flash.style.width = '100vw'; + flash.style.height = '100vh'; + flash.style.background = 'rgba(255, 215, 0, 0.3)'; + flash.style.animation = 'lightningFlash 0.2s ease-out'; + flash.style.pointerEvents = 'none'; + flash.style.zIndex = '998'; + + document.body.appendChild(flash); + setTimeout(() => flash.remove(), 200); + } + + createFireRipple(enemyChar) { + const ripple = document.createElement('div'); + const rect = enemyChar.getBoundingClientRect(); + + ripple.style.position = 'fixed'; + ripple.style.left = rect.left + rect.width/2 - 100 + 'px'; + ripple.style.top = rect.top + rect.height/2 - 100 + 'px'; + ripple.style.width = '200px'; + ripple.style.height = '200px'; + ripple.style.border = '3px solid #ff4757'; + ripple.style.borderRadius = '50%'; + ripple.style.animation = 'fireRipple 0.8s ease-out forwards'; + ripple.style.pointerEvents = 'none'; + ripple.style.zIndex = '997'; + + document.body.appendChild(ripple); + setTimeout(() => ripple.remove(), 800); + } + + showCastingEffect(spellType) { + const wizardChar = document.querySelector('.wizard-character'); + const rect = wizardChar.getBoundingClientRect(); + + // Create magical circle around wizard + this.createMagicCircle(rect, spellType); + + // Add casting sparkles + this.createCastingSparkles(rect, spellType); + + // Wizard glow effect + wizardChar.style.filter = 'drop-shadow(0 0 20px #ffd700)'; + wizardChar.style.transform = 'scale(1.05)'; + + setTimeout(() => { + wizardChar.style.filter = ''; + wizardChar.style.transform = ''; + }, 600); + } + + createMagicCircle(wizardRect, spellType) { + const circle = document.createElement('div'); + circle.style.position = 'fixed'; + circle.style.left = wizardRect.left + wizardRect.width/2 - 75 + 'px'; + circle.style.top = wizardRect.top + wizardRect.height/2 - 75 + 'px'; + circle.style.width = '150px'; + circle.style.height = '150px'; + circle.style.borderRadius = '50%'; + circle.style.pointerEvents = 'none'; + circle.style.zIndex = '500'; + + // Spell-specific circle colors + if (spellType === 'fire') { + circle.style.border = '3px solid #ff4757'; + circle.style.boxShadow = '0 0 30px #ff4757, inset 0 0 30px rgba(255, 71, 87, 0.3)'; + } else if (spellType === 'lightning') { + circle.style.border = '3px solid #ffd700'; + circle.style.boxShadow = '0 0 30px #ffd700, inset 0 0 30px rgba(255, 215, 0, 0.3)'; + } else if (spellType === 'meteor') { + circle.style.border = '3px solid #6c5ce7'; + circle.style.boxShadow = '0 0 30px #6c5ce7, inset 0 0 30px rgba(108, 92, 231, 0.3)'; + } + + circle.style.animation = 'magicCircleForm 0.6s ease-out forwards'; + + document.body.appendChild(circle); + setTimeout(() => circle.remove(), 600); + } + + createCastingSparkles(wizardRect, spellType) { + const sparkleCount = 8; + + for (let i = 0; i < sparkleCount; i++) { + const sparkle = document.createElement('div'); + sparkle.style.position = 'fixed'; + sparkle.style.width = '4px'; + sparkle.style.height = '4px'; + sparkle.style.borderRadius = '50%'; + sparkle.style.pointerEvents = 'none'; + sparkle.style.zIndex = '501'; + + // Position around wizard + const angle = (i / sparkleCount) * 2 * Math.PI; + const radius = 60; + const x = wizardRect.left + wizardRect.width/2 + Math.cos(angle) * radius; + const y = wizardRect.top + wizardRect.height/2 + Math.sin(angle) * radius; + + sparkle.style.left = x + 'px'; + sparkle.style.top = y + 'px'; + + // Spell-specific sparkle colors + if (spellType === 'fire') { + sparkle.style.background = '#ff4757'; + sparkle.style.boxShadow = '0 0 8px #ff4757'; + } else if (spellType === 'lightning') { + sparkle.style.background = '#ffd700'; + sparkle.style.boxShadow = '0 0 8px #ffd700'; + } else if (spellType === 'meteor') { + sparkle.style.background = '#6c5ce7'; + sparkle.style.boxShadow = '0 0 8px #6c5ce7'; + } + + sparkle.style.animation = 'castingSparkle 0.6s ease-out forwards'; + + document.body.appendChild(sparkle); + setTimeout(() => sparkle.remove(), 600); + } + } + + showDamageNumber(damage) { + const damageEl = document.createElement('div'); + damageEl.className = 'damage-number'; + damageEl.textContent = `-${damage}`; + + const enemyChar = document.querySelector('.enemy-character'); + const rect = enemyChar.getBoundingClientRect(); + + damageEl.style.position = 'fixed'; + damageEl.style.left = rect.left + rect.width/2 + 'px'; + damageEl.style.top = rect.top + 'px'; + + document.body.appendChild(damageEl); + + setTimeout(() => { + damageEl.remove(); + }, 1500); + } + + showFailEffect(type = 'random') { + const effects = ['spawnMinion', 'loseHP', 'magicQuirk', 'flyingBirds']; + const selectedEffect = type === 'random' ? + effects[Math.floor(Math.random() * effects.length)] : + type; + + // Show fail message first + this.showFailMessage(); + + // Then trigger specific effect + switch(selectedEffect) { + case 'spawnMinion': + this.spawnMiniEnemy(); + break; + case 'loseHP': + this.wizardTakesDamage(); + break; + case 'magicQuirk': + this.triggerMagicQuirk(); + break; + case 'flyingBirds': + this.summonFlyingBirds(); + break; + case 'noSpell': + this.showFailMessage('Select a spell first! ๐Ÿช„'); + break; + } + } + + showFailMessage(customMessage = null) { + const messages = [ + "Spell backfired! ๐Ÿ’ฅ", + "Magic went wrong! ๐ŸŒ€", + "Oops! Wrong incantation! ๐Ÿ˜…", + "The magic gods are not pleased! โšก", + "Your spell turned into chaos! ๐ŸŽญ", + "Magic malfunction detected! ๐Ÿ”ง" + ]; + + const message = customMessage || messages[Math.floor(Math.random() * messages.length)]; + + const failEl = document.createElement('div'); + failEl.className = 'fail-message'; + failEl.textContent = message; + + document.body.appendChild(failEl); + + setTimeout(() => { + failEl.remove(); + }, 2000); + } + + spawnMiniEnemy() { + console.log('๐ŸงŒ Spawning mini enemy!'); + + const miniEnemy = document.createElement('div'); + miniEnemy.className = 'mini-enemy'; + miniEnemy.textContent = '๐Ÿ‘บ'; + + // Random position around the main enemy + const mainEnemy = document.querySelector('.enemy-character'); + const rect = mainEnemy.getBoundingClientRect(); + + miniEnemy.style.position = 'fixed'; + miniEnemy.style.left = (rect.left + Math.random() * 200 - 100) + 'px'; + miniEnemy.style.top = (rect.top + Math.random() * 200 - 100) + 'px'; + + document.body.appendChild(miniEnemy); + + // Mini enemy disappears after 5 seconds + setTimeout(() => { + miniEnemy.remove(); + }, 5000); + + // Make main enemy slightly stronger + this.enemyHP = Math.min(100, this.enemyHP + 5); + this.updateEnemyHealth(); + } + + wizardTakesDamage() { + console.log('๐Ÿ”ฅ Wizard takes damage!'); + + this.playerHP = Math.max(0, this.playerHP - 10); + document.getElementById('player-health').style.width = this.playerHP + '%'; + + // Screen shake effect + document.body.classList.add('screen-shake'); + setTimeout(() => { + document.body.classList.remove('screen-shake'); + }, 500); + + // Show damage on wizard + const damageEl = document.createElement('div'); + damageEl.className = 'damage-number'; + damageEl.textContent = '-10'; + damageEl.style.color = '#ff4757'; + + const wizardChar = document.querySelector('.wizard-character'); + const rect = wizardChar.getBoundingClientRect(); + + damageEl.style.position = 'fixed'; + damageEl.style.left = rect.left + rect.width/2 + 'px'; + damageEl.style.top = rect.top + 'px'; + + document.body.appendChild(damageEl); + + setTimeout(() => { + damageEl.remove(); + }, 1500); + + // Check if wizard dies + if (this.playerHP <= 0) { + setTimeout(() => { + this.handleDefeat(); + }, 1000); + } + } + + triggerMagicQuirk() { + console.log('๐ŸŒ€ Magic quirk activated!'); + + // Create multiple quirks at random positions + const numQuirks = 2 + Math.floor(Math.random() * 2); // 2-3 quirks + + for (let i = 0; i < numQuirks; i++) { + setTimeout(() => { + const quirk = document.createElement('div'); + quirk.className = 'magic-quirk'; + + // Random position within viewport + const x = 20 + Math.random() * 60; // 20% to 80% of viewport width + const y = 20 + Math.random() * 60; // 20% to 80% of viewport height + + quirk.style.left = x + '%'; + quirk.style.top = y + '%'; + quirk.style.transform = 'translate(-50%, -50%)'; + + document.body.appendChild(quirk); + + setTimeout(() => { + quirk.remove(); + }, 2000); + }, i * 300); // Stagger the quirks + } + + // Scramble the word bank for extra chaos + setTimeout(() => { + this.updateWordBank(); + }, 1000); + } + + summonFlyingBirds() { + console.log('๐Ÿฆ Summoning flying birds!'); + + const birds = ['๐Ÿฆ', '๐Ÿ•Š๏ธ', '๐Ÿฆ…', '๐Ÿฆœ', '๐Ÿง', '๐Ÿฆ†', '๐Ÿฆข', '๐Ÿ“', '๐Ÿฆƒ', '๐Ÿฆš', '๐Ÿค', '๐Ÿฃ', '๐Ÿฅ']; + const paths = ['bird-path-1', 'bird-path-2', 'bird-path-3', 'bird-path-4', 'bird-path-5']; + const numBirds = 5 + Math.floor(Math.random() * 3); // 5-7 birds maintenant ! + + for (let i = 0; i < numBirds; i++) { + setTimeout(() => { + const bird = document.createElement('div'); + const pathClass = paths[i % paths.length]; + bird.className = `flying-bird ${pathClass}`; + bird.textContent = birds[Math.floor(Math.random() * birds.length)]; + + document.body.appendChild(bird); + + console.log(`๐Ÿฆ Bird ${i+1} spawned with class: ${bird.className}`); + + setTimeout(() => { + bird.remove(); + }, 30000); + + }, i * 500); // Stagger bird appearances + } + } + + updateEnemyHealth() { + const healthBar = document.getElementById('enemy-health'); + const percentage = (this.enemyHP / 100) * 100; + healthBar.style.width = percentage + '%'; + } + + + getRandomAttackTime() { + // Enemy attacks every 8-15 seconds randomly + return 8000 + Math.random() * 7000; + } + + startEnemyAttackSystem() { + this.scheduleNextEnemyAttack(); + } + + scheduleNextEnemyAttack() { + this.enemyAttackTimer = setTimeout(() => { + this.executeEnemyAttack(); + this.scheduleNextEnemyAttack(); // Schedule next attack + }, this.nextEnemyAttack); + } + + executeEnemyAttack() { + console.log('๐Ÿ‘น Enemy is attacking!'); + + const enemyChar = document.querySelector('.enemy-character'); + + // Show attack warning + this.showEnemyAttackWarning(); + + // Enemy charging animation + enemyChar.classList.add('enemy-charging'); + + // Attack after 2 seconds warning + setTimeout(() => { + enemyChar.classList.remove('enemy-charging'); + this.dealEnemyDamage(); + this.showEnemyAttackEffect(); + }, 2000); + + // Set next attack time + this.nextEnemyAttack = this.getRandomAttackTime(); + } + + showEnemyAttackWarning() { + const enemyChar = document.querySelector('.enemy-character'); + + // Remove existing warning + const existingWarning = enemyChar.querySelector('.enemy-attack-warning'); + if (existingWarning) { + existingWarning.remove(); + } + + const warning = document.createElement('div'); + warning.className = 'enemy-attack-warning'; + warning.textContent = 'โš ๏ธ INCOMING ATTACK!'; + + enemyChar.style.position = 'relative'; + enemyChar.appendChild(warning); + + // Remove warning after attack + setTimeout(() => { + warning.remove(); + }, 2000); + } + + dealEnemyDamage() { + const damage = 12 + Math.floor(Math.random() * 8); // 12-20 damage + + this.playerHP = Math.max(0, this.playerHP - damage); + document.getElementById('player-health').style.width = this.playerHP + '%'; + + // Screen shake + document.body.classList.add('screen-shake'); + setTimeout(() => { + document.body.classList.remove('screen-shake'); + }, 500); + + // Show damage number on wizard + const damageEl = document.createElement('div'); + damageEl.className = 'damage-number'; + damageEl.textContent = `-${damage}`; + damageEl.style.color = '#ff4757'; + + const wizardChar = document.querySelector('.wizard-character'); + const rect = wizardChar.getBoundingClientRect(); + + damageEl.style.position = 'fixed'; + damageEl.style.left = rect.left + rect.width/2 + 'px'; + damageEl.style.top = rect.top + 'px'; + + document.body.appendChild(damageEl); + + setTimeout(() => { + damageEl.remove(); + }, 1500); + + console.log(`๐Ÿ’” Player took ${damage} damage! HP: ${this.playerHP}`); + + // Check if player dies + if (this.playerHP <= 0) { + setTimeout(() => { + this.handleDefeat(); + }, 1000); + } + } + + showEnemyAttackEffect() { + const effect = document.createElement('div'); + effect.className = 'enemy-attack-effect'; + + const wizardChar = document.querySelector('.wizard-character'); + const rect = wizardChar.getBoundingClientRect(); + + effect.style.position = 'fixed'; + effect.style.left = rect.left + rect.width/2 - 75 + 'px'; + effect.style.top = rect.top + rect.height/2 - 75 + 'px'; + + document.body.appendChild(effect); + + setTimeout(() => { + effect.remove(); + }, 1000); + } + + handleVictory() { + clearTimeout(this.enemyAttackTimer); + + const bonusScore = 1000; // Fixed victory bonus + this.score += bonusScore; + + this.container.innerHTML += ` +
+
๐ŸŽ‰ VICTORY! ๐ŸŽ‰
+
You defeated the Grammar Demon!
+
Final Score: ${this.score}
+
Time Bonus: +${bonusScore}
+ +
+ `; + + this.onGameEnd(this.score); + } + + handleDefeat() { + clearTimeout(this.enemyAttackTimer); + + this.container.innerHTML += ` +
+
๐Ÿ’€ DEFEATED ๐Ÿ’€
+
The Grammar Demon proved too strong!
+
Final Score: ${this.score}
+ +
+ `; + + this.onGameEnd(this.score); + } + + start() { + // Game starts immediately when initialized + } + + destroy() { + if (this.enemyAttackTimer) { + clearTimeout(this.enemyAttackTimer); + } + + const styleSheet = document.getElementById('wizard-spell-caster-styles'); + if (styleSheet) { + styleSheet.remove(); + } + } + + restart() { + this.destroy(); + this.score = 0; + this.enemyHP = 100; + this.playerHP = 100; + this.spellStartTime = Date.now(); + this.averageSpellTime = 0; + this.spellCount = 0; + this.nextEnemyAttack = this.getRandomAttackTime(); + this.init(); + } +} + +// Register the game module +window.GameModules = window.GameModules || {}; +window.GameModules.WizardSpellCaster = WizardSpellCaster; \ No newline at end of file diff --git a/src/games/word-discovery.js b/src/games/word-discovery.js new file mode 100644 index 0000000..b16cbd4 --- /dev/null +++ b/src/games/word-discovery.js @@ -0,0 +1,1046 @@ +class WordDiscovery { + constructor({ container, content, onScoreUpdate, onGameEnd }) { + this.container = container; + this.content = content; + this.onScoreUpdate = onScoreUpdate; + this.onGameEnd = onGameEnd; + + // Expose content globally for SettingsManager TTS language detection + window.currentGameContent = content; + + this.currentWordIndex = 0; + this.discoveredWords = []; + this.currentPhase = 'discovery'; // discovery, practice + this.score = 0; + this.lives = 3; + this.wordsToLearn = []; + + // Practice system - Global practice after all words discovered + this.practiceLevel = 1; // 1=Easy, 2=Medium, 3=Hard, 4=Expert + this.practiceRound = 0; + this.maxPracticeRounds = 6; // More rounds for mixed practice + this.practiceCorrectAnswers = 0; + this.practiceErrors = 0; + this.currentPracticeWords = []; // Mixed selection of all discovered words + + this.injectCSS(); + this.extractContent(); + this.init(); + } + + injectCSS() { + if (document.getElementById('word-discovery-styles')) return; + + const styleSheet = document.createElement('style'); + styleSheet.id = 'word-discovery-styles'; + styleSheet.textContent = ` + .word-discovery-wrapper { + display: flex; + flex-direction: column; + height: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + } + + .discovery-hud { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: rgba(0,0,0,0.2); + backdrop-filter: blur(10px); + } + + .discovery-progress { + display: flex; + align-items: center; + gap: 15px; + } + + .progress-bar { + width: 200px; + height: 8px; + background: rgba(255,255,255,0.2); + border-radius: 4px; + overflow: hidden; + } + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, #00ff88, #00cc6a); + transition: width 0.3s ease; + } + + .discovery-main { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 20px; + position: relative; + } + + .word-card { + background: white; + border-radius: 20px; + padding: 40px; + box-shadow: 0 20px 40px rgba(0,0,0,0.3); + text-align: center; + max-width: 500px; + width: 100%; + color: #333; + transform: scale(0.9); + opacity: 0; + animation: cardAppear 0.5s ease forwards; + } + + @keyframes cardAppear { + to { + transform: scale(1); + opacity: 1; + } + } + + .word-image { + width: 200px; + height: 200px; + object-fit: cover; + border-radius: 15px; + margin-bottom: 20px; + box-shadow: 0 10px 20px rgba(0,0,0,0.2); + } + + .word-text { + font-size: 2.5em; + font-weight: bold; + color: #2c3e50; + margin-bottom: 10px; + } + + .word-pronunciation { + font-size: 1.2em; + color: #7f8c8d; + margin-bottom: 15px; + font-style: italic; + } + + .word-translation { + font-size: 1.8em; + color: #e74c3c; + font-weight: 600; + margin-bottom: 20px; + } + + .discovery-controls { + display: flex; + gap: 15px; + margin-top: 20px; + } + + .discovery-btn { + padding: 12px 25px; + border: none; + border-radius: 25px; + font-size: 1.1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + min-width: 120px; + } + + .btn-primary { + background: linear-gradient(45deg, #667eea, #764ba2); + color: white; + } + + .btn-secondary { + background: linear-gradient(45deg, #f093fb, #f5576c); + color: white; + } + + .btn-success { + background: linear-gradient(45deg, #4facfe, #00f2fe); + color: white; + } + + .discovery-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(0,0,0,0.2); + } + + .association-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 30px; + max-width: 800px; + width: 100%; + } + + .association-item { + background: white; + border-radius: 15px; + padding: 20px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + color: #333; + border: 3px solid transparent; + } + + .association-item:hover { + transform: translateY(-5px); + box-shadow: 0 15px 30px rgba(0,0,0,0.2); + } + + .association-item.selected { + border-color: #667eea; + background: #f8f9ff; + } + + .association-item.correct { + border-color: #00ff88; + background: #f0fff4; + } + + .association-item.incorrect { + border-color: #ff4757; + background: #fff0f0; + } + + .association-image { + width: 120px; + height: 120px; + object-fit: cover; + border-radius: 10px; + margin-bottom: 15px; + } + + .association-text { + font-size: 1.4em; + font-weight: 600; + } + + .phase-indicator { + position: absolute; + top: 20px; + left: 20px; + background: rgba(255,255,255,0.2); + padding: 8px 16px; + border-radius: 20px; + font-weight: 600; + backdrop-filter: blur(10px); + } + + .practice-progress { + position: absolute; + top: 20px; + right: 20px; + background: rgba(255,255,255,0.2); + padding: 8px 16px; + border-radius: 20px; + font-weight: 600; + backdrop-filter: blur(10px); + font-size: 0.9em; + } + + .difficulty-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 15px; + font-size: 0.8em; + font-weight: bold; + margin-left: 10px; + } + + .difficulty-easy { background: #4CAF50; color: white; } + .difficulty-medium { background: #FF9800; color: white; } + .difficulty-hard { background: #F44336; color: white; } + .difficulty-expert { background: #9C27B0; color: white; } + + .practice-grid-6 { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; + max-width: 900px; + width: 100%; + } + + .practice-grid-8 { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 15px; + max-width: 1000px; + width: 100%; + } + + .practice-challenge { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: rgba(255,255,255,0.1); + border-radius: 15px; + backdrop-filter: blur(10px); + } + + .challenge-timer { + font-size: 2em; + font-weight: bold; + color: #FFD700; + margin-bottom: 10px; + } + + .challenge-text { + font-size: 1.2em; + margin-bottom: 15px; + } + + .practice-stats { + display: flex; + justify-content: space-around; + margin-top: 20px; + font-size: 1.1em; + } + + .stat-item { + text-align: center; + padding: 10px; + background: rgba(255,255,255,0.1); + border-radius: 10px; + backdrop-filter: blur(5px); + } + + .association-item.time-pressure { + animation: timePressure 0.5s ease-in-out infinite alternate; + } + + @keyframes timePressure { + from { box-shadow: 0 0 10px rgba(255,215,0,0.5); } + to { box-shadow: 0 0 20px rgba(255,215,0,0.8); } + } + + .audio-btn { + background: none; + border: none; + font-size: 2em; + cursor: pointer; + color: #667eea; + margin-left: 10px; + transition: all 0.3s ease; + } + + .audio-btn:hover { + transform: scale(1.2); + color: #764ba2; + } + + .completion-message { + text-align: center; + padding: 40px; + background: rgba(255,255,255,0.1); + border-radius: 20px; + backdrop-filter: blur(10px); + } + + .completion-title { + font-size: 2.5em; + margin-bottom: 20px; + color: #00ff88; + } + + .completion-stats { + font-size: 1.3em; + margin-bottom: 30px; + line-height: 1.6; + } + + .content-warning { + background: rgba(255, 193, 7, 0.2); + border: 2px solid #FFC107; + border-radius: 10px; + padding: 15px; + margin: 20px 0; + color: #856404; + font-size: 0.9em; + } + + .feature-missing { + opacity: 0.6; + position: relative; + } + + .feature-missing::after { + content: '๐Ÿ“ต'; + position: absolute; + top: 5px; + right: 5px; + font-size: 0.8em; + } + `; + document.head.appendChild(styleSheet); + } + + extractContent() { + if (!this.content || !this.content.vocabulary) { + this.wordsToLearn = []; + return; + } + + this.wordsToLearn = Object.entries(this.content.vocabulary).map(([word, data]) => ({ + word: word, + translation: typeof data === 'string' ? data : data.translation, + pronunciation: typeof data === 'object' ? data.pronunciation : null, + image: typeof data === 'object' ? data.image : null, + type: typeof data === 'object' ? data.type : 'word' + })).filter(item => item.translation); + + // Shuffle words for variety + this.wordsToLearn = this.shuffleArray([...this.wordsToLearn]); + } + + shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } + + init() { + this.container.innerHTML = ` +
+
+
+ Progress: +
+
+
+ 0/${this.wordsToLearn.length} +
+
+ Score: 0 + โค๏ธ 3 +
+
+
+
Discovery Phase
+
+
+
+ `; + + if (this.wordsToLearn.length === 0) { + this.showNoContent(); + return; + } + + this.updateHUD(); + this.startDiscoveryPhase(); + } + + updateHUD() { + const progressFill = this.container.querySelector('.progress-fill'); + const progressText = this.container.querySelector('.progress-text'); + const scoreDisplay = this.container.querySelector('.score-display'); + const livesDisplay = this.container.querySelector('.lives-display'); + + const progressPercent = (this.currentWordIndex / this.wordsToLearn.length) * 100; + progressFill.style.width = `${progressPercent}%`; + progressText.textContent = `${this.currentWordIndex}/${this.wordsToLearn.length}`; + scoreDisplay.textContent = this.score; + livesDisplay.textContent = this.lives; + } + + startDiscoveryPhase() { + this.currentPhase = 'discovery'; + this.container.querySelector('.phase-indicator').textContent = 'Discovery Phase'; + this.showWordCard(); + } + + showWordCard() { + if (this.currentWordIndex >= this.wordsToLearn.length) { + // All words discovered - start global practice phase + this.startGlobalPractice(); + return; + } + + const word = this.wordsToLearn[this.currentWordIndex]; + const gameContent = this.container.querySelector('.game-content'); + + // Check what features are missing for this word + const missingFeatures = []; + if (!word.image) missingFeatures.push('image'); + if (!word.pronunciation) missingFeatures.push('pronunciation'); + + gameContent.innerHTML = ` +
+ ${word.image ? + `${word.word}` : + `
๐Ÿ“ท No Image
` + } +
${word.word}
+ ${word.pronunciation ? + `
/${word.pronunciation}/
` : + `
No pronunciation guide
` + } +
${word.translation}
+ ${missingFeatures.length > 0 ? + `
+ โš ๏ธ Missing: ${missingFeatures.join(', ')}. Practice questions will be adapted accordingly. +
` : '' + } +
+ + +
+
+ `; + + // Store game reference for button access + this.container.querySelector('.word-discovery-wrapper').game = this; + + // Auto-play TTS when new word appears (with delay for card animation) + setTimeout(() => { + this.hearPronunciation(); + }, 800); + } + + async hearPronunciation(options = {}) { + let wordToSpeak; + + if (this.currentPhase === 'practice') { + // In practice phase, use current practice word + wordToSpeak = this.currentPracticeWords[this.practiceRound % this.currentPracticeWords.length]; + } else { + // In discovery phase, use current word being learned + wordToSpeak = this.wordsToLearn[this.currentWordIndex]; + } + + if (!wordToSpeak) return; + + // Try to play audio file first if available + if (wordToSpeak.audioFile || wordToSpeak.pronunciation) { + const audioPath = wordToSpeak.audioFile; + if (audioPath) { + try { + const audio = new Audio(audioPath); + + // Handle audio loading errors + audio.onerror = () => { + console.warn(`Audio file not found: ${audioPath}, falling back to TTS`); + this.fallbackToTTS(wordToSpeak, options); + }; + + // Handle successful audio loading + audio.oncanplaythrough = () => { + // Adjust playback rate if supported + if (options.rate && audio.playbackRate !== undefined) { + audio.playbackRate = options.rate; + } + audio.play().catch(error => { + console.warn('Audio playback failed:', error); + this.fallbackToTTS(wordToSpeak, options); + }); + }; + + // Load the audio + audio.load(); + + // Timeout fallback if audio takes too long + setTimeout(() => { + if (audio.readyState === 0) { + console.warn('Audio loading timeout, falling back to TTS'); + this.fallbackToTTS(wordToSpeak, options); + } + }, 2000); + + return; // Don't proceed to TTS if we're trying audio + } catch (error) { + console.warn('Audio creation failed:', error); + } + } + } + + // Fallback to TTS immediately if no audio file + this.fallbackToTTS(wordToSpeak, options); + } + + fallbackToTTS(wordToSpeak, options = {}) { + // Use SettingsManager if available, otherwise fallback to basic TTS + if (window.SettingsManager) { + // Pass custom rate if specified + const ttsOptions = {}; + if (options.rate) { + ttsOptions.rate = options.rate; + } + + window.SettingsManager.speak(wordToSpeak.word, ttsOptions) + .catch(error => { + console.warn('SettingsManager TTS failed:', error); + this.basicTTS(wordToSpeak, options); + }); + } else { + this.basicTTS(wordToSpeak, options); + } + } + + basicTTS(wordToSpeak, options = {}) { + // Try to speak the word using Web Speech API + if ('speechSynthesis' in window && wordToSpeak) { + const utterance = new SpeechSynthesisUtterance(wordToSpeak.word); + utterance.lang = 'en-US'; + utterance.rate = options.rate || 0.8; + speechSynthesis.speak(utterance); + } else { + // Last resort: show pronunciation text if available + if (wordToSpeak.pronunciation) { + alert(`Pronunciation: /${wordToSpeak.pronunciation}/`); + } else { + alert(`Word: ${wordToSpeak.word}`); + } + } + } + + + updatePhaseIndicator() { + const phaseIndicator = this.container.querySelector('.phase-indicator'); + const difficultyNames = ['', 'Easy', 'Medium', 'Hard', 'Expert']; + const difficultyClasses = ['', 'difficulty-easy', 'difficulty-medium', 'difficulty-hard', 'difficulty-expert']; + + if (this.currentPhase === 'practice') { + phaseIndicator.innerHTML = `Mixed Practice ${difficultyNames[this.practiceLevel]}`; + } else { + phaseIndicator.textContent = 'Discovery Phase'; + } + + // Update or create practice progress indicator + let progressIndicator = this.container.querySelector('.practice-progress'); + if (!progressIndicator) { + progressIndicator = document.createElement('div'); + progressIndicator.className = 'practice-progress'; + this.container.querySelector('.discovery-main').appendChild(progressIndicator); + } + + if (this.currentPhase === 'practice') { + progressIndicator.textContent = `Round ${this.practiceRound + 1}/${this.maxPracticeRounds}`; + } else { + progressIndicator.textContent = ''; + } + } + + showMixedPracticeChallenge() { + // Get a random word from discovered words for this challenge + const currentWord = this.currentPracticeWords[this.practiceRound % this.currentPracticeWords.length]; + const gameContent = this.container.querySelector('.game-content'); + + // Check available content features + const hasImages = this.discoveredWords.some(word => word.image); + const hasPronunciation = this.discoveredWords.some(word => word.pronunciation); + const hasAudioFiles = this.discoveredWords.some(word => word.audioFile); + const currentWordHasImage = currentWord.image; + const currentWordHasPronunciation = currentWord.pronunciation; + const currentWordHasAudio = currentWord.audioFile; + + // Determine challenge based on practice level and available content + const challenges = { + 1: { options: 4, time: null, question: 'translation' }, // Easy: 4 options, no timer + 2: { options: 6, time: 15, question: 'translation' }, // Medium: 6 options, 15s timer + 3: { options: 6, time: 10, question: hasImages ? 'mixed' : 'translation' }, // Hard: mixed if images available + 4: { options: 8, time: 8, question: (hasAudioFiles || hasPronunciation) ? 'audio' : 'translation' } // Expert: audio if available + }; + + const challenge = challenges[this.practiceLevel]; + const numOptions = challenge.options; + + // Create options: current word + random others from ALL discovered words + const options = [currentWord]; + const otherWords = this.discoveredWords.filter(word => word.word !== currentWord.word); + const randomOthers = this.shuffleArray([...otherWords]).slice(0, numOptions - 1); + options.push(...randomOthers); + + // Shuffle the options + const shuffledOptions = this.shuffleArray([...options]); + + // Determine question type - TEST FOREIGN WORD KNOWLEDGE, NOT NATIVE LANGUAGE + let questionText = ''; + let showImages = true; + let showText = true; + + if (challenge.question === 'translation') { + // Test: Show foreign word, find translation/image + questionText = `Which one means "${currentWord.word}"?`; + } else if (challenge.question === 'mixed') { + // Build available question types based on content + const questionTypes = [`Which one means "${currentWord.word}"?`]; + + // Add pronunciation question if available (text or audio) + if (currentWordHasPronunciation || currentWordHasAudio) { + questionTypes.push(`Find the word that sounds like "${currentWord.pronunciation || currentWord.word}"`); + } + + // Add image question if current word has image AND other words have images for comparison + if (currentWordHasImage && hasImages) { + questionTypes.push(`Which image represents "${currentWord.word}"?`); + } + + questionText = questionTypes[Math.floor(Math.random() * questionTypes.length)]; + if (questionText.includes('image')) { + showText = false; + // Ensure we only show options that have images + const imageOptions = [currentWord]; + const otherWordsWithImages = this.discoveredWords.filter(word => + word.word !== currentWord.word && word.image + ); + if (otherWordsWithImages.length >= numOptions - 1) { + const randomOthers = this.shuffleArray([...otherWordsWithImages]).slice(0, numOptions - 1); + options.length = 1; // Reset to just current word + options.push(...randomOthers); + } + } + } else if (challenge.question === 'audio') { + if (currentWordHasPronunciation || currentWordHasAudio) { + questionText = 'Listen and find the correct word!'; + showImages = false; + // Auto-play pronunciation + setTimeout(() => this.hearPronunciation(), 500); + } else { + // Fallback to translation if no audio + questionText = `Which one means "${currentWord.word}"?`; + } + } + + const gridClass = numOptions <= 4 ? 'association-grid' : + numOptions <= 6 ? 'practice-grid-6' : 'practice-grid-8'; + + gameContent.innerHTML = ` +
+ ${challenge.time ? `
${challenge.time}
` : ''} +
${questionText}
+
+
Correct: ${this.practiceCorrectAnswers}
+
Errors: ${this.practiceErrors}
+
Level: ${this.practiceLevel}/4
+
+
+
+ ${shuffledOptions.map((option, index) => ` +
+ ${showImages && option.image ? `${option.word}` : ''} + ${showText ? `
${option.translation}
` : ''} + ${!showText && !showImages ? `
?
` : ''} +
+ `).join('')} +
+ `; + + // Start timer if needed + if (challenge.time) { + this.startPracticeTimer(challenge.time); + } + + // Auto-play TTS based on practice level with appropriate speed + setTimeout(() => { + let ttsSpeed; + switch (this.practiceLevel) { + case 1: // Easy - 0.7 speed + ttsSpeed = 0.7; + break; + case 2: // Medium - 0.9 speed + ttsSpeed = 0.9; + break; + case 3: // Hard - 1.0 speed + ttsSpeed = 1.0; + break; + case 4: // Expert - 1.1 speed (already has audio auto-play) + ttsSpeed = 1.1; + break; + default: + ttsSpeed = 0.8; // Fallback + } + + // Don't auto-play if it's an audio-only challenge (Expert mode already handles this) + if (challenge.question !== 'audio') { + this.hearPronunciation({ rate: ttsSpeed }); + } + }, 1000); // Delay to let interface render + } + + startPracticeTimer(seconds) { + this.practiceTimer = seconds; + this.practiceTimerInterval = setInterval(() => { + this.practiceTimer--; + const timerElement = document.getElementById('practice-timer'); + if (timerElement) { + timerElement.textContent = this.practiceTimer; + if (this.practiceTimer <= 3) { + timerElement.style.color = '#FF4444'; + timerElement.style.animation = 'pulse 0.5s infinite'; + } + } + + if (this.practiceTimer <= 0) { + this.clearPracticeTimer(); + this.selectPractice(-1, 'TIMEOUT'); // Handle timeout + } + }, 1000); + } + + clearPracticeTimer() { + if (this.practiceTimerInterval) { + clearInterval(this.practiceTimerInterval); + this.practiceTimerInterval = null; + } + } + + selectMixedPractice(selectedIndex, selectedWord) { + this.clearPracticeTimer(); + + const currentWord = this.currentPracticeWords[this.practiceRound % this.currentPracticeWords.length]; + const items = this.container.querySelectorAll('.association-item'); + + let isCorrect = false; + + if (selectedWord === 'TIMEOUT') { + // Timer expired + this.practiceErrors++; + // Show correct answer + items.forEach((item) => { + const text = item.querySelector('.association-text'); + if (text && text.textContent === currentWord.word) { + item.classList.add('correct'); + } + }); + } else if (selectedWord === currentWord.word) { + // Correct answer + isCorrect = true; + items[selectedIndex].classList.add('correct'); + this.practiceCorrectAnswers++; + + // Score based on difficulty level + const scoreBonus = [0, 5, 10, 15, 25][this.practiceLevel]; + this.score += scoreBonus; + this.onScoreUpdate(this.score); + } else { + // Wrong answer + items[selectedIndex].classList.add('incorrect'); + this.practiceErrors++; + + // Show correct answer + items.forEach((item) => { + const text = item.querySelector('.association-text'); + if (text && text.textContent === currentWord.word) { + item.classList.add('correct'); + } + }); + } + + this.updateHUD(); + + // Continue to next practice round or advance + setTimeout(() => { + this.practiceRound++; + + if (this.practiceRound >= this.maxPracticeRounds) { + // Check if ready for next level + const accuracy = this.practiceCorrectAnswers / this.maxPracticeRounds; + + if (accuracy >= 0.75 && this.practiceLevel < 4) { + // Advance to next difficulty level + this.practiceLevel++; + this.practiceRound = 0; + this.practiceCorrectAnswers = 0; + this.practiceErrors = 0; + + // SHUFFLE words again for new difficulty level + this.currentPracticeWords = this.shuffleArray([...this.discoveredWords]); + console.log(`๐Ÿ”€ Shuffled words for Level ${this.practiceLevel} - new variation order`); + + this.updatePhaseIndicator(); + + setTimeout(() => { + this.showLevelUpMessage(); + }, 500); + } else if (accuracy >= 0.5) { + // Passed all practice levels - show completion + this.showCompletion(); + } else { + // Failed practice - retry current level + this.practiceRound = Math.max(0, this.practiceRound - 2); // Go back 2 rounds + this.practiceCorrectAnswers = 0; + this.practiceErrors = 0; + this.lives--; + if (this.lives <= 0) { + this.endGame(); + return; + } + } + } else { + // Continue current difficulty level with next random word + this.updatePhaseIndicator(); + this.showMixedPracticeChallenge(); + } + }, 1500); + } + + showLevelUpMessage() { + const gameContent = this.container.querySelector('.game-content'); + const difficultyNames = ['', 'Easy', 'Medium', 'Hard', 'Expert']; + + gameContent.innerHTML = ` +
+
๐ŸŽ‰ Level Up!
+
+ Advanced to ${difficultyNames[this.practiceLevel]} difficulty!
+ Keep practicing to master this word! +
+
+ `; + + setTimeout(() => { + this.showMixedPracticeChallenge(); + }, 2000); + } + + markAsLearned() { + this.discoveredWords.push(this.wordsToLearn[this.currentWordIndex]); + this.currentWordIndex++; + this.score += 5; + this.onScoreUpdate(this.score); + this.updateHUD(); + + setTimeout(() => { + this.startDiscoveryPhase(); + }, 300); + } + + startGlobalPractice() { + // Transition message + const gameContent = this.container.querySelector('.game-content'); + gameContent.innerHTML = ` +
+
๐Ÿ† Discovery Complete!
+
+ You've discovered all ${this.discoveredWords.length} words!
+ Now let's practice with mixed vocabulary challenges! +
+
+ + +
+
+ `; + } + + startMixedPractice() { + this.currentPhase = 'practice'; + this.practiceLevel = 1; + this.practiceRound = 0; + this.practiceCorrectAnswers = 0; + this.practiceErrors = 0; + + // SHUFFLE discovered words for varied practice order + this.currentPracticeWords = this.shuffleArray([...this.discoveredWords]); + console.log(`๐Ÿ”€ Shuffled ${this.currentPracticeWords.length} words for practice variation`); + + this.updatePhaseIndicator(); + this.showMixedPracticeChallenge(); + } + + skipToCompletion() { + this.showCompletion(); + } + + showCompletion() { + const gameContent = this.container.querySelector('.game-content'); + const accuracy = Math.round((this.discoveredWords.length / this.wordsToLearn.length) * 100); + const practiceAccuracy = this.practiceRound > 0 ? Math.round((this.practiceCorrectAnswers / this.practiceRound) * 100) : 0; + + gameContent.innerHTML = ` +
+
๐Ÿ† Vocabulary Mastered!
+
+ Words Discovered: ${this.discoveredWords.length}/${this.wordsToLearn.length}
+ Practice Accuracy: ${practiceAccuracy}%
+ Final Score: ${this.score}
+ Practice Level Reached: ${this.practiceLevel}/4 +
+
+ + +
+
+ `; + } + + showNoContent() { + const gameContent = this.container.querySelector('.game-content'); + gameContent.innerHTML = ` +
+
๐Ÿ“š No Vocabulary Found
+
+ This content doesn't have vocabulary for the Word Discovery game.
+ Note: Images and audio are optional but enhance the experience! +
+
+ +
+
+ `; + } + + start() { + // Game starts automatically in constructor + } + + restart() { + this.currentWordIndex = 0; + this.discoveredWords = []; + this.score = 0; + this.lives = 3; + this.practiceLevel = 1; + this.practiceRound = 0; + this.practiceCorrectAnswers = 0; + this.practiceErrors = 0; + this.currentPracticeWords = []; + this.clearPracticeTimer(); + this.wordsToLearn = this.shuffleArray([...this.wordsToLearn]); + this.updateHUD(); + this.startDiscoveryPhase(); + } + + endGame() { + this.onGameEnd(this.score); + } + + destroy() { + this.clearPracticeTimer(); + + // Clean up global content reference + if (window.currentGameContent === this.content) { + window.currentGameContent = null; + } + + const styleSheet = document.getElementById('word-discovery-styles'); + if (styleSheet) { + styleSheet.remove(); + } + } +} + +// Register the game module +window.GameModules = window.GameModules || {}; +window.GameModules.WordDiscovery = WordDiscovery; \ No newline at end of file diff --git a/src/games/word-storm.js b/src/games/word-storm.js new file mode 100644 index 0000000..a3add1a --- /dev/null +++ b/src/games/word-storm.js @@ -0,0 +1,656 @@ +// === WORD STORM GAME === +// Game where words fall from the sky like meteorites! + +class WordStormGame { + constructor(options) { + logSh('Word Storm constructor called', 'DEBUG'); + + this.container = options.container; + this.content = options.content; + this.onScoreUpdate = options.onScoreUpdate || (() => {}); + this.onGameEnd = options.onGameEnd || (() => {}); + + // Inject game-specific CSS + this.injectCSS(); + + logSh('Options processed, initializing game state...', 'DEBUG'); + + // Game state + this.score = 0; + this.level = 1; + this.lives = 3; + this.combo = 0; + this.isGamePaused = false; + this.isGameOver = false; + + // Game mechanics + this.fallingWords = []; + this.gameInterval = null; + this.spawnInterval = null; + this.currentWordIndex = 0; + + // Game settings + this.fallSpeed = 8000; // ms to fall from top to bottom (very slow) + this.spawnRate = 4000; // ms between spawns (not frequent) + this.wordLifetime = 15000; // ms before word disappears (long time) + + logSh('Game state initialized, extracting vocabulary...', 'DEBUG'); + + // Content extraction + try { + this.vocabulary = this.extractVocabulary(this.content); + this.shuffledVocab = [...this.vocabulary]; + this.shuffleArray(this.shuffledVocab); + + logSh(`Word Storm initialized with ${this.vocabulary.length} words`, 'INFO'); + } catch (error) { + logSh(`Error extracting vocabulary: ${error.message}`, 'ERROR'); + throw error; + } + + logSh('Calling init()...', 'DEBUG'); + this.init(); + } + + injectCSS() { + // Avoid injecting CSS multiple times + if (document.getElementById('word-storm-styles')) return; + + const styleSheet = document.createElement('style'); + styleSheet.id = 'word-storm-styles'; + styleSheet.textContent = ` + .falling-word { + position: absolute; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px 30px; + border-radius: 25px; + font-size: 2rem; + font-weight: 600; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(102, 126, 234, 0.4); + cursor: default; + user-select: none; + transform: translateX(-50%); + animation: wordGlow 2s ease-in-out infinite; + } + + .falling-word.exploding { + animation: explode 0.8s ease-out forwards; + } + + .falling-word.wrong-shake { + animation: wrongShake 0.6s ease-in-out forwards; + } + + .answer-panel.wrong-flash { + animation: wrongFlash 0.5s ease-in-out; + } + + @keyframes wordGlow { + 0%, 100% { box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(102, 126, 234, 0.4); } + 50% { box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 30px rgba(102, 126, 234, 0.6); } + } + + @keyframes explode { + 0% { + transform: translateX(-50%) scale(1) rotate(0deg); + opacity: 1; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(102, 126, 234, 0.4); + } + 25% { + transform: translateX(-50%) scale(1.3) rotate(5deg); + opacity: 0.9; + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5), 0 0 40px rgba(16, 185, 129, 0.8); + } + 50% { + transform: translateX(-50%) scale(1.5) rotate(-3deg); + opacity: 0.7; + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); + box-shadow: 0 12px 35px rgba(245, 158, 11, 0.6), 0 0 60px rgba(245, 158, 11, 0.9); + } + 75% { + transform: translateX(-50%) scale(0.8) rotate(2deg); + opacity: 0.4; + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + } + 100% { + transform: translateX(-50%) scale(0.1) rotate(0deg); + opacity: 0; + } + } + + @keyframes wrongShake { + 0%, 100% { + transform: translateX(-50%) scale(1); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + } + 10%, 30%, 50%, 70%, 90% { + transform: translateX(-60%) scale(0.95); + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + box-shadow: 0 4px 15px rgba(239, 68, 68, 0.6), 0 0 25px rgba(239, 68, 68, 0.8); + } + 20%, 40%, 60%, 80% { + transform: translateX(-40%) scale(0.95); + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + box-shadow: 0 4px 15px rgba(239, 68, 68, 0.6), 0 0 25px rgba(239, 68, 68, 0.8); + } + } + + @keyframes wrongFlash { + 0%, 100% { + background: transparent; + box-shadow: none; + } + 50% { + background: rgba(239, 68, 68, 0.4); + box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), inset 0 0 20px rgba(239, 68, 68, 0.3); + } + } + + @keyframes screenShake { + 0%, 100% { transform: translateX(0); } + 10% { transform: translateX(-3px) translateY(1px); } + 20% { transform: translateX(3px) translateY(-1px); } + 30% { transform: translateX(-2px) translateY(2px); } + 40% { transform: translateX(2px) translateY(-2px); } + 50% { transform: translateX(-1px) translateY(1px); } + 60% { transform: translateX(1px) translateY(-1px); } + 70% { transform: translateX(-2px) translateY(0px); } + 80% { transform: translateX(2px) translateY(1px); } + 90% { transform: translateX(-1px) translateY(-1px); } + } + + @keyframes pointsFloat { + 0% { + transform: translateY(0) scale(1); + opacity: 1; + } + 30% { + transform: translateY(-20px) scale(1.3); + opacity: 1; + } + 100% { + transform: translateY(-80px) scale(0.5); + opacity: 0; + } + } + + @media (max-width: 768px) { + .falling-word { + padding: 18px 25px; + font-size: 1.8rem; + } + } + + @media (max-width: 480px) { + .falling-word { + font-size: 1.5rem; + padding: 15px 20px; + } + } + `; + + document.head.appendChild(styleSheet); + logSh('Word Storm CSS injected', 'DEBUG'); + } + + extractVocabulary(content) { + let vocabulary = []; + + logSh(`Word Storm extracting vocabulary from content`, 'DEBUG'); + + // Support Dragon's Pearl and other formats + if (content.vocabulary && typeof content.vocabulary === 'object') { + vocabulary = Object.entries(content.vocabulary).map(([original, vocabData]) => { + if (typeof vocabData === 'string') { + return { + original: original, + translation: vocabData + }; + } else if (typeof vocabData === 'object') { + return { + original: original, + translation: vocabData.user_language || vocabData.translation || 'No translation', + pronunciation: vocabData.pronunciation + }; + } + return null; + }).filter(item => item !== null); + + logSh(`Extracted ${vocabulary.length} words from content.vocabulary`, 'DEBUG'); + } + + // Support rawContent format + if (content.rawContent && content.rawContent.vocabulary) { + const rawVocab = Object.entries(content.rawContent.vocabulary).map(([original, vocabData]) => { + if (typeof vocabData === 'string') { + return { original: original, translation: vocabData }; + } else if (typeof vocabData === 'object') { + return { + original: original, + translation: vocabData.user_language || vocabData.translation, + pronunciation: vocabData.pronunciation + }; + } + return null; + }).filter(item => item !== null); + + vocabulary = vocabulary.concat(rawVocab); + logSh(`Added ${rawVocab.length} words from rawContent.vocabulary, total: ${vocabulary.length}`, 'DEBUG'); + } + + // Limit to 50 words max for performance + return vocabulary.slice(0, 50); + } + + init() { + if (this.vocabulary.length === 0) { + this.showNoVocabularyMessage(); + return; + } + + this.container.innerHTML = ` +
+
+
+
Score: 0
+
Level: 1
+
+
+
Lives: 3
+
Combo: 0
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ `; + + this.setupEventListeners(); + this.generateAnswerOptions(); + } + + setupEventListeners() { + const pauseBtn = document.getElementById('pause-btn'); + if (pauseBtn) { + pauseBtn.addEventListener('click', () => this.togglePause()); + } + + // Answer button clicks + document.addEventListener('click', (e) => { + if (e.target.classList.contains('answer-btn')) { + const answer = e.target.textContent; + this.checkAnswer(answer); + } + }); + + // Keyboard support + document.addEventListener('keydown', (e) => { + if (e.key >= '1' && e.key <= '4') { + const btnIndex = parseInt(e.key) - 1; + const buttons = document.querySelectorAll('.answer-btn'); + if (buttons[btnIndex]) { + buttons[btnIndex].click(); + } + } + }); + } + + start() { + logSh('Word Storm game started', 'INFO'); + this.startSpawning(); + } + + startSpawning() { + this.spawnInterval = setInterval(() => { + if (!this.isGamePaused && !this.isGameOver) { + this.spawnFallingWord(); + } + }, this.spawnRate); + } + + spawnFallingWord() { + if (this.vocabulary.length === 0) return; + + const word = this.vocabulary[this.currentWordIndex % this.vocabulary.length]; + this.currentWordIndex++; + + const gameArea = document.getElementById('game-area'); + const wordElement = document.createElement('div'); + wordElement.className = 'falling-word'; + wordElement.textContent = word.original; + wordElement.style.left = Math.random() * 80 + 10 + '%'; + wordElement.style.top = '-60px'; + + gameArea.appendChild(wordElement); + + this.fallingWords.push({ + element: wordElement, + word: word, + startTime: Date.now() + }); + + // Generate new answer options when word spawns + this.generateAnswerOptions(); + + // Animate falling + this.animateFalling(wordElement); + + // Remove after lifetime + setTimeout(() => { + if (wordElement.parentNode) { + this.missWord(wordElement); + } + }, this.wordLifetime); + } + + animateFalling(wordElement) { + wordElement.style.transition = `top ${this.fallSpeed}ms linear`; + setTimeout(() => { + wordElement.style.top = '100vh'; + }, 50); + } + + generateAnswerOptions() { + if (this.vocabulary.length === 0) return; + + const buttons = []; + const correctWord = this.fallingWords.length > 0 ? + this.fallingWords[this.fallingWords.length - 1].word : + this.vocabulary[0]; + + // Add correct answer + buttons.push(correctWord.translation); + + // Add 3 random incorrect answers + while (buttons.length < 4) { + const randomWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; + if (!buttons.includes(randomWord.translation)) { + buttons.push(randomWord.translation); + } + } + + // Shuffle buttons + this.shuffleArray(buttons); + + // Update answer panel + const answerButtons = document.getElementById('answer-buttons'); + if (answerButtons) { + answerButtons.innerHTML = buttons.map(answer => + `` + ).join(''); + } + } + + checkAnswer(selectedAnswer) { + const activeFallingWords = this.fallingWords.filter(fw => fw.element.parentNode); + + for (let i = 0; i < activeFallingWords.length; i++) { + const fallingWord = activeFallingWords[i]; + if (fallingWord.word.translation === selectedAnswer) { + this.correctAnswer(fallingWord); + return; + } + } + + // Wrong answer + this.wrongAnswer(); + } + + correctAnswer(fallingWord) { + // Remove from game with epic explosion + if (fallingWord.element.parentNode) { + fallingWord.element.classList.add('exploding'); + + // Add screen shake effect + const gameArea = document.getElementById('game-area'); + if (gameArea) { + gameArea.style.animation = 'none'; + gameArea.offsetHeight; // Force reflow + gameArea.style.animation = 'screenShake 0.3s ease-in-out'; + setTimeout(() => { + gameArea.style.animation = ''; + }, 300); + } + + setTimeout(() => { + if (fallingWord.element.parentNode) { + fallingWord.element.remove(); + } + }, 800); + } + + // Remove from tracking + this.fallingWords = this.fallingWords.filter(fw => fw !== fallingWord); + + // Update score + this.combo++; + const points = 10 + (this.combo * 2); + this.score += points; + this.onScoreUpdate(this.score); + + // Update display with animation + document.getElementById('score').textContent = this.score; + document.getElementById('combo').textContent = this.combo; + + // Add points popup animation + this.showPointsPopup(points, fallingWord.element); + + // Vibration feedback (if supported) + if (navigator.vibrate) { + navigator.vibrate([50, 30, 50]); + } + + // Level up check + if (this.score > 0 && this.score % 100 === 0) { + this.levelUp(); + } + } + + wrongAnswer() { + this.combo = 0; + document.getElementById('combo').textContent = this.combo; + + // Enhanced wrong answer animation + const answerPanel = document.getElementById('answer-panel'); + if (answerPanel) { + answerPanel.classList.add('wrong-flash'); + setTimeout(() => { + answerPanel.classList.remove('wrong-flash'); + }, 500); + } + + // Shake all falling words to show disappointment + this.fallingWords.forEach(fw => { + if (fw.element.parentNode && !fw.element.classList.contains('exploding')) { + fw.element.classList.add('wrong-shake'); + setTimeout(() => { + fw.element.classList.remove('wrong-shake'); + }, 600); + } + }); + + // Screen flash red + const gameArea = document.getElementById('game-area'); + if (gameArea) { + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(239, 68, 68, 0.3); + pointer-events: none; + animation: wrongFlash 0.4s ease-in-out; + z-index: 100; + `; + gameArea.appendChild(overlay); + setTimeout(() => { + if (overlay.parentNode) overlay.remove(); + }, 400); + } + + // Wrong answer vibration (stronger/longer) + if (navigator.vibrate) { + navigator.vibrate([200, 100, 200, 100, 200]); + } + } + + showPointsPopup(points, wordElement) { + const popup = document.createElement('div'); + popup.textContent = `+${points}`; + popup.style.cssText = ` + position: absolute; + left: ${wordElement.style.left}; + top: ${wordElement.offsetTop}px; + font-size: 2rem; + font-weight: bold; + color: #10b981; + pointer-events: none; + z-index: 1000; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); + animation: pointsFloat 1.5s ease-out forwards; + `; + + const gameArea = document.getElementById('game-area'); + if (gameArea) { + gameArea.appendChild(popup); + setTimeout(() => { + if (popup.parentNode) popup.remove(); + }, 1500); + } + } + + missWord(wordElement) { + // Remove word + if (wordElement.parentNode) { + wordElement.remove(); + } + + // Remove from tracking + this.fallingWords = this.fallingWords.filter(fw => fw.element !== wordElement); + + // Lose life + this.lives--; + this.combo = 0; + + document.getElementById('lives').textContent = this.lives; + document.getElementById('combo').textContent = this.combo; + + if (this.lives <= 0) { + this.gameOver(); + } + } + + levelUp() { + this.level++; + document.getElementById('level').textContent = this.level; + + // Increase difficulty + this.fallSpeed = Math.max(1000, this.fallSpeed * 0.9); + this.spawnRate = Math.max(800, this.spawnRate * 0.95); + + // Restart intervals with new timing + if (this.spawnInterval) { + clearInterval(this.spawnInterval); + this.startSpawning(); + } + + // Show level up message + const gameArea = document.getElementById('game-area'); + const levelUpMsg = document.createElement('div'); + levelUpMsg.innerHTML = ` +
+

โšก LEVEL UP! โšก

+

Level ${this.level}

+
+ `; + gameArea.appendChild(levelUpMsg); + + setTimeout(() => { + if (levelUpMsg.parentNode) { + levelUpMsg.remove(); + } + }, 2000); + } + + togglePause() { + this.isGamePaused = !this.isGamePaused; + const pauseBtn = document.getElementById('pause-btn'); + if (pauseBtn) { + pauseBtn.textContent = this.isGamePaused ? 'โ–ถ๏ธ Resume' : 'โธ๏ธ Pause'; + } + } + + gameOver() { + this.isGameOver = true; + + // Clear intervals + if (this.spawnInterval) { + clearInterval(this.spawnInterval); + } + + // Clear falling words + this.fallingWords.forEach(fw => { + if (fw.element.parentNode) { + fw.element.remove(); + } + }); + + this.onGameEnd(this.score); + } + + showNoVocabularyMessage() { + this.container.innerHTML = ` +
+
+

๐ŸŒช๏ธ Word Storm

+

โŒ No vocabulary found in this content.

+

This game requires content with vocabulary words.

+ +
+
+ `; + } + + shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + destroy() { + if (this.spawnInterval) { + clearInterval(this.spawnInterval); + } + + // Remove CSS + const styleSheet = document.getElementById('word-storm-styles'); + if (styleSheet) { + styleSheet.remove(); + } + + logSh('Word Storm destroyed', 'INFO'); + } +} + +// Export to global namespace +window.GameModules = window.GameModules || {}; +window.GameModules.WordStorm = WordStormGame; \ No newline at end of file diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..3261b54 --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,328 @@ +/** + * Base Styles - Core styling foundation + * Reset, typography, layout fundamentals + */ + +/* CSS Reset and Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + line-height: 1.5; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + color: #2d3748; + background-color: #f7fafc; + overflow-x: hidden; +} + +/* Layout Containers */ +.app-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.app-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1rem 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: sticky; + top: 0; + z-index: 100; +} + +.header-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.app-title { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.025em; +} + +.app-main { + flex: 1; + max-width: 1200px; + margin: 0 auto; + padding: 2rem 1rem; + width: 100%; +} + +/* Loading Screen */ +.loading-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: white; + z-index: 9999; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 4px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 1s ease-in-out infinite; + margin-bottom: 1rem; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.loading-screen h2 { + font-size: 1.5rem; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.loading-screen p { + font-size: 1rem; + opacity: 0.8; +} + +/* Connection Status */ +.connection-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #48bb78; + animation: pulse 2s infinite; +} + +.status-indicator.online { + background-color: #48bb78; +} + +.status-indicator.offline { + background-color: #f56565; +} + +.status-indicator.connecting { + background-color: #ed8936; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 600; + line-height: 1.25; + margin-bottom: 0.5rem; + color: #2d3748; +} + +h1 { font-size: 2.25rem; } +h2 { font-size: 1.875rem; } +h3 { font-size: 1.5rem; } +h4 { font-size: 1.25rem; } +h5 { font-size: 1.125rem; } +h6 { font-size: 1rem; } + +p { + margin-bottom: 1rem; + color: #4a5568; +} + +/* Error Messages */ +.error-message { + background-color: #fed7d7; + color: #9b2c2c; + padding: 1rem; + border-radius: 0.5rem; + border: 1px solid #feb2b2; + text-align: center; +} + +.error-message h2 { + color: #9b2c2c; + margin-bottom: 0.5rem; +} + +.error-message button { + background-color: #e53e3e; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.25rem; + cursor: pointer; + margin-top: 1rem; + font-size: 0.875rem; + transition: background-color 0.2s; +} + +.error-message button:hover { + background-color: #c53030; +} + +/* Debug Panel */ +.debug-panel { + position: fixed; + top: 70px; + right: 20px; + width: 300px; + background: white; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + z-index: 1000; + font-size: 0.875rem; +} + +.debug-header { + background-color: #2d3748; + color: white; + padding: 0.75rem 1rem; + border-radius: 0.5rem 0.5rem 0 0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.debug-header h3 { + font-size: 0.875rem; + font-weight: 600; + margin: 0; + color: white; +} + +.debug-toggle { + background: none; + border: none; + color: white; + font-size: 1.25rem; + cursor: pointer; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.debug-content { + padding: 1rem; + max-height: 400px; + overflow-y: auto; +} + +.debug-content h4 { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: #718096; + margin-bottom: 0.5rem; +} + +.debug-content ul { + list-style: none; + margin-bottom: 1rem; +} + +.debug-content li { + padding: 0.25rem 0; + font-size: 0.75rem; + color: #4a5568; + border-bottom: 1px solid #f7fafc; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .app-main { + padding: 1rem; + } + + .header-content { + padding: 0 1rem; + } + + .app-title { + font-size: 1.25rem; + } + + .connection-status { + font-size: 0.75rem; + } + + .status-indicator { + width: 6px; + height: 6px; + } + + .debug-panel { + width: calc(100% - 40px); + right: 20px; + left: 20px; + } + + .loading-screen h2 { + font-size: 1.25rem; + } + + .loading-screen p { + font-size: 0.875rem; + } +} + +/* Accessibility */ +@media (prefers-reduced-motion: reduce) { + .loading-spinner, + .status-indicator { + animation: none; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + body { + background-color: white; + color: black; + } + + .app-header { + background: #000; + color: white; + } + + .debug-panel { + border: 2px solid black; + } +} \ No newline at end of file diff --git a/src/styles/components.css b/src/styles/components.css new file mode 100644 index 0000000..6d696c3 --- /dev/null +++ b/src/styles/components.css @@ -0,0 +1,441 @@ +/** + * Component Styles - Reusable UI component styles + * Buttons, cards, modals, forms, and other interactive elements + */ + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + border: none; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.2s ease-in-out; + min-height: 44px; /* Touch-friendly minimum */ + gap: 0.5rem; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Button Variants */ +.btn-primary { + background-color: #3182ce; + color: white; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.btn-primary:hover:not(:disabled) { + background-color: #2c5282; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(49, 130, 206, 0.3); +} + +.btn-secondary { + background-color: #edf2f7; + color: #4a5568; + border: 1px solid #e2e8f0; +} + +.btn-secondary:hover:not(:disabled) { + background-color: #e2e8f0; + border-color: #cbd5e0; +} + +.btn-success { + background-color: #38a169; + color: white; +} + +.btn-success:hover:not(:disabled) { + background-color: #2f855a; +} + +.btn-danger { + background-color: #e53e3e; + color: white; +} + +.btn-danger:hover:not(:disabled) { + background-color: #c53030; +} + +/* Button Sizes */ +.btn-sm { + padding: 0.5rem 1rem; + font-size: 0.75rem; + min-height: 36px; +} + +.btn-lg { + padding: 1rem 2rem; + font-size: 1rem; + min-height: 52px; +} + +/* Cards */ +.card { + background: white; + border-radius: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + border: 1px solid #e2e8f0; + overflow: hidden; + transition: all 0.2s ease-in-out; +} + +.card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +.card-header { + padding: 1rem; + border-bottom: 1px solid #e2e8f0; + background-color: #f7fafc; +} + +.card-body { + padding: 1rem; +} + +.card-footer { + padding: 1rem; + border-top: 1px solid #e2e8f0; + background-color: #f7fafc; +} + +/* Grid Layout */ +.grid { + display: grid; + gap: 1rem; +} + +.grid-cols-1 { grid-template-columns: repeat(1, 1fr); } +.grid-cols-2 { grid-template-columns: repeat(2, 1fr); } +.grid-cols-3 { grid-template-columns: repeat(3, 1fr); } +.grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + +@media (max-width: 768px) { + .grid-cols-2, + .grid-cols-3, + .grid-cols-4 { + grid-template-columns: 1fr; + } +} + +/* Modal */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +} + +.modal { + background: white; + border-radius: 0.5rem; + box-shadow: 0 20px 25px rgba(0, 0, 0, 0.25); + max-width: 500px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + animation: modalSlideIn 0.3s ease-out; +} + +.modal-header { + padding: 1.5rem; + border-bottom: 1px solid #e2e8f0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-title { + font-size: 1.125rem; + font-weight: 600; + margin: 0; +} + +.modal-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #718096; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.25rem; +} + +.modal-close:hover { + background-color: #f7fafc; + color: #2d3748; +} + +.modal-body { + padding: 1.5rem; +} + +.modal-footer { + padding: 1.5rem; + border-top: 1px solid #e2e8f0; + display: flex; + gap: 0.5rem; + justify-content: flex-end; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* Forms */ +.form-group { + margin-bottom: 1rem; +} + +.form-label { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: #374151; + margin-bottom: 0.25rem; +} + +.form-input { + width: 100%; + padding: 0.75rem; + font-size: 0.875rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + background-color: white; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.form-input:focus { + outline: none; + border-color: #3182ce; + box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1); +} + +.form-input:invalid { + border-color: #e53e3e; +} + +.form-textarea { + resize: vertical; + min-height: 100px; +} + +.form-select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; +} + +/* Progress Bar */ +.progress { + width: 100%; + height: 0.5rem; + background-color: #e2e8f0; + border-radius: 0.25rem; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background-color: #3182ce; + border-radius: 0.25rem; + transition: width 0.3s ease; +} + +.progress-bar.success { + background-color: #38a169; +} + +.progress-bar.warning { + background-color: #d69e2e; +} + +.progress-bar.danger { + background-color: #e53e3e; +} + +/* Badges */ +.badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.badge-primary { + background-color: #dbeafe; + color: #1e40af; +} + +.badge-success { + background-color: #d1fae5; + color: #065f46; +} + +.badge-warning { + background-color: #fef3c7; + color: #92400e; +} + +.badge-danger { + background-color: #fee2e2; + color: #991b1b; +} + +/* Tooltips */ +.tooltip { + position: relative; + display: inline-block; + cursor: help; +} + +.tooltip:hover::after { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #1a202c; + color: white; + padding: 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + white-space: nowrap; + z-index: 1000; + margin-bottom: 0.25rem; +} + +.tooltip:hover::before { + content: ''; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: #1a202c; + z-index: 1000; +} + +/* Alerts */ +.alert { + padding: 1rem; + border-radius: 0.375rem; + margin-bottom: 1rem; + border: 1px solid transparent; +} + +.alert-info { + background-color: #dbeafe; + border-color: #bfdbfe; + color: #1e40af; +} + +.alert-success { + background-color: #d1fae5; + border-color: #a7f3d0; + color: #065f46; +} + +.alert-warning { + background-color: #fef3c7; + border-color: #fde68a; + color: #92400e; +} + +.alert-danger { + background-color: #fee2e2; + border-color: #fecaca; + color: #991b1b; +} + +/* Utility Classes */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.hidden { display: none !important; } +.visible { display: block !important; } + +.mt-1 { margin-top: 0.25rem; } +.mt-2 { margin-top: 0.5rem; } +.mt-4 { margin-top: 1rem; } +.mb-1 { margin-bottom: 0.25rem; } +.mb-2 { margin-bottom: 0.5rem; } +.mb-4 { margin-bottom: 1rem; } + +.p-1 { padding: 0.25rem; } +.p-2 { padding: 0.5rem; } +.p-4 { padding: 1rem; } + +.w-full { width: 100%; } +.h-full { height: 100%; } + +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.gap-2 { gap: 0.5rem; } +.gap-4 { gap: 1rem; } + +/* Animations */ +.fade-in { + animation: fadeIn 0.3s ease-in; +} + +.slide-up { + animation: slideUp 0.3s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..996a61d --- /dev/null +++ b/start.bat @@ -0,0 +1,27 @@ +@echo off +title Class Generator - Development Server + +echo. +echo ๐Ÿš€ Starting Class Generator Development Server... +echo. + +:: Check if Node.js is installed +node --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo โŒ Node.js is not installed or not in PATH + echo Please install Node.js from https://nodejs.org/ + pause + exit /b 1 +) + +:: Start the server +echo โœ… Node.js found +echo ๐Ÿ”„ Starting server... +echo. + +node server.js + +:: If we get here, the server stopped +echo. +echo ๐Ÿ‘‹ Server stopped +pause \ No newline at end of file