Add LEDU Chinese course content and documentation
Add comprehensive Chinese reading course (乐读) with 4 chapters of vocabulary, texts, and exercises. Include architecture documentation for module development and progress tracking system. Content: - LEDU book metadata with 12 chapter outline - Chapter 1: Food culture (民以食为天) - 45+ vocabulary, etiquette - Chapter 2: Shopping (货比三家) - comparative shopping vocabulary - Chapter 3: Sports & fitness (生命在于运动) - exercise habits - Chapter 4: Additional vocabulary and grammar Documentation: - Architecture principles and patterns - Module creation guide (Game, DRS, Progress) - Interface system (C++ style contracts) - Progress tracking and prerequisites Game Enhancements: - MarioEducational helper classes (Physics, Renderer, Sound, Enemies) - VocabularyModule TTS improvements - Updated CLAUDE.md with project status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7a18e27a44
commit
325b97060c
206
content/books/ledu.json
Normal file
206
content/books/ledu.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
548
content/chapters/ledu-chapter1.json
Normal file
548
content/chapters/ledu-chapter1.json
Normal file
@ -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 米"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
750
content/chapters/ledu-chapter2.json
Normal file
750
content/chapters/ledu-chapter2.json
Normal file
@ -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": "赚人气"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
643
content/chapters/ledu-chapter3.json
Normal file
643
content/chapters/ledu-chapter3.json
Normal file
@ -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工作"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
762
content/chapters/ledu-chapter4.json
Normal file
762
content/chapters/ledu-chapter4.json
Normal file
@ -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": "环保"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
259
docs/architecture.md
Normal file
259
docs/architecture.md
Normal file
@ -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
|
||||||
312
docs/creating-new-module.md
Normal file
312
docs/creating-new-module.md
Normal file
@ -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 = `
|
||||||
|
<div class="exercise-container">
|
||||||
|
<h2>${this.content.question}</h2>
|
||||||
|
<input type="text" id="answer-input" />
|
||||||
|
<button id="submit-btn">Submit</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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`
|
||||||
314
docs/interfaces.md
Normal file
314
docs/interfaces.md
Normal file
@ -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
|
||||||
268
docs/progress-system.md
Normal file
268
docs/progress-system.md
Normal file
@ -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
|
||||||
@ -528,9 +528,11 @@ class VocabularyModule extends DRSExerciseInterface {
|
|||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="word-card">
|
<div class="word-card">
|
||||||
<div class="word-display">
|
<div class="word-display">
|
||||||
<h3 class="target-word">${currentWord.word}</h3>
|
<h3 class="target-word clickable" id="target-word-tts" title="Click to hear pronunciation">
|
||||||
|
${currentWord.word}
|
||||||
|
</h3>
|
||||||
${this.config.showPronunciation && currentWord.pronunciation ?
|
${this.config.showPronunciation && currentWord.pronunciation ?
|
||||||
`<div class="pronunciation">[${currentWord.pronunciation}]</div>` : ''}
|
`<div class="pronunciation" id="pronunciation-display">[${currentWord.pronunciation}]</div>` : ''}
|
||||||
<div class="word-type">${currentWord.type || 'word'}</div>
|
<div class="word-type">${currentWord.type || 'word'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -545,11 +547,11 @@ class VocabularyModule extends DRSExerciseInterface {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="revealed-answer" id="revealed-answer" style="display: none;">
|
<div class="revealed-answer" id="revealed-answer" style="display: none;">
|
||||||
<div class="correct-translation">
|
<div class="correct-translation clickable" id="answer-tts" title="Click to hear pronunciation">
|
||||||
<strong>Correct Answer:</strong> ${currentWord.cleanTranslation}
|
<strong>Correct Answer:</strong> ${currentWord.cleanTranslation}
|
||||||
</div>
|
</div>
|
||||||
${this.config.showPronunciation && currentWord.pronunciation ?
|
${this.config.showPronunciation && currentWord.pronunciation ?
|
||||||
`<div class="pronunciation-text">[${currentWord.pronunciation}]</div>` : ''}
|
`<div class="pronunciation-text" id="pronunciation-reveal">[${currentWord.pronunciation}]</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -567,6 +569,14 @@ class VocabularyModule extends DRSExerciseInterface {
|
|||||||
document.getElementById('reveal-btn').onclick = this._handleRevealAnswer;
|
document.getElementById('reveal-btn').onclick = this._handleRevealAnswer;
|
||||||
document.getElementById('submit-btn').onclick = this._handleUserInput;
|
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
|
// Allow Enter key to submit
|
||||||
const input = document.getElementById('translation-input');
|
const input = document.getElementById('translation-input');
|
||||||
@ -625,9 +635,19 @@ class VocabularyModule extends DRSExerciseInterface {
|
|||||||
answerSection.style.display = 'none';
|
answerSection.style.display = 'none';
|
||||||
this.isRevealed = true;
|
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
|
// Auto-play TTS when answer is revealed
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._handleTTS();
|
this._handleTTS();
|
||||||
|
this._highlightPronunciation();
|
||||||
}, 100); // Quick delay to let the answer appear
|
}, 100); // Quick delay to let the answer appear
|
||||||
|
|
||||||
// Don't mark as incorrect yet - wait for user self-assessment
|
// Don't mark as incorrect yet - wait for user self-assessment
|
||||||
@ -780,22 +800,25 @@ class VocabularyModule extends DRSExerciseInterface {
|
|||||||
|
|
||||||
const utterance = new SpeechSynthesisUtterance(text);
|
const utterance = new SpeechSynthesisUtterance(text);
|
||||||
|
|
||||||
// Configure voice settings
|
// Get language from chapter data, fallback to options or en-US
|
||||||
utterance.lang = options.lang || 'en-US';
|
const chapterLanguage = this.currentExerciseData?.language || 'en-US';
|
||||||
|
utterance.lang = options.lang || chapterLanguage;
|
||||||
utterance.rate = options.rate || 0.8;
|
utterance.rate = options.rate || 0.8;
|
||||||
utterance.pitch = options.pitch || 1;
|
utterance.pitch = options.pitch || 1;
|
||||||
utterance.volume = options.volume || 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();
|
const voices = window.speechSynthesis.getVoices();
|
||||||
if (voices.length > 0) {
|
if (voices.length > 0) {
|
||||||
// Prefer English voices
|
// Find voice matching the chapter language
|
||||||
const englishVoice = voices.find(voice =>
|
const langPrefix = chapterLanguage.split('-')[0]; // e.g., "zh" from "zh-CN"
|
||||||
voice.lang.startsWith('en') && voice.default
|
const matchingVoice = voices.find(voice =>
|
||||||
) || voices.find(voice => voice.lang.startsWith('en'));
|
voice.lang.startsWith(langPrefix) && voice.default
|
||||||
|
) || voices.find(voice => voice.lang.startsWith(langPrefix));
|
||||||
|
|
||||||
if (englishVoice) {
|
if (matchingVoice) {
|
||||||
utterance.voice = englishVoice;
|
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() {
|
_showGroupResults() {
|
||||||
const resultsContainer = document.getElementById('group-results');
|
const resultsContainer = document.getElementById('group-results');
|
||||||
const card = document.getElementById('vocabulary-card');
|
const card = document.getElementById('vocabulary-card');
|
||||||
@ -1039,10 +1078,33 @@ class VocabularyModule extends DRSExerciseInterface {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.target-word.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-word.clickable:hover {
|
||||||
|
color: #667eea;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
.pronunciation {
|
.pronunciation {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 5px;
|
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 {
|
.word-type {
|
||||||
@ -1089,9 +1151,22 @@ class VocabularyModule extends DRSExerciseInterface {
|
|||||||
margin-bottom: 5px;
|
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 {
|
.pronunciation-text {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.exercise-controls {
|
.exercise-controls {
|
||||||
|
|||||||
343
src/gameHelpers/MarioEducational/PhysicsEngine.js
Normal file
343
src/gameHelpers/MarioEducational/PhysicsEngine.js
Normal file
@ -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;
|
||||||
123
src/gameHelpers/MarioEducational/README.md
Normal file
123
src/gameHelpers/MarioEducational/README.md
Normal file
@ -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! 🎮
|
||||||
625
src/gameHelpers/MarioEducational/Renderer.js
Normal file
625
src/gameHelpers/MarioEducational/Renderer.js
Normal file
@ -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();
|
||||||
386
src/gameHelpers/MarioEducational/SentenceGenerator.js
Normal file
386
src/gameHelpers/MarioEducational/SentenceGenerator.js
Normal file
@ -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();
|
||||||
272
src/gameHelpers/MarioEducational/SoundSystem.js
Normal file
272
src/gameHelpers/MarioEducational/SoundSystem.js
Normal file
@ -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();
|
||||||
254
src/gameHelpers/MarioEducational/enemies/Boss.js
Normal file
254
src/gameHelpers/MarioEducational/enemies/Boss.js
Normal file
@ -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;
|
||||||
347
src/gameHelpers/MarioEducational/enemies/Catapult.js
Normal file
347
src/gameHelpers/MarioEducational/enemies/Catapult.js
Normal file
@ -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;
|
||||||
187
src/gameHelpers/MarioEducational/enemies/FlyingEye.js
Normal file
187
src/gameHelpers/MarioEducational/enemies/FlyingEye.js
Normal file
@ -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;
|
||||||
133
src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js
Normal file
133
src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js
Normal file
@ -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;
|
||||||
147
src/gameHelpers/MarioEducational/enemies/Projectile.js
Normal file
147
src/gameHelpers/MarioEducational/enemies/Projectile.js
Normal file
@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user