game.js 28 KB

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