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 {
-
+
Correct Answer: ${currentWord.cleanTranslation}
${this.config.showPronunciation && currentWord.pronunciation ?
- `
[${currentWord.pronunciation}]
` : ''}
+ `
[${currentWord.pronunciation}]
` : ''}
`;
@@ -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