class Game { constructor() { this.canvas = document.getElementById('gameCanvas'); this.ctx = this.canvas.getContext('2d'); this.ctx.imageSmoothingEnabled = false; this.spriteSheet = new Image(); this.backgroundMusic = new Audio(); this.player = null; this.backgroundElements = []; this.backgroundLayers = { far: [], // Elementos muy lejanos (nubes, montañas) mid: [], // Elementos medios (árboles, edificios) near: [] // Elementos cercanos (decoraciones, elementos interactivos) }; // Sistema de generación de nubes mejorado this.cloudSystem = new CloudSystem(this.canvas); // Sistema de objetos de fondo this.backgroundObjectSystem = new BackgroundObjectSystem(this.canvas, this); // Sistema de letras sincronizadas this.lyricsSystem = new LyricsSystem(); // Estado del juego this.gameState = 'playing'; // 'playing', 'gameOver', 'paused' // Definir tipos de elementos con sus configuraciones this.elementTypes = { floor: { renderMethod: 'sprite', defaultSpriteCoords: { x: 2, y: 430, w: 32, h: 32 }, layer: 'near' }, decoration: { renderMethod: 'sprite', defaultSpriteCoords: { x: 137, y: 104, w: 35, h: 26 }, layer: 'near' }, background: { renderMethod: 'color', color: '#4A90E2', layer: 'far' }, text: { renderMethod: 'text', font: '12px Arial', color: 'white', align: 'center', layer: 'near' }, sprite: { renderMethod: 'sprite', layer: 'near' }, cloud: { renderMethod: 'sprite', layer: 'far', defaultSpriteCoords: { x: 231, y: 112, w: 287, h: 148 }, autoMove: true, moveSpeed: 0.5 }, backgroundObject: { renderMethod: 'sprite', layer: 'mid', autoMove: true, moveSpeed: 0.1, // Definir las variantes de sprites disponibles spriteVariants: [ { x: 546, y: 264, w: 30, h: 21 }, { x: 546, y: 286, w: 30, h: 21 }, { x: 546, y: 308, w: 30, h: 25 }, { x: 546, y: 334, w: 30, h: 24 }, { x: 546, y: 359, w: 30, h: 21 }, { x: 545, y: 382, w: 31, h: 29 }, { x: 516, y: 264, w: 29, h: 17 }, { x: 516, y: 282, w: 29, h: 21 }, { x: 516, y: 304, w: 29, h: 18 }, { x: 516, y: 323, w: 29, h: 18 }, { x: 516, y: 342, w: 29, h: 19 }, { x: 516, y: 362, w: 29, h: 19 }, { x: 516, y: 382, w: 28, h: 29 }, { x: 491, y: 270, w: 24, h: 26 }, { x: 491, y: 297, w: 24, h: 26 }, { x: 491, y: 324, w: 24, h: 26 }, { x: 491, y: 353, w: 24, h: 26 }, { x: 489, y: 382, w: 26, h: 29 }, { x: 465, y: 310, w: 25, h: 23 }, { x: 465, y: 334, w: 25, h: 23 }, { x: 465, y: 358, w: 25, h: 23 }, { x: 465, y: 382, w: 11, h: 29 }, { x: 477, y: 382, w: 11, h: 29 } ] } }; this.init(); } init() { // Load assets this.spriteSheet.src = 'assets/sprites.png'; this.backgroundMusic.src = 'assets/music.mp3'; // Configure audio this.backgroundMusic.loop = true; this.backgroundMusic.volume = 0.05; // Create sprite processor for color key-out this.spriteProcessor = new SpriteProcessor(); // Wait for assets to load Promise.all([ new Promise(resolve => this.spriteSheet.onload = resolve), new Promise(resolve => this.backgroundMusic.oncanplaythrough = resolve) ]).then(() => { // Process sprite sheet to remove #00ffff background this.processedSpriteSheet = this.spriteProcessor.processSpriteSheet(this.spriteSheet, '#00ffff'); this.processedSpriteSheet.onload = () => { this.startGame(); }; }); // Handle audio context (for browsers that require user interaction) const startMusic = () => { if (this.backgroundMusic.paused && this.gameState === 'playing') { this.backgroundMusic.play().catch(e => console.log('Audio play failed:', e)); } }; this.canvas.addEventListener('click', startMusic); this.canvas.addEventListener('touchstart', startMusic, { passive: true }); // Agregar controles de teclado this.setupKeyboardControls(); // Inicializar controles táctiles para dispositivos móviles this.touchControls = new TouchControls(this.canvas, input); } // Método para agregar elementos de forma dinámica addElement(type, x, y, w, h, options = {}) { const elementConfig = this.elementTypes[type]; const layer = options.layer || elementConfig?.layer || 'near'; const element = { type, x, y, w, h, ...options }; // Agregar a la capa correspondiente if (this.backgroundLayers[layer]) { this.backgroundLayers[layer].push(element); } else { this.backgroundElements.push(element); } return element; } // Método para agregar elementos de piso addFloorTile(x, y) { return this.addElement('floor', x, y, 32, 32); } // Método para agregar decoraciones addDecoration(x, y, w = 35, h = 26) { return this.addElement('decoration', x, y, w, h); } // Método para agregar elementos de color sólido addColorElement(x, y, w, h, color, layer = 'far') { return this.addElement('background', x, y, w, h, { color, layer }); } // Método para agregar texto addText(x, y, text, options = {}) { return this.addElement('text', x, y, 0, 0, { text, ...options }); } // Método para agregar sprites personalizados addSprite(x, y, w, h, spriteCoords, options = {}) { return this.addElement('sprite', x, y, w, h, { spriteCoords, ...options }); } // Método para agregar cualquier sprite con coordenadas específicas addCustomSprite(type, x, y, w, h, spriteCoords, options = {}) { return this.addElement(type, x, y, w, h, { spriteCoords, ...options }); } startGame() { // Create player this.player = new Player(72, 88, this.processedSpriteSheet); // Crear elementos de fondo usando los nuevos métodos // Piso for (let i = 0; i < 10; i++) { this.addFloorTile(i * 32, 208); } // Inicializar sistema de nubes this.cloudSystem.init(this.processedSpriteSheet, this.backgroundLayers.far); // Inicializar sistema de objetos de fondo this.backgroundObjectSystem.init(this.processedSpriteSheet, this.backgroundLayers.mid); // Macarons this.addSprite(10, 183, 35, 26, { x: 137, y: 104, w: 35, h: 26 }); this.addSprite(42, 183, 35, 26, { x: 173, y: 104, w: 35, h: 26 }); this.addSprite(74, 183, 35, 26, { x: 137, y: 131, w: 35, h: 26 }); this.addSprite(106, 183, 35, 26, { x: 173, y: 131, w: 35, h: 26 }); // Candybar this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 }); // Cake this.addSprite(260, 134, 100, 75, { x: 461, y: 414, w: 100, h: 75 }); // Cat this.addSprite(185, 187, 20, 23, { x: 235, y: 87, w: 20, h: 23 }); this.addSprite(200, 187, 20, 23, { x: 389, y: 87, w: 16, h: 23 }); this.gameLoop(); } update() { if (this.gameState !== 'playing') return; if (this.player) { this.player.update(); } // Actualizar sistema de nubes this.cloudSystem.update(); // Actualizar sistema de objetos de fondo this.backgroundObjectSystem.update(); // Actualizar sistema de letras if (!this.backgroundMusic.paused) { this.lyricsSystem.update(this.backgroundMusic.currentTime); // Verificar si la canción ha terminado y detenerla if (this.lyricsSystem.isGameOver && !this.backgroundMusic.paused) { this.backgroundMusic.pause(); this.gameState = 'gameOver'; } } } // Método para renderizar un elemento individual renderElement(element) { const elementConfig = this.elementTypes[element.type]; if (!elementConfig) { console.warn(`Tipo de elemento desconocido: ${element.type}`); return; } switch (elementConfig.renderMethod) { case 'sprite': this.renderSpriteElement(element, elementConfig); break; case 'color': this.renderColorElement(element, elementConfig); break; case 'text': this.renderTextElement(element, elementConfig); break; default: console.warn(`Método de renderizado desconocido: ${elementConfig.renderMethod}`); } } renderSpriteElement(element, config) { // Usar coordenadas personalizadas si están especificadas, sino usar las por defecto const spriteCoords = element.spriteCoords || config.defaultSpriteCoords; if (!spriteCoords) { console.warn(`No se especificaron coordenadas de sprite para elemento tipo: ${element.type}`); return; } // Si el elemento tiene rotación, aplicar transformaciones if (element.rotation) { this.ctx.save(); // Calcular el centro del sprite para la rotación const centerX = element.x + element.w / 2; const centerY = element.y + element.h / 2; // Aplicar transformaciones this.ctx.translate(centerX, centerY); this.ctx.rotate(element.rotation); this.ctx.translate(-centerX, -centerY); } this.ctx.drawImage( this.processedSpriteSheet, spriteCoords.x, spriteCoords.y, spriteCoords.w, spriteCoords.h, element.x, element.y, element.w, element.h ); // Restaurar el contexto si se aplicó rotación if (element.rotation) { this.ctx.restore(); } } renderColorElement(element, config) { const color = element.color || config.color; this.ctx.fillStyle = color; this.ctx.fillRect(element.x, element.y, element.w, element.h); } renderTextElement(element, config) { this.ctx.fillStyle = element.color || config.color; this.ctx.font = element.font || config.font; this.ctx.textAlign = element.align || config.align; this.ctx.fillText(element.text, element.x, element.y); } draw() { // Clear canvas con degradado const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height); gradient.addColorStop(0, '#d9e6ff'); // Color superior gradient.addColorStop(1, '#eee5ff'); // Color inferior this.ctx.fillStyle = gradient; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Renderizar elementos por capas (de lejos a cerca) // Capa lejana (nubes, montañas) this.backgroundLayers.far.forEach(element => { this.renderElement(element); }); // Capa media (árboles, edificios) this.backgroundLayers.mid.forEach(element => { this.renderElement(element); }); // Capa cercana (decoraciones, elementos interactivos) this.backgroundLayers.near.forEach(element => { this.renderElement(element); }); // Elementos legacy (para compatibilidad) this.backgroundElements.forEach(element => { this.renderElement(element); }); // Draw player if (this.player) { this.player.draw(this.ctx); } // Dibujar letras si están activadas this.lyricsSystem.draw(this.ctx, this.canvas); // Draw instructions if music hasn't started if (this.backgroundMusic.paused && this.gameState === 'playing') { this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillStyle = 'white'; this.ctx.font = '10px Courier New'; this.ctx.textAlign = 'center'; this.ctx.fillText('Click to start music', 160, 100); this.ctx.fillText('WASD or ↑↓←→ to move', 160, 115); this.ctx.fillText('L - Show/Hide lyrics', 160, 130); this.ctx.fillText('R - Restart game', 160, 145); } // Dibujar pantalla de fin de juego if (this.lyricsSystem.gameOverScreen) { this.drawGameOverScreen(); } } gameLoop() { this.update(); this.draw(); requestAnimationFrame(() => this.gameLoop()); } setupKeyboardControls() { document.addEventListener('keydown', (e) => { switch(e.key.toLowerCase()) { case 'l': this.lyricsSystem.toggleLyrics(); break; case 'r': this.restartGame(); break; case 'g': // Shortcut para desarrollo: simular fin de canción if (e.ctrlKey) { this.lyricsSystem.simulateGameOver(); } break; } }); } drawGameOverScreen() { // Fondo negro translúcido this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Texto de fin de juego this.ctx.fillStyle = 'white'; this.ctx.font = '20px Courier New'; this.ctx.textAlign = 'center'; this.ctx.fillText('Song finished!', 160, 100); this.ctx.fillText('Want to play again?', 160, 125); // Botones this.ctx.font = '12px Courier New'; this.ctx.fillText('Press R to restart', 160, 175); this.ctx.fillText('or reload the page', 160, 190); } restartGame() { // Detener música this.backgroundMusic.pause(); this.backgroundMusic.currentTime = 0; // Reiniciar sistemas this.lyricsSystem.reset(); // Reiniciar estado del juego this.gameState = 'playing'; // Limpiar elementos de fondo this.backgroundLayers.far = []; this.backgroundLayers.mid = []; this.backgroundLayers.near = []; this.backgroundElements = []; // Reiniciar sistemas this.cloudSystem.init(this.processedSpriteSheet, this.backgroundLayers.far); this.backgroundObjectSystem.init(this.processedSpriteSheet, this.backgroundLayers.mid); // Reiniciar jugador this.player = new Player(144, 176, this.processedSpriteSheet); // Recrear elementos de fondo this.recreateBackgroundElements(); } recreateBackgroundElements() { // Piso - adjusted for 320x240 canvas for (let i = 0; i < 10; i++) { this.addFloorTile(i * 32, 208); // 240-32=208 } // Macarons - original positions this.addSprite(10, 183, 35, 26, { x: 137, y: 104, w: 35, h: 26 }); this.addSprite(42, 183, 35, 26, { x: 173, y: 104, w: 35, h: 26 }); this.addSprite(74, 183, 35, 26, { x: 137, y: 131, w: 35, h: 26 }); this.addSprite(106, 183, 35, 26, { x: 173, y: 131, w: 35, h: 26 }); // Candybar - original size and position this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 }); // Cake - original size and position this.addSprite(260, 134, 100, 75, { x: 461, y: 414, w: 100, h: 75 }); // Cat - original positions this.addSprite(185, 187, 20, 23, { x: 235, y: 87, w: 20, h: 23 }); this.addSprite(200, 187, 20, 23, { x: 389, y: 87, w: 16, h: 23 }); } } // Clase para manejar el sistema de nubes de forma modular class CloudSystem { constructor(canvas) { this.canvas = canvas; this.spriteSheet = null; this.cloudLayer = null; this.frameCount = 0; // Configuración para canvas 320x240 this.config = { spawnRate: 0.1, // Probabilidad de spawn por frame maxClouds: 2, // Máximo número de nubes en pantalla speedRange: { min: 0.125, max: 0.1875 }, // Velocidad de movimiento (original) yRange: { min: 1, max: 150 }, // Rango de altura (original) spriteCoords: { x: 231, y: 112, w: 287, h: 148 }, size: { w: 287, h: 148 }, // Tamaño original de nube minSpacing: 125, // Espaciado mínimo entre nubes (original) minTimeBetweenSpawns: 60 // Frames mínimos entre apariciones }; this.lastSpawnTime = 0; } init(spriteSheet, cloudLayer) { this.spriteSheet = spriteSheet; this.cloudLayer = cloudLayer; // Generar nubes iniciales para que el fondo no se vea vacío this.spawnInitialClouds(); } spawnInitialClouds() { // Generar 2-3 nubes iniciales en posiciones aleatorias const initialCount = Math.floor(Math.random() * 2) + 2; for (let i = 0; i < initialCount; i++) { const x = Math.random() * this.canvas.width; const y = this.getRandomY(); const speed = this.getRandomSpeed(); this.createCloud(x, y, speed); } } update() { this.frameCount++; // Limpiar nubes que salieron de la pantalla this.cleanupOffscreenClouds(); // Intentar generar nueva nube if (this.shouldSpawnCloud()) { this.spawnCloud(); } // Mover nubes existentes this.moveClouds(); } shouldSpawnCloud() { // Verificar tiempo mínimo entre apariciones if (this.frameCount - this.lastSpawnTime < this.config.minTimeBetweenSpawns) { return false; } // Verificar número máximo de nubes const cloudCount = this.cloudLayer.filter(e => e.type === 'cloud').length; if (cloudCount >= this.config.maxClouds) { return false; } // Verificar probabilidad de spawn if (Math.random() > this.config.spawnRate) { return false; } // Verificar espaciado mínimo const hasSpacing = this.hasEnoughSpacing(); // Debug: mostrar información cada 60 frames (1 segundo) if (this.frameCount % 60 === 0) { console.log(`Clouds: ${cloudCount}/${this.config.maxClouds}, Spacing: ${hasSpacing}, Time: ${this.frameCount - this.lastSpawnTime}`); } return hasSpacing; } hasEnoughSpacing() { const existingClouds = this.cloudLayer.filter(e => e.type === 'cloud'); // Si no hay nubes, siempre se puede generar if (existingClouds.length === 0) { return true; } // Verificar que no haya nubes muy cerca del borde derecho for (const cloud of existingClouds) { const distanceFromRight = this.canvas.width - cloud.x; if (distanceFromRight < this.config.minSpacing) { return false; } } return true; } spawnCloud() { const y = this.getRandomY(); const speed = this.getRandomSpeed(); // Generar desde la derecha de la pantalla const x = this.canvas.width; this.createCloud(x, y, speed); this.lastSpawnTime = this.frameCount; console.log(`Nueva nube generada en (${x}, ${y}) con velocidad ${speed}`); } createCloud(x, y, speed) { const cloud = { type: 'cloud', x: x, y: y, w: this.config.size.w, h: this.config.size.h, spriteCoords: this.config.spriteCoords, moveSpeed: speed, layer: 'far' }; this.cloudLayer.push(cloud); } moveClouds() { this.cloudLayer.forEach(cloud => { if (cloud.type === 'cloud' && cloud.moveSpeed) { cloud.x -= cloud.moveSpeed; } }); } cleanupOffscreenClouds() { for (let i = this.cloudLayer.length - 1; i >= 0; i--) { const element = this.cloudLayer[i]; if (element.type === 'cloud' && element.x + element.w < 0) { this.cloudLayer.splice(i, 1); } } } getRandomY() { return Math.random() * (this.config.yRange.max - this.config.yRange.min) + this.config.yRange.min; } getRandomSpeed() { return Math.random() * (this.config.speedRange.max - this.config.speedRange.min) + this.config.speedRange.min; } } // Clase para manejar el sistema de objetos de fondo class BackgroundObjectSystem { constructor(canvas, game) { this.canvas = canvas; this.game = game; this.spriteSheet = null; this.objectLayer = null; this.frameCount = 0; // Configuración para objetos de fondo (320x240 canvas) this.config = { spawnRate: 0.15, // Probabilidad de spawn por frame maxObjects: 150, // Máximo número de objetos en pantalla (reducido) speedRange: { min: 0.0625, max: 0.125 }, // Velocidad de movimiento (original) yRange: { min: 12, max: 225 }, // Rango de altura (original) minSpacing: 150, // Espaciado mínimo entre grupos (original) minTimeBetweenSpawns: 120, // Frames mínimos entre apariciones groupSize: { min: 4, max: 8 }, // Tamaño del grupo de objetos (original) groupSpacing: { min: 8, max: 40 } // Espaciado entre objetos en el grupo (original) }; this.lastSpawnTime = 0; } init(spriteSheet, objectLayer) { this.spriteSheet = spriteSheet; this.objectLayer = objectLayer; // Generar objetos iniciales this.spawnInitialObjects(); } spawnInitialObjects() { // Generar objetos en múltiples filas para mejor distribución const layers = 3; // Número de capas de profundidad const groupsPerLayer = 4; // Grupos por capa for (let layer = 0; layer < layers; layer++) { for (let i = 0; i < groupsPerLayer; i++) { // Distribuir horizontalmente const x = (i * this.canvas.width / groupsPerLayer) + (Math.random() * 150); // Crear diferentes niveles de altura const baseY = 100 + (layer * 120); // Capas a diferentes alturas const y = baseY + (Math.random() * 80 - 40); // Variación aleatoria // Velocidad ligeramente diferente por capa para efecto parallax const baseSpeed = this.getRandomSpeed(); const speed = baseSpeed * (1 + layer * 0.1); this.createObjectGroup(x, y, speed); } } } update() { this.frameCount++; // Limpiar objetos que salieron de la pantalla this.cleanupOffscreenObjects(); // Intentar generar nuevo grupo if (this.shouldSpawnObjectGroup()) { this.spawnObjectGroup(); } // Mover objetos existentes this.moveObjects(); } shouldSpawnObjectGroup() { // Verificar tiempo mínimo entre apariciones if (this.frameCount - this.lastSpawnTime < this.config.minTimeBetweenSpawns) { return false; } // Verificar número máximo de objetos const objectCount = this.objectLayer.filter(e => e.type === 'backgroundObject').length; if (objectCount >= this.config.maxObjects) { return false; } // Verificar probabilidad de spawn if (Math.random() > this.config.spawnRate) { return false; } // Verificar espaciado mínimo const hasSpacing = this.hasEnoughSpacing(); return hasSpacing; } hasEnoughSpacing() { const existingObjects = this.objectLayer.filter(e => e.type === 'backgroundObject'); // Si no hay objetos, siempre se puede generar if (existingObjects.length === 0) { return true; } // Verificar que no haya objetos muy cerca del borde derecho for (const obj of existingObjects) { const distanceFromRight = this.canvas.width - obj.x; if (distanceFromRight < this.config.minSpacing) { return false; } } return true; } spawnObjectGroup() { // Generar múltiples grupos a diferentes alturas simultáneamente const numLayers = Math.floor(Math.random() * 2) + 2; // 2-3 capas for (let i = 0; i < numLayers; i++) { // Diferentes alturas para cada capa const baseY = 100 + (i * 150); const y = baseY + (Math.random() * 100 - 50); // Velocidad ligeramente diferente por capa const baseSpeed = this.getRandomSpeed(); const speed = baseSpeed * (1 + i * 0.15); // Generar desde la derecha de la pantalla con ligera variación const x = this.canvas.width + (Math.random() * 100); this.createObjectGroup(x, y, speed); } this.lastSpawnTime = this.frameCount; console.log(`Nuevos grupos de objetos generados en múltiples capas`); } createObjectGroup(x, y, speed) { const groupSize = Math.floor(Math.random() * (this.config.groupSize.max - this.config.groupSize.min + 1)) + this.config.groupSize.min; // Obtener las variantes de sprites desde elementTypes const spriteVariants = this.game.elementTypes.backgroundObject.spriteVariants; let currentX = x; for (let i = 0; i < groupSize; i++) { // Seleccionar sprite aleatorio const spriteVariant = spriteVariants[Math.floor(Math.random() * spriteVariants.length)]; // Aplicar rotación aleatoria const rotation = (Math.random() - 0.5) * 0.8; // ±0.4 radianes // Aplicar dispersión vertical aleatoria const yOffset = (Math.random() - 0.5) * 40; // ±20 píxeles // Use original size for background objects const object = { type: 'backgroundObject', x: currentX, y: y + yOffset, w: spriteVariant.w, h: spriteVariant.h, spriteCoords: spriteVariant, moveSpeed: speed, rotation: rotation, layer: 'mid' }; this.objectLayer.push(object); // Calcular siguiente posición con espaciado aleatorio const spacing = Math.random() * (this.config.groupSpacing.max - this.config.groupSpacing.min) + this.config.groupSpacing.min; currentX += spriteVariant.w + spacing; } } moveObjects() { this.objectLayer.forEach(obj => { if (obj.type === 'backgroundObject' && obj.moveSpeed) { obj.x -= obj.moveSpeed; } }); } cleanupOffscreenObjects() { for (let i = this.objectLayer.length - 1; i >= 0; i--) { const element = this.objectLayer[i]; if (element.type === 'backgroundObject' && element.x + element.w < 0) { this.objectLayer.splice(i, 1); } } } getRandomY() { return Math.random() * (this.config.yRange.max - this.config.yRange.min) + this.config.yRange.min; } getRandomSpeed() { return Math.random() * (this.config.speedRange.max - this.config.speedRange.min) + this.config.speedRange.min; } } // Initialize game when DOM is loaded window.addEventListener('DOMContentLoaded', () => { new Game(); });