瀏覽代碼

Okayu Scroller V2

Geovanny Andrés Vega Mite 4 月之前
父節點
當前提交
7165d71848
共有 9 個文件被更改,包括 892 次插入184 次删除
  1. 103 63
      README.md
  2. 33 0
      assets/favicon.svg
  3. 109 8
      css/styles.css
  4. 14 8
      index.html
  5. 108 81
      js/game.js
  6. 17 3
      js/input.js
  7. 8 8
      js/lyrics.js
  8. 309 13
      js/player.js
  9. 191 0
      js/touchControls.js

+ 103 - 63
README.md

@@ -1,93 +1,133 @@
 # Nekomata Okayu Scroller 🐱
 
-Un minijuego simple y encantador estilo pixel art basado en la VTuber Nekomata Okayu. Un scroller lateral donde puedes controlar a Okayu mientras explora un mundo pixelado.
-
-## 🎮 Controles
-
-- **A / ←** - Moverse a la izquierda
-- **D / →** - Moverse a la derecha  
-- **W / ↑** - Saltar
-
-## 🎯 Características
-
-- ✅ **Movimiento fluido** - Control preciso con animaciones suaves
-- ✅ **Física simple** - Gravedad y colisiones básicas
-- ✅ **Animaciones** - Sprites animados para idle y caminar
-- ✅ **Dirección dinámica** - El personaje mira hacia donde se mueve
-- ✅ **Música de fondo** - Reproduce música al hacer clic
-- ✅ **Pixel art** - Estilo retro pixelado con efectos crisp
-- ✅ **Responsive** - Se adapta a diferentes tamaños de pantalla
-
-## 🚀 Cómo usar
-
-1. **Prepara tus assets:**
-   - Coloca tu sprite sheet en `assets/sprites.png` (930x614px)
-   - Coloca tu música en `assets/music.mp3`
-   - Ajusta las coordenadas de sprites en `js/player.js` según tu sprite sheet
-
-2. **Abre el juego:**
-   - Simplemente abre `index.html` en tu navegador
-   - Haz clic en el canvas para iniciar la música
-   - ¡Empieza a jugar!
-
-## 🎨 Personalización
-
-### Coordenadas de sprites
-En `js/player.js`, ajusta las coordenadas de sprites según tu sprite sheet:
+A simple and charming pixel art mini-game based on VTuber Nekomata Okayu. A side-scrolling experience where you can control Okayu while she explores a pixelated world with synchronized lyrics.
+
+## 🎮 Controls
+
+- **A / ←** - Move left
+- **D / →** - Move right
+- **W / ↑** - Jump
+- **Rapid A/D alternation** - Lateral dash with smoke effects (alternate left/right quickly)
+- **Shift + Direction** - Teleport dash (instant movement with smoke trail)
+- **L** - Show/Hide lyrics
+- **R** - Restart game
+
+## 🎯 Features
+
+- ✅ **Smooth Movement** - Precise control with fluid animations
+- ✅ **Jump Mechanics** - Smooth physics-based jumping with directional momentum
+- ✅ **Lateral Dash System** - Rapid movement by alternating left/right keys quickly
+- ✅ **Teleport Dash System** - Instant directional movement with Shift + direction keys
+- ✅ **Character Animations** - Animated sprites for idle and walking states
+- ✅ **Dynamic Direction** - Character faces the direction of movement
+- ✅ **Background Music** - Plays music when you click the canvas
+- ✅ **Synchronized Lyrics** - Real-time lyrics display synced with the music
+- ✅ **Dynamic Background** - Procedurally generated clouds and background objects
+- ✅ **Pixel Art Style** - Retro pixelated aesthetic with crisp rendering
+- ✅ **Responsive Design** - Adapts to different screen sizes
+- ✅ **Game Over Screen** - Displays when the song ends
+- ✅ **Restart Functionality** - Press R to restart the game anytime
+
+## 🚀 How to Use
+
+1. **Prepare your assets:**
+   - Place your sprite sheet in `assets/sprites.png` (930x614px)
+   - Place your music in `assets/music.mp3`
+   - Adjust sprite coordinates in `js/player.js` according to your sprite sheet
+
+2. **Open the game:**
+   - Simply open `index.html` in your browser
+   - Click on the canvas to start the music
+   - Start playing!
+
+## 🎨 Customization
+
+### Sprite Coordinates
+In `js/player.js`, adjust sprite coordinates according to your sprite sheet:
 
 ```javascript
 this.sprites = {
     idle: [
-        { x: 0, y: 0, w: 32, h: 32 },
-        // ... más frames
+        { x: 370, y: 15, w: 24, h: 34 },
+        // ... more frames
     ],
     walk: [
-        { x: 0, y: 32, w: 32, h: 32 },
-        // ... más frames
+        { x: 2, y: 16, w: 24, h: 34 },
+        // ... more frames
     ]
 };
 ```
 
-### Física del juego
-Ajusta estos valores en `js/player.js`:
-- `speed` - Velocidad de movimiento horizontal
-- `jumpPower` - Fuerza del salto
-- `gravity` - Intensidad de la gravedad
+### Game Physics
+Adjust these values in `js/player.js`:
+- `speed` - Horizontal movement speed
+- `width` and `height` - Character size
+- Canvas bounds are automatically handled
 
