From ce640e1d931007b7ab5ef6c29d0129545fff7506 Mon Sep 17 00:00:00 2001 From: luisgulo Date: Sun, 4 Oct 2026 15:11:10 +0200 Subject: [PATCH] mejora movimiento --- highway_vector.py | 288 ++++++++++++++++++++++++++-------- img-versiones/version_0.2.png | Bin 0 -> 16870 bytes 2 files changed, 219 insertions(+), 69 deletions(-) create mode 100644 img-versiones/version_0.2.png 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 0000000000000000000000000000000000000000..c883b6cb543ed2c54fb61d198b4548ef3de918f2 GIT binary patch literal 16870 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