Загрузка данных
import pygame
import pygame.gfxdraw
import random
import math
import sys
pygame.init()
pygame.font.init()
# --- Конфигурация экрана и поля ---
WIDTH, HEIGHT = 1200, 720
GRID_COLS, GRID_ROWS = 9, 5
CELL_W, CELL_H = 92, 100
GRID_X, GRID_Y = 280, 150
FPS = 60
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Plants vs. Zombies 2: All-Star Deluxe")
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)
# --- БАЗА ДАННЫХ: 24 РАСТЕНИЯ ---
PLANTS_DB = {
"peashooter": {"cost": 100, "hp": 200, "cd": 90, "price": 0, "name": "Peashooter", "desc": "Стреляет горошинами по врагам на линии."},
"sunflower": {"cost": 50, "hp": 150, "cd": 90, "price": 0, "name": "Sunflower", "desc": "Производит дополнительное солнце для посадок."},
"wallnut": {"cost": 50, "hp": 900, "cd": 350, "price": 0, "name": "Wall-nut", "desc": "Прочная преграда, задерживающая толпы зомби."},
"cherrybomb": {"cost": 150, "hp": 600, "cd": 500, "price": 0, "name": "Cherry Bomb", "desc": "Взрывает всех зомби в области 3х3."},
"potatomine": {"cost": 25, "hp": 150, "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": 300, "cd": 150, "price": 200, "name": "Chomper", "desc": "Проглатывает зомби целиком, но долго жует."},
"repeater": {"cost": 200, "hp": 200, "cd": 120, "price": 250, "name": "Repeater", "desc": "Выпускает сразу две горошины за один залп."},
"puffshroom": {"cost": 0, "hp": 80, "cd": 60, "price": 150, "name": "Puff-shroom", "desc": "Бесплатный гриб малой дальности стрельбы."},
"sunshroom": {"cost": 25, "hp": 120, "cd": 90, "price": 200, "name": "Sun-shroom", "desc": "Сначала дает мало солнца, затем вырастает."},
"fumeshroom": {"cost": 75, "hp": 200, "cd": 120, "price": 250, "name": "Fume-shroom", "desc": "Стреляет спорами сквозь щиты, двери и ведра."},
"gravebuster": {"cost": 25, "hp": 200, "cd": 150, "price": 100, "name": "Grave Buster", "desc": "Пожирает надгробия на лужайке."},
"hypnoshroom": {"cost": 75, "hp": 100, "cd": 400, "price": 300, "name": "Hypno-shroom", "desc": "Зомби разворачивается и начинает драться за вас."},
"scaredyshroom":{"cost": 25, "hp": 100, "cd": 90, "price": 180, "name": "Scaredy-shroom","desc": "Дальнобойный гриб, прячется при сближении зомби."},
"iceshroom": {"cost": 75, "hp": 200, "cd": 500, "price": 350, "name": "Ice-shroom", "desc": "Замораживает всех зомби на лужайке."},
"doomshroom": {"cost": 125, "hp": 200, "cd": 650, "price": 400, "name": "Doom-shroom", "desc": "Колоссальный взрыв по гигантскому радиусу."},
"lilypad": {"cost": 25, "hp": 150, "cd": 60, "price": 100, "name": "Lily Pad", "desc": "Кувшинка для высадки растений на воду."},
"squash": {"cost": 50, "hp": 400, "cd": 300, "price": 250, "name": "Squash", "desc": "Раздавливает ближайшего зомби всмятку."},
"threepeater": {"cost": 325, "hp": 250, "cd": 150, "price": 450, "name": "Threepeater", "desc": "Стреляет сразу по трем линиям лужайки."},
"tanglekelp": {"cost": 25, "hp": 100, "cd": 200, "price": 150, "name": "Tangle Kelp", "desc": "Утаскивает первого наступившего зомби на дно."},
"jalapeno": {"cost": 125, "hp": 300, "cd": 450, "price": 350, "name": "Jalapeno", "desc": "Испепеляет всю полосу огненной волной."},
"spikeweed": {"cost": 100, "hp": 300, "cd": 100, "price": 300, "name": "Spikeweed", "desc": "Колет зомби снизу, не может быть съеден."},
"torchwood": {"cost": 175, "hp": 350, "cd": 150, "price": 400, "name": "Torchwood", "desc": "Зажигает пролетающий горох, удваивая урон."},
"tallnut": {"cost": 125, "hp": 1800, "cd": 450, "price": 450, "name": "Tall-nut", "desc": "Огромный орех повышенной прочности, нельзя перепрыгнуть."}
}
# --- БАЗА ДАННЫХ ЗОМБИ ---
ZOMBIES_DB = {
"regular": {"name": "Zombie", "hp": 150, "spd": 0.35, "desc": "Обычный рядовой зомби. Медленный и голодный."},
"conehead": {"name": "Conehead", "hp": 350, "spd": 0.35, "desc": "Дорожный конус делает его вдвое прочнее."},
"buckethead": {"name": "Buckethead", "hp": 800, "spd": 0.35, "desc": "Ведро на голове выдерживает мощный град снарядов."},
"gargantuar": {"name": "Gargantuar", "hp": 2200, "spd": 0.20, "desc": "Гигант! Крушит растения телеграфным столбом."}
}
WORLDS_CONFIG = [
{
"id": "egypt", "name": "1. Древний Египет", "grass": (196, 160, 95), "alt": (180, 145, 85),
"reward": 150, "waves": [["regular"], ["regular", "conehead"], ["conehead", "buckethead"]]
},
{
"id": "pirate", "name": "2. Пиратские Моря", "grass": (95, 140, 150), "alt": (85, 125, 135),
"reward": 250, "waves": [["regular", "conehead"], ["conehead", "buckethead"], ["buckethead", "buckethead", "conehead"]]
},
{
"id": "west", "name": "3. Дикий Запад (БОСС)", "grass": (165, 115, 75), "alt": (150, 105, 65),
"reward": 500, "waves": [["buckethead", "conehead"], ["gargantuar"], ["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_ellipse_aa(surf, rect, color):
pygame.draw.ellipse(surf, color, rect)
# --- ПРОЦЕДУРНЫЙ РЕНДЕР МОДЕЛЕЙ И ИКОНОК ---
def draw_plant_art(surf, p_type, x, y, size=60, phase=0.0, hp_pct=1.0, has_armor=False):
s = pygame.Surface((size, size), pygame.SRCALPHA)
cx, cy = size // 2, size // 2
bob = math.sin(phase) * (2 if size > 40 else 0)
# Тень под растением
pygame.draw.ellipse(s, (0, 0, 0, 70), (cx - 16, size - 14, 32, 10))
if p_type in ["peashooter", "repeater", "snowpea"]:
head_col = (90, 220, 255) if p_type == "snowpea" else (60, 195, 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, head_col)
pygame.draw.rect(s, head_col, (cx + 6, cy - 12 + bob, 12, 11), 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 == "threepeater":
pygame.draw.line(s, (45, 160, 45), (cx, cy + 18), (cx - 12, cy - 2 + bob), 3)
pygame.draw.line(s, (45, 160, 45), (cx, cy + 18), (cx + 12, cy - 2 + bob), 3)
pygame.draw.line(s, (45, 160, 45), (cx, cy + 18), (cx, cy - 8 + bob), 3)
for ox, oy in [(-12, -2), (0, -8), (12, -2)]:
draw_circle_aa(s, cx + ox, cy + oy + bob, 8, (60, 195, 50))
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, 205, 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))
if hp_pct < 0.6:
pygame.draw.line(s, (70, 35, 15), (cx - 6, cy + bob), (cx + 4, cy + 8 + bob), 2)
if hp_pct < 0.3:
pygame.draw.line(s, (70, 35, 15), (cx + 4, cy - 6 + bob), (cx + 10, cy + 4 + bob), 2)
if has_armor:
pygame.draw.rect(s, (180, 190, 200), (cx - 16, cy - h - 2 + bob, 32, 14), border_radius=3)
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 ["puffshroom", "sunshroom", "fumeshroom", "hypnoshroom", "scaredyshroom", "iceshroom", "doomshroom"]:
colors = {
"puffshroom": (190, 80, 220), "sunshroom": (240, 210, 40), "fumeshroom": (160, 50, 180),
"hypnoshroom": (255, 105, 180), "scaredyshroom": (150, 90, 200), "iceshroom": (120, 220, 255), "doomshroom": (45, 45, 55)
}
col = colors[p_type]
pygame.draw.rect(s, (215, 215, 215), (cx - 5, cy, 10, 16), border_radius=3)
pygame.draw.ellipse(s, col, (cx - 16, cy - 16 + bob, 32, 22))
elif p_type == "gravebuster":
pygame.draw.arc(s, (80, 140, 60), (cx - 14, cy - 12 + bob, 28, 28), 0, math.pi, 4)
pygame.draw.line(s, (255, 255, 255), (cx - 8, cy + 2 + bob), (cx - 8, cy + 8 + bob), 2)
pygame.draw.line(s, (255, 255, 255), (cx + 8, cy + 2 + bob), (cx + 8, cy + 8 + bob), 2)
elif p_type in ["lilypad", "tanglekelp"]:
col = (40, 160, 80) if p_type == "lilypad" else (20, 100, 50)
pygame.draw.ellipse(s, col, (cx - 18, cy + 4, 36, 16))
if p_type == "tanglekelp":
draw_circle_aa(s, cx - 4, cy + 8, 2, (255, 0, 0))
draw_circle_aa(s, cx + 4, cy + 8, 2, (255, 0, 0))
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))
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 = (135, 165, 130)
bob = math.sin(phase) * 3
pygame.draw.ellipse(s, (0, 0, 0, 70), (cx - 16, size + 6, 32, 10))
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 Particle:
def __init__(self, x, y, color, vx, vy, life, size=4):
self.x, self.y = x, y
self.color = color
self.vx, self.vy = vx, vy
self.life = life
self.max_life = life
self.size = size
def update(self):
self.x += self.vx
self.y += self.vy
self.life -= 1
def draw(self, surf):
if self.life > 0:
alpha = int((self.life / self.max_life) * 255)
s = pygame.Surface((self.size * 2, self.size * 2), pygame.SRCALPHA)
pygame.draw.circle(s, (*self.color, alpha), (self.size, self.size), self.size)
surf.blit(s, (self.x - self.size, self.y - self.size))
# --- ИГРОВЫЕ СУЩНОСТИ ---
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 PlantFoodOrb:
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):
pulse = math.sin(self.timer * 0.1) * 3
draw_circle_aa(surf, self.x, self.y, 14 + pulse, (50, 255, 100))
draw_circle_aa(surf, self.x, self.y, 9, (255, 255, 255))
pygame.draw.ellipse(surf, (30, 180, 50), (self.x - 4, self.y - 7, 8, 14))
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 = 45 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 == "spore":
draw_circle_aa(surf, self.x, self.y, 6, (200, 100, 255))
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.max_hp = self.hp
self.x = GRID_X + col * CELL_W
self.y = GRID_Y + row * CELL_H
self.timer = 0
self.phase = random.uniform(0, 6.0)
self.alive = True
self.armed = False if p_type == "potatomine" else True
self.has_armor = False
self.plant_food_active = 0
def apply_plant_food(self, game):
self.plant_food_active = 180
if self.type in ["sunflower", "sunshroom"]:
for _ in range(5):
game.suns.append(Sun(self.x + random.randint(10, 50), self.y + random.randint(10, 50)))
self.plant_food_active = 0
elif self.type in ["wallnut", "tallnut"]:
self.has_armor = True
self.hp = 2600
self.plant_food_active = 0
def update(self, game):
self.timer += 1
self.phase += 0.06
# Логика Подкормки (Plant Food)
if self.plant_food_active > 0:
self.plant_food_active -= 1
if self.type in ["peashooter", "repeater"]:
if self.timer % 4 == 0:
game.projectiles.append(Projectile(self.x + CELL_W - 10, self.y + 35, self.row, "pea", vx=12.0))
return
# Регулярные действия
if self.type in ["sunflower", "sunshroom"] 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"]:
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 == "threepeater" and self.timer >= 90:
if any(abs(z.row - self.row) <= 1 and z.x > self.x for z in game.zombies):
for r_off in [-1, 0, 1]:
tr = self.row + r_off
if 0 <= tr < GRID_ROWS:
game.projectiles.append(Projectile(self.x + CELL_W - 10, self.y + 35, tr, "pea", vy=r_off * 0.8))
self.timer = 0
elif self.type in ["puffshroom", "fumeshroom", "scaredyshroom"] and self.timer >= 85:
dist_max = CELL_W * 4 if self.type == "puffshroom" else WIDTH
if any(z.row == self.row and 0 < z.x - self.x < dist_max for z in game.zombies):
game.projectiles.append(Projectile(self.x + CELL_W - 10, self.y + 35, self.row, "spore"))
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)
def draw(self, surf):
pct = max(0.0, self.hp / self.max_hp)
draw_plant_art(surf, self.type, self.x + (CELL_W - 60) // 2, self.y + (CELL_H - 60) // 2, 60, self.phase, pct, self.has_armor)
if self.plant_food_active > 0:
draw_circle_aa(surf, self.x + CELL_W // 2, self.y + CELL_H // 2, 34, (100, 255, 150))
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)
self.carries_food = (random.random() < 0.25)
def take_damage(self, dmg):
self.hp -= dmg
if self.hp <= 0:
self.alive = False
def freeze(self, dur):
self.frozen_timer = dur
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):
if self.carries_food:
draw_circle_aa(surf, self.x, self.y + 10, 24, (50, 255, 120))
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 PVZEngine:
def __init__(self):
self.state = "MENU" # MENU, WORLD_SELECT, SHOP, ALMANAC, PLAY, GAMEOVER, WIN
self.coins = 600
self.plant_food = 3
self.unlocked_plants = ["peashooter", "sunflower", "wallnut", "cherrybomb"]
self.deck = ["peashooter", "sunflower", "wallnut", "cherrybomb", "snowpea", "repeater", "puffshroom", "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.food_orbs = []
self.particles = []
self.mowers = []
self.world_idx = 0
self.wave_idx = 0
self.wave_timer = 0
self.sky_sun_timer = 0
self.almanac_tab = "plants"
self.almanac_selected = "peashooter"
def start_battle(self, w_idx):
self.world_idx = w_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.food_orbs.clear()
self.particles.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 = 250
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)
for _ in range(40):
ang = random.uniform(0, math.pi * 2)
spd = random.uniform(2, 7)
col = random.choice([(255, 60, 0), (255, 200, 0), (80, 80, 80)])
self.particles.append(Particle(GRID_X + c_col * CELL_W + 40, GRID_Y + c_row * CELL_H + 40, col, math.cos(ang)*spd, math.sin(ang)*spd, 35))
def handle_click(self, pos):
# 1. ГЛАВНОЕ МЕНЮ
if self.state == "MENU":
btns = [
("ИГРАТЬ", 260, lambda: setattr(self, "state", "WORLD_SELECT")),
("МАГАЗИН ДЭЙВА", 340, lambda: setattr(self, "state", "SHOP")),
("БЕСТИАРИЙ", 420, lambda: setattr(self, "state", "ALMANAC")),
("ВЫХОД", 500, lambda: (pygame.quit(), sys.exit()))
]
for text, y, action in btns:
if 460 <= pos[0] <= 740 and y <= pos[1] <= y + 55:
action()
return
# 2. ВЫБОР МИРА
elif self.state == "WORLD_SELECT":
for idx, w in enumerate(WORLDS_CONFIG):
r = pygame.Rect(160 + idx * 300, 240, 270, 150)
if r.collidepoint(pos):
self.start_battle(idx)
return
if 520 <= 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 = 80 + (idx % 6) * 175
y = 90 + (idx // 6) * 125
btn_r = pygame.Rect(x, y + 75, 160, 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 520 <= pos[0] <= 680 and 640 <= pos[1] <= 685: self.state = "MENU"
# 4. БЕСТИАРИЙ / АЛЬМАНАХ
elif self.state == "ALMANAC":
if 100 <= pos[0] <= 240 and 70 <= pos[1] <= 105:
self.almanac_tab = "plants"
self.almanac_selected = "peashooter"
elif 250 <= pos[0] <= 390 and 70 <= pos[1] <= 105:
self.almanac_tab = "zombies"
self.almanac_selected = "regular"
if self.almanac_tab == "plants":
for idx, p in enumerate(PLANTS_DB.keys()):
x = 80 + (idx % 6) * 82
y = 125 + (idx // 6) * 82
if pygame.Rect(x, y, 74, 74).collidepoint(pos):
self.almanac_selected = p
else:
for idx, z in enumerate(ZOMBIES_DB.keys()):
x = 100 + (idx % 4) * 110
y = 140 + (idx // 4) * 110
if pygame.Rect(x, y, 90, 90).collidepoint(pos):
self.almanac_selected = z
if 520 <= pos[0] <= 680 and 640 <= pos[1] <= 685: 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 fo in self.food_orbs:
if math.hypot(pos[0] - fo.x, pos[1] - fo.y) < 25:
if self.plant_food < 3: self.plant_food += 1
fo.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
# Кнопка Подкормки (Листик)
if pygame.Rect(750, 16, 60, 65).collidepoint(pos):
if self.plant_food > 0:
self.selected_tool = "plant_food" if self.selected_tool != "plant_food" else None
return
# Суперспособность Молния (Power Zap)
if pygame.Rect(825, 16, 75, 65).collidepoint(pos):
if self.coins >= 150:
self.selected_tool = "power_zap" if self.selected_tool != "power_zap" else None
return
# Лопата
if pygame.Rect(915, 16, 60, 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 == "power_zap":
self.coins -= 150
for z in self.zombies:
if z.row == row and abs(z.x - pos[0]) < CELL_W * 1.5:
z.take_damage(600)
self.selected_tool = None
return
if self.selected_tool == "plant_food":
if self.grid[row][col] is not None:
self.grid[row][col].apply_plant_food(self)
self.plant_food -= 1
self.selected_tool = None
return
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 not in ["shovel", "plant_food", "power_zap"]:
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 = WORLDS_CONFIG[self.world_idx]["waves"]
self.wave_timer += 1
if self.wave_timer >= 520:
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 += WORLDS_CONFIG[self.world_idx]["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 fo in self.food_orbs: fo.update()
for part in self.particles: part.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
for _ in range(4):
self.particles.append(Particle(pr.x, pr.y, (80, 220, 80), random.uniform(-2, 2), random.uniform(-2, 2), 15))
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:
if z.carries_food: self.food_orbs.append(PlantFoodOrb(z.x, z.y))
elif random.random() < 0.5: self.coins_list.append(Coin(z.x, z.y + 15))
# Очистка мертвых
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]
self.food_orbs = [fo for fo in self.food_orbs if fo.alive]
self.particles = [pt for pt in self.particles if pt.life > 0]
def draw(self, surf):
# 1. ГЛАВНОЕ МЕНЮ
if self.state == "MENU":
surf.fill((30, 55, 30))
t = f_big.render("PLANTS VS. ZOMBIES 2", True, (255, 215, 0))
st = f_med.render("ALL-STAR DELUXE ENGINE", True, (150, 255, 150))
surf.blit(t, (WIDTH // 2 - t.get_width() // 2, 90))
surf.blit(st, (WIDTH // 2 - st.get_width() // 2, 150))
btns = ["ИГРАТЬ", "МАГАЗИН ДЭЙВА", "БЕСТИАРИЙ", "ВЫХОД"]
for idx, text in enumerate(btns):
r = pygame.Rect(460, 260 + idx * 80, 280, 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)
surf.blit(f_med.render(f"Монеты: {self.coins} $", True, (255, 215, 0)), (WIDTH - 190, 28))
return
# 2. ВЫБОР МИРА
elif self.state == "WORLD_SELECT":
surf.fill((35, 45, 40))
t = f_big.render("ВЫБЕРИТЕ ЭПОХУ ПРИКЛЮЧЕНИЯ", True, (255, 215, 0))
surf.blit(t, (WIDTH // 2 - t.get_width() // 2, 60))
for idx, w in enumerate(WORLDS_CONFIG):
r = pygame.Rect(160 + idx * 300, 240, 270, 150)
pygame.draw.rect(surf, w["grass"], r, border_radius=10)
pygame.draw.rect(surf, (20, 20, 20), r, 3, border_radius=10)
n = f_med.render(w["name"], True, (255, 255, 255))
rw = f_small.render(f"Награда: {w['reward']} $", True, (255, 230, 80))
surf.blit(n, (r.centerx - n.get_width() // 2, r.y + 40))
surf.blit(rw, (r.centerx - rw.get_width() // 2, r.y + 80))
b_back = pygame.Rect(520, 590, 160, 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((45, 38, 32))
t = f_big.render("МАГАЗИН БЕЗУМНОГО ДЭЙВА", True, (255, 215, 0))
surf.blit(t, (WIDTH // 2 - t.get_width() // 2, 20))
surf.blit(f_med.render(f"Баланс: {self.coins} $", True, (255, 230, 80)), (WIDTH - 220, 28))
all_p = list(PLANTS_DB.keys())
for idx, p in enumerate(all_p):
x = 80 + (idx % 6) * 175
y = 80 + (idx // 6) * 125
pygame.draw.rect(surf, (70, 58, 50), (x, y, 160, 115), border_radius=6)
draw_plant_art(surf, p, x + 6, y + 8, 45)
surf.blit(f_tiny.render(PLANTS_DB[p]["name"][:11], True, (255, 255, 255)), (x + 55, y + 12))
btn_r = pygame.Rect(x + 8, y + 70, 144, 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(520, 640, 160, 45)
pygame.draw.rect(surf, (140, 60, 40), b_back, border_radius=8)
surf.blit(f_med.render("НАЗАД", True, (255, 255, 255)), (b_back.centerx - 30, b_back.centery - 10))
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, 20))
tab1 = pygame.Rect(100, 70, 140, 35)
tab2 = pygame.Rect(250, 70, 140, 35)
pygame.draw.rect(surf, (70, 130, 60) if self.almanac_tab == "plants" else (50, 60, 65), tab1, border_radius=4)
pygame.draw.rect(surf, (150, 80, 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))
info_r = pygame.Rect(600, 120, 520, 490)
pygame.draw.rect(surf, (45, 60, 65), info_r, border_radius=8)
if self.almanac_tab == "plants":
for idx, p in enumerate(PLANTS_DB.keys()):
x = 80 + (idx % 6) * 82
y = 125 + (idx // 6) * 82
r = pygame.Rect(x, y, 74, 74)
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)
spec = PLANTS_DB[self.almanac_selected]
draw_plant_art(surf, self.almanac_selected, 630, 150, 110)
surf.blit(f_big.render(spec["name"], True, (255, 255, 255)), (760, 160))
surf.blit(f_med.render(f"Стоимость: {spec['cost']} солнца", True, (255, 215, 0)), (760, 215))
surf.blit(f_med.render(f"Здоровье: {spec['hp']}", True, (150, 255, 150)), (760, 245))
surf.blit(f_small.render(spec["desc"], True, (220, 220, 220)), (630, 330))
elif self.almanac_tab == "zombies":
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, (160, 100, 80) if sel else (55, 70, 75), r, border_radius=6)
draw_zombie_art(surf, z, x + 15, y + 10, 60)
spec = ZOMBIES_DB[self.almanac_selected]
draw_zombie_art(surf, self.almanac_selected, 630, 150, 110)
surf.blit(f_big.render(spec["name"], True, (255, 255, 255)), (760, 160))
surf.blit(f_med.render(f"Здоровье: {spec['hp']}", True, (255, 100, 100)), (760, 215))
surf.blit(f_med.render(f"Скорость: {spec['spd']}", True, (100, 200, 255)), (760, 245))
surf.blit(f_small.render(spec["desc"], True, (220, 220, 220)), (630, 330))
b_back = pygame.Rect(520, 640, 160, 45)
pygame.draw.rect(surf, (140, 60, 40), b_back, border_radius=8)
surf.blit(f_med.render("НАЗАД", True, (255, 255, 255)), (b_back.centerx - 30, b_back.centery - 10))
return
# 5. БОЕВОЙ ГАЗОН
w_cfg = WORLDS_CONFIG[self.world_idx]
surf.fill((40, 70, 30))
for r in range(GRID_ROWS):
for c in range(GRID_COLS):
col = w_cfg["grass"] if (r + c) % 2 == 0 else w_cfg["alt"]
pygame.draw.rect(surf, col, (GRID_X + c * CELL_W, GRID_Y + r * CELL_H, CELL_W, CELL_H))
pygame.draw.rect(surf, (0, 0, 0, 35), (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)
for fo in self.food_orbs: fo.draw(surf)
for pt in self.particles: pt.draw(surf)
# Верхняя панель HUD
pygame.draw.rect(surf, (90, 60, 35), (0, 0, WIDTH, 120))
# Солнце
pygame.draw.rect(surf, (220, 200, 150), (15, 12, 80, 95), 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:
h = int(92 * (cd / PLANTS_DB[p_type]["cd"]))
cd_surf = pygame.Surface((70, h), pygame.SRCALPHA)
cd_surf.fill((0, 0, 0, 160))
surf.blit(cd_surf, (x, 14 + (92 - h)))
# Кнопка Удобрения (Plant Food)
pf_r = pygame.Rect(750, 16, 60, 65)
pygame.draw.rect(surf, (50, 160, 80) if self.selected_tool == "plant_food" else (30, 100, 50), pf_r, border_radius=6)
draw_circle_aa(surf, 780, 48, 14, (80, 255, 120))
surf.blit(f_small.render(f"x{self.plant_food}", True, (255, 255, 255)), (798, 40))
# Суперспособность Power Zap
zap_r = pygame.Rect(825, 16, 75, 65)
pygame.draw.rect(surf, (80, 160, 220) if self.selected_tool == "power_zap" else (40, 90, 140), zap_r, border_radius=6)
surf.blit(f_tiny.render("POWER ZAP", True, (255, 255, 255)), (830, 22))
surf.blit(f_tiny.render("150 $", True, (255, 220, 50)), (848, 54))
# Лопата
sh_r = pygame.Rect(915, 16, 60, 65)
pygame.draw.rect(surf, (255, 255, 120) if self.selected_tool == "shovel" else (140, 90, 50), sh_r, border_radius=6)
pygame.draw.line(surf, (150, 90, 40), (925, 66), (960, 26), 6)
# Монеты и индикатор волны
pygame.draw.rect(surf, (20, 20, 20), (WIDTH - 205, 20, 185, 35), border_radius=4)
surf.blit(f_small.render(f"Монеты: {self.coins} $", True, (255, 215, 0)), (WIDTH - 190, 28))
total_w = len(WORLDS_CONFIG[self.world_idx]["waves"])
prog = min(1.0, self.wave_idx / max(1, total_w))
pygame.draw.rect(surf, (30, 30, 30), (WIDTH - 205, 65, 185, 18), border_radius=4)
pygame.draw.rect(surf, (40, 220, 40), (WIDTH - 205, 65, int(185 * prog), 18), border_radius=4)
if self.state == "GAMEOVER":
s = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
s.fill((100, 0, 0, 200))
surf.blit(s, (0, 0))
surf.blit(f_big.render("ЗОМБИ СЪЕЛИ ВАШИ МОЗГИ!", True, (255, 80, 80)), (WIDTH // 2 - 250, HEIGHT // 2 - 30))
elif self.state == "WIN":
s = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
s.fill((20, 90, 20, 200))
surf.blit(s, (0, 0))
surf.blit(f_big.render("ЭПОХА УСПЕШНО ЗАЩИЩЕНА!", True, (240, 220, 50)), (WIDTH // 2 - 260, HEIGHT // 2 - 30))
# --- ТОЧКА ВХОДА ---
def main():
game = PVZEngine()
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:
game.state = "MENU"
elif event.key == pygame.K_r and game.state in ["GAMEOVER", "WIN"]:
game.start_battle(game.world_idx)
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()