|
|
@@ -0,0 +1,832 @@
|
|
|
+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();
|
|
|
+});
|