From 325b97060ce756b8a3c03f060280a22b4830ac0f Mon Sep 17 00:00:00 2001 From: StillHammer Date: Wed, 15 Oct 2025 07:25:53 +0800 Subject: [PATCH] Add LEDU Chinese course content and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive Chinese reading course (乐读) with 4 chapters of vocabulary, texts, and exercises. Include architecture documentation for module development and progress tracking system. Content: - LEDU book metadata with 12 chapter outline - Chapter 1: Food culture (民以食为天) - 45+ vocabulary, etiquette - Chapter 2: Shopping (货比三家) - comparative shopping vocabulary - Chapter 3: Sports & fitness (生命在于运动) - exercise habits - Chapter 4: Additional vocabulary and grammar Documentation: - Architecture principles and patterns - Module creation guide (Game, DRS, Progress) - Interface system (C++ style contracts) - Progress tracking and prerequisites Game Enhancements: - MarioEducational helper classes (Physics, Renderer, Sound, Enemies) - VocabularyModule TTS improvements - Updated CLAUDE.md with project status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 1426 +------------ content/books/ledu.json | 206 ++ content/chapters/ledu-chapter1.json | 548 +++++ content/chapters/ledu-chapter2.json | 750 +++++++ content/chapters/ledu-chapter3.json | 643 ++++++ content/chapters/ledu-chapter4.json | 762 +++++++ docs/architecture.md | 259 +++ docs/creating-new-module.md | 312 +++ docs/interfaces.md | 314 +++ docs/progress-system.md | 268 +++ src/DRS/exercise-modules/VocabularyModule.js | 101 +- .../MarioEducational/PhysicsEngine.js | 343 ++++ src/gameHelpers/MarioEducational/README.md | 123 ++ src/gameHelpers/MarioEducational/Renderer.js | 625 ++++++ .../MarioEducational/SentenceGenerator.js | 386 ++++ .../MarioEducational/SoundSystem.js | 272 +++ .../MarioEducational/enemies/Boss.js | 254 +++ .../MarioEducational/enemies/Catapult.js | 347 ++++ .../MarioEducational/enemies/FlyingEye.js | 187 ++ .../MarioEducational/enemies/PiranhaPlant.js | 133 ++ .../MarioEducational/enemies/Projectile.js | 147 ++ src/games/MarioEducational.js | 1820 +---------------- 22 files changed, 7131 insertions(+), 3095 deletions(-) create mode 100644 content/books/ledu.json create mode 100644 content/chapters/ledu-chapter1.json create mode 100644 content/chapters/ledu-chapter2.json create mode 100644 content/chapters/ledu-chapter3.json create mode 100644 content/chapters/ledu-chapter4.json create mode 100644 docs/architecture.md create mode 100644 docs/creating-new-module.md create mode 100644 docs/interfaces.md create mode 100644 docs/progress-system.md create mode 100644 src/gameHelpers/MarioEducational/PhysicsEngine.js create mode 100644 src/gameHelpers/MarioEducational/README.md create mode 100644 src/gameHelpers/MarioEducational/Renderer.js create mode 100644 src/gameHelpers/MarioEducational/SentenceGenerator.js create mode 100644 src/gameHelpers/MarioEducational/SoundSystem.js create mode 100644 src/gameHelpers/MarioEducational/enemies/Boss.js create mode 100644 src/gameHelpers/MarioEducational/enemies/Catapult.js create mode 100644 src/gameHelpers/MarioEducational/enemies/FlyingEye.js create mode 100644 src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js create mode 100644 src/gameHelpers/MarioEducational/enemies/Projectile.js diff --git a/CLAUDE.md b/CLAUDE.md index a86f18f..29963b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,1392 +1,90 @@ -# CLAUDE.md - Project Context & Instructions +# CLAUDE.md - Class Generator 2.0 -## 📋 Project Overview +## 📋 Overview +Educational platform with ultra-modular vanilla JS/HTML/CSS architecture. Strict separation, sealed modules, dependency injection. -**Class Generator 2.0** - Complete rewrite of educational games platform with ultra-modular architecture. +## 🏗️ Status -### 🎯 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. +**Core ✅**: Module.js, EventBus.js, ModuleLoader.js, Router.js, Application.js, Dev Server +**DRS ✅**: ContentLoader, IAEngine (OpenAI→DeepSeek), LLMValidator, AI Reports, UnifiedDRS +**Exercises ✅**: VocabularyModule, TextAnalysis, GrammarAnalysis, Translation, OpenResponse +**AI ✅**: Production ready, strict scoring (wrong: 0-20, correct: 70-100), no mock fallbacks -### 🏗️ Architecture Status +## ⚠️ Critical Rules -**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 +### Architecture (NON-NEGOTIABLE) +1. **Single responsibility** per module +2. **EventBus only** for communication (no direct deps) +3. **Object.seal()** prevents modification +4. **WeakMap** for private state +5. **Abstract enforcement** - missing methods = fatal error +6. **Dependency injection** - no globals -**DRS SYSTEM COMPLETED ✅** - Advanced learning modules with dual AI approach: - -**Core Exercise Generation:** -- ✅ **ContentLoader** - Pure AI content generation when no real content available -- ✅ **IAEngine** - Multi-provider AI system (OpenAI → DeepSeek → Hard Fail) -- ✅ **LLMValidator** - Intelligent answer validation with detailed feedback -- ✅ **AI Report System** - Session tracking with exportable reports (text/HTML/JSON) -- ✅ **UnifiedDRS** - Component-based exercise presentation system - -**DRS Exercise Modules:** -- ✅ **Vocabulary Flashcards** - VocabularyModule provides spaced repetition learning (local validation, no AI) -- ✅ **Intelligent QCM** - AI generates questions + 1 correct + 5 plausible wrong answers (16.7% random chance) -- ✅ **Open Analysis Modules** - Free-text responses validated by AI with personalized feedback - - ✅ TextAnalysisModule - Deep comprehension with AI coaching (0-100 strict scoring) - - ✅ GrammarAnalysisModule - Grammar correction with explanations (0-100 strict scoring) - - ✅ TranslationModule - Translation validation with multi-language support (0-100 strict scoring) - - ✅ OpenResponseModule - Free-form questions with intelligent evaluation - -**AI Architecture - PRODUCTION READY:** -- ✅ **AI-Mandatory System** - No mock/fallback, real AI only, ensures educational quality -- ✅ **Strict Scoring Logic** - Wrong answers: 0-20 points, Correct answers: 70-100 points -- ✅ **Multi-Provider Fallback** - OpenAI → DeepSeek → Hard Fail (no fake responses) -- ✅ **Comprehensive Testing** - 100% validation with multiple test scenarios -- ✅ **Smart Prompt Engineering** - Context-aware prompts with proper language detection -- ⚠️ **Cache System** - Currently disabled for testing (see Cache Management section) - -## ⚠️ CRITICAL ARCHITECTURAL SEPARATION - -### 🎯 DRS vs Games - NEVER MIX - -**DRS (Dynamic Response System)** - Educational exercise modules in `src/DRS/`: -- ✅ **VocabularyModule.js** - Spaced repetition flashcards (local validation) -- ✅ **TextAnalysisModule.js** - AI-powered text comprehension -- ✅ **GrammarAnalysisModule.js** - AI grammar correction -- ✅ **TranslationModule.js** - AI translation validation -- ✅ **All modules in `src/DRS/`** - Educational exercises with strict interface compliance - -**Games** - Independent game modules in `src/games/`: -- ❌ **FlashcardLearning.js** - Standalone flashcard game (NOT part of DRS) -- ❌ **Other game modules** - Entertainment-focused, different architecture -- ❌ **NEVER import games into DRS** - Violates separation of concerns - -### 🚫 FORBIDDEN MIXING -```javascript -// ❌ NEVER DO THIS - DRS importing games -import FlashcardLearning from '../games/FlashcardLearning.js'; - -// ✅ CORRECT - DRS uses its own modules -import VocabularyModule from './exercise-modules/VocabularyModule.js'; -``` - -**Rule**: **DRS = Educational Exercises**, **Games = Entertainment**. They MUST remain separate. - -## 🔥 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 +### DRS vs Games - NEVER MIX +- **DRS** (`src/DRS/`) = Educational exercises with strict interfaces +- **Games** (`src/games/`) = Entertainment, different architecture +- ❌ NEVER `import FlashcardLearning from '../games/'` in DRS +- ✅ ALWAYS `import VocabularyModule from './exercise-modules/'` in DRS ### 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 +- Vanilla JS/HTML/CSS only (no frameworks) +- ES6 modules, HTTP protocol (never file://) +- **NO SCROLL** - all UI fits viewport height +- Mobile-first, compact headers, vertical space precious -### UI/UX Design Principles (CRITICAL) -- **NO SCROLL POLICY** - All interfaces MUST fit within viewport height without scrolling -- **Height Management** - Vertical space is precious, horizontal space is abundant -- **Compact Navigation** - Top bars and headers must be minimal height -- **Responsive Layout** - Use available width, preserve viewport height -- **Mobile-First** - Design for smallest screens first, then scale up +### Development DO's/DON'Ts +✅ Extend Module base, use EventBus, validate deps, seal objects, simple solutions first +❌ Never access internals, globals, skip validation, hardcode paths, overcomplicate positioning -## 🚀 Development Workflow - -### Starting the System +## 🚀 Quick Start ```bash -# Option 1: Windows batch file -start.bat - -# Option 2: Node.js directly -node server.js - -# Option 3: NPM scripts -npm start +start.bat # or: node server.js +# http://localhost:3000 ``` -**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 - +## 📁 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 -│ ├── DRS/ # COMPLETED - DRS educational modules -│ │ ├── exercise-modules/ # Educational exercise modules -│ │ ├── services/ # AI and validation services -│ │ └── interfaces/ # Module interfaces -│ ├── games/ # SEPARATE - Independent game modules -│ │ └── FlashcardLearning.js # External flashcard game (NOT part of DRS) -│ ├── components/ # TODO - UI components -│ ├── 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 +src/ +├── core/ # Module, EventBus, ModuleLoader, Router, Application +├── DRS/ # Exercise modules, services, interfaces +├── games/ # Independent game modules (NOT DRS) +├── styles/ # base.css, components.css +└── Application.js # Bootstrap ``` -## 🎮 Creating New Modules +## 📚 Documentation -### Game Module Template +**Core Guides:** +- `docs/architecture.md` - Architecture principles and patterns +- `docs/creating-new-module.md` - How to create new modules (Game, DRS, Progress) +- `docs/interfaces.md` - Interface system (C++ style contracts) +- `docs/progress-system.md` - Progress tracking and prerequisites + +## 🔍 Debug ```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 +// F12 toggles debug panel +window.app.getStatus() 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 - -### 📚 DRS Flashcard System - -**VocabularyModule.js** serves as the DRS's integrated flashcard system: -- ✅ **Spaced Repetition** - Again, Hard, Good, Easy difficulty selection -- ✅ **Local Validation** - No AI required, simple string matching with fuzzy logic -- ✅ **Mastery Tracking** - Integration with PrerequisiteEngine -- ✅ **Word Discovery Integration** - WordDiscoveryModule transitions to VocabularyModule -- ✅ **Full UI** - Card-based interface with pronunciation, progress tracking - -**This eliminates the need for external flashcard games in DRS context.** - -### 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 -- ✅ **Start with simple solutions first** - Test basic functionality before adding complexity -- ✅ **Test code in console first** - Validate logic with quick console tests before file changes - -### 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) -- ❌ **NEVER HARDCODE JSON PATHS** - Always use dynamic paths based on selected book/chapter -- ❌ **Never overcomplicate positioning logic** - Use simple CSS transforms (translate(-50%, -50%)) for centering before complex calculations - -## 🧠 Problem-Solving Best Practices - -### UI Positioning Issues -1. **Start Simple**: Use basic CSS positioning (center with transform) first -2. **Test in Console**: Validate positioning logic with `console.log` and direct DOM manipulation -3. **Check Scope**: Ensure variables like `contentLoader` are globally accessible when needed -4. **Cache-bust**: Add `?v=2` to CSS/JS files when browser cache causes issues -5. **Verify Real Dimensions**: Use `getBoundingClientRect()` only when basic centering fails - -### Debugging Workflow -1. **Console First**: Test functions directly in browser console before modifying files -2. **Log Everything**: Add extensive logging to understand execution flow -3. **One Change at a Time**: Make incremental changes and test each step -4. **Simple Solutions Win**: Prefer `left: 50%; transform: translateX(-50%)` over complex calculations +## 🗄️ AI Cache +Cache currently disabled for testing. See `IAEngine.js` lines 165-170. +**Why**: 100-char cache key too short. **Fix**: Improve to 200+ chars with language/exerciseType/contentHash. ## 🎯 Success Metrics +- **<100ms** module loading +- **<50ms** event propagation +- **<200ms** startup +- **Zero** memory leaks +- **Zero** direct coupling -### 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 +## 🚧 Next Phase +1. Component-based UI system +2. Example game module +3. Content system integration +4. Testing framework --- -## 📋 COMPREHENSIVE TEST CHECKLIST - -### 🏗️ Architecture Tests - -#### Core System Tests -- [ ] **Module.js Tests** - - [ ] Abstract class cannot be instantiated directly - - [ ] WeakMap private data is truly private - - [ ] Object.seal() prevents modification - - [ ] Lifecycle methods work correctly (init, destroy) - - [ ] Validation methods throw appropriate errors - -- [ ] **EventBus.js Tests** - - [ ] Event registration and deregistration - - [ ] Module validation before event usage - - [ ] Event history tracking - - [ ] Cross-module communication isolation - - [ ] Memory leak prevention on module destroy - -- [ ] **ModuleLoader.js Tests** - - [ ] Dependency injection order - - [ ] Circular dependency detection - - [ ] Module initialization sequence - - [ ] Error handling for missing dependencies - - [ ] Module unloading and cleanup - -- [ ] **Router.js Tests** - - [ ] Navigation guards functionality - - [ ] Middleware execution order - - [ ] State management - - [ ] URL parameter handling - - [ ] History management - -- [ ] **Application.js Tests** - - [ ] Auto-bootstrap system - - [ ] Lifecycle management - - [ ] Module registration - - [ ] Error recovery - - [ ] Debug panel functionality - -### 🎮 DRS System Tests - -#### Module Interface Tests -- [ ] **ExerciseModuleInterface Tests** - - [ ] All required methods implemented - - [ ] Method signatures correct - - [ ] Error throwing for abstract methods - -#### Individual Module Tests -- [ ] **TextModule Tests** - - [ ] Text loading and display - - [ ] Question generation/extraction - - [ ] AI validation with fallback - - [ ] Progress tracking - - [ ] UI interaction (buttons, inputs) - - [ ] Viewing time tracking - - [ ] Results calculation - -- [ ] **AudioModule Tests** - - [ ] Audio playback controls - - [ ] Playback counting - - [ ] Transcript reveal timing - - [ ] AI audio analysis - - [ ] Progress tracking - - [ ] Penalty system for excessive playbacks - -- [ ] **ImageModule Tests** - - [ ] Image loading and display - - [ ] Zoom functionality - - [ ] Observation time tracking - - [ ] AI vision analysis - - [ ] Question types (description, details, interpretation) - - [ ] Progress tracking - -- [ ] **GrammarModule Tests** - - [ ] Rule explanation display - - [ ] Exercise type variety (fill-blank, correction, etc.) - - [ ] Hint system - - [ ] Attempt tracking - - [ ] AI grammar analysis - - [ ] Scoring with penalties/bonuses - -### 🤖 AI Integration Tests - -#### AI Provider Tests -- [ ] **OpenAI Integration** - - [ ] API connectivity test - - [ ] Response format validation - - [ ] Error handling - - [ ] Timeout management - -- [ ] **DeepSeek Integration** - - [ ] API connectivity test - - [ ] Fallback from OpenAI - - [ ] Response format validation - - [ ] Error handling - -- [ ] **AI Fallback System** - - [ ] Provider switching logic - - [ ] Graceful degradation to basic validation - - [ ] Status tracking and reporting - - [ ] Recovery mechanisms - -#### Response Parsing Tests -- [ ] **Structured Response Parsing** - - [ ] [answer]yes/no extraction - - [ ] [explanation] extraction - - [ ] Error handling for malformed responses - - [ ] Multiple format support - -### 💾 Data Persistence Tests - -#### Progress Tracking Tests -- [ ] **Mastery Tracking** - - [ ] Timestamp recording - - [ ] Metadata storage - - [ ] Progress calculation - - [ ] Persistent storage integration - -- [ ] **Data Merge System** - - [ ] Local vs external data merging - - [ ] Conflict resolution strategies - - [ ] Import/export functionality - - [ ] Data integrity validation - -### 🎨 UI/UX Tests - -#### Design Principles Tests -- [ ] **No Scroll Policy** - - [ ] All interfaces fit viewport height - - [ ] Responsive breakpoint testing - - [ ] Mobile viewport compliance - -- [ ] **Responsive Design** - - [ ] Mobile-first approach validation - - [ ] Horizontal space utilization - - [ ] Vertical space conservation - -#### Component Tests -- [ ] **Button Interactions** - - [ ] Hover effects - - [ ] Disabled states - - [ ] Click handlers - - [ ] Loading states - -- [ ] **Form Controls** - - [ ] Input validation - - [ ] Error display - - [ ] Accessibility compliance - - [ ] Keyboard navigation - -### 🌐 Network & Server Tests - -#### Development Server Tests -- [ ] **ES6 Modules Support** - - [ ] Import/export functionality - - [ ] MIME type handling - - [ ] CORS configuration - -- [ ] **Caching Strategy** - - [ ] Assets cached correctly - - [ ] HTML not cached for development - - [ ] Cache invalidation - -- [ ] **Error Handling** - - [ ] 404 page display - - [ ] Graceful error recovery - - [ ] Error message clarity - -### 🔄 Integration Tests - -#### End-to-End Scenarios -- [ ] **Complete Exercise Flow** - - [ ] Module loading - - [ ] Exercise presentation - - [ ] User interaction - - [ ] AI validation - - [ ] Progress saving - - [ ] Results display - -- [ ] **Multi-Module Navigation** - - [ ] Module switching - - [ ] State preservation - - [ ] Memory cleanup - -- [ ] **Data Persistence Flow** - - [ ] Progress tracking across sessions - - [ ] Data export/import - - [ ] Sync functionality - -### ⚡ Performance Tests - -#### Loading Performance -- [ ] **Module Loading Times** - - [ ] <100ms module loading - - [ ] <50ms event propagation - - [ ] <200ms application startup - -#### Memory Management -- [ ] **Memory Leaks** - - [ ] Module cleanup verification - - [ ] Event listener removal - - [ ] DOM element cleanup - -### 🔒 Security Tests - -#### Module Isolation Tests -- [ ] **Private State Protection** - - [ ] WeakMap data inaccessible - - [ ] Sealed object modification prevention - - [ ] Cross-module boundary enforcement - -#### Input Validation Tests -- [ ] **Boundary Validation** - - [ ] All inputs validated - - [ ] Error messages for violations - - [ ] Malicious input handling - -### 🎯 TESTING PRIORITY - -#### **HIGH PRIORITY** (Core System) -1. Module.js lifecycle and sealing tests -2. EventBus communication isolation -3. ModuleLoader dependency injection -4. Basic DRS module functionality - -#### **MEDIUM PRIORITY** (Integration) -1. AI provider fallback system -2. Data persistence and merging -3. UI/UX compliance tests -4. End-to-end exercise flows - -#### **LOW PRIORITY** (Polish) -1. Performance benchmarks -2. Advanced security tests -3. Edge case scenarios -4. Browser compatibility - ---- - -## 🗄️ AI Cache Management - -### Current Status -The AI response cache system is **currently disabled** to ensure accurate testing and debugging of the scoring logic. - -### Cache System Overview -The cache improves performance and reduces API costs by storing AI responses for similar prompts. - -**Cache Logic (src/DRS/services/IAEngine.js):** -```javascript -// Lines 165-170: Cache check (currently commented out) -const cacheKey = this._generateCacheKey(prompt, options); -if (this.cache.has(cacheKey)) { - this.stats.cacheHits++; - this._log('📦 Cache hit for educational validation'); - return this.cache.get(cacheKey); -} - -// Lines 198: Cache storage (still active) -this.cache.set(cacheKey, result); -``` - -### ⚠️ Why Cache is Disabled -During testing, we discovered the cache key generation uses only the first 100 characters of prompts: -```javascript -_generateCacheKey(prompt, options) { - const keyData = { - prompt: prompt.substring(0, 100), // PROBLEMATIC - Too short - temperature: options.temperature || 0.3, - type: this._detectExerciseType(prompt) - }; - return JSON.stringify(keyData); -} -``` - -**Problems identified:** -- ❌ Different questions with similar beginnings share cache entries -- ❌ False consistency in test results (all 100% same scores) -- ❌ Masks real AI variance and bugs -- ❌ Wrong answers getting cached as correct answers - -### 🔧 How to Re-enable Cache - -**Option 1: Simple Re-activation (Testing Complete)** -```javascript -// In src/DRS/services/IAEngine.js, lines 165-170 -// Uncomment these lines: -const cacheKey = this._generateCacheKey(prompt, options); -if (this.cache.has(cacheKey)) { - this.stats.cacheHits++; - this._log('📦 Cache hit for educational validation'); - return this.cache.get(cacheKey); -} -``` - -**Option 2: Improved Cache Key (Recommended)** -```javascript -_generateCacheKey(prompt, options) { - const keyData = { - prompt: prompt.substring(0, 200), // Increase from 100 to 200 - temperature: options.temperature || 0.3, - type: this._detectExerciseType(prompt), - // Add more distinguishing factors: - language: options.language, - exerciseType: options.exerciseType, - contentHash: this._hashContent(prompt) // Full content hash - }; - return JSON.stringify(keyData); -} -``` - -**Option 3: Selective Caching** -```javascript -// Only cache if prompt is long enough and specific enough -if (prompt.length > 150 && options.exerciseType) { - const cacheKey = this._generateCacheKey(prompt, options); - if (this.cache.has(cacheKey)) { - // ... cache logic - } -} -``` - -### 🎯 Production Recommendations - -**For Production Use:** -1. **Re-enable cache** after comprehensive testing -2. **Improve cache key** to include more context -3. **Monitor cache hit rates** (target: 30-50%) -4. **Set cache expiration** (e.g., 24 hours) -5. **Cache size limits** (currently: 1000 entries) - -**For Development/Testing:** -1. **Keep cache disabled** during AI prompt development -2. **Enable only for performance testing** -3. **Clear cache between test suites** - -### 📊 Cache Performance Benefits -When properly configured: -- **Cost Reduction**: 40-60% fewer API calls -- **Speed Improvement**: Instant responses for repeated content -- **Rate Limiting**: Avoids API limits during peak usage -- **Reliability**: Reduces dependency on external AI services - -### 🔍 Cache Monitoring -Access cache statistics: -```javascript -window.app.getCore().iaEngine.stats.cacheHits -window.app.getCore().iaEngine.cache.size -``` - ---- - -## 🧪 AI Testing Results - -### Final Validation (Without Cache) -**Test Date**: December 2024 -**Scoring Accuracy**: 100% (4/4 test cases passed) - -**Test Results:** -- ✅ **Wrong Science Answer**: 0 points (expected: 0-30) -- ✅ **Correct History Answer**: 90 points (expected: 70-100) -- ✅ **Wrong Translation**: 0 points (expected: 0-30) -- ✅ **Correct Spanish Translation**: 100 points (expected: 70-100) - -**Bug Fixed**: Translation prompt now correctly uses `context.toLang` instead of hardcoded languages. - -**System Status**: ✅ **PRODUCTION READY** -- Real AI scoring (no mock responses) -- Strict scoring logic enforced -- Multi-language support working -- OpenAI → DeepSeek fallback functional - ---- - -## 🧠 Intelligent Content Dependency System - -### Smart Vocabulary Prerequisites - -**NEW APPROACH**: Instead of forcing vocabulary based on arbitrary mastery percentages, the system now uses **intelligent content dependency analysis**. - -#### 🎯 **Core Logic** - -**Before executing any exercise, analyze the next content:** - -1. **Content Analysis** - Extract all words from upcoming content (phrases, texts, dialogs) -2. **Dependency Check** - For each word in content: - - Is it in our vocabulary module list? - - Is it already discovered by the user? -3. **Smart Decision** - Only force vocabulary if content has undiscovered words that are in our vocabulary list -4. **Targeted Learning** - Focus vocabulary practice on words actually needed for next content - -#### 🏗️ **Implementation Architecture** - -**ContentDependencyAnalyzer Class:** -```javascript -class ContentDependencyAnalyzer { - analyzeContentDependencies(nextContent, vocabularyModule) { - const wordsInContent = this.extractWordsFromContent(nextContent); - const vocabularyWords = vocabularyModule.getVocabularyWords(); - const missingWords = this.findMissingWords(wordsInContent, vocabularyWords); - - return { - hasUnmetDependencies: missingWords.length > 0, - missingWords: missingWords, - totalWordsInContent: wordsInContent.length - }; - } -} -``` - -**Smart Override Logic:** -```javascript -_shouldUseWordDiscovery(exerciseType, exerciseConfig) { - const nextContent = await this.getNextContent(exerciseConfig); - const analysis = this.analyzer.analyzeContentDependencies(nextContent, this.vocabularyModule); - - if (analysis.hasUnmetDependencies) { - window.vocabularyOverrideActive = { - originalType: exerciseConfig.type, - reason: `Content requires ${analysis.missingWords.length} undiscovered words`, - missingWords: analysis.missingWords - }; - return true; - } - return false; -} -``` - -#### 🎯 **User Experience Impact** - -**Before (Dumb System):** -- "Vocabulary mastery too low (15%), forcing flashcards" -- User learns random words not related to next content -- Arbitrary percentage-based decisions - -**After (Smart System):** -- "Next content requires these words: refrigerator, elevator, closet" -- User learns exactly the words needed for comprehension -- Content-driven vocabulary acquisition - -#### 📊 **Smart Guide Integration** - -**Interface Updates:** -``` -📚 Vocabulary Practice (3 words needed for next content) -Type: 📚 Vocabulary Practice -Mode: Adaptive Flashcards -Why this exercise? -Next content requires these words: refrigerator, elevator, closet. Learning vocabulary first ensures comprehension. -``` - -#### 🔧 **Key Functions** - -- `extractWordsFromContent()` - Parse text/phrases/dialogs for vocabulary -- `findMissingWords()` - Identify vocabulary words that aren't discovered -- `getNextContent()` - Fetch upcoming exercise content for analysis -- `updateVocabularyOverrideUI()` - Smart Guide interface adaptation - -#### ✅ **Benefits** - -- **Targeted Learning** - Only learn words actually needed -- **Context-Driven** - Vocabulary tied to real content usage -- **Efficient Progress** - No time wasted on irrelevant words -- **Better Retention** - Words learned in context of upcoming usage -- **Smart Adaptation** - UI accurately reflects what's happening - -**Status**: ✅ **DESIGN READY FOR IMPLEMENTATION** - ---- - -## 📊 ROBUST PROGRESS SYSTEM - Ultra-Strict Architecture - -### 🎯 Core Philosophy - -**FUNDAMENTAL RULE**: Every piece of content is a trackable progress item with strict validation and type safety. - -### 🏗️ Pedagogical Flow (NON-NEGOTIABLE) - -``` -1. DISCOVERY → 2. MASTERY → 3. APPLICATION - (passive) (active) (context) -``` - -**Flow Rules:** -- ❌ **NO Flashcards on undiscovered words** - Must discover first -- ❌ **NO Text exercises on unmastered vocabulary** - Must master first -- ✅ **Always check prerequisites before ANY exercise** -- ✅ **Form vocabulary lists on-the-fly** from next exercise content - -### 📦 Progress Item System - -#### **Item Types & Weights** - -Each content piece = 1 or more progress items with defined weights: - -| Type | Weight | Description | Prerequisites | -|------|--------|-------------|---------------| -| **vocabulary-discovery** | 1 | Passive exposure to new word | None | -| **vocabulary-mastery** | 1 | Active flashcard practice | Must be discovered | -| **phrase** | 6 | Phrase comprehension (3x vocab) | Vocabulary mastered | -| **dialog** | 12 | Dialog comprehension (6x vocab, complex) | Vocabulary mastered | -| **text** | 15 | Full text analysis (7.5x vocab, most complex) | Vocabulary mastered | -| **audio** | 12 | Audio comprehension (6x vocab) | Vocabulary mastered | -| **image** | 6 | Image description (3x vocab) | Vocabulary discovered | -| **grammar** | 6 | Grammar rules (3x vocab) | Vocabulary discovered | - -**Total for 1 vocabulary word** = 2 points (1 discovery + 1 mastery) - -#### **Strict Interface Contract** - -```javascript -// ALL progress items MUST implement this interface -class ProgressItemInterface { - validate() // ⚠️ REQUIRED - Validate item data - serialize() // ⚠️ REQUIRED - Convert to JSON - getWeight() // ⚠️ REQUIRED - Return item weight - canComplete(state) // ⚠️ REQUIRED - Check prerequisites -} -``` - -**Missing implementation = FATAL ERROR** (red screen, app refuses to start) - -### 🔥 Ultra-Strict Validation System - -#### **Runtime Checks** - -**At Application Startup:** -1. Validate ALL item implementations -2. Check ALL methods are implemented -3. Verify weight calculations -4. Test prerequisite logic - -**If ANY validation fails:** -- 🔴 **Full-screen red error overlay** -- 🔊 **Alert sound** (dev mode) -- 📳 **Screen shake animation** -- 🚨 **Giant error message with:** - - Class name - - Missing method name - - Stack trace -- ❌ **Application REFUSES to start** - -#### **Error Display Example** - -``` -┌─────────────────────────────────────────────┐ -│ │ -│ 🔥 FATAL ERROR 🔥 │ -│ │ -│ Implementation Missing │ -│ │ -│ Class: VocabularyMasteryItem │ -│ Missing Method: canComplete() │ -│ │ -│ ❌ MUST implement all interface methods │ -│ │ -│ [ Dismiss (Fix Required!) ] │ -│ │ -└─────────────────────────────────────────────┘ -``` - -**Impossible to ignore. Impossible to skip. Forces correct implementation.** - -### 📈 Progress Calculation - -#### **Chapter Analysis** - -When loading a chapter, the system: - -1. **Scans ALL content** (vocabulary, phrases, dialogs, texts, etc.) -2. **Creates progress items** for each piece -3. **Calculates total weight** (sum of all item weights) -4. **Stores item registry** for tracking - -**Example Chapter:** -- 171 vocabulary words → 342 points (171×2: discovery + mastery) -- 75 phrases → 450 points (75×6) -- 6 dialogs → 72 points (6×12) -- 3 lessons → 45 points (3×15) -- **TOTAL: 909 points** - -#### **Progress Calculation** - -```javascript -percentage = (completedWeight / totalWeight) × 100 - -Example: -- Discovered 50 words = 50 points -- Mastered 20 words = 20 points -- Completed 3 phrases = 18 points (3×6) -- Completed 1 dialog = 12 points -- Total completed = 100 points -- Progress = (100 / 909) × 100 = 11% -``` - -#### **Breakdown Display** - -```javascript -{ - percentage: 11, - completedWeight: 100, - totalWeight: 909, - breakdown: { - 'vocabulary-discovery': { count: 50, weight: 50 }, - 'vocabulary-mastery': { count: 20, weight: 20 }, - 'phrase': { count: 3, weight: 18 }, - 'dialog': { count: 1, weight: 12 } - } -} -``` - -### 🎯 On-The-Fly Vocabulary Formation - -**OLD (Wrong):** Pre-load all 171 words for flashcards - -**NEW (Correct):** -1. **Analyze next exercise** (e.g., Dialog #3) -2. **Extract words used** in that specific dialog -3. **Check user's status** for those words -4. **Form targeted list** of only needed words -5. **Force discovery/mastery** for just those words - -**Example:** -```javascript -// Next exercise: Dialog "Academic Conference" -// Words in dialog: methodology, hypothesis, analysis, paradigm, framework - -// User status check: -// - methodology: never seen → Discovery needed -// - hypothesis: discovered, not mastered → Mastery needed -// - analysis: mastered → Skip -// - paradigm: never seen → Discovery needed -// - framework: discovered, not mastered → Mastery needed - -// Smart system creates: -// 1. Discovery module: [methodology, paradigm] (2 words) -// 2. Mastery module: [hypothesis, framework] (2 words) -// 3. Then allow dialog exercise -``` - -### 🔧 Implementation Components - -#### **Required Classes** - -1. **ProgressItemInterface** - Abstract base with strict validation -2. **StrictInterface** - Enforcement mechanism with visual errors -3. **ContentProgressAnalyzer** - Scans content, creates items, calculates total -4. **ProgressTracker** - Manages state, marks completion, saves progress -5. **ImplementationValidator** - Runtime validation at startup - -#### **Concrete Item Implementations** - -- VocabularyDiscoveryItem -- VocabularyMasteryItem -- PhraseItem -- DialogItem -- TextItem -- AudioItem -- ImageItem -- GrammarItem - -**Each MUST implement all 4 interface methods or app fails to start** - -### ✅ Validation Checklist - -**Before ANY exercise can run:** -- [ ] Prerequisites analyzed for next specific content -- [ ] Missing words identified -- [ ] Discovery forced for never-seen words -- [ ] Mastery forced for seen-but-not-mastered words -- [ ] Progress item created with correct weight -- [ ] Completion properly tracked and saved -- [ ] Total progress recalculated - -**If ANY step fails → Clear error message, app stops gracefully** - -### 🎨 UI Integration - -**Progress Display:** -``` -Chapter Progress: 11% (100/909 points) - -✅ Vocabulary Discovery: 50/171 words (50pts) -✅ Vocabulary Mastery: 20/171 words (20pts) -✅ Phrases: 3/75 (18pts) -✅ Dialogs: 1/6 (12pts) -⬜ Texts: 0/3 (0/45pts) -``` - -**Smart Guide Updates:** -``` -🔍 Analyzing next exercise: Dialog "Academic Conference" -📚 4 words needed (2 discovery, 2 mastery) -🎯 Starting Vocabulary Discovery for: methodology, paradigm -``` - -### 🚨 Error Prevention - -**Compile-Time (Startup):** -- Interface validation -- Method implementation checks -- Weight configuration validation - -**Runtime:** -- Prerequisite enforcement -- State consistency checks -- Progress calculation validation - -**Visual Feedback:** -- Red screen for missing implementations -- Clear prerequisite errors -- Progress breakdown always visible - ---- - -**Status**: 📝 **DOCUMENTED - READY FOR IMPLEMENTATION** - ---- - -## 🔒 STRICT INTERFACE SYSTEM - C++ Style Contracts - -### 🎯 Philosophy: Contract-Driven Architecture - -**Like C++ header files (.h)**, we enforce strict interfaces that MUST be implemented. Any missing method = **RED SCREEN ERROR** at startup. - -### 📦 Interface Hierarchy - -``` -StrictInterface (base) -├── ProgressItemInterface # For progress tracking items -│ ├── VocabularyDiscoveryItem -│ ├── VocabularyMasteryItem -│ └── ContentProgressItems (Phrase, Dialog, Text, etc.) -│ -├── ProgressSystemInterface # For progress systems -│ ├── ProgressTracker -│ └── PrerequisiteEngine -│ -└── DRSExerciseInterface # For exercise modules - ├── VocabularyModule - ├── TextAnalysisModule - ├── GrammarAnalysisModule - ├── TranslationModule - └── OpenResponseModule -``` - -### 🏗️ 1. **StrictInterface** (Base Class) - -**Location**: `src/DRS/interfaces/StrictInterface.js` - -**Purpose**: Ultra-strict base class with visual error enforcement - -**Features**: -- ✅ Validates implementation at construction -- ✅ Full-screen red error overlay if method missing -- ✅ Sound alert in dev mode -- ✅ Screen shake animation -- ✅ Impossible to ignore - forces correct implementation - -**Error Display**: -``` -┌─────────────────────────────────────────────┐ -│ │ -│ 🔥 FATAL ERROR 🔥 │ -│ │ -│ Implementation Missing │ -│ │ -│ Class: VocabularyModule │ -│ Missing Method: validate() │ -│ │ -│ ❌ MUST implement all interface methods │ -│ │ -│ [ DISMISS (Fix Required!) ] │ -│ │ -└─────────────────────────────────────────────┘ -``` - -### 🎯 2. **ProgressItemInterface** - -**Location**: `src/DRS/interfaces/ProgressItemInterface.js` - -**Purpose**: Contract for all progress tracking items - -**Required Methods** (4): -```javascript -validate() // Validate item data -serialize() // Convert to JSON -getWeight() // Return item weight for progress calculation -canComplete(state) // Check prerequisites -``` - -**Implementations**: -- VocabularyDiscoveryItem (1pt) -- VocabularyMasteryItem (1pt) -- PhraseItem (6pts) -- DialogItem (12pts) -- TextItem (15pts) -- AudioItem (12pts) -- ImageItem (6pts) -- GrammarItem (6pts) - -### 🔧 3. **ProgressSystemInterface** - -**Location**: `src/DRS/interfaces/ProgressSystemInterface.js` - -**Purpose**: Contract for all progress management systems - -**Required Methods** (17): - -**Vocabulary Tracking:** -- `markWordDiscovered(word, metadata)` -- `markWordMastered(word, metadata)` -- `isWordDiscovered(word)` -- `isWordMastered(word)` - -**Content Tracking:** -- `markPhraseCompleted(id, metadata)` -- `markDialogCompleted(id, metadata)` -- `markTextCompleted(id, metadata)` -- `markAudioCompleted(id, metadata)` -- `markImageCompleted(id, metadata)` -- `markGrammarCompleted(id, metadata)` - -**Prerequisites:** -- `canComplete(itemType, itemId, context)` - -**Progress:** -- `getProgress(chapterId)` - -**Persistence:** -- `saveProgress(bookId, chapterId)` -- `loadProgress(bookId, chapterId)` - -**Utility:** -- `reset(bookId, chapterId)` - -**Implementations**: -- ProgressTracker - Weight-based progress with items -- PrerequisiteEngine - Prerequisite checking and mastery tracking - -### 🎮 4. **DRSExerciseInterface** - -**Location**: `src/DRS/interfaces/DRSExerciseInterface.js` - -**Purpose**: Contract for all DRS exercise modules - -**Required Methods** (10): - -**Lifecycle:** -- `init(config, content)` - Initialize exercise -- `render(container)` - Render UI -- `destroy()` - Clean up - -**Exercise Logic:** -- `validate(userAnswer)` - Validate answer, return { isCorrect, score, feedback, explanation } -- `getResults()` - Return { score, attempts, timeSpent, completed, details } -- `handleUserInput(event, data)` - Handle user input - -**Progress Tracking:** -- `markCompleted(results)` - Mark as completed -- `getProgress()` - Return { percentage, currentStep, totalSteps, itemsCompleted, itemsTotal } - -**Metadata:** -- `getExerciseType()` - Return exercise type string -- `getExerciseConfig()` - Return { type, difficulty, estimatedTime, prerequisites, metadata } - -**Implementations**: -- VocabularyModule - Flashcard spaced repetition -- TextAnalysisModule - AI-powered text comprehension -- GrammarAnalysisModule - AI grammar correction -- TranslationModule - AI translation validation -- OpenResponseModule - Free-form AI evaluation - -### ✅ 5. **ImplementationValidator** - -**Location**: `src/DRS/services/ImplementationValidator.js` - -**Purpose**: Validate ALL implementations at application startup - -**Validation Phases**: - -```javascript -🔍 VALIDATING DRS IMPLEMENTATIONS... - -📦 PART 1: Validating Progress Items... - ✅ VocabularyDiscoveryItem - OK - ✅ VocabularyMasteryItem - OK - ✅ PhraseItem - OK - ✅ DialogItem - OK - ✅ TextItem - OK - ✅ AudioItem - OK - ✅ ImageItem - OK - ✅ GrammarItem - OK - -🔧 PART 2: Validating Progress Systems... - ✅ ProgressTracker - OK - ✅ PrerequisiteEngine - OK - -🎮 PART 3: Validating DRS Exercise Modules... - ✅ VocabularyModule - OK - ✅ TextAnalysisModule - OK - ✅ GrammarAnalysisModule - OK - ✅ TranslationModule - OK - ✅ OpenResponseModule - OK - -✅ ALL DRS IMPLEMENTATIONS VALID -``` - -**If ANY validation fails**: -- 🔴 Full-screen red error -- 🚫 Application REFUSES to start -- 📋 Clear error message with missing method name -- 🔊 Alert sound (dev mode) -- 📳 Screen shake - -### 🎯 Integration with Application.js - -**At Startup** (lines 55-62): -```javascript -// Validate all progress item implementations (STRICT MODE) -console.log('🔍 Validating progress item implementations...'); -const { default: ImplementationValidator } = await import('./DRS/services/ImplementationValidator.js'); -const isValid = await ImplementationValidator.validateAll(); - -if (!isValid) { - throw new Error('❌ Implementation validation failed - check console for details'); -} -``` - -### 📋 Creating New Implementations - -#### **New Progress Item**: -```javascript -import ProgressItemInterface from '../interfaces/ProgressItemInterface.js'; - -class MyCustomItem extends ProgressItemInterface { - constructor(id, metadata) { - super('my-custom-item', id, metadata); - } - - validate() { - // MUST implement - if (!this.metadata.required) { - throw new Error('Missing required data'); - } - return true; - } - - serialize() { - // MUST implement - return { - ...this._getBaseSerialization(), - customData: this.metadata.custom - }; - } - - getWeight() { - // MUST implement - return ProgressItemInterface.WEIGHTS['my-custom-item'] || 5; - } - - canComplete(userProgress) { - // MUST implement - return true; // Check prerequisites here - } -} -``` - -#### **New Progress System**: -```javascript -import ProgressSystemInterface from '../interfaces/ProgressSystemInterface.js'; - -class MyProgressSystem extends ProgressSystemInterface { - constructor() { - super('MyProgressSystem'); - } - - // MUST implement all 17 required methods - async markWordDiscovered(word, metadata = {}) { /* ... */ } - async markWordMastered(word, metadata = {}) { /* ... */ } - isWordDiscovered(word) { /* ... */ } - isWordMastered(word) { /* ... */ } - // ... 13 more methods -} -``` - -#### **New Exercise Module**: -```javascript -import DRSExerciseInterface from '../interfaces/DRSExerciseInterface.js'; - -class MyExerciseModule extends DRSExerciseInterface { - constructor() { - super('MyExerciseModule'); - } - - // MUST implement all 10 required methods - async init(config, content) { /* ... */ } - async render(container) { /* ... */ } - async destroy() { /* ... */ } - async validate(userAnswer) { /* ... */ } - getResults() { /* ... */ } - handleUserInput(event, data) { /* ... */ } - async markCompleted(results) { /* ... */ } - getProgress() { /* ... */ } - getExerciseType() { return 'my-exercise'; } - getExerciseConfig() { /* ... */ } -} -``` - -### 🚨 Enforcement Rules - -**NON-NEGOTIABLE**: -1. ❌ **Missing method** → RED SCREEN ERROR → App refuses to start -2. ❌ **Wrong signature** → Runtime error on call -3. ❌ **Wrong return format** → Runtime error on usage -4. ✅ **All methods implemented** → App starts normally - -**Validation happens**: -- ✅ At application startup (before any UI renders) -- ✅ On module registration -- ✅ At interface instantiation - -**Benefits**: -- 🛡️ **Impossible to forget implementation** - Visual error forces fix -- 📋 **Self-documenting** - Interface defines exact contract -- 🔒 **Type safety** - Like TypeScript interfaces but enforced at runtime -- 🧪 **Testable** - Can mock interfaces for unit tests -- 🔄 **Maintainable** - Adding new method = update interface + all implementations get errors - ---- - -**Status**: ✅ **INTERFACE SYSTEM IMPLEMENTED** - ---- - -**This is a high-quality, maintainable system built for educational software that will scale.** \ No newline at end of file +**High-quality, maintainable educational software that scales.** diff --git a/content/books/ledu.json b/content/books/ledu.json new file mode 100644 index 0000000..5961d1c --- /dev/null +++ b/content/books/ledu.json @@ -0,0 +1,206 @@ +{ + "id": "ledu", + "name": "乐读 (Lè dú) - Chinese Reading Course", + "description": "Comprehensive Chinese reading course designed for intermediate learners focusing on reading comprehension, vocabulary acquisition, and cultural understanding", + "difficulty": "intermediate", + "language": "zh-CN", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "Jiaotong University Chinese Program", + "target_level": "intermediate", + "total_estimated_hours": 120, + "prerequisites": ["basic-chinese", "hsk-3"], + "learning_objectives": [ + "Master intermediate Chinese vocabulary for daily life contexts", + "Develop reading comprehension skills with authentic Chinese texts", + "Understand Chinese character structures and radicals", + "Practice inferring meaning from context", + "Learn Chinese cultural concepts through reading" + ], + "content_tags": ["chinese", "reading", "vocabulary", "comprehension", "culture"], + "total_chapters": 12, + "available_chapters": [ + "ledu-chapter1", + "ledu-chapter2", + "ledu-chapter3", + "ledu-chapter4", + "ledu-chapter5", + "ledu-chapter6", + "ledu-chapter7", + "ledu-chapter8", + "ledu-chapter9", + "ledu-chapter10", + "ledu-chapter11", + "ledu-chapter12" + ], + "completion_criteria": { + "overall_progress": 85, + "chapters_completed": 12, + "vocabulary_mastery": 90, + "comprehension_score": 80 + } + }, + "chapters": [ + { + "id": "ledu-chapter1", + "chapter_number": "1", + "name": "民以食为天 (Food is Heaven for the People)", + "description": "Introduction to Chinese food culture and dietary vocabulary", + "estimated_hours": 10, + "difficulty": "intermediate", + "prerequisites": ["hsk-3"], + "learning_objectives": [ + "Master food-related vocabulary", + "Understand Chinese dining etiquette", + "Learn to infer character meanings from radicals", + "Practice reading authentic texts about Chinese cuisine" + ], + "vocabulary_count": 45, + "phrases_count": 20, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter2", + "chapter_number": "2", + "name": "Chapter 2", + "description": "Second chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter3", + "chapter_number": "3", + "name": "生命在于运动 (Life Lies in Movement)", + "description": "Comprehensive chapter on sports, fitness, and healthy lifestyle with focus on forming exercise habits", + "estimated_hours": 10, + "difficulty": "intermediate", + "prerequisites": ["ledu-chapter1", "ledu-chapter2"], + "learning_objectives": [ + "Master 30+ sports and fitness vocabulary terms", + "Understand strategies for building exercise habits", + "Learn about ping-pong history and Chinese sports culture", + "Practice reading comprehension with authentic texts", + "Develop skills in contextual vocabulary inference" + ], + "vocabulary_count": 35, + "phrases_count": 15, + "texts_count": 3, + "exercises_count": 20 + }, + { + "id": "ledu-chapter4", + "chapter_number": "4", + "name": "Chapter 4", + "description": "Fourth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 38, + "phrases_count": 17, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter5", + "chapter_number": "5", + "name": "Chapter 5", + "description": "Fifth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter6", + "chapter_number": "6", + "name": "Chapter 6", + "description": "Sixth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 42, + "phrases_count": 19, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter7", + "chapter_number": "7", + "name": "Chapter 7", + "description": "Seventh chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter8", + "chapter_number": "8", + "name": "Chapter 8", + "description": "Eighth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 38, + "phrases_count": 17, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter9", + "chapter_number": "9", + "name": "Chapter 9", + "description": "Ninth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter10", + "chapter_number": "10", + "name": "Chapter 10", + "description": "Tenth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 42, + "phrases_count": 19, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter11", + "chapter_number": "11", + "name": "Chapter 11", + "description": "Eleventh chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter12", + "chapter_number": "12", + "name": "Chapter 12", + "description": "Twelfth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 38, + "phrases_count": 17, + "texts_count": 3, + "exercises_count": 15 + } + ] +} diff --git a/content/chapters/ledu-chapter1.json b/content/chapters/ledu-chapter1.json new file mode 100644 index 0000000..bff380e --- /dev/null +++ b/content/chapters/ledu-chapter1.json @@ -0,0 +1,548 @@ +{ + "id": "ledu-chapter1", + "book_id": "ledu", + "name": "民以食为天 (Food is Heaven for the People)", + "description": "Introduction to Chinese food culture, dining etiquette, and dietary vocabulary. Explores the importance of food in Chinese culture and regional taste preferences.", + "difficulty": "intermediate", + "language": "zh-CN", + "chapter_number": "1", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "LEDU Textbook - Jiaotong University", + "target_level": "intermediate", + "estimated_hours": 10, + "prerequisites": ["hsk-3"], + "learning_objectives": [ + "Master 45+ food and dining vocabulary terms", + "Understand Chinese dining etiquette and table manners", + "Learn about regional taste differences in China", + "Practice character inference from radicals", + "Develop reading comprehension skills with authentic texts" + ], + "content_tags": ["food", "culture", "etiquette", "regional-cuisine", "chinese-culture"], + "completion_criteria": { + "vocabulary_mastery": 90, + "comprehension_score": 80, + "exercises_completed": 15 + } + }, + "vocabulary": { + "食": { + "pronunciation": "shí", + "type": "morpheme", + "user_language": "food, to eat", + "examples": ["饮食", "甜食", "肉食", "食堂"], + "notes": "Pictographic character representing food in a container. Usually used as morpheme." + }, + "重": { + "pronunciation": "zhòng", + "type": "adjective/verb", + "user_language": "heavy; important; to attach importance to", + "examples": ["体重", "严重", "重要", "敬重"] + }, + "嘴": { + "pronunciation": "zuǐ", + "type": "noun", + "user_language": "mouth", + "examples": ["一张嘴", "张开嘴", "烫嘴"] + }, + "脑": { + "pronunciation": "nǎo", + "type": "noun", + "user_language": "brain", + "examples": ["大脑", "脑子"] + }, + "重视": { + "pronunciation": "zhòngshì", + "type": "verb", + "user_language": "to attach importance to, to value", + "examples": ["重视健康", "重视学习", "对…很/不重视"] + }, + "营养": { + "pronunciation": "yíngyǎng", + "type": "noun", + "user_language": "nutrition", + "examples": ["有营养", "营养丰富"] + }, + "文化": { + "pronunciation": "wénhuà", + "type": "noun", + "user_language": "culture", + "examples": ["饮食文化", "传统文化"] + }, + "丰富": { + "pronunciation": "fēngfù", + "type": "adjective/verb", + "user_language": "rich, abundant; to enrich", + "examples": ["营养丰富", "丰富生活"] + }, + "味道": { + "pronunciation": "wèidao", + "type": "noun", + "user_language": "taste, flavor", + "examples": ["味道鲜美", "品尝味道"] + }, + "麻辣烫": { + "pronunciation": "málàtàng", + "type": "noun", + "user_language": "spicy hot pot" + }, + "麻婆豆腐": { + "pronunciation": "mápó dòufu", + "type": "noun", + "user_language": "Mapo tofu (tofu in spicy sauce)" + }, + "适合": { + "pronunciation": "shìhé", + "type": "verb", + "user_language": "to suit, to be suitable for", + "examples": ["这种颜色不适合我", "他很适合做这个工作"] + }, + "满": { + "pronunciation": "mǎn", + "type": "adjective", + "user_language": "full, complete", + "examples": ["满桌", "坐满", "满心欢喜"] + }, + "四川": { + "pronunciation": "Sichuān", + "type": "proper noun", + "user_language": "Sichuan (province of China)" + }, + "历史": { + "pronunciation": "lishǐ", + "type": "noun", + "user_language": "history" + }, + "规矩": { + "pronunciation": "guīju", + "type": "noun", + "user_language": "rule, norm, convention" + }, + "安排": { + "pronunciation": "ānpái", + "type": "verb", + "user_language": "to arrange, to organize" + }, + "敲": { + "pronunciation": "qiāo", + "type": "verb", + "user_language": "to knock, to tap" + }, + "竖": { + "pronunciation": "shù", + "type": "adjective", + "user_language": "vertical, upright" + }, + "礼貌": { + "pronunciation": "lǐmào", + "type": "adjective", + "user_language": "polite, courteous" + }, + "倒": { + "pronunciation": "dào", + "type": "verb", + "user_language": "to pour" + }, + "尊重": { + "pronunciation": "zūnzhòng", + "type": "verb", + "user_language": "to respect, to esteem" + }, + "敬酒": { + "pronunciation": "jìng jiǔ", + "type": "verb", + "user_language": "to propose a toast" + }, + "但": { + "pronunciation": "dàn", + "type": "conjunction", + "user_language": "but (written language)", + "examples": ["这家饭馆不大,但很有名", "这份工作比较辛苦,但我很喜欢"], + "notes": "Written Chinese, same as 但是" + }, + "时": { + "pronunciation": "shí", + "type": "noun", + "user_language": "time, moment (written language)", + "examples": ["他看书时喜欢听音乐", "工作时他很认真"], + "notes": "Written Chinese, means '……的时候'" + }, + "讲究": { + "pronunciation": "jiǎngju", + "type": "verb", + "user_language": "to be particular about, to pay attention to" + }, + "色": { + "pronunciation": "sè", + "type": "noun", + "user_language": "color" + }, + "香": { + "pronunciation": "xiāng", + "type": "adjective", + "user_language": "fragrant, aromatic" + }, + "形": { + "pronunciation": "xíng", + "type": "noun", + "user_language": "shape, form" + }, + "意": { + "pronunciation": "yì", + "type": "noun", + "user_language": "meaning, significance" + }, + "颜色": { + "pronunciation": "yánsè", + "type": "noun", + "user_language": "color" + }, + "漂亮": { + "pronunciation": "piàoliang", + "type": "adjective", + "user_language": "beautiful, pretty" + }, + "口感": { + "pronunciation": "kǒugǎn", + "type": "noun", + "user_language": "taste, mouthfeel" + }, + "样子": { + "pronunciation": "yàngzi", + "type": "noun", + "user_language": "appearance, look" + }, + "意义": { + "pronunciation": "yìyì", + "type": "noun", + "user_language": "meaning, significance" + }, + "以前": { + "pronunciation": "yǐqián", + "type": "noun", + "user_language": "before, previously" + }, + "甜": { + "pronunciation": "tián", + "type": "adjective", + "user_language": "sweet" + }, + "咸": { + "pronunciation": "xián", + "type": "adjective", + "user_language": "salty" + }, + "辣": { + "pronunciation": "là", + "type": "adjective", + "user_language": "spicy" + }, + "酸": { + "pronunciation": "suān", + "type": "adjective", + "user_language": "sour" + }, + "各地": { + "pronunciation": "gèdì", + "type": "noun", + "user_language": "various places, everywhere" + }, + "爱上": { + "pronunciation": "àishang", + "type": "verb", + "user_language": "to fall in love with" + }, + "辣椒": { + "pronunciation": "làjiāo", + "type": "noun", + "user_language": "chili pepper" + }, + "离不开": { + "pronunciation": "lí bu kāi", + "type": "verb", + "user_language": "can't do without" + }, + "麻": { + "pronunciation": "má", + "type": "adjective", + "user_language": "numbing (Sichuan pepper sensation)" + }, + "最爱": { + "pronunciation": "zuì'ài", + "type": "noun", + "user_language": "favorite" + }, + "火锅": { + "pronunciation": "huǒguō", + "type": "noun", + "user_language": "hot pot" + }, + "口味": { + "pronunciation": "kǒuwèi", + "type": "noun", + "user_language": "taste preference" + }, + "温暖": { + "pronunciation": "wēnnuǎn", + "type": "adjective", + "user_language": "warm" + }, + "快乐": { + "pronunciation": "kuàilè", + "type": "adjective", + "user_language": "happy" + }, + "食欲": { + "pronunciation": "shíyù", + "type": "noun", + "user_language": "appetite" + }, + "喜爱": { + "pronunciation": "xǐ'ài", + "type": "verb", + "user_language": "to like, to love" + }, + "古人": { + "pronunciation": "gǔrén", + "type": "noun", + "user_language": "ancient people, ancestors" + }, + "事情": { + "pronunciation": "shìqing", + "type": "noun", + "user_language": "matter, thing, affair" + }, + "年长": { + "pronunciation": "niánzhǎng", + "type": "adjective", + "user_language": "elderly, senior" + }, + "入座": { + "pronunciation": "rùzuò", + "type": "verb", + "user_language": "to take a seat" + }, + "主人": { + "pronunciation": "zhǔrén", + "type": "noun", + "user_language": "host" + }, + "动筷子": { + "pronunciation": "dòng kuàizi", + "type": "verb phrase", + "user_language": "to start eating (lit. move chopsticks)" + }, + "品尝": { + "pronunciation": "pǐncháng", + "type": "verb", + "user_language": "to taste" + }, + "夹菜": { + "pronunciation": "jiācài", + "type": "verb", + "user_language": "to pick up food with chopsticks" + }, + "盘子": { + "pronunciation": "pánzi", + "type": "noun", + "user_language": "plate" + }, + "热情": { + "pronunciation": "rèqíng", + "type": "noun", + "user_language": "enthusiasm, warmth" + }, + "碗": { + "pronunciation": "wǎn", + "type": "noun", + "user_language": "bowl" + }, + "插": { + "pronunciation": "chā", + "type": "verb", + "user_language": "to insert, to stick in" + }, + "米饭": { + "pronunciation": "mǐfàn", + "type": "noun", + "user_language": "rice" + }, + "同样": { + "pronunciation": "tóngyàng", + "type": "adverb", + "user_language": "likewise, similarly" + }, + "长辈": { + "pronunciation": "zhǎngbèi", + "type": "noun", + "user_language": "elder, senior" + }, + "烫手": { + "pronunciation": "tàng shǒu", + "type": "verb phrase", + "user_language": "to burn one's hand" + }, + "酒": { + "pronunciation": "jiǔ", + "type": "noun", + "user_language": "alcohol, wine" + }, + "友情": { + "pronunciation": "yǒuqíng", + "type": "noun", + "user_language": "friendship" + }, + "双手": { + "pronunciation": "shuāngshǒu", + "type": "noun", + "user_language": "both hands" + }, + "杯子": { + "pronunciation": "bēizi", + "type": "noun", + "user_language": "cup, glass" + }, + "对方": { + "pronunciation": "duìfāng", + "type": "noun", + "user_language": "the other party" + }, + "吃饱": { + "pronunciation": "chī bǎo", + "type": "verb", + "user_language": "to eat one's fill" + }, + "感谢": { + "pronunciation": "gǎnxiè", + "type": "verb", + "user_language": "to thank" + } + }, + "grammar": { + "因此-therefore": { + "title": "……,因此…… - therefore, consequently", + "pattern": "Cause/Reason + 因此 + Result/Consequence", + "explanation": "Used to express cause and effect relationship. More formal than 所以.", + "examples": [ + { + "chinese": "这家饭馆很有特色,因此生意特别火。", + "pronunciation": "Zhè jiā fànguǎn hěn yǒu tèsè, yīncǐ shēngyì tèbié huǒ.", + "translation": "This restaurant has specialties, therefore the business is very good." + }, + { + "chinese": "这个牌子的饮料口感很好,因此很受年轻人欢迎。", + "pronunciation": "Zhège páizi de yǐnliào kǒugǎn hěn hǎo, yīncǐ hěn shòu niánqīngrén huānyíng.", + "translation": "This brand of beverage has a good taste, therefore it's very popular among young people." + } + ] + }, + "但-written": { + "title": "但 (dàn) - but (written language)", + "explanation": "Used in written Chinese to mean '但是' (but). More formal and concise.", + "examples": [ + { + "chinese": "这家饭馆不大,但很有名。", + "pronunciation": "Zhè jiā fànguǎn bù dà, dàn hěn yǒumíng.", + "translation": "This restaurant is not big, but it's very famous." + } + ] + }, + "时-written": { + "title": "时 (shí) - when, at the time of (written language)", + "explanation": "Used in written Chinese to mean '……的时候' (when, at the time of).", + "examples": [ + { + "chinese": "他看书时喜欢听音乐。", + "pronunciation": "Tā kàn shū shí xǐhuan tīng yīnyuè.", + "translation": "When he reads, he likes listening to music." + } + ] + } + }, + "texts": [ + { + "id": "main-text", + "title": "中国人用\"嘴\"吃饭 (Chinese People Eat with Their 'Mouth')", + "type": "main", + "content": "有人说:\"外国人用'脑'吃饭,中国人用'嘴'吃饭。\"外国人比较重视营养,吃什么、怎么吃首先想的是要有营养;中国人讲究菜要色、香、味、形、意都好。中华饮食文化非常丰富,一道菜要颜色漂亮、味道香、口感好、样子好看,还要有美好的意义。\n\n色、香、味、形、意中,味是第一位的。以前中国有句话说:\"南甜北咸,东辣西酸。\"意思是各地的人们各有所爱。而现在有越来越多的人爱上了辣,可以说是无辣不欢。其实辣椒来到中国还不到400年,但现在很多年轻人已经离不开辣了。四川菜又麻又辣,是很多年轻人的最爱。川菜馆在很多地方都很受人们的欢迎,很多人都爱吃麻辣火锅、麻辣烫和麻婆豆腐。\n\n为什么辣更适合年轻人的口味呢?辣味儿能让人感到温暖和快乐,而且红红的辣椒看着就让人满心欢喜,很有食欲,因此更受年轻人喜爱。", + "wordCount": 334, + "questions": [ + { + "question": "中国菜的特点是什么?", + "type": "open", + "answer": "色、香、味、形、意都好" + }, + { + "question": "现代中国年轻人最喜欢什么口味?原因是什么?", + "type": "open", + "answer": "辣。因为辣味能让人感到温暖和快乐,红辣椒让人满心欢喜,很有食欲" + }, + { + "question": "中国人认为,菜有没有营养没关系。", + "type": "true_false", + "answer": "错" + }, + { + "question": "很多年轻人特别喜欢吃川菜。", + "type": "true_false", + "answer": "对" + }, + { + "question": "辣椒最早产自中国。", + "type": "true_false", + "answer": "错" + } + ] + }, + { + "id": "etiquette-text", + "title": "中国人吃饭的规矩 (Chinese Dining Etiquette)", + "type": "extensive", + "content": "中国古人说:\"民以食为天。\"对人们来说,吃饭是最重要的事情。中国有五千多年的历史,有丰富的饮食文化,吃饭时也就有了很多规矩,比如说:\n\n规矩一:很多人一起吃饭时,先请客人或者最年长的人入座,客人一般会等主人安排后再入座。\n\n规矩二:开始吃饭时,请客人或者最年长的人先动筷子;每上一道菜,都要请客人或者最年长的人先品尝。\n\n规矩三:有时主人会为客人夹菜,放在客人的盘子里,这是对客人的热情。\n\n规矩四:吃饭时不能用筷子敲碗、盘子;筷子也不可以竖着插在米饭里,这是不礼貌的。\n\n规矩五:\"食不言\",吃东西时不讲话;同样,看到别人嘴里有食物,也不要去跟人讲话。\n\n规矩六:年轻人为客人或者长辈倒茶、倒酒。\n\n规矩七:给人倒茶时,只倒七分满,还有三分是对客人的热情,而且也不会烫手;但倒酒时,一般要倒满,意思是满满的尊重和友情。\n\n规矩八:年轻人向年长的人敬酒时,一般双手拿杯子,杯子要拿得比对方低一点儿,这是对年长的人的尊重;看到别人正在夹菜,先不要敬酒。\n\n规矩九:慢慢地品尝每一道菜,如果菜只上了一半,客人就说吃饱了,这是不礼貌的;吃完饭以后,客人一般会向主人说几句感谢的话。", + "wordCount": 466, + "questions": [ + { + "question": "最适合做本文标题的是:", + "type": "multiple_choice", + "options": ["A中国人吃饭的规矩", "B中国人怎么请客", "C中国的饮食文化", "D中国人的礼貌"], + "correctAnswer": "A中国人吃饭的规矩" + }, + { + "question": "\"民以食为天\"的意思是:", + "type": "multiple_choice", + "options": ["A人们喜欢吃", "B人们觉得吃非常重要", "C人们每天要吃饭", "D为了吃,人们要每天工作"], + "correctAnswer": "B人们觉得吃非常重要" + } + ] + } + ], + "exercises": [ + { + "type": "character_inference", + "title": "运用汉字知识推断字义 (Infer meaning from character components)", + "description": "Use radical knowledge to infer the meaning of unfamiliar characters", + "questions": [ + { + "question": "菜刚做好,太烫了。", + "options": ["A非常饿", "B非常热", "C非常好吃"], + "correctAnswer": "B非常热", + "hint": "烫 has the fire radical 火" + }, + { + "question": "这种苹果比较酸,再看看别的吧。", + "options": ["A一种味道", "B一种颜色", "C一种吃的东西"], + "correctAnswer": "A一种味道" + }, + { + "question": "这个电视节目介绍了糍粑的做法。", + "options": ["A一种衣物", "B一种用的东西", "C一种吃的东西"], + "correctAnswer": "C一种吃的东西", + "hint": "糍粑 has the rice radical 米" + } + ] + } + ] +} diff --git a/content/chapters/ledu-chapter2.json b/content/chapters/ledu-chapter2.json new file mode 100644 index 0000000..4ebdbff --- /dev/null +++ b/content/chapters/ledu-chapter2.json @@ -0,0 +1,750 @@ +{ + "id": "ledu-chapter2", + "book_id": "ledu", + "name": "货比三家 (Compare Prices at Three Shops)", + "description": "Chapter on online shopping, consumer behavior, and China's Double 11 shopping festival. Explores shopping methods, consumer preferences, and the advantages and disadvantages of online shopping.", + "difficulty": "intermediate", + "language": "zh-CN", + "chapter_number": "2", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "LEDU Textbook - Jiaotong University", + "target_level": "intermediate", + "estimated_hours": 10, + "prerequisites": ["ledu-chapter1"], + "learning_objectives": [ + "Master 40+ shopping and consumer vocabulary terms", + "Understand Chinese online shopping culture and Double 11", + "Learn about consumer behavior and shopping preferences", + "Practice reading comprehension with authentic texts", + "Develop vocabulary inference skills using affixes" + ], + "content_tags": ["shopping", "consumer-culture", "online-shopping", "double-11", "chinese-culture"], + "completion_criteria": { + "vocabulary_mastery": 90, + "comprehension_score": 80, + "exercises_completed": 20 + } + }, + "vocabulary": { + "品": { + "pronunciation": "pǐn", + "type": "morpheme", + "user_language": "item, product, grade, quality", + "examples": ["商品", "产品", "上品", "品茶"], + "notes": "Associative character composed of three 口 (mouth). Generally used as morpheme, not alone." + }, + "物": { + "pronunciation": "wù", + "type": "morpheme", + "user_language": "thing, object, matter", + "examples": ["动物", "物品", "礼物", "博物馆"], + "notes": "Usually used as morpheme, not alone" + }, + "消费": { + "pronunciation": "xiāofèi", + "type": "verb", + "user_language": "to consume", + "examples": ["消费水平", "消费者"] + }, + "方式": { + "pronunciation": "fāngshì", + "type": "noun", + "user_language": "way, method, manner", + "examples": ["生活方式", "消费方式"] + }, + "打折": { + "pronunciation": "dǎ//zhé", + "type": "verb", + "user_language": "to sell at a discount", + "examples": ["打八折", "打折促销"], + "notes": "打八折 means 20% off (pay 80%)" + }, + "促销": { + "pronunciation": "cùxiāo", + "type": "verb", + "user_language": "to promote sales", + "examples": ["打折促销", "促销员"] + }, + "优惠": { + "pronunciation": "yōuhuì", + "type": "adjective", + "user_language": "preferential, discount", + "examples": ["优惠价格", "八折优惠"] + }, + "调查": { + "pronunciation": "diàochá", + "type": "noun/verb", + "user_language": "investigation; to investigate", + "examples": ["一项调查", "做调查"] + }, + "需要": { + "pronunciation": "xūyào", + "type": "verb/noun", + "user_language": "to need; need", + "examples": ["需要时间", "需要帮助", "工作的需要"] + }, + "实用": { + "pronunciation": "shíyòng", + "type": "adjective", + "user_language": "practical, functional" + }, + "折扣": { + "pronunciation": "zhékòu", + "type": "noun", + "user_language": "discount", + "examples": ["有折扣", "折扣价"] + }, + "吸引": { + "pronunciation": "xīyǐn", + "type": "verb", + "user_language": "to attract", + "examples": ["吸引人", "有吸引力", "吸引住"] + }, + "产品": { + "pronunciation": "chǎnpǐn", + "type": "noun", + "user_language": "product", + "examples": ["进口产品", "产品质量"] + }, + "省": { + "pronunciation": "shěng", + "type": "verb", + "user_language": "to save, to economize", + "examples": ["省钱", "省时间", "省心"] + }, + "满意": { + "pronunciation": "mǎnyì", + "type": "verb", + "user_language": "to be satisfied", + "examples": ["对…满意"] + }, + "光棍儿节": { + "pronunciation": "Guānggùnr Jié", + "type": "proper noun", + "user_language": "Singles' Day", + "notes": "November 11th, because of the four 1's representing single people" + }, + "降": { + "pronunciation": "jiàng", + "type": "verb", + "user_language": "to lower, to reduce" + }, + "涨": { + "pronunciation": "zhǎng", + "type": "verb", + "user_language": "to rise (of prices, water, wages)" + }, + "骗": { + "pronunciation": "piàn", + "type": "verb", + "user_language": "to cheat, to deceive" + }, + "经验": { + "pronunciation": "jīngyàn", + "type": "noun", + "user_language": "experience" + }, + "赚": { + "pronunciation": "zhuàn", + "type": "verb", + "user_language": "to make a profit, to earn" + }, + "服装": { + "pronunciation": "fúzhuāng", + "type": "noun", + "user_language": "clothing, costume" + }, + "满": { + "pronunciation": "mǎn", + "type": "verb", + "user_language": "to reach, to come up to (formal/business)", + "examples": ["每满299元"] + }, + "返": { + "pronunciation": "fǎn", + "type": "verb", + "user_language": "to return, to pay back (formal/business)" + }, + "优惠券": { + "pronunciation": "yōuhuìquàn", + "type": "noun", + "user_language": "coupon" + }, + "化妆品": { + "pronunciation": "huàzhuāngpǐn", + "type": "noun", + "user_language": "cosmetics, makeup" + }, + "电饭锅": { + "pronunciation": "diànfànguō", + "type": "noun", + "user_language": "electric rice cooker" + }, + "网购": { + "pronunciation": "wǎnggòu", + "type": "verb", + "user_language": "to shop online" + }, + "近年来": { + "pronunciation": "jìn nián lái", + "type": "phrase", + "user_language": "in recent years" + }, + "已经": { + "pronunciation": "yǐjīng", + "type": "adverb", + "user_language": "already" + }, + "成为": { + "pronunciation": "chéngwéi", + "type": "verb", + "user_language": "to become" + }, + "日常": { + "pronunciation": "rìcháng", + "type": "adjective", + "user_language": "daily, everyday" + }, + "谈到": { + "pronunciation": "tándào", + "type": "verb", + "user_language": "to mention, to talk about" + }, + "首先": { + "pronunciation": "shǒuxiān", + "type": "adverb", + "user_language": "first, firstly" + }, + "想到": { + "pronunciation": "xiǎngdào", + "type": "verb", + "user_language": "to think of" + }, + "双十一": { + "pronunciation": "shuāng shí yī", + "type": "proper noun", + "user_language": "Double 11 (November 11)" + }, + "单身": { + "pronunciation": "dānshēn", + "type": "adjective", + "user_language": "single (unmarried)" + }, + "后来": { + "pronunciation": "hòulái", + "type": "adverb", + "user_language": "later, afterwards" + }, + "说法": { + "pronunciation": "shuōfǎ", + "type": "noun", + "user_language": "way of saying, statement" + }, + "计划": { + "pronunciation": "jìhuà", + "type": "verb/noun", + "user_language": "to plan; plan" + }, + "举办": { + "pronunciation": "jǔbàn", + "type": "verb", + "user_language": "to hold, to organize (event)" + }, + "购物节": { + "pronunciation": "gòuwù jié", + "type": "noun", + "user_language": "shopping festival" + }, + "选择": { + "pronunciation": "xuǎnzé", + "type": "verb", + "user_language": "to choose, to select" + }, + "刚好": { + "pronunciation": "gānghǎo", + "type": "adverb", + "user_language": "just right, exactly" + }, + "购买": { + "pronunciation": "gòumǎi", + "type": "verb", + "user_language": "to purchase" + }, + "网友": { + "pronunciation": "wǎngyǒu", + "type": "noun", + "user_language": "netizen, internet user" + }, + "戏称": { + "pronunciation": "xìchēng", + "type": "verb", + "user_language": "to jokingly call" + }, + "每年": { + "pronunciation": "měinián", + "type": "noun", + "user_language": "every year" + }, + "电商网站": { + "pronunciation": "diànshāng wǎngzhàn", + "type": "noun", + "user_language": "e-commerce website" + }, + "价格": { + "pronunciation": "jiàgé", + "type": "noun", + "user_language": "price" + }, + "一般": { + "pronunciation": "yībān", + "type": "adverb", + "user_language": "generally, usually" + }, + "平时": { + "pronunciation": "píngshí", + "type": "noun", + "user_language": "usually, ordinarily" + }, + "物美价廉": { + "pronunciation": "wù měi jià lián", + "type": "idiom", + "user_language": "good quality and cheap price" + }, + "网站": { + "pronunciation": "wǎngzhàn", + "type": "noun", + "user_language": "website" + }, + "其中": { + "pronunciation": "qízhōng", + "type": "pronoun", + "user_language": "among them" + }, + "大部分": { + "pronunciation": "dà bùfen", + "type": "noun", + "user_language": "most, the majority" + }, + "认为": { + "pronunciation": "rènwéi", + "type": "verb", + "user_language": "to think, to believe" + }, + "买到": { + "pronunciation": "mǎidào", + "type": "verb", + "user_language": "to buy (successfully)" + }, + "便宜": { + "pronunciation": "piányi", + "type": "adjective", + "user_language": "cheap, inexpensive" + }, + "而且": { + "pronunciation": "érqiě", + "type": "conjunction", + "user_language": "and, moreover" + }, + "现在": { + "pronunciation": "xiànzài", + "type": "noun", + "user_language": "now, currently" + }, + "东西": { + "pronunciation": "dōngxi", + "type": "noun", + "user_language": "thing, stuff" + }, + "参加": { + "pronunciation": "cānjiā", + "type": "verb", + "user_language": "to participate" + }, + "觉得": { + "pronunciation": "juéde", + "type": "verb", + "user_language": "to feel, to think" + }, + "特别": { + "pronunciation": "tèbié", + "type": "adverb", + "user_language": "especially, particularly" + }, + "工作": { + "pronunciation": "gōngzuò", + "type": "noun/verb", + "user_language": "work, job; to work" + }, + "忙": { + "pronunciation": "máng", + "type": "adjective", + "user_language": "busy" + }, + "时间": { + "pronunciation": "shíjiān", + "type": "noun", + "user_language": "time" + }, + "感觉": { + "pronunciation": "gǎnjué", + "type": "verb", + "user_language": "to feel" + }, + "男性": { + "pronunciation": "nánxìng", + "type": "noun", + "user_language": "male" + }, + "女性": { + "pronunciation": "nǚxìng", + "type": "noun", + "user_language": "female" + }, + "吸引力": { + "pronunciation": "xīyǐnlì", + "type": "noun", + "user_language": "attraction, appeal" + }, + "日用百货": { + "pronunciation": "rìyòng bǎihuò", + "type": "noun", + "user_language": "daily necessities" + }, + "家电": { + "pronunciation": "jiādiàn", + "type": "noun", + "user_language": "household appliances" + }, + "数码产品": { + "pronunciation": "shùmǎ chǎnpǐn", + "type": "noun", + "user_language": "digital products" + }, + "有利有弊": { + "pronunciation": "yǒu lì yǒu bì", + "type": "idiom", + "user_language": "has advantages and disadvantages" + }, + "方便": { + "pronunciation": "fāngbiàn", + "type": "adjective", + "user_language": "convenient" + }, + "购物": { + "pronunciation": "gòuwù", + "type": "verb", + "user_language": "shopping" + }, + "乐趣": { + "pronunciation": "lèqù", + "type": "noun", + "user_language": "pleasure, joy" + }, + "但是": { + "pronunciation": "dànshì", + "type": "conjunction", + "user_language": "but, however" + }, + "有时": { + "pronunciation": "yǒushí", + "type": "adverb", + "user_language": "sometimes" + }, + "商品": { + "pronunciation": "shāngpǐn", + "type": "noun", + "user_language": "goods, merchandise" + }, + "本来": { + "pronunciation": "běnlái", + "type": "adverb", + "user_language": "originally" + }, + "为了": { + "pronunciation": "wèile", + "type": "preposition", + "user_language": "in order to, for" + }, + "却": { + "pronunciation": "què", + "type": "adverb", + "user_language": "however, but" + }, + "钱": { + "pronunciation": "qián", + "type": "noun", + "user_language": "money" + }, + "真正": { + "pronunciation": "zhēnzhēng", + "type": "adjective", + "user_language": "real, genuine" + }, + "降价": { + "pronunciation": "jiàngjià", + "type": "verb", + "user_language": "to reduce prices" + }, + "涨价": { + "pronunciation": "zhǎngjià", + "type": "verb", + "user_language": "to increase prices" + }, + "新手": { + "pronunciation": "xīnshǒu", + "type": "noun", + "user_language": "novice, beginner" + }, + "不用": { + "pronunciation": "bùyòng", + "type": "auxiliary verb", + "user_language": "no need to" + }, + "价廉物美": { + "pronunciation": "jià lián wù měi", + "type": "idiom", + "user_language": "cheap and good quality" + }, + "为什么": { + "pronunciation": "wèishénme", + "type": "pronoun", + "user_language": "why" + }, + "会": { + "pronunciation": "huì", + "type": "auxiliary verb", + "user_language": "will" + }, + "可能": { + "pronunciation": "kěnéng", + "type": "auxiliary verb", + "user_language": "possibly, maybe" + }, + "开网店": { + "pronunciation": "kāi wǎngdiàn", + "type": "verb phrase", + "user_language": "to run an online store" + }, + "网店": { + "pronunciation": "wǎngdiàn", + "type": "noun", + "user_language": "online store" + }, + "做生意": { + "pronunciation": "zuò shēngyi", + "type": "verb phrase", + "user_language": "to do business" + }, + "先": { + "pronunciation": "xiān", + "type": "adverb", + "user_language": "first" + }, + "如果": { + "pronunciation": "rúguǒ", + "type": "conjunction", + "user_language": "if" + }, + "对...来说": { + "pronunciation": "duì...lái shuō", + "type": "phrase", + "user_language": "for, as far as...is concerned" + }, + "店主": { + "pronunciation": "diànzhǔ", + "type": "noun", + "user_language": "shop owner" + }, + "期间": { + "pronunciation": "qījiān", + "type": "noun", + "user_language": "period, during" + }, + "主要": { + "pronunciation": "zhǔyào", + "type": "adverb", + "user_language": "mainly" + }, + "人气": { + "pronunciation": "rénqì", + "type": "noun", + "user_language": "popularity" + } + }, + "grammar": { + "既然": { + "title": "既然…… - since, now that", + "pattern": "既然 + Reason, Result", + "explanation": "Used to express that since a certain condition exists, a certain result follows logically.", + "examples": [ + { + "chinese": "既然你这周没有时间,那就下周再去吧。", + "pronunciation": "Jìrán nǐ zhè zhōu méiyǒu shíjiān, nà jiù xià zhōu zài qù ba.", + "translation": "Since you don't have time this week, let's go next week then." + }, + { + "chinese": "既然父母不同意你去国外工作,你就别去了。", + "pronunciation": "Jìrán fùmǔ bù tóngyì nǐ qù guówài gōngzuò, nǐ jiù bié qù le.", + "translation": "Since your parents don't agree with you working abroad, then don't go." + }, + { + "chinese": "既然是自己需要的东西,价格又便宜,为什么不买?", + "pronunciation": "Jìrán shì zìjǐ xūyào de dōngxi, jiàgé yòu piányi, wèishénme bù mǎi?", + "translation": "Since it's something you need and the price is cheap, why not buy it?" + } + ] + }, + "自-从-起": { + "title": "自/从……起 - from...onwards (written language)", + "explanation": "Written Chinese expression meaning '从……开始' (from...starting). More formal.", + "examples": [ + { + "chinese": "自今日起至1月1日,本店部分商品半价。", + "pronunciation": "Zì jīnrì qǐ zhì 1 yuè 1 rì, běn diàn bùfen shāngpǐn bàn jià.", + "translation": "From today until January 1st, some products in this shop are half price." + }, + { + "chinese": "从下周起,学校放假。", + "pronunciation": "Cóng xià zhōu qǐ, xuéxiào fàngjià.", + "translation": "Starting from next week, school is on vacation." + } + ] + }, + "为-written": { + "title": "为 (wèi) - to be (written language)", + "explanation": "Written Chinese verb meaning '是' (to be). More formal.", + "examples": [ + { + "chinese": "考试时间为两小时。", + "pronunciation": "Kǎoshì shíjiān wèi liǎng xiǎoshí.", + "translation": "The exam time is two hours." + }, + { + "chinese": "公司上班时间为上午9:00至下午5:00。", + "pronunciation": "Gōngsī shàngbān shíjiān wèi shàngwǔ 9:00 zhì xiàwǔ 5:00.", + "translation": "Company working hours are from 9:00 AM to 5:00 PM." + } + ] + } + }, + "texts": [ + { + "id": "main-text", + "title": "\"双十一\",买还是不买? (Double 11: To Buy or Not to Buy?)", + "type": "main", + "content": "近年来,网购已经成为人们的日常消费方式之一。谈到网购,人们首先会想到\"双十一\"。\"双十一\"是什么?这得从\"光棍儿节\"说起。11月11日,因为有4个\"1\",让人们想到了单身的人——\"光棍儿\",后来就有了\"光棍儿节\"的说法。2009年,一个网上商城计划举办一个网上购物节,他们选择在11月进行,因为那时刚好是人们购买冬装的时候。11月11日被网友戏称为\"光棍儿节\",购物节就选在了这一天,\"双十一\"购物节就是这么来的。后来,每年的11月11日0点起,各大电商网站都会打折促销,价格一般都比平时优惠很多。\n\n在物美价廉面前,买还是不买?一家网站做了一个\"双十一\"网购的调查。有75%的网友说会在\"双十一\"网购,其中大部分人认为\"能买到便宜而且现在需要的东西\";不准备参加的人中,有人觉得\"没有特别想买的东西\",有人\"工作忙,没有时间\",还有人\"感觉买的东西不实用\"。男性的热情不比女性低,网购的低折扣对他们也有很大的吸引力。\n\n从调查中可以知道,人们对网购服装、日用百货、家电及数码产品最感兴趣。\n\n网购有利有弊。网购方便、省钱,\"双十一\"给很多人带来了跟平时不一样的购物乐趣。但是,有时人们对买到的商品不太满意,还有人买了很多不需要的东西,本来是为了省钱去网购,却多花了钱。", + "wordCount": 536, + "questions": [ + { + "question": "文章第1段主要介绍了\"双十一\"的:", + "type": "multiple_choice", + "options": ["A传统", "B由来", "C促销活动", "D商家"], + "correctAnswer": "B由来" + }, + { + "question": "人们喜欢在\"双十一\"网购的主要原因是什么?", + "type": "multiple_choice", + "options": ["A网购比去实体店购物更方便", "B能买到便宜而且需要的东西", "C平时很忙,没有时间买东西", "D网购能给人们带来很多乐趣"], + "correctAnswer": "B能买到便宜而且需要的东西" + }, + { + "question": "人们不想参加\"双十一\"网购的原因中,下列哪一项文中没有提到?", + "type": "multiple_choice", + "options": ["A没有想买的东西", "B太忙,没有时间", "C觉得网上的商品质量不好", "D网购的东西可能不太实用"], + "correctAnswer": "C觉得网上的商品质量不好" + } + ] + }, + { + "id": "netizen-comments", + "title": "关于\"双十一\"的网友评价 (Netizen Comments About Double 11)", + "type": "extensive", + "content": "网友1:不是真正的降价,不少东西是涨价了,骗骗新手的。\n\n网友2:平时也促销,有折扣,\"双十一\"没有优惠很多,不用急着那一天买。\n\n网友3:省钱最重要,大家都想买到价廉物美的东西。\n\n网友4:既然是自己需要的东西,价格又便宜,为什么不买?\n\n网友5:很多人会买,便宜嘛。不过等不了多久,再看看买回来的东西,大多是用不上的。\n\n网友6:我开网店五年了,\"双十一\"大部分网店会降价,可能极少数网店涨了价,我觉得那些人不是真正做生意的。\n\n网友7:我的经验是\"双十一\"前先在网店看看想买的东西,如果\"双十一\"降价了就买。\n\n网友8:对我们中小店主来说,\"双十一\"期间,钱真赚不了多少,主要是赚人气。", + "wordCount": 271, + "questions": [ + { + "question": "给\"双十一\"好评的是哪几位网友?", + "type": "open", + "answer": "网友3、网友4" + }, + { + "question": "给\"双十一\"差评的是哪几位网友?", + "type": "open", + "answer": "网友1、网友2、网友5" + }, + { + "question": "哪些网友对\"双十一\"购物给出了建议?", + "type": "open", + "answer": "网友7:先看价格,如果降价了再买" + }, + { + "question": "网友中哪几位是卖家?他们对\"双十一\"是什么态度?", + "type": "open", + "answer": "网友6、网友8。网友6说大部分网店会降价;网友8说赚不了多少钱,主要是赚人气" + } + ] + } + ], + "exercises": [ + { + "type": "vocabulary_inference", + "title": "通过词缀猜测词义 (Infer meaning through affixes)", + "description": "Chinese has morphemes that function like affixes: 家 (expert), 者 (person), 手 (skilled person), 热 (craze)", + "questions": [ + { + "question": "这家网站有很多国外的买家。", + "hint": "家 = person who does something", + "answer": "buyer" + }, + { + "question": "这家书店举办了作者与读者见面会。", + "hint": "者 = person who does something", + "answer": "author and reader" + }, + { + "question": "在网上买东西,特别是新手,可能会遇到一些问题。", + "hint": "手 = person skilled at something", + "answer": "novice, beginner" + }, + { + "question": "网购热已经从城市到了乡村。", + "hint": "热 = social trend/craze", + "answer": "online shopping craze" + } + ] + }, + { + "type": "phrase_matching", + "title": "从课文中找出与下列说法意思相近的词语", + "questions": [ + { + "question": "东西比以前贵了(网友1)", + "answer": "涨价" + }, + { + "question": "没有经验的买家(网友1)", + "answer": "新手" + }, + { + "question": "用打折、送礼物的方式卖东西(网友2)", + "answer": "促销" + }, + { + "question": "东西好,而且价格便宜(网友3)", + "answer": "价廉物美" + }, + { + "question": "东西比以前便宜了(网友6)", + "answer": "降价" + }, + { + "question": "增加受关注、受欢迎的程度(网友8)", + "answer": "赚人气" + } + ] + } + ] +} diff --git a/content/chapters/ledu-chapter3.json b/content/chapters/ledu-chapter3.json new file mode 100644 index 0000000..5435276 --- /dev/null +++ b/content/chapters/ledu-chapter3.json @@ -0,0 +1,643 @@ +{ + "id": "ledu-chapter3", + "book_id": "ledu", + "name": "生命在于运动 (Life Lies in Movement)", + "description": "Comprehensive chapter on sports, fitness, healthy lifestyle, and the importance of making exercise a habit. Includes reading about ping-pong history and Chinese sports culture.", + "difficulty": "intermediate", + "language": "zh-CN", + "chapter_number": "3", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "LEDU Textbook - Jiaotong University", + "target_level": "intermediate", + "estimated_hours": 10, + "prerequisites": ["ledu-chapter1", "ledu-chapter2"], + "learning_objectives": [ + "Master 35+ sports and fitness vocabulary terms", + "Understand strategies for building exercise habits", + "Learn about ping-pong history and sports in China", + "Practice reading comprehension with authentic texts", + "Develop contextual vocabulary inference skills" + ], + "content_tags": ["sports", "fitness", "health", "habits", "chinese-culture", "ping-pong"], + "completion_criteria": { + "vocabulary_mastery": 90, + "comprehension_score": 80, + "exercises_completed": 18 + } + }, + "vocabulary": { + "习惯": { + "pronunciation": "xíguàn", + "type": "noun", + "user_language": "habit", + "examples": ["成为习惯", "生活习惯", "好习惯"], + "notes": "Can also be used as verb meaning 'to be accustomed to'" + }, + "健身": { + "pronunciation": "jiànshēn", + "type": "verb", + "user_language": "to work out, to do fitness", + "examples": ["健身房", "健身运动"], + "notes": "Very common word for working out or exercising" + }, + "压力": { + "pronunciation": "yālì", + "type": "noun", + "user_language": "pressure, stress", + "examples": ["生活压力", "工作压力", "压力大"] + }, + "锻炼": { + "pronunciation": "duànliàn", + "type": "verb", + "user_language": "to exercise, to work out", + "examples": ["锻炼身体", "锻炼能力"] + }, + "身材": { + "pronunciation": "shēncái", + "type": "noun", + "user_language": "figure, physique", + "examples": ["身材苗条", "中等身材"] + }, + "放松": { + "pronunciation": "fàngsōng", + "type": "verb", + "user_language": "to relax, to loosen up", + "examples": ["放松肌肉", "放松心情", "放松身心"] + }, + "坚持": { + "pronunciation": "jiānchí", + "type": "verb", + "user_language": "to persist, to persevere", + "examples": ["坚持运动", "坚持自己的意见", "坚持下来"] + }, + "适应": { + "pronunciation": "shìyìng", + "type": "verb", + "user_language": "to adapt, to adjust to", + "examples": ["适应生活", "适应环境"] + }, + "强度": { + "pronunciation": "qiángdù", + "type": "noun", + "user_language": "intensity", + "examples": ["运动强度", "工作强度"] + }, + "体": { + "pronunciation": "tǐ", + "type": "noun/morpheme", + "user_language": "body", + "examples": ["体重", "体育", "物体", "体验"], + "notes": "Usually used as morpheme, not alone" + }, + "力": { + "pronunciation": "lì", + "type": "noun/morpheme", + "user_language": "force, power", + "examples": ["体力", "风力", "听力", "视力"], + "notes": "Usually used as morpheme" + }, + "伦敦": { + "pronunciation": "Lúndūn", + "type": "proper noun", + "user_language": "London" + }, + "绳子": { + "pronunciation": "shéngzi", + "type": "noun", + "user_language": "rope" + }, + "软木塞": { + "pronunciation": "ruǎnmùsāi", + "type": "noun", + "user_language": "cork" + }, + "欧洲": { + "pronunciation": "Ōuzhōu", + "type": "proper noun", + "user_language": "Europe" + }, + "流行": { + "pronunciation": "liúxíng", + "type": "verb", + "user_language": "to be popular, to prevail" + }, + "世乒赛": { + "pronunciation": "Shìpīngsài", + "type": "proper noun", + "user_language": "World Table Tennis Championships" + }, + "举办": { + "pronunciation": "jǔbàn", + "type": "verb", + "user_language": "to hold, to organize (an event)" + }, + "娱乐": { + "pronunciation": "yúlè", + "type": "verb/noun", + "user_language": "to entertain; entertainment" + }, + "东亚": { + "pronunciation": "Dōng Yà", + "type": "proper noun", + "user_language": "East Asia" + }, + "接触": { + "pronunciation": "jiēchù", + "type": "verb", + "user_language": "to touch, to have contact with" + }, + "技巧": { + "pronunciation": "jìqiǎo", + "type": "noun", + "user_language": "technique, skill" + }, + "胜": { + "pronunciation": "shèng", + "type": "verb", + "user_language": "to win, to triumph" + }, + "冠军": { + "pronunciation": "guànjūn", + "type": "noun", + "user_language": "champion" + }, + "迎战": { + "pronunciation": "yíngzhàn", + "type": "verb", + "user_language": "to face (in a match)" + }, + "击败": { + "pronunciation": "jībài", + "type": "verb", + "user_language": "to defeat, to beat" + }, + "以弱胜强": { + "pronunciation": "yǐ ruò shèng qiáng", + "type": "idiom", + "user_language": "to defeat the strong with the weak" + }, + "瑜伽": { + "pronunciation": "yújiā", + "type": "noun", + "user_language": "yoga" + }, + "街舞": { + "pronunciation": "jiēwǔ", + "type": "noun", + "user_language": "street dance" + }, + "肚皮舞": { + "pronunciation": "dùpíwǔ", + "type": "noun", + "user_language": "belly dance" + }, + "拉丁舞": { + "pronunciation": "lādīngwǔ", + "type": "noun", + "user_language": "Latin dance" + }, + "芭蕾舞": { + "pronunciation": "bālěiwǔ", + "type": "noun", + "user_language": "ballet" + }, + "有氧操": { + "pronunciation": "yǒuyǎngcāo", + "type": "noun", + "user_language": "aerobics" + }, + "普拉提": { + "pronunciation": "pǔlātí", + "type": "noun", + "user_language": "Pilates" + }, + "便": { + "pronunciation": "biàn", + "type": "adverb", + "user_language": "then, therefore (written language)", + "examples": ["他很好学,不懂便问", "他们俩大学一毕业便结婚了"], + "notes": "Written Chinese, means '就'" + }, + "以": { + "pronunciation": "yǐ", + "type": "preposition", + "user_language": "with, to use (written language)", + "examples": ["孩子不应该以这种态度跟父母说话", "以什么样的方法教孩子"], + "notes": "Written Chinese, means '用'" + }, + "着迷": { + "pronunciation": "zháomí", + "type": "verb", + "user_language": "to be fascinated, to be captivated" + }, + "只要": { + "pronunciation": "zhǐyào", + "type": "conjunction", + "user_language": "as long as, so long as" + }, + "改变": { + "pronunciation": "gǎibiàn", + "type": "verb", + "user_language": "to change" + }, + "生活方式": { + "pronunciation": "shēnghuó fāngshì", + "type": "noun", + "user_language": "lifestyle" + }, + "成为": { + "pronunciation": "chéngwéi", + "type": "verb", + "user_language": "to become" + }, + "舒服": { + "pronunciation": "shūfu", + "type": "adjective", + "user_language": "comfortable" + }, + "秘诀": { + "pronunciation": "mìjué", + "type": "noun", + "user_language": "secret, key, tip" + }, + "定": { + "pronunciation": "dìng", + "type": "verb", + "user_language": "to fix, to set" + }, + "运动量": { + "pronunciation": "yùndòngliàng", + "type": "noun", + "user_language": "amount of exercise" + }, + "慢慢地": { + "pronunciation": "mànmàn de", + "type": "adverb", + "user_language": "slowly, gradually" + }, + "加大": { + "pronunciation": "jiādà", + "type": "verb", + "user_language": "to increase, to enlarge" + }, + "尝试": { + "pronunciation": "chángshì", + "type": "verb", + "user_language": "to try, to attempt" + }, + "项目": { + "pronunciation": "xiàngmù", + "type": "noun", + "user_language": "project, item, event" + }, + "部位": { + "pronunciation": "bùwèi", + "type": "noun", + "user_language": "part, location (of body)" + }, + "增加": { + "pronunciation": "zēngjiā", + "type": "verb", + "user_language": "to increase, to add" + }, + "偷懒": { + "pronunciation": "tōulǎn", + "type": "verb", + "user_language": "to be lazy, to slack off" + }, + "装备": { + "pronunciation": "zhuāngbèi", + "type": "noun", + "user_language": "equipment, gear" + }, + "穿戴": { + "pronunciation": "chuāndài", + "type": "verb", + "user_language": "to wear, to put on" + }, + "准备": { + "pronunciation": "zhǔnbèi", + "type": "verb", + "user_language": "to prepare, to get ready" + }, + "变成": { + "pronunciation": "biànchéng", + "type": "verb", + "user_language": "to become, to turn into" + }, + "当然": { + "pronunciation": "dāngrán", + "type": "adverb", + "user_language": "of course, naturally" + }, + "愿意": { + "pronunciation": "yuànyì", + "type": "auxiliary verb", + "user_language": "willing, to be willing" + }, + "重要": { + "pronunciation": "zhòngyào", + "type": "adjective", + "user_language": "important" + }, + "据说": { + "pronunciation": "jùshuō", + "type": "verb", + "user_language": "it is said, allegedly" + }, + "世纪": { + "pronunciation": "shìjì", + "type": "noun", + "user_language": "century" + }, + "天气": { + "pronunciation": "tiānqì", + "type": "noun", + "user_language": "weather" + }, + "网球": { + "pronunciation": "wǎngqiú", + "type": "noun", + "user_language": "tennis" + }, + "办法": { + "pronunciation": "bànfǎ", + "type": "noun", + "user_language": "method, way, solution" + }, + "餐桌": { + "pronunciation": "cānzhuō", + "type": "noun", + "user_language": "dining table" + }, + "诞生": { + "pronunciation": "dànshēng", + "type": "verb", + "user_language": "to be born, to come into being" + }, + "出现": { + "pronunciation": "chūxiàn", + "type": "verb", + "user_language": "to appear, to emerge" + }, + "不久": { + "pronunciation": "bùjiǔ", + "type": "noun", + "user_language": "soon, before long" + }, + "受欢迎": { + "pronunciation": "shòu huānyíng", + "type": "verb phrase", + "user_language": "to be popular, to be welcomed" + }, + "各国": { + "pronunciation": "gèguó", + "type": "noun", + "user_language": "various countries" + }, + "起源地": { + "pronunciation": "qǐyuándì", + "type": "noun", + "user_language": "place of origin" + }, + "成绩": { + "pronunciation": "chéngjī", + "type": "noun", + "user_language": "result, achievement, grade" + }, + "当作": { + "pronunciation": "dàngzuò", + "type": "verb", + "user_language": "to regard as, to treat as" + }, + "活动": { + "pronunciation": "huódòng", + "type": "noun", + "user_language": "activity" + }, + "来到": { + "pronunciation": "láidào", + "type": "verb", + "user_language": "to arrive, to come to" + }, + "场地": { + "pronunciation": "chǎngdì", + "type": "noun", + "user_language": "venue, space, field" + }, + "用具": { + "pronunciation": "yòngjù", + "type": "noun", + "user_language": "equipment, utensil" + }, + "简单": { + "pronunciation": "jiǎndān", + "type": "adjective", + "user_language": "simple, easy" + }, + "另外": { + "pronunciation": "lìngwài", + "type": "conjunction", + "user_language": "in addition, besides" + }, + "喜欢": { + "pronunciation": "xǐhuan", + "type": "verb", + "user_language": "to like" + }, + "身体": { + "pronunciation": "shēntǐ", + "type": "noun", + "user_language": "body" + }, + "取胜": { + "pronunciation": "qǔshèng", + "type": "verb", + "user_language": "to win, to triumph" + }, + "因此": { + "pronunciation": "yīncǐ", + "type": "conjunction", + "user_language": "therefore, thus" + }, + "发展": { + "pronunciation": "fāzhǎn", + "type": "verb", + "user_language": "to develop" + }, + "多年来": { + "pronunciation": "duō nián lái", + "type": "phrase", + "user_language": "for many years" + }, + "世界": { + "pronunciation": "shìjiè", + "type": "noun", + "user_language": "world" + }, + "比赛": { + "pronunciation": "bǐsài", + "type": "noun", + "user_language": "competition, match" + }, + "选手": { + "pronunciation": "xuǎnshǒu", + "type": "noun", + "user_language": "player, athlete" + }, + "叫": { + "pronunciation": "jiào", + "type": "verb", + "user_language": "to call, to be called" + }, + "从此": { + "pronunciation": "cóngcǐ", + "type": "adverb", + "user_language": "from then on, since then" + }, + "传到": { + "pronunciation": "chuándào", + "type": "verb", + "user_language": "to spread to, to reach" + }, + "目前": { + "pronunciation": "mùqián", + "type": "noun", + "user_language": "currently, at present" + }, + "水平": { + "pronunciation": "shuǐpíng", + "type": "noun", + "user_language": "level, standard" + }, + "国球": { + "pronunciation": "guóqiú", + "type": "noun", + "user_language": "national ball game" + } + }, + "grammar": { + "既-又-也": { + "title": "既……(,)又/也…… - both...and...", + "pattern": "既 + Adj/Verb + 又/也 + Adj/Verb", + "explanation": "Used to express that something has two qualities or characteristics at the same time", + "examples": [ + { + "chinese": "这个孩子既聪明又可爱。", + "pronunciation": "Zhège háizi jì cōngming yòu kě'ài.", + "translation": "This child is both intelligent and cute." + }, + { + "chinese": "他既会踢足球,也会打篮球。", + "pronunciation": "Tā jì huì tī zúqiú, yě huì dǎ lánqiú.", + "translation": "He can both play soccer and basketball." + }, + { + "chinese": "不同的运动既能锻炼身体的不同部位,也可以增加运动的乐趣。", + "pronunciation": "Bùtóng de yùndòng jì néng duànliàn shēntǐ de bùtóng bùwèi, yě kěyǐ zēngjiā yùndòng de lèqù.", + "translation": "Different sports can both train different parts of the body and increase the enjoyment of exercise." + } + ] + }, + "便-written": { + "title": "便 (biàn) - then, therefore (written language)", + "explanation": "Used in written Chinese to mean '就' (then, therefore). More formal than 就.", + "examples": [ + { + "chinese": "他很好学,不懂便问。", + "pronunciation": "Tā hěn hàoxué, bù dǒng biàn wèn.", + "translation": "He is studious; when he doesn't understand, he asks right away." + }, + { + "chinese": "他们俩大学一毕业便结婚了。", + "pronunciation": "Tāmen liǎ dàxué yī bìyè biàn jiéhūn le.", + "translation": "As soon as they graduated from university, they got married." + } + ] + }, + "以-written": { + "title": "以 (yǐ) - with, to use (written language)", + "explanation": "Used in written Chinese to mean '用' (to use). More formal writing style.", + "examples": [ + { + "chinese": "孩子不应该以这种态度跟父母说话。", + "pronunciation": "Háizi bù yīnggāi yǐ zhè zhǒng tàidu gēn fùmǔ shuōhuà.", + "translation": "A child should not speak to parents with this kind of attitude." + }, + { + "chinese": "父母要好好想一想,以什么样的方法教孩子更好。", + "pronunciation": "Fùmǔ yào hǎohao xiǎng yīxiǎng, yǐ shénme yàng de fāngfǎ jiāo háizi gèng hǎo.", + "translation": "Parents should think carefully about what method to use to teach their children better." + } + ] + } + }, + "texts": [ + { + "id": "main-text", + "title": "让运动成为习惯 (Make Exercise Become a Habit)", + "type": "main", + "content": "让运动成为习惯?很多人觉得上班都那么忙、那么累了,哪里有时间和力气去运动健身?但有健身经验的人便会明白,压力越大越应该锻炼,因为它不仅可以让你拥有好身材,而且可以放松身心,带给人健康和快乐。\n\n其实,万事开头难,只要改变一下你的生活方式,把开头的那一段日子坚持下来,就能让你对运动着迷。每天的健身就会跟吃饭、睡觉一样,成为你生活中不可缺少的一部分,哪天不运动你就会觉得不舒服。那么,让运动成为习惯有哪些秘诀呢?\n\n第一,定好运动的时间,每天都是这个时间,不要改变。\n\n第二,开始时运动量不要太大,只锻炼10~15分钟就可以了,而且不要做强度太大的运动,要让你的身体慢慢地适应。然后可以慢慢地加大运动量和运动强度,不过最少在两周以后再这样做。\n\n第三,多尝试几种运动项目,不同的运动既能锻炼身体的不同部位,也可以增加运动的乐趣。\n\n第四,和朋友一起去健身,这样如果想要偷懒,会有人看着你。\n\n第五,给自己买些开心的运动装备,穿戴上它们会让自己的身心做好准备。\n\n第六,让运动变成一种快乐,如果运动让你感到快乐,你当然愿意去做。\n\n当然,最重要的一点——贵在坚持。", + "wordCount": 488, + "questions": [ + { + "question": "课文中提到了哪些让运动成为习惯的方法?", + "type": "open", + "answer": "六个方法加一个重点:1)定好时间 2)开始时运动量不要太大 3)多尝试几种运动 4)和朋友一起 5)买开心的运动装备 6)让运动变成快乐 重点:贵在坚持" + }, + { + "question": "句子中的\"它\"说的是:A压力 B锻炼", + "type": "multiple_choice", + "options": ["A压力", "B锻炼"], + "correctAnswer": "B锻炼" + }, + { + "question": "让运动成为习惯最重要的是什么?", + "type": "multiple_choice", + "options": ["A找到喜欢的运动", "B定好运动的时间", "C多尝试几种运动", "D要每天坚持运动"], + "correctAnswer": "D要每天坚持运动" + } + ] + }, + { + "id": "pingpong-text", + "title": "乒乓球的由来 (The Origin of Ping-Pong)", + "type": "extensive", + "content": "乒乓球英文叫作table tennis,它是怎么来的呢?据说,在19世纪末的一天,伦敦天气非常热,而且有雨。有两个年轻人不能去外边打网球,就想了一个办法,以饭馆的大餐桌作球台,中间用绳子作网,酒瓶的软木塞作球,用烟盒打球。女店主见到了,大声说\"Table Tennis!Table Tennis!\"就这样,乒乓球运动诞生了。\n\n乒乓球出现后不久,便成了一种很受欢迎的运动,在欧洲各国流行。1926年,第一届世乒赛在乒乓球的起源地——伦敦举办,但英国人的比赛成绩不太好,他们没有真正重视这项运动,只是把乒乓球当作娱乐活动。\n\n乒乓球运动在20世纪初来到东亚。东亚国家人多地少,乒乓球只需要很小的场地,用具也很简单,室内室外都可以打。另外,东亚人更喜欢没有身体接触的运动,讲究以技巧取胜。因此乒乓球在东亚很受欢迎,快速发展了起来。多年来,世界上各种乒乓球比赛的冠军大多是中、日、韩等东亚国家的选手。\n\n为什么叫\"乒乓球\"呢?20世纪初,一位美国乒乓球用具生产商以打乒乓球时发出的\"ping-pong\"声作为商标名。从此,ping-pong成了乒乓球的另一个英文名。传到中国后,中文里就有了\"乒乓球\"这个新词。中国人喜爱打乒乓球,是目前世界上乒乓球运动水平最高的国家之一,乒乓球也被中国人叫作\"国球\"。", + "wordCount": 483, + "questions": [ + { + "question": "说说乒乓球运动是怎么来的。", + "type": "open", + "answer": "19世纪末伦敦,两个年轻人因为下雨不能去外面打网球,用餐桌、绳子、软木塞和烟盒创造了这项运动" + }, + { + "question": "乒乓球运动最早起源于英国,为什么却在东亚地区快速发展起来?", + "type": "open", + "answer": "因为东亚人多地少,乒乓球场地小、用具简单;东亚人喜欢没有身体接触的运动,讲究技巧" + } + ] + } + ], + "exercises": [ + { + "type": "character_inference", + "title": "根据搭配的词语猜测词义", + "questions": [ + { + "question": "运动不仅可以强身健体,而且可以带来快乐。", + "options": ["A只有", "B不管", "C不但"], + "correctAnswer": "C不但" + }, + { + "question": "他不想干这么累的活儿。", + "options": ["A工作", "B运动", "C学习"], + "correctAnswer": "A工作" + } + ] + } + ] +} diff --git a/content/chapters/ledu-chapter4.json b/content/chapters/ledu-chapter4.json new file mode 100644 index 0000000..9832a06 --- /dev/null +++ b/content/chapters/ledu-chapter4.json @@ -0,0 +1,762 @@ +{ + "id": "ledu-chapter4", + "book_id": "ledu", + "name": "绿色生活 (Green Living)", + "description": "Chapter on environmental protection, green transportation, and sustainable lifestyle. Explores traffic problems in big cities, World Car Free Day, and daily environmental actions.", + "difficulty": "intermediate", + "language": "zh-CN", + "chapter_number": "4", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "LEDU Textbook - Jiaotong University", + "target_level": "intermediate", + "estimated_hours": 10, + "prerequisites": ["ledu-chapter1", "ledu-chapter2", "ledu-chapter3"], + "learning_objectives": [ + "Master 90+ environmental and transportation vocabulary", + "Understand green living concepts and practices", + "Learn about traffic management in Chinese cities", + "Practice reading comprehension with authentic texts", + "Develop abbreviation recognition skills" + ], + "content_tags": ["environment", "transportation", "green-living", "pollution", "sustainability", "chinese-culture"], + "completion_criteria": { + "vocabulary_mastery": 90, + "comprehension_score": 80, + "exercises_completed": 20 + } + }, + "vocabulary": { + "生": { + "pronunciation": "shēng", + "type": "morpheme", + "user_language": "to be born, life, to grow", + "examples": ["出生", "生长", "生活", "发生"], + "notes": "Self-explanatory character showing grass growing from earth" + }, + "出": { + "pronunciation": "chū", + "type": "verb/morpheme", + "user_language": "to go out, to come out", + "examples": ["出门", "出主意", "出问题", "出汗"] + }, + "交通": { + "pronunciation": "jiāotōng", + "type": "noun", + "user_language": "traffic, transportation", + "examples": ["交通便利", "交通安全", "交通部门"] + }, + "拥堵": { + "pronunciation": "yōngdǔ", + "type": "verb", + "user_language": "to be stuck in (a traffic jam)", + "examples": ["交通拥堵", "道路拥堵"] + }, + "解决": { + "pronunciation": "jiějué", + "type": "verb", + "user_language": "to solve, to settle", + "examples": ["解决问题"] + }, + "发展": { + "pronunciation": "fāzhǎn", + "type": "verb", + "user_language": "to develop", + "examples": ["发展公共交通", "发展经济", "发展速度"] + }, + "租": { + "pronunciation": "zū", + "type": "verb/noun", + "user_language": "to rent; rent", + "examples": ["租房", "租车", "房租", "租金"] + }, + "控制": { + "pronunciation": "kòngzhì", + "type": "verb", + "user_language": "to control, to dominate", + "examples": ["控制比赛", "控制数量"] + }, + "鼓励": { + "pronunciation": "gǔlì", + "type": "verb", + "user_language": "to encourage", + "examples": ["鼓励学生", "互相鼓励", "得到鼓励"] + }, + "污染": { + "pronunciation": "wūrǎn", + "type": "verb/noun", + "user_language": "to pollute; pollution", + "examples": ["污染空气", "污染环境", "受到污染"] + }, + "噪声": { + "pronunciation": "zàoshēng", + "type": "noun", + "user_language": "noise", + "examples": ["噪声污染"] + }, + "资源": { + "pronunciation": "zīyuán", + "type": "noun", + "user_language": "resource(s)", + "examples": ["自然资源", "人力资源", "资源丰富"] + }, + "提高": { + "pronunciation": "tí//gāo", + "type": "verb", + "user_language": "to improve, to increase, to enhance", + "examples": ["提高水平", "提高能力"] + }, + "意识": { + "pronunciation": "yìshí", + "type": "noun/verb", + "user_language": "awareness; to be conscious of, to realize", + "examples": ["安全意识", "环境保护意识"] + }, + "世界无车日": { + "pronunciation": "Shìjiè Wúchē Rì", + "type": "proper noun", + "user_language": "World Car Free Day" + }, + "朝阳区": { + "pronunciation": "Cháoyáng Qū", + "type": "proper noun", + "user_language": "Chaoyang District (of Beijing)" + }, + "法国": { + "pronunciation": "Fǎguó", + "type": "proper noun", + "user_language": "France" + }, + "抱怨": { + "pronunciation": "bàoyuàn", + "type": "verb", + "user_language": "to complain" + }, + "糟糕": { + "pronunciation": "zāogāo", + "type": "adjective", + "user_language": "terrible, too bad" + }, + "节约": { + "pronunciation": "jiéyuē", + "type": "verb", + "user_language": "to economize, to save" + }, + "告示": { + "pronunciation": "gàoshi", + "type": "noun", + "user_language": "bulletin, official notice" + }, + "法院": { + "pronunciation": "fǎyuàn", + "type": "noun", + "user_language": "court of justice" + }, + "通病": { + "pronunciation": "tōngbìng", + "type": "noun", + "user_language": "common problem, common defect" + }, + "便利": { + "pronunciation": "biànlì", + "type": "adjective", + "user_language": "convenient, easy" + }, + "出行": { + "pronunciation": "chūxíng", + "type": "verb", + "user_language": "to go out, to travel" + }, + "管理": { + "pronunciation": "guǎnlǐ", + "type": "verb/noun", + "user_language": "to manage; management", + "examples": ["交通管理部门"] + }, + "部门": { + "pronunciation": "bùmén", + "type": "noun", + "user_language": "department, section" + }, + "修建": { + "pronunciation": "xiūjiàn", + "type": "verb", + "user_language": "to build, to construct" + }, + "地铁": { + "pronunciation": "dìtiě", + "type": "noun", + "user_language": "subway, metro" + }, + "自行车": { + "pronunciation": "zìxíngchē", + "type": "noun", + "user_language": "bicycle" + }, + "数量": { + "pronunciation": "shùliàng", + "type": "noun", + "user_language": "quantity, amount" + }, + "市民": { + "pronunciation": "shìmín", + "type": "noun", + "user_language": "citizen, city resident" + }, + "步行": { + "pronunciation": "bùxíng", + "type": "verb", + "user_language": "to walk, on foot" + }, + "公共交通": { + "pronunciation": "gōnggòng jiāotōng", + "type": "noun", + "user_language": "public transportation" + }, + "绿色": { + "pronunciation": "lǜsè", + "type": "adjective", + "user_language": "green; environmentally friendly" + }, + "诞生": { + "pronunciation": "dànshēng", + "type": "verb", + "user_language": "to be born, to come into being" + }, + "欧洲": { + "pronunciation": "Ōuzhōu", + "type": "proper noun", + "user_language": "Europe" + }, + "汽车": { + "pronunciation": "qìchē", + "type": "noun", + "user_language": "car, automobile" + }, + "空气": { + "pronunciation": "kōngqì", + "type": "noun", + "user_language": "air" + }, + "严重": { + "pronunciation": "yánzhòng", + "type": "adjective", + "user_language": "serious, severe" + }, + "提出": { + "pronunciation": "tíchū", + "type": "verb", + "user_language": "to put forward, to propose" + }, + "支持": { + "pronunciation": "zhīchí", + "type": "verb/noun", + "user_language": "to support; support" + }, + "开展": { + "pronunciation": "kāizhǎn", + "type": "verb", + "user_language": "to launch, to carry out" + }, + "活动": { + "pronunciation": "huódòng", + "type": "noun", + "user_language": "activity" + }, + "世界性": { + "pronunciation": "shìjièxìng", + "type": "adjective", + "user_language": "worldwide, global" + }, + "建议": { + "pronunciation": "jiànyì", + "type": "verb/noun", + "user_language": "to suggest; suggestion" + }, + "选择": { + "pronunciation": "xuǎnzé", + "type": "verb", + "user_language": "to choose, to select" + }, + "公交": { + "pronunciation": "gōngjiāo", + "type": "noun", + "user_language": "public transportation (abbreviation)" + }, + "利用": { + "pronunciation": "lìyòng", + "type": "verb", + "user_language": "to use, to utilize" + }, + "道路": { + "pronunciation": "dàolù", + "type": "noun", + "user_language": "road, path" + }, + "减少": { + "pronunciation": "jiǎnshǎo", + "type": "verb", + "user_language": "to reduce, to decrease" + }, + "了解": { + "pronunciation": "liǎojiě", + "type": "verb", + "user_language": "to understand, to know" + }, + "过多": { + "pronunciation": "guòduō", + "type": "adjective", + "user_language": "too much, excessive" + }, + "城市": { + "pronunciation": "chéngshì", + "type": "noun", + "user_language": "city" + }, + "环境": { + "pronunciation": "huánjìng", + "type": "noun", + "user_language": "environment" + }, + "危害": { + "pronunciation": "wēihài", + "type": "noun/verb", + "user_language": "harm, danger; to harm" + }, + "环保": { + "pronunciation": "huánbǎo", + "type": "noun", + "user_language": "environmental protection (abbreviation)" + }, + "加重": { + "pronunciation": "jiāzhòng", + "type": "verb", + "user_language": "to worsen, to aggravate" + }, + "同时": { + "pronunciation": "tóngshí", + "type": "adverb/conjunction", + "user_language": "at the same time, meanwhile" + }, + "应": { + "pronunciation": "yīng", + "type": "auxiliary verb", + "user_language": "should, ought to (written language)" + }, + "保护": { + "pronunciation": "bǎohù", + "type": "verb", + "user_language": "to protect" + }, + "生活方式": { + "pronunciation": "shēnghuó fāngshì", + "type": "noun", + "user_language": "lifestyle, way of life" + }, + "息息相关": { + "pronunciation": "xīxī xiāngguān", + "type": "idiom", + "user_language": "closely related" + }, + "随手": { + "pronunciation": "suíshǒu", + "type": "adverb", + "user_language": "conveniently, in passing" + }, + "做到": { + "pronunciation": "zuòdào", + "type": "verb", + "user_language": "to achieve, to accomplish" + }, + "水龙头": { + "pronunciation": "shuǐlóngtóu", + "type": "noun", + "user_language": "faucet, tap" + }, + "一直": { + "pronunciation": "yīzhí", + "type": "adverb", + "user_language": "continuously, always" + }, + "洗手": { + "pronunciation": "xǐshǒu", + "type": "verb", + "user_language": "to wash hands" + }, + "洗澡": { + "pronunciation": "xǐzǎo", + "type": "verb", + "user_language": "to take a bath/shower" + }, + "洗衣服": { + "pronunciation": "xǐ yīfu", + "type": "verb phrase", + "user_language": "to wash clothes" + }, + "关灯": { + "pronunciation": "guān dēng", + "type": "verb phrase", + "user_language": "to turn off the light" + }, + "电费": { + "pronunciation": "diànfèi", + "type": "noun", + "user_language": "electricity bill" + }, + "度": { + "pronunciation": "dù", + "type": "measure word", + "user_language": "degree; kilowatt-hour (for electricity)" + }, + "有害": { + "pronunciation": "yǒuhài", + "type": "adjective", + "user_language": "harmful" + }, + "气体": { + "pronunciation": "qìtǐ", + "type": "noun", + "user_language": "gas" + }, + "擦地": { + "pronunciation": "cā dì", + "type": "verb phrase", + "user_language": "to mop the floor" + }, + "冲厕所": { + "pronunciation": "chōng cèsuǒ", + "type": "verb phrase", + "user_language": "to flush the toilet" + }, + "塑料袋": { + "pronunciation": "sùliàodài", + "type": "noun", + "user_language": "plastic bag" + }, + "自备": { + "pronunciation": "zìbèi", + "type": "verb", + "user_language": "to bring one's own" + }, + "袋子": { + "pronunciation": "dàizi", + "type": "noun", + "user_language": "bag" + }, + "食用": { + "pronunciation": "shíyòng", + "type": "verb", + "user_language": "to eat, to consume" + }, + "野生动物": { + "pronunciation": "yěshēng dòngwù", + "type": "noun", + "user_language": "wild animal" + }, + "穿": { + "pronunciation": "chuān", + "type": "verb", + "user_language": "to wear" + }, + "毛皮": { + "pronunciation": "máopí", + "type": "noun", + "user_language": "fur, pelt" + }, + "选购": { + "pronunciation": "xuǎngòu", + "type": "verb", + "user_language": "to select and purchase" + }, + "农药": { + "pronunciation": "nóngyào", + "type": "noun", + "user_language": "pesticide" + }, + "新鲜": { + "pronunciation": "xīnxiān", + "type": "adjective", + "user_language": "fresh" + }, + "果蔬": { + "pronunciation": "guǒshū", + "type": "noun", + "user_language": "fruits and vegetables (abbreviation)" + }, + "包装": { + "pronunciation": "bāozhuāng", + "type": "noun/verb", + "user_language": "packaging; to pack" + }, + "绿色食品": { + "pronunciation": "lǜsè shípǐn", + "type": "noun", + "user_language": "green food, organic food" + }, + "标识": { + "pronunciation": "biāoshí", + "type": "noun", + "user_language": "mark, sign, logo" + }, + "工具": { + "pronunciation": "gōngjù", + "type": "noun", + "user_language": "tool, instrument" + }, + "汽油": { + "pronunciation": "qìyóu", + "type": "noun", + "user_language": "gasoline, petrol" + }, + "尾气": { + "pronunciation": "wěiqì", + "type": "noun", + "user_language": "exhaust gas, emissions" + }, + "公共场所": { + "pronunciation": "gōnggòng chǎngsuǒ", + "type": "noun", + "user_language": "public place" + }, + "室内": { + "pronunciation": "shìnèi", + "type": "noun", + "user_language": "indoor, interior" + }, + "吸烟": { + "pronunciation": "xīyān", + "type": "verb", + "user_language": "to smoke" + }, + "做好": { + "pronunciation": "zuòhǎo", + "type": "verb", + "user_language": "to do well, to complete" + }, + "垃圾分类": { + "pronunciation": "lājī fēnlèi", + "type": "noun", + "user_language": "garbage sorting, waste classification" + }, + "种类": { + "pronunciation": "zhǒnglèi", + "type": "noun", + "user_language": "type, kind, category" + }, + "分开": { + "pronunciation": "fēnkāi", + "type": "verb", + "user_language": "to separate, to divide" + }, + "放": { + "pronunciation": "fàng", + "type": "verb", + "user_language": "to put, to place" + }, + "当作": { + "pronunciation": "dàngzuò", + "type": "verb", + "user_language": "to regard as, to treat as" + }, + "有用": { + "pronunciation": "yǒuyòng", + "type": "adjective", + "user_language": "useful" + }, + "混装": { + "pronunciation": "hùnzhuāng", + "type": "verb", + "user_language": "to mix and pack together" + }, + "土地": { + "pronunciation": "tǔdì", + "type": "noun", + "user_language": "land, soil" + }, + "照顾": { + "pronunciation": "zhàogu", + "type": "verb", + "user_language": "to take care of, to look after" + }, + "附近": { + "pronunciation": "fùjìn", + "type": "noun", + "user_language": "nearby, vicinity" + }, + "树": { + "pronunciation": "shù", + "type": "noun", + "user_language": "tree" + }, + "定期": { + "pronunciation": "dìngqī", + "type": "adverb", + "user_language": "regularly, periodically" + }, + "浇水": { + "pronunciation": "jiāoshuǐ", + "type": "verb", + "user_language": "to water (plants)" + }, + "家庭": { + "pronunciation": "jiātíng", + "type": "noun", + "user_language": "family, household" + }, + "一员": { + "pronunciation": "yī yuán", + "type": "noun", + "user_language": "a member" + } + }, + "grammar": { + "随着": { + "title": "随着…… - along with, as", + "pattern": "随着 + Noun/Phrase, Result", + "explanation": "Used to indicate that something changes as another thing changes", + "examples": [ + { + "chinese": "随着春天的到来,天气暖和起来了。", + "pronunciation": "Suízhe chūntiān de dàolái, tiānqì nuǎnhuo qǐlái le.", + "translation": "As spring arrives, the weather is getting warmer." + }, + { + "chinese": "随着年龄的增长,人的身体会发生很多变化。", + "pronunciation": "Suízhe niánlíng de zēngzhǎng, rén de shēntǐ huì fāshēng hěnduō biànhuà.", + "translation": "As age increases, people's bodies undergo many changes." + }, + { + "chinese": "随着城市的发展,环境污染问题也在加重。", + "pronunciation": "Suízhe chéngshì de fāzhǎn, huánjìng wūrǎn wèntí yě zài jiāzhòng.", + "translation": "As cities develop, environmental pollution problems are also worsening." + } + ] + }, + "已-written": { + "title": "已 (yǐ) - already (written language)", + "explanation": "Written Chinese adverb meaning '已经' (already). More formal.", + "examples": [ + { + "chinese": "比赛已结束。", + "pronunciation": "Bǐsài yǐ jiéshù.", + "translation": "The competition has already ended." + }, + { + "chinese": "这项工作已完成。", + "pronunciation": "Zhè xiàng gōngzuò yǐ wánchéng.", + "translation": "This work has already been completed." + } + ] + }, + "单-written": { + "title": "单 (dān) - only, merely (written language)", + "explanation": "Written Chinese adverb meaning '只' (only). More formal.", + "examples": [ + { + "chinese": "这部动画片不单孩子喜欢,大人也爱看。", + "pronunciation": "Zhè bù dònghuàpiàn bù dān háizi xǐhuan, dàrén yě ài kàn.", + "translation": "Not only do children like this cartoon, adults love watching it too." + }, + { + "chinese": "我每个月的花费很多,单交通费就要几百元。", + "pronunciation": "Wǒ měi gè yuè de huāfèi hěnduō, dān jiāotōngfèi jiù yào jǐ bǎi yuán.", + "translation": "My monthly expenses are high; transportation alone costs several hundred yuan." + } + ] + } + }, + "texts": [ + { + "id": "main-text", + "title": "世界无车日 (World Car Free Day)", + "type": "main", + "content": "有这样一个笑话:\"早上上班时间,你在北京朝阳区;坐一小时的车,你在朝阳区;再坐一小时,你还是在朝阳区。\"\n\n交通拥堵是很多大城市的通病。为解决交通拥堵问题、便利人们出行,交通管理部门想了很多办法,比如:修建地铁、发展自行车租车服务、控制汽车数量等。每年9月22日\"世界无车日\"前后,北京市交管部门都会鼓励市民不开车,用步行、自行车、公共交通等绿色方式出行。\n\n\"无车日\"最早诞生于1998年的法国。那时候欧洲的很多城市里,汽车带来的空气污染、噪声污染越来越严重。1998年9月22日,法国一些年轻人最先提出\"In Town, Without My Car!\",这个说法得到了人们的支持。后来世界上的很多城市都开展了\"无车日\"活动,\"无车日\"慢慢成了世界性的活动。\"世界无车日\"活动鼓励绿色出行,建议市民们更多地选择公交出行,一是为了更好地利用道路资源,减少交通拥堵,二是让人们了解汽车过多对城市环境的危害,提高人们的环保意识。", + "wordCount": 376, + "questions": [ + { + "question": "第1段的笑话说的是什么情况?", + "type": "open", + "answer": "交通拥堵,坐车很久还在同一个地方" + }, + { + "question": "根据第2段,为解决交通拥堵问题,交通管理部门想了哪些办法?", + "type": "open", + "answer": "修建地铁、发展自行车租车服务、控制汽车数量" + }, + { + "question": "根据第3段,\"世界无车日\"是怎样诞生的?", + "type": "open", + "answer": "1998年法国,因为汽车带来的污染严重,年轻人提出无车日,得到支持" + }, + { + "question": "根据课文,说说\"绿色出行\"是什么意思。", + "type": "open", + "answer": "用步行、自行车、公共交通等环保方式出行" + } + ] + }, + { + "id": "environmental-tips", + "title": "环保小贴士 (Environmental Tips)", + "type": "extensive", + "content": "随着城市的发展,环境污染问题也在加重。人们在抱怨城市空气越来越糟糕的同时,也应意识到环境的保护和每个人的生活方式息息相关。\n\n下面是生活中可以随手做到的几件环保小事。\n\n一、随手关水龙头,不要一直开着水龙头洗手、洗澡、洗衣服。\n\n二、随手关灯,这不只是为了节约电费,每节约一度电,空气中就会减少很多有害的气体。\n\n三、洗衣服后的水可以擦地或冲厕所等。\n\n四、买东西时不用塑料袋,出门购物带上自备的袋子。\n\n五、不食用野生动物,不穿野生动物毛皮做的衣服。\n\n六、选购不用农药的新鲜果蔬,买包装上有\"绿色食品\"标识的食品。\n\n七、多用公共交通工具,这样既可以节约汽油,又可以减少汽车尾气带来的空气污染。\n\n八、公共场所、室内工作场所、公共交通工具内不吸烟。\n\n九、做好垃圾分类,不同种类的垃圾分开放。分装垃圾是把垃圾当作有用的资源,混装的垃圾会污染土地和空气。\n\n十、照顾附近的一棵树,定期给它浇水,把它当作家庭里的一员。", + "wordCount": 404, + "questions": [ + { + "question": "从衣、食、住、行这四个方面说说身边的环保小事", + "type": "open", + "answer": "衣:不穿野生动物毛皮;食:买绿色食品;住:节约水电,垃圾分类;行:多用公共交通" + }, + { + "question": "关于身边的环保小事,文中没有提到的是:", + "type": "multiple_choice", + "options": ["A用温水煮饭", "B节约用水用电", "C生活垃圾分类", "D不在公共场所吸烟"], + "correctAnswer": "A用温水煮饭" + } + ] + } + ], + "exercises": [ + { + "type": "abbreviations", + "title": "缩略语练习 (Abbreviations)", + "description": "Practice recognizing and forming Chinese abbreviations", + "questions": [ + { + "question": "北京大学", + "answer": "北大" + }, + { + "question": "电子邮件", + "answer": "电邮" + }, + { + "question": "空中小姐", + "answer": "空姐" + }, + { + "question": "奥林匹克运动会", + "answer": "奥运会" + }, + { + "question": "交通管理部门", + "answer": "交管部门" + }, + { + "question": "城市居民", + "answer": "市民" + }, + { + "question": "公共交通", + "answer": "公交" + }, + { + "question": "环境保护", + "answer": "环保" + } + ] + } + ] +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..9b1e1d9 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,259 @@ +# Architecture Guide + +## 🏗️ Core Principles + +### 1. Single Responsibility +Each module has exactly one purpose. No mixing of concerns. + +### 2. Event-Driven Communication +All inter-module communication happens through EventBus. Zero direct dependencies. + +```javascript +// ❌ BAD - Direct access +const gameManager = window.app.modules.gameManager; +gameManager.startGame(); + +// ✅ GOOD - EventBus +eventBus.emit('game:start', { difficulty: 'medium' }); +``` + +### 3. Sealed Modules +Modules cannot be modified after creation using `Object.seal()`. + +```javascript +constructor() { + super('ModuleName'); + this._privateState = {}; + Object.seal(this); // Prevents adding/removing properties +} +``` + +### 4. WeakMap Private State +Internal data is completely inaccessible from outside. + +```javascript +const privateData = new WeakMap(); + +class SecureModule { + constructor() { + privateData.set(this, { + apiKey: 'secret', + internalState: {} + }); + } + + getPrivateData() { + return privateData.get(this); + } +} +``` + +### 5. Dependency Injection +No globals. Everything injected through constructor. + +```javascript +class GameModule extends Module { + constructor(name, dependencies, config) { + super(name, ['eventBus', 'renderer']); + + // Dependencies injected, not accessed globally + this._eventBus = dependencies.eventBus; + this._renderer = dependencies.renderer; + this._config = config; + } +} +``` + +## 🔄 Module Lifecycle + +``` +1. REGISTRATION → Application.js modules array +2. LOADING → ModuleLoader imports class +3. INSTANTIATION → new Module(name, deps, config) +4. INITIALIZATION → module.init() called +5. READY → Module emits 'ready' event +6. DESTRUCTION → module.destroy() on cleanup +``` + +## 📦 System Components + +### Core Layer (`src/core/`) + +**Module.js** - Abstract base class +- WeakMap private state +- Lifecycle management (init/destroy) +- State validation +- Abstract enforcement + +**EventBus.js** - Event communication +- Module registration required +- Event history tracking +- Cross-module isolation +- Memory leak prevention + +**ModuleLoader.js** - Dependency injection +- Topological sort for dependencies +- Circular dependency detection +- Proper initialization order +- Dynamic import system + +**Router.js** - Navigation system +- Route guards +- Middleware execution +- State management +- History integration + +**Application.js** - Bootstrap system +- Auto-initialization +- Module registration +- Lifecycle coordination +- Debug panel + +### DRS Layer (`src/DRS/`) + +**Exercise Modules** (`exercise-modules/`) +- VocabularyModule - Flashcard spaced repetition +- TextAnalysisModule - AI text comprehension +- GrammarAnalysisModule - AI grammar correction +- TranslationModule - AI translation validation +- OpenResponseModule - Free-form AI evaluation + +**Services** (`services/`) +- IAEngine - Multi-provider AI system +- LLMValidator - Answer validation +- ContentLoader - Content generation +- ProgressTracker - Progress management +- PrerequisiteEngine - Prerequisite checking + +**Interfaces** (`interfaces/`) +- StrictInterface - Base enforcement class +- ProgressItemInterface - Progress tracking contract +- ProgressSystemInterface - Progress system contract +- DRSExerciseInterface - Exercise module contract + +### Games Layer (`src/games/`) + +Independent game modules for entertainment (NOT part of DRS). +- FlashcardLearning.js - Standalone flashcard game +- Future games... + +## 🚫 Separation Rules + +### DRS vs Games - NEVER MIX + +**DRS** = Educational exercises with strict interfaces +**Games** = Entertainment with different architecture + +```javascript +// ❌ FORBIDDEN - DRS importing games +import FlashcardLearning from '../games/FlashcardLearning.js'; + +// ✅ CORRECT - DRS uses its own modules +import VocabularyModule from './exercise-modules/VocabularyModule.js'; +``` + +## 🔒 Security Layers + +1. **Object.seal()** - Prevents property addition/deletion +2. **Object.freeze()** - Prevents prototype modification +3. **WeakMap** - Internal state hidden +4. **Abstract enforcement** - Missing methods throw errors +5. **Validation at boundaries** - All inputs validated + +## 📊 Data Flow + +``` +User Action + ↓ +UI Component + ↓ +Event Emission (EventBus) + ↓ +Module Event Handler + ↓ +Business Logic + ↓ +State Update + ↓ +Event Emission (state changed) + ↓ +UI Update +``` + +## 🎯 Module Types + +### 1. Core Modules +System-level functionality. Never modify these. + +### 2. Game Modules +Entertainment-focused, extend Module base class. + +### 3. DRS Exercise Modules +Educational exercises, implement DRSExerciseInterface. + +### 4. Service Modules +Support functionality (AI, progress, content). + +### 5. UI Components +Reusable interface elements (future phase). + +## ⚡ Performance Targets + +- **<100ms** module loading time +- **<50ms** event propagation time +- **<200ms** application startup time +- **Zero** memory leaks in module lifecycle + +## 🧪 Testing Strategy + +1. **Unit Tests** - Individual module behavior +2. **Integration Tests** - Module communication via EventBus +3. **Interface Tests** - Contract compliance (ImplementationValidator) +4. **E2E Tests** - Complete user flows + +## 📋 Architecture Checklist + +For every new feature: +- [ ] Single responsibility maintained +- [ ] EventBus for all communication +- [ ] No direct module dependencies +- [ ] Proper dependency injection +- [ ] Object.seal() applied +- [ ] Abstract methods implemented +- [ ] Lifecycle methods complete +- [ ] Memory cleanup in destroy() +- [ ] Interface compliance validated +- [ ] No global variables used + +## 🔍 Debug Tools + +```javascript +// Application status +window.app.getStatus() + +// Module inspection +window.app.getCore().moduleLoader.getStatus() + +// Event history +window.app.getCore().eventBus.getEventHistory() + +// Navigate programmatically +window.app.getCore().router.navigate('/path') +``` + +## 🚨 Common Violations + +1. **Direct module access** → Use EventBus +2. **Global variables** → Use dependency injection +3. **Mixed responsibilities** → Split into separate modules +4. **No cleanup** → Implement destroy() properly +5. **Hardcoded dependencies** → Declare in constructor +6. **Missing validation** → Validate all inputs +7. **Modifying core** → Extend, don't modify + +## 📖 Further Reading + +- `docs/creating-new-module.md` - Module creation guide +- `docs/interfaces.md` - Interface system details +- `docs/progress-system.md` - Progress tracking guide +- `README.md` - Project overview diff --git a/docs/creating-new-module.md b/docs/creating-new-module.md new file mode 100644 index 0000000..e60f1f5 --- /dev/null +++ b/docs/creating-new-module.md @@ -0,0 +1,312 @@ +# Creating New Modules + +## 🎮 Game Module Template + +### Basic Structure + +```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: remove event listeners, DOM elements, timers + this._eventBus.off('game:start', this._handleStart, this.name); + + 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', + scoreToWin: 100 + } + } +] +``` + +## 📋 DRS Exercise Module Template + +### Using DRSExerciseInterface + +```javascript +import DRSExerciseInterface from '../DRS/interfaces/DRSExerciseInterface.js'; + +class MyExercise extends DRSExerciseInterface { + constructor() { + super('MyExercise'); + + // Internal state + this.score = 0; + this.attempts = 0; + this.startTime = null; + this.container = null; + } + + // ⚠️ REQUIRED - Initialize exercise + async init(config, content) { + this.config = config; + this.content = content; + this.startTime = Date.now(); + + // Validate content + if (!content || !content.question) { + throw new Error('MyExercise requires content with question'); + } + } + + // ⚠️ REQUIRED - Render UI + async render(container) { + this.container = container; + + container.innerHTML = ` +
+

${this.content.question}

+ + +
+ `; + + // Event listeners + container.querySelector('#submit-btn').addEventListener('click', () => { + const answer = container.querySelector('#answer-input').value; + this.handleUserInput('submit', { answer }); + }); + } + + // ⚠️ REQUIRED - Clean up + async destroy() { + if (this.container) { + this.container.innerHTML = ''; + } + } + + // ⚠️ REQUIRED - Validate answer + async validate(userAnswer) { + this.attempts++; + + const isCorrect = userAnswer.toLowerCase() === this.content.correctAnswer.toLowerCase(); + const score = isCorrect ? 100 - (this.attempts - 1) * 10 : 0; + + return { + isCorrect, + score: Math.max(score, 0), + feedback: isCorrect ? 'Correct!' : 'Try again', + explanation: `The correct answer is: ${this.content.correctAnswer}` + }; + } + + // ⚠️ REQUIRED - Get results + getResults() { + return { + score: this.score, + attempts: this.attempts, + timeSpent: Date.now() - this.startTime, + completed: this.score > 0, + details: { + question: this.content.question, + correctAnswer: this.content.correctAnswer + } + }; + } + + // ⚠️ REQUIRED - Handle user input + handleUserInput(event, data) { + if (event === 'submit') { + this.validate(data.answer).then(result => { + this.score = result.score; + this.displayFeedback(result); + }); + } + } + + // ⚠️ REQUIRED - Mark as completed + async markCompleted(results) { + // Save to progress system + await window.app.getCore().progressTracker.markExerciseCompleted( + 'my-exercise', + this.content.id, + results + ); + } + + // ⚠️ REQUIRED - Get progress + getProgress() { + return { + percentage: this.score > 0 ? 100 : 0, + currentStep: 1, + totalSteps: 1, + itemsCompleted: this.score > 0 ? 1 : 0, + itemsTotal: 1 + }; + } + + // ⚠️ REQUIRED - Get exercise type + getExerciseType() { + return 'my-exercise'; + } + + // ⚠️ REQUIRED - Get exercise config + getExerciseConfig() { + return { + type: 'my-exercise', + difficulty: this.config?.difficulty || 'medium', + estimatedTime: 120, // seconds + prerequisites: [], + metadata: { + hasAI: false, + requiresInternet: false + } + }; + } + + // Helper methods + displayFeedback(result) { + const feedbackDiv = this.container.querySelector('.feedback') || + document.createElement('div'); + feedbackDiv.className = 'feedback'; + feedbackDiv.textContent = result.feedback; + + if (!this.container.querySelector('.feedback')) { + this.container.appendChild(feedbackDiv); + } + } +} + +export default MyExercise; +``` + +## 📊 Progress Item Template + +### Using ProgressItemInterface + +```javascript +import ProgressItemInterface from '../DRS/interfaces/ProgressItemInterface.js'; + +class MyCustomItem extends ProgressItemInterface { + constructor(id, metadata) { + super('my-custom-item', id, metadata); + } + + // ⚠️ REQUIRED - Validate item data + validate() { + if (!this.metadata.requiredField) { + throw new Error('MyCustomItem requires requiredField in metadata'); + } + return true; + } + + // ⚠️ REQUIRED - Convert to JSON + serialize() { + return { + ...this._getBaseSerialization(), + customData: this.metadata.custom, + timestamp: Date.now() + }; + } + + // ⚠️ REQUIRED - Return item weight + getWeight() { + return ProgressItemInterface.WEIGHTS['my-custom-item'] || 5; + } + + // ⚠️ REQUIRED - Check prerequisites + canComplete(userProgress) { + // Check if user has completed prerequisites + const prerequisite = this.metadata.prerequisite; + if (prerequisite) { + return userProgress.hasCompleted(prerequisite); + } + return true; + } +} + +export default MyCustomItem; +``` + +## ✅ Checklist for New Modules + +### For Game Modules +- [ ] Extends `Module` base class +- [ ] Validates dependencies in constructor +- [ ] Uses `Object.seal(this)` at end of constructor +- [ ] Implements `init()` and calls `_setInitialized()` +- [ ] Implements `destroy()` and calls `_setDestroyed()` +- [ ] Uses EventBus for all communication +- [ ] No direct access to other modules +- [ ] Registered in `Application.js` modules array + +### For DRS Exercise Modules +- [ ] Extends `DRSExerciseInterface` +- [ ] Implements all 10 required methods +- [ ] Validates content in `init()` +- [ ] Cleans up in `destroy()` +- [ ] Returns correct format from `validate()` +- [ ] Integrates with progress system +- [ ] Tested with ImplementationValidator + +### For Progress Items +- [ ] Extends `ProgressItemInterface` +- [ ] Implements all 4 required methods +- [ ] Validates data correctly +- [ ] Returns proper weight +- [ ] Checks prerequisites properly +- [ ] Added to ImplementationValidator + +## 🚨 Common Mistakes to Avoid + +1. **Forgetting Object.seal()** - Module can be modified externally +2. **Not validating dependencies** - Module fails at runtime +3. **Direct module access** - Use EventBus instead +4. **Missing required methods** - Red screen error at startup +5. **Not cleaning up** - Memory leaks on destroy +6. **Hardcoded paths** - Use dynamic content loading +7. **Skipping ImplementationValidator** - Interface violations not caught + +## 📚 Examples in Codebase + +- **Game Module**: `src/games/FlashcardLearning.js` +- **DRS Exercise**: `src/DRS/exercise-modules/VocabularyModule.js` +- **Progress Item**: `src/DRS/services/ProgressItemInterface.js` +- **Validation**: `src/DRS/services/ImplementationValidator.js` diff --git a/docs/interfaces.md b/docs/interfaces.md new file mode 100644 index 0000000..cea1e48 --- /dev/null +++ b/docs/interfaces.md @@ -0,0 +1,314 @@ +# Interface System (C++ Style) + +## 🎯 Philosophy + +Like C++ header files (.h), we enforce **strict interfaces** that MUST be implemented. Any missing method = **RED SCREEN ERROR** at startup. + +## 📦 Interface Hierarchy + +``` +StrictInterface (base) +├── ProgressItemInterface # For progress tracking items +│ ├── VocabularyDiscoveryItem +│ ├── VocabularyMasteryItem +│ └── Content Items (Phrase, Dialog, Text, Audio, Image, Grammar) +│ +├── ProgressSystemInterface # For progress systems +│ ├── ProgressTracker +│ └── PrerequisiteEngine +│ +└── DRSExerciseInterface # For exercise modules + ├── VocabularyModule + ├── TextAnalysisModule + ├── GrammarAnalysisModule + ├── TranslationModule + └── OpenResponseModule +``` + +## 🔥 1. StrictInterface (Base) + +**Location**: `src/DRS/interfaces/StrictInterface.js` + +**Purpose**: Ultra-strict base class with visual error enforcement. + +**Features**: +- Validates implementation at construction +- Full-screen red error overlay if method missing +- Sound alert in dev mode +- Screen shake animation +- Impossible to ignore + +**Error Display**: +``` +┌─────────────────────────────────────────────┐ +│ │ +│ 🔥 FATAL ERROR 🔥 │ +│ │ +│ Implementation Missing │ +│ │ +│ Class: VocabularyModule │ +│ Missing Method: validate() │ +│ │ +│ ❌ MUST implement all interface methods │ +│ │ +│ [ DISMISS (Fix Required!) ] │ +│ │ +└─────────────────────────────────────────────┘ +``` + +## 📋 2. ProgressItemInterface + +**Location**: `src/DRS/interfaces/ProgressItemInterface.js` + +**Purpose**: Contract for all progress tracking items. + +### Required Methods (4) + +```javascript +validate() // Validate item data +serialize() // Convert to JSON +getWeight() // Return item weight for progress calculation +canComplete(state) // Check prerequisites +``` + +### Implementations + +| Class | Weight | Prerequisites | +|-------|--------|---------------| +| VocabularyDiscoveryItem | 1 | None | +| VocabularyMasteryItem | 1 | Discovered | +| PhraseItem | 6 | Vocabulary mastered | +| DialogItem | 12 | Vocabulary mastered | +| TextItem | 15 | Vocabulary mastered | +| AudioItem | 12 | Vocabulary mastered | +| ImageItem | 6 | Vocabulary discovered | +| GrammarItem | 6 | Vocabulary discovered | + +### Example Implementation + +```javascript +import ProgressItemInterface from '../interfaces/ProgressItemInterface.js'; + +class MyItem extends ProgressItemInterface { + constructor(id, metadata) { + super('my-item', id, metadata); + } + + // ⚠️ REQUIRED + validate() { + if (!this.metadata.requiredField) { + throw new Error('Missing requiredField'); + } + return true; + } + + // ⚠️ REQUIRED + serialize() { + return { + ...this._getBaseSerialization(), + customData: this.metadata.custom + }; + } + + // ⚠️ REQUIRED + getWeight() { + return ProgressItemInterface.WEIGHTS['my-item'] || 5; + } + + // ⚠️ REQUIRED + canComplete(userProgress) { + // Check prerequisites + return true; + } +} +``` + +## 🔧 3. ProgressSystemInterface + +**Location**: `src/DRS/interfaces/ProgressSystemInterface.js` + +**Purpose**: Contract for all progress management systems. + +### Required Methods (17) + +**Vocabulary Tracking:** +- `markWordDiscovered(word, metadata)` +- `markWordMastered(word, metadata)` +- `isWordDiscovered(word)` +- `isWordMastered(word)` + +**Content Tracking:** +- `markPhraseCompleted(id, metadata)` +- `markDialogCompleted(id, metadata)` +- `markTextCompleted(id, metadata)` +- `markAudioCompleted(id, metadata)` +- `markImageCompleted(id, metadata)` +- `markGrammarCompleted(id, metadata)` + +**Prerequisites:** +- `canComplete(itemType, itemId, context)` + +**Progress:** +- `getProgress(chapterId)` + +**Persistence:** +- `saveProgress(bookId, chapterId)` +- `loadProgress(bookId, chapterId)` + +**Utility:** +- `reset(bookId, chapterId)` + +### Implementations + +- **ProgressTracker** - Weight-based progress with items +- **PrerequisiteEngine** - Prerequisite checking and mastery tracking + +## 🎮 4. DRSExerciseInterface + +**Location**: `src/DRS/interfaces/DRSExerciseInterface.js` + +**Purpose**: Contract for all DRS exercise modules. + +### Required Methods (10) + +**Lifecycle:** +- `init(config, content)` - Initialize exercise +- `render(container)` - Render UI +- `destroy()` - Clean up + +**Exercise Logic:** +- `validate(userAnswer)` - Validate answer + - Returns: `{ isCorrect, score, feedback, explanation }` +- `getResults()` - Get results + - Returns: `{ score, attempts, timeSpent, completed, details }` +- `handleUserInput(event, data)` - Handle user input + +**Progress Tracking:** +- `markCompleted(results)` - Mark as completed +- `getProgress()` - Get progress + - Returns: `{ percentage, currentStep, totalSteps, itemsCompleted, itemsTotal }` + +**Metadata:** +- `getExerciseType()` - Return exercise type string +- `getExerciseConfig()` - Return config object + - Returns: `{ type, difficulty, estimatedTime, prerequisites, metadata }` + +### Implementations + +- **VocabularyModule** - Flashcard spaced repetition +- **TextAnalysisModule** - AI-powered text comprehension +- **GrammarAnalysisModule** - AI grammar correction +- **TranslationModule** - AI translation validation +- **OpenResponseModule** - Free-form AI evaluation + +## ✅ 5. ImplementationValidator + +**Location**: `src/DRS/services/ImplementationValidator.js` + +**Purpose**: Validate ALL implementations at application startup. + +### Validation Phases + +```javascript +🔍 VALIDATING DRS IMPLEMENTATIONS... + +📦 PART 1: Validating Progress Items... + ✅ VocabularyDiscoveryItem - OK + ✅ VocabularyMasteryItem - OK + ✅ PhraseItem - OK + ✅ DialogItem - OK + ✅ TextItem - OK + ✅ AudioItem - OK + ✅ ImageItem - OK + ✅ GrammarItem - OK + +🔧 PART 2: Validating Progress Systems... + ✅ ProgressTracker - OK + ✅ PrerequisiteEngine - OK + +🎮 PART 3: Validating DRS Exercise Modules... + ✅ VocabularyModule - OK + ✅ TextAnalysisModule - OK + ✅ GrammarAnalysisModule - OK + ✅ TranslationModule - OK + ✅ OpenResponseModule - OK + +✅ ALL DRS IMPLEMENTATIONS VALID +``` + +### Integration with Application.js + +```javascript +// At startup (lines 55-62) +console.log('🔍 Validating progress item implementations...'); +const { default: ImplementationValidator } = await import('./DRS/services/ImplementationValidator.js'); +const isValid = await ImplementationValidator.validateAll(); + +if (!isValid) { + throw new Error('❌ Implementation validation failed - check console for details'); +} +``` + +## 🚨 Enforcement Rules + +### NON-NEGOTIABLE + +1. ❌ **Missing method** → RED SCREEN ERROR → App refuses to start +2. ❌ **Wrong signature** → Runtime error on call +3. ❌ **Wrong return format** → Runtime error on usage +4. ✅ **All methods implemented** → App starts normally + +### Validation Happens + +- ✅ At application startup (before any UI renders) +- ✅ On module registration +- ✅ At interface instantiation + +## ✨ Benefits + +1. 🛡️ **Impossible to forget implementation** - Visual error forces fix +2. 📋 **Self-documenting** - Interface defines exact contract +3. 🔒 **Type safety** - Like TypeScript but enforced at runtime +4. 🧪 **Testable** - Can mock interfaces for unit tests +5. 🔄 **Maintainable** - Adding new method updates all implementations + +## 📋 Interface Compliance Checklist + +Before creating a new implementation: + +- [ ] Identified correct interface to extend +- [ ] Implemented ALL required methods +- [ ] Correct method signatures +- [ ] Correct return formats +- [ ] Validation logic in place +- [ ] Added to ImplementationValidator +- [ ] Tested with validation at startup +- [ ] Documentation updated + +## 🔍 Testing Your Implementation + +```javascript +// Manual test in console +const validator = await import('./DRS/services/ImplementationValidator.js'); +const result = await validator.default.validateAll(); +console.log(result); // true if valid, throws error otherwise +``` + +## 🚧 Adding New Interface Methods + +When adding a new method to an interface: + +1. Update the interface class +2. Update ALL implementations +3. Update ImplementationValidator +4. Update this documentation +5. Test with validation +6. Commit changes + +**Result**: All implementations will show RED SCREEN ERROR until updated. + +## 📖 Further Reading + +- `docs/creating-new-module.md` - How to create new modules +- `docs/progress-system.md` - Progress tracking details +- `README.md` - Project overview diff --git a/docs/progress-system.md b/docs/progress-system.md new file mode 100644 index 0000000..0eb5182 --- /dev/null +++ b/docs/progress-system.md @@ -0,0 +1,268 @@ +# Progress System + +## 🎯 Core Philosophy + +**FUNDAMENTAL RULE**: Every piece of content is a trackable progress item with strict validation and type safety. + +## 🏗️ Pedagogical Flow + +``` +1. DISCOVERY → 2. MASTERY → 3. APPLICATION + (passive) (active) (context) +``` + +### Flow Rules (NON-NEGOTIABLE) + +- ❌ **NO Flashcards on undiscovered words** - Must discover first +- ❌ **NO Text exercises on unmastered vocabulary** - Must master first +- ✅ **Always check prerequisites before ANY exercise** +- ✅ **Form vocabulary lists on-the-fly** from next exercise content + +## 📦 Progress Item Types & Weights + +| Type | Weight | Prerequisites | +|------|--------|---------------| +| vocabulary-discovery | 1 | None | +| vocabulary-mastery | 1 | Must be discovered | +| phrase | 6 | Vocabulary mastered | +| dialog | 12 | Vocabulary mastered | +| text | 15 | Vocabulary mastered | +| audio | 12 | Vocabulary mastered | +| image | 6 | Vocabulary discovered | +| grammar | 6 | Vocabulary discovered | + +**Total for 1 vocabulary word** = 2 points (1 discovery + 1 mastery) + +## 📈 Progress Calculation + +### Chapter Analysis + +When loading a chapter: + +1. **Scans ALL content** (vocabulary, phrases, dialogs, texts, etc.) +2. **Creates progress items** for each piece +3. **Calculates total weight** (sum of all item weights) +4. **Stores item registry** for tracking + +**Example Chapter:** +- 171 vocabulary words → 342 points (171×2: discovery + mastery) +- 75 phrases → 450 points (75×6) +- 6 dialogs → 72 points (6×12) +- 3 lessons → 45 points (3×15) +- **TOTAL: 909 points** + +### Progress Formula + +```javascript +percentage = (completedWeight / totalWeight) × 100 + +// Example: +// - Discovered 50 words = 50 points +// - Mastered 20 words = 20 points +// - Completed 3 phrases = 18 points (3×6) +// - Completed 1 dialog = 12 points +// Total completed = 100 points +// Progress = (100 / 909) × 100 = 11% +``` + +### Breakdown Display + +```javascript +{ + percentage: 11, + completedWeight: 100, + totalWeight: 909, + breakdown: { + 'vocabulary-discovery': { count: 50, weight: 50 }, + 'vocabulary-mastery': { count: 20, weight: 20 }, + 'phrase': { count: 3, weight: 18 }, + 'dialog': { count: 1, weight: 12 } + } +} +``` + +## 🎯 Smart Vocabulary Prerequisites + +### OLD Approach (Wrong) +Force all 171 words upfront based on arbitrary percentages. + +### NEW Approach (Correct) +Analyze next content → extract words → check user status → force only needed words. + +### Example Flow + +```javascript +// Next exercise: Dialog "Academic Conference" +// Words in dialog: methodology, hypothesis, analysis, paradigm, framework + +// User status check: +// - methodology: never seen → Discovery needed +// - hypothesis: discovered, not mastered → Mastery needed +// - analysis: mastered → Skip +// - paradigm: never seen → Discovery needed +// - framework: discovered, not mastered → Mastery needed + +// Smart system creates: +// 1. Discovery module: [methodology, paradigm] (2 words) +// 2. Mastery module: [hypothesis, framework] (2 words) +// 3. Then allow dialog exercise +``` + +### Benefits + +- **Targeted Learning** - Only learn words actually needed +- **Context-Driven** - Vocabulary tied to real content usage +- **Efficient Progress** - No time wasted on irrelevant words +- **Better Retention** - Words learned in context of upcoming usage +- **Smart Adaptation** - UI accurately reflects what's happening + +## 🔧 Key Components + +### 1. ProgressItemInterface +Abstract base with strict validation for all progress items. + +**Location**: `src/DRS/interfaces/ProgressItemInterface.js` + +**Methods**: +- `validate()` - Validate item data +- `serialize()` - Convert to JSON +- `getWeight()` - Return item weight +- `canComplete(state)` - Check prerequisites + +### 2. ProgressTracker +Manages state, marks completion, saves progress. + +**Location**: `src/DRS/services/ProgressTracker.js` + +**Key Methods**: +- `markWordDiscovered(word, metadata)` +- `markWordMastered(word, metadata)` +- `markContentCompleted(type, id, metadata)` +- `getProgress(chapterId)` +- `saveProgress(bookId, chapterId)` +- `loadProgress(bookId, chapterId)` + +### 3. PrerequisiteEngine +Checks prerequisites and enforces pedagogical flow. + +**Location**: `src/DRS/services/PrerequisiteEngine.js` + +**Key Methods**: +- `canComplete(itemType, itemId, context)` +- `getUnmetPrerequisites(itemType, itemId)` +- `enforcePrerequisites(exerciseConfig)` + +### 4. ContentDependencyAnalyzer +Analyzes content and extracts vocabulary dependencies. + +**Location**: `src/DRS/services/ContentDependencyAnalyzer.js` + +**Key Methods**: +- `analyzeContentDependencies(nextContent, vocabularyModule)` +- `extractWordsFromContent(content)` +- `findMissingWords(wordsInContent, vocabularyWords)` + +## 📊 UI Integration + +### Progress Display + +``` +Chapter Progress: 11% (100/909 points) + +✅ Vocabulary Discovery: 50/171 words (50pts) +✅ Vocabulary Mastery: 20/171 words (20pts) +✅ Phrases: 3/75 (18pts) +✅ Dialogs: 1/6 (12pts) +⬜ Texts: 0/3 (0/45pts) +``` + +### Smart Guide Updates + +``` +🔍 Analyzing next exercise: Dialog "Academic Conference" +📚 4 words needed (2 discovery, 2 mastery) +🎯 Starting Vocabulary Discovery for: methodology, paradigm +``` + +## ✅ Validation Checklist + +**Before ANY exercise can run:** + +- [ ] Prerequisites analyzed for next specific content +- [ ] Missing words identified +- [ ] Discovery forced for never-seen words +- [ ] Mastery forced for seen-but-not-mastered words +- [ ] Progress item created with correct weight +- [ ] Completion properly tracked and saved +- [ ] Total progress recalculated + +**If ANY step fails → Clear error message, app stops gracefully** + +## 🚨 Error Prevention + +### Compile-Time (Startup) +- Interface validation via ImplementationValidator +- Method implementation checks +- Weight configuration validation + +### Runtime +- Prerequisite enforcement before exercises +- State consistency checks +- Progress calculation validation + +### Visual Feedback +- Red screen for missing implementations +- Clear prerequisite errors +- Progress breakdown always visible + +## 🔍 Debug Commands + +```javascript +// Get current progress +window.app.getCore().progressTracker.getProgress('chapter-1') + +// Check if word discovered +window.app.getCore().progressTracker.isWordDiscovered('methodology') + +// Check if word mastered +window.app.getCore().progressTracker.isWordMastered('hypothesis') + +// Check prerequisites +window.app.getCore().prerequisiteEngine.canComplete('dialog', 'dialog-3') + +// Get unmet prerequisites +window.app.getCore().prerequisiteEngine.getUnmetPrerequisites('text', 'lesson-1') +``` + +## 📋 Adding New Progress Item Types + +1. Create new class extending `ProgressItemInterface` +2. Implement all 4 required methods +3. Add weight to `WEIGHTS` constant +4. Add to `ImplementationValidator` +5. Update `ProgressTracker` tracking methods +6. Update UI components +7. Test with validation + +## 🧪 Testing Progress System + +```javascript +// Test progress calculation +const tracker = window.app.getCore().progressTracker; + +// Mark some progress +await tracker.markWordDiscovered('test', {}); +await tracker.markWordMastered('test', {}); + +// Check progress +const progress = tracker.getProgress('chapter-1'); +console.log(progress); + +// Should show updated percentage and breakdown +``` + +## 📖 Further Reading + +- `docs/interfaces.md` - Interface system details +- `docs/creating-new-module.md` - Module creation guide +- `README.md` - Project overview diff --git a/src/DRS/exercise-modules/VocabularyModule.js b/src/DRS/exercise-modules/VocabularyModule.js index 59ac984..d51c941 100644 --- a/src/DRS/exercise-modules/VocabularyModule.js +++ b/src/DRS/exercise-modules/VocabularyModule.js @@ -528,9 +528,11 @@ class VocabularyModule extends DRSExerciseInterface { card.innerHTML = `
-

${currentWord.word}

+

+ ${currentWord.word} +

${this.config.showPronunciation && currentWord.pronunciation ? - `
[${currentWord.pronunciation}]
` : ''} + `
[${currentWord.pronunciation}]
` : ''}
${currentWord.type || 'word'}
@@ -545,11 +547,11 @@ class VocabularyModule extends DRSExerciseInterface {
`; @@ -567,6 +569,14 @@ class VocabularyModule extends DRSExerciseInterface { document.getElementById('reveal-btn').onclick = this._handleRevealAnswer; document.getElementById('submit-btn').onclick = this._handleUserInput; + // Add click listener on the word itself for TTS + const targetWord = document.getElementById('target-word-tts'); + if (targetWord) { + targetWord.onclick = () => { + this._handleTTS(); + this._highlightPronunciation(); + }; + } // Allow Enter key to submit const input = document.getElementById('translation-input'); @@ -625,9 +635,19 @@ class VocabularyModule extends DRSExerciseInterface { answerSection.style.display = 'none'; this.isRevealed = true; + // Add click listener on revealed answer for TTS + const answerTTS = document.getElementById('answer-tts'); + if (answerTTS) { + answerTTS.onclick = () => { + this._handleTTS(); + this._highlightPronunciation(); + }; + } + // Auto-play TTS when answer is revealed setTimeout(() => { this._handleTTS(); + this._highlightPronunciation(); }, 100); // Quick delay to let the answer appear // Don't mark as incorrect yet - wait for user self-assessment @@ -780,22 +800,25 @@ class VocabularyModule extends DRSExerciseInterface { const utterance = new SpeechSynthesisUtterance(text); - // Configure voice settings - utterance.lang = options.lang || 'en-US'; + // Get language from chapter data, fallback to options or en-US + const chapterLanguage = this.currentExerciseData?.language || 'en-US'; + utterance.lang = options.lang || chapterLanguage; utterance.rate = options.rate || 0.8; utterance.pitch = options.pitch || 1; utterance.volume = options.volume || 1; - // Try to find a suitable voice + // Try to find a suitable voice for the language const voices = window.speechSynthesis.getVoices(); if (voices.length > 0) { - // Prefer English voices - const englishVoice = voices.find(voice => - voice.lang.startsWith('en') && voice.default - ) || voices.find(voice => voice.lang.startsWith('en')); + // Find voice matching the chapter language + const langPrefix = chapterLanguage.split('-')[0]; // e.g., "zh" from "zh-CN" + const matchingVoice = voices.find(voice => + voice.lang.startsWith(langPrefix) && voice.default + ) || voices.find(voice => voice.lang.startsWith(langPrefix)); - if (englishVoice) { - utterance.voice = englishVoice; + if (matchingVoice) { + utterance.voice = matchingVoice; + console.log('🔊 Using voice:', matchingVoice.name, matchingVoice.lang); } } @@ -852,6 +875,22 @@ class VocabularyModule extends DRSExerciseInterface { } } + _highlightPronunciation() { + // Highlight pronunciation when TTS is played + const pronunciation = document.getElementById('pronunciation-display') || + document.getElementById('pronunciation-reveal'); + + if (pronunciation) { + // Add highlight class + pronunciation.classList.add('pronunciation-highlight'); + + // Remove highlight after animation + setTimeout(() => { + pronunciation.classList.remove('pronunciation-highlight'); + }, 2000); + } + } + _showGroupResults() { const resultsContainer = document.getElementById('group-results'); const card = document.getElementById('vocabulary-card'); @@ -1039,10 +1078,33 @@ class VocabularyModule extends DRSExerciseInterface { font-weight: bold; } + .target-word.clickable { + cursor: pointer; + transition: all 0.2s ease; + } + + .target-word.clickable:hover { + color: #667eea; + transform: scale(1.05); + } + .pronunciation { font-style: italic; color: #666; margin-bottom: 5px; + transition: all 0.3s ease; + } + + .pronunciation-highlight { + color: #667eea !important; + font-weight: bold; + font-size: 1.2em; + animation: pulse 0.5s ease-in-out; + } + + @keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } } .word-type { @@ -1089,9 +1151,22 @@ class VocabularyModule extends DRSExerciseInterface { margin-bottom: 5px; } + .correct-translation.clickable { + cursor: pointer; + transition: all 0.2s ease; + padding: 5px; + border-radius: 4px; + } + + .correct-translation.clickable:hover { + background-color: #d4edda; + transform: scale(1.02); + } + .pronunciation-text { font-style: italic; color: #666; + transition: all 0.3s ease; } .exercise-controls { diff --git a/src/gameHelpers/MarioEducational/PhysicsEngine.js b/src/gameHelpers/MarioEducational/PhysicsEngine.js new file mode 100644 index 0000000..94ac22e --- /dev/null +++ b/src/gameHelpers/MarioEducational/PhysicsEngine.js @@ -0,0 +1,343 @@ +/** + * PhysicsEngine.js + * Helper for physics simulation, collision detection, and movement + * Handles Mario physics, enemy physics, particles, and camera + */ + +export class PhysicsEngine { + /** + * Update Mario movement based on key inputs + * @param {Object} mario - Mario object + * @param {Object} keys - Key states object + * @param {Object} config - Game config with moveSpeed and jumpForce + * @param {boolean} isCelebrating - If true, disable movement + * @param {Function} playSound - Sound callback for jump + */ + static updateMarioMovement(mario, keys, config, isCelebrating, playSound) { + // Don't update movement during celebration + if (isCelebrating) return; + + // Horizontal movement + if (keys['ArrowLeft'] || keys['KeyA']) { + mario.velocityX = -config.moveSpeed; + mario.facing = 'left'; + } else if (keys['ArrowRight'] || keys['KeyD']) { + mario.velocityX = config.moveSpeed; + mario.facing = 'right'; + } else { + mario.velocityX *= 0.8; // Friction + } + + // Jumping + if ((keys['ArrowUp'] || keys['KeyW'] || keys['Space']) && mario.onGround) { + mario.velocityY = config.jumpForce; + mario.onGround = false; + if (playSound) playSound('jump'); + } + } + + /** + * Update Mario physics (gravity and position) + * @param {Object} mario - Mario object + * @param {Object} config - Game config with gravity + * @param {Object} level - Current level data + * @param {boolean} levelCompleted - If level is completed + * @param {Function} onFallOff - Callback when Mario falls off world + */ + static updateMarioPhysics(mario, config, level, levelCompleted, onFallOff) { + // Apply gravity + mario.velocityY += config.gravity; + + // Update position + mario.x += mario.velocityX; + mario.y += mario.velocityY; + + // Prevent going off left edge + if (mario.x < 0) { + mario.x = 0; + } + + // Stop Mario at finish line during celebration + if (mario.x > level.endX && levelCompleted) { + mario.x = level.endX; + mario.velocityX = 0; + } + + // Check if Mario fell off the world + if (mario.y > config.canvasHeight + 100) { + if (onFallOff) onFallOff(); + } + } + + /** + * Update enemy movement and AI + * @param {Array} enemies - Array of enemies + * @param {Array} walls - Array of walls + * @param {Array} platforms - Array of platforms + * @param {number} levelWidth - Level width + * @param {boolean} isCelebrating - If true, disable updates + */ + static updateEnemies(enemies, walls, platforms, levelWidth, isCelebrating) { + // Don't update enemies during celebration + if (isCelebrating) return; + + enemies.forEach(enemy => { + // Store old position for collision detection + const oldX = enemy.x; + enemy.x += enemy.velocityX; + + // Check wall collisions + const hitWall = walls.some(wall => { + return enemy.x < wall.x + wall.width && + enemy.x + enemy.width > wall.x && + enemy.y < wall.y + wall.height && + enemy.y + enemy.height > wall.y; + }); + + if (hitWall) { + // Reverse position and direction + enemy.x = oldX; + enemy.velocityX *= -1; + console.log(`🧱 Enemy hit wall, reversing direction`); + } + + // Simple AI: reverse direction at platform edges + const platform = platforms.find(p => + enemy.x >= p.x - 10 && enemy.x <= p.x + p.width + 10 && + enemy.y >= p.y - enemy.height - 5 && enemy.y <= p.y + 5 + ); + + if (!platform || enemy.x <= 0 || enemy.x >= levelWidth) { + enemy.velocityX *= -1; + } + }); + } + + /** + * Check all collisions (platforms, walls, enemies, etc.) + * @param {Object} mario - Mario object + * @param {Object} gameState - All game entities + * @param {Object} callbacks - Callbacks for various collision events + */ + static checkCollisions(mario, gameState, callbacks) { + const { + platforms, questionBlocks, enemies, walls, catapults, + piranhaPlants, boulders + } = gameState; + + const { + onQuestionBlock, onEnemyDefeat, onMarioDeath, onAddParticles + } = callbacks; + + // Platform collisions + mario.onGround = false; + + platforms.forEach(platform => { + if (this.isColliding(mario, platform)) { + // Check if Mario is landing on top + if (mario.velocityY > 0 && mario.y + mario.height - mario.velocityY <= platform.y + 5) { + mario.y = platform.y - mario.height; + mario.velocityY = 0; + mario.onGround = true; + } + // Hit from below + else if (mario.velocityY < 0 && mario.y - mario.velocityY >= platform.y + platform.height - 5) { + mario.y = platform.y + platform.height; + mario.velocityY = 0; + } + // Side collision + else { + if (mario.velocityX > 0) { + mario.x = platform.x - mario.width; + } else if (mario.velocityX < 0) { + mario.x = platform.x + platform.width; + } + mario.velocityX = 0; + } + } + }); + + // Boulder collisions (grounded boulders only) + boulders.forEach(boulder => { + if (boulder.hasLanded && this.isColliding(mario, boulder)) { + console.log(`🪨 Mario hit by grounded boulder - restarting level`); + if (onMarioDeath) onMarioDeath(); + } + }); + + // Question block collisions + questionBlocks.forEach(block => { + if (!block.hit && this.isColliding(mario, block)) { + // Check if Mario hit from below + if (mario.velocityY < 0 && mario.y < block.y + block.height) { + if (onQuestionBlock) onQuestionBlock(block); + } + // Solid collision (treat as platform) + else if (mario.velocityY > 0) { + mario.y = block.y - mario.height; + mario.velocityY = 0; + mario.onGround = true; + } + } + }); + + // Wall collisions + walls.forEach(wall => { + if (this.isColliding(mario, wall)) { + // Side collision + if (mario.velocityX > 0) { + mario.x = wall.x - mario.width; + } else if (mario.velocityX < 0) { + mario.x = wall.x + wall.width; + } + mario.velocityX = 0; + + // Top/bottom collision + if (mario.velocityY > 0) { + mario.y = wall.y - mario.height; + mario.velocityY = 0; + mario.onGround = true; + } else if (mario.velocityY < 0) { + mario.y = wall.y + wall.height; + mario.velocityY = 0; + } + } + }); + + // Catapult collisions (solid obstacles) + catapults.forEach(catapult => { + if (this.isColliding(mario, catapult)) { + // Treat catapults as solid platforms + if (mario.velocityY > 0) { + mario.y = catapult.y - mario.height; + mario.velocityY = 0; + mario.onGround = true; + } + } + }); + + // Enemy collisions + enemies.forEach((enemy, index) => { + if (this.isColliding(mario, enemy)) { + // Check if Mario jumped on enemy + if (mario.velocityY > 0 && mario.y < enemy.y + enemy.height / 2) { + // Enemy defeated + mario.velocityY = -8; // Bounce + if (onEnemyDefeat) onEnemyDefeat(index); + if (onAddParticles) onAddParticles(enemy.x, enemy.y, '#FFD700'); + } else { + // Mario hit by enemy + console.log(`👾 Mario hit by enemy - restarting level`); + if (onMarioDeath) onMarioDeath(); + } + } + }); + + // Piranha Plant collisions + piranhaPlants.forEach(plant => { + if (!plant.flattened && this.isColliding(mario, plant)) { + // Check if Mario jumped on plant + if (mario.velocityY > 0 && mario.y < plant.y + plant.height / 2) { + // Plant flattened + plant.flattened = true; + plant.flattenedTimer = 120; // Flattened for 2 seconds + mario.velocityY = -8; // Bounce + if (onAddParticles) onAddParticles(plant.x, plant.y, '#228B22'); + console.log(`🌸 Mario flattened piranha plant`); + } else { + // Mario hit by plant + console.log(`🌸 Mario hit by piranha plant - restarting level`); + if (onMarioDeath) onMarioDeath(); + } + } + + // Check if stepping on flattened plant + if (plant.flattened && this.isColliding(mario, plant)) { + mario.onGround = true; + } + }); + } + + /** + * Rectangle-Rectangle collision detection + * @param {Object} rect1 - First rectangle + * @param {Object} rect2 - Second rectangle + * @returns {boolean} - True if colliding + */ + static 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; + } + + /** + * Update camera to follow Mario + * @param {Object} camera - Camera object with x, y + * @param {Object} mario - Mario object + * @param {number} canvasWidth - Canvas width + */ + static updateCamera(camera, mario, canvasWidth) { + // Camera follows Mario horizontally, centered + camera.x = mario.x - canvasWidth / 2 + mario.width / 2; + camera.y = 0; // Fixed vertical camera + } + + /** + * Create particle effects + * @param {number} x - X position + * @param {number} y - Y position + * @param {string} color - Particle color + * @param {Array} particles - Particles array to add to + * @param {number} count - Number of particles to create + */ + static addParticles(x, y, color, particles, count = 10) { + for (let i = 0; i < count; i++) { + particles.push({ + x: x, + y: y, + velocityX: (Math.random() - 0.5) * 8, + velocityY: (Math.random() - 0.5) * 8, + life: 1.0, + decay: 0.02, + size: 4, + color: color + }); + } + } + + /** + * Create small particle burst + * @param {number} x - X position + * @param {number} y - Y position + * @param {string} color - Particle color + * @param {Array} particles - Particles array to add to + */ + static addSmallParticles(x, y, color, particles) { + this.addParticles(x, y, color, particles, 5); + } + + /** + * Update all particles + * @param {Array} particles - Array of particles + * @returns {Array} - Updated particles array (with dead particles removed) + */ + static updateParticles(particles) { + const updatedParticles = []; + + particles.forEach(particle => { + particle.x += particle.velocityX; + particle.y += particle.velocityY; + particle.velocityY += 0.3; // Gravity + particle.life -= particle.decay; + + if (particle.life > 0) { + updatedParticles.push(particle); + } + }); + + return updatedParticles; + } +} + +export default PhysicsEngine; diff --git a/src/gameHelpers/MarioEducational/README.md b/src/gameHelpers/MarioEducational/README.md new file mode 100644 index 0000000..d3b26ed --- /dev/null +++ b/src/gameHelpers/MarioEducational/README.md @@ -0,0 +1,123 @@ +# MarioEducational Game Helpers + +Modular helper classes to reduce the size of the main MarioEducational.js file. + +## 📁 Structure + +``` +gameHelpers/MarioEducational/ +├── SentenceGenerator.js (386 lines) - Educational sentence generation with proper grammar +├── SoundSystem.js (271 lines) - Web Audio API sound management +├── Renderer.js (625 lines) - All rendering methods with camera translation +├── enemies/ +│ ├── PiranhaPlant.js (133 lines) - Piranha plant enemy logic +│ ├── Catapult.js (347 lines) - Catapult/Onager + Boulders + Stones +│ ├── FlyingEye.js (187 lines) - Flying eye chase AI + dash attacks +│ ├── Boss.js (254 lines) - Colossal boss + turrets + minions +│ └── Projectile.js (147 lines) - Projectile management +└── README.md +``` + +## 📊 Statistics + +### Helpers Created +- **Total Files**: 9 files +- **Total Lines**: 2,350 lines of modular, reusable code +- **Categories**: Sentences, Sound, Rendering, Enemies (5 types) + +### Main File Reduction +- **Before**: 3,901 lines / 156 KB +- **After helpers**: ~1,900 lines / ~75 KB (estimated after full integration) +- **Reduction**: ~51% smaller, ~2,000 lines extracted + +## 🎯 Usage + +### Sentence Generation +```javascript +import { sentenceGenerator } from './gameHelpers/MarioEducational/SentenceGenerator.js'; + +const sentence = sentenceGenerator.generateSentence('apple', { + type: 'noun', + user_language: 'pomme' +}); +// Returns: { english: "I see an apple.", translation: "pomme - I see an **apple**." } +``` + +### Sound System +```javascript +import { soundSystem } from './gameHelpers/MarioEducational/SoundSystem.js'; + +soundSystem.initialize(); +soundSystem.play('jump'); +soundSystem.play('enemy_defeat', 0.5); // Volume 0-1 +``` + +### Rendering +```javascript +import { renderer } from './gameHelpers/MarioEducational/Renderer.js'; + +const gameState = { + mario, camera, platforms, enemies, /* ... */ +}; +renderer.render(ctx, gameState, config); +``` + +### Enemies +```javascript +import PiranhaPlant from './gameHelpers/MarioEducational/enemies/PiranhaPlant.js'; +import Catapult from './gameHelpers/MarioEducational/enemies/Catapult.js'; +import FlyingEye from './gameHelpers/MarioEducational/enemies/FlyingEye.js'; +import Boss from './gameHelpers/MarioEducational/enemies/Boss.js'; +import Projectile from './gameHelpers/MarioEducational/enemies/Projectile.js'; + +// Generation +const plants = PiranhaPlant.generate(level, difficulty); +const catapults = Catapult.generate(level, levelIndex, levelWidth, canvasHeight); +const eyes = FlyingEye.generate(level, difficulty); +const { boss, turrets } = Boss.generate(level, levelWidth, canvasHeight); + +// Update +PiranhaPlant.update(plants, mario, projectiles, playSound); +Catapult.update(catapults, mario, boulders, stones, playSound); +FlyingEye.update(eyes, mario, playSound); +Boss.update(boss, turrets, mario, projectiles, flyingEyes, playSound); + +// Projectiles +const updatedProjectiles = Projectile.update(projectiles, mario, platforms, walls, levelWidth, onMarioHit, onObstacleHit); +``` + +## ✨ Benefits + +1. **Modularity**: Each system is self-contained and testable +2. **Reusability**: Helpers can be used in other games +3. **Maintainability**: Smaller files are easier to understand and modify +4. **Performance**: No performance impact, pure code organization +5. **Scalability**: Easy to add new enemy types or features + +## 🔧 Architecture Principles + +- **Single Responsibility**: Each helper has one clear purpose +- **Stateless**: Most helpers are stateless (except SoundSystem) +- **Dependency Injection**: Helpers receive data as parameters +- **No Side Effects**: Helpers don't modify global state +- **Pure Functions**: Most methods are pure (same input → same output) + +## 📚 Documentation + +Each helper file contains detailed JSDoc comments explaining: +- Method parameters and return values +- Usage examples +- Edge cases and assumptions +- Performance considerations + +## 🚀 Future Improvements + +- Extract LevelGenerator (~1500 lines) - largest remaining opportunity +- Extract PhysicsEngine (~400 lines) - collision detection and movement +- Add unit tests for each helper +- Create EnemyFactory for unified enemy creation +- Add TypeScript definitions for better IDE support + +--- + +**Result**: Clean, maintainable, and scalable game architecture! 🎮 diff --git a/src/gameHelpers/MarioEducational/Renderer.js b/src/gameHelpers/MarioEducational/Renderer.js new file mode 100644 index 0000000..21299f8 --- /dev/null +++ b/src/gameHelpers/MarioEducational/Renderer.js @@ -0,0 +1,625 @@ +/** + * Renderer.js + * Helper for rendering all game elements (Mario, enemies, platforms, effects, etc.) + * Handles canvas drawing with proper layering and camera translation + */ + +export class Renderer { + constructor() { + // No internal state - all rendering is stateless based on game data + } + + /** + * Main render method - orchestrates all rendering + * @param {CanvasRenderingContext2D} ctx - Canvas 2D context + * @param {Object} gameState - Current game state with all entities + * @param {Object} config - Game configuration + */ + render(ctx, gameState, config) { + // Clear canvas + ctx.clearRect(0, 0, config.canvasWidth, config.canvasHeight); + + // Render background (no camera translation) + this.renderBackground(ctx, gameState.camera, config); + + // Save context for camera translation + ctx.save(); + ctx.translate(-gameState.camera.x, -gameState.camera.y); + + // Render world elements (with camera translation) + this.renderPlatforms(ctx, gameState.platforms); + this.renderQuestionBlocks(ctx, gameState.questionBlocks); + this.renderEnemies(ctx, gameState.enemies); + this.renderWalls(ctx, gameState.walls); + + // Advanced level elements + if (gameState.piranhaPlants) this.renderPiranhaPlants(ctx, gameState.piranhaPlants); + if (gameState.projectiles) this.renderProjectiles(ctx, gameState.projectiles); + + // Level 4+ elements + if (gameState.catapults) this.renderCatapults(ctx, gameState.catapults); + if (gameState.boulders) this.renderBoulders(ctx, gameState.boulders); + if (gameState.stones) this.renderStones(ctx, gameState.stones); + + // Level 5+ elements + if (gameState.flyingEyes) this.renderFlyingEyes(ctx, gameState.flyingEyes); + + // Level 6 boss elements + if (gameState.boss) this.renderBoss(ctx, gameState.boss); + + // Castle + if (gameState.castle) this.renderCastle(ctx, gameState.castle); + + // Finish line + if (gameState.finishLine) this.renderFinishLine(ctx, gameState.finishLine, gameState.currentLevel); + + // Mario + this.renderMario(ctx, gameState.mario); + + // Particles + if (gameState.particles) this.renderParticles(ctx, gameState.particles); + + // Debug hitboxes + if (gameState.debugMode) { + this.renderDebugHitboxes(ctx, gameState); + } + + // Restore context + ctx.restore(); + + // Render UI (no camera translation) + this.renderUI(ctx, gameState, config); + } + + /** + * Render background with sky gradient and clouds + */ + renderBackground(ctx, camera, config) { + // Sky gradient + const gradient = ctx.createLinearGradient(0, 0, 0, config.canvasHeight); + gradient.addColorStop(0, '#87CEEB'); + gradient.addColorStop(1, '#98FB98'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, config.canvasWidth, config.canvasHeight); + + // Clouds (parallax scrolling - slower than camera) + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + for (let i = 0; i < 5; i++) { + const x = (i * 300 + 100 - camera.x * 0.3) % (config.canvasWidth + 200); + const y = 80 + i * 30; + this.renderCloud(ctx, x, y); + } + } + + /** + * Render a single cloud + */ + renderCloud(ctx, x, y) { + ctx.beginPath(); + ctx.arc(x, y, 30, 0, Math.PI * 2); + ctx.arc(x + 30, y, 40, 0, Math.PI * 2); + ctx.arc(x + 60, y, 30, 0, Math.PI * 2); + ctx.fill(); + } + + /** + * Render platforms + */ + renderPlatforms(ctx, platforms) { + platforms.forEach(platform => { + ctx.fillStyle = platform.color; + ctx.fillRect(platform.x, platform.y, platform.width, platform.height); + + // Add outline + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(platform.x, platform.y, platform.width, platform.height); + }); + } + + /** + * Render question blocks + */ + renderQuestionBlocks(ctx, questionBlocks) { + questionBlocks.forEach(block => { + // Block body + ctx.fillStyle = block.hit ? '#666' : '#FFD700'; + ctx.fillRect(block.x, block.y, block.width, block.height); + + // Border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(block.x, block.y, block.width, block.height); + + // Question mark (if not hit) + if (!block.hit) { + ctx.fillStyle = '#000'; + ctx.font = 'bold 24px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('?', block.x + block.width / 2, block.y + block.height / 2); + } + }); + } + + /** + * Render enemies + */ + renderEnemies(ctx, enemies) { + enemies.forEach(enemy => { + // Enemy body + ctx.fillStyle = '#FF6B6B'; + ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height); + + // Eyes + ctx.fillStyle = '#FFF'; + ctx.fillRect(enemy.x + 5, enemy.y + 5, 8, 8); + ctx.fillRect(enemy.x + enemy.width - 13, enemy.y + 5, 8, 8); + + // Pupils + ctx.fillStyle = '#000'; + ctx.fillRect(enemy.x + 7, enemy.y + 7, 4, 4); + ctx.fillRect(enemy.x + enemy.width - 11, enemy.y + 7, 4, 4); + + // Border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(enemy.x, enemy.y, enemy.width, enemy.height); + }); + } + + /** + * Render walls + */ + renderWalls(ctx, walls) { + walls.forEach(wall => { + // Main wall color + ctx.fillStyle = wall.color || '#8B4513'; + ctx.fillRect(wall.x, wall.y, wall.width, wall.height); + + // Brick pattern + ctx.strokeStyle = '#654321'; + ctx.lineWidth = 2; + + const brickWidth = 40; + const brickHeight = 20; + + for (let y = wall.y; y < wall.y + wall.height; y += brickHeight) { + const offset = Math.floor((y - wall.y) / brickHeight) % 2 === 0 ? 0 : brickWidth / 2; + for (let x = wall.x + offset; x < wall.x + wall.width; x += brickWidth) { + ctx.strokeRect(x, y, Math.min(brickWidth, wall.x + wall.width - x), brickHeight); + } + } + + // Outer border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 3; + ctx.strokeRect(wall.x, wall.y, wall.width, wall.height); + }); + } + + /** + * Render piranha plants + */ + renderPiranhaPlants(ctx, plants) { + plants.forEach(plant => { + if (!plant.visible) return; + + const pipeHeight = 60; + const pipeY = plant.y + plant.height - pipeHeight; + + // Pipe + ctx.fillStyle = '#2D882D'; + ctx.fillRect(plant.x, pipeY, plant.width, pipeHeight); + + // Pipe rim + ctx.fillStyle = '#3A9F3A'; + ctx.fillRect(plant.x - 5, pipeY, plant.width + 10, 10); + + // Pipe border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(plant.x, pipeY, plant.width, pipeHeight); + ctx.strokeRect(plant.x - 5, pipeY, plant.width + 10, 10); + + // Plant head (if extended) + if (plant.extended > 0) { + const headY = pipeY - plant.extended; + const headSize = 30; + + // Head + ctx.fillStyle = '#FF0000'; + ctx.beginPath(); + ctx.arc(plant.x + plant.width / 2, headY, headSize, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + // Spots + ctx.fillStyle = '#FFF'; + ctx.beginPath(); + ctx.arc(plant.x + plant.width / 2 - 10, headY - 5, 6, 0, Math.PI * 2); + ctx.arc(plant.x + plant.width / 2 + 10, headY - 5, 6, 0, Math.PI * 2); + ctx.fill(); + + // Mouth (open/close animation) + const mouthOpen = Math.sin(Date.now() / 200) > 0; + if (mouthOpen) { + ctx.fillStyle = '#000'; + ctx.fillRect(plant.x + plant.width / 2 - 12, headY + 8, 24, 8); + } + + // Stem + ctx.fillStyle = '#2D882D'; + ctx.fillRect(plant.x + plant.width / 2 - 5, headY, 10, plant.extended); + } + }); + } + + /** + * Render projectiles + */ + renderProjectiles(ctx, projectiles) { + projectiles.forEach(proj => { + ctx.fillStyle = proj.color || '#FF4444'; + ctx.beginPath(); + ctx.arc(proj.x, proj.y, proj.radius, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.stroke(); + }); + } + + /** + * Render catapults + */ + renderCatapults(ctx, catapults) { + catapults.forEach(catapult => { + // Base + ctx.fillStyle = '#654321'; + ctx.fillRect(catapult.x, catapult.y + 20, catapult.width, 20); + + // Arm + ctx.save(); + ctx.translate(catapult.x + catapult.width / 2, catapult.y + 30); + ctx.rotate(catapult.armAngle); + ctx.fillStyle = '#8B4513'; + ctx.fillRect(-5, -40, 10, 40); + ctx.restore(); + + // Border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(catapult.x, catapult.y + 20, catapult.width, 20); + }); + } + + /** + * Render boulders + */ + renderBoulders(ctx, boulders) { + boulders.forEach(boulder => { + // Boulder body + ctx.fillStyle = '#808080'; + ctx.beginPath(); + ctx.arc(boulder.x, boulder.y, boulder.radius, 0, Math.PI * 2); + ctx.fill(); + + // Cracks/texture + ctx.strokeStyle = '#606060'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(boulder.x - boulder.radius * 0.5, boulder.y - boulder.radius * 0.3); + ctx.lineTo(boulder.x + boulder.radius * 0.5, boulder.y + boulder.radius * 0.3); + ctx.stroke(); + + // Border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(boulder.x, boulder.y, boulder.radius, 0, Math.PI * 2); + ctx.stroke(); + }); + } + + /** + * Render stones (stone rain) + */ + renderStones(ctx, stones) { + stones.forEach(stone => { + ctx.fillStyle = '#A9A9A9'; + ctx.fillRect(stone.x, stone.y, stone.width, stone.height); + + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.strokeRect(stone.x, stone.y, stone.width, stone.height); + }); + } + + /** + * Render flying eyes + */ + renderFlyingEyes(ctx, eyes) { + eyes.forEach(eye => { + // Outer eye shape + ctx.fillStyle = '#FFE6E6'; + ctx.beginPath(); + ctx.ellipse(eye.x, eye.y, eye.width / 2, eye.height / 2, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#FF0000'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Iris + ctx.fillStyle = '#8B0000'; + ctx.beginPath(); + ctx.arc(eye.x, eye.y, eye.width / 4, 0, Math.PI * 2); + ctx.fill(); + + // Pupil + ctx.fillStyle = '#000'; + ctx.beginPath(); + ctx.arc(eye.x, eye.y, eye.width / 8, 0, Math.PI * 2); + ctx.fill(); + + // Health bar + if (eye.health < eye.maxHealth) { + const barWidth = eye.width; + const barHeight = 4; + const barX = eye.x - barWidth / 2; + const barY = eye.y - eye.height / 2 - 10; + + // Background + ctx.fillStyle = '#FF0000'; + ctx.fillRect(barX, barY, barWidth, barHeight); + + // Health + ctx.fillStyle = '#00FF00'; + const healthWidth = (eye.health / eye.maxHealth) * barWidth; + ctx.fillRect(barX, barY, healthWidth, barHeight); + + // Border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.strokeRect(barX, barY, barWidth, barHeight); + } + }); + } + + /** + * Render boss + */ + renderBoss(ctx, boss) { + if (!boss || !boss.active) return; + + // Boss body (large imposing figure) + ctx.fillStyle = boss.enraged ? '#8B0000' : '#FF4444'; + ctx.fillRect(boss.x, boss.y, boss.width, boss.height); + + // Armor plates + ctx.fillStyle = '#2F4F4F'; + ctx.fillRect(boss.x + 10, boss.y + 20, boss.width - 20, 20); + ctx.fillRect(boss.x + 10, boss.y + 60, boss.width - 20, 20); + + // Eyes (angry) + ctx.fillStyle = boss.enraged ? '#FFFF00' : '#FFF'; + ctx.fillRect(boss.x + 20, boss.y + 40, 30, 20); + ctx.fillRect(boss.x + boss.width - 50, boss.y + 40, 30, 20); + + // Pupils (follow player) + ctx.fillStyle = '#000'; + ctx.fillRect(boss.x + 35, boss.y + 45, 10, 10); + ctx.fillRect(boss.x + boss.width - 35, boss.y + 45, 10, 10); + + // Boss border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 4; + ctx.strokeRect(boss.x, boss.y, boss.width, boss.height); + + // Health bar (prominent) + const barWidth = boss.width; + const barHeight = 10; + const barX = boss.x; + const barY = boss.y - 20; + + // Background + ctx.fillStyle = '#400000'; + ctx.fillRect(barX, barY, barWidth, barHeight); + + // Health + const healthPercent = boss.health / boss.maxHealth; + ctx.fillStyle = healthPercent > 0.5 ? '#00FF00' : healthPercent > 0.25 ? '#FFFF00' : '#FF0000'; + ctx.fillRect(barX, barY, barWidth * healthPercent, barHeight); + + // Border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(barX, barY, barWidth, barHeight); + + // Boss name/title + ctx.fillStyle = '#FFF'; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 3; + ctx.font = 'bold 16px Arial'; + ctx.textAlign = 'center'; + ctx.strokeText('COLOSSAL BOSS', boss.x + boss.width / 2, barY - 5); + ctx.fillText('COLOSSAL BOSS', boss.x + boss.width / 2, barY - 5); + } + + /** + * Render castle + */ + renderCastle(ctx, castle) { + if (!castle) return; + + // Main castle body + ctx.fillStyle = '#888'; + ctx.fillRect(castle.x, castle.y, castle.width, castle.height); + + // Towers + const towerWidth = 40; + const towerHeight = 80; + + // Left tower + ctx.fillRect(castle.x - 20, castle.y - 30, towerWidth, towerHeight); + + // Right tower + ctx.fillRect(castle.x + castle.width - 20, castle.y - 30, towerWidth, towerHeight); + + // Tower tops (triangular) + ctx.fillStyle = '#666'; + ctx.beginPath(); + ctx.moveTo(castle.x - 20, castle.y - 30); + ctx.lineTo(castle.x + 20, castle.y - 60); + ctx.lineTo(castle.x + 20, castle.y - 30); + ctx.fill(); + + ctx.beginPath(); + ctx.moveTo(castle.x + castle.width - 20, castle.y - 30); + ctx.lineTo(castle.x + castle.width + 20, castle.y - 60); + ctx.lineTo(castle.x + castle.width + 20, castle.y - 30); + ctx.fill(); + + // Door + ctx.fillStyle = '#654321'; + ctx.fillRect(castle.x + castle.width / 2 - 20, castle.y + castle.height - 50, 40, 50); + + // Windows + ctx.fillStyle = '#FFFF00'; + ctx.fillRect(castle.x + 20, castle.y + 20, 15, 20); + ctx.fillRect(castle.x + castle.width - 35, castle.y + 20, 15, 20); + + // Borders + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(castle.x, castle.y, castle.width, castle.height); + } + + /** + * Render finish line + */ + renderFinishLine(ctx, finishLine, currentLevel) { + // Checkered flag pole + ctx.fillStyle = '#000'; + ctx.fillRect(finishLine.x, finishLine.y, 5, finishLine.height); + + // Flag (checkered pattern) + const flagWidth = 60; + const flagHeight = 40; + const squareSize = 10; + + for (let y = 0; y < flagHeight; y += squareSize) { + for (let x = 0; x < flagWidth; x += squareSize) { + const isBlack = ((x / squareSize) + (y / squareSize)) % 2 === 0; + ctx.fillStyle = isBlack ? '#000' : '#FFF'; + ctx.fillRect(finishLine.x + 5 + x, finishLine.y + y, squareSize, squareSize); + } + } + + // Flag border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(finishLine.x + 5, finishLine.y, flagWidth, flagHeight); + + // Level number on flag + ctx.fillStyle = '#FFD700'; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.font = 'bold 16px Arial'; + ctx.textAlign = 'center'; + ctx.strokeText(`${currentLevel + 1}`, finishLine.x + 35, finishLine.y + 25); + ctx.fillText(`${currentLevel + 1}`, finishLine.x + 35, finishLine.y + 25); + } + + /** + * Render Mario + */ + renderMario(ctx, mario) { + // Mario body + ctx.fillStyle = '#FF0000'; + ctx.fillRect(mario.x, mario.y, mario.width, mario.height); + + // Overalls + ctx.fillStyle = '#0000FF'; + ctx.fillRect(mario.x + 5, mario.y + mario.height / 2, mario.width - 10, mario.height / 2); + + // Face + ctx.fillStyle = '#FFD700'; + ctx.fillRect(mario.x + 8, mario.y + 5, mario.width - 16, 15); + + // Eyes + ctx.fillStyle = '#000'; + ctx.fillRect(mario.x + 10, mario.y + 10, 4, 4); + ctx.fillRect(mario.x + mario.width - 14, mario.y + 10, 4, 4); + + // Border + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + ctx.strokeRect(mario.x, mario.y, mario.width, mario.height); + } + + /** + * Render particles (explosions, dust, etc.) + */ + renderParticles(ctx, particles) { + particles.forEach(particle => { + ctx.fillStyle = particle.color; + ctx.globalAlpha = particle.life; + ctx.fillRect(particle.x - 2, particle.y - 2, 4, 4); + }); + ctx.globalAlpha = 1.0; // Reset alpha + } + + /** + * Render UI overlay (lives, score, level) + */ + renderUI(ctx, gameState, config) { + // Semi-transparent background + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(0, 0, config.canvasWidth, 40); + + // Text style + ctx.fillStyle = '#FFF'; + ctx.font = 'bold 16px Arial'; + ctx.textAlign = 'left'; + + // Lives + ctx.fillText(`❤️ Lives: ${gameState.lives}`, 10, 25); + + // Score + ctx.textAlign = 'center'; + ctx.fillText(`Score: ${gameState.score}`, config.canvasWidth / 2, 25); + + // Level + ctx.textAlign = 'right'; + ctx.fillText(`Level: ${gameState.currentLevel + 1}`, config.canvasWidth - 10, 25); + } + + /** + * Render debug hitboxes (for development) + */ + renderDebugHitboxes(ctx, gameState) { + ctx.strokeStyle = '#FF00FF'; + ctx.lineWidth = 1; + + // Mario hitbox + ctx.strokeRect(gameState.mario.x, gameState.mario.y, gameState.mario.width, gameState.mario.height); + + // Enemy hitboxes + if (gameState.enemies) { + gameState.enemies.forEach(enemy => { + ctx.strokeRect(enemy.x, enemy.y, enemy.width, enemy.height); + }); + } + + // Platform hitboxes + if (gameState.platforms) { + gameState.platforms.forEach(platform => { + ctx.strokeRect(platform.x, platform.y, platform.width, platform.height); + }); + } + } +} + +// Export singleton instance +export const renderer = new Renderer(); diff --git a/src/gameHelpers/MarioEducational/SentenceGenerator.js b/src/gameHelpers/MarioEducational/SentenceGenerator.js new file mode 100644 index 0000000..488ac04 --- /dev/null +++ b/src/gameHelpers/MarioEducational/SentenceGenerator.js @@ -0,0 +1,386 @@ +/** + * SentenceGenerator.js + * Helper for generating contextual educational sentences from vocabulary words + * Handles proper grammar, articles, verb conjugation, and context variety + */ + +export class SentenceGenerator { + constructor() { + // Vowel sounds for article detection + this._vowelSounds = ['a', 'e', 'i', 'o', 'u']; + + // Special cases for articles (words starting with 'u' that use 'a') + this._aExceptions = ['university', 'uniform', 'unicorn', 'unique', 'unit', 'union']; + + // Common irregular verbs (present tense 3rd person) + this._irregularVerbs = { + 'go': 'goes', + 'do': 'does', + 'have': 'has', + 'be': 'is', + 'say': 'says', + 'try': 'tries', + 'fly': 'flies', + 'cry': 'cries', + 'study': 'studies' + }; + + // Sentence templates by word type with proper grammar + this._templates = { + 'noun': { + beginner: [ + (w) => `This is ${this._getArticle(w)} ${w}.`, + (w) => `I see ${this._getArticle(w)} ${w}.`, + (w) => `Look at the ${w}!`, + (w) => `I have ${this._getArticle(w)} ${w}.`, + (w) => `Where is the ${w}?` + ], + intermediate: [ + (w) => `The ${w} is on the table.`, + (w) => `I need ${this._getArticle(w)} ${w} for this task.`, + (w) => `Can you find the ${w}?`, + (w) => `She bought ${this._getArticle(w)} ${w} yesterday.`, + (w) => `The ${w} looks beautiful today.` + ], + advanced: [ + (w) => `The ${w} represents an important concept in our discussion.`, + (w) => `Without ${this._getArticle(w)} ${w}, this would be impossible.`, + (w) => `The ${w} has become increasingly popular recently.`, + (w) => `Many people underestimate the value of ${this._getArticle(w)} ${w}.`, + (w) => `The ${w} plays a crucial role in this process.` + ] + }, + 'verb': { + beginner: [ + (w) => `I ${w} every day.`, + (w) => `Please ${w} this.`, + (w) => `Can you ${w}?`, + (w) => `Let's ${w} together.`, + (w) => `Don't ${w} too fast.` + ], + intermediate: [ + (w) => `She ${this._conjugateThirdPerson(w)} every morning.`, + (w) => `They will ${w} tomorrow.`, + (w) => `We should ${w} more often.`, + (w) => `He ${this._conjugateThirdPerson(w)} very well.`, + (w) => `I want to ${w} better.` + ], + advanced: [ + (w) => `The ability to ${w} effectively is essential.`, + (w) => `She has been ${this._getGerund(w)} for years.`, + (w) => `Learning to ${w} requires practice and patience.`, + (w) => `He ${this._conjugateThirdPerson(w)} with remarkable skill.`, + (w) => `They decided to ${w} despite the challenges.` + ] + }, + 'adjective': { + beginner: [ + (w) => `It is ${w}.`, + (w) => `The house is ${w}.`, + (w) => `This looks ${w}.`, + (w) => `How ${w}!`, + (w) => `Very ${w} indeed.` + ], + intermediate: [ + (w) => `The weather seems quite ${w} today.`, + (w) => `She appears ${w} and happy.`, + (w) => `This is more ${w} than before.`, + (w) => `The ${w} building stands tall.`, + (w) => `Everyone feels ${w} about it.` + ], + advanced: [ + (w) => `The ${w} atmosphere created a perfect ambiance.`, + (w) => `His ${w} demeanor impressed everyone.`, + (w) => `The situation became increasingly ${w}.`, + (w) => `She maintained a ${w} attitude throughout.`, + (w) => `The ${w} nature of the problem requires attention.` + ] + }, + 'adverb': { + beginner: [ + (w) => `Walk ${w}.`, + (w) => `Do it ${w}.`, + (w) => `Move ${w}.`, + (w) => `Talk ${w}.`, + (w) => `Run ${w}.` + ], + intermediate: [ + (w) => `He speaks ${w} and clearly.`, + (w) => `She works ${w} every day.`, + (w) => `They arrived ${w} at the meeting.`, + (w) => `The car moves ${w} down the road.`, + (w) => `Please listen ${w} to the instructions.` + ], + advanced: [ + (w) => `The team performed ${w} under pressure.`, + (w) => `She ${w} explained the complex concept.`, + (w) => `The project progressed ${w} despite setbacks.`, + (w) => `He ${w} adapted to the new environment.`, + (w) => `The strategy was ${w} implemented.` + ] + }, + 'preposition': { + beginner: [ + (w) => `The book is ${w} the table.`, + (w) => `Go ${w} the door.`, + (w) => `Look ${w} the window.`, + (w) => `It's ${w} the box.`, + (w) => `Put it ${w} here.` + ], + intermediate: [ + (w) => `The cat jumped ${w} the fence.`, + (w) => `We walked ${w} the park together.`, + (w) => `She placed it ${w} the shelf.`, + (w) => `They traveled ${w} the mountains.`, + (w) => `The bird flew ${w} the trees.` + ], + advanced: [ + (w) => `The discussion centered ${w} the main topic.`, + (w) => `Success depends ${w} consistent effort.`, + (w) => `The solution lies ${w} these principles.`, + (w) => `Progress moved ${w} expectations.`, + (w) => `The argument stands ${w} scrutiny.` + ] + } + }; + } + + /** + * Get appropriate article (a/an) for a word + * @param {string} word - The word to check + * @returns {string} - "a" or "an" + */ + _getArticle(word) { + if (!word) return 'a'; + + const firstLetter = word.charAt(0).toLowerCase(); + + // Check exceptions (words starting with 'u' but pronounced with consonant sound) + if (this._aExceptions.some(exception => word.toLowerCase().startsWith(exception))) { + return 'a'; + } + + // Check if starts with vowel sound + return this._vowelSounds.includes(firstLetter) ? 'an' : 'a'; + } + + /** + * Conjugate verb to 3rd person singular present + * @param {string} verb - Base form of verb + * @returns {string} - Conjugated verb + */ + _conjugateThirdPerson(verb) { + if (!verb) return verb; + + // Check irregular verbs + if (this._irregularVerbs[verb.toLowerCase()]) { + return this._irregularVerbs[verb.toLowerCase()]; + } + + // Regular conjugation rules + const lowerVerb = verb.toLowerCase(); + + // Verbs ending in -y preceded by consonant: try -> tries + if (lowerVerb.endsWith('y') && lowerVerb.length > 1) { + const beforeY = lowerVerb.charAt(lowerVerb.length - 2); + if (!'aeiou'.includes(beforeY)) { + return verb.slice(0, -1) + 'ies'; + } + } + + // Verbs ending in -s, -x, -z, -ch, -sh: add -es + if (lowerVerb.endsWith('s') || lowerVerb.endsWith('x') || + lowerVerb.endsWith('z') || lowerVerb.endsWith('ch') || + lowerVerb.endsWith('sh')) { + return verb + 'es'; + } + + // Verbs ending in -o: add -es + if (lowerVerb.endsWith('o')) { + return verb + 'es'; + } + + // Default: add -s + return verb + 's'; + } + + /** + * Convert verb to gerund form (present participle) + * @param {string} verb - Base form of verb + * @returns {string} - Gerund form + */ + _getGerund(verb) { + if (!verb) return verb; + + const lowerVerb = verb.toLowerCase(); + + // Verbs ending in -e: remove -e and add -ing (make -> making) + if (lowerVerb.endsWith('e') && lowerVerb !== 'be') { + return verb.slice(0, -1) + 'ing'; + } + + // Verbs ending in -ie: change to -ying (lie -> lying) + if (lowerVerb.endsWith('ie')) { + return verb.slice(0, -2) + 'ying'; + } + + // Single syllable verbs ending in consonant-vowel-consonant: double last letter + // (run -> running, stop -> stopping) + if (lowerVerb.length >= 3) { + const last = lowerVerb.slice(-1); + const secondLast = lowerVerb.slice(-2, -1); + const thirdLast = lowerVerb.slice(-3, -2); + + const isConsonant = (c) => !'aeiou'.includes(c); + + if (isConsonant(last) && !isConsonant(secondLast) && isConsonant(thirdLast)) { + // But not for verbs ending in w, x, y + if (!'wxy'.includes(last)) { + return verb + last + 'ing'; + } + } + } + + // Default: add -ing + return verb + 'ing'; + } + + /** + * Determine difficulty level from word frequency or context + * @param {Object} data - Word data + * @returns {string} - "beginner", "intermediate", or "advanced" + */ + _getDifficultyLevel(data) { + // You can enhance this with actual word frequency data or CEFR levels + // For now, use simple heuristics + + if (data.level) { + return data.level; // If explicitly provided + } + + // Simple heuristic: word length + const wordLength = (data.word || '').length; + if (wordLength <= 5) return 'beginner'; + if (wordLength <= 8) return 'intermediate'; + return 'advanced'; + } + + /** + * Generate a contextual sentence from a vocabulary word + * @param {string} word - The vocabulary word + * @param {Object} data - Word data including type and translation + * @returns {Object} - Generated sentence with English and translation + */ + generateSentence(word, data) { + if (!word || !data) { + console.warn('Invalid word or data for sentence generation'); + return { + english: `This is ${word}.`, + translation: `${data?.user_language || word}` + }; + } + + const type = (data.type || 'noun').toLowerCase(); + const translation = data.user_language ? data.user_language.split(';')[0].trim() : word; + const difficulty = this._getDifficultyLevel({...data, word}); + + // Get templates for this type and difficulty + const typeTemplates = this._templates[type] || this._templates['noun']; + const difficultyTemplates = typeTemplates[difficulty] || typeTemplates['beginner']; + + // Select random template + const randomTemplate = difficultyTemplates[Math.floor(Math.random() * difficultyTemplates.length)]; + + // Generate sentence using template function + const englishSentence = randomTemplate(word); + + // Create translation with highlighted word + const highlightedWord = `**${word}**`; + const translationText = `${translation} - ${englishSentence.replace(new RegExp(`\\b${word}\\b`, 'gi'), highlightedWord)}`; + + return { + english: englishSentence, + translation: translationText, + difficulty: difficulty, + wordType: type + }; + } + + /** + * Split long text into individual sentences + * @param {string} text - Long text to split + * @returns {Array} - Array of sentences + */ + splitTextIntoSentences(text) { + if (!text || typeof text !== 'string') return []; + + // Clean the text + text = text.trim(); + + // Split by common sentence endings (., !, ?) + // But preserve abbreviations like "Mr.", "Dr.", etc. + const sentences = text.split(/(?<=[.!?])\s+(?=[A-Z])/); + + return sentences + .map(s => s.trim()) + .filter(s => s.length > 0 && s.length < 200) // Filter out too long or empty + .filter(s => { + // Filter out sentences that are just numbers or too short + const words = s.split(/\s+/); + return words.length >= 3 && words.length <= 30; + }); + } + + /** + * Validate generated sentence quality + * @param {Object} sentence - Generated sentence object + * @returns {boolean} - True if sentence meets quality standards + */ + isValidSentence(sentence) { + if (!sentence || !sentence.english) return false; + + const words = sentence.english.split(/\s+/); + + // Must have at least 2 words + if (words.length < 2) return false; + + // Must end with proper punctuation + if (!/[.!?]$/.test(sentence.english)) return false; + + // Must start with capital letter + if (!/^[A-Z]/.test(sentence.english)) return false; + + return true; + } + + /** + * Generate multiple sentences from a word list + * @param {Array} wordList - Array of {word, data} objects + * @param {number} count - Number of sentences to generate + * @returns {Array} - Array of sentence objects + */ + generateMultipleSentences(wordList, count = 10) { + const sentences = []; + + for (let i = 0; i < Math.min(count, wordList.length); i++) { + const {word, data} = wordList[i]; + const sentence = this.generateSentence(word, data); + + if (this.isValidSentence(sentence)) { + sentences.push({ + type: 'vocabulary', + english: sentence.english, + translation: sentence.translation, + context: data.type || 'vocabulary', + difficulty: sentence.difficulty, + wordType: sentence.wordType + }); + } + } + + return sentences; + } +} + +// Export singleton instance +export const sentenceGenerator = new SentenceGenerator(); diff --git a/src/gameHelpers/MarioEducational/SoundSystem.js b/src/gameHelpers/MarioEducational/SoundSystem.js new file mode 100644 index 0000000..2e72304 --- /dev/null +++ b/src/gameHelpers/MarioEducational/SoundSystem.js @@ -0,0 +1,272 @@ +/** + * SoundSystem.js + * Helper for managing Web Audio API sound generation and playback + * Generates programmatic retro game sounds without external audio files + */ + +export class SoundSystem { + constructor() { + this._audioContext = null; + this._sounds = {}; + this._initialized = false; + } + + /** + * Initialize the Web Audio Context + * @returns {boolean} - True if initialization successful + */ + initialize() { + if (this._initialized) { + console.log('🔊 Sound system already initialized'); + return true; + } + + try { + // Initialize Web Audio Context + this._audioContext = new (window.AudioContext || window.webkitAudioContext)(); + console.log('🔊 Sound system initialized'); + + // Create sound library + this._createSoundLibrary(); + this._initialized = true; + return true; + } catch (error) { + console.warn('⚠️ Sound system not available:', error); + this._audioContext = null; + this._initialized = false; + return false; + } + } + + /** + * Create the library of available sounds + * Each sound is defined with parameters for programmatic generation + */ + _createSoundLibrary() { + // Sound definitions with parameters for programmatic generation + this._sounds = { + jump: { + type: 'sweep', + frequency: 330, + endFrequency: 600, + duration: 0.1 + }, + coin: { + type: 'bell', + frequency: 800, + duration: 0.3 + }, + powerup: { + type: 'arpeggio', + frequencies: [264, 330, 396, 528], + duration: 0.6 + }, + enemy_defeat: { + type: 'noise_sweep', + frequency: 200, + endFrequency: 50, + duration: 0.2 + }, + question_block: { + type: 'sparkle', + frequency: 600, + endFrequency: 1200, + duration: 0.4 + }, + level_complete: { + type: 'victory', + frequencies: [523, 659, 784, 1047], + duration: 1.0 + }, + death: { + type: 'descend', + frequency: 300, + endFrequency: 100, + duration: 0.8 + }, + finish_stars: { + type: 'magical', + frequencies: [880, 1100, 1320, 1760], + duration: 2.0 + } + }; + + console.log('🎵 Sound library created with', Object.keys(this._sounds).length, 'sounds'); + } + + /** + * Play a sound by name + * @param {string} soundName - Name of the sound to play + * @param {number} volume - Volume level (0.0 to 1.0) + */ + play(soundName, volume = 0.3) { + if (!this._initialized || !this._audioContext || !this._sounds[soundName]) { + if (!this._sounds[soundName] && this._initialized) { + console.warn(`⚠️ Sound not found: ${soundName}`); + } + return; + } + + try { + const sound = this._sounds[soundName]; + const oscillator = this._audioContext.createOscillator(); + const gainNode = this._audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(this._audioContext.destination); + + const currentTime = this._audioContext.currentTime; + const duration = sound.duration; + + // Set volume envelope (fade in/out) + gainNode.gain.setValueAtTime(0, currentTime); + gainNode.gain.linearRampToValueAtTime(volume, currentTime + 0.01); + gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + duration); + + // Configure sound based on type + this._configureSoundType(oscillator, sound, currentTime, duration); + + oscillator.start(currentTime); + oscillator.stop(currentTime + duration); + + console.log(`🎵 Playing sound: ${soundName}`); + + } catch (error) { + console.warn('⚠️ Failed to play sound:', soundName, error); + } + } + + /** + * Configure oscillator based on sound type + * @param {OscillatorNode} oscillator - Web Audio oscillator + * @param {Object} sound - Sound configuration + * @param {number} currentTime - Current audio context time + * @param {number} duration - Sound duration + */ + _configureSoundType(oscillator, sound, currentTime, duration) { + switch (sound.type) { + case 'sweep': + // Frequency sweep (jump sound) + oscillator.type = 'square'; + oscillator.frequency.setValueAtTime(sound.frequency, currentTime); + oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration); + break; + + case 'bell': + // Bell-like sound (coin) + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(sound.frequency, currentTime); + oscillator.frequency.exponentialRampToValueAtTime(sound.frequency * 0.5, currentTime + duration); + break; + + case 'noise_sweep': + // Noise sweep (enemy defeat) + oscillator.type = 'sawtooth'; + oscillator.frequency.setValueAtTime(sound.frequency, currentTime); + oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration); + break; + + case 'sparkle': + // Sparkle effect (question block) + oscillator.type = 'triangle'; + oscillator.frequency.setValueAtTime(sound.frequency, currentTime); + oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration * 0.7); + oscillator.frequency.linearRampToValueAtTime(sound.frequency, currentTime + duration); + break; + + case 'descend': + // Descending tone (death) + oscillator.type = 'square'; + oscillator.frequency.setValueAtTime(sound.frequency, currentTime); + oscillator.frequency.exponentialRampToValueAtTime(sound.endFrequency, currentTime + duration); + break; + + case 'arpeggio': + case 'victory': + case 'magical': + // Complex multi-note sounds + oscillator.type = sound.type === 'magical' ? 'triangle' : 'square'; + oscillator.frequency.setValueAtTime(sound.frequencies[0], currentTime); + + // Schedule frequency changes for arpeggio effect + const noteLength = duration / sound.frequencies.length; + sound.frequencies.forEach((freq, index) => { + if (index > 0) { + oscillator.frequency.setValueAtTime(freq, currentTime + noteLength * index); + } + }); + break; + + default: + // Default fallback + oscillator.type = 'square'; + oscillator.frequency.setValueAtTime(sound.frequency || 440, currentTime); + } + } + + /** + * Add a custom sound to the library + * @param {string} name - Sound name + * @param {Object} config - Sound configuration + */ + addSound(name, config) { + if (!name || !config) { + console.warn('⚠️ Invalid sound configuration'); + return; + } + + this._sounds[name] = config; + console.log(`🎵 Added custom sound: ${name}`); + } + + /** + * Remove a sound from the library + * @param {string} name - Sound name to remove + */ + removeSound(name) { + if (this._sounds[name]) { + delete this._sounds[name]; + console.log(`🎵 Removed sound: ${name}`); + } + } + + /** + * Get list of available sounds + * @returns {Array} - Array of sound names + */ + getAvailableSounds() { + return Object.keys(this._sounds); + } + + /** + * Check if sound system is initialized + * @returns {boolean} + */ + isInitialized() { + return this._initialized; + } + + /** + * Get the audio context (for advanced usage) + * @returns {AudioContext|null} + */ + getAudioContext() { + return this._audioContext; + } + + /** + * Cleanup and close audio context + */ + destroy() { + if (this._audioContext) { + this._audioContext.close(); + this._audioContext = null; + } + this._sounds = {}; + this._initialized = false; + console.log('🔊 Sound system destroyed'); + } +} + +// Export singleton instance for convenience +export const soundSystem = new SoundSystem(); diff --git a/src/gameHelpers/MarioEducational/enemies/Boss.js b/src/gameHelpers/MarioEducational/enemies/Boss.js new file mode 100644 index 0000000..2fe13b6 --- /dev/null +++ b/src/gameHelpers/MarioEducational/enemies/Boss.js @@ -0,0 +1,254 @@ +/** + * Boss.js + * Colossal Boss enemy for level 6 + * Large immobile boss with turrets and special attacks + */ + +export class Boss { + /** + * Generate the colossal boss for level 6 + * @param {Object} level - Level data + * @param {number} levelWidth - Width of the level + * @param {number} canvasHeight - Canvas height + * @returns {Object} - Boss data with turrets + */ + static generate(level, levelWidth, canvasHeight) { + console.log(`👹 Generating Colossal Boss for level 6!`); + + // Boss positioned in center-right of level to block the path + const bossX = levelWidth * 0.6; // 60% through the level + const bossY = canvasHeight - 250; // Standing on ground + const bossWidth = 150; + const bossHeight = 200; + + const boss = { + x: bossX, + y: bossY, + width: bossWidth, + height: bossHeight, + health: 5, // Takes 5 hits + maxHealth: 5, + color: '#2F4F4F', // Dark slate gray + type: 'colossus', + active: true, + // Collision boxes (knees for damage) + leftKnee: { + x: bossX + 20, + y: bossY + bossHeight - 60, + width: 40, + height: 40 + }, + rightKnee: { + x: bossX + bossWidth - 60, + y: bossY + bossHeight - 60, + width: 40, + height: 40 + }, + // Boss behavior + lastTurretShot: Date.now(), + turretCooldown: 2000, // Turrets fire every 2 seconds + lastMinionLaunch: Date.now(), + minionCooldown: 4000, // Launch minions every 4 seconds + // Visual + eyeColor: '#FF0000', // Red glowing eyes + isDamaged: false, + damageFlashTimer: 0, + enraged: false // Becomes enraged at low health + }; + + // Generate turrets on the boss (2 turrets) + const turrets = [ + { + x: bossX + 30, + y: bossY + 50, + width: 25, + height: 25, + color: '#8B4513', + type: 'turret', + lastShot: Date.now(), + shootCooldown: 2500 // Individual cooldown + }, + { + x: bossX + bossWidth - 55, + y: bossY + 50, + width: 25, + height: 25, + color: '#8B4513', + type: 'turret', + lastShot: Date.now(), + shootCooldown: 3000 // Slightly different timing + } + ]; + + console.log(`👹 Colossal Boss spawned at x=${bossX.toFixed(0)}, health=${boss.health}`); + console.log(`🔫 ${turrets.length} turrets mounted on boss`); + + return { boss, turrets }; + } + + /** + * Update boss behavior + * @param {Object} boss - Boss object + * @param {Array} turrets - Boss turrets + * @param {Object} mario - Mario object + * @param {Array} projectiles - Projectiles array + * @param {Array} flyingEyes - Flying eyes array (for spawning minions) + * @param {Function} playSound - Sound callback + */ + static update(boss, turrets, mario, projectiles, flyingEyes, playSound) { + if (!boss || !boss.active) return; + + const currentTime = Date.now(); + + // Update boss state + if (boss.health < boss.maxHealth / 2) { + boss.enraged = true; + } + + // Damage flash animation + if (boss.isDamaged) { + boss.damageFlashTimer--; + if (boss.damageFlashTimer <= 0) { + boss.isDamaged = false; + } + } + + // Update turrets - they shoot projectiles at Mario + turrets.forEach(turret => { + if (currentTime - turret.lastShot > turret.shootCooldown) { + // Calculate trajectory to Mario + const dx = mario.x - turret.x; + const dy = mario.y - turret.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + const speed = boss.enraged ? 6 : 4; // Faster when enraged + const velocityX = (dx / distance) * speed; + const velocityY = (dy / distance) * speed; + + projectiles.push({ + x: turret.x + turret.width / 2, + y: turret.y + turret.height / 2, + velocityX: velocityX, + velocityY: velocityY, + radius: 10, + color: boss.enraged ? '#FF0000' : '#FF8C00', // Red when enraged, orange normally + type: 'boss_projectile', + life: 300 + }); + + turret.lastShot = currentTime; + if (playSound) playSound('enemy_defeat'); + console.log(`🔫 Boss turret fired at Mario!`); + } + }); + + // Spawn flying eye minions periodically + if (boss.enraged && currentTime - boss.lastMinionLaunch > boss.minionCooldown) { + // Spawn a flying eye minion + const minionX = boss.x + boss.width / 2; + const minionY = boss.y + 50; + + flyingEyes.push({ + x: minionX, + y: minionY, + width: 25, + height: 25, + velocityX: (Math.random() - 0.5) * 2, + velocityY: -3, // Fly upward initially + color: '#8B0000', // Dark red for minions + pupilColor: '#000000', + type: 'flying_eye_minion', + health: 1, + maxHealth: 1, + chaseDistance: 300, + chaseSpeed: 3, + idleSpeed: 1, + lastDirectionChange: Date.now(), + directionChangeInterval: 2000, + isChasing: false, + dashCooldown: 0, + dashDuration: 0, + isDashing: false, + dashSpeed: 6, + lastDashTime: Date.now(), + dashInterval: 4000, + blinkTimer: 0, + isBlinking: false + }); + + boss.lastMinionLaunch = currentTime; + if (playSound) playSound('powerup'); + console.log(`👁️ Boss spawned a flying eye minion!`); + } + } + + /** + * Damage the boss + * @param {Object} boss - Boss object + * @param {Function} playSound - Sound callback + * @returns {boolean} - True if boss was defeated + */ + static damage(boss, playSound) { + if (!boss || !boss.active) return false; + + boss.health--; + boss.isDamaged = true; + boss.damageFlashTimer = 15; // Flash for 15 frames + + if (playSound) playSound('enemy_defeat'); + console.log(`👹 Boss damaged! Health: ${boss.health}/${boss.maxHealth}`); + + if (boss.health <= 0) { + boss.active = false; + console.log(`👹 BOSS DEFEATED!`); + return true; + } + + return false; + } + + /** + * Check collision between Mario and boss knees (weak points) + * @param {Object} mario - Mario object + * @param {Object} boss - Boss object + * @returns {boolean} - True if Mario hit a knee from above + */ + static checkKneeCollision(mario, boss) { + if (!boss || !boss.active) return false; + + // Check if Mario is jumping down onto knees + const isFalling = mario.velocityY > 0; + + // Check left knee + const hitLeftKnee = this._isCollidingRectRect(mario, boss.leftKnee) && isFalling; + + // Check right knee + const hitRightKnee = this._isCollidingRectRect(mario, boss.rightKnee) && isFalling; + + return hitLeftKnee || hitRightKnee; + } + + /** + * Check collision between Mario and boss body (damage to Mario) + * @param {Object} mario - Mario object + * @param {Object} boss - Boss object + * @returns {boolean} - True if Mario touched boss body + */ + static checkBodyCollision(mario, boss) { + if (!boss || !boss.active) return false; + + return this._isCollidingRectRect(mario, boss); + } + + /** + * Rectangle-Rectangle collision detection + */ + static _isCollidingRectRect(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; + } +} + +export default Boss; diff --git a/src/gameHelpers/MarioEducational/enemies/Catapult.js b/src/gameHelpers/MarioEducational/enemies/Catapult.js new file mode 100644 index 0000000..f2376d7 --- /dev/null +++ b/src/gameHelpers/MarioEducational/enemies/Catapult.js @@ -0,0 +1,347 @@ +/** + * Catapult.js + * Enemy that launches boulders and stone rain at Mario + * Catapults appear in level 4+, Onagers (stronger) in level 5+ + */ + +export class Catapult { + /** + * Generate catapults for a level + * @param {Object} level - Level data + * @param {number} levelIndex - Level index + * @param {number} levelWidth - Width of the level + * @param {number} canvasHeight - Canvas height + * @returns {Array} - Array of catapult objects + */ + static generate(level, levelIndex, levelWidth, canvasHeight) { + const catapults = []; + + let catapultCount = 1; // Always 1 catapult for level 4+ + let onagerCount = 0; + + // Level 5+ gets onagers + if (levelIndex >= 4) { + onagerCount = 1; // 1 onager for level 5+ + } + + const totalCount = catapultCount + onagerCount; + console.log(`🏹 Generating ${catapultCount} catapult(s) and ${onagerCount} onager(s) for level ${levelIndex + 1}`); + + for (let i = 0; i < totalCount; i++) { + const isOnager = i >= catapultCount; // Onagers come after catapults + + // Place catapults near END of level + const nearEndX = levelWidth * 0.7; // 70% through level + const catapultX = nearEndX + (i * 300) + Math.random() * 200; + let catapultY = canvasHeight - 100; // Default: on background ground + + // Check if there's a platform, wall, or stair above this position + const platformAbove = this._findPlatformAbove(catapultX, catapultY, level.platforms || []); + const wallAbove = this._findWallAbove(catapultX, catapultY, level.walls || []); + const stairAbove = this._findStairAbove(catapultX, catapultY, level.stairs || []); + + // Choose the lowest obstacle (closest to ground = highest Y value) + const obstacles = [platformAbove, wallAbove, stairAbove].filter(obs => obs !== null); + + if (obstacles.length > 0) { + const obstacleAbove = obstacles.reduce((lowest, current) => + current.y > lowest.y ? current : lowest + ); + catapultY = obstacleAbove.y - 80; // 80 is catapult height + console.log(`🏹 Catapult moved to obstacle at y=${catapultY.toFixed(0)}`); + } + + catapults.push({ + x: catapultX, + y: catapultY, + width: 60, + height: 80, + color: isOnager ? '#654321' : '#8B4513', + lastShot: 0, + shootCooldown: isOnager ? 6000 + Math.random() * 2000 : 4000 + Math.random() * 2000, + type: isOnager ? 'onager' : 'catapult', + isOnager: isOnager, + armAngle: 0 // For rendering + }); + + console.log(`${isOnager ? '🏛️' : '🏹'} ${isOnager ? 'Onager' : 'Catapult'} placed at x=${catapultX.toFixed(0)}, y=${catapultY.toFixed(0)}`); + } + + return catapults; + } + + /** + * Find platform above a position + */ + static _findPlatformAbove(x, groundY, platforms) { + let bestPlatform = null; + let lowestY = 0; + + platforms.forEach(platform => { + const catapultLeft = x; + const catapultRight = x + 60; + const platformLeft = platform.x; + const platformRight = platform.x + platform.width; + + const hasHorizontalOverlap = catapultLeft < platformRight && catapultRight > platformLeft; + + if (hasHorizontalOverlap && platform.y < groundY && platform.y > lowestY) { + bestPlatform = platform; + lowestY = platform.y; + } + }); + + return bestPlatform; + } + + /** + * Find wall above a position + */ + static _findWallAbove(x, groundY, walls) { + let bestWall = null; + let lowestY = 0; + + walls.forEach(wall => { + const catapultLeft = x; + const catapultRight = x + 60; + const wallLeft = wall.x; + const wallRight = wall.x + wall.width; + + const hasHorizontalOverlap = catapultLeft < wallRight && catapultRight > wallLeft; + + if (hasHorizontalOverlap && wall.y < groundY && wall.y > lowestY) { + bestWall = wall; + lowestY = wall.y; + } + }); + + return bestWall; + } + + /** + * Find stair above a position + */ + static _findStairAbove(x, groundY, stairs) { + let bestStair = null; + let lowestY = 0; + + stairs.forEach(stair => { + const catapultLeft = x; + const catapultRight = x + 60; + const stairLeft = stair.x; + const stairRight = stair.x + stair.width; + + const hasHorizontalOverlap = catapultLeft < stairRight && catapultRight > stairLeft; + + if (hasHorizontalOverlap && stair.y < groundY && stair.y > lowestY) { + bestStair = stair; + lowestY = stair.y; + } + }); + + return bestStair; + } + + /** + * Update all catapults + * @param {Array} catapults - Array of catapults + * @param {Object} mario - Mario object + * @param {Array} boulders - Boulders array + * @param {Array} stones - Stones array (for onagers) + * @param {Function} playSound - Sound callback + */ + static update(catapults, mario, boulders, stones, playSound) { + const currentTime = Date.now(); + + catapults.forEach(catapult => { + // Arm animation + catapult.armAngle = Math.sin(Date.now() / 200) * 0.2; + + // Check if it's time to shoot + if (currentTime - catapult.lastShot > catapult.shootCooldown) { + const distanceToMario = Math.abs(catapult.x - mario.x); + + // Catapult shoots boulders (single target) + if (!catapult.isOnager && distanceToMario < 600) { + // Calculate trajectory to Mario + const dx = mario.x - catapult.x; + const dy = mario.y - catapult.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + const speed = 8; + const velocityX = (dx / distance) * speed; + const velocityY = (dy / distance) * speed; + + boulders.push({ + x: catapult.x + catapult.width / 2, + y: catapult.y, + velocityX: velocityX, + velocityY: velocityY, + radius: 20, + type: 'boulder', + launched: true + }); + + catapult.lastShot = currentTime; + if (playSound) playSound('jump'); // Boulder launch sound + console.log(`🪨 Catapult launched boulder towards Mario!`); + } + // Onager shoots stone rain (area attack) + else if (catapult.isOnager && distanceToMario < 800) { + // Create stone rain above Mario's area + const stoneCount = 8 + Math.floor(Math.random() * 5); // 8-12 stones + + for (let i = 0; i < stoneCount; i++) { + const offsetX = (Math.random() - 0.5) * 400; // Spread 400px around Mario + + stones.push({ + x: mario.x + offsetX, + y: -50 - Math.random() * 100, // Start above screen + velocityX: (Math.random() - 0.5) * 2, + velocityY: 2 + Math.random() * 3, + width: 15 + Math.random() * 10, + height: 15 + Math.random() * 10, + type: 'stone', + rotation: Math.random() * Math.PI * 2 + }); + } + + catapult.lastShot = currentTime; + if (playSound) playSound('enemy_defeat'); // Different sound for stone rain + console.log(`☄️ Onager launched stone rain (${stoneCount} stones)!`); + } + } + }); + } + + /** + * Update boulders + * @param {Array} boulders - Array of boulders + * @param {Object} mario - Mario object + * @param {Array} platforms - Platforms for collision + * @param {Array} walls - Walls for collision + * @param {Function} onImpact - Callback when boulder hits something + * @returns {Array} - Updated boulders array + */ + static updateBoulders(boulders, mario, platforms, walls, onImpact) { + const GRAVITY = 0.3; + const updatedBoulders = []; + + boulders.forEach((boulder, index) => { + // Apply physics + boulder.velocityY += GRAVITY; + boulder.x += boulder.velocityX; + boulder.y += boulder.velocityY; + + // Check collision with platforms + let hitPlatform = false; + platforms.forEach((platform, platformIndex) => { + if (this._isCollidingCircleRect(boulder, platform)) { + hitPlatform = true; + if (onImpact) { + onImpact(boulder, index, boulder.x, boulder.y, platform, platformIndex, 'platform'); + } + } + }); + + // Check collision with walls + let hitWall = false; + walls.forEach((wall, wallIndex) => { + if (this._isCollidingCircleRect(boulder, wall)) { + hitWall = true; + if (onImpact) { + onImpact(boulder, index, boulder.x, boulder.y, wall, wallIndex, 'wall'); + } + } + }); + + // Check collision with Mario + if (this._isCollidingCircleRect(boulder, mario)) { + if (onImpact) { + onImpact(boulder, index, boulder.x, boulder.y, mario, -1, 'mario'); + } + return; // Remove boulder + } + + // Remove if out of bounds or hit something + if (!hitPlatform && !hitWall && boulder.y < 1000) { + updatedBoulders.push(boulder); + } + }); + + return updatedBoulders; + } + + /** + * Update stones (stone rain) + * @param {Array} stones - Array of stones + * @param {Object} mario - Mario object + * @param {Array} platforms - Platforms for collision + * @param {Function} onImpact - Callback when stone hits something + * @returns {Array} - Updated stones array + */ + static updateStones(stones, mario, platforms, onImpact) { + const GRAVITY = 0.5; + const updatedStones = []; + + stones.forEach((stone, index) => { + // Apply physics + stone.velocityY += GRAVITY; + stone.x += stone.velocityX; + stone.y += stone.velocityY; + stone.rotation += 0.1; + + // Check collision with platforms + let hitPlatform = false; + platforms.forEach(platform => { + if (this._isCollidingRectRect(stone, platform)) { + hitPlatform = true; + if (onImpact) { + onImpact(stone, index, 'platform'); + } + } + }); + + // Check collision with Mario + if (this._isCollidingRectRect(stone, mario)) { + if (onImpact) { + onImpact(stone, index, 'mario'); + } + return; // Remove stone + } + + // Keep stone if not hit and still on screen + if (!hitPlatform && stone.y < 1000) { + updatedStones.push(stone); + } + }); + + return updatedStones; + } + + /** + * Circle-Rectangle collision detection + */ + static _isCollidingCircleRect(circle, rect) { + const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width)); + const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height)); + + const distanceX = circle.x - closestX; + const distanceY = circle.y - closestY; + + const distanceSquared = distanceX * distanceX + distanceY * distanceY; + return distanceSquared < (circle.radius * circle.radius); + } + + /** + * Rectangle-Rectangle collision detection + */ + static _isCollidingRectRect(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; + } +} + +export default Catapult; diff --git a/src/gameHelpers/MarioEducational/enemies/FlyingEye.js b/src/gameHelpers/MarioEducational/enemies/FlyingEye.js new file mode 100644 index 0000000..0cbc342 --- /dev/null +++ b/src/gameHelpers/MarioEducational/enemies/FlyingEye.js @@ -0,0 +1,187 @@ +/** + * FlyingEye.js + * Flying enemy that chases Mario and performs dash attacks + * Appears in level 5+ + */ + +export class FlyingEye { + /** + * Generate flying eyes for a level + * @param {Object} level - Level data + * @param {number} difficulty - Difficulty level + * @returns {Array} - Array of flying eye objects + */ + static generate(level, difficulty) { + const eyes = []; + const eyeCount = Math.min(4, Math.max(3, difficulty - 2)); // 3-4 flying eyes + console.log(`👁️ Generating ${eyeCount} flying eyes for level 5+`); + + for (let i = 0; i < eyeCount; i++) { + // Eyes spawn in the middle-upper area of the level + const eyeX = 300 + (i * 400) + Math.random() * 200; // Spread across level + const eyeY = 100 + Math.random() * 150; // Upper area of screen + + eyes.push({ + x: eyeX, + y: eyeY, + width: 30, + height: 30, + velocityX: (Math.random() - 0.5) * 2, // Random horizontal drift -1 to +1 + velocityY: (Math.random() - 0.5) * 2, // Random vertical drift -1 to +1 + color: '#DC143C', // Crimson red + pupilColor: '#000000', // Black pupil + type: 'flying_eye', + health: 1, + maxHealth: 1, + // AI behavior properties + chaseDistance: 200, // Start chasing Mario within 200px + chaseSpeed: 3.5, // Faster chase speed + idleSpeed: 1.2, // Faster idle movement + lastDirectionChange: Date.now(), + directionChangeInterval: 2000 + Math.random() * 3000, // Change direction every 2-5 seconds + isChasing: false, + // Dash behavior + dashCooldown: 0, + dashDuration: 0, + isDashing: false, + dashSpeed: 8, // Very fast dash + lastDashTime: Date.now(), + dashInterval: 3000 + Math.random() * 2000, // Dash every 3-5 seconds + // Visual properties + blinkTimer: 0, + isBlinking: false + }); + + console.log(`👁️ Flying eye ${i + 1} placed at x=${eyeX.toFixed(0)}, y=${eyeY.toFixed(0)}`); + } + + return eyes; + } + + /** + * Update all flying eyes + * @param {Array} eyes - Array of flying eyes + * @param {Object} mario - Mario object + * @param {Function} playSound - Sound callback + */ + static update(eyes, mario, playSound) { + const currentTime = Date.now(); + + eyes.forEach(eye => { + const distanceToMario = Math.sqrt( + Math.pow(eye.x - mario.x, 2) + Math.pow(eye.y - mario.y, 2) + ); + + // Blinking animation + eye.blinkTimer++; + if (eye.blinkTimer > 120) { + eye.isBlinking = true; + } + if (eye.blinkTimer > 125) { + eye.isBlinking = false; + eye.blinkTimer = 0; + } + + // Check if should chase Mario + eye.isChasing = distanceToMario < eye.chaseDistance; + + // Dash behavior + if (eye.isDashing) { + eye.dashDuration--; + if (eye.dashDuration <= 0) { + eye.isDashing = false; + eye.dashCooldown = 60; // Cooldown frames after dash + } + } else if (eye.dashCooldown > 0) { + eye.dashCooldown--; + } else if (eye.isChasing && currentTime - eye.lastDashTime > eye.dashInterval) { + // Start dash towards Mario + eye.isDashing = true; + eye.dashDuration = 30; // 30 frames of dash + eye.lastDashTime = currentTime; + + // Set dash velocity towards Mario + const dx = mario.x - eye.x; + const dy = mario.y - eye.y; + const distance = Math.sqrt(dx * dx + dy * dy); + eye.velocityX = (dx / distance) * eye.dashSpeed; + eye.velocityY = (dy / distance) * eye.dashSpeed; + + console.log(`👁️ Flying eye dashes towards Mario!`); + } + + // Movement behavior + if (eye.isDashing) { + // Continue dash movement + eye.x += eye.velocityX; + eye.y += eye.velocityY; + } else if (eye.isChasing) { + // Chase Mario + const dx = mario.x - eye.x; + const dy = mario.y - eye.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + eye.velocityX = (dx / distance) * eye.chaseSpeed; + eye.velocityY = (dy / distance) * eye.chaseSpeed; + + eye.x += eye.velocityX; + eye.y += eye.velocityY; + } else { + // Idle wandering + if (currentTime - eye.lastDirectionChange > eye.directionChangeInterval) { + eye.velocityX = (Math.random() - 0.5) * eye.idleSpeed * 2; + eye.velocityY = (Math.random() - 0.5) * eye.idleSpeed * 2; + eye.lastDirectionChange = currentTime; + } + + eye.x += eye.velocityX; + eye.y += eye.velocityY; + } + + // Keep eyes within bounds (with some margin) + if (eye.x < 50) { + eye.x = 50; + eye.velocityX = Math.abs(eye.velocityX); + } + if (eye.y < 50) { + eye.y = 50; + eye.velocityY = Math.abs(eye.velocityY); + } + if (eye.y > 400) { + eye.y = 400; + eye.velocityY = -Math.abs(eye.velocityY); + } + }); + } + + /** + * Check collision between Mario and flying eyes + * @param {Object} mario - Mario object + * @param {Array} eyes - Array of flying eyes + * @returns {Object|null} - Colliding eye or null + */ + static checkCollision(mario, eyes) { + for (const eye of eyes) { + // Simple rectangle collision + if (mario.x < eye.x + eye.width && + mario.x + mario.width > eye.x && + mario.y < eye.y + eye.height && + mario.y + mario.height > eye.y) { + return eye; + } + } + return null; + } + + /** + * Damage a flying eye + * @param {Object} eye - Flying eye to damage + * @returns {boolean} - True if eye was killed + */ + static damage(eye) { + eye.health--; + return eye.health <= 0; + } +} + +export default FlyingEye; diff --git a/src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js b/src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js new file mode 100644 index 0000000..d3c8a77 --- /dev/null +++ b/src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js @@ -0,0 +1,133 @@ +/** + * PiranhaPlant.js + * Enemy that shoots fireballs at Mario when in range + * Appears starting from level 3+ + */ + +export class PiranhaPlant { + /** + * Generate piranha plants for a level + * @param {Object} level - Level data + * @param {number} difficulty - Difficulty level (1-5) + * @returns {Array} - Array of piranha plant objects + */ + static generate(level, difficulty) { + const plants = []; + const plantCount = Math.min(difficulty - 2, 2); // 0-2 plants for level 3+ + + if (plantCount <= 0) return plants; + + for (let i = 0; i < plantCount; i++) { + // Find a suitable ground platform for the plant + const groundPlatforms = level.platforms.filter(p => p.type === 'ground'); + if (groundPlatforms.length === 0) continue; + + const platform = groundPlatforms[Math.floor(Math.random() * groundPlatforms.length)]; + const plantX = platform.x + Math.random() * (platform.width - 30); + + plants.push({ + x: plantX, + y: platform.y - 40, // Plant height above platform + width: 30, + height: 40, + color: '#228B22', // Forest green + lastShot: 0, + shootCooldown: 2000 + Math.random() * 1000, // 2-3 second intervals + type: 'piranha', + visible: true, + extended: 0, // For animation (how much plant extends from pipe) + maxExtension: 40, + extending: true + }); + + console.log(`🌸 Piranha plant placed at x=${plantX.toFixed(0)}`); + } + + return plants; + } + + /** + * Update all piranha plants + * @param {Array} plants - Array of piranha plants + * @param {Object} mario - Mario object + * @param {Array} projectiles - Projectiles array to add new projectiles + * @param {Function} playSound - Sound callback + */ + static update(plants, mario, projectiles, playSound) { + const currentTime = Date.now(); + + plants.forEach(plant => { + // Animate plant extension/retraction + if (plant.extending) { + plant.extended = Math.min(plant.extended + 1, plant.maxExtension); + if (plant.extended >= plant.maxExtension) { + plant.extending = false; + } + } else { + plant.extended = Math.max(plant.extended - 1, 0); + if (plant.extended <= 0) { + plant.extending = true; + } + } + + // Check if it's time to shoot + if (currentTime - plant.lastShot > plant.shootCooldown) { + // Check if Mario is in range (within 400 pixels) + const distanceToMario = Math.abs(plant.x - mario.x); + + if (distanceToMario < 400) { + // Shoot projectile towards Mario + const direction = mario.x > plant.x ? 1 : -1; + + projectiles.push({ + x: plant.x + plant.width / 2, + y: plant.y + plant.height / 2, + velocityX: direction * 3, // Projectile speed + velocityY: 0, + radius: 8, + color: '#FF4500', // Orange fireball + type: 'fireball', + life: 200 // 200 frames lifetime + }); + + plant.lastShot = currentTime; + if (playSound) playSound('enemy_defeat'); // Shooting sound + console.log(`🔥 Piranha plant shot fireball towards Mario!`); + } + } + }); + } + + /** + * Check collision between Mario and piranha plants + * @param {Object} mario - Mario object + * @param {Array} plants - Array of piranha plants + * @returns {Object|null} - Colliding plant or null + */ + static checkCollision(mario, plants) { + for (const plant of plants) { + if (!plant.visible) continue; + + // Only check collision when plant is extended + if (plant.extended > 20) { + const headY = plant.y - plant.extended; + const headRadius = 30; + + // Simple circle-rectangle collision + const closestX = Math.max(mario.x, Math.min(plant.x + plant.width / 2, mario.x + mario.width)); + const closestY = Math.max(mario.y, Math.min(headY, mario.y + mario.height)); + + const distanceX = (plant.x + plant.width / 2) - closestX; + const distanceY = headY - closestY; + const distanceSquared = distanceX * distanceX + distanceY * distanceY; + + if (distanceSquared < headRadius * headRadius) { + return plant; + } + } + } + return null; + } +} + +export default PiranhaPlant; diff --git a/src/gameHelpers/MarioEducational/enemies/Projectile.js b/src/gameHelpers/MarioEducational/enemies/Projectile.js new file mode 100644 index 0000000..233cb1a --- /dev/null +++ b/src/gameHelpers/MarioEducational/enemies/Projectile.js @@ -0,0 +1,147 @@ +/** + * Projectile.js + * Helper for managing projectiles (fireballs, boss shots, etc.) + * Handles movement, collision, and lifetime + */ + +export class Projectile { + /** + * Update all projectiles + * @param {Array} projectiles - Array of projectiles + * @param {Object} mario - Mario object + * @param {Array} platforms - Platforms for collision + * @param {Array} walls - Walls for collision + * @param {number} levelWidth - Level width for bounds checking + * @param {Function} onMarioHit - Callback when Mario is hit + * @param {Function} onObstacleHit - Callback when projectile hits obstacle + * @returns {Array} - Updated projectiles array + */ + static update(projectiles, mario, platforms, walls, levelWidth, onMarioHit, onObstacleHit) { + const updatedProjectiles = []; + + projectiles.forEach((projectile, index) => { + // Update position + projectile.x += projectile.velocityX; + projectile.y += projectile.velocityY; + projectile.life--; + + // Remove projectiles that are off-screen or expired + if (projectile.life <= 0 || projectile.x < -50 || projectile.x > levelWidth + 50) { + return; // Don't add to updated array (remove) + } + + // Check collision with Mario + if (this._isCollidingCircleRect(projectile, mario)) { + if (onMarioHit) { + onMarioHit(projectile); + } + return; // Remove projectile + } + + // Check collision with walls/platforms + const hitPlatform = platforms.some(platform => + this._isCollidingCircleRect(projectile, platform) + ); + const hitWall = walls.some(wall => + this._isCollidingCircleRect(projectile, wall) + ); + + if (hitPlatform || hitWall) { + if (onObstacleHit) { + onObstacleHit(projectile, index, projectile.x, projectile.y); + } + return; // Remove projectile + } + + // Keep projectile if no collision + updatedProjectiles.push(projectile); + }); + + return updatedProjectiles; + } + + /** + * Create a new projectile + * @param {number} x - Start X position + * @param {number} y - Start Y position + * @param {number} velocityX - Horizontal velocity + * @param {number} velocityY - Vertical velocity + * @param {Object} options - Additional options (radius, color, type, life) + * @returns {Object} - Projectile object + */ + static create(x, y, velocityX, velocityY, options = {}) { + return { + x: x, + y: y, + velocityX: velocityX, + velocityY: velocityY, + radius: options.radius || 8, + color: options.color || '#FF4500', + type: options.type || 'projectile', + life: options.life || 200 + }; + } + + /** + * Create a projectile aimed at a target + * @param {number} fromX - Start X + * @param {number} fromY - Start Y + * @param {number} toX - Target X + * @param {number} toY - Target Y + * @param {number} speed - Projectile speed + * @param {Object} options - Additional options + * @returns {Object} - Projectile object + */ + static createAimed(fromX, fromY, toX, toY, speed, options = {}) { + const dx = toX - fromX; + const dy = toY - fromY; + const distance = Math.sqrt(dx * dx + dy * dy); + + const velocityX = (dx / distance) * speed; + const velocityY = (dy / distance) * speed; + + return this.create(fromX, fromY, velocityX, velocityY, options); + } + + /** + * Circle-Rectangle collision detection + * @param {Object} circle - Circle object with x, y, radius + * @param {Object} rect - Rectangle object with x, y, width, height + * @returns {boolean} - True if colliding + */ + static _isCollidingCircleRect(circle, rect) { + const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width)); + const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height)); + + const distanceX = circle.x - closestX; + const distanceY = circle.y - closestY; + + const distanceSquared = distanceX * distanceX + distanceY * distanceY; + return distanceSquared < (circle.radius * circle.radius); + } + + /** + * Check if a projectile is out of bounds + * @param {Object} projectile - Projectile to check + * @param {number} levelWidth - Level width + * @param {number} canvasHeight - Canvas height + * @returns {boolean} - True if out of bounds + */ + static isOutOfBounds(projectile, levelWidth, canvasHeight) { + return projectile.x < -50 || + projectile.x > levelWidth + 50 || + projectile.y < -50 || + projectile.y > canvasHeight + 50; + } + + /** + * Check if projectile has expired + * @param {Object} projectile - Projectile to check + * @returns {boolean} - True if expired + */ + static isExpired(projectile) { + return projectile.life <= 0; + } +} + +export default Projectile; diff --git a/src/games/MarioEducational.js b/src/games/MarioEducational.js index ba69cc5..f4e4364 100644 --- a/src/games/MarioEducational.js +++ b/src/games/MarioEducational.js @@ -1,4 +1,13 @@ import Module from '../core/Module.js'; +import { sentenceGenerator } from '../gameHelpers/MarioEducational/SentenceGenerator.js'; +import { soundSystem } from '../gameHelpers/MarioEducational/SoundSystem.js'; +import { renderer } from '../gameHelpers/MarioEducational/Renderer.js'; +import PhysicsEngine from '../gameHelpers/MarioEducational/PhysicsEngine.js'; +import PiranhaPlant from '../gameHelpers/MarioEducational/enemies/PiranhaPlant.js'; +import Catapult from '../gameHelpers/MarioEducational/enemies/Catapult.js'; +import FlyingEye from '../gameHelpers/MarioEducational/enemies/FlyingEye.js'; +import Boss from '../gameHelpers/MarioEducational/enemies/Boss.js'; +import Projectile from '../gameHelpers/MarioEducational/enemies/Projectile.js'; /** * MarioEducational - 2D Mario-style educational game with question blocks @@ -106,10 +115,6 @@ class MarioEducational extends Module { // UI elements (need to be declared before seal) this._uiOverlay = null; - // Sound system - this._audioContext = null; - this._sounds = {}; - Object.seal(this); } @@ -217,7 +222,7 @@ class MarioEducational extends Module { this._setupInputHandlers(); // Initialize sound system - this._initializeSoundSystem(); + soundSystem.initialize(); // Generate all levels this._generateAllLevels(); @@ -305,7 +310,7 @@ class MarioEducational extends Module { // Extract sentences from story text if (story && typeof story === 'string') { - const storySentences = this._splitTextIntoSentences(story); + const storySentences = sentenceGenerator.splitTextIntoSentences(story); storySentences.forEach(sentence => { this._sentences.push({ type: 'story', @@ -320,7 +325,7 @@ class MarioEducational extends Module { if (Array.isArray(texts)) { texts.forEach((text, index) => { if (typeof text === 'string') { - const textSentences = this._splitTextIntoSentences(text); + const textSentences = sentenceGenerator.splitTextIntoSentences(text); textSentences.forEach(sentence => { this._sentences.push({ type: 'text', @@ -330,7 +335,7 @@ class MarioEducational extends Module { }); }); } else if (text.content) { - const textSentences = this._splitTextIntoSentences(text.content); + const textSentences = sentenceGenerator.splitTextIntoSentences(text.content); textSentences.forEach(sentence => { this._sentences.push({ type: 'text', @@ -346,12 +351,14 @@ class MarioEducational extends Module { // Add vocabulary as contextual sentences Object.entries(vocab).forEach(([word, data]) => { if (data.user_language) { - const generatedSentence = this._generateSentenceFromWord(word, data); + const generatedSentence = sentenceGenerator.generateSentence(word, data); this._sentences.push({ type: 'vocabulary', english: generatedSentence.english, translation: generatedSentence.translation, - context: data.type || 'vocabulary' + context: data.type || 'vocabulary', + difficulty: generatedSentence.difficulty, + wordType: generatedSentence.wordType }); } }); @@ -362,93 +369,6 @@ class MarioEducational extends Module { console.log(`📝 Extracted ${this._sentences.length} sentences/vocabulary for questions`); } - /** - * Generate contextual sentences from vocabulary words - * @param {string} word - The vocabulary word - * @param {Object} data - Word data including type and translation - * @returns {Object} Generated sentence with English and translation - */ - /** - * Split long text into individual sentences - * @param {string} text - Long text to split - * @returns {Array} Array of sentences - */ - _splitTextIntoSentences(text) { - if (!text || typeof text !== 'string') return []; - - // Clean the text - const cleanText = text.trim(); - - // Split by sentence-ending punctuation - const sentences = cleanText - .split(/[.!?]+/) - .map(s => s.trim()) - .filter(s => s.length > 0) - .map(s => { - // Add period if sentence doesn't end with punctuation - if (!s.match(/[.!?]$/)) { - s += '.'; - } - // Capitalize first letter - return s.charAt(0).toUpperCase() + s.slice(1); - }); - - // Filter out very short sentences (less than 3 words) - return sentences.filter(sentence => sentence.split(' ').length >= 3); - } - - _generateSentenceFromWord(word, data) { - const type = data.type || 'noun'; - const translation = data.user_language.split(';')[0]; // Use first translation - - // Simple sentence templates based on word type - const templates = { - 'noun': [ - `This is a ${word}.`, - `I see a ${word}.`, - `The ${word} is here.`, - `Where is the ${word}?`, - `I need a ${word}.` - ], - 'adjective': [ - `The house is ${word}.`, - `This looks ${word}.`, - `It seems ${word}.`, - `How ${word} it is!`, - `The weather is ${word}.` - ], - 'verb': [ - `I ${word} every day.`, - `Please ${word} this.`, - `Don't ${word} too fast.`, - `Can you ${word}?`, - `Let's ${word} together.` - ], - 'adverb': [ - `He walks ${word}.`, - `She speaks ${word}.`, - `They work ${word}.`, - `Do it ${word}.`, - `Move ${word}.` - ], - 'preposition': [ - `The book is ${word} the table.`, - `Walk ${word} the street.`, - `It's ${word} the house.`, - `Look ${word} the window.`, - `Go ${word} the door.` - ] - }; - - // Get templates for this word type, fallback to noun if type unknown - const typeTemplates = templates[type] || templates['noun']; - const randomTemplate = typeTemplates[Math.floor(Math.random() * typeTemplates.length)]; - - return { - english: randomTemplate, - translation: `${translation} - ${randomTemplate.replace(word, `**${word}**`)}` - }; - } _setupCanvas() { this._canvas = document.createElement('canvas'); @@ -482,120 +402,6 @@ class MarioEducational extends Module { document.addEventListener('keyup', this._handleKeyUp); } - _initializeSoundSystem() { - try { - // Initialize Web Audio Context - this._audioContext = new (window.AudioContext || window.webkitAudioContext)(); - console.log('🔊 Sound system initialized'); - - // Create sound library - this._createSoundLibrary(); - } catch (error) { - console.warn('⚠️ Sound system not available:', error); - this._audioContext = null; - } - } - - _createSoundLibrary() { - // Sound definitions with parameters for programmatic generation - this._sounds = { - jump: { type: 'sweep', frequency: 330, endFrequency: 600, duration: 0.1 }, - coin: { type: 'bell', frequency: 800, duration: 0.3 }, - powerup: { type: 'arpeggio', frequencies: [264, 330, 396, 528], duration: 0.6 }, - enemy_defeat: { type: 'noise_sweep', frequency: 200, endFrequency: 50, duration: 0.2 }, - question_block: { type: 'sparkle', frequency: 600, endFrequency: 1200, duration: 0.4 }, - level_complete: { type: 'victory', frequencies: [523, 659, 784, 1047], duration: 1.0 }, - death: { type: 'descend', frequency: 300, endFrequency: 100, duration: 0.8 }, - finish_stars: { type: 'magical', frequencies: [880, 1100, 1320, 1760], duration: 2.0 } - }; - - console.log('🎵 Sound library created with', Object.keys(this._sounds).length, 'sounds'); - } - - _playSound(soundName, volume = 0.3) { - if (!this._audioContext || !this._sounds[soundName]) { - return; - } - - try { - const sound = this._sounds[soundName]; - const oscillator = this._audioContext.createOscillator(); - const gainNode = this._audioContext.createGain(); - - oscillator.connect(gainNode); - gainNode.connect(this._audioContext.destination); - - const currentTime = this._audioContext.currentTime; - const duration = sound.duration; - - // Set volume - gainNode.gain.setValueAtTime(0, currentTime); - gainNode.gain.linearRampToValueAtTime(volume, currentTime + 0.01); - gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + duration); - - // Configure sound based on type - switch (sound.type) { - case 'sweep': - oscillator.type = 'square'; - oscillator.frequency.setValueAtTime(sound.frequency, currentTime); - oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration); - break; - - case 'bell': - oscillator.type = 'sine'; - oscillator.frequency.setValueAtTime(sound.frequency, currentTime); - oscillator.frequency.exponentialRampToValueAtTime(sound.frequency * 0.5, currentTime + duration); - break; - - case 'noise_sweep': - oscillator.type = 'sawtooth'; - oscillator.frequency.setValueAtTime(sound.frequency, currentTime); - oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration); - break; - - case 'sparkle': - oscillator.type = 'triangle'; - oscillator.frequency.setValueAtTime(sound.frequency, currentTime); - oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration * 0.7); - oscillator.frequency.linearRampToValueAtTime(sound.frequency, currentTime + duration); - break; - - case 'descend': - oscillator.type = 'square'; - oscillator.frequency.setValueAtTime(sound.frequency, currentTime); - oscillator.frequency.exponentialRampToValueAtTime(sound.endFrequency, currentTime + duration); - break; - - case 'arpeggio': - case 'victory': - case 'magical': - // For complex sounds, play the first frequency and schedule others - oscillator.type = sound.type === 'magical' ? 'triangle' : 'square'; - oscillator.frequency.setValueAtTime(sound.frequencies[0], currentTime); - - // Schedule frequency changes for arpeggio effect - const noteLength = duration / sound.frequencies.length; - sound.frequencies.forEach((freq, index) => { - if (index > 0) { - oscillator.frequency.setValueAtTime(freq, currentTime + noteLength * index); - } - }); - break; - - default: - oscillator.type = 'square'; - oscillator.frequency.setValueAtTime(sound.frequency || 440, currentTime); - } - - oscillator.start(currentTime); - oscillator.stop(currentTime + duration); - - console.log(`🎵 Playing sound: ${soundName}`); - - } catch (error) { - console.warn('⚠️ Failed to play sound:', soundName, error); - } - } _generateAllLevels() { this._levelData = []; @@ -1341,33 +1147,14 @@ class MarioEducational extends Module { } _generatePiranhaPlants(level, difficulty) { - const plantCount = Math.min(difficulty - 2, 2); // 0-2 plants for level 3+ - - for (let i = 0; i < plantCount; i++) { - // Find a suitable ground platform for the plant - const groundPlatforms = level.platforms.filter(p => p.type === 'ground'); - if (groundPlatforms.length === 0) continue; - - const platform = groundPlatforms[Math.floor(Math.random() * groundPlatforms.length)]; - const plantX = platform.x + Math.random() * (platform.width - 30); - - level.piranhaPlants = level.piranhaPlants || []; - level.piranhaPlants.push({ - x: plantX, - y: platform.y - 40, // Plant height - width: 30, - height: 40, - color: '#228B22', // Forest green - lastShot: 0, - shootCooldown: 2000 + Math.random() * 1000, // 2-3 second intervals - type: 'piranha' - }); - - console.log(`🌸 Piranha plant placed at x=${plantX.toFixed(0)}`); - } + level.piranhaPlants = PiranhaPlant.generate(level, difficulty); } _generateCatapults(level, difficulty) { + level.catapults = Catapult.generate(level, level.index, this._levelWidth, this._config.canvasHeight); + } + + _generateCatapultsOLD(level, difficulty) { const { index } = level; let catapultCount = 1; // Always 1 catapult for level 4+ @@ -1842,326 +1629,45 @@ class MarioEducational extends Module { } _updateMarioMovement() { - // Don't update movement during celebration - if (this._isCelebrating) return; - - // Horizontal movement - if (this._keys['ArrowLeft'] || this._keys['KeyA']) { - this._mario.velocityX = -this._config.moveSpeed; - this._mario.facing = 'left'; - } else if (this._keys['ArrowRight'] || this._keys['KeyD']) { - this._mario.velocityX = this._config.moveSpeed; - this._mario.facing = 'right'; - } else { - this._mario.velocityX *= 0.8; // Friction - } - - // Jumping - if ((this._keys['ArrowUp'] || this._keys['KeyW'] || this._keys['Space']) && this._mario.onGround) { - this._mario.velocityY = this._config.jumpForce; - this._mario.onGround = false; - this._playSound('jump'); - } + PhysicsEngine.updateMarioMovement(this._mario, this._keys, this._config, this._isCelebrating, (sound) => soundSystem.play(sound)); } _updateMarioPhysics() { - // Apply gravity - this._mario.velocityY += this._config.gravity; - - // Update position - this._mario.x += this._mario.velocityX; - this._mario.y += this._mario.velocityY; - - // Prevent going off left edge - if (this._mario.x < 0) { - this._mario.x = 0; - } - - // Stop Mario at finish line during celebration const level = this._levelData[this._currentLevelIndex]; - if (this._mario.x > level.endX && this._levelCompleted) { - this._mario.x = level.endX; - this._mario.velocityX = 0; - } - - // Check if Mario fell off the world - if (this._mario.y > this._config.canvasHeight + 100) { - this._restartLevel(); - } + PhysicsEngine.updateMarioPhysics(this._mario, this._config, level, this._levelCompleted, () => this._restartLevel()); } _updateEnemies() { - // Don't update enemies during celebration - if (this._isCelebrating) return; - - this._enemies.forEach(enemy => { - // Store old position for collision detection - const oldX = enemy.x; - enemy.x += enemy.velocityX; - - // Check wall collisions - const hitWall = this._walls.some(wall => { - return enemy.x < wall.x + wall.width && - enemy.x + enemy.width > wall.x && - enemy.y < wall.y + wall.height && - enemy.y + enemy.height > wall.y; - }); - - if (hitWall) { - // Reverse position and direction - enemy.x = oldX; - enemy.velocityX *= -1; - console.log(`🧱 Enemy hit wall, reversing direction`); - } - - // Simple AI: reverse direction at platform edges - const platform = this._platforms.find(p => - enemy.x >= p.x - 10 && enemy.x <= p.x + p.width + 10 && - enemy.y >= p.y - enemy.height - 5 && enemy.y <= p.y + 5 - ); - - if (!platform || enemy.x <= 0 || enemy.x >= this._levelWidth) { - enemy.velocityX *= -1; - } - }); + PhysicsEngine.updateEnemies(this._enemies, this._walls, this._platforms, this._levelWidth, this._isCelebrating); } _checkCollisions() { - // Platform collisions - this._mario.onGround = false; + const gameState = { + platforms: this._platforms, + questionBlocks: this._questionBlocks, + enemies: this._enemies, + walls: this._walls, + catapults: this._catapults, + piranhaPlants: this._piranhaPlants, + boulders: this._boulders + }; - this._platforms.forEach(platform => { - if (this._isColliding(this._mario, platform)) { - // Calculate overlap amounts to determine collision direction - const overlapLeft = (this._mario.x + this._mario.width) - platform.x; - const overlapRight = (platform.x + platform.width) - this._mario.x; - const overlapTop = (this._mario.y + this._mario.height) - platform.y; - const overlapBottom = (platform.y + platform.height) - this._mario.y; + const callbacks = { + onQuestionBlock: (block) => this._hitQuestionBlock(block), + onEnemyDefeat: (index) => { + this._score += 50; + this._enemies.splice(index, 1); + }, + onMarioDeath: () => this._restartLevel(), + onAddParticles: (x, y, color) => this._addParticles(x, y, color) + }; - // Find the smallest overlap to determine collision side - const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom); - - if (minOverlap === overlapTop && this._mario.velocityY > 0) { - // Landing on top of platform - this._mario.y = platform.y - this._mario.height; - this._mario.velocityY = 0; - this._mario.onGround = true; - } - else if (minOverlap === overlapBottom && this._mario.velocityY < 0) { - // Hitting platform from below - this._mario.y = platform.y + platform.height; - this._mario.velocityY = 0; - } - else if (minOverlap === overlapLeft && this._mario.velocityX > 0) { - // Hitting platform from left - this._mario.x = platform.x - this._mario.width; - this._mario.velocityX = 0; - } - else if (minOverlap === overlapRight && this._mario.velocityX < 0) { - // Hitting platform from right - this._mario.x = platform.x + platform.width; - this._mario.velocityX = 0; - } - } - }); - - // Boulder platform collisions (landed boulders act as platforms) - this._boulders.forEach(boulder => { - if (boulder.hasLanded && this._isColliding(this._mario, boulder)) { - // Same collision logic as platforms - const overlapLeft = (this._mario.x + this._mario.width) - boulder.x; - const overlapRight = (boulder.x + boulder.width) - this._mario.x; - const overlapTop = (this._mario.y + this._mario.height) - boulder.y; - const overlapBottom = (boulder.y + boulder.height) - this._mario.y; - const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom); - - if (minOverlap === overlapTop && this._mario.velocityY > 0) { - // Landing on top of boulder - this._mario.y = boulder.y - this._mario.height; - this._mario.velocityY = 0; - this._mario.onGround = true; - } - else if (minOverlap === overlapBottom && this._mario.velocityY < 0) { - // Hitting boulder from below - this._mario.y = boulder.y + boulder.height; - this._mario.velocityY = 0; - } - else if (minOverlap === overlapLeft && this._mario.velocityX > 0) { - // Hitting boulder from left - this._mario.x = boulder.x - this._mario.width; - this._mario.velocityX = 0; - } - else if (minOverlap === overlapRight && this._mario.velocityX < 0) { - // Hitting boulder from right - this._mario.x = boulder.x + boulder.width; - this._mario.velocityX = 0; - } - } - }); - - // Question block collisions - this._questionBlocks.forEach(block => { - if (!block.hit && this._isColliding(this._mario, block)) { - // Hit from below (jumping into block) - if (this._mario.velocityY < 0 && - this._mario.y > block.y + block.height / 2) { - this._hitQuestionBlock(block); - } - // Touch from side or top - else { - this._hitQuestionBlock(block); - } - } - }); - - // Wall collisions - this._walls.forEach(wall => { - if (this._isColliding(this._mario, wall)) { - // Calculate overlap amounts to determine collision direction - const overlapLeft = (this._mario.x + this._mario.width) - wall.x; - const overlapRight = (wall.x + wall.width) - this._mario.x; - const overlapTop = (this._mario.y + this._mario.height) - wall.y; - const overlapBottom = (wall.y + wall.height) - this._mario.y; - - // Find the smallest overlap to determine collision side - const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom); - - if (minOverlap === overlapTop && this._mario.velocityY > 0) { - // Landing on top of wall - this._mario.y = wall.y - this._mario.height; - this._mario.velocityY = 0; - this._mario.onGround = true; - } - else if (minOverlap === overlapBottom && this._mario.velocityY < 0) { - // Hitting wall from below - this._mario.y = wall.y + wall.height; - this._mario.velocityY = 0; - } - else if (minOverlap === overlapLeft && this._mario.velocityX > 0) { - // Hitting wall from left - this._mario.x = wall.x - this._mario.width; - this._mario.velocityX = 0; - } - else if (minOverlap === overlapRight && this._mario.velocityX < 0) { - // Hitting wall from right - this._mario.x = wall.x + wall.width; - this._mario.velocityX = 0; - } - } - }); - - // Catapult collisions (Mario destroys catapults!) - this._catapults.forEach((catapult, index) => { - if (this._isColliding(this._mario, catapult)) { - console.log(`💥 Mario destroyed catapult! Explosion!`); - - // Create massive explosion effect at catapult center - const explosionX = catapult.x + catapult.width / 2; - const explosionY = catapult.y + catapult.height / 2; - - // Generate lots of particles flying in all directions - for (let i = 0; i < 25; i++) { - const angle = (Math.PI * 2 * i) / 25; // Spread particles in circle - const speed = 3 + Math.random() * 4; // Random speed 3-7 - const particleColor = ['#8B4513', '#654321', '#D2691E', '#FF4500', '#FFD700'][Math.floor(Math.random() * 5)]; - - this._particles.push({ - x: explosionX, - y: explosionY, - velocityX: Math.cos(angle) * speed, - velocityY: Math.sin(angle) * speed, - color: particleColor, - life: 60 + Math.random() * 30, // Live 60-90 frames - maxLife: 60 + Math.random() * 30, - size: 3 + Math.random() * 4 // Size 3-7 - }); - } - - // Remove the destroyed catapult - this._catapults.splice(index, 1); - - // Play destruction sound and bounce Mario up slightly - this._playSound('enemy_defeat'); - this._mario.velocityY = this._config.jumpForce * 0.5; // Small bounce from impact - - console.log(`🏹 Catapult destroyed! ${this._catapults.length} catapults remaining.`); - } - }); - - // Enemy collisions - this._enemies.forEach((enemy, index) => { - if (this._isColliding(this._mario, enemy)) { - // Calculate overlap to determine if Mario is stomping enemy - const overlapTop = (this._mario.y + this._mario.height) - enemy.y; - const overlapBottom = (enemy.y + enemy.height) - this._mario.y; - - // Stomp enemy if Mario is coming from above - if (this._mario.velocityY > 0 && overlapTop < overlapBottom) { - if (enemy.hasHelmet) { - // Can't stomp helmet enemies! Mario bounces off - this._mario.velocityY = this._config.jumpForce * 0.7; // Big bounce back - this._addParticles(enemy.x, enemy.y, '#C0C0C0'); // Silver particles - this._playSound('jump'); // Bounce sound - console.log(`🛡️ Mario bounced off helmet enemy!`); - } else { - // Normal enemy - can be stomped - this._enemies.splice(index, 1); - this._mario.velocityY = this._config.jumpForce / 2; // Small bounce - // NO SCORE for killing enemies anymore! - this._addParticles(enemy.x, enemy.y, '#FFD700'); - this._playSound('enemy_defeat'); - console.log(`🦶 Mario stomped normal enemy! (No score)`); - } - } else { - // Mario gets hurt by side/bottom collision - console.log(`💥 Mario hit by enemy - restarting level`); - this._restartLevel(); - } - } - }); - - // Check piranha plant collisions (skip flattened plants) - this._piranhaPlants.forEach((plant, index) => { - if (!plant.flattened && this._isColliding(this._mario, plant)) { - // Calculate overlap to determine if Mario is stomping plant - const overlapTop = (this._mario.y + this._mario.height) - plant.y; - const overlapBottom = (plant.y + plant.height) - this._mario.y; - - // Stomp plant if Mario is coming from above - if (this._mario.velocityY > 0 && overlapTop < overlapBottom) { - // Flatten the plant instead of removing it - plant.flattened = true; - plant.height = 5; // Very flat - plant.y += 35; // Move to ground level (original height was 40, new is 5) - plant.shootCooldown = Infinity; // Stop shooting - this._mario.velocityY = this._config.jumpForce / 2; // Small bounce - this._addParticles(plant.x, plant.y, '#228B22'); // Green particles - this._playSound('enemy_defeat'); - console.log(`🌸 Mario flattened piranha plant!`); - } else { - // Mario gets hurt by side collision - console.log(`💥 Mario hit by piranha plant - restarting level`); - this._restartLevel(); - } - } - }); - - // Check walking on flattened plants (for particles) - this._piranhaPlants.forEach((plant, index) => { - if (plant.flattened && this._isColliding(this._mario, plant)) { - // Mario is standing on a flattened plant - add particles occasionally - if (Math.random() < 0.1) { // 10% chance per frame for particles - this._addSmallParticles(plant.x + Math.random() * plant.width, plant.y, '#8B4513'); // Brown dust particles - } - } - }); + PhysicsEngine.checkCollisions(this._mario, gameState, callbacks); } + _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; + return PhysicsEngine.isColliding(rect1, rect2); } _hitQuestionBlock(block) { @@ -2173,7 +1679,7 @@ class MarioEducational extends Module { this._score += 100; // Increased points for question blocks this._addParticles(block.x, block.y, '#FFD700'); - this._playSound('question_block'); + soundSystem.play('question_block'); // Show question dialog this._showQuestionDialog(block.sentence); @@ -2291,300 +1797,33 @@ class MarioEducational extends Module { } _updatePiranhaPlants() { - const currentTime = Date.now(); - - this._piranhaPlants.forEach(plant => { - // Check if it's time to shoot - if (currentTime - plant.lastShot > plant.shootCooldown) { - // Check if Mario is in range (within 400 pixels) - const distanceToMario = Math.abs(plant.x - this._mario.x); - - if (distanceToMario < 400) { - // Shoot projectile towards Mario - const direction = this._mario.x > plant.x ? 1 : -1; - - this._projectiles.push({ - x: plant.x + plant.width / 2, - y: plant.y + plant.height / 2, - velocityX: direction * 3, // Projectile speed - velocityY: 0, - width: 8, - height: 8, - color: '#FF4500', // Orange fireball - type: 'fireball', - life: 200 // 200 frames lifetime - }); - - plant.lastShot = currentTime; - this._playSound('enemy_defeat'); // Shooting sound - console.log(`🔥 Piranha plant shot fireball towards Mario!`); - } - } - }); + PiranhaPlant.update(this._piranhaPlants, this._mario, this._projectiles, (sound) => soundSystem.play(sound)); } _updateProjectiles() { - // Update projectile positions - this._projectiles.forEach((projectile, index) => { - projectile.x += projectile.velocityX; - projectile.y += projectile.velocityY; - projectile.life--; - - // Remove projectiles that are off-screen or expired - if (projectile.life <= 0 || projectile.x < -50 || projectile.x > this._levelWidth + 50) { - this._projectiles.splice(index, 1); - return; - } - - // Check collision with Mario - if (this._isColliding(this._mario, projectile)) { - console.log(`🔥 Mario hit by projectile - restarting level`); - this._restartLevel(); - return; - } - - // Check collision with walls/platforms - const hitObstacle = this._platforms.some(platform => this._isColliding(projectile, platform)) || - this._walls.some(wall => this._isColliding(projectile, wall)); - - if (hitObstacle) { - this._projectiles.splice(index, 1); - this._addParticles(projectile.x, projectile.y, '#FF4500'); - console.log(`💥 Projectile hit obstacle`); - } - }); + this._projectiles = Projectile.update( + this._projectiles, + this._mario, + this._platforms, + this._walls, + this._levelWidth, + () => { console.log(`🔥 Mario hit by projectile`); this._restartLevel(); }, + (proj, idx, x, y) => this._addParticles(x, y, '#FF4500') + ); } _updateCatapults() { - const currentTime = Date.now(); - - this._catapults.forEach(catapult => { - // Check if it's time to shoot - if (currentTime - catapult.lastShot > catapult.shootCooldown) { - // Target Mario's position with imperfect aim (randomness) - const aimOffset = 100 + Math.random() * 150; // 100-250 pixel spread - const aimDirection = Math.random() < 0.5 ? -1 : 1; - const targetX = this._mario.x + (aimOffset * aimDirection); - const targetY = this._config.canvasHeight - 50; // Ground level - - // ONAGER ONLY: Check minimum range - don't fire if Mario is too close - if (catapult.isOnager) { - const distanceToMario = Math.abs(catapult.x - this._mario.x); - const minimumRange = 300; // Onager won't fire if Mario is within 300px - - if (distanceToMario < minimumRange) { - console.log(`🏛️ Onager held fire - Mario too close! Distance: ${distanceToMario.toFixed(0)}px (min: ${minimumRange}px)`); - return; // Skip this shot, Mario is too close - } - } - - // Check if there's a clear line of sight (no platform blocking) - if (!this._hasLineOfSight(catapult, targetX, targetY)) { - console.log(`🚫 ${catapult.type} blocked by obstacle, skipping shot`); - return; // Skip this shot, try again next time - } - - if (catapult.isOnager) { - // ONAGER: Fire 8 small stones in spread pattern - console.log(`🏛️ Onager firing stone rain!`); - - for (let stone = 0; stone < 8; stone++) { - // Much more random targeting for fear factor - const randomSpreadX = (Math.random() - 0.5) * 400; // ±200px spread - const randomSpreadY = (Math.random() - 0.5) * 100; // ±50px spread - const stoneTargetX = targetX + randomSpreadX; - const stoneTargetY = targetY + randomSpreadY; - - // Calculate trajectory for small stone - const deltaX = stoneTargetX - catapult.x; - const deltaY = stoneTargetY - catapult.y; - const time = 5 + Math.random() * 2; // 5-7 seconds flight time (varied) - const velocityX = deltaX / (time * 60); - const velocityY = (deltaY - 0.5 * 0.015 * time * time * 60 * 60) / (time * 60); // Slightly more gravity - - this._stones.push({ - x: catapult.x + 30 + (Math.random() - 0.5) * 20, // Spread launch point - y: catapult.y - 10 + (Math.random() - 0.5) * 10, - width: 8, // Much smaller than boulders - height: 8, - velocityX: velocityX, - velocityY: velocityY, - color: '#A0522D', // Brown stone color - type: 'stone', - sourceCatapultX: catapult.x, - sourceCatapultY: catapult.y - }); - } - - catapult.lastShot = currentTime; - this._playSound('enemy_defeat'); // Different sound for stone rain - console.log(`🏛️ Onager fired 8 stones in spread pattern!`); - } else { - // CATAPULT: Fire single boulder (original behavior) - const deltaX = targetX - catapult.x; - const deltaY = targetY - catapult.y; - const time = 7.5; // 7.5 seconds flight time - const velocityX = deltaX / (time * 60); - const velocityY = (deltaY - 0.5 * 0.01 * time * time * 60 * 60) / (time * 60); - - this._boulders.push({ - x: catapult.x + 30, - y: catapult.y - 10, - width: 25, - height: 25, - velocityX: velocityX, - velocityY: velocityY, - color: '#696969', // Dark gray - type: 'boulder', - hasLanded: false, - sourceCatapultX: catapult.x, - sourceCatapultY: catapult.y, - health: 2, - maxHealth: 2 - }); - - catapult.lastShot = currentTime; - this._playSound('jump'); // Boulder launch sound - console.log(`🏹 Catapult fired boulder towards x=${targetX.toFixed(0)}`); - } - } - }); + Catapult.update(this._catapults, this._mario, this._boulders, this._stones, (sound) => soundSystem.play(sound)); } _updateBoulders() { - this._boulders.forEach((boulder, index) => { - if (boulder.hasLanded) return; // Don't update landed boulders - - // Apply gravity and movement (MUCH slower with very light gravity) - boulder.velocityY += 0.01; // Ultra-light gravity so boulders can actually fly properly - boulder.x += boulder.velocityX; - boulder.y += boulder.velocityY; - - // Check ground collision - const groundLevel = this._config.canvasHeight - 50; - if (boulder.y + boulder.height >= groundLevel) { - this._handleBoulderImpact(boulder, index, boulder.x, groundLevel - boulder.height); - return; + this._boulders = Catapult.updateBoulders( + this._boulders, this._mario, this._platforms, this._walls, + (boulder, idx, x, y, obj, objIdx, type) => { + this._addParticles(x, y, '#808080'); + if (type === 'mario') this._restartLevel(); } - - // Check collision with platforms, walls, stairs - let hasHit = false; - - // Check platforms - this._platforms.forEach((platform, platformIndex) => { - if (!hasHit && this._isColliding(boulder, platform)) { - this._handleBoulderImpact(boulder, index, boulder.x, platform.y - boulder.height, platform, platformIndex); - hasHit = true; - } - }); - - // Check walls (boulder damages walls) - if (!hasHit) { - this._walls.forEach((wall, wallIndex) => { - if (!hasHit && this._isColliding(boulder, wall)) { - console.log(`🪨 Boulder hit wall! Wall damage system activated.`); - - // Boulder is DESTROYED when hitting wall - this._addParticles(boulder.x + boulder.width/2, boulder.y + boulder.height/2, '#696969'); - this._boulders.splice(index, 1); - this._playSound('enemy_defeat'); - console.log(`💥 Boulder destroyed on wall impact`); - - // Wall takes damage - wall.health--; - console.log(`🧱 Wall health: ${wall.health}/${wall.maxHealth}`); - - if (wall.health <= 0) { - // Wall is destroyed - this._addParticles(wall.x + wall.width/2, wall.y + wall.height/2, '#8B4513'); - this._walls.splice(wallIndex, 1); - console.log(`💥 Wall destroyed!`); - } else { - // Visual damage - change color to show damage - if (wall.health === 2) { - wall.color = '#A0522D'; // Slightly damaged - darker brown - } else if (wall.health === 1) { - wall.color = '#654321'; // Heavily damaged - dark brown - } - console.log(`🩹 Wall damaged but still standing`); - } - - hasHit = true; - } - }); - } - - // Check collision with catapults (but NOT the source catapult) - if (!hasHit) { - this._catapults.forEach((catapult, catapultIndex) => { - // Don't collide with the catapult that fired this boulder - const isSameCatapult = Math.abs(boulder.sourceCatapultX - catapult.x) < 10 && - Math.abs(boulder.sourceCatapultY - catapult.y) < 10; - - if (!hasHit && !isSameCatapult && this._isColliding(boulder, catapult)) { - // Boulder hits a different catapult - land on top - this._handleBoulderImpact(boulder, index, boulder.x, catapult.y - boulder.height, catapult, catapultIndex, 'catapult'); - hasHit = true; - console.log(`🪨 Boulder hit different catapult!`); - } - }); - } - - // Check collision with Mario (boulder resets level like enemies!) - if (!hasHit && this._isColliding(boulder, this._mario)) { - console.log(`💀 Boulder hit Mario! Respawning at level start.`); - this._addParticles(this._mario.x, this._mario.y, '#FF0000'); // Red death particles - this._playSound('enemy_defeat'); - - // Reset Mario to start position like other enemy deaths - const level = this._levelData[this._currentLevelIndex]; - this._mario.x = level.startX; - this._mario.y = level.startY; - this._mario.velocityX = 0; - this._mario.velocityY = 0; - - // Boulder lands after hitting Mario - this._handleBoulderImpact(boulder, index, boulder.x, this._config.canvasHeight - 75); - hasHit = true; - return; - } - - // Check collision with OTHER boulders (boulder-to-boulder damage system) - if (!hasHit) { - this._boulders.forEach((otherBoulder, otherIndex) => { - if (!hasHit && otherIndex !== index && otherBoulder.hasLanded && this._isColliding(boulder, otherBoulder)) { - console.log(`🪨 Flying boulder hit landed boulder! Damage system activated.`); - - // Flying boulder is DESTROYED (removed completely) - this._addParticles(boulder.x + boulder.width/2, boulder.y + boulder.height/2, '#696969'); - this._boulders.splice(index, 1); - this._playSound('enemy_defeat'); - console.log(`💥 Flying boulder destroyed on impact`); - - // Landed boulder takes damage - otherBoulder.health--; - console.log(`🩹 Landed boulder health: ${otherBoulder.health}/${otherBoulder.maxHealth}`); - - if (otherBoulder.health <= 0) { - // Landed boulder is destroyed - this._addParticles(otherBoulder.x + otherBoulder.width/2, otherBoulder.y + otherBoulder.height/2, '#8B4513'); - this._boulders.splice(otherIndex, 1); - console.log(`💥 Landed boulder destroyed!`); - } else { - // Visual damage - change color to show damage - otherBoulder.color = otherBoulder.health === 1 ? '#A0522D' : '#696969'; // Darker when damaged - } - - hasHit = true; - } - }); - } - - // Remove boulders that go off-screen - if (boulder.x < -100 || boulder.x > this._levelWidth + 100 || boulder.y > this._config.canvasHeight + 100) { - this._boulders.splice(index, 1); - } - }); + ); } _handleBoulderImpact(boulder, boulderIndex, impactX, impactY, hitObject = null, hitObjectIndex = -1, hitType = 'platform') { @@ -2598,7 +1837,7 @@ class MarioEducational extends Module { // Explosion effect this._addParticles(boulder.x + boulder.width/2, boulder.y + boulder.height/2, '#FF4500'); - this._playSound('enemy_defeat'); + soundSystem.play('enemy_defeat'); // Kill enemies in explosion radius (50 pixels) this._enemies.forEach((enemy, enemyIndex) => { @@ -2621,7 +1860,7 @@ class MarioEducational extends Module { if (marioDistance < 50) { console.log(`💀 Mario killed by boulder explosion! Respawning at level start. Distance: ${marioDistance.toFixed(0)}`); this._addParticles(this._mario.x, this._mario.y, '#FF0000'); // Red death particles - this._playSound('enemy_defeat'); + soundSystem.play('enemy_defeat'); // Reset Mario to start position like other enemy deaths const level = this._levelData[this._currentLevelIndex]; @@ -2640,69 +1879,12 @@ class MarioEducational extends Module { } _updateStones() { - this._stones.forEach((stone, index) => { - // Apply gravity and movement (similar to boulders but simpler) - stone.velocityY += 0.015; // Slightly more gravity than boulders - stone.x += stone.velocityX; - stone.y += stone.velocityY; - - // Check ground collision - const groundLevel = this._config.canvasHeight - 50; - if (stone.y + stone.height >= groundLevel) { - // Stone hits ground - create small particle effect and disappear - this._addParticles(stone.x + stone.width/2, stone.y + stone.height/2, '#A0522D'); - this._stones.splice(index, 1); - return; + this._stones = Catapult.updateStones( + this._stones, this._mario, this._platforms, + (stone, idx, type) => { + if (type === 'mario') this._restartLevel(); } - - // Check collision with Mario (stones kill Mario!) - if (this._isColliding(stone, this._mario)) { - console.log(`🪨 Stone hit Mario! Respawning at level start.`); - this._addParticles(this._mario.x, this._mario.y, '#FF0000'); // Red death particles - this._playSound('enemy_defeat'); - - // Reset Mario to start position like other deaths - const level = this._levelData[this._currentLevelIndex]; - this._mario.x = level.startX; - this._mario.y = level.startY; - this._mario.velocityX = 0; - this._mario.velocityY = 0; - - // Remove the stone that hit Mario - this._stones.splice(index, 1); - return; - } - - // Check collision with platforms/walls (stones disappear on impact) - let hasHit = false; - - // Check platforms - this._platforms.forEach((platform) => { - if (!hasHit && this._isColliding(stone, platform)) { - this._addParticles(stone.x + stone.width/2, stone.y + stone.height/2, '#A0522D'); - this._stones.splice(index, 1); - hasHit = true; - return; - } - }); - - // Check walls - if (!hasHit) { - this._walls.forEach((wall) => { - if (!hasHit && this._isColliding(stone, wall)) { - this._addParticles(stone.x + stone.width/2, stone.y + stone.height/2, '#A0522D'); - this._stones.splice(index, 1); - hasHit = true; - return; - } - }); - } - - // Remove stones that go off-screen - if (stone.x < -100 || stone.x > this._levelWidth + 100 || stone.y > this._config.canvasHeight + 100) { - this._stones.splice(index, 1); - } - }); + ); } _renderStones() { @@ -2794,7 +1976,7 @@ class MarioEducational extends Module { // Create star animation particles this._createFinishLineStars(); - this._playSound('finish_stars'); + soundSystem.play('finish_stars'); // Complete level after animation setTimeout(() => { @@ -2882,11 +2064,11 @@ class MarioEducational extends Module { } // Play sound effects for each wave - this._playSound('finish_stars'); + soundSystem.play('finish_stars'); // Extra sound effect for bigger waves if (wave >= 4) { - setTimeout(() => this._playSound('powerup'), 100); + setTimeout(() => soundSystem.play('powerup'), 100); } }, wave * 200); // Faster succession } @@ -2909,7 +2091,7 @@ class MarioEducational extends Module { isEpicFinalStar: true }); } - this._playSound('level_complete'); + soundSystem.play('level_complete'); console.log('🌟💥🎆 MEGA BONUS EXPLOSION! 300 GIANT STARS! 🎆💥🌟'); }, 2000); @@ -2921,7 +2103,7 @@ class MarioEducational extends Module { this._isCelebrating = true; // Block enemies but allow animations // Play level complete sound - this._playSound('level_complete'); + soundSystem.play('level_complete'); // Add star animations for every level completion this._createLevelCompleteStars(); @@ -3066,837 +2248,39 @@ class MarioEducational extends Module { // Subtract score penalty for death const penalty = 250; this._score = Math.max(0, this._score - penalty); - this._playSound('death'); + soundSystem.play('death'); console.log(`💀 Mario died! Score penalty: -${penalty}. New score: ${this._score}`); } _render() { - // Clear canvas - this._ctx.clearRect(0, 0, this._config.canvasWidth, this._config.canvasHeight); + // Build game state object for renderer + const gameState = { + mario: this._mario, + camera: this._camera, + platforms: this._platforms, + questionBlocks: this._questionBlocks, + enemies: this._enemies, + walls: this._walls, + piranhaPlants: this._piranhaPlants, + projectiles: this._projectiles, + catapults: this._catapults, + boulders: this._boulders, + stones: this._stones, + flyingEyes: this._flyingEyes, + boss: this._boss, + castle: this._castle, + finishLine: this._finishLine, + particles: this._particles, + currentLevel: this._currentLevel, + lives: this._lives, + score: this._score, + debugMode: false // Can be toggled + }; - // Render background - this._renderBackground(); - - // Save context for camera translation - this._ctx.save(); - this._ctx.translate(-this._camera.x, -this._camera.y); - - // Render platforms - this._renderPlatforms(); - - // Render question blocks - this._renderQuestionBlocks(); - - // Render enemies - this._renderEnemies(); - - // Render walls - this._renderWalls(); - - // Render advanced level elements - this._renderPiranhaPlants(); - this._renderProjectiles(); - - // Render level 4+ elements - this._renderCatapults(); - this._renderBoulders(); - this._renderStones(); - - // Render level 5+ elements - this._renderFlyingEyes(); - - // Render level 6 boss elements - this._renderBoss(); - - // Render castle (visual only) - this._renderCastle(); - - // Render finish line - this._renderFinishLine(); - - // Render Mario - this._renderMario(); - - // Render particles - this._renderParticles(); - - // Render debug hitboxes if needed - this._renderDebugHitboxes(); - - // Restore context - this._ctx.restore(); - - // Render UI - this._renderUI(); + // Delegate rendering to helper + renderer.render(this._ctx, gameState, this._config); } - _renderBackground() { - // Sky gradient - const gradient = this._ctx.createLinearGradient(0, 0, 0, this._config.canvasHeight); - gradient.addColorStop(0, '#87CEEB'); - gradient.addColorStop(1, '#98FB98'); - this._ctx.fillStyle = gradient; - this._ctx.fillRect(0, 0, this._config.canvasWidth, this._config.canvasHeight); - - // Clouds (fixed position, don't scroll with camera) - this._ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; - for (let i = 0; i < 5; i++) { - const x = (i * 300 + 100 - this._camera.x * 0.3) % (this._config.canvasWidth + 200); - const y = 80 + i * 30; - this._renderCloud(x, y); - } - } - - _renderCloud(x, y) { - this._ctx.beginPath(); - this._ctx.arc(x, y, 30, 0, Math.PI * 2); - this._ctx.arc(x + 30, y, 40, 0, Math.PI * 2); - this._ctx.arc(x + 60, y, 30, 0, Math.PI * 2); - this._ctx.fill(); - } - - _renderPlatforms() { - this._platforms.forEach(platform => { - this._ctx.fillStyle = platform.color; - this._ctx.fillRect(platform.x, platform.y, platform.width, platform.height); - - // Add outline - this._ctx.strokeStyle = '#000'; - this._ctx.lineWidth = 2; - this._ctx.strokeRect(platform.x, platform.y, platform.width, platform.height); - }); - } - - _renderQuestionBlocks() { - this._questionBlocks.forEach(block => { - // Block - this._ctx.fillStyle = block.color; - this._ctx.fillRect(block.x, block.y, block.width, block.height); - - // Outline - this._ctx.strokeStyle = '#000'; - this._ctx.lineWidth = 2; - this._ctx.strokeRect(block.x, block.y, block.width, block.height); - - // Symbol - this._ctx.fillStyle = '#000'; - this._ctx.font = 'bold 24px Arial'; - this._ctx.textAlign = 'center'; - this._ctx.textBaseline = 'middle'; - this._ctx.fillText( - block.symbol, - block.x + block.width / 2, - block.y + block.height / 2 - ); - }); - } - - _renderEnemies() { - this._enemies.forEach(enemy => { - this._ctx.fillStyle = enemy.color; - this._ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height); - - // Simple face - this._ctx.fillStyle = '#FFF'; - this._ctx.fillRect(enemy.x + 3, enemy.y + 3, 3, 3); // Eye - this._ctx.fillRect(enemy.x + 14, enemy.y + 3, 3, 3); // Eye - - // Draw helmet for protected enemies - if (enemy.hasHelmet) { - this._ctx.fillStyle = '#C0C0C0'; // Silver helmet - this._ctx.fillRect(enemy.x - 1, enemy.y - 4, enemy.width + 2, 6); - - // Helmet shine - this._ctx.fillStyle = '#E8E8E8'; - this._ctx.fillRect(enemy.x + 2, enemy.y - 3, 4, 2); - - // Helmet symbol (shield) - this._ctx.fillStyle = '#FFD700'; - this._ctx.fillRect(enemy.x + 8, enemy.y - 2, 4, 2); - } - }); - } - - _renderWalls() { - this._walls.forEach(wall => { - // Wall main body - this._ctx.fillStyle = wall.color; - this._ctx.fillRect(wall.x, wall.y, wall.width, wall.height); - - // Add brick pattern for visual appeal - this._ctx.strokeStyle = '#654321'; // Darker brown for lines - this._ctx.lineWidth = 1; - - // Horizontal lines - for (let y = wall.y + 20; y < wall.y + wall.height; y += 20) { - this._ctx.beginPath(); - this._ctx.moveTo(wall.x, y); - this._ctx.lineTo(wall.x + wall.width, y); - this._ctx.stroke(); - } - - // Vertical lines (offset every other row) - for (let row = 0; row < Math.floor(wall.height / 20); row++) { - const yPos = wall.y + row * 20; - const offset = (row % 2) * 10; // Offset every other row - - for (let x = wall.x + offset + 20; x < wall.x + wall.width; x += 20) { - this._ctx.beginPath(); - this._ctx.moveTo(x, yPos); - this._ctx.lineTo(x, Math.min(yPos + 20, wall.y + wall.height)); - this._ctx.stroke(); - } - } - - // Wall type indicator (for debugging) - if (wall.type === 'tall') { - this._ctx.fillStyle = '#FF0000'; - this._ctx.fillRect(wall.x + wall.width - 5, wall.y, 5, 20); - } - }); - } - - - _renderPiranhaPlants() { - this._piranhaPlants.forEach(plant => { - if (plant.flattened) { - // Flattened plant - just a green pancake on the ground - this._ctx.fillStyle = '#556B2F'; // Dark olive green (dead/flat) - this._ctx.fillRect(plant.x, plant.y + plant.height - 5, plant.width, 5); - - // Some scattered debris - this._ctx.fillStyle = '#8B4513'; // Brown debris - this._ctx.fillRect(plant.x + 5, plant.y + plant.height - 3, 3, 2); - this._ctx.fillRect(plant.x + 20, plant.y + plant.height - 4, 2, 3); - this._ctx.fillRect(plant.x + 12, plant.y + plant.height - 2, 4, 1); - } else { - // Normal living plant - // Plant stem - this._ctx.fillStyle = '#228B22'; // Forest green - this._ctx.fillRect(plant.x + 10, plant.y, 10, plant.height); - - // Plant head (circular) - this._ctx.fillStyle = '#32CD32'; // Lime green - this._ctx.beginPath(); - this._ctx.arc(plant.x + 15, plant.y + 10, 12, 0, Math.PI * 2); - this._ctx.fill(); - - // Sharp teeth - this._ctx.fillStyle = '#FFFFFF'; - for (let i = 0; i < 6; i++) { - const angle = (i * Math.PI * 2) / 6; - const toothX = plant.x + 15 + Math.cos(angle) * 8; - const toothY = plant.y + 10 + Math.sin(angle) * 8; - this._ctx.fillRect(toothX - 1, toothY - 1, 2, 4); - } - - // Red mouth center - this._ctx.fillStyle = '#DC143C'; // Crimson - this._ctx.beginPath(); - this._ctx.arc(plant.x + 15, plant.y + 10, 6, 0, Math.PI * 2); - this._ctx.fill(); - - // Eyes - this._ctx.fillStyle = '#000000'; - this._ctx.fillRect(plant.x + 10, plant.y + 5, 3, 3); - this._ctx.fillRect(plant.x + 17, plant.y + 5, 3, 3); - } - }); - } - - _renderProjectiles() { - this._projectiles.forEach(projectile => { - // Fireball effect - this._ctx.fillStyle = projectile.color; - this._ctx.beginPath(); - this._ctx.arc(projectile.x + projectile.width/2, projectile.y + projectile.height/2, projectile.width/2, 0, Math.PI * 2); - this._ctx.fill(); - - // Inner glow - this._ctx.fillStyle = '#FFFF00'; // Yellow center - this._ctx.beginPath(); - this._ctx.arc(projectile.x + projectile.width/2, projectile.y + projectile.height/2, projectile.width/4, 0, Math.PI * 2); - this._ctx.fill(); - }); - } - - _renderCatapults() { - this._catapults.forEach(catapult => { - // Catapult base - this._ctx.fillStyle = catapult.color; - this._ctx.fillRect(catapult.x, catapult.y + 40, catapult.width, 40); - - // Catapult arm - this._ctx.fillStyle = '#654321'; // Dark brown - this._ctx.fillRect(catapult.x + 10, catapult.y, 8, 50); - - // Bucket - this._ctx.fillStyle = '#8B4513'; - this._ctx.fillRect(catapult.x + 5, catapult.y, 18, 12); - - // Support beams - this._ctx.strokeStyle = '#654321'; - this._ctx.lineWidth = 3; - this._ctx.beginPath(); - this._ctx.moveTo(catapult.x + 30, catapult.y + 40); - this._ctx.lineTo(catapult.x + 14, catapult.y + 25); - this._ctx.stroke(); - }); - } - - _renderBoulders() { - this._boulders.forEach(boulder => { - // Boulder body - this._ctx.fillStyle = boulder.color; - this._ctx.beginPath(); - this._ctx.arc(boulder.x + boulder.width/2, boulder.y + boulder.height/2, boulder.width/2, 0, Math.PI * 2); - this._ctx.fill(); - - // Boulder texture/cracks - this._ctx.strokeStyle = '#555555'; - this._ctx.lineWidth = 1; - this._ctx.beginPath(); - this._ctx.arc(boulder.x + boulder.width/2 - 5, boulder.y + boulder.height/2 - 3, 3, 0, Math.PI); - this._ctx.stroke(); - this._ctx.beginPath(); - this._ctx.arc(boulder.x + boulder.width/2 + 4, boulder.y + boulder.height/2 + 2, 2, 0, Math.PI); - this._ctx.stroke(); - - // Show trajectory line for flying boulders (debug) - if (!boulder.hasLanded && boulder.velocityX !== 0) { - this._ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; - this._ctx.lineWidth = 1; - this._ctx.setLineDash([5, 5]); - this._ctx.beginPath(); - this._ctx.moveTo(boulder.x + boulder.width/2, boulder.y + boulder.height/2); - this._ctx.lineTo(boulder.x + boulder.velocityX * 10, boulder.y + boulder.velocityY * 10); - this._ctx.stroke(); - this._ctx.setLineDash([]); - } - }); - } - - _renderFinishLine() { - const level = this._levelData[this._currentLevelIndex]; - const finishX = level.endX; - - // Draw checkered flag pattern - this._ctx.fillStyle = '#FFD700'; // Gold color - this._ctx.fillRect(finishX - 5, 50, 10, this._config.canvasHeight - 100); - - // Add black and white checkered pattern - const squareSize = 20; - for (let y = 50; y < this._config.canvasHeight - 50; y += squareSize) { - for (let x = 0; x < 10; x += squareSize / 2) { - const isBlack = Math.floor((y - 50) / squareSize) % 2 === Math.floor(x / (squareSize / 2)) % 2; - this._ctx.fillStyle = isBlack ? '#000000' : '#FFFFFF'; - this._ctx.fillRect(finishX - 5 + x, y, squareSize / 2, squareSize); - } - } - - // Add "FINISH" text - this._ctx.save(); - this._ctx.translate(finishX, 30); - this._ctx.rotate(-Math.PI / 2); - this._ctx.fillStyle = '#FF0000'; - this._ctx.font = 'bold 16px Arial'; - this._ctx.textAlign = 'center'; - this._ctx.fillText('FINISH', 0, 0); - this._ctx.restore(); - - // Add arrow pointing to finish - if (this._mario.x < finishX - 200) { - this._ctx.fillStyle = '#FFD700'; - this._ctx.font = 'bold 20px Arial'; - this._ctx.textAlign = 'left'; - this._ctx.fillText('→ FINISH', finishX - 150, 25); - } - } - - _updateFlyingEyes() { - const currentTime = Date.now(); - - this._flyingEyes.forEach((eye, index) => { - // Calculate distance to Mario - const distanceToMario = Math.sqrt( - Math.pow(eye.x - this._mario.x, 2) + - Math.pow(eye.y - this._mario.y, 2) - ); - - // Determine if eye should chase Mario - eye.isChasing = distanceToMario < eye.chaseDistance; - - // Handle dash behavior - if (eye.isDashing) { - eye.dashDuration--; - if (eye.dashDuration <= 0) { - eye.isDashing = false; - eye.lastDashTime = currentTime; - console.log(`👁️ Eye finished dashing!`); - } - // During dash, maintain dash velocity (no other movement changes) - } else { - // Check if it's time to dash (only when chasing) - if (eye.isChasing && currentTime - eye.lastDashTime > eye.dashInterval && Math.random() < 0.3) { - // Start dash in random 90-degree direction - const dashDirections = [ - { x: 1, y: 0 }, // Right - { x: -1, y: 0 }, // Left - { x: 0, y: 1 }, // Down - { x: 0, y: -1 } // Up - ]; - const dashDir = dashDirections[Math.floor(Math.random() * 4)]; - - eye.velocityX = dashDir.x * eye.dashSpeed; - eye.velocityY = dashDir.y * eye.dashSpeed; - eye.isDashing = true; - eye.dashDuration = 20; // Dash for 20 frames (~0.33 seconds) - console.log(`👁️ Eye started dashing! Direction: ${dashDir.x}, ${dashDir.y}`); - } else if (eye.isChasing) { - // Normal chase behavior - move toward Mario - const deltaX = this._mario.x - eye.x; - const deltaY = this._mario.y - eye.y; - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - if (distance > 0) { - // Normalize direction and apply chase speed - eye.velocityX = (deltaX / distance) * eye.chaseSpeed; - eye.velocityY = (deltaY / distance) * eye.chaseSpeed; - } - - console.log(`👁️ Eye chasing Mario! Distance: ${distanceToMario.toFixed(0)}`); - } else { - // Idle behavior - random floating movement (faster now) - if (currentTime - eye.lastDirectionChange > eye.directionChangeInterval) { - // Change direction randomly - eye.velocityX = (Math.random() - 0.5) * eye.idleSpeed * 2; - eye.velocityY = (Math.random() - 0.5) * eye.idleSpeed * 2; - eye.lastDirectionChange = currentTime; - eye.directionChangeInterval = 2000 + Math.random() * 3000; - } - } - } - - // Apply movement - eye.x += eye.velocityX; - eye.y += eye.velocityY; - - // Keep eyes within screen bounds - if (eye.x < 0) { - eye.x = 0; - eye.velocityX = Math.abs(eye.velocityX); - } else if (eye.x > this._levelWidth - eye.width) { - eye.x = this._levelWidth - eye.width; - eye.velocityX = -Math.abs(eye.velocityX); - } - - if (eye.y < 0) { - eye.y = 0; - eye.velocityY = Math.abs(eye.velocityY); - } else if (eye.y > this._config.canvasHeight - eye.height - 50) { // Stay above ground - eye.y = this._config.canvasHeight - eye.height - 50; - eye.velocityY = -Math.abs(eye.velocityY); - } - - // Blinking animation - eye.blinkTimer++; - if (eye.blinkTimer > 120 + Math.random() * 180) { // Blink every 2-5 seconds - eye.isBlinking = true; - eye.blinkTimer = 0; - } - if (eye.isBlinking && eye.blinkTimer > 10) { // Blink lasts 10 frames - eye.isBlinking = false; - } - - // Check collision with Mario - if (this._isColliding(this._mario, eye)) { - // Eye can be stomped like normal enemies - const overlapTop = (this._mario.y + this._mario.height) - eye.y; - const overlapBottom = (eye.y + eye.height) - this._mario.y; - - if (this._mario.velocityY > 0 && overlapTop < overlapBottom) { - // Mario stomped the eye - this._flyingEyes.splice(index, 1); - this._mario.velocityY = this._config.jumpForce / 2; // Small bounce - this._addParticles(eye.x, eye.y, '#DC143C'); // Red particles - this._playSound('enemy_defeat'); - console.log(`👁️ Mario stomped flying eye!`); - } else { - // Eye hurts Mario - reset level - console.log(`💥 Mario hit by flying eye - restarting level`); - this._restartLevel(); - } - } - }); - } - - _renderFlyingEyes() { - this._flyingEyes.forEach(eye => { - // Draw eye body (white oval) - this._ctx.fillStyle = '#FFFFFF'; - this._ctx.beginPath(); - this._ctx.ellipse(eye.x + eye.width/2, eye.y + eye.height/2, eye.width/2, eye.height/2, 0, 0, 2 * Math.PI); - this._ctx.fill(); - - if (!eye.isBlinking) { - // Draw red iris - this._ctx.fillStyle = eye.color; - this._ctx.beginPath(); - this._ctx.ellipse(eye.x + eye.width/2, eye.y + eye.height/2, eye.width/3, eye.height/3, 0, 0, 2 * Math.PI); - this._ctx.fill(); - - // Draw black pupil that follows Mario - const deltaX = this._mario.x - eye.x; - const deltaY = this._mario.y - eye.y; - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - let pupilOffsetX = 0; - let pupilOffsetY = 0; - - if (distance > 0) { - const maxOffset = 4; // Maximum pupil movement - pupilOffsetX = (deltaX / distance) * maxOffset; - pupilOffsetY = (deltaY / distance) * maxOffset; - } - - this._ctx.fillStyle = eye.pupilColor; - this._ctx.beginPath(); - this._ctx.arc( - eye.x + eye.width/2 + pupilOffsetX, - eye.y + eye.height/2 + pupilOffsetY, - eye.width/6, - 0, - 2 * Math.PI - ); - this._ctx.fill(); - } else { - // Draw closed eye (horizontal line) - this._ctx.strokeStyle = '#000000'; - this._ctx.lineWidth = 2; - this._ctx.beginPath(); - this._ctx.moveTo(eye.x + 5, eye.y + eye.height/2); - this._ctx.lineTo(eye.x + eye.width - 5, eye.y + eye.height/2); - this._ctx.stroke(); - } - - // Add subtle outline - this._ctx.strokeStyle = '#CCCCCC'; - this._ctx.lineWidth = 1; - this._ctx.beginPath(); - this._ctx.ellipse(eye.x + eye.width/2, eye.y + eye.height/2, eye.width/2, eye.height/2, 0, 0, 2 * Math.PI); - this._ctx.stroke(); - }); - } - - _updateBoss() { - if (!this._boss) return; - - const currentTime = Date.now(); - - // Update boss collision cooldown - if (this._bossCollisionCooldown > 0) { - this._bossCollisionCooldown--; - } - - // Update damage flash timer - if (this._boss.damageFlashTimer > 0) { - this._boss.damageFlashTimer--; - if (this._boss.damageFlashTimer <= 0) { - this._boss.isDamaged = false; - } - } - - // Boss collision with Mario (blocks the path) - with cooldown to prevent loop - if (this._isColliding(this._mario, this._boss) && this._bossCollisionCooldown <= 0) { - console.log(`👹 Mario hit boss body - bounced back!`); - - // TRUE velocity inversion: velocity = velocity * -1 - this._mario.velocityX = this._mario.velocityX * -1 - 3; // Invert + add knockback - this._mario.velocityY = this._config.jumpForce * 0.7; // Bounce upward - - // Set collision cooldown (3 ticks) - this._bossCollisionCooldown = 3; - - // Impact particles - this._addParticles( - this._mario.x + this._mario.width / 2, - this._mario.y + this._mario.height / 2, - '#FFFF00' // Yellow impact particles - ); - - // Impact sound - this._playSound('enemy_defeat'); - - console.log(`⏱️ Boss collision cooldown set: ${this._bossCollisionCooldown} ticks`); - } - } - - _renderBoss() { - if (!this._boss) return; - - const ctx = this._ctx; - const x = this._boss.x; - const y = this._boss.y; - const w = this._boss.width; - const h = this._boss.height; - - // Boss main body (torso) - more humanoid proportions - ctx.fillStyle = this._boss.isDamaged ? '#FF6666' : this._boss.color; - ctx.fillRect(x + 20, y, w - 40, h * 0.6); // Torso - - // Boss legs (separated) - ctx.fillRect(x + 10, y + h * 0.6, 35, h * 0.4); // Left leg - ctx.fillRect(x + w - 45, y + h * 0.6, 35, h * 0.4); // Right leg - - // Boss head (bigger and more menacing) - const headWidth = 60; - const headHeight = 50; - ctx.fillRect(x + (w - headWidth) / 2, y - headHeight, headWidth, headHeight); - - // Boss outline for all parts - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 3; - ctx.strokeRect(x + 20, y, w - 40, h * 0.6); // Torso outline - ctx.strokeRect(x + 10, y + h * 0.6, 35, h * 0.4); // Left leg outline - ctx.strokeRect(x + w - 45, y + h * 0.6, 35, h * 0.4); // Right leg outline - ctx.strokeRect(x + (w - headWidth) / 2, y - headHeight, headWidth, headHeight); // Head outline - - // Boss eyes (glowing red, larger) - ctx.fillStyle = this._boss.eyeColor; - const eyeSize = 12; - ctx.fillRect(x + (w - headWidth) / 2 + 10, y - headHeight + 15, eyeSize, eyeSize); // Left eye - ctx.fillRect(x + (w - headWidth) / 2 + headWidth - 22, y - headHeight + 15, eyeSize, eyeSize); // Right eye - - // Boss mouth (menacing) - ctx.fillStyle = '#000000'; - ctx.fillRect(x + (w - headWidth) / 2 + 15, y - headHeight + 35, headWidth - 30, 8); - - // Shoulder spikes for more intimidating look - ctx.fillStyle = '#8B4513'; - ctx.fillRect(x + 15, y + 5, 15, 25); // Left shoulder - ctx.fillRect(x + w - 30, y + 5, 15, 25); // Right shoulder - - // Boss knees (damage zones) - integrated styling as kneepads - ctx.fillStyle = '#FFD700'; // Gold kneepads - ctx.fillRect( - this._boss.leftKnee.x + 5, - this._boss.leftKnee.y + 5, - this._boss.leftKnee.width - 10, - this._boss.leftKnee.height - 10 - ); - ctx.fillRect( - this._boss.rightKnee.x + 5, - this._boss.rightKnee.y + 5, - this._boss.rightKnee.width - 10, - this._boss.rightKnee.height - 10 - ); - - // Kneepad outlines - ctx.strokeStyle = '#B8860B'; // Dark goldenrod outline - ctx.lineWidth = 2; - ctx.strokeRect( - this._boss.leftKnee.x + 5, - this._boss.leftKnee.y + 5, - this._boss.leftKnee.width - 10, - this._boss.leftKnee.height - 10 - ); - ctx.strokeRect( - this._boss.rightKnee.x + 5, - this._boss.rightKnee.y + 5, - this._boss.rightKnee.width - 10, - this._boss.rightKnee.height - 10 - ); - - // Boss health bar - const healthBarWidth = 200; - const healthBarHeight = 20; - const healthBarX = this._boss.x + (this._boss.width / 2) - (healthBarWidth / 2); - const healthBarY = this._boss.y - 40; - - // Health bar background - ctx.fillStyle = '#FF0000'; - ctx.fillRect(healthBarX, healthBarY, healthBarWidth, healthBarHeight); - - // Health bar foreground - const healthPercent = this._boss.health / this._boss.maxHealth; - ctx.fillStyle = '#00FF00'; - ctx.fillRect(healthBarX, healthBarY, healthBarWidth * healthPercent, healthBarHeight); - - // Health bar border - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 2; - ctx.strokeRect(healthBarX, healthBarY, healthBarWidth, healthBarHeight); - - // Render turrets on boss - this._bossTurrets.forEach(turret => { - ctx.fillStyle = turret.color; - ctx.fillRect(turret.x, turret.y, turret.width, turret.height); - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 1; - ctx.strokeRect(turret.x, turret.y, turret.width, turret.height); - }); - } - - _renderMario() { - // Mario body - this._ctx.fillStyle = this._mario.color; - this._ctx.fillRect(this._mario.x, this._mario.y, this._mario.width, this._mario.height); - - // Hat - this._ctx.fillStyle = '#8B0000'; - this._ctx.fillRect(this._mario.x + 4, this._mario.y - 8, this._mario.width - 8, 12); - - // Face - this._ctx.fillStyle = '#FFDBAC'; - this._ctx.fillRect(this._mario.x + 8, this._mario.y + 8, 16, 16); - - // Eyes - this._ctx.fillStyle = '#000'; - const eyeOffset = this._mario.facing === 'right' ? 2 : -2; - this._ctx.fillRect(this._mario.x + 10 + eyeOffset, this._mario.y + 12, 2, 2); - this._ctx.fillRect(this._mario.x + 18 + eyeOffset, this._mario.y + 12, 2, 2); - - // Mustache - this._ctx.fillRect(this._mario.x + 12, this._mario.y + 18, 8, 2); - } - - _renderParticles() { - this._particles.forEach(particle => { - const alpha = particle.life / particle.maxLife; - - if (particle.isFinishStar) { - // Render finish line stars as actual star symbols - this._ctx.font = `${particle.size}px serif`; - this._ctx.textAlign = 'center'; - this._ctx.textBaseline = 'middle'; - this._ctx.fillStyle = particle.color + Math.floor(alpha * 255).toString(16).padStart(2, '0'); - this._ctx.fillText('⭐', particle.x, particle.y); - } else { - // Regular particles - this._ctx.fillStyle = particle.color + Math.floor(alpha * 255).toString(16).padStart(2, '0'); - this._ctx.fillRect(particle.x, particle.y, 4, 4); - } - }); - - // Reset text alignment - this._ctx.textAlign = 'left'; - this._ctx.textBaseline = 'top'; - } - - _renderCastle() { - if (!this._castleStructure) return; - - const castle = this._castleStructure; - - // Set font for castle emoji - this._ctx.font = `${castle.size}px serif`; - this._ctx.textAlign = 'center'; - this._ctx.textBaseline = 'middle'; - - // Render castle emoji - this._ctx.fillText(castle.emoji, castle.x, castle.y); - - // Render princess in the castle - if (castle.princess) { - this._ctx.font = `${castle.princess.size}px serif`; - this._ctx.fillText(castle.princess.emoji, castle.princess.x, castle.princess.y); - } - - // Add sparkles around the massive castle (360px) - this._ctx.font = '40px serif'; - this._ctx.fillText('✨', castle.x - 180, castle.y - 120); - this._ctx.fillText('✨', castle.x + 180, castle.y - 120); - this._ctx.fillText('✨', castle.x - 120, castle.y + 120); - this._ctx.fillText('✨', castle.x + 120, castle.y + 120); - - // Additional sparkles for the massive castle - this._ctx.font = '30px serif'; - this._ctx.fillText('✨', castle.x - 220, castle.y); - this._ctx.fillText('✨', castle.x + 220, castle.y); - this._ctx.fillText('✨', castle.x, castle.y - 200); - this._ctx.fillText('✨', castle.x, castle.y + 180); - - // Extra sparkles for grandeur - this._ctx.font = '25px serif'; - this._ctx.fillText('⭐', castle.x - 160, castle.y - 60); - this._ctx.fillText('⭐', castle.x + 160, castle.y - 60); - this._ctx.fillText('⭐', castle.x - 60, castle.y + 160); - this._ctx.fillText('⭐', castle.x + 60, castle.y + 160); - - // Reset text alignment - this._ctx.textAlign = 'left'; - this._ctx.textBaseline = 'top'; - } - - _renderUI() { - if (this._uiOverlay) { - this._uiOverlay.innerHTML = ` -
Score: ${this._score}
-
Level: ${this._currentLevel}/${this._config.maxLevels}
-
Questions: ${this._questionsAnswered}
-
- Use Arrow Keys or WASD to move, Space/Up to jump -
- `; - } - } - - _getRandomSentence() { - const availableSentences = this._sentences.filter(s => !this._usedSentences.includes(s)); - if (availableSentences.length === 0) { - // Reset used sentences if all are used - this._usedSentences = []; - return this._sentences[0]; - } - - const sentence = availableSentences[Math.floor(Math.random() * availableSentences.length)]; - this._usedSentences.push(sentence); - return sentence; - } - - _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; - } - - _renderDebugHitboxes() { - // Only render in debug mode (can be toggled) - if (!window.DEBUG_HITBOXES) return; - - this._ctx.strokeStyle = '#FF0000'; - this._ctx.lineWidth = 2; - - // Mario hitbox - this._ctx.strokeRect(this._mario.x, this._mario.y, this._mario.width, this._mario.height); - - // Platform hitboxes - this._ctx.strokeStyle = '#00FF00'; - this._platforms.forEach(platform => { - this._ctx.strokeRect(platform.x, platform.y, platform.width, platform.height); - }); - - // Wall hitboxes - this._ctx.strokeStyle = '#8B4513'; - this._walls.forEach(wall => { - this._ctx.strokeRect(wall.x, wall.y, wall.width, wall.height); - }); - - // Enemy hitboxes - this._ctx.strokeStyle = '#FF00FF'; - this._enemies.forEach(enemy => { - this._ctx.strokeRect(enemy.x, enemy.y, enemy.width, enemy.height); - }); - - // Question block hitboxes - this._ctx.strokeStyle = '#FFFF00'; - this._questionBlocks.forEach(block => { - this._ctx.strokeRect(block.x, block.y, block.width, block.height); - }); - } } export default MarioEducational; \ No newline at end of file