Class_generator/src/games/MarioEducational.js
StillHammer 05142bdfbc Implement comprehensive AI text report/export system
- Add AIReportSystem.js for detailed AI response capture and report generation
- Add AIReportInterface.js UI component for report access and export
- Integrate AI reporting into LLMValidator and SmartPreviewOrchestrator
- Add missing modules to Application.js configuration (unifiedDRS, smartPreviewOrchestrator)
- Create missing content/chapters/sbs.json for book metadata
- Enhance Application.js with debug logging for module loading
- Add multi-format export capabilities (text, HTML, JSON)
- Implement automatic learning insights extraction from AI feedback
- Add session management and performance tracking for AI reports

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 21:24:13 +08:00

3902 lines
154 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Module from '../core/Module.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;
// 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;
// UI elements (need to be declared before seal)
this._uiOverlay = null;
// Sound system
this._audioContext = null;
this._sounds = {};
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 vocab = content?.vocabulary || {};
const texts = content?.texts || [];
const story = content?.story || '';
const vocabCount = Object.keys(vocab).length;
const sentenceCount = sentences.length;
// Count sentences from texts and story
let extraSentenceCount = 0;
if (story && typeof story === 'string') {
extraSentenceCount += story.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
}
if (Array.isArray(texts)) {
texts.forEach(text => {
if (typeof text === 'string') {
extraSentenceCount += text.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
} else if (text.content) {
extraSentenceCount += text.content.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
}
});
}
const totalSentences = sentenceCount + extraSentenceCount;
if (totalSentences < 3 && vocabCount < 10) {
return {
score: 0,
reason: `Insufficient content (${totalSentences} sentences, ${vocabCount} vocabulary words)`,
requirements: ['sentences', 'vocabulary', 'texts', 'story'],
minSentences: 3,
minVocabulary: 10,
details: 'Mario Educational needs at least 3 sentences OR 10+ vocabulary words'
};
}
if (vocabCount < 5) {
return {
score: 0.3,
reason: `Limited vocabulary (${vocabCount}/5 minimum)`,
requirements: ['sentences', 'vocabulary'],
minWords: 5,
details: 'Game can work with sentences but vocabulary enhances learning'
};
}
// Perfect score at 30+ sentences, good score for 10+
const score = Math.min((totalSentences + vocabCount) / 50, 1);
return {
score,
reason: `${totalSentences} sentences and ${vocabCount} vocabulary words available`,
requirements: ['sentences', 'vocabulary', 'texts', 'story'],
minSentences: 3,
optimalSentences: 30,
details: `Can create ${Math.min(totalSentences + vocabCount, 50)} question blocks from all content 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
this._initializeSoundSystem();
// Generate all levels
this._generateAllLevels();
// Start first level
this._startLevel(5); // Start at level 6 (index 5) to continue boss work
// 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);
// 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 vocab = this._content.vocabulary || {};
const texts = this._content.texts || [];
const story = this._content.story || '';
// Combine sentences and vocabulary for questions
this._sentences = [];
// Add actual sentences - handle both formats
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'}`
});
}
});
// Extract sentences from story text
if (story && typeof story === 'string') {
const storySentences = this._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 = this._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 = this._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}`
});
});
}
});
}
// Add vocabulary as contextual sentences
Object.entries(vocab).forEach(([word, data]) => {
if (data.user_language) {
const generatedSentence = this._generateSentenceFromWord(word, data);
this._sentences.push({
type: 'vocabulary',
english: generatedSentence.english,
translation: generatedSentence.translation,
context: data.type || 'vocabulary'
});
}
});
// Shuffle sentences for variety
this._sentences = this._shuffleArray(this._sentences);
console.log(`📝 Extracted ${this._sentences.length} sentences/vocabulary for questions`);
}
/**
* Generate contextual sentences from vocabulary words
* @param {string} word - The vocabulary word
* @param {Object} data - Word data including type and translation
* @returns {Object} Generated sentence with English and translation
*/
/**
* Split long text into individual sentences
* @param {string} text - Long text to split
* @returns {Array} Array of sentences
*/
_splitTextIntoSentences(text) {
if (!text || typeof text !== 'string') return [];
// Clean the text
const cleanText = text.trim();
// Split by sentence-ending punctuation
const sentences = cleanText
.split(/[.!?]+/)
.map(s => s.trim())
.filter(s => s.length > 0)
.map(s => {
// Add period if sentence doesn't end with punctuation
if (!s.match(/[.!?]$/)) {
s += '.';
}
// Capitalize first letter
return s.charAt(0).toUpperCase() + s.slice(1);
});
// Filter out very short sentences (less than 3 words)
return sentences.filter(sentence => sentence.split(' ').length >= 3);
}
_generateSentenceFromWord(word, data) {
const type = data.type || 'noun';
const translation = data.user_language.split('')[0]; // Use first translation
// Simple sentence templates based on word type
const templates = {
'noun': [
`This is a ${word}.`,
`I see a ${word}.`,
`The ${word} is here.`,
`Where is the ${word}?`,
`I need a ${word}.`
],
'adjective': [
`The house is ${word}.`,
`This looks ${word}.`,
`It seems ${word}.`,
`How ${word} it is!`,
`The weather is ${word}.`
],
'verb': [
`I ${word} every day.`,
`Please ${word} this.`,
`Don't ${word} too fast.`,
`Can you ${word}?`,
`Let's ${word} together.`
],
'adverb': [
`He walks ${word}.`,
`She speaks ${word}.`,
`They work ${word}.`,
`Do it ${word}.`,
`Move ${word}.`
],
'preposition': [
`The book is ${word} the table.`,
`Walk ${word} the street.`,
`It's ${word} the house.`,
`Look ${word} the window.`,
`Go ${word} the door.`
]
};
// Get templates for this word type, fallback to noun if type unknown
const typeTemplates = templates[type] || templates['noun'];
const randomTemplate = typeTemplates[Math.floor(Math.random() * typeTemplates.length)];
return {
english: randomTemplate,
translation: `${translation} - ${randomTemplate.replace(word, `**${word}**`)}`
};
}
_setupCanvas() {
this._canvas = document.createElement('canvas');
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;
// Prevent default for game controls
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'Space'].includes(e.code)) {
e.preventDefault();
}
};
this._handleKeyUp = (e) => {
this._keys[e.code] = false;
};
document.addEventListener('keydown', this._handleKeyDown);
document.addEventListener('keyup', this._handleKeyUp);
}
_initializeSoundSystem() {
try {
// Initialize Web Audio Context
this._audioContext = new (window.AudioContext || window.webkitAudioContext)();
console.log('🔊 Sound system initialized');
// Create sound library
this._createSoundLibrary();
} catch (error) {
console.warn('⚠️ Sound system not available:', error);
this._audioContext = null;
}
}
_createSoundLibrary() {
// Sound definitions with parameters for programmatic generation
this._sounds = {
jump: { type: 'sweep', frequency: 330, endFrequency: 600, duration: 0.1 },
coin: { type: 'bell', frequency: 800, duration: 0.3 },
powerup: { type: 'arpeggio', frequencies: [264, 330, 396, 528], duration: 0.6 },
enemy_defeat: { type: 'noise_sweep', frequency: 200, endFrequency: 50, duration: 0.2 },
question_block: { type: 'sparkle', frequency: 600, endFrequency: 1200, duration: 0.4 },
level_complete: { type: 'victory', frequencies: [523, 659, 784, 1047], duration: 1.0 },
death: { type: 'descend', frequency: 300, endFrequency: 100, duration: 0.8 },
finish_stars: { type: 'magical', frequencies: [880, 1100, 1320, 1760], duration: 2.0 }
};
console.log('🎵 Sound library created with', Object.keys(this._sounds).length, 'sounds');
}
_playSound(soundName, volume = 0.3) {
if (!this._audioContext || !this._sounds[soundName]) {
return;
}
try {
const sound = this._sounds[soundName];
const oscillator = this._audioContext.createOscillator();
const gainNode = this._audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this._audioContext.destination);
const currentTime = this._audioContext.currentTime;
const duration = sound.duration;
// Set volume
gainNode.gain.setValueAtTime(0, currentTime);
gainNode.gain.linearRampToValueAtTime(volume, currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + duration);
// Configure sound based on type
switch (sound.type) {
case 'sweep':
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(sound.frequency, currentTime);
oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration);
break;
case 'bell':
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(sound.frequency, currentTime);
oscillator.frequency.exponentialRampToValueAtTime(sound.frequency * 0.5, currentTime + duration);
break;
case 'noise_sweep':
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(sound.frequency, currentTime);
oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration);
break;
case 'sparkle':
oscillator.type = 'triangle';
oscillator.frequency.setValueAtTime(sound.frequency, currentTime);
oscillator.frequency.linearRampToValueAtTime(sound.endFrequency, currentTime + duration * 0.7);
oscillator.frequency.linearRampToValueAtTime(sound.frequency, currentTime + duration);
break;
case 'descend':
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(sound.frequency, currentTime);
oscillator.frequency.exponentialRampToValueAtTime(sound.endFrequency, currentTime + duration);
break;
case 'arpeggio':
case 'victory':
case 'magical':
// For complex sounds, play the first frequency and schedule others
oscillator.type = sound.type === 'magical' ? 'triangle' : 'square';
oscillator.frequency.setValueAtTime(sound.frequencies[0], currentTime);
// Schedule frequency changes for arpeggio effect
const noteLength = duration / sound.frequencies.length;
sound.frequencies.forEach((freq, index) => {
if (index > 0) {
oscillator.frequency.setValueAtTime(freq, currentTime + noteLength * index);
}
});
break;
default:
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(sound.frequency || 440, currentTime);
}
oscillator.start(currentTime);
oscillator.stop(currentTime + duration);
console.log(`🎵 Playing sound: ${soundName}`);
} catch (error) {
console.warn('⚠️ Failed to play sound:', soundName, error);
}
}
_generateAllLevels() {
this._levelData = [];
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);
// 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;
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
});
// Check if enemy has solid ground/platform/stair support below
const hasSolidSupport = this._hasSolidSupportBelow(enemyX, enemyY + 20, level); // Check below enemy position
if (!wouldOverlapWall && !wouldOverlapHole && hasSolidSupport) {
// Level 2+ gets exactly ONE helmet enemy per level
const isHelmetEnemy = index >= 1 && !helmetEnemyPlaced && i === 0; // Only first enemy can be helmet
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;
} else {
console.log(`🚫 Enemy ${i} attempt ${attempts} would overlap wall/hole/unsupported ground, retrying...`);
}
attempts++;
}
if (!enemyPlaced) {
console.log(`⚠️ Failed to place enemy ${i} after ${maxAttempts} attempts`);
}
}
// Level 3+ gets advanced features (except level 6 boss level)
if (index >= 2 && index <= 4) {
this._generateHoles(level, difficulty);
this._generateStairs(level, difficulty);
this._generatePiranhaPlants(level, difficulty);
}
// Level 6 boss level: only stairs (no holes, limited piranha plants)
if (index === 5) {
this._generateBossStairs(level, difficulty);
// Reduced piranha plants for boss level
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
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) {
const plantCount = Math.min(difficulty - 2, 2); // 0-2 plants for level 3+
for (let i = 0; i < plantCount; i++) {
// Find a suitable ground platform for the plant
const groundPlatforms = level.platforms.filter(p => p.type === 'ground');
if (groundPlatforms.length === 0) continue;
const platform = groundPlatforms[Math.floor(Math.random() * groundPlatforms.length)];
const plantX = platform.x + Math.random() * (platform.width - 30);
level.piranhaPlants = level.piranhaPlants || [];
level.piranhaPlants.push({
x: plantX,
y: platform.y - 40, // Plant height
width: 30,
height: 40,
color: '#228B22', // Forest green
lastShot: 0,
shootCooldown: 2000 + Math.random() * 1000, // 2-3 second intervals
type: 'piranha'
});
console.log(`🌸 Piranha plant placed at x=${plantX.toFixed(0)}`);
}
}
_generateCatapults(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;
// 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() {
// Don't update movement during celebration
if (this._isCelebrating) return;
// Horizontal movement
if (this._keys['ArrowLeft'] || this._keys['KeyA']) {
this._mario.velocityX = -this._config.moveSpeed;
this._mario.facing = 'left';
} else if (this._keys['ArrowRight'] || this._keys['KeyD']) {
this._mario.velocityX = this._config.moveSpeed;
this._mario.facing = 'right';
} else {
this._mario.velocityX *= 0.8; // Friction
}
// Jumping
if ((this._keys['ArrowUp'] || this._keys['KeyW'] || this._keys['Space']) && this._mario.onGround) {
this._mario.velocityY = this._config.jumpForce;
this._mario.onGround = false;
this._playSound('jump');
}
}
_updateMarioPhysics() {
// Apply gravity
this._mario.velocityY += this._config.gravity;
// Update position
this._mario.x += this._mario.velocityX;
this._mario.y += this._mario.velocityY;
// Prevent going off left edge
if (this._mario.x < 0) {
this._mario.x = 0;
}
// Stop Mario at finish line during celebration
const level = this._levelData[this._currentLevelIndex];
if (this._mario.x > level.endX && this._levelCompleted) {
this._mario.x = level.endX;
this._mario.velocityX = 0;
}
// Check if Mario fell off the world
if (this._mario.y > this._config.canvasHeight + 100) {
this._restartLevel();
}
}
_updateEnemies() {
// Don't update enemies during celebration
if (this._isCelebrating) return;
this._enemies.forEach(enemy => {
// Store old position for collision detection
const oldX = enemy.x;
enemy.x += enemy.velocityX;
// Check wall collisions
const hitWall = this._walls.some(wall => {
return enemy.x < wall.x + wall.width &&
enemy.x + enemy.width > wall.x &&
enemy.y < wall.y + wall.height &&
enemy.y + enemy.height > wall.y;
});
if (hitWall) {
// Reverse position and direction
enemy.x = oldX;
enemy.velocityX *= -1;
console.log(`🧱 Enemy hit wall, reversing direction`);
}
// Simple AI: reverse direction at platform edges
const platform = this._platforms.find(p =>
enemy.x >= p.x - 10 && enemy.x <= p.x + p.width + 10 &&
enemy.y >= p.y - enemy.height - 5 && enemy.y <= p.y + 5
);
if (!platform || enemy.x <= 0 || enemy.x >= this._levelWidth) {
enemy.velocityX *= -1;
}
});
}
_checkCollisions() {
// Platform collisions
this._mario.onGround = false;
this._platforms.forEach(platform => {
if (this._isColliding(this._mario, platform)) {
// Calculate overlap amounts to determine collision direction
const overlapLeft = (this._mario.x + this._mario.width) - platform.x;
const overlapRight = (platform.x + platform.width) - this._mario.x;
const overlapTop = (this._mario.y + this._mario.height) - platform.y;
const overlapBottom = (platform.y + platform.height) - this._mario.y;
// Find the smallest overlap to determine collision side
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if (minOverlap === overlapTop && this._mario.velocityY > 0) {
// Landing on top of platform
this._mario.y = platform.y - this._mario.height;
this._mario.velocityY = 0;
this._mario.onGround = true;
}
else if (minOverlap === overlapBottom && this._mario.velocityY < 0) {
// Hitting platform from below
this._mario.y = platform.y + platform.height;
this._mario.velocityY = 0;
}
else if (minOverlap === overlapLeft && this._mario.velocityX > 0) {
// Hitting platform from left
this._mario.x = platform.x - this._mario.width;
this._mario.velocityX = 0;
}
else if (minOverlap === overlapRight && this._mario.velocityX < 0) {
// Hitting platform from right
this._mario.x = platform.x + platform.width;
this._mario.velocityX = 0;
}
}
});
// Boulder platform collisions (landed boulders act as platforms)
this._boulders.forEach(boulder => {
if (boulder.hasLanded && this._isColliding(this._mario, boulder)) {
// Same collision logic as platforms
const overlapLeft = (this._mario.x + this._mario.width) - boulder.x;
const overlapRight = (boulder.x + boulder.width) - this._mario.x;
const overlapTop = (this._mario.y + this._mario.height) - boulder.y;
const overlapBottom = (boulder.y + boulder.height) - this._mario.y;
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if (minOverlap === overlapTop && this._mario.velocityY > 0) {
// Landing on top of boulder
this._mario.y = boulder.y - this._mario.height;
this._mario.velocityY = 0;
this._mario.onGround = true;
}
else if (minOverlap === overlapBottom && this._mario.velocityY < 0) {
// Hitting boulder from below
this._mario.y = boulder.y + boulder.height;
this._mario.velocityY = 0;
}
else if (minOverlap === overlapLeft && this._mario.velocityX > 0) {
// Hitting boulder from left
this._mario.x = boulder.x - this._mario.width;
this._mario.velocityX = 0;
}
else if (minOverlap === overlapRight && this._mario.velocityX < 0) {
// Hitting boulder from right
this._mario.x = boulder.x + boulder.width;
this._mario.velocityX = 0;
}
}
});
// Question block collisions
this._questionBlocks.forEach(block => {
if (!block.hit && this._isColliding(this._mario, block)) {
// Hit from below (jumping into block)
if (this._mario.velocityY < 0 &&
this._mario.y > block.y + block.height / 2) {
this._hitQuestionBlock(block);
}
// Touch from side or top
else {
this._hitQuestionBlock(block);
}
}
});
// Wall collisions
this._walls.forEach(wall => {
if (this._isColliding(this._mario, wall)) {
// Calculate overlap amounts to determine collision direction
const overlapLeft = (this._mario.x + this._mario.width) - wall.x;
const overlapRight = (wall.x + wall.width) - this._mario.x;
const overlapTop = (this._mario.y + this._mario.height) - wall.y;
const overlapBottom = (wall.y + wall.height) - this._mario.y;
// Find the smallest overlap to determine collision side
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if (minOverlap === overlapTop && this._mario.velocityY > 0) {
// Landing on top of wall
this._mario.y = wall.y - this._mario.height;
this._mario.velocityY = 0;
this._mario.onGround = true;
}
else if (minOverlap === overlapBottom && this._mario.velocityY < 0) {
// Hitting wall from below
this._mario.y = wall.y + wall.height;
this._mario.velocityY = 0;
}
else if (minOverlap === overlapLeft && this._mario.velocityX > 0) {
// Hitting wall from left
this._mario.x = wall.x - this._mario.width;
this._mario.velocityX = 0;
}
else if (minOverlap === overlapRight && this._mario.velocityX < 0) {
// Hitting wall from right
this._mario.x = wall.x + wall.width;
this._mario.velocityX = 0;
}
}
});
// Catapult collisions (Mario destroys catapults!)
this._catapults.forEach((catapult, index) => {
if (this._isColliding(this._mario, catapult)) {
console.log(`💥 Mario destroyed catapult! Explosion!`);
// Create massive explosion effect at catapult center
const explosionX = catapult.x + catapult.width / 2;
const explosionY = catapult.y + catapult.height / 2;
// Generate lots of particles flying in all directions
for (let i = 0; i < 25; i++) {
const angle = (Math.PI * 2 * i) / 25; // Spread particles in circle
const speed = 3 + Math.random() * 4; // Random speed 3-7
const particleColor = ['#8B4513', '#654321', '#D2691E', '#FF4500', '#FFD700'][Math.floor(Math.random() * 5)];
this._particles.push({
x: explosionX,
y: explosionY,
velocityX: Math.cos(angle) * speed,
velocityY: Math.sin(angle) * speed,
color: particleColor,
life: 60 + Math.random() * 30, // Live 60-90 frames
maxLife: 60 + Math.random() * 30,
size: 3 + Math.random() * 4 // Size 3-7
});
}
// Remove the destroyed catapult
this._catapults.splice(index, 1);
// Play destruction sound and bounce Mario up slightly
this._playSound('enemy_defeat');
this._mario.velocityY = this._config.jumpForce * 0.5; // Small bounce from impact
console.log(`🏹 Catapult destroyed! ${this._catapults.length} catapults remaining.`);
}
});
// Enemy collisions
this._enemies.forEach((enemy, index) => {
if (this._isColliding(this._mario, enemy)) {
// Calculate overlap to determine if Mario is stomping enemy
const overlapTop = (this._mario.y + this._mario.height) - enemy.y;
const overlapBottom = (enemy.y + enemy.height) - this._mario.y;
// Stomp enemy if Mario is coming from above
if (this._mario.velocityY > 0 && overlapTop < overlapBottom) {
if (enemy.hasHelmet) {
// Can't stomp helmet enemies! Mario bounces off
this._mario.velocityY = this._config.jumpForce * 0.7; // Big bounce back
this._addParticles(enemy.x, enemy.y, '#C0C0C0'); // Silver particles
this._playSound('jump'); // Bounce sound
console.log(`🛡️ Mario bounced off helmet enemy!`);
} else {
// Normal enemy - can be stomped
this._enemies.splice(index, 1);
this._mario.velocityY = this._config.jumpForce / 2; // Small bounce
// NO SCORE for killing enemies anymore!
this._addParticles(enemy.x, enemy.y, '#FFD700');
this._playSound('enemy_defeat');
console.log(`🦶 Mario stomped normal enemy! (No score)`);
}
} else {
// Mario gets hurt by side/bottom collision
console.log(`💥 Mario hit by enemy - restarting level`);
this._restartLevel();
}
}
});
// Check piranha plant collisions (skip flattened plants)
this._piranhaPlants.forEach((plant, index) => {
if (!plant.flattened && this._isColliding(this._mario, plant)) {
// Calculate overlap to determine if Mario is stomping plant
const overlapTop = (this._mario.y + this._mario.height) - plant.y;
const overlapBottom = (plant.y + plant.height) - this._mario.y;
// Stomp plant if Mario is coming from above
if (this._mario.velocityY > 0 && overlapTop < overlapBottom) {
// Flatten the plant instead of removing it
plant.flattened = true;
plant.height = 5; // Very flat
plant.y += 35; // Move to ground level (original height was 40, new is 5)
plant.shootCooldown = Infinity; // Stop shooting
this._mario.velocityY = this._config.jumpForce / 2; // Small bounce
this._addParticles(plant.x, plant.y, '#228B22'); // Green particles
this._playSound('enemy_defeat');
console.log(`🌸 Mario flattened piranha plant!`);
} else {
// Mario gets hurt by side collision
console.log(`💥 Mario hit by piranha plant - restarting level`);
this._restartLevel();
}
}
});
// Check walking on flattened plants (for particles)
this._piranhaPlants.forEach((plant, index) => {
if (plant.flattened && this._isColliding(this._mario, plant)) {
// Mario is standing on a flattened plant - add particles occasionally
if (Math.random() < 0.1) { // 10% chance per frame for particles
this._addSmallParticles(plant.x + Math.random() * plant.width, plant.y, '#8B4513'); // Brown dust particles
}
}
});
}
_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;
}
_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');
this._playSound('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);
}
_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;
// Try to use a nice English voice
const voices = speechSynthesis.getVoices();
const englishVoice = voices.find(voice =>
voice.lang.startsWith('en') && (voice.name.includes('Female') || voice.name.includes('Google'))
) || voices.find(voice => voice.lang.startsWith('en'));
if (englishVoice) {
utterance.voice = englishVoice;
console.log(`🎤 Using voice: ${englishVoice.name}`);
}
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() {
const currentTime = Date.now();
this._piranhaPlants.forEach(plant => {
// Check if it's time to shoot
if (currentTime - plant.lastShot > plant.shootCooldown) {
// Check if Mario is in range (within 400 pixels)
const distanceToMario = Math.abs(plant.x - this._mario.x);
if (distanceToMario < 400) {
// Shoot projectile towards Mario
const direction = this._mario.x > plant.x ? 1 : -1;
this._projectiles.push({
x: plant.x + plant.width / 2,
y: plant.y + plant.height / 2,
velocityX: direction * 3, // Projectile speed
velocityY: 0,
width: 8,
height: 8,
color: '#FF4500', // Orange fireball
type: 'fireball',
life: 200 // 200 frames lifetime
});
plant.lastShot = currentTime;
this._playSound('enemy_defeat'); // Shooting sound
console.log(`🔥 Piranha plant shot fireball towards Mario!`);
}
}
});
}
_updateProjectiles() {
// Update projectile positions
this._projectiles.forEach((projectile, index) => {
projectile.x += projectile.velocityX;
projectile.y += projectile.velocityY;
projectile.life--;
// Remove projectiles that are off-screen or expired
if (projectile.life <= 0 || projectile.x < -50 || projectile.x > this._levelWidth + 50) {
this._projectiles.splice(index, 1);
return;
}
// Check collision with Mario
if (this._isColliding(this._mario, projectile)) {
console.log(`🔥 Mario hit by projectile - restarting level`);
this._restartLevel();
return;
}
// Check collision with walls/platforms
const hitObstacle = this._platforms.some(platform => this._isColliding(projectile, platform)) ||
this._walls.some(wall => this._isColliding(projectile, wall));
if (hitObstacle) {
this._projectiles.splice(index, 1);
this._addParticles(projectile.x, projectile.y, '#FF4500');
console.log(`💥 Projectile hit obstacle`);
}
});
}
_updateCatapults() {
const currentTime = Date.now();
this._catapults.forEach(catapult => {
// Check if it's time to shoot
if (currentTime - catapult.lastShot > catapult.shootCooldown) {
// Target Mario's position with imperfect aim (randomness)
const aimOffset = 100 + Math.random() * 150; // 100-250 pixel spread
const aimDirection = Math.random() < 0.5 ? -1 : 1;
const targetX = this._mario.x + (aimOffset * aimDirection);
const targetY = this._config.canvasHeight - 50; // Ground level
// ONAGER ONLY: Check minimum range - don't fire if Mario is too close
if (catapult.isOnager) {
const distanceToMario = Math.abs(catapult.x - this._mario.x);
const minimumRange = 300; // Onager won't fire if Mario is within 300px
if (distanceToMario < minimumRange) {
console.log(`🏛️ Onager held fire - Mario too close! Distance: ${distanceToMario.toFixed(0)}px (min: ${minimumRange}px)`);
return; // Skip this shot, Mario is too close
}
}
// Check if there's a clear line of sight (no platform blocking)
if (!this._hasLineOfSight(catapult, targetX, targetY)) {
console.log(`🚫 ${catapult.type} blocked by obstacle, skipping shot`);
return; // Skip this shot, try again next time
}
if (catapult.isOnager) {
// ONAGER: Fire 8 small stones in spread pattern
console.log(`🏛️ Onager firing stone rain!`);
for (let stone = 0; stone < 8; stone++) {
// Much more random targeting for fear factor
const randomSpreadX = (Math.random() - 0.5) * 400; // ±200px spread
const randomSpreadY = (Math.random() - 0.5) * 100; // ±50px spread
const stoneTargetX = targetX + randomSpreadX;
const stoneTargetY = targetY + randomSpreadY;
// Calculate trajectory for small stone
const deltaX = stoneTargetX - catapult.x;
const deltaY = stoneTargetY - catapult.y;
const time = 5 + Math.random() * 2; // 5-7 seconds flight time (varied)
const velocityX = deltaX / (time * 60);
const velocityY = (deltaY - 0.5 * 0.015 * time * time * 60 * 60) / (time * 60); // Slightly more gravity
this._stones.push({
x: catapult.x + 30 + (Math.random() - 0.5) * 20, // Spread launch point
y: catapult.y - 10 + (Math.random() - 0.5) * 10,
width: 8, // Much smaller than boulders
height: 8,
velocityX: velocityX,
velocityY: velocityY,
color: '#A0522D', // Brown stone color
type: 'stone',
sourceCatapultX: catapult.x,
sourceCatapultY: catapult.y
});
}
catapult.lastShot = currentTime;
this._playSound('enemy_defeat'); // Different sound for stone rain
console.log(`🏛️ Onager fired 8 stones in spread pattern!`);
} else {
// CATAPULT: Fire single boulder (original behavior)
const deltaX = targetX - catapult.x;
const deltaY = targetY - catapult.y;
const time = 7.5; // 7.5 seconds flight time
const velocityX = deltaX / (time * 60);
const velocityY = (deltaY - 0.5 * 0.01 * time * time * 60 * 60) / (time * 60);
this._boulders.push({
x: catapult.x + 30,
y: catapult.y - 10,
width: 25,
height: 25,
velocityX: velocityX,
velocityY: velocityY,
color: '#696969', // Dark gray
type: 'boulder',
hasLanded: false,
sourceCatapultX: catapult.x,
sourceCatapultY: catapult.y,
health: 2,
maxHealth: 2
});
catapult.lastShot = currentTime;
this._playSound('jump'); // Boulder launch sound
console.log(`🏹 Catapult fired boulder towards x=${targetX.toFixed(0)}`);
}
}
});
}
_updateBoulders() {
this._boulders.forEach((boulder, index) => {
if (boulder.hasLanded) return; // Don't update landed boulders
// Apply gravity and movement (MUCH slower with very light gravity)
boulder.velocityY += 0.01; // Ultra-light gravity so boulders can actually fly properly
boulder.x += boulder.velocityX;
boulder.y += boulder.velocityY;
// Check ground collision
const groundLevel = this._config.canvasHeight - 50;
if (boulder.y + boulder.height >= groundLevel) {
this._handleBoulderImpact(boulder, index, boulder.x, groundLevel - boulder.height);
return;
}
// Check collision with platforms, walls, stairs
let hasHit = false;
// Check platforms
this._platforms.forEach((platform, platformIndex) => {
if (!hasHit && this._isColliding(boulder, platform)) {
this._handleBoulderImpact(boulder, index, boulder.x, platform.y - boulder.height, platform, platformIndex);
hasHit = true;
}
});
// Check walls (boulder damages walls)
if (!hasHit) {
this._walls.forEach((wall, wallIndex) => {
if (!hasHit && this._isColliding(boulder, wall)) {
console.log(`🪨 Boulder hit wall! Wall damage system activated.`);
// Boulder is DESTROYED when hitting wall
this._addParticles(boulder.x + boulder.width/2, boulder.y + boulder.height/2, '#696969');
this._boulders.splice(index, 1);
this._playSound('enemy_defeat');
console.log(`💥 Boulder destroyed on wall impact`);
// Wall takes damage
wall.health--;
console.log(`🧱 Wall health: ${wall.health}/${wall.maxHealth}`);
if (wall.health <= 0) {
// Wall is destroyed
this._addParticles(wall.x + wall.width/2, wall.y + wall.height/2, '#8B4513');
this._walls.splice(wallIndex, 1);
console.log(`💥 Wall destroyed!`);
} else {
// Visual damage - change color to show damage
if (wall.health === 2) {
wall.color = '#A0522D'; // Slightly damaged - darker brown
} else if (wall.health === 1) {
wall.color = '#654321'; // Heavily damaged - dark brown
}
console.log(`🩹 Wall damaged but still standing`);
}
hasHit = true;
}
});
}
// Check collision with catapults (but NOT the source catapult)
if (!hasHit) {
this._catapults.forEach((catapult, catapultIndex) => {
// Don't collide with the catapult that fired this boulder
const isSameCatapult = Math.abs(boulder.sourceCatapultX - catapult.x) < 10 &&
Math.abs(boulder.sourceCatapultY - catapult.y) < 10;
if (!hasHit && !isSameCatapult && this._isColliding(boulder, catapult)) {
// Boulder hits a different catapult - land on top
this._handleBoulderImpact(boulder, index, boulder.x, catapult.y - boulder.height, catapult, catapultIndex, 'catapult');
hasHit = true;
console.log(`🪨 Boulder hit different catapult!`);
}
});
}
// Check collision with Mario (boulder resets level like enemies!)
if (!hasHit && this._isColliding(boulder, this._mario)) {
console.log(`💀 Boulder hit Mario! Respawning at level start.`);
this._addParticles(this._mario.x, this._mario.y, '#FF0000'); // Red death particles
this._playSound('enemy_defeat');
// Reset Mario to start position like other enemy deaths
const level = this._levelData[this._currentLevelIndex];
this._mario.x = level.startX;
this._mario.y = level.startY;
this._mario.velocityX = 0;
this._mario.velocityY = 0;
// Boulder lands after hitting Mario
this._handleBoulderImpact(boulder, index, boulder.x, this._config.canvasHeight - 75);
hasHit = true;
return;
}
// Check collision with OTHER boulders (boulder-to-boulder damage system)
if (!hasHit) {
this._boulders.forEach((otherBoulder, otherIndex) => {
if (!hasHit && otherIndex !== index && otherBoulder.hasLanded && this._isColliding(boulder, otherBoulder)) {
console.log(`🪨 Flying boulder hit landed boulder! Damage system activated.`);
// Flying boulder is DESTROYED (removed completely)
this._addParticles(boulder.x + boulder.width/2, boulder.y + boulder.height/2, '#696969');
this._boulders.splice(index, 1);
this._playSound('enemy_defeat');
console.log(`💥 Flying boulder destroyed on impact`);
// Landed boulder takes damage
otherBoulder.health--;
console.log(`🩹 Landed boulder health: ${otherBoulder.health}/${otherBoulder.maxHealth}`);
if (otherBoulder.health <= 0) {
// Landed boulder is destroyed
this._addParticles(otherBoulder.x + otherBoulder.width/2, otherBoulder.y + otherBoulder.height/2, '#8B4513');
this._boulders.splice(otherIndex, 1);
console.log(`💥 Landed boulder destroyed!`);
} else {
// Visual damage - change color to show damage
otherBoulder.color = otherBoulder.health === 1 ? '#A0522D' : '#696969'; // Darker when damaged
}
hasHit = true;
}
});
}
// Remove boulders that go off-screen
if (boulder.x < -100 || boulder.x > this._levelWidth + 100 || boulder.y > this._config.canvasHeight + 100) {
this._boulders.splice(index, 1);
}
});
}
_handleBoulderImpact(boulder, boulderIndex, impactX, impactY, hitObject = null, hitObjectIndex = -1, hitType = 'platform') {
// 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');
this._playSound('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)}`);
}
});
// Reset Mario to level start if he's in explosion radius (50 pixels)
const marioDistance = Math.sqrt(
Math.pow(this._mario.x - (boulder.x + boulder.width/2), 2) +
Math.pow(this._mario.y - (boulder.y + boulder.height/2), 2)
);
if (marioDistance < 50) {
console.log(`💀 Mario killed by boulder explosion! Respawning at level start. Distance: ${marioDistance.toFixed(0)}`);
this._addParticles(this._mario.x, this._mario.y, '#FF0000'); // Red death particles
this._playSound('enemy_defeat');
// Reset Mario to start position like other enemy deaths
const level = this._levelData[this._currentLevelIndex];
this._mario.x = level.startX;
this._mario.y = level.startY;
this._mario.velocityX = 0;
this._mario.velocityY = 0;
}
// Boulder lands 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.forEach((stone, index) => {
// Apply gravity and movement (similar to boulders but simpler)
stone.velocityY += 0.015; // Slightly more gravity than boulders
stone.x += stone.velocityX;
stone.y += stone.velocityY;
// Check ground collision
const groundLevel = this._config.canvasHeight - 50;
if (stone.y + stone.height >= groundLevel) {
// Stone hits ground - create small particle effect and disappear
this._addParticles(stone.x + stone.width/2, stone.y + stone.height/2, '#A0522D');
this._stones.splice(index, 1);
return;
}
// Check collision with Mario (stones kill Mario!)
if (this._isColliding(stone, this._mario)) {
console.log(`🪨 Stone hit Mario! Respawning at level start.`);
this._addParticles(this._mario.x, this._mario.y, '#FF0000'); // Red death particles
this._playSound('enemy_defeat');
// Reset Mario to start position like other deaths
const level = this._levelData[this._currentLevelIndex];
this._mario.x = level.startX;
this._mario.y = level.startY;
this._mario.velocityX = 0;
this._mario.velocityY = 0;
// Remove the stone that hit Mario
this._stones.splice(index, 1);
return;
}
// Check collision with platforms/walls (stones disappear on impact)
let hasHit = false;
// Check platforms
this._platforms.forEach((platform) => {
if (!hasHit && this._isColliding(stone, platform)) {
this._addParticles(stone.x + stone.width/2, stone.y + stone.height/2, '#A0522D');
this._stones.splice(index, 1);
hasHit = true;
return;
}
});
// Check walls
if (!hasHit) {
this._walls.forEach((wall) => {
if (!hasHit && this._isColliding(stone, wall)) {
this._addParticles(stone.x + stone.width/2, stone.y + stone.height/2, '#A0522D');
this._stones.splice(index, 1);
hasHit = true;
return;
}
});
}
// Remove stones that go off-screen
if (stone.x < -100 || stone.x > this._levelWidth + 100 || stone.y > this._config.canvasHeight + 100) {
this._stones.splice(index, 1);
}
});
}
_renderStones() {
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();
this._playSound('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
this._playSound('finish_stars');
// Extra sound effect for bigger waves
if (wave >= 4) {
setTimeout(() => this._playSound('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
});
}
this._playSound('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
this._playSound('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);
this._playSound('death');
console.log(`💀 Mario died! Score penalty: -${penalty}. New score: ${this._score}`);
}
_render() {
// Clear canvas
this._ctx.clearRect(0, 0, this._config.canvasWidth, this._config.canvasHeight);
// Render background
this._renderBackground();
// Save context for camera translation
this._ctx.save();
this._ctx.translate(-this._camera.x, -this._camera.y);
// Render platforms
this._renderPlatforms();
// Render question blocks
this._renderQuestionBlocks();
// Render enemies
this._renderEnemies();
// Render walls
this._renderWalls();
// Render advanced level elements
this._renderPiranhaPlants();
this._renderProjectiles();
// Render level 4+ elements
this._renderCatapults();
this._renderBoulders();
this._renderStones();
// Render level 5+ elements
this._renderFlyingEyes();
// Render level 6 boss elements
this._renderBoss();
// Render castle (visual only)
this._renderCastle();
// Render finish line
this._renderFinishLine();
// Render Mario
this._renderMario();
// Render particles
this._renderParticles();
// Render debug hitboxes if needed
this._renderDebugHitboxes();
// Restore context
this._ctx.restore();
// Render UI
this._renderUI();
}
_renderBackground() {
// Sky gradient
const gradient = this._ctx.createLinearGradient(0, 0, 0, this._config.canvasHeight);
gradient.addColorStop(0, '#87CEEB');
gradient.addColorStop(1, '#98FB98');
this._ctx.fillStyle = gradient;
this._ctx.fillRect(0, 0, this._config.canvasWidth, this._config.canvasHeight);
// Clouds (fixed position, don't scroll with camera)
this._ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
for (let i = 0; i < 5; i++) {
const x = (i * 300 + 100 - this._camera.x * 0.3) % (this._config.canvasWidth + 200);
const y = 80 + i * 30;
this._renderCloud(x, y);
}
}
_renderCloud(x, y) {
this._ctx.beginPath();
this._ctx.arc(x, y, 30, 0, Math.PI * 2);
this._ctx.arc(x + 30, y, 40, 0, Math.PI * 2);
this._ctx.arc(x + 60, y, 30, 0, Math.PI * 2);
this._ctx.fill();
}
_renderPlatforms() {
this._platforms.forEach(platform => {
this._ctx.fillStyle = platform.color;
this._ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
// Add outline
this._ctx.strokeStyle = '#000';
this._ctx.lineWidth = 2;
this._ctx.strokeRect(platform.x, platform.y, platform.width, platform.height);
});
}
_renderQuestionBlocks() {
this._questionBlocks.forEach(block => {
// Block
this._ctx.fillStyle = block.color;
this._ctx.fillRect(block.x, block.y, block.width, block.height);
// Outline
this._ctx.strokeStyle = '#000';
this._ctx.lineWidth = 2;
this._ctx.strokeRect(block.x, block.y, block.width, block.height);
// Symbol
this._ctx.fillStyle = '#000';
this._ctx.font = 'bold 24px Arial';
this._ctx.textAlign = 'center';
this._ctx.textBaseline = 'middle';
this._ctx.fillText(
block.symbol,
block.x + block.width / 2,
block.y + block.height / 2
);
});
}
_renderEnemies() {
this._enemies.forEach(enemy => {
this._ctx.fillStyle = enemy.color;
this._ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
// Simple face
this._ctx.fillStyle = '#FFF';
this._ctx.fillRect(enemy.x + 3, enemy.y + 3, 3, 3); // Eye
this._ctx.fillRect(enemy.x + 14, enemy.y + 3, 3, 3); // Eye
// Draw helmet for protected enemies
if (enemy.hasHelmet) {
this._ctx.fillStyle = '#C0C0C0'; // Silver helmet
this._ctx.fillRect(enemy.x - 1, enemy.y - 4, enemy.width + 2, 6);
// Helmet shine
this._ctx.fillStyle = '#E8E8E8';
this._ctx.fillRect(enemy.x + 2, enemy.y - 3, 4, 2);
// Helmet symbol (shield)
this._ctx.fillStyle = '#FFD700';
this._ctx.fillRect(enemy.x + 8, enemy.y - 2, 4, 2);
}
});
}
_renderWalls() {
this._walls.forEach(wall => {
// Wall main body
this._ctx.fillStyle = wall.color;
this._ctx.fillRect(wall.x, wall.y, wall.width, wall.height);
// Add brick pattern for visual appeal
this._ctx.strokeStyle = '#654321'; // Darker brown for lines
this._ctx.lineWidth = 1;
// Horizontal lines
for (let y = wall.y + 20; y < wall.y + wall.height; y += 20) {
this._ctx.beginPath();
this._ctx.moveTo(wall.x, y);
this._ctx.lineTo(wall.x + wall.width, y);
this._ctx.stroke();
}
// Vertical lines (offset every other row)
for (let row = 0; row < Math.floor(wall.height / 20); row++) {
const yPos = wall.y + row * 20;
const offset = (row % 2) * 10; // Offset every other row
for (let x = wall.x + offset + 20; x < wall.x + wall.width; x += 20) {
this._ctx.beginPath();
this._ctx.moveTo(x, yPos);
this._ctx.lineTo(x, Math.min(yPos + 20, wall.y + wall.height));
this._ctx.stroke();
}
}
// Wall type indicator (for debugging)
if (wall.type === 'tall') {
this._ctx.fillStyle = '#FF0000';
this._ctx.fillRect(wall.x + wall.width - 5, wall.y, 5, 20);
}
});
}
_renderPiranhaPlants() {
this._piranhaPlants.forEach(plant => {
if (plant.flattened) {
// Flattened plant - just a green pancake on the ground
this._ctx.fillStyle = '#556B2F'; // Dark olive green (dead/flat)
this._ctx.fillRect(plant.x, plant.y + plant.height - 5, plant.width, 5);
// Some scattered debris
this._ctx.fillStyle = '#8B4513'; // Brown debris
this._ctx.fillRect(plant.x + 5, plant.y + plant.height - 3, 3, 2);
this._ctx.fillRect(plant.x + 20, plant.y + plant.height - 4, 2, 3);
this._ctx.fillRect(plant.x + 12, plant.y + plant.height - 2, 4, 1);
} else {
// Normal living plant
// Plant stem
this._ctx.fillStyle = '#228B22'; // Forest green
this._ctx.fillRect(plant.x + 10, plant.y, 10, plant.height);
// Plant head (circular)
this._ctx.fillStyle = '#32CD32'; // Lime green
this._ctx.beginPath();
this._ctx.arc(plant.x + 15, plant.y + 10, 12, 0, Math.PI * 2);
this._ctx.fill();
// Sharp teeth
this._ctx.fillStyle = '#FFFFFF';
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI * 2) / 6;
const toothX = plant.x + 15 + Math.cos(angle) * 8;
const toothY = plant.y + 10 + Math.sin(angle) * 8;
this._ctx.fillRect(toothX - 1, toothY - 1, 2, 4);
}
// Red mouth center
this._ctx.fillStyle = '#DC143C'; // Crimson
this._ctx.beginPath();
this._ctx.arc(plant.x + 15, plant.y + 10, 6, 0, Math.PI * 2);
this._ctx.fill();
// Eyes
this._ctx.fillStyle = '#000000';
this._ctx.fillRect(plant.x + 10, plant.y + 5, 3, 3);
this._ctx.fillRect(plant.x + 17, plant.y + 5, 3, 3);
}
});
}
_renderProjectiles() {
this._projectiles.forEach(projectile => {
// Fireball effect
this._ctx.fillStyle = projectile.color;
this._ctx.beginPath();
this._ctx.arc(projectile.x + projectile.width/2, projectile.y + projectile.height/2, projectile.width/2, 0, Math.PI * 2);
this._ctx.fill();
// Inner glow
this._ctx.fillStyle = '#FFFF00'; // Yellow center
this._ctx.beginPath();
this._ctx.arc(projectile.x + projectile.width/2, projectile.y + projectile.height/2, projectile.width/4, 0, Math.PI * 2);
this._ctx.fill();
});
}
_renderCatapults() {
this._catapults.forEach(catapult => {
// Catapult base
this._ctx.fillStyle = catapult.color;
this._ctx.fillRect(catapult.x, catapult.y + 40, catapult.width, 40);
// Catapult arm
this._ctx.fillStyle = '#654321'; // Dark brown
this._ctx.fillRect(catapult.x + 10, catapult.y, 8, 50);
// Bucket
this._ctx.fillStyle = '#8B4513';
this._ctx.fillRect(catapult.x + 5, catapult.y, 18, 12);
// Support beams
this._ctx.strokeStyle = '#654321';
this._ctx.lineWidth = 3;
this._ctx.beginPath();
this._ctx.moveTo(catapult.x + 30, catapult.y + 40);
this._ctx.lineTo(catapult.x + 14, catapult.y + 25);
this._ctx.stroke();
});
}
_renderBoulders() {
this._boulders.forEach(boulder => {
// Boulder body
this._ctx.fillStyle = boulder.color;
this._ctx.beginPath();
this._ctx.arc(boulder.x + boulder.width/2, boulder.y + boulder.height/2, boulder.width/2, 0, Math.PI * 2);
this._ctx.fill();
// Boulder texture/cracks
this._ctx.strokeStyle = '#555555';
this._ctx.lineWidth = 1;
this._ctx.beginPath();
this._ctx.arc(boulder.x + boulder.width/2 - 5, boulder.y + boulder.height/2 - 3, 3, 0, Math.PI);
this._ctx.stroke();
this._ctx.beginPath();
this._ctx.arc(boulder.x + boulder.width/2 + 4, boulder.y + boulder.height/2 + 2, 2, 0, Math.PI);
this._ctx.stroke();
// Show trajectory line for flying boulders (debug)
if (!boulder.hasLanded && boulder.velocityX !== 0) {
this._ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
this._ctx.lineWidth = 1;
this._ctx.setLineDash([5, 5]);
this._ctx.beginPath();
this._ctx.moveTo(boulder.x + boulder.width/2, boulder.y + boulder.height/2);
this._ctx.lineTo(boulder.x + boulder.velocityX * 10, boulder.y + boulder.velocityY * 10);
this._ctx.stroke();
this._ctx.setLineDash([]);
}
});
}
_renderFinishLine() {
const level = this._levelData[this._currentLevelIndex];
const finishX = level.endX;
// Draw checkered flag pattern
this._ctx.fillStyle = '#FFD700'; // Gold color
this._ctx.fillRect(finishX - 5, 50, 10, this._config.canvasHeight - 100);
// Add black and white checkered pattern
const squareSize = 20;
for (let y = 50; y < this._config.canvasHeight - 50; y += squareSize) {
for (let x = 0; x < 10; x += squareSize / 2) {
const isBlack = Math.floor((y - 50) / squareSize) % 2 === Math.floor(x / (squareSize / 2)) % 2;
this._ctx.fillStyle = isBlack ? '#000000' : '#FFFFFF';
this._ctx.fillRect(finishX - 5 + x, y, squareSize / 2, squareSize);
}
}
// Add "FINISH" text
this._ctx.save();
this._ctx.translate(finishX, 30);
this._ctx.rotate(-Math.PI / 2);
this._ctx.fillStyle = '#FF0000';
this._ctx.font = 'bold 16px Arial';
this._ctx.textAlign = 'center';
this._ctx.fillText('FINISH', 0, 0);
this._ctx.restore();
// Add arrow pointing to finish
if (this._mario.x < finishX - 200) {
this._ctx.fillStyle = '#FFD700';
this._ctx.font = 'bold 20px Arial';
this._ctx.textAlign = 'left';
this._ctx.fillText('→ FINISH', finishX - 150, 25);
}
}
_updateFlyingEyes() {
const currentTime = Date.now();
this._flyingEyes.forEach((eye, index) => {
// Calculate distance to Mario
const distanceToMario = Math.sqrt(
Math.pow(eye.x - this._mario.x, 2) +
Math.pow(eye.y - this._mario.y, 2)
);
// Determine if eye should chase Mario
eye.isChasing = distanceToMario < eye.chaseDistance;
// Handle dash behavior
if (eye.isDashing) {
eye.dashDuration--;
if (eye.dashDuration <= 0) {
eye.isDashing = false;
eye.lastDashTime = currentTime;
console.log(`👁️ Eye finished dashing!`);
}
// During dash, maintain dash velocity (no other movement changes)
} else {
// Check if it's time to dash (only when chasing)
if (eye.isChasing && currentTime - eye.lastDashTime > eye.dashInterval && Math.random() < 0.3) {
// Start dash in random 90-degree direction
const dashDirections = [
{ x: 1, y: 0 }, // Right
{ x: -1, y: 0 }, // Left
{ x: 0, y: 1 }, // Down
{ x: 0, y: -1 } // Up
];
const dashDir = dashDirections[Math.floor(Math.random() * 4)];
eye.velocityX = dashDir.x * eye.dashSpeed;
eye.velocityY = dashDir.y * eye.dashSpeed;
eye.isDashing = true;
eye.dashDuration = 20; // Dash for 20 frames (~0.33 seconds)
console.log(`👁️ Eye started dashing! Direction: ${dashDir.x}, ${dashDir.y}`);
} else if (eye.isChasing) {
// Normal chase behavior - move toward Mario
const deltaX = this._mario.x - eye.x;
const deltaY = this._mario.y - eye.y;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 0) {
// Normalize direction and apply chase speed
eye.velocityX = (deltaX / distance) * eye.chaseSpeed;
eye.velocityY = (deltaY / distance) * eye.chaseSpeed;
}
console.log(`👁️ Eye chasing Mario! Distance: ${distanceToMario.toFixed(0)}`);
} else {
// Idle behavior - random floating movement (faster now)
if (currentTime - eye.lastDirectionChange > eye.directionChangeInterval) {
// Change direction randomly
eye.velocityX = (Math.random() - 0.5) * eye.idleSpeed * 2;
eye.velocityY = (Math.random() - 0.5) * eye.idleSpeed * 2;
eye.lastDirectionChange = currentTime;
eye.directionChangeInterval = 2000 + Math.random() * 3000;
}
}
}
// Apply movement
eye.x += eye.velocityX;
eye.y += eye.velocityY;
// Keep eyes within screen bounds
if (eye.x < 0) {
eye.x = 0;
eye.velocityX = Math.abs(eye.velocityX);
} else if (eye.x > this._levelWidth - eye.width) {
eye.x = this._levelWidth - eye.width;
eye.velocityX = -Math.abs(eye.velocityX);
}
if (eye.y < 0) {
eye.y = 0;
eye.velocityY = Math.abs(eye.velocityY);
} else if (eye.y > this._config.canvasHeight - eye.height - 50) { // Stay above ground
eye.y = this._config.canvasHeight - eye.height - 50;
eye.velocityY = -Math.abs(eye.velocityY);
}
// Blinking animation
eye.blinkTimer++;
if (eye.blinkTimer > 120 + Math.random() * 180) { // Blink every 2-5 seconds
eye.isBlinking = true;
eye.blinkTimer = 0;
}
if (eye.isBlinking && eye.blinkTimer > 10) { // Blink lasts 10 frames
eye.isBlinking = false;
}
// Check collision with Mario
if (this._isColliding(this._mario, eye)) {
// Eye can be stomped like normal enemies
const overlapTop = (this._mario.y + this._mario.height) - eye.y;
const overlapBottom = (eye.y + eye.height) - this._mario.y;
if (this._mario.velocityY > 0 && overlapTop < overlapBottom) {
// Mario stomped the eye
this._flyingEyes.splice(index, 1);
this._mario.velocityY = this._config.jumpForce / 2; // Small bounce
this._addParticles(eye.x, eye.y, '#DC143C'); // Red particles
this._playSound('enemy_defeat');
console.log(`👁️ Mario stomped flying eye!`);
} else {
// Eye hurts Mario - reset level
console.log(`💥 Mario hit by flying eye - restarting level`);
this._restartLevel();
}
}
});
}
_renderFlyingEyes() {
this._flyingEyes.forEach(eye => {
// Draw eye body (white oval)
this._ctx.fillStyle = '#FFFFFF';
this._ctx.beginPath();
this._ctx.ellipse(eye.x + eye.width/2, eye.y + eye.height/2, eye.width/2, eye.height/2, 0, 0, 2 * Math.PI);
this._ctx.fill();
if (!eye.isBlinking) {
// Draw red iris
this._ctx.fillStyle = eye.color;
this._ctx.beginPath();
this._ctx.ellipse(eye.x + eye.width/2, eye.y + eye.height/2, eye.width/3, eye.height/3, 0, 0, 2 * Math.PI);
this._ctx.fill();
// Draw black pupil that follows Mario
const deltaX = this._mario.x - eye.x;
const deltaY = this._mario.y - eye.y;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
let pupilOffsetX = 0;
let pupilOffsetY = 0;
if (distance > 0) {
const maxOffset = 4; // Maximum pupil movement
pupilOffsetX = (deltaX / distance) * maxOffset;
pupilOffsetY = (deltaY / distance) * maxOffset;
}
this._ctx.fillStyle = eye.pupilColor;
this._ctx.beginPath();
this._ctx.arc(
eye.x + eye.width/2 + pupilOffsetX,
eye.y + eye.height/2 + pupilOffsetY,
eye.width/6,
0,
2 * Math.PI
);
this._ctx.fill();
} else {
// Draw closed eye (horizontal line)
this._ctx.strokeStyle = '#000000';
this._ctx.lineWidth = 2;
this._ctx.beginPath();
this._ctx.moveTo(eye.x + 5, eye.y + eye.height/2);
this._ctx.lineTo(eye.x + eye.width - 5, eye.y + eye.height/2);
this._ctx.stroke();
}
// Add subtle outline
this._ctx.strokeStyle = '#CCCCCC';
this._ctx.lineWidth = 1;
this._ctx.beginPath();
this._ctx.ellipse(eye.x + eye.width/2, eye.y + eye.height/2, eye.width/2, eye.height/2, 0, 0, 2 * Math.PI);
this._ctx.stroke();
});
}
_updateBoss() {
if (!this._boss) return;
const currentTime = Date.now();
// Update boss collision cooldown
if (this._bossCollisionCooldown > 0) {
this._bossCollisionCooldown--;
}
// Update damage flash timer
if (this._boss.damageFlashTimer > 0) {
this._boss.damageFlashTimer--;
if (this._boss.damageFlashTimer <= 0) {
this._boss.isDamaged = false;
}
}
// Boss collision with Mario (blocks the path) - with cooldown to prevent loop
if (this._isColliding(this._mario, this._boss) && this._bossCollisionCooldown <= 0) {
console.log(`👹 Mario hit boss body - bounced back!`);
// TRUE velocity inversion: velocity = velocity * -1
this._mario.velocityX = this._mario.velocityX * -1 - 3; // Invert + add knockback
this._mario.velocityY = this._config.jumpForce * 0.7; // Bounce upward
// Set collision cooldown (3 ticks)
this._bossCollisionCooldown = 3;
// Impact particles
this._addParticles(
this._mario.x + this._mario.width / 2,
this._mario.y + this._mario.height / 2,
'#FFFF00' // Yellow impact particles
);
// Impact sound
this._playSound('enemy_defeat');
console.log(`⏱️ Boss collision cooldown set: ${this._bossCollisionCooldown} ticks`);
}
}
_renderBoss() {
if (!this._boss) return;
const ctx = this._ctx;
const x = this._boss.x;
const y = this._boss.y;
const w = this._boss.width;
const h = this._boss.height;
// Boss main body (torso) - more humanoid proportions
ctx.fillStyle = this._boss.isDamaged ? '#FF6666' : this._boss.color;
ctx.fillRect(x + 20, y, w - 40, h * 0.6); // Torso
// Boss legs (separated)
ctx.fillRect(x + 10, y + h * 0.6, 35, h * 0.4); // Left leg
ctx.fillRect(x + w - 45, y + h * 0.6, 35, h * 0.4); // Right leg
// Boss head (bigger and more menacing)
const headWidth = 60;
const headHeight = 50;
ctx.fillRect(x + (w - headWidth) / 2, y - headHeight, headWidth, headHeight);
// Boss outline for all parts
ctx.strokeStyle = '#000000';
ctx.lineWidth = 3;
ctx.strokeRect(x + 20, y, w - 40, h * 0.6); // Torso outline
ctx.strokeRect(x + 10, y + h * 0.6, 35, h * 0.4); // Left leg outline
ctx.strokeRect(x + w - 45, y + h * 0.6, 35, h * 0.4); // Right leg outline
ctx.strokeRect(x + (w - headWidth) / 2, y - headHeight, headWidth, headHeight); // Head outline
// Boss eyes (glowing red, larger)
ctx.fillStyle = this._boss.eyeColor;
const eyeSize = 12;
ctx.fillRect(x + (w - headWidth) / 2 + 10, y - headHeight + 15, eyeSize, eyeSize); // Left eye
ctx.fillRect(x + (w - headWidth) / 2 + headWidth - 22, y - headHeight + 15, eyeSize, eyeSize); // Right eye
// Boss mouth (menacing)
ctx.fillStyle = '#000000';
ctx.fillRect(x + (w - headWidth) / 2 + 15, y - headHeight + 35, headWidth - 30, 8);
// Shoulder spikes for more intimidating look
ctx.fillStyle = '#8B4513';
ctx.fillRect(x + 15, y + 5, 15, 25); // Left shoulder
ctx.fillRect(x + w - 30, y + 5, 15, 25); // Right shoulder
// Boss knees (damage zones) - integrated styling as kneepads
ctx.fillStyle = '#FFD700'; // Gold kneepads
ctx.fillRect(
this._boss.leftKnee.x + 5,
this._boss.leftKnee.y + 5,
this._boss.leftKnee.width - 10,
this._boss.leftKnee.height - 10
);
ctx.fillRect(
this._boss.rightKnee.x + 5,
this._boss.rightKnee.y + 5,
this._boss.rightKnee.width - 10,
this._boss.rightKnee.height - 10
);
// Kneepad outlines
ctx.strokeStyle = '#B8860B'; // Dark goldenrod outline
ctx.lineWidth = 2;
ctx.strokeRect(
this._boss.leftKnee.x + 5,
this._boss.leftKnee.y + 5,
this._boss.leftKnee.width - 10,
this._boss.leftKnee.height - 10
);
ctx.strokeRect(
this._boss.rightKnee.x + 5,
this._boss.rightKnee.y + 5,
this._boss.rightKnee.width - 10,
this._boss.rightKnee.height - 10
);
// Boss health bar
const healthBarWidth = 200;
const healthBarHeight = 20;
const healthBarX = this._boss.x + (this._boss.width / 2) - (healthBarWidth / 2);
const healthBarY = this._boss.y - 40;
// Health bar background
ctx.fillStyle = '#FF0000';
ctx.fillRect(healthBarX, healthBarY, healthBarWidth, healthBarHeight);
// Health bar foreground
const healthPercent = this._boss.health / this._boss.maxHealth;
ctx.fillStyle = '#00FF00';
ctx.fillRect(healthBarX, healthBarY, healthBarWidth * healthPercent, healthBarHeight);
// Health bar border
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
ctx.strokeRect(healthBarX, healthBarY, healthBarWidth, healthBarHeight);
// Render turrets on boss
this._bossTurrets.forEach(turret => {
ctx.fillStyle = turret.color;
ctx.fillRect(turret.x, turret.y, turret.width, turret.height);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
ctx.strokeRect(turret.x, turret.y, turret.width, turret.height);
});
}
_renderMario() {
// Mario body
this._ctx.fillStyle = this._mario.color;
this._ctx.fillRect(this._mario.x, this._mario.y, this._mario.width, this._mario.height);
// Hat
this._ctx.fillStyle = '#8B0000';
this._ctx.fillRect(this._mario.x + 4, this._mario.y - 8, this._mario.width - 8, 12);
// Face
this._ctx.fillStyle = '#FFDBAC';
this._ctx.fillRect(this._mario.x + 8, this._mario.y + 8, 16, 16);
// Eyes
this._ctx.fillStyle = '#000';
const eyeOffset = this._mario.facing === 'right' ? 2 : -2;
this._ctx.fillRect(this._mario.x + 10 + eyeOffset, this._mario.y + 12, 2, 2);
this._ctx.fillRect(this._mario.x + 18 + eyeOffset, this._mario.y + 12, 2, 2);
// Mustache
this._ctx.fillRect(this._mario.x + 12, this._mario.y + 18, 8, 2);
}
_renderParticles() {
this._particles.forEach(particle => {
const alpha = particle.life / particle.maxLife;
if (particle.isFinishStar) {
// Render finish line stars as actual star symbols
this._ctx.font = `${particle.size}px serif`;
this._ctx.textAlign = 'center';
this._ctx.textBaseline = 'middle';
this._ctx.fillStyle = particle.color + Math.floor(alpha * 255).toString(16).padStart(2, '0');
this._ctx.fillText('⭐', particle.x, particle.y);
} else {
// Regular particles
this._ctx.fillStyle = particle.color + Math.floor(alpha * 255).toString(16).padStart(2, '0');
this._ctx.fillRect(particle.x, particle.y, 4, 4);
}
});
// Reset text alignment
this._ctx.textAlign = 'left';
this._ctx.textBaseline = 'top';
}
_renderCastle() {
if (!this._castleStructure) return;
const castle = this._castleStructure;
// Set font for castle emoji
this._ctx.font = `${castle.size}px serif`;
this._ctx.textAlign = 'center';
this._ctx.textBaseline = 'middle';
// Render castle emoji
this._ctx.fillText(castle.emoji, castle.x, castle.y);
// Render princess in the castle
if (castle.princess) {
this._ctx.font = `${castle.princess.size}px serif`;
this._ctx.fillText(castle.princess.emoji, castle.princess.x, castle.princess.y);
}
// Add sparkles around the massive castle (360px)
this._ctx.font = '40px serif';
this._ctx.fillText('✨', castle.x - 180, castle.y - 120);
this._ctx.fillText('✨', castle.x + 180, castle.y - 120);
this._ctx.fillText('✨', castle.x - 120, castle.y + 120);
this._ctx.fillText('✨', castle.x + 120, castle.y + 120);
// Additional sparkles for the massive castle
this._ctx.font = '30px serif';
this._ctx.fillText('✨', castle.x - 220, castle.y);
this._ctx.fillText('✨', castle.x + 220, castle.y);
this._ctx.fillText('✨', castle.x, castle.y - 200);
this._ctx.fillText('✨', castle.x, castle.y + 180);
// Extra sparkles for grandeur
this._ctx.font = '25px serif';
this._ctx.fillText('⭐', castle.x - 160, castle.y - 60);
this._ctx.fillText('⭐', castle.x + 160, castle.y - 60);
this._ctx.fillText('⭐', castle.x - 60, castle.y + 160);
this._ctx.fillText('⭐', castle.x + 60, castle.y + 160);
// Reset text alignment
this._ctx.textAlign = 'left';
this._ctx.textBaseline = 'top';
}
_renderUI() {
if (this._uiOverlay) {
this._uiOverlay.innerHTML = `
<div>Score: ${this._score}</div>
<div>Level: ${this._currentLevel}/${this._config.maxLevels}</div>
<div>Questions: ${this._questionsAnswered}</div>
<div style="margin-top: 10px; font-size: 14px;">
Use Arrow Keys or WASD to move, Space/Up to jump
</div>
`;
}
}
_getRandomSentence() {
const availableSentences = this._sentences.filter(s => !this._usedSentences.includes(s));
if (availableSentences.length === 0) {
// Reset used sentences if all are used
this._usedSentences = [];
return this._sentences[0];
}
const sentence = availableSentences[Math.floor(Math.random() * availableSentences.length)];
this._usedSentences.push(sentence);
return sentence;
}
_shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
_renderDebugHitboxes() {
// Only render in debug mode (can be toggled)
if (!window.DEBUG_HITBOXES) return;
this._ctx.strokeStyle = '#FF0000';
this._ctx.lineWidth = 2;
// Mario hitbox
this._ctx.strokeRect(this._mario.x, this._mario.y, this._mario.width, this._mario.height);
// Platform hitboxes
this._ctx.strokeStyle = '#00FF00';
this._platforms.forEach(platform => {
this._ctx.strokeRect(platform.x, platform.y, platform.width, platform.height);
});
// Wall hitboxes
this._ctx.strokeStyle = '#8B4513';
this._walls.forEach(wall => {
this._ctx.strokeRect(wall.x, wall.y, wall.width, wall.height);
});
// Enemy hitboxes
this._ctx.strokeStyle = '#FF00FF';
this._enemies.forEach(enemy => {
this._ctx.strokeRect(enemy.x, enemy.y, enemy.width, enemy.height);
});
// Question block hitboxes
this._ctx.strokeStyle = '#FFFF00';
this._questionBlocks.forEach(block => {
this._ctx.strokeRect(block.x, block.y, block.width, block.height);
});
}
}
export default MarioEducational;