player.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. class Player {
  2. constructor(x, y, spriteSheet) {
  3. this.x = x;
  4. this.y = y;
  5. this.width = 24; // Original size
  6. this.height = 34; // Original size
  7. this.speed = 0.3125; // Original speed
  8. this.direction = 'right';
  9. // Jump physics
  10. this.velocityY = 0;
  11. this.velocityX = 0;
  12. this.gravity = 0.4; // Gravedad más suave
  13. this.jumpPower = -7; // Salto más suave
  14. this.airSpeed = 0.4; // Velocidad horizontal en el aire
  15. this.groundY = 176; // Ground level (original for 320x240)
  16. this.isOnGround = true;
  17. this.jumpPressed = false; // Para evitar saltos múltiples
  18. // Dash system
  19. this.dashCooldown = 0;
  20. this.isDashing = false;
  21. this.dashDuration = 0;
  22. this.dashSpeed = 8;
  23. this.lastKeyPresses = [];
  24. this.keyPressWindow = 300; // ms para detectar teclas rápidas (reducido)
  25. this.smokeParticles = [];
  26. // Vibration effect
  27. this.isVibrating = false;
  28. this.vibrationDuration = 0;
  29. this.vibrationIntensity = 2;
  30. this.originalX = 0;
  31. this.originalY = 0;
  32. // Key state tracking for proper alternation detection
  33. this.leftKeyPressed = false;
  34. this.rightKeyPressed = false;
  35. this.lastKeyReleased = null;
  36. // Teleport dash system
  37. this.teleportDashCooldown = 0;
  38. this.teleportDashDistance = 50;
  39. this.shiftPressed = false;
  40. // Animation
  41. this.frameIndex = 0;
  42. this.frameTimer = 0;
  43. this.idleFrameInterval = 60; // Más lento para idle
  44. this.walkFrameInterval = 15; // Más rápido para caminar
  45. this.crouchFrameInterval = 30; // Intervalo para animación de sentarse
  46. this.isMoving = false;
  47. this.isCrouching = false;
  48. this.stayingCrouched = false; // Para mantener el estado de agachado
  49. // Sprite coordinates from the 930x614 sprite sheet
  50. this.sprites = {
  51. idle: [
  52. { x: 370, y: 15, w: 24, h: 34 },
  53. { x: 341, y: 15, w: 24, h: 34 },
  54. { x: 404, y: 15, w: 24, h: 34 },
  55. { x: 341, y: 15, w: 24, h: 34 }
  56. ],
  57. walk: [
  58. { x: 2, y: 16, w: 24, h: 34 },
  59. { x: 28, y: 16, w: 24, h: 34 },
  60. { x: 54, y: 16, w: 24, h: 34 },
  61. { x: 80, y: 16, w: 24, h: 34 },
  62. { x: 106, y: 16, w: 24, h: 34 },
  63. { x: 132, y: 16, w: 24, h: 34 }
  64. ],
  65. crouch: [
  66. { x: 287, y: 16, w: 24, h: 34 },
  67. { x: 313, y: 16, w: 24, h: 34 }
  68. ]
  69. };
  70. this.currentAnimation = 'idle';
  71. this.spriteSheet = spriteSheet;
  72. }
  73. update() {
  74. this.isMoving = false;
  75. // Detectar estado de agacharse
  76. if (input.isCrouching() && !this.stayingCrouched) {
  77. this.stayingCrouched = true;
  78. }
  79. // Salir del estado agachado si se presionan teclas de movimiento o salto
  80. if (this.stayingCrouched && (input.isMovingLeft() || input.isMovingRight() || input.isJumping())) {
  81. this.stayingCrouched = false;
  82. }
  83. this.isCrouching = this.stayingCrouched;
  84. // Detectar teclas rápidas para dash
  85. this.detectRapidKeyPresses();
  86. // Actualizar dash cooldowns y vibración
  87. if (this.dashCooldown > 0) this.dashCooldown--;
  88. if (this.dashDuration > 0) this.dashDuration--;
  89. if (this.dashDuration <= 0) this.isDashing = false;
  90. if (this.teleportDashCooldown > 0) this.teleportDashCooldown--;
  91. if (this.vibrationDuration > 0) {
  92. this.vibrationDuration--;
  93. if (this.vibrationDuration <= 0) {
  94. this.isVibrating = false;
  95. }
  96. }
  97. // Detectar dash direccional con Shift
  98. if (input.isShiftPressed() && !this.shiftPressed && this.teleportDashCooldown <= 0) {
  99. if (input.isMovingLeft() || input.isMovingRight()) {
  100. this.performTeleportDash(input.isMovingLeft() ? 'left' : 'right');
  101. }
  102. }
  103. this.shiftPressed = input.isShiftPressed();
  104. // Horizontal movement con detección de alternancia mejorada
  105. let horizontalInput = 0;
  106. // Detectar cambios en el estado de las teclas
  107. const currentLeftPressed = input.isMovingLeft();
  108. const currentRightPressed = input.isMovingRight();
  109. // Registrar cuando se presiona una tecla nueva
  110. if (currentLeftPressed && !this.leftKeyPressed) {
  111. this.recordKeyPress('left');
  112. }
  113. if (currentRightPressed && !this.rightKeyPressed) {
  114. this.recordKeyPress('right');
  115. }
  116. // Actualizar estado de teclas
  117. this.leftKeyPressed = currentLeftPressed;
  118. this.rightKeyPressed = currentRightPressed;
  119. // Solo permitir movimiento si no se presionan ambas teclas a la vez y no está agachado
  120. if (!this.isCrouching) {
  121. if (currentLeftPressed && !currentRightPressed) {
  122. horizontalInput = -1;
  123. this.direction = 'left';
  124. this.isMoving = true;
  125. } else if (currentRightPressed && !currentLeftPressed) {
  126. horizontalInput = 1;
  127. this.direction = 'right';
  128. this.isMoving = true;
  129. }
  130. }
  131. // Jump logic con control de una sola pulsación (no saltar si está agachado)
  132. if (input.isJumping() && this.isOnGround && !this.jumpPressed && !this.isCrouching) {
  133. this.velocityY = this.jumpPower;
  134. this.isOnGround = false;
  135. this.jumpPressed = true;
  136. // Salto direccional - agregar velocidad horizontal si se está moviendo
  137. if (horizontalInput !== 0) {
  138. this.velocityX = horizontalInput * 3; // Impulso horizontal al saltar
  139. }
  140. }
  141. // Resetear jumpPressed cuando se suelta la tecla
  142. if (!input.isJumping()) {
  143. this.jumpPressed = false;
  144. }
  145. // Movimiento horizontal con dash
  146. if (this.isDashing) {
  147. // Durante el dash, movimiento súper rápido
  148. this.x += this.direction === 'left' ? -this.dashSpeed : this.dashSpeed;
  149. this.createSmokeParticle();
  150. } else {
  151. // Movimiento normal
  152. if (this.isOnGround) {
  153. this.x += horizontalInput * this.speed;
  154. this.velocityX *= 0.8; // Fricción en el suelo
  155. } else {
  156. // Movimiento en el aire - más limitado pero posible
  157. this.x += horizontalInput * this.airSpeed;
  158. }
  159. // Aplicar velocidad horizontal (para el salto direccional)
  160. this.x += this.velocityX;
  161. // Fricción del aire para velocidad horizontal
  162. this.velocityX *= 0.95;
  163. }
  164. // Actualizar partículas de humo
  165. this.updateSmokeParticles();
  166. // Apply gravity
  167. this.velocityY += this.gravity;
  168. this.y += this.velocityY;
  169. // Ground collision
  170. if (this.y >= this.groundY) {
  171. this.y = this.groundY;
  172. this.velocityY = 0;
  173. this.isOnGround = true;
  174. }
  175. // Keep player within horizontal bounds (320px canvas width)
  176. this.x = Math.max(0, Math.min(320 - this.width, this.x));
  177. // Update animation
  178. this.updateAnimation();
  179. }
  180. updateAnimation() {
  181. let newAnimation;
  182. if (this.isCrouching) {
  183. newAnimation = 'crouch';
  184. } else if (this.isMoving) {
  185. newAnimation = 'walk';
  186. } else {
  187. newAnimation = 'idle';
  188. }
  189. if (this.currentAnimation !== newAnimation) {
  190. this.currentAnimation = newAnimation;
  191. this.frameIndex = 0;
  192. this.frameTimer = 0;
  193. }
  194. this.frameTimer++;
  195. const frames = this.sprites[this.currentAnimation];
  196. // Usar diferentes velocidades según la animación
  197. let frameInterval;
  198. if (this.currentAnimation === 'walk') {
  199. frameInterval = this.walkFrameInterval;
  200. } else if (this.currentAnimation === 'crouch') {
  201. frameInterval = this.crouchFrameInterval;
  202. } else {
  203. frameInterval = this.idleFrameInterval;
  204. }
  205. if (this.frameTimer >= frameInterval) {
  206. this.frameTimer = 0;
  207. this.frameIndex = (this.frameIndex + 1) % frames.length;
  208. }
  209. }
  210. draw(ctx) {
  211. const frame = this.sprites[this.currentAnimation][this.frameIndex];
  212. // Calcular posición con vibración
  213. let drawX = this.x;
  214. let drawY = this.y;
  215. if (this.isVibrating) {
  216. drawX += (Math.random() - 0.5) * this.vibrationIntensity;
  217. drawY += (Math.random() - 0.5) * this.vibrationIntensity;
  218. // Crear humo continuo durante la vibración
  219. if (Math.random() < 0.3) {
  220. this.createHeadSmokeParticle();
  221. }
  222. }
  223. ctx.save();
  224. if (this.direction === 'left') {
  225. ctx.scale(-1, 1);
  226. ctx.drawImage(
  227. this.spriteSheet,
  228. frame.x, frame.y, frame.w, frame.h,
  229. -drawX - this.width, drawY,
  230. this.width, this.height
  231. );
  232. } else {
  233. ctx.drawImage(
  234. this.spriteSheet,
  235. frame.x, frame.y, frame.w, frame.h,
  236. drawX, drawY,
  237. this.width, this.height
  238. );
  239. }
  240. ctx.restore();
  241. // Dibujar partículas de humo
  242. this.drawSmokeParticles(ctx);
  243. }
  244. recordKeyPress(direction) {
  245. const now = Date.now();
  246. this.lastKeyPresses.push({ direction, time: now });
  247. // Limpiar teclas antiguas
  248. this.lastKeyPresses = this.lastKeyPresses.filter(press =>
  249. now - press.time < this.keyPressWindow
  250. );
  251. }
  252. detectRapidKeyPresses() {
  253. if (this.dashCooldown > 0 || this.isDashing) return;
  254. const now = Date.now();
  255. const recentPresses = this.lastKeyPresses.filter(press =>
  256. now - press.time < this.keyPressWindow
  257. );
  258. // Detectar alternancia rápida entre izquierda y derecha (simplificado)
  259. if (recentPresses.length >= 3) {
  260. let alternating = true;
  261. for (let i = 1; i < recentPresses.length; i++) {
  262. if (recentPresses[i].direction === recentPresses[i-1].direction) {
  263. alternating = false;
  264. break;
  265. }
  266. }
  267. if (alternating) {
  268. this.startLateralDash();
  269. console.log('¡Dash lateral activado!');
  270. }
  271. }
  272. }
  273. startLateralDash() {
  274. this.isDashing = true;
  275. this.dashDuration = 15; // frames
  276. this.dashCooldown = 60; // frames antes de poder hacer otro dash
  277. this.lastKeyPresses = []; // Limpiar historial
  278. // Activar vibración
  279. this.isVibrating = true;
  280. this.vibrationDuration = 45; // frames de vibración
  281. this.originalX = this.x;
  282. this.originalY = this.y;
  283. // Crear humo tipo fogata encima de la cabeza
  284. for (let i = 0; i < 15; i++) {
  285. this.createHeadSmokeParticle();
  286. }
  287. }
  288. performTeleportDash(direction) {
  289. this.teleportDashCooldown = 90; // frames antes de poder hacer otro teleport dash
  290. // Calcular nueva posición
  291. const dashDistance = direction === 'left' ? -this.teleportDashDistance : this.teleportDashDistance;
  292. const newX = this.x + dashDistance;
  293. // Mantener dentro de los límites
  294. this.x = Math.max(0, Math.min(800 - this.width, newX));
  295. // Crear efecto de humo en posición inicial y final
  296. const oldX = this.x - dashDistance;
  297. for (let i = 0; i < 12; i++) {
  298. // Humo en posición inicial
  299. this.smokeParticles.push({
  300. x: oldX + this.width / 2 + (Math.random() - 0.5) * this.width,
  301. y: this.y + this.height / 2 + (Math.random() - 0.5) * this.height,
  302. vx: (Math.random() - 0.5) * 6,
  303. vy: (Math.random() - 0.5) * 6,
  304. life: 40,
  305. maxLife: 40,
  306. size: Math.random() * 12 + 6,
  307. type: 'normal'
  308. });
  309. // Humo en posición final
  310. this.createSmokeParticle();
  311. }
  312. console.log(`¡Dash direccional ${direction} activado!`);
  313. }
  314. createSmokeParticle() {
  315. this.smokeParticles.push({
  316. x: this.x + this.width / 2 + (Math.random() - 0.5) * this.width,
  317. y: this.y + this.height / 2 + (Math.random() - 0.5) * this.height,
  318. vx: (Math.random() - 0.5) * 4,
  319. vy: (Math.random() - 0.5) * 4,
  320. life: 30,
  321. maxLife: 30,
  322. size: Math.random() * 8 + 4,
  323. type: 'normal'
  324. });
  325. }
  326. createHeadSmokeParticle() {
  327. this.smokeParticles.push({
  328. x: this.x + this.width / 2 + (Math.random() - 0.5) * 20,
  329. y: this.y - 10 + (Math.random() * 15), // Encima de la cabeza
  330. vx: (Math.random() - 0.5) * 2,
  331. vy: -Math.random() * 3 - 1, // Hacia arriba como humo
  332. life: 60,
  333. maxLife: 60,
  334. size: Math.random() * 6 + 3,
  335. type: 'head'
  336. });
  337. }
  338. updateSmokeParticles() {
  339. for (let i = this.smokeParticles.length - 1; i >= 0; i--) {
  340. const particle = this.smokeParticles[i];
  341. particle.x += particle.vx;
  342. particle.y += particle.vy;
  343. particle.life--;
  344. particle.vx *= 0.98;
  345. particle.vy *= 0.98;
  346. if (particle.life <= 0) {
  347. this.smokeParticles.splice(i, 1);
  348. }
  349. }
  350. }
  351. drawSmokeParticles(ctx) {
  352. ctx.save();
  353. for (const particle of this.smokeParticles) {
  354. const alpha = particle.life / particle.maxLife;
  355. ctx.globalAlpha = alpha * 0.7;
  356. // Diferentes colores según el tipo de humo
  357. if (particle.type === 'head') {
  358. // Humo de cabeza - más amarillento como fogata
  359. const intensity = Math.floor(255 * alpha);
  360. ctx.fillStyle = `rgb(${intensity}, ${Math.floor(intensity * 0.8)}, ${Math.floor(intensity * 0.3)})`;
  361. } else {
  362. // Humo normal - blanco
  363. ctx.fillStyle = '#ffffff';
  364. }
  365. ctx.beginPath();
  366. ctx.arc(particle.x, particle.y, particle.size * alpha, 0, Math.PI * 2);
  367. ctx.fill();
  368. }
  369. ctx.restore();
  370. }
  371. }