Tower defense
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()
Leave a Comment