Version final del juego y documentacion
This commit is contained in:
parent
717a7f5e80
commit
2ebc0052ae
102
README.md
102
README.md
@ -0,0 +1,102 @@
|
||||
# TANK BATTLEZONE
|
||||
|
||||
Un juego de combate de tanques en 3D retro con gráficos vectoriales al estilo de la mítica máquina arcade *BattleZone* (1980), desarrollado en Python y Pygame.
|
||||
|
||||
---
|
||||
|
||||
## 📖 Descripción
|
||||
|
||||
**Tank BattleZone** recrea la experiencia arcade de combate vectorial tridimensional en un entorno acotado.
|
||||
|
||||
Pilotas un tanque táctico en un mapa plagado de pirámides obstáculo, enfrentándote a hordas progresivas de tanques enemigos con distintas habilidades y buscando recargar tus escudos destruyendo OVNIs y recogiendo potenciadores.
|
||||
|
||||
Todos los gráficos se renderizan mediante proyección matemática 3D a 2D en líneas vectoriales puras, y el audio (disparos, explosiones, motor y escudos) se sintetiza dinámicamente mediante software sin requerir archivos de audio externos.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Características Principales
|
||||
|
||||
* 📐 **Renderizado Vectorial 3D Reales:** Proyección de perspectiva 3D personalizada mediante ecuaciones de álgebra vectorial.
|
||||
* 🤖 **Inteligencia Artificial Enemiga:** Varios tipos de tanques enemigos con comportamiento de acecho, orientación de torreta independiente y patrulla.
|
||||
* 🛸 **Eventos Dinámicos:** Aparición periódica de OVNIs cruzadores que otorgan recompensas críticas.
|
||||
* 🔊 **Síntesis de Audio PCM:** Efectos de sonido retro sintetizados por software en tiempo real (procedimentales).
|
||||
* 🕹️ **Pantalla Inicial Descriptiva:** Menú de inicio interactivo con previsualización gráfica de todos los elementos vectoriales del juego.
|
||||
* 🐧 **Soporte Nativo para Linux:** Diseñado y optimizado para ejecutarse en distribuciones basadas en Debian.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Requisitos
|
||||
|
||||
### Requisitos de Software
|
||||
* **Python:** 3.8 o superior.
|
||||
* **Pygame:** 2.0.0 o superior.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Instalación y Ejecución
|
||||
|
||||
Sigue estos pasos para instalar las dependencias e iniciar el juego en tu sistema Debian:
|
||||
|
||||
1. Clonar o descargar el repositorio
|
||||
|
||||
```bash
|
||||
git clone https://gitea.soloconlinux.org.es/luisgulo/Tank_BattleZone.git
|
||||
cd Tank_BattleZone
|
||||
```
|
||||
|
||||
2. Instalar dependencias en Debian
|
||||
|
||||
Asegúrate de tener Python 3 y pip instalados, e instala la librería pygame:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt -y install python3 python3-pip python3-pygame
|
||||
```
|
||||
|
||||
(Opcional: Si prefieres usar un entorno virtual)
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install pygame
|
||||
```
|
||||
|
||||
3. Ejecutar el juego
|
||||
|
||||
Lanza el script principal del juego desde tu terminal:
|
||||
|
||||
```bash
|
||||
python3 tank_battlezone.py
|
||||
```
|
||||
|
||||
## 🕹️ Cómo Jugar
|
||||
|
||||
⌨️ Controles
|
||||
|
||||
| Acción | Tecla / Control |
|
||||
| --- | --- |
|
||||
|Avanzar / Retroceder | Flecha Arriba / W \| Flecha Abajo | / S |
|
||||
|Girar Tanque | Flecha Izquierda / A | Flecha Derecha / D|
|
||||
|Disparar Cañón|Barra Espaciadora|
|
||||
|Comenzar / Reiniciar|Intro (Menú) / R (Tras Game Over)|
|
||||
|
||||
🎯 Elementos del Juego
|
||||
|Elemento | Descripción |Puntos / Efecto|
|
||||
| --- | --- | --- |
|
||||
|🟩 Pirámide verde|Obstáculo infranqueable. Bloquea el paso y los disparos.|—|
|
||||
|🔴 Tanque Rojo|Enemigo básico. Requiere 1 impacto.|+1,000 Pts|
|
||||
|🔵 Tanque Azul|Enemigo intermedio. Requiere 2 impactos.|+2,500 Pts|
|
||||
|🟣 Tanque Púrpura|Enemigo pesado. Requiere 3 impactos. Puede soltar un potenciador.|+5,000 Pts|
|
||||
|🟡 Diamante Amarillo|Potenciador de escudo flotante.|+1 Escudo / +500 Pts|
|
||||
|🛸 OVNI Amarillo|Objeto volador no identificado cruzador.|Recarga ESCUDO MÁXIMO / +3,000 Pts|
|
||||
|
||||
📁 Estructura del Proyecto
|
||||
|
||||
```
|
||||
Tank_BattleZone/
|
||||
├── battlezone.py # Código fuente principal del juego (Pygame)
|
||||
└── README.md # Documentación del proyecto
|
||||
```
|
||||
|
||||
|
||||
|
||||
973
tank_battlezone.py
Executable file
973
tank_battlezone.py
Executable file
@ -0,0 +1,973 @@
|
||||
#!/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()
|
||||
Loading…
x
Reference in New Issue
Block a user