Tower defense
unknown
python
10 months ago
20 kB
14
Indexable
import pygame
import math
import sys
import numpy as np
import time
pygame.init()
# --- Konfiguracje okna i podstawowe parametry ---
WIDTH, HEIGHT = 900, 600
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tower Defense - Gobliny, Bossy, Wieże i Pociski")
FPS = 60
CLOCK = pygame.time.Clock()
# --- Kolory ---
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 128, 255)
YELLOW = (255, 255, 0)
GRAY = (100, 100, 100)
BROWN = (139, 69, 19)
PINK = (255, 192, 203)
DARK_RED= (150, 0, 0)
ORANGE = (255, 140, 0)
PURPLE = (160, 32, 240)
SILVER = (192,192,192)
# --- Parametry gry ---
ENEMY_SPEED = 1.0 # prędkość poruszania się wrogów
BASE_ENEMY_HEALTH = 50 # podstawowe HP zwykłego wroga (wzrośnie wraz z falą)
BOSS_HEALTH_MULT = 5 # wielokrotność zdrowia wroga u bossa
TOWER_BASE_RANGE = 80 # zasięg wieży na poziomie 1
TOWER_BASE_DAMAGE = 20 # obrażenia wieży na poziomie 1
TOWER_COOLDOWN = 50 # co ile klatek strzela (mniejsza liczba = szybszy strzał)
TOWER_COST = 50 # koszt zakupu wieży
UPGRADE_COST = 75 # koszt ulepszenia wieży na kolejny poziom
TOWER_MAX_LEVEL = 3 # maksymalny poziom wieży
PLAYER_START_MONEY = 200 # startowe złoto
PLAYER_LIVES = 3 # ilość żyć gracza (zmniejszone do 3)
WAVE_SIZE = 10 # ilu wrogów w standardowej fali (bez bossa)
TOTAL_WAVES = 9 # ile fal ma się pojawić (co 3. fala jest z bossem)
FIREBOMB_COST = 50 # koszt użycia umiejętności "ognista bomba"
FIREBOMB_DAMAGE = 100 # obrażenia ognistej bomby
FIREBOMB_PER_WAVE = 1 # ile razy na falę można użyć bomby
BOMB_ANIMATION_TIME = 20 # ile klatek trwa animacja "flash" przy bombie
PROJECTILE_SPEED = 6.0 # prędkość pocisków w pikselach na klatkę
# --- Generator dźwięków "z palca" ---
def create_beep_sound(frequency=440, duration=0.1, volume=0.5):
"""
Tworzy prosty dźwięk sinusoidalny o zadanej częstotliwości i czasie trwania.
Zwraca obiekt pygame.mixer.Sound.
"""
sample_rate = 44100
n_samples = int(sample_rate * duration)
t = np.linspace(0, duration, n_samples, False)
wave = 32767 * np.sin(2 * math.pi * frequency * t)
wave = wave.astype(np.int16)
sound = pygame.mixer.Sound(buffer=wave.tobytes())
sound.set_volume(volume)
return sound
# Różne dźwięki strzału dla różnych poziomów wieży
shot_sounds = [
create_beep_sound(frequency=600, duration=0.05, volume=0.3), # dla poziomu 1
create_beep_sound(frequency=700, duration=0.05, volume=0.4), # dla poziomu 2
create_beep_sound(frequency=800, duration=0.05, volume=0.5) # dla poziomu 3
]
# Inne dźwięki
place_tower_sound = create_beep_sound(frequency=220, duration=0.1, volume=0.4)
upgrade_sound = create_beep_sound(frequency=350, duration=0.1, volume=0.5)
# Specjalny dźwięk bomby (nieco dłuższy i bardziej basowy)
bomb_sound = create_beep_sound(frequency=70, duration=0.4, volume=0.7)
# -- ŚCIEŻKA w grze: wrogowie przechodzą przez serię punktów --
PATH_POINTS = [
(50, 300),
(200, 300),
(200, 200),
(400, 200),
(400, 400),
(600, 400),
(600, 300),
(800, 300),
(850, 320)
]
# =================================================
# === Klasa Wroga (zwykłego i bossa) z animacją ===
# =================================================
class Enemy:
def __init__(self, path_points, wave_num, is_boss=False):
self.path_points = path_points
self.current_point = 0
self.x, self.y = path_points[0]
base_health = BASE_ENEMY_HEALTH + 10*(wave_num - 1) # z każdą falą trudniejsi
if is_boss:
self.health = base_health * BOSS_HEALTH_MULT
else:
self.health = base_health
self.is_boss = is_boss
self.is_alive = True
def update(self):
"""
Przesuń wroga w stronę kolejnego punktu ścieżki.
"""
if self.current_point < len(self.path_points) - 1:
tx, ty = self.path_points[self.current_point + 1]
dx, dy = tx - self.x, ty - self.y
dist = math.hypot(dx, dy)
if dist > 0:
move_x = ENEMY_SPEED * dx / dist
move_y = ENEMY_SPEED * dy / dist
self.x += move_x
self.y += move_y
if math.hypot(tx - self.x, ty - self.y) < 1:
self.current_point += 1
def draw(self, surface):
"""
Rysujemy "goblina" lub "bossa".
"""
if self.is_boss:
color = DARK_RED
size_mod = 1.5
else:
color = GREEN
size_mod = 1.0
# Głowa (koło)
head_radius = int(7 * size_mod)
pygame.draw.circle(surface, color, (int(self.x), int(self.y) - 10), head_radius)
# Tułów (prostokąt)
body_width = int(10 * size_mod)
body_height = int(14 * size_mod)
body_rect = pygame.Rect(int(self.x - body_width/2), int(self.y) - 10, body_width, body_height)
pygame.draw.rect(surface, color, body_rect)
# Ręce (linie)
arm_length = int(8 * size_mod)
pygame.draw.line(surface, color,
(self.x - arm_length, self.y - 5),
(self.x + arm_length, self.y - 5), 2)
# Nogi (linie)
leg_length = int(10 * size_mod)
pygame.draw.line(surface, color,
(self.x - 3, self.y + body_height - 10),
(self.x - 3, self.y + body_height - 10 + leg_length), 2)
pygame.draw.line(surface, color,
(self.x + 3, self.y + body_height - 10),
(self.x + 3, self.y + body_height - 10 + leg_length), 2)
# Pasek zdrowia (nad głową)
max_width = 30
# Poniższe obliczenie paska HP można dostosować (tu - jeśli boss, to bierzemy max = base * BOSS_HEALTH_MULT)
if self.is_boss:
max_hp = (BASE_ENEMY_HEALTH + 10*(9 - 1)) * BOSS_HEALTH_MULT
else:
max_hp = BASE_ENEMY_HEALTH + 10*(9 - 1)
hp_percent = max(0, self.health) / max_hp
bar_width = int(max_width * hp_percent)
bar_height = 4
bar_x = int(self.x - max_width/2)
bar_y = int(self.y) - 25
pygame.draw.rect(surface, RED, (bar_x, bar_y, max_width, bar_height))
pygame.draw.rect(surface, GREEN, (bar_x, bar_y, bar_width, bar_height))
def take_damage(self, dmg):
self.health -= dmg
if self.health <= 0:
self.is_alive = False
# =========================================
# === Klasa Pocisku (Projectile) wieży ===
# =========================================
class Projectile:
def __init__(self, x, y, target_enemy, dmg, tower_level):
self.x = x
self.y = y
self.target = target_enemy
self.damage = dmg
self.alive = True
self.level = tower_level
def update(self):
"""
Pocisk leci w stronę wroga z ustaloną prędkością.
Jeśli trafi, zada obrażenia i znika.
Jeśli wróg zginie po drodze albo dotrze do końca, pocisk też się usuwa.
"""
if not self.target.is_alive:
# Wróg zginął zanim pocisk doleciał
self.alive = False
return
dx = self.target.x - self.x
dy = self.target.y - self.y
dist = math.hypot(dx, dy)
if dist < PROJECTILE_SPEED:
# Trafienie
self.target.take_damage(self.damage)
self.alive = False
else:
# Ruch w stronę wroga
self.x += (dx / dist) * PROJECTILE_SPEED
self.y += (dy / dist) * PROJECTILE_SPEED
def draw(self, surface):
"""
Rysujemy pocisk inaczej w zależności od poziomu wieży (np. kolorem).
"""
if self.level == 1:
color = SILVER
radius = 4
elif self.level == 2:
color = BLUE
radius = 5
else:
color = ORANGE
radius = 6
pygame.draw.circle(surface, color, (int(self.x), int(self.y)), radius)
# ======================
# === Klasa Wieży ===
# ======================
class Tower:
def __init__(self, x, y, level=1):
self.x = x
self.y = y
self.level = level
# Parametry
self._calc_stats() # oblicza: damage, range, cooldown
self.counter = 0 # licznik do cooldownu
def _calc_stats(self):
"""Ustawia statystyki wieży zależnie od poziomu."""
self.damage = TOWER_BASE_DAMAGE * self.level
self.range = TOWER_BASE_RANGE + 15*(self.level - 1)
self.cooldown = TOWER_COOLDOWN - 5*(self.level - 1)
if self.cooldown < 15:
self.cooldown = 15
def upgrade(self):
"""Ulepszenie wieży na kolejny poziom, jeśli to możliwe."""
if self.level < TOWER_MAX_LEVEL:
self.level += 1
self._calc_stats()
def update(self, enemies, projectiles):
"""Sprawdza, czy może strzelić pociskiem w najbliższego wroga w zasięgu."""
if self.counter > 0:
self.counter -= 1
return
# Szukamy dowolnego żywego wroga w zasięgu (można też wyszukać najbliższego itd.)
for enemy in enemies:
if enemy.is_alive:
dist = math.hypot(self.x - enemy.x, self.y - enemy.y)
if dist <= self.range:
# Strzelamy pociskiem
# Różny dźwięk zależnie od poziomu
shot_sounds[self.level - 1].play()
projectile = Projectile(self.x, self.y, enemy, self.damage, self.level)
projectiles.append(projectile)
self.counter = self.cooldown
break
def draw(self, surface):
"""
Rysujemy wieżę.
Każdy poziom ma inny kolor/gabaryt "wieżyczki" i lufy.
"""
# Zasięg wieży (cienka linia)
pygame.draw.circle(surface, (0, 0, 255, 50), (int(self.x), int(self.y)), self.range, 1)
# Parametry wyglądu zależne od poziomu
if self.level == 1:
turret_color = GRAY
base_size = 24
turret_radius = 10
barrel_color = GRAY
elif self.level == 2:
turret_color = BLUE
base_size = 26
turret_radius = 12
barrel_color = WHITE
else: # level 3
turret_color = PURPLE
base_size = 28
turret_radius = 14
barrel_color = ORANGE
# Podstawa (kwadrat)
base_rect = pygame.Rect(self.x - base_size//2, self.y - base_size//2, base_size, base_size)
pygame.draw.rect(surface, BROWN, base_rect)
# Górna część (wieżyczka) - okrąg
pygame.draw.circle(surface, turret_color, (int(self.x), int(self.y)), turret_radius)
# Lufa (prostokąt/lufa wychodząca z wieżyczki do góry)
barrel_length = 10 + 4*(self.level)
barrel_width = 4
barrel_rect = pygame.Rect(self.x - barrel_width//2, self.y - turret_radius - barrel_length,
barrel_width, barrel_length)
pygame.draw.rect(surface, barrel_color, barrel_rect)
# ============================
# === Logika główna gry ===
# ============================
def main():
run = True
player_money = PLAYER_START_MONEY
player_lives = PLAYER_LIVES
wave = 1
enemies = []
towers = []
projectiles = [] # lista pocisków
wave_in_progress = False
enemies_to_spawn = 0
# Mechanika bomby: ile nam zostało do użycia w danej fali
bomb_available = FIREBOMB_PER_WAVE
# Animacja bomby (flash)
bomb_flash_frames = 0 # ile klatek jeszcze ma być flash
font = pygame.font.SysFont("Arial", 18)
while run:
CLOCK.tick(FPS)
# ---------------------------------
# Obsługa zdarzeń (eventy)
# ---------------------------------
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
break
elif event.type == pygame.MOUSEBUTTONDOWN:
# LPM - stawianie nowej wieży (jeśli stać gracza)
if event.button == 1:
mx, my = pygame.mouse.get_pos()
if player_money >= TOWER_COST:
# Sprawdź kolizję z innymi wieżami, by się nie nakładały
can_place = True
for t in towers:
if math.hypot(mx - t.x, my - t.y) < 30:
can_place = False
break
if can_place:
towers.append(Tower(mx, my))
player_money -= TOWER_COST
place_tower_sound.play()
# PPM - ulepszenie wieży (jeśli kursor jest na niej i gracza stać)
elif event.button == 3:
mx, my = pygame.mouse.get_pos()
for t in towers:
# Sprawdź, czy kliknięcie jest w środku wieży (mniej więcej).
# Przyjmijmy promień ~ 15 pikseli od środka
if math.hypot(mx - t.x, my - t.y) < 15:
if t.level < TOWER_MAX_LEVEL and player_money >= UPGRADE_COST:
t.upgrade()
player_money -= UPGRADE_COST
upgrade_sound.play()
break
# Mechanika klawiszy
elif event.type == pygame.KEYDOWN:
# Spacja = użycie umiejętności "ognista bomba"
if event.key == pygame.K_SPACE:
if bomb_available > 0 and player_money >= FIREBOMB_COST:
# Zadaj duże obrażenia wszystkim wrogom na ekranie
for e in enemies:
e.take_damage(FIREBOMB_DAMAGE)
bomb_sound.play()
player_money -= FIREBOMB_COST
bomb_available -= 1
bomb_flash_frames = BOMB_ANIMATION_TIME # uruchamiamy flash przez X klatek
# ---------------------------------
# Logika fal wrogów
# ---------------------------------
if not wave_in_progress:
if wave <= TOTAL_WAVES:
wave_in_progress = True
enemies_to_spawn = WAVE_SIZE
bomb_available = FIREBOMB_PER_WAVE # reset bomby na nową falę
else:
# Wygrana
draw_text = font.render("Wygrana! Pokonano wszystkie fale!", True, GREEN)
WIN.blit(draw_text, (WIDTH//2 - draw_text.get_width()//2, HEIGHT//2))
pygame.display.update()
continue # czekamy, aż gracz zamknie okno
if wave_in_progress:
# Spawn wrogów w pewnym tempie
# Co np. 30 ticków dorzucamy jednego wroga (jeśli jeszcze mamy do spawnowania)
if enemies_to_spawn > 0 and pygame.time.get_ticks() % 30 == 0:
# Co 3 falę wrzucamy bossa (ale tylko jednego - jako pierwszego wroga)
if wave % 3 == 0 and enemies_to_spawn == WAVE_SIZE:
enemies.append(Enemy(PATH_POINTS, wave, is_boss=True))
enemies_to_spawn -= 1
else:
enemies.append(Enemy(PATH_POINTS, wave, is_boss=False))
enemies_to_spawn -= 1
# ---------------------------------
# Aktualizacja wrogów
# ---------------------------------
for e in enemies:
if e.is_alive:
e.update()
# Jeśli dotarł do końca ścieżki - zabiera nam życie
if e.current_point == len(e.path_points) - 1:
if math.hypot(e.x - PATH_POINTS[-1][0], e.y - PATH_POINTS[-1][1]) < 15:
player_lives -= 1
e.is_alive = False
# Usuwamy martwych wrogów i nagradzamy goldem
alive_enemies = []
for e in enemies:
if e.is_alive:
alive_enemies.append(e)
else:
# Jeśli wróg zginął (a nie doszedł do końca), gracz dostaje złoto
if e.current_point < len(e.path_points) - 1:
# boss daje bonus
bonus = 10 + (10 if e.is_boss else 0)
player_money += bonus
enemies = alive_enemies
# Czy fala się skończyła?
if wave_in_progress:
if len(enemies) == 0 and enemies_to_spawn == 0:
wave += 1
wave_in_progress = False
# ---------------------------------
# Aktualizacja wież i pocisków
# ---------------------------------
# 1) Wieże -> strzelają (dodają pociski do listy)
for t in towers:
t.update(enemies, projectiles)
# 2) Pociski -> poruszają się i sprawdzają kolizje
alive_projectiles = []
for p in projectiles:
if p.alive:
p.update()
if p.alive:
alive_projectiles.append(p)
projectiles = alive_projectiles
# ---------------------------------
# Sprawdzamy przegraną
# ---------------------------------
if player_lives <= 0:
draw_text = font.render("Przegrana! Gobliny przedarły się do końca...", True, RED)
WIN.blit(draw_text, (WIDTH//2 - draw_text.get_width()//2, HEIGHT//2))
pygame.display.update()
continue
# ---------------------------------
# Rysowanie
# ---------------------------------
WIN.fill(GRAY)
# Ścieżka
draw_path(WIN, PATH_POINTS)
# Wrogowie
for e in enemies:
e.draw(WIN)
# Pociski
for p in projectiles:
p.draw(WIN)
# Wieże
for t in towers:
t.draw(WIN)
# Pasek informacji
info_text = f"Fala: {wave}/{TOTAL_WAVES} | Złoto: {player_money} | Życia: {player_lives} | Bomba (spacja): {bomb_available}/{FIREBOMB_PER_WAVE}"
draw_text = font.render(info_text, True, WHITE)
WIN.blit(draw_text, (10, 10))
# Instrukcje
instructions = "LPM - postaw wieżę (50 zł) | PPM - ulepsz wieżę (75 zł, max 3) | Spacja - bomba (50 zł, 1x/falę)"
inst_text = font.render(instructions, True, WHITE)
WIN.blit(inst_text, (10, 35))
# Animacja flash (bomba)
if bomb_flash_frames > 0:
bomb_flash_frames -= 1
# Nakładamy półprzezroczysty czerwony prostokąt na cały ekran
s = pygame.Surface((WIDTH, HEIGHT))
s.set_alpha(100) # przezroczystość
s.fill((255, 0, 0))
WIN.blit(s, (0, 0))
pygame.display.update()
pygame.quit()
sys.exit()
def draw_path(surface, points):
"""
Rysuje linię łączącą kolejne punkty ścieżki,
aby było widać, którędy wrogowie się poruszają.
"""
if len(points) < 2:
return
pygame.draw.lines(surface, YELLOW, False, points, 6)
if __name__ == "__main__":
main()
Editor is loading...
Leave a Comment