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:
StillHammer 2025-10-15 07:25:53 +08:00
parent 7a18e27a44
commit 325b97060c
22 changed files with 7131 additions and 3095 deletions

1426
CLAUDE.md

File diff suppressed because it is too large Load Diff

206
content/books/ledu.json Normal file
View 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
}
]
}

View 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 米"
}
]
}
]
}

View 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": "赚人气"
}
]
}
]
}

View 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第二开始时运动量不要太大只锻炼1015分钟就可以了而且不要做强度太大的运动要让你的身体慢慢地适应。然后可以慢慢地加大运动量和运动强度不过最少在两周以后再这样做。\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 TennisTable 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工作"
}
]
}
]
}

View 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
View 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
View 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
View 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
View 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

View File

@ -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 {

View 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;

View 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! 🎮

View 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();

View 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();

View 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();

View 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;

View 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;

View 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;

View 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;

View 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