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) this.canvas.addEventListener('click', () => { if (this.backgroundMusic.paused && this.gameState === 'playing') { this.backgroundMusic.play().catch(e => console.log('Audio play failed:', e)); } }); // Agregar controles de teclado this.setupKeyboardControls(); } // 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(144, 176, 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); } } // 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 = '8px Courier New'; this.ctx.textAlign = 'center'; this.ctx.fillText('Click para iniciar música', 160, 120); this.ctx.fillText('AD o ← → para moverte', 160, 130); this.ctx.fillText('L - Mostrar/Ocultar letras', 160, 140); this.ctx.fillText('R - Reiniciar juego', 160, 150); // this.ctx.fillText('Ctrl+G - Simular fin de canción', 160, 160); } // 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 = '16px Courier New'; this.ctx.textAlign = 'center'; this.ctx.fillText('¡Canción terminada!', 160, 100); this.ctx.fillText('¿Quieres jugar otra vez?', 160, 120); // Botones this.ctx.font = '12px Courier New'; this.ctx.fillText('Presiona R para reiniciar', 160, 150); this.ctx.fillText('o recarga la página', 160, 165); } 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 for (let i = 0; i < 10; i++) { this.addFloorTile(i * 32, 208); } // 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 }); } } // 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 simplificada this.config = { spawnRate: 0.1, // Probabilidad de spawn por frame (aumentada) maxClouds: 2, // Máximo número de nubes en pantalla (aumentado) speedRange: { min: 0.1, max: 0.15 }, // Velocidad de movimiento yRange: { min: 1, max: 120 }, // Rango de altura spriteCoords: { x: 231, y: 112, w: 287, h: 148 }, size: { w: 287, h: 148 }, minSpacing: 100, // Espaciado mínimo entre nubes (reducido) minTimeBetweenSpawns: 60 // Frames mínimos entre apariciones (reducido) }; 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 this.config = { spawnRate: 0.3, // Probabilidad de spawn por frame (aumentada) maxObjects: 300, // Máximo número de objetos en pantalla (aumentado) speedRange: { min: 0.05, max: 0.1 }, // Velocidad de movimiento yRange: { min: 10, max: 180 }, // Rango de altura (expandido) minSpacing: 80, // Espaciado mínimo entre grupos (reducido) minTimeBetweenSpawns: 60, // Frames mínimos entre apariciones (reducido) groupSize: { min: 15, max: 25 }, // Tamaño del grupo de objetos (reducido) groupSpacing: { min: 5, max: 40 } // Espaciado entre objetos en el grupo (ajustado) }; this.lastSpawnTime = 0; } init(spriteSheet, objectLayer) { this.spriteSheet = spriteSheet; this.objectLayer = objectLayer; // Generar objetos iniciales this.spawnInitialObjects(); } spawnInitialObjects() { // Generar 3-5 grupos iniciales para llenar la pantalla const initialGroups = Math.floor(Math.random() * 3) + 3; for (let i = 0; i < initialGroups; i++) { // Distribuir los grupos iniciales por toda la pantalla const x = (i * this.canvas.width / initialGroups) + (Math.random() * 100); const y = this.getRandomY(); const speed = this.getRandomSpeed(); 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() { const y = this.getRandomY(); const speed = this.getRandomSpeed(); // Generar desde la derecha de la pantalla const x = this.canvas.width; this.createObjectGroup(x, y, speed); this.lastSpawnTime = this.frameCount; console.log(`Nuevo grupo de objetos generado en (${x}, ${y}) con velocidad ${speed}`); } 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 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(); });