Tank_BattleZone/tank_battlezone.py

974 lines
35 KiB
Python
Executable File

#!/usr/bin/env python3
import math
import random
import sys
import array
import pygame
# Inicialización de Pygame y mezclador de audio
pygame.init()
SAMPLE_RATE = 22050
pygame.mixer.init(frequency=SAMPLE_RATE, size=-16, channels=1, buffer=512)
# Configuración de pantalla
WIDTH, HEIGHT = 800, 600
FOV = 320 # Distancia focal
SCREEN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tank BattleZone")
CLOCK = pygame.time.Clock()
FPS = 60
# Configuración de cámara y visor
CAMERA_HEIGHT = 1.2
HORIZON_Y = int(HEIGHT * 0.58)
# Colores Retro Vectoriales
COLOR_BG = (10, 15, 10)
COLOR_GREEN = (0, 255, 66)
COLOR_DARK_GREEN = (0, 80, 20)
COLOR_RED = (255, 40, 40)
COLOR_BLUE = (0, 150, 255)
COLOR_PURPLE = (180, 0, 255)
COLOR_YELLOW = (255, 230, 0)
COLOR_WHITE = (255, 255, 255)
# --- GENERADOR SINTÉTICO DE EFECTOS DE SONIDO ---
def create_sound_sample(generator_func, duration):
"""Crea un objeto pygame.mixer.Sound generando muestras de audio PCM."""
num_samples = int(SAMPLE_RATE * duration)
buf = array.array("h")
for i in range(num_samples):
t = i / SAMPLE_RATE
val = generator_func(t, i, num_samples)
val = max(-32768, min(32767, int(val)))
buf.append(val)
return pygame.mixer.Sound(buffer=buf)
# 1. Sonido de disparo (Frecuencia descendente)
def gen_shoot(t, i, total):
freq = 600 * (1.0 - t / 0.15)
env = 1.0 - (t / 0.15)
return math.sin(2 * math.pi * freq * t) * 20000 * max(0, env)
SOUND_SHOOT = create_sound_sample(gen_shoot, 0.15)
# 2. Sonido de impacto enemigo (Explosión / Ruido blanco con caída)
def gen_explosion(t, i, total):
env = (1.0 - t / 0.4) ** 2
noise = random.uniform(-1.0, 1.0)
return noise * 28000 * max(0, env)
SOUND_EXPLOSION = create_sound_sample(gen_explosion, 0.4)
# 3. Sonido de impacto en nuestro tanque (Golpe seco metálico)
def gen_hit_player(t, i, total):
freq = 90 * math.exp(-t * 15)
env = math.exp(-t * 12)
noise = random.uniform(-0.3, 0.3)
return (math.sin(2 * math.pi * freq * t) + noise) * 30000 * max(0, env)
SOUND_HIT_PLAYER = create_sound_sample(gen_hit_player, 0.35)
# 4. Sonido de rebote u obstáculo (Clink metálico agudo)
def gen_clink(t, i, total):
freq = 1800
env = math.exp(-t * 35)
return math.sin(2 * math.pi * freq * t) * 18000 * max(0, env)
SOUND_CLINK = create_sound_sample(gen_clink, 0.08)
# 5. Sonido de recarga de escudo (Tono ascendente armónico)
def gen_powerup(t, i, total):
freq = 400 + (t / 0.25) * 800
env = math.sin(math.pi * (t / 0.25))
return math.sin(2 * math.pi * freq * t) * 22000 * max(0, env)
SOUND_POWERUP = create_sound_sample(gen_powerup, 0.25)
# 6. Sonido espaciado para movimiento y giro
def gen_tread_click(t, i, total):
period = 0.12 # Intervalo de tiempo entre pulsos
phase = t % period
if phase < 0.015:
freq = 120
env = math.sin(math.pi * (phase / 0.015))
noise = random.uniform(-0.3, 0.3)
return (math.sin(2 * math.pi * freq * phase) + noise) * 8000 * env
return 0
SOUND_MOVEMENT = create_sound_sample(gen_tread_click, 0.24)
# Canal de audio para el movimiento/giro
CHANNEL_MOVEMENT = pygame.mixer.Channel(0)
def project_3d(x, y, z, player_x, player_y, player_angle):
"""Proyecta un punto 3D del mundo a coordenadas 2D relativas a la visión del jugador."""
dx = x - player_x
dy = y - player_y
cos_a = math.cos(player_angle)
sin_a = math.sin(player_angle)
rx = dx * cos_a - dy * sin_a
ry = dx * sin_a + dy * cos_a
if ry <= 0.2:
return None
screen_x = int(WIDTH / 2 + (rx * FOV) / ry)
screen_y = int(HORIZON_Y - ((z - CAMERA_HEIGHT) * FOV) / ry)
return (screen_x, screen_y)
def draw_vector_shape(surface, points_3d, color, p_x, p_y, p_angle, closed=True):
"""Dibuja una figura 3D compuesta por segmentos vectoriales."""
pts_2d = []
for pt in points_3d:
p2d = project_3d(pt[0], pt[1], pt[2], p_x, p_y, p_angle)
if p2d is None:
return
pts_2d.append(p2d)
if len(pts_2d) > 1:
if closed:
pygame.draw.polygon(surface, color, pts_2d, 1)
else:
pygame.draw.lines(surface, color, False, pts_2d, 1)
# --- FUNCIONES AUXILIARES PARA DIBUJAR ICONOS 2D/3D EN PANTALLA DE INICIO ---
def draw_pyramid_icon(surface, cx, cy):
size, height = 18, 22
apex = (cx, cy - height)
base = [
(cx - size, cy + 8),
(cx + size, cy + 8),
(cx + size - 6, cy - 4),
(cx - size + 6, cy - 4)
]
pygame.draw.polygon(surface, COLOR_DARK_GREEN, base, 1)
for b in base:
pygame.draw.line(surface, COLOR_GREEN, b, apex, 1)
def draw_tank_icon(surface, cx, cy, color):
# Oruga izquierda / derecha
pygame.draw.rect(surface, color, (cx - 16, cy - 10, 6, 20), 1)
pygame.draw.rect(surface, color, (cx + 10, cy - 10, 6, 20), 1)
# Cuerpo central
pygame.draw.rect(surface, color, (cx - 10, cy - 8, 20, 16), 1)
# Torreta
pygame.draw.rect(surface, color, (cx - 6, cy - 5, 12, 10), 1)
# Cañón
pygame.draw.line(surface, color, (cx, cy - 5), (cx, cy - 18), 2)
def draw_diamond_icon(surface, cx, cy):
size = 12
pts = [(cx, cy - size), (cx + size, cy), (cx, cy + size), (cx - size, cy)]
pygame.draw.polygon(surface, COLOR_YELLOW, pts, 1)
pygame.draw.line(surface, COLOR_YELLOW, (cx, cy - size), (cx, cy + size), 1)
pygame.draw.line(surface, COLOR_YELLOW, (cx - size, cy), (cx + size, cy), 1)
def draw_ufo_icon(surface, cx, cy):
# Anillo exterior
pygame.draw.ellipse(surface, COLOR_YELLOW, (cx - 18, cy - 6, 36, 12), 1)
# Cúpula superior
pygame.draw.arc(surface, COLOR_YELLOW, (cx - 10, cy - 12, 20, 12), 0, math.pi, 1)
# Cúpula inferior
pygame.draw.arc(surface, COLOR_YELLOW, (cx - 8, cy - 2, 16, 10), math.pi, 2 * math.pi, 1)
class Pyramid:
def __init__(self, x, y, size=3.0, height=4.0):
self.x = x
self.y = y
self.size = size
self.height = height
self.radius = size / 1.8
def draw(self, surface, p_x, p_y, p_angle):
s = self.size / 2
h = self.height
apex = (self.x, self.y, h)
base = [
(self.x - s, self.y - s, 0),
(self.x + s, self.y - s, 0),
(self.x + s, self.y + s, 0),
(self.x - s, self.y + s, 0),
]
draw_vector_shape(surface, base, COLOR_DARK_GREEN, p_x, p_y, p_angle, True)
for b in base:
draw_vector_shape(
surface, [b, apex], COLOR_GREEN, p_x, p_y, p_angle, False
)
class ShieldPowerup:
def __init__(self, x, y):
self.x = x
self.y = y
self.z = 1.0
self.radius = 1.2
self.alive = True
self.rot = 0.0
def update(self):
self.rot += 0.05
def draw(self, surface, p_x, p_y, p_angle):
# Diamante flotante amarillo giratorio
s = 0.8
h = 0.8
cos_r = math.cos(self.rot)
sin_r = math.sin(self.rot)
local_pts = [
(-s, 0, 0), (0, -s, 0), (s, 0, 0), (0, s, 0)
]
base_3d = []
for lx, ly, lz in local_pts:
rx = lx * cos_r - ly * sin_r
ry = lx * sin_r + ly * cos_r
base_3d.append((self.x + rx, self.y + ry, self.z))
top = (self.x, self.y, self.z + h)
bottom = (self.x, self.y, self.z - h)
draw_vector_shape(surface, base_3d, COLOR_YELLOW, p_x, p_y, p_angle, True)
for pt in base_3d:
draw_vector_shape(surface, [pt, top], COLOR_YELLOW, p_x, p_y, p_angle, False)
draw_vector_shape(surface, [pt, bottom], COLOR_YELLOW, p_x, p_y, p_angle, False)
class UFO:
def __init__(self, player_x, player_y, player_angle):
self.alive = True
self.radius = 2.0
self.score_value = 3000
# Se engendra a una distancia fija al frente y se desplaza perpendicularmente
distance = 35.0
direction = random.choice([-1, 1]) # -1: de derecha a izquierda, 1: de izquierda a derecha
# Posición central frente a la mirada del jugador
center_x = player_x + math.sin(player_angle) * distance
center_y = player_y + math.cos(player_angle) * distance
# Perpendicular a la dirección de la cámara
perp_angle = player_angle + math.pi / 2
offset = 30.0 * direction
self.x = center_x - math.sin(perp_angle) * offset
self.y = center_y - math.cos(perp_angle) * offset
self.z = 2.5
speed = 0.12
self.vx = math.sin(perp_angle) * offset / abs(offset) * speed
self.vy = math.cos(perp_angle) * offset / abs(offset) * speed
# Contador de vida útil para desaparecer tras cruzar la pantalla
self.lifetime = 500
def update(self):
self.x += self.vx
self.y += self.vy
self.lifetime -= 1
if self.lifetime <= 0:
self.alive = False
def draw(self, surface, p_x, p_y, p_angle):
# Dibuja la silueta de Platillo Volador / OVNI vectorial retro
r = 1.8
h = 0.6
# Anillo exterior
ring_local = [
(r, 0, 0), (r * 0.7, r * 0.7, 0), (0, r, 0), (-r * 0.7, r * 0.7, 0),
(-r, 0, 0), (-r * 0.7, -r * 0.7, 0), (0, -r, 0), (r * 0.7, -r * 0.7, 0)
]
ring_3d = [(self.x + lx, self.y + ly, self.z) for lx, ly, lz in ring_local]
# Cúpula superior e inferior
top = (self.x, self.y, self.z + h)
bottom = (self.x, self.y, self.z - h)
draw_vector_shape(surface, ring_3d, COLOR_YELLOW, p_x, p_y, p_angle, True)
for pt in ring_3d[::2]:
draw_vector_shape(surface, [pt, top], COLOR_YELLOW, p_x, p_y, p_angle, False)
draw_vector_shape(surface, [pt, bottom], COLOR_YELLOW, p_x, p_y, p_angle, False)
class EnemyTank:
def __init__(self, x, y, enemy_type="RED"):
self.x = x
self.y = y
self.enemy_type = enemy_type
# Atributos según el tipo de tanque
if enemy_type == "RED":
self.hp = 1
self.color = COLOR_RED
self.score_value = 1000
elif enemy_type == "BLUE":
self.hp = 2
self.color = COLOR_BLUE
self.score_value = 2500
elif enemy_type == "PURPLE":
self.hp = 3
self.color = COLOR_PURPLE
self.score_value = 5000
self.angle = random.uniform(0, math.pi * 2)
self.turret_angle = self.angle
self.speed = 0.03
self.shoot_cooldown = random.randint(120, 240)
self.alive = True
self.size = 2.5
self.radius = 1.5
def update(self, player_x, player_y, bullets_list, obstacles, enemies, player_radius):
dx = player_x - self.x
dy = player_y - self.y
target_angle = math.atan2(dx, dy)
diff_turret = target_angle - self.turret_angle
diff_turret = (diff_turret + math.pi) % (2 * math.pi) - math.pi
self.turret_angle += max(-0.03, min(0.03, diff_turret))
diff_body = target_angle - self.angle
diff_body = (diff_body + math.pi) % (2 * math.pi) - math.pi
self.angle += max(-0.015, min(0.015, diff_body))
dist = math.hypot(dx, dy)
if dist > 6.0:
next_x = self.x + math.sin(self.angle) * self.speed
next_y = self.y + math.cos(self.angle) * self.speed
# Comprobar colisiones con obstáculos
can_move = True
for obstacle in obstacles:
min_dist = obstacle.radius + self.radius
if math.hypot(next_x - obstacle.x, next_y - obstacle.y) < min_dist:
can_move = False
break
# Comprobar colisiones con otros tanques enemigos
if can_move:
for other in enemies:
if other is not self and other.alive:
min_dist = other.radius + self.radius
if math.hypot(next_x - other.x, next_y - other.y) < min_dist:
can_move = False
break
# Comprobar colisión con el tanque del jugador
if can_move:
min_dist = player_radius + self.radius
if math.hypot(next_x - player_x, next_y - player_y) < min_dist:
can_move = False
if can_move:
self.x = next_x
self.y = next_y
if self.shoot_cooldown > 0:
self.shoot_cooldown -= 1
else:
if dist < 40.0:
bullets_list.append(
Bullet(self.x, self.y, self.turret_angle, is_player=False)
)
SOUND_SHOOT.play()
self.shoot_cooldown = random.randint(180, 300)
def draw(self, surface, p_x, p_y, p_angle):
cos_b = math.cos(self.angle)
sin_b = math.sin(self.angle)
def transform_body(px, py, pz):
rx = px * cos_b - py * sin_b
ry = px * sin_b + py * cos_b
return (self.x + rx, self.y + ry, pz)
cos_t = math.cos(self.turret_angle)
sin_t = math.sin(self.turret_angle)
def transform_turret(px, py, pz):
rx = px * cos_t - py * sin_t
ry = px * sin_t + py * cos_t
return (self.x + rx, self.y + ry, pz)
body_local = [
(-1.2, -1.5, 0), (1.2, -1.5, 0),
(1.2, 1.5, 0), (-1.2, 1.5, 0),
(-1.0, -1.2, 0.8), (1.0, -1.2, 0.8),
(1.0, 1.2, 0.8), (-1.0, 1.2, 0.8)
]
body_3d = [transform_body(*pt) for pt in body_local]
turret_local = [
(-0.6, -0.6, 0.8), (0.6, -0.6, 0.8),
(0.6, 0.6, 0.8), (-0.6, 0.6, 0.8),
(-0.4, -0.4, 1.4), (0.4, -0.4, 1.4),
(0.4, 0.4, 1.4), (-0.4, 0.4, 1.4)
]
turret_3d = [transform_turret(*pt) for pt in turret_local]
cannon_3d = [transform_turret(0, 0.4, 1.1), transform_turret(0, 2.6, 1.1)]
draw_vector_shape(surface, body_3d[:4], self.color, p_x, p_y, p_angle)
draw_vector_shape(surface, body_3d[4:], self.color, p_x, p_y, p_angle)
for i in range(4):
draw_vector_shape(
surface, [body_3d[i], body_3d[i + 4]], self.color, p_x, p_y, p_angle, False
)
draw_vector_shape(surface, turret_3d[4:], self.color, p_x, p_y, p_angle)
for i in range(4):
draw_vector_shape(
surface, [turret_3d[i], turret_3d[i + 4]], self.color, p_x, p_y, p_angle, False
)
draw_vector_shape(surface, cannon_3d, self.color, p_x, p_y, p_angle, False)
class Bullet:
def __init__(self, x, y, angle, is_player=True):
self.x = x
self.y = y
self.z = 0.8
self.angle = angle
self.speed = 1.2
self.max_distance = 80.0
self.traveled = 0.0
self.is_player = is_player
self.alive = True
def update(self):
self.x += math.sin(self.angle) * self.speed
self.y += math.cos(self.angle) * self.speed
self.traveled += self.speed
if self.traveled >= self.max_distance:
self.alive = False
def draw(self, surface, p_x, p_y, p_angle):
length = 1.8
front_x = self.x + math.sin(self.angle) * length
front_y = self.y + math.cos(self.angle) * length
p_back = project_3d(self.x, self.y, self.z, p_x, p_y, p_angle)
p_front = project_3d(front_x, front_y, self.z, p_x, p_y, p_angle)
color = COLOR_WHITE if self.is_player else COLOR_RED
if p_back and p_front:
pygame.draw.line(surface, color, p_back, p_front, 2)
class Game:
def __init__(self):
self.player_x = 0.0
self.player_y = 0.0
self.player_angle = 0.0
self.player_radius = 1.5
self.move_speed = 0.12
self.rot_speed = 0.03
self.shoot_cooldown = 0
self.flash_timer = 0
self.shield = 10 # Escudo / Vidas del jugador
self.score = 0
self.kills = 0 # Contador de tanques destruidos
self.initial_enemies = 2
self.spawn_delay = 0 # Temporizador para controlar la velocidad de reaparición
self.ufo = None # Instancia del OVNI / Platillo volante
self.ufo_spawn_timer = random.randint(600, 1200) # Cada 10-20 segundos
self.bullets = []
self.powerups = []
self.enemies = []
# Obstáculos
self.obstacles = [
Pyramid(-15, 15),
Pyramid(20, 10),
Pyramid(-10, 40),
Pyramid(15, 35),
Pyramid(0, 50),
]
self.font = pygame.font.SysFont("monospace", 15, bold=True)
self.font_title = pygame.font.SysFont("monospace", 28, bold=True)
self.game_over = False
self.in_start_screen = True # Estado para la pantalla de inicio
# Generar enemigos iniciales
for _ in range(self.initial_enemies):
self.spawn_enemy()
def draw_start_screen(self):
"""Muestra las instrucciones del juego con iconos vectoriales en la pantalla inicial."""
SCREEN.fill(COLOR_BG)
title_surf = self.font_title.render("TANK BATTLEZONE", True, COLOR_GREEN)
SCREEN.blit(title_surf, (WIDTH // 2 - title_surf.get_width() // 2, 25))
y_offset = 70
lines_ctrl = [
("CONTROLES:", COLOR_YELLOW),
(" [ARRIBA/W][ABAJO/S] : Avanzar / Retroceder", COLOR_WHITE),
(" [IZQ/A][DER/D] : Girar tanque", COLOR_WHITE),
(" [ESPACIO] : Disparar", COLOR_WHITE),
]
for text, color in lines_ctrl:
line_surf = self.font.render(text, True, color)
SCREEN.blit(line_surf, (40, y_offset))
y_offset += 20
y_offset += 10
hdr_surf = self.font.render("ELEMENTOS DEL JUEGO:", True, COLOR_YELLOW)
SCREEN.blit(hdr_surf, (40, y_offset))
y_offset += 28
# Lista de items con sus respectivos dibujadores de iconos
items = [
(draw_pyramid_icon, "PIRAMIDE : Obstaculo infranqueable", COLOR_DARK_GREEN),
(lambda s, cx, cy: draw_tank_icon(s, cx, cy, COLOR_RED), "TANQUE ROJO : 1 impacto (1000 Pts)", COLOR_RED),
(lambda s, cx, cy: draw_tank_icon(s, cx, cy, COLOR_BLUE), "TANQUE AZUL : 2 impactos (2500 Pts)", COLOR_BLUE),
(lambda s, cx, cy: draw_tank_icon(s, cx, cy, COLOR_PURPLE), "TANQUE PURPURA : 3 impactos (5000 Pts) - Suelta escudo", COLOR_PURPLE),
(draw_diamond_icon, "DIAMANTE AMARILLO : Escudo (+1 SHIELD / 500 Pts)", COLOR_YELLOW),
(draw_ufo_icon, "OVNI AMARILLO : Cruzador - ¡Rellena ESCUDO MAX! (3000 Pts)", COLOR_YELLOW),
]
icon_x = 60
text_x = 95
for draw_fn, text, color in items:
draw_fn(SCREEN, icon_x, y_offset + 10)
line_surf = self.font.render(text, True, color)
SCREEN.blit(line_surf, (text_x, y_offset))
y_offset += 38
y_offset += 15
start_surf = self.font.render("PRESIONA INTRO PARA COMENZAR", True, COLOR_GREEN)
SCREEN.blit(start_surf, (WIDTH // 2 - start_surf.get_width() // 2, y_offset))
pygame.display.flip()
def get_next_enemy_type(self):
"""Calcula el tipo de enemigo a generar según la progresión de bajas."""
if self.kills >= 25:
return "PURPLE"
prob_purple = min(0.8, self.kills / 25.0)
prob_blue = min(0.6, self.kills / 15.0)
r = random.random()
if r < prob_purple:
return "PURPLE"
elif r < prob_purple + prob_blue:
return "BLUE"
else:
return "RED"
def spawn_enemy(self):
"""Genera un nuevo enemigo a una distancia razonable del jugador sin solaparse con obstáculos."""
e_type = self.get_next_enemy_type()
for _ in range(50):
spawn_angle = random.uniform(0, math.pi * 2)
spawn_dist = random.uniform(25, 45)
new_x = self.player_x + math.sin(spawn_angle) * spawn_dist
new_y = self.player_y + math.cos(spawn_angle) * spawn_dist
# Verificar que no aparezca dentro de un obstáculo
valid = True
for obstacle in self.obstacles:
if math.hypot(new_x - obstacle.x, new_y - obstacle.y) < obstacle.radius + 2.0:
valid = False
break
if valid:
self.enemies.append(EnemyTank(new_x, new_y, enemy_type=e_type))
break
def shoot(self):
if self.shoot_cooldown <= 0 and not self.game_over and not self.in_start_screen:
start_x = self.player_x + math.sin(self.player_angle) * 1.0
start_y = self.player_y + math.cos(self.player_angle) * 1.0
self.bullets.append(Bullet(start_x, start_y, self.player_angle, is_player=True))
self.shoot_cooldown = 15
self.flash_timer = 3
SOUND_SHOOT.play()
def draw_mountains(self):
mountain_profile = [
0, 15, 30, 10, 0, 25, 45, 15, 0, 10, 20, 5, 0, 35, 15, 0
]
step = WIDTH / (len(mountain_profile) - 1)
angle_offset = (math.degrees(self.player_angle) * 4) % WIDTH
pts = []
for i, h in enumerate(mountain_profile):
x = (i * step - angle_offset) % WIDTH
y = HORIZON_Y - h
pts.append((x, y))
sorted_pts = sorted(pts, key=lambda p: p[0])
for i in range(len(sorted_pts) - 1):
if abs(sorted_pts[i][0] - sorted_pts[i + 1][0]) < step * 2:
pygame.draw.line(
SCREEN, COLOR_DARK_GREEN, sorted_pts[i], sorted_pts[i + 1], 1
)
def draw_radar(self):
radar_cx, radar_cy, radar_r = WIDTH // 2, 45, 35
pygame.draw.circle(SCREEN, COLOR_BG, (radar_cx, radar_cy), radar_r)
pygame.draw.circle(SCREEN, COLOR_GREEN, (radar_cx, radar_cy), radar_r, 1)
fov_rad = math.radians(30)
pygame.draw.line(
SCREEN,
COLOR_DARK_GREEN,
(radar_cx, radar_cy),
(
radar_cx + int(radar_r * math.sin(-fov_rad)),
radar_cy - int(radar_r * math.cos(-fov_rad)),
),
)
pygame.draw.line(
SCREEN,
COLOR_DARK_GREEN,
(radar_cx, radar_cy),
(
radar_cx + int(radar_r * math.sin(fov_rad)),
radar_cy - int(radar_r * math.cos(fov_rad)),
),
)
for enemy in self.enemies:
if not enemy.alive:
continue
dx = enemy.x - self.player_x
dy = enemy.y - self.player_y
cos_a = math.cos(self.player_angle)
sin_a = math.sin(self.player_angle)
rx = dx * cos_a - dy * sin_a
ry = dx * sin_a + dy * cos_a
dist = math.hypot(rx, ry)
if dist < 60.0:
blip_x = radar_cx + int((rx / 60.0) * radar_r)
blip_y = radar_cy - int((ry / 60.0) * radar_r)
if math.hypot(blip_x - radar_cx, blip_y - radar_cy) <= radar_r - 2:
pygame.draw.circle(SCREEN, enemy.color, (blip_x, blip_y), 2)
# Mostrar OVNI en el radar
if self.ufo and self.ufo.alive:
dx = self.ufo.x - self.player_x
dy = self.ufo.y - self.player_y
cos_a = math.cos(self.player_angle)
sin_a = math.sin(self.player_angle)
rx = dx * cos_a - dy * sin_a
ry = dx * sin_a + dy * cos_a
dist = math.hypot(rx, ry)
if dist < 60.0:
blip_x = radar_cx + int((rx / 60.0) * radar_r)
blip_y = radar_cy - int((ry / 60.0) * radar_r)
if math.hypot(blip_x - radar_cx, blip_y - radar_cy) <= radar_r - 2:
pygame.draw.circle(SCREEN, COLOR_YELLOW, (blip_x, blip_y), 3)
def draw_hud(self):
pygame.draw.line(
SCREEN, COLOR_DARK_GREEN, (0, HORIZON_Y), (WIDTH, HORIZON_Y), 1
)
cx, cy = WIDTH // 2, HORIZON_Y
if self.flash_timer > 0:
pygame.draw.circle(SCREEN, COLOR_WHITE, (cx, cy), 12, 1)
self.flash_timer -= 1
pygame.draw.line(SCREEN, COLOR_GREEN, (cx - 15, cy), (cx - 5, cy), 1)
pygame.draw.line(SCREEN, COLOR_GREEN, (cx + 5, cy), (cx + 15, cy), 1)
pygame.draw.line(SCREEN, COLOR_GREEN, (cx, cy - 15), (cx, cy - 5), 1)
pygame.draw.line(SCREEN, COLOR_GREEN, (cx, cy + 5), (cx, cy + 15), 1)
score_surf = self.font.render(f"SCORE: {self.score:06d}", True, COLOR_GREEN)
kills_surf = self.font.render(f"KILLS: {self.kills:03d}", True, COLOR_GREEN)
shield_surf = self.font.render(f"SHIELD: {'I ' * self.shield}", True, COLOR_YELLOW)
SCREEN.blit(score_surf, (20, 20))
SCREEN.blit(kills_surf, (20, 40))
SCREEN.blit(shield_surf, (20, 60))
if self.game_over:
go_surf = self.font_title.render("GAME OVER", True, COLOR_RED)
score_final_surf = self.font.render(f"PUNTUACION FINAL: {self.score:06d}", True, COLOR_WHITE)
kills_final_surf = self.font.render(f"TANQUES DESTRUIDOS: {self.kills:03d}", True, COLOR_WHITE)
restart_surf = self.font.render("PRESIONA R PARA VOLVER A JUGAR", True, COLOR_YELLOW)
SCREEN.blit(go_surf, (WIDTH // 2 - go_surf.get_width() // 2, HEIGHT // 2 - 60))
SCREEN.blit(score_final_surf, (WIDTH // 2 - score_final_surf.get_width() // 2, HEIGHT // 2 - 10))
SCREEN.blit(kills_final_surf, (WIDTH // 2 - kills_final_surf.get_width() // 2, HEIGHT // 2 + 15))
SCREEN.blit(restart_surf, (WIDTH // 2 - restart_surf.get_width() // 2, HEIGHT // 2 + 50))
self.draw_radar()
def reset_game(self):
self.player_x = 0.0
self.player_y = 0.0
self.player_angle = 0.0
self.shield = 10
self.score = 0
self.kills = 0
self.spawn_delay = 0
self.ufo = None
self.ufo_spawn_timer = random.randint(600, 1200)
self.bullets.clear()
self.powerups.clear()
self.enemies.clear()
self.game_over = False
for _ in range(self.initial_enemies):
self.spawn_enemy()
def try_move_player(self, dx, dy):
"""Intenta mover al jugador comprobando colisiones físicas con obstáculos y tanques enemigos."""
next_x = self.player_x + dx
next_y = self.player_y + dy
# Colisión con obstáculos (Pirámides)
for obstacle in self.obstacles:
min_dist = obstacle.radius + self.player_radius
if math.hypot(next_x - obstacle.x, next_y - obstacle.y) < min_dist:
SOUND_CLINK.play()
return
# Colisión con tanques enemigos
for enemy in self.enemies:
if enemy.alive:
min_dist = enemy.radius + self.player_radius
if math.hypot(next_x - enemy.x, next_y - enemy.y) < min_dist:
SOUND_CLINK.play()
return
self.player_x = next_x
self.player_y = next_y
def run(self):
running = True
while running:
CLOCK.tick(FPS)
# Control de la pantalla de inicio
if self.in_start_screen:
self.draw_start_screen()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN:
self.in_start_screen = False
continue
if self.shoot_cooldown > 0:
self.shoot_cooldown -= 1
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
self.shoot()
elif event.key == pygame.K_r and self.game_over:
self.reset_game()
keys = pygame.key.get_pressed()
# Movimiento y Control
if not self.game_over:
moving_fwd_back = keys[pygame.K_UP] or keys[pygame.K_w] or keys[pygame.K_DOWN] or keys[pygame.K_s]
turning = keys[pygame.K_LEFT] or keys[pygame.K_a] or keys[pygame.K_RIGHT] or keys[pygame.K_d]
if keys[pygame.K_UP] or keys[pygame.K_w]:
dx = math.sin(self.player_angle) * self.move_speed
dy = math.cos(self.player_angle) * self.move_speed
self.try_move_player(dx, dy)
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
dx = -math.sin(self.player_angle) * self.move_speed
dy = -math.cos(self.player_angle) * self.move_speed
self.try_move_player(dx, dy)
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.player_angle -= self.rot_speed
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.player_angle += self.rot_speed
if moving_fwd_back or turning:
if not CHANNEL_MOVEMENT.get_busy():
CHANNEL_MOVEMENT.play(SOUND_MOVEMENT, loops=-1)
else:
CHANNEL_MOVEMENT.stop()
else:
CHANNEL_MOVEMENT.stop()
# Gestión de reaparición progresiva de enemigos
if not self.game_over:
max_enemies = min(6, self.initial_enemies + (self.kills // 8))
if len(self.enemies) < max_enemies:
if self.spawn_delay <= 0:
self.spawn_enemy()
self.spawn_delay = 180
else:
self.spawn_delay -= 1
# Temporizador de aparición del OVNI
if self.ufo is None or not self.ufo.alive:
if self.ufo_spawn_timer <= 0:
self.ufo = UFO(self.player_x, self.player_y, self.player_angle)
self.ufo_spawn_timer = random.randint(900, 1800) # 15-30 segundos para la siguiente aparición
else:
self.ufo_spawn_timer -= 1
else:
self.ufo.update()
# Actualización de Powerups
for pw in self.powerups[:]:
pw.update()
if math.hypot(pw.x - self.player_x, pw.y - self.player_y) < pw.radius + 0.5:
if self.shield < 10:
self.shield = min(10, self.shield + 1)
self.score += 500
SOUND_POWERUP.play()
self.powerups.remove(pw)
# Actualización y Colisiones de Proyectiles
for bullet in self.bullets[:]:
bullet.update()
if not bullet.alive:
self.bullets.remove(bullet)
continue
# Colisión con obstáculos (Pirámides)
hit_obstacle = False
for obstacle in self.obstacles:
if math.hypot(bullet.x - obstacle.x, bullet.y - obstacle.y) < obstacle.radius:
bullet.alive = False
hit_obstacle = True
SOUND_CLINK.play()
break
if hit_obstacle:
continue
# Colisión con el OVNI
if bullet.is_player and self.ufo and self.ufo.alive:
if math.hypot(bullet.x - self.ufo.x, bullet.y - self.ufo.y) < self.ufo.radius:
bullet.alive = False
self.ufo.alive = False
self.score += self.ufo.score_value
self.shield = 10 # Recarga el escudo al máximo (10)
SOUND_POWERUP.play()
SOUND_EXPLOSION.play()
continue
# Colisión con el jugador
if not bullet.is_player and not self.game_over:
if math.hypot(bullet.x - self.player_x, bullet.y - self.player_y) < 1.5:
bullet.alive = False
self.shield -= 1
SOUND_HIT_PLAYER.play()
if self.shield <= 0:
self.game_over = True
continue
# Colisión con enemigo
if bullet.is_player:
for enemy in self.enemies:
if (
enemy.alive
and math.hypot(bullet.x - enemy.x, bullet.y - enemy.y)
< enemy.size
):
bullet.alive = False
enemy.hp -= 1
SOUND_CLINK.play()
if enemy.hp <= 0:
enemy.alive = False
self.score += enemy.score_value
self.kills += 1
SOUND_EXPLOSION.play()
if enemy.enemy_type == "PURPLE" and random.random() < 0.5:
self.powerups.append(ShieldPowerup(enemy.x, enemy.y))
self.enemies = [e for e in self.enemies if e.alive]
self.spawn_delay = 180
break
if not self.game_over:
for enemy in self.enemies:
if enemy.alive:
enemy.update(
self.player_x,
self.player_y,
self.bullets,
self.obstacles,
self.enemies,
self.player_radius,
)
SCREEN.fill(COLOR_BG)
self.draw_mountains()
for obstacle in self.obstacles:
obstacle.draw(SCREEN, self.player_x, self.player_y, self.player_angle)
for pw in self.powerups:
pw.draw(SCREEN, self.player_x, self.player_y, self.player_angle)
if self.ufo and self.ufo.alive:
self.ufo.draw(SCREEN, self.player_x, self.player_y, self.player_angle)
for enemy in self.enemies:
if enemy.alive:
enemy.draw(SCREEN, self.player_x, self.player_y, self.player_angle)
for bullet in self.bullets:
bullet.draw(SCREEN, self.player_x, self.player_y, self.player_angle)
self.draw_hud()
pygame.display.flip()
pygame.quit()
sys.exit()
if __name__ == "__main__":
Game().run()