Spaces:
Running
Running
Crea un videojuego de aventura estilo "Zelda" en una sola página web (HTML + CSS + JavaScript) con las siguientes especificaciones: 1) Modo de juego: - Local, **2 jugadores** simultáneos en **pantalla dividida** (split-screen). Cada jugador controla a su personaje y mueve su propia cámara en el mismo mapa grande. - La pantalla debe dividirse **verticalmente** (jugador 1 a la izquierda, jugador 2 a la derecha). Si el tamaño del viewport es estrecho, usa división horizontal como respaldo. 2) Mecánica & diseño: - Vista superior (top-down) con movimiento 8 direcciones (arriba/abajo/izq/der y diagonales). - Mapa tile-based (mosaicos), tamaño razonable (por ejemplo 60x40 tiles) con zonas: bosque, pueblo, río, mazmorra pequeña. - Incluye exploración libre, objetos recogibles (llaves, pociones), puertas que requieren llaves, y al menos **una mazmorra** con 1 jefe. - Puzles simples (palancas, bloques que empujar) y enemigos básicos con IA sencilla (patrullan y persiguen si ven al jugador). - Inventario por jugador (3 ranuras visibles) y barra de vida por jugador. - Sistema sencillo de combate cuerpo a cuerpo: ataque con espada (ataque en frente), tiempo de invulnerabilidad corto al recibir daño. 3) Controles: - Jugador 1: WASD para moverse, F para atacar/usar, G para interactuar. - Jugador 2: Flechas para moverse, Numpad 0 (o tecla L) para atacar/usar, Numpad 1 (o tecla K) para interactuar. - Soporte para gamepad si es posible (pero no obligatorio). 4) Cámara & pantalla partida: - Cada mitad de pantalla muestra la cámara centrada en su jugador; las cámaras se mueven independientemente (no forzar unión). - HUD separado para cada jugador (vida, pociones, llaves) en su lado de la pantalla. - Minimapa pequeño compartido en la parte superior central (muestra la posición aproximada de ambos jugadores y la mazmorra si está explorada). 5) Estética y assets: - Estilo pixel art 16×16 o 32×32, paleta simple tipo 8-12 colores. Usa sprites simples generados por la IA o patrones CSS/Canvas si no hay assets externos. - Sonidos simples (pasos, golpe, recolectar objeto, abrir puerta, efecto de daño) generados por la web o sonidos cortos en base64. 6) Entregables: - Generar **un solo archivo HTML** funcional o un paquete descargable con HTML/CSS/JS y carpeta `assets/`. Debe ser fácil de ejecutar localmente (doble clic en el HTML). - Incluir comentarios en el código explicando las partes principales (mapa, cámara, colisiones, IA, controles). - Proveer en el archivo un pequeño README visible como comentario con instrucciones de control y cómo cambiar tamaño de mapa o añadir enemigos. 7) Rendimiento y fallback: - Optimizar para navegador moderno; si el navegador no soporta WebAudio o gamepad, debe seguir funcionando sin esas características. - Si no hay tiempo para generación de todo el mapa, crear una versión jugable limitada (un mapa pequeño con una mazmorra) claramente comentada en el código. Entrega: juego jugable en local con pantalla dividida, dos personajes, exploración, combate básico, mazmorra y jefe. - Initial Deployment
4160c18 verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Pixel Quest - Split-Screen Adventure</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| /* Custom CSS for game elements */ | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| background-color: #222; | |
| font-family: 'Courier New', monospace; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: row; | |
| } | |
| @media (max-width: 768px) { | |
| #game-container { | |
| flex-direction: column; | |
| } | |
| } | |
| .player-view { | |
| position: relative; | |
| flex: 1; | |
| overflow: hidden; | |
| border: 2px solid #444; | |
| } | |
| .game-canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| image-rendering: pixelated; | |
| } | |
| .hud { | |
| position: absolute; | |
| padding: 10px; | |
| color: white; | |
| text-shadow: 2px 2px 0 #000; | |
| font-size: 16px; | |
| z-index: 100; | |
| } | |
| .health-bar { | |
| height: 10px; | |
| background-color: #ff0000; | |
| border: 2px solid #000; | |
| margin-top: 5px; | |
| } | |
| .health-fill { | |
| height: 100%; | |
| background-color: #00ff00; | |
| width: 100%; | |
| transition: width 0.3s; | |
| } | |
| .inventory { | |
| display: flex; | |
| gap: 5px; | |
| margin-top: 10px; | |
| } | |
| .inventory-slot { | |
| width: 32px; | |
| height: 32px; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| border: 2px solid #555; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| #minimap { | |
| position: absolute; | |
| top: 10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 200px; | |
| height: 150px; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| border: 2px solid #555; | |
| z-index: 200; | |
| image-rendering: pixelated; | |
| } | |
| .game-object { | |
| position: absolute; | |
| image-rendering: pixelated; | |
| } | |
| .damage-effect { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(255, 0, 0, 0.3); | |
| pointer-events: none; | |
| opacity: 0; | |
| z-index: 90; | |
| } | |
| .title-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| z-index: 1000; | |
| } | |
| .title-screen h1 { | |
| font-size: 48px; | |
| margin-bottom: 30px; | |
| text-shadow: 4px 4px 0 #8b4513; | |
| color: #ffcc00; | |
| } | |
| .start-button { | |
| padding: 15px 30px; | |
| background-color: #8b4513; | |
| color: white; | |
| border: none; | |
| font-size: 24px; | |
| cursor: pointer; | |
| border-radius: 5px; | |
| transition: background-color 0.3s; | |
| } | |
| .start-button:hover { | |
| background-color: #a0522d; | |
| } | |
| .controls { | |
| margin-top: 30px; | |
| text-align: center; | |
| } | |
| .controls h2 { | |
| color: #ffcc00; | |
| margin-bottom: 10px; | |
| } | |
| .control-group { | |
| display: inline-block; | |
| margin: 0 20px; | |
| text-align: left; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <!-- Player 1 View --> | |
| <div class="player-view" id="player1-view"> | |
| <canvas class="game-canvas" id="player1-canvas"></canvas> | |
| <div class="hud" id="player1-hud"> | |
| <div>Player 1</div> | |
| <div class="health-bar"> | |
| <div class="health-fill" id="player1-health"></div> | |
| </div> | |
| <div class="inventory" id="player1-inventory"> | |
| <div class="inventory-slot"></div> | |
| <div class="inventory-slot"></div> | |
| <div class="inventory-slot"></div> | |
| </div> | |
| </div> | |
| <div class="damage-effect" id="player1-damage"></div> | |
| </div> | |
| <!-- Player 2 View --> | |
| <div class="player-view" id="player2-view"> | |
| <canvas class="game-canvas" id="player2-canvas"></canvas> | |
| <div class="hud" id="player2-hud"> | |
| <div>Player 2</div> | |
| <div class="health-bar"> | |
| <div class="health-fill" id="player2-health"></div> | |
| </div> | |
| <div class="inventory" id="player2-inventory"> | |
| <div class="inventory-slot"></div> | |
| <div class="inventory-slot"></div> | |
| <div class="inventory-slot"></div> | |
| </div> | |
| </div> | |
| <div class="damage-effect" id="player2-damage"></div> | |
| </div> | |
| <!-- Minimap --> | |
| <canvas id="minimap"></canvas> | |
| <!-- Title Screen --> | |
| <div class="title-screen" id="title-screen"> | |
| <h1>Pixel Quest</h1> | |
| <button class="start-button" id="start-button">Start Adventure</button> | |
| <div class="controls"> | |
| <h2>Controls</h2> | |
| <div class="control-group"> | |
| <h3>Player 1</h3> | |
| <p>WASD: Move</p> | |
| <p>F: Attack/Use</p> | |
| <p>G: Interact</p> | |
| </div> | |
| <div class="control-group"> | |
| <h3>Player 2</h3> | |
| <p>Arrow Keys: Move</p> | |
| <p>L: Attack/Use</p> | |
| <p>K: Interact</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /* | |
| * PIXEL QUEST - Split-Screen Adventure Game | |
| * | |
| * Features: | |
| * - Split-screen multiplayer (vertical/horizontal based on screen size) | |
| * - Top-down 8-directional movement | |
| * - Tile-based map with different zones | |
| * - Collectible items (keys, potions) | |
| * - Locked doors requiring keys | |
| * - Dungeon with a boss | |
| * - Simple puzzles (levers, pushable blocks) | |
| * - Enemy AI (patrol and chase) | |
| * - Health system and inventory | |
| * - Melee combat with invulnerability frames | |
| * - Minimap showing player positions | |
| */ | |
| // Game Constants | |
| const TILE_SIZE = 32; | |
| const MAP_WIDTH = 60; | |
| const MAP_HEIGHT = 40; | |
| const PLAYER_SPEED = 3; | |
| const ATTACK_RANGE = 20; | |
| const ATTACK_DURATION = 300; // ms | |
| const INVULNERABILITY_DURATION = 1000; // ms | |
| const CAMERA_SMOOTHING = 0.1; | |
| // Game State | |
| let gameState = { | |
| players: [], | |
| enemies: [], | |
| items: [], | |
| doors: [], | |
| levers: [], | |
| blocks: [], | |
| boss: null, | |
| gameTime: 0, | |
| gameStarted: false | |
| }; | |
| // Tile Types | |
| const TILE_TYPES = { | |
| GRASS: 0, | |
| WATER: 1, | |
| SAND: 2, | |
| STONE: 3, | |
| WOOD: 4, | |
| BRICK: 5, | |
| LAVA: 6 | |
| }; | |
| // Game Objects | |
| class GameObject { | |
| constructor(x, y, width, height) { | |
| this.x = x; | |
| this.y = y; | |
| this.width = width; | |
| this.height = height; | |
| this.vx = 0; | |
| this.vy = 0; | |
| } | |
| get centerX() { | |
| return this.x + this.width / 2; | |
| } | |
| get centerY() { | |
| return this.y + this.height / 2; | |
| } | |
| collidesWith(other) { | |
| return this.x < other.x + other.width && | |
| this.x + this.width > other.x && | |
| this.y < other.y + other.height && | |
| this.y + this.height > other.y; | |
| } | |
| distanceTo(other) { | |
| const dx = this.centerX - other.centerX; | |
| const dy = this.centerY - other.centerY; | |
| return Math.sqrt(dx * dx + dy * dy); | |
| } | |
| update(deltaTime) { | |
| this.x += this.vx * deltaTime; | |
| this.y += this.vy * deltaTime; | |
| } | |
| } | |
| class Player extends GameObject { | |
| constructor(x, y, playerId) { | |
| super(x, y, 24, 24); | |
| this.playerId = playerId; | |
| this.maxHealth = 100; | |
| this.health = this.maxHealth; | |
| this.inventory = []; | |
| this.facing = { x: 0, y: 1 }; // Facing down by default | |
| this.isAttacking = false; | |
| this.attackCooldown = 0; | |
| this.isInvulnerable = false; | |
| this.invulnerabilityTimer = 0; | |
| this.keys = 0; | |
| this.color = playerId === 1 ? '#3498db' : '#e74c3c'; | |
| } | |
| update(deltaTime) { | |
| super.update(deltaTime); | |
| // Update attack cooldown | |
| if (this.attackCooldown > 0) { | |
| this.attackCooldown -= deltaTime; | |
| if (this.attackCooldown <= 0) { | |
| this.isAttacking = false; | |
| } | |
| } | |
| // Update invulnerability | |
| if (this.isInvulnerable) { | |
| this.invulnerabilityTimer -= deltaTime; | |
| if (this.invulnerabilityTimer <= 0) { | |
| this.isInvulnerable = false; | |
| } | |
| } | |
| // Apply friction | |
| this.vx *= 0.9; | |
| this.vy *= 0.9; | |
| // Update facing direction if moving | |
| if (Math.abs(this.vx) > 0.1 || Math.abs(this.vy) > 0.1) { | |
| this.facing = { x: Math.sign(this.vx), y: Math.sign(this.vy) }; | |
| } | |
| // Keep player within bounds | |
| this.x = Math.max(0, Math.min(MAP_WIDTH * TILE_SIZE - this.width, this.x)); | |
| this.y = Math.max(0, Math.min(MAP_HEIGHT * TILE_SIZE - this.height, this.y)); | |
| } | |
| attack() { | |
| if (!this.isAttacking && this.attackCooldown <= 0) { | |
| this.isAttacking = true; | |
| this.attackCooldown = ATTACK_DURATION; | |
| return true; | |
| } | |
| return false; | |
| } | |
| takeDamage(amount) { | |
| if (!this.isInvulnerable) { | |
| this.health = Math.max(0, this.health - amount); | |
| this.isInvulnerable = true; | |
| this.invulnerabilityTimer = INVULNERABILITY_DURATION; | |
| // Show damage effect | |
| const damageEffect = document.getElementById(`player${this.playerId}-damage`); | |
| damageEffect.style.opacity = 1; | |
| setTimeout(() => { | |
| damageEffect.style.opacity = 0; | |
| }, 200); | |
| return true; | |
| } | |
| return false; | |
| } | |
| useItem(index) { | |
| if (index >= 0 && index < this.inventory.length) { | |
| const item = this.inventory[index]; | |
| if (item.type === 'potion') { | |
| this.health = Math.min(this.maxHealth, this.health + 30); | |
| this.inventory.splice(index, 1); | |
| playSound('potion'); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| addToInventory(item) { | |
| if (this.inventory.length < 3) { | |
| this.inventory.push(item); | |
| return true; | |
| } | |
| return false; | |
| } | |
| draw(ctx, cameraX, cameraY) { | |
| const screenX = this.x - cameraX; | |
| const screenY = this.y - cameraY; | |
| // Draw player body | |
| ctx.fillStyle = this.color; | |
| ctx.fillRect(screenX, screenY, this.width, this.height); | |
| // Draw facing indicator (head) | |
| ctx.fillStyle = '#fff'; | |
| const headX = screenX + this.width / 2 + this.facing.x * 5; | |
| const headY = screenY + this.height / 2 + this.facing.y * 5; | |
| ctx.beginPath(); | |
| ctx.arc(headX, headY, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw attack if attacking | |
| if (this.isAttacking) { | |
| ctx.fillStyle = 'rgba(255, 255, 0, 0.5)'; | |
| const attackX = screenX + this.width / 2 + this.facing.x * ATTACK_RANGE; | |
| const attackY = screenY + this.height / 2 + this.facing.y * ATTACK_RANGE; | |
| ctx.beginPath(); | |
| ctx.arc(attackX, attackY, 15, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| } | |
| class Enemy extends GameObject { | |
| constructor(x, y, type) { | |
| super(x, y, 24, 24); | |
| this.type = type; | |
| this.health = type === 'boss' ? 150 : 50; | |
| this.speed = type === 'boss' ? 1.5 : 1; | |
| this.damage = type === 'boss' ? 20 : 10; | |
| this.detectionRange = type === 'boss' ? 300 : 150; | |
| this.attackCooldown = 0; | |
| this.patrolPoints = []; | |
| this.currentPatrolIndex = 0; | |
| this.color = type === 'boss' ? '#8b0000' : '#9b59b6'; | |
| } | |
| update(deltaTime, players) { | |
| super.update(deltaTime); | |
| // Update attack cooldown | |
| if (this.attackCooldown > 0) { | |
| this.attackCooldown -= deltaTime; | |
| } | |
| // Find closest player | |
| let closestPlayer = null; | |
| let minDistance = Infinity; | |
| for (const player of players) { | |
| const distance = this.distanceTo(player); | |
| if (distance < minDistance) { | |
| minDistance = distance; | |
| closestPlayer = player; | |
| } | |
| } | |
| // Behavior based on distance to player | |
| if (closestPlayer && minDistance < this.detectionRange) { | |
| // Chase player | |
| const dx = closestPlayer.centerX - this.centerX; | |
| const dy = closestPlayer.centerY - this.centerY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance > 0) { | |
| this.vx = (dx / distance) * this.speed; | |
| this.vy = (dy / distance) * this.speed; | |
| } | |
| // Attack if close enough | |
| if (minDistance < ATTACK_RANGE && this.attackCooldown <= 0) { | |
| closestPlayer.takeDamage(this.damage); | |
| this.attackCooldown = 1000; // 1 second cooldown | |
| } | |
| } else { | |
| // Patrol behavior | |
| if (this.patrolPoints.length > 0) { | |
| const target = this.patrolPoints[this.currentPatrolIndex]; | |
| const dx = target.x - this.centerX; | |
| const dy = target.y - this.centerY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance > 10) { | |
| this.vx = (dx / distance) * this.speed * 0.5; | |
| this.vy = (dy / distance) * this.speed * 0.5; | |
| } else { | |
| this.currentPatrolIndex = (this.currentPatrolIndex + 1) % this.patrolPoints.length; | |
| } | |
| } else { | |
| // Random wandering if no patrol points | |
| if (Math.random() < 0.02) { | |
| this.vx = (Math.random() - 0.5) * this.speed * 0.5; | |
| this.vy = (Math.random() - 0.5) * this.speed * 0.5; | |
| } | |
| } | |
| } | |
| // Apply friction | |
| this.vx *= 0.95; | |
| this.vy *= 0.95; | |
| } | |
| takeDamage(amount) { | |
| this.health = Math.max(0, this.health - amount); | |
| return this.health <= 0; | |
| } | |
| draw(ctx, cameraX, cameraY) { | |
| const screenX = this.x - cameraX; | |
| const screenY = this.y - cameraY; | |
| // Draw enemy body | |
| ctx.fillStyle = this.color; | |
| ctx.fillRect(screenX, screenY, this.width, this.height); | |
| // Draw health bar for bosses | |
| if (this.type === 'boss') { | |
| const healthWidth = 30; | |
| const healthHeight = 5; | |
| const healthX = screenX + (this.width - healthWidth) / 2; | |
| const healthY = screenY - 10; | |
| ctx.fillStyle = '#ff0000'; | |
| ctx.fillRect(healthX, healthY, healthWidth, healthHeight); | |
| ctx.fillStyle = '#00ff00'; | |
| ctx.fillRect(healthX, healthY, healthWidth * (this.health / 150), healthHeight); | |
| } | |
| } | |
| } | |
| class Item extends GameObject { | |
| constructor(x, y, type) { | |
| super(x, y, 16, 16); | |
| this.type = type; | |
| this.color = this.getItemColor(); | |
| } | |
| getItemColor() { | |
| switch (this.type) { | |
| case 'key': return '#ffcc00'; | |
| case 'potion': return '#ff0000'; | |
| case 'sword': return '#999999'; | |
| default: return '#ffffff'; | |
| } | |
| } | |
| draw(ctx, cameraX, cameraY) { | |
| const screenX = this.x - cameraX; | |
| const screenY = this.y - cameraY; | |
| ctx.fillStyle = this.color; | |
| if (this.type === 'key') { | |
| // Draw key shape | |
| ctx.fillRect(screenX + 5, screenY + 3, 10, 2); // Key handle | |
| ctx.fillRect(screenX + 10, screenY, 2, 10); // Key shaft | |
| ctx.fillRect(screenX + 12, screenY + 2, 3, 2); // Teeth | |
| ctx.fillRect(screenX + 12, screenY + 6, 3, 2); // Teeth | |
| } else if (this.type === 'potion') { | |
| // Draw potion shape | |
| ctx.beginPath(); | |
| ctx.moveTo(screenX + 5, screenY + 12); | |
| ctx.lineTo(screenX + 5, screenY + 5); | |
| ctx.lineTo(screenX + 11, screenY + 5); | |
| ctx.lineTo(screenX + 11, screenY + 12); | |
| ctx.lineTo(screenX + 8, screenY + 15); | |
| ctx.lineTo(screenX + 5, screenY + 12); | |
| ctx.fill(); | |
| // Draw potion liquid | |
| ctx.fillStyle = '#ff6666'; | |
| ctx.beginPath(); | |
| ctx.moveTo(screenX + 6, screenY + 11); | |
| ctx.lineTo(screenX + 6, screenY + 7); | |
| ctx.lineTo(screenX + 10, screenY + 7); | |
| ctx.lineTo(screenX + 10, screenY + 11); | |
| ctx.lineTo(screenX + 8, screenY + 13); | |
| ctx.lineTo(screenX + 6, screenY + 11); | |
| ctx.fill(); | |
| } else if (this.type === 'sword') { | |
| // Draw sword shape | |
| ctx.fillRect(screenX + 7, screenY, 2, 12); // Blade | |
| ctx.fillRect(screenX + 3, screenY + 10, 10, 2); // Crossguard | |
| ctx.fillRect(screenX + 6, screenY + 12, 4, 4); // Hilt | |
| } | |
| } | |
| } | |
| class Door extends GameObject { | |
| constructor(x, y, width, height, locked, keyId) { | |
| super(x, y, width, height); | |
| this.locked = locked; | |
| this.keyId = keyId; | |
| this.isOpen = false; | |
| } | |
| unlock(player) { | |
| if (this.locked) { | |
| // Check if player has the key | |
| for (let i = 0; i < player.inventory.length; i++) { | |
| const item = player.inventory[i]; | |
| if (item.type === 'key' && item.keyId === this.keyId) { | |
| player.inventory.splice(i, 1); | |
| this.locked = false; | |
| playSound('door'); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| return true; | |
| } | |
| draw(ctx, cameraX, cameraY) { | |
| const screenX = this.x - cameraX; | |
| const screenY = this.y - cameraY; | |
| if (this.isOpen) { | |
| ctx.fillStyle = 'rgba(139, 69, 19, 0.5)'; | |
| } else if (this.locked) { | |
| ctx.fillStyle = '#8b4513'; | |
| } else { | |
| ctx.fillStyle = '#a0522d'; | |
| } | |
| ctx.fillRect(screenX, screenY, this.width, this.height); | |
| // Draw door details | |
| if (!this.isOpen) { | |
| ctx.fillStyle = '#8b0000'; | |
| ctx.beginPath(); | |
| ctx.arc(screenX + this.width - 5, screenY + this.height / 2, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| if (this.locked) { | |
| // Draw lock icon | |
| ctx.fillStyle = '#ffcc00'; | |
| ctx.fillRect(screenX + this.width - 10, screenY + this.height / 2 - 5, 5, 8); | |
| ctx.fillRect(screenX + this.width - 12, screenY + this.height / 2 + 3, 9, 2); | |
| } | |
| } | |
| } | |
| } | |
| class Lever extends GameObject { | |
| constructor(x, y, target) { | |
| super(x, y, 16, 16); | |
| this.target = target; | |
| this.isActivated = false; | |
| } | |
| activate() { | |
| this.isActivated = !this.isActivated; | |
| playSound('lever'); | |
| if (this.target) { | |
| if (Array.isArray(this.target)) { | |
| this.target.forEach(t => t.trigger()); | |
| } else { | |
| this.target.trigger(); | |
| } | |
| } | |
| return true; | |
| } | |
| draw(ctx, cameraX, cameraY) { | |
| const screenX = this.x - cameraX; | |
| const screenY = this.y - cameraY; | |
| // Draw lever base | |
| ctx.fillStyle = '#999999'; | |
| ctx.fillRect(screenX + 6, screenY + 4, 4, 8); | |
| // Draw lever handle | |
| ctx.fillStyle = '#666666'; | |
| if (this.isActivated) { | |
| ctx.fillRect(screenX + 4, screenY + 8, 8, 2); | |
| } else { | |
| ctx.fillRect(screenX + 8, screenY + 2, 2, 8); | |
| } | |
| } | |
| } | |
| class PushableBlock extends GameObject { | |
| constructor(x, y) { | |
| super(x, y, TILE_SIZE, TILE_SIZE); | |
| this.isBeingPushed = false; | |
| this.pushDirection = { x: 0, y: 0 }; | |
| } | |
| push(directionX, directionY) { | |
| this.vx = directionX * 0.5; | |
| this.vy = directionY * 0.5; | |
| this.isBeingPushed = true; | |
| this.pushDirection = { x: directionX, y: directionY }; | |
| return true; | |
| } | |
| update(deltaTime) { | |
| super.update(deltaTime); | |
| // Stop pushing if no velocity | |
| if (Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1) { | |
| this.isBeingPushed = false; | |
| } | |
| // Snap to grid when not being pushed | |
| if (!this.isBeingPushed) { | |
| const targetX = Math.round(this.x / TILE_SIZE) * TILE_SIZE; | |
| const targetY = Math.round(this.y / TILE_SIZE) * TILE_SIZE; | |
| if (Math.abs(this.x - targetX) > 0.1 || Math.abs(this.y - targetY) > 0.1) { | |
| this.x += (targetX - this.x) * 0.2; | |
| this.y += (targetY - this.y) * 0.2; | |
| } else { | |
| this.x = targetX; | |
| this.y = targetY; | |
| this.vx = 0; | |
| this.vy = 0; | |
| } | |
| } | |
| } | |
| draw(ctx, cameraX, cameraY) { | |
| const screenX = this.x - cameraX; | |
| const screenY = this.y - cameraY; | |
| // Draw block | |
| ctx.fillStyle = '#8b4513'; | |
| ctx.fillRect(screenX, screenY, this.width, this.height); | |
| // Draw wood grain | |
| ctx.fillStyle = '#a0522d'; | |
| for (let i = 0; i < 3; i++) { | |
| ctx.fillRect(screenX + 2, screenY + 6 + i * 6, this.width - 4, 2); | |
| } | |
| } | |
| } | |
| // Game Map | |
| class GameMap { | |
| constructor(width, height) { | |
| this.width = width; | |
| this.height = height; | |
| this.tiles = []; | |
| this.initMap(); | |
| } | |
| initMap() { | |
| // Create empty map | |
| for (let y = 0; y < this.height; y++) { | |
| this.tiles[y] = []; | |
| for (let x = 0; x < this.width; x++) { | |
| // Default to grass | |
| this.tiles[y][x] = TILE_TYPES.GRASS; | |
| } | |
| } | |
| // Create a river | |
| for (let x = 15; x < 45; x++) { | |
| for (let y = 15; y < 25; y++) { | |
| this.tiles[y][x] = TILE_TYPES.WATER; | |
| } | |
| } | |
| // Create a village | |
| for (let x = 5; x < 15; x++) { | |
| for (let y = 5; y < 15; y++) { | |
| this.tiles[y][x] = TILE_TYPES.WOOD; | |
| } | |
| } | |
| // Create a forest | |
| for (let x = 40; x < 55; x++) { | |
| for (let y = 5; y < 15; y++) { | |
| if (Math.random() > 0.3) { | |
| this.tiles[y][x] = TILE_TYPES.GRASS; | |
| } else { | |
| this.tiles[y][x] = TILE_TYPES.SAND; | |
| } | |
| } | |
| } | |
| // Create a dungeon | |
| for (let x = 25; x < 35; x++) { | |
| for (let y = 30; y < 38; y++) { | |
| this.tiles[y][x] = TILE_TYPES.STONE; | |
| } | |
| } | |
| // Dungeon entrance | |
| this.tiles[29][30] = TILE_TYPES.GRASS; | |
| this.tiles[29][31] = TILE_TYPES.GRASS; | |
| // Boss room | |
| for (let x = 28; x < 32; x++) { | |
| for (let y = 35; y < 38; y++) { | |
| this.tiles[y][x] = TILE_TYPES.BRICK; | |
| } | |
| } | |
| // Lava around boss | |
| this.tiles[34][30] = TILE_TYPES.LAVA; | |
| this.tiles[34][31] = TILE_TYPES.LAVA; | |
| this.tiles[34][32] = TILE_TYPES.LAVA; | |
| this.tiles[34][33] = TILE_TYPES.LAVA; | |
| } | |
| getTileColor(tileType) { | |
| switch (tileType) { | |
| case TILE_TYPES.GRASS: return '#2ecc71'; | |
| case TILE_TYPES.WATER: return '#3498db'; | |
| case TILE_TYPES.SAND: return '#f1c40f'; | |
| case TILE_TYPES.STONE: return '#95a5a6'; | |
| case TILE_TYPES.WOOD: return '#8b4513'; | |
| case TILE_TYPES.BRICK: return '#c0392b'; | |
| case TILE_TYPES.LAVA: return '#e74c3c'; | |
| default: return '#2ecc71'; | |
| } | |
| } | |
| isSolid(x, y) { | |
| // Check if coordinates are out of bounds | |
| if (x < 0 || x >= this.width || y < 0 || y >= this.height) { | |
| return true; | |
| } | |
| const tile = this.tiles[y][x]; | |
| return tile === TILE_TYPES.WATER || tile === TILE_TYPES.LAVA; | |
| } | |
| draw(ctx, cameraX, cameraY, viewWidth, viewHeight) { | |
| // Calculate visible tile range | |
| const startX = Math.max(0, Math.floor(cameraX / TILE_SIZE)); | |
| const startY = Math.max(0, Math.floor(cameraY / TILE_SIZE)); | |
| const endX = Math.min(this.width, Math.ceil((cameraX + viewWidth) / TILE_SIZE) + 1); | |
| const endY = Math.min(this.height, Math.ceil((cameraY + viewHeight) / TILE_SIZE) + 1); | |
| // Draw visible tiles | |
| for (let y = startY; y < endY; y++) { | |
| for (let x = startX; x < endX; x++) { | |
| const tileX = x * TILE_SIZE - cameraX; | |
| const tileY = y * TILE_SIZE - cameraY; | |
| // Draw tile | |
| ctx.fillStyle = this.getTileColor(this.tiles[y][x]); | |
| ctx.fillRect(tileX, tileY, TILE_SIZE, TILE_SIZE); | |
| // Add some variation to grass tiles | |
| if (this.tiles[y][x] === TILE_TYPES.GRASS && Math.random() > 0.7) { | |
| ctx.fillStyle = '#27ae60'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| tileX + Math.random() * TILE_SIZE, | |
| tileY + Math.random() * TILE_SIZE, | |
| Math.random() * 2 + 1, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| } | |
| // Add waves to water | |
| if (this.tiles[y][x] === TILE_TYPES.WATER) { | |
| ctx.fillStyle = '#2980b9'; | |
| for (let i = 0; i < 3; i++) { | |
| ctx.beginPath(); | |
| ctx.arc( | |
| tileX + Math.random() * TILE_SIZE, | |
| tileY + Math.random() * TILE_SIZE, | |
| Math.random() * 1.5, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| } | |
| } | |
| // Add wood grain | |
| if (this.tiles[y][x] === TILE_TYPES.WOOD) { | |
| ctx.fillStyle = '#a0522d'; | |
| for (let i = 0; i < 3; i++) { | |
| ctx.fillRect( | |
| tileX + 2, | |
| tileY + 5 + i * 3, | |
| TILE_SIZE - 4, | |
| 1 | |
| ); | |
| } | |
| } | |
| // Add brick pattern | |
| if (this.tiles[y][x] === TILE_TYPES.BRICK) { | |
| ctx.fillStyle = '#e74c3c'; | |
| for (let row = 0; row < 2; row++) { | |
| for (let col = 0; col < 2; col++) { | |
| const offset = row % 2 === 0 ? 0 : 8; | |
| ctx.fillRect( | |
| tileX + offset + col * 8, | |
| tileY + row * 8, | |
| 6, | |
| 6 | |
| ); | |
| } | |
| } | |
| } | |
| // Add lava bubbles | |
| if (this.tiles[y][x] === TILE_TYPES.LAVA) { | |
| ctx.fillStyle = '#f39c12'; | |
| for (let i = 0; i < 2; i++) { | |
| ctx.beginPath(); | |
| ctx.arc( | |
| tileX + Math.random() * TILE_SIZE, | |
| tileY + Math.random() * TILE_SIZE, | |
| Math.random() * 3 + 1, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| drawMinimap(ctx, minimapWidth, minimapHeight) { | |
| // Calculate scale factors | |
| const scaleX = minimapWidth / this.width; | |
| const scaleY = minimapHeight / this.height; | |
| // Draw all tiles | |
| for (let y = 0; y < this.height; y++) { | |
| for (let x = 0; x < this.width; x++) { | |
| ctx.fillStyle = this.getTileColor(this.tiles[y][x]); | |
| ctx.fillRect(x * scaleX, y * scaleY, scaleX, scaleY); | |
| } | |
| } | |
| } | |
| } | |
| // Camera | |
| class Camera { | |
| constructor(target, viewWidth, viewHeight) { | |
| this.target = target; | |
| this.x = target.x; | |
| this.y = target.y; | |
| this.viewWidth = viewWidth; | |
| this.viewHeight = viewHeight; | |
| } | |
| update() { | |
| // Smoothly follow the target | |
| this.x += (this.target.x - this.x) * CAMERA_SMOOTHING; | |
| this.y += (this.target.y - this.y) * CAMERA_SMOOTHING; | |
| // Keep camera within bounds | |
| const halfWidth = this.viewWidth / 2; | |
| const halfHeight = this.viewHeight / 2; | |
| this.x = Math.max(halfWidth, Math.min(MAP_WIDTH * TILE_SIZE - halfWidth, this.x)); | |
| this.y = Math.max(halfHeight, Math.min(MAP_HEIGHT * TILE_SIZE - halfHeight, this.y)); | |
| } | |
| } | |
| // Game Initialization | |
| function initGame() { | |
| // Create canvases | |
| const player1Canvas = document.getElementById('player1-canvas'); | |
| const player2Canvas = document.getElementById('player2-canvas'); | |
| const minimapCanvas = document.getElementById('minimap'); | |
| // Set canvas sizes based on container | |
| resizeCanvases(); | |
| // Create game map | |
| const gameMap = new GameMap(MAP_WIDTH, MAP_HEIGHT); | |
| // Create players | |
| const player1 = new Player(100, 100, 1); | |
| const player2 = new Player(150, 100, 2); | |
| gameState.players = [player1, player2]; | |
| // Create cameras | |
| const player1Camera = new Camera(player1, player1Canvas.width, player1Canvas.height); | |
| const player2Camera = new Camera(player2, player2Canvas.width, player2Canvas.height); | |
| // Create items | |
| gameState.items = [ | |
| new Item(200, 150, 'key'), | |
| new Item(250, 200, 'potion'), | |
| new Item(300, 250, 'key'), | |
| new Item(350, 300, 'potion'), | |
| new Item(400, 350, 'sword'), | |
| new Item(500, 400, 'key') | |
| ]; | |
| // Create doors | |
| const dungeonDoor = new Door(30 * TILE_SIZE, 29 * TILE_SIZE, TILE_SIZE, TILE_SIZE * 2, true, 1); | |
| const bossDoor = new Door(28 * TILE_SIZE, 34 * TILE_SIZE, TILE_SIZE * 4, TILE_SIZE, true, 2); | |
| gameState.doors = [dungeonDoor, bossDoor]; | |
| // Create levers | |
| const lever1 = new Lever(28 * TILE_SIZE, 32 * TILE_SIZE, bossDoor); | |
| gameState.levers = [lever1]; | |
| // Create pushable blocks | |
| const block1 = new PushableBlock(26 * TILE_SIZE, 32 * TILE_SIZE); | |
| const block2 = new PushableBlock(27 * TILE_SIZE, 32 * TILE_SIZE); | |
| gameState.blocks = [block1, block2]; | |
| // Create enemies | |
| const enemy1 = new Enemy(400, 300, 'normal'); | |
| enemy1.patrolPoints = [ | |
| { x: 400, y: 300 }, | |
| { x: 450, y: 300 }, | |
| { x: 450, y: 350 }, | |
| { x: 400, y: 350 } | |
| ]; | |
| const enemy2 = new Enemy(500, 400, 'normal'); | |
| enemy2.patrolPoints = [ | |
| { x: 500, y: 400 }, | |
| { x: 550, y: 400 }, | |
| { x: 550, y: 450 }, | |
| { x: 500, y: 450 } | |
| ]; | |
| const boss = new Enemy(30 * TILE_SIZE, 36 * TILE_SIZE, 'boss'); | |
| gameState.enemies = [enemy1, enemy2, boss]; | |
| gameState.boss = boss; | |
| // Set up input handlers | |
| setupInput(); | |
| // Start game loop | |
| let lastTime = 0; | |
| function gameLoop(timestamp) { | |
| const deltaTime = timestamp - lastTime; | |
| lastTime = timestamp; | |
| // Update game state | |
| updateGame(deltaTime); | |
| // Render game | |
| renderGame(); | |
| // Continue loop | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Start the game loop | |
| requestAnimationFrame(gameLoop); | |
| // Handle window resize | |
| window.addEventListener('resize', resizeCanvases); | |
| // Start button | |
| document.getElementById('start-button').addEventListener('click', () => { | |
| document.getElementById('title-screen').style.display = 'none'; | |
| gameState.gameStarted = true; | |
| playSound('start'); | |
| }); | |
| function resizeCanvases() { | |
| const container = document.getElementById('game-container'); | |
| const isVertical = container.offsetWidth > container.offsetHeight; | |
| if (isVertical) { | |
| // Vertical split (side by side) | |
| player1Canvas.width = container.offsetWidth / 2; | |
| player1Canvas.height = container.offsetHeight; | |
| player2Canvas.width = container.offsetWidth / 2; | |
| player2Canvas.height = container.offsetHeight; | |
| } else { | |
| // Horizontal split (top and bottom) | |
| player1Canvas.width = container.offsetWidth; | |
| player1Canvas.height = container.offsetHeight / 2; | |
| player2Canvas.width = container.offsetWidth; | |
| player2Canvas.height = container.offsetHeight / 2; | |
| } | |
| // Update camera view sizes | |
| player1Camera.viewWidth = player1Canvas.width; | |
| player1Camera.viewHeight = player1Canvas.height; | |
| player2Camera.viewWidth = player2Canvas.width; | |
| player2Camera.viewHeight = player2Canvas.height; | |
| } | |
| function updateGame(deltaTime) { | |
| if (!gameState.gameStarted) return; | |
| gameState.gameTime += deltaTime; | |
| // Update players | |
| gameState.players.forEach(player => player.update(deltaTime)); | |
| // Update enemies | |
| gameState.enemies.forEach(enemy => enemy.update(deltaTime, gameState.players)); | |
| // Update cameras | |
| player1Camera.update(); | |
| player2Camera.update(); | |
| // Update blocks | |
| gameState.blocks.forEach(block => block.update(deltaTime)); | |
| // Check collisions between players and enemies | |
| checkPlayerEnemyCollisions(); | |
| // Check collisions between players and items | |
| checkPlayerItemCollisions(); | |
| // Check collisions between players and doors | |
| checkPlayerDoorCollisions(); | |
| // Check if players are attacking enemies | |
| checkPlayerAttacks(); | |
| // Remove dead enemies | |
| gameState.enemies = gameState.enemies.filter(enemy => enemy.health > 0); | |
| // Check if boss is dead | |
| if (gameState.boss && gameState.boss.health <= 0) { | |
| // Boss is dead - open the door permanently | |
| gameState.doors.forEach(door => { | |
| door.locked = false; | |
| door.isOpen = true; | |
| }); | |
| } | |
| // Update HUD | |
| updateHUD(); | |
| } | |
| function renderGame() { | |
| if (!gameState.gameStarted) return; | |
| // Get contexts | |
| const ctx1 = player1Canvas.getContext('2d'); | |
| const ctx2 = player2Canvas.getContext('2d'); | |
| const minimapCtx = minimapCanvas.getContext('2d'); | |
| // Clear canvases | |
| ctx1.clearRect(0, 0, player1Canvas.width, player1Canvas.height); | |
| ctx2.clearRect(0, 0, player2Canvas.width, player2Canvas.height); | |
| minimapCtx.clearRect(0, 0, minimapCanvas.width, minimapCanvas.height); | |
| // Draw map for each player | |
| gameMap.draw(ctx1, player1Camera.x - player1Canvas.width / 2, player1Camera.y - player1Canvas.height / 2, player1Canvas.width, player1Canvas.height); | |
| gameMap.draw(ctx2, player2Camera.x - player2Canvas.width / 2, player2Camera.y - player2Canvas.height / 2, player2Canvas.width, player2Canvas.height); | |
| // Draw doors | |
| gameState.doors.forEach(door => { | |
| door.draw(ctx1, player1Camera.x - player1Canvas.width / 2, player1Camera.y - player1Canvas.height / 2); | |
| door.draw(ctx2, player2Camera.x - player2Canvas.width / 2, player2Camera.y - player2Canvas.height / 2); | |
| }); | |
| // Draw levers | |
| gameState.levers.forEach(lever => { | |
| lever.draw(ctx1, player1Camera.x - player1Canvas.width / 2, player1Camera.y - player1Canvas.height / 2); | |
| lever.draw(ctx2, player2Camera.x - player2Canvas.width / 2, player2Camera.y - player2Canvas.height / 2); | |
| }); | |
| // Draw blocks | |
| gameState.blocks.forEach(block => { | |
| block.draw(ctx1, player1Camera.x - player1Canvas.width / 2, player1Camera.y - player1Canvas.height / 2); | |
| block.draw(ctx2, player2Camera.x - player2Canvas.width / 2, player2Camera.y - player2Canvas.height / 2); | |
| }); | |
| // Draw items | |
| gameState.items.forEach(item => { | |
| item.draw(ctx1, player1Camera.x - player1Canvas.width / 2, player1Camera.y - player1Canvas.height / 2); | |
| item.draw(ctx2, player2Camera.x - player2Canvas.width / 2, player2Camera.y - player2Canvas.height / 2); | |
| }); | |
| // Draw enemies | |
| gameState.enemies.forEach(enemy => { | |
| enemy.draw(ctx1, player1Camera.x - player1Canvas.width / 2, player1Camera.y - player1Canvas.height / 2); | |
| enemy.draw(ctx2, player2Camera.x - player2Canvas.width / 2, player2Camera.y - player2Canvas.height / 2); | |
| }); | |
| // Draw players | |
| player1.draw(ctx1, player1Camera.x - player1Canvas.width / 2, player1Camera.y - player1Canvas.height / 2); | |
| player2.draw(ctx2, player2Camera.x - player2Canvas.width / 2, player2Camera.y - player2Canvas.height / 2); | |
| // Draw minimap | |
| drawMinimap(minimapCtx); | |
| } | |
| function drawMinimap(ctx) { | |
| // Draw the map | |
| gameMap.drawMinimap(ctx, minimapCanvas.width, minimapCanvas.height); | |
| // Draw players | |
| ctx.fillStyle = '#3498db'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| gameState.players[0].x / MAP_WIDTH * minimapCanvas.width, | |
| gameState.players[0].y / MAP_HEIGHT * minimapCanvas.height, | |
| 3, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| ctx.fillStyle = '#e74c3c'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| gameState.players[1].x / MAP_WIDTH * minimapCanvas.width, | |
| gameState.players[1].y / MAP_HEIGHT * minimapCanvas.height, | |
| 3, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| // Draw dungeon | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillRect( | |
| 25 / MAP_WIDTH * minimapCanvas.width, | |
| 30 / MAP_HEIGHT * minimapCanvas.height, | |
| 10 / MAP_WIDTH * minimapCanvas.width, | |
| 8 / MAP_HEIGHT * minimapCanvas.height | |
| ); | |
| } | |
| function updateHUD() { | |
| // Update player 1 HUD | |
| document.getElementById('player1-health').style.width = `${(gameState.players[0].health / gameState.players[0].maxHealth) * 100}%`; | |
| const inventory1 = document.getElementById('player1-inventory'); | |
| // Clear inventory slots | |
| for (let i = 0; i < 3; i++) { | |
| const slot = inventory1.children[i]; | |
| slot.innerHTML = ''; | |
| if (i < gameState.players[0].inventory.length) { | |
| const item = gameState.players[0].inventory[i]; | |
| if (item.type === 'key') { | |
| slot.innerHTML = '🔑'; | |
| } else if (item.type === 'potion') { | |
| slot.innerHTML = '❤️'; | |
| } else if (item.type === 'sword') { | |
| slot.innerHTML = '⚔️'; | |
| } | |
| } | |
| } | |
| // Update player 2 HUD | |
| document.getElementById('player2-health').style.width = `${(gameState.players[1].health / gameState.players[1].maxHealth) * 100}%`; | |
| const inventory2 = document.getElementById('player2-inventory'); | |
| // Clear inventory slots | |
| for (let i = 0; i < 3; i++) { | |
| const slot = inventory2.children[i]; | |
| slot.innerHTML = ''; | |
| if (i < gameState.players[1].inventory.length) { | |
| const item = gameState.players[1].inventory[i]; | |
| if (item.type === 'key') { | |
| slot.innerHTML = '🔑'; | |
| } else if (item.type === 'potion') { | |
| slot.innerHTML = '❤️'; | |
| } else if (item.type === 'sword') { | |
| slot.innerHTML = '⚔️'; | |
| } | |
| } | |
| } | |
| } | |
| function checkPlayerEnemyCollisions() { | |
| for (const player of gameState.players) { | |
| for (const enemy of gameState.enemies) { | |
| if (player.collidesWith(enemy) && enemy.attackCooldown <= 0) { | |
| player.takeDamage(enemy.damage); | |
| enemy.attackCooldown = 1000; // 1 second cooldown | |
| } | |
| } | |
| } | |
| } | |
| function checkPlayerItemCollisions() { | |
| for (let i = gameState.items.length - 1; i >= 0; i--) { | |
| const item = gameState.items[i]; | |
| for (const player of gameState.players) { | |
| if (player.collidesWith(item)) { | |
| if (player.addToInventory(item)) { | |
| gameState.items.splice(i, 1); | |
| playSound('item'); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function checkPlayerDoorCollisions() { | |
| for (const door of gameState.doors) { | |
| for (const player of gameState.players) { | |
| if (player.collidesWith(door) && door.locked) { | |
| // Player is trying to open a locked door | |
| if (player.keys > 0) { | |
| door.unlock(player); | |
| player.keys--; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function checkPlayerAttacks() { | |
| for (const player of gameState.players) { | |
| if (player.isAttacking) { | |
| for (const enemy of gameState.enemies) { | |
| // Calculate attack position | |
| const attackX = player.centerX + player.facing.x * ATTACK_RANGE; | |
| const attackY = player.centerY + player.facing.y * ATTACK_RANGE; | |
| // Check if enemy is within attack range | |
| const dx = attackX - enemy.centerX; | |
| const dy = attackY - enemy.centerY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 20) { | |
| if (enemy.takeDamage(25)) { | |
| playSound('enemy-death'); | |
| } else { | |
| playSound('hit'); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Input Handling | |
| function setupInput() { | |
| const keys = { | |
| // Player 1 | |
| w: false, | |
| a: false, | |
| s: false, | |
| d: false, | |
| f: false, | |
| g: false, | |
| // Player 2 | |
| ArrowUp: false, | |
| ArrowLeft: false, | |
| ArrowDown: false, | |
| ArrowRight: false, | |
| l: false, | |
| k: false | |
| }; | |
| // Keyboard event listeners | |
| window.addEventListener('keydown', (e) => { | |
| if (e.key in keys) { | |
| keys[e.key] = true; | |
| e.preventDefault(); | |
| } | |
| // Player 1 item use | |
| if (e.key === '1' && gameState.players[0]) { | |
| gameState.players[0].useItem(0); | |
| } else if (e.key === '2' && gameState.players[0]) { | |
| gameState.players[0].useItem(1); | |
| } else if (e.key === '3' && gameState.players[0]) { | |
| gameState.players[0].useItem(2); | |
| } | |
| // Player 2 item use | |
| if (e.key === '7' && gameState.players[1]) { | |
| gameState.players[1].useItem(0); | |
| } else if (e.key === '8' && gameState.players[1]) { | |
| gameState.players[1].useItem(1); | |
| } else if (e.key === '9' && gameState.players[1]) { | |
| gameState.players[1].useItem(2); | |
| } | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| if (e.key in keys) { | |
| keys[e.key] = false; | |
| e.preventDefault(); | |
| } | |
| }); | |
| // Gamepad support | |
| let gamepads = []; | |
| window.addEventListener("gamepadconnected", (e) => { | |
| console.log("Gamepad connected:", e.gamepad); | |
| gamepads[e.gamepad.index] = e.gamepad; | |
| }); | |
| window.addEventListener("gamepaddisconnected", (e) => { | |
| console.log("Gamepad disconnected:", e.gamepad); | |
| delete gamepads[e.gamepad.index]; | |
| }); | |
| // Input update loop | |
| function updateInput() { | |
| if (!gameState.gameStarted) return; | |
| // Player 1 controls (WASD, F, G) | |
| const player1 = gameState.players[0]; | |
| if (player1) { | |
| player1.vx = 0; | |
| player1.vy = 0; | |
| if (keys.w) player1.vy -= PLAYER_SPEED; | |
| if (keys.s) player1.vy += PLAYER_SPEED; | |
| if (keys.a) player1.vx -= PLAYER_SPEED; | |
| if (keys.d) player1.vx += PLAYER_SPEED; | |
| // Normalize diagonal movement | |
| if (player1.vx !== 0 && player1.vy !== 0) { | |
| player1.vx *= 0.7071; // 1/sqrt(2) | |
| player1.vy *= 0.7071; | |
| } | |
| // Attack | |
| if (keys.f) { | |
| if (player1.attack()) { | |
| playSound('sword'); | |
| } | |
| } | |
| // Interact (use levers, push blocks) | |
| if (keys.g) { | |
| checkPlayerInteractions(player1); | |
| } | |
| } | |
| // Player 2 controls (Arrows, L, K) | |
| const player2 = gameState.players[1]; | |
| if (player2) { | |
| player2.vx = 0; | |
| player2.vy = 0; | |
| if (keys.ArrowUp) player2.vy -= PLAYER_SPEED; | |
| if (keys.ArrowDown) player2.vy += PLAYER_SPEED; | |
| if (keys.ArrowLeft) player2.vx -= PLAYER_SPEED; | |
| if (keys.ArrowRight) player2.vx += PLAYER_SPEED; | |
| // Normalize diagonal movement | |
| if (player2.vx !== 0 && player2.vy !== 0) { | |
| player2.vx *= 0.7071; // 1/sqrt(2) | |
| player2.vy *= 0.7071; | |
| } | |
| // Attack | |
| if (keys.l) { | |
| if (player2.attack()) { | |
| playSound('sword'); | |
| } | |
| } | |
| // Interact (use levers, push blocks) | |
| if (keys.k) { | |
| checkPlayerInteractions(player2); | |
| } | |
| } | |
| // Gamepad controls | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Deigomax02/zfgjf" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |