Tower defense

 avatar
unknown
python
a month ago
20 kB
12
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()
Leave a Comment