Class_generator/src/games/MarioEducational.js
StillHammer ab84bbbc71 Reduce game sizes and fix Mario level display
- Reduce RiverRun game height from 100vh to 75vh for better screen fit
- Reduce AdventureReader game height from 100vh to 75vh
- Fix Mario level number display (was showing currentLevel + 1 twice)
  - Updated HUD level display in Renderer.js
  - Updated finish line flag level display in Renderer.js
- Add portable setup files and documentation
- Add new game modules: SentenceInvaders, ThematicQuestions
- Add new content: wte2 book, sbs chapters 2-3, wte2-2 chapter
- Update various game modules for improved compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 16:22:33 +08:00

2540 lines
96 KiB
JavaScript

import Module from '../core/Module.js';
import { sentenceGenerator } from '../gameHelpers/MarioEducational/SentenceGenerator.js';
import { soundSystem } from '../gameHelpers/MarioEducational/SoundSystem.js';
import { renderer } from '../gameHelpers/MarioEducational/Renderer.js';
import PhysicsEngine from '../gameHelpers/MarioEducational/PhysicsEngine.js';
import PiranhaPlant from '../gameHelpers/MarioEducational/enemies/PiranhaPlant.js';
import Catapult from '../gameHelpers/MarioEducational/enemies/Catapult.js';
import FlyingEye from '../gameHelpers/MarioEducational/enemies/FlyingEye.js';
import Boss from '../gameHelpers/MarioEducational/enemies/Boss.js';
import Projectile from '../gameHelpers/MarioEducational/enemies/Projectile.js';
/**
* MarioEducational - 2D Mario-style educational game with question blocks
* Classic side-scrolling platformer that integrates vocabulary learning through interactive question blocks
*/
class MarioEducational extends Module {
constructor(name, dependencies, config = {}) {
super(name, ['eventBus']);
// Validate dependencies
if (!dependencies.eventBus || !dependencies.content) {
throw new Error('MarioEducational requires eventBus and content dependencies');
}
this._eventBus = dependencies.eventBus;
this._content = dependencies.content;
this._config = {
container: null,
canvasWidth: 1200,
canvasHeight: 690, // +15% viewport height (600 * 1.15 = 690)
gravity: 0.8,
jumpForce: -16,
moveSpeed: 5,
maxLevels: 7,
...config
};
// Game state
this._canvas = null;
this._ctx = null;
this._gameLoop = null;
this._keys = {};
this._isGameOver = false;
this._isPaused = false;
this._score = 0;
this._questionsAnswered = 0;
this._currentLevel = 1;
this._gameStartTime = null;
// Mario character
this._mario = {
x: 100,
y: 400,
width: 32,
height: 32,
velocityX: 0,
velocityY: 0,
onGround: false,
facing: 'right',
color: '#FF6B6B' // Red Mario
};
// Game world
this._camera = { x: 0, y: 0 };
this._platforms = [];
this._questionBlocks = [];
this._enemies = [];
this._collectibles = [];
this._particles = [];
this._walls = [];
this._castleStructure = null;
this._finishLine = null;
// Advanced level elements
this._piranhaPlants = [];
this._projectiles = [];
// Level 4+ catapult system
this._catapults = [];
this._boulders = [];
this._stones = []; // Small stones from onager
// Level 5+ flying eyes system
this._flyingEyes = [];
// Level 6 boss system
this._boss = null;
this._bossTurrets = [];
this._bossMinions = [];
this._powerUps = [];
this._isBoostMode = false;
this._boostTimer = 0;
this._bossCollisionCooldown = 0; // Prevent bounce loop
// Level generation
this._levelData = [];
this._currentLevelIndex = 0;
this._levelWidth = 3000;
this._sentences = [];
this._usedSentences = [];
// Question system
this._questionDialog = null;
this._isQuestionActive = false;
// Celebration mode - blocks enemies but allows particles
this._isCelebrating = false;
// Level completion flag to prevent multiple triggers
this._levelCompleted = false;
// Input handlers (need to be declared before seal)
this._handleKeyDown = null;
this._handleKeyUp = null;
this._handleMouseDown = null;
this._handleMouseUp = null;
this._handleMouseMove = null;
// Mouse control state
this._mousePressed = false;
this._mouseTarget = { x: null, y: null };
this._mouseWorldPos = { x: null, y: null };
// UI elements (need to be declared before seal)
this._uiOverlay = null;
// Debug mode (toggle with 'D' key)
this._debugMode = false;
Object.seal(this);
}
/**
* Get game metadata
* @returns {Object} Game metadata
*/
static getMetadata() {
return {
name: 'Mario Educational Adventure',
description: '2D Mario-style platformer with educational question blocks and vocabulary challenges',
difficulty: 'intermediate',
category: 'platformer',
estimatedTime: 15, // minutes
skills: ['reading', 'vocabulary', 'comprehension', 'reflexes', 'problem-solving']
};
}
/**
* Calculate compatibility score with content
* @param {Object} content - Content to check compatibility with
* @returns {Object} Compatibility score and details
*/
static getCompatibilityScore(content) {
const sentences = content?.sentences || [];
const phrases = content?.phrases || {};
const dialogs = content?.dialogs || {};
const texts = content?.texts || [];
const story = content?.story || '';
let totalSentences = sentences.length;
// Count phrases (SBS format)
totalSentences += Object.keys(phrases).length;
// Count dialog lines (SBS format)
Object.values(dialogs).forEach(dialog => {
if (dialog.lines && Array.isArray(dialog.lines)) {
totalSentences += dialog.lines.length;
}
});
// Count sentences from texts
if (Array.isArray(texts)) {
texts.forEach(text => {
if (typeof text === 'string') {
totalSentences += text.split(/[.!?]+/).filter(s => s.trim().length > 10).length;
} else if (text.content) {
totalSentences += text.content.split(/[.!?]+/).filter(s => s.trim().length > 10).length;
}
});
}
// Count sentences from story
if (story && typeof story === 'string') {
totalSentences += story.split(/[.!?]+/).filter(s => s.trim().length > 10).length;
}
// Mario Educational requires at least 5 sentences
if (totalSentences < 5) {
return {
score: 0,
reason: `Insufficient sentences (${totalSentences}/5 minimum)`,
requirements: ['sentences', 'phrases', 'dialogs', 'texts', 'story'],
minSentences: 5,
details: 'Mario Educational needs at least 5 complete sentences from any source'
};
}
// Good score for 10+ sentences, perfect at 30+
const score = Math.min(totalSentences / 30, 1);
return {
score,
reason: `${totalSentences} sentences available`,
requirements: ['sentences', 'phrases', 'dialogs', 'texts', 'story'],
minSentences: 5,
optimalSentences: 30,
details: `Can create ${Math.min(totalSentences, 50)} question blocks from sentence sources`
};
}
async init() {
this._validateNotDestroyed();
try {
// Validate container
if (!this._config.container) {
throw new Error('Game container is required');
}
// Extract sentences from content
this._extractSentences();
if (this._sentences.length < 5) {
throw new Error('Insufficient content for Mario Educational game');
}
// Setup canvas
this._setupCanvas();
// Setup input handlers
this._setupInputHandlers();
// Initialize sound system
soundSystem.initialize();
// Generate all levels
this._generateAllLevels();
// Start first level
this._startLevel(0); // Start at level 1
// Setup game UI
this._setupGameUI();
// Start game loop
this._startGameLoop();
this._gameStartTime = Date.now();
console.log('🎮 Mario Educational game initialized successfully');
this._setInitialized();
} catch (error) {
console.error('❌ Error initializing Mario Educational game:', error);
throw error;
}
}
async destroy() {
this._validateNotDestroyed();
// Stop game loop
if (this._gameLoop) {
cancelAnimationFrame(this._gameLoop);
this._gameLoop = null;
}
// Remove event listeners
document.removeEventListener('keydown', this._handleKeyDown);
document.removeEventListener('keyup', this._handleKeyUp);
if (this._canvas) {
this._canvas.removeEventListener('mousedown', this._handleMouseDown);
this._canvas.removeEventListener('mouseup', this._handleMouseUp);
this._canvas.removeEventListener('mousemove', this._handleMouseMove);
this._canvas.removeEventListener('mouseleave', this._handleMouseUp);
}
// Clear canvas
if (this._canvas && this._canvas.parentNode) {
this._canvas.parentNode.removeChild(this._canvas);
}
// Clear game state
this._platforms = [];
this._questionBlocks = [];
this._enemies = [];
this._collectibles = [];
this._particles = [];
console.log('🎮 Mario Educational game destroyed');
this._setDestroyed();
}
// Private methods
_extractSentences() {
const sentences = this._content.sentences || [];
const phrases = this._content.phrases || {};
const dialogs = this._content.dialogs || {};
const texts = this._content.texts || [];
const story = this._content.story || '';
// Combine all sentence sources
this._sentences = [];
// Add sentences from 'sentences' array
sentences.forEach(sentence => {
// Format 1: Modern format with english/user_language
if (sentence.english && sentence.user_language) {
this._sentences.push({
type: 'sentence',
english: sentence.english,
translation: sentence.user_language,
context: sentence.context || ''
});
}
// Format 2: Current format with text field
else if (sentence.text) {
this._sentences.push({
type: 'sentence',
english: sentence.text,
translation: sentence.translation || 'Read this sentence carefully',
context: `Difficulty: ${sentence.difficulty || 'intermediate'}`
});
}
});
// Add phrases from 'phrases' object (SBS format)
Object.entries(phrases).forEach(([english, data]) => {
if (data.user_language) {
this._sentences.push({
type: 'phrase',
english: english,
translation: data.user_language,
context: data.context || 'phrase'
});
}
});
// Add dialog lines from 'dialogs' object (SBS format)
Object.values(dialogs).forEach(dialog => {
if (dialog.lines && Array.isArray(dialog.lines)) {
dialog.lines.forEach(line => {
if (line.text && line.user_language) {
this._sentences.push({
type: 'dialog',
english: line.text,
translation: line.user_language,
context: dialog.title || 'dialog'
});
}
});
}
});
// Extract sentences from story text
if (story && typeof story === 'string') {
const storySentences = sentenceGenerator.splitTextIntoSentences(story);
storySentences.forEach(sentence => {
this._sentences.push({
type: 'story',
english: sentence,
translation: 'Story sentence - read carefully',
context: 'Story'
});
});
}
// Extract sentences from texts array
if (Array.isArray(texts)) {
texts.forEach((text, index) => {
if (typeof text === 'string') {
const textSentences = sentenceGenerator.splitTextIntoSentences(text);
textSentences.forEach(sentence => {
this._sentences.push({
type: 'text',
english: sentence,
translation: `Text passage ${index + 1} - practice reading`,
context: `Text ${index + 1}`
});
});
} else if (text.content) {
const textSentences = sentenceGenerator.splitTextIntoSentences(text.content);
textSentences.forEach(sentence => {
this._sentences.push({
type: 'text',
english: sentence,
translation: text.translation || `Read this text sentence`,
context: text.title || `Text ${index + 1}`
});
});
}
});
}
// Shuffle sentences for variety
this._sentences = this._shuffleArray(this._sentences);
console.log(`📝 Extracted ${this._sentences.length} sentences for questions`);
}
_setupCanvas() {
this._canvas = document.createElement('canvas');
this._canvas.width = this._config.canvasWidth;
this._canvas.height = this._config.canvasHeight;
this._canvas.style.border = '2px solid #333';
this._canvas.style.borderRadius = '8px';
this._canvas.style.background = 'linear-gradient(to bottom, #87CEEB, #98FB98)';
this._ctx = this._canvas.getContext('2d');
this._ctx.imageSmoothingEnabled = false; // Pixel art style
this._config.container.appendChild(this._canvas);
}
_setupInputHandlers() {
this._handleKeyDown = (e) => {
this._keys[e.code] = true;
// Toggle debug mode with 'D' key
if (e.code === 'KeyD') {
this._debugMode = !this._debugMode;
console.log(`🐛 Debug mode: ${this._debugMode ? 'ON' : 'OFF'}`);
}
// Prevent default for game controls
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'Space'].includes(e.code)) {
e.preventDefault();
}
};
this._handleKeyUp = (e) => {
this._keys[e.code] = false;
};
// Mouse handlers for click and drag controls
this._handleMouseDown = (e) => {
if (this._isGameOver || this._isPaused || this._isQuestionActive) return;
const rect = this._canvas.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
// Convert to world coordinates (account for camera)
this._mouseWorldPos.x = canvasX + this._camera.x;
this._mouseWorldPos.y = canvasY;
this._mousePressed = true;
console.log(`🖱️ Mouse down at world: (${this._mouseWorldPos.x.toFixed(0)}, ${this._mouseWorldPos.y.toFixed(0)})`);
};
this._handleMouseUp = (e) => {
this._mousePressed = false;
this._mouseTarget.x = null;
this._mouseTarget.y = null;
console.log(`🖱️ Mouse released`);
};
this._handleMouseMove = (e) => {
if (!this._mousePressed) return;
if (this._isGameOver || this._isPaused || this._isQuestionActive) return;
const rect = this._canvas.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
// Convert to world coordinates
this._mouseWorldPos.x = canvasX + this._camera.x;
this._mouseWorldPos.y = canvasY;
};
document.addEventListener('keydown', this._handleKeyDown);
document.addEventListener('keyup', this._handleKeyUp);
if (this._canvas) {
this._canvas.addEventListener('mousedown', this._handleMouseDown);
this._canvas.addEventListener('mouseup', this._handleMouseUp);
this._canvas.addEventListener('mousemove', this._handleMouseMove);
// Also handle mouse leaving canvas
this._canvas.addEventListener('mouseleave', this._handleMouseUp);
console.log('🖱️ Mouse event listeners attached to canvas');
} else {
console.error('❌ Canvas not found when setting up mouse handlers!');
}
}
_generateAllLevels() {
this._levelData = [];
for (let i = 0; i < this._config.maxLevels; i++) {
const isLastLevel = i === this._config.maxLevels - 1;
const level = this._generateLevel(i, isLastLevel);
this._levelData.push(level);
}
console.log(`🏗️ Generated ${this._levelData.length} levels`);
}
_generateLevel(levelIndex, isCastleLevel = false) {
const level = {
index: levelIndex,
isCastleLevel,
platforms: [],
questionBlocks: [],
enemies: [],
collectibles: [],
walls: [],
startX: 100,
startY: 400,
endX: this._levelWidth - 200,
width: this._levelWidth
};
if (isCastleLevel) {
return this._generateCastleLevel(level);
} else {
return this._generateRegularLevel(level);
}
}
_generateRegularLevel(level) {
const { index } = level;
const difficulty = Math.min(index + 1, 5); // Difficulty scales 1-5
// Generate ground platforms
for (let x = 0; x < this._levelWidth; x += 200) {
level.platforms.push({
x: x,
y: this._config.canvasHeight - 50,
width: 200,
height: 50,
type: 'ground',
color: '#8B4513'
});
}
// Generate floating platforms with intelligent placement
this._generateIntelligentPlatforms(level, difficulty);
// Level 3+ gets advanced features BEFORE enemies (so enemy spawning can avoid them)
if (index >= 2 && index <= 4) {
this._generateHoles(level, difficulty);
this._generateStairs(level, difficulty);
}
// Level 6 boss level: only stairs (no holes)
if (index === 5) {
this._generateBossStairs(level, difficulty);
}
// Generate question blocks on reachable platforms
const questionCount = 3 + difficulty;
const availablePlatforms = level.platforms.filter(p => p.y < this._config.canvasHeight - 100);
for (let i = 0; i < questionCount; i++) {
const maxAttempts = 5;
let attemptCount = 0;
let blockPlaced = false;
while (attemptCount < maxAttempts && !blockPlaced) {
attemptCount++;
const platform = availablePlatforms[Math.floor(Math.random() * availablePlatforms.length)];
const sentence = this._getRandomSentence();
if (sentence && platform) {
const blockX = platform.x + platform.width / 2 - 16;
const blockY = platform.y - 32;
const blockWidth = 32;
const blockHeight = 32;
// Check minimum distance from other question blocks
const hasMinDistance = this._checkQuestionBlockDistance(
level.questionBlocks, blockX, blockY, blockWidth, blockHeight, 60
);
if (hasMinDistance) {
level.questionBlocks.push({
x: blockX,
y: blockY,
width: blockWidth,
height: blockHeight,
sentence: sentence,
hit: false,
color: '#FFD700',
symbol: '?'
});
console.log(`💰 Question block ${i}: x=${blockX.toFixed(0)}, y=${blockY.toFixed(0)}, attempts=${attemptCount}`);
blockPlaced = true;
} else {
console.log(`❌ Question block ${i} attempt ${attemptCount} failed: too close to other blocks`);
}
}
}
if (!blockPlaced) {
console.log(`⚠️ Failed to place question block ${i} after ${maxAttempts} attempts`);
}
}
// Generate simple enemies (avoid walls)
let helmetEnemyPlaced = false;
console.log(`🎮 Level ${index + 1}: Generating ${difficulty} enemies (helmetEnemy condition: index=${index} >= 1)`);
for (let i = 0; i < difficulty; i++) {
let enemyPlaced = false;
let attempts = 0;
const maxAttempts = 20;
while (!enemyPlaced && attempts < maxAttempts) {
const platform = level.platforms[Math.floor(Math.random() * level.platforms.length)];
const enemyX = platform.x + Math.random() * (platform.width - 20);
const enemyY = platform.y - 20;
// Check if enemy would spawn inside a wall
const wouldOverlapWall = level.walls.some(wall => {
return enemyX < wall.x + wall.width &&
enemyX + 20 > wall.x &&
enemyY < wall.y + wall.height &&
enemyY + 20 > wall.y;
});
// Check if enemy would spawn over a hole (more thorough check)
const wouldOverlapHole = level.holes && level.holes.some(hole => {
return enemyX + 10 >= hole.x && enemyX + 10 <= hole.x + hole.width; // Check center of enemy
});
// Enemy is spawned on a platform, so it has support by definition
// No need to check hasSolidSupport - the platform we selected IS the support
if (!wouldOverlapWall && !wouldOverlapHole) {
// Level 2+ gets exactly ONE helmet enemy per level
const isHelmetEnemy = index >= 1 && !helmetEnemyPlaced;
if (isHelmetEnemy) {
helmetEnemyPlaced = true;
}
level.enemies.push({
x: enemyX,
y: enemyY,
width: 20,
height: 20,
velocityX: (Math.random() > 0.5 ? 1 : -1) * (1 + Math.random()),
color: isHelmetEnemy ? '#4169E1' : '#8B0000', // Blue for helmet, red for normal
type: isHelmetEnemy ? 'koopa' : 'goomba',
hasHelmet: isHelmetEnemy
});
enemyPlaced = true;
console.log(`✅ Enemy ${i} placed at x=${enemyX.toFixed(0)}, y=${enemyY.toFixed(0)} on platform - Type: ${isHelmetEnemy ? '🔵 KOOPA' : '🔴 Goomba'}`);
} else {
const reason = wouldOverlapWall ? 'wall' : 'hole';
console.log(`🚫 Enemy ${i} attempt ${attempts} failed: ${reason}`);
}
attempts++;
}
if (!enemyPlaced) {
console.log(`⚠️ Failed to place enemy ${i} after ${maxAttempts} attempts`);
}
}
// Generate piranha plants AFTER enemies (visual decoration)
if (index >= 2 && index <= 4) {
this._generatePiranhaPlants(level, difficulty);
}
// Level 6 boss level: limited piranha plants
if (index === 5) {
if (difficulty > 3) {
this._generatePiranhaPlants(level, Math.min(difficulty - 2, 2));
}
}
// Level 4-5 gets catapults (NOT level 6 - boss level)
if (index >= 3 && index <= 4) {
this._generateCatapults(level, difficulty);
}
// Level 5+ gets flying eyes
if (index >= 4) {
this._generateFlyingEyes(level, difficulty);
}
// Level 6 gets colossal boss (DISABLED)
// if (index === 5) {
// this._generateColossalBoss(level, difficulty);
// }
return level;
}
_generateIntelligentPlatforms(level, difficulty) {
// Calculate Mario's jump capabilities
const jumpForce = Math.abs(this._config.jumpForce);
const gravity = this._config.gravity;
const moveSpeed = this._config.moveSpeed;
// Physics calculations for jump distances
const maxJumpHeight = (jumpForce * jumpForce) / (2 * gravity); // Peak height
const totalAirTime = (2 * jumpForce) / gravity; // Time in air
const maxHorizontalDistance = moveSpeed * totalAirTime; // Max horizontal distance
// Safe margins for reachable platforms
const safeJumpHeight = maxJumpHeight * 0.85;
const safeHorizontalDistance = maxHorizontalDistance * 0.8;
console.log(`🔧 Jump physics: height=${safeJumpHeight.toFixed(1)}, distance=${safeHorizontalDistance.toFixed(1)}`);
const platformCount = 8 + difficulty * 2;
const groundHeight = this._config.canvasHeight - 50;
// Define height zones for better vertical distribution
const heightZones = {
low: groundHeight - safeJumpHeight * 0.3, // Just above ground level
mid: groundHeight - safeJumpHeight * 0.6, // Medium height
high: groundHeight - safeJumpHeight * 0.9, // High platforms
veryHigh: groundHeight - safeJumpHeight // Max reachable height
};
// Force vertical progression - alternate between height zones
for (let i = 0; i < platformCount; i++) {
const maxAttempts = 5;
let attemptCount = 0;
let platformPlaced = false;
while (attemptCount < maxAttempts && !platformPlaced) {
attemptCount++;
// Find a suitable previous platform to connect from
const availablePlatforms = level.platforms.filter(p =>
p.x > 100 && p.x < this._levelWidth - 400
);
if (availablePlatforms.length === 0) break;
const fromPlatform = availablePlatforms[Math.floor(Math.random() * availablePlatforms.length)];
// Force better height distribution
let targetY;
const progressRatio = i / platformCount;
// Bias towards higher platforms as we progress
if (progressRatio < 0.3) {
// Early platforms: mix of low and mid
targetY = Math.random() < 0.7 ? heightZones.low : heightZones.mid;
} else if (progressRatio < 0.7) {
// Middle platforms: mix of mid and high
targetY = Math.random() < 0.6 ? heightZones.mid : heightZones.high;
} else {
// Later platforms: mix of high and very high
targetY = Math.random() < 0.5 ? heightZones.high : heightZones.veryHigh;
}
// Add some randomness around target height
targetY += (Math.random() - 0.5) * safeJumpHeight * 0.2;
// Calculate horizontal position within jump range
const distanceRange = {
min: safeHorizontalDistance * 0.4,
max: safeHorizontalDistance * 0.9
};
const direction = Math.random() > 0.5 ? 1 : -1;
const distance = distanceRange.min + Math.random() * (distanceRange.max - distanceRange.min);
const newX = fromPlatform.x + (direction * distance);
const width = 120 + Math.random() * 80;
const height = 20;
const finalY = Math.max(50, Math.min(this._config.canvasHeight - 100, targetY));
// Validate all conditions
const heightDifference = Math.abs(finalY - fromPlatform.y);
const horizontalDistance = Math.abs(newX - fromPlatform.x);
const isReachable = this._validateJumpPossible(
horizontalDistance, heightDifference,
safeHorizontalDistance, safeJumpHeight,
finalY < fromPlatform.y // jumping up or down
);
const minDistance = 100; // Increased minimum distance
const hasMinDistance = this._checkMinimumDistance(
level.platforms, newX, finalY, width, height, minDistance
);
const isInBounds = newX > 150 && newX + width < this._levelWidth - 300;
if (isReachable && hasMinDistance && isInBounds) {
level.platforms.push({
x: newX,
y: finalY,
width: width,
height: height,
type: 'floating',
color: this._getPlatformColor(finalY, heightZones)
});
console.log(`📍 Platform ${i}: x=${newX.toFixed(0)}, y=${finalY.toFixed(0)}, attempts=${attemptCount}`);
platformPlaced = true;
} else {
console.log(`❌ Platform ${i} attempt ${attemptCount} failed: reachable=${isReachable}, minDist=${hasMinDistance}, bounds=${isInBounds}`);
}
}
if (!platformPlaced) {
console.log(`⚠️ Failed to place platform ${i} after ${maxAttempts} attempts`);
}
}
// Add some challenge platforms at max difficulty
if (difficulty >= 3) {
this._addChallengePlatforms(level, safeHorizontalDistance, safeJumpHeight);
}
// Generate walls as obstacles
this._generateWalls(level, difficulty, safeJumpHeight);
}
_validateJumpPossible(horizontalDist, heightDiff, maxHorizontalDist, maxJumpHeight, isJumpingUp) {
// Check horizontal distance
if (horizontalDist > maxHorizontalDist) return false;
// Check vertical distance
if (isJumpingUp && heightDiff > maxJumpHeight) return false;
// For downward jumps, allow more flexibility (gravity helps)
if (!isJumpingUp && heightDiff > maxJumpHeight * 1.5) return false;
return true;
}
_checkMinimumDistance(existingPlatforms, newX, newY, newWidth, newHeight, minDistance) {
for (const platform of existingPlatforms) {
// Skip ground platforms for distance check, but increase minimum distance from ground
if (platform.type === 'ground') {
// Check minimum distance from ground level (increased)
if (Math.abs(newY - platform.y) < 120) { // Increased from 80 to 120
return false;
}
continue;
}
// Check for overlap and minimum distance
const newRight = newX + newWidth;
const newBottom = newY + newHeight;
const platformRight = platform.x + platform.width;
const platformBottom = platform.y + platform.height;
// Calculate distances between edges
const horizontalDistance = Math.max(0,
Math.max(platform.x - newRight, newX - platformRight)
);
const verticalDistance = Math.max(0,
Math.max(platform.y - newBottom, newY - platformBottom)
);
// Check if platforms overlap or are too close
if (horizontalDistance === 0 && verticalDistance === 0) {
// Overlapping
return false;
}
// Check minimum distance
const totalDistance = Math.sqrt(horizontalDistance * horizontalDistance + verticalDistance * verticalDistance);
if (totalDistance < minDistance) {
return false;
}
}
return true;
}
_checkQuestionBlockDistance(existingBlocks, newX, newY, newWidth, newHeight, minDistance) {
for (const block of existingBlocks) {
// Calculate distances between edges
const newRight = newX + newWidth;
const newBottom = newY + newHeight;
const blockRight = block.x + block.width;
const blockBottom = block.y + block.height;
const horizontalDistance = Math.max(0,
Math.max(block.x - newRight, newX - blockRight)
);
const verticalDistance = Math.max(0,
Math.max(block.y - newBottom, newY - blockBottom)
);
// Check if blocks overlap or are too close
if (horizontalDistance === 0 && verticalDistance === 0) {
// Overlapping
return false;
}
// Check minimum distance
const totalDistance = Math.sqrt(horizontalDistance * horizontalDistance + verticalDistance * verticalDistance);
if (totalDistance < minDistance) {
return false;
}
}
return true;
}
_getPlatformColor(y, heightZones) {
if (y >= heightZones.low) return '#90EE90'; // Light green (low)
if (y >= heightZones.mid) return '#32CD32'; // Green (mid)
if (y >= heightZones.high) return '#228B22'; // Forest green (high)
return '#006400'; // Dark green (very high)
}
_generateWalls(level, difficulty, maxJumpHeight) {
const wallCount = Math.floor(difficulty * 1.5); // More walls as difficulty increases
const groundHeight = this._config.canvasHeight - 50;
console.log(`🧱 Generating ${wallCount} walls for difficulty ${difficulty}`);
for (let i = 0; i < wallCount; i++) {
// Find suitable positions that don't block critical paths
const attempts = 20;
let placed = false;
for (let attempt = 0; attempt < attempts && !placed; attempt++) {
// Random position in the middle section of the level
const x = 400 + Math.random() * (this._levelWidth - 800);
// Wall heights based on difficulty
let wallHeight;
if (difficulty <= 2) {
// Low walls - can be jumped over
wallHeight = maxJumpHeight * (0.3 + Math.random() * 0.4);
} else if (difficulty <= 4) {
// Medium walls - some jumpable, some need platform
wallHeight = Math.random() < 0.5
? maxJumpHeight * (0.3 + Math.random() * 0.4) // Jumpable
: maxJumpHeight * (0.8 + Math.random() * 0.3); // Need platform
} else {
// High walls - mostly need platforms or alternate routes
wallHeight = maxJumpHeight * (0.7 + Math.random() * 0.4);
}
const wallY = groundHeight - wallHeight;
const wallWidth = 20 + Math.random() * 30; // Variable width
// Check if wall placement is valid (not too close to other obstacles)
const isValidPosition = this._isValidWallPosition(
level, x, wallY, wallWidth, wallHeight
);
if (isValidPosition) {
level.walls.push({
x: x,
y: wallY,
width: wallWidth,
height: wallHeight,
color: '#8B4513', // Brown color
type: wallHeight > maxJumpHeight * 0.6 ? 'tall' : 'short',
health: 3, // Wall can be damaged 3 times before being destroyed
maxHealth: 3
});
console.log(`🧱 Wall placed: x=${x.toFixed(0)}, height=${wallHeight.toFixed(0)} (${wallHeight > maxJumpHeight * 0.6 ? 'tall' : 'short'})`);
placed = true;
}
}
}
}
_isValidWallPosition(level, wallX, wallY, wallWidth, wallHeight) {
const wallRight = wallX + wallWidth;
const wallBottom = wallY + wallHeight;
const minDistance = 120; // Minimum distance from other obstacles
// Check distance from platforms
for (const platform of level.platforms) {
if (platform.type === 'ground') continue; // Skip ground check
const platformRight = platform.x + platform.width;
// Check overlap or too close
if (!(wallRight < platform.x - minDistance ||
wallX > platformRight + minDistance ||
wallBottom < platform.y - minDistance ||
wallY > platform.y + platform.height + minDistance)) {
return false;
}
}
// Check distance from other walls
for (const wall of level.walls) {
const wallRight2 = wall.x + wall.width;
if (!(wallRight < wall.x - minDistance ||
wallX > wallRight2 + minDistance)) {
return false;
}
}
// Don't place walls too close to start/end
if (wallX < 300 || wallRight > this._levelWidth - 300) {
return false;
}
return true;
}
_addChallengePlatforms(level, maxDistance, maxHeight) {
// Add 2-3 challenging but reachable platforms
const challengeCount = 2 + Math.floor(Math.random() * 2);
for (let i = 0; i < challengeCount; i++) {
const availablePlatforms = level.platforms.filter(p =>
p.type === 'floating' && p.x > 300 && p.x < this._levelWidth - 500
);
if (availablePlatforms.length === 0) continue;
const fromPlatform = availablePlatforms[Math.floor(Math.random() * availablePlatforms.length)];
// Use 90-95% of max capabilities for challenge
const challengeDistance = maxDistance * (0.9 + Math.random() * 0.05);
const challengeHeight = maxHeight * (0.8 + Math.random() * 0.15);
const direction = Math.random() > 0.5 ? 1 : -1;
const newX = fromPlatform.x + (direction * challengeDistance);
const newY = Math.max(50, fromPlatform.y - challengeHeight);
if (newX > 150 && newX < this._levelWidth - 300) {
level.platforms.push({
x: newX,
y: newY,
width: 100, // Smaller platforms for challenge
height: 20,
type: 'floating',
color: '#FF6B35' // Orange for challenge platforms
});
}
}
}
_generateCastleLevel(level) {
// Ground platforms for entire level
for (let x = 0; x < this._levelWidth; x += 200) {
level.platforms.push({
x: x,
y: this._config.canvasHeight - 50,
width: 200,
height: 50,
type: 'ground',
color: '#8B4513'
});
}
// Create single ascending staircase to castle
const jumpDistance = 180; // Comfortable jump distance
const castleX = this._levelWidth - 200; // Castle position
const numberOfPlatforms = Math.floor((castleX - 400) / jumpDistance) + 1;
// Progressive platforms leading directly to castle
for (let i = 0; i < numberOfPlatforms; i++) {
const x = 400 + i * jumpDistance;
const y = this._config.canvasHeight - 100 - (i * 30); // Steeper but manageable ascent
const isNearCastle = x > castleX - 300;
level.platforms.push({
x: x,
y: Math.max(200, y), // Higher ceiling for castle approach
width: isNearCastle ? 160 : 140, // Wider near castle
height: 20,
type: 'ascending',
color: isNearCastle ? '#FFD700' : '#32CD32' // Gold near castle
});
}
// Final platform right at castle level
level.platforms.push({
x: this._levelWidth - 360, // Aligned with shifted castle
y: this._config.canvasHeight - 160, // Lower since castle is on ground
width: 160,
height: 20,
type: 'castle_entrance',
color: '#FFD700'
});
// Castle emoji (visual only - no collision!)
const groundLevel = this._config.canvasHeight - 50;
level.castleStructure = {
x: this._levelWidth - 280, // Shifted left
y: groundLevel - 140, // Slightly in ground (360/2 = 180, but -140 puts it 40px in ground)
emoji: '🏰',
size: 360,
princess: {
x: this._levelWidth - 280, // Same X as castle
y: groundLevel - 200, // Above castle, adjusted for lower castle
emoji: '👸',
size: 50 // Slightly bigger princess
}
};
// Question blocks on accessible platforms
const accessiblePlatforms = level.platforms.filter(p => p.type === 'ascending' || p.type === 'floating');
for (let i = 0; i < Math.min(5, accessiblePlatforms.length); i++) {
const platform = accessiblePlatforms[i];
const sentence = this._getRandomSentence();
if (sentence) {
level.questionBlocks.push({
x: platform.x + platform.width / 2 - 16,
y: platform.y - 32,
width: 32,
height: 32,
sentence: sentence,
hit: false,
color: '#FFD700',
symbol: '?'
});
}
}
console.log(`🏰 Castle level generated with ${level.platforms.length} platforms and castle structure`);
return level;
}
_generateHoles(level, difficulty) {
const holeCount = Math.min(difficulty - 1, 3); // 1-3 holes based on difficulty
level.holes = []; // Store hole positions to avoid spawning over them
for (let i = 0; i < holeCount; i++) {
let holeX, holeWidth = 1; // TEST: 1 pixel wide to see the holes clearly
let validPosition = false;
let attempts = 0;
const maxAttempts = 20;
while (!validPosition && attempts < maxAttempts) {
holeX = 300 + (i * 800) + Math.random() * 400; // Spread holes out
// Check minimum distance from other holes (at least 2 tiles = 400px apart)
const tooCloseToOtherHole = level.holes.some(existingHole => {
return Math.abs(holeX - existingHole.x) < 400;
});
if (!tooCloseToOtherHole) {
validPosition = true;
}
attempts++;
}
if (validPosition) {
// Store hole position for collision avoidance
level.holes.push({
x: holeX,
width: holeWidth
});
// Simply remove ground platforms that overlap with hole area
level.platforms = level.platforms.filter(platform => {
if (platform.type === 'ground') {
return !(platform.x < holeX + holeWidth && platform.x + platform.width > holeX);
}
return true;
});
console.log(`🕳️ Hole ${i} created by removing ground at x=${holeX.toFixed(0)}, width=${holeWidth}px`);
} else {
console.log(`⚠️ Failed to place hole ${i} after ${maxAttempts} attempts - too close to other holes`);
}
}
}
_generateStairs(level, difficulty) {
const groundLevel = this._config.canvasHeight - 50;
const stairCount = Math.min(difficulty, 2); // 1-2 stair sets
for (let i = 0; i < stairCount; i++) {
let stairPlaced = false;
let attempts = 0;
const maxAttempts = 20;
while (!stairPlaced && attempts < maxAttempts) {
const stairX = 500 + (i * 1000) + Math.random() * 300;
const stepWidth = 80;
const stepHeight = 40;
const steps = 3 + Math.floor(Math.random() * 3); // 3-5 steps
// Check if stairs would collide with existing platforms or holes
let wouldCollide = false;
// Check collision with holes first
const overHole = level.holes && level.holes.some(hole => {
return stairX < hole.x + hole.width && stairX + (steps * stepWidth) > hole.x;
});
if (overHole) {
wouldCollide = true;
} else {
// Check collision with existing platforms
for (let step = 0; step < steps; step++) {
const stepX = stairX + (step * stepWidth);
const stepY = groundLevel - ((step + 1) * stepHeight);
const hasCollision = level.platforms.some(platform => {
return stepX < platform.x + platform.width &&
stepX + stepWidth > platform.x &&
stepY < platform.y + platform.height &&
stepY + stepHeight > platform.y;
});
if (hasCollision) {
wouldCollide = true;
break;
}
}
}
if (!wouldCollide) {
// Generate ascending stairs
for (let step = 0; step < steps; step++) {
level.platforms.push({
x: stairX + (step * stepWidth),
y: groundLevel - ((step + 1) * stepHeight),
width: stepWidth,
height: stepHeight,
color: '#8B4513', // Brown like walls
type: 'stair'
});
}
stairPlaced = true;
console.log(`🪜 Stairs created at x=${stairX.toFixed(0)}, ${steps} steps`);
} else {
console.log(`🚫 Stair attempt ${attempts} at x=${stairX.toFixed(0)} would collide, retrying...`);
}
attempts++;
}
if (!stairPlaced) {
console.log(`⚠️ Failed to place stairs ${i} after ${maxAttempts} attempts`);
}
}
}
_generatePiranhaPlants(level, difficulty) {
level.piranhaPlants = PiranhaPlant.generate(level, difficulty);
}
_generateCatapults(level, difficulty) {
level.catapults = Catapult.generate(level, level.index, this._levelWidth, this._config.canvasHeight);
}
_generateCatapultsOLD(level, difficulty) {
const { index } = level;
let catapultCount = 1; // Always 1 catapult for level 4+
let onagerCount = 0;
// Level 5+ gets onagers
if (index >= 4) {
onagerCount = 1; // 1 onager for level 5+
}
const totalCount = catapultCount + onagerCount;
console.log(`🏹 Generating ${catapultCount} catapult(s) and ${onagerCount} onager(s) for level ${index + 1}`);
for (let i = 0; i < totalCount; i++) {
const isOnager = i >= catapultCount; // Onagers come after catapults
// Place catapults near END of level to help finish it
const nearEndX = this._levelWidth * 0.7; // 70% through level
const catapultX = nearEndX + (i * 300) + Math.random() * 200;
let catapultY = this._config.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)
let obstacleAbove = null;
const obstacles = [platformAbove, wallAbove, stairAbove].filter(obs => obs !== null);
if (obstacles.length > 0) {
// Find the obstacle with the highest Y value (closest to ground)
obstacleAbove = obstacles.reduce((lowest, current) =>
current.y > lowest.y ? current : lowest
);
}
if (obstacleAbove) {
// Place catapult ON TOP of the obstacle
catapultY = obstacleAbove.y - 80; // 80 is catapult height
console.log(`🏹 Catapult moved to obstacle at y=${catapultY.toFixed(0)}`);
}
level.catapults = level.catapults || [];
level.catapults.push({
x: catapultX,
y: catapultY,
width: 60,
height: 80,
color: isOnager ? '#654321' : '#8B4513', // Onager: darker brown, Catapult: normal brown
lastShot: 0,
shootCooldown: isOnager ? 6000 + Math.random() * 2000 : 4000 + Math.random() * 2000, // Onager shoots much less often (6-8s vs 4-6s)
type: isOnager ? 'onager' : 'catapult',
isOnager: isOnager
});
console.log(`${isOnager ? '🏛️' : '🏹'} ${isOnager ? 'Onager' : 'Catapult'} placed at x=${catapultX.toFixed(0)}, y=${catapultY.toFixed(0)}`);
}
}
_findPlatformAbove(x, groundY, platforms) {
// Find the lowest platform that's above the ground position and overlaps horizontally
let bestPlatform = null;
let lowestY = 0; // We want the platform closest to ground (highest Y value)
platforms.forEach(platform => {
// Check horizontal overlap (catapult width is 60)
const catapultLeft = x;
const catapultRight = x + 60;
const platformLeft = platform.x;
const platformRight = platform.x + platform.width;
const hasHorizontalOverlap = catapultLeft < platformRight && catapultRight > platformLeft;
// Check if platform is above ground and has horizontal overlap
if (hasHorizontalOverlap && platform.y < groundY && platform.y > lowestY) {
bestPlatform = platform;
lowestY = platform.y;
}
});
return bestPlatform;
}
_findWallAbove(x, groundY, walls) {
// Find the lowest wall that's above the ground position and overlaps horizontally
let bestWall = null;
let lowestY = 0; // We want the wall closest to ground (highest Y value)
walls.forEach(wall => {
// Check horizontal overlap (catapult width is 60)
const catapultLeft = x;
const catapultRight = x + 60;
const wallLeft = wall.x;
const wallRight = wall.x + wall.width;
const hasHorizontalOverlap = catapultLeft < wallRight && catapultRight > wallLeft;
// Check if wall is above ground and has horizontal overlap
if (hasHorizontalOverlap && wall.y < groundY && wall.y > lowestY) {
bestWall = wall;
lowestY = wall.y;
}
});
return bestWall;
}
_findStairAbove(x, groundY, stairs) {
// Find the lowest stair that's above the ground position and overlaps horizontally
let bestStair = null;
let lowestY = 0; // We want the stair closest to ground (highest Y value)
stairs.forEach(stair => {
// Check horizontal overlap (catapult width is 60)
const catapultLeft = x;
const catapultRight = x + 60;
const stairLeft = stair.x;
const stairRight = stair.x + stair.width;
const hasHorizontalOverlap = catapultLeft < stairRight && catapultRight > stairLeft;
// Check if stair is above ground and has horizontal overlap
if (hasHorizontalOverlap && stair.y < groundY && stair.y > lowestY) {
bestStair = stair;
lowestY = stair.y;
}
});
return bestStair;
}
_generateColossalBoss(level, difficulty) {
console.log(`👹 Generating Colossal Boss for level 6!`);
// Boss positioned in center-right of level to block the path
const bossX = this._levelWidth * 0.6; // 60% through the level
const bossY = this._config.canvasHeight - 250; // Standing on ground
const bossWidth = 150;
const bossHeight = 200;
level.boss = {
x: bossX,
y: bossY,
width: bossWidth,
height: bossHeight,
health: 5, // Takes 5 hits (1/5 each)
maxHealth: 5,
color: '#2F4F4F', // Dark slate gray
type: 'colossus',
// 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
};
// Generate turrets on the boss (2 turrets)
level.bossTurrets = [
{
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=${level.boss.health}`);
console.log(`🔫 ${level.bossTurrets.length} turrets mounted on boss`);
}
_generateBossStairs(level, difficulty) {
const groundLevel = this._config.canvasHeight - 50;
const stairCount = Math.min(3, 2); // Maximum 3 stair sets for boss level
for (let i = 0; i < stairCount; i++) {
let stairPlaced = false;
let attempts = 0;
const maxAttempts = 20;
while (!stairPlaced && attempts < maxAttempts) {
const stairX = 400 + (i * 800) + Math.random() * 300;
const stepWidth = 80;
const stepHeight = 30;
const maxSteps = 2; // BOSS LEVEL: Maximum 2 steps only
// Check if stair area is clear of holes and other obstacles
const stairAreaClear = true; // No holes in boss level
if (stairAreaClear) {
level.stairs = level.stairs || [];
// Generate stair steps (max 2 steps)
for (let step = 0; step < maxSteps; step++) {
level.stairs.push({
x: stairX + (step * stepWidth),
y: groundLevel - ((step + 1) * stepHeight),
width: stepWidth,
height: stepHeight * (step + 1), // Height increases with each step
color: '#D2691E', // Orange brown for boss level stairs
type: 'stair'
});
}
stairPlaced = true;
console.log(`🏰 Boss stair ${i + 1} placed at x=${stairX.toFixed(0)} (${maxSteps} steps max)`);
}
attempts++;
}
if (!stairPlaced) {
console.log(`⚠️ Failed to place boss stair ${i} after ${maxAttempts} attempts`);
}
}
}
_generateFlyingEyes(level, difficulty) {
const eyeCount = Math.min(4, Math.max(3, difficulty - 2)); // 3-4 flying eyes (more guaranteed)
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
level.flyingEyes = level.flyingEyes || [];
level.flyingEyes.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,
// 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)}`);
}
}
_hasSolidSupportBelow(x, y, level) {
// Check if there's solid support (ground platforms, floating platforms, stairs) directly below this position
const enemyWidth = 20;
const enemyCenterX = x + 10; // Center of enemy
const checkY = y + 5; // Just below the enemy
// Check if there's a ground platform below (that wasn't removed by holes)
const hasGroundPlatform = level.platforms.some(platform => {
const isGroundLevel = platform.type === 'ground' && platform.y >= this._config.canvasHeight - 60;
const isUnderEnemy = platform.x <= enemyCenterX && platform.x + platform.width >= enemyCenterX;
const isBelow = platform.y >= checkY;
return isGroundLevel && isUnderEnemy && isBelow;
});
// Check if there's any other platform below
const hasFloatingPlatform = level.platforms.some(platform => {
const isFloating = platform.type !== 'ground';
const isUnderEnemy = platform.x <= enemyCenterX && platform.x + platform.width >= enemyCenterX;
const isBelow = platform.y >= checkY && platform.y <= checkY + 50; // Within reasonable distance below
return isFloating && isUnderEnemy && isBelow;
});
// Check if there's a stair below
const hasStairSupport = level.stairs && level.stairs.some(stair => {
const isUnderEnemy = stair.x <= enemyCenterX && stair.x + stair.width >= enemyCenterX;
const isBelow = stair.y >= checkY && stair.y <= checkY + 50;
return isUnderEnemy && isBelow;
});
return hasGroundPlatform || hasFloatingPlatform || hasStairSupport;
}
_hasLineOfSight(catapult, targetX, targetY) {
// Check only the BEGINNING of the trajectory (immediate launch area)
const startX = catapult.x + 30; // Center of catapult
const startY = catapult.y - 10; // Top of catapult
// Check just the first 3 points of trajectory (immediate launch area only)
const steps = 3;
for (let i = 1; i <= steps; i++) {
const progress = i / 10; // Only check first 30% of trajectory
const checkX = startX + (targetX - startX) * progress;
const checkY = startY + (targetY - startY) * progress * 0.5; // Slight arc
// Check if any platform blocks this immediate launch area
const blocked = this._platforms.some(platform =>
checkX >= platform.x &&
checkX <= platform.x + platform.width &&
checkY >= platform.y &&
checkY <= platform.y + platform.height
);
if (blocked) {
return false; // Launch area blocked
}
}
return true; // Clear launch area
}
_startLevel(levelIndex) {
if (levelIndex >= this._levelData.length) {
this._completeGame();
return;
}
this._currentLevelIndex = levelIndex;
this._currentLevel = levelIndex + 1;
// Reset completion flag for new level
this._levelCompleted = false;
this._isCelebrating = false;
const level = this._levelData[levelIndex];
this._platforms = [...level.platforms];
this._questionBlocks = [...level.questionBlocks];
this._enemies = [...level.enemies];
this._collectibles = [...level.collectibles];
this._walls = [...(level.walls || [])];
this._castleStructure = level.castleStructure || null;
// Create finish line at level end
this._finishLine = {
x: level.endX,
y: this._config.canvasHeight - 150,
height: 150
};
// Advanced level elements
this._piranhaPlants = [...(level.piranhaPlants || [])];
this._projectiles = []; // Reset projectiles each level
// Level 4+ catapult system
this._catapults = [...(level.catapults || [])];
this._boulders = []; // Reset boulders each level
this._stones = []; // Reset stones each level
// Level 5+ flying eyes system
this._flyingEyes = [...(level.flyingEyes || [])];
// Level 6 boss system
this._boss = level.boss || null;
this._bossTurrets = [...(level.bossTurrets || [])];
this._bossMinions = []; // Reset minions each level
this._powerUps = []; // Reset power-ups each level
// Reset Mario position
this._mario.x = level.startX;
this._mario.y = level.startY;
this._mario.velocityX = 0;
this._mario.velocityY = 0;
this._mario.onGround = false;
// Reset camera
this._camera.x = 0;
this._camera.y = 0;
console.log(`🎮 Started level ${this._currentLevel}${level.isCastleLevel ? ' (Castle)' : ''}`);
}
_setupGameUI() {
// Create UI overlay
const uiOverlay = document.createElement('div');
uiOverlay.style.position = 'absolute';
uiOverlay.style.top = '10px';
uiOverlay.style.left = '10px';
uiOverlay.style.color = 'white';
uiOverlay.style.fontFamily = 'Arial, sans-serif';
uiOverlay.style.fontSize = '18px';
uiOverlay.style.fontWeight = 'bold';
uiOverlay.style.textShadow = '2px 2px 4px rgba(0,0,0,0.8)';
uiOverlay.style.pointerEvents = 'none';
uiOverlay.style.zIndex = '10';
this._uiOverlay = uiOverlay;
this._config.container.style.position = 'relative';
this._config.container.appendChild(uiOverlay);
}
_startGameLoop() {
const gameLoop = () => {
if (!this._isGameOver && !this._isPaused && !this._isQuestionActive) {
this._update();
}
this._render();
this._gameLoop = requestAnimationFrame(gameLoop);
};
gameLoop();
}
_update() {
// Handle input
this._updateMarioMovement();
// Update Mario physics
this._updateMarioPhysics();
// Update enemies
this._updateEnemies();
// Check collisions
this._checkCollisions();
// Update camera
this._updateCamera();
// Update advanced elements
this._updatePiranhaPlants();
this._updateProjectiles();
// Update level 4+ elements
this._updateCatapults();
this._updateBoulders();
this._updateStones();
// Update level 5+ elements
this._updateFlyingEyes();
// Update level 6 boss elements
this._updateBoss();
// Update particles
this._updateParticles();
// Check level completion
this._checkLevelCompletion();
}
_updateMarioMovement() {
// Mouse controls take priority when mouse is pressed
if (this._mousePressed && this._mouseWorldPos.x !== null) {
this._handleMouseMovement();
} else {
// Fallback to keyboard controls
PhysicsEngine.updateMarioMovement(this._mario, this._keys, this._config, this._isCelebrating, (sound) => soundSystem.play(sound));
}
}
_handleMouseMovement() {
if (this._isCelebrating) return;
const targetX = this._mouseWorldPos.x;
const targetY = this._mouseWorldPos.y;
const marioX = this._mario.x + this._mario.width / 2; // Mario center
const marioY = this._mario.y + this._mario.height / 2;
const distanceX = targetX - marioX;
const distanceY = targetY - marioY;
const totalDistance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
// Dead zone - don't move if target is very close
if (totalDistance < 10) {
this._mario.velocityX *= 0.8; // Friction
return;
}
// Horizontal movement
if (Math.abs(distanceX) > 5) {
if (distanceX > 0) {
this._mario.velocityX = this._config.moveSpeed;
this._mario.facing = 'right';
} else {
this._mario.velocityX = -this._config.moveSpeed;
this._mario.facing = 'left';
}
}
// Jump if target is above Mario and Mario is on ground
if (distanceY < -20 && this._mario.onGround && Math.abs(distanceX) < 100) {
this._mario.velocityY = this._config.jumpForce;
this._mario.onGround = false;
soundSystem.play('jump');
console.log(`🖱️ Auto-jump towards target`);
}
}
_updateMarioPhysics() {
const level = this._levelData[this._currentLevelIndex];
PhysicsEngine.updateMarioPhysics(this._mario, this._config, level, this._levelCompleted, () => this._restartLevel());
}
_updateEnemies() {
PhysicsEngine.updateEnemies(this._enemies, this._walls, this._platforms, this._levelWidth, this._isCelebrating);
}
_checkCollisions() {
const gameState = {
platforms: this._platforms,
questionBlocks: this._questionBlocks,
enemies: this._enemies,
walls: this._walls,
catapults: this._catapults,
piranhaPlants: this._piranhaPlants,
boulders: this._boulders,
flyingEyes: this._flyingEyes
};
const callbacks = {
onQuestionBlock: (block) => this._hitQuestionBlock(block),
onEnemyDefeat: (index) => {
this._score += 50;
this._enemies.splice(index, 1);
},
onMarioDeath: () => this._restartLevel(),
onAddParticles: (x, y, color) => this._addParticles(x, y, color)
};
PhysicsEngine.checkCollisions(this._mario, gameState, callbacks);
}
_isColliding(rect1, rect2) {
return PhysicsEngine.isColliding(rect1, rect2);
}
_hitQuestionBlock(block) {
if (block.hit) return;
block.hit = true;
block.color = '#8B4513'; // Brown when hit
block.symbol = '!';
this._score += 100; // Increased points for question blocks
this._addParticles(block.x, block.y, '#FFD700');
soundSystem.play('question_block');
// Show question dialog
this._showQuestionDialog(block.sentence);
}
_showQuestionDialog(sentence) {
this._isPaused = true;
this._isQuestionActive = true;
// Create dialog overlay
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.zIndex = '1000';
const dialog = document.createElement('div');
dialog.style.backgroundColor = 'white';
dialog.style.padding = '30px';
dialog.style.borderRadius = '15px';
dialog.style.maxWidth = '500px';
dialog.style.textAlign = 'center';
dialog.style.boxShadow = '0 10px 30px rgba(0,0,0,0.3)';
dialog.style.fontFamily = 'Arial, sans-serif';
const title = document.createElement('h2');
title.textContent = '🎧 Listen & Learn!';
title.style.color = '#FF6B6B';
title.style.marginBottom = '20px';
const questionText = document.createElement('div');
questionText.innerHTML = `
<p style="font-size: 18px; margin-bottom: 10px;"><strong>English:</strong> ${sentence.english}</p>
<p style="font-size: 18px; margin-bottom: 20px; color: #4ECDC4;"><strong>Translation:</strong> ${sentence.translation}</p>
<p style="font-size: 14px; color: #666;">${sentence.context ? `Context: ${sentence.context}` : ''}</p>
`;
// Add progress indicator
const progressBar = document.createElement('div');
progressBar.style.width = '100%';
progressBar.style.height = '4px';
progressBar.style.backgroundColor = '#E0E0E0';
progressBar.style.borderRadius = '2px';
progressBar.style.marginTop = '20px';
progressBar.style.overflow = 'hidden';
const progressFill = document.createElement('div');
progressFill.style.width = '0%';
progressFill.style.height = '100%';
progressFill.style.backgroundColor = '#4ECDC4';
progressFill.style.transition = 'width linear';
progressBar.appendChild(progressFill);
dialog.appendChild(title);
dialog.appendChild(questionText);
dialog.appendChild(progressBar);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Start TTS and auto-close
this._playTTSAndAutoClose(sentence.english, overlay, progressFill);
}
async _playTTSAndAutoClose(text, overlay, progressBar) {
// Calculate duration based on text length (words per minute estimation)
const words = text.split(' ').length;
const wordsPerMinute = 150; // Average speaking speed
const baseDuration = Math.max(2000, (words / wordsPerMinute) * 60000); // Minimum 2 seconds
const duration = Math.min(baseDuration, 8000); // Maximum 8 seconds
console.log(`🔊 Playing TTS for: "${text}" (${words} words, ${duration}ms)`);
// Use Web Speech API for TTS
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 0.8; // Slightly slower for clarity
utterance.pitch = 1.0;
utterance.volume = 0.8;
// Get target language from content
const targetLanguage = this._content?.language || 'en-US';
utterance.lang = targetLanguage;
// Wait for voices to be loaded before selecting one
const voices = await this._getVoices();
const langPrefix = targetLanguage.split('-')[0];
const matchingVoice = voices.find(voice =>
voice.lang.startsWith(langPrefix) && voice.default
) || voices.find(voice => voice.lang.startsWith(langPrefix));
if (matchingVoice) {
utterance.voice = matchingVoice;
console.log(`🎤 Using voice: ${matchingVoice.name} (${matchingVoice.lang})`);
} else {
console.warn(`🔊 No voice found for language: ${targetLanguage}, available:`, voices.map(v => v.lang));
}
speechSynthesis.speak(utterance);
}
// Animate progress bar
progressBar.style.transitionDuration = `${duration}ms`;
progressBar.style.width = '100%';
// Auto close after duration
setTimeout(() => {
if (overlay && overlay.parentNode) {
document.body.removeChild(overlay);
this._isPaused = false;
this._isQuestionActive = false;
this._questionsAnswered++;
this._score += 300; // Increased bonus for listening
console.log(`✅ Question dialog auto-closed. Questions answered: ${this._questionsAnswered}`);
}
}, duration);
}
_updatePiranhaPlants() {
PiranhaPlant.update(this._piranhaPlants, this._mario, this._projectiles, (sound) => soundSystem.play(sound));
}
_updateProjectiles() {
this._projectiles = Projectile.update(
this._projectiles,
this._mario,
this._platforms,
this._walls,
this._levelWidth,
() => { console.log(`🔥 Mario hit by projectile`); this._restartLevel(); },
(proj, idx, x, y) => this._addParticles(x, y, '#FF4500')
);
}
_updateCatapults() {
Catapult.update(this._catapults, this._mario, this._boulders, this._stones, (sound) => soundSystem.play(sound), this._config.canvasHeight);
}
_updateBoulders() {
Catapult.updateBoulders(
this._boulders, this._mario, this._platforms, this._walls,
(boulder, idx, impactX, impactY, obj, objIdx, type) => {
this._handleBoulderImpact(boulder, idx, impactX, impactY, obj, objIdx, type);
}
);
}
_handleBoulderImpact(boulder, boulderIndex, impactX, impactY, hitObject = null, hitObjectIndex = -1, hitType = 'platform') {
// Special case: Boulder hit Mario while flying
if (hitType === 'mario') {
console.log(`💀 Flying boulder hit Mario - restarting level`);
this._addParticles(this._mario.x, this._mario.y, '#FF0000'); // Red death particles
soundSystem.play('enemy_defeat');
// Remove the boulder
this._boulders.splice(boulderIndex, 1);
// Restart level
this._restartLevel();
return;
}
// Set boulder as landed at impact point
boulder.x = impactX;
boulder.y = impactY;
boulder.velocityX = 0;
boulder.velocityY = 0;
boulder.hasLanded = true;
boulder.color = '#8B4513'; // Brown when landed (becomes platform)
// Explosion effect
this._addParticles(boulder.x + boulder.width/2, boulder.y + boulder.height/2, '#FF4500');
soundSystem.play('enemy_defeat');
// Kill enemies in explosion radius (50 pixels)
this._enemies.forEach((enemy, enemyIndex) => {
const distance = Math.sqrt(
Math.pow(enemy.x - (boulder.x + boulder.width/2), 2) +
Math.pow(enemy.y - (boulder.y + boulder.height/2), 2)
);
if (distance < 50) {
this._enemies.splice(enemyIndex, 1);
this._addParticles(enemy.x, enemy.y, '#FFD700');
console.log(`💥 Boulder explosion killed enemy at distance ${distance.toFixed(0)}`);
}
});
// Note: Mario is NOT killed by boulder landing explosion
// Only flying boulders kill Mario (handled above)
// Boulder lands only on platforms, ground, stairs - NOT on walls or other boulders (they're destroyed on impact)
if (hitObject && (hitType === 'platform' || hitType === 'stair')) {
console.log(`🪨 Boulder landed on ${hitType}, creating additional platform`);
}
console.log(`🪨 Boulder landed at x=${impactX.toFixed(0)}, y=${impactY.toFixed(0)} - now a platform!`);
}
_updateStones() {
this._stones = Catapult.updateStones(
this._stones, this._mario, this._platforms,
(stone, idx, type) => {
if (type === 'mario') this._restartLevel();
}
);
}
_renderStones() {
this._stones.forEach(stone => {
// Draw small stone as a circle
this._ctx.fillStyle = stone.color;
this._ctx.beginPath();
this._ctx.arc(stone.x + stone.width/2, stone.y + stone.height/2, stone.width/2, 0, 2 * Math.PI);
this._ctx.fill();
// Add small highlight for 3D effect
this._ctx.fillStyle = '#D2B48C'; // Light brown highlight
this._ctx.beginPath();
this._ctx.arc(stone.x + stone.width/2 - 1, stone.y + stone.height/2 - 1, stone.width/4, 0, 2 * Math.PI);
this._ctx.fill();
});
}
_updateCamera() {
// Follow Mario with smooth camera movement
const targetX = this._mario.x - this._config.canvasWidth / 2;
this._camera.x = Math.max(0, Math.min(targetX, this._levelWidth - this._config.canvasWidth));
}
_addParticles(x, y, color) {
for (let i = 0; i < 8; i++) {
this._particles.push({
x: x,
y: y,
velocityX: (Math.random() - 0.5) * 10,
velocityY: -Math.random() * 8 - 2,
color: color,
life: 60,
maxLife: 60
});
}
}
_addSmallParticles(x, y, color) {
// Only 2-3 small particles for walking dust
for (let i = 0; i < 3; i++) {
this._particles.push({
x: x,
y: y,
velocityX: (Math.random() - 0.5) * 4, // Smaller velocity
velocityY: -Math.random() * 3 - 1, // Less upward movement
color: color,
life: 30, // Shorter life
maxLife: 30
});
}
}
_updateParticles() {
this._particles = this._particles.filter(particle => {
particle.x += particle.velocityX;
particle.y += particle.velocityY;
if (particle.isFinishStar) {
// Slower gravity for stars and some sparkle effect
particle.velocityY += 0.1; // Light gravity
particle.velocityX *= 0.98; // Slight friction
} else {
particle.velocityY += 0.3; // Normal gravity
}
particle.life--;
return particle.life > 0;
});
}
_checkLevelCompletion() {
const level = this._levelData[this._currentLevelIndex];
// Debug: log Mario position vs end position
if (this._mario.x > level.endX - 100) { // Close to end
console.log(`🎯 Mario position: ${this._mario.x.toFixed(0)}, Level end: ${level.endX.toFixed(0)}`);
}
if (this._mario.x >= level.endX && !this._levelCompleted) {
console.log(`🏁 Level ${this._currentLevel} completed!`);
this._levelCompleted = true; // Prevent multiple triggers
this._triggerFinishLineAnimation();
}
}
_triggerFinishLineAnimation() {
this._isCelebrating = true; // Block enemies but allow animations
// Create star animation particles
this._createFinishLineStars();
soundSystem.play('finish_stars');
// Complete level after animation
setTimeout(() => {
this._completeLevel();
}, 2000);
}
_createFinishLineStars() {
const centerX = this._mario.x;
const centerY = this._mario.y;
// Create explosion of stars
for (let i = 0; i < 20; i++) {
const angle = (Math.PI * 2 * i) / 20;
const speed = 5 + Math.random() * 5;
const distance = 50 + Math.random() * 100;
this._particles.push({
x: centerX,
y: centerY,
velocityX: Math.cos(angle) * speed,
velocityY: Math.sin(angle) * speed,
life: 2000,
maxLife: 2000,
color: i % 3 === 0 ? '#FFD700' : i % 3 === 1 ? '#FF69B4' : '#00BFFF',
size: 8 + Math.random() * 4,
isFinishStar: true
});
}
console.log('🌟 Finish line star animation triggered!');
}
_createLevelCompleteStars() {
const centerX = this._mario.x;
const centerY = this._mario.y;
// Create celebration stars around Mario
for (let i = 0; i < 15; i++) {
const angle = (Math.PI * 2 * i) / 15;
const speed = 3 + Math.random() * 4;
this._particles.push({
x: centerX,
y: centerY,
velocityX: Math.cos(angle) * speed,
velocityY: Math.sin(angle) * speed,
life: 2500,
maxLife: 2500,
color: ['#FFD700', '#FF69B4', '#00BFFF', '#32CD32', '#FF4500'][Math.floor(Math.random() * 5)],
size: 6 + Math.random() * 3,
isLevelCompleteStar: true
});
}
console.log('⭐ Level complete star animation created!');
}
_createEpicFinalStars() {
const centerX = this._mario.x;
const centerY = this._mario.y;
// 🌟💥 ULTRA EPIC FINAL EXPLOSION - 8 MASSIVE WAVES! 💥🌟
for (let wave = 0; wave < 8; wave++) {
setTimeout(() => {
// MASSIVE number of stars per wave (80, 100, 120, 140, 160, 180, 200, 220)
const starsInWave = 80 + (wave * 20);
for (let i = 0; i < starsInWave; i++) {
const angle = (Math.PI * 2 * i) / starsInWave;
const speed = 3 + Math.random() * 8 + (wave * 1.5);
const waveOffset = wave * 50; // Much bigger spread
this._particles.push({
x: centerX + (Math.random() - 0.5) * waveOffset,
y: centerY + (Math.random() - 0.5) * waveOffset,
velocityX: Math.cos(angle) * speed,
velocityY: Math.sin(angle) * speed - (Math.random() * 3), // More upward bias
life: 6000 + (wave * 800), // Longer lasting
maxLife: 6000 + (wave * 800),
color: ['#FFD700', '#FF69B4', '#00BFFF', '#32CD32', '#FF4500', '#9400D3', '#FF1493', '#00FF7F', '#FFFF00', '#FF6347', '#98FB98', '#DDA0DD'][Math.floor(Math.random() * 12)],
size: 10 + Math.random() * 8 + (wave * 0.5), // Bigger stars
isEpicFinalStar: true
});
}
// Play sound effects for each wave
soundSystem.play('finish_stars');
// Extra sound effect for bigger waves
if (wave >= 4) {
setTimeout(() => soundSystem.play('powerup'), 100);
}
}, wave * 200); // Faster succession
}
// BONUS MEGA EXPLOSION at the end!
setTimeout(() => {
for (let i = 0; i < 300; i++) { // 300 bonus stars!
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 12;
this._particles.push({
x: centerX + (Math.random() - 0.5) * 200,
y: centerY + (Math.random() - 0.5) * 200,
velocityX: Math.cos(angle) * speed,
velocityY: Math.sin(angle) * speed - Math.random() * 4,
life: 8000,
maxLife: 8000,
color: ['#FFD700', '#FF1493', '#00FFFF', '#FF4500', '#9400D3'][Math.floor(Math.random() * 5)],
size: 15 + Math.random() * 10, // HUGE stars
isEpicFinalStar: true
});
}
soundSystem.play('level_complete');
console.log('🌟💥🎆 MEGA BONUS EXPLOSION! 300 GIANT STARS! 🎆💥🌟');
}, 2000);
console.log('🌟💥🎆 ULTRA EPIC FINAL STAR EXPLOSION ACTIVATED! OVER 1500+ STARS! 🎆💥🌟');
}
_completeLevel() {
// NO SCORE for completing level anymore!
this._isCelebrating = true; // Block enemies but allow animations
// Play level complete sound
soundSystem.play('level_complete');
// Add star animations for every level completion
this._createLevelCompleteStars();
// Show level complete message
this._showLevelCompleteMessage();
if (this._currentLevelIndex < this._levelData.length - 1) {
// Move to next level after delay
setTimeout(() => {
this._isCelebrating = false;
this._startLevel(this._currentLevelIndex + 1);
}, 3000); // Increased delay for star animation
} else {
// Game completed - EPIC FINAL STAR EXPLOSION
this._createEpicFinalStars();
setTimeout(() => {
this._completeGame();
}, 5000); // Longer delay for epic animation
}
}
_showLevelCompleteMessage() {
// Create level complete overlay
const overlay = document.createElement('div');
overlay.id = 'level-complete-overlay';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 150, 0, 0.8)';
overlay.style.display = 'flex';
overlay.style.flexDirection = 'column';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.zIndex = '1000';
overlay.style.color = 'white';
overlay.style.textAlign = 'center';
overlay.style.fontFamily = 'Arial, sans-serif';
const isLastLevel = this._currentLevelIndex >= this._levelData.length - 1;
overlay.innerHTML = `
<h1 style="font-size: 3em; margin: 0; text-shadow: 2px 2px 4px rgba(0,0,0,0.5);">
${isLastLevel ? '🏆 VICTORY!' : '🎉 LEVEL COMPLETE!'}
</h1>
<h2 style="font-size: 2em; margin: 20px 0; text-shadow: 1px 1px 2px rgba(0,0,0,0.5);">
${isLastLevel ? 'You completed all levels!' : `Level ${this._currentLevel} Complete`}
</h2>
<p style="font-size: 1.5em; margin: 10px 0;">Score: ${this._score.toLocaleString()}</p>
<p style="font-size: 1.2em; margin: 10px 0;">Questions Answered: ${this._questionsAnswered}</p>
${!isLastLevel ? '<p style="font-size: 1em; margin: 20px 0; opacity: 0.8;">Next level starting soon...</p>' : ''}
`;
document.body.appendChild(overlay);
// Remove overlay after delay
setTimeout(() => {
const existingOverlay = document.getElementById('level-complete-overlay');
if (existingOverlay) {
existingOverlay.remove();
}
}, isLastLevel ? 5000 : 1800);
}
_completeGame() {
this._isGameOver = true;
// Show victory screen
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 100, 0, 0.9)';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.zIndex = '1000';
overlay.style.color = 'white';
overlay.style.textAlign = 'center';
overlay.style.fontFamily = 'Arial, sans-serif';
const playTime = Math.round((Date.now() - this._gameStartTime) / 1000);
const content = document.createElement('div');
content.innerHTML = `
<h1 style="font-size: 48px; margin-bottom: 20px;">🏰 Congratulations! 🏰</h1>
<h2 style="font-size: 32px; margin-bottom: 30px;">You completed Mario Educational Adventure!</h2>
<div style="font-size: 20px; line-height: 2;">
<p>🏆 Final Score: ${this._score}</p>
<p>📚 Questions Answered: ${this._questionsAnswered}</p>
<p>⏰ Time Played: ${playTime} seconds</p>
<p>🎮 Levels Completed: ${this._config.maxLevels}</p>
</div>
`;
const exitButton = document.createElement('button');
exitButton.textContent = 'Return to Games';
exitButton.style.cssText = `
background: #FF6B6B;
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
margin-top: 30px;
`;
exitButton.addEventListener('click', () => {
document.body.removeChild(overlay);
// Try to go back to game selection or reload page
if (window.history.length > 1) {
window.history.back();
} else {
window.location.reload();
}
});
content.appendChild(exitButton);
overlay.appendChild(content);
document.body.appendChild(overlay);
}
_restartLevel() {
// Reset Mario to start position
const level = this._levelData[this._currentLevelIndex];
this._mario.x = level.startX;
this._mario.y = level.startY;
this._mario.velocityX = 0;
this._mario.velocityY = 0;
this._mario.onGround = false;
// Reset camera
this._camera.x = 0;
this._camera.y = 0;
// Subtract score penalty for death
const penalty = 250;
this._score = Math.max(0, this._score - penalty);
soundSystem.play('death');
console.log(`💀 Mario died! Score penalty: -${penalty}. New score: ${this._score}`);
}
_shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
_getRandomSentence() {
if (this._sentences.length === 0) {
return null;
}
// Get a sentence that hasn't been used yet, or recycle if all used
const availableSentences = this._sentences.filter(
s => !this._usedSentences.includes(s)
);
if (availableSentences.length === 0) {
// All sentences used, reset the used list
this._usedSentences = [];
return this._sentences[Math.floor(Math.random() * this._sentences.length)];
}
const randomSentence = availableSentences[Math.floor(Math.random() * availableSentences.length)];
this._usedSentences.push(randomSentence);
return randomSentence;
}
_updateFlyingEyes() {
FlyingEye.update(this._flyingEyes, this._mario, () => this._restartLevel());
}
_updateBoss() {
Boss.update(this._boss, this._bossTurrets, this._bossMinions, this._mario, this._projectiles, (sound) => soundSystem.play(sound));
}
_render() {
// Build game state object for renderer
const gameState = {
mario: this._mario,
camera: this._camera,
platforms: this._platforms,
questionBlocks: this._questionBlocks,
enemies: this._enemies,
walls: this._walls,
piranhaPlants: this._piranhaPlants,
projectiles: this._projectiles,
catapults: this._catapults,
boulders: this._boulders,
stones: this._stones,
flyingEyes: this._flyingEyes,
boss: this._boss,
castleStructure: this._castleStructure,
finishLine: this._finishLine,
particles: this._particles,
currentLevel: this._currentLevel,
lives: this._lives,
score: this._score,
debugMode: this._debugMode // Toggle with 'D' key during gameplay
};
// Delegate rendering to helper
renderer.render(this._ctx, gameState, this._config);
// Draw mouse target indicator if mouse is pressed
if (this._mousePressed && this._mouseWorldPos.x !== null) {
this._drawMouseTarget();
}
}
_drawMouseTarget() {
const ctx = this._ctx;
const targetX = this._mouseWorldPos.x - this._camera.x;
const targetY = this._mouseWorldPos.y;
// Draw a crosshair at the target position
ctx.save();
ctx.strokeStyle = '#FFD700';
ctx.lineWidth = 2;
ctx.globalAlpha = 0.8;
// Draw circle
ctx.beginPath();
ctx.arc(targetX, targetY, 15, 0, Math.PI * 2);
ctx.stroke();
// Draw crosshair lines
ctx.beginPath();
ctx.moveTo(targetX - 20, targetY);
ctx.lineTo(targetX + 20, targetY);
ctx.moveTo(targetX, targetY - 20);
ctx.lineTo(targetX, targetY + 20);
ctx.stroke();
ctx.restore();
}
/**
* Get available speech synthesis voices, waiting for them to load if necessary
* @returns {Promise<SpeechSynthesisVoice[]>} Array of available voices
* @private
*/
_getVoices() {
return new Promise((resolve) => {
let voices = window.speechSynthesis.getVoices();
// If voices are already loaded, return them immediately
if (voices.length > 0) {
resolve(voices);
return;
}
// Otherwise, wait for voiceschanged event
const voicesChangedHandler = () => {
voices = window.speechSynthesis.getVoices();
if (voices.length > 0) {
window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler);
resolve(voices);
}
};
window.speechSynthesis.addEventListener('voiceschanged', voicesChangedHandler);
// Fallback timeout in case voices never load
setTimeout(() => {
window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler);
resolve(window.speechSynthesis.getVoices());
}, 1000);
});
}
}
export default MarioEducational;