-## 📁 Estructura del proyecto
+### Lyrics Synchronization
+In `js/lyrics.js`, you can:
+- Adjust `timeOffset` to sync lyrics with music
+- Modify lyrics data in the `parseLyrics()` method
+- Toggle lyrics visibility with the L key
+
+## 📁 Project Structure
 
 ```
 okayuScroller/
-├── index.html          # Página principal
+├── index.html              # Main page
 ├── css/
-│   └── styles.css      # Estilos del juego
+│   └── styles.css          # Game styles
 ├── js/
-│   ├── game.js         # Lógica principal del juego
-│   ├── player.js       # Clase del jugador
-│   └── input.js        # Manejo de controles
+│   ├── game.js             # Main game logic
+│   ├── player.js           # Player class
+│   ├── input.js            # Input handling
+│   ├── lyrics.js           # Lyrics system
+│   └── spriteProcessor.js  # Sprite processing
 ├── assets/
-│   ├── sprites.png     # Sprite sheet (930x614px)
-│   └── music.mp3       # Música de fondo
-└── README.md           # Este archivo
+│   ├── sprites.png         # Sprite sheet (930x614px)
+│   ├── music.mp3           # Background music
+│   ├── font.ttf            # Custom font for lyrics
+│   └── favicon.svg         # Game favicon
+└── README.md               # This file
 ```
 
-## 🛠️ Tecnologías usadas
+## 🛠️ Technologies Used
 
-- **HTML5 Canvas** - Renderizado del juego
-- **JavaScript ES6+** - Lógica del juego
-- **CSS3** - Estilos y diseño responsive
-- **Pixel art** - Estilo visual retro
+- **HTML5 Canvas** - Game rendering
+- **JavaScript ES6+** - Game logic
+- **CSS3** - Styling and responsive design
+- **SVG** - Favicon creation
+- **Pixel art** - Retro visual style
 
-## 🎵 Notas sobre la música
+## 🎵 Music Notes
 
-El juego espera un archivo `music.mp3` en la carpeta `assets/`. La música se reproduce automáticamente después de hacer clic en el canvas (requerido por las políticas de navegadores modernos).
+The game expects a `music.mp3` file in the `assets/` folder. Music plays automatically after clicking the canvas (required by modern browser policies).
 
-## 📱 Compatibilidad
+## 📱 Compatibility
 
 - ✅ Chrome, Firefox, Safari, Edge
-- ✅ Móviles (responsive)
-- ✅ Requiere JavaScript habilitado
+- ✅ Mobile devices (responsive)
+- ✅ Requires JavaScript enabled
+- ✅ Scales automatically to screen size
+
+## 🎮 Game Systems
+
+### Cloud System
+- Procedurally generates background clouds
+- Configurable spawn rates and movement speeds
+- Automatic cleanup of off-screen elements
+
+### Background Object System
+- Spawns groups of decorative objects
+- Multiple sprite variants for variety
+- Configurable group sizes and spacing
+
+### Lyrics System
+- Real-time synchronization with music
+- Adjustable time offset for perfect sync
+- Fade in/out transitions
+- Multi-line text wrapping
 
 ---
 
-¡Disfruta jugando con Okayu! 🐾
+Enjoy playing with Okayu! 🐾

+ 33 - 0
assets/favicon.svg

@@ -0,0 +1,33 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
+  <!-- Cat face background -->
+  <circle cx="16" cy="16" r="14" fill="#f5f5f5" stroke="#333" stroke-width="1"/>
+  
+  <!-- Cat ears -->
+  <polygon points="8,8 12,2 16,8" fill="#f5f5f5" stroke="#333" stroke-width="1"/>
+  <polygon points="16,8 20,2 24,8" fill="#f5f5f5" stroke="#333" stroke-width="1"/>
+  
+  <!-- Inner ears -->
+  <polygon points="9,7 11,4 13,7" fill="#ffb3d9"/>
+  <polygon points="19,7 21,4 23,7" fill="#ffb3d9"/>
+  
+  <!-- Eyes -->
+  <ellipse cx="12" cy="14" rx="2" ry="3" fill="#333"/>
+  <ellipse cx="20" cy="14" rx="2" ry="3" fill="#333"/>
+  
+  <!-- Eye highlights -->
+  <ellipse cx="12.5" cy="13" rx="0.5" ry="1" fill="white"/>
+  <ellipse cx="20.5" cy="13" rx="0.5" ry="1" fill="white"/>
+  
+  <!-- Nose -->
+  <polygon points="16,18 14,20 18,20" fill="#ffb3d9"/>
+  
+  <!-- Mouth -->
+  <path d="M 16 20 Q 13 22 11 20" stroke="#333" stroke-width="1" fill="none"/>
+  <path d="M 16 20 Q 19 22 21 20" stroke="#333" stroke-width="1" fill="none"/>
+  
+  <!-- Whiskers -->
+  <line x1="6" y1="16" x2="10" y2="15" stroke="#333" stroke-width="1"/>
+  <line x1="6" y1="18" x2="10" y2="18" stroke="#333" stroke-width="1"/>
+  <line x1="22" y1="15" x2="26" y2="16" stroke="#333" stroke-width="1"/>
+  <line x1="22" y1="18" x2="26" y2="18" stroke="#333" stroke-width="1"/>
+</svg>

+ 109 - 8
css/styles.css

@@ -14,6 +14,25 @@ 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;
@@ -28,16 +47,31 @@ body {
     image-rendering: -moz-crisp-edges;
     image-rendering: crisp-edges;
     cursor: pointer;
+    max-width: 100%;
+    height: auto;
+    width: 800px;
+    max-height: 80vh;
 }
 
 .controls {
-    margin-top: 15px;
-    font-size: 12px;
-    color: #ccc;
+    text-align: center;
+    margin-top: 20px;
+    font-size: 13px;
+    opacity: 0.9;
+    background: rgba(0,0,0,0.3);
+    padding: 15px;
+    border-radius: 10px;
+    backdrop-filter: blur(5px);
 }
 
 .controls p {
-    margin: 0;
+    margin: 8px 0;
+    line-height: 1.4;
+}
+
+.controls strong {
+    color: #4ecdc4;
+    text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
 }
 
 /* Estilos para las letras */
@@ -54,14 +88,81 @@ body {
 }
 
 /* Responsive design */
