game.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. class Game {
  2. constructor() {
  3. this.canvas = document.getElementById('gameCanvas');
  4. this.ctx = this.canvas.getContext('2d');
  5. this.ctx.imageSmoothingEnabled = false;
  6. this.spriteSheet = new Image();
  7. this.backgroundMusic = new Audio();
  8. this.player = null;
  9. this.backgroundElements = [];
  10. this.backgroundLayers = {
  11. far: [], // Elementos muy lejanos (nubes, montañas)
  12. mid: [], // Elementos medios (árboles, edificios)
  13. near: [] // Elementos cercanos (decoraciones, elementos interactivos)
  14. };
  15. // Sistema de generación de nubes mejorado
  16. this.cloudSystem = new CloudSystem(this.canvas);
  17. // Sistema de objetos de fondo
  18. this.backgroundObjectSystem = new BackgroundObjectSystem(this.canvas, this);
  19. // Sistema de letras sincronizadas
  20. this.lyricsSystem = new LyricsSystem();
  21. // Estado del juego
  22. this.gameState = 'playing'; // 'playing', 'gameOver', 'paused'
  23. // Definir tipos de elementos con sus configuraciones
  24. this.elementTypes = {
  25. floor: {
  26. renderMethod: 'sprite',
  27. defaultSpriteCoords: { x: 2, y: 430, w: 32, h: 32 },
  28. layer: 'near'
  29. },
  30. decoration: {
  31. renderMethod: 'sprite',
  32. defaultSpriteCoords: { x: 137, y: 104, w: 35, h: 26 },
  33. layer: 'near'
  34. },
  35. background: {
  36. renderMethod: 'color',
  37. color: '#4A90E2',
  38. layer: 'far'
  39. },
  40. text: {
  41. renderMethod: 'text',
  42. font: '12px Arial',
  43. color: 'white',
  44. align: 'center',
  45. layer: 'near'
  46. },
  47. sprite: {
  48. renderMethod: 'sprite',
  49. layer: 'near'
  50. },
  51. cloud: {
  52. renderMethod: 'sprite',
  53. layer: 'far',
  54. defaultSpriteCoords: { x: 231, y: 112, w: 287, h: 148 },
  55. autoMove: true,
  56. moveSpeed: 0.5
  57. },
  58. backgroundObject: {
  59. renderMethod: 'sprite',
  60. layer: 'mid',
  61. autoMove: true,
  62. moveSpeed: 0.1,
  63. // Definir las variantes de sprites disponibles
  64. spriteVariants: [
  65. { x: 546, y: 264, w: 30, h: 21 },
  66. { x: 546, y: 286, w: 30, h: 21 },
  67. { x: 546, y: 308, w: 30, h: 25 },
  68. { x: 546, y: 334, w: 30, h: 24 },
  69. { x: 546, y: 359, w: 30, h: 21 },
  70. { x: 545, y: 382, w: 31, h: 29 },
  71. { x: 516, y: 264, w: 29, h: 17 },
  72. { x: 516, y: 282, w: 29, h: 21 },
  73. { x: 516, y: 304, w: 29, h: 18 },
  74. { x: 516, y: 323, w: 29, h: 18 },
  75. { x: 516, y: 342, w: 29, h: 19 },
  76. { x: 516, y: 362, w: 29, h: 19 },
  77. { x: 516, y: 382, w: 28, h: 29 },
  78. { x: 491, y: 270, w: 24, h: 26 },
  79. { x: 491, y: 297, w: 24, h: 26 },
  80. { x: 491, y: 324, w: 24, h: 26 },
  81. { x: 491, y: 353, w: 24, h: 26 },
  82. { x: 489, y: 382, w: 26, h: 29 },
  83. { x: 465, y: 310, w: 25, h: 23 },
  84. { x: 465, y: 334, w: 25, h: 23 },
  85. { x: 465, y: 358, w: 25, h: 23 },
  86. { x: 465, y: 382, w: 11, h: 29 },
  87. { x: 477, y: 382, w: 11, h: 29 }
  88. ]
  89. }
  90. };
  91. this.init();
  92. }
  93. init() {
  94. // Load assets
  95. this.spriteSheet.src = 'assets/sprites.png';
  96. this.backgroundMusic.src = 'assets/music.mp3';
  97. // Configure audio
  98. this.backgroundMusic.loop = true;
  99. this.backgroundMusic.volume = 0.05;
  100. // Create sprite processor for color key-out
  101. this.spriteProcessor = new SpriteProcessor();
  102. // Wait for assets to load
  103. Promise.all([
  104. new Promise(resolve => this.spriteSheet.onload = resolve),
  105. new Promise(resolve => this.backgroundMusic.oncanplaythrough = resolve)
  106. ]).then(() => {
  107. // Process sprite sheet to remove #00ffff background
  108. this.processedSpriteSheet = this.spriteProcessor.processSpriteSheet(this.spriteSheet, '#00ffff');
  109. this.processedSpriteSheet.onload = () => {
  110. this.startGame();
  111. };
  112. });
  113. // Handle audio context (for browsers that require user interaction)
  114. const startMusic = () => {
  115. if (this.backgroundMusic.paused && this.gameState === 'playing') {
  116. this.backgroundMusic.play().catch(e => console.log('Audio play failed:', e));
  117. }
  118. };
  119. this.canvas.addEventListener('click', startMusic);
  120. this.canvas.addEventListener('touchstart', startMusic, { passive: true });
  121. // Agregar controles de teclado
  122. this.setupKeyboardControls();
  123. // Inicializar controles táctiles para dispositivos móviles
  124. this.touchControls = new TouchControls(this.canvas, input);
  125. }
  126. // Método para agregar elementos de forma dinámica
  127. addElement(type, x, y, w, h, options = {}) {
  128. const elementConfig = this.elementTypes[type];
  129. const layer = options.layer || elementConfig?.layer || 'near';
  130. const element = {
  131. type,
  132. x,
  133. y,
  134. w,
  135. h,
  136. ...options
  137. };
  138. // Agregar a la capa correspondiente
  139. if (this.backgroundLayers[layer]) {
  140. this.backgroundLayers[layer].push(element);
  141. } else {
  142. this.backgroundElements.push(element);
  143. }
  144. return element;
  145. }
  146. // Método para agregar elementos de piso
  147. addFloorTile(x, y) {
  148. return this.addElement('floor', x, y, 32, 32);
  149. }
  150. // Método para agregar decoraciones
  151. addDecoration(x, y, w = 35, h = 26) {
  152. return this.addElement('decoration', x, y, w, h);
  153. }
  154. // Método para agregar elementos de color sólido
  155. addColorElement(x, y, w, h, color, layer = 'far') {
  156. return this.addElement('background', x, y, w, h, { color, layer });
  157. }
  158. // Método para agregar texto
  159. addText(x, y, text, options = {}) {
  160. return this.addElement('text', x, y, 0, 0, { text, ...options });
  161. }
  162. // Método para agregar sprites personalizados
  163. addSprite(x, y, w, h, spriteCoords, options = {}) {
  164. return this.addElement('sprite', x, y, w, h, {
  165. spriteCoords,
  166. ...options
  167. });
  168. }
  169. // Método para agregar cualquier sprite con coordenadas específicas
  170. addCustomSprite(type, x, y, w, h, spriteCoords, options = {}) {
  171. return this.addElement(type, x, y, w, h, {
  172. spriteCoords,
  173. ...options
  174. });
  175. }
  176. startGame() {
  177. // Create player (scaled for 800x600 canvas)
  178. this.player = new Player(360, 500, this.processedSpriteSheet);
  179. // Crear elementos de fondo usando los nuevos métodos
  180. // Piso (scaled and extended for wider canvas) - moved down to fill bottom space
  181. for (let i = 0; i < 26; i++) {
  182. this.addFloorTile(i * 32, 568); // Moved from 520 to 568 (600-32=568)
  183. }
  184. // Inicializar sistema de nubes
  185. this.cloudSystem.init(this.processedSpriteSheet, this.backgroundLayers.far);
  186. // Inicializar sistema de objetos de fondo
  187. this.backgroundObjectSystem.init(this.processedSpriteSheet, this.backgroundLayers.mid);
  188. // Macarons (scaled positions) - adjusted Y position
  189. this.addSprite(25, 518, 70, 52, { x: 137, y: 104, w: 35, h: 26 });
  190. this.addSprite(105, 518, 70, 52, { x: 173, y: 104, w: 35, h: 26 });
  191. this.addSprite(185, 518, 70, 52, { x: 137, y: 131, w: 35, h: 26 });
  192. this.addSprite(265, 518, 70, 52, { x: 173, y: 131, w: 35, h: 26 });
  193. // Candybar (scaled) - adjusted to ground level
  194. this.addSprite(500, 410, 96, 158, { x: 179, y: 438, w: 48, h: 79 });
  195. // Cake (scaled) - adjusted to ground level
  196. this.addSprite(650, 418, 200, 150, { x: 461, y: 414, w: 100, h: 75 });
  197. // Cat (scaled) - adjusted Y position
  198. this.addSprite(463, 528, 40, 46, { x: 235, y: 87, w: 20, h: 23 });
  199. this.addSprite(500, 528, 40, 46, { x: 389, y: 87, w: 16, h: 23 });
  200. this.gameLoop();
  201. }
  202. update() {
  203. if (this.gameState !== 'playing') return;
  204. if (this.player) {
  205. this.player.update();
  206. }
  207. // Actualizar sistema de nubes
  208. this.cloudSystem.update();
  209. // Actualizar sistema de objetos de fondo
  210. this.backgroundObjectSystem.update();
  211. // Actualizar sistema de letras
  212. if (!this.backgroundMusic.paused) {
  213. this.lyricsSystem.update(this.backgroundMusic.currentTime);
  214. }
  215. }
  216. // Método para renderizar un elemento individual
  217. renderElement(element) {
  218. const elementConfig = this.elementTypes[element.type];
  219. if (!elementConfig) {
  220. console.warn(`Tipo de elemento desconocido: ${element.type}`);
  221. return;
  222. }
  223. switch (elementConfig.renderMethod) {
  224. case 'sprite':
  225. this.renderSpriteElement(element, elementConfig);
  226. break;
  227. case 'color':
  228. this.renderColorElement(element, elementConfig);
  229. break;
  230. case 'text':
  231. this.renderTextElement(element, elementConfig);
  232. break;
  233. default:
  234. console.warn(`Método de renderizado desconocido: ${elementConfig.renderMethod}`);
  235. }
  236. }
  237. renderSpriteElement(element, config) {
  238. // Usar coordenadas personalizadas si están especificadas, sino usar las por defecto
  239. const spriteCoords = element.spriteCoords || config.defaultSpriteCoords;
  240. if (!spriteCoords) {
  241. console.warn(`No se especificaron coordenadas de sprite para elemento tipo: ${element.type}`);
  242. return;
  243. }
  244. // Si el elemento tiene rotación, aplicar transformaciones
  245. if (element.rotation) {
  246. this.ctx.save();
  247. // Calcular el centro del sprite para la rotación
  248. const centerX = element.x + element.w / 2;
  249. const centerY = element.y + element.h / 2;
  250. // Aplicar transformaciones
  251. this.ctx.translate(centerX, centerY);
  252. this.ctx.rotate(element.rotation);
  253. this.ctx.translate(-centerX, -centerY);
  254. }
  255. this.ctx.drawImage(
  256. this.processedSpriteSheet,
  257. spriteCoords.x, spriteCoords.y, spriteCoords.w, spriteCoords.h,
  258. element.x, element.y, element.w, element.h
  259. );
  260. // Restaurar el contexto si se aplicó rotación
  261. if (element.rotation) {
  262. this.ctx.restore();
  263. }
  264. }
  265. renderColorElement(element, config) {
  266. const color = element.color || config.color;
  267. this.ctx.fillStyle = color;
  268. this.ctx.fillRect(element.x, element.y, element.w, element.h);
  269. }
  270. renderTextElement(element, config) {
  271. this.ctx.fillStyle = element.color || config.color;
  272. this.ctx.font = element.font || config.font;
  273. this.ctx.textAlign = element.align || config.align;
  274. this.ctx.fillText(element.text, element.x, element.y);
  275. }
  276. draw() {
  277. // Clear canvas con degradado
  278. const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
  279. gradient.addColorStop(0, '#d9e6ff'); // Color superior
  280. gradient.addColorStop(1, '#eee5ff'); // Color inferior
  281. this.ctx.fillStyle = gradient;
  282. this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  283. // Renderizar elementos por capas (de lejos a cerca)
  284. // Capa lejana (nubes, montañas)
  285. this.backgroundLayers.far.forEach(element => {
  286. this.renderElement(element);
  287. });
  288. // Capa media (árboles, edificios)
  289. this.backgroundLayers.mid.forEach(element => {
  290. this.renderElement(element);
  291. });
  292. // Capa cercana (decoraciones, elementos interactivos)
  293. this.backgroundLayers.near.forEach(element => {
  294. this.renderElement(element);
  295. });
  296. // Elementos legacy (para compatibilidad)
  297. this.backgroundElements.forEach(element => {
  298. this.renderElement(element);
  299. });
  300. // Draw player
  301. if (this.player) {
  302. this.player.draw(this.ctx);
  303. }
  304. // Dibujar letras si están activadas
  305. this.lyricsSystem.draw(this.ctx, this.canvas);
  306. // Draw instructions if music hasn't started
  307. if (this.backgroundMusic.paused && this.gameState === 'playing') {
  308. this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
  309. this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  310. this.ctx.fillStyle = 'white';
  311. this.ctx.font = '20px Courier New';
  312. this.ctx.textAlign = 'center';
  313. this.ctx.fillText('Click to start music', 400, 300);
  314. this.ctx.fillText('AD or ← → to move', 400, 325);
  315. this.ctx.fillText('L - Show/Hide lyrics', 400, 350);
  316. this.ctx.fillText('R - Restart game', 400, 375);
  317. // this.ctx.fillText('Ctrl+G - Simular fin de canción', 160, 160);
  318. }
  319. // Dibujar pantalla de fin de juego
  320. if (this.lyricsSystem.gameOverScreen) {
  321. this.drawGameOverScreen();
  322. }
  323. }
  324. gameLoop() {
  325. this.update();
  326. this.draw();
  327. requestAnimationFrame(() => this.gameLoop());
  328. }
  329. setupKeyboardControls() {
  330. document.addEventListener('keydown', (e) => {
  331. switch(e.key.toLowerCase()) {
  332. case 'l':
  333. this.lyricsSystem.toggleLyrics();
  334. break;
  335. case 'r':
  336. this.restartGame();
  337. break;
  338. case 'g':
  339. // Shortcut para desarrollo: simular fin de canción
  340. if (e.ctrlKey) {
  341. this.lyricsSystem.simulateGameOver();
  342. }
  343. break;
  344. }
  345. });
  346. }
  347. drawGameOverScreen() {
  348. // Fondo negro translúcido
  349. this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
  350. this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  351. // Texto de fin de juego
  352. this.ctx.fillStyle = 'white';
  353. this.ctx.font = '40px Courier New';
  354. this.ctx.textAlign = 'center';
  355. this.ctx.fillText('Song finished!', 400, 250);
  356. this.ctx.fillText('Want to play again?', 400, 300);
  357. // Botones
  358. this.ctx.font = '24px Courier New';
  359. this.ctx.fillText('Press R to restart', 400, 375);
  360. this.ctx.fillText('or reload the page', 400, 412);
  361. }
  362. restartGame() {
  363. // Detener música
  364. this.backgroundMusic.pause();
  365. this.backgroundMusic.currentTime = 0;
  366. // Reiniciar sistemas
  367. this.lyricsSystem.reset();
  368. // Reiniciar estado del juego
  369. this.gameState = 'playing';
  370. // Limpiar elementos de fondo
  371. this.backgroundLayers.far = [];
  372. this.backgroundLayers.mid = [];
  373. this.backgroundLayers.near = [];
  374. this.backgroundElements = [];
  375. // Reiniciar sistemas
  376. this.cloudSystem.init(this.processedSpriteSheet, this.backgroundLayers.far);
  377. this.backgroundObjectSystem.init(this.processedSpriteSheet, this.backgroundLayers.mid);
  378. // Reiniciar jugador
  379. this.player = new Player(144, 176, this.processedSpriteSheet);
  380. // Recrear elementos de fondo
  381. this.recreateBackgroundElements();
  382. }
  383. recreateBackgroundElements() {
  384. // Piso (scaled and extended for wider canvas) - moved down to fill bottom space
  385. for (let i = 0; i < 26; i++) {
  386. this.addFloorTile(i * 32, 568); // Moved from 520 to 568 (600-32=568)
  387. }
  388. // Macarons (scaled positions) - adjusted Y position
  389. this.addSprite(25, 518, 70, 52, { x: 137, y: 104, w: 35, h: 26 });
  390. this.addSprite(105, 518, 70, 52, { x: 173, y: 104, w: 35, h: 26 });
  391. this.addSprite(185, 518, 70, 52, { x: 137, y: 131, w: 35, h: 26 });
  392. this.addSprite(265, 518, 70, 52, { x: 173, y: 131, w: 35, h: 26 });
  393. // Candybar (scaled) - adjusted to ground level
  394. this.addSprite(500, 410, 96, 158, { x: 179, y: 438, w: 48, h: 79 });
  395. // Cake (scaled) - adjusted to ground level
  396. this.addSprite(650, 418, 200, 150, { x: 461, y: 414, w: 100, h: 75 });
  397. // Cat (scaled) - adjusted Y position
  398. this.addSprite(463, 528, 40, 46, { x: 235, y: 87, w: 20, h: 23 });
  399. this.addSprite(500, 528, 40, 46, { x: 389, y: 87, w: 16, h: 23 });
  400. }
  401. }
  402. // Clase para manejar el sistema de nubes de forma modular
  403. class CloudSystem {
  404. constructor(canvas) {
  405. this.canvas = canvas;
  406. this.spriteSheet = null;
  407. this.cloudLayer = null;
  408. this.frameCount = 0;
  409. // Configuración simplificada (scaled for 800x600 canvas)
  410. this.config = {
  411. spawnRate: 0.1, // Probabilidad de spawn por frame (aumentada)
  412. maxClouds: 3, // Máximo número de nubes en pantalla (aumentado)
  413. speedRange: { min: 0.25, max: 0.375 }, // Velocidad de movimiento (scaled)
  414. yRange: { min: 2, max: 300 }, // Rango de altura (scaled)
  415. spriteCoords: { x: 231, y: 112, w: 287, h: 148 },
  416. size: { w: 574, h: 296 }, // Scaled cloud size
  417. minSpacing: 250, // Espaciado mínimo entre nubes (scaled)
  418. minTimeBetweenSpawns: 60 // Frames mínimos entre apariciones (reducido)
  419. };
  420. this.lastSpawnTime = 0;
  421. }
  422. init(spriteSheet, cloudLayer) {
  423. this.spriteSheet = spriteSheet;
  424. this.cloudLayer = cloudLayer;
  425. // Generar nubes iniciales para que el fondo no se vea vacío
  426. this.spawnInitialClouds();
  427. }
  428. spawnInitialClouds() {
  429. // Generar 2-3 nubes iniciales en posiciones aleatorias
  430. const initialCount = Math.floor(Math.random() * 2) + 2;
  431. for (let i = 0; i < initialCount; i++) {
  432. const x = Math.random() * this.canvas.width;
  433. const y = this.getRandomY();
  434. const speed = this.getRandomSpeed();
  435. this.createCloud(x, y, speed);
  436. }
  437. }
  438. update() {
  439. this.frameCount++;
  440. // Limpiar nubes que salieron de la pantalla
  441. this.cleanupOffscreenClouds();
  442. // Intentar generar nueva nube
  443. if (this.shouldSpawnCloud()) {
  444. this.spawnCloud();
  445. }
  446. // Mover nubes existentes
  447. this.moveClouds();
  448. }
  449. shouldSpawnCloud() {
  450. // Verificar tiempo mínimo entre apariciones
  451. if (this.frameCount - this.lastSpawnTime < this.config.minTimeBetweenSpawns) {
  452. return false;
  453. }
  454. // Verificar número máximo de nubes
  455. const cloudCount = this.cloudLayer.filter(e => e.type === 'cloud').length;
  456. if (cloudCount >= this.config.maxClouds) {
  457. return false;
  458. }
  459. // Verificar probabilidad de spawn
  460. if (Math.random() > this.config.spawnRate) {
  461. return false;
  462. }
  463. // Verificar espaciado mínimo
  464. const hasSpacing = this.hasEnoughSpacing();
  465. // Debug: mostrar información cada 60 frames (1 segundo)
  466. if (this.frameCount % 60 === 0) {
  467. console.log(`Clouds: ${cloudCount}/${this.config.maxClouds}, Spacing: ${hasSpacing}, Time: ${this.frameCount - this.lastSpawnTime}`);
  468. }
  469. return hasSpacing;
  470. }
  471. hasEnoughSpacing() {
  472. const existingClouds = this.cloudLayer.filter(e => e.type === 'cloud');
  473. // Si no hay nubes, siempre se puede generar
  474. if (existingClouds.length === 0) {
  475. return true;
  476. }
  477. // Verificar que no haya nubes muy cerca del borde derecho
  478. for (const cloud of existingClouds) {
  479. const distanceFromRight = this.canvas.width - cloud.x;
  480. if (distanceFromRight < this.config.minSpacing) {
  481. return false;
  482. }
  483. }
  484. return true;
  485. }
  486. spawnCloud() {
  487. const y = this.getRandomY();
  488. const speed = this.getRandomSpeed();
  489. // Generar desde la derecha de la pantalla
  490. const x = this.canvas.width;
  491. this.createCloud(x, y, speed);
  492. this.lastSpawnTime = this.frameCount;
  493. console.log(`Nueva nube generada en (${x}, ${y}) con velocidad ${speed}`);
  494. }
  495. createCloud(x, y, speed) {
  496. const cloud = {
  497. type: 'cloud',
  498. x: x,
  499. y: y,
  500. w: this.config.size.w,
  501. h: this.config.size.h,
  502. spriteCoords: this.config.spriteCoords,
  503. moveSpeed: speed,
  504. layer: 'far'
  505. };
  506. this.cloudLayer.push(cloud);
  507. }
  508. moveClouds() {
  509. this.cloudLayer.forEach(cloud => {
  510. if (cloud.type === 'cloud' && cloud.moveSpeed) {
  511. cloud.x -= cloud.moveSpeed;
  512. }
  513. });
  514. }
  515. cleanupOffscreenClouds() {
  516. for (let i = this.cloudLayer.length - 1; i >= 0; i--) {
  517. const element = this.cloudLayer[i];
  518. if (element.type === 'cloud' && element.x + element.w < 0) {
  519. this.cloudLayer.splice(i, 1);
  520. }
  521. }
  522. }
  523. getRandomY() {
  524. return Math.random() *
  525. (this.config.yRange.max - this.config.yRange.min) +
  526. this.config.yRange.min;
  527. }
  528. getRandomSpeed() {
  529. return Math.random() *
  530. (this.config.speedRange.max - this.config.speedRange.min) +
  531. this.config.speedRange.min;
  532. }
  533. }
  534. // Clase para manejar el sistema de objetos de fondo
  535. class BackgroundObjectSystem {
  536. constructor(canvas, game) {
  537. this.canvas = canvas;
  538. this.game = game;
  539. this.spriteSheet = null;
  540. this.objectLayer = null;
  541. this.frameCount = 0;
  542. // Configuración para objetos de fondo (scaled for 800x600 canvas)
  543. this.config = {
  544. spawnRate: 0.15, // Probabilidad de spawn por frame (reducida para mejor control)
  545. maxObjects: 300, // Máximo número de objetos en pantalla
  546. speedRange: { min: 0.125, max: 0.25 }, // Velocidad de movimiento (scaled)
  547. yRange: { min: 25, max: 450 }, // Rango de altura (scaled)
  548. minSpacing: 300, // Espaciado mínimo entre grupos (aumentado)
  549. minTimeBetweenSpawns: 120, // Frames mínimos entre apariciones (aumentado)
  550. groupSize: { min: 8, max: 15 }, // Tamaño del grupo de objetos (reducido)
  551. groupSpacing: { min: 15, max: 80 } // Espaciado entre objetos en el grupo
  552. };
  553. this.lastSpawnTime = 0;
  554. }
  555. init(spriteSheet, objectLayer) {
  556. this.spriteSheet = spriteSheet;
  557. this.objectLayer = objectLayer;
  558. // Generar objetos iniciales
  559. this.spawnInitialObjects();
  560. }
  561. spawnInitialObjects() {
  562. // Generar objetos en múltiples filas para mejor distribución
  563. const layers = 3; // Número de capas de profundidad
  564. const groupsPerLayer = 4; // Grupos por capa
  565. for (let layer = 0; layer < layers; layer++) {
  566. for (let i = 0; i < groupsPerLayer; i++) {
  567. // Distribuir horizontalmente
  568. const x = (i * this.canvas.width / groupsPerLayer) + (Math.random() * 150);
  569. // Crear diferentes niveles de altura
  570. const baseY = 100 + (layer * 120); // Capas a diferentes alturas
  571. const y = baseY + (Math.random() * 80 - 40); // Variación aleatoria
  572. // Velocidad ligeramente diferente por capa para efecto parallax
  573. const baseSpeed = this.getRandomSpeed();
  574. const speed = baseSpeed * (1 + layer * 0.1);
  575. this.createObjectGroup(x, y, speed);
  576. }
  577. }
  578. }
  579. update() {
  580. this.frameCount++;
  581. // Limpiar objetos que salieron de la pantalla
  582. this.cleanupOffscreenObjects();
  583. // Intentar generar nuevo grupo
  584. if (this.shouldSpawnObjectGroup()) {
  585. this.spawnObjectGroup();
  586. }
  587. // Mover objetos existentes
  588. this.moveObjects();
  589. }
  590. shouldSpawnObjectGroup() {
  591. // Verificar tiempo mínimo entre apariciones
  592. if (this.frameCount - this.lastSpawnTime < this.config.minTimeBetweenSpawns) {
  593. return false;
  594. }
  595. // Verificar número máximo de objetos
  596. const objectCount = this.objectLayer.filter(e => e.type === 'backgroundObject').length;
  597. if (objectCount >= this.config.maxObjects) {
  598. return false;
  599. }
  600. // Verificar probabilidad de spawn
  601. if (Math.random() > this.config.spawnRate) {
  602. return false;
  603. }
  604. // Verificar espaciado mínimo
  605. const hasSpacing = this.hasEnoughSpacing();
  606. return hasSpacing;
  607. }
  608. hasEnoughSpacing() {
  609. const existingObjects = this.objectLayer.filter(e => e.type === 'backgroundObject');
  610. // Si no hay objetos, siempre se puede generar
  611. if (existingObjects.length === 0) {
  612. return true;
  613. }
  614. // Verificar que no haya objetos muy cerca del borde derecho
  615. for (const obj of existingObjects) {
  616. const distanceFromRight = this.canvas.width - obj.x;
  617. if (distanceFromRight < this.config.minSpacing) {
  618. return false;
  619. }
  620. }
  621. return true;
  622. }
  623. spawnObjectGroup() {
  624. // Generar múltiples grupos a diferentes alturas simultáneamente
  625. const numLayers = Math.floor(Math.random() * 2) + 2; // 2-3 capas
  626. for (let i = 0; i < numLayers; i++) {
  627. // Diferentes alturas para cada capa
  628. const baseY = 100 + (i * 150);
  629. const y = baseY + (Math.random() * 100 - 50);
  630. // Velocidad ligeramente diferente por capa
  631. const baseSpeed = this.getRandomSpeed();
  632. const speed = baseSpeed * (1 + i * 0.15);
  633. // Generar desde la derecha de la pantalla con ligera variación
  634. const x = this.canvas.width + (Math.random() * 100);
  635. this.createObjectGroup(x, y, speed);
  636. }
  637. this.lastSpawnTime = this.frameCount;
  638. console.log(`Nuevos grupos de objetos generados en múltiples capas`);
  639. }
  640. createObjectGroup(x, y, speed) {
  641. const groupSize = Math.floor(Math.random() *
  642. (this.config.groupSize.max - this.config.groupSize.min + 1)) +
  643. this.config.groupSize.min;
  644. // Obtener las variantes de sprites desde elementTypes
  645. const spriteVariants = this.game.elementTypes.backgroundObject.spriteVariants;
  646. let currentX = x;
  647. for (let i = 0; i < groupSize; i++) {
  648. // Seleccionar sprite aleatorio
  649. const spriteVariant = spriteVariants[Math.floor(Math.random() * spriteVariants.length)];
  650. // Aplicar rotación aleatoria
  651. const rotation = (Math.random() - 0.5) * 0.8; // ±0.4 radianes
  652. // Aplicar dispersión vertical aleatoria
  653. const yOffset = (Math.random() - 0.5) * 40; // ±20 píxeles
  654. // Scale the background objects (2x scale factor)
  655. const scaleFactor = 2;
  656. const object = {
  657. type: 'backgroundObject',
  658. x: currentX,
  659. y: y + yOffset,
  660. w: spriteVariant.w * scaleFactor,
  661. h: spriteVariant.h * scaleFactor,
  662. spriteCoords: spriteVariant,
  663. moveSpeed: speed,
  664. rotation: rotation,
  665. layer: 'mid'
  666. };
  667. this.objectLayer.push(object);
  668. // Calcular siguiente posición con espaciado aleatorio (adjusted for scaled objects)
  669. const spacing = Math.random() *
  670. (this.config.groupSpacing.max - this.config.groupSpacing.min) +
  671. this.config.groupSpacing.min;
  672. currentX += (spriteVariant.w * scaleFactor) + spacing;
  673. }
  674. }
  675. moveObjects() {
  676. this.objectLayer.forEach(obj => {
  677. if (obj.type === 'backgroundObject' && obj.moveSpeed) {
  678. obj.x -= obj.moveSpeed;
  679. }
  680. });
  681. }
  682. cleanupOffscreenObjects() {
  683. for (let i = this.objectLayer.length - 1; i >= 0; i--) {
  684. const element = this.objectLayer[i];
  685. if (element.type === 'backgroundObject' && element.x + element.w < 0) {
  686. this.objectLayer.splice(i, 1);
  687. }
  688. }
  689. }
  690. getRandomY() {
  691. return Math.random() *
  692. (this.config.yRange.max - this.config.yRange.min) +
  693. this.config.yRange.min;
  694. }
  695. getRandomSpeed() {
  696. return Math.random() *
  697. (this.config.speedRange.max - this.config.speedRange.min) +
  698. this.config.speedRange.min;
  699. }
  700. }
  701. // Initialize game when DOM is loaded
  702. window.addEventListener('DOMContentLoaded', () => {
  703. new Game();
  704. });