Загрузка данных


import pygame
import pygame.gfxdraw
import random
import math
import sys

pygame.init()
pygame.font.init()

WIDTH, HEIGHT = 1180, 700
GRID_COLS, GRID_ROWS = 9, 5
CELL_W, CELL_H = 88, 96
GRID_X, GRID_Y = 270, 140
FPS = 60

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Plants vs. Zombies - Ultimate Edition")
clock = pygame.time.Clock()

f_tiny = pygame.font.SysFont("Verdana", 10, bold=True)
f_small = pygame.font.SysFont("Verdana", 13, bold=True)
f_med = pygame.font.SysFont("Verdana", 18, bold=True)
f_big = pygame.font.SysFont("Verdana", 34, bold=True)

# --- 20 РАСТЕНИЙ: ХАРАКТЕРИСТИКИ И СТОИМОСТЬ В МАГАЗИНЕ ---
PLANTS_DB = {
    "peashooter":   {"cost": 100, "hp": 200, "cd": 100, "price": 0,    "name": "Peashooter",   "desc": "Стреляет горохом по врагам."},
    "sunflower":    {"cost": 50,  "hp": 150, "cd": 100, "price": 0,    "name": "Sunflower",    "desc": "Производит дополнительное солнце."},
    "wallnut":      {"cost": 50,  "hp": 800, "cd": 350, "price": 0,    "name": "Wall-nut",     "desc": "Крепкий орех, задерживает зомби."},
    "cherrybomb":   {"cost": 150, "hp": 500, "cd": 500, "price": 0,    "name": "Cherry Bomb",  "desc": "Взрывает всех в области 3х3."},
    "potatomine":   {"cost": 25,  "hp": 120, "cd": 300, "price": 100,  "name": "Potato Mine",  "desc": "Взрывается при контакте после взвода."},
    "snowpea":      {"cost": 175, "hp": 200, "cd": 120, "price": 150,  "name": "Snow Pea",     "desc": "Стреляет морозным горохом, замедляя врага."},
    "chomper":      {"cost": 150, "hp": 250, "cd": 150, "price": 200,  "name": "Chomper",      "desc": "Съедает зомби целиком, долго жует."},
    "repeater":     {"cost": 200, "hp": 200, "cd": 120, "price": 250,  "name": "Repeater",     "desc": "Выпускает две горошины за раз."},
    "fumeshroom":   {"cost": 75,  "hp": 200, "cd": 120, "price": 250,  "name": "Fume-shroom",  "desc": "Стреляет спорами сквозь щиты и двери."},
    "hypnoshroom":  {"cost": 75,  "hp": 100, "cd": 400, "price": 300,  "name": "Hypno-shroom", "desc": "Зомби разворачивается и воюет за вас."},
    "iceshroom":    {"cost": 75,  "hp": 200, "cd": 500, "price": 350,  "name": "Ice-shroom",   "desc": "Замораживает всех зомби на экране."},
    "doomshroom":   {"cost": 125, "hp": 200, "cd": 600, "price": 400,  "name": "Doom-shroom",  "desc": "Уничтожает все в огромном радиусе."},
    "squash":       {"cost": 50,  "hp": 400, "cd": 300, "price": 300,  "name": "Squash",       "desc": "Раздавливает подошедшего зомби."},
    "jalapeno":     {"cost": 125, "hp": 300, "cd": 450, "price": 350,  "name": "Jalapeno",     "desc": "Сжигает всю линию огненной волной."},
    "spikeweed":    {"cost": 100, "hp": 250, "cd": 120, "price": 300,  "name": "Spikeweed",    "desc": "Колет зомби снизу, не может быть съеден."},
    "torchwood":    {"cost": 175, "hp": 300, "cd": 150, "price": 400,  "name": "Torchwood",    "desc": "Зажигает горошины, удваивая их урон."},
    "tallnut":      {"cost": 125, "hp": 1600,"cd": 450, "price": 450,  "name": "Tall-nut",     "desc": "Огромный орех с двойной прочностью."},
    "cactus":       {"cost": 125, "hp": 200, "cd": 120, "price": 350,  "name": "Cactus",       "desc": "Стреляет шипами, пробивающими врагов."},
    "blover":       {"cost": 100, "hp": 100, "cd": 250, "price": 250,  "name": "Blover",       "desc": "Сдувает туман и отталкивает зомби назад."},
    "starfruit":    {"cost": 125, "hp": 200, "cd": 120, "price": 400,  "name": "Starfruit",    "desc": "Стреляет звездами в 5 разных направлениях."}
}

ZOMBIES_DB = {
    "regular":    {"name": "Zombie",      "hp": 150, "spd": 0.35, "desc": "Обычный рядовой садовый зомби."},
    "conehead":   {"name": "Conehead",    "hp": 350, "spd": 0.35, "desc": "Дорожный конус делает его вдвое крепче."},
    "buckethead": {"name": "Buckethead",  "hp": 750, "spd": 0.35, "desc": "Ведро на голове дает высокую устойчивость."},
    "gargantuar": {"name": "Gargantuar",  "hp": 2000,"spd": 0.22, "desc": "ГИГАНТ! Сокрушает растения столбом."}
}

LEVELS_CONFIG = [
    {"num": 1, "name": "Уровень 1-1", "reward": 100, "waves": [["regular"], ["regular", "regular"], ["conehead"]]},
    {"num": 2, "name": "Уровень 1-2", "reward": 150, "waves": [["regular", "conehead"], ["conehead", "conehead"], ["buckethead", "regular"]]},
    {"num": 3, "name": "Уровень 1-3", "reward": 200, "waves": [["conehead", "buckethead"], ["buckethead", "buckethead"], ["regular", "conehead", "buckethead"]]},
    {"num": 4, "name": "Уровень 1-4", "reward": 250, "waves": [["buckethead", "buckethead"], ["buckethead", "conehead", "conehead"], ["gargantuar"]]},
    {"num": 5, "name": "Уровень 1-5 БОСС", "reward": 500, "waves": [["buckethead", "buckethead"], ["gargantuar", "conehead"], ["gargantuar", "buckethead", "conehead"]]}
]

