game.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  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
  178. this.player = new Player(72, 88, this.processedSpriteSheet);
  179. // Crear elementos de fondo usando los nuevos métodos
  180. // Piso
  181. for (let i = 0; i < 10; i++) {
  182. this.addFloorTile(i * 32, 208);
  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
  189. this.addSprite(10, 183, 35, 26, { x: 137, y: 104, w: 35, h: 26 });
  190. this.addSprite(42, 183, 35, 26, { x: 173, y: 104, w: 35, h: 26 });
  191. this.addSprite(74, 183, 35, 26, { x: 137, y: 131, w: 35, h: 26 });
  192. this.addSprite(106, 183, 35, 26, { x: 173, y: 131, w: 35, h: 26 });
  193. // Candybar
  194. this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 });
  195. // Cake
  196. this.addSprite(260, 134, 100, 75, { x: 461, y: 414, w: 100, h: 75 });
  197. // Cat
  198. this.addSprite(185, 187, 20, 23, { x: 235, y: 87, w: 20, h: 23 });
  199. this.addSprite(200, 187, 20, 23, { 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. // Verificar si la canción ha terminado y detenerla
  215. if (this.lyricsSystem.isGameOver && !this.backgroundMusic.paused) {
  216. this.backgroundMusic.pause();
  217. this.gameState = 'gameOver';
  218. }
  219. }
  220. }
  221. // Método para renderizar un elemento individual
  222. renderElement(element) {
  223. const elementConfig = this.elementTypes[element.type];
  224. if (!elementConfig) {
  225. console.warn(`Tipo de elemento desconocido: ${element.type}`);
  226. return;
  227. }
  228. switch (elementConfig.renderMethod) {
  229. case 'sprite':
  230. this.renderSpriteElement(element, elementConfig);
  231. break;
  232. case 'color':
  233. this.renderColorElement(element, elementConfig);
  234. break;
  235. case 'text':
  236. this.renderTextElement(element, elementConfig);
  237. break;
  238. default:
  239. console.warn(`Método de renderizado desconocido: ${elementConfig.renderMethod}`);
  240. }
  241. }
  242. renderSpriteElement(element, config) {
  243. // Usar coordenadas personalizadas si están especificadas, sino usar las por defecto
  244. const spriteCoords = element.spriteCoords || config.defaultSpriteCoords;
  245. if (!spriteCoords) {
  246. console.warn(`No se especificaron coordenadas de sprite para elemento tipo: ${element.type}`);
  247. return;
  248. }
  249. // Si el elemento tiene rotación, aplicar transformaciones
  250. if (element.rotation) {
  251. this.ctx.save();
  252. // Calcular el centro del sprite para la rotación
  253. const centerX = element.x + element.w / 2;
  254. const centerY = element.y + element.h / 2;
  255. // Aplicar transformaciones
  256. this.ctx.translate(centerX, centerY);
  257. this.ctx.rotate(element.rotation);
  258. this.ctx.translate(-centerX, -centerY);
  259. }
  260. this.ctx.drawImage(
  261. this.processedSpriteSheet,
  262. spriteCoords.x, spriteCoords.y, spriteCoords.w, spriteCoords.h,
  263. element.x, element.y, element.w, element.h
  264. );
  265. // Restaurar el contexto si se aplicó rotación
  266. if (element.rotation) {
  267. this.ctx.restore();
  268. }
  269. }
  270. renderColorElement(element, config) {
  271. const color = element.color || config.color;
  272. this.ctx.fillStyle = color;
  273. this.ctx.fillRect(element.x, element.y, element.w, element.h);
  274. }
  275. renderTextElement(element, config) {
  276. this.ctx.fillStyle = element.color || config.color;
  277. this.ctx.font = element.font || config.font;
  278. this.ctx.textAlign = element.align || config.align;
  279. this.ctx.fillText(element.text, element.x, element.y);
  280. }
  281. draw() {
  282. // Clear canvas con degradado
  283. const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
  284. gradient.addColorStop(0, '#d9e6ff'); // Color superior
  285. gradient.addColorStop(1, '#eee5ff'); // Color inferior
  286. this.ctx.fillStyle = gradient;
  287. this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  288. // Renderizar elementos por capas (de lejos a cerca)
  289. // Capa lejana (nubes, montañas)
  290. this.backgroundLayers.far.forEach(element => {
  291. this.renderElement(element);
  292. });
  293. // Capa media (árboles, edificios)
  294. this.backgroundLayers.mid.forEach(element => {
  295. this.renderElement(element);
  296. });
  297. // Capa cercana (decoraciones, elementos interactivos)
  298. this.backgroundLayers.near.forEach(element => {
  299. this.renderElement(element);
  300. });
  301. // Elementos legacy (para compatibilidad)
  302. this.backgroundElements.forEach(element => {
  303. this.renderElement(element);
  304. });
  305. // Draw player
  306. if (this.player) {
  307. this.player.draw(this.ctx);
  308. }
  309. // Dibujar letras si están activadas
  310. this.lyricsSystem.draw(this.ctx, this.canvas);
  311. // Draw instructions if music hasn't started
  312. if (this.backgroundMusic.paused && this.gameState === 'playing') {
  313. this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
  314. this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  315. this.ctx.fillStyle = 'white';
  316. this.ctx.font = '10px Courier New';
  317. this.ctx.textAlign = 'center';
  318. this.ctx.fillText('Click to start music', 160, 100);
  319. this.ctx.fillText('WASD or ↑↓←→ to move', 160, 115);
  320. this.ctx.fillText('L - Show/Hide lyrics', 160, 130);
  321. this.ctx.fillText('R - Restart game', 160, 145);
  322. }
  323. // Dibujar pantalla de fin de juego
  324. if (this.lyricsSystem.gameOverScreen) {
  325. this.drawGameOverScreen();
  326. }
  327. }
  328. gameLoop() {
  329. this.update();
  330. this.draw();
  331. requestAnimationFrame(() => this.gameLoop());
  332. }
  333. setupKeyboardControls() {
  334. document.addEventListener('keydown', (e) => {
  335. switch(e.key.toLowerCase()) {
  336. case 'l':
  337. this.lyricsSystem.toggleLyrics();
  338. break;
  339. case 'r':
  340. this.restartGame();
  341. break;
  342. case 'g':
  343. // Shortcut para desarrollo: simular fin de canción
  344. if (e.ctrlKey) {
  345. this.lyricsSystem.simulateGameOver();
  346. }
  347. break;
  348. }
  349. });
  350. }
  351. drawGameOverScreen() {
  352. // Fondo negro translúcido
  353. this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
  354. this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
  355. // Texto de fin de juego
  356. this.ctx.fillStyle = 'white';
  357. this.ctx.font = '20px Courier New';
  358. this.ctx.textAlign = 'center';
  359. this.ctx.fillText('Song finished!', 160, 100);
  360. this.ctx.fillText('Want to play again?', 160, 125);
  361. // Botones
  362. this.ctx.font = '12px Courier New';
  363. this.ctx.fillText('Press R to restart', 160, 175);
  364. this.ctx.fillText('or reload the page', 160, 190);
  365. }
  366. restartGame() {
  367. // Detener música
  368. this.backgroundMusic.pause();
  369. this.backgroundMusic.currentTime = 0;
  370. // Reiniciar sistemas
  371. this.lyricsSystem.reset();
  372. // Reiniciar estado del juego
  373. this.gameState = 'playing';
  374. // Limpiar elementos de fondo
  375. this.backgroundLayers.far = [];
  376. this.backgroundLayers.mid = [];
  377. this.backgroundLayers.near = [];
  378. this.backgroundElements = [];
  379. // Reiniciar sistemas
  380. this.cloudSystem.init(this.processedSpriteSheet, this.backgroundLayers.far);
  381. this.backgroundObjectSystem.init(this.processedSpriteSheet, this.backgroundLayers.mid);
  382. // Reiniciar jugador
  383. this.player = new Player(144, 176, this.processedSpriteSheet);
  384. // Recrear elementos de fondo
  385. this.recreateBackgroundElements();
  386. }
  387. recreateBackgroundElements() {
  388. // Piso - adjusted for 320x240 canvas
  389. for (let i = 0; i < 10; i++) {
  390. this.addFloorTile(i * 32, 208); // 240-32=208
  391. }
  392. // Macarons - original positions
  393. this.addSprite(10, 183, 35, 26, { x: 137, y: 104, w: 35, h: 26 });
  394. this.addSprite(42, 183, 35, 26, { x: 173, y: 104, w: 35, h: 26 });
  395. this.addSprite(74, 183, 35, 26, { x: 137, y: 131, w: 35, h: 26 });
  396. this.addSprite(106, 183, 35, 26, { x: 173, y: 131, w: 35, h: 26 });
  397. // Candybar - original size and position
  398. this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 });
  399. // Cake - original size and position
  400. this.addSprite(260, 134, 100, 75, { x: 461, y: 414, w: 100, h: 75 });
  401. // Cat - original positions
  402. this.addSprite(185, 187, 20, 23, { x: 235, y: 87, w: 20, h: 23 });
  403. this.addSprite(200, 187, 20, 23, { x: 389, y: 87, w: 16, h: 23 });
  404. }
  405. }
  406. // Clase para manejar el sistema de nubes de forma modular
  407. class CloudSystem {
  408. constructor(canvas) {
  409. this.canvas = canvas;
  410. this.spriteSheet = null;
  411. this.cloudLayer = null;
  412. this.frameCount = 0;
  413. // Configuración para canvas 320x240
  414. this.config = {
  415. spawnRate: 0.1, // Probabilidad de spawn por frame
  416. maxClouds: 2, // Máximo número de nubes en pantalla
  417. speedRange: { min: 0.125, max: 0.1875 }, // Velocidad de movimiento (original)
  418. yRange: { min: 1, max: 150 }, // Rango de altura (original)
  419. spriteCoords: { x: 231, y: 112, w: 287, h: 148 },
  420. size: { w: 287, h: 148 }, // Tamaño original de nube
  421. minSpacing: 125, // Espaciado mínimo entre nubes (original)
  422. minTimeBetweenSpawns: 60 // Frames mínimos entre apariciones
  423. };
  424. this.lastSpawnTime = 0;
  425. }
  426. init(spriteSheet, cloudLayer) {
  427. this.spriteSheet = spriteSheet;
  428. this.cloudLayer = cloudLayer;
  429. // Generar nubes iniciales para que el fondo no se vea vacío
  430. this.spawnInitialClouds();
  431. }
  432. spawnInitialClouds() {
  433. // Generar 2-3 nubes iniciales en posiciones aleatorias
  434. const initialCount = Math.floor(Math.random() * 2) + 2;
  435. for (let i = 0; i < initialCount; i++) {
  436. const x = Math.random() * this.canvas.width;
  437. const y = this.getRandomY();
  438. const speed = this.getRandomSpeed();
  439. this.createCloud(x, y, speed);
  440. }
  441. }
  442. update() {
  443. this.frameCount++;
  444. // Limpiar nubes que salieron de la pantalla
  445. this.cleanupOffscreenClouds();
  446. // Intentar generar nueva nube
  447. if (this.shouldSpawnCloud()) {
  448. this.spawnCloud();
  449. }
  450. // Mover nubes existentes
  451. this.moveClouds();
  452. }
  453. shouldSpawnCloud() {
  454. // Verificar tiempo mínimo entre apariciones
  455. if (this.frameCount - this.lastSpawnTime < this.config.minTimeBetweenSpawns) {
  456. return false;
  457. }
  458. // Verificar número máximo de nubes
  459. const cloudCount = this.cloudLayer.filter(e => e.type === 'cloud').length;
  460. if (cloudCount >= this.config.maxClouds) {
  461. return false;
  462. }
  463. // Verificar probabilidad de spawn
  464. if (Math.random() > this.config.spawnRate) {
  465. return false;
  466. }
  467. // Verificar espaciado mínimo
  468. const hasSpacing = this.hasEnoughSpacing();
  469. // Debug: mostrar información cada 60 frames (1 segundo)
  470. if (this.frameCount % 60 === 0) {
  471. console.log(`Clouds: ${cloudCount}/${this.config.maxClouds}, Spacing: ${hasSpacing}, Time: ${this.frameCount - this.lastSpawnTime}`);
  472. }
  473. return hasSpacing;
  474. }
  475. hasEnoughSpacing() {
  476. const existingClouds = this.cloudLayer.filter(e => e.type === 'cloud');
  477. // Si no hay nubes, siempre se puede generar
  478. if (existingClouds.length === 0) {
  479. return true;
  480. }
  481. // Verificar que no haya nubes muy cerca del borde derecho
  482. for (const cloud of existingClouds) {
  483. const distanceFromRight = this.canvas.width - cloud.x;
  484. if (distanceFromRight < this.config.minSpacing) {
  485. return false;
  486. }
  487. }
  488. return true;
  489. }
  490. spawnCloud() {
  491. const y = this.getRandomY();
  492. const speed = this.getRandomSpeed();
  493. // Generar desde la derecha de la pantalla
  494. const x = this.canvas.width;
  495. this.createCloud(x, y, speed);
  496. this.lastSpawnTime = this.frameCount;
  497. console.log(`Nueva nube generada en (${x}, ${y}) con velocidad ${speed}`);
  498. }
  499. createCloud(x, y, speed) {
  500. const cloud = {
  501. type: 'cloud',
  502. x: x,
  503. y: y,
  504. w: this.config.size.w,
  505. h: this.config.size.h,
  506. spriteCoords: this.config.spriteCoords,
  507. moveSpeed: speed,
  508. layer: 'far'
  509. };
  510. this.cloudLayer.push(cloud);
  511. }
  512. moveClouds() {
  513. this.cloudLayer.forEach(cloud => {
  514. if (cloud.type === 'cloud' && cloud.moveSpeed) {
  515. cloud.x -= cloud.moveSpeed;
  516. }
  517. });
  518. }
  519. cleanupOffscreenClouds() {
  520. for (let i = this.cloudLayer.length - 1; i >= 0; i--) {
  521. const element = this.cloudLayer[i];
  522. if (element.type === 'cloud' && element.x + element.w < 0) {
  523. this.cloudLayer.splice(i, 1);
  524. }
  525. }
  526. }
  527. getRandomY() {
  528. return Math.random() *
  529. (this.config.yRange.max - this.config.yRange.min) +
  530. this.config.yRange.min;
  531. }
  532. getRandomSpeed() {
  533. return Math.random() *
  534. (this.config.speedRange.max - this.config.speedRange.min) +
  535. this.config.speedRange.min;
  536. }
  537. }
  538. // Clase para manejar el sistema de objetos de fondo
  539. class BackgroundObjectSystem {
  540. constructor(canvas, game) {
  541. this.canvas = canvas;
  542. this.game = game;
  543. this.spriteSheet = null;
  544. this.objectLayer = null;
  545. this.frameCount = 0;
  546. // Configuración para objetos de fondo (320x240 canvas)
  547. this.config = {
  548. spawnRate: 0.15, // Probabilidad de spawn por frame
  549. maxObjects: 150, // Máximo número de objetos en pantalla (reducido)
  550. speedRange: { min: 0.0625, max: 0.125 }, // Velocidad de movimiento (original)
  551. yRange: { min: 12, max: 225 }, // Rango de altura (original)
  552. minSpacing: 150, // Espaciado mínimo entre grupos (original)
  553. minTimeBetweenSpawns: 120, // Frames mínimos entre apariciones
  554. groupSize: { min: 4, max: 8 }, // Tamaño del grupo de objetos (original)
  555. groupSpacing: { min: 8, max: 40 } // Espaciado entre objetos en el grupo (original)
  556. };
  557. this.lastSpawnTime = 0;
  558. }
  559. init(spriteSheet, objectLayer) {
  560. this.spriteSheet = spriteSheet;
  561. this.objectLayer = objectLayer;
  562. // Generar objetos iniciales
  563. this.spawnInitialObjects();
  564. }
  565. spawnInitialObjects() {
  566. // Generar objetos en múltiples filas para mejor distribución
  567. const layers = 3; // Número de capas de profundidad
  568. const groupsPerLayer = 4; // Grupos por capa
  569. for (let layer = 0; layer < layers; layer++) {
  570. for (let i = 0; i < groupsPerLayer; i++) {
  571. // Distribuir horizontalmente
  572. const x = (i * this.canvas.width / groupsPerLayer) + (Math.random() * 150);
  573. // Crear diferentes niveles de altura
  574. const baseY = 100 + (layer * 120); // Capas a diferentes alturas
  575. const y = baseY + (Math.random() * 80 - 40); // Variación aleatoria
  576. // Velocidad ligeramente diferente por capa para efecto parallax
  577. const baseSpeed = this.getRandomSpeed();
  578. const speed = baseSpeed * (1 + layer * 0.1);
  579. this.createObjectGroup(x, y, speed);
  580. }
  581. }
  582. }
  583. update() {
  584. this.frameCount++;
  585. // Limpiar objetos que salieron de la pantalla
  586. this.cleanupOffscreenObjects();
  587. // Intentar generar nuevo grupo
  588. if (this.shouldSpawnObjectGroup()) {
  589. this.spawnObjectGroup();
  590. }
  591. // Mover objetos existentes
  592. this.moveObjects();
  593. }
  594. shouldSpawnObjectGroup() {
  595. // Verificar tiempo mínimo entre apariciones
  596. if (this.frameCount - this.lastSpawnTime < this.config.minTimeBetweenSpawns) {
  597. return false;
  598. }
  599. // Verificar número máximo de objetos
  600. const objectCount = this.objectLayer.filter(e => e.type === 'backgroundObject').length;
  601. if (objectCount >= this.config.maxObjects) {
  602. return false;
  603. }
  604. // Verificar probabilidad de spawn
  605. if (Math.random() > this.config.spawnRate) {
  606. return false;
  607. }
  608. // Verificar espaciado mínimo
  609. const hasSpacing = this.hasEnoughSpacing();
  610. return hasSpacing;
  611. }
  612. hasEnoughSpacing() {
  613. const existingObjects = this.objectLayer.filter(e => e.type === 'backgroundObject');
  614. // Si no hay objetos, siempre se puede generar
  615. if (existingObjects.length === 0) {
  616. return true;
  617. }
  618. // Verificar que no haya objetos muy cerca del borde derecho
  619. for (const obj of existingObjects) {
  620. const distanceFromRight = this.canvas.width - obj.x;
  621. if (distanceFromRight < this.config.minSpacing) {
  622. return false;
  623. }
  624. }
  625. return true;
  626. }
  627. spawnObjectGroup() {
  628. // Generar múltiples grupos a diferentes alturas simultáneamente
  629. const numLayers = Math.floor(Math.random() * 2) + 2; // 2-3 capas
  630. for (let i = 0; i < numLayers; i++) {
  631. // Diferentes alturas para cada capa
  632. const baseY = 100 + (i * 150);
  633. const y = baseY + (Math.random() * 100 - 50);
  634. // Velocidad ligeramente diferente por capa
  635. const baseSpeed = this.getRandomSpeed();
  636. const speed = baseSpeed * (1 + i * 0.15);
  637. // Generar desde la derecha de la pantalla con ligera variación
  638. const x = this.canvas.width + (Math.random() * 100);
  639. this.createObjectGroup(x, y, speed);
  640. }
  641. this.lastSpawnTime = this.frameCount;
  642. console.log(`Nuevos grupos de objetos generados en múltiples capas`);
  643. }
  644. createObjectGroup(x, y, speed) {
  645. const groupSize = Math.floor(Math.random() *
  646. (this.config.groupSize.max - this.config.groupSize.min + 1)) +
  647. this.config.groupSize.min;
  648. // Obtener las variantes de sprites desde elementTypes
  649. const spriteVariants = this.game.elementTypes.backgroundObject.spriteVariants;
  650. let currentX = x;
  651. for (let i = 0; i < groupSize; i++) {
  652. // Seleccionar sprite aleatorio
  653. const spriteVariant = spriteVariants[Math.floor(Math.random() * spriteVariants.length)];
  654. // Aplicar rotación aleatoria
  655. const rotation = (Math.random() - 0.5) * 0.8; // ±0.4 radianes
  656. // Aplicar dispersión vertical aleatoria
  657. const yOffset = (Math.random() - 0.5) * 40; // ±20 píxeles
  658. // Use original size for background objects
  659. const object = {
  660. type: 'backgroundObject',
  661. x: currentX,
  662. y: y + yOffset,
  663. w: spriteVariant.w,
  664. h: spriteVariant.h,
  665. spriteCoords: spriteVariant,
  666. moveSpeed: speed,
  667. rotation: rotation,
  668. layer: 'mid'
  669. };
  670. this.objectLayer.push(object);
  671. // Calcular siguiente posición con espaciado aleatorio
  672. const spacing = Math.random() *
  673. (this.config.groupSpacing.max - this.config.groupSpacing.min) +
  674. this.config.groupSpacing.min;
  675. currentX += spriteVariant.w + spacing;
  676. }
  677. }
  678. moveObjects() {
  679. this.objectLayer.forEach(obj => {
  680. if (obj.type === 'backgroundObject' && obj.moveSpeed) {
  681. obj.x -= obj.moveSpeed;
  682. }
  683. });
  684. }
  685. cleanupOffscreenObjects() {
  686. for (let i = this.objectLayer.length - 1; i >= 0; i--) {
  687. const element = this.objectLayer[i];
  688. if (element.type === 'backgroundObject' && element.x + element.w < 0) {
  689. this.objectLayer.splice(i, 1);
  690. }
  691. }
  692. }
  693. getRandomY() {
  694. return Math.random() *
  695. (this.config.yRange.max - this.config.yRange.min) +
  696. this.config.yRange.min;
  697. }
  698. getRandomSpeed() {
  699. return Math.random() *
  700. (this.config.speedRange.max - this.config.speedRange.min) +
  701. this.config.speedRange.min;
  702. }
  703. }
  704. // Initialize game when DOM is loaded
  705. window.addEventListener('DOMContentLoaded', () => {
  706. new Game();
  707. });