diff --git a/highway_vector.py b/highway_vector.py index 1096105..0933950 100644 --- a/highway_vector.py +++ b/highway_vector.py @@ -1,32 +1,35 @@ +import math import sys import pygame -# Inicialización de Pygame pygame.init() -# Constantes de Pantalla +# Configuración de pantalla SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 FPS = 60 -# Configuración de Cuadrícula e Isometría +# Isometría y Cuadrícula TILE_WIDTH = 64 TILE_HEIGHT = 32 GRID_SIZE_X = 8 -GRID_SIZE_Y = 100 # Largo de la autopista +GRID_SIZE_Y = 120 # Distancia total de la autopista -# Colores (Paleta Retro Spectrum/Amstrad) -COLOR_BG = (10, 10, 15) -COLOR_GRID = (40, 40, 60) -COLOR_ROAD = (60, 60, 80) -COLOR_VORTEX = (0, 255, 200) -COLOR_PLAYER = (255, 200, 0) -COLOR_BLOCK = (200, 50, 50) -COLOR_WALL = (100, 100, 120) +# Paleta de Colores +COLOR_BG = (15, 15, 25) +COLOR_ROAD = (50, 50, 65) +COLOR_GRID = (30, 30, 45) +COLOR_VORTEX = (220, 50, 50) +COLOR_PLAYER = (255, 220, 0) +COLOR_SPARE = (180, 160, 0) +COLOR_BLOCK = (180, 70, 40) +COLOR_WALL = (80, 90, 110) +COLOR_ENEMY = (50, 200, 100) +COLOR_BULLET = (0, 255, 255) def grid_to_iso(gx, gy, gz=0): - """Convierte coordenadas de cuadrícula 3D (x, y, z) a coordenadas de pantalla 2D.""" + """Convierte coordenadas 3D de rejilla en coordenadas 2D en pantalla.""" iso_x = (gx - gy) * (TILE_WIDTH // 2) iso_y = (gx + gy) * (TILE_HEIGHT // 2) - (gz * 16) return iso_x, iso_y @@ -41,44 +44,79 @@ class Entity: self.color = color self.is_solid = is_solid self.radius = 0.4 + self.height = 20 def draw(self, surface, camera_offset_x, camera_offset_y): screen_x, screen_y = grid_to_iso(self.gx, self.gy, self.gz) draw_x = screen_x + camera_offset_x draw_y = screen_y + camera_offset_y - # Dibujar un bloque o personaje estilo isométrico points = [ - (draw_x, draw_y - 20), - (draw_x + TILE_WIDTH // 2, draw_y - 20 + TILE_HEIGHT // 2), - (draw_x, draw_y - 20 + TILE_HEIGHT), - (draw_x - TILE_WIDTH // 2, draw_y - 20 + TILE_HEIGHT // 2), + (draw_x, draw_y - self.height), + ( + draw_x + TILE_WIDTH // 2, + draw_y - self.height + TILE_HEIGHT // 2, + ), + (draw_x, draw_y - self.height + TILE_HEIGHT), + ( + draw_x - TILE_WIDTH // 2, + draw_y - self.height + TILE_HEIGHT // 2, + ), ] pygame.draw.polygon(surface, self.color, points) - pygame.draw.polygon( - surface, (255, 255, 255), points, 2 - ) # Borde brillante + pygame.draw.polygon(surface, (255, 255, 255), points, 2) + + +class Bullet(Entity): + + def __init__(self, gx, gy, dir_x, dir_y): + super().__init__(gx, gy, COLOR_BULLET, is_solid=False) + self.dir_x = dir_x + self.dir_y = dir_y + self.speed = 0.25 + self.height = 10 + self.alive = True + + def update(self, entities): + self.gx += self.dir_x * self.speed + self.gy += self.dir_y * self.speed + + # Comprobar límites + if not (0 <= self.gx < GRID_SIZE_X and 0 <= self.gy < GRID_SIZE_Y): + self.alive = False + return + + # Impacto con entidades + for entity in entities: + if entity != self and entity.is_solid: + if ( + abs(entity.gx - self.gx) < 0.5 + and abs(entity.gy - self.gy) < 0.5 + ): + self.alive = False + if isinstance(entity, (PushBlock, Enemy)): + entity.health -= 1 + break class PushBlock(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, COLOR_BLOCK, is_solid=True) + self.health = 2 # Se puede destruir tras 2 disparos def push(self, dx, dy, entities): new_gx = self.gx + dx new_gy = self.gy + dy - # Comprobar límites de la carretera if not (0 <= new_gx < GRID_SIZE_X and 0 <= new_gy < GRID_SIZE_Y): return False - # Comprobar colisión con otras entidades for entity in entities: if entity != self and entity.is_solid: if ( - abs(entity.gx - new_gx) < 0.6 - and abs(entity.gy - new_gy) < 0.6 + abs(entity.gx - new_gx) < 0.5 + and abs(entity.gy - new_gy) < 0.5 ): return False @@ -87,13 +125,35 @@ class PushBlock(Entity): return True +class Enemy(Entity): + + def __init__(self, gx, gy): + super().__init__(gx, gy, COLOR_ENEMY, is_solid=True) + self.speed = 0.03 + self.direction = 1 + self.health = 1 + + def update(self, entities): + target_gx = self.gx + (self.speed * self.direction) + + # Rebotar en los bordes de la carretera + if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5: + self.direction *= -1 + target_gx = self.gx + (self.speed * self.direction) + + self.gx = target_gx + + class Player(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, COLOR_PLAYER, is_solid=True) - self.speed = 0.08 + self.speed = 0.09 + self.facing_x = 0.0 + self.facing_y = -1.0 # Mirando hacia adelante por defecto + self.shoot_cooldown = 0 - def update(self, keys, entities): + def update(self, keys, entities, bullets): dx, dy = 0, 0 if keys[pygame.K_LEFT] or keys[pygame.K_a]: dx -= self.speed @@ -105,51 +165,66 @@ class Player(Entity): dy += self.speed if dx != 0 or dy != 0: + # Actualizar la dirección en la que mira el jugador + norm = math.sqrt(dx * dx + dy * dy) + self.facing_x = dx / norm + self.facing_y = dy / norm + target_gx = self.gx + dx target_gy = self.gy + dy - # Limites horizontales - if not (0 <= target_gx < GRID_SIZE_X): - dx = 0 - if not (0 <= target_gy < GRID_SIZE_Y): - dy = 0 + if 0 <= target_gx < GRID_SIZE_X and 0 <= target_gy < GRID_SIZE_Y: + blocked = False + for entity in entities: + if entity != self and entity.is_solid: + if ( + abs(entity.gx - target_gx) < 0.5 + and abs(entity.gy - target_gy) < 0.5 + ): + if isinstance(entity, PushBlock): + push_dx = ( + 0.15 if dx > 0 else (-0.15 if dx < 0 else 0) + ) + push_dy = ( + 0.15 if dy > 0 else (-0.15 if dy < 0 else 0) + ) + if not entity.push(push_dx, push_dy, entities): + blocked = True + else: + blocked = True + if not blocked: + self.gx = target_gx + self.gy = target_gy - # Intento de movimiento y colisión con bloques - for entity in entities: - if entity != self and entity.is_solid: - if ( - abs(entity.gx - (self.gx + dx)) < 0.6 - and abs(entity.gy - (self.gy + dy)) < 0.6 - ): - # Intentar empujar si es un PushBlock - if isinstance(entity, PushBlock): - push_dx = 0.1 if dx > 0 else (-0.1 if dx < 0 else 0) - push_dy = 0.1 if dy > 0 else (-0.1 if dy < 0 else 0) - if not entity.push(push_dx, push_dy, entities): - return - else: - return # Bloqueado + # Gestión del enfriamiento de disparo + if self.shoot_cooldown > 0: + self.shoot_cooldown -= 1 - self.gx += dx - self.gy += dy + # Disparar al pulsar la barra espaciadora + if keys[pygame.K_SPACE] and self.shoot_cooldown == 0: + # Generar la bala desplazada por delante del jugador para evitar la auto-colisión + spawn_x = self.gx + (self.facing_x * 0.7) + spawn_y = self.gy + (self.facing_y * 0.7) + + bullets.append(Bullet(spawn_x, spawn_y, self.facing_x, self.facing_y)) + self.shoot_cooldown = 15 # Cadencia de tiro class Vortex(Entity): def __init__(self, gx, gy): super().__init__(gx, gy, COLOR_VORTEX, is_solid=True) - self.speed = 0.015 # Avance constante automático + self.speed = 0.012 def update(self, entities): target_gy = self.gy - self.speed - # Verificar si hay algún obstáculo al avanzar blocked = False for entity in entities: if entity != self and entity.is_solid: if ( - abs(entity.gx - self.gx) < 0.7 - and abs(entity.gy - target_gy) < 0.7 + abs(entity.gx - self.gx) < 0.6 + and abs(entity.gy - target_gy) < 0.6 ): blocked = True break @@ -162,51 +237,123 @@ class Game: def __init__(self): self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) - pygame.display.set_caption("Highway Encounter Python Remake") + pygame.display.set_caption("Highway Encounter - Full Game") self.clock = pygame.time.Clock() + self.font = pygame.font.SysFont("monospace", 18, bold=True) - self.vortex = Vortex(3.5, 95) - self.player = Player(3.5, 92) + self.lives = 4 + self.score = 0 + + self.reset_level() + + def reset_level(self): + self.vortex = Vortex(3.5, GRID_SIZE_Y - 5) + self.player = Player(3.5, GRID_SIZE_Y - 8) self.entities = [self.vortex, self.player] + self.bullets = [] - # Generar bloques de prueba a lo largo de la autopista - for y in range(10, 90, 5): + # Crear escuadrón de reserva (Vidas restantes) + self.spares = [] + for i in range(self.lives - 1): + spare = Entity( + 3.5, GRID_SIZE_Y - 3 + (i * 1.2), COLOR_SPARE, is_solid=False + ) + self.spares.append(spare) + + # Generar obstáculos y enemigos en la autopista + for y in range(10, GRID_SIZE_Y - 15, 6): self.entities.append(PushBlock(y % GRID_SIZE_X, y)) - self.entities.append(PushBlock((y + 3) % GRID_SIZE_X, y + 2)) + if y % 12 == 0: + self.entities.append(Enemy(3.5, y - 2)) + + def handle_collisions(self): + # Muerte del jugador por contacto con enemigos + for entity in self.entities: + if isinstance(entity, Enemy): + if ( + abs(entity.gx - self.player.gx) < 0.5 + and abs(entity.gy - self.player.gy) < 0.5 + ): + self.lives -= 1 + if self.lives > 0: + self.reset_level() + return + + # Eliminar bloques/enemigos destruidos + for entity in self.entities[:]: + if hasattr(entity, "health") and entity.health <= 0: + self.entities.remove(entity) + self.score += 100 + + def draw_hud(self): + zone = int((GRID_SIZE_Y - self.vortex.gy) / 4) + hud_text = f"ZONE: {zone:02d} | LIVES: {self.lives} | SCORE: {self.score:06d}" + text_surface = self.font.render(hud_text, True, (255, 255, 255)) + pygame.draw.rect(self.screen, (0, 0, 0), (0, 0, SCREEN_WIDTH, 40)) + self.screen.blit(text_surface, (20, 10)) def run(self): running = True while running: self.clock.tick(FPS) - # 1. Procesar Eventos for event in pygame.event.get(): if event.type == pygame.QUIT: running = False - # 2. Actualizar Estado + if self.lives <= 0: + self.screen.fill((0, 0, 0)) + msg = self.font.render( + "GAME OVER - Press R to Restart", True, (255, 50, 50) + ) + self.screen.blit( + msg, (SCREEN_WIDTH // 2 - 150, SCREEN_HEIGHT // 2) + ) + pygame.display.flip() + + keys = pygame.key.get_pressed() + if keys[pygame.K_r]: + self.lives = 4 + self.score = 0 + self.reset_level() + continue + + # Actualizaciones keys = pygame.key.get_pressed() - self.player.update(keys, self.entities) + self.player.update(keys, self.entities, self.bullets) self.vortex.update(self.entities) - # 3. Calcular desplazamiento de cámara (centrada en el VORTEX o el jugador) + for entity in self.entities: + if isinstance(entity, Enemy): + entity.update(self.entities) + + for bullet in self.bullets[:]: + bullet.update(self.entities) + if not bullet.alive: + self.bullets.remove(bullet) + + self.handle_collisions() + + # Posicionamiento de cámara centrado en el jugador iso_x, iso_y = grid_to_iso(self.player.gx, self.player.gy) camera_offset_x = SCREEN_WIDTH // 2 - iso_x camera_offset_y = SCREEN_HEIGHT // 2 - iso_y + 100 - # 4. Renderizado + # Renderizado self.screen.fill(COLOR_BG) - # Dibujar suelo (Autopista) + # Dibujar suelo for gy in range(0, GRID_SIZE_Y): for gx in range(0, GRID_SIZE_X): sx, sy = grid_to_iso(gx, gy) draw_x = sx + camera_offset_x draw_y = sy + camera_offset_y - # Solo dibujar si está visible en pantalla - if -100 < draw_x < SCREEN_WIDTH + 100 and -100 < draw_y < SCREEN_HEIGHT + 100: + if ( + -100 < draw_x < SCREEN_WIDTH + 100 + and -100 < draw_y < SCREEN_HEIGHT + 100 + ): pts = [ (draw_x, draw_y), ( @@ -222,13 +369,15 @@ class Game: pygame.draw.polygon(self.screen, COLOR_ROAD, pts) pygame.draw.polygon(self.screen, COLOR_GRID, pts, 1) - # Ordenar entidades por coordenada 'gy' descendente para renderizar correctamente la profundidad + # Renderizado en orden de profundidad (Z-Sorting por Y) + all_drawables = self.entities + self.bullets + self.spares sorted_entities = sorted( - self.entities, key=lambda e: e.gy, reverse=True + all_drawables, key=lambda e: e.gy, reverse=True ) for entity in sorted_entities: entity.draw(self.screen, camera_offset_x, camera_offset_y) + self.draw_hud() pygame.display.flip() pygame.quit() @@ -237,3 +386,4 @@ class Game: if __name__ == "__main__": Game().run() + diff --git a/img-versiones/version_0.2.png b/img-versiones/version_0.2.png new file mode 100644 index 0000000..c883b6c Binary files /dev/null and b/img-versiones/version_0.2.png differ