Class_generator/src/gameHelpers/MarioEducational/enemies/Catapult.js
StillHammer 325b97060c Add LEDU Chinese course content and documentation
Add comprehensive Chinese reading course (乐读) with 4 chapters of vocabulary, texts, and exercises. Include architecture documentation for module development and progress tracking system.

Content:
- LEDU book metadata with 12 chapter outline
- Chapter 1: Food culture (民以食为天) - 45+ vocabulary, etiquette
- Chapter 2: Shopping (货比三家) - comparative shopping vocabulary
- Chapter 3: Sports & fitness (生命在于运动) - exercise habits
- Chapter 4: Additional vocabulary and grammar

Documentation:
- Architecture principles and patterns
- Module creation guide (Game, DRS, Progress)
- Interface system (C++ style contracts)
- Progress tracking and prerequisites

Game Enhancements:
- MarioEducational helper classes (Physics, Renderer, Sound, Enemies)
- VocabularyModule TTS improvements
- Updated CLAUDE.md with project status

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 07:25:53 +08:00

348 lines
12 KiB
JavaScript

/**
* Catapult.js
* Enemy that launches boulders and stone rain at Mario
* Catapults appear in level 4+, Onagers (stronger) in level 5+
*/
export class Catapult {
/**
* Generate catapults for a level
* @param {Object} level - Level data
* @param {number} levelIndex - Level index
* @param {number} levelWidth - Width of the level
* @param {number} canvasHeight - Canvas height
* @returns {Array} - Array of catapult objects
*/
static generate(level, levelIndex, levelWidth, canvasHeight) {
const catapults = [];
let catapultCount = 1; // Always 1 catapult for level 4+
let onagerCount = 0;
// Level 5+ gets onagers
if (levelIndex >= 4) {
onagerCount = 1; // 1 onager for level 5+
}
const totalCount = catapultCount + onagerCount;
console.log(`🏹 Generating ${catapultCount} catapult(s) and ${onagerCount} onager(s) for level ${levelIndex + 1}`);
for (let i = 0; i < totalCount; i++) {
const isOnager = i >= catapultCount; // Onagers come after catapults
// Place catapults near END of level
const nearEndX = levelWidth * 0.7; // 70% through level
const catapultX = nearEndX + (i * 300) + Math.random() * 200;
let catapultY = canvasHeight - 100; // Default: on background ground
// Check if there's a platform, wall, or stair above this position
const platformAbove = this._findPlatformAbove(catapultX, catapultY, level.platforms || []);
const wallAbove = this._findWallAbove(catapultX, catapultY, level.walls || []);
const stairAbove = this._findStairAbove(catapultX, catapultY, level.stairs || []);
// Choose the lowest obstacle (closest to ground = highest Y value)
const obstacles = [platformAbove, wallAbove, stairAbove].filter(obs => obs !== null);
if (obstacles.length > 0) {
const obstacleAbove = obstacles.reduce((lowest, current) =>
current.y > lowest.y ? current : lowest
);
catapultY = obstacleAbove.y - 80; // 80 is catapult height
console.log(`🏹 Catapult moved to obstacle at y=${catapultY.toFixed(0)}`);
}
catapults.push({
x: catapultX,
y: catapultY,
width: 60,
height: 80,
color: isOnager ? '#654321' : '#8B4513',
lastShot: 0,
shootCooldown: isOnager ? 6000 + Math.random() * 2000 : 4000 + Math.random() * 2000,
type: isOnager ? 'onager' : 'catapult',
isOnager: isOnager,
armAngle: 0 // For rendering
});
console.log(`${isOnager ? '🏛️' : '🏹'} ${isOnager ? 'Onager' : 'Catapult'} placed at x=${catapultX.toFixed(0)}, y=${catapultY.toFixed(0)}`);
}
return catapults;
}
/**
* Find platform above a position
*/
static _findPlatformAbove(x, groundY, platforms) {
let bestPlatform = null;
let lowestY = 0;
platforms.forEach(platform => {
const catapultLeft = x;
const catapultRight = x + 60;
const platformLeft = platform.x;
const platformRight = platform.x + platform.width;
const hasHorizontalOverlap = catapultLeft < platformRight && catapultRight > platformLeft;
if (hasHorizontalOverlap && platform.y < groundY && platform.y > lowestY) {
bestPlatform = platform;
lowestY = platform.y;
}
});
return bestPlatform;
}
/**
* Find wall above a position
*/
static _findWallAbove(x, groundY, walls) {
let bestWall = null;
let lowestY = 0;
walls.forEach(wall => {
const catapultLeft = x;
const catapultRight = x + 60;
const wallLeft = wall.x;
const wallRight = wall.x + wall.width;
const hasHorizontalOverlap = catapultLeft < wallRight && catapultRight > wallLeft;
if (hasHorizontalOverlap && wall.y < groundY && wall.y > lowestY) {
bestWall = wall;
lowestY = wall.y;
}
});
return bestWall;
}
/**
* Find stair above a position
*/
static _findStairAbove(x, groundY, stairs) {
let bestStair = null;
let lowestY = 0;
stairs.forEach(stair => {
const catapultLeft = x;
const catapultRight = x + 60;
const stairLeft = stair.x;
const stairRight = stair.x + stair.width;
const hasHorizontalOverlap = catapultLeft < stairRight && catapultRight > stairLeft;
if (hasHorizontalOverlap && stair.y < groundY && stair.y > lowestY) {
bestStair = stair;
lowestY = stair.y;
}
});
return bestStair;
}
/**
* Update all catapults
* @param {Array} catapults - Array of catapults
* @param {Object} mario - Mario object
* @param {Array} boulders - Boulders array
* @param {Array} stones - Stones array (for onagers)
* @param {Function} playSound - Sound callback
*/
static update(catapults, mario, boulders, stones, playSound) {
const currentTime = Date.now();
catapults.forEach(catapult => {
// Arm animation
catapult.armAngle = Math.sin(Date.now() / 200) * 0.2;
// Check if it's time to shoot
if (currentTime - catapult.lastShot > catapult.shootCooldown) {
const distanceToMario = Math.abs(catapult.x - mario.x);
// Catapult shoots boulders (single target)
if (!catapult.isOnager && distanceToMario < 600) {
// Calculate trajectory to Mario
const dx = mario.x - catapult.x;
const dy = mario.y - catapult.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const speed = 8;
const velocityX = (dx / distance) * speed;
const velocityY = (dy / distance) * speed;
boulders.push({
x: catapult.x + catapult.width / 2,
y: catapult.y,
velocityX: velocityX,
velocityY: velocityY,
radius: 20,
type: 'boulder',
launched: true
});
catapult.lastShot = currentTime;
if (playSound) playSound('jump'); // Boulder launch sound
console.log(`🪨 Catapult launched boulder towards Mario!`);
}
// Onager shoots stone rain (area attack)
else if (catapult.isOnager && distanceToMario < 800) {
// Create stone rain above Mario's area
const stoneCount = 8 + Math.floor(Math.random() * 5); // 8-12 stones
for (let i = 0; i < stoneCount; i++) {
const offsetX = (Math.random() - 0.5) * 400; // Spread 400px around Mario
stones.push({
x: mario.x + offsetX,
y: -50 - Math.random() * 100, // Start above screen
velocityX: (Math.random() - 0.5) * 2,
velocityY: 2 + Math.random() * 3,
width: 15 + Math.random() * 10,
height: 15 + Math.random() * 10,
type: 'stone',
rotation: Math.random() * Math.PI * 2
});
}
catapult.lastShot = currentTime;
if (playSound) playSound('enemy_defeat'); // Different sound for stone rain
console.log(`☄️ Onager launched stone rain (${stoneCount} stones)!`);
}
}
});
}
/**
* Update boulders
* @param {Array} boulders - Array of boulders
* @param {Object} mario - Mario object
* @param {Array} platforms - Platforms for collision
* @param {Array} walls - Walls for collision
* @param {Function} onImpact - Callback when boulder hits something
* @returns {Array} - Updated boulders array
*/
static updateBoulders(boulders, mario, platforms, walls, onImpact) {
const GRAVITY = 0.3;
const updatedBoulders = [];
boulders.forEach((boulder, index) => {
// Apply physics
boulder.velocityY += GRAVITY;
boulder.x += boulder.velocityX;
boulder.y += boulder.velocityY;
// Check collision with platforms
let hitPlatform = false;
platforms.forEach((platform, platformIndex) => {
if (this._isCollidingCircleRect(boulder, platform)) {
hitPlatform = true;
if (onImpact) {
onImpact(boulder, index, boulder.x, boulder.y, platform, platformIndex, 'platform');
}
}
});
// Check collision with walls
let hitWall = false;
walls.forEach((wall, wallIndex) => {
if (this._isCollidingCircleRect(boulder, wall)) {
hitWall = true;
if (onImpact) {
onImpact(boulder, index, boulder.x, boulder.y, wall, wallIndex, 'wall');
}
}
});
// Check collision with Mario
if (this._isCollidingCircleRect(boulder, mario)) {
if (onImpact) {
onImpact(boulder, index, boulder.x, boulder.y, mario, -1, 'mario');
}
return; // Remove boulder
}
// Remove if out of bounds or hit something
if (!hitPlatform && !hitWall && boulder.y < 1000) {
updatedBoulders.push(boulder);
}
});
return updatedBoulders;
}
/**
* Update stones (stone rain)
* @param {Array} stones - Array of stones
* @param {Object} mario - Mario object
* @param {Array} platforms - Platforms for collision
* @param {Function} onImpact - Callback when stone hits something
* @returns {Array} - Updated stones array
*/
static updateStones(stones, mario, platforms, onImpact) {
const GRAVITY = 0.5;
const updatedStones = [];
stones.forEach((stone, index) => {
// Apply physics
stone.velocityY += GRAVITY;
stone.x += stone.velocityX;
stone.y += stone.velocityY;
stone.rotation += 0.1;
// Check collision with platforms
let hitPlatform = false;
platforms.forEach(platform => {
if (this._isCollidingRectRect(stone, platform)) {
hitPlatform = true;
if (onImpact) {
onImpact(stone, index, 'platform');
}
}
});
// Check collision with Mario
if (this._isCollidingRectRect(stone, mario)) {
if (onImpact) {
onImpact(stone, index, 'mario');
}
return; // Remove stone
}
// Keep stone if not hit and still on screen
if (!hitPlatform && stone.y < 1000) {
updatedStones.push(stone);
}
});
return updatedStones;
}
/**
* Circle-Rectangle collision detection
*/
static _isCollidingCircleRect(circle, rect) {
const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width));
const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height));
const distanceX = circle.x - closestX;
const distanceY = circle.y - closestY;
const distanceSquared = distanceX * distanceX + distanceY * distanceY;
return distanceSquared < (circle.radius * circle.radius);
}
/**
* Rectangle-Rectangle collision detection
*/
static _isCollidingRectRect(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
}
export default Catapult;