Browse Source

fix canvas resolution, downscale everything by 0.5x and then scale it back up 2x with css for pixel perfect graphics

Matthew Trejo 4 months ago
parent
commit
7fc8259524
7 changed files with 199 additions and 166 deletions
  1. 2 21
      css/styles.css
  2. 3 4
      index.html
  3. 71 67
      js/game.js
  4. 8 1
      js/input.js
  5. 28 48
      js/lyrics.js
  6. 58 23
      js/player.js
  7. 29 2
      js/touchControls.js

+ 2 - 21
css/styles.css

@@ -14,25 +14,6 @@ body {
     color: white;
 }
 
-.game-title {
-    text-align: center;
-    font-size: 2.5em;
-    margin: 0 0 20px 0;
-    text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
-    background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
-    background-size: 300% 300%;
-    -webkit-background-clip: text;
-    -webkit-text-fill-color: transparent;
-    background-clip: text;
-    animation: gradientShift 3s ease infinite;
-}
-
-@keyframes gradientShift {
-    0% { background-position: 0% 50%; }
-    50% { background-position: 100% 50%; }
-    100% { background-position: 0% 50%; }
-}
-
 .game-container {
     text-align: center;
     background-color: #2a2a2a;
@@ -49,7 +30,8 @@ body {
     cursor: pointer;
     max-width: 100%;
     height: auto;
-    width: 800px;
+    width: 640px;
+    height: 480px;
     max-height: 80vh;
 }
 
@@ -83,7 +65,6 @@ body {
     text-align: center;
     font-family: 'CustomFont', Arial, sans-serif;
     color: white;
-    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
     z-index: 1000;
 }
 

+ 3 - 4
index.html

@@ -11,14 +11,13 @@
 
 <body>
     <div class="game-container">
-        <h1 class="game-title">Nekomata Okayu Scroller</h1>
-        <canvas id="gameCanvas" width="800" height="600"></canvas>
-        <div class="controls">
+        <canvas id="gameCanvas" width="320" height="240"></canvas>
+        <!-- <div class="controls">
             <p><strong>Movement:</strong> A/← - Left | D/→ - Right | W/↑ - Jump</p>
             <p><strong>Special:</strong> Rapid A/D alternation - Lateral dash | Shift + Direction - Teleport dash</p>
             <p><strong>Game:</strong> L - Show/Hide lyrics | R - Restart game</p>
             <p><strong>Touch:</strong> Tap bottom-left/right to move | Tap top half to jump | Double tap sides for dash</p>
-        </div>
+        </div> -->
     </div>
 
     <script src="js/input.js"></script>

+ 71 - 67
js/game.js

@@ -201,13 +201,13 @@ class Game {
     }
     
     startGame() {
-        // Create player (scaled for 800x600 canvas)
-        this.player = new Player(360, 500, this.processedSpriteSheet);
+        // Create player
+        this.player = new Player(72, 88, this.processedSpriteSheet);
         
         // Crear elementos de fondo usando los nuevos métodos
-        // Piso (scaled and extended for wider canvas) - moved down to fill bottom space
-        for (let i = 0; i < 26; i++) {
-            this.addFloorTile(i * 32, 568); // Moved from 520 to 568 (600-32=568)
+        // Piso
+        for (let i = 0; i < 10; i++) {
+            this.addFloorTile(i * 32, 208);
         }
         
         // Inicializar sistema de nubes
@@ -216,21 +216,21 @@ class Game {
         // Inicializar sistema de objetos de fondo
         this.backgroundObjectSystem.init(this.processedSpriteSheet, this.backgroundLayers.mid);
         
-        // Macarons (scaled positions) - adjusted Y position
-        this.addSprite(25, 518, 70, 52, { x: 137, y: 104, w: 35, h: 26 });
-        this.addSprite(105, 518, 70, 52, { x: 173, y: 104, w: 35, h: 26 });
-        this.addSprite(185, 518, 70, 52, { x: 137, y: 131, w: 35, h: 26 });
-        this.addSprite(265, 518, 70, 52, { x: 173, y: 131, w: 35, h: 26 });
+        // Macarons
+        this.addSprite(10, 183, 35, 26, { x: 137, y: 104, w: 35, h: 26 });
+        this.addSprite(42, 183, 35, 26, { x: 173, y: 104, w: 35, h: 26 });
+        this.addSprite(74, 183, 35, 26, { x: 137, y: 131, w: 35, h: 26 });
+        this.addSprite(106, 183, 35, 26, { x: 173, y: 131, w: 35, h: 26 });
 
-        // Candybar (scaled) - adjusted to ground level
-        this.addSprite(500, 410, 96, 158, { x: 179, y: 438, w: 48, h: 79 });
+        // Candybar
+        this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 });
 
-        // Cake (scaled) - adjusted to ground level
-        this.addSprite(650, 418, 200, 150, { x: 461, y: 414, w: 100, h: 75 });
+        // Cake
+        this.addSprite(260, 134, 100, 75, { x: 461, y: 414, w: 100, h: 75 });
 
-        // Cat (scaled) - adjusted Y position
-        this.addSprite(463, 528, 40, 46, { x: 235, y: 87, w: 20, h: 23 });
-        this.addSprite(500, 528, 40, 46, { x: 389, y: 87, w: 16, h: 23 });
+        // Cat
+        this.addSprite(185, 187, 20, 23, { x: 235, y: 87, w: 20, h: 23 });
+        this.addSprite(200, 187, 20, 23, { x: 389, y: 87, w: 16, h: 23 });
         
         this.gameLoop();
     }
@@ -251,6 +251,12 @@ class Game {
         // Actualizar sistema de letras
         if (!this.backgroundMusic.paused) {
             this.lyricsSystem.update(this.backgroundMusic.currentTime);
+            
+            // Verificar si la canción ha terminado y detenerla
+            if (this.lyricsSystem.isGameOver && !this.backgroundMusic.paused) {
+                this.backgroundMusic.pause();
+                this.gameState = 'gameOver';
+            }
         }
     }
     
@@ -369,13 +375,12 @@ class Game {
             this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
             
             this.ctx.fillStyle = 'white';
-            this.ctx.font = '20px Courier New';
+            this.ctx.font = '10px Courier New';
             this.ctx.textAlign = 'center';
-            this.ctx.fillText('Click to start music', 400, 300);
-            this.ctx.fillText('AD or ← → to move', 400, 325);
-            this.ctx.fillText('L - Show/Hide lyrics', 400, 350);
-            this.ctx.fillText('R - Restart game', 400, 375);
-            // this.ctx.fillText('Ctrl+G - Simular fin de canción', 160, 160);
+            this.ctx.fillText('Click to start music', 160, 100);
+            this.ctx.fillText('WASD or ↑↓←→  to move', 160, 115);
+            this.ctx.fillText('L - Show/Hide lyrics', 160, 130);
+            this.ctx.fillText('R - Restart game', 160, 145);
         }
         
         // Dibujar pantalla de fin de juego
@@ -416,16 +421,16 @@ class Game {
         
         // Texto de fin de juego
         this.ctx.fillStyle = 'white';
-        this.ctx.font = '40px Courier New';
+        this.ctx.font = '20px Courier New';
         this.ctx.textAlign = 'center';
         
-        this.ctx.fillText('Song finished!', 400, 250);
-        this.ctx.fillText('Want to play again?', 400, 300);
+        this.ctx.fillText('Song finished!', 160, 100);
+        this.ctx.fillText('Want to play again?', 160, 125);
         
         // Botones
-        this.ctx.font = '24px Courier New';
-        this.ctx.fillText('Press R to restart', 400, 375);
-        this.ctx.fillText('or reload the page', 400, 412);
+        this.ctx.font = '12px Courier New';
+        this.ctx.fillText('Press R to restart', 160, 175);
+        this.ctx.fillText('or reload the page', 160, 190);
     }
     
     restartGame() {
@@ -457,26 +462,26 @@ class Game {
     }
     
     recreateBackgroundElements() {
-        // Piso (scaled and extended for wider canvas) - moved down to fill bottom space
-        for (let i = 0; i < 26; i++) {
-            this.addFloorTile(i * 32, 568); // Moved from 520 to 568 (600-32=568)
+        // Piso - adjusted for 320x240 canvas
+        for (let i = 0; i < 10; i++) {
+            this.addFloorTile(i * 32, 208); // 240-32=208
         }
         
-        // Macarons (scaled positions) - adjusted Y position
-        this.addSprite(25, 518, 70, 52, { x: 137, y: 104, w: 35, h: 26 });
-        this.addSprite(105, 518, 70, 52, { x: 173, y: 104, w: 35, h: 26 });
-        this.addSprite(185, 518, 70, 52, { x: 137, y: 131, w: 35, h: 26 });
-        this.addSprite(265, 518, 70, 52, { x: 173, y: 131, w: 35, h: 26 });
+        // Macarons - original positions
+        this.addSprite(10, 183, 35, 26, { x: 137, y: 104, w: 35, h: 26 });
+        this.addSprite(42, 183, 35, 26, { x: 173, y: 104, w: 35, h: 26 });
+        this.addSprite(74, 183, 35, 26, { x: 137, y: 131, w: 35, h: 26 });
+        this.addSprite(106, 183, 35, 26, { x: 173, y: 131, w: 35, h: 26 });
 
-        // Candybar (scaled) - adjusted to ground level
-         this.addSprite(500, 410, 96, 158, { x: 179, y: 438, w: 48, h: 79 });
+        // Candybar - original size and position
+        this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 });
 
-         // Cake (scaled) - adjusted to ground level
-         this.addSprite(650, 418, 200, 150, { x: 461, y: 414, w: 100, h: 75 });
+        // Cake - original size and position
+        this.addSprite(260, 134, 100, 75, { x: 461, y: 414, w: 100, h: 75 });
 
-        // Cat (scaled) - adjusted Y position
-        this.addSprite(463, 528, 40, 46, { x: 235, y: 87, w: 20, h: 23 });
-        this.addSprite(500, 528, 40, 46, { x: 389, y: 87, w: 16, h: 23 });
+        // Cat - original positions
+        this.addSprite(185, 187, 20, 23, { x: 235, y: 87, w: 20, h: 23 });
+        this.addSprite(200, 187, 20, 23, { x: 389, y: 87, w: 16, h: 23 });
     }
 }
 
@@ -488,16 +493,16 @@ class CloudSystem {
         this.cloudLayer = null;
         this.frameCount = 0;
         
-        // Configuración simplificada (scaled for 800x600 canvas)
+        // Configuración para canvas 320x240
         this.config = {
-            spawnRate: 0.1,           // Probabilidad de spawn por frame (aumentada)
-            maxClouds: 3,               // Máximo número de nubes en pantalla (aumentado)
-            speedRange: { min: 0.25, max: 0.375 },  // Velocidad de movimiento (scaled)
-            yRange: { min: 2, max: 300 },        // Rango de altura (scaled)
+            spawnRate: 0.1,           // Probabilidad de spawn por frame
+            maxClouds: 2,               // Máximo número de nubes en pantalla
+            speedRange: { min: 0.125, max: 0.1875 },  // Velocidad de movimiento (original)
+            yRange: { min: 1, max: 150 },        // Rango de altura (original)
             spriteCoords: { x: 231, y: 112, w: 287, h: 148 },
-            size: { w: 574, h: 296 },  // Scaled cloud size
-            minSpacing: 250,            // Espaciado mínimo entre nubes (scaled)
-            minTimeBetweenSpawns: 60    // Frames mínimos entre apariciones (reducido)
+            size: { w: 287, h: 148 },  // Tamaño original de nube
+            minSpacing: 125,            // Espaciado mínimo entre nubes (original)
+            minTimeBetweenSpawns: 60    // Frames mínimos entre apariciones
         };
         
         this.lastSpawnTime = 0;
@@ -653,16 +658,16 @@ class BackgroundObjectSystem {
         this.objectLayer = null;
         this.frameCount = 0;
         
-        // Configuración para objetos de fondo (scaled for 800x600 canvas)
+        // Configuración para objetos de fondo (320x240 canvas)
         this.config = {
-            spawnRate: 0.15,           // Probabilidad de spawn por frame (reducida para mejor control)
-            maxObjects: 300,              // Máximo número de objetos en pantalla
-            speedRange: { min: 0.125, max: 0.25 },  // Velocidad de movimiento (scaled)
-            yRange: { min: 25, max: 450 },        // Rango de altura (scaled)
-            minSpacing: 300,            // Espaciado mínimo entre grupos (aumentado)
-            minTimeBetweenSpawns: 120,  // Frames mínimos entre apariciones (aumentado)
-            groupSize: { min: 8, max: 15 }, // Tamaño del grupo de objetos (reducido)
-            groupSpacing: { min: 15, max: 80 } // Espaciado entre objetos en el grupo
+            spawnRate: 0.15,           // Probabilidad de spawn por frame
+            maxObjects: 150,              // Máximo número de objetos en pantalla (reducido)
+            speedRange: { min: 0.0625, max: 0.125 },  // Velocidad de movimiento (original)
+            yRange: { min: 12, max: 225 },        // Rango de altura (original)
+            minSpacing: 150,            // Espaciado mínimo entre grupos (original)
+            minTimeBetweenSpawns: 120,  // Frames mínimos entre apariciones
+            groupSize: { min: 4, max: 8 }, // Tamaño del grupo de objetos (original)
+            groupSpacing: { min: 8, max: 40 } // Espaciado entre objetos en el grupo (original)
         };
         
         this.lastSpawnTime = 0;
@@ -799,14 +804,13 @@ class BackgroundObjectSystem {
             // Aplicar dispersión vertical aleatoria
             const yOffset = (Math.random() - 0.5) * 40; // ±20 píxeles
             
-            // Scale the background objects (2x scale factor)
-            const scaleFactor = 2;
+            // Use original size for background objects
             const object = {
                 type: 'backgroundObject',
                 x: currentX,
                 y: y + yOffset,
-                w: spriteVariant.w * scaleFactor,
-                h: spriteVariant.h * scaleFactor,
+                w: spriteVariant.w,
+                h: spriteVariant.h,
                 spriteCoords: spriteVariant,
                 moveSpeed: speed,
                 rotation: rotation,
@@ -815,11 +819,11 @@ class BackgroundObjectSystem {
             
             this.objectLayer.push(object);
             
-            // Calcular siguiente posición con espaciado aleatorio (adjusted for scaled objects)
+            // Calcular siguiente posición con espaciado aleatorio
             const spacing = Math.random() * 
                 (this.config.groupSpacing.max - this.config.groupSpacing.min) + 
                 this.config.groupSpacing.min;
-            currentX += (spriteVariant.w * scaleFactor) + spacing;
+            currentX += spriteVariant.w + spacing;
         }
     }
     

+ 8 - 1
js/input.js

@@ -7,10 +7,13 @@ class InputHandler {
             D: false,
             w: false,
             W: false,
+            s: false,
+            S: false,
             Shift: false,
             ArrowLeft: false,
             ArrowRight: false,
-            ArrowUp: false
+            ArrowUp: false,
+            ArrowDown: false
         };
         
         this.init();
@@ -47,6 +50,10 @@ class InputHandler {
     isShiftPressed() {
         return this.keys.Shift;
     }
+    
+    isCrouching() {
+        return this.keys.s || this.keys.S || this.keys.ArrowDown;
+    }
 }
 
 const input = new InputHandler();

+ 28 - 48
js/lyrics.js

@@ -13,11 +13,11 @@ class LyricsSystem {
         // Offset de tiempo para ajustar sincronización (en segundos)
         // Valor positivo = letras aparecen ANTES
         // Valor negativo = letras aparecen DESPUÉS
-        this.timeOffset = 0.25;
+        this.timeOffset = 0.2;
         
-        // Configuración de texto (scaled for 800x600 canvas)
-        this.maxLineWidth = 700; // Ancho máximo de línea en píxeles (scaled)
-        this.lineHeight = 45; // Altura entre líneas (scaled)
+        // Configuración de texto
+        this.maxLineWidth = 280; // Ancho máximo de línea en píxeles (original)
+        this.lineHeight = 18; // Altura entre líneas (original)
         
         // Cargar la fuente personalizada
         this.loadCustomFont();
@@ -39,7 +39,7 @@ class LyricsSystem {
         // Letras en romaji usando los tiempos exactos del archivo
         const lyricsData = [
             { time: 2.8, text: "mogumogu" },
-            { time: 6.4, text: "shinra banshou no akusenkutou mo mayoneizu kaketara daitai oishiku naru?" },
+            { time: 6.4, text: "shinra banshou no akusenkutou mo mayoneizpu kaketara daitai oishiku naru?" },
             { time: 13.3, text: "kimi ga naite mo onaka wa suku yo shouka dekinakatta \"gomen gomen\"" },
             { time: 19.8, text: "sui mo amai mo katte ni tabete gomeiwaku okakeshite imasu" },
             { time: 26.7, text: "pakuchii na okite hen na aji no juusu kokoro wo mu ni shite nomikomimasu" },
@@ -50,21 +50,22 @@ class LyricsSystem {
             { time: 53, text: "mogumogu (mogumogu) mogumogu (mogumogu) sekai ga owaru mae ni" },
             { time: 59, text: "mogumogu yamii maji kami yamuyamu ari no manma" },
             { time: 66, text: "mogumogu yamii Magic Coming konoyo wo ajiwaun da umauma" },
-            { time: 67, text: "sorry i got lazy lmfao" },
-            // { time: 43.2, text: "riron busou no mirufiiyu sando (sando)" },
-            // { time: 44.9, text: "tokumei kibou no ourora sousu (sousu)" },
-            // { time: 48.3, text: "oozappa na ajitsuke oome ni mite yo oishiku nattara \"ok ok.\"" },
-            // { time: 51.1, text: "ii mon warui mon ippai tabete yogoreta kuchimoto nuguimasu" },
-            // { time: 55.2, text: "onigiri piza keiki gouka na furu kousu wana wo utagai ki wo tsukemasu" },
-            // { time: 58.6, text: "aa mousou ryuugen higo no baikingu de dokuiri to zeppin ryouri yoriwakete" },
-            // { time: 61.2, text: "kimi to mogumogu (mogumogu) mogumogu (mogumogu)" },
-            // { time: 64.5, text: "desugeimu no kyoushitsu de mogumogu (ugya~)" },
-            // { time: 67.9, text: "chimi mouryou harapeko no mure naka no ii kimi to ikinokoretara ii na" },
-            // { time: 162, text: "mogumogu (mogumogu) mogumogu (mogumogu) suu nen-go mo kono basho de" },
-            // { time: 74.6, text: "mogumogu yamii maji kami kon'ya no bangohan wa" },
-            // { time: 78, text: "mogumogu yamii Magic Coming tsugi wa omae no ban da" },
-            // { time: 81.4, text: "mogumogu yamii maji kami yamuyamu ari no manma" },
-            // { time: 84.8, text: "mogumogu yamii Magic Coming konoyo wo ajiwaun da umauma" },
+            { time: 77, text: "" },
+            { time: 80, text: "riron busou no mirufiiyu sando (sando)" },
+            { time: 83, text: "tokumei kibou no ourora sousu (sousu)" },
+            { time: 87, text: "oozappa na ajitsuke oome ni mite yo oishiku nattara \"ok ok.\"" },
+            { time: 93, text: "ii mon warui mon ippai tabete yogoreta kuchimoto nuguimasu" },
+            { time: 100, text: "onigiri piza keiki gouka na furu kousu wana wo utagai ki wo tsukemasu" },
+            { time: 106, text: "aa mousou ryuugen higo no baikingu de dokuiri to zeppin ryouri yoriwakete" },
+            { time: 113, text: "kimi to mogumogu (mogumogu) mogumogu (mogumogu)" },
+            { time: 117, text: "desugeimu no kyoushitsu de mogumogu (ugya~)" },
+            { time: 120, text: "chimi mouryou harapeko no mure naka no ii kimi to ikinokoretara ii na" },
+            { time: 127, text: "mogumogu (mogumogu) mogumogu (mogumogu) suu nen-go mo kono basho de" },
+            { time: 133, text: "mogumogu yamii maji kami kon'ya no bangohan wa" },
+            { time: 139, text: "mogumogu yamii Magic Coming tsugi wa omae no ban da" },
+            { time: 146, text: "mogumogu yamii maji kami yamuyamu ari no manma" },
+            { time: 152, text: "mogumogu yamii Magic Coming konoyo wo ajiwaun da umauma" },
+            { time: 160, text: "" },
             { time: 162, text: "mogumogu mogumogu mogumogu mogumogu mogumogu mogumogu mogumogu yamii" },
             { time: 168, text: "mogumogu mogumogu mogumogu mogumogu mogumogu mogumogu mogumogu yamii" },
             { time: 175, text: "mogumogu mogumogu mogumogu mogumogu mogumogu mogumogu mogumogu yamii" },
@@ -83,6 +84,7 @@ class LyricsSystem {
         if (adjustedTime >= this.songDuration && !this.isGameOver) {
             this.isGameOver = true;
             this.gameOverScreen = true;
+            this.showLyrics = false; // Ocultar letras cuando termine la canción
             return;
         }
         
@@ -120,26 +122,19 @@ class LyricsSystem {
         if (this.currentLyricIndex >= 0 && this.currentLyricIndex < this.lyrics.length) {
             const currentLyric = this.lyrics[this.currentLyricIndex];
             
-            // Configurar fuente (scaled)
-            ctx.font = '35px CustomFont, Arial, sans-serif';
+            // Configurar fuente (original)
+            ctx.font = '14px CustomFont, Arial, sans-serif';
             ctx.fillStyle = `rgba(255, 255, 255, ${this.fadeOpacity})`;
-            ctx.strokeStyle = `rgba(0, 0, 0, ${this.fadeOpacity})`;
-            ctx.lineWidth = 3;
             ctx.textAlign = 'center';
             
             // Dividir texto en líneas
             const lines = this.wrapText(currentLyric.text, ctx, this.maxLineWidth);
             
-            // Calcular posición Y inicial (scaled for 800x600)
+            // Calcular posición Y inicial (original for 320x240)
             const totalHeight = lines.length * this.lineHeight;
-            const startY = canvas.height - 450 - (totalHeight - this.lineHeight);
-            
-            // Agregar sombra
-            ctx.shadowColor = `rgba(0, 0, 0, ${this.fadeOpacity * 0.8})`;
-            ctx.shadowBlur = 4;
-            ctx.shadowOffsetX = 2;
-            ctx.shadowOffsetY = 2;
+            const startY = canvas.height - 180 - (totalHeight - this.lineHeight);
             
+
             // Dibujar cada línea
             lines.forEach((line, index) => {
                 const x = canvas.width / 2;
@@ -163,22 +158,7 @@ class LyricsSystem {
         this.showLyrics = !this.showLyrics;
         this.fadeDirection = this.showLyrics ? 1 : -1;
     }
-    
-    // Método para ajustar offset de tiempo
-    // seconds > 0: letras aparecen ANTES (más temprano)
-    // seconds < 0: letras aparecen DESPUÉS (más tarde)
-    adjustTimeOffset(seconds) {
-        this.timeOffset += seconds;
-        console.log(`Offset de tiempo ajustado a: ${this.timeOffset} segundos`);
-        console.log(`Positivo = letras ANTES, Negativo = letras DESPUÉS`);
-    }
-    
-    // Método para establecer offset de tiempo específico
-    setTimeOffset(seconds) {
-        this.timeOffset = seconds;
-        console.log(`Offset de tiempo establecido a: ${this.timeOffset} segundos`);
-    }
-    
+
     // Método para dividir texto en líneas
     wrapText(text, ctx, maxWidth) {
         const words = text.split(' ');

+ 58 - 23
js/player.js

@@ -2,18 +2,18 @@ class Player {
     constructor(x, y, spriteSheet) {
         this.x = x;
         this.y = y;
-        this.width = 48; // Scaled for larger canvas
-        this.height = 68; // Scaled for larger canvas
-        this.speed = 0.625; // Scaled speed
+        this.width = 24; // Original size
+        this.height = 34; // Original size
+        this.speed = 0.3125; // Original speed
         this.direction = 'right';
         
         // Jump physics
         this.velocityY = 0;
         this.velocityX = 0;
-        this.gravity = 0.3; // Gravedad más suave
-        this.jumpPower = -8; // Salto más suave
+        this.gravity = 0.4; // Gravedad más suave
+        this.jumpPower = -7; // Salto más suave
         this.airSpeed = 0.4; // Velocidad horizontal en el aire
-        this.groundY = 500; // Ground level (adjusted for new canvas size)
+        this.groundY = 176; // Ground level (original for 320x240)
         this.isOnGround = true;
         this.jumpPressed = false; // Para evitar saltos múltiples
         
@@ -40,7 +40,7 @@ class Player {
         
         // Teleport dash system
         this.teleportDashCooldown = 0;
-        this.teleportDashDistance = 100;
+        this.teleportDashDistance = 50;
         this.shiftPressed = false;
         
         // Animation
@@ -48,10 +48,12 @@ class Player {
         this.frameTimer = 0;
         this.idleFrameInterval = 60; // Más lento para idle
         this.walkFrameInterval = 15;  // Más rápido para caminar
+        this.crouchFrameInterval = 30; // Intervalo para animación de sentarse
         this.isMoving = false;
+        this.isCrouching = false;
+        this.stayingCrouched = false; // Para mantener el estado de agachado
         
         // Sprite coordinates from the 930x614 sprite sheet
-        // Adjust these based on your actual sprite positions
         this.sprites = {
             idle: [
                 { x: 370, y: 15, w: 24, h: 34 },
@@ -66,6 +68,10 @@ class Player {
                 { x: 80, y: 16, w: 24, h: 34 },
                 { x: 106, y: 16, w: 24, h: 34 },
                 { x: 132, y: 16, w: 24, h: 34 }
+            ],
+            crouch: [
+                { x: 287, y: 16, w: 24, h: 34 },
+                { x: 313, y: 16, w: 24, h: 34 }
             ]
         };
         
@@ -76,6 +82,18 @@ class Player {
     update() {
         this.isMoving = false;
         
+        // Detectar estado de agacharse
+        if (input.isCrouching() && !this.stayingCrouched) {
+            this.stayingCrouched = true;
+        }
+        
+        // Salir del estado agachado si se presionan teclas de movimiento o salto
+        if (this.stayingCrouched && (input.isMovingLeft() || input.isMovingRight() || input.isJumping())) {
+            this.stayingCrouched = false;
+        }
+        
+        this.isCrouching = this.stayingCrouched;
+        
         // Detectar teclas rápidas para dash
         this.detectRapidKeyPresses();
         
@@ -118,19 +136,21 @@ class Player {
         this.leftKeyPressed = currentLeftPressed;
         this.rightKeyPressed = currentRightPressed;
         
-        // Solo permitir movimiento si no se presionan ambas teclas a la vez
-        if (currentLeftPressed && !currentRightPressed) {
-            horizontalInput = -1;
-            this.direction = 'left';
-            this.isMoving = true;
-        } else if (currentRightPressed && !currentLeftPressed) {
-            horizontalInput = 1;
-            this.direction = 'right';
-            this.isMoving = true;
+        // Solo permitir movimiento si no se presionan ambas teclas a la vez y no está agachado
+        if (!this.isCrouching) {
+            if (currentLeftPressed && !currentRightPressed) {
+                horizontalInput = -1;
+                this.direction = 'left';
+                this.isMoving = true;
+            } else if (currentRightPressed && !currentLeftPressed) {
+                horizontalInput = 1;
+                this.direction = 'right';
+                this.isMoving = true;
+            }
         }
         
-        // Jump logic con control de una sola pulsación
-        if (input.isJumping() && this.isOnGround && !this.jumpPressed) {
+        // Jump logic con control de una sola pulsación (no saltar si está agachado)
+        if (input.isJumping() && this.isOnGround && !this.jumpPressed && !this.isCrouching) {
             this.velocityY = this.jumpPower;
             this.isOnGround = false;
             this.jumpPressed = true;
@@ -182,15 +202,23 @@ class Player {
             this.isOnGround = true;
         }
         
-        // Keep player within horizontal bounds (800px canvas width)
-        this.x = Math.max(0, Math.min(800 - this.width, this.x));
+        // Keep player within horizontal bounds (320px canvas width)
+        this.x = Math.max(0, Math.min(320 - this.width, this.x));
         
         // Update animation
         this.updateAnimation();
     }
     
     updateAnimation() {
-        const newAnimation = this.isMoving ? 'walk' : 'idle';
+        let newAnimation;
+        
+        if (this.isCrouching) {
+            newAnimation = 'crouch';
+        } else if (this.isMoving) {
+            newAnimation = 'walk';
+        } else {
+            newAnimation = 'idle';
+        }
         
         if (this.currentAnimation !== newAnimation) {
             this.currentAnimation = newAnimation;
@@ -202,7 +230,14 @@ class Player {
         const frames = this.sprites[this.currentAnimation];
         
         // Usar diferentes velocidades según la animación
-        const frameInterval = this.currentAnimation === 'walk' ? this.walkFrameInterval : this.idleFrameInterval;
+        let frameInterval;
+        if (this.currentAnimation === 'walk') {
+            frameInterval = this.walkFrameInterval;
+        } else if (this.currentAnimation === 'crouch') {
+            frameInterval = this.crouchFrameInterval;
+        } else {
+            frameInterval = this.idleFrameInterval;
+        }
         
         if (this.frameTimer >= frameInterval) {
             this.frameTimer = 0;

+ 29 - 2
js/touchControls.js

@@ -5,7 +5,8 @@ class TouchControls {
         this.touchZones = {
             left: { x: 0, y: 0, width: 0, height: 0 },
             right: { x: 0, y: 0, width: 0, height: 0 },
-            jump: { x: 0, y: 0, width: 0, height: 0 }
+            jump: { x: 0, y: 0, width: 0, height: 0 },
+            crouch: { x: 0, y: 0, width: 0, height: 0 }
         };
         
         this.lastTapTime = 0;
@@ -42,7 +43,15 @@ class TouchControls {
             x: 0,
             y: 0,
             width: width,
-            height: height * 0.5
+            height: height * 0.4
+        };
+        
+        // Zona de agacharse (centro inferior)
+        this.touchZones.crouch = {
+            x: width * 0.35,
+            y: height * 0.8,
+            width: width * 0.3,
+            height: height * 0.2
         };
     }
     
@@ -98,6 +107,12 @@ class TouchControls {
                 // Simular tecla W presionada
                 this.input.keys.W = true;
                 this.input.keys.w = true;
+            } else if (this.isInZone(pos, this.touchZones.crouch)) {
+                console.log('Crouch zone touched');
+                // Simular tecla ArrowDown presionada
+                this.input.keys.ArrowDown = true;
+                this.input.keys.s = true;
+                this.input.keys.S = true;
             } else if (this.isInZone(pos, this.touchZones.left)) {
                 console.log('Left zone touched');
                 // Simular tecla A presionada
@@ -122,6 +137,9 @@ class TouchControls {
         this.input.keys.d = false;
         this.input.keys.W = false;
         this.input.keys.w = false;
+        this.input.keys.ArrowDown = false;
+        this.input.keys.s = false;
+        this.input.keys.S = false;
         console.log('Touch ended - all keys released');
     }
     
@@ -179,6 +197,15 @@ class TouchControls {
             this.touchZones.jump.height
         );
         
+        // Zona de agacharse
+        ctx.fillStyle = '#ffff00';
+        ctx.fillRect(
+            this.touchZones.crouch.x,
+            this.touchZones.crouch.y,
+            this.touchZones.crouch.width,
+            this.touchZones.crouch.height
+        );
+        
         ctx.restore();
     }