Explorar el Código

skill issue

ye
Matthew Trejo hace 4 meses
padre
commit
e5dff72506
Se han modificado 14 ficheros con 1599 adiciones y 0 borrados
  1. 93 0
      README.md
  2. 52 0
      assets/README.md
  3. BIN
      assets/font.ttf
  4. 106 0
      assets/lyrics.txt
  5. BIN
      assets/music.mp3
  6. BIN
      assets/sprites.png
  7. BIN
      assets/sprites.psd
  8. 67 0
      css/styles.css
  9. 26 0
      index.html
  10. 832 0
      js/game.js
  11. 38 0
      js/input.js
  12. 219 0
      js/lyrics.js
  13. 107 0
      js/player.js
  14. 59 0
      js/spriteProcessor.js

+ 93 - 0
README.md

@@ -0,0 +1,93 @@
+# 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:
+
+```javascript
+this.sprites = {
+    idle: [
+        { x: 0, y: 0, w: 32, h: 32 },
+        // ... más frames
+    ],
+    walk: [
+        { x: 0, y: 32, w: 32, h: 32 },
+        // ... más 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
+
+## 📁 Estructura del proyecto
+
+```
+okayuScroller/
+├── index.html          # Página principal
+├── css/
+│   └── styles.css      # Estilos del juego
+├── js/
+│   ├── game.js         # Lógica principal del juego
+│   ├── player.js       # Clase del jugador
+│   └── input.js        # Manejo de controles
+├── assets/
+│   ├── sprites.png     # Sprite sheet (930x614px)
+│   └── music.mp3       # Música de fondo
+└── README.md           # Este archivo
+```
+
+## 🛠️ Tecnologías usadas
+
+- **HTML5 Canvas** - Renderizado del juego
+- **JavaScript ES6+** - Lógica del juego
+- **CSS3** - Estilos y diseño responsive
+- **Pixel art** - Estilo visual retro
+
+## 🎵 Notas sobre la música
+
+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).
+
+## 📱 Compatibilidad
+
+- ✅ Chrome, Firefox, Safari, Edge
+- ✅ Móviles (responsive)
+- ✅ Requiere JavaScript habilitado
+
+---
+
+¡Disfruta jugando con Okayu! 🐾

+ 52 - 0
assets/README.md

@@ -0,0 +1,52 @@
+# Nekomata Okayu Scroller - Assets Guide
+
+## Required Assets
+
+### 1. Sprite Sheet (`sprites.png`)
+- **File**: `assets/sprites.png`
+- **Dimensions**: 930x614 pixels
+- **Format**: PNG with transparency
+
+### 2. Background Music (`music.mp3`)
+- **File**: `assets/music.mp3`
+- **Format**: MP3, OGG, or WAV
+- **Loop**: Should loop seamlessly
+
+## Sprite Sheet Layout Guide
+
+The game expects the following sprite positions (adjust as needed):
+
+### Player Sprites (32x32 pixels each)
+- **Idle Animation**: Row 0, frames 0-3
+  - Frame 0: (0, 0, 32, 32)
+  - Frame 1: (32, 0, 32, 32)
+  - Frame 2: (64, 0, 32, 32)
+  - Frame 3: (96, 0, 32, 32)
+
+- **Walk Animation**: Row 1, frames 0-5
+  - Frame 0: (0, 32, 32, 32)
+  - Frame 1: (32, 32, 32, 32)
+  - Frame 2: (64, 32, 32, 32)
+  - Frame 3: (96, 32, 32, 32)
+  - Frame 4: (128, 32, 32, 32)
+  - Frame 5: (160, 32, 32, 32)
+
+### Environment Sprites
+- **Floor Tiles**: (0, 64, 32, 32)
+- **Decorations**: (64, 0, 64, 64)
+
+## Setup Instructions
+
+1. Place your sprite sheet file in `assets/sprites.png`
+2. Place your background music in `assets/music.mp3`
+3. Open `index.html` in your browser
+4. Click on the canvas to start the music
+5. Use WASD or arrow keys to move Okayu around
+
+## Customization
+
+To adjust sprite positions, edit the coordinates in:
+- `js/player.js` (player sprites)
+- `js/game.js` (background elements)
+
+The game uses pixel-perfect rendering to maintain the pixel art aesthetic.

BIN
assets/font.ttf


+ 106 - 0
assets/lyrics.txt

@@ -0,0 +1,106 @@
+mogumogu
+
+shinra banshou no akusenkutou mo mayoneizu kaketara daitai oishiku naru?
+kimi ga naite mo onaka wa suku yo shouka dekinakatta "gomen gomen"
+sui mo amai mo katte ni tabete gomeiwaku okakeshite imasu
+pakuchii na okite hen na aji no juusu kokoro wo mu ni shite nomikomimasu
+
+aa konton jouhou kata no resutoran de usoppachi no menyuu ni ocha koboshite
+kimi to mogumogu (mogumogu) mogumogu (mogumogu)
+juugeki-sen no mannaka de mogumogu (ugya~)
+
+chimi mouryou harapeko no mure manpuku ni naru mirai wo negatteiru yo
+mogumogu (mogumogu) mogumogu (mogumogu) sekai ga owaru mae ni
+mogumogu yamii maji kami yamuyamu ari no manma
+mogumogu yamii Magic Coming konoyo wo ajiwaun da umauma
+
+riron busou no mirufiiyu sando (sando)
+tokumei kibou no ourora sousu (sousu)
+oozappa na ajitsuke oome ni mite yo oishiku nattara “ok ok.”
+ii mon warui mon ippai tabete yogoreta kuchimoto nuguimasu
+onigiri piza keiki gouka na furu kousu wana wo utagai ki wo tsukemasu
+
+aa mousou ryuugen higo no baikingu de dokuiri to zeppin ryouri yoriwakete
+kimi to mogumogu (mogumogu) mogumogu (mogumogu)
+desugeimu no kyoushitsu de mogumogu (ugya~)
+
+chimi mouryou harapeko no mure naka no ii kimi to ikinokoretara ii na
+mogumogu (mogumogu) mogumogu (mogumogu) suu nen-go mo kono basho de
+mogumogu yamii maji kami kon’ya no bangohan wa
+mogumogu yamii Magic Coming tsugi wa omae no ban da
+
+mogumogu yamii maji kami yamuyamu ari no manma
+mogumogu yamii Magic Coming konoyo wo ajiwaun da umauma
+
+mogumogu mogumogu mogumogu mogumogu
+mogumogu mogumogu mogumogu yamii
+
+mogumogu mogumogu mogumogu mogumogu
+mogumogu mogumogu mogumogu yamii
+
+mogumogu mogumogu mogumogu mogumogu
+mogumogu mogumogu mogumogu yamii
+
+mogumogu mogumogu mogumogu mogumogu
+mogumogu mogumogu mogumogu yamii yamii 
+
+timings:
+
+[00:00:00.000 --> 00:00:02.000]  (もう食うもう食う)
+[00:00:03.000 --> 00:00:05.000]  (もう食うもう食う)
+[00:00:05.000 --> 00:00:10.300]  死んだ��上の白���苦糖も
+[00:00:10.300 --> 00:00:13.900]  マ����ーズかけたらだいたい美味しくなる
+[00:00:13.900 --> 00:00:17.500]  君が��いてもお��すくよ
+[00:00:17.500 --> 00:00:20.900]  消化できなかったごめんごめん
+[00:00:20.900 --> 00:00:24.200]  ��いも��いも勝手に食べて
+[00:00:24.200 --> 00:00:27.600]  ごめんはご加��しています
+[00:00:27.600 --> 00:00:31.000]  ��ァクチー��おきて変な味のジュース
+[00:00:31.000 --> 00:00:34.200]  心を無にして��み��みます
+[00:00:34.200 --> 00:00:37.400]  ああ 簡��情報買ったのレストランで
+[00:00:37.400 --> 00:00:40.800]  ウソパチのメニューにお茶こ��して
+[00:00:40.800 --> 00:00:43.200]  君と(もう食うもう食う)
+[00:00:43.200 --> 00:00:44.900]  (もう食うもう食う)
+[00:00:44.900 --> 00:00:48.300]  ������の真ん中で(もう食うもう食う)
+[00:00:48.300 --> 00:00:51.100]  死にもりを��ってこの��れ
+[00:00:51.100 --> 00:00:55.200]  ��足になる未来を願っているよ
+[00:00:55.200 --> 00:00:58.600]  (もう食うもう食う)
+[00:00:58.600 --> 00:01:01.200]  世界が終わる前に
+[00:01:01.200 --> 00:01:05.500]  もぐもぐやみまじかみ
+[00:01:05.500 --> 00:01:08.100]  やむやむありのまんま
+[00:01:08.100 --> 00:01:12.400]  もぐもぐやみまじかみ
+[00:01:12.400 --> 00:01:14.700]  この世を味わうんだ
+[00:01:14.700 --> 00:01:16.200]  うまうま
+[00:01:16.200 --> 00:01:25.900]  色分��のミルフィー��サンド
+[00:01:25.900 --> 00:01:29.300]  ��名希望のオー��ラソース
+[00:01:29.300 --> 00:01:32.900]  大��把な味付け多めに見てよ
+[00:01:32.900 --> 00:01:36.300]  美味しくなったらOK OK
+[00:01:36.300 --> 00:01:39.500]  良いも��いもいっぱい食べて
+[00:01:39.500 --> 00:01:43.000]  ��れた口元��います
+[00:01:43.000 --> 00:01:46.600]  おにぎりピ��ケーキ高��なフルコース
+[00:01:46.600 --> 00:01:49.600]  ��を��い気をつけます
+[00:01:49.600 --> 00:01:52.800]  ああ ��想流����語のバイキングで
+[00:01:52.800 --> 00:01:56.200]  毒入りと��品料理より分けて
+[00:01:56.200 --> 00:01:58.600]  君ともぐもぐ
+[00:01:58.600 --> 00:02:00.300]  もぐもぐ
+[00:02:00.300 --> 00:02:03.700]  レスゲームの教��でもぐもぐ
+[00:02:03.700 --> 00:02:06.500]  君も����でこの��れ
+[00:02:06.500 --> 00:02:09.900]  ��のいい君と生き��れたら
+[00:02:09.900 --> 00:02:12.300]  いいなもぐもぐ
+[00:02:12.300 --> 00:02:14.000]  もぐもぐ
+[00:02:14.000 --> 00:02:16.600]  数年後もこの場所で
+[00:02:16.600 --> 00:02:20.800]  もぐもぐやみまじかみ
+[00:02:20.800 --> 00:02:23.300]  今夜の����飯は
+[00:02:23.300 --> 00:02:27.600]  もぐもぐやみまじかみ
+[00:02:27.600 --> 00:02:30.200]  次はお前の番だ
+[00:02:30.200 --> 00:02:34.500]  もぐもぐやみまじかみ
+[00:02:34.500 --> 00:02:37.100]  やむやむありのまんま
+[00:02:37.100 --> 00:02:41.300]  もぐもぐやみまじかみ
+[00:02:41.300 --> 00:02:43.700]  この世を味わうんだ
+[00:02:43.700 --> 00:02:45.300]  もぐもぐ もぐもぐ
+[00:02:46.600 --> 00:02:52.300]  もぐもぐ もぐもぐ
+[00:02:52.300 --> 00:02:53.600]  もぐもぐやみ
+[00:02:53.600 --> 00:02:59.200]  もぐもぐ
+[00:02:59.200 --> 00:03:00.400]  もぐもぐやみ
+[00:03:00.400 --> 00:03:06.000]  もぐもぐ
+[00:03:06.000 --> 00:03:07.300]  もぐもぐやみ
+[00:03:07.300 --> 00:03:09.000]  もぐもぐやみまじ

BIN
assets/music.mp3


BIN
assets/sprites.png


BIN
assets/sprites.psd


+ 67 - 0
css/styles.css

@@ -0,0 +1,67 @@
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+body {
+    background-color: #1a1a1a;
+    font-family: 'Courier New', monospace;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    min-height: 100vh;
+    color: white;
+}
+
+.game-container {
+    text-align: center;
+    background-color: #2a2a2a;
+    padding: 20px;
+    border-radius: 10px;
+    box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
+}
+
+#gameCanvas {
+    border: 2px solid #444;
+    image-rendering: pixelated;
+    image-rendering: -moz-crisp-edges;
+    image-rendering: crisp-edges;
+    cursor: pointer;
+}
+
+.controls {
+    margin-top: 15px;
+    font-size: 12px;
+    color: #ccc;
+}
+
+.controls p {
+    margin: 0;
+}
+
+/* Estilos para las letras */
+.lyrics-container {
+    position: absolute;
+    bottom: 10px;
+    left: 50%;
+    transform: translateX(-50%);
+    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;
+}
+
+/* Responsive design */
+@media (max-width: 400px) {
+    .game-container {
+        padding: 10px;
+    }
+    
+    #gameCanvas {
+        width: 100%;
+        max-width: 320px;
+        height: auto;
+    }
+}

+ 26 - 0
index.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="es">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Nekomata Okayu Scroller</title>
+    <link rel="stylesheet" href="css/styles.css">
+</head>
+<body>
+    <div class="game-container">
+        <canvas id="gameCanvas" width="320" height="240"></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> -->
+        </div>
+    </div>
+    
+    <script src="js/input.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>

+ 832 - 0
js/game.js

@@ -0,0 +1,832 @@
+class Game {
+    constructor() {
+        this.canvas = document.getElementById('gameCanvas');
+        this.ctx = this.canvas.getContext('2d');
+        this.ctx.imageSmoothingEnabled = false;
+        
+        this.spriteSheet = new Image();
+        this.backgroundMusic = new Audio();
+        
+        this.player = null;
+        this.backgroundElements = [];
+        this.backgroundLayers = {
+            far: [],      // Elementos muy lejanos (nubes, montañas)
+            mid: [],      // Elementos medios (árboles, edificios)
+            near: []      // Elementos cercanos (decoraciones, elementos interactivos)
+        };
+        
+        // Sistema de generación de nubes mejorado
+        this.cloudSystem = new CloudSystem(this.canvas);
+        
+        // Sistema de objetos de fondo
+        this.backgroundObjectSystem = new BackgroundObjectSystem(this.canvas, this);
+        
+        // Sistema de letras sincronizadas
+        this.lyricsSystem = new LyricsSystem();
+        
+        // Estado del juego
+        this.gameState = 'playing'; // 'playing', 'gameOver', 'paused'
+        
+        // Definir tipos de elementos con sus configuraciones
+        this.elementTypes = {
+            floor: {
+                renderMethod: 'sprite',
+                defaultSpriteCoords: { x: 2, y: 430, w: 32, h: 32 },
+                layer: 'near'
+            },
+            decoration: {
+                renderMethod: 'sprite',
+                defaultSpriteCoords: { x: 137, y: 104, w: 35, h: 26 },
+                layer: 'near'
+            },
+            background: {
+                renderMethod: 'color',
+                color: '#4A90E2',
+                layer: 'far'
+            },
+            text: {
+                renderMethod: 'text',
+                font: '12px Arial',
+                color: 'white',
+                align: 'center',
+                layer: 'near'
+            },
+            sprite: {
+                renderMethod: 'sprite',
+                layer: 'near'
+            },
+            cloud: {
+                renderMethod: 'sprite',
+                layer: 'far',
+                defaultSpriteCoords: { x: 231, y: 112, w: 287, h: 148 },
+                autoMove: true,
+                moveSpeed: 0.5
+            },
+            backgroundObject: {
+                renderMethod: 'sprite',
+                layer: 'mid',
+                autoMove: true,
+                moveSpeed: 0.1,
+                // Definir las variantes de sprites disponibles
+                spriteVariants: [
+                    { x: 546, y: 264, w: 30, h: 21 },
+                    { x: 546, y: 286, w: 30, h: 21 },
+                    { x: 546, y: 308, w: 30, h: 25 },
+                    { x: 546, y: 334, w: 30, h: 24 },
+                    { x: 546, y: 359, w: 30, h: 21 },
+                    { x: 545, y: 382, w: 31, h: 29 },
+                    { x: 516, y: 264, w: 29, h: 17 },
+                    { x: 516, y: 282, w: 29, h: 21 },
+                    { x: 516, y: 304, w: 29, h: 18 },
+                    { x: 516, y: 323, w: 29, h: 18 },
+                    { x: 516, y: 342, w: 29, h: 19 },
+                    { x: 516, y: 362, w: 29, h: 19 },
+                    { x: 516, y: 382, w: 28, h: 29 },
+                    { x: 491, y: 270, w: 24, h: 26 },
+                    { x: 491, y: 297, w: 24, h: 26 },
+                    { x: 491, y: 324, w: 24, h: 26 },
+                    { x: 491, y: 353, w: 24, h: 26 },
+                    { x: 489, y: 382, w: 26, h: 29 },
+                    { x: 465, y: 310, w: 25, h: 23 },
+                    { x: 465, y: 334, w: 25, h: 23 },
+                    { x: 465, y: 358, w: 25, h: 23 },
+                    { x: 465, y: 382, w: 11, h: 29 },
+                    { x: 477, y: 382, w: 11, h: 29 }
+                ]
+            }
+        };
+        
+        this.init();
+    }
+    
+    init() {
+        // Load assets
+        this.spriteSheet.src = 'assets/sprites.png';
+        this.backgroundMusic.src = 'assets/music.mp3';
+        
+        // Configure audio
+        this.backgroundMusic.loop = true;
+        this.backgroundMusic.volume = 0.05;
+        
+        // Create sprite processor for color key-out
+        this.spriteProcessor = new SpriteProcessor();
+        
+        // Wait for assets to load
+        Promise.all([
+            new Promise(resolve => this.spriteSheet.onload = resolve),
+            new Promise(resolve => this.backgroundMusic.oncanplaythrough = resolve)
+        ]).then(() => {
+            // Process sprite sheet to remove #00ffff background
+            this.processedSpriteSheet = this.spriteProcessor.processSpriteSheet(this.spriteSheet, '#00ffff');
+            this.processedSpriteSheet.onload = () => {
+                this.startGame();
+            };
+        });
+        
+        // Handle audio context (for browsers that require user interaction)
+        this.canvas.addEventListener('click', () => {
+            if (this.backgroundMusic.paused && this.gameState === 'playing') {
+                this.backgroundMusic.play().catch(e => console.log('Audio play failed:', e));
+            }
+        });
+        
+        // Agregar controles de teclado
+        this.setupKeyboardControls();
+    }
+    
+    // Método para agregar elementos de forma dinámica
+    addElement(type, x, y, w, h, options = {}) {
+        const elementConfig = this.elementTypes[type];
+        const layer = options.layer || elementConfig?.layer || 'near';
+        
+        const element = {
+            type,
+            x,
+            y,
+            w,
+            h,
+            ...options
+        };
+        
+        // Agregar a la capa correspondiente
+        if (this.backgroundLayers[layer]) {
+            this.backgroundLayers[layer].push(element);
+        } else {
+            this.backgroundElements.push(element);
+        }
+        
+        return element;
+    }
+    
+    // Método para agregar elementos de piso
+    addFloorTile(x, y) {
+        return this.addElement('floor', x, y, 32, 32);
+    }
+    
+    // Método para agregar decoraciones
+    addDecoration(x, y, w = 35, h = 26) {
+        return this.addElement('decoration', x, y, w, h);
+    }
+    
+    // Método para agregar elementos de color sólido
+    addColorElement(x, y, w, h, color, layer = 'far') {
+        return this.addElement('background', x, y, w, h, { color, layer });
+    }
+    
+    // Método para agregar texto
+    addText(x, y, text, options = {}) {
+        return this.addElement('text', x, y, 0, 0, { text, ...options });
+    }
+    
+    // Método para agregar sprites personalizados
+    addSprite(x, y, w, h, spriteCoords, options = {}) {
+        return this.addElement('sprite', x, y, w, h, { 
+            spriteCoords,
+            ...options 
+        });
+    }
+    
+    // Método para agregar cualquier sprite con coordenadas específicas
+    addCustomSprite(type, x, y, w, h, spriteCoords, options = {}) {
+        return this.addElement(type, x, y, w, h, { 
+            spriteCoords,
+            ...options 
+        });
+    }
+    
+    startGame() {
+        // Create player
+        this.player = new Player(144, 176, this.processedSpriteSheet);
+        
+        // Crear elementos de fondo usando los nuevos métodos
+        // Piso
+        for (let i = 0; i < 10; i++) {
+            this.addFloorTile(i * 32, 208);
+        }
+        
+        // Inicializar sistema de nubes
+        this.cloudSystem.init(this.processedSpriteSheet, this.backgroundLayers.far);
+        
+        // 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 });
+
+        // Candybar
+        this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 });
+
+        // Cake
+        this.addSprite(260, 134, 100, 75, { 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 });
+        
+        this.gameLoop();
+    }
+    
+    update() {
+        if (this.gameState !== 'playing') return;
+        
+        if (this.player) {
+            this.player.update();
+        }
+        
+        // Actualizar sistema de nubes
+        this.cloudSystem.update();
+        
+        // Actualizar sistema de objetos de fondo
+        this.backgroundObjectSystem.update();
+        
+        // Actualizar sistema de letras
+        if (!this.backgroundMusic.paused) {
+            this.lyricsSystem.update(this.backgroundMusic.currentTime);
+        }
+    }
+    
+    // Método para renderizar un elemento individual
+    renderElement(element) {
+        const elementConfig = this.elementTypes[element.type];
+        if (!elementConfig) {
+            console.warn(`Tipo de elemento desconocido: ${element.type}`);
+            return;
+        }
+        
+        switch (elementConfig.renderMethod) {
+            case 'sprite':
+                this.renderSpriteElement(element, elementConfig);
+                break;
+            case 'color':
+                this.renderColorElement(element, elementConfig);
+                break;
+            case 'text':
+                this.renderTextElement(element, elementConfig);
+                break;
+            default:
+                console.warn(`Método de renderizado desconocido: ${elementConfig.renderMethod}`);
+        }
+    }
+    
+    renderSpriteElement(element, config) {
+        // Usar coordenadas personalizadas si están especificadas, sino usar las por defecto
+        const spriteCoords = element.spriteCoords || config.defaultSpriteCoords;
+        
+        if (!spriteCoords) {
+            console.warn(`No se especificaron coordenadas de sprite para elemento tipo: ${element.type}`);
+            return;
+        }
+        
+        // Si el elemento tiene rotación, aplicar transformaciones
+        if (element.rotation) {
+            this.ctx.save();
+            
+            // Calcular el centro del sprite para la rotación
+            const centerX = element.x + element.w / 2;
+            const centerY = element.y + element.h / 2;
+            
+            // Aplicar transformaciones
+            this.ctx.translate(centerX, centerY);
+            this.ctx.rotate(element.rotation);
+            this.ctx.translate(-centerX, -centerY);
+        }
+        
+        this.ctx.drawImage(
+            this.processedSpriteSheet,
+            spriteCoords.x, spriteCoords.y, spriteCoords.w, spriteCoords.h,
+            element.x, element.y, element.w, element.h
+        );
+        
+        // Restaurar el contexto si se aplicó rotación
+        if (element.rotation) {
+            this.ctx.restore();
+        }
+    }
+    
+    renderColorElement(element, config) {
+        const color = element.color || config.color;
+        this.ctx.fillStyle = color;
+        this.ctx.fillRect(element.x, element.y, element.w, element.h);
+    }
+    
+    renderTextElement(element, config) {
+        this.ctx.fillStyle = element.color || config.color;
+        this.ctx.font = element.font || config.font;
+        this.ctx.textAlign = element.align || config.align;
+        this.ctx.fillText(element.text, element.x, element.y);
+    }
+    
+    draw() {
+        // Clear canvas con degradado
+        const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
+        gradient.addColorStop(0, '#d9e6ff');    // Color superior
+        gradient.addColorStop(1, '#eee5ff');    // Color inferior
+        
+        this.ctx.fillStyle = gradient;
+        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+        
+        // Renderizar elementos por capas (de lejos a cerca)
+        // Capa lejana (nubes, montañas)
+        this.backgroundLayers.far.forEach(element => {
+            this.renderElement(element);
+        });
+        
+        // Capa media (árboles, edificios)
+        this.backgroundLayers.mid.forEach(element => {
+            this.renderElement(element);
+        });
+        
+        // Capa cercana (decoraciones, elementos interactivos)
+        this.backgroundLayers.near.forEach(element => {
+            this.renderElement(element);
+        });
+        
+        // Elementos legacy (para compatibilidad)
+        this.backgroundElements.forEach(element => {
+            this.renderElement(element);
+        });
+        
+        // Draw player
+        if (this.player) {
+            this.player.draw(this.ctx);
+        }
+        
+        // Dibujar letras si están activadas
+        this.lyricsSystem.draw(this.ctx, this.canvas);
+        
+        // Draw instructions if music hasn't started
+        if (this.backgroundMusic.paused && this.gameState === 'playing') {
+            this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
+            this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+            
+            this.ctx.fillStyle = 'white';
+            this.ctx.font = '8px 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('Ctrl+G - Simular fin de canción', 160, 160);
+        }
+        
+        // Dibujar pantalla de fin de juego
+        if (this.lyricsSystem.gameOverScreen) {
+            this.drawGameOverScreen();
+        }
+    }
+    
+    gameLoop() {
+        this.update();
+        this.draw();
+        requestAnimationFrame(() => this.gameLoop());
+    }
+    
+    setupKeyboardControls() {
+        document.addEventListener('keydown', (e) => {
+            switch(e.key.toLowerCase()) {
+                case 'l':
+                    this.lyricsSystem.toggleLyrics();
+                    break;
+                case 'r':
+                    this.restartGame();
+                    break;
+                case 'g':
+                    // Shortcut para desarrollo: simular fin de canción
+                    if (e.ctrlKey) {
+                        this.lyricsSystem.simulateGameOver();
+                    }
+                    break;
+            }
+        });
+    }
+    
+    drawGameOverScreen() {
+        // Fondo negro translúcido
+        this.ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
+        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+        
+        // Texto de fin de juego
+        this.ctx.fillStyle = 'white';
+        this.ctx.font = '16px Courier New';
+        this.ctx.textAlign = 'center';
+        
+        this.ctx.fillText('¡Canción terminada!', 160, 100);
+        this.ctx.fillText('¿Quieres jugar otra vez?', 160, 120);
+        
+        // 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);
+    }
+    
+    restartGame() {
+        // Detener música
+        this.backgroundMusic.pause();
+        this.backgroundMusic.currentTime = 0;
+        
+        // Reiniciar sistemas
+        this.lyricsSystem.reset();
+        
+        // Reiniciar estado del juego
+        this.gameState = 'playing';
+        
+        // Limpiar elementos de fondo
+        this.backgroundLayers.far = [];
+        this.backgroundLayers.mid = [];
+        this.backgroundLayers.near = [];
+        this.backgroundElements = [];
+        
+        // Reiniciar sistemas
+        this.cloudSystem.init(this.processedSpriteSheet, this.backgroundLayers.far);
+        this.backgroundObjectSystem.init(this.processedSpriteSheet, this.backgroundLayers.mid);
+        
+        // Reiniciar jugador
+        this.player = new Player(144, 176, this.processedSpriteSheet);
+        
+        // Recrear elementos de fondo
+        this.recreateBackgroundElements();
+    }
+    
+    recreateBackgroundElements() {
+        // Piso
+        for (let i = 0; i < 10; i++) {
+            this.addFloorTile(i * 32, 208);
+        }
+        
+        // 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
+        this.addSprite(200, 130, 48, 79, { x: 179, y: 438, w: 48, h: 79 });
+
+        // Cake
+        this.addSprite(260, 134, 100, 75, { 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 });
+    }
+}
+
+// Clase para manejar el sistema de nubes de forma modular
+class CloudSystem {
+    constructor(canvas) {
+        this.canvas = canvas;
+        this.spriteSheet = null;
+        this.cloudLayer = null;
+        this.frameCount = 0;
+        
+        // Configuración simplificada
+        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
+            spriteCoords: { x: 231, y: 112, w: 287, h: 148 },
+            size: { w: 287, h: 148 },
+            minSpacing: 100,            // Espaciado mínimo entre nubes (reducido)
+            minTimeBetweenSpawns: 60    // Frames mínimos entre apariciones (reducido)
+        };
+        
+        this.lastSpawnTime = 0;
+    }
+    
+    init(spriteSheet, cloudLayer) {
+        this.spriteSheet = spriteSheet;
+        this.cloudLayer = cloudLayer;
+        
+        // Generar nubes iniciales para que el fondo no se vea vacío
+        this.spawnInitialClouds();
+    }
+    
+    spawnInitialClouds() {
+        // Generar 2-3 nubes iniciales en posiciones aleatorias
+        const initialCount = Math.floor(Math.random() * 2) + 2;
+        
+        for (let i = 0; i < initialCount; i++) {
+            const x = Math.random() * this.canvas.width;
+            const y = this.getRandomY();
+            const speed = this.getRandomSpeed();
+            
+            this.createCloud(x, y, speed);
+        }
+    }
+    
+    update() {
+        this.frameCount++;
+        
+        // Limpiar nubes que salieron de la pantalla
+        this.cleanupOffscreenClouds();
+        
+        // Intentar generar nueva nube
+        if (this.shouldSpawnCloud()) {
+            this.spawnCloud();
+        }
+        
+        // Mover nubes existentes
+        this.moveClouds();
+    }
+    
+    shouldSpawnCloud() {
+        // Verificar tiempo mínimo entre apariciones
+        if (this.frameCount - this.lastSpawnTime < this.config.minTimeBetweenSpawns) {
+            return false;
+        }
+        
+        // Verificar número máximo de nubes
+        const cloudCount = this.cloudLayer.filter(e => e.type === 'cloud').length;
+        if (cloudCount >= this.config.maxClouds) {
+            return false;
+        }
+        
+        // Verificar probabilidad de spawn
+        if (Math.random() > this.config.spawnRate) {
+            return false;
+        }
+        
+        // Verificar espaciado mínimo
+        const hasSpacing = this.hasEnoughSpacing();
+        
+        // Debug: mostrar información cada 60 frames (1 segundo)
+        if (this.frameCount % 60 === 0) {
+            console.log(`Clouds: ${cloudCount}/${this.config.maxClouds}, Spacing: ${hasSpacing}, Time: ${this.frameCount - this.lastSpawnTime}`);
+        }
+        
+        return hasSpacing;
+    }
+    
+    hasEnoughSpacing() {
+        const existingClouds = this.cloudLayer.filter(e => e.type === 'cloud');
+        
+        // Si no hay nubes, siempre se puede generar
+        if (existingClouds.length === 0) {
+            return true;
+        }
+        
+        // Verificar que no haya nubes muy cerca del borde derecho
+        for (const cloud of existingClouds) {
+            const distanceFromRight = this.canvas.width - cloud.x;
+            if (distanceFromRight < this.config.minSpacing) {
+                return false;
+            }
+        }
+        
+        return true;
+    }
+    
+    spawnCloud() {
+        const y = this.getRandomY();
+        const speed = this.getRandomSpeed();
+        
+        // Generar desde la derecha de la pantalla
+        const x = this.canvas.width;
+        
+        this.createCloud(x, y, speed);
+        this.lastSpawnTime = this.frameCount;
+        
+        console.log(`Nueva nube generada en (${x}, ${y}) con velocidad ${speed}`);
+    }
+    
+    createCloud(x, y, speed) {
+        const cloud = {
+            type: 'cloud',
+            x: x,
+            y: y,
+            w: this.config.size.w,
+            h: this.config.size.h,
+            spriteCoords: this.config.spriteCoords,
+            moveSpeed: speed,
+            layer: 'far'
+        };
+        
+        this.cloudLayer.push(cloud);
+    }
+    
+    moveClouds() {
+        this.cloudLayer.forEach(cloud => {
+            if (cloud.type === 'cloud' && cloud.moveSpeed) {
+                cloud.x -= cloud.moveSpeed;
+            }
+        });
+    }
+    
+    cleanupOffscreenClouds() {
+        for (let i = this.cloudLayer.length - 1; i >= 0; i--) {
+            const element = this.cloudLayer[i];
+            if (element.type === 'cloud' && element.x + element.w < 0) {
+                this.cloudLayer.splice(i, 1);
+            }
+        }
+    }
+    
+    getRandomY() {
+        return Math.random() * 
+            (this.config.yRange.max - this.config.yRange.min) + 
+            this.config.yRange.min;
+    }
+    
+    getRandomSpeed() {
+        return Math.random() * 
+            (this.config.speedRange.max - this.config.speedRange.min) + 
+            this.config.speedRange.min;
+    }
+}
+
+// Clase para manejar el sistema de objetos de fondo
+class BackgroundObjectSystem {
+    constructor(canvas, game) {
+        this.canvas = canvas;
+        this.game = game;
+        this.spriteSheet = null;
+        this.objectLayer = null;
+        this.frameCount = 0;
+        
+        // Configuración para objetos de fondo
+        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)
+        };
+        
+        this.lastSpawnTime = 0;
+    }
+    
+    init(spriteSheet, objectLayer) {
+        this.spriteSheet = spriteSheet;
+        this.objectLayer = objectLayer;
+        
+        // Generar objetos iniciales
+        this.spawnInitialObjects();
+    }
+    
+    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);
+        }
+    }
+    
+    update() {
+        this.frameCount++;
+        
+        // Limpiar objetos que salieron de la pantalla
+        this.cleanupOffscreenObjects();
+        
+        // Intentar generar nuevo grupo
+        if (this.shouldSpawnObjectGroup()) {
+            this.spawnObjectGroup();
+        }
+        
+        // Mover objetos existentes
+        this.moveObjects();
+    }
+    
+    shouldSpawnObjectGroup() {
+        // Verificar tiempo mínimo entre apariciones
+        if (this.frameCount - this.lastSpawnTime < this.config.minTimeBetweenSpawns) {
+            return false;
+        }
+        
+        // Verificar número máximo de objetos
+        const objectCount = this.objectLayer.filter(e => e.type === 'backgroundObject').length;
+        if (objectCount >= this.config.maxObjects) {
+            return false;
+        }
+        
+        // Verificar probabilidad de spawn
+        if (Math.random() > this.config.spawnRate) {
+            return false;
+        }
+        
+        // Verificar espaciado mínimo
+        const hasSpacing = this.hasEnoughSpacing();
+        
+        return hasSpacing;
+    }
+    
+    hasEnoughSpacing() {
+        const existingObjects = this.objectLayer.filter(e => e.type === 'backgroundObject');
+        
+        // Si no hay objetos, siempre se puede generar
+        if (existingObjects.length === 0) {
+            return true;
+        }
+        
+        // Verificar que no haya objetos muy cerca del borde derecho
+        for (const obj of existingObjects) {
+            const distanceFromRight = this.canvas.width - obj.x;
+            if (distanceFromRight < this.config.minSpacing) {
+                return false;
+            }
+        }
+        
+        return true;
+    }
+    
+    spawnObjectGroup() {
+        const y = this.getRandomY();
+        const speed = this.getRandomSpeed();
+        
+        // Generar desde la derecha de la pantalla
+        const x = this.canvas.width;
+        
+        this.createObjectGroup(x, y, speed);
+        this.lastSpawnTime = this.frameCount;
+        
+        console.log(`Nuevo grupo de objetos generado en (${x}, ${y}) con velocidad ${speed}`);
+    }
+    
+    createObjectGroup(x, y, speed) {
+        const groupSize = Math.floor(Math.random() * 
+            (this.config.groupSize.max - this.config.groupSize.min + 1)) + 
+            this.config.groupSize.min;
+        
+        // Obtener las variantes de sprites desde elementTypes
+        const spriteVariants = this.game.elementTypes.backgroundObject.spriteVariants;
+        
+        let currentX = x;
+        
+        for (let i = 0; i < groupSize; i++) {
+            // Seleccionar sprite aleatorio
+            const spriteVariant = spriteVariants[Math.floor(Math.random() * spriteVariants.length)];
+            
+            // Aplicar rotación aleatoria
+            const rotation = (Math.random() - 0.5) * 0.8; // ±0.4 radianes
+            
+            // Aplicar dispersión vertical aleatoria
+            const yOffset = (Math.random() - 0.5) * 40; // ±20 píxeles
+            
+            const object = {
+                type: 'backgroundObject',
+                x: currentX,
+                y: y + yOffset,
+                w: spriteVariant.w,
+                h: spriteVariant.h,
+                spriteCoords: spriteVariant,
+                moveSpeed: speed,
+                rotation: rotation,
+                layer: 'mid'
+            };
+            
+            this.objectLayer.push(object);
+            
+            // 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 + spacing;
+        }
+    }
+    
+    moveObjects() {
+        this.objectLayer.forEach(obj => {
+            if (obj.type === 'backgroundObject' && obj.moveSpeed) {
+                obj.x -= obj.moveSpeed;
+            }
+        });
+    }
+    
+    cleanupOffscreenObjects() {
+        for (let i = this.objectLayer.length - 1; i >= 0; i--) {
+            const element = this.objectLayer[i];
+            if (element.type === 'backgroundObject' && element.x + element.w < 0) {
+                this.objectLayer.splice(i, 1);
+            }
+        }
+    }
+    
+    getRandomY() {
+        return Math.random() * 
+            (this.config.yRange.max - this.config.yRange.min) + 
+            this.config.yRange.min;
+    }
+    
+    getRandomSpeed() {
+        return Math.random() * 
+            (this.config.speedRange.max - this.config.speedRange.min) + 
+            this.config.speedRange.min;
+    }
+}
+
+// Initialize game when DOM is loaded
+window.addEventListener('DOMContentLoaded', () => {
+    new Game();
+});

+ 38 - 0
js/input.js

@@ -0,0 +1,38 @@
+class InputHandler {
+    constructor() {
+        this.keys = {
+            a: false,
+            d: false,
+            ArrowLeft: false,
+            ArrowRight: false
+        };
+        
+        this.init();
+    }
+    
+    init() {
+        window.addEventListener('keydown', (e) => {
+            if (this.keys.hasOwnProperty(e.key)) {
+                e.preventDefault();
+                this.keys[e.key] = true;
+            }
+        });
+        
+        window.addEventListener('keyup', (e) => {
+            if (this.keys.hasOwnProperty(e.key)) {
+                e.preventDefault();
+                this.keys[e.key] = false;
+            }
+        });
+    }
+    
+    isMovingLeft() {
+        return this.keys.a || this.keys.ArrowLeft;
+    }
+    
+    isMovingRight() {
+        return this.keys.d || this.keys.ArrowRight;
+    }
+}
+
+const input = new InputHandler();

+ 219 - 0
js/lyrics.js

@@ -0,0 +1,219 @@
+class LyricsSystem {
+    constructor() {
+        this.lyrics = [];
+        this.currentLyricIndex = -1;
+        this.showLyrics = true; // Mostrar letras por defecto
+        this.font = null;
+        this.songDuration = 189; // 3:09 en segundos
+        this.isGameOver = false;
+        this.gameOverScreen = false;
+        this.fadeOpacity = 1.0;
+        this.fadeDirection = 1; // 1 para aparecer, -1 para desaparecer
+        
+        // Offset de tiempo para ajustar sincronización (en segundos)
+        // Valor positivo = letras aparecen ANTES
+        // 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
+        
+        // Cargar la fuente personalizada
+        this.loadCustomFont();
+        
+        // Parsear las letras desde el archivo
+        this.parseLyrics();
+    }
+    
+    loadCustomFont() {
+        this.font = new FontFace('CustomFont', 'url(assets/font.ttf)');
+        this.font.load().then(() => {
+            document.fonts.add(this.font);
+        }).catch(err => {
+            console.warn('No se pudo cargar la fuente personalizada:', err);
+        });
+    }
+    
+    parseLyrics() {
+        // 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: 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" },
+            { time: 33.3, text: "aa konton jouhou kata no resutoran de usoppachi no menyuu ni ocha koboshite" },
+            { time: 39.8, text: "kimi to mogumogu (mogumogu) mogumogu (mogumogu)" },
+            { time: 43, text: "juugeki-sen no mannaka de mogumogu (ugya~)" },
+            { time: 46, text: "chimi mouryou harapeko no mure manpuku ni naru mirai wo negatteiru yo" },
+            { 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: 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" },
+            { time: 181, text: "mogumogu mogumogu mogumogu mogumogu"},
+            { time: 187, text: "yamii yamii" }
+        ];
+        
+        this.lyrics = lyricsData;
+    }
+    
+    update(currentTime) {
+        // Aplicar offset de tiempo
+        const adjustedTime = currentTime + this.timeOffset;
+        
+        // Verificar si la canción ha terminado
+        if (adjustedTime >= this.songDuration && !this.isGameOver) {
+            this.isGameOver = true;
+            this.gameOverScreen = true;
+            return;
+        }
+        
+        // Actualizar letra actual basada en el tiempo ajustado
+        this.updateCurrentLyric(adjustedTime);
+    }
+    
+    updateCurrentLyric(currentTime) {
+        let newIndex = -1;
+        
+        for (let i = 0; i < this.lyrics.length; i++) {
+            if (currentTime >= this.lyrics[i].time) {
+                newIndex = i;
+            } else {
+                break;
+            }
+        }
+        
+        if (newIndex !== this.currentLyricIndex) {
+            this.currentLyricIndex = newIndex;
+        }
+    }
+    
+    draw(ctx, canvas) {
+        if (!this.showLyrics && this.fadeOpacity <= 0) return;
+        
+        // Actualizar opacidad para transición suave
+        if (this.showLyrics && this.fadeOpacity < 1.0) {
+            this.fadeOpacity += 0.05;
+        } else if (!this.showLyrics && this.fadeOpacity > 0) {
+            this.fadeOpacity -= 0.05;
+        }
+        
+        // Dibujar letra actual
+        if (this.currentLyricIndex >= 0 && this.currentLyricIndex < this.lyrics.length) {
+            const currentLyric = this.lyrics[this.currentLyricIndex];
+            
+            // Configurar fuente
+            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
+            const totalHeight = lines.length * this.lineHeight;
+            const startY = canvas.height - 180 - (totalHeight - this.lineHeight);
+            
+            // Agregar sombra
+            ctx.shadowColor = `rgba(0, 0, 0, ${this.fadeOpacity * 0.8})`;
+            ctx.shadowBlur = 4;
+            ctx.shadowOffsetX = 2;
+            ctx.shadowOffsetY = 2;
+            
+            // Dibujar cada línea
+            lines.forEach((line, index) => {
+                const x = canvas.width / 2;
+                const y = startY + (index * this.lineHeight);
+                
+                ctx.strokeText(line, x, y);
+                ctx.fillText(line, x, y);
+            });
+            
+            // Resetear sombra
+            ctx.shadowColor = 'transparent';
+            ctx.shadowBlur = 0;
+            ctx.shadowOffsetX = 0;
+            ctx.shadowOffsetY = 0;
+        }
+        
+
+    }
+    
+    toggleLyrics() {
+        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(' ');
+        const lines = [];
+        let currentLine = words[0];
+        
+        for (let i = 1; i < words.length; i++) {
+            const word = words[i];
+            const width = ctx.measureText(currentLine + ' ' + word).width;
+            
+            if (width < maxWidth) {
+                currentLine += ' ' + word;
+            } else {
+                lines.push(currentLine);
+                currentLine = word;
+            }
+        }
+        
+        lines.push(currentLine);
+        return lines;
+    }
+    
+    // Método para desarrollo: simular fin de canción
+    simulateGameOver() {
+        this.isGameOver = true;
+        this.gameOverScreen = true;
+    }
+    
+    // Método para reiniciar el juego
+    reset() {
+        this.currentLyricIndex = -1;
+        this.isGameOver = false;
+        this.gameOverScreen = false;
+        this.showLyrics = true;
+        this.fadeOpacity = 1.0;
+        this.timeOffset = 0; // Resetear offset
+    }
+} 

+ 107 - 0
js/player.js

@@ -0,0 +1,107 @@
+class Player {
+    constructor(x, y, spriteSheet) {
+        this.x = x;
+        this.y = y;
+        this.width = 24;
+        this.height = 34;
+        this.speed = 0.25;
+        this.direction = 'right';
+        
+        // Animation
+        this.frameIndex = 0;
+        this.frameTimer = 0;
+        this.idleFrameInterval = 60; // Más lento para idle
+        this.walkFrameInterval = 15;  // Más rápido para caminar
+        this.isMoving = false;
+        
+        // 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 },
+                { x: 341, y: 15, w: 24, h: 34 },
+                { x: 404, y: 15, w: 24, h: 34 },
+                { x: 341, y: 15, w: 24, h: 34 }
+            ],
+            walk: [
+                { x: 2, y: 16, w: 24, h: 34 },
+                { x: 28, y: 16, w: 24, h: 34 },
+                { x: 54, y: 16, w: 24, h: 34 },
+                { x: 80, y: 16, w: 24, h: 34 },
+                { x: 106, y: 16, w: 24, h: 34 },
+                { x: 132, y: 16, w: 24, h: 34 }
+            ]
+        };
+        
+        this.currentAnimation = 'idle';
+        this.spriteSheet = spriteSheet;
+    }
+    
+    update() {
+        this.isMoving = false;
+        
+        // Horizontal movement
+        if (input.isMovingLeft()) {
+            this.x -= this.speed;
+            this.direction = 'left';
+            this.isMoving = true;
+        }
+        if (input.isMovingRight()) {
+            this.x += this.speed;
+            this.direction = 'right';
+            this.isMoving = true;
+        }
+        
+        // Keep player within horizontal bounds
+        this.x = Math.max(0, Math.min(320 - this.width, this.x));
+        
+        // Update animation
+        this.updateAnimation();
+    }
+    
+    updateAnimation() {
+        const newAnimation = this.isMoving ? 'walk' : 'idle';
+        
+        if (this.currentAnimation !== newAnimation) {
+            this.currentAnimation = newAnimation;
+            this.frameIndex = 0;
+            this.frameTimer = 0;
+        }
+        
+        this.frameTimer++;
+        const frames = this.sprites[this.currentAnimation];
+        
+        // Usar diferentes velocidades según la animación
+        const frameInterval = this.currentAnimation === 'walk' ? this.walkFrameInterval : this.idleFrameInterval;
+        
+        if (this.frameTimer >= frameInterval) {
+            this.frameTimer = 0;
+            this.frameIndex = (this.frameIndex + 1) % frames.length;
+        }
+    }
+    
+    draw(ctx) {
+        const frame = this.sprites[this.currentAnimation][this.frameIndex];
+        
+        ctx.save();
+        
+        if (this.direction === 'left') {
+            ctx.scale(-1, 1);
+            ctx.drawImage(
+                this.spriteSheet,
+                frame.x, frame.y, frame.w, frame.h,
+                -this.x - this.width, this.y,
+                this.width, this.height
+            );
+        } else {
+            ctx.drawImage(
+                this.spriteSheet,
+                frame.x, frame.y, frame.w, frame.h,
+                this.x, this.y,
+                this.width, this.height
+            );
+        }
+        
+        ctx.restore();
+    }
+}

+ 59 - 0
js/spriteProcessor.js

@@ -0,0 +1,59 @@
+class SpriteProcessor {
+    constructor() {
+        this.processedSprites = new Map();
+    }
+    
+    processSpriteSheet(originalImage, keyColor = '#00ffff') {
+        const canvas = document.createElement('canvas');
+        const ctx = canvas.getContext('2d');
+        
+        canvas.width = originalImage.width;
+        canvas.height = originalImage.height;
+        
+        // Draw original image
+        ctx.drawImage(originalImage, 0, 0);
+        
+        // Get image data
+        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+        const data = imageData.data;
+        
+        // Convert key colors to RGB
+        const keyR = parseInt(keyColor.slice(1, 3), 16);
+        const keyG = parseInt(keyColor.slice(3, 5), 16);
+        const keyB = parseInt(keyColor.slice(5, 7), 16);
+        
+        // Convert spritesheet background color to RGB
+        const bgColor = '#a0a7ca';
+        const bgR = parseInt(bgColor.slice(1, 3), 16);
+        const bgG = parseInt(bgColor.slice(3, 5), 16);
+        const bgB = parseInt(bgColor.slice(5, 7), 16);
+        
+        // Make key colors transparent
+        for (let i = 0; i < data.length; i += 4) {
+            const r = data[i];
+            const g = data[i + 1];
+            const b = data[i + 2];
+            
+            // Check if pixel matches key color (with small tolerance)
+            if (Math.abs(r - keyR) < 10 && Math.abs(g - keyG) < 10 && Math.abs(b - keyB) < 10) {
+                data[i + 3] = 0; // Set alpha to 0 (transparent)
+            }
+            // Check if pixel matches spritesheet background color (with small tolerance)
+            else if (Math.abs(r - bgR) < 10 && Math.abs(g - bgG) < 10 && Math.abs(b - bgB) < 10) {
+                data[i + 3] = 0; // Set alpha to 0 (transparent)
+            }
+        }
+        
+        // Put processed image back
+        ctx.putImageData(imageData, 0, 0);
+        
+        // Create new image with transparency
+        const processedImage = new Image();
+        processedImage.src = canvas.toDataURL('image/png');
+        
+        return processedImage;
+    }
+}
+
+// Export for use in game
+window.SpriteProcessor = SpriteProcessor;