-@media (max-width: 400px) {
-    .game-container {
+@media (max-width: 900px) {
+    body {
         padding: 10px;
     }
     
+    .game-container {
+        margin: 0;
+    }
+    
+    .game-title {
+        font-size: 2em;
+        margin-bottom: 15px;
+    }
+    
     #gameCanvas {
-        width: 100%;
-        max-width: 320px;
+        max-width: 100%;
         height: auto;
     }
+    
+    .controls {
+        font-size: 11px;
+        margin-top: 10px;
+        padding: 12px;
+    }
+}
+
+@media (max-width: 600px) {
+    body {
+        padding: 5px;
+    }
+    
+    .game-title {
+        font-size: 1.5em;
+        margin-bottom: 10px;
+    }
+    
+    .controls {
+        font-size: 10px;
+        margin-top: 8px;
+        padding: 10px;
+    }
+    
+    .controls p {
+        margin: 6px 0;
+    }
+}
+
+@media (max-height: 700px) {
+    #gameCanvas {
+        max-height: 70vh;
+    }
+    
+    .game-title {
+        font-size: 1.8em;
+        margin-bottom: 10px;
+    }
+}
+
+/* Estilos específicos para dispositivos táctiles */
+@media (hover: none) and (pointer: coarse) {
+    .controls {
+        background: rgba(0,0,0,0.5);
+        border: 2px solid #4ecdc4;
+    }
+    
+    .controls p:last-child {
+        color: #4ecdc4;
+        font-weight: bold;
+        margin-top: 10px;
+    }
+    
+    #gameCanvas {
+        touch-action: none;
+        user-select: none;
+        -webkit-user-select: none;
+        -webkit-touch-callout: none;
+    }
 }

+ 14 - 8
index.html

@@ -1,26 +1,32 @@
 <!DOCTYPE html>
-<html lang="es">
+<html lang="en">
+
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Nekomata Okayu Scroller</title>
+    <link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
     <link rel="stylesheet" href="css/styles.css">
 </head>
+
 <body>
     <div class="game-container">
-        <canvas id="gameCanvas" width="320" height="240"></canvas>
+        <h1 class="game-title">Nekomata Okayu Scroller</h1>
+        <canvas id="gameCanvas" width="800" height="600"></canvas>
         <div class="controls">
-            <p>AD o ← → para moverte</p>
-            <p>L - Mostrar/Ocultar letras</p>
-            <p>R - Reiniciar juego</p>
-            <!-- <p>Ctrl+G - Simular fin de canción (desarrollo)</p> -->
+            <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>
-    
+
     <script src="js/input.js"></script>
+    <script src="js/touchControls.js"></script>
     <script src="js/spriteProcessor.js"></script>
     <script src="js/player.js"></script>
     <script src="js/lyrics.js"></script>
     <script src="js/game.js"></script>
 </body>
-</html>
+
+</html>

+ 108 - 81
js/game.js

@@ -124,14 +124,20 @@ class Game {
         });
         
         // Handle audio context (for browsers that require user interaction)
-        this.canvas.addEventListener('click', () => {
+        const startMusic = () => {
             if (this.backgroundMusic.paused && this.gameState === 'playing') {
                 this.backgroundMusic.play().catch(e => console.log('Audio play failed:', e));
             }
-        });
+        };
+        
+        this.canvas.addEventListener('click', startMusic);
+        this.canvas.addEventListener('touchstart', startMusic, { passive: true });
         
         // Agregar controles de teclado
         this.setupKeyboardControls();
+        
+        // Inicializar controles táctiles para dispositivos móviles
+        this.touchControls = new TouchControls(this.canvas, input);
     }
     
     // Método para agregar elementos de forma dinámica
@@ -195,13 +201,13 @@ class Game {
     }
     
     startGame() {
-        // Create player
-        this.player = new Player(144, 176, this.processedSpriteSheet);
+        // Create player (scaled for 800x600 canvas)
+        this.player = new Player(360, 500, this.processedSpriteSheet);
         
         // Crear elementos de fondo usando los nuevos métodos
-        // Piso
-        for (let i = 0; i < 10; i++) {
-            this.addFloorTile(i * 32, 208);
+        // 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)
         }
         
         // Inicializar sistema de nubes
@@ -210,21 +216,21 @@ class Game {
         // Inicializar sistema de objetos de fondo
         this.backgroundObjectSystem.init(this.processedSpriteSheet, this.backgroundLayers.mid);
         
-        // 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 });
+        // 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 });
 
-        // Candybar
-        this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 });
+        // Candybar (scaled) - adjusted to ground level
+        this.addSprite(500, 410, 96, 158, { x: 179, y: 438, w: 48, h: 79 });
 
-        // Cake
-        this.addSprite(260, 134, 100, 75, { x: 461, y: 414, w: 100, h: 75 });
+        // Cake (scaled) - adjusted to ground level
+        this.addSprite(650, 418, 200, 150, { x: 461, y: 414, w: 100, h: 75 });
 
-        // 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 });
+        // 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 });
         
         this.gameLoop();
     }
@@ -363,12 +369,12 @@ class Game {
             this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
             
             this.ctx.fillStyle = 'white';
-            this.ctx.font = '8px Courier New';
+            this.ctx.font = '20px Courier New';
             this.ctx.textAlign = 'center';
-            this.ctx.fillText('Click para iniciar música', 160, 120);
-            this.ctx.fillText('AD o ← → para moverte', 160, 130);
-            this.ctx.fillText('L - Mostrar/Ocultar letras', 160, 140);
-            this.ctx.fillText('R - Reiniciar juego', 160, 150);
+            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);
         }
         
@@ -410,16 +416,16 @@ class Game {
         
         // Texto de fin de juego
         this.ctx.fillStyle = 'white';
-        this.ctx.font = '16px Courier New';
+        this.ctx.font = '40px Courier New';
         this.ctx.textAlign = 'center';
         
-        this.ctx.fillText('¡Canción terminada!', 160, 100);
-        this.ctx.fillText('¿Quieres jugar otra vez?', 160, 120);
+        this.ctx.fillText('Song finished!', 400, 250);
+        this.ctx.fillText('Want to play again?', 400, 300);
         
         // Botones
-        this.ctx.font = '12px Courier New';
-        this.ctx.fillText('Presiona R para reiniciar', 160, 150);
-        this.ctx.fillText('o recarga la página', 160, 165);
+        this.ctx.font = '24px Courier New';
+        this.ctx.fillText('Press R to restart', 400, 375);
+        this.ctx.fillText('or reload the page', 400, 412);
     }
     
     restartGame() {
@@ -451,26 +457,26 @@ class Game {
     }
     
     recreateBackgroundElements() {
-        // Piso
-        for (let i = 0; i < 10; i++) {
-            this.addFloorTile(i * 32, 208);
+        // 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)
         }
         
-        // 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 });
+        // 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 });
 
-        // Candybar
-        this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 });
+        // Candybar (scaled) - adjusted to ground level
+         this.addSprite(500, 410, 96, 158, { x: 179, y: 438, w: 48, h: 79 });
 
-        // Cake
-        this.addSprite(260, 134, 100, 75, { x: 461, y: 414, w: 100, h: 75 });
+         // Cake (scaled) - adjusted to ground level
+         this.addSprite(650, 418, 200, 150, { x: 461, y: 414, w: 100, h: 75 });
 
-        // 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 });
+        // 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 });
     }
 }
 
@@ -482,15 +488,15 @@ class CloudSystem {
         this.cloudLayer = null;
         this.frameCount = 0;
         
-        // Configuración simplificada
+        // Configuración simplificada (scaled for 800x600 canvas)
         this.config = {
             spawnRate: 0.1,           // Probabilidad de spawn por frame (aumentada)
-            maxClouds: 2,               // Máximo número de nubes en pantalla (aumentado)
-            speedRange: { min: 0.1, max: 0.15 },  // Velocidad de movimiento
-            yRange: { min: 1, max: 120 },        // Rango de altura
+            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)
             spriteCoords: { x: 231, y: 112, w: 287, h: 148 },
-            size: { w: 287, h: 148 },
-            minSpacing: 100,            // Espaciado mínimo entre nubes (reducido)
+            size: { w: 574, h: 296 },  // Scaled cloud size
+            minSpacing: 250,            // Espaciado mínimo entre nubes (scaled)
             minTimeBetweenSpawns: 60    // Frames mínimos entre apariciones (reducido)
         };
         
@@ -647,16 +653,16 @@ class BackgroundObjectSystem {
         this.objectLayer = null;
         this.frameCount = 0;
         
-        // Configuración para objetos de fondo
+        // Configuración para objetos de fondo (scaled for 800x600 canvas)
         this.config = {
-            spawnRate: 0.3,           // Probabilidad de spawn por frame (aumentada)
-            maxObjects: 300,              // Máximo número de objetos en pantalla (aumentado)
-            speedRange: { min: 0.05, max: 0.1 },  // Velocidad de movimiento
-            yRange: { min: 10, max: 180 },        // Rango de altura (expandido)
-            minSpacing: 80,            // Espaciado mínimo entre grupos (reducido)
-            minTimeBetweenSpawns: 60,  // Frames mínimos entre apariciones (reducido)
-            groupSize: { min: 15, max: 25 }, // Tamaño del grupo de objetos (reducido)
-            groupSpacing: { min: 5, max: 40 } // Espaciado entre objetos en el grupo (ajustado)
+            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
         };
         
         this.lastSpawnTime = 0;
@@ -671,16 +677,25 @@ class BackgroundObjectSystem {
     }
     
     spawnInitialObjects() {
-        // Generar 3-5 grupos iniciales para llenar la pantalla
-        const initialGroups = Math.floor(Math.random() * 3) + 3;
-        
-        for (let i = 0; i < initialGroups; i++) {
-            // Distribuir los grupos iniciales por toda la pantalla
-            const x = (i * this.canvas.width / initialGroups) + (Math.random() * 100);
-            const y = this.getRandomY();
-            const speed = this.getRandomSpeed();
-            
-            this.createObjectGroup(x, y, speed);
+        // Generar objetos en múltiples filas para mejor distribución
+        const layers = 3; // Número de capas de profundidad
+        const groupsPerLayer = 4; // Grupos por capa
+        
+        for (let layer = 0; layer < layers; layer++) {
+            for (let i = 0; i < groupsPerLayer; i++) {
+                // Distribuir horizontalmente
+                const x = (i * this.canvas.width / groupsPerLayer) + (Math.random() * 150);
+                
+                // Crear diferentes niveles de altura
+                const baseY = 100 + (layer * 120); // Capas a diferentes alturas
+                const y = baseY + (Math.random() * 80 - 40); // Variación aleatoria
+                
+                // Velocidad ligeramente diferente por capa para efecto parallax
+                const baseSpeed = this.getRandomSpeed();
+                const speed = baseSpeed * (1 + layer * 0.1);
+                
+                this.createObjectGroup(x, y, speed);
+            }
         }
     }
     
@@ -742,16 +757,26 @@ class BackgroundObjectSystem {
     }
     
     spawnObjectGroup() {
-        const y = this.getRandomY();
-        const speed = this.getRandomSpeed();
+        // Generar múltiples grupos a diferentes alturas simultáneamente
+        const numLayers = Math.floor(Math.random() * 2) + 2; // 2-3 capas
         
-        // Generar desde la derecha de la pantalla
-        const x = this.canvas.width;
+        for (let i = 0; i < numLayers; i++) {
+            // Diferentes alturas para cada capa
+            const baseY = 100 + (i * 150);
+            const y = baseY + (Math.random() * 100 - 50);
+            
+            // Velocidad ligeramente diferente por capa
+            const baseSpeed = this.getRandomSpeed();
+            const speed = baseSpeed * (1 + i * 0.15);
+            
+            // Generar desde la derecha de la pantalla con ligera variación
+            const x = this.canvas.width + (Math.random() * 100);
+            
+            this.createObjectGroup(x, y, speed);
+        }
         
-        this.createObjectGroup(x, y, speed);
         this.lastSpawnTime = this.frameCount;
-        
-        console.log(`Nuevo grupo de objetos generado en (${x}, ${y}) con velocidad ${speed}`);
+        console.log(`Nuevos grupos de objetos generados en múltiples capas`);
     }
     
     createObjectGroup(x, y, speed) {
@@ -774,12 +799,14 @@ 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;
             const object = {
                 type: 'backgroundObject',
                 x: currentX,
                 y: y + yOffset,
-                w: spriteVariant.w,
-                h: spriteVariant.h,
+                w: spriteVariant.w * scaleFactor,
+                h: spriteVariant.h * scaleFactor,
                 spriteCoords: spriteVariant,
                 moveSpeed: speed,
                 rotation: rotation,
@@ -788,11 +815,11 @@ class BackgroundObjectSystem {
             
             this.objectLayer.push(object);
             
-            // Calcular siguiente posición con espaciado aleatorio
+            // Calcular siguiente posición con espaciado aleatorio (adjusted for scaled objects)
             const spacing = Math.random() * 
                 (this.config.groupSpacing.max - this.config.groupSpacing.min) + 
                 this.config.groupSpacing.min;
-            currentX += spriteVariant.w + spacing;
+            currentX += (spriteVariant.w * scaleFactor) + spacing;
         }
     }
     

+ 17 - 3
js/input.js

@@ -2,9 +2,15 @@ class InputHandler {
     constructor() {
         this.keys = {
             a: false,
+            A: false,
             d: false,
+            D: false,
+            w: false,
+            W: false,
+            Shift: false,
             ArrowLeft: false,
-            ArrowRight: false
+            ArrowRight: false,
+            ArrowUp: false
         };
         
         this.init();
@@ -27,11 +33,19 @@ class InputHandler {
     }
     
     isMovingLeft() {
-        return this.keys.a || this.keys.ArrowLeft;
+        return this.keys.a || this.keys.A || this.keys.ArrowLeft;
     }
     
     isMovingRight() {
-        return this.keys.d || this.keys.ArrowRight;
+        return this.keys.d || this.keys.D || this.keys.ArrowRight;
+    }
+    
+    isJumping() {
+        return this.keys.w || this.keys.W || this.keys.ArrowUp;
+    }
+    
+    isShiftPressed() {
+        return this.keys.Shift;
     }
 }
 

+ 8 - 8
js/lyrics.js

@@ -15,9 +15,9 @@ class LyricsSystem {
         // Valor negativo = letras aparecen DESPUÉS
         this.timeOffset = 0.25;
         
-        // Configuración de texto
-        this.maxLineWidth = 280; // Ancho máximo de línea en píxeles
-        this.lineHeight = 18; // Altura entre líneas
+        // 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)
         
         // Cargar la fuente personalizada
         this.loadCustomFont();
@@ -120,8 +120,8 @@ class LyricsSystem {
         if (this.currentLyricIndex >= 0 && this.currentLyricIndex < this.lyrics.length) {
             const currentLyric = this.lyrics[this.currentLyricIndex];
             
-            // Configurar fuente
-            ctx.font = '14px CustomFont, Arial, sans-serif';
+            // Configurar fuente (scaled)
+            ctx.font = '35px CustomFont, Arial, sans-serif';
             ctx.fillStyle = `rgba(255, 255, 255, ${this.fadeOpacity})`;
             ctx.strokeStyle = `rgba(0, 0, 0, ${this.fadeOpacity})`;
             ctx.lineWidth = 3;
@@ -130,9 +130,9 @@ class LyricsSystem {
             // Dividir texto en líneas
             const lines = this.wrapText(currentLyric.text, ctx, this.maxLineWidth);
             
-            // Calcular posición Y inicial
+            // Calcular posición Y inicial (scaled for 800x600)
             const totalHeight = lines.length * this.lineHeight;
-            const startY = canvas.height - 180 - (totalHeight - this.lineHeight);
+            const startY = canvas.height - 450 - (totalHeight - this.lineHeight);
             
             // Agregar sombra
             ctx.shadowColor = `rgba(0, 0, 0, ${this.fadeOpacity * 0.8})`;
@@ -216,4 +216,4 @@ class LyricsSystem {
         this.fadeOpacity = 1.0;
         this.timeOffset = 0; // Resetear offset
     }
-} 
+}

+ 309 - 13
js/player.js

@@ -2,11 +2,47 @@ class Player {
     constructor(x, y, spriteSheet) {
         this.x = x;
         this.y = y;
-        this.width = 24;
-        this.height = 34;
-        this.speed = 0.25;
+        this.width = 48; // Scaled for larger canvas
+        this.height = 68; // Scaled for larger canvas
+        this.speed = 0.625; // Scaled 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.airSpeed = 0.4; // Velocidad horizontal en el aire
+        this.groundY = 500; // Ground level (adjusted for new canvas size)
+        this.isOnGround = true;
+        this.jumpPressed = false; // Para evitar saltos múltiples
+        
+        // Dash system
+        this.dashCooldown = 0;
+        this.isDashing = false;
+        this.dashDuration = 0;
+        this.dashSpeed = 8;
+        this.lastKeyPresses = [];
+        this.keyPressWindow = 300; // ms para detectar teclas rápidas (reducido)
+        this.smokeParticles = [];
+        
+        // Vibration effect
+        this.isVibrating = false;
+        this.vibrationDuration = 0;
+        this.vibrationIntensity = 2;
+        this.originalX = 0;
+        this.originalY = 0;
+        
+        // Key state tracking for proper alternation detection
+        this.leftKeyPressed = false;
+        this.rightKeyPressed = false;
+        this.lastKeyReleased = null;
+        
+        // Teleport dash system
+        this.teleportDashCooldown = 0;
+        this.teleportDashDistance = 100;
+        this.shiftPressed = false;
+        
         // Animation
         this.frameIndex = 0;
         this.frameTimer = 0;
@@ -40,20 +76,114 @@ class Player {
     update() {
         this.isMoving = false;
         
-        // Horizontal movement
-        if (input.isMovingLeft()) {
-            this.x -= this.speed;
+        // Detectar teclas rápidas para dash
+        this.detectRapidKeyPresses();
+        
+        // Actualizar dash cooldowns y vibración
+        if (this.dashCooldown > 0) this.dashCooldown--;
+        if (this.dashDuration > 0) this.dashDuration--;
+        if (this.dashDuration <= 0) this.isDashing = false;
+        if (this.teleportDashCooldown > 0) this.teleportDashCooldown--;
+        if (this.vibrationDuration > 0) {
+            this.vibrationDuration--;
+            if (this.vibrationDuration <= 0) {
+                this.isVibrating = false;
+            }
+        }
+        
+        // Detectar dash direccional con Shift
+        if (input.isShiftPressed() && !this.shiftPressed && this.teleportDashCooldown <= 0) {
+            if (input.isMovingLeft() || input.isMovingRight()) {
+                this.performTeleportDash(input.isMovingLeft() ? 'left' : 'right');
+            }
+        }
+        this.shiftPressed = input.isShiftPressed();
+        
+        // Horizontal movement con detección de alternancia mejorada
+        let horizontalInput = 0;
+        
+        // Detectar cambios en el estado de las teclas
+        const currentLeftPressed = input.isMovingLeft();
+        const currentRightPressed = input.isMovingRight();
+        
+        // Registrar cuando se presiona una tecla nueva
+        if (currentLeftPressed && !this.leftKeyPressed) {
+            this.recordKeyPress('left');
+        }
+        if (currentRightPressed && !this.rightKeyPressed) {
+            this.recordKeyPress('right');
+        }
+        
+        // Actualizar estado de teclas
+        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;
-        }
-        if (input.isMovingRight()) {
-            this.x += this.speed;
+        } else if (currentRightPressed && !currentLeftPressed) {
+            horizontalInput = 1;
             this.direction = 'right';
             this.isMoving = true;
         }
         
-        // Keep player within horizontal bounds
-        this.x = Math.max(0, Math.min(320 - this.width, this.x));
+        // Jump logic con control de una sola pulsación
+        if (input.isJumping() && this.isOnGround && !this.jumpPressed) {
+            this.velocityY = this.jumpPower;
+            this.isOnGround = false;
+            this.jumpPressed = true;
+            
+            // Salto direccional - agregar velocidad horizontal si se está moviendo
+            if (horizontalInput !== 0) {
+                this.velocityX = horizontalInput * 3; // Impulso horizontal al saltar
+            }
+        }
+        
+        // Resetear jumpPressed cuando se suelta la tecla
+        if (!input.isJumping()) {
+            this.jumpPressed = false;
+        }
+        
+        // Movimiento horizontal con dash
+        if (this.isDashing) {
+            // Durante el dash, movimiento súper rápido
+            this.x += this.direction === 'left' ? -this.dashSpeed : this.dashSpeed;
+            this.createSmokeParticle();
+        } else {
+            // Movimiento normal
+            if (this.isOnGround) {
+                this.x += horizontalInput * this.speed;
+                this.velocityX *= 0.8; // Fricción en el suelo
+            } else {
+                // Movimiento en el aire - más limitado pero posible
+                this.x += horizontalInput * this.airSpeed;
+            }
+            
+            // Aplicar velocidad horizontal (para el salto direccional)
+            this.x += this.velocityX;
+            
+            // Fricción del aire para velocidad horizontal
+            this.velocityX *= 0.95;
+        }
+        
+        // Actualizar partículas de humo
+        this.updateSmokeParticles();
+        
+        // Apply gravity
+        this.velocityY += this.gravity;
+        this.y += this.velocityY;
+        
+        // Ground collision
+        if (this.y >= this.groundY) {
+            this.y = this.groundY;
+            this.velocityY = 0;
+            this.isOnGround = true;
+        }
+        
+        // Keep player within horizontal bounds (800px canvas width)
+        this.x = Math.max(0, Math.min(800 - this.width, this.x));
         
         // Update animation
         this.updateAnimation();
@@ -83,6 +213,20 @@ class Player {
     draw(ctx) {
         const frame = this.sprites[this.currentAnimation][this.frameIndex];
         
+        // Calcular posición con vibración
+        let drawX = this.x;
+        let drawY = this.y;
+        
+        if (this.isVibrating) {
+            drawX += (Math.random() - 0.5) * this.vibrationIntensity;
+            drawY += (Math.random() - 0.5) * this.vibrationIntensity;
+            
+            // Crear humo continuo durante la vibración
+            if (Math.random() < 0.3) {
+                this.createHeadSmokeParticle();
+            }
+        }
+        
         ctx.save();
         
         if (this.direction === 'left') {
@@ -90,18 +234,170 @@ class Player {
             ctx.drawImage(
                 this.spriteSheet,
                 frame.x, frame.y, frame.w, frame.h,
-                -this.x - this.width, this.y,
+                -drawX - this.width, drawY,
                 this.width, this.height
             );
         } else {
             ctx.drawImage(
                 this.spriteSheet,
                 frame.x, frame.y, frame.w, frame.h,
-                this.x, this.y,
+                drawX, drawY,
                 this.width, this.height
             );
         }
         
         ctx.restore();
+        
+        // Dibujar partículas de humo
+        this.drawSmokeParticles(ctx);
+    }
+    
+    recordKeyPress(direction) {
+        const now = Date.now();
+        this.lastKeyPresses.push({ direction, time: now });
+        
+        // Limpiar teclas antiguas
+        this.lastKeyPresses = this.lastKeyPresses.filter(press => 
+            now - press.time < this.keyPressWindow
+        );
+    }
+    
+    detectRapidKeyPresses() {
+        if (this.dashCooldown > 0 || this.isDashing) return;
+        
+        const now = Date.now();
+        const recentPresses = this.lastKeyPresses.filter(press => 
+            now - press.time < this.keyPressWindow
+        );
+        
+        // Detectar alternancia rápida entre izquierda y derecha (simplificado)
+        if (recentPresses.length >= 3) {
+            let alternating = true;
+            for (let i = 1; i < recentPresses.length; i++) {
+                if (recentPresses[i].direction === recentPresses[i-1].direction) {
+                    alternating = false;
+                    break;
+                }
+            }
+            
+            if (alternating) {
+                this.startLateralDash();
+                console.log('¡Dash lateral activado!');
+            }
+        }
+    }
+    
+    startLateralDash() {
+        this.isDashing = true;
+        this.dashDuration = 15; // frames
+        this.dashCooldown = 60; // frames antes de poder hacer otro dash
+        this.lastKeyPresses = []; // Limpiar historial
+        
+        // Activar vibración
+        this.isVibrating = true;
+        this.vibrationDuration = 45; // frames de vibración
+        this.originalX = this.x;
+        this.originalY = this.y;
+        
+        // Crear humo tipo fogata encima de la cabeza
+        for (let i = 0; i < 15; i++) {
+            this.createHeadSmokeParticle();
+        }
+    }
+    
+    performTeleportDash(direction) {
+        this.teleportDashCooldown = 90; // frames antes de poder hacer otro teleport dash
+        
+        // Calcular nueva posición
+        const dashDistance = direction === 'left' ? -this.teleportDashDistance : this.teleportDashDistance;
+        const newX = this.x + dashDistance;
+        
+        // Mantener dentro de los límites
+        this.x = Math.max(0, Math.min(800 - this.width, newX));
+        
+        // Crear efecto de humo en posición inicial y final
+        const oldX = this.x - dashDistance;
+        for (let i = 0; i < 12; i++) {
+            // Humo en posición inicial
+            this.smokeParticles.push({
+                x: oldX + this.width / 2 + (Math.random() - 0.5) * this.width,
+                y: this.y + this.height / 2 + (Math.random() - 0.5) * this.height,
+                vx: (Math.random() - 0.5) * 6,
+                vy: (Math.random() - 0.5) * 6,
+                life: 40,
+                maxLife: 40,
+                size: Math.random() * 12 + 6,
+                type: 'normal'
+            });
+            
+            // Humo en posición final
+            this.createSmokeParticle();
+        }
+        
+        console.log(`¡Dash direccional ${direction} activado!`);
+    }
+    
+    createSmokeParticle() {
+        this.smokeParticles.push({
+            x: this.x + this.width / 2 + (Math.random() - 0.5) * this.width,
+            y: this.y + this.height / 2 + (Math.random() - 0.5) * this.height,
+            vx: (Math.random() - 0.5) * 4,
+            vy: (Math.random() - 0.5) * 4,
+            life: 30,
+            maxLife: 30,
+            size: Math.random() * 8 + 4,
+            type: 'normal'
+        });
+    }
+    
+    createHeadSmokeParticle() {
+        this.smokeParticles.push({
+            x: this.x + this.width / 2 + (Math.random() - 0.5) * 20,
+            y: this.y - 10 + (Math.random() * 15), // Encima de la cabeza
+            vx: (Math.random() - 0.5) * 2,
+            vy: -Math.random() * 3 - 1, // Hacia arriba como humo
+            life: 60,
+            maxLife: 60,
+            size: Math.random() * 6 + 3,
+            type: 'head'
+        });
+    }
+    
+    updateSmokeParticles() {
+        for (let i = this.smokeParticles.length - 1; i >= 0; i--) {
+            const particle = this.smokeParticles[i];
+            particle.x += particle.vx;
+            particle.y += particle.vy;
+            particle.life--;
+            particle.vx *= 0.98;
+            particle.vy *= 0.98;
+            
+            if (particle.life <= 0) {
+                this.smokeParticles.splice(i, 1);
+            }
+        }
+    }
+    
+    drawSmokeParticles(ctx) {
+        ctx.save();
+        for (const particle of this.smokeParticles) {
+            const alpha = particle.life / particle.maxLife;
+            ctx.globalAlpha = alpha * 0.7;
+            
+            // Diferentes colores según el tipo de humo
+            if (particle.type === 'head') {
+                // Humo de cabeza - más amarillento como fogata
+                const intensity = Math.floor(255 * alpha);
+                ctx.fillStyle = `rgb(${intensity}, ${Math.floor(intensity * 0.8)}, ${Math.floor(intensity * 0.3)})`;
+            } else {
+                // Humo normal - blanco
+                ctx.fillStyle = '#ffffff';
+            }
+            
+            ctx.beginPath();
+            ctx.arc(particle.x, particle.y, particle.size * alpha, 0, Math.PI * 2);
+            ctx.fill();
+        }
+        ctx.restore();
     }
 }

+ 191 - 0
js/touchControls.js

@@ -0,0 +1,191 @@
+class TouchControls {
+    constructor(canvas, inputHandler) {
+        this.canvas = canvas;
+        this.input = inputHandler;
+        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 }
+        };
+        
+        this.lastTapTime = 0;
+        this.doubleTapDelay = 300; // ms
+        this.lastTapSide = null;
+        
+        this.setupTouchZones();
+        this.addTouchListeners();
+    }
+    
+    setupTouchZones() {
+        // Usar dimensiones del canvas directamente
+        const width = this.canvas.width;
+        const height = this.canvas.height;
+        
+        // Zona izquierda (1/3 izquierdo, parte inferior)
+        this.touchZones.left = {
+            x: 0,
+            y: height * 0.4, // Desde 40% hacia abajo
+            width: width * 0.35,
+            height: height * 0.6
+        };
+        
+        // Zona derecha (1/3 derecho, parte inferior)
+        this.touchZones.right = {
+            x: width * 0.65,
+            y: height * 0.4,
+            width: width * 0.35,
+            height: height * 0.6
+        };
+        
+        // Zona de salto (mitad superior)
+        this.touchZones.jump = {
+            x: 0,
+            y: 0,
+            width: width,
+            height: height * 0.5
+        };
+    }
+    
+    addTouchListeners() {
+        // Prevenir comportamiento por defecto del navegador
+        this.canvas.addEventListener('touchstart', (e) => {
+            e.preventDefault();
+            this.handleTouchStart(e);
+        }, { passive: false });
+        
+        this.canvas.addEventListener('touchend', (e) => {
+            e.preventDefault();
+            this.handleTouchEnd(e);
+        }, { passive: false });
+        
+        this.canvas.addEventListener('touchmove', (e) => {
+            e.preventDefault();
+        }, { passive: false });
+        
+        // Redimensionar zonas cuando cambie el tamaño
+        window.addEventListener('resize', () => {
+            this.setupTouchZones();
+        });
+    }
+    
+    getTouchPosition(touch) {
+        const rect = this.canvas.getBoundingClientRect();
+        const scaleX = this.canvas.width / rect.width;
+        const scaleY = this.canvas.height / rect.height;
+        
+        return {
+            x: (touch.clientX - rect.left) * scaleX,
+            y: (touch.clientY - rect.top) * scaleY
+        };
+    }
+    
+    isInZone(pos, zone) {
+        return pos.x >= zone.x && 
+               pos.x <= zone.x + zone.width && 
+               pos.y >= zone.y && 
+               pos.y <= zone.y + zone.height;
+    }
+    
+    handleTouchStart(e) {
+        for (let touch of e.touches) {
+            const pos = this.getTouchPosition(touch);
+            
+            console.log('Touch at:', pos.x, pos.y);
+            console.log('Zones:', this.touchZones);
+            
+            if (this.isInZone(pos, this.touchZones.jump)) {
+                console.log('Jump zone touched');
+                // Simular tecla W presionada
+                this.input.keys.W = true;
+                this.input.keys.w = true;
+            } else if (this.isInZone(pos, this.touchZones.left)) {
+                console.log('Left zone touched');
+                // Simular tecla A presionada
+                this.input.keys.A = true;
+                this.input.keys.a = true;
+                this.checkDoubleTap('left');
+            } else if (this.isInZone(pos, this.touchZones.right)) {
+                console.log('Right zone touched');
+                // Simular tecla D presionada
+                this.input.keys.D = true;
+                this.input.keys.d = true;
+                this.checkDoubleTap('right');
+            }
+        }
+    }
+    
+    handleTouchEnd(e) {
+        // Liberar todas las teclas cuando se levanta el dedo
+        this.input.keys.A = false;
+        this.input.keys.a = false;
+        this.input.keys.D = false;
+        this.input.keys.d = false;
+        this.input.keys.W = false;
+        this.input.keys.w = false;
+        console.log('Touch ended - all keys released');
+    }
+    
+    checkDoubleTap(side) {
+        const now = Date.now();
+        
+        if (this.lastTapSide === side && 
+            now - this.lastTapTime < this.doubleTapDelay) {
+            // Doble tap detectado - activar dash direccional
+            this.input.keys.Shift = true;
+            
+            // Liberar Shift después de un frame
+            setTimeout(() => {
+                this.input.keys.Shift = false;
+            }, 50);
+            
+            this.lastTapTime = 0; // Resetear para evitar múltiples activaciones
+        } else {
+            this.lastTapTime = now;
+            this.lastTapSide = side;
+        }
+    }
+    
+    // Método para dibujar las zonas táctiles (debug)
+    drawTouchZones(ctx) {
+        if (!this.showDebug) return;
+        
+        ctx.save();
+        ctx.globalAlpha = 0.3;
+        
+        // Zona izquierda
+        ctx.fillStyle = '#ff0000';
+        ctx.fillRect(
+            this.touchZones.left.x,
+            this.touchZones.left.y,
+            this.touchZones.left.width,
+            this.touchZones.left.height
+        );
+        
+        // Zona derecha
+        ctx.fillStyle = '#00ff00';
+        ctx.fillRect(
+            this.touchZones.right.x,
+            this.touchZones.right.y,
+            this.touchZones.right.width,
+            this.touchZones.right.height
+        );
+        
+        // Zona de salto
+        ctx.fillStyle = '#0000ff';
+        ctx.fillRect(
+            this.touchZones.jump.x,
+            this.touchZones.jump.y,
+            this.touchZones.jump.width,
+            this.touchZones.jump.height
+        );
+        
+        ctx.restore();
+    }
+    
+    toggleDebug() {
+        this.showDebug = !this.showDebug;
+    }
+}
+
+// Exportar para uso global
+window.TouchControls = TouchControls;