# --- УТИЛИТЫ СГЛАЖЕННОЙ ОТРИСОВКИ ---
def draw_circle_aa(surf, x, y, r, color):
    pygame.gfxdraw.aacircle(surf, int(x), int(y), int(r), color)
    pygame.gfxdraw.filled_circle(surf, int(x), int(y), int(r), color)

# --- УНИФИЦИРОВАННЫЙ ПРОЦЕДУРНЫЙ РЕНДЕР МОДЕЛЕЙ И ИКОНОК ---
def draw_plant_art(surf, p_type, x, y, size=60, phase=0.0):
    s = pygame.Surface((size, size), pygame.SRCALPHA)
    cx, cy = size // 2, size // 2
    bob = math.sin(phase) * (2 if size > 40 else 0)

    if p_type in ["peashooter", "repeater", "snowpea"]:
        col = (100, 210, 255) if p_type == "snowpea" else (60, 190, 50)
        pygame.draw.ellipse(s, (40, 130, 30), (cx - 15, cy + 12, 30, 10))
        pygame.draw.line(s, (45, 160, 45), (cx, cy + 15), (cx - 4, cy), 4)
        draw_circle_aa(s, cx - 2, cy - 6 + bob, 14, col)
        pygame.draw.rect(s, col, (cx + 6, cy - 12 + bob, 12, 10), border_radius=3)
        draw_circle_aa(s, cx + 18, cy - 7 + bob, 5, (20, 80, 20))
        draw_circle_aa(s, cx + 2, cy - 10 + bob, 3, (255, 255, 255))
        draw_circle_aa(s, cx + 3, cy - 10 + bob, 1, (0, 0, 0))
        if p_type == "repeater":
            pygame.draw.polygon(s, (35, 140, 25), [(cx - 12, cy - 12 + bob), (cx - 24, cy - 18 + bob), (cx - 14, cy - 4 + bob)])
        elif p_type == "snowpea":
            for ang in [-15, 0, 15]:
                draw_circle_aa(s, cx - 14 + ang//2, cy - 10 + ang + bob, 3, (200, 240, 255))

    elif p_type == "sunflower":
        for i in range(8):
            ang = phase * 0.5 + i * (math.pi / 4)
            px = cx + math.cos(ang) * 14
            py = cy - 4 + math.sin(ang) * 14 + bob
            draw_circle_aa(s, px, py, 6, (255, 200, 0))
        draw_circle_aa(s, cx, cy - 4 + bob, 12, (150, 80, 30))
        draw_circle_aa(s, cx - 4, cy - 7 + bob, 2, (0, 0, 0))
        draw_circle_aa(s, cx + 4, cy - 7 + bob, 2, (0, 0, 0))

    elif p_type in ["wallnut", "tallnut"]:
        h = 24 if p_type == "tallnut" else 18
        pygame.draw.ellipse(s, (160, 95, 45), (cx - 14, cy - h + bob, 28, h * 2))
        draw_circle_aa(s, cx - 5, cy - h // 2 + bob, 4, (255, 255, 255))
        draw_circle_aa(s, cx + 5, cy - h // 2 + bob, 4, (255, 255, 255))
        draw_circle_aa(s, cx - 4, cy - h // 2 + bob, 1, (0, 0, 0))
        draw_circle_aa(s, cx + 6, cy - h // 2 + bob, 1, (0, 0, 0))

    elif p_type == "cherrybomb":
        pygame.draw.line(s, (20, 100, 20), (cx - 8, cy + 4), (cx, cy - 12), 3)
        pygame.draw.line(s, (20, 100, 20), (cx + 8, cy), (cx, cy - 12), 3)
        draw_circle_aa(s, cx - 9, cy + 6 + bob, 11, (220, 30, 30))
        draw_circle_aa(s, cx + 9, cy + 2 + bob, 10, (230, 40, 40))

    elif p_type == "potatomine":
        pygame.draw.ellipse(s, (180, 140, 90), (cx - 14, cy + 2, 28, 16))
        draw_circle_aa(s, cx, cy - 4 + bob, 4, (255, 40, 40))

    elif p_type == "chomper":
        pygame.draw.ellipse(s, (130, 40, 160), (cx - 14, cy - 16 + bob, 28, 30))
        pygame.draw.polygon(s, (255, 255, 255), [(cx - 10, cy - 2 + bob), (cx - 5, cy + 4 + bob), (cx, cy - 2 + bob)])
        pygame.draw.polygon(s, (255, 255, 255), [(cx, cy - 2 + bob), (cx + 5, cy + 4 + bob), (cx + 10, cy - 2 + bob)])

    elif p_type in ["fumeshroom", "hypnoshroom", "iceshroom", "doomshroom"]:
        colors = {"fumeshroom": (160, 60, 180), "hypnoshroom": (255, 105, 180), "iceshroom": (120, 220, 255), "doomshroom": (40, 40, 50)}
        col = colors[p_type]
        pygame.draw.rect(s, (210, 210, 210), (cx - 5, cy, 10, 16), border_radius=3)
        pygame.draw.ellipse(s, col, (cx - 16, cy - 16 + bob, 32, 22))

    elif p_type == "squash":
        pygame.draw.ellipse(s, (80, 160, 70), (cx - 15, cy - 14 + bob, 30, 32))
        draw_circle_aa(s, cx - 4, cy - 6 + bob, 2, (0, 0, 0))
        draw_circle_aa(s, cx + 4, cy - 6 + bob, 2, (0, 0, 0))

    elif p_type == "jalapeno":
        pygame.draw.ellipse(s, (220, 20, 20), (cx - 8, cy - 16 + bob, 16, 34))
        pygame.draw.line(s, (30, 120, 30), (cx, cy - 16 + bob), (cx + 4, cy - 24 + bob), 3)

    elif p_type == "spikeweed":
        for i in range(5):
            pygame.draw.polygon(s, (70, 130, 60), [(cx - 18 + i * 8, cy + 14), (cx - 14 + i * 8, cy - 4), (cx - 10 + i * 8, cy + 14)])

    elif p_type == "torchwood":
        pygame.draw.rect(s, (120, 70, 30), (cx - 12, cy - 6, 24, 24), border_radius=4)
        draw_circle_aa(s, cx, cy - 10 + bob, 8, (255, 140, 0))
        draw_circle_aa(s, cx, cy - 10 + bob, 4, (255, 230, 40))

    elif p_type == "cactus":
        pygame.draw.ellipse(s, (40, 140, 50), (cx - 8, cy - 18 + bob, 16, 36))
        pygame.draw.line(s, (255, 255, 255), (cx - 12, cy - 6 + bob), (cx + 12, cy - 6 + bob), 1)

    elif p_type == "blover":
        for i in range(3):
            ang = phase * 4.0 + i * (2 * math.pi / 3)
            bx = cx + math.cos(ang) * 12
            by = cy + math.sin(ang) * 12
            draw_circle_aa(s, bx, by, 7, (80, 220, 120))
        draw_circle_aa(s, cx, cy, 5, (255, 255, 255))

    elif p_type == "starfruit":
        pts = []
        for i in range(10):
            r_cur = 15 if i % 2 == 0 else 7
            ang = i * (math.pi / 5) - math.pi / 2
            pts.append((cx + math.cos(ang) * r_cur, cy + math.sin(ang) * r_cur + bob))
        pygame.draw.polygon(s, (255, 220, 0), pts)

    surf.blit(s, (x, y))

def draw_zombie_art(surf, z_type, x, y, size=70, phase=0.0):
    s = pygame.Surface((size, size + 20), pygame.SRCALPHA)
    cx, cy = size // 2, (size + 20) // 2
    tint = (130, 165, 125)
    bob = math.sin(phase) * 3

    if z_type == "gargantuar":
        pygame.draw.rect(s, (50, 45, 40), (cx - 18, cy - 12, 36, 42), border_radius=5)
        draw_circle_aa(s, cx, cy - 24 + bob, 18, tint)
        draw_circle_aa(s, cx - 6, cy - 26 + bob, 3, (255, 0, 0))
        pygame.draw.rect(s, (90, 50, 20), (cx - 28, cy - 35, 10, 65), border_radius=2)
    else:
        pygame.draw.line(s, (40, 40, 70), (cx - 5, cy + 12), (cx - 7, cy + 28), 5)
        pygame.draw.line(s, (40, 40, 70), (cx + 5, cy + 12), (cx + 7, cy + 28), 5)
        pygame.draw.rect(s, (70, 60, 55), (cx - 10, cy - 8, 20, 22), border_radius=3)
        pygame.draw.line(s, tint, (cx - 4, cy - 2), (cx - 18, cy - 2 + bob), 4)
        draw_circle_aa(s, cx, cy - 18 + bob, 12, tint)
        draw_circle_aa(s, cx - 4, cy - 20 + bob, 2, (255, 0, 0))
        draw_circle_aa(s, cx + 4, cy - 20 + bob, 2, (255, 0, 0))

        if z_type == "conehead":
            pygame.draw.polygon(s, (255, 130, 0), [(cx - 9, cy - 24 + bob), (cx + 9, cy - 24 + bob), (cx, cy - 44 + bob)])
        elif z_type == "buckethead":
            pygame.draw.rect(s, (170, 175, 185), (cx - 9, cy - 34 + bob), (18, 16), border_radius=2)

    surf.blit(s, (x, y))

# --- ИГРОВЫЕ СУЩНОСТИ ---
class Coin:
    def __init__(self, x, y):
        self.x, self.y = x, y
        self.alive = True
        self.timer = 0

    def update(self):
        self.timer += 1
        if self.timer > 600:
            self.alive = False

    def draw(self, surf):
        draw_circle_aa(surf, self.x, self.y, 11, (218, 165, 32))
        draw_circle_aa(surf, self.x, self.y, 8, (255, 215, 0))
        t = f_tiny.render("$", True, (130, 80, 0))
        surf.blit(t, (self.x - t.get_width() // 2, self.y - t.get_height() // 2))

class Sun:
    def __init__(self, x, y, target_y=None):
        self.x, self.y = x, y
        self.target_y = target_y if target_y else y
        self.radius = 20
        self.alive = True
        self.timer = 0

    def update(self):
        if self.y < self.target_y:
            self.y += 2.5
        self.timer += 1
        if self.timer > 650:
            self.alive = False

    def draw(self, surf):
        draw_circle_aa(surf, self.x, self.y, self.radius + 3, (255, 170, 0))
        draw_circle_aa(surf, self.x, self.y, self.radius, (255, 235, 40))

class Projectile:
    def __init__(self, x, y, row, p_type="pea", vx=7.0, vy=0.0):
        self.x, self.y = x, y
        self.row = row
        self.type = p_type
        self.vx = vx
        self.vy = vy
        self.damage = 40 if p_type == "fire_pea" else 22
        self.alive = True

    def update(self):
        self.x += self.vx
        self.y += self.vy
        if self.x > WIDTH or self.y < 0 or self.y > HEIGHT:
            self.alive = False

    def draw(self, surf):
        if self.type == "ice_pea":
            draw_circle_aa(surf, self.x, self.y, 7, (100, 220, 255))
        elif self.type == "fire_pea":
            draw_circle_aa(surf, self.x, self.y, 9, (255, 80, 20))
        elif self.type == "star":
            draw_circle_aa(surf, self.x, self.y, 6, (255, 240, 0))
        elif self.type == "spike":
            pygame.draw.line(surf, (255, 255, 255), (self.x - 6, self.y), (self.x + 6, self.y), 3)
        else:
            draw_circle_aa(surf, self.x, self.y, 7, (60, 210, 50))

class PlantEntity:
    def __init__(self, col, row, p_type):
        self.col, self.row = col, row
        self.type = p_type
        self.hp = PLANTS_DB[p_type]["hp"]
        self.x = GRID_X + col * CELL_W
        self.y = GRID_Y + row * CELL_H
        self.timer = 0
        self.phase = random.uniform(0, math.pi * 2)
        self.alive = True
        self.armed = False if p_type == "potatomine" else True

    def update(self, game):
        self.timer += 1
        self.phase += 0.06

        if self.type == "sunflower" and self.timer >= 420:
            game.suns.append(Sun(self.x + CELL_W // 2, self.y + 20))
            self.timer = 0

        elif self.type in ["peashooter", "snowpea", "repeater", "cactus"]:
            if any(z.row == self.row and z.x > self.x and not z.hypnotized for z in game.zombies):
                if self.type == "repeater" and (self.timer == 85 or self.timer == 100):
                    game.projectiles.append(Projectile(self.x + CELL_W - 10, self.y + 35, self.row, "pea"))
                    if self.timer == 100: self.timer = 0
                elif self.type == "snowpea" and self.timer >= 85:
                    game.projectiles.append(Projectile(self.x + CELL_W - 10, self.y + 35, self.row, "ice_pea"))
                    self.timer = 0
                elif self.type == "peashooter" and self.timer >= 85:
                    game.projectiles.append(Projectile(self.x + CELL_W - 10, self.y + 35, self.row, "pea"))
                    self.timer = 0
                elif self.type == "cactus" and self.timer >= 85:
                    game.projectiles.append(Projectile(self.x + CELL_W - 10, self.y + 35, self.row, "spike"))
                    self.timer = 0

        elif self.type == "starfruit" and self.timer >= 90:
            if game.zombies:
                angles = [0, 72, 144, 216, 288]
                for a in angles:
                    rad = math.radians(a)
                    game.projectiles.append(Projectile(self.x + CELL_W//2, self.y + CELL_H//2, self.row, "star", math.cos(rad)*6, math.sin(rad)*6))
                self.timer = 0

        elif self.type == "potatomine":
            if not self.armed and self.timer >= 280:
                self.armed = True
            if self.armed:
                for z in game.zombies:
                    if z.row == self.row and abs((self.x + CELL_W // 2) - z.x) < 30 and not z.hypnotized:
                        z.take_damage(1800)
                        self.alive = False
                        break

        elif self.type == "chomper":
            for z in game.zombies:
                if z.row == self.row and 0 < (z.x - self.x) < CELL_W * 1.3 and not z.hypnotized:
                    z.take_damage(9999)
                    self.timer = -500  # Долгое пережевывание
                    break

        elif self.type == "squash":
            for z in game.zombies:
                if z.row == self.row and abs((self.x + CELL_W // 2) - z.x) < 40 and not z.hypnotized:
                    z.take_damage(1800)
                    self.alive = False
                    break

        elif self.type == "cherrybomb" and self.timer >= 90:
            self.alive = False
            game.explode_area(self.col, self.row, radius=1)

        elif self.type == "doomshroom" and self.timer >= 90:
            self.alive = False
            game.explode_area(self.col, self.row, radius=3)

        elif self.type == "jalapeno" and self.timer >= 75:
            self.alive = False
            for z in game.zombies:
                if z.row == self.row: z.take_damage(1800)

        elif self.type == "iceshroom" and self.timer >= 60:
            self.alive = False
            for z in game.zombies: z.freeze(300)

        elif self.type == "blover" and self.timer >= 60:
            self.alive = False
            for z in game.zombies: z.x += 160

    def draw(self, surf):
        draw_plant_art(surf, self.type, self.x + (CELL_W - 60) // 2, self.y + (CELL_H - 60) // 2, 60, self.phase)

class ZombieEntity:
    def __init__(self, row, z_type="regular"):
        self.row = row
        self.type = z_type
        self.x = WIDTH + random.randint(20, 80)
        self.y = GRID_Y + row * CELL_H + 12
        self.hp = ZOMBIES_DB[z_type]["hp"]
        self.speed = ZOMBIES_DB[z_type]["spd"]
        self.alive = True
        self.frozen_timer = 0
        self.hypnotized = False
        self.phase = random.uniform(0, 5.0)

    def take_damage(self, dmg):
        self.hp -= dmg
        if self.hp <= 0:
            self.alive = False

    def freeze(self, duration):
        self.frozen_timer = duration

    def update(self, game):
        self.phase += 0.06
        spd = self.speed * (0.5 if self.frozen_timer > 0 else 1.0)
        if self.frozen_timer > 0: self.frozen_timer -= 1

        if self.hypnotized:
            self.x += spd
            for z in game.zombies:
                if z != self and z.row == self.row and abs(self.x - z.x) < 25 and not z.hypnotized:
                    z.take_damage(1.0)
                    self.take_damage(1.0)
                    break
            return

        eating = False
        for p in game.plants:
            if p.row == self.row and abs(self.x - (p.x + 30)) < 24:
                eating = True
                if p.type == "hypnoshroom":
                    self.hypnotized = True
                    p.alive = False
                elif p.type != "spikeweed":
                    p.hp -= 0.6
                break

        # Колючки ранят наступающего зомби
        for p in game.plants:
            if p.type == "spikeweed" and p.row == self.row and abs(self.x - (p.x + 30)) < 30:
                self.take_damage(0.4)

        if not eating:
            self.x -= spd

    def draw(self, surf):
        draw_zombie_art(surf, self.type, self.x - 30, self.y - 10, 60, self.phase)
        if self.frozen_timer > 0:
            draw_circle_aa(surf, self.x, self.y + 20, 15, (0, 150, 255))

class Lawnmower:
    def __init__(self, row):
        self.row = row
        self.x = GRID_X - 60
        self.y = GRID_Y + row * CELL_H + 25
        self.active = False
        self.used = False

    def update(self, zombies):
        if self.active:
            self.x += 12
            for z in zombies:
                if z.row == self.row and abs(z.x - self.x) < 40:
                    z.take_damage(9999)
            if self.x > WIDTH + 50:
                self.used = True
        else:
            for z in zombies:
                if z.row == self.row and z.x <= self.x + 35 and not z.hypnotized:
                    self.active = True
                    break

    def draw(self, surf):
        if not self.used:
            pygame.draw.rect(surf, (220, 30, 30), (self.x, self.y, 42, 26), border_radius=4)
            draw_circle_aa(surf, self.x + 8, self.y + 28, 6, (40, 40, 40))
            draw_circle_aa(surf, self.x + 34, self.y + 28, 6, (40, 40, 40))

# --- ОСНОВНОЙ КОНТРОЛЛЕР ---
class PVZGame:
    def __init__(self):
        self.state = "MENU"  # MENU, LEVEL_SELECT, SHOP, ALMANAC, PLAY, PAUSE, GAMEOVER, WIN
        self.coins = 250
        self.unlocked_plants = ["peashooter", "sunflower", "wallnut", "cherrybomb"]
        self.deck = ["peashooter", "sunflower", "wallnut", "cherrybomb", "snowpea", "repeater", "chomper", "potatomine"]
        
        self.sun = 200
        self.selected_tool = None
        self.cooldowns = {k: 0 for k in PLANTS_DB}
        
        self.grid = [[None for _ in range(GRID_COLS)] for _ in range(GRID_ROWS)]
        self.plants = []
        self.zombies = []
        self.projectiles = []
        self.suns = []
        self.coins_list = []
        self.mowers = []
        
        self.cur_level = 0
        self.wave_idx = 0
        self.wave_timer = 0
        self.sky_sun_timer = 0
        self.almanac_tab = "plants"
        self.almanac_selected = "peashooter"

    def start_level(self, lvl_idx):
        self.cur_level = lvl_idx
        self.sun = 200
        self.selected_tool = None
        self.grid = [[None for _ in range(GRID_COLS)] for _ in range(GRID_ROWS)]
        self.plants.clear()
        self.zombies.clear()
        self.projectiles.clear()
        self.suns.clear()
        self.coins_list.clear()
        self.mowers = [Lawnmower(r) for r in range(GRID_ROWS)]
        self.cooldowns = {k: 0 for k in PLANTS_DB}
        self.wave_idx = 0
        self.wave_timer = 300
        self.state = "PLAY"

    def explode_area(self, c_col, c_row, radius=1):
        for r in range(max(0, c_row - radius), min(GRID_ROWS, c_row + radius + 1)):
            for z in self.zombies:
                if z.row == r and abs(z.x - (GRID_X + c_col * CELL_W + CELL_W // 2)) < CELL_W * (radius + 0.5):
                    z.take_damage(1800)

    def handle_click(self, pos):
        # 1. ГЛАВНОЕ МЕНЮ
        if self.state == "MENU":
            btns = [
                ("ИГРАТЬ", 250, lambda: setattr(self, "state", "LEVEL_SELECT")),
                ("МАГАЗИН ДЭЙВА", 330, lambda: setattr(self, "state", "SHOP")),
                ("АЛЬМАНАХ", 410, lambda: setattr(self, "state", "ALMANAC")),
                ("ВЫХОД", 490, lambda: (pygame.quit(), sys.exit()))
            ]
            for text, y, action in btns:
                if 460 <= pos[0] <= 720 and y <= pos[1] <= y + 55:
                    action()
                    return

        # 2. ВЫБОР УРОВНЕЙ
        elif self.state == "LEVEL_SELECT":
            for idx, lvl in enumerate(LEVELS_CONFIG):
                r = pygame.Rect(180 + (idx % 3) * 280, 200 + (idx // 3) * 140, 240, 100)
                if r.collidepoint(pos):
                    self.start_level(idx)
                    return
            if 500 <= pos[0] <= 680 and 590 <= pos[1] <= 640:
                self.state = "MENU"

        # 3. МАГАЗИН
        elif self.state == "SHOP":
            all_p = list(PLANTS_DB.keys())
            for idx, p in enumerate(all_p):
                x = 100 + (idx % 5) * 200
                y = 110 + (idx // 5) * 125
                btn_r = pygame.Rect(x, y + 80, 180, 32)
                if btn_r.collidepoint(pos):
                    cost = PLANTS_DB[p]["price"]
                    if p not in self.unlocked_plants and self.coins >= cost:
                        self.coins -= cost
                        self.unlocked_plants.append(p)
                        if len(self.deck) < 8 and p not in self.deck:
                            self.deck.append(p)
            if 510 <= pos[0] <= 670 and 630 <= pos[1] <= 675:
                self.state = "MENU"

        # 4. АЛЬМАНАХ / БЕСТИАРИЙ
        elif self.state == "ALMANAC":
            if 100 <= pos[0] <= 240 and 80 <= pos[1] <= 115: self.almanac_tab = "plants"
            if 250 <= pos[0] <= 390 and 80 <= pos[1] <= 115: self.almanac_tab = "zombies"

            if self.almanac_tab == "plants":
                for idx, p in enumerate(PLANTS_DB.keys()):
                    x = 100 + (idx % 5) * 90
                    y = 140 + (idx // 5) * 90
                    if pygame.Rect(x, y, 75, 75).collidepoint(pos):
                        self.almanac_selected = p
            else:
                for idx, z in enumerate(ZOMBIES_DB.keys()):
                    x = 100 + (idx % 4) * 110
                    y = 150 + (idx // 4) * 110
                    if pygame.Rect(x, y, 90, 90).collidepoint(pos):
                        self.almanac_selected = z

            if 520 <= pos[0] <= 660 and 630 <= pos[1] <= 675:
                self.state = "MENU"

        # 5. БОЕВОЙ РЕЖИМ
        elif self.state == "PLAY":
            # Сбор монет
            for c in self.coins_list:
                if math.hypot(pos[0] - c.x, pos[1] - c.y) < 25:
                    self.coins += 25
                    c.alive = False
                    return

            # Сбор солнца
            for s in self.suns:
                if math.hypot(pos[0] - s.x, pos[1] - s.y) < s.radius + 12:
                    self.sun += 25
                    s.alive = False
                    return

            # Выбор лопаты (координаты: x=780, y=18, w=65, h=65)
            if pygame.Rect(780, 18, 65, 65).collidepoint(pos):
                self.selected_tool = "shovel" if self.selected_tool != "shovel" else None
                return

            # Выбор карты на верхней панели
            for idx, p_type in enumerate(self.deck):
                r = pygame.Rect(110 + idx * 78, 14, 70, 92)
                if r.collidepoint(pos):
                    if p_type in self.unlocked_plants:
                        cost = PLANTS_DB[p_type]["cost"]
                        if self.sun >= cost and self.cooldowns[p_type] == 0:
                            self.selected_tool = p_type
                    return

            # Клик по сетке лужайки
            if GRID_X <= pos[0] < GRID_X + GRID_COLS * CELL_W and GRID_Y <= pos[1] < GRID_Y + GRID_ROWS * CELL_H:
                col = (pos[0] - GRID_X) // CELL_W
                row = (pos[1] - GRID_Y) // CELL_H

                if self.selected_tool == "shovel":
                    if self.grid[row][col] is not None:
                        self.plants.remove(self.grid[row][col])
                        self.grid[row][col] = None
                        self.selected_tool = None
                    return

                if self.selected_tool and self.selected_tool != "shovel":
                    if self.grid[row][col] is None:
                        cost = PLANTS_DB[self.selected_tool]["cost"]
                        p = PlantEntity(col, row, self.selected_tool)
                        self.plants.append(p)
                        self.grid[row][col] = p
                        self.sun -= cost
                        self.cooldowns[self.selected_tool] = PLANTS_DB[self.selected_tool]["cd"]
                        self.selected_tool = None

    def update(self):
        if self.state != "PLAY": return

        for k in self.cooldowns:
            if self.cooldowns[k] > 0: self.cooldowns[k] -= 1

        # Естественное солнце
        self.sky_sun_timer += 1
        if self.sky_sun_timer >= 320:
            tx = random.randint(GRID_X, GRID_X + GRID_COLS * CELL_W - 40)
            ty = random.randint(GRID_Y + 40, HEIGHT - 80)
            self.suns.append(Sun(tx, 0, ty))
            self.sky_sun_timer = 0

        # Спавн волн зомби
        waves = LEVELS_CONFIG[self.cur_level]["waves"]
        self.wave_timer += 1
        if self.wave_timer >= 550:
            if self.wave_idx < len(waves):
                for z_type in waves[self.wave_idx]:
                    self.zombies.append(ZombieEntity(random.randint(0, GRID_ROWS - 1), z_type))
                self.wave_idx += 1
                self.wave_timer = 0
            elif len(self.zombies) == 0:
                self.coins += LEVELS_CONFIG[self.cur_level]["reward"]
                self.state = "WIN"

        for p in self.plants: p.update(self)
        for pr in self.projectiles: pr.update()
        for z in self.zombies: z.update(self)
        for s in self.suns: s.update()
        for c in self.coins_list: c.update()
        for m in self.mowers: m.update(self.zombies)

        # Факельный пень зажигает горошины
        for pr in self.projectiles:
            if pr.type == "pea":
                for p in self.plants:
                    if p.type == "torchwood" and p.row == pr.row and abs(pr.x - (p.x + 40)) < 15:
                        pr.type = "fire_pea"

        # Столкновения снарядов с зомби
        for pr in self.projectiles:
            for z in self.zombies:
                if z.row == pr.row and abs(pr.x - z.x) < 25 and not z.hypnotized:
                    z.take_damage(pr.damage)
                    if pr.type == "ice_pea": z.freeze(180)
                    pr.alive = False
                    break

        for z in self.zombies:
            if not z.hypnotized and z.x < GRID_X - 60:
                self.state = "GAMEOVER"

        # Выпадение монет из погибших зомби
        for z in self.zombies:
            if not z.alive and random.random() < 0.6:
                self.coins_list.append(Coin(z.x, z.y + 20))

        # Очистка мертвых сущностей
        for p in self.plants:
            if p.hp <= 0 or not p.alive:
                self.grid[p.row][p.col] = None
        self.plants = [p for p in self.plants if p.hp > 0 and p.alive]
        self.projectiles = [pr for pr in self.projectiles if pr.alive]
        self.zombies = [z for z in self.zombies if z.alive]
        self.suns = [s for s in self.suns if s.alive]
        self.coins_list = [c for c in self.coins_list if c.alive]

    def draw(self, surf):
        # 1. МЕНЮ
        if self.state == "MENU":
            surf.fill((35, 70, 30))
            t = f_big.render("PLANTS VS. ZOMBIES", True, (245, 215, 30))
            surf.blit(t, (WIDTH // 2 - t.get_width() // 2, 90))

            btns = ["ИГРАТЬ", "МАГАЗИН ДЭЙВА", "АЛЬМАНАХ", "ВЫХОД"]
            for idx, text in enumerate(btns):
                r = pygame.Rect(460, 250 + idx * 80, 260, 55)
                pygame.draw.rect(surf, (140, 80, 40), r, border_radius=10)
                pygame.draw.rect(surf, (60, 30, 10), r, 3, border_radius=10)
                lbl = f_med.render(text, True, (255, 255, 255))
                surf.blit(lbl, (r.centerx - lbl.get_width() // 2, r.centery - lbl.get_height() // 2))

            # Отображение монет в углу
            pygame.draw.rect(surf, (20, 20, 20), (WIDTH - 200, 20, 180, 40), border_radius=6)
            c_t = f_med.render(f"Монеты: {self.coins} $", True, (255, 215, 0))
            surf.blit(c_t, (WIDTH - 190, 28))
            return

        # 2. ВЫБОР УРОВНЕЙ
        elif self.state == "LEVEL_SELECT":
            surf.fill((40, 50, 40))
            t = f_big.render("ВЫБОР УРОВНЯ", True, (245, 215, 30))
            surf.blit(t, (WIDTH // 2 - t.get_width() // 2, 40))

            for idx, lvl in enumerate(LEVELS_CONFIG):
                r = pygame.Rect(180 + (idx % 3) * 280, 200 + (idx // 3) * 140, 240, 100)
                pygame.draw.rect(surf, (90, 140, 60), r, border_radius=8)
                pygame.draw.rect(surf, (30, 60, 20), r, 3, border_radius=8)
                n = f_med.render(lvl["name"], True, (255, 255, 255))
                rw = f_small.render(f"Награда: {lvl['reward']} $", True, (255, 230, 80))
                surf.blit(n, (r.centerx - n.get_width() // 2, r.y + 25))
                surf.blit(rw, (r.centerx - rw.get_width() // 2, r.y + 60))

            b_back = pygame.Rect(500, 590, 180, 50)
            pygame.draw.rect(surf, (140, 60, 40), b_back, border_radius=8)
            lbl = f_med.render("НАЗАД", True, (255, 255, 255))
            surf.blit(lbl, (b_back.centerx - lbl.get_width() // 2, b_back.centery - lbl.get_height() // 2))
            return

        # 3. МАГАЗИН ДЭЙВА
        elif self.state == "SHOP":
            surf.fill((50, 40, 35))
            t = f_big.render("МАГАЗИН БЕЗУМНОГО ДЭЙВА", True, (255, 215, 0))
            surf.blit(t, (WIDTH // 2 - t.get_width() // 2, 30))
            c_lbl = f_med.render(f"Ваш баланс: {self.coins} $", True, (255, 230, 80))
            surf.blit(c_lbl, (WIDTH - 240, 38))

            all_p = list(PLANTS_DB.keys())
            for idx, p in enumerate(all_p):
                x = 100 + (idx % 5) * 200
                y = 100 + (idx // 5) * 125
                box = pygame.Rect(x, y, 180, 115)
                pygame.draw.rect(surf, (80, 65, 55), box, border_radius=6)
                draw_plant_art(surf, p, x + 8, y + 10, 45)

                name_t = f_tiny.render(PLANTS_DB[p]["name"][:11], True, (255, 255, 255))
                surf.blit(name_t, (x + 60, y + 15))

                btn_r = pygame.Rect(x + 10, y + 70, 160, 32)
                if p in self.unlocked_plants:
                    pygame.draw.rect(surf, (60, 100, 60), btn_r, border_radius=4)
                    bt = f_small.render("КУПЛЕНО", True, (200, 255, 200))
                else:
                    cost = PLANTS_DB[p]["price"]
                    col = (180, 120, 40) if self.coins >= cost else (90, 80, 75)
                    pygame.draw.rect(surf, col, btn_r, border_radius=4)
                    bt = f_small.render(f"Купить: {cost} $", True, (255, 255, 255))
                surf.blit(bt, (btn_r.centerx - bt.get_width() // 2, btn_r.centery - bt.get_height() // 2))

            b_back = pygame.Rect(510, 630, 160, 45)
            pygame.draw.rect(surf, (140, 60, 40), b_back, border_radius=8)
            lbl = f_med.render("НАЗАД", True, (255, 255, 255))
            surf.blit(lbl, (b_back.centerx - lbl.get_width() // 2, b_back.centery - lbl.get_height() // 2))
            return

        # 4. АЛЬМАНАХ / БЕСТИАРИЙ
        elif self.state == "ALMANAC":
            surf.fill((35, 45, 50))
            t = f_big.render("ПРИГОРОДНЫЙ АЛЬМАНАХ", True, (245, 215, 30))
            surf.blit(t, (WIDTH // 2 - t.get_width() // 2, 25))

            tab1 = pygame.Rect(100, 80, 140, 35)
            tab2 = pygame.Rect(250, 80, 140, 35)
            pygame.draw.rect(surf, (70, 120, 60) if self.almanac_tab == "plants" else (50, 60, 65), tab1, border_radius=4)
            pygame.draw.rect(surf, (70, 120, 60) if self.almanac_tab == "zombies" else (50, 60, 65), tab2, border_radius=4)
            surf.blit(f_small.render("РАСТЕНИЯ", True, (255, 255, 255)), (tab1.centerx - 38, tab1.centery - 8))
            surf.blit(f_small.render("ЗОМБИ", True, (255, 255, 255)), (tab2.centerx - 25, tab2.centery - 8))

            # Сетка иконок
            if self.almanac_tab == "plants":
                for idx, p in enumerate(PLANTS_DB.keys()):
                    x = 100 + (idx % 5) * 90
                    y = 135 + (idx // 5) * 90
                    r = pygame.Rect(x, y, 75, 75)
                    sel = (self.almanac_selected == p)
                    pygame.draw.rect(surf, (100, 160, 80) if sel else (55, 70, 75), r, border_radius=6)
                    draw_plant_art(surf, p, x + 7, y + 7, 60)

                # Панель описания
                info_r = pygame.Rect(600, 135, 460, 440)
                pygame.draw.rect(surf, (45, 60, 65), info_r, border_radius=8)
                spec = PLANTS_DB[self.almanac_selected]
                draw_plant_art(surf, self.almanac_selected, 620, 160, 100)
                surf.blit(f_big.render(spec["name"], True, (255, 255, 255)), (740, 170))
                surf.blit(f_med.render(f"Стоимость: {spec['cost']} солнца", True, (255, 215, 0)), (740, 220))
                surf.blit(f_med.render(f"Здоровье: {spec['hp']}", True, (150, 255, 150)), (740, 250))
                # Описание
                desc_lines = spec["desc"]
                surf.blit(f_small.render(desc_lines, True, (220, 220, 220)), (630, 320))
            else:
                for idx, z in enumerate(ZOMBIES_DB.keys()):
                    x = 100 + (idx % 4) * 110
                    y = 140 + (idx // 4) * 110
                    r = pygame.Rect(x, y, 90, 90)
                    sel = (self.almanac_selected == z)
                    pygame.draw.rect(surf, (140, 100, 80) if sel else (55, 70, 75), r, border_radius=6)
                    draw_zombie_art(surf, z, x + 15, y + 10, 60)

                info_r = pygame.Rect(600, 135, 460, 440)
                pygame.draw.rect(surf, (45, 60, 65), info_r, border_radius=8)
                if self.almanac_selected in ZOMBIES_DB:
                    spec = ZOMBIES_DB[self.almanac_selected]
                    draw_zombie_art(surf, self.almanac_selected, 630, 160, 100)
                    surf.blit(f_big.render(spec["name"], True, (255, 255, 255)), (750, 170))
                    surf.blit(f_med.render(f"Здоровье: {spec['hp']}", True, (255, 100, 100)), (750, 220))
                    surf.blit(f_small.render(spec["desc"], True, (220, 220, 220)), (630, 320))

            b_back = pygame.Rect(520, 630, 140, 45)
            pygame.draw.rect(surf, (140, 60, 40), b_back, border_radius=8)
            lbl = f_med.render("НАЗАД", True, (255, 255, 255))
            surf.blit(lbl, (b_back.centerx - lbl.get_width() // 2, b_back.centery - lbl.get_height() // 2))
            return

        # 5. БОЕВОЙ ГАЗОН
        surf.fill((55, 100, 40))
        for r in range(GRID_ROWS):
            for c in range(GRID_COLS):
                col = (110, 180, 35) if (r + c) % 2 == 0 else (95, 165, 30)
                pygame.draw.rect(surf, col, (GRID_X + c * CELL_W, GRID_Y + r * CELL_H, CELL_W, CELL_H))
                pygame.draw.rect(surf, (65, 125, 20), (GRID_X + c * CELL_W, GRID_Y + r * CELL_H, CELL_W, CELL_H), 1)

        for m in self.mowers: m.draw(surf)
        for p in self.plants: p.draw(surf)
        for z in self.zombies: z.draw(surf)
        for pr in self.projectiles: pr.draw(surf)
        for s in self.suns: s.draw(surf)
        for c in self.coins_list: c.draw(surf)

        # Верхняя панель колоды
        pygame.draw.rect(surf, (100, 65, 35), (0, 0, WIDTH, 115))

        # Солнце
        pygame.draw.rect(surf, (220, 200, 150), (15, 12, 80, 90), border_radius=6)
        draw_circle_aa(surf, 55, 42, 18, (255, 200, 0))
        s_lbl = f_med.render(str(self.sun), True, (50, 30, 10))
        surf.blit(s_lbl, (55 - s_lbl.get_width() // 2, 70))

        # Карточки с анатомическими иконками
        for idx, p_type in enumerate(self.deck):
            x = 110 + idx * 78
            card_r = pygame.Rect(x, 14, 70, 92)
            cost = PLANTS_DB[p_type]["cost"]
            cd = self.cooldowns[p_type]
            can_afford = (self.sun >= cost and cd == 0 and p_type in self.unlocked_plants)

            col = (230, 220, 180) if can_afford else (130, 120, 110)
            if self.selected_tool == p_type: col = (255, 255, 120)
            pygame.draw.rect(surf, col, card_r, border_radius=5)
            pygame.draw.rect(surf, (40, 25, 10), card_r, 2, border_radius=5)

            # Иконка растения на карточке
            draw_plant_art(surf, p_type, x + 10, 18, 50)

            # Стоимость
            c_txt = f_small.render(str(cost), True, (160, 20, 20))
            surf.blit(c_txt, (x + 35 - c_txt.get_width() // 2, 72))

            # Перезарядка
            if cd > 0:
                pct = cd / PLANTS_DB[p_type]["cd"]
                h = int(92 * pct)
                s = pygame.Surface((70, h), pygame.SRCALPHA)
                s.fill((0, 0, 0, 160))
                surf.blit(s, (x, 14 + (92 - h)))

        # Лопата
        sh_r = pygame.Rect(780, 18, 65, 65)
        sh_col = (255, 255, 120) if self.selected_tool == "shovel" else (140, 90, 50)
        pygame.draw.rect(surf, sh_col, sh_r, border_radius=6)
        pygame.draw.rect(surf, (40, 20, 10), sh_r, 2, border_radius=6)
        pygame.draw.line(surf, (150, 90, 40), (790, 70), (825, 30), 6)
        pygame.draw.polygon(surf, (190, 200, 210), [(820, 38), (835, 22), (825, 15), (810, 30)])

        # Монеты и волны
        pygame.draw.rect(surf, (20, 20, 20), (WIDTH - 240, 20, 210, 35), border_radius=4)
        c_disp = f_small.render(f"Монеты: {self.coins} $", True, (255, 215, 0))
        surf.blit(c_disp, (WIDTH - 225, 28))

        # Индикатор прогресса волны
        total_w = len(LEVELS_CONFIG[self.cur_level]["waves"])
        prog = min(1.0, self.wave_idx / max(1, total_w))
        pygame.draw.rect(surf, (30, 30, 30), (WIDTH - 240, 65, 210, 18), border_radius=4)
        pygame.draw.rect(surf, (40, 220, 40), (WIDTH - 240, 65, int(210 * prog), 18), border_radius=4)
        w_lbl = f_tiny.render("ВОЛНА", True, (255, 255, 255))
        surf.blit(w_lbl, (WIDTH - 150, 67))

        # Модальные экраны
        if self.state == "GAMEOVER":
            s = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
            s.fill((100, 0, 0, 200))
            surf.blit(s, (0, 0))
            t = f_big.render("ЗОМБИ СЪЕЛИ ВАШИ МОЗГИ!", True, (255, 80, 80))
            st = f_med.render("Нажмите R для рестарта или ESC для выхода", True, (255, 255, 255))
            surf.blit(t, (WIDTH // 2 - t.get_width() // 2, HEIGHT // 2 - 40))
            surf.blit(st, (WIDTH // 2 - st.get_width() // 2, HEIGHT // 2 + 20))

        elif self.state == "WIN":
            s = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
            s.fill((20, 90, 20, 200))
            surf.blit(s, (0, 0))
            t = f_big.render("УРОВЕНЬ ПРОЙДЕН!", True, (240, 220, 50))
            st = f_med.render(f"Получено: +{LEVELS_CONFIG[self.cur_level]['reward']} $. Нажмите ESC для выхода в меню", True, (255, 255, 255))
            surf.blit(t, (WIDTH // 2 - t.get_width() // 2, HEIGHT // 2 - 40))
            surf.blit(st, (WIDTH // 2 - st.get_width() // 2, HEIGHT // 2 + 20))

# --- ТОЧКА ВХОДА ---
def main():
    game = PVZGame()
    running = True

    while running:
        clock.tick(FPS)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    if game.state in ["PLAY", "SHOP", "ALMANAC", "LEVEL_SELECT", "WIN", "GAMEOVER"]:
                        game.state = "MENU"
                elif event.key == pygame.K_r and game.state in ["GAMEOVER", "WIN"]:
                    game.start_level(game.cur_level)
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                game.handle_click(pygame.mouse.get_pos())

        game.update()
        game.draw(screen)
        pygame.display.flip()

    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()