805 lines
26 KiB
Python
805 lines
26 KiB
Python
import math
|
|
import os
|
|
import sys
|
|
import array
|
|
import random
|
|
import pygame
|
|
|
|
# Inicialización de Pygame y el mezclador de audio
|
|
pygame.init()
|
|
pygame.mixer.init(frequency=22050, size=-16, channels=1)
|
|
|
|
# Configuración de Pantalla
|
|
SCREEN_WIDTH = 800
|
|
SCREEN_HEIGHT = 600
|
|
FPS = 60
|
|
|
|
# Isometría y Cuadrícula
|
|
TILE_WIDTH = 64
|
|
TILE_HEIGHT = 32
|
|
GRID_SIZE_X = 8
|
|
GRID_SIZE_Y = 120
|
|
|
|
# Colores del Entorno
|
|
COLOR_BG = (15, 15, 25)
|
|
COLOR_ROAD = (50, 50, 65)
|
|
COLOR_GRID = (30, 30, 45)
|
|
|
|
# Paleta de Colores
|
|
COLOR_YELLOW = (255, 215, 0)
|
|
COLOR_WHITE = (240, 240, 240)
|
|
COLOR_PURPLE = (100, 30, 140)
|
|
COLOR_RED = (220, 40, 40)
|
|
COLOR_BROWN = (139, 69, 19)
|
|
COLOR_ORANGE = (255, 140, 0)
|
|
COLOR_CYAN = (0, 200, 255)
|
|
|
|
|
|
def generate_sound(sound_type):
|
|
"""Genera efectos de sonido retro de forma sintética sin archivos externos."""
|
|
sample_rate = 22050
|
|
buffer = array.array("h")
|
|
|
|
if sound_type == "shoot":
|
|
# Disparo: Tono descendente agudo rápido
|
|
duration = 0.1
|
|
total_samples = int(sample_rate * duration)
|
|
for i in range(total_samples):
|
|
t = i / total_samples
|
|
freq = 800 * (1 - t * 0.7)
|
|
val = math.sin(2 * math.pi * freq * (i / sample_rate))
|
|
env = 1 - t
|
|
sample = int(val * env * 12000)
|
|
buffer.append(sample)
|
|
|
|
elif sound_type == "hit":
|
|
# Choque: Golpe sordo de baja frecuencia
|
|
duration = 0.15
|
|
total_samples = int(sample_rate * duration)
|
|
for i in range(total_samples):
|
|
t = i / total_samples
|
|
freq = 150 * (1 - t * 0.5)
|
|
noise = (random.random() * 2 - 1) * 0.3
|
|
val = math.sin(2 * math.pi * freq * (i / sample_rate)) + noise
|
|
env = (1 - t) ** 2
|
|
sample = int(val * env * 15000)
|
|
buffer.append(max(-32768, min(32767, sample)))
|
|
|
|
elif sound_type == "destroy":
|
|
# Destrucción: Ruido de explosión corta
|
|
duration = 0.2
|
|
total_samples = int(sample_rate * duration)
|
|
for i in range(total_samples):
|
|
t = i / total_samples
|
|
noise = random.random() * 2 - 1
|
|
env = (1 - t) ** 1.5
|
|
sample = int(noise * env * 14000)
|
|
buffer.append(sample)
|
|
|
|
return pygame.mixer.Sound(buffer=buffer)
|
|
|
|
|
|
# Cargar sonidos sintéticos
|
|
SOUND_SHOOT = generate_sound("shoot")
|
|
SOUND_HIT = generate_sound("hit")
|
|
SOUND_DESTROY = generate_sound("destroy")
|
|
|
|
|
|
def grid_to_iso(gx, gy, gz=0):
|
|
"""Convierte coordenadas 3D de rejilla a coordenadas 2D de pantalla."""
|
|
iso_x = (gx - gy) * (TILE_WIDTH // 2)
|
|
iso_y = (gx + gy) * (TILE_HEIGHT // 2) - (gz * 16)
|
|
return iso_x, iso_y
|
|
|
|
|
|
def draw_isometric_prism(surface, color, width=40, height=35):
|
|
"""Genera una superficie isométrica con forma de prisma o cubo."""
|
|
surf = pygame.Surface((64, 64), pygame.SRCALPHA)
|
|
cx, cy = 32, 32
|
|
|
|
w2 = width // 2
|
|
h2 = width // 4
|
|
|
|
top_pts = [
|
|
(cx, cy - height),
|
|
(cx + w2, cy - height + h2),
|
|
(cx, cy - height + 2 * h2),
|
|
(cx - w2, cy - height + h2),
|
|
]
|
|
|
|
left_pts = [
|
|
(cx - w2, cy - height + h2),
|
|
(cx, cy - height + 2 * h2),
|
|
(cx, cy + 2 * h2),
|
|
(cx - w2, cy + h2),
|
|
]
|
|
|
|
right_pts = [
|
|
(cx, cy - height + 2 * h2),
|
|
(cx + w2, cy - height + h2),
|
|
(cx + w2, cy + h2),
|
|
(cx, cy + 2 * h2),
|
|
]
|
|
|
|
c_top = color
|
|
c_left = tuple(max(0, c - 40) for c in color)
|
|
c_right = tuple(max(0, c - 70) for c in color)
|
|
|
|
pygame.draw.polygon(surf, c_top, top_pts)
|
|
pygame.draw.polygon(surf, c_left, left_pts)
|
|
pygame.draw.polygon(surf, c_right, right_pts)
|
|
|
|
pygame.draw.polygon(surf, (20, 20, 20), top_pts, 1)
|
|
pygame.draw.polygon(surf, (20, 20, 20), left_pts, 1)
|
|
pygame.draw.polygon(surf, (20, 20, 20), right_pts, 1)
|
|
|
|
return surf
|
|
|
|
|
|
def create_placeholder_sprites():
|
|
sprites = {}
|
|
|
|
surf_player = pygame.Surface((64, 64), pygame.SRCALPHA)
|
|
base_pts = [(32, 48), (52, 38), (32, 28), (12, 38)]
|
|
apex = (32, 8)
|
|
|
|
pygame.draw.polygon(
|
|
surf_player, (200, 160, 0), [base_pts[3], base_pts[0], apex]
|
|
)
|
|
pygame.draw.polygon(
|
|
surf_player, (255, 215, 0), [base_pts[0], base_pts[1], apex]
|
|
)
|
|
pygame.draw.polygon(surf_player, (255, 235, 100), base_pts)
|
|
pygame.draw.line(surf_player, (50, 50, 0), apex, base_pts[0], 2)
|
|
sprites["player"] = surf_player
|
|
|
|
surf_vortex = draw_isometric_prism(None, COLOR_YELLOW, width=38, height=25)
|
|
pygame.draw.circle(surf_vortex, (255, 255, 200), (32, 12), 8)
|
|
pygame.draw.circle(surf_vortex, (0, 0, 0), (32, 12), 8, 1)
|
|
sprites["vortex"] = surf_vortex
|
|
|
|
# Bloques
|
|
sprites["block_white"] = draw_isometric_prism(
|
|
None, COLOR_WHITE, width=40, height=25
|
|
)
|
|
sprites["block_purple"] = draw_isometric_prism(
|
|
None, COLOR_PURPLE, width=40, height=25
|
|
)
|
|
|
|
# Enemigos
|
|
sprites["enemy_indestructible"] = draw_isometric_prism(
|
|
None, COLOR_WHITE, width=32, height=35
|
|
)
|
|
sprites["enemy_red"] = draw_isometric_prism(
|
|
None, COLOR_RED, width=32, height=35
|
|
)
|
|
sprites["enemy_brown"] = draw_isometric_prism(
|
|
None, COLOR_BROWN, width=32, height=35
|
|
)
|
|
sprites["enemy_orange"] = draw_isometric_prism(
|
|
None, COLOR_ORANGE, width=32, height=35
|
|
)
|
|
sprites["enemy_diagonal"] = draw_isometric_prism(
|
|
None, COLOR_CYAN, width=32, height=35
|
|
)
|
|
|
|
surf_bullet = pygame.Surface((32, 32), pygame.SRCALPHA)
|
|
pygame.draw.circle(surf_bullet, (0, 255, 255), (16, 16), 4)
|
|
sprites["bullet"] = surf_bullet
|
|
|
|
return sprites
|
|
|
|
|
|
class SpriteManager:
|
|
|
|
def __init__(self):
|
|
self.placeholders = create_placeholder_sprites()
|
|
self.cache = {}
|
|
|
|
def get_sprite(self, name, filename=None):
|
|
if name in self.cache:
|
|
return self.cache[name]
|
|
|
|
if filename and os.path.exists(filename):
|
|
try:
|
|
img = pygame.image.load(filename).convert_alpha()
|
|
self.cache[name] = img
|
|
return img
|
|
except Exception as e:
|
|
print(f"Error cargando {filename}: {e}")
|
|
|
|
self.cache[name] = self.placeholders[name]
|
|
return self.cache[name]
|
|
|
|
|
|
class Entity:
|
|
|
|
def __init__(self, gx, gy, sprite_name, filename=None, is_solid=True):
|
|
self.gx = float(gx)
|
|
self.gy = float(gy)
|
|
self.gz = 0
|
|
self.sprite_name = sprite_name
|
|
self.filename = filename
|
|
self.is_solid = is_solid
|
|
|
|
def draw(self, surface, camera_offset_x, camera_offset_y, sprite_mgr):
|
|
screen_x, screen_y = grid_to_iso(self.gx, self.gy, self.gz)
|
|
sprite = sprite_mgr.get_sprite(self.sprite_name, self.filename)
|
|
|
|
draw_x = screen_x + camera_offset_x - (sprite.get_width() // 2)
|
|
draw_y = (
|
|
screen_y + camera_offset_y - sprite.get_height() + (TILE_HEIGHT // 2)
|
|
)
|
|
|
|
surface.blit(sprite, (draw_x, draw_y))
|
|
|
|
|
|
class Bullet(Entity):
|
|
|
|
def __init__(self, gx, gy, dir_x, dir_y):
|
|
super().__init__(gx, gy, "bullet", "bullet.png", is_solid=False)
|
|
self.dir_x = dir_x
|
|
self.dir_y = dir_y
|
|
self.speed = 0.25
|
|
self.alive = True
|
|
|
|
def update(self, entities):
|
|
self.gx += self.dir_x * self.speed
|
|
self.gy += self.dir_y * self.speed
|
|
|
|
if not (0 <= self.gx < GRID_SIZE_X and 0 <= self.gy < GRID_SIZE_Y):
|
|
self.alive = False
|
|
return
|
|
|
|
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
|
|
SOUND_HIT.play()
|
|
if isinstance(entity, WhiteBlock):
|
|
entity.health -= 1
|
|
elif isinstance(entity, PurpleBlock):
|
|
entity.push_by_bullet(self.dir_x, self.dir_y, entities)
|
|
elif isinstance(entity, Enemy) and entity.destructible:
|
|
entity.take_damage()
|
|
break
|
|
|
|
|
|
class WhiteBlock(Entity):
|
|
|
|
def __init__(self, gx, gy):
|
|
super().__init__(
|
|
gx, gy, "block_white", "block_white.png", is_solid=True
|
|
)
|
|
self.health = 2
|
|
|
|
def push(self, dx, dy, entities):
|
|
new_gx = self.gx + dx
|
|
new_gy = self.gy + dy
|
|
|
|
if not (0 <= new_gx < GRID_SIZE_X and 0 <= new_gy < GRID_SIZE_Y):
|
|
return False
|
|
|
|
for entity in entities:
|
|
if entity != self and entity.is_solid:
|
|
if (
|
|
abs(entity.gx - new_gx) < 0.5
|
|
and abs(entity.gy - new_gy) < 0.5
|
|
):
|
|
return False
|
|
|
|
self.gx = new_gx
|
|
self.gy = new_gy
|
|
return True
|
|
|
|
|
|
class PurpleBlock(Entity):
|
|
|
|
def __init__(self, gx, gy):
|
|
super().__init__(
|
|
gx, gy, "block_purple", "block_purple.png", is_solid=True
|
|
)
|
|
|
|
def push_by_bullet(self, dir_x, dir_y, entities):
|
|
step_x = 1 if dir_x > 0.3 else (-1 if dir_x < -0.3 else 0)
|
|
step_y = 1 if dir_y > 0.3 else (-1 if dir_y < -0.3 else 0)
|
|
|
|
new_gx = self.gx + step_x
|
|
new_gy = self.gy + step_y
|
|
|
|
if not (0 <= new_gx < GRID_SIZE_X and 0 <= new_gy < GRID_SIZE_Y):
|
|
return False
|
|
|
|
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
|
|
):
|
|
return False
|
|
|
|
self.gx = new_gx
|
|
self.gy = new_gy
|
|
return True
|
|
|
|
|
|
class Enemy(Entity):
|
|
|
|
def __init__(self, gx, gy, destructible=True):
|
|
sprite = "enemy_red" if destructible else "enemy_indestructible"
|
|
super().__init__(gx, gy, sprite, "enemy.png", is_solid=True)
|
|
self.speed = 0.04
|
|
self.direction = 1
|
|
self.destructible = destructible
|
|
self.health = 3 if destructible else 999
|
|
|
|
def take_damage(self):
|
|
if not self.destructible:
|
|
return
|
|
|
|
self.health -= 1
|
|
if self.health == 2:
|
|
self.sprite_name = "enemy_brown"
|
|
elif self.health == 1:
|
|
self.sprite_name = "enemy_orange"
|
|
|
|
def update(self, entities):
|
|
target_gx = self.gx + (self.speed * self.direction)
|
|
|
|
if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5:
|
|
self.direction *= -1
|
|
return
|
|
|
|
for entity in entities:
|
|
if entity != self and entity.is_solid:
|
|
if (
|
|
abs(entity.gx - target_gx) < 0.6
|
|
and abs(entity.gy - self.gy) < 0.6
|
|
):
|
|
self.direction *= -1
|
|
return
|
|
|
|
self.gx = target_gx
|
|
|
|
|
|
class DiagonalEnemy(Enemy):
|
|
|
|
def __init__(self, gx, gy):
|
|
super().__init__(gx, gy, destructible=False)
|
|
self.sprite_name = "enemy_diagonal"
|
|
self.speed_x = 0.02 # Cambiado de 0.035 a 0.02
|
|
self.speed_y = 0.02 # Cambiado de 0.035 a 0.02
|
|
self.dir_x = 1
|
|
self.dir_y = 1
|
|
self.min_y = gy - 6
|
|
self.max_y = gy + 6
|
|
|
|
def update(self, entities):
|
|
target_gx = self.gx + (self.speed_x * self.dir_x)
|
|
target_gy = self.gy + (self.speed_y * self.dir_y)
|
|
|
|
if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5:
|
|
self.dir_x *= -1
|
|
target_gx = self.gx + (self.speed_x * self.dir_x)
|
|
|
|
if target_gy <= self.min_y or target_gy >= self.max_y:
|
|
self.dir_y *= -1
|
|
target_gy = self.gy + (self.speed_y * self.dir_y)
|
|
|
|
for entity in entities:
|
|
if entity != self and entity.is_solid:
|
|
if (
|
|
abs(entity.gx - target_gx) < 0.6
|
|
and abs(entity.gy - target_gy) < 0.6
|
|
):
|
|
self.dir_x *= -1
|
|
self.dir_y *= -1
|
|
return
|
|
|
|
self.gx = target_gx
|
|
self.gy = target_gy
|
|
|
|
|
|
class Player(Entity):
|
|
|
|
def __init__(self, gx, gy):
|
|
super().__init__(gx, gy, "player", "player.png", is_solid=True)
|
|
self.speed = 0.09
|
|
self.facing_x = 0.0
|
|
self.facing_y = -1.0
|
|
self.shoot_cooldown = 0
|
|
|
|
def update(self, keys, entities, bullets):
|
|
dx, dy = 0, 0
|
|
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
|
|
dx -= self.speed
|
|
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
|
|
dx += self.speed
|
|
if keys[pygame.K_UP] or keys[pygame.K_w]:
|
|
dy -= self.speed
|
|
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
|
|
dy += self.speed
|
|
|
|
if dx != 0 or dy != 0:
|
|
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
|
|
|
|
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, WhiteBlock):
|
|
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
|
|
|
|
if self.shoot_cooldown > 0:
|
|
self.shoot_cooldown -= 1
|
|
|
|
if keys[pygame.K_SPACE] and self.shoot_cooldown == 0:
|
|
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)
|
|
)
|
|
SOUND_SHOOT.play()
|
|
self.shoot_cooldown = 15
|
|
|
|
|
|
class Vortex(Entity):
|
|
|
|
def __init__(self, gx, gy):
|
|
super().__init__(gx, gy, "vortex", "vortex.png", is_solid=True)
|
|
self.speed = 0.012
|
|
self.was_blocked = False
|
|
|
|
def update(self, entities):
|
|
target_gy = self.gy - self.speed
|
|
|
|
blocked = False
|
|
for entity in entities:
|
|
if entity != self and entity.is_solid:
|
|
if (
|
|
abs(entity.gx - self.gx) < 0.6
|
|
and abs(entity.gy - target_gy) < 0.6
|
|
):
|
|
blocked = True
|
|
break
|
|
|
|
if blocked and not self.was_blocked:
|
|
SOUND_HIT.play()
|
|
|
|
self.was_blocked = blocked
|
|
|
|
if not blocked and target_gy >= 0:
|
|
self.gy = target_gy
|
|
|
|
|
|
class Game:
|
|
|
|
def __init__(self):
|
|
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
|
|
pygame.display.set_caption("Highway Vector - Edición Isométrica")
|
|
self.clock = pygame.time.Clock()
|
|
self.font = pygame.font.SysFont("monospace", 14, bold=True)
|
|
self.big_font = pygame.font.SysFont("monospace", 28, bold=True)
|
|
self.title_font = pygame.font.SysFont("monospace", 34, bold=True)
|
|
self.sprite_mgr = SpriteManager()
|
|
|
|
self.lives = 4
|
|
self.score = 0
|
|
self.won = False
|
|
self.in_opening = True
|
|
|
|
self.reset_level()
|
|
|
|
def show_opening_screen(self):
|
|
"""Muestra la pantalla explicativa inicial al ejecutar el juego."""
|
|
while self.in_opening:
|
|
self.clock.tick(FPS)
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
pygame.quit()
|
|
sys.exit()
|
|
if event.type == pygame.KEYDOWN:
|
|
if event.key == pygame.K_RETURN:
|
|
self.in_opening = False
|
|
|
|
self.screen.fill((10, 10, 20))
|
|
|
|
# Título principal
|
|
title_surf = self.title_font.render(
|
|
"HIGHWAY VECTOR", True, COLOR_YELLOW
|
|
)
|
|
self.screen.blit(
|
|
title_surf,
|
|
(SCREEN_WIDTH // 2 - title_surf.get_width() // 2, 20),
|
|
)
|
|
|
|
# Lista de elementos a mostrar
|
|
items = [
|
|
(
|
|
"player",
|
|
"Jugador: Flechas / WASD para mover, ESPACIO para disparar.",
|
|
),
|
|
("vortex", "Vortex: Objetivo llevar hasta la Zona 0."),
|
|
(
|
|
"block_white",
|
|
"Bloque Blanco: Se rompe con disparo y se puede empujar.",
|
|
),
|
|
(
|
|
"block_purple",
|
|
"Bloque Morado: No se rompe, solo se empuja con disparos.",
|
|
),
|
|
(
|
|
"enemy_red",
|
|
"Enemigo Rojo: Se mueve de lado a lado. Matable y bloqueable.",
|
|
),
|
|
(
|
|
"enemy_indestructible",
|
|
"Enemigo Blanco: Se mueve de lado a lado. Solo bloqueable.",
|
|
),
|
|
(
|
|
"enemy_diagonal",
|
|
"Enemigo Azul: Se mueve rebotando en diagonal. Solo bloqueable.",
|
|
),
|
|
]
|
|
|
|
start_y = 80
|
|
spacing_y = 65
|
|
|
|
for idx, (sprite_key, desc_text) in enumerate(items):
|
|
y_pos = start_y + idx * spacing_y
|
|
|
|
# Dibujar imagen del elemento
|
|
sprite = self.sprite_mgr.get_sprite(sprite_key)
|
|
img_x = 60 - sprite.get_width() // 2
|
|
img_y = y_pos + 12 - sprite.get_height() // 2
|
|
self.screen.blit(sprite, (img_x, img_y))
|
|
|
|
# Dibujar texto descriptivo
|
|
text_surf = self.font.render(desc_text, True, (230, 230, 230))
|
|
self.screen.blit(text_surf, (110, y_pos + 4))
|
|
|
|
# Línea divisoria discreta entre filas
|
|
if idx < len(items) - 1:
|
|
pygame.draw.line(
|
|
self.screen,
|
|
(30, 30, 50),
|
|
(40, y_pos + spacing_y - 8),
|
|
(SCREEN_WIDTH - 40, y_pos + spacing_y - 8),
|
|
)
|
|
|
|
# Mensaje para comenzar con efecto de parpadeo suave
|
|
blink = (pygame.time.get_ticks() // 400) % 2 == 0
|
|
start_color = (0, 255, 200) if blink else (0, 150, 120)
|
|
start_surf = self.big_font.render(
|
|
"Pulse INTRO para comenzar", True, start_color
|
|
)
|
|
self.screen.blit(
|
|
start_surf,
|
|
(SCREEN_WIDTH // 2 - start_surf.get_width() // 2, 545),
|
|
)
|
|
|
|
pygame.display.flip()
|
|
|
|
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 = []
|
|
|
|
self.spares = []
|
|
for i in range(self.lives - 1):
|
|
spare = Entity(
|
|
3.5,
|
|
GRID_SIZE_Y - 3 + (i * 1.2),
|
|
"player",
|
|
"player.png",
|
|
is_solid=False,
|
|
)
|
|
self.spares.append(spare)
|
|
|
|
for y in range(10, GRID_SIZE_Y - 12):
|
|
if y % 6 == 0:
|
|
lane = (y * 3) % GRID_SIZE_X
|
|
if (y // 6) % 2 == 0:
|
|
self.entities.append(WhiteBlock(lane, y))
|
|
else:
|
|
self.entities.append(PurpleBlock(lane, y))
|
|
|
|
if y % 14 == 0:
|
|
self.entities.append(Enemy(2.0, y, destructible=True))
|
|
|
|
if y % 20 == 0:
|
|
self.entities.append(Enemy(5.0, y, destructible=False))
|
|
|
|
if y % 25 == 0:
|
|
self.entities.append(DiagonalEnemy(3.5, y))
|
|
|
|
def handle_collisions(self):
|
|
for entity in self.entities:
|
|
if isinstance(entity, Enemy):
|
|
player_hit = (
|
|
abs(entity.gx - self.player.gx) < 0.6
|
|
and abs(entity.gy - self.player.gy) < 0.6
|
|
)
|
|
vortex_hit = (
|
|
abs(entity.gx - self.vortex.gx) < 0.7
|
|
and abs(entity.gy - self.vortex.gy) < 0.7
|
|
)
|
|
|
|
if player_hit or vortex_hit:
|
|
SOUND_DESTROY.play()
|
|
return True
|
|
|
|
for entity in self.entities[:]:
|
|
if hasattr(entity, "health") and entity.health <= 0:
|
|
self.entities.remove(entity)
|
|
SOUND_DESTROY.play()
|
|
self.score += 200
|
|
|
|
return False
|
|
|
|
def draw_hud(self):
|
|
zone = max(0, int((self.vortex.gy - 10) / 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):
|
|
if self.in_opening:
|
|
self.show_opening_screen()
|
|
|
|
running = True
|
|
while running:
|
|
self.clock.tick(FPS)
|
|
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
running = False
|
|
|
|
if self.won:
|
|
self.screen.fill((10, 30, 20))
|
|
title = self.big_font.render(
|
|
"¡MISIÓN CUMPLIDA!", True, (100, 255, 100)
|
|
)
|
|
subtitle = self.font.render(
|
|
"El VORTEX ha llegado con éxito a la base.",
|
|
True,
|
|
(220, 255, 220),
|
|
)
|
|
score_msg = self.font.render(
|
|
f"Puntuación Final: {self.score:06d}", True, (255, 215, 0)
|
|
)
|
|
restart_msg = self.font.render(
|
|
"Pulsa R para volver a jugar", True, (200, 200, 200)
|
|
)
|
|
|
|
self.screen.blit(title, (SCREEN_WIDTH // 2 - 170, 200))
|
|
self.screen.blit(subtitle, (SCREEN_WIDTH // 2 - 210, 260))
|
|
self.screen.blit(score_msg, (SCREEN_WIDTH // 2 - 130, 310))
|
|
self.screen.blit(restart_msg, (SCREEN_WIDTH // 2 - 150, 380))
|
|
pygame.display.flip()
|
|
|
|
keys = pygame.key.get_pressed()
|
|
if keys[pygame.K_r]:
|
|
self.lives = 4
|
|
self.score = 0
|
|
self.won = False
|
|
self.reset_level()
|
|
continue
|
|
|
|
if self.lives <= 0:
|
|
self.screen.fill((0, 0, 0))
|
|
msg = self.font.render(
|
|
"GAME OVER - Pulsa R para Reiniciar", 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.won = False
|
|
self.reset_level()
|
|
continue
|
|
|
|
keys = pygame.key.get_pressed()
|
|
self.player.update(keys, self.entities, self.bullets)
|
|
self.vortex.update(self.entities)
|
|
|
|
if self.vortex.gy <= 10:
|
|
self.won = True
|
|
self.score += 5000
|
|
continue
|
|
|
|
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)
|
|
|
|
if self.handle_collisions():
|
|
self.lives -= 1
|
|
if self.lives > 0:
|
|
self.reset_level()
|
|
continue
|
|
|
|
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
|
|
|
|
self.screen.fill(COLOR_BG)
|
|
|
|
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
|
|
|
|
if (
|
|
-100 < draw_x < SCREEN_WIDTH + 100
|
|
and -100 < draw_y < SCREEN_HEIGHT + 100
|
|
):
|
|
pts = [
|
|
(draw_x, draw_y),
|
|
(
|
|
draw_x + TILE_WIDTH // 2,
|
|
draw_y + TILE_HEIGHT // 2,
|
|
),
|
|
(draw_x, draw_y + TILE_HEIGHT),
|
|
(
|
|
draw_x - TILE_WIDTH // 2,
|
|
draw_y + TILE_HEIGHT // 2,
|
|
),
|
|
]
|
|
pygame.draw.polygon(self.screen, COLOR_ROAD, pts)
|
|
pygame.draw.polygon(self.screen, COLOR_GRID, pts, 1)
|
|
|
|
all_drawables = self.entities + self.bullets + self.spares
|
|
sorted_entities = sorted(all_drawables, key=lambda e: e.gy + e.gx)
|
|
for entity in sorted_entities:
|
|
entity.draw(
|
|
self.screen, camera_offset_x, camera_offset_y, self.sprite_mgr
|
|
)
|
|
|
|
self.draw_hud()
|
|
pygame.display.flip()
|
|
|
|
pygame.quit()
|
|
sys.exit()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
Game().run()
|