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 = `
|
||||
<div class="word-card">
|
||||
<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 ?
|
||||
`<div class="pronunciation">[${currentWord.pronunciation}]</div>` : ''}
|
||||
`<div class="pronunciation" id="pronunciation-display">[${currentWord.pronunciation}]</div>` : ''}
|
||||
<div class="word-type">${currentWord.type || 'word'}</div>
|
||||
</div>
|
||||
|
||||
@ -545,11 +547,11 @@ class VocabularyModule extends DRSExerciseInterface {
|
||||
</div>
|
||||
|
||||
<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}
|
||||
</div>
|
||||
${this.config.showPronunciation && currentWord.pronunciation ?
|
||||
`<div class="pronunciation-text">[${currentWord.pronunciation}]</div>` : ''}
|
||||
`<div class="pronunciation-text" id="pronunciation-reveal">[${currentWord.pronunciation}]</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -567,6 +569,14 @@ class VocabularyModule extends DRSExerciseInterface {
|
||||
document.getElementById('reveal-btn').onclick = this._handleRevealAnswer;
|
||||
document.getElementById('submit-btn').onclick = this._handleUserInput;
|
||||
|
||||
// Add click listener on the word itself for TTS
|
||||
const targetWord = document.getElementById('target-word-tts');
|
||||
if (targetWord) {
|
||||
targetWord.onclick = () => {
|
||||
this._handleTTS();
|
||||
this._highlightPronunciation();
|
||||
};
|
||||
}
|
||||
|
||||
// Allow Enter key to submit
|
||||
const input = document.getElementById('translation-input');
|
||||
@ -625,9 +635,19 @@ class VocabularyModule extends DRSExerciseInterface {
|
||||
answerSection.style.display = 'none';
|
||||
this.isRevealed = true;
|
||||
|
||||
// Add click listener on revealed answer for TTS
|
||||
const answerTTS = document.getElementById('answer-tts');
|
||||
if (answerTTS) {
|
||||
answerTTS.onclick = () => {
|
||||
this._handleTTS();
|
||||
this._highlightPronunciation();
|
||||
};
|
||||
}
|
||||
|
||||
// Auto-play TTS when answer is revealed
|
||||
setTimeout(() => {
|
||||
this._handleTTS();
|
||||
this._highlightPronunciation();
|
||||
}, 100); // Quick delay to let the answer appear
|
||||
|
||||
// Don't mark as incorrect yet - wait for user self-assessment
|
||||
@ -780,22 +800,25 @@ class VocabularyModule extends DRSExerciseInterface {
|
||||
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
|
||||
// Configure voice settings
|
||||
utterance.lang = options.lang || 'en-US';
|
||||
// Get language from chapter data, fallback to options or en-US
|
||||
const chapterLanguage = this.currentExerciseData?.language || 'en-US';
|
||||
utterance.lang = options.lang || chapterLanguage;
|
||||
utterance.rate = options.rate || 0.8;
|
||||
utterance.pitch = options.pitch || 1;
|
||||
utterance.volume = options.volume || 1;
|
||||
|
||||
// Try to find a suitable voice
|
||||
// Try to find a suitable voice for the language
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
// Prefer English voices
|
||||
const englishVoice = voices.find(voice =>
|
||||
voice.lang.startsWith('en') && voice.default
|
||||
) || voices.find(voice => voice.lang.startsWith('en'));
|
||||
// Find voice matching the chapter language
|
||||
const langPrefix = chapterLanguage.split('-')[0]; // e.g., "zh" from "zh-CN"
|
||||
const matchingVoice = voices.find(voice =>
|
||||
voice.lang.startsWith(langPrefix) && voice.default
|
||||
) || voices.find(voice => voice.lang.startsWith(langPrefix));
|
||||
|
||||
if (englishVoice) {
|
||||
utterance.voice = englishVoice;
|
||||
if (matchingVoice) {
|
||||
utterance.voice = matchingVoice;
|
||||
console.log('🔊 Using voice:', matchingVoice.name, matchingVoice.lang);
|
||||
}
|
||||
}
|
||||
|
||||
@ -852,6 +875,22 @@ class VocabularyModule extends DRSExerciseInterface {
|
||||
}
|
||||
}
|
||||
|
||||
_highlightPronunciation() {
|
||||
// Highlight pronunciation when TTS is played
|
||||
const pronunciation = document.getElementById('pronunciation-display') ||
|
||||
document.getElementById('pronunciation-reveal');
|
||||
|
||||
if (pronunciation) {
|
||||
// Add highlight class
|
||||
pronunciation.classList.add('pronunciation-highlight');
|
||||
|
||||
// Remove highlight after animation
|
||||
setTimeout(() => {
|
||||
pronunciation.classList.remove('pronunciation-highlight');
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
_showGroupResults() {
|
||||
const resultsContainer = document.getElementById('group-results');
|
||||
const card = document.getElementById('vocabulary-card');
|
||||
@ -1039,10 +1078,33 @@ class VocabularyModule extends DRSExerciseInterface {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.target-word.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.target-word.clickable:hover {
|
||||
color: #667eea;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.pronunciation {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pronunciation-highlight {
|
||||
color: #667eea !important;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
animation: pulse 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.word-type {
|
||||
@ -1089,9 +1151,22 @@ class VocabularyModule extends DRSExerciseInterface {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.correct-translation.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.correct-translation.clickable:hover {
|
||||
background-color: #d4edda;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.pronunciation-text {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.exercise-controls {
|
||||
|
||||
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