diff --git a/CLAUDE.md b/CLAUDE.md index a86f18f..29963b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,1392 +1,90 @@ -# CLAUDE.md - Project Context & Instructions +# CLAUDE.md - Class Generator 2.0 -## ๐Ÿ“‹ Project Overview +## ๐Ÿ“‹ Overview +Educational platform with ultra-modular vanilla JS/HTML/CSS architecture. Strict separation, sealed modules, dependency injection. -**Class Generator 2.0** - Complete rewrite of educational games platform with ultra-modular architecture. +## ๐Ÿ—๏ธ Status -### ๐ŸŽฏ Current Mission -Building a **bulletproof modular system** with strict separation of concerns using vanilla JavaScript, HTML, and CSS. The architecture enforces inviolable responsibility patterns with sealed modules and dependency injection. +**Core โœ…**: Module.js, EventBus.js, ModuleLoader.js, Router.js, Application.js, Dev Server +**DRS โœ…**: ContentLoader, IAEngine (OpenAIโ†’DeepSeek), LLMValidator, AI Reports, UnifiedDRS +**Exercises โœ…**: VocabularyModule, TextAnalysis, GrammarAnalysis, Translation, OpenResponse +**AI โœ…**: Production ready, strict scoring (wrong: 0-20, correct: 70-100), no mock fallbacks -### ๐Ÿ—๏ธ Architecture Status +## โš ๏ธ Critical Rules -**PHASE 1 COMPLETED โœ…** - Core foundation built with rigorous architectural patterns: -- โœ… **Module.js** - Abstract base class with WeakMap privates and sealed instances -- โœ… **EventBus.js** - Strict event system with validation and module registration -- โœ… **ModuleLoader.js** - Dependency injection with proper initialization order -- โœ… **Router.js** - Navigation with guards, middleware, and state management -- โœ… **Application.js** - Auto-bootstrap system with lifecycle management -- โœ… **Development Server** - HTTP server with ES6 modules and CORS support +### Architecture (NON-NEGOTIABLE) +1. **Single responsibility** per module +2. **EventBus only** for communication (no direct deps) +3. **Object.seal()** prevents modification +4. **WeakMap** for private state +5. **Abstract enforcement** - missing methods = fatal error +6. **Dependency injection** - no globals -**DRS SYSTEM COMPLETED โœ…** - Advanced learning modules with dual AI approach: - -**Core Exercise Generation:** -- โœ… **ContentLoader** - Pure AI content generation when no real content available -- โœ… **IAEngine** - Multi-provider AI system (OpenAI โ†’ DeepSeek โ†’ Hard Fail) -- โœ… **LLMValidator** - Intelligent answer validation with detailed feedback -- โœ… **AI Report System** - Session tracking with exportable reports (text/HTML/JSON) -- โœ… **UnifiedDRS** - Component-based exercise presentation system - -**DRS Exercise Modules:** -- โœ… **Vocabulary Flashcards** - VocabularyModule provides spaced repetition learning (local validation, no AI) -- โœ… **Intelligent QCM** - AI generates questions + 1 correct + 5 plausible wrong answers (16.7% random chance) -- โœ… **Open Analysis Modules** - Free-text responses validated by AI with personalized feedback - - โœ… TextAnalysisModule - Deep comprehension with AI coaching (0-100 strict scoring) - - โœ… GrammarAnalysisModule - Grammar correction with explanations (0-100 strict scoring) - - โœ… TranslationModule - Translation validation with multi-language support (0-100 strict scoring) - - โœ… OpenResponseModule - Free-form questions with intelligent evaluation - -**AI Architecture - PRODUCTION READY:** -- โœ… **AI-Mandatory System** - No mock/fallback, real AI only, ensures educational quality -- โœ… **Strict Scoring Logic** - Wrong answers: 0-20 points, Correct answers: 70-100 points -- โœ… **Multi-Provider Fallback** - OpenAI โ†’ DeepSeek โ†’ Hard Fail (no fake responses) -- โœ… **Comprehensive Testing** - 100% validation with multiple test scenarios -- โœ… **Smart Prompt Engineering** - Context-aware prompts with proper language detection -- โš ๏ธ **Cache System** - Currently disabled for testing (see Cache Management section) - -## โš ๏ธ CRITICAL ARCHITECTURAL SEPARATION - -### ๐ŸŽฏ DRS vs Games - NEVER MIX - -**DRS (Dynamic Response System)** - Educational exercise modules in `src/DRS/`: -- โœ… **VocabularyModule.js** - Spaced repetition flashcards (local validation) -- โœ… **TextAnalysisModule.js** - AI-powered text comprehension -- โœ… **GrammarAnalysisModule.js** - AI grammar correction -- โœ… **TranslationModule.js** - AI translation validation -- โœ… **All modules in `src/DRS/`** - Educational exercises with strict interface compliance - -**Games** - Independent game modules in `src/games/`: -- โŒ **FlashcardLearning.js** - Standalone flashcard game (NOT part of DRS) -- โŒ **Other game modules** - Entertainment-focused, different architecture -- โŒ **NEVER import games into DRS** - Violates separation of concerns - -### ๐Ÿšซ FORBIDDEN MIXING -```javascript -// โŒ NEVER DO THIS - DRS importing games -import FlashcardLearning from '../games/FlashcardLearning.js'; - -// โœ… CORRECT - DRS uses its own modules -import VocabularyModule from './exercise-modules/VocabularyModule.js'; -``` - -**Rule**: **DRS = Educational Exercises**, **Games = Entertainment**. They MUST remain separate. - -## ๐Ÿ”ฅ Critical Requirements - -### Architecture Principles (NON-NEGOTIABLE) -1. **Inviolable Responsibility** - Each module has exactly one purpose -2. **Zero Direct Dependencies** - All communication via EventBus only -3. **Sealed Instances** - Modules cannot be modified after creation -4. **Private State** - Internal data completely inaccessible via WeakMap -5. **Contract Enforcement** - Abstract methods must be implemented -6. **Dependency Injection** - No globals, everything injected through constructor +### DRS vs Games - NEVER MIX +- **DRS** (`src/DRS/`) = Educational exercises with strict interfaces +- **Games** (`src/games/`) = Entertainment, different architecture +- โŒ NEVER `import FlashcardLearning from '../games/'` in DRS +- โœ… ALWAYS `import VocabularyModule from './exercise-modules/'` in DRS ### Technical Constraints -- **Vanilla JS/HTML/CSS only** - No frameworks -- **ES6 Modules** - Import/export syntax required -- **HTTP Protocol** - Never file:// (use development server) -- **Modular CSS** - Component-scoped styling -- **Event-Driven** - No direct module coupling +- Vanilla JS/HTML/CSS only (no frameworks) +- ES6 modules, HTTP protocol (never file://) +- **NO SCROLL** - all UI fits viewport height +- Mobile-first, compact headers, vertical space precious -### UI/UX Design Principles (CRITICAL) -- **NO SCROLL POLICY** - All interfaces MUST fit within viewport height without scrolling -- **Height Management** - Vertical space is precious, horizontal space is abundant -- **Compact Navigation** - Top bars and headers must be minimal height -- **Responsive Layout** - Use available width, preserve viewport height -- **Mobile-First** - Design for smallest screens first, then scale up +### Development DO's/DON'Ts +โœ… Extend Module base, use EventBus, validate deps, seal objects, simple solutions first +โŒ Never access internals, globals, skip validation, hardcode paths, overcomplicate positioning -## ๐Ÿš€ Development Workflow - -### Starting the System +## ๐Ÿš€ Quick Start ```bash -# Option 1: Windows batch file -start.bat - -# Option 2: Node.js directly -node server.js - -# Option 3: NPM scripts -npm start +start.bat # or: node server.js +# http://localhost:3000 ``` -**Access:** http://localhost:3000 - -### Development Server Features -- โœ… ES6 modules support -- โœ… CORS enabled for online communication -- โœ… Proper MIME types for all file formats -- โœ… Development-friendly caching (assets cached, HTML not cached) -- โœ… Graceful error handling with styled 404 pages - -## ๐Ÿ“ Project Structure - +## ๐Ÿ“ Structure ``` -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ core/ # COMPLETED - Core system (sealed) -โ”‚ โ”‚ โ”œโ”€โ”€ Module.js # Abstract base class -โ”‚ โ”‚ โ”œโ”€โ”€ EventBus.js # Event communication system -โ”‚ โ”‚ โ”œโ”€โ”€ ModuleLoader.js # Dependency injection -โ”‚ โ”‚ โ”œโ”€โ”€ Router.js # Navigation system -โ”‚ โ”‚ โ””โ”€โ”€ index.js # Core exports -โ”‚ โ”œโ”€โ”€ DRS/ # COMPLETED - DRS educational modules -โ”‚ โ”‚ โ”œโ”€โ”€ exercise-modules/ # Educational exercise modules -โ”‚ โ”‚ โ”œโ”€โ”€ services/ # AI and validation services -โ”‚ โ”‚ โ””โ”€โ”€ interfaces/ # Module interfaces -โ”‚ โ”œโ”€โ”€ games/ # SEPARATE - Independent game modules -โ”‚ โ”‚ โ””โ”€โ”€ FlashcardLearning.js # External flashcard game (NOT part of DRS) -โ”‚ โ”œโ”€โ”€ components/ # TODO - UI components -โ”‚ โ”œโ”€โ”€ content/ # TODO - Content system -โ”‚ โ”œโ”€โ”€ styles/ # COMPLETED - Modular CSS -โ”‚ โ”‚ โ”œโ”€โ”€ base.css # Foundation styles -โ”‚ โ”‚ โ””โ”€โ”€ components.css # Reusable UI components -โ”‚ โ””โ”€โ”€ Application.js # COMPLETED - Bootstrap system -โ”œโ”€โ”€ Legacy/ # Archived old system -โ”œโ”€โ”€ index.html # COMPLETED - Entry point -โ”œโ”€โ”€ server.js # COMPLETED - Development server -โ”œโ”€โ”€ start.bat # COMPLETED - Quick start script -โ”œโ”€โ”€ package.json # COMPLETED - Node.js config -โ””โ”€โ”€ README.md # COMPLETED - Documentation +src/ +โ”œโ”€โ”€ core/ # Module, EventBus, ModuleLoader, Router, Application +โ”œโ”€โ”€ DRS/ # Exercise modules, services, interfaces +โ”œโ”€โ”€ games/ # Independent game modules (NOT DRS) +โ”œโ”€โ”€ styles/ # base.css, components.css +โ””โ”€โ”€ Application.js # Bootstrap ``` -## ๐ŸŽฎ Creating New Modules +## ๐Ÿ“š Documentation -### Game Module Template +**Core Guides:** +- `docs/architecture.md` - Architecture principles and patterns +- `docs/creating-new-module.md` - How to create new modules (Game, DRS, Progress) +- `docs/interfaces.md` - Interface system (C++ style contracts) +- `docs/progress-system.md` - Progress tracking and prerequisites + +## ๐Ÿ” Debug ```javascript -import Module from '../core/Module.js'; - -class GameName extends Module { - constructor(name, dependencies, config) { - super(name, ['eventBus']); // Declare dependencies - - // Validate dependencies - if (!dependencies.eventBus) { - throw new Error('GameName requires EventBus dependency'); - } - - this._eventBus = dependencies.eventBus; - this._config = config; - - Object.seal(this); // Prevent modification - } - - async init() { - this._validateNotDestroyed(); - - // Set up event listeners - this._eventBus.on('game:start', this._handleStart.bind(this), this.name); - - this._setInitialized(); - } - - async destroy() { - this._validateNotDestroyed(); - - // Cleanup logic here - - this._setDestroyed(); - } - - // Private methods - _handleStart(event) { - this._validateInitialized(); - // Game logic here - } -} - -export default GameName; -``` - -### Registration in Application.js -```javascript -modules: [ - { - name: 'gameName', - path: './games/GameName.js', - dependencies: ['eventBus'], - config: { difficulty: 'medium' } - } -] -``` - -## ๐Ÿ” Debugging & Monitoring - -### Debug Panel (F12 to toggle) -- System status and uptime -- Loaded modules list -- Event history -- Module registration status - -### Console Access -```javascript -window.app.getStatus() // Application status -window.app.getCore().eventBus // EventBus instance -window.app.getCore().moduleLoader // ModuleLoader instance -window.app.getCore().router // Router instance -``` - -### Common Commands -```bash -# Check module status -window.app.getCore().moduleLoader.getStatus() - -# View event history +// F12 toggles debug panel +window.app.getStatus() window.app.getCore().eventBus.getEventHistory() - -# Navigate programmatically window.app.getCore().router.navigate('/games') ``` -## ๐Ÿšง Next Development Phase - -### Immediate Tasks (PHASE 2) -1. โŒ **Component-based UI System** - Reusable UI components with scoped CSS -2. โŒ **Example Game Module** - Simple memory game to validate architecture -3. โŒ **Content System Integration** - Port content loading from Legacy -4. โŒ **Testing Framework** - Validate module contracts and event flow - -### ๐Ÿ“š DRS Flashcard System - -**VocabularyModule.js** serves as the DRS's integrated flashcard system: -- โœ… **Spaced Repetition** - Again, Hard, Good, Easy difficulty selection -- โœ… **Local Validation** - No AI required, simple string matching with fuzzy logic -- โœ… **Mastery Tracking** - Integration with PrerequisiteEngine -- โœ… **Word Discovery Integration** - WordDiscoveryModule transitions to VocabularyModule -- โœ… **Full UI** - Card-based interface with pronunciation, progress tracking - -**This eliminates the need for external flashcard games in DRS context.** - -### Known Legacy Issues to Fix -31 bug fixes and improvements from the old system: -- Grammar game functionality issues -- Word Storm duration and difficulty problems -- Memory card display issues -- Adventure game text repetition -- UI alignment and feedback issues -- Performance optimizations needed - -## ๐Ÿ”’ Security & Rigidity Enforcement - -### Module Protection Layers -1. **Object.seal()** - Prevents property addition/deletion -2. **Object.freeze()** - Prevents prototype modification -3. **WeakMap privates** - Internal state completely hidden -4. **Abstract enforcement** - Missing methods throw errors -5. **Validation at boundaries** - All inputs validated - -### Error Messages -The system provides explicit error messages for violations: -- "Module is abstract and cannot be instantiated directly" -- "Module name is required and must be a string" -- "EventBus requires module registration before use" -- "Module must be initialized before use" - -## ๐Ÿ“ Development Guidelines - -### DO's -- โœ… Always extend Module base class for game modules -- โœ… Use EventBus for all inter-module communication -- โœ… Validate dependencies in constructor -- โœ… Call `_setInitialized()` after successful init -- โœ… Use private methods with underscore prefix -- โœ… Seal objects to prevent modification -- โœ… **Start with simple solutions first** - Test basic functionality before adding complexity -- โœ… **Test code in console first** - Validate logic with quick console tests before file changes - -### DON'Ts -- โŒ Never access another module's internals directly -- โŒ Never use global variables for communication -- โŒ Never modify Module base class or core system -- โŒ Never skip dependency validation -- โŒ Never use file:// protocol (always use HTTP server) -- โŒ **NEVER HARDCODE JSON PATHS** - Always use dynamic paths based on selected book/chapter -- โŒ **Never overcomplicate positioning logic** - Use simple CSS transforms (translate(-50%, -50%)) for centering before complex calculations - -## ๐Ÿง  Problem-Solving Best Practices - -### UI Positioning Issues -1. **Start Simple**: Use basic CSS positioning (center with transform) first -2. **Test in Console**: Validate positioning logic with `console.log` and direct DOM manipulation -3. **Check Scope**: Ensure variables like `contentLoader` are globally accessible when needed -4. **Cache-bust**: Add `?v=2` to CSS/JS files when browser cache causes issues -5. **Verify Real Dimensions**: Use `getBoundingClientRect()` only when basic centering fails - -### Debugging Workflow -1. **Console First**: Test functions directly in browser console before modifying files -2. **Log Everything**: Add extensive logging to understand execution flow -3. **One Change at a Time**: Make incremental changes and test each step -4. **Simple Solutions Win**: Prefer `left: 50%; transform: translateX(-50%)` over complex calculations +## ๐Ÿ—„๏ธ AI Cache +Cache currently disabled for testing. See `IAEngine.js` lines 165-170. +**Why**: 100-char cache key too short. **Fix**: Improve to 200+ chars with language/exerciseType/contentHash. ## ๐ŸŽฏ Success Metrics +- **<100ms** module loading +- **<50ms** event propagation +- **<200ms** startup +- **Zero** memory leaks +- **Zero** direct coupling -### Architecture Quality -- **Zero direct coupling** between modules -- **100% sealed instances** - no external modification possible -- **Complete test coverage** of module contracts -- **Event-driven communication** only - -### Performance Targets -- **<100ms** module loading time -- **<50ms** event propagation time -- **<200ms** application startup time -- **Zero memory leaks** in module lifecycle - -## ๐Ÿ”„ Migration Notes - -### From Legacy System -The `Legacy/` folder contains the complete old system. Key architectural changes: - -**Old Approach:** -- Global variables and direct coupling -- Manual module registration -- CSS modifications in global files -- Mixed responsibilities in single files - -**New Approach:** -- Strict modules with dependency injection -- Automatic loading with dependency resolution -- Component-scoped CSS injection -- Single responsibility per module - -### Data Migration -- Content modules need adaptation to new Module base class -- Game logic needs EventBus integration -- CSS needs component scoping -- Configuration needs dependency declaration +## ๐Ÿšง Next Phase +1. Component-based UI system +2. Example game module +3. Content system integration +4. Testing framework --- -## ๐Ÿ“‹ COMPREHENSIVE TEST CHECKLIST - -### ๐Ÿ—๏ธ Architecture Tests - -#### Core System Tests -- [ ] **Module.js Tests** - - [ ] Abstract class cannot be instantiated directly - - [ ] WeakMap private data is truly private - - [ ] Object.seal() prevents modification - - [ ] Lifecycle methods work correctly (init, destroy) - - [ ] Validation methods throw appropriate errors - -- [ ] **EventBus.js Tests** - - [ ] Event registration and deregistration - - [ ] Module validation before event usage - - [ ] Event history tracking - - [ ] Cross-module communication isolation - - [ ] Memory leak prevention on module destroy - -- [ ] **ModuleLoader.js Tests** - - [ ] Dependency injection order - - [ ] Circular dependency detection - - [ ] Module initialization sequence - - [ ] Error handling for missing dependencies - - [ ] Module unloading and cleanup - -- [ ] **Router.js Tests** - - [ ] Navigation guards functionality - - [ ] Middleware execution order - - [ ] State management - - [ ] URL parameter handling - - [ ] History management - -- [ ] **Application.js Tests** - - [ ] Auto-bootstrap system - - [ ] Lifecycle management - - [ ] Module registration - - [ ] Error recovery - - [ ] Debug panel functionality - -### ๐ŸŽฎ DRS System Tests - -#### Module Interface Tests -- [ ] **ExerciseModuleInterface Tests** - - [ ] All required methods implemented - - [ ] Method signatures correct - - [ ] Error throwing for abstract methods - -#### Individual Module Tests -- [ ] **TextModule Tests** - - [ ] Text loading and display - - [ ] Question generation/extraction - - [ ] AI validation with fallback - - [ ] Progress tracking - - [ ] UI interaction (buttons, inputs) - - [ ] Viewing time tracking - - [ ] Results calculation - -- [ ] **AudioModule Tests** - - [ ] Audio playback controls - - [ ] Playback counting - - [ ] Transcript reveal timing - - [ ] AI audio analysis - - [ ] Progress tracking - - [ ] Penalty system for excessive playbacks - -- [ ] **ImageModule Tests** - - [ ] Image loading and display - - [ ] Zoom functionality - - [ ] Observation time tracking - - [ ] AI vision analysis - - [ ] Question types (description, details, interpretation) - - [ ] Progress tracking - -- [ ] **GrammarModule Tests** - - [ ] Rule explanation display - - [ ] Exercise type variety (fill-blank, correction, etc.) - - [ ] Hint system - - [ ] Attempt tracking - - [ ] AI grammar analysis - - [ ] Scoring with penalties/bonuses - -### ๐Ÿค– AI Integration Tests - -#### AI Provider Tests -- [ ] **OpenAI Integration** - - [ ] API connectivity test - - [ ] Response format validation - - [ ] Error handling - - [ ] Timeout management - -- [ ] **DeepSeek Integration** - - [ ] API connectivity test - - [ ] Fallback from OpenAI - - [ ] Response format validation - - [ ] Error handling - -- [ ] **AI Fallback System** - - [ ] Provider switching logic - - [ ] Graceful degradation to basic validation - - [ ] Status tracking and reporting - - [ ] Recovery mechanisms - -#### Response Parsing Tests -- [ ] **Structured Response Parsing** - - [ ] [answer]yes/no extraction - - [ ] [explanation] extraction - - [ ] Error handling for malformed responses - - [ ] Multiple format support - -### ๐Ÿ’พ Data Persistence Tests - -#### Progress Tracking Tests -- [ ] **Mastery Tracking** - - [ ] Timestamp recording - - [ ] Metadata storage - - [ ] Progress calculation - - [ ] Persistent storage integration - -- [ ] **Data Merge System** - - [ ] Local vs external data merging - - [ ] Conflict resolution strategies - - [ ] Import/export functionality - - [ ] Data integrity validation - -### ๐ŸŽจ UI/UX Tests - -#### Design Principles Tests -- [ ] **No Scroll Policy** - - [ ] All interfaces fit viewport height - - [ ] Responsive breakpoint testing - - [ ] Mobile viewport compliance - -- [ ] **Responsive Design** - - [ ] Mobile-first approach validation - - [ ] Horizontal space utilization - - [ ] Vertical space conservation - -#### Component Tests -- [ ] **Button Interactions** - - [ ] Hover effects - - [ ] Disabled states - - [ ] Click handlers - - [ ] Loading states - -- [ ] **Form Controls** - - [ ] Input validation - - [ ] Error display - - [ ] Accessibility compliance - - [ ] Keyboard navigation - -### ๐ŸŒ Network & Server Tests - -#### Development Server Tests -- [ ] **ES6 Modules Support** - - [ ] Import/export functionality - - [ ] MIME type handling - - [ ] CORS configuration - -- [ ] **Caching Strategy** - - [ ] Assets cached correctly - - [ ] HTML not cached for development - - [ ] Cache invalidation - -- [ ] **Error Handling** - - [ ] 404 page display - - [ ] Graceful error recovery - - [ ] Error message clarity - -### ๐Ÿ”„ Integration Tests - -#### End-to-End Scenarios -- [ ] **Complete Exercise Flow** - - [ ] Module loading - - [ ] Exercise presentation - - [ ] User interaction - - [ ] AI validation - - [ ] Progress saving - - [ ] Results display - -- [ ] **Multi-Module Navigation** - - [ ] Module switching - - [ ] State preservation - - [ ] Memory cleanup - -- [ ] **Data Persistence Flow** - - [ ] Progress tracking across sessions - - [ ] Data export/import - - [ ] Sync functionality - -### โšก Performance Tests - -#### Loading Performance -- [ ] **Module Loading Times** - - [ ] <100ms module loading - - [ ] <50ms event propagation - - [ ] <200ms application startup - -#### Memory Management -- [ ] **Memory Leaks** - - [ ] Module cleanup verification - - [ ] Event listener removal - - [ ] DOM element cleanup - -### ๐Ÿ”’ Security Tests - -#### Module Isolation Tests -- [ ] **Private State Protection** - - [ ] WeakMap data inaccessible - - [ ] Sealed object modification prevention - - [ ] Cross-module boundary enforcement - -#### Input Validation Tests -- [ ] **Boundary Validation** - - [ ] All inputs validated - - [ ] Error messages for violations - - [ ] Malicious input handling - -### ๐ŸŽฏ TESTING PRIORITY - -#### **HIGH PRIORITY** (Core System) -1. Module.js lifecycle and sealing tests -2. EventBus communication isolation -3. ModuleLoader dependency injection -4. Basic DRS module functionality - -#### **MEDIUM PRIORITY** (Integration) -1. AI provider fallback system -2. Data persistence and merging -3. UI/UX compliance tests -4. End-to-end exercise flows - -#### **LOW PRIORITY** (Polish) -1. Performance benchmarks -2. Advanced security tests -3. Edge case scenarios -4. Browser compatibility - ---- - -## ๐Ÿ—„๏ธ AI Cache Management - -### Current Status -The AI response cache system is **currently disabled** to ensure accurate testing and debugging of the scoring logic. - -### Cache System Overview -The cache improves performance and reduces API costs by storing AI responses for similar prompts. - -**Cache Logic (src/DRS/services/IAEngine.js):** -```javascript -// Lines 165-170: Cache check (currently commented out) -const cacheKey = this._generateCacheKey(prompt, options); -if (this.cache.has(cacheKey)) { - this.stats.cacheHits++; - this._log('๐Ÿ“ฆ Cache hit for educational validation'); - return this.cache.get(cacheKey); -} - -// Lines 198: Cache storage (still active) -this.cache.set(cacheKey, result); -``` - -### โš ๏ธ Why Cache is Disabled -During testing, we discovered the cache key generation uses only the first 100 characters of prompts: -```javascript -_generateCacheKey(prompt, options) { - const keyData = { - prompt: prompt.substring(0, 100), // PROBLEMATIC - Too short - temperature: options.temperature || 0.3, - type: this._detectExerciseType(prompt) - }; - return JSON.stringify(keyData); -} -``` - -**Problems identified:** -- โŒ Different questions with similar beginnings share cache entries -- โŒ False consistency in test results (all 100% same scores) -- โŒ Masks real AI variance and bugs -- โŒ Wrong answers getting cached as correct answers - -### ๐Ÿ”ง How to Re-enable Cache - -**Option 1: Simple Re-activation (Testing Complete)** -```javascript -// In src/DRS/services/IAEngine.js, lines 165-170 -// Uncomment these lines: -const cacheKey = this._generateCacheKey(prompt, options); -if (this.cache.has(cacheKey)) { - this.stats.cacheHits++; - this._log('๐Ÿ“ฆ Cache hit for educational validation'); - return this.cache.get(cacheKey); -} -``` - -**Option 2: Improved Cache Key (Recommended)** -```javascript -_generateCacheKey(prompt, options) { - const keyData = { - prompt: prompt.substring(0, 200), // Increase from 100 to 200 - temperature: options.temperature || 0.3, - type: this._detectExerciseType(prompt), - // Add more distinguishing factors: - language: options.language, - exerciseType: options.exerciseType, - contentHash: this._hashContent(prompt) // Full content hash - }; - return JSON.stringify(keyData); -} -``` - -**Option 3: Selective Caching** -```javascript -// Only cache if prompt is long enough and specific enough -if (prompt.length > 150 && options.exerciseType) { - const cacheKey = this._generateCacheKey(prompt, options); - if (this.cache.has(cacheKey)) { - // ... cache logic - } -} -``` - -### ๐ŸŽฏ Production Recommendations - -**For Production Use:** -1. **Re-enable cache** after comprehensive testing -2. **Improve cache key** to include more context -3. **Monitor cache hit rates** (target: 30-50%) -4. **Set cache expiration** (e.g., 24 hours) -5. **Cache size limits** (currently: 1000 entries) - -**For Development/Testing:** -1. **Keep cache disabled** during AI prompt development -2. **Enable only for performance testing** -3. **Clear cache between test suites** - -### ๐Ÿ“Š Cache Performance Benefits -When properly configured: -- **Cost Reduction**: 40-60% fewer API calls -- **Speed Improvement**: Instant responses for repeated content -- **Rate Limiting**: Avoids API limits during peak usage -- **Reliability**: Reduces dependency on external AI services - -### ๐Ÿ” Cache Monitoring -Access cache statistics: -```javascript -window.app.getCore().iaEngine.stats.cacheHits -window.app.getCore().iaEngine.cache.size -``` - ---- - -## ๐Ÿงช AI Testing Results - -### Final Validation (Without Cache) -**Test Date**: December 2024 -**Scoring Accuracy**: 100% (4/4 test cases passed) - -**Test Results:** -- โœ… **Wrong Science Answer**: 0 points (expected: 0-30) -- โœ… **Correct History Answer**: 90 points (expected: 70-100) -- โœ… **Wrong Translation**: 0 points (expected: 0-30) -- โœ… **Correct Spanish Translation**: 100 points (expected: 70-100) - -**Bug Fixed**: Translation prompt now correctly uses `context.toLang` instead of hardcoded languages. - -**System Status**: โœ… **PRODUCTION READY** -- Real AI scoring (no mock responses) -- Strict scoring logic enforced -- Multi-language support working -- OpenAI โ†’ DeepSeek fallback functional - ---- - -## ๐Ÿง  Intelligent Content Dependency System - -### Smart Vocabulary Prerequisites - -**NEW APPROACH**: Instead of forcing vocabulary based on arbitrary mastery percentages, the system now uses **intelligent content dependency analysis**. - -#### ๐ŸŽฏ **Core Logic** - -**Before executing any exercise, analyze the next content:** - -1. **Content Analysis** - Extract all words from upcoming content (phrases, texts, dialogs) -2. **Dependency Check** - For each word in content: - - Is it in our vocabulary module list? - - Is it already discovered by the user? -3. **Smart Decision** - Only force vocabulary if content has undiscovered words that are in our vocabulary list -4. **Targeted Learning** - Focus vocabulary practice on words actually needed for next content - -#### ๐Ÿ—๏ธ **Implementation Architecture** - -**ContentDependencyAnalyzer Class:** -```javascript -class ContentDependencyAnalyzer { - analyzeContentDependencies(nextContent, vocabularyModule) { - const wordsInContent = this.extractWordsFromContent(nextContent); - const vocabularyWords = vocabularyModule.getVocabularyWords(); - const missingWords = this.findMissingWords(wordsInContent, vocabularyWords); - - return { - hasUnmetDependencies: missingWords.length > 0, - missingWords: missingWords, - totalWordsInContent: wordsInContent.length - }; - } -} -``` - -**Smart Override Logic:** -```javascript -_shouldUseWordDiscovery(exerciseType, exerciseConfig) { - const nextContent = await this.getNextContent(exerciseConfig); - const analysis = this.analyzer.analyzeContentDependencies(nextContent, this.vocabularyModule); - - if (analysis.hasUnmetDependencies) { - window.vocabularyOverrideActive = { - originalType: exerciseConfig.type, - reason: `Content requires ${analysis.missingWords.length} undiscovered words`, - missingWords: analysis.missingWords - }; - return true; - } - return false; -} -``` - -#### ๐ŸŽฏ **User Experience Impact** - -**Before (Dumb System):** -- "Vocabulary mastery too low (15%), forcing flashcards" -- User learns random words not related to next content -- Arbitrary percentage-based decisions - -**After (Smart System):** -- "Next content requires these words: refrigerator, elevator, closet" -- User learns exactly the words needed for comprehension -- Content-driven vocabulary acquisition - -#### ๐Ÿ“Š **Smart Guide Integration** - -**Interface Updates:** -``` -๐Ÿ“š Vocabulary Practice (3 words needed for next content) -Type: ๐Ÿ“š Vocabulary Practice -Mode: Adaptive Flashcards -Why this exercise? -Next content requires these words: refrigerator, elevator, closet. Learning vocabulary first ensures comprehension. -``` - -#### ๐Ÿ”ง **Key Functions** - -- `extractWordsFromContent()` - Parse text/phrases/dialogs for vocabulary -- `findMissingWords()` - Identify vocabulary words that aren't discovered -- `getNextContent()` - Fetch upcoming exercise content for analysis -- `updateVocabularyOverrideUI()` - Smart Guide interface adaptation - -#### โœ… **Benefits** - -- **Targeted Learning** - Only learn words actually needed -- **Context-Driven** - Vocabulary tied to real content usage -- **Efficient Progress** - No time wasted on irrelevant words -- **Better Retention** - Words learned in context of upcoming usage -- **Smart Adaptation** - UI accurately reflects what's happening - -**Status**: โœ… **DESIGN READY FOR IMPLEMENTATION** - ---- - -## ๐Ÿ“Š ROBUST PROGRESS SYSTEM - Ultra-Strict Architecture - -### ๐ŸŽฏ Core Philosophy - -**FUNDAMENTAL RULE**: Every piece of content is a trackable progress item with strict validation and type safety. - -### ๐Ÿ—๏ธ Pedagogical Flow (NON-NEGOTIABLE) - -``` -1. DISCOVERY โ†’ 2. MASTERY โ†’ 3. APPLICATION - (passive) (active) (context) -``` - -**Flow Rules:** -- โŒ **NO Flashcards on undiscovered words** - Must discover first -- โŒ **NO Text exercises on unmastered vocabulary** - Must master first -- โœ… **Always check prerequisites before ANY exercise** -- โœ… **Form vocabulary lists on-the-fly** from next exercise content - -### ๐Ÿ“ฆ Progress Item System - -#### **Item Types & Weights** - -Each content piece = 1 or more progress items with defined weights: - -| Type | Weight | Description | Prerequisites | -|------|--------|-------------|---------------| -| **vocabulary-discovery** | 1 | Passive exposure to new word | None | -| **vocabulary-mastery** | 1 | Active flashcard practice | Must be discovered | -| **phrase** | 6 | Phrase comprehension (3x vocab) | Vocabulary mastered | -| **dialog** | 12 | Dialog comprehension (6x vocab, complex) | Vocabulary mastered | -| **text** | 15 | Full text analysis (7.5x vocab, most complex) | Vocabulary mastered | -| **audio** | 12 | Audio comprehension (6x vocab) | Vocabulary mastered | -| **image** | 6 | Image description (3x vocab) | Vocabulary discovered | -| **grammar** | 6 | Grammar rules (3x vocab) | Vocabulary discovered | - -**Total for 1 vocabulary word** = 2 points (1 discovery + 1 mastery) - -#### **Strict Interface Contract** - -```javascript -// ALL progress items MUST implement this interface -class ProgressItemInterface { - validate() // โš ๏ธ REQUIRED - Validate item data - serialize() // โš ๏ธ REQUIRED - Convert to JSON - getWeight() // โš ๏ธ REQUIRED - Return item weight - canComplete(state) // โš ๏ธ REQUIRED - Check prerequisites -} -``` - -**Missing implementation = FATAL ERROR** (red screen, app refuses to start) - -### ๐Ÿ”ฅ Ultra-Strict Validation System - -#### **Runtime Checks** - -**At Application Startup:** -1. Validate ALL item implementations -2. Check ALL methods are implemented -3. Verify weight calculations -4. Test prerequisite logic - -**If ANY validation fails:** -- ๐Ÿ”ด **Full-screen red error overlay** -- ๐Ÿ”Š **Alert sound** (dev mode) -- ๐Ÿ“ณ **Screen shake animation** -- ๐Ÿšจ **Giant error message with:** - - Class name - - Missing method name - - Stack trace -- โŒ **Application REFUSES to start** - -#### **Error Display Example** - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ โ”‚ -โ”‚ ๐Ÿ”ฅ FATAL ERROR ๐Ÿ”ฅ โ”‚ -โ”‚ โ”‚ -โ”‚ Implementation Missing โ”‚ -โ”‚ โ”‚ -โ”‚ Class: VocabularyMasteryItem โ”‚ -โ”‚ Missing Method: canComplete() โ”‚ -โ”‚ โ”‚ -โ”‚ โŒ MUST implement all interface methods โ”‚ -โ”‚ โ”‚ -โ”‚ [ Dismiss (Fix Required!) ] โ”‚ -โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -**Impossible to ignore. Impossible to skip. Forces correct implementation.** - -### ๐Ÿ“ˆ Progress Calculation - -#### **Chapter Analysis** - -When loading a chapter, the system: - -1. **Scans ALL content** (vocabulary, phrases, dialogs, texts, etc.) -2. **Creates progress items** for each piece -3. **Calculates total weight** (sum of all item weights) -4. **Stores item registry** for tracking - -**Example Chapter:** -- 171 vocabulary words โ†’ 342 points (171ร—2: discovery + mastery) -- 75 phrases โ†’ 450 points (75ร—6) -- 6 dialogs โ†’ 72 points (6ร—12) -- 3 lessons โ†’ 45 points (3ร—15) -- **TOTAL: 909 points** - -#### **Progress Calculation** - -```javascript -percentage = (completedWeight / totalWeight) ร— 100 - -Example: -- Discovered 50 words = 50 points -- Mastered 20 words = 20 points -- Completed 3 phrases = 18 points (3ร—6) -- Completed 1 dialog = 12 points -- Total completed = 100 points -- Progress = (100 / 909) ร— 100 = 11% -``` - -#### **Breakdown Display** - -```javascript -{ - percentage: 11, - completedWeight: 100, - totalWeight: 909, - breakdown: { - 'vocabulary-discovery': { count: 50, weight: 50 }, - 'vocabulary-mastery': { count: 20, weight: 20 }, - 'phrase': { count: 3, weight: 18 }, - 'dialog': { count: 1, weight: 12 } - } -} -``` - -### ๐ŸŽฏ On-The-Fly Vocabulary Formation - -**OLD (Wrong):** Pre-load all 171 words for flashcards - -**NEW (Correct):** -1. **Analyze next exercise** (e.g., Dialog #3) -2. **Extract words used** in that specific dialog -3. **Check user's status** for those words -4. **Form targeted list** of only needed words -5. **Force discovery/mastery** for just those words - -**Example:** -```javascript -// Next exercise: Dialog "Academic Conference" -// Words in dialog: methodology, hypothesis, analysis, paradigm, framework - -// User status check: -// - methodology: never seen โ†’ Discovery needed -// - hypothesis: discovered, not mastered โ†’ Mastery needed -// - analysis: mastered โ†’ Skip -// - paradigm: never seen โ†’ Discovery needed -// - framework: discovered, not mastered โ†’ Mastery needed - -// Smart system creates: -// 1. Discovery module: [methodology, paradigm] (2 words) -// 2. Mastery module: [hypothesis, framework] (2 words) -// 3. Then allow dialog exercise -``` - -### ๐Ÿ”ง Implementation Components - -#### **Required Classes** - -1. **ProgressItemInterface** - Abstract base with strict validation -2. **StrictInterface** - Enforcement mechanism with visual errors -3. **ContentProgressAnalyzer** - Scans content, creates items, calculates total -4. **ProgressTracker** - Manages state, marks completion, saves progress -5. **ImplementationValidator** - Runtime validation at startup - -#### **Concrete Item Implementations** - -- VocabularyDiscoveryItem -- VocabularyMasteryItem -- PhraseItem -- DialogItem -- TextItem -- AudioItem -- ImageItem -- GrammarItem - -**Each MUST implement all 4 interface methods or app fails to start** - -### โœ… Validation Checklist - -**Before ANY exercise can run:** -- [ ] Prerequisites analyzed for next specific content -- [ ] Missing words identified -- [ ] Discovery forced for never-seen words -- [ ] Mastery forced for seen-but-not-mastered words -- [ ] Progress item created with correct weight -- [ ] Completion properly tracked and saved -- [ ] Total progress recalculated - -**If ANY step fails โ†’ Clear error message, app stops gracefully** - -### ๐ŸŽจ UI Integration - -**Progress Display:** -``` -Chapter Progress: 11% (100/909 points) - -โœ… Vocabulary Discovery: 50/171 words (50pts) -โœ… Vocabulary Mastery: 20/171 words (20pts) -โœ… Phrases: 3/75 (18pts) -โœ… Dialogs: 1/6 (12pts) -โฌœ Texts: 0/3 (0/45pts) -``` - -**Smart Guide Updates:** -``` -๐Ÿ” Analyzing next exercise: Dialog "Academic Conference" -๐Ÿ“š 4 words needed (2 discovery, 2 mastery) -๐ŸŽฏ Starting Vocabulary Discovery for: methodology, paradigm -``` - -### ๐Ÿšจ Error Prevention - -**Compile-Time (Startup):** -- Interface validation -- Method implementation checks -- Weight configuration validation - -**Runtime:** -- Prerequisite enforcement -- State consistency checks -- Progress calculation validation - -**Visual Feedback:** -- Red screen for missing implementations -- Clear prerequisite errors -- Progress breakdown always visible - ---- - -**Status**: ๐Ÿ“ **DOCUMENTED - READY FOR IMPLEMENTATION** - ---- - -## ๐Ÿ”’ STRICT INTERFACE SYSTEM - C++ Style Contracts - -### ๐ŸŽฏ Philosophy: Contract-Driven Architecture - -**Like C++ header files (.h)**, we enforce strict interfaces that MUST be implemented. Any missing method = **RED SCREEN ERROR** at startup. - -### ๐Ÿ“ฆ Interface Hierarchy - -``` -StrictInterface (base) -โ”œโ”€โ”€ ProgressItemInterface # For progress tracking items -โ”‚ โ”œโ”€โ”€ VocabularyDiscoveryItem -โ”‚ โ”œโ”€โ”€ VocabularyMasteryItem -โ”‚ โ””โ”€โ”€ ContentProgressItems (Phrase, Dialog, Text, etc.) -โ”‚ -โ”œโ”€โ”€ ProgressSystemInterface # For progress systems -โ”‚ โ”œโ”€โ”€ ProgressTracker -โ”‚ โ””โ”€โ”€ PrerequisiteEngine -โ”‚ -โ””โ”€โ”€ DRSExerciseInterface # For exercise modules - โ”œโ”€โ”€ VocabularyModule - โ”œโ”€โ”€ TextAnalysisModule - โ”œโ”€โ”€ GrammarAnalysisModule - โ”œโ”€โ”€ TranslationModule - โ””โ”€โ”€ OpenResponseModule -``` - -### ๐Ÿ—๏ธ 1. **StrictInterface** (Base Class) - -**Location**: `src/DRS/interfaces/StrictInterface.js` - -**Purpose**: Ultra-strict base class with visual error enforcement - -**Features**: -- โœ… Validates implementation at construction -- โœ… Full-screen red error overlay if method missing -- โœ… Sound alert in dev mode -- โœ… Screen shake animation -- โœ… Impossible to ignore - forces correct implementation - -**Error Display**: -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ โ”‚ -โ”‚ ๐Ÿ”ฅ FATAL ERROR ๐Ÿ”ฅ โ”‚ -โ”‚ โ”‚ -โ”‚ Implementation Missing โ”‚ -โ”‚ โ”‚ -โ”‚ Class: VocabularyModule โ”‚ -โ”‚ Missing Method: validate() โ”‚ -โ”‚ โ”‚ -โ”‚ โŒ MUST implement all interface methods โ”‚ -โ”‚ โ”‚ -โ”‚ [ DISMISS (Fix Required!) ] โ”‚ -โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -### ๐ŸŽฏ 2. **ProgressItemInterface** - -**Location**: `src/DRS/interfaces/ProgressItemInterface.js` - -**Purpose**: Contract for all progress tracking items - -**Required Methods** (4): -```javascript -validate() // Validate item data -serialize() // Convert to JSON -getWeight() // Return item weight for progress calculation -canComplete(state) // Check prerequisites -``` - -**Implementations**: -- VocabularyDiscoveryItem (1pt) -- VocabularyMasteryItem (1pt) -- PhraseItem (6pts) -- DialogItem (12pts) -- TextItem (15pts) -- AudioItem (12pts) -- ImageItem (6pts) -- GrammarItem (6pts) - -### ๐Ÿ”ง 3. **ProgressSystemInterface** - -**Location**: `src/DRS/interfaces/ProgressSystemInterface.js` - -**Purpose**: Contract for all progress management systems - -**Required Methods** (17): - -**Vocabulary Tracking:** -- `markWordDiscovered(word, metadata)` -- `markWordMastered(word, metadata)` -- `isWordDiscovered(word)` -- `isWordMastered(word)` - -**Content Tracking:** -- `markPhraseCompleted(id, metadata)` -- `markDialogCompleted(id, metadata)` -- `markTextCompleted(id, metadata)` -- `markAudioCompleted(id, metadata)` -- `markImageCompleted(id, metadata)` -- `markGrammarCompleted(id, metadata)` - -**Prerequisites:** -- `canComplete(itemType, itemId, context)` - -**Progress:** -- `getProgress(chapterId)` - -**Persistence:** -- `saveProgress(bookId, chapterId)` -- `loadProgress(bookId, chapterId)` - -**Utility:** -- `reset(bookId, chapterId)` - -**Implementations**: -- ProgressTracker - Weight-based progress with items -- PrerequisiteEngine - Prerequisite checking and mastery tracking - -### ๐ŸŽฎ 4. **DRSExerciseInterface** - -**Location**: `src/DRS/interfaces/DRSExerciseInterface.js` - -**Purpose**: Contract for all DRS exercise modules - -**Required Methods** (10): - -**Lifecycle:** -- `init(config, content)` - Initialize exercise -- `render(container)` - Render UI -- `destroy()` - Clean up - -**Exercise Logic:** -- `validate(userAnswer)` - Validate answer, return { isCorrect, score, feedback, explanation } -- `getResults()` - Return { score, attempts, timeSpent, completed, details } -- `handleUserInput(event, data)` - Handle user input - -**Progress Tracking:** -- `markCompleted(results)` - Mark as completed -- `getProgress()` - Return { percentage, currentStep, totalSteps, itemsCompleted, itemsTotal } - -**Metadata:** -- `getExerciseType()` - Return exercise type string -- `getExerciseConfig()` - Return { type, difficulty, estimatedTime, prerequisites, metadata } - -**Implementations**: -- VocabularyModule - Flashcard spaced repetition -- TextAnalysisModule - AI-powered text comprehension -- GrammarAnalysisModule - AI grammar correction -- TranslationModule - AI translation validation -- OpenResponseModule - Free-form AI evaluation - -### โœ… 5. **ImplementationValidator** - -**Location**: `src/DRS/services/ImplementationValidator.js` - -**Purpose**: Validate ALL implementations at application startup - -**Validation Phases**: - -```javascript -๐Ÿ” VALIDATING DRS IMPLEMENTATIONS... - -๐Ÿ“ฆ PART 1: Validating Progress Items... - โœ… VocabularyDiscoveryItem - OK - โœ… VocabularyMasteryItem - OK - โœ… PhraseItem - OK - โœ… DialogItem - OK - โœ… TextItem - OK - โœ… AudioItem - OK - โœ… ImageItem - OK - โœ… GrammarItem - OK - -๐Ÿ”ง PART 2: Validating Progress Systems... - โœ… ProgressTracker - OK - โœ… PrerequisiteEngine - OK - -๐ŸŽฎ PART 3: Validating DRS Exercise Modules... - โœ… VocabularyModule - OK - โœ… TextAnalysisModule - OK - โœ… GrammarAnalysisModule - OK - โœ… TranslationModule - OK - โœ… OpenResponseModule - OK - -โœ… ALL DRS IMPLEMENTATIONS VALID -``` - -**If ANY validation fails**: -- ๐Ÿ”ด Full-screen red error -- ๐Ÿšซ Application REFUSES to start -- ๐Ÿ“‹ Clear error message with missing method name -- ๐Ÿ”Š Alert sound (dev mode) -- ๐Ÿ“ณ Screen shake - -### ๐ŸŽฏ Integration with Application.js - -**At Startup** (lines 55-62): -```javascript -// Validate all progress item implementations (STRICT MODE) -console.log('๐Ÿ” Validating progress item implementations...'); -const { default: ImplementationValidator } = await import('./DRS/services/ImplementationValidator.js'); -const isValid = await ImplementationValidator.validateAll(); - -if (!isValid) { - throw new Error('โŒ Implementation validation failed - check console for details'); -} -``` - -### ๐Ÿ“‹ Creating New Implementations - -#### **New Progress Item**: -```javascript -import ProgressItemInterface from '../interfaces/ProgressItemInterface.js'; - -class MyCustomItem extends ProgressItemInterface { - constructor(id, metadata) { - super('my-custom-item', id, metadata); - } - - validate() { - // MUST implement - if (!this.metadata.required) { - throw new Error('Missing required data'); - } - return true; - } - - serialize() { - // MUST implement - return { - ...this._getBaseSerialization(), - customData: this.metadata.custom - }; - } - - getWeight() { - // MUST implement - return ProgressItemInterface.WEIGHTS['my-custom-item'] || 5; - } - - canComplete(userProgress) { - // MUST implement - return true; // Check prerequisites here - } -} -``` - -#### **New Progress System**: -```javascript -import ProgressSystemInterface from '../interfaces/ProgressSystemInterface.js'; - -class MyProgressSystem extends ProgressSystemInterface { - constructor() { - super('MyProgressSystem'); - } - - // MUST implement all 17 required methods - async markWordDiscovered(word, metadata = {}) { /* ... */ } - async markWordMastered(word, metadata = {}) { /* ... */ } - isWordDiscovered(word) { /* ... */ } - isWordMastered(word) { /* ... */ } - // ... 13 more methods -} -``` - -#### **New Exercise Module**: -```javascript -import DRSExerciseInterface from '../interfaces/DRSExerciseInterface.js'; - -class MyExerciseModule extends DRSExerciseInterface { - constructor() { - super('MyExerciseModule'); - } - - // MUST implement all 10 required methods - async init(config, content) { /* ... */ } - async render(container) { /* ... */ } - async destroy() { /* ... */ } - async validate(userAnswer) { /* ... */ } - getResults() { /* ... */ } - handleUserInput(event, data) { /* ... */ } - async markCompleted(results) { /* ... */ } - getProgress() { /* ... */ } - getExerciseType() { return 'my-exercise'; } - getExerciseConfig() { /* ... */ } -} -``` - -### ๐Ÿšจ Enforcement Rules - -**NON-NEGOTIABLE**: -1. โŒ **Missing method** โ†’ RED SCREEN ERROR โ†’ App refuses to start -2. โŒ **Wrong signature** โ†’ Runtime error on call -3. โŒ **Wrong return format** โ†’ Runtime error on usage -4. โœ… **All methods implemented** โ†’ App starts normally - -**Validation happens**: -- โœ… At application startup (before any UI renders) -- โœ… On module registration -- โœ… At interface instantiation - -**Benefits**: -- ๐Ÿ›ก๏ธ **Impossible to forget implementation** - Visual error forces fix -- ๐Ÿ“‹ **Self-documenting** - Interface defines exact contract -- ๐Ÿ”’ **Type safety** - Like TypeScript interfaces but enforced at runtime -- ๐Ÿงช **Testable** - Can mock interfaces for unit tests -- ๐Ÿ”„ **Maintainable** - Adding new method = update interface + all implementations get errors - ---- - -**Status**: โœ… **INTERFACE SYSTEM IMPLEMENTED** - ---- - -**This is a high-quality, maintainable system built for educational software that will scale.** \ No newline at end of file +**High-quality, maintainable educational software that scales.** diff --git a/content/books/ledu.json b/content/books/ledu.json new file mode 100644 index 0000000..5961d1c --- /dev/null +++ b/content/books/ledu.json @@ -0,0 +1,206 @@ +{ + "id": "ledu", + "name": "ไน่ฏป (Lรจ dรบ) - Chinese Reading Course", + "description": "Comprehensive Chinese reading course designed for intermediate learners focusing on reading comprehension, vocabulary acquisition, and cultural understanding", + "difficulty": "intermediate", + "language": "zh-CN", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "Jiaotong University Chinese Program", + "target_level": "intermediate", + "total_estimated_hours": 120, + "prerequisites": ["basic-chinese", "hsk-3"], + "learning_objectives": [ + "Master intermediate Chinese vocabulary for daily life contexts", + "Develop reading comprehension skills with authentic Chinese texts", + "Understand Chinese character structures and radicals", + "Practice inferring meaning from context", + "Learn Chinese cultural concepts through reading" + ], + "content_tags": ["chinese", "reading", "vocabulary", "comprehension", "culture"], + "total_chapters": 12, + "available_chapters": [ + "ledu-chapter1", + "ledu-chapter2", + "ledu-chapter3", + "ledu-chapter4", + "ledu-chapter5", + "ledu-chapter6", + "ledu-chapter7", + "ledu-chapter8", + "ledu-chapter9", + "ledu-chapter10", + "ledu-chapter11", + "ledu-chapter12" + ], + "completion_criteria": { + "overall_progress": 85, + "chapters_completed": 12, + "vocabulary_mastery": 90, + "comprehension_score": 80 + } + }, + "chapters": [ + { + "id": "ledu-chapter1", + "chapter_number": "1", + "name": "ๆฐ‘ไปฅ้ฃŸไธบๅคฉ (Food is Heaven for the People)", + "description": "Introduction to Chinese food culture and dietary vocabulary", + "estimated_hours": 10, + "difficulty": "intermediate", + "prerequisites": ["hsk-3"], + "learning_objectives": [ + "Master food-related vocabulary", + "Understand Chinese dining etiquette", + "Learn to infer character meanings from radicals", + "Practice reading authentic texts about Chinese cuisine" + ], + "vocabulary_count": 45, + "phrases_count": 20, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter2", + "chapter_number": "2", + "name": "Chapter 2", + "description": "Second chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter3", + "chapter_number": "3", + "name": "็”Ÿๅ‘ฝๅœจไบŽ่ฟๅŠจ (Life Lies in Movement)", + "description": "Comprehensive chapter on sports, fitness, and healthy lifestyle with focus on forming exercise habits", + "estimated_hours": 10, + "difficulty": "intermediate", + "prerequisites": ["ledu-chapter1", "ledu-chapter2"], + "learning_objectives": [ + "Master 30+ sports and fitness vocabulary terms", + "Understand strategies for building exercise habits", + "Learn about ping-pong history and Chinese sports culture", + "Practice reading comprehension with authentic texts", + "Develop skills in contextual vocabulary inference" + ], + "vocabulary_count": 35, + "phrases_count": 15, + "texts_count": 3, + "exercises_count": 20 + }, + { + "id": "ledu-chapter4", + "chapter_number": "4", + "name": "Chapter 4", + "description": "Fourth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 38, + "phrases_count": 17, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter5", + "chapter_number": "5", + "name": "Chapter 5", + "description": "Fifth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter6", + "chapter_number": "6", + "name": "Chapter 6", + "description": "Sixth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 42, + "phrases_count": 19, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter7", + "chapter_number": "7", + "name": "Chapter 7", + "description": "Seventh chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter8", + "chapter_number": "8", + "name": "Chapter 8", + "description": "Eighth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 38, + "phrases_count": 17, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter9", + "chapter_number": "9", + "name": "Chapter 9", + "description": "Ninth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter10", + "chapter_number": "10", + "name": "Chapter 10", + "description": "Tenth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 42, + "phrases_count": 19, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter11", + "chapter_number": "11", + "name": "Chapter 11", + "description": "Eleventh chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 40, + "phrases_count": 18, + "texts_count": 3, + "exercises_count": 15 + }, + { + "id": "ledu-chapter12", + "chapter_number": "12", + "name": "Chapter 12", + "description": "Twelfth chapter of LEDU reading course", + "estimated_hours": 10, + "difficulty": "intermediate", + "vocabulary_count": 38, + "phrases_count": 17, + "texts_count": 3, + "exercises_count": 15 + } + ] +} diff --git a/content/chapters/ledu-chapter1.json b/content/chapters/ledu-chapter1.json new file mode 100644 index 0000000..bff380e --- /dev/null +++ b/content/chapters/ledu-chapter1.json @@ -0,0 +1,548 @@ +{ + "id": "ledu-chapter1", + "book_id": "ledu", + "name": "ๆฐ‘ไปฅ้ฃŸไธบๅคฉ (Food is Heaven for the People)", + "description": "Introduction to Chinese food culture, dining etiquette, and dietary vocabulary. Explores the importance of food in Chinese culture and regional taste preferences.", + "difficulty": "intermediate", + "language": "zh-CN", + "chapter_number": "1", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "LEDU Textbook - Jiaotong University", + "target_level": "intermediate", + "estimated_hours": 10, + "prerequisites": ["hsk-3"], + "learning_objectives": [ + "Master 45+ food and dining vocabulary terms", + "Understand Chinese dining etiquette and table manners", + "Learn about regional taste differences in China", + "Practice character inference from radicals", + "Develop reading comprehension skills with authentic texts" + ], + "content_tags": ["food", "culture", "etiquette", "regional-cuisine", "chinese-culture"], + "completion_criteria": { + "vocabulary_mastery": 90, + "comprehension_score": 80, + "exercises_completed": 15 + } + }, + "vocabulary": { + "้ฃŸ": { + "pronunciation": "shรญ", + "type": "morpheme", + "user_language": "food, to eat", + "examples": ["้ฅฎ้ฃŸ", "็”œ้ฃŸ", "่‚‰้ฃŸ", "้ฃŸๅ ‚"], + "notes": "Pictographic character representing food in a container. Usually used as morpheme." + }, + "้‡": { + "pronunciation": "zhรฒng", + "type": "adjective/verb", + "user_language": "heavy; important; to attach importance to", + "examples": ["ไฝ“้‡", "ไธฅ้‡", "้‡่ฆ", "ๆ•ฌ้‡"] + }, + "ๅ˜ด": { + "pronunciation": "zuว", + "type": "noun", + "user_language": "mouth", + "examples": ["ไธ€ๅผ ๅ˜ด", "ๅผ ๅผ€ๅ˜ด", "็ƒซๅ˜ด"] + }, + "่„‘": { + "pronunciation": "nวŽo", + "type": "noun", + "user_language": "brain", + "examples": ["ๅคง่„‘", "่„‘ๅญ"] + }, + "้‡่ง†": { + "pronunciation": "zhรฒngshรฌ", + "type": "verb", + "user_language": "to attach importance to, to value", + "examples": ["้‡่ง†ๅฅๅบท", "้‡่ง†ๅญฆไน ", "ๅฏนโ€ฆๅพˆ/ไธ้‡่ง†"] + }, + "่ฅๅ…ป": { + "pronunciation": "yรญngyวŽng", + "type": "noun", + "user_language": "nutrition", + "examples": ["ๆœ‰่ฅๅ…ป", "่ฅๅ…ปไธฐๅฏŒ"] + }, + "ๆ–‡ๅŒ–": { + "pronunciation": "wรฉnhuร ", + "type": "noun", + "user_language": "culture", + "examples": ["้ฅฎ้ฃŸๆ–‡ๅŒ–", "ไผ ็ปŸๆ–‡ๅŒ–"] + }, + "ไธฐๅฏŒ": { + "pronunciation": "fฤ“ngfรน", + "type": "adjective/verb", + "user_language": "rich, abundant; to enrich", + "examples": ["่ฅๅ…ปไธฐๅฏŒ", "ไธฐๅฏŒ็”Ÿๆดป"] + }, + "ๅ‘ณ้“": { + "pronunciation": "wรจidao", + "type": "noun", + "user_language": "taste, flavor", + "examples": ["ๅ‘ณ้“้ฒœ็พŽ", "ๅ“ๅฐๅ‘ณ้“"] + }, + "้บป่พฃ็ƒซ": { + "pronunciation": "mรกlร tร ng", + "type": "noun", + "user_language": "spicy hot pot" + }, + "้บปๅฉ†่ฑ†่…": { + "pronunciation": "mรกpรณ dรฒufu", + "type": "noun", + "user_language": "Mapo tofu (tofu in spicy sauce)" + }, + "้€‚ๅˆ": { + "pronunciation": "shรฌhรฉ", + "type": "verb", + "user_language": "to suit, to be suitable for", + "examples": ["่ฟ™็ง้ขœ่‰ฒไธ้€‚ๅˆๆˆ‘", "ไป–ๅพˆ้€‚ๅˆๅš่ฟ™ไธชๅทฅไฝœ"] + }, + "ๆปก": { + "pronunciation": "mวŽn", + "type": "adjective", + "user_language": "full, complete", + "examples": ["ๆปกๆกŒ", "ๅๆปก", "ๆปกๅฟƒๆฌขๅ–œ"] + }, + "ๅ››ๅท": { + "pronunciation": "Sichuฤn", + "type": "proper noun", + "user_language": "Sichuan (province of China)" + }, + "ๅކๅฒ": { + "pronunciation": "lishว", + "type": "noun", + "user_language": "history" + }, + "่ง„็Ÿฉ": { + "pronunciation": "guฤซju", + "type": "noun", + "user_language": "rule, norm, convention" + }, + "ๅฎ‰ๆŽ’": { + "pronunciation": "ฤnpรกi", + "type": "verb", + "user_language": "to arrange, to organize" + }, + "ๆ•ฒ": { + "pronunciation": "qiฤo", + "type": "verb", + "user_language": "to knock, to tap" + }, + "็ซ–": { + "pronunciation": "shรน", + "type": "adjective", + "user_language": "vertical, upright" + }, + "็คผ่ฒŒ": { + "pronunciation": "lวmร o", + "type": "adjective", + "user_language": "polite, courteous" + }, + "ๅ€’": { + "pronunciation": "dร o", + "type": "verb", + "user_language": "to pour" + }, + "ๅฐŠ้‡": { + "pronunciation": "zลซnzhรฒng", + "type": "verb", + "user_language": "to respect, to esteem" + }, + "ๆ•ฌ้…’": { + "pronunciation": "jรฌng jiว”", + "type": "verb", + "user_language": "to propose a toast" + }, + "ไฝ†": { + "pronunciation": "dร n", + "type": "conjunction", + "user_language": "but (written language)", + "examples": ["่ฟ™ๅฎถ้ฅญ้ฆ†ไธๅคง๏ผŒไฝ†ๅพˆๆœ‰ๅ", "่ฟ™ไปฝๅทฅไฝœๆฏ”่พƒ่พ›่‹ฆ๏ผŒไฝ†ๆˆ‘ๅพˆๅ–œๆฌข"], + "notes": "Written Chinese, same as ไฝ†ๆ˜ฏ" + }, + "ๆ—ถ": { + "pronunciation": "shรญ", + "type": "noun", + "user_language": "time, moment (written language)", + "examples": ["ไป–็œ‹ไนฆๆ—ถๅ–œๆฌขๅฌ้Ÿณไน", "ๅทฅไฝœๆ—ถไป–ๅพˆ่ฎค็œŸ"], + "notes": "Written Chinese, means 'โ€ฆโ€ฆ็š„ๆ—ถๅ€™'" + }, + "่ฎฒ็ฉถ": { + "pronunciation": "jiวŽngju", + "type": "verb", + "user_language": "to be particular about, to pay attention to" + }, + "่‰ฒ": { + "pronunciation": "sรจ", + "type": "noun", + "user_language": "color" + }, + "้ฆ™": { + "pronunciation": "xiฤng", + "type": "adjective", + "user_language": "fragrant, aromatic" + }, + "ๅฝข": { + "pronunciation": "xรญng", + "type": "noun", + "user_language": "shape, form" + }, + "ๆ„": { + "pronunciation": "yรฌ", + "type": "noun", + "user_language": "meaning, significance" + }, + "้ขœ่‰ฒ": { + "pronunciation": "yรกnsรจ", + "type": "noun", + "user_language": "color" + }, + "ๆผ‚ไบฎ": { + "pronunciation": "piร oliang", + "type": "adjective", + "user_language": "beautiful, pretty" + }, + "ๅฃๆ„Ÿ": { + "pronunciation": "kว’ugวŽn", + "type": "noun", + "user_language": "taste, mouthfeel" + }, + "ๆ ทๅญ": { + "pronunciation": "yร ngzi", + "type": "noun", + "user_language": "appearance, look" + }, + "ๆ„ไน‰": { + "pronunciation": "yรฌyรฌ", + "type": "noun", + "user_language": "meaning, significance" + }, + "ไปฅๅ‰": { + "pronunciation": "yวqiรกn", + "type": "noun", + "user_language": "before, previously" + }, + "็”œ": { + "pronunciation": "tiรกn", + "type": "adjective", + "user_language": "sweet" + }, + "ๅ’ธ": { + "pronunciation": "xiรกn", + "type": "adjective", + "user_language": "salty" + }, + "่พฃ": { + "pronunciation": "lร ", + "type": "adjective", + "user_language": "spicy" + }, + "้…ธ": { + "pronunciation": "suฤn", + "type": "adjective", + "user_language": "sour" + }, + "ๅ„ๅœฐ": { + "pronunciation": "gรจdรฌ", + "type": "noun", + "user_language": "various places, everywhere" + }, + "็ˆฑไธŠ": { + "pronunciation": "ร ishang", + "type": "verb", + "user_language": "to fall in love with" + }, + "่พฃๆค’": { + "pronunciation": "lร jiฤo", + "type": "noun", + "user_language": "chili pepper" + }, + "็ฆปไธๅผ€": { + "pronunciation": "lรญ bu kฤi", + "type": "verb", + "user_language": "can't do without" + }, + "้บป": { + "pronunciation": "mรก", + "type": "adjective", + "user_language": "numbing (Sichuan pepper sensation)" + }, + "ๆœ€็ˆฑ": { + "pronunciation": "zuรฌ'ร i", + "type": "noun", + "user_language": "favorite" + }, + "็ซ้”…": { + "pronunciation": "huว’guล", + "type": "noun", + "user_language": "hot pot" + }, + "ๅฃๅ‘ณ": { + "pronunciation": "kว’uwรจi", + "type": "noun", + "user_language": "taste preference" + }, + "ๆธฉๆš–": { + "pronunciation": "wฤ“nnuวŽn", + "type": "adjective", + "user_language": "warm" + }, + "ๅฟซไน": { + "pronunciation": "kuร ilรจ", + "type": "adjective", + "user_language": "happy" + }, + "้ฃŸๆฌฒ": { + "pronunciation": "shรญyรน", + "type": "noun", + "user_language": "appetite" + }, + "ๅ–œ็ˆฑ": { + "pronunciation": "xว'ร i", + "type": "verb", + "user_language": "to like, to love" + }, + "ๅคไบบ": { + "pronunciation": "gว”rรฉn", + "type": "noun", + "user_language": "ancient people, ancestors" + }, + "ไบ‹ๆƒ…": { + "pronunciation": "shรฌqing", + "type": "noun", + "user_language": "matter, thing, affair" + }, + "ๅนด้•ฟ": { + "pronunciation": "niรกnzhวŽng", + "type": "adjective", + "user_language": "elderly, senior" + }, + "ๅ…ฅๅบง": { + "pronunciation": "rรนzuรฒ", + "type": "verb", + "user_language": "to take a seat" + }, + "ไธปไบบ": { + "pronunciation": "zhว”rรฉn", + "type": "noun", + "user_language": "host" + }, + "ๅŠจ็ญทๅญ": { + "pronunciation": "dรฒng kuร izi", + "type": "verb phrase", + "user_language": "to start eating (lit. move chopsticks)" + }, + "ๅ“ๅฐ": { + "pronunciation": "pวnchรกng", + "type": "verb", + "user_language": "to taste" + }, + "ๅคน่œ": { + "pronunciation": "jiฤcร i", + "type": "verb", + "user_language": "to pick up food with chopsticks" + }, + "็›˜ๅญ": { + "pronunciation": "pรกnzi", + "type": "noun", + "user_language": "plate" + }, + "็ƒญๆƒ…": { + "pronunciation": "rรจqรญng", + "type": "noun", + "user_language": "enthusiasm, warmth" + }, + "็ข—": { + "pronunciation": "wวŽn", + "type": "noun", + "user_language": "bowl" + }, + "ๆ’": { + "pronunciation": "chฤ", + "type": "verb", + "user_language": "to insert, to stick in" + }, + "็ฑณ้ฅญ": { + "pronunciation": "mวfร n", + "type": "noun", + "user_language": "rice" + }, + "ๅŒๆ ท": { + "pronunciation": "tรณngyร ng", + "type": "adverb", + "user_language": "likewise, similarly" + }, + "้•ฟ่พˆ": { + "pronunciation": "zhวŽngbรจi", + "type": "noun", + "user_language": "elder, senior" + }, + "็ƒซๆ‰‹": { + "pronunciation": "tร ng shว’u", + "type": "verb phrase", + "user_language": "to burn one's hand" + }, + "้…’": { + "pronunciation": "jiว”", + "type": "noun", + "user_language": "alcohol, wine" + }, + "ๅ‹ๆƒ…": { + "pronunciation": "yว’uqรญng", + "type": "noun", + "user_language": "friendship" + }, + "ๅŒๆ‰‹": { + "pronunciation": "shuฤngshว’u", + "type": "noun", + "user_language": "both hands" + }, + "ๆฏๅญ": { + "pronunciation": "bฤ“izi", + "type": "noun", + "user_language": "cup, glass" + }, + "ๅฏนๆ–น": { + "pronunciation": "duรฌfฤng", + "type": "noun", + "user_language": "the other party" + }, + "ๅƒ้ฅฑ": { + "pronunciation": "chฤซ bวŽo", + "type": "verb", + "user_language": "to eat one's fill" + }, + "ๆ„Ÿ่ฐข": { + "pronunciation": "gวŽnxiรจ", + "type": "verb", + "user_language": "to thank" + } + }, + "grammar": { + "ๅ› ๆญค-therefore": { + "title": "โ€ฆโ€ฆ๏ผŒๅ› ๆญคโ€ฆโ€ฆ - therefore, consequently", + "pattern": "Cause/Reason + ๅ› ๆญค + Result/Consequence", + "explanation": "Used to express cause and effect relationship. More formal than ๆ‰€ไปฅ.", + "examples": [ + { + "chinese": "่ฟ™ๅฎถ้ฅญ้ฆ†ๅพˆๆœ‰็‰น่‰ฒ๏ผŒๅ› ๆญค็”Ÿๆ„็‰นๅˆซ็ซใ€‚", + "pronunciation": "Zhรจ jiฤ fร nguวŽn hฤ›n yว’u tรจsรจ, yฤซncว shฤ“ngyรฌ tรจbiรฉ huว’.", + "translation": "This restaurant has specialties, therefore the business is very good." + }, + { + "chinese": "่ฟ™ไธช็‰Œๅญ็š„้ฅฎๆ–™ๅฃๆ„Ÿๅพˆๅฅฝ๏ผŒๅ› ๆญคๅพˆๅ—ๅนด่ฝปไบบๆฌข่ฟŽใ€‚", + "pronunciation": "Zhรจge pรกizi de yวnliร o kว’ugวŽn hฤ›n hวŽo, yฤซncว hฤ›n shรฒu niรกnqฤซngrรฉn huฤnyรญng.", + "translation": "This brand of beverage has a good taste, therefore it's very popular among young people." + } + ] + }, + "ไฝ†-written": { + "title": "ไฝ† (dร n) - but (written language)", + "explanation": "Used in written Chinese to mean 'ไฝ†ๆ˜ฏ' (but). More formal and concise.", + "examples": [ + { + "chinese": "่ฟ™ๅฎถ้ฅญ้ฆ†ไธๅคง๏ผŒไฝ†ๅพˆๆœ‰ๅใ€‚", + "pronunciation": "Zhรจ jiฤ fร nguวŽn bรน dร , dร n hฤ›n yว’umรญng.", + "translation": "This restaurant is not big, but it's very famous." + } + ] + }, + "ๆ—ถ-written": { + "title": "ๆ—ถ (shรญ) - when, at the time of (written language)", + "explanation": "Used in written Chinese to mean 'โ€ฆโ€ฆ็š„ๆ—ถๅ€™' (when, at the time of).", + "examples": [ + { + "chinese": "ไป–็œ‹ไนฆๆ—ถๅ–œๆฌขๅฌ้Ÿณไนใ€‚", + "pronunciation": "Tฤ kร n shลซ shรญ xวhuan tฤซng yฤซnyuรจ.", + "translation": "When he reads, he likes listening to music." + } + ] + } + }, + "texts": [ + { + "id": "main-text", + "title": "ไธญๅ›ฝไบบ็”จ\"ๅ˜ด\"ๅƒ้ฅญ (Chinese People Eat with Their 'Mouth')", + "type": "main", + "content": "ๆœ‰ไบบ่ฏด๏ผš\"ๅค–ๅ›ฝไบบ็”จ'่„‘'ๅƒ้ฅญ๏ผŒไธญๅ›ฝไบบ็”จ'ๅ˜ด'ๅƒ้ฅญใ€‚\"ๅค–ๅ›ฝไบบๆฏ”่พƒ้‡่ง†่ฅๅ…ป๏ผŒๅƒไป€ไนˆใ€ๆ€Žไนˆๅƒ้ฆ–ๅ…ˆๆƒณ็š„ๆ˜ฏ่ฆๆœ‰่ฅๅ…ป๏ผ›ไธญๅ›ฝไบบ่ฎฒ็ฉถ่œ่ฆ่‰ฒใ€้ฆ™ใ€ๅ‘ณใ€ๅฝขใ€ๆ„้ƒฝๅฅฝใ€‚ไธญๅŽ้ฅฎ้ฃŸๆ–‡ๅŒ–้žๅธธไธฐๅฏŒ๏ผŒไธ€้“่œ่ฆ้ขœ่‰ฒๆผ‚ไบฎใ€ๅ‘ณ้“้ฆ™ใ€ๅฃๆ„Ÿๅฅฝใ€ๆ ทๅญๅฅฝ็œ‹๏ผŒ่ฟ˜่ฆๆœ‰็พŽๅฅฝ็š„ๆ„ไน‰ใ€‚\n\n่‰ฒใ€้ฆ™ใ€ๅ‘ณใ€ๅฝขใ€ๆ„ไธญ๏ผŒๅ‘ณๆ˜ฏ็ฌฌไธ€ไฝ็š„ใ€‚ไปฅๅ‰ไธญๅ›ฝๆœ‰ๅฅ่ฏ่ฏด๏ผš\"ๅ—็”œๅŒ—ๅ’ธ๏ผŒไธœ่พฃ่ฅฟ้…ธใ€‚\"ๆ„ๆ€ๆ˜ฏๅ„ๅœฐ็š„ไบบไปฌๅ„ๆœ‰ๆ‰€็ˆฑใ€‚่€Œ็Žฐๅœจๆœ‰่ถŠๆฅ่ถŠๅคš็š„ไบบ็ˆฑไธŠไบ†่พฃ๏ผŒๅฏไปฅ่ฏดๆ˜ฏๆ— ่พฃไธๆฌขใ€‚ๅ…ถๅฎž่พฃๆค’ๆฅๅˆฐไธญๅ›ฝ่ฟ˜ไธๅˆฐ400ๅนด๏ผŒไฝ†็Žฐๅœจๅพˆๅคšๅนด่ฝปไบบๅทฒ็ป็ฆปไธๅผ€่พฃไบ†ใ€‚ๅ››ๅท่œๅˆ้บปๅˆ่พฃ๏ผŒๆ˜ฏๅพˆๅคšๅนด่ฝปไบบ็š„ๆœ€็ˆฑใ€‚ๅท่œ้ฆ†ๅœจๅพˆๅคšๅœฐๆ–น้ƒฝๅพˆๅ—ไบบไปฌ็š„ๆฌข่ฟŽ๏ผŒๅพˆๅคšไบบ้ƒฝ็ˆฑๅƒ้บป่พฃ็ซ้”…ใ€้บป่พฃ็ƒซๅ’Œ้บปๅฉ†่ฑ†่…ใ€‚\n\nไธบไป€ไนˆ่พฃๆ›ด้€‚ๅˆๅนด่ฝปไบบ็š„ๅฃๅ‘ณๅ‘ข๏ผŸ่พฃๅ‘ณๅ„ฟ่ƒฝ่ฎฉไบบๆ„Ÿๅˆฐๆธฉๆš–ๅ’Œๅฟซไน๏ผŒ่€Œไธ”็บข็บข็š„่พฃๆค’็œ‹็€ๅฐฑ่ฎฉไบบๆปกๅฟƒๆฌขๅ–œ๏ผŒๅพˆๆœ‰้ฃŸๆฌฒ๏ผŒๅ› ๆญคๆ›ดๅ—ๅนด่ฝปไบบๅ–œ็ˆฑใ€‚", + "wordCount": 334, + "questions": [ + { + "question": "ไธญๅ›ฝ่œ็š„็‰น็‚นๆ˜ฏไป€ไนˆ๏ผŸ", + "type": "open", + "answer": "่‰ฒใ€้ฆ™ใ€ๅ‘ณใ€ๅฝขใ€ๆ„้ƒฝๅฅฝ" + }, + { + "question": "็Žฐไปฃไธญๅ›ฝๅนด่ฝปไบบๆœ€ๅ–œๆฌขไป€ไนˆๅฃๅ‘ณ๏ผŸๅŽŸๅ› ๆ˜ฏไป€ไนˆ๏ผŸ", + "type": "open", + "answer": "่พฃใ€‚ๅ› ไธบ่พฃๅ‘ณ่ƒฝ่ฎฉไบบๆ„Ÿๅˆฐๆธฉๆš–ๅ’Œๅฟซไน๏ผŒ็บข่พฃๆค’่ฎฉไบบๆปกๅฟƒๆฌขๅ–œ๏ผŒๅพˆๆœ‰้ฃŸๆฌฒ" + }, + { + "question": "ไธญๅ›ฝไบบ่ฎคไธบ๏ผŒ่œๆœ‰ๆฒกๆœ‰่ฅๅ…ปๆฒกๅ…ณ็ณปใ€‚", + "type": "true_false", + "answer": "้”™" + }, + { + "question": "ๅพˆๅคšๅนด่ฝปไบบ็‰นๅˆซๅ–œๆฌขๅƒๅท่œใ€‚", + "type": "true_false", + "answer": "ๅฏน" + }, + { + "question": "่พฃๆค’ๆœ€ๆ—ฉไบง่‡ชไธญๅ›ฝใ€‚", + "type": "true_false", + "answer": "้”™" + } + ] + }, + { + "id": "etiquette-text", + "title": "ไธญๅ›ฝไบบๅƒ้ฅญ็š„่ง„็Ÿฉ (Chinese Dining Etiquette)", + "type": "extensive", + "content": "ไธญๅ›ฝๅคไบบ่ฏด๏ผš\"ๆฐ‘ไปฅ้ฃŸไธบๅคฉใ€‚\"ๅฏนไบบไปฌๆฅ่ฏด๏ผŒๅƒ้ฅญๆ˜ฏๆœ€้‡่ฆ็š„ไบ‹ๆƒ…ใ€‚ไธญๅ›ฝๆœ‰ไบ”ๅƒๅคšๅนด็š„ๅކๅฒ๏ผŒๆœ‰ไธฐๅฏŒ็š„้ฅฎ้ฃŸๆ–‡ๅŒ–๏ผŒๅƒ้ฅญๆ—ถไนŸๅฐฑๆœ‰ไบ†ๅพˆๅคš่ง„็Ÿฉ๏ผŒๆฏ”ๅฆ‚่ฏด๏ผš\n\n่ง„็Ÿฉไธ€๏ผšๅพˆๅคšไบบไธ€่ตทๅƒ้ฅญๆ—ถ๏ผŒๅ…ˆ่ฏทๅฎขไบบๆˆ–่€…ๆœ€ๅนด้•ฟ็š„ไบบๅ…ฅๅบง๏ผŒๅฎขไบบไธ€่ˆฌไผš็ญ‰ไธปไบบๅฎ‰ๆŽ’ๅŽๅ†ๅ…ฅๅบงใ€‚\n\n่ง„็ŸฉไบŒ๏ผšๅผ€ๅง‹ๅƒ้ฅญๆ—ถ๏ผŒ่ฏทๅฎขไบบๆˆ–่€…ๆœ€ๅนด้•ฟ็š„ไบบๅ…ˆๅŠจ็ญทๅญ๏ผ›ๆฏไธŠไธ€้“่œ๏ผŒ้ƒฝ่ฆ่ฏทๅฎขไบบๆˆ–่€…ๆœ€ๅนด้•ฟ็š„ไบบๅ…ˆๅ“ๅฐใ€‚\n\n่ง„็Ÿฉไธ‰๏ผšๆœ‰ๆ—ถไธปไบบไผšไธบๅฎขไบบๅคน่œ๏ผŒๆ”พๅœจๅฎขไบบ็š„็›˜ๅญ้‡Œ๏ผŒ่ฟ™ๆ˜ฏๅฏนๅฎขไบบ็š„็ƒญๆƒ…ใ€‚\n\n่ง„็Ÿฉๅ››๏ผšๅƒ้ฅญๆ—ถไธ่ƒฝ็”จ็ญทๅญๆ•ฒ็ข—ใ€็›˜ๅญ๏ผ›็ญทๅญไนŸไธๅฏไปฅ็ซ–็€ๆ’ๅœจ็ฑณ้ฅญ้‡Œ๏ผŒ่ฟ™ๆ˜ฏไธ็คผ่ฒŒ็š„ใ€‚\n\n่ง„็Ÿฉไบ”๏ผš\"้ฃŸไธ่จ€\"๏ผŒๅƒไธœ่ฅฟๆ—ถไธ่ฎฒ่ฏ๏ผ›ๅŒๆ ท๏ผŒ็œ‹ๅˆฐๅˆซไบบๅ˜ด้‡Œๆœ‰้ฃŸ็‰ฉ๏ผŒไนŸไธ่ฆๅŽป่ทŸไบบ่ฎฒ่ฏใ€‚\n\n่ง„็Ÿฉๅ…ญ๏ผšๅนด่ฝปไบบไธบๅฎขไบบๆˆ–่€…้•ฟ่พˆๅ€’่Œถใ€ๅ€’้…’ใ€‚\n\n่ง„็Ÿฉไธƒ๏ผš็ป™ไบบๅ€’่Œถๆ—ถ๏ผŒๅชๅ€’ไธƒๅˆ†ๆปก๏ผŒ่ฟ˜ๆœ‰ไธ‰ๅˆ†ๆ˜ฏๅฏนๅฎขไบบ็š„็ƒญๆƒ…๏ผŒ่€Œไธ”ไนŸไธไผš็ƒซๆ‰‹๏ผ›ไฝ†ๅ€’้…’ๆ—ถ๏ผŒไธ€่ˆฌ่ฆๅ€’ๆปก๏ผŒๆ„ๆ€ๆ˜ฏๆปกๆปก็š„ๅฐŠ้‡ๅ’Œๅ‹ๆƒ…ใ€‚\n\n่ง„็Ÿฉๅ…ซ๏ผšๅนด่ฝปไบบๅ‘ๅนด้•ฟ็š„ไบบๆ•ฌ้…’ๆ—ถ๏ผŒไธ€่ˆฌๅŒๆ‰‹ๆ‹ฟๆฏๅญ๏ผŒๆฏๅญ่ฆๆ‹ฟๅพ—ๆฏ”ๅฏนๆ–นไฝŽไธ€็‚นๅ„ฟ๏ผŒ่ฟ™ๆ˜ฏๅฏนๅนด้•ฟ็š„ไบบ็š„ๅฐŠ้‡๏ผ›็œ‹ๅˆฐๅˆซไบบๆญฃๅœจๅคน่œ๏ผŒๅ…ˆไธ่ฆๆ•ฌ้…’ใ€‚\n\n่ง„็Ÿฉไน๏ผšๆ…ขๆ…ขๅœฐๅ“ๅฐๆฏไธ€้“่œ๏ผŒๅฆ‚ๆžœ่œๅชไธŠไบ†ไธ€ๅŠ๏ผŒๅฎขไบบๅฐฑ่ฏดๅƒ้ฅฑไบ†๏ผŒ่ฟ™ๆ˜ฏไธ็คผ่ฒŒ็š„๏ผ›ๅƒๅฎŒ้ฅญไปฅๅŽ๏ผŒๅฎขไบบไธ€่ˆฌไผšๅ‘ไธปไบบ่ฏดๅ‡ ๅฅๆ„Ÿ่ฐข็š„่ฏใ€‚", + "wordCount": 466, + "questions": [ + { + "question": "ๆœ€้€‚ๅˆๅšๆœฌๆ–‡ๆ ‡้ข˜็š„ๆ˜ฏ๏ผš", + "type": "multiple_choice", + "options": ["Aไธญๅ›ฝไบบๅƒ้ฅญ็š„่ง„็Ÿฉ", "Bไธญๅ›ฝไบบๆ€Žไนˆ่ฏทๅฎข", "Cไธญๅ›ฝ็š„้ฅฎ้ฃŸๆ–‡ๅŒ–", "Dไธญๅ›ฝไบบ็š„็คผ่ฒŒ"], + "correctAnswer": "Aไธญๅ›ฝไบบๅƒ้ฅญ็š„่ง„็Ÿฉ" + }, + { + "question": "\"ๆฐ‘ไปฅ้ฃŸไธบๅคฉ\"็š„ๆ„ๆ€ๆ˜ฏ๏ผš", + "type": "multiple_choice", + "options": ["Aไบบไปฌๅ–œๆฌขๅƒ", "Bไบบไปฌ่ง‰ๅพ—ๅƒ้žๅธธ้‡่ฆ", "Cไบบไปฌๆฏๅคฉ่ฆๅƒ้ฅญ", "Dไธบไบ†ๅƒ๏ผŒไบบไปฌ่ฆๆฏๅคฉๅทฅไฝœ"], + "correctAnswer": "Bไบบไปฌ่ง‰ๅพ—ๅƒ้žๅธธ้‡่ฆ" + } + ] + } + ], + "exercises": [ + { + "type": "character_inference", + "title": "่ฟ็”จๆฑ‰ๅญ—็Ÿฅ่ฏ†ๆŽจๆ–ญๅญ—ไน‰ (Infer meaning from character components)", + "description": "Use radical knowledge to infer the meaning of unfamiliar characters", + "questions": [ + { + "question": "่œๅˆšๅšๅฅฝ๏ผŒๅคช็ƒซไบ†ใ€‚", + "options": ["A้žๅธธ้ฅฟ", "B้žๅธธ็ƒญ", "C้žๅธธๅฅฝๅƒ"], + "correctAnswer": "B้žๅธธ็ƒญ", + "hint": "็ƒซ has the fire radical ็ซ" + }, + { + "question": "่ฟ™็ง่‹นๆžœๆฏ”่พƒ้…ธ๏ผŒๅ†็œ‹็œ‹ๅˆซ็š„ๅงใ€‚", + "options": ["Aไธ€็งๅ‘ณ้“", "Bไธ€็ง้ขœ่‰ฒ", "Cไธ€็งๅƒ็š„ไธœ่ฅฟ"], + "correctAnswer": "Aไธ€็งๅ‘ณ้“" + }, + { + "question": "่ฟ™ไธช็”ต่ง†่Š‚็›ฎไป‹็ปไบ†็ณ็ฒ‘็š„ๅšๆณ•ใ€‚", + "options": ["Aไธ€็ง่กฃ็‰ฉ", "Bไธ€็ง็”จ็š„ไธœ่ฅฟ", "Cไธ€็งๅƒ็š„ไธœ่ฅฟ"], + "correctAnswer": "Cไธ€็งๅƒ็š„ไธœ่ฅฟ", + "hint": "็ณ็ฒ‘ has the rice radical ็ฑณ" + } + ] + } + ] +} diff --git a/content/chapters/ledu-chapter2.json b/content/chapters/ledu-chapter2.json new file mode 100644 index 0000000..4ebdbff --- /dev/null +++ b/content/chapters/ledu-chapter2.json @@ -0,0 +1,750 @@ +{ + "id": "ledu-chapter2", + "book_id": "ledu", + "name": "่ดงๆฏ”ไธ‰ๅฎถ (Compare Prices at Three Shops)", + "description": "Chapter on online shopping, consumer behavior, and China's Double 11 shopping festival. Explores shopping methods, consumer preferences, and the advantages and disadvantages of online shopping.", + "difficulty": "intermediate", + "language": "zh-CN", + "chapter_number": "2", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "LEDU Textbook - Jiaotong University", + "target_level": "intermediate", + "estimated_hours": 10, + "prerequisites": ["ledu-chapter1"], + "learning_objectives": [ + "Master 40+ shopping and consumer vocabulary terms", + "Understand Chinese online shopping culture and Double 11", + "Learn about consumer behavior and shopping preferences", + "Practice reading comprehension with authentic texts", + "Develop vocabulary inference skills using affixes" + ], + "content_tags": ["shopping", "consumer-culture", "online-shopping", "double-11", "chinese-culture"], + "completion_criteria": { + "vocabulary_mastery": 90, + "comprehension_score": 80, + "exercises_completed": 20 + } + }, + "vocabulary": { + "ๅ“": { + "pronunciation": "pวn", + "type": "morpheme", + "user_language": "item, product, grade, quality", + "examples": ["ๅ•†ๅ“", "ไบงๅ“", "ไธŠๅ“", "ๅ“่Œถ"], + "notes": "Associative character composed of three ๅฃ (mouth). Generally used as morpheme, not alone." + }, + "็‰ฉ": { + "pronunciation": "wรน", + "type": "morpheme", + "user_language": "thing, object, matter", + "examples": ["ๅŠจ็‰ฉ", "็‰ฉๅ“", "็คผ็‰ฉ", "ๅš็‰ฉ้ฆ†"], + "notes": "Usually used as morpheme, not alone" + }, + "ๆถˆ่ดน": { + "pronunciation": "xiฤofรจi", + "type": "verb", + "user_language": "to consume", + "examples": ["ๆถˆ่ดนๆฐดๅนณ", "ๆถˆ่ดน่€…"] + }, + "ๆ–นๅผ": { + "pronunciation": "fฤngshรฌ", + "type": "noun", + "user_language": "way, method, manner", + "examples": ["็”Ÿๆดปๆ–นๅผ", "ๆถˆ่ดนๆ–นๅผ"] + }, + "ๆ‰“ๆŠ˜": { + "pronunciation": "dวŽ//zhรฉ", + "type": "verb", + "user_language": "to sell at a discount", + "examples": ["ๆ‰“ๅ…ซๆŠ˜", "ๆ‰“ๆŠ˜ไฟƒ้”€"], + "notes": "ๆ‰“ๅ…ซๆŠ˜ means 20% off (pay 80%)" + }, + "ไฟƒ้”€": { + "pronunciation": "cรนxiฤo", + "type": "verb", + "user_language": "to promote sales", + "examples": ["ๆ‰“ๆŠ˜ไฟƒ้”€", "ไฟƒ้”€ๅ‘˜"] + }, + "ไผ˜ๆƒ ": { + "pronunciation": "yลuhuรฌ", + "type": "adjective", + "user_language": "preferential, discount", + "examples": ["ไผ˜ๆƒ ไปทๆ ผ", "ๅ…ซๆŠ˜ไผ˜ๆƒ "] + }, + "่ฐƒๆŸฅ": { + "pronunciation": "diร ochรก", + "type": "noun/verb", + "user_language": "investigation; to investigate", + "examples": ["ไธ€้กน่ฐƒๆŸฅ", "ๅš่ฐƒๆŸฅ"] + }, + "้œ€่ฆ": { + "pronunciation": "xลซyร o", + "type": "verb/noun", + "user_language": "to need; need", + "examples": ["้œ€่ฆๆ—ถ้—ด", "้œ€่ฆๅธฎๅŠฉ", "ๅทฅไฝœ็š„้œ€่ฆ"] + }, + "ๅฎž็”จ": { + "pronunciation": "shรญyรฒng", + "type": "adjective", + "user_language": "practical, functional" + }, + "ๆŠ˜ๆ‰ฃ": { + "pronunciation": "zhรฉkรฒu", + "type": "noun", + "user_language": "discount", + "examples": ["ๆœ‰ๆŠ˜ๆ‰ฃ", "ๆŠ˜ๆ‰ฃไปท"] + }, + "ๅธๅผ•": { + "pronunciation": "xฤซyวn", + "type": "verb", + "user_language": "to attract", + "examples": ["ๅธๅผ•ไบบ", "ๆœ‰ๅธๅผ•ๅŠ›", "ๅธๅผ•ไฝ"] + }, + "ไบงๅ“": { + "pronunciation": "chวŽnpวn", + "type": "noun", + "user_language": "product", + "examples": ["่ฟ›ๅฃไบงๅ“", "ไบงๅ“่ดจ้‡"] + }, + "็œ": { + "pronunciation": "shฤ›ng", + "type": "verb", + "user_language": "to save, to economize", + "examples": ["็œ้’ฑ", "็œๆ—ถ้—ด", "็œๅฟƒ"] + }, + "ๆปกๆ„": { + "pronunciation": "mวŽnyรฌ", + "type": "verb", + "user_language": "to be satisfied", + "examples": ["ๅฏนโ€ฆๆปกๆ„"] + }, + "ๅ…‰ๆฃๅ„ฟ่Š‚": { + "pronunciation": "Guฤnggรนnr Jiรฉ", + "type": "proper noun", + "user_language": "Singles' Day", + "notes": "November 11th, because of the four 1's representing single people" + }, + "้™": { + "pronunciation": "jiร ng", + "type": "verb", + "user_language": "to lower, to reduce" + }, + "ๆถจ": { + "pronunciation": "zhวŽng", + "type": "verb", + "user_language": "to rise (of prices, water, wages)" + }, + "้ช—": { + "pronunciation": "piร n", + "type": "verb", + "user_language": "to cheat, to deceive" + }, + "็ป้ชŒ": { + "pronunciation": "jฤซngyร n", + "type": "noun", + "user_language": "experience" + }, + "่ตš": { + "pronunciation": "zhuร n", + "type": "verb", + "user_language": "to make a profit, to earn" + }, + "ๆœ่ฃ…": { + "pronunciation": "fรบzhuฤng", + "type": "noun", + "user_language": "clothing, costume" + }, + "ๆปก": { + "pronunciation": "mวŽn", + "type": "verb", + "user_language": "to reach, to come up to (formal/business)", + "examples": ["ๆฏๆปก299ๅ…ƒ"] + }, + "่ฟ”": { + "pronunciation": "fวŽn", + "type": "verb", + "user_language": "to return, to pay back (formal/business)" + }, + "ไผ˜ๆƒ ๅˆธ": { + "pronunciation": "yลuhuรฌquร n", + "type": "noun", + "user_language": "coupon" + }, + "ๅŒ–ๅฆ†ๅ“": { + "pronunciation": "huร zhuฤngpวn", + "type": "noun", + "user_language": "cosmetics, makeup" + }, + "็”ต้ฅญ้”…": { + "pronunciation": "diร nfร nguล", + "type": "noun", + "user_language": "electric rice cooker" + }, + "็ฝ‘่ดญ": { + "pronunciation": "wวŽnggรฒu", + "type": "verb", + "user_language": "to shop online" + }, + "่ฟ‘ๅนดๆฅ": { + "pronunciation": "jรฌn niรกn lรกi", + "type": "phrase", + "user_language": "in recent years" + }, + "ๅทฒ็ป": { + "pronunciation": "yวjฤซng", + "type": "adverb", + "user_language": "already" + }, + "ๆˆไธบ": { + "pronunciation": "chรฉngwรฉi", + "type": "verb", + "user_language": "to become" + }, + "ๆ—ฅๅธธ": { + "pronunciation": "rรฌchรกng", + "type": "adjective", + "user_language": "daily, everyday" + }, + "่ฐˆๅˆฐ": { + "pronunciation": "tรกndร o", + "type": "verb", + "user_language": "to mention, to talk about" + }, + "้ฆ–ๅ…ˆ": { + "pronunciation": "shว’uxiฤn", + "type": "adverb", + "user_language": "first, firstly" + }, + "ๆƒณๅˆฐ": { + "pronunciation": "xiวŽngdร o", + "type": "verb", + "user_language": "to think of" + }, + "ๅŒๅไธ€": { + "pronunciation": "shuฤng shรญ yฤซ", + "type": "proper noun", + "user_language": "Double 11 (November 11)" + }, + "ๅ•่บซ": { + "pronunciation": "dฤnshฤ“n", + "type": "adjective", + "user_language": "single (unmarried)" + }, + "ๅŽๆฅ": { + "pronunciation": "hรฒulรกi", + "type": "adverb", + "user_language": "later, afterwards" + }, + "่ฏดๆณ•": { + "pronunciation": "shuลfวŽ", + "type": "noun", + "user_language": "way of saying, statement" + }, + "่ฎกๅˆ’": { + "pronunciation": "jรฌhuร ", + "type": "verb/noun", + "user_language": "to plan; plan" + }, + "ไธพๅŠž": { + "pronunciation": "jว”bร n", + "type": "verb", + "user_language": "to hold, to organize (event)" + }, + "่ดญ็‰ฉ่Š‚": { + "pronunciation": "gรฒuwรน jiรฉ", + "type": "noun", + "user_language": "shopping festival" + }, + "้€‰ๆ‹ฉ": { + "pronunciation": "xuวŽnzรฉ", + "type": "verb", + "user_language": "to choose, to select" + }, + "ๅˆšๅฅฝ": { + "pronunciation": "gฤnghวŽo", + "type": "adverb", + "user_language": "just right, exactly" + }, + "่ดญไนฐ": { + "pronunciation": "gรฒumวŽi", + "type": "verb", + "user_language": "to purchase" + }, + "็ฝ‘ๅ‹": { + "pronunciation": "wวŽngyว’u", + "type": "noun", + "user_language": "netizen, internet user" + }, + "ๆˆ็งฐ": { + "pronunciation": "xรฌchฤ“ng", + "type": "verb", + "user_language": "to jokingly call" + }, + "ๆฏๅนด": { + "pronunciation": "mฤ›iniรกn", + "type": "noun", + "user_language": "every year" + }, + "็”ตๅ•†็ฝ‘็ซ™": { + "pronunciation": "diร nshฤng wวŽngzhร n", + "type": "noun", + "user_language": "e-commerce website" + }, + "ไปทๆ ผ": { + "pronunciation": "jiร gรฉ", + "type": "noun", + "user_language": "price" + }, + "ไธ€่ˆฌ": { + "pronunciation": "yฤซbฤn", + "type": "adverb", + "user_language": "generally, usually" + }, + "ๅนณๆ—ถ": { + "pronunciation": "pรญngshรญ", + "type": "noun", + "user_language": "usually, ordinarily" + }, + "็‰ฉ็พŽไปทๅป‰": { + "pronunciation": "wรน mฤ›i jiร  liรกn", + "type": "idiom", + "user_language": "good quality and cheap price" + }, + "็ฝ‘็ซ™": { + "pronunciation": "wวŽngzhร n", + "type": "noun", + "user_language": "website" + }, + "ๅ…ถไธญ": { + "pronunciation": "qรญzhลng", + "type": "pronoun", + "user_language": "among them" + }, + "ๅคง้ƒจๅˆ†": { + "pronunciation": "dร  bรนfen", + "type": "noun", + "user_language": "most, the majority" + }, + "่ฎคไธบ": { + "pronunciation": "rรจnwรฉi", + "type": "verb", + "user_language": "to think, to believe" + }, + "ไนฐๅˆฐ": { + "pronunciation": "mวŽidร o", + "type": "verb", + "user_language": "to buy (successfully)" + }, + "ไพฟๅฎœ": { + "pronunciation": "piรกnyi", + "type": "adjective", + "user_language": "cheap, inexpensive" + }, + "่€Œไธ”": { + "pronunciation": "รฉrqiฤ›", + "type": "conjunction", + "user_language": "and, moreover" + }, + "็Žฐๅœจ": { + "pronunciation": "xiร nzร i", + "type": "noun", + "user_language": "now, currently" + }, + "ไธœ่ฅฟ": { + "pronunciation": "dลngxi", + "type": "noun", + "user_language": "thing, stuff" + }, + "ๅ‚ๅŠ ": { + "pronunciation": "cฤnjiฤ", + "type": "verb", + "user_language": "to participate" + }, + "่ง‰ๅพ—": { + "pronunciation": "juรฉde", + "type": "verb", + "user_language": "to feel, to think" + }, + "็‰นๅˆซ": { + "pronunciation": "tรจbiรฉ", + "type": "adverb", + "user_language": "especially, particularly" + }, + "ๅทฅไฝœ": { + "pronunciation": "gลngzuรฒ", + "type": "noun/verb", + "user_language": "work, job; to work" + }, + "ๅฟ™": { + "pronunciation": "mรกng", + "type": "adjective", + "user_language": "busy" + }, + "ๆ—ถ้—ด": { + "pronunciation": "shรญjiฤn", + "type": "noun", + "user_language": "time" + }, + "ๆ„Ÿ่ง‰": { + "pronunciation": "gวŽnjuรฉ", + "type": "verb", + "user_language": "to feel" + }, + "็”ทๆ€ง": { + "pronunciation": "nรกnxรฌng", + "type": "noun", + "user_language": "male" + }, + "ๅฅณๆ€ง": { + "pronunciation": "nวšxรฌng", + "type": "noun", + "user_language": "female" + }, + "ๅธๅผ•ๅŠ›": { + "pronunciation": "xฤซyวnlรฌ", + "type": "noun", + "user_language": "attraction, appeal" + }, + "ๆ—ฅ็”จ็™พ่ดง": { + "pronunciation": "rรฌyรฒng bวŽihuรฒ", + "type": "noun", + "user_language": "daily necessities" + }, + "ๅฎถ็”ต": { + "pronunciation": "jiฤdiร n", + "type": "noun", + "user_language": "household appliances" + }, + "ๆ•ฐ็ ไบงๅ“": { + "pronunciation": "shรนmวŽ chวŽnpวn", + "type": "noun", + "user_language": "digital products" + }, + "ๆœ‰ๅˆฉๆœ‰ๅผŠ": { + "pronunciation": "yว’u lรฌ yว’u bรฌ", + "type": "idiom", + "user_language": "has advantages and disadvantages" + }, + "ๆ–นไพฟ": { + "pronunciation": "fฤngbiร n", + "type": "adjective", + "user_language": "convenient" + }, + "่ดญ็‰ฉ": { + "pronunciation": "gรฒuwรน", + "type": "verb", + "user_language": "shopping" + }, + "ไน่ถฃ": { + "pronunciation": "lรจqรน", + "type": "noun", + "user_language": "pleasure, joy" + }, + "ไฝ†ๆ˜ฏ": { + "pronunciation": "dร nshรฌ", + "type": "conjunction", + "user_language": "but, however" + }, + "ๆœ‰ๆ—ถ": { + "pronunciation": "yว’ushรญ", + "type": "adverb", + "user_language": "sometimes" + }, + "ๅ•†ๅ“": { + "pronunciation": "shฤngpวn", + "type": "noun", + "user_language": "goods, merchandise" + }, + "ๆœฌๆฅ": { + "pronunciation": "bฤ›nlรกi", + "type": "adverb", + "user_language": "originally" + }, + "ไธบไบ†": { + "pronunciation": "wรจile", + "type": "preposition", + "user_language": "in order to, for" + }, + "ๅด": { + "pronunciation": "quรจ", + "type": "adverb", + "user_language": "however, but" + }, + "้’ฑ": { + "pronunciation": "qiรกn", + "type": "noun", + "user_language": "money" + }, + "็œŸๆญฃ": { + "pronunciation": "zhฤ“nzhฤ“ng", + "type": "adjective", + "user_language": "real, genuine" + }, + "้™ไปท": { + "pronunciation": "jiร ngjiร ", + "type": "verb", + "user_language": "to reduce prices" + }, + "ๆถจไปท": { + "pronunciation": "zhวŽngjiร ", + "type": "verb", + "user_language": "to increase prices" + }, + "ๆ–ฐๆ‰‹": { + "pronunciation": "xฤซnshว’u", + "type": "noun", + "user_language": "novice, beginner" + }, + "ไธ็”จ": { + "pronunciation": "bรนyรฒng", + "type": "auxiliary verb", + "user_language": "no need to" + }, + "ไปทๅป‰็‰ฉ็พŽ": { + "pronunciation": "jiร  liรกn wรน mฤ›i", + "type": "idiom", + "user_language": "cheap and good quality" + }, + "ไธบไป€ไนˆ": { + "pronunciation": "wรจishรฉnme", + "type": "pronoun", + "user_language": "why" + }, + "ไผš": { + "pronunciation": "huรฌ", + "type": "auxiliary verb", + "user_language": "will" + }, + "ๅฏ่ƒฝ": { + "pronunciation": "kฤ›nรฉng", + "type": "auxiliary verb", + "user_language": "possibly, maybe" + }, + "ๅผ€็ฝ‘ๅบ—": { + "pronunciation": "kฤi wวŽngdiร n", + "type": "verb phrase", + "user_language": "to run an online store" + }, + "็ฝ‘ๅบ—": { + "pronunciation": "wวŽngdiร n", + "type": "noun", + "user_language": "online store" + }, + "ๅš็”Ÿๆ„": { + "pronunciation": "zuรฒ shฤ“ngyi", + "type": "verb phrase", + "user_language": "to do business" + }, + "ๅ…ˆ": { + "pronunciation": "xiฤn", + "type": "adverb", + "user_language": "first" + }, + "ๅฆ‚ๆžœ": { + "pronunciation": "rรบguว’", + "type": "conjunction", + "user_language": "if" + }, + "ๅฏน...ๆฅ่ฏด": { + "pronunciation": "duรฌ...lรกi shuล", + "type": "phrase", + "user_language": "for, as far as...is concerned" + }, + "ๅบ—ไธป": { + "pronunciation": "diร nzhว”", + "type": "noun", + "user_language": "shop owner" + }, + "ๆœŸ้—ด": { + "pronunciation": "qฤซjiฤn", + "type": "noun", + "user_language": "period, during" + }, + "ไธป่ฆ": { + "pronunciation": "zhว”yร o", + "type": "adverb", + "user_language": "mainly" + }, + "ไบบๆฐ”": { + "pronunciation": "rรฉnqรฌ", + "type": "noun", + "user_language": "popularity" + } + }, + "grammar": { + "ๆ—ข็„ถ": { + "title": "ๆ—ข็„ถโ€ฆโ€ฆ - since, now that", + "pattern": "ๆ—ข็„ถ + Reason, Result", + "explanation": "Used to express that since a certain condition exists, a certain result follows logically.", + "examples": [ + { + "chinese": "ๆ—ข็„ถไฝ ่ฟ™ๅ‘จๆฒกๆœ‰ๆ—ถ้—ด๏ผŒ้‚ฃๅฐฑไธ‹ๅ‘จๅ†ๅŽปๅงใ€‚", + "pronunciation": "Jรฌrรกn nว zhรจ zhลu mรฉiyว’u shรญjiฤn, nร  jiรน xiร  zhลu zร i qรน ba.", + "translation": "Since you don't have time this week, let's go next week then." + }, + { + "chinese": "ๆ—ข็„ถ็ˆถๆฏไธๅŒๆ„ไฝ ๅŽปๅ›ฝๅค–ๅทฅไฝœ๏ผŒไฝ ๅฐฑๅˆซๅŽปไบ†ใ€‚", + "pronunciation": "Jรฌrรกn fรนmว” bรน tรณngyรฌ nว qรน guรณwร i gลngzuรฒ, nว jiรน biรฉ qรน le.", + "translation": "Since your parents don't agree with you working abroad, then don't go." + }, + { + "chinese": "ๆ—ข็„ถๆ˜ฏ่‡ชๅทฑ้œ€่ฆ็š„ไธœ่ฅฟ๏ผŒไปทๆ ผๅˆไพฟๅฎœ๏ผŒไธบไป€ไนˆไธไนฐ๏ผŸ", + "pronunciation": "Jรฌrรกn shรฌ zรฌjว xลซyร o de dลngxi, jiร gรฉ yรฒu piรกnyi, wรจishรฉnme bรน mวŽi?", + "translation": "Since it's something you need and the price is cheap, why not buy it?" + } + ] + }, + "่‡ช-ไปŽ-่ตท": { + "title": "่‡ช/ไปŽโ€ฆโ€ฆ่ตท - from...onwards (written language)", + "explanation": "Written Chinese expression meaning 'ไปŽโ€ฆโ€ฆๅผ€ๅง‹' (from...starting). More formal.", + "examples": [ + { + "chinese": "่‡ชไปŠๆ—ฅ่ตท่‡ณ1ๆœˆ1ๆ—ฅ๏ผŒๆœฌๅบ—้ƒจๅˆ†ๅ•†ๅ“ๅŠไปทใ€‚", + "pronunciation": "Zรฌ jฤซnrรฌ qว zhรฌ 1 yuรจ 1 rรฌ, bฤ›n diร n bรนfen shฤngpวn bร n jiร .", + "translation": "From today until January 1st, some products in this shop are half price." + }, + { + "chinese": "ไปŽไธ‹ๅ‘จ่ตท๏ผŒๅญฆๆ กๆ”พๅ‡ใ€‚", + "pronunciation": "Cรณng xiร  zhลu qว, xuรฉxiร o fร ngjiร .", + "translation": "Starting from next week, school is on vacation." + } + ] + }, + "ไธบ-written": { + "title": "ไธบ (wรจi) - to be (written language)", + "explanation": "Written Chinese verb meaning 'ๆ˜ฏ' (to be). More formal.", + "examples": [ + { + "chinese": "่€ƒ่ฏ•ๆ—ถ้—ดไธบไธคๅฐๆ—ถใ€‚", + "pronunciation": "KวŽoshรฌ shรญjiฤn wรจi liวŽng xiวŽoshรญ.", + "translation": "The exam time is two hours." + }, + { + "chinese": "ๅ…ฌๅธไธŠ็ญๆ—ถ้—ดไธบไธŠๅˆ9:00่‡ณไธ‹ๅˆ5:00ใ€‚", + "pronunciation": "Gลngsฤซ shร ngbฤn shรญjiฤn wรจi shร ngwว” 9:00 zhรฌ xiร wว” 5:00.", + "translation": "Company working hours are from 9:00 AM to 5:00 PM." + } + ] + } + }, + "texts": [ + { + "id": "main-text", + "title": "\"ๅŒๅไธ€\"๏ผŒไนฐ่ฟ˜ๆ˜ฏไธไนฐ๏ผŸ (Double 11: To Buy or Not to Buy?)", + "type": "main", + "content": "่ฟ‘ๅนดๆฅ๏ผŒ็ฝ‘่ดญๅทฒ็ปๆˆไธบไบบไปฌ็š„ๆ—ฅๅธธๆถˆ่ดนๆ–นๅผไน‹ไธ€ใ€‚่ฐˆๅˆฐ็ฝ‘่ดญ๏ผŒไบบไปฌ้ฆ–ๅ…ˆไผšๆƒณๅˆฐ\"ๅŒๅไธ€\"ใ€‚\"ๅŒๅไธ€\"ๆ˜ฏไป€ไนˆ๏ผŸ่ฟ™ๅพ—ไปŽ\"ๅ…‰ๆฃๅ„ฟ่Š‚\"่ฏด่ตทใ€‚11ๆœˆ11ๆ—ฅ๏ผŒๅ› ไธบๆœ‰4ไธช\"1\"๏ผŒ่ฎฉไบบไปฌๆƒณๅˆฐไบ†ๅ•่บซ็š„ไบบโ€”โ€”\"ๅ…‰ๆฃๅ„ฟ\"๏ผŒๅŽๆฅๅฐฑๆœ‰ไบ†\"ๅ…‰ๆฃๅ„ฟ่Š‚\"็š„่ฏดๆณ•ใ€‚2009ๅนด๏ผŒไธ€ไธช็ฝ‘ไธŠๅ•†ๅŸŽ่ฎกๅˆ’ไธพๅŠžไธ€ไธช็ฝ‘ไธŠ่ดญ็‰ฉ่Š‚๏ผŒไป–ไปฌ้€‰ๆ‹ฉๅœจ11ๆœˆ่ฟ›่กŒ๏ผŒๅ› ไธบ้‚ฃๆ—ถๅˆšๅฅฝๆ˜ฏไบบไปฌ่ดญไนฐๅ†ฌ่ฃ…็š„ๆ—ถๅ€™ใ€‚11ๆœˆ11ๆ—ฅ่ขซ็ฝ‘ๅ‹ๆˆ็งฐไธบ\"ๅ…‰ๆฃๅ„ฟ่Š‚\"๏ผŒ่ดญ็‰ฉ่Š‚ๅฐฑ้€‰ๅœจไบ†่ฟ™ไธ€ๅคฉ๏ผŒ\"ๅŒๅไธ€\"่ดญ็‰ฉ่Š‚ๅฐฑๆ˜ฏ่ฟ™ไนˆๆฅ็š„ใ€‚ๅŽๆฅ๏ผŒๆฏๅนด็š„11ๆœˆ11ๆ—ฅ0็‚น่ตท๏ผŒๅ„ๅคง็”ตๅ•†็ฝ‘็ซ™้ƒฝไผšๆ‰“ๆŠ˜ไฟƒ้”€๏ผŒไปทๆ ผไธ€่ˆฌ้ƒฝๆฏ”ๅนณๆ—ถไผ˜ๆƒ ๅพˆๅคšใ€‚\n\nๅœจ็‰ฉ็พŽไปทๅป‰้ขๅ‰๏ผŒไนฐ่ฟ˜ๆ˜ฏไธไนฐ๏ผŸไธ€ๅฎถ็ฝ‘็ซ™ๅšไบ†ไธ€ไธช\"ๅŒๅไธ€\"็ฝ‘่ดญ็š„่ฐƒๆŸฅใ€‚ๆœ‰75%็š„็ฝ‘ๅ‹่ฏดไผšๅœจ\"ๅŒๅไธ€\"็ฝ‘่ดญ๏ผŒๅ…ถไธญๅคง้ƒจๅˆ†ไบบ่ฎคไธบ\"่ƒฝไนฐๅˆฐไพฟๅฎœ่€Œไธ”็Žฐๅœจ้œ€่ฆ็š„ไธœ่ฅฟ\"๏ผ›ไธๅ‡†ๅค‡ๅ‚ๅŠ ็š„ไบบไธญ๏ผŒๆœ‰ไบบ่ง‰ๅพ—\"ๆฒกๆœ‰็‰นๅˆซๆƒณไนฐ็š„ไธœ่ฅฟ\"๏ผŒๆœ‰ไบบ\"ๅทฅไฝœๅฟ™๏ผŒๆฒกๆœ‰ๆ—ถ้—ด\"๏ผŒ่ฟ˜ๆœ‰ไบบ\"ๆ„Ÿ่ง‰ไนฐ็š„ไธœ่ฅฟไธๅฎž็”จ\"ใ€‚็”ทๆ€ง็š„็ƒญๆƒ…ไธๆฏ”ๅฅณๆ€งไฝŽ๏ผŒ็ฝ‘่ดญ็š„ไฝŽๆŠ˜ๆ‰ฃๅฏนไป–ไปฌไนŸๆœ‰ๅพˆๅคง็š„ๅธๅผ•ๅŠ›ใ€‚\n\nไปŽ่ฐƒๆŸฅไธญๅฏไปฅ็Ÿฅ้“๏ผŒไบบไปฌๅฏน็ฝ‘่ดญๆœ่ฃ…ใ€ๆ—ฅ็”จ็™พ่ดงใ€ๅฎถ็”ตๅŠๆ•ฐ็ ไบงๅ“ๆœ€ๆ„Ÿๅ…ด่ถฃใ€‚\n\n็ฝ‘่ดญๆœ‰ๅˆฉๆœ‰ๅผŠใ€‚็ฝ‘่ดญๆ–นไพฟใ€็œ้’ฑ๏ผŒ\"ๅŒๅไธ€\"็ป™ๅพˆๅคšไบบๅธฆๆฅไบ†่ทŸๅนณๆ—ถไธไธ€ๆ ท็š„่ดญ็‰ฉไน่ถฃใ€‚ไฝ†ๆ˜ฏ๏ผŒๆœ‰ๆ—ถไบบไปฌๅฏนไนฐๅˆฐ็š„ๅ•†ๅ“ไธๅคชๆปกๆ„๏ผŒ่ฟ˜ๆœ‰ไบบไนฐไบ†ๅพˆๅคšไธ้œ€่ฆ็š„ไธœ่ฅฟ๏ผŒๆœฌๆฅๆ˜ฏไธบไบ†็œ้’ฑๅŽป็ฝ‘่ดญ๏ผŒๅดๅคš่Šฑไบ†้’ฑใ€‚", + "wordCount": 536, + "questions": [ + { + "question": "ๆ–‡็ซ ็ฌฌ1ๆฎตไธป่ฆไป‹็ปไบ†\"ๅŒๅไธ€\"็š„๏ผš", + "type": "multiple_choice", + "options": ["Aไผ ็ปŸ", "B็”ฑๆฅ", "Cไฟƒ้”€ๆดปๅŠจ", "Dๅ•†ๅฎถ"], + "correctAnswer": "B็”ฑๆฅ" + }, + { + "question": "ไบบไปฌๅ–œๆฌขๅœจ\"ๅŒๅไธ€\"็ฝ‘่ดญ็š„ไธป่ฆๅŽŸๅ› ๆ˜ฏไป€ไนˆ๏ผŸ", + "type": "multiple_choice", + "options": ["A็ฝ‘่ดญๆฏ”ๅŽปๅฎžไฝ“ๅบ—่ดญ็‰ฉๆ›ดๆ–นไพฟ", "B่ƒฝไนฐๅˆฐไพฟๅฎœ่€Œไธ”้œ€่ฆ็š„ไธœ่ฅฟ", "Cๅนณๆ—ถๅพˆๅฟ™๏ผŒๆฒกๆœ‰ๆ—ถ้—ดไนฐไธœ่ฅฟ", "D็ฝ‘่ดญ่ƒฝ็ป™ไบบไปฌๅธฆๆฅๅพˆๅคšไน่ถฃ"], + "correctAnswer": "B่ƒฝไนฐๅˆฐไพฟๅฎœ่€Œไธ”้œ€่ฆ็š„ไธœ่ฅฟ" + }, + { + "question": "ไบบไปฌไธๆƒณๅ‚ๅŠ \"ๅŒๅไธ€\"็ฝ‘่ดญ็š„ๅŽŸๅ› ไธญ๏ผŒไธ‹ๅˆ—ๅ“ชไธ€้กนๆ–‡ไธญๆฒกๆœ‰ๆๅˆฐ๏ผŸ", + "type": "multiple_choice", + "options": ["Aๆฒกๆœ‰ๆƒณไนฐ็š„ไธœ่ฅฟ", "Bๅคชๅฟ™๏ผŒๆฒกๆœ‰ๆ—ถ้—ด", "C่ง‰ๅพ—็ฝ‘ไธŠ็š„ๅ•†ๅ“่ดจ้‡ไธๅฅฝ", "D็ฝ‘่ดญ็š„ไธœ่ฅฟๅฏ่ƒฝไธๅคชๅฎž็”จ"], + "correctAnswer": "C่ง‰ๅพ—็ฝ‘ไธŠ็š„ๅ•†ๅ“่ดจ้‡ไธๅฅฝ" + } + ] + }, + { + "id": "netizen-comments", + "title": "ๅ…ณไบŽ\"ๅŒๅไธ€\"็š„็ฝ‘ๅ‹่ฏ„ไปท (Netizen Comments About Double 11)", + "type": "extensive", + "content": "็ฝ‘ๅ‹1๏ผšไธๆ˜ฏ็œŸๆญฃ็š„้™ไปท๏ผŒไธๅฐ‘ไธœ่ฅฟๆ˜ฏๆถจไปทไบ†๏ผŒ้ช—้ช—ๆ–ฐๆ‰‹็š„ใ€‚\n\n็ฝ‘ๅ‹2๏ผšๅนณๆ—ถไนŸไฟƒ้”€๏ผŒๆœ‰ๆŠ˜ๆ‰ฃ๏ผŒ\"ๅŒๅไธ€\"ๆฒกๆœ‰ไผ˜ๆƒ ๅพˆๅคš๏ผŒไธ็”จๆ€ฅ็€้‚ฃไธ€ๅคฉไนฐใ€‚\n\n็ฝ‘ๅ‹3๏ผš็œ้’ฑๆœ€้‡่ฆ๏ผŒๅคงๅฎถ้ƒฝๆƒณไนฐๅˆฐไปทๅป‰็‰ฉ็พŽ็š„ไธœ่ฅฟใ€‚\n\n็ฝ‘ๅ‹4๏ผšๆ—ข็„ถๆ˜ฏ่‡ชๅทฑ้œ€่ฆ็š„ไธœ่ฅฟ๏ผŒไปทๆ ผๅˆไพฟๅฎœ๏ผŒไธบไป€ไนˆไธไนฐ๏ผŸ\n\n็ฝ‘ๅ‹5๏ผšๅพˆๅคšไบบไผšไนฐ๏ผŒไพฟๅฎœๅ˜›ใ€‚ไธ่ฟ‡็ญ‰ไธไบ†ๅคšไน…๏ผŒๅ†็œ‹็œ‹ไนฐๅ›žๆฅ็š„ไธœ่ฅฟ๏ผŒๅคงๅคšๆ˜ฏ็”จไธไธŠ็š„ใ€‚\n\n็ฝ‘ๅ‹6๏ผšๆˆ‘ๅผ€็ฝ‘ๅบ—ไบ”ๅนดไบ†๏ผŒ\"ๅŒๅไธ€\"ๅคง้ƒจๅˆ†็ฝ‘ๅบ—ไผš้™ไปท๏ผŒๅฏ่ƒฝๆžๅฐ‘ๆ•ฐ็ฝ‘ๅบ—ๆถจไบ†ไปท๏ผŒๆˆ‘่ง‰ๅพ—้‚ฃไบ›ไบบไธๆ˜ฏ็œŸๆญฃๅš็”Ÿๆ„็š„ใ€‚\n\n็ฝ‘ๅ‹7๏ผšๆˆ‘็š„็ป้ชŒๆ˜ฏ\"ๅŒๅไธ€\"ๅ‰ๅ…ˆๅœจ็ฝ‘ๅบ—็œ‹็œ‹ๆƒณไนฐ็š„ไธœ่ฅฟ๏ผŒๅฆ‚ๆžœ\"ๅŒๅไธ€\"้™ไปทไบ†ๅฐฑไนฐใ€‚\n\n็ฝ‘ๅ‹8๏ผšๅฏนๆˆ‘ไปฌไธญๅฐๅบ—ไธปๆฅ่ฏด๏ผŒ\"ๅŒๅไธ€\"ๆœŸ้—ด๏ผŒ้’ฑ็œŸ่ตšไธไบ†ๅคšๅฐ‘๏ผŒไธป่ฆๆ˜ฏ่ตšไบบๆฐ”ใ€‚", + "wordCount": 271, + "questions": [ + { + "question": "็ป™\"ๅŒๅไธ€\"ๅฅฝ่ฏ„็š„ๆ˜ฏๅ“ชๅ‡ ไฝ็ฝ‘ๅ‹๏ผŸ", + "type": "open", + "answer": "็ฝ‘ๅ‹3ใ€็ฝ‘ๅ‹4" + }, + { + "question": "็ป™\"ๅŒๅไธ€\"ๅทฎ่ฏ„็š„ๆ˜ฏๅ“ชๅ‡ ไฝ็ฝ‘ๅ‹๏ผŸ", + "type": "open", + "answer": "็ฝ‘ๅ‹1ใ€็ฝ‘ๅ‹2ใ€็ฝ‘ๅ‹5" + }, + { + "question": "ๅ“ชไบ›็ฝ‘ๅ‹ๅฏน\"ๅŒๅไธ€\"่ดญ็‰ฉ็ป™ๅ‡บไบ†ๅปบ่ฎฎ๏ผŸ", + "type": "open", + "answer": "็ฝ‘ๅ‹7๏ผšๅ…ˆ็œ‹ไปทๆ ผ๏ผŒๅฆ‚ๆžœ้™ไปทไบ†ๅ†ไนฐ" + }, + { + "question": "็ฝ‘ๅ‹ไธญๅ“ชๅ‡ ไฝๆ˜ฏๅ–ๅฎถ๏ผŸไป–ไปฌๅฏน\"ๅŒๅไธ€\"ๆ˜ฏไป€ไนˆๆ€ๅบฆ๏ผŸ", + "type": "open", + "answer": "็ฝ‘ๅ‹6ใ€็ฝ‘ๅ‹8ใ€‚็ฝ‘ๅ‹6่ฏดๅคง้ƒจๅˆ†็ฝ‘ๅบ—ไผš้™ไปท๏ผ›็ฝ‘ๅ‹8่ฏด่ตšไธไบ†ๅคšๅฐ‘้’ฑ๏ผŒไธป่ฆๆ˜ฏ่ตšไบบๆฐ”" + } + ] + } + ], + "exercises": [ + { + "type": "vocabulary_inference", + "title": "้€š่ฟ‡่ฏ็ผ€็Œœๆต‹่ฏไน‰ (Infer meaning through affixes)", + "description": "Chinese has morphemes that function like affixes: ๅฎถ (expert), ่€… (person), ๆ‰‹ (skilled person), ็ƒญ (craze)", + "questions": [ + { + "question": "่ฟ™ๅฎถ็ฝ‘็ซ™ๆœ‰ๅพˆๅคšๅ›ฝๅค–็š„ไนฐๅฎถใ€‚", + "hint": "ๅฎถ = person who does something", + "answer": "buyer" + }, + { + "question": "่ฟ™ๅฎถไนฆๅบ—ไธพๅŠžไบ†ไฝœ่€…ไธŽ่ฏป่€…่ง้ขไผšใ€‚", + "hint": "่€… = person who does something", + "answer": "author and reader" + }, + { + "question": "ๅœจ็ฝ‘ไธŠไนฐไธœ่ฅฟ๏ผŒ็‰นๅˆซๆ˜ฏๆ–ฐๆ‰‹๏ผŒๅฏ่ƒฝไผš้‡ๅˆฐไธ€ไบ›้—ฎ้ข˜ใ€‚", + "hint": "ๆ‰‹ = person skilled at something", + "answer": "novice, beginner" + }, + { + "question": "็ฝ‘่ดญ็ƒญๅทฒ็ปไปŽๅŸŽๅธ‚ๅˆฐไบ†ไนกๆ‘ใ€‚", + "hint": "็ƒญ = social trend/craze", + "answer": "online shopping craze" + } + ] + }, + { + "type": "phrase_matching", + "title": "ไปŽ่ฏพๆ–‡ไธญๆ‰พๅ‡บไธŽไธ‹ๅˆ—่ฏดๆณ•ๆ„ๆ€็›ธ่ฟ‘็š„่ฏ่ฏญ", + "questions": [ + { + "question": "ไธœ่ฅฟๆฏ”ไปฅๅ‰่ดตไบ†๏ผˆ็ฝ‘ๅ‹1๏ผ‰", + "answer": "ๆถจไปท" + }, + { + "question": "ๆฒกๆœ‰็ป้ชŒ็š„ไนฐๅฎถ๏ผˆ็ฝ‘ๅ‹1๏ผ‰", + "answer": "ๆ–ฐๆ‰‹" + }, + { + "question": "็”จๆ‰“ๆŠ˜ใ€้€็คผ็‰ฉ็š„ๆ–นๅผๅ–ไธœ่ฅฟ๏ผˆ็ฝ‘ๅ‹2๏ผ‰", + "answer": "ไฟƒ้”€" + }, + { + "question": "ไธœ่ฅฟๅฅฝ๏ผŒ่€Œไธ”ไปทๆ ผไพฟๅฎœ๏ผˆ็ฝ‘ๅ‹3๏ผ‰", + "answer": "ไปทๅป‰็‰ฉ็พŽ" + }, + { + "question": "ไธœ่ฅฟๆฏ”ไปฅๅ‰ไพฟๅฎœไบ†๏ผˆ็ฝ‘ๅ‹6๏ผ‰", + "answer": "้™ไปท" + }, + { + "question": "ๅขžๅŠ ๅ—ๅ…ณๆณจใ€ๅ—ๆฌข่ฟŽ็š„็จ‹ๅบฆ๏ผˆ็ฝ‘ๅ‹8๏ผ‰", + "answer": "่ตšไบบๆฐ”" + } + ] + } + ] +} diff --git a/content/chapters/ledu-chapter3.json b/content/chapters/ledu-chapter3.json new file mode 100644 index 0000000..5435276 --- /dev/null +++ b/content/chapters/ledu-chapter3.json @@ -0,0 +1,643 @@ +{ + "id": "ledu-chapter3", + "book_id": "ledu", + "name": "็”Ÿๅ‘ฝๅœจไบŽ่ฟๅŠจ (Life Lies in Movement)", + "description": "Comprehensive chapter on sports, fitness, healthy lifestyle, and the importance of making exercise a habit. Includes reading about ping-pong history and Chinese sports culture.", + "difficulty": "intermediate", + "language": "zh-CN", + "chapter_number": "3", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "LEDU Textbook - Jiaotong University", + "target_level": "intermediate", + "estimated_hours": 10, + "prerequisites": ["ledu-chapter1", "ledu-chapter2"], + "learning_objectives": [ + "Master 35+ sports and fitness vocabulary terms", + "Understand strategies for building exercise habits", + "Learn about ping-pong history and sports in China", + "Practice reading comprehension with authentic texts", + "Develop contextual vocabulary inference skills" + ], + "content_tags": ["sports", "fitness", "health", "habits", "chinese-culture", "ping-pong"], + "completion_criteria": { + "vocabulary_mastery": 90, + "comprehension_score": 80, + "exercises_completed": 18 + } + }, + "vocabulary": { + "ไน ๆƒฏ": { + "pronunciation": "xรญguร n", + "type": "noun", + "user_language": "habit", + "examples": ["ๆˆไธบไน ๆƒฏ", "็”Ÿๆดปไน ๆƒฏ", "ๅฅฝไน ๆƒฏ"], + "notes": "Can also be used as verb meaning 'to be accustomed to'" + }, + "ๅฅ่บซ": { + "pronunciation": "jiร nshฤ“n", + "type": "verb", + "user_language": "to work out, to do fitness", + "examples": ["ๅฅ่บซๆˆฟ", "ๅฅ่บซ่ฟๅŠจ"], + "notes": "Very common word for working out or exercising" + }, + "ๅŽ‹ๅŠ›": { + "pronunciation": "yฤlรฌ", + "type": "noun", + "user_language": "pressure, stress", + "examples": ["็”ŸๆดปๅŽ‹ๅŠ›", "ๅทฅไฝœๅŽ‹ๅŠ›", "ๅŽ‹ๅŠ›ๅคง"] + }, + "้”ป็‚ผ": { + "pronunciation": "duร nliร n", + "type": "verb", + "user_language": "to exercise, to work out", + "examples": ["้”ป็‚ผ่บซไฝ“", "้”ป็‚ผ่ƒฝๅŠ›"] + }, + "่บซๆ": { + "pronunciation": "shฤ“ncรกi", + "type": "noun", + "user_language": "figure, physique", + "examples": ["่บซๆ่‹—ๆก", "ไธญ็ญ‰่บซๆ"] + }, + "ๆ”พๆพ": { + "pronunciation": "fร ngsลng", + "type": "verb", + "user_language": "to relax, to loosen up", + "examples": ["ๆ”พๆพ่‚Œ่‚‰", "ๆ”พๆพๅฟƒๆƒ…", "ๆ”พๆพ่บซๅฟƒ"] + }, + "ๅšๆŒ": { + "pronunciation": "jiฤnchรญ", + "type": "verb", + "user_language": "to persist, to persevere", + "examples": ["ๅšๆŒ่ฟๅŠจ", "ๅšๆŒ่‡ชๅทฑ็š„ๆ„่ง", "ๅšๆŒไธ‹ๆฅ"] + }, + "้€‚ๅบ”": { + "pronunciation": "shรฌyรฌng", + "type": "verb", + "user_language": "to adapt, to adjust to", + "examples": ["้€‚ๅบ”็”Ÿๆดป", "้€‚ๅบ”็Žฏๅขƒ"] + }, + "ๅผบๅบฆ": { + "pronunciation": "qiรกngdรน", + "type": "noun", + "user_language": "intensity", + "examples": ["่ฟๅŠจๅผบๅบฆ", "ๅทฅไฝœๅผบๅบฆ"] + }, + "ไฝ“": { + "pronunciation": "tว", + "type": "noun/morpheme", + "user_language": "body", + "examples": ["ไฝ“้‡", "ไฝ“่‚ฒ", "็‰ฉไฝ“", "ไฝ“้ชŒ"], + "notes": "Usually used as morpheme, not alone" + }, + "ๅŠ›": { + "pronunciation": "lรฌ", + "type": "noun/morpheme", + "user_language": "force, power", + "examples": ["ไฝ“ๅŠ›", "้ฃŽๅŠ›", "ๅฌๅŠ›", "่ง†ๅŠ›"], + "notes": "Usually used as morpheme" + }, + "ไผฆๆ•ฆ": { + "pronunciation": "Lรบndลซn", + "type": "proper noun", + "user_language": "London" + }, + "็ปณๅญ": { + "pronunciation": "shรฉngzi", + "type": "noun", + "user_language": "rope" + }, + "่ฝฏๆœจๅกž": { + "pronunciation": "ruวŽnmรนsฤi", + "type": "noun", + "user_language": "cork" + }, + "ๆฌงๆดฒ": { + "pronunciation": "ลŒuzhลu", + "type": "proper noun", + "user_language": "Europe" + }, + "ๆต่กŒ": { + "pronunciation": "liรบxรญng", + "type": "verb", + "user_language": "to be popular, to prevail" + }, + "ไธ–ไน’่ต›": { + "pronunciation": "Shรฌpฤซngsร i", + "type": "proper noun", + "user_language": "World Table Tennis Championships" + }, + "ไธพๅŠž": { + "pronunciation": "jว”bร n", + "type": "verb", + "user_language": "to hold, to organize (an event)" + }, + "ๅจฑไน": { + "pronunciation": "yรบlรจ", + "type": "verb/noun", + "user_language": "to entertain; entertainment" + }, + "ไธœไบš": { + "pronunciation": "Dลng Yร ", + "type": "proper noun", + "user_language": "East Asia" + }, + "ๆŽฅ่งฆ": { + "pronunciation": "jiฤ“chรน", + "type": "verb", + "user_language": "to touch, to have contact with" + }, + "ๆŠ€ๅทง": { + "pronunciation": "jรฌqiวŽo", + "type": "noun", + "user_language": "technique, skill" + }, + "่ƒœ": { + "pronunciation": "shรจng", + "type": "verb", + "user_language": "to win, to triumph" + }, + "ๅ† ๅ†›": { + "pronunciation": "guร njลซn", + "type": "noun", + "user_language": "champion" + }, + "่ฟŽๆˆ˜": { + "pronunciation": "yรญngzhร n", + "type": "verb", + "user_language": "to face (in a match)" + }, + "ๅ‡ป่ดฅ": { + "pronunciation": "jฤซbร i", + "type": "verb", + "user_language": "to defeat, to beat" + }, + "ไปฅๅผฑ่ƒœๅผบ": { + "pronunciation": "yว ruรฒ shรจng qiรกng", + "type": "idiom", + "user_language": "to defeat the strong with the weak" + }, + "็‘œไผฝ": { + "pronunciation": "yรบjiฤ", + "type": "noun", + "user_language": "yoga" + }, + "่ก—่ˆž": { + "pronunciation": "jiฤ“wว”", + "type": "noun", + "user_language": "street dance" + }, + "่‚š็šฎ่ˆž": { + "pronunciation": "dรนpรญwว”", + "type": "noun", + "user_language": "belly dance" + }, + "ๆ‹‰ไธ่ˆž": { + "pronunciation": "lฤdฤซngwว”", + "type": "noun", + "user_language": "Latin dance" + }, + "่Šญ่•พ่ˆž": { + "pronunciation": "bฤlฤ›iwว”", + "type": "noun", + "user_language": "ballet" + }, + "ๆœ‰ๆฐงๆ“": { + "pronunciation": "yว’uyวŽngcฤo", + "type": "noun", + "user_language": "aerobics" + }, + "ๆ™ฎๆ‹‰ๆ": { + "pronunciation": "pว”lฤtรญ", + "type": "noun", + "user_language": "Pilates" + }, + "ไพฟ": { + "pronunciation": "biร n", + "type": "adverb", + "user_language": "then, therefore (written language)", + "examples": ["ไป–ๅพˆๅฅฝๅญฆ๏ผŒไธๆ‡‚ไพฟ้—ฎ", "ไป–ไปฌไฟฉๅคงๅญฆไธ€ๆฏ•ไธšไพฟ็ป“ๅฉšไบ†"], + "notes": "Written Chinese, means 'ๅฐฑ'" + }, + "ไปฅ": { + "pronunciation": "yว", + "type": "preposition", + "user_language": "with, to use (written language)", + "examples": ["ๅญฉๅญไธๅบ”่ฏฅไปฅ่ฟ™็งๆ€ๅบฆ่ทŸ็ˆถๆฏ่ฏด่ฏ", "ไปฅไป€ไนˆๆ ท็š„ๆ–นๆณ•ๆ•™ๅญฉๅญ"], + "notes": "Written Chinese, means '็”จ'" + }, + "็€่ฟท": { + "pronunciation": "zhรกomรญ", + "type": "verb", + "user_language": "to be fascinated, to be captivated" + }, + "ๅช่ฆ": { + "pronunciation": "zhวyร o", + "type": "conjunction", + "user_language": "as long as, so long as" + }, + "ๆ”นๅ˜": { + "pronunciation": "gวŽibiร n", + "type": "verb", + "user_language": "to change" + }, + "็”Ÿๆดปๆ–นๅผ": { + "pronunciation": "shฤ“nghuรณ fฤngshรฌ", + "type": "noun", + "user_language": "lifestyle" + }, + "ๆˆไธบ": { + "pronunciation": "chรฉngwรฉi", + "type": "verb", + "user_language": "to become" + }, + "่ˆ’ๆœ": { + "pronunciation": "shลซfu", + "type": "adjective", + "user_language": "comfortable" + }, + "็ง˜่ฏ€": { + "pronunciation": "mรฌjuรฉ", + "type": "noun", + "user_language": "secret, key, tip" + }, + "ๅฎš": { + "pronunciation": "dรฌng", + "type": "verb", + "user_language": "to fix, to set" + }, + "่ฟๅŠจ้‡": { + "pronunciation": "yรนndรฒngliร ng", + "type": "noun", + "user_language": "amount of exercise" + }, + "ๆ…ขๆ…ขๅœฐ": { + "pronunciation": "mร nmร n de", + "type": "adverb", + "user_language": "slowly, gradually" + }, + "ๅŠ ๅคง": { + "pronunciation": "jiฤdร ", + "type": "verb", + "user_language": "to increase, to enlarge" + }, + "ๅฐ่ฏ•": { + "pronunciation": "chรกngshรฌ", + "type": "verb", + "user_language": "to try, to attempt" + }, + "้กน็›ฎ": { + "pronunciation": "xiร ngmรน", + "type": "noun", + "user_language": "project, item, event" + }, + "้ƒจไฝ": { + "pronunciation": "bรนwรจi", + "type": "noun", + "user_language": "part, location (of body)" + }, + "ๅขžๅŠ ": { + "pronunciation": "zฤ“ngjiฤ", + "type": "verb", + "user_language": "to increase, to add" + }, + "ๅทๆ‡’": { + "pronunciation": "tลulวŽn", + "type": "verb", + "user_language": "to be lazy, to slack off" + }, + "่ฃ…ๅค‡": { + "pronunciation": "zhuฤngbรจi", + "type": "noun", + "user_language": "equipment, gear" + }, + "็ฉฟๆˆด": { + "pronunciation": "chuฤndร i", + "type": "verb", + "user_language": "to wear, to put on" + }, + "ๅ‡†ๅค‡": { + "pronunciation": "zhว”nbรจi", + "type": "verb", + "user_language": "to prepare, to get ready" + }, + "ๅ˜ๆˆ": { + "pronunciation": "biร nchรฉng", + "type": "verb", + "user_language": "to become, to turn into" + }, + "ๅฝ“็„ถ": { + "pronunciation": "dฤngrรกn", + "type": "adverb", + "user_language": "of course, naturally" + }, + "ๆ„ฟๆ„": { + "pronunciation": "yuร nyรฌ", + "type": "auxiliary verb", + "user_language": "willing, to be willing" + }, + "้‡่ฆ": { + "pronunciation": "zhรฒngyร o", + "type": "adjective", + "user_language": "important" + }, + "ๆฎ่ฏด": { + "pronunciation": "jรนshuล", + "type": "verb", + "user_language": "it is said, allegedly" + }, + "ไธ–็บช": { + "pronunciation": "shรฌjรฌ", + "type": "noun", + "user_language": "century" + }, + "ๅคฉๆฐ”": { + "pronunciation": "tiฤnqรฌ", + "type": "noun", + "user_language": "weather" + }, + "็ฝ‘็ƒ": { + "pronunciation": "wวŽngqiรบ", + "type": "noun", + "user_language": "tennis" + }, + "ๅŠžๆณ•": { + "pronunciation": "bร nfวŽ", + "type": "noun", + "user_language": "method, way, solution" + }, + "้คๆกŒ": { + "pronunciation": "cฤnzhuล", + "type": "noun", + "user_language": "dining table" + }, + "่ฏž็”Ÿ": { + "pronunciation": "dร nshฤ“ng", + "type": "verb", + "user_language": "to be born, to come into being" + }, + "ๅ‡บ็Žฐ": { + "pronunciation": "chลซxiร n", + "type": "verb", + "user_language": "to appear, to emerge" + }, + "ไธไน…": { + "pronunciation": "bรนjiว”", + "type": "noun", + "user_language": "soon, before long" + }, + "ๅ—ๆฌข่ฟŽ": { + "pronunciation": "shรฒu huฤnyรญng", + "type": "verb phrase", + "user_language": "to be popular, to be welcomed" + }, + "ๅ„ๅ›ฝ": { + "pronunciation": "gรจguรณ", + "type": "noun", + "user_language": "various countries" + }, + "่ตทๆบๅœฐ": { + "pronunciation": "qวyuรกndรฌ", + "type": "noun", + "user_language": "place of origin" + }, + "ๆˆ็ปฉ": { + "pronunciation": "chรฉngjฤซ", + "type": "noun", + "user_language": "result, achievement, grade" + }, + "ๅฝ“ไฝœ": { + "pronunciation": "dร ngzuรฒ", + "type": "verb", + "user_language": "to regard as, to treat as" + }, + "ๆดปๅŠจ": { + "pronunciation": "huรณdรฒng", + "type": "noun", + "user_language": "activity" + }, + "ๆฅๅˆฐ": { + "pronunciation": "lรกidร o", + "type": "verb", + "user_language": "to arrive, to come to" + }, + "ๅœบๅœฐ": { + "pronunciation": "chวŽngdรฌ", + "type": "noun", + "user_language": "venue, space, field" + }, + "็”จๅ…ท": { + "pronunciation": "yรฒngjรน", + "type": "noun", + "user_language": "equipment, utensil" + }, + "็ฎ€ๅ•": { + "pronunciation": "jiวŽndฤn", + "type": "adjective", + "user_language": "simple, easy" + }, + "ๅฆๅค–": { + "pronunciation": "lรฌngwร i", + "type": "conjunction", + "user_language": "in addition, besides" + }, + "ๅ–œๆฌข": { + "pronunciation": "xวhuan", + "type": "verb", + "user_language": "to like" + }, + "่บซไฝ“": { + "pronunciation": "shฤ“ntว", + "type": "noun", + "user_language": "body" + }, + "ๅ–่ƒœ": { + "pronunciation": "qว”shรจng", + "type": "verb", + "user_language": "to win, to triumph" + }, + "ๅ› ๆญค": { + "pronunciation": "yฤซncว", + "type": "conjunction", + "user_language": "therefore, thus" + }, + "ๅ‘ๅฑ•": { + "pronunciation": "fฤzhวŽn", + "type": "verb", + "user_language": "to develop" + }, + "ๅคšๅนดๆฅ": { + "pronunciation": "duล niรกn lรกi", + "type": "phrase", + "user_language": "for many years" + }, + "ไธ–็•Œ": { + "pronunciation": "shรฌjiรจ", + "type": "noun", + "user_language": "world" + }, + "ๆฏ”่ต›": { + "pronunciation": "bวsร i", + "type": "noun", + "user_language": "competition, match" + }, + "้€‰ๆ‰‹": { + "pronunciation": "xuวŽnshว’u", + "type": "noun", + "user_language": "player, athlete" + }, + "ๅซ": { + "pronunciation": "jiร o", + "type": "verb", + "user_language": "to call, to be called" + }, + "ไปŽๆญค": { + "pronunciation": "cรณngcว", + "type": "adverb", + "user_language": "from then on, since then" + }, + "ไผ ๅˆฐ": { + "pronunciation": "chuรกndร o", + "type": "verb", + "user_language": "to spread to, to reach" + }, + "็›ฎๅ‰": { + "pronunciation": "mรนqiรกn", + "type": "noun", + "user_language": "currently, at present" + }, + "ๆฐดๅนณ": { + "pronunciation": "shuวpรญng", + "type": "noun", + "user_language": "level, standard" + }, + "ๅ›ฝ็ƒ": { + "pronunciation": "guรณqiรบ", + "type": "noun", + "user_language": "national ball game" + } + }, + "grammar": { + "ๆ—ข-ๅˆ-ไนŸ": { + "title": "ๆ—ขโ€ฆโ€ฆ๏ผˆ๏ผŒ๏ผ‰ๅˆ/ไนŸโ€ฆโ€ฆ - both...and...", + "pattern": "ๆ—ข + Adj/Verb + ๅˆ/ไนŸ + Adj/Verb", + "explanation": "Used to express that something has two qualities or characteristics at the same time", + "examples": [ + { + "chinese": "่ฟ™ไธชๅญฉๅญๆ—ข่ชๆ˜Žๅˆๅฏ็ˆฑใ€‚", + "pronunciation": "Zhรจge hรกizi jรฌ cลngming yรฒu kฤ›'ร i.", + "translation": "This child is both intelligent and cute." + }, + { + "chinese": "ไป–ๆ—ขไผš่ธข่ถณ็ƒ๏ผŒไนŸไผšๆ‰“็ฏฎ็ƒใ€‚", + "pronunciation": "Tฤ jรฌ huรฌ tฤซ zรบqiรบ, yฤ› huรฌ dวŽ lรกnqiรบ.", + "translation": "He can both play soccer and basketball." + }, + { + "chinese": "ไธๅŒ็š„่ฟๅŠจๆ—ข่ƒฝ้”ป็‚ผ่บซไฝ“็š„ไธๅŒ้ƒจไฝ๏ผŒไนŸๅฏไปฅๅขžๅŠ ่ฟๅŠจ็š„ไน่ถฃใ€‚", + "pronunciation": "Bรนtรณng de yรนndรฒng jรฌ nรฉng duร nliร n shฤ“ntว de bรนtรณng bรนwรจi, yฤ› kฤ›yว zฤ“ngjiฤ yรนndรฒng de lรจqรน.", + "translation": "Different sports can both train different parts of the body and increase the enjoyment of exercise." + } + ] + }, + "ไพฟ-written": { + "title": "ไพฟ (biร n) - then, therefore (written language)", + "explanation": "Used in written Chinese to mean 'ๅฐฑ' (then, therefore). More formal than ๅฐฑ.", + "examples": [ + { + "chinese": "ไป–ๅพˆๅฅฝๅญฆ๏ผŒไธๆ‡‚ไพฟ้—ฎใ€‚", + "pronunciation": "Tฤ hฤ›n hร oxuรฉ, bรน dว’ng biร n wรจn.", + "translation": "He is studious; when he doesn't understand, he asks right away." + }, + { + "chinese": "ไป–ไปฌไฟฉๅคงๅญฆไธ€ๆฏ•ไธšไพฟ็ป“ๅฉšไบ†ใ€‚", + "pronunciation": "Tฤmen liวŽ dร xuรฉ yฤซ bรฌyรจ biร n jiรฉhลซn le.", + "translation": "As soon as they graduated from university, they got married." + } + ] + }, + "ไปฅ-written": { + "title": "ไปฅ (yว) - with, to use (written language)", + "explanation": "Used in written Chinese to mean '็”จ' (to use). More formal writing style.", + "examples": [ + { + "chinese": "ๅญฉๅญไธๅบ”่ฏฅไปฅ่ฟ™็งๆ€ๅบฆ่ทŸ็ˆถๆฏ่ฏด่ฏใ€‚", + "pronunciation": "Hรกizi bรน yฤซnggฤi yว zhรจ zhว’ng tร idu gฤ“n fรนmว” shuลhuร .", + "translation": "A child should not speak to parents with this kind of attitude." + }, + { + "chinese": "็ˆถๆฏ่ฆๅฅฝๅฅฝๆƒณไธ€ๆƒณ๏ผŒไปฅไป€ไนˆๆ ท็š„ๆ–นๆณ•ๆ•™ๅญฉๅญๆ›ดๅฅฝใ€‚", + "pronunciation": "Fรนmว” yร o hวŽohao xiวŽng yฤซxiวŽng, yว shรฉnme yร ng de fฤngfวŽ jiฤo hรกizi gรจng hวŽo.", + "translation": "Parents should think carefully about what method to use to teach their children better." + } + ] + } + }, + "texts": [ + { + "id": "main-text", + "title": "่ฎฉ่ฟๅŠจๆˆไธบไน ๆƒฏ (Make Exercise Become a Habit)", + "type": "main", + "content": "่ฎฉ่ฟๅŠจๆˆไธบไน ๆƒฏ๏ผŸๅพˆๅคšไบบ่ง‰ๅพ—ไธŠ็ญ้ƒฝ้‚ฃไนˆๅฟ™ใ€้‚ฃไนˆ็ดฏไบ†๏ผŒๅ“ช้‡Œๆœ‰ๆ—ถ้—ดๅ’ŒๅŠ›ๆฐ”ๅŽป่ฟๅŠจๅฅ่บซ๏ผŸไฝ†ๆœ‰ๅฅ่บซ็ป้ชŒ็š„ไบบไพฟไผšๆ˜Ž็™ฝ๏ผŒๅŽ‹ๅŠ›่ถŠๅคง่ถŠๅบ”่ฏฅ้”ป็‚ผ๏ผŒๅ› ไธบๅฎƒไธไป…ๅฏไปฅ่ฎฉไฝ ๆ‹ฅๆœ‰ๅฅฝ่บซๆ๏ผŒ่€Œไธ”ๅฏไปฅๆ”พๆพ่บซๅฟƒ๏ผŒๅธฆ็ป™ไบบๅฅๅบทๅ’Œๅฟซไนใ€‚\n\nๅ…ถๅฎž๏ผŒไธ‡ไบ‹ๅผ€ๅคด้šพ๏ผŒๅช่ฆๆ”นๅ˜ไธ€ไธ‹ไฝ ็š„็”Ÿๆดปๆ–นๅผ๏ผŒๆŠŠๅผ€ๅคด็š„้‚ฃไธ€ๆฎตๆ—ฅๅญๅšๆŒไธ‹ๆฅ๏ผŒๅฐฑ่ƒฝ่ฎฉไฝ ๅฏน่ฟๅŠจ็€่ฟทใ€‚ๆฏๅคฉ็š„ๅฅ่บซๅฐฑไผš่ทŸๅƒ้ฅญใ€็ก่ง‰ไธ€ๆ ท๏ผŒๆˆไธบไฝ ็”Ÿๆดปไธญไธๅฏ็ผบๅฐ‘็š„ไธ€้ƒจๅˆ†๏ผŒๅ“ชๅคฉไธ่ฟๅŠจไฝ ๅฐฑไผš่ง‰ๅพ—ไธ่ˆ’ๆœใ€‚้‚ฃไนˆ๏ผŒ่ฎฉ่ฟๅŠจๆˆไธบไน ๆƒฏๆœ‰ๅ“ชไบ›็ง˜่ฏ€ๅ‘ข๏ผŸ\n\n็ฌฌไธ€๏ผŒๅฎšๅฅฝ่ฟๅŠจ็š„ๆ—ถ้—ด๏ผŒๆฏๅคฉ้ƒฝๆ˜ฏ่ฟ™ไธชๆ—ถ้—ด๏ผŒไธ่ฆๆ”นๅ˜ใ€‚\n\n็ฌฌไบŒ๏ผŒๅผ€ๅง‹ๆ—ถ่ฟๅŠจ้‡ไธ่ฆๅคชๅคง๏ผŒๅช้”ป็‚ผ10๏ฝž15ๅˆ†้’Ÿๅฐฑๅฏไปฅไบ†๏ผŒ่€Œไธ”ไธ่ฆๅšๅผบๅบฆๅคชๅคง็š„่ฟๅŠจ๏ผŒ่ฆ่ฎฉไฝ ็š„่บซไฝ“ๆ…ขๆ…ขๅœฐ้€‚ๅบ”ใ€‚็„ถๅŽๅฏไปฅๆ…ขๆ…ขๅœฐๅŠ ๅคง่ฟๅŠจ้‡ๅ’Œ่ฟๅŠจๅผบๅบฆ๏ผŒไธ่ฟ‡ๆœ€ๅฐ‘ๅœจไธคๅ‘จไปฅๅŽๅ†่ฟ™ๆ ทๅšใ€‚\n\n็ฌฌไธ‰๏ผŒๅคšๅฐ่ฏ•ๅ‡ ็ง่ฟๅŠจ้กน็›ฎ๏ผŒไธๅŒ็š„่ฟๅŠจๆ—ข่ƒฝ้”ป็‚ผ่บซไฝ“็š„ไธๅŒ้ƒจไฝ๏ผŒไนŸๅฏไปฅๅขžๅŠ ่ฟๅŠจ็š„ไน่ถฃใ€‚\n\n็ฌฌๅ››๏ผŒๅ’Œๆœ‹ๅ‹ไธ€่ตทๅŽปๅฅ่บซ๏ผŒ่ฟ™ๆ ทๅฆ‚ๆžœๆƒณ่ฆๅทๆ‡’๏ผŒไผšๆœ‰ไบบ็œ‹็€ไฝ ใ€‚\n\n็ฌฌไบ”๏ผŒ็ป™่‡ชๅทฑไนฐไบ›ๅผ€ๅฟƒ็š„่ฟๅŠจ่ฃ…ๅค‡๏ผŒ็ฉฟๆˆดไธŠๅฎƒไปฌไผš่ฎฉ่‡ชๅทฑ็š„่บซๅฟƒๅšๅฅฝๅ‡†ๅค‡ใ€‚\n\n็ฌฌๅ…ญ๏ผŒ่ฎฉ่ฟๅŠจๅ˜ๆˆไธ€็งๅฟซไน๏ผŒๅฆ‚ๆžœ่ฟๅŠจ่ฎฉไฝ ๆ„Ÿๅˆฐๅฟซไน๏ผŒไฝ ๅฝ“็„ถๆ„ฟๆ„ๅŽปๅšใ€‚\n\nๅฝ“็„ถ๏ผŒๆœ€้‡่ฆ็š„ไธ€็‚นโ€”โ€”่ดตๅœจๅšๆŒใ€‚", + "wordCount": 488, + "questions": [ + { + "question": "่ฏพๆ–‡ไธญๆๅˆฐไบ†ๅ“ชไบ›่ฎฉ่ฟๅŠจๆˆไธบไน ๆƒฏ็š„ๆ–นๆณ•๏ผŸ", + "type": "open", + "answer": "ๅ…ญไธชๆ–นๆณ•ๅŠ ไธ€ไธช้‡็‚น๏ผš1)ๅฎšๅฅฝๆ—ถ้—ด 2)ๅผ€ๅง‹ๆ—ถ่ฟๅŠจ้‡ไธ่ฆๅคชๅคง 3)ๅคšๅฐ่ฏ•ๅ‡ ็ง่ฟๅŠจ 4)ๅ’Œๆœ‹ๅ‹ไธ€่ตท 5)ไนฐๅผ€ๅฟƒ็š„่ฟๅŠจ่ฃ…ๅค‡ 6)่ฎฉ่ฟๅŠจๅ˜ๆˆๅฟซไน ้‡็‚น๏ผš่ดตๅœจๅšๆŒ" + }, + { + "question": "ๅฅๅญไธญ็š„\"ๅฎƒ\"่ฏด็š„ๆ˜ฏ๏ผšAๅŽ‹ๅŠ› B้”ป็‚ผ", + "type": "multiple_choice", + "options": ["AๅŽ‹ๅŠ›", "B้”ป็‚ผ"], + "correctAnswer": "B้”ป็‚ผ" + }, + { + "question": "่ฎฉ่ฟๅŠจๆˆไธบไน ๆƒฏๆœ€้‡่ฆ็š„ๆ˜ฏไป€ไนˆ๏ผŸ", + "type": "multiple_choice", + "options": ["Aๆ‰พๅˆฐๅ–œๆฌข็š„่ฟๅŠจ", "Bๅฎšๅฅฝ่ฟๅŠจ็š„ๆ—ถ้—ด", "Cๅคšๅฐ่ฏ•ๅ‡ ็ง่ฟๅŠจ", "D่ฆๆฏๅคฉๅšๆŒ่ฟๅŠจ"], + "correctAnswer": "D่ฆๆฏๅคฉๅšๆŒ่ฟๅŠจ" + } + ] + }, + { + "id": "pingpong-text", + "title": "ไน’ไน“็ƒ็š„็”ฑๆฅ (The Origin of Ping-Pong)", + "type": "extensive", + "content": "ไน’ไน“็ƒ่‹ฑๆ–‡ๅซไฝœtable tennis๏ผŒๅฎƒๆ˜ฏๆ€Žไนˆๆฅ็š„ๅ‘ข๏ผŸๆฎ่ฏด๏ผŒๅœจ19ไธ–็บชๆœซ็š„ไธ€ๅคฉ๏ผŒไผฆๆ•ฆๅคฉๆฐ”้žๅธธ็ƒญ๏ผŒ่€Œไธ”ๆœ‰้›จใ€‚ๆœ‰ไธคไธชๅนด่ฝปไบบไธ่ƒฝๅŽปๅค–่พนๆ‰“็ฝ‘็ƒ๏ผŒๅฐฑๆƒณไบ†ไธ€ไธชๅŠžๆณ•๏ผŒไปฅ้ฅญ้ฆ†็š„ๅคง้คๆกŒไฝœ็ƒๅฐ๏ผŒไธญ้—ด็”จ็ปณๅญไฝœ็ฝ‘๏ผŒ้…’็“ถ็š„่ฝฏๆœจๅกžไฝœ็ƒ๏ผŒ็”จ็ƒŸ็›’ๆ‰“็ƒใ€‚ๅฅณๅบ—ไธป่งๅˆฐไบ†๏ผŒๅคงๅฃฐ่ฏด\"Table Tennis๏ผTable Tennis๏ผ\"ๅฐฑ่ฟ™ๆ ท๏ผŒไน’ไน“็ƒ่ฟๅŠจ่ฏž็”Ÿไบ†ใ€‚\n\nไน’ไน“็ƒๅ‡บ็ŽฐๅŽไธไน…๏ผŒไพฟๆˆไบ†ไธ€็งๅพˆๅ—ๆฌข่ฟŽ็š„่ฟๅŠจ๏ผŒๅœจๆฌงๆดฒๅ„ๅ›ฝๆต่กŒใ€‚1926ๅนด๏ผŒ็ฌฌไธ€ๅฑŠไธ–ไน’่ต›ๅœจไน’ไน“็ƒ็š„่ตทๆบๅœฐโ€”โ€”ไผฆๆ•ฆไธพๅŠž๏ผŒไฝ†่‹ฑๅ›ฝไบบ็š„ๆฏ”่ต›ๆˆ็ปฉไธๅคชๅฅฝ๏ผŒไป–ไปฌๆฒกๆœ‰็œŸๆญฃ้‡่ง†่ฟ™้กน่ฟๅŠจ๏ผŒๅชๆ˜ฏๆŠŠไน’ไน“็ƒๅฝ“ไฝœๅจฑไนๆดปๅŠจใ€‚\n\nไน’ไน“็ƒ่ฟๅŠจๅœจ20ไธ–็บชๅˆๆฅๅˆฐไธœไบšใ€‚ไธœไบšๅ›ฝๅฎถไบบๅคšๅœฐๅฐ‘๏ผŒไน’ไน“็ƒๅช้œ€่ฆๅพˆๅฐ็š„ๅœบๅœฐ๏ผŒ็”จๅ…ทไนŸๅพˆ็ฎ€ๅ•๏ผŒๅฎคๅ†…ๅฎคๅค–้ƒฝๅฏไปฅๆ‰“ใ€‚ๅฆๅค–๏ผŒไธœไบšไบบๆ›ดๅ–œๆฌขๆฒกๆœ‰่บซไฝ“ๆŽฅ่งฆ็š„่ฟๅŠจ๏ผŒ่ฎฒ็ฉถไปฅๆŠ€ๅทงๅ–่ƒœใ€‚ๅ› ๆญคไน’ไน“็ƒๅœจไธœไบšๅพˆๅ—ๆฌข่ฟŽ๏ผŒๅฟซ้€Ÿๅ‘ๅฑ•ไบ†่ตทๆฅใ€‚ๅคšๅนดๆฅ๏ผŒไธ–็•ŒไธŠๅ„็งไน’ไน“็ƒๆฏ”่ต›็š„ๅ† ๅ†›ๅคงๅคšๆ˜ฏไธญใ€ๆ—ฅใ€้Ÿฉ็ญ‰ไธœไบšๅ›ฝๅฎถ็š„้€‰ๆ‰‹ใ€‚\n\nไธบไป€ไนˆๅซ\"ไน’ไน“็ƒ\"ๅ‘ข๏ผŸ20ไธ–็บชๅˆ๏ผŒไธ€ไฝ็พŽๅ›ฝไน’ไน“็ƒ็”จๅ…ท็”Ÿไบงๅ•†ไปฅๆ‰“ไน’ไน“็ƒๆ—ถๅ‘ๅ‡บ็š„\"ping-pong\"ๅฃฐไฝœไธบๅ•†ๆ ‡ๅใ€‚ไปŽๆญค๏ผŒping-pongๆˆไบ†ไน’ไน“็ƒ็š„ๅฆไธ€ไธช่‹ฑๆ–‡ๅใ€‚ไผ ๅˆฐไธญๅ›ฝๅŽ๏ผŒไธญๆ–‡้‡Œๅฐฑๆœ‰ไบ†\"ไน’ไน“็ƒ\"่ฟ™ไธชๆ–ฐ่ฏใ€‚ไธญๅ›ฝไบบๅ–œ็ˆฑๆ‰“ไน’ไน“็ƒ๏ผŒๆ˜ฏ็›ฎๅ‰ไธ–็•ŒไธŠไน’ไน“็ƒ่ฟๅŠจๆฐดๅนณๆœ€้ซ˜็š„ๅ›ฝๅฎถไน‹ไธ€๏ผŒไน’ไน“็ƒไนŸ่ขซไธญๅ›ฝไบบๅซไฝœ\"ๅ›ฝ็ƒ\"ใ€‚", + "wordCount": 483, + "questions": [ + { + "question": "่ฏด่ฏดไน’ไน“็ƒ่ฟๅŠจๆ˜ฏๆ€Žไนˆๆฅ็š„ใ€‚", + "type": "open", + "answer": "19ไธ–็บชๆœซไผฆๆ•ฆ๏ผŒไธคไธชๅนด่ฝปไบบๅ› ไธบไธ‹้›จไธ่ƒฝๅŽปๅค–้ขๆ‰“็ฝ‘็ƒ๏ผŒ็”จ้คๆกŒใ€็ปณๅญใ€่ฝฏๆœจๅกžๅ’Œ็ƒŸ็›’ๅˆ›้€ ไบ†่ฟ™้กน่ฟๅŠจ" + }, + { + "question": "ไน’ไน“็ƒ่ฟๅŠจๆœ€ๆ—ฉ่ตทๆบไบŽ่‹ฑๅ›ฝ๏ผŒไธบไป€ไนˆๅดๅœจไธœไบšๅœฐๅŒบๅฟซ้€Ÿๅ‘ๅฑ•่ตทๆฅ๏ผŸ", + "type": "open", + "answer": "ๅ› ไธบไธœไบšไบบๅคšๅœฐๅฐ‘๏ผŒไน’ไน“็ƒๅœบๅœฐๅฐใ€็”จๅ…ท็ฎ€ๅ•๏ผ›ไธœไบšไบบๅ–œๆฌขๆฒกๆœ‰่บซไฝ“ๆŽฅ่งฆ็š„่ฟๅŠจ๏ผŒ่ฎฒ็ฉถๆŠ€ๅทง" + } + ] + } + ], + "exercises": [ + { + "type": "character_inference", + "title": "ๆ นๆฎๆญ้…็š„่ฏ่ฏญ็Œœๆต‹่ฏไน‰", + "questions": [ + { + "question": "่ฟๅŠจไธไป…ๅฏไปฅๅผบ่บซๅฅไฝ“๏ผŒ่€Œไธ”ๅฏไปฅๅธฆๆฅๅฟซไนใ€‚", + "options": ["Aๅชๆœ‰", "Bไธ็ฎก", "Cไธไฝ†"], + "correctAnswer": "Cไธไฝ†" + }, + { + "question": "ไป–ไธๆƒณๅนฒ่ฟ™ไนˆ็ดฏ็š„ๆดปๅ„ฟใ€‚", + "options": ["Aๅทฅไฝœ", "B่ฟๅŠจ", "Cๅญฆไน "], + "correctAnswer": "Aๅทฅไฝœ" + } + ] + } + ] +} diff --git a/content/chapters/ledu-chapter4.json b/content/chapters/ledu-chapter4.json new file mode 100644 index 0000000..9832a06 --- /dev/null +++ b/content/chapters/ledu-chapter4.json @@ -0,0 +1,762 @@ +{ + "id": "ledu-chapter4", + "book_id": "ledu", + "name": "็ปฟ่‰ฒ็”Ÿๆดป (Green Living)", + "description": "Chapter on environmental protection, green transportation, and sustainable lifestyle. Explores traffic problems in big cities, World Car Free Day, and daily environmental actions.", + "difficulty": "intermediate", + "language": "zh-CN", + "chapter_number": "4", + "metadata": { + "version": "1.0", + "created": "2025-10-14", + "updated": "2025-10-14", + "source": "LEDU Textbook - Jiaotong University", + "target_level": "intermediate", + "estimated_hours": 10, + "prerequisites": ["ledu-chapter1", "ledu-chapter2", "ledu-chapter3"], + "learning_objectives": [ + "Master 90+ environmental and transportation vocabulary", + "Understand green living concepts and practices", + "Learn about traffic management in Chinese cities", + "Practice reading comprehension with authentic texts", + "Develop abbreviation recognition skills" + ], + "content_tags": ["environment", "transportation", "green-living", "pollution", "sustainability", "chinese-culture"], + "completion_criteria": { + "vocabulary_mastery": 90, + "comprehension_score": 80, + "exercises_completed": 20 + } + }, + "vocabulary": { + "็”Ÿ": { + "pronunciation": "shฤ“ng", + "type": "morpheme", + "user_language": "to be born, life, to grow", + "examples": ["ๅ‡บ็”Ÿ", "็”Ÿ้•ฟ", "็”Ÿๆดป", "ๅ‘็”Ÿ"], + "notes": "Self-explanatory character showing grass growing from earth" + }, + "ๅ‡บ": { + "pronunciation": "chลซ", + "type": "verb/morpheme", + "user_language": "to go out, to come out", + "examples": ["ๅ‡บ้—จ", "ๅ‡บไธปๆ„", "ๅ‡บ้—ฎ้ข˜", "ๅ‡บๆฑ—"] + }, + "ไบค้€š": { + "pronunciation": "jiฤotลng", + "type": "noun", + "user_language": "traffic, transportation", + "examples": ["ไบค้€šไพฟๅˆฉ", "ไบค้€šๅฎ‰ๅ…จ", "ไบค้€š้ƒจ้—จ"] + }, + "ๆ‹ฅๅ ต": { + "pronunciation": "yลngdว”", + "type": "verb", + "user_language": "to be stuck in (a traffic jam)", + "examples": ["ไบค้€šๆ‹ฅๅ ต", "้“่ทฏๆ‹ฅๅ ต"] + }, + "่งฃๅ†ณ": { + "pronunciation": "jiฤ›juรฉ", + "type": "verb", + "user_language": "to solve, to settle", + "examples": ["่งฃๅ†ณ้—ฎ้ข˜"] + }, + "ๅ‘ๅฑ•": { + "pronunciation": "fฤzhวŽn", + "type": "verb", + "user_language": "to develop", + "examples": ["ๅ‘ๅฑ•ๅ…ฌๅ…ฑไบค้€š", "ๅ‘ๅฑ•็ปๆตŽ", "ๅ‘ๅฑ•้€Ÿๅบฆ"] + }, + "็งŸ": { + "pronunciation": "zลซ", + "type": "verb/noun", + "user_language": "to rent; rent", + "examples": ["็งŸๆˆฟ", "็งŸ่ฝฆ", "ๆˆฟ็งŸ", "็งŸ้‡‘"] + }, + "ๆŽงๅˆถ": { + "pronunciation": "kรฒngzhรฌ", + "type": "verb", + "user_language": "to control, to dominate", + "examples": ["ๆŽงๅˆถๆฏ”่ต›", "ๆŽงๅˆถๆ•ฐ้‡"] + }, + "้ผ“ๅŠฑ": { + "pronunciation": "gว”lรฌ", + "type": "verb", + "user_language": "to encourage", + "examples": ["้ผ“ๅŠฑๅญฆ็”Ÿ", "ไบ’็›ธ้ผ“ๅŠฑ", "ๅพ—ๅˆฐ้ผ“ๅŠฑ"] + }, + "ๆฑกๆŸ“": { + "pronunciation": "wลซrวŽn", + "type": "verb/noun", + "user_language": "to pollute; pollution", + "examples": ["ๆฑกๆŸ“็ฉบๆฐ”", "ๆฑกๆŸ“็Žฏๅขƒ", "ๅ—ๅˆฐๆฑกๆŸ“"] + }, + "ๅ™ชๅฃฐ": { + "pronunciation": "zร oshฤ“ng", + "type": "noun", + "user_language": "noise", + "examples": ["ๅ™ชๅฃฐๆฑกๆŸ“"] + }, + "่ต„ๆบ": { + "pronunciation": "zฤซyuรกn", + "type": "noun", + "user_language": "resource(s)", + "examples": ["่‡ช็„ถ่ต„ๆบ", "ไบบๅŠ›่ต„ๆบ", "่ต„ๆบไธฐๅฏŒ"] + }, + "ๆ้ซ˜": { + "pronunciation": "tรญ//gฤo", + "type": "verb", + "user_language": "to improve, to increase, to enhance", + "examples": ["ๆ้ซ˜ๆฐดๅนณ", "ๆ้ซ˜่ƒฝๅŠ›"] + }, + "ๆ„่ฏ†": { + "pronunciation": "yรฌshรญ", + "type": "noun/verb", + "user_language": "awareness; to be conscious of, to realize", + "examples": ["ๅฎ‰ๅ…จๆ„่ฏ†", "็ŽฏๅขƒไฟๆŠคๆ„่ฏ†"] + }, + "ไธ–็•Œๆ— ่ฝฆๆ—ฅ": { + "pronunciation": "Shรฌjiรจ Wรบchฤ“ Rรฌ", + "type": "proper noun", + "user_language": "World Car Free Day" + }, + "ๆœ้˜ณๅŒบ": { + "pronunciation": "Chรกoyรกng Qลซ", + "type": "proper noun", + "user_language": "Chaoyang District (of Beijing)" + }, + "ๆณ•ๅ›ฝ": { + "pronunciation": "FวŽguรณ", + "type": "proper noun", + "user_language": "France" + }, + "ๆŠฑๆ€จ": { + "pronunciation": "bร oyuร n", + "type": "verb", + "user_language": "to complain" + }, + "็ณŸ็ณ•": { + "pronunciation": "zฤogฤo", + "type": "adjective", + "user_language": "terrible, too bad" + }, + "่Š‚็บฆ": { + "pronunciation": "jiรฉyuฤ“", + "type": "verb", + "user_language": "to economize, to save" + }, + "ๅ‘Š็คบ": { + "pronunciation": "gร oshi", + "type": "noun", + "user_language": "bulletin, official notice" + }, + "ๆณ•้™ข": { + "pronunciation": "fวŽyuร n", + "type": "noun", + "user_language": "court of justice" + }, + "้€š็—…": { + "pronunciation": "tลngbรฌng", + "type": "noun", + "user_language": "common problem, common defect" + }, + "ไพฟๅˆฉ": { + "pronunciation": "biร nlรฌ", + "type": "adjective", + "user_language": "convenient, easy" + }, + "ๅ‡บ่กŒ": { + "pronunciation": "chลซxรญng", + "type": "verb", + "user_language": "to go out, to travel" + }, + "็ฎก็†": { + "pronunciation": "guวŽnlว", + "type": "verb/noun", + "user_language": "to manage; management", + "examples": ["ไบค้€š็ฎก็†้ƒจ้—จ"] + }, + "้ƒจ้—จ": { + "pronunciation": "bรนmรฉn", + "type": "noun", + "user_language": "department, section" + }, + "ไฟฎๅปบ": { + "pronunciation": "xiลซjiร n", + "type": "verb", + "user_language": "to build, to construct" + }, + "ๅœฐ้“": { + "pronunciation": "dรฌtiฤ›", + "type": "noun", + "user_language": "subway, metro" + }, + "่‡ช่กŒ่ฝฆ": { + "pronunciation": "zรฌxรญngchฤ“", + "type": "noun", + "user_language": "bicycle" + }, + "ๆ•ฐ้‡": { + "pronunciation": "shรนliร ng", + "type": "noun", + "user_language": "quantity, amount" + }, + "ๅธ‚ๆฐ‘": { + "pronunciation": "shรฌmรญn", + "type": "noun", + "user_language": "citizen, city resident" + }, + "ๆญฅ่กŒ": { + "pronunciation": "bรนxรญng", + "type": "verb", + "user_language": "to walk, on foot" + }, + "ๅ…ฌๅ…ฑไบค้€š": { + "pronunciation": "gลnggรฒng jiฤotลng", + "type": "noun", + "user_language": "public transportation" + }, + "็ปฟ่‰ฒ": { + "pronunciation": "lวœsรจ", + "type": "adjective", + "user_language": "green; environmentally friendly" + }, + "่ฏž็”Ÿ": { + "pronunciation": "dร nshฤ“ng", + "type": "verb", + "user_language": "to be born, to come into being" + }, + "ๆฌงๆดฒ": { + "pronunciation": "ลŒuzhลu", + "type": "proper noun", + "user_language": "Europe" + }, + "ๆฑฝ่ฝฆ": { + "pronunciation": "qรฌchฤ“", + "type": "noun", + "user_language": "car, automobile" + }, + "็ฉบๆฐ”": { + "pronunciation": "kลngqรฌ", + "type": "noun", + "user_language": "air" + }, + "ไธฅ้‡": { + "pronunciation": "yรกnzhรฒng", + "type": "adjective", + "user_language": "serious, severe" + }, + "ๆๅ‡บ": { + "pronunciation": "tรญchลซ", + "type": "verb", + "user_language": "to put forward, to propose" + }, + "ๆ”ฏๆŒ": { + "pronunciation": "zhฤซchรญ", + "type": "verb/noun", + "user_language": "to support; support" + }, + "ๅผ€ๅฑ•": { + "pronunciation": "kฤizhวŽn", + "type": "verb", + "user_language": "to launch, to carry out" + }, + "ๆดปๅŠจ": { + "pronunciation": "huรณdรฒng", + "type": "noun", + "user_language": "activity" + }, + "ไธ–็•Œๆ€ง": { + "pronunciation": "shรฌjiรจxรฌng", + "type": "adjective", + "user_language": "worldwide, global" + }, + "ๅปบ่ฎฎ": { + "pronunciation": "jiร nyรฌ", + "type": "verb/noun", + "user_language": "to suggest; suggestion" + }, + "้€‰ๆ‹ฉ": { + "pronunciation": "xuวŽnzรฉ", + "type": "verb", + "user_language": "to choose, to select" + }, + "ๅ…ฌไบค": { + "pronunciation": "gลngjiฤo", + "type": "noun", + "user_language": "public transportation (abbreviation)" + }, + "ๅˆฉ็”จ": { + "pronunciation": "lรฌyรฒng", + "type": "verb", + "user_language": "to use, to utilize" + }, + "้“่ทฏ": { + "pronunciation": "dร olรน", + "type": "noun", + "user_language": "road, path" + }, + "ๅ‡ๅฐ‘": { + "pronunciation": "jiวŽnshวŽo", + "type": "verb", + "user_language": "to reduce, to decrease" + }, + "ไบ†่งฃ": { + "pronunciation": "liวŽojiฤ›", + "type": "verb", + "user_language": "to understand, to know" + }, + "่ฟ‡ๅคš": { + "pronunciation": "guรฒduล", + "type": "adjective", + "user_language": "too much, excessive" + }, + "ๅŸŽๅธ‚": { + "pronunciation": "chรฉngshรฌ", + "type": "noun", + "user_language": "city" + }, + "็Žฏๅขƒ": { + "pronunciation": "huรกnjรฌng", + "type": "noun", + "user_language": "environment" + }, + "ๅฑๅฎณ": { + "pronunciation": "wฤ“ihร i", + "type": "noun/verb", + "user_language": "harm, danger; to harm" + }, + "็Žฏไฟ": { + "pronunciation": "huรกnbวŽo", + "type": "noun", + "user_language": "environmental protection (abbreviation)" + }, + "ๅŠ ้‡": { + "pronunciation": "jiฤzhรฒng", + "type": "verb", + "user_language": "to worsen, to aggravate" + }, + "ๅŒๆ—ถ": { + "pronunciation": "tรณngshรญ", + "type": "adverb/conjunction", + "user_language": "at the same time, meanwhile" + }, + "ๅบ”": { + "pronunciation": "yฤซng", + "type": "auxiliary verb", + "user_language": "should, ought to (written language)" + }, + "ไฟๆŠค": { + "pronunciation": "bวŽohรน", + "type": "verb", + "user_language": "to protect" + }, + "็”Ÿๆดปๆ–นๅผ": { + "pronunciation": "shฤ“nghuรณ fฤngshรฌ", + "type": "noun", + "user_language": "lifestyle, way of life" + }, + "ๆฏๆฏ็›ธๅ…ณ": { + "pronunciation": "xฤซxฤซ xiฤngguฤn", + "type": "idiom", + "user_language": "closely related" + }, + "้šๆ‰‹": { + "pronunciation": "suรญshว’u", + "type": "adverb", + "user_language": "conveniently, in passing" + }, + "ๅšๅˆฐ": { + "pronunciation": "zuรฒdร o", + "type": "verb", + "user_language": "to achieve, to accomplish" + }, + "ๆฐด้พ™ๅคด": { + "pronunciation": "shuวlรณngtรณu", + "type": "noun", + "user_language": "faucet, tap" + }, + "ไธ€็›ด": { + "pronunciation": "yฤซzhรญ", + "type": "adverb", + "user_language": "continuously, always" + }, + "ๆด—ๆ‰‹": { + "pronunciation": "xวshว’u", + "type": "verb", + "user_language": "to wash hands" + }, + "ๆด—ๆพก": { + "pronunciation": "xวzวŽo", + "type": "verb", + "user_language": "to take a bath/shower" + }, + "ๆด—่กฃๆœ": { + "pronunciation": "xว yฤซfu", + "type": "verb phrase", + "user_language": "to wash clothes" + }, + "ๅ…ณ็ฏ": { + "pronunciation": "guฤn dฤ“ng", + "type": "verb phrase", + "user_language": "to turn off the light" + }, + "็”ต่ดน": { + "pronunciation": "diร nfรจi", + "type": "noun", + "user_language": "electricity bill" + }, + "ๅบฆ": { + "pronunciation": "dรน", + "type": "measure word", + "user_language": "degree; kilowatt-hour (for electricity)" + }, + "ๆœ‰ๅฎณ": { + "pronunciation": "yว’uhร i", + "type": "adjective", + "user_language": "harmful" + }, + "ๆฐ”ไฝ“": { + "pronunciation": "qรฌtว", + "type": "noun", + "user_language": "gas" + }, + "ๆ“ฆๅœฐ": { + "pronunciation": "cฤ dรฌ", + "type": "verb phrase", + "user_language": "to mop the floor" + }, + "ๅ†ฒๅŽ•ๆ‰€": { + "pronunciation": "chลng cรจsuว’", + "type": "verb phrase", + "user_language": "to flush the toilet" + }, + "ๅก‘ๆ–™่ข‹": { + "pronunciation": "sรนliร odร i", + "type": "noun", + "user_language": "plastic bag" + }, + "่‡ชๅค‡": { + "pronunciation": "zรฌbรจi", + "type": "verb", + "user_language": "to bring one's own" + }, + "่ข‹ๅญ": { + "pronunciation": "dร izi", + "type": "noun", + "user_language": "bag" + }, + "้ฃŸ็”จ": { + "pronunciation": "shรญyรฒng", + "type": "verb", + "user_language": "to eat, to consume" + }, + "้‡Ž็”ŸๅŠจ็‰ฉ": { + "pronunciation": "yฤ›shฤ“ng dรฒngwรน", + "type": "noun", + "user_language": "wild animal" + }, + "็ฉฟ": { + "pronunciation": "chuฤn", + "type": "verb", + "user_language": "to wear" + }, + "ๆฏ›็šฎ": { + "pronunciation": "mรกopรญ", + "type": "noun", + "user_language": "fur, pelt" + }, + "้€‰่ดญ": { + "pronunciation": "xuวŽngรฒu", + "type": "verb", + "user_language": "to select and purchase" + }, + "ๅ†œ่ฏ": { + "pronunciation": "nรณngyร o", + "type": "noun", + "user_language": "pesticide" + }, + "ๆ–ฐ้ฒœ": { + "pronunciation": "xฤซnxiฤn", + "type": "adjective", + "user_language": "fresh" + }, + "ๆžœ่”ฌ": { + "pronunciation": "guว’shลซ", + "type": "noun", + "user_language": "fruits and vegetables (abbreviation)" + }, + "ๅŒ…่ฃ…": { + "pronunciation": "bฤozhuฤng", + "type": "noun/verb", + "user_language": "packaging; to pack" + }, + "็ปฟ่‰ฒ้ฃŸๅ“": { + "pronunciation": "lวœsรจ shรญpวn", + "type": "noun", + "user_language": "green food, organic food" + }, + "ๆ ‡่ฏ†": { + "pronunciation": "biฤoshรญ", + "type": "noun", + "user_language": "mark, sign, logo" + }, + "ๅทฅๅ…ท": { + "pronunciation": "gลngjรน", + "type": "noun", + "user_language": "tool, instrument" + }, + "ๆฑฝๆฒน": { + "pronunciation": "qรฌyรณu", + "type": "noun", + "user_language": "gasoline, petrol" + }, + "ๅฐพๆฐ”": { + "pronunciation": "wฤ›iqรฌ", + "type": "noun", + "user_language": "exhaust gas, emissions" + }, + "ๅ…ฌๅ…ฑๅœบๆ‰€": { + "pronunciation": "gลnggรฒng chวŽngsuว’", + "type": "noun", + "user_language": "public place" + }, + "ๅฎคๅ†…": { + "pronunciation": "shรฌnรจi", + "type": "noun", + "user_language": "indoor, interior" + }, + "ๅธ็ƒŸ": { + "pronunciation": "xฤซyฤn", + "type": "verb", + "user_language": "to smoke" + }, + "ๅšๅฅฝ": { + "pronunciation": "zuรฒhวŽo", + "type": "verb", + "user_language": "to do well, to complete" + }, + "ๅžƒๅœพๅˆ†็ฑป": { + "pronunciation": "lฤjฤซ fฤ“nlรจi", + "type": "noun", + "user_language": "garbage sorting, waste classification" + }, + "็ง็ฑป": { + "pronunciation": "zhว’nglรจi", + "type": "noun", + "user_language": "type, kind, category" + }, + "ๅˆ†ๅผ€": { + "pronunciation": "fฤ“nkฤi", + "type": "verb", + "user_language": "to separate, to divide" + }, + "ๆ”พ": { + "pronunciation": "fร ng", + "type": "verb", + "user_language": "to put, to place" + }, + "ๅฝ“ไฝœ": { + "pronunciation": "dร ngzuรฒ", + "type": "verb", + "user_language": "to regard as, to treat as" + }, + "ๆœ‰็”จ": { + "pronunciation": "yว’uyรฒng", + "type": "adjective", + "user_language": "useful" + }, + "ๆทท่ฃ…": { + "pronunciation": "hรนnzhuฤng", + "type": "verb", + "user_language": "to mix and pack together" + }, + "ๅœŸๅœฐ": { + "pronunciation": "tว”dรฌ", + "type": "noun", + "user_language": "land, soil" + }, + "็…ง้กพ": { + "pronunciation": "zhร ogu", + "type": "verb", + "user_language": "to take care of, to look after" + }, + "้™„่ฟ‘": { + "pronunciation": "fรนjรฌn", + "type": "noun", + "user_language": "nearby, vicinity" + }, + "ๆ ‘": { + "pronunciation": "shรน", + "type": "noun", + "user_language": "tree" + }, + "ๅฎšๆœŸ": { + "pronunciation": "dรฌngqฤซ", + "type": "adverb", + "user_language": "regularly, periodically" + }, + "ๆต‡ๆฐด": { + "pronunciation": "jiฤoshuว", + "type": "verb", + "user_language": "to water (plants)" + }, + "ๅฎถๅบญ": { + "pronunciation": "jiฤtรญng", + "type": "noun", + "user_language": "family, household" + }, + "ไธ€ๅ‘˜": { + "pronunciation": "yฤซ yuรกn", + "type": "noun", + "user_language": "a member" + } + }, + "grammar": { + "้š็€": { + "title": "้š็€โ€ฆโ€ฆ - along with, as", + "pattern": "้š็€ + Noun/Phrase, Result", + "explanation": "Used to indicate that something changes as another thing changes", + "examples": [ + { + "chinese": "้š็€ๆ˜ฅๅคฉ็š„ๅˆฐๆฅ๏ผŒๅคฉๆฐ”ๆš–ๅ’Œ่ตทๆฅไบ†ใ€‚", + "pronunciation": "Suรญzhe chลซntiฤn de dร olรกi, tiฤnqรฌ nuวŽnhuo qวlรกi le.", + "translation": "As spring arrives, the weather is getting warmer." + }, + { + "chinese": "้š็€ๅนด้พ„็š„ๅขž้•ฟ๏ผŒไบบ็š„่บซไฝ“ไผšๅ‘็”Ÿๅพˆๅคšๅ˜ๅŒ–ใ€‚", + "pronunciation": "Suรญzhe niรกnlรญng de zฤ“ngzhวŽng, rรฉn de shฤ“ntว huรฌ fฤshฤ“ng hฤ›nduล biร nhuร .", + "translation": "As age increases, people's bodies undergo many changes." + }, + { + "chinese": "้š็€ๅŸŽๅธ‚็š„ๅ‘ๅฑ•๏ผŒ็ŽฏๅขƒๆฑกๆŸ“้—ฎ้ข˜ไนŸๅœจๅŠ ้‡ใ€‚", + "pronunciation": "Suรญzhe chรฉngshรฌ de fฤzhวŽn, huรกnjรฌng wลซrวŽn wรจntรญ yฤ› zร i jiฤzhรฒng.", + "translation": "As cities develop, environmental pollution problems are also worsening." + } + ] + }, + "ๅทฒ-written": { + "title": "ๅทฒ (yว) - already (written language)", + "explanation": "Written Chinese adverb meaning 'ๅทฒ็ป' (already). More formal.", + "examples": [ + { + "chinese": "ๆฏ”่ต›ๅทฒ็ป“ๆŸใ€‚", + "pronunciation": "Bวsร i yว jiรฉshรน.", + "translation": "The competition has already ended." + }, + { + "chinese": "่ฟ™้กนๅทฅไฝœๅทฒๅฎŒๆˆใ€‚", + "pronunciation": "Zhรจ xiร ng gลngzuรฒ yว wรกnchรฉng.", + "translation": "This work has already been completed." + } + ] + }, + "ๅ•-written": { + "title": "ๅ• (dฤn) - only, merely (written language)", + "explanation": "Written Chinese adverb meaning 'ๅช' (only). More formal.", + "examples": [ + { + "chinese": "่ฟ™้ƒจๅŠจ็”ป็‰‡ไธๅ•ๅญฉๅญๅ–œๆฌข๏ผŒๅคงไบบไนŸ็ˆฑ็œ‹ใ€‚", + "pronunciation": "Zhรจ bรน dรฒnghuร piร n bรน dฤn hรกizi xวhuan, dร rรฉn yฤ› ร i kร n.", + "translation": "Not only do children like this cartoon, adults love watching it too." + }, + { + "chinese": "ๆˆ‘ๆฏไธชๆœˆ็š„่Šฑ่ดนๅพˆๅคš๏ผŒๅ•ไบค้€š่ดนๅฐฑ่ฆๅ‡ ็™พๅ…ƒใ€‚", + "pronunciation": "Wว’ mฤ›i gรจ yuรจ de huฤfรจi hฤ›nduล, dฤn jiฤotลngfรจi jiรน yร o jว bวŽi yuรกn.", + "translation": "My monthly expenses are high; transportation alone costs several hundred yuan." + } + ] + } + }, + "texts": [ + { + "id": "main-text", + "title": "ไธ–็•Œๆ— ่ฝฆๆ—ฅ (World Car Free Day)", + "type": "main", + "content": "ๆœ‰่ฟ™ๆ ทไธ€ไธช็ฌ‘่ฏ๏ผš\"ๆ—ฉไธŠไธŠ็ญๆ—ถ้—ด๏ผŒไฝ ๅœจๅŒ—ไบฌๆœ้˜ณๅŒบ๏ผ›ๅไธ€ๅฐๆ—ถ็š„่ฝฆ๏ผŒไฝ ๅœจๆœ้˜ณๅŒบ๏ผ›ๅ†ๅไธ€ๅฐๆ—ถ๏ผŒไฝ ่ฟ˜ๆ˜ฏๅœจๆœ้˜ณๅŒบใ€‚\"\n\nไบค้€šๆ‹ฅๅ ตๆ˜ฏๅพˆๅคšๅคงๅŸŽๅธ‚็š„้€š็—…ใ€‚ไธบ่งฃๅ†ณไบค้€šๆ‹ฅๅ ต้—ฎ้ข˜ใ€ไพฟๅˆฉไบบไปฌๅ‡บ่กŒ๏ผŒไบค้€š็ฎก็†้ƒจ้—จๆƒณไบ†ๅพˆๅคšๅŠžๆณ•๏ผŒๆฏ”ๅฆ‚๏ผšไฟฎๅปบๅœฐ้“ใ€ๅ‘ๅฑ•่‡ช่กŒ่ฝฆ็งŸ่ฝฆๆœๅŠกใ€ๆŽงๅˆถๆฑฝ่ฝฆๆ•ฐ้‡็ญ‰ใ€‚ๆฏๅนด9ๆœˆ22ๆ—ฅ\"ไธ–็•Œๆ— ่ฝฆๆ—ฅ\"ๅ‰ๅŽ๏ผŒๅŒ—ไบฌๅธ‚ไบค็ฎก้ƒจ้—จ้ƒฝไผš้ผ“ๅŠฑๅธ‚ๆฐ‘ไธๅผ€่ฝฆ๏ผŒ็”จๆญฅ่กŒใ€่‡ช่กŒ่ฝฆใ€ๅ…ฌๅ…ฑไบค้€š็ญ‰็ปฟ่‰ฒๆ–นๅผๅ‡บ่กŒใ€‚\n\n\"ๆ— ่ฝฆๆ—ฅ\"ๆœ€ๆ—ฉ่ฏž็”ŸไบŽ1998ๅนด็š„ๆณ•ๅ›ฝใ€‚้‚ฃๆ—ถๅ€™ๆฌงๆดฒ็š„ๅพˆๅคšๅŸŽๅธ‚้‡Œ๏ผŒๆฑฝ่ฝฆๅธฆๆฅ็š„็ฉบๆฐ”ๆฑกๆŸ“ใ€ๅ™ชๅฃฐๆฑกๆŸ“่ถŠๆฅ่ถŠไธฅ้‡ใ€‚1998ๅนด9ๆœˆ22ๆ—ฅ๏ผŒๆณ•ๅ›ฝไธ€ไบ›ๅนด่ฝปไบบๆœ€ๅ…ˆๆๅ‡บ\"In Town, Without My Car!\"๏ผŒ่ฟ™ไธช่ฏดๆณ•ๅพ—ๅˆฐไบ†ไบบไปฌ็š„ๆ”ฏๆŒใ€‚ๅŽๆฅไธ–็•ŒไธŠ็š„ๅพˆๅคšๅŸŽๅธ‚้ƒฝๅผ€ๅฑ•ไบ†\"ๆ— ่ฝฆๆ—ฅ\"ๆดปๅŠจ๏ผŒ\"ๆ— ่ฝฆๆ—ฅ\"ๆ…ขๆ…ขๆˆไบ†ไธ–็•Œๆ€ง็š„ๆดปๅŠจใ€‚\"ไธ–็•Œๆ— ่ฝฆๆ—ฅ\"ๆดปๅŠจ้ผ“ๅŠฑ็ปฟ่‰ฒๅ‡บ่กŒ๏ผŒๅปบ่ฎฎๅธ‚ๆฐ‘ไปฌๆ›ดๅคšๅœฐ้€‰ๆ‹ฉๅ…ฌไบคๅ‡บ่กŒ๏ผŒไธ€ๆ˜ฏไธบไบ†ๆ›ดๅฅฝๅœฐๅˆฉ็”จ้“่ทฏ่ต„ๆบ๏ผŒๅ‡ๅฐ‘ไบค้€šๆ‹ฅๅ ต๏ผŒไบŒๆ˜ฏ่ฎฉไบบไปฌไบ†่งฃๆฑฝ่ฝฆ่ฟ‡ๅคšๅฏนๅŸŽๅธ‚็Žฏๅขƒ็š„ๅฑๅฎณ๏ผŒๆ้ซ˜ไบบไปฌ็š„็Žฏไฟๆ„่ฏ†ใ€‚", + "wordCount": 376, + "questions": [ + { + "question": "็ฌฌ1ๆฎต็š„็ฌ‘่ฏ่ฏด็š„ๆ˜ฏไป€ไนˆๆƒ…ๅ†ต๏ผŸ", + "type": "open", + "answer": "ไบค้€šๆ‹ฅๅ ต๏ผŒๅ่ฝฆๅพˆไน…่ฟ˜ๅœจๅŒไธ€ไธชๅœฐๆ–น" + }, + { + "question": "ๆ นๆฎ็ฌฌ2ๆฎต๏ผŒไธบ่งฃๅ†ณไบค้€šๆ‹ฅๅ ต้—ฎ้ข˜๏ผŒไบค้€š็ฎก็†้ƒจ้—จๆƒณไบ†ๅ“ชไบ›ๅŠžๆณ•๏ผŸ", + "type": "open", + "answer": "ไฟฎๅปบๅœฐ้“ใ€ๅ‘ๅฑ•่‡ช่กŒ่ฝฆ็งŸ่ฝฆๆœๅŠกใ€ๆŽงๅˆถๆฑฝ่ฝฆๆ•ฐ้‡" + }, + { + "question": "ๆ นๆฎ็ฌฌ3ๆฎต๏ผŒ\"ไธ–็•Œๆ— ่ฝฆๆ—ฅ\"ๆ˜ฏๆ€Žๆ ท่ฏž็”Ÿ็š„๏ผŸ", + "type": "open", + "answer": "1998ๅนดๆณ•ๅ›ฝ๏ผŒๅ› ไธบๆฑฝ่ฝฆๅธฆๆฅ็š„ๆฑกๆŸ“ไธฅ้‡๏ผŒๅนด่ฝปไบบๆๅ‡บๆ— ่ฝฆๆ—ฅ๏ผŒๅพ—ๅˆฐๆ”ฏๆŒ" + }, + { + "question": "ๆ นๆฎ่ฏพๆ–‡๏ผŒ่ฏด่ฏด\"็ปฟ่‰ฒๅ‡บ่กŒ\"ๆ˜ฏไป€ไนˆๆ„ๆ€ใ€‚", + "type": "open", + "answer": "็”จๆญฅ่กŒใ€่‡ช่กŒ่ฝฆใ€ๅ…ฌๅ…ฑไบค้€š็ญ‰็Žฏไฟๆ–นๅผๅ‡บ่กŒ" + } + ] + }, + { + "id": "environmental-tips", + "title": "็Žฏไฟๅฐ่ดดๅฃซ (Environmental Tips)", + "type": "extensive", + "content": "้š็€ๅŸŽๅธ‚็š„ๅ‘ๅฑ•๏ผŒ็ŽฏๅขƒๆฑกๆŸ“้—ฎ้ข˜ไนŸๅœจๅŠ ้‡ใ€‚ไบบไปฌๅœจๆŠฑๆ€จๅŸŽๅธ‚็ฉบๆฐ”่ถŠๆฅ่ถŠ็ณŸ็ณ•็š„ๅŒๆ—ถ๏ผŒไนŸๅบ”ๆ„่ฏ†ๅˆฐ็Žฏๅขƒ็š„ไฟๆŠคๅ’Œๆฏไธชไบบ็š„็”Ÿๆดปๆ–นๅผๆฏๆฏ็›ธๅ…ณใ€‚\n\nไธ‹้ขๆ˜ฏ็”Ÿๆดปไธญๅฏไปฅ้šๆ‰‹ๅšๅˆฐ็š„ๅ‡ ไปถ็Žฏไฟๅฐไบ‹ใ€‚\n\nไธ€ใ€้šๆ‰‹ๅ…ณๆฐด้พ™ๅคด๏ผŒไธ่ฆไธ€็›ดๅผ€็€ๆฐด้พ™ๅคดๆด—ๆ‰‹ใ€ๆด—ๆพกใ€ๆด—่กฃๆœใ€‚\n\nไบŒใ€้šๆ‰‹ๅ…ณ็ฏ๏ผŒ่ฟ™ไธๅชๆ˜ฏไธบไบ†่Š‚็บฆ็”ต่ดน๏ผŒๆฏ่Š‚็บฆไธ€ๅบฆ็”ต๏ผŒ็ฉบๆฐ”ไธญๅฐฑไผšๅ‡ๅฐ‘ๅพˆๅคšๆœ‰ๅฎณ็š„ๆฐ”ไฝ“ใ€‚\n\nไธ‰ใ€ๆด—่กฃๆœๅŽ็š„ๆฐดๅฏไปฅๆ“ฆๅœฐๆˆ–ๅ†ฒๅŽ•ๆ‰€็ญ‰ใ€‚\n\nๅ››ใ€ไนฐไธœ่ฅฟๆ—ถไธ็”จๅก‘ๆ–™่ข‹๏ผŒๅ‡บ้—จ่ดญ็‰ฉๅธฆไธŠ่‡ชๅค‡็š„่ข‹ๅญใ€‚\n\nไบ”ใ€ไธ้ฃŸ็”จ้‡Ž็”ŸๅŠจ็‰ฉ๏ผŒไธ็ฉฟ้‡Ž็”ŸๅŠจ็‰ฉๆฏ›็šฎๅš็š„่กฃๆœใ€‚\n\nๅ…ญใ€้€‰่ดญไธ็”จๅ†œ่ฏ็š„ๆ–ฐ้ฒœๆžœ่”ฌ๏ผŒไนฐๅŒ…่ฃ…ไธŠๆœ‰\"็ปฟ่‰ฒ้ฃŸๅ“\"ๆ ‡่ฏ†็š„้ฃŸๅ“ใ€‚\n\nไธƒใ€ๅคš็”จๅ…ฌๅ…ฑไบค้€šๅทฅๅ…ท๏ผŒ่ฟ™ๆ ทๆ—ขๅฏไปฅ่Š‚็บฆๆฑฝๆฒน๏ผŒๅˆๅฏไปฅๅ‡ๅฐ‘ๆฑฝ่ฝฆๅฐพๆฐ”ๅธฆๆฅ็š„็ฉบๆฐ”ๆฑกๆŸ“ใ€‚\n\nๅ…ซใ€ๅ…ฌๅ…ฑๅœบๆ‰€ใ€ๅฎคๅ†…ๅทฅไฝœๅœบๆ‰€ใ€ๅ…ฌๅ…ฑไบค้€šๅทฅๅ…ทๅ†…ไธๅธ็ƒŸใ€‚\n\nไนใ€ๅšๅฅฝๅžƒๅœพๅˆ†็ฑป๏ผŒไธๅŒ็ง็ฑป็š„ๅžƒๅœพๅˆ†ๅผ€ๆ”พใ€‚ๅˆ†่ฃ…ๅžƒๅœพๆ˜ฏๆŠŠๅžƒๅœพๅฝ“ไฝœๆœ‰็”จ็š„่ต„ๆบ๏ผŒๆทท่ฃ…็š„ๅžƒๅœพไผšๆฑกๆŸ“ๅœŸๅœฐๅ’Œ็ฉบๆฐ”ใ€‚\n\nๅใ€็…ง้กพ้™„่ฟ‘็š„ไธ€ๆฃตๆ ‘๏ผŒๅฎšๆœŸ็ป™ๅฎƒๆต‡ๆฐด๏ผŒๆŠŠๅฎƒๅฝ“ไฝœๅฎถๅบญ้‡Œ็š„ไธ€ๅ‘˜ใ€‚", + "wordCount": 404, + "questions": [ + { + "question": "ไปŽ่กฃใ€้ฃŸใ€ไฝใ€่กŒ่ฟ™ๅ››ไธชๆ–น้ข่ฏด่ฏด่บซ่พน็š„็Žฏไฟๅฐไบ‹", + "type": "open", + "answer": "่กฃ๏ผšไธ็ฉฟ้‡Ž็”ŸๅŠจ็‰ฉๆฏ›็šฎ๏ผ›้ฃŸ๏ผšไนฐ็ปฟ่‰ฒ้ฃŸๅ“๏ผ›ไฝ๏ผš่Š‚็บฆๆฐด็”ต๏ผŒๅžƒๅœพๅˆ†็ฑป๏ผ›่กŒ๏ผšๅคš็”จๅ…ฌๅ…ฑไบค้€š" + }, + { + "question": "ๅ…ณไบŽ่บซ่พน็š„็Žฏไฟๅฐไบ‹๏ผŒๆ–‡ไธญๆฒกๆœ‰ๆๅˆฐ็š„ๆ˜ฏ๏ผš", + "type": "multiple_choice", + "options": ["A็”จๆธฉๆฐด็…ฎ้ฅญ", "B่Š‚็บฆ็”จๆฐด็”จ็”ต", "C็”Ÿๆดปๅžƒๅœพๅˆ†็ฑป", "Dไธๅœจๅ…ฌๅ…ฑๅœบๆ‰€ๅธ็ƒŸ"], + "correctAnswer": "A็”จๆธฉๆฐด็…ฎ้ฅญ" + } + ] + } + ], + "exercises": [ + { + "type": "abbreviations", + "title": "็ผฉ็•ฅ่ฏญ็ปƒไน  (Abbreviations)", + "description": "Practice recognizing and forming Chinese abbreviations", + "questions": [ + { + "question": "ๅŒ—ไบฌๅคงๅญฆ", + "answer": "ๅŒ—ๅคง" + }, + { + "question": "็”ตๅญ้‚ฎไปถ", + "answer": "็”ต้‚ฎ" + }, + { + "question": "็ฉบไธญๅฐๅง", + "answer": "็ฉบๅง" + }, + { + "question": "ๅฅฅๆž—ๅŒนๅ…‹่ฟๅŠจไผš", + "answer": "ๅฅฅ่ฟไผš" + }, + { + "question": "ไบค้€š็ฎก็†้ƒจ้—จ", + "answer": "ไบค็ฎก้ƒจ้—จ" + }, + { + "question": "ๅŸŽๅธ‚ๅฑ…ๆฐ‘", + "answer": "ๅธ‚ๆฐ‘" + }, + { + "question": "ๅ…ฌๅ…ฑไบค้€š", + "answer": "ๅ…ฌไบค" + }, + { + "question": "็ŽฏๅขƒไฟๆŠค", + "answer": "็Žฏไฟ" + } + ] + } + ] +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..9b1e1d9 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,259 @@ +# Architecture Guide + +## ๐Ÿ—๏ธ Core Principles + +### 1. Single Responsibility +Each module has exactly one purpose. No mixing of concerns. + +### 2. Event-Driven Communication +All inter-module communication happens through EventBus. Zero direct dependencies. + +```javascript +// โŒ BAD - Direct access +const gameManager = window.app.modules.gameManager; +gameManager.startGame(); + +// โœ… GOOD - EventBus +eventBus.emit('game:start', { difficulty: 'medium' }); +``` + +### 3. Sealed Modules +Modules cannot be modified after creation using `Object.seal()`. + +```javascript +constructor() { + super('ModuleName'); + this._privateState = {}; + Object.seal(this); // Prevents adding/removing properties +} +``` + +### 4. WeakMap Private State +Internal data is completely inaccessible from outside. + +```javascript +const privateData = new WeakMap(); + +class SecureModule { + constructor() { + privateData.set(this, { + apiKey: 'secret', + internalState: {} + }); + } + + getPrivateData() { + return privateData.get(this); + } +} +``` + +### 5. Dependency Injection +No globals. Everything injected through constructor. + +```javascript +class GameModule extends Module { + constructor(name, dependencies, config) { + super(name, ['eventBus', 'renderer']); + + // Dependencies injected, not accessed globally + this._eventBus = dependencies.eventBus; + this._renderer = dependencies.renderer; + this._config = config; + } +} +``` + +## ๐Ÿ”„ Module Lifecycle + +``` +1. REGISTRATION โ†’ Application.js modules array +2. LOADING โ†’ ModuleLoader imports class +3. INSTANTIATION โ†’ new Module(name, deps, config) +4. INITIALIZATION โ†’ module.init() called +5. READY โ†’ Module emits 'ready' event +6. DESTRUCTION โ†’ module.destroy() on cleanup +``` + +## ๐Ÿ“ฆ System Components + +### Core Layer (`src/core/`) + +**Module.js** - Abstract base class +- WeakMap private state +- Lifecycle management (init/destroy) +- State validation +- Abstract enforcement + +**EventBus.js** - Event communication +- Module registration required +- Event history tracking +- Cross-module isolation +- Memory leak prevention + +**ModuleLoader.js** - Dependency injection +- Topological sort for dependencies +- Circular dependency detection +- Proper initialization order +- Dynamic import system + +**Router.js** - Navigation system +- Route guards +- Middleware execution +- State management +- History integration + +**Application.js** - Bootstrap system +- Auto-initialization +- Module registration +- Lifecycle coordination +- Debug panel + +### DRS Layer (`src/DRS/`) + +**Exercise Modules** (`exercise-modules/`) +- VocabularyModule - Flashcard spaced repetition +- TextAnalysisModule - AI text comprehension +- GrammarAnalysisModule - AI grammar correction +- TranslationModule - AI translation validation +- OpenResponseModule - Free-form AI evaluation + +**Services** (`services/`) +- IAEngine - Multi-provider AI system +- LLMValidator - Answer validation +- ContentLoader - Content generation +- ProgressTracker - Progress management +- PrerequisiteEngine - Prerequisite checking + +**Interfaces** (`interfaces/`) +- StrictInterface - Base enforcement class +- ProgressItemInterface - Progress tracking contract +- ProgressSystemInterface - Progress system contract +- DRSExerciseInterface - Exercise module contract + +### Games Layer (`src/games/`) + +Independent game modules for entertainment (NOT part of DRS). +- FlashcardLearning.js - Standalone flashcard game +- Future games... + +## ๐Ÿšซ Separation Rules + +### DRS vs Games - NEVER MIX + +**DRS** = Educational exercises with strict interfaces +**Games** = Entertainment with different architecture + +```javascript +// โŒ FORBIDDEN - DRS importing games +import FlashcardLearning from '../games/FlashcardLearning.js'; + +// โœ… CORRECT - DRS uses its own modules +import VocabularyModule from './exercise-modules/VocabularyModule.js'; +``` + +## ๐Ÿ”’ Security Layers + +1. **Object.seal()** - Prevents property addition/deletion +2. **Object.freeze()** - Prevents prototype modification +3. **WeakMap** - Internal state hidden +4. **Abstract enforcement** - Missing methods throw errors +5. **Validation at boundaries** - All inputs validated + +## ๐Ÿ“Š Data Flow + +``` +User Action + โ†“ +UI Component + โ†“ +Event Emission (EventBus) + โ†“ +Module Event Handler + โ†“ +Business Logic + โ†“ +State Update + โ†“ +Event Emission (state changed) + โ†“ +UI Update +``` + +## ๐ŸŽฏ Module Types + +### 1. Core Modules +System-level functionality. Never modify these. + +### 2. Game Modules +Entertainment-focused, extend Module base class. + +### 3. DRS Exercise Modules +Educational exercises, implement DRSExerciseInterface. + +### 4. Service Modules +Support functionality (AI, progress, content). + +### 5. UI Components +Reusable interface elements (future phase). + +## โšก Performance Targets + +- **<100ms** module loading time +- **<50ms** event propagation time +- **<200ms** application startup time +- **Zero** memory leaks in module lifecycle + +## ๐Ÿงช Testing Strategy + +1. **Unit Tests** - Individual module behavior +2. **Integration Tests** - Module communication via EventBus +3. **Interface Tests** - Contract compliance (ImplementationValidator) +4. **E2E Tests** - Complete user flows + +## ๐Ÿ“‹ Architecture Checklist + +For every new feature: +- [ ] Single responsibility maintained +- [ ] EventBus for all communication +- [ ] No direct module dependencies +- [ ] Proper dependency injection +- [ ] Object.seal() applied +- [ ] Abstract methods implemented +- [ ] Lifecycle methods complete +- [ ] Memory cleanup in destroy() +- [ ] Interface compliance validated +- [ ] No global variables used + +## ๐Ÿ” Debug Tools + +```javascript +// Application status +window.app.getStatus() + +// Module inspection +window.app.getCore().moduleLoader.getStatus() + +// Event history +window.app.getCore().eventBus.getEventHistory() + +// Navigate programmatically +window.app.getCore().router.navigate('/path') +``` + +## ๐Ÿšจ Common Violations + +1. **Direct module access** โ†’ Use EventBus +2. **Global variables** โ†’ Use dependency injection +3. **Mixed responsibilities** โ†’ Split into separate modules +4. **No cleanup** โ†’ Implement destroy() properly +5. **Hardcoded dependencies** โ†’ Declare in constructor +6. **Missing validation** โ†’ Validate all inputs +7. **Modifying core** โ†’ Extend, don't modify + +## ๐Ÿ“– Further Reading + +- `docs/creating-new-module.md` - Module creation guide +- `docs/interfaces.md` - Interface system details +- `docs/progress-system.md` - Progress tracking guide +- `README.md` - Project overview diff --git a/docs/creating-new-module.md b/docs/creating-new-module.md new file mode 100644 index 0000000..e60f1f5 --- /dev/null +++ b/docs/creating-new-module.md @@ -0,0 +1,312 @@ +# Creating New Modules + +## ๐ŸŽฎ Game Module Template + +### Basic Structure + +```javascript +import Module from '../core/Module.js'; + +class GameName extends Module { + constructor(name, dependencies, config) { + super(name, ['eventBus']); // Declare dependencies + + // Validate dependencies + if (!dependencies.eventBus) { + throw new Error('GameName requires EventBus dependency'); + } + + this._eventBus = dependencies.eventBus; + this._config = config; + + Object.seal(this); // Prevent modification + } + + async init() { + this._validateNotDestroyed(); + + // Set up event listeners + this._eventBus.on('game:start', this._handleStart.bind(this), this.name); + + this._setInitialized(); + } + + async destroy() { + this._validateNotDestroyed(); + + // Cleanup: remove event listeners, DOM elements, timers + this._eventBus.off('game:start', this._handleStart, this.name); + + this._setDestroyed(); + } + + // Private methods + _handleStart(event) { + this._validateInitialized(); + // Game logic here + } +} + +export default GameName; +``` + +### Registration in Application.js + +```javascript +modules: [ + { + name: 'gameName', + path: './games/GameName.js', + dependencies: ['eventBus'], + config: { + difficulty: 'medium', + scoreToWin: 100 + } + } +] +``` + +## ๐Ÿ“‹ DRS Exercise Module Template + +### Using DRSExerciseInterface + +```javascript +import DRSExerciseInterface from '../DRS/interfaces/DRSExerciseInterface.js'; + +class MyExercise extends DRSExerciseInterface { + constructor() { + super('MyExercise'); + + // Internal state + this.score = 0; + this.attempts = 0; + this.startTime = null; + this.container = null; + } + + // โš ๏ธ REQUIRED - Initialize exercise + async init(config, content) { + this.config = config; + this.content = content; + this.startTime = Date.now(); + + // Validate content + if (!content || !content.question) { + throw new Error('MyExercise requires content with question'); + } + } + + // โš ๏ธ REQUIRED - Render UI + async render(container) { + this.container = container; + + container.innerHTML = ` +
+

${this.content.question}

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

${currentWord.word}

+

+ ${currentWord.word} +

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