Class_generator/src/gameHelpers/MarioEducational/PhysicsEngine.js
StillHammer 4714a4a1c6 Add TTS support and improve content compatibility system
Major improvements:
- Add TTSHelper utility for text-to-speech functionality
- Enhance content compatibility scoring across all games
- Improve sentence extraction from multiple content sources
- Update all game modules to support diverse content formats
- Refine MarioEducational physics and rendering
- Polish UI styles and remove unused CSS

Games updated: AdventureReader, FillTheBlank, FlashcardLearning,
GrammarDiscovery, MarioEducational, QuizGame, RiverRun, WhackAMole,
WhackAMoleHard, WizardSpellCaster, WordDiscovery, WordStorm

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 02:49:48 +08:00

376 lines
14 KiB
JavaScript

/**
* PhysicsEngine.js
* Helper for physics simulation, collision detection, and movement
* Handles Mario physics, enemy physics, particles, and camera
*/
export class PhysicsEngine {
/**
* Update Mario movement based on key inputs
* @param {Object} mario - Mario object
* @param {Object} keys - Key states object
* @param {Object} config - Game config with moveSpeed and jumpForce
* @param {boolean} isCelebrating - If true, disable movement
* @param {Function} playSound - Sound callback for jump
*/
static updateMarioMovement(mario, keys, config, isCelebrating, playSound) {
// Don't update movement during celebration
if (isCelebrating) return;
// Horizontal movement
if (keys['ArrowLeft'] || keys['KeyA']) {
mario.velocityX = -config.moveSpeed;
mario.facing = 'left';
} else if (keys['ArrowRight'] || keys['KeyD']) {
mario.velocityX = config.moveSpeed;
mario.facing = 'right';
} else {
mario.velocityX *= 0.8; // Friction
}
// Jumping
if ((keys['ArrowUp'] || keys['KeyW'] || keys['Space']) && mario.onGround) {
mario.velocityY = config.jumpForce;
mario.onGround = false;
if (playSound) playSound('jump');
}
}
/**
* Update Mario physics (gravity and position)
* @param {Object} mario - Mario object
* @param {Object} config - Game config with gravity
* @param {Object} level - Current level data
* @param {boolean} levelCompleted - If level is completed
* @param {Function} onFallOff - Callback when Mario falls off world
*/
static updateMarioPhysics(mario, config, level, levelCompleted, onFallOff) {
// Apply gravity
mario.velocityY += config.gravity;
// Update position
mario.x += mario.velocityX;
mario.y += mario.velocityY;
// Prevent going off left edge
if (mario.x < 0) {
mario.x = 0;
}
// Stop Mario at finish line during celebration
if (mario.x > level.endX && levelCompleted) {
mario.x = level.endX;
mario.velocityX = 0;
}
// Check if Mario fell off the world
if (mario.y > config.canvasHeight + 100) {
if (onFallOff) onFallOff();
}
}
/**
* Update enemy movement and AI
* @param {Array} enemies - Array of enemies
* @param {Array} walls - Array of walls
* @param {Array} platforms - Array of platforms
* @param {number} levelWidth - Level width
* @param {boolean} isCelebrating - If true, disable updates
*/
static updateEnemies(enemies, walls, platforms, levelWidth, isCelebrating) {
// Don't update enemies during celebration
if (isCelebrating) return;
enemies.forEach(enemy => {
// Store old position for collision detection
const oldX = enemy.x;
enemy.x += enemy.velocityX;
// Check wall collisions
const hitWall = walls.some(wall => {
return enemy.x < wall.x + wall.width &&
enemy.x + enemy.width > wall.x &&
enemy.y < wall.y + wall.height &&
enemy.y + enemy.height > wall.y;
});
if (hitWall) {
// Reverse position and direction
enemy.x = oldX;
enemy.velocityX *= -1;
console.log(`🧱 Enemy hit wall, reversing direction`);
}
// Simple AI: reverse direction at platform edges
const platform = platforms.find(p =>
enemy.x >= p.x - 10 && enemy.x <= p.x + p.width + 10 &&
enemy.y >= p.y - enemy.height - 5 && enemy.y <= p.y + 5
);
if (!platform || enemy.x <= 0 || enemy.x >= levelWidth) {
enemy.velocityX *= -1;
}
});
}
/**
* Check all collisions (platforms, walls, enemies, etc.)
* @param {Object} mario - Mario object
* @param {Object} gameState - All game entities
* @param {Object} callbacks - Callbacks for various collision events
*/
static checkCollisions(mario, gameState, callbacks) {
const {
platforms, questionBlocks, enemies, walls, catapults,
piranhaPlants, boulders, flyingEyes
} = gameState;
const {
onQuestionBlock, onEnemyDefeat, onMarioDeath, onAddParticles
} = callbacks;
// Platform collisions
mario.onGround = false;
platforms.forEach(platform => {
if (this.isColliding(mario, platform)) {
// Check if Mario is landing on top
if (mario.velocityY > 0 && mario.y + mario.height - mario.velocityY <= platform.y + 5) {
mario.y = platform.y - mario.height;
mario.velocityY = 0;
mario.onGround = true;
}
// Hit from below
else if (mario.velocityY < 0 && mario.y - mario.velocityY >= platform.y + platform.height - 5) {
mario.y = platform.y + platform.height;
mario.velocityY = 0;
}
// Side collision
else {
if (mario.velocityX > 0) {
mario.x = platform.x - mario.width;
} else if (mario.velocityX < 0) {
mario.x = platform.x + platform.width;
}
mario.velocityX = 0;
}
}
});
// Boulder collisions (grounded boulders only)
boulders.forEach(boulder => {
if (boulder.hasLanded && this.isColliding(mario, boulder)) {
console.log(`🪨 Mario hit by grounded boulder - restarting level`);
if (onMarioDeath) onMarioDeath();
}
});
// Question block collisions (non-blocking - just trigger on contact)
questionBlocks.forEach(block => {
if (!block.hit && this.isColliding(mario, block)) {
// Trigger question block on any contact (pass-through)
if (onQuestionBlock) onQuestionBlock(block);
}
});
// Wall collisions
walls.forEach(wall => {
if (this.isColliding(mario, wall)) {
// Determine collision direction based on previous position
const overlapLeft = (mario.x + mario.width) - wall.x;
const overlapRight = (wall.x + wall.width) - mario.x;
const overlapTop = (mario.y + mario.height) - wall.y;
const overlapBottom = (wall.y + wall.height) - mario.y;
// Find the smallest overlap to determine collision side
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
if (minOverlap === overlapTop && mario.velocityY > 0) {
// Landing on top of wall
mario.y = wall.y - mario.height;
mario.velocityY = 0;
mario.onGround = true;
} else if (minOverlap === overlapBottom && mario.velocityY < 0) {
// Hit wall from below
mario.y = wall.y + wall.height;
mario.velocityY = 0;
} else if (minOverlap === overlapLeft && mario.velocityX > 0) {
// Hit wall from left side
mario.x = wall.x - mario.width;
mario.velocityX = 0;
} else if (minOverlap === overlapRight && mario.velocityX < 0) {
// Hit wall from right side
mario.x = wall.x + wall.width;
mario.velocityX = 0;
}
}
});
// Catapult collisions (solid obstacles)
catapults.forEach(catapult => {
if (this.isColliding(mario, catapult)) {
// Treat catapults as solid platforms
if (mario.velocityY > 0) {
mario.y = catapult.y - mario.height;
mario.velocityY = 0;
mario.onGround = true;
}
}
});
// Enemy collisions
enemies.forEach((enemy, index) => {
if (this.isColliding(mario, enemy)) {
// Check if Mario jumped on enemy
if (mario.velocityY > 0 && mario.y < enemy.y + enemy.height / 2) {
// Enemy defeated
mario.velocityY = -8; // Bounce
if (onEnemyDefeat) onEnemyDefeat(index);
if (onAddParticles) onAddParticles(enemy.x, enemy.y, '#FFD700');
} else {
// Mario hit by enemy
console.log(`👾 Mario hit by enemy - restarting level`);
if (onMarioDeath) onMarioDeath();
}
}
});
// Piranha Plant collisions
piranhaPlants.forEach(plant => {
if (!plant.flattened && this.isColliding(mario, plant)) {
// Check if Mario jumped on plant
if (mario.velocityY > 0 && mario.y < plant.y + plant.height / 2) {
// Plant flattened
plant.flattened = true;
plant.flattenedTimer = 120; // Flattened for 2 seconds
mario.velocityY = -8; // Bounce
if (onAddParticles) onAddParticles(plant.x, plant.y, '#228B22');
console.log(`🌸 Mario flattened piranha plant`);
} else {
// Mario hit by plant
console.log(`🌸 Mario hit by piranha plant - restarting level`);
if (onMarioDeath) onMarioDeath();
}
}
// Check if stepping on flattened plant
if (plant.flattened && this.isColliding(mario, plant)) {
mario.onGround = true;
}
});
// Flying Eye collisions
if (flyingEyes) {
flyingEyes.forEach((eye, index) => {
// Eye position is CENTER, convert to top-left corner for collision
const eyeLeft = eye.x - eye.width / 2;
const eyeTop = eye.y - eye.height / 2;
const eyeRect = {
x: eyeLeft,
y: eyeTop,
width: eye.width,
height: eye.height
};
if (this.isColliding(mario, eyeRect)) {
// Check if Mario jumped on eye from above
if (mario.velocityY > 0 && mario.y < eyeTop + eye.height / 2) {
// Eye defeated
mario.velocityY = -8; // Bounce
flyingEyes.splice(index, 1); // Remove eye
if (onAddParticles) onAddParticles(eye.x, eye.y, '#DC143C');
console.log(`👁️ Mario defeated flying eye`);
} else {
// Mario hit by eye
console.log(`👁️ Mario hit by flying eye - restarting level`);
if (onMarioDeath) onMarioDeath();
}
}
});
}
}
/**
* Rectangle-Rectangle collision detection
* @param {Object} rect1 - First rectangle
* @param {Object} rect2 - Second rectangle
* @returns {boolean} - True if colliding
*/
static isColliding(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
/**
* Update camera to follow Mario
* @param {Object} camera - Camera object with x, y
* @param {Object} mario - Mario object
* @param {number} canvasWidth - Canvas width
*/
static updateCamera(camera, mario, canvasWidth) {
// Camera follows Mario horizontally, centered
camera.x = mario.x - canvasWidth / 2 + mario.width / 2;
camera.y = 0; // Fixed vertical camera
}
/**
* Create particle effects
* @param {number} x - X position
* @param {number} y - Y position
* @param {string} color - Particle color
* @param {Array} particles - Particles array to add to
* @param {number} count - Number of particles to create
*/
static addParticles(x, y, color, particles, count = 10) {
for (let i = 0; i < count; i++) {
particles.push({
x: x,
y: y,
velocityX: (Math.random() - 0.5) * 8,
velocityY: (Math.random() - 0.5) * 8,
life: 1.0,
decay: 0.02,
size: 4,
color: color
});
}
}
/**
* Create small particle burst
* @param {number} x - X position
* @param {number} y - Y position
* @param {string} color - Particle color
* @param {Array} particles - Particles array to add to
*/
static addSmallParticles(x, y, color, particles) {
this.addParticles(x, y, color, particles, 5);
}
/**
* Update all particles
* @param {Array} particles - Array of particles
* @returns {Array} - Updated particles array (with dead particles removed)
*/
static updateParticles(particles) {
const updatedParticles = [];
particles.forEach(particle => {
particle.x += particle.velocityX;
particle.y += particle.velocityY;
particle.velocityY += 0.3; // Gravity
particle.life -= particle.decay;
if (particle.life > 0) {
updatedParticles.push(particle);
}
});
return updatedParticles;
}
}
export default PhysicsEngine;