zfgjf / index.html
Deigomax02's picture
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
<!DOCTYPE html>
